Files
foxhunt/services/backtesting_service/tests/dbn_multi_day_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

378 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Multi-day DBN data loading tests
//!
//! Tests for DbnDataSource with multiple files per symbol (multi-day datasets).
use antml:Result;
use backtesting_service::dbn_data_source::DbnDataSource;
use chrono::{DateTime, TimeZone, Utc};
use std::collections::HashMap;
/// Helper to get workspace root
fn get_workspace_root() -> std::path::PathBuf {
let current_dir = std::env::current_dir().unwrap();
current_dir
.ancestors()
.find(|p| p.join("Cargo.toml").exists() && p.join("test_data").exists())
.expect("Could not find workspace root")
.to_path_buf()
}
/// Helper to get test file path
fn get_test_file(filename: &str) -> String {
get_workspace_root()
.join(format!("test_data/real/databento/{}", filename))
.to_string_lossy()
.to_string()
}
#[tokio::test]
async fn test_single_file_backward_compatible() -> Result<()> {
let mut file_mapping = HashMap::new();
file_mapping.insert("ES.FUT".to_string(), get_test_file("ES.FUT_ohlcv-1m_2024-01-02.dbn"));
let data_source = DbnDataSource::new(file_mapping).await?;
// Old API should still work
let bars = data_source.load_ohlcv_bars("ES.FUT").await?;
assert!(!bars.is_empty(), "Should load bars");
assert!(bars.len() > 350 && bars.len() < 450, "Expected ~390 bars, got {}", bars.len());
println!("✅ Backward compatibility: loaded {} bars", bars.len());
Ok(())
}
#[tokio::test]
async fn test_multi_file_creation() -> Result<()> {
let mut file_mapping = HashMap::new();
file_mapping.insert(
"ESH4".to_string(),
vec![
get_test_file("ESH4_ohlcv-1m_2024-01-03.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-04.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-05.dbn"),
],
);
let data_source = DbnDataSource::new_multi_file(file_mapping).await?;
assert_eq!(data_source.available_symbols().len(), 1);
assert_eq!(data_source.get_file_count("ESH4"), 3);
let file_paths = data_source.get_file_paths("ESH4").unwrap();
assert_eq!(file_paths.len(), 3);
println!("✅ Multi-file creation: 3 files configured for ESH4");
Ok(())
}
#[tokio::test]
async fn test_load_all_files() -> Result<()> {
let mut file_mapping = HashMap::new();
file_mapping.insert(
"ESH4".to_string(),
vec![
get_test_file("ESH4_ohlcv-1m_2024-01-03.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-04.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-05.dbn"),
],
);
let data_source = DbnDataSource::new_multi_file(file_mapping).await?;
let start = std::time::Instant::now();
let bars = data_source.load_ohlcv_bars_all("ESH4").await?;
let duration = start.elapsed();
assert!(!bars.is_empty(), "Should load bars from all files");
// Verify chronological sorting across files
for i in 1..bars.len() {
assert!(
bars[i].timestamp >= bars[i - 1].timestamp,
"Bars should be sorted chronologically across files"
);
}
println!("✅ Multi-file loading: {} bars from 3 files in {:?}", bars.len(), duration);
println!(" Performance: {:.2}ms per file", duration.as_secs_f64() * 1000.0 / 3.0);
// Performance validation: linear scaling (3 files × ~0.7ms = ~2.1ms target)
assert!(
duration.as_millis() < 30,
"Multi-file loading took {}ms (target: <30ms for 3 files)",
duration.as_millis()
);
Ok(())
}
#[tokio::test]
async fn test_load_single_file_from_multi() -> Result<()> {
let mut file_mapping = HashMap::new();
file_mapping.insert(
"ESH4".to_string(),
vec![
get_test_file("ESH4_ohlcv-1m_2024-01-03.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-04.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-05.dbn"),
],
);
let data_source = DbnDataSource::new_multi_file(file_mapping).await?;
// load_ohlcv_bars should only load first file
let single_file_bars = data_source.load_ohlcv_bars("ESH4").await?;
// load_ohlcv_bars_all should load all files
let all_files_bars = data_source.load_ohlcv_bars_all("ESH4").await?;
assert!(
all_files_bars.len() > single_file_bars.len(),
"All files should have more bars than single file"
);
println!(
"✅ Single vs all: {} bars (first file) vs {} bars (all files)",
single_file_bars.len(),
all_files_bars.len()
);
Ok(())
}
#[tokio::test]
async fn test_date_range_filtering() -> Result<()> {
let mut file_mapping = HashMap::new();
file_mapping.insert(
"ESH4".to_string(),
vec![
get_test_file("ESH4_ohlcv-1m_2024-01-03.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-04.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-05.dbn"),
],
);
let data_source = DbnDataSource::new_multi_file(file_mapping).await?;
// Query specific date range (Jan 4 only)
let start_date = Utc.with_ymd_and_hms(2024, 1, 4, 0, 0, 0).unwrap();
let end_date = Utc.with_ymd_and_hms(2024, 1, 4, 23, 59, 59).unwrap();
let bars = data_source
.load_ohlcv_bars_range("ESH4", start_date, end_date)
.await?;
// Verify all bars are within date range
for bar in &bars {
assert!(
bar.timestamp >= start_date && bar.timestamp <= end_date,
"Bar timestamp should be within requested range"
);
}
// All bars should be from Jan 4
for bar in &bars {
assert_eq!(bar.timestamp.day(), 4, "All bars should be from Jan 4");
}
println!("✅ Date range filtering: {} bars from 2024-01-04", bars.len());
Ok(())
}
#[tokio::test]
async fn test_multi_symbol_multi_file() -> Result<()> {
let mut file_mapping = HashMap::new();
// ES.FUT: single file
file_mapping.insert(
"ES.FUT".to_string(),
vec![get_test_file("ES.FUT_ohlcv-1m_2024-01-02.dbn")],
);
// NQ.FUT: single file
file_mapping.insert(
"NQ.FUT".to_string(),
vec![get_test_file("NQ.FUT_ohlcv-1m_2024-01-02.dbn")],
);
let data_source = DbnDataSource::new_multi_file(file_mapping).await?;
let symbols = vec!["ES.FUT".to_string(), "NQ.FUT".to_string()];
let bars = data_source.load_multi_symbol_bars_all(&symbols).await?;
assert!(!bars.is_empty(), "Should load bars from multiple symbols");
// Verify bars are sorted by timestamp
for i in 1..bars.len() {
assert!(
bars[i].timestamp >= bars[i - 1].timestamp,
"Multi-symbol bars should be sorted chronologically"
);
}
// Verify both symbols present
let has_es = bars.iter().any(|b| b.symbol == "ES.FUT");
let has_nq = bars.iter().any(|b| b.symbol == "NQ.FUT");
assert!(has_es, "Should have ES.FUT bars");
assert!(has_nq, "Should have NQ.FUT bars");
println!("✅ Multi-symbol: {} total bars (ES.FUT + NQ.FUT)", bars.len());
Ok(())
}
#[tokio::test]
async fn test_add_symbol_mapping_multi() -> Result<()> {
let file_mapping = HashMap::new();
let mut data_source = DbnDataSource::new(file_mapping).await?;
// Add multiple files for symbol
data_source.add_symbol_mapping_multi(
"ESH4".to_string(),
vec![
get_test_file("ESH4_ohlcv-1m_2024-01-03.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-04.dbn"),
],
);
assert_eq!(data_source.get_file_count("ESH4"), 2);
let bars = data_source.load_ohlcv_bars_all("ESH4").await?;
assert!(!bars.is_empty(), "Should load from dynamically added files");
println!("✅ Dynamic mapping: {} bars from 2 files", bars.len());
Ok(())
}
#[tokio::test]
async fn test_performance_linear_scaling() -> Result<()> {
// Test 1 file
let mut file_mapping_1 = HashMap::new();
file_mapping_1.insert(
"ESH4".to_string(),
vec![get_test_file("ESH4_ohlcv-1m_2024-01-03.dbn")],
);
let data_source_1 = DbnDataSource::new_multi_file(file_mapping_1).await?;
let start_1 = std::time::Instant::now();
let bars_1 = data_source_1.load_ohlcv_bars_all("ESH4").await?;
let duration_1 = start_1.elapsed();
// Test 3 files
let mut file_mapping_3 = HashMap::new();
file_mapping_3.insert(
"ESH4".to_string(),
vec![
get_test_file("ESH4_ohlcv-1m_2024-01-03.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-04.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-05.dbn"),
],
);
let data_source_3 = DbnDataSource::new_multi_file(file_mapping_3).await?;
let start_3 = std::time::Instant::now();
let bars_3 = data_source_3.load_ohlcv_bars_all("ESH4").await?;
let duration_3 = start_3.elapsed();
let avg_per_file_1 = duration_1.as_secs_f64() * 1000.0;
let avg_per_file_3 = duration_3.as_secs_f64() * 1000.0 / 3.0;
println!("✅ Performance scaling:");
println!(" 1 file: {} bars in {:.2}ms", bars_1.len(), avg_per_file_1);
println!(" 3 files: {} bars in {:.2}ms total ({:.2}ms/file)",
bars_3.len(), duration_3.as_secs_f64() * 1000.0, avg_per_file_3);
// Linear scaling validation: 3-file average should be within 2x of 1-file time
// (allows for slight overhead from sorting/merging)
assert!(
avg_per_file_3 < avg_per_file_1 * 2.0,
"Performance should scale linearly (got {:.2}ms/file for 3 files vs {:.2}ms for 1 file)",
avg_per_file_3,
avg_per_file_1
);
// Absolute performance: should maintain <10ms per file target
assert!(
avg_per_file_3 < 10.0,
"Should maintain <10ms per file (got {:.2}ms)",
avg_per_file_3
);
Ok(())
}
#[tokio::test]
async fn test_data_availability_multi_file() -> Result<()> {
let mut file_mapping = HashMap::new();
file_mapping.insert(
"ESH4".to_string(),
vec![
get_test_file("ESH4_ohlcv-1m_2024-01-03.dbn"),
get_test_file("ESH4_ohlcv-1m_2024-01-04.dbn"),
],
);
let data_source = DbnDataSource::new_multi_file(file_mapping).await?;
let start = Utc.with_ymd_and_hms(2024, 1, 3, 0, 0, 0).unwrap();
let end = Utc.with_ymd_and_hms(2024, 1, 5, 0, 0, 0).unwrap();
let available = data_source
.check_data_availability("ESH4", start, end)
.await?;
assert!(available, "Data should be available");
// Nonexistent symbol
let not_available = data_source
.check_data_availability("NONEXISTENT", start, end)
.await?;
assert!(!not_available, "Nonexistent symbol should not be available");
println!("✅ Data availability check passed");
Ok(())
}
#[tokio::test]
async fn test_partial_day_range() -> Result<()> {
let mut file_mapping = HashMap::new();
file_mapping.insert(
"ES.FUT".to_string(),
vec![get_test_file("ES.FUT_ohlcv-1m_2024-01-02.dbn")],
);
let data_source = DbnDataSource::new_multi_file(file_mapping).await?;
// Query just first hour
let start = Utc.with_ymd_and_hms(2024, 1, 2, 14, 30, 0).unwrap(); // Market open (ET)
let end = Utc.with_ymd_and_hms(2024, 1, 2, 15, 30, 0).unwrap();
let bars = data_source
.load_ohlcv_bars_range("ES.FUT", start, end)
.await?;
assert!(!bars.is_empty(), "Should load partial day data");
// All bars should be within 1-hour window
for bar in &bars {
assert!(bar.timestamp >= start && bar.timestamp <= end);
}
// Should be ~60 bars (1-minute data for 1 hour)
assert!(
bars.len() >= 50 && bars.len() <= 70,
"Expected ~60 bars for 1 hour, got {}",
bars.len()
);
println!("✅ Partial day: {} bars in 1-hour window", bars.len());
Ok(())
}