pub mod hmm;
use crate::error::FinError;
use crate::signals::indicators::{Adx, BollingerWidth, HistoricalVolatility, HurstExponent};
use crate::signals::{BarInput, Signal, SignalValue};
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum MarketRegime {
Trending,
MeanReverting,
HighVolatility,
LowVolatility,
Crisis,
Neutral,
Unknown,
}
impl MarketRegime {
pub fn is_risk_off(self) -> bool {
matches!(self, MarketRegime::Crisis | MarketRegime::Unknown)
}
pub fn short_code(self) -> &'static str {
match self {
MarketRegime::Trending => "TRD",
MarketRegime::MeanReverting => "MRV",
MarketRegime::HighVolatility => "HVL",
MarketRegime::LowVolatility => "LVL",
MarketRegime::Crisis => "CRS",
MarketRegime::Neutral => "NEU",
MarketRegime::Unknown => "UNK",
}
}
}
impl std::fmt::Display for MarketRegime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MarketRegime::Trending => write!(f, "Trending"),
MarketRegime::MeanReverting => write!(f, "MeanReverting"),
MarketRegime::HighVolatility => write!(f, "HighVolatility"),
MarketRegime::LowVolatility => write!(f, "LowVolatility"),
MarketRegime::Crisis => write!(f, "Crisis"),
MarketRegime::Neutral => write!(f, "Neutral"),
MarketRegime::Unknown => write!(f, "Unknown"),
}
}
}
#[derive(Debug, Clone)]
pub struct RegimeConfig {
pub hurst_trending: f64,
pub hurst_mean_reverting: f64,
pub vol_high_multiplier: f64,
pub vol_low_multiplier: f64,
pub adx_trend_threshold: f64,
pub bb_width_quiet: f64,
pub crisis_correlation_threshold: f64,
pub crisis_pair_fraction: f64,
pub garch_alpha: f64,
pub garch_beta: f64,
pub garch_omega: f64,
pub garch_vol_multiplier: f64,
}
impl Default for RegimeConfig {
fn default() -> Self {
Self {
hurst_trending: 0.6,
hurst_mean_reverting: 0.4,
vol_high_multiplier: 2.0,
vol_low_multiplier: 0.5,
adx_trend_threshold: 25.0,
bb_width_quiet: 0.02,
crisis_correlation_threshold: 0.3,
crisis_pair_fraction: 0.6,
garch_alpha: 0.1,
garch_beta: 0.85,
garch_omega: 1e-6,
garch_vol_multiplier: 1.5,
}
}
}
#[derive(Debug, Clone)]
pub struct Garch11 {
alpha: f64,
beta: f64,
omega: f64,
variance: f64,
mean: f64,
count: usize,
}
impl Garch11 {
pub fn new(alpha: f64, beta: f64, omega: f64) -> Result<Self, FinError> {
if alpha <= 0.0 || beta <= 0.0 || omega <= 0.0 {
return Err(FinError::InvalidInput(
"GARCH parameters alpha, beta, and omega must all be positive".to_owned(),
));
}
if alpha + beta >= 1.0 {
return Err(FinError::InvalidInput(format!(
"GARCH(1,1) requires alpha + beta < 1 for stationarity, got {:.4}",
alpha + beta
)));
}
let long_run_var = omega / (1.0 - alpha - beta);
Ok(Self { alpha, beta, omega, variance: long_run_var, mean: 0.0, count: 0 })
}
pub fn update(&mut self, log_return: f64) -> f64 {
self.count += 1;
let delta = log_return - self.mean;
self.mean += delta / self.count as f64;
let demeaned = log_return - self.mean;
self.variance = self.omega
+ self.alpha * demeaned * demeaned
+ self.beta * self.variance;
self.variance.sqrt()
}
pub fn variance(&self) -> f64 {
self.variance
}
pub fn sigma(&self) -> f64 {
self.variance.sqrt()
}
pub fn long_run_sigma(&self) -> f64 {
(self.omega / (1.0 - self.alpha - self.beta)).sqrt()
}
pub fn is_vol_elevated(&self, multiplier: f64) -> bool {
self.sigma() > self.long_run_sigma() * multiplier
}
pub fn count(&self) -> usize {
self.count
}
pub fn reset(&mut self) {
let long_run_var = self.omega / (1.0 - self.alpha - self.beta);
self.variance = long_run_var;
self.mean = 0.0;
self.count = 0;
}
}
#[derive(Debug, Clone)]
pub struct CorrelationBreakdownDetector {
window: usize,
threshold: f64,
crisis_fraction: f64,
returns: Vec<std::collections::VecDeque<f64>>,
n_assets: usize,
}
impl CorrelationBreakdownDetector {
pub fn new(window: usize, threshold: f64, crisis_fraction: f64) -> Result<Self, FinError> {
if window < 3 {
return Err(FinError::InvalidInput(
"correlation window must be at least 3".to_owned(),
));
}
if !(0.0..=1.0).contains(&threshold) {
return Err(FinError::InvalidInput(
"correlation threshold must be in [0, 1]".to_owned(),
));
}
if !(0.0..=1.0).contains(&crisis_fraction) {
return Err(FinError::InvalidInput(
"crisis_fraction must be in [0, 1]".to_owned(),
));
}
Ok(Self {
window,
threshold,
crisis_fraction,
returns: Vec::new(),
n_assets: 0,
})
}
pub fn update(&mut self, asset_idx: usize, log_return: f64) {
while self.returns.len() <= asset_idx {
self.returns.push(std::collections::VecDeque::with_capacity(self.window + 1));
self.n_assets = self.returns.len();
}
let buf = &mut self.returns[asset_idx];
buf.push_back(log_return);
if buf.len() > self.window {
buf.pop_front();
}
}
pub fn is_crisis(&self) -> bool {
if self.n_assets < 2 {
return false;
}
let mut total_pairs = 0usize;
let mut decorrelated_pairs = 0usize;
for i in 0..self.n_assets {
for j in (i + 1)..self.n_assets {
let ri = &self.returns[i];
let rj = &self.returns[j];
if ri.len() < 3 || rj.len() < 3 {
continue;
}
let len = ri.len().min(rj.len());
let r = pearson_r(
ri.iter().rev().take(len).copied().collect::<Vec<_>>().as_slice(),
rj.iter().rev().take(len).copied().collect::<Vec<_>>().as_slice(),
);
total_pairs += 1;
if r.abs() < self.threshold {
decorrelated_pairs += 1;
}
}
}
if total_pairs == 0 {
return false;
}
(decorrelated_pairs as f64 / total_pairs as f64) >= self.crisis_fraction
}
pub fn n_assets(&self) -> usize {
self.n_assets
}
pub fn reset(&mut self) {
for buf in &mut self.returns {
buf.clear();
}
}
}
fn pearson_r(x: &[f64], y: &[f64]) -> f64 {
let n = x.len().min(y.len());
if n < 2 {
return 0.0;
}
let n_f = n as f64;
let mean_x = x[..n].iter().sum::<f64>() / n_f;
let mean_y = y[..n].iter().sum::<f64>() / n_f;
let mut cov = 0.0;
let mut var_x = 0.0;
let mut var_y = 0.0;
for i in 0..n {
let dx = x[i] - mean_x;
let dy = y[i] - mean_y;
cov += dx * dy;
var_x += dx * dx;
var_y += dy * dy;
}
let denom = (var_x * var_y).sqrt();
if denom < 1e-12 {
return 0.0;
}
(cov / denom).clamp(-1.0, 1.0)
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RegimeHistory {
pub regime: MarketRegime,
pub started_at_bar: usize,
pub confidence: f64,
pub ended_at_bar: Option<usize>,
}
impl RegimeHistory {
pub fn duration_bars(&self) -> Option<usize> {
self.ended_at_bar.map(|end| end - self.started_at_bar)
}
pub fn is_active(&self) -> bool {
self.ended_at_bar.is_none()
}
}
pub struct RegimeDetector {
adx: Adx,
hurst: HurstExponent,
hv: HistoricalVolatility,
bb_width: BollingerWidth,
garch: Garch11,
correlation: CorrelationBreakdownDetector,
config: RegimeConfig,
hv_mean: f64,
hv_count: usize,
prev_close: Option<f64>,
bar_count: usize,
history: Vec<RegimeHistory>,
current_regime: MarketRegime,
}
impl RegimeDetector {
pub fn new(period: usize, config: RegimeConfig) -> Result<Self, FinError> {
if period < 2 {
return Err(FinError::InvalidPeriod(period));
}
let garch = Garch11::new(config.garch_alpha, config.garch_beta, config.garch_omega)?;
let correlation = CorrelationBreakdownDetector::new(
period.max(5),
config.crisis_correlation_threshold,
config.crisis_pair_fraction,
)?;
Ok(Self {
adx: Adx::new("regime_adx", period)?,
hurst: HurstExponent::new("regime_hurst", period)?,
hv: HistoricalVolatility::new("regime_hv", period, 252)?,
bb_width: BollingerWidth::new("regime_bb_width", period, Decimal::from(2u32))?,
garch,
correlation,
config,
hv_mean: 0.0,
hv_count: 0,
prev_close: None,
bar_count: 0,
history: Vec::new(),
current_regime: MarketRegime::Unknown,
})
}
pub fn with_defaults(period: usize) -> Result<Self, FinError> {
Self::new(period, RegimeConfig::default())
}
pub fn update(
&mut self,
bar: &BarInput,
cross_returns: &[(usize, f64)],
) -> Result<(MarketRegime, f64), FinError> {
self.bar_count += 1;
let close_f = bar.close.to_f64().unwrap_or(0.0);
if let Some(prev) = self.prev_close {
if prev > 0.0 {
let log_ret = (close_f / prev).ln();
self.garch.update(log_ret);
self.correlation.update(0, log_ret);
}
}
self.prev_close = Some(close_f);
for &(idx, ret) in cross_returns {
self.correlation.update(idx + 1, ret); }
let adx_val = self.adx.update(bar)?;
let hurst_val = self.hurst.update(bar)?;
let hv_val = self.hv.update(bar)?;
let bb_w_val = self.bb_width.update(bar)?;
let (adx_f, hurst_f, hv_f, bb_w_f) = match (adx_val, hurst_val, hv_val, bb_w_val) {
(
SignalValue::Scalar(a),
SignalValue::Scalar(h),
SignalValue::Scalar(v),
SignalValue::Scalar(b),
) => (
a.to_f64().unwrap_or(0.0),
h.to_f64().unwrap_or(0.5),
v.to_f64().unwrap_or(0.0),
b.to_f64().unwrap_or(f64::MAX),
),
_ => {
self.record_regime(MarketRegime::Unknown, 0.0);
return Ok((MarketRegime::Unknown, 0.0));
}
};
self.hv_count += 1;
self.hv_mean += (hv_f - self.hv_mean) / self.hv_count as f64;
if self.correlation.is_crisis() {
let conf = 0.9;
self.record_regime(MarketRegime::Crisis, conf);
return Ok((MarketRegime::Crisis, conf));
}
let vol_ratio = if self.hv_mean > 0.0 { hv_f / self.hv_mean } else { 1.0 };
let garch_elevated = self.garch.is_vol_elevated(self.config.garch_vol_multiplier);
if vol_ratio > self.config.vol_high_multiplier || (vol_ratio > 1.5 && garch_elevated) {
let conf = (vol_ratio - self.config.vol_high_multiplier).abs().min(1.0) * 0.8 + 0.2;
let conf = conf.min(1.0);
self.record_regime(MarketRegime::HighVolatility, conf);
return Ok((MarketRegime::HighVolatility, conf));
}
if hurst_f > self.config.hurst_trending {
let adx_factor = if adx_f > self.config.adx_trend_threshold { 1.0 } else { 0.7 };
let conf = ((hurst_f - self.config.hurst_trending)
/ (1.0 - self.config.hurst_trending))
.min(1.0)
* adx_factor;
self.record_regime(MarketRegime::Trending, conf);
return Ok((MarketRegime::Trending, conf));
}
if hurst_f < self.config.hurst_mean_reverting {
let conf = ((self.config.hurst_mean_reverting - hurst_f)
/ self.config.hurst_mean_reverting)
.min(1.0);
self.record_regime(MarketRegime::MeanReverting, conf);
return Ok((MarketRegime::MeanReverting, conf));
}
if vol_ratio < self.config.vol_low_multiplier || bb_w_f < self.config.bb_width_quiet {
let conf = (1.0 - vol_ratio / self.config.vol_low_multiplier).max(0.1).min(1.0);
self.record_regime(MarketRegime::LowVolatility, conf);
return Ok((MarketRegime::LowVolatility, conf));
}
self.record_regime(MarketRegime::Neutral, 0.5);
Ok((MarketRegime::Neutral, 0.5))
}
fn record_regime(&mut self, regime: MarketRegime, confidence: f64) {
if regime == self.current_regime {
return;
}
if let Some(last) = self.history.last_mut() {
if last.ended_at_bar.is_none() {
last.ended_at_bar = Some(self.bar_count);
}
}
self.current_regime = regime;
self.history.push(RegimeHistory {
regime,
started_at_bar: self.bar_count,
confidence,
ended_at_bar: None,
});
}
pub fn current_regime(&self) -> MarketRegime {
self.current_regime
}
pub fn history(&self) -> &[RegimeHistory] {
&self.history
}
pub fn is_ready(&self) -> bool {
self.adx.is_ready()
&& self.hurst.is_ready()
&& self.hv.is_ready()
&& self.bb_width.is_ready()
}
pub fn config(&self) -> &RegimeConfig {
&self.config
}
pub fn garch(&self) -> &Garch11 {
&self.garch
}
pub fn correlation_detector(&self) -> &CorrelationBreakdownDetector {
&self.correlation
}
pub fn reset(&mut self) {
self.adx.reset();
self.hurst.reset();
self.hv.reset();
self.bb_width.reset();
self.garch.reset();
self.correlation.reset();
self.hv_mean = 0.0;
self.hv_count = 0;
self.prev_close = None;
self.bar_count = 0;
self.history.clear();
self.current_regime = MarketRegime::Unknown;
}
pub fn bar_count(&self) -> usize {
self.bar_count
}
}
pub struct MarketRegimeDetector {
adx: Adx,
hurst: HurstExponent,
hv: HistoricalVolatility,
bb_width: BollingerWidth,
config: RegimeConfig,
}
impl MarketRegimeDetector {
pub fn new(period: usize, config: RegimeConfig) -> Result<Self, FinError> {
if period < 2 {
return Err(FinError::InvalidPeriod(period));
}
Ok(Self {
adx: Adx::new("regime_adx", period)?,
hurst: HurstExponent::new("regime_hurst", period)?,
hv: HistoricalVolatility::new("regime_hv", period, 252)?,
bb_width: BollingerWidth::new("regime_bb_width", period, Decimal::from(2u32))?,
config,
})
}
pub fn with_defaults(period: usize) -> Result<Self, FinError> {
Self::new(period, RegimeConfig::default())
}
pub fn update(&mut self, bar: &BarInput) -> Result<MarketRegime, FinError> {
let adx_val = self.adx.update(bar)?;
let hurst_val = self.hurst.update(bar)?;
let hv_val = self.hv.update(bar)?;
let bb_w_val = self.bb_width.update(bar)?;
let (adx, hurst, hv, bb_w) = match (adx_val, hurst_val, hv_val, bb_w_val) {
(
SignalValue::Scalar(a),
SignalValue::Scalar(h),
SignalValue::Scalar(v),
SignalValue::Scalar(b),
) => (a, h, v, b),
_ => return Ok(MarketRegime::Unknown),
};
let adx_f = adx.to_f64().unwrap_or(0.0);
let hurst_f = hurst.to_f64().unwrap_or(0.5);
let hv_f = hv.to_f64().unwrap_or(0.0);
let bb_w_f = bb_w.to_f64().unwrap_or(f64::MAX);
if hurst_f > self.config.hurst_trending && adx_f > self.config.adx_trend_threshold {
return Ok(MarketRegime::Trending);
}
if hv_f > self.config.vol_high_multiplier * 15.0 {
return Ok(MarketRegime::HighVolatility);
}
if hurst_f < self.config.hurst_mean_reverting {
return Ok(MarketRegime::MeanReverting);
}
if bb_w_f < self.config.bb_width_quiet {
return Ok(MarketRegime::LowVolatility);
}
Ok(MarketRegime::Neutral)
}
pub fn is_ready(&self) -> bool {
self.adx.is_ready()
&& self.hurst.is_ready()
&& self.hv.is_ready()
&& self.bb_width.is_ready()
}
pub fn config(&self) -> &RegimeConfig {
&self.config
}
pub fn reset(&mut self) {
self.adx.reset();
self.hurst.reset();
self.hv.reset();
self.bb_width.reset();
}
}
pub struct RegimeConditionalSignal {
rsi_trending: crate::signals::indicators::Rsi,
rsi_mean_reverting: crate::signals::indicators::Rsi,
rsi_neutral: crate::signals::indicators::Rsi,
}
impl RegimeConditionalSignal {
pub fn new(
trending_period: usize,
mean_reverting_period: usize,
neutral_period: usize,
) -> Result<Self, FinError> {
Ok(Self {
rsi_trending: crate::signals::indicators::Rsi::new(
"rsi_trending",
trending_period,
)?,
rsi_mean_reverting: crate::signals::indicators::Rsi::new(
"rsi_mean_reverting",
mean_reverting_period,
)?,
rsi_neutral: crate::signals::indicators::Rsi::new("rsi_neutral", neutral_period)?,
})
}
pub fn update(
&mut self,
bar: &BarInput,
regime: MarketRegime,
) -> Option<Result<f64, FinError>> {
let v_trending = self.rsi_trending.update(bar);
let v_mr = self.rsi_mean_reverting.update(bar);
let v_neutral = self.rsi_neutral.update(bar);
if regime.is_risk_off() {
return None;
}
let chosen = match regime {
MarketRegime::Trending => v_trending,
MarketRegime::MeanReverting => v_mr,
_ => v_neutral,
};
match chosen {
Ok(SignalValue::Scalar(v)) => {
Some(Ok(v.to_f64().unwrap_or(50.0)))
}
Ok(_) => None,
Err(e) => Some(Err(e)),
}
}
pub fn is_ready(&self) -> bool {
self.rsi_trending.is_ready()
&& self.rsi_mean_reverting.is_ready()
&& self.rsi_neutral.is_ready()
}
pub fn reset(&mut self) {
self.rsi_trending.reset();
self.rsi_mean_reverting.reset();
self.rsi_neutral.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn bar(h: f64, l: f64, c: f64) -> BarInput {
BarInput::new(
Decimal::try_from(c).unwrap_or(dec!(100)),
Decimal::try_from(h).unwrap_or(dec!(102)),
Decimal::try_from(l).unwrap_or(dec!(98)),
Decimal::try_from(c).unwrap_or(dec!(100)),
dec!(1000),
)
}
#[test]
fn test_regime_display_all_variants() {
assert_eq!(MarketRegime::Trending.to_string(), "Trending");
assert_eq!(MarketRegime::MeanReverting.to_string(), "MeanReverting");
assert_eq!(MarketRegime::HighVolatility.to_string(), "HighVolatility");
assert_eq!(MarketRegime::LowVolatility.to_string(), "LowVolatility");
assert_eq!(MarketRegime::Crisis.to_string(), "Crisis");
assert_eq!(MarketRegime::Neutral.to_string(), "Neutral");
assert_eq!(MarketRegime::Unknown.to_string(), "Unknown");
}
#[test]
fn test_regime_short_codes() {
assert_eq!(MarketRegime::Trending.short_code(), "TRD");
assert_eq!(MarketRegime::Crisis.short_code(), "CRS");
assert_eq!(MarketRegime::Unknown.short_code(), "UNK");
}
#[test]
fn test_is_risk_off() {
assert!(MarketRegime::Crisis.is_risk_off());
assert!(MarketRegime::Unknown.is_risk_off());
assert!(!MarketRegime::Trending.is_risk_off());
assert!(!MarketRegime::Neutral.is_risk_off());
}
#[test]
fn test_garch_invalid_params() {
assert!(Garch11::new(0.0, 0.85, 1e-6).is_err());
assert!(Garch11::new(0.1, 0.0, 1e-6).is_err());
assert!(Garch11::new(0.1, 0.85, 0.0).is_err());
assert!(Garch11::new(0.5, 0.6, 1e-6).is_err()); }
#[test]
fn test_garch_produces_positive_sigma() {
let mut g = Garch11::new(0.1, 0.85, 1e-6).unwrap();
let returns = [-0.01, 0.02, -0.015, 0.005, 0.03, -0.02, 0.01];
for ret in returns {
let sigma = g.update(ret);
assert!(sigma > 0.0, "sigma must be positive, got {sigma}");
}
}
#[test]
fn test_garch_reset() {
let mut g = Garch11::new(0.1, 0.85, 1e-6).unwrap();
for ret in [-0.05, 0.05, -0.05] {
g.update(ret);
}
let sigma_before = g.sigma();
g.reset();
let lr = g.long_run_sigma();
assert!((g.sigma() - lr).abs() < 1e-10);
assert_ne!(sigma_before, g.sigma());
assert_eq!(g.count(), 0);
}
#[test]
fn test_garch_vol_elevated() {
let mut g = Garch11::new(0.1, 0.85, 1e-4).unwrap();
for _ in 0..10 {
g.update(0.1); }
assert!(g.is_vol_elevated(1.0) || g.sigma() > 0.0); }
#[test]
fn test_correlation_invalid_params() {
assert!(CorrelationBreakdownDetector::new(1, 0.3, 0.6).is_err()); assert!(CorrelationBreakdownDetector::new(20, 1.5, 0.6).is_err()); assert!(CorrelationBreakdownDetector::new(20, 0.3, 1.5).is_err()); }
#[test]
fn test_no_crisis_single_asset() {
let mut d = CorrelationBreakdownDetector::new(10, 0.3, 0.6).unwrap();
for i in 0..15 {
d.update(0, if i % 2 == 0 { 0.01 } else { -0.01 });
}
assert!(!d.is_crisis()); }
#[test]
fn test_correlation_reset() {
let mut d = CorrelationBreakdownDetector::new(10, 0.3, 0.6).unwrap();
for i in 0..15 {
d.update(0, if i % 2 == 0 { 0.01 } else { -0.01 });
d.update(1, if i % 3 == 0 { 0.01 } else { -0.01 });
}
d.reset();
assert!(!d.is_crisis());
}
#[test]
fn test_pearson_r_perfect_correlation() {
let x = [1.0, 2.0, 3.0, 4.0, 5.0];
let r = pearson_r(&x, &x);
assert!((r - 1.0).abs() < 1e-10);
}
#[test]
fn test_pearson_r_perfect_anti_correlation() {
let x = [1.0, 2.0, 3.0, 4.0, 5.0];
let y: Vec<f64> = x.iter().map(|v| -v).collect();
let r = pearson_r(&x, &y);
assert!((r + 1.0).abs() < 1e-10);
}
#[test]
fn test_pearson_r_constant_series_returns_zero() {
let x = [1.0, 1.0, 1.0, 1.0];
let y = [2.0, 2.0, 2.0, 2.0];
let r = pearson_r(&x, &y);
assert_eq!(r, 0.0);
}
#[test]
fn test_regime_history_duration() {
let h = RegimeHistory {
regime: MarketRegime::Trending,
started_at_bar: 10,
confidence: 0.8,
ended_at_bar: Some(25),
};
assert_eq!(h.duration_bars(), Some(15));
assert!(!h.is_active());
}
#[test]
fn test_regime_history_active() {
let h = RegimeHistory {
regime: MarketRegime::Neutral,
started_at_bar: 5,
confidence: 0.5,
ended_at_bar: None,
};
assert!(h.is_active());
assert_eq!(h.duration_bars(), None);
}
#[test]
fn test_detector_period_validation() {
assert!(RegimeDetector::new(0, RegimeConfig::default()).is_err());
assert!(RegimeDetector::new(1, RegimeConfig::default()).is_err());
assert!(RegimeDetector::new(2, RegimeConfig::default()).is_ok());
}
#[test]
fn test_detector_unknown_before_warmup() {
let mut d = RegimeDetector::new(5, RegimeConfig::default()).unwrap();
let (regime, _) = d.update(&bar(102.0, 98.0, 100.0), &[]).unwrap();
assert_eq!(regime, MarketRegime::Unknown);
assert!(!d.is_ready());
}
#[test]
fn test_detector_bar_count() {
let mut d = RegimeDetector::new(5, RegimeConfig::default()).unwrap();
for i in 0..5 {
d.update(&bar(100.0 + i as f64, 99.0, 100.0 + i as f64), &[]).unwrap();
}
assert_eq!(d.bar_count(), 5);
}
#[test]
fn test_detector_reset_clears_state() {
let mut d = RegimeDetector::with_defaults(5).unwrap();
for i in 0..30 {
let c = 100.0 + i as f64;
d.update(&bar(c + 1.0, c - 1.0, c), &[]).unwrap();
}
d.reset();
assert!(!d.is_ready());
assert_eq!(d.bar_count(), 0);
assert!(d.history().is_empty());
}
#[test]
fn test_detector_history_populated_after_transition() {
let mut d = RegimeDetector::new(3, RegimeConfig::default()).unwrap();
for i in 0..40 {
let c = 100.0 + i as f64 * 0.1;
d.update(&bar(c + 0.2, c - 0.2, c), &[]).unwrap();
}
let _ = d.history(); }
#[test]
fn test_detector_garch_accessor() {
let d = RegimeDetector::with_defaults(5).unwrap();
assert!(d.garch().sigma() > 0.0);
}
#[test]
fn test_detector_no_panic_many_bars() {
let mut d = RegimeDetector::new(10, RegimeConfig::default()).unwrap();
for i in 0..200 {
let c = 100.0 + (i as f64 * 0.5).sin() * 5.0;
d.update(&bar(c + 1.0, c - 1.0, c), &[]).unwrap();
}
}
#[test]
fn test_legacy_detector_period_zero_fails() {
assert!(MarketRegimeDetector::new(0, RegimeConfig::default()).is_err());
assert!(MarketRegimeDetector::new(1, RegimeConfig::default()).is_err());
}
#[test]
fn test_legacy_unknown_before_warmup() {
let mut d = MarketRegimeDetector::new(5, RegimeConfig::default()).unwrap();
let regime = d.update(&bar(102.0, 98.0, 100.0)).unwrap();
assert_eq!(regime, MarketRegime::Unknown);
assert!(!d.is_ready());
}
#[test]
fn test_legacy_reset_clears_warmup() {
let mut d = MarketRegimeDetector::with_defaults(5).unwrap();
for i in 0..30 {
let h = 100.0 + i as f64;
d.update(&bar(h + 1.0, h - 1.0, h)).unwrap();
}
d.reset();
assert!(!d.is_ready());
}
#[test]
fn test_conditional_signal_invalid_period() {
assert!(RegimeConditionalSignal::new(0, 21, 14).is_err());
assert!(RegimeConditionalSignal::new(14, 0, 14).is_err());
assert!(RegimeConditionalSignal::new(14, 21, 0).is_err());
}
#[test]
fn test_conditional_signal_suppressed_in_crisis() {
let mut sig = RegimeConditionalSignal::new(5, 10, 7).unwrap();
let b = bar(102.0, 98.0, 100.0);
let result = sig.update(&b, MarketRegime::Crisis);
assert!(result.is_none());
}
#[test]
fn test_conditional_signal_suppressed_when_unknown() {
let mut sig = RegimeConditionalSignal::new(5, 10, 7).unwrap();
let b = bar(102.0, 98.0, 100.0);
let result = sig.update(&b, MarketRegime::Unknown);
assert!(result.is_none());
}
#[test]
fn test_conditional_signal_produces_value_after_warmup() {
let period = 5usize;
let mut sig = RegimeConditionalSignal::new(period, period + 2, period).unwrap();
let mut last_val = None;
for i in 0..((period + 2) * 3) {
let c = 100.0 + i as f64 * 0.1;
last_val = sig.update(&bar(c + 0.5, c - 0.5, c), MarketRegime::Trending);
}
if let Some(Ok(rsi_val)) = last_val {
assert!(rsi_val >= 0.0 && rsi_val <= 100.0);
}
}
#[test]
fn test_conditional_signal_reset() {
let mut sig = RegimeConditionalSignal::new(5, 10, 7).unwrap();
let b = bar(102.0, 98.0, 100.0);
for _ in 0..30 {
let _ = sig.update(&b, MarketRegime::Neutral);
}
sig.reset();
assert!(!sig.is_ready());
}
}