feat(core): B4 price oscillators (TsfOscillator, MacdHistogram, PpoHistogram) (#184)

Adds three **Price Oscillators** family indicators (420 → 423).

## Indicators

- **TsfOscillator** — `100·(close − TSF)/close`, the percentage gap of the close to the **one-bar-ahead** time-series forecast. Close-relative companion to `Cfo`, which measures the same gap against the regression value at the *current* bar; the two differ by exactly the slope term `100·b/close`.
- **MacdHistogram** — the standalone `macd − signal` bar of MACD exposed as a plain `f64` series.
- **PpoHistogram** — the Percentage Price Oscillator with its 9-period signal EMA and the resulting scale-free, zero-centered histogram (PPO itself only emits the line).

All three are scalar `f64` indicators wrapping existing, already-tested building blocks (`MacdIndicator`, `Ppo` + `Ema`, `Tsf`).

## Scope notes (VORAB-CHECK)

The B4 roadmap listed six items; three were dropped to avoid duplicates:
- *Forecast Oscillator* already ships as `Cfo`.
- *Derivative Oscillator* already ships (`DerivativeOscillator`, B2).
- *Detrended Synthetic Price* deferred — no citable formula distinct from the existing `Apo`/`Dpo`.

## Touchpoints

Core (`tsf_oscillator.rs`, `macd_histogram.rs`, `ppo_histogram.rs`) with full per-branch unit tests, `mod.rs`/`lib.rs`, python/node/wasm bindings (wasm via typed-arg macro, python/node hand-written for the multi-arg histograms), fuzz drivers, python reference + streaming-vs-batch tests, node factories, README family row + counter, CHANGELOG.

Local verify: `cargo test --workspace` green, `clippy -D warnings` clean, node 498 tests, full python suite green.
This commit is contained in:
kingchenc
2026-06-04 19:36:43 +02:00
committed by GitHub
parent d36d514f56
commit 1f4bf9e3a6
17 changed files with 942 additions and 19 deletions
+3
View File
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- **TSF Oscillator** — the percentage gap of the close to the one-bar-ahead time-series forecast, a close-relative companion to CFO (`TsfOscillator`).
- **MACD Histogram** — the standalone macd-minus-signal bar of MACD as a scalar series (`MacdHistogram`).
- **PPO Histogram** — the Percentage Price Oscillator with its signal EMA and the resulting zero-centered histogram (`PpoHistogram`).
## [0.5.7] - 2026-06-04
- **Qstick** — Qstick (Chande), the SMA of the candle body (close open) as a net buying/selling pressure gauge (`QSTICK`).
+5 -5
View File
@@ -1,5 +1,5 @@
<p align="center">
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=420" alt="Wickra — streaming-first technical indicators" width="100%"></a>
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=423" alt="Wickra — streaming-first technical indicators" width="100%"></a>
</p>
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
@@ -48,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM).
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
every one of the 420 indicators; start at the
every one of the 423 indicators; start at the
[indicators overview](https://docs.wickra.org/Indicators-Overview).
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
@@ -136,7 +136,7 @@ python -m benchmarks.compare_libraries
## Indicators
420 streaming-first indicators across twenty-four families. Every one passes the
423 streaming-first indicators across twenty-four families. Every one passes the
`batch == streaming` equivalence test, reference-value tests, and reset
semantics tests. Each has a per-indicator deep dive (formula, parameters,
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
@@ -146,7 +146,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, SWMA, GMA, EHMA, Median MA, Adaptive Laguerre, GD, Holt-Winters |
| Momentum Oscillators | RSI (Wilder), Anchored RSI, Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia, ROC Percentage (ROCP), ROC Ratio (ROCR), ROC Ratio 100 (ROCR100), Disparity Index, Fisher RSI, RSX, Dynamic Momentum Index, Stochastic CCI, RMI, Derivative Oscillator, Elder Ray, Intraday Momentum Index, QQE |
| Trend & Directional | MACD, MACD Fixed (MACDFIX), MACD Extended (MACDEXT), ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter, Plus DM, Minus DM, Plus DI, Minus DI, DX, TTM Trend, Trend Strength Index, Qstick, Polarized Fractal Efficiency, Wave PM, Gator Oscillator, Kase Permission Stochastic |
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC |
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC, TSF Oscillator, MACD Histogram, PPO Histogram |
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility |
| Bands & Channels | MA Envelope, Acceleration Bands, STARC Bands, ATR Bands, Hurst Channel, LinReg Channel, Standard Error Bands, Double Bollinger Bands, TTM Squeeze, Fractal Chaos Bands, VWAP StdDev Bands |
| Trailing Stops | Parabolic SAR, Parabolic SAR Extended (SAREXT), SuperTrend, Chandelier Exit, Chande Kroll Stop, ATR Trailing Stop, HiLo Activator, Volty Stop, Yo-Yo Exit, Donchian Channel Stop, Percentage Trailing Stop, Step Trailing Stop, Renko Trailing Stop |
@@ -245,7 +245,7 @@ A Python live-trading example using the public `websockets` package lives at
```
wickra/
├── crates/
│ ├── wickra-core/ core engine + all 420 indicators
│ ├── wickra-core/ core engine + all 423 indicators
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
├── bindings/
@@ -28,6 +28,9 @@ function num(v) {
// --- Scalar indicators: update(value) vs batch(prices) ---
const scalarFactories = {
PpoHistogram: () => new wickra.PpoHistogram(3, 6, 3),
MacdHistogram: () => new wickra.MacdHistogram(3, 6, 3),
TsfOscillator: () => new wickra.TsfOscillator(3),
WAVE_PM: () => new wickra.WAVE_PM(32, 3),
POLARIZED_FRACTAL_EFFICIENCY: () => new wickra.POLARIZED_FRACTAL_EFFICIENCY(10, 5),
TREND_STRENGTH_INDEX: () => new wickra.TREND_STRENGTH_INDEX(20),
+27
View File
@@ -978,6 +978,15 @@ export declare class TREND_STRENGTH_INDEX {
isReady(): boolean
warmupPeriod(): number
}
export type TsfOscillatorNode = TsfOscillator
export declare class TsfOscillator {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type JumpIndicatorNode = JumpIndicator
export declare class JumpIndicator {
constructor(period: number, threshold: number)
@@ -1909,6 +1918,24 @@ export declare class DerivativeOscillator {
isReady(): boolean
warmupPeriod(): number
}
export type MacdHistogramNode = MacdHistogram
export declare class MacdHistogram {
constructor(fast: number, slow: number, signal: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type PpoHistogramNode = PpoHistogram
export declare class PpoHistogram {
constructor(fast: number, slow: number, signal: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TsiNode = TSI
export declare class TSI {
constructor(long: number, short: number)
File diff suppressed because one or more lines are too long
+77
View File
@@ -219,6 +219,7 @@ node_scalar_indicator!(
"TREND_STRENGTH_INDEX",
wc::TrendStrengthIndex
);
node_scalar_indicator!(TsfOscillatorNode, "TsfOscillator", wc::TsfOscillator);
#[napi(js_name = "JumpIndicator")]
pub struct JumpIndicatorNode {
inner: wc::JumpIndicator,
@@ -4533,6 +4534,82 @@ impl DerivativeOscillatorNode {
}
}
// ============================== MacdHistogram ==============================
#[napi(js_name = "MacdHistogram")]
pub struct MacdHistogramNode {
inner: wc::MacdHistogram,
}
#[napi]
impl MacdHistogramNode {
#[napi(constructor)]
pub fn new(fast: u32, slow: u32, signal: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::MacdHistogram::new(fast as usize, slow as usize, signal as usize)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
#[napi]
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
flatten(self.inner.batch(&prices))
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== PpoHistogram ==============================
#[napi(js_name = "PpoHistogram")]
pub struct PpoHistogramNode {
inner: wc::PpoHistogram,
}
#[napi]
impl PpoHistogramNode {
#[napi(constructor)]
pub fn new(fast: u32, slow: u32, signal: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::PpoHistogram::new(fast as usize, slow as usize, signal as usize)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
#[napi]
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
flatten(self.inner.batch(&prices))
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== TSI ==============================
#[napi(js_name = "TSI")]
@@ -25,6 +25,9 @@ from __future__ import annotations
from ._wickra import (
__version__,
PpoHistogram,
MacdHistogram,
TsfOscillator,
Qstick,
GatorOscillator,
KasePermissionStochastic,
@@ -473,6 +476,9 @@ from ._wickra import (
)
__all__ = [
"PpoHistogram",
"MacdHistogram",
"TsfOscillator",
"Qstick",
"GatorOscillator",
"KasePermissionStochastic",
+141
View File
@@ -3295,6 +3295,144 @@ impl PyKasePermissionStochastic {
}
}
// ============================== TsfOscillator ==============================
#[pyclass(name = "TsfOscillator", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyTsfOscillator {
inner: wc::TsfOscillator,
}
#[pymethods]
impl PyTsfOscillator {
#[new]
#[pyo3(signature = (period=14))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::TsfOscillator::new(period).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("TsfOscillator(period={})", self.inner.period())
}
}
// ============================== MacdHistogram ==============================
#[pyclass(name = "MacdHistogram", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyMacdHistogram {
inner: wc::MacdHistogram,
}
#[pymethods]
impl PyMacdHistogram {
#[new]
#[pyo3(signature = (fast=12, slow=26, signal=9))]
fn new(fast: usize, slow: usize, signal: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::MacdHistogram::new(fast, slow, signal).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
let (fast, slow, signal) = self.inner.periods();
format!("MacdHistogram(fast={fast}, slow={slow}, signal={signal})")
}
}
// ============================== PpoHistogram ==============================
#[pyclass(name = "PpoHistogram", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyPpoHistogram {
inner: wc::PpoHistogram,
}
#[pymethods]
impl PyPpoHistogram {
#[new]
#[pyo3(signature = (fast=12, slow=26, signal=9))]
fn new(fast: usize, slow: usize, signal: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::PpoHistogram::new(fast, slow, signal).map_err(map_err)?,
})
}
fn update(&mut self, value: f64) -> Option<f64> {
self.inner.update(value)
}
fn batch<'py>(
&mut self,
py: Python<'py>,
prices: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let s = prices
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
let (fast, slow, signal) = self.inner.periods();
format!("PpoHistogram(fast={fast}, slow={slow}, signal={signal})")
}
}
// ============================== Stochastic ==============================
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
@@ -21422,5 +21560,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyWavePm>()?;
m.add_class::<PyGatorOscillator>()?;
m.add_class::<PyKasePermissionStochastic>()?;
m.add_class::<PyTsfOscillator>()?;
m.add_class::<PyMacdHistogram>()?;
m.add_class::<PyPpoHistogram>()?;
Ok(())
}
@@ -45,6 +45,9 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
# --- Scalar (f64 -> f64) indicators ---------------------------------------
SCALAR = [
(ta.PpoHistogram, (3, 6, 3)),
(ta.MacdHistogram, (3, 6, 3)),
(ta.TsfOscillator, (3,)),
(ta.WAVE_PM, (32, 3)),
(ta.POLARIZED_FRACTAL_EFFICIENCY, (10, 5)),
(ta.TREND_STRENGTH_INDEX, (20,)),
@@ -2862,6 +2865,31 @@ def test_kase_permission_stochastic_reference():
assert out[-1][0] == pytest.approx(50.0)
assert out[-1][1] == pytest.approx(50.0)
def test_tsf_oscillator_reference():
t = ta.TsfOscillator(3)
assert t.update(1.0) is None
assert t.update(2.0) is None
assert t.update(9.0) == pytest.approx(-33.33333333333333)
def test_macd_histogram_reference():
# On a constant-slope ramp the MACD line is flat once seeded, so the
# signal EMA catches up and the histogram collapses to 0.
t = ta.MacdHistogram(3, 6, 3)
for i in range(7):
assert t.update(100.0 + i * 2.0) is None
assert t.update(100.0 + 7 * 2.0) == pytest.approx(0.0, abs=1e-9)
def test_ppo_histogram_reference():
# PPO divides the EMA gap by the slow EMA, so on the same ramp the ratio
# keeps drifting and the histogram stays non-zero.
t = ta.PpoHistogram(3, 6, 3)
for i in range(7):
assert t.update(100.0 + i * 2.0) is None
assert t.update(100.0 + 7 * 2.0) == pytest.approx(-0.052098, abs=1e-6)
# --- Lifecycle ------------------------------------------------------------
+3
View File
@@ -10653,6 +10653,9 @@ wasm_scalar_indicator!(WasmDynamicMomentumIndex, "DynamicMomentumIndex", wc::Dyn
wasm_scalar_indicator!(WasmRmi, "RMI", wc::Rmi, period: usize, momentum: usize);
wasm_scalar_indicator!(WasmDerivativeOscillator, "DerivativeOscillator", wc::DerivativeOscillator, rsi_period: usize, smooth1: usize, smooth2: usize, signal_period: usize);
wasm_scalar_indicator!(WasmTrendStrengthIndex, "TREND_STRENGTH_INDEX", wc::TrendStrengthIndex, period: usize);
wasm_scalar_indicator!(WasmTsfOscillator, "TsfOscillator", wc::TsfOscillator, period: usize);
wasm_scalar_indicator!(WasmMacdHistogram, "MacdHistogram", wc::MacdHistogram, fast: usize, slow: usize, signal: usize);
wasm_scalar_indicator!(WasmPpoHistogram, "PpoHistogram", wc::PpoHistogram, fast: usize, slow: usize, signal: usize);
// --- DrawdownDuration: u32 output, no constructor args ---
@@ -0,0 +1,184 @@
//! MACD Histogram (standalone).
use crate::error::Result;
use crate::indicators::macd::MacdIndicator;
use crate::traits::Indicator;
/// MACD Histogram — the `macd signal` bar of [`MacdIndicator`] as a
/// standalone scalar indicator.
///
/// ```text
/// macd = EMA(fast) EMA(slow)
/// signal = EMA(macd, signal)
/// histogram = macd signal
/// ```
///
/// The histogram is the most actively traded part of MACD: it crosses zero
/// exactly when the MACD line crosses its signal, and its slope measures
/// whether that momentum is accelerating or fading. This wrapper exposes just
/// that series for pipelines that want a plain `f64` stream rather than the
/// full [`MacdOutput`](crate::MacdOutput); for the line and signal alongside
/// it, use [`MacdIndicator`](crate::MacdIndicator) directly.
///
/// Standard parameters are `fast = 12`, `slow = 26`, `signal = 9`, so the
/// first value lands after `slow + signal 1` inputs — exactly when
/// [`MacdIndicator`] emits its first full output.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, MacdHistogram};
///
/// let mut indicator = MacdHistogram::new(12, 26, 9).unwrap();
/// let mut last = None;
/// for i in 0..80 {
/// last = indicator.update(100.0 + f64::from(i));
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct MacdHistogram {
macd: MacdIndicator,
}
impl MacdHistogram {
/// Construct a MACD histogram with the given periods.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if any period is zero, and
/// [`Error::InvalidPeriod`] if `fast >= slow`.
pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
Ok(Self {
macd: MacdIndicator::new(fast, slow, signal)?,
})
}
/// Default `(12, 26, 9)` configuration, matching every classical chart package.
pub fn classic() -> Self {
Self::new(12, 26, 9).expect("classic MACD periods are valid")
}
/// Configured periods as `(fast, slow, signal)`.
pub const fn periods(&self) -> (usize, usize, usize) {
self.macd.periods()
}
}
impl Indicator for MacdHistogram {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
self.macd.update(input).map(|out| out.histogram)
}
fn reset(&mut self) {
self.macd.reset();
}
fn warmup_period(&self) -> usize {
self.macd.warmup_period()
}
fn is_ready(&self) -> bool {
self.macd.is_ready()
}
fn name(&self) -> &'static str {
"MacdHistogram"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_periods() {
assert!(matches!(
MacdHistogram::new(0, 26, 9),
Err(Error::PeriodZero)
));
assert!(matches!(
MacdHistogram::new(12, 26, 0),
Err(Error::PeriodZero)
));
assert!(matches!(
MacdHistogram::new(26, 12, 9),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let osc = MacdHistogram::classic();
assert_eq!(osc.periods(), (12, 26, 9));
assert_eq!(osc.name(), "MacdHistogram");
assert_eq!(osc.warmup_period(), 26 + 9 - 1);
assert!(!osc.is_ready());
}
#[test]
fn equals_macd_histogram_field() {
// The standalone series must be exactly MacdIndicator's histogram bar.
let prices: Vec<f64> = (1..=120)
.map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 8.0)
.collect();
let hist = MacdHistogram::classic().batch(&prices);
let full = MacdIndicator::classic().batch(&prices);
assert_eq!(hist.len(), full.len());
for (h, m) in hist.iter().zip(full.iter()) {
assert_eq!(h.is_some(), m.is_some());
if let (Some(h), Some(m)) = (h, m) {
assert_relative_eq!(*h, m.histogram, epsilon = 1e-12);
}
}
}
#[test]
fn warmup_emits_first_value_at_warmup_period() {
let mut osc = MacdHistogram::new(3, 6, 3).unwrap();
let warmup = osc.warmup_period();
assert_eq!(warmup, 6 + 3 - 1);
for i in 1..warmup {
assert!(osc.update(100.0 + i as f64).is_none());
}
assert!(osc.update(100.0 + warmup as f64).is_some());
assert!(osc.is_ready());
}
#[test]
fn constant_series_converges_to_zero() {
let mut osc = MacdHistogram::classic();
let out = osc.batch(&[100.0_f64; 200]);
let last = out.iter().rev().flatten().next().expect("emits a value");
assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=100)
.map(|i| (f64::from(i) * 0.4).cos() * 10.0)
.collect();
let mut a = MacdHistogram::classic();
let mut b = MacdHistogram::classic();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut osc = MacdHistogram::classic();
osc.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
assert!(osc.is_ready());
osc.reset();
assert!(!osc.is_ready());
assert_eq!(osc.update(1.0), None);
}
}
+10 -1
View File
@@ -214,6 +214,7 @@ mod ma_envelope;
mod macd;
mod macd_ext;
mod macd_fix;
mod macd_histogram;
mod mama;
mod market_facilitation_index;
mod marubozu;
@@ -270,6 +271,7 @@ mod pmo;
mod point_and_figure_bars;
mod polarized_fractal_efficiency;
mod ppo;
mod ppo_histogram;
mod profit_factor;
mod psar;
mod pvi;
@@ -381,6 +383,7 @@ mod triple_top_bottom;
mod trix;
mod true_range;
mod tsf;
mod tsf_oscillator;
mod tsi;
mod tsv;
mod ttm_squeeze;
@@ -634,6 +637,7 @@ pub use ma_envelope::{MaEnvelope, MaEnvelopeOutput};
pub use macd::{MacdIndicator, MacdOutput};
pub use macd_ext::{MaType, MacdExt};
pub use macd_fix::MacdFix;
pub use macd_histogram::MacdHistogram;
pub use mama::{Mama, MamaOutput};
pub use market_facilitation_index::MarketFacilitationIndex;
pub use marubozu::Marubozu;
@@ -690,6 +694,7 @@ pub use pmo::Pmo;
pub use point_and_figure_bars::{PnfColumn, PointAndFigureBars};
pub use polarized_fractal_efficiency::PolarizedFractalEfficiency;
pub use ppo::Ppo;
pub use ppo_histogram::PpoHistogram;
pub use profit_factor::ProfitFactor;
pub use psar::Psar;
pub use pvi::Pvi;
@@ -801,6 +806,7 @@ pub use triple_top_bottom::TripleTopBottom;
pub use trix::Trix;
pub use true_range::TrueRange;
pub use tsf::Tsf;
pub use tsf_oscillator::TsfOscillator;
pub use tsi::Tsi;
pub use tsv::Tsv;
pub use ttm_squeeze::{TtmSqueeze, TtmSqueezeOutput};
@@ -973,6 +979,9 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"ZeroLagMacd",
"ElderImpulse",
"Stc",
"TsfOscillator",
"MacdHistogram",
"PpoHistogram",
],
),
(
@@ -1414,6 +1423,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, 420, "FAMILIES total drifted from indicator count");
assert_eq!(total, 423, "FAMILIES total drifted from indicator count");
}
}
@@ -0,0 +1,230 @@
//! Percentage Price Oscillator Histogram.
use crate::error::{Error, Result};
use crate::indicators::ema::Ema;
use crate::indicators::ppo::Ppo;
use crate::traits::Indicator;
/// PPO Histogram — the `ppo signal` bar of the Percentage Price Oscillator.
///
/// ```text
/// ppo = 100 · (EMA_fast EMA_slow) / EMA_slow
/// signal = EMA(ppo, signal_period)
/// histogram = ppo signal
/// ```
///
/// [`Ppo`](crate::Ppo) itself only emits the percentage line; this indicator
/// adds the classic 9-period signal EMA on top and reports the resulting
/// zero-centered histogram. Because PPO is scale-free (the EMA gap is divided
/// by the slow EMA), the histogram is **comparable across instruments** — a
/// PPO histogram of `0.4` means the same relative momentum on any asset, unlike
/// the price-unit [`MacdHistogram`](crate::MacdHistogram).
///
/// With Appel's defaults `fast = 12`, `slow = 26`, `signal = 9`, the first
/// value lands after `slow + signal 1` inputs — the point at which the slow
/// EMA and then the signal EMA are both seeded.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, PpoHistogram};
///
/// let mut indicator = PpoHistogram::new(12, 26, 9).unwrap();
/// let mut last = None;
/// for i in 0..80 {
/// last = indicator.update(100.0 + f64::from(i));
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct PpoHistogram {
ppo: Ppo,
signal_ema: Ema,
signal_period: usize,
current: Option<f64>,
}
impl PpoHistogram {
/// Construct a PPO histogram with the `fast`/`slow` EMA periods and the
/// `signal` EMA period.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if any period is `0`, or
/// [`Error::InvalidPeriod`] if `fast >= slow`.
pub fn new(fast: usize, slow: usize, signal: usize) -> Result<Self> {
if signal == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
ppo: Ppo::new(fast, slow)?,
signal_ema: Ema::new(signal)?,
signal_period: signal,
current: None,
})
}
/// Default `(12, 26, 9)` configuration.
pub fn classic() -> Self {
Self::new(12, 26, 9).expect("classic PPO periods are valid")
}
/// Configured periods as `(fast, slow, signal)`.
pub const fn periods(&self) -> (usize, usize, usize) {
let (fast, slow) = self.ppo.periods();
(fast, slow, self.signal_period)
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.current
}
}
impl Indicator for PpoHistogram {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
// Guard before touching either stage so a non-finite input never
// advances the signal EMA on a stale, re-fed PPO value.
if !input.is_finite() {
return self.current;
}
let ppo = self.ppo.update(input)?;
let signal = self.signal_ema.update(ppo)?;
let histogram = ppo - signal;
self.current = Some(histogram);
Some(histogram)
}
fn reset(&mut self) {
self.ppo.reset();
self.signal_ema.reset();
self.current = None;
}
fn warmup_period(&self) -> usize {
// Slow EMA seeds the PPO, then the signal EMA needs `signal 1` more.
self.ppo.warmup_period() + self.signal_period - 1
}
fn is_ready(&self) -> bool {
self.current.is_some()
}
fn name(&self) -> &'static str {
"PpoHistogram"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_invalid_periods() {
assert!(matches!(
PpoHistogram::new(0, 26, 9),
Err(Error::PeriodZero)
));
assert!(matches!(
PpoHistogram::new(12, 0, 9),
Err(Error::PeriodZero)
));
assert!(matches!(
PpoHistogram::new(12, 26, 0),
Err(Error::PeriodZero)
));
assert!(matches!(
PpoHistogram::new(26, 12, 9),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let osc = PpoHistogram::classic();
assert_eq!(osc.periods(), (12, 26, 9));
assert_eq!(osc.name(), "PpoHistogram");
assert_eq!(osc.warmup_period(), 26 + 9 - 1);
assert_eq!(osc.value(), None);
assert!(!osc.is_ready());
}
#[test]
fn equals_ppo_minus_signal_ema() {
// The histogram must equal PPO minus an EMA(signal) composed by hand.
let prices: Vec<f64> = (1..=120)
.map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 6.0)
.collect();
let got = PpoHistogram::new(12, 26, 9).unwrap().batch(&prices);
let mut ppo = Ppo::new(12, 26).unwrap();
let mut sig = Ema::new(9).unwrap();
let mut expected = Vec::with_capacity(prices.len());
for p in &prices {
let out = ppo
.update(*p)
.and_then(|line| sig.update(line).map(|signal| line - signal));
expected.push(out);
}
assert_eq!(got, expected);
}
#[test]
fn warmup_emits_first_value_at_warmup_period() {
let mut osc = PpoHistogram::new(3, 6, 3).unwrap();
let warmup = osc.warmup_period();
assert_eq!(warmup, 6 + 3 - 1);
for i in 1..warmup {
assert!(osc.update(100.0 + i as f64).is_none());
}
assert!(osc.update(100.0 + warmup as f64).is_some());
assert!(osc.is_ready());
}
#[test]
fn constant_series_converges_to_zero() {
let mut osc = PpoHistogram::classic();
let out = osc.batch(&[100.0_f64; 200]);
let last = out.iter().rev().flatten().next().expect("emits a value");
assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
}
#[test]
fn ignores_non_finite_input() {
let mut osc = PpoHistogram::new(3, 6, 3).unwrap();
let out = osc.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
let before = *out.last().unwrap();
assert!(before.is_some());
assert_eq!(osc.update(f64::NAN), before);
assert_eq!(osc.update(f64::INFINITY), before);
assert_eq!(osc.value(), before);
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=100)
.map(|i| 100.0 + (f64::from(i) * 0.4).cos() * 10.0)
.collect();
let mut a = PpoHistogram::classic();
let mut b = PpoHistogram::classic();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut osc = PpoHistogram::classic();
osc.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
assert!(osc.is_ready());
osc.reset();
assert!(!osc.is_ready());
assert_eq!(osc.update(1.0), None);
}
}
@@ -0,0 +1,206 @@
//! Time Series Forecast Oscillator (TSF Oscillator).
use crate::error::{Error, Result};
use crate::indicators::tsf::Tsf;
use crate::traits::Indicator;
/// Time Series Forecast Oscillator — the percentage gap between the close and
/// the **one-bar-ahead** time-series forecast of the close.
///
/// ```text
/// TSFOsc_t = 100 · (close_t TSF(close, period)_t) / close_t
/// ```
///
/// where [`Tsf`](crate::Tsf) projects the rolling least-squares line one bar
/// past the window (`a + b·period`). It is the close-relative companion to
/// [`Cfo`](crate::Cfo), which measures the same percentage gap against the
/// regression value at the *current* bar (`a + b·(period 1)`). Because `TSF`
/// advances one bar further than `LinearRegression`, the two differ by exactly
/// the slope term `100·b/close`: on a trending series `TSFOsc` reads more
/// negative in an uptrend (the forecast has already stepped above price) and
/// more positive in a downtrend.
///
/// Positive readings mean the close sits *above* its forward forecast (price
/// has overshot the projected trend); negative readings mean it sits below.
/// Wraps the existing `Tsf` so the warmup matches.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, TsfOscillator};
///
/// let mut indicator = TsfOscillator::new(14).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 TsfOscillator {
period: usize,
tsf: Tsf,
current: Option<f64>,
}
impl TsfOscillator {
/// Construct a new TSF oscillator over `period` inputs.
///
/// # Errors
/// Returns [`Error::InvalidPeriod`] if `period < 2` — a regression line is
/// undefined for fewer than two points.
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "TSF oscillator needs period >= 2",
});
}
Ok(Self {
period,
tsf: Tsf::new(period)?,
current: None,
})
}
/// Configured period.
pub const fn period(&self) -> usize {
self.period
}
}
impl Indicator for TsfOscillator {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
let forecast = self.tsf.update(input)?;
// Hold the previous value if the close is zero — the percentage form
// is undefined and a return of inf would propagate badly.
if input == 0.0 {
return self.current;
}
let value = 100.0 * (input - forecast) / input;
self.current = Some(value);
Some(value)
}
fn reset(&mut self) {
self.tsf.reset();
self.current = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.current.is_some()
}
fn name(&self) -> &'static str {
"TsfOscillator"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_short_period() {
assert!(matches!(
TsfOscillator::new(1),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
TsfOscillator::new(0),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let osc = TsfOscillator::new(14).unwrap();
assert_eq!(osc.period(), 14);
assert_eq!(osc.warmup_period(), 14);
assert_eq!(osc.name(), "TsfOscillator");
assert!(!osc.is_ready());
}
#[test]
fn reference_value() {
// period 3 over [1, 2, 9]: fit y = 0 + 4x, one-bar-ahead TSF at x = 3
// is 12. With close = 9, TSFOsc = 100·(9 12)/9 = 33.3333…%.
let mut osc = TsfOscillator::new(3).unwrap();
let out = osc.batch(&[1.0_f64, 2.0, 9.0]);
assert!(out[0].is_none());
assert!(out[1].is_none());
assert_relative_eq!(out[2].unwrap(), -100.0 / 3.0, epsilon = 1e-9);
assert!(osc.is_ready());
}
#[test]
fn constant_series_yields_zero() {
// On a flat series the regression slope is 0, so the one-bar-ahead TSF
// equals the constant and close forecast is exactly 0.
let mut osc = TsfOscillator::new(5).unwrap();
let out = osc.batch(&[42.0_f64; 30]);
for v in out.iter().skip(4).flatten() {
assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
}
}
#[test]
fn linear_uptrend_reads_negative() {
// Unlike CFO (evaluated at the current bar), the forecast steps one bar
// ahead, so on a rising line the projection sits above the close and the
// oscillator is negative: TSFOsc = 100·slope/close.
let mut osc = TsfOscillator::new(5).unwrap();
let prices: Vec<f64> = (1..=20).map(|i| f64::from(i) * 2.0).collect();
let out = osc.batch(&prices);
for v in out.iter().skip(4).flatten() {
assert!(*v < 0.0, "uptrend forecast overshoots close, got {v}");
}
}
#[test]
fn warmup_emits_first_value_at_period() {
let mut osc = TsfOscillator::new(3).unwrap();
assert_eq!(osc.update(1.0), None);
assert_eq!(osc.update(2.0), None);
assert!(osc.update(3.0).is_some());
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=80)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
.collect();
let mut a = TsfOscillator::new(14).unwrap();
let mut b = TsfOscillator::new(14).unwrap();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut osc = TsfOscillator::new(5).unwrap();
osc.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
assert!(osc.is_ready());
osc.reset();
assert!(!osc.is_ready());
assert_eq!(osc.update(1.0), None);
}
#[test]
fn zero_close_holds_value() {
let mut osc = TsfOscillator::new(3).unwrap();
osc.batch(&[1.0_f64, 2.0, 3.0]);
let before = osc.current;
assert_eq!(osc.update(0.0), before);
}
}
+10 -10
View File
@@ -99,7 +99,7 @@ pub use indicators::{
LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel,
LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures,
LiquidationFeaturesOutput, LogReturn, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope,
MaEnvelopeOutput, MacdExt, MacdFix, MacdIndicator, MacdOutput, Mama, MamaOutput,
MaEnvelopeOutput, MacdExt, MacdFix, MacdHistogram, MacdIndicator, MacdOutput, Mama, MamaOutput,
MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, MaxDrawdown,
McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation,
MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, Mom,
@@ -109,10 +109,10 @@ pub use indicators::{
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,
PointAndFigureBars, PolarizedFractalEfficiency, Ppo, PpoHistogram, 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,
@@ -129,11 +129,11 @@ pub use indicators::{
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,
TripleTopBottom, Trix, TrueRange, Tsf, TsfOscillator, 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,
+1 -1
View File
@@ -8,7 +8,7 @@ That includes:
[Python](https://docs.wickra.org/Quickstart-Python),
[Node](https://docs.wickra.org/Quickstart-Node), and
[WASM](https://docs.wickra.org/Quickstart-WASM).
- A per-indicator deep dive for every one of the **420 indicators** across
- A per-indicator deep dive for every one of the **423 indicators** across
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
+4 -1
View File
@@ -14,7 +14,7 @@
//! `Ema(20)`. This target now covers every scalar indicator in the catalogue.
use libfuzzer_sys::fuzz_target;
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trima, Trix, Tsf, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
use wickra_core::{AdaptiveCycle, AdaptiveLaguerreFilter, Alma, AnchoredRsi, Apo, Autocorrelation, AverageDrawdown, BatchExt, Beta, BollingerBands, CalmarRatio, CenterOfGravity, Cfo, Cmo, CoefficientOfVariation, ConditionalValueAtRisk, ConnorsRsi, Coppock, CyberneticCycle, Decycler, DecyclerOscillator, Dema, DerivativeOscillator, DetrendedStdDev, DisparityIndex, DoubleBollinger, Dpo, DrawdownDuration, DynamicMomentumIndex, EhlersStochastic, Ehma, ElderImpulse, Ema, EmpiricalModeDecomposition, Expectancy, Fama, FisherRsi, FisherTransform, Frama, GainLossRatio, GeneralizedDema, GeometricMa, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HtDcPhase, HtPhasor, HtTrendMode, HurstExponent, Indicator, InstantaneousTrendline, InverseFisherTransform, Jma, JumpIndicator, Kama, KellyCriterion, Kst, Kurtosis, LaguerreRsi, LinRegAngle, LinRegChannel, LinRegIntercept, LinRegSlope, LinearRegression, LogReturn, MaEnvelope, MaType, MacdExt, MacdFix, MacdHistogram, MacdIndicator, Mama, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianMa, MidPoint, Mom, OmegaRatio, PainIndex, PearsonCorrelation, PercentageTrailingStop, Pmo, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, Qqe, RSquared, RealizedVolatility, RecoveryFactor, RegimeLabel, RenkoTrailingStop, Rmi, Roc, Rocp, Rocr, Rocr100, RollingIqr, RollingPercentileRank, RollingQuantile, RoofingFilter, Rsi, Rsx, RviVolatility, SharpeRatio, SineWave, SineWeightedMa, Skewness, Sma, Smma, SortinoRatio, SpearmanCorrelation, StandardError, StandardErrorBands, Stc, StdDev, StepTrailingStop, StochRsi, SuperSmoother, Tema, Tii, TrendLabel, TrendStrengthIndex, Trima, Trix, Tsf, TsfOscillator, Tsi, UlcerIndex, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, WavePm, WinRate, Wma, ZScore, ZeroLagMacd, Zlema, T3};
/// Drive a single streaming + batch run through one scalar indicator. Marked
/// `#[inline(never)]` so a panic backtrace pin-points the specific indicator.
@@ -84,6 +84,9 @@ fuzz_target!(|data: Vec<f64>| {
drive(|| Ppo::new(12, 26).unwrap(), &data);
drive(|| Apo::new(12, 26).unwrap(), &data);
drive(|| Cfo::new(14).unwrap(), &data);
drive(|| TsfOscillator::new(14).unwrap(), &data);
drive(|| MacdHistogram::new(12, 26, 9).unwrap(), &data);
drive(|| PpoHistogram::new(12, 26, 9).unwrap(), &data);
drive(|| ElderImpulse::classic(), &data);
drive(|| Stc::classic(), &data);
drive(|| Coppock::new(14, 11, 10).unwrap(), &data);