use crate::specialized::finance::risk::realtime::{
EventDrivenRisk, IncrementalVaR, MarketTick, PositionLimitChecker, RiskAggregator, RiskMonitor,
StreamingGreeks,
};
pub use crate::specialized::finance::risk::realtime::RiskAlert;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AlertSeverity {
Info,
Warning,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RiskAlertType {
PositionLimitBreach,
VarLimitBreach,
GreeksThresholdBreach,
}
impl RiskAlertType {
pub fn classify(alert: &RiskAlert) -> Self {
match alert {
RiskAlert::LimitBreach { .. } => RiskAlertType::PositionLimitBreach,
RiskAlert::VarExceeded { .. } => RiskAlertType::VarLimitBreach,
RiskAlert::GreeksThreshold { .. } => RiskAlertType::GreeksThresholdBreach,
}
}
}
pub fn classify_severity(alert: &RiskAlert) -> AlertSeverity {
match alert {
RiskAlert::LimitBreach { current, limit, .. } => severity_from_ratio(*current, *limit),
RiskAlert::VarExceeded {
current_var, limit, ..
} => severity_from_ratio(*current_var, *limit),
RiskAlert::GreeksThreshold { .. } => AlertSeverity::Warning,
}
}
fn severity_from_ratio(current: f64, limit: f64) -> AlertSeverity {
if limit.abs() < 1e-12 {
return AlertSeverity::Critical;
}
let ratio = (current / limit).abs();
if ratio >= 2.0 {
AlertSeverity::Critical
} else if ratio >= 1.0 {
AlertSeverity::Warning
} else {
AlertSeverity::Info
}
}
#[derive(Debug, Clone)]
pub struct DashboardAlert {
pub tick_sequence: u64,
pub alert_type: RiskAlertType,
pub severity: AlertSeverity,
pub alert: RiskAlert,
}
#[derive(Debug, Clone)]
pub struct RiskSnapshot {
pub tick_count: u64,
pub total_alerts: usize,
pub critical_alerts: usize,
pub warning_alerts: usize,
pub info_alerts: usize,
pub recent_alerts: Vec<DashboardAlert>,
}
#[derive(Debug, Clone)]
pub struct RiskDashboard {
pub monitor_names: Vec<String>,
pub snapshot: RiskSnapshot,
}
impl RiskDashboard {
pub fn worst_severity(&self) -> Option<AlertSeverity> {
self.snapshot.recent_alerts.iter().map(|a| a.severity).max()
}
}
pub struct RealTimeRiskMonitor {
dispatcher: EventDrivenRisk,
monitor_names: Vec<String>,
history: Vec<DashboardAlert>,
tick_count: u64,
max_history: usize,
}
impl RealTimeRiskMonitor {
pub fn new() -> Self {
Self {
dispatcher: EventDrivenRisk::new(),
monitor_names: Vec::new(),
history: Vec::new(),
tick_count: 0,
max_history: 1000,
}
}
pub fn with_history_capacity(max_history: usize) -> Self {
Self {
max_history,
..Self::new()
}
}
pub fn add_position_limit_checker(&mut self, checker: PositionLimitChecker) {
self.monitor_names.push(checker.name().to_string());
self.dispatcher.add_monitor(Box::new(checker));
}
pub fn add_incremental_var(&mut self, ivar: IncrementalVaR) {
self.monitor_names.push(ivar.name().to_string());
self.dispatcher.add_monitor(Box::new(ivar));
}
pub fn add_streaming_greeks(&mut self, greeks: StreamingGreeks) {
self.monitor_names.push(greeks.name().to_string());
self.dispatcher.add_monitor(Box::new(greeks));
}
pub fn add_risk_aggregator(&mut self, aggregator: RiskAggregator) {
self.monitor_names.push(aggregator.name().to_string());
self.dispatcher.add_monitor(Box::new(aggregator));
}
pub fn process_tick(&mut self, tick: &MarketTick) -> Vec<DashboardAlert> {
self.tick_count += 1;
let alerts = self.dispatcher.process_tick(tick);
let classified: Vec<DashboardAlert> = alerts
.into_iter()
.map(|alert| DashboardAlert {
tick_sequence: self.tick_count,
alert_type: RiskAlertType::classify(&alert),
severity: classify_severity(&alert),
alert,
})
.collect();
for entry in &classified {
self.history.push(entry.clone());
}
while self.history.len() > self.max_history {
self.history.remove(0);
}
classified
}
pub fn snapshot(&self) -> RiskSnapshot {
let critical_alerts = self
.history
.iter()
.filter(|a| a.severity == AlertSeverity::Critical)
.count();
let warning_alerts = self
.history
.iter()
.filter(|a| a.severity == AlertSeverity::Warning)
.count();
let info_alerts = self
.history
.iter()
.filter(|a| a.severity == AlertSeverity::Info)
.count();
RiskSnapshot {
tick_count: self.tick_count,
total_alerts: self.history.len(),
critical_alerts,
warning_alerts,
info_alerts,
recent_alerts: self.history.clone(),
}
}
pub fn dashboard(&self) -> RiskDashboard {
RiskDashboard {
monitor_names: self.monitor_names.clone(),
snapshot: self.snapshot(),
}
}
}
impl Default for RealTimeRiskMonitor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tick(symbol: &str, price: f64) -> MarketTick {
MarketTick {
symbol: symbol.to_string(),
price,
volume: 1000.0,
timestamp: 1_700_000_000_000,
bid: price - 0.01,
ask: price + 0.01,
}
}
#[test]
fn test_dashboard_matches_direct_position_limit_checker_and_classifies_severity() {
let mut direct_checker = PositionLimitChecker::new();
direct_checker.set_limit("AAPL".to_string(), 5_000.0);
direct_checker.set_position("AAPL".to_string(), 100.0);
let tick = make_tick("AAPL", 110.0);
let direct_alert = direct_checker
.on_market_data(&tick)
.expect("direct monitor should succeed")
.expect("direct monitor should fire an alert");
let mut facade_checker = PositionLimitChecker::new();
facade_checker.set_limit("AAPL".to_string(), 5_000.0);
facade_checker.set_position("AAPL".to_string(), 100.0);
let mut monitor = RealTimeRiskMonitor::new();
monitor.add_position_limit_checker(facade_checker);
let fired = monitor.process_tick(&tick);
assert_eq!(fired.len(), 1, "facade should fire exactly one alert");
match (&fired[0].alert, &direct_alert) {
(
RiskAlert::LimitBreach {
symbol: s1,
current: c1,
limit: l1,
},
RiskAlert::LimitBreach {
symbol: s2,
current: c2,
limit: l2,
},
) => {
assert_eq!(s1, s2);
assert!((c1 - c2).abs() < 1e-9, "current mismatch: {c1} vs {c2}");
assert!((l1 - l2).abs() < 1e-9, "limit mismatch: {l1} vs {l2}");
}
other => panic!("Expected matching LimitBreach alerts, got {other:?}"),
}
assert_eq!(fired[0].alert_type, RiskAlertType::PositionLimitBreach);
assert_eq!(fired[0].severity, AlertSeverity::Critical);
let snapshot = monitor.snapshot();
assert_eq!(snapshot.total_alerts, 1);
assert_eq!(snapshot.critical_alerts, 1);
assert_eq!(snapshot.warning_alerts, 0);
assert_eq!(snapshot.tick_count, 1);
let dashboard = monitor.dashboard();
assert_eq!(dashboard.monitor_names, vec!["PositionLimitChecker"]);
assert_eq!(dashboard.worst_severity(), Some(AlertSeverity::Critical));
}
#[test]
fn test_severity_thresholds() {
let warning_alert = RiskAlert::LimitBreach {
symbol: "X".to_string(),
current: 1_200.0,
limit: 1_000.0,
}; let critical_alert = RiskAlert::LimitBreach {
symbol: "X".to_string(),
current: 2_500.0,
limit: 1_000.0,
}; assert_eq!(classify_severity(&warning_alert), AlertSeverity::Warning);
assert_eq!(classify_severity(&critical_alert), AlertSeverity::Critical);
assert!(AlertSeverity::Info < AlertSeverity::Warning);
assert!(AlertSeverity::Warning < AlertSeverity::Critical);
}
#[test]
fn test_history_capacity_is_enforced() {
let mut monitor = RealTimeRiskMonitor::with_history_capacity(2);
let mut checker = PositionLimitChecker::new();
checker.set_limit("X".to_string(), 1.0); checker.set_position("X".to_string(), 10.0);
monitor.add_position_limit_checker(checker);
for _ in 0..5 {
let tick = make_tick("X", 100.0);
let _ = monitor.process_tick(&tick);
}
let snapshot = monitor.snapshot();
assert_eq!(snapshot.total_alerts, 2, "history should be capped at 2");
assert_eq!(snapshot.tick_count, 5);
}
}