use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LiquidityCategory {
Level1,
Level2A,
Level2B,
NonHQLA,
}
impl LiquidityCategory {
pub fn haircut(&self) -> Decimal {
match self {
LiquidityCategory::Level1 => dec!(0.00), LiquidityCategory::Level2A => dec!(0.15), LiquidityCategory::Level2B => dec!(0.50), LiquidityCategory::NonHQLA => dec!(1.00), }
}
pub fn is_hqla(&self) -> bool {
!matches!(self, LiquidityCategory::NonHQLA)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidAsset {
pub asset_id: i64,
pub name: String,
pub market_value: Decimal,
pub category: LiquidityCategory,
pub timestamp: DateTime<Utc>,
}
impl LiquidAsset {
pub fn hqla_value(&self) -> Decimal {
if self.category.is_hqla() {
self.market_value * (dec!(1) - self.category.haircut())
} else {
dec!(0)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CashOutflow {
pub category: String,
pub gross_amount: Decimal,
pub outflow_rate: Decimal,
pub time_horizon_days: u32,
}
impl CashOutflow {
pub fn net_outflow(&self) -> Decimal {
self.gross_amount * self.outflow_rate
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CashInflow {
pub category: String,
pub gross_amount: Decimal,
pub inflow_rate: Decimal,
pub time_horizon_days: u32,
}
impl CashInflow {
pub fn net_inflow(&self) -> Decimal {
self.gross_amount * self.inflow_rate
}
}
pub struct LCRCalculator {
liquid_assets: Vec<LiquidAsset>,
outflows: Vec<CashOutflow>,
inflows: Vec<CashInflow>,
}
impl LCRCalculator {
pub fn new() -> Self {
Self {
liquid_assets: Vec::new(),
outflows: Vec::new(),
inflows: Vec::new(),
}
}
pub fn add_asset(&mut self, asset: LiquidAsset) {
self.liquid_assets.push(asset);
}
pub fn add_outflow(&mut self, outflow: CashOutflow) {
self.outflows.push(outflow);
}
pub fn add_inflow(&mut self, inflow: CashInflow) {
self.inflows.push(inflow);
}
pub fn total_hqla(&self) -> Decimal {
self.liquid_assets.iter().map(|a| a.hqla_value()).sum()
}
pub fn total_net_outflows_30d(&self) -> Decimal {
let outflows: Decimal = self
.outflows
.iter()
.filter(|o| o.time_horizon_days <= 30)
.map(|o| o.net_outflow())
.sum();
let inflows: Decimal = self
.inflows
.iter()
.filter(|i| i.time_horizon_days <= 30)
.map(|i| i.net_inflow())
.sum();
let capped_inflows = inflows.min(outflows * dec!(0.75));
(outflows - capped_inflows).max(dec!(0))
}
pub fn calculate_lcr(&self) -> Decimal {
let net_outflows = self.total_net_outflows_30d();
if net_outflows == dec!(0) {
return Decimal::MAX; }
(self.total_hqla() / net_outflows) * dec!(100)
}
pub fn meets_regulatory_minimum(&self) -> bool {
self.calculate_lcr() >= dec!(100)
}
}
impl Default for LCRCalculator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketLiquidityMetrics {
pub asset_id: i64,
pub bid_ask_spread: Decimal,
pub trading_volume_24h: Decimal,
pub market_depth: Decimal,
pub price_impact_10k: Decimal,
pub timestamp: DateTime<Utc>,
}
impl MarketLiquidityMetrics {
pub fn liquidity_score(&self) -> Decimal {
let spread_score = if self.bid_ask_spread < dec!(0.001) {
dec!(25)
} else if self.bid_ask_spread < dec!(0.01) {
dec!(20)
} else if self.bid_ask_spread < dec!(0.05) {
dec!(15)
} else if self.bid_ask_spread < dec!(0.10) {
dec!(10)
} else {
dec!(5)
};
let volume_score = if self.trading_volume_24h > dec!(1000000) {
dec!(25)
} else if self.trading_volume_24h > dec!(100000) {
dec!(20)
} else if self.trading_volume_24h > dec!(10000) {
dec!(15)
} else if self.trading_volume_24h > dec!(1000) {
dec!(10)
} else {
dec!(5)
};
let depth_score = if self.market_depth > dec!(500000) {
dec!(25)
} else if self.market_depth > dec!(100000) {
dec!(20)
} else if self.market_depth > dec!(50000) {
dec!(15)
} else if self.market_depth > dec!(10000) {
dec!(10)
} else {
dec!(5)
};
let impact_score = if self.price_impact_10k < dec!(0.005) {
dec!(25)
} else if self.price_impact_10k < dec!(0.02) {
dec!(20)
} else if self.price_impact_10k < dec!(0.05) {
dec!(15)
} else if self.price_impact_10k < dec!(0.10) {
dec!(10)
} else {
dec!(5)
};
spread_score + volume_score + depth_score + impact_score
}
pub fn liquidity_category(&self) -> &'static str {
let score = self.liquidity_score();
if score >= dec!(80) {
"Highly Liquid"
} else if score >= dec!(60) {
"Liquid"
} else if score >= dec!(40) {
"Moderately Liquid"
} else if score >= dec!(20) {
"Illiquid"
} else {
"Highly Illiquid"
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingLiquidityMetrics {
pub available_funding: Decimal,
pub required_funding: Decimal,
pub funding_sources: usize,
pub largest_source_pct: Decimal,
pub timestamp: DateTime<Utc>,
}
impl FundingLiquidityMetrics {
pub fn funding_gap(&self) -> Decimal {
self.required_funding - self.available_funding
}
pub fn is_adequate(&self) -> bool {
self.available_funding >= self.required_funding
}
pub fn concentration_risk(&self) -> Decimal {
self.largest_source_pct * dec!(100)
}
pub fn diversification_score(&self) -> Decimal {
if self.funding_sources == 0 {
return dec!(0);
}
let sources_score = if self.funding_sources >= 10 {
dec!(60)
} else if self.funding_sources >= 5 {
dec!(40)
} else if self.funding_sources >= 3 {
dec!(20)
} else {
dec!(10)
};
let concentration_penalty = self.largest_source_pct * dec!(40);
(sources_score + dec!(40) - concentration_penalty).max(dec!(0))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidityStressScenario {
pub name: String,
pub description: String,
pub additional_haircut: Decimal,
pub outflow_multiplier: Decimal,
pub inflow_reduction: Decimal,
}
impl LiquidityStressScenario {
pub fn severe_stress() -> Self {
Self {
name: "Severe Liquidity Stress".to_string(),
description: "Market-wide liquidity crisis".to_string(),
additional_haircut: dec!(0.30), outflow_multiplier: dec!(2.0), inflow_reduction: dec!(0.50), }
}
pub fn moderate_stress() -> Self {
Self {
name: "Moderate Liquidity Stress".to_string(),
description: "Sector-specific liquidity stress".to_string(),
additional_haircut: dec!(0.15), outflow_multiplier: dec!(1.5), inflow_reduction: dec!(0.25), }
}
pub fn mild_stress() -> Self {
Self {
name: "Mild Liquidity Stress".to_string(),
description: "Temporary liquidity pressure".to_string(),
additional_haircut: dec!(0.05), outflow_multiplier: dec!(1.2), inflow_reduction: dec!(0.10), }
}
pub fn apply_to_lcr(&self, calculator: &LCRCalculator) -> Decimal {
let stressed_hqla: Decimal = calculator
.liquid_assets
.iter()
.map(|asset| {
let base_haircut = asset.category.haircut();
let total_haircut = (base_haircut + self.additional_haircut).min(dec!(1.0));
if asset.category.is_hqla() {
asset.market_value * (dec!(1) - total_haircut)
} else {
dec!(0)
}
})
.sum();
let stressed_outflows: Decimal = calculator
.outflows
.iter()
.filter(|o| o.time_horizon_days <= 30)
.map(|o| o.net_outflow() * self.outflow_multiplier)
.sum();
let stressed_inflows: Decimal = calculator
.inflows
.iter()
.filter(|i| i.time_horizon_days <= 30)
.map(|i| i.net_inflow() * (dec!(1) - self.inflow_reduction))
.sum();
let capped_inflows = stressed_inflows.min(stressed_outflows * dec!(0.75));
let stressed_net_outflows = (stressed_outflows - capped_inflows).max(dec!(0));
if stressed_net_outflows == dec!(0) {
return Decimal::MAX;
}
(stressed_hqla / stressed_net_outflows) * dec!(100)
}
}
pub struct LiquidityRiskManager {
market_liquidity: HashMap<i64, MarketLiquidityMetrics>,
funding_metrics_history: Vec<FundingLiquidityMetrics>,
}
impl LiquidityRiskManager {
pub fn new() -> Self {
Self {
market_liquidity: HashMap::new(),
funding_metrics_history: Vec::new(),
}
}
pub fn update_market_liquidity(&mut self, metrics: MarketLiquidityMetrics) {
self.market_liquidity.insert(metrics.asset_id, metrics);
}
pub fn add_funding_metrics(&mut self, metrics: FundingLiquidityMetrics) {
self.funding_metrics_history.push(metrics);
}
pub fn get_market_liquidity_score(&self, asset_id: i64) -> Option<Decimal> {
self.market_liquidity
.get(&asset_id)
.map(|m| m.liquidity_score())
}
pub fn get_low_liquidity_assets(&self) -> Vec<i64> {
self.market_liquidity
.iter()
.filter(|(_, m)| m.liquidity_score() < dec!(40))
.map(|(&id, _)| id)
.collect()
}
pub fn average_liquidity_score(&self) -> Decimal {
if self.market_liquidity.is_empty() {
return dec!(0);
}
let total: Decimal = self
.market_liquidity
.values()
.map(|m| m.liquidity_score())
.sum();
total / Decimal::from(self.market_liquidity.len())
}
pub fn latest_funding_metrics(&self) -> Option<&FundingLiquidityMetrics> {
self.funding_metrics_history.last()
}
pub fn is_funding_gap_increasing(&self) -> bool {
if self.funding_metrics_history.len() < 2 {
return false;
}
let recent = &self.funding_metrics_history[self.funding_metrics_history.len() - 1];
let previous = &self.funding_metrics_history[self.funding_metrics_history.len() - 2];
recent.funding_gap() > previous.funding_gap()
}
}
impl Default for LiquidityRiskManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_liquidity_category_haircut() {
assert_eq!(LiquidityCategory::Level1.haircut(), dec!(0.00));
assert_eq!(LiquidityCategory::Level2A.haircut(), dec!(0.15));
assert_eq!(LiquidityCategory::Level2B.haircut(), dec!(0.50));
assert_eq!(LiquidityCategory::NonHQLA.haircut(), dec!(1.00));
}
#[test]
fn test_liquid_asset_hqla_value() {
let asset = LiquidAsset {
asset_id: 1,
name: "Treasury Bond".to_string(),
market_value: dec!(10000),
category: LiquidityCategory::Level2A,
timestamp: Utc::now(),
};
assert_eq!(asset.hqla_value(), dec!(8500));
}
#[test]
fn test_lcr_calculation() {
let mut calculator = LCRCalculator::new();
calculator.add_asset(LiquidAsset {
asset_id: 1,
name: "Cash".to_string(),
market_value: dec!(100000),
category: LiquidityCategory::Level1,
timestamp: Utc::now(),
});
calculator.add_outflow(CashOutflow {
category: "Retail Deposits".to_string(),
gross_amount: dec!(80000),
outflow_rate: dec!(1.0),
time_horizon_days: 30,
});
calculator.add_inflow(CashInflow {
category: "Receivables".to_string(),
gross_amount: dec!(20000),
inflow_rate: dec!(1.0),
time_horizon_days: 30,
});
let lcr = calculator.calculate_lcr();
assert!(lcr > dec!(166) && lcr < dec!(167));
assert!(calculator.meets_regulatory_minimum());
}
#[test]
fn test_market_liquidity_score() {
let metrics = MarketLiquidityMetrics {
asset_id: 1,
bid_ask_spread: dec!(0.0005), trading_volume_24h: dec!(2000000), market_depth: dec!(600000), price_impact_10k: dec!(0.003), timestamp: Utc::now(),
};
let score = metrics.liquidity_score();
assert_eq!(score, dec!(100)); assert_eq!(metrics.liquidity_category(), "Highly Liquid");
}
#[test]
fn test_funding_liquidity_metrics() {
let metrics = FundingLiquidityMetrics {
available_funding: dec!(1000000),
required_funding: dec!(800000),
funding_sources: 5,
largest_source_pct: dec!(0.30),
timestamp: Utc::now(),
};
assert_eq!(metrics.funding_gap(), dec!(-200000)); assert!(metrics.is_adequate());
assert_eq!(metrics.concentration_risk(), dec!(30));
assert!(metrics.diversification_score() > dec!(0));
}
#[test]
fn test_liquidity_stress_scenario() {
let mut calculator = LCRCalculator::new();
calculator.add_asset(LiquidAsset {
asset_id: 1,
name: "HQLA".to_string(),
market_value: dec!(100000),
category: LiquidityCategory::Level1,
timestamp: Utc::now(),
});
calculator.add_outflow(CashOutflow {
category: "Deposits".to_string(),
gross_amount: dec!(50000),
outflow_rate: dec!(1.0),
time_horizon_days: 30,
});
let normal_lcr = calculator.calculate_lcr();
let scenario = LiquidityStressScenario::severe_stress();
let stressed_lcr = scenario.apply_to_lcr(&calculator);
assert!(stressed_lcr < normal_lcr);
}
#[test]
fn test_liquidity_risk_manager() {
let mut manager = LiquidityRiskManager::new();
manager.update_market_liquidity(MarketLiquidityMetrics {
asset_id: 1,
bid_ask_spread: dec!(0.001),
trading_volume_24h: dec!(100000),
market_depth: dec!(50000),
price_impact_10k: dec!(0.02),
timestamp: Utc::now(),
});
manager.update_market_liquidity(MarketLiquidityMetrics {
asset_id: 2,
bid_ask_spread: dec!(0.10),
trading_volume_24h: dec!(100),
market_depth: dec!(1000),
price_impact_10k: dec!(0.20),
timestamp: Utc::now(),
});
let avg_score = manager.average_liquidity_score();
assert!(avg_score > dec!(0));
let low_liquidity_assets = manager.get_low_liquidity_assets();
assert!(!low_liquidity_assets.is_empty());
}
#[test]
fn test_cash_outflow_net_outflow() {
let outflow = CashOutflow {
category: "Test".to_string(),
gross_amount: dec!(1000),
outflow_rate: dec!(0.5),
time_horizon_days: 30,
};
assert_eq!(outflow.net_outflow(), dec!(500));
}
#[test]
fn test_cash_inflow_net_inflow() {
let inflow = CashInflow {
category: "Test".to_string(),
gross_amount: dec!(1000),
inflow_rate: dec!(0.8),
time_horizon_days: 30,
};
assert_eq!(inflow.net_inflow(), dec!(800));
}
}