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 within_tolerance_count: usize,
32 pub outside_tolerance_count: usize,
34 pub netting_efficiency: Option<f64>,
36}
37
38#[derive(Debug, Clone)]
40pub struct ICMatchingData {
41 pub total_pairs: usize,
43 pub matched_pairs: usize,
45 pub total_receivables: Decimal,
47 pub total_payables: Decimal,
49 pub unmatched_items: Vec<UnmatchedICItem>,
51 pub gross_volume: Option<Decimal>,
53 pub net_settlement: Option<Decimal>,
55}
56
57#[derive(Debug, Clone)]
59pub struct UnmatchedICItem {
60 pub company: String,
62 pub counterparty: String,
64 pub amount: Decimal,
66 pub is_receivable: bool,
68}
69
70pub struct ICMatchingEvaluator {
72 tolerance: Decimal,
74}
75
76impl ICMatchingEvaluator {
77 pub fn new(tolerance: Decimal) -> Self {
79 Self { tolerance }
80 }
81
82 pub fn evaluate(&self, data: &ICMatchingData) -> EvalResult<ICMatchingEvaluation> {
84 let match_rate = if data.total_pairs > 0 {
85 data.matched_pairs as f64 / data.total_pairs as f64
86 } else {
87 1.0
88 };
89
90 let total_unmatched: Decimal = data.unmatched_items.iter().map(|i| i.amount.abs()).sum();
91 let net_position = data.total_receivables - data.total_payables;
92
93 let within_tolerance_count = data
95 .unmatched_items
96 .iter()
97 .filter(|item| item.amount.abs() <= self.tolerance)
98 .count();
99 let outside_tolerance_count = data.unmatched_items.len() - within_tolerance_count;
100 let discrepancy_count = outside_tolerance_count;
102
103 let netting_efficiency = match (data.gross_volume, data.net_settlement) {
105 (Some(gross), Some(net)) if gross > Decimal::ZERO => {
106 Some(1.0 - (net / gross).to_f64().unwrap_or(0.0))
107 }
108 _ => None,
109 };
110
111 Ok(ICMatchingEvaluation {
112 total_pairs: data.total_pairs,
113 matched_pairs: data.matched_pairs,
114 match_rate,
115 total_receivables: data.total_receivables,
116 total_payables: data.total_payables,
117 total_unmatched,
118 net_position,
119 discrepancy_count,
120 within_tolerance_count,
121 outside_tolerance_count,
122 netting_efficiency,
123 })
124 }
125}
126
127impl Default for ICMatchingEvaluator {
128 fn default() -> Self {
129 Self::new(Decimal::new(1, 2)) }
131}
132
133#[cfg(test)]
134#[allow(clippy::unwrap_used)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_fully_matched_ic() {
140 let data = ICMatchingData {
141 total_pairs: 5,
142 matched_pairs: 5,
143 total_receivables: Decimal::new(100000, 2),
144 total_payables: Decimal::new(100000, 2),
145 unmatched_items: vec![],
146 gross_volume: Some(Decimal::new(200000, 2)),
147 net_settlement: Some(Decimal::new(20000, 2)),
148 };
149
150 let evaluator = ICMatchingEvaluator::default();
151 let result = evaluator.evaluate(&data).unwrap();
152
153 assert_eq!(result.match_rate, 1.0);
154 assert_eq!(result.total_unmatched, Decimal::ZERO);
155 assert_eq!(result.net_position, Decimal::ZERO);
156 assert!(result.netting_efficiency.unwrap() > 0.8);
157 }
158
159 #[test]
160 fn test_partial_match() {
161 let data = ICMatchingData {
162 total_pairs: 10,
163 matched_pairs: 8,
164 total_receivables: Decimal::new(100000, 2),
165 total_payables: Decimal::new(95000, 2),
166 unmatched_items: vec![UnmatchedICItem {
167 company: "1000".to_string(),
168 counterparty: "2000".to_string(),
169 amount: Decimal::new(5000, 2),
170 is_receivable: true,
171 }],
172 gross_volume: None,
173 net_settlement: None,
174 };
175
176 let evaluator = ICMatchingEvaluator::default();
177 let result = evaluator.evaluate(&data).unwrap();
178
179 assert_eq!(result.match_rate, 0.8);
180 assert_eq!(result.discrepancy_count, 1);
181 assert_eq!(result.net_position, Decimal::new(5000, 2));
182 }
183
184 #[test]
185 fn test_no_ic_transactions() {
186 let data = ICMatchingData {
187 total_pairs: 0,
188 matched_pairs: 0,
189 total_receivables: Decimal::ZERO,
190 total_payables: Decimal::ZERO,
191 unmatched_items: vec![],
192 gross_volume: None,
193 net_settlement: None,
194 };
195
196 let evaluator = ICMatchingEvaluator::default();
197 let result = evaluator.evaluate(&data).unwrap();
198
199 assert_eq!(result.match_rate, 1.0); }
201}