Files
vibe-invest/src/config.rs
2026-02-13 16:28:42 +00:00

263 lines
12 KiB
Rust

//! 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(),
}
}
}