mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 14:01:29 +08:00
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.
This commit is contained in:
@@ -998,10 +998,151 @@ func (at *AutoTrader) autoAdjustGrid() {
|
||||
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)
|
||||
|
||||
// Cancel existing orders first (before taking the lock for state modification)
|
||||
if err := at.cancelAllGridOrders(); err != nil {
|
||||
logger.Errorf("[Grid] Failed to cancel orders during auto-adjust: %v", err)
|
||||
// Continue with adjustment anyway
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Hold lock for the entire adjustment operation to ensure atomicity
|
||||
at.gridState.mu.Lock()
|
||||
defer at.gridState.mu.Unlock()
|
||||
|
||||
// Preserve filled positions before reinitializing
|
||||
filledPositions := make(map[int]kernel.GridLevelInfo)
|
||||
for i, level := range at.gridState.Levels {
|
||||
if level.State == "filled" {
|
||||
filledPositions[i] = level
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Recalculate grid bounds centered on current price
|
||||
// Use the same logic as InitializeGrid() - either ATR-based or default percentage
|
||||
if gridConfig.UseATRBounds {
|
||||
// Try to get ATR for bound calculation
|
||||
mktData, err := market.GetWithTimeframes(gridConfig.Symbol, []string{"4h"}, "4h", 20)
|
||||
if err != nil {
|
||||
logger.Warnf("[Grid] Failed to get market data for ATR during adjust: %v, using default bounds", err)
|
||||
at.calculateDefaultBoundsLocked(currentPrice, gridConfig)
|
||||
} else {
|
||||
at.calculateATRBoundsLocked(currentPrice, mktData, gridConfig)
|
||||
}
|
||||
} else {
|
||||
// Use default bounds calculation (scaled by grid count)
|
||||
at.calculateDefaultBoundsLocked(currentPrice, gridConfig)
|
||||
}
|
||||
|
||||
// Recalculate grid spacing based on new bounds
|
||||
at.gridState.GridSpacing = (at.gridState.UpperPrice - at.gridState.LowerPrice) / float64(gridConfig.GridCount-1)
|
||||
|
||||
logger.Infof("[Grid] New bounds: $%.2f - $%.2f, spacing: $%.2f",
|
||||
at.gridState.LowerPrice, at.gridState.UpperPrice, at.gridState.GridSpacing)
|
||||
|
||||
// Initialize new grid levels (without lock since we already hold it)
|
||||
at.initializeGridLevelsLocked(currentPrice, gridConfig)
|
||||
|
||||
// CRITICAL FIX: Restore filled positions - find closest new level for each filled position
|
||||
for _, filledLevel := range filledPositions {
|
||||
closestIdx := -1
|
||||
closestDist := math.MaxFloat64
|
||||
|
||||
for i, newLevel := range at.gridState.Levels {
|
||||
dist := math.Abs(newLevel.Price - filledLevel.PositionEntry)
|
||||
if dist < closestDist {
|
||||
closestDist = dist
|
||||
closestIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
if closestIdx >= 0 {
|
||||
// Restore the filled state to the closest level
|
||||
at.gridState.Levels[closestIdx].State = "filled"
|
||||
at.gridState.Levels[closestIdx].PositionEntry = filledLevel.PositionEntry
|
||||
at.gridState.Levels[closestIdx].PositionSize = filledLevel.PositionSize
|
||||
at.gridState.Levels[closestIdx].UnrealizedPnL = filledLevel.UnrealizedPnL
|
||||
at.gridState.Levels[closestIdx].OrderID = filledLevel.OrderID
|
||||
at.gridState.Levels[closestIdx].OrderQuantity = filledLevel.OrderQuantity
|
||||
logger.Infof("[Grid] Restored filled position at level %d (entry $%.2f)", closestIdx, filledLevel.PositionEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculateDefaultBoundsLocked calculates default bounds (caller must hold lock)
|
||||
func (at *AutoTrader) calculateDefaultBoundsLocked(price float64, config *store.GridStrategyConfig) {
|
||||
// Default: ±3% from current price, scaled by grid count
|
||||
multiplier := 0.03 * float64(config.GridCount) / 10
|
||||
at.gridState.UpperPrice = price * (1 + multiplier)
|
||||
at.gridState.LowerPrice = price * (1 - multiplier)
|
||||
}
|
||||
|
||||
// calculateATRBoundsLocked calculates bounds using ATR (caller must hold lock)
|
||||
func (at *AutoTrader) calculateATRBoundsLocked(price float64, mktData *market.Data, config *store.GridStrategyConfig) {
|
||||
atr := 0.0
|
||||
if mktData.LongerTermContext != nil {
|
||||
atr = mktData.LongerTermContext.ATR14
|
||||
}
|
||||
|
||||
if atr <= 0 {
|
||||
at.calculateDefaultBoundsLocked(price, config)
|
||||
return
|
||||
}
|
||||
|
||||
multiplier := config.ATRMultiplier
|
||||
if multiplier <= 0 {
|
||||
multiplier = 2.0
|
||||
}
|
||||
|
||||
halfRange := atr * multiplier
|
||||
at.gridState.UpperPrice = price + halfRange
|
||||
at.gridState.LowerPrice = price - halfRange
|
||||
}
|
||||
|
||||
// initializeGridLevelsLocked creates the grid level structure (caller must hold lock)
|
||||
func (at *AutoTrader) initializeGridLevelsLocked(currentPrice float64, config *store.GridStrategyConfig) {
|
||||
levels := make([]kernel.GridLevelInfo, config.GridCount)
|
||||
totalWeight := 0.0
|
||||
weights := make([]float64, config.GridCount)
|
||||
|
||||
// Calculate weights based on distribution
|
||||
for i := 0; i < config.GridCount; i++ {
|
||||
switch config.Distribution {
|
||||
case "gaussian":
|
||||
// Gaussian distribution - more weight in the middle
|
||||
center := float64(config.GridCount-1) / 2
|
||||
sigma := float64(config.GridCount) / 4
|
||||
weights[i] = math.Exp(-math.Pow(float64(i)-center, 2) / (2 * sigma * sigma))
|
||||
case "pyramid":
|
||||
// Pyramid - more weight at bottom
|
||||
weights[i] = float64(config.GridCount - i)
|
||||
default: // uniform
|
||||
weights[i] = 1.0
|
||||
}
|
||||
totalWeight += weights[i]
|
||||
}
|
||||
|
||||
// Create levels
|
||||
for i := 0; i < config.GridCount; i++ {
|
||||
price := at.gridState.LowerPrice + float64(i)*at.gridState.GridSpacing
|
||||
allocatedUSD := config.TotalInvestment * weights[i] / totalWeight
|
||||
|
||||
// Determine initial side (below current price = buy, above = sell)
|
||||
side := "buy"
|
||||
if price > currentPrice {
|
||||
side = "sell"
|
||||
}
|
||||
|
||||
levels[i] = kernel.GridLevelInfo{
|
||||
Index: i,
|
||||
Price: price,
|
||||
State: "empty",
|
||||
Side: side,
|
||||
AllocatedUSD: allocatedUSD,
|
||||
}
|
||||
}
|
||||
|
||||
at.gridState.Levels = levels
|
||||
}
|
||||
|
||||
// checkAndExecuteStopLoss checks if any filled level has exceeded stop loss and closes it
|
||||
|
||||
Reference in New Issue
Block a user