datasynth-eval 3.1.0

Evaluation framework for synthetic financial data quality and coherence
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
//! Treasury coherence evaluator.
//!
//! Validates cash position balance equations, hedge effectiveness ranges,
//! covenant compliance logic, and intercompany netting calculations.

use crate::error::EvalResult;
use serde::{Deserialize, Serialize};

/// Thresholds for treasury evaluation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreasuryThresholds {
    /// Minimum accuracy for closing = opening + inflows - outflows.
    pub min_balance_accuracy: f64,
    /// Tolerance for balance comparisons.
    pub balance_tolerance: f64,
    /// Minimum rate of hedges with correct effectiveness classification.
    pub min_hedge_effectiveness_rate: f64,
    /// Minimum rate of covenants with correct compliance classification.
    pub min_covenant_compliance_rate: f64,
    /// Minimum accuracy for netting settlement calculations.
    pub min_netting_accuracy: f64,
}

impl Default for TreasuryThresholds {
    fn default() -> Self {
        Self {
            min_balance_accuracy: 0.999,
            balance_tolerance: 0.01,
            min_hedge_effectiveness_rate: 0.95,
            min_covenant_compliance_rate: 0.95,
            min_netting_accuracy: 0.999,
        }
    }
}

/// Cash position data for balance validation.
#[derive(Debug, Clone)]
pub struct CashPositionData {
    /// Position identifier.
    pub position_id: String,
    /// Opening balance.
    pub opening_balance: f64,
    /// Total inflows.
    pub inflows: f64,
    /// Total outflows.
    pub outflows: f64,
    /// Closing balance.
    pub closing_balance: f64,
}

/// Hedge effectiveness data for range validation.
#[derive(Debug, Clone)]
pub struct HedgeEffectivenessData {
    /// Hedge identifier.
    pub hedge_id: String,
    /// Effectiveness ratio (should be 0.80-1.25 for effective hedges).
    pub effectiveness_ratio: f64,
    /// Whether classified as effective.
    pub is_effective: bool,
}

/// Covenant data for compliance validation.
#[derive(Debug, Clone)]
pub struct CovenantData {
    /// Covenant identifier.
    pub covenant_id: String,
    /// Covenant threshold value.
    pub threshold: f64,
    /// Actual measured value.
    pub actual_value: f64,
    /// Whether classified as compliant.
    pub is_compliant: bool,
    /// Whether this is a maximum covenant (actual must be <= threshold).
    /// If false, it's a minimum covenant (actual must be >= threshold).
    pub is_max_covenant: bool,
}

/// Netting data for settlement validation.
#[derive(Debug, Clone)]
pub struct NettingData {
    /// Netting run identifier.
    pub run_id: String,
    /// Gross receivables.
    pub gross_receivables: f64,
    /// Gross payables.
    pub gross_payables: f64,
    /// Net settlement amount.
    pub net_settlement: f64,
}

/// Results of treasury coherence evaluation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreasuryEvaluation {
    /// Fraction of positions where closing ≈ opening + inflows - outflows.
    pub balance_accuracy: f64,
    /// Fraction of hedges with correct effectiveness classification.
    pub hedge_effectiveness_accuracy: f64,
    /// Fraction of covenants with correct compliance classification.
    pub covenant_compliance_accuracy: f64,
    /// Fraction of netting runs where net ≈ |receivables - payables|.
    pub netting_accuracy: f64,
    /// Total cash positions evaluated.
    pub total_positions: usize,
    /// Total hedges evaluated.
    pub total_hedges: usize,
    /// Total covenants evaluated.
    pub total_covenants: usize,
    /// Total netting runs evaluated.
    pub total_netting_runs: usize,
    /// Overall pass/fail.
    pub passes: bool,
    /// Issues found.
    pub issues: Vec<String>,
}

/// Evaluator for treasury coherence.
pub struct TreasuryEvaluator {
    thresholds: TreasuryThresholds,
}

