Skip to main content

datasynth_eval/coherence/
treasury.rs

1//! Treasury coherence evaluator.
2//!
3//! Validates cash position balance equations, hedge effectiveness ranges,
4//! covenant compliance logic, and intercompany netting calculations.
5
6use crate::error::EvalResult;
7use serde::{Deserialize, Serialize};
8
9/// Thresholds for treasury evaluation.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct TreasuryThresholds {
12    /// Minimum accuracy for closing = opening + inflows - outflows.
13    pub min_balance_accuracy: f64,
14    /// Tolerance for balance comparisons.
15    pub balance_tolerance: f64,
16    /// Minimum rate of hedges with correct effectiveness classification.
17    pub min_hedge_effectiveness_rate: f64,
18    /// Minimum rate of covenants with correct compliance classification.
19    pub min_covenant_compliance_rate: f64,
20    /// Minimum accuracy for netting settlement calculations.
21    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/// Cash position data for balance validation.
37#[derive(Debug, Clone)]
38pub struct CashPositionData {
39    /// Position identifier.
40    pub position_id: String,
41    /// Opening balance.
42    pub opening_balance: f64,
43    /// Total inflows.
44    pub inflows: f64,
45    /// Total outflows.
46    pub outflows: f64,
47    /// Closing balance.
48    pub closing_balance: f64,
49}
50
51/// Hedge effectiveness data for range validation.
52#[derive(Debug, Clone)]
53pub struct HedgeEffectivenessData {
54    /// Hedge identifier.
55    pub hedge_id: String,
56    /// Effectiveness ratio (should be 0.80-1.25 for effective hedges).
57    pub effectiveness_ratio: f64,
58    /// Whether classified as effective.
59    pub is_effective: bool,
60}
61
62/// Covenant data for compliance validation.
63#[derive(Debug, Clone)]
64pub struct CovenantData {
65    /// Covenant identifier.
66    pub covenant_id: String,
67    /// Covenant threshold value.
68    pub threshold: f64,
69    /// Actual measured value.
70    pub actual_value: f64,
71    /// Whether classified as compliant.
72    pub is_compliant: bool,
73    /// Whether this is a maximum covenant (actual must be <= threshold).
74    /// If false, it's a minimum covenant (actual must be >= threshold).
75    pub is_max_covenant: bool,
76}
77
78/// Netting data for settlement validation.
79#[derive(Debug, Clone)]
80pub struct NettingData {
81    /// Netting run identifier.
82    pub run_id: String,
83    /// Gross receivables.
84    pub gross_receivables: f64,
85    /// Gross payables.
86    pub gross_payables: f64,
87    /// Net settlement amount.
88    pub net_settlement: f64,
89}
90
91/// Results of treasury coherence evaluation.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct TreasuryEvaluation {
94    /// Fraction of positions where closing ≈ opening + inflows - outflows.
95    pub balance_accuracy: f64,
96    /// Fraction of hedges with correct effectiveness classification.
97    pub hedge_effectiveness_accuracy: f64,
98    /// Fraction of covenants with correct compliance classification.
99    pub covenant_compliance_accuracy: f64,
100    /// Fraction of netting runs where net ≈ |receivables - payables|.
101    pub netting_accuracy: f64,
102    /// Total cash positions evaluated.
103    pub total_positions: usize,
104    /// Total hedges evaluated.
105    pub total_hedges: usize,
106    /// Total covenants evaluated.
107    pub total_covenants: usize,
108    /// Total netting runs evaluated.
109    pub total_netting_runs: usize,
110    /// Overall pass/fail.
111    pub passes: bool,
112    /// Issues found.
113    pub issues: Vec<String>,
114}
115
116/// Evaluator for treasury coherence.
117pub struct TreasuryEvaluator {
118    thresholds: TreasuryThresholds,
119}
120
121impl TreasuryEvaluator {
122    /// Create a new evaluator with default thresholds.
123    pub fn new() -> Self {
124        Self {
125            thresholds: TreasuryThresholds::default(),
126        }
127    }
128
129    /// Create with custom thresholds.
130    pub fn with_thresholds(thresholds: TreasuryThresholds) -> Self {
131        Self { thresholds }
132    }
133
134    /// Evaluate treasury data coherence.
135    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        // 1. Cash position balance: closing ≈ opening + inflows - outflows
146        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        // 2. Hedge effectiveness: is_effective iff ratio in [0.80, 1.25]
160        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        // 3. Covenant compliance: is_compliant iff actual meets threshold
174        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        // 4. Netting: net_settlement ≈ |gross_receivables - gross_payables|
192        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        // Check thresholds
207        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, // Wrong: should be 120,000
312        }];
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, // Out of range
325            is_effective: true,        // Wrong: should be false
326        }];
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,  // Exceeds max covenant
340            is_compliant: true, // Wrong: should be false
341            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, // Wrong: should be 20,000
357        }];
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// ---------------------------------------------------------------------------
373// Treasury↔Cash Flow↔Bank Reconciliation Proof (v2.5 — cross-domain coherence)
374// ---------------------------------------------------------------------------
375
376/// Data for validating that treasury cash positions match GL and cash flow statement.
377#[derive(Debug, Clone)]
378pub struct TreasuryCashProofData {
379    /// Total cash position from treasury module.
380    pub treasury_cash_total: rust_decimal::Decimal,
381    /// Total cash from GL accounts (1000 + 1010 + 1020).
382    pub gl_cash_total: rust_decimal::Decimal,
383    /// Cash and equivalents from balance sheet (if available).
384    pub balance_sheet_cash: Option<rust_decimal::Decimal>,
385    /// Ending cash from cash flow statement (if available).
386    pub cash_flow_ending_cash: Option<rust_decimal::Decimal>,
387    /// Bank reconciliation adjusted balance total.
388    pub bank_recon_adjusted_total: Option<rust_decimal::Decimal>,
389}
390
391/// Results of treasury↔cash flow↔bank reconciliation proof.
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct TreasuryCashProofEvaluation {
394    /// Whether treasury cash matches GL cash accounts.
395    pub treasury_gl_reconciled: bool,
396    /// Difference between treasury and GL cash.
397    pub treasury_gl_difference: rust_decimal::Decimal,
398    /// Whether cash flow ending balance matches GL cash.
399    pub cash_flow_reconciled: Option<bool>,
400    /// Whether bank reconciliation matches GL cash.
401    pub bank_recon_reconciled: Option<bool>,
402    /// Issues found.
403    pub issues: Vec<String>,
404}
405
406/// Validates that cash balances agree across treasury, GL, cash flow statement, and bank recon.
407pub struct TreasuryCashProofEvaluator {
408    tolerance: rust_decimal::Decimal,
409}
410
411impl TreasuryCashProofEvaluator {
412    /// Create with custom tolerance.
413    pub fn new(tolerance: rust_decimal::Decimal) -> Self {
414        Self { tolerance }
415    }
416
417    /// Validate treasury cash proof.
418    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)) // $100 tolerance
468    }
469}