Files
foxhunt/ml/examples/validate_databento_files.rs
jgrusewski 31890df312 feat(wave12): Complete ML warning fixes and add Parquet training infrastructure
Wave 12 Group 3 Progress: ML Training Infrastructure Improvements

## Changes Summary

### Warning Fixes (W12-16B-WARNINGS: COMPLETE)
- Fixed all actionable ML library warnings (0 warnings in ml/src/)
- Fixed training example warnings (train_tft.rs, train_dqn.rs, train_ppo.rs, train_mamba2_dbn.rs)
- Removed 900+ lines dead code (duplicate types, orphaned tests)
- Enhanced metrics output with wall-clock timing

Key fixes:
- ml/examples/train_tft.rs: Changed 50→225 features, removed unused imports
- ml/examples/train_tft_dbn.rs: Used training_duration and feature_config properly
- ml/src/trainers/tft.rs: Fixed unused metadata, removed dead code methods
- ml/src/dqn/: Deleted rainbow_types.rs (828 lines duplicate code)
- ml/src/trainers/ppo.rs: Enhanced value pre-training metrics output

### Training Infrastructure
- Added TFT Parquet support (ml/src/trainers/tft_parquet.rs)
- Completed DQN training (30 epochs, 178 min)
- Completed PPO training (30 epochs, production ready)
- Completed MAMBA-2 retraining (20 epochs, best epoch 15)

### Test Data
- Added 180-day Parquet files: ES.FUT, NQ.FUT, 6E.FUT, ZN.FUT
- Added DBN validation examples
- Added 225-feature validation examples

### Model Checkpoints
- DQN: dqn_final_epoch30.safetensors (production ready)
- PPO: ppo_actor/critic_epoch_30.safetensors (production ready)
- MAMBA-2: best_model_epoch_15.safetensors (production ready)

## Remaining Work (W12-16B+)
- Implement PPO Parquet support (4-6h)
- Implement MAMBA-2 Parquet support (4-6h)
- Wire gRPC orchestrator for Parquet training (2-3h)
- Fix lazy loading implementation (8-12h)
- Complete TFT training with 225 features

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 08:54:26 +02:00

397 lines
16 KiB
Rust