impl TreasuryEvaluator {
    /// Create a new evaluator with default thresholds.
    pub fn new() -> Self {
        Self {
            thresholds: TreasuryThresholds::default(),
        }
    }

    /// Create with custom thresholds.
    pub fn with_thresholds(thresholds: TreasuryThresholds) -> Self {
        Self { thresholds }
    }

    /// Evaluate treasury data coherence.
    pub fn evaluate(
        &self,
        positions: &[CashPositionData],
        hedges: &[HedgeEffectivenessData],
        covenants: &[CovenantData],
        netting_runs: &[NettingData],
    ) -> EvalResult<TreasuryEvaluation> {
        let mut issues = Vec::new();
        let tolerance = self.thresholds.balance_tolerance;

        // 1. Cash position balance: closing ≈ opening + inflows - outflows
        let balance_ok = positions
            .iter()
            .filter(|p| {
                let expected = p.opening_balance + p.inflows - p.outflows;
                (p.closing_balance - expected).abs() <= tolerance * p.opening_balance.abs().max(1.0)
            })
            .count();
        let balance_accuracy = if positions.is_empty() {
            1.0
        } else {
            balance_ok as f64 / positions.len() as f64
        };

        // 2. Hedge effectiveness: is_effective iff ratio in [0.80, 1.25]
        let hedge_ok = hedges
            .iter()
            .filter(|h| {
                let in_range = h.effectiveness_ratio >= 0.80 && h.effectiveness_ratio <= 1.25;
                h.is_effective == in_range
            })
            .count();
        let hedge_effectiveness_accuracy = if hedges.is_empty() {
            1.0
        } else {
            hedge_ok as f64 / hedges.len() as f64
        };

        // 3. Covenant compliance: is_compliant iff actual meets threshold
        let covenant_ok = covenants
            .iter()
            .filter(|c| {
                let should_comply = if c.is_max_covenant {
                    c.actual_value <= c.threshold
                } else {
                    c.actual_value >= c.threshold
                };
                c.is_compliant == should_comply
            })
            .count();
        let covenant_compliance_accuracy = if covenants.is_empty() {
            1.0
        } else {
            covenant_ok as f64 / covenants.len() as f64
        };

        // 4. Netting: net_settlement ≈ |gross_receivables - gross_payables|
        let netting_ok = netting_runs
            .iter()
            .filter(|n| {
                let expected = (n.gross_receivables - n.gross_payables).abs();
                (n.net_settlement - expected).abs()
                    <= tolerance * n.gross_receivables.abs().max(1.0)
            })
            .count();
        let netting_accuracy = if netting_runs.is_empty() {
            1.0
        } else {
            netting_ok as f64 / netting_runs.len() as f64
        };

        // Check thresholds
        if balance_accuracy < self.thresholds.min_balance_accuracy {
            issues.push(format!(
                "Cash position balance accuracy {:.4} < {:.4}",
                balance_accuracy, self.thresholds.min_balance_accuracy
            ));
        }
        if hedge_effectiveness_accuracy < self.thresholds.min_hedge_effectiveness_rate {
            issues.push(format!(
                "Hedge effectiveness accuracy {:.4} < {:.4}",
                hedge_effectiveness_accuracy, self.thresholds.min_hedge_effectiveness_rate
            ));
        }
        if covenant_compliance_accuracy < self.thresholds.min_covenant_compliance_rate {
            issues.push(format!(
                "Covenant compliance accuracy {:.4} < {:.4}",
                covenant_compliance_accuracy, self.thresholds.min_covenant_compliance_rate
            ));
        }
        if netting_accuracy < self.thresholds.min_netting_accuracy {
            issues.push(format!(
                "Netting accuracy {:.4} < {:.4}",
                netting_accuracy, self.thresholds.min_netting_accuracy
            ));
        }

        let passes = issues.is_empty();

        Ok(TreasuryEvaluation {
            balance_accuracy,
            hedge_effectiveness_accuracy,
            covenant_compliance_accuracy,
            netting_accuracy,
            total_positions: positions.len(),
            total_hedges: hedges.len(),
            total_covenants: covenants.len(),
            total_netting_runs: netting_runs.len(),
            passes,
            issues,
        })
    }
}

