//! Backtesting service gRPC client //! //! Provides a gRPC client for communicating with the backtesting service, //! including strategy testing, performance analysis, and historical simulation. use anyhow::{Context, Result as AnyhowResult}; use serde::{Deserialize, Serialize}; use tonic::transport::Channel; /// Configuration for the backtesting service gRPC client /// /// Contains connection parameters and client behavior settings for /// communicating with the backtesting service. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BacktestingClientConfig { /// gRPC endpoint URL for the backtesting service (MUST be https://) pub endpoint: String, /// Request timeout in milliseconds pub timeout_ms: u64, } impl Default for BacktestingClientConfig { /// Create default backtesting client configuration /// /// Returns configuration with API Gateway HTTPS endpoint and 60-second timeout /// (longer than trading client due to potentially long-running backtests). /// /// # Security /// /// Defaults to HTTPS (not HTTP) to enforce encrypted connections. /// /// # Wave 71 Update /// /// Endpoint changed to API Gateway (port 50050) instead of direct backtesting service. /// /// All requests now route through the API Gateway for centralized authentication. fn default() -> Self { Self { endpoint: "https://localhost:50050".to_owned(), // API Gateway endpoint timeout_ms: 60_000, } } } /// gRPC client for the backtesting service /// /// Manages connection to the backtesting service and provides methods for /// running strategy backtests, retrieving performance metrics, and managing /// historical simulations. #[derive(Debug)] pub struct BacktestingClient { /// Client configuration config: BacktestingClientConfig, /// Active gRPC channel (None if disconnected) channel: Option, } impl BacktestingClient { /// Create a new backtesting client with the specified configuration /// /// # Arguments /// * `config` - Client configuration including endpoint and timeout settings /// /// # Returns /// /// New `BacktestingClient` instance (not yet connected) pub const fn new(config: BacktestingClientConfig) -> Self { Self { config, channel: None, } } /// Validate URL scheme is HTTPS (rejects HTTP) /// /// # Security /// /// This enforces fail-closed behavior - only HTTPS connections are allowed. /// /// Insecure HTTP connections are rejected with a clear error message. fn validate_endpoint_security(endpoint: &str) -> AnyhowResult<()> { if endpoint.starts_with("http://") { anyhow::bail!( "SECURITY ERROR: Insecure HTTP endpoint rejected: {}. TLS (https://) is required for all gRPC connections to protect sensitive backtesting data.", endpoint ); } if !endpoint.starts_with("https://") { anyhow::bail!( "Invalid endpoint scheme in URL: {}. Only HTTPS is supported (example: https://backtesting-service:50053).", endpoint ); } Ok(()) } /// Establish connection to the backtesting service /// /// Creates a gRPC channel to the configured backtesting service endpoint. /// /// Must be called before making any service requests. /// /// # Returns /// `Result<(), anyhow::Error>` - Ok if connection successful /// /// # Errors /// /// Returns error if: /// - Endpoint uses insecure HTTP scheme (security validation failure) /// /// - Unable to parse endpoint URL /// - Unable to connect to the service endpoint /// /// # Security /// /// This method enforces TLS by rejecting any HTTP endpoints. /// /// Use HTTPS endpoints only (e.g., ). pub async fn connect(&mut self) -> AnyhowResult<()> { // SECURITY: Validate endpoint uses HTTPS before attempting connection Self::validate_endpoint_security(&self.config.endpoint) .context("Backtesting client endpoint security validation failed")?; // Parse endpoint with proper error handling (no unwrap/panic) let channel = Channel::from_shared(self.config.endpoint.clone()) .context("Failed to parse backtesting service endpoint URL - check URL format")? .connect() .await .context("Failed to establish connection to backtesting service - check network and TLS configuration")?; self.channel = Some(channel); tracing::info!( "\u{2705} Backtesting client connected securely via TLS to {}", self.config.endpoint ); Ok(()) } /// Check if the client is currently connected to the backtesting service /// /// # Returns /// `true` if connected, `false` if disconnected pub const fn is_connected(&self) -> bool { self.channel.is_some() } /// Shutdown the client and close the connection /// /// Cleanly closes the gRPC channel and releases associated resources. /// /// The client can be reconnected after shutdown by calling `connect()`. pub async fn shutdown(&mut self) { if self.channel.is_some() { tracing::info!( "Shutting down backtesting client connection to {}", self.config.endpoint ); } self.channel = None; } } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_uses_https() { let config = BacktestingClientConfig::default(); assert!( config.endpoint.starts_with("https://"), "Default config must use HTTPS" ); } #[test] fn test_http_validation_rejects_insecure() { let result = BacktestingClient::validate_endpoint_security("http://localhost:50053"); assert!(result.is_err(), "HTTP endpoints should be rejected"); assert!(result.unwrap_err().to_string().contains("Insecure HTTP")); } #[test] fn test_https_validation_accepts_secure() { let result = BacktestingClient::validate_endpoint_security("https://localhost:50053"); assert!(result.is_ok(), "HTTPS endpoints should be accepted"); } #[test] fn test_invalid_scheme_rejected() { let result = BacktestingClient::validate_endpoint_security("grpc://localhost:50053"); assert!(result.is_err(), "Non-HTTPS schemes should be rejected"); } }