Files
foxhunt/crates/ml-risk/src/circuit_breakers.rs
jgrusewski d313486dc2 refactor(ml): split monolith into 9 sub-crates + delete dead code
Extract 9 new sub-crates from the ml monolith to enable parallel
compilation across the workspace:

New crates (this commit):
- ml-features (282 tests): feature engineering, 21 modules
- ml-labeling (45 tests): triple barrier, meta-labeling, fractional diff
- ml-ensemble (116 tests): ensemble coordination, voting, confidence
- ml-hyperopt (47 tests): core PSO/TPE optimizer, parameter space
- ml-checkpoint (41 tests): checkpoint persistence, compression, signing
- ml-regime (68 tests): CUSUM, Bayesian changepoint, regime classification
- ml-data-validation (67 tests): FDR correction, CPCV, data quality
- ml-risk (33 tests): neural VaR, Kelly criterion, circuit breakers
- ml-validation (43 tests): statistical validation, walk-forward, DSR

Extended existing crates:
- ml-dqn: added evaluation/ (backtesting engine, metrics, reports)
  and checkpoint implementation
- ml-supervised: added checkpoint implementations
- ml-core: added shared types needed by new sub-crates

Pattern: each module in ml/ becomes a thin facade (pub use subcrate::*)
with bridge modules staying in ml for cross-model adapter code.

Dead code deleted (~7K lines):
- 13 undeclared files in microstructure/ (never compiled)
- 7 undeclared files + tests/ in risk/ (never compiled)
- parquet_io, cache_service, cache_storage, minio_integration (unused)
- extraction_wave_d_impl.rs (bare fn outside impl block)

All 2,746 sub-crate tests + 951 ml tests pass.
Full workspace builds clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:17:22 +01:00

473 lines
16 KiB
Rust

