Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs
Line
Count
Source
1
//! Risk management configuration structures
2
//!
3
//! Provides configuration types for risk management components including
4
//! stress testing scenarios, asset class definitions, and market shock parameters.
5
6
use serde::{Deserialize, Serialize};
7
use std::collections::HashMap;
8
9
/// Configuration for stress testing scenarios
10
///
11
/// Defines how stress scenarios are configured and applied to portfolios.
12
/// Supports both individual instrument shocks and asset class-based shocks
13
/// for more flexible and maintainable stress testing.
14
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15
pub struct StressScenarioConfig {
16
    /// Unique identifier for this stress test scenario
17
    pub id: String,
18
    /// Human-readable name describing the scenario
19
    pub name: String,
20
    /// Description of the stress scenario and its historical context
21
    pub description: String,
22
    /// Individual instrument-specific shocks (symbol -> shock percentage)
23
    pub instrument_shocks: HashMap<String, f64>,
24
    /// Asset class-based shocks that apply to all instruments in a class
25
    pub asset_class_shocks: HashMap<AssetClass, f64>,
26
    /// Global volatility multiplier to apply across all instruments
27
    pub volatility_multiplier: f64,
28
    /// Asset class-specific volatility multipliers
29
    pub volatility_multipliers: HashMap<AssetClass, f64>,
30
    /// Correlation adjustments between asset classes
31
    pub correlation_adjustments: HashMap<String, f64>,
32
    /// Liquidity haircuts to apply per asset class
33
    pub liquidity_haircuts: HashMap<AssetClass, f64>,
34
    /// Whether this scenario is active and available for use
35
    pub is_active: bool,
36
}
37
38
/// Asset class definitions for grouping instruments
39
///
40
/// Provides a hierarchical way to apply stress shocks to groups
41
/// of related instruments rather than hardcoding individual symbols.
42
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
43
pub enum AssetClass {
44
    /// Large-cap US equities (S&P 500 companies)
45
    LargeCapEquity,
46
    /// Small-cap US equities
47
    SmallCapEquity,
48
    /// Technology sector equities
49
    Technology,
50
    /// Financial sector equities
51
    Financials,
52
    /// Healthcare sector equities
53
    Healthcare,
54
    /// Energy sector equities
55
    Energy,
56
    /// Consumer discretionary equities
57
    ConsumerDiscretionary,
58
    /// Consumer staples equities
59
    ConsumerStaples,
60
    /// Industrial sector equities
61
    Industrials,
62
    /// Materials sector equities
63
    Materials,
64
    /// Real estate sector equities
65
    RealEstate,
66
    /// Utilities sector equities
67
    Utilities,
68
    /// Communication services sector equities
69
    CommunicationServices,
70
    /// US Treasury bonds
71
    USBonds,
72
    /// Corporate bonds
73
    CorporateBonds,
74
    /// High-yield bonds
75
    HighYieldBonds,
76
    /// International developed market equities
77
    InternationalEquity,
78
    /// Emerging market equities
79
    EmergingMarkets,
80
    /// Commodities
81
    Commodities,
82
    /// Foreign exchange
83
    ForeignExchange,
84
    /// Cryptocurrencies
85
    Crypto,
86
    /// Alternative investments
87
    Alternatives,
88
}
89
90
/// Asset class mapping configuration
91
///
92
/// Maps individual instrument symbols to their asset classes for
93
/// applying class-based stress shocks and risk calculations.
94
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95
pub struct AssetClassMapping {
96
    /// Symbol to asset class mappings
97
    pub mappings: HashMap<String, AssetClass>,
98
    /// Default asset class for unmapped symbols
99
    pub default_class: AssetClass,
100
}
101
102
/// Complete risk configuration containing all risk-related settings
103
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104
pub struct RiskConfig {
105
    /// Available stress test scenarios
106
    pub stress_scenarios: Vec<StressScenarioConfig>,
107
    /// Asset class mappings for instruments
108
    pub asset_class_mapping: AssetClassMapping,
109
    /// Default volatility settings
110
    pub default_volatility_multiplier: f64,
111
    /// Maximum allowed portfolio loss percentage
112
    pub max_portfolio_loss_pct: f64,
113
    /// VaR confidence level (e.g., 0.95 for 95% confidence)
114
    pub var_confidence_level: f64,
115
    /// Time horizon for VaR calculations in days
116
    pub var_time_horizon_days: u32,
117
}
118
119
impl Default for RiskConfig {
120
0
    fn default() -> Self {
121
0
        Self {
122
0
            stress_scenarios: create_default_stress_scenarios(),
123
0
            asset_class_mapping: create_default_asset_class_mapping(),
124
0
            default_volatility_multiplier: 1.0,
125
0
            max_portfolio_loss_pct: 20.0,
126
0
            var_confidence_level: 0.95,
127
0
            var_time_horizon_days: 1,
128
0
        }
129
0
    }
130
}
131
132
impl StressScenarioConfig {
133
    /// Get the effective shock for a given instrument symbol
134
    ///
135
    /// Returns the instrument-specific shock if available, otherwise
136
    /// returns the asset class shock based on the symbol's asset class mapping.
137
3
    pub fn get_shock_for_symbol(
138
3
        &self,
139
3
        symbol: &str,
140
3
        asset_mapping: &AssetClassMapping,
141
3
    ) -> Option<f64> {
142
        // First check for instrument-specific shock
143
3
        if let Some(
shock1
) = self.instrument_shocks.get(symbol) {
144
1
            return Some(*shock);
145
2
        }
146
147
        // Then check for asset class shock
148
2
        if let Some(
asset_class1
) = asset_mapping.mappings.get(symbol) {
149
1
            return self.asset_class_shocks.get(asset_class).copied();
150
1
        }
151
152
        // Fall back to default asset class shock
153
1
        self.asset_class_shocks
154
1
            .get(&asset_mapping.default_class)
155
1
            .copied()
156
3
    }
157
158
    /// Get volatility multiplier for a given instrument symbol
159
0
    pub fn get_volatility_multiplier_for_symbol(
160
0
        &self,
161
0
        symbol: &str,
162
0
        asset_mapping: &AssetClassMapping,
163
0
    ) -> f64 {
164
        // Check for asset class-specific volatility multiplier
165
0
        if let Some(asset_class) = asset_mapping.mappings.get(symbol) {
166
0
            if let Some(multiplier) = self.volatility_multipliers.get(asset_class) {
167
0
                return *multiplier;
168
0
            }
169
0
        }
170
171
        // Fall back to default asset class
172
0
        if let Some(multiplier) = self
173
0
            .volatility_multipliers
174
0
            .get(&asset_mapping.default_class)
175
        {
176
0
            return *multiplier;
177
0
        }
178
179
        // Fall back to global multiplier
180
0
        self.volatility_multiplier
181
0
    }
182
}
183
184
/// Create default stress test scenarios based on historical events
185
0
fn create_default_stress_scenarios() -> Vec<StressScenarioConfig> {
186
0
    vec![
187
0
        StressScenarioConfig {
188
0
            id: "market_crash_2008".to_string(),
189
0
            name: "2008 Financial Crisis".to_string(),
190
0
            description: "Simulates the market conditions during the 2008 financial crisis with severe equity declines and financial sector stress".to_string(),
191
0
            instrument_shocks: HashMap::new(),
192
0
            asset_class_shocks: {
193
0
                let mut shocks = HashMap::new();
194
0
                shocks.insert(AssetClass::LargeCapEquity, -37.0);
195
0
                shocks.insert(AssetClass::SmallCapEquity, -45.0);
196
0
                shocks.insert(AssetClass::Financials, -55.0);
197
0
                shocks.insert(AssetClass::Technology, -40.0);
198
0
                shocks.insert(AssetClass::RealEstate, -60.0);
199
0
                shocks.insert(AssetClass::EmergingMarkets, -50.0);
200
0
                shocks.insert(AssetClass::HighYieldBonds, -25.0);
201
0
                shocks
202
0
            },
203
0
            volatility_multiplier: 2.5,
204
0
            volatility_multipliers: HashMap::new(),
205
0
            correlation_adjustments: HashMap::new(),
206
0
            liquidity_haircuts: {
207
0
                let mut haircuts = HashMap::new();
208
0
                haircuts.insert(AssetClass::SmallCapEquity, 0.15);
209
0
                haircuts.insert(AssetClass::EmergingMarkets, 0.20);
210
0
                haircuts.insert(AssetClass::HighYieldBonds, 0.10);
211
0
                haircuts
212
0
            },
213
0
            is_active: true,
214
0
        },
215
0
        StressScenarioConfig {
216
0
            id: "covid_crash_2020".to_string(),
217
0
            name: "COVID-19 Market Crash".to_string(),
218
0
            description: "Simulates the market crash of March 2020 due to COVID-19 pandemic with broad-based equity declines".to_string(),
219
0
            instrument_shocks: HashMap::new(),
220
0
            asset_class_shocks: {
221
0
                let mut shocks = HashMap::new();
222
0
                shocks.insert(AssetClass::LargeCapEquity, -34.0);
223
0
                shocks.insert(AssetClass::SmallCapEquity, -40.0);
224
0
                shocks.insert(AssetClass::Energy, -50.0);
225
0
                shocks.insert(AssetClass::Financials, -45.0);
226
0
                shocks.insert(AssetClass::RealEstate, -35.0);
227
0
                shocks.insert(AssetClass::Technology, -25.0);
228
0
                shocks.insert(AssetClass::EmergingMarkets, -45.0);
229
0
                shocks
230
0
            },
231
0
            volatility_multiplier: 3.0,
232
0
            volatility_multipliers: HashMap::new(),
233
0
            correlation_adjustments: HashMap::new(),
234
0
            liquidity_haircuts: HashMap::new(),
235
0
            is_active: true,
236
0
        },
237
0
        StressScenarioConfig {
238
0
            id: "flash_crash_2010".to_string(),
239
0
            name: "Flash Crash 2010".to_string(),
240
0
            description: "Simulates the May 6, 2010 flash crash with rapid market decline and liquidity issues".to_string(),
241
0
            instrument_shocks: HashMap::new(),
242
0
            asset_class_shocks: {
243
0
                let mut shocks = HashMap::new();
244
0
                shocks.insert(AssetClass::LargeCapEquity, -9.0);
245
0
                shocks.insert(AssetClass::SmallCapEquity, -15.0);
246
0
                shocks.insert(AssetClass::Technology, -12.0);
247
0
                shocks
248
0
            },
249
0
            volatility_multiplier: 5.0,
250
0
            volatility_multipliers: HashMap::new(),
251
0
            correlation_adjustments: HashMap::new(),
252
0
            liquidity_haircuts: {
253
0
                let mut haircuts = HashMap::new();
254
0
                haircuts.insert(AssetClass::LargeCapEquity, 0.05);
255
0
                haircuts.insert(AssetClass::SmallCapEquity, 0.20);
256
0
                haircuts.insert(AssetClass::Technology, 0.10);
257
0
                haircuts
258
0
            },
259
0
            is_active: true,
260
0
        },
261
0
        StressScenarioConfig {
262
0
            id: "volatility_spike".to_string(),
263
0
            name: "Volatility Spike".to_string(),
264
0
            description: "Simulates a sudden spike in market volatility without significant price moves".to_string(),
265
0
            instrument_shocks: HashMap::new(),
266
0
            asset_class_shocks: HashMap::new(),
267
0
            volatility_multiplier: 3.0,
268
0
            volatility_multipliers: {
269
0
                let mut multipliers = HashMap::new();
270
0
                multipliers.insert(AssetClass::SmallCapEquity, 4.0);
271
0
                multipliers.insert(AssetClass::EmergingMarkets, 3.5);
272
0
                multipliers.insert(AssetClass::HighYieldBonds, 2.5);
273
0
                multipliers
274
0
            },
275
0
            correlation_adjustments: HashMap::new(),
276
0
            liquidity_haircuts: HashMap::new(),
277
0
            is_active: true,
278
0
        },
279
0
        StressScenarioConfig {
280
0
            id: "interest_rate_shock".to_string(),
281
0
            name: "Interest Rate Shock".to_string(),
282
0
            description: "Simulates a sudden rise in interest rates affecting bonds and rate-sensitive sectors".to_string(),
283
0
            instrument_shocks: HashMap::new(),
284
0
            asset_class_shocks: {
285
0
                let mut shocks = HashMap::new();
286
0
                shocks.insert(AssetClass::USBonds, -8.0);
287
0
                shocks.insert(AssetClass::CorporateBonds, -12.0);
288
0
                shocks.insert(AssetClass::RealEstate, -15.0);
289
0
                shocks.insert(AssetClass::Utilities, -10.0);
290
0
                shocks.insert(AssetClass::Financials, 5.0); // Banks benefit from higher rates
291
0
                shocks
292
0
            },
293
0
            volatility_multiplier: 1.5,
294
0
            volatility_multipliers: HashMap::new(),
295
0
            correlation_adjustments: HashMap::new(),
296
0
            liquidity_haircuts: HashMap::new(),
297
0
            is_active: true,
298
0
        },
299
    ]
300
0
}
301
302
/// Create default asset class mapping for common symbols
303
2
fn create_default_asset_class_mapping() -> AssetClassMapping {
304
2
    let mut mappings = HashMap::new();
305
306
    // Large Cap Technology
307
2
    mappings.insert("AAPL".to_string(), AssetClass::Technology);
308
2
    mappings.insert("MSFT".to_string(), AssetClass::Technology);
309
2
    mappings.insert("GOOGL".to_string(), AssetClass::Technology);
310
2
    mappings.insert("GOOG".to_string(), AssetClass::Technology);
311
2
    mappings.insert("AMZN".to_string(), AssetClass::Technology);
312
2
    mappings.insert("META".to_string(), AssetClass::Technology);
313
2
    mappings.insert("TSLA".to_string(), AssetClass::Technology);
314
2
    mappings.insert("NVDA".to_string(), AssetClass::Technology);
315
316
    // Large Cap Financials
317
2
    mappings.insert("JPM".to_string(), AssetClass::Financials);
318
2
    mappings.insert("BAC".to_string(), AssetClass::Financials);
319
2
    mappings.insert("WFC".to_string(), AssetClass::Financials);
320
2
    mappings.insert("GS".to_string(), AssetClass::Financials);
321
2
    mappings.insert("MS".to_string(), AssetClass::Financials);
322
323
    // ETFs
324
2
    mappings.insert("SPY".to_string(), AssetClass::LargeCapEquity);
325
2
    mappings.insert("QQQ".to_string(), AssetClass::Technology);
326
2
    mappings.insert("IWM".to_string(), AssetClass::SmallCapEquity);
327
2
    mappings.insert("VTI".to_string(), AssetClass::LargeCapEquity);
328
2
    mappings.insert("EEM".to_string(), AssetClass::EmergingMarkets);
329
2
    mappings.insert("VEA".to_string(), AssetClass::InternationalEquity);
330
2
    mappings.insert("TLT".to_string(), AssetClass::USBonds);
331
2
    mappings.insert("HYG".to_string(), AssetClass::HighYieldBonds);
332
333
    // Healthcare
334
2
    mappings.insert("JNJ".to_string(), AssetClass::Healthcare);
335
2
    mappings.insert("PFE".to_string(), AssetClass::Healthcare);
336
2
    mappings.insert("UNH".to_string(), AssetClass::Healthcare);
337
338
    // Energy
339
2
    mappings.insert("XOM".to_string(), AssetClass::Energy);
340
2
    mappings.insert("CVX".to_string(), AssetClass::Energy);
341
342
2
    AssetClassMapping {
343
2
        mappings,
344
2
        default_class: AssetClass::LargeCapEquity,
345
2
    }
346
2
}
347
348
#[cfg(test)]
349
mod tests {
350
    use super::*;
351
352
    #[test]
353
1
    fn test_stress_scenario_config_creation() {
354
1
        let config = StressScenarioConfig {
355
1
            id: "test".to_string(),
356
1
            name: "Test Scenario".to_string(),
357
1
            description: "Test description".to_string(),
358
1
            instrument_shocks: HashMap::new(),
359
1
            asset_class_shocks: {
360
1
                let mut shocks = HashMap::new();
361
1
                shocks.insert(AssetClass::Technology, -10.0);
362
1
                shocks
363
1
            },
364
1
            volatility_multiplier: 2.0,
365
1
            volatility_multipliers: HashMap::new(),
366
1
            correlation_adjustments: HashMap::new(),
367
1
            liquidity_haircuts: HashMap::new(),
368
1
            is_active: true,
369
1
        };
370
371
1
        assert_eq!(config.id, "test");
372
1
        assert_eq!(config.volatility_multiplier, 2.0);
373
1
    }
374
375
    #[test]
376
1
    fn test_asset_class_mapping() {
377
1
        let mapping = create_default_asset_class_mapping();
378
379
1
        assert_eq!(mapping.mappings.get("AAPL"), Some(&AssetClass::Technology));
380
1
        assert_eq!(
381
1
            mapping.mappings.get("SPY"),
382
            Some(&AssetClass::LargeCapEquity)
383
        );
384
1
        assert_eq!(mapping.default_class, AssetClass::LargeCapEquity);
385
1
    }
386
387
    #[test]
388
1
    fn test_get_shock_for_symbol() {
389
1
        let config = StressScenarioConfig {
390
1
            id: "test".to_string(),
391
1
            name: "Test".to_string(),
392
1
            description: "Test".to_string(),
393
1
            instrument_shocks: {
394
1
                let mut shocks = HashMap::new();
395
1
                shocks.insert("AAPL".to_string(), -15.0);
396
1
                shocks
397
1
            },
398
1
            asset_class_shocks: {
399
1
                let mut shocks = HashMap::new();
400
1
                shocks.insert(AssetClass::Technology, -10.0);
401
1
                shocks.insert(AssetClass::LargeCapEquity, -5.0);
402
1
                shocks
403
1
            },
404
1
            volatility_multiplier: 1.0,
405
1
            volatility_multipliers: HashMap::new(),
406
1
            correlation_adjustments: HashMap::new(),
407
1
            liquidity_haircuts: HashMap::new(),
408
1
            is_active: true,
409
1
        };
410
411
1
        let mapping = create_default_asset_class_mapping();
412
413
        // Should get instrument-specific shock
414
1
        assert_eq!(config.get_shock_for_symbol("AAPL", &mapping), Some(-15.0));
415
416
        // Should get asset class shock for GOOGL (Technology)
417
1
        assert_eq!(config.get_shock_for_symbol("GOOGL", &mapping), Some(-10.0));
418
419
        // Should get default class shock for unknown symbol
420
1
        assert_eq!(config.get_shock_for_symbol("UNKNOWN", &mapping), Some(-5.0));
421
1
    }
422
}