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

1656 lines
44 KiB
Markdown

# 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`:
```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`:
```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**
```bash
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`:
```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**
```bash
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`:
```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`:
```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**
```bash
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`:
```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**
```bash
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`:
```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**
```bash
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`:
```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`:
```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**
```bash
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`:
```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`:
```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**
```bash
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`:
```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`:
```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**
```bash
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`:
```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`:
```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**
```bash
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:
```go
// 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:
```go
// Check multi-period box breakout
if err := at.checkBoxBreakout(); err != nil {
logger.Infof("Box breakout check error: %v", err)
}
```
**Step 3: Commit**
```bash
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`:
```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**
```go
// Check for false breakout recovery
if err := at.checkFalseBreakoutRecovery(); err != nil {
logger.Infof("False breakout recovery check error: %v", err)
}
```
**Step 3: Commit**
```bash
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`:
```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**
```bash
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:
```typescript
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**
```bash
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`:
```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`:
```go
api.GET("/traders/:id/grid-risk", s.handleGetGridRiskInfo)
```
**Step 3: Commit**
```bash
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`:
```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**
```bash
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`:
```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**
```bash
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`:
```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:
```go
type GridContext struct {
// ... existing fields ...
// Box data
BoxData *market.BoxData
}
```
**Step 3: Commit**
```bash
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:
```bash
cd /Users/yida/gopro/open-nofx && go run . migrate
```
**Step 2: Commit**
```bash
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**
```bash
cd /Users/yida/gopro/open-nofx && go test -v ./...
```
**Step 2: Run frontend tests (if available)**
```bash
cd /Users/yida/gopro/open-nofx/web && npm test
```
**Step 3: Fix any failing tests and commit**
```bash
git add .
git commit -m "test: fix tests for grid regime implementation"
```
---
## Task 20: Final Integration Test
**Step 1: Start the server**
```bash
cd /Users/yida/gopro/open-nofx && go run .
```
**Step 2: Verify API endpoint**
```bash
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**
```bash
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 | - |