rustkernel_risk/
market.rs

1//! Market risk kernels.
2//!
3//! This module provides market risk analytics:
4//! - Monte Carlo Value at Risk (VaR)
5//! - Portfolio risk aggregation
6//! - Expected Shortfall (CVaR)
7
8use crate::messages::{
9    MonteCarloVaRInput, MonteCarloVaROutput, PortfolioRiskAggregationInput,
10    PortfolioRiskAggregationOutput,
11};
12use crate::ring_messages::{
13    K2KMarketUpdate, K2KMarketUpdateAck, K2KVaRAggregation, K2KVaRAggregationResponse,
14    QueryVaRResponse, QueryVaRRing, RecalculateVaRResponse, RecalculateVaRRing,
15    UpdatePositionResponse, UpdatePositionRing, from_currency_fp, from_fixed_point, to_currency_fp,
16};
17use crate::types::{Portfolio, PortfolioRiskResult, VaRParams, VaRResult};
18use async_trait::async_trait;
19use ringkernel_core::RingContext;
20use rustkernel_core::error::Result;
21use rustkernel_core::traits::{BatchKernel, RingKernelHandler};
22use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
23use std::time::Instant;
24
25// ============================================================================
26// Monte Carlo VaR Kernel
27// ============================================================================
28
29/// Monte Carlo VaR state for Ring mode operations.
30#[derive(Debug, Clone, Default)]
31pub struct MonteCarloVaRState {
32    /// Current portfolio.
33    pub portfolio: Option<Portfolio>,
34    /// Cached VaR result.
35    pub var: f64,
36    /// Cached Expected Shortfall.
37    pub es: f64,
38    /// Confidence level used for cached result.
39    pub confidence_level: f64,
40    /// Holding period used for cached result.
41    pub holding_period: u32,
42    /// Whether cached result is stale.
43    pub is_stale: bool,
44    /// Number of simulations used.
45    pub n_simulations: u32,
46}
47
48/// Monte Carlo Value at Risk kernel.
49///
50/// Simulates portfolio P&L using correlated random variates.
51#[derive(Debug)]
52pub struct MonteCarloVaR {
53    metadata: KernelMetadata,
54    /// Internal state for Ring mode operations.
55    state: std::sync::RwLock<MonteCarloVaRState>,
56}
57
58impl Clone for MonteCarloVaR {
59    fn clone(&self) -> Self {
60        Self {
61            metadata: self.metadata.clone(),
62            state: std::sync::RwLock::new(self.state.read().unwrap().clone()),
63        }
64    }
65}
66
67impl Default for MonteCarloVaR {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl MonteCarloVaR {
74    /// Create a new Monte Carlo VaR kernel.
75    #[must_use]
76    pub fn new() -> Self {
77        Self {
78            metadata: KernelMetadata::ring("risk/monte-carlo-var", Domain::RiskAnalytics)
79                .with_description("Monte Carlo Value at Risk simulation")
80                .with_throughput(100_000)
81                .with_latency_us(1000.0),
82            state: std::sync::RwLock::new(MonteCarloVaRState::default()),
83        }
84    }
85
86    /// Initialize the kernel with a portfolio for Ring mode operations.
87    pub fn initialize(&self, portfolio: Portfolio) {
88        let mut state = self.state.write().unwrap();
89        state.portfolio = Some(portfolio);
90        state.is_stale = true;
91    }
92
93    /// Update a position value in the portfolio.
94    pub fn update_position(&self, asset_id: u64, new_value: f64) -> bool {
95        let mut state = self.state.write().unwrap();
96        if let Some(ref mut portfolio) = state.portfolio {
97            // Find and update the asset
98            for (i, &id) in portfolio.asset_ids.iter().enumerate() {
99                if id == asset_id {
100                    portfolio.values[i] = new_value;
101                    state.is_stale = true;
102                    return true;
103                }
104            }
105        }
106        false
107    }
108
109    /// Get cached VaR result.
110    pub fn cached_var(&self) -> (f64, f64, bool) {
111        let state = self.state.read().unwrap();
112        (state.var, state.es, !state.is_stale)
113    }
114
115    /// Recalculate VaR using given parameters.
116    pub fn recalculate(
117        &self,
118        confidence_level: f64,
119        holding_period: u32,
120        n_simulations: u32,
121    ) -> (f64, f64) {
122        let mut state = self.state.write().unwrap();
123        let Some(ref portfolio) = state.portfolio else {
124            return (0.0, 0.0);
125        };
126
127        let params = VaRParams::new(confidence_level, holding_period, n_simulations);
128        let result = Self::compute(portfolio, params);
129
130        state.var = result.var;
131        state.es = result.expected_shortfall;
132        state.confidence_level = confidence_level;
133        state.holding_period = holding_period;
134        state.n_simulations = n_simulations;
135        state.is_stale = false;
136
137        (result.var, result.expected_shortfall)
138    }
139
140    /// Calculate VaR using Monte Carlo simulation.
141    ///
142    /// # Arguments
143    /// * `portfolio` - Portfolio to analyze
144    /// * `params` - VaR calculation parameters
145    pub fn compute(portfolio: &Portfolio, params: VaRParams) -> VaRResult {
146        if portfolio.n_assets() == 0 {
147            return VaRResult {
148                var: 0.0,
149                expected_shortfall: 0.0,
150                confidence_level: params.confidence_level,
151                holding_period: params.holding_period,
152                component_var: Vec::new(),
153                marginal_var: Vec::new(),
154                percentiles: Vec::new(),
155            };
156        }
157
158        let n_sims = params.n_simulations as usize;
159        let holding_factor = (params.holding_period as f64).sqrt();
160
161        // Generate correlated random returns using Cholesky decomposition
162        let cholesky = Self::cholesky_decomposition(portfolio);
163
164        // Simulate P&L scenarios
165        let mut pnl_scenarios = Vec::with_capacity(n_sims);
166        let mut rng = SimpleRng::new(42); // Deterministic for reproducibility
167
168        for _ in 0..n_sims {
169            // Generate independent standard normal variates
170            let z: Vec<f64> = (0..portfolio.n_assets()).map(|_| rng.normal()).collect();
171
172            // Correlate using Cholesky
173            let correlated = Self::apply_cholesky(&cholesky, &z, portfolio.n_assets());
174
175            // Calculate portfolio P&L
176            let mut scenario_pnl = 0.0;
177            for (i, (&z_corr, (&vol, &value))) in correlated
178                .iter()
179                .zip(portfolio.volatilities.iter().zip(portfolio.values.iter()))
180                .enumerate()
181            {
182                let ret = portfolio.expected_returns[i] * params.holding_period as f64 / 252.0
183                    + vol * holding_factor / (252.0_f64).sqrt() * z_corr;
184                scenario_pnl += value * ret;
185            }
186
187            pnl_scenarios.push(scenario_pnl);
188        }
189
190        // Sort scenarios for percentile calculation
191        pnl_scenarios.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
192
193        // Calculate VaR (loss at confidence level)
194        let var_idx = ((1.0 - params.confidence_level) * n_sims as f64) as usize;
195        let var = -pnl_scenarios[var_idx.min(n_sims - 1)]; // Positive VaR = loss
196
197        // Calculate Expected Shortfall (average of tail losses)
198        let tail_start = var_idx.min(n_sims - 1);
199        let expected_shortfall = if tail_start > 0 {
200            -pnl_scenarios[..tail_start].iter().sum::<f64>() / tail_start as f64
201        } else {
202            var
203        };
204
205        // Calculate component VaR (marginal contribution)
206        let component_var = Self::calculate_component_var(portfolio, var, &cholesky);
207
208        // Calculate marginal VaR
209        let marginal_var = Self::calculate_marginal_var(portfolio, params, &cholesky);
210
211        // Calculate percentiles
212        let percentiles: Vec<(f64, f64)> = [0.01, 0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95, 0.99]
213            .iter()
214            .map(|&p| {
215                let idx = ((1.0 - p) * n_sims as f64) as usize;
216                (p, pnl_scenarios[idx.min(n_sims - 1)])
217            })
218            .collect();
219
220        VaRResult {
221            var,
222            expected_shortfall,
223            confidence_level: params.confidence_level,
224            holding_period: params.holding_period,
225            component_var,
226            marginal_var,
227            percentiles,
228        }
229    }
230
231    /// Perform Cholesky decomposition of correlation matrix.
232    fn cholesky_decomposition(portfolio: &Portfolio) -> Vec<f64> {
233        let n = portfolio.n_assets();
234        let mut l = vec![0.0; n * n];
235
236        for i in 0..n {
237            for j in 0..=i {
238                let mut sum = 0.0;
239                for k in 0..j {
240                    sum += l[i * n + k] * l[j * n + k];
241                }
242
243                if i == j {
244                    let diag = portfolio.correlation(i, i) - sum;
245                    l[i * n + j] = if diag > 0.0 { diag.sqrt() } else { 0.0 };
246                } else {
247                    let l_jj = l[j * n + j];
248                    l[i * n + j] = if l_jj.abs() > 1e-10 {
249                        (portfolio.correlation(i, j) - sum) / l_jj
250                    } else {
251                        0.0
252                    };
253                }
254            }
255        }
256
257        l
258    }
259
260    /// Apply Cholesky factor to independent normals.
261    fn apply_cholesky(l: &[f64], z: &[f64], n: usize) -> Vec<f64> {
262        let mut result = vec![0.0; n];
263        for i in 0..n {
264            for j in 0..=i {
265                result[i] += l[i * n + j] * z[j];
266            }
267        }
268        result
269    }
270
271    /// Calculate component VaR.
272    fn calculate_component_var(
273        portfolio: &Portfolio,
274        total_var: f64,
275        _cholesky: &[f64],
276    ) -> Vec<f64> {
277        // Component VaR = weight_i * sigma_i * rho_i,p * VaR / sigma_p
278        let weights = portfolio.weights();
279        let n = portfolio.n_assets();
280
281        // Calculate portfolio volatility
282        let port_var = Self::portfolio_variance(portfolio);
283        let port_vol = port_var.sqrt();
284
285        if port_vol < 1e-10 {
286            return vec![0.0; n];
287        }
288
289        // Calculate covariance of each asset with portfolio
290        let mut component_vars = Vec::with_capacity(n);
291
292        for i in 0..n {
293            let mut cov_i_p = 0.0;
294            for j in 0..n {
295                cov_i_p += weights[j]
296                    * portfolio.volatilities[i]
297                    * portfolio.volatilities[j]
298                    * portfolio.correlation(i, j);
299            }
300
301            let beta_i = cov_i_p / port_var;
302            let component_var_i = weights[i] * beta_i * total_var;
303            component_vars.push(component_var_i);
304        }
305
306        component_vars
307    }
308
309    /// Calculate marginal VaR.
310    fn calculate_marginal_var(
311        portfolio: &Portfolio,
312        params: VaRParams,
313        _cholesky: &[f64],
314    ) -> Vec<f64> {
315        let n = portfolio.n_assets();
316        let weights = portfolio.weights();
317        let holding_factor = (params.holding_period as f64).sqrt();
318
319        // Z-score for confidence level
320        let z = Self::norm_inv(params.confidence_level);
321
322        // Portfolio volatility
323        let port_vol = Self::portfolio_variance(portfolio).sqrt();
324
325        if port_vol < 1e-10 {
326            return vec![0.0; n];
327        }
328
329        // Marginal VaR = dVaR/dw_i = z * sigma_p * d_sigma_p/dw_i
330        let mut marginal_vars = Vec::with_capacity(n);
331
332        for i in 0..n {
333            let mut cov_i_p = 0.0;
334            for j in 0..n {
335                cov_i_p += weights[j]
336                    * portfolio.volatilities[i]
337                    * portfolio.volatilities[j]
338                    * portfolio.correlation(i, j);
339            }
340
341            let d_sigma_dw = cov_i_p / port_vol;
342            let marginal_var_i = z * d_sigma_dw * holding_factor / (252.0_f64).sqrt();
343            marginal_vars.push(marginal_var_i * portfolio.total_value());
344        }
345
346        marginal_vars
347    }
348
349    /// Calculate portfolio variance.
350    fn portfolio_variance(portfolio: &Portfolio) -> f64 {
351        let weights = portfolio.weights();
352        let n = portfolio.n_assets();
353        let mut var = 0.0;
354
355        for i in 0..n {
356            for j in 0..n {
357                var += weights[i]
358                    * weights[j]
359                    * portfolio.volatilities[i]
360                    * portfolio.volatilities[j]
361                    * portfolio.correlation(i, j);
362            }
363        }
364
365        var
366    }
367
368    /// Standard normal inverse CDF.
369    fn norm_inv(p: f64) -> f64 {
370        // Rational approximation
371        let p_clamped = p.clamp(1e-10, 1.0 - 1e-10);
372        let t = if p_clamped < 0.5 {
373            (-2.0 * p_clamped.ln()).sqrt()
374        } else {
375            (-2.0 * (1.0 - p_clamped).ln()).sqrt()
376        };
377
378        let c0 = 2.515517;
379        let c1 = 0.802853;
380        let c2 = 0.010328;
381        let d1 = 1.432788;
382        let d2 = 0.189269;
383        let d3 = 0.001308;
384
385        let result = t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t);
386
387        if p_clamped < 0.5 { -result } else { result }
388    }
389}
390
391impl GpuKernel for MonteCarloVaR {
392    fn metadata(&self) -> &KernelMetadata {
393        &self.metadata
394    }
395}
396
397// ============================================================================
398// Monte Carlo VaR RingKernelHandler Implementations
399// ============================================================================
400
401/// RingKernelHandler for position updates.
402///
403/// Enables streaming position updates for real-time VaR.
404#[async_trait]
405impl RingKernelHandler<UpdatePositionRing, UpdatePositionResponse> for MonteCarloVaR {
406    async fn handle(
407        &self,
408        _ctx: &mut RingContext,
409        msg: UpdatePositionRing,
410    ) -> Result<UpdatePositionResponse> {
411        // Update position in internal state
412        let new_value = from_currency_fp(msg.value_fp);
413        let updated = self.update_position(msg.asset_id, new_value);
414
415        // Position changed, VaR needs recalculation
416        Ok(UpdatePositionResponse {
417            request_id: msg.id.0,
418            asset_id: msg.asset_id,
419            var_stale: updated,
420        })
421    }
422}
423
424/// RingKernelHandler for VaR queries.
425///
426/// Returns current cached VaR or triggers recalculation.
427#[async_trait]
428impl RingKernelHandler<QueryVaRRing, QueryVaRResponse> for MonteCarloVaR {
429    async fn handle(&self, _ctx: &mut RingContext, msg: QueryVaRRing) -> Result<QueryVaRResponse> {
430        // Query cached VaR from internal state
431        let (var, es, is_fresh) = self.cached_var();
432
433        Ok(QueryVaRResponse {
434            request_id: msg.id.0,
435            var_fp: to_currency_fp(var),
436            es_fp: to_currency_fp(es),
437            confidence_fp: msg.confidence_fp,
438            holding_period: msg.holding_period,
439            is_fresh,
440        })
441    }
442}
443
444/// RingKernelHandler for VaR recalculation.
445///
446/// Triggers full Monte Carlo simulation with given parameters.
447#[async_trait]
448impl RingKernelHandler<RecalculateVaRRing, RecalculateVaRResponse> for MonteCarloVaR {
449    async fn handle(
450        &self,
451        _ctx: &mut RingContext,
452        msg: RecalculateVaRRing,
453    ) -> Result<RecalculateVaRResponse> {
454        let start = Instant::now();
455        let confidence = from_fixed_point(msg.confidence_fp);
456
457        // Run full Monte Carlo simulation on internal state
458        let (var, es) = self.recalculate(confidence, msg.holding_period, msg.n_simulations);
459        let compute_time_us = start.elapsed().as_micros() as u64;
460
461        Ok(RecalculateVaRResponse {
462            request_id: msg.id.0,
463            var_fp: to_currency_fp(var),
464            es_fp: to_currency_fp(es),
465            compute_time_us,
466            n_simulations: msg.n_simulations,
467        })
468    }
469}
470
471/// RingKernelHandler for K2K market updates.
472///
473/// Processes streaming market data for real-time VaR adjustments.
474#[async_trait]
475impl RingKernelHandler<K2KMarketUpdate, K2KMarketUpdateAck> for MonteCarloVaR {
476    async fn handle(
477        &self,
478        _ctx: &mut RingContext,
479        msg: K2KMarketUpdate,
480    ) -> Result<K2KMarketUpdateAck> {
481        // Get current portfolio value for impact estimation
482        let portfolio_value = {
483            let state = self.state.read().unwrap();
484            state
485                .portfolio
486                .as_ref()
487                .map(|p| p.total_value())
488                .unwrap_or(0.0)
489        };
490
491        // Estimate VaR impact from volatility change
492        // VaR impact ≈ portfolio_value * z * Δvolatility * sqrt(holding_period)
493        let vol_delta = from_fixed_point(msg.vol_delta_fp);
494        let z_95 = 1.645; // 95% confidence z-score
495        let var_impact = portfolio_value * z_95 * vol_delta.abs();
496
497        // Mark state as stale since market conditions changed
498        {
499            let mut state = self.state.write().unwrap();
500            state.is_stale = true;
501        }
502
503        Ok(K2KMarketUpdateAck {
504            request_id: msg.id.0,
505            worker_id: 0, // Single instance worker
506            var_impact_fp: to_currency_fp(var_impact),
507        })
508    }
509}
510
511/// RingKernelHandler for K2K partial VaR aggregation.
512///
513/// Used in distributed VaR calculation to aggregate partial results.
514#[async_trait]
515impl RingKernelHandler<K2KVaRAggregation, K2KVaRAggregationResponse> for MonteCarloVaR {
516    async fn handle(
517        &self,
518        _ctx: &mut RingContext,
519        msg: K2KVaRAggregation,
520    ) -> Result<K2KVaRAggregationResponse> {
521        let complete = msg.workers_reported >= msg.expected_workers;
522        let aggregated_var = from_currency_fp(msg.aggregated_var_fp);
523
524        // Calculate diversification benefit based on portfolio correlation structure
525        // With n workers, diversification benefit scales with 1 - sqrt(1/n) for uncorrelated
526        // For partially correlated, use average correlation from portfolio if available
527        let diversification_factor = {
528            let state = self.state.read().unwrap();
529            if let Some(ref portfolio) = state.portfolio {
530                let n = portfolio.n_assets();
531                if n > 1 {
532                    // Calculate average off-diagonal correlation
533                    let mut avg_corr = 0.0;
534                    let mut count = 0;
535                    for i in 0..n {
536                        for j in (i + 1)..n {
537                            avg_corr += portfolio.correlation(i, j);
538                            count += 1;
539                        }
540                    }
541                    if count > 0 {
542                        avg_corr /= count as f64;
543                    }
544                    // Diversification benefit = 1 - sqrt(average correlation adjustment)
545                    // Higher correlation = less benefit
546                    (1.0 - avg_corr.max(0.0).sqrt()) * 0.3 // Cap at 30%
547                } else {
548                    0.0
549                }
550            } else {
551                0.15 // Default 15% when no portfolio available
552            }
553        };
554
555        let diversification_benefit = aggregated_var * diversification_factor;
556        let final_var = aggregated_var - diversification_benefit;
557
558        // ES ratio depends on confidence level (1.25x for 95%, 1.15x for 99%)
559        let es_ratio = 1.25;
560        let final_es = final_var * es_ratio;
561
562        Ok(K2KVaRAggregationResponse {
563            correlation_id: msg.correlation_id,
564            complete,
565            final_var_fp: to_currency_fp(final_var),
566            final_es_fp: to_currency_fp(final_es),
567            diversification_benefit_fp: to_currency_fp(diversification_benefit),
568        })
569    }
570}
571
572#[async_trait]
573impl BatchKernel<MonteCarloVaRInput, MonteCarloVaROutput> for MonteCarloVaR {
574    async fn execute(&self, input: MonteCarloVaRInput) -> Result<MonteCarloVaROutput> {
575        let start = Instant::now();
576        let result = Self::compute(&input.portfolio, input.params);
577        Ok(MonteCarloVaROutput {
578            result,
579            compute_time_us: start.elapsed().as_micros() as u64,
580        })
581    }
582}
583
584// ============================================================================
585// Portfolio Risk Aggregation Kernel
586// ============================================================================
587
588/// Portfolio risk aggregation kernel.
589///
590/// Aggregates risk measures across portfolio with correlation adjustment.
591#[derive(Debug, Clone)]
592pub struct PortfolioRiskAggregation {
593    metadata: KernelMetadata,
594}
595
596impl Default for PortfolioRiskAggregation {
597    fn default() -> Self {
598        Self::new()
599    }
600}
601
602impl PortfolioRiskAggregation {
603    /// Create a new portfolio risk aggregation kernel.
604    #[must_use]
605    pub fn new() -> Self {
606        Self {
607            metadata: KernelMetadata::ring("risk/portfolio-aggregation", Domain::RiskAnalytics)
608                .with_description("Correlation-adjusted portfolio risk")
609                .with_throughput(10_000)
610                .with_latency_us(500.0),
611        }
612    }
613
614    /// Aggregate portfolio risk.
615    ///
616    /// # Arguments
617    /// * `portfolio` - Portfolio to analyze
618    /// * `confidence_level` - Confidence level for VaR
619    /// * `holding_period` - Holding period in days
620    pub fn compute(
621        portfolio: &Portfolio,
622        confidence_level: f64,
623        holding_period: u32,
624    ) -> PortfolioRiskResult {
625        let n = portfolio.n_assets();
626        if n == 0 {
627            return PortfolioRiskResult {
628                portfolio_var: 0.0,
629                portfolio_es: 0.0,
630                undiversified_var: 0.0,
631                diversification_benefit: 0.0,
632                asset_vars: Vec::new(),
633                risk_contributions: Vec::new(),
634                covariance_matrix: Vec::new(),
635            };
636        }
637
638        let z = Self::norm_inv(confidence_level);
639        let holding_factor = (holding_period as f64).sqrt() / (252.0_f64).sqrt();
640
641        // Calculate covariance matrix
642        let mut cov_matrix = vec![0.0; n * n];
643        for i in 0..n {
644            for j in 0..n {
645                cov_matrix[i * n + j] = portfolio.volatilities[i]
646                    * portfolio.volatilities[j]
647                    * portfolio.correlation(i, j);
648            }
649        }
650
651        // Calculate individual asset VaRs
652        let asset_vars: Vec<f64> = portfolio
653            .values
654            .iter()
655            .zip(portfolio.volatilities.iter())
656            .map(|(&value, &vol)| value * z * vol * holding_factor)
657            .collect();
658
659        // Undiversified VaR (sum of individual VaRs)
660        let undiversified_var: f64 = asset_vars.iter().sum();
661
662        // Portfolio variance
663        let weights = portfolio.weights();
664        let mut portfolio_variance = 0.0;
665        for i in 0..n {
666            for j in 0..n {
667                portfolio_variance += weights[i] * weights[j] * cov_matrix[i * n + j];
668            }
669        }
670
671        // Portfolio VaR
672        let portfolio_var =
673            portfolio.total_value() * z * portfolio_variance.sqrt() * holding_factor;
674
675        // Diversification benefit
676        let diversification_benefit = undiversified_var - portfolio_var;
677
678        // Risk contributions (Euler allocation)
679        let port_vol = portfolio_variance.sqrt();
680        let risk_contributions: Vec<f64> = if port_vol > 1e-10 {
681            (0..n)
682                .map(|i| {
683                    let mut cov_i_p = 0.0;
684                    for j in 0..n {
685                        cov_i_p += weights[j] * cov_matrix[i * n + j];
686                    }
687                    weights[i] * cov_i_p / port_vol * z * holding_factor * portfolio.total_value()
688                })
689                .collect()
690        } else {
691            vec![0.0; n]
692        };
693
694        // Expected Shortfall (using normal approximation)
695        let pdf_at_z = (-z * z / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt();
696        let portfolio_es = portfolio.total_value() * port_vol * holding_factor * pdf_at_z
697            / (1.0 - confidence_level);
698
699        PortfolioRiskResult {
700            portfolio_var,
701            portfolio_es,
702            undiversified_var,
703            diversification_benefit,
704            asset_vars,
705            risk_contributions,
706            covariance_matrix: cov_matrix,
707        }
708    }
709
710    /// Standard normal inverse CDF.
711    fn norm_inv(p: f64) -> f64 {
712        MonteCarloVaR::norm_inv(p)
713    }
714}
715
716impl GpuKernel for PortfolioRiskAggregation {
717    fn metadata(&self) -> &KernelMetadata {
718        &self.metadata
719    }
720}
721
722#[async_trait]
723impl BatchKernel<PortfolioRiskAggregationInput, PortfolioRiskAggregationOutput>
724    for PortfolioRiskAggregation
725{
726    async fn execute(
727        &self,
728        input: PortfolioRiskAggregationInput,
729    ) -> Result<PortfolioRiskAggregationOutput> {
730        let start = Instant::now();
731        let result = Self::compute(
732            &input.portfolio,
733            input.confidence_level,
734            input.holding_period,
735        );
736        Ok(PortfolioRiskAggregationOutput {
737            result,
738            compute_time_us: start.elapsed().as_micros() as u64,
739        })
740    }
741}
742
743// ============================================================================
744// Simple RNG for Monte Carlo
745// ============================================================================
746
747/// Simple pseudo-random number generator (xorshift64).
748struct SimpleRng {
749    state: u64,
750}
751
752impl SimpleRng {
753    fn new(seed: u64) -> Self {
754        Self { state: seed.max(1) }
755    }
756
757    fn next_u64(&mut self) -> u64 {
758        self.state ^= self.state << 13;
759        self.state ^= self.state >> 7;
760        self.state ^= self.state << 17;
761        self.state
762    }
763
764    fn next_f64(&mut self) -> f64 {
765        (self.next_u64() as f64) / (u64::MAX as f64)
766    }
767
768    /// Generate standard normal using Box-Muller transform.
769    fn normal(&mut self) -> f64 {
770        let u1 = self.next_f64().max(1e-10);
771        let u2 = self.next_f64();
772        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
773    }
774}
775
776#[cfg(test)]
777mod tests {
778    use super::*;
779
780    fn create_simple_portfolio() -> Portfolio {
781        // Two-asset portfolio with known properties
782        Portfolio::new(
783            vec![1, 2],
784            vec![100_000.0, 100_000.0],
785            vec![0.08, 0.10],         // 8%, 10% expected returns
786            vec![0.15, 0.20],         // 15%, 20% volatility
787            vec![1.0, 0.5, 0.5, 1.0], // Correlation matrix
788        )
789    }
790
791    fn create_diversified_portfolio() -> Portfolio {
792        // Four-asset portfolio with low correlations
793        Portfolio::new(
794            vec![1, 2, 3, 4],
795            vec![50_000.0, 30_000.0, 15_000.0, 5_000.0],
796            vec![0.06, 0.08, 0.10, 0.12],
797            vec![0.10, 0.15, 0.20, 0.25],
798            vec![
799                1.0, 0.2, 0.1, 0.0, 0.2, 1.0, 0.3, 0.1, 0.1, 0.3, 1.0, 0.2, 0.0, 0.1, 0.2, 1.0,
800            ],
801        )
802    }
803
804    #[test]
805    fn test_monte_carlo_var_metadata() {
806        let kernel = MonteCarloVaR::new();
807        assert_eq!(kernel.metadata().id, "risk/monte-carlo-var");
808        assert_eq!(kernel.metadata().domain, Domain::RiskAnalytics);
809    }
810
811    #[test]
812    fn test_monte_carlo_var_calculation() {
813        let portfolio = create_simple_portfolio();
814        let params = VaRParams::new(0.95, 10, 10_000);
815
816        let result = MonteCarloVaR::compute(&portfolio, params);
817
818        assert!(result.var > 0.0, "VaR should be positive");
819        assert!(result.expected_shortfall >= result.var, "ES >= VaR");
820        assert_eq!(result.confidence_level, 0.95);
821        assert_eq!(result.holding_period, 10);
822
823        // VaR should be reasonable (not more than 50% of portfolio)
824        assert!(
825            result.var < 100_000.0,
826            "VaR seems too large: {}",
827            result.var
828        );
829    }
830
831    #[test]
832    fn test_var_increases_with_holding_period() {
833        let portfolio = create_simple_portfolio();
834
835        let var_1d = MonteCarloVaR::compute(&portfolio, VaRParams::new(0.95, 1, 10_000));
836        let var_10d = MonteCarloVaR::compute(&portfolio, VaRParams::new(0.95, 10, 10_000));
837
838        // 10-day VaR should be roughly sqrt(10) times 1-day VaR
839        let ratio = var_10d.var / var_1d.var;
840        assert!(
841            ratio > 2.5 && ratio < 4.0,
842            "VaR scaling ratio should be ~sqrt(10): {}",
843            ratio
844        );
845    }
846
847    #[test]
848    fn test_var_increases_with_confidence() {
849        let portfolio = create_simple_portfolio();
850
851        let var_95 = MonteCarloVaR::compute(&portfolio, VaRParams::new(0.95, 10, 10_000));
852        let var_99 = MonteCarloVaR::compute(&portfolio, VaRParams::new(0.99, 10, 10_000));
853
854        assert!(
855            var_99.var > var_95.var,
856            "99% VaR should exceed 95% VaR: {} vs {}",
857            var_99.var,
858            var_95.var
859        );
860    }
861
862    #[test]
863    fn test_component_var_sums_to_total() {
864        let portfolio = create_diversified_portfolio();
865        let params = VaRParams::new(0.95, 10, 10_000);
866
867        let result = MonteCarloVaR::compute(&portfolio, params);
868
869        // Component VaRs should roughly sum to total VaR
870        let component_sum: f64 = result.component_var.iter().sum();
871        let diff = (component_sum - result.var).abs() / result.var;
872
873        assert!(
874            diff < 0.20, // Allow 20% tolerance due to Monte Carlo noise
875            "Component VaR sum should be close to total: {} vs {}",
876            component_sum,
877            result.var
878        );
879    }
880
881    #[test]
882    fn test_portfolio_aggregation_metadata() {
883        let kernel = PortfolioRiskAggregation::new();
884        assert_eq!(kernel.metadata().id, "risk/portfolio-aggregation");
885    }
886
887    #[test]
888    fn test_portfolio_aggregation() {
889        let portfolio = create_diversified_portfolio();
890        let result = PortfolioRiskAggregation::compute(&portfolio, 0.95, 10);
891
892        assert!(result.portfolio_var > 0.0);
893        assert!(result.undiversified_var > result.portfolio_var);
894        assert!(result.diversification_benefit > 0.0);
895    }
896
897    #[test]
898    fn test_diversification_benefit() {
899        let portfolio = create_diversified_portfolio();
900        let result = PortfolioRiskAggregation::compute(&portfolio, 0.95, 10);
901
902        // Diversification benefit = undiversified - portfolio
903        assert!(
904            (result.diversification_benefit - (result.undiversified_var - result.portfolio_var))
905                .abs()
906                < 1.0
907        );
908
909        // With low correlations, should have significant benefit
910        let benefit_pct = result.diversification_benefit / result.undiversified_var;
911        assert!(
912            benefit_pct > 0.10,
913            "Should have >10% diversification benefit: {}%",
914            benefit_pct * 100.0
915        );
916    }
917
918    #[test]
919    fn test_risk_contributions_sum() {
920        let portfolio = create_diversified_portfolio();
921        let result = PortfolioRiskAggregation::compute(&portfolio, 0.95, 10);
922
923        let contrib_sum: f64 = result.risk_contributions.iter().sum();
924
925        // Risk contributions should sum to portfolio VaR
926        let diff = (contrib_sum - result.portfolio_var).abs() / result.portfolio_var;
927        assert!(
928            diff < 0.01,
929            "Risk contributions should sum to portfolio VaR: {} vs {}",
930            contrib_sum,
931            result.portfolio_var
932        );
933    }
934
935    #[test]
936    fn test_empty_portfolio() {
937        let empty = Portfolio::new(Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new());
938
939        let var_result = MonteCarloVaR::compute(&empty, VaRParams::default());
940        assert_eq!(var_result.var, 0.0);
941
942        let agg_result = PortfolioRiskAggregation::compute(&empty, 0.95, 10);
943        assert_eq!(agg_result.portfolio_var, 0.0);
944    }
945
946    #[test]
947    fn test_covariance_matrix() {
948        let portfolio = create_simple_portfolio();
949        let result = PortfolioRiskAggregation::compute(&portfolio, 0.95, 10);
950
951        // Covariance matrix should be symmetric
952        assert_eq!(result.covariance_matrix.len(), 4);
953        assert!((result.covariance_matrix[1] - result.covariance_matrix[2]).abs() < 1e-10);
954
955        // Diagonal should be variance
956        let var1 = 0.15 * 0.15;
957        let var2 = 0.20 * 0.20;
958        assert!((result.covariance_matrix[0] - var1).abs() < 1e-10);
959        assert!((result.covariance_matrix[3] - var2).abs() < 1e-10);
960    }
961}