//! Regime Detection gRPC Integration Tests //! //! Tests for Wave D regime detection endpoints: //! - GetRegimeState: Current regime state for a symbol //! - GetRegimeTransitions: Historical regime transitions //! //! **IMPORTANT**: These tests require a running Trading Service instance. //! Run with: `cargo test -p trading_service --test regime_grpc_integration_test -- --ignored` //! //! Setup: //! 1. Start services: `docker-compose up -d` //! 2. Start Trading Service: `cargo run -p trading_service --bin trading_service --release &` //! 3. Wait for startup: `sleep 5` //! 4. Run tests: `cargo test -p trading_service --test regime_grpc_integration_test -- --ignored` #![allow(unused_crate_dependencies, clippy::expect_fun_call)] mod common; use common::auth_helpers::{create_test_jwt, TestAuthConfig}; use tonic::metadata::MetadataValue; use tonic::transport::Channel; use tonic::{Request, Status}; use trading_service::proto::trading::trading_service_client::TradingServiceClient; use trading_service::proto::trading::{GetRegimeStateRequest, GetRegimeTransitionsRequest}; /// Helper function to create an authenticated gRPC client async fn create_client() -> Result< TradingServiceClient< tonic::service::interceptor::InterceptedService< Channel, impl Fn(Request<()>) -> Result, Status> + Clone, >, >, Box, > { // Create JWT token with trader permissions let config = TestAuthConfig::trader() .with_user_id("test_trader_001") .with_roles(vec!["trader".to_string()]) .with_permissions(vec![ "api.access".to_string(), "trading.submit".to_string(), "trading.view".to_string(), ]); let token = create_test_jwt(config.clone())?; let user_id = config.user_id.clone(); let roles_str = config.roles.join(","); // Connect to Trading Service directly let channel = Channel::from_static("http://localhost:50052") .connect() .await?; // Create interceptor that injects JWT token and user context let interceptor = move |mut req: Request<()>| -> Result, Status> { // JWT token in authorization header let token_value = format!("Bearer {}", token); let metadata_value = MetadataValue::try_from(token_value) .map_err(|_| Status::internal("Failed to create metadata value"))?; req.metadata_mut().insert("authorization", metadata_value); // User context in metadata headers let user_id_value = MetadataValue::try_from(user_id.clone()) .map_err(|_| Status::internal("Failed to create user_id metadata"))?; req.metadata_mut().insert("x-user-id", user_id_value); let role_value = MetadataValue::try_from(roles_str.clone()) .map_err(|_| Status::internal("Failed to create role metadata"))?; req.metadata_mut().insert("x-user-role", role_value); Ok(req) }; Ok(TradingServiceClient::with_interceptor(channel, interceptor)) } // ==================== REGIME STATE TESTS ==================== #[tokio::test] #[ignore = "Requires running Trading Service"] async fn test_get_regime_state_es_fut() { // Test GetRegimeState for ES.FUT let mut client = create_client() .await .expect("Failed to connect to Trading Service"); let request = Request::new(GetRegimeStateRequest { symbol: "ES.FUT".to_string(), }); let response = client .get_regime_state(request) .await .expect("GetRegimeState RPC failed"); let regime_state = response.into_inner(); // Validate response structure assert_eq!(regime_state.symbol, "ES.FUT"); assert!(!regime_state.current_regime.is_empty()); assert!(["NORMAL", "TRENDING", "RANGING", "VOLATILE", "CRISIS"] .contains(®ime_state.current_regime.to_uppercase().as_str())); assert!(regime_state.confidence >= 0.0 && regime_state.confidence <= 1.0); assert!(regime_state.updated_at > 0); assert!(regime_state.stability >= 0.0 && regime_state.stability <= 1.0); assert!(regime_state.entropy >= 0.0 && regime_state.entropy <= 1.0); println!( "✅ GetRegimeState ES.FUT: regime={}, confidence={:.2}, ADX={:.2}, stability={:.2}", regime_state.current_regime, regime_state.confidence, regime_state.adx, regime_state.stability ); } #[tokio::test] #[ignore = "Requires running Trading Service"] async fn test_get_regime_state_nq_fut() { // Test GetRegimeState for NQ.FUT let mut client = create_client() .await .expect("Failed to connect to Trading Service"); let request = Request::new(GetRegimeStateRequest { symbol: "NQ.FUT".to_string(), }); let response = client .get_regime_state(request) .await .expect("GetRegimeState RPC failed"); let regime_state = response.into_inner(); assert_eq!(regime_state.symbol, "NQ.FUT"); assert!(!regime_state.current_regime.is_empty()); assert!(regime_state.confidence >= 0.0 && regime_state.confidence <= 1.0); println!( "✅ GetRegimeState NQ.FUT: regime={}, confidence={:.2}", regime_state.current_regime, regime_state.confidence ); } #[tokio::test] #[ignore = "Requires running Trading Service"] async fn test_get_regime_state_invalid_symbol() { // Test GetRegimeState with invalid symbol (should return error or default state) let mut client = create_client() .await .expect("Failed to connect to Trading Service"); let request = Request::new(GetRegimeStateRequest { symbol: "INVALID.SYM".to_string(), }); let result = client.get_regime_state(request).await; // Either error or default state with low confidence match result { Ok(response) => { let state = response.into_inner(); println!( "⚠️ Invalid symbol returned default state: regime={}, confidence={:.2}", state.current_regime, state.confidence ); assert!(state.confidence < 0.5); // Low confidence for unknown symbols }, Err(e) => { println!("✅ Invalid symbol correctly rejected: {:?}", e); }, } } // ==================== REGIME TRANSITIONS TESTS ==================== #[tokio::test] #[ignore = "Requires running Trading Service"] async fn test_get_regime_transitions_es_fut() { // Test GetRegimeTransitions for ES.FUT with limit let mut client = create_client() .await .expect("Failed to connect to Trading Service"); let request = Request::new(GetRegimeTransitionsRequest { symbol: "ES.FUT".to_string(), limit: 10, }); let response = client .get_regime_transitions(request) .await .expect("GetRegimeTransitions RPC failed"); let transitions = response.into_inner().transitions; // Validate response assert!( !transitions.is_empty(), "No transitions returned for ES.FUT" ); assert!( transitions.len() <= 10, "Returned more than requested limit" ); // Validate first transition let first = &transitions[0]; assert!(!first.from_regime.is_empty()); assert!(!first.to_regime.is_empty()); assert!(first.transition_probability >= 0.0 && first.transition_probability <= 1.0); assert!(first.timestamp > 0); assert!(first.duration_bars >= 0); println!("✅ GetRegimeTransitions ES.FUT: {} transitions, latest: {} → {} (probability={:.2}, duration={} bars)", transitions.len(), first.from_regime, first.to_regime, first.transition_probability, first.duration_bars ); } #[tokio::test] #[ignore = "Requires running Trading Service"] async fn test_get_regime_transitions_large_limit() { // Test GetRegimeTransitions with large limit let mut client = create_client() .await .expect("Failed to connect to Trading Service"); let request = Request::new(GetRegimeTransitionsRequest { symbol: "ES.FUT".to_string(), limit: 100, }); let response = client .get_regime_transitions(request) .await .expect("GetRegimeTransitions RPC failed"); let transitions = response.into_inner().transitions; assert!( !transitions.is_empty(), "No transitions returned for large limit" ); assert!( transitions.len() <= 100, "Returned more than requested limit" ); // Validate transitions are sorted by timestamp (descending) for i in 1..transitions.len() { assert!( transitions[i - 1].timestamp >= transitions[i].timestamp, "Transitions not sorted by timestamp descending" ); } println!( "✅ GetRegimeTransitions with limit=100: {} transitions returned", transitions.len() ); } #[tokio::test] #[ignore = "Requires running Trading Service"] async fn test_get_regime_transitions_multiple_symbols() { // Test GetRegimeTransitions for multiple symbols let symbols = vec!["ES.FUT", "NQ.FUT", "CL.FUT"]; let mut client = create_client() .await .expect("Failed to connect to Trading Service"); for symbol in symbols { let request = Request::new(GetRegimeTransitionsRequest { symbol: symbol.to_string(), limit: 5, }); let response = client .get_regime_transitions(request) .await .expect(&format!("GetRegimeTransitions failed for {}", symbol)); let transitions = response.into_inner().transitions; println!("✅ {}: {} transitions", symbol, transitions.len()); // Validate transition structure for transition in &transitions { assert!(!transition.from_regime.is_empty()); assert!(!transition.to_regime.is_empty()); } } } // ==================== PERFORMANCE TESTS ==================== #[tokio::test] #[ignore = "Requires running Trading Service"] async fn test_regime_state_performance() { // Test GetRegimeState performance (should be <10ms) let mut client = create_client() .await .expect("Failed to connect to Trading Service"); let mut latencies = Vec::new(); for _ in 0..100 { let request = Request::new(GetRegimeStateRequest { symbol: "ES.FUT".to_string(), }); let start = std::time::Instant::now(); let _response = client .get_regime_state(request) .await .expect("GetRegimeState RPC failed"); let latency = start.elapsed(); latencies.push(latency); } // Calculate statistics let avg_latency: std::time::Duration = latencies.iter().sum::() / latencies.len() as u32; let mut sorted = latencies.clone(); sorted.sort(); let p50 = sorted[sorted.len() / 2]; let p99 = sorted[sorted.len() * 99 / 100]; println!("✅ GetRegimeState Performance (100 requests):"); println!(" Average: {:?}", avg_latency); println!(" P50: {:?}", p50); println!(" P99: {:?}", p99); // Performance targets: P99 < 10ms assert!( p99 < std::time::Duration::from_millis(10), "P99 latency too high: {:?}", p99 ); } #[tokio::test] #[ignore = "Requires running Trading Service"] async fn test_regime_transitions_performance() { // Test GetRegimeTransitions performance (should be <50ms for 100 records) let mut client = create_client() .await .expect("Failed to connect to Trading Service"); let mut latencies = Vec::new(); for _ in 0..50 { let request = Request::new(GetRegimeTransitionsRequest { symbol: "ES.FUT".to_string(), limit: 100, }); let start = std::time::Instant::now(); let _response = client .get_regime_transitions(request) .await .expect("GetRegimeTransitions RPC failed"); let latency = start.elapsed(); latencies.push(latency); } // Calculate statistics let avg_latency: std::time::Duration = latencies.iter().sum::() / latencies.len() as u32; let mut sorted = latencies.clone(); sorted.sort(); let p50 = sorted[sorted.len() / 2]; let p99 = sorted[sorted.len() * 99 / 100]; println!("✅ GetRegimeTransitions Performance (50 requests, limit=100):"); println!(" Average: {:?}", avg_latency); println!(" P50: {:?}", p50); println!(" P99: {:?}", p99); // Performance targets: P99 < 50ms assert!( p99 < std::time::Duration::from_millis(50), "P99 latency too high: {:?}", p99 ); } // ==================== CONCURRENT ACCESS TESTS ==================== #[tokio::test] #[ignore = "Requires running Trading Service"] async fn test_concurrent_regime_state_requests() { // Test concurrent GetRegimeState requests let mut handles = vec![]; for i in 0..10 { let handle = tokio::spawn(async move { let mut client = create_client() .await .expect("Failed to connect to Trading Service"); let request = Request::new(GetRegimeStateRequest { symbol: "ES.FUT".to_string(), }); let response = client .get_regime_state(request) .await .expect(&format!("GetRegimeState failed for request {}", i)); response.into_inner() }); handles.push(handle); } // Wait for all requests let results: Vec<_> = futures::future::join_all(handles) .await .into_iter() .map(|r| r.expect("Task panicked")) .collect(); // All should succeed assert_eq!(results.len(), 10); // All should have consistent regime (within a few seconds) let regimes: Vec<_> = results.iter().map(|r| r.current_regime.clone()).collect(); println!("✅ Concurrent requests: regimes={:?}", regimes); }