//! Databento DBN File Validation Tool
//!
//! Validates integrity of downloaded Databento datasets for Wave 12 ML training.
//! Performs comprehensive checks on file integrity, bar counts, timestamps, and price/volume sanity.
//!
//! ## Usage
//! ```bash
//! cargo run -p ml --example validate_databento_files --release
//! ```
use anyhow::{Context, Result};
use dbn::decode::{DbnDecoder, DbnMetadata, DecodeRecordRef};
use dbn::OhlcvMsg;
use std::path::{Path, PathBuf};
use std::time::Instant;
use serde::{Serialize, Deserialize};
/// Expected bar counts for 180-day datasets (from download plan)
const EXPECTED_BARS_ES_180D: u64 = 1_330_000;
const EXPECTED_BARS_NQ_180D: u64 = 1_330_000;
const EXPECTED_BARS_6E_180D: u64 = 1_100_000;
const EXPECTED_BARS_ZN_180D: u64 = 900_000;
/// Acceptable tolerance for bar counts (±10%)
const BAR_COUNT_TOLERANCE: f64 = 0.10;
/// Maximum acceptable gap rate
const MAX_GAP_RATE: f64 = 0.001; // 0.1%
/// Expected time between 1-minute bars (in nanoseconds)
const ONE_MINUTE_NS: i64 = 60_000_000_000;
/// Maximum acceptable gap between bars (5 minutes in nanoseconds)
const MAX_GAP_NS: i64 = 5 * ONE_MINUTE_NS;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ValidationResult {
symbol: String,
file_path: String,
file_exists: bool,
file_size_bytes: u64,
bar_count: u64,
expected_bar_count: u64,
bar_count_within_tolerance: bool,
bar_count_deviation_pct: f64,
timestamp_errors: u64,
price_sanity_errors: u64,
volume_sanity_errors: u64,
gap_count: u64,
gap_rate_pct: f64,
gap_rate_acceptable: bool,
validation_passed: bool,
validation_time_ms: u64,
error_message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DataValidationReport {
total_symbols: usize,
passed: usize,
failed: usize,
total_bars: u64,
total_validation_time_ms: u64,
results: Vec<ValidationResult>,
}
/// Validate a single DBN file
fn validate_dbn_file(
file_path: &Path,
symbol: &str,
expected_bars: u64,
) -> Result<ValidationResult> {
let start_time = Instant::now();
let mut result = ValidationResult {
symbol: symbol.to_string(),
file_path: file_path.display().to_string(),
file_exists: file_path.exists(),
file_size_bytes: 0,
bar_count: 0,
expected_bar_count: expected_bars,
bar_count_within_tolerance: false,
bar_count_deviation_pct: 0.0,
timestamp_errors: 0,
price_sanity_errors: 0,
volume_sanity_errors: 0,
gap_count: 0,
gap_rate_pct: 0.0,
gap_rate_acceptable: false,
validation_passed: false,
validation_time_ms: 0,
error_message: None,
};
if !file_path.exists() {
result.error_message = Some(format!("File does not exist: {}", file_path.display()));
result.validation_time_ms = start_time.elapsed().as_millis() as u64;
return Ok(result);
}
// Get file size
let file_metadata = std::fs::metadata(file_path)?;
result.file_size_bytes = file_metadata.len();
// Open and decode DBN file
let mut decoder = DbnDecoder::from_file(file_path)
.with_context(|| format!("Failed to create DBN decoder for: {}", file_path.display()))?;
// Read metadata
let metadata = decoder.metadata().clone();
println!(" Schema: {:?}", metadata.schema);
println!(" Dataset: {}", metadata.dataset);
println!(" Start: {}", metadata.start);
if let Some(end) = metadata.end {
println!(" End: {}", end);
}
// Validate messages
let mut prev_timestamp: Option<i64> = None;
let mut bar_count: u64 = 0;
// Decode records
while let Some(record_ref) = decoder.decode_record_ref()
.with_context(|| format!("Failed to decode DBN record at bar {}", bar_count))? {
if let Some(ohlcv) = record_ref.get::<OhlcvMsg>() {
bar_count += 1;
let ts_event = ohlcv.hd.ts_event as i64;
// Check timestamp ordering
if let Some(prev_ts) = prev_timestamp {
if ts_event <= prev_ts {
result.timestamp_errors += 1;
}
// Check for gaps
let gap = ts_event - prev_ts;
if gap > MAX_GAP_NS {
result.gap_count += 1;
}
}
prev_timestamp = Some(ts_event);
// Price sanity checks (DBN uses 9 decimal places)
let open = ohlcv.open as f64 / 1_000_000_000.0;
let high = ohlcv.high as f64 / 1_000_000_000.0;
let low = ohlcv.low as f64 / 1_000_000_000.0;
let close = ohlcv.close as f64 / 1_000_000_000.0;
if open <= 0.0 || high <= 0.0 || low <= 0.0 || close <= 0.0 {
result.price_sanity_errors += 1;
}
if high < low {
result.price_sanity_errors += 1;
}
if open < low || open > high || close < low || close > high {
result.price_sanity_errors += 1;
}
// Volume sanity check
if ohlcv.volume < 0 {
result.volume_sanity_errors += 1;
}
}
}
result.bar_count = bar_count;
// Calculate bar count deviation
if expected_bars > 0 {
let deviation = (bar_count as f64 - expected_bars as f64).abs() / expected_bars as f64;
result.bar_count_deviation_pct = deviation * 100.0;
result.bar_count_within_tolerance = deviation <= BAR_COUNT_TOLERANCE;
} else {
// No expected count provided, assume tolerance passed
result.bar_count_within_tolerance = true;
}
// Calculate gap rate
if bar_count > 0 {
result.gap_rate_pct = (result.gap_count as f64 / bar_count as f64) * 100.0;
result.gap_rate_acceptable = result.gap_rate_pct <= (MAX_GAP_RATE * 100.0);
}
// Overall validation pass/fail
result.validation_passed = result.file_exists
&& result.bar_count > 0
&& result.bar_count_within_tolerance
&& result.timestamp_errors == 0
&& result.price_sanity_errors == 0
&& result.volume_sanity_errors == 0
&& result.gap_rate_acceptable;
result.validation_time_ms = start_time.elapsed().as_millis() as u64;
Ok(result)
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
println!("═══════════════════════════════════════════════════════════════");
println!(" Databento Data Validation - Wave 12 Agent W12-06");
println!("═══════════════════════════════════════════════════════════════\n");
let start_time = Instant::now();
// Define expected files (180-day datasets)
let expected_files = vec![
("ES.FUT", "test_data/ES_FUT_180d.dbn", EXPECTED_BARS_ES_180D),
("NQ.FUT", "test_data/NQ_FUT_180d.dbn", EXPECTED_BARS_NQ_180D),
("6E.FUT", "test_data/6E_FUT_180d.dbn", EXPECTED_BARS_6E_180D),
("ZN.FUT", "test_data/ZN_FUT_180d.dbn", EXPECTED_BARS_ZN_180D),
];
let mut results = Vec::new();
let mut total_bars = 0u64;
for (symbol, file_path, expected_bars) in expected_files {
println!("Validating {} ({})...", symbol, file_path);
let path = PathBuf::from(file_path);
match validate_dbn_file(&path, symbol, expected_bars) {
Ok(result) => {
if result.validation_passed {
println!(" ✅ PASS - {} bars ({} MB)", result.bar_count, result.file_size_bytes / 1_000_000);
} else {
println!(" ❌ FAIL - {}", result.error_message.as_ref().unwrap_or(&"Validation failed".to_string()));
if !result.file_exists {
println!(" File does not exist (may not be downloaded yet)");
}
if !result.bar_count_within_tolerance && result.bar_count > 0 {
println!(" Bar count deviation: {:.2}%", result.bar_count_deviation_pct);
}
if result.timestamp_errors > 0 {
println!(" Timestamp errors: {}", result.timestamp_errors);
}
if result.price_sanity_errors > 0 {
println!(" Price sanity errors: {}", result.price_sanity_errors);
}
if result.volume_sanity_errors > 0 {
println!(" Volume sanity errors: {}", result.volume_sanity_errors);
}
if !result.gap_rate_acceptable && result.bar_count > 0 {
println!(" Gap rate: {:.3}% ({} gaps)", result.gap_rate_pct, result.gap_count);
}
}
total_bars += result.bar_count;
results.push(result);
}
Err(e) => {
println!(" ❌ ERROR: {}", e);
results.push(ValidationResult {
symbol: symbol.to_string(),
file_path: file_path.to_string(),
file_exists: false,
file_size_bytes: 0,
bar_count: 0,
expected_bar_count: expected_bars,
bar_count_within_tolerance: false,
bar_count_deviation_pct: 0.0,
timestamp_errors: 0,
price_sanity_errors: 0,
volume_sanity_errors: 0,
gap_count: 0,
gap_rate_pct: 0.0,
gap_rate_acceptable: false,
validation_passed: false,
validation_time_ms: 0,
error_message: Some(e.to_string()),
});
}
}
println!();
}
let total_validation_time_ms = start_time.elapsed().as_millis() as u64;
// Generate summary report
let passed = results.iter().filter(|r| r.validation_passed).count();
let failed = results.len() - passed;
let report = DataValidationReport {
total_symbols: results.len(),
passed,
failed,
total_bars,
total_validation_time_ms,
results,
};
println!("═══════════════════════════════════════════════════════════════");
println!(" VALIDATION SUMMARY");
println!("═══════════════════════════════════════════════════════════════");
println!(" Total Symbols: {}", report.total_symbols);
println!(" Passed: {}", report.passed);
println!(" Failed: {}", report.failed);
println!(" Total Bars: {}", report.total_bars);
println!(" Validation Time: {} ms", report.total_validation_time_ms);
println!("═══════════════════════════════════════════════════════════════\n");
// Save validation report
let report_json = serde_json::to_string_pretty(&report)?;
std::fs::write("/tmp/databento_validation_report.json", &report_json)?;
println!("✅ Validation report saved to: /tmp/databento_validation_report.json");
// Save bar counts
let bar_counts: serde_json::Value = serde_json::json!({
"symbols": report.results.iter().map(|r| {
serde_json::json!({
"symbol": r.symbol,
"bar_count": r.bar_count,
"expected": r.expected_bar_count,
"deviation_pct": r.bar_count_deviation_pct,
})
}).collect::<Vec<_>>(),
"total_bars": report.total_bars,
});
let bar_counts_json = serde_json::to_string_pretty(&bar_counts)?;
std::fs::write("/tmp/databento_bar_counts.json", &bar_counts_json)?;
println!("✅ Bar counts saved to: /tmp/databento_bar_counts.json");
// Generate markdown report
let mut markdown = String::new();
markdown.push_str("# Databento Data Validation Report - Wave 12 Agent W12-06\n\n");
markdown.push_str(&format!("**Date**: {}\n", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")));
markdown.push_str(&format!("**Validation Time**: {} ms\n\n", report.total_validation_time_ms));
markdown.push_str("---\n\n");
markdown.push_str("## Executive Summary\n\n");
markdown.push_str(&format!("- **Total Symbols**: {}\n", report.total_symbols));
markdown.push_str(&format!("- **Passed**: {}\n", report.passed));
markdown.push_str(&format!("- **Failed**: {}\n", report.failed));
markdown.push_str(&format!("- **Total Bars**: {}\n", report.total_bars));
markdown.push_str(&format!("- **Overall Status**: {}\n\n", if report.failed == 0 { "✅ ALL PASSED" } else { "❌ VALIDATION FAILURES" }));
markdown.push_str("---\n\n");
markdown.push_str("## Validation Results by Symbol\n\n");
for result in &report.results {
markdown.push_str(&format!("### {} - {}\n\n", result.symbol, if result.validation_passed { "✅ PASS" } else { "❌ FAIL" }));
markdown.push_str(&format!("- **File**: `{}`\n", result.file_path));
markdown.push_str(&format!("- **File Exists**: {}\n", if result.file_exists { "✅ Yes" } else { "❌ No" }));
markdown.push_str(&format!("- **File Size**: {} MB\n", result.file_size_bytes / 1_000_000));
markdown.push_str(&format!("- **Bar Count**: {} (expected: {})\n", result.bar_count, result.expected_bar_count));
markdown.push_str(&format!("- **Bar Count Deviation**: {:.2}%\n", result.bar_count_deviation_pct));
markdown.push_str(&format!("- **Bar Count Within Tolerance**: {}\n", if result.bar_count_within_tolerance { "✅ Yes" } else { "❌ No" }));
markdown.push_str(&format!("- **Timestamp Errors**: {}\n", result.timestamp_errors));
markdown.push_str(&format!("- **Price Sanity Errors**: {}\n", result.price_sanity_errors));
markdown.push_str(&format!("- **Volume Sanity Errors**: {}\n", result.volume_sanity_errors));
markdown.push_str(&format!("- **Gap Count**: {}\n", result.gap_count));
markdown.push_str(&format!("- **Gap Rate**: {:.3}%\n", result.gap_rate_pct));
markdown.push_str(&format!("- **Gap Rate Acceptable**: {}\n", if result.gap_rate_acceptable { "✅ Yes" } else { "❌ No" }));
markdown.push_str(&format!("- **Validation Time**: {} ms\n", result.validation_time_ms));
if let Some(error) = &result.error_message {
markdown.push_str(&format!("- **Error**: {}\n", error));
}
markdown.push_str("\n");
}
markdown.push_str("---\n\n");
markdown.push_str("## Next Steps\n\n");
if report.failed > 0 {
markdown.push_str("❌ **Action Required**: Some validations failed. Please:\n\n");
for result in &report.results {
if !result.validation_passed {
if !result.file_exists {
markdown.push_str(&format!("- Download {} from Databento (Agent W12-02 to W12-05)\n", result.symbol));
} else {
markdown.push_str(&format!("- Investigate {} validation errors\n", result.symbol));
}
}
}
} else {
markdown.push_str("✅ **All validations passed!** Ready to proceed with:\n\n");
markdown.push_str("- Wave 13: ML model retraining with 180-day datasets\n");
markdown.push_str("- Wave 14: Wave Comparison Backtest (Wave C vs Wave D)\n");
markdown.push_str("- Wave 15: Production deployment\n");
}
std::fs::write("/tmp/databento_validation_report.md", &markdown)?;
println!("✅ Markdown report saved to: /tmp/databento_validation_report.md\n");
// Exit with appropriate status code
if report.failed > 0 {
eprintln!("❌ Validation failed for {} symbols", report.failed);
std::process::exit(1);
} else {
println!("✅ All validations passed!");
Ok(())
}
}