mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 00:36:56 +08:00
- 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
1073 lines
30 KiB
Markdown
1073 lines
30 KiB
Markdown
# AI自适应网格交易系统修复计划
|
||
|
||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||
|
||
**Goal:** 修复AI网格交易系统的所有致命和严重问题,添加代码级风控保护机制。
|
||
|
||
**Architecture:**
|
||
1. 在AI决策和订单执行之间添加风控验证层
|
||
2. 实现代码级止损、仓位限制、突破检测
|
||
3. 修复杠杆设置和订单取消的BUG
|
||
4. 添加自动网格调整机制
|
||
|
||
**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`:
|
||
|
||
```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()` 函数末尾添加:
|
||
|
||
```go
|
||
// 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: 运行测试验证
|
||
|
||
```bash
|
||
go build ./trader/
|
||
go test -v -run "TestLighter.*Leverage" ./trader/ -timeout 60s
|
||
```
|
||
|
||
### Step 1.4: 提交
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```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` 顶部有:
|
||
```go
|
||
import (
|
||
"fmt"
|
||
// ... 其他imports
|
||
)
|
||
```
|
||
|
||
### Step 2.3: 运行测试验证
|
||
|
||
```bash
|
||
go build ./trader/
|
||
```
|
||
|
||
### Step 2.4: 提交
|
||
|
||
```bash
|
||
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` 函数之前添加:
|
||
|
||
```go
|
||
// 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` 函数,在现有检查之后添加:
|
||
|
||
```go
|
||
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: 运行测试验证
|
||
|
||
```bash
|
||
go build ./trader/
|
||
```
|
||
|
||
### Step 3.4: 提交
|
||
|
||
```bash
|
||
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` 中添加:
|
||
|
||
```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` 函数末尾:
|
||
|
||
```go
|
||
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: 运行测试验证
|
||
|
||
```bash
|
||
go build ./trader/
|
||
```
|
||
|
||
### Step 4.4: 提交
|
||
|
||
```bash
|
||
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` 中添加:
|
||
|
||
```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` 函数:
|
||
|
||
```go
|
||
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: 运行测试验证
|
||
|
||
```bash
|
||
go build ./trader/
|
||
```
|
||
|
||
### Step 5.4: 提交
|
||
|
||
```bash
|
||
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` 中添加:
|
||
|
||
```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` 函数,在突破检测后添加:
|
||
|
||
```go
|
||
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: 运行测试验证
|
||
|
||
```bash
|
||
go build ./trader/
|
||
```
|
||
|
||
### Step 6.4: 提交
|
||
|
||
```bash
|
||
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` 中添加:
|
||
|
||
```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` 函数:
|
||
|
||
```go
|
||
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: 运行测试验证
|
||
|
||
```bash
|
||
go build ./trader/
|
||
```
|
||
|
||
### Step 7.4: 提交
|
||
|
||
```bash
|
||
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` 中添加:
|
||
|
||
```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` 函数:
|
||
|
||
```go
|
||
func (at *AutoTrader) syncGridState() {
|
||
// ... 现有代码 ...
|
||
|
||
// Check stop loss
|
||
at.checkAndExecuteStopLoss()
|
||
|
||
// Check grid skew and auto-adjust if needed
|
||
at.autoAdjustGrid()
|
||
}
|
||
```
|
||
|
||
### Step 8.3: 运行测试验证
|
||
|
||
```bash
|
||
go build ./trader/
|
||
```
|
||
|
||
### Step 8.4: 提交
|
||
|
||
```bash
|
||
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` 函数:
|
||
|
||
```go
|
||
// 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: 运行测试验证
|
||
|
||
```bash
|
||
go build ./trader/
|
||
```
|
||
|
||
### Step 9.3: 提交
|
||
|
||
```bash
|
||
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"
|
||
```
|
||
|
||
---
|
||
|
||
## 完成后的验证步骤
|
||
|
||
### 全面测试
|
||
|
||
```bash
|
||
# 编译验证
|
||
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() - 杠杆设置 │
|
||
└────────────────────────────────────────────────────┘
|
||
```
|