diff --git a/CHANGELOG.md b/CHANGELOG.md index e32ee506..268b9c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **Family 03 — MACD & Price Oscillators.** `AwesomeOscillatorHistogram`: + `AO − SMA(AO, sma_period)`. A configurable variant of the existing + `AcceleratorOscillator` (which fixes `(fast, slow, sma) = (5, 34, 5)`). + Three parameters; defaults match Bill Williams' Accelerator. Exposed + in all four bindings. - **Family 03 — MACD & Price Oscillators.** `APO` (Absolute Price Oscillator): `EMA(close, fast) − EMA(close, slow)`. Like MACD's line without the signal EMA. Default `(fast = 12, slow = 26)`. `fast` must diff --git a/README.md b/README.md index 83e04639..2782c372 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ python -m benchmarks.compare_libraries ## Indicators -72 streaming-first indicators across eight families. Every one passes the +73 streaming-first indicators across eight families. Every one passes the `batch == streaming` equivalence test, reference-value tests, and reset semantics tests. @@ -118,7 +118,7 @@ semantics tests. | Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA | | Momentum Oscillators | RSI (Wilder), Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator | | Trend & Directional | MACD, ADX (+DI/-DI), Aroon, TRIX, Aroon Oscillator, Vortex, Mass Index, Choppiness Index, Vertical Horizontal Filter | -| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO | +| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram | | Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility | | Trailing Stops | Parabolic SAR, SuperTrend, Chandelier Exit, Chande Kroll Stop, ATR Trailing Stop | | Volume | OBV, VWAP (cumulative + rolling), ADL, Volume-Price Trend, Chaikin Money Flow, Chaikin Oscillator, Force Index, Ease of Movement | diff --git a/bindings/node/__tests__/indicators.test.js b/bindings/node/__tests__/indicators.test.js index d86b71b6..ef222372 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -102,6 +102,7 @@ const candleScalar = { MedianPrice: { make: () => new wickra.MedianPrice(), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, WeightedClose: { make: () => new wickra.WeightedClose(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, AcceleratorOscillator: { make: () => new wickra.AcceleratorOscillator(5, 34, 5), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, + AwesomeOscillatorHistogram: { make: () => new wickra.AwesomeOscillatorHistogram(5, 34, 5), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, BalanceOfPower: { make: () => new wickra.BalanceOfPower(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ChoppinessIndex: { make: () => new wickra.ChoppinessIndex(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, TrueRange: { make: () => new wickra.TrueRange(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, @@ -260,6 +261,16 @@ test('LinRegAngle of a unit-slope series is 45 degrees', () => { assert.ok(Math.abs(out[4] - 45) < 1e-9); }); +test('AwesomeOscillatorHistogram on a flat median converges to zero', () => { + const n = 50; + const out = new wickra.AwesomeOscillatorHistogram(3, 5, 3).batch( + Array(n).fill(11), + Array(n).fill(9), + ); + // warmup = 5 + 3 - 1 = 7. + for (let i = 6; i < n; i++) assert.ok(Math.abs(out[i]) < 1e-12); +}); + test('APO(3, 5) on a flat series converges to zero', () => { const out = new wickra.APO(3, 5).batch(Array(30).fill(42)); for (let i = 0; i < 4; i++) assert.ok(Number.isNaN(out[i])); diff --git a/bindings/node/index.js b/bindings/node/index.js index cec8d084..a6193c16 100644 --- a/bindings/node/index.js +++ b/bindings/node/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, KAMA, APO, T3, TSI, PMO, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA } = nativeBinding +const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, KAMA, APO, AwesomeOscillatorHistogram, T3, TSI, PMO, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA } = nativeBinding module.exports.version = version module.exports.SMA = SMA @@ -350,6 +350,7 @@ module.exports.AwesomeOscillator = AwesomeOscillator module.exports.Aroon = Aroon module.exports.KAMA = KAMA module.exports.APO = APO +module.exports.AwesomeOscillatorHistogram = AwesomeOscillatorHistogram module.exports.T3 = T3 module.exports.TSI = TSI module.exports.PMO = PMO diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index da0d31a5..8baa693e 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -1068,6 +1068,58 @@ impl AroonNode { } } +#[napi(js_name = "AwesomeOscillatorHistogram")] +pub struct AwesomeOscillatorHistogramNode { + inner: wc::AwesomeOscillatorHistogram, +} +#[napi] +impl AwesomeOscillatorHistogramNode { + #[napi(constructor)] + pub fn new(fast: u32, slow: u32, sma_period: u32) -> napi::Result { + Ok(Self { + inner: wc::AwesomeOscillatorHistogram::new( + clamp_period(fast), + clamp_period(slow), + clamp_period(sma_period), + ) + .map_err(map_err)?, + }) + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + Ok(self.inner.update(cnd(high, low, low, 0.0)?)) + } + #[napi] + pub fn batch(&mut self, high: Vec, low: Vec) -> napi::Result> { + if high.len() != low.len() { + return Err(NapiError::from_reason( + "high and low must be equal length".to_string(), + )); + } + let mut out = Vec::with_capacity(high.len()); + for i in 0..high.len() { + out.push( + self.inner + .update(cnd(high[i], low[i], low[i], 0.0)?) + .unwrap_or(f64::NAN), + ); + } + Ok(out) + } + #[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 + } +} + #[napi(js_name = "APO")] pub struct ApoNode { inner: wc::Apo, diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index 7e4bf78f..e4c09291 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -57,6 +57,7 @@ from ._wickra import ( StochRSI, UltimateOscillator, APO, + AwesomeOscillatorHistogram, PPO, DPO, Coppock, @@ -139,6 +140,7 @@ __all__ = [ "StochRSI", "UltimateOscillator", "APO", + "AwesomeOscillatorHistogram", "PPO", "DPO", "Coppock", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 6fa464be..d7635ad0 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -812,6 +812,68 @@ impl PyKama { } } +// ============================== AwesomeOscillatorHistogram ============================== + +#[pyclass( + name = "AwesomeOscillatorHistogram", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyAoHist { + inner: wc::AwesomeOscillatorHistogram, +} + +#[pymethods] +impl PyAoHist { + #[new] + #[pyo3(signature = (fast=5, slow=34, sma_period=5))] + fn new(fast: usize, slow: usize, sma_period: usize) -> PyResult { + Ok(Self { + inner: wc::AwesomeOscillatorHistogram::new(fast, slow, sma_period).map_err(map_err)?, + }) + } + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c)) + } + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() { + return Err(PyValueError::new_err("high and low must be equal length")); + } + let mut out = Vec::with_capacity(h.len()); + for i in 0..h.len() { + let candle = wc::Candle::new(l[i], h[i], l[i], l[i], 0.0, 0).map_err(map_err)?; + out.push(self.inner.update(candle).unwrap_or(f64::NAN)); + } + Ok(out.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 (f, s, k) = self.inner.periods(); + format!("AwesomeOscillatorHistogram(fast={f}, slow={s}, sma_period={k})") + } +} + // ============================== APO ============================== #[pyclass(name = "APO", module = "wickra._wickra", skip_from_py_object)] @@ -4540,6 +4602,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/bindings/python/tests/test_known_values.py b/bindings/python/tests/test_known_values.py index 8485efd6..792b9963 100644 --- a/bindings/python/tests/test_known_values.py +++ b/bindings/python/tests/test_known_values.py @@ -66,6 +66,16 @@ def test_rsi_wilder_textbook_first_value(): assert math.isclose(out[14], 70.464, abs_tol=0.05) +def test_awesome_oscillator_histogram_flat_series_converges_to_zero(): + # Flat median price -> AO = 0 -> SMA(AO) = 0 -> AOHist = 0. + n = 50 + high = np.full(n, 11.0) + low = np.full(n, 9.0) + out = ta.AwesomeOscillatorHistogram(3, 5, 3).batch(high, low) + # warmup = slow + sma - 1 = 5 + 3 - 1 = 7. + np.testing.assert_allclose(out[6:], 0.0, atol=1e-12) + + def test_apo_constant_series_converges_to_zero(): # Both EMAs reproduce a constant exactly, so APO = 0 after warmup. out = ta.APO(3, 5).batch(np.full(30, 42.0, dtype=np.float64)) diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py index 50088d46..0f42d16a 100644 --- a/bindings/python/tests/test_new_indicators.py +++ b/bindings/python/tests/test_new_indicators.py @@ -139,6 +139,10 @@ CANDLE_SCALAR = { lambda: ta.AcceleratorOscillator(5, 34, 5), lambda ind, h, l, c, v: ind.batch(h, l), ), + "AwesomeOscillatorHistogram": ( + lambda: ta.AwesomeOscillatorHistogram(5, 34, 5), + lambda ind, h, l, c, v: ind.batch(h, l), + ), "BalanceOfPower": ( # The streaming 6-tuple feeds open == close, so batch matches with # the close column standing in for open. diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 3628710a..7f0774b6 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -1902,6 +1902,47 @@ impl WasmRollingVwap { } } +#[wasm_bindgen(js_name = AwesomeOscillatorHistogram)] +pub struct WasmAoHist { + inner: wc::AwesomeOscillatorHistogram, +} + +#[wasm_bindgen(js_class = AwesomeOscillatorHistogram)] +impl WasmAoHist { + #[wasm_bindgen(constructor)] + pub fn new(fast: usize, slow: usize, sma_period: usize) -> Result { + Ok(Self { + inner: wc::AwesomeOscillatorHistogram::new(fast, slow, sma_period).map_err(map_err)?, + }) + } + pub fn update(&mut self, high: f64, low: f64) -> Result, JsError> { + let c = make_candle(high, low, low, 0.0)?; + Ok(self.inner.update(c)) + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high and low must be equal length")); + } + let mut out = Vec::with_capacity(high.len()); + for i in 0..high.len() { + let c = make_candle(high[i], low[i], low[i], 0.0)?; + out.push(self.inner.update(c).unwrap_or(f64::NAN)); + } + Ok(Float64Array::from(out.as_slice())) + } + pub fn reset(&mut self) { + self.inner.reset(); + } + #[wasm_bindgen(js_name = isReady)] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[wasm_bindgen(js_name = warmupPeriod)] + pub fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } +} + #[wasm_bindgen(js_name = AwesomeOscillator)] pub struct WasmAo { inner: wc::AwesomeOscillator, diff --git a/crates/wickra-core/src/indicators/awesome_oscillator_histogram.rs b/crates/wickra-core/src/indicators/awesome_oscillator_histogram.rs new file mode 100644 index 00000000..610e03da --- /dev/null +++ b/crates/wickra-core/src/indicators/awesome_oscillator_histogram.rs @@ -0,0 +1,198 @@ +//! Awesome Oscillator Histogram. + +use crate::error::{Error, Result}; +use crate::indicators::awesome_oscillator::AwesomeOscillator; +use crate::indicators::sma::Sma; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// "Awesome Oscillator Histogram" — the difference between the Awesome +/// Oscillator and its `sma_period`-bar `SMA`. Positive bars mean `AO` is +/// trending up (bullish acceleration); negative bars mean `AO` is trending +/// down (bearish acceleration). +/// +/// ```text +/// AO = SMA(median, fast) − SMA(median, slow) +/// AOHist = AO − SMA(AO, sma_period) +/// ``` +/// +/// With Williams' default `sma_period = 5`, this collapses to the existing +/// `AcceleratorOscillator` for `fast = 5, slow = 34, sma_period = 5`; for any +/// other parameterisation this is a more flexible variant. +/// +/// # Example +/// +/// ``` +/// use wickra_core::{AwesomeOscillatorHistogram, Candle, Indicator}; +/// +/// let mut hist = AwesomeOscillatorHistogram::classic(); +/// let mut last = None; +/// for i in 0..80 { +/// let p = 100.0 + f64::from(i); +/// let candle = Candle::new(p, p + 0.5, p - 0.5, p, 1.0, i64::from(i)).unwrap(); +/// last = hist.update(candle); +/// } +/// assert!(last.is_some()); +/// ``` +#[derive(Debug, Clone)] +pub struct AwesomeOscillatorHistogram { + fast_period: usize, + slow_period: usize, + sma_period: usize, + ao: AwesomeOscillator, + sma: Sma, +} + +impl AwesomeOscillatorHistogram { + /// # Errors + /// - [`Error::PeriodZero`] if any period is zero. + /// - [`Error::InvalidPeriod`] if `fast >= slow`. + pub fn new(fast: usize, slow: usize, sma_period: usize) -> Result { + if fast == 0 || slow == 0 || sma_period == 0 { + return Err(Error::PeriodZero); + } + if fast >= slow { + return Err(Error::InvalidPeriod { + message: "AwesomeOscillatorHistogram fast must be strictly less than slow", + }); + } + Ok(Self { + fast_period: fast, + slow_period: slow, + sma_period, + ao: AwesomeOscillator::new(fast, slow)?, + sma: Sma::new(sma_period)?, + }) + } + + /// Bill Williams' Accelerator-equivalent defaults `(5, 34, 5)`. + pub fn classic() -> Self { + Self::new(5, 34, 5).expect("classic Awesome Oscillator Histogram parameters are valid") + } + + /// Configured `(fast_period, slow_period, sma_period)`. + pub const fn periods(&self) -> (usize, usize, usize) { + (self.fast_period, self.slow_period, self.sma_period) + } +} + +impl Indicator for AwesomeOscillatorHistogram { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + let ao = self.ao.update(candle)?; + let sma = self.sma.update(ao)?; + Some(ao - sma) + } + + fn reset(&mut self) { + self.ao.reset(); + self.sma.reset(); + } + + fn warmup_period(&self) -> usize { + // AO emits at `slow` candles; the SMA then needs `sma_period - 1` + // more AO values to fill its window. + self.slow_period + self.sma_period - 1 + } + + fn is_ready(&self) -> bool { + self.sma.is_ready() + } + + fn name(&self) -> &'static str { + "AwesomeOscillatorHistogram" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + fn candle(price: f64, ts: i64) -> Candle { + Candle::new(price, price + 0.5, price - 0.5, price, 1.0, ts).unwrap() + } + + #[test] + fn rejects_zero_period() { + assert!(matches!( + AwesomeOscillatorHistogram::new(0, 34, 5), + Err(Error::PeriodZero) + )); + assert!(matches!( + AwesomeOscillatorHistogram::new(5, 0, 5), + Err(Error::PeriodZero) + )); + assert!(matches!( + AwesomeOscillatorHistogram::new(5, 34, 0), + Err(Error::PeriodZero) + )); + } + + #[test] + fn rejects_fast_geq_slow() { + assert!(matches!( + AwesomeOscillatorHistogram::new(34, 5, 5), + Err(Error::InvalidPeriod { .. }) + )); + } + + #[test] + fn accessors_and_metadata() { + let hist = AwesomeOscillatorHistogram::classic(); + assert_eq!(hist.periods(), (5, 34, 5)); + assert_eq!(hist.warmup_period(), 38); + assert_eq!(hist.name(), "AwesomeOscillatorHistogram"); + } + + #[test] + fn constant_series_converges_to_zero() { + // AO of a flat series is 0; SMA of 0 is 0; difference is 0. + let mut hist = AwesomeOscillatorHistogram::new(3, 5, 3).unwrap(); + let candles: Vec = (0..30).map(|i| candle(42.0, i)).collect(); + let out = hist.batch(&candles); + for v in out.iter().skip(hist.warmup_period() - 1).flatten() { + assert_relative_eq!(*v, 0.0, epsilon = 1e-12); + } + } + + #[test] + fn warmup_emits_first_value_at_warmup_period() { + let mut hist = AwesomeOscillatorHistogram::new(2, 4, 3).unwrap(); + assert_eq!(hist.warmup_period(), 6); + let candles: Vec = (0..8) + .map(|i| candle(10.0 + f64::from(i), i64::from(i))) + .collect(); + let out = hist.batch(&candles); + for v in out.iter().take(5) { + assert!(v.is_none()); + } + assert!(out[5].is_some()); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..100_i64) + .map(|i| candle(100.0 + (i as f64 * 0.3).sin() * 5.0, i)) + .collect(); + let batch = AwesomeOscillatorHistogram::classic().batch(&candles); + let mut b = AwesomeOscillatorHistogram::classic(); + let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect(); + assert_eq!(batch, streamed); + } + + #[test] + fn reset_clears_state() { + let mut hist = AwesomeOscillatorHistogram::classic(); + let candles: Vec = (0..80) + .map(|i| candle(10.0 + f64::from(i), i64::from(i))) + .collect(); + hist.batch(&candles); + assert!(hist.is_ready()); + hist.reset(); + assert!(!hist.is_ready()); + } +} diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs index 5301d91d..22826311 100644 --- a/crates/wickra-core/src/indicators/mod.rs +++ b/crates/wickra-core/src/indicators/mod.rs @@ -13,6 +13,7 @@ mod aroon_oscillator; mod atr; mod atr_trailing_stop; mod awesome_oscillator; +mod awesome_oscillator_histogram; mod balance_of_power; mod bollinger; mod bollinger_bandwidth; @@ -86,6 +87,7 @@ pub use aroon_oscillator::AroonOscillator; pub use atr::Atr; pub use atr_trailing_stop::AtrTrailingStop; pub use awesome_oscillator::AwesomeOscillator; +pub use awesome_oscillator_histogram::AwesomeOscillatorHistogram; pub use balance_of_power::BalanceOfPower; pub use bollinger::{BollingerBands, BollingerOutput}; pub use bollinger_bandwidth::BollingerBandwidth; diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs index db4e5518..ec07344a 100644 --- a/crates/wickra-core/src/lib.rs +++ b/crates/wickra-core/src/lib.rs @@ -45,16 +45,17 @@ pub mod indicators; pub use error::{Error, Result}; pub use indicators::{ AcceleratorOscillator, Adl, Adx, AdxOutput, Apo, Aroon, AroonOscillator, AroonOutput, Atr, - AtrTrailingStop, AwesomeOscillator, BalanceOfPower, BollingerBands, BollingerBandwidth, - BollingerOutput, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, - ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex, Cmo, Coppock, - Dema, Donchian, DonchianOutput, Dpo, EaseOfMovement, Ema, ForceIndex, HistoricalVolatility, - Hma, Kama, Keltner, KeltnerOutput, LinRegAngle, LinRegSlope, LinearRegression, MacdIndicator, - MacdOutput, MassIndex, MedianPrice, Mfi, Mom, Natr, Obv, PercentB, Pmo, Ppo, Psar, Roc, - RollingVwap, Rsi, Sma, Smma, StdDev, StochRsi, Stochastic, StochasticOutput, SuperTrend, - SuperTrendOutput, Tema, Trima, Trix, TrueRange, Tsi, TypicalPrice, UlcerIndex, - UltimateOscillator, VerticalHorizontalFilter, VolumePriceTrend, Vortex, VortexOutput, Vwap, - Vwma, WeightedClose, WilliamsR, Wma, ZScore, Zlema, T3, + AtrTrailingStop, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BollingerBands, + BollingerBandwidth, BollingerOutput, Cci, ChaikinMoneyFlow, ChaikinOscillator, + ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, + ChandelierExitOutput, ChoppinessIndex, Cmo, Coppock, Dema, Donchian, DonchianOutput, Dpo, + EaseOfMovement, Ema, ForceIndex, HistoricalVolatility, Hma, Kama, Keltner, KeltnerOutput, + LinRegAngle, LinRegSlope, LinearRegression, MacdIndicator, MacdOutput, MassIndex, MedianPrice, + Mfi, Mom, Natr, Obv, PercentB, Pmo, Ppo, Psar, Roc, RollingVwap, Rsi, Sma, Smma, StdDev, + StochRsi, Stochastic, StochasticOutput, SuperTrend, SuperTrendOutput, Tema, Trima, Trix, + TrueRange, Tsi, TypicalPrice, UlcerIndex, UltimateOscillator, VerticalHorizontalFilter, + VolumePriceTrend, Vortex, VortexOutput, Vwap, Vwma, WeightedClose, WilliamsR, Wma, ZScore, + Zlema, T3, }; pub use ohlcv::{Candle, Tick}; pub use traits::{BatchExt, Chain, Indicator}; diff --git a/fuzz/fuzz_targets/indicator_update_candle.rs b/fuzz/fuzz_targets/indicator_update_candle.rs index 4d36dc80..c4495d4b 100644 --- a/fuzz/fuzz_targets/indicator_update_candle.rs +++ b/fuzz/fuzz_targets/indicator_update_candle.rs @@ -24,7 +24,8 @@ use libfuzzer_sys::fuzz_target; use wickra_core::{ AcceleratorOscillator, Adl, Adx, Aroon, AroonOscillator, Atr, AtrTrailingStop, - AwesomeOscillator, BalanceOfPower, BatchExt, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, + AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, BatchExt, Candle, Cci, + ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, Donchian, EaseOfMovement, ForceIndex, Indicator, Keltner, MassIndex, MedianPrice, Mfi, Natr, Obv, Psar, RollingVwap, Stochastic, SuperTrend, TrueRange, TypicalPrice, UltimateOscillator, VolumePriceTrend, Vortex, @@ -97,6 +98,10 @@ fuzz_target!(|data: Vec| { drive(|| Cci::new(20).unwrap(), &candles); drive(|| WilliamsR::new(14).unwrap(), &candles); drive(|| AwesomeOscillator::new(5, 34).unwrap(), &candles); + drive( + || AwesomeOscillatorHistogram::new(5, 34, 5).unwrap(), + &candles, + ); drive(|| AcceleratorOscillator::new(5, 34, 5).unwrap(), &candles); drive(|| UltimateOscillator::new(7, 14, 28).unwrap(), &candles); drive(BalanceOfPower::new, &candles);