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)] 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)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_fully_matched_ic() {
125 let data = ICMatchingData {
126 total_pairs: 5,
127 matched_pairs: 5,
128 total_receivables: Decimal::new(100000, 2),
129 total_payables: Decimal::new(100000, 2),
130 unmatched_items: vec![],
131 gross_volume: Some(Decimal::new(200000, 2)),
132 net_settlement: Some(Decimal::new(20000, 2)),
133 };
134
135 let evaluator = ICMatchingEvaluator::default();
136 let result = evaluator.evaluate(&data).unwrap();
137
138 assert_eq!(result.match_rate, 1.0);
139 assert_eq!(result.total_unmatched, Decimal::ZERO);
140 assert_eq!(result.net_position, Decimal::ZERO);
141 assert!(result.netting_efficiency.unwrap() > 0.8);
142 }
143
144 #[test]
145 fn test_partial_match() {
146 let data = ICMatchingData {
147 total_pairs: 10,
148 matched_pairs: 8,
149 total_receivables: Decimal::new(100000, 2),
150 total_payables: Decimal::new(95000, 2),
151 unmatched_items: vec![UnmatchedICItem {
152 company: "1000".to_string(),
153 counterparty: "2000".to_string(),
154 amount: Decimal::new(5000, 2),
155 is_receivable: true,
156 }],
157 gross_volume: None,
158 net_settlement: None,
159 };
160
161 let evaluator = ICMatchingEvaluator::default();
162 let result = evaluator.evaluate(&data).unwrap();
163
164 assert_eq!(result.match_rate, 0.8);
165 assert_eq!(result.discrepancy_count, 1);
166 assert_eq!(result.net_position, Decimal::new(5000, 2));
167 }
168
169 #[test]
170 fn test_no_ic_transactions() {
171 let data = ICMatchingData {
172 total_pairs: 0,
173 matched_pairs: 0,
174 total_receivables: Decimal::ZERO,
175 total_payables: Decimal::ZERO,
176 unmatched_items: vec![],
177 gross_volume: None,
178 net_settlement: None,
179 };
180
181 let evaluator = ICMatchingEvaluator::default();
182 let result = evaluator.evaluate(&data).unwrap();
183
184 assert_eq!(result.match_rate, 1.0); }
186}