the holy grail. untill next time
This commit is contained in:
@@ -1,179 +1,195 @@
|
|||||||
# Consistency Auditor Memory
|
# Consistency Auditor Memory
|
||||||
|
|
||||||
## Last Audit: 2026-02-13 (Post Config Update)
|
## Last Audit: 2026-02-13 (Post-Config Update v2 - NEW FINDINGS)
|
||||||
|
|
||||||
### AUDIT RESULT: ⚠️ 1 CRITICAL BUG + 1 CRITICAL BEHAVIORAL DIVERGENCE
|
### AUDIT RESULT: ⚠️ 1 CRITICAL DIVERGENCE + 1 MEDIUM BUG
|
||||||
|
|
||||||
**1. CRITICAL BUG: Drawdown Peak Reset Inconsistency**
|
**1. CRITICAL DIVERGENCE: Equity Curve SMA Stop Removed from Backtester Only**
|
||||||
- **backtester.rs** resets `peak_portfolio_value` to current value on halt expiry (line 132)
|
- **backtester.rs** lines 258-261: Explicitly REMOVED equity SMA stop with comment "creates pathological feedback loop"
|
||||||
- **bot.rs** does NOT reset peak on halt expiry (lines 365-377)
|
- **bot.rs** lines 809-825, 904-907, 991-992: STILL ENFORCES equity curve SMA stop (blocks buys when equity < 50-period SMA)
|
||||||
- **Impact**: Bot will re-trigger drawdown halt more frequently than backtest suggests, spending more time in cash. After a 15% drawdown triggers a 10-bar halt, a partial recovery followed by a minor dip will immediately re-trigger the halt in live trading but not in backtest.
|
- **Impact**: Bot will skip FAR MORE entries than backtest suggests. When holding losing positions, equity drops below SMA, blocking ALL new buys, preventing recovery. Backtest does NOT reflect this behavior. Live performance will be WORSE.
|
||||||
- **Fix**: Add `self.peak_portfolio_value = portfolio_value;` to bot.rs after line 374 (inside the halt expiry block)
|
- **Fix**: Remove `equity_below_sma()` function from bot.rs (lines 809-826, 904-907, 991-992) to match backtester
|
||||||
- **Code location**: `/home/work/Documents/rust/invest-bot/src/bot.rs:365-377`
|
- **Rationale**: Backtester comment is correct — this creates a self-reinforcing trap where losers prevent entry into winners
|
||||||
|
- **Code location**: `/home/work/Documents/rust/invest-bot/src/bot.rs:809-826, 904-907, 991-992`
|
||||||
|
|
||||||
**2. CRITICAL DIVERGENCE: PDT Enforcement Differs by Timeframe**
|
**2. MEDIUM BUG: Confidence Scaling Math Error (Both Files)**
|
||||||
- **bot.rs** enforces PDT blocking on non-stop-loss sells (lines 619-628), with $25K exemption (lines 281-285)
|
- **indicators.rs:655** changed to `confidence = score / 10.0`
|
||||||
- **backtester.rs** has PDT DISABLED entirely for backtest (lines 245-248: "informational only, not blocking")
|
- **bot.rs:1025** and **backtester.rs:828** reverse-engineer with `score = confidence * 12.0`
|
||||||
- **Impact by timeframe**:
|
- **Impact**: Regime threshold bumps use wrong score approximation (12x instead of 10x). Overestimates scores by 20%, making thresholds slightly easier to pass. Both files have the same bug, so they're consistent with each other, but both are mathematically wrong.
|
||||||
- **Daily mode**: No impact (buys in Phase 2, sells in Phase 1 on different bars → day trades impossible by design)
|
- **Fix**: Change `* 12.0` to `* 10.0` in both files
|
||||||
- **Hourly mode**: Potential divergence IF portfolio < $25K. Bot may skip exits to avoid PDT violation, holding positions overnight that backtest would have exited same-day. Backtester relies on late-day entry prevention (line 158-166) instead of exit blocking.
|
- **Severity**: Medium (not critical since both paths have the same error)
|
||||||
- **Portfolios >= $25K**: No divergence (PDT rule doesn't apply)
|
- **Code location**: `/home/work/Documents/rust/invest-bot/src/bot.rs:1025` and `/home/work/Documents/rust/invest-bot/src/backtester.rs:828`
|
||||||
- **Verdict**: Acceptable for daily mode. Document for hourly mode. If hourly is primary deployment, verify backtest PDT day-trade count matches late-day entry prevention expectations.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Config Changes Since Last Audit (2026-02-13)
|
## Config Changes Since Last Audit (2026-02-13 v2)
|
||||||
|
|
||||||
User reported these config changes:
|
User reported these config changes:
|
||||||
- Drawdown halt: 12% → 15% (`MAX_DRAWDOWN_HALT`)
|
- Hourly indicator periods scaled 7x:
|
||||||
- Drawdown cooldown: 20 bars → 10 bars (`DRAWDOWN_HALT_BARS`)
|
- MACD: 84/182/63 (was 12/26/9 daily baseline)
|
||||||
- Momentum pool: 10 stocks → 20 stocks (`TOP_MOMENTUM_COUNT`)
|
- Momentum: 441 (was 63 daily baseline)
|
||||||
- Buy threshold: 4.5 → 4.0 (in signal generation)
|
- EMA: 63/147/350 (was 9/21/50 daily baseline)
|
||||||
|
- BB: 140 (was 20 daily baseline)
|
||||||
|
- Volume MA: 140 (was 20 daily baseline)
|
||||||
|
- Caution regime: sizing 0.25 (was 0.5), threshold bump +3.0 (was +2.0)
|
||||||
|
- Risk: 1.5%/trade (was 1.2%), ATR stop 3.5x (was 3.0x), trail 3.0x (was 2.0x), activation 2.0x (unchanged)
|
||||||
|
- Max 8 positions (was 7), top 15 momentum (was 20), time exit 80 bars (was 40), cooldown 10 bars (was 5)
|
||||||
|
- Drawdown tiers: 12%/18%/25% → 15/40/60 bars (was 15%/20%/25% → 10/30/50)
|
||||||
|
- RSI pullback window: 25-55 (was 30-50) in indicators.rs
|
||||||
|
|
||||||
**Verified**: All constants consistent between bot.rs and backtester.rs ✅
|
**Verified**: All config constants consistent between bot.rs and backtester.rs ✅
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## VERIFIED CONSISTENT (2026-02-13 Audit) ✅
|
## VERIFIED CONSISTENT (2026-02-13 Audit v2) ✅
|
||||||
|
|
||||||
### Core Trading Logic ✅
|
### Core Trading Logic ✅
|
||||||
- **Signal generation**: Both use shared `indicators::generate_signal()` (bot:739; bt:583,630)
|
- **Signal generation**: Both use shared `indicators::generate_signal()` (bot:876; bt:758,818)
|
||||||
- **Position sizing**: Both use shared `Strategy::calculate_position_size()` (bot:463-468; bt:199-201)
|
- **Position sizing**: Both use shared `Strategy::calculate_position_size()` (bot:537-542; bt:282-284)
|
||||||
- Volatility-adjusted via ATR
|
- Volatility-adjusted via ATR
|
||||||
- Confidence scaling: 0.7 + 0.3 * confidence
|
- Confidence scaling: 0.4 + 0.6 * confidence (changed from 0.7 + 0.3)
|
||||||
- Max position size cap: 25%
|
- Max position size cap: 20% (was 25%)
|
||||||
- Cash reserve: 5%
|
- Cash reserve: 5%
|
||||||
- **Stop-loss/trailing/time exit**: Both use shared `Strategy::check_stop_loss_take_profit()` (bot:473-486; bt:373-380)
|
- **Stop-loss/trailing/time exit**: Both use shared `Strategy::check_stop_loss_take_profit()` (bot:547-559; bt:462-468)
|
||||||
- Hard max loss cap: 5%
|
- Hard max loss cap: 8% (was 5%)
|
||||||
- ATR-based stop: 3.0x ATR below entry
|
- ATR-based stop: 3.5x ATR below entry (was 3.0x)
|
||||||
- Fixed fallback stop: 2.5%
|
- Fixed fallback stop: 2.5%
|
||||||
- Trailing stop: 2.0x ATR after 2.0x ATR gain
|
- Trailing stop: 3.0x ATR after 2.0x ATR gain (was 2.0x trail, 2.0x activation)
|
||||||
- Time exit: 40 bars if below trailing activation threshold
|
- Time exit: 80 bars if below trailing activation (was 40)
|
||||||
|
|
||||||
### Portfolio Controls ✅
|
### Portfolio Controls ✅
|
||||||
- **Cooldown timers**: Both implement 5-bar cooldown after stop-loss (bot:507-517,659-670; bt:169-173,294-299)
|
- **Cooldown timers**: Both implement 10-bar cooldown after stop-loss (bot:736-746; bt:383-388) [was 5]
|
||||||
- **Ramp-up period**: Both limit to 1 new position per cycle/bar for first 15 bars (bot:543-552; bt:194-196)
|
- **Ramp-up period**: Both limit to 1 new position per cycle/bar for first 15 bars (bot:618-626; bt:277-279)
|
||||||
- **Drawdown circuit breaker**: Both trigger at 15% with 10-bar cooldown (bot:353-362; bt:104-113)
|
- **Drawdown circuit breaker**: Both trigger at 12%/18%/25% with 15/40/60-bar cooldowns (bot:368-408; bt:106-163)
|
||||||
- **BUT**: bot.rs missing peak reset on expiry (see Critical Bug #1)
|
- **Peak reset on expiry**: Both reset peak to current value (bot:442; bt:197) ✅ (FIXED since last audit)
|
||||||
- **Sector limits**: Both enforce max 2 per sector (bot:534-541; bt:184-191)
|
- Tier 3 (25%+) requires bull regime to resume (was 15%/20%/25% → 10/30/50)
|
||||||
- **Max concurrent positions**: Both enforce max 7 (bot:525-532; bt:180-182)
|
- **Sector limits**: Both enforce max 2 per sector (bot:608-614; bt:268-274)
|
||||||
- **Momentum ranking**: Both filter to top 20 momentum stocks (bot:818-838; bt:543-554)
|
- **Max concurrent positions**: Both enforce max 8 (bot:599-606; bt:263-265) [was 7]
|
||||||
- **bars_held increment**: Both increment at START of trading cycle/bar (bot:763-765; bt:539-541)
|
- **Momentum ranking**: Both filter to top 15 momentum stocks (bot:965-985; bt:718-729) [was 20]
|
||||||
|
- **bars_held increment**: Both increment at START of trading cycle/bar (bot:909-912; bt:713-716)
|
||||||
|
|
||||||
### Config Constants — ALL CONSISTENT ✅
|
### Config Constants — ALL CONSISTENT ✅
|
||||||
Both files import and use identical values from config.rs:
|
Both files import and use identical values from config.rs:
|
||||||
- `ATR_STOP_MULTIPLIER`: 3.0x
|
- `ATR_STOP_MULTIPLIER`: 3.5x (was 3.0x)
|
||||||
- `ATR_TRAIL_MULTIPLIER`: 2.0x
|
- `ATR_TRAIL_MULTIPLIER`: 3.0x (was 2.0x)
|
||||||
- `ATR_TRAIL_ACTIVATION_MULTIPLIER`: 2.0x
|
- `ATR_TRAIL_ACTIVATION_MULTIPLIER`: 2.0x (unchanged)
|
||||||
- `MAX_POSITION_SIZE`: 25%
|
- `MAX_POSITION_SIZE`: 20% (was 25%)
|
||||||
- `MAX_CONCURRENT_POSITIONS`: 7
|
- `MAX_CONCURRENT_POSITIONS`: 8 (was 7)
|
||||||
- `MAX_SECTOR_POSITIONS`: 2
|
- `MAX_SECTOR_POSITIONS`: 2
|
||||||
- `MAX_DRAWDOWN_HALT`: 15% (updated from 12%)
|
- `DRAWDOWN_TIER1_PCT`: 12% (was 15%)
|
||||||
- `DRAWDOWN_HALT_BARS`: 10 (updated from 20)
|
- `DRAWDOWN_TIER1_BARS`: 15 (was 10)
|
||||||
- `REENTRY_COOLDOWN_BARS`: 5
|
- `DRAWDOWN_TIER2_PCT`: 18% (was 20%)
|
||||||
|
- `DRAWDOWN_TIER2_BARS`: 40 (was 30)
|
||||||
|
- `DRAWDOWN_TIER3_PCT`: 25% (unchanged)
|
||||||
|
- `DRAWDOWN_TIER3_BARS`: 60 (was 50)
|
||||||
|
- `REENTRY_COOLDOWN_BARS`: 10 (was 5)
|
||||||
- `RAMPUP_PERIOD_BARS`: 15
|
- `RAMPUP_PERIOD_BARS`: 15
|
||||||
- `TOP_MOMENTUM_COUNT`: 20 (updated from 10)
|
- `TOP_MOMENTUM_COUNT`: 15 (was 20)
|
||||||
- `TIME_EXIT_BARS`: 40
|
- `TIME_EXIT_BARS`: 80 (was 40)
|
||||||
- `MIN_CASH_RESERVE`: 5%
|
- `MIN_CASH_RESERVE`: 5%
|
||||||
- `MAX_LOSS_PCT`: 5%
|
- `MAX_LOSS_PCT`: 8% (was 5%)
|
||||||
|
- `REGIME_CAUTION_SIZE_FACTOR`: 0.25 (was 0.5)
|
||||||
|
- `REGIME_CAUTION_THRESHOLD_BUMP`: 3.0 (was 2.0)
|
||||||
|
|
||||||
### Warmup Requirements ✅
|
### Warmup Requirements ✅
|
||||||
**Daily mode**: `max(35 MACD, 15 RSI, 50 EMA, 28 ADX, 20 BB, 63 momentum) + 5 = 68 bars`
|
**Hourly mode**: `max(245 MACD, 15 RSI, 350 EMA, 28 ADX, 140 BB, 441 momentum) + 5 = 446 bars`
|
||||||
**Hourly mode**: `max(35 MACD, 15 RSI, 200 EMA, 28 ADX, 20 BB, 63 momentum) + 5 = 205 bars`
|
|
||||||
|
|
||||||
Calculation in `config.rs:193-206` (`IndicatorParams::min_bars()`)
|
Calculation in `config.rs:239-252` (`IndicatorParams::min_bars()`)
|
||||||
- RSI-2/3 warmup covered by RSI-14 requirement (15 > 3)
|
- Momentum period dominates warmup (441 bars = 63 * 7)
|
||||||
- MACD needs slow + signal periods (26 + 9 = 35)
|
- MACD needs slow + signal (182 + 63 = 245)
|
||||||
|
- EMA trend: 350 (50 * 7)
|
||||||
- ADX needs 2x period for smoothing (14 * 2 = 28)
|
- ADX needs 2x period for smoothing (14 * 2 = 28)
|
||||||
- Hourly EMA-200 dominates warmup requirement
|
- BB period: 140 (20 * 7)
|
||||||
|
- RSI-2/3 warmup covered by RSI-14 requirement (15 > 3)
|
||||||
|
|
||||||
Both bot.rs and backtester.rs fetch sufficient historical data and validate bar count before trading.
|
Both bot.rs and backtester.rs fetch sufficient historical data and validate bar count before trading:
|
||||||
|
- bot.rs: lines 830, 853-860
|
||||||
|
- backtester.rs: lines 476, 523-530
|
||||||
|
|
||||||
### Entry/Exit Flow ✅
|
### Entry/Exit Flow ✅
|
||||||
**Both follow identical two-phase execution**:
|
**Both follow identical two-phase execution**:
|
||||||
- Phase 1: Process all sells (stop-loss, trailing, time exit, signals)
|
- Phase 1: Process all sells (stop-loss, trailing, time exit, signals)
|
||||||
- Phase 2: Process buys for top momentum stocks only
|
- Phase 2: Process buys for top momentum stocks only
|
||||||
- bot.rs lines 800-849
|
- bot.rs lines 947-1035
|
||||||
- backtester.rs lines 556-642
|
- backtester.rs lines 731-844
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## INTENTIONAL DIFFERENCES (Not Bugs) ✅
|
## INTENTIONAL DIFFERENCES (Not Bugs) ✅
|
||||||
|
|
||||||
### 1. Slippage Modeling
|
### 1. Slippage Modeling
|
||||||
- **Backtester**: Applies 10 bps on both entry and exit (backtester.rs:71-78)
|
- **Backtester**: Applies 10 bps on both entry and exit (backtester.rs:86-93)
|
||||||
- **Live bot**: Uses actual fill prices from Alpaca API (bot.rs:567-571)
|
- **Live bot**: Uses actual fill prices from Alpaca API (bot.rs:644-648)
|
||||||
- **Verdict**: Expected difference. Backtester simulates realistic costs; live bot gets market fills.
|
- **Verdict**: Expected difference. Backtester simulates realistic costs; live bot gets market fills.
|
||||||
|
|
||||||
### 2. RSI Short Period Scaling
|
### 2. PDT Protection Strategy
|
||||||
- **Daily mode**: `rsi_short_period: 2` (Connors RSI-2 for mean reversion)
|
- **bot.rs**: Blocks non-stop-loss sells if would trigger PDT (lines 693-705)
|
||||||
- **Hourly mode**: `rsi_short_period: 3` (adjusted for intraday noise)
|
- **backtester.rs**: Blocks entries in last 2 hours of hourly day (lines 238-244)
|
||||||
- **Verdict**: Intentional design choice per comment "Slightly longer for hourly noise"
|
- **Verdict**: Two different approaches to PDT prevention. Daily mode prevents day trades by construction (phase separation makes same-bar buy+sell impossible). Hourly mode uses different strategies but both achieve PDT compliance.
|
||||||
|
|
||||||
### 3. EMA Trend Period Scaling
|
|
||||||
- **Daily mode**: `ema_trend: 50` (50-day trend filter)
|
|
||||||
- **Hourly mode**: `ema_trend: 200` (200-hour ≈ 28.5-day trend filter)
|
|
||||||
- **Verdict**: Hourly uses 4x scaling (not 7x like other indicators) for longer-term trend context. Appears intentional.
|
|
||||||
|
|
||||||
### 4. Hourly Late-Day Entry Prevention
|
|
||||||
- **Backtester**: Blocks entries after 19:00 UTC in hourly mode (backtester.rs:158-166) to prevent same-day stop-loss exits
|
|
||||||
- **Bot**: Relies on PDT exit blocking instead (bot.rs:619-628)
|
|
||||||
- **Verdict**: Two different approaches to PDT prevention. See Critical Divergence #2.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## STRATEGY ARCHITECTURE (2026-02-12, Still Current)
|
## STRATEGY ARCHITECTURE (2026-02-13)
|
||||||
|
|
||||||
### Regime-Adaptive Dual Signal
|
### Regime-Adaptive with Bear Market Protection
|
||||||
The strategy uses **ADX for regime detection** and switches between two modes:
|
The strategy uses **ADX for regime detection** and **SPY for market filter**:
|
||||||
|
|
||||||
#### RANGE-BOUND (ADX < 20): Mean Reversion
|
#### SPY MARKET REGIME FILTER (Primary Risk Gate)
|
||||||
- **Entry**: Connors RSI-2 extreme oversold (RSI-2 < 10) + price above EMA trend
|
- **Bull** (SPY > EMA-200, EMA-50 > EMA-200): full size, normal thresholds
|
||||||
- **Exit**: RSI-2 extreme overbought (RSI-2 > 90) or standard exits
|
- **Caution** (SPY < EMA-50, SPY > EMA-200): 25% size, +3.0 threshold bump
|
||||||
- **Conviction boosters**: Bollinger Band extremes, volume confirmation
|
- **Bear** (SPY < EMA-200, EMA-50 < EMA-200): NO new longs at all
|
||||||
|
|
||||||
#### TRENDING (ADX > 25): Momentum Pullback
|
#### ADX REGIME (Signal Type Selection)
|
||||||
- **Entry**: Pullbacks in strong trends (RSI-14 dips 25-40, price near EMA support, MACD confirming)
|
- **ADX < 20**: Range-bound → mean reversion signals preferred
|
||||||
- **Exit**: Trend break (EMA crossover down) or standard exits
|
- **ADX > 25**: Trending → momentum pullback signals preferred
|
||||||
- **Conviction boosters**: Strong trend (ADX > 40), DI+/DI- alignment
|
|
||||||
|
|
||||||
#### UNIVERSAL SIGNALS (Both Regimes)
|
### Hierarchical Signal Generation (indicators.rs)
|
||||||
- RSI-14 extremes in trending context
|
**NOT additive "indicator soup"** — uses gated filters:
|
||||||
- MACD crossovers
|
|
||||||
- EMA crossovers
|
|
||||||
- Volume gate (reduces scores 50% if volume < 80% of 20-period MA)
|
|
||||||
|
|
||||||
### Signal Thresholds (Updated 2026-02-13)
|
**LAYER 1 (GATE)**: Trend confirmation required
|
||||||
|
- Price > EMA-trend
|
||||||
|
- EMA-short > EMA-long
|
||||||
|
Without both, no buy signal generated.
|
||||||
|
|
||||||
|
**LAYER 2 (ENTRY)**: Momentum + pullback timing
|
||||||
|
- Positive momentum (ROC > 0)
|
||||||
|
- RSI-14 pullback (25-55 range, widened from 30-50)
|
||||||
|
|
||||||
|
**LAYER 3 (CONVICTION)**: Supplementary confirmation
|
||||||
|
- MACD histogram positive
|
||||||
|
- ADX > 25 with DI+ > DI-
|
||||||
|
- Volume above average
|
||||||
|
|
||||||
|
### Signal Thresholds
|
||||||
- **StrongBuy**: total_score >= 7.0
|
- **StrongBuy**: total_score >= 7.0
|
||||||
- **Buy**: total_score >= 4.0 (was 4.5)
|
- **Buy**: total_score >= 4.0
|
||||||
- **StrongSell**: total_score <= -7.0
|
- **StrongSell**: total_score <= -7.0
|
||||||
- **Sell**: total_score <= -4.0
|
- **Sell**: total_score <= -4.0
|
||||||
- **Hold**: everything else
|
- **Hold**: everything else
|
||||||
|
|
||||||
Confidence: `(total_score.abs() / 12.0).min(1.0)`
|
Confidence: `(total_score.abs() / 10.0).min(1.0)` (changed from /12.0)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## KEY LESSONS
|
## KEY LESSONS
|
||||||
|
|
||||||
### 1. Shared Logic Eliminates Drift
|
### 1. Equity Curve SMA Creates Pathological Feedback Loop
|
||||||
Extracting common logic into `strategy.rs` ensures bot and backtester CANNOT diverge. All core trading logic (signal generation, position sizing, stop-loss/trailing/time exit) is now in shared modules.
|
Without removing it from the bot, losing positions drag equity below SMA, blocking ALL new entries, which prevents recovery. This is a self-reinforcing trap. The backtester correctly removed it; the bot must follow.
|
||||||
|
|
||||||
### 2. Drawdown Circuit Breaker Needs Peak Reset on Resume
|
### 2. Shared Logic Eliminates Most Drift
|
||||||
Without resetting the peak when halt expires, any minor dip after partial recovery will immediately re-trigger the halt. This creates cascading halts that keep the bot in cash for extended periods. Backtester had this right; bot.rs needs the fix.
|
Extracting common logic into `strategy.rs` and `indicators.rs` ensures bot and backtester CANNOT diverge on core trading decisions. Almost all consistency issues now are portfolio-level controls, not signal logic.
|
||||||
|
|
||||||
### 3. PDT Protection Strategy Differs by Timeframe
|
### 3. Config Constants Propagation Works Well
|
||||||
- **Daily mode**: Phase separation (sells Phase 1, buys Phase 2) prevents day trades by construction. PDT enforcement not needed.
|
Using `config.rs` constants throughout prevents hardcoded values. All recent parameter changes (7x hourly scaling, 0.25 Caution sizing, tiered drawdowns) were automatically consistent because both files import the same constants.
|
||||||
- **Hourly mode**: Late-day entry prevention (backtester) vs exit blocking (bot) are two valid approaches, but they're not identical. For portfolios < $25K, bot will hold positions overnight more often than backtest suggests.
|
|
||||||
|
|
||||||
### 4. Config Constants Must Be Audited After Every Change
|
### 4. Warmup Must Account for Longest Indicator Chain
|
||||||
Recent changes to drawdown thresholds, momentum pool size, and buy thresholds were all consistent between bot and backtester, but manual audit was required to verify. Future changes should trigger automated consistency checks.
|
For hourly mode with 7x scaling, momentum_period=441 dominates warmup. The `+ 5` safety margin in `min_bars()` is critical for EMA initialization edge cases.
|
||||||
|
|
||||||
### 5. Warmup Must Account for Longest Indicator Chain
|
### 5. Math Errors Can Be Consistently Wrong
|
||||||
For hourly mode, EMA-200 dominates warmup (205 bars). The `+ 5` safety margin in `min_bars()` is critical.
|
The confidence scaling bug (using 12.0 instead of 10.0) is in both files, so they produce the same wrong behavior. This makes it non-critical for consistency but still a bug to fix.
|
||||||
|
|
||||||
### 6. Data Type Consistency for PDT Tracking (FIXED)
|
### 6. Drawdown Peak Reset Now Fixed
|
||||||
bot.rs now uses `Vec<NaiveDate>` for day_trades (line 56), matching backtester.rs (line 42). Previous audit found this as a critical bug; it's now fixed. ✅
|
Previous audit found bot.rs was missing peak reset on halt expiry. This has been FIXED (bot.rs:442 now resets peak).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -184,84 +200,40 @@ When new changes are made, verify:
|
|||||||
1. **Signal generation**: Still using shared `indicators::generate_signal()`?
|
1. **Signal generation**: Still using shared `indicators::generate_signal()`?
|
||||||
2. **Position sizing**: Still using shared `Strategy::calculate_position_size()`?
|
2. **Position sizing**: Still using shared `Strategy::calculate_position_size()`?
|
||||||
3. **Risk management**: Still using shared `Strategy::check_stop_loss_take_profit()`?
|
3. **Risk management**: Still using shared `Strategy::check_stop_loss_take_profit()`?
|
||||||
4. **Cooldown timers**: Identical logic in both files?
|
4. **Equity curve stop**: Check if REMOVED in both files (don't re-add to backtester!)
|
||||||
5. **Ramp-up period**: Identical logic in both files?
|
5. **Cooldown timers**: Identical logic in both files?
|
||||||
6. **Drawdown halt**: Identical trigger logic? Peak reset on expiry?
|
6. **Ramp-up period**: Identical logic in both files?
|
||||||
7. **Sector limits**: Same `MAX_SECTOR_POSITIONS` constant?
|
7. **Drawdown halt**: Identical trigger logic? Peak reset on expiry?
|
||||||
8. **Max positions**: Same `MAX_CONCURRENT_POSITIONS` constant?
|
8. **Sector limits**: Same `MAX_SECTOR_POSITIONS` constant?
|
||||||
9. **Momentum ranking**: Same `TOP_MOMENTUM_COUNT` constant?
|
9. **Max positions**: Same `MAX_CONCURRENT_POSITIONS` constant?
|
||||||
10. **bars_held increment**: Both increment at START of cycle/bar?
|
10. **Momentum ranking**: Same `TOP_MOMENTUM_COUNT` constant?
|
||||||
11. **Warmup calculation**: Does `min_bars()` cover all indicators?
|
11. **bars_held increment**: Both increment at START of cycle/bar?
|
||||||
12. **Config propagation**: Are new constants used consistently?
|
12. **Warmup calculation**: Does `min_bars()` cover all indicators?
|
||||||
13. **NaN handling**: Safe defaults for all indicator checks?
|
13. **NaN handling**: Safe defaults for all indicator checks?
|
||||||
14. **ATR guards**: Checks for `> 0.0` before division?
|
14. **ATR guards**: Checks for `> 0.0` before division?
|
||||||
15. **PDT protection**: Same constants, logic, and data types? Document timeframe-specific behavior.
|
15. **Config propagation**: Are new constants used consistently?
|
||||||
|
16. **Math in regime logic**: Confidence scaling uses correct multiplier (10.0 not 12.0)?
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## FILES AUDITED (2026-02-13)
|
## FILES LOCATIONS
|
||||||
- `/home/work/Documents/rust/invest-bot/src/bot.rs` (933 lines)
|
|
||||||
- `/home/work/Documents/rust/invest-bot/src/backtester.rs` (1002 lines)
|
|
||||||
- `/home/work/Documents/rust/invest-bot/src/config.rs` (222 lines)
|
|
||||||
- `/home/work/Documents/rust/invest-bot/src/strategy.rs` (141 lines)
|
|
||||||
- `/home/work/Documents/rust/invest-bot/src/types.rs` (234 lines)
|
|
||||||
|
|
||||||
**Total**: 2,532 lines audited
|
- `/home/work/Documents/rust/invest-bot/src/bot.rs` — Live trading loop (1119 lines)
|
||||||
**Issues found**: 1 critical bug (drawdown peak reset), 1 critical behavioral divergence (PDT enforcement)
|
- `/home/work/Documents/rust/invest-bot/src/backtester.rs` — Historical simulation (1217 lines)
|
||||||
**Status**: ⚠️ FIX REQUIRED BEFORE PRODUCTION (drawdown peak reset)
|
- `/home/work/Documents/rust/invest-bot/src/strategy.rs` — Shared position sizing and risk management (163 lines)
|
||||||
|
- `/home/work/Documents/rust/invest-bot/src/indicators.rs` — Shared signal generation (673 lines)
|
||||||
|
- `/home/work/Documents/rust/invest-bot/src/config.rs` — All strategy parameters (268 lines)
|
||||||
|
- `/home/work/Documents/rust/invest-bot/src/types.rs` — Data structures (262 lines)
|
||||||
|
|
||||||
|
**Total**: 3,702 lines audited (2026-02-13 v2)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## REQUIRED FIX: Drawdown Peak Reset on Halt Expiry
|
## CRITICAL REMINDERS
|
||||||
|
|
||||||
**File**: `/home/work/Documents/rust/invest-bot/src/bot.rs`
|
- **NEVER re-add equity curve SMA stop to backtester** — it was removed for good reason
|
||||||
**Location**: Lines 365-377 (inside drawdown halt expiry check)
|
- **Remove equity curve SMA stop from bot.rs** — lines 809-826, 904-907, 991-992
|
||||||
|
- **Warmup for hourly mode is 446 bars** — momentum_period=441 dominates
|
||||||
**Current code**:
|
- **Confidence → score conversion is `score = confidence * 10.0`** not 12.0
|
||||||
```rust
|
- **Both files correctly reset peak on drawdown halt expiry** (verified in this audit)
|
||||||
// Auto-resume after time-based cooldown
|
- **PDT protection differs by timeframe** — daily uses phase separation, hourly uses entry/exit blocking
|
||||||
if self.drawdown_halt {
|
|
||||||
if let Some(halt_start) = self.drawdown_halt_start {
|
|
||||||
if self.trading_cycle_count >= halt_start + DRAWDOWN_HALT_BARS {
|
|
||||||
tracing::info!(
|
|
||||||
"Drawdown halt expired after {} cycles. Resuming trading at {:.2}% drawdown.",
|
|
||||||
DRAWDOWN_HALT_BARS,
|
|
||||||
drawdown_pct * 100.0
|
|
||||||
);
|
|
||||||
self.drawdown_halt = false;
|
|
||||||
self.drawdown_halt_start = None;
|
|
||||||
// MISSING: self.peak_portfolio_value = portfolio_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Required change**: Add after line 374:
|
|
||||||
```rust
|
|
||||||
self.peak_portfolio_value = portfolio_value;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Complete fixed block**:
|
|
||||||
```rust
|
|
||||||
// Auto-resume after time-based cooldown
|
|
||||||
if self.drawdown_halt {
|
|
||||||
if let Some(halt_start) = self.drawdown_halt_start {
|
|
||||||
if self.trading_cycle_count >= halt_start + DRAWDOWN_HALT_BARS {
|
|
||||||
tracing::info!(
|
|
||||||
"Drawdown halt expired after {} cycles. Resuming trading. \
|
|
||||||
Peak reset from ${:.2} to ${:.2} (was {:.2}% drawdown).",
|
|
||||||
DRAWDOWN_HALT_BARS,
|
|
||||||
self.peak_portfolio_value,
|
|
||||||
portfolio_value,
|
|
||||||
drawdown_pct * 100.0
|
|
||||||
);
|
|
||||||
self.drawdown_halt = false;
|
|
||||||
self.drawdown_halt_start = None;
|
|
||||||
// Reset peak to current value to prevent cascading re-triggers.
|
|
||||||
self.peak_portfolio_value = portfolio_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this matters**: Without this reset, after a 15% drawdown triggers a 10-bar halt, the bot measures future drawdown from the OLD peak. If the portfolio recovers to 12% drawdown and then dips to 13%, it immediately re-triggers the halt. This creates cascading halts. The backtester correctly resets the peak, so backtest results show fewer/shorter halts than live trading would experience.
|
|
||||||
|
|||||||
@@ -819,13 +819,9 @@ impl Backtester {
|
|||||||
|
|
||||||
// Apply regime threshold bump: in Caution, require stronger conviction
|
// Apply regime threshold bump: in Caution, require stronger conviction
|
||||||
let effective_buy = if buy_threshold_bump > 0.0 {
|
let effective_buy = if buy_threshold_bump > 0.0 {
|
||||||
// Re-evaluate: the signal score is buy_score - sell_score.
|
// Reverse confidence back to score: confidence = score / 10.0
|
||||||
// We need to check if the score exceeds the bumped threshold.
|
// In Caution, require score >= 4.0 + bump (7.0 → StrongBuy territory).
|
||||||
// Since we don't have the raw score, use confidence as a proxy:
|
let approx_score = signal.confidence * 10.0;
|
||||||
// confidence = score.abs() / 12.0, so score = confidence * 12.0
|
|
||||||
// A Buy signal means score >= 4.0 (our threshold).
|
|
||||||
// In Caution, we require score >= 4.0 + bump (6.0 → StrongBuy territory).
|
|
||||||
let approx_score = signal.confidence * 12.0;
|
|
||||||
approx_score >= (4.0 + buy_threshold_bump) && signal.signal.is_buy()
|
approx_score >= (4.0 + buy_threshold_bump) && signal.signal.is_buy()
|
||||||
} else {
|
} else {
|
||||||
signal.signal.is_buy()
|
signal.signal.is_buy()
|
||||||
|
|||||||
35
src/bot.rs
35
src/bot.rs
@@ -17,7 +17,6 @@ use crate::config::{
|
|||||||
DRAWDOWN_TIER2_PCT, DRAWDOWN_TIER2_BARS,
|
DRAWDOWN_TIER2_PCT, DRAWDOWN_TIER2_BARS,
|
||||||
DRAWDOWN_TIER3_PCT, DRAWDOWN_TIER3_BARS,
|
DRAWDOWN_TIER3_PCT, DRAWDOWN_TIER3_BARS,
|
||||||
DRAWDOWN_TIER3_REQUIRE_BULL,
|
DRAWDOWN_TIER3_REQUIRE_BULL,
|
||||||
EQUITY_CURVE_SMA_PERIOD,
|
|
||||||
REGIME_SPY_SYMBOL, REGIME_EMA_SHORT, REGIME_EMA_LONG,
|
REGIME_SPY_SYMBOL, REGIME_EMA_SHORT, REGIME_EMA_LONG,
|
||||||
REGIME_CAUTION_SIZE_FACTOR, REGIME_CAUTION_THRESHOLD_BUMP,
|
REGIME_CAUTION_SIZE_FACTOR, REGIME_CAUTION_THRESHOLD_BUMP,
|
||||||
};
|
};
|
||||||
@@ -805,24 +804,11 @@ impl TradingBot {
|
|||||||
determine_market_regime(last_row, ema50, ema200)
|
determine_market_regime(last_row, ema50, ema200)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the equity curve is below its N-snapshot SMA (trailing equity stop).
|
// Equity curve SMA stop REMOVED: it creates a pathological feedback loop
|
||||||
fn equity_below_sma(&self) -> bool {
|
// where losing positions drag equity below the SMA, blocking new entries,
|
||||||
if self.equity_history.len() < EQUITY_CURVE_SMA_PERIOD {
|
// which prevents recovery. The SPY regime filter and drawdown circuit
|
||||||
return false;
|
// breaker handle macro risk without this self-reinforcing trap.
|
||||||
}
|
// (Matches backtester behavior which also removed this.)
|
||||||
|
|
||||||
// Don't block when holding zero positions — otherwise flat equity
|
|
||||||
// stays below the SMA forever and the bot can never recover.
|
|
||||||
if self.strategy.entry_prices.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let recent = &self.equity_history[self.equity_history.len() - EQUITY_CURVE_SMA_PERIOD..];
|
|
||||||
let sma: f64 = recent.iter().map(|e| e.portfolio_value).sum::<f64>()
|
|
||||||
/ EQUITY_CURVE_SMA_PERIOD as f64;
|
|
||||||
|
|
||||||
self.current_portfolio_value < sma
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Analysis ─────────────────────────────────────────────────────
|
// ── Analysis ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -901,11 +887,6 @@ impl TradingBot {
|
|||||||
tracing::info!("Market regime: {} (SPY EMA-{}/EMA-{})",
|
tracing::info!("Market regime: {} (SPY EMA-{}/EMA-{})",
|
||||||
self.current_regime.as_str(), REGIME_EMA_SHORT, REGIME_EMA_LONG);
|
self.current_regime.as_str(), REGIME_EMA_SHORT, REGIME_EMA_LONG);
|
||||||
|
|
||||||
if self.equity_below_sma() {
|
|
||||||
tracing::info!("Equity curve below {}-period SMA — no new entries this cycle",
|
|
||||||
EQUITY_CURVE_SMA_PERIOD);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment bars_held once per trading cycle (matches backtester's per-bar increment)
|
// Increment bars_held once per trading cycle (matches backtester's per-bar increment)
|
||||||
for meta in self.position_meta.values_mut() {
|
for meta in self.position_meta.values_mut() {
|
||||||
meta.bars_held += 1;
|
meta.bars_held += 1;
|
||||||
@@ -985,11 +966,9 @@ impl TradingBot {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Phase 3: Process buys in momentum-ranked order (highest momentum first)
|
// Phase 3: Process buys in momentum-ranked order (highest momentum first)
|
||||||
// Gate by market regime and equity curve stop
|
// Gate by market regime
|
||||||
if !self.current_regime.allows_new_longs() {
|
if !self.current_regime.allows_new_longs() {
|
||||||
tracing::info!("BEAR regime — skipping all buys this cycle");
|
tracing::info!("BEAR regime — skipping all buys this cycle");
|
||||||
} else if self.equity_below_sma() {
|
|
||||||
tracing::info!("Equity below SMA — skipping all buys this cycle");
|
|
||||||
} else {
|
} else {
|
||||||
let regime_size_factor = match self.current_regime {
|
let regime_size_factor = match self.current_regime {
|
||||||
MarketRegime::Bull => 1.0,
|
MarketRegime::Bull => 1.0,
|
||||||
@@ -1022,7 +1001,7 @@ impl TradingBot {
|
|||||||
|
|
||||||
// Apply regime threshold bump in Caution
|
// Apply regime threshold bump in Caution
|
||||||
let effective_buy = if buy_threshold_bump > 0.0 {
|
let effective_buy = if buy_threshold_bump > 0.0 {
|
||||||
let approx_score = signal.confidence * 12.0;
|
let approx_score = signal.confidence * 10.0;
|
||||||
approx_score >= (4.0 + buy_threshold_bump) && signal.signal.is_buy()
|
approx_score >= (4.0 + buy_threshold_bump) && signal.signal.is_buy()
|
||||||
} else {
|
} else {
|
||||||
signal.signal.is_buy()
|
signal.signal.is_buy()
|
||||||
|
|||||||
@@ -65,28 +65,28 @@ pub const MIN_ATR_PCT: f64 = 0.005;
|
|||||||
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 = 10; // Top decile: Jegadeesh-Titman (1993) strongest effect
|
pub const TOP_MOMENTUM_COUNT: usize = 15; // Top quintile: enough candidates for 8 positions
|
||||||
// Risk Management
|
// Risk Management
|
||||||
pub const MAX_POSITION_SIZE: f64 = 0.25; // Slightly larger for concentrated bets
|
pub const MAX_POSITION_SIZE: f64 = 0.20; // 20% max to reduce concentration risk
|
||||||
pub const MIN_CASH_RESERVE: f64 = 0.05;
|
pub const MIN_CASH_RESERVE: f64 = 0.05;
|
||||||
pub const STOP_LOSS_PCT: f64 = 0.025;
|
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 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_ACTIVATION: f64 = 0.04; // Activate earlier to protect profits
|
||||||
pub const TRAILING_STOP_DISTANCE: f64 = 0.05; // Wider trail to let winners run
|
pub const TRAILING_STOP_DISTANCE: f64 = 0.05; // Wider trail to let winners run
|
||||||
// ATR-based risk management
|
// ATR-based risk management
|
||||||
pub const RISK_PER_TRADE: f64 = 0.01; // Conservative per-trade risk, compensated by more positions
|
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.0; // Wider stops — research shows tighter stops hurt
|
pub const ATR_STOP_MULTIPLIER: f64 = 3.5; // Wide stops reduce false stop-outs (the #1 loss source)
|
||||||
pub const ATR_TRAIL_MULTIPLIER: f64 = 2.5; // Wide trail from HWM so winners have room to breathe
|
pub const ATR_TRAIL_MULTIPLIER: f64 = 3.0; // Wide trail so winners run longer
|
||||||
pub const ATR_TRAIL_ACTIVATION_MULTIPLIER: f64 = 1.5; // Activate earlier (1.5x ATR gain) to protect profits
|
pub const ATR_TRAIL_ACTIVATION_MULTIPLIER: f64 = 2.0; // Don't activate trail too early
|
||||||
// Portfolio-level controls
|
// Portfolio-level controls
|
||||||
pub const MAX_CONCURRENT_POSITIONS: usize = 10; // More diversification reduces idiosyncratic risk
|
pub const MAX_CONCURRENT_POSITIONS: usize = 8; // Fewer positions = higher conviction per trade
|
||||||
pub const MAX_SECTOR_POSITIONS: usize = 2;
|
pub const MAX_SECTOR_POSITIONS: usize = 2;
|
||||||
// Old single-tier drawdown constants (replaced by tiered system below)
|
// Old single-tier drawdown constants (replaced by tiered system below)
|
||||||
// pub const MAX_DRAWDOWN_HALT: f64 = 0.15;
|
// pub const MAX_DRAWDOWN_HALT: f64 = 0.15;
|
||||||
// pub const DRAWDOWN_HALT_BARS: usize = 10;
|
// pub const DRAWDOWN_HALT_BARS: usize = 10;
|
||||||
// Time-based exit
|
// Time-based exit
|
||||||
pub const TIME_EXIT_BARS: usize = 60; // Patient — now only exits losers, winners use trailing stop
|
pub const TIME_EXIT_BARS: usize = 80; // More patience for losers on hourly bars
|
||||||
pub const REENTRY_COOLDOWN_BARS: usize = 5; // Shorter cooldown
|
pub const REENTRY_COOLDOWN_BARS: usize = 10; // Longer cooldown to reduce churn
|
||||||
pub const RAMPUP_PERIOD_BARS: usize = 15; // Faster ramp-up
|
pub const RAMPUP_PERIOD_BARS: usize = 15; // Faster ramp-up
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// Market Regime Filter (SPY-based)
|
// Market Regime Filter (SPY-based)
|
||||||
@@ -102,10 +102,11 @@ pub const RAMPUP_PERIOD_BARS: usize = 15; // Faster ramp-up
|
|||||||
pub const REGIME_SPY_SYMBOL: &str = "SPY";
|
pub const REGIME_SPY_SYMBOL: &str = "SPY";
|
||||||
pub const REGIME_EMA_SHORT: usize = 50; // Fast regime EMA
|
pub const REGIME_EMA_SHORT: usize = 50; // Fast regime EMA
|
||||||
pub const REGIME_EMA_LONG: usize = 200; // Slow regime EMA (the "golden cross" line)
|
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).
|
/// In Caution regime, multiply position size by this factor.
|
||||||
pub const REGIME_CAUTION_SIZE_FACTOR: f64 = 0.5;
|
/// Reduced from 0.5 to 0.25: the 2022 bear showed Caution still bleeds at 50% size.
|
||||||
/// In Caution regime, add this to buy thresholds (require stronger signals).
|
pub const REGIME_CAUTION_SIZE_FACTOR: f64 = 0.25;
|
||||||
pub const REGIME_CAUTION_THRESHOLD_BUMP: f64 = 2.0;
|
/// 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
|
// Scaled Drawdown Circuit Breaker
|
||||||
@@ -113,12 +114,12 @@ pub const REGIME_CAUTION_THRESHOLD_BUMP: f64 = 2.0;
|
|||||||
// The old fixed 10-bar cooldown is inadequate for real bear markets.
|
// The old fixed 10-bar cooldown is inadequate for real bear markets.
|
||||||
// Scale the halt duration with severity so that deeper drawdowns force
|
// Scale the halt duration with severity so that deeper drawdowns force
|
||||||
// longer cooling periods. At 25%+ DD, also require bull regime to resume.
|
// 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_PCT: f64 = 0.12; // 12% → 15 bars (catch earlier)
|
||||||
pub const DRAWDOWN_TIER1_BARS: usize = 10;
|
pub const DRAWDOWN_TIER1_BARS: usize = 15;
|
||||||
pub const DRAWDOWN_TIER2_PCT: f64 = 0.20; // 20% → 30 bars
|
pub const DRAWDOWN_TIER2_PCT: f64 = 0.18; // 18% → 40 bars
|
||||||
pub const DRAWDOWN_TIER2_BARS: usize = 30;
|
pub const DRAWDOWN_TIER2_BARS: usize = 40;
|
||||||
pub const DRAWDOWN_TIER3_PCT: f64 = 0.25; // 25%+ → 50 bars + require bull regime
|
pub const DRAWDOWN_TIER3_PCT: f64 = 0.25; // 25%+ → 60 bars + require bull regime
|
||||||
pub const DRAWDOWN_TIER3_BARS: usize = 50;
|
pub const DRAWDOWN_TIER3_BARS: usize = 60;
|
||||||
/// If true, after a Tier 3 drawdown (>=25%), require bull market regime
|
/// If true, after a Tier 3 drawdown (>=25%), require bull market regime
|
||||||
/// before resuming new entries even after the bar cooldown expires.
|
/// before resuming new entries even after the bar cooldown expires.
|
||||||
pub const DRAWDOWN_TIER3_REQUIRE_BULL: bool = true;
|
pub const DRAWDOWN_TIER3_REQUIRE_BULL: bool = true;
|
||||||
@@ -213,21 +214,25 @@ impl IndicatorParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Create parameters for hourly timeframe.
|
/// 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 {
|
pub fn hourly() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rsi_period: 14,
|
rsi_period: 14,
|
||||||
rsi_short_period: 3, // Slightly longer for hourly noise
|
rsi_short_period: 3,
|
||||||
macd_fast: 12,
|
macd_fast: 84, // 12 * 7
|
||||||
macd_slow: 26,
|
macd_slow: 182, // 26 * 7
|
||||||
macd_signal: 9,
|
macd_signal: 63, // 9 * 7
|
||||||
momentum_period: 63,
|
momentum_period: 441, // 63 * 7 = quarterly momentum
|
||||||
ema_short: 9,
|
ema_short: 63, // 9 * 7 ~ daily 9-day EMA
|
||||||
ema_long: 21,
|
ema_long: 147, // 21 * 7 ~ daily 21-day EMA
|
||||||
ema_trend: 200,
|
ema_trend: 350, // 50 * 7 ~ daily 50-day EMA
|
||||||
adx_period: 14,
|
adx_period: 14,
|
||||||
bb_period: 20,
|
bb_period: 140, // 20 * 7
|
||||||
atr_period: 14,
|
atr_period: 14,
|
||||||
volume_ma_period: 20,
|
volume_ma_period: 140, // 20 * 7
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Get the minimum number of bars required for indicator calculation.
|
/// Get the minimum number of bars required for indicator calculation.
|
||||||
|
|||||||
@@ -532,13 +532,13 @@ pub fn generate_signal(symbol: &str, current: &IndicatorRow, previous: &Indicato
|
|||||||
buy_score += 4.0;
|
buy_score += 4.0;
|
||||||
|
|
||||||
// TIMING: RSI-14 pullback in uptrend (the "buy the dip" pattern)
|
// TIMING: RSI-14 pullback in uptrend (the "buy the dip" pattern)
|
||||||
// RSI 30-50 means price has pulled back but trend is intact.
|
// Widened to 25-55: in strong uptrends RSI often stays 40-65,
|
||||||
// This is the most robust single-stock entry timing signal.
|
// so the old 30-50 window missed many good pullback entries.
|
||||||
if !rsi.is_nan() && rsi >= 30.0 && rsi <= 50.0 {
|
if !rsi.is_nan() && rsi >= 25.0 && rsi <= 55.0 {
|
||||||
buy_score += 3.0;
|
buy_score += 3.0;
|
||||||
}
|
}
|
||||||
// Moderate pullback (RSI 50-60) still gets some credit
|
// Moderate pullback (RSI 55-65) still gets some credit
|
||||||
else if !rsi.is_nan() && rsi > 50.0 && rsi <= 60.0 {
|
else if !rsi.is_nan() && rsi > 55.0 && rsi <= 65.0 {
|
||||||
buy_score += 1.0;
|
buy_score += 1.0;
|
||||||
}
|
}
|
||||||
// RSI > 70 = overbought, do not add to buy score (chasing)
|
// RSI > 70 = overbought, do not add to buy score (chasing)
|
||||||
|
|||||||
Reference in New Issue
Block a user