Add B9 Price Statistics deepening (5 indicators) (#197)

Deepens the **Price Statistics** family (B9) with five rolling-statistics indicators (447 -> 452):

- **ShannonEntropy** — Shannon entropy of a binned rolling value distribution.
- **SampleEntropy** — Richman-Moorman sample entropy (regularity/complexity of a window).
- **KendallTau** — Kendall rank correlation (tau-b) over paired observations (pairwise; distinct from Pearson/Spearman).
- **JarqueBera** — Jarque-Bera normality test statistic over a rolling window.
- **RollingMinMaxScaler** — maps the latest value to 0..1 over a rolling window.

All scalar f64 input except KendallTau (pairwise). Multi-arg scalars (Shannon/Sample entropy) use hand-written Python/Node bindings + the variadic wasm macro; KendallTau uses the pair macros. Verified locally: 3668 core lib + 410 doc tests, clippy clean, 527 node tests, 871 pytest, counter 452.
This commit is contained in:
kingchenc
2026-06-07 03:08:53 +02:00
committed by GitHub
parent 81406e7a1b
commit 389200f855
20 changed files with 1922 additions and 43 deletions
@@ -0,0 +1,263 @@
//! Jarque-Bera — a normality-test statistic on a rolling window.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Jarque-Bera — the Jarque-Bera test statistic measuring how far a window's
/// distribution departs from normal, via its **skewness** and **excess
/// kurtosis**.
///
/// ```text
/// S = skewness = m3 / m2^(3/2)
/// K = excess kurtosis = m4 / m2² 3
/// JB = (period / 6) · ( S² + K²/4 )
/// ```
///
/// where `m2`, `m3`, `m4` are the second, third and fourth central moments of the
/// window. A perfectly normal sample has zero skew and zero excess kurtosis, so
/// `JB = 0`; the statistic grows as the distribution becomes asymmetric (non-zero
/// skew) or fat- or thin-tailed (non-zero excess kurtosis). Under the null of
/// normality `JB` is asymptotically χ² with two degrees of freedom, so values
/// above roughly `6` reject normality at the 95% level — a useful streaming flag
/// for fat-tail / crash-risk regimes in a return series.
///
/// The statistic is `≥ 0`. A degenerate window with zero variance (`m2 == 0`)
/// returns `0`. The first value lands after `period` inputs; each `update`
/// recomputes the four moments over the window in O(`period`).
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, JarqueBera};
///
/// let mut indicator = JarqueBera::new(50).unwrap();
/// let mut last = None;
/// for i in 0..80 {
/// last = indicator.update((f64::from(i) * 0.3).sin());
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct JarqueBera {
period: usize,
window: VecDeque<f64>,
last: Option<f64>,
}
impl JarqueBera {
/// Construct a rolling Jarque-Bera over `period` values.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `period == 0` and
/// [`Error::InvalidPeriod`] if `period < 4` (the statistic is degenerate on
/// fewer than four points).
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
if period < 4 {
return Err(Error::InvalidPeriod {
message: "Jarque-Bera needs period >= 4",
});
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
last: None,
})
}
/// Configured window length.
pub const fn period(&self) -> usize {
self.period
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
fn compute(&self) -> f64 {
let n = self.period as f64;
let mean = self.window.iter().sum::<f64>() / n;
let mut m2 = 0.0;
let mut m3 = 0.0;
let mut m4 = 0.0;
for &v in &self.window {
let d = v - mean;
let d2 = d * d;
m2 += d2;
m3 += d2 * d;
m4 += d2 * d2;
}
m2 /= n;
m3 /= n;
m4 /= n;
if m2 == 0.0 {
return 0.0;
}
let skew = m3 / m2.powf(1.5);
let excess_kurt = m4 / (m2 * m2) - 3.0;
(n / 6.0) * (skew * skew + excess_kurt * excess_kurt / 4.0)
}
}
impl Indicator for JarqueBera {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.last;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(input);
if self.window.len() < self.period {
return None;
}
let out = self.compute();
self.last = Some(out);
Some(out)
}
fn reset(&mut self) {
self.window.clear();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"JarqueBera"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_period() {
assert!(matches!(JarqueBera::new(0), Err(Error::PeriodZero)));
assert!(matches!(
JarqueBera::new(3),
Err(Error::InvalidPeriod { .. })
));
assert!(JarqueBera::new(4).is_ok());
}
#[test]
fn accessors_and_metadata() {
let jb = JarqueBera::new(50).unwrap();
assert_eq!(jb.period(), 50);
assert_eq!(jb.warmup_period(), 50);
assert_eq!(jb.name(), "JarqueBera");
assert!(!jb.is_ready());
assert_eq!(jb.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut jb = JarqueBera::new(4).unwrap();
let out = jb.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
for v in out.iter().take(3) {
assert!(v.is_none());
}
assert!(out[3].is_some());
}
#[test]
fn constant_window_is_zero() {
let mut jb = JarqueBera::new(8).unwrap();
let last = jb.batch(&[5.0; 12]).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn output_is_non_negative() {
let mut jb = JarqueBera::new(30).unwrap();
for v in jb
.batch(
&(0..200)
.map(|i| (f64::from(i) * 0.3).sin() * 5.0)
.collect::<Vec<_>>(),
)
.into_iter()
.flatten()
{
assert!(v >= 0.0, "JB must be non-negative, got {v}");
}
}
#[test]
fn skewed_window_exceeds_symmetric() {
// A symmetric window vs. one with a heavy outlier (high skew + kurtosis).
let symmetric: Vec<f64> = vec![-3.0, -1.0, 0.0, 1.0, 3.0, -2.0, 2.0, 0.0];
let skewed: Vec<f64> = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0];
let jb_sym = JarqueBera::new(8)
.unwrap()
.batch(&symmetric)
.into_iter()
.flatten()
.last()
.unwrap();
let jb_skew = JarqueBera::new(8)
.unwrap()
.batch(&skewed)
.into_iter()
.flatten()
.last()
.unwrap();
assert!(
jb_skew > jb_sym,
"skewed ({jb_skew}) should exceed symmetric ({jb_sym})"
);
}
#[test]
fn ignores_non_finite() {
let mut jb = JarqueBera::new(4).unwrap();
let ready = jb
.batch(&[1.0, 2.0, 3.0, 5.0])
.into_iter()
.flatten()
.last()
.unwrap();
assert_eq!(jb.update(f64::NAN), Some(ready));
}
#[test]
fn reset_clears_state() {
let mut jb = JarqueBera::new(4).unwrap();
jb.batch(&[1.0, 2.0, 3.0, 5.0]);
assert!(jb.is_ready());
jb.reset();
assert!(!jb.is_ready());
assert_eq!(jb.value(), None);
assert_eq!(jb.update(1.0), None);
}
#[test]
fn batch_equals_streaming() {
let xs: Vec<f64> = (0..120)
.map(|i| (f64::from(i) * 0.25).sin() * 9.0)
.collect();
let batch = JarqueBera::new(30).unwrap().batch(&xs);
let mut b = JarqueBera::new(30).unwrap();
let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,296 @@
//! Kendall's tau-b — rank correlation by concordant vs. discordant pairs.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// `+1` / `0` / `-1` sign of `a b`.
fn sign(a: f64, b: f64) -> i32 {
if a > b {
1
} else if a < b {
-1
} else {
0
}
}
/// Kendall's tau-b — a rank correlation between two synchronised series based on
/// the balance of **concordant** and **discordant** pairs, with a tie correction.
///
/// ```text
/// over all pairs (i < j) in the window:
/// concordant if (x_j x_i) and (y_j y_i) share a sign
/// discordant if they have opposite signs
/// tie_x / tie_y if the respective difference is zero
/// n0 = N(N1)/2
/// tau_b = (n_concordant n_discordant) / sqrt((n0 tie_x)(n0 tie_y))
/// ```
///
/// Where [`PearsonCorrelation`](crate::PearsonCorrelation) measures *linear*
/// co-movement and [`SpearmanCorrelation`](crate::SpearmanCorrelation) correlates
/// ranks via their differences, Kendall's tau counts how often the two series move
/// the **same direction** between every pair of observations. It is the most
/// robust of the three to outliers and to non-linear-but-monotonic
/// relationships, and the tau-b form corrects for ties so repeated values do not
/// bias it. The output is in `[1, +1]`: `+1` perfectly concordant, `1`
/// perfectly discordant, `0` no monotonic association.
///
/// The window holds the last `period` pairs and is recomputed each bar in
/// O(`period²`). A window with no untied pairs on one side returns `0`. The first
/// value lands after `period` inputs.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, KendallTau};
///
/// let mut indicator = KendallTau::new(20).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// let x = f64::from(i);
/// last = indicator.update((x, 2.0 * x)); // perfectly concordant
/// }
/// assert!((last.unwrap() - 1.0).abs() < 1e-9);
/// ```
#[derive(Debug, Clone)]
pub struct KendallTau {
period: usize,
window: VecDeque<(f64, f64)>,
last: Option<f64>,
}
impl KendallTau {
/// Construct a rolling Kendall's tau-b over `period` pairs.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 2` (a correlation needs at
/// least two pairs).
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "Kendall tau needs period >= 2",
});
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
last: None,
})
}
/// Configured window of pairs.
pub const fn period(&self) -> usize {
self.period
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
fn compute(&self) -> f64 {
let pairs: Vec<(f64, f64)> = self.window.iter().copied().collect();
let len = pairs.len();
let mut concordant: i64 = 0;
let mut discordant: i64 = 0;
let mut tie_x: i64 = 0;
let mut tie_y: i64 = 0;
for i in 0..len {
for j in (i + 1)..len {
let sx = sign(pairs[j].0, pairs[i].0);
let sy = sign(pairs[j].1, pairs[i].1);
if sx == 0 {
tie_x += 1;
}
if sy == 0 {
tie_y += 1;
}
let prod = sx * sy;
if prod > 0 {
concordant += 1;
} else if prod < 0 {
discordant += 1;
}
}
}
let n0 = (len * (len - 1) / 2) as f64;
let denom = ((n0 - tie_x as f64) * (n0 - tie_y as f64)).sqrt();
if denom == 0.0 {
return 0.0;
}
((concordant - discordant) as f64 / denom).clamp(-1.0, 1.0)
}
}
impl Indicator for KendallTau {
type Input = (f64, f64);
type Output = f64;
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(input);
if self.window.len() < self.period {
return None;
}
let out = self.compute();
self.last = Some(out);
Some(out)
}
fn reset(&mut self) {
self.window.clear();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"KendallTau"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_below_two() {
assert!(matches!(
KendallTau::new(1),
Err(Error::InvalidPeriod { .. })
));
assert!(KendallTau::new(2).is_ok());
}
#[test]
fn accessors_and_metadata() {
let k = KendallTau::new(20).unwrap();
assert_eq!(k.period(), 20);
assert_eq!(k.warmup_period(), 20);
assert_eq!(k.name(), "KendallTau");
assert!(!k.is_ready());
assert_eq!(k.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut k = KendallTau::new(4).unwrap();
let out = k.batch(&[(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0), (5.0, 5.0)]);
for v in out.iter().take(3) {
assert!(v.is_none());
}
assert!(out[3].is_some());
}
#[test]
fn monotone_increasing_is_one() {
let pairs: Vec<(f64, f64)> = (0..20)
.map(|i| (f64::from(i), 2.0 * f64::from(i) + 1.0))
.collect();
let last = KendallTau::new(10)
.unwrap()
.batch(&pairs)
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 1.0, epsilon = 1e-9);
}
#[test]
fn monotone_decreasing_is_minus_one() {
let pairs: Vec<(f64, f64)> = (0..20)
.map(|i| (f64::from(i), -3.0 * f64::from(i)))
.collect();
let last = KendallTau::new(10)
.unwrap()
.batch(&pairs)
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, -1.0, epsilon = 1e-9);
}
#[test]
fn constant_channel_yields_zero() {
// y constant -> every y-difference is a tie -> denom 0 -> 0.
let pairs: Vec<(f64, f64)> = (0..20).map(|i| (f64::from(i), 7.0)).collect();
let last = KendallTau::new(8)
.unwrap()
.batch(&pairs)
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn output_in_range() {
let pairs: Vec<(f64, f64)> = (0..80)
.map(|i| {
let t = f64::from(i);
(100.0 + t.sin() * 5.0, 50.0 + (t * 0.3).cos() * 3.0)
})
.collect();
for v in KendallTau::new(20)
.unwrap()
.batch(&pairs)
.into_iter()
.flatten()
{
assert!((-1.0..=1.0).contains(&v));
}
}
#[test]
fn reset_clears_state() {
let mut k = KendallTau::new(4).unwrap();
k.batch(&[(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)]);
assert!(k.is_ready());
k.reset();
assert!(!k.is_ready());
assert_eq!(k.value(), None);
assert_eq!(k.update((1.0, 1.0)), None);
}
#[test]
fn batch_equals_streaming() {
let pairs: Vec<(f64, f64)> = (0..60)
.map(|i| {
let t = f64::from(i);
(t.sin(), (t * 0.5).cos())
})
.collect();
let batch = KendallTau::new(14).unwrap().batch(&pairs);
let mut b = KendallTau::new(14).unwrap();
let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn ties_are_corrected() {
// Tied x values (points 0 and 1) and tied y values (points 1 and 2)
// exercise the tie_x / tie_y correction counters.
let mut k = KendallTau::new(4).unwrap();
assert_eq!(k.update((1.0, 1.0)), None);
assert_eq!(k.update((1.0, 2.0)), None);
assert_eq!(k.update((2.0, 2.0)), None);
let v = k.update((3.0, 3.0)).unwrap();
assert!((-1.0..=1.0).contains(&v), "got {v}");
}
}
+16 -1
View File
@@ -191,6 +191,7 @@ mod intraday_momentum_index;
mod intraday_volatility_profile;
mod inverse_fisher_transform;
mod inverted_hammer;
mod jarque_bera;
mod jma;
mod jump_indicator;
mod kagi_bars;
@@ -200,6 +201,7 @@ mod kase_devstop;
mod kase_permission_stochastic;
mod kelly_criterion;
mod keltner;
mod kendall_tau;
mod kicking;
mod kicking_by_length;
mod kst;
@@ -314,6 +316,7 @@ mod roll_measure;
mod rolling_correlation;
mod rolling_covariance;
mod rolling_iqr;
mod rolling_min_max_scaler;
mod rolling_percentile_rank;
mod rolling_quantile;
mod roofing_filter;
@@ -322,12 +325,14 @@ mod rsx;
mod rvi;
mod rvi_volatility;
mod rwi;
mod sample_entropy;
mod sar_ext;
mod seasonal_z_score;
mod separating_lines;
mod session_high_low;
mod session_range;
mod session_vwap;
mod shannon_entropy;
mod shark;
mod sharpe_ratio;
mod shooting_star;
@@ -638,6 +643,7 @@ pub use intraday_momentum_index::IntradayMomentumIndex;
pub use intraday_volatility_profile::{IntradayVolatilityProfile, IntradayVolatilityProfileOutput};
pub use inverse_fisher_transform::InverseFisherTransform;
pub use inverted_hammer::InvertedHammer;
pub use jarque_bera::JarqueBera;
pub use jma::Jma;
pub use jump_indicator::JumpIndicator;
pub use kagi_bars::{KagiBar, KagiBars};
@@ -647,6 +653,7 @@ pub use kase_devstop::{KaseDevStop, KaseDevStopOutput};
pub use kase_permission_stochastic::{KasePermissionStochastic, KasePermissionStochasticOutput};
pub use kelly_criterion::KellyCriterion;
pub use keltner::{Keltner, KeltnerOutput};
pub use kendall_tau::KendallTau;
pub use kicking::Kicking;
pub use kicking_by_length::KickingByLength;
pub use kst::{Kst, KstOutput};
@@ -761,6 +768,7 @@ pub use roll_measure::RollMeasure;
pub use rolling_correlation::RollingCorrelation;
pub use rolling_covariance::RollingCovariance;
pub use rolling_iqr::RollingIqr;
pub use rolling_min_max_scaler::RollingMinMaxScaler;
pub use rolling_percentile_rank::RollingPercentileRank;
pub use rolling_quantile::RollingQuantile;
pub use roofing_filter::RoofingFilter;
@@ -769,12 +777,14 @@ pub use rsx::Rsx;
pub use rvi::Rvi;
pub use rvi_volatility::RviVolatility;
pub use rwi::{Rwi, RwiOutput};
pub use sample_entropy::SampleEntropy;
pub use sar_ext::SarExt;
pub use seasonal_z_score::SeasonalZScore;
pub use separating_lines::SeparatingLines;
pub use session_high_low::{SessionHighLow, SessionHighLowOutput};
pub use session_range::{SessionRange, SessionRangeOutput};
pub use session_vwap::SessionVwap;
pub use shannon_entropy::ShannonEntropy;
pub use shark::Shark;
pub use sharpe_ratio::SharpeRatio;
pub use shooting_star::ShootingStar;
@@ -1191,6 +1201,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"BodySizePct",
"WickRatio",
"HighLowRange",
"JarqueBera",
"RollingMinMaxScaler",
"ShannonEntropy",
"SampleEntropy",
"KendallTau",
],
),
(
@@ -1495,6 +1510,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, 447, "FAMILIES total drifted from indicator count");
assert_eq!(total, 452, "FAMILIES total drifted from indicator count");
}
}
@@ -0,0 +1,261 @@
//! Rolling Min-Max Scaler — normalises the latest value to `[0, 1]` over a window.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Rolling Min-Max Scaler — maps the current value onto `[0, 1]` relative to the
/// minimum and maximum of the trailing window.
///
/// ```text
/// scaled = (x min(window)) / (max(window) min(window))
/// ```
///
/// This is the streaming form of scikit-learn's `MinMaxScaler` applied over a
/// sliding window: `0` means the value is the lowest in the window, `1` the
/// highest, `0.5` the midpoint of the range. It is the engine behind oscillators
/// like the Stochastic %K and a handy normaliser for feeding any indicator into a
/// bounded model input. Because it rescales to the window's own range it is
/// scale-free across instruments.
///
/// The output is in `[0, 1]`. A flat window (`max == min`) has no range to scale
/// against and returns the neutral `0.5`. The first value lands after `period`
/// inputs; each `update` scans the window in O(`period`).
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, RollingMinMaxScaler};
///
/// let mut indicator = RollingMinMaxScaler::new(14).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// last = indicator.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct RollingMinMaxScaler {
period: usize,
window: VecDeque<f64>,
last: Option<f64>,
}
impl RollingMinMaxScaler {
/// Construct a rolling min-max scaler over `period` values.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `period == 0` and
/// [`Error::InvalidPeriod`] if `period < 2` (a range needs two points).
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
if period < 2 {
return Err(Error::InvalidPeriod {
message: "min-max scaler needs period >= 2",
});
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
last: None,
})
}
/// Configured window length.
pub const fn period(&self) -> usize {
self.period
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
}
impl Indicator for RollingMinMaxScaler {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.last;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(input);
if self.window.len() < self.period {
return None;
}
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for &v in &self.window {
min = min.min(v);
max = max.max(v);
}
let range = max - min;
let scaled = if range > 0.0 {
(input - min) / range
} else {
0.5
};
self.last = Some(scaled);
Some(scaled)
}
fn reset(&mut self) {
self.window.clear();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"RollingMinMaxScaler"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_period() {
assert!(matches!(
RollingMinMaxScaler::new(0),
Err(Error::PeriodZero)
));
assert!(matches!(
RollingMinMaxScaler::new(1),
Err(Error::InvalidPeriod { .. })
));
assert!(RollingMinMaxScaler::new(2).is_ok());
}
#[test]
fn accessors_and_metadata() {
let s = RollingMinMaxScaler::new(14).unwrap();
assert_eq!(s.period(), 14);
assert_eq!(s.warmup_period(), 14);
assert_eq!(s.name(), "RollingMinMaxScaler");
assert!(!s.is_ready());
assert_eq!(s.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut s = RollingMinMaxScaler::new(4).unwrap();
let out = s.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
for v in out.iter().take(3) {
assert!(v.is_none());
}
assert!(out[3].is_some());
}
#[test]
fn highest_in_window_is_one() {
let mut s = RollingMinMaxScaler::new(4).unwrap();
// last value is the highest -> 1.0.
let last = s
.batch(&[1.0, 2.0, 3.0, 4.0])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 1.0, epsilon = 1e-12);
}
#[test]
fn lowest_in_window_is_zero() {
let mut s = RollingMinMaxScaler::new(4).unwrap();
let last = s
.batch(&[4.0, 3.0, 2.0, 1.0])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn midpoint_is_half() {
let mut s = RollingMinMaxScaler::new(3).unwrap();
// window [0, 2, 1]: min 0, max 2, current 1 -> 0.5.
let last = s
.batch(&[0.0, 2.0, 1.0])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.5, epsilon = 1e-12);
}
#[test]
fn flat_window_is_half() {
let mut s = RollingMinMaxScaler::new(4).unwrap();
let last = s.batch(&[7.0; 8]).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.5, epsilon = 1e-12);
}
#[test]
fn output_in_range() {
let mut s = RollingMinMaxScaler::new(14).unwrap();
for v in s
.batch(
&(0..200)
.map(|i| (f64::from(i) * 0.3).sin() * 10.0)
.collect::<Vec<_>>(),
)
.into_iter()
.flatten()
{
assert!((0.0..=1.0).contains(&v));
}
}
#[test]
fn ignores_non_finite() {
let mut s = RollingMinMaxScaler::new(4).unwrap();
let ready = s
.batch(&[1.0, 2.0, 3.0, 4.0])
.into_iter()
.flatten()
.last()
.unwrap();
assert_eq!(s.update(f64::NAN), Some(ready));
}
#[test]
fn reset_clears_state() {
let mut s = RollingMinMaxScaler::new(4).unwrap();
s.batch(&[1.0, 2.0, 3.0, 4.0]);
assert!(s.is_ready());
s.reset();
assert!(!s.is_ready());
assert_eq!(s.value(), None);
assert_eq!(s.update(1.0), None);
}
#[test]
fn batch_equals_streaming() {
let xs: Vec<f64> = (0..120)
.map(|i| (f64::from(i) * 0.25).sin() * 9.0)
.collect();
let batch = RollingMinMaxScaler::new(14).unwrap().batch(&xs);
let mut b = RollingMinMaxScaler::new(14).unwrap();
let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,337 @@
//! Sample Entropy (`SampEn`) — the regularity / predictability of a window.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Population standard deviation of a slice (used for the matching tolerance).
fn population_stddev(window: &[f64]) -> f64 {
let n = window.len() as f64;
let mean = window.iter().sum::<f64>() / n;
let var = window.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / n;
var.max(0.0).sqrt()
}
/// Whether two length-`len` templates starting at `i` and `j` match within the
/// Chebyshev tolerance `tol`.
fn templates_match(window: &[f64], i: usize, j: usize, len: usize, tol: f64) -> bool {
for k in 0..len {
if (window[i + k] - window[j + k]).abs() > tol {
return false;
}
}
true
}
/// Sample Entropy (`SampEn`) — Richman & Moorman's measure of how *regular* (i.e.
/// predictable) a series is: the negative log conditional probability that two
/// sub-sequences similar for `m` points stay similar at the next point.
///
/// ```text
/// tol = r_factor · stddev(window)
/// B = # template pairs of length m within tol (i < j)
/// A = # template pairs of length m+1 within tol (i < j)
/// `SampEn` = ln(A / B)
/// ```
///
/// Low `SampEn` means the window is **regular** — patterns of length `m` reliably
/// extend to length `m + 1`, the fingerprint of a trending or cyclic market. High
/// `SampEn` means the series is **irregular** — knowing the last `m` points tells
/// you little about the next, the fingerprint of noise. Unlike the older
/// approximate entropy (`ApEn`), `SampEn` excludes self-matches, so it is far less
/// biased on short windows.
///
/// The tolerance is `r_factor` times the window's standard deviation, so the
/// measure self-scales. A perfectly flat window (`stddev == 0`) is maximally
/// regular and returns `0`. If no length-`m` pairs match, the entropy is
/// undefined and `0` is returned; if length-`m` pairs match but none extend, the
/// estimator falls back to treating the unseen count as one (`ln(1/B) = ln(B)`).
/// The first value lands after `period` inputs; each `update` is O(`period²`).
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, SampleEntropy};
///
/// let mut indicator = SampleEntropy::new(50, 2, 0.2).unwrap();
/// let mut last = None;
/// for i in 0..80 {
/// last = indicator.update((f64::from(i) * 0.3).sin() * 5.0);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct SampleEntropy {
period: usize,
emb_dim: usize,
r_factor: f64,
window: VecDeque<f64>,
last: Option<f64>,
}
impl SampleEntropy {
/// Construct a Sample Entropy over `period` values with embedding dimension
/// `m` and tolerance factor `r_factor`.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `period` or `m` is `0`,
/// [`Error::InvalidPeriod`] if `period < m + 2` (no length-`m+1` template
/// pairs otherwise), and [`Error::InvalidParameter`] if `r_factor` is not
/// finite and positive.
pub fn new(period: usize, m: usize, r_factor: f64) -> Result<Self> {
if period == 0 || m == 0 {
return Err(Error::PeriodZero);
}
if period < m + 2 {
return Err(Error::InvalidPeriod {
message: "sample entropy needs period >= m + 2",
});
}
if !r_factor.is_finite() || r_factor <= 0.0 {
return Err(Error::InvalidParameter {
message: "sample entropy r_factor must be finite and positive",
});
}
Ok(Self {
period,
emb_dim: m,
r_factor,
window: VecDeque::with_capacity(period),
last: None,
})
}
/// Configured `(period, m, r_factor)`.
pub const fn params(&self) -> (usize, usize, f64) {
(self.period, self.emb_dim, self.r_factor)
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
fn compute(&self) -> f64 {
let window: Vec<f64> = self.window.iter().copied().collect();
let std = population_stddev(&window);
if std == 0.0 {
return 0.0;
}
let tol = self.r_factor * std;
let m = self.emb_dim;
// Restrict both template lengths to the same index range so A and B share
// their candidate pairs: there are `period m` length-(m+1) templates.
let count = self.period - m;
let mut matches_m = 0u64;
let mut matches_m1 = 0u64;
for i in 0..count {
for j in (i + 1)..count {
if templates_match(&window, i, j, m, tol) {
matches_m += 1;
if templates_match(&window, i, j, m + 1, tol) {
matches_m1 += 1;
}
}
}
}
if matches_m == 0 {
return 0.0;
}
if matches_m1 == 0 {
// No length-(m+1) matches: fall back to one unseen count.
return (matches_m as f64).ln();
}
-((matches_m1 as f64) / (matches_m as f64)).ln()
}
}
impl Indicator for SampleEntropy {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.last;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(input);
if self.window.len() < self.period {
return None;
}
let out = self.compute();
self.last = Some(out);
Some(out)
}
fn reset(&mut self) {
self.window.clear();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"SampleEntropy"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_params() {
assert!(matches!(
SampleEntropy::new(0, 2, 0.2),
Err(Error::PeriodZero)
));
assert!(matches!(
SampleEntropy::new(50, 0, 0.2),
Err(Error::PeriodZero)
));
assert!(matches!(
SampleEntropy::new(3, 2, 0.2),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
SampleEntropy::new(50, 2, 0.0),
Err(Error::InvalidParameter { .. })
));
}
#[test]
fn accessors_and_metadata() {
let s = SampleEntropy::new(50, 2, 0.2).unwrap();
assert_eq!(s.params(), (50, 2, 0.2));
assert_eq!(s.warmup_period(), 50);
assert_eq!(s.name(), "SampleEntropy");
assert!(!s.is_ready());
assert_eq!(s.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut s = SampleEntropy::new(10, 2, 0.2).unwrap();
let xs: Vec<f64> = (0..14).map(|i| (f64::from(i) * 0.5).sin()).collect();
let out = s.batch(&xs);
for v in out.iter().take(9) {
assert!(v.is_none());
}
assert!(out[9].is_some());
}
#[test]
fn constant_window_is_zero() {
let mut s = SampleEntropy::new(20, 2, 0.2).unwrap();
let last = s.batch(&[5.0; 30]).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn output_is_non_negative() {
let mut s = SampleEntropy::new(40, 2, 0.2).unwrap();
for v in s
.batch(
&(0..200)
.map(|i| (f64::from(i) * 0.3).sin() * 5.0)
.collect::<Vec<_>>(),
)
.into_iter()
.flatten()
{
assert!(v >= 0.0, "sample entropy must be non-negative, got {v}");
}
}
#[test]
fn regular_below_irregular() {
// A smooth sine is far more regular (lower `SampEn`) than a chaotic
// logistic-map series. (An *alternating* series would be periodic, hence
// regular too -- chaos is what makes the window genuinely unpredictable.)
let smooth: Vec<f64> = (0..60).map(|i| (f64::from(i) * 0.2).sin() * 5.0).collect();
let mut x = 0.37_f64;
let chaotic: Vec<f64> = (0..60)
.map(|_| {
x = 3.99 * x * (1.0 - x);
x * 5.0
})
.collect();
let s_smooth = SampleEntropy::new(50, 2, 0.2)
.unwrap()
.batch(&smooth)
.into_iter()
.flatten()
.last()
.unwrap();
let s_chaotic = SampleEntropy::new(50, 2, 0.2)
.unwrap()
.batch(&chaotic)
.into_iter()
.flatten()
.last()
.unwrap();
assert!(
s_smooth <= s_chaotic,
"smooth ({s_smooth}) should be <= chaotic ({s_chaotic})"
);
}
#[test]
fn ignores_non_finite() {
let mut s = SampleEntropy::new(10, 2, 0.2).unwrap();
let xs: Vec<f64> = (0..10).map(|i| (f64::from(i) * 0.5).sin()).collect();
let ready = s.batch(&xs).into_iter().flatten().last().unwrap();
assert_eq!(s.update(f64::NAN), Some(ready));
}
#[test]
fn reset_clears_state() {
let mut s = SampleEntropy::new(10, 2, 0.2).unwrap();
let xs: Vec<f64> = (0..10).map(|i| (f64::from(i) * 0.5).sin()).collect();
s.batch(&xs);
assert!(s.is_ready());
s.reset();
assert!(!s.is_ready());
assert_eq!(s.value(), None);
assert_eq!(s.update(1.0), None);
}
#[test]
fn batch_equals_streaming() {
let xs: Vec<f64> = (0..120)
.map(|i| (f64::from(i) * 0.25).sin() * 9.0)
.collect();
let batch = SampleEntropy::new(40, 2, 0.2).unwrap().batch(&xs);
let mut b = SampleEntropy::new(40, 2, 0.2).unwrap();
let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn falls_back_when_no_m_plus_one_matches() {
// `[1, 1, 1, 5]` with m = 2: the length-2 template `(1, 1)` repeats
// (matches_m > 0) but no length-3 template repeats (matches_m1 == 0),
// so SampEn takes the `ln(matches_m)` fallback branch.
let xs = [1.0, 1.0, 1.0, 5.0];
let v = SampleEntropy::new(4, 2, 0.2)
.unwrap()
.batch(&xs)
.into_iter()
.flatten()
.last()
.unwrap();
assert!(v.is_finite() && v >= 0.0, "got {v}");
}
}
@@ -0,0 +1,261 @@
//! Shannon Entropy — the information content of a price window's distribution.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Shannon Entropy — the Shannon information entropy (in **bits**) of the
/// distribution of values in a rolling window, after binning them into a fixed
/// number of equal-width buckets.
///
/// ```text
/// bucket each of the last `period` values into `bins` equal-width bins over
/// [min, max] of the window
/// p_i = count_i / period
/// H = Σ p_i · log2(p_i) (over non-empty bins)
/// ```
///
/// Entropy measures how *spread out* and unpredictable the recent values are. A
/// window concentrated in one bin (a flat or tightly-ranging market) has low
/// entropy near `0`; a window whose values are spread evenly across all bins (a
/// noisy, directionless market) approaches the maximum `log2(bins)`. Traders use
/// it as a **regime filter**: low entropy favours trend/breakout strategies, high
/// entropy favours mean-reversion or standing aside.
///
/// The output lies in `[0, log2(bins)]`. A degenerate window where every value is
/// identical (`max == min`) returns `0`. The first value lands after `period`
/// inputs; each `update` rebins the window in O(`period`).
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, ShannonEntropy};
///
/// let mut indicator = ShannonEntropy::new(32, 8).unwrap();
/// let mut last = None;
/// for i in 0..64 {
/// last = indicator.update((f64::from(i) * 0.7).sin() * 10.0);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct ShannonEntropy {
period: usize,
bins: usize,
window: VecDeque<f64>,
last: Option<f64>,
}
impl ShannonEntropy {
/// Construct a Shannon entropy over `period` values binned into `bins`
/// buckets.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if either argument is `0`, or
/// [`Error::InvalidPeriod`] if `bins < 2` (entropy needs at least two bins).
pub fn new(period: usize, bins: usize) -> Result<Self> {
if period == 0 || bins == 0 {
return Err(Error::PeriodZero);
}
if bins < 2 {
return Err(Error::InvalidPeriod {
message: "Shannon entropy needs bins >= 2",
});
}
Ok(Self {
period,
bins,
window: VecDeque::with_capacity(period),
last: None,
})
}
/// Configured `(period, bins)`.
pub const fn params(&self) -> (usize, usize) {
(self.period, self.bins)
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
}
impl Indicator for ShannonEntropy {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.last;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(input);
if self.window.len() < self.period {
return None;
}
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for &v in &self.window {
min = min.min(v);
max = max.max(v);
}
if max <= min {
// Degenerate window: all values identical -> zero entropy.
self.last = Some(0.0);
return Some(0.0);
}
let width = (max - min) / self.bins as f64;
let mut counts = vec![0usize; self.bins];
for &v in &self.window {
// `(v - min) / width` is in [0, bins]; the cast truncates toward zero
// (intended) and the value is non-negative, then clamped to the last
// bin so the index is always valid.
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let raw = ((v - min) / width) as usize;
let idx = raw.min(self.bins - 1);
counts[idx] += 1;
}
let n = self.period as f64;
let mut h = 0.0;
for &count in &counts {
if count > 0 {
let p = count as f64 / n;
h -= p * p.log2();
}
}
self.last = Some(h);
Some(h)
}
fn reset(&mut self) {
self.window.clear();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"ShannonEntropy"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_params() {
assert!(matches!(ShannonEntropy::new(0, 8), Err(Error::PeriodZero)));
assert!(matches!(ShannonEntropy::new(32, 0), Err(Error::PeriodZero)));
assert!(matches!(
ShannonEntropy::new(32, 1),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let e = ShannonEntropy::new(32, 8).unwrap();
assert_eq!(e.params(), (32, 8));
assert_eq!(e.warmup_period(), 32);
assert_eq!(e.name(), "ShannonEntropy");
assert!(!e.is_ready());
assert_eq!(e.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut e = ShannonEntropy::new(4, 4).unwrap();
let out = e.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
for v in out.iter().take(3) {
assert!(v.is_none());
}
assert!(out[3].is_some());
}
#[test]
fn constant_window_is_zero() {
let mut e = ShannonEntropy::new(8, 4).unwrap();
let last = e.batch(&[5.0; 12]).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn uniform_window_is_max_entropy() {
// One value per bin -> uniform distribution -> H = log2(bins).
let mut e = ShannonEntropy::new(4, 4).unwrap();
// Values 0,1,2,3 with min=0,max=3,width=0.75 -> bins 0,1,2,3.
let last = e
.batch(&[0.0, 1.0, 2.0, 3.0])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 2.0, epsilon = 1e-9); // log2(4) = 2
}
#[test]
fn output_in_range() {
let mut e = ShannonEntropy::new(32, 8).unwrap();
let max_h = 8f64.log2();
for v in e
.batch(
&(0..200)
.map(|i| (f64::from(i) * 0.3).sin() * 10.0)
.collect::<Vec<_>>(),
)
.into_iter()
.flatten()
{
assert!((0.0..=max_h + 1e-9).contains(&v));
}
}
#[test]
fn ignores_non_finite() {
let mut e = ShannonEntropy::new(4, 4).unwrap();
let ready = e
.batch(&[1.0, 2.0, 3.0, 4.0])
.into_iter()
.flatten()
.last()
.unwrap();
assert_eq!(e.update(f64::NAN), Some(ready));
}
#[test]
fn reset_clears_state() {
let mut e = ShannonEntropy::new(4, 4).unwrap();
e.batch(&[1.0, 2.0, 3.0, 4.0]);
assert!(e.is_ready());
e.reset();
assert!(!e.is_ready());
assert_eq!(e.value(), None);
assert_eq!(e.update(1.0), None);
}
#[test]
fn batch_equals_streaming() {
let xs: Vec<f64> = (0..120)
.map(|i| (f64::from(i) * 0.25).sin() * 9.0)
.collect();
let batch = ShannonEntropy::new(32, 8).unwrap().batch(&xs);
let mut b = ShannonEntropy::new(32, 8).unwrap();
let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
+33 -32
View File
@@ -93,39 +93,40 @@ pub use indicators::{
HtTrendMode, HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput,
IdenticalThreeCrows, InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
InstantaneousTrendline, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile,
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, Jma, JumpIndicator,
KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop, KaseDevStopOutput,
KasePermissionStochastic, KasePermissionStochasticOutput, 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, MacdHistogram, MacdIndicator,
MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex, MatHold,
MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex, McGinleyDynamic,
MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa, MedianPrice, Mfi,
Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop, ModifiedMaStopOutput, Mom,
MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nrtr, NrtrOutput, 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, PpoHistogram,
ProfitFactor, ProjectionBands, ProjectionBandsOutput, ProjectionOscillator, Psar, Pvi, Qqe,
QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, 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,
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, JarqueBera, Jma,
JumpIndicator, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop,
KaseDevStopOutput, KasePermissionStochastic, KasePermissionStochasticOutput, KellyCriterion,
Keltner, KeltnerOutput, KendallTau, 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, MacdHistogram,
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa,
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nrtr,
NrtrOutput, 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, PpoHistogram, ProfitFactor, ProjectionBands,
ProjectionBandsOutput, ProjectionOscillator, Psar, Pvi, Qqe, QqeOutput, Qstick, QuartileBands,
QuartileBandsOutput, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility,
RecoveryFactor, RectangleRange, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput,
RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100,
RogersSatchellVolatility, RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr,
RollingMinMaxScaler, RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi,
Rsx, Rvi, RviVolatility, Rwi, RwiOutput, SampleEntropy, 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,
SessionVwap, ShannonEntropy, 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,