Skip to main content

datasynth_core/distributions/
seasonality.rs

1//! Industry-specific seasonality patterns for transaction generation.
2//!
3//! Defines seasonal events and multipliers for different industries,
4//! enabling realistic volume variations throughout the year.
5
6use chrono::{Datelike, NaiveDate};
7use serde::{Deserialize, Serialize};
8
9use crate::models::IndustrySector;
10
11/// A seasonal event that affects transaction volume.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SeasonalEvent {
14    /// Name of the event.
15    pub name: String,
16    /// Start month (1-12).
17    pub start_month: u8,
18    /// Start day of the month.
19    pub start_day: u8,
20    /// End month (1-12).
21    pub end_month: u8,
22    /// End day of the month.
23    pub end_day: u8,
24    /// Volume multiplier during this event.
25    pub multiplier: f64,
26    /// Priority for overlapping events (higher = takes precedence).
27    pub priority: u8,
28}
29
30impl SeasonalEvent {
31    /// Create a new seasonal event.
32    pub fn new(
33        name: impl Into<String>,
34        start_month: u8,
35        start_day: u8,
36        end_month: u8,
37        end_day: u8,
38        multiplier: f64,
39    ) -> Self {
40        Self {
41            name: name.into(),
42            start_month,
43            start_day,
44            end_month,
45            end_day,
46            multiplier,
47            priority: 0,
48        }
49    }
50
51    /// Set the priority for this event.
52    pub fn with_priority(mut self, priority: u8) -> Self {
53        self.priority = priority;
54        self
55    }
56
57    /// Check if this event is active on a given date.
58    pub fn is_active(&self, date: NaiveDate) -> bool {
59        let month = date.month() as u8;
60        let day = date.day() as u8;
61
62        // Handle events that span year boundary (e.g., Dec 20 - Jan 5)
63        if self.start_month > self.end_month {
64            // Event spans year boundary
65            if month > self.start_month || month < self.end_month {
66                return true;
67            }
68            if month == self.start_month && day >= self.start_day {
69                return true;
70            }
71            if month == self.end_month && day <= self.end_day {
72                return true;
73            }
74            return false;
75        }
76
77        // Normal case: event within same year
78        if month < self.start_month || month > self.end_month {
79            return false;
80        }
81
82        if month == self.start_month && day < self.start_day {
83            return false;
84        }
85
86        if month == self.end_month && day > self.end_day {
87            return false;
88        }
89
90        true
91    }
92}
93
94/// Industry-specific seasonality patterns.
95#[derive(Debug, Clone)]
96pub struct IndustrySeasonality {
97    /// The industry sector.
98    pub industry: IndustrySector,
99    /// Seasonal events for this industry.
100    pub events: Vec<SeasonalEvent>,
101}
102
103impl IndustrySeasonality {
104    /// Create a new industry seasonality definition.
105    pub fn new(industry: IndustrySector) -> Self {
106        Self {
107            industry,
108            events: Vec::new(),
109        }
110    }
111
112    /// Create seasonality patterns for a specific industry.
113    pub fn for_industry(industry: IndustrySector) -> Self {
114        match industry {
115            IndustrySector::Retail => Self::retail(),
116            IndustrySector::Manufacturing => Self::manufacturing(),
117            IndustrySector::FinancialServices => Self::financial_services(),
118            IndustrySector::Healthcare => Self::healthcare(),
119            IndustrySector::Technology => Self::technology(),
120            IndustrySector::ProfessionalServices => Self::professional_services(),
121            IndustrySector::Energy => Self::energy(),
122            IndustrySector::Transportation => Self::transportation(),
123            IndustrySector::RealEstate => Self::real_estate(),
124            IndustrySector::Telecommunications => Self::telecommunications(),
125        }
126    }
127
128    /// Get the volume multiplier for a specific date.
129    ///
130    /// If multiple events are active, returns the one with highest priority.
131    /// If no events are active, returns 1.0.
132    pub fn get_multiplier(&self, date: NaiveDate) -> f64 {
133        let active_events: Vec<&SeasonalEvent> =
134            self.events.iter().filter(|e| e.is_active(date)).collect();
135
136        if active_events.is_empty() {
137            return 1.0;
138        }
139
140        // Return the multiplier from the highest priority active event
141        active_events
142            .into_iter()
143            .max_by_key(|e| e.priority)
144            .map(|e| e.multiplier)
145            .unwrap_or(1.0)
146    }
147
148    /// Add a seasonal event.
149    pub fn add_event(&mut self, event: SeasonalEvent) {
150        self.events.push(event);
151    }
152
153    /// Retail industry seasonality patterns.
154    fn retail() -> Self {
155        let mut s = Self::new(IndustrySector::Retail);
156
157        // Black Friday / Cyber Monday (Nov 20-30): 8x
158        s.add_event(
159            SeasonalEvent::new("Black Friday/Cyber Monday", 11, 20, 11, 30, 8.0).with_priority(10),
160        );
161
162        // Christmas rush (Dec 15-24): 6x
163        s.add_event(SeasonalEvent::new("Christmas Rush", 12, 15, 12, 24, 6.0).with_priority(9));
164
165        // Post-holiday returns (Jan 1-15): 3x
166        s.add_event(SeasonalEvent::new("Post-Holiday Returns", 1, 1, 1, 15, 3.0).with_priority(7));
167
168        // Back-to-school (Aug 1-31): 2x
169        s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 8, 31, 2.0).with_priority(5));
170
171        // Valentine's Day surge (Feb 7-14): 1.8x
172        s.add_event(SeasonalEvent::new("Valentine's Day", 2, 7, 2, 14, 1.8).with_priority(4));
173
174        // Easter season (late March - mid April): 1.5x
175        s.add_event(SeasonalEvent::new("Easter Season", 3, 20, 4, 15, 1.5).with_priority(3));
176
177        // Summer slowdown (Jun-Jul): 0.7x
178        s.add_event(SeasonalEvent::new("Summer Slowdown", 6, 1, 7, 31, 0.7).with_priority(2));
179
180        s
181    }
182
183    /// Manufacturing industry seasonality patterns.
184    fn manufacturing() -> Self {
185        let mut s = Self::new(IndustrySector::Manufacturing);
186
187        // Year-end close (Dec 20-31): 4x
188        s.add_event(SeasonalEvent::new("Year-End Close", 12, 20, 12, 31, 4.0).with_priority(10));
189
190        // Q4 inventory buildup (Oct-Nov): 2x
191        s.add_event(
192            SeasonalEvent::new("Q4 Inventory Buildup", 10, 1, 11, 30, 2.0).with_priority(6),
193        );
194
195        // Model year transitions (Sep): 1.5x
196        s.add_event(SeasonalEvent::new("Model Year Transition", 9, 1, 9, 30, 1.5).with_priority(5));
197
198        // Spring production ramp (Mar-Apr): 1.3x
199        s.add_event(
200            SeasonalEvent::new("Spring Production Ramp", 3, 1, 4, 30, 1.3).with_priority(3),
201        );
202
203        // Summer slowdown/maintenance (Jul): 0.6x
204        s.add_event(SeasonalEvent::new("Summer Shutdown", 7, 1, 7, 31, 0.6).with_priority(4));
205
206        // Holiday shutdown (Dec 24-26): 0.2x
207        s.add_event(SeasonalEvent::new("Holiday Shutdown", 12, 24, 12, 26, 0.2).with_priority(11));
208
209        s
210    }
211
212    /// Financial services industry seasonality patterns.
213    fn financial_services() -> Self {
214        let mut s = Self::new(IndustrySector::FinancialServices);
215
216        // Year-end (Dec 15-31): 8x
217        s.add_event(SeasonalEvent::new("Year-End", 12, 15, 12, 31, 8.0).with_priority(10));
218
219        // Q1 end (Mar 26-31): 5x
220        s.add_event(SeasonalEvent::new("Q1 Close", 3, 26, 3, 31, 5.0).with_priority(9));
221
222        // Q2 end (Jun 25-30): 5x
223        s.add_event(SeasonalEvent::new("Q2 Close", 6, 25, 6, 30, 5.0).with_priority(9));
224
225        // Q3 end (Sep 25-30): 5x
226        s.add_event(SeasonalEvent::new("Q3 Close", 9, 25, 9, 30, 5.0).with_priority(9));
227
228        // Tax deadline (Apr 10-20): 3x
229        s.add_event(SeasonalEvent::new("Tax Deadline", 4, 10, 4, 20, 3.0).with_priority(7));
230
231        // Annual audit season (Jan 15 - Feb 28): 2.5x
232        s.add_event(SeasonalEvent::new("Audit Season", 1, 15, 2, 28, 2.5).with_priority(6));
233
234        // SEC/Regulatory filing periods (Feb 1-28): 2x
235        s.add_event(SeasonalEvent::new("Regulatory Filing", 2, 1, 2, 28, 2.0).with_priority(5));
236
237        s
238    }
239
240    /// Healthcare industry seasonality patterns.
241    fn healthcare() -> Self {
242        let mut s = Self::new(IndustrySector::Healthcare);
243
244        // Year-end (Dec 15-31): 3x
245        s.add_event(SeasonalEvent::new("Year-End", 12, 15, 12, 31, 3.0).with_priority(10));
246
247        // Insurance renewal/benefits enrollment (Jan 1-31): 2x
248        s.add_event(SeasonalEvent::new("Insurance Enrollment", 1, 1, 1, 31, 2.0).with_priority(8));
249
250        // Flu season (Oct-Feb): 1.5x
251        s.add_event(SeasonalEvent::new("Flu Season", 10, 1, 10, 31, 1.5).with_priority(4));
252        s.add_event(SeasonalEvent::new("Flu Season Extended", 11, 1, 2, 28, 1.5).with_priority(4));
253
254        // Open enrollment period (Nov): 1.8x
255        s.add_event(SeasonalEvent::new("Open Enrollment", 11, 1, 11, 30, 1.8).with_priority(6));
256
257        // Summer elective procedure slowdown (Jun-Aug): 0.8x
258        s.add_event(SeasonalEvent::new("Summer Slowdown", 6, 15, 8, 15, 0.8).with_priority(3));
259
260        s
261    }
262
263    /// Technology industry seasonality patterns.
264    fn technology() -> Self {
265        let mut s = Self::new(IndustrySector::Technology);
266
267        // Q4 enterprise deals (Dec): 4x
268        s.add_event(
269            SeasonalEvent::new("Q4 Enterprise Deals", 12, 1, 12, 31, 4.0).with_priority(10),
270        );
271
272        // Black Friday/holiday sales (Nov 15-30): 2x
273        s.add_event(SeasonalEvent::new("Holiday Sales", 11, 15, 11, 30, 2.0).with_priority(8));
274
275        // Back-to-school (Aug-Sep): 1.5x
276        s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 9, 15, 1.5).with_priority(5));
277
278        // Product launch seasons (Mar, Sep): 1.8x
279        s.add_event(SeasonalEvent::new("Spring Launches", 3, 1, 3, 31, 1.8).with_priority(6));
280        s.add_event(SeasonalEvent::new("Fall Launches", 9, 1, 9, 30, 1.8).with_priority(6));
281
282        // Summer slowdown (Jul-Aug): 0.7x
283        s.add_event(SeasonalEvent::new("Summer Slowdown", 7, 1, 8, 15, 0.7).with_priority(3));
284
285        s
286    }
287
288    /// Professional services industry seasonality patterns.
289    fn professional_services() -> Self {
290        let mut s = Self::new(IndustrySector::ProfessionalServices);
291
292        // Year-end (Dec): 3x
293        s.add_event(SeasonalEvent::new("Year-End", 12, 10, 12, 31, 3.0).with_priority(10));
294
295        // Tax season (Feb-Apr): 2.5x for accounting firms
296        s.add_event(SeasonalEvent::new("Tax Season", 2, 1, 4, 15, 2.5).with_priority(8));
297
298        // Budget season (Oct-Nov): 1.8x
299        s.add_event(SeasonalEvent::new("Budget Season", 10, 1, 11, 30, 1.8).with_priority(6));
300
301        // Summer slowdown (Jul-Aug): 0.75x
302        s.add_event(SeasonalEvent::new("Summer Slowdown", 7, 1, 8, 31, 0.75).with_priority(3));
303
304        // Holiday period (Dec 23-Jan 2): 0.3x
305        s.add_event(SeasonalEvent::new("Holiday Period", 12, 23, 12, 26, 0.3).with_priority(11));
306
307        s
308    }
309
310    /// Energy industry seasonality patterns.
311    fn energy() -> Self {
312        let mut s = Self::new(IndustrySector::Energy);
313
314        // Winter heating season (Nov-Feb): 1.8x
315        s.add_event(
316            SeasonalEvent::new("Winter Heating Season", 11, 1, 2, 28, 1.8).with_priority(6),
317        );
318
319        // Summer cooling season (Jun-Aug): 1.5x
320        s.add_event(SeasonalEvent::new("Summer Cooling Season", 6, 1, 8, 31, 1.5).with_priority(5));
321
322        // Year-end reconciliation (Dec 15-31): 3x
323        s.add_event(SeasonalEvent::new("Year-End", 12, 15, 12, 31, 3.0).with_priority(10));
324
325        // Spring/Fall shoulder seasons: 0.8x
326        s.add_event(SeasonalEvent::new("Spring Shoulder", 3, 15, 5, 15, 0.8).with_priority(3));
327        s.add_event(SeasonalEvent::new("Fall Shoulder", 9, 15, 10, 15, 0.8).with_priority(3));
328
329        s
330    }
331
332    /// Transportation industry seasonality patterns.
333    fn transportation() -> Self {
334        let mut s = Self::new(IndustrySector::Transportation);
335
336        // Holiday shipping season (Nov 15 - Dec 24): 4x
337        s.add_event(SeasonalEvent::new("Holiday Shipping", 11, 15, 12, 24, 4.0).with_priority(10));
338
339        // Back-to-school (Aug): 1.5x
340        s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 8, 31, 1.5).with_priority(5));
341
342        // Summer travel season (Jun-Aug): 1.3x for passenger
343        s.add_event(SeasonalEvent::new("Summer Travel", 6, 15, 8, 15, 1.3).with_priority(4));
344
345        // Post-holiday slowdown (Jan): 0.7x
346        s.add_event(SeasonalEvent::new("January Slowdown", 1, 5, 1, 31, 0.7).with_priority(3));
347
348        s
349    }
350
351    /// Real estate industry seasonality patterns.
352    fn real_estate() -> Self {
353        let mut s = Self::new(IndustrySector::RealEstate);
354
355        // Spring buying season (Mar-Jun): 2x
356        s.add_event(SeasonalEvent::new("Spring Buying Season", 3, 1, 6, 30, 2.0).with_priority(6));
357
358        // Year-end closings (Dec): 2.5x
359        s.add_event(SeasonalEvent::new("Year-End Closings", 12, 1, 12, 31, 2.5).with_priority(8));
360
361        // Summer moving season (Jun-Aug): 1.8x
362        s.add_event(SeasonalEvent::new("Summer Moving", 6, 1, 8, 31, 1.8).with_priority(5));
363
364        // Winter slowdown (Jan-Feb): 0.6x
365        s.add_event(SeasonalEvent::new("Winter Slowdown", 1, 1, 2, 28, 0.6).with_priority(3));
366
367        s
368    }
369
370    /// Telecommunications industry seasonality patterns.
371    fn telecommunications() -> Self {
372        let mut s = Self::new(IndustrySector::Telecommunications);
373
374        // Holiday activations (Nov-Dec): 2x
375        s.add_event(
376            SeasonalEvent::new("Holiday Activations", 11, 15, 12, 31, 2.0).with_priority(8),
377        );
378
379        // Back-to-school (Aug-Sep): 1.5x
380        s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 9, 15, 1.5).with_priority(5));
381
382        // Year-end billing (Dec 15-31): 1.8x
383        s.add_event(SeasonalEvent::new("Year-End Billing", 12, 15, 12, 31, 1.8).with_priority(7));
384
385        // Q1 slowdown (Jan-Feb): 0.8x
386        s.add_event(SeasonalEvent::new("Q1 Slowdown", 1, 15, 2, 28, 0.8).with_priority(3));
387
388        s
389    }
390}
391
392/// Custom seasonal event configuration for YAML/JSON input.
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct CustomSeasonalEventConfig {
395    /// Event name.
396    pub name: String,
397    /// Start month (1-12).
398    pub start_month: u8,
399    /// Start day of month.
400    pub start_day: u8,
401    /// End month (1-12).
402    pub end_month: u8,
403    /// End day of month.
404    pub end_day: u8,
405    /// Volume multiplier.
406    pub multiplier: f64,
407    /// Priority (optional, defaults to 5).
408    #[serde(default = "default_priority")]
409    pub priority: u8,
410}
411
412fn default_priority() -> u8 {
413    5
414}
415
416impl From<CustomSeasonalEventConfig> for SeasonalEvent {
417    fn from(config: CustomSeasonalEventConfig) -> Self {
418        SeasonalEvent::new(
419            config.name,
420            config.start_month,
421            config.start_day,
422            config.end_month,
423            config.end_day,
424            config.multiplier,
425        )
426        .with_priority(config.priority)
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn test_seasonal_event_active() {
436        let event = SeasonalEvent::new("Test Event", 11, 20, 11, 30, 2.0);
437
438        // Active dates
439        assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 11, 20).unwrap()));
440        assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 11, 25).unwrap()));
441        assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 11, 30).unwrap()));
442
443        // Inactive dates
444        assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 11, 19).unwrap()));
445        assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 12, 1).unwrap()));
446        assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 10, 25).unwrap()));
447    }
448
449    #[test]
450    fn test_year_spanning_event() {
451        let event = SeasonalEvent::new("Holiday Period", 12, 20, 1, 5, 0.3);
452
453        // Active in December
454        assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 12, 20).unwrap()));
455        assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()));
456
457        // Active in January
458        assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()));
459        assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 1, 5).unwrap()));
460
461        // Inactive
462        assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 12, 19).unwrap()));
463        assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 1, 6).unwrap()));
464        assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()));
465    }
466
467    #[test]
468    fn test_retail_seasonality() {
469        let seasonality = IndustrySeasonality::for_industry(IndustrySector::Retail);
470
471        // Black Friday should be 8x
472        let black_friday = NaiveDate::from_ymd_opt(2024, 11, 25).unwrap();
473        assert!((seasonality.get_multiplier(black_friday) - 8.0).abs() < 0.01);
474
475        // Regular day should be 1x
476        let regular_day = NaiveDate::from_ymd_opt(2024, 5, 15).unwrap();
477        assert!((seasonality.get_multiplier(regular_day) - 1.0).abs() < 0.01);
478
479        // Summer slowdown should be 0.7x
480        let summer = NaiveDate::from_ymd_opt(2024, 7, 15).unwrap();
481        assert!((seasonality.get_multiplier(summer) - 0.7).abs() < 0.01);
482    }
483
484    #[test]
485    fn test_financial_services_seasonality() {
486        let seasonality = IndustrySeasonality::for_industry(IndustrySector::FinancialServices);
487
488        // Year-end should be 8x
489        let year_end = NaiveDate::from_ymd_opt(2024, 12, 20).unwrap();
490        assert!((seasonality.get_multiplier(year_end) - 8.0).abs() < 0.01);
491
492        // Quarter-end should be 5x
493        let q1_end = NaiveDate::from_ymd_opt(2024, 3, 28).unwrap();
494        assert!((seasonality.get_multiplier(q1_end) - 5.0).abs() < 0.01);
495    }
496
497    #[test]
498    fn test_priority_handling() {
499        let mut s = IndustrySeasonality::new(IndustrySector::Retail);
500
501        // Add two overlapping events
502        s.add_event(SeasonalEvent::new("Low Priority", 12, 1, 12, 31, 2.0).with_priority(1));
503        s.add_event(SeasonalEvent::new("High Priority", 12, 15, 12, 25, 5.0).with_priority(10));
504
505        // In overlap period, high priority should win
506        let overlap = NaiveDate::from_ymd_opt(2024, 12, 20).unwrap();
507        assert!((s.get_multiplier(overlap) - 5.0).abs() < 0.01);
508
509        // Outside overlap, low priority applies
510        let low_only = NaiveDate::from_ymd_opt(2024, 12, 5).unwrap();
511        assert!((s.get_multiplier(low_only) - 2.0).abs() < 0.01);
512    }
513
514    #[test]
515    fn test_all_industries_have_events() {
516        let industries = [
517            IndustrySector::Retail,
518            IndustrySector::Manufacturing,
519            IndustrySector::FinancialServices,
520            IndustrySector::Healthcare,
521            IndustrySector::Technology,
522            IndustrySector::ProfessionalServices,
523            IndustrySector::Energy,
524            IndustrySector::Transportation,
525            IndustrySector::RealEstate,
526            IndustrySector::Telecommunications,
527        ];
528
529        for industry in industries {
530            let s = IndustrySeasonality::for_industry(industry);
531            assert!(
532                !s.events.is_empty(),
533                "Industry {:?} should have seasonal events",
534                industry
535            );
536        }
537    }
538}