mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
cb31782be4
- 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)
346 lines
11 KiB
Go
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,
|
|
}
|
|
}
|