Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
199 lines
6.5 KiB
Rust
199 lines
6.5 KiB
Rust
//! 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<Channel>,
|
|
}
|
|
|
|
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., <https://localhost:50053>).
|
|
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");
|
|
}
|
|
}
|