feat(ao-histogram): add Awesome Oscillator Histogram
AO - SMA(AO, sma_period). A configurable variant of the existing AcceleratorOscillator (which fixes fast=5, slow=34, sma=5). Three parameters; defaults match Bill Williams' Accelerator. Touchpoints: awesome_oscillator_histogram.rs + mod.rs + lib.rs re-export, PyAoHist + __init__.py + test_new_indicators CANDLE_SCALAR + test_known_values flat reference, AwesomeOscillatorHistogramNode + index.d.ts/index.js + indicators.test.js factory + reference, WasmAoHist, candle-fuzz target, README + CHANGELOG.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Self> {
|
||||
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<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, low, 0.0)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AwesomeOscillatorHistogram::new(fast, slow, sma_period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
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<Bound<'py, PyArray1<f64>>> {
|
||||
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::<PyHma>()?;
|
||||
m.add_class::<PyKama>()?;
|
||||
m.add_class::<PyApo>()?;
|
||||
m.add_class::<PyAoHist>()?;
|
||||
m.add_class::<PyCci>()?;
|
||||
m.add_class::<PyRoc>()?;
|
||||
m.add_class::<PyWilliamsR>()?;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<WasmAoHist, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::AwesomeOscillatorHistogram::new(fast, slow, sma_period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64) -> Result<Option<f64>, 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<Float64Array, JsError> {
|
||||
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,
|
||||
|
||||
@@ -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<Self> {
|
||||
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<f64> {
|
||||
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<Candle> = (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<Candle> = (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<Candle> = (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<Candle> = (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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<f64>| {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user