impl Default for TreasuryEvaluator {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;

    #[test]
    fn test_valid_treasury_data() {
        let evaluator = TreasuryEvaluator::new();
        let positions = vec![CashPositionData {
            position_id: "CP001".to_string(),
            opening_balance: 100_000.0,
            inflows: 50_000.0,
            outflows: 30_000.0,
            closing_balance: 120_000.0,
        }];
        let hedges = vec![
            HedgeEffectivenessData {
                hedge_id: "H001".to_string(),
                effectiveness_ratio: 0.95,
                is_effective: true,
            },
            HedgeEffectivenessData {
                hedge_id: "H002".to_string(),
                effectiveness_ratio: 0.70,
                is_effective: false,
            },
        ];
        let covenants = vec![CovenantData {
            covenant_id: "COV001".to_string(),
            threshold: 3.0,
            actual_value: 2.5,
            is_compliant: true,
            is_max_covenant: true,
        }];
        let netting = vec![NettingData {
            run_id: "NET001".to_string(),
            gross_receivables: 50_000.0,
            gross_payables: 30_000.0,
            net_settlement: 20_000.0,
        }];

        let result = evaluator
            .evaluate(&positions, &hedges, &covenants, &netting)
            .unwrap();
        assert!(result.passes);
        assert_eq!(result.total_positions, 1);
        assert_eq!(result.total_hedges, 2);
    }

    #[test]
    fn test_wrong_closing_balance() {
        let evaluator = TreasuryEvaluator::new();
        let positions = vec![CashPositionData {
            position_id: "CP001".to_string(),
            opening_balance: 100_000.0,
            inflows: 50_000.0,
            outflows: 30_000.0,
            closing_balance: 200_000.0, // Wrong: should be 120,000
        }];

        let result = evaluator.evaluate(&positions, &[], &[], &[]).unwrap();
        assert!(!result.passes);
        assert!(result.issues[0].contains("Cash position balance"));
    }

    #[test]
    fn test_wrong_hedge_classification() {
        let evaluator = TreasuryEvaluator::new();
        let hedges = vec![HedgeEffectivenessData {
            hedge_id: "H001".to_string(),
            effectiveness_ratio: 0.70, // Out of range
            is_effective: true,        // Wrong: should be false
        }];

        let result = evaluator.evaluate(&[], &hedges, &[], &[]).unwrap();
        assert!(!result.passes);
        assert!(result.issues[0].contains("Hedge effectiveness"));
    }

    #[test]
    fn test_wrong_covenant_compliance() {
        let evaluator = TreasuryEvaluator::new();
        let covenants = vec![CovenantData {
            covenant_id: "COV001".to_string(),
            threshold: 3.0,
            actual_value: 4.0,  // Exceeds max covenant
            is_compliant: true, // Wrong: should be false
            is_max_covenant: true,
        }];

        let result = evaluator.evaluate(&[], &[], &covenants, &[]).unwrap();
        assert!(!result.passes);
        assert!(result.issues[0].contains("Covenant compliance"));
    }

    #[test]
    fn test_wrong_netting() {
        let evaluator = TreasuryEvaluator::new();
        let netting = vec![NettingData {
            run_id: "NET001".to_string(),
            gross_receivables: 50_000.0,
            gross_payables: 30_000.0,
            net_settlement: 5_000.0, // Wrong: should be 20,000
        }];

        let result = evaluator.evaluate(&[], &[], &[], &netting).unwrap();
        assert!(!result.passes);
        assert!(result.issues[0].contains("Netting accuracy"));
    }

