//! 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 = 15; // Top quintile: enough candidates for 8 positions // Risk Management pub const MAX_POSITION_SIZE: f64 = 0.20; // 20% max to reduce concentration risk 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 (DAILY timeframe - wider stops for longer-term holds) 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_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; // 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 = 80; // More patience for losers on hourly bars pub const REENTRY_COOLDOWN_BARS: usize = 10; // Longer cooldown to reduce churn 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. /// 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; // ═══════════════════════════════════════════════════════════════════════ // 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.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. 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. /// /// Hourly bars need ~7x longer periods than daily to capture the same /// market structure (~7 trading hours/day). Without this, EMA-9 hourly /// = 1.3 days (noise), and the trend/momentum gates whipsaw constantly. pub fn hourly() -> Self { Self { rsi_period: 14, rsi_short_period: 3, macd_fast: 84, // 12 * 7 macd_slow: 182, // 26 * 7 macd_signal: 63, // 9 * 7 momentum_period: 441, // 63 * 7 = quarterly momentum ema_short: 63, // 9 * 7 ~ daily 9-day EMA ema_long: 147, // 21 * 7 ~ daily 21-day EMA ema_trend: 350, // 50 * 7 ~ daily 50-day EMA adx_period: 14, bb_period: 140, // 20 * 7 atr_period: 14, volume_ma_period: 140, // 20 * 7 } } /// 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(), } } }