use crate::error::Result;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CreditRating {
AAA,
AA,
A,
BBB,
BB,
B,
CCC,
CC,
C,
D,
}
impl CreditRating {
pub fn default_probability(&self) -> Decimal {
match self {
CreditRating::AAA => dec!(0.0001), CreditRating::AA => dec!(0.0005), CreditRating::A => dec!(0.001), CreditRating::BBB => dec!(0.005), CreditRating::BB => dec!(0.02), CreditRating::B => dec!(0.05), CreditRating::CCC => dec!(0.15), CreditRating::CC => dec!(0.30), CreditRating::C => dec!(0.50), CreditRating::D => dec!(1.00), }
}
pub fn recovery_rate(&self) -> Decimal {
match self {
CreditRating::AAA => dec!(0.80),
CreditRating::AA => dec!(0.75),
CreditRating::A => dec!(0.70),
CreditRating::BBB => dec!(0.60),
CreditRating::BB => dec!(0.50),
CreditRating::B => dec!(0.40),
CreditRating::CCC => dec!(0.30),
CreditRating::CC => dec!(0.20),
CreditRating::C => dec!(0.10),
CreditRating::D => dec!(0.05),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Counterparty {
pub id: i64,
pub name: String,
pub credit_rating: CreditRating,
pub credit_limit: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Exposure {
pub counterparty_id: i64,
pub current_exposure: Decimal,
pub potential_future_exposure: Decimal,
pub collateral: Decimal,
pub timestamp: DateTime<Utc>,
}
impl Exposure {
pub fn net_exposure(&self) -> Decimal {
(self.current_exposure + self.potential_future_exposure - self.collateral).max(dec!(0))
}
pub fn exposure_at_default(&self) -> Decimal {
self.net_exposure()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpectedLoss {
pub exposure_at_default: Decimal,
pub probability_of_default: Decimal,
pub loss_given_default: Decimal,
pub expected_loss: Decimal,
}
impl ExpectedLoss {
pub fn calculate(
exposure_at_default: Decimal,
probability_of_default: Decimal,
recovery_rate: Decimal,
) -> Self {
let loss_given_default = dec!(1) - recovery_rate;
let expected_loss = exposure_at_default * probability_of_default * loss_given_default;
Self {
exposure_at_default,
probability_of_default,
loss_given_default,
expected_loss,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollateralPosition {
pub id: i64,
pub counterparty_id: i64,
pub token_id: i64,
pub quantity: Decimal,
pub market_value: Decimal,
pub haircut: Decimal,
pub timestamp: DateTime<Utc>,
}
impl CollateralPosition {
pub fn adjusted_value(&self) -> Decimal {
self.market_value * (dec!(1) - self.haircut)
}
pub fn needs_revaluation(&self) -> bool {
let now = Utc::now();
let age = now.signed_duration_since(self.timestamp);
age.num_hours() >= 1
}
}
pub struct CounterpartyRiskManager {
counterparties: HashMap<i64, Counterparty>,
exposures: HashMap<i64, Exposure>,
collateral: HashMap<i64, Vec<CollateralPosition>>,
}
impl CounterpartyRiskManager {
pub fn new() -> Self {
Self {
counterparties: HashMap::new(),
exposures: HashMap::new(),
collateral: HashMap::new(),
}
}
pub fn add_counterparty(&mut self, counterparty: Counterparty) {
self.counterparties.insert(counterparty.id, counterparty);
}
pub fn update_exposure(&mut self, exposure: Exposure) {
self.exposures.insert(exposure.counterparty_id, exposure);
}
pub fn add_collateral(&mut self, collateral: CollateralPosition) {
self.collateral
.entry(collateral.counterparty_id)
.or_default()
.push(collateral);
}
pub fn get_total_collateral(&self, counterparty_id: i64) -> Decimal {
self.collateral
.get(&counterparty_id)
.map(|positions| positions.iter().map(|p| p.adjusted_value()).sum())
.unwrap_or(dec!(0))
}
pub fn calculate_expected_loss(&self, counterparty_id: i64) -> Option<ExpectedLoss> {
let counterparty = self.counterparties.get(&counterparty_id)?;
let exposure = self.exposures.get(&counterparty_id)?;
let ead = exposure.exposure_at_default();
let pd = counterparty.credit_rating.default_probability();
let recovery_rate = counterparty.credit_rating.recovery_rate();
Some(ExpectedLoss::calculate(ead, pd, recovery_rate))
}
pub fn calculate_total_expected_loss(&self) -> Decimal {
self.counterparties
.keys()
.filter_map(|&id| self.calculate_expected_loss(id))
.map(|el| el.expected_loss)
.sum()
}
pub fn check_credit_limit(&self, counterparty_id: i64) -> Result<bool> {
let counterparty = self.counterparties.get(&counterparty_id).ok_or_else(|| {
crate::error::CoreError::NotFound(format!("Counterparty {}", counterparty_id))
})?;
let exposure = self.exposures.get(&counterparty_id).ok_or_else(|| {
crate::error::CoreError::NotFound(format!(
"Exposure for counterparty {}",
counterparty_id
))
})?;
Ok(exposure.net_exposure() > counterparty.credit_limit)
}
pub fn get_limit_breaches(&self) -> Vec<i64> {
self.counterparties
.keys()
.filter(|&&id| self.check_credit_limit(id).unwrap_or(false))
.copied()
.collect()
}
pub fn aggregate_by_rating(&self) -> HashMap<CreditRating, Decimal> {
let mut aggregated: HashMap<CreditRating, Decimal> = HashMap::new();
for (id, counterparty) in &self.counterparties {
if let Some(exposure) = self.exposures.get(id) {
let net_exp = exposure.net_exposure();
*aggregated
.entry(counterparty.credit_rating)
.or_insert(dec!(0)) += net_exp;
}
}
aggregated
}
pub fn calculate_concentration(&self, top_n: usize) -> Decimal {
let total_exposure: Decimal = self.exposures.values().map(|e| e.net_exposure()).sum();
if total_exposure == dec!(0) {
return dec!(0);
}
let mut exposures: Vec<Decimal> =
self.exposures.values().map(|e| e.net_exposure()).collect();
exposures.sort_by(|a, b| b.cmp(a));
let top_exposure: Decimal = exposures.iter().take(top_n).sum();
(top_exposure / total_exposure) * dec!(100)
}
pub fn get_stale_collateral(&self) -> Vec<&CollateralPosition> {
self.collateral
.values()
.flatten()
.filter(|c| c.needs_revaluation())
.collect()
}
}
impl Default for CounterpartyRiskManager {
fn default() -> Self {
Self::new()
}
}
pub struct CVACalculator;
impl CVACalculator {
pub fn calculate(
exposure: Decimal,
probability_of_default: Decimal,
recovery_rate: Decimal,
) -> Decimal {
let loss_given_default = dec!(1) - recovery_rate;
exposure * probability_of_default * loss_given_default
}
pub fn calculate_time_series(
exposures: &[(Decimal, Decimal)], probability_of_default: Decimal,
recovery_rate: Decimal,
risk_free_rate: Decimal,
) -> Decimal {
let loss_given_default = dec!(1) - recovery_rate;
let mut cva = dec!(0);
for &(time, exposure) in exposures {
let discount_factor = Self::exp_approx(-risk_free_rate * time);
let survival_prob = dec!(1) - probability_of_default * time;
cva += exposure
* probability_of_default
* loss_given_default
* discount_factor
* survival_prob;
}
cva
}
#[allow(dead_code)]
fn exp_approx(x: Decimal) -> Decimal {
let x2 = x * x;
let x3 = x2 * x;
let x4 = x3 * x;
let x5 = x4 * x;
dec!(1) + x + x2 / dec!(2) + x3 / dec!(6) + x4 / dec!(24) + x5 / dec!(120)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CDSContract {
pub notional: Decimal,
pub credit_rating: CreditRating,
pub maturity_years: Decimal,
pub premium_rate: Decimal,
pub recovery_rate: Decimal,
}
impl CDSContract {
pub fn new(notional: Decimal, credit_rating: CreditRating, maturity_years: Decimal) -> Self {
let recovery_rate = credit_rating.recovery_rate();
let premium_rate = Self::default_premium(credit_rating, maturity_years);
Self {
notional,
credit_rating,
maturity_years,
premium_rate,
recovery_rate,
}
}
fn default_premium(credit_rating: CreditRating, maturity_years: Decimal) -> Decimal {
let pd = credit_rating.default_probability();
let recovery = credit_rating.recovery_rate();
let lgd = dec!(1) - recovery;
let maturity_factor = dec!(1) + (maturity_years - dec!(1)) * dec!(0.1);
(pd * lgd / (dec!(1) - pd)) * maturity_factor
}
pub fn premium_leg_pv(&self, risk_free_rate: Decimal) -> Decimal {
let mut pv = dec!(0);
let n_periods = (self.maturity_years * dec!(4)).to_i64().unwrap_or(4);
for i in 1..=n_periods {
let t = Decimal::from(i) / dec!(4); let discount = Self::exp_approx(-risk_free_rate * t);
let survival = dec!(1) - self.credit_rating.default_probability() * t;
pv += self.premium_rate * self.notional * dec!(0.25) * discount * survival;
}
pv
}
pub fn default_leg_pv(&self, risk_free_rate: Decimal) -> Decimal {
let lgd = dec!(1) - self.recovery_rate;
let pd = self.credit_rating.default_probability();
let expected_time_to_default = self.maturity_years / dec!(2);
let discount = Self::exp_approx(-risk_free_rate * expected_time_to_default);
self.notional * lgd * pd * self.maturity_years * discount
}
pub fn fair_spread(&self, risk_free_rate: Decimal) -> Decimal {
let default_leg = self.default_leg_pv(risk_free_rate);
let mut risky_annuity = dec!(0);
let n_periods = (self.maturity_years * dec!(4)).to_i64().unwrap_or(4);
for i in 1..=n_periods {
let t = Decimal::from(i) / dec!(4);
let discount = Self::exp_approx(-risk_free_rate * t);
let survival = dec!(1) - self.credit_rating.default_probability() * t;
risky_annuity += dec!(0.25) * discount * survival;
}
if risky_annuity > dec!(0) {
default_leg / (self.notional * risky_annuity)
} else {
dec!(0)
}
}
fn exp_approx(x: Decimal) -> Decimal {
let x2 = x * x;
let x3 = x2 * x;
let x4 = x3 * x;
let x5 = x4 * x;
dec!(1) + x + x2 / dec!(2) + x3 / dec!(6) + x4 / dec!(24) + x5 / dec!(120)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NettingAgreement {
pub id: i64,
pub counterparty_a: i64,
pub counterparty_b: i64,
pub netting_type: NettingType,
pub active: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NettingType {
CloseOut,
Payment,
CrossProduct,
}
impl NettingAgreement {
pub fn new(
id: i64,
counterparty_a: i64,
counterparty_b: i64,
netting_type: NettingType,
) -> Self {
Self {
id,
counterparty_a,
counterparty_b,
netting_type,
active: true,
}
}
pub fn calculate_netted_exposure(
&self,
exposures_a: &[Exposure],
exposures_b: &[Exposure],
) -> Decimal {
let total_a: Decimal = exposures_a.iter().map(|e| e.current_exposure).sum();
let total_b: Decimal = exposures_b.iter().map(|e| e.current_exposure).sum();
(total_a - total_b).abs()
}
pub fn calculate_netting_benefit(
&self,
exposures_a: &[Exposure],
exposures_b: &[Exposure],
) -> Decimal {
let gross_a: Decimal = exposures_a.iter().map(|e| e.current_exposure).sum();
let gross_b: Decimal = exposures_b.iter().map(|e| e.current_exposure).sum();
let gross_exposure = gross_a + gross_b;
let net_exposure = self.calculate_netted_exposure(exposures_a, exposures_b);
gross_exposure - net_exposure
}
}
pub struct CollateralOptimizer;
impl CollateralOptimizer {
pub fn optimize_allocation(
required_collateral: Decimal,
available_assets: &[(i64, Decimal, Decimal, Decimal)], ) -> Vec<(i64, Decimal)> {
let mut sorted_assets: Vec<_> = available_assets
.iter()
.map(|&(id, qty, val, haircut)| {
let adjusted_val = val * (dec!(1) - haircut);
(id, qty, adjusted_val, haircut)
})
.collect();
sorted_assets.sort_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
let mut allocation = Vec::new();
let mut remaining = required_collateral;
for (id, qty, adj_val, _) in sorted_assets {
if remaining <= dec!(0) {
break;
}
let value_per_unit = adj_val / qty;
let units_needed = remaining / value_per_unit;
let units_to_use = units_needed.min(qty);
allocation.push((id, units_to_use));
remaining -= units_to_use * value_per_unit;
}
allocation
}
pub fn calculate_efficiency(
collateral_posted: Decimal,
collateral_value_after_haircut: Decimal,
) -> Decimal {
if collateral_posted > dec!(0) {
collateral_value_after_haircut / collateral_posted
} else {
dec!(0)
}
}
pub fn suggest_rebalancing(
current_positions: &[CollateralPosition],
target_efficiency: Decimal,
) -> Vec<(i64, Decimal)> {
let total_value: Decimal = current_positions.iter().map(|p| p.market_value).sum();
let adjusted_value: Decimal = current_positions.iter().map(|p| p.adjusted_value()).sum();
let current_efficiency = Self::calculate_efficiency(total_value, adjusted_value);
if current_efficiency >= target_efficiency {
return Vec::new(); }
let mut suggestions = Vec::new();
for pos in current_positions {
if pos.haircut > dec!(0.15) {
suggestions.push((pos.token_id, -pos.quantity / dec!(2)));
}
}
suggestions
}
pub fn calculate_minimum_required(
exposure: Decimal,
available_assets: &[(Decimal, Decimal)], ) -> Decimal {
if available_assets.is_empty() {
return exposure;
}
let default_haircut = dec!(0);
let min_haircut = available_assets
.iter()
.map(|(_, haircut)| haircut)
.min()
.unwrap_or(&default_haircut);
exposure / (dec!(1) - min_haircut)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_credit_rating_default_probability() {
assert_eq!(CreditRating::AAA.default_probability(), dec!(0.0001));
assert_eq!(CreditRating::BBB.default_probability(), dec!(0.005));
assert_eq!(CreditRating::D.default_probability(), dec!(1.00));
}
#[test]
fn test_credit_rating_recovery_rate() {
assert_eq!(CreditRating::AAA.recovery_rate(), dec!(0.80));
assert_eq!(CreditRating::BBB.recovery_rate(), dec!(0.60));
assert_eq!(CreditRating::D.recovery_rate(), dec!(0.05));
}
#[test]
fn test_exposure_net_exposure() {
let exposure = Exposure {
counterparty_id: 1,
current_exposure: dec!(1000),
potential_future_exposure: dec!(200),
collateral: dec!(300),
timestamp: Utc::now(),
};
assert_eq!(exposure.net_exposure(), dec!(900));
}
#[test]
fn test_exposure_net_exposure_with_excess_collateral() {
let exposure = Exposure {
counterparty_id: 1,
current_exposure: dec!(1000),
potential_future_exposure: dec!(200),
collateral: dec!(1500),
timestamp: Utc::now(),
};
assert_eq!(exposure.net_exposure(), dec!(0));
}
#[test]
fn test_expected_loss_calculation() {
let el = ExpectedLoss::calculate(
dec!(10000), dec!(0.05), dec!(0.60), );
assert_eq!(el.exposure_at_default, dec!(10000));
assert_eq!(el.probability_of_default, dec!(0.05));
assert_eq!(el.loss_given_default, dec!(0.40));
assert_eq!(el.expected_loss, dec!(200)); }
#[test]
fn test_collateral_adjusted_value() {
let collateral = CollateralPosition {
id: 1,
counterparty_id: 1,
token_id: 1,
quantity: dec!(100),
market_value: dec!(10000),
haircut: dec!(0.20), timestamp: Utc::now(),
};
assert_eq!(collateral.adjusted_value(), dec!(8000)); }
#[test]
fn test_counterparty_risk_manager() {
let mut manager = CounterpartyRiskManager::new();
let counterparty = Counterparty {
id: 1,
name: "Test Counterparty".to_string(),
credit_rating: CreditRating::BBB,
credit_limit: dec!(10000),
};
manager.add_counterparty(counterparty);
let exposure = Exposure {
counterparty_id: 1,
current_exposure: dec!(5000),
potential_future_exposure: dec!(1000),
collateral: dec!(500),
timestamp: Utc::now(),
};
manager.update_exposure(exposure);
let el = manager.calculate_expected_loss(1).unwrap();
assert_eq!(el.expected_loss, dec!(11));
}
#[test]
fn test_credit_limit_check() {
let mut manager = CounterpartyRiskManager::new();
let counterparty = Counterparty {
id: 1,
name: "Test Counterparty".to_string(),
credit_rating: CreditRating::BBB,
credit_limit: dec!(10000),
};
manager.add_counterparty(counterparty);
let exposure = Exposure {
counterparty_id: 1,
current_exposure: dec!(15000),
potential_future_exposure: dec!(0),
collateral: dec!(0),
timestamp: Utc::now(),
};
manager.update_exposure(exposure);
assert!(manager.check_credit_limit(1).unwrap());
}
#[test]
fn test_aggregate_by_rating() {
let mut manager = CounterpartyRiskManager::new();
manager.add_counterparty(Counterparty {
id: 1,
name: "CP1".to_string(),
credit_rating: CreditRating::BBB,
credit_limit: dec!(10000),
});
manager.add_counterparty(Counterparty {
id: 2,
name: "CP2".to_string(),
credit_rating: CreditRating::BBB,
credit_limit: dec!(10000),
});
manager.add_counterparty(Counterparty {
id: 3,
name: "CP3".to_string(),
credit_rating: CreditRating::AA,
credit_limit: dec!(20000),
});
manager.update_exposure(Exposure {
counterparty_id: 1,
current_exposure: dec!(5000),
potential_future_exposure: dec!(0),
collateral: dec!(0),
timestamp: Utc::now(),
});
manager.update_exposure(Exposure {
counterparty_id: 2,
current_exposure: dec!(7000),
potential_future_exposure: dec!(0),
collateral: dec!(0),
timestamp: Utc::now(),
});
manager.update_exposure(Exposure {
counterparty_id: 3,
current_exposure: dec!(10000),
potential_future_exposure: dec!(0),
collateral: dec!(0),
timestamp: Utc::now(),
});
let aggregated = manager.aggregate_by_rating();
assert_eq!(aggregated.get(&CreditRating::BBB), Some(&dec!(12000)));
assert_eq!(aggregated.get(&CreditRating::AA), Some(&dec!(10000)));
}
#[test]
fn test_concentration_risk() {
let mut manager = CounterpartyRiskManager::new();
for i in 1..=10 {
manager.add_counterparty(Counterparty {
id: i,
name: format!("CP{}", i),
credit_rating: CreditRating::BBB,
credit_limit: dec!(10000),
});
manager.update_exposure(Exposure {
counterparty_id: i,
current_exposure: Decimal::from(i * 1000),
potential_future_exposure: dec!(0),
collateral: dec!(0),
timestamp: Utc::now(),
});
}
let concentration = manager.calculate_concentration(3);
assert!(concentration > dec!(49) && concentration < dec!(50));
}
#[test]
fn test_cva_calculation() {
let cva = CVACalculator::calculate(
dec!(10000), dec!(0.05), dec!(0.60), );
assert_eq!(cva, dec!(200));
}
#[test]
fn test_total_collateral() {
let mut manager = CounterpartyRiskManager::new();
manager.add_collateral(CollateralPosition {
id: 1,
counterparty_id: 1,
token_id: 1,
quantity: dec!(100),
market_value: dec!(10000),
haircut: dec!(0.20),
timestamp: Utc::now(),
});
manager.add_collateral(CollateralPosition {
id: 2,
counterparty_id: 1,
token_id: 2,
quantity: dec!(50),
market_value: dec!(5000),
haircut: dec!(0.10),
timestamp: Utc::now(),
});
let total = manager.get_total_collateral(1);
assert_eq!(total, dec!(12500));
}
#[test]
fn test_cds_contract_creation() {
let cds = CDSContract::new(dec!(100000), CreditRating::BBB, dec!(5));
assert_eq!(cds.notional, dec!(100000));
assert_eq!(cds.credit_rating, CreditRating::BBB);
assert_eq!(cds.maturity_years, dec!(5));
assert_eq!(cds.recovery_rate, CreditRating::BBB.recovery_rate());
}
#[test]
fn test_cds_fair_spread() {
let cds = CDSContract::new(dec!(100000), CreditRating::BBB, dec!(5));
let risk_free_rate = dec!(0.03);
let fair_spread = cds.fair_spread(risk_free_rate);
assert!(fair_spread > dec!(0));
assert!(fair_spread < dec!(0.1)); }
#[test]
fn test_cds_premium_leg() {
let cds = CDSContract::new(dec!(100000), CreditRating::A, dec!(3));
let risk_free_rate = dec!(0.02);
let premium_pv = cds.premium_leg_pv(risk_free_rate);
assert!(premium_pv > dec!(0));
}
#[test]
fn test_cds_default_leg() {
let cds = CDSContract::new(dec!(100000), CreditRating::BB, dec!(5));
let risk_free_rate = dec!(0.03);
let default_pv = cds.default_leg_pv(risk_free_rate);
assert!(default_pv > dec!(0));
}
#[test]
fn test_netting_agreement() {
let agreement = NettingAgreement::new(1, 101, 102, NettingType::CloseOut);
assert_eq!(agreement.id, 1);
assert_eq!(agreement.counterparty_a, 101);
assert_eq!(agreement.counterparty_b, 102);
assert_eq!(agreement.netting_type, NettingType::CloseOut);
assert!(agreement.active);
}
#[test]
fn test_netting_exposure_calculation() {
let agreement = NettingAgreement::new(1, 101, 102, NettingType::Payment);
let exposures_a = vec![
Exposure {
counterparty_id: 102,
current_exposure: dec!(5000),
potential_future_exposure: dec!(0),
collateral: dec!(0),
timestamp: Utc::now(),
},
Exposure {
counterparty_id: 102,
current_exposure: dec!(3000),
potential_future_exposure: dec!(0),
collateral: dec!(0),
timestamp: Utc::now(),
},
];
let exposures_b = vec![Exposure {
counterparty_id: 101,
current_exposure: dec!(6000),
potential_future_exposure: dec!(0),
collateral: dec!(0),
timestamp: Utc::now(),
}];
let net_exposure = agreement.calculate_netted_exposure(&exposures_a, &exposures_b);
assert_eq!(net_exposure, dec!(2000));
}
#[test]
fn test_netting_benefit() {
let agreement = NettingAgreement::new(1, 101, 102, NettingType::CrossProduct);
let exposures_a = vec![Exposure {
counterparty_id: 102,
current_exposure: dec!(10000),
potential_future_exposure: dec!(0),
collateral: dec!(0),
timestamp: Utc::now(),
}];
let exposures_b = vec![Exposure {
counterparty_id: 101,
current_exposure: dec!(7000),
potential_future_exposure: dec!(0),
collateral: dec!(0),
timestamp: Utc::now(),
}];
let benefit = agreement.calculate_netting_benefit(&exposures_a, &exposures_b);
assert_eq!(benefit, dec!(14000));
}
#[test]
fn test_collateral_optimization() {
let available = vec![
(1i64, dec!(100), dec!(10000), dec!(0.20)),
(2i64, dec!(50), dec!(5000), dec!(0.10)),
(3i64, dec!(200), dec!(20000), dec!(0.30)),
];
let required = dec!(10000);
let allocation = CollateralOptimizer::optimize_allocation(required, &available);
assert!(!allocation.is_empty());
assert_eq!(allocation[0].0, 2); }
#[test]
fn test_collateral_efficiency() {
let posted = dec!(10000);
let value_after_haircut = dec!(8000);
let efficiency = CollateralOptimizer::calculate_efficiency(posted, value_after_haircut);
assert_eq!(efficiency, dec!(0.8)); }
#[test]
fn test_collateral_minimum_required() {
let exposure = dec!(10000);
let assets = vec![(dec!(5000), dec!(0.20)), (dec!(8000), dec!(0.10))];
let min_required = CollateralOptimizer::calculate_minimum_required(exposure, &assets);
assert!(min_required > dec!(11000));
assert!(min_required < dec!(11200));
}
#[test]
fn test_collateral_rebalancing_suggestion() {
let positions = vec![
CollateralPosition {
id: 1,
counterparty_id: 1,
token_id: 1,
quantity: dec!(100),
market_value: dec!(10000),
haircut: dec!(0.25), timestamp: Utc::now(),
},
CollateralPosition {
id: 2,
counterparty_id: 1,
token_id: 2,
quantity: dec!(50),
market_value: dec!(5000),
haircut: dec!(0.05), timestamp: Utc::now(),
},
];
let suggestions = CollateralOptimizer::suggest_rebalancing(&positions, dec!(0.90));
assert!(!suggestions.is_empty());
}
}