#![allow(unexpected_cfgs)] #![cfg(feature = "__trading_service_integration")] //! ML Performance Metrics Tests - TDD Implementation //! //! Following strict RED-GREEN-REFACTOR methodology use chrono::Utc; use sqlx::PgPool; use std::env; /// Get test database pool async fn get_test_db_pool() -> PgPool { let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); PgPool::connect(&database_url) .await .expect("Failed to connect to test database") } /// Helper to create test prediction fn create_test_prediction() -> trading_service::ml_performance_metrics::MLPrediction { trading_service::ml_performance_metrics::MLPrediction { model_name: "DQN".to_string(), features: vec![0.5; 26], predicted_action: 0, // Buy confidence: 0.85, symbol: "ES.FUT".to_string(), timestamp: Utc::now(), } } #[tokio::test] async fn test_ml_predictions_table_exists() { // RED: ml_predictions table doesn't exist yet let pool = get_test_db_pool().await; let result = sqlx::query("SELECT * FROM ml_predictions LIMIT 1") .fetch_optional(&pool) .await; assert!(result.is_ok(), "ml_predictions table should exist"); } #[tokio::test] async fn test_insert_ml_prediction() { // RED: MLMetricsStore doesn't exist yet let pool = get_test_db_pool().await; let store = trading_service::ml_performance_metrics::MLMetricsStore::new(pool.clone()); let prediction = create_test_prediction(); let prediction_id = store .insert_prediction(&prediction) .await .expect("Failed to insert prediction"); assert!(prediction_id > 0, "Prediction ID should be positive"); } #[tokio::test] async fn test_record_outcome() { // RED: Test recording actual outcome let pool = get_test_db_pool().await; let store = trading_service::ml_performance_metrics::MLMetricsStore::new(pool.clone()); let prediction = create_test_prediction(); let prediction_id = store .insert_prediction(&prediction) .await .expect("Failed to insert prediction"); // Record outcome after 5 minutes let outcome = trading_service::ml_performance_metrics::PredictionOutcome { prediction_id, actual_action: 0, // Actual was Buy (correct) pnl: 250.0, // Profit timestamp: Utc::now(), }; store .record_outcome(&outcome) .await .expect("Failed to record outcome"); let stats = store .get_accuracy_stats("DQN") .await .expect("Failed to get accuracy stats"); assert_eq!(stats.total_predictions, 1, "Total predictions should be 1"); assert_eq!( stats.correct_predictions, 1, "Correct predictions should be 1" ); assert!( (stats.accuracy - 1.0).abs() < 0.01, "Accuracy should be 1.0" ); } #[tokio::test] async fn test_model_accuracy_calculation() { // RED: Test accuracy calculation across multiple predictions let pool = get_test_db_pool().await; let store = trading_service::ml_performance_metrics::MLMetricsStore::new(pool.clone()); // Clean up test data let model_name = format!("TEST_DQN_{}", Utc::now().timestamp_millis()); // Insert 10 predictions for i in 0..10 { let pred = trading_service::ml_performance_metrics::MLPrediction { model_name: model_name.clone(), features: vec![0.5; 26], predicted_action: (i % 3) as i16, // Vary actions confidence: 0.8, symbol: "ES.FUT".to_string(), timestamp: Utc::now(), }; let pred_id = store .insert_prediction(&pred) .await .expect("Failed to insert prediction"); // Record outcomes (7/10 correct) let outcome = trading_service::ml_performance_metrics::PredictionOutcome { prediction_id: pred_id, actual_action: if i < 7 { pred.predicted_action } else { ((pred.predicted_action + 1) % 3) }, pnl: if i < 7 { 100.0 } else { -50.0 }, timestamp: Utc::now(), }; store .record_outcome(&outcome) .await .expect("Failed to record outcome"); } let stats = store .get_accuracy_stats(&model_name) .await .expect("Failed to get accuracy stats"); assert_eq!( stats.total_predictions, 10, "Total predictions should be 10" ); assert_eq!( stats.correct_predictions, 7, "Correct predictions should be 7" ); assert!( (stats.accuracy - 0.7).abs() < 0.01, "Accuracy should be 0.7" ); } #[tokio::test] async fn test_sharpe_ratio_calculation() { // RED: Test Sharpe ratio calculation let pool = get_test_db_pool().await; let store = trading_service::ml_performance_metrics::MLMetricsStore::new(pool.clone()); let model_name = format!("TEST_SHARPE_{}", Utc::now().timestamp_millis()); // Insert predictions with PnL outcomes for pnl in vec![100.0, -50.0, 200.0, -30.0, 150.0] { let pred = trading_service::ml_performance_metrics::MLPrediction { model_name: model_name.clone(), features: vec![0.5; 26], predicted_action: 0, confidence: 0.8, symbol: "ES.FUT".to_string(), timestamp: Utc::now(), }; let pred_id = store .insert_prediction(&pred) .await .expect("Failed to insert prediction"); let outcome = trading_service::ml_performance_metrics::PredictionOutcome { prediction_id: pred_id, actual_action: pred.predicted_action, pnl, timestamp: Utc::now(), }; store .record_outcome(&outcome) .await .expect("Failed to record outcome"); } let sharpe = store .calculate_sharpe_ratio(&model_name) .await .expect("Failed to calculate Sharpe"); assert!(sharpe > 0.0, "Sharpe ratio should be positive (profitable)"); } #[tokio::test] async fn test_ensemble_vs_individual_accuracy() { // RED: Test comparing ensemble accuracy vs individual models let pool = get_test_db_pool().await; let store = trading_service::ml_performance_metrics::MLMetricsStore::new(pool.clone()); let timestamp = Utc::now().timestamp_millis(); // Insert predictions for each model for model in &["DQN", "PPO", "MAMBA2", "TFT"] { let model_name = format!("TEST_{}_{}", model, timestamp); for _ in 0..5 { let pred = trading_service::ml_performance_metrics::MLPrediction { model_name: model_name.clone(), features: vec![0.5; 26], predicted_action: 0, confidence: 0.8, symbol: "ES.FUT".to_string(), timestamp: Utc::now(), }; let pred_id = store .insert_prediction(&pred) .await .expect("Failed to insert prediction"); let outcome = trading_service::ml_performance_metrics::PredictionOutcome { prediction_id: pred_id, actual_action: 0, pnl: 100.0, timestamp: Utc::now(), }; store .record_outcome(&outcome) .await .expect("Failed to record outcome"); } } let comparison = store .compare_model_accuracy() .await .expect("Failed to compare models"); assert!( comparison.len() >= 4, "Should have at least 4 models in comparison" ); for (model, accuracy) in comparison { assert!(!model.is_empty(), "Model name should not be empty"); assert!( accuracy >= 0.0 && accuracy <= 1.0, "Accuracy should be between 0 and 1" ); } }