datasynth_core/models/expected_credit_loss.rs
1//! Expected Credit Loss (ECL) models — IFRS 9 / ASC 326.
2//!
3//! This module provides data structures for the simplified approach ECL model
4//! applied to trade receivables via a provision matrix based on AR aging.
5//!
6//! Key IFRS 9 / ASC 326 concepts modelled:
7//! - Simplified approach (trade receivables): lifetime ECL at all times
8//! - Provision matrix: historical loss rates by aging bucket + forward-looking adjustment
9//! - ECL = Exposure × PD × LGD (Stage 1/2/3 for completeness)
10//! - Provision movement: opening → new originations → stage transfers → write-offs → closing
11
12use chrono::NaiveDate;
13use rust_decimal::Decimal;
14use serde::{Deserialize, Serialize};
15
16use crate::models::subledger::ar::AgingBucket;
17
18// ============================================================================
19// Enums
20// ============================================================================
21
22/// ECL measurement approach.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum EclApproach {
25 /// Simplified approach — lifetime ECL at all times (used for trade receivables).
26 Simplified,
27 /// General approach — 3-stage model based on credit deterioration.
28 General,
29}
30
31/// IFRS 9 / ASC 326 stage classification.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33pub enum EclStage {
34 /// Stage 1 — performing; 12-month ECL.
35 Stage1Month12,
36 /// Stage 2 — significant credit deterioration; lifetime ECL.
37 Stage2Lifetime,
38 /// Stage 3 — credit-impaired; lifetime ECL, interest on net carrying amount.
39 Stage3CreditImpaired,
40}
41
42// ============================================================================
43// Core model structs
44// ============================================================================
45
46/// Top-level ECL model for one entity / measurement date.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct EclModel {
49 /// Unique model ID.
50 pub id: String,
51
52 /// Entity (company) code.
53 pub entity_code: String,
54
55 /// Measurement approach.
56 pub approach: EclApproach,
57
58 /// Measurement date (balance-sheet date).
59 pub measurement_date: NaiveDate,
60
61 /// Accounting framework ("IFRS_9" or "ASC_326").
62 pub framework: String,
63
64 /// Portfolio segments within this model.
65 pub portfolio_segments: Vec<EclPortfolioSegment>,
66
67 /// Provision matrix (simplified approach only).
68 pub provision_matrix: Option<ProvisionMatrix>,
69
70 /// Total ECL across all segments.
71 #[serde(with = "rust_decimal::serde::str")]
72 pub total_ecl: Decimal,
73
74 /// Total gross exposure.
75 #[serde(with = "rust_decimal::serde::str")]
76 pub total_exposure: Decimal,
77}
78
79/// A portfolio segment within the ECL model (e.g. "Trade Receivables", "Intercompany").
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct EclPortfolioSegment {
82 /// Segment name.
83 pub segment_name: String,
84
85 /// Gross exposure at the measurement date.
86 #[serde(with = "rust_decimal::serde::str")]
87 pub exposure_at_default: Decimal,
88
89 /// Stage allocations within this segment.
90 pub staging: Vec<EclStageAllocation>,
91
92 /// Total ECL for this segment (sum of stage ECLs).
93 #[serde(with = "rust_decimal::serde::str")]
94 pub total_ecl: Decimal,
95}
96
97/// ECL split by IFRS 9 stage.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct EclStageAllocation {
100 /// IFRS 9 / ASC 326 stage.
101 pub stage: EclStage,
102
103 /// Gross exposure in this stage.
104 #[serde(with = "rust_decimal::serde::str")]
105 pub exposure: Decimal,
106
107 /// Probability of default (0–1).
108 #[serde(with = "rust_decimal::serde::str")]
109 pub probability_of_default: Decimal,
110
111 /// Loss given default (0–1).
112 #[serde(with = "rust_decimal::serde::str")]
113 pub loss_given_default: Decimal,
114
115 /// Computed ECL = exposure × PD × LGD × forward_looking_adjustment.
116 #[serde(with = "rust_decimal::serde::str")]
117 pub ecl_amount: Decimal,
118
119 /// Forward-looking multiplier applied to historical rate (1.0 = no adjustment).
120 #[serde(with = "rust_decimal::serde::str")]
121 pub forward_looking_adjustment: Decimal,
122}
123
124// ============================================================================
125// Provision matrix
126// ============================================================================
127
128/// Provision matrix for the simplified approach — one row per aging bucket.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ProvisionMatrix {
131 /// Entity code.
132 pub entity_code: String,
133
134 /// Measurement date.
135 pub measurement_date: NaiveDate,
136
137 /// Scenario weights used for forward-looking adjustment.
138 pub scenario_weights: ScenarioWeights,
139
140 /// One row per AR aging bucket.
141 pub aging_buckets: Vec<ProvisionMatrixRow>,
142
143 /// Sum of all provisions across all buckets.
144 #[serde(with = "rust_decimal::serde::str")]
145 pub total_provision: Decimal,
146
147 /// Sum of all exposures across all buckets.
148 #[serde(with = "rust_decimal::serde::str")]
149 pub total_exposure: Decimal,
150
151 /// Blended loss rate = total_provision / total_exposure.
152 #[serde(with = "rust_decimal::serde::str")]
153 pub blended_loss_rate: Decimal,
154}
155
156/// Scenario weights for forward-looking macro adjustment.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ScenarioWeights {
159 /// Weight for base scenario.
160 #[serde(with = "rust_decimal::serde::str")]
161 pub base: Decimal,
162
163 /// Multiplier applied to historical rates under base scenario (typically 1.0).
164 #[serde(with = "rust_decimal::serde::str")]
165 pub base_multiplier: Decimal,
166
167 /// Weight for optimistic scenario.
168 #[serde(with = "rust_decimal::serde::str")]
169 pub optimistic: Decimal,
170
171 /// Multiplier applied to historical rates under optimistic scenario (< 1.0).
172 #[serde(with = "rust_decimal::serde::str")]
173 pub optimistic_multiplier: Decimal,
174
175 /// Weight for pessimistic scenario.
176 #[serde(with = "rust_decimal::serde::str")]
177 pub pessimistic: Decimal,
178
179 /// Multiplier applied to historical rates under pessimistic scenario (> 1.0).
180 #[serde(with = "rust_decimal::serde::str")]
181 pub pessimistic_multiplier: Decimal,
182
183 /// Resulting blended forward-looking multiplier
184 /// = base*base_m + optimistic*opt_m + pessimistic*pes_m.
185 #[serde(with = "rust_decimal::serde::str")]
186 pub blended_multiplier: Decimal,
187}
188
189/// One row of the provision matrix, corresponding to an AR aging bucket.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ProvisionMatrixRow {
192 /// Aging bucket this row covers.
193 pub bucket: AgingBucket,
194
195 /// Historical loss rate for this bucket (e.g. 0.005 = 0.5%).
196 #[serde(with = "rust_decimal::serde::str")]
197 pub historical_loss_rate: Decimal,
198
199 /// Forward-looking adjustment multiplier (scenario-weighted).
200 #[serde(with = "rust_decimal::serde::str")]
201 pub forward_looking_adjustment: Decimal,
202
203 /// Applied loss rate = historical_loss_rate × forward_looking_adjustment.
204 #[serde(with = "rust_decimal::serde::str")]
205 pub applied_loss_rate: Decimal,
206
207 /// Gross exposure in this bucket.
208 #[serde(with = "rust_decimal::serde::str")]
209 pub exposure: Decimal,
210
211 /// Provision = exposure × applied_loss_rate.
212 #[serde(with = "rust_decimal::serde::str")]
213 pub provision: Decimal,
214}
215
216// ============================================================================
217// Provision movement
218// ============================================================================
219
220/// Provision movement (roll-forward) for one fiscal period.
221///
222/// Reconciles the opening and closing allowance for doubtful accounts.
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct EclProvisionMovement {
225 /// Unique movement record ID.
226 pub id: String,
227
228 /// Entity code.
229 pub entity_code: String,
230
231 /// Fiscal period label (e.g. "2024-Q1", "2024-12").
232 pub period: String,
233
234 /// Opening allowance balance.
235 #[serde(with = "rust_decimal::serde::str")]
236 pub opening: Decimal,
237
238 /// New originations charged to P&L (increase in allowance).
239 #[serde(with = "rust_decimal::serde::str")]
240 pub new_originations: Decimal,
241
242 /// Stage-transfer adjustments (positive = provision increase).
243 #[serde(with = "rust_decimal::serde::str")]
244 pub stage_transfers: Decimal,
245
246 /// Write-offs charged against the allowance (reduces allowance balance).
247 #[serde(with = "rust_decimal::serde::str")]
248 pub write_offs: Decimal,
249
250 /// Cash recoveries on previously written-off receivables (increases allowance).
251 #[serde(with = "rust_decimal::serde::str")]
252 pub recoveries: Decimal,
253
254 /// Closing allowance = opening + new_originations + stage_transfers - write_offs + recoveries.
255 #[serde(with = "rust_decimal::serde::str")]
256 pub closing: Decimal,
257
258 /// P&L charge for the period = new_originations + stage_transfers + recoveries - write_offs.
259 #[serde(with = "rust_decimal::serde::str")]
260 pub pl_charge: Decimal,
261}