Wave 64-65 cleanup: Proto regeneration and build system updates from Tonic 0.12→0.14 upgrade Files updated: - Cargo.lock: Dependency resolution for Tonic 0.14.2 - All build.rs: Updated for tonic-prost-build - Proto files: Regenerated with tonic-prost 0.14 - Examples/tests: Updated for new gRPC API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
885 lines
40 KiB
Rust
885 lines
40 KiB
Rust
#![allow(unused_crate_dependencies)]
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::time::timeout;
|
|
use chrono::{DateTime, Utc, TimeZone};
|
|
|
|
use crate::framework::{TestOrchestrator, TestFrameworkConfig, PerformanceThresholds, IntegrationTestResult};
|
|
use crate::framework::mocks::MockServiceRegistry;
|
|
|
|
use config::{ConfigManager, BacktestingConfig, MLConfig};
|
|
use common::Order;
|
|
use common::OrderType;
|
|
use common::Position;
|
|
use common::MarketData;
|
|
use common::TimeRange;
|
|
use ml::models::{ModelPrediction, TradingSignal};
|
|
|
|
/// Backtesting Service Integration Tests
|
|
///
|
|
/// Tests the backtesting service functionality including:
|
|
/// - Strategy backtesting execution
|
|
/// - Historical data processing
|
|
/// - Performance metrics calculation
|
|
/// - ML model integration for backtesting
|
|
/// - Multi-timeframe analysis
|
|
/// - Risk metrics validation
|
|
pub struct BacktestingServiceTests {
|
|
orchestrator: TestOrchestrator,
|
|
mock_registry: MockServiceRegistry,
|
|
backtesting_config: BacktestingConfig,
|
|
}
|
|
|
|
impl BacktestingServiceTests {
|
|
/// Initialize Backtesting Service test suite
|
|
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
|
let config = TestFrameworkConfig {
|
|
performance_thresholds: PerformanceThresholds {
|
|
max_e2e_latency_us: 50, // 50μs end-to-end
|
|
max_order_latency_us: 20, // 20μs order processing
|
|
max_risk_latency_us: 10, // 10μs risk validation
|
|
max_ml_latency_ms: 50, // 50ms ML inference
|
|
max_config_reload_ms: 100, // 100ms config reload
|
|
min_throughput_ops_sec: 10000, // 10k ops/sec minimum
|
|
},
|
|
database_url: std::env::var("TEST_DATABASE_URL")
|
|
.unwrap_or_else(|_| "postgresql://test:test@localhost:5432/foxhunt_test".to_string()),
|
|
service_ports: {
|
|
let mut ports = HashMap::new();
|
|
ports.insert("trading".to_string(), 50051);
|
|
ports.insert("backtesting".to_string(), 50052);
|
|
ports.insert("ml_training".to_string(), 50053);
|
|
ports
|
|
},
|
|
test_timeout_secs: 60, // Backtesting may take longer
|
|
};
|
|
|
|
let orchestrator = TestOrchestrator::new(config).await?;
|
|
let mock_registry = MockServiceRegistry::new().await?;
|
|
|
|
// Load backtesting configuration
|
|
let config_manager = ConfigManager::new().await?;
|
|
let backtesting_config = config_manager.get_backtesting_config().await?;
|
|
|
|
Ok(Self {
|
|
orchestrator,
|
|
mock_registry,
|
|
backtesting_config,
|
|
})
|
|
}
|
|
|
|
/// Test Suite 1: Strategy Backtesting Execution
|
|
///
|
|
/// Validates core backtesting functionality:
|
|
/// - Strategy parameter loading
|
|
/// - Historical data replay
|
|
/// - Order simulation and execution
|
|
/// - Performance metrics calculation
|
|
pub async fn test_strategy_backtesting_execution(&self) -> IntegrationTestResult {
|
|
println!("🔄 Testing Backtesting Service - Strategy Execution");
|
|
|
|
let start_time = Instant::now();
|
|
let mut test_results = IntegrationTestResult::new("Backtesting Service Strategy Execution");
|
|
|
|
// Start backtesting service
|
|
self.orchestrator.start_service("backtesting").await
|
|
.map_err(|e| format!("Failed to start backtesting service: {}", e))?;
|
|
|
|
// Wait for service readiness
|
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
|
|
|
// Test Case 1: Simple Moving Average Strategy Backtest
|
|
let sma_strategy_config = BacktestingStrategyConfig {
|
|
id: uuid::Uuid::new_v4(),
|
|
name: "SMA_Crossover_Test".to_string(),
|
|
strategy_type: "SMA_Crossover".to_string(),
|
|
parameters: {
|
|
let mut params = HashMap::new();
|
|
params.insert("fast_period".to_string(), serde_json::Value::from(10));
|
|
params.insert("slow_period".to_string(), serde_json::Value::from(20));
|
|
params.insert("symbol".to_string(), serde_json::Value::from("EURUSD"));
|
|
params.insert("position_size".to_string(), serde_json::Value::from(100000.0));
|
|
params
|
|
},
|
|
time_range: TimeRange {
|
|
start: Utc.ymd_opt(2024, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap(),
|
|
end: Utc.ymd_opt(2024, 1, 31).unwrap().and_hms_opt(23, 59, 59).unwrap(),
|
|
},
|
|
timeframe: "1H".to_string(),
|
|
};
|
|
|
|
let backtest_start = Instant::now();
|
|
let backtest_result = self.orchestrator.run_backtest(sma_strategy_config.clone()).await;
|
|
let backtest_duration = backtest_start.elapsed();
|
|
|
|
match backtest_result {
|
|
Ok(backtest_id) => {
|
|
test_results.add_success("SMA strategy backtest started successfully");
|
|
|
|
// Wait for backtest completion
|
|
let mut completion_checks = 0;
|
|
let max_checks = 30; // Max 30 seconds
|
|
let mut is_complete = false;
|
|
|
|
while completion_checks < max_checks {
|
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
|
|
|
match self.orchestrator.get_backtest_status(&backtest_id).await {
|
|
Ok(status) => {
|
|
match status.as_str() {
|
|
"completed" => {
|
|
is_complete = true;
|
|
break;
|
|
}
|
|
"failed" | "error" => {
|
|
test_results.add_failure(&format!("Backtest failed with status: {}", status));
|
|
break;
|
|
}
|
|
"running" | "pending" => {
|
|
// Continue waiting
|
|
}
|
|
_ => {
|
|
test_results.add_failure(&format!("Unknown backtest status: {}", status));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to get backtest status: {}", e));
|
|
break;
|
|
}
|
|
}
|
|
|
|
completion_checks += 1;
|
|
}
|
|
|
|
if is_complete {
|
|
test_results.add_success("SMA strategy backtest completed successfully");
|
|
|
|
// Retrieve and validate backtest results
|
|
match self.orchestrator.get_backtest_results(&backtest_id).await {
|
|
Ok(results) => {
|
|
test_results.add_success("Backtest results retrieved successfully");
|
|
|
|
// Validate basic metrics presence
|
|
if results.total_trades > 0 {
|
|
test_results.add_success(&format!("Backtest generated trades: {}", results.total_trades));
|
|
} else {
|
|
test_results.add_failure("Backtest generated no trades");
|
|
}
|
|
|
|
if results.total_pnl.is_some() {
|
|
test_results.add_success("Total PnL calculated");
|
|
} else {
|
|
test_results.add_failure("Total PnL not calculated");
|
|
}
|
|
|
|
if results.sharpe_ratio.is_some() {
|
|
test_results.add_success("Sharpe ratio calculated");
|
|
} else {
|
|
test_results.add_failure("Sharpe ratio not calculated");
|
|
}
|
|
|
|
if results.max_drawdown.is_some() {
|
|
test_results.add_success("Max drawdown calculated");
|
|
} else {
|
|
test_results.add_failure("Max drawdown not calculated");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to retrieve backtest results: {}", e));
|
|
}
|
|
}
|
|
} else {
|
|
test_results.add_failure("Backtest did not complete within timeout");
|
|
}
|
|
|
|
// Validate backtest execution time (should be reasonable for 1 month of data)
|
|
if backtest_duration.as_secs() <= 10 {
|
|
test_results.add_success(&format!("Backtest execution time acceptable: {}s", backtest_duration.as_secs()));
|
|
} else {
|
|
test_results.add_failure(&format!("Backtest execution time too long: {}s", backtest_duration.as_secs()));
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to start backtest: {}", e));
|
|
}
|
|
}
|
|
|
|
test_results.duration = start_time.elapsed();
|
|
test_results.finalize();
|
|
|
|
Ok(test_results)
|
|
}
|
|
|
|
/// Test Suite 2: Historical Data Processing
|
|
///
|
|
/// Validates historical data handling:
|
|
/// - Data loading and validation
|
|
/// - Multiple timeframe support
|
|
/// - Data quality checks
|
|
/// - Missing data handling
|
|
pub async fn test_historical_data_processing(&self) -> IntegrationTestResult {
|
|
println!("📊 Testing Backtesting Service - Historical Data Processing");
|
|
|
|
let start_time = Instant::now();
|
|
let mut test_results = IntegrationTestResult::new("Backtesting Service Historical Data Processing");
|
|
|
|
// Test Case 1: Data Range Validation
|
|
let data_request = HistoricalDataRequest {
|
|
symbol: "EURUSD".to_string(),
|
|
timeframe: "1H".to_string(),
|
|
start_time: Utc.ymd_opt(2024, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap(),
|
|
end_time: Utc.ymd_opt(2024, 1, 31).unwrap().and_hms_opt(23, 59, 59).unwrap(),
|
|
};
|
|
|
|
let data_load_start = Instant::now();
|
|
let data_result = self.orchestrator.load_historical_data(data_request.clone()).await;
|
|
let data_load_duration = data_load_start.elapsed();
|
|
|
|
match data_result {
|
|
Ok(data) => {
|
|
test_results.add_success("Historical data loaded successfully");
|
|
|
|
// Validate data completeness
|
|
if data.len() > 0 {
|
|
test_results.add_success(&format!("Historical data contains {} records", data.len()));
|
|
} else {
|
|
test_results.add_failure("Historical data is empty");
|
|
}
|
|
|
|
// Validate data quality
|
|
let mut valid_records = 0;
|
|
let mut invalid_records = 0;
|
|
|
|
for record in &data {
|
|
if record.open > 0.0 && record.high >= record.open &&
|
|
record.low <= record.open && record.close > 0.0 &&
|
|
record.high >= record.low {
|
|
valid_records += 1;
|
|
} else {
|
|
invalid_records += 1;
|
|
}
|
|
}
|
|
|
|
if invalid_records == 0 {
|
|
test_results.add_success("All historical data records are valid");
|
|
} else {
|
|
test_results.add_failure(&format!(
|
|
"Invalid historical data records found: {} invalid out of {}",
|
|
invalid_records, data.len()
|
|
));
|
|
}
|
|
|
|
// Validate data chronological order
|
|
let mut is_chronological = true;
|
|
for i in 1..data.len() {
|
|
if data[i].timestamp <= data[i-1].timestamp {
|
|
is_chronological = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if is_chronological {
|
|
test_results.add_success("Historical data is in chronological order");
|
|
} else {
|
|
test_results.add_failure("Historical data is not in chronological order");
|
|
}
|
|
|
|
// Validate data loading performance
|
|
if data_load_duration.as_millis() <= 1000 {
|
|
test_results.add_success(&format!("Data loading time acceptable: {}ms", data_load_duration.as_millis()));
|
|
} else {
|
|
test_results.add_failure(&format!("Data loading time too long: {}ms", data_load_duration.as_millis()));
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to load historical data: {}", e));
|
|
}
|
|
}
|
|
|
|
// Test Case 2: Multiple Timeframe Support
|
|
let timeframes = vec!["1M", "5M", "15M", "1H", "4H", "1D"];
|
|
|
|
for timeframe in &timeframes {
|
|
let tf_request = HistoricalDataRequest {
|
|
symbol: "EURUSD".to_string(),
|
|
timeframe: timeframe.to_string(),
|
|
start_time: Utc.ymd_opt(2024, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap(),
|
|
end_time: Utc.ymd_opt(2024, 1, 7).unwrap().and_hms_opt(23, 59, 59).unwrap(), // 1 week
|
|
};
|
|
|
|
match self.orchestrator.load_historical_data(tf_request).await {
|
|
Ok(data) => {
|
|
test_results.add_success(&format!("Historical data loaded for {} timeframe", timeframe));
|
|
|
|
if data.len() > 0 {
|
|
test_results.add_success(&format!("{} timeframe has {} records", timeframe, data.len()));
|
|
} else {
|
|
test_results.add_failure(&format!("{} timeframe has no data", timeframe));
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to load {} timeframe data: {}", timeframe, e));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test Case 3: Data Gap Detection
|
|
let gap_request = HistoricalDataRequest {
|
|
symbol: "EURUSD".to_string(),
|
|
timeframe: "1H".to_string(),
|
|
start_time: Utc.ymd_opt(2023, 12, 15).unwrap().and_hms_opt(0, 0, 0).unwrap(), // Include weekend
|
|
end_time: Utc.ymd_opt(2023, 12, 18).unwrap().and_hms_opt(23, 59, 59).unwrap(),
|
|
};
|
|
|
|
match self.orchestrator.load_historical_data(gap_request).await {
|
|
Ok(data) => {
|
|
let gaps = self.orchestrator.detect_data_gaps(&data, "1H").await;
|
|
|
|
match gaps {
|
|
Ok(gap_list) => {
|
|
if gap_list.len() > 0 {
|
|
test_results.add_success(&format!("Data gaps detected successfully: {} gaps found", gap_list.len()));
|
|
} else {
|
|
test_results.add_success("No data gaps found (or continuous data)");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to detect data gaps: {}", e));
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to load data for gap detection: {}", e));
|
|
}
|
|
}
|
|
|
|
test_results.duration = start_time.elapsed();
|
|
test_results.finalize();
|
|
|
|
Ok(test_results)
|
|
}
|
|
|
|
/// Test Suite 3: ML Model Integration
|
|
///
|
|
/// Validates ML model integration in backtesting:
|
|
/// - Model loading for backtesting
|
|
/// - Signal generation from historical data
|
|
/// - Model prediction accuracy tracking
|
|
/// - Multiple model comparison
|
|
pub async fn test_ml_model_integration(&self) -> IntegrationTestResult {
|
|
println!("🧠 Testing Backtesting Service - ML Model Integration");
|
|
|
|
let start_time = Instant::now();
|
|
let mut test_results = IntegrationTestResult::new("Backtesting Service ML Model Integration");
|
|
|
|
// Test Case 1: ML Model Loading for Backtesting
|
|
let model_config = MLModelConfig {
|
|
model_name: "MAMBA_EURUSD_v1".to_string(),
|
|
model_type: "MAMBA".to_string(),
|
|
version: "v1.0.0".to_string(),
|
|
parameters: {
|
|
let mut params = HashMap::new();
|
|
params.insert("sequence_length".to_string(), serde_json::Value::from(100));
|
|
params.insert("prediction_horizon".to_string(), serde_json::Value::from(5));
|
|
params
|
|
},
|
|
};
|
|
|
|
let model_load_start = Instant::now();
|
|
let model_load_result = self.orchestrator.load_model_for_backtesting(model_config.clone()).await;
|
|
let model_load_duration = model_load_start.elapsed();
|
|
|
|
match model_load_result {
|
|
Ok(model_id) => {
|
|
test_results.add_success("ML model loaded for backtesting successfully");
|
|
|
|
// Validate model loading performance
|
|
if model_load_duration.as_millis() <= self.orchestrator.config.performance_thresholds.max_ml_latency_ms {
|
|
test_results.add_success(&format!("Model loading time within threshold: {}ms <= {}ms",
|
|
model_load_duration.as_millis(),
|
|
self.orchestrator.config.performance_thresholds.max_ml_latency_ms
|
|
));
|
|
} else {
|
|
test_results.add_failure(&format!("Model loading time exceeds threshold: {}ms > {}ms",
|
|
model_load_duration.as_millis(),
|
|
self.orchestrator.config.performance_thresholds.max_ml_latency_ms
|
|
));
|
|
}
|
|
|
|
// Test Case 2: ML-Based Strategy Backtesting
|
|
let ml_strategy_config = BacktestingStrategyConfig {
|
|
id: uuid::Uuid::new_v4(),
|
|
name: "ML_MAMBA_Strategy_Test".to_string(),
|
|
strategy_type: "ML_Prediction".to_string(),
|
|
parameters: {
|
|
let mut params = HashMap::new();
|
|
params.insert("model_id".to_string(), serde_json::Value::from(model_id.to_string()));
|
|
params.insert("symbol".to_string(), serde_json::Value::from("EURUSD"));
|
|
params.insert("confidence_threshold".to_string(), serde_json::Value::from(0.7));
|
|
params.insert("position_size".to_string(), serde_json::Value::from(100000.0));
|
|
params
|
|
},
|
|
time_range: TimeRange {
|
|
start: Utc.ymd_opt(2024, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap(),
|
|
end: Utc.ymd_opt(2024, 1, 15).unwrap().and_hms_opt(23, 59, 59).unwrap(),
|
|
},
|
|
timeframe: "1H".to_string(),
|
|
};
|
|
|
|
let ml_backtest_start = Instant::now();
|
|
match self.orchestrator.run_backtest(ml_strategy_config.clone()).await {
|
|
Ok(backtest_id) => {
|
|
test_results.add_success("ML-based backtest started successfully");
|
|
|
|
// Wait for completion with extended timeout for ML processing
|
|
let mut completion_checks = 0;
|
|
let max_checks = 60; // 60 seconds for ML backtesting
|
|
let mut is_complete = false;
|
|
|
|
while completion_checks < max_checks {
|
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
|
|
|
match self.orchestrator.get_backtest_status(&backtest_id).await {
|
|
Ok(status) => {
|
|
if status == "completed" {
|
|
is_complete = true;
|
|
break;
|
|
} else if status == "failed" || status == "error" {
|
|
test_results.add_failure(&format!("ML backtest failed: {}", status));
|
|
break;
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to get ML backtest status: {}", e));
|
|
break;
|
|
}
|
|
}
|
|
|
|
completion_checks += 1;
|
|
}
|
|
|
|
if is_complete {
|
|
test_results.add_success("ML-based backtest completed successfully");
|
|
|
|
// Retrieve ML-specific results
|
|
match self.orchestrator.get_backtest_results(&backtest_id).await {
|
|
Ok(results) => {
|
|
// Validate ML-specific metrics
|
|
if results.prediction_accuracy.is_some() {
|
|
let accuracy = results.prediction_accuracy.unwrap();
|
|
test_results.add_success(&format!("ML prediction accuracy: {:.2}%", accuracy * 100.0));
|
|
|
|
if accuracy > 0.5 {
|
|
test_results.add_success("ML model shows better than random performance");
|
|
} else {
|
|
test_results.add_failure("ML model performance not better than random");
|
|
}
|
|
} else {
|
|
test_results.add_failure("ML prediction accuracy not calculated");
|
|
}
|
|
|
|
if results.signal_count.is_some() {
|
|
test_results.add_success(&format!("ML signals generated: {}", results.signal_count.unwrap()));
|
|
} else {
|
|
test_results.add_failure("ML signal count not available");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to get ML backtest results: {}", e));
|
|
}
|
|
}
|
|
} else {
|
|
test_results.add_failure("ML backtest did not complete within timeout");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to start ML backtest: {}", e));
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to load ML model for backtesting: {}", e));
|
|
}
|
|
}
|
|
|
|
// Test Case 3: Model Performance Comparison
|
|
let comparison_models = vec![
|
|
("MAMBA_EURUSD_v1", "MAMBA"),
|
|
("DQN_EURUSD_v1", "DQN"),
|
|
("TFT_EURUSD_v1", "TFT"),
|
|
];
|
|
|
|
let mut model_results = Vec::new();
|
|
|
|
for (model_name, model_type) in &comparison_models {
|
|
let model_config = MLModelConfig {
|
|
model_name: model_name.to_string(),
|
|
model_type: model_type.to_string(),
|
|
version: "v1.0.0".to_string(),
|
|
parameters: HashMap::new(),
|
|
};
|
|
|
|
// Quick comparison backtest (shorter period)
|
|
let comparison_strategy = BacktestingStrategyConfig {
|
|
id: uuid::Uuid::new_v4(),
|
|
name: format!("{}_Comparison", model_name),
|
|
strategy_type: "ML_Prediction".to_string(),
|
|
parameters: {
|
|
let mut params = HashMap::new();
|
|
params.insert("symbol".to_string(), serde_json::Value::from("EURUSD"));
|
|
params.insert("confidence_threshold".to_string(), serde_json::Value::from(0.7));
|
|
params.insert("position_size".to_string(), serde_json::Value::from(100000.0));
|
|
params
|
|
},
|
|
time_range: TimeRange {
|
|
start: Utc.ymd_opt(2024, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap(),
|
|
end: Utc.ymd_opt(2024, 1, 7).unwrap().and_hms_opt(23, 59, 59).unwrap(), // 1 week
|
|
},
|
|
timeframe: "1H".to_string(),
|
|
};
|
|
|
|
// Store results for comparison (in real implementation)
|
|
model_results.push((model_name.clone(), format!("Model {} comparison queued", model_name)));
|
|
}
|
|
|
|
test_results.add_success(&format!("Model performance comparison initialized for {} models", model_results.len()));
|
|
|
|
test_results.duration = start_time.elapsed();
|
|
test_results.finalize();
|
|
|
|
Ok(test_results)
|
|
}
|
|
|
|
/// Test Suite 4: Performance Metrics Validation
|
|
///
|
|
/// Validates backtesting performance metrics:
|
|
/// - Risk metrics calculation (Sharpe, Sortino, etc.)
|
|
/// - Drawdown analysis
|
|
/// - Trade statistics
|
|
/// - Benchmark comparison
|
|
pub async fn test_performance_metrics_validation(&self) -> IntegrationTestResult {
|
|
println!("📈 Testing Backtesting Service - Performance Metrics");
|
|
|
|
let start_time = Instant::now();
|
|
let mut test_results = IntegrationTestResult::new("Backtesting Service Performance Metrics");
|
|
|
|
// Run a comprehensive backtest for metrics validation
|
|
let metrics_strategy_config = BacktestingStrategyConfig {
|
|
id: uuid::Uuid::new_v4(),
|
|
name: "Metrics_Validation_Test".to_string(),
|
|
strategy_type: "SMA_Crossover".to_string(),
|
|
parameters: {
|
|
let mut params = HashMap::new();
|
|
params.insert("fast_period".to_string(), serde_json::Value::from(5));
|
|
params.insert("slow_period".to_string(), serde_json::Value::from(15));
|
|
params.insert("symbol".to_string(), serde_json::Value::from("EURUSD"));
|
|
params.insert("position_size".to_string(), serde_json::Value::from(100000.0));
|
|
params
|
|
},
|
|
time_range: TimeRange {
|
|
start: Utc.ymd_opt(2024, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap(),
|
|
end: Utc.ymd_opt(2024, 3, 31).unwrap().and_hms_opt(23, 59, 59).unwrap(), // 3 months
|
|
},
|
|
timeframe: "1H".to_string(),
|
|
};
|
|
|
|
match self.orchestrator.run_backtest(metrics_strategy_config).await {
|
|
Ok(backtest_id) => {
|
|
test_results.add_success("Metrics validation backtest started");
|
|
|
|
// Wait for completion
|
|
let mut completion_checks = 0;
|
|
let max_checks = 90; // 90 seconds for 3-month backtest
|
|
let mut is_complete = false;
|
|
|
|
while completion_checks < max_checks {
|
|
tokio::time::sleep(Duration::from_millis(1000)).await;
|
|
|
|
match self.orchestrator.get_backtest_status(&backtest_id).await {
|
|
Ok(status) => {
|
|
if status == "completed" {
|
|
is_complete = true;
|
|
break;
|
|
} else if status == "failed" || status == "error" {
|
|
test_results.add_failure(&format!("Metrics backtest failed: {}", status));
|
|
break;
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to get backtest status: {}", e));
|
|
break;
|
|
}
|
|
}
|
|
|
|
completion_checks += 1;
|
|
}
|
|
|
|
if is_complete {
|
|
test_results.add_success("Metrics validation backtest completed");
|
|
|
|
// Retrieve comprehensive results
|
|
match self.orchestrator.get_backtest_results(&backtest_id).await {
|
|
Ok(results) => {
|
|
// Test Case 1: Basic Performance Metrics
|
|
if results.total_pnl.is_some() {
|
|
test_results.add_success("Total PnL calculated");
|
|
} else {
|
|
test_results.add_failure("Total PnL missing");
|
|
}
|
|
|
|
if results.total_trades > 0 {
|
|
test_results.add_success(&format!("Trades executed: {}", results.total_trades));
|
|
} else {
|
|
test_results.add_failure("No trades executed");
|
|
}
|
|
|
|
if results.win_rate.is_some() {
|
|
let win_rate = results.win_rate.unwrap();
|
|
test_results.add_success(&format!("Win rate: {:.2}%", win_rate * 100.0));
|
|
} else {
|
|
test_results.add_failure("Win rate not calculated");
|
|
}
|
|
|
|
// Test Case 2: Risk Metrics
|
|
if results.sharpe_ratio.is_some() {
|
|
let sharpe = results.sharpe_ratio.unwrap();
|
|
test_results.add_success(&format!("Sharpe ratio: {:.3}", sharpe));
|
|
|
|
if sharpe.is_finite() {
|
|
test_results.add_success("Sharpe ratio is valid (finite)");
|
|
} else {
|
|
test_results.add_failure("Sharpe ratio is invalid (infinite/NaN)");
|
|
}
|
|
} else {
|
|
test_results.add_failure("Sharpe ratio not calculated");
|
|
}
|
|
|
|
if results.sortino_ratio.is_some() {
|
|
test_results.add_success(&format!("Sortino ratio: {:.3}", results.sortino_ratio.unwrap()));
|
|
} else {
|
|
test_results.add_failure("Sortino ratio not calculated");
|
|
}
|
|
|
|
if results.max_drawdown.is_some() {
|
|
let max_dd = results.max_drawdown.unwrap();
|
|
test_results.add_success(&format!("Max drawdown: {:.2}%", max_dd * 100.0));
|
|
|
|
if max_dd >= 0.0 {
|
|
test_results.add_success("Max drawdown has correct sign (non-negative)");
|
|
} else {
|
|
test_results.add_failure("Max drawdown has incorrect sign (should be non-negative)");
|
|
}
|
|
} else {
|
|
test_results.add_failure("Max drawdown not calculated");
|
|
}
|
|
|
|
// Test Case 3: Advanced Statistics
|
|
if results.var_95.is_some() {
|
|
test_results.add_success(&format!("VaR 95%: {:.2}", results.var_95.unwrap()));
|
|
} else {
|
|
test_results.add_failure("VaR 95% not calculated");
|
|
}
|
|
|
|
if results.calmar_ratio.is_some() {
|
|
test_results.add_success(&format!("Calmar ratio: {:.3}", results.calmar_ratio.unwrap()));
|
|
} else {
|
|
test_results.add_failure("Calmar ratio not calculated");
|
|
}
|
|
|
|
// Test Case 4: Trade Statistics
|
|
if results.average_trade_duration.is_some() {
|
|
test_results.add_success(&format!("Average trade duration: {:.1} hours",
|
|
results.average_trade_duration.unwrap()));
|
|
} else {
|
|
test_results.add_failure("Average trade duration not calculated");
|
|
}
|
|
|
|
if results.largest_winning_trade.is_some() && results.largest_losing_trade.is_some() {
|
|
test_results.add_success("Largest win/loss trades tracked");
|
|
} else {
|
|
test_results.add_failure("Largest win/loss trades not tracked");
|
|
}
|
|
|
|
// Test Case 5: Benchmark Comparison
|
|
if results.benchmark_comparison.is_some() {
|
|
test_results.add_success("Benchmark comparison available");
|
|
} else {
|
|
test_results.add_failure("Benchmark comparison not performed");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to retrieve backtest results: {}", e));
|
|
}
|
|
}
|
|
} else {
|
|
test_results.add_failure("Metrics validation backtest timed out");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
test_results.add_failure(&format!("Failed to start metrics validation backtest: {}", e));
|
|
}
|
|
}
|
|
|
|
test_results.duration = start_time.elapsed();
|
|
test_results.finalize();
|
|
|
|
Ok(test_results)
|
|
}
|
|
|
|
/// Execute complete Backtesting Service test suite
|
|
pub async fn run_all_tests(&self) -> Result<Vec<IntegrationTestResult>, Box<dyn std::error::Error>> {
|
|
println!("🚀 Starting Backtesting Service Integration Test Suite");
|
|
|
|
let mut results = Vec::new();
|
|
|
|
// Test Suite 1: Strategy Backtesting Execution
|
|
match timeout(Duration::from_secs(self.orchestrator.config.test_timeout_secs * 2),
|
|
self.test_strategy_backtesting_execution()).await {
|
|
Ok(Ok(result)) => results.push(result),
|
|
Ok(Err(e)) => {
|
|
let mut failed_result = IntegrationTestResult::new("Backtesting Service Strategy Execution");
|
|
failed_result.add_failure(&format!("Test suite failed: {}", e));
|
|
failed_result.finalize();
|
|
results.push(failed_result);
|
|
},
|
|
Err(_) => {
|
|
let mut timeout_result = IntegrationTestResult::new("Backtesting Service Strategy Execution");
|
|
timeout_result.add_failure("Test suite timed out");
|
|
timeout_result.finalize();
|
|
results.push(timeout_result);
|
|
}
|
|
}
|
|
|
|
// Test Suite 2: Historical Data Processing
|
|
match timeout(Duration::from_secs(self.orchestrator.config.test_timeout_secs),
|
|
self.test_historical_data_processing()).await {
|
|
Ok(Ok(result)) => results.push(result),
|
|
Ok(Err(e)) => {
|
|
let mut failed_result = IntegrationTestResult::new("Backtesting Service Historical Data Processing");
|
|
failed_result.add_failure(&format!("Test suite failed: {}", e));
|
|
failed_result.finalize();
|
|
results.push(failed_result);
|
|
},
|
|
Err(_) => {
|
|
let mut timeout_result = IntegrationTestResult::new("Backtesting Service Historical Data Processing");
|
|
timeout_result.add_failure("Test suite timed out");
|
|
timeout_result.finalize();
|
|
results.push(timeout_result);
|
|
}
|
|
}
|
|
|
|
// Test Suite 3: ML Model Integration
|
|
match timeout(Duration::from_secs(self.orchestrator.config.test_timeout_secs * 3),
|
|
self.test_ml_model_integration()).await {
|
|
Ok(Ok(result)) => results.push(result),
|
|
Ok(Err(e)) => {
|
|
let mut failed_result = IntegrationTestResult::new("Backtesting Service ML Model Integration");
|
|
failed_result.add_failure(&format!("Test suite failed: {}", e));
|
|
failed_result.finalize();
|
|
results.push(failed_result);
|
|
},
|
|
Err(_) => {
|
|
let mut timeout_result = IntegrationTestResult::new("Backtesting Service ML Model Integration");
|
|
timeout_result.add_failure("Test suite timed out");
|
|
timeout_result.finalize();
|
|
results.push(timeout_result);
|
|
}
|
|
}
|
|
|
|
// Test Suite 4: Performance Metrics Validation
|
|
match timeout(Duration::from_secs(self.orchestrator.config.test_timeout_secs * 3),
|
|
self.test_performance_metrics_validation()).await {
|
|
Ok(Ok(result)) => results.push(result),
|
|
Ok(Err(e)) => {
|
|
let mut failed_result = IntegrationTestResult::new("Backtesting Service Performance Metrics");
|
|
failed_result.add_failure(&format!("Test suite failed: {}", e));
|
|
failed_result.finalize();
|
|
results.push(failed_result);
|
|
},
|
|
Err(_) => {
|
|
let mut timeout_result = IntegrationTestResult::new("Backtesting Service Performance Metrics");
|
|
timeout_result.add_failure("Test suite timed out");
|
|
timeout_result.finalize();
|
|
results.push(timeout_result);
|
|
}
|
|
}
|
|
|
|
// Print summary
|
|
let total_tests = results.len();
|
|
let passed_tests = results.iter().filter(|r| r.passed).count();
|
|
let failed_tests = total_tests - passed_tests;
|
|
|
|
println!("📊 Backtesting Service Integration Test Summary:");
|
|
println!(" Total Test Suites: {}", total_tests);
|
|
println!(" Passed: {} ✅", passed_tests);
|
|
println!(" Failed: {} ❌", failed_tests);
|
|
|
|
if failed_tests == 0 {
|
|
println!("🎉 All Backtesting Service integration tests passed!");
|
|
} else {
|
|
println!("⚠️ {} Backtesting Service integration test suite(s) failed", failed_tests);
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
}
|
|
|
|
// Supporting types for backtesting tests
|
|
#[derive(Debug, Clone)]
|
|
pub struct BacktestingStrategyConfig {
|
|
pub id: uuid::Uuid,
|
|
pub name: String,
|
|
pub strategy_type: String,
|
|
pub parameters: HashMap<String, serde_json::Value>,
|
|
pub time_range: TimeRange,
|
|
pub timeframe: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct HistoricalDataRequest {
|
|
pub symbol: String,
|
|
pub timeframe: String,
|
|
pub start_time: DateTime<Utc>,
|
|
pub end_time: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct MLModelConfig {
|
|
pub model_name: String,
|
|
pub model_type: String,
|
|
pub version: String,
|
|
pub parameters: HashMap<String, serde_json::Value>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tokio;
|
|
|
|
#[tokio::test]
|
|
async fn integration_test_backtesting_service_complete() {
|
|
let test_suite = BacktestingServiceTests::new().await
|
|
.expect("Failed to initialize Backtesting Service test suite");
|
|
|
|
let results = test_suite.run_all_tests().await
|
|
.expect("Failed to run Backtesting Service test suite");
|
|
|
|
// Ensure all tests passed
|
|
for result in &results {
|
|
assert!(result.passed, "Test suite '{}' failed: {:?}", result.test_name, result.failures);
|
|
}
|
|
|
|
// Validate performance requirements met
|
|
for result in &results {
|
|
assert!(
|
|
result.duration.as_secs() <= 180, // 3 minutes max for backtesting tests
|
|
"Test suite '{}' took too long: {}s",
|
|
result.test_name,
|
|
result.duration.as_secs()
|
|
);
|
|
}
|
|
}
|
|
} |