1use super::*;
2
3pub struct RiskAssessor {
5 risk_models: HashMap<String, RiskModel>,
6 risk_metrics: HashMap<String, RiskMetric>,
7 scenario_analyzer: ScenarioAnalyzer,
8}
9
10#[derive(Debug, Clone)]
12pub struct RiskModel {
13 pub model_id: String,
14 pub model_type: RiskModelType,
15 pub parameters: RiskModelParameters,
16 pub validation_results: ValidationResults,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub enum RiskModelType {
22 VaR,
23 CVaR,
24 MonteCarlo,
25 Historical,
26 Parametric,
27 StressTest,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct RiskModelParameters {
33 pub confidence_level: f64,
34 pub time_horizon: u32,
35 pub lookback_period: u32,
36 pub simulation_count: u32,
37}
38
39#[derive(Debug, Clone)]
41pub struct ValidationResults {
42 pub backtest_results: BacktestResults,
43 pub model_accuracy: f64,
44 pub calibration_quality: f64,
45}
46
47#[derive(Debug, Clone)]
49pub struct BacktestResults {
50 pub period: (u64, u64),
51 pub hit_rate: f64,
52 pub average_loss: f64,
53 pub maximum_loss: f64,
54 pub sharpe_ratio: f64,
55}
56
57#[derive(Debug, Clone)]
59pub struct RiskMetric {
60 pub metric_id: String,
61 pub metric_name: String,
62 pub metric_type: MetricType,
63 pub value: f64,
64 pub timestamp: u64,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub enum MetricType {
70 VaR,
71 CVaR,
72 Volatility,
73 Beta,
74 Alpha,
75 Sharpe,
76 Sortino,
77}
78
79pub struct ScenarioAnalyzer {
81 scenarios: HashMap<String, Scenario>,
82 stress_tests: HashMap<String, StressTest>,
83 sensitivity_analyzer: SensitivityAnalyzer,
84 market_scenarios: Vec<MarketScenario>,
87}
88
89#[derive(Debug, Clone)]
91pub struct Scenario {
92 pub scenario_id: String,
93 pub scenario_name: String,
94 pub scenario_type: ScenarioType,
95 pub parameters: ScenarioParameters,
96 pub probability: f64,
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub enum ScenarioType {
102 Economic,
103 Market,
104 Geopolitical,
105 Environmental,
106 Regulatory,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ScenarioParameters {
112 pub market_shocks: HashMap<String, f64>,
113 pub interest_rate_changes: HashMap<String, f64>,
114 pub currency_movements: HashMap<String, f64>,
115 pub commodity_price_changes: HashMap<String, f64>,
116}
117
118#[derive(Debug, Clone)]
120pub struct StressTest {
121 pub test_id: String,
122 pub test_name: String,
123 pub test_type: StressTestType,
124 pub scenarios: Vec<String>,
125 pub results: StressTestResults,
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub enum StressTestType {
131 Historical,
132 Hypothetical,
133 Reverse,
134 Custom,
135}
136
137#[derive(Debug, Clone)]
139pub struct StressTestResults {
140 pub portfolio_value_change: f64,
141 pub worst_case_loss: f64,
142 pub recovery_time: u32,
143 pub affected_assets: Vec<String>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct MarketScenario {
153 pub name: String,
154 pub probability: f64,
155 pub shocks: HashMap<String, f64>,
156}
157
158impl MarketScenario {
159 pub fn new(name: impl Into<String>, probability: f64, shocks: HashMap<String, f64>) -> Self {
160 Self {
161 name: name.into(),
162 probability,
163 shocks,
164 }
165 }
166}
167
168#[derive(Debug, Clone, PartialEq)]
177pub struct StressTestResult {
178 pub var_95: f64,
180 pub var_99: f64,
182 pub expected_shortfall: f64,
184 pub max_drawdown: f64,
186 pub probability_of_loss: f64,
188 pub mean_portfolio_value: f64,
190 pub std_dev: f64,
192 pub num_simulations: usize,
194}
195
196#[derive(Debug, Clone, PartialEq)]
198pub struct ScenarioResult {
199 pub scenario_name: String,
201 pub portfolio_impact: f64,
203 pub final_value: f64,
205 pub probability: f64,
207}
208
209pub struct SensitivityAnalyzer {
211 sensitivity_factors: HashMap<String, SensitivityFactor>,
212 correlation_matrix: CorrelationMatrix,
213}
214
215#[derive(Debug, Clone)]
217pub struct SensitivityFactor {
218 pub factor_id: String,
219 pub factor_name: String,
220 pub factor_type: FactorType,
221 pub sensitivity: f64,
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226pub enum FactorType {
227 InterestRate,
228 Equity,
229 Credit,
230 Currency,
231 Commodity,
232}
233
234#[derive(Debug, Clone)]
236pub struct CorrelationMatrix {
237 pub assets: Vec<String>,
238 pub correlations: Vec<Vec<f64>>,
239 pub last_updated: u64,
240}
241
242pub struct RiskAnalyzer {
244 risk_models: HashMap<String, RiskModel>,
245 risk_metrics: HashMap<String, RiskMetric>,
246 scenario_analyzer: ScenarioAnalyzer,
247 benchmark_comparator: BenchmarkComparator,
251 active_benchmark: Option<String>,
253}
254
255impl RiskAssessor {
256 pub fn new() -> Self {
257 Self {
258 risk_models: HashMap::new(),
259 risk_metrics: HashMap::new(),
260 scenario_analyzer: ScenarioAnalyzer::new(),
261 }
262 }
263
264 pub fn initialize(&mut self) -> Result<(), FinancialError> {
265 self.scenario_analyzer.initialize()?;
266 Ok(())
267 }
268
269 pub fn add_risk_model(&mut self, model: RiskModel) {
270 self.risk_models.insert(model.model_id.clone(), model);
271 }
272
273 pub fn get_risk_model(&self, model_id: &str) -> Option<&RiskModel> {
274 self.risk_models.get(model_id)
275 }
276
277 pub fn list_risk_models(&self) -> Vec<String> {
278 self.risk_models.keys().cloned().collect()
279 }
280
281 pub fn add_risk_metric(&mut self, metric: RiskMetric) {
282 self.risk_metrics.insert(metric.metric_id.clone(), metric);
283 }
284
285 pub fn get_risk_metric(&self, metric_id: &str) -> Option<&RiskMetric> {
286 self.risk_metrics.get(metric_id)
287 }
288
289 pub fn list_risk_metrics(&self) -> Vec<String> {
290 self.risk_metrics.keys().cloned().collect()
291 }
292}
293
294impl ScenarioAnalyzer {
295 pub fn new() -> Self {
296 Self {
297 scenarios: HashMap::new(),
298 stress_tests: HashMap::new(),
299 sensitivity_analyzer: SensitivityAnalyzer::new(),
300 market_scenarios: Vec::new(),
301 }
302 }
303
304 pub fn initialize(&mut self) -> Result<(), FinancialError> {
305 Ok(())
306 }
307
308 pub fn add_scenario(&mut self, scenario: MarketScenario) {
310 self.market_scenarios.push(scenario);
311 }
312
313 pub fn run_monte_carlo(
328 &self,
329 portfolio: &Portfolio,
330 num_simulations: usize,
331 volatility: f64,
332 ) -> Result<StressTestResult, FinancialError> {
333 if num_simulations == 0 {
334 return Err(FinancialError::ValidationError(
335 "num_simulations must be greater than zero".to_string(),
336 ));
337 }
338 if volatility < 0.0 {
339 return Err(FinancialError::ValidationError(
340 "volatility must be non-negative".to_string(),
341 ));
342 }
343 if portfolio.assets.is_empty() {
344 return Err(FinancialError::PortfolioError(
345 "portfolio has no assets to simulate".to_string(),
346 ));
347 }
348
349 let initial_value: f64 =
350 portfolio.assets.iter().map(|a| a.market_value).sum::<f64>() + portfolio.cash_balance;
351 if !(initial_value > 0.0) {
352 return Err(FinancialError::PortfolioError(
353 "portfolio has no positive value to simulate".to_string(),
354 ));
355 }
356
357 let mut rng = McRng::new(0x9E37_79B9_7F4A_7C15);
359
360 let mut values: Vec<f64> = Vec::with_capacity(num_simulations);
361 for _ in 0..num_simulations {
362 let mut sim_value = portfolio.cash_balance;
363 for asset in &portfolio.assets {
364 let z = if volatility > 0.0 {
365 rng.next_normal()
366 } else {
367 0.0
368 };
369 let shock = z * volatility;
370 let new_price = asset.current_price * (1.0 + shock);
371 sim_value += new_price * asset.quantity;
372 }
373 values.push(sim_value);
374 }
375
376 Ok(aggregate_stress_test_result(
377 &values,
378 initial_value,
379 num_simulations,
380 ))
381 }
382
383 pub fn run_scenarios(
387 &self,
388 portfolio: &Portfolio,
389 ) -> Result<Vec<ScenarioResult>, FinancialError> {
390 if portfolio.assets.is_empty() {
391 return Err(FinancialError::PortfolioError(
392 "portfolio has no assets to stress".to_string(),
393 ));
394 }
395
396 let initial_value: f64 =
397 portfolio.assets.iter().map(|a| a.market_value).sum::<f64>() + portfolio.cash_balance;
398
399 let mut results = Vec::with_capacity(self.market_scenarios.len());
400 for scenario in &self.market_scenarios {
401 let mut final_value = portfolio.cash_balance;
402 for asset in &portfolio.assets {
403 let shock = scenario.shocks.get(&asset.asset_id).copied().unwrap_or(0.0);
404 let new_price = asset.current_price * (1.0 + shock);
405 final_value += new_price * asset.quantity;
406 }
407 results.push(ScenarioResult {
408 scenario_name: scenario.name.clone(),
409 portfolio_impact: final_value - initial_value,
410 final_value,
411 probability: scenario.probability,
412 });
413 }
414 Ok(results)
415 }
416
417 pub fn add_named_scenario(&mut self, scenario: Scenario) {
418 self.scenarios
419 .insert(scenario.scenario_id.clone(), scenario);
420 }
421
422 pub fn get_named_scenario(&self, scenario_id: &str) -> Option<&Scenario> {
423 self.scenarios.get(scenario_id)
424 }
425
426 pub fn list_named_scenarios(&self) -> Vec<String> {
427 self.scenarios.keys().cloned().collect()
428 }
429
430 pub fn add_stress_test(&mut self, test: StressTest) {
431 self.stress_tests.insert(test.test_id.clone(), test);
432 }
433
434 pub fn get_stress_test(&self, test_id: &str) -> Option<&StressTest> {
435 self.stress_tests.get(test_id)
436 }
437
438 pub fn list_stress_tests(&self) -> Vec<String> {
439 self.stress_tests.keys().cloned().collect()
440 }
441
442 pub fn sensitivity_analyzer(&self) -> &SensitivityAnalyzer {
443 &self.sensitivity_analyzer
444 }
445
446 pub fn sensitivity_analyzer_mut(&mut self) -> &mut SensitivityAnalyzer {
447 &mut self.sensitivity_analyzer
448 }
449}
450
451fn aggregate_stress_test_result(
456 values: &[f64],
457 initial_value: f64,
458 num_simulations: usize,
459) -> StressTestResult {
460 let n = values.len();
461 let mean: f64 = values.iter().sum::<f64>() / n as f64;
462 let variance: f64 = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
463 let std_dev = variance.sqrt();
464
465 let mut losses: Vec<f64> = values.iter().map(|v| initial_value - v).collect();
467 losses.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
468
469 let percentile = |p: f64| -> f64 {
472 if n == 1 {
473 return losses[0];
474 }
475 let rank = (p * (n - 1) as f64).round() as usize;
476 losses[rank.min(n - 1)]
477 };
478
479 let var_95 = percentile(0.95).max(0.0);
480 let var_99 = percentile(0.99).max(0.0);
481
482 let tail_threshold = percentile(0.95);
484 let tail_losses: Vec<f64> = losses
485 .iter()
486 .filter(|&&l| l >= tail_threshold)
487 .copied()
488 .collect();
489 let expected_shortfall = if tail_losses.is_empty() {
490 var_95
491 } else {
492 tail_losses.iter().sum::<f64>() / tail_losses.len() as f64
493 };
494
495 let max_drawdown = losses.last().copied().unwrap_or(0.0).max(0.0);
496
497 let num_losses = values.iter().filter(|v| **v < initial_value).count() as f64;
498 let probability_of_loss = num_losses / n as f64;
499
500 StressTestResult {
501 var_95,
502 var_99,
503 expected_shortfall,
504 max_drawdown,
505 probability_of_loss,
506 mean_portfolio_value: mean,
507 std_dev,
508 num_simulations,
509 }
510}
511
512struct McRng {
518 state: u64,
519}
520
521impl McRng {
522 fn new(seed: u64) -> Self {
523 Self {
525 state: if seed == 0 {
526 0x9E37_79B9_7F4A_7C15
527 } else {
528 seed
529 },
530 }
531 }
532
533 fn next_u64(&mut self) -> u64 {
535 self.state = self
537 .state
538 .wrapping_mul(6364136223846793005)
539 .wrapping_add(1442695040888963407);
540 self.state
541 }
542
543 fn next_uniform(&mut self) -> f64 {
545 let x = self.next_u64() >> 11;
547 (x as f64) * (1.0 / (1u64 << 53) as f64)
548 }
549
550 fn next_normal(&mut self) -> f64 {
552 let mut u1 = self.next_uniform();
554 if u1 < f64::MIN_POSITIVE {
555 u1 = f64::MIN_POSITIVE;
556 }
557 let u2 = self.next_uniform();
558 let r = (-2.0 * u1.ln()).sqrt();
559 let theta = 2.0 * std::f64::consts::PI * u2;
560 r * theta.cos()
561 }
562}
563
564impl SensitivityAnalyzer {
565 pub fn new() -> Self {
566 Self {
567 sensitivity_factors: HashMap::new(),
568 correlation_matrix: CorrelationMatrix::new(),
569 }
570 }
571
572 pub fn add_factor(&mut self, factor: SensitivityFactor) {
573 self.sensitivity_factors
574 .insert(factor.factor_id.clone(), factor);
575 }
576
577 pub fn get_factor(&self, factor_id: &str) -> Option<&SensitivityFactor> {
578 self.sensitivity_factors.get(factor_id)
579 }
580
581 pub fn list_factors(&self) -> Vec<String> {
582 self.sensitivity_factors.keys().cloned().collect()
583 }
584
585 pub fn correlation_matrix(&self) -> &CorrelationMatrix {
586 &self.correlation_matrix
587 }
588
589 pub fn set_correlation_matrix(&mut self, matrix: CorrelationMatrix) {
590 self.correlation_matrix = matrix;
591 }
592}
593
594impl CorrelationMatrix {
595 pub fn new() -> Self {
596 Self {
597 assets: Vec::new(),
598 correlations: Vec::new(),
599 last_updated: 0,
600 }
601 }
602}
603
604impl RiskAnalyzer {
605 pub fn new() -> Self {
606 Self {
607 risk_models: HashMap::new(),
608 risk_metrics: HashMap::new(),
609 scenario_analyzer: ScenarioAnalyzer::new(),
610 benchmark_comparator: BenchmarkComparator::new(),
611 active_benchmark: None,
612 }
613 }
614
615 pub fn initialize(&mut self) -> Result<(), FinancialError> {
616 self.scenario_analyzer.initialize()?;
617 Ok(())
618 }
619
620 pub fn add_benchmark(&mut self, name: &str, returns: Vec<f64>) {
624 self.benchmark_comparator.add_benchmark(name, returns);
625 if self.active_benchmark.is_none() {
626 self.active_benchmark = Some(name.to_string());
627 }
628 }
629
630 pub fn set_active_benchmark(&mut self, name: Option<&str>) {
633 self.active_benchmark = name.map(|s| s.to_string());
634 }
635
636 pub fn calculate_risk_metrics(
637 &self,
638 portfolio: &Portfolio,
639 ) -> Result<RiskMetrics, FinancialError> {
640 let benchmark_returns = self
650 .active_benchmark
651 .as_deref()
652 .and_then(|name| self.benchmark_comparator.benchmark_returns(name));
653 let mut metrics = portfolio_risk::compute_risk_metrics(portfolio, benchmark_returns)?;
654
655 metrics.risk_profile_assessment =
660 assess_risk_profile(&portfolio.risk_profile.risk_tolerance, &metrics);
661 Ok(metrics)
662 }
663
664 pub fn add_risk_model(&mut self, model: RiskModel) {
665 self.risk_models.insert(model.model_id.clone(), model);
666 }
667
668 pub fn get_risk_model(&self, model_id: &str) -> Option<&RiskModel> {
669 self.risk_models.get(model_id)
670 }
671
672 pub fn list_risk_models(&self) -> Vec<String> {
673 self.risk_models.keys().cloned().collect()
674 }
675
676 pub fn add_risk_metric(&mut self, metric: RiskMetric) {
677 self.risk_metrics.insert(metric.metric_id.clone(), metric);
678 }
679
680 pub fn get_risk_metric(&self, metric_id: &str) -> Option<&RiskMetric> {
681 self.risk_metrics.get(metric_id)
682 }
683
684 pub fn list_risk_metrics(&self) -> Vec<String> {
685 self.risk_metrics.keys().cloned().collect()
686 }
687}
688
689fn assess_risk_profile(tolerance: &RiskTolerance, metrics: &RiskMetrics) -> Option<String> {
693 let (max_vol, max_var): (f64, f64) = match tolerance {
697 RiskTolerance::Conservative => (0.10, 0.05),
698 RiskTolerance::Moderate => (0.20, 0.10),
699 RiskTolerance::Aggressive => (0.35, 0.18),
700 RiskTolerance::VeryAggressive => (f64::INFINITY, f64::INFINITY),
701 };
702 let over_vol = metrics.volatility > max_vol;
703 let over_var = metrics.var_95 > max_var;
704 if over_vol || over_var {
705 let label = match tolerance {
706 RiskTolerance::Conservative => "Conservative",
707 RiskTolerance::Moderate => "Moderate",
708 RiskTolerance::Aggressive => "Aggressive",
709 RiskTolerance::VeryAggressive => "VeryAggressive",
710 };
711 Some(format!(
712 "Portfolio declared as {label} but computed risk exceeds its tolerance band \
713 (volatility {:.4} > limit {:.4}, VaR(95%) {:.4} > limit {:.4}).",
714 metrics.volatility, max_vol, metrics.var_95, max_var,
715 ))
716 } else {
717 None
718 }
719}
720
721impl RiskModel {
722 pub fn new() -> Self {
723 Self {
724 model_id: "model_1".to_string(),
725 model_type: RiskModelType::VaR,
726 parameters: RiskModelParameters::new(),
727 validation_results: ValidationResults::new(),
728 }
729 }
730}
731
732impl RiskModelParameters {
733 pub fn new() -> Self {
734 Self {
735 confidence_level: 0.95,
736 time_horizon: 1,
737 lookback_period: 252,
738 simulation_count: 10000,
739 }
740 }
741}
742
743impl ValidationResults {
744 pub fn new() -> Self {
745 Self {
746 backtest_results: BacktestResults::new(),
747 model_accuracy: 0.0,
749 calibration_quality: 0.0,
750 }
751 }
752}
753
754impl BacktestResults {
755 pub fn new() -> Self {
756 Self {
757 period: (0, 86400 * 365), hit_rate: 0.95,
759 average_loss: 1000.0,
760 maximum_loss: 5000.0,
761 sharpe_ratio: 1.5,
762 }
763 }
764}
765
766impl RiskMetric {
767 pub fn new() -> Self {
768 Self {
769 metric_id: "metric_1".to_string(),
770 metric_name: "VaR".to_string(),
771 metric_type: MetricType::VaR,
772 value: 1000.0,
773 timestamp: 0,
774 }
775 }
776}
777
778impl Scenario {
779 pub fn new() -> Self {
780 Self {
781 scenario_id: "scenario_1".to_string(),
782 scenario_name: "Market crash".to_string(),
783 scenario_type: ScenarioType::Market,
784 parameters: ScenarioParameters::new(),
785 probability: 0.05,
786 }
787 }
788}
789
790impl ScenarioParameters {
791 pub fn new() -> Self {
792 Self {
793 market_shocks: HashMap::new(),
794 interest_rate_changes: HashMap::new(),
795 currency_movements: HashMap::new(),
796 commodity_price_changes: HashMap::new(),
797 }
798 }
799}
800
801impl StressTest {
802 pub fn new() -> Self {
803 Self {
804 test_id: "test_1".to_string(),
805 test_name: "Market stress test".to_string(),
806 test_type: StressTestType::Historical,
807 scenarios: vec!["scenario_1".to_string()],
808 results: StressTestResults::new(),
809 }
810 }
811}
812
813impl StressTestResults {
814 pub fn new() -> Self {
815 Self {
816 portfolio_value_change: -0.2,
817 worst_case_loss: 20000.0,
818 recovery_time: 30,
819 affected_assets: vec!["asset_1".to_string()],
820 }
821 }
822}
823
824impl SensitivityFactor {
825 pub fn new() -> Self {
826 Self {
827 factor_id: "factor_1".to_string(),
828 factor_name: "Interest rate".to_string(),
829 factor_type: FactorType::InterestRate,
830 sensitivity: 0.5,
831 }
832 }
833}