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 = 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 pub fn calculate_nsfr(
148 assets: &[LiquidityPosition],
149 funding: &[FundingSource],
150 config: &NSFRConfig,
151 ) -> NSFRResult {
152 let asf: f64 = funding
154 .iter()
155 .map(|f| f.amount * Self::get_asf_factor(f, config))
156 .sum();
157
158 let rsf: f64 = assets
160 .iter()
161 .map(|a| a.amount * Self::get_rsf_factor(a, config))
162 .sum();
163
164 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 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 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 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 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 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 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 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 for (i, outflow) in outflows.iter().enumerate() {
318 if matches!(outflow.category, OutflowCategory::CommittedFacilities) {
319 let reduce_amount = outflow.amount * 0.1; 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 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 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, cost,
368 });
369
370 if actions.iter().map(|a| a.lcr_impact).sum::<f64>() >= shortfall {
371 break;
372 }
373 }
374 }
375
376 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 pub fn stress_test(
398 assets: &[LiquidityPosition],
399 outflows: &[LiquidityOutflow],
400 inflows: &[LiquidityInflow],
401 scenario: &StressScenario,
402 ) -> StressTestResult {
403 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 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 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 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 let mut modified_assets: Vec<LiquidityPosition> = assets.to_vec();
483 let mut modified_outflows: Vec<LiquidityOutflow> = outflows.to_vec();
484
485 for action in actions {
487 match action.action_type {
488 LiquidityActionType::ConvertToHQLA => {
489 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 let asset_id = modified_assets[idx].id.clone();
495 let asset_currency = modified_assets[idx].currency.clone();
496
497 modified_assets[idx].amount -= action.amount;
499
500 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 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 modified_outflows[idx].amount -= action.amount;
521 }
522 }
523 }
524 }
525 _ => {
526 }
528 }
529 }
530
531 let lcr_after = Self::calculate_lcr(&modified_assets, &modified_outflows, inflows, config);
533
534 lcr_after.buffer - lcr_before.buffer
536 }
537
538 fn estimate_days_until_breach(lcr: &LCRResult) -> Option<u32> {
545 if lcr.is_compliant {
546 return None;
547 }
548
549 let daily_outflow = lcr.net_outflows / 30.0;
551
552 if daily_outflow <= 0.0 {
553 return None;
555 }
556
557 let current_hqla = lcr.hqla;
559
560 let critical_hqla = lcr.net_outflows * 0.5;
565
566 if current_hqla <= critical_hqla {
569 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 Some(days.min(90))
578 }
579}
580
581impl GpuKernel for LiquidityOptimization {
582 fn metadata(&self) -> &KernelMetadata {
583 &self.metadata
584 }
585}
586
587#[derive(Debug, Clone)]
589pub struct LCRConfig {
590 pub min_lcr: f64,
592 pub level2a_cap: f64,
594 pub level2b_cap: f64,
596 pub level2_total_cap: f64,
598 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#[derive(Debug, Clone)]
616pub struct NSFRConfig {
617 pub min_nsfr: f64,
619 pub asf_stable_retail: f64,
621 pub asf_less_stable_retail: f64,
623 pub asf_wholesale: f64,
625 pub asf_6m_1y: f64,
627 pub asf_under_6m: f64,
629 pub asf_other: f64,
631 pub rsf_level1: f64,
633 pub rsf_level2a: f64,
635 pub rsf_level2b: f64,
637 pub rsf_other_liquid: f64,
639 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#[derive(Debug, Clone)]
664pub struct OptimizationConfig {
665 pub target_lcr: f64,
667 pub target_nsfr: f64,
669 pub lcr_config: LCRConfig,
671 pub nsfr_config: NSFRConfig,
673 pub conversion_cost_rate: f64,
675 pub commitment_reduction_cost: f64,
677 pub term_funding_spread: f64,
679 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#[derive(Debug, Clone)]
700pub struct LiquidityInflow {
701 pub category: String,
703 pub amount: f64,
705 pub currency: String,
707 pub inflow_rate: f64,
709 pub days_to_maturity: u32,
711}
712
713#[derive(Debug, Clone)]
715pub struct FundingSource {
716 pub id: String,
718 pub funding_type: FundingType,
720 pub amount: f64,
722 pub currency: String,
724 pub remaining_maturity_days: u32,
726 pub is_stable: bool,
728}
729
730#[derive(Debug, Clone, Copy, PartialEq, Eq)]
732pub enum FundingType {
733 Equity,
735 LongTermDebt,
737 RetailDeposit,
739 WholesaleDeposit,
741 Other,
743}
744
745#[derive(Debug, Clone)]
747pub struct StressScenario {
748 pub name: String,
750 pub outflow_multipliers: HashMap<OutflowCategory, f64>,
752 pub default_outflow_multiplier: f64,
754 pub additional_haircut: f64,
756 pub inflow_reduction: f64,
758 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#[derive(Debug, Clone)]
781pub struct StressTestResult {
782 pub scenario_name: String,
784 pub base_lcr: f64,
786 pub stressed_lcr: f64,
788 pub lcr_impact: f64,
790 pub survives_stress: bool,
792 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 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, 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 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 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 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 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, 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, days_to_maturity: 30,
1072 }];
1073
1074 let inflows = vec![LiquidityInflow {
1075 category: "Loans".to_string(),
1076 amount: 200_000.0, 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 assert!((lcr.net_outflows - 25_000.0).abs() < 0.01);
1090 }
1091}