This commit is contained in:
zastian-dev
2026-02-13 20:04:32 +00:00
parent 79816b9e2e
commit 0e820852fa
3 changed files with 95 additions and 64 deletions

View File

@@ -16,9 +16,13 @@ use crate::config::{
DRAWDOWN_TIER2_PCT, DRAWDOWN_TIER2_BARS, DRAWDOWN_TIER2_PCT, DRAWDOWN_TIER2_BARS,
DRAWDOWN_TIER3_PCT, DRAWDOWN_TIER3_BARS, DRAWDOWN_TIER3_PCT, DRAWDOWN_TIER3_BARS,
DRAWDOWN_TIER3_REQUIRE_BULL, 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, EQUITY_CURVE_SMA_PERIOD,
REGIME_SPY_SYMBOL, REGIME_EMA_SHORT, REGIME_EMA_LONG, REGIME_SPY_SYMBOL, REGIME_EMA_SHORT, REGIME_EMA_LONG,
REGIME_CAUTION_SIZE_FACTOR, REGIME_CAUTION_THRESHOLD_BUMP, 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::indicators::{calculate_all_indicators, calculate_ema, determine_market_regime, generate_signal};
use crate::strategy::Strategy; use crate::strategy::Strategy;
@@ -112,23 +116,41 @@ impl Backtester {
/// ///
/// On resume, the peak is reset to the current portfolio value to prevent /// On resume, the peak is reset to the current portfolio value to prevent
/// cascading re-triggers from the same drawdown event. /// 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) { fn update_drawdown_state(&mut self, portfolio_value: f64) {
if portfolio_value > self.peak_portfolio_value { if portfolio_value > self.peak_portfolio_value {
self.peak_portfolio_value = portfolio_value; self.peak_portfolio_value = portfolio_value;
} }
let drawdown_pct = (self.peak_portfolio_value - portfolio_value) / self.peak_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) // 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 // 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; self.drawdown_requires_bull = DRAWDOWN_TIER3_REQUIRE_BULL;
(DRAWDOWN_TIER3_BARS, "TIER 3 (SEVERE)") (t3_bars, "TIER 3 (SEVERE)")
} else if drawdown_pct >= DRAWDOWN_TIER2_PCT { } else if drawdown_pct >= t2_pct {
(DRAWDOWN_TIER2_BARS, "TIER 2") (t2_bars, "TIER 2")
} else { } else {
(DRAWDOWN_TIER1_BARS, "TIER 1") (t1_bars, "TIER 1")
}; };
tracing::warn!( tracing::warn!(
@@ -145,14 +167,14 @@ impl Backtester {
// Upgrade severity if drawdown deepens while already halted // Upgrade severity if drawdown deepens while already halted
if self.drawdown_halt && drawdown_pct > self.drawdown_halt_severity { 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_requires_bull = DRAWDOWN_TIER3_REQUIRE_BULL;
self.drawdown_halt_start = Some(self.current_bar); // Reset timer for deeper tier self.drawdown_halt_start = Some(self.current_bar); // Reset timer for deeper tier
tracing::warn!( tracing::warn!(
"Drawdown deepened to {:.2}% — UPGRADED to TIER 3. Requires BULL regime.", "Drawdown deepened to {:.2}% — UPGRADED to TIER 3. Requires BULL regime.",
drawdown_pct * 100.0 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); self.drawdown_halt_start = Some(self.current_bar);
tracing::warn!( tracing::warn!(
"Drawdown deepened to {:.2}% — upgraded to TIER 2.", "Drawdown deepened to {:.2}% — upgraded to TIER 2.",
@@ -165,12 +187,12 @@ impl Backtester {
// Auto-resume after time-based cooldown // Auto-resume after time-based cooldown
if self.drawdown_halt { if self.drawdown_halt {
if let Some(halt_start) = self.drawdown_halt_start { if let Some(halt_start) = self.drawdown_halt_start {
let required_bars = if self.drawdown_halt_severity >= DRAWDOWN_TIER3_PCT { let required_bars = if self.drawdown_halt_severity >= t3_pct {
DRAWDOWN_TIER3_BARS t3_bars
} else if self.drawdown_halt_severity >= DRAWDOWN_TIER2_PCT { } else if self.drawdown_halt_severity >= t2_pct {
DRAWDOWN_TIER2_BARS t2_bars
} else { } else {
DRAWDOWN_TIER1_BARS t1_bars
}; };
let time_served = self.current_bar >= halt_start + required_bars; let time_served = self.current_bar >= halt_start + required_bars;
@@ -693,9 +715,16 @@ impl Backtester {
self.current_regime = regime; self.current_regime = regime;
// Regime-based sizing factor and threshold adjustment // Regime-based sizing factor and threshold adjustment
// Use timeframe-specific parameters: hourly needs defensiveness, daily needs aggression
let regime_size_factor = match regime { let regime_size_factor = match regime {
MarketRegime::Bull => 1.0, 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 MarketRegime::Bear => 0.0, // No new longs
}; };
@@ -775,8 +804,15 @@ impl Backtester {
// In Bear regime, skip the entire buy phase (no new longs). // In Bear regime, skip the entire buy phase (no new longs).
if regime.allows_new_longs() { if regime.allows_new_longs() {
// In Caution regime, raise the buy threshold to require stronger signals // 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 { 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, _ => 0.0,
}; };
@@ -1523,15 +1559,13 @@ impl Backtester {
" Max Per Sector: {:>15}", " Max Per Sector: {:>15}",
MAX_SECTOR_POSITIONS MAX_SECTOR_POSITIONS
); );
println!( {
" Drawdown Halt: {:>13.0}%/{:.0}%/{:.0}% ({}/{}/{} bars)", let (t1p, t1b, t2p, t2b, t3p, t3b) = self.drawdown_tiers();
DRAWDOWN_TIER1_PCT * 100.0, println!(
DRAWDOWN_TIER2_PCT * 100.0, " Drawdown Halt: {:>13.0}%/{:.0}%/{:.0}% ({}/{}/{} bars)",
DRAWDOWN_TIER3_PCT * 100.0, t1p * 100.0, t2p * 100.0, t3p * 100.0, t1b, t2b, t3b,
DRAWDOWN_TIER1_BARS, );
DRAWDOWN_TIER2_BARS, }
DRAWDOWN_TIER3_BARS,
);
println!( println!(
" Market Regime Filter: {:>15}", " Market Regime Filter: {:>15}",
format!("SPY EMA-{}/EMA-{}", REGIME_EMA_SHORT, REGIME_EMA_LONG) format!("SPY EMA-{}/EMA-{}", REGIME_EMA_SHORT, REGIME_EMA_LONG)

View File

@@ -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 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_ACTIVATION: f64 = 0.04; // Activate earlier to protect profits
pub const TRAILING_STOP_DISTANCE: f64 = 0.05; // Wider trail to let winners run 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 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_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 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 // Portfolio-level controls
pub const MAX_CONCURRENT_POSITIONS: usize = 8; // Fewer positions = higher conviction per trade pub const MAX_CONCURRENT_POSITIONS: usize = 8; // Fewer positions = higher conviction per trade
pub const MAX_SECTOR_POSITIONS: usize = 2; 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_SPY_SYMBOL: &str = "SPY";
pub const REGIME_EMA_SHORT: usize = 50; // Fast regime EMA pub const REGIME_EMA_SHORT: usize = 50; // Fast regime EMA
pub const REGIME_EMA_LONG: usize = 200; // Slow regime EMA (the "golden cross" line) pub const REGIME_EMA_LONG: usize = 200; // Slow regime EMA (the "golden cross" line)
/// In Caution regime, multiply position size by this factor. /// In Caution regime, multiply position size by this factor (DAILY bars).
/// Reduced from 0.5 to 0.25: the 2022 bear showed Caution still bleeds at 50% size. /// Daily benefits from being more aggressive in Caution (60% size) to capture bull markets.
pub const REGIME_CAUTION_SIZE_FACTOR: f64 = 0.25; pub const REGIME_CAUTION_SIZE_FACTOR: f64 = 0.6;
/// In Caution regime, add this to buy thresholds (require near-StrongBuy signals). /// In Caution regime, add this to buy thresholds (DAILY bars).
pub const REGIME_CAUTION_THRESHOLD_BUMP: f64 = 3.0; /// 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 // 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. // The old fixed 10-bar cooldown is inadequate for real bear markets.
// Scale the halt duration with severity so that deeper drawdowns force // Scale the halt duration with severity so that deeper drawdowns force
// longer cooling periods. At 25%+ DD, also require bull regime to resume. // 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) // Daily drawdown tiers: relaxed to avoid halting on normal 10-15% bull pullbacks
pub const DRAWDOWN_TIER1_BARS: usize = 15; pub const DRAWDOWN_TIER1_PCT: f64 = 0.18; // 18% → 10 bars
pub const DRAWDOWN_TIER2_PCT: f64 = 0.18; // 18% → 40 bars pub const DRAWDOWN_TIER1_BARS: usize = 10;
pub const DRAWDOWN_TIER2_BARS: usize = 40; pub const DRAWDOWN_TIER2_PCT: f64 = 0.25; // 25% → 30 bars
pub const DRAWDOWN_TIER3_PCT: f64 = 0.25; // 25%+ → 60 bars + require bull regime pub const DRAWDOWN_TIER2_BARS: usize = 30;
pub const DRAWDOWN_TIER3_BARS: usize = 60; pub const DRAWDOWN_TIER3_PCT: f64 = 0.35; // 35%+ → 50 bars + require bull
/// If true, after a Tier 3 drawdown (>=25%), require bull market regime pub const DRAWDOWN_TIER3_BARS: usize = 50;
/// before resuming new entries even after the bar cooldown expires. /// If true, after a Tier 3 drawdown, require bull market regime to resume.
pub const DRAWDOWN_TIER3_REQUIRE_BULL: bool = true; 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 // Trailing Equity Curve Stop
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════

View File

@@ -5,7 +5,6 @@ use crate::config::{
ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER, MAX_LOSS_PCT, MAX_POSITION_SIZE, 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, MIN_ATR_PCT, RISK_PER_TRADE, STOP_LOSS_PCT, TIME_EXIT_BARS,
TRAILING_STOP_ACTIVATION, TRAILING_STOP_DISTANCE, TRAILING_STOP_ACTIVATION, TRAILING_STOP_DISTANCE,
HOURLY_ATR_STOP_MULTIPLIER, HOURLY_ATR_TRAIL_MULTIPLIER, HOURLY_ATR_TRAIL_ACTIVATION_MULTIPLIER,
}; };
use crate::types::{Signal, TradeSignal}; use crate::types::{Signal, TradeSignal};
@@ -15,7 +14,6 @@ pub struct Strategy {
pub high_water_marks: HashMap<String, f64>, pub high_water_marks: HashMap<String, f64>,
pub entry_atrs: HashMap<String, f64>, pub entry_atrs: HashMap<String, f64>,
pub entry_prices: HashMap<String, f64>, pub entry_prices: HashMap<String, f64>,
pub timeframe: Timeframe,
} }
impl Strategy { impl Strategy {
@@ -25,7 +23,6 @@ impl Strategy {
high_water_marks: HashMap::new(), high_water_marks: HashMap::new(),
entry_atrs: HashMap::new(), entry_atrs: HashMap::new(),
entry_prices: HashMap::new(), entry_prices: HashMap::new(),
timeframe,
} }
} }
@@ -108,14 +105,8 @@ impl Strategy {
} }
// 2. ATR-based initial stop-loss (primary risk control) // 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 { if entry_atr > 0.0 {
let stop_multiplier = if self.timeframe == Timeframe::Hourly { let atr_stop_price = entry_price - ATR_STOP_MULTIPLIER * entry_atr;
HOURLY_ATR_STOP_MULTIPLIER
} else {
ATR_STOP_MULTIPLIER
};
let atr_stop_price = entry_price - stop_multiplier * entry_atr;
if current_price <= atr_stop_price { if current_price <= atr_stop_price {
return Some(Signal::StrongSell); return Some(Signal::StrongSell);
} }
@@ -125,15 +116,10 @@ impl Strategy {
} }
// 4. ATR-based trailing stop (profit protection) // 4. ATR-based trailing stop (profit protection)
// Hourly uses much tighter trail to lock in gains quickly // Activates earlier than before (1.5x ATR gain) so profits are locked in.
let (trail_activation_mult, trail_mult) = if self.timeframe == Timeframe::Hourly { // Distance is wider (2.5x ATR from HWM) so normal retracements don't trigger it.
(HOURLY_ATR_TRAIL_ACTIVATION_MULTIPLIER, HOURLY_ATR_TRAIL_MULTIPLIER)
} else {
(ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER)
};
let activation_gain = if entry_atr > 0.0 { let activation_gain = if entry_atr > 0.0 {
(trail_activation_mult * entry_atr) / entry_price (ATR_TRAIL_ACTIVATION_MULTIPLIER * entry_atr) / entry_price
} else { } else {
TRAILING_STOP_ACTIVATION TRAILING_STOP_ACTIVATION
}; };
@@ -141,7 +127,7 @@ impl Strategy {
if pnl_pct >= activation_gain { if pnl_pct >= activation_gain {
if let Some(&high_water) = self.high_water_marks.get(symbol) { if let Some(&high_water) = self.high_water_marks.get(symbol) {
let trail_distance = if entry_atr > 0.0 { let trail_distance = if entry_atr > 0.0 {
trail_mult * entry_atr ATR_TRAIL_MULTIPLIER * entry_atr
} else { } else {
high_water * TRAILING_STOP_DISTANCE high_water * TRAILING_STOP_DISTANCE
}; };