Skip to main content

datasynth_core/distributions/
market_drift.rs

1//! Market drift models for economic and industry cycle simulation.
2//!
3//! Provides comprehensive market drift modeling including:
4//! - Economic cycles (sinusoidal, asymmetric, mean-reverting)
5//! - Industry-specific cycles
6//! - Commodity price drift
7//! - Price shock events
8
9use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14/// Main market drift model.
15#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct MarketDriftModel {
17    /// Economic cycle model.
18    #[serde(default)]
19    pub economic_cycle: EconomicCycleModel,
20    /// Industry-specific cycles.
21    #[serde(default)]
22    pub industry_cycles: HashMap<MarketIndustryType, IndustryCycleConfig>,
23    /// Commodity drift configuration.
24    #[serde(default)]
25    pub commodity_drift: CommodityDriftConfig,
26    /// Price shock events.
27    #[serde(default)]
28    pub price_shocks: Vec<PriceShockEvent>,
29}
30
31impl MarketDriftModel {
32    /// Compute market effects for a given period.
33    pub fn compute_effects(&self, period: u32, rng: &mut ChaCha8Rng) -> MarketEffects {
34        let mut effects = MarketEffects::neutral();
35
36        // Economic cycle
37        if self.economic_cycle.enabled {
38            let cycle_effect = self.economic_cycle.effect_at_period(period);
39            effects.economic_cycle_factor = cycle_effect.cycle_factor;
40            effects.is_recession = cycle_effect.is_recession;
41        }
42
43        // Commodity effects
44        if self.commodity_drift.enabled {
45            effects.commodity_effects = self.commodity_drift.effects_at_period(period, rng);
46        }
47
48        // Price shock effects
49        for shock in &self.price_shocks {
50            if shock.is_active_at_period(period) {
51                effects.apply_shock(shock, period);
52            }
53        }
54
55        effects
56    }
57}
58
59/// Industry type for industry-specific market cycles.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum MarketIndustryType {
63    /// Technology sector.
64    Technology,
65    /// Retail sector.
66    Retail,
67    /// Manufacturing sector.
68    Manufacturing,
69    /// Financial services sector.
70    FinancialServices,
71    /// Healthcare sector.
72    Healthcare,
73    /// Energy sector.
74    Energy,
75    /// Real estate sector.
76    RealEstate,
77}
78
79impl MarketIndustryType {
80    /// Get the typical cycle period for this industry.
81    pub fn typical_cycle_months(&self) -> u32 {
82        match self {
83            Self::Technology => 36,
84            Self::Retail => 12,
85            Self::Manufacturing => 48,
86            Self::FinancialServices => 60,
87            Self::Healthcare => 36,
88            Self::Energy => 48,
89            Self::RealEstate => 84,
90        }
91    }
92
93    /// Get the typical cycle amplitude for this industry.
94    pub fn typical_amplitude(&self) -> f64 {
95        match self {
96            Self::Technology => 0.25,
97            Self::Retail => 0.35,
98            Self::Manufacturing => 0.20,
99            Self::FinancialServices => 0.15,
100            Self::Healthcare => 0.10,
101            Self::Energy => 0.30,
102            Self::RealEstate => 0.20,
103        }
104    }
105}
106
107/// Market effects computed for a period.
108#[derive(Debug, Clone, Default)]
109pub struct MarketEffects {
110    /// Economic cycle factor (1.0 = neutral).
111    pub economic_cycle_factor: f64,
112    /// Whether in recession.
113    pub is_recession: bool,
114    /// Commodity price effects.
115    pub commodity_effects: CommodityEffects,
116    /// Active price shocks.
117    pub active_shocks: Vec<String>,
118    /// Price shock multiplier.
119    pub shock_multiplier: f64,
120}
121
122impl MarketEffects {
123    /// Create neutral effects.
124    pub fn neutral() -> Self {
125        Self {
126            economic_cycle_factor: 1.0,
127            is_recession: false,
128            commodity_effects: CommodityEffects::default(),
129            active_shocks: Vec::new(),
130            shock_multiplier: 1.0,
131        }
132    }
133
134    /// Apply a price shock.
135    fn apply_shock(&mut self, shock: &PriceShockEvent, period: u32) {
136        self.active_shocks.push(shock.shock_id.clone());
137        let progress = shock.progress_at_period(period);
138        let shock_factor = 1.0
139            + shock.price_increase_range.0
140            + (shock.price_increase_range.1 - shock.price_increase_range.0) * progress;
141        self.shock_multiplier *= shock_factor;
142    }
143}
144
145// =============================================================================
146// Economic Cycle Model
147// =============================================================================
148
149/// Economic cycle model.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct EconomicCycleModel {
152    /// Enable economic cycle.
153    #[serde(default)]
154    pub enabled: bool,
155    /// Cycle type.
156    #[serde(default)]
157    pub cycle_type: CycleType,
158    /// Cycle period in months.
159    #[serde(default = "default_cycle_period")]
160    pub period_months: u32,
161    /// Amplitude of cycle effect.
162    #[serde(default = "default_amplitude")]
163    pub amplitude: f64,
164    /// Phase offset in months.
165    #[serde(default)]
166    pub phase_offset: u32,
167    /// Recession configuration.
168    #[serde(default)]
169    pub recession: RecessionConfig,
170}
171
172fn default_cycle_period() -> u32 {
173    48
174}
175
176fn default_amplitude() -> f64 {
177    0.15
178}
179
180impl Default for EconomicCycleModel {
181    fn default() -> Self {
182        Self {
183            enabled: false,
184            cycle_type: CycleType::Sinusoidal,
185            period_months: 48,
186            amplitude: 0.15,
187            phase_offset: 0,
188            recession: RecessionConfig::default(),
189        }
190    }
191}
192
193impl EconomicCycleModel {
194    /// Calculate the cycle effect at a given period.
195    pub fn effect_at_period(&self, period: u32) -> CycleEffect {
196        if !self.enabled {
197            return CycleEffect {
198                cycle_factor: 1.0,
199                is_recession: false,
200                cycle_position: 0.0,
201            };
202        }
203
204        let adjusted_period = period + self.phase_offset;
205        let cycle_position =
206            (adjusted_period % self.period_months) as f64 / self.period_months as f64;
207
208        let base_factor = match self.cycle_type {
209            CycleType::Sinusoidal => {
210                let radians = cycle_position * 2.0 * std::f64::consts::PI;
211                1.0 + self.amplitude * radians.sin()
212            }
213            CycleType::Asymmetric => {
214                // Faster decline, slower recovery
215                let radians = cycle_position * 2.0 * std::f64::consts::PI;
216                let sine_value = radians.sin();
217                if sine_value < 0.0 {
218                    1.0 + self.amplitude * sine_value * 1.3 // Deeper troughs
219                } else {
220                    1.0 + self.amplitude * sine_value * 0.7 // Shallower peaks
221                }
222            }
223            CycleType::MeanReverting => {
224                // Oscillates with dampening
225                let radians = cycle_position * 2.0 * std::f64::consts::PI;
226                let dampening = (-cycle_position * 0.5).exp();
227                1.0 + self.amplitude * radians.sin() * dampening
228            }
229        };
230
231        // Check for recession
232        let is_recession = self.recession.enabled && self.recession.is_recession_at(period);
233        let recession_factor = if is_recession {
234            match self.recession.severity {
235                RecessionSeverity::Mild => 0.90,
236                RecessionSeverity::Moderate => 0.80,
237                RecessionSeverity::Severe => 0.65,
238            }
239        } else {
240            1.0
241        };
242
243        CycleEffect {
244            cycle_factor: base_factor * recession_factor,
245            is_recession,
246            cycle_position,
247        }
248    }
249}
250
251/// Cycle type.
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
253#[serde(rename_all = "snake_case")]
254pub enum CycleType {
255    /// Simple sinusoidal cycle.
256    #[default]
257    Sinusoidal,
258    /// Asymmetric cycle (faster decline, slower recovery).
259    Asymmetric,
260    /// Mean-reverting with dampening.
261    MeanReverting,
262}
263
264/// Cycle effect at a point in time.
265#[derive(Debug, Clone)]
266pub struct CycleEffect {
267    /// Cycle factor (multiplier).
268    pub cycle_factor: f64,
269    /// Whether in recession.
270    pub is_recession: bool,
271    /// Position in cycle (0.0 to 1.0).
272    pub cycle_position: f64,
273}
274
275/// Recession configuration.
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct RecessionConfig {
278    /// Enable recession simulation.
279    #[serde(default)]
280    pub enabled: bool,
281    /// Probability of recession per year.
282    #[serde(default = "default_recession_prob")]
283    pub probability_per_year: f64,
284    /// Recession onset type.
285    #[serde(default)]
286    pub onset: RecessionOnset,
287    /// Duration range in months.
288    #[serde(default = "default_recession_duration")]
289    pub duration_months: (u32, u32),
290    /// Recession severity.
291    #[serde(default)]
292    pub severity: RecessionSeverity,
293    /// Specific recession periods (optional, for deterministic simulation).
294    #[serde(default)]
295    pub recession_periods: Vec<(u32, u32)>, // (start_month, duration)
296}
297
298fn default_recession_prob() -> f64 {
299    0.10
300}
301
302fn default_recession_duration() -> (u32, u32) {
303    (12, 24)
304}
305
306impl Default for RecessionConfig {
307    fn default() -> Self {
308        Self {
309            enabled: false,
310            probability_per_year: 0.10,
311            onset: RecessionOnset::Gradual,
312            duration_months: (12, 24),
313            severity: RecessionSeverity::Moderate,
314            recession_periods: Vec::new(),
315        }
316    }
317}
318
319impl RecessionConfig {
320    /// Check if a given period is in recession.
321    pub fn is_recession_at(&self, period: u32) -> bool {
322        for (start, duration) in &self.recession_periods {
323            if period >= *start && period < start + duration {
324                return true;
325            }
326        }
327        false
328    }
329}
330
331/// Recession onset type.
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
333#[serde(rename_all = "snake_case")]
334pub enum RecessionOnset {
335    /// Gradual onset over several months.
336    #[default]
337    Gradual,
338    /// Sudden onset (e.g., crisis).
339    Sudden,
340}
341
342/// Recession severity level.
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
344#[serde(rename_all = "snake_case")]
345pub enum RecessionSeverity {
346    /// Mild recession (10% reduction).
347    Mild,
348    /// Moderate recession (20% reduction).
349    #[default]
350    Moderate,
351    /// Severe recession (35% reduction).
352    Severe,
353}
354
355// =============================================================================
356// Industry Cycles
357// =============================================================================
358
359/// Industry-specific cycle configuration.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct IndustryCycleConfig {
362    /// Cycle period in months.
363    #[serde(default = "default_industry_period")]
364    pub period_months: u32,
365    /// Cycle amplitude.
366    #[serde(default = "default_industry_amplitude")]
367    pub amplitude: f64,
368    /// Phase offset relative to economic cycle.
369    #[serde(default)]
370    pub phase_offset: u32,
371    /// Correlation with general economy.
372    #[serde(default = "default_correlation")]
373    pub economic_correlation: f64,
374}
375
376fn default_industry_period() -> u32 {
377    36
378}
379
380fn default_industry_amplitude() -> f64 {
381    0.20
382}
383
384fn default_correlation() -> f64 {
385    0.7
386}
387
388impl Default for IndustryCycleConfig {
389    fn default() -> Self {
390        Self {
391            period_months: 36,
392            amplitude: 0.20,
393            phase_offset: 0,
394            economic_correlation: 0.7,
395        }
396    }
397}
398
399// =============================================================================
400// Commodity Drift
401// =============================================================================
402
403/// Commodity drift configuration.
404#[derive(Debug, Clone, Default, Serialize, Deserialize)]
405pub struct CommodityDriftConfig {
406    /// Enable commodity drift.
407    #[serde(default)]
408    pub enabled: bool,
409    /// Commodity configurations.
410    #[serde(default)]
411    pub commodities: Vec<CommodityConfig>,
412}
413
414impl CommodityDriftConfig {
415    /// Calculate commodity effects at a period.
416    pub fn effects_at_period(&self, period: u32, rng: &mut ChaCha8Rng) -> CommodityEffects {
417        let mut effects = CommodityEffects::default();
418
419        for commodity in &self.commodities {
420            let price_factor = commodity.price_factor_at(period, rng);
421            effects
422                .price_factors
423                .insert(commodity.name.clone(), price_factor);
424
425            // Calculate pass-through effect on costs
426            effects.cogs_impact += (price_factor - 1.0) * commodity.cogs_pass_through;
427            effects.overhead_impact += (price_factor - 1.0) * commodity.overhead_pass_through;
428        }
429
430        effects
431    }
432}
433
434/// Commodity configuration.
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct CommodityConfig {
437    /// Commodity name.
438    pub name: String,
439    /// Base price.
440    #[serde(default = "default_base_price")]
441    pub base_price: f64,
442    /// Price volatility (standard deviation as fraction of price).
443    #[serde(default = "default_volatility")]
444    pub volatility: f64,
445    /// Correlation with economic cycle.
446    #[serde(default = "default_econ_correlation")]
447    pub economic_correlation: f64,
448    /// Pass-through to COGS (fraction).
449    #[serde(default)]
450    pub cogs_pass_through: f64,
451    /// Pass-through to overhead (fraction).
452    #[serde(default)]
453    pub overhead_pass_through: f64,
454}
455
456fn default_base_price() -> f64 {
457    100.0
458}
459
460fn default_volatility() -> f64 {
461    0.20
462}
463
464fn default_econ_correlation() -> f64 {
465    0.5
466}
467
468impl CommodityConfig {
469    /// Calculate price factor at a period.
470    pub fn price_factor_at(&self, period: u32, rng: &mut ChaCha8Rng) -> f64 {
471        // Mean-reverting random walk
472        let random: f64 = rng.gen();
473        let z_score = (random - 0.5) * 2.0; // Approximate normal
474        let price_change = z_score * self.volatility;
475
476        // Trend component (slight mean reversion)
477        let trend = -0.01 * period as f64 / 12.0;
478
479        1.0 + price_change + trend
480    }
481}
482
483/// Commodity effects.
484#[derive(Debug, Clone, Default)]
485pub struct CommodityEffects {
486    /// Price factors by commodity name.
487    pub price_factors: HashMap<String, f64>,
488    /// Impact on COGS.
489    pub cogs_impact: f64,
490    /// Impact on overhead.
491    pub overhead_impact: f64,
492}
493
494// =============================================================================
495// Price Shocks
496// =============================================================================
497
498/// Price shock event.
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct PriceShockEvent {
501    /// Shock identifier.
502    pub shock_id: String,
503    /// Shock type.
504    pub shock_type: PriceShockType,
505    /// Start period.
506    pub start_period: u32,
507    /// Duration in months.
508    pub duration_months: u32,
509    /// Price increase range (min, max) as fraction.
510    #[serde(default = "default_price_increase")]
511    pub price_increase_range: (f64, f64),
512    /// Affected categories.
513    #[serde(default)]
514    pub affected_categories: Vec<String>,
515}
516
517fn default_price_increase() -> (f64, f64) {
518    (0.10, 0.30)
519}
520
521impl PriceShockEvent {
522    /// Check if shock is active at a period.
523    pub fn is_active_at_period(&self, period: u32) -> bool {
524        period >= self.start_period && period < self.start_period + self.duration_months
525    }
526
527    /// Get progress through the shock (0.0 to 1.0).
528    pub fn progress_at_period(&self, period: u32) -> f64 {
529        if !self.is_active_at_period(period) {
530            return 0.0;
531        }
532        let elapsed = period - self.start_period;
533        elapsed as f64 / self.duration_months as f64
534    }
535}
536
537/// Price shock type.
538#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
539#[serde(rename_all = "snake_case")]
540pub enum PriceShockType {
541    /// Supply chain disruption.
542    #[default]
543    SupplyDisruption,
544    /// Demand surge.
545    DemandSurge,
546    /// Regulatory change.
547    RegulatoryChange,
548    /// Geopolitical event.
549    GeopoliticalEvent,
550    /// Natural disaster.
551    NaturalDisaster,
552}
553
554impl PriceShockType {
555    /// Get typical duration range for this shock type.
556    pub fn typical_duration_months(&self) -> (u32, u32) {
557        match self {
558            Self::SupplyDisruption => (3, 12),
559            Self::DemandSurge => (2, 6),
560            Self::RegulatoryChange => (6, 24),
561            Self::GeopoliticalEvent => (6, 18),
562            Self::NaturalDisaster => (1, 6),
563        }
564    }
565}
566
567// =============================================================================
568// Market Drift Controller
569// =============================================================================
570
571/// Market drift controller.
572pub struct MarketDriftController {
573    model: MarketDriftModel,
574    rng: ChaCha8Rng,
575}
576
577impl MarketDriftController {
578    /// Create a new market drift controller.
579    pub fn new(model: MarketDriftModel, seed: u64) -> Self {
580        Self {
581            model,
582            rng: ChaCha8Rng::seed_from_u64(seed),
583        }
584    }
585
586    /// Compute market effects for a period.
587    pub fn compute_effects(&mut self, period: u32) -> MarketEffects {
588        self.model.compute_effects(period, &mut self.rng)
589    }
590
591    /// Get the underlying model.
592    pub fn model(&self) -> &MarketDriftModel {
593        &self.model
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600
601    #[test]
602    fn test_sinusoidal_cycle() {
603        let model = EconomicCycleModel {
604            enabled: true,
605            cycle_type: CycleType::Sinusoidal,
606            period_months: 12,
607            amplitude: 0.20,
608            phase_offset: 0,
609            recession: RecessionConfig::default(),
610        };
611
612        let effect_0 = model.effect_at_period(0);
613        let effect_3 = model.effect_at_period(3); // 25% through cycle (peak)
614        let effect_9 = model.effect_at_period(9); // 75% through cycle (trough)
615
616        // At start, factor should be near 1.0
617        assert!((effect_0.cycle_factor - 1.0).abs() < 0.1);
618        // At peak, factor should be above 1.0
619        assert!(effect_3.cycle_factor > 1.0);
620        // At trough, factor should be below 1.0
621        assert!(effect_9.cycle_factor < 1.0);
622    }
623
624    #[test]
625    fn test_recession() {
626        let model = EconomicCycleModel {
627            enabled: true,
628            cycle_type: CycleType::Sinusoidal,
629            period_months: 48,
630            amplitude: 0.15,
631            phase_offset: 0,
632            recession: RecessionConfig {
633                enabled: true,
634                severity: RecessionSeverity::Moderate,
635                recession_periods: vec![(12, 6)], // Recession from month 12-17
636                ..Default::default()
637            },
638        };
639
640        let effect_10 = model.effect_at_period(10);
641        let effect_14 = model.effect_at_period(14);
642
643        assert!(!effect_10.is_recession);
644        assert!(effect_14.is_recession);
645        assert!(effect_14.cycle_factor < effect_10.cycle_factor);
646    }
647
648    #[test]
649    fn test_price_shock() {
650        let shock = PriceShockEvent {
651            shock_id: "SHOCK-001".to_string(),
652            shock_type: PriceShockType::SupplyDisruption,
653            start_period: 6,
654            duration_months: 3,
655            price_increase_range: (0.10, 0.30),
656            affected_categories: vec!["raw_materials".to_string()],
657        };
658
659        assert!(!shock.is_active_at_period(5));
660        assert!(shock.is_active_at_period(6));
661        assert!(shock.is_active_at_period(8));
662        assert!(!shock.is_active_at_period(9));
663
664        let progress = shock.progress_at_period(7);
665        assert!(progress > 0.3 && progress < 0.5);
666    }
667
668    #[test]
669    fn test_market_drift_model() {
670        let model = MarketDriftModel {
671            economic_cycle: EconomicCycleModel {
672                enabled: true,
673                period_months: 12,
674                amplitude: 0.15,
675                ..Default::default()
676            },
677            ..Default::default()
678        };
679
680        let mut rng = ChaCha8Rng::seed_from_u64(42);
681        let effects = model.compute_effects(6, &mut rng);
682
683        assert!((effects.economic_cycle_factor - 1.0).abs() < 0.5);
684    }
685}