//! Configuration constants for the trading bot. // Stock Universe pub const MAG7: &[&str] = &["AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA"]; pub const SEMIS: &[&str] = &["AVGO", "AMD", "ASML", "QCOM", "MU"]; pub const GROWTH_TECH: &[&str] = &["NFLX", "CRM", "NOW", "UBER", "SNOW"]; pub const HEALTHCARE: &[&str] = &["LLY", "UNH", "ISRG", "VRTX", "ABBV", "MRK", "PFE"]; pub const FINTECH_VOLATILE: &[&str] = &["V", "MA", "COIN", "PLTR", "MSTR"]; pub const SP500_FINANCIALS: &[&str] = &["JPM", "GS", "MS", "BLK", "AXP", "C"]; pub const SP500_INDUSTRIALS: &[&str] = &["CAT", "GE", "HON", "BA", "RTX", "LMT", "DE"]; pub const SP500_CONSUMER: &[&str] = &["COST", "WMT", "HD", "NKE", "SBUX", "MCD", "DIS"]; pub const SP500_ENERGY: &[&str] = &["XOM", "CVX", "COP", "SLB", "OXY"]; /// Get all symbols in the trading universe (50 stocks). 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(HEALTHCARE); 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 } // Strategy Parameters pub const RSI_PERIOD: usize = 14; pub const RSI_OVERSOLD: f64 = 30.0; pub const RSI_OVERBOUGHT: f64 = 70.0; pub const RSI_PULLBACK_LOW: f64 = 35.0; pub const RSI_PULLBACK_HIGH: f64 = 60.0; 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 - Trend Strength pub const ADX_PERIOD: usize = 14; pub const ADX_THRESHOLD: f64 = 20.0; pub const ADX_STRONG: f64 = 35.0; // Bollinger Bands pub const BB_PERIOD: usize = 20; pub const BB_STD: f64 = 2.0; // ATR for volatility-based stops pub const ATR_PERIOD: usize = 14; pub const MIN_ATR_PCT: f64 = 0.005; // 0.5% floor to prevent extreme position sizing // Volume filter pub const VOLUME_MA_PERIOD: usize = 20; pub const VOLUME_THRESHOLD: f64 = 0.8; // Momentum Ranking pub const TOP_MOMENTUM_COUNT: usize = 8; // Risk Management pub const MAX_POSITION_SIZE: f64 = 0.22; pub const MIN_CASH_RESERVE: f64 = 0.05; pub const STOP_LOSS_PCT: f64 = 0.025; // fixed % fallback when no ATR pub const MAX_LOSS_PCT: f64 = 0.04; // hard cap: no trade loses more than 4% regardless of ATR pub const TRAILING_STOP_ACTIVATION: f64 = 0.08; // fixed % fallback for trailing activation pub const TRAILING_STOP_DISTANCE: f64 = 0.05; // fixed % fallback for trailing distance // ATR-based risk management (overrides fixed % when ATR is available) /// Risk budget per trade as fraction of portfolio. Used with ATR for position sizing: /// position_value = (portfolio * RISK_PER_TRADE) / (ATR_STOP_MULTIPLIER * atr_pct). /// Reduced to 0.75% for hourly trading to account for more frequent trades and higher transaction costs. pub const RISK_PER_TRADE: f64 = 0.0075; // 0.75% of portfolio risk per trade /// Initial stop-loss distance in ATR multiples. At 2.5x ATR for hourly bars, provides /// adequate room for intraday volatility while maintaining risk control. /// Hourly ATR is noisier than daily, requiring wider stops to avoid premature exits. pub const ATR_STOP_MULTIPLIER: f64 = 2.5; /// Trailing stop distance in ATR multiples once activated. /// At 1.5x ATR (same as initial stop), we lock in gains without giving back too much. pub const ATR_TRAIL_MULTIPLIER: f64 = 1.5; /// Trailing stop activates after this many ATR of unrealized gain. /// At 1.5x ATR, activates once the trade has earned its risk budget. pub const ATR_TRAIL_ACTIVATION_MULTIPLIER: f64 = 1.5; // Portfolio-level controls /// Max concurrent positions reduced to 5 for hourly trading to limit correlation risk /// with faster rebalancing and more frequent signals. pub const MAX_CONCURRENT_POSITIONS: usize = 5; pub const MAX_SECTOR_POSITIONS: usize = 2; pub const MAX_DRAWDOWN_HALT: f64 = 0.10; // trigger circuit breaker at 10% drawdown pub const DRAWDOWN_HALT_BARS: usize = 35; // halt for 35 bars (~5 trading days on hourly), then auto-resume // Time-based exit /// Stale position exit threshold (in bars). Positions that haven't reached /// trailing stop activation after this many bars are closed to free capital. /// 30 hourly bars ~ 4.3 trading days. Gives positions enough time to work /// without tying up capital in dead trades indefinitely. pub const TIME_EXIT_BARS: usize = 30; /// Re-entry cooldown period (in bars) after a stop-loss exit. /// Prevents whipsaw churning where a stock is sold at stop-loss then /// immediately re-bought on the same bar. 7 bars = 1 trading day on hourly. /// This single parameter prevents the majority of same-day round-trip losses. pub const REENTRY_COOLDOWN_BARS: usize = 7; /// Gradual ramp-up period (in bars) at backtest start. /// Limits new positions to 1 per bar during this initial period to prevent /// flash-deployment of full capital. 30 bars = ~4.3 trading days on hourly. pub const RAMPUP_PERIOD_BARS: usize = 30; // Backtester slippage pub const SLIPPAGE_BPS: f64 = 10.0; // 10 basis points per trade // 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 (for scaling parameters) 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 HEALTHCARE.contains(&symbol) { "healthcare" } 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 { "unknown" } } /// Indicator parameters that can be scaled for different timeframes. #[derive(Debug, Clone)] pub struct IndicatorParams { pub rsi_period: usize, 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: RSI_PERIOD, macd_fast: MACD_FAST, macd_slow: MACD_SLOW, macd_signal: MACD_SIGNAL, momentum_period: MOMENTUM_PERIOD, ema_short: EMA_SHORT, ema_long: EMA_LONG, ema_trend: EMA_TREND, adx_period: ADX_PERIOD, bb_period: BB_PERIOD, atr_period: ATR_PERIOD, volume_ma_period: VOLUME_MA_PERIOD, } } /// Create parameters for hourly timeframe. /// Uses standard textbook periods appropriate for hourly bars. /// Research shows indicator periods work on bar counts, not calendar time. pub fn hourly() -> Self { Self { rsi_period: 14, // Standard RSI-14 (works on any timeframe) macd_fast: 12, // Standard MACD macd_slow: 26, // Standard MACD macd_signal: 9, // Standard MACD momentum_period: 63, // ~9 trading days on hourly (tactical momentum) ema_short: 20, // ~3 trading days ema_long: 50, // ~7 trading days ema_trend: 100, // ~14 trading days adx_period: 14, // Standard ADX-14 bb_period: 20, // Standard BB-20 atr_period: 14, // Standard ATR-14 volume_ma_period: 20, // Standard 20-bar volume MA } } /// Get the minimum number of bars required for indicator calculation. pub fn min_bars(&self) -> usize { *[ self.macd_slow + self.macd_signal, // MACD needs slow + signal periods self.rsi_period + 1, // RSI needs period + 1 self.ema_trend, self.adx_period * 2, // ADX needs 2x period (DI smoothing + ADX smoothing) 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(), } } }