1use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct MarketDriftModel {
17 #[serde(default)]
19 pub economic_cycle: EconomicCycleModel,
20 #[serde(default)]
22 pub industry_cycles: HashMap<MarketIndustryType, IndustryCycleConfig>,
23 #[serde(default)]
25 pub commodity_drift: CommodityDriftConfig,
26 #[serde(default)]
28 pub price_shocks: Vec<PriceShockEvent>,
29}
30
31impl MarketDriftModel {
32 pub fn compute_effects(&self, period: u32, rng: &mut ChaCha8Rng) -> MarketEffects {
34 let mut effects = MarketEffects::neutral();
35
36 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 if self.commodity_drift.enabled {
45 effects.commodity_effects = self.commodity_drift.effects_at_period(period, rng);
46 }
47
48 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum MarketIndustryType {
63 Technology,
65 Retail,
67 Manufacturing,
69 FinancialServices,
71 Healthcare,
73 Energy,
75 RealEstate,
77}
78
79impl MarketIndustryType {
80 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 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#[derive(Debug, Clone, Default)]
109pub struct MarketEffects {
110 pub economic_cycle_factor: f64,
112 pub is_recession: bool,
114 pub commodity_effects: CommodityEffects,
116 pub active_shocks: Vec<String>,
118 pub shock_multiplier: f64,
120}
121
122impl MarketEffects {
123 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct EconomicCycleModel {
152 #[serde(default)]
154 pub enabled: bool,
155 #[serde(default)]
157 pub cycle_type: CycleType,
158 #[serde(default = "default_cycle_period")]
160 pub period_months: u32,
161 #[serde(default = "default_amplitude")]
163 pub amplitude: f64,
164 #[serde(default)]
166 pub phase_offset: u32,
167 #[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 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 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 } else {
220 1.0 + self.amplitude * sine_value * 0.7 }
222 }
223 CycleType::MeanReverting => {
224 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
253#[serde(rename_all = "snake_case")]
254pub enum CycleType {
255 #[default]
257 Sinusoidal,
258 Asymmetric,
260 MeanReverting,
262}
263
264#[derive(Debug, Clone)]
266pub struct CycleEffect {
267 pub cycle_factor: f64,
269 pub is_recession: bool,
271 pub cycle_position: f64,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct RecessionConfig {
278 #[serde(default)]
280 pub enabled: bool,
281 #[serde(default = "default_recession_prob")]
283 pub probability_per_year: f64,
284 #[serde(default)]
286 pub onset: RecessionOnset,
287 #[serde(default = "default_recession_duration")]
289 pub duration_months: (u32, u32),
290 #[serde(default)]
292 pub severity: RecessionSeverity,
293 #[serde(default)]
295 pub recession_periods: Vec<(u32, u32)>, }
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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
333#[serde(rename_all = "snake_case")]
334pub enum RecessionOnset {
335 #[default]
337 Gradual,
338 Sudden,
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
344#[serde(rename_all = "snake_case")]
345pub enum RecessionSeverity {
346 Mild,
348 #[default]
350 Moderate,
351 Severe,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct IndustryCycleConfig {
362 #[serde(default = "default_industry_period")]
364 pub period_months: u32,
365 #[serde(default = "default_industry_amplitude")]
367 pub amplitude: f64,
368 #[serde(default)]
370 pub phase_offset: u32,
371 #[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
405pub struct CommodityDriftConfig {
406 #[serde(default)]
408 pub enabled: bool,
409 #[serde(default)]
411 pub commodities: Vec<CommodityConfig>,
412}
413
414impl CommodityDriftConfig {
415 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct CommodityConfig {
437 pub name: String,
439 #[serde(default = "default_base_price")]
441 pub base_price: f64,
442 #[serde(default = "default_volatility")]
444 pub volatility: f64,
445 #[serde(default = "default_econ_correlation")]
447 pub economic_correlation: f64,
448 #[serde(default)]
450 pub cogs_pass_through: f64,
451 #[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 pub fn price_factor_at(&self, period: u32, rng: &mut ChaCha8Rng) -> f64 {
471 let random: f64 = rng.gen();
473 let z_score = (random - 0.5) * 2.0; let price_change = z_score * self.volatility;
475
476 let trend = -0.01 * period as f64 / 12.0;
478
479 1.0 + price_change + trend
480 }
481}
482
483#[derive(Debug, Clone, Default)]
485pub struct CommodityEffects {
486 pub price_factors: HashMap<String, f64>,
488 pub cogs_impact: f64,
490 pub overhead_impact: f64,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct PriceShockEvent {
501 pub shock_id: String,
503 pub shock_type: PriceShockType,
505 pub start_period: u32,
507 pub duration_months: u32,
509 #[serde(default = "default_price_increase")]
511 pub price_increase_range: (f64, f64),
512 #[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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
539#[serde(rename_all = "snake_case")]
540pub enum PriceShockType {
541 #[default]
543 SupplyDisruption,
544 DemandSurge,
546 RegulatoryChange,
548 GeopoliticalEvent,
550 NaturalDisaster,
552}
553
554impl PriceShockType {
555 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
567pub struct MarketDriftController {
573 model: MarketDriftModel,
574 rng: ChaCha8Rng,
575}
576
577impl MarketDriftController {
578 pub fn new(model: MarketDriftModel, seed: u64) -> Self {
580 Self {
581 model,
582 rng: ChaCha8Rng::seed_from_u64(seed),
583 }
584 }
585
586 pub fn compute_effects(&mut self, period: u32) -> MarketEffects {
588 self.model.compute_effects(period, &mut self.rng)
589 }
590
591 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); let effect_9 = model.effect_at_period(9); assert!((effect_0.cycle_factor - 1.0).abs() < 0.1);
618 assert!(effect_3.cycle_factor > 1.0);
620 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)], ..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}