F-Abschluss: wire the Python package, refresh docs and extend the test suites
Finalises the F1-F12 indicator expansion (25 -> 63 indicators). - Python `wickra/__init__.py`: import and re-export all 63 indicators, grouped by family, with a matching `__all__`. The package previously exposed only the original 25 even though the compiled module and the `.pyi` stubs already carried the rest. - Docs: `Home.md` and `README.md` indicator counts and family tables updated to 63; `Indicators-Overview.md` already restructured per family in F10-F12; `Warmup-Periods.md` gains all 38 new indicators across the single- and multi-output tables (and the stale two-arg `Psar::new` example is corrected to three args); `CHANGELOG.md` `[Unreleased]` lists every new indicator by family. - Tests: `bindings/node/__tests__/indicators.test.js` covers all 63 indicators (streaming==batch plus four new reference-value checks), 80/80 green; new `bindings/python/tests/test_new_indicators.py` covers the 38 additions (streaming==batch, shapes, reference values, lifecycle), Python suite 105/105 green. - `bindings/node/index.js` regenerated by `napi build`. cargo fmt + clippy (core/wickra/data/wasm/node) clean; 454 core tests, 25 data tests, 66 doctests, 80 Node tests and 105 Python tests green; `cargo check -p wickra-wasm --tests` green.
This commit is contained in:
@@ -8,6 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- 38 new technical indicators, taking the library from 25 to 63. Each is
|
||||
implemented once in the Rust core and wired through the Python, Node and
|
||||
WASM bindings, with reference-value tests and a dedicated wiki page:
|
||||
- Trend: `Smma`, `Trima`, `Zlema`, `T3`, `Vwma`.
|
||||
- Momentum: `Mom`, `Cmo`, `Tsi`, `Pmo`, `StochRsi`, `UltimateOscillator`,
|
||||
`Ppo`, `Dpo`, `Coppock`, `AroonOscillator`, `Vortex`, `MassIndex`.
|
||||
- Volatility: `Natr`, `StdDev`, `UlcerIndex`, `HistoricalVolatility`,
|
||||
`BollingerBandwidth`, `PercentB`, `SuperTrend`, `ChandelierExit`,
|
||||
`ChandeKrollStop`, `AtrTrailingStop`.
|
||||
- Volume: `Adl`, `VolumePriceTrend`, `ChaikinMoneyFlow`,
|
||||
`ChaikinOscillator`, `ForceIndex`, `EaseOfMovement`.
|
||||
- Statistics: `TypicalPrice`, `MedianPrice`, `WeightedClose`,
|
||||
`LinearRegression`, `LinRegSlope`.
|
||||
- `TickAggregator::with_gap_fill` — opt-in mode that emits a flat placeholder
|
||||
candle for every empty bucket between two ticks, keeping the candle series
|
||||
evenly spaced for downstream indicators.
|
||||
|
||||
@@ -95,16 +95,17 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## Indicators
|
||||
|
||||
25 streaming-first indicators across four families. Every one passes the
|
||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||
semantics tests.
|
||||
63 streaming-first indicators across four families plus a statistics group.
|
||||
Every one passes the `batch == streaming` equivalence test, reference-value
|
||||
tests, and reset semantics tests.
|
||||
|
||||
| Family | Indicators |
|
||||
|-------------|-----------|
|
||||
| Trend | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA |
|
||||
| Momentum | RSI (Wilder), MACD, Stochastic, CCI, ROC, Williams %R, ADX (+DI/-DI), MFI, TRIX, Awesome Oscillator, Aroon |
|
||||
| Volatility | Bollinger Bands, ATR, Keltner Channels, Donchian Channels, Parabolic SAR |
|
||||
| Volume | OBV, VWAP (cumulative + rolling) |
|
||||
| Trend | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA |
|
||||
| Momentum | RSI (Wilder), MACD, Stochastic, CCI, ROC, Williams %R, ADX (+DI/-DI), MFI, TRIX, Awesome Oscillator, Aroon, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, PPO, DPO, Coppock, Aroon Oscillator, Vortex, Mass Index |
|
||||
| Volatility | Bollinger Bands, ATR, Keltner Channels, Donchian Channels, Parabolic SAR, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, 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 |
|
||||
| Statistics | Typical Price, Median Price, Weighted Close, Linear Regression, Linear Regression Slope |
|
||||
|
||||
Adding a new indicator means implementing one trait in Rust; all four bindings
|
||||
inherit it automatically.
|
||||
@@ -174,7 +175,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 25 indicators
|
||||
│ ├── wickra-core/ core engine + all 63 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io)
|
||||
│ │ + benches/ and examples/backtest.rs
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Comprehensive tests for the Wickra Node bindings: streaming-vs-batch
|
||||
// equivalence, reference values, and lifecycle methods across all 25
|
||||
// equivalence, reference values, and lifecycle methods across all 63
|
||||
// indicators. Ported from the Python test_streaming_vs_batch / test_known_values
|
||||
// suites.
|
||||
|
||||
@@ -36,6 +36,25 @@ const scalarFactories = {
|
||||
ROC: () => new wickra.ROC(12),
|
||||
TRIX: () => new wickra.TRIX(9),
|
||||
KAMA: () => new wickra.KAMA(10, 2, 30),
|
||||
SMMA: () => new wickra.SMMA(14),
|
||||
TRIMA: () => new wickra.TRIMA(20),
|
||||
ZLEMA: () => new wickra.ZLEMA(14),
|
||||
T3: () => new wickra.T3(5, 0.7),
|
||||
MOM: () => new wickra.MOM(10),
|
||||
CMO: () => new wickra.CMO(14),
|
||||
TSI: () => new wickra.TSI(25, 13),
|
||||
PMO: () => new wickra.PMO(35, 20),
|
||||
StochRSI: () => new wickra.StochRSI(14, 14),
|
||||
PPO: () => new wickra.PPO(12, 26),
|
||||
DPO: () => new wickra.DPO(20),
|
||||
Coppock: () => new wickra.Coppock(14, 11, 10),
|
||||
StdDev: () => new wickra.StdDev(20),
|
||||
UlcerIndex: () => new wickra.UlcerIndex(14),
|
||||
HistoricalVolatility: () => new wickra.HistoricalVolatility(20, 252),
|
||||
BollingerBandwidth: () => new wickra.BollingerBandwidth(20, 2),
|
||||
PercentB: () => new wickra.PercentB(20, 2),
|
||||
LinearRegression: () => new wickra.LinearRegression(14),
|
||||
LinRegSlope: () => new wickra.LinRegSlope(14),
|
||||
};
|
||||
|
||||
for (const [name, make] of Object.entries(scalarFactories)) {
|
||||
@@ -61,6 +80,21 @@ const candleScalar = {
|
||||
VWAP: { make: () => new wickra.VWAP(), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
|
||||
AwesomeOscillator: { make: () => new wickra.AwesomeOscillator(5, 34), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
OBV: { make: () => new wickra.OBV(), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) },
|
||||
VWMA: { make: () => new wickra.VWMA(20), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) },
|
||||
UltimateOscillator: { make: () => new wickra.UltimateOscillator(7, 14, 28), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
AroonOscillator: { make: () => new wickra.AroonOscillator(14), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
NATR: { make: () => new wickra.NATR(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
MassIndex: { make: () => new wickra.MassIndex(9, 25), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
ADL: { make: () => new wickra.ADL(), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
|
||||
VolumePriceTrend: { make: () => new wickra.VolumePriceTrend(), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) },
|
||||
ChaikinMoneyFlow: { make: () => new wickra.ChaikinMoneyFlow(20), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
|
||||
ChaikinOscillator: { make: () => new wickra.ChaikinOscillator(3, 10), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
|
||||
ForceIndex: { make: () => new wickra.ForceIndex(13), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) },
|
||||
EaseOfMovement: { make: () => new wickra.EaseOfMovement(14, 1e8), step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
|
||||
AtrTrailingStop: { make: () => new wickra.AtrTrailingStop(14, 3), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
TypicalPrice: { make: () => new wickra.TypicalPrice(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
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) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(candleScalar)) {
|
||||
@@ -85,6 +119,10 @@ const multi = {
|
||||
Keltner: { make: () => new wickra.Keltner(20, 10, 2), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
Donchian: { make: () => new wickra.Donchian(20), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
Aroon: { make: () => new wickra.Aroon(14), fields: ['up', 'down'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
Vortex: { make: () => new wickra.Vortex(14), fields: ['plus', 'minus'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
SuperTrend: { make: () => new wickra.SuperTrend(10, 3), fields: ['value', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
ChandelierExit: { make: () => new wickra.ChandelierExit(22, 3), fields: ['longStop', 'shortStop'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
ChandeKrollStop: { make: () => new wickra.ChandeKrollStop(10, 1, 9), fields: ['stopLong', 'stopShort'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(multi)) {
|
||||
@@ -163,3 +201,34 @@ test('MACD histogram equals macd minus signal', () => {
|
||||
assert.ok(v);
|
||||
assert.ok(Math.abs(v.histogram - (v.macd - v.signal)) < 1e-9);
|
||||
});
|
||||
|
||||
test('TypicalPrice reference value', () => {
|
||||
// (high + low + close) / 3 = (12 + 6 + 9) / 3 = 9.
|
||||
assert.equal(new wickra.TypicalPrice().update(12, 6, 9), 9);
|
||||
});
|
||||
|
||||
test('ChaikinMoneyFlow(2) reference value equals 0.5', () => {
|
||||
// Bar 1 closes at the high (MFV +100); bar 2 closes mid-range (MFV 0).
|
||||
const cmf = new wickra.ChaikinMoneyFlow(2);
|
||||
assert.equal(cmf.update(10, 8, 10, 100), null);
|
||||
assert.ok(Math.abs(cmf.update(12, 8, 10, 100) - 0.5) < 1e-9);
|
||||
});
|
||||
|
||||
test('LinearRegression(3) reference values', () => {
|
||||
// Least-squares line through [1, 2, 9] is y = 4x; endpoint 4·2 = 8.
|
||||
const out = new wickra.LinearRegression(3).batch([1, 2, 9]);
|
||||
assert.ok(Number.isNaN(out[0]) && Number.isNaN(out[1]));
|
||||
assert.ok(Math.abs(out[2] - 8) < 1e-9);
|
||||
});
|
||||
|
||||
test('SuperTrend flat market holds the lower band and an uptrend', () => {
|
||||
// Flat candles: ATR 2, hl2 10, lower band 10 - 3·2 = 4.
|
||||
const n = 20;
|
||||
const out = new wickra.SuperTrend(5, 3).batch(
|
||||
Array(n).fill(11),
|
||||
Array(n).fill(9),
|
||||
Array(n).fill(10),
|
||||
);
|
||||
assert.ok(Math.abs(out[2 * n - 2] - 4) < 1e-9); // value
|
||||
assert.equal(out[2 * n - 1], 1); // direction
|
||||
});
|
||||
|
||||
+31
-31
@@ -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, T3, VWMA, MOM, CMO, TSI, PMO, StochRSI, UltimateOscillator, PPO, DPO, Coppock, AroonOscillator, Vortex, MassIndex, NATR, StdDev, UlcerIndex, HistoricalVolatility, BollingerBandwidth, PercentB, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, AwesomeOscillator, Aroon, KAMA } = nativeBinding
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, AwesomeOscillator, Aroon, KAMA, T3, TSI, PMO, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA } = nativeBinding
|
||||
|
||||
module.exports.version = version
|
||||
module.exports.SMA = SMA
|
||||
@@ -325,41 +325,11 @@ module.exports.TRIX = TRIX
|
||||
module.exports.SMMA = SMMA
|
||||
module.exports.TRIMA = TRIMA
|
||||
module.exports.ZLEMA = ZLEMA
|
||||
module.exports.T3 = T3
|
||||
module.exports.VWMA = VWMA
|
||||
module.exports.MOM = MOM
|
||||
module.exports.CMO = CMO
|
||||
module.exports.TSI = TSI
|
||||
module.exports.PMO = PMO
|
||||
module.exports.StochRSI = StochRSI
|
||||
module.exports.UltimateOscillator = UltimateOscillator
|
||||
module.exports.PPO = PPO
|
||||
module.exports.DPO = DPO
|
||||
module.exports.Coppock = Coppock
|
||||
module.exports.AroonOscillator = AroonOscillator
|
||||
module.exports.Vortex = Vortex
|
||||
module.exports.MassIndex = MassIndex
|
||||
module.exports.NATR = NATR
|
||||
module.exports.StdDev = StdDev
|
||||
module.exports.UlcerIndex = UlcerIndex
|
||||
module.exports.HistoricalVolatility = HistoricalVolatility
|
||||
module.exports.BollingerBandwidth = BollingerBandwidth
|
||||
module.exports.PercentB = PercentB
|
||||
module.exports.ADL = ADL
|
||||
module.exports.VolumePriceTrend = VolumePriceTrend
|
||||
module.exports.ChaikinMoneyFlow = ChaikinMoneyFlow
|
||||
module.exports.ChaikinOscillator = ChaikinOscillator
|
||||
module.exports.ForceIndex = ForceIndex
|
||||
module.exports.EaseOfMovement = EaseOfMovement
|
||||
module.exports.SuperTrend = SuperTrend
|
||||
module.exports.ChandelierExit = ChandelierExit
|
||||
module.exports.ChandeKrollStop = ChandeKrollStop
|
||||
module.exports.AtrTrailingStop = AtrTrailingStop
|
||||
module.exports.TypicalPrice = TypicalPrice
|
||||
module.exports.MedianPrice = MedianPrice
|
||||
module.exports.WeightedClose = WeightedClose
|
||||
module.exports.LinearRegression = LinearRegression
|
||||
module.exports.LinRegSlope = LinRegSlope
|
||||
module.exports.MACD = MACD
|
||||
module.exports.BollingerBands = BollingerBands
|
||||
module.exports.ATR = ATR
|
||||
@@ -376,3 +346,33 @@ module.exports.VWAP = VWAP
|
||||
module.exports.AwesomeOscillator = AwesomeOscillator
|
||||
module.exports.Aroon = Aroon
|
||||
module.exports.KAMA = KAMA
|
||||
module.exports.T3 = T3
|
||||
module.exports.TSI = TSI
|
||||
module.exports.PMO = PMO
|
||||
module.exports.ADL = ADL
|
||||
module.exports.VolumePriceTrend = VolumePriceTrend
|
||||
module.exports.ChaikinMoneyFlow = ChaikinMoneyFlow
|
||||
module.exports.ChaikinOscillator = ChaikinOscillator
|
||||
module.exports.ForceIndex = ForceIndex
|
||||
module.exports.EaseOfMovement = EaseOfMovement
|
||||
module.exports.SuperTrend = SuperTrend
|
||||
module.exports.ChandelierExit = ChandelierExit
|
||||
module.exports.ChandeKrollStop = ChandeKrollStop
|
||||
module.exports.AtrTrailingStop = AtrTrailingStop
|
||||
module.exports.TypicalPrice = TypicalPrice
|
||||
module.exports.MedianPrice = MedianPrice
|
||||
module.exports.WeightedClose = WeightedClose
|
||||
module.exports.LinearRegression = LinearRegression
|
||||
module.exports.LinRegSlope = LinRegSlope
|
||||
module.exports.BollingerBandwidth = BollingerBandwidth
|
||||
module.exports.PercentB = PercentB
|
||||
module.exports.NATR = NATR
|
||||
module.exports.HistoricalVolatility = HistoricalVolatility
|
||||
module.exports.AroonOscillator = AroonOscillator
|
||||
module.exports.Vortex = Vortex
|
||||
module.exports.MassIndex = MassIndex
|
||||
module.exports.StochRSI = StochRSI
|
||||
module.exports.UltimateOscillator = UltimateOscillator
|
||||
module.exports.PPO = PPO
|
||||
module.exports.Coppock = Coppock
|
||||
module.exports.VWMA = VWMA
|
||||
|
||||
@@ -25,58 +25,144 @@ from __future__ import annotations
|
||||
|
||||
from ._wickra import (
|
||||
__version__,
|
||||
ADX,
|
||||
ATR,
|
||||
Aroon,
|
||||
AwesomeOscillator,
|
||||
BollingerBands,
|
||||
CCI,
|
||||
DEMA,
|
||||
Donchian,
|
||||
# Trend
|
||||
SMA,
|
||||
EMA,
|
||||
WMA,
|
||||
DEMA,
|
||||
TEMA,
|
||||
HMA,
|
||||
KAMA,
|
||||
Keltner,
|
||||
MACD,
|
||||
MFI,
|
||||
OBV,
|
||||
PSAR,
|
||||
ROC,
|
||||
SMMA,
|
||||
TRIMA,
|
||||
ZLEMA,
|
||||
T3,
|
||||
VWMA,
|
||||
# Momentum
|
||||
RSI,
|
||||
SMA,
|
||||
MACD,
|
||||
Stochastic,
|
||||
TEMA,
|
||||
TRIX,
|
||||
VWAP,
|
||||
CCI,
|
||||
ROC,
|
||||
WilliamsR,
|
||||
WMA,
|
||||
ADX,
|
||||
MFI,
|
||||
TRIX,
|
||||
AwesomeOscillator,
|
||||
Aroon,
|
||||
MOM,
|
||||
CMO,
|
||||
TSI,
|
||||
PMO,
|
||||
StochRSI,
|
||||
UltimateOscillator,
|
||||
PPO,
|
||||
DPO,
|
||||
Coppock,
|
||||
AroonOscillator,
|
||||
Vortex,
|
||||
MassIndex,
|
||||
# Volatility
|
||||
BollingerBands,
|
||||
ATR,
|
||||
Keltner,
|
||||
Donchian,
|
||||
PSAR,
|
||||
NATR,
|
||||
StdDev,
|
||||
UlcerIndex,
|
||||
HistoricalVolatility,
|
||||
BollingerBandwidth,
|
||||
PercentB,
|
||||
SuperTrend,
|
||||
ChandelierExit,
|
||||
ChandeKrollStop,
|
||||
AtrTrailingStop,
|
||||
# Volume
|
||||
OBV,
|
||||
VWAP,
|
||||
ADL,
|
||||
VolumePriceTrend,
|
||||
ChaikinMoneyFlow,
|
||||
ChaikinOscillator,
|
||||
ForceIndex,
|
||||
EaseOfMovement,
|
||||
# Statistics
|
||||
TypicalPrice,
|
||||
MedianPrice,
|
||||
WeightedClose,
|
||||
LinearRegression,
|
||||
LinRegSlope,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
# Trend
|
||||
"SMA",
|
||||
"EMA",
|
||||
"WMA",
|
||||
"RSI",
|
||||
"MACD",
|
||||
"BollingerBands",
|
||||
"ATR",
|
||||
"Stochastic",
|
||||
"OBV",
|
||||
"DEMA",
|
||||
"TEMA",
|
||||
"HMA",
|
||||
"KAMA",
|
||||
"SMMA",
|
||||
"TRIMA",
|
||||
"ZLEMA",
|
||||
"T3",
|
||||
"VWMA",
|
||||
# Momentum
|
||||
"RSI",
|
||||
"MACD",
|
||||
"Stochastic",
|
||||
"CCI",
|
||||
"ROC",
|
||||
"WilliamsR",
|
||||
"ADX",
|
||||
"MFI",
|
||||
"TRIX",
|
||||
"PSAR",
|
||||
"Keltner",
|
||||
"Donchian",
|
||||
"VWAP",
|
||||
"AwesomeOscillator",
|
||||
"Aroon",
|
||||
"MOM",
|
||||
"CMO",
|
||||
"TSI",
|
||||
"PMO",
|
||||
"StochRSI",
|
||||
"UltimateOscillator",
|
||||
"PPO",
|
||||
"DPO",
|
||||
"Coppock",
|
||||
"AroonOscillator",
|
||||
"Vortex",
|
||||
"MassIndex",
|
||||
# Volatility
|
||||
"BollingerBands",
|
||||
"ATR",
|
||||
"Keltner",
|
||||
"Donchian",
|
||||
"PSAR",
|
||||
"NATR",
|
||||
"StdDev",
|
||||
"UlcerIndex",
|
||||
"HistoricalVolatility",
|
||||
"BollingerBandwidth",
|
||||
"PercentB",
|
||||
"SuperTrend",
|
||||
"ChandelierExit",
|
||||
"ChandeKrollStop",
|
||||
"AtrTrailingStop",
|
||||
# Volume
|
||||
"OBV",
|
||||
"VWAP",
|
||||
"ADL",
|
||||
"VolumePriceTrend",
|
||||
"ChaikinMoneyFlow",
|
||||
"ChaikinOscillator",
|
||||
"ForceIndex",
|
||||
"EaseOfMovement",
|
||||
# Statistics
|
||||
"TypicalPrice",
|
||||
"MedianPrice",
|
||||
"WeightedClose",
|
||||
"LinearRegression",
|
||||
"LinRegSlope",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
"""Streaming-vs-batch, shape and reference-value tests for the F1-F12 families.
|
||||
|
||||
Every indicator added since the original 25 is exercised here. The central
|
||||
contract is the same as the rest of the suite: ``batch(...)`` must equal
|
||||
repeated streaming ``update(...)`` across the whole warmup -> steady-state
|
||||
transition, and batch shapes must match the input length.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import wickra as ta
|
||||
|
||||
|
||||
def _eq_nan(a: np.ndarray, b: np.ndarray, tol: float = 1e-9) -> bool:
|
||||
"""Compare two float arrays treating NaN positions as equal."""
|
||||
a = np.asarray(a, dtype=np.float64)
|
||||
b = np.asarray(b, dtype=np.float64)
|
||||
if a.shape != b.shape:
|
||||
return False
|
||||
both_nan = np.isnan(a) & np.isnan(b)
|
||||
return bool(np.all(np.where(both_nan, 0.0, np.abs(a - b)) <= tol))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""Synthetic high / low / close / volume series, 200 bars."""
|
||||
t = np.arange(200, dtype=np.float64)
|
||||
close = 100.0 + np.sin(t * 0.15) * 8.0 + np.cos(t * 0.32) * 3.0
|
||||
spread = 0.5 + np.abs(np.sin(t * 0.07))
|
||||
high = close + spread
|
||||
low = close - spread
|
||||
volume = 1000.0 + (t % 7) * 50.0
|
||||
return high, low, close, volume
|
||||
|
||||
|
||||
# --- Scalar (f64 -> f64) indicators ---------------------------------------
|
||||
|
||||
SCALAR = [
|
||||
(ta.SMMA, (14,)),
|
||||
(ta.TRIMA, (20,)),
|
||||
(ta.ZLEMA, (14,)),
|
||||
(ta.T3, (5, 0.7)),
|
||||
(ta.MOM, (10,)),
|
||||
(ta.CMO, (14,)),
|
||||
(ta.TSI, (25, 13)),
|
||||
(ta.PMO, (35, 20)),
|
||||
(ta.StochRSI, (14, 14)),
|
||||
(ta.PPO, (12, 26)),
|
||||
(ta.DPO, (20,)),
|
||||
(ta.Coppock, (14, 11, 10)),
|
||||
(ta.StdDev, (20,)),
|
||||
(ta.UlcerIndex, (14,)),
|
||||
(ta.HistoricalVolatility, (20, 252)),
|
||||
(ta.BollingerBandwidth, (20, 2.0)),
|
||||
(ta.PercentB, (20, 2.0)),
|
||||
(ta.LinearRegression, (14,)),
|
||||
(ta.LinRegSlope, (14,)),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cls, args", SCALAR, ids=[c.__name__ for c, _ in SCALAR])
|
||||
def test_scalar_streaming_matches_batch(cls, args, sine_prices):
|
||||
batch = cls(*args).batch(sine_prices)
|
||||
assert batch.shape == sine_prices.shape
|
||||
assert batch.dtype == np.float64
|
||||
|
||||
streamer = cls(*args)
|
||||
streamed = []
|
||||
for p in sine_prices:
|
||||
v = streamer.update(float(p))
|
||||
streamed.append(math.nan if v is None else float(v))
|
||||
assert _eq_nan(batch, np.array(streamed, dtype=np.float64))
|
||||
|
||||
|
||||
# --- Candle-input, single-output indicators -------------------------------
|
||||
#
|
||||
# Each entry is (factory, batch-call). Streaming always feeds the full
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"VWMA": (lambda: ta.VWMA(20), lambda ind, h, l, c, v: ind.batch(c, v)),
|
||||
"UltimateOscillator": (
|
||||
lambda: ta.UltimateOscillator(7, 14, 28),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"AroonOscillator": (
|
||||
lambda: ta.AroonOscillator(14),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
),
|
||||
"NATR": (lambda: ta.NATR(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"MassIndex": (lambda: ta.MassIndex(9, 25), lambda ind, h, l, c, v: ind.batch(h, l)),
|
||||
"ADL": (lambda: ta.ADL(), lambda ind, h, l, c, v: ind.batch(h, l, c, v)),
|
||||
"VolumePriceTrend": (
|
||||
lambda: ta.VolumePriceTrend(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, v),
|
||||
),
|
||||
"ChaikinMoneyFlow": (
|
||||
lambda: ta.ChaikinMoneyFlow(20),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c, v),
|
||||
),
|
||||
"ChaikinOscillator": (
|
||||
lambda: ta.ChaikinOscillator(3, 10),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c, v),
|
||||
),
|
||||
"ForceIndex": (
|
||||
lambda: ta.ForceIndex(13),
|
||||
lambda ind, h, l, c, v: ind.batch(c, v),
|
||||
),
|
||||
"EaseOfMovement": (
|
||||
lambda: ta.EaseOfMovement(14),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, v),
|
||||
),
|
||||
"AtrTrailingStop": (
|
||||
lambda: ta.AtrTrailingStop(14, 3.0),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"TypicalPrice": (
|
||||
lambda: ta.TypicalPrice(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"MedianPrice": (
|
||||
lambda: ta.MedianPrice(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
),
|
||||
"WeightedClose": (
|
||||
lambda: ta.WeightedClose(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(CANDLE_SCALAR))
|
||||
def test_candle_scalar_streaming_matches_batch(name, ohlcv):
|
||||
high, low, close, volume = ohlcv
|
||||
make, batch_call = CANDLE_SCALAR[name]
|
||||
|
||||
batch = batch_call(make(), high, low, close, volume)
|
||||
assert batch.shape == close.shape
|
||||
|
||||
streamer = make()
|
||||
streamed = []
|
||||
for i in range(close.size):
|
||||
candle = (
|
||||
float(close[i]),
|
||||
float(high[i]),
|
||||
float(low[i]),
|
||||
float(close[i]),
|
||||
float(volume[i]),
|
||||
i,
|
||||
)
|
||||
v = streamer.update(candle)
|
||||
streamed.append(math.nan if v is None else float(v))
|
||||
assert _eq_nan(batch, np.array(streamed, dtype=np.float64)), f"{name} mismatch"
|
||||
|
||||
|
||||
# --- Candle-input, multi-output indicators --------------------------------
|
||||
|
||||
MULTI = {
|
||||
"Vortex": (lambda: ta.Vortex(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"SuperTrend": (
|
||||
lambda: ta.SuperTrend(10, 3.0),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"ChandelierExit": (
|
||||
lambda: ta.ChandelierExit(22, 3.0),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"ChandeKrollStop": (
|
||||
lambda: ta.ChandeKrollStop(10, 1.0, 9),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(MULTI))
|
||||
def test_multi_streaming_matches_batch(name, ohlcv):
|
||||
high, low, close, volume = ohlcv
|
||||
make, batch_call = MULTI[name]
|
||||
|
||||
batch = batch_call(make(), high, low, close, volume)
|
||||
assert batch.shape == (close.size, 2)
|
||||
|
||||
streamer = make()
|
||||
rows = []
|
||||
for i in range(close.size):
|
||||
candle = (
|
||||
float(close[i]),
|
||||
float(high[i]),
|
||||
float(low[i]),
|
||||
float(close[i]),
|
||||
float(volume[i]),
|
||||
i,
|
||||
)
|
||||
v = streamer.update(candle)
|
||||
rows.append([math.nan, math.nan] if v is None else list(v))
|
||||
assert _eq_nan(batch, np.array(rows, dtype=np.float64)), f"{name} mismatch"
|
||||
|
||||
|
||||
# --- Reference values -----------------------------------------------------
|
||||
|
||||
|
||||
def test_typical_price_reference():
|
||||
# (high + low + close) / 3 = (12 + 6 + 9) / 3 = 9.
|
||||
assert ta.TypicalPrice().update((9.0, 12.0, 6.0, 9.0, 1.0, 0)) == pytest.approx(9.0)
|
||||
|
||||
|
||||
def test_median_price_reference():
|
||||
# (high + low) / 2 = (12 + 8) / 2 = 10.
|
||||
assert ta.MedianPrice().update((10.0, 12.0, 8.0, 11.0, 1.0, 0)) == pytest.approx(10.0)
|
||||
|
||||
|
||||
def test_weighted_close_reference():
|
||||
# (high + low + 2*close) / 4 = (12 + 8 + 22) / 4 = 10.5.
|
||||
assert ta.WeightedClose().update((10.0, 12.0, 8.0, 11.0, 1.0, 0)) == pytest.approx(
|
||||
10.5
|
||||
)
|
||||
|
||||
|
||||
def test_chaikin_money_flow_reference():
|
||||
cmf = ta.ChaikinMoneyFlow(2)
|
||||
assert cmf.update((8.0, 10.0, 8.0, 10.0, 100.0, 0)) is None
|
||||
assert cmf.update((10.0, 12.0, 8.0, 10.0, 100.0, 1)) == pytest.approx(0.5)
|
||||
|
||||
|
||||
def test_linear_regression_reference():
|
||||
out = ta.LinearRegression(3).batch(np.array([1.0, 2.0, 9.0]))
|
||||
assert math.isnan(out[0]) and math.isnan(out[1])
|
||||
assert out[2] == pytest.approx(8.0)
|
||||
|
||||
|
||||
def test_linreg_slope_reference():
|
||||
out = ta.LinRegSlope(3).batch(np.array([1.0, 2.0, 9.0]))
|
||||
assert math.isnan(out[0]) and math.isnan(out[1])
|
||||
assert out[2] == pytest.approx(4.0)
|
||||
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
def test_new_indicators_expose_lifecycle():
|
||||
instances = [make() for make, _ in CANDLE_SCALAR.values()]
|
||||
instances += [make() for make, _ in MULTI.values()]
|
||||
instances += [cls(*args) for cls, args in SCALAR]
|
||||
for ind in instances:
|
||||
assert ind.is_ready() is False
|
||||
assert ind.warmup_period() >= 1
|
||||
ind.reset()
|
||||
assert ind.is_ready() is False
|
||||
+4
-3
@@ -7,9 +7,10 @@ Node.js, WebAssembly, and Rust itself. The same `update` call you write inside
|
||||
a live trading loop also drives the historical backtest of that same
|
||||
strategy — there is no second code path that drifts behind the streaming one.
|
||||
|
||||
The project ships 25 indicators across the four classical families (trend,
|
||||
momentum, volatility, volume) and a small set of supporting types (`Candle`,
|
||||
`Tick`, `Chain`). The Rust core forbids `unsafe`, so every binding inherits a
|
||||
The project ships 63 indicators across the four classical families (trend,
|
||||
momentum, volatility, volume) plus a statistics group, and a small set of
|
||||
supporting types (`Candle`, `Tick`, `Chain`). The Rust core forbids `unsafe`,
|
||||
so every binding inherits a
|
||||
memory-safe implementation. Install is one command on every supported
|
||||
platform: `pip install wickra`, `cargo add wickra`, `npm install wickra` — no
|
||||
system compilers, no C dependencies, no headers.
|
||||
|
||||
@@ -38,10 +38,44 @@ index" in 0-indexed terms is `warmup_period − 1`.
|
||||
| `Trix` | `Trix::new(15)` | `3 * period - 1` | 44 | 44th |
|
||||
| `AwesomeOscillator` | `AwesomeOscillator::new(5, 34)` | `slow_period` | 34 | 34th |
|
||||
| `Atr` | `Atr::new(14)` | `period` | 14 | 14th |
|
||||
| `Psar` | `Psar::new(0.02, 0.20)` | constant `2` | 2 | 2nd |
|
||||
| `Psar` | `Psar::new(0.02, 0.02, 0.20)` | constant `2` | 2 | 2nd |
|
||||
| `Obv` | `Obv::new()` | constant `1` | 1 | 1st |
|
||||
| `Vwap` | `Vwap::new()` | constant `1` | 1 | 1st |
|
||||
| `RollingVwap` | `RollingVwap::new(20)` | `period` | 20 | 20th |
|
||||
| `Smma` | `Smma::new(14)` | `period` | 14 | 14th |
|
||||
| `Trima` | `Trima::new(20)` | `period` | 20 | 20th |
|
||||
| `Zlema` | `Zlema::new(14)` | `lag + period` (`lag = (period − 1) / 2`) | 20 | 20th |
|
||||
| `T3` | `T3::new(5, 0.7)` | `6 * period - 5` | 25 | 25th |
|
||||
| `Vwma` | `Vwma::new(20)` | `period` | 20 | 20th |
|
||||
| `Mom` | `Mom::new(10)` | `period + 1` | 11 | 11th |
|
||||
| `Cmo` | `Cmo::new(14)` | `period + 1` | 15 | 15th |
|
||||
| `Tsi` | `Tsi::new(25, 13)` | `long + short` | 38 | 38th |
|
||||
| `Pmo` | `Pmo::new(35, 20)` | constant `2` | 2 | 2nd |
|
||||
| `StochRsi` | `StochRsi::new(14, 14)` | `rsi_period + stoch_period` | 28 | 28th |
|
||||
| `UltimateOscillator` | `UltimateOscillator::new(7, 14, 28)` | `max(short, mid, long) + 1` | 29 | 29th |
|
||||
| `Ppo` | `Ppo::new(12, 26)` | `slow` | 26 | 26th |
|
||||
| `Dpo` | `Dpo::new(20)` | `max(period, period / 2 + 2)` | 20 | 20th |
|
||||
| `Coppock` | `Coppock::new(14, 11, 10)` | `max(roc_long, roc_short) + wma_period` | 24 | 24th |
|
||||
| `AroonOscillator` | `AroonOscillator::new(14)` | `period + 1` | 15 | 15th |
|
||||
| `MassIndex` | `MassIndex::new(9, 25)` | `2 * ema_period + sum_period - 2` | 41 | 41st |
|
||||
| `Natr` | `Natr::new(14)` | `period` | 14 | 14th |
|
||||
| `StdDev` | `StdDev::new(20)` | `period` | 20 | 20th |
|
||||
| `UlcerIndex` | `UlcerIndex::new(14)` | `2 * period - 1` | 27 | 27th |
|
||||
| `HistoricalVolatility` | `HistoricalVolatility::new(20, 252)` | `period + 1` | 21 | 21st |
|
||||
| `BollingerBandwidth` | `BollingerBandwidth::new(20, 2.0)` | `period` | 20 | 20th |
|
||||
| `PercentB` | `PercentB::new(20, 2.0)` | `period` | 20 | 20th |
|
||||
| `AtrTrailingStop` | `AtrTrailingStop::new(14, 3.0)` | `atr_period` | 14 | 14th |
|
||||
| `Adl` | `Adl::new()` | constant `1` | 1 | 1st |
|
||||
| `VolumePriceTrend` | `VolumePriceTrend::new()` | constant `1` | 1 | 1st |
|
||||
| `ChaikinMoneyFlow` | `ChaikinMoneyFlow::new(20)` | `period` | 20 | 20th |
|
||||
| `ChaikinOscillator` | `ChaikinOscillator::new(3, 10)` | `slow` | 10 | 10th |
|
||||
| `ForceIndex` | `ForceIndex::new(13)` | `period + 1` | 14 | 14th |
|
||||
| `EaseOfMovement` | `EaseOfMovement::new(14)` | `period + 1` | 15 | 15th |
|
||||
| `TypicalPrice` | `TypicalPrice::new()` | constant `1` | 1 | 1st |
|
||||
| `MedianPrice` | `MedianPrice::new()` | constant `1` | 1 | 1st |
|
||||
| `WeightedClose` | `WeightedClose::new()` | constant `1` | 1 | 1st |
|
||||
| `LinearRegression` | `LinearRegression::new(14)` | `period` | 14 | 14th |
|
||||
| `LinRegSlope` | `LinRegSlope::new(14)` | `period` | 14 | 14th |
|
||||
|
||||
## Multi-output indicators
|
||||
|
||||
@@ -59,6 +93,10 @@ ready" to "ready" together — there are no rows that have a `signal` but no
|
||||
| `Aroon` | `Aroon::new(14)` | `period + 1` | 15 | 15th | `up`, `down` |
|
||||
| `Keltner` | `Keltner::new(20, 10, 2.0)` | `ema_period.max(atr_period)` | 20 | 20th | `upper`, `middle`, `lower` |
|
||||
| `Donchian` | `Donchian::new(20)` | `period` | 20 | 20th | `upper`, `middle`, `lower` |
|
||||
| `Vortex` | `Vortex::new(14)` | `period + 1` | 15 | 15th | `plus`, `minus` |
|
||||
| `SuperTrend` | `SuperTrend::new(10, 3.0)` | `atr_period` | 10 | 10th | `value`, `direction` |
|
||||
| `ChandelierExit` | `ChandelierExit::new(22, 3.0)` | `period` | 22 | 22nd | `long_stop`, `short_stop` |
|
||||
| `ChandeKrollStop` | `ChandeKrollStop::new(10, 1.0, 9)` | `atr_period + stop_period - 1` | 18 | 18th | `stop_long`, `stop_short` |
|
||||
|
||||
## "Off-by-one" cases worth memorising
|
||||
|
||||
|
||||
Reference in New Issue
Block a user