From bd8cc9c176effe5151b0110f6ff716ab1438a0a8 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Sat, 17 Jan 2026 21:47:07 +0800 Subject: [PATCH] feat(market): add GetBoxData for multi-period box calculation Adds calculateBoxData internal function and GetBoxData public API that fetches 1h klines and computes three Donchian box levels (short/mid/long). This will be used by the grid trading system to detect market regime. --- market/data.go | 56 +++++++++++++++++++++++++++++++++++++++++++++ market/data_test.go | 28 +++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/market/data.go b/market/data.go index 4f4fa76e..a993736a 100644 --- a/market/data.go +++ b/market/data.go @@ -1242,3 +1242,59 @@ func calculateDonchian(klines []Kline, period int) (upper, lower float64) { func ExportCalculateDonchian(klines []Kline, period int) (float64, float64) { return calculateDonchian(klines, period) } + +// Box period constants (in 1h candles) +const ( + ShortBoxPeriod = 72 // 3 days of 1h candles + MidBoxPeriod = 240 // 10 days of 1h candles + LongBoxPeriod = 500 // ~21 days of 1h candles +) + +// calculateBoxData calculates multi-period box data from klines +func calculateBoxData(klines []Kline, currentPrice float64) *BoxData { + box := &BoxData{ + CurrentPrice: currentPrice, + } + + if len(klines) == 0 { + return box + } + + box.ShortUpper, box.ShortLower = calculateDonchian(klines, ShortBoxPeriod) + box.MidUpper, box.MidLower = calculateDonchian(klines, MidBoxPeriod) + box.LongUpper, box.LongLower = calculateDonchian(klines, LongBoxPeriod) + + return box +} + +// ExportCalculateBoxData exports calculateBoxData for testing +func ExportCalculateBoxData(klines []Kline, currentPrice float64) *BoxData { + return calculateBoxData(klines, currentPrice) +} + +// GetBoxData fetches 1h klines and calculates box data for a symbol +func GetBoxData(symbol string) (*BoxData, error) { + symbol = Normalize(symbol) + + // Fetch 500 1h klines + var klines []Kline + var err error + + if IsXyzDexAsset(symbol) { + klines, err = getKlinesFromHyperliquid(symbol, "1h", LongBoxPeriod) + } else { + klines, err = getKlinesFromCoinAnk(symbol, "1h", LongBoxPeriod) + } + + if err != nil { + return nil, fmt.Errorf("failed to get 1h klines: %w", err) + } + + if len(klines) == 0 { + return nil, fmt.Errorf("no kline data available") + } + + currentPrice := klines[len(klines)-1].Close + + return calculateBoxData(klines, currentPrice), nil +} diff --git a/market/data_test.go b/market/data_test.go index 17142167..231c4806 100644 --- a/market/data_test.go +++ b/market/data_test.go @@ -555,3 +555,31 @@ func TestCalculateDonchian_InvalidPeriod(t *testing.T) { t.Errorf("Expected (0, 0) for negative period, got (%v, %v)", upper, lower) } } + +func TestCalculateBoxData(t *testing.T) { + // Create synthetic kline data + klines := make([]Kline, 500) + for i := 0; i < 500; i++ { + basePrice := 100.0 + klines[i] = Kline{ + High: basePrice + float64(i%10), + Low: basePrice - float64(i%10), + Close: basePrice, + } + } + + box := ExportCalculateBoxData(klines, 100.0) + + if box.ShortUpper == 0 || box.ShortLower == 0 { + t.Error("Short box should not be zero") + } + if box.MidUpper == 0 || box.MidLower == 0 { + t.Error("Mid box should not be zero") + } + if box.LongUpper == 0 || box.LongLower == 0 { + t.Error("Long box should not be zero") + } + if box.CurrentPrice != 100.0 { + t.Errorf("Expected CurrentPrice = 100.0, got %v", box.CurrentPrice) + } +}