diff --git a/trader/grid_regime.go b/trader/grid_regime.go new file mode 100644 index 00000000..e574cc1b --- /dev/null +++ b/trader/grid_regime.go @@ -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 + } +} diff --git a/trader/grid_regime_test.go b/trader/grid_regime_test.go new file mode 100644 index 00000000..25d0753a --- /dev/null +++ b/trader/grid_regime_test.go @@ -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) + } + }) + } +}