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_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)