Files
nofx/docs/plans/2026-01-17-grid-market-regime-impl.md
tinkle-community 7e96c5d0f2 Ai grid (#1344)
* feat: add AI grid trading and market regime classification

- Add GridTrader interface with PlaceLimitOrder, CancelOrder, GetOrderBook
- Implement GridTrader for all exchanges (Binance, Bybit, OKX, Bitget, Hyperliquid, Aster, Lighter)
- Add grid engine with ATR-based boundary calculation and fund distribution
- Add market regime classification documents (Chinese/English)
- Add GridConfigEditor component for frontend configuration

* fix: implement GetOpenOrders for Lighter exchange

* debug: add logging for Lighter GetActiveOrders API call

* fix: correct Lighter API response parsing for GetOpenOrders

- Changed response field from 'data' to 'orders' to match Lighter API
- Updated OrderResponse struct to match Lighter's actual field names
- Fixed field types: price/quantity as strings, is_ask for side

* feat: implement GetOpenOrders for Aster, OKX, Bitget exchanges

- Aster: uses /fapi/v3/openOrders endpoint
- OKX: uses /api/v5/trade/orders-pending and orders-algo-pending
- Bitget: uses /api/v2/mix/order/orders-pending and orders-plan-pending

* fix: address code review issues for GetOpenOrders

- Add error logging for OKX/Bitget API failures (was silently swallowed)
- Fix Lighter position side logic to handle reduce-only orders
- Change verbose debug logs from Infof to Debugf level

* fix: provide FromAccountIndex and ApiKeyIndex for Lighter nonce auto-fetch

Root cause: SDK requires these fields to fetch nonce from API, otherwise nonce gets cached/stuck

* fix: use auth query parameter instead of Authorization header for Lighter API

* test: add Lighter API authentication tests and diagnostic tools

* fix(grid): add leverage setting before order placement

CRITICAL BUG FIX:
- Call SetLeverage() in GridTraderAdapter.PlaceLimitOrder()
- Set leverage during grid initialization
- Log leverage setting results

* fix(grid): prevent CancelOrder from canceling all orders

CRITICAL BUG FIX:
- CancelOrder no longer calls CancelAllOrders
- Try exchange-specific CancelOrder if available
- Return error if individual cancellation not supported

* fix(grid): add total position value limit check

CRITICAL: Prevent excessive position accumulation
- New checkTotalPositionLimit() function
- Checks current + pending + new order value
- Rejects orders that would exceed TotalInvestment x Leverage
- Logs clear error messages when limit exceeded

* feat(grid): implement stop loss execution

CRITICAL: Add code-level stop loss protection
- New checkAndExecuteStopLoss() function
- Checks each filled level against StopLossPct
- Automatically closes positions exceeding stop loss
- Called during every grid state sync

* feat(grid): add breakout detection and auto-pause

CRITICAL: Detect price breakout from grid range
- New checkBreakout() function to detect upper/lower breakouts
- Auto-pause grid on significant breakout (>2%)
- Cancel all orders when breakout detected
- Prevent continued losses in trending market
- Minor breakouts (1-2%) logged for AI consideration

* feat(grid): enforce max drawdown limit with emergency exit

CRITICAL: Add drawdown protection
- New checkMaxDrawdown() function tracks peak equity
- emergencyExit() closes all positions and cancels orders
- Auto-pause grid when MaxDrawdownPct exceeded
- Protect capital from excessive losses

* feat(grid): enforce daily loss limit

- Add checkDailyLossLimit() function to check if daily loss exceeds limit
- Track daily PnL with auto-reset at midnight
- Pause grid when DailyLossLimitPct exceeded
- Add updateDailyPnL() helper for realized PnL tracking
- Prevent excessive single-day losses

* fix(grid): update daily PnL when stop loss is executed

The updateDailyPnL() function was added but never called, leaving
DailyPnL always at 0 and preventing daily loss limit checks from
triggering.

This fix updates DailyPnL and TotalProfit directly in checkAndExecuteStopLoss()
when a stop loss is executed. We update directly rather than calling
updateDailyPnL() because the mutex is already held in that function.

* feat(grid): add automatic grid adjustment

- New checkGridSkew() detects imbalanced grid
- autoAdjustGrid() reinitializes around current price
- Prevents grid from becoming ineffective after drift
- Triggers when one side is 3x more filled than other

* fix(grid): recalculate bounds in autoAdjustGrid before reinitializing levels

Critical fix for grid auto-adjustment:
- Recalculate grid bounds (UpperPrice, LowerPrice, GridSpacing) centered
  on current price before reinitializing grid levels
- Preserve filled positions during adjustment by saving and restoring
  them to the closest new level after reinitialization
- Hold mutex lock for the entire adjustment operation to ensure atomicity
- Add locked variants of calculateDefaultBounds, calculateATRBounds, and
  initializeGridLevels to use during adjustment

Without this fix, autoAdjustGrid was using old boundaries when creating
new grid levels, defeating the purpose of auto-adjustment when price
moved significantly.

* fix(grid): improve order state sync logic

- Don't assume missing orders are filled
- Compare position size to determine fill vs cancel
- Properly reset cancelled orders to empty state
- More accurate grid state tracking

* fix(grid): use actual PositionSize sum instead of count in syncGridState heuristic

The position-based heuristic was using `float64(previousFilledCount) * level.OrderQuantity`
which incorrectly assumed uniform order quantities. Since the grid uses weighted distribution
(gaussian, pyramid, uniform) where orders have different quantities, this could lead to
incorrect fill detection.

Now sums the actual PositionSize from filled levels for accurate comparison.
Also adds warning log when GetPositions() fails.

* docs: add grid market regime detection design

Design for enhanced market state recognition with:
- Multi-dimensional indicators (ATR, Bollinger, EMA, MACD, RSI)
- Multi-period box indicators (72/240/500 1h candles)
- 4-level ranging classification
- Breakout detection and handling
- Frontend risk control panel

* docs: add grid market regime implementation plan

20 tasks covering:
- Donchian channel calculation
- Box data types and API
- Regime classification (4 levels)
- Breakout detection and handling
- False breakout recovery
- Frontend risk panel
- AI prompt updates

* feat(market): add Donchian channel calculation

Add calculateDonchian function to compute highest high and lowest low
over a specified period. This is the foundation for box (range) detection
in the multi-period box indicator system for grid trading.

* fix(market): handle invalid period in calculateDonchian

* feat(market): add BoxData and RegimeLevel types

* 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.

* feat(store): add box and regime fields to grid models

* feat(trader): add regime classification and breakout detection

Implements Tasks 6-9 for grid market regime awareness:
- Task 6: classifyRegimeLevel with Bollinger/ATR thresholds
- Task 7: detectBoxBreakout for multi-period box breakouts
- Task 8: confirmBreakout with 3-candle confirmation logic
- Task 9: getBreakoutAction mapping breakout levels to actions

* feat(trader): integrate box breakout detection into grid cycle

- Task 10: Add checkBoxBreakout with 3-candle confirmation
- Task 11: Add checkFalseBreakoutRecovery for 50% position recovery
- Task 12: Add box/breakout/regime fields to GridState

* feat: add grid risk panel with API endpoint

- Task 13: Add GridRiskInfo type to frontend
- Task 14: Add /traders/:id/grid-risk API endpoint
- Task 15: Add GetGridRiskInfo method to AutoTrader
- Task 16: Create GridRiskPanel component with i18n

* feat(kernel): add box indicators to AI prompt

- Add BoxData field to GridContext
- Add box indicator table to both zh/en prompts
- Show breakout/warning alerts based on price position

* feat(web): integrate GridRiskPanel into TraderDashboardPage

* feat(lighter): improve API key validation and market caching

- Add API key validation status tracking
- Add market list caching to reduce API calls
- Improve logging (debug vs info levels)
- Add comprehensive integration tests
- Update trader manager and store for lighter support

* fix: remove hardcoded test wallet address

* fix(grid): improve GridRiskPanel layout and fix liquidation data

- Make panel collapsible with summary badges when collapsed
- Use compact 2-column grid layout for detailed info
- Fix auth token key (token -> auth_token)
- Only calculate liquidation distance when position exists

* fix(grid): add isRunning checks to prevent trades after Stop() is called
2026-01-19 12:07:14 +08:00

44 KiB

Grid Market Regime Detection Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Implement multi-period box indicators and 4-level ranging classification for grid trading with automatic parameter adjustment and breakout handling.

Architecture: Add Donchian channel calculation to market package, extend grid models with box/regime fields, implement breakout detection in auto_trader_grid, add risk control panel to frontend.

Tech Stack: Go (backend), React/TypeScript (frontend), GORM (database), 1-hour Kline data


Task 1: Add Donchian Channel Calculation

Files:

  • Modify: market/data.go
  • Test: market/data_test.go

Step 1: Write the failing test

Add to market/data_test.go:

func TestCalculateDonchian(t *testing.T) {
	// Create test klines with known high/low values
	klines := []Kline{
		{High: 100, Low: 90},
		{High: 105, Low: 88},
		{High: 102, Low: 92},
		{High: 108, Low: 85},
		{High: 103, Low: 91},
	}

	upper, lower := calculateDonchian(klines, 5)

	if upper != 108 {
		t.Errorf("Expected upper = 108, got %v", upper)
	}
	if lower != 85 {
		t.Errorf("Expected lower = 85, got %v", lower)
	}
}

func TestCalculateDonchian_PartialPeriod(t *testing.T) {
	klines := []Kline{
		{High: 100, Low: 90},
		{High: 105, Low: 88},
	}

	upper, lower := calculateDonchian(klines, 10)

	// Should use all available klines when period > len(klines)
	if upper != 105 {
		t.Errorf("Expected upper = 105, got %v", upper)
	}
	if lower != 88 {
		t.Errorf("Expected lower = 88, got %v", lower)
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/yida/gopro/open-nofx && go test -v ./market/... -run TestCalculateDonchian Expected: FAIL with "undefined: calculateDonchian"

Step 3: Write minimal implementation

Add to market/data.go:

// calculateDonchian calculates Donchian channel (highest high, lowest low) for given period
func calculateDonchian(klines []Kline, period int) (upper, lower float64) {
	if len(klines) == 0 {
		return 0, 0
	}

	// Use all available klines if period > len(klines)
	start := len(klines) - period
	if start < 0 {
		start = 0
	}

	upper = klines[start].High
	lower = klines[start].Low

	for i := start + 1; i < len(klines); i++ {
		if klines[i].High > upper {
			upper = klines[i].High
		}
		if klines[i].Low < lower {
			lower = klines[i].Low
		}
	}

	return upper, lower
}

// ExportCalculateDonchian exports calculateDonchian for testing
func ExportCalculateDonchian(klines []Kline, period int) (float64, float64) {
	return calculateDonchian(klines, period)
}

Step 4: Run test to verify it passes

Run: cd /Users/yida/gopro/open-nofx && go test -v ./market/... -run TestCalculateDonchian Expected: PASS

Step 5: Commit

git add market/data.go market/data_test.go
git commit -m "feat(market): add Donchian channel calculation"

Task 2: Add Box Data Types

Files:

  • Modify: market/types.go

Step 1: Add BoxData struct

Add to market/types.go:

// BoxData represents multi-period Donchian channel (box) data
type BoxData struct {
	// Short-term box (72 1h candles = 3 days)
	ShortUpper float64 `json:"short_upper"`
	ShortLower float64 `json:"short_lower"`

	// Mid-term box (240 1h candles = 10 days)
	MidUpper float64 `json:"mid_upper"`
	MidLower float64 `json:"mid_lower"`

	// Long-term box (500 1h candles = ~21 days)
	LongUpper float64 `json:"long_upper"`
	LongLower float64 `json:"long_lower"`

	// Current price position relative to boxes
	CurrentPrice float64 `json:"current_price"`
}

// RegimeLevel represents the ranging classification level
type RegimeLevel string

const (
	RegimeLevelNarrow   RegimeLevel = "narrow"   // 窄幅震荡
	RegimeLevelStandard RegimeLevel = "standard" // 标准震荡
	RegimeLevelWide     RegimeLevel = "wide"     // 宽幅震荡
	RegimeLevelVolatile RegimeLevel = "volatile" // 剧烈震荡
	RegimeLevelTrending RegimeLevel = "trending" // 趋势
)

// BreakoutLevel represents which box level has been broken
type BreakoutLevel string

const (
	BreakoutNone   BreakoutLevel = "none"
	BreakoutShort  BreakoutLevel = "short"
	BreakoutMid    BreakoutLevel = "mid"
	BreakoutLong   BreakoutLevel = "long"
)

Step 2: Commit

git add market/types.go
git commit -m "feat(market): add BoxData and RegimeLevel types"

Task 3: Add GetBoxData Function

Files:

  • Modify: market/data.go
  • Test: market/data_test.go

Step 1: Write the failing test

Add to market/data_test.go:

func TestGetBoxData(t *testing.T) {
	// This test requires mocking kline data source
	// For now, test the internal calculation logic
	klines := make([]Kline, 500)
	for i := 0; i < 500; i++ {
		// Create synthetic price data
		basePrice := 100.0
		klines[i] = Kline{
			High: basePrice + float64(i%10),
			Low:  basePrice - float64(i%10),
		}
	}

	box := calculateBoxData(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)
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/yida/gopro/open-nofx && go test -v ./market/... -run TestGetBoxData Expected: FAIL with "undefined: calculateBoxData"

Step 3: Write minimal implementation

Add to market/data.go:

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
}

// 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
}

Step 4: Run test to verify it passes

Run: cd /Users/yida/gopro/open-nofx && go test -v ./market/... -run TestGetBoxData Expected: PASS

Step 5: Commit

git add market/data.go market/data_test.go
git commit -m "feat(market): add GetBoxData for multi-period box calculation"

Task 4: Update GridConfigModel with Box Parameters

Files:

  • Modify: store/grid.go

Step 1: Add new fields to GridConfigModel

Add fields after TrendResumeThreshold in store/grid.go:

	// Box indicator periods (1h candles)
	ShortBoxPeriod int `json:"short_box_period" gorm:"default:72"`  // 3 days
	MidBoxPeriod   int `json:"mid_box_period" gorm:"default:240"`   // 10 days
	LongBoxPeriod  int `json:"long_box_period" gorm:"default:500"`  // 21 days

	// Effective leverage limits by regime level
	NarrowRegimeLeverage   int `json:"narrow_regime_leverage" gorm:"default:2"`
	StandardRegimeLeverage int `json:"standard_regime_leverage" gorm:"default:4"`
	WideRegimeLeverage     int `json:"wide_regime_leverage" gorm:"default:3"`
	VolatileRegimeLeverage int `json:"volatile_regime_leverage" gorm:"default:2"`

	// Position limits by regime level (percentage of total investment)
	NarrowRegimePositionPct   float64 `json:"narrow_regime_position_pct" gorm:"default:40"`
	StandardRegimePositionPct float64 `json:"standard_regime_position_pct" gorm:"default:70"`
	WideRegimePositionPct     float64 `json:"wide_regime_position_pct" gorm:"default:60"`
	VolatileRegimePositionPct float64 `json:"volatile_regime_position_pct" gorm:"default:40"`

Step 2: Commit

git add store/grid.go
git commit -m "feat(store): add box period and regime leverage fields to GridConfigModel"

Task 5: Update GridInstanceModel with Box State

Files:

  • Modify: store/grid.go

Step 1: Add new fields to GridInstanceModel

Add fields after ConsecutiveTrending in store/grid.go:

	// Current regime level (narrow/standard/wide/volatile/trending)
	CurrentRegimeLevel string `json:"current_regime_level" gorm:"default:standard"`

	// Box state
	ShortBoxUpper float64 `json:"short_box_upper"`
	ShortBoxLower float64 `json:"short_box_lower"`
	MidBoxUpper   float64 `json:"mid_box_upper"`
	MidBoxLower   float64 `json:"mid_box_lower"`
	LongBoxUpper  float64 `json:"long_box_upper"`
	LongBoxLower  float64 `json:"long_box_lower"`

	// Breakout state
	BreakoutLevel        string    `json:"breakout_level" gorm:"default:none"` // none/short/mid/long
	BreakoutDirection    string    `json:"breakout_direction"`                 // up/down
	BreakoutConfirmCount int       `json:"breakout_confirm_count" gorm:"default:0"`
	BreakoutStartTime    time.Time `json:"breakout_start_time"`

	// Position adjustment due to breakout
	PositionReductionPct float64 `json:"position_reduction_pct" gorm:"default:0"` // 0 = normal, 50 = reduced

Step 2: Commit

git add store/grid.go
git commit -m "feat(store): add box state and breakout fields to GridInstanceModel"

Task 6: Add Regime Level Classification

Files:

  • Create: trader/grid_regime.go
  • Test: trader/grid_regime_test.go

Step 1: Write the failing test

Create trader/grid_regime_test.go:

package trader

import (
	"nofx/market"
	"testing"
)

func TestClassifyRegimeLevel(t *testing.T) {
	tests := []struct {
		name           string
		bollingerWidth float64
		atr14Pct       float64
		expected       market.RegimeLevel
	}{
		{"narrow", 1.5, 0.8, market.RegimeLevelNarrow},
		{"standard", 2.5, 1.5, market.RegimeLevelStandard},
		{"wide", 3.5, 2.5, market.RegimeLevelWide},
		{"volatile", 5.0, 4.0, market.RegimeLevelVolatile},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := classifyRegimeLevel(tt.bollingerWidth, tt.atr14Pct)
			if result != tt.expected {
				t.Errorf("Expected %v, got %v", tt.expected, result)
			}
		})
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/yida/gopro/open-nofx && go test -v ./trader/... -run TestClassifyRegimeLevel Expected: FAIL with "undefined: classifyRegimeLevel"

Step 3: Write minimal implementation

Create trader/grid_regime.go:

package trader

import "nofx/market"

// classifyRegimeLevel determines the regime level based on market indicators
// bollingerWidth: Bollinger band width as percentage
// atr14Pct: ATR14 as percentage of current price
func classifyRegimeLevel(bollingerWidth, atr14Pct float64) market.RegimeLevel {
	// Narrow: Bollinger < 2%, ATR < 1%
	if bollingerWidth < 2.0 && atr14Pct < 1.0 {
		return market.RegimeLevelNarrow
	}

	// Standard: Bollinger 2-3%, ATR 1-2%
	if bollingerWidth <= 3.0 && atr14Pct <= 2.0 {
		return market.RegimeLevelStandard
	}

	// Wide: Bollinger 3-4%, ATR 2-3%
	if bollingerWidth <= 4.0 && atr14Pct <= 3.0 {
		return market.RegimeLevelWide
	}

	// Volatile: Bollinger > 4%, ATR > 3%
	return market.RegimeLevelVolatile
}

// getRegimeLeverageLimit returns the effective leverage limit for a regime level
func getRegimeLeverageLimit(level market.RegimeLevel, config *store.GridStrategyConfig) int {
	switch level {
	case market.RegimeLevelNarrow:
		return config.NarrowRegimeLeverage
	case market.RegimeLevelStandard:
		return config.StandardRegimeLeverage
	case market.RegimeLevelWide:
		return config.WideRegimeLeverage
	case market.RegimeLevelVolatile:
		return config.VolatileRegimeLeverage
	default:
		return 2 // Conservative default
	}
}

// getRegimePositionLimit returns the position limit percentage for a regime level
func getRegimePositionLimit(level market.RegimeLevel, config *store.GridStrategyConfig) float64 {
	switch level {
	case market.RegimeLevelNarrow:
		return config.NarrowRegimePositionPct
	case market.RegimeLevelStandard:
		return config.StandardRegimePositionPct
	case market.RegimeLevelWide:
		return config.WideRegimePositionPct
	case market.RegimeLevelVolatile:
		return config.VolatileRegimePositionPct
	default:
		return 40.0 // Conservative default
	}
}

Step 4: Run test to verify it passes

Run: cd /Users/yida/gopro/open-nofx && go test -v ./trader/... -run TestClassifyRegimeLevel Expected: PASS

Step 5: Commit

git add trader/grid_regime.go trader/grid_regime_test.go
git commit -m "feat(trader): add regime level classification"

Task 7: Add Breakout Detection

Files:

  • Modify: trader/grid_regime.go
  • Test: trader/grid_regime_test.go

Step 1: Write the failing test

Add to trader/grid_regime_test.go:

func TestDetectBoxBreakout(t *testing.T) {
	box := &market.BoxData{
		ShortUpper:   100,
		ShortLower:   90,
		MidUpper:     105,
		MidLower:     85,
		LongUpper:    110,
		LongLower:    80,
		CurrentPrice: 95,
	}

	// No breakout
	level, direction := detectBoxBreakout(box)
	if level != market.BreakoutNone {
		t.Errorf("Expected no breakout, got %v", level)
	}

	// Short breakout up
	box.CurrentPrice = 101
	level, direction = detectBoxBreakout(box)
	if level != market.BreakoutShort || direction != "up" {
		t.Errorf("Expected short breakout up, got %v %v", level, direction)
	}

	// Mid breakout down
	box.CurrentPrice = 84
	level, direction = detectBoxBreakout(box)
	if level != market.BreakoutMid || direction != "down" {
		t.Errorf("Expected mid breakout down, got %v %v", level, direction)
	}

	// Long breakout up
	box.CurrentPrice = 112
	level, direction = detectBoxBreakout(box)
	if level != market.BreakoutLong || direction != "up" {
		t.Errorf("Expected long breakout up, got %v %v", level, direction)
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/yida/gopro/open-nofx && go test -v ./trader/... -run TestDetectBoxBreakout Expected: FAIL with "undefined: detectBoxBreakout"

Step 3: Write minimal implementation

Add to trader/grid_regime.go:

// detectBoxBreakout checks if price has broken out of any box level
// Returns the highest breakout level and direction
func detectBoxBreakout(box *market.BoxData) (market.BreakoutLevel, string) {
	price := box.CurrentPrice

	// Check long box first (highest priority)
	if price > box.LongUpper {
		return market.BreakoutLong, "up"
	}
	if price < box.LongLower {
		return market.BreakoutLong, "down"
	}

	// Check mid box
	if price > box.MidUpper {
		return market.BreakoutMid, "up"
	}
	if price < box.MidLower {
		return market.BreakoutMid, "down"
	}

	// Check short box
	if price > box.ShortUpper {
		return market.BreakoutShort, "up"
	}
	if price < box.ShortLower {
		return market.BreakoutShort, "down"
	}

	return market.BreakoutNone, ""
}

Step 4: Run test to verify it passes

Run: cd /Users/yida/gopro/open-nofx && go test -v ./trader/... -run TestDetectBoxBreakout Expected: PASS

Step 5: Commit

git add trader/grid_regime.go trader/grid_regime_test.go
git commit -m "feat(trader): add box breakout detection"

Task 8: Add Breakout Confirmation Logic

Files:

  • Modify: trader/grid_regime.go
  • Test: trader/grid_regime_test.go

Step 1: Write the failing test

Add to trader/grid_regime_test.go:

func TestBreakoutConfirmation(t *testing.T) {
	state := &BreakoutState{
		Level:        market.BreakoutShort,
		Direction:    "up",
		ConfirmCount: 0,
	}

	// First confirmation
	confirmed := confirmBreakout(state, market.BreakoutShort, "up")
	if confirmed || state.ConfirmCount != 1 {
		t.Errorf("Expected not confirmed, count=1, got confirmed=%v count=%d", confirmed, state.ConfirmCount)
	}

	// Second confirmation
	confirmed = confirmBreakout(state, market.BreakoutShort, "up")
	if confirmed || state.ConfirmCount != 2 {
		t.Errorf("Expected not confirmed, count=2, got confirmed=%v count=%d", confirmed, state.ConfirmCount)
	}

	// Third confirmation - should confirm
	confirmed = confirmBreakout(state, market.BreakoutShort, "up")
	if !confirmed || state.ConfirmCount != 3 {
		t.Errorf("Expected confirmed, count=3, got confirmed=%v count=%d", confirmed, state.ConfirmCount)
	}

	// Reset on price return
	state.ConfirmCount = 2
	confirmed = confirmBreakout(state, market.BreakoutNone, "")
	if state.ConfirmCount != 0 {
		t.Errorf("Expected count reset to 0, got %d", state.ConfirmCount)
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/yida/gopro/open-nofx && go test -v ./trader/... -run TestBreakoutConfirmation Expected: FAIL with "undefined: BreakoutState"

Step 3: Write minimal implementation

Add to trader/grid_regime.go:

const BreakoutConfirmRequired = 3 // 3 candles to confirm breakout

// BreakoutState tracks the current breakout state
type BreakoutState struct {
	Level        market.BreakoutLevel
	Direction    string
	ConfirmCount int
	StartTime    time.Time
}

// confirmBreakout updates breakout state and returns true if breakout is confirmed
func confirmBreakout(state *BreakoutState, currentLevel market.BreakoutLevel, direction string) bool {
	// If price returned to box, reset state
	if currentLevel == market.BreakoutNone {
		state.ConfirmCount = 0
		state.Level = market.BreakoutNone
		state.Direction = ""
		return false
	}

	// If same breakout continues, increment count
	if state.Level == currentLevel && state.Direction == direction {
		state.ConfirmCount++
	} else {
		// New breakout, reset count
		state.Level = currentLevel
		state.Direction = direction
		state.ConfirmCount = 1
		state.StartTime = time.Now()
	}

	return state.ConfirmCount >= BreakoutConfirmRequired
}

Step 4: Run test to verify it passes

Run: cd /Users/yida/gopro/open-nofx && go test -v ./trader/... -run TestBreakoutConfirmation Expected: PASS

Step 5: Commit

git add trader/grid_regime.go trader/grid_regime_test.go
git commit -m "feat(trader): add breakout confirmation logic"

Task 9: Add Breakout Handler

Files:

  • Modify: trader/grid_regime.go
  • Test: trader/grid_regime_test.go

Step 1: Write the failing test

Add to trader/grid_regime_test.go:

func TestGetBreakoutAction(t *testing.T) {
	tests := []struct {
		level    market.BreakoutLevel
		expected BreakoutAction
	}{
		{market.BreakoutNone, BreakoutActionNone},
		{market.BreakoutShort, BreakoutActionReducePosition},
		{market.BreakoutMid, BreakoutActionPauseGrid},
		{market.BreakoutLong, BreakoutActionCloseAll},
	}

	for _, tt := range tests {
		t.Run(string(tt.level), func(t *testing.T) {
			action := getBreakoutAction(tt.level)
			if action != tt.expected {
				t.Errorf("Expected %v, got %v", tt.expected, action)
			}
		})
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/yida/gopro/open-nofx && go test -v ./trader/... -run TestGetBreakoutAction Expected: FAIL with "undefined: BreakoutAction"

Step 3: Write minimal implementation

Add to trader/grid_regime.go:

// BreakoutAction represents the action to take on breakout
type BreakoutAction int

const (
	BreakoutActionNone BreakoutAction = iota
	BreakoutActionReducePosition // Short box breakout: reduce to 50%
	BreakoutActionPauseGrid      // Mid box breakout: pause grid + cancel orders
	BreakoutActionCloseAll       // Long box breakout: pause + cancel + close all
)

// getBreakoutAction returns the appropriate action for a breakout level
func getBreakoutAction(level market.BreakoutLevel) BreakoutAction {
	switch level {
	case market.BreakoutShort:
		return BreakoutActionReducePosition
	case market.BreakoutMid:
		return BreakoutActionPauseGrid
	case market.BreakoutLong:
		return BreakoutActionCloseAll
	default:
		return BreakoutActionNone
	}
}

Step 4: Run test to verify it passes

Run: cd /Users/yida/gopro/open-nofx && go test -v ./trader/... -run TestGetBreakoutAction Expected: PASS

Step 5: Commit

git add trader/grid_regime.go trader/grid_regime_test.go
git commit -m "feat(trader): add breakout action handler"

Task 10: Integrate Breakout Detection into Grid Cycle

Files:

  • Modify: trader/auto_trader_grid.go

Step 1: Add checkBoxBreakout method

Add to trader/auto_trader_grid.go after checkBreakout function:

// checkBoxBreakout checks for multi-period box breakouts and takes appropriate action
func (at *AutoTrader) checkBoxBreakout() error {
	gridConfig := at.config.StrategyConfig.GridConfig
	if gridConfig == nil {
		return nil
	}

	// Get box data
	box, err := market.GetBoxData(gridConfig.Symbol)
	if err != nil {
		logger.Infof("Failed to get box data: %v", err)
		return nil // Non-fatal, continue with other checks
	}

	// Update instance with box values
	at.gridState.mu.Lock()
	// Store box values in grid state for reference
	at.gridState.mu.Unlock()

	// Detect breakout
	breakoutLevel, direction := detectBoxBreakout(box)

	// Get current breakout state from instance
	state := &BreakoutState{
		Level:        market.BreakoutLevel(at.gridState.BreakoutLevel),
		Direction:    at.gridState.BreakoutDirection,
		ConfirmCount: at.gridState.BreakoutConfirmCount,
	}

	// Check if breakout is confirmed (3 candles)
	confirmed := confirmBreakout(state, breakoutLevel, direction)

	// Update grid state
	at.gridState.mu.Lock()
	at.gridState.BreakoutLevel = string(state.Level)
	at.gridState.BreakoutDirection = state.Direction
	at.gridState.BreakoutConfirmCount = state.ConfirmCount
	at.gridState.mu.Unlock()

	if !confirmed {
		return nil
	}

	// Take action based on breakout level
	action := getBreakoutAction(breakoutLevel)
	return at.executeBreakoutAction(action)
}

// executeBreakoutAction executes the appropriate action for a breakout
func (at *AutoTrader) executeBreakoutAction(action BreakoutAction) error {
	gridConfig := at.config.StrategyConfig.GridConfig

	switch action {
	case BreakoutActionReducePosition:
		// Short box breakout: reduce position to 50%
		logger.Infof("Short box breakout confirmed, reducing position to 50%%")
		at.gridState.mu.Lock()
		at.gridState.PositionReductionPct = 50
		at.gridState.mu.Unlock()
		return nil

	case BreakoutActionPauseGrid:
		// Mid box breakout: pause grid + cancel orders
		logger.Infof("Mid box breakout confirmed, pausing grid and canceling orders")
		at.gridState.mu.Lock()
		at.gridState.IsPaused = true
		at.gridState.mu.Unlock()
		return at.cancelAllGridOrders()

	case BreakoutActionCloseAll:
		// Long box breakout: pause + cancel + close all
		logger.Infof("Long box breakout confirmed, closing all positions")
		at.gridState.mu.Lock()
		at.gridState.IsPaused = true
		at.gridState.mu.Unlock()
		if err := at.cancelAllGridOrders(); err != nil {
			logger.Infof("Failed to cancel orders: %v", err)
		}
		return at.closeAllPositions()
	}

	return nil
}

// closeAllPositions closes all open positions
func (at *AutoTrader) closeAllPositions() error {
	gridConfig := at.config.StrategyConfig.GridConfig

	positions, err := at.trader.GetPositions()
	if err != nil {
		return fmt.Errorf("failed to get positions: %w", err)
	}

	for _, pos := range positions {
		symbol, _ := pos["symbol"].(string)
		if symbol != gridConfig.Symbol {
			continue
		}

		size, _ := pos["positionAmt"].(float64)
		if size == 0 {
			continue
		}

		if size > 0 {
			_, err = at.trader.CloseLong(symbol, size)
		} else {
			_, err = at.trader.CloseShort(symbol, -size)
		}
		if err != nil {
			logger.Infof("Failed to close position: %v", err)
		}
	}

	return nil
}

Step 2: Add checkBoxBreakout call to RunGridCycle

In RunGridCycle, add after existing breakout check:

	// Check multi-period box breakout
	if err := at.checkBoxBreakout(); err != nil {
		logger.Infof("Box breakout check error: %v", err)
	}

Step 3: Commit

git add trader/auto_trader_grid.go
git commit -m "feat(trader): integrate box breakout detection into grid cycle"

Task 11: Add False Breakout Recovery

Files:

  • Modify: trader/auto_trader_grid.go

Step 1: Add recovery logic

Add to trader/auto_trader_grid.go:

// checkFalseBreakoutRecovery checks if price has returned to box after breakout
func (at *AutoTrader) checkFalseBreakoutRecovery() error {
	gridConfig := at.config.StrategyConfig.GridConfig
	if gridConfig == nil {
		return nil
	}

	at.gridState.mu.RLock()
	breakoutLevel := at.gridState.BreakoutLevel
	isPaused := at.gridState.IsPaused
	positionReduction := at.gridState.PositionReductionPct
	at.gridState.mu.RUnlock()

	// Only check if we had a breakout
	if breakoutLevel == string(market.BreakoutNone) && positionReduction == 0 && !isPaused {
		return nil
	}

	// Get current box data
	box, err := market.GetBoxData(gridConfig.Symbol)
	if err != nil {
		return nil
	}

	// Check if price is back inside the long box
	if box.CurrentPrice >= box.LongLower && box.CurrentPrice <= box.LongUpper {
		logger.Infof("Price returned to box, recovering with 50%% position")

		at.gridState.mu.Lock()
		at.gridState.BreakoutLevel = string(market.BreakoutNone)
		at.gridState.BreakoutDirection = ""
		at.gridState.BreakoutConfirmCount = 0
		at.gridState.PositionReductionPct = 50 // Recover at 50%
		at.gridState.IsPaused = false
		at.gridState.mu.Unlock()
	}

	return nil
}

Step 2: Add call in RunGridCycle

	// Check for false breakout recovery
	if err := at.checkFalseBreakoutRecovery(); err != nil {
		logger.Infof("False breakout recovery check error: %v", err)
	}

Step 3: Commit

git add trader/auto_trader_grid.go
git commit -m "feat(trader): add false breakout recovery logic"

Task 12: Update GridState with Box Fields

Files:

  • Modify: trader/auto_trader_grid.go

Step 1: Add box fields to GridState struct

Add to GridState struct in trader/auto_trader_grid.go:

	// Box state
	ShortBoxUpper float64
	ShortBoxLower float64
	MidBoxUpper   float64
	MidBoxLower   float64
	LongBoxUpper  float64
	LongBoxLower  float64

	// Breakout state
	BreakoutLevel        string
	BreakoutDirection    string
	BreakoutConfirmCount int

	// Position reduction (0 = normal, 50 = reduced after false breakout)
	PositionReductionPct float64

	// Current regime level
	CurrentRegimeLevel string

Step 2: Commit

git add trader/auto_trader_grid.go
git commit -m "feat(trader): add box and regime fields to GridState"

Task 13: Add Frontend Types

Files:

  • Modify: web/src/types.ts (or equivalent types file)

Step 1: Add grid risk info types

Add to types file:

export interface GridRiskInfo {
  // Leverage info
  currentLeverage: number
  effectiveLeverage: number
  recommendedLeverage: number

  // Position info
  currentPosition: number
  maxPosition: number
  positionPercent: number

  // Liquidation info
  liquidationPrice: number
  liquidationDistance: number // percentage

  // Market state
  regimeLevel: 'narrow' | 'standard' | 'wide' | 'volatile' | 'trending'

  // Box state
  shortBoxUpper: number
  shortBoxLower: number
  midBoxUpper: number
  midBoxLower: number
  longBoxUpper: number
  longBoxLower: number
  currentPrice: number

  // Breakout state
  breakoutLevel: 'none' | 'short' | 'mid' | 'long'
  breakoutDirection: 'up' | 'down' | ''
}

Step 2: Commit

git add web/src/types.ts
git commit -m "feat(web): add GridRiskInfo type"

Task 14: Add API Endpoint for Risk Info

Files:

  • Modify: api/server.go

Step 1: Add handler function

Add to api/server.go:

// handleGetGridRiskInfo returns current risk information for a grid trader
func (s *Server) handleGetGridRiskInfo(c *gin.Context) {
	traderID := c.Param("id")

	trader, err := s.manager.GetTrader(traderID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "trader not found"})
		return
	}

	autoTrader, ok := trader.(*trader.AutoTrader)
	if !ok {
		c.JSON(http.StatusBadRequest, gin.H{"error": "not an auto trader"})
		return
	}

	riskInfo := autoTrader.GetGridRiskInfo()
	c.JSON(http.StatusOK, riskInfo)
}

Step 2: Add route

Add route in setupRoutes:

	api.GET("/traders/:id/grid-risk", s.handleGetGridRiskInfo)

Step 3: Commit

git add api/server.go
git commit -m "feat(api): add grid risk info endpoint"

Task 15: Add GetGridRiskInfo Method to AutoTrader

Files:

  • Modify: trader/auto_trader_grid.go

Step 1: Add method

Add to trader/auto_trader_grid.go:

// GridRiskInfo contains risk information for frontend display
type GridRiskInfo struct {
	CurrentLeverage     int     `json:"current_leverage"`
	EffectiveLeverage   float64 `json:"effective_leverage"`
	RecommendedLeverage int     `json:"recommended_leverage"`

	CurrentPosition  float64 `json:"current_position"`
	MaxPosition      float64 `json:"max_position"`
	PositionPercent  float64 `json:"position_percent"`

	LiquidationPrice    float64 `json:"liquidation_price"`
	LiquidationDistance float64 `json:"liquidation_distance"`

	RegimeLevel string `json:"regime_level"`

	ShortBoxUpper float64 `json:"short_box_upper"`
	ShortBoxLower float64 `json:"short_box_lower"`
	MidBoxUpper   float64 `json:"mid_box_upper"`
	MidBoxLower   float64 `json:"mid_box_lower"`
	LongBoxUpper  float64 `json:"long_box_upper"`
	LongBoxLower  float64 `json:"long_box_lower"`
	CurrentPrice  float64 `json:"current_price"`

	BreakoutLevel     string `json:"breakout_level"`
	BreakoutDirection string `json:"breakout_direction"`
}

// GetGridRiskInfo returns current risk information
func (at *AutoTrader) GetGridRiskInfo() *GridRiskInfo {
	gridConfig := at.config.StrategyConfig.GridConfig
	if gridConfig == nil {
		return &GridRiskInfo{}
	}

	at.gridState.mu.RLock()
	defer at.gridState.mu.RUnlock()

	// Get current price
	currentPrice, _ := at.trader.GetMarketPrice(gridConfig.Symbol)

	// Calculate effective leverage
	totalInvestment := gridConfig.TotalInvestment
	leverage := gridConfig.Leverage

	// Get current position value
	positions, _ := at.trader.GetPositions()
	var currentPositionValue float64
	for _, pos := range positions {
		if sym, _ := pos["symbol"].(string); sym == gridConfig.Symbol {
			size, _ := pos["positionAmt"].(float64)
			entry, _ := pos["entryPrice"].(float64)
			currentPositionValue = math.Abs(size * entry)
			break
		}
	}

	effectiveLeverage := currentPositionValue / totalInvestment

	// Calculate max position based on regime
	regimeLevel := market.RegimeLevel(at.gridState.CurrentRegimeLevel)
	maxPositionPct := getRegimePositionLimit(regimeLevel, gridConfig)
	maxPosition := totalInvestment * maxPositionPct / 100 * float64(leverage)
	recommendedLeverage := getRegimeLeverageLimit(regimeLevel, gridConfig)

	// Calculate liquidation distance
	liquidationDistance := 100.0 / float64(leverage) * 0.9 // ~90% of theoretical max

	var liquidationPrice float64
	if currentPositionValue > 0 {
		liquidationPrice = currentPrice * (1 - liquidationDistance/100)
	}

	return &GridRiskInfo{
		CurrentLeverage:     leverage,
		EffectiveLeverage:   effectiveLeverage,
		RecommendedLeverage: recommendedLeverage,

		CurrentPosition:  currentPositionValue,
		MaxPosition:      maxPosition,
		PositionPercent:  currentPositionValue / maxPosition * 100,

		LiquidationPrice:    liquidationPrice,
		LiquidationDistance: liquidationDistance,

		RegimeLevel: at.gridState.CurrentRegimeLevel,

		ShortBoxUpper: at.gridState.ShortBoxUpper,
		ShortBoxLower: at.gridState.ShortBoxLower,
		MidBoxUpper:   at.gridState.MidBoxUpper,
		MidBoxLower:   at.gridState.MidBoxLower,
		LongBoxUpper:  at.gridState.LongBoxUpper,
		LongBoxLower:  at.gridState.LongBoxLower,
		CurrentPrice:  currentPrice,

		BreakoutLevel:     at.gridState.BreakoutLevel,
		BreakoutDirection: at.gridState.BreakoutDirection,
	}
}

Step 2: Commit

git add trader/auto_trader_grid.go
git commit -m "feat(trader): add GetGridRiskInfo method"

Task 16: Create GridRiskPanel Component

Files:

  • Create: web/src/components/strategy/GridRiskPanel.tsx

Step 1: Create component

Create web/src/components/strategy/GridRiskPanel.tsx:

import { useState, useEffect } from 'react'
import { AlertTriangle, TrendingUp, Shield, Box } from 'lucide-react'

interface GridRiskInfo {
  currentLeverage: number
  effectiveLeverage: number
  recommendedLeverage: number
  currentPosition: number
  maxPosition: number
  positionPercent: number
  liquidationPrice: number
  liquidationDistance: number
  regimeLevel: string
  shortBoxUpper: number
  shortBoxLower: number
  midBoxUpper: number
  midBoxLower: number
  longBoxUpper: number
  longBoxLower: number
  currentPrice: number
  breakoutLevel: string
  breakoutDirection: string
}

interface GridRiskPanelProps {
  traderId: string
  language: string
}

export function GridRiskPanel({ traderId, language }: GridRiskPanelProps) {
  const [riskInfo, setRiskInfo] = useState<GridRiskInfo | null>(null)
  const [loading, setLoading] = useState(true)

  const t = (key: string) => {
    const translations: Record<string, Record<string, string>> = {
      leverageInfo: { zh: '杠杆信息', en: 'Leverage Info' },
      currentLeverage: { zh: '当前杠杆', en: 'Current Leverage' },
      effectiveLeverage: { zh: '有效杠杆', en: 'Effective Leverage' },
      recommendedLeverage: { zh: '推荐杠杆', en: 'Recommended Leverage' },
      positionInfo: { zh: '仓位信息', en: 'Position Info' },
      currentPosition: { zh: '当前仓位', en: 'Current Position' },
      maxPosition: { zh: '最大仓位', en: 'Max Position' },
      liquidationInfo: { zh: '爆仓信息', en: 'Liquidation Info' },
      liquidationPrice: { zh: '爆仓价格', en: 'Liquidation Price' },
      liquidationDistance: { zh: '爆仓距离', en: 'Distance' },
      marketState: { zh: '市场状态', en: 'Market State' },
      regimeLevel: { zh: '震荡级别', en: 'Regime Level' },
      boxState: { zh: '箱体状态', en: 'Box State' },
      shortBox: { zh: '短期箱体', en: 'Short Box' },
      midBox: { zh: '中期箱体', en: 'Mid Box' },
      longBox: { zh: '长期箱体', en: 'Long Box' },
      narrow: { zh: '窄幅震荡', en: 'Narrow' },
      standard: { zh: '标准震荡', en: 'Standard' },
      wide: { zh: '宽幅震荡', en: 'Wide' },
      volatile: { zh: '剧烈震荡', en: 'Volatile' },
      trending: { zh: '趋势', en: 'Trending' },
      breakout: { zh: '突破', en: 'Breakout' },
      none: { zh: '无', en: 'None' },
    }
    return translations[key]?.[language] || key
  }

  useEffect(() => {
    const fetchRiskInfo = async () => {
      try {
        const res = await fetch(`/api/traders/${traderId}/grid-risk`)
        if (res.ok) {
          const data = await res.json()
          setRiskInfo(data)
        }
      } catch (err) {
        console.error('Failed to fetch risk info:', err)
      } finally {
        setLoading(false)
      }
    }

    fetchRiskInfo()
    const interval = setInterval(fetchRiskInfo, 10000) // Update every 10s
    return () => clearInterval(interval)
  }, [traderId])

  if (loading || !riskInfo) {
    return <div className="animate-pulse bg-gray-800 h-48 rounded" />
  }

  const getRegimeColor = (level: string) => {
    switch (level) {
      case 'narrow': return 'text-green-400'
      case 'standard': return 'text-blue-400'
      case 'wide': return 'text-yellow-400'
      case 'volatile': return 'text-orange-400'
      case 'trending': return 'text-red-400'
      default: return 'text-gray-400'
    }
  }

  return (
    <div className="bg-[#0B0E11] rounded-lg p-4 space-y-4">
      {/* Leverage Info */}
      <div className="border-b border-gray-700 pb-3">
        <h3 className="text-sm font-medium text-gray-400 flex items-center gap-2 mb-2">
          <TrendingUp size={14} />
          {t('leverageInfo')}
        </h3>
        <div className="grid grid-cols-3 gap-2 text-sm">
          <div>
            <div className="text-gray-500">{t('currentLeverage')}</div>
            <div className="text-white">{riskInfo.currentLeverage}x</div>
          </div>
          <div>
            <div className="text-gray-500">{t('effectiveLeverage')}</div>
            <div className="text-white">{riskInfo.effectiveLeverage.toFixed(2)}x</div>
          </div>
          <div>
            <div className="text-gray-500">{t('recommendedLeverage')}</div>
            <div className="text-green-400">{riskInfo.recommendedLeverage}x</div>
          </div>
        </div>
      </div>

      {/* Position Info */}
      <div className="border-b border-gray-700 pb-3">
        <h3 className="text-sm font-medium text-gray-400 flex items-center gap-2 mb-2">
          <Shield size={14} />
          {t('positionInfo')}
        </h3>
        <div className="grid grid-cols-2 gap-2 text-sm">
          <div>
            <div className="text-gray-500">{t('currentPosition')}</div>
            <div className="text-white">${riskInfo.currentPosition.toFixed(2)}</div>
          </div>
          <div>
            <div className="text-gray-500">{t('maxPosition')}</div>
            <div className="text-white">${riskInfo.maxPosition.toFixed(2)}</div>
          </div>
        </div>
        <div className="mt-2 bg-gray-800 rounded h-2">
          <div
            className="bg-blue-500 h-full rounded"
            style={{ width: `${Math.min(riskInfo.positionPercent, 100)}%` }}
          />
        </div>
      </div>

      {/* Liquidation Info */}
      <div className="border-b border-gray-700 pb-3">
        <h3 className="text-sm font-medium text-gray-400 flex items-center gap-2 mb-2">
          <AlertTriangle size={14} />
          {t('liquidationInfo')}
        </h3>
        <div className="grid grid-cols-2 gap-2 text-sm">
          <div>
            <div className="text-gray-500">{t('liquidationPrice')}</div>
            <div className="text-red-400">${riskInfo.liquidationPrice.toFixed(2)}</div>
          </div>
          <div>
            <div className="text-gray-500">{t('liquidationDistance')}</div>
            <div className="text-white">{riskInfo.liquidationDistance.toFixed(1)}%</div>
          </div>
        </div>
      </div>

      {/* Market State */}
      <div className="border-b border-gray-700 pb-3">
        <h3 className="text-sm font-medium text-gray-400 flex items-center gap-2 mb-2">
          <Box size={14} />
          {t('marketState')}
        </h3>
        <div className="flex items-center gap-4">
          <div>
            <div className="text-gray-500 text-sm">{t('regimeLevel')}</div>
            <div className={`font-medium ${getRegimeColor(riskInfo.regimeLevel)}`}>
              {t(riskInfo.regimeLevel)}
            </div>
          </div>
          {riskInfo.breakoutLevel !== 'none' && (
            <div className="text-red-400">
              {t('breakout')}: {riskInfo.breakoutLevel} ({riskInfo.breakoutDirection})
            </div>
          )}
        </div>
      </div>

      {/* Box State */}
      <div>
        <h3 className="text-sm font-medium text-gray-400 mb-2">{t('boxState')}</h3>
        <div className="text-xs space-y-1">
          <div className="flex justify-between">
            <span className="text-gray-500">{t('shortBox')}</span>
            <span className="text-white">{riskInfo.shortBoxLower.toFixed(2)} - {riskInfo.shortBoxUpper.toFixed(2)}</span>
          </div>
          <div className="flex justify-between">
            <span className="text-gray-500">{t('midBox')}</span>
            <span className="text-white">{riskInfo.midBoxLower.toFixed(2)} - {riskInfo.midBoxUpper.toFixed(2)}</span>
          </div>
          <div className="flex justify-between">
            <span className="text-gray-500">{t('longBox')}</span>
            <span className="text-white">{riskInfo.longBoxLower.toFixed(2)} - {riskInfo.longBoxUpper.toFixed(2)}</span>
          </div>
          <div className="flex justify-between font-medium">
            <span className="text-gray-400">Current Price</span>
            <span className="text-yellow-400">${riskInfo.currentPrice.toFixed(2)}</span>
          </div>
        </div>
      </div>
    </div>
  )
}

Step 2: Commit

git add web/src/components/strategy/GridRiskPanel.tsx
git commit -m "feat(web): add GridRiskPanel component"

Task 17: Update AI Prompt with Box Indicators

Files:

  • Modify: kernel/grid_engine.go

Step 1: Update BuildGridUserPrompt to include box data

Add box data section to the prompt in kernel/grid_engine.go:

// In BuildGridUserPrompt function, add after market data section:

	// Box Indicator Section
	if gridCtx.BoxData != nil {
		sb.WriteString("\n## Box Indicators (Donchian Channels)\n\n")
		sb.WriteString("| Box Level | Upper | Lower | Width |\n")
		sb.WriteString("|-----------|-------|-------|-------|\n")

		shortWidth := (gridCtx.BoxData.ShortUpper - gridCtx.BoxData.ShortLower) / gridCtx.BoxData.CurrentPrice * 100
		midWidth := (gridCtx.BoxData.MidUpper - gridCtx.BoxData.MidLower) / gridCtx.BoxData.CurrentPrice * 100
		longWidth := (gridCtx.BoxData.LongUpper - gridCtx.BoxData.LongLower) / gridCtx.BoxData.CurrentPrice * 100

		sb.WriteString(fmt.Sprintf("| Short (3d) | %.2f | %.2f | %.2f%% |\n",
			gridCtx.BoxData.ShortUpper, gridCtx.BoxData.ShortLower, shortWidth))
		sb.WriteString(fmt.Sprintf("| Mid (10d) | %.2f | %.2f | %.2f%% |\n",
			gridCtx.BoxData.MidUpper, gridCtx.BoxData.MidLower, midWidth))
		sb.WriteString(fmt.Sprintf("| Long (21d) | %.2f | %.2f | %.2f%% |\n",
			gridCtx.BoxData.LongUpper, gridCtx.BoxData.LongLower, longWidth))

		// Price position
		sb.WriteString(fmt.Sprintf("\nCurrent Price: %.2f\n", gridCtx.BoxData.CurrentPrice))

		// Check position relative to boxes
		price := gridCtx.BoxData.CurrentPrice
		if price > gridCtx.BoxData.LongUpper || price < gridCtx.BoxData.LongLower {
			sb.WriteString("⚠️ BREAKOUT: Price outside long-term box!\n")
		} else if price > gridCtx.BoxData.MidUpper || price < gridCtx.BoxData.MidLower {
			sb.WriteString("⚠️ WARNING: Price approaching long-term box boundary\n")
		}
	}

Step 2: Update GridContext struct

Add BoxData field to GridContext:

type GridContext struct {
	// ... existing fields ...

	// Box data
	BoxData *market.BoxData
}

Step 3: Commit

git add kernel/grid_engine.go
git commit -m "feat(kernel): add box indicators to AI prompt"

Task 18: Database Migration

Files:

  • Modify: store/grid.go

Step 1: Update InitGridSchema to migrate new fields

The GORM AutoMigrate will handle adding new columns. Verify by running:

cd /Users/yida/gopro/open-nofx && go run . migrate

Step 2: Commit

git add store/grid.go
git commit -m "chore(store): ensure new grid fields are migrated"

Task 19: Run All Tests

Step 1: Run backend tests

cd /Users/yida/gopro/open-nofx && go test -v ./...

Step 2: Run frontend tests (if available)

cd /Users/yida/gopro/open-nofx/web && npm test

Step 3: Fix any failing tests and commit

git add .
git commit -m "test: fix tests for grid regime implementation"

Task 20: Final Integration Test

Step 1: Start the server

cd /Users/yida/gopro/open-nofx && go run .

Step 2: Verify API endpoint

curl http://localhost:8080/api/traders/<trader-id>/grid-risk

Step 3: Verify frontend displays risk panel

Open browser and check grid trading page shows risk panel.

Step 4: Final commit

git add .
git commit -m "feat: complete grid market regime detection implementation"

Summary

Task Description Files
1 Donchian calculation market/data.go
2 Box data types market/types.go
3 GetBoxData function market/data.go
4 GridConfigModel fields store/grid.go
5 GridInstanceModel fields store/grid.go
6 Regime classification trader/grid_regime.go
7 Breakout detection trader/grid_regime.go
8 Breakout confirmation trader/grid_regime.go
9 Breakout handler trader/grid_regime.go
10 Grid cycle integration trader/auto_trader_grid.go
11 False breakout recovery trader/auto_trader_grid.go
12 GridState fields trader/auto_trader_grid.go
13 Frontend types web/src/types.ts
14 API endpoint api/server.go
15 GetGridRiskInfo method trader/auto_trader_grid.go
16 GridRiskPanel component web/src/components/
17 AI prompt update kernel/grid_engine.go
18 Database migration store/grid.go
19 Run all tests -
20 Integration test -