B5 volatility & bands batch (423 -> 429) (#189)
Adds six **Volatility & Bands** indicators (Part B5 of the expansion roadmap), 423 → 429. | Indicator | Input → Output | Summary | |-----------|----------------|---------| | `EwmaVolatility` | `f64` → `f64` | RiskMetrics exponentially-weighted volatility (λ decay) | | `Garch11` | `f64` → `f64` | GARCH(1,1) conditional volatility with a long-run-variance anchor | | `BipowerVariation` | `f64` → `f64` | jump-robust realized bipower variation (π/2 · Σ\|rₜ\|\|rₜ₋₁\|) | | `VolatilityRatio` | `Candle` → `f64` | Schwager's true range over the EMA of prior true ranges (>2 = wide-ranging day) | | `VolatilityCone` | `Candle` → `VolatilityConeOutput` | current realized volatility within its min/median/max envelope + percentile | | `VolatilityOfVolatility` | `f64` → `f64` | sample stddev of a rolling realized-volatility series | ### Notes - Two B5 roadmap items were dropped as duplicates/by-construction: `RealizedVolatility` already ships (v0.5.4); `Downside Semi-Deviation` is internal to Sortino. `Bipower Variation` confirmed distinct from `JumpIndicator` (a ±1 flag, not a variance measure). - `VolatilityRatio` implements the widely-charted EMA-of-true-range convention (denominator excludes the current bar so the 2.0 threshold means "twice typical"), distinct from the existing pairwise `variance_ratio`. - `Garch11` mean-reverts to `ω/(1−β)` on a flat series (does not decay to 0 like EWMA) — pinned by a dedicated test. ### Coverage / verification - Full core + Python/Node/WASM bindings, fuzz drivers (scalar + candle), registries, CHANGELOG, README + docs counter sync. - 100% unit-test coverage per indicator (every branch). - Green locally: `cargo clippy --workspace --all-targets --all-features -D warnings`, core lib (3479) + doc (387), node (504), python (830). Deep-dive docs for all six are staged for `wickra-docs` and pushed after release (gated).
This commit is contained in:
@@ -28,6 +28,10 @@ function num(v) {
|
||||
// --- Scalar indicators: update(value) vs batch(prices) ---
|
||||
|
||||
const scalarFactories = {
|
||||
BipowerVariation: () => new wickra.BipowerVariation(20),
|
||||
VolatilityOfVolatility: () => new wickra.VolatilityOfVolatility(20, 20),
|
||||
Garch11: () => new wickra.Garch11(0.000002, 0.1, 0.88),
|
||||
EwmaVolatility: () => new wickra.EwmaVolatility(0.94),
|
||||
PpoHistogram: () => new wickra.PpoHistogram(3, 6, 3),
|
||||
MacdHistogram: () => new wickra.MacdHistogram(3, 6, 3),
|
||||
TsfOscillator: () => new wickra.TsfOscillator(3),
|
||||
@@ -350,6 +354,7 @@ const candleScalar = {
|
||||
IMI: { make: () => new wickra.IMI(14), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
TTM_TREND: { make: () => new wickra.TTM_TREND(6), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
Qstick: { make: () => new wickra.Qstick(10), step: (ind, i) => ind.update(open[i], close[i]), batch: (ind) => ind.batch(open, close) },
|
||||
VolatilityRatio: { make: () => new wickra.VolatilityRatio(14), 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)) {
|
||||
@@ -436,6 +441,7 @@ const multi = {
|
||||
QQE: { make: () => new wickra.QQE(14, 5, 4.236), fields: ['rsiMa', 'trailingLine'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
GatorOscillator: { make: () => new wickra.GatorOscillator(13, 8, 5), fields: ['upper', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
KasePermissionStochastic: { make: () => new wickra.KasePermissionStochastic(9, 3), fields: ['fast', 'slow'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
VolatilityCone: { make: () => new wickra.VolatilityCone(20, 60), fields: ['current', 'min', 'median', 'max', 'percentile'], 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)) {
|
||||
|
||||
Vendored
+65
@@ -5,6 +5,17 @@
|
||||
|
||||
/** Library version (matches the Rust crate version). */
|
||||
export declare function version(): string
|
||||
/**
|
||||
* Volatility-cone result: current realized volatility and its lookback
|
||||
* envelope (min / median / max) plus the percentile rank of `current`.
|
||||
*/
|
||||
export interface VolatilityConeValue {
|
||||
current: number
|
||||
min: number
|
||||
median: number
|
||||
max: number
|
||||
percentile: number
|
||||
}
|
||||
/** Lead/lag result: the offset that maximises correlation, and that correlation. */
|
||||
export interface LeadLagValue {
|
||||
/** Offset that maximises `|corr(a, b shifted)|`. Positive ⇒ `a` leads `b`. */
|
||||
@@ -987,6 +998,51 @@ export declare class TsfOscillator {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type BipowerVariationNode = BipowerVariation
|
||||
export declare class BipowerVariation {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type EwmaVolatilityNode = EwmaVolatility
|
||||
export declare class EwmaVolatility {
|
||||
constructor(lambda: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type Garch11Node = Garch11
|
||||
export declare class Garch11 {
|
||||
constructor(omega: number, alpha: number, beta: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type VolatilityOfVolatilityNode = VolatilityOfVolatility
|
||||
export declare class VolatilityOfVolatility {
|
||||
constructor(volWindow: number, vovWindow: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type VolatilityConeNode = VolatilityCone
|
||||
export declare class VolatilityCone {
|
||||
constructor(window: number, lookback: number)
|
||||
update(high: number, low: number, close: number): VolatilityConeValue | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type JumpIndicatorNode = JumpIndicator
|
||||
export declare class JumpIndicator {
|
||||
constructor(period: number, threshold: number)
|
||||
@@ -1574,6 +1630,15 @@ export declare class KasePermissionStochastic {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type VolatilityRatioNode = VolatilityRatio
|
||||
export declare class VolatilityRatio {
|
||||
constructor(period: number)
|
||||
update(high: number, low: number, close: number): number | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type StochNode = Stochastic
|
||||
export declare class Stochastic {
|
||||
constructor(kPeriod: number, dPeriod: number)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -220,6 +220,199 @@ node_scalar_indicator!(
|
||||
wc::TrendStrengthIndex
|
||||
);
|
||||
node_scalar_indicator!(TsfOscillatorNode, "TsfOscillator", wc::TsfOscillator);
|
||||
node_scalar_indicator!(
|
||||
BipowerVariationNode,
|
||||
"BipowerVariation",
|
||||
wc::BipowerVariation
|
||||
);
|
||||
|
||||
#[napi(js_name = "EwmaVolatility")]
|
||||
pub struct EwmaVolatilityNode {
|
||||
inner: wc::EwmaVolatility,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl EwmaVolatilityNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(lambda: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::EwmaVolatility::new(lambda).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "Garch11")]
|
||||
pub struct Garch11Node {
|
||||
inner: wc::Garch11,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Garch11Node {
|
||||
#[napi(constructor)]
|
||||
pub fn new(omega: f64, alpha: f64, beta: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Garch11::new(omega, alpha, beta).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "VolatilityOfVolatility")]
|
||||
pub struct VolatilityOfVolatilityNode {
|
||||
inner: wc::VolatilityOfVolatility,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl VolatilityOfVolatilityNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(vol_window: u32, vov_window: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityOfVolatility::new(vol_window as usize, vov_window as usize)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Volatility-cone result: current realized volatility and its lookback
|
||||
/// envelope (min / median / max) plus the percentile rank of `current`.
|
||||
#[napi(object)]
|
||||
pub struct VolatilityConeValue {
|
||||
pub current: f64,
|
||||
pub min: f64,
|
||||
pub median: f64,
|
||||
pub max: f64,
|
||||
pub percentile: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "VolatilityCone")]
|
||||
pub struct VolatilityConeNode {
|
||||
inner: wc::VolatilityCone,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl VolatilityConeNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(window: u32, lookback: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityCone::new(window as usize, lookback as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<VolatilityConeValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| VolatilityConeValue {
|
||||
current: o.current,
|
||||
min: o.min,
|
||||
median: o.median,
|
||||
max: o.max,
|
||||
percentile: o.percentile,
|
||||
}))
|
||||
}
|
||||
#[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 * 5];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
out[i * 5] = o.current;
|
||||
out[i * 5 + 1] = o.min;
|
||||
out[i * 5 + 2] = o.median;
|
||||
out[i * 5 + 3] = o.max;
|
||||
out[i * 5 + 4] = o.percentile;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "JumpIndicator")]
|
||||
pub struct JumpIndicatorNode {
|
||||
inner: wc::JumpIndicator,
|
||||
@@ -2623,6 +2816,59 @@ impl KasePermissionStochasticNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "VolatilityRatio")]
|
||||
pub struct VolatilityRatioNode {
|
||||
inner: wc::VolatilityRatio,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl VolatilityRatioNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolatilityRatio::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
|
||||
}
|
||||
#[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 mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(high[i], low[i], close[i], 0.0)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct StochValue {
|
||||
pub k: f64,
|
||||
|
||||
Reference in New Issue
Block a user