mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +08:00
feat(trader): add regime classification and breakout detection
Implements Tasks 6-9 for grid market regime awareness: - Task 6: classifyRegimeLevel with Bollinger/ATR thresholds - Task 7: detectBoxBreakout for multi-period box breakouts - Task 8: confirmBreakout with 3-candle confirmation logic - Task 9: getBreakoutAction mapping breakout levels to actions
This commit is contained in:
196
trader/grid_regime.go
Normal file
196
trader/grid_regime.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
122
trader/grid_regime_test.go
Normal file
122
trader/grid_regime_test.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package trader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"nofx/market"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClassifyRegimeLevel(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
bollingerWidth float64
|
||||||
|
atr14Pct float64
|
||||||
|
expected market.RegimeLevel
|
||||||
|
}{
|
||||||
|
{"narrow", 1.5, 0.8, market.RegimeLevelNarrow},
|
||||||
|
{"standard", 2.5, 1.5, market.RegimeLevelStandard},
|
||||||
|
{"wide", 3.5, 2.5, market.RegimeLevelWide},
|
||||||
|
{"volatile", 5.0, 4.0, market.RegimeLevelVolatile},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := classifyRegimeLevel(tt.bollingerWidth, tt.atr14Pct)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectBoxBreakout(t *testing.T) {
|
||||||
|
box := &market.BoxData{
|
||||||
|
ShortUpper: 100,
|
||||||
|
ShortLower: 90,
|
||||||
|
MidUpper: 105,
|
||||||
|
MidLower: 85,
|
||||||
|
LongUpper: 110,
|
||||||
|
LongLower: 80,
|
||||||
|
CurrentPrice: 95,
|
||||||
|
}
|
||||||
|
|
||||||
|
// No breakout
|
||||||
|
level, direction := detectBoxBreakout(box)
|
||||||
|
if level != market.BreakoutNone {
|
||||||
|
t.Errorf("Expected no breakout, got %v", level)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short breakout up
|
||||||
|
box.CurrentPrice = 101
|
||||||
|
level, direction = detectBoxBreakout(box)
|
||||||
|
if level != market.BreakoutShort || direction != "up" {
|
||||||
|
t.Errorf("Expected short breakout up, got %v %v", level, direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mid breakout down
|
||||||
|
box.CurrentPrice = 84
|
||||||
|
level, direction = detectBoxBreakout(box)
|
||||||
|
if level != market.BreakoutMid || direction != "down" {
|
||||||
|
t.Errorf("Expected mid breakout down, got %v %v", level, direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Long breakout up
|
||||||
|
box.CurrentPrice = 112
|
||||||
|
level, direction = detectBoxBreakout(box)
|
||||||
|
if level != market.BreakoutLong || direction != "up" {
|
||||||
|
t.Errorf("Expected long breakout up, got %v %v", level, direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBreakoutConfirmation(t *testing.T) {
|
||||||
|
state := &BreakoutState{
|
||||||
|
Level: market.BreakoutNone,
|
||||||
|
Direction: "",
|
||||||
|
ConfirmCount: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// First detection
|
||||||
|
confirmed := confirmBreakout(state, market.BreakoutShort, "up")
|
||||||
|
if confirmed || state.ConfirmCount != 1 {
|
||||||
|
t.Errorf("Expected not confirmed, count=1, got confirmed=%v count=%d", confirmed, state.ConfirmCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second confirmation
|
||||||
|
confirmed = confirmBreakout(state, market.BreakoutShort, "up")
|
||||||
|
if confirmed || state.ConfirmCount != 2 {
|
||||||
|
t.Errorf("Expected not confirmed, count=2, got confirmed=%v count=%d", confirmed, state.ConfirmCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third confirmation - should confirm
|
||||||
|
confirmed = confirmBreakout(state, market.BreakoutShort, "up")
|
||||||
|
if !confirmed || state.ConfirmCount != 3 {
|
||||||
|
t.Errorf("Expected confirmed, count=3, got confirmed=%v count=%d", confirmed, state.ConfirmCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset on price return
|
||||||
|
state.ConfirmCount = 2
|
||||||
|
confirmed = confirmBreakout(state, market.BreakoutNone, "")
|
||||||
|
if state.ConfirmCount != 0 {
|
||||||
|
t.Errorf("Expected count reset to 0, got %d", state.ConfirmCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBreakoutAction(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
level market.BreakoutLevel
|
||||||
|
expected BreakoutAction
|
||||||
|
}{
|
||||||
|
{market.BreakoutNone, BreakoutActionNone},
|
||||||
|
{market.BreakoutShort, BreakoutActionReducePosition},
|
||||||
|
{market.BreakoutMid, BreakoutActionPauseGrid},
|
||||||
|
{market.BreakoutLong, BreakoutActionCloseAll},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(string(tt.level), func(t *testing.T) {
|
||||||
|
action := getBreakoutAction(tt.level)
|
||||||
|
if action != tt.expected {
|
||||||
|
t.Errorf("Expected %v, got %v", tt.expected, action)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user