Files
nofx/trader/auto_trader_grid_regime.go
tinkle-community cb31782be4 refactor: split large files and clean up project structure
- Rename experience/ to telemetry/ for clarity
- Split 15+ large Go files (800-2200 lines) into focused modules:
  kernel/engine.go, backtest/runner.go, market/data.go, store/position.go,
  api/handler_trader.go, trader/auto_trader_grid.go, and 9 exchange traders
- Split frontend monoliths: types.ts, api.ts, AITradersPage.tsx, BacktestPage.tsx
  into domain-specific modules with barrel re-exports
- Remove stale files: screenshots, .yml.old, pyproject.toml
- Remove unused scripts/ and cmd/ directories
- Remove broken/outdated test files (network-dependent, stale expectations)
2026-03-12 12:53:57 +08:00

346 lines
11 KiB
Go

package trader
import (
"fmt"
"math"
"nofx/logger"
"nofx/market"
"time"
)
// ============================================================================
// Regime Detection and Strategy Switching
// ============================================================================
// 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 grid state with box values
at.gridState.mu.Lock()
at.gridState.ShortBoxUpper = box.ShortUpper
at.gridState.ShortBoxLower = box.ShortLower
at.gridState.MidBoxUpper = box.MidUpper
at.gridState.MidBoxLower = box.MidLower
at.gridState.LongBoxUpper = box.LongUpper
at.gridState.LongBoxLower = box.LongLower
at.gridState.mu.Unlock()
// Detect breakout
breakoutLevel, direction := detectBoxBreakout(box)
// Get current breakout state
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
// Use direction-aware action if enabled
enableDirectionAdjust := gridConfig.EnableDirectionAdjust
action := getBreakoutActionWithDirection(breakoutLevel, enableDirectionAdjust)
// If direction adjustment action, determine the new direction
if action == BreakoutActionAdjustDirection {
box, _ := market.GetBoxData(gridConfig.Symbol)
newDirection := determineGridDirection(box, at.gridState.CurrentDirection, breakoutLevel, direction)
return at.executeDirectionAdjustment(newDirection)
}
return at.executeBreakoutAction(action)
}
// executeBreakoutAction executes the appropriate action for a breakout
func (at *AutoTrader) executeBreakoutAction(action BreakoutAction) error {
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()
case BreakoutActionAdjustDirection:
// Direction adjustment is handled separately via executeDirectionAdjustment
// This case should not be reached, but handle gracefully
logger.Infof("Direction adjustment action received via executeBreakoutAction")
return nil
}
return nil
}
// executeDirectionAdjustment handles grid direction changes based on box breakout
func (at *AutoTrader) executeDirectionAdjustment(newDirection market.GridDirection) error {
at.gridState.mu.RLock()
oldDirection := at.gridState.CurrentDirection
at.gridState.mu.RUnlock()
if oldDirection == newDirection {
return nil // No change needed
}
logger.Infof("[Grid] Direction adjustment: %s -> %s", oldDirection, newDirection)
// Cancel existing orders before adjusting
if err := at.cancelAllGridOrders(); err != nil {
logger.Warnf("[Grid] Failed to cancel orders during direction adjustment: %v", err)
}
// Apply the new direction
return at.adjustGridDirection(newDirection)
}
// adjustGridDirection handles runtime direction adjustment when breakout is detected
func (at *AutoTrader) adjustGridDirection(newDirection market.GridDirection) error {
at.gridState.mu.Lock()
defer at.gridState.mu.Unlock()
oldDirection := at.gridState.CurrentDirection
if oldDirection == newDirection {
return nil // No change needed
}
at.gridState.CurrentDirection = newDirection
at.gridState.DirectionChangedAt = time.Now()
at.gridState.DirectionChangeCount++
logger.Infof("[Grid] Direction changed: %s -> %s (change count: %d)",
oldDirection, newDirection, at.gridState.DirectionChangeCount)
// Get current price for recalculation
currentPrice, err := at.trader.GetMarketPrice(at.gridState.Config.Symbol)
if err != nil {
return fmt.Errorf("failed to get market price: %w", err)
}
// Reapply direction to grid levels
at.applyGridDirection(currentPrice)
return nil
}
// 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
currentDirection := at.gridState.CurrentDirection
at.gridState.mu.RUnlock()
// Only check if we had a breakout or non-neutral direction
needsRecoveryCheck := breakoutLevel != string(market.BreakoutNone) ||
positionReduction != 0 ||
isPaused ||
(gridConfig.EnableDirectionAdjust && currentDirection != market.GridDirectionNeutral)
if !needsRecoveryCheck {
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()
}
// Check for direction recovery toward neutral (if direction adjustment is enabled)
if gridConfig.EnableDirectionAdjust && currentDirection != market.GridDirectionNeutral {
if shouldRecoverDirection(box, currentDirection) {
newDirection := determineRecoveryDirection(box.CurrentPrice, box, currentDirection)
if newDirection != currentDirection {
logger.Infof("[Grid] Direction recovery: %s -> %s (price back in short box)",
currentDirection, newDirection)
at.adjustGridDirection(newDirection)
}
}
}
return nil
}
// GetGridRiskInfo returns current risk information for frontend display
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
var currentPositionSize 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)
currentPositionSize = size
break
}
}
effectiveLeverage := 0.0
if totalInvestment > 0 {
effectiveLeverage = currentPositionValue / totalInvestment
}
// Calculate max position based on regime
regimeLevel := market.RegimeLevel(at.gridState.CurrentRegimeLevel)
if regimeLevel == "" {
regimeLevel = market.RegimeLevelStandard
}
// Use default position limit since GridStrategyConfig doesn't have regime-specific limits
// Default is 70% for standard regime
maxPositionPct := 70.0
switch regimeLevel {
case market.RegimeLevelNarrow:
maxPositionPct = 40.0
case market.RegimeLevelStandard:
maxPositionPct = 70.0
case market.RegimeLevelWide:
maxPositionPct = 60.0
case market.RegimeLevelVolatile:
maxPositionPct = 40.0
}
maxPosition := totalInvestment * maxPositionPct / 100 * float64(leverage)
// Use default leverage limits since GridStrategyConfig doesn't have regime-specific limits
recommendedLeverage := leverage
switch regimeLevel {
case market.RegimeLevelNarrow:
recommendedLeverage = min(leverage, 2)
case market.RegimeLevelStandard:
recommendedLeverage = min(leverage, 4)
case market.RegimeLevelWide:
recommendedLeverage = min(leverage, 3)
case market.RegimeLevelVolatile:
recommendedLeverage = min(leverage, 2)
}
// Calculate liquidation distance and price only when there's a position
var liquidationDistance float64
var liquidationPrice float64
if currentPositionSize != 0 && currentPrice > 0 {
liquidationDistance = 100.0 / float64(leverage) * 0.9 // ~90% of theoretical max
if currentPositionSize > 0 {
// Long position: liquidation below entry
liquidationPrice = currentPrice * (1 - liquidationDistance/100)
} else {
// Short position: liquidation above entry
liquidationPrice = currentPrice * (1 + liquidationDistance/100)
}
}
positionPercent := 0.0
if maxPosition > 0 {
positionPercent = currentPositionValue / maxPosition * 100
}
return &GridRiskInfo{
CurrentLeverage: leverage,
EffectiveLeverage: effectiveLeverage,
RecommendedLeverage: recommendedLeverage,
CurrentPosition: currentPositionValue,
MaxPosition: maxPosition,
PositionPercent: positionPercent,
LiquidationPrice: liquidationPrice,
LiquidationDistance: liquidationDistance,
RegimeLevel: string(regimeLevel),
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,
CurrentGridDirection: string(at.gridState.CurrentDirection),
DirectionChangeCount: at.gridState.DirectionChangeCount,
EnableDirectionAdjust: gridConfig.EnableDirectionAdjust,
}
}