use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MarketDriftModel {
#[serde(default)]
pub economic_cycle: EconomicCycleModel,
#[serde(default)]
pub industry_cycles: HashMap<MarketIndustryType, IndustryCycleConfig>,
#[serde(default)]
pub commodity_drift: CommodityDriftConfig,
#[serde(default)]
pub price_shocks: Vec<PriceShockEvent>,
}
impl MarketDriftModel {
pub fn compute_effects(&self, period: u32, rng: &mut ChaCha8Rng) -> MarketEffects {
let mut effects = MarketEffects::neutral();
if self.economic_cycle.enabled {
let cycle_effect = self.economic_cycle.effect_at_period(period);
effects.economic_cycle_factor = cycle_effect.cycle_factor;
effects.is_recession = cycle_effect.is_recession;
}
if self.commodity_drift.enabled {
effects.commodity_effects = self.commodity_drift.effects_at_period(period, rng);
}
for shock in &self.price_shocks {
if shock.is_active_at_period(period) {
effects.apply_shock(shock, period);
}
}
effects
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketIndustryType {
Technology,
Retail,
Manufacturing,
FinancialServices,
Healthcare,
Energy,
RealEstate,
}
impl MarketIndustryType {
pub fn typical_cycle_months(&self) -> u32 {
match self {
Self::Technology => 36,
Self::Retail => 12,
Self::Manufacturing => 48,
Self::FinancialServices => 60,
Self::Healthcare => 36,
Self::Energy => 48,
Self::RealEstate => 84,
}
}
pub fn typical_amplitude(&self) -> f64 {
match self {
Self::Technology => 0.25,
Self::Retail => 0.35,
Self::Manufacturing => 0.20,
Self::FinancialServices => 0.15,
Self::Healthcare => 0.10,
Self::Energy => 0.30,
Self::RealEstate => 0.20,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MarketEffects {
pub economic_cycle_factor: f64,
pub is_recession: bool,
pub commodity_effects: CommodityEffects,
pub active_shocks: Vec<String>,
pub shock_multiplier: f64,
}
impl MarketEffects {
pub fn neutral() -> Self {
Self {
economic_cycle_factor: 1.0,
is_recession: false,
commodity_effects: CommodityEffects::default(),
active_shocks: Vec::new(),
shock_multiplier: 1.0,
}
}
fn apply_shock(&mut self, shock: &PriceShockEvent, period: u32) {
self.active_shocks.push(shock.shock_id.clone());
let progress = shock.progress_at_period(period);
let shock_factor = 1.0
+ shock.price_increase_range.0
+ (shock.price_increase_range.1 - shock.price_increase_range.0) * progress;
self.shock_multiplier *= shock_factor;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EconomicCycleModel {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub cycle_type: CycleType,
#[serde(default = "default_cycle_period")]
pub period_months: u32,
#[serde(default = "default_amplitude")]
pub amplitude: f64,
#[serde(default)]
pub phase_offset: u32,
#[serde(default)]
pub recession: RecessionConfig,
}
fn default_cycle_period() -> u32 {
48
}
fn default_amplitude() -> f64 {
0.15
}
impl Default for EconomicCycleModel {
fn default() -> Self {
Self {
enabled: false,
cycle_type: CycleType::Sinusoidal,
period_months: 48,
amplitude: 0.15,
phase_offset: 0,
recession: RecessionConfig::default(),
}
}
}
impl EconomicCycleModel {
pub fn effect_at_period(&self, period: u32) -> CycleEffect {
if !self.enabled {
return CycleEffect {
cycle_factor: 1.0,
is_recession: false,
cycle_position: 0.0,
};
}
let adjusted_period = period + self.phase_offset;
let cycle_position =
(adjusted_period % self.period_months) as f64 / self.period_months as f64;
let base_factor = match self.cycle_type {
CycleType::Sinusoidal => {
let radians = cycle_position * 2.0 * std::f64::consts::PI;
1.0 + self.amplitude * radians.sin()
}
CycleType::Asymmetric => {
let radians = cycle_position * 2.0 * std::f64::consts::PI;
let sine_value = radians.sin();
if sine_value < 0.0 {
1.0 + self.amplitude * sine_value * 1.3 } else {
1.0 + self.amplitude * sine_value * 0.7 }
}
CycleType::MeanReverting => {
let radians = cycle_position * 2.0 * std::f64::consts::PI;
let dampening = (-cycle_position * 0.5).exp();
1.0 + self.amplitude * radians.sin() * dampening
}
};
let is_recession = self.recession.enabled && self.recession.is_recession_at(period);
let recession_factor = if is_recession {
match self.recession.severity {
RecessionSeverity::Mild => 0.90,
RecessionSeverity::Moderate => 0.80,
RecessionSeverity::Severe => 0.65,
}
} else {
1.0
};
CycleEffect {
cycle_factor: base_factor * recession_factor,
is_recession,
cycle_position,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CycleType {
#[default]
Sinusoidal,
Asymmetric,
MeanReverting,
}
#[derive(Debug, Clone)]
pub struct CycleEffect {
pub cycle_factor: f64,
pub is_recession: bool,
pub cycle_position: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecessionConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_recession_prob")]
pub probability_per_year: f64,
#[serde(default)]
pub onset: RecessionOnset,
#[serde(default = "default_recession_duration")]
pub duration_months: (u32, u32),
#[serde(default)]
pub severity: RecessionSeverity,
#[serde(default)]
pub recession_periods: Vec<(u32, u32)>, }
fn default_recession_prob() -> f64 {
0.10
}
fn default_recession_duration() -> (u32, u32) {
(12, 24)
}
impl Default for RecessionConfig {
fn default() -> Self {
Self {
enabled: false,
probability_per_year: 0.10,
onset: RecessionOnset::Gradual,
duration_months: (12, 24),
severity: RecessionSeverity::Moderate,
recession_periods: Vec::new(),
}
}
}
impl RecessionConfig {
pub fn is_recession_at(&self, period: u32) -> bool {
for (start, duration) in &self.recession_periods {
if period >= *start && period < start + duration {
return true;
}
}
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RecessionOnset {
#[default]
Gradual,
Sudden,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RecessionSeverity {
Mild,
#[default]
Moderate,
Severe,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndustryCycleConfig {
#[serde(default = "default_industry_period")]
pub period_months: u32,
#[serde(default = "default_industry_amplitude")]
pub amplitude: f64,
#[serde(default)]
pub phase_offset: u32,
#[serde(default = "default_correlation")]
pub economic_correlation: f64,
}
fn default_industry_period() -> u32 {
36
}
fn default_industry_amplitude() -> f64 {
0.20
}
fn default_correlation() -> f64 {
0.7
}
impl Default for IndustryCycleConfig {
fn default() -> Self {
Self {
period_months: 36,
amplitude: 0.20,
phase_offset: 0,
economic_correlation: 0.7,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CommodityDriftConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub commodities: Vec<CommodityConfig>,
}
impl CommodityDriftConfig {
pub fn effects_at_period(&self, period: u32, rng: &mut ChaCha8Rng) -> CommodityEffects {
let mut effects = CommodityEffects::default();
for commodity in &self.commodities {
let price_factor = commodity.price_factor_at(period, rng);
effects
.price_factors
.insert(commodity.name.clone(), price_factor);
effects.cogs_impact += (price_factor - 1.0) * commodity.cogs_pass_through;
effects.overhead_impact += (price_factor - 1.0) * commodity.overhead_pass_through;
}
effects
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommodityConfig {
pub name: String,
#[serde(default = "default_base_price")]
pub base_price: f64,
#[serde(default = "default_volatility")]
pub volatility: f64,
#[serde(default = "default_econ_correlation")]
pub economic_correlation: f64,
#[serde(default)]
pub cogs_pass_through: f64,
#[serde(default)]
pub overhead_pass_through: f64,
}
fn default_base_price() -> f64 {
100.0
}
fn default_volatility() -> f64 {
0.20
}
fn default_econ_correlation() -> f64 {
0.5
}
impl CommodityConfig {
pub fn price_factor_at(&self, period: u32, rng: &mut ChaCha8Rng) -> f64 {
let random: f64 = rng.random();
let z_score = (random - 0.5) * 2.0; let price_change = z_score * self.volatility;
let trend = -0.01 * period as f64 / 12.0;
1.0 + price_change + trend
}
}
#[derive(Debug, Clone, Default)]
pub struct CommodityEffects {
pub price_factors: HashMap<String, f64>,
pub cogs_impact: f64,
pub overhead_impact: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceShockEvent {
pub shock_id: String,
pub shock_type: PriceShockType,
pub start_period: u32,
pub duration_months: u32,
#[serde(default = "default_price_increase")]
pub price_increase_range: (f64, f64),
#[serde(default)]
pub affected_categories: Vec<String>,
}
fn default_price_increase() -> (f64, f64) {
(0.10, 0.30)
}
impl PriceShockEvent {
pub fn is_active_at_period(&self, period: u32) -> bool {
period >= self.start_period && period < self.start_period + self.duration_months
}
pub fn progress_at_period(&self, period: u32) -> f64 {
if !self.is_active_at_period(period) {
return 0.0;
}
let elapsed = period - self.start_period;
elapsed as f64 / self.duration_months as f64
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PriceShockType {
#[default]
SupplyDisruption,
DemandSurge,
RegulatoryChange,
GeopoliticalEvent,
NaturalDisaster,
}
impl PriceShockType {
pub fn typical_duration_months(&self) -> (u32, u32) {
match self {
Self::SupplyDisruption => (3, 12),
Self::DemandSurge => (2, 6),
Self::RegulatoryChange => (6, 24),
Self::GeopoliticalEvent => (6, 18),
Self::NaturalDisaster => (1, 6),
}
}
}
pub struct MarketDriftController {
model: MarketDriftModel,
rng: ChaCha8Rng,
}
impl MarketDriftController {
pub fn new(model: MarketDriftModel, seed: u64) -> Self {
Self {
model,
rng: ChaCha8Rng::seed_from_u64(seed),
}
}
pub fn compute_effects(&mut self, period: u32) -> MarketEffects {
self.model.compute_effects(period, &mut self.rng)
}
pub fn model(&self) -> &MarketDriftModel {
&self.model
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_sinusoidal_cycle() {
let model = EconomicCycleModel {
enabled: true,
cycle_type: CycleType::Sinusoidal,
period_months: 12,
amplitude: 0.20,
phase_offset: 0,
recession: RecessionConfig::default(),
};
let effect_0 = model.effect_at_period(0);
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);
assert!(effect_3.cycle_factor > 1.0);
assert!(effect_9.cycle_factor < 1.0);
}
#[test]
fn test_recession() {
let model = EconomicCycleModel {
enabled: true,
cycle_type: CycleType::Sinusoidal,
period_months: 48,
amplitude: 0.15,
phase_offset: 0,
recession: RecessionConfig {
enabled: true,
severity: RecessionSeverity::Moderate,
recession_periods: vec![(12, 6)], ..Default::default()
},
};
let effect_10 = model.effect_at_period(10);
let effect_14 = model.effect_at_period(14);
assert!(!effect_10.is_recession);
assert!(effect_14.is_recession);
assert!(effect_14.cycle_factor < effect_10.cycle_factor);
}
#[test]
fn test_price_shock() {
let shock = PriceShockEvent {
shock_id: "SHOCK-001".to_string(),
shock_type: PriceShockType::SupplyDisruption,
start_period: 6,
duration_months: 3,
price_increase_range: (0.10, 0.30),
affected_categories: vec!["raw_materials".to_string()],
};
assert!(!shock.is_active_at_period(5));
assert!(shock.is_active_at_period(6));
assert!(shock.is_active_at_period(8));
assert!(!shock.is_active_at_period(9));
let progress = shock.progress_at_period(7);
assert!(progress > 0.3 && progress < 0.5);
}
#[test]
fn test_market_drift_model() {
let model = MarketDriftModel {
economic_cycle: EconomicCycleModel {
enabled: true,
period_months: 12,
amplitude: 0.15,
..Default::default()
},
..Default::default()
};
let mut rng = ChaCha8Rng::seed_from_u64(42);
let effects = model.compute_effects(6, &mut rng);
assert!((effects.economic_cycle_factor - 1.0).abs() < 0.5);
}
}