datasynth_eval/coherence/
intercompany.rs1use crate::error::EvalResult;
7use rust_decimal::prelude::ToPrimitive;
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ICMatchingEvaluation {
14 pub total_pairs: usize,
16 pub matched_pairs: usize,
18 pub match_rate: f64,
20 pub total_receivables: Decimal,
22 pub total_payables: Decimal,
24 pub total_unmatched: Decimal,
26 pub net_position: Decimal,
28 pub discrepancy_count: usize,
30 pub netting_efficiency: Option<f64>,
32}
33
34#[derive(Debug, Clone)]
36pub struct ICMatchingData {
37 pub total_pairs: usize,
39 pub matched_pairs: usize,
41 pub total_receivables: Decimal,
43 pub total_payables: Decimal,
45 pub unmatched_items: Vec<UnmatchedICItem>,
47 pub gross_volume: Option<Decimal>,
49 pub net_settlement: Option<Decimal>,
51}
52
53#[derive(Debug, Clone)]
55pub struct UnmatchedICItem {
56 pub company: String,
58 pub counterparty: String,
60 pub amount: Decimal,
62 pub is_receivable: bool,
64}
65
66pub struct ICMatchingEvaluator {
68 #[allow(dead_code)]
70 tolerance: Decimal,
71}
72
73impl ICMatchingEvaluator {
74 pub fn new(tolerance: Decimal) -> Self {
76 Self { tolerance }
77 }
78
79 pub fn evaluate(&self, data: &ICMatchingData) -> EvalResult<ICMatchingEvaluation> {
81 let match_rate = if data.total_pairs > 0 {
82 data.matched_pairs as f64 / data.total_pairs as f64
83 } else {
84 1.0
85 };
86
87 let total_unmatched: Decimal = data.unmatched_items.iter().map(|i| i.amount.abs()).sum();
88 let net_position = data.total_receivables - data.total_payables;
89 let discrepancy_count = data.unmatched_items.len();
90
91 let netting_efficiency = match (data.gross_volume, data.net_settlement) {
93 (Some(gross), Some(net)) if gross > Decimal::ZERO => {
94 Some(1.0 - (net / gross).to_f64().unwrap_or(0.0))
95 }
96 _ => None,
97 };
98
99 Ok(ICMatchingEvaluation {
100 total_pairs: data.total_pairs,
101 matched_pairs: data.matched_pairs,
102 match_rate,
103 total_receivables: data.total_receivables,
104 total_payables: data.total_payables,
105 total_unmatched,
106 net_position,
107 discrepancy_count,
108 netting_efficiency,
109 })
110 }
111}
112
113impl Default for ICMatchingEvaluator {
114 fn default() -> Self {
115 Self::new(Decimal::new(1, 2)) }
117}
118
119#[cfg(test)]
120#[allow(clippy::unwrap_used)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_fully_matched_ic() {
126 let data = ICMatchingData {
127 total_pairs: 5,
128 matched_pairs: 5,
129 total_receivables: Decimal::new(100000, 2),
130 total_payables: Decimal::new(100000, 2),
131 unmatched_items: vec![],
132 gross_volume: Some(Decimal::new(200000, 2)),
133 net_settlement: Some(Decimal::new(20000, 2)),
134 };
135
136 let evaluator = ICMatchingEvaluator::default();
137 let result = evaluator.evaluate(&data).unwrap();
138
139 assert_eq!(result.match_rate, 1.0);
140 assert_eq!(result.total_unmatched, Decimal::ZERO);
141 assert_eq!(result.net_position, Decimal::ZERO);
142 assert!(result.netting_efficiency.unwrap() > 0.8);
143 }
144
145 #[test]
146 fn test_partial_match() {
147 let data = ICMatchingData {
148 total_pairs: 10,
149 matched_pairs: 8,
150 total_receivables: Decimal::new(100000, 2),
151 total_payables: Decimal::new(95000, 2),
152 unmatched_items: vec![UnmatchedICItem {
153 company: "1000".to_string(),
154 counterparty: "2000".to_string(),
155 amount: Decimal::new(5000, 2),
156 is_receivable: true,
157 }],
158 gross_volume: None,
159 net_settlement: None,
160 };
161
162 let evaluator = ICMatchingEvaluator::default();
163 let result = evaluator.evaluate(&data).unwrap();
164
165 assert_eq!(result.match_rate, 0.8);
166 assert_eq!(result.discrepancy_count, 1);
167 assert_eq!(result.net_position, Decimal::new(5000, 2));
168 }
169
170 #[test]
171 fn test_no_ic_transactions() {
172 let data = ICMatchingData {
173 total_pairs: 0,
174 matched_pairs: 0,
175 total_receivables: Decimal::ZERO,
176 total_payables: Decimal::ZERO,
177 unmatched_items: vec![],
178 gross_volume: None,
179 net_settlement: None,
180 };
181
182 let evaluator = ICMatchingEvaluator::default();
183 let result = evaluator.evaluate(&data).unwrap();
184
185 assert_eq!(result.match_rate, 1.0); }
187}