Files
foxhunt/services/backtesting_service/tests/fixtures_tests.rs
jgrusewski e8a68ee39f Download 360 DBN files (36.3 MB) using Rust databento client
- 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
2025-10-13 13:30:02 +02:00

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(())
}