diff --git a/src/backtester.rs b/src/backtester.rs index e2e7a19..6bc92c7 100644 --- a/src/backtester.rs +++ b/src/backtester.rs @@ -16,9 +16,13 @@ use crate::config::{ DRAWDOWN_TIER2_PCT, DRAWDOWN_TIER2_BARS, DRAWDOWN_TIER3_PCT, DRAWDOWN_TIER3_BARS, DRAWDOWN_TIER3_REQUIRE_BULL, + HOURLY_DRAWDOWN_TIER1_PCT, HOURLY_DRAWDOWN_TIER1_BARS, + HOURLY_DRAWDOWN_TIER2_PCT, HOURLY_DRAWDOWN_TIER2_BARS, + HOURLY_DRAWDOWN_TIER3_PCT, HOURLY_DRAWDOWN_TIER3_BARS, EQUITY_CURVE_SMA_PERIOD, REGIME_SPY_SYMBOL, REGIME_EMA_SHORT, REGIME_EMA_LONG, REGIME_CAUTION_SIZE_FACTOR, REGIME_CAUTION_THRESHOLD_BUMP, + HOURLY_REGIME_CAUTION_SIZE_FACTOR, HOURLY_REGIME_CAUTION_THRESHOLD_BUMP, }; use crate::indicators::{calculate_all_indicators, calculate_ema, determine_market_regime, generate_signal}; use crate::strategy::Strategy; @@ -112,23 +116,41 @@ impl Backtester { /// /// On resume, the peak is reset to the current portfolio value to prevent /// cascading re-triggers from the same drawdown event. + /// Get the drawdown tier thresholds for the current timeframe. + fn drawdown_tiers(&self) -> (f64, usize, f64, usize, f64, usize) { + if self.timeframe == Timeframe::Hourly { + ( + HOURLY_DRAWDOWN_TIER1_PCT, HOURLY_DRAWDOWN_TIER1_BARS, + HOURLY_DRAWDOWN_TIER2_PCT, HOURLY_DRAWDOWN_TIER2_BARS, + HOURLY_DRAWDOWN_TIER3_PCT, HOURLY_DRAWDOWN_TIER3_BARS, + ) + } else { + ( + DRAWDOWN_TIER1_PCT, DRAWDOWN_TIER1_BARS, + DRAWDOWN_TIER2_PCT, DRAWDOWN_TIER2_BARS, + DRAWDOWN_TIER3_PCT, DRAWDOWN_TIER3_BARS, + ) + } + } + fn update_drawdown_state(&mut self, portfolio_value: f64) { if portfolio_value > self.peak_portfolio_value { self.peak_portfolio_value = portfolio_value; } let drawdown_pct = (self.peak_portfolio_value - portfolio_value) / self.peak_portfolio_value; + let (t1_pct, t1_bars, t2_pct, t2_bars, t3_pct, t3_bars) = self.drawdown_tiers(); // Trigger halt at the lowest tier that matches (if not already halted) - if !self.drawdown_halt && drawdown_pct >= DRAWDOWN_TIER1_PCT { + if !self.drawdown_halt && drawdown_pct >= t1_pct { // Determine severity tier - let (halt_bars, tier_name) = if drawdown_pct >= DRAWDOWN_TIER3_PCT { + let (halt_bars, tier_name) = if drawdown_pct >= t3_pct { self.drawdown_requires_bull = DRAWDOWN_TIER3_REQUIRE_BULL; - (DRAWDOWN_TIER3_BARS, "TIER 3 (SEVERE)") - } else if drawdown_pct >= DRAWDOWN_TIER2_PCT { - (DRAWDOWN_TIER2_BARS, "TIER 2") + (t3_bars, "TIER 3 (SEVERE)") + } else if drawdown_pct >= t2_pct { + (t2_bars, "TIER 2") } else { - (DRAWDOWN_TIER1_BARS, "TIER 1") + (t1_bars, "TIER 1") }; tracing::warn!( @@ -145,14 +167,14 @@ impl Backtester { // Upgrade severity if drawdown deepens while already halted if self.drawdown_halt && drawdown_pct > self.drawdown_halt_severity { - if drawdown_pct >= DRAWDOWN_TIER3_PCT && self.drawdown_halt_severity < DRAWDOWN_TIER3_PCT { + if drawdown_pct >= t3_pct && self.drawdown_halt_severity < t3_pct { self.drawdown_requires_bull = DRAWDOWN_TIER3_REQUIRE_BULL; self.drawdown_halt_start = Some(self.current_bar); // Reset timer for deeper tier tracing::warn!( "Drawdown deepened to {:.2}% — UPGRADED to TIER 3. Requires BULL regime.", drawdown_pct * 100.0 ); - } else if drawdown_pct >= DRAWDOWN_TIER2_PCT && self.drawdown_halt_severity < DRAWDOWN_TIER2_PCT { + } else if drawdown_pct >= t2_pct && self.drawdown_halt_severity < t2_pct { self.drawdown_halt_start = Some(self.current_bar); tracing::warn!( "Drawdown deepened to {:.2}% — upgraded to TIER 2.", @@ -165,12 +187,12 @@ impl Backtester { // Auto-resume after time-based cooldown if self.drawdown_halt { if let Some(halt_start) = self.drawdown_halt_start { - let required_bars = if self.drawdown_halt_severity >= DRAWDOWN_TIER3_PCT { - DRAWDOWN_TIER3_BARS - } else if self.drawdown_halt_severity >= DRAWDOWN_TIER2_PCT { - DRAWDOWN_TIER2_BARS + let required_bars = if self.drawdown_halt_severity >= t3_pct { + t3_bars + } else if self.drawdown_halt_severity >= t2_pct { + t2_bars } else { - DRAWDOWN_TIER1_BARS + t1_bars }; let time_served = self.current_bar >= halt_start + required_bars; @@ -693,9 +715,16 @@ impl Backtester { self.current_regime = regime; // Regime-based sizing factor and threshold adjustment + // Use timeframe-specific parameters: hourly needs defensiveness, daily needs aggression let regime_size_factor = match regime { MarketRegime::Bull => 1.0, - MarketRegime::Caution => REGIME_CAUTION_SIZE_FACTOR, + MarketRegime::Caution => { + if self.timeframe == Timeframe::Hourly { + HOURLY_REGIME_CAUTION_SIZE_FACTOR + } else { + REGIME_CAUTION_SIZE_FACTOR + } + }, MarketRegime::Bear => 0.0, // No new longs }; @@ -775,8 +804,15 @@ impl Backtester { // In Bear regime, skip the entire buy phase (no new longs). if regime.allows_new_longs() { // In Caution regime, raise the buy threshold to require stronger signals + // Use timeframe-specific parameters: hourly needs high bump, daily needs low bump let buy_threshold_bump = match regime { - MarketRegime::Caution => REGIME_CAUTION_THRESHOLD_BUMP, + MarketRegime::Caution => { + if self.timeframe == Timeframe::Hourly { + HOURLY_REGIME_CAUTION_THRESHOLD_BUMP + } else { + REGIME_CAUTION_THRESHOLD_BUMP + } + }, _ => 0.0, }; @@ -1523,15 +1559,13 @@ impl Backtester { " Max Per Sector: {:>15}", MAX_SECTOR_POSITIONS ); - println!( - " Drawdown Halt: {:>13.0}%/{:.0}%/{:.0}% ({}/{}/{} bars)", - DRAWDOWN_TIER1_PCT * 100.0, - DRAWDOWN_TIER2_PCT * 100.0, - DRAWDOWN_TIER3_PCT * 100.0, - DRAWDOWN_TIER1_BARS, - DRAWDOWN_TIER2_BARS, - DRAWDOWN_TIER3_BARS, - ); + { + let (t1p, t1b, t2p, t2b, t3p, t3b) = self.drawdown_tiers(); + println!( + " Drawdown Halt: {:>13.0}%/{:.0}%/{:.0}% ({}/{}/{} bars)", + t1p * 100.0, t2p * 100.0, t3p * 100.0, t1b, t2b, t3b, + ); + } println!( " Market Regime Filter: {:>15}", format!("SPY EMA-{}/EMA-{}", REGIME_EMA_SHORT, REGIME_EMA_LONG) diff --git a/src/config.rs b/src/config.rs index b374c0f..d4baa12 100644 --- a/src/config.rs +++ b/src/config.rs @@ -73,17 +73,11 @@ pub const STOP_LOSS_PCT: f64 = 0.025; pub const MAX_LOSS_PCT: f64 = 0.08; // Gap protection only — ATR stop handles normal exits pub const TRAILING_STOP_ACTIVATION: f64 = 0.04; // Activate earlier to protect profits pub const TRAILING_STOP_DISTANCE: f64 = 0.05; // Wider trail to let winners run -// ATR-based risk management (DAILY timeframe - wider stops for longer-term holds) +// ATR-based risk management pub const RISK_PER_TRADE: f64 = 0.015; // 1.5% risk per trade (8 positions * 1.5% = 12% worst-case) -pub const ATR_STOP_MULTIPLIER: f64 = 3.5; // Wide stops reduce false stop-outs on daily +pub const ATR_STOP_MULTIPLIER: f64 = 3.5; // Wide stops reduce false stop-outs (the #1 loss source) pub const ATR_TRAIL_MULTIPLIER: f64 = 3.0; // Wide trail so winners run longer pub const ATR_TRAIL_ACTIVATION_MULTIPLIER: f64 = 2.0; // Don't activate trail too early - -// ATR-based risk management (HOURLY timeframe - much tighter to prevent 70-90% losses) -// Hourly intraday noise requires stops 40-50% tighter than daily to avoid catastrophic drawdowns -pub const HOURLY_ATR_STOP_MULTIPLIER: f64 = 1.8; // Tight stops prevent -$9k NVDA disasters -pub const HOURLY_ATR_TRAIL_MULTIPLIER: f64 = 1.5; // Tight trail locks in hourly gains quickly -pub const HOURLY_ATR_TRAIL_ACTIVATION_MULTIPLIER: f64 = 1.2; // Activate trail early on hourly // Portfolio-level controls pub const MAX_CONCURRENT_POSITIONS: usize = 8; // Fewer positions = higher conviction per trade pub const MAX_SECTOR_POSITIONS: usize = 2; @@ -108,11 +102,19 @@ pub const RAMPUP_PERIOD_BARS: usize = 15; // Faster ramp-up pub const REGIME_SPY_SYMBOL: &str = "SPY"; pub const REGIME_EMA_SHORT: usize = 50; // Fast regime EMA pub const REGIME_EMA_LONG: usize = 200; // Slow regime EMA (the "golden cross" line) -/// In Caution regime, multiply position size by this factor. -/// Reduced from 0.5 to 0.25: the 2022 bear showed Caution still bleeds at 50% size. -pub const REGIME_CAUTION_SIZE_FACTOR: f64 = 0.25; -/// In Caution regime, add this to buy thresholds (require near-StrongBuy signals). -pub const REGIME_CAUTION_THRESHOLD_BUMP: f64 = 3.0; +/// In Caution regime, multiply position size by this factor (DAILY bars). +/// Daily benefits from being more aggressive in Caution (60% size) to capture bull markets. +pub const REGIME_CAUTION_SIZE_FACTOR: f64 = 0.6; +/// In Caution regime, add this to buy thresholds (DAILY bars). +/// Daily needs lower bump (1.0) to participate in bull rallies. +pub const REGIME_CAUTION_THRESHOLD_BUMP: f64 = 1.0; + +/// In Caution regime, multiply position size by this factor (HOURLY bars). +/// Hourly needs to be very defensive (25% size) due to intraday noise. +pub const HOURLY_REGIME_CAUTION_SIZE_FACTOR: f64 = 0.25; +/// In Caution regime, add this to buy thresholds (HOURLY bars). +/// Hourly needs high bump (3.0) to avoid whipsaws. +pub const HOURLY_REGIME_CAUTION_THRESHOLD_BUMP: f64 = 3.0; // ═══════════════════════════════════════════════════════════════════════ // Scaled Drawdown Circuit Breaker @@ -120,16 +122,25 @@ pub const REGIME_CAUTION_THRESHOLD_BUMP: f64 = 3.0; // The old fixed 10-bar cooldown is inadequate for real bear markets. // Scale the halt duration with severity so that deeper drawdowns force // longer cooling periods. At 25%+ DD, also require bull regime to resume. -pub const DRAWDOWN_TIER1_PCT: f64 = 0.12; // 12% → 15 bars (catch earlier) -pub const DRAWDOWN_TIER1_BARS: usize = 15; -pub const DRAWDOWN_TIER2_PCT: f64 = 0.18; // 18% → 40 bars -pub const DRAWDOWN_TIER2_BARS: usize = 40; -pub const DRAWDOWN_TIER3_PCT: f64 = 0.25; // 25%+ → 60 bars + require bull regime -pub const DRAWDOWN_TIER3_BARS: usize = 60; -/// If true, after a Tier 3 drawdown (>=25%), require bull market regime -/// before resuming new entries even after the bar cooldown expires. +// Daily drawdown tiers: relaxed to avoid halting on normal 10-15% bull pullbacks +pub const DRAWDOWN_TIER1_PCT: f64 = 0.18; // 18% → 10 bars +pub const DRAWDOWN_TIER1_BARS: usize = 10; +pub const DRAWDOWN_TIER2_PCT: f64 = 0.25; // 25% → 30 bars +pub const DRAWDOWN_TIER2_BARS: usize = 30; +pub const DRAWDOWN_TIER3_PCT: f64 = 0.35; // 35%+ → 50 bars + require bull +pub const DRAWDOWN_TIER3_BARS: usize = 50; +/// If true, after a Tier 3 drawdown, require bull market regime to resume. pub const DRAWDOWN_TIER3_REQUIRE_BULL: bool = true; +// Hourly drawdown tiers: tighter because hourly has more whipsaw exposure +// and the bot needs to cut losses faster to preserve capital in bear periods. +pub const HOURLY_DRAWDOWN_TIER1_PCT: f64 = 0.12; // 12% → 15 bars +pub const HOURLY_DRAWDOWN_TIER1_BARS: usize = 15; +pub const HOURLY_DRAWDOWN_TIER2_PCT: f64 = 0.18; // 18% → 40 bars +pub const HOURLY_DRAWDOWN_TIER2_BARS: usize = 40; +pub const HOURLY_DRAWDOWN_TIER3_PCT: f64 = 0.25; // 25%+ → 60 bars + require bull +pub const HOURLY_DRAWDOWN_TIER3_BARS: usize = 60; + // ═══════════════════════════════════════════════════════════════════════ // Trailing Equity Curve Stop // ═══════════════════════════════════════════════════════════════════════ diff --git a/src/strategy.rs b/src/strategy.rs index c2c2b84..5d39057 100644 --- a/src/strategy.rs +++ b/src/strategy.rs @@ -5,7 +5,6 @@ use crate::config::{ ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER, MAX_LOSS_PCT, MAX_POSITION_SIZE, MIN_ATR_PCT, RISK_PER_TRADE, STOP_LOSS_PCT, TIME_EXIT_BARS, TRAILING_STOP_ACTIVATION, TRAILING_STOP_DISTANCE, - HOURLY_ATR_STOP_MULTIPLIER, HOURLY_ATR_TRAIL_MULTIPLIER, HOURLY_ATR_TRAIL_ACTIVATION_MULTIPLIER, }; use crate::types::{Signal, TradeSignal}; @@ -15,7 +14,6 @@ pub struct Strategy { pub high_water_marks: HashMap, pub entry_atrs: HashMap, pub entry_prices: HashMap, - pub timeframe: Timeframe, } impl Strategy { @@ -25,7 +23,6 @@ impl Strategy { high_water_marks: HashMap::new(), entry_atrs: HashMap::new(), entry_prices: HashMap::new(), - timeframe, } } @@ -108,14 +105,8 @@ impl Strategy { } // 2. ATR-based initial stop-loss (primary risk control) - // Use tighter stops for hourly to prevent catastrophic 70-90% losses if entry_atr > 0.0 { - let stop_multiplier = if self.timeframe == Timeframe::Hourly { - HOURLY_ATR_STOP_MULTIPLIER - } else { - ATR_STOP_MULTIPLIER - }; - let atr_stop_price = entry_price - stop_multiplier * entry_atr; + let atr_stop_price = entry_price - ATR_STOP_MULTIPLIER * entry_atr; if current_price <= atr_stop_price { return Some(Signal::StrongSell); } @@ -125,15 +116,10 @@ impl Strategy { } // 4. ATR-based trailing stop (profit protection) - // Hourly uses much tighter trail to lock in gains quickly - let (trail_activation_mult, trail_mult) = if self.timeframe == Timeframe::Hourly { - (HOURLY_ATR_TRAIL_ACTIVATION_MULTIPLIER, HOURLY_ATR_TRAIL_MULTIPLIER) - } else { - (ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER) - }; - + // Activates earlier than before (1.5x ATR gain) so profits are locked in. + // Distance is wider (2.5x ATR from HWM) so normal retracements don't trigger it. let activation_gain = if entry_atr > 0.0 { - (trail_activation_mult * entry_atr) / entry_price + (ATR_TRAIL_ACTIVATION_MULTIPLIER * entry_atr) / entry_price } else { TRAILING_STOP_ACTIVATION }; @@ -141,7 +127,7 @@ impl Strategy { if pnl_pct >= activation_gain { if let Some(&high_water) = self.high_water_marks.get(symbol) { let trail_distance = if entry_atr > 0.0 { - trail_mult * entry_atr + ATR_TRAIL_MULTIPLIER * entry_atr } else { high_water * TRAILING_STOP_DISTANCE };