pub struct CreditScorecard {
pub age_score: f64,
pub income_score: f64,
pub debt_ratio_score: f64,
pub payment_history_score: f64,
pub credit_utilization_score: f64,
}
impl CreditScorecard {
pub fn total_score(&self) -> f64 {
self.age_score * 0.10
+ self.income_score * 0.25
+ self.debt_ratio_score * 0.20
+ self.payment_history_score * 0.35
+ self.credit_utilization_score * 0.10
}
pub fn pd_estimate(&self) -> f64 {
let s = self.total_score();
1.0 / (1.0 + ((s - 500.0) / 50.0).exp())
}
}
pub struct LoanExposure {
pub principal: f64,
pub outstanding: f64,
pub committed: f64,
pub lgd: f64,
pub maturity_years: f64,
}
impl LoanExposure {
pub fn ead(&self) -> f64 {
self.outstanding + 0.5 * (self.committed - self.outstanding).max(0.0)
}
pub fn expected_loss(&self, pd: f64) -> f64 {
self.ead() * self.lgd * pd
}
}
pub struct CreditVaR {
pub confidence: f64,
pub time_horizon_years: f64,
}
impl CreditVaR {
pub fn calculate(exposures: &[(LoanExposure, f64)]) -> f64 {
Self::calculate_with_params(exposures, 0.99, 42)
}
pub fn calculate_with_params(
exposures: &[(LoanExposure, f64)],
confidence: f64,
seed: u64,
) -> f64 {
let n_sims: usize = 10_000;
let mut losses = Vec::with_capacity(n_sims);
let mut state = seed;
for _ in 0..n_sims {
let mut sim_loss = 0.0;
for (exp, pd) in exposures {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let u = (state >> 11) as f64 / (1u64 << 53) as f64;
if u < *pd {
sim_loss += exp.ead() * exp.lgd;
}
}
losses.push(sim_loss);
}
losses.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((1.0 - confidence) * n_sims as f64) as usize;
let idx = idx.min(n_sims - 1);
losses[idx]
}
}
pub struct ZSpreadCalculator;
impl ZSpreadCalculator {
pub fn z_spread(
cashflows: &[(f64, f64)],
market_price: f64,
risk_free_curve: &[(f64, f64)],
) -> f64 {
let pv = |spread: f64| -> f64 {
cashflows
.iter()
.map(|(t, cf)| {
let rf = interpolate_rate(risk_free_curve, *t);
let disc = (-(rf + spread) * t).exp();
cf * disc
})
.sum::<f64>()
};
let mut lo = -0.10_f64;
let mut hi = 0.50_f64;
for _ in 0..60 {
let mid = (lo + hi) / 2.0;
if pv(mid) > market_price {
lo = mid;
} else {
hi = mid;
}
}
(lo + hi) / 2.0
}
}
fn interpolate_rate(curve: &[(f64, f64)], t: f64) -> f64 {
if curve.is_empty() {
return 0.0;
}
if t <= curve[0].0 {
return curve[0].1;
}
if t >= curve[curve.len() - 1].0 {
return curve[curve.len() - 1].1;
}
for i in 1..curve.len() {
if t <= curve[i].0 {
let (t0, r0) = curve[i - 1];
let (t1, r1) = curve[i];
let frac = (t - t0) / (t1 - t0);
return r0 + frac * (r1 - r0);
}
}
curve[curve.len() - 1].1
}
pub struct CreditMigration {
pub transition_matrix: Vec<Vec<f64>>,
pub rating_labels: Vec<String>,
}
impl CreditMigration {
pub fn from_standard() -> Self {
let labels = ["AAA", "AA", "A", "BBB", "BB", "B", "CCC"]
.iter()
.map(|s| s.to_string())
.collect();
let matrix = vec![
vec![0.9081, 0.0833, 0.0068, 0.0006, 0.0008, 0.0002, 0.0001, 0.0001],
vec![0.0070, 0.9065, 0.0779, 0.0064, 0.0006, 0.0013, 0.0001, 0.0002],
vec![0.0009, 0.0227, 0.9105, 0.0552, 0.0074, 0.0026, 0.0001, 0.0006],
vec![0.0002, 0.0033, 0.0595, 0.8693, 0.0530, 0.0117, 0.0012, 0.0018],
vec![0.0003, 0.0014, 0.0067, 0.0773, 0.8053, 0.0884, 0.0100, 0.0106],
vec![0.0000, 0.0011, 0.0024, 0.0043, 0.0648, 0.8346, 0.0407, 0.0521],
vec![0.0022, 0.0000, 0.0022, 0.0130, 0.0238, 0.1117, 0.6490, 0.1981],
];
let matrix = matrix
.into_iter()
.map(|row| row[..7].to_vec())
.collect();
CreditMigration {
transition_matrix: matrix,
rating_labels: labels,
}
}
pub fn migrate_one_year(&self, rating_idx: usize, rand: f64) -> usize {
let row = &self.transition_matrix[rating_idx];
let mut cumulative = 0.0;
for (j, &p) in row.iter().enumerate() {
cumulative += p;
if rand < cumulative {
return j;
}
}
row.len() - 1
}
}