just a checkpoint
This commit is contained in:
@@ -7,16 +7,23 @@ use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use crate::alpaca::{fetch_backtest_data, AlpacaClient};
|
||||
use crate::config::{
|
||||
get_all_symbols, get_sector, Timeframe, ATR_STOP_MULTIPLIER,
|
||||
ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER, DRAWDOWN_HALT_BARS, HOURS_PER_DAY,
|
||||
MAX_CONCURRENT_POSITIONS, MAX_DRAWDOWN_HALT, MAX_LOSS_PCT, MAX_POSITION_SIZE,
|
||||
ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER, HOURS_PER_DAY,
|
||||
MAX_CONCURRENT_POSITIONS, MAX_LOSS_PCT, MAX_POSITION_SIZE,
|
||||
MAX_SECTOR_POSITIONS, MIN_CASH_RESERVE, RAMPUP_PERIOD_BARS,
|
||||
REENTRY_COOLDOWN_BARS, SLIPPAGE_BPS, TIME_EXIT_BARS,
|
||||
TOP_MOMENTUM_COUNT, TRADING_DAYS_PER_YEAR,
|
||||
DRAWDOWN_TIER1_PCT, DRAWDOWN_TIER1_BARS,
|
||||
DRAWDOWN_TIER2_PCT, DRAWDOWN_TIER2_BARS,
|
||||
DRAWDOWN_TIER3_PCT, DRAWDOWN_TIER3_BARS,
|
||||
DRAWDOWN_TIER3_REQUIRE_BULL,
|
||||
EQUITY_CURVE_SMA_PERIOD,
|
||||
REGIME_SPY_SYMBOL, REGIME_EMA_SHORT, REGIME_EMA_LONG,
|
||||
REGIME_CAUTION_SIZE_FACTOR, REGIME_CAUTION_THRESHOLD_BUMP,
|
||||
};
|
||||
use crate::indicators::{calculate_all_indicators, generate_signal};
|
||||
use crate::indicators::{calculate_all_indicators, calculate_ema, determine_market_regime, generate_signal};
|
||||
use crate::strategy::Strategy;
|
||||
use crate::types::{
|
||||
BacktestPosition, BacktestResult, EquityPoint, IndicatorRow, Signal, Trade, TradeSignal,
|
||||
BacktestPosition, BacktestResult, EquityPoint, IndicatorRow, MarketRegime, Signal, Trade, TradeSignal,
|
||||
};
|
||||
|
||||
/// Backtesting engine for the trading strategy.
|
||||
@@ -30,6 +37,12 @@ pub struct Backtester {
|
||||
drawdown_halt: bool,
|
||||
/// Bar index when drawdown halt started (for time-based resume)
|
||||
drawdown_halt_start: Option<usize>,
|
||||
/// The drawdown severity that triggered the current halt (for scaled cooldowns)
|
||||
drawdown_halt_severity: f64,
|
||||
/// Current market regime (from SPY analysis)
|
||||
current_regime: MarketRegime,
|
||||
/// Whether the drawdown halt requires bull regime to resume (Tier 3)
|
||||
drawdown_requires_bull: bool,
|
||||
strategy: Strategy,
|
||||
timeframe: Timeframe,
|
||||
/// Current bar index in the simulation
|
||||
@@ -57,6 +70,9 @@ impl Backtester {
|
||||
peak_portfolio_value: initial_capital,
|
||||
drawdown_halt: false,
|
||||
drawdown_halt_start: None,
|
||||
drawdown_halt_severity: 0.0,
|
||||
current_regime: MarketRegime::Bull,
|
||||
drawdown_requires_bull: false,
|
||||
strategy: Strategy::new(timeframe),
|
||||
timeframe,
|
||||
current_bar: 0,
|
||||
@@ -87,12 +103,15 @@ impl Backtester {
|
||||
self.cash + positions_value
|
||||
}
|
||||
|
||||
/// Update drawdown circuit breaker state.
|
||||
/// Uses time-based halt: pause for DRAWDOWN_HALT_BARS after trigger, then auto-resume.
|
||||
/// On resume, the peak is reset to the current portfolio value to prevent cascading
|
||||
/// re-triggers from the same drawdown event. Without this reset, a partial recovery
|
||||
/// followed by a minor dip re-triggers the halt, causing the bot to spend excessive
|
||||
/// time in cash (observed: 7+ triggers in a 3-year backtest = ~140 bars lost).
|
||||
/// Update drawdown circuit breaker state with scaled cooldowns.
|
||||
///
|
||||
/// Drawdown severity determines halt duration:
|
||||
/// - Tier 1 (15%): 10 bars — normal correction
|
||||
/// - Tier 2 (20%): 30 bars — significant bear market
|
||||
/// - Tier 3 (25%+): 50 bars + require bull regime — severe bear (COVID, 2022)
|
||||
///
|
||||
/// On resume, the peak is reset to the current portfolio value to prevent
|
||||
/// cascading re-triggers from the same drawdown event.
|
||||
fn update_drawdown_state(&mut self, portfolio_value: f64) {
|
||||
if portfolio_value > self.peak_portfolio_value {
|
||||
self.peak_portfolio_value = portfolio_value;
|
||||
@@ -100,36 +119,91 @@ impl Backtester {
|
||||
|
||||
let drawdown_pct = (self.peak_portfolio_value - portfolio_value) / self.peak_portfolio_value;
|
||||
|
||||
// Trigger halt if drawdown exceeds threshold
|
||||
if drawdown_pct >= MAX_DRAWDOWN_HALT && !self.drawdown_halt {
|
||||
// Trigger halt at the lowest tier that matches (if not already halted)
|
||||
if !self.drawdown_halt && drawdown_pct >= DRAWDOWN_TIER1_PCT {
|
||||
// Determine severity tier
|
||||
let (halt_bars, tier_name) = if drawdown_pct >= DRAWDOWN_TIER3_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")
|
||||
} else {
|
||||
(DRAWDOWN_TIER1_BARS, "TIER 1")
|
||||
};
|
||||
|
||||
tracing::warn!(
|
||||
"DRAWDOWN CIRCUIT BREAKER: {:.2}% drawdown exceeds {:.0}% limit. Halting for {} bars.",
|
||||
"DRAWDOWN CIRCUIT BREAKER {}: {:.2}% drawdown. Halting for {} bars.{}",
|
||||
tier_name,
|
||||
drawdown_pct * 100.0,
|
||||
MAX_DRAWDOWN_HALT * 100.0,
|
||||
DRAWDOWN_HALT_BARS
|
||||
halt_bars,
|
||||
if self.drawdown_requires_bull { " Requires BULL regime to resume." } else { "" }
|
||||
);
|
||||
self.drawdown_halt = true;
|
||||
self.drawdown_halt_start = Some(self.current_bar);
|
||||
self.drawdown_halt_severity = drawdown_pct;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
self.drawdown_halt_start = Some(self.current_bar);
|
||||
tracing::warn!(
|
||||
"Drawdown deepened to {:.2}% — upgraded to TIER 2.",
|
||||
drawdown_pct * 100.0
|
||||
);
|
||||
}
|
||||
self.drawdown_halt_severity = drawdown_pct;
|
||||
}
|
||||
|
||||
// Auto-resume after time-based cooldown
|
||||
if self.drawdown_halt {
|
||||
if let Some(halt_start) = self.drawdown_halt_start {
|
||||
if self.current_bar >= halt_start + DRAWDOWN_HALT_BARS {
|
||||
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
|
||||
} else {
|
||||
DRAWDOWN_TIER1_BARS
|
||||
};
|
||||
|
||||
let time_served = self.current_bar >= halt_start + required_bars;
|
||||
let regime_ok = if self.drawdown_requires_bull {
|
||||
self.current_regime == MarketRegime::Bull
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if time_served && regime_ok {
|
||||
tracing::info!(
|
||||
"Drawdown halt expired after {} bars. Resuming trading. \
|
||||
"Drawdown halt expired after {} bars (regime: {}). \
|
||||
Peak reset from ${:.2} to ${:.2} (was {:.2}% drawdown).",
|
||||
DRAWDOWN_HALT_BARS,
|
||||
required_bars,
|
||||
self.current_regime.as_str(),
|
||||
self.peak_portfolio_value,
|
||||
portfolio_value,
|
||||
drawdown_pct * 100.0
|
||||
);
|
||||
self.drawdown_halt = false;
|
||||
self.drawdown_halt_start = None;
|
||||
// Reset peak to current value to prevent cascading re-triggers.
|
||||
// The previous peak is no longer relevant after a halt — measuring
|
||||
// drawdown from it would immediately re-trigger on any minor dip.
|
||||
self.drawdown_halt_severity = 0.0;
|
||||
self.drawdown_requires_bull = false;
|
||||
self.peak_portfolio_value = portfolio_value;
|
||||
} else if time_served && !regime_ok {
|
||||
// Log periodically that we're waiting for bull regime
|
||||
if self.current_bar % 50 == 0 {
|
||||
tracing::info!(
|
||||
"Drawdown halt: time served but waiting for BULL regime (currently {}). DD: {:.2}%",
|
||||
self.current_regime.as_str(),
|
||||
drawdown_pct * 100.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +214,9 @@ impl Backtester {
|
||||
/// For hourly timeframe, entries are blocked in the last 2 hours of the
|
||||
/// trading day to avoid creating positions that might need same-day
|
||||
/// stop-loss exits (PDT prevention at entry rather than blocking exits).
|
||||
///
|
||||
/// The `regime_size_factor` parameter scales position size based on the
|
||||
/// current market regime (1.0 for bull, 0.5 for caution, 0.0 for bear).
|
||||
fn execute_buy(
|
||||
&mut self,
|
||||
symbol: &str,
|
||||
@@ -147,19 +224,20 @@ impl Backtester {
|
||||
timestamp: DateTime<Utc>,
|
||||
portfolio_value: f64,
|
||||
signal: &TradeSignal,
|
||||
regime_size_factor: f64,
|
||||
) -> bool {
|
||||
if self.positions.contains_key(symbol) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Market regime gate: no new longs in bear market
|
||||
if regime_size_factor <= 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// PDT-safe entry: on hourly, avoid buying in the last 2 hours of the day.
|
||||
// This prevents positions that might need a same-day stop-loss exit.
|
||||
// Market hours are roughly 9:30-16:00 ET; avoid entries after 14:00 ET.
|
||||
if self.timeframe == Timeframe::Hourly {
|
||||
let hour = timestamp.hour();
|
||||
// IEX timestamps are in UTC; ET = UTC-5 in winter, UTC-4 in summer.
|
||||
// 14:00 ET = 19:00 UTC (winter) or 18:00 UTC (summer).
|
||||
// Conservative: block entries after 19:00 UTC (covers both).
|
||||
if hour >= 19 {
|
||||
return false;
|
||||
}
|
||||
@@ -168,7 +246,7 @@ impl Backtester {
|
||||
// Cooldown guard: prevent whipsaw re-entry after stop-loss
|
||||
if let Some(&cooldown_until) = self.cooldown_timers.get(symbol) {
|
||||
if self.current_bar < cooldown_until {
|
||||
return false; // Still in cooldown period
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +255,11 @@ impl Backtester {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Equity curve SMA stop REMOVED: it creates a pathological feedback loop
|
||||
// where losing positions drag equity below the SMA, blocking new entries,
|
||||
// which prevents recovery. The SPY regime filter and drawdown circuit
|
||||
// breaker handle macro risk without this self-reinforcing trap.
|
||||
|
||||
if self.positions.len() >= MAX_CONCURRENT_POSITIONS {
|
||||
return false;
|
||||
}
|
||||
@@ -196,9 +279,15 @@ impl Backtester {
|
||||
}
|
||||
|
||||
let available_cash = self.cash - (portfolio_value * MIN_CASH_RESERVE);
|
||||
let shares =
|
||||
let mut shares =
|
||||
self.strategy
|
||||
.calculate_position_size(price, portfolio_value, available_cash, signal);
|
||||
|
||||
// Apply regime-based size adjustment (e.g., 50% in Caution)
|
||||
shares *= regime_size_factor;
|
||||
// Re-truncate to 4 decimal places after adjustment
|
||||
shares = (shares * 10000.0).floor() / 10000.0;
|
||||
|
||||
if shares <= 0.0 {
|
||||
return false;
|
||||
}
|
||||
@@ -214,7 +303,7 @@ impl Backtester {
|
||||
symbol.to_string(),
|
||||
BacktestPosition {
|
||||
symbol: symbol.to_string(),
|
||||
shares: shares,
|
||||
shares,
|
||||
entry_price: fill_price,
|
||||
entry_time: timestamp,
|
||||
entry_atr: signal.atr,
|
||||
@@ -229,7 +318,7 @@ impl Backtester {
|
||||
self.trades.push(Trade {
|
||||
symbol: symbol.to_string(),
|
||||
side: "BUY".to_string(),
|
||||
shares: shares,
|
||||
shares,
|
||||
price: fill_price,
|
||||
timestamp,
|
||||
pnl: 0.0,
|
||||
@@ -444,6 +533,32 @@ impl Backtester {
|
||||
data.insert(symbol.clone(), indicators);
|
||||
}
|
||||
|
||||
// Pre-compute SPY regime EMAs for the entire backtest period.
|
||||
// We use the raw SPY close prices to compute EMA-50 and EMA-200
|
||||
// independently of the per-symbol indicator params (regime uses
|
||||
// fixed periods regardless of hourly/daily timeframe scaling).
|
||||
let spy_key = REGIME_SPY_SYMBOL.to_string();
|
||||
let spy_ema50_series: Vec<f64>;
|
||||
let spy_ema200_series: Vec<f64>;
|
||||
let has_spy_data = raw_data.contains_key(&spy_key);
|
||||
|
||||
if has_spy_data {
|
||||
let spy_closes: Vec<f64> = raw_data[&spy_key].iter().map(|b| b.close).collect();
|
||||
spy_ema50_series = calculate_ema(&spy_closes, REGIME_EMA_SHORT);
|
||||
spy_ema200_series = calculate_ema(&spy_closes, REGIME_EMA_LONG);
|
||||
tracing::info!(
|
||||
"SPY regime filter: EMA-{} / EMA-{} ({} bars of SPY data)",
|
||||
REGIME_EMA_SHORT, REGIME_EMA_LONG, spy_closes.len()
|
||||
);
|
||||
} else {
|
||||
spy_ema50_series = vec![];
|
||||
spy_ema200_series = vec![];
|
||||
tracing::warn!(
|
||||
"SPY data not available — market regime filter DISABLED. \
|
||||
All bars will be treated as BULL regime."
|
||||
);
|
||||
}
|
||||
|
||||
// Get common date range
|
||||
let mut all_dates: BTreeMap<DateTime<Utc>, HashSet<String>> = BTreeMap::new();
|
||||
for (symbol, rows) in &data {
|
||||
@@ -508,6 +623,18 @@ impl Backtester {
|
||||
symbol_date_index.insert(symbol.clone(), idx_map);
|
||||
}
|
||||
|
||||
// Build SPY raw bar index (maps timestamp → index into raw_data["SPY"])
|
||||
// so we can look up the pre-computed EMA-50/200 at each trading date.
|
||||
let spy_raw_date_index: HashMap<DateTime<Utc>, usize> = if has_spy_data {
|
||||
raw_data[&spy_key]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, bar)| (bar.timestamp, i))
|
||||
.collect()
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
// Main simulation loop
|
||||
for (day_num, current_date) in trading_dates.iter().enumerate() {
|
||||
self.current_bar = day_num;
|
||||
@@ -532,6 +659,54 @@ impl Backtester {
|
||||
|
||||
let portfolio_value = self.get_portfolio_value(¤t_prices);
|
||||
|
||||
// ── SPY Market Regime Detection ─────────────────────────
|
||||
// Determine if we're in bull/caution/bear based on SPY EMAs.
|
||||
// This gates all new long entries and adjusts position sizing.
|
||||
let regime = if has_spy_data {
|
||||
if let (Some(&spy_raw_idx), Some(spy_indicator_row)) = (
|
||||
spy_raw_date_index.get(current_date),
|
||||
data.get(&spy_key)
|
||||
.and_then(|rows| {
|
||||
symbol_date_index
|
||||
.get(&spy_key)
|
||||
.and_then(|m| m.get(current_date))
|
||||
.map(|&i| &rows[i])
|
||||
}),
|
||||
) {
|
||||
let ema50 = if spy_raw_idx < spy_ema50_series.len() {
|
||||
spy_ema50_series[spy_raw_idx]
|
||||
} else {
|
||||
f64::NAN
|
||||
};
|
||||
let ema200 = if spy_raw_idx < spy_ema200_series.len() {
|
||||
spy_ema200_series[spy_raw_idx]
|
||||
} else {
|
||||
f64::NAN
|
||||
};
|
||||
determine_market_regime(spy_indicator_row, ema50, ema200)
|
||||
} else {
|
||||
MarketRegime::Caution // No SPY data for this bar
|
||||
}
|
||||
} else {
|
||||
MarketRegime::Bull // No SPY data at all, don't penalize
|
||||
};
|
||||
self.current_regime = regime;
|
||||
|
||||
// Regime-based sizing factor and threshold adjustment
|
||||
let regime_size_factor = match regime {
|
||||
MarketRegime::Bull => 1.0,
|
||||
MarketRegime::Caution => REGIME_CAUTION_SIZE_FACTOR,
|
||||
MarketRegime::Bear => 0.0, // No new longs
|
||||
};
|
||||
|
||||
// Log regime changes (only on transitions)
|
||||
if day_num == 0 || (day_num > 0 && regime != self.current_regime) {
|
||||
// Already set above, but log on first bar
|
||||
}
|
||||
if day_num % 100 == 0 {
|
||||
tracing::info!(" Market regime: {} (SPY)", regime.as_str());
|
||||
}
|
||||
|
||||
// Update drawdown circuit breaker
|
||||
self.update_drawdown_state(portfolio_value);
|
||||
|
||||
@@ -597,47 +772,75 @@ impl Backtester {
|
||||
}
|
||||
|
||||
// Phase 2: Process buys (only for top momentum stocks)
|
||||
for symbol in &ranked_symbols {
|
||||
let rows = match data.get(symbol) {
|
||||
Some(r) => r,
|
||||
None => continue,
|
||||
// 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
|
||||
let buy_threshold_bump = match regime {
|
||||
MarketRegime::Caution => REGIME_CAUTION_THRESHOLD_BUMP,
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
// Only buy top momentum stocks
|
||||
if !top_momentum_symbols.contains(symbol) {
|
||||
continue;
|
||||
}
|
||||
for symbol in &ranked_symbols {
|
||||
// Don't buy SPY itself — it's used as the regime benchmark
|
||||
if symbol == REGIME_SPY_SYMBOL {
|
||||
continue;
|
||||
}
|
||||
|
||||
let idx = match symbol_date_index
|
||||
.get(symbol)
|
||||
.and_then(|m| m.get(current_date))
|
||||
{
|
||||
Some(&i) => i,
|
||||
None => continue,
|
||||
};
|
||||
let rows = match data.get(symbol) {
|
||||
Some(r) => r,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if idx < 1 {
|
||||
continue;
|
||||
}
|
||||
// Only buy top momentum stocks
|
||||
if !top_momentum_symbols.contains(symbol) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let current_row = &rows[idx];
|
||||
let previous_row = &rows[idx - 1];
|
||||
let idx = match symbol_date_index
|
||||
.get(symbol)
|
||||
.and_then(|m| m.get(current_date))
|
||||
{
|
||||
Some(&i) => i,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if current_row.rsi.is_nan() || current_row.macd.is_nan() {
|
||||
continue;
|
||||
}
|
||||
if idx < 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let signal = generate_signal(symbol, current_row, previous_row);
|
||||
let current_row = &rows[idx];
|
||||
let previous_row = &rows[idx - 1];
|
||||
|
||||
// Execute buys
|
||||
if signal.signal.is_buy() {
|
||||
self.execute_buy(
|
||||
symbol,
|
||||
signal.current_price,
|
||||
*current_date,
|
||||
portfolio_value,
|
||||
&signal,
|
||||
);
|
||||
if current_row.rsi.is_nan() || current_row.macd.is_nan() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let signal = generate_signal(symbol, current_row, previous_row);
|
||||
|
||||
// Apply regime threshold bump: in Caution, require stronger conviction
|
||||
let effective_buy = if buy_threshold_bump > 0.0 {
|
||||
// Re-evaluate: the signal score is buy_score - sell_score.
|
||||
// We need to check if the score exceeds the bumped threshold.
|
||||
// Since we don't have the raw score, use confidence as a proxy:
|
||||
// confidence = score.abs() / 12.0, so score = confidence * 12.0
|
||||
// A Buy signal means score >= 4.0 (our threshold).
|
||||
// In Caution, we require score >= 4.0 + bump (6.0 → StrongBuy territory).
|
||||
let approx_score = signal.confidence * 12.0;
|
||||
approx_score >= (4.0 + buy_threshold_bump) && signal.signal.is_buy()
|
||||
} else {
|
||||
signal.signal.is_buy()
|
||||
};
|
||||
|
||||
if effective_buy {
|
||||
self.execute_buy(
|
||||
symbol,
|
||||
signal.current_price,
|
||||
*current_date,
|
||||
portfolio_value,
|
||||
&signal,
|
||||
regime_size_factor,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -899,9 +1102,21 @@ impl Backtester {
|
||||
MAX_SECTOR_POSITIONS
|
||||
);
|
||||
println!(
|
||||
" Drawdown Halt: {:>14.0}% ({} bar cooldown)",
|
||||
MAX_DRAWDOWN_HALT * 100.0,
|
||||
DRAWDOWN_HALT_BARS
|
||||
" 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,
|
||||
);
|
||||
println!(
|
||||
" Market Regime Filter: {:>15}",
|
||||
format!("SPY EMA-{}/EMA-{}", REGIME_EMA_SHORT, REGIME_EMA_LONG)
|
||||
);
|
||||
println!(
|
||||
" Equity Curve Stop: {:>13}-bar SMA",
|
||||
EQUITY_CURVE_SMA_PERIOD
|
||||
);
|
||||
println!(
|
||||
" Time Exit: {:>13} bars",
|
||||
|
||||
Reference in New Issue
Block a user