- 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
439 lines
14 KiB
Rust
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);
|