1use 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#[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 #[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 pub fn calculate_lcr(
47 assets: &[LiquidityPosition],
48 outflows: &[LiquidityOutflow],
49 inflows: &[LiquidityInflow],
50 config: &LCRConfig,
51 ) -> LCRResult {
52 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 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 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 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 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 let capped_inflows = gross_inflows.min(gross_outflows * config.inflow_cap);
123 let net_outflows = (gross_outflows - capped_inflows).max(0.0);
124
125 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) };
138
139 LCRResult {
140 hqla,
141 net_outflows,
142 lcr_ratio,
143 is_compliant,
144 buffer,
145 hqla_breakdown,
146 }
147 }
148
149 pub fn calculate_nsfr(
151 assets: &[LiquidityPosition],
152 funding: &[FundingSource],
153 config: &NSFRConfig,
154 ) -> NSFRResult {
155 let asf: f64 = funding
157 .iter()
158 .map(|f| f.amount * Self::get_asf_factor(f, config))
159 .sum();
160
161 let rsf: f64 = assets
163 .iter()
164 .map(|a| a.amount * Self::get_rsf_factor(a, config))
165 .sum();
166
167 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 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 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 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 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 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 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 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 for (i, outflow) in outflows.iter().enumerate() {
321 if matches!(outflow.category, OutflowCategory::CommittedFacilities) {
322 let reduce_amount = outflow.amount * 0.1; 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 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 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, cost,
371 });
372
373 if actions.iter().map(|a| a.lcr_impact).sum::<f64>() >= shortfall {
374 break;
375 }
376 }
377 }
378
379 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 pub fn stress_test(
401 assets: &[LiquidityPosition],
402 outflows: &[LiquidityOutflow],
403 inflows: &[LiquidityInflow],
404 scenario: &StressScenario,
405 ) -> StressTestResult {
406 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 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 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 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 let mut modified_assets: Vec<LiquidityPosition> = assets.to_vec();
486 let mut modified_outflows: Vec<LiquidityOutflow> = outflows.to_vec();
487
488 for action in actions {
490 match action.action_type {
491 LiquidityActionType::ConvertToHQLA => {
492 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 let asset_id = modified_assets[idx].id.clone();
498 let asset_currency = modified_assets[idx].currency.clone();
499
500 modified_assets[idx].amount -= action.amount;
502
503 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 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 modified_outflows[idx].amount -= action.amount;
524 }
525 }
526 }
527 }
528 _ => {
529 }
531 }
532 }
533
534 let lcr_after = Self::calculate_lcr(&modified_assets, &modified_outflows, inflows, config);
536
537 lcr_after.buffer - lcr_before.buffer
539 }
540
541 fn estimate_days_until_breach(lcr: &LCRResult) -> Option<u32> {
548 if lcr.is_compliant {
549 return None;
550 }
551
552 let daily_outflow = lcr.net_outflows / 30.0;
554
555 if daily_outflow <= 0.0 {
556 return None;
558 }
559
560 let current_hqla = lcr.hqla;
562
563 let critical_hqla = lcr.net_outflows * 0.5;
568
569 if current_hqla <= critical_hqla {
572 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 Some(days.min(90))
581 }
582}
583
584impl GpuKernel for LiquidityOptimization {
585 fn metadata(&self) -> &KernelMetadata {
586 &self.metadata
587 }
588}
589
590#[derive(Debug, Clone)]
592pub struct LCRConfig {
593 pub min_lcr: f64,
595 pub level2a_cap: f64,
597 pub level2b_cap: f64,
599 pub level2_total_cap: f64,
601 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#[derive(Debug, Clone)]
619pub struct NSFRConfig {
620 pub min_nsfr: f64,
622 pub asf_stable_retail: f64,
624 pub asf_less_stable_retail: f64,
626 pub asf_wholesale: f64,
628 pub asf_6m_1y: f64,
630 pub asf_under_6m: f64,
632 pub asf_other: f64,
634 pub rsf_level1: f64,
636 pub rsf_level2a: f64,
638 pub rsf_level2b: f64,
640 pub rsf_other_liquid: f64,
642 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#[derive(Debug, Clone)]
667pub struct OptimizationConfig {
668 pub target_lcr: f64,
670 pub target_nsfr: f64,
672 pub lcr_config: LCRConfig,
674 pub nsfr_config: NSFRConfig,
676 pub conversion_cost_rate: f64,
678 pub commitment_reduction_cost: f64,
680 pub term_funding_spread: f64,
682 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#[derive(Debug, Clone)]
703pub struct LiquidityInflow {
704 pub category: String,
706 pub amount: f64,
708 pub currency: String,
710 pub inflow_rate: f64,
712 pub days_to_maturity: u32,
714}
715
716#[derive(Debug, Clone)]
718pub struct FundingSource {
719 pub id: String,
721 pub funding_type: FundingType,
723 pub amount: f64,
725 pub currency: String,
727 pub remaining_maturity_days: u32,
729 pub is_stable: bool,
731}
732
733#[derive(Debug, Clone, Copy, PartialEq, Eq)]
735pub enum FundingType {
736 Equity,
738 LongTermDebt,
740 RetailDeposit,
742 WholesaleDeposit,
744 Other,
746}
747
748#[derive(Debug, Clone)]
750pub struct StressScenario {
751 pub name: String,
753 pub outflow_multipliers: HashMap<OutflowCategory, f64>,
755 pub default_outflow_multiplier: f64,
757 pub additional_haircut: f64,
759 pub inflow_reduction: f64,
761 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#[derive(Debug, Clone)]
784pub struct StressTestResult {
785 pub scenario_name: String,
787 pub base_lcr: f64,
789 pub stressed_lcr: f64,
791 pub lcr_impact: f64,
793 pub survives_stress: bool,
795 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 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, 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 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 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 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 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, 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, days_to_maturity: 30,
1075 }];
1076
1077 let inflows = vec![LiquidityInflow {
1078 category: "Loans".to_string(),
1079 amount: 200_000.0, 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 assert!((lcr.net_outflows - 25_000.0).abs() < 0.01);
1093 }
1094}