feat(indicators): B3 Trend & Directional batch (413 -> 420) (#181)
Adds the **B3 — Trend & Directional** batch: seven new indicators, taking the catalog from 413 to 420 (Trend & Directional family). | Indicator | Input → Output | Summary | |-----------|----------------|---------| | `Qstick` | candle → f64 | Chande's SMA of the candle body (close − open) | | `TtmTrend` | candle → f64 (±1) | John Carter close-vs-median-SMA trend filter | | `TrendStrengthIndex` | f64 → f64 | signed r² of an OLS regression of price vs time | | `PolarizedFractalEfficiency` | f64 → f64 | Hannula directional trend efficiency | | `WavePm` | f64 → f64 | Kase variance-normalised peak-momentum statistic (reconstruction) | | `GatorOscillator` | candle → struct | Bill Williams Alligator convergence/divergence histogram | | `KasePermissionStochastic` | candle → struct | double-smoothed stochastic permission filter | Note: the roadmap's "Directional Indicator +DI/−DI" item is already covered by the existing standalone `PlusDi` / `MinusDi` / `Dx`, so it is intentionally not re-added. All touchpoints wired: core (every-branch unit tests), Python/Node/WASM bindings, fuzz drivers, Python test registries + reference tests, Node factories, README/CHANGELOG counters. Local verify: `cargo test -p wickra-core` (lib 3389 + doc 378), `cargo clippy --workspace --all-targets --all-features -- -D warnings`, node build + 495 tests, maturin + 815 pytest, counter 420 == 420.
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
//! Bill Williams' Gator Oscillator (derived from the Alligator).
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::indicators::alligator::Alligator;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Gator Oscillator output: the two histogram bars drawn above and below the
|
||||
/// zero line.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct GatorOscillatorOutput {
|
||||
/// Upper histogram `|jaw - teeth|`, always `>= 0`.
|
||||
pub upper: f64,
|
||||
/// Lower histogram `-|teeth - lips|`, always `<= 0`.
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
/// Bill Williams' Gator Oscillator: a convergence/divergence view of the
|
||||
/// [`Alligator`] lines. The upper bar is the absolute gap between Jaw and
|
||||
/// Teeth; the lower bar is the negated absolute gap between Teeth and Lips.
|
||||
///
|
||||
/// ```text
|
||||
/// upper = |jaw - teeth|
|
||||
/// lower = -|teeth - lips |
|
||||
/// ```
|
||||
///
|
||||
/// Widening bars mean the Alligator's mouth is opening (a trending market);
|
||||
/// shrinking bars mean it is closing (consolidation). Warmup matches the
|
||||
/// underlying Alligator — the first value appears once the slowest line (Jaw)
|
||||
/// has warmed up.
|
||||
///
|
||||
/// Reference: Bill Williams, *Trading Chaos*, 1995.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, GatorOscillator, Indicator};
|
||||
///
|
||||
/// let mut indicator = GatorOscillator::classic();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 1.0, base - 1.0, base, 1.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GatorOscillator {
|
||||
alligator: Alligator,
|
||||
}
|
||||
|
||||
impl GatorOscillator {
|
||||
/// Construct a Gator Oscillator from explicit Alligator periods
|
||||
/// `(jaw, teeth, lips)`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`](crate::error::Error::PeriodZero) if any period is zero.
|
||||
pub fn new(jaw_period: usize, teeth_period: usize, lips_period: usize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
alligator: Alligator::new(jaw_period, teeth_period, lips_period)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Bill Williams' classic parameters: `(jaw = 13, teeth = 8, lips = 5)`.
|
||||
pub fn classic() -> Self {
|
||||
Self {
|
||||
alligator: Alligator::classic(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Configured `(jaw_period, teeth_period, lips_period)`.
|
||||
pub const fn periods(&self) -> (usize, usize, usize) {
|
||||
self.alligator.periods()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for GatorOscillator {
|
||||
type Input = Candle;
|
||||
type Output = GatorOscillatorOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<GatorOscillatorOutput> {
|
||||
let lines = self.alligator.update(candle)?;
|
||||
Some(GatorOscillatorOutput {
|
||||
upper: (lines.jaw - lines.teeth).abs(),
|
||||
lower: -(lines.teeth - lines.lips).abs(),
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.alligator.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.alligator.warmup_period()
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.alligator.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"GatorOscillator"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(high: f64, low: f64, ts: i64) -> Candle {
|
||||
let close = f64::midpoint(high, low);
|
||||
Candle::new(close, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(
|
||||
GatorOscillator::new(0, 8, 5),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
GatorOscillator::new(13, 0, 5),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
GatorOscillator::new(13, 8, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let g = GatorOscillator::classic();
|
||||
assert_eq!(g.periods(), (13, 8, 5));
|
||||
assert_eq!(g.warmup_period(), 13);
|
||||
assert_eq!(g.name(), "GatorOscillator");
|
||||
assert!(!g.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_collapses_both_bars() {
|
||||
// All three Alligator lines equal the constant median -> zero spread.
|
||||
let mut g = GatorOscillator::classic();
|
||||
let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, i)).collect();
|
||||
let out = g.batch(&candles);
|
||||
let last = out.last().unwrap().unwrap();
|
||||
assert_relative_eq!(last.upper, 0.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(last.lower, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trending_series_opens_the_mouth() {
|
||||
// On a clean trend the lines separate -> upper > 0, lower < 0.
|
||||
let mut g = GatorOscillator::classic();
|
||||
let candles: Vec<Candle> = (0_i64..80)
|
||||
.map(|i| candle(10.0 + i as f64, 9.0 + i as f64, i))
|
||||
.collect();
|
||||
let last = g.batch(&candles).last().unwrap().unwrap();
|
||||
assert!(last.upper > 0.0, "upper {} should be positive", last.upper);
|
||||
assert!(last.lower < 0.0, "lower {} should be negative", last.lower);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_first_value_at_longest_period() {
|
||||
let mut g = GatorOscillator::new(5, 3, 2).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 9.0, i)).collect();
|
||||
let out = g.batch(&candles);
|
||||
for v in out.iter().take(4) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[4].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut g = GatorOscillator::classic();
|
||||
let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, i)).collect();
|
||||
g.batch(&candles);
|
||||
assert!(g.is_ready());
|
||||
g.reset();
|
||||
assert!(!g.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..80_i64)
|
||||
.map(|i| {
|
||||
let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
|
||||
candle(base + 1.0, base - 1.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = GatorOscillator::classic();
|
||||
let mut b = GatorOscillator::classic();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
//! Kase Permission Stochastic — a double-smoothed stochastic used as a
|
||||
//! trade-permission filter.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::ema::Ema;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Kase Permission Stochastic output: a fast and a slow line.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct KasePermissionStochasticOutput {
|
||||
/// Fast line: EMA of the raw `%K` over the smoothing period.
|
||||
pub fast: f64,
|
||||
/// Slow line: EMA of the fast line over the smoothing period.
|
||||
pub slow: f64,
|
||||
}
|
||||
|
||||
/// Cynthia Kase's Permission Stochastic: a stochastic oscillator smoothed twice,
|
||||
/// whose fast/slow relationship grants or denies "permission" to trade in the
|
||||
/// direction of a higher-timeframe signal.
|
||||
///
|
||||
/// ```text
|
||||
/// raw%K = 100 * (close - LL) / (HH - LL) over `length` (50 when HH == LL)
|
||||
/// fast = EMA(raw%K, smooth)
|
||||
/// slow = EMA(fast, smooth)
|
||||
/// ```
|
||||
///
|
||||
/// The raw stochastic is the usual `%K`, then an EMA produces the *fast* line
|
||||
/// and a second EMA of that produces the *slow* line. Kase uses the pair as a
|
||||
/// gate: a fast line above the slow line (and rising) gives permission for
|
||||
/// longs, the reverse for shorts. When the lookback window is perfectly flat
|
||||
/// (`HH == LL`), the raw stochastic is undefined and defaults to the neutral
|
||||
/// `50`.
|
||||
///
|
||||
/// Reference: Cynthia Kase, *Trading with the Odds*, 1996.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, KasePermissionStochastic};
|
||||
///
|
||||
/// let mut indicator = KasePermissionStochastic::new(9, 3).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 1.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KasePermissionStochastic {
|
||||
length: usize,
|
||||
smooth: usize,
|
||||
window: VecDeque<(f64, f64)>,
|
||||
fast_ema: Ema,
|
||||
slow_ema: Ema,
|
||||
}
|
||||
|
||||
impl KasePermissionStochastic {
|
||||
/// Construct with the stochastic `length` and the EMA `smooth` period
|
||||
/// applied twice.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `length == 0` or `smooth == 0`.
|
||||
pub fn new(length: usize, smooth: usize) -> Result<Self> {
|
||||
if length == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
length,
|
||||
smooth,
|
||||
window: VecDeque::with_capacity(length),
|
||||
fast_ema: Ema::new(smooth)?,
|
||||
slow_ema: Ema::new(smooth)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Cynthia Kase's classic parameters: `length = 9`, `smooth = 3`.
|
||||
pub fn classic() -> Self {
|
||||
Self::new(9, 3).expect("classic Kase Permission Stochastic parameters are valid")
|
||||
}
|
||||
|
||||
/// Configured `(length, smooth)`.
|
||||
pub const fn periods(&self) -> (usize, usize) {
|
||||
(self.length, self.smooth)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for KasePermissionStochastic {
|
||||
type Input = Candle;
|
||||
type Output = KasePermissionStochasticOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<KasePermissionStochasticOutput> {
|
||||
self.window.push_back((candle.high, candle.low));
|
||||
if self.window.len() > self.length {
|
||||
self.window.pop_front();
|
||||
}
|
||||
if self.window.len() < self.length {
|
||||
return None;
|
||||
}
|
||||
|
||||
let highest = self.window.iter().map(|w| w.0).fold(f64::MIN, f64::max);
|
||||
let lowest = self.window.iter().map(|w| w.1).fold(f64::MAX, f64::min);
|
||||
let raw_k = if highest > lowest {
|
||||
100.0 * (candle.close - lowest) / (highest - lowest)
|
||||
} else {
|
||||
50.0
|
||||
};
|
||||
|
||||
let fast = self.fast_ema.update(raw_k)?;
|
||||
let slow = self.slow_ema.update(fast)?;
|
||||
Some(KasePermissionStochasticOutput { fast, slow })
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.fast_ema.reset();
|
||||
self.slow_ema.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
// raw%K ready after `length` bars; each EMA seeds over `smooth` values.
|
||||
self.length + 2 * self.smooth - 2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.slow_ema.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"KasePermissionStochastic"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(
|
||||
KasePermissionStochastic::new(0, 3),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
KasePermissionStochastic::new(9, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let k = KasePermissionStochastic::classic();
|
||||
assert_eq!(k.periods(), (9, 3));
|
||||
// 9 + 2*3 - 2 = 13.
|
||||
assert_eq!(k.warmup_period(), 13);
|
||||
assert_eq!(k.name(), "KasePermissionStochastic");
|
||||
assert!(!k.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_at_expected_bar() {
|
||||
let mut k = KasePermissionStochastic::new(3, 2).unwrap();
|
||||
// warmup = 3 + 2*2 - 2 = 5 -> first value at input 5 (index 4).
|
||||
let candles: Vec<Candle> = (0..8).map(|i| candle(11.0, 9.0, 10.5, i)).collect();
|
||||
let out = k.batch(&candles);
|
||||
assert!(out[3].is_none());
|
||||
assert!(out[4].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_of_range_is_high() {
|
||||
// Close pinned at the top of a rising range -> raw%K near 100, both
|
||||
// smoothed lines high.
|
||||
let mut k = KasePermissionStochastic::new(5, 3).unwrap();
|
||||
let candles: Vec<Candle> = (0_i64..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
candle(base + 2.0, base - 2.0, base + 2.0, i)
|
||||
})
|
||||
.collect();
|
||||
let last = k.batch(&candles).last().unwrap().unwrap();
|
||||
assert!(last.fast > 80.0, "fast {} should be high", last.fast);
|
||||
assert!(last.slow > 80.0, "slow {} should be high", last.slow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_window_defaults_to_neutral() {
|
||||
// Constant high/low/close -> HH == LL -> raw%K defaults to 50, so both
|
||||
// EMAs converge to 50.
|
||||
let mut k = KasePermissionStochastic::new(4, 2).unwrap();
|
||||
let candles: Vec<Candle> = (0..20).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
|
||||
let last = k.batch(&candles).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last.fast, 50.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(last.slow, 50.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut k = KasePermissionStochastic::classic();
|
||||
let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, 10.5, i)).collect();
|
||||
k.batch(&candles);
|
||||
assert!(k.is_ready());
|
||||
k.reset();
|
||||
assert!(!k.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..80_i64)
|
||||
.map(|i| {
|
||||
let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
|
||||
candle(base + 2.0, base - 2.0, base + (i as f64 * 0.3).cos(), i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = KasePermissionStochastic::classic();
|
||||
let mut b = KasePermissionStochastic::classic();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,7 @@ mod gain_loss_ratio;
|
||||
mod gap_side_by_side_white;
|
||||
mod garman_klass;
|
||||
mod gartley;
|
||||
mod gator_oscillator;
|
||||
mod generalized_dema;
|
||||
mod geometric_ma;
|
||||
mod golden_pocket;
|
||||
@@ -187,6 +188,7 @@ mod jump_indicator;
|
||||
mod kagi_bars;
|
||||
mod kalman_hedge_ratio;
|
||||
mod kama;
|
||||
mod kase_permission_stochastic;
|
||||
mod kelly_criterion;
|
||||
mod keltner;
|
||||
mod kicking;
|
||||
@@ -266,11 +268,13 @@ mod plus_di;
|
||||
mod plus_dm;
|
||||
mod pmo;
|
||||
mod point_and_figure_bars;
|
||||
mod polarized_fractal_efficiency;
|
||||
mod ppo;
|
||||
mod profit_factor;
|
||||
mod psar;
|
||||
mod pvi;
|
||||
mod qqe;
|
||||
mod qstick;
|
||||
mod quoted_spread;
|
||||
mod r_squared;
|
||||
mod realized_spread;
|
||||
@@ -368,6 +372,7 @@ mod time_of_day_return_profile;
|
||||
mod tpo_profile;
|
||||
mod trade_imbalance;
|
||||
mod trend_label;
|
||||
mod trend_strength_index;
|
||||
mod treynor_ratio;
|
||||
mod triangle;
|
||||
mod trima;
|
||||
@@ -379,6 +384,7 @@ mod tsf;
|
||||
mod tsi;
|
||||
mod tsv;
|
||||
mod ttm_squeeze;
|
||||
mod ttm_trend;
|
||||
mod turn_of_month;
|
||||
mod tweezer;
|
||||
mod two_crows;
|
||||
@@ -406,6 +412,7 @@ mod vwap;
|
||||
mod vwap_stddev_bands;
|
||||
mod vwma;
|
||||
mod vzo;
|
||||
mod wave_pm;
|
||||
mod wave_trend;
|
||||
mod wedge;
|
||||
mod weighted_close;
|
||||
@@ -558,6 +565,7 @@ pub use gain_loss_ratio::GainLossRatio;
|
||||
pub use gap_side_by_side_white::GapSideBySideWhite;
|
||||
pub use garman_klass::GarmanKlassVolatility;
|
||||
pub use gartley::Gartley;
|
||||
pub use gator_oscillator::GatorOscillator;
|
||||
pub use generalized_dema::GeneralizedDema;
|
||||
pub use geometric_ma::GeometricMa;
|
||||
pub use golden_pocket::{GoldenPocket, GoldenPocketOutput};
|
||||
@@ -600,6 +608,7 @@ pub use jump_indicator::JumpIndicator;
|
||||
pub use kagi_bars::{KagiBar, KagiBars};
|
||||
pub use kalman_hedge_ratio::{KalmanHedgeRatio, KalmanHedgeRatioOutput};
|
||||
pub use kama::Kama;
|
||||
pub use kase_permission_stochastic::KasePermissionStochastic;
|
||||
pub use kelly_criterion::KellyCriterion;
|
||||
pub use keltner::{Keltner, KeltnerOutput};
|
||||
pub use kicking::Kicking;
|
||||
@@ -679,11 +688,13 @@ pub use plus_di::PlusDi;
|
||||
pub use plus_dm::PlusDm;
|
||||
pub use pmo::Pmo;
|
||||
pub use point_and_figure_bars::{PnfColumn, PointAndFigureBars};
|
||||
pub use polarized_fractal_efficiency::PolarizedFractalEfficiency;
|
||||
pub use ppo::Ppo;
|
||||
pub use profit_factor::ProfitFactor;
|
||||
pub use psar::Psar;
|
||||
pub use pvi::Pvi;
|
||||
pub use qqe::{Qqe, QqeOutput};
|
||||
pub use qstick::Qstick;
|
||||
pub use quoted_spread::QuotedSpread;
|
||||
pub use r_squared::RSquared;
|
||||
pub use realized_spread::RealizedSpread;
|
||||
@@ -781,6 +792,7 @@ pub use time_of_day_return_profile::{TimeOfDayReturnProfile, TimeOfDayReturnProf
|
||||
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
|
||||
pub use trade_imbalance::TradeImbalance;
|
||||
pub use trend_label::TrendLabel;
|
||||
pub use trend_strength_index::TrendStrengthIndex;
|
||||
pub use treynor_ratio::TreynorRatio;
|
||||
pub use triangle::Triangle;
|
||||
pub use trima::Trima;
|
||||
@@ -792,6 +804,7 @@ pub use tsf::Tsf;
|
||||
pub use tsi::Tsi;
|
||||
pub use tsv::Tsv;
|
||||
pub use ttm_squeeze::{TtmSqueeze, TtmSqueezeOutput};
|
||||
pub use ttm_trend::TtmTrend;
|
||||
pub use turn_of_month::TurnOfMonth;
|
||||
pub use tweezer::Tweezer;
|
||||
pub use two_crows::TwoCrows;
|
||||
@@ -819,6 +832,7 @@ pub use vwap::{RollingVwap, Vwap};
|
||||
pub use vwap_stddev_bands::{VwapStdDevBands, VwapStdDevBandsOutput};
|
||||
pub use vwma::Vwma;
|
||||
pub use vzo::Vzo;
|
||||
pub use wave_pm::WavePm;
|
||||
pub use wave_trend::{WaveTrend, WaveTrendOutput};
|
||||
pub use wedge::Wedge;
|
||||
pub use weighted_close::WeightedClose;
|
||||
@@ -936,6 +950,13 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"MinusDi",
|
||||
"Dx",
|
||||
"TrendLabel",
|
||||
"TtmTrend",
|
||||
"TrendStrengthIndex",
|
||||
"Qstick",
|
||||
"PolarizedFractalEfficiency",
|
||||
"WavePm",
|
||||
"GatorOscillator",
|
||||
"KasePermissionStochastic",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1393,6 +1414,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 413, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 420, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
//! Polarized Fractal Efficiency (PFE).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::ema::Ema;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Polarized Fractal Efficiency: how efficiently price travelled over the last
|
||||
/// `period` bars, signed by direction and smoothed by an EMA.
|
||||
///
|
||||
/// ```text
|
||||
/// straight = sqrt((C_t - C_{t-n})^2 + n^2) (direct distance over n bars)
|
||||
/// path = Σ_{i=1..n} sqrt((C_{t-i+1} - C_{t-i})^2 + 1) (sum of single-bar steps)
|
||||
/// raw = 100 * sign(C_t - C_{t-n}) * straight / path
|
||||
/// PFE = EMA(raw, smoothing)
|
||||
/// ```
|
||||
///
|
||||
/// The ratio `straight / path` is the fractal efficiency: it is `1` when price
|
||||
/// moved in a perfectly straight line and falls toward `0` as the path becomes
|
||||
/// jagged. Polarizing it by the sign of the net move pushes the reading to
|
||||
/// `+100` for an efficient up-move and `-100` for an efficient down-move, with
|
||||
/// choppy markets oscillating near zero. Because each single-bar step and the
|
||||
/// `n`-bar diagonal both carry the bar count on the x-axis (`+1` and `+n^2`),
|
||||
/// the path length is always `>= n`, so the denominator can never be zero.
|
||||
///
|
||||
/// Reference: Hans Hannula, *Stocks & Commodities*, 1994.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, PolarizedFractalEfficiency};
|
||||
///
|
||||
/// let mut indicator = PolarizedFractalEfficiency::new(10, 5).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PolarizedFractalEfficiency {
|
||||
period: usize,
|
||||
smoothing: usize,
|
||||
closes: VecDeque<f64>,
|
||||
prev_close: Option<f64>,
|
||||
segments: VecDeque<f64>,
|
||||
segment_sum: f64,
|
||||
ema: Ema,
|
||||
}
|
||||
|
||||
impl PolarizedFractalEfficiency {
|
||||
/// Construct a PFE with the fractal lookback `period` and the EMA
|
||||
/// `smoothing` period.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0` or `smoothing == 0`.
|
||||
pub fn new(period: usize, smoothing: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
smoothing,
|
||||
closes: VecDeque::with_capacity(period + 1),
|
||||
prev_close: None,
|
||||
segments: VecDeque::with_capacity(period),
|
||||
segment_sum: 0.0,
|
||||
ema: Ema::new(smoothing)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(period, smoothing)`.
|
||||
pub const fn periods(&self) -> (usize, usize) {
|
||||
(self.period, self.smoothing)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for PolarizedFractalEfficiency {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, close: f64) -> Option<f64> {
|
||||
if let Some(prev) = self.prev_close {
|
||||
let diff = close - prev;
|
||||
let segment = diff.mul_add(diff, 1.0).sqrt();
|
||||
self.segment_sum += segment;
|
||||
self.segments.push_back(segment);
|
||||
if self.segments.len() > self.period {
|
||||
self.segment_sum -= self.segments.pop_front().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
self.prev_close = Some(close);
|
||||
|
||||
self.closes.push_back(close);
|
||||
if self.closes.len() > self.period + 1 {
|
||||
self.closes.pop_front();
|
||||
}
|
||||
if self.closes.len() <= self.period {
|
||||
return None;
|
||||
}
|
||||
|
||||
let oldest = *self.closes.front().unwrap_or(&close);
|
||||
let net = close - oldest;
|
||||
let direction = if net > 0.0 {
|
||||
1.0
|
||||
} else if net < 0.0 {
|
||||
-1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let span = self.period as f64;
|
||||
let straight = net.mul_add(net, span * span).sqrt();
|
||||
let raw = 100.0 * direction * straight / self.segment_sum;
|
||||
self.ema.update(raw)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.closes.clear();
|
||||
self.prev_close = None;
|
||||
self.segments.clear();
|
||||
self.segment_sum = 0.0;
|
||||
self.ema.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period + self.smoothing
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ema.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"PolarizedFractalEfficiency"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(
|
||||
PolarizedFractalEfficiency::new(0, 5),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
assert!(matches!(
|
||||
PolarizedFractalEfficiency::new(10, 0),
|
||||
Err(Error::PeriodZero)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let pfe = PolarizedFractalEfficiency::new(10, 5).unwrap();
|
||||
assert_eq!(pfe.periods(), (10, 5));
|
||||
assert_eq!(pfe.warmup_period(), 15);
|
||||
assert_eq!(pfe.name(), "PolarizedFractalEfficiency");
|
||||
assert!(!pfe.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_after_period_plus_smoothing() {
|
||||
let mut pfe = PolarizedFractalEfficiency::new(4, 2).unwrap();
|
||||
// raw needs period+1 = 5 closes; EMA(2) needs 2 raws -> first value at
|
||||
// input 6 (index 5).
|
||||
let inputs: Vec<f64> = (0..10).map(f64::from).collect();
|
||||
let out = pfe.batch(&inputs);
|
||||
assert!(out[4].is_none());
|
||||
assert!(out[5].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perfect_uptrend_is_strongly_positive() {
|
||||
// A straight ramp: every step is +1, the diagonal is maximally
|
||||
// efficient, so PFE saturates near +100.
|
||||
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..30).map(f64::from).collect();
|
||||
let last = pfe.batch(&inputs).last().unwrap().unwrap();
|
||||
assert!(last > 99.0, "pfe {last} should be near +100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perfect_downtrend_is_strongly_negative() {
|
||||
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..30).map(|i| -f64::from(i)).collect();
|
||||
let last = pfe.batch(&inputs).last().unwrap().unwrap();
|
||||
assert!(last < -99.0, "pfe {last} should be near -100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_market_returns_zero() {
|
||||
// No net move over the window -> direction 0 -> raw 0 -> PFE 0.
|
||||
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
|
||||
let inputs = [10.0; 20];
|
||||
let last = pfe.batch(&inputs).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choppy_market_is_inefficient() {
|
||||
// A sawtooth whip: the net move is tiny relative to the jagged path, so
|
||||
// efficiency stays well below the +-100 saturation of a clean trend.
|
||||
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..40)
|
||||
.map(|i| if i % 2 == 0 { 100.0 } else { 102.0 })
|
||||
.collect();
|
||||
let last = pfe.batch(&inputs).last().unwrap().unwrap();
|
||||
assert!(
|
||||
last.abs() < 60.0,
|
||||
"choppy pfe {last} should be far from +-100"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..30).map(f64::from).collect();
|
||||
pfe.batch(&inputs);
|
||||
assert!(pfe.is_ready());
|
||||
pfe.reset();
|
||||
assert!(!pfe.is_ready());
|
||||
assert_eq!(pfe.periods(), (5, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let inputs: Vec<f64> = (0..80)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
|
||||
.collect();
|
||||
let mut a = PolarizedFractalEfficiency::new(10, 5).unwrap();
|
||||
let mut b = PolarizedFractalEfficiency::new(10, 5).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&inputs),
|
||||
inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
//! Qstick — Tushar Chande's measure of buying vs. selling pressure.
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::indicators::sma::Sma;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Qstick: the simple moving average of the body `close - open` over `period`
|
||||
/// bars.
|
||||
///
|
||||
/// Positive values indicate a run of bars that closed above their open (net
|
||||
/// buying pressure); negative values indicate net selling pressure. A zero
|
||||
/// crossing is read as a shift in short-term sentiment.
|
||||
///
|
||||
/// ```text
|
||||
/// Qstick = SMA(close - open, period)
|
||||
/// ```
|
||||
///
|
||||
/// Reference: Tushar Chande, *The New Technical Trader*, 1994.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, Qstick};
|
||||
///
|
||||
/// let mut indicator = Qstick::new(5).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..20 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 2.0, base - 1.0, base + 1.0, 1.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Qstick {
|
||||
period: usize,
|
||||
sma: Sma,
|
||||
}
|
||||
|
||||
impl Qstick {
|
||||
/// Construct a Qstick with the given averaging period.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`](crate::error::Error::PeriodZero) if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
period,
|
||||
sma: Sma::new(period)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured averaging period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Qstick {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.sma.update(candle.close - candle.open)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.sma.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.sma.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Qstick"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn candle(open: f64, close: f64, ts: i64) -> Candle {
|
||||
let high = open.max(close) + 1.0;
|
||||
let low = open.min(close) - 1.0;
|
||||
Candle::new(open, high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(Qstick::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let q = Qstick::new(5).unwrap();
|
||||
assert_eq!(q.period(), 5);
|
||||
assert_eq!(q.warmup_period(), 5);
|
||||
assert_eq!(q.name(), "Qstick");
|
||||
assert!(!q.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_first_value_at_period() {
|
||||
let mut q = Qstick::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..3).map(|i| candle(10.0, 11.0, i)).collect();
|
||||
let out = q.batch(&candles);
|
||||
assert!(out[0].is_none());
|
||||
assert!(out[1].is_none());
|
||||
assert!(out[2].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_bodies_yield_the_body() {
|
||||
// Every bar closes 1.5 above its open -> Qstick converges to 1.5.
|
||||
let mut q = Qstick::new(4).unwrap();
|
||||
let candles: Vec<Candle> = (0..10).map(|i| candle(10.0, 11.5, i)).collect();
|
||||
let out = q.batch(&candles);
|
||||
assert_relative_eq!(out.last().unwrap().unwrap(), 1.5, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selling_pressure_is_negative() {
|
||||
let mut q = Qstick::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 10.0, i)).collect();
|
||||
let last = q.batch(&candles).last().unwrap().unwrap();
|
||||
assert!(last < 0.0, "qstick {last} should be negative");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut q = Qstick::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(10.0, 11.0, i)).collect();
|
||||
q.batch(&candles);
|
||||
assert!(q.is_ready());
|
||||
q.reset();
|
||||
assert!(!q.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40_i64)
|
||||
.map(|i| {
|
||||
candle(
|
||||
100.0 + (i as f64 * 0.3).sin(),
|
||||
100.0 + (i as f64 * 0.4).cos(),
|
||||
i,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut a = Qstick::new(7).unwrap();
|
||||
let mut b = Qstick::new(7).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
//! Trend Strength Index — the signed coefficient of determination of a linear
|
||||
//! regression of price against time.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Trend Strength Index: fits an ordinary-least-squares line to the last
|
||||
/// `period` prices against their bar index and reports the coefficient of
|
||||
/// determination `r^2`, signed by the slope of the fit.
|
||||
///
|
||||
/// ```text
|
||||
/// regress y = close on x = 0..period-1
|
||||
/// r^2 = (n·Σxy − Σx·Σy)^2 / [ (n·Σx² − (Σx)²)(n·Σy² − (Σy)²) ]
|
||||
/// TSI = sign(slope) · r^2 (slope sign = sign of n·Σxy − Σx·Σy)
|
||||
/// ```
|
||||
///
|
||||
/// `r^2` in `[0, 1]` measures how well a straight line explains the price over
|
||||
/// the window — how *trendy* the segment is, regardless of direction. Carrying
|
||||
/// the slope sign turns it into a directional reading in `[-1, 1]`: values near
|
||||
/// `+1` are a strong, clean uptrend; near `-1` a strong downtrend; near `0` a
|
||||
/// flat or noisy market with no linear structure. A window of constant prices
|
||||
/// (zero variance in `y`) has no defined trend and returns `0`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, TrendStrengthIndex};
|
||||
///
|
||||
/// let mut indicator = TrendStrengthIndex::new(20).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// // A clean ramp is a perfect uptrend -> r^2 = 1.
|
||||
/// assert!((last.unwrap() - 1.0).abs() < 1e-9);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrendStrengthIndex {
|
||||
period: usize,
|
||||
buf: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl TrendStrengthIndex {
|
||||
/// Construct a Trend Strength Index over the given window.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`, or [`Error::InvalidPeriod`]
|
||||
/// if `period == 1` (a regression needs at least two points).
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
if period == 1 {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "period must be >= 2 for a regression",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
buf: VecDeque::with_capacity(period),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured window length.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TrendStrengthIndex {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, price: f64) -> Option<f64> {
|
||||
self.buf.push_back(price);
|
||||
if self.buf.len() > self.period {
|
||||
self.buf.pop_front();
|
||||
}
|
||||
if self.buf.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
|
||||
let count = self.period as f64;
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_xx = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
let mut sum_yy = 0.0;
|
||||
let mut sum_xy = 0.0;
|
||||
for (idx, &price) in self.buf.iter().enumerate() {
|
||||
let x = idx as f64;
|
||||
sum_x += x;
|
||||
sum_xx += x * x;
|
||||
sum_y += price;
|
||||
sum_yy += price * price;
|
||||
sum_xy += x * price;
|
||||
}
|
||||
|
||||
let cov = count.mul_add(sum_xy, -(sum_x * sum_y));
|
||||
let var_x = count.mul_add(sum_xx, -(sum_x * sum_x));
|
||||
let var_y = count.mul_add(sum_yy, -(sum_y * sum_y));
|
||||
if var_y <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let r2 = (cov * cov) / (var_x * var_y);
|
||||
Some(if cov >= 0.0 { r2 } else { -r2 })
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.buf.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.buf.len() >= self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TrendStrengthIndex"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_period() {
|
||||
assert!(matches!(TrendStrengthIndex::new(0), Err(Error::PeriodZero)));
|
||||
assert!(matches!(
|
||||
TrendStrengthIndex::new(1),
|
||||
Err(Error::InvalidPeriod { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let tsi = TrendStrengthIndex::new(20).unwrap();
|
||||
assert_eq!(tsi.period(), 20);
|
||||
assert_eq!(tsi.warmup_period(), 20);
|
||||
assert_eq!(tsi.name(), "TrendStrengthIndex");
|
||||
assert!(!tsi.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_at_period() {
|
||||
let mut tsi = TrendStrengthIndex::new(4).unwrap();
|
||||
let inputs: Vec<f64> = (0..6).map(f64::from).collect();
|
||||
let out = tsi.batch(&inputs);
|
||||
assert!(out[2].is_none());
|
||||
assert!(out[3].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perfect_uptrend_is_plus_one() {
|
||||
let mut tsi = TrendStrengthIndex::new(10).unwrap();
|
||||
let inputs: Vec<f64> = (0..10).map(f64::from).collect();
|
||||
let last = tsi.batch(&inputs).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last, 1.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perfect_downtrend_is_minus_one() {
|
||||
let mut tsi = TrendStrengthIndex::new(10).unwrap();
|
||||
let inputs: Vec<f64> = (0..10).map(|i| 100.0 - f64::from(i)).collect();
|
||||
let last = tsi.batch(&inputs).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last, -1.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_market_returns_zero() {
|
||||
let mut tsi = TrendStrengthIndex::new(8).unwrap();
|
||||
let inputs = [42.0; 12];
|
||||
let last = tsi.batch(&inputs).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noisy_trend_is_between() {
|
||||
// An upward drift with noise: positive but not a perfect fit.
|
||||
let mut tsi = TrendStrengthIndex::new(12).unwrap();
|
||||
let inputs: Vec<f64> = (0..12)
|
||||
.map(|i| f64::from(i) + if i % 2 == 0 { 0.0 } else { 3.0 })
|
||||
.collect();
|
||||
let last = tsi.batch(&inputs).last().unwrap().unwrap();
|
||||
assert!(last > 0.0 && last < 1.0, "tsi {last} should be in (0, 1)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut tsi = TrendStrengthIndex::new(10).unwrap();
|
||||
let inputs: Vec<f64> = (0..10).map(f64::from).collect();
|
||||
tsi.batch(&inputs);
|
||||
assert!(tsi.is_ready());
|
||||
tsi.reset();
|
||||
assert!(!tsi.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let inputs: Vec<f64> = (0..80)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
|
||||
.collect();
|
||||
let mut a = TrendStrengthIndex::new(15).unwrap();
|
||||
let mut b = TrendStrengthIndex::new(15).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&inputs),
|
||||
inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//! TTM Trend — John Carter's bar-coloring trend filter.
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::indicators::sma::Sma;
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// TTM Trend: compares the current close to the simple moving average of the
|
||||
/// recent median prices `(high + low) / 2`. A close above that reference colors
|
||||
/// the bar as an uptrend (`+1.0`); a close at or below it as a downtrend
|
||||
/// (`-1.0`).
|
||||
///
|
||||
/// ```text
|
||||
/// reference = SMA((high + low) / 2, period)
|
||||
/// TTM Trend = +1 if close > reference
|
||||
/// -1 otherwise
|
||||
/// ```
|
||||
///
|
||||
/// The classic TTM Trend uses the trailing six bars. The signal is a regime
|
||||
/// label rather than a level: it stays `None` during warmup and then emits
|
||||
/// `±1.0` on every bar.
|
||||
///
|
||||
/// Reference: John Carter, *Mastering the Trade*, 2005.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, TtmTrend};
|
||||
///
|
||||
/// let mut indicator = TtmTrend::new(6).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..20 {
|
||||
/// let base = 100.0 + f64::from(i);
|
||||
/// let candle =
|
||||
/// Candle::new(base, base + 1.0, base - 1.0, base + 0.5, 1.0, i64::from(i)).unwrap();
|
||||
/// last = indicator.update(candle);
|
||||
/// }
|
||||
/// assert_eq!(last, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TtmTrend {
|
||||
period: usize,
|
||||
sma: Sma,
|
||||
}
|
||||
|
||||
impl TtmTrend {
|
||||
/// Construct a TTM Trend over the given lookback.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`](crate::error::Error::PeriodZero) if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
period,
|
||||
sma: Sma::new(period)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured lookback period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for TtmTrend {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let median = f64::midpoint(candle.high, candle.low);
|
||||
let reference = self.sma.update(median)?;
|
||||
Some(if candle.close > reference { 1.0 } else { -1.0 })
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.sma.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.sma.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TtmTrend"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
|
||||
Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(TtmTrend::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = TtmTrend::new(6).unwrap();
|
||||
assert_eq!(t.period(), 6);
|
||||
assert_eq!(t.warmup_period(), 6);
|
||||
assert_eq!(t.name(), "TtmTrend");
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_then_emits() {
|
||||
let mut t = TtmTrend::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..3).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
|
||||
let out = t.batch(&candles);
|
||||
assert!(out[0].is_none());
|
||||
assert!(out[1].is_none());
|
||||
assert!(out[2].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_above_reference_is_uptrend() {
|
||||
// Close (12) sits above the median reference (13 + 9) / 2 = 11 -> +1.
|
||||
let mut t = TtmTrend::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
|
||||
assert_eq!(t.batch(&candles).last().unwrap().unwrap(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_at_or_below_reference_is_downtrend() {
|
||||
// Constant median 10, close equal to the reference -> not strictly above -> -1.
|
||||
let mut t = TtmTrend::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
|
||||
assert_eq!(t.batch(&candles).last().unwrap().unwrap(), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = TtmTrend::new(3).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|i| candle(13.0, 9.0, 12.0, i)).collect();
|
||||
t.batch(&candles);
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40_i64)
|
||||
.map(|i| {
|
||||
let base = 100.0 + (i as f64 * 0.25).sin() * 4.0;
|
||||
candle(base + 1.0, base - 1.0, base + (i as f64 * 0.5).cos(), i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = TtmTrend::new(6).unwrap();
|
||||
let mut b = TtmTrend::new(6).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
//! Wave PM — Cynthia Kase's peak-momentum statistic (Wickra reconstruction).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::indicators::ema::Ema;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Wave PM (Peak Momentum): a `0..100` statistic that rises when the current
|
||||
/// `length`-bar momentum is large relative to its own recent energy — Cynthia
|
||||
/// Kase's gauge of how "peaked" the move is.
|
||||
///
|
||||
/// ```text
|
||||
/// m = close_t - close_{t-length} (length-bar momentum)
|
||||
/// energy = EMA(m^2, length) (mean squared momentum)
|
||||
/// raw = 1 - exp( -m^2 / (2 * energy) ) (0 if energy == 0)
|
||||
/// WavePM = 100 * EMA(raw, smoothing)
|
||||
/// ```
|
||||
///
|
||||
/// The momentum `m` is normalised by its recent variance (`energy`): a move that
|
||||
/// merely matches its typical energy sits at the baseline
|
||||
/// `100·(1 − e^{−1/2}) ≈ 39.35`, while a momentum *spike* that exceeds recent
|
||||
/// energy drives the reading toward `100`. A flat market (`m = 0`) reads `0`.
|
||||
/// High readings mark a peaking, possibly exhausted move rather than a fresh one.
|
||||
///
|
||||
/// Kase's published `WavePM` is platform-specific; this is Wickra's faithful
|
||||
/// reconstruction of its variance-normalised peak-momentum form. The exact
|
||||
/// constants differ from any single vendor implementation, but the shape — flat
|
||||
/// at zero, a fixed baseline on a steady trend, and saturation on an
|
||||
/// acceleration — matches the indicator's intent.
|
||||
///
|
||||
/// Reference: Cynthia Kase, *Trading with the Odds*, 1996 (Wickra reconstruction).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Indicator, WavePm};
|
||||
///
|
||||
/// let mut indicator = WavePm::new(10, 3).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..60 {
|
||||
/// last = indicator.update(100.0 + f64::from(i));
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WavePm {
|
||||
length: usize,
|
||||
smoothing: usize,
|
||||
closes: VecDeque<f64>,
|
||||
energy_ema: Ema,
|
||||
smooth_ema: Ema,
|
||||
}
|
||||
|
||||
impl WavePm {
|
||||
/// Construct a Wave PM with the momentum `length` and the output `smoothing`
|
||||
/// period.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `length == 0` or `smoothing == 0`.
|
||||
pub fn new(length: usize, smoothing: usize) -> Result<Self> {
|
||||
if length == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
length,
|
||||
smoothing,
|
||||
closes: VecDeque::with_capacity(length + 1),
|
||||
energy_ema: Ema::new(length)?,
|
||||
smooth_ema: Ema::new(smoothing)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(length, smoothing)`.
|
||||
pub const fn periods(&self) -> (usize, usize) {
|
||||
(self.length, self.smoothing)
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for WavePm {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, close: f64) -> Option<f64> {
|
||||
self.closes.push_back(close);
|
||||
if self.closes.len() > self.length + 1 {
|
||||
self.closes.pop_front();
|
||||
}
|
||||
if self.closes.len() <= self.length {
|
||||
return None;
|
||||
}
|
||||
|
||||
let oldest = *self.closes.front().unwrap_or(&close);
|
||||
let momentum = close - oldest;
|
||||
let energy = self.energy_ema.update(momentum * momentum)?;
|
||||
let raw = if energy <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
1.0 - (-(momentum * momentum) / (2.0 * energy)).exp()
|
||||
};
|
||||
self.smooth_ema.update(raw).map(|v| v * 100.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.closes.clear();
|
||||
self.energy_ema.reset();
|
||||
self.smooth_ema.reset();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2 * self.length + self.smoothing - 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.smooth_ema.is_ready()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"WavePm"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(WavePm::new(0, 3), Err(Error::PeriodZero)));
|
||||
assert!(matches!(WavePm::new(10, 0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let w = WavePm::new(10, 3).unwrap();
|
||||
assert_eq!(w.periods(), (10, 3));
|
||||
// 2*10 + 3 - 1 = 22.
|
||||
assert_eq!(w.warmup_period(), 22);
|
||||
assert_eq!(w.name(), "WavePm");
|
||||
assert!(!w.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_emits_at_expected_bar() {
|
||||
let mut w = WavePm::new(3, 2).unwrap();
|
||||
// warmup = 2*3 + 2 - 1 = 7 -> first value at input 7 (index 6).
|
||||
let inputs: Vec<f64> = (0..12).map(f64::from).collect();
|
||||
let out = w.batch(&inputs);
|
||||
assert!(out[5].is_none());
|
||||
assert!(out[6].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_market_reads_zero() {
|
||||
let mut w = WavePm::new(4, 2).unwrap();
|
||||
let inputs = [50.0; 20];
|
||||
let last = w.batch(&inputs).last().unwrap().unwrap();
|
||||
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steady_trend_reads_baseline() {
|
||||
// Constant-slope ramp: momentum equals its own energy every bar, so the
|
||||
// reading pins to the baseline 100*(1 - e^-0.5).
|
||||
let mut w = WavePm::new(10, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..60).map(|i| f64::from(i) * 5.0).collect();
|
||||
let last = w.batch(&inputs).last().unwrap().unwrap();
|
||||
let baseline = 100.0 * (1.0 - (-0.5_f64).exp());
|
||||
assert_relative_eq!(last, baseline, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acceleration_reads_above_baseline() {
|
||||
// A quadratic path: momentum keeps outrunning its lagged energy, so the
|
||||
// reading sits above the steady-trend baseline.
|
||||
let mut w = WavePm::new(10, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..60).map(|i| f64::from(i * i) * 0.1).collect();
|
||||
let last = w.batch(&inputs).last().unwrap().unwrap();
|
||||
let baseline = 100.0 * (1.0 - (-0.5_f64).exp());
|
||||
assert!(
|
||||
last > baseline,
|
||||
"accelerating wpm {last} should exceed {baseline}"
|
||||
);
|
||||
assert!(last <= 100.0, "wpm {last} must stay <= 100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut w = WavePm::new(10, 3).unwrap();
|
||||
let inputs: Vec<f64> = (0..60).map(|i| f64::from(i) * 5.0).collect();
|
||||
w.batch(&inputs);
|
||||
assert!(w.is_ready());
|
||||
w.reset();
|
||||
assert!(!w.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let inputs: Vec<f64> = (0..80)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
|
||||
.collect();
|
||||
let mut a = WavePm::new(10, 3).unwrap();
|
||||
let mut b = WavePm::new(10, 3).unwrap();
|
||||
assert_eq!(
|
||||
a.batch(&inputs),
|
||||
inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -84,59 +84,60 @@ pub use indicators::{
|
||||
FibonacciPivots, FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint,
|
||||
FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis,
|
||||
FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite,
|
||||
GarmanKlassVolatility, Gartley, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
|
||||
GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi,
|
||||
HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, Hikkake,
|
||||
HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
|
||||
HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
|
||||
HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
|
||||
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
GarmanKlassVolatility, Gartley, GatorOscillator, GeneralizedDema, GeometricMa, GoldenPocket,
|
||||
GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami,
|
||||
HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange,
|
||||
HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
|
||||
HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
|
||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
IntradayMomentumIndex, IntradayVolatilityProfile, IntradayVolatilityProfileOutput,
|
||||
InverseFisherTransform, InvertedHammer, Jma, JumpIndicator, KagiBars, KalmanHedgeRatio,
|
||||
KalmanHedgeRatioOutput, Kama, KellyCriterion, Keltner, KeltnerOutput, Kicking, KickingByLength,
|
||||
Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation,
|
||||
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput,
|
||||
LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput,
|
||||
LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt,
|
||||
MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu,
|
||||
MassIndex, MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
|
||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MedianPrice, Mfi, Microprice, MidPoint,
|
||||
MidPrice, MinusDi, MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows,
|
||||
Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta,
|
||||
OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap,
|
||||
OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore,
|
||||
PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa, PercentB,
|
||||
PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars, Ppo,
|
||||
ProfitFactor, Psar, Pvi, Qqe, QqeOutput, QuotedSpread, RSquared, RealizedSpread,
|
||||
RealizedVolatility, RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB,
|
||||
RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi,
|
||||
Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollMeasure, RollingCorrelation,
|
||||
RollingCovariance, RollingIqr, RollingPercentileRank, RollingQuantile, RollingVwap,
|
||||
RoofingFilter, Rsi, Rsx, Rvi, RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore,
|
||||
SeparatingLines, SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput,
|
||||
SessionVwap, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave,
|
||||
SineWeightedMa, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop,
|
||||
SpreadAr1Coefficient, SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst,
|
||||
StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands,
|
||||
StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi, Stochastic,
|
||||
StochasticCci, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput,
|
||||
TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential,
|
||||
TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei,
|
||||
TdRiskLevel, TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema,
|
||||
TermStructureBasis, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside,
|
||||
ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii, TimeOfDayReturnProfile,
|
||||
TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput, TradeImbalance, TrendLabel,
|
||||
TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Trix, TrueRange, Tsf, Tsi, Tsv,
|
||||
TtmSqueeze, TtmSqueezeOutput, TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex,
|
||||
UltimateOscillator, UniqueThreeRiver, UpDownVolumeRatio, UpsideGapThreeMethods,
|
||||
UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio,
|
||||
VerticalHorizontalFilter, Vidya, VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput,
|
||||
VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput,
|
||||
Vpin, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput,
|
||||
Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate,
|
||||
Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
||||
ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
KalmanHedgeRatioOutput, Kama, KasePermissionStochastic, KellyCriterion, Keltner, KeltnerOutput,
|
||||
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
|
||||
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||
LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression,
|
||||
LiquidationFeatures, LiquidationFeaturesOutput, LogReturn, LongLeggedDoji, LongLine,
|
||||
LongShortRatio, MaEnvelope, MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput,
|
||||
Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow,
|
||||
MaxDrawdown, McClellanOscillator, McClellanSummationIndex, McGinleyDynamic,
|
||||
MedianAbsoluteDeviation, MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi,
|
||||
MinusDm, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nvi,
|
||||
OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu,
|
||||
OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1,
|
||||
OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
|
||||
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
|
||||
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
|
||||
PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo, ProfitFactor, Psar,
|
||||
Pvi, Qqe, QqeOutput, Qstick, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility,
|
||||
RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput,
|
||||
RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100,
|
||||
RogersSatchellVolatility, RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr,
|
||||
RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi,
|
||||
RviVolatility, Rwi, RwiOutput, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow,
|
||||
SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap, Shark, SharpeRatio,
|
||||
ShootingStar, ShortLine, SignedVolume, SineWave, SineWeightedMa, Skewness, Sma, Smi, Smma,
|
||||
SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands,
|
||||
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
|
||||
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
|
||||
StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput, SuperSmoother,
|
||||
SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown,
|
||||
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
|
||||
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex,
|
||||
Tii, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput,
|
||||
TradeImbalance, TrendLabel, TrendStrengthIndex, TreynorRatio, Triangle, Trima, Trin,
|
||||
TripleTopBottom, Trix, TrueRange, Tsf, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend,
|
||||
TurnOfMonth, Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
|
||||
UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput,
|
||||
ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya, VoltyStop,
|
||||
VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
|
||||
VolumeProfile, VolumeProfileOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, Vwma, Vzo, WavePm, WaveTrend, WaveTrendOutput, Wedge, WeightedClose,
|
||||
WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma, WoodiePivots,
|
||||
WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput,
|
||||
ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
};
|
||||
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
||||
// line so the indicator-count tooling (which scans the braced block above and
|
||||
|
||||
Reference in New Issue
Block a user