quantrs2_anneal/applications/
finance.rs

1//! Finance Industry Optimization
2//!
3//! This module provides optimization solutions for the finance industry,
4//! including portfolio optimization, risk management, and fraud detection.
5
6use 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/// Portfolio optimization problem
17#[derive(Debug, Clone)]
18pub struct PortfolioOptimization {
19    /// Asset expected returns
20    pub expected_returns: Vec<f64>,
21    /// Covariance matrix of asset returns
22    pub covariance_matrix: Vec<Vec<f64>>,
23    /// Investment budget
24    pub budget: f64,
25    /// Risk tolerance parameter
26    pub risk_tolerance: f64,
27    /// Minimum position sizes
28    pub min_positions: Vec<f64>,
29    /// Maximum position sizes
30    pub max_positions: Vec<f64>,
31    /// Sector constraints (asset -> sector mapping)
32    pub sector_constraints: HashMap<usize, String>,
33    /// Maximum sector allocation
34    pub max_sector_allocation: HashMap<String, f64>,
35    /// Transaction costs
36    pub transaction_costs: Vec<f64>,
37    /// Regulatory constraints
38    pub regulatory_constraints: Vec<IndustryConstraint>,
39}
40
41impl PortfolioOptimization {
42    /// Create a new portfolio optimization problem
43    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    /// Add sector constraint
86    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    /// Set position bounds
104    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    /// Calculate portfolio risk
122    #[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    /// Calculate portfolio return
136    #[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    /// Calculate Sharpe ratio
146    #[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        // Check covariance matrix is positive semidefinite (simplified check)
203        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; // Discretization precision for continuous weights
217
218        let mut builder = QuboBuilder::new();
219
220        // Binary variables: x_{i,k} = 1 if asset i gets allocation level k
221        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        // Objective: maximize return - risk_penalty * risk
232        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                // Return term (to be maximized, so negate)
238                let return_coeff = -self.expected_returns[asset] * weight * self.budget;
239                builder.add_bias(var_idx, return_coeff);
240
241                // Risk penalty term
242                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        // Cross-terms for risk calculation
253        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        // Constraint: exactly one allocation level per asset
279        let constraint_penalty = 1000.0;
280        for asset in 0..n_assets {
281            // Penalty for not selecting exactly one level
282            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            // Penalty for selecting no level
291            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        // Mean-variance objective: return - risk_penalty * risk
315        Ok((self.risk_tolerance * portfolio_risk).mul_add(-portfolio_risk, portfolio_return))
316    }
317
318    fn is_feasible(&self, solution: &Self::Solution) -> bool {
319        // Check budget constraint
320        let total_investment: f64 = solution.weights.iter().sum();
321        if (total_investment - 1.0).abs() > 1e-6 {
322            return false;
323        }
324
325        // Check position bounds
326        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        // Check sector constraints
334        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/// Binary wrapper for portfolio optimization that works with `Vec<i8>` solutions
353#[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        // Simple heuristic: treat binary solution as asset selection
390        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); // Heavy penalty for no assets
400        }
401
402        // Equal weight portfolio among selected assets
403        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        // Calculate portfolio return
410        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        // At least one asset must be selected
421        solution.iter().any(|&x| x == 1)
422    }
423}
424
425/// Portfolio optimization solution
426#[derive(Debug, Clone)]
427pub struct PortfolioSolution {
428    /// Asset weights (sum to 1.0)
429    pub weights: Vec<f64>,
430    /// Portfolio metrics
431    pub metrics: PortfolioMetrics,
432}
433
434/// Portfolio performance metrics
435#[derive(Debug, Clone)]
436pub struct PortfolioMetrics {
437    /// Expected return
438    pub expected_return: f64,
439    /// Portfolio volatility (risk)
440    pub volatility: f64,
441    /// Sharpe ratio
442    pub sharpe_ratio: f64,
443    /// Maximum drawdown
444    pub max_drawdown: f64,
445    /// Value at Risk (`VaR`)
446    pub var_95: f64,
447    /// Conditional Value at Risk (`CVaR`)
448    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        // Normalize weights to sum to 1
472        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        // Calculate metrics
480        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); // 2% risk-free rate
483
484        let metrics = PortfolioMetrics {
485            expected_return,
486            volatility,
487            sharpe_ratio,
488            max_drawdown: 0.0,          // Would require time series data
489            var_95: volatility * 1.645, // Simplified VaR calculation
490            cvar_95: volatility * 2.0,  // Simplified CVaR calculation
491        };
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        // Top 5 positions
514        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        // Concentration metrics
541        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                // Only show significant positions
560                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/// Risk management problem for financial institutions
590#[derive(Debug, Clone)]
591pub struct RiskManagement {
592    /// Portfolio positions
593    pub positions: Vec<f64>,
594    /// Risk factors and exposures
595    pub risk_factors: HashMap<String, Vec<f64>>,
596    /// Risk limits by factor
597    pub risk_limits: HashMap<String, f64>,
598    /// Stress test scenarios
599    pub stress_scenarios: Vec<HashMap<String, f64>>,
600    /// Current market data
601    pub market_data: HashMap<String, f64>,
602}
603
604impl RiskManagement {
605    /// Create new risk management problem
606    #[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    /// Add risk factor exposure
618    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    /// Calculate total risk exposure for a factor
636    #[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    /// Run stress test
650    #[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/// Credit risk assessment problem
666#[derive(Debug, Clone)]
667pub struct CreditRiskAssessment {
668    /// Loan applications
669    pub applications: Vec<CreditApplication>,
670    /// Risk model parameters
671    pub risk_model: CreditRiskModel,
672    /// Portfolio constraints
673    pub portfolio_constraints: Vec<IndustryConstraint>,
674}
675
676/// Credit application data
677#[derive(Debug, Clone)]
678pub struct CreditApplication {
679    /// Application ID
680    pub id: String,
681    /// Loan amount requested
682    pub amount: f64,
683    /// Applicant credit score
684    pub credit_score: f64,
685    /// Debt-to-income ratio
686    pub debt_to_income: f64,
687    /// Employment history (years)
688    pub employment_years: f64,
689    /// Collateral value
690    pub collateral_value: f64,
691    /// Loan purpose
692    pub purpose: String,
693    /// Additional features
694    pub features: HashMap<String, f64>,
695}
696
697/// Credit risk model
698#[derive(Debug, Clone)]
699pub struct CreditRiskModel {
700    /// Feature weights
701    pub weights: HashMap<String, f64>,
702    /// Risk threshold
703    pub risk_threshold: f64,
704    /// Expected loss rates by risk bucket
705    pub loss_rates: Vec<f64>,
706}
707
708impl CreditRiskAssessment {
709    /// Calculate probability of default for an application
710    #[must_use]
711    pub fn calculate_pd(&self, application: &CreditApplication) -> f64 {
712        let mut score = 0.0;
713
714        // Standard credit features
715        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        // Additional features
731        for (feature, value) in &application.features {
732            score += self.risk_model.weights.get(feature).unwrap_or(&0.0) * value;
733        }
734
735        // Convert to probability using logistic function
736        1.0 / (1.0 + (-score).exp())
737    }
738
739    /// Calculate expected loss for a portfolio selection
740    #[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; // Loss given default (typical value)
749                let ead = app.amount; // Exposure at default
750
751                total_loss += pd * lgd * ead;
752            }
753        }
754
755        total_loss
756    }
757}
758
759/// Utility functions for finance applications
760
761/// Create benchmark portfolio optimization problems
762pub 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    // Problem 1: Conservative portfolio
769    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, // Low risk tolerance
778    )?;
779
780    problems.push(
781        Box::new(BinaryPortfolioOptimization::new(conservative_portfolio))
782            as Box<dyn OptimizationProblem<Solution = Vec<i8>, ObjectiveValue = f64>>,
783    );
784
785    // Problem 2: Aggressive portfolio
786    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, // High risk tolerance
795    )?;
796
797    problems.push(
798        Box::new(BinaryPortfolioOptimization::new(aggressive_portfolio))
799            as Box<dyn OptimizationProblem<Solution = Vec<i8>, ObjectiveValue = f64>>,
800    );
801
802    // Problem 3: Sector-constrained portfolio
803    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    // Add sector constraints
813    for i in 0..num_assets {
814        let sector = format!("Sector_{}", i % 5); // 5 sectors
815        sector_portfolio.add_sector_constraint(i, sector, 0.3)?; // Max 30% per sector
816    }
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
826/// Create sample covariance matrix for testing
827fn 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                // Diagonal: individual variances
834                matrix[i][j] =
835                    base_volatility * base_volatility * (1.0 + 0.5 * (i as f64) / (n as f64));
836            } else {
837                // Off-diagonal: correlations
838                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
849/// Solve portfolio optimization problem
850pub fn solve_portfolio_optimization(
851    problem: &PortfolioOptimization,
852    params: Option<AnnealingParams>,
853) -> ApplicationResult<PortfolioSolution> {
854    // Convert to QUBO
855    let (qubo, _var_map) = problem.to_qubo()?;
856
857    // Convert to Ising
858    let ising = IsingModel::from_qubo(&qubo);
859
860    // Set up annealing parameters
861    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    // Solve with classical annealing
871    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    // Convert solution back to portfolio
879    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        // Expected: sqrt(0.6^2 * 0.04 + 0.4^2 * 0.09 + 2 * 0.6 * 0.4 * 0.01)
913        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        // Valid portfolio
936        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        // Invalid portfolio (negative budget)
944        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()); // Invalid asset index
965    }
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}