1use super::{
7 ApplicationError, ApplicationResult, IndustryConstraint, IndustryObjective, IndustrySolution,
8 OptimizationProblem,
9};
10use crate::ising::IsingModel;
11use crate::qubo::{QuboBuilder, QuboFormulation};
12use crate::simulator::{AnnealingParams, ClassicalAnnealingSimulator};
13use std::collections::HashMap;
14
15use std::fmt::Write;
16#[derive(Debug, Clone)]
18pub struct PortfolioOptimization {
19 pub expected_returns: Vec<f64>,
21 pub covariance_matrix: Vec<Vec<f64>>,
23 pub budget: f64,
25 pub risk_tolerance: f64,
27 pub min_positions: Vec<f64>,
29 pub max_positions: Vec<f64>,
31 pub sector_constraints: HashMap<usize, String>,
33 pub max_sector_allocation: HashMap<String, f64>,
35 pub transaction_costs: Vec<f64>,
37 pub regulatory_constraints: Vec<IndustryConstraint>,
39}
40
41impl PortfolioOptimization {
42 pub fn new(
44 expected_returns: Vec<f64>,
45 covariance_matrix: Vec<Vec<f64>>,
46 budget: f64,
47 risk_tolerance: f64,
48 ) -> ApplicationResult<Self> {
49 let n_assets = expected_returns.len();
50
51 if covariance_matrix.len() != n_assets {
52 return Err(ApplicationError::InvalidConfiguration(
53 "Covariance matrix dimension mismatch".to_string(),
54 ));
55 }
56
57 for row in &covariance_matrix {
58 if row.len() != n_assets {
59 return Err(ApplicationError::InvalidConfiguration(
60 "Covariance matrix is not square".to_string(),
61 ));
62 }
63 }
64
65 if budget <= 0.0 {
66 return Err(ApplicationError::InvalidConfiguration(
67 "Budget must be positive".to_string(),
68 ));
69 }
70
71 Ok(Self {
72 expected_returns,
73 covariance_matrix,
74 budget,
75 risk_tolerance,
76 min_positions: vec![0.0; n_assets],
77 max_positions: vec![budget; n_assets],
78 sector_constraints: HashMap::new(),
79 max_sector_allocation: HashMap::new(),
80 transaction_costs: vec![0.0; n_assets],
81 regulatory_constraints: Vec::new(),
82 })
83 }
84
85 pub fn add_sector_constraint(
87 &mut self,
88 asset: usize,
89 sector: String,
90 max_allocation: f64,
91 ) -> ApplicationResult<()> {
92 if asset >= self.expected_returns.len() {
93 return Err(ApplicationError::InvalidConfiguration(
94 "Asset index out of bounds".to_string(),
95 ));
96 }
97
98 self.sector_constraints.insert(asset, sector.clone());
99 self.max_sector_allocation.insert(sector, max_allocation);
100 Ok(())
101 }
102
103 pub fn set_position_bounds(
105 &mut self,
106 asset: usize,
107 min: f64,
108 max: f64,
109 ) -> ApplicationResult<()> {
110 if asset >= self.expected_returns.len() {
111 return Err(ApplicationError::InvalidConfiguration(
112 "Asset index out of bounds".to_string(),
113 ));
114 }
115
116 self.min_positions[asset] = min;
117 self.max_positions[asset] = max;
118 Ok(())
119 }
120
121 #[must_use]
123 pub fn calculate_risk(&self, weights: &[f64]) -> f64 {
124 let mut risk = 0.0;
125
126 for i in 0..weights.len() {
127 for j in 0..weights.len() {
128 risk += weights[i] * weights[j] * self.covariance_matrix[i][j];
129 }
130 }
131
132 risk.sqrt()
133 }
134
135 #[must_use]
137 pub fn calculate_return(&self, weights: &[f64]) -> f64 {
138 weights
139 .iter()
140 .zip(self.expected_returns.iter())
141 .map(|(w, r)| w * r)
142 .sum()
143 }
144
145 #[must_use]
147 pub fn calculate_sharpe_ratio(&self, weights: &[f64], risk_free_rate: f64) -> f64 {
148 let portfolio_return = self.calculate_return(weights);
149 let portfolio_risk = self.calculate_risk(weights);
150
151 if portfolio_risk > 1e-8 {
152 (portfolio_return - risk_free_rate) / portfolio_risk
153 } else {
154 0.0
155 }
156 }
157}
158
159impl OptimizationProblem for PortfolioOptimization {
160 type Solution = PortfolioSolution;
161 type ObjectiveValue = f64;
162
163 fn description(&self) -> String {
164 format!(
165 "Portfolio optimization with {} assets, budget ${:.2}, risk tolerance {:.3}",
166 self.expected_returns.len(),
167 self.budget,
168 self.risk_tolerance
169 )
170 }
171
172 fn size_metrics(&self) -> HashMap<String, usize> {
173 let mut metrics = HashMap::new();
174 metrics.insert("num_assets".to_string(), self.expected_returns.len());
175 metrics.insert("num_sectors".to_string(), self.max_sector_allocation.len());
176 metrics.insert(
177 "num_constraints".to_string(),
178 self.regulatory_constraints.len(),
179 );
180 metrics
181 }
182
183 fn validate(&self) -> ApplicationResult<()> {
184 if self.expected_returns.is_empty() {
185 return Err(ApplicationError::DataValidationError(
186 "No assets provided".to_string(),
187 ));
188 }
189
190 if self.budget <= 0.0 {
191 return Err(ApplicationError::DataValidationError(
192 "Budget must be positive".to_string(),
193 ));
194 }
195
196 if self.risk_tolerance < 0.0 {
197 return Err(ApplicationError::DataValidationError(
198 "Risk tolerance must be non-negative".to_string(),
199 ));
200 }
201
202 for i in 0..self.covariance_matrix.len() {
204 if self.covariance_matrix[i][i] < 0.0 {
205 return Err(ApplicationError::DataValidationError(
206 "Covariance matrix has negative diagonal elements".to_string(),
207 ));
208 }
209 }
210
211 Ok(())
212 }
213
214 fn to_qubo(&self) -> ApplicationResult<(crate::ising::QuboModel, HashMap<String, usize>)> {
215 let n_assets = self.expected_returns.len();
216 let precision = 100; let mut builder = QuboBuilder::new();
219
220 let mut var_map = HashMap::new();
222 let mut var_counter = 0;
223
224 for asset in 0..n_assets {
225 for level in 0..precision {
226 var_map.insert((asset, level), var_counter);
227 var_counter += 1;
228 }
229 }
230
231 for asset in 0..n_assets {
233 for level in 0..precision {
234 let weight = f64::from(level) / f64::from(precision);
235 let var_idx = var_map[&(asset, level)];
236
237 let return_coeff = -self.expected_returns[asset] * weight * self.budget;
239 builder.add_bias(var_idx, return_coeff);
240
241 let risk_coeff = self.risk_tolerance
243 * weight
244 * weight
245 * self.covariance_matrix[asset][asset]
246 * self.budget
247 * self.budget;
248 builder.add_bias(var_idx, risk_coeff);
249 }
250 }
251
252 for asset1 in 0..n_assets {
254 for asset2 in (asset1 + 1)..n_assets {
255 let covar = self.covariance_matrix[asset1][asset2];
256 if covar.abs() > 1e-8 {
257 for level1 in 0..precision {
258 for level2 in 0..precision {
259 let weight1 = f64::from(level1) / f64::from(precision);
260 let weight2 = f64::from(level2) / f64::from(precision);
261 let var1 = var_map[&(asset1, level1)];
262 let var2 = var_map[&(asset2, level2)];
263
264 let risk_cross = 2.0
265 * self.risk_tolerance
266 * weight1
267 * weight2
268 * covar
269 * self.budget
270 * self.budget;
271 builder.add_coupling(var1, var2, risk_cross);
272 }
273 }
274 }
275 }
276 }
277
278 let constraint_penalty = 1000.0;
280 for asset in 0..n_assets {
281 for level1 in 0..precision {
283 for level2 in (level1 + 1)..precision {
284 let var1 = var_map[&(asset, level1)];
285 let var2 = var_map[&(asset, level2)];
286 builder.add_coupling(var1, var2, constraint_penalty);
287 }
288 }
289
290 let mut constraint_bias = constraint_penalty;
292 for level in 0..precision {
293 let var_idx = var_map[&(asset, level)];
294 builder.add_bias(var_idx, -constraint_bias);
295 }
296 }
297
298 Ok((
299 builder.build(),
300 var_map
301 .into_iter()
302 .map(|((asset, level), idx)| (format!("asset_{asset}_level_{level}"), idx))
303 .collect(),
304 ))
305 }
306
307 fn evaluate_solution(
308 &self,
309 solution: &Self::Solution,
310 ) -> ApplicationResult<Self::ObjectiveValue> {
311 let portfolio_return = self.calculate_return(&solution.weights);
312 let portfolio_risk = self.calculate_risk(&solution.weights);
313
314 Ok((self.risk_tolerance * portfolio_risk).mul_add(-portfolio_risk, portfolio_return))
316 }
317
318 fn is_feasible(&self, solution: &Self::Solution) -> bool {
319 let total_investment: f64 = solution.weights.iter().sum();
321 if (total_investment - 1.0).abs() > 1e-6 {
322 return false;
323 }
324
325 for (i, &weight) in solution.weights.iter().enumerate() {
327 let position_value = weight * self.budget;
328 if position_value < self.min_positions[i] || position_value > self.max_positions[i] {
329 return false;
330 }
331 }
332
333 let mut sector_allocations = HashMap::new();
335 for (asset, sector) in &self.sector_constraints {
336 let allocation = *sector_allocations.entry(sector.clone()).or_insert(0.0);
337 sector_allocations.insert(sector.clone(), allocation + solution.weights[*asset]);
338 }
339
340 for (sector, &max_allocation) in &self.max_sector_allocation {
341 if let Some(&allocation) = sector_allocations.get(sector) {
342 if allocation > max_allocation {
343 return false;
344 }
345 }
346 }
347
348 true
349 }
350}
351
352#[derive(Debug, Clone)]
354pub struct BinaryPortfolioOptimization {
355 inner: PortfolioOptimization,
356}
357
358impl BinaryPortfolioOptimization {
359 #[must_use]
360 pub const fn new(inner: PortfolioOptimization) -> Self {
361 Self { inner }
362 }
363}
364
365impl OptimizationProblem for BinaryPortfolioOptimization {
366 type Solution = Vec<i8>;
367 type ObjectiveValue = f64;
368
369 fn description(&self) -> String {
370 self.inner.description()
371 }
372
373 fn size_metrics(&self) -> HashMap<String, usize> {
374 self.inner.size_metrics()
375 }
376
377 fn validate(&self) -> ApplicationResult<()> {
378 self.inner.validate()
379 }
380
381 fn to_qubo(&self) -> ApplicationResult<(crate::ising::QuboModel, HashMap<String, usize>)> {
382 self.inner.to_qubo()
383 }
384
385 fn evaluate_solution(
386 &self,
387 solution: &Self::Solution,
388 ) -> ApplicationResult<Self::ObjectiveValue> {
389 let num_assets = self.inner.expected_returns.len();
391 let selected_assets: Vec<usize> = solution
392 .iter()
393 .enumerate()
394 .filter(|(_, &val)| val == 1)
395 .map(|(i, _)| i % num_assets)
396 .collect();
397
398 if selected_assets.is_empty() {
399 return Ok(-1000.0); }
401
402 let weight_per_asset = 1.0 / selected_assets.len() as f64;
404 let mut weights = vec![0.0; num_assets];
405 for &asset_idx in &selected_assets {
406 weights[asset_idx] = weight_per_asset;
407 }
408
409 let portfolio_return: f64 = weights
411 .iter()
412 .zip(&self.inner.expected_returns)
413 .map(|(w, r)| w * r)
414 .sum();
415
416 Ok(portfolio_return)
417 }
418
419 fn is_feasible(&self, solution: &Self::Solution) -> bool {
420 solution.iter().any(|&x| x == 1)
422 }
423}
424
425#[derive(Debug, Clone)]
427pub struct PortfolioSolution {
428 pub weights: Vec<f64>,
430 pub metrics: PortfolioMetrics,
432}
433
434#[derive(Debug, Clone)]
436pub struct PortfolioMetrics {
437 pub expected_return: f64,
439 pub volatility: f64,
441 pub sharpe_ratio: f64,
443 pub max_drawdown: f64,
445 pub var_95: f64,
447 pub cvar_95: f64,
449}
450
451impl IndustrySolution for PortfolioSolution {
452 type Problem = PortfolioOptimization;
453
454 fn from_binary(problem: &Self::Problem, binary_solution: &[i8]) -> ApplicationResult<Self> {
455 let n_assets = problem.expected_returns.len();
456 let precision = 100;
457
458 let mut weights = vec![0.0; n_assets];
459 let mut var_idx = 0;
460
461 for asset in 0..n_assets {
462 for level in 0..precision {
463 if var_idx < binary_solution.len() && binary_solution[var_idx] == 1 {
464 weights[asset] = f64::from(level) / f64::from(precision);
465 break;
466 }
467 var_idx += 1;
468 }
469 }
470
471 let total_weight: f64 = weights.iter().sum();
473 if total_weight > 1e-8 {
474 for weight in &mut weights {
475 *weight /= total_weight;
476 }
477 }
478
479 let expected_return = problem.calculate_return(&weights);
481 let volatility = problem.calculate_risk(&weights);
482 let sharpe_ratio = problem.calculate_sharpe_ratio(&weights, 0.02); let metrics = PortfolioMetrics {
485 expected_return,
486 volatility,
487 sharpe_ratio,
488 max_drawdown: 0.0, var_95: volatility * 1.645, cvar_95: volatility * 2.0, };
492
493 Ok(Self { weights, metrics })
494 }
495
496 fn summary(&self) -> HashMap<String, String> {
497 let mut summary = HashMap::new();
498 summary.insert("type".to_string(), "Portfolio Optimization".to_string());
499 summary.insert("num_assets".to_string(), self.weights.len().to_string());
500 summary.insert(
501 "expected_return".to_string(),
502 format!("{:.2}%", self.metrics.expected_return * 100.0),
503 );
504 summary.insert(
505 "volatility".to_string(),
506 format!("{:.2}%", self.metrics.volatility * 100.0),
507 );
508 summary.insert(
509 "sharpe_ratio".to_string(),
510 format!("{:.3}", self.metrics.sharpe_ratio),
511 );
512
513 let mut indexed_weights: Vec<(usize, f64)> = self
515 .weights
516 .iter()
517 .enumerate()
518 .map(|(i, &w)| (i, w))
519 .collect();
520 indexed_weights.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
521
522 let top_positions: Vec<String> = indexed_weights
523 .iter()
524 .take(5)
525 .map(|(i, w)| format!("Asset {}: {:.1}%", i, w * 100.0))
526 .collect();
527 summary.insert("top_positions".to_string(), top_positions.join(", "));
528
529 summary
530 }
531
532 fn metrics(&self) -> HashMap<String, f64> {
533 let mut metrics = HashMap::new();
534 metrics.insert("expected_return".to_string(), self.metrics.expected_return);
535 metrics.insert("volatility".to_string(), self.metrics.volatility);
536 metrics.insert("sharpe_ratio".to_string(), self.metrics.sharpe_ratio);
537 metrics.insert("var_95".to_string(), self.metrics.var_95);
538 metrics.insert("cvar_95".to_string(), self.metrics.cvar_95);
539
540 let herfindahl_index: f64 = self.weights.iter().map(|w| w * w).sum();
542 metrics.insert("concentration_hhi".to_string(), herfindahl_index);
543
544 let max_weight = self.weights.iter().fold(0.0f64, |a, &b| a.max(b));
545 metrics.insert("max_position".to_string(), max_weight);
546
547 metrics
548 }
549
550 fn export_format(&self) -> ApplicationResult<String> {
551 use std::fmt::Write;
552
553 let mut output = String::new();
554 output.push_str("# Portfolio Allocation Report\n\n");
555
556 output.push_str("## Asset Allocation\n");
557 for (i, &weight) in self.weights.iter().enumerate() {
558 if weight > 0.001 {
559 writeln!(output, "Asset {}: {:.2}%", i, weight * 100.0)
561 .expect("Writing to String should not fail");
562 }
563 }
564
565 output.push_str("\n## Risk Metrics\n");
566 write!(
567 output,
568 "Expected Return: {:.2}%\n",
569 self.metrics.expected_return * 100.0
570 )
571 .expect("Writing to String should not fail");
572 write!(
573 output,
574 "Volatility: {:.2}%\n",
575 self.metrics.volatility * 100.0
576 )
577 .expect("Writing to String should not fail");
578 writeln!(output, "Sharpe Ratio: {:.3}", self.metrics.sharpe_ratio)
579 .expect("Writing to String should not fail");
580 writeln!(output, "VaR (95%): {:.2}%", self.metrics.var_95 * 100.0)
581 .expect("Writing to String should not fail");
582 writeln!(output, "CVaR (95%): {:.2}%", self.metrics.cvar_95 * 100.0)
583 .expect("Writing to String should not fail");
584
585 Ok(output)
586 }
587}
588
589#[derive(Debug, Clone)]
591pub struct RiskManagement {
592 pub positions: Vec<f64>,
594 pub risk_factors: HashMap<String, Vec<f64>>,
596 pub risk_limits: HashMap<String, f64>,
598 pub stress_scenarios: Vec<HashMap<String, f64>>,
600 pub market_data: HashMap<String, f64>,
602}
603
604impl RiskManagement {
605 #[must_use]
607 pub fn new(positions: Vec<f64>) -> Self {
608 Self {
609 positions,
610 risk_factors: HashMap::new(),
611 risk_limits: HashMap::new(),
612 stress_scenarios: Vec::new(),
613 market_data: HashMap::new(),
614 }
615 }
616
617 pub fn add_risk_factor(
619 &mut self,
620 name: String,
621 exposures: Vec<f64>,
622 limit: f64,
623 ) -> ApplicationResult<()> {
624 if exposures.len() != self.positions.len() {
625 return Err(ApplicationError::InvalidConfiguration(
626 "Risk factor exposure dimension mismatch".to_string(),
627 ));
628 }
629
630 self.risk_factors.insert(name.clone(), exposures);
631 self.risk_limits.insert(name, limit);
632 Ok(())
633 }
634
635 #[must_use]
637 pub fn calculate_factor_exposure(&self, factor: &str) -> f64 {
638 if let Some(exposures) = self.risk_factors.get(factor) {
639 self.positions
640 .iter()
641 .zip(exposures.iter())
642 .map(|(pos, exp)| pos * exp)
643 .sum()
644 } else {
645 0.0
646 }
647 }
648
649 #[must_use]
651 pub fn run_stress_test(&self, scenario: &HashMap<String, f64>) -> f64 {
652 let mut total_impact = 0.0;
653
654 for (factor, &shock) in scenario {
655 if let Some(exposures) = self.risk_factors.get(factor) {
656 let factor_exposure = self.calculate_factor_exposure(factor);
657 total_impact += factor_exposure * shock;
658 }
659 }
660
661 total_impact
662 }
663}
664
665#[derive(Debug, Clone)]
667pub struct CreditRiskAssessment {
668 pub applications: Vec<CreditApplication>,
670 pub risk_model: CreditRiskModel,
672 pub portfolio_constraints: Vec<IndustryConstraint>,
674}
675
676#[derive(Debug, Clone)]
678pub struct CreditApplication {
679 pub id: String,
681 pub amount: f64,
683 pub credit_score: f64,
685 pub debt_to_income: f64,
687 pub employment_years: f64,
689 pub collateral_value: f64,
691 pub purpose: String,
693 pub features: HashMap<String, f64>,
695}
696
697#[derive(Debug, Clone)]
699pub struct CreditRiskModel {
700 pub weights: HashMap<String, f64>,
702 pub risk_threshold: f64,
704 pub loss_rates: Vec<f64>,
706}
707
708impl CreditRiskAssessment {
709 #[must_use]
711 pub fn calculate_pd(&self, application: &CreditApplication) -> f64 {
712 let mut score = 0.0;
713
714 score +=
716 self.risk_model.weights.get("credit_score").unwrap_or(&0.0) * application.credit_score;
717 score += self
718 .risk_model
719 .weights
720 .get("debt_to_income")
721 .unwrap_or(&0.0)
722 * application.debt_to_income;
723 score += self
724 .risk_model
725 .weights
726 .get("employment_years")
727 .unwrap_or(&0.0)
728 * application.employment_years;
729
730 for (feature, value) in &application.features {
732 score += self.risk_model.weights.get(feature).unwrap_or(&0.0) * value;
733 }
734
735 1.0 / (1.0 + (-score).exp())
737 }
738
739 #[must_use]
741 pub fn calculate_expected_loss(&self, selection: &[bool]) -> f64 {
742 let mut total_loss = 0.0;
743
744 for (i, &selected) in selection.iter().enumerate() {
745 if selected && i < self.applications.len() {
746 let app = &self.applications[i];
747 let pd = self.calculate_pd(app);
748 let lgd = 0.45; let ead = app.amount; total_loss += pd * lgd * ead;
752 }
753 }
754
755 total_loss
756 }
757}
758
759pub fn create_benchmark_problems(
763 num_assets: usize,
764) -> ApplicationResult<Vec<Box<dyn OptimizationProblem<Solution = Vec<i8>, ObjectiveValue = f64>>>>
765{
766 let mut problems = Vec::new();
767
768 let conservative_returns: Vec<f64> = (0..num_assets)
770 .map(|i| 0.03 + 0.02 * (i as f64) / (num_assets as f64))
771 .collect();
772 let conservative_covar = create_sample_covariance_matrix(num_assets, 0.15);
773 let conservative_portfolio = PortfolioOptimization::new(
774 conservative_returns,
775 conservative_covar,
776 1_000_000.0,
777 0.5, )?;
779
780 problems.push(
781 Box::new(BinaryPortfolioOptimization::new(conservative_portfolio))
782 as Box<dyn OptimizationProblem<Solution = Vec<i8>, ObjectiveValue = f64>>,
783 );
784
785 let aggressive_returns: Vec<f64> = (0..num_assets)
787 .map(|i| 0.05 + 0.10 * (i as f64) / (num_assets as f64))
788 .collect();
789 let aggressive_covar = create_sample_covariance_matrix(num_assets, 0.25);
790 let aggressive_portfolio = PortfolioOptimization::new(
791 aggressive_returns,
792 aggressive_covar,
793 1_000_000.0,
794 0.1, )?;
796
797 problems.push(
798 Box::new(BinaryPortfolioOptimization::new(aggressive_portfolio))
799 as Box<dyn OptimizationProblem<Solution = Vec<i8>, ObjectiveValue = f64>>,
800 );
801
802 let mut sector_portfolio = PortfolioOptimization::new(
804 (0..num_assets)
805 .map(|i| 0.04 + 0.06 * (i as f64) / (num_assets as f64))
806 .collect(),
807 create_sample_covariance_matrix(num_assets, 0.20),
808 1_000_000.0,
809 0.3,
810 )?;
811
812 for i in 0..num_assets {
814 let sector = format!("Sector_{}", i % 5); sector_portfolio.add_sector_constraint(i, sector, 0.3)?; }
817
818 problems.push(Box::new(BinaryPortfolioOptimization::new(sector_portfolio))
819 as Box<
820 dyn OptimizationProblem<Solution = Vec<i8>, ObjectiveValue = f64>,
821 >);
822
823 Ok(problems)
824}
825
826fn create_sample_covariance_matrix(n: usize, base_volatility: f64) -> Vec<Vec<f64>> {
828 let mut matrix = vec![vec![0.0; n]; n];
829
830 for i in 0..n {
831 for j in 0..n {
832 if i == j {
833 matrix[i][j] =
835 base_volatility * base_volatility * (1.0 + 0.5 * (i as f64) / (n as f64));
836 } else {
837 let correlation = 0.1 * (1.0 - (i as f64 - j as f64).abs() / (n as f64));
839 let vol_i = (matrix[i][i]).sqrt();
840 let vol_j = (matrix[j][j]).sqrt();
841 matrix[i][j] = correlation * vol_i * vol_j;
842 }
843 }
844 }
845
846 matrix
847}
848
849pub fn solve_portfolio_optimization(
851 problem: &PortfolioOptimization,
852 params: Option<AnnealingParams>,
853) -> ApplicationResult<PortfolioSolution> {
854 let (qubo, _var_map) = problem.to_qubo()?;
856
857 let ising = IsingModel::from_qubo(&qubo);
859
860 let annealing_params = params.unwrap_or_else(|| {
862 let mut p = AnnealingParams::default();
863 p.num_sweeps = 10_000;
864 p.num_repetitions = 20;
865 p.initial_temperature = 2.0;
866 p.final_temperature = 0.01;
867 p
868 });
869
870 let simulator = ClassicalAnnealingSimulator::new(annealing_params)
872 .map_err(|e| ApplicationError::OptimizationError(e.to_string()))?;
873
874 let result = simulator
875 .solve(&ising)
876 .map_err(|e| ApplicationError::OptimizationError(e.to_string()))?;
877
878 PortfolioSolution::from_binary(problem, &result.best_spins)
880}
881
882#[cfg(test)]
883mod tests {
884 use super::*;
885
886 #[test]
887 fn test_portfolio_optimization_creation() {
888 let returns = vec![0.05, 0.08, 0.06];
889 let covar = vec![
890 vec![0.04, 0.01, 0.02],
891 vec![0.01, 0.09, 0.03],
892 vec![0.02, 0.03, 0.05],
893 ];
894
895 let portfolio = PortfolioOptimization::new(returns, covar, 100_000.0, 0.5)
896 .expect("Portfolio creation should succeed with valid inputs");
897 assert_eq!(portfolio.expected_returns.len(), 3);
898 assert_eq!(portfolio.budget, 100_000.0);
899 }
900
901 #[test]
902 fn test_portfolio_risk_calculation() {
903 let returns = vec![0.05, 0.08];
904 let covar = vec![vec![0.04, 0.01], vec![0.01, 0.09]];
905
906 let portfolio = PortfolioOptimization::new(returns, covar, 100_000.0, 0.5)
907 .expect("Portfolio creation should succeed with valid inputs");
908
909 let weights = vec![0.6, 0.4];
910 let risk = portfolio.calculate_risk(&weights);
911
912 let expected_risk = (0.36_f64 * 0.04 + 0.16 * 0.09 + 2.0 * 0.6 * 0.4 * 0.01).sqrt();
914
915 assert!((risk - expected_risk).abs() < 1e-10);
916 }
917
918 #[test]
919 fn test_portfolio_return_calculation() {
920 let returns = vec![0.05, 0.08];
921 let covar = vec![vec![0.04, 0.01], vec![0.01, 0.09]];
922
923 let portfolio = PortfolioOptimization::new(returns, covar, 100_000.0, 0.5)
924 .expect("Portfolio creation should succeed with valid inputs");
925
926 let weights = vec![0.6, 0.4];
927 let portfolio_return = portfolio.calculate_return(&weights);
928
929 let expected_return = 0.6 * 0.05 + 0.4 * 0.08;
930 assert!((portfolio_return - expected_return).abs() < 1e-10);
931 }
932
933 #[test]
934 fn test_portfolio_validation() {
935 let returns = vec![0.05, 0.08];
937 let covar = vec![vec![0.04, 0.01], vec![0.01, 0.09]];
938
939 let portfolio = PortfolioOptimization::new(returns, covar, 100_000.0, 0.5)
940 .expect("Portfolio creation should succeed with valid inputs");
941 assert!(portfolio.validate().is_ok());
942
943 let invalid = PortfolioOptimization::new(vec![0.05], vec![vec![0.04]], -1000.0, 0.5);
945 assert!(invalid.is_err());
946 }
947
948 #[test]
949 fn test_sector_constraints() {
950 let returns = vec![0.05, 0.08, 0.06];
951 let covar = create_sample_covariance_matrix(3, 0.2);
952
953 let mut portfolio = PortfolioOptimization::new(returns, covar, 100_000.0, 0.5)
954 .expect("Portfolio creation should succeed with valid inputs");
955
956 assert!(portfolio
957 .add_sector_constraint(0, "Tech".to_string(), 0.5)
958 .is_ok());
959 assert!(portfolio
960 .add_sector_constraint(1, "Tech".to_string(), 0.5)
961 .is_ok());
962 assert!(portfolio
963 .add_sector_constraint(5, "Finance".to_string(), 0.3)
964 .is_err()); }
966
967 #[test]
968 fn test_credit_risk_calculation() {
969 let app = CreditApplication {
970 id: "TEST001".to_string(),
971 amount: 50_000.0,
972 credit_score: 720.0,
973 debt_to_income: 0.3,
974 employment_years: 5.0,
975 collateral_value: 60_000.0,
976 purpose: "Home".to_string(),
977 features: HashMap::new(),
978 };
979
980 let mut weights = HashMap::new();
981 weights.insert("credit_score".to_string(), 0.002);
982 weights.insert("debt_to_income".to_string(), -2.0);
983 weights.insert("employment_years".to_string(), 0.1);
984
985 let risk_model = CreditRiskModel {
986 weights,
987 risk_threshold: 0.05,
988 loss_rates: vec![0.01, 0.03, 0.05, 0.10],
989 };
990
991 let assessment = CreditRiskAssessment {
992 applications: vec![app],
993 risk_model,
994 portfolio_constraints: Vec::new(),
995 };
996
997 let pd = assessment.calculate_pd(&assessment.applications[0]);
998 assert!(pd > 0.0 && pd < 1.0);
999 }
1000
1001 #[test]
1002 fn test_benchmark_problems() {
1003 let problems =
1004 create_benchmark_problems(5).expect("Benchmark problem creation should succeed");
1005 assert_eq!(problems.len(), 3);
1006
1007 for problem in &problems {
1008 assert!(problem.validate().is_ok());
1009 let metrics = problem.size_metrics();
1010 assert_eq!(metrics["num_assets"], 5);
1011 }
1012 }
1013}