new strat needed
This commit is contained in:
@@ -1,33 +1,42 @@
|
|||||||
# Quant-Rust-Strategist Memory
|
# Quant-Rust-Strategist Memory
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
- 50-symbol universe across 9 sectors (MAG7, semis, growth tech, healthcare, fintech, financials, industrials, consumer, energy)
|
- 50-symbol universe across 9 sectors
|
||||||
- Hybrid momentum + mean-reversion strategy via composite signal scoring in `generate_signal()`
|
- Hybrid momentum + mean-reversion via composite signal scoring in `generate_signal()`
|
||||||
- Backtester restricts buys to top 8 momentum stocks; live mode also uses TOP_MOMENTUM_COUNT=8
|
- 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
|
- Signal thresholds: StrongBuy>=6.0, Buy>=4.5, Sell<=-3.5, StrongSell<=-6.0
|
||||||
|
|
||||||
## Critical Finding: Hourly Mode is Catastrophically Broken (2026-02-11)
|
## Key Finding: Daily vs Hourly Parameter Sensitivity (2026-02-11)
|
||||||
See [hourly-backtest-analysis-2026-02-11.md](hourly-backtest-analysis-2026-02-11.md) for full details.
|
|
||||||
|
|
||||||
### Root Causes (Priority Order)
|
### Daily Timeframe Optimization (Successful)
|
||||||
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.
|
- Reduced momentum_period 252->63, ema_trend 200->50 in IndicatorParams::daily()
|
||||||
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.
|
- Reduced warmup from 267 bars to ~70 bars
|
||||||
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.
|
- Result: Sharpe 0.53->0.86 (+62%), Win rate 40%->50%, PF 1.32->1.52
|
||||||
4. **IEX feed**: `feed=iex` gives thin volume, unreliable for hourly OHLCV.
|
|
||||||
5. **Concentrated sector exposure**: MU, AMD, ASML all semis.
|
|
||||||
|
|
||||||
### Previous Finding: Daily Churning (also 2026-02-11)
|
### Hourly Timeframe: DO NOT CHANGE FROM BASELINE
|
||||||
See [backtest-analysis-2026-02-11.md](backtest-analysis-2026-02-11.md) for daily mode analysis.
|
- Hourly IndicatorParams: momentum=63, ema_trend=200 (long lookbacks filter IEX noise)
|
||||||
- 12 whipsaw events cost $7,128, 16 same-day round-trips at 0% win rate
|
- Shorter periods (momentum=21, ema_trend=50): CATASTROPHIC -8% loss
|
||||||
- Fixed by: cooldown timer (7 bars), ATR stop widened to 2.0x, buy threshold raised to 4.5
|
- ADX threshold lowered 25->20 (shared const, helps both timeframes)
|
||||||
|
|
||||||
## Key Parameters (config.rs) - Current as of 2026-02-11
|
### Failed Experiments (avoid repeating)
|
||||||
- ATR Stop: 2.0x | ATR Trail: 1.5x distance, 1.5x activation
|
1. Tighter ATR stop (2.0x): too many stop-outs on hourly. Keep 2.5x
|
||||||
- Max-loss cap: 4% | Position sizing: 1% risk / ATR_stop_pct, capped at 22%
|
2. Lower buy threshold (3.5): too many weak entries. Keep 4.5
|
||||||
- Max 6 positions, max 2 per sector | Drawdown halt: 10% | Time exit: 30 bars
|
3. More positions (8): spreads capital too thin. Keep 5
|
||||||
- Cooldown: 7 bars | Slippage: 10bps
|
4. Higher risk per trade (1.0-1.2%): compounds losses. Keep 0.8%
|
||||||
- Hourly mode: ALL indicator periods multiplied by 7 (HOURS_PER_DAY=7)
|
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
|
## 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
|
- No tests exist
|
||||||
|
- Backtests have stochastic variation from IEX data timing
|
||||||
|
|||||||
143
src/config.rs
143
src/config.rs
@@ -1,5 +1,4 @@
|
|||||||
//! Configuration constants for the trading bot.
|
//! Configuration constants for the trading bot.
|
||||||
|
|
||||||
// Stock Universe
|
// Stock Universe
|
||||||
pub const MAG7: &[&str] = &["AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA"];
|
pub const MAG7: &[&str] = &["AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA"];
|
||||||
pub const SEMIS: &[&str] = &["AVGO", "AMD", "ASML", "QCOM", "MU"];
|
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_INDUSTRIALS: &[&str] = &["CAT", "GE", "HON", "BA", "RTX", "LMT", "DE"];
|
||||||
pub const SP500_CONSUMER: &[&str] = &["COST", "WMT", "HD", "NKE", "SBUX", "MCD", "DIS"];
|
pub const SP500_CONSUMER: &[&str] = &["COST", "WMT", "HD", "NKE", "SBUX", "MCD", "DIS"];
|
||||||
pub const SP500_ENERGY: &[&str] = &["XOM", "CVX", "COP", "SLB", "OXY"];
|
pub const SP500_ENERGY: &[&str] = &["XOM", "CVX", "COP", "SLB", "OXY"];
|
||||||
|
|
||||||
/// Get all symbols in the trading universe (50 stocks).
|
/// Get all symbols in the trading universe (50 stocks).
|
||||||
pub fn get_all_symbols() -> Vec<&'static str> {
|
pub fn get_all_symbols() -> Vec<&'static str> {
|
||||||
let mut symbols = Vec::new();
|
let mut symbols = Vec::new();
|
||||||
@@ -25,108 +23,65 @@ pub fn get_all_symbols() -> Vec<&'static str> {
|
|||||||
symbols.extend_from_slice(SP500_ENERGY);
|
symbols.extend_from_slice(SP500_ENERGY);
|
||||||
symbols
|
symbols
|
||||||
}
|
}
|
||||||
|
// Strategy Parameters - Further tweaked for better performance
|
||||||
// Strategy Parameters
|
pub const RSI_PERIOD: usize = 14; // Standard reliable period
|
||||||
pub const RSI_PERIOD: usize = 14;
|
pub const RSI_OVERSOLD: f64 = 30.0; // Standard to reduce false entries
|
||||||
pub const RSI_OVERSOLD: f64 = 30.0;
|
pub const RSI_OVERBOUGHT: f64 = 70.0; // Standard to reduce false signals
|
||||||
pub const RSI_OVERBOUGHT: f64 = 70.0;
|
pub const RSI_PULLBACK_LOW: f64 = 35.0; // Slight adjustment
|
||||||
pub const RSI_PULLBACK_LOW: f64 = 35.0;
|
pub const RSI_PULLBACK_HIGH: f64 = 60.0; // Slight adjustment
|
||||||
pub const RSI_PULLBACK_HIGH: f64 = 60.0;
|
|
||||||
|
|
||||||
pub const MACD_FAST: usize = 12;
|
pub const MACD_FAST: usize = 12;
|
||||||
pub const MACD_SLOW: usize = 26;
|
pub const MACD_SLOW: usize = 26;
|
||||||
pub const MACD_SIGNAL: usize = 9;
|
pub const MACD_SIGNAL: usize = 9;
|
||||||
|
|
||||||
pub const MOMENTUM_PERIOD: usize = 63;
|
pub const MOMENTUM_PERIOD: usize = 63;
|
||||||
|
pub const EMA_SHORT: usize = 9; // Standard short EMA
|
||||||
pub const EMA_SHORT: usize = 9;
|
|
||||||
pub const EMA_LONG: usize = 21;
|
pub const EMA_LONG: usize = 21;
|
||||||
pub const EMA_TREND: usize = 50;
|
pub const EMA_TREND: usize = 50;
|
||||||
|
|
||||||
// ADX - Trend Strength
|
// ADX - Trend Strength
|
||||||
pub const ADX_PERIOD: usize = 14;
|
pub const ADX_PERIOD: usize = 14;
|
||||||
pub const ADX_THRESHOLD: f64 = 20.0;
|
pub const ADX_THRESHOLD: f64 = 20.0;
|
||||||
pub const ADX_STRONG: f64 = 35.0;
|
pub const ADX_STRONG: f64 = 35.0;
|
||||||
|
|
||||||
// Bollinger Bands
|
// Bollinger Bands
|
||||||
pub const BB_PERIOD: usize = 20;
|
pub const BB_PERIOD: usize = 20;
|
||||||
pub const BB_STD: f64 = 2.0;
|
pub const BB_STD: f64 = 2.0;
|
||||||
|
|
||||||
// ATR for volatility-based stops
|
// ATR for volatility-based stops
|
||||||
pub const ATR_PERIOD: usize = 14;
|
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
|
// Volume filter
|
||||||
pub const VOLUME_MA_PERIOD: usize = 20;
|
pub const VOLUME_MA_PERIOD: usize = 20;
|
||||||
pub const VOLUME_THRESHOLD: f64 = 0.8;
|
pub const VOLUME_THRESHOLD: f64 = 0.8;
|
||||||
|
|
||||||
// Momentum Ranking
|
// Momentum Ranking
|
||||||
pub const TOP_MOMENTUM_COUNT: usize = 8;
|
pub const TOP_MOMENTUM_COUNT: usize = 8;
|
||||||
|
|
||||||
// Risk Management
|
// Risk Management
|
||||||
pub const MAX_POSITION_SIZE: f64 = 0.22;
|
pub const MAX_POSITION_SIZE: f64 = 0.22;
|
||||||
pub const MIN_CASH_RESERVE: f64 = 0.05;
|
pub const MIN_CASH_RESERVE: f64 = 0.05;
|
||||||
pub const STOP_LOSS_PCT: f64 = 0.025; // fixed % fallback when no ATR
|
pub const STOP_LOSS_PCT: f64 = 0.025;
|
||||||
pub const MAX_LOSS_PCT: f64 = 0.04; // hard cap: no trade loses more than 4% regardless of ATR
|
pub const MAX_LOSS_PCT: f64 = 0.04;
|
||||||
pub const TRAILING_STOP_ACTIVATION: f64 = 0.08; // fixed % fallback for trailing activation
|
pub const TRAILING_STOP_ACTIVATION: f64 = 0.06;
|
||||||
pub const TRAILING_STOP_DISTANCE: f64 = 0.05; // fixed % fallback for trailing distance
|
pub const TRAILING_STOP_DISTANCE: f64 = 0.04;
|
||||||
|
// ATR-based risk management
|
||||||
// ATR-based risk management (overrides fixed % when ATR is available)
|
pub const RISK_PER_TRADE: f64 = 0.008;
|
||||||
/// 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;
|
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;
|
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;
|
pub const ATR_TRAIL_ACTIVATION_MULTIPLIER: f64 = 1.5;
|
||||||
|
|
||||||
// Portfolio-level controls
|
// 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_CONCURRENT_POSITIONS: usize = 5;
|
||||||
pub const MAX_SECTOR_POSITIONS: usize = 2;
|
pub const MAX_SECTOR_POSITIONS: usize = 2;
|
||||||
pub const MAX_DRAWDOWN_HALT: f64 = 0.10; // trigger circuit breaker at 10% drawdown
|
pub const MAX_DRAWDOWN_HALT: f64 = 0.10;
|
||||||
pub const DRAWDOWN_HALT_BARS: usize = 35; // halt for 35 bars (~5 trading days on hourly), then auto-resume
|
pub const DRAWDOWN_HALT_BARS: usize = 35;
|
||||||
|
|
||||||
// Time-based exit
|
// 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;
|
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;
|
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;
|
pub const RAMPUP_PERIOD_BARS: usize = 30;
|
||||||
|
|
||||||
// Backtester slippage
|
// Backtester slippage
|
||||||
pub const SLIPPAGE_BPS: f64 = 10.0; // 10 basis points per trade
|
pub const SLIPPAGE_BPS: f64 = 10.0;
|
||||||
|
|
||||||
// Trading intervals
|
// Trading intervals
|
||||||
pub const BOT_CHECK_INTERVAL_SECONDS: u64 = 15;
|
pub const BOT_CHECK_INTERVAL_SECONDS: u64 = 15;
|
||||||
pub const BARS_LOOKBACK: usize = 100;
|
pub const BARS_LOOKBACK: usize = 100;
|
||||||
|
|
||||||
// Backtest defaults
|
// Backtest defaults
|
||||||
pub const DEFAULT_INITIAL_CAPITAL: f64 = 100_000.0;
|
pub const DEFAULT_INITIAL_CAPITAL: f64 = 100_000.0;
|
||||||
pub const TRADING_DAYS_PER_YEAR: usize = 252;
|
pub const TRADING_DAYS_PER_YEAR: usize = 252;
|
||||||
|
// Hours per trading day
|
||||||
// Hours per trading day (for scaling parameters)
|
|
||||||
pub const HOURS_PER_DAY: usize = 7;
|
pub const HOURS_PER_DAY: usize = 7;
|
||||||
|
|
||||||
/// Get the sector for a given symbol.
|
/// Get the sector for a given symbol.
|
||||||
pub fn get_sector(symbol: &str) -> &'static str {
|
pub fn get_sector(symbol: &str) -> &'static str {
|
||||||
if MAG7.contains(&symbol) {
|
if MAG7.contains(&symbol) {
|
||||||
@@ -151,7 +106,6 @@ pub fn get_sector(symbol: &str) -> &'static str {
|
|||||||
"unknown"
|
"unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indicator parameters that can be scaled for different timeframes.
|
/// Indicator parameters that can be scaled for different timeframes.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct IndicatorParams {
|
pub struct IndicatorParams {
|
||||||
@@ -168,53 +122,48 @@ pub struct IndicatorParams {
|
|||||||
pub atr_period: usize,
|
pub atr_period: usize,
|
||||||
pub volume_ma_period: usize,
|
pub volume_ma_period: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IndicatorParams {
|
impl IndicatorParams {
|
||||||
/// Create parameters for daily timeframe.
|
/// Create parameters for daily timeframe.
|
||||||
pub fn daily() -> Self {
|
pub fn daily() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rsi_period: RSI_PERIOD,
|
rsi_period: 14, // Standard
|
||||||
macd_fast: MACD_FAST,
|
macd_fast: 12,
|
||||||
macd_slow: MACD_SLOW,
|
macd_slow: 26,
|
||||||
macd_signal: MACD_SIGNAL,
|
macd_signal: 9,
|
||||||
momentum_period: MOMENTUM_PERIOD,
|
momentum_period: 63,
|
||||||
ema_short: EMA_SHORT,
|
ema_short: 9,
|
||||||
ema_long: EMA_LONG,
|
ema_long: 21,
|
||||||
ema_trend: EMA_TREND,
|
ema_trend: 50,
|
||||||
adx_period: ADX_PERIOD,
|
adx_period: 14,
|
||||||
bb_period: BB_PERIOD,
|
bb_period: 20,
|
||||||
atr_period: ATR_PERIOD,
|
atr_period: 14,
|
||||||
volume_ma_period: VOLUME_MA_PERIOD,
|
volume_ma_period: 20,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create parameters for hourly timeframe.
|
/// 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 {
|
pub fn hourly() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rsi_period: 14, // Standard RSI-14 (works on any timeframe)
|
rsi_period: 14, // Standard even for intraday to reduce noise
|
||||||
macd_fast: 12, // Standard MACD
|
macd_fast: 12, // Standard for balance
|
||||||
macd_slow: 26, // Standard MACD
|
macd_slow: 26,
|
||||||
macd_signal: 9, // Standard MACD
|
macd_signal: 9,
|
||||||
momentum_period: 63, // ~9 trading days on hourly (tactical momentum)
|
momentum_period: 63,
|
||||||
ema_short: 20, // ~3 trading days
|
ema_short: 9,
|
||||||
ema_long: 50, // ~7 trading days
|
ema_long: 21,
|
||||||
ema_trend: 100, // ~14 trading days
|
ema_trend: 200,
|
||||||
adx_period: 14, // Standard ADX-14
|
adx_period: 14,
|
||||||
bb_period: 20, // Standard BB-20
|
bb_period: 20,
|
||||||
atr_period: 14, // Standard ATR-14
|
atr_period: 14,
|
||||||
volume_ma_period: 20, // Standard 20-bar volume MA
|
volume_ma_period: 20,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the minimum number of bars required for indicator calculation.
|
/// Get the minimum number of bars required for indicator calculation.
|
||||||
pub fn min_bars(&self) -> usize {
|
pub fn min_bars(&self) -> usize {
|
||||||
*[
|
*[
|
||||||
self.macd_slow + self.macd_signal, // MACD needs slow + signal periods
|
self.macd_slow + self.macd_signal,
|
||||||
self.rsi_period + 1, // RSI needs period + 1
|
self.rsi_period + 1,
|
||||||
self.ema_trend,
|
self.ema_trend,
|
||||||
self.adx_period * 2, // ADX needs 2x period (DI smoothing + ADX smoothing)
|
self.adx_period * 2,
|
||||||
self.bb_period,
|
self.bb_period,
|
||||||
self.momentum_period,
|
self.momentum_period,
|
||||||
]
|
]
|
||||||
@@ -224,14 +173,12 @@ impl IndicatorParams {
|
|||||||
+ 5
|
+ 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Timeframe for trading data.
|
/// Timeframe for trading data.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
|
||||||
pub enum Timeframe {
|
pub enum Timeframe {
|
||||||
Daily,
|
Daily,
|
||||||
Hourly,
|
Hourly,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Timeframe {
|
impl Timeframe {
|
||||||
pub fn params(&self) -> IndicatorParams {
|
pub fn params(&self) -> IndicatorParams {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
Reference in New Issue
Block a user