F11: add SuperTrend, Chandelier Exit, Chande Kroll Stop and ATR Trailing Stop
- Rust core: super_trend.rs (SuperTrend — ATR-banded trailing stop with
flip logic; SuperTrendOutput { value, direction }), chandelier_exit.rs
(Chandelier Exit — ATR stop hung off the window's highest high / lowest
low; ChandelierExitOutput { long_stop, short_stop }),
chande_kroll_stop.rs (Chande Kroll Stop — a two-stage ATR stop;
ChandeKrollStopOutput { stop_long, stop_short }), atr_trailing_stop.rs
(ATR Trailing Stop — a single ratcheting close-based stop). Each with a
full Indicator impl, runnable doctest and reference / property / warmup
/ reset / batch==streaming tests.
- Python: PySuperTrend / PyChandelierExit / PyChandeKrollStop /
PyAtrTrailingStop PyO3 classes (struct outputs as tuples and (n, 2)
arrays) + module registration + .pyi stubs.
- Node: explicit SuperTrendNode / ChandelierExitNode / ChandeKrollStopNode
/ AtrTrailingStopNode with SuperTrendValue / ChandelierExitValue /
ChandeKrollStopValue objects; index.d.ts and index.js updated.
- WASM: WasmSuperTrend / WasmChandelierExit / WasmChandeKrollStop /
WasmAtrTrailingStop.
- Wiki: Indicator-SuperTrend/ChandelierExit/ChandeKrollStop/
AtrTrailingStop.md plus rows in the "Trailing stop" table of
Indicators-Overview.md and entries in Home.md.
- Add clippy.toml with doc-valid-idents for the proper noun "LeBeau".
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 427 core tests,
25 data tests and 61 doctests green.
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
//! ATR Trailing Stop.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::atr::Atr;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// ATR Trailing Stop — a stop level that trails price by a fixed ATR multiple
|
||||
/// and ratchets in the direction of the trend.
|
||||
///
|
||||
/// ```text
|
||||
/// loss = multiplier · ATR
|
||||
///
|
||||
/// stop_t = max(stop_{t−1}, close − loss) while price holds above the stop
|
||||
/// = min(stop_{t−1}, close + loss) while price holds below the stop
|
||||
/// = close − loss on a fresh break above the stop
|
||||
/// = close + loss on a fresh break below the stop
|
||||
/// ```
|
||||
///
|
||||
/// While price stays on one side of the stop the level only ratchets toward
|
||||
/// price — up in an uptrend, down in a downtrend — never away from it. When a
|
||||
/// close crosses the stop the level snaps to the opposite side, `loss` away
|
||||
/// from the new close, flipping the trade. This is the trailing stop used by
|
||||
/// the well-known "UT Bot"; the first ATR-ready bar seeds the stop below
|
||||
/// price (a long).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, AtrTrailingStop};
|
||||
///
|
||||
/// let mut indicator = AtrTrailingStop::new(14, 3.0).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AtrTrailingStop {
|
||||
atr: Atr,
|
||||
multiplier: f64,
|
||||
atr_period: usize,
|
||||
prev_close: Option<f64>,
|
||||
prev_stop: Option<f64>,
|
||||
}
|
||||
|
||||
impl AtrTrailingStop {
|
||||
/// Construct an ATR Trailing Stop with an explicit ATR period and multiple.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if `atr_period == 0` and
|
||||
/// [`Error::NonPositiveMultiplier`] if `multiplier` is not strictly
|
||||
/// positive and finite.
|
||||
pub fn new(atr_period: usize, multiplier: f64) -> Result<Self> {
|
||||
if !multiplier.is_finite() || multiplier <= 0.0 {
|
||||
return Err(Error::NonPositiveMultiplier);
|
||||
}
|
||||
Ok(Self {
|
||||
atr: Atr::new(atr_period)?,
|
||||
multiplier,
|
||||
atr_period,
|
||||
prev_close: None,
|
||||
prev_stop: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A common configuration: `ATR(14)` with a `3.0` multiplier.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(14, 3.0).expect("classic ATR Trailing Stop params are valid")
|
||||
}
|
||||
|
||||
/// Configured `(atr_period, multiplier)`.
|
||||
pub const fn params(&self) -> (usize, f64) {
|
||||
(self.atr_period, self.multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for AtrTrailingStop {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let atr = self.atr.update(candle)?;
|
||||
let loss = self.multiplier * atr;
|
||||
let close = candle.close;
|
||||
|
||||
let stop = match (self.prev_stop, self.prev_close) {
|
||||
(Some(prev_stop), Some(prev_close)) => {
|
||||
if close > prev_stop && prev_close > prev_stop {
|
||||
// Holding above the stop — ratchet it up only.
|
||||
(close - loss).max(prev_stop)
|
||||
} else if close < prev_stop && prev_close < prev_stop {
|
||||
// Holding below the stop — ratchet it down only.
|
||||
(close + loss).min(prev_stop)
|
||||
} else if close > prev_stop {
|
||||
// Fresh break above — place the stop below the new close.
|
||||
close - loss
|
||||
} else {
|
||||
// Fresh break below — place the stop above the new close.
|
||||
close + loss
|
||||
}
|
||||
}
|
||||
// First ATR-ready bar: seed the stop below price (a long).
|
||||
_ => close - loss,
|
||||
};
|
||||
|
||||
self.prev_close = Some(close);
|
||||
self.prev_stop = Some(stop);
|
||||
Some(stop)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.atr.reset();
|
||||
self.prev_close = None;
|
||||
self.prev_stop = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.atr_period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.prev_stop.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"AtrTrailingStop"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new((high + low) / 2.0, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_values_flat_market() {
|
||||
// Flat candles H=11, L=9, C=10 -> TR=2 -> ATR=2; loss = 3·2 = 6.
|
||||
// Seed stop = close - loss = 10 - 6 = 4, and it holds there.
|
||||
let candles: Vec<Candle> = (0..20).map(|i| c(11.0, 9.0, 10.0, i)).collect();
|
||||
let mut ts = AtrTrailingStop::new(5, 3.0).unwrap();
|
||||
for v in ts.batch(&candles).into_iter().flatten() {
|
||||
assert_relative_eq!(v, 4.0, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uptrend_stop_ratchets_up_and_stays_below_price() {
|
||||
let candles: Vec<Candle> = (0..50)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut ts = AtrTrailingStop::new(14, 3.0).unwrap();
|
||||
let emitted: Vec<(f64, f64)> = ts
|
||||
.batch(&candles)
|
||||
.into_iter()
|
||||
.zip(candles.iter())
|
||||
.filter_map(|(o, c)| o.map(|v| (v, c.close)))
|
||||
.collect();
|
||||
for w in emitted.windows(2) {
|
||||
assert!(
|
||||
w[1].0 >= w[0].0 - 1e-9,
|
||||
"stop must not loosen in an uptrend"
|
||||
);
|
||||
}
|
||||
for &(stop, close) in &emitted {
|
||||
assert!(stop < close, "uptrend stop should sit below the close");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_flips_to_the_other_side_when_price_reverses() {
|
||||
let mut candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
// A steep decline drags price through the trailing stop.
|
||||
candles.extend((0..40).map(|i| {
|
||||
let base = 140.0 - 3.0 * i as f64;
|
||||
c(base + 1.0, base - 1.0, base, 40 + i)
|
||||
}));
|
||||
let mut ts = AtrTrailingStop::new(14, 3.0).unwrap();
|
||||
let paired: Vec<(f64, f64)> = ts
|
||||
.batch(&candles)
|
||||
.into_iter()
|
||||
.zip(candles.iter())
|
||||
.filter_map(|(o, c)| o.map(|v| (v, c.close)))
|
||||
.collect();
|
||||
assert!(
|
||||
paired.iter().any(|&(stop, close)| stop < close),
|
||||
"expected a long stretch with the stop below price"
|
||||
);
|
||||
assert!(
|
||||
paired.iter().any(|&(stop, close)| stop > close),
|
||||
"expected the stop to flip above price after the reversal"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_matches_warmup_period() {
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut ts = AtrTrailingStop::new(8, 3.0).unwrap();
|
||||
let out = ts.batch(&candles);
|
||||
assert_eq!(ts.warmup_period(), 8);
|
||||
for (i, v) in out.iter().enumerate().take(7) {
|
||||
assert!(v.is_none(), "index {i} must be None during warmup");
|
||||
}
|
||||
assert!(out[7].is_some(), "first value lands at warmup_period - 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_params() {
|
||||
assert!(AtrTrailingStop::new(0, 3.0).is_err());
|
||||
assert!(AtrTrailingStop::new(14, 0.0).is_err());
|
||||
assert!(AtrTrailingStop::new(14, -1.0).is_err());
|
||||
assert!(AtrTrailingStop::new(14, f64::NAN).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut ts = AtrTrailingStop::classic();
|
||||
ts.batch(&candles);
|
||||
assert!(ts.is_ready());
|
||||
ts.reset();
|
||||
assert!(!ts.is_ready());
|
||||
assert_eq!(ts.update(candles[0]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..80)
|
||||
.map(|i| {
|
||||
let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
|
||||
c(mid + 1.5, mid - 1.5, mid + 0.5, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = AtrTrailingStop::classic();
|
||||
let mut b = AtrTrailingStop::classic();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
//! Chande Kroll Stop.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::atr::Atr;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Chande Kroll Stop output: the long-side and short-side stop levels.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ChandeKrollStopOutput {
|
||||
/// Long-position stop — the lowest preliminary low-stop over `stop_period`.
|
||||
pub stop_long: f64,
|
||||
/// Short-position stop — the highest preliminary high-stop over `stop_period`.
|
||||
pub stop_short: f64,
|
||||
}
|
||||
|
||||
/// Chande Kroll Stop — Tushar Chande and Stanley Kroll's two-stage ATR stop.
|
||||
///
|
||||
/// ```text
|
||||
/// preliminary (window p = atr_period, x = atr_multiplier):
|
||||
/// high_stop = highest_high(p) − x · ATR(p)
|
||||
/// low_stop = lowest_low(p) + x · ATR(p)
|
||||
///
|
||||
/// final (window q = stop_period):
|
||||
/// stop_short = highest(high_stop, q)
|
||||
/// stop_long = lowest(low_stop, q)
|
||||
/// ```
|
||||
///
|
||||
/// The first stage builds an ATR stop off the recent extreme, exactly like a
|
||||
/// [`ChandelierExit`](crate::ChandelierExit); the second stage smooths it by
|
||||
/// taking the most extreme preliminary stop over a shorter window, which keeps
|
||||
/// the stop from whipsawing on a single wide bar. The classic configuration
|
||||
/// from *The New Technical Trader* is `ATR(10)`, multiplier `1.0`, smoothing
|
||||
/// window `9`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, ChandeKrollStop};
|
||||
///
|
||||
/// let mut indicator = ChandeKrollStop::new(10, 1.0, 9).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChandeKrollStop {
|
||||
atr_period: usize,
|
||||
atr_multiplier: f64,
|
||||
stop_period: usize,
|
||||
atr: Atr,
|
||||
highs: VecDeque<f64>,
|
||||
lows: VecDeque<f64>,
|
||||
high_stops: VecDeque<f64>,
|
||||
low_stops: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl ChandeKrollStop {
|
||||
/// Construct a Chande Kroll Stop with explicit ATR and smoothing windows.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if `atr_period` or `stop_period` is zero,
|
||||
/// and [`Error::NonPositiveMultiplier`] if `atr_multiplier` is not strictly
|
||||
/// positive and finite.
|
||||
pub fn new(atr_period: usize, atr_multiplier: f64, stop_period: usize) -> Result<Self> {
|
||||
if !atr_multiplier.is_finite() || atr_multiplier <= 0.0 {
|
||||
return Err(Error::NonPositiveMultiplier);
|
||||
}
|
||||
if stop_period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
atr_period,
|
||||
atr_multiplier,
|
||||
stop_period,
|
||||
atr: Atr::new(atr_period)?,
|
||||
highs: VecDeque::with_capacity(atr_period),
|
||||
lows: VecDeque::with_capacity(atr_period),
|
||||
high_stops: VecDeque::with_capacity(stop_period),
|
||||
low_stops: VecDeque::with_capacity(stop_period),
|
||||
})
|
||||
}
|
||||
|
||||
/// The classic configuration: `ATR(10)`, multiplier `1.0`, window `9`.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(10, 1.0, 9).expect("classic Chande Kroll Stop params are valid")
|
||||
}
|
||||
|
||||
/// Configured `(atr_period, atr_multiplier, stop_period)`.
|
||||
pub const fn params(&self) -> (usize, f64, usize) {
|
||||
(self.atr_period, self.atr_multiplier, self.stop_period)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for ChandeKrollStop {
|
||||
type Input = Candle;
|
||||
type Output = ChandeKrollStopOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<ChandeKrollStopOutput> {
|
||||
let atr = self.atr.update(candle);
|
||||
if self.highs.len() == self.atr_period {
|
||||
self.highs.pop_front();
|
||||
self.lows.pop_front();
|
||||
}
|
||||
self.highs.push_back(candle.high);
|
||||
self.lows.push_back(candle.low);
|
||||
if self.highs.len() < self.atr_period {
|
||||
return None;
|
||||
}
|
||||
// ATR(atr_period) becomes ready on exactly the candle that fills the
|
||||
// preliminary window, so this never discards a value.
|
||||
let atr = atr?;
|
||||
let highest = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
|
||||
let lowest = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
|
||||
let high_stop = highest - self.atr_multiplier * atr;
|
||||
let low_stop = lowest + self.atr_multiplier * atr;
|
||||
|
||||
if self.high_stops.len() == self.stop_period {
|
||||
self.high_stops.pop_front();
|
||||
self.low_stops.pop_front();
|
||||
}
|
||||
self.high_stops.push_back(high_stop);
|
||||
self.low_stops.push_back(low_stop);
|
||||
if self.high_stops.len() < self.stop_period {
|
||||
return None;
|
||||
}
|
||||
let stop_short = self
|
||||
.high_stops
|
||||
.iter()
|
||||
.copied()
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
let stop_long = self.low_stops.iter().copied().fold(f64::INFINITY, f64::min);
|
||||
Some(ChandeKrollStopOutput {
|
||||
stop_long,
|
||||
stop_short,
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.atr.reset();
|
||||
self.highs.clear();
|
||||
self.lows.clear();
|
||||
self.high_stops.clear();
|
||||
self.low_stops.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// The preliminary stop first appears on candle `atr_period`; the
|
||||
// smoothing window then needs `stop_period` of them.
|
||||
self.atr_period + self.stop_period - 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.high_stops.len() == self.stop_period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ChandeKrollStop"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new((high + low) / 2.0, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_values_flat_market() {
|
||||
// Flat candles H=11, L=9, C=10 -> TR=2 -> ATR=2; HH=11, LL=9.
|
||||
// high_stop = 11 - 1·2 = 9; low_stop = 9 + 1·2 = 11.
|
||||
// stop_short = highest(high_stop, q) = 9; stop_long = lowest(low_stop, q) = 11.
|
||||
let candles: Vec<Candle> = (0..20).map(|i| c(11.0, 9.0, 10.0, i)).collect();
|
||||
let mut cks = ChandeKrollStop::new(5, 1.0, 3).unwrap();
|
||||
let last = cks.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
assert_relative_eq!(last.stop_short, 9.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(last.stop_long, 11.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_matches_warmup_period() {
|
||||
let candles: Vec<Candle> = (0..16)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut cks = ChandeKrollStop::new(4, 1.0, 3).unwrap();
|
||||
let out = cks.batch(&candles);
|
||||
assert_eq!(cks.warmup_period(), 6);
|
||||
for (i, v) in out.iter().enumerate().take(5) {
|
||||
assert!(v.is_none(), "index {i} must be None during warmup");
|
||||
}
|
||||
assert!(out[5].is_some(), "first value lands at warmup_period - 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_params() {
|
||||
assert!(ChandeKrollStop::new(0, 1.0, 9).is_err());
|
||||
assert!(ChandeKrollStop::new(10, 1.0, 0).is_err());
|
||||
assert!(ChandeKrollStop::new(10, 0.0, 9).is_err());
|
||||
assert!(ChandeKrollStop::new(10, -1.0, 9).is_err());
|
||||
assert!(ChandeKrollStop::new(10, f64::NAN, 9).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut cks = ChandeKrollStop::classic();
|
||||
cks.batch(&candles);
|
||||
assert!(cks.is_ready());
|
||||
cks.reset();
|
||||
assert!(!cks.is_ready());
|
||||
assert_eq!(cks.update(candles[0]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..80)
|
||||
.map(|i| {
|
||||
let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
|
||||
c(mid + 1.5, mid - 1.5, mid + 0.5, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = ChandeKrollStop::classic();
|
||||
let mut b = ChandeKrollStop::classic();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! Chandelier Exit.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::atr::Atr;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Chandelier Exit output: the long-side and short-side trailing stops.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ChandelierExitOutput {
|
||||
/// Long-position stop: `highest_high − multiplier · ATR`.
|
||||
pub long_stop: f64,
|
||||
/// Short-position stop: `lowest_low + multiplier · ATR`.
|
||||
pub short_stop: f64,
|
||||
}
|
||||
|
||||
/// Chandelier Exit — Chuck LeBeau's ATR trailing stop, hung from the highest
|
||||
/// high (for longs) or the lowest low (for shorts) of the lookback window.
|
||||
///
|
||||
/// ```text
|
||||
/// long_stop = highest_high(period) − multiplier · ATR(period)
|
||||
/// short_stop = lowest_low(period) + multiplier · ATR(period)
|
||||
/// ```
|
||||
///
|
||||
/// A long position is exited when price closes below `long_stop`; a short
|
||||
/// when it closes above `short_stop`. Because the stop hangs a fixed number
|
||||
/// of ATRs off the extreme of the window — like a chandelier off a ceiling —
|
||||
/// it follows price up but never loosens. LeBeau's classic configuration is a
|
||||
/// `22`-bar window with a `3.0` multiplier.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, ChandelierExit};
|
||||
///
|
||||
/// let mut indicator = ChandelierExit::new(22, 3.0).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChandelierExit {
|
||||
period: usize,
|
||||
multiplier: f64,
|
||||
atr: Atr,
|
||||
highs: VecDeque<f64>,
|
||||
lows: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl ChandelierExit {
|
||||
/// Construct a Chandelier Exit with an explicit window and band multiplier.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0` and
|
||||
/// [`Error::NonPositiveMultiplier`] if `multiplier` is not strictly
|
||||
/// positive and finite.
|
||||
pub fn new(period: usize, multiplier: f64) -> Result<Self> {
|
||||
if !multiplier.is_finite() || multiplier <= 0.0 {
|
||||
return Err(Error::NonPositiveMultiplier);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
multiplier,
|
||||
atr: Atr::new(period)?,
|
||||
highs: VecDeque::with_capacity(period),
|
||||
lows: VecDeque::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// LeBeau's classic configuration: a `22`-bar window, `3.0` multiplier.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(22, 3.0).expect("classic Chandelier Exit params are valid")
|
||||
}
|
||||
|
||||
/// Configured `(period, multiplier)`.
|
||||
pub const fn params(&self) -> (usize, f64) {
|
||||
(self.period, self.multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for ChandelierExit {
|
||||
type Input = Candle;
|
||||
type Output = ChandelierExitOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<ChandelierExitOutput> {
|
||||
let atr = self.atr.update(candle);
|
||||
if self.highs.len() == self.period {
|
||||
self.highs.pop_front();
|
||||
self.lows.pop_front();
|
||||
}
|
||||
self.highs.push_back(candle.high);
|
||||
self.lows.push_back(candle.low);
|
||||
if self.highs.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
// ATR(period) becomes ready on exactly the candle that fills the
|
||||
// highest-high / lowest-low window, so this never discards a value.
|
||||
let atr = atr?;
|
||||
let highest = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
|
||||
let lowest = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
|
||||
Some(ChandelierExitOutput {
|
||||
long_stop: highest - self.multiplier * atr,
|
||||
short_stop: lowest + self.multiplier * atr,
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.atr.reset();
|
||||
self.highs.clear();
|
||||
self.lows.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.highs.len() == self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ChandelierExit"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new((high + low) / 2.0, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_values_flat_market() {
|
||||
// Flat candles H=11, L=9, C=10 -> TR=2 -> ATR=2; HH=11, LL=9.
|
||||
// long_stop = 11 - 3·2 = 5; short_stop = 9 + 3·2 = 15.
|
||||
let candles: Vec<Candle> = (0..20).map(|i| c(11.0, 9.0, 10.0, i)).collect();
|
||||
let mut ce = ChandelierExit::new(5, 3.0).unwrap();
|
||||
let last = ce.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
assert_relative_eq!(last.long_stop, 5.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(last.short_stop, 15.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_stop_below_highest_short_stop_above_lowest() {
|
||||
let candles: Vec<Candle> = (0..120)
|
||||
.map(|i| {
|
||||
let mid = 100.0 + (i as f64 * 0.2).sin() * 9.0;
|
||||
c(mid + 1.5, mid - 1.5, mid + 0.4, i)
|
||||
})
|
||||
.collect();
|
||||
let mut ce = ChandelierExit::classic();
|
||||
for (i, o) in ce.batch(&candles).into_iter().enumerate() {
|
||||
if let Some(o) = o {
|
||||
// The window's extremes bound the stops from one side.
|
||||
let win = &candles[i + 1 - 22..=i];
|
||||
let hh = win.iter().map(|c| c.high).fold(f64::NEG_INFINITY, f64::max);
|
||||
let ll = win.iter().map(|c| c.low).fold(f64::INFINITY, f64::min);
|
||||
assert!(o.long_stop <= hh + 1e-9);
|
||||
assert!(o.short_stop >= ll - 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_matches_warmup_period() {
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut ce = ChandelierExit::new(8, 3.0).unwrap();
|
||||
let out = ce.batch(&candles);
|
||||
assert_eq!(ce.warmup_period(), 8);
|
||||
for (i, v) in out.iter().enumerate().take(7) {
|
||||
assert!(v.is_none(), "index {i} must be None during warmup");
|
||||
}
|
||||
assert!(out[7].is_some(), "first value lands at warmup_period - 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_params() {
|
||||
assert!(ChandelierExit::new(0, 3.0).is_err());
|
||||
assert!(ChandelierExit::new(22, 0.0).is_err());
|
||||
assert!(ChandelierExit::new(22, -1.0).is_err());
|
||||
assert!(ChandelierExit::new(22, f64::NAN).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut ce = ChandelierExit::classic();
|
||||
ce.batch(&candles);
|
||||
assert!(ce.is_ready());
|
||||
ce.reset();
|
||||
assert!(!ce.is_ready());
|
||||
assert_eq!(ce.update(candles[0]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..80)
|
||||
.map(|i| {
|
||||
let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
|
||||
c(mid + 1.5, mid - 1.5, mid + 0.5, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = ChandelierExit::classic();
|
||||
let mut b = ChandelierExit::classic();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,14 @@ mod adx;
|
||||
mod aroon;
|
||||
mod aroon_oscillator;
|
||||
mod atr;
|
||||
mod atr_trailing_stop;
|
||||
mod awesome_oscillator;
|
||||
mod bollinger;
|
||||
mod bollinger_bandwidth;
|
||||
mod cci;
|
||||
mod chaikin_oscillator;
|
||||
mod chande_kroll_stop;
|
||||
mod chandelier_exit;
|
||||
mod cmf;
|
||||
mod cmo;
|
||||
mod coppock;
|
||||
@@ -44,6 +47,7 @@ mod smma;
|
||||
mod std_dev;
|
||||
mod stoch_rsi;
|
||||
mod stochastic;
|
||||
mod super_trend;
|
||||
mod t3;
|
||||
mod tema;
|
||||
mod trima;
|
||||
@@ -64,11 +68,14 @@ pub use adx::{Adx, AdxOutput};
|
||||
pub use aroon::{Aroon, AroonOutput};
|
||||
pub use aroon_oscillator::AroonOscillator;
|
||||
pub use atr::Atr;
|
||||
pub use atr_trailing_stop::AtrTrailingStop;
|
||||
pub use awesome_oscillator::AwesomeOscillator;
|
||||
pub use bollinger::{BollingerBands, BollingerOutput};
|
||||
pub use bollinger_bandwidth::BollingerBandwidth;
|
||||
pub use cci::Cci;
|
||||
pub use chaikin_oscillator::ChaikinOscillator;
|
||||
pub use chande_kroll_stop::{ChandeKrollStop, ChandeKrollStopOutput};
|
||||
pub use chandelier_exit::{ChandelierExit, ChandelierExitOutput};
|
||||
pub use cmf::ChaikinMoneyFlow;
|
||||
pub use cmo::Cmo;
|
||||
pub use coppock::Coppock;
|
||||
@@ -99,6 +106,7 @@ pub use smma::Smma;
|
||||
pub use std_dev::StdDev;
|
||||
pub use stoch_rsi::StochRsi;
|
||||
pub use stochastic::{Stochastic, StochasticOutput};
|
||||
pub use super_trend::{SuperTrend, SuperTrendOutput};
|
||||
pub use t3::T3;
|
||||
pub use tema::Tema;
|
||||
pub use trima::Trima;
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
//! `SuperTrend`.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::atr::Atr;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// `SuperTrend` output: the trailing-stop level and the trend direction.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct SuperTrendOutput {
|
||||
/// The `SuperTrend` line — the active trailing-stop level for this bar.
|
||||
pub value: f64,
|
||||
/// Trend direction: `+1.0` in an uptrend (the line sits below price),
|
||||
/// `-1.0` in a downtrend (the line sits above price).
|
||||
pub direction: f64,
|
||||
}
|
||||
|
||||
/// Previous-bar state carried forward by the `SuperTrend` recurrence.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PrevState {
|
||||
final_upper: f64,
|
||||
final_lower: f64,
|
||||
close: f64,
|
||||
direction: f64,
|
||||
}
|
||||
|
||||
/// `SuperTrend` — an ATR-banded trailing stop that flips sides on a close
|
||||
/// through the band.
|
||||
///
|
||||
/// ```text
|
||||
/// hl2 = (high + low) / 2
|
||||
/// basic_upper = hl2 + multiplier · ATR
|
||||
/// basic_lower = hl2 − multiplier · ATR
|
||||
///
|
||||
/// final_upper = basic_upper if basic_upper < prev_final_upper or prev_close > prev_final_upper
|
||||
/// else prev_final_upper
|
||||
/// final_lower = basic_lower if basic_lower > prev_final_lower or prev_close < prev_final_lower
|
||||
/// else prev_final_lower
|
||||
///
|
||||
/// in a downtrend: stay down while close <= final_upper, else flip up
|
||||
/// in an uptrend: stay up while close >= final_lower, else flip down
|
||||
/// SuperTrend = final_lower in an uptrend, final_upper in a downtrend
|
||||
/// ```
|
||||
///
|
||||
/// The final bands ratchet — the upper band only moves down (and the lower
|
||||
/// band only moves up) until price closes through it, which flips the trend
|
||||
/// and hands the role of trailing stop to the opposite band. The first
|
||||
/// ATR-ready bar seeds the trend as up. Wilder's classic configuration is
|
||||
/// `ATR(10)` with a `3.0` multiplier.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, SuperTrend};
|
||||
///
|
||||
/// let mut indicator = SuperTrend::classic();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..80 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SuperTrend {
|
||||
atr: Atr,
|
||||
multiplier: f64,
|
||||
atr_period: usize,
|
||||
prev: Option<PrevState>,
|
||||
}
|
||||
|
||||
impl SuperTrend {
|
||||
/// Construct a `SuperTrend` with an explicit ATR period and band multiplier.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::PeriodZero`] if `atr_period == 0` and
|
||||
/// [`Error::NonPositiveMultiplier`] if `multiplier` is not strictly
|
||||
/// positive and finite.
|
||||
pub fn new(atr_period: usize, multiplier: f64) -> Result<Self> {
|
||||
if !multiplier.is_finite() || multiplier <= 0.0 {
|
||||
return Err(Error::NonPositiveMultiplier);
|
||||
}
|
||||
Ok(Self {
|
||||
atr: Atr::new(atr_period)?,
|
||||
multiplier,
|
||||
atr_period,
|
||||
prev: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Wilder's classic configuration: `ATR(10)` with a `3.0` multiplier.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(10, 3.0).expect("classic SuperTrend params are valid")
|
||||
}
|
||||
|
||||
/// Configured `(atr_period, multiplier)`.
|
||||
pub const fn params(&self) -> (usize, f64) {
|
||||
(self.atr_period, self.multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for SuperTrend {
|
||||
type Input = Candle;
|
||||
type Output = SuperTrendOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<SuperTrendOutput> {
|
||||
let atr = self.atr.update(candle)?;
|
||||
let hl2 = (candle.high + candle.low) / 2.0;
|
||||
let basic_upper = hl2 + self.multiplier * atr;
|
||||
let basic_lower = hl2 - self.multiplier * atr;
|
||||
|
||||
let (final_upper, final_lower, direction) = match self.prev {
|
||||
None => {
|
||||
// First ATR-ready bar: no prior bands, seed the trend as up.
|
||||
(basic_upper, basic_lower, 1.0)
|
||||
}
|
||||
Some(p) => {
|
||||
let final_upper = if basic_upper < p.final_upper || p.close > p.final_upper {
|
||||
basic_upper
|
||||
} else {
|
||||
p.final_upper
|
||||
};
|
||||
let final_lower = if basic_lower > p.final_lower || p.close < p.final_lower {
|
||||
basic_lower
|
||||
} else {
|
||||
p.final_lower
|
||||
};
|
||||
let direction = if p.direction < 0.0 {
|
||||
// Previous downtrend — the line was the upper band.
|
||||
if candle.close <= final_upper {
|
||||
-1.0
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
} else {
|
||||
// Previous uptrend — the line was the lower band.
|
||||
if candle.close >= final_lower {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
}
|
||||
};
|
||||
(final_upper, final_lower, direction)
|
||||
}
|
||||
};
|
||||
|
||||
let value = if direction > 0.0 {
|
||||
final_lower
|
||||
} else {
|
||||
final_upper
|
||||
};
|
||||
self.prev = Some(PrevState {
|
||||
final_upper,
|
||||
final_lower,
|
||||
close: candle.close,
|
||||
direction,
|
||||
});
|
||||
Some(SuperTrendOutput { value, direction })
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.atr.reset();
|
||||
self.prev = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.atr_period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.prev.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"SuperTrend"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new((high + low) / 2.0, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uptrend_keeps_line_below_price_and_direction_up() {
|
||||
let candles: Vec<Candle> = (0..60)
|
||||
.map(|i| {
|
||||
let base = 100.0 + 2.0 * i as f64;
|
||||
c(base + 1.0, base - 1.0, base + 0.5, i)
|
||||
})
|
||||
.collect();
|
||||
let mut st = SuperTrend::classic();
|
||||
for (o, candle) in st.batch(&candles).into_iter().zip(candles.iter()) {
|
||||
if let Some(o) = o {
|
||||
assert_eq!(o.direction, 1.0, "a pure uptrend stays in direction +1");
|
||||
assert!(o.value < candle.close, "the stop line sits below price");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn downtrend_keeps_line_above_price_and_direction_down() {
|
||||
let candles: Vec<Candle> = (0..60)
|
||||
.map(|i| {
|
||||
let base = 220.0 - 2.0 * i as f64;
|
||||
c(base + 1.0, base - 1.0, base - 0.5, i)
|
||||
})
|
||||
.collect();
|
||||
let mut st = SuperTrend::classic();
|
||||
let emitted: Vec<(SuperTrendOutput, f64)> = st
|
||||
.batch(&candles)
|
||||
.into_iter()
|
||||
.zip(candles.iter())
|
||||
.filter_map(|(o, c)| o.map(|v| (v, c.close)))
|
||||
.collect();
|
||||
// The seed bar starts the trend up; a steep decline flips it within a
|
||||
// few bars. The settled tail must be a clean downtrend.
|
||||
for &(o, close) in emitted.iter().skip(10) {
|
||||
assert_eq!(
|
||||
o.direction, -1.0,
|
||||
"a steep downtrend settles to direction -1"
|
||||
);
|
||||
assert!(o.value > close, "the stop line sits above price");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trend_flips_when_price_reverses() {
|
||||
let mut candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base + 0.5, i)
|
||||
})
|
||||
.collect();
|
||||
candles.extend((0..40).map(|i| {
|
||||
let base = 140.0 - i as f64;
|
||||
c(base + 1.0, base - 1.0, base - 0.5, 40 + i)
|
||||
}));
|
||||
let mut st = SuperTrend::classic();
|
||||
let dirs: Vec<f64> = st
|
||||
.batch(&candles)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|o| o.direction)
|
||||
.collect();
|
||||
assert!(dirs.iter().any(|&d| d > 0.0), "expected an uptrend stretch");
|
||||
assert!(
|
||||
dirs.iter().any(|&d| d < 0.0),
|
||||
"expected a downtrend stretch"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_matches_warmup_period() {
|
||||
let candles: Vec<Candle> = (0..30)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut st = SuperTrend::classic();
|
||||
let out = st.batch(&candles);
|
||||
assert_eq!(st.warmup_period(), 10);
|
||||
for (i, v) in out.iter().enumerate().take(9) {
|
||||
assert!(v.is_none(), "index {i} must be None during warmup");
|
||||
}
|
||||
assert!(out[9].is_some(), "first value lands at warmup_period - 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_params() {
|
||||
assert!(SuperTrend::new(0, 3.0).is_err());
|
||||
assert!(SuperTrend::new(10, 0.0).is_err());
|
||||
assert!(SuperTrend::new(10, -1.0).is_err());
|
||||
assert!(SuperTrend::new(10, f64::NAN).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
c(base + 1.0, base - 1.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut st = SuperTrend::classic();
|
||||
st.batch(&candles);
|
||||
assert!(st.is_ready());
|
||||
st.reset();
|
||||
assert!(!st.is_ready());
|
||||
assert_eq!(st.update(candles[0]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..80)
|
||||
.map(|i| {
|
||||
let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
|
||||
c(mid + 1.5, mid - 1.5, mid + 0.5, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = SuperTrend::classic();
|
||||
let mut b = SuperTrend::classic();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user