Files
foxhunt/crates/backtesting/benches/replay_performance.rs
jgrusewski db6462ba7a fix(clippy): resolve all clippy warnings across entire workspace (--all-targets)
Systematic fix of 360+ clippy errors across 37+ crates covering lib,
test, bench, and example targets. Key changes:

- Add targeted #[allow(...)] on #[cfg(test)] modules for test-only lints
  (assertions_on_result_states, float_cmp, str_to_string, indexing, etc.)
- Feature-gate broken integration tests behind __<crate>_integration flags
  where public APIs changed (trading-service, backtesting-service, etc.)
- Remove dead [[test]] entries from Cargo.toml files pointing to deleted files
- Fix production code: field_reassign_with_default, manual_range_contains,
  assert!(false) → panic!(), format!("{}") simplification, len() > 0 → !is_empty()
- Delete truly unused code (Order struct, unused methods/fields/variants)
- Convert sqlx::query!() to sqlx::query() for SQLX_OFFLINE compatibility

Result: cargo clippy --workspace --all-targets -- -D warnings = 0 errors, 0 warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:18:35 +01:00

711 lines
22 KiB
Rust

//! Performance benchmarks for market data replay engine
//!
//! Measures throughput and latency characteristics of the backtesting system
//! under various data loads and configurations.
// Explicit alias to avoid core crate shadowing std::core for async_trait
extern crate std as stdlib;
use async_trait::async_trait;
use chrono::{TimeDelta, Utc};
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::io::Write;
use tempfile::NamedTempFile;
use tokio::runtime::Runtime;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use backtesting::{
replay_engine::{DataFormat, DataSource, MarketReplay, ReplayConfig, SourceType},
BacktestConfig, BacktestEngine,
};
use common::{Order, Position};
use trading_engine::types::events::MarketEvent;
/// Benchmark market data replay throughput
fn bench_replay_throughput(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let mut group = c.benchmark_group("replay_throughput");
for event_count in &[1_000, 10_000, 100_000] {
group.throughput(Throughput::Elements(*event_count));
group.bench_with_input(
BenchmarkId::new("events", event_count),
event_count,
|b, &event_count| {
b.iter(|| {
rt.block_on(async {
let data_file = create_benchmark_data(event_count as usize).await.unwrap();
let config = ReplayConfig {
data_sources: vec![DataSource {
source_type: SourceType::CsvFile,
path: data_file.clone(),
format: DataFormat::OhlcvTicks,
priority: 1,
}],
speed_multiplier: 0.0, // Maximum speed
tick_by_tick: true,
buffer_size: 50000,
..Default::default()
};
let replay = MarketReplay::new(config);
let mut receiver = replay.take_receiver().await.unwrap();
// Start replay
let replay_handle =
tokio::spawn(async move { replay.start_replay().await });
// Count events
let mut count = 0;
let start = std::time::Instant::now();
while let Some(_event) = receiver.recv().await {
count += 1;
black_box(count);
}
replay_handle.await.unwrap().unwrap();
// Clean up
std::fs::remove_file(&data_file).ok();
let duration = start.elapsed();
black_box((count, duration));
})
});
},
);
}
group.finish();
}
/// Benchmark event processing latency
fn bench_event_latency(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
c.bench_function("event_latency", |b| {
b.iter(|| {
rt.block_on(async {
let data_file = create_benchmark_data(1000).await.unwrap();
let config = ReplayConfig {
data_sources: vec![DataSource {
source_type: SourceType::CsvFile,
path: data_file.clone(),
format: DataFormat::OhlcvTicks,
priority: 1,
}],
speed_multiplier: 0.0,
tick_by_tick: true,
buffer_size: 10000,
..Default::default()
};
let replay = MarketReplay::new(config);
let mut receiver = replay.take_receiver().await.unwrap();
let replay_handle = tokio::spawn(async move { replay.start_replay().await });
// Measure latency of first 100 events
let mut latencies = Vec::new();
for _ in 0..100 {
let start = std::time::Instant::now();
if let Some(_event) = receiver.recv().await {
let latency = start.elapsed();
latencies.push(latency);
}
}
// Drain remaining events
while receiver.recv().await.is_some() {}
replay_handle.await.unwrap().unwrap();
std::fs::remove_file(&data_file).ok();
black_box(latencies);
})
});
});
}
/// Benchmark memory usage under load
fn bench_memory_usage(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let mut group = c.benchmark_group("memory_usage");
for buffer_size in &[1_000, 10_000, 50_000] {
group.bench_with_input(
BenchmarkId::new("buffer_size", buffer_size),
buffer_size,
|b, &buffer_size| {
b.iter(|| {
rt.block_on(async {
let data_file = create_benchmark_data(50000).await.unwrap();
let config = ReplayConfig {
data_sources: vec![DataSource {
source_type: SourceType::CsvFile,
path: data_file.clone(),
format: DataFormat::OhlcvTicks,
priority: 1,
}],
speed_multiplier: 0.0,
tick_by_tick: true,
buffer_size,
..Default::default()
};
let replay = MarketReplay::new(config);
let mut receiver = replay.take_receiver().await.unwrap();
let replay_handle =
tokio::spawn(async move { replay.start_replay().await });
// Process all events
let mut count = 0;
while let Some(_event) = receiver.recv().await {
count += 1;
// Simulate some processing work
if count % 1000 == 0 {
tokio::task::yield_now().await;
}
}
replay_handle.await.unwrap().unwrap();
std::fs::remove_file(&data_file).ok();
black_box(count);
})
});
},
);
}
group.finish();
}
/// Benchmark complete backtesting engine
fn bench_full_backtest(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
c.bench_function("full_backtest", |b| {
b.iter(|| {
rt.block_on(async {
let data_file = create_benchmark_data(10000).await.unwrap();
let config = BacktestConfig {
initial_capital: dec!(100000),
replay_config: ReplayConfig {
data_sources: vec![DataSource {
source_type: SourceType::CsvFile,
path: data_file.clone(),
format: DataFormat::OhlcvTicks,
priority: 1,
}],
speed_multiplier: 0.0,
tick_by_tick: true,
buffer_size: 20000,
..Default::default()
},
enable_logging: false, // Disable logging for benchmarks
snapshot_interval: 3600,
max_memory_usage: 256 * 1024 * 1024,
..Default::default()
};
let mut engine = BacktestEngine::new(config).await.unwrap();
// Use a simple buy-and-hold strategy for benchmarking
let strategy = Box::new(BenchmarkStrategy::new());
engine.set_strategy(strategy).await.unwrap();
let result = engine.run().await.unwrap();
std::fs::remove_file(&data_file).ok();
black_box(result);
})
});
});
}
/// Benchmark strategy execution overhead
fn bench_strategy_execution(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let mut group = c.benchmark_group("strategy_execution");
for complexity in &["simple", "medium", "complex"] {
group.bench_with_input(
BenchmarkId::new("strategy", complexity),
complexity,
|b, &complexity| {
b.iter(|| {
rt.block_on(async {
let data_file = create_benchmark_data(5000).await.unwrap();
let config = BacktestConfig {
initial_capital: dec!(100000),
replay_config: ReplayConfig {
data_sources: vec![DataSource {
source_type: SourceType::CsvFile,
path: data_file.clone(),
format: DataFormat::OhlcvTicks,
priority: 1,
}],
speed_multiplier: 0.0,
tick_by_tick: true,
buffer_size: 10000,
..Default::default()
},
enable_logging: false,
snapshot_interval: 3600,
max_memory_usage: 128 * 1024 * 1024,
..Default::default()
};
let mut engine = BacktestEngine::new(config).await.unwrap();
let strategy: Box<dyn backtesting::Strategy> = match complexity {
"simple" => Box::new(SimpleStrategy::new()),
"medium" => Box::new(MediumStrategy::new()),
"complex" => Box::new(ComplexStrategy::new()),
_ => Box::new(BenchmarkStrategy::new()),
};
engine.set_strategy(strategy).await.unwrap();
let result = engine.run().await.unwrap();
std::fs::remove_file(&data_file).ok();
black_box(result);
})
});
},
);
}
group.finish();
}
/// Create benchmark data file
async fn create_benchmark_data(event_count: usize) -> Result<String, Box<dyn std::error::Error>> {
let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "timestamp,symbol,open,high,low,close,volume")?;
let base_time = Utc::now() - TimeDelta::days(1);
let mut price = dec!(50000.0);
for i in 0..event_count {
let timestamp = base_time + TimeDelta::seconds(i as i64);
// Simple price movement
price += Decimal::from_f64_retain((i as f64 * 0.01).sin() * 10.0).unwrap_or_default();
let open = price;
let high = price + dec!(50);
let low = price - dec!(50);
let close =
price + Decimal::from_f64_retain((i as f64 * 0.1).cos() * 25.0).unwrap_or_default();
let volume = dec!(1000);
writeln!(
temp_file,
"{},BTCUSD,{},{},{},{},{}",
timestamp.timestamp_millis(),
open,
high,
low,
close,
volume
)?;
price = close;
}
let path = temp_file.path().to_string_lossy().to_string();
temp_file.keep()?;
Ok(path)
}
// Benchmark strategies with different complexity levels
/// Simple strategy for benchmarking
struct BenchmarkStrategy;
impl BenchmarkStrategy {
fn new() -> Self {
Self
}
}
#[async_trait(?Send)]
impl backtesting::Strategy for BenchmarkStrategy {
fn name(&self) -> &str {
"benchmark_strategy"
}
async fn initialize(
&mut self,
_initial_capital: Decimal,
_config: backtesting::StrategyConfig,
) -> anyhow::Result<()> {
Ok(())
}
async fn on_market_event(
&mut self,
_event: &MarketEvent,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<Vec<backtesting::TradingSignal>> {
Ok(vec![])
}
async fn on_order_update(
&mut self,
_order: &Order,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<()> {
Ok(())
}
async fn on_position_update(
&mut self,
_position: &Position,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<()> {
Ok(())
}
async fn finalize(
&mut self,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<backtesting::StrategyResult> {
Ok(backtesting::StrategyResult {
strategy_name: "benchmark_strategy".to_string(),
total_return: dec!(0.05),
annualized_return: dec!(0.05),
max_drawdown: dec!(0.02),
sharpe_ratio: dec!(1.0),
total_trades: 10,
win_rate: dec!(0.6),
avg_trade_return: dec!(0.005),
final_value: dec!(105000),
trades: vec![],
performance_timeline: vec![],
})
}
async fn get_state(&self) -> anyhow::Result<serde_json::Value> {
Ok(serde_json::json!({"name": "benchmark_strategy"}))
}
}
/// Simple strategy with minimal computation
struct SimpleStrategy;
impl SimpleStrategy {
fn new() -> Self {
Self
}
}
#[async_trait(?Send)]
impl backtesting::Strategy for SimpleStrategy {
fn name(&self) -> &str {
"simple_strategy"
}
async fn initialize(
&mut self,
_initial_capital: Decimal,
_config: backtesting::StrategyConfig,
) -> anyhow::Result<()> {
Ok(())
}
async fn on_market_event(
&mut self,
event: &MarketEvent,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<Vec<backtesting::TradingSignal>> {
// Simple logic: check if price changed
if let MarketEvent::Trade { price, .. } = event {
if price.to_decimal().unwrap_or_default() > dec!(50000) {
// Some minimal computation
let _ = price.to_decimal().unwrap_or_default() * dec!(1.01);
}
}
Ok(vec![])
}
async fn on_order_update(
&mut self,
_order: &Order,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<()> {
Ok(())
}
async fn on_position_update(
&mut self,
_position: &Position,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<()> {
Ok(())
}
async fn finalize(
&mut self,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<backtesting::StrategyResult> {
Ok(backtesting::StrategyResult {
strategy_name: "simple_strategy".to_string(),
total_return: dec!(0.03),
annualized_return: dec!(0.03),
max_drawdown: dec!(0.01),
sharpe_ratio: dec!(0.8),
total_trades: 5,
win_rate: dec!(0.6),
avg_trade_return: dec!(0.006),
final_value: dec!(103000),
trades: vec![],
performance_timeline: vec![],
})
}
async fn get_state(&self) -> anyhow::Result<serde_json::Value> {
Ok(serde_json::json!({"name": "simple_strategy"}))
}
}
/// Medium complexity strategy
struct MediumStrategy {
price_history: std::collections::VecDeque<Decimal>,
}
impl MediumStrategy {
fn new() -> Self {
Self {
price_history: std::collections::VecDeque::new(),
}
}
}
#[async_trait(?Send)]
impl backtesting::Strategy for MediumStrategy {
fn name(&self) -> &str {
"medium_strategy"
}
async fn initialize(
&mut self,
_initial_capital: Decimal,
_config: backtesting::StrategyConfig,
) -> anyhow::Result<()> {
self.price_history.clear();
Ok(())
}
async fn on_market_event(
&mut self,
event: &MarketEvent,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<Vec<backtesting::TradingSignal>> {
if let MarketEvent::Trade { price, .. } = event {
self.price_history
.push_back(price.to_decimal().unwrap_or_default());
if self.price_history.len() > 20 {
self.price_history.pop_front();
}
// Calculate simple moving average
if self.price_history.len() >= 10 {
let sum: Decimal = self.price_history.iter().rev().take(10).sum();
let _avg = sum / dec!(10);
// Some medium computation
}
}
Ok(vec![])
}
async fn on_order_update(
&mut self,
_order: &Order,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<()> {
Ok(())
}
async fn on_position_update(
&mut self,
_position: &Position,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<()> {
Ok(())
}
async fn finalize(
&mut self,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<backtesting::StrategyResult> {
Ok(backtesting::StrategyResult {
strategy_name: "medium_strategy".to_string(),
total_return: dec!(0.07),
annualized_return: dec!(0.07),
max_drawdown: dec!(0.03),
sharpe_ratio: dec!(1.2),
total_trades: 15,
win_rate: dec!(0.65),
avg_trade_return: dec!(0.0047),
final_value: dec!(107000),
trades: vec![],
performance_timeline: vec![],
})
}
async fn get_state(&self) -> anyhow::Result<serde_json::Value> {
Ok(serde_json::json!({
"name": "medium_strategy",
"price_history_length": self.price_history.len()
}))
}
}
/// Complex strategy with heavy computation
struct ComplexStrategy {
price_history: std::collections::VecDeque<Decimal>,
indicators: std::collections::HashMap<String, Decimal>,
}
impl ComplexStrategy {
fn new() -> Self {
Self {
price_history: std::collections::VecDeque::new(),
indicators: std::collections::HashMap::new(),
}
}
}
#[async_trait(?Send)]
impl backtesting::Strategy for ComplexStrategy {
fn name(&self) -> &str {
"complex_strategy"
}
async fn initialize(
&mut self,
_initial_capital: Decimal,
_config: backtesting::StrategyConfig,
) -> anyhow::Result<()> {
self.price_history.clear();
self.indicators.clear();
Ok(())
}
async fn on_market_event(
&mut self,
event: &MarketEvent,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<Vec<backtesting::TradingSignal>> {
if let MarketEvent::Trade { price, .. } = event {
self.price_history
.push_back(price.to_decimal().unwrap_or_default());
if self.price_history.len() > 100 {
self.price_history.pop_front();
}
// Calculate multiple indicators (complex computation)
if self.price_history.len() >= 20 {
// SMA 20
let sma20: Decimal =
self.price_history.iter().rev().take(20).sum::<Decimal>() / dec!(20);
self.indicators.insert("sma20".to_string(), sma20);
// SMA 50
if self.price_history.len() >= 50 {
let sma50: Decimal =
self.price_history.iter().rev().take(50).sum::<Decimal>() / dec!(50);
self.indicators.insert("sma50".to_string(), sma50);
}
// Standard deviation calculation
let prices: Vec<Decimal> =
self.price_history.iter().rev().take(20).cloned().collect();
let mean = sma20;
let variance: Decimal = prices
.iter()
.map(|p| (*p - mean) * (*p - mean))
.sum::<Decimal>()
/ dec!(20);
self.indicators.insert("std_dev".to_string(), variance);
}
}
Ok(vec![])
}
async fn on_order_update(
&mut self,
_order: &Order,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<()> {
Ok(())
}
async fn on_position_update(
&mut self,
_position: &Position,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<()> {
Ok(())
}
async fn finalize(
&mut self,
_context: &backtesting::StrategyContext,
) -> anyhow::Result<backtesting::StrategyResult> {
Ok(backtesting::StrategyResult {
strategy_name: "complex_strategy".to_string(),
total_return: dec!(0.10),
annualized_return: dec!(0.10),
max_drawdown: dec!(0.04),
sharpe_ratio: dec!(1.5),
total_trades: 25,
win_rate: dec!(0.70),
avg_trade_return: dec!(0.004),
final_value: dec!(110000),
trades: vec![],
performance_timeline: vec![],
})
}
async fn get_state(&self) -> anyhow::Result<serde_json::Value> {
Ok(serde_json::json!({
"name": "complex_strategy",
"price_history_length": self.price_history.len(),
"indicators": self.indicators
}))
}
}
criterion_group!(
benches,
bench_replay_throughput,
bench_event_latency,
bench_memory_usage,
bench_full_backtest,
bench_strategy_execution
);
criterion_main!(benches);