Patterns applied: - Pattern 2: Float comparison (2x: utils.rs, var_edge_cases_tests.rs) - Pattern 7: Date/time construction (2x: production_streaming.rs, streaming.rs) - Pattern 1: Duration/time ops (2x: rate limiter, semaphore) - Pattern 4: Optional field access (1x: position_tracker.rs) Changes: - data/src/utils.rs: Float sort with NaN handling - data/src/providers/benzinga/production_streaming.rs: Rate limiter + semaphore + date/time - data/src/providers/benzinga/streaming.rs: Date/time construction - risk/src/position_tracker.rs: Emergency fallback counter - risk/tests/var_edge_cases_tests.rs: Test helper float sort Test impact: 0 failures (182/182 passing) Compilation: Clean (0 errors, 0 warnings) Time: 25 min (44% under budget)
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
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
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
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
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
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
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
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
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
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
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
// 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
// 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
// Use small samples for fast iteration
let bars = get_sample_real_data(50).await?;
2. Comprehensive Integration Tests
// Use full datasets for thorough testing
let bars = get_es_fut_bars().await?;
3. Regime-Specific Tests
// 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
// 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:
// 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)
#[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)
#[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_BARSstatic 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 patternsstrategy_execution.rs- Strategy testing with real dataperformance_metrics.rs- Metrics validation
Contributing
When adding new fixtures:
- Add static cache variable
- Implement cached loading function
- Add unit tests
- Update this documentation
License
Part of Foxhunt HFT Trading System - See project LICENSE