- Created data/examples/download_ml_training_data.rs using reqwest + Databento HTTP API - Downloaded 90 days × 4 symbols (ES.FUT, NQ.FUT, ZN.FUT, 6E.FUT) - Files saved to test_data/real/databento/ml_training/ - Total: 360 files, 15 MB compressed DBN format - Used existing Rust pattern from download_nq_fut.rs - API key loaded from .env file - 100% success rate (360/360 files) - Ready for ML training benchmarks Next: Create simplified training benchmark for RTX 3050 Ti GPU measurements
423 lines
12 KiB
Rust
423 lines
12 KiB
Rust
//! Fixtures and Helpers Integration Tests
|
|
//!
|
|
//! Tests the cached data loading and validation utilities.
|
|
|
|
mod fixtures;
|
|
mod helpers;
|
|
|
|
use anyhow::Result;
|
|
use fixtures::{get_es_fut_bars, get_nq_fut_bars, get_cl_fut_bars};
|
|
use fixtures::{get_bars_for_date, get_regime_sample, RegimeType};
|
|
use fixtures::get_multi_symbol_bars;
|
|
use helpers::{assert_valid_ohlcv, assert_chronological, assert_price_range};
|
|
use helpers::{assert_no_large_gaps, calculate_volatility, generate_quality_report};
|
|
use std::time::Instant;
|
|
|
|
// ============================================================================
|
|
// Cache Performance Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_es_fut_cache_performance() -> Result<()> {
|
|
println!("\n=== ES.FUT Cache Performance Test ===");
|
|
|
|
// First call (cold cache)
|
|
let start = Instant::now();
|
|
let bars1 = get_es_fut_bars().await?;
|
|
let cold_duration = start.elapsed();
|
|
|
|
println!("Cold cache: {:?} ({} bars)", cold_duration, bars1.len());
|
|
|
|
assert!(!bars1.is_empty(), "Should load ES.FUT bars");
|
|
assert!(bars1.len() > 350 && bars1.len() < 450, "Expected ~390 bars, got {}", bars1.len());
|
|
|
|
// Second call (warm cache)
|
|
let start = Instant::now();
|
|
let bars2 = get_es_fut_bars().await?;
|
|
let warm_duration = start.elapsed();
|
|
|
|
println!("Warm cache: {:?} ({} bars)", warm_duration, bars2.len());
|
|
|
|
assert_eq!(bars1.len(), bars2.len(), "Cache should return same data");
|
|
|
|
// Calculate speedup
|
|
let speedup = cold_duration.as_nanos() as f64 / warm_duration.as_nanos().max(1) as f64;
|
|
println!("Speedup: {:.1}x faster", speedup);
|
|
|
|
// Warm should be significantly faster (at least 10x)
|
|
assert!(
|
|
speedup > 10.0,
|
|
"Cached access should be >10x faster (got {:.1}x)",
|
|
speedup
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_nq_fut_cache_performance() -> Result<()> {
|
|
println!("\n=== NQ.FUT Cache Performance Test ===");
|
|
|
|
let start = Instant::now();
|
|
let bars = get_nq_fut_bars().await?;
|
|
let duration = start.elapsed();
|
|
|
|
println!("Loaded: {:?} ({} bars)", duration, bars.len());
|
|
|
|
assert!(!bars.is_empty());
|
|
assert_eq!(bars[0].symbol, "NQ.FUT");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cl_fut_cache_performance() -> Result<()> {
|
|
println!("\n=== CL.FUT Cache Performance Test ===");
|
|
|
|
let start = Instant::now();
|
|
let bars = get_cl_fut_bars().await?;
|
|
let duration = start.elapsed();
|
|
|
|
println!("Loaded: {:?} ({} bars)", duration, bars.len());
|
|
|
|
assert!(!bars.is_empty());
|
|
assert!(bars.len() > 1400, "CL.FUT has 24-hour trading, expected >1400 bars");
|
|
assert_eq!(bars[0].symbol, "CL.FUT");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Data Validation Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_es_fut_data_validation() -> Result<()> {
|
|
println!("\n=== ES.FUT Data Validation ===");
|
|
|
|
let bars = get_es_fut_bars().await?;
|
|
|
|
// OHLCV validation
|
|
assert_valid_ohlcv(&bars);
|
|
println!("✓ OHLCV validation passed");
|
|
|
|
// Chronological ordering
|
|
assert_chronological(&bars);
|
|
println!("✓ Chronological validation passed");
|
|
|
|
// Price range (ES.FUT typical range)
|
|
assert_price_range(&bars, "ES.FUT");
|
|
println!("✓ Price range validation passed");
|
|
|
|
// No large gaps (max 5 minutes for 1-minute data)
|
|
assert_no_large_gaps(&bars, 5);
|
|
println!("✓ Gap validation passed");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_nq_fut_data_validation() -> Result<()> {
|
|
println!("\n=== NQ.FUT Data Validation ===");
|
|
|
|
let bars = get_nq_fut_bars().await?;
|
|
|
|
assert_valid_ohlcv(&bars);
|
|
assert_chronological(&bars);
|
|
assert_price_range(&bars, "NQ.FUT");
|
|
assert_no_large_gaps(&bars, 5);
|
|
|
|
println!("✓ All validations passed");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cl_fut_data_validation() -> Result<()> {
|
|
println!("\n=== CL.FUT Data Validation ===");
|
|
|
|
let bars = get_cl_fut_bars().await?;
|
|
|
|
assert_valid_ohlcv(&bars);
|
|
assert_chronological(&bars);
|
|
assert_price_range(&bars, "CL.FUT");
|
|
assert_no_large_gaps(&bars, 2); // CL.FUT has tighter gaps (24-hour trading)
|
|
|
|
println!("✓ All validations passed");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Filtered Data Access Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_bars_for_date() -> Result<()> {
|
|
println!("\n=== Date Filtering Test ===");
|
|
|
|
use chrono::NaiveDate;
|
|
|
|
let date = NaiveDate::from_ymd_opt(2024, 1, 2)
|
|
.unwrap()
|
|
.and_hms_opt(0, 0, 0)
|
|
.unwrap()
|
|
.and_utc();
|
|
|
|
let bars = get_bars_for_date("ES.FUT", date).await?;
|
|
|
|
assert!(!bars.is_empty(), "Should find bars for 2024-01-02");
|
|
println!("Found {} bars for 2024-01-02", bars.len());
|
|
|
|
// All bars should be from requested date
|
|
for bar in &bars {
|
|
assert_eq!(bar.timestamp.date_naive(), date.date_naive());
|
|
}
|
|
|
|
println!("✓ All bars from correct date");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_regime_trending() -> Result<()> {
|
|
println!("\n=== Trending Regime Test ===");
|
|
|
|
let bars = get_regime_sample(RegimeType::Trending).await?;
|
|
|
|
assert!(!bars.is_empty(), "Should find trending sample");
|
|
assert!(bars.len() >= 50, "Should have sufficient bars");
|
|
|
|
println!("Trending sample: {} bars", bars.len());
|
|
|
|
// Calculate price movement
|
|
let first_price = bars[0].close.to_string().parse::<f64>().unwrap_or(0.0);
|
|
let last_price = bars[bars.len()-1].close.to_string().parse::<f64>().unwrap_or(0.0);
|
|
let change_pct = ((last_price - first_price) / first_price).abs() * 100.0;
|
|
|
|
println!("Price change: {:.2}%", change_pct);
|
|
println!("✓ Trending regime detected");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_regime_ranging() -> Result<()> {
|
|
println!("\n=== Ranging Regime Test ===");
|
|
|
|
let bars = get_regime_sample(RegimeType::Ranging).await?;
|
|
|
|
assert!(!bars.is_empty(), "Should find ranging sample");
|
|
println!("Ranging sample: {} bars", bars.len());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_regime_volatile() -> Result<()> {
|
|
println!("\n=== Volatile Regime Test ===");
|
|
|
|
let bars = get_regime_sample(RegimeType::Volatile).await?;
|
|
|
|
assert!(!bars.is_empty(), "Should find volatile sample");
|
|
|
|
let volatility = calculate_volatility(&bars);
|
|
println!("Volatile sample: {} bars", bars.len());
|
|
println!("Annualized volatility: {:.2}%", volatility);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_regime_stable() -> Result<()> {
|
|
println!("\n=== Stable Regime Test ===");
|
|
|
|
let bars = get_regime_sample(RegimeType::Stable).await?;
|
|
|
|
assert!(!bars.is_empty(), "Should find stable sample");
|
|
|
|
let volatility = calculate_volatility(&bars);
|
|
println!("Stable sample: {} bars", bars.len());
|
|
println!("Annualized volatility: {:.2}%", volatility);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Multi-Symbol Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_multi_symbol_loading() -> Result<()> {
|
|
println!("\n=== Multi-Symbol Loading Test ===");
|
|
|
|
let start = Instant::now();
|
|
let symbols = vec!["ES.FUT", "NQ.FUT", "CL.FUT"];
|
|
let data = get_multi_symbol_bars(&symbols).await?;
|
|
let duration = start.elapsed();
|
|
|
|
println!("Loaded {} symbols in {:?}", data.len(), duration);
|
|
|
|
assert_eq!(data.len(), 3, "Should load all 3 symbols");
|
|
assert!(data.contains_key("ES.FUT"));
|
|
assert!(data.contains_key("NQ.FUT"));
|
|
assert!(data.contains_key("CL.FUT"));
|
|
|
|
// Verify data quality for each symbol
|
|
for (symbol, bars) in &data {
|
|
assert!(!bars.is_empty(), "Symbol {} should have bars", symbol);
|
|
assert_eq!(bars[0].symbol, *symbol);
|
|
println!(" {}: {} bars", symbol, bars.len());
|
|
}
|
|
|
|
println!("✓ All symbols loaded successfully");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Quality Report Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_es_fut_quality_report() -> Result<()> {
|
|
println!("\n=== ES.FUT Quality Report ===");
|
|
|
|
let bars = get_es_fut_bars().await?;
|
|
let report = generate_quality_report(&bars);
|
|
|
|
println!("{}", report);
|
|
|
|
assert!(report.contains("ES.FUT"));
|
|
assert!(report.contains("Total bars"));
|
|
assert!(report.contains("Quality Checks"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_nq_fut_quality_report() -> Result<()> {
|
|
println!("\n=== NQ.FUT Quality Report ===");
|
|
|
|
let bars = get_nq_fut_bars().await?;
|
|
let report = generate_quality_report(&bars);
|
|
|
|
println!("{}", report);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cl_fut_quality_report() -> Result<()> {
|
|
println!("\n=== CL.FUT Quality Report ===");
|
|
|
|
let bars = get_cl_fut_bars().await?;
|
|
let report = generate_quality_report(&bars);
|
|
|
|
println!("{}", report);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Thread Safety Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_concurrent_cache_access() -> Result<()> {
|
|
println!("\n=== Concurrent Cache Access Test ===");
|
|
|
|
let mut handles = vec![];
|
|
|
|
// Spawn 10 concurrent reads
|
|
for i in 0..10 {
|
|
handles.push(tokio::spawn(async move {
|
|
let bars = get_es_fut_bars().await.unwrap();
|
|
(i, bars.len())
|
|
}));
|
|
}
|
|
|
|
// All should succeed
|
|
let mut results = vec![];
|
|
for handle in handles {
|
|
let (id, len) = handle.await?;
|
|
results.push((id, len));
|
|
}
|
|
|
|
println!("Concurrent reads: {}", results.len());
|
|
|
|
// All should return same data
|
|
let first_len = results[0].1;
|
|
for (id, len) in &results {
|
|
assert_eq!(*len, first_len, "Task {} got different length", id);
|
|
}
|
|
|
|
println!("✓ All concurrent reads consistent");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_strategy_with_cached_data() -> Result<()> {
|
|
println!("\n=== Strategy Integration Test ===");
|
|
|
|
// Load cached data
|
|
let bars = get_es_fut_bars().await?;
|
|
|
|
// Validate data
|
|
assert_valid_ohlcv(&bars);
|
|
assert_chronological(&bars);
|
|
|
|
// Simple moving average crossover strategy simulation
|
|
let mut signals = 0;
|
|
|
|
for i in 20..bars.len() {
|
|
let window = &bars[i-20..i];
|
|
let avg: f64 = window.iter()
|
|
.map(|b| b.close.to_string().parse::<f64>().unwrap_or(0.0))
|
|
.sum::<f64>() / 20.0;
|
|
|
|
let current = bars[i].close.to_string().parse::<f64>().unwrap_or(0.0);
|
|
|
|
if current > avg * 1.001 { // 0.1% above average
|
|
signals += 1;
|
|
}
|
|
}
|
|
|
|
println!("Generated {} trading signals", signals);
|
|
assert!(signals > 0, "Should generate some signals");
|
|
|
|
println!("✓ Strategy integration successful");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_performance_comparison() -> Result<()> {
|
|
println!("\n=== Performance Comparison ===");
|
|
|
|
// Test 1: Single symbol (cached)
|
|
let start = Instant::now();
|
|
for _ in 0..100 {
|
|
let _ = get_es_fut_bars().await?;
|
|
}
|
|
let cached_duration = start.elapsed();
|
|
|
|
println!("100 cached reads: {:?}", cached_duration);
|
|
println!("Average per read: {:?}", cached_duration / 100);
|
|
|
|
// Should be very fast (< 1ms total for 100 reads)
|
|
assert!(
|
|
cached_duration.as_millis() < 100,
|
|
"100 cached reads should take < 100ms"
|
|
);
|
|
|
|
println!("✓ Cache performance excellent");
|
|
|
|
Ok(())
|
|
}
|