* 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
30 KiB
AI自适应网格交易系统修复计划
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: 修复AI网格交易系统的所有致命和严重问题,添加代码级风控保护机制。
Architecture:
- 在AI决策和订单执行之间添加风控验证层
- 实现代码级止损、仓位限制、突破检测
- 修复杠杆设置和订单取消的BUG
- 添加自动网格调整机制
Tech Stack: Go, GORM, 交易所API接口
问题优先级
| 优先级 | 问题 | Task |
|---|---|---|
| P0 致命 | 杠杆未生效 | Task 1 |
| P0 致命 | 取消订单逻辑错误 | Task 2 |
| P0 致命 | 无总仓位限制 | Task 3 |
| P1 严重 | 无止损执行 | Task 4 |
| P1 严重 | 无突破检测 | Task 5 |
| P1 严重 | MaxDrawdown未执行 | Task 6 |
| P1 严重 | DailyLossLimit未执行 | Task 7 |
| P2 中等 | 无动态调整 | Task 8 |
| P2 中等 | 订单状态同步错误 | Task 9 |
Task 1: 修复杠杆设置BUG
问题: PlaceLimitOrder 完全忽略 Leverage 字段,从未调用 SetLeverage()
Files:
- Modify:
trader/interface.go:171-194 - Modify:
trader/auto_trader_grid.go:324-409 - Create:
trader/grid_test.go(新增测试)
Step 1.1: 在 GridTraderAdapter.PlaceLimitOrder 中添加杠杆设置
修改 trader/interface.go:
// PlaceLimitOrder implements limit order using available methods
// For exchanges without native limit order support, this uses conditional orders
func (a *GridTraderAdapter) PlaceLimitOrder(req *LimitOrderRequest) (*LimitOrderResult, error) {
// CRITICAL FIX: Set leverage before placing order
if req.Leverage > 0 {
if err := a.Trader.SetLeverage(req.Symbol, req.Leverage); err != nil {
logger.Warnf("[Grid] Failed to set leverage %dx: %v", req.Leverage, err)
// Continue anyway - some exchanges don't require explicit leverage setting
}
}
// Use SetStopLoss/SetTakeProfit as conditional limit orders
// For buy orders below current price, use stop-loss mechanism
// For sell orders above current price, use take-profit mechanism
var err error
if req.Side == "BUY" {
err = a.Trader.SetStopLoss(req.Symbol, "SHORT", req.Quantity, req.Price)
} else {
err = a.Trader.SetTakeProfit(req.Symbol, "LONG", req.Quantity, req.Price)
}
if err != nil {
return nil, err
}
return &LimitOrderResult{
OrderID: req.ClientID,
ClientID: req.ClientID,
Symbol: req.Symbol,
Side: req.Side,
PositionSide: req.PositionSide,
Price: req.Price,
Quantity: req.Quantity,
Status: "NEW",
}, nil
}
Step 1.2: 在 InitializeGrid 中设置杠杆
修改 trader/auto_trader_grid.go, 在 InitializeGrid() 函数末尾添加:
// InitializeGrid initializes the grid state and calculates levels
func (at *AutoTrader) InitializeGrid() error {
// ... 现有代码 ...
at.gridState.IsInitialized = true
// CRITICAL: Set leverage on exchange before trading
if err := at.trader.SetLeverage(gridConfig.Symbol, gridConfig.Leverage); err != nil {
logger.Warnf("[Grid] Failed to set leverage %dx on exchange: %v", gridConfig.Leverage, err)
// Not fatal - continue with default leverage
} else {
logger.Infof("[Grid] Leverage set to %dx for %s", gridConfig.Leverage, gridConfig.Symbol)
}
logger.Infof("[Grid] Initialized: %d levels, $%.2f - $%.2f, spacing $%.2f",
gridConfig.GridCount, at.gridState.LowerPrice, at.gridState.UpperPrice, at.gridState.GridSpacing)
return nil
}
Step 1.3: 运行测试验证
go build ./trader/
go test -v -run "TestLighter.*Leverage" ./trader/ -timeout 60s
Step 1.4: 提交
git add trader/interface.go trader/auto_trader_grid.go
git commit -m "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"
Task 2: 修复订单取消逻辑BUG
问题: GridTraderAdapter.CancelOrder() 错误地调用 CancelAllOrders()
Files:
- Modify:
trader/interface.go:196-200
Step 2.1: 修复 CancelOrder 实现
修改 trader/interface.go:
// CancelOrder cancels a specific order
func (a *GridTraderAdapter) CancelOrder(symbol, orderID string) error {
// Try to use CancelOrder if trader supports it directly
if canceler, ok := a.Trader.(interface {
CancelOrder(symbol, orderID string) error
}); ok {
return canceler.CancelOrder(symbol, orderID)
}
// For traders that only support CancelAllOrders, log a warning
// This is a limitation - we cannot cancel individual orders
logger.Warnf("[Grid] Trader does not support individual order cancellation, " +
"cannot cancel order %s. Consider using exchange-specific GridTrader implementation.", orderID)
// Return error instead of canceling all orders
return fmt.Errorf("individual order cancellation not supported for this exchange")
}
Step 2.2: 添加 fmt import (如果缺失)
确保 trader/interface.go 顶部有:
import (
"fmt"
// ... 其他imports
)
Step 2.3: 运行测试验证
go build ./trader/
Step 2.4: 提交
git add trader/interface.go
git commit -m "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"
Task 3: 添加总仓位限制
问题: 只检查单层仓位,不检查总仓位,导致可能开出巨额仓位
Files:
- Modify:
trader/auto_trader_grid.go:324-409 - Modify:
trader/auto_trader_grid.go(新增checkTotalPositionLimit函数)
Step 3.1: 添加总仓位检查函数
在 trader/auto_trader_grid.go 中 placeGridLimitOrder 函数之前添加:
// checkTotalPositionLimit checks if adding a new position would exceed total limits
// Returns: (allowed bool, currentPositionValue float64, maxAllowed float64)
func (at *AutoTrader) checkTotalPositionLimit(symbol string, additionalValue float64) (bool, float64, float64) {
gridConfig := at.config.StrategyConfig.GridConfig
// Calculate max allowed total position value
// Total position should not exceed: TotalInvestment × Leverage
maxTotalPositionValue := gridConfig.TotalInvestment * float64(gridConfig.Leverage)
// Get current position value from exchange
currentPositionValue := 0.0
positions, err := at.trader.GetPositions()
if err == nil {
for _, pos := range positions {
if sym, ok := pos["symbol"].(string); ok && sym == symbol {
if size, ok := pos["positionAmt"].(float64); ok {
if price, ok := pos["markPrice"].(float64); ok {
currentPositionValue = math.Abs(size) * price
} else if entryPrice, ok := pos["entryPrice"].(float64); ok {
currentPositionValue = math.Abs(size) * entryPrice
}
}
}
}
}
// Also count pending orders as potential position
at.gridState.mu.RLock()
pendingValue := 0.0
for _, level := range at.gridState.Levels {
if level.State == "pending" {
pendingValue += level.OrderQuantity * level.Price
}
}
at.gridState.mu.RUnlock()
totalAfterOrder := currentPositionValue + pendingValue + additionalValue
allowed := totalAfterOrder <= maxTotalPositionValue
return allowed, currentPositionValue + pendingValue, maxTotalPositionValue
}
Step 3.2: 在 placeGridLimitOrder 中使用总仓位检查
修改 trader/auto_trader_grid.go 的 placeGridLimitOrder 函数,在现有检查之后添加:
func (at *AutoTrader) placeGridLimitOrder(d *kernel.Decision, side string) error {
// ... 现有代码到 line 377 ...
// CRITICAL: Check total position limit before placing order
orderValue := quantity * d.Price
allowed, currentValue, maxValue := at.checkTotalPositionLimit(d.Symbol, orderValue)
if !allowed {
logger.Errorf("[Grid] TOTAL POSITION LIMIT EXCEEDED: current=$%.2f + order=$%.2f > max=$%.2f. Rejecting order.",
currentValue, orderValue, maxValue)
return fmt.Errorf("total position value $%.2f would exceed limit $%.2f", currentValue+orderValue, maxValue)
}
req := &LimitOrderRequest{
// ... 现有代码 ...
}
// ... 其余代码 ...
}
Step 3.3: 运行测试验证
go build ./trader/
Step 3.4: 提交
git add trader/auto_trader_grid.go
git commit -m "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 × Leverage
- Logs clear error messages when limit exceeded"
Task 4: 添加止损执行机制
问题: StopLossPct 存在于配置但从未使用
Files:
- Modify:
trader/auto_trader_grid.go(添加checkAndExecuteStopLoss函数) - Modify:
trader/auto_trader_grid.go:504-565(在syncGridState中调用)
Step 4.1: 添加止损检查和执行函数
在 trader/auto_trader_grid.go 中添加:
// checkAndExecuteStopLoss checks if any filled level has exceeded stop loss and closes it
func (at *AutoTrader) checkAndExecuteStopLoss() {
gridConfig := at.config.StrategyConfig.GridConfig
if gridConfig.StopLossPct <= 0 {
return // Stop loss not configured
}
currentPrice, err := at.trader.GetMarketPrice(gridConfig.Symbol)
if err != nil {
logger.Warnf("[Grid] Failed to get market price for stop loss check: %v", err)
return
}
at.gridState.mu.Lock()
defer at.gridState.mu.Unlock()
for i := range at.gridState.Levels {
level := &at.gridState.Levels[i]
if level.State != "filled" || level.PositionEntry <= 0 {
continue
}
// Calculate loss percentage
var lossPct float64
if level.Side == "buy" {
// Long position: loss when price drops
lossPct = (level.PositionEntry - currentPrice) / level.PositionEntry * 100
} else {
// Short position: loss when price rises
lossPct = (currentPrice - level.PositionEntry) / level.PositionEntry * 100
}
// Check if stop loss triggered
if lossPct >= gridConfig.StopLossPct {
logger.Warnf("[Grid] STOP LOSS TRIGGERED: Level %d, entry=$%.2f, current=$%.2f, loss=%.2f%%",
i, level.PositionEntry, currentPrice, lossPct)
// Close the position
var closeErr error
if level.Side == "buy" {
_, closeErr = at.trader.CloseLong(gridConfig.Symbol, level.PositionSize)
} else {
_, closeErr = at.trader.CloseShort(gridConfig.Symbol, level.PositionSize)
}
if closeErr != nil {
logger.Errorf("[Grid] Failed to execute stop loss for level %d: %v", i, closeErr)
} else {
level.State = "stopped"
level.UnrealizedPnL = -lossPct * level.AllocatedUSD / 100
at.gridState.TotalTrades++
logger.Infof("[Grid] Stop loss executed: Level %d closed at $%.2f (loss %.2f%%)",
i, currentPrice, lossPct)
}
}
}
}
Step 4.2: 在 syncGridState 中调用止损检查
修改 trader/auto_trader_grid.go 的 syncGridState 函数末尾:
func (at *AutoTrader) syncGridState() {
// ... 现有代码 ...
logger.Debugf("[Grid] Synced state: position=%.4f, orders=%d", totalPosition, len(openOrders))
// CRITICAL: Check stop loss for filled levels
at.checkAndExecuteStopLoss()
}
Step 4.3: 运行测试验证
go build ./trader/
Step 4.4: 提交
git add trader/auto_trader_grid.go
git commit -m "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"
Task 5: 添加突破检测机制
问题: 价格突破网格边界时无响应,继续执行导致单边亏损
Files:
- Modify:
trader/auto_trader_grid.go(添加checkBreakout函数) - Modify:
trader/auto_trader_grid.go:184-224(在RunGridCycle中调用)
Step 5.1: 添加突破检测函数
在 trader/auto_trader_grid.go 中添加:
// BreakoutType represents the type of price breakout
type BreakoutType string
const (
BreakoutNone BreakoutType = "none"
BreakoutUpper BreakoutType = "upper"
BreakoutLower BreakoutType = "lower"
)
// checkBreakout detects if price has broken out of grid range
// Returns breakout type and percentage beyond boundary
func (at *AutoTrader) checkBreakout() (BreakoutType, float64) {
gridConfig := at.config.StrategyConfig.GridConfig
currentPrice, err := at.trader.GetMarketPrice(gridConfig.Symbol)
if err != nil {
return BreakoutNone, 0
}
at.gridState.mu.RLock()
upper := at.gridState.UpperPrice
lower := at.gridState.LowerPrice
at.gridState.mu.RUnlock()
if upper <= 0 || lower <= 0 {
return BreakoutNone, 0
}
// Check upper breakout
if currentPrice > upper {
breakoutPct := (currentPrice - upper) / upper * 100
return BreakoutUpper, breakoutPct
}
// Check lower breakout
if currentPrice < lower {
breakoutPct := (lower - currentPrice) / lower * 100
return BreakoutLower, breakoutPct
}
return BreakoutNone, 0
}
// handleBreakout handles price breakout from grid range
func (at *AutoTrader) handleBreakout(breakoutType BreakoutType, breakoutPct float64) error {
gridConfig := at.config.StrategyConfig.GridConfig
logger.Warnf("[Grid] BREAKOUT DETECTED: %s, %.2f%% beyond boundary", breakoutType, breakoutPct)
// If breakout exceeds 2%, pause grid and cancel orders
if breakoutPct >= 2.0 {
logger.Warnf("[Grid] Significant breakout (%.2f%%), pausing grid and canceling orders", breakoutPct)
// Cancel all pending orders to prevent further losses
if err := at.cancelAllGridOrders(); err != nil {
logger.Errorf("[Grid] Failed to cancel orders on breakout: %v", err)
}
// Pause grid trading
at.gridState.mu.Lock()
at.gridState.IsPaused = true
at.gridState.mu.Unlock()
return fmt.Errorf("grid paused due to %s breakout (%.2f%%)", breakoutType, breakoutPct)
}
// If breakout is minor (< 2%), consider adjusting grid
if breakoutPct >= 1.0 {
logger.Infof("[Grid] Minor breakout (%.2f%%), considering grid adjustment", breakoutPct)
// Let AI decide whether to adjust
}
return nil
}
Step 5.2: 在 RunGridCycle 中添加突破检测
修改 trader/auto_trader_grid.go 的 RunGridCycle 函数:
func (at *AutoTrader) RunGridCycle() error {
if at.gridState == nil || !at.gridState.IsInitialized {
if err := at.InitializeGrid(); err != nil {
return fmt.Errorf("failed to initialize grid: %w", err)
}
}
// CRITICAL: Check for breakout before executing any trades
breakoutType, breakoutPct := at.checkBreakout()
if breakoutType != BreakoutNone {
if err := at.handleBreakout(breakoutType, breakoutPct); err != nil {
return err // Grid paused due to breakout
}
}
// Check if grid is paused
at.gridState.mu.RLock()
isPaused := at.gridState.IsPaused
at.gridState.mu.RUnlock()
if isPaused {
logger.Infof("[Grid] Grid is paused, skipping cycle")
return nil
}
gridConfig := at.config.StrategyConfig.GridConfig
// ... 其余现有代码 ...
}
Step 5.3: 运行测试验证
go build ./trader/
Step 5.4: 提交
git add trader/auto_trader_grid.go
git commit -m "feat(grid): add breakout detection and auto-pause
CRITICAL: Detect price breakout from grid range
- New checkBreakout() function
- Auto-pause grid on significant breakout (>2%)
- Cancel all orders when breakout detected
- Prevent continued losses in trending market"
Task 6: 添加 MaxDrawdown 强制执行
问题: MaxDrawdownPct 存在于配置但从未检查
Files:
- Modify:
trader/auto_trader_grid.go(添加checkMaxDrawdown函数) - Modify:
trader/auto_trader_grid.go:184-224(在RunGridCycle中调用)
Step 6.1: 添加最大回撤检查函数
在 trader/auto_trader_grid.go 中添加:
// checkMaxDrawdown checks if current drawdown exceeds maximum allowed
// Returns: (exceeded bool, currentDrawdown float64)
func (at *AutoTrader) checkMaxDrawdown() (bool, float64) {
gridConfig := at.config.StrategyConfig.GridConfig
if gridConfig.MaxDrawdownPct <= 0 {
return false, 0
}
// Get current equity
balance, err := at.trader.GetBalance()
if err != nil {
return false, 0
}
currentEquity := 0.0
if equity, ok := balance["total_equity"].(float64); ok {
currentEquity = equity
} else if total, ok := balance["totalWalletBalance"].(float64); ok {
if unrealized, ok := balance["totalUnrealizedProfit"].(float64); ok {
currentEquity = total + unrealized
}
}
if currentEquity <= 0 {
return false, 0
}
// Update peak equity
at.gridState.mu.Lock()
if currentEquity > at.gridState.PeakEquity {
at.gridState.PeakEquity = currentEquity
}
peakEquity := at.gridState.PeakEquity
at.gridState.mu.Unlock()
if peakEquity <= 0 {
return false, 0
}
// Calculate current drawdown
drawdown := (peakEquity - currentEquity) / peakEquity * 100
// Update max drawdown tracking
at.gridState.mu.Lock()
if drawdown > at.gridState.MaxDrawdown {
at.gridState.MaxDrawdown = drawdown
}
at.gridState.mu.Unlock()
return drawdown >= gridConfig.MaxDrawdownPct, drawdown
}
// emergencyExit closes all positions and cancels all orders
func (at *AutoTrader) emergencyExit(reason string) error {
gridConfig := at.config.StrategyConfig.GridConfig
logger.Errorf("[Grid] EMERGENCY EXIT: %s", reason)
// Cancel all orders
if err := at.cancelAllGridOrders(); err != nil {
logger.Errorf("[Grid] Failed to cancel orders in emergency: %v", err)
}
// Close all positions
positions, err := at.trader.GetPositions()
if err == nil {
for _, pos := range positions {
if sym, ok := pos["symbol"].(string); ok && sym == gridConfig.Symbol {
if size, ok := pos["positionAmt"].(float64); ok && size != 0 {
if size > 0 {
at.trader.CloseLong(gridConfig.Symbol, size)
} else {
at.trader.CloseShort(gridConfig.Symbol, -size)
}
}
}
}
}
// Pause grid
at.gridState.mu.Lock()
at.gridState.IsPaused = true
at.gridState.mu.Unlock()
return nil
}
Step 6.2: 在 RunGridCycle 中添加回撤检查
修改 trader/auto_trader_grid.go 的 RunGridCycle 函数,在突破检测后添加:
func (at *AutoTrader) RunGridCycle() error {
// ... 初始化检查 ...
// CRITICAL: Check for breakout
// ... 突破检测代码 ...
// CRITICAL: Check max drawdown
exceeded, drawdown := at.checkMaxDrawdown()
if exceeded {
return at.emergencyExit(fmt.Sprintf("max drawdown exceeded: %.2f%%", drawdown))
}
// ... 其余代码 ...
}
Step 6.3: 运行测试验证
go build ./trader/
Step 6.4: 提交
git add trader/auto_trader_grid.go
git commit -m "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"
Task 7: 添加 DailyLossLimit 强制执行
问题: DailyLossLimitPct 存在于配置但从未检查
Files:
- Modify:
trader/auto_trader_grid.go(添加checkDailyLossLimit函数) - Modify:
trader/auto_trader_grid.go:184-224(在RunGridCycle中调用)
Step 7.1: 添加日损失限制检查函数
在 trader/auto_trader_grid.go 中添加:
// checkDailyLossLimit checks if daily loss exceeds limit
// Returns: (exceeded bool, dailyLossPct float64)
func (at *AutoTrader) checkDailyLossLimit() (bool, float64) {
gridConfig := at.config.StrategyConfig.GridConfig
if gridConfig.DailyLossLimitPct <= 0 {
return false, 0
}
at.gridState.mu.Lock()
// Reset daily PnL if new day
now := time.Now()
if now.YearDay() != at.gridState.LastDailyReset.YearDay() ||
now.Year() != at.gridState.LastDailyReset.Year() {
at.gridState.DailyPnL = 0
at.gridState.LastDailyReset = now
}
dailyPnL := at.gridState.DailyPnL
at.gridState.mu.Unlock()
// Calculate daily loss as percentage of total investment
dailyLossPct := 0.0
if gridConfig.TotalInvestment > 0 && dailyPnL < 0 {
dailyLossPct = (-dailyPnL) / gridConfig.TotalInvestment * 100
}
return dailyLossPct >= gridConfig.DailyLossLimitPct, dailyLossPct
}
// updateDailyPnL updates the daily PnL tracking
func (at *AutoTrader) updateDailyPnL(realizedPnL float64) {
at.gridState.mu.Lock()
at.gridState.DailyPnL += realizedPnL
at.gridState.TotalProfit += realizedPnL
at.gridState.mu.Unlock()
}
Step 7.2: 在 RunGridCycle 中添加日损失检查
修改 trader/auto_trader_grid.go 的 RunGridCycle 函数:
func (at *AutoTrader) RunGridCycle() error {
// ... 初始化和突破检测 ...
// CRITICAL: Check max drawdown
// ...
// CRITICAL: Check daily loss limit
exceeded, dailyLossPct := at.checkDailyLossLimit()
if exceeded {
logger.Errorf("[Grid] Daily loss limit exceeded: %.2f%%", dailyLossPct)
at.gridState.mu.Lock()
at.gridState.IsPaused = true
at.gridState.mu.Unlock()
return fmt.Errorf("daily loss limit exceeded: %.2f%%", dailyLossPct)
}
// ... 其余代码 ...
}
Step 7.3: 运行测试验证
go build ./trader/
Step 7.4: 提交
git add trader/auto_trader_grid.go
git commit -m "feat(grid): enforce daily loss limit
- New checkDailyLossLimit() function
- Track daily PnL with auto-reset at midnight
- Pause grid when DailyLossLimitPct exceeded
- Prevent excessive single-day losses"
Task 8: 添加自动网格调整
问题: 网格无法自动适应价格偏移
Files:
- Modify:
trader/auto_trader_grid.go(添加checkGridSkew函数) - Modify:
trader/auto_trader_grid.go:504-565(在syncGridState中调用)
Step 8.1: 添加网格倾斜检测函数
在 trader/auto_trader_grid.go 中添加:
// checkGridSkew checks if grid is heavily skewed (too many fills on one side)
// Returns: (skewed bool, buyFilledCount int, sellFilledCount int)
func (at *AutoTrader) checkGridSkew() (bool, int, int) {
at.gridState.mu.RLock()
defer at.gridState.mu.RUnlock()
buyFilled := 0
sellFilled := 0
buyEmpty := 0
sellEmpty := 0
for _, level := range at.gridState.Levels {
if level.Side == "buy" {
if level.State == "filled" {
buyFilled++
} else if level.State == "empty" {
buyEmpty++
}
} else {
if level.State == "filled" {
sellFilled++
} else if level.State == "empty" {
sellEmpty++
}
}
}
// Grid is skewed if one side has 3x more fills than the other
// or if one side is completely empty
skewed := false
if buyFilled > 0 && sellFilled == 0 && sellEmpty > 5 {
skewed = true // All buys filled, no sells
} else if sellFilled > 0 && buyFilled == 0 && buyEmpty > 5 {
skewed = true // All sells filled, no buys
} else if buyFilled >= 3*sellFilled && buyFilled > 5 {
skewed = true
} else if sellFilled >= 3*buyFilled && sellFilled > 5 {
skewed = true
}
return skewed, buyFilled, sellFilled
}
// autoAdjustGrid automatically adjusts grid when heavily skewed
func (at *AutoTrader) autoAdjustGrid() {
skewed, buyFilled, sellFilled := at.checkGridSkew()
if !skewed {
return
}
logger.Warnf("[Grid] Grid heavily skewed: buy_filled=%d, sell_filled=%d. Auto-adjusting...",
buyFilled, sellFilled)
gridConfig := at.config.StrategyConfig.GridConfig
// Get current price
currentPrice, err := at.trader.GetMarketPrice(gridConfig.Symbol)
if err != nil {
logger.Errorf("[Grid] Failed to get price for auto-adjust: %v", err)
return
}
// Check if price is near grid boundary
at.gridState.mu.RLock()
upper := at.gridState.UpperPrice
lower := at.gridState.LowerPrice
at.gridState.mu.RUnlock()
// Only adjust if price has moved significantly (>50% of grid range)
gridRange := upper - lower
midPrice := (upper + lower) / 2
priceDeviation := math.Abs(currentPrice - midPrice)
if priceDeviation < gridRange*0.3 {
return // Price still near center, don't adjust
}
// Cancel existing orders and reinitialize
logger.Infof("[Grid] Adjusting grid around new price $%.2f", currentPrice)
at.cancelAllGridOrders()
at.initializeGridLevels(currentPrice, gridConfig)
}
Step 8.2: 在 syncGridState 中调用自动调整
修改 trader/auto_trader_grid.go 的 syncGridState 函数:
func (at *AutoTrader) syncGridState() {
// ... 现有代码 ...
// Check stop loss
at.checkAndExecuteStopLoss()
// Check grid skew and auto-adjust if needed
at.autoAdjustGrid()
}
Step 8.3: 运行测试验证
go build ./trader/
Step 8.4: 提交
git add trader/auto_trader_grid.go
git commit -m "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"
Task 9: 修复订单状态同步逻辑
问题: 假设订单不存在就是成交,但可能是被取消
Files:
- Modify:
trader/auto_trader_grid.go:504-565
Step 9.1: 改进订单状态同步逻辑
修改 trader/auto_trader_grid.go 的 syncGridState 函数:
// syncGridState syncs grid state with exchange
func (at *AutoTrader) syncGridState() {
gridConfig := at.config.StrategyConfig.GridConfig
// Get open orders from exchange
openOrders, err := at.trader.GetOpenOrders(gridConfig.Symbol)
if err != nil {
logger.Warnf("[Grid] Failed to get open orders: %v", err)
return
}
// Build set of active order IDs
activeOrderIDs := make(map[string]bool)
for _, order := range openOrders {
activeOrderIDs[order.OrderID] = true
}
// Get current positions to verify fills
positions, err := at.trader.GetPositions()
currentPositionSize := 0.0
if err == nil {
for _, pos := range positions {
if sym, ok := pos["symbol"].(string); ok && sym == gridConfig.Symbol {
if size, ok := pos["positionAmt"].(float64); ok {
currentPositionSize = size
}
}
}
}
// Update levels based on order status
at.gridState.mu.Lock()
previousFilledCount := 0
for _, level := range at.gridState.Levels {
if level.State == "filled" {
previousFilledCount++
}
}
for i := range at.gridState.Levels {
level := &at.gridState.Levels[i]
if level.State == "pending" && level.OrderID != "" {
if !activeOrderIDs[level.OrderID] {
// Order no longer exists - check if position changed to determine fill vs cancel
// This is a heuristic - ideally we'd query order history
if math.Abs(currentPositionSize) > math.Abs(float64(previousFilledCount)*level.OrderQuantity) {
// Position increased, likely filled
level.State = "filled"
level.PositionEntry = level.Price
level.PositionSize = level.OrderQuantity
at.gridState.TotalTrades++
logger.Infof("[Grid] Level %d order filled at $%.2f", i, level.Price)
} else {
// Position didn't increase as expected, likely cancelled
level.State = "empty"
level.OrderID = ""
level.OrderQuantity = 0
logger.Infof("[Grid] Level %d order cancelled/expired", i)
}
delete(at.gridState.OrderBook, level.OrderID)
}
}
}
at.gridState.mu.Unlock()
logger.Debugf("[Grid] Synced state: position=%.4f, orders=%d", currentPositionSize, len(openOrders))
// Check stop loss
at.checkAndExecuteStopLoss()
// Check grid skew
at.autoAdjustGrid()
}
Step 9.2: 运行测试验证
go build ./trader/
Step 9.3: 提交
git add trader/auto_trader_grid.go
git commit -m "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"
完成后的验证步骤
全面测试
# 编译验证
go build ./...
# 运行所有trader测试
go test -v ./trader/... -timeout 300s
# 运行网格相关测试
go test -v -run "Grid" ./trader/ -timeout 60s
代码审查清单
- 所有P0致命问题已修复
- 所有P1严重问题已修复
- 杠杆在初始化时设置
- 订单取消逻辑正确
- 总仓位有限制
- 止损被执行
- 突破时自动暂停
- MaxDrawdown触发紧急退出
- DailyLossLimit暂停交易
- 网格自动调整
架构改进总结
修复后的架构:
┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ ┌─────────────┐
│ 市场数据 │ ──▶ │ AI决策 │ ──▶ │ 代码级风控验证 │ ──▶ │ 执行交易 │
└─────────────┘ └─────────────┘ └─────────────────────────┘ └─────────────┘
│
▼
┌────────────────────────────────────────────────────┐
│ 风控检查清单 (每个周期执行) │
│ ✓ checkBreakout() - 突破检测 │
│ ✓ checkMaxDrawdown() - 最大回撤 │
│ ✓ checkDailyLossLimit() - 日损失限制 │
│ ✓ checkTotalPositionLimit() - 总仓位限制 │
│ ✓ checkAndExecuteStopLoss() - 止损执行 │
│ ✓ checkGridSkew() - 网格平衡 │
│ ✓ SetLeverage() - 杠杆设置 │
└────────────────────────────────────────────────────┘