#[cfg(feature = "serde")]
use serde::Serialize;
use super::regime::{RegimeReading, RegimeState};
use crate::indicator::smoothing::SmootherKind;
use crate::indicator::smoothing::{Jma, Smoother};
use crate::Bar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum TrendDirection {
Up,
Flat,
Down,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum MarketPhase {
StrongUp,
Up,
Range,
Down,
StrongDown,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct TrendReading {
pub direction: TrendDirection,
pub phase: MarketPhase,
pub slope_pct: f64,
}
#[allow(clippy::too_many_arguments)]
pub fn trend_reading(
bars: &[Bar],
regime: &RegimeReading,
kind: SmootherKind,
len: usize,
jma_phase: f64,
jma_power: f64,
deadband_pct: f64,
) -> Option<TrendReading> {
if len < 1 {
return None;
}
let series = smoothed_series(bars, kind, len, jma_phase, jma_power);
if series.len() < len + 1 {
return None;
}
let last = series[series.len() - 1];
let prior = series[series.len() - 1 - len];
if prior == 0.0 {
return None;
}
let slope_pct = 100.0 * (last / prior - 1.0);
let direction = if slope_pct > deadband_pct {
TrendDirection::Up
} else if slope_pct < -deadband_pct {
TrendDirection::Down
} else {
TrendDirection::Flat
};
let phase = match direction {
TrendDirection::Flat => MarketPhase::Range,
TrendDirection::Up if regime.state == RegimeState::Trending => MarketPhase::StrongUp,
TrendDirection::Up => MarketPhase::Up,
TrendDirection::Down if regime.state == RegimeState::Trending => MarketPhase::StrongDown,
TrendDirection::Down => MarketPhase::Down,
};
Some(TrendReading {
direction,
phase,
slope_pct,
})
}
fn build_smoother(
kind: SmootherKind,
len: usize,
jma_phase: f64,
jma_power: f64,
) -> Box<dyn Smoother> {
match kind {
SmootherKind::Jma => Box::new(Jma::new(len, jma_phase, jma_power)),
other => other.build(len),
}
}
fn smoothed_series(
bars: &[Bar],
kind: SmootherKind,
len: usize,
jma_phase: f64,
jma_power: f64,
) -> Vec<f64> {
let mut smoother = build_smoother(kind, len, jma_phase, jma_power);
bars.iter()
.filter_map(|b| smoother.update(b.close))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn bar(c: f64) -> Bar {
Bar {
timestamp: 0,
open: c,
high: c + 0.5,
low: c - 0.5,
close: c,
volume: 0.0,
}
}
fn trending_regime() -> RegimeReading {
RegimeReading {
state: RegimeState::Trending,
adx: 30.0,
choppiness: 30.0,
efficiency: 0.8,
trend_votes: 3,
}
}
fn ranging_regime() -> RegimeReading {
RegimeReading {
state: RegimeState::Ranging,
adx: 15.0,
choppiness: 70.0,
efficiency: 0.2,
trend_votes: 0,
}
}
#[test]
fn ramp_with_trending_regime_is_strong_up() {
let bars: Vec<Bar> = (0..60).map(|i| bar(100.0 + i as f64)).collect();
let r = trend_reading(
&bars,
&trending_regime(),
SmootherKind::Ema,
20,
0.0,
2.0,
0.1,
)
.expect("enough bars");
assert_eq!(r.direction, TrendDirection::Up);
assert_eq!(r.phase, MarketPhase::StrongUp);
assert!(r.slope_pct > 0.0);
}
#[test]
fn ramp_with_ranging_regime_is_weak_up() {
let bars: Vec<Bar> = (0..60).map(|i| bar(100.0 + i as f64)).collect();
let r = trend_reading(
&bars,
&ranging_regime(),
SmootherKind::Ema,
20,
0.0,
2.0,
0.1,
)
.expect("enough bars");
assert_eq!(r.direction, TrendDirection::Up);
assert_eq!(r.phase, MarketPhase::Up);
}
#[test]
fn flat_price_is_range_regardless_of_regime() {
let bars: Vec<Bar> = (0..60).map(|_| bar(100.0)).collect();
let r = trend_reading(
&bars,
&trending_regime(),
SmootherKind::Ema,
20,
0.0,
2.0,
0.1,
)
.expect("enough bars");
assert_eq!(r.direction, TrendDirection::Flat);
assert_eq!(r.phase, MarketPhase::Range);
}
#[test]
fn downtrend_with_trending_regime_is_strong_down() {
let bars: Vec<Bar> = (0..60).map(|i| bar(200.0 - i as f64)).collect();
let r = trend_reading(
&bars,
&trending_regime(),
SmootherKind::Ema,
20,
0.0,
2.0,
0.1,
)
.expect("enough bars");
assert_eq!(r.direction, TrendDirection::Down);
assert_eq!(r.phase, MarketPhase::StrongDown);
}
#[test]
fn insufficient_bars_is_none() {
let bars: Vec<Bar> = (0..5).map(|_| bar(100.0)).collect();
assert!(trend_reading(
&bars,
&trending_regime(),
SmootherKind::Ema,
20,
0.0,
2.0,
0.1
)
.is_none());
}
#[test]
fn jma_kernel_also_produces_a_reading() {
let bars: Vec<Bar> = (0..60).map(|i| bar(100.0 + i as f64)).collect();
let r = trend_reading(
&bars,
&trending_regime(),
SmootherKind::Jma,
20,
0.0,
2.0,
0.1,
)
.expect("jma is valid from the first sample");
assert_eq!(r.direction, TrendDirection::Up);
}
}