use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum DriftType {
#[default]
Gradual,
Sudden,
Recurring,
Mixed,
Regime,
EconomicCycle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum RegimeChangeType {
Acquisition,
Divestiture,
PriceIncrease,
PriceDecrease,
ProductLaunch,
ProductDiscontinuation,
#[default]
PolicyChange,
CompetitorEntry,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegimeEffect {
pub field: String,
pub multiplier: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegimeChange {
pub period: u32,
pub change_type: RegimeChangeType,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub effects: Vec<RegimeEffect>,
#[serde(default)]
pub transition_periods: u32,
}
impl RegimeChange {
pub fn new(period: u32, change_type: RegimeChangeType) -> Self {
Self {
period,
change_type,
description: None,
effects: Vec::new(),
transition_periods: 0,
}
}
pub fn volume_multiplier(&self) -> f64 {
match self.change_type {
RegimeChangeType::Acquisition => 1.35,
RegimeChangeType::Divestiture => 0.70,
RegimeChangeType::PriceIncrease => 0.95,
RegimeChangeType::PriceDecrease => 1.10,
RegimeChangeType::ProductLaunch => 1.20,
RegimeChangeType::ProductDiscontinuation => 0.85,
RegimeChangeType::PolicyChange => 1.0,
RegimeChangeType::CompetitorEntry => 0.90,
RegimeChangeType::Custom => self
.effects
.iter()
.find(|e| e.field == "transaction_volume")
.map(|e| e.multiplier)
.unwrap_or(1.0),
}
}
pub fn amount_mean_multiplier(&self) -> f64 {
match self.change_type {
RegimeChangeType::Acquisition => 1.15,
RegimeChangeType::Divestiture => 0.90,
RegimeChangeType::PriceIncrease => 1.25,
RegimeChangeType::PriceDecrease => 0.80,
RegimeChangeType::ProductLaunch => 0.90, RegimeChangeType::ProductDiscontinuation => 1.10, RegimeChangeType::PolicyChange => 1.0,
RegimeChangeType::CompetitorEntry => 0.95,
RegimeChangeType::Custom => self
.effects
.iter()
.find(|e| e.field == "amount_mean")
.map(|e| e.multiplier)
.unwrap_or(1.0),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EconomicCycleConfig {
pub enabled: bool,
#[serde(default = "default_cycle_length")]
pub cycle_length: u32,
#[serde(default = "default_amplitude")]
pub amplitude: f64,
#[serde(default)]
pub phase_offset: u32,
#[serde(default)]
pub recession_periods: Vec<u32>,
#[serde(default = "default_recession_severity")]
pub recession_severity: f64,
}
fn default_cycle_length() -> u32 {
48 }
fn default_amplitude() -> f64 {
0.15 }
fn default_recession_severity() -> f64 {
0.75 }
impl Default for EconomicCycleConfig {
fn default() -> Self {
Self {
enabled: false,
cycle_length: 48,
amplitude: 0.15,
phase_offset: 0,
recession_periods: Vec::new(),
recession_severity: 0.75,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ParameterDriftType {
#[default]
Linear,
Exponential,
Logistic,
Step,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterDrift {
pub parameter: String,
pub drift_type: ParameterDriftType,
pub initial_value: f64,
pub target_or_rate: f64,
#[serde(default)]
pub start_period: u32,
#[serde(default)]
pub end_period: Option<u32>,
#[serde(default = "default_steepness")]
pub steepness: f64,
}
fn default_steepness() -> f64 {
0.1
}
impl Default for ParameterDrift {
fn default() -> Self {
Self {
parameter: String::new(),
drift_type: ParameterDriftType::Linear,
initial_value: 1.0,
target_or_rate: 0.01,
start_period: 0,
end_period: None,
steepness: 0.1,
}
}
}
impl ParameterDrift {
pub fn value_at(&self, period: u32) -> f64 {
if period < self.start_period {
return self.initial_value;
}
let effective_period = period - self.start_period;
match self.drift_type {
ParameterDriftType::Linear => {
self.initial_value + self.target_or_rate * (effective_period as f64)
}
ParameterDriftType::Exponential => {
self.initial_value * (1.0 + self.target_or_rate).powi(effective_period as i32)
}
ParameterDriftType::Logistic => {
let end_period = self.end_period.unwrap_or(self.start_period + 24);
let midpoint = (self.start_period + end_period) as f64 / 2.0;
let t = period as f64;
let range = self.target_or_rate - self.initial_value;
self.initial_value + range / (1.0 + (-self.steepness * (t - midpoint)).exp())
}
ParameterDriftType::Step => {
if let Some(end) = self.end_period {
if period >= end {
return self.target_or_rate;
}
}
self.initial_value
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DriftConfig {
pub enabled: bool,
pub amount_mean_drift: f64,
pub amount_variance_drift: f64,
pub anomaly_rate_drift: f64,
pub concept_drift_rate: f64,
pub sudden_drift_probability: f64,
pub sudden_drift_magnitude: f64,
pub seasonal_drift: bool,
pub drift_start_period: u32,
pub drift_type: DriftType,
#[serde(default)]
pub regime_changes: Vec<RegimeChange>,
#[serde(default)]
pub economic_cycle: EconomicCycleConfig,
#[serde(default)]
pub parameter_drifts: Vec<ParameterDrift>,
}
impl Default for DriftConfig {
fn default() -> Self {
Self {
enabled: false,
amount_mean_drift: 0.02,
amount_variance_drift: 0.0,
anomaly_rate_drift: 0.0,
concept_drift_rate: 0.01,
sudden_drift_probability: 0.0,
sudden_drift_magnitude: 2.0,
seasonal_drift: false,
drift_start_period: 0,
drift_type: DriftType::Gradual,
regime_changes: Vec::new(),
economic_cycle: EconomicCycleConfig::default(),
parameter_drifts: Vec::new(),
}
}
}
impl DriftConfig {
pub fn with_regime_changes(regime_changes: Vec<RegimeChange>) -> Self {
Self {
enabled: true,
drift_type: DriftType::Regime,
regime_changes,
..Default::default()
}
}
pub fn with_economic_cycle(cycle_config: EconomicCycleConfig) -> Self {
Self {
enabled: true,
drift_type: DriftType::EconomicCycle,
economic_cycle: cycle_config,
..Default::default()
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DriftAdjustments {
pub amount_mean_multiplier: f64,
pub amount_variance_multiplier: f64,
pub anomaly_rate_adjustment: f64,
pub concept_drift_factor: f64,
pub sudden_drift_occurred: bool,
pub seasonal_factor: f64,
pub volume_multiplier: f64,
pub economic_cycle_factor: f64,
pub in_recession: bool,
pub active_regime_changes: Vec<RegimeChangeType>,
pub parameter_values: std::collections::HashMap<String, f64>,
}
impl DriftAdjustments {
pub fn none() -> Self {
Self {
amount_mean_multiplier: 1.0,
amount_variance_multiplier: 1.0,
anomaly_rate_adjustment: 0.0,
concept_drift_factor: 0.0,
sudden_drift_occurred: false,
seasonal_factor: 1.0,
volume_multiplier: 1.0,
economic_cycle_factor: 1.0,
in_recession: false,
active_regime_changes: Vec::new(),
parameter_values: std::collections::HashMap::new(),
}
}
pub fn combined_amount_multiplier(&self) -> f64 {
self.amount_mean_multiplier * self.seasonal_factor * self.economic_cycle_factor
}
pub fn combined_volume_multiplier(&self) -> f64 {
self.volume_multiplier * self.seasonal_factor * self.economic_cycle_factor
}
}
#[derive(Clone)]
pub struct DriftController {
config: DriftConfig,
rng: ChaCha8Rng,
sudden_drift_periods: Vec<u32>,
total_periods: u32,
}
impl DriftController {
pub fn new(config: DriftConfig, seed: u64, total_periods: u32) -> Self {
let mut controller = Self {
config,
rng: ChaCha8Rng::seed_from_u64(seed),
sudden_drift_periods: Vec::new(),
total_periods,
};
if controller.config.enabled
&& (controller.config.drift_type == DriftType::Sudden
|| controller.config.drift_type == DriftType::Mixed)
{
controller.precompute_sudden_drifts();
}
controller
}
fn precompute_sudden_drifts(&mut self) {
for period in 0..self.total_periods {
if period >= self.config.drift_start_period
&& self.rng.random::<f64>() < self.config.sudden_drift_probability
{
self.sudden_drift_periods.push(period);
}
}
}
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
pub fn compute_adjustments(&self, period: u32) -> DriftAdjustments {
if !self.config.enabled {
return DriftAdjustments::none();
}
if period < self.config.drift_start_period {
return DriftAdjustments::none();
}
let effective_period = period - self.config.drift_start_period;
let mut adjustments = DriftAdjustments::none();
match self.config.drift_type {
DriftType::Gradual => {
self.apply_gradual_drift(&mut adjustments, effective_period);
}
DriftType::Sudden => {
self.apply_sudden_drift(&mut adjustments, period);
}
DriftType::Recurring => {
self.apply_recurring_drift(&mut adjustments, effective_period);
}
DriftType::Mixed => {
self.apply_gradual_drift(&mut adjustments, effective_period);
self.apply_sudden_drift(&mut adjustments, period);
}
DriftType::Regime => {
self.apply_regime_drift(&mut adjustments, period);
}
DriftType::EconomicCycle => {
self.apply_economic_cycle(&mut adjustments, period);
}
}
if self.config.seasonal_drift {
adjustments.seasonal_factor = self.compute_seasonal_factor(period);
}
self.apply_parameter_drifts(&mut adjustments, period);
adjustments
}
fn apply_gradual_drift(&self, adjustments: &mut DriftAdjustments, effective_period: u32) {
let p = effective_period as f64;
adjustments.amount_mean_multiplier = (1.0 + self.config.amount_mean_drift).powf(p);
adjustments.amount_variance_multiplier = (1.0 + self.config.amount_variance_drift).powf(p);
adjustments.anomaly_rate_adjustment = self.config.anomaly_rate_drift * p;
adjustments.concept_drift_factor = (self.config.concept_drift_rate * p).min(1.0);
}
fn apply_sudden_drift(&self, adjustments: &mut DriftAdjustments, period: u32) {
let events_occurred: usize = self
.sudden_drift_periods
.iter()
.filter(|&&p| p <= period)
.count();
if events_occurred > 0 {
adjustments.sudden_drift_occurred = self.sudden_drift_periods.contains(&period);
let cumulative_magnitude = self
.config
.sudden_drift_magnitude
.powi(events_occurred as i32);
adjustments.amount_mean_multiplier *= cumulative_magnitude;
adjustments.amount_variance_multiplier *= cumulative_magnitude.sqrt();
}
}
fn apply_recurring_drift(&self, adjustments: &mut DriftAdjustments, effective_period: u32) {
let cycle_position = (effective_period % 12) as f64;
let cycle_radians = (cycle_position / 12.0) * 2.0 * std::f64::consts::PI;
let seasonal_amplitude = self.config.concept_drift_rate;
adjustments.amount_mean_multiplier = 1.0 + seasonal_amplitude * cycle_radians.sin();
adjustments.amount_variance_multiplier =
1.0 + (seasonal_amplitude * 0.5) * (cycle_radians + std::f64::consts::FRAC_PI_2).sin();
}
fn compute_seasonal_factor(&self, period: u32) -> f64 {
let month = period % 12;
match month {
0 | 1 => 0.85, 2 => 0.90, 3 | 4 => 0.95, 5 => 1.0, 6 | 7 => 0.95, 8 => 1.0, 9 => 1.10, 10 => 1.20, 11 => 1.30, _ => 1.0,
}
}
pub fn sudden_drift_periods(&self) -> &[u32] {
&self.sudden_drift_periods
}
pub fn config(&self) -> &DriftConfig {
&self.config
}
fn apply_regime_drift(&self, adjustments: &mut DriftAdjustments, period: u32) {
let mut volume_mult = 1.0;
let mut amount_mult = 1.0;
for regime_change in &self.config.regime_changes {
if period >= regime_change.period {
let periods_since = period - regime_change.period;
let transition_factor = if regime_change.transition_periods == 0 {
1.0
} else {
(periods_since as f64 / regime_change.transition_periods as f64).min(1.0)
};
let vol_change = regime_change.volume_multiplier() - 1.0;
let amt_change = regime_change.amount_mean_multiplier() - 1.0;
volume_mult *= 1.0 + vol_change * transition_factor;
amount_mult *= 1.0 + amt_change * transition_factor;
adjustments
.active_regime_changes
.push(regime_change.change_type);
}
}
adjustments.volume_multiplier = volume_mult;
adjustments.amount_mean_multiplier *= amount_mult;
}
fn apply_economic_cycle(&self, adjustments: &mut DriftAdjustments, period: u32) {
let cycle = &self.config.economic_cycle;
if !cycle.enabled {
return;
}
let adjusted_period = period + cycle.phase_offset;
let cycle_position =
(adjusted_period % cycle.cycle_length) as f64 / cycle.cycle_length as f64;
let cycle_radians = cycle_position * 2.0 * std::f64::consts::PI;
let cycle_factor = 1.0 + cycle.amplitude * cycle_radians.sin();
let in_recession = cycle.recession_periods.contains(&period);
adjustments.in_recession = in_recession;
let final_factor = if in_recession {
cycle_factor * cycle.recession_severity
} else {
cycle_factor
};
adjustments.economic_cycle_factor = final_factor;
adjustments.amount_mean_multiplier *= final_factor;
adjustments.volume_multiplier = final_factor;
}
fn apply_parameter_drifts(&self, adjustments: &mut DriftAdjustments, period: u32) {
for param_drift in &self.config.parameter_drifts {
let value = param_drift.value_at(period);
adjustments
.parameter_values
.insert(param_drift.parameter.clone(), value);
}
}
pub fn regime_changes_until(&self, period: u32) -> Vec<&RegimeChange> {
self.config
.regime_changes
.iter()
.filter(|rc| rc.period <= period)
.collect()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_no_drift_when_disabled() {
let config = DriftConfig::default();
let controller = DriftController::new(config, 42, 12);
let adjustments = controller.compute_adjustments(6);
assert!(!controller.is_enabled());
assert!((adjustments.amount_mean_multiplier - 1.0).abs() < 0.001);
assert!((adjustments.anomaly_rate_adjustment).abs() < 0.001);
}
#[test]
fn test_gradual_drift() {
let config = DriftConfig {
enabled: true,
amount_mean_drift: 0.02,
anomaly_rate_drift: 0.001,
drift_type: DriftType::Gradual,
..Default::default()
};
let controller = DriftController::new(config, 42, 12);
let adj0 = controller.compute_adjustments(0);
assert!((adj0.amount_mean_multiplier - 1.0).abs() < 0.001);
let adj6 = controller.compute_adjustments(6);
assert!(adj6.amount_mean_multiplier > 1.10);
assert!(adj6.amount_mean_multiplier < 1.15);
let adj12 = controller.compute_adjustments(12);
assert!(adj12.amount_mean_multiplier > 1.20);
assert!(adj12.amount_mean_multiplier < 1.30);
}
#[test]
fn test_drift_start_period() {
let config = DriftConfig {
enabled: true,
amount_mean_drift: 0.02,
drift_start_period: 3,
drift_type: DriftType::Gradual,
..Default::default()
};
let controller = DriftController::new(config, 42, 12);
let adj2 = controller.compute_adjustments(2);
assert!((adj2.amount_mean_multiplier - 1.0).abs() < 0.001);
let adj3 = controller.compute_adjustments(3);
assert!((adj3.amount_mean_multiplier - 1.0).abs() < 0.001);
let adj6 = controller.compute_adjustments(6);
assert!(adj6.amount_mean_multiplier > 1.0);
}
#[test]
fn test_seasonal_factor() {
let config = DriftConfig {
enabled: true,
seasonal_drift: true,
drift_type: DriftType::Gradual,
..Default::default()
};
let controller = DriftController::new(config, 42, 12);
let adj_dec = controller.compute_adjustments(11);
assert!(adj_dec.seasonal_factor > 1.2);
let adj_jan = controller.compute_adjustments(0);
assert!(adj_jan.seasonal_factor < 0.9);
}
#[test]
fn test_sudden_drift_reproducibility() {
let config = DriftConfig {
enabled: true,
sudden_drift_probability: 0.5,
sudden_drift_magnitude: 1.5,
drift_type: DriftType::Sudden,
..Default::default()
};
let controller1 = DriftController::new(config.clone(), 42, 12);
let controller2 = DriftController::new(config, 42, 12);
assert_eq!(
controller1.sudden_drift_periods(),
controller2.sudden_drift_periods()
);
}
#[test]
fn test_regime_change() {
let config = DriftConfig {
enabled: true,
drift_type: DriftType::Regime,
regime_changes: vec![RegimeChange::new(6, RegimeChangeType::Acquisition)],
..Default::default()
};
let controller = DriftController::new(config, 42, 12);
let adj_before = controller.compute_adjustments(5);
assert!((adj_before.volume_multiplier - 1.0).abs() < 0.001);
let adj_after = controller.compute_adjustments(6);
assert!(adj_after.volume_multiplier > 1.3); assert!(adj_after.amount_mean_multiplier > 1.1); assert!(adj_after
.active_regime_changes
.contains(&RegimeChangeType::Acquisition));
}
#[test]
fn test_regime_change_gradual_transition() {
let config = DriftConfig {
enabled: true,
drift_type: DriftType::Regime,
regime_changes: vec![RegimeChange {
period: 6,
change_type: RegimeChangeType::PriceIncrease,
description: None,
effects: vec![],
transition_periods: 4, }],
..Default::default()
};
let controller = DriftController::new(config, 42, 12);
let adj_start = controller.compute_adjustments(6);
let adj_mid = controller.compute_adjustments(8);
let adj_end = controller.compute_adjustments(10);
assert!(adj_start.amount_mean_multiplier < adj_mid.amount_mean_multiplier);
assert!(adj_mid.amount_mean_multiplier < adj_end.amount_mean_multiplier);
}
#[test]
fn test_economic_cycle() {
let config = DriftConfig {
enabled: true,
drift_type: DriftType::EconomicCycle,
economic_cycle: EconomicCycleConfig {
enabled: true,
cycle_length: 12, amplitude: 0.20,
phase_offset: 0,
recession_periods: vec![],
recession_severity: 0.75,
},
..Default::default()
};
let controller = DriftController::new(config, 42, 24);
let adj_0 = controller.compute_adjustments(0);
let adj_3 = controller.compute_adjustments(3);
let adj_9 = controller.compute_adjustments(9);
assert!(adj_3.economic_cycle_factor > adj_0.economic_cycle_factor);
assert!(adj_9.economic_cycle_factor < adj_0.economic_cycle_factor);
}
#[test]
fn test_economic_cycle_recession() {
let config = DriftConfig {
enabled: true,
drift_type: DriftType::EconomicCycle,
economic_cycle: EconomicCycleConfig {
enabled: true,
cycle_length: 12,
amplitude: 0.10,
phase_offset: 0,
recession_periods: vec![6, 7, 8],
recession_severity: 0.70,
},
..Default::default()
};
let controller = DriftController::new(config, 42, 12);
let adj_5 = controller.compute_adjustments(5);
assert!(!adj_5.in_recession);
let adj_7 = controller.compute_adjustments(7);
assert!(adj_7.in_recession);
assert!(adj_7.economic_cycle_factor < adj_5.economic_cycle_factor);
}
#[test]
fn test_parameter_drift_linear() {
let config = DriftConfig {
enabled: true,
drift_type: DriftType::Gradual,
parameter_drifts: vec![ParameterDrift {
parameter: "discount_rate".to_string(),
drift_type: ParameterDriftType::Linear,
initial_value: 0.02,
target_or_rate: 0.001, start_period: 0,
end_period: None,
steepness: 0.1,
}],
..Default::default()
};
let controller = DriftController::new(config, 42, 12);
let adj_0 = controller.compute_adjustments(0);
let adj_6 = controller.compute_adjustments(6);
let rate_0 = adj_0.parameter_values.get("discount_rate").unwrap();
let rate_6 = adj_6.parameter_values.get("discount_rate").unwrap();
assert!((rate_0 - 0.02).abs() < 0.0001);
assert!((rate_6 - 0.026).abs() < 0.0001);
}
#[test]
fn test_parameter_drift_logistic() {
let config = DriftConfig {
enabled: true,
drift_type: DriftType::Gradual,
parameter_drifts: vec![ParameterDrift {
parameter: "market_share".to_string(),
drift_type: ParameterDriftType::Logistic,
initial_value: 0.10, target_or_rate: 0.40, start_period: 0,
end_period: Some(24), steepness: 0.3,
}],
..Default::default()
};
let controller = DriftController::new(config, 42, 36);
let adj_0 = controller.compute_adjustments(0);
let adj_12 = controller.compute_adjustments(12);
let adj_24 = controller.compute_adjustments(24);
let share_0 = *adj_0.parameter_values.get("market_share").unwrap();
let share_12 = *adj_12.parameter_values.get("market_share").unwrap();
let share_24 = *adj_24.parameter_values.get("market_share").unwrap();
assert!(share_0 < 0.15); assert!(share_12 > 0.20 && share_12 < 0.30); assert!(share_24 > 0.35); }
#[test]
fn test_combined_drift_adjustments() {
let adj = DriftAdjustments {
amount_mean_multiplier: 1.2,
seasonal_factor: 1.1,
economic_cycle_factor: 0.9,
volume_multiplier: 1.3,
..DriftAdjustments::none()
};
assert!((adj.combined_amount_multiplier() - 1.188).abs() < 0.001);
assert!((adj.combined_volume_multiplier() - 1.287).abs() < 0.001);
}
#[test]
fn test_regime_change_volume_multipliers() {
assert!(RegimeChange::new(0, RegimeChangeType::Acquisition).volume_multiplier() > 1.0);
assert!(RegimeChange::new(0, RegimeChangeType::Divestiture).volume_multiplier() < 1.0);
assert!(
(RegimeChange::new(0, RegimeChangeType::PolicyChange).volume_multiplier() - 1.0).abs()
< 0.001
);
}
}