rustkernel_treasury/
liquidity.rs

1//! Liquidity optimization kernel.
2//!
3//! This module provides liquidity optimization for treasury:
4//! - LCR (Liquidity Coverage Ratio) calculation
5//! - NSFR (Net Stable Funding Ratio) calculation
6//! - Liquidity optimization recommendations
7
8use crate::types::{
9    LCRResult, LiquidityAction, LiquidityActionType, LiquidityAssetType,
10    LiquidityOptimizationResult, LiquidityOutflow, LiquidityPosition, NSFRResult, OutflowCategory,
11};
12use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
13use std::collections::HashMap;
14
15// ============================================================================
16// Liquidity Optimization Kernel
17// ============================================================================
18
19/// Liquidity optimization kernel.
20///
21/// Calculates LCR/NSFR ratios and recommends optimization actions.
22#[derive(Debug, Clone)]
23pub struct LiquidityOptimization {
24    metadata: KernelMetadata,
25}
26
27impl Default for LiquidityOptimization {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl LiquidityOptimization {
34    /// Create a new liquidity optimization kernel.
35    #[must_use]
36    pub fn new() -> Self {
37        Self {
38            metadata: KernelMetadata::batch("treasury/liquidity-opt", Domain::TreasuryManagement)
39                .with_description("Liquidity ratio optimization (LCR/NSFR)")
40                .with_throughput(5_000)
41                .with_latency_us(1000.0),
42        }
43    }
44
45    /// Calculate LCR (Liquidity Coverage Ratio).
46    pub fn calculate_lcr(
47        assets: &[LiquidityPosition],
48        outflows: &[LiquidityOutflow],
49        inflows: &[LiquidityInflow],
50        config: &LCRConfig,
51    ) -> LCRResult {
52        // Calculate HQLA by level
53        let mut hqla_breakdown: HashMap<String, f64> = HashMap::new();
54        let mut level1 = 0.0;
55        let mut level2a = 0.0;
56        let mut level2b = 0.0;
57
58        for asset in assets {
59            let haircut_value = asset.amount * (1.0 - asset.lcr_haircut);
60
61            match asset.asset_type {
62                LiquidityAssetType::CashReserves | LiquidityAssetType::Level1HQLA => {
63                    level1 += haircut_value;
64                }
65                LiquidityAssetType::Level2AHQLA => {
66                    level2a += haircut_value;
67                }
68                LiquidityAssetType::Level2BHQLA => {
69                    level2b += haircut_value;
70                }
71                _ => {}
72            }
73        }
74
75        // Apply caps (Basel III compliant)
76        // Level 2A cap: L2A <= cap% of total HQLA
77        // This means L2A <= cap% * (L1 + L2A), solving: L2A <= L1 * cap / (1 - cap)
78        let max_l2a_from_cap = if config.level2a_cap < 1.0 {
79            level1 * config.level2a_cap / (1.0 - config.level2a_cap)
80        } else {
81            f64::INFINITY
82        };
83        let capped_level2a = level2a.min(max_l2a_from_cap);
84
85        // Level 2B cap: L2B <= cap% of total HQLA (considering L1 + L2A + L2B)
86        let max_l2b_from_cap = if config.level2b_cap < 1.0 {
87            (level1 + capped_level2a) * config.level2b_cap / (1.0 - config.level2b_cap)
88        } else {
89            f64::INFINITY
90        };
91        let capped_level2b = level2b.min(max_l2b_from_cap);
92
93        // Total Level 2 cap
94        let max_total_l2_from_cap = if config.level2_total_cap < 1.0 {
95            level1 * config.level2_total_cap / (1.0 - config.level2_total_cap)
96        } else {
97            f64::INFINITY
98        };
99        let total_level2 = (capped_level2a + capped_level2b).min(max_total_l2_from_cap);
100
101        let hqla = level1 + total_level2;
102
103        hqla_breakdown.insert("Level1".to_string(), level1);
104        hqla_breakdown.insert("Level2A".to_string(), capped_level2a);
105        hqla_breakdown.insert("Level2B".to_string(), capped_level2b);
106        hqla_breakdown.insert("Total".to_string(), hqla);
107
108        // Calculate net outflows
109        let gross_outflows: f64 = outflows
110            .iter()
111            .filter(|o| o.days_to_maturity <= 30)
112            .map(|o| o.amount * o.runoff_rate)
113            .sum();
114
115        let gross_inflows: f64 = inflows
116            .iter()
117            .filter(|i| i.days_to_maturity <= 30)
118            .map(|i| i.amount * i.inflow_rate)
119            .sum();
120
121        // Inflow cap: max 75% of outflows
122        let capped_inflows = gross_inflows.min(gross_outflows * config.inflow_cap);
123        let net_outflows = (gross_outflows - capped_inflows).max(0.0);
124
125        // Calculate LCR
126        let lcr_ratio = if net_outflows > 0.0 {
127            hqla / net_outflows
128        } else {
129            f64::INFINITY
130        };
131
132        let is_compliant = lcr_ratio >= config.min_lcr;
133        let buffer = if is_compliant {
134            hqla - (net_outflows * config.min_lcr)
135        } else {
136            hqla - (net_outflows * config.min_lcr) // Will be negative
137        };
138
139        LCRResult {
140            hqla,
141            net_outflows,
142            lcr_ratio,
143            is_compliant,
144            buffer,
145            hqla_breakdown,
146        }
147    }
148
149    /// Calculate NSFR (Net Stable Funding Ratio).
150    pub fn calculate_nsfr(
151        assets: &[LiquidityPosition],
152        funding: &[FundingSource],
153        config: &NSFRConfig,
154    ) -> NSFRResult {
155        // Calculate Available Stable Funding (ASF)
156        let asf: f64 = funding
157            .iter()
158            .map(|f| f.amount * Self::get_asf_factor(f, config))
159            .sum();
160
161        // Calculate Required Stable Funding (RSF)
162        let rsf: f64 = assets
163            .iter()
164            .map(|a| a.amount * Self::get_rsf_factor(a, config))
165            .sum();
166
167        // Calculate NSFR
168        let nsfr_ratio = if rsf > 0.0 { asf / rsf } else { f64::INFINITY };
169
170        let is_compliant = nsfr_ratio >= config.min_nsfr;
171        let buffer = asf - (rsf * config.min_nsfr);
172
173        NSFRResult {
174            asf,
175            rsf,
176            nsfr_ratio,
177            is_compliant,
178            buffer,
179        }
180    }
181
182    /// Get ASF (Available Stable Funding) factor for funding source.
183    fn get_asf_factor(funding: &FundingSource, config: &NSFRConfig) -> f64 {
184        match funding.funding_type {
185            FundingType::Equity => 1.0,
186            FundingType::LongTermDebt => {
187                if funding.remaining_maturity_days > 365 {
188                    1.0
189                } else if funding.remaining_maturity_days > 180 {
190                    config.asf_6m_1y
191                } else {
192                    config.asf_under_6m
193                }
194            }
195            FundingType::RetailDeposit => {
196                if funding.is_stable {
197                    config.asf_stable_retail
198                } else {
199                    config.asf_less_stable_retail
200                }
201            }
202            FundingType::WholesaleDeposit => {
203                if funding.remaining_maturity_days > 365 {
204                    1.0
205                } else {
206                    config.asf_wholesale
207                }
208            }
209            FundingType::Other => config.asf_other,
210        }
211    }
212
213    /// Get RSF (Required Stable Funding) factor for asset.
214    fn get_rsf_factor(asset: &LiquidityPosition, config: &NSFRConfig) -> f64 {
215        match asset.asset_type {
216            LiquidityAssetType::CashReserves => 0.0,
217            LiquidityAssetType::Level1HQLA => config.rsf_level1,
218            LiquidityAssetType::Level2AHQLA => config.rsf_level2a,
219            LiquidityAssetType::Level2BHQLA => config.rsf_level2b,
220            LiquidityAssetType::OtherLiquid => {
221                if asset.days_to_liquidate <= 30 {
222                    config.rsf_other_liquid
223                } else {
224                    config.rsf_illiquid
225                }
226            }
227            LiquidityAssetType::Illiquid => config.rsf_illiquid,
228        }
229    }
230
231    /// Optimize liquidity ratios.
232    pub fn optimize(
233        assets: &[LiquidityPosition],
234        outflows: &[LiquidityOutflow],
235        inflows: &[LiquidityInflow],
236        funding: &[FundingSource],
237        config: &OptimizationConfig,
238    ) -> LiquidityOptimizationResult {
239        let lcr_before = Self::calculate_lcr(assets, outflows, inflows, &config.lcr_config);
240        let nsfr_before = Self::calculate_nsfr(assets, funding, &config.nsfr_config);
241
242        let mut actions = Vec::new();
243        let mut total_cost = 0.0;
244
245        // Generate optimization actions if below targets
246        if !lcr_before.is_compliant || lcr_before.lcr_ratio < config.target_lcr {
247            let lcr_actions = Self::generate_lcr_actions(assets, outflows, &lcr_before, config);
248            for action in lcr_actions {
249                total_cost += action.cost;
250                actions.push(action);
251            }
252        }
253
254        if !nsfr_before.is_compliant || nsfr_before.nsfr_ratio < config.target_nsfr {
255            let nsfr_actions = Self::generate_nsfr_actions(assets, funding, &nsfr_before, config);
256            for action in nsfr_actions {
257                total_cost += action.cost;
258                actions.push(action);
259            }
260        }
261
262        // Calculate actual improvement by simulating actions applied
263        let lcr_improvement = Self::calculate_lcr_improvement(
264            assets,
265            outflows,
266            inflows,
267            &actions,
268            &lcr_before,
269            &config.lcr_config,
270        );
271
272        LiquidityOptimizationResult {
273            lcr: lcr_before,
274            nsfr: nsfr_before,
275            actions,
276            total_cost,
277            lcr_improvement,
278        }
279    }
280
281    /// Generate LCR improvement actions.
282    fn generate_lcr_actions(
283        assets: &[LiquidityPosition],
284        outflows: &[LiquidityOutflow],
285        lcr: &LCRResult,
286        config: &OptimizationConfig,
287    ) -> Vec<LiquidityAction> {
288        let mut actions = Vec::new();
289        let shortfall = if lcr.buffer < 0.0 { -lcr.buffer } else { 0.0 };
290
291        if shortfall == 0.0 {
292            return actions;
293        }
294
295        // Action 1: Convert non-HQLA to HQLA
296        for (i, asset) in assets.iter().enumerate() {
297            if matches!(
298                asset.asset_type,
299                LiquidityAssetType::OtherLiquid | LiquidityAssetType::Illiquid
300            ) {
301                let convert_amount = asset.amount.min(shortfall);
302                let cost = convert_amount * config.conversion_cost_rate;
303                let lcr_impact = convert_amount * (1.0 - asset.lcr_haircut);
304
305                actions.push(LiquidityAction {
306                    action_type: LiquidityActionType::ConvertToHQLA,
307                    target_id: format!("asset_{}", i),
308                    amount: convert_amount,
309                    lcr_impact,
310                    cost,
311                });
312
313                if actions.iter().map(|a| a.lcr_impact).sum::<f64>() >= shortfall {
314                    break;
315                }
316            }
317        }
318
319        // Action 2: Reduce outflow commitments
320        for (i, outflow) in outflows.iter().enumerate() {
321            if matches!(outflow.category, OutflowCategory::CommittedFacilities) {
322                let reduce_amount = outflow.amount * 0.1; // Max 10% reduction
323                let cost = reduce_amount * config.commitment_reduction_cost;
324                let lcr_impact = reduce_amount * outflow.runoff_rate;
325
326                actions.push(LiquidityAction {
327                    action_type: LiquidityActionType::ReduceCommitment,
328                    target_id: format!("outflow_{}", i),
329                    amount: reduce_amount,
330                    lcr_impact,
331                    cost,
332                });
333            }
334        }
335
336        actions
337    }
338
339    /// Generate NSFR improvement actions.
340    fn generate_nsfr_actions(
341        assets: &[LiquidityPosition],
342        funding: &[FundingSource],
343        nsfr: &NSFRResult,
344        config: &OptimizationConfig,
345    ) -> Vec<LiquidityAction> {
346        let mut actions = Vec::new();
347        let shortfall = if nsfr.buffer < 0.0 { -nsfr.buffer } else { 0.0 };
348
349        if shortfall == 0.0 {
350            return actions;
351        }
352
353        // Action 1: Issue term funding
354        for (i, fund) in funding.iter().enumerate() {
355            if fund.remaining_maturity_days < 365
356                && matches!(fund.funding_type, FundingType::WholesaleDeposit)
357            {
358                let extend_amount = fund.amount.min(shortfall);
359                let cost = extend_amount
360                    * config.term_funding_spread
361                    * (365.0 - fund.remaining_maturity_days as f64)
362                    / 365.0;
363                let asf_improvement = extend_amount * (1.0 - config.nsfr_config.asf_wholesale);
364
365                actions.push(LiquidityAction {
366                    action_type: LiquidityActionType::IssueTerm,
367                    target_id: format!("funding_{}", i),
368                    amount: extend_amount,
369                    lcr_impact: asf_improvement, // Reusing field for NSFR impact
370                    cost,
371                });
372
373                if actions.iter().map(|a| a.lcr_impact).sum::<f64>() >= shortfall {
374                    break;
375                }
376            }
377        }
378
379        // Action 2: Sell illiquid assets
380        for (i, asset) in assets.iter().enumerate() {
381            if matches!(asset.asset_type, LiquidityAssetType::Illiquid) {
382                let sell_amount = asset.amount.min(shortfall);
383                let cost = sell_amount * config.illiquid_sale_haircut;
384                let rsf_reduction = sell_amount * config.nsfr_config.rsf_illiquid;
385
386                actions.push(LiquidityAction {
387                    action_type: LiquidityActionType::SellIlliquid,
388                    target_id: format!("asset_{}", i),
389                    amount: sell_amount,
390                    lcr_impact: rsf_reduction,
391                    cost,
392                });
393            }
394        }
395
396        actions
397    }
398
399    /// Calculate liquidity stress metrics.
400    pub fn stress_test(
401        assets: &[LiquidityPosition],
402        outflows: &[LiquidityOutflow],
403        inflows: &[LiquidityInflow],
404        scenario: &StressScenario,
405    ) -> StressTestResult {
406        // Apply stress factors to outflows
407        let stressed_outflows: Vec<LiquidityOutflow> = outflows
408            .iter()
409            .map(|o| {
410                let stress_factor = scenario
411                    .outflow_multipliers
412                    .get(&o.category)
413                    .copied()
414                    .unwrap_or(scenario.default_outflow_multiplier);
415                LiquidityOutflow {
416                    category: o.category,
417                    amount: o.amount,
418                    currency: o.currency.clone(),
419                    runoff_rate: (o.runoff_rate * stress_factor).min(1.0),
420                    days_to_maturity: o.days_to_maturity,
421                }
422            })
423            .collect();
424
425        // Apply haircut to assets
426        let stressed_assets: Vec<LiquidityPosition> = assets
427            .iter()
428            .map(|a| LiquidityPosition {
429                id: a.id.clone(),
430                asset_type: a.asset_type,
431                amount: a.amount,
432                currency: a.currency.clone(),
433                hqla_level: a.hqla_level,
434                lcr_haircut: (a.lcr_haircut + scenario.additional_haircut).min(1.0),
435                days_to_liquidate: (a.days_to_liquidate as f64 * scenario.liquidation_delay_factor)
436                    as u32,
437            })
438            .collect();
439
440        // Reduce inflows
441        let stressed_inflows: Vec<LiquidityInflow> = inflows
442            .iter()
443            .map(|i| LiquidityInflow {
444                category: i.category.clone(),
445                amount: i.amount,
446                currency: i.currency.clone(),
447                inflow_rate: i.inflow_rate * scenario.inflow_reduction,
448                days_to_maturity: i.days_to_maturity,
449            })
450            .collect();
451
452        let lcr_config = LCRConfig::default();
453        let base_lcr = Self::calculate_lcr(assets, outflows, inflows, &lcr_config);
454        let stressed_lcr = Self::calculate_lcr(
455            &stressed_assets,
456            &stressed_outflows,
457            &stressed_inflows,
458            &lcr_config,
459        );
460
461        StressTestResult {
462            scenario_name: scenario.name.clone(),
463            base_lcr: base_lcr.lcr_ratio,
464            stressed_lcr: stressed_lcr.lcr_ratio,
465            lcr_impact: stressed_lcr.lcr_ratio - base_lcr.lcr_ratio,
466            survives_stress: stressed_lcr.is_compliant,
467            days_until_breach: Self::estimate_days_until_breach(&stressed_lcr),
468        }
469    }
470
471    /// Calculate actual LCR improvement by simulating actions applied.
472    fn calculate_lcr_improvement(
473        assets: &[LiquidityPosition],
474        outflows: &[LiquidityOutflow],
475        inflows: &[LiquidityInflow],
476        actions: &[LiquidityAction],
477        lcr_before: &LCRResult,
478        config: &LCRConfig,
479    ) -> f64 {
480        if actions.is_empty() {
481            return 0.0;
482        }
483
484        // Create modified copies of assets and outflows
485        let mut modified_assets: Vec<LiquidityPosition> = assets.to_vec();
486        let mut modified_outflows: Vec<LiquidityOutflow> = outflows.to_vec();
487
488        // Apply each action
489        for action in actions {
490            match action.action_type {
491                LiquidityActionType::ConvertToHQLA => {
492                    // Parse asset index from target_id (e.g., "asset_0")
493                    if let Some(idx_str) = action.target_id.strip_prefix("asset_") {
494                        if let Ok(idx) = idx_str.parse::<usize>() {
495                            if idx < modified_assets.len() {
496                                // Extract data we need before modifying
497                                let asset_id = modified_assets[idx].id.clone();
498                                let asset_currency = modified_assets[idx].currency.clone();
499
500                                // Reduce original asset amount
501                                modified_assets[idx].amount -= action.amount;
502
503                                // Add new Level 1 HQLA position
504                                modified_assets.push(LiquidityPosition {
505                                    id: format!("{}_converted", asset_id),
506                                    asset_type: LiquidityAssetType::Level1HQLA,
507                                    amount: action.amount,
508                                    currency: asset_currency,
509                                    hqla_level: Some(1),
510                                    lcr_haircut: 0.0,
511                                    days_to_liquidate: 1,
512                                });
513                            }
514                        }
515                    }
516                }
517                LiquidityActionType::ReduceCommitment => {
518                    // Parse outflow index from target_id (e.g., "outflow_0")
519                    if let Some(idx_str) = action.target_id.strip_prefix("outflow_") {
520                        if let Ok(idx) = idx_str.parse::<usize>() {
521                            if idx < modified_outflows.len() {
522                                // Reduce outflow amount
523                                modified_outflows[idx].amount -= action.amount;
524                            }
525                        }
526                    }
527                }
528                _ => {
529                    // Other action types (NSFR-related) don't affect LCR
530                }
531            }
532        }
533
534        // Recalculate LCR with modified positions
535        let lcr_after = Self::calculate_lcr(&modified_assets, &modified_outflows, inflows, config);
536
537        // Return the improvement in HQLA buffer (positive = improvement)
538        lcr_after.buffer - lcr_before.buffer
539    }
540
541    /// Estimate days until LCR breach under stress.
542    ///
543    /// Uses a liquidity runoff model considering:
544    /// - Current LCR ratio and buffer
545    /// - Daily net outflow rate under stress
546    /// - HQLA depletion trajectory
547    fn estimate_days_until_breach(lcr: &LCRResult) -> Option<u32> {
548        if lcr.is_compliant {
549            return None;
550        }
551
552        // Calculate daily net outflow rate (30-day outflows spread daily)
553        let daily_outflow = lcr.net_outflows / 30.0;
554
555        if daily_outflow <= 0.0 {
556            // No outflows, won't breach
557            return None;
558        }
559
560        // Current HQLA level
561        let current_hqla = lcr.hqla;
562
563        // Calculate minimum HQLA needed for compliance (LCR = 100%)
564        // LCR = HQLA / NetOutflows >= 1.0
565        // We're already below compliance, so estimate how long until
566        // HQLA depletes to a critical level (e.g., 50% of net outflows)
567        let critical_hqla = lcr.net_outflows * 0.5;
568
569        // Days until HQLA drops below critical threshold
570        // Assuming linear HQLA depletion at daily outflow rate
571        if current_hqla <= critical_hqla {
572            // Already at critical level
573            return Some(0);
574        }
575
576        let hqla_buffer = current_hqla - critical_hqla;
577        let days = (hqla_buffer / daily_outflow).ceil() as u32;
578
579        // Cap at reasonable maximum (regulatory typically look at 30-day horizon)
580        Some(days.min(90))
581    }
582}
583
584impl GpuKernel for LiquidityOptimization {
585    fn metadata(&self) -> &KernelMetadata {
586        &self.metadata
587    }
588}
589
590/// LCR configuration.
591#[derive(Debug, Clone)]
592pub struct LCRConfig {
593    /// Minimum LCR (usually 1.0 = 100%).
594    pub min_lcr: f64,
595    /// Level 2A cap.
596    pub level2a_cap: f64,
597    /// Level 2B cap.
598    pub level2b_cap: f64,
599    /// Total Level 2 cap.
600    pub level2_total_cap: f64,
601    /// Inflow cap.
602    pub inflow_cap: f64,
603}
604
605impl Default for LCRConfig {
606    fn default() -> Self {
607        Self {
608            min_lcr: 1.0,
609            level2a_cap: 0.40,
610            level2b_cap: 0.15,
611            level2_total_cap: 0.40,
612            inflow_cap: 0.75,
613        }
614    }
615}
616
617/// NSFR configuration.
618#[derive(Debug, Clone)]
619pub struct NSFRConfig {
620    /// Minimum NSFR.
621    pub min_nsfr: f64,
622    /// ASF factor for stable retail deposits.
623    pub asf_stable_retail: f64,
624    /// ASF factor for less stable retail deposits.
625    pub asf_less_stable_retail: f64,
626    /// ASF factor for wholesale funding.
627    pub asf_wholesale: f64,
628    /// ASF factor for 6-month to 1-year funding.
629    pub asf_6m_1y: f64,
630    /// ASF factor for under 6-month funding.
631    pub asf_under_6m: f64,
632    /// ASF factor for other stable funding.
633    pub asf_other: f64,
634    /// RSF factor for Level 1 HQLA.
635    pub rsf_level1: f64,
636    /// RSF factor for Level 2A HQLA.
637    pub rsf_level2a: f64,
638    /// RSF factor for Level 2B HQLA.
639    pub rsf_level2b: f64,
640    /// RSF factor for other liquid assets.
641    pub rsf_other_liquid: f64,
642    /// RSF factor for illiquid assets.
643    pub rsf_illiquid: f64,
644}
645
646impl Default for NSFRConfig {
647    fn default() -> Self {
648        Self {
649            min_nsfr: 1.0,
650            asf_stable_retail: 0.95,
651            asf_less_stable_retail: 0.90,
652            asf_wholesale: 0.50,
653            asf_6m_1y: 0.50,
654            asf_under_6m: 0.0,
655            asf_other: 0.0,
656            rsf_level1: 0.0,
657            rsf_level2a: 0.15,
658            rsf_level2b: 0.50,
659            rsf_other_liquid: 0.50,
660            rsf_illiquid: 1.0,
661        }
662    }
663}
664
665/// Optimization configuration.
666#[derive(Debug, Clone)]
667pub struct OptimizationConfig {
668    /// Target LCR.
669    pub target_lcr: f64,
670    /// Target NSFR.
671    pub target_nsfr: f64,
672    /// LCR config.
673    pub lcr_config: LCRConfig,
674    /// NSFR config.
675    pub nsfr_config: NSFRConfig,
676    /// Conversion cost rate.
677    pub conversion_cost_rate: f64,
678    /// Commitment reduction cost.
679    pub commitment_reduction_cost: f64,
680    /// Term funding spread.
681    pub term_funding_spread: f64,
682    /// Illiquid asset sale haircut.
683    pub illiquid_sale_haircut: f64,
684}
685
686impl Default for OptimizationConfig {
687    fn default() -> Self {
688        Self {
689            target_lcr: 1.1,
690            target_nsfr: 1.1,
691            lcr_config: LCRConfig::default(),
692            nsfr_config: NSFRConfig::default(),
693            conversion_cost_rate: 0.01,
694            commitment_reduction_cost: 0.005,
695            term_funding_spread: 0.02,
696            illiquid_sale_haircut: 0.10,
697        }
698    }
699}
700
701/// Liquidity inflow.
702#[derive(Debug, Clone)]
703pub struct LiquidityInflow {
704    /// Category.
705    pub category: String,
706    /// Amount.
707    pub amount: f64,
708    /// Currency.
709    pub currency: String,
710    /// Inflow rate.
711    pub inflow_rate: f64,
712    /// Days to maturity.
713    pub days_to_maturity: u32,
714}
715
716/// Funding source for NSFR.
717#[derive(Debug, Clone)]
718pub struct FundingSource {
719    /// Funding ID.
720    pub id: String,
721    /// Funding type.
722    pub funding_type: FundingType,
723    /// Amount.
724    pub amount: f64,
725    /// Currency.
726    pub currency: String,
727    /// Remaining maturity in days.
728    pub remaining_maturity_days: u32,
729    /// Is stable (for retail).
730    pub is_stable: bool,
731}
732
733/// Funding type.
734#[derive(Debug, Clone, Copy, PartialEq, Eq)]
735pub enum FundingType {
736    /// Equity/Tier 1 capital.
737    Equity,
738    /// Long-term debt.
739    LongTermDebt,
740    /// Retail deposits.
741    RetailDeposit,
742    /// Wholesale deposits.
743    WholesaleDeposit,
744    /// Other funding.
745    Other,
746}
747
748/// Stress scenario.
749#[derive(Debug, Clone)]
750pub struct StressScenario {
751    /// Scenario name.
752    pub name: String,
753    /// Outflow multipliers by category.
754    pub outflow_multipliers: HashMap<OutflowCategory, f64>,
755    /// Default outflow multiplier.
756    pub default_outflow_multiplier: f64,
757    /// Additional haircut on assets.
758    pub additional_haircut: f64,
759    /// Inflow reduction factor.
760    pub inflow_reduction: f64,
761    /// Liquidation delay factor.
762    pub liquidation_delay_factor: f64,
763}
764
765impl Default for StressScenario {
766    fn default() -> Self {
767        let mut multipliers = HashMap::new();
768        multipliers.insert(OutflowCategory::WholesaleFunding, 1.5);
769        multipliers.insert(OutflowCategory::CommittedFacilities, 1.3);
770
771        Self {
772            name: "Standard Stress".to_string(),
773            outflow_multipliers: multipliers,
774            default_outflow_multiplier: 1.2,
775            additional_haircut: 0.05,
776            inflow_reduction: 0.8,
777            liquidation_delay_factor: 1.5,
778        }
779    }
780}
781
782/// Stress test result.
783#[derive(Debug, Clone)]
784pub struct StressTestResult {
785    /// Scenario name.
786    pub scenario_name: String,
787    /// Base LCR.
788    pub base_lcr: f64,
789    /// Stressed LCR.
790    pub stressed_lcr: f64,
791    /// LCR impact.
792    pub lcr_impact: f64,
793    /// Survives stress test.
794    pub survives_stress: bool,
795    /// Days until breach (if stressed below minimum).
796    pub days_until_breach: Option<u32>,
797}
798
799#[cfg(test)]
800mod tests {
801    use super::*;
802
803    fn create_test_assets() -> Vec<LiquidityPosition> {
804        vec![
805            LiquidityPosition {
806                id: "CASH".to_string(),
807                asset_type: LiquidityAssetType::CashReserves,
808                amount: 100_000.0,
809                currency: "USD".to_string(),
810                hqla_level: Some(1),
811                lcr_haircut: 0.0,
812                days_to_liquidate: 0,
813            },
814            LiquidityPosition {
815                id: "GOV_BOND".to_string(),
816                asset_type: LiquidityAssetType::Level1HQLA,
817                amount: 200_000.0,
818                currency: "USD".to_string(),
819                hqla_level: Some(1),
820                lcr_haircut: 0.0,
821                days_to_liquidate: 1,
822            },
823            LiquidityPosition {
824                id: "CORP_BOND".to_string(),
825                asset_type: LiquidityAssetType::Level2AHQLA,
826                amount: 100_000.0,
827                currency: "USD".to_string(),
828                hqla_level: Some(2),
829                lcr_haircut: 0.15,
830                days_to_liquidate: 3,
831            },
832        ]
833    }
834
835    fn create_test_outflows() -> Vec<LiquidityOutflow> {
836        vec![
837            LiquidityOutflow {
838                category: OutflowCategory::RetailDeposits,
839                amount: 500_000.0,
840                currency: "USD".to_string(),
841                runoff_rate: 0.05,
842                days_to_maturity: 30,
843            },
844            LiquidityOutflow {
845                category: OutflowCategory::WholesaleFunding,
846                amount: 200_000.0,
847                currency: "USD".to_string(),
848                runoff_rate: 0.25,
849                days_to_maturity: 30,
850            },
851        ]
852    }
853
854    fn create_test_inflows() -> Vec<LiquidityInflow> {
855        vec![LiquidityInflow {
856            category: "Loans".to_string(),
857            amount: 100_000.0,
858            currency: "USD".to_string(),
859            inflow_rate: 0.50,
860            days_to_maturity: 30,
861        }]
862    }
863
864    fn create_test_funding() -> Vec<FundingSource> {
865        vec![
866            FundingSource {
867                id: "EQUITY".to_string(),
868                funding_type: FundingType::Equity,
869                amount: 100_000.0,
870                currency: "USD".to_string(),
871                remaining_maturity_days: u32::MAX,
872                is_stable: true,
873            },
874            FundingSource {
875                id: "RETAIL".to_string(),
876                funding_type: FundingType::RetailDeposit,
877                amount: 300_000.0,
878                currency: "USD".to_string(),
879                remaining_maturity_days: 365,
880                is_stable: true,
881            },
882            FundingSource {
883                id: "WHOLESALE".to_string(),
884                funding_type: FundingType::WholesaleDeposit,
885                amount: 200_000.0,
886                currency: "USD".to_string(),
887                remaining_maturity_days: 90,
888                is_stable: false,
889            },
890        ]
891    }
892
893    #[test]
894    fn test_liquidity_metadata() {
895        let kernel = LiquidityOptimization::new();
896        assert_eq!(kernel.metadata().id, "treasury/liquidity-opt");
897        assert_eq!(kernel.metadata().domain, Domain::TreasuryManagement);
898    }
899
900    #[test]
901    fn test_calculate_lcr() {
902        let assets = create_test_assets();
903        let outflows = create_test_outflows();
904        let inflows = create_test_inflows();
905        let config = LCRConfig::default();
906
907        let lcr = LiquidityOptimization::calculate_lcr(&assets, &outflows, &inflows, &config);
908
909        assert!(lcr.hqla > 0.0);
910        assert!(lcr.net_outflows > 0.0);
911        assert!(lcr.lcr_ratio > 0.0);
912        assert!(lcr.hqla_breakdown.contains_key("Level1"));
913    }
914
915    #[test]
916    fn test_hqla_breakdown() {
917        let assets = create_test_assets();
918        let outflows = create_test_outflows();
919        let inflows = create_test_inflows();
920        let config = LCRConfig::default();
921
922        let lcr = LiquidityOptimization::calculate_lcr(&assets, &outflows, &inflows, &config);
923
924        // Level 1 = Cash (100k) + Gov Bond (200k) = 300k
925        assert!((lcr.hqla_breakdown.get("Level1").unwrap() - 300_000.0).abs() < 0.01);
926    }
927
928    #[test]
929    fn test_level2_caps() {
930        let assets = vec![
931            LiquidityPosition {
932                id: "CASH".to_string(),
933                asset_type: LiquidityAssetType::CashReserves,
934                amount: 100_000.0,
935                currency: "USD".to_string(),
936                hqla_level: Some(1),
937                lcr_haircut: 0.0,
938                days_to_liquidate: 0,
939            },
940            LiquidityPosition {
941                id: "L2A".to_string(),
942                asset_type: LiquidityAssetType::Level2AHQLA,
943                amount: 1_000_000.0, // Large amount that should be capped
944                currency: "USD".to_string(),
945                hqla_level: Some(2),
946                lcr_haircut: 0.15,
947                days_to_liquidate: 3,
948            },
949        ];
950
951        let outflows = create_test_outflows();
952        let inflows = create_test_inflows();
953        let config = LCRConfig::default();
954
955        let lcr = LiquidityOptimization::calculate_lcr(&assets, &outflows, &inflows, &config);
956
957        // Level 2A should be capped at 40% of total
958        let l2a = *lcr.hqla_breakdown.get("Level2A").unwrap();
959        let l1 = *lcr.hqla_breakdown.get("Level1").unwrap();
960        assert!(l2a <= (l1 + l2a) * 0.40 + 0.01);
961    }
962
963    #[test]
964    fn test_calculate_nsfr() {
965        let assets = create_test_assets();
966        let funding = create_test_funding();
967        let config = NSFRConfig::default();
968
969        let nsfr = LiquidityOptimization::calculate_nsfr(&assets, &funding, &config);
970
971        assert!(nsfr.asf > 0.0);
972        assert!(nsfr.rsf >= 0.0);
973        assert!(nsfr.nsfr_ratio > 0.0);
974    }
975
976    #[test]
977    fn test_asf_factors() {
978        let funding = vec![FundingSource {
979            id: "EQUITY".to_string(),
980            funding_type: FundingType::Equity,
981            amount: 100_000.0,
982            currency: "USD".to_string(),
983            remaining_maturity_days: u32::MAX,
984            is_stable: true,
985        }];
986        let config = NSFRConfig::default();
987
988        let nsfr = LiquidityOptimization::calculate_nsfr(&[], &funding, &config);
989
990        // Equity has 100% ASF factor
991        assert!((nsfr.asf - 100_000.0).abs() < 0.01);
992    }
993
994    #[test]
995    fn test_optimization() {
996        let assets = create_test_assets();
997        let outflows = create_test_outflows();
998        let inflows = create_test_inflows();
999        let funding = create_test_funding();
1000        let config = OptimizationConfig::default();
1001
1002        let result =
1003            LiquidityOptimization::optimize(&assets, &outflows, &inflows, &funding, &config);
1004
1005        // Should have LCR and NSFR results
1006        assert!(result.lcr.hqla > 0.0);
1007        assert!(result.nsfr.asf > 0.0);
1008    }
1009
1010    #[test]
1011    fn test_stress_test() {
1012        let assets = create_test_assets();
1013        let outflows = create_test_outflows();
1014        let inflows = create_test_inflows();
1015        let scenario = StressScenario::default();
1016
1017        let result = LiquidityOptimization::stress_test(&assets, &outflows, &inflows, &scenario);
1018
1019        // Stressed LCR should be lower than base
1020        assert!(result.stressed_lcr <= result.base_lcr);
1021        assert!(result.lcr_impact <= 0.0);
1022    }
1023
1024    #[test]
1025    fn test_lcr_compliant() {
1026        let assets = vec![LiquidityPosition {
1027            id: "CASH".to_string(),
1028            asset_type: LiquidityAssetType::CashReserves,
1029            amount: 500_000.0, // Large cash balance
1030            currency: "USD".to_string(),
1031            hqla_level: Some(1),
1032            lcr_haircut: 0.0,
1033            days_to_liquidate: 0,
1034        }];
1035
1036        let outflows = vec![LiquidityOutflow {
1037            category: OutflowCategory::RetailDeposits,
1038            amount: 100_000.0,
1039            currency: "USD".to_string(),
1040            runoff_rate: 0.05,
1041            days_to_maturity: 30,
1042        }];
1043
1044        let inflows: Vec<LiquidityInflow> = vec![];
1045        let config = LCRConfig::default();
1046
1047        let lcr = LiquidityOptimization::calculate_lcr(&assets, &outflows, &inflows, &config);
1048
1049        assert!(lcr.is_compliant);
1050        assert!(lcr.buffer > 0.0);
1051    }
1052
1053    #[test]
1054    fn test_empty_inputs() {
1055        let assets: Vec<LiquidityPosition> = vec![];
1056        let outflows: Vec<LiquidityOutflow> = vec![];
1057        let inflows: Vec<LiquidityInflow> = vec![];
1058        let config = LCRConfig::default();
1059
1060        let lcr = LiquidityOptimization::calculate_lcr(&assets, &outflows, &inflows, &config);
1061
1062        assert_eq!(lcr.hqla, 0.0);
1063        assert_eq!(lcr.net_outflows, 0.0);
1064    }
1065
1066    #[test]
1067    fn test_inflow_cap() {
1068        let assets = create_test_assets();
1069        let outflows = vec![LiquidityOutflow {
1070            category: OutflowCategory::RetailDeposits,
1071            amount: 100_000.0,
1072            currency: "USD".to_string(),
1073            runoff_rate: 1.0, // 100% runoff
1074            days_to_maturity: 30,
1075        }];
1076
1077        let inflows = vec![LiquidityInflow {
1078            category: "Loans".to_string(),
1079            amount: 200_000.0, // More than outflows
1080            currency: "USD".to_string(),
1081            inflow_rate: 1.0,
1082            days_to_maturity: 30,
1083        }];
1084
1085        let config = LCRConfig::default();
1086        let lcr = LiquidityOptimization::calculate_lcr(&assets, &outflows, &inflows, &config);
1087
1088        // Inflows should be capped at 75% of outflows
1089        // Gross outflows = 100k * 1.0 = 100k
1090        // Gross inflows = 200k * 1.0 = 200k, capped at 75k
1091        // Net outflows = 100k - 75k = 25k
1092        assert!((lcr.net_outflows - 25_000.0).abs() < 0.01);
1093    }
1094}