//! ML-Enhanced Circuit Breakers for HFT Risk Management
//!
//! Implements intelligent circuit breakers that use machine learning models
//! to detect market stress, model degradation, and risk anomalies with
//! canonical types for production HFT systems.
use std::collections::{HashMap, VecDeque};
use chrono::{DateTime, Utc};
use common::types::{Price, Volume};
use serde::{Deserialize, Serialize};
use crate::{MLError, MLResult as Result};
/// Circuit breaker type enumeration
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum CircuitBreakerType {
/// Volatility-based circuit breaker
Volatility,
/// Drawdown-based circuit breaker
Drawdown,
/// Volume-based circuit breaker
Volume,
/// Model performance circuit breaker
ModelPerformance,
/// Market stress circuit breaker
MarketStress,
/// Position concentration circuit breaker
PositionConcentration,
}
/// Circuit breaker configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircuitBreakerConfig {
pub volatility_threshold: f64,
pub drawdown_threshold: f64,
pub volume_spike_threshold: f64,
pub model_accuracy_threshold: f64,
pub max_position_size: f64,
pub lookback_period: usize,
pub cooldown_seconds: u64,
pub enable_auto_reset: bool,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
volatility_threshold: 0.05, // 5% volatility threshold
drawdown_threshold: 0.03, // 3% drawdown threshold
volume_spike_threshold: 3.0, // 3x normal volume
model_accuracy_threshold: 0.8, // 80% minimum accuracy
max_position_size: 0.1, // 10% max position size
lookback_period: 100, // 100 periods lookback
cooldown_seconds: 300, // 5 minute cooldown
enable_auto_reset: true,
}
}
}
/// Circuit breaker state
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircuitBreakerState {
pub breaker_type: CircuitBreakerType,
pub is_triggered: bool,
pub trigger_time: Option<DateTime<Utc>>,
pub trigger_value: f64,
pub threshold: f64,
pub reset_time: Option<DateTime<Utc>>,
pub trigger_count: u64,
}
impl CircuitBreakerState {
pub fn new(breaker_type: CircuitBreakerType, threshold: f64) -> Self {
Self {
breaker_type,
is_triggered: false,
trigger_time: None,
trigger_value: 0.0,
threshold,
reset_time: None,
trigger_count: 0,
}
}
pub fn trigger(&mut self, value: f64) {
self.is_triggered = true;
self.trigger_time = Some(Utc::now());
self.trigger_value = value;
self.trigger_count += 1;
}
pub fn reset(&mut self) {
self.is_triggered = false;
self.reset_time = Some(Utc::now());
self.trigger_value = 0.0;
}
pub fn can_reset(&self, cooldown_seconds: u64) -> bool {
if let Some(trigger_time) = self.trigger_time {
let elapsed = Utc::now().signed_duration_since(trigger_time);
elapsed.num_seconds() >= cooldown_seconds as i64
} else {
true
}
}
}
/// ML Circuit Breaker system
#[derive(Debug)]
pub struct MLCircuitBreaker {
config: CircuitBreakerConfig,
states: HashMap<CircuitBreakerType, CircuitBreakerState>,
historical_data: VecDeque<MarketDataPoint>,
performance_history: VecDeque<f64>,
}
/// Market data point for circuit breaker analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketDataPoint {
pub timestamp: DateTime<Utc>,
pub price: Price,
pub volume: Volume,
pub volatility: f64,
pub returns: f64,
}
impl MLCircuitBreaker {
pub fn new(config: CircuitBreakerConfig) -> Result<Self> {
let mut states = HashMap::new();
// Initialize all circuit breaker states
states.insert(
CircuitBreakerType::Volatility,
CircuitBreakerState::new(CircuitBreakerType::Volatility, config.volatility_threshold),
);
states.insert(
CircuitBreakerType::Drawdown,
CircuitBreakerState::new(CircuitBreakerType::Drawdown, config.drawdown_threshold),
);
states.insert(
CircuitBreakerType::Volume,
CircuitBreakerState::new(CircuitBreakerType::Volume, config.volume_spike_threshold),
);
states.insert(
CircuitBreakerType::ModelPerformance,
CircuitBreakerState::new(
CircuitBreakerType::ModelPerformance,
config.model_accuracy_threshold,
),
);
states.insert(
CircuitBreakerType::MarketStress,
CircuitBreakerState::new(CircuitBreakerType::MarketStress, 0.95), // 95th percentile
);
states.insert(
CircuitBreakerType::PositionConcentration,
CircuitBreakerState::new(
CircuitBreakerType::PositionConcentration,
config.max_position_size,
),
);
let lookback_period = config.lookback_period;
Ok(Self {
config,
states,
historical_data: VecDeque::with_capacity(lookback_period),
performance_history: VecDeque::with_capacity(100),
})
}
/// Update circuit breaker with new market data
pub fn update_market_data(&mut self, data: MarketDataPoint) -> Result<Vec<CircuitBreakerType>> {
let mut triggered_breakers = Vec::new();
// Add to historical data
self.historical_data.push_back(data.clone());
if self.historical_data.len() > self.config.lookback_period {
self.historical_data.pop_front();
}
// Check volatility circuit breaker
if data.volatility > self.config.volatility_threshold {
if let Some(state) = self.states.get_mut(&CircuitBreakerType::Volatility) {
if !state.is_triggered {
state.trigger(data.volatility);
triggered_breakers.push(CircuitBreakerType::Volatility);
}
}
}
// Check volume circuit breaker
if self.historical_data.len() > 10 {
let avg_volume = self
.historical_data
.iter()
.take(self.historical_data.len() - 1)
.map(|d| d.volume.to_f64())
.sum::<f64>()
/ (self.historical_data.len() - 1) as f64;
let volume_ratio = data.volume.to_f64() / avg_volume;
if volume_ratio > self.config.volume_spike_threshold {
if let Some(state) = self.states.get_mut(&CircuitBreakerType::Volume) {
if !state.is_triggered {
state.trigger(volume_ratio);
triggered_breakers.push(CircuitBreakerType::Volume);
}
}
}
}
// Check drawdown circuit breaker
if let Some(max_price) =
self.historical_data
.iter()
.map(|d| d.price.to_f64())
.fold(None, |max, price| match max {
None => Some(price),
Some(m) => Some(m.max(price)),
})
{
let current_drawdown = (max_price - data.price.to_f64()) / max_price;
if current_drawdown > self.config.drawdown_threshold {
if let Some(state) = self.states.get_mut(&CircuitBreakerType::Drawdown) {
if !state.is_triggered {
state.trigger(current_drawdown);
triggered_breakers.push(CircuitBreakerType::Drawdown);
}
}
}
}
Ok(triggered_breakers)
}
/// Update model performance and check performance circuit breaker
pub fn update_model_performance(&mut self, accuracy: f64) -> Result<bool> {
self.performance_history.push_back(accuracy);
if self.performance_history.len() > 100 {
self.performance_history.pop_front();
}
if accuracy < self.config.model_accuracy_threshold {
if let Some(state) = self.states.get_mut(&CircuitBreakerType::ModelPerformance) {
if !state.is_triggered {
state.trigger(accuracy);
return Ok(true);
}
}
}
Ok(false)
}
/// Check if any circuit breakers are triggered
pub fn is_any_triggered(&self) -> bool {
self.states.values().any(|state| state.is_triggered)
}
/// Check if specific circuit breaker is triggered
pub fn is_triggered(&self, breaker_type: CircuitBreakerType) -> bool {
self.states
.get(&breaker_type)
.map(|state| state.is_triggered)
.unwrap_or(false)
}
/// Get all triggered circuit breakers
pub fn get_triggered_breakers(&self) -> Vec<CircuitBreakerType> {
self.states
.iter()
.filter_map(|(breaker_type, state)| state.is_triggered.then_some(*breaker_type))
.collect()
}
/// Reset all circuit breakers that can be reset
pub fn reset_eligible_breakers(&mut self) -> Vec<CircuitBreakerType> {
let mut reset_breakers = Vec::new();
for (breaker_type, state) in self.states.iter_mut() {
if state.is_triggered && state.can_reset(self.config.cooldown_seconds) {
state.reset();
reset_breakers.push(*breaker_type);
}
}
reset_breakers
}
/// Manually reset specific circuit breaker
pub fn reset_breaker(&mut self, breaker_type: CircuitBreakerType) -> Result<()> {
if let Some(state) = self.states.get_mut(&breaker_type) {
state.reset();
Ok(())
} else {
Err(MLError::InvalidInput(format!(
"Unknown circuit breaker type: {:?}",
breaker_type
)))
}
}
/// Get circuit breaker state
pub fn get_state(&self, breaker_type: CircuitBreakerType) -> Option<&CircuitBreakerState> {
self.states.get(&breaker_type)
}
/// Get all circuit breaker states
pub fn get_all_states(&self) -> &HashMap<CircuitBreakerType, CircuitBreakerState> {
&self.states
}
/// Calculate market stress score
pub fn calculate_market_stress(&self) -> f64 {
if self.historical_data.len() < 10 {
return 0.0;
}
// Calculate volatility score
let volatilities: Vec<f64> = self.historical_data.iter().map(|d| d.volatility).collect();
let avg_volatility = volatilities.iter().sum::<f64>() / volatilities.len() as f64;
let volatility_score = (avg_volatility / self.config.volatility_threshold).min(1.0);
// Calculate volume score
let volumes: Vec<f64> = self
.historical_data
.iter()
.map(|d| d.volume.to_f64())
.collect();
let avg_volume = volumes.iter().sum::<f64>() / volumes.len() as f64;
let recent_volume = volumes.iter().rev().take(5).sum::<f64>() / 5.0;
let volume_score =
(recent_volume / avg_volume / self.config.volume_spike_threshold).min(1.0);
// Calculate returns volatility
let returns: Vec<f64> = self.historical_data.iter().map(|d| d.returns).collect();
let returns_std = {
let mean = returns.iter().sum::<f64>() / returns.len() as f64;
let variance =
returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / returns.len() as f64;
variance.sqrt()
};
let returns_score = (returns_std / 0.02).min(1.0); // 2% daily volatility baseline
// Combined stress score
volatility_score * 0.4 + volume_score * 0.3 + returns_score * 0.3
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circuit_breaker_creation() -> Result<()> {
let config = CircuitBreakerConfig::default();
let breaker = MLCircuitBreaker::new(config);
assert!(breaker.is_ok());
let breaker = breaker?;
assert!(!breaker.is_any_triggered());
assert_eq!(breaker.get_triggered_breakers().len(), 0);
Ok(())
}
#[test]
fn test_circuit_breaker_state() {
let mut state = CircuitBreakerState::new(CircuitBreakerType::Volatility, 0.05);
assert!(!state.is_triggered);
assert_eq!(state.trigger_count, 0);
state.trigger(0.08);
assert!(state.is_triggered);
assert_eq!(state.trigger_count, 1);
assert_eq!(state.trigger_value, 0.08);
state.reset();
assert!(!state.is_triggered);
assert_eq!(state.trigger_value, 0.0);
}
#[test]
fn test_volatility_circuit_breaker() -> Result<()> {
let config = CircuitBreakerConfig::default();
let mut breaker = MLCircuitBreaker::new(config)?;
// Normal volatility - should not trigger
let normal_data = MarketDataPoint {
timestamp: Utc::now(),
price: Price::from_f64(100.0).unwrap(),
volume: Volume::from_f64(1000.0).unwrap(),
volatility: 0.02, // 2% - below threshold
returns: 0.01,
};
let triggered = breaker.update_market_data(normal_data)?;
assert!(triggered.is_empty());
assert!(!breaker.is_any_triggered());
// High volatility - should trigger
let high_vol_data = MarketDataPoint {
timestamp: Utc::now(),
price: Price::from_f64(105.0).unwrap(),
volume: Volume::from_f64(1000.0).unwrap(),
volatility: 0.08, // 8% - above 5% threshold
returns: 0.05,
};
let triggered = breaker.update_market_data(high_vol_data)?;
assert!(!triggered.is_empty());
assert!(triggered.contains(&CircuitBreakerType::Volatility));
assert!(breaker.is_triggered(CircuitBreakerType::Volatility));
Ok(())
}
#[test]
fn test_model_performance_circuit_breaker() -> Result<()> {
let config = CircuitBreakerConfig::default();
let mut breaker = MLCircuitBreaker::new(config)?;
// Good performance - should not trigger
let good_triggered = breaker.update_model_performance(0.95)?;
assert!(!good_triggered);
assert!(!breaker.is_triggered(CircuitBreakerType::ModelPerformance));
// Poor performance - should trigger
let poor_triggered = breaker.update_model_performance(0.60)?; // Below 80% threshold
assert!(poor_triggered);
assert!(breaker.is_triggered(CircuitBreakerType::ModelPerformance));
Ok(())
}
#[test]
fn test_circuit_breaker_reset() -> Result<()> {
let config = CircuitBreakerConfig::default();
let mut breaker = MLCircuitBreaker::new(config)?;
// Trigger a circuit breaker
breaker.update_model_performance(0.60)?;
assert!(breaker.is_triggered(CircuitBreakerType::ModelPerformance));
// Manual reset
breaker.reset_breaker(CircuitBreakerType::ModelPerformance)?;
assert!(!breaker.is_triggered(CircuitBreakerType::ModelPerformance));
Ok(())
}
#[test]
fn test_market_stress_calculation() -> Result<()> {
let config = CircuitBreakerConfig::default();
let mut breaker = MLCircuitBreaker::new(config)?;
// Add some historical data
for i in 0..20 {
let data = MarketDataPoint {
timestamp: Utc::now(),
price: Price::from_f64(100.0 + i as f64).unwrap(),
volume: Volume::from_f64(1000.0).unwrap(),
volatility: 0.02,
returns: 0.01,
};
breaker.update_market_data(data)?;
}
let stress_score = breaker.calculate_market_stress();
assert!(stress_score >= 0.0 && stress_score <= 1.0);
Ok(())
}
}