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)]
72 tolerance: Decimal,
73}
74
75impl ICMatchingEvaluator {
76 pub fn new(tolerance: Decimal) -> Self {
78 Self { tolerance }
79 }
80
81 pub fn evaluate(&self, data: &ICMatchingData) -> EvalResult<ICMatchingEvaluation> {
83 let match_rate = if data.total_pairs > 0 {
84 data.matched_pairs as f64 / data.total_pairs as f64
85 } else {
86 1.0
87 };
88
89 let total_unmatched: Decimal = data.unmatched_items.iter().map(|i| i.amount.abs()).sum();
90 let net_position = data.total_receivables - data.total_payables;
91 let discrepancy_count = data.unmatched_items.len();
92
93 let netting_efficiency = match (data.gross_volume, data.net_settlement) {
95 (Some(gross), Some(net)) if gross > Decimal::ZERO => {
96 Some(1.0 - (net / gross).to_f64().unwrap_or(0.0))
97 }
98 _ => None,
99 };
100
101 Ok(ICMatchingEvaluation {
102 total_pairs: data.total_pairs,
103 matched_pairs: data.matched_pairs,
104 match_rate,
105 total_receivables: data.total_receivables,
106 total_payables: data.total_payables,
107 total_unmatched,
108 net_position,
109 discrepancy_count,
110 netting_efficiency,
111 })
112 }
113}
114
115impl Default for ICMatchingEvaluator {
116 fn default() -> Self {
117 Self::new(Decimal::new(1, 2)) }
119}
120
121#[cfg(test)]
122#[allow(clippy::unwrap_used)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_fully_matched_ic() {
128 let data = ICMatchingData {
129 total_pairs: 5,
130 matched_pairs: 5,
131 total_receivables: Decimal::new(100000, 2),
132 total_payables: Decimal::new(100000, 2),
133 unmatched_items: vec![],
134 gross_volume: Some(Decimal::new(200000, 2)),
135 net_settlement: Some(Decimal::new(20000, 2)),
136 };
137
138 let evaluator = ICMatchingEvaluator::default();
139 let result = evaluator.evaluate(&data).unwrap();
140
141 assert_eq!(result.match_rate, 1.0);
142 assert_eq!(result.total_unmatched, Decimal::ZERO);
143 assert_eq!(result.net_position, Decimal::ZERO);
144 assert!(result.netting_efficiency.unwrap() > 0.8);
145 }
146
147 #[test]
148 fn test_partial_match() {
149 let data = ICMatchingData {
150 total_pairs: 10,
151 matched_pairs: 8,
152 total_receivables: Decimal::new(100000, 2),
153 total_payables: Decimal::new(95000, 2),
154 unmatched_items: vec![UnmatchedICItem {
155 company: "1000".to_string(),
156 counterparty: "2000".to_string(),
157 amount: Decimal::new(5000, 2),
158 is_receivable: true,
159 }],
160 gross_volume: None,
161 net_settlement: None,
162 };
163
164 let evaluator = ICMatchingEvaluator::default();
165 let result = evaluator.evaluate(&data).unwrap();
166
167 assert_eq!(result.match_rate, 0.8);
168 assert_eq!(result.discrepancy_count, 1);
169 assert_eq!(result.net_position, Decimal::new(5000, 2));
170 }
171
172 #[test]
173 fn test_no_ic_transactions() {
174 let data = ICMatchingData {
175 total_pairs: 0,
176 matched_pairs: 0,
177 total_receivables: Decimal::ZERO,
178 total_payables: Decimal::ZERO,
179 unmatched_items: vec![],
180 gross_volume: None,
181 net_settlement: None,
182 };
183
184 let evaluator = ICMatchingEvaluator::default();
185 let result = evaluator.evaluate(&data).unwrap();
186
187 assert_eq!(result.match_rate, 1.0); }
189}