feat: Family 05 Bands & Channels - 11 new price-envelope indicators (#43)
* feat(bands-channels): add Family 05 with 11 indicators
Eleven price-envelope overlays organised into a new "Bands & Channels"
family, exposed across all four bindings (Rust core, Python, Node, WASM)
plus fuzz/test/bench/docs coverage:
- MaEnvelope - SMA centerline with fixed-percent envelope (the oldest
band overlay still in regular use).
- AccelerationBands (Price Headley) - momentum-biased bands that widen
with the bar's relative range (H - L) / (H + L).
- StarcBands (Stoller Average Range Channel) - SMA(close) +/- k*ATR;
Keltner's SMA-centerline sibling.
- AtrBands - close-anchored envelope of width k*ATR; the standard
volatility-targeting stop/target band.
- HurstChannel - SMA centerline wrapped by the rolling high-low range
(Brian Millard / Hurst-cycle channel).
- LinRegChannel - rolling OLS endpoint +/- k * population stddev of the
residuals; dispersion about the trend rather than the mean.
- StandardErrorBands - regression line +/- k * OLS standard error
(denominator n - 2) for prediction-interval bands.
- DoubleBollinger (Kathy Lien) - two concentric BB envelopes
(typically +/- 1 sigma and +/- 2 sigma) for the zone-partition setup.
- TtmSqueeze (John Carter) - BB-inside-KC squeeze flag paired with a
detrended-close linear-regression momentum reading.
- FractalChaosBands - Bill Williams 5-bar fractal high/low envelope.
- VwapStdDevBands - cumulative VWAP with volume-weighted population
standard deviation bands.
Each indicator ships:
- Core impl with the full Indicator trait, classic() where applicable,
and unit tests (rejects_zero_period / multiplier, accessors, flat
market, monotonic ordering, batch == streaming, reset, plus
algebraically verifiable reference values).
- Python PyO3 binding with multi-column NumPy batch (PyArray2).
- Node napi binding with #[napi(object)] struct + interleaved flat
batch.
- WASM wasm-bindgen binding via Object/Reflect for update +
Float64Array for batch.
- Fuzz coverage in fuzz_targets/indicator_update{,_candle}.rs.
- Python streaming-vs-batch parametric test + reference test.
- Node streaming-vs-interleaved-batch test + reference test.
- Criterion microbench under crates/wickra/benches/indicators.rs.
README family table, README indicator-count line, and CHANGELOG
Unreleased entry updated: indicator total rises from 71 to 82 across
nine families. Wiki pages are updated in a separate commit in the
wickra.wiki repo.
* test(acceleration-bands): cover sum_hl==0 zero-price guard
Exercises line 104 (`0.0` branch of the `sum_hl == 0.0` guard) which
was the last patch-coverage miss on the family-05 PR. `Candle::new`
accepts a fully-zero bar so the branch is reachable in principle —
add a degenerate-candle unit test to hit it.
This commit is contained in:
@@ -158,6 +158,18 @@ const multi = {
|
||||
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) },
|
||||
// Family 05: bands & channels
|
||||
MaEnvelope: { make: () => new wickra.MaEnvelope(20, 0.025), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
AccelerationBands: { make: () => new wickra.AccelerationBands(20, 0.001), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
StarcBands: { make: () => new wickra.StarcBands(6, 15, 2), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
AtrBands: { make: () => new wickra.AtrBands(14, 3), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
HurstChannel: { make: () => new wickra.HurstChannel(10, 0.5), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
LinRegChannel: { make: () => new wickra.LinRegChannel(20, 2), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
StandardErrorBands: { make: () => new wickra.StandardErrorBands(21, 2), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
DoubleBollinger: { make: () => new wickra.DoubleBollinger(20, 1, 2), fields: ['upperOuter', 'upperInner', 'middle', 'lowerInner', 'lowerOuter'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
TtmSqueeze: { make: () => new wickra.TtmSqueeze(20, 2, 1.5), fields: ['squeeze', 'momentum'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
FractalChaosBands: { make: () => new wickra.FractalChaosBands(2), fields: ['upper', 'lower'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
VwapStdDevBands: { make: () => new wickra.VwapStdDevBands(2), fields: ['upper', 'middle', 'lower', 'stddev'], step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(multi)) {
|
||||
@@ -284,6 +296,44 @@ test('LinRegAngle of a unit-slope series is 45 degrees', () => {
|
||||
assert.ok(Math.abs(out[4] - 45) < 1e-9);
|
||||
});
|
||||
|
||||
test('MaEnvelope reference values', () => {
|
||||
// SMA([10, 20, 30]) = 20; with percent 0.10: upper=22, lower=18.
|
||||
const out = new wickra.MaEnvelope(3, 0.10).batch([10, 20, 30]);
|
||||
assert.ok(Number.isNaN(out[0]) && Number.isNaN(out[3]));
|
||||
assert.ok(Math.abs(out[2 * 3 + 0] - 22) < 1e-9); // upper
|
||||
assert.ok(Math.abs(out[2 * 3 + 1] - 20) < 1e-9); // middle
|
||||
assert.ok(Math.abs(out[2 * 3 + 2] - 18) < 1e-9); // lower
|
||||
});
|
||||
|
||||
test('AccelerationBands single-bar reference', () => {
|
||||
// high=12, low=8, close=10, factor=0.5, period=1.
|
||||
// ratio=0.2, raw_up=13.2, raw_lo=7.2.
|
||||
const v = new wickra.AccelerationBands(1, 0.5).update(12, 8, 10);
|
||||
assert.ok(Math.abs(v.upper - 13.2) < 1e-9);
|
||||
assert.ok(Math.abs(v.middle - 10) < 1e-9);
|
||||
assert.ok(Math.abs(v.lower - 7.2) < 1e-9);
|
||||
});
|
||||
|
||||
test('LinRegChannel reference values for [1, 2, 9]', () => {
|
||||
// Line y=4x, endpoint=8, residuals=[1,-2,1], sigma=sqrt(2).
|
||||
const out = new wickra.LinRegChannel(3, 2).batch([1, 2, 9]);
|
||||
const s = Math.sqrt(2);
|
||||
const i = 2;
|
||||
assert.ok(Math.abs(out[i * 3 + 0] - (8 + 2 * s)) < 1e-9);
|
||||
assert.ok(Math.abs(out[i * 3 + 1] - 8) < 1e-9);
|
||||
assert.ok(Math.abs(out[i * 3 + 2] - (8 - 2 * s)) < 1e-9);
|
||||
});
|
||||
|
||||
test('VwapStdDevBands two-bar reference', () => {
|
||||
const v = new wickra.VwapStdDevBands(1.5);
|
||||
v.update(8, 8, 8, 1);
|
||||
const o = v.update(12, 12, 12, 1);
|
||||
assert.ok(Math.abs(o.upper - 13) < 1e-9);
|
||||
assert.ok(Math.abs(o.middle - 10) < 1e-9);
|
||||
assert.ok(Math.abs(o.lower - 7) < 1e-9);
|
||||
assert.ok(Math.abs(o.stddev - 2) < 1e-9);
|
||||
});
|
||||
|
||||
test('RVIVolatility pure uptrend saturates at 100', () => {
|
||||
const prices = Array.from({ length: 40 }, (_, i) => i + 1);
|
||||
const out = new wickra.RVIVolatility(5).batch(prices);
|
||||
|
||||
+12
-1
@@ -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, RVI, PGO, KST, SMI, LaguerreRSI, ConnorsRSI, Inertia, ALMA, McGinleyDynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, APO, AwesomeOscillatorHistogram, CFO, ZeroLagMACD, ElderImpulse, STC, 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, RVIVolatility, ParkinsonVolatility, GarmanKlassVolatility, RogersSatchellVolatility, YangZhangVolatility } = 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, RVI, PGO, KST, SMI, LaguerreRSI, ConnorsRSI, Inertia, ALMA, McGinleyDynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, APO, AwesomeOscillatorHistogram, CFO, ZeroLagMACD, ElderImpulse, STC, 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, RVIVolatility, ParkinsonVolatility, GarmanKlassVolatility, RogersSatchellVolatility, YangZhangVolatility, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands } = nativeBinding
|
||||
|
||||
module.exports.version = version
|
||||
module.exports.SMA = SMA
|
||||
@@ -410,3 +410,14 @@ module.exports.ParkinsonVolatility = ParkinsonVolatility
|
||||
module.exports.GarmanKlassVolatility = GarmanKlassVolatility
|
||||
module.exports.RogersSatchellVolatility = RogersSatchellVolatility
|
||||
module.exports.YangZhangVolatility = YangZhangVolatility
|
||||
module.exports.MaEnvelope = MaEnvelope
|
||||
module.exports.AccelerationBands = AccelerationBands
|
||||
module.exports.StarcBands = StarcBands
|
||||
module.exports.AtrBands = AtrBands
|
||||
module.exports.HurstChannel = HurstChannel
|
||||
module.exports.LinRegChannel = LinRegChannel
|
||||
module.exports.StandardErrorBands = StandardErrorBands
|
||||
module.exports.DoubleBollinger = DoubleBollinger
|
||||
module.exports.TtmSqueeze = TtmSqueeze
|
||||
module.exports.FractalChaosBands = FractalChaosBands
|
||||
module.exports.VwapStdDevBands = VwapStdDevBands
|
||||
|
||||
@@ -4131,3 +4131,762 @@ impl VwmaNode {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Family 05: Bands & Channels ==============================
|
||||
|
||||
// ---------- MA Envelope ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct MaEnvelopeValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "MaEnvelope")]
|
||||
pub struct MaEnvelopeNode {
|
||||
inner: wc::MaEnvelope,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl MaEnvelopeNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, percent: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::MaEnvelope::new(period as usize, percent).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<MaEnvelopeValue> {
|
||||
self.inner.update(value).map(|o| MaEnvelopeValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
})
|
||||
}
|
||||
/// Flat `[upper0, middle0, lower0, upper1, ...]`, length `3 * n`.
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
let mut out = vec![f64::NAN; prices.len() * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Acceleration Bands ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct AccelerationBandsValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "AccelerationBands")]
|
||||
pub struct AccelerationBandsNode {
|
||||
inner: wc::AccelerationBands,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl AccelerationBandsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, factor: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AccelerationBands::new(period as usize, factor).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<AccelerationBandsValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| AccelerationBandsValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- STARC Bands ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct StarcBandsValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "StarcBands")]
|
||||
pub struct StarcBandsNode {
|
||||
inner: wc::StarcBands,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl StarcBandsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(sma_period: u32, atr_period: u32, multiplier: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::StarcBands::new(sma_period as usize, atr_period as usize, multiplier)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<StarcBandsValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| StarcBandsValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- ATR Bands ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct AtrBandsValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "AtrBands")]
|
||||
pub struct AtrBandsNode {
|
||||
inner: wc::AtrBands,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl AtrBandsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, multiplier: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AtrBands::new(period as usize, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<AtrBandsValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| AtrBandsValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Hurst Channel ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct HurstChannelValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "HurstChannel")]
|
||||
pub struct HurstChannelNode {
|
||||
inner: wc::HurstChannel,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl HurstChannelNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, multiplier: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HurstChannel::new(period as usize, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<HurstChannelValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| HurstChannelValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- LinReg Channel ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct LinRegChannelValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "LinRegChannel")]
|
||||
pub struct LinRegChannelNode {
|
||||
inner: wc::LinRegChannel,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl LinRegChannelNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, multiplier: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::LinRegChannel::new(period as usize, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<LinRegChannelValue> {
|
||||
self.inner.update(value).map(|o| LinRegChannelValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
let mut out = vec![f64::NAN; prices.len() * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Standard Error Bands ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct StandardErrorBandsValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "StandardErrorBands")]
|
||||
pub struct StandardErrorBandsNode {
|
||||
inner: wc::StandardErrorBands,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl StandardErrorBandsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, multiplier: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::StandardErrorBands::new(period as usize, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<StandardErrorBandsValue> {
|
||||
self.inner.update(value).map(|o| StandardErrorBandsValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
let mut out = vec![f64::NAN; prices.len() * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Double Bollinger ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct DoubleBollingerValue {
|
||||
#[napi(js_name = "upperOuter")]
|
||||
pub upper_outer: f64,
|
||||
#[napi(js_name = "upperInner")]
|
||||
pub upper_inner: f64,
|
||||
pub middle: f64,
|
||||
#[napi(js_name = "lowerInner")]
|
||||
pub lower_inner: f64,
|
||||
#[napi(js_name = "lowerOuter")]
|
||||
pub lower_outer: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "DoubleBollinger")]
|
||||
pub struct DoubleBollingerNode {
|
||||
inner: wc::DoubleBollinger,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl DoubleBollingerNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, k_inner: f64, k_outer: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::DoubleBollinger::new(period as usize, k_inner, k_outer).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<DoubleBollingerValue> {
|
||||
self.inner.update(value).map(|o| DoubleBollingerValue {
|
||||
upper_outer: o.upper_outer,
|
||||
upper_inner: o.upper_inner,
|
||||
middle: o.middle,
|
||||
lower_inner: o.lower_inner,
|
||||
lower_outer: o.lower_outer,
|
||||
})
|
||||
}
|
||||
/// Flat `[u_o, u_i, m, l_i, l_o, ...]`, length `5 * n`.
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
let mut out = vec![f64::NAN; prices.len() * 5];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 5] = o.upper_outer;
|
||||
out[i * 5 + 1] = o.upper_inner;
|
||||
out[i * 5 + 2] = o.middle;
|
||||
out[i * 5 + 3] = o.lower_inner;
|
||||
out[i * 5 + 4] = o.lower_outer;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- TTM Squeeze ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct TtmSqueezeValue {
|
||||
pub squeeze: f64,
|
||||
pub momentum: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "TtmSqueeze")]
|
||||
pub struct TtmSqueezeNode {
|
||||
inner: wc::TtmSqueeze,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl TtmSqueezeNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, bb_mult: f64, kc_mult: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TtmSqueeze::new(period as usize, bb_mult, kc_mult).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<TtmSqueezeValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| TtmSqueezeValue {
|
||||
squeeze: o.squeeze,
|
||||
momentum: o.momentum,
|
||||
}))
|
||||
}
|
||||
/// Flat `[sq0, mom0, sq1, mom1, ...]`, length `2 * n`.
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
out[i * 2] = o.squeeze;
|
||||
out[i * 2 + 1] = o.momentum;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Fractal Chaos Bands ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct FractalChaosBandsValue {
|
||||
pub upper: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "FractalChaosBands")]
|
||||
pub struct FractalChaosBandsNode {
|
||||
inner: wc::FractalChaosBands,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl FractalChaosBandsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(k: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::FractalChaosBands::new(k as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FractalChaosBandsValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, low, 0.0)?)
|
||||
.map(|o| FractalChaosBandsValue {
|
||||
upper: o.upper,
|
||||
lower: o.lower,
|
||||
}))
|
||||
}
|
||||
/// Flat `[u0, l0, u1, l1, ...]`, length `2 * n`.
|
||||
#[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 n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], low[i], 0.0)?) {
|
||||
out[i * 2] = o.upper;
|
||||
out[i * 2 + 1] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- VWAP StdDev Bands ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct VwapStdDevBandsValue {
|
||||
pub upper: f64,
|
||||
pub middle: f64,
|
||||
pub lower: f64,
|
||||
pub stddev: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "VwapStdDevBands")]
|
||||
pub struct VwapStdDevBandsNode {
|
||||
inner: wc::VwapStdDevBands,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl VwapStdDevBandsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(multiplier: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VwapStdDevBands::new(multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> napi::Result<Option<VwapStdDevBandsValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, volume)?)
|
||||
.map(|o| VwapStdDevBandsValue {
|
||||
upper: o.upper,
|
||||
middle: o.middle,
|
||||
lower: o.lower,
|
||||
stddev: o.stddev,
|
||||
}))
|
||||
}
|
||||
/// Flat `[u0, m0, l0, sd0, ...]`, length `4 * n`.
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n || volume.len() != n {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close, volume must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self
|
||||
.inner
|
||||
.update(cnd(high[i], low[i], close[i], volume[i])?)
|
||||
{
|
||||
out[i * 4] = o.upper;
|
||||
out[i * 4 + 1] = o.middle;
|
||||
out[i * 4 + 2] = o.lower;
|
||||
out[i * 4 + 3] = o.stddev;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,18 @@ from ._wickra import (
|
||||
LinRegSlope,
|
||||
ZScore,
|
||||
LinRegAngle,
|
||||
# Bands & Channels
|
||||
MaEnvelope,
|
||||
AccelerationBands,
|
||||
StarcBands,
|
||||
AtrBands,
|
||||
HurstChannel,
|
||||
LinRegChannel,
|
||||
StandardErrorBands,
|
||||
DoubleBollinger,
|
||||
TtmSqueeze,
|
||||
FractalChaosBands,
|
||||
VwapStdDevBands,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -233,4 +245,16 @@ __all__ = [
|
||||
"LinRegSlope",
|
||||
"ZScore",
|
||||
"LinRegAngle",
|
||||
# Bands & Channels
|
||||
"MaEnvelope",
|
||||
"AccelerationBands",
|
||||
"StarcBands",
|
||||
"AtrBands",
|
||||
"HurstChannel",
|
||||
"LinRegChannel",
|
||||
"StandardErrorBands",
|
||||
"DoubleBollinger",
|
||||
"TtmSqueeze",
|
||||
"FractalChaosBands",
|
||||
"VwapStdDevBands",
|
||||
]
|
||||
|
||||
@@ -5949,9 +5949,736 @@ impl PyRviVolatility {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== MA Envelope ==============================
|
||||
|
||||
#[pyclass(name = "MaEnvelope", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyMaEnvelope {
|
||||
inner: wc::MaEnvelope,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyMaEnvelope {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20, percent=0.025))]
|
||||
fn new(period: usize, percent: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::MaEnvelope::new(period, percent).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `(upper, middle, lower)` or `None` during warmup.
|
||||
fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
|
||||
self.inner
|
||||
.update(value)
|
||||
.map(|o| (o.upper, o.middle, o.lower))
|
||||
}
|
||||
/// Batch returns shape `(n, 3)` columns `[upper, middle, lower]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let n = slice.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in slice.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Acceleration Bands ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "AccelerationBands",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyAccelerationBands {
|
||||
inner: wc::AccelerationBands,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAccelerationBands {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20, factor=0.001))]
|
||||
fn new(period: usize, factor: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AccelerationBands::new(period, factor).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.upper, o.middle, o.lower)))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<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))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== STARC Bands ==============================
|
||||
|
||||
#[pyclass(name = "StarcBands", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyStarcBands {
|
||||
inner: wc::StarcBands,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyStarcBands {
|
||||
#[new]
|
||||
#[pyo3(signature = (sma_period=6, atr_period=15, multiplier=2.0))]
|
||||
fn new(sma_period: usize, atr_period: usize, multiplier: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::StarcBands::new(sma_period, atr_period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.upper, o.middle, o.lower)))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<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))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== ATR Bands ==============================
|
||||
|
||||
#[pyclass(name = "AtrBands", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyAtrBands {
|
||||
inner: wc::AtrBands,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAtrBands {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14, multiplier=3.0))]
|
||||
fn new(period: usize, multiplier: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AtrBands::new(period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.upper, o.middle, o.lower)))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<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))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Hurst Channel ==============================
|
||||
|
||||
#[pyclass(name = "HurstChannel", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyHurstChannel {
|
||||
inner: wc::HurstChannel,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyHurstChannel {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=10, multiplier=0.5))]
|
||||
fn new(period: usize, multiplier: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HurstChannel::new(period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.upper, o.middle, o.lower)))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<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))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== LinReg Channel ==============================
|
||||
|
||||
#[pyclass(name = "LinRegChannel", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyLinRegChannel {
|
||||
inner: wc::LinRegChannel,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyLinRegChannel {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20, multiplier=2.0))]
|
||||
fn new(period: usize, multiplier: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::LinRegChannel::new(period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
|
||||
self.inner
|
||||
.update(value)
|
||||
.map(|o| (o.upper, o.middle, o.lower))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let n = slice.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in slice.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Standard Error Bands ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "StandardErrorBands",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyStandardErrorBands {
|
||||
inner: wc::StandardErrorBands,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyStandardErrorBands {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=21, multiplier=2.0))]
|
||||
fn new(period: usize, multiplier: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::StandardErrorBands::new(period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<(f64, f64, f64)> {
|
||||
self.inner
|
||||
.update(value)
|
||||
.map(|o| (o.upper, o.middle, o.lower))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let n = slice.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in slice.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Double Bollinger ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "DoubleBollinger",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyDoubleBollinger {
|
||||
inner: wc::DoubleBollinger,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyDoubleBollinger {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20, k_inner=1.0, k_outer=2.0))]
|
||||
fn new(period: usize, k_inner: f64, k_outer: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::DoubleBollinger::new(period, k_inner, k_outer).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `(upper_outer, upper_inner, middle, lower_inner, lower_outer)`.
|
||||
fn update(&mut self, value: f64) -> Option<(f64, f64, f64, f64, f64)> {
|
||||
self.inner.update(value).map(|o| {
|
||||
(
|
||||
o.upper_outer,
|
||||
o.upper_inner,
|
||||
o.middle,
|
||||
o.lower_inner,
|
||||
o.lower_outer,
|
||||
)
|
||||
})
|
||||
}
|
||||
/// Returns shape `(n, 5)` columns
|
||||
/// `[upper_outer, upper_inner, middle, lower_inner, lower_outer]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let n = slice.len();
|
||||
let mut out = vec![f64::NAN; n * 5];
|
||||
for (i, p) in slice.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 5] = o.upper_outer;
|
||||
out[i * 5 + 1] = o.upper_inner;
|
||||
out[i * 5 + 2] = o.middle;
|
||||
out[i * 5 + 3] = o.lower_inner;
|
||||
out[i * 5 + 4] = o.lower_outer;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 5), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TTM Squeeze ==============================
|
||||
|
||||
#[pyclass(name = "TtmSqueeze", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTtmSqueeze {
|
||||
inner: wc::TtmSqueeze,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTtmSqueeze {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20, bb_mult=2.0, kc_mult=1.5))]
|
||||
fn new(period: usize, bb_mult: f64, kc_mult: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TtmSqueeze::new(period, bb_mult, kc_mult).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `(squeeze, momentum)` or `None` during warmup. `squeeze` is
|
||||
/// `1.0` while BB ⊂ KC, `0.0` otherwise.
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.squeeze, o.momentum)))
|
||||
}
|
||||
/// Returns shape `(n, 2)` columns `[squeeze, momentum]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<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))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.squeeze;
|
||||
out[i * 2 + 1] = o.momentum;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Fractal Chaos Bands ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "FractalChaosBands",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyFractalChaosBands {
|
||||
inner: wc::FractalChaosBands,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyFractalChaosBands {
|
||||
#[new]
|
||||
#[pyo3(signature = (k=2))]
|
||||
fn new(k: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::FractalChaosBands::new(k).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `(upper, lower)` or `None` until both fractals have confirmed.
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.upper, o.lower)))
|
||||
}
|
||||
/// Returns shape `(n, 2)` columns `[upper, lower]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<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 n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(l[i], h[i], l[i], l[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.upper;
|
||||
out[i * 2 + 1] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== VWAP StdDev Bands ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "VwapStdDevBands",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyVwapStdDevBands {
|
||||
inner: wc::VwapStdDevBands,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyVwapStdDevBands {
|
||||
#[new]
|
||||
#[pyo3(signature = (multiplier=2.0))]
|
||||
fn new(multiplier: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VwapStdDevBands::new(multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `(upper, middle, lower, stddev)` or `None` until volume is non-zero.
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(c)
|
||||
.map(|o| (o.upper, o.middle, o.lower, o.stddev)))
|
||||
}
|
||||
/// Returns shape `(n, 4)` columns `[upper, middle, lower, stddev]`.
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<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))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let v = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() || c.len() != v.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close, volume must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], v[i], 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 4] = o.upper;
|
||||
out[i * 4 + 1] = o.middle;
|
||||
out[i * 4 + 2] = o.lower;
|
||||
out[i * 4 + 3] = o.stddev;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 4), out)
|
||||
.expect("shape consistent")
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Module ==============================
|
||||
|
||||
#[pymodule]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
|
||||
m.add_class::<PySma>()?;
|
||||
@@ -6051,5 +6778,16 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyGarmanKlassVolatility>()?;
|
||||
m.add_class::<PyRogersSatchellVolatility>()?;
|
||||
m.add_class::<PyYangZhangVolatility>()?;
|
||||
m.add_class::<PyMaEnvelope>()?;
|
||||
m.add_class::<PyAccelerationBands>()?;
|
||||
m.add_class::<PyStarcBands>()?;
|
||||
m.add_class::<PyAtrBands>()?;
|
||||
m.add_class::<PyHurstChannel>()?;
|
||||
m.add_class::<PyLinRegChannel>()?;
|
||||
m.add_class::<PyStandardErrorBands>()?;
|
||||
m.add_class::<PyDoubleBollinger>()?;
|
||||
m.add_class::<PyTtmSqueeze>()?;
|
||||
m.add_class::<PyFractalChaosBands>()?;
|
||||
m.add_class::<PyVwapStdDevBands>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -78,6 +78,16 @@ SCALAR = [
|
||||
]
|
||||
|
||||
|
||||
# Family 05 band/channel indicators with scalar input and multi-output.
|
||||
# `cols` is the expected number of band columns from `batch`.
|
||||
SCALAR_MULTI = {
|
||||
"MaEnvelope": (lambda: ta.MaEnvelope(20, 0.025), 3),
|
||||
"LinRegChannel": (lambda: ta.LinRegChannel(20, 2.0), 3),
|
||||
"StandardErrorBands": (lambda: ta.StandardErrorBands(21, 2.0), 3),
|
||||
"DoubleBollinger": (lambda: ta.DoubleBollinger(20, 1.0, 2.0), 5),
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
@@ -247,6 +257,26 @@ MULTI = {
|
||||
lambda: ta.ChandeKrollStop(10, 1.0, 9),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
# Family 05 candle-input bands. Each entry is
|
||||
# `(factory, batch_call, output_arity, streaming_fields)` where
|
||||
# `streaming_fields` is the tuple shape returned by `update(...)`.
|
||||
"TtmSqueeze": (
|
||||
lambda: ta.TtmSqueeze(20, 2.0, 1.5),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"FractalChaosBands": (
|
||||
lambda: ta.FractalChaosBands(2),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# Bands with 3 outputs upper/middle/lower from a candle (h, l, c).
|
||||
HLC_BAND3 = {
|
||||
"AccelerationBands": lambda: ta.AccelerationBands(20, 0.001),
|
||||
"StarcBands": lambda: ta.StarcBands(6, 15, 2.0),
|
||||
"AtrBands": lambda: ta.AtrBands(14, 3.0),
|
||||
"HurstChannel": lambda: ta.HurstChannel(10, 0.5),
|
||||
}
|
||||
|
||||
# --- Scalar-input, multi-output indicators --------------------------------
|
||||
@@ -286,6 +316,73 @@ def test_multi_streaming_matches_batch(name, ohlcv):
|
||||
assert _eq_nan(batch, np.array(rows, dtype=np.float64)), f"{name} mismatch"
|
||||
|
||||
|
||||
# --- Family 05: scalar-input multi-output band/channel indicators ----------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(SCALAR_MULTI))
|
||||
def test_scalar_multi_streaming_matches_batch(name, sine_prices):
|
||||
make, cols = SCALAR_MULTI[name]
|
||||
batch = make().batch(sine_prices)
|
||||
assert batch.shape == (sine_prices.size, cols)
|
||||
|
||||
streamer = make()
|
||||
rows = []
|
||||
for p in sine_prices:
|
||||
v = streamer.update(float(p))
|
||||
rows.append([math.nan] * cols if v is None else list(v))
|
||||
assert _eq_nan(batch, np.array(rows, dtype=np.float64)), f"{name} mismatch"
|
||||
|
||||
|
||||
# --- Family 05: 3-band candle-input indicators ------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(HLC_BAND3))
|
||||
def test_hlc_band3_streaming_matches_batch(name, ohlcv):
|
||||
high, low, close, _ = ohlcv
|
||||
make = HLC_BAND3[name]
|
||||
batch = make().batch(high, low, close)
|
||||
assert batch.shape == (close.size, 3)
|
||||
|
||||
streamer = make()
|
||||
rows = []
|
||||
for i in range(close.size):
|
||||
candle = (
|
||||
float(close[i]),
|
||||
float(high[i]),
|
||||
float(low[i]),
|
||||
float(close[i]),
|
||||
1.0,
|
||||
i,
|
||||
)
|
||||
v = streamer.update(candle)
|
||||
rows.append([math.nan] * 3 if v is None else list(v))
|
||||
assert _eq_nan(batch, np.array(rows, dtype=np.float64)), f"{name} mismatch"
|
||||
|
||||
|
||||
# --- VWAP StdDev Bands (4 outputs, needs volume) ----------------------------
|
||||
|
||||
|
||||
def test_vwap_stddev_bands_streaming_matches_batch(ohlcv):
|
||||
high, low, close, volume = ohlcv
|
||||
batch = ta.VwapStdDevBands(2.0).batch(high, low, close, volume)
|
||||
assert batch.shape == (close.size, 4)
|
||||
|
||||
streamer = ta.VwapStdDevBands(2.0)
|
||||
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] * 4 if v is None else list(v))
|
||||
assert _eq_nan(batch, np.array(rows, dtype=np.float64))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(MULTI_SCALAR_INPUT))
|
||||
def test_multi_scalar_streaming_matches_batch(name, ohlcv):
|
||||
_, _, close, _ = ohlcv
|
||||
@@ -399,6 +496,108 @@ def test_z_score_reference():
|
||||
assert out[1] == pytest.approx(1.0)
|
||||
|
||||
|
||||
# --- Family 05 reference values ---------------------------------------------
|
||||
|
||||
|
||||
def test_ma_envelope_reference():
|
||||
# SMA([10, 20, 30]) = 20; with percent = 0.10: upper = 22, lower = 18.
|
||||
out = ta.MaEnvelope(3, 0.10).batch(np.array([10.0, 20.0, 30.0]))
|
||||
assert math.isnan(out[0, 0]) and math.isnan(out[1, 0])
|
||||
assert out[2, 0] == pytest.approx(22.0) # upper
|
||||
assert out[2, 1] == pytest.approx(20.0) # middle
|
||||
assert out[2, 2] == pytest.approx(18.0) # lower
|
||||
|
||||
|
||||
def test_acceleration_bands_reference():
|
||||
# Single bar: high=12, low=8, close=10, factor=0.5, period=1.
|
||||
# ratio = 4/20 = 0.2; raw_up = 12·1.1 = 13.2; raw_lo = 8·0.9 = 7.2.
|
||||
v = ta.AccelerationBands(1, 0.5).update((10.0, 12.0, 8.0, 10.0, 1.0, 0))
|
||||
assert v == pytest.approx((13.2, 10.0, 7.2))
|
||||
|
||||
|
||||
def test_atr_bands_reference():
|
||||
# Five identical bars (h=11, l=9, c=10) → ATR=2, close=10, mult=3:
|
||||
# upper=16, middle=10, lower=4.
|
||||
out = ta.AtrBands(5, 3.0).batch(
|
||||
np.array([11.0] * 5), np.array([9.0] * 5), np.array([10.0] * 5)
|
||||
)
|
||||
assert math.isnan(out[3, 0])
|
||||
assert out[4, 0] == pytest.approx(16.0)
|
||||
assert out[4, 1] == pytest.approx(10.0)
|
||||
assert out[4, 2] == pytest.approx(4.0)
|
||||
|
||||
|
||||
def test_hurst_channel_reference():
|
||||
# Five identical (h=12, l=8, c=10): SMA(close)=10, range=4, mult=0.5.
|
||||
out = ta.HurstChannel(5, 0.5).batch(
|
||||
np.array([12.0] * 5), np.array([8.0] * 5), np.array([10.0] * 5)
|
||||
)
|
||||
assert out[4, 0] == pytest.approx(12.0)
|
||||
assert out[4, 1] == pytest.approx(10.0)
|
||||
assert out[4, 2] == pytest.approx(8.0)
|
||||
|
||||
|
||||
def test_linreg_channel_reference():
|
||||
# period 3 over [1, 2, 9]: line y=4x, endpoint=8, residuals=[1, -2, 1],
|
||||
# population sigma=sqrt(2); mult=2 → upper=8+2√2, lower=8-2√2.
|
||||
out = ta.LinRegChannel(3, 2.0).batch(np.array([1.0, 2.0, 9.0]))
|
||||
s = math.sqrt(2.0)
|
||||
assert out[2, 0] == pytest.approx(8.0 + 2.0 * s)
|
||||
assert out[2, 1] == pytest.approx(8.0)
|
||||
assert out[2, 2] == pytest.approx(8.0 - 2.0 * s)
|
||||
|
||||
|
||||
def test_standard_error_bands_reference():
|
||||
# Same [1, 2, 9] with n=3: SSE=6, n-2=1, stderr=sqrt(6); mult=2 →
|
||||
# upper=8+2√6, lower=8-2√6.
|
||||
out = ta.StandardErrorBands(3, 2.0).batch(np.array([1.0, 2.0, 9.0]))
|
||||
s = math.sqrt(6.0)
|
||||
assert out[2, 0] == pytest.approx(8.0 + 2.0 * s)
|
||||
assert out[2, 1] == pytest.approx(8.0)
|
||||
assert out[2, 2] == pytest.approx(8.0 - 2.0 * s)
|
||||
|
||||
|
||||
def test_double_bollinger_orders_bands():
|
||||
# On a non-trivial dispersion, outer >= inner >= middle >= -inner >= -outer.
|
||||
out = ta.DoubleBollinger(5, 1.0, 2.0).batch(
|
||||
np.array([1.0, 5.0, 2.0, 4.0, 3.0, 6.0])
|
||||
)
|
||||
v = out[5]
|
||||
assert v[0] >= v[1] >= v[2] >= v[3] >= v[4]
|
||||
|
||||
|
||||
def test_vwap_stddev_bands_reference():
|
||||
# Two equal-volume bars with tp=8, tp=12: vwap=10, σ=2, mult=1.5 →
|
||||
# upper=13, lower=7.
|
||||
v = ta.VwapStdDevBands(1.5)
|
||||
v.update((8.0, 8.0, 8.0, 8.0, 1.0, 0))
|
||||
out = v.update((12.0, 12.0, 12.0, 12.0, 1.0, 1))
|
||||
assert out[0] == pytest.approx(13.0)
|
||||
assert out[1] == pytest.approx(10.0)
|
||||
assert out[2] == pytest.approx(7.0)
|
||||
assert out[3] == pytest.approx(2.0)
|
||||
|
||||
|
||||
def test_ttm_squeeze_flat_market():
|
||||
# Zero volatility: BB and KC both collapse to a point → squeeze=1.0,
|
||||
# momentum=0.0.
|
||||
candles_h = np.array([10.0] * 25)
|
||||
out = ta.TtmSqueeze(20, 2.0, 1.5).batch(candles_h, candles_h, candles_h)
|
||||
assert out[24, 0] == pytest.approx(1.0)
|
||||
assert out[24, 1] == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_fractal_chaos_bands_detects_peak_and_trough():
|
||||
# Sequence that creates one fractal high (i=2) and one low (i=3).
|
||||
h = np.array([1.0, 2.0, 5.0, 3.0, 2.0, 1.0, 2.0])
|
||||
l = np.array([1.0, 2.0, 3.0, 0.5, 2.0, 1.0, 2.0])
|
||||
out = ta.FractalChaosBands(2).batch(h, l)
|
||||
# First bar with both bands set is index 5.
|
||||
assert math.isnan(out[4, 0])
|
||||
assert out[5, 0] == pytest.approx(5.0)
|
||||
assert out[5, 1] == pytest.approx(0.5)
|
||||
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -407,6 +606,9 @@ def test_new_indicators_expose_lifecycle():
|
||||
instances += [make() for make, _ in MULTI.values()]
|
||||
instances += [make() for make, _ in MULTI_SCALAR_INPUT.values()]
|
||||
instances += [cls(*args) for cls, args in SCALAR]
|
||||
instances += [make() for make, _ in SCALAR_MULTI.values()]
|
||||
instances += [make() for make in HLC_BAND3.values()]
|
||||
instances += [ta.VwapStdDevBands(2.0)]
|
||||
instances.append(ta.Alligator(13, 8, 5))
|
||||
instances.append(ta.ZeroLagMACD(12, 26, 9))
|
||||
for ind in instances:
|
||||
|
||||
@@ -2686,6 +2686,672 @@ impl WasmAroon {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Family 05: Bands & Channels ==============================
|
||||
|
||||
// Every indicator below is multi-output (2-5 bands), so they bypass the
|
||||
// `wasm_scalar_indicator!` macro and follow the hand-rolled pattern used by
|
||||
// Bollinger Bands, Keltner Channels, etc. above: `update` returns a JS object
|
||||
// via `Object::new` + `Reflect::set`; `batch` returns a flat interleaved
|
||||
// `Float64Array`.
|
||||
|
||||
// ---------- MA Envelope (scalar input, 3 outputs) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = MaEnvelope)]
|
||||
pub struct WasmMaEnvelope {
|
||||
inner: wc::MaEnvelope,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = MaEnvelope)]
|
||||
impl WasmMaEnvelope {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, percent: f64) -> Result<WasmMaEnvelope, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::MaEnvelope::new(period, percent).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> JsValue {
|
||||
match self.inner.update(value) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
/// Flat `[upper0, middle0, lower0, upper1, ...]`, length `3 * n`.
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let n = prices.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Acceleration Bands ----------
|
||||
|
||||
#[wasm_bindgen(js_name = AccelerationBands)]
|
||||
pub struct WasmAccelerationBands {
|
||||
inner: wc::AccelerationBands,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = AccelerationBands)]
|
||||
impl WasmAccelerationBands {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, factor: f64) -> Result<WasmAccelerationBands, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::AccelerationBands::new(period, factor).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Returns `[u0, m0, l0, u1, m1, l1, ...]`, length `3 * n`.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- STARC Bands ----------
|
||||
|
||||
#[wasm_bindgen(js_name = StarcBands)]
|
||||
pub struct WasmStarcBands {
|
||||
inner: wc::StarcBands,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = StarcBands)]
|
||||
impl WasmStarcBands {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
sma_period: usize,
|
||||
atr_period: usize,
|
||||
multiplier: f64,
|
||||
) -> Result<WasmStarcBands, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::StarcBands::new(sma_period, atr_period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- ATR Bands ----------
|
||||
|
||||
#[wasm_bindgen(js_name = AtrBands)]
|
||||
pub struct WasmAtrBands {
|
||||
inner: wc::AtrBands,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = AtrBands)]
|
||||
impl WasmAtrBands {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, multiplier: f64) -> Result<WasmAtrBands, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::AtrBands::new(period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Hurst Channel ----------
|
||||
|
||||
#[wasm_bindgen(js_name = HurstChannel)]
|
||||
pub struct WasmHurstChannel {
|
||||
inner: wc::HurstChannel,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = HurstChannel)]
|
||||
impl WasmHurstChannel {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, multiplier: f64) -> Result<WasmHurstChannel, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::HurstChannel::new(period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- LinReg Channel (scalar input) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = LinRegChannel)]
|
||||
pub struct WasmLinRegChannel {
|
||||
inner: wc::LinRegChannel,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = LinRegChannel)]
|
||||
impl WasmLinRegChannel {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, multiplier: f64) -> Result<WasmLinRegChannel, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::LinRegChannel::new(period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> JsValue {
|
||||
match self.inner.update(value) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let n = prices.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Standard Error Bands (scalar input) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = StandardErrorBands)]
|
||||
pub struct WasmStandardErrorBands {
|
||||
inner: wc::StandardErrorBands,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = StandardErrorBands)]
|
||||
impl WasmStandardErrorBands {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, multiplier: f64) -> Result<WasmStandardErrorBands, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::StandardErrorBands::new(period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> JsValue {
|
||||
match self.inner.update(value) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let n = prices.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 3] = o.upper;
|
||||
out[i * 3 + 1] = o.middle;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Double Bollinger (scalar input, 5 outputs) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = DoubleBollinger)]
|
||||
pub struct WasmDoubleBollinger {
|
||||
inner: wc::DoubleBollinger,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = DoubleBollinger)]
|
||||
impl WasmDoubleBollinger {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, k_inner: f64, k_outer: f64) -> Result<WasmDoubleBollinger, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::DoubleBollinger::new(period, k_inner, k_outer).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> JsValue {
|
||||
match self.inner.update(value) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upperOuter".into(), &o.upper_outer.into()).ok();
|
||||
Reflect::set(&obj, &"upperInner".into(), &o.upper_inner.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lowerInner".into(), &o.lower_inner.into()).ok();
|
||||
Reflect::set(&obj, &"lowerOuter".into(), &o.lower_outer.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
/// Flat `[u_o, u_i, m, l_i, l_o, ...]`, length `5 * n`.
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let n = prices.len();
|
||||
let mut out = vec![f64::NAN; n * 5];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 5] = o.upper_outer;
|
||||
out[i * 5 + 1] = o.upper_inner;
|
||||
out[i * 5 + 2] = o.middle;
|
||||
out[i * 5 + 3] = o.lower_inner;
|
||||
out[i * 5 + 4] = o.lower_outer;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- TTM Squeeze ----------
|
||||
|
||||
#[wasm_bindgen(js_name = TtmSqueeze)]
|
||||
pub struct WasmTtmSqueeze {
|
||||
inner: wc::TtmSqueeze,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = TtmSqueeze)]
|
||||
impl WasmTtmSqueeze {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, bb_mult: f64, kc_mult: f64) -> Result<WasmTtmSqueeze, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::TtmSqueeze::new(period, bb_mult, kc_mult).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"squeeze".into(), &o.squeeze.into()).ok();
|
||||
Reflect::set(&obj, &"momentum".into(), &o.momentum.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Flat `[sq0, mom0, sq1, mom1, ...]`, length `2 * n`.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.squeeze;
|
||||
out[i * 2 + 1] = o.momentum;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Fractal Chaos Bands ----------
|
||||
|
||||
#[wasm_bindgen(js_name = FractalChaosBands)]
|
||||
pub struct WasmFractalChaosBands {
|
||||
inner: wc::FractalChaosBands,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = FractalChaosBands)]
|
||||
impl WasmFractalChaosBands {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(k: usize) -> Result<WasmFractalChaosBands, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::FractalChaosBands::new(k).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, low, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Flat `[u0, l0, u1, l1, ...]`, length `2 * n`.
|
||||
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n {
|
||||
return Err(JsError::new("high and low must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], low[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.upper;
|
||||
out[i * 2 + 1] = o.lower;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- VWAP StdDev Bands ----------
|
||||
|
||||
#[wasm_bindgen(js_name = VwapStdDevBands)]
|
||||
pub struct WasmVwapStdDevBands {
|
||||
inner: wc::VwapStdDevBands,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = VwapStdDevBands)]
|
||||
impl WasmVwapStdDevBands {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(multiplier: f64) -> Result<WasmVwapStdDevBands, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::VwapStdDevBands::new(multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, volume)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
Reflect::set(&obj, &"stddev".into(), &o.stddev.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Flat `[u0, m0, l0, sd0, ...]`, length `4 * n`.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
volume: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n || volume.len() != n {
|
||||
return Err(JsError::new(
|
||||
"high, low, close, volume must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], volume[i])?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 4] = o.upper;
|
||||
out[i * 4 + 1] = o.middle;
|
||||
out[i * 4 + 2] = o.lower;
|
||||
out[i * 4 + 3] = o.stddev;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user