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)]
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, // Wrong: should be 120,000
313        }];
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, // Out of range
326            is_effective: true,        // Wrong: should be false
327        }];
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,  // Exceeds max covenant
341            is_compliant: true, // Wrong: should be false
342            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, // Wrong: should be 20,000
358        }];
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}