Reduce log noise for non-critical operational paths: connection retries, expected fallbacks, graceful degradation, and optional feature absence. Keeps warn/error for genuine failures requiring attention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1274 lines
50 KiB
Rust
1274 lines
50 KiB
Rust
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
|
|
|
//! Trading Service - Main Entry Point
|
|
//!
|
|
//! This is the main entry point for the Foxhunt HFT Trading Service.
|
|
//! It initializes all components including the PostgreSQL ConfigLoader
|
|
//! for direct configuration management with hot-reload support.
|
|
|
|
use anyhow::{Context, Result};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::signal;
|
|
use tonic::transport::Server;
|
|
use tracing::{debug, error, info, warn};
|
|
|
|
use trading_service::auth_interceptor::{AuthConfig, TonicAuthInterceptor};
|
|
|
|
// Use central configuration and shared libraries with canonical imports
|
|
use common::DatabasePool;
|
|
use config::manager::ConfigManager;
|
|
use config::DatabaseConfig;
|
|
|
|
// Import repository dependencies with explicit imports
|
|
use trading_service::repositories::{
|
|
ConfigRepository, MarketDataRepository, RiskRepository, TradingRepository,
|
|
};
|
|
use trading_service::repository_impls::{
|
|
PostgresConfigRepository, PostgresMarketDataRepository, PostgresRiskRepository,
|
|
PostgresTradingRepository,
|
|
};
|
|
|
|
use trading_service::compliance_service::{ComplianceConfig, ComplianceService};
|
|
use trading_service::event_persistence::EventPersistence;
|
|
use trading_service::kill_switch_integration::TradingServiceKillSwitch;
|
|
use trading_service::rate_limiter::{RateLimitConfig, RateLimiter};
|
|
use trading_service::services::ml::MLService;
|
|
use trading_service::services::ml_fallback_manager::MLFallbackManager;
|
|
use trading_service::services::ml_performance_monitor::MLPerformanceMonitor;
|
|
use trading_service::services::monitoring::MonitoringServiceImpl;
|
|
use trading_service::services::risk::RiskServiceImpl;
|
|
use trading_service::services::trading::TradingServiceImpl;
|
|
use trading_service::feedback_loop::{FeedbackLoop, FeedbackLoopConfig};
|
|
use trading_service::questdb_metrics::QuestDBMetricsProvider;
|
|
use trading_service::state::TradingServiceState;
|
|
|
|
use common::metrics::GrpcMetricsLayer;
|
|
|
|
/// Default configuration values
|
|
const DEFAULT_GRPC_PORT: u16 = 50052; // Trading Service port (API Gateway uses 50051)
|
|
const DEFAULT_HEALTH_PORT: u16 = 8081; // Health check port (separate from API Gateway 8080)
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
// Initialize observability (JSON logging + OpenTelemetry tracing via OTLP)
|
|
let otlp_endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
.unwrap_or_else(|_| "http://localhost:4317".to_string());
|
|
if let Err(e) = common::observability::init_observability(
|
|
"trading_service",
|
|
Some(&otlp_endpoint),
|
|
) {
|
|
eprintln!("Failed to initialize observability: {}", e);
|
|
}
|
|
|
|
info!("Starting Foxhunt Trading Service...");
|
|
|
|
// Create service config manually
|
|
let service_config = config::ServiceConfig {
|
|
name: "trading_service".to_string(),
|
|
environment: std::env::var("ENVIRONMENT").unwrap_or_else(|_| "production".to_string()),
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
settings: serde_json::json!({}),
|
|
};
|
|
let _config_manager = Arc::new(ConfigManager::new(service_config));
|
|
|
|
info!("Central ConfigManager initialized successfully");
|
|
|
|
// Get database URL from environment (DatabaseConfig::new() already handles this)
|
|
let mut database_config = DatabaseConfig::new();
|
|
// DatabaseConfig::new() already sets url from DATABASE_URL env var
|
|
database_config.max_connections = 20;
|
|
database_config.min_connections = 5;
|
|
|
|
// Initialize HFT-optimized database pool directly from config
|
|
let db_pool_wrapper = DatabasePool::new(database_config.into())
|
|
.await
|
|
.context("Failed to create HFT-optimized database pool")?;
|
|
|
|
let db_pool = db_pool_wrapper.pool().clone();
|
|
|
|
info!("Database connection pool initialized");
|
|
|
|
// Initialize repositories with dependency injection
|
|
let trading_repository: Arc<dyn TradingRepository> =
|
|
Arc::new(PostgresTradingRepository::new(db_pool.clone()));
|
|
let market_data_repository: Arc<dyn MarketDataRepository> =
|
|
Arc::new(PostgresMarketDataRepository::new(db_pool.clone()));
|
|
let risk_repository: Arc<dyn RiskRepository> =
|
|
Arc::new(PostgresRiskRepository::new(db_pool.clone()));
|
|
let config_repository_impl = Arc::new(PostgresConfigRepository::new(db_pool.clone()));
|
|
|
|
info!("Repository layer initialized with dependency injection");
|
|
|
|
// Initialize event persistence for compliance and audit trail
|
|
let process_id = std::process::id();
|
|
let event_persistence = Arc::new(EventPersistence::new(
|
|
db_pool.clone(),
|
|
"trading_service".to_string(),
|
|
process_id,
|
|
));
|
|
info!("Event persistence initialized for compliance audit trail");
|
|
|
|
// Initialize default configurations if they don't exist
|
|
initialize_default_configs(&config_repository_impl).await?;
|
|
|
|
// Initialize kill switch system for regulatory compliance
|
|
let redis_url =
|
|
std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
|
let kill_switch_system = Arc::new(
|
|
TradingServiceKillSwitch::new(redis_url)
|
|
.await
|
|
.context("Failed to initialize kill switch system")?,
|
|
);
|
|
info!("Kill switch system initialized for regulatory compliance");
|
|
|
|
// Start kill switch monitoring (Unix socket, signal handlers, etc.)
|
|
kill_switch_system
|
|
.start_monitoring()
|
|
.await
|
|
.context("Failed to start kill switch monitoring")?;
|
|
info!("Kill switch monitoring started - emergency shutdown ready");
|
|
|
|
// Start configuration hot-reload monitoring
|
|
start_config_monitoring(Arc::clone(&config_repository_impl)).await?;
|
|
|
|
// The centralized config manager now handles all configuration persistence
|
|
// and provenance tracking internally
|
|
|
|
// Initialize authentication configuration using Tonic interceptor
|
|
let auth_config = initialize_auth_config().await;
|
|
let auth_interceptor = TonicAuthInterceptor::new(auth_config);
|
|
|
|
info!("✅ Authentication interceptor initialized with Tonic 0.14 compatibility");
|
|
|
|
// Initialize compliance service for SOX and MiFID II regulatory requirements
|
|
let compliance_config = ComplianceConfig {
|
|
enable_sox_audit: std::env::var("ENABLE_SOX_AUDIT")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(true), // Production default: enabled
|
|
enable_mifid_reporting: std::env::var("ENABLE_MIFID_REPORTING")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(true), // Production default: enabled
|
|
enable_position_monitoring: std::env::var("ENABLE_POSITION_MONITORING")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(true), // Production default: enabled
|
|
enable_best_execution_analysis: std::env::var("ENABLE_BEST_EXECUTION_ANALYSIS")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(true), // Production default: enabled
|
|
kill_switch_enabled: std::env::var("COMPLIANCE_KILL_SWITCH_ENABLED")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(true), // Production default: enabled
|
|
max_position_utilization: std::env::var("MAX_POSITION_UTILIZATION")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(0.95), // 95% default
|
|
critical_risk_threshold: std::env::var("CRITICAL_RISK_THRESHOLD")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(0.8), // 80% default
|
|
};
|
|
|
|
let compliance_service = Arc::new(ComplianceService::new(db_pool.clone(), compliance_config));
|
|
|
|
// Verify compliance service health
|
|
if let Err(e) = compliance_service.health_check().await {
|
|
error!("Compliance service health check failed: {}", e);
|
|
return Err(anyhow::anyhow!("Failed to initialize compliance service"));
|
|
}
|
|
|
|
info!("Compliance service initialized with SOX and MiFID II audit trails");
|
|
|
|
// Initialize advanced rate limiter with production defaults
|
|
let rate_limit_config = RateLimitConfig {
|
|
user_requests_per_minute: std::env::var("USER_REQUESTS_PER_MINUTE")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(1000), // 1000 requests per minute per user
|
|
user_burst_capacity: std::env::var("USER_BURST_CAPACITY")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(100), // Allow bursts up to 100 requests
|
|
ip_requests_per_minute: std::env::var("IP_REQUESTS_PER_MINUTE")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(2000), // 2000 requests per minute per IP
|
|
ip_burst_capacity: std::env::var("IP_BURST_CAPACITY")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(200), // Allow bursts up to 200 requests
|
|
global_requests_per_minute: std::env::var("GLOBAL_REQUESTS_PER_MINUTE")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(50000), // 50k requests per minute globally
|
|
global_burst_capacity: std::env::var("GLOBAL_BURST_CAPACITY")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(5000), // Allow bursts up to 5k requests
|
|
auth_failures_per_minute: std::env::var("AUTH_FAILURES_PER_MINUTE")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(5), // Max 5 auth failures per minute
|
|
auth_failure_penalty_minutes: std::env::var("AUTH_FAILURE_PENALTY_MINUTES")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(15), // 15 minute penalty
|
|
orders_per_minute: std::env::var("ORDERS_PER_MINUTE")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(600), // 600 orders per minute (10/second)
|
|
order_burst_capacity: std::env::var("ORDER_BURST_CAPACITY")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(60), // Allow bursts up to 60 orders
|
|
cleanup_interval_minutes: std::env::var("RATE_LIMIT_CLEANUP_INTERVAL")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(60), // Cleanup every hour
|
|
};
|
|
|
|
let rate_limiter = Arc::new(RateLimiter::new(rate_limit_config));
|
|
|
|
// Start background cleanup task for rate limiter
|
|
Arc::clone(&rate_limiter).start_cleanup_task().await;
|
|
|
|
info!("Advanced rate limiter initialized with per-user, per-IP, and global limits");
|
|
info!("Authentication system initialized with mTLS and JWT support");
|
|
|
|
// Initialize ensemble coordinator with real ML inference adapters
|
|
let ensemble_coordinator = {
|
|
use trading_service::adapter_bridge::InferenceAdapterBridge;
|
|
use trading_service::ensemble_coordinator::EnsembleCoordinator;
|
|
|
|
let coordinator = EnsembleCoordinator::new();
|
|
|
|
// Create real candle-backed inference adapters and register them
|
|
// via the InferenceAdapterBridge (ModelInferenceAdapter -> MLModel).
|
|
// Models are initialised with random weights; production checkpoints
|
|
// are hot-loaded later via the model registry's shadow-buffer swap.
|
|
|
|
// DQN adapter (51-dim input, 45 factored actions)
|
|
match ml::ensemble::adapters::DqnInferenceAdapter::new(ml::dqn::dqn::DQNConfig {
|
|
state_dim: 51,
|
|
num_actions: 45,
|
|
hidden_dims: vec![64, 64],
|
|
..Default::default()
|
|
}) {
|
|
Ok(adapter) => {
|
|
let bridge = Arc::new(InferenceAdapterBridge::new(
|
|
Box::new(adapter),
|
|
"DQN".to_string(),
|
|
ml::ModelType::DQN,
|
|
));
|
|
if let Err(e) = coordinator
|
|
.register_loaded_model("DQN".to_string(), bridge, 0.10)
|
|
.await
|
|
{
|
|
error!("Failed to register DQN adapter: {}", e);
|
|
} else {
|
|
info!("Registered DQN inference adapter (51-dim, candle)");
|
|
}
|
|
}
|
|
Err(e) => error!("Failed to create DQN inference adapter: {}", e),
|
|
}
|
|
|
|
// PPO adapter (51-dim input, 3 actions: buy/sell/hold)
|
|
match ml::ensemble::adapters::PpoInferenceAdapter::new(ml::ppo::ppo::PPOConfig {
|
|
state_dim: 51,
|
|
num_actions: 3,
|
|
policy_hidden_dims: vec![64, 64],
|
|
value_hidden_dims: vec![64, 64],
|
|
..Default::default()
|
|
}) {
|
|
Ok(adapter) => {
|
|
let bridge = Arc::new(InferenceAdapterBridge::new(
|
|
Box::new(adapter),
|
|
"PPO".to_string(),
|
|
ml::ModelType::PPO,
|
|
));
|
|
if let Err(e) = coordinator
|
|
.register_loaded_model("PPO".to_string(), bridge, 0.10)
|
|
.await
|
|
{
|
|
error!("Failed to register PPO adapter: {}", e);
|
|
} else {
|
|
info!("Registered PPO inference adapter (51-dim, candle)");
|
|
}
|
|
}
|
|
Err(e) => error!("Failed to create PPO inference adapter: {}", e),
|
|
}
|
|
|
|
// TFT adapter (51-dim split: 3 static + 8 known + 40 unknown)
|
|
match ml::ensemble::adapters::TftInferenceAdapter::new(
|
|
ml::tft::TFTConfig {
|
|
input_dim: 51,
|
|
hidden_dim: 32,
|
|
num_heads: 4,
|
|
num_layers: 1,
|
|
prediction_horizon: 1,
|
|
sequence_length: 10,
|
|
num_quantiles: 3,
|
|
num_static_features: 3,
|
|
num_known_features: 8,
|
|
num_unknown_features: 40,
|
|
..Default::default()
|
|
},
|
|
10, // sequence_length for buffer
|
|
) {
|
|
Ok(adapter) => {
|
|
let bridge = Arc::new(InferenceAdapterBridge::new(
|
|
Box::new(adapter),
|
|
"TFT".to_string(),
|
|
ml::ModelType::TFT,
|
|
));
|
|
if let Err(e) = coordinator
|
|
.register_loaded_model("TFT".to_string(), bridge, 0.10)
|
|
.await
|
|
{
|
|
error!("Failed to register TFT adapter: {}", e);
|
|
} else {
|
|
info!("Registered TFT inference adapter (51-dim, candle)");
|
|
}
|
|
}
|
|
Err(e) => error!("Failed to create TFT inference adapter: {}", e),
|
|
}
|
|
|
|
// Mamba2 adapter (d_model=64, zero-pads from 51-dim features)
|
|
match ml::ensemble::adapters::Mamba2InferenceAdapter::new(
|
|
ml::mamba::Mamba2Config {
|
|
d_model: 64,
|
|
d_state: 16,
|
|
d_head: 16,
|
|
num_heads: 2,
|
|
expand: 1,
|
|
num_layers: 1,
|
|
..Default::default()
|
|
},
|
|
10, // sequence_length for buffer
|
|
) {
|
|
Ok(adapter) => {
|
|
let bridge = Arc::new(InferenceAdapterBridge::new(
|
|
Box::new(adapter),
|
|
"MAMBA2".to_string(),
|
|
ml::ModelType::MAMBA,
|
|
));
|
|
if let Err(e) = coordinator
|
|
.register_loaded_model("MAMBA2".to_string(), bridge, 0.10)
|
|
.await
|
|
{
|
|
error!("Failed to register MAMBA2 adapter: {}", e);
|
|
} else {
|
|
info!("Registered MAMBA2 inference adapter (64-dim SSM, candle)");
|
|
}
|
|
}
|
|
Err(e) => error!("Failed to create MAMBA2 inference adapter: {}", e),
|
|
}
|
|
|
|
// Liquid CfC adapter (51-dim input, 3 output: buy/hold/sell)
|
|
match ml::ensemble::adapters::LiquidInferenceAdapter::new(
|
|
ml::liquid::candle_cfc::CfCTrainConfig {
|
|
input_size: 51,
|
|
hidden_size: 32,
|
|
output_size: 3,
|
|
backbone_hidden_sizes: vec![32],
|
|
seq_len: 1,
|
|
device: ml::liquid::candle_cfc::DeviceConfig::Cpu,
|
|
..ml::liquid::candle_cfc::CfCTrainConfig::default()
|
|
},
|
|
) {
|
|
Ok(adapter) => {
|
|
let bridge = Arc::new(InferenceAdapterBridge::new(
|
|
Box::new(adapter),
|
|
"Liquid-CfC".to_string(),
|
|
ml::ModelType::LNN,
|
|
));
|
|
if let Err(e) = coordinator
|
|
.register_loaded_model("Liquid-CfC".to_string(), bridge, 0.10)
|
|
.await
|
|
{
|
|
error!("Failed to register Liquid-CfC adapter: {}", e);
|
|
} else {
|
|
info!("Registered Liquid-CfC inference adapter (51-dim, candle ODE)");
|
|
}
|
|
}
|
|
Err(e) => error!("Failed to create Liquid-CfC inference adapter: {}", e),
|
|
}
|
|
|
|
// TGGN adapter (51-dim input, 2-layer candle projection)
|
|
match ml::ensemble::adapters::TggnInferenceAdapter::new(51, 64) {
|
|
Ok(adapter) => {
|
|
let bridge = Arc::new(InferenceAdapterBridge::new(
|
|
Box::new(adapter),
|
|
"TGGN".to_string(),
|
|
ml::ModelType::TGGN,
|
|
));
|
|
if let Err(e) = coordinator
|
|
.register_loaded_model("TGGN".to_string(), bridge, 0.10)
|
|
.await
|
|
{
|
|
error!("Failed to register TGGN adapter: {}", e);
|
|
} else {
|
|
info!("Registered TGGN inference adapter (51-dim, candle projection)");
|
|
}
|
|
}
|
|
Err(e) => error!("Failed to create TGGN inference adapter: {}", e),
|
|
}
|
|
|
|
// TLOB adapter (51-dim features, 3-layer MLP, seq_len=10)
|
|
match ml::ensemble::adapters::TlobInferenceAdapter::new(51, 64, 10) {
|
|
Ok(adapter) => {
|
|
let bridge = Arc::new(InferenceAdapterBridge::new(
|
|
Box::new(adapter),
|
|
"TLOB".to_string(),
|
|
ml::ModelType::TLOB,
|
|
));
|
|
if let Err(e) = coordinator
|
|
.register_loaded_model("TLOB".to_string(), bridge, 0.10)
|
|
.await
|
|
{
|
|
error!("Failed to register TLOB adapter: {}", e);
|
|
} else {
|
|
info!("Registered TLOB inference adapter (51-dim, seq=10, candle MLP)");
|
|
}
|
|
}
|
|
Err(e) => error!("Failed to create TLOB inference adapter: {}", e),
|
|
}
|
|
|
|
// KAN adapter (51-dim input, B-spline activations)
|
|
match ml::ensemble::adapters::KanInferenceAdapter::new(ml::kan::KANConfig {
|
|
layer_widths: vec![51, 32, 16, 1],
|
|
..Default::default()
|
|
}) {
|
|
Ok(adapter) => {
|
|
let bridge = Arc::new(InferenceAdapterBridge::new(
|
|
Box::new(adapter),
|
|
"KAN".to_string(),
|
|
ml::ModelType::KAN,
|
|
));
|
|
if let Err(e) = coordinator
|
|
.register_loaded_model("KAN".to_string(), bridge, 0.10)
|
|
.await
|
|
{
|
|
error!("Failed to register KAN adapter: {}", e);
|
|
} else {
|
|
info!("Registered KAN inference adapter (51-dim, B-spline, candle)");
|
|
}
|
|
}
|
|
Err(e) => error!("Failed to create KAN inference adapter: {}", e),
|
|
}
|
|
|
|
// xLSTM adapter (51-dim input, sLSTM+mLSTM, seq_len=10)
|
|
match ml::ensemble::adapters::XlstmInferenceAdapter::new(
|
|
ml::xlstm::XLSTMConfig {
|
|
input_dim: 51,
|
|
hidden_dim: 64,
|
|
num_blocks: 2,
|
|
num_heads: 2,
|
|
..Default::default()
|
|
},
|
|
10, // sequence_length for buffer
|
|
) {
|
|
Ok(adapter) => {
|
|
let bridge = Arc::new(InferenceAdapterBridge::new(
|
|
Box::new(adapter),
|
|
"xLSTM".to_string(),
|
|
ml::ModelType::XLSTM,
|
|
));
|
|
if let Err(e) = coordinator
|
|
.register_loaded_model("xLSTM".to_string(), bridge, 0.10)
|
|
.await
|
|
{
|
|
error!("Failed to register xLSTM adapter: {}", e);
|
|
} else {
|
|
info!("Registered xLSTM inference adapter (51-dim, seq=10, candle)");
|
|
}
|
|
}
|
|
Err(e) => error!("Failed to create xLSTM inference adapter: {}", e),
|
|
}
|
|
|
|
// Diffusion adapter (51-dim features, DDPM denoiser, t=1 inference)
|
|
match ml::ensemble::adapters::DiffusionInferenceAdapter::new(ml::diffusion::DiffusionConfig {
|
|
seq_len: 1,
|
|
feature_dim: 51,
|
|
hidden_dim: 64,
|
|
num_layers: 2,
|
|
time_embed_dim: 32,
|
|
num_timesteps: 100,
|
|
sampling_steps: 5,
|
|
..Default::default()
|
|
}) {
|
|
Ok(adapter) => {
|
|
let bridge = Arc::new(InferenceAdapterBridge::new(
|
|
Box::new(adapter),
|
|
"Diffusion".to_string(),
|
|
ml::ModelType::Diffusion,
|
|
));
|
|
if let Err(e) = coordinator
|
|
.register_loaded_model("Diffusion".to_string(), bridge, 0.10)
|
|
.await
|
|
{
|
|
error!("Failed to register Diffusion adapter: {}", e);
|
|
} else {
|
|
info!("Registered Diffusion inference adapter (51-dim, DDPM, candle)");
|
|
}
|
|
}
|
|
Err(e) => error!("Failed to create Diffusion inference adapter: {}", e),
|
|
}
|
|
|
|
let model_count = coordinator.model_count().await;
|
|
info!(
|
|
"Ensemble coordinator initialized with {} real inference adapters \
|
|
(all 10 models at 0.10 weight each)",
|
|
model_count
|
|
);
|
|
|
|
Arc::new(coordinator)
|
|
};
|
|
|
|
// Initialize service state with repository dependency injection
|
|
let service_state = TradingServiceState::new_with_repositories(
|
|
trading_repository,
|
|
market_data_repository,
|
|
risk_repository,
|
|
Arc::clone(&config_repository_impl),
|
|
db_pool.clone(),
|
|
Arc::clone(&event_persistence),
|
|
Some(Arc::clone(&kill_switch_system)),
|
|
Some(ensemble_coordinator),
|
|
)
|
|
.await?;
|
|
info!("Trading service state initialized with repository dependency injection and ensemble coordinator");
|
|
|
|
// Initialize ML performance monitoring and fallback management
|
|
let ml_performance_monitor = Arc::new(MLPerformanceMonitor::new());
|
|
let ml_fallback_manager = Arc::new(MLFallbackManager::new());
|
|
|
|
info!("ML performance monitoring and fallback management initialized");
|
|
|
|
// Initialize paper trading executor for prediction consumption
|
|
use trading_service::paper_trading_executor::{PaperTradingConfig, PaperTradingExecutor};
|
|
|
|
let paper_trading_config = PaperTradingConfig {
|
|
enabled: std::env::var("PAPER_TRADING_ENABLED")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(true), // Default: enabled
|
|
min_confidence: std::env::var("PAPER_TRADING_MIN_CONFIDENCE")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(0.60), // 60% minimum confidence
|
|
poll_interval_ms: std::env::var("PAPER_TRADING_POLL_INTERVAL_MS")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(100), // 100ms polling
|
|
max_position_size: std::env::var("PAPER_TRADING_MAX_POSITION_SIZE")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(10_000.0), // $10,000 max position
|
|
allowed_symbols: std::env::var("PAPER_TRADING_ALLOWED_SYMBOLS")
|
|
.ok()
|
|
.map(|s| s.split(',').map(|sym| sym.trim().to_string()).collect())
|
|
.unwrap_or_else(|| {
|
|
vec![
|
|
"ES.FUT".to_string(),
|
|
"NQ.FUT".to_string(),
|
|
"ZN.FUT".to_string(),
|
|
"6E.FUT".to_string(),
|
|
]
|
|
}),
|
|
account_id: std::env::var("PAPER_TRADING_ACCOUNT_ID")
|
|
.unwrap_or_else(|_| "paper_trading_001".to_string()),
|
|
initial_capital: std::env::var("PAPER_TRADING_INITIAL_CAPITAL")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(100_000.0), // $100,000 initial capital
|
|
batch_size: std::env::var("PAPER_TRADING_BATCH_SIZE")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(100), // Process 100 predictions per batch
|
|
};
|
|
|
|
let paper_trading_executor = Arc::new(PaperTradingExecutor::new(
|
|
db_pool.clone(),
|
|
paper_trading_config.clone(),
|
|
)?);
|
|
|
|
info!(
|
|
"Paper trading executor initialized: enabled={}, min_confidence={:.1}%, poll_interval={}ms",
|
|
paper_trading_config.enabled,
|
|
paper_trading_config.min_confidence * 100.0,
|
|
paper_trading_config.poll_interval_ms
|
|
);
|
|
|
|
// Spawn paper trading executor background task
|
|
let executor_clone = Arc::clone(&paper_trading_executor);
|
|
tokio::spawn(async move {
|
|
info!("Paper trading executor background task starting...");
|
|
if let Err(e) = executor_clone.start().await {
|
|
error!("Paper trading executor failed: {}", e);
|
|
}
|
|
});
|
|
|
|
// Spawn background ML prediction generation loop (Wave 14.2 Agent 7)
|
|
// This generates predictions every 60 seconds and saves to ensemble_predictions table
|
|
use trading_service::prediction_generation_loop::{
|
|
PredictionGenerationLoop, PredictionLoopConfig,
|
|
};
|
|
|
|
// Shutdown senders for background loops -- dropped at end of main to signal shutdown.
|
|
let mut prediction_shutdown_handles: Vec<tokio::sync::broadcast::Sender<()>> = Vec::new();
|
|
|
|
if let Some(ensemble_coordinator) = service_state.ensemble_coordinator() {
|
|
let prediction_config = PredictionLoopConfig::from_env();
|
|
let prediction_loop = PredictionGenerationLoop::new(
|
|
Arc::clone(ensemble_coordinator),
|
|
db_pool.clone(),
|
|
prediction_config.clone(),
|
|
);
|
|
|
|
// Create shutdown channel for prediction loop
|
|
let (prediction_shutdown_tx, prediction_shutdown_rx) = tokio::sync::broadcast::channel(1);
|
|
|
|
// Spawn prediction generation loop
|
|
tokio::spawn(async move {
|
|
info!(
|
|
"ML prediction generation loop starting: symbols={:?}, interval={:?}",
|
|
prediction_config.symbols, prediction_config.prediction_interval
|
|
);
|
|
if let Err(e) = prediction_loop.run(prediction_shutdown_rx).await {
|
|
error!("Prediction generation loop failed: {}", e);
|
|
}
|
|
});
|
|
|
|
info!("✅ Background ML prediction generation loop initialized");
|
|
|
|
// Store shutdown sender so it is dropped during graceful shutdown,
|
|
// which closes the channel and signals the prediction loop to stop.
|
|
prediction_shutdown_handles.push(prediction_shutdown_tx);
|
|
} else {
|
|
info!("Ensemble coordinator not available - prediction generation loop disabled");
|
|
}
|
|
|
|
// Spawn background market data feed for ensemble feature extractor warmup.
|
|
// The ensemble coordinator's `update_market_data()` feeds per-symbol
|
|
// `ProductionFeatureExtractorAdapter`s that require ~51 bars before they can
|
|
// produce real 51-dim feature vectors. Without this feed the extractors stay
|
|
// cold and `fetch_features_for_symbol` returns zeros.
|
|
//
|
|
// In production this task would be replaced by a Databento or exchange feed;
|
|
// for now it generates synthetic OHLCV bars at 1-second intervals.
|
|
if let Some(ensemble_coordinator) = service_state.ensemble_coordinator() {
|
|
let coord = Arc::clone(ensemble_coordinator);
|
|
|
|
let warmup_symbols: Vec<String> = std::env::var("MARKET_FEED_SYMBOLS")
|
|
.ok()
|
|
.map(|s| s.split(',').map(|sym| sym.trim().to_string()).collect())
|
|
.unwrap_or_else(|| {
|
|
vec![
|
|
"ES.FUT".to_string(),
|
|
"NQ.FUT".to_string(),
|
|
"ZN.FUT".to_string(),
|
|
"6E.FUT".to_string(),
|
|
]
|
|
});
|
|
|
|
let feed_interval_ms: u64 = std::env::var("MARKET_FEED_INTERVAL_MS")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(1_000); // 1 second default
|
|
|
|
// Base prices per symbol (representative of real futures contracts)
|
|
let base_prices: Vec<f64> = warmup_symbols
|
|
.iter()
|
|
.map(|sym| match sym.as_str() {
|
|
"ES.FUT" => 4_500.0,
|
|
"NQ.FUT" => 15_800.0,
|
|
"ZN.FUT" => 110.5,
|
|
"6E.FUT" => 1.085,
|
|
_ => 100.0,
|
|
})
|
|
.collect();
|
|
|
|
tokio::spawn(async move {
|
|
use rand::Rng;
|
|
use rand::SeedableRng;
|
|
|
|
info!(
|
|
"Market data feed starting: symbols={:?}, interval={}ms, warmup_target={} bars",
|
|
warmup_symbols,
|
|
feed_interval_ms,
|
|
trading_service::ensemble_coordinator::FEATURE_WARMUP_BARS,
|
|
);
|
|
|
|
let mut interval =
|
|
tokio::time::interval(Duration::from_millis(feed_interval_ms));
|
|
let mut tick: u64 = 0;
|
|
let mut rng = rand::rngs::StdRng::from_entropy();
|
|
let mut prices: Vec<f64> = base_prices;
|
|
let warmup_target = trading_service::ensemble_coordinator::FEATURE_WARMUP_BARS as u64;
|
|
|
|
loop {
|
|
interval.tick().await;
|
|
let now = chrono::Utc::now();
|
|
|
|
for (idx, symbol) in warmup_symbols.iter().enumerate() {
|
|
let price = match prices.get(idx) {
|
|
Some(p) => *p,
|
|
None => continue,
|
|
};
|
|
|
|
// Random walk: +/- 0.02% per tick (realistic micro-volatility)
|
|
let pct_change: f64 = rng.gen_range(-0.0002..0.0002);
|
|
let new_price = price * (1.0 + pct_change);
|
|
let volume = rng.gen_range(500.0..5_000.0);
|
|
|
|
if let Some(slot) = prices.get_mut(idx) {
|
|
*slot = new_price;
|
|
}
|
|
|
|
if let Err(e) = coord
|
|
.update_market_data(symbol, new_price, volume, now)
|
|
.await
|
|
{
|
|
debug!(
|
|
"Market data feed: failed to update {} (tick {}): {}",
|
|
symbol, tick, e
|
|
);
|
|
}
|
|
}
|
|
|
|
tick += 1;
|
|
|
|
// Log warmup progress at key milestones
|
|
if tick == warmup_target {
|
|
info!(
|
|
"Market data feed: warmup complete ({} bars) -- \
|
|
feature extraction now producing real vectors for {:?}",
|
|
warmup_target, warmup_symbols,
|
|
);
|
|
} else if tick < warmup_target && tick % 10 == 0 {
|
|
info!(
|
|
"Market data feed: warmup progress {}/{} bars",
|
|
tick, warmup_target,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
info!(
|
|
"Background market data feed spawned for ensemble feature extractor warmup"
|
|
);
|
|
} else {
|
|
info!("Ensemble coordinator not available - market data feed disabled");
|
|
}
|
|
|
|
// Initialize autonomous feedback loop (weight + gate optimization with kill switch)
|
|
{
|
|
let feedback_config = FeedbackLoopConfig {
|
|
cycle_interval: Duration::from_secs(
|
|
std::env::var("FEEDBACK_LOOP_INTERVAL_SECS")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(24 * 3600),
|
|
),
|
|
kill_switch_threshold: std::env::var("FEEDBACK_KILL_SWITCH_SHARPE")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(-1.0),
|
|
..Default::default()
|
|
};
|
|
|
|
let feedback_loop = Arc::new(FeedbackLoop::new(feedback_config));
|
|
|
|
// Connect to QuestDB for real metrics (pg wire protocol on port 8812)
|
|
let questdb_url = std::env::var("QUESTDB_PG_URL")
|
|
.unwrap_or_else(|_| "postgresql://admin:quest@localhost:8812/qdb".to_string());
|
|
|
|
match QuestDBMetricsProvider::try_new(&questdb_url).await {
|
|
Some(provider) => {
|
|
if let Err(e) = provider.ensure_tables().await {
|
|
warn!("Failed to create QuestDB tables: {} — feedback loop will retry", e);
|
|
}
|
|
let metrics_provider: Arc<dyn trading_service::feedback_loop::MetricsProvider> =
|
|
Arc::new(provider);
|
|
let _feedback_handle = Arc::clone(&feedback_loop).spawn(metrics_provider);
|
|
info!("Autonomous feedback loop spawned with QuestDB metrics provider");
|
|
}
|
|
None => {
|
|
info!("QuestDB unavailable — feedback loop disabled until QUESTDB_PG_URL is reachable");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Subscribe to ML performance alerts
|
|
let monitor_clone = Arc::clone(&ml_performance_monitor);
|
|
tokio::spawn(async move {
|
|
use trading_service::ml_metrics;
|
|
let mut alert_receiver = monitor_clone.subscribe_alerts();
|
|
|
|
info!("ML performance alert handler started");
|
|
|
|
while let Ok(alert) = alert_receiver.recv().await {
|
|
// Log alert based on severity
|
|
match alert.severity {
|
|
trading_service::services::ml_performance_monitor::AlertSeverity::Emergency
|
|
| trading_service::services::ml_performance_monitor::AlertSeverity::Critical => {
|
|
error!("ML ALERT [{}]: {}", alert.model_id, alert.message);
|
|
},
|
|
trading_service::services::ml_performance_monitor::AlertSeverity::Warning => {
|
|
warn!("ML ALERT [{}]: {}", alert.model_id, alert.message);
|
|
},
|
|
trading_service::services::ml_performance_monitor::AlertSeverity::Info => {
|
|
info!("ML ALERT [{}]: {}", alert.model_id, alert.message);
|
|
},
|
|
}
|
|
|
|
// Update Prometheus alert counter
|
|
ml_metrics::ML_ALERTS_TOTAL
|
|
.with_label_values(&[
|
|
&alert.model_id,
|
|
&ml_metrics::alert_type_str(&alert.alert_type).to_string(),
|
|
&ml_metrics::alert_severity_str(&alert.severity).to_string(),
|
|
])
|
|
.inc();
|
|
}
|
|
|
|
info!("ML performance alert handler stopped");
|
|
});
|
|
|
|
// Create gRPC services with enhanced ML capabilities
|
|
// Services expect TradingServiceState directly, not Arc
|
|
let trading_service = TradingServiceImpl::new(Arc::new(service_state.clone()));
|
|
let risk_service = RiskServiceImpl::new(service_state.clone());
|
|
let ml_service = MLService::new(
|
|
service_state.clone(),
|
|
Arc::clone(&ml_performance_monitor),
|
|
Arc::clone(&ml_fallback_manager),
|
|
);
|
|
let config_service = trading_service::services::config::ConfigServiceImpl::new(db_pool.clone());
|
|
let monitoring_service = MonitoringServiceImpl::new(service_state);
|
|
|
|
// Create health service
|
|
let (health_reporter, health_service) = tonic_health::server::health_reporter();
|
|
health_reporter
|
|
.set_serving::<trading_service::proto::trading::trading_service_server::TradingServiceServer<TradingServiceImpl>>()
|
|
.await;
|
|
|
|
// Build gRPC server with TLS, authentication, and rate limiting
|
|
let grpc_port = std::env::var("GRPC_PORT")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(DEFAULT_GRPC_PORT);
|
|
let addr = format!("0.0.0.0:{}", grpc_port).parse()?;
|
|
|
|
// Load TLS configuration if enabled
|
|
let tls_config = if std::env::var("TLS_ENABLED")
|
|
.unwrap_or_else(|_| "false".to_string())
|
|
.parse::<bool>()
|
|
.unwrap_or(false)
|
|
{
|
|
info!("Loading TLS configuration...");
|
|
use trading_service::tls_config::TradingServiceTlsConfig;
|
|
|
|
let tls = TradingServiceTlsConfig::from_files(
|
|
&std::env::var("TLS_CERT_PATH")
|
|
.unwrap_or_else(|_| "/app/certs/trading_service/server.crt".to_string()),
|
|
&std::env::var("TLS_KEY_PATH")
|
|
.unwrap_or_else(|_| "/app/certs/trading_service/server.key".to_string()),
|
|
&std::env::var("TLS_CA_PATH")
|
|
.unwrap_or_else(|_| "/app/certs/trading_service/ca.crt".to_string()),
|
|
std::env::var("TLS_REQUIRE_CLIENT_CERT")
|
|
.unwrap_or_else(|_| "false".to_string())
|
|
.parse::<bool>()
|
|
.unwrap_or(false),
|
|
)
|
|
.await?;
|
|
|
|
info!("✓ TLS 1.3 enabled with mTLS client certificate validation");
|
|
Some(tls.to_server_tls_config())
|
|
} else {
|
|
warn!("⚠ TLS DISABLED - Running in insecure mode (development only)");
|
|
None
|
|
};
|
|
|
|
info!("🔒 Starting gRPC server with authentication enabled");
|
|
|
|
// Wave 67 Agent 3: HTTP/2 streaming performance optimizations
|
|
// Load streaming configuration with feature flag support
|
|
let streaming_config = trading_service::streaming::StreamingConfig::from_env();
|
|
|
|
if streaming_config.is_enabled() {
|
|
info!("✅ HTTP/2 optimizations enabled:");
|
|
info!(
|
|
" - tcp_nodelay: {} (-40ms Nagle delay)",
|
|
streaming_config.tcp_nodelay
|
|
);
|
|
info!(
|
|
" - Stream window: {}KB",
|
|
streaming_config.initial_stream_window_size / 1024
|
|
);
|
|
info!(
|
|
" - Connection window: {}MB",
|
|
streaming_config.initial_connection_window_size / (1024 * 1024)
|
|
);
|
|
info!(
|
|
" - Adaptive window: {}",
|
|
streaming_config.http2_adaptive_window
|
|
);
|
|
info!(" - Max streams: 10,000 (production scale)");
|
|
} else {
|
|
info!("⚠️ HTTP/2 optimizations disabled via feature flag");
|
|
}
|
|
|
|
// Start Prometheus metrics HTTP endpoint on port 9092
|
|
use trading_service::metrics_server::{MetricsServerConfig, TradingMetricsServer};
|
|
|
|
let metrics_config = MetricsServerConfig {
|
|
bind_address: "0.0.0.0".to_string(),
|
|
bind_port: 9092,
|
|
scrape_timeout_ms: 500,
|
|
max_metrics_per_scrape: 10000,
|
|
};
|
|
|
|
let metrics_server = TradingMetricsServer::new(metrics_config);
|
|
tokio::spawn(async move {
|
|
if let Err(e) = metrics_server.start().await {
|
|
tracing::error!("Metrics server failed: {}", e);
|
|
}
|
|
});
|
|
|
|
// Apply authentication interceptor to all gRPC services
|
|
let mut server_builder = match tls_config {
|
|
Some(tls) => Server::builder()
|
|
.layer(GrpcMetricsLayer::new("trading-service"))
|
|
.tls_config(tls)
|
|
.context("Failed to configure TLS")?,
|
|
None => Server::builder()
|
|
.layer(GrpcMetricsLayer::new("trading-service")),
|
|
};
|
|
|
|
// Apply HTTP/2 optimizations if enabled
|
|
if streaming_config.is_enabled() {
|
|
server_builder = server_builder
|
|
.tcp_nodelay(streaming_config.tcp_nodelay) // Critical: eliminates 40ms Nagle delay
|
|
.http2_keepalive_interval(Some(streaming_config.http2_keepalive_interval))
|
|
.http2_keepalive_timeout(Some(streaming_config.http2_keepalive_timeout))
|
|
.initial_stream_window_size(Some(streaming_config.initial_stream_window_size))
|
|
.initial_connection_window_size(Some(streaming_config.initial_connection_window_size))
|
|
.http2_adaptive_window(Some(streaming_config.http2_adaptive_window))
|
|
.max_concurrent_streams(Some(10_000)); // Increased from 1,024 to 10,000 for production scale
|
|
}
|
|
|
|
let server = server_builder
|
|
.add_service(health_service)
|
|
.add_service(
|
|
trading_service::proto::trading::trading_service_server::TradingServiceServer::with_interceptor(
|
|
trading_service,
|
|
auth_interceptor.clone()
|
|
)
|
|
)
|
|
.add_service(
|
|
trading_service::proto::risk::risk_service_server::RiskServiceServer::with_interceptor(
|
|
risk_service,
|
|
auth_interceptor.clone()
|
|
)
|
|
)
|
|
.add_service(
|
|
trading_service::proto::ml::ml_service_server::MlServiceServer::with_interceptor(
|
|
ml_service,
|
|
auth_interceptor.clone()
|
|
)
|
|
)
|
|
.add_service(
|
|
trading_service::proto::monitoring::monitoring_service_server::MonitoringServiceServer::with_interceptor(
|
|
monitoring_service,
|
|
auth_interceptor.clone()
|
|
)
|
|
)
|
|
.add_service(
|
|
trading_service::proto::config::config_service_server::ConfigServiceServer::with_interceptor(
|
|
config_service,
|
|
auth_interceptor.clone()
|
|
)
|
|
)
|
|
.serve_with_shutdown(addr, shutdown_signal());
|
|
|
|
info!("Trading Service listening on {}", addr);
|
|
|
|
// Start background tasks with kill switch monitoring
|
|
tokio::select! {
|
|
result = server => {
|
|
if let Err(e) = result {
|
|
error!("gRPC server error: {}", e);
|
|
}
|
|
}
|
|
_ = start_health_endpoint(std::env::var("HEALTH_PORT")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(DEFAULT_HEALTH_PORT)) => {
|
|
info!("Health endpoint stopped");
|
|
}
|
|
_ = monitor_kill_switch_status(Arc::clone(&kill_switch_system)) => {
|
|
info!("Kill switch monitoring stopped");
|
|
}
|
|
}
|
|
|
|
// Signal prediction loop(s) to stop, then drop senders
|
|
for tx in &prediction_shutdown_handles {
|
|
let _ = tx.send(());
|
|
}
|
|
drop(prediction_shutdown_handles);
|
|
info!("Background prediction loops signalled for shutdown");
|
|
|
|
// Cleanup kill switch monitoring on shutdown
|
|
info!("Stopping kill switch monitoring...");
|
|
if let Err(e) = kill_switch_system.stop_monitoring().await {
|
|
warn!("Error stopping kill switch monitoring: {}", e);
|
|
}
|
|
|
|
info!("Trading Service shutdown complete");
|
|
Ok(())
|
|
}
|
|
|
|
/// Initialize authentication configuration
|
|
///
|
|
/// NOTE: Authentication is handled by API Gateway (Wave 70)
|
|
/// This function returns a minimal stub configuration for compatibility
|
|
async fn initialize_auth_config() -> AuthConfig {
|
|
// API Gateway handles all authentication - this is just a stub
|
|
// All auth configuration is managed by API Gateway, not this service
|
|
AuthConfig::default()
|
|
}
|
|
|
|
/// Initialize default configuration values if they don't exist
|
|
async fn initialize_default_configs(_config_repository: &PostgresConfigRepository) -> Result<()> {
|
|
info!("Initializing default configurations...");
|
|
|
|
// Note: Configuration initialization is now handled by ConfigManager
|
|
// This function is kept for backward compatibility but is a no-op
|
|
// The ConfigManager handles all configuration persistence and defaults
|
|
|
|
info!("Default configurations initialized via ConfigManager");
|
|
Ok(())
|
|
}
|
|
|
|
/// Start configuration monitoring for hot-reload
|
|
async fn start_config_monitoring(config_repository: Arc<PostgresConfigRepository>) -> Result<()> {
|
|
let mut change_receiver = config_repository
|
|
.subscribe_to_changes()
|
|
.await
|
|
.context("Failed to subscribe to configuration changes")?;
|
|
|
|
tokio::spawn(async move {
|
|
info!("Configuration hot-reload monitoring started");
|
|
|
|
while let Ok((category, key)) = change_receiver.recv().await {
|
|
info!("Configuration changed: {}.{}", category, key);
|
|
|
|
// Handle specific configuration changes using typed methods
|
|
match (category.as_str(), key.as_str()) {
|
|
("Trading", "max_order_size") => {
|
|
if let Ok(Some(value)) = config_repository
|
|
.get_config_f64("Trading", "max_order_size")
|
|
.await
|
|
{
|
|
info!("Updated max order size to: ${}", value);
|
|
}
|
|
},
|
|
("MachineLearning", "inference_timeout_ms") => {
|
|
if let Ok(Some(value)) = config_repository
|
|
.get_config_u64("MachineLearning", "inference_timeout_ms")
|
|
.await
|
|
{
|
|
info!("Updated ML inference timeout to: {}ms", value);
|
|
}
|
|
},
|
|
("Risk", "var_confidence") => {
|
|
if let Ok(Some(value)) = config_repository
|
|
.get_config_f64("Risk", "var_confidence")
|
|
.await
|
|
{
|
|
info!("Updated VaR confidence to: {}", value);
|
|
}
|
|
},
|
|
_ => {
|
|
info!("Configuration updated: {}.{}", category, key);
|
|
},
|
|
}
|
|
}
|
|
|
|
info!("Configuration monitoring stopped");
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Start health check endpoint
|
|
async fn start_health_endpoint(port: u16) -> Result<()> {
|
|
use hyper::server::conn::http1;
|
|
use hyper::service::service_fn;
|
|
use hyper_util::rt::TokioIo;
|
|
|
|
use tokio::net::TcpListener;
|
|
|
|
let addr: std::net::SocketAddr = ([0, 0, 0, 0], port).into();
|
|
let listener = TcpListener::bind(addr)
|
|
.await
|
|
.context("Failed to bind health endpoint")?;
|
|
|
|
info!("Health endpoint listening on http://{}", addr);
|
|
|
|
loop {
|
|
let (stream, _) = match listener.accept().await {
|
|
Ok(conn) => conn,
|
|
Err(e) => {
|
|
error!("Failed to accept connection: {}", e);
|
|
continue;
|
|
},
|
|
};
|
|
|
|
tokio::spawn(async move {
|
|
let io = TokioIo::new(stream);
|
|
if let Err(e) = http1::Builder::new()
|
|
.serve_connection(io, service_fn(health_handler))
|
|
.await
|
|
{
|
|
error!("Health server error: {}", e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Health check handler with database pool statistics
|
|
async fn health_handler(
|
|
_: hyper::Request<hyper::body::Incoming>,
|
|
) -> Result<hyper::Response<http_body_util::Full<bytes::Bytes>>, std::convert::Infallible> {
|
|
use bytes::Bytes;
|
|
use http_body_util::Full;
|
|
|
|
// Note: In a production system, you'd pass the db_pool_wrapper here
|
|
// For now, we'll indicate that database connection pooling is active
|
|
let health_response = serde_json::json!({
|
|
"status": "healthy",
|
|
"service": "trading_service",
|
|
"timestamp": chrono::Utc::now().to_rfc3339(),
|
|
"version": env!("CARGO_PKG_VERSION"),
|
|
"database": {
|
|
"connection_pool": "HFT-optimized",
|
|
"query_timeout_micros": 800,
|
|
"connection_prewarming": "enabled",
|
|
"prepared_statements": "enabled"
|
|
}
|
|
});
|
|
|
|
// Build HTTP response with error handling to prevent panics in production
|
|
let response = hyper::Response::builder()
|
|
.status(200)
|
|
.header("content-type", "application/json")
|
|
.body(Full::new(Bytes::from(health_response.to_string())))
|
|
.unwrap_or_else(|e| {
|
|
// Return a minimal error response if primary response building fails
|
|
error!("Failed to build primary health response: {}", e);
|
|
|
|
// Try to build error response
|
|
match hyper::Response::builder()
|
|
.status(500)
|
|
.header("content-type", "application/json")
|
|
.body(Full::new(Bytes::from(
|
|
r#"{"status":"error","message":"Health check failed"}"#,
|
|
))) {
|
|
Ok(err_response) => err_response,
|
|
Err(fatal_err) => {
|
|
// Last resort: return minimal response without builder
|
|
error!("FATAL: Cannot build any HTTP response: {}", fatal_err);
|
|
hyper::Response::new(Full::new(Bytes::from(r#"{"status":"error"}"#)))
|
|
},
|
|
}
|
|
});
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
/// Handle shutdown signals
|
|
async fn shutdown_signal() {
|
|
let ctrl_c = async {
|
|
if let Err(e) = signal::ctrl_c().await {
|
|
error!("Failed to install Ctrl+C handler: {}", e);
|
|
}
|
|
};
|
|
|
|
#[cfg(unix)]
|
|
let terminate = async {
|
|
match signal::unix::signal(signal::unix::SignalKind::terminate()) {
|
|
Ok(mut signal_stream) => {
|
|
signal_stream.recv().await;
|
|
},
|
|
Err(e) => {
|
|
error!("Failed to install SIGTERM handler: {}", e);
|
|
},
|
|
}
|
|
};
|
|
|
|
#[cfg(not(unix))]
|
|
let terminate = std::future::pending::<()>();
|
|
|
|
tokio::select! {
|
|
_ = ctrl_c => {},
|
|
_ = terminate => {},
|
|
}
|
|
|
|
info!("Shutdown signal received");
|
|
}
|
|
|
|
/// Monitor kill switch status and log important events
|
|
async fn monitor_kill_switch_status(kill_switch_system: Arc<TradingServiceKillSwitch>) {
|
|
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
|
|
|
loop {
|
|
interval.tick().await;
|
|
|
|
match kill_switch_system.get_status().await {
|
|
Ok(status) => {
|
|
if status.is_active {
|
|
error!(
|
|
"KILL SWITCH ACTIVE - Trading blocked | Checks: {} | Commands: {} | Error Rate: {:.2}%",
|
|
status.total_checks,
|
|
status.total_commands,
|
|
status.error_rate * 100.0
|
|
);
|
|
}
|
|
|
|
if status.is_emergency_active {
|
|
error!("🚨🚨🚨 EMERGENCY SHUTDOWN ACTIVE - System halted");
|
|
}
|
|
|
|
if !status.is_healthy {
|
|
warn!(
|
|
"⚠️ Kill switch system unhealthy - Error rate: {:.2}% | Consecutive failures: {}",
|
|
status.error_rate * 100.0,
|
|
status.consecutive_failures
|
|
);
|
|
}
|
|
|
|
// Log periodic status (every 5 minutes)
|
|
if status.total_checks % 100 == 0 {
|
|
info!(
|
|
"Kill switch status: Active={} | Healthy={} | Checks={} | Commands={}",
|
|
status.is_active,
|
|
status.is_healthy,
|
|
status.total_checks,
|
|
status.total_commands
|
|
);
|
|
}
|
|
},
|
|
Err(e) => {
|
|
error!("Failed to get kill switch status: {}", e);
|
|
},
|
|
}
|
|
}
|
|
}
|