mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 01:14:40 +08:00
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)
This commit is contained in:
345
trader/auto_trader_grid_regime.go
Normal file
345
trader/auto_trader_grid_regime.go
Normal file
@@ -0,0 +1,345 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user