feat(ml): WAVE 29 DQN Codebase Cleanup & Refactoring Campaign

BREAKING CHANGES:
- Removed orphaned dqn.rs monolithic trainer (4,975 lines)
- Removed orphaned dqn_ensemble.rs module (816 lines)
- Removed orphaned tft.rs and tft_complete_int8_integration_test.rs
- TFT trainer split into modular directory structure

DQN Module Refactoring:
- Split trainers/dqn.rs into modular structure (config.rs, statistics.rs, trainer.rs)
- Fixed hyperopt 39D search space (continuous params only)
- Boolean flags (use_dueling, use_double_dqn, use_per, use_noisy_nets) are now FIXED architectural decisions
- use_distributional defaults to false (Candle BUG #36 - scatter_add gradient issues)

Clean Module Structure:
- ml/src/trainers/dqn/ directory with proper mod.rs exports
- ml/src/trainers/tft/ directory with config.rs, types.rs, model.rs, trainer.rs, tests.rs
- All P0 features validated: TD-error clamping, batch diversity, LR scheduler, priority staleness

Documentation:
- Added comprehensive docs in docs/codebase-cleanup/
- ADR-001 for DQN refactoring decisions
- Rainbow DQN component matrix and quick reference guides

Build Status: Compiles with zero errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2025-11-27 23:46:13 +01:00
parent 2c1acda2f3
commit 2df1ea92e1
763 changed files with 247870 additions and 1714 deletions

View File

@@ -0,0 +1,286 @@
#!/usr/bin/env python3
"""Add priority staleness tracking to PER buffer"""
import re
def add_staleness_tracking(filepath):
with open(filepath, 'r') as f:
content = f.read()
# 1. Add field to struct
struct_pattern = r'(pub struct PrioritizedReplayBuffer \{[^}]+priorities: Arc<Mutex<SegmentTree>>,)'
struct_replacement = r'\1\n priority_update_steps: Arc<RwLock<Vec<u64>>>,'
content = re.sub(struct_pattern, struct_replacement, content)
# 2. Initialize in new()
new_pattern = r'(priorities: Arc::new\(Mutex::new\(SegmentTree::new\(config\.capacity\)\)\),)'
new_replacement = r'\1\n priority_update_steps: Arc::new(RwLock::new(vec![0; config.capacity])),'
content = re.sub(new_pattern, new_replacement, content)
# 3. Update push() - add current_step variable
push_pattern1 = r'(let index = self\.position\.fetch_add\(1, Ordering::AcqRel\) % self\.config\.capacity;)'
push_replacement1 = r'\1\n let current_step = self.training_step.load(Ordering::Acquire) as u64;'
content = re.sub(push_pattern1, push_replacement1, content)
# 3b. Update push() - initialize priority update step
push_pattern2 = r'(\{[\s\n]+let mut tree = self\.priorities\.lock\(\);[\s\n]+tree\.update\(index, priority\)\?;[\s\n]+\})'
push_replacement2 = r'''\1
// Initialize priority update step
{
let mut update_steps = self.priority_update_steps.write();
if index < update_steps.len() {
update_steps[index] = current_step;
}
}'''
content = re.sub(push_pattern2, push_replacement2, content)
# 4. Add staleness decay to sample() at the beginning
sample_pattern = r'(pub fn sample\(\s+&self,\s+batch_size: usize,\s+\) -> Result<\(Vec<Experience>, Vec<f32>, Vec<usize>\), MLError> \{)'
sample_replacement = r'''\1
// Apply staleness decay before sampling
// Default: max_age = 10000 steps, decay_factor = 0.9
self.apply_staleness_decay(
self.training_step.load(Ordering::Acquire) as u64,
10000,
0.9,
)?;
'''
content = re.sub(sample_pattern, sample_replacement, content)
# 5. Update update_priorities() - add current_step
update_pattern1 = r'(let mut max_priority = f32::from_bits\(self\.max_priority\.load\(Ordering::Acquire\) as u32\);)'
update_replacement1 = r'\1\n let current_step = self.training_step.load(Ordering::Acquire) as u64;'
content = re.sub(update_pattern1, update_replacement1, content)
# 5b. Update update_priorities() - track update steps
update_pattern2 = r'(self\.max_priority[\s\n]+\.store\(max_priority\.to_bits\(\) as u64, Ordering::Release\);)'
update_replacement2 = r'''\1
// Update priority update steps
{
let mut update_steps = self.priority_update_steps.write();
for &idx in indices {
if idx < update_steps.len() {
update_steps[idx] = current_step;
}
}
}'''
content = re.sub(update_pattern2, update_replacement2, content)
# 6. Add apply_staleness_decay method before clear()
clear_pattern = r'( /// Reset buffer \(clear all experiences\)[\s\n]+ pub fn clear\(&self\) \{)'
staleness_method = r''' /// Apply staleness decay to old priorities
pub fn apply_staleness_decay(
&self,
current_step: u64,
max_age: u64,
decay_factor: f64,
) -> Result<(), MLError> {
let size = self.size.load(Ordering::Acquire);
if size == 0 {
return Ok(());
}
let update_steps = self.priority_update_steps.read();
let mut tree = self.priorities.lock();
for idx in 0..size {
if idx >= update_steps.len() {
break;
}
let last_update = update_steps[idx];
let age = current_step.saturating_sub(last_update);
if age > max_age {
let current_priority = tree.get_priority(idx);
if current_priority > 0.0 {
let decay = decay_factor.powi((age / max_age) as i32) as f32;
let new_priority = (current_priority * decay).max(self.config.min_priority);
tree.update(idx, new_priority)?;
}
}
}
Ok(())
}
\1'''
content = re.sub(clear_pattern, staleness_method, content)
# 7. Update clear() to reset update steps
clear_update_pattern = r'(\{[\s\n]+let mut tree = self\.priorities\.lock\(\);[\s\n]+for i in 0\.\.self\.config\.capacity \{[\s\n]+let _ = tree\.update\(i, 0\.0\);[\s\n]+\}[\s\n]+\})'
clear_update_replacement = r'''\1
{
let mut update_steps = self.priority_update_steps.write();
for step in update_steps.iter_mut() {
*step = 0;
}
}'''
content = re.sub(clear_update_pattern, clear_update_replacement, content)
# 8. Add tests before the closing brace of mod tests
test_pattern = r'( \}\n\}\n)$'
tests_addition = r'''
#[test]
fn test_priority_staleness_decay() {
let config = PrioritizedReplayConfig {
capacity: 100,
initial_priority: 1.0,
min_priority: 1e-6,
..Default::default()
};
let buffer = PrioritizedReplayBuffer::new(config)
.expect("Failed to create prioritized replay buffer in test");
// Push 10 experiences at step 0
buffer.set_training_step(0);
for _ in 0..10 {
buffer
.push(create_test_experience())
.expect("Failed to push experience in test");
}
// Get initial priorities
let tree = buffer.priorities.lock();
let initial_priorities: Vec<f32> = (0..10).map(|i| tree.get_priority(i)).collect();
drop(tree);
// Simulate 15000 steps passing without priority updates
buffer.set_training_step(15000);
// Apply staleness decay (max_age=10000, decay_factor=0.9)
buffer
.apply_staleness_decay(15000, 10000, 0.9)
.expect("Failed to apply staleness decay in test");
// Check that priorities have decayed
let tree = buffer.priorities.lock();
for i in 0..10 {
let current_priority = tree.get_priority(i);
let initial_priority = initial_priorities[i];
// Age = 15000 steps, max_age = 10000
// Decay should be: decay_factor^(age/max_age) = 0.9^1
let expected_decay = 0.9_f64.powi((15000 / 10000) as i32) as f32;
let expected_priority = (initial_priority * expected_decay).max(1e-6);
// Allow small floating point tolerance
assert!(
(current_priority - expected_priority).abs() < 0.01,
"Priority at index {} should have decayed from {} to ~{}, got {}",
i,
initial_priority,
expected_priority,
current_priority
);
// Verify priority was reduced
assert!(
current_priority < initial_priority,
"Priority at index {} should be reduced after staleness decay",
i
);
}
drop(tree);
// Test that recently updated priorities don't decay
buffer.set_training_step(16000);
// Update some priorities
let indices = vec![0, 1, 2];
let new_priorities = vec![5.0, 6.0, 7.0];
buffer
.update_priorities(&indices, &new_priorities)
.expect("Failed to update priorities in test");
// Move forward only 5000 steps (below max_age threshold)
buffer.set_training_step(21000);
buffer
.apply_staleness_decay(21000, 10000, 0.9)
.expect("Failed to apply staleness decay in test");
// Recently updated priorities should not decay significantly
let tree = buffer.priorities.lock();
for &idx in &indices {
let priority = tree.get_priority(idx);
// Age = 5000, which is < max_age (10000), so no decay should occur
assert!(
priority > 4.5,
"Recently updated priority at index {} should not decay significantly, got {}",
idx,
priority
);
}
}
#[test]
fn test_staleness_tracking_on_push() {
let config = PrioritizedReplayConfig {
capacity: 100,
..Default::default()
};
let buffer = PrioritizedReplayBuffer::new(config)
.expect("Failed to create prioritized replay buffer in test");
// Push experiences at different training steps
buffer.set_training_step(100);
buffer
.push(create_test_experience())
.expect("Failed to push experience in test");
buffer.set_training_step(200);
buffer
.push(create_test_experience())
.expect("Failed to push experience in test");
// Verify update steps were recorded
let update_steps = buffer.priority_update_steps.read();
assert_eq!(update_steps[0], 100, "First experience should have update step 100");
assert_eq!(update_steps[1], 200, "Second experience should have update step 200");
}
#[test]
fn test_staleness_tracking_on_update() {
let config = PrioritizedReplayConfig {
capacity: 100,
..Default::default()
};
let buffer = PrioritizedReplayBuffer::new(config)
.expect("Failed to create prioritized replay buffer in test");
// Push experiences
buffer.set_training_step(100);
for _ in 0..10 {
buffer
.push(create_test_experience())
.expect("Failed to push experience in test");
}
// Update priorities at a later step
buffer.set_training_step(500);
let indices = vec![0, 1, 2];
let priorities = vec![2.0, 3.0, 4.0];
buffer
.update_priorities(&indices, &priorities)
.expect("Failed to update priorities in test");
// Verify update steps were updated
let update_steps = buffer.priority_update_steps.read();
assert_eq!(update_steps[0], 500, "Updated experience should have new update step");
assert_eq!(update_steps[1], 500, "Updated experience should have new update step");
assert_eq!(update_steps[2], 500, "Updated experience should have new update step");
assert_eq!(update_steps[3], 100, "Non-updated experience should retain old update step");
}
\1'''
content = re.sub(test_pattern, tests_addition, content)
with open(filepath, 'w') as f:
f.write(content)
print(f"Successfully updated {filepath}")
if __name__ == "__main__":
add_staleness_tracking("/home/jgrusewski/Work/foxhunt/ml/src/dqn/prioritized_replay.rs")

50
scripts/analyze_dead_code.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Dead Code Analysis Script for Foxhunt Workspace
echo "=== FOXHUNT DEAD CODE ANALYSIS ==="
echo ""
# 1. Find public items that might be unused
echo "1. PUBLIC API SURFACE ANALYSIS"
echo "-------------------------------"
find . -name "*.rs" -type f -not -path "./target/*" -exec grep -l "^pub fn\|^pub struct\|^pub enum\|^pub trait" {} \; | wc -l
echo " files with public items found"
echo ""
# 2. Find large commented code blocks
echo "2. LARGE COMMENTED CODE BLOCKS (>10 lines)"
echo "-------------------------------------------"
find . -name "*.rs" -type f -not -path "./target/*" -print0 | while IFS= read -r -d '' file; do
awk '/^[[:space:]]*\/\// {count++} !/^[[:space:]]*\/\// {if (count >= 10) print FILENAME":"NR-count"-"NR-1":"count" lines"; count=0} END {if (count >= 10) print FILENAME":"NR-count"-"NR":"count" lines"}' "$file"
done | head -20
echo ""
# 3. Find empty or nearly empty modules
echo "3. EMPTY OR MINIMAL MODULES"
echo "----------------------------"
find . -name "*.rs" -type f -not -path "./target/*" -exec sh -c 'lines=$(wc -l < "$1"); if [ "$lines" -lt 10 ]; then echo "$1: $lines lines"; fi' _ {} \; | head -20
echo ""
# 4. Find test modules with no tests
echo "4. TEST MODULES WITHOUT TESTS"
echo "------------------------------"
find . -name "*.rs" -type f -not -path "./target/*" -exec grep -l "#\[cfg(test)\]" {} \; | while read file; do
if ! grep -q "#\[test\]" "$file"; then
echo "$file - has #[cfg(test)] but no #[test] functions"
fi
done | head -10
echo ""
# 5. Find feature-gated code
echo "5. FEATURE-GATED CODE"
echo "----------------------"
find . -name "*.rs" -type f -not -path "./target/*" -exec grep -l "#\[cfg(feature" {} \; | head -10
echo ""
# 6. Analyze Cargo.toml dependencies
echo "6. DEPENDENCY ANALYSIS PER CRATE"
echo "---------------------------------"
find . -name "Cargo.toml" -not -path "./target/*" | head -5
echo ""
echo "=== ANALYSIS COMPLETE ==="

154
scripts/check_unused_deps.py Executable file
View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
Analyze Rust workspace for unused dependencies.
Checks if dependencies in Cargo.toml are actually used in source code.
"""
import os
import re
import subprocess
from pathlib import Path
from collections import defaultdict
def parse_cargo_toml(path):
"""Parse dependencies from Cargo.toml"""
deps = []
try:
with open(path, 'r') as f:
content = f.read()
# Extract dependencies section
in_deps = False
in_dev_deps = False
for line in content.split('\n'):
if line.strip().startswith('[dependencies]'):
in_deps = True
in_dev_deps = False
continue
elif line.strip().startswith('[dev-dependencies]'):
in_deps = False
in_dev_deps = True
continue
elif line.strip().startswith('['):
in_deps = False
in_dev_deps = False
continue
if in_deps and line.strip() and not line.strip().startswith('#'):
# Extract dependency name
match = re.match(r'^([a-zA-Z0-9_-]+)\s*=', line)
if match:
dep_name = match.group(1).replace('-', '_')
deps.append((dep_name, 'normal'))
elif in_dev_deps and line.strip() and not line.strip().startswith('#'):
match = re.match(r'^([a-zA-Z0-9_-]+)\s*=', line)
if match:
dep_name = match.group(1).replace('-', '_')
deps.append((dep_name, 'dev'))
except Exception as e:
print(f"Error parsing {path}: {e}")
return deps
def check_dep_usage(crate_path, dep_name):
"""Check if dependency is used in Rust source files"""
src_path = os.path.join(crate_path, 'src')
tests_path = os.path.join(crate_path, 'tests')
benches_path = os.path.join(crate_path, 'benches')
examples_path = os.path.join(crate_path, 'examples')
patterns = [
f'use {dep_name}',
f'use {dep_name}::',
f'extern crate {dep_name}',
f'{dep_name}::',
]
for base_path in [src_path, tests_path, benches_path, examples_path]:
if not os.path.exists(base_path):
continue
for root, dirs, files in os.walk(base_path):
# Skip target directory
if 'target' in dirs:
dirs.remove('target')
for file in files:
if not file.endswith('.rs'):
continue
file_path = os.path.join(root, file)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
for pattern in patterns:
if pattern in content:
return True
except Exception:
continue
return False
def analyze_workspace():
"""Analyze all crates in workspace"""
workspace_root = '/home/jgrusewski/Work/foxhunt'
# Find all Cargo.toml files (excluding target directories)
cargo_tomls = []
for root, dirs, files in os.walk(workspace_root):
# Skip target and vendor directories
if 'target' in dirs:
dirs.remove('target')
if '.git' in dirs:
dirs.remove('.git')
if 'Cargo.toml' in files:
cargo_path = os.path.join(root, 'Cargo.toml')
# Skip if it's a workspace-only Cargo.toml
with open(cargo_path, 'r') as f:
content = f.read()
if '[package]' in content:
cargo_tomls.append(cargo_path)
results = {}
for cargo_toml in cargo_tomls[:15]: # Limit to first 15 for performance
crate_path = os.path.dirname(cargo_toml)
crate_name = os.path.basename(crate_path)
print(f"\nAnalyzing {crate_name}...")
deps = parse_cargo_toml(cargo_toml)
unused = []
for dep_name, dep_type in deps:
# Skip workspace crates
if dep_name in ['trading_engine', 'risk', 'data', 'backtesting',
'common', 'storage', 'ml', 'config', 'database',
'market_data', 'tli', 'risk_data', 'trading_data',
'adaptive_strategy', 'model_loader']:
continue
# Check if dependency is used
if not check_dep_usage(crate_path, dep_name):
unused.append((dep_name, dep_type))
if unused:
results[crate_name] = unused
return results
if __name__ == '__main__':
print("=== UNUSED DEPENDENCY ANALYSIS ===\n")
results = analyze_workspace()
if not results:
print("\n✅ No obvious unused dependencies found!")
else:
print("\n⚠️ POTENTIALLY UNUSED DEPENDENCIES:\n")
for crate, unused in results.items():
print(f"\n{crate}:")
for dep, dep_type in unused:
print(f" - {dep} ({dep_type})")
print("\n=== ANALYSIS COMPLETE ===")

View File

@@ -0,0 +1,35 @@
#!/bin/bash
# WAVE 26 P1.12: Polyak Soft Updates Test Runner
# Run this script to execute the comprehensive TDD test suite
set -e
echo "============================================="
echo "WAVE 26 P1.12: Polyak Soft Updates Tests"
echo "============================================="
echo ""
echo "Running target_update module unit tests..."
cargo test --package ml --lib dqn::target_update::tests --no-fail-fast
echo ""
echo "Running comprehensive TDD tests..."
cargo test --package ml --lib dqn::tests::target_update_comprehensive_tests --no-fail-fast -- --nocapture
echo ""
echo "============================================="
echo "✅ All tests passed!"
echo "============================================="
echo ""
echo "Test Coverage:"
echo " - 13 comprehensive TDD tests"
echo " - Formula verification"
echo " - Boundary conditions"
echo " - Convergence rates"
echo " - Network divergence"
echo ""
echo "Next steps:"
echo " 1. Run hyperopt with 30D search space (includes tau)"
echo " 2. Monitor divergence during training"
echo " 3. Compare tau values: 0.0001, 0.001, 0.005, 0.01"
echo ""

117
scripts/test_dropout_scheduler.sh Executable file
View File

@@ -0,0 +1,117 @@
#!/bin/bash
# Standalone test for DropoutScheduler logic verification
cat > /tmp/test_dropout.rs << 'EOF'
/// Adaptive dropout scheduler that decreases dropout rate over training
#[derive(Debug, Clone)]
pub struct DropoutScheduler {
initial_rate: f64,
final_rate: f64,
decay_steps: usize,
current_step: usize,
}
impl DropoutScheduler {
pub fn new(initial_rate: f64, final_rate: f64, decay_steps: usize) -> Self {
Self {
initial_rate,
final_rate,
decay_steps,
current_step: 0,
}
}
pub fn get_rate(&self) -> f64 {
if self.decay_steps == 0 {
return self.final_rate;
}
let progress = (self.current_step as f64 / self.decay_steps as f64).min(1.0);
self.initial_rate * (1.0 - progress) + self.final_rate * progress
}
pub fn step(&mut self, steps: usize) {
self.current_step = self.current_step.saturating_add(steps);
}
pub fn current_step(&self) -> usize {
self.current_step
}
}
fn main() {
println!("Testing DropoutScheduler...\n");
// Test 1: Basic creation
let scheduler = DropoutScheduler::new(0.5, 0.1, 10000);
println!("✓ Test 1: Creation - initial rate: {}", scheduler.get_rate());
assert!((scheduler.get_rate() - 0.5).abs() < 1e-6);
// Test 2: Linear decay
let mut scheduler = DropoutScheduler::new(0.5, 0.1, 10000);
scheduler.step(2500); // 25%
let rate_25 = scheduler.get_rate();
println!("✓ Test 2: 25% progress - rate: {:.6} (expected: 0.4)", rate_25);
assert!((rate_25 - 0.4).abs() < 1e-6);
scheduler.step(2500); // 50%
let rate_50 = scheduler.get_rate();
println!("✓ Test 3: 50% progress - rate: {:.6} (expected: 0.3)", rate_50);
assert!((rate_50 - 0.3).abs() < 1e-6);
scheduler.step(2500); // 75%
let rate_75 = scheduler.get_rate();
println!("✓ Test 4: 75% progress - rate: {:.6} (expected: 0.2)", rate_75);
assert!((rate_75 - 0.2).abs() < 1e-6);
scheduler.step(2500); // 100%
println!("✓ Test 5: 100% progress - rate: {:.6} (expected: 0.1)", scheduler.get_rate());
assert!((scheduler.get_rate() - 0.1).abs() < 1e-6);
// Test 3: Beyond decay steps
scheduler.step(5000); // 150%
println!("✓ Test 6: Beyond decay - rate stays at: {:.6}", scheduler.get_rate());
assert!((scheduler.get_rate() - 0.1).abs() < 1e-6);
// Test 4: Step tracking
let mut scheduler = DropoutScheduler::new(0.8, 0.2, 1000);
assert_eq!(scheduler.current_step(), 0);
scheduler.step(100);
assert_eq!(scheduler.current_step(), 100);
println!("✓ Test 7: Step tracking works correctly");
// Test 5: Zero decay steps edge case
let scheduler = DropoutScheduler::new(0.5, 0.1, 0);
println!("✓ Test 8: Zero decay steps - immediately at final: {:.6}", scheduler.get_rate());
assert!((scheduler.get_rate() - 0.1).abs() < 1e-6);
// Test 6: Constant rate
let mut scheduler = DropoutScheduler::new(0.3, 0.3, 1000);
scheduler.step(500);
println!("✓ Test 9: Constant rate - stays at: {:.6}", scheduler.get_rate());
assert!((scheduler.get_rate() - 0.3).abs() < 1e-6);
// Test 7: Realistic schedule
let mut scheduler = DropoutScheduler::new(0.5, 0.05, 100_000);
scheduler.step(10_000); // Early training
let early = scheduler.get_rate();
println!("✓ Test 10: Realistic early training (10k/100k) - rate: {:.6}", early);
assert!(early > 0.4);
scheduler.step(40_000); // Mid training (total 50k)
let mid = scheduler.get_rate();
println!("✓ Test 11: Realistic mid training (50k/100k) - rate: {:.6}", mid);
assert!((mid - 0.275).abs() < 0.01);
scheduler.step(50_000); // Late training (total 100k)
let late = scheduler.get_rate();
println!("✓ Test 12: Realistic late training (100k/100k) - rate: {:.6}", late);
assert!((late - 0.05).abs() < 1e-6);
println!("\n✅ All tests passed!");
}
EOF
rustc /tmp/test_dropout.rs -o /tmp/test_dropout && /tmp/test_dropout