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 {}",
107                retail_sum
108            ));
109        }
110
111        // Validate typology rates
112        let total_suspicious = self.typologies.structuring_rate
113            + self.typologies.funnel_rate
114            + self.typologies.layering_rate
115            + self.typologies.mule_rate
116            + self.typologies.fraud_rate;
117        if total_suspicious > self.typologies.suspicious_rate + 0.001 {
118            errors.push(format!(
119                "Sum of typology rates ({}) exceeds suspicious_rate ({})",
120                total_suspicious, self.typologies.suspicious_rate
121            ));
122        }
123
124        // Validate spoofing intensity
125        if self.spoofing.intensity < 0.0 || self.spoofing.intensity > 1.0 {
126            errors.push("Spoofing intensity must be between 0.0 and 1.0".to_string());
127        }
128
129        if errors.is_empty() {
130            Ok(())
131        } else {
132            Err(errors)
133        }
134    }
135}
136
137/// Population configuration.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct PopulationConfig {
140    /// Number of retail customers
141    pub retail_customers: u32,
142    /// Retail persona weight distribution
143    pub retail_persona_weights: HashMap<String, f64>,
144    /// Number of business customers
145    pub business_customers: u32,
146    /// Business persona weight distribution
147    pub business_persona_weights: HashMap<String, f64>,
148    /// Number of trust customers
149    pub trusts: u32,
150    /// Household formation rate (proportion of retail in households)
151    pub household_rate: f64,
152    /// Average household size
153    pub avg_household_size: f64,
154    /// Simulation period in months
155    pub period_months: u32,
156    /// Simulation start date (YYYY-MM-DD)
157    pub start_date: String,
158}
159
160impl Default for PopulationConfig {
161    fn default() -> Self {
162        let mut retail_weights = HashMap::new();
163        retail_weights.insert("student".to_string(), 0.15);
164        retail_weights.insert("early_career".to_string(), 0.25);
165        retail_weights.insert("mid_career".to_string(), 0.30);
166        retail_weights.insert("retiree".to_string(), 0.15);
167        retail_weights.insert("high_net_worth".to_string(), 0.05);
168        retail_weights.insert("gig_worker".to_string(), 0.10);
169
170        let mut business_weights = HashMap::new();
171        business_weights.insert("small_business".to_string(), 0.50);
172        business_weights.insert("mid_market".to_string(), 0.25);
173        business_weights.insert("enterprise".to_string(), 0.05);
174        business_weights.insert("cash_intensive".to_string(), 0.10);
175        business_weights.insert("import_export".to_string(), 0.05);
176        business_weights.insert("professional_services".to_string(), 0.05);
177
178        Self {
179            retail_customers: 10_000,
180            retail_persona_weights: retail_weights,
181            business_customers: 1_000,
182            business_persona_weights: business_weights,
183            trusts: 100,
184            household_rate: 0.4,
185            avg_household_size: 2.3,
186            period_months: 12,
187            start_date: "2024-01-01".to_string(),
188        }
189    }
190}
191
192/// Product configuration.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ProductConfig {
195    /// Cash transaction intensity (0.0-1.0)
196    pub cash_intensity: f64,
197    /// Cross-border transaction rate (0.0-1.0)
198    pub cross_border_rate: f64,
199    /// Card vs transfer ratio for payments
200    pub card_vs_transfer: f64,
201    /// Average accounts per retail customer
202    pub avg_accounts_retail: f64,
203    /// Average accounts per business customer
204    pub avg_accounts_business: f64,
205    /// Proportion of customers with debit cards
206    pub debit_card_rate: f64,
207    /// Proportion of customers with international capability
208    pub international_rate: f64,
209}
210
211impl Default for ProductConfig {
212    fn default() -> Self {
213        Self {
214            cash_intensity: 0.15,
215            cross_border_rate: 0.05,
216            card_vs_transfer: 0.6,
217            avg_accounts_retail: 1.5,
218            avg_accounts_business: 2.5,
219            debit_card_rate: 0.85,
220            international_rate: 0.10,
221        }
222    }
223}
224
225/// Compliance configuration.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ComplianceConfig {
228    /// Risk appetite (low, medium, high)
229    pub risk_appetite: RiskAppetite,
230    /// KYC completeness rate (0.0-1.0)
231    pub kyc_completeness: f64,
232    /// Proportion of high-risk customers accepted
233    pub high_risk_tolerance: f64,
234    /// PEP proportion in customer base
235    pub pep_rate: f64,
236    /// Enhanced due diligence trigger threshold
237    pub edd_threshold: u64,
238}
239
240impl Default for ComplianceConfig {
241    fn default() -> Self {
242        Self {
243            risk_appetite: RiskAppetite::Medium,
244            kyc_completeness: 0.95,
245            high_risk_tolerance: 0.05,
246            pep_rate: 0.01,
247            edd_threshold: 50_000,
248        }
249    }
250}
251
252/// Risk appetite level.
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
254#[serde(rename_all = "snake_case")]
255pub enum RiskAppetite {
256    /// Low risk tolerance
257    Low,
258    /// Medium risk tolerance
259    #[default]
260    Medium,
261    /// High risk tolerance
262    High,
263}
264
265impl RiskAppetite {
266    /// High-risk customer multiplier.
267    pub fn high_risk_multiplier(&self) -> f64 {
268        match self {
269            Self::Low => 0.5,
270            Self::Medium => 1.0,
271            Self::High => 2.0,
272        }
273    }
274}
275
276/// AML typology configuration.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct TypologyConfig {
279    /// Overall suspicious activity rate (0.0-1.0)
280    pub suspicious_rate: f64,
281    /// Structuring typology rate
282    pub structuring_rate: f64,
283    /// Funnel account rate
284    pub funnel_rate: f64,
285    /// Layering chain rate
286    pub layering_rate: f64,
287    /// Money mule rate
288    pub mule_rate: f64,
289    /// Fraud rate (ATO, synthetic, etc.)
290    pub fraud_rate: f64,
291    /// Sophistication distribution
292    pub sophistication: SophisticationDistribution,
293    /// Base detectability (0.0-1.0)
294    pub detectability: f64,
295    /// Round-tripping rate
296    pub round_tripping_rate: f64,
297    /// Trade-based ML rate
298    pub trade_based_rate: f64,
299}
300
301impl Default for TypologyConfig {
302    fn default() -> Self {
303        Self {
304            suspicious_rate: 0.02,
305            structuring_rate: 0.004,
306            funnel_rate: 0.003,
307            layering_rate: 0.003,
308            mule_rate: 0.005,
309            fraud_rate: 0.005,
310            sophistication: SophisticationDistribution::default(),
311            detectability: 0.5,
312            round_tripping_rate: 0.001,
313            trade_based_rate: 0.001,
314        }
315    }
316}
317
318/// Sophistication level distribution.
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct SophisticationDistribution {
321    /// Basic sophistication weight
322    pub basic: f64,
323    /// Standard sophistication weight
324    pub standard: f64,
325    /// Professional sophistication weight
326    pub professional: f64,
327    /// Advanced sophistication weight
328    pub advanced: f64,
329}
330
331impl Default for SophisticationDistribution {
332    fn default() -> Self {
333        Self {
334            basic: 0.4,
335            standard: 0.35,
336            professional: 0.2,
337            advanced: 0.05,
338        }
339    }
340}
341
342/// Spoofing (adversarial) configuration.
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct SpoofingConfig {
345    /// Enable spoofing mode
346    pub enabled: bool,
347    /// Spoofing intensity (0.0-1.0)
348    pub intensity: f64,
349    /// Spoof transaction timing
350    pub spoof_timing: bool,
351    /// Spoof transaction amounts
352    pub spoof_amounts: bool,
353    /// Spoof merchant selection
354    pub spoof_merchants: bool,
355    /// Spoof geographic patterns
356    pub spoof_geography: bool,
357    /// Add delays to reduce velocity detection
358    pub add_delays: bool,
359}
360
361impl Default for SpoofingConfig {
362    fn default() -> Self {
363        Self {
364            enabled: true,
365            intensity: 0.3,
366            spoof_timing: true,
367            spoof_amounts: true,
368            spoof_merchants: true,
369            spoof_geography: false,
370            add_delays: true,
371        }
372    }
373}
374
375/// Banking output configuration.
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct BankingOutputConfig {
378    /// Output directory (relative to main output)
379    pub directory: String,
380    /// Include customer master data
381    pub include_customers: bool,
382    /// Include account master data
383    pub include_accounts: bool,
384    /// Include transactions
385    pub include_transactions: bool,
386    /// Include counterparties
387    pub include_counterparties: bool,
388    /// Include beneficial ownership
389    pub include_beneficial_ownership: bool,
390    /// Include transaction labels
391    pub include_transaction_labels: bool,
392    /// Include entity labels
393    pub include_entity_labels: bool,
394    /// Include relationship labels
395    pub include_relationship_labels: bool,
396    /// Include case narratives
397    pub include_case_narratives: bool,
398    /// Export graph data
399    pub include_graph: bool,
400}
401
402impl Default for BankingOutputConfig {
403    fn default() -> Self {
404        Self {
405            directory: "banking".to_string(),
406            include_customers: true,
407            include_accounts: true,
408            include_transactions: true,
409            include_counterparties: true,
410            include_beneficial_ownership: true,
411            include_transaction_labels: true,
412            include_entity_labels: true,
413            include_relationship_labels: true,
414            include_case_narratives: true,
415            include_graph: true,
416        }
417    }
418}
419
420#[cfg(test)]
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}