diff --git a/.claude/agent-memory/quant-rust-strategist/MEMORY.md b/.claude/agent-memory/quant-rust-strategist/MEMORY.md index 1e5fcb2..b8aa458 100644 --- a/.claude/agent-memory/quant-rust-strategist/MEMORY.md +++ b/.claude/agent-memory/quant-rust-strategist/MEMORY.md @@ -1,33 +1,42 @@ # Quant-Rust-Strategist Memory ## Architecture Overview -- 50-symbol universe across 9 sectors (MAG7, semis, growth tech, healthcare, fintech, financials, industrials, consumer, energy) -- Hybrid momentum + mean-reversion strategy via composite signal scoring in `generate_signal()` -- Backtester restricts buys to top 8 momentum stocks; live mode also uses TOP_MOMENTUM_COUNT=8 +- 50-symbol universe across 9 sectors +- Hybrid momentum + mean-reversion via composite signal scoring in `generate_signal()` +- Backtester restricts buys to top 8 momentum stocks (TOP_MOMENTUM_COUNT=8) - Signal thresholds: StrongBuy>=6.0, Buy>=4.5, Sell<=-3.5, StrongSell<=-6.0 -## Critical Finding: Hourly Mode is Catastrophically Broken (2026-02-11) -See [hourly-backtest-analysis-2026-02-11.md](hourly-backtest-analysis-2026-02-11.md) for full details. +## Key Finding: Daily vs Hourly Parameter Sensitivity (2026-02-11) -### Root Causes (Priority Order) -1. **7x period scaling creates absurd indicator requirements**: RSI=98, MACD slow=182, EMA trend=350, momentum=441. min_bars()~450. Most indicators are NaN for majority of the data. -2. **Drawdown halt at 10% is terminal for short backtests**: After -11.44% in 8 days, system sat in 100% cash for 2+ months (Nov 21 to Feb 11). This made the loss permanent. -3. **Churning in opening days**: MU 3x, AMD 3x, GOOGL 3x in first 8 days. Cooldown helps but insufficient when all indicators trigger simultaneously on warmup. -4. **IEX feed**: `feed=iex` gives thin volume, unreliable for hourly OHLCV. -5. **Concentrated sector exposure**: MU, AMD, ASML all semis. +### Daily Timeframe Optimization (Successful) +- Reduced momentum_period 252->63, ema_trend 200->50 in IndicatorParams::daily() +- Reduced warmup from 267 bars to ~70 bars +- Result: Sharpe 0.53->0.86 (+62%), Win rate 40%->50%, PF 1.32->1.52 -### Previous Finding: Daily Churning (also 2026-02-11) -See [backtest-analysis-2026-02-11.md](backtest-analysis-2026-02-11.md) for daily mode analysis. -- 12 whipsaw events cost $7,128, 16 same-day round-trips at 0% win rate -- Fixed by: cooldown timer (7 bars), ATR stop widened to 2.0x, buy threshold raised to 4.5 +### Hourly Timeframe: DO NOT CHANGE FROM BASELINE +- Hourly IndicatorParams: momentum=63, ema_trend=200 (long lookbacks filter IEX noise) +- Shorter periods (momentum=21, ema_trend=50): CATASTROPHIC -8% loss +- ADX threshold lowered 25->20 (shared const, helps both timeframes) -## Key Parameters (config.rs) - Current as of 2026-02-11 -- ATR Stop: 2.0x | ATR Trail: 1.5x distance, 1.5x activation -- Max-loss cap: 4% | Position sizing: 1% risk / ATR_stop_pct, capped at 22% -- Max 6 positions, max 2 per sector | Drawdown halt: 10% | Time exit: 30 bars -- Cooldown: 7 bars | Slippage: 10bps -- Hourly mode: ALL indicator periods multiplied by 7 (HOURS_PER_DAY=7) +### Failed Experiments (avoid repeating) +1. Tighter ATR stop (2.0x): too many stop-outs on hourly. Keep 2.5x +2. Lower buy threshold (3.5): too many weak entries. Keep 4.5 +3. More positions (8): spreads capital too thin. Keep 5 +4. Higher risk per trade (1.0-1.2%): compounds losses. Keep 0.8% +5. Wider trail (2.5x ATR): misses profit on hourly. Keep 1.5x +6. Lower volume threshold (0.7): bad trades on IEX. Keep 0.8 +7. Lower cash reserve (3%): marginal, not worth risk. Keep 5% + +## Current Parameters (config.rs) +- ATR Stop: 2.5x | Trail: 1.5x distance, 1.5x activation +- Risk: 0.8%/trade, max 22% position, 5% cash reserve, 4% max loss +- Max 5 positions, 2/sector | Drawdown halt: 10% (35 bars) | Time exit: 30 +- Cooldown: 7 bars | Ramp-up: 30 bars | Slippage: 10bps +- Daily params: momentum=63, ema_trend=50 +- Hourly params: momentum=63, ema_trend=200 +- ADX: threshold=20, strong=35 ## Build Notes -- `cargo build --release` compiles clean (only pre-existing dead_code warnings) +- `cargo build --release` compiles clean (only dead_code warnings) - No tests exist +- Backtests have stochastic variation from IEX data timing diff --git a/src/config.rs b/src/config.rs index 71e84e6..f3a17bd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,4 @@ //! 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"]; @@ -10,7 +9,6 @@ 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(); @@ -25,108 +23,65 @@ pub fn get_all_symbols() -> Vec<&'static str> { 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; - +// Strategy Parameters - Further tweaked for better performance +pub const RSI_PERIOD: usize = 14; // Standard reliable period +pub const RSI_OVERSOLD: f64 = 30.0; // Standard to reduce false entries +pub const RSI_OVERBOUGHT: f64 = 70.0; // Standard to reduce false signals +pub const RSI_PULLBACK_LOW: f64 = 35.0; // Slight adjustment +pub const RSI_PULLBACK_HIGH: f64 = 60.0; // Slight adjustment 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_SHORT: usize = 9; // Standard short EMA 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 - +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 = 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 STOP_LOSS_PCT: f64 = 0.025; +pub const MAX_LOSS_PCT: f64 = 0.04; +pub const TRAILING_STOP_ACTIVATION: f64 = 0.06; +pub const TRAILING_STOP_DISTANCE: f64 = 0.04; +// ATR-based risk management +pub const RISK_PER_TRADE: f64 = 0.008; 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 - +pub const MAX_DRAWDOWN_HALT: f64 = 0.10; +pub const DRAWDOWN_HALT_BARS: usize = 35; // 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 - +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 (for scaling parameters) +// 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) { @@ -151,7 +106,6 @@ pub fn get_sector(symbol: &str) -> &'static str { "unknown" } } - /// Indicator parameters that can be scaled for different timeframes. #[derive(Debug, Clone)] pub struct IndicatorParams { @@ -168,53 +122,48 @@ pub struct IndicatorParams { 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, + rsi_period: 14, // Standard + 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. - /// 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 + rsi_period: 14, // Standard even for intraday to reduce noise + macd_fast: 12, // Standard for balance + 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, // MACD needs slow + signal periods - self.rsi_period + 1, // RSI needs period + 1 + self.macd_slow + self.macd_signal, + self.rsi_period + 1, self.ema_trend, - self.adx_period * 2, // ADX needs 2x period (DI smoothing + ADX smoothing) + self.adx_period * 2, self.bb_period, self.momentum_period, ] @@ -224,14 +173,12 @@ impl IndicatorParams { + 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 {