//! Configuration constants for the trading bot. // Stock Universe (~100 symbols across 14 sectors) pub const MAG7: &[&str] = &["AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA"]; pub const SEMIS: &[&str] = &["AVGO", "AMD", "ASML", "QCOM", "MU", "MRVL", "LRCX", "KLAC", "AMAT"]; pub const GROWTH_TECH: &[&str] = &["NFLX", "CRM", "NOW", "UBER", "SNOW", "DDOG", "CRWD", "ZS", "WDAY"]; pub const SOFTWARE: &[&str] = &["ADBE", "INTU", "PANW", "FTNT", "TEAM", "HUBS", "MNDY"]; pub const HEALTHCARE: &[&str] = &["LLY", "UNH", "ISRG", "VRTX", "ABBV", "MRK", "PFE", "TMO", "ABT", "DHR"]; pub const BIOTECH: &[&str] = &["GILD", "AMGN", "REGN", "BIIB", "MRNA"]; pub const FINTECH_VOLATILE: &[&str] = &["V", "MA", "COIN", "PLTR", "MSTR", "SQ", "PYPL"]; pub const SP500_FINANCIALS: &[&str] = &["JPM", "GS", "MS", "BLK", "AXP", "C", "SCHW", "ICE"]; pub const SP500_INDUSTRIALS: &[&str] = &["CAT", "GE", "HON", "BA", "RTX", "LMT", "DE", "UNP", "UPS"]; pub const SP500_CONSUMER: &[&str] = &["COST", "WMT", "HD", "NKE", "SBUX", "MCD", "DIS", "TGT", "LOW", "ABNB", "BKNG"]; pub const SP500_ENERGY: &[&str] = &["XOM", "CVX", "COP", "SLB", "OXY", "EOG", "MPC"]; pub const TELECOM_MEDIA: &[&str] = &["T", "VZ", "CMCSA", "TMUS", "NFLX"]; pub const INTERNATIONAL: &[&str] = &["TSM", "BABA", "JD", "SHOP", "MELI"]; pub const MATERIALS: &[&str] = &["FCX", "NEM", "LIN", "APD", "SHW"]; /// Get all symbols in the trading universe (~100 stocks + SPY for regime). pub fn get_all_symbols() -> Vec<&'static str> { let mut symbols = Vec::new(); symbols.extend_from_slice(MAG7); symbols.extend_from_slice(SEMIS); symbols.extend_from_slice(GROWTH_TECH); symbols.extend_from_slice(SOFTWARE); symbols.extend_from_slice(HEALTHCARE); symbols.extend_from_slice(BIOTECH); symbols.extend_from_slice(FINTECH_VOLATILE); symbols.extend_from_slice(SP500_FINANCIALS); symbols.extend_from_slice(SP500_INDUSTRIALS); symbols.extend_from_slice(SP500_CONSUMER); symbols.extend_from_slice(SP500_ENERGY); symbols.extend_from_slice(TELECOM_MEDIA); symbols.extend_from_slice(INTERNATIONAL); symbols.extend_from_slice(MATERIALS); // SPY is included for market regime detection (never traded directly) symbols.push(REGIME_SPY_SYMBOL); // Deduplicate (NFLX appears in both GROWTH_TECH and TELECOM_MEDIA) symbols.sort(); symbols.dedup(); symbols } // Strategy Parameters — Regime-Adaptive Dual Signal // RSI-14 for trend assessment, RSI-2 for mean-reversion entries (Connors) pub const RSI_PERIOD: usize = 14; pub const RSI_SHORT_PERIOD: usize = 2; // Connors RSI-2 for mean reversion pub const MACD_FAST: usize = 12; pub const MACD_SLOW: usize = 26; pub const MACD_SIGNAL: usize = 9; pub const MOMENTUM_PERIOD: usize = 63; pub const EMA_SHORT: usize = 9; pub const EMA_LONG: usize = 21; pub const EMA_TREND: usize = 50; // ADX — Regime Detection // ADX < RANGE_THRESHOLD = ranging (use mean reversion) // ADX > TREND_THRESHOLD = trending (use momentum/pullback) // Between = transition zone (reduce size, be cautious) pub const ADX_PERIOD: usize = 14; pub const ADX_TREND_THRESHOLD: f64 = 25.0; // Above this = trending // Bollinger Bands pub const BB_PERIOD: usize = 20; pub const BB_STD: f64 = 2.0; // ATR pub const ATR_PERIOD: usize = 14; pub const MIN_ATR_PCT: f64 = 0.005; // Volume filter pub const VOLUME_MA_PERIOD: usize = 20; pub const VOLUME_THRESHOLD: f64 = 0.8; // Momentum Ranking pub const TOP_MOMENTUM_COUNT: usize = 10; // Top decile: Jegadeesh-Titman (1993) strongest effect // Risk Management pub const MAX_POSITION_SIZE: f64 = 0.25; // Slightly larger for concentrated bets pub const MIN_CASH_RESERVE: f64 = 0.05; 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 pub const RISK_PER_TRADE: f64 = 0.01; // Conservative per-trade risk, compensated by more positions pub const ATR_STOP_MULTIPLIER: f64 = 3.0; // Wider stops — research shows tighter stops hurt pub const ATR_TRAIL_MULTIPLIER: f64 = 2.5; // Wide trail from HWM so winners have room to breathe pub const ATR_TRAIL_ACTIVATION_MULTIPLIER: f64 = 1.5; // Activate earlier (1.5x ATR gain) to protect profits // Portfolio-level controls pub const MAX_CONCURRENT_POSITIONS: usize = 10; // More diversification reduces idiosyncratic risk pub const MAX_SECTOR_POSITIONS: usize = 2; // Old single-tier drawdown constants (replaced by tiered system below) // pub const MAX_DRAWDOWN_HALT: f64 = 0.15; // pub const DRAWDOWN_HALT_BARS: usize = 10; // Time-based exit pub const TIME_EXIT_BARS: usize = 60; // Patient — now only exits losers, winners use trailing stop pub const REENTRY_COOLDOWN_BARS: usize = 5; // Shorter cooldown pub const RAMPUP_PERIOD_BARS: usize = 15; // Faster ramp-up // ═══════════════════════════════════════════════════════════════════════ // Market Regime Filter (SPY-based) // ═══════════════════════════════════════════════════════════════════════ // Uses SPY as a broad market proxy to detect bull/caution/bear regimes. // Based on the dual moving average framework (Faber 2007, "A Quantitative // Approach to Tactical Asset Allocation"): price vs 200-day SMA is the // single most effective regime filter in academic literature. // // Bull: SPY > EMA-200 AND EMA-50 > EMA-200 → trade normally // Caution: SPY < EMA-50 but SPY > EMA-200 → reduce size, raise thresholds // Bear: SPY < EMA-200 AND EMA-50 < EMA-200 → no new buys, manage exits only 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 (50% reduction). pub const REGIME_CAUTION_SIZE_FACTOR: f64 = 0.5; /// In Caution regime, add this to buy thresholds (require stronger signals). pub const REGIME_CAUTION_THRESHOLD_BUMP: f64 = 2.0; // ═══════════════════════════════════════════════════════════════════════ // Scaled Drawdown Circuit Breaker // ═══════════════════════════════════════════════════════════════════════ // 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.15; // 15% → 10 bars pub const DRAWDOWN_TIER1_BARS: usize = 10; pub const DRAWDOWN_TIER2_PCT: f64 = 0.20; // 20% → 30 bars pub const DRAWDOWN_TIER2_BARS: usize = 30; pub const DRAWDOWN_TIER3_PCT: f64 = 0.25; // 25%+ → 50 bars + require bull regime pub const DRAWDOWN_TIER3_BARS: usize = 50; /// If true, after a Tier 3 drawdown (>=25%), require bull market regime /// before resuming new entries even after the bar cooldown expires. pub const DRAWDOWN_TIER3_REQUIRE_BULL: bool = true; // ═══════════════════════════════════════════════════════════════════════ // Trailing Equity Curve Stop // ═══════════════════════════════════════════════════════════════════════ // If the portfolio equity drops below its own N-bar moving average, stop // all new entries. This is a secondary defense independent of the drawdown // breaker. Uses a 200-bar SMA of the equity curve (roughly 200 trading // days for daily, ~29 trading days for hourly). pub const EQUITY_CURVE_SMA_PERIOD: usize = 50; // Shorter window so bot can recover // Backtester slippage pub const SLIPPAGE_BPS: f64 = 10.0; // Trading intervals pub const BOT_CHECK_INTERVAL_SECONDS: u64 = 15; pub const BARS_LOOKBACK: usize = 100; // Backtest defaults pub const DEFAULT_INITIAL_CAPITAL: f64 = 100_000.0; pub const TRADING_DAYS_PER_YEAR: usize = 252; // Hours per trading day pub const HOURS_PER_DAY: usize = 7; /// Get the sector for a given symbol. pub fn get_sector(symbol: &str) -> &'static str { if MAG7.contains(&symbol) { "mag7" } else if SEMIS.contains(&symbol) { "semis" } else if GROWTH_TECH.contains(&symbol) { "growth_tech" } else if SOFTWARE.contains(&symbol) { "software" } else if HEALTHCARE.contains(&symbol) { "healthcare" } else if BIOTECH.contains(&symbol) { "biotech" } else if FINTECH_VOLATILE.contains(&symbol) { "fintech_volatile" } else if SP500_FINANCIALS.contains(&symbol) { "financials" } else if SP500_INDUSTRIALS.contains(&symbol) { "industrials" } else if SP500_CONSUMER.contains(&symbol) { "consumer" } else if SP500_ENERGY.contains(&symbol) { "energy" } else if TELECOM_MEDIA.contains(&symbol) { "telecom_media" } else if INTERNATIONAL.contains(&symbol) { "international" } else if MATERIALS.contains(&symbol) { "materials" } else { "unknown" } } /// Indicator parameters that can be scaled for different timeframes. #[derive(Debug, Clone)] pub struct IndicatorParams { pub rsi_period: usize, pub rsi_short_period: usize, // RSI-2 for mean reversion pub macd_fast: usize, pub macd_slow: usize, pub macd_signal: usize, pub momentum_period: usize, pub ema_short: usize, pub ema_long: usize, pub ema_trend: usize, pub adx_period: usize, pub bb_period: usize, pub atr_period: usize, pub volume_ma_period: usize, } impl IndicatorParams { /// Create parameters for daily timeframe. pub fn daily() -> Self { Self { rsi_period: 14, rsi_short_period: 2, // Connors RSI-2 macd_fast: 12, macd_slow: 26, macd_signal: 9, momentum_period: 63, ema_short: 9, ema_long: 21, ema_trend: 50, adx_period: 14, bb_period: 20, atr_period: 14, volume_ma_period: 20, } } /// Create parameters for hourly timeframe. pub fn hourly() -> Self { Self { rsi_period: 14, rsi_short_period: 3, // Slightly longer for hourly noise macd_fast: 12, macd_slow: 26, macd_signal: 9, momentum_period: 63, ema_short: 9, ema_long: 21, ema_trend: 200, adx_period: 14, bb_period: 20, atr_period: 14, volume_ma_period: 20, } } /// Get the minimum number of bars required for indicator calculation. pub fn min_bars(&self) -> usize { *[ self.macd_slow + self.macd_signal, self.rsi_period + 1, self.ema_trend, self.adx_period * 2, self.bb_period, self.momentum_period, ] .iter() .max() .unwrap() + 5 } } /// Timeframe for trading data. #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] pub enum Timeframe { Daily, Hourly, } impl Timeframe { pub fn params(&self) -> IndicatorParams { match self { Timeframe::Daily => IndicatorParams::daily(), Timeframe::Hourly => IndicatorParams::hourly(), } } }