feat: trade-flow microstructure indicators (part 2 of 4) (#113)
* feat(core): add 3 trade-flow microstructure indicators SignedVolume (per-trade size signed by aggressor), CumulativeVolumeDelta (running signed-volume total), and TradeImbalance (rolling buy/sell volume imbalance over a trade window). All consume the Trade type, with full unit coverage. Extends the Microstructure family. * feat(bindings): expose trade-flow microstructure indicators Python, Node and WASM bindings for SignedVolume, CumulativeVolumeDelta and TradeImbalance. Each takes a trade via update(price, size, is_buy); Python and Node expose a batch over three parallel arrays, WASM exposes per-trade update. Regenerates node index.d.ts/.js. * test(bindings,fuzz,bench): cover trade-flow microstructure indicators Python and Node: reference values, streaming-vs-batch, lifecycle/repr and input validation (zero window, negative size, non-positive price, mismatched batch lengths). New indicator_update_trade fuzz target. Synthetic trade-tape benches (signed_volume cheapest, trade_imbalance windowed/expensive). * docs: add trade-flow indicators + bump counter to 227 README Microstructure family row gains signed volume / CVD / trade imbalance and the counter goes 224 -> 227; CHANGELOG records the trade-flow indicators.
This commit is contained in:
@@ -951,3 +951,33 @@ test('order-book TopN rejects zero levels', () => {
|
||||
test('order-book update rejects a crossed book', () => {
|
||||
assert.throws(() => new wickra.QuotedSpread().update([102], [1], [101], [1]));
|
||||
});
|
||||
|
||||
test('trade-flow indicators reference values', () => {
|
||||
assert.equal(new wickra.SignedVolume().update(100, 2, true), 2);
|
||||
assert.equal(new wickra.SignedVolume().update(100, 3, false), -3);
|
||||
const cvd = new wickra.CumulativeVolumeDelta();
|
||||
assert.equal(cvd.update(100, 5, true), 5);
|
||||
assert.equal(cvd.update(100, 2, false), 3);
|
||||
const ti = new wickra.TradeImbalance(2);
|
||||
assert.equal(ti.update(100, 3, true), null); // warming up
|
||||
assert.equal(ti.update(100, 1, false), 0.5); // (3 - 1) / 4
|
||||
});
|
||||
|
||||
test('trade-flow streaming update matches batch', () => {
|
||||
const n = 30;
|
||||
const price = Array.from({ length: n }, () => 100);
|
||||
const size = Array.from({ length: n }, (_, i) => 1 + (i % 4));
|
||||
const isBuy = Array.from({ length: n }, (_, i) => i % 3 !== 0);
|
||||
const batch = new wickra.CumulativeVolumeDelta().batch(price, size, isBuy);
|
||||
const streamer = new wickra.CumulativeVolumeDelta();
|
||||
assert.equal(batch.length, n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const s = streamer.update(price[i], size[i], isBuy[i]);
|
||||
assert.ok(Math.abs(s - batch[i]) < 1e-12, `mismatch at ${i}: ${s} vs ${batch[i]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('trade-flow rejects bad input', () => {
|
||||
assert.throws(() => new wickra.TradeImbalance(0));
|
||||
assert.throws(() => new wickra.SignedVolume().update(100, -1, true));
|
||||
});
|
||||
|
||||
Vendored
+27
@@ -2241,6 +2241,33 @@ export declare class OrderBookImbalanceTopN {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SignedVolumeNode = SignedVolume
|
||||
export declare class SignedVolume {
|
||||
constructor()
|
||||
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 CumulativeVolumeDeltaNode = CumulativeVolumeDelta
|
||||
export declare class CumulativeVolumeDelta {
|
||||
constructor()
|
||||
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 TradeImbalanceNode = TradeImbalance
|
||||
export declare class TradeImbalance {
|
||||
constructor(window: 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 SharpeRatioNode = SharpeRatio
|
||||
export declare class SharpeRatio {
|
||||
constructor(period: number, riskFree: number)
|
||||
|
||||
@@ -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, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, OrderBookImbalanceTopN, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
|
||||
|
||||
module.exports.version = version
|
||||
module.exports.SMA = SMA
|
||||
@@ -520,6 +520,9 @@ module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
|
||||
module.exports.Microprice = Microprice
|
||||
module.exports.QuotedSpread = QuotedSpread
|
||||
module.exports.OrderBookImbalanceTopN = OrderBookImbalanceTopN
|
||||
module.exports.SignedVolume = SignedVolume
|
||||
module.exports.CumulativeVolumeDelta = CumulativeVolumeDelta
|
||||
module.exports.TradeImbalance = TradeImbalance
|
||||
module.exports.SharpeRatio = SharpeRatio
|
||||
module.exports.SortinoRatio = SortinoRatio
|
||||
module.exports.CalmarRatio = CalmarRatio
|
||||
|
||||
@@ -8914,6 +8914,144 @@ impl OrderBookImbalanceTopNNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Microstructure: Trade Flow ==============================
|
||||
//
|
||||
// Trade-flow indicators consume a trade tape rather than OHLCV. Streaming
|
||||
// `update(price, size, isBuy)` takes one trade (`isBuy=true` for a
|
||||
// buyer-initiated trade); `batch` takes three equal-length arrays.
|
||||
|
||||
fn build_trade(price: f64, size: f64, is_buy: bool) -> napi::Result<wc::Trade> {
|
||||
let side = if is_buy {
|
||||
wc::Side::Buy
|
||||
} else {
|
||||
wc::Side::Sell
|
||||
};
|
||||
wc::Trade::new(price, size, side, 0).map_err(map_err)
|
||||
}
|
||||
|
||||
macro_rules! node_trade_indicator {
|
||||
($node:ident, $inner:ty, $js:literal) => {
|
||||
#[napi(js_name = $js)]
|
||||
pub struct $node {
|
||||
inner: $inner,
|
||||
}
|
||||
|
||||
impl Default for $node {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl $node {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: <$inner>::new(),
|
||||
}
|
||||
}
|
||||
#[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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
node_trade_indicator!(SignedVolumeNode, wc::SignedVolume, "SignedVolume");
|
||||
node_trade_indicator!(
|
||||
CumulativeVolumeDeltaNode,
|
||||
wc::CumulativeVolumeDelta,
|
||||
"CumulativeVolumeDelta"
|
||||
);
|
||||
|
||||
// Trade imbalance carries a `window` parameter, so it is hand-written.
|
||||
#[napi(js_name = "TradeImbalance")]
|
||||
pub struct TradeImbalanceNode {
|
||||
inner: wc::TradeImbalance,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl TradeImbalanceNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(window: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TradeImbalance::new(window 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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Family 15: Risk / Performance ==============================
|
||||
|
||||
// Risk metrics with fallible `new` (most need `period >= 2`), so each wrapper
|
||||
|
||||
Reference in New Issue
Block a user