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:
tinkle-community
2026-01-17 21:52:34 +08:00
parent 4642671e77
commit 587efba52c
2 changed files with 318 additions and 0 deletions

196
trader/grid_regime.go Normal file
View 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
View 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)
}
})
}
}