//! System state observability snapshot. //! //! Aggregates health, risk, and model state into a single serializable //! structure for monitoring and alerting (Prometheus/Grafana). use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Complete system state snapshot. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SystemState { /// When this snapshot was captured. pub timestamp: DateTime, /// Pipeline health status. pub pipeline_health: PipelineHealth, /// Current drawdown state. pub drawdown: DrawdownState, /// Active positions summary. pub positions: PositionsSummary, /// Drift detection scores per model. pub drift_scores: HashMap, /// Model versions currently loaded. pub model_versions: HashMap, /// Risk enforcement state. pub risk_state: RiskState, } /// Pipeline processing health. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PipelineHealth { /// Whether the pipeline is currently running. pub is_running: bool, /// Messages processed in the last minute. pub messages_per_minute: u64, /// Number of rejected messages in the last minute. pub rejections_per_minute: u64, /// Average latency in microseconds. pub avg_latency_us: f64, } impl Default for PipelineHealth { fn default() -> Self { Self { is_running: false, messages_per_minute: 0, rejections_per_minute: 0, avg_latency_us: 0.0, } } } /// Current drawdown information. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DrawdownState { /// Current drawdown as fraction (0.05 = 5%). pub current_pct: f64, /// Maximum drawdown seen today. pub max_today_pct: f64, /// Whether any drawdown threshold is breached. pub threshold_breached: bool, /// Active alert severity, if any. pub active_severity: Option, } impl Default for DrawdownState { fn default() -> Self { Self { current_pct: 0.0, max_today_pct: 0.0, threshold_breached: false, active_severity: None, } } } /// Summary of open positions. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PositionsSummary { /// Number of open positions. pub open_count: u32, /// Total exposure in base currency. pub total_exposure: f64, /// Per-symbol exposure. pub per_symbol: HashMap, } impl Default for PositionsSummary { fn default() -> Self { Self { open_count: 0, total_exposure: 0.0, per_symbol: HashMap::new(), } } } /// Information about a loaded model. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ModelInfo { /// Semantic version of the model. pub version: String, /// When the model was loaded. pub loaded_at: DateTime, /// Total inferences run since load. pub inference_count: u64, /// Average inference latency in microseconds. pub avg_inference_us: f64, } /// Risk enforcement state. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RiskState { /// Whether the kill switch is active. pub kill_switch_active: bool, /// Current position size multiplier (1.0 = full size). pub position_multiplier: f64, /// Whether new entries are halted. pub entries_halted: bool, /// Whether in recovery mode. pub in_recovery: bool, /// Recovery multiplier if in recovery. pub recovery_multiplier: Option, } impl Default for RiskState { fn default() -> Self { Self { kill_switch_active: false, position_multiplier: 1.0, entries_halted: false, in_recovery: false, recovery_multiplier: None, } } } impl SystemState { /// Create a new system state snapshot at the current time. pub fn capture( pipeline_health: PipelineHealth, drawdown: DrawdownState, positions: PositionsSummary, drift_scores: HashMap, model_versions: HashMap, risk_state: RiskState, ) -> Self { Self { timestamp: Utc::now(), pipeline_health, drawdown, positions, drift_scores, model_versions, risk_state, } } /// Check if the system is in a healthy state. /// /// A system is healthy when: /// - The pipeline is running /// - The kill switch is not active /// - No drawdown threshold is breached /// - New entries are not halted pub fn is_healthy(&self) -> bool { self.pipeline_health.is_running && !self.risk_state.kill_switch_active && !self.drawdown.threshold_breached && !self.risk_state.entries_halted } /// Serialize to JSON string. pub fn to_json(&self) -> Result { serde_json::to_string_pretty(self) } } impl Default for SystemState { fn default() -> Self { Self { timestamp: Utc::now(), pipeline_health: PipelineHealth::default(), drawdown: DrawdownState::default(), positions: PositionsSummary::default(), drift_scores: HashMap::new(), model_versions: HashMap::new(), risk_state: RiskState::default(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_system_state_not_healthy() { let state = SystemState::default(); // Default pipeline is not running, so not healthy assert!(!state.is_healthy()); } #[test] fn test_healthy_system() { let state = SystemState::capture( PipelineHealth { is_running: true, messages_per_minute: 100, rejections_per_minute: 0, avg_latency_us: 50.0, }, DrawdownState::default(), PositionsSummary::default(), HashMap::new(), HashMap::new(), RiskState::default(), ); assert!(state.is_healthy()); } #[test] fn test_kill_switch_makes_unhealthy() { let state = SystemState::capture( PipelineHealth { is_running: true, ..PipelineHealth::default() }, DrawdownState::default(), PositionsSummary::default(), HashMap::new(), HashMap::new(), RiskState { kill_switch_active: true, ..RiskState::default() }, ); assert!(!state.is_healthy()); } #[test] fn test_serialization_roundtrip() { let state = SystemState::default(); let json = state.to_json(); assert!(json.is_ok()); if let Ok(json_str) = json { let deserialized: Result = serde_json::from_str(&json_str); assert!(deserialized.is_ok()); } } #[test] fn test_drawdown_breach_makes_unhealthy() { let state = SystemState::capture( PipelineHealth { is_running: true, ..PipelineHealth::default() }, DrawdownState { threshold_breached: true, ..DrawdownState::default() }, PositionsSummary::default(), HashMap::new(), HashMap::new(), RiskState::default(), ); assert!(!state.is_healthy()); } #[test] fn test_entries_halted_makes_unhealthy() { let state = SystemState::capture( PipelineHealth { is_running: true, ..PipelineHealth::default() }, DrawdownState::default(), PositionsSummary::default(), HashMap::new(), HashMap::new(), RiskState { entries_halted: true, ..RiskState::default() }, ); assert!(!state.is_healthy()); } #[test] fn test_pipeline_health_default() { let health = PipelineHealth::default(); assert!(!health.is_running); assert_eq!(health.messages_per_minute, 0); assert_eq!(health.rejections_per_minute, 0); assert!((health.avg_latency_us - 0.0).abs() < f64::EPSILON); } #[test] fn test_risk_state_default() { let risk = RiskState::default(); assert!(!risk.kill_switch_active); assert!((risk.position_multiplier - 1.0).abs() < f64::EPSILON); assert!(!risk.entries_halted); assert!(!risk.in_recovery); assert!(risk.recovery_multiplier.is_none()); } #[test] fn test_positions_summary_default() { let pos = PositionsSummary::default(); assert_eq!(pos.open_count, 0); assert!((pos.total_exposure - 0.0).abs() < f64::EPSILON); assert!(pos.per_symbol.is_empty()); } #[test] fn test_drawdown_state_default() { let dd = DrawdownState::default(); assert!((dd.current_pct - 0.0).abs() < f64::EPSILON); assert!((dd.max_today_pct - 0.0).abs() < f64::EPSILON); assert!(!dd.threshold_breached); assert!(dd.active_severity.is_none()); } #[test] fn test_system_state_with_drift_scores() { let mut drift = HashMap::new(); drift.insert("dqn_v1".to_string(), 0.03); drift.insert("ppo_v2".to_string(), 0.15); let state = SystemState::capture( PipelineHealth { is_running: true, ..PipelineHealth::default() }, DrawdownState::default(), PositionsSummary::default(), drift, HashMap::new(), RiskState::default(), ); assert!(state.is_healthy()); assert_eq!(state.drift_scores.len(), 2); } #[test] fn test_system_state_with_model_versions() { let mut models = HashMap::new(); models.insert( "dqn".to_string(), ModelInfo { version: "1.2.3".to_string(), loaded_at: Utc::now(), inference_count: 1000, avg_inference_us: 45.0, }, ); let state = SystemState::capture( PipelineHealth { is_running: true, ..PipelineHealth::default() }, DrawdownState::default(), PositionsSummary::default(), HashMap::new(), models, RiskState::default(), ); assert!(state.is_healthy()); assert_eq!(state.model_versions.len(), 1); } #[test] fn test_recovery_mode_still_healthy() { // Recovery mode alone does not make the system unhealthy let state = SystemState::capture( PipelineHealth { is_running: true, ..PipelineHealth::default() }, DrawdownState::default(), PositionsSummary::default(), HashMap::new(), HashMap::new(), RiskState { in_recovery: true, recovery_multiplier: Some(0.5), ..RiskState::default() }, ); assert!(state.is_healthy()); } }