Skip to main content

datasynth_banking/
config.rs

1//! Configuration for banking data generation.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Configuration for banking data generation.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct BankingConfig {
9    /// Whether banking generation is enabled
10    #[serde(default = "default_true")]
11    pub enabled: bool,
12    /// Population configuration
13    #[serde(default)]
14    pub population: PopulationConfig,
15    /// Product configuration
16    #[serde(default)]
17    pub products: ProductConfig,
18    /// Compliance configuration
19    #[serde(default)]
20    pub compliance: ComplianceConfig,
21    /// AML typology configuration
22    #[serde(default)]
23    pub typologies: TypologyConfig,
24    /// Spoofing (adversarial) configuration
25    #[serde(default)]
26    pub spoofing: SpoofingConfig,
27    /// Output configuration
28    #[serde(default)]
29    pub output: BankingOutputConfig,
30}
31
32fn default_true() -> bool {
33    true
34}
35
36impl Default for BankingConfig {
37    fn default() -> Self {
38        Self {
39            enabled: true,
40            population: PopulationConfig::default(),
41            products: ProductConfig::default(),
42            compliance: ComplianceConfig::default(),
43            typologies: TypologyConfig::default(),
44            spoofing: SpoofingConfig::default(),
45            output: BankingOutputConfig::default(),
46        }
47    }
48}
49
50impl BankingConfig {
51    /// Create a small configuration for testing.
52    pub fn small() -> Self {
53        Self {
54            population: PopulationConfig {
55                retail_customers: 100,
56                business_customers: 20,
57                trusts: 5,
58                ..Default::default()
59            },
60            ..Default::default()
61        }
62    }
63
64    /// Create a medium configuration.
65    pub fn medium() -> Self {
66        Self {
67            population: PopulationConfig {
68                retail_customers: 1_000,
69                business_customers: 200,
70                trusts: 50,
71                ..Default::default()
72            },
73            ..Default::default()
74        }
75    }
76
77    /// Create a large configuration.
78    pub fn large() -> Self {
79        Self {
80            population: PopulationConfig {
81                retail_customers: 10_000,
82                business_customers: 1_000,
83                trusts: 100,
84                ..Default::default()
85            },
86            ..Default::default()
87        }
88    }
89
90    /// Validate the configuration.
91    pub fn validate(&self) -> Result<(), Vec<String>> {
92        let mut errors = Vec::new();
93
94        // Validate population
95        if self.population.retail_customers == 0
96            && self.population.business_customers == 0
97            && self.population.trusts == 0
98        {
99            errors.push("At least one customer type must have non-zero count".to_string());
100        }
101
102        // Validate persona weights sum to 1.0
103        let retail_sum: f64 = self.population.retail_persona_weights.values().sum();
104        if (retail_sum - 1.0).abs() > 0.01 {
105            errors.push(format!(
106                "Retail persona weights must sum to 1.0, got {retail_sum}"
107            ));
108        }
109
110        // Validate typology rates
111        let total_suspicious = self.typologies.structuring_rate
112            + self.typologies.funnel_rate
113            + self.typologies.layering_rate
114            + self.typologies.mule_rate
115            + self.typologies.fraud_rate;
116        if total_suspicious > self.typologies.suspicious_rate + 0.001 {
117            errors.push(format!(
118                "Sum of typology rates ({}) exceeds suspicious_rate ({})",
119                total_suspicious, self.typologies.suspicious_rate
120            ));
121        }
122
123        // Validate spoofing intensity
124        if self.spoofing.intensity < 0.0 || self.spoofing.intensity > 1.0 {
125            errors.push("Spoofing intensity must be between 0.0 and 1.0".to_string());
126        }
127
128        if errors.is_empty() {
129            Ok(())
130        } else {
131            Err(errors)
132        }
133    }
134}
135
136/// Population configuration.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct PopulationConfig {
139    /// Number of retail customers
140    pub retail_customers: u32,
141    /// Retail persona weight distribution
142    pub retail_persona_weights: HashMap<String, f64>,
143    /// Number of business customers
144    pub business_customers: u32,
145    /// Business persona weight distribution
146    pub business_persona_weights: HashMap<String, f64>,
147    /// Number of trust customers
148    pub trusts: u32,
149    /// Household formation rate (proportion of retail in households)
150    pub household_rate: f64,
151    /// Average household size
152    pub avg_household_size: f64,
153    /// Simulation period in months
154    pub period_months: u32,
155    /// Simulation start date (YYYY-MM-DD)
156    pub start_date: String,
157}
158
159impl Default for PopulationConfig {
160    fn default() -> Self {
161        let mut retail_weights = HashMap::new();
162        retail_weights.insert("student".to_string(), 0.15);
163        retail_weights.insert("early_career".to_string(), 0.25);
164        retail_weights.insert("mid_career".to_string(), 0.30);
165        retail_weights.insert("retiree".to_string(), 0.15);
166        retail_weights.insert("high_net_worth".to_string(), 0.05);
167        retail_weights.insert("gig_worker".to_string(), 0.10);
168
169        let mut business_weights = HashMap::new();
170        business_weights.insert("small_business".to_string(), 0.50);
171        business_weights.insert("mid_market".to_string(), 0.25);
172        business_weights.insert("enterprise".to_string(), 0.05);
173        business_weights.insert("cash_intensive".to_string(), 0.10);
174        business_weights.insert("import_export".to_string(), 0.05);
175        business_weights.insert("professional_services".to_string(), 0.05);
176
177        Self {
178            retail_customers: 10_000,
179            retail_persona_weights: retail_weights,
180            business_customers: 1_000,
181            business_persona_weights: business_weights,
182            trusts: 100,
183            household_rate: 0.4,
184            avg_household_size: 2.3,
185            period_months: 12,
186            start_date: "2024-01-01".to_string(),
187        }
188    }
189}
190
191/// Product configuration.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ProductConfig {
194    /// Cash transaction intensity (0.0-1.0)
195    pub cash_intensity: f64,
196    /// Cross-border transaction rate (0.0-1.0)
197    pub cross_border_rate: f64,
198    /// Card vs transfer ratio for payments
199    pub card_vs_transfer: f64,
200    /// Average accounts per retail customer
201    pub avg_accounts_retail: f64,
202    /// Average accounts per business customer
203    pub avg_accounts_business: f64,
204    /// Proportion of customers with debit cards
205    pub debit_card_rate: f64,
206    /// Proportion of customers with international capability
207    pub international_rate: f64,
208}
209
210impl Default for ProductConfig {
211    fn default() -> Self {
212        Self {
213            cash_intensity: 0.15,
214            cross_border_rate: 0.05,
215            card_vs_transfer: 0.6,
216            avg_accounts_retail: 1.5,
217            avg_accounts_business: 2.5,
218            debit_card_rate: 0.85,
219            international_rate: 0.10,
220        }
221    }
222}
223
224/// Compliance configuration.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ComplianceConfig {
227    /// Risk appetite (low, medium, high)
228    pub risk_appetite: RiskAppetite,
229    /// KYC completeness rate (0.0-1.0)
230    pub kyc_completeness: f64,
231    /// Proportion of high-risk customers accepted
232    pub high_risk_tolerance: f64,
233    /// PEP proportion in customer base
234    pub pep_rate: f64,
235    /// Enhanced due diligence trigger threshold
236    pub edd_threshold: u64,
237}
238
239impl Default for ComplianceConfig {
240    fn default() -> Self {
241        Self {
242            risk_appetite: RiskAppetite::Medium,
243            kyc_completeness: 0.95,
244            high_risk_tolerance: 0.05,
245            pep_rate: 0.01,
246            edd_threshold: 50_000,
247        }
248    }
249}
250
251/// Risk appetite level.
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
253#[serde(rename_all = "snake_case")]
254pub enum RiskAppetite {
255    /// Low risk tolerance
256    Low,
257    /// Medium risk tolerance
258    #[default]
259    Medium,
260    /// High risk tolerance
261    High,
262}
263
264impl RiskAppetite {
265    /// High-risk customer multiplier.
266    pub fn high_risk_multiplier(&self) -> f64 {
267        match self {
268            Self::Low => 0.5,
269            Self::Medium => 1.0,
270            Self::High => 2.0,
271        }
272    }
273}
274
275/// AML typology configuration.
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct TypologyConfig {
278    /// Overall suspicious activity rate (0.0-1.0)
279    pub suspicious_rate: f64,
280    /// Structuring typology rate
281    pub structuring_rate: f64,
282    /// Funnel account rate
283    pub funnel_rate: f64,
284    /// Layering chain rate
285    pub layering_rate: f64,
286    /// Money mule rate
287    pub mule_rate: f64,
288    /// Fraud rate (ATO, synthetic, etc.)
289    pub fraud_rate: f64,
290    /// Sophistication distribution
291    pub sophistication: SophisticationDistribution,
292    /// Base detectability (0.0-1.0)
293    pub detectability: f64,
294    /// Round-tripping rate
295    pub round_tripping_rate: f64,
296    /// Trade-based ML rate
297    pub trade_based_rate: f64,
298}
299
300impl Default for TypologyConfig {
301    fn default() -> Self {
302        Self {
303            suspicious_rate: 0.02,
304            structuring_rate: 0.004,
305            funnel_rate: 0.003,
306            layering_rate: 0.003,
307            mule_rate: 0.005,
308            fraud_rate: 0.005,
309            sophistication: SophisticationDistribution::default(),
310            detectability: 0.5,
311            round_tripping_rate: 0.001,
312            trade_based_rate: 0.001,
313        }
314    }
315}
316
317/// Sophistication level distribution.
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct SophisticationDistribution {
320    /// Basic sophistication weight
321    pub basic: f64,
322    /// Standard sophistication weight
323    pub standard: f64,
324    /// Professional sophistication weight
325    pub professional: f64,
326    /// Advanced sophistication weight
327    pub advanced: f64,
328}
329
330impl Default for SophisticationDistribution {
331    fn default() -> Self {
332        Self {
333            basic: 0.4,
334            standard: 0.35,
335            professional: 0.2,
336            advanced: 0.05,
337        }
338    }
339}
340
341/// Spoofing (adversarial) configuration.
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct SpoofingConfig {
344    /// Enable spoofing mode
345    pub enabled: bool,
346    /// Spoofing intensity (0.0-1.0)
347    pub intensity: f64,
348    /// Spoof transaction timing
349    pub spoof_timing: bool,
350    /// Spoof transaction amounts
351    pub spoof_amounts: bool,
352    /// Spoof merchant selection
353    pub spoof_merchants: bool,
354    /// Spoof geographic patterns
355    pub spoof_geography: bool,
356    /// Add delays to reduce velocity detection
357    pub add_delays: bool,
358}
359
360impl Default for SpoofingConfig {
361    fn default() -> Self {
362        Self {
363            enabled: true,
364            intensity: 0.3,
365            spoof_timing: true,
366            spoof_amounts: true,
367            spoof_merchants: true,
368            spoof_geography: false,
369            add_delays: true,
370        }
371    }
372}
373
374/// Banking output configuration.
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct BankingOutputConfig {
377    /// Output directory (relative to main output)
378    pub directory: String,
379    /// Include customer master data
380    pub include_customers: bool,
381    /// Include account master data
382    pub include_accounts: bool,
383    /// Include transactions
384    pub include_transactions: bool,
385    /// Include counterparties
386    pub include_counterparties: bool,
387    /// Include beneficial ownership
388    pub include_beneficial_ownership: bool,
389    /// Include transaction labels
390    pub include_transaction_labels: bool,
391    /// Include entity labels
392    pub include_entity_labels: bool,
393    /// Include relationship labels
394    pub include_relationship_labels: bool,
395    /// Include case narratives
396    pub include_case_narratives: bool,
397    /// Export graph data
398    pub include_graph: bool,
399}
400
401impl Default for BankingOutputConfig {
402    fn default() -> Self {
403        Self {
404            directory: "banking".to_string(),
405            include_customers: true,
406            include_accounts: true,
407            include_transactions: true,
408            include_counterparties: true,
409            include_beneficial_ownership: true,
410            include_transaction_labels: true,
411            include_entity_labels: true,
412            include_relationship_labels: true,
413            include_case_narratives: true,
414            include_graph: true,
415        }
416    }
417}
418
419#[cfg(test)]
420#[allow(clippy::unwrap_used)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_default_config() {
426        let config = BankingConfig::default();
427        assert!(config.enabled);
428        assert!(config.validate().is_ok());
429    }
430
431    #[test]
432    fn test_small_config() {
433        let config = BankingConfig::small();
434        assert_eq!(config.population.retail_customers, 100);
435        assert!(config.validate().is_ok());
436    }
437
438    #[test]
439    fn test_validation_empty_population() {
440        let config = BankingConfig {
441            population: PopulationConfig {
442                retail_customers: 0,
443                business_customers: 0,
444                trusts: 0,
445                ..Default::default()
446            },
447            ..Default::default()
448        };
449        assert!(config.validate().is_err());
450    }
451
452    #[test]
453    fn test_persona_weights() {
454        let config = BankingConfig::default();
455        let sum: f64 = config.population.retail_persona_weights.values().sum();
456        assert!((sum - 1.0).abs() < 0.01);
457    }
458}