1use 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)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_valid_treasury_data() {
261 let evaluator = TreasuryEvaluator::new();
262 let positions = vec![CashPositionData {
263 position_id: "CP001".to_string(),
264 opening_balance: 100_000.0,
265 inflows: 50_000.0,
266 outflows: 30_000.0,
267 closing_balance: 120_000.0,
268 }];
269 let hedges = vec![
270 HedgeEffectivenessData {
271 hedge_id: "H001".to_string(),
272 effectiveness_ratio: 0.95,
273 is_effective: true,
274 },
275 HedgeEffectivenessData {
276 hedge_id: "H002".to_string(),
277 effectiveness_ratio: 0.70,
278 is_effective: false,
279 },
280 ];
281 let covenants = vec![CovenantData {
282 covenant_id: "COV001".to_string(),
283 threshold: 3.0,
284 actual_value: 2.5,
285 is_compliant: true,
286 is_max_covenant: true,
287 }];
288 let netting = vec![NettingData {
289 run_id: "NET001".to_string(),
290 gross_receivables: 50_000.0,
291 gross_payables: 30_000.0,
292 net_settlement: 20_000.0,
293 }];
294
295 let result = evaluator
296 .evaluate(&positions, &hedges, &covenants, &netting)
297 .unwrap();
298 assert!(result.passes);
299 assert_eq!(result.total_positions, 1);
300 assert_eq!(result.total_hedges, 2);
301 }
302
303 #[test]
304 fn test_wrong_closing_balance() {
305 let evaluator = TreasuryEvaluator::new();
306 let positions = vec![CashPositionData {
307 position_id: "CP001".to_string(),
308 opening_balance: 100_000.0,
309 inflows: 50_000.0,
310 outflows: 30_000.0,
311 closing_balance: 200_000.0, }];
313
314 let result = evaluator.evaluate(&positions, &[], &[], &[]).unwrap();
315 assert!(!result.passes);
316 assert!(result.issues[0].contains("Cash position balance"));
317 }
318
319 #[test]
320 fn test_wrong_hedge_classification() {
321 let evaluator = TreasuryEvaluator::new();
322 let hedges = vec![HedgeEffectivenessData {
323 hedge_id: "H001".to_string(),
324 effectiveness_ratio: 0.70, is_effective: true, }];
327
328 let result = evaluator.evaluate(&[], &hedges, &[], &[]).unwrap();
329 assert!(!result.passes);
330 assert!(result.issues[0].contains("Hedge effectiveness"));
331 }
332
333 #[test]
334 fn test_wrong_covenant_compliance() {
335 let evaluator = TreasuryEvaluator::new();
336 let covenants = vec![CovenantData {
337 covenant_id: "COV001".to_string(),
338 threshold: 3.0,
339 actual_value: 4.0, is_compliant: true, is_max_covenant: true,
342 }];
343
344 let result = evaluator.evaluate(&[], &[], &covenants, &[]).unwrap();
345 assert!(!result.passes);
346 assert!(result.issues[0].contains("Covenant compliance"));
347 }
348
349 #[test]
350 fn test_wrong_netting() {
351 let evaluator = TreasuryEvaluator::new();
352 let netting = vec![NettingData {
353 run_id: "NET001".to_string(),
354 gross_receivables: 50_000.0,
355 gross_payables: 30_000.0,
356 net_settlement: 5_000.0, }];
358
359 let result = evaluator.evaluate(&[], &[], &[], &netting).unwrap();
360 assert!(!result.passes);
361 assert!(result.issues[0].contains("Netting accuracy"));
362 }
363
364 #[test]
365 fn test_empty_data() {
366 let evaluator = TreasuryEvaluator::new();
367 let result = evaluator.evaluate(&[], &[], &[], &[]).unwrap();
368 assert!(result.passes);
369 }
370}
371
372#[derive(Debug, Clone)]
378pub struct TreasuryCashProofData {
379 pub treasury_cash_total: rust_decimal::Decimal,
381 pub gl_cash_total: rust_decimal::Decimal,
383 pub balance_sheet_cash: Option<rust_decimal::Decimal>,
385 pub cash_flow_ending_cash: Option<rust_decimal::Decimal>,
387 pub bank_recon_adjusted_total: Option<rust_decimal::Decimal>,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct TreasuryCashProofEvaluation {
394 pub treasury_gl_reconciled: bool,
396 pub treasury_gl_difference: rust_decimal::Decimal,
398 pub cash_flow_reconciled: Option<bool>,
400 pub bank_recon_reconciled: Option<bool>,
402 pub issues: Vec<String>,
404}
405
406pub struct TreasuryCashProofEvaluator {
408 tolerance: rust_decimal::Decimal,
409}
410
411impl TreasuryCashProofEvaluator {
412 pub fn new(tolerance: rust_decimal::Decimal) -> Self {
414 Self { tolerance }
415 }
416
417 pub fn evaluate(
419 &self,
420 data: &TreasuryCashProofData,
421 ) -> crate::error::EvalResult<TreasuryCashProofEvaluation> {
422 let mut issues = Vec::new();
423
424 let treasury_gl_difference = (data.treasury_cash_total - data.gl_cash_total).abs();
425 let treasury_gl_reconciled = treasury_gl_difference <= self.tolerance;
426 if !treasury_gl_reconciled {
427 issues.push(format!(
428 "Treasury cash ({}) != GL cash accounts ({}), diff={}",
429 data.treasury_cash_total, data.gl_cash_total, treasury_gl_difference
430 ));
431 }
432
433 let cash_flow_reconciled = data.cash_flow_ending_cash.map(|cf| {
434 let diff = (cf - data.gl_cash_total).abs();
435 if diff > self.tolerance {
436 issues.push(format!(
437 "Cash flow ending balance ({}) != GL cash ({}), diff={}",
438 cf, data.gl_cash_total, diff
439 ));
440 }
441 diff <= self.tolerance
442 });
443
444 let bank_recon_reconciled = data.bank_recon_adjusted_total.map(|br| {
445 let diff = (br - data.gl_cash_total).abs();
446 if diff > self.tolerance {
447 issues.push(format!(
448 "Bank recon adjusted balance ({}) != GL cash ({}), diff={}",
449 br, data.gl_cash_total, diff
450 ));
451 }
452 diff <= self.tolerance
453 });
454
455 Ok(TreasuryCashProofEvaluation {
456 treasury_gl_reconciled,
457 treasury_gl_difference,
458 cash_flow_reconciled,
459 bank_recon_reconciled,
460 issues,
461 })
462 }
463}
464
465impl Default for TreasuryCashProofEvaluator {
466 fn default() -> Self {
467 Self::new(rust_decimal::Decimal::new(100, 0)) }
469}