mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
773857351f
Add GridDirection type with 5 states: - neutral (50% buy + 50% sell) - long/short (100% one direction) - long_bias/short_bias (70%/30% configurable) Direction adjustment logic: - Short box breakout → bias direction (long_bias/short_bias) - Mid box breakout → full direction (long/short) - Long box breakout → emergency handling (unchanged) - Recovery: long → long_bias → neutral ← short_bias ← short Config options: - EnableDirectionAdjust (default: false) - DirectionBiasRatio (default: 0.7) Includes unit tests for all direction-related functions.
313 lines
9.4 KiB
Go
313 lines
9.4 KiB
Go
package trader
|
|
|
|
import (
|
|
"nofx/market"
|
|
"nofx/store"
|
|
"time"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Task 6: Regime Level Classification
|
|
// ============================================================================
|
|
|
|
// classifyRegimeLevel determines the regime level based on market indicators
|
|
// bollingerWidth: Bollinger band width as percentage
|
|
// atr14Pct: ATR14 as percentage of current price
|
|
func classifyRegimeLevel(bollingerWidth, atr14Pct float64) market.RegimeLevel {
|
|
// Narrow: Bollinger < 2%, ATR < 1%
|
|
if bollingerWidth < 2.0 && atr14Pct < 1.0 {
|
|
return market.RegimeLevelNarrow
|
|
}
|
|
|
|
// Standard: Bollinger 2-3%, ATR 1-2%
|
|
if bollingerWidth <= 3.0 && atr14Pct <= 2.0 {
|
|
return market.RegimeLevelStandard
|
|
}
|
|
|
|
// Wide: Bollinger 3-4%, ATR 2-3%
|
|
if bollingerWidth <= 4.0 && atr14Pct <= 3.0 {
|
|
return market.RegimeLevelWide
|
|
}
|
|
|
|
// Volatile: Bollinger > 4%, ATR > 3%
|
|
return market.RegimeLevelVolatile
|
|
}
|
|
|
|
// getRegimeLeverageLimit returns the effective leverage limit for a regime level
|
|
func getRegimeLeverageLimit(level market.RegimeLevel, config *store.GridConfigModel) int {
|
|
switch level {
|
|
case market.RegimeLevelNarrow:
|
|
if config.NarrowRegimeLeverage > 0 {
|
|
return config.NarrowRegimeLeverage
|
|
}
|
|
return 2
|
|
case market.RegimeLevelStandard:
|
|
if config.StandardRegimeLeverage > 0 {
|
|
return config.StandardRegimeLeverage
|
|
}
|
|
return 4
|
|
case market.RegimeLevelWide:
|
|
if config.WideRegimeLeverage > 0 {
|
|
return config.WideRegimeLeverage
|
|
}
|
|
return 3
|
|
case market.RegimeLevelVolatile:
|
|
if config.VolatileRegimeLeverage > 0 {
|
|
return config.VolatileRegimeLeverage
|
|
}
|
|
return 2
|
|
default:
|
|
return 2 // Conservative default
|
|
}
|
|
}
|
|
|
|
// getRegimePositionLimit returns the position limit percentage for a regime level
|
|
func getRegimePositionLimit(level market.RegimeLevel, config *store.GridConfigModel) float64 {
|
|
switch level {
|
|
case market.RegimeLevelNarrow:
|
|
if config.NarrowRegimePositionPct > 0 {
|
|
return config.NarrowRegimePositionPct
|
|
}
|
|
return 40.0
|
|
case market.RegimeLevelStandard:
|
|
if config.StandardRegimePositionPct > 0 {
|
|
return config.StandardRegimePositionPct
|
|
}
|
|
return 70.0
|
|
case market.RegimeLevelWide:
|
|
if config.WideRegimePositionPct > 0 {
|
|
return config.WideRegimePositionPct
|
|
}
|
|
return 60.0
|
|
case market.RegimeLevelVolatile:
|
|
if config.VolatileRegimePositionPct > 0 {
|
|
return config.VolatileRegimePositionPct
|
|
}
|
|
return 40.0
|
|
default:
|
|
return 40.0 // Conservative default
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Task 7: Breakout Detection
|
|
// ============================================================================
|
|
|
|
// detectBoxBreakout checks if price has broken out of any box level
|
|
// Returns the highest breakout level and direction
|
|
func detectBoxBreakout(box *market.BoxData) (market.BreakoutLevel, string) {
|
|
if box == nil {
|
|
return market.BreakoutNone, ""
|
|
}
|
|
|
|
price := box.CurrentPrice
|
|
|
|
// Check long box first (highest priority)
|
|
if price > box.LongUpper {
|
|
return market.BreakoutLong, "up"
|
|
}
|
|
if price < box.LongLower {
|
|
return market.BreakoutLong, "down"
|
|
}
|
|
|
|
// Check mid box
|
|
if price > box.MidUpper {
|
|
return market.BreakoutMid, "up"
|
|
}
|
|
if price < box.MidLower {
|
|
return market.BreakoutMid, "down"
|
|
}
|
|
|
|
// Check short box
|
|
if price > box.ShortUpper {
|
|
return market.BreakoutShort, "up"
|
|
}
|
|
if price < box.ShortLower {
|
|
return market.BreakoutShort, "down"
|
|
}
|
|
|
|
return market.BreakoutNone, ""
|
|
}
|
|
|
|
// ============================================================================
|
|
// Task 8: Breakout Confirmation Logic
|
|
// ============================================================================
|
|
|
|
const BreakoutConfirmRequired = 3 // 3 candles to confirm breakout
|
|
|
|
// BreakoutState tracks the current breakout state
|
|
type BreakoutState struct {
|
|
Level market.BreakoutLevel
|
|
Direction string
|
|
ConfirmCount int
|
|
StartTime time.Time
|
|
}
|
|
|
|
// confirmBreakout updates breakout state and returns true if breakout is confirmed
|
|
func confirmBreakout(state *BreakoutState, currentLevel market.BreakoutLevel, direction string) bool {
|
|
// If price returned to box, reset state
|
|
if currentLevel == market.BreakoutNone {
|
|
state.ConfirmCount = 0
|
|
state.Level = market.BreakoutNone
|
|
state.Direction = ""
|
|
return false
|
|
}
|
|
|
|
// If same breakout continues, increment count
|
|
if state.Level == currentLevel && state.Direction == direction {
|
|
state.ConfirmCount++
|
|
} else {
|
|
// New breakout, reset count
|
|
state.Level = currentLevel
|
|
state.Direction = direction
|
|
state.ConfirmCount = 1
|
|
state.StartTime = time.Now()
|
|
}
|
|
|
|
return state.ConfirmCount >= BreakoutConfirmRequired
|
|
}
|
|
|
|
// ============================================================================
|
|
// Task 9: Breakout Handler
|
|
// ============================================================================
|
|
|
|
// BreakoutAction represents the action to take on breakout
|
|
type BreakoutAction int
|
|
|
|
const (
|
|
BreakoutActionNone BreakoutAction = iota
|
|
BreakoutActionReducePosition // Short box breakout: reduce to 50%
|
|
BreakoutActionPauseGrid // Mid box breakout: pause grid + cancel orders
|
|
BreakoutActionCloseAll // Long box breakout: pause + cancel + close all
|
|
)
|
|
|
|
// getBreakoutAction returns the appropriate action for a breakout level
|
|
func getBreakoutAction(level market.BreakoutLevel) BreakoutAction {
|
|
switch level {
|
|
case market.BreakoutShort:
|
|
return BreakoutActionReducePosition
|
|
case market.BreakoutMid:
|
|
return BreakoutActionPauseGrid
|
|
case market.BreakoutLong:
|
|
return BreakoutActionCloseAll
|
|
default:
|
|
return BreakoutActionNone
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Task 10: Grid Direction Adjustment
|
|
// ============================================================================
|
|
|
|
const (
|
|
// BreakoutActionAdjustDirection adjusts grid direction based on breakout
|
|
BreakoutActionAdjustDirection BreakoutAction = 4
|
|
)
|
|
|
|
// determineGridDirection determines the new grid direction based on box breakout
|
|
// currentDirection: the current grid direction
|
|
// breakoutLevel: which box level has been broken (short/mid/long)
|
|
// direction: breakout direction ("up" or "down")
|
|
// Returns: the new grid direction
|
|
func determineGridDirection(box *market.BoxData, currentDirection market.GridDirection, breakoutLevel market.BreakoutLevel, direction string) market.GridDirection {
|
|
if box == nil {
|
|
return currentDirection
|
|
}
|
|
|
|
price := box.CurrentPrice
|
|
|
|
switch breakoutLevel {
|
|
case market.BreakoutShort:
|
|
// Short box breakout: bias direction
|
|
// Still within mid box, so not a full trend yet
|
|
if direction == "up" {
|
|
return market.GridDirectionLongBias
|
|
}
|
|
return market.GridDirectionShortBias
|
|
|
|
case market.BreakoutMid:
|
|
// Mid box breakout: full direction
|
|
// More significant move, commit fully
|
|
if direction == "up" {
|
|
return market.GridDirectionLong
|
|
}
|
|
return market.GridDirectionShort
|
|
|
|
case market.BreakoutLong:
|
|
// Long box breakout: handled by existing emergency logic
|
|
// Return current direction, let existing handlers take over
|
|
return currentDirection
|
|
|
|
case market.BreakoutNone:
|
|
// No breakout - check if we should recover toward neutral
|
|
return determineRecoveryDirection(price, box, currentDirection)
|
|
|
|
default:
|
|
return currentDirection
|
|
}
|
|
}
|
|
|
|
// determineRecoveryDirection determines if grid direction should recover toward neutral
|
|
// This implements the gradual recovery logic: long → long_bias → neutral ← short_bias ← short
|
|
func determineRecoveryDirection(price float64, box *market.BoxData, currentDirection market.GridDirection) market.GridDirection {
|
|
// Check if price is back inside the short box
|
|
insideShortBox := price >= box.ShortLower && price <= box.ShortUpper
|
|
|
|
if !insideShortBox {
|
|
// Still outside short box, maintain current direction
|
|
return currentDirection
|
|
}
|
|
|
|
// Price is inside short box, start recovery toward neutral
|
|
switch currentDirection {
|
|
case market.GridDirectionLong:
|
|
// Full long → bias long
|
|
return market.GridDirectionLongBias
|
|
case market.GridDirectionLongBias:
|
|
// Bias long → neutral
|
|
return market.GridDirectionNeutral
|
|
case market.GridDirectionShort:
|
|
// Full short → bias short
|
|
return market.GridDirectionShortBias
|
|
case market.GridDirectionShortBias:
|
|
// Bias short → neutral
|
|
return market.GridDirectionNeutral
|
|
default:
|
|
return currentDirection
|
|
}
|
|
}
|
|
|
|
// getBreakoutActionWithDirection returns the appropriate action for a breakout level
|
|
// when direction adjustment is enabled
|
|
func getBreakoutActionWithDirection(level market.BreakoutLevel, enableDirectionAdjust bool) BreakoutAction {
|
|
if !enableDirectionAdjust {
|
|
// Fall back to original behavior
|
|
return getBreakoutAction(level)
|
|
}
|
|
|
|
switch level {
|
|
case market.BreakoutShort:
|
|
// Short box breakout with direction adjustment: adjust direction instead of reducing position
|
|
return BreakoutActionAdjustDirection
|
|
case market.BreakoutMid:
|
|
// Mid box breakout with direction adjustment: adjust to full direction
|
|
return BreakoutActionAdjustDirection
|
|
case market.BreakoutLong:
|
|
// Long box breakout: always trigger emergency handling
|
|
return BreakoutActionCloseAll
|
|
default:
|
|
return BreakoutActionNone
|
|
}
|
|
}
|
|
|
|
// shouldRecoverDirection checks if the current grid direction should start recovering toward neutral
|
|
func shouldRecoverDirection(box *market.BoxData, currentDirection market.GridDirection) bool {
|
|
if box == nil || currentDirection == market.GridDirectionNeutral {
|
|
return false
|
|
}
|
|
|
|
price := box.CurrentPrice
|
|
// Check if price is back inside the short box
|
|
return price >= box.ShortLower && price <= box.ShortUpper
|
|
}
|