Files
foxhunt/services/trading_service/src/main.rs
jgrusewski e4870b17b9 fix: tune log levels across workspace — demote noisy warn to debug/trace
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>
2026-03-14 11:35:15 +01:00

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);
},
}
}
}