new best
This commit is contained in:
@@ -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
|
||||
);
|
||||
{
|
||||
let (t1p, t1b, t2p, t2b, t3p, t3b) = self.drawdown_tiers();
|
||||
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,
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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<String, f64>,
|
||||
pub entry_atrs: HashMap<String, f64>,
|
||||
pub entry_prices: HashMap<String, f64>,
|
||||
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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user