- 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
432 lines
9.8 KiB
Markdown
432 lines
9.8 KiB
Markdown
# Test Fixtures and Helpers Documentation
|
|
|
|
## Overview
|
|
|
|
This module provides **high-performance, cached access** to real DBN market data for backtesting tests. All data is loaded once and cached in static memory, dramatically improving test execution speed.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
tests/
|
|
├── fixtures/
|
|
│ └── mod.rs # Cached data loading (singleton pattern)
|
|
└── helpers.rs # Validation utilities
|
|
```
|
|
|
|
## Performance Characteristics
|
|
|
|
### Without Caching (Naive Approach)
|
|
- **Per test**: 5-10ms (DBN file I/O)
|
|
- **100 tests**: 500-1000ms total
|
|
- **Problem**: Repeated file reads, slow test suites
|
|
|
|
### With Caching (This Module)
|
|
- **First test**: 5-10ms (one-time load)
|
|
- **Subsequent tests**: ~0.1μs (memory read)
|
|
- **100 tests**: ~10ms total after first load
|
|
- **Benefit**: **50-100x faster** test execution
|
|
|
|
## Usage Guide
|
|
|
|
### 1. Basic Data Loading
|
|
|
|
```rust
|
|
use fixtures::{get_es_fut_bars, get_nq_fut_bars, get_cl_fut_bars};
|
|
|
|
#[tokio::test]
|
|
async fn test_with_real_data() -> anyhow::Result<()> {
|
|
// Load ES.FUT data (cached after first call)
|
|
let bars = get_es_fut_bars().await?;
|
|
|
|
assert!(!bars.is_empty());
|
|
assert_eq!(bars[0].symbol, "ES.FUT");
|
|
assert!(bars.len() > 350); // ~390 bars for trading day
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### 2. Multi-Symbol Testing
|
|
|
|
```rust
|
|
use fixtures::get_multi_symbol_bars;
|
|
|
|
#[tokio::test]
|
|
async fn test_multiple_symbols() -> anyhow::Result<()> {
|
|
// Load multiple symbols in parallel
|
|
let symbols = vec!["ES.FUT", "NQ.FUT", "CL.FUT"];
|
|
let data = get_multi_symbol_bars(&symbols).await?;
|
|
|
|
assert_eq!(data.len(), 3);
|
|
assert!(data.contains_key("ES.FUT"));
|
|
|
|
// Access data for each symbol
|
|
let es_bars = &data["ES.FUT"];
|
|
let nq_bars = &data["NQ.FUT"];
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### 3. Regime-Specific Testing
|
|
|
|
```rust
|
|
use fixtures::{get_regime_sample, RegimeType};
|
|
|
|
#[tokio::test]
|
|
async fn test_trending_strategy() -> anyhow::Result<()> {
|
|
// Get sample of trending market data
|
|
let bars = get_regime_sample(RegimeType::Trending).await?;
|
|
|
|
// Test trend-following strategy
|
|
let signals = my_strategy.generate_signals(&bars);
|
|
assert!(signals.len() > 0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_ranging_strategy() -> anyhow::Result<()> {
|
|
// Get sample of ranging/sideways market
|
|
let bars = get_regime_sample(RegimeType::Ranging).await?;
|
|
|
|
// Test mean-reversion strategy
|
|
let signals = my_strategy.generate_signals(&bars);
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### 4. Date-Specific Data
|
|
|
|
```rust
|
|
use fixtures::get_bars_for_date;
|
|
use chrono::NaiveDate;
|
|
|
|
#[tokio::test]
|
|
async fn test_specific_date() -> anyhow::Result<()> {
|
|
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?;
|
|
|
|
// All bars from 2024-01-02
|
|
for bar in &bars {
|
|
assert_eq!(bar.timestamp.date_naive(), date.date_naive());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## Data Validation Helpers
|
|
|
|
### 5. OHLCV Validation
|
|
|
|
```rust
|
|
use helpers::assert_valid_ohlcv;
|
|
|
|
#[test]
|
|
fn test_data_quality() {
|
|
let bars = load_my_data();
|
|
|
|
// Validates:
|
|
// - High >= Low
|
|
// - High >= Open, Close
|
|
// - Low <= Open, Close
|
|
// - All prices > 0
|
|
// - Volume >= 0
|
|
assert_valid_ohlcv(&bars);
|
|
}
|
|
```
|
|
|
|
### 6. Time Series Validation
|
|
|
|
```rust
|
|
use helpers::{assert_chronological, assert_no_large_gaps};
|
|
|
|
#[test]
|
|
fn test_timestamp_quality() {
|
|
let bars = load_my_data();
|
|
|
|
// Ensure bars sorted by timestamp
|
|
assert_chronological(&bars);
|
|
|
|
// Ensure no gaps > 5 minutes (for 1-minute data)
|
|
assert_no_large_gaps(&bars, 5);
|
|
}
|
|
```
|
|
|
|
### 7. Price Range Validation
|
|
|
|
```rust
|
|
use helpers::assert_price_range;
|
|
|
|
#[test]
|
|
fn test_realistic_prices() {
|
|
let bars = load_my_data();
|
|
|
|
// Validates prices within realistic range:
|
|
// ES.FUT: 3000-6000
|
|
// NQ.FUT: 12000-20000
|
|
// CL.FUT: 50-100
|
|
assert_price_range(&bars, "ES.FUT");
|
|
}
|
|
```
|
|
|
|
### 8. Trade Validation
|
|
|
|
```rust
|
|
use helpers::{assert_valid_trade, assert_valid_trade_sequence};
|
|
|
|
#[test]
|
|
fn test_backtest_trades() {
|
|
let trades = run_backtest();
|
|
|
|
// Validate individual trade
|
|
assert_valid_trade(&trades[0]);
|
|
|
|
// Validate entire sequence:
|
|
// - No overlapping trades
|
|
// - Chronological order
|
|
// - Valid PnL calculations
|
|
assert_valid_trade_sequence(&trades);
|
|
}
|
|
```
|
|
|
|
### 9. Performance Metrics Validation
|
|
|
|
```rust
|
|
use helpers::{assert_sharpe_bounds, assert_drawdown_bounds, assert_win_rate_valid};
|
|
|
|
#[test]
|
|
fn test_metrics_realistic() {
|
|
let metrics = calculate_performance_metrics();
|
|
|
|
// Sharpe ratio between -3.0 and 5.0
|
|
assert_sharpe_bounds(metrics.sharpe_ratio, -3.0, 5.0);
|
|
|
|
// Max drawdown <= 50%
|
|
assert_drawdown_bounds(metrics.max_drawdown, 50.0);
|
|
|
|
// Win rate between 0% and 100%
|
|
assert_win_rate_valid(metrics.win_rate);
|
|
}
|
|
```
|
|
|
|
### 10. Data Quality Reports
|
|
|
|
```rust
|
|
use helpers::generate_quality_report;
|
|
|
|
#[test]
|
|
fn test_print_quality_report() {
|
|
let bars = load_my_data();
|
|
|
|
let report = generate_quality_report(&bars);
|
|
println!("{}", report);
|
|
|
|
// Output:
|
|
// === Data Quality Report ===
|
|
//
|
|
// Total bars: 390
|
|
// Symbol: ES.FUT
|
|
// Date range: 2024-01-02 00:00:00 to 2024-01-02 23:59:00
|
|
//
|
|
// Price Statistics:
|
|
// Min: 4705.25
|
|
// Max: 4748.75
|
|
// Avg: 4725.50
|
|
// Range: 0.92%
|
|
//
|
|
// Volatility:
|
|
// Annualized: 18.5%
|
|
//
|
|
// Quality Checks:
|
|
// OHLCV errors: 0
|
|
// Chronology errors: 0
|
|
//
|
|
// === End Report ===
|
|
}
|
|
```
|
|
|
|
## Available Data
|
|
|
|
### ES.FUT (E-mini S&P 500)
|
|
- **File**: `ES.FUT_ohlcv-1m_2024-01-02.dbn`
|
|
- **Bars**: ~390 (trading day)
|
|
- **Size**: 95KB
|
|
- **Price range**: 4700-4750 (typical 2024)
|
|
- **Use case**: General strategy testing, high liquidity
|
|
|
|
### NQ.FUT (E-mini NASDAQ-100)
|
|
- **File**: `NQ.FUT_ohlcv-1m_2024-01-02.dbn`
|
|
- **Bars**: ~390
|
|
- **Size**: 93KB
|
|
- **Price range**: 16500-16700 (typical 2024)
|
|
- **Use case**: Tech sector strategies
|
|
|
|
### CL.FUT (WTI Crude Oil)
|
|
- **File**: `CL.FUT_ohlcv-1m_2024-01-02.dbn`
|
|
- **Bars**: ~1440 (24-hour trading)
|
|
- **Size**: 1.5MB
|
|
- **Price range**: 71-73 USD/barrel
|
|
- **Use case**: Extended hours trading, energy strategies
|
|
|
|
## Regime Types
|
|
|
|
### RegimeType::Trending
|
|
- **Characteristics**: Strong directional movement
|
|
- **Detection**: Price change > 1.5% over 60 bars
|
|
- **Use case**: Trend-following strategies
|
|
|
|
### RegimeType::Ranging
|
|
- **Characteristics**: Bounded oscillation, sideways movement
|
|
- **Detection**: Price range < 0.8% over 60 bars
|
|
- **Use case**: Mean-reversion strategies
|
|
|
|
### RegimeType::Volatile
|
|
- **Characteristics**: High price fluctuations
|
|
- **Detection**: Standard deviation > 0.5%
|
|
- **Use case**: Options strategies, volatility trading
|
|
|
|
### RegimeType::Stable
|
|
- **Characteristics**: Low volatility, steady prices
|
|
- **Detection**: Standard deviation < 0.3%
|
|
- **Use case**: Low-risk strategies, carry trades
|
|
|
|
## Performance Tips
|
|
|
|
### ✅ DO
|
|
|
|
```rust
|
|
// Reuse cached data across tests
|
|
let bars = get_es_fut_bars().await?;
|
|
|
|
// Use regime samples for targeted testing
|
|
let trending = get_regime_sample(RegimeType::Trending).await?;
|
|
|
|
// Validate data quality with helpers
|
|
assert_valid_ohlcv(&bars);
|
|
assert_chronological(&bars);
|
|
```
|
|
|
|
### ❌ DON'T
|
|
|
|
```rust
|
|
// Don't load DBN files directly (bypass cache)
|
|
let data_source = DbnDataSource::new(file_mapping).await?; // SLOW
|
|
|
|
// Don't generate synthetic data when real data available
|
|
let fake_bars = generate_fake_bars(100); // LESS REALISTIC
|
|
|
|
// Don't skip validation (catch bugs early)
|
|
// (missing assert_valid_ohlcv check)
|
|
```
|
|
|
|
## Testing Best Practices
|
|
|
|
### 1. Fast Unit Tests
|
|
```rust
|
|
// Use small samples for fast iteration
|
|
let bars = get_sample_real_data(50).await?;
|
|
```
|
|
|
|
### 2. Comprehensive Integration Tests
|
|
```rust
|
|
// Use full datasets for thorough testing
|
|
let bars = get_es_fut_bars().await?;
|
|
```
|
|
|
|
### 3. Regime-Specific Tests
|
|
```rust
|
|
// Test each regime type separately
|
|
let trending = get_regime_sample(RegimeType::Trending).await?;
|
|
let ranging = get_regime_sample(RegimeType::Ranging).await?;
|
|
```
|
|
|
|
### 4. Multi-Symbol Tests
|
|
```rust
|
|
// Test portfolio strategies with multiple symbols
|
|
let data = get_multi_symbol_bars(&["ES.FUT", "NQ.FUT"]).await?;
|
|
```
|
|
|
|
## Thread Safety
|
|
|
|
All fixtures use `once_cell::sync::Lazy` and `tokio::sync::RwLock` for safe concurrent access:
|
|
|
|
```rust
|
|
// Multiple tests can access cache concurrently
|
|
#[tokio::test]
|
|
async fn test_1() {
|
|
let bars = get_es_fut_bars().await?; // Thread-safe
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_2() {
|
|
let bars = get_es_fut_bars().await?; // Same cache, different test
|
|
}
|
|
```
|
|
|
|
## Migration Guide
|
|
|
|
### Before (Slow, No Cache)
|
|
```rust
|
|
#[tokio::test]
|
|
async fn test_old_way() {
|
|
let mut file_mapping = HashMap::new();
|
|
file_mapping.insert(
|
|
"ES.FUT".to_string(),
|
|
"../../test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn".to_string()
|
|
);
|
|
let data_source = DbnDataSource::new(file_mapping).await?;
|
|
let bars = data_source.load_ohlcv_bars("ES.FUT").await?;
|
|
// 5-10ms PER TEST
|
|
}
|
|
```
|
|
|
|
### After (Fast, Cached)
|
|
```rust
|
|
#[tokio::test]
|
|
async fn test_new_way() {
|
|
let bars = get_es_fut_bars().await?;
|
|
// ~0.1μs after first load
|
|
}
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Cache Not Working?
|
|
- Ensure using `get_es_fut_bars()` not direct DBN loading
|
|
- Check `CACHED_ES_BARS` static is initialized
|
|
|
|
### File Not Found?
|
|
- Verify DBN files exist: `ls test_data/real/databento/`
|
|
- Check `get_project_root()` finds correct directory
|
|
|
|
### Test Failures?
|
|
- Use `generate_quality_report()` to inspect data
|
|
- Check validation helpers for detailed error messages
|
|
|
|
## Examples Directory
|
|
|
|
See `services/backtesting_service/tests/` for full examples:
|
|
- `dbn_integration_tests.rs` - DBN loading patterns
|
|
- `strategy_execution.rs` - Strategy testing with real data
|
|
- `performance_metrics.rs` - Metrics validation
|
|
|
|
## Contributing
|
|
|
|
When adding new fixtures:
|
|
1. Add static cache variable
|
|
2. Implement cached loading function
|
|
3. Add unit tests
|
|
4. Update this documentation
|
|
|
|
## License
|
|
|
|
Part of Foxhunt HFT Trading System - See project LICENSE
|