Files
foxhunt/services/backtesting_service/benches/real_data_comprehensive_benchmark.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

439 lines
14 KiB
Rust

//! Comprehensive Performance Benchmarks with Real DBN Data
//!
//! This benchmark suite validates production readiness by testing all components
//! with real market data from Databento files.
//!
//! ## Performance Targets
//!
//! - DBN file loading: <10ms (target) / 0.70ms (baseline from Wave 18)
//! - Market data query: <1ms
//! - Full backtest (1 day): <5s
//! - Memory usage: <100MB for typical backtest
//!
//! ## Real Data Files
//!
//! - ES.FUT (E-mini S&P 500): 390 bars, 1-minute OHLCV, 2024-01-02
//! - NQ.FUT (E-mini NASDAQ): 390 bars, 1-minute OHLCV, 2024-01-02
//! - CL.FUT (Crude Oil): 390 bars, 1-minute OHLCV, 2024-01-02
//! - ESH4 (ES March 2024): 77 bars, 1-minute OHLCV, 2024-01-03
use backtesting_service::dbn_repository::DbnMarketDataRepository;
use backtesting_service::repositories::MarketDataRepository;
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::collections::HashMap;
use tokio::runtime::Runtime;
/// Get absolute path to test data directory
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()
}
/// Get test file paths for all available symbols
fn get_test_files() -> HashMap<String, String> {
let workspace_root = get_workspace_root();
let data_dir = workspace_root.join("test_data/real/databento");
let mut files = HashMap::new();
// ES.FUT - E-mini S&P 500 (390 bars)
let es_file = data_dir.join("ES.FUT_ohlcv-1m_2024-01-02.dbn");
if es_file.exists() {
files.insert("ES.FUT".to_string(), es_file.to_string_lossy().to_string());
}
// NQ.FUT - E-mini NASDAQ (390 bars)
let nq_file = data_dir.join("NQ.FUT_ohlcv-1m_2024-01-02.dbn");
if nq_file.exists() {
files.insert("NQ.FUT".to_string(), nq_file.to_string_lossy().to_string());
}
// CL.FUT - Crude Oil (390 bars)
let cl_file = data_dir.join("CL.FUT_ohlcv-1m_2024-01-02.dbn");
if cl_file.exists() {
files.insert("CL.FUT".to_string(), cl_file.to_string_lossy().to_string());
}
// ESH4 - ES March 2024 contract (77 bars)
// NOTE: Skipping ESH4 files - they may be in different format or compressed
// let esh4_file = data_dir.join("ESH4_ohlcv-1m_2024-01-03.dbn");
// if esh4_file.exists() {
// files.insert("ESH4".to_string(), esh4_file.to_string_lossy().to_string());
// }
if files.is_empty() {
panic!("No valid DBN test files found in {}. Please run data validation first.", data_dir.display());
}
files
}
/// Benchmark 1: Single-file DBN loading (baseline)
fn bench_single_file_loading(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let files = get_test_files();
if !files.contains_key("ES.FUT") {
eprintln!("Warning: ES.FUT test file not found, skipping single-file benchmark");
return;
}
let mut file_mapping = HashMap::new();
file_mapping.insert("ES.FUT".to_string(), files["ES.FUT"].clone());
let repo = rt.block_on(async {
DbnMarketDataRepository::new(file_mapping).await.unwrap()
});
let mut group = c.benchmark_group("single_file_loading");
group.throughput(Throughput::Elements(390)); // 390 bars expected
group.bench_function("es_fut_full_day", |b| {
b.to_async(&rt).iter(|| async {
let symbols = vec!["ES.FUT".to_string()];
let start_time = 1704153600_000_000_000i64; // 2024-01-02 00:00:00
let end_time = 1704240000_000_000_000i64; // 2024-01-03 00:00:00
let data = repo
.load_historical_data(&symbols, start_time, end_time)
.await
.unwrap();
black_box(data);
});
});
group.finish();
}
/// Benchmark 2: Multi-file loading (different symbols)
fn bench_multi_file_loading(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let files = get_test_files();
if files.len() < 2 {
eprintln!("Warning: Need at least 2 test files, skipping multi-file benchmark");
return;
}
let repo = rt.block_on(async {
DbnMarketDataRepository::new(files.clone()).await.unwrap()
});
let mut group = c.benchmark_group("multi_file_loading");
// Test with 2, 3, and all available symbols
let symbols: Vec<String> = files.keys().cloned().collect();
let test_configs: Vec<(usize, Vec<String>)> = if symbols.len() >= 3 {
vec![
(2, vec![symbols[0].clone(), symbols[1].clone()]),
(3, vec![symbols[0].clone(), symbols[1].clone(), symbols[2].clone()]),
]
} else if symbols.len() == 2 {
vec![
(2, vec![symbols[0].clone(), symbols[1].clone()]),
]
} else {
vec![]
};
for (count, symbols) in test_configs {
if symbols.iter().all(|s| files.contains_key(s)) {
group.throughput(Throughput::Elements((count * 390) as u64));
group.bench_with_input(
BenchmarkId::new("symbols", count),
&symbols,
|b, symbols| {
b.to_async(&rt).iter(|| async {
let start_time = 1704153600_000_000_000i64;
let end_time = 1704240000_000_000_000i64;
let data = repo
.load_historical_data(symbols, start_time, end_time)
.await
.unwrap();
black_box(data);
});
},
);
}
}
group.finish();
}
/// Benchmark 3: Time range queries (partial day)
fn bench_time_range_queries(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let files = get_test_files();
if !files.contains_key("ES.FUT") {
eprintln!("Warning: ES.FUT test file not found, skipping time range benchmark");
return;
}
let mut file_mapping = HashMap::new();
file_mapping.insert("ES.FUT".to_string(), files["ES.FUT"].clone());
let repo = rt.block_on(async {
DbnMarketDataRepository::new(file_mapping).await.unwrap()
});
let mut group = c.benchmark_group("time_range_queries");
// Test different time ranges
let test_ranges = vec![
("1_hour", 1704203400_000_000_000i64, 1704207000_000_000_000i64), // 1 hour
("4_hours", 1704203400_000_000_000i64, 1704217800_000_000_000i64), // 4 hours
("full_day", 1704153600_000_000_000i64, 1704240000_000_000_000i64), // 24 hours
];
for (name, start, end) in test_ranges {
group.bench_with_input(
BenchmarkId::new("range", name),
&(start, end),
|b, &(start, end)| {
b.to_async(&rt).iter(|| async {
let symbols = vec!["ES.FUT".to_string()];
let data = repo
.load_historical_data(&symbols, start, end)
.await
.unwrap();
black_box(data);
});
},
);
}
group.finish();
}
/// Benchmark 4: Repeated queries (caching performance)
fn bench_repeated_queries(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let files = get_test_files();
if !files.contains_key("ES.FUT") {
eprintln!("Warning: ES.FUT test file not found, skipping repeated queries benchmark");
return;
}
let mut file_mapping = HashMap::new();
file_mapping.insert("ES.FUT".to_string(), files["ES.FUT"].clone());
let repo = rt.block_on(async {
DbnMarketDataRepository::new(file_mapping).await.unwrap()
});
let mut group = c.benchmark_group("repeated_queries");
for iterations in [5, 10, 20].iter() {
group.bench_with_input(
BenchmarkId::from_parameter(iterations),
iterations,
|b, &iterations| {
b.to_async(&rt).iter(|| async {
for _ in 0..iterations {
let symbols = vec!["ES.FUT".to_string()];
let start_time = 1704153600_000_000_000i64;
let end_time = 1704240000_000_000_000i64;
let data = repo
.load_historical_data(&symbols, start_time, end_time)
.await
.unwrap();
black_box(data);
}
});
},
);
}
group.finish();
}
/// Benchmark 5: Query latency percentiles
fn bench_query_latency(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let files = get_test_files();
if !files.contains_key("ES.FUT") {
eprintln!("Warning: ES.FUT test file not found, skipping latency benchmark");
return;
}
let mut file_mapping = HashMap::new();
file_mapping.insert("ES.FUT".to_string(), files["ES.FUT"].clone());
let repo = rt.block_on(async {
DbnMarketDataRepository::new(file_mapping).await.unwrap()
});
c.bench_function("query_latency_p50_p95_p99", |b| {
b.to_async(&rt).iter(|| async {
let symbols = vec!["ES.FUT".to_string()];
let start_time = 1704153600_000_000_000i64;
let end_time = 1704240000_000_000_000i64;
let data = repo
.load_historical_data(&symbols, start_time, end_time)
.await
.unwrap();
black_box(data);
});
});
}
/// Benchmark 6: Memory usage estimation
fn bench_memory_usage(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let files = get_test_files();
if files.is_empty() {
eprintln!("Warning: No test files found, skipping memory benchmark");
return;
}
let repo = rt.block_on(async {
DbnMarketDataRepository::new(files.clone()).await.unwrap()
});
let mut group = c.benchmark_group("memory_usage");
// Load all symbols and measure memory
let all_symbols: Vec<String> = files.keys().cloned().collect();
group.bench_function("all_symbols_full_day", |b| {
b.to_async(&rt).iter(|| async {
let start_time = 1704153600_000_000_000i64;
let end_time = 1704240000_000_000_000i64;
let data = repo
.load_historical_data(&all_symbols, start_time, end_time)
.await
.unwrap();
// Calculate approximate memory usage
let bar_size = std::mem::size_of::<backtesting_service::strategy_engine::MarketData>();
let total_bytes = data.len() * bar_size;
let total_mb = total_bytes as f64 / 1_048_576.0;
println!("\nMemory usage estimate:");
println!(" Bars loaded: {}", data.len());
println!(" Approx memory: {:.2} MB", total_mb);
black_box(data);
});
});
group.finish();
}
/// Benchmark 7: Cold start performance
fn bench_cold_start(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let files = get_test_files();
if !files.contains_key("ES.FUT") {
eprintln!("Warning: ES.FUT test file not found, skipping cold start benchmark");
return;
}
let mut group = c.benchmark_group("cold_start");
// Measure repository creation + first query
group.bench_function("repo_creation_and_first_query", |b| {
b.to_async(&rt).iter(|| async {
let mut file_mapping = HashMap::new();
file_mapping.insert("ES.FUT".to_string(), files["ES.FUT"].clone());
let repo = DbnMarketDataRepository::new(file_mapping).await.unwrap();
let symbols = vec!["ES.FUT".to_string()];
let start_time = 1704153600_000_000_000i64;
let end_time = 1704240000_000_000_000i64;
let data = repo
.load_historical_data(&symbols, start_time, end_time)
.await
.unwrap();
black_box(data);
});
});
group.finish();
}
/// Benchmark 8: Concurrent queries
fn bench_concurrent_queries(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let files = get_test_files();
if files.len() < 2 {
eprintln!("Warning: Need at least 2 test files, skipping concurrent benchmark");
return;
}
let repo = rt.block_on(async {
DbnMarketDataRepository::new(files.clone()).await.unwrap()
});
let mut group = c.benchmark_group("concurrent_queries");
// Test concurrent queries for different symbols
group.bench_function("concurrent_2_symbols", |b| {
b.to_async(&rt).iter(|| async {
let start_time = 1704153600_000_000_000i64;
let end_time = 1704240000_000_000_000i64;
// Spawn concurrent queries
let handles: Vec<_> = vec!["ES.FUT", "NQ.FUT"]
.into_iter()
.filter(|s| files.contains_key(*s))
.map(|symbol| {
let repo_clone = &repo;
let symbols = vec![symbol.to_string()];
async move {
repo_clone
.load_historical_data(&symbols, start_time, end_time)
.await
.unwrap()
}
})
.collect();
// Wait for all to complete
let results = futures::future::join_all(handles).await;
black_box(results);
});
});
group.finish();
}
criterion_group!(
benches,
bench_single_file_loading,
bench_multi_file_loading,
bench_time_range_queries,
bench_repeated_queries,
bench_query_latency,
bench_memory_usage,
bench_cold_start,
bench_concurrent_queries,
);
criterion_main!(benches);