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