use crate::error::EvalResult;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ICMatchingEvaluation {
pub total_pairs: usize,
pub matched_pairs: usize,
pub match_rate: f64,
pub total_receivables: Decimal,
pub total_payables: Decimal,
pub total_unmatched: Decimal,
pub net_position: Decimal,
pub discrepancy_count: usize,
pub within_tolerance_count: usize,
pub outside_tolerance_count: usize,
pub netting_efficiency: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct ICMatchingData {
pub total_pairs: usize,
pub matched_pairs: usize,
pub total_receivables: Decimal,
pub total_payables: Decimal,
pub unmatched_items: Vec<UnmatchedICItem>,
pub gross_volume: Option<Decimal>,
pub net_settlement: Option<Decimal>,
}
#[derive(Debug, Clone)]
pub struct UnmatchedICItem {
pub company: String,
pub counterparty: String,
pub amount: Decimal,
pub is_receivable: bool,
}
pub struct ICMatchingEvaluator {
tolerance: Decimal,
}
impl ICMatchingEvaluator {
pub fn new(tolerance: Decimal) -> Self {
Self { tolerance }
}
pub fn evaluate(&self, data: &ICMatchingData) -> EvalResult<ICMatchingEvaluation> {
let match_rate = if data.total_pairs > 0 {
data.matched_pairs as f64 / data.total_pairs as f64
} else {
1.0
};
let total_unmatched: Decimal = data.unmatched_items.iter().map(|i| i.amount.abs()).sum();
let net_position = data.total_receivables - data.total_payables;
let within_tolerance_count = data
.unmatched_items
.iter()
.filter(|item| item.amount.abs() <= self.tolerance)
.count();
let outside_tolerance_count = data.unmatched_items.len() - within_tolerance_count;
let discrepancy_count = outside_tolerance_count;
let netting_efficiency = match (data.gross_volume, data.net_settlement) {
(Some(gross), Some(net)) if gross > Decimal::ZERO => {
Some(1.0 - (net / gross).to_f64().unwrap_or(0.0))
}
_ => None,
};
Ok(ICMatchingEvaluation {
total_pairs: data.total_pairs,
matched_pairs: data.matched_pairs,
match_rate,
total_receivables: data.total_receivables,
total_payables: data.total_payables,
total_unmatched,
net_position,
discrepancy_count,
within_tolerance_count,
outside_tolerance_count,
netting_efficiency,
})
}
}
impl Default for ICMatchingEvaluator {
fn default() -> Self {
Self::new(Decimal::new(1, 2)) }
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_fully_matched_ic() {
let data = ICMatchingData {
total_pairs: 5,
matched_pairs: 5,
total_receivables: Decimal::new(100000, 2),
total_payables: Decimal::new(100000, 2),
unmatched_items: vec![],
gross_volume: Some(Decimal::new(200000, 2)),
net_settlement: Some(Decimal::new(20000, 2)),
};
let evaluator = ICMatchingEvaluator::default();
let result = evaluator.evaluate(&data).unwrap();
assert_eq!(result.match_rate, 1.0);
assert_eq!(result.total_unmatched, Decimal::ZERO);
assert_eq!(result.net_position, Decimal::ZERO);
assert!(result.netting_efficiency.unwrap() > 0.8);
}
#[test]
fn test_partial_match() {
let data = ICMatchingData {
total_pairs: 10,
matched_pairs: 8,
total_receivables: Decimal::new(100000, 2),
total_payables: Decimal::new(95000, 2),
unmatched_items: vec![UnmatchedICItem {
company: "1000".to_string(),
counterparty: "2000".to_string(),
amount: Decimal::new(5000, 2),
is_receivable: true,
}],
gross_volume: None,
net_settlement: None,
};
let evaluator = ICMatchingEvaluator::default();
let result = evaluator.evaluate(&data).unwrap();
assert_eq!(result.match_rate, 0.8);
assert_eq!(result.discrepancy_count, 1);
assert_eq!(result.net_position, Decimal::new(5000, 2));
}
#[test]
fn test_no_ic_transactions() {
let data = ICMatchingData {
total_pairs: 0,
matched_pairs: 0,
total_receivables: Decimal::ZERO,
total_payables: Decimal::ZERO,
unmatched_items: vec![],
gross_volume: None,
net_settlement: None,
};
let evaluator = ICMatchingEvaluator::default();
let result = evaluator.evaluate(&data).unwrap();
assert_eq!(result.match_rate, 1.0); }
}