    #[test]
    fn test_empty_data() {
        let evaluator = TreasuryEvaluator::new();
        let result = evaluator.evaluate(&[], &[], &[], &[]).unwrap();
        assert!(result.passes);
    }
}

// ---------------------------------------------------------------------------
// Treasury↔Cash Flow↔Bank Reconciliation Proof (v2.5 — cross-domain coherence)
// ---------------------------------------------------------------------------

/// Data for validating that treasury cash positions match GL and cash flow statement.
#[derive(Debug, Clone)]
pub struct TreasuryCashProofData {
    /// Total cash position from treasury module.
    pub treasury_cash_total: rust_decimal::Decimal,
    /// Total cash from GL accounts (1000 + 1010 + 1020).
    pub gl_cash_total: rust_decimal::Decimal,
    /// Cash and equivalents from balance sheet (if available).
    pub balance_sheet_cash: Option<rust_decimal::Decimal>,
    /// Ending cash from cash flow statement (if available).
    pub cash_flow_ending_cash: Option<rust_decimal::Decimal>,
    /// Bank reconciliation adjusted balance total.
    pub bank_recon_adjusted_total: Option<rust_decimal::Decimal>,
}

/// Results of treasury↔cash flow↔bank reconciliation proof.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreasuryCashProofEvaluation {
    /// Whether treasury cash matches GL cash accounts.
    pub treasury_gl_reconciled: bool,
    /// Difference between treasury and GL cash.
    pub treasury_gl_difference: rust_decimal::Decimal,
    /// Whether cash flow ending balance matches GL cash.
    pub cash_flow_reconciled: Option<bool>,
    /// Whether bank reconciliation matches GL cash.
    pub bank_recon_reconciled: Option<bool>,
    /// Issues found.
    pub issues: Vec<String>,
}

/// Validates that cash balances agree across treasury, GL, cash flow statement, and bank recon.
pub struct TreasuryCashProofEvaluator {
    tolerance: rust_decimal::Decimal,
}

impl TreasuryCashProofEvaluator {
    /// Create with custom tolerance.
    pub fn new(tolerance: rust_decimal::Decimal) -> Self {
        Self { tolerance }
    }

    /// Validate treasury cash proof.
    pub fn evaluate(
        &self,
        data: &TreasuryCashProofData,
    ) -> crate::error::EvalResult<TreasuryCashProofEvaluation> {
        let mut issues = Vec::new();

        let treasury_gl_difference = (data.treasury_cash_total - data.gl_cash_total).abs();
        let treasury_gl_reconciled = treasury_gl_difference <= self.tolerance;
        if !treasury_gl_reconciled {
            issues.push(format!(
                "Treasury cash ({}) != GL cash accounts ({}), diff={}",
                data.treasury_cash_total, data.gl_cash_total, treasury_gl_difference
            ));
        }

        let cash_flow_reconciled = data.cash_flow_ending_cash.map(|cf| {
            let diff = (cf - data.gl_cash_total).abs();
            if diff > self.tolerance {
                issues.push(format!(
                    "Cash flow ending balance ({}) != GL cash ({}), diff={}",
                    cf, data.gl_cash_total, diff
                ));
            }
            diff <= self.tolerance
        });

        let bank_recon_reconciled = data.bank_recon_adjusted_total.map(|br| {
            let diff = (br - data.gl_cash_total).abs();
            if diff > self.tolerance {
                issues.push(format!(
                    "Bank recon adjusted balance ({}) != GL cash ({}), diff={}",
                    br, data.gl_cash_total, diff
                ));
            }
            diff <= self.tolerance
        });

        Ok(TreasuryCashProofEvaluation {
            treasury_gl_reconciled,
            treasury_gl_difference,
            cash_flow_reconciled,
            bank_recon_reconciled,
            issues,
        })
    }
}

impl Default for TreasuryCashProofEvaluator {
    fn default() -> Self {
        Self::new(rust_decimal::Decimal::new(100, 0)) // $100 tolerance
    }
}