datasynth_eval/coherence/
treasury.rs1use crate::error::EvalResult;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct TreasuryThresholds {
12 pub min_balance_accuracy: f64,
14 pub balance_tolerance: f64,
16 pub min_hedge_effectiveness_rate: f64,
18 pub min_covenant_compliance_rate: f64,
20 pub min_netting_accuracy: f64,
22}
23
24impl Default for TreasuryThresholds {
25 fn default() -> Self {
26 Self {
27 min_balance_accuracy: 0.999,
28 balance_tolerance: 0.01,
29 min_hedge_effectiveness_rate: 0.95,
30 min_covenant_compliance_rate: 0.95,
31 min_netting_accuracy: 0.999,
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct CashPositionData {
39 pub position_id: String,
41 pub opening_balance: f64,
43 pub inflows: f64,
45 pub outflows: f64,
47 pub closing_balance: f64,
49}
50
51#[derive(Debug, Clone)]
53pub struct HedgeEffectivenessData {
54 pub hedge_id: String,
56 pub effectiveness_ratio: f64,
58 pub is_effective: bool,
60}
61
62#[derive(Debug, Clone)]
64pub struct CovenantData {
65 pub covenant_id: String,
67 pub threshold: f64,
69 pub actual_value: f64,
71 pub is_compliant: bool,
73 pub is_max_covenant: bool,
76}
77
78#[derive(Debug, Clone)]
80pub struct NettingData {
81 pub run_id: String,
83 pub gross_receivables: f64,
85 pub gross_payables: f64,
87 pub net_settlement: f64,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct TreasuryEvaluation {
94 pub balance_accuracy: f64,
96 pub hedge_effectiveness_accuracy: f64,
98 pub covenant_compliance_accuracy: f64,
100 pub netting_accuracy: f64,
102 pub total_positions: usize,
104 pub total_hedges: usize,
106 pub total_covenants: usize,
108 pub total_netting_runs: usize,
110 pub passes: bool,
112 pub issues: Vec<String>,
114}
115
116pub struct TreasuryEvaluator {
118 thresholds: TreasuryThresholds,
119}
120
121impl TreasuryEvaluator {
122 pub fn new() -> Self {
124 Self {
125 thresholds: TreasuryThresholds::default(),
126 }
127 }
128
129 pub fn with_thresholds(thresholds: TreasuryThresholds) -> Self {
131 Self { thresholds }
132 }
133
134 pub fn evaluate(
136 &self,
137 positions: &[CashPositionData],
138 hedges: &[HedgeEffectivenessData],
139 covenants: &[CovenantData],
140 netting_runs: &[NettingData],
141 ) -> EvalResult<TreasuryEvaluation> {
142 let mut issues = Vec::new();
143 let tolerance = self.thresholds.balance_tolerance;
144
145 let balance_ok = positions
147 .iter()
148 .filter(|p| {
149 let expected = p.opening_balance + p.inflows - p.outflows;
150 (p.closing_balance - expected).abs() <= tolerance * p.opening_balance.abs().max(1.0)
151 })
152 .count();
153 let balance_accuracy = if positions.is_empty() {
154 1.0
155 } else {
156 balance_ok as f64 / positions.len() as f64
157 };
158
159 let hedge_ok = hedges
161 .iter()
162 .filter(|h| {
163 let in_range = h.effectiveness_ratio >= 0.80 && h.effectiveness_ratio <= 1.25;
164 h.is_effective == in_range
165 })
166 .count();
167 let hedge_effectiveness_accuracy = if hedges.is_empty() {
168 1.0
169 } else {
170 hedge_ok as f64 / hedges.len() as f64
171 };
172
173 let covenant_ok = covenants
175 .iter()
176 .filter(|c| {
177 let should_comply = if c.is_max_covenant {
178 c.actual_value <= c.threshold
179 } else {
180 c.actual_value >= c.threshold
181 };
182 c.is_compliant == should_comply
183 })
184 .count();
185 let covenant_compliance_accuracy = if covenants.is_empty() {
186 1.0
187 } else {
188 covenant_ok as f64 / covenants.len() as f64
189 };
190
191 let netting_ok = netting_runs
193 .iter()
194 .filter(|n| {
195 let expected = (n.gross_receivables - n.gross_payables).abs();
196 (n.net_settlement - expected).abs()
197 <= tolerance * n.gross_receivables.abs().max(1.0)
198 })
199 .count();
200 let netting_accuracy = if netting_runs.is_empty() {
201 1.0
202 } else {
203 netting_ok as f64 / netting_runs.len() as f64
204 };
205
206 if balance_accuracy < self.thresholds.min_balance_accuracy {
208 issues.push(format!(
209 "Cash position balance accuracy {:.4} < {:.4}",
210 balance_accuracy, self.thresholds.min_balance_accuracy
211 ));
212 }
213 if hedge_effectiveness_accuracy < self.thresholds.min_hedge_effectiveness_rate {
214 issues.push(format!(
215 "Hedge effectiveness accuracy {:.4} < {:.4}",
216 hedge_effectiveness_accuracy, self.thresholds.min_hedge_effectiveness_rate
217 ));
218 }
219 if covenant_compliance_accuracy < self.thresholds.min_covenant_compliance_rate {
220 issues.push(format!(
221 "Covenant compliance accuracy {:.4} < {:.4}",
222 covenant_compliance_accuracy, self.thresholds.min_covenant_compliance_rate
223 ));
224 }
225 if netting_accuracy < self.thresholds.min_netting_accuracy {
226 issues.push(format!(
227 "Netting accuracy {:.4} < {:.4}",
228 netting_accuracy, self.thresholds.min_netting_accuracy
229 ));
230 }
231
232 let passes = issues.is_empty();
233
234 Ok(TreasuryEvaluation {
235 balance_accuracy,
236 hedge_effectiveness_accuracy,
237 covenant_compliance_accuracy,
238 netting_accuracy,
239 total_positions: positions.len(),
240 total_hedges: hedges.len(),
241 total_covenants: covenants.len(),
242 total_netting_runs: netting_runs.len(),
243 passes,
244 issues,
245 })
246 }
247}
248
249impl Default for TreasuryEvaluator {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255#[cfg(test)]
256#[allow(clippy::unwrap_used)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_valid_treasury_data() {
262 let evaluator = TreasuryEvaluator::new();
263 let positions = vec![CashPositionData {
264 position_id: "CP001".to_string(),
265 opening_balance: 100_000.0,
266 inflows: 50_000.0,
267 outflows: 30_000.0,
268 closing_balance: 120_000.0,
269 }];
270 let hedges = vec![
271 HedgeEffectivenessData {
272 hedge_id: "H001".to_string(),
273 effectiveness_ratio: 0.95,
274 is_effective: true,
275 },
276 HedgeEffectivenessData {
277 hedge_id: "H002".to_string(),
278 effectiveness_ratio: 0.70,
279 is_effective: false,
280 },
281 ];
282 let covenants = vec![CovenantData {
283 covenant_id: "COV001".to_string(),
284 threshold: 3.0,
285 actual_value: 2.5,
286 is_compliant: true,
287 is_max_covenant: true,
288 }];
289 let netting = vec![NettingData {
290 run_id: "NET001".to_string(),
291 gross_receivables: 50_000.0,
292 gross_payables: 30_000.0,
293 net_settlement: 20_000.0,
294 }];
295
296 let result = evaluator
297 .evaluate(&positions, &hedges, &covenants, &netting)
298 .unwrap();
299 assert!(result.passes);
300 assert_eq!(result.total_positions, 1);
301 assert_eq!(result.total_hedges, 2);
302 }
303
304 #[test]
305 fn test_wrong_closing_balance() {
306 let evaluator = TreasuryEvaluator::new();
307 let positions = vec![CashPositionData {
308 position_id: "CP001".to_string(),
309 opening_balance: 100_000.0,
310 inflows: 50_000.0,
311 outflows: 30_000.0,
312 closing_balance: 200_000.0, }];
314
315 let result = evaluator.evaluate(&positions, &[], &[], &[]).unwrap();
316 assert!(!result.passes);
317 assert!(result.issues[0].contains("Cash position balance"));
318 }
319
320 #[test]
321 fn test_wrong_hedge_classification() {
322 let evaluator = TreasuryEvaluator::new();
323 let hedges = vec![HedgeEffectivenessData {
324 hedge_id: "H001".to_string(),
325 effectiveness_ratio: 0.70, is_effective: true, }];
328
329 let result = evaluator.evaluate(&[], &hedges, &[], &[]).unwrap();
330 assert!(!result.passes);
331 assert!(result.issues[0].contains("Hedge effectiveness"));
332 }
333
334 #[test]
335 fn test_wrong_covenant_compliance() {
336 let evaluator = TreasuryEvaluator::new();
337 let covenants = vec![CovenantData {
338 covenant_id: "COV001".to_string(),
339 threshold: 3.0,
340 actual_value: 4.0, is_compliant: true, is_max_covenant: true,
343 }];
344
345 let result = evaluator.evaluate(&[], &[], &covenants, &[]).unwrap();
346 assert!(!result.passes);
347 assert!(result.issues[0].contains("Covenant compliance"));
348 }
349
350 #[test]
351 fn test_wrong_netting() {
352 let evaluator = TreasuryEvaluator::new();
353 let netting = vec![NettingData {
354 run_id: "NET001".to_string(),
355 gross_receivables: 50_000.0,
356 gross_payables: 30_000.0,
357 net_settlement: 5_000.0, }];
359
360 let result = evaluator.evaluate(&[], &[], &[], &netting).unwrap();
361 assert!(!result.passes);
362 assert!(result.issues[0].contains("Netting accuracy"));
363 }
364
365 #[test]
366 fn test_empty_data() {
367 let evaluator = TreasuryEvaluator::new();
368 let result = evaluator.evaluate(&[], &[], &[], &[]).unwrap();
369 assert!(result.passes);
370 }
371}