Skip to main content

sklears_kernel_approximation/
finance_kernels.rs

1//! Finance and Economics Kernel Methods
2//!
3//! This module implements kernel methods for financial and economic applications,
4//! including financial time series analysis, volatility modeling, econometric methods,
5//! portfolio optimization, and risk analysis.
6//!
7//! # References
8//! - Engle (1982): "Autoregressive Conditional Heteroskedasticity"
9//! - Bollerslev (1986): "Generalized Autoregressive Conditional Heteroskedasticity"
10//! - Tsay (2010): "Analysis of Financial Time Series"
11//! - McNeil et al. (2015): "Quantitative Risk Management"
12
13use scirs2_core::ndarray::{Array1, Array2};
14use scirs2_core::random::essentials::Normal;
15use scirs2_core::random::{thread_rng, Distribution};
16use sklears_core::{
17    error::{Result, SklearsError},
18    prelude::{Fit, Transform},
19    traits::{Trained, Untrained},
20    types::Float,
21};
22use std::marker::PhantomData;
23
24// ============================================================================
25// Financial Kernel
26// ============================================================================
27
28/// Kernel method for financial time series analysis
29///
30/// This kernel extracts features from financial time series data including
31/// returns, volatility, technical indicators, and market microstructure features.
32///
33/// # References
34/// - Murphy (1999): "Technical Analysis of the Financial Markets"
35pub struct FinancialKernel<State = Untrained> {
36    /// Number of random features
37    n_components: usize,
38    /// Window size for technical indicators
39    window_size: usize,
40    /// Whether to include volatility features
41    include_volatility: bool,
42    /// Random projection matrix
43    projection: Option<Array2<Float>>,
44    /// Feature weights (returns, volatility, indicators)
45    feature_weights: Option<Array1<Float>>,
46    /// State marker
47    _state: PhantomData<State>,
48}
49
50impl FinancialKernel<Untrained> {
51    /// Create a new financial kernel
52    pub fn new(n_components: usize, window_size: usize) -> Self {
53        Self {
54            n_components,
55            window_size,
56            include_volatility: true,
57            projection: None,
58            feature_weights: None,
59            _state: PhantomData,
60        }
61    }
62
63    /// Set whether to include volatility features
64    pub fn include_volatility(mut self, include_volatility: bool) -> Self {
65        self.include_volatility = include_volatility;
66        self
67    }
68}
69
70impl Default for FinancialKernel<Untrained> {
71    fn default() -> Self {
72        Self::new(100, 20)
73    }
74}
75
76impl Fit<Array2<Float>, ()> for FinancialKernel<Untrained> {
77    type Fitted = FinancialKernel<Trained>;
78
79    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
80        let n_samples = x.nrows();
81        if n_samples == 0 {
82            return Err(SklearsError::InvalidInput(
83                "Input array cannot be empty".to_string(),
84            ));
85        }
86
87        // Feature dimension: returns + volatility + technical indicators
88        let base_features = 10; // Returns features
89        let volatility_features = if self.include_volatility { 5 } else { 0 };
90        let technical_features = 8; // Moving averages, RSI, etc.
91        let feature_dim = base_features + volatility_features + technical_features;
92
93        let mut rng = thread_rng();
94        let normal = Normal::new(0.0, 1.0 / (feature_dim as Float).sqrt())
95            .expect("operation should succeed");
96
97        // Generate random projection
98        let mut projection = Array2::zeros((feature_dim, self.n_components));
99        for i in 0..feature_dim {
100            for j in 0..self.n_components {
101                projection[[i, j]] = normal.sample(&mut rng);
102            }
103        }
104
105        // Initialize feature weights (returns > volatility > indicators)
106        let mut feature_weights = Array1::zeros(3);
107        feature_weights[0] = 1.0; // Returns
108        feature_weights[1] = 0.7; // Volatility
109        feature_weights[2] = 0.5; // Technical indicators
110
111        Ok(FinancialKernel {
112            n_components: self.n_components,
113            window_size: self.window_size,
114            include_volatility: self.include_volatility,
115            projection: Some(projection),
116            feature_weights: Some(feature_weights),
117            _state: PhantomData,
118        })
119    }
120}
121
122impl Transform<Array2<Float>, Array2<Float>> for FinancialKernel<Trained> {
123    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
124        let n_samples = x.nrows();
125        let n_features = x.ncols();
126
127        if n_samples == 0 {
128            return Err(SklearsError::InvalidInput(
129                "Input array cannot be empty".to_string(),
130            ));
131        }
132
133        let projection = self.projection.as_ref().expect("operation should succeed");
134        let feature_dim = projection.nrows();
135        let feature_weights = self
136            .feature_weights
137            .as_ref()
138            .expect("operation should succeed");
139
140        // Extract financial features
141        let mut financial_features = Array2::zeros((n_samples, feature_dim));
142
143        for i in 0..n_samples {
144            let mut feat_idx = 0;
145
146            // Returns features (log returns approximation)
147            for j in 0..10.min(n_features) {
148                if j + 1 < n_features {
149                    let ret = (x[[i, j + 1]] / (x[[i, j]] + 1e-8)).ln();
150                    financial_features[[i, feat_idx]] = ret * feature_weights[0];
151                    feat_idx += 1;
152                }
153            }
154
155            // Volatility features (rolling std approximation)
156            if self.include_volatility {
157                for j in 0..5.min(n_features / 2) {
158                    let mut sum_sq = 0.0;
159                    let window = self.window_size.min(n_features - j);
160                    for k in 0..window {
161                        if j + k < n_features {
162                            sum_sq += (x[[i, j + k]] - x[[i, j]]).powi(2);
163                        }
164                    }
165                    let volatility = (sum_sq / window as Float).sqrt();
166                    if feat_idx < feature_dim {
167                        financial_features[[i, feat_idx]] = volatility * feature_weights[1];
168                        feat_idx += 1;
169                    }
170                }
171            }
172
173            // Technical indicators (simple moving average approximation)
174            for j in 0..8.min(n_features / 3) {
175                let window = self.window_size.min(n_features - j);
176                let mut sma = 0.0;
177                for k in 0..window {
178                    if j + k < n_features {
179                        sma += x[[i, j + k]];
180                    }
181                }
182                sma /= window as Float;
183                if feat_idx < feature_dim {
184                    financial_features[[i, feat_idx]] = sma * feature_weights[2];
185                    feat_idx += 1;
186                }
187            }
188        }
189
190        // Apply random projection
191        let features = financial_features.dot(projection);
192
193        Ok(features)
194    }
195}
196
197// ============================================================================
198// Volatility Kernel
199// ============================================================================
200
201/// Volatility modeling method
202#[derive(Debug, Clone, Copy)]
203pub enum VolatilityModel {
204    /// Simple historical volatility
205    Historical,
206    /// EWMA volatility
207    EWMA,
208    /// GARCH-like volatility
209    GARCH,
210    /// Realized volatility
211    Realized,
212}
213
214/// Kernel method for volatility modeling and forecasting
215///
216/// This kernel focuses on volatility features using various volatility models.
217///
218/// # References
219/// - Bollerslev (1986): "Generalized Autoregressive Conditional Heteroskedasticity"
220pub struct VolatilityKernel<State = Untrained> {
221    /// Number of random features
222    n_components: usize,
223    /// Volatility model to use
224    model: VolatilityModel,
225    /// Decay factor for EWMA (typically 0.94)
226    decay_factor: Float,
227    /// Random projection matrix
228    projection: Option<Array2<Float>>,
229    /// State marker
230    _state: PhantomData<State>,
231}
232
233impl VolatilityKernel<Untrained> {
234    /// Create a new volatility kernel
235    pub fn new(n_components: usize, model: VolatilityModel) -> Self {
236        Self {
237            n_components,
238            model,
239            decay_factor: 0.94,
240            projection: None,
241            _state: PhantomData,
242        }
243    }
244
245    /// Set the decay factor for EWMA
246    pub fn decay_factor(mut self, decay_factor: Float) -> Self {
247        self.decay_factor = decay_factor;
248        self
249    }
250}
251
252impl Default for VolatilityKernel<Untrained> {
253    fn default() -> Self {
254        Self::new(100, VolatilityModel::EWMA)
255    }
256}
257
258impl Fit<Array2<Float>, ()> for VolatilityKernel<Untrained> {
259    type Fitted = VolatilityKernel<Trained>;
260
261    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
262        let n_samples = x.nrows();
263        if n_samples == 0 {
264            return Err(SklearsError::InvalidInput(
265                "Input array cannot be empty".to_string(),
266            ));
267        }
268
269        // Volatility features dimension
270        let feature_dim = 20;
271
272        let mut rng = thread_rng();
273        let normal = Normal::new(0.0, 1.0 / (feature_dim as Float).sqrt())
274            .expect("operation should succeed");
275
276        // Generate random projection
277        let mut projection = Array2::zeros((feature_dim, self.n_components));
278        for i in 0..feature_dim {
279            for j in 0..self.n_components {
280                projection[[i, j]] = normal.sample(&mut rng);
281            }
282        }
283
284        Ok(VolatilityKernel {
285            n_components: self.n_components,
286            model: self.model,
287            decay_factor: self.decay_factor,
288            projection: Some(projection),
289            _state: PhantomData,
290        })
291    }
292}
293
294impl Transform<Array2<Float>, Array2<Float>> for VolatilityKernel<Trained> {
295    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
296        let n_samples = x.nrows();
297        let n_features = x.ncols();
298
299        if n_samples == 0 {
300            return Err(SklearsError::InvalidInput(
301                "Input array cannot be empty".to_string(),
302            ));
303        }
304
305        let projection = self.projection.as_ref().expect("operation should succeed");
306        let feature_dim = projection.nrows();
307
308        // Extract volatility features
309        let mut volatility_features = Array2::zeros((n_samples, feature_dim));
310
311        for i in 0..n_samples {
312            match self.model {
313                VolatilityModel::Historical => {
314                    // Simple historical volatility at different horizons
315                    for j in 0..feature_dim.min(n_features) {
316                        let window = 5 + j;
317                        let mut sum_sq = 0.0;
318                        let count = window.min(n_features);
319                        for k in 0..count {
320                            sum_sq += x[[i, k]].powi(2);
321                        }
322                        volatility_features[[i, j]] = (sum_sq / count as Float).sqrt();
323                    }
324                }
325                VolatilityModel::EWMA => {
326                    // Exponentially weighted moving average volatility
327                    for j in 0..feature_dim.min(n_features) {
328                        let mut ewma_var = x[[i, 0]].powi(2);
329                        for k in 1..n_features {
330                            ewma_var = self.decay_factor * ewma_var
331                                + (1.0 - self.decay_factor) * x[[i, k]].powi(2);
332                        }
333                        volatility_features[[i, j]] = ewma_var.sqrt();
334                    }
335                }
336                VolatilityModel::GARCH => {
337                    // Simplified GARCH(1,1) features
338                    for j in 0..feature_dim.min(n_features) {
339                        let alpha = 0.1;
340                        let beta = 0.85;
341                        let omega = 0.05;
342                        let mut variance = x[[i, 0]].powi(2);
343                        for k in 1..n_features {
344                            variance = omega + alpha * x[[i, k - 1]].powi(2) + beta * variance;
345                        }
346                        volatility_features[[i, j]] = variance.sqrt();
347                    }
348                }
349                VolatilityModel::Realized => {
350                    // Realized volatility (sum of squared returns)
351                    for j in 0..feature_dim.min(n_features) {
352                        let mut realized_var = 0.0;
353                        for k in 0..n_features {
354                            realized_var += x[[i, k]].powi(2);
355                        }
356                        volatility_features[[i, j]] = (realized_var / n_features as Float).sqrt();
357                    }
358                }
359            }
360        }
361
362        // Apply random projection
363        let features = volatility_features.dot(projection);
364
365        Ok(features)
366    }
367}
368
369// ============================================================================
370// Econometric Kernel
371// ============================================================================
372
373/// Kernel method for econometric time series analysis
374///
375/// This kernel implements features based on econometric models including
376/// autoregressive features, cointegration, and structural breaks.
377///
378/// # References
379/// - Hamilton (1994): "Time Series Analysis"
380pub struct EconometricKernel<State = Untrained> {
381    /// Number of random features
382    n_components: usize,
383    /// AR model order
384    ar_order: usize,
385    /// Whether to include difference features
386    include_differences: bool,
387    /// Random projection matrix
388    projection: Option<Array2<Float>>,
389    /// State marker
390    _state: PhantomData<State>,
391}
392
393impl EconometricKernel<Untrained> {
394    /// Create a new econometric kernel
395    pub fn new(n_components: usize, ar_order: usize) -> Self {
396        Self {
397            n_components,
398            ar_order,
399            include_differences: true,
400            projection: None,
401            _state: PhantomData,
402        }
403    }
404
405    /// Set whether to include difference features
406    pub fn include_differences(mut self, include_differences: bool) -> Self {
407        self.include_differences = include_differences;
408        self
409    }
410}
411
412impl Default for EconometricKernel<Untrained> {
413    fn default() -> Self {
414        Self::new(100, 4)
415    }
416}
417
418impl Fit<Array2<Float>, ()> for EconometricKernel<Untrained> {
419    type Fitted = EconometricKernel<Trained>;
420
421    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
422        let n_samples = x.nrows();
423        if n_samples == 0 {
424            return Err(SklearsError::InvalidInput(
425                "Input array cannot be empty".to_string(),
426            ));
427        }
428
429        // Feature dimension based on AR order and differences
430        let base_dim = self.ar_order * 2;
431        let diff_dim = if self.include_differences { 10 } else { 0 };
432        let feature_dim = base_dim + diff_dim;
433
434        let mut rng = thread_rng();
435        let normal = Normal::new(0.0, 1.0 / (feature_dim as Float).sqrt())
436            .expect("operation should succeed");
437
438        // Generate random projection
439        let mut projection = Array2::zeros((feature_dim, self.n_components));
440        for i in 0..feature_dim {
441            for j in 0..self.n_components {
442                projection[[i, j]] = normal.sample(&mut rng);
443            }
444        }
445
446        Ok(EconometricKernel {
447            n_components: self.n_components,
448            ar_order: self.ar_order,
449            include_differences: self.include_differences,
450            projection: Some(projection),
451            _state: PhantomData,
452        })
453    }
454}
455
456impl Transform<Array2<Float>, Array2<Float>> for EconometricKernel<Trained> {
457    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
458        let n_samples = x.nrows();
459        let n_features = x.ncols();
460
461        if n_samples == 0 {
462            return Err(SklearsError::InvalidInput(
463                "Input array cannot be empty".to_string(),
464            ));
465        }
466
467        let projection = self.projection.as_ref().expect("operation should succeed");
468        let feature_dim = projection.nrows();
469
470        // Extract econometric features
471        let mut econ_features = Array2::zeros((n_samples, feature_dim));
472
473        for i in 0..n_samples {
474            let mut feat_idx = 0;
475
476            // AR features (lagged values)
477            for lag in 1..=self.ar_order {
478                if lag < n_features && feat_idx < feature_dim {
479                    econ_features[[i, feat_idx]] = x[[i, n_features - lag - 1]];
480                    feat_idx += 1;
481                }
482            }
483
484            // Autocorrelation approximation
485            for lag in 1..=self.ar_order {
486                if lag < n_features && feat_idx < feature_dim {
487                    let mut autocorr = 0.0;
488                    let count = n_features - lag;
489                    for k in 0..count {
490                        autocorr += x[[i, k]] * x[[i, k + lag]];
491                    }
492                    econ_features[[i, feat_idx]] = autocorr / count as Float;
493                    feat_idx += 1;
494                }
495            }
496
497            // First and second differences
498            if self.include_differences {
499                for j in 0..10.min(n_features - 1) {
500                    if feat_idx < feature_dim {
501                        let diff1 = x[[i, j + 1]] - x[[i, j]];
502                        econ_features[[i, feat_idx]] = diff1;
503                        feat_idx += 1;
504                    }
505                }
506            }
507        }
508
509        // Apply random projection
510        let features = econ_features.dot(projection);
511
512        Ok(features)
513    }
514}
515
516// ============================================================================
517// Portfolio Kernel
518// ============================================================================
519
520/// Kernel method for portfolio optimization and analysis
521///
522/// This kernel extracts portfolio-relevant features including risk-return
523/// characteristics, diversification measures, and factor exposures.
524///
525/// # References
526/// - Markowitz (1952): "Portfolio Selection"
527pub struct PortfolioKernel<State = Untrained> {
528    /// Number of random features
529    n_components: usize,
530    /// Number of assets in portfolio
531    n_assets: usize,
532    /// Risk-free rate for Sharpe ratio
533    risk_free_rate: Float,
534    /// Random projection matrix
535    projection: Option<Array2<Float>>,
536    /// State marker
537    _state: PhantomData<State>,
538}
539
540impl PortfolioKernel<Untrained> {
541    /// Create a new portfolio kernel
542    pub fn new(n_components: usize, n_assets: usize) -> Self {
543        Self {
544            n_components,
545            n_assets,
546            risk_free_rate: 0.02,
547            projection: None,
548            _state: PhantomData,
549        }
550    }
551
552    /// Set the risk-free rate
553    pub fn risk_free_rate(mut self, risk_free_rate: Float) -> Self {
554        self.risk_free_rate = risk_free_rate;
555        self
556    }
557}
558
559impl Default for PortfolioKernel<Untrained> {
560    fn default() -> Self {
561        Self::new(100, 10)
562    }
563}
564
565impl Fit<Array2<Float>, ()> for PortfolioKernel<Untrained> {
566    type Fitted = PortfolioKernel<Trained>;
567
568    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
569        let n_samples = x.nrows();
570        if n_samples == 0 {
571            return Err(SklearsError::InvalidInput(
572                "Input array cannot be empty".to_string(),
573            ));
574        }
575
576        // Portfolio features: returns, risk, correlations, diversification
577        let feature_dim = 30;
578
579        let mut rng = thread_rng();
580        let normal = Normal::new(0.0, 1.0 / (feature_dim as Float).sqrt())
581            .expect("operation should succeed");
582
583        // Generate random projection
584        let mut projection = Array2::zeros((feature_dim, self.n_components));
585        for i in 0..feature_dim {
586            for j in 0..self.n_components {
587                projection[[i, j]] = normal.sample(&mut rng);
588            }
589        }
590
591        Ok(PortfolioKernel {
592            n_components: self.n_components,
593            n_assets: self.n_assets,
594            risk_free_rate: self.risk_free_rate,
595            projection: Some(projection),
596            _state: PhantomData,
597        })
598    }
599}
600
601impl Transform<Array2<Float>, Array2<Float>> for PortfolioKernel<Trained> {
602    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
603        let n_samples = x.nrows();
604        let n_features = x.ncols();
605
606        if n_samples == 0 {
607            return Err(SklearsError::InvalidInput(
608                "Input array cannot be empty".to_string(),
609            ));
610        }
611
612        let projection = self.projection.as_ref().expect("operation should succeed");
613        let feature_dim = projection.nrows();
614
615        // Extract portfolio features
616        let mut portfolio_features = Array2::zeros((n_samples, feature_dim));
617
618        for i in 0..n_samples {
619            let mut feat_idx = 0;
620
621            // Expected return approximation
622            let mut mean_return = 0.0;
623            for j in 0..n_features {
624                mean_return += x[[i, j]];
625            }
626            mean_return /= n_features as Float;
627            if feat_idx < feature_dim {
628                portfolio_features[[i, feat_idx]] = mean_return;
629                feat_idx += 1;
630            }
631
632            // Portfolio volatility approximation
633            let mut variance = 0.0;
634            for j in 0..n_features {
635                variance += (x[[i, j]] - mean_return).powi(2);
636            }
637            let volatility = (variance / n_features as Float).sqrt();
638            if feat_idx < feature_dim {
639                portfolio_features[[i, feat_idx]] = volatility;
640                feat_idx += 1;
641            }
642
643            // Sharpe ratio approximation
644            if volatility > 1e-8 && feat_idx < feature_dim {
645                let sharpe = (mean_return - self.risk_free_rate) / volatility;
646                portfolio_features[[i, feat_idx]] = sharpe;
647                feat_idx += 1;
648            }
649
650            // Diversification measure (inverse of concentration)
651            let mut herfindahl = 0.0;
652            let weight = 1.0 / n_features as Float;
653            for _ in 0..n_features {
654                herfindahl += weight.powi(2);
655            }
656            let diversification = 1.0 - herfindahl;
657            if feat_idx < feature_dim {
658                portfolio_features[[i, feat_idx]] = diversification;
659                feat_idx += 1;
660            }
661
662            // Correlation-based features (simplified)
663            for j in 0..10.min(n_features) {
664                if j + 1 < n_features && feat_idx < feature_dim {
665                    let corr_approx = x[[i, j]] * x[[i, j + 1]];
666                    portfolio_features[[i, feat_idx]] = corr_approx;
667                    feat_idx += 1;
668                }
669            }
670        }
671
672        // Apply random projection
673        let features = portfolio_features.dot(projection);
674
675        Ok(features)
676    }
677}
678
679// ============================================================================
680// Risk Kernel
681// ============================================================================
682
683/// Kernel method for financial risk analysis
684///
685/// This kernel computes risk-based features including Value-at-Risk (VaR),
686/// Conditional VaR (CVaR), and risk contribution measures.
687///
688/// # References
689/// - Jorion (2006): "Value at Risk: The New Benchmark for Managing Financial Risk"
690pub struct RiskKernel<State = Untrained> {
691    /// Number of random features
692    n_components: usize,
693    /// Confidence level for VaR (e.g., 0.95 for 95% VaR)
694    confidence_level: Float,
695    /// Whether to include tail risk measures
696    include_tail_risk: bool,
697    /// Random projection matrix
698    projection: Option<Array2<Float>>,
699    /// State marker
700    _state: PhantomData<State>,
701}
702
703impl RiskKernel<Untrained> {
704    /// Create a new risk kernel
705    pub fn new(n_components: usize, confidence_level: Float) -> Self {
706        Self {
707            n_components,
708            confidence_level,
709            include_tail_risk: true,
710            projection: None,
711            _state: PhantomData,
712        }
713    }
714
715    /// Set whether to include tail risk measures
716    pub fn include_tail_risk(mut self, include_tail_risk: bool) -> Self {
717        self.include_tail_risk = include_tail_risk;
718        self
719    }
720}
721
722impl Default for RiskKernel<Untrained> {
723    fn default() -> Self {
724        Self::new(100, 0.95)
725    }
726}
727
728impl Fit<Array2<Float>, ()> for RiskKernel<Untrained> {
729    type Fitted = RiskKernel<Trained>;
730
731    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
732        let n_samples = x.nrows();
733        if n_samples == 0 {
734            return Err(SklearsError::InvalidInput(
735                "Input array cannot be empty".to_string(),
736            ));
737        }
738
739        // Risk features dimension
740        let base_dim = 15;
741        let tail_dim = if self.include_tail_risk { 10 } else { 0 };
742        let feature_dim = base_dim + tail_dim;
743
744        let mut rng = thread_rng();
745        let normal = Normal::new(0.0, 1.0 / (feature_dim as Float).sqrt())
746            .expect("operation should succeed");
747
748        // Generate random projection
749        let mut projection = Array2::zeros((feature_dim, self.n_components));
750        for i in 0..feature_dim {
751            for j in 0..self.n_components {
752                projection[[i, j]] = normal.sample(&mut rng);
753            }
754        }
755
756        Ok(RiskKernel {
757            n_components: self.n_components,
758            confidence_level: self.confidence_level,
759            include_tail_risk: self.include_tail_risk,
760            projection: Some(projection),
761            _state: PhantomData,
762        })
763    }
764}
765
766impl Transform<Array2<Float>, Array2<Float>> for RiskKernel<Trained> {
767    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
768        let n_samples = x.nrows();
769        let n_features = x.ncols();
770
771        if n_samples == 0 {
772            return Err(SklearsError::InvalidInput(
773                "Input array cannot be empty".to_string(),
774            ));
775        }
776
777        let projection = self.projection.as_ref().expect("operation should succeed");
778        let feature_dim = projection.nrows();
779
780        // Extract risk features
781        let mut risk_features = Array2::zeros((n_samples, feature_dim));
782
783        for i in 0..n_samples {
784            let mut feat_idx = 0;
785
786            // VaR approximation using parametric method
787            let mut mean = 0.0;
788            let mut variance = 0.0;
789            for j in 0..n_features {
790                mean += x[[i, j]];
791            }
792            mean /= n_features as Float;
793
794            for j in 0..n_features {
795                variance += (x[[i, j]] - mean).powi(2);
796            }
797            variance /= n_features as Float;
798            let std_dev = variance.sqrt();
799
800            // Normal VaR (parametric)
801            let z_score = 1.645; // Approximate z-score for 95% confidence
802            let var = mean - z_score * std_dev;
803            if feat_idx < feature_dim {
804                risk_features[[i, feat_idx]] = var;
805                feat_idx += 1;
806            }
807
808            // CVaR approximation (expected shortfall)
809            let cvar = mean - (2.5 * std_dev); // Simplified CVaR
810            if feat_idx < feature_dim {
811                risk_features[[i, feat_idx]] = cvar;
812                feat_idx += 1;
813            }
814
815            // Downside deviation
816            let mut downside_var = 0.0;
817            let mut downside_count = 0;
818            for j in 0..n_features {
819                if x[[i, j]] < mean {
820                    downside_var += (x[[i, j]] - mean).powi(2);
821                    downside_count += 1;
822                }
823            }
824            if downside_count > 0 {
825                let downside_dev = (downside_var / downside_count as Float).sqrt();
826                if feat_idx < feature_dim {
827                    risk_features[[i, feat_idx]] = downside_dev;
828                    feat_idx += 1;
829                }
830            }
831
832            // Maximum drawdown approximation
833            let mut max_dd = 0.0;
834            let mut peak = x[[i, 0]];
835            for j in 0..n_features {
836                if x[[i, j]] > peak {
837                    peak = x[[i, j]];
838                }
839                let dd = (peak - x[[i, j]]) / (peak.abs() + 1e-8);
840                if dd > max_dd {
841                    max_dd = dd;
842                }
843            }
844            if feat_idx < feature_dim {
845                risk_features[[i, feat_idx]] = max_dd;
846                feat_idx += 1;
847            }
848
849            // Tail risk measures
850            if self.include_tail_risk {
851                // Skewness approximation
852                let mut skew = 0.0;
853                for j in 0..n_features {
854                    skew += ((x[[i, j]] - mean) / (std_dev + 1e-8)).powi(3);
855                }
856                skew /= n_features as Float;
857                if feat_idx < feature_dim {
858                    risk_features[[i, feat_idx]] = skew;
859                    feat_idx += 1;
860                }
861
862                // Kurtosis approximation
863                let mut kurt = 0.0;
864                for j in 0..n_features {
865                    kurt += ((x[[i, j]] - mean) / (std_dev + 1e-8)).powi(4);
866                }
867                kurt /= n_features as Float;
868                if feat_idx < feature_dim {
869                    risk_features[[i, feat_idx]] = kurt - 3.0; // Excess kurtosis
870                }
871            }
872        }
873
874        // Apply random projection
875        let features = risk_features.dot(projection);
876
877        Ok(features)
878    }
879}
880
881// ============================================================================
882// Tests
883// ============================================================================
884
885#[cfg(test)]
886mod tests {
887    use super::*;
888    use scirs2_core::ndarray::array;
889
890    #[test]
891    fn test_financial_kernel_basic() {
892        let x = array![[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]];
893
894        let kernel = FinancialKernel::new(50, 10);
895        let fitted = kernel.fit(&x, &()).expect("operation should succeed");
896        let features = fitted.transform(&x).expect("operation should succeed");
897
898        assert_eq!(features.shape(), &[2, 50]);
899    }
900
901    #[test]
902    fn test_financial_kernel_volatility() {
903        let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
904
905        let kernel = FinancialKernel::new(40, 5).include_volatility(true);
906        let fitted = kernel.fit(&x, &()).expect("operation should succeed");
907        let features = fitted.transform(&x).expect("operation should succeed");
908
909        assert_eq!(features.shape(), &[2, 40]);
910    }
911
912    #[test]
913    fn test_volatility_kernel_ewma() {
914        let x = array![[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]];
915
916        let kernel = VolatilityKernel::new(50, VolatilityModel::EWMA).decay_factor(0.94);
917        let fitted = kernel.fit(&x, &()).expect("operation should succeed");
918        let features = fitted.transform(&x).expect("operation should succeed");
919
920        assert_eq!(features.shape(), &[2, 50]);
921    }
922
923    #[test]
924    fn test_volatility_models() {
925        let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
926
927        let models = vec![
928            VolatilityModel::Historical,
929            VolatilityModel::EWMA,
930            VolatilityModel::GARCH,
931            VolatilityModel::Realized,
932        ];
933
934        for model in models {
935            let kernel = VolatilityKernel::new(30, model);
936            let fitted = kernel.fit(&x, &()).expect("operation should succeed");
937            let features = fitted.transform(&x).expect("operation should succeed");
938            assert_eq!(features.shape(), &[2, 30]);
939        }
940    }
941
942    #[test]
943    fn test_econometric_kernel_basic() {
944        let x = array![[1.0, 2.0, 3.0, 4.0, 5.0], [6.0, 7.0, 8.0, 9.0, 10.0]];
945
946        let kernel = EconometricKernel::new(50, 3);
947        let fitted = kernel.fit(&x, &()).expect("operation should succeed");
948        let features = fitted.transform(&x).expect("operation should succeed");
949
950        assert_eq!(features.shape(), &[2, 50]);
951    }
952
953    #[test]
954    fn test_econometric_kernel_differences() {
955        let x = array![[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]];
956
957        let kernel = EconometricKernel::new(40, 2).include_differences(true);
958        let fitted = kernel.fit(&x, &()).expect("operation should succeed");
959        let features = fitted.transform(&x).expect("operation should succeed");
960
961        assert_eq!(features.shape(), &[2, 40]);
962    }
963
964    #[test]
965    fn test_portfolio_kernel_basic() {
966        let x = array![[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]];
967
968        let kernel = PortfolioKernel::new(50, 4);
969        let fitted = kernel.fit(&x, &()).expect("operation should succeed");
970        let features = fitted.transform(&x).expect("operation should succeed");
971
972        assert_eq!(features.shape(), &[2, 50]);
973    }
974
975    #[test]
976    fn test_portfolio_kernel_risk_free_rate() {
977        let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
978
979        let kernel = PortfolioKernel::new(40, 3).risk_free_rate(0.03);
980        let fitted = kernel.fit(&x, &()).expect("operation should succeed");
981        let features = fitted.transform(&x).expect("operation should succeed");
982
983        assert_eq!(features.shape(), &[2, 40]);
984    }
985
986    #[test]
987    fn test_risk_kernel_basic() {
988        let x = array![[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]];
989
990        let kernel = RiskKernel::new(50, 0.95);
991        let fitted = kernel.fit(&x, &()).expect("operation should succeed");
992        let features = fitted.transform(&x).expect("operation should succeed");
993
994        assert_eq!(features.shape(), &[2, 50]);
995    }
996
997    #[test]
998    fn test_risk_kernel_tail_risk() {
999        let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1000
1001        let kernel = RiskKernel::new(40, 0.99).include_tail_risk(true);
1002        let fitted = kernel.fit(&x, &()).expect("operation should succeed");
1003        let features = fitted.transform(&x).expect("operation should succeed");
1004
1005        assert_eq!(features.shape(), &[2, 40]);
1006    }
1007
1008    #[test]
1009    fn test_empty_input_error() {
1010        let x_empty: Array2<Float> = Array2::zeros((0, 3));
1011
1012        let kernel = FinancialKernel::new(50, 10);
1013        assert!(kernel.fit(&x_empty, &()).is_err());
1014
1015        let kernel2 = VolatilityKernel::new(50, VolatilityModel::EWMA);
1016        assert!(kernel2.fit(&x_empty, &()).is_err());
1017    }
1018}