/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 | | } |