Market regime
classify_regime sorts the market into one of four regimes from a 20-bar moving average, ADX and ATR. The rules are few and fixed, so the result is easy to explain.
The four regimes
MarketRegime has four variants:
| Regime | Meaning |
|---|---|
BullishExpansion |
A strong trend, pointing up, with price above its average |
BearishExpansion |
A strong trend, pointing down, with price below its average |
Consolidation |
No strong trend and low volatility |
Transition |
Everything in between, and the warmup |
The rules
pub fn classify_regime(bars: &[Bar], adx_val: f64, atr_val: f64) -> MarketRegime
bars is the history up to and including the current bar. adx_val is the current ADX value,
atr_val the current ATR as a fraction of price (ATR / close).
- Fewer than 21 bars:
Transition. - The function computes the 20-bar simple moving average of the close, for the current and the
previous bar, and its slope:
(sma_20 - prev_sma_20) / prev_sma_20. - Trending means
adx_val > 20.- Slope above
0.001and close above the average:BullishExpansion. - Slope below
-0.001and close below the average:BearishExpansion. - Otherwise:
Transition.
- Slope above
- Not trending:
atr_val > 0.02givesTransition, anything elseConsolidation.
ATR units
The atr indicator reports 100 * ATR / close, a percentage. classify_regime expects the
fraction, so divide by 100 before passing it on. The crate’s own pipeline does the same.
use kestrel_chartkit::{build_checked, classify_regime, Bar};
use std::collections::HashMap;
fn regimes(bars: &[Bar]) -> Result<(), Box<dyn std::error::Error>> {
let mut adx = build_checked("adx", &HashMap::new())?;
let mut atr = build_checked("atr", &HashMap::new())?;
for (i, bar) in bars.iter().enumerate() {
let (Some(a), Some(t)) = (adx.on_bar(bar), atr.on_bar(bar)) else {
continue; // still warming up
};
let regime = classify_regime(&bars[..=i], a.value, t.value / 100.0);
println!("{} {}", bar.timestamp, regime);
}
Ok(())
}
The market regime chart in the showcase runs exactly this loop over synthetic bars and shades every run of equal regimes.
Building on the regime
regime_advanced holds small, independent helpers for consumers that track the regime over time:
RegimeMarkovModelcounts observed transitions and returnstransition_probability(from, to)andnext_state_distribution(from).RegimePersistenceTrackerreports how long the current regime has lasted.HysteresisBandswitches levels only after a value has crossed separate enter and exit thresholds, so a reading that hovers at a boundary does not flip back and forth.
The composite scoring uses the regime to grade a signal: see Composite scoring.
The analytics module has its own on-demand regime vote with different inputs. It is a separate
function, not a wrapper around classify_regime.