feat: add 19 indicators for external feature-extractor coverage (377 -> 396) (#175)

Adds 19 streaming indicators so an external trading-bot feature extractor can replace its hand-built features with native, batch/streaming-equivalent ones. Each is a real gap (verified against the existing catalogue), production-only, with full Python/Node/WASM bindings, fuzz drivers, and tests. Five commits, one per family group; counter 377 -> 396.

## What's added

**Price Statistics (6)** — `LogReturn`, `RealizedVolatility` (raw quadratic variation, the un-annualised counterpart to `HistoricalVolatility`), `RollingQuantile`, `RollingIqr`, `RollingPercentileRank`, `SpreadAr1Coefficient` (pairwise AR(1) rho of the spread; complements `OuHalfLife`).

**Price Action (4)** — `CloseVsOpen`, `BodySizePct`, `WickRatio`, `HighLowRange` (stateless per-bar OHLC transforms).

**Regime / Trend / Jump labels (3)** — `TrendLabel` (sign of the rolling OLS slope), `JumpIndicator` (return outliers vs trailing volatility, measured as deviation from the trailing mean so steady drift is not flagged), `RegimeLabel` (volatility-quantile regime split).

**Risk / Performance (2)** — `WinRate`, `Expectancy` (R-multiple).

**Microstructure (4)** — `OrderFlowImbalance` (Cont-Kukanov-Stoikov OFI), `Vpin`, `AmihudIlliquidity`, `RollMeasure`. These reuse the existing `OrderBook` / `Trade` inputs (no new input type).

## Intentionally NOT added (already present, would be duplicates)

- **Population skew / kurtosis** — `skewness.rs` / `kurtosis.rs` are already population moments (divisor n).
- **Hurst R/S** — `hurst_exponent.rs` already uses rescaled-range (R/S) analysis.
- **Queue Imbalance** — exactly `OrderBookImbalanceTop1` ((bidSize - askSize) / (bidSize + askSize)).

## Verification

`cargo test -p wickra-core` (lib 3187 + doc 354), `cargo clippy --workspace --all-targets --all-features -D warnings` clean, node `npm run build && npm test` (471), python `pytest` (784). Counter consistent across `mod.rs`, lib block, README, and docs/README at 396.
This commit is contained in:
kingchenc
2026-06-04 12:00:35 +02:00
committed by GitHub
parent a93af60796
commit fcb221ec03
37 changed files with 6697 additions and 84 deletions
@@ -28,6 +28,16 @@ function num(v) {
// --- Scalar indicators: update(value) vs batch(prices) ---
const scalarFactories = {
Expectancy: () => new wickra.Expectancy(20),
WinRate: () => new wickra.WinRate(20),
RegimeLabel: () => new wickra.RegimeLabel(5, 20),
JumpIndicator: () => new wickra.JumpIndicator(20, 3.0),
TrendLabel: () => new wickra.TrendLabel(10),
RollingQuantile: () => new wickra.RollingQuantile(20, 0.5),
RollingPercentileRank: () => new wickra.RollingPercentileRank(14),
RollingIqr: () => new wickra.RollingIqr(14),
RealizedVolatility: () => new wickra.RealizedVolatility(20),
LogReturn: () => new wickra.LogReturn(1),
TSF: () => new wickra.TSF(14),
LINEARREG_INTERCEPT: () => new wickra.LINEARREG_INTERCEPT(14),
ROCR100: () => new wickra.ROCR100(10),
@@ -313,6 +323,10 @@ const candleScalar = {
Shark: { make: () => new wickra.Shark(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
Cypher: { make: () => new wickra.Cypher(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
ThreeDrives: { make: () => new wickra.ThreeDrives(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
CloseVsOpen: { make: () => new wickra.CloseVsOpen(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
BodySizePct: { make: () => new wickra.BodySizePct(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
WickRatio: { make: () => new wickra.WickRatio(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
HighLowRange: { make: () => new wickra.HighLowRange(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
};
for (const [name, d] of Object.entries(candleScalar)) {
@@ -563,6 +577,7 @@ const pairFactories = {
BetaNeutralSpread: () => new wickra.BetaNeutralSpread(20),
VarianceRatio: () => new wickra.VarianceRatio(60, 2),
GrangerCausality: () => new wickra.GrangerCausality(60, 1),
SpreadAr1Coefficient: () => new wickra.SpreadAr1Coefficient(40),
};
for (const [name, make] of Object.entries(pairFactories)) {
@@ -1126,6 +1141,57 @@ test('trade-flow rejects bad input', () => {
assert.throws(() => new wickra.SignedVolume().update(100, -1, true));
});
test('order-flow imbalance reference + streaming matches batch', () => {
// Rising bid (px up, size 6) with an unchanged ask -> +6 flow.
const ofi = new wickra.OrderFlowImbalance(1);
assert.equal(ofi.update([100], [5], [101], [4]), null); // seeds the reference
assert.ok(Math.abs(ofi.update([100.5], [6], [101], [4]) - 6.0) < 1e-12);
const snaps = Array.from({ length: 30 }, (_, i) => ({
bidPx: [100 + Math.sin(i * 0.3)],
bidSz: [5 + Math.abs(Math.cos(i * 0.5))],
askPx: [101 + Math.sin(i * 0.3)],
askSz: [4 + Math.abs(Math.sin(i * 0.4))],
}));
const batch = new wickra.OrderFlowImbalance(10).batch(snaps);
const streamer = new wickra.OrderFlowImbalance(10);
assert.equal(batch.length, snaps.length);
for (let i = 0; i < snaps.length; i++) {
const s = streamer.update(snaps[i].bidPx, snaps[i].bidSz, snaps[i].askPx, snaps[i].askSz);
assert.ok((Number.isNaN(batch[i]) && s === null) || Math.abs(s - batch[i]) < 1e-9, `mismatch at ${i}`);
}
});
test('vpin / amihud / roll reference + streaming matches batch', () => {
// VPIN: two pure-buy buckets of size 10 -> imbalance == size -> 1.
const v = new wickra.Vpin(10, 2);
let last;
for (let i = 0; i < 4; i++) last = v.update(100, 5, true);
assert.equal(last, 1.0);
// Amihud(1): |ln(101/100)| / (101 * 10).
const a = new wickra.AmihudIlliquidity(1);
assert.equal(a.update(100, 10, true), null);
assert.ok(Math.abs(a.update(101, 10, true) - Math.abs(Math.log(101 / 100)) / (101 * 10)) < 1e-15);
// Roll(6): a clean bid-ask bounce of ±1 implies a spread of 2.
const r = new wickra.RollMeasure(6);
let roll = null;
for (let i = 0; i < 20; i++) roll = r.update(i % 2 === 0 ? 100 : 101, 1, true);
assert.ok(Math.abs(roll - 2.0) < 1e-12);
// Streaming-vs-batch for the three trade-input indicators.
const n = 40;
const price = Array.from({ length: n }, (_, i) => 100 + Math.sin(i * 0.25) * 4);
const size = Array.from({ length: n }, (_, i) => 1 + (i % 5));
const isBuy = Array.from({ length: n }, (_, i) => i % 2 === 0);
for (const make of [() => new wickra.Vpin(8, 5), () => new wickra.AmihudIlliquidity(14), () => new wickra.RollMeasure(14)]) {
const batch = make().batch(price, size, isBuy);
const streamer = make();
assert.equal(batch.length, n);
for (let i = 0; i < n; i++) {
const s = streamer.update(price[i], size[i], isBuy[i]);
assert.ok((Number.isNaN(batch[i]) && s === null) || Math.abs(s - batch[i]) < 1e-9, `mismatch at ${i}`);
}
}
});
test('price-impact indicators reference values', () => {
// Buy at 100.05 vs mid 100.0: 2 * (100.05 - 100) / 100 * 10000 = 10 bps.
assert.ok(Math.abs(new wickra.EffectiveSpread().update(100.05, 1, true, 100.0) - 10.0) < 1e-9);
+175
View File
@@ -809,6 +809,96 @@ export declare class TSF {
isReady(): boolean
warmupPeriod(): number
}
export type LogReturnNode = LogReturn
export declare class LogReturn {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type RealizedVolatilityNode = RealizedVolatility
export declare class RealizedVolatility {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type RollingIqrNode = RollingIqr
export declare class RollingIqr {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type RollingPercentileRankNode = RollingPercentileRank
export declare class RollingPercentileRank {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TrendLabelNode = TrendLabel
export declare class TrendLabel {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type WinRateNode = WinRate
export declare class WinRate {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type ExpectancyNode = Expectancy
export declare class Expectancy {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type JumpIndicatorNode = JumpIndicator
export declare class JumpIndicator {
constructor(period: number, threshold: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type RegimeLabelNode = RegimeLabel
export declare class RegimeLabel {
constructor(volPeriod: number, lookback: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type RollingQuantileNode = RollingQuantile
export declare class RollingQuantile {
constructor(period: number, quantile: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type AutocorrelationNode = Autocorrelation
export declare class Autocorrelation {
constructor(period: number, lag: number)
@@ -866,6 +956,19 @@ export declare class PairwiseBeta {
isReady(): boolean
warmupPeriod(): number
}
export type SpreadAr1CoefficientNode = SpreadAr1Coefficient
export declare class SpreadAr1Coefficient {
constructor(period: number)
update(x: number, y: number): number | null
/**
* Batch over two equally-sized arrays. Returns a length-`n` array
* with `NaN` for warmup positions.
*/
batch(x: Array<number>, y: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type SpearmanCorrelationNode = SpearmanCorrelation
export declare class SpearmanCorrelation {
constructor(period: number)
@@ -1230,6 +1333,42 @@ export declare class HT_PHASOR {
isReady(): boolean
warmupPeriod(): number
}
export type CloseVsOpenNode = CloseVsOpen
export declare class CloseVsOpen {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type BodySizePctNode = BodySizePct
export declare class BodySizePct {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type WickRatioNode = WickRatio
export declare class WickRatio {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type HighLowRangeNode = HighLowRange
export declare class HighLowRange {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, 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)
@@ -3317,6 +3456,42 @@ export declare class TradeImbalance {
isReady(): boolean
warmupPeriod(): number
}
export type OrderFlowImbalanceNode = OrderFlowImbalance
export declare class OrderFlowImbalance {
constructor(period: number)
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
batch(snapshots: Array<ObSnapshot>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type VpinNode = Vpin
export declare class Vpin {
constructor(bucketVolume: number, numBuckets: number)
update(price: number, size: number, isBuy: boolean): number | null
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type AmihudIlliquidityNode = AmihudIlliquidity
export declare class AmihudIlliquidity {
constructor(period: number)
update(price: number, size: number, isBuy: boolean): number | null
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type RollMeasureNode = RollMeasure
export declare class RollMeasure {
constructor(period: number)
update(price: number, size: number, isBuy: boolean): number | null
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type EffectiveSpreadNode = EffectiveSpread
export declare class EffectiveSpread {
constructor()
+20 -1
View File
File diff suppressed because one or more lines are too long
+585
View File
@@ -182,6 +182,125 @@ node_scalar_indicator!(
wc::LinRegIntercept
);
node_scalar_indicator!(TsfNode, "TSF", wc::Tsf);
node_scalar_indicator!(LogReturnNode, "LogReturn", wc::LogReturn);
node_scalar_indicator!(
RealizedVolatilityNode,
"RealizedVolatility",
wc::RealizedVolatility
);
node_scalar_indicator!(RollingIqrNode, "RollingIqr", wc::RollingIqr);
node_scalar_indicator!(
RollingPercentileRankNode,
"RollingPercentileRank",
wc::RollingPercentileRank
);
node_scalar_indicator!(TrendLabelNode, "TrendLabel", wc::TrendLabel);
node_scalar_indicator!(WinRateNode, "WinRate", wc::WinRate);
node_scalar_indicator!(ExpectancyNode, "Expectancy", wc::Expectancy);
#[napi(js_name = "JumpIndicator")]
pub struct JumpIndicatorNode {
inner: wc::JumpIndicator,
}
#[napi]
impl JumpIndicatorNode {
#[napi(constructor)]
pub fn new(period: u32, threshold: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::JumpIndicator::new(period as usize, threshold).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 = "RegimeLabel")]
pub struct RegimeLabelNode {
inner: wc::RegimeLabel,
}
#[napi]
impl RegimeLabelNode {
#[napi(constructor)]
pub fn new(vol_period: u32, lookback: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::RegimeLabel::new(vol_period as usize, lookback 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
}
}
#[napi(js_name = "RollingQuantile")]
pub struct RollingQuantileNode {
inner: wc::RollingQuantile,
}
#[napi]
impl RollingQuantileNode {
#[napi(constructor)]
pub fn new(period: u32, quantile: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::RollingQuantile::new(period as usize, quantile).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
}
}
// ============================== Autocorrelation (period + lag) ==============================
@@ -317,6 +436,11 @@ node_pair_indicator!(
);
node_pair_indicator!(BetaNode, "Beta", wc::Beta);
node_pair_indicator!(PairwiseBetaNode, "PairwiseBeta", wc::PairwiseBeta);
node_pair_indicator!(
SpreadAr1CoefficientNode,
"SpreadAr1Coefficient",
wc::SpreadAr1Coefficient
);
node_pair_indicator!(
SpearmanCorrelationNode,
"SpearmanCorrelation",
@@ -1658,6 +1782,266 @@ impl HtPhasorNode {
}
}
#[napi(js_name = "CloseVsOpen")]
pub struct CloseVsOpenNode {
inner: wc::CloseVsOpen,
}
impl Default for CloseVsOpenNode {
fn default() -> Self {
Self::new()
}
}
#[napi]
impl CloseVsOpenNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::CloseVsOpen::new(),
}
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Option<f64>> {
let candle = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
Ok(self.inner.update(candle))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"open, high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(open.len());
for i in 0..open.len() {
let candle =
wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(js_name = "BodySizePct")]
pub struct BodySizePctNode {
inner: wc::BodySizePct,
}
impl Default for BodySizePctNode {
fn default() -> Self {
Self::new()
}
}
#[napi]
impl BodySizePctNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::BodySizePct::new(),
}
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Option<f64>> {
let candle = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
Ok(self.inner.update(candle))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"open, high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(open.len());
for i in 0..open.len() {
let candle =
wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(js_name = "WickRatio")]
pub struct WickRatioNode {
inner: wc::WickRatio,
}
impl Default for WickRatioNode {
fn default() -> Self {
Self::new()
}
}
#[napi]
impl WickRatioNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::WickRatio::new(),
}
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Option<f64>> {
let candle = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
Ok(self.inner.update(candle))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"open, high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(open.len());
for i in 0..open.len() {
let candle =
wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
#[napi(js_name = "HighLowRange")]
pub struct HighLowRangeNode {
inner: wc::HighLowRange,
}
impl Default for HighLowRangeNode {
fn default() -> Self {
Self::new()
}
}
#[napi]
impl HighLowRangeNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::HighLowRange::new(),
}
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Option<f64>> {
let candle = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
Ok(self.inner.update(candle))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"open, high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(open.len());
for i in 0..open.len() {
let candle =
wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).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,
@@ -10334,6 +10718,207 @@ impl TradeImbalanceNode {
}
}
// Order Flow Imbalance: order-book input with a `period` parameter.
#[napi(js_name = "OrderFlowImbalance")]
pub struct OrderFlowImbalanceNode {
inner: wc::OrderFlowImbalance,
}
#[napi]
impl OrderFlowImbalanceNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::OrderFlowImbalance::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
bid_px: Vec<f64>,
bid_sz: Vec<f64>,
ask_px: Vec<f64>,
ask_sz: Vec<f64>,
) -> napi::Result<Option<f64>> {
let book = build_order_book(&bid_px, &bid_sz, &ask_px, &ask_sz)?;
Ok(self.inner.update(book))
}
#[napi]
pub fn batch(&mut self, snapshots: Vec<ObSnapshot>) -> napi::Result<Vec<f64>> {
let mut out = Vec::with_capacity(snapshots.len());
for snap in &snapshots {
let book = build_order_book(&snap.bid_px, &snap.bid_sz, &snap.ask_px, &snap.ask_sz)?;
out.push(self.inner.update(book).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
}
}
// VPIN: trade input, volume-bucketed `(bucket_volume, num_buckets)`.
#[napi(js_name = "Vpin")]
pub struct VpinNode {
inner: wc::Vpin,
}
#[napi]
impl VpinNode {
#[napi(constructor)]
pub fn new(bucket_volume: f64, num_buckets: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::Vpin::new(bucket_volume, num_buckets as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> napi::Result<Option<f64>> {
Ok(self.inner.update(build_trade(price, size, is_buy)?))
}
#[napi]
pub fn batch(
&mut self,
price: Vec<f64>,
size: Vec<f64>,
is_buy: Vec<bool>,
) -> napi::Result<Vec<f64>> {
if price.len() != size.len() || size.len() != is_buy.len() {
return Err(NapiError::from_reason(
"price, size, is_buy must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(price.len());
for i in 0..price.len() {
let trade = build_trade(price[i], size[i], is_buy[i])?;
out.push(self.inner.update(trade).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
}
}
// Amihud Illiquidity: trade input with a `period` parameter.
#[napi(js_name = "AmihudIlliquidity")]
pub struct AmihudIlliquidityNode {
inner: wc::AmihudIlliquidity,
}
#[napi]
impl AmihudIlliquidityNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::AmihudIlliquidity::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> napi::Result<Option<f64>> {
Ok(self.inner.update(build_trade(price, size, is_buy)?))
}
#[napi]
pub fn batch(
&mut self,
price: Vec<f64>,
size: Vec<f64>,
is_buy: Vec<bool>,
) -> napi::Result<Vec<f64>> {
if price.len() != size.len() || size.len() != is_buy.len() {
return Err(NapiError::from_reason(
"price, size, is_buy must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(price.len());
for i in 0..price.len() {
let trade = build_trade(price[i], size[i], is_buy[i])?;
out.push(self.inner.update(trade).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
}
}
// Roll Measure: trade input with a `period` parameter.
#[napi(js_name = "RollMeasure")]
pub struct RollMeasureNode {
inner: wc::RollMeasure,
}
#[napi]
impl RollMeasureNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::RollMeasure::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> napi::Result<Option<f64>> {
Ok(self.inner.update(build_trade(price, size, is_buy)?))
}
#[napi]
pub fn batch(
&mut self,
price: Vec<f64>,
size: Vec<f64>,
is_buy: Vec<bool>,
) -> napi::Result<Vec<f64>> {
if price.len() != size.len() || size.len() != is_buy.len() {
return Err(NapiError::from_reason(
"price, size, is_buy must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(price.len());
for i in 0..price.len() {
let trade = build_trade(price[i], size[i], is_buy[i])?;
out.push(self.inner.update(trade).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
}
}
// ============================== Microstructure: Price Impact ==============================
//
// Price-impact indicators consume a trade paired with the mid prevailing at