mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 13:00:59 +08:00
feat(trader): integrate box breakout detection into grid cycle
- Task 10: Add checkBoxBreakout with 3-candle confirmation - Task 11: Add checkFalseBreakoutRecovery for 50% position recovery - Task 12: Add box/breakout/regime fields to GridState
This commit is contained in:
@@ -46,6 +46,25 @@ type GridState struct {
|
||||
|
||||
// Order tracking
|
||||
OrderBook map[string]int // OrderID -> LevelIndex
|
||||
|
||||
// Box state
|
||||
ShortBoxUpper float64
|
||||
ShortBoxLower float64
|
||||
MidBoxUpper float64
|
||||
MidBoxLower float64
|
||||
LongBoxUpper float64
|
||||
LongBoxLower float64
|
||||
|
||||
// Breakout state
|
||||
BreakoutLevel string
|
||||
BreakoutDirection string
|
||||
BreakoutConfirmCount int
|
||||
|
||||
// Position reduction (0 = normal, 50 = reduced after false breakout)
|
||||
PositionReductionPct float64
|
||||
|
||||
// Current regime level
|
||||
CurrentRegimeLevel string
|
||||
}
|
||||
|
||||
// NewGridState creates a new grid state
|
||||
@@ -257,6 +276,169 @@ func (at *AutoTrader) handleBreakout(breakoutType BreakoutType, breakoutPct floa
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
action := getBreakoutAction(breakoutLevel)
|
||||
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()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeAllPositions closes all open positions for the grid symbol
|
||||
func (at *AutoTrader) closeAllPositions() error {
|
||||
gridConfig := at.config.StrategyConfig.GridConfig
|
||||
if gridConfig == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
positions, err := at.trader.GetPositions()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get positions: %w", err)
|
||||
}
|
||||
|
||||
for _, pos := range positions {
|
||||
symbol, _ := pos["symbol"].(string)
|
||||
if symbol != gridConfig.Symbol {
|
||||
continue
|
||||
}
|
||||
|
||||
size, _ := pos["positionAmt"].(float64)
|
||||
if size == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if size > 0 {
|
||||
_, err = at.trader.CloseLong(symbol, size)
|
||||
} else {
|
||||
_, err = at.trader.CloseShort(symbol, -size)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Infof("Failed to close position: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
at.gridState.mu.RUnlock()
|
||||
|
||||
// Only check if we had a breakout
|
||||
if breakoutLevel == string(market.BreakoutNone) && positionReduction == 0 && !isPaused {
|
||||
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()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AutoTrader Grid Methods
|
||||
// ============================================================================
|
||||
@@ -422,6 +604,16 @@ func (at *AutoTrader) RunGridCycle() error {
|
||||
return fmt.Errorf("daily loss limit exceeded: %.2f%%", dailyLossPct)
|
||||
}
|
||||
|
||||
// Check multi-period box breakout
|
||||
if err := at.checkBoxBreakout(); err != nil {
|
||||
logger.Infof("Box breakout check error: %v", err)
|
||||
}
|
||||
|
||||
// Check for false breakout recovery
|
||||
if err := at.checkFalseBreakoutRecovery(); err != nil {
|
||||
logger.Infof("False breakout recovery check error: %v", err)
|
||||
}
|
||||
|
||||
// Check if grid is paused
|
||||
at.gridState.mu.RLock()
|
||||
isPaused := at.gridState.IsPaused
|
||||
|
||||
Reference in New Issue
Block a user