Files
vibe-invest/.claude/agent-memory/consistency-auditor/MEMORY.md
2026-02-12 18:14:53 +00:00

295 lines
12 KiB
Markdown

# Consistency Auditor Memory
## Last Audit: 2026-02-12 (PDT Protection)
### AUDIT RESULT: ⚠️ 1 CRITICAL BUG FOUND
**PDT (Pattern Day Trading) protection data type mismatch:**
- bot.rs uses `Vec<String>` for day_trades (line 56)
- backtester.rs uses `Vec<NaiveDate>` for day_trades (line 42)
- **Impact**: String parsing on every PDT check, silent failures on malformed dates, performance degradation
- **Fix required**: Change bot.rs to use `Vec<NaiveDate>` internally (see detailed fix below)
---
## Previous Audit: 2026-02-12 (Regime-Adaptive Dual Strategy Update)
The refactor to extract shared logic into `strategy.rs` has **eliminated all previous consistency issues**. Bot and backtester now share identical implementations for all critical trading logic.
---
## VERIFIED CONSISTENT (2026-02-12)
### Core Trading Logic ✅
- **Signal generation**: Both use shared `indicators::generate_signal()` (indicators.rs:442-650)
- **Position sizing**: Both use shared `Strategy::calculate_position_size()` (strategy.rs:29-55)
- Volatility-adjusted via ATR
- Confidence scaling: 0.7 + 0.3 * confidence
- Max position size cap: 25%
- Cash reserve: 5%
- **Stop-loss/trailing/time exit**: Both use shared `Strategy::check_stop_loss_take_profit()` (strategy.rs:57-128)
- Hard max loss cap: 5%
- ATR-based stop: 3.0x ATR below entry
- Fixed fallback stop: 2.5%
- Trailing stop: 2.0x ATR after 2.0x ATR gain
- Time exit: 40 bars if below trailing activation threshold
### Portfolio Controls ✅
- **Cooldown timers**: Both implement 5-bar cooldown after stop-loss (bot:395-406,521-533; bt:133-138,242-247)
- **Ramp-up period**: Both limit to 1 new position per bar for first 15 bars (bot:433-441; bt:158-161)
- **Drawdown circuit breaker**: Both halt for 20 bars at 12% drawdown (bot:244-268; bt:83-118)
- **Sector limits**: Both enforce max 2 per sector (bot:423-430; bt:149-156)
- **Max concurrent positions**: Both enforce max 7 (bot:414-421; bt:145-147)
- **Momentum ranking**: Both filter to top 10 momentum stocks (bot:669-690; bt:438-449)
- **bars_held increment**: Both increment at START of trading cycle/bar (bot:614-617; bt:433-436)
### Warmup Requirements ✅
**Daily mode**: `max(35 MACD, 15 RSI, 50 EMA, 28 ADX, 20 BB, 63 momentum) + 5 = 68 bars`
**Hourly mode**: `max(35 MACD, 15 RSI, 200 EMA, 28 ADX, 20 BB, 63 momentum) + 5 = 205 bars`
Calculation in `config.rs:169-183` (`IndicatorParams::min_bars()`)
- RSI-2/3 warmup covered by RSI-14 requirement (15 > 3)
- MACD needs slow + signal periods (26 + 9 = 35)
- ADX needs 2x period for smoothing (14 * 2 = 28)
- Hourly EMA-200 dominates warmup requirement
Both bot.rs and backtester.rs fetch sufficient historical data and validate bar count before trading.
---
## INTENTIONAL DIFFERENCES (Not Bugs) ✅
### 1. Slippage Modeling
- **Backtester**: Applies 10 bps on both entry and exit (backtester.rs:63-71)
- **Live bot**: Uses actual fill prices from Alpaca API (bot.rs:456-460)
- **Verdict**: Expected difference. Backtester simulates realistic costs; live bot gets market fills.
### 2. RSI Short Period Scaling
- **Daily mode**: `rsi_short_period: 2` (Connors RSI-2 for mean reversion)
- **Hourly mode**: `rsi_short_period: 3` (adjusted for intraday noise)
- **Verdict**: Intentional design choice per comment "Slightly longer for hourly noise"
### 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.
---
## STRATEGY ARCHITECTURE (2026-02-12)
### Regime-Adaptive Dual Signal
The new strategy uses **ADX for regime detection** and switches between two modes:
#### RANGE-BOUND (ADX < 20): Mean Reversion
- **Entry**: Connors RSI-2 extreme oversold (RSI-2 < 10) + price above 200 EMA
- **Exit**: RSI-2 extreme overbought (RSI-2 > 90) or standard exits
- **Conviction boosters**: Bollinger Band extremes, volume confirmation
- **Logic**: indicators.rs:490-526
#### TRENDING (ADX > 25): Momentum Pullback
- **Entry**: Pullbacks in strong trends (RSI-14 dips 25-40, price near EMA support, MACD confirming)
- **Exit**: Trend break (EMA crossover down) or standard exits
- **Conviction boosters**: Strong trend (ADX > 40), DI+/DI- alignment
- **Logic**: indicators.rs:531-557
#### UNIVERSAL SIGNALS (Both Regimes)
- RSI-14 extremes in trending context (indicators.rs:564-570)
- MACD crossovers (indicators.rs:573-583)
- EMA crossovers (indicators.rs:599-608)
- Volume gate (reduces scores 50% if volume < 80% of 20-period MA) (indicators.rs:611-614)
### Signal Thresholds
- **StrongBuy**: total_score >= 7.0
- **Buy**: total_score >= 4.5
- **StrongSell**: total_score <= -7.0
- **Sell**: total_score <= -4.0
- **Hold**: everything else
Confidence: `(total_score.abs() / 12.0).min(1.0)`
---
## CONFIG PARAMETERS (2026-02-12)
### Indicator Periods
- RSI: 14 (standard), RSI-2 (daily) / RSI-3 (hourly) for mean reversion
- MACD: 12/26/9 (standard)
- Momentum: 63 bars
- EMA: 9/21/50 (daily), 9/21/200 (hourly)
- ADX: 14, thresholds: 20 (range), 25 (trend), 40 (strong)
- Bollinger Bands: 20-period, 2 std dev
- ATR: 14-period
- Volume MA: 20-period, threshold: 0.8x
### Risk Management
- **Position sizing**: 1.2% risk per trade (RISK_PER_TRADE)
- **ATR stop**: 3.0x ATR below entry (was 2.5x)
- **ATR trailing stop**: 2.0x ATR distance, activates after 2.0x ATR gain (was 1.5x/1.5x)
- **Max position size**: 25% (was 22%)
- **Max loss cap**: 5% (was 4%)
- **Stop loss fallback**: 2.5% (when ATR unavailable)
- **Time exit**: 40 bars (was 30)
- **Cash reserve**: 5%
### Portfolio Limits
- **Max concurrent positions**: 7 (was 5)
- **Max per sector**: 2 (unchanged)
- **Momentum ranking**: Top 10 stocks (was 4)
- **Drawdown halt**: 12% triggers 20-bar cooldown (was 35 bars)
- **Reentry cooldown**: 5 bars after stop-loss (was 7)
- **Ramp-up period**: 15 bars, 1 new position per bar (was 30 bars)
- **PDT protection**: Max 3 day trades in rolling 5-business-day window (bot:34-36; bt:279-280)
### Backtester
- **Slippage**: 10 bps per trade
- **Risk-free rate**: 5% annually for Sharpe/Sortino
---
## KEY LESSONS
### 1. Shared Logic Eliminates Drift
Extracting common logic into `strategy.rs` ensures bot and backtester CANNOT diverge. Previously, duplicate implementations led to subtle differences (partial exits, bars_held increment timing, cooldown logic).
### 2. Warmup Must Account for Longest Chain
For hourly mode, EMA-200 dominates warmup (205 bars). ADX also needs 2x period (28 bars) for proper smoothing. The `+ 5` safety margin is critical.
### 3. NaN Handling is Critical
Indicators can produce NaN during warmup or with insufficient data. The signal generator uses safe defaults (e.g., `if adx.is_nan() { 22.0 }`) to prevent scoring errors.
### 4. ATR Fallbacks Prevent Edge Cases
When ATR is zero/unavailable (e.g., low volatility or warmup), code falls back to fixed percentage stops. Without this, position sizing could explode or stops could fail.
### 5. Slippage Modeling is Non-Negotiable
The backtester applies 10 bps slippage on both sides (20 bps round-trip) to simulate realistic fills. This prevents overfitting to unrealistic backtest performance.
### 6. Data Type Consistency Matters for PDT Protection
**CRITICAL**: bot.rs and backtester.rs must use the SAME data type for day_trades tracking. Using `Vec<String>` in bot.rs vs `Vec<NaiveDate>` in backtester.rs creates:
- String parsing overhead on every PDT check
- Silent failures if malformed dates enter the system (parse errors return false)
- Inconsistent error handling between live and backtest
- **Fix**: Change bot.rs to store `Vec<NaiveDate>` internally, parse once on load, serialize to JSON as strings
---
## AUDIT CHECKLIST (For Future Audits)
When new changes are made, verify:
1. **Signal generation**: Still using shared `indicators::generate_signal()`?
2. **Position sizing**: Still using shared `Strategy::calculate_position_size()`?
3. **Risk management**: Still using shared `Strategy::check_stop_loss_take_profit()`?
4. **Cooldown timers**: Identical logic in both files?
5. **Ramp-up period**: Identical logic in both files?
6. **Drawdown halt**: Identical trigger and resume logic?
7. **Sector limits**: Same `MAX_SECTOR_POSITIONS` constant?
8. **Max positions**: Same `MAX_CONCURRENT_POSITIONS` constant?
9. **Momentum ranking**: Same `TOP_MOMENTUM_COUNT` constant?
10. **bars_held increment**: Both increment at START of cycle/bar?
11. **Warmup calculation**: Does `min_bars()` cover all indicators?
12. **Config propagation**: Are new constants used consistently?
13. **NaN handling**: Safe defaults for all indicator checks?
14. **ATR guards**: Checks for `> 0.0` before division?
15. **PDT protection**: Same constants, logic, and DATA TYPES in both files?
---
## FILES AUDITED (2026-02-12 PDT Audit)
- `/home/work/Documents/rust/invest-bot/src/bot.rs` (921 lines)
- `/home/work/Documents/rust/invest-bot/src/backtester.rs` (907 lines)
- `/home/work/Documents/rust/invest-bot/src/types.rs` (234 lines)
- `/home/work/Documents/rust/invest-bot/src/paths.rs` (68 lines)
**Total**: 2,130 lines audited
**Issues found**: 1 critical (data type mismatch), 0 medium, 0 low
**Status**: ⚠️ FIX REQUIRED BEFORE PRODUCTION
---
## CRITICAL FIX REQUIRED: PDT Data Type Mismatch
**Problem**: bot.rs stores day_trades as `Vec<String>`, backtester.rs stores as `Vec<NaiveDate>`
**Required Changes to bot.rs:**
1. Line 56: Change field type
```rust
day_trades: Vec<NaiveDate>, // was Vec<String>
```
2. Lines 197-218: Load with parse-once strategy
```rust
fn load_day_trades(&mut self) {
if LIVE_DAY_TRADES_FILE.exists() {
match std::fs::read_to_string(&*LIVE_DAY_TRADES_FILE) {
Ok(content) if !content.is_empty() => {
match serde_json::from_str::<Vec<String>>(&content) {
Ok(date_strings) => {
self.day_trades = date_strings
.iter()
.filter_map(|s| NaiveDate::parse_from_str(s, "%Y-%m-%d").ok())
.collect();
self.prune_old_day_trades();
if !self.day_trades.is_empty() {
tracing::info!("Loaded {} day trades in rolling window.", self.day_trades.len());
}
}
Err(e) => tracing::error!("Error parsing day trades file: {}", e),
}
}
_ => {}
}
}
}
```
3. Lines 220-229: Serialize to JSON as strings
```rust
fn save_day_trades(&self) {
let date_strings: Vec<String> = self.day_trades
.iter()
.map(|d| d.format("%Y-%m-%d").to_string())
.collect();
match serde_json::to_string_pretty(&date_strings) {
Ok(json) => {
if let Err(e) = std::fs::write(&*LIVE_DAY_TRADES_FILE, json) {
tracing::error!("Error saving day trades file: {}", e);
}
}
Err(e) => tracing::error!("Error serializing day trades: {}", e),
}
}
```
4. Lines 231-239: Use native date comparison
```rust
fn prune_old_day_trades(&mut self) {
let cutoff = self.business_days_ago(PDT_ROLLING_BUSINESS_DAYS);
self.day_trades.retain(|&d| d >= cutoff);
}
```
5. Lines 256-267: Use native date comparison
```rust
fn day_trades_in_window(&self) -> usize {
let cutoff = self.business_days_ago(PDT_ROLLING_BUSINESS_DAYS);
self.day_trades.iter().filter(|&&d| d >= cutoff).count()
}
```
6. Lines 284-289: Push NaiveDate
```rust
fn record_day_trade(&mut self) {
let today = Utc::now().date_naive();
self.day_trades.push(today);
self.save_day_trades();
}
```
**Benefits of fix:**
- Type safety: malformed dates cannot enter the vec
- Performance: native date comparison vs string parsing on every check
- Consistency: matches backtester implementation exactly
- Reliability: no silent failures from parse errors