PDT protection

This commit is contained in:
zastian-dev
2026-02-12 18:14:53 +00:00
parent 223051f9d8
commit 80a8e7c346
5 changed files with 396 additions and 46 deletions

View File

@@ -1,8 +1,18 @@
# Consistency Auditor Memory
## Last Audit: 2026-02-12 (Regime-Adaptive Dual Strategy Update)
## Last Audit: 2026-02-12 (PDT Protection)
### AUDIT RESULT: ✅ NO CRITICAL BUGS FOUND
### 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.
@@ -129,6 +139,7 @@ Confidence: `(total_score.abs() / 12.0).min(1.0)`
- **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
@@ -153,6 +164,13 @@ When ATR is zero/unavailable (e.g., low volatility or warmup), code falls back t
### 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)
@@ -173,17 +191,104 @@ When new changes are made, verify:
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)
- `/home/work/Documents/rust/invest-bot/src/bot.rs` (785 lines)
- `/home/work/Documents/rust/invest-bot/src/backtester.rs` (880 lines)
- `/home/work/Documents/rust/invest-bot/src/config.rs` (199 lines)
- `/home/work/Documents/rust/invest-bot/src/indicators.rs` (651 lines)
- `/home/work/Documents/rust/invest-bot/src/strategy.rs` (141 lines)
## 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,890 lines audited
**Issues found**: 0 critical, 0 medium, 0 low
**Status**: PRODUCTION READY
**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