// Comprehensive tests for the Wickra Node bindings: streaming-vs-batch // equivalence, reference values, and lifecycle methods across all 71 // indicators. Ported from the Python test_streaming_vs_batch / test_known_values // suites. const test = require('node:test'); const assert = require('node:assert/strict'); const wickra = require('..'); // Synthetic OHLCV series long enough to warm up every indicator. const N = 120; const close = Array.from({ length: N }, (_, i) => 100 + Math.sin(i * 0.2) * 10 + i * 0.1); const high = close.map((c) => c + 1.5); const low = close.map((c) => c - 1.5); const volume = Array.from({ length: N }, (_, i) => 1000 + (i % 7) * 50); const open = close.map((c) => c - 0.5); function eq(a, b) { if (Number.isNaN(a)) return Number.isNaN(b); if (!Number.isFinite(a) || !Number.isFinite(b)) return a === b; return Math.abs(a - b) < 1e-9; } function num(v) { return v === null || v === undefined ? NaN : v; } // --- Scalar indicators: update(value) vs batch(prices) --- const scalarFactories = { SMA: () => new wickra.SMA(14), EMA: () => new wickra.EMA(14), WMA: () => new wickra.WMA(14), RSI: () => new wickra.RSI(14), DEMA: () => new wickra.DEMA(10), TEMA: () => new wickra.TEMA(10), HMA: () => new wickra.HMA(9), ROC: () => new wickra.ROC(12), TRIX: () => new wickra.TRIX(9), KAMA: () => new wickra.KAMA(10, 2, 30), ALMA: () => new wickra.ALMA(9, 0.85, 6.0), McGinleyDynamic: () => new wickra.McGinleyDynamic(10), FRAMA: () => new wickra.FRAMA(16), VIDYA: () => new wickra.VIDYA(14, 9), JMA: () => new wickra.JMA(14, 0, 2), SMMA: () => new wickra.SMMA(14), TRIMA: () => new wickra.TRIMA(20), ZLEMA: () => new wickra.ZLEMA(14), T3: () => new wickra.T3(5, 0.7), MOM: () => new wickra.MOM(10), CMO: () => new wickra.CMO(14), TSI: () => new wickra.TSI(25, 13), PMO: () => new wickra.PMO(35, 20), TII: () => new wickra.TII(20, 10), StochRSI: () => new wickra.StochRSI(14, 14), PPO: () => new wickra.PPO(12, 26), APO: () => new wickra.APO(12, 26), CFO: () => new wickra.CFO(14), ElderImpulse: () => new wickra.ElderImpulse(13, 12, 26, 9), STC: () => new wickra.STC(23, 50, 10, 0.5), DPO: () => new wickra.DPO(20), Coppock: () => new wickra.Coppock(14, 11, 10), StdDev: () => new wickra.StdDev(20), UlcerIndex: () => new wickra.UlcerIndex(14), HistoricalVolatility: () => new wickra.HistoricalVolatility(20, 252), BollingerBandwidth: () => new wickra.BollingerBandwidth(20, 2), PercentB: () => new wickra.PercentB(20, 2), LinearRegression: () => new wickra.LinearRegression(14), LinRegSlope: () => new wickra.LinRegSlope(14), VerticalHorizontalFilter: () => new wickra.VerticalHorizontalFilter(28), ZScore: () => new wickra.ZScore(20), LinRegAngle: () => new wickra.LinRegAngle(14), PercentageTrailingStop: () => new wickra.PercentageTrailingStop(5), StepTrailingStop: () => new wickra.StepTrailingStop(1), RenkoTrailingStop: () => new wickra.RenkoTrailingStop(1), LaguerreRSI: () => new wickra.LaguerreRSI(0.5), ConnorsRSI: () => new wickra.ConnorsRSI(3, 2, 100), RVIVolatility: () => new wickra.RVIVolatility(10), // Family 10 — Ehlers / Cycle SuperSmoother: () => new wickra.SuperSmoother(10), FisherTransform: () => new wickra.FisherTransform(10), InverseFisherTransform: () => new wickra.InverseFisherTransform(1.0), Decycler: () => new wickra.Decycler(20), DecyclerOscillator: () => new wickra.DecyclerOscillator(10, 30), RoofingFilter: () => new wickra.RoofingFilter(10, 48), CenterOfGravity: () => new wickra.CenterOfGravity(10), CyberneticCycle: () => new wickra.CyberneticCycle(10), InstantaneousTrendline: () => new wickra.InstantaneousTrendline(20), EhlersStochastic: () => new wickra.EhlersStochastic(20), EmpiricalModeDecomposition: () => new wickra.EmpiricalModeDecomposition(20, 0.5), HilbertDominantCycle: () => new wickra.HilbertDominantCycle(), AdaptiveCycle: () => new wickra.AdaptiveCycle(), SineWave: () => new wickra.SineWave(), FAMA: () => new wickra.FAMA(0.5, 0.05), // Family 12 — Statistik / Regression Variance: () => new wickra.Variance(20), CoefficientOfVariation: () => new wickra.CoefficientOfVariation(20), Skewness: () => new wickra.Skewness(20), Kurtosis: () => new wickra.Kurtosis(20), StandardError: () => new wickra.StandardError(14), DetrendedStdDev: () => new wickra.DetrendedStdDev(14), RSquared: () => new wickra.RSquared(14), MedianAbsoluteDeviation: () => new wickra.MedianAbsoluteDeviation(20), Autocorrelation: () => new wickra.Autocorrelation(20, 1), HurstExponent: () => new wickra.HurstExponent(40, 4), // Family 15 — Risk / Performance metrics (scalar f64 input). SharpeRatio: () => new wickra.SharpeRatio(20, 0), SortinoRatio: () => new wickra.SortinoRatio(20, 0), CalmarRatio: () => new wickra.CalmarRatio(20), OmegaRatio: () => new wickra.OmegaRatio(20, 0), MaxDrawdown: () => new wickra.MaxDrawdown(20), AverageDrawdown: () => new wickra.AverageDrawdown(20), DrawdownDuration: () => new wickra.DrawdownDuration(), PainIndex: () => new wickra.PainIndex(20), ValueAtRisk: () => new wickra.ValueAtRisk(20, 0.95), ConditionalValueAtRisk: () => new wickra.ConditionalValueAtRisk(20, 0.95), ProfitFactor: () => new wickra.ProfitFactor(20), GainLossRatio: () => new wickra.GainLossRatio(20), RecoveryFactor: () => new wickra.RecoveryFactor(), KellyCriterion: () => new wickra.KellyCriterion(20), }; // --- Two-series (asset, benchmark) ratio indicators --- const ratioPairFactories = { TreynorRatio: () => new wickra.TreynorRatio(20, 0), InformationRatio: () => new wickra.InformationRatio(20), Alpha: () => new wickra.Alpha(20, 0), }; const asset = Array.from({ length: N }, (_, i) => 0.001 + Math.sin(i * 0.15) * 0.01); const bench = Array.from({ length: N }, (_, i) => 0.001 + Math.sin(i * 0.15) * 0.007); for (const [name, make] of Object.entries(ratioPairFactories)) { test(`${name}: streaming update matches batch (pair)`, () => { const batch = make().batch(asset, bench); const streaming = make(); assert.equal(batch.length, N); for (let i = 0; i < N; i++) { const s = num(streaming.update(asset[i], bench[i])); assert.ok(eq(s, batch[i]), `${name} mismatch at ${i}: ${s} vs ${batch[i]}`); } }); } for (const [name, make] of Object.entries(scalarFactories)) { test(`${name}: streaming update matches batch`, () => { const batch = make().batch(close); const streaming = make(); assert.equal(batch.length, N); for (let i = 0; i < N; i++) { const s = num(streaming.update(close[i])); assert.ok(eq(s, batch[i]), `${name} mismatch at ${i}: ${s} vs ${batch[i]}`); } }); } // --- Scalar-output candle indicators: update(...) vs batch(...) --- const candleScalar = { ATR: { make: () => new wickra.ATR(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, CCI: { make: () => new wickra.CCI(20), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, WilliamsR: { make: () => new wickra.WilliamsR(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, PSAR: { make: () => new wickra.PSAR(0.02, 0.02, 0.2), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, MFI: { make: () => new wickra.MFI(14), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, VWAP: { make: () => new wickra.VWAP(), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, RollingVWAP: { make: () => new wickra.RollingVWAP(20), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, AwesomeOscillator: { make: () => new wickra.AwesomeOscillator(5, 34), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, OBV: { make: () => new wickra.OBV(), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, VWMA: { make: () => new wickra.VWMA(20), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, RVI: { make: () => new wickra.RVI(10), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Inertia: { make: () => new wickra.Inertia(14, 20), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, PGO: { make: () => new wickra.PGO(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, SMI: { make: () => new wickra.SMI(5, 3, 3), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, EVWMA: { make: () => new wickra.EVWMA(20), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, UltimateOscillator: { make: () => new wickra.UltimateOscillator(7, 14, 28), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, AroonOscillator: { make: () => new wickra.AroonOscillator(14), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, NATR: { make: () => new wickra.NATR(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, MassIndex: { make: () => new wickra.MassIndex(9, 25), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, ADL: { make: () => new wickra.ADL(), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, VolumePriceTrend: { make: () => new wickra.VolumePriceTrend(), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, ChaikinMoneyFlow: { make: () => new wickra.ChaikinMoneyFlow(20), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, ChaikinOscillator: { make: () => new wickra.ChaikinOscillator(3, 10), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, ForceIndex: { make: () => new wickra.ForceIndex(13), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, EaseOfMovement: { make: () => new wickra.EaseOfMovement(14, 1e8), step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) }, KVO: { make: () => new wickra.KVO(34, 55), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, VolumeOscillator: { make: () => new wickra.VolumeOscillator(14, 28), step: (ind, i) => ind.update(volume[i]), batch: (ind) => ind.batch(volume) }, NVI: { make: () => new wickra.NVI(), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, PVI: { make: () => new wickra.PVI(), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, WilliamsAD: { make: () => new wickra.WilliamsAD(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, AnchoredVWAP: { make: () => new wickra.AnchoredVWAP(), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, DemandIndex: { make: () => new wickra.DemandIndex(10), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, TSV: { make: () => new wickra.TSV(18), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, VZO: { make: () => new wickra.VZO(14), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, MarketFacilitationIndex: { make: () => new wickra.MarketFacilitationIndex(), step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) }, AtrTrailingStop: { make: () => new wickra.AtrTrailingStop(14, 3), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, HiLoActivator: { make: () => new wickra.HiLoActivator(3), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, VoltyStop: { make: () => new wickra.VoltyStop(14, 2), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, YoyoExit: { make: () => new wickra.YoyoExit(14, 2), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, TypicalPrice: { make: () => new wickra.TypicalPrice(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, MedianPrice: { make: () => new wickra.MedianPrice(), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, WeightedClose: { make: () => new wickra.WeightedClose(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, AcceleratorOscillator: { make: () => new wickra.AcceleratorOscillator(5, 34, 5), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, AwesomeOscillatorHistogram: { make: () => new wickra.AwesomeOscillatorHistogram(5, 34, 5), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, BalanceOfPower: { make: () => new wickra.BalanceOfPower(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ChoppinessIndex: { make: () => new wickra.ChoppinessIndex(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, TrueRange: { make: () => new wickra.TrueRange(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, ChaikinVolatility: { make: () => new wickra.ChaikinVolatility(10, 10), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, ADXR: { make: () => new wickra.ADXR(7), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, ParkinsonVolatility: { make: () => new wickra.ParkinsonVolatility(20, 252), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, GarmanKlassVolatility: { make: () => new wickra.GarmanKlassVolatility(20, 252), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, RogersSatchellVolatility: { make: () => new wickra.RogersSatchellVolatility(20, 252), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, YangZhangVolatility: { make: () => new wickra.YangZhangVolatility(20, 252), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, TDSetup: { make: () => new wickra.TDSetup(4, 9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, TDDeMarker: { make: () => new wickra.TDDeMarker(14), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, TDREI: { make: () => new wickra.TDREI(5), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, TDPressure: { make: () => new wickra.TDPressure(5), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(open, high, low, close, volume) }, TDCombo: { make: () => new wickra.TDCombo(4, 9, 2, 13), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, TDCountdown: { make: () => new wickra.TDCountdown(4, 9, 2, 13), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, TDDifferential: { make: () => new wickra.TDDifferential(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, TDOpen: { make: () => new wickra.TDOpen(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, // Family 14 — Candlestick patterns Doji: { make: () => new wickra.Doji(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Hammer: { make: () => new wickra.Hammer(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, InvertedHammer: { make: () => new wickra.InvertedHammer(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, HangingMan: { make: () => new wickra.HangingMan(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ShootingStar: { make: () => new wickra.ShootingStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Engulfing: { make: () => new wickra.Engulfing(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Harami: { make: () => new wickra.Harami(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, MorningEveningStar: { make: () => new wickra.MorningEveningStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ThreeSoldiersOrCrows: { make: () => new wickra.ThreeSoldiersOrCrows(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, PiercingDarkCloud: { make: () => new wickra.PiercingDarkCloud(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Marubozu: { make: () => new wickra.Marubozu(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Tweezer: { make: () => new wickra.Tweezer(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, SpinningTop: { make: () => new wickra.SpinningTop(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ThreeInside: { make: () => new wickra.ThreeInside(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ThreeOutside: { make: () => new wickra.ThreeOutside(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, TwoCrows: { make: () => new wickra.TwoCrows(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, UpsideGapTwoCrows: { make: () => new wickra.UpsideGapTwoCrows(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, IdenticalThreeCrows: { make: () => new wickra.IdenticalThreeCrows(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ThreeLineStrike: { make: () => new wickra.ThreeLineStrike(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ThreeStarsInSouth: { make: () => new wickra.ThreeStarsInSouth(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, AbandonedBaby: { make: () => new wickra.AbandonedBaby(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, AdvanceBlock: { make: () => new wickra.AdvanceBlock(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, BeltHold: { make: () => new wickra.BeltHold(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Breakaway: { make: () => new wickra.Breakaway(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Counterattack: { make: () => new wickra.Counterattack(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, DojiStar: { make: () => new wickra.DojiStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, DragonflyDoji: { make: () => new wickra.DragonflyDoji(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, GravestoneDoji: { make: () => new wickra.GravestoneDoji(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, LongLeggedDoji: { make: () => new wickra.LongLeggedDoji(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, RickshawMan: { make: () => new wickra.RickshawMan(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, EveningDojiStar: { make: () => new wickra.EveningDojiStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, MorningDojiStar: { make: () => new wickra.MorningDojiStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, GapSideBySideWhite: { make: () => new wickra.GapSideBySideWhite(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, HighWave: { make: () => new wickra.HighWave(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Hikkake: { make: () => new wickra.Hikkake(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, HikkakeModified: { make: () => new wickra.HikkakeModified(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, HomingPigeon: { make: () => new wickra.HomingPigeon(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, OnNeck: { make: () => new wickra.OnNeck(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, InNeck: { make: () => new wickra.InNeck(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Thrusting: { make: () => new wickra.Thrusting(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, SeparatingLines: { make: () => new wickra.SeparatingLines(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Kicking: { make: () => new wickra.Kicking(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, KickingByLength: { make: () => new wickra.KickingByLength(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, LadderBottom: { make: () => new wickra.LadderBottom(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, MatHold: { make: () => new wickra.MatHold(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, MatchingLow: { make: () => new wickra.MatchingLow(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, LongLine: { make: () => new wickra.LongLine(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ShortLine: { make: () => new wickra.ShortLine(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, RisingThreeMethods: { make: () => new wickra.RisingThreeMethods(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, FallingThreeMethods: { make: () => new wickra.FallingThreeMethods(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, UpsideGapThreeMethods: { make: () => new wickra.UpsideGapThreeMethods(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, DownsideGapThreeMethods: { make: () => new wickra.DownsideGapThreeMethods(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, StalledPattern: { make: () => new wickra.StalledPattern(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, StickSandwich: { make: () => new wickra.StickSandwich(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Takuri: { make: () => new wickra.Takuri(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ClosingMarubozu: { make: () => new wickra.ClosingMarubozu(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, OpeningMarubozu: { make: () => new wickra.OpeningMarubozu(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, TasukiGap: { make: () => new wickra.TasukiGap(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, UniqueThreeRiver: { make: () => new wickra.UniqueThreeRiver(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, ConcealingBabySwallow: { make: () => new wickra.ConcealingBabySwallow(), 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)) { test(`${name}: streaming update matches batch`, () => { const batch = d.batch(d.make()); const streaming = d.make(); assert.equal(batch.length, N); for (let i = 0; i < N; i++) { const s = num(d.step(streaming, i)); assert.ok(eq(s, batch[i]), `${name} mismatch at ${i}: ${s} vs ${batch[i]}`); } }); } // --- Multi-output indicators: object update vs interleaved batch --- const multi = { KST: { make: () => new wickra.KST(10, 15, 20, 30, 10, 10, 10, 15, 9), fields: ['kst', 'signal'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) }, Alligator: { make: () => new wickra.Alligator(13, 8, 5), fields: ['jaw', 'teeth', 'lips'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, ZeroLagMACD: { make: () => new wickra.ZeroLagMACD(12, 26, 9), fields: ['macd', 'signal', 'histogram'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) }, MACD: { make: () => new wickra.MACD(12, 26, 9), fields: ['macd', 'signal', 'histogram'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) }, KST: { make: () => wickra.KST.classic(), fields: ['kst', 'signal'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) }, BollingerBands: { make: () => new wickra.BollingerBands(20, 2), fields: ['upper', 'middle', 'lower', 'stddev'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) }, Stochastic: { make: () => new wickra.Stochastic(14, 3), fields: ['k', 'd'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, ADX: { make: () => new wickra.ADX(14), fields: ['plusDi', 'minusDi', 'adx'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, Keltner: { make: () => new wickra.Keltner(20, 10, 2), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, Donchian: { make: () => new wickra.Donchian(20), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, Aroon: { make: () => new wickra.Aroon(14), fields: ['up', 'down'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, Vortex: { make: () => new wickra.Vortex(14), fields: ['plus', 'minus'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, RWI: { make: () => new wickra.RWI(14), fields: ['high', 'low'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, WaveTrend: { make: () => wickra.WaveTrend.classic(), fields: ['wt1', 'wt2'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, SuperTrend: { make: () => new wickra.SuperTrend(10, 3), fields: ['value', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, ChandelierExit: { make: () => new wickra.ChandelierExit(22, 3), fields: ['longStop', 'shortStop'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, ChandeKrollStop: { make: () => new wickra.ChandeKrollStop(10, 1, 9), fields: ['stopLong', 'stopShort'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, // Family 16: Market Profile ValueArea: { make: () => new wickra.ValueArea(20, 50, 0.70), fields: ['poc', 'vah', 'val'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) }, InitialBalance: { make: () => new wickra.InitialBalance(12), fields: ['high', 'low'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, OpeningRange: { make: () => new wickra.OpeningRange(6), fields: ['high', 'low', 'breakoutDistance'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, DonchianStop: { make: () => new wickra.DonchianStop(10), fields: ['stopLong', 'stopShort'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, // 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) }, // Family 08: Pivots & Support/Resistance ClassicPivots: { make: () => new wickra.ClassicPivots(), fields: ['pp', 'r1', 'r2', 'r3', 's1', 's2', 's3'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, FibonacciPivots: { make: () => new wickra.FibonacciPivots(), fields: ['pp', 'r1', 'r2', 'r3', 's1', 's2', 's3'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, Camarilla: { make: () => new wickra.Camarilla(), fields: ['pp', 'r1', 'r2', 'r3', 'r4', 's1', 's2', 's3', 's4'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, WoodiePivots: { make: () => new wickra.WoodiePivots(), fields: ['pp', 'r1', 'r2', 's1', 's2'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, DemarkPivots: { make: () => new wickra.DemarkPivots(), fields: ['pp', 'r1', 's1'], step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, WilliamsFractals: { make: () => new wickra.WilliamsFractals(), fields: ['up', 'down'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, ZigZag: { make: () => new wickra.ZigZag(0.02), fields: ['swing', 'direction'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, // Family 11: DeMark TDSequential: { make: () => new wickra.TDSequential(4, 9, 2, 13), fields: ['setup', 'countdown', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, TDLines: { make: () => new wickra.TDLines(4, 9), fields: ['resistance', 'support'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, TDRangeProjection: { make: () => new wickra.TDRangeProjection(), fields: ['high', 'low'], step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, TDRiskLevel: { make: () => new wickra.TDRiskLevel(4, 9), fields: ['buyRisk', 'sellRisk'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, // Family 10: Ehlers / Cycle (multi-output) MAMA: { make: () => new wickra.MAMA(0.5, 0.05), fields: ['mama', 'fama'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) }, // Family 13: Ichimoku & alternative charts Ichimoku: { make: () => new wickra.Ichimoku(9, 26, 52, 26), fields: ['tenkan', 'kijun', 'senkouA', 'senkouB', 'chikou'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, HeikinAshi: { make: () => new wickra.HeikinAshi(), fields: ['open', 'high', 'low', 'close'], 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(multi)) { test(`${name}: streaming update matches interleaved batch`, () => { const k = d.fields.length; const batch = d.batch(d.make()); const streaming = d.make(); assert.equal(batch.length, N * k); for (let i = 0; i < N; i++) { const o = d.step(streaming, i); d.fields.forEach((field, j) => { const s = o === null || o === undefined ? NaN : o[field]; assert.ok(eq(s, batch[i * k + j]), `${name}.${field} mismatch at ${i}`); }); } }); } // --- Lifecycle: every indicator exposes reset / isReady / warmupPeriod --- test('every indicator exposes reset, isReady and warmupPeriod', () => { const all = [ ...Object.values(scalarFactories).map((f) => f()), ...Object.values(candleScalar).map((d) => d.make()), ...Object.values(multi).map((d) => d.make()), ]; for (const ind of all) { assert.equal(typeof ind.reset, 'function'); assert.equal(typeof ind.isReady, 'function'); assert.equal(typeof ind.warmupPeriod, 'function'); assert.equal(ind.isReady(), false); assert.ok(ind.warmupPeriod() >= 1); } }); test('reset returns an indicator to its un-warmed state', () => { const sma = new wickra.SMA(5); sma.batch([1, 2, 3, 4, 5]); assert.equal(sma.isReady(), true); sma.reset(); assert.equal(sma.isReady(), false); assert.equal(sma.update(10), null); }); // --- Reference values --- test('SMA(3) reference values', () => { const out = new wickra.SMA(3).batch([2, 4, 6, 8, 10]); assert.ok(Number.isNaN(out[0]) && Number.isNaN(out[1])); assert.equal(out[2], 4); assert.equal(out[3], 6); assert.equal(out[4], 8); }); test('MFI(2) reference value equals 1200/23', () => { // Candle 1 seeds; candle 2 (tp 12 > 10) +mf 1200; candle 3 (tp 11 < 12) -mf 1100. const mfi = new wickra.MFI(2); assert.equal(mfi.update(10, 10, 10, 100), null); assert.equal(mfi.update(12, 12, 12, 100), null); const v = mfi.update(11, 11, 11, 100); assert.ok(Math.abs(v - 1200 / 23) < 1e-9); }); test('RSI pure uptrend yields 100', () => { const prices = Array.from({ length: 20 }, (_, i) => i + 1); const out = new wickra.RSI(14).batch(prices); for (let i = 14; i < out.length; i++) { assert.equal(out[i], 100); } }); test('MACD histogram equals macd minus signal', () => { const macd = new wickra.MACD(12, 26, 9); let v = null; for (let i = 1; i <= 60; i++) v = macd.update(i); assert.ok(v); assert.ok(Math.abs(v.histogram - (v.macd - v.signal)) < 1e-9); }); test('TypicalPrice reference value', () => { // (high + low + close) / 3 = (12 + 6 + 9) / 3 = 9. assert.equal(new wickra.TypicalPrice().update(12, 6, 9), 9); }); test('ChaikinMoneyFlow(2) reference value equals 0.5', () => { // Bar 1 closes at the high (MFV +100); bar 2 closes mid-range (MFV 0). const cmf = new wickra.ChaikinMoneyFlow(2); assert.equal(cmf.update(10, 8, 10, 100), null); assert.ok(Math.abs(cmf.update(12, 8, 10, 100) - 0.5) < 1e-9); }); test('LinearRegression(3) reference values', () => { // Least-squares line through [1, 2, 9] is y = 4x; endpoint 4·2 = 8. const out = new wickra.LinearRegression(3).batch([1, 2, 9]); assert.ok(Number.isNaN(out[0]) && Number.isNaN(out[1])); assert.ok(Math.abs(out[2] - 8) < 1e-9); }); test('SuperTrend flat market holds the lower band and an uptrend', () => { // Flat candles: ATR 2, hl2 10, lower band 10 - 3·2 = 4. const n = 20; const out = new wickra.SuperTrend(5, 3).batch( Array(n).fill(11), Array(n).fill(9), Array(n).fill(10), ); assert.ok(Math.abs(out[2 * n - 2] - 4) < 1e-9); // value assert.equal(out[2 * n - 1], 1); // direction }); test('BalanceOfPower reference value', () => { // (close - open) / (high - low) = (12 - 10) / (14 - 10) = 0.5. assert.ok(Math.abs(new wickra.BalanceOfPower().update(10, 14, 10, 12) - 0.5) < 1e-9); }); test('TrueRange reference values', () => { const tr = new wickra.TrueRange(); assert.equal(tr.update(12, 8, 11), 4); // no prev close -> high - low assert.equal(tr.update(10, 9, 9.5), 2); // prev close 11 -> max(1, 1, 2) }); test('LinRegAngle of a unit-slope series is 45 degrees', () => { const out = new wickra.LinRegAngle(5).batch([1, 2, 3, 4, 5, 6]); assert.ok(Math.abs(out[4] - 45) < 1e-9); }); test('InitialBalance(2) locks after period and ignores subsequent bars', () => { const ib = new wickra.InitialBalance(2); let v = ib.update(102, 100); assert.equal(v.high, 102); assert.equal(v.low, 100); v = ib.update(103, 99); assert.equal(v.high, 103); assert.equal(v.low, 99); assert.equal(ib.isLocked(), true); // Extreme bar after lock must not modify the IB. v = ib.update(200, 50); assert.equal(v.high, 103); assert.equal(v.low, 99); }); test('OpeningRange(2) breakout distance is signed close minus midpoint', () => { const or = new wickra.OpeningRange(2); or.update(102, 100, 101); or.update(103, 101, 102); // OR locked at high 103 / low 100 / mid 101.5. Close 105 -> +3.5. const v = or.update(110, 102, 105); assert.equal(v.high, 103); assert.equal(v.low, 100); assert.ok(Math.abs(v.breakoutDistance - 3.5) < 1e-9); }); // --- Family 12: two-series indicators (Pearson / Beta / Spearman) --- const pairFactories = { PearsonCorrelation: () => new wickra.PearsonCorrelation(14), Beta: () => new wickra.Beta(14), PairwiseBeta: () => new wickra.PairwiseBeta(14), PairSpreadZScore: () => new wickra.PairSpreadZScore(14, 14), SpearmanCorrelation: () => new wickra.SpearmanCorrelation(14), }; for (const [name, make] of Object.entries(pairFactories)) { test(`${name}: streaming update matches batch over a pair of series`, () => { const xs = Array.from({ length: N }, (_, i) => Math.sin(i * 0.2) + 0.05 * i); const ys = Array.from({ length: N }, (_, i) => Math.cos(i * 0.3) + 0.02 * i); const batch = make().batch(xs, ys); const streaming = make(); assert.equal(batch.length, N); for (let i = 0; i < N; i++) { const s = num(streaming.update(xs[i], ys[i])); assert.ok(eq(s, batch[i]), `${name} mismatch at ${i}: ${s} vs ${batch[i]}`); } }); } test('PearsonCorrelation perfect positive is 1', () => { const x = Array.from({ length: 10 }, (_, i) => i); const y = x.map((v) => 2 * v + 3); const out = new wickra.PearsonCorrelation(5).batch(x, y); assert.ok(Math.abs(out[out.length - 1] - 1) < 1e-9); }); test('Beta perfect two-to-one', () => { const bench = Array.from({ length: 10 }, (_, i) => i); const asset = bench.map((v) => 2 * v); const out = new wickra.Beta(5).batch(asset, bench); assert.ok(Math.abs(out[out.length - 1] - 2) < 1e-9); }); test('PairwiseBeta squared price is two', () => { // b needs varying returns; a = b² ⇒ a's log-returns are exactly 2× b's. const bench = Array.from({ length: 20 }, (_, i) => 100 + 10 * Math.sin(i * 0.5)); const asset = bench.map((v) => v * v); const out = new wickra.PairwiseBeta(5).batch(asset, bench); assert.ok(Math.abs(out[out.length - 1] - 2) < 1e-9); }); test('PairSpreadZScore flat benchmark is sign of last move', () => { // Flat b ⇒ hedge ratio 0 ⇒ spread = ln(a); z_period = 2 ⇒ z = sign of move. const a = [100, 100, 110, 105, 130]; const b = [100, 100, 100, 100, 100]; const out = new wickra.PairSpreadZScore(2, 2).batch(a, b); assert.ok(Math.abs(out[out.length - 1] - 1) < 1e-9); assert.ok(Math.abs(out[out.length - 2] + 1) < 1e-9); }); const llSignal = (t) => Math.sin(t * 0.4) + 0.4 * Math.sin(t * 1.1) + 0.2 * Math.cos(t * 0.27); test('LeadLagCrossCorrelation detects positive lead (object output)', () => { const ll = new wickra.LeadLagCrossCorrelation(12, 5); let last = null; // b is a delayed by 3 ⇒ a leads b ⇒ lag = +3. for (let t = 0; t < 60; t++) last = ll.update(llSignal(t), llSignal(t - 3)); assert.equal(last.lag, 3); assert.ok(last.correlation > 0.99); }); test('LeadLagCrossCorrelation batch is flat 2*n with last row matching', () => { const n = 60; const a = Array.from({ length: n }, (_, t) => llSignal(t)); const b = Array.from({ length: n }, (_, t) => llSignal(t - 3)); const out = new wickra.LeadLagCrossCorrelation(12, 5).batch(a, b); assert.equal(out.length, 2 * n); assert.equal(out[2 * (n - 1)], 3); assert.ok(out[2 * (n - 1) + 1] > 0.99); }); test('Cointegration detects mean-reverting pair (object output)', () => { const n = 80; const b = Array.from({ length: n }, (_, t) => 50 + 0.5 * t); const a = b.map((v, t) => 2 * v + 1 + 0.5 * Math.sin(t * 0.6)); const co = new wickra.Cointegration(40, 1); let last = null; for (let i = 0; i < n; i++) last = co.update(a[i], b[i]); assert.ok(Math.abs(last.hedgeRatio - 2) < 0.1); assert.ok(last.adfStat < -2); }); test('Cointegration batch is flat 3*n with last row matching', () => { const n = 80; const b = Array.from({ length: n }, (_, t) => 50 + 0.5 * t); const a = b.map((v, t) => 2 * v + 1 + 0.5 * Math.sin(t * 0.6)); const out = new wickra.Cointegration(40, 1).batch(a, b); assert.equal(out.length, 3 * n); assert.ok(Math.abs(out[3 * (n - 1)] - 2) < 0.1); assert.ok(out[3 * (n - 1) + 2] < -2); }); test('RelativeStrengthAB constant ratio is flat (object output)', () => { const rs = new wickra.RelativeStrengthAB(5, 5); let last = null; for (let i = 0; i < 30; i++) last = rs.update(200, 100); // ratio is a constant 2 assert.ok(Math.abs(last.ratio - 2) < 1e-12); assert.ok(Math.abs(last.ratioMa - 2) < 1e-12); assert.ok(Math.abs(last.ratioRsi - 50) < 1e-9); }); test('RelativeStrengthAB batch is flat 3*n with last row matching', () => { const n = 30; const a = Array.from({ length: n }, () => 200); const b = Array.from({ length: n }, () => 100); const out = new wickra.RelativeStrengthAB(5, 5).batch(a, b); assert.equal(out.length, 3 * n); assert.ok(Math.abs(out[3 * (n - 1)] - 2) < 1e-12); assert.ok(Math.abs(out[3 * (n - 1) + 2] - 50) < 1e-9); }); test('SpearmanCorrelation monotone non-linear is 1', () => { const x = Array.from({ length: 10 }, (_, i) => i + 1); const y = x.map((v) => v ** 3); const out = new wickra.SpearmanCorrelation(5).batch(x, y); assert.ok(Math.abs(out[out.length - 1] - 1) < 1e-9); }); test('Variance(3) of [2, 4, 6] equals 8/3', () => { const out = new wickra.Variance(3).batch([2, 4, 6]); assert.ok(Math.abs(out[2] - 8 / 3) < 1e-12); }); test('RSquared on a perfect line is 1', () => { const xs = Array.from({ length: 20 }, (_, i) => 2 * i + 5); const out = new wickra.RSquared(5).batch(xs); for (let i = 5; i < out.length; i++) { assert.ok(Math.abs(out[i] - 1) < 1e-9); } }); test('MedianAbsoluteDeviation ignores a single huge outlier', () => { const xs = Array(9).fill(5).concat([1000]); const out = new wickra.MedianAbsoluteDeviation(10).batch(xs); assert.ok(Math.abs(out[9]) < 1e-12); }); test('Autocorrelation of an alternating series is strongly negative at lag 1', () => { const xs = Array.from({ length: 20 }, (_, i) => (i % 2 === 0 ? -1 : 1)); const out = new wickra.Autocorrelation(10, 1).batch(xs); assert.ok(out[out.length - 1] < -0.5); }); test('HurstExponent of a monotone ramp is above 0.5', () => { const xs = Array.from({ length: 200 }, (_, i) => i); const out = new wickra.HurstExponent(100, 4).batch(xs); assert.ok(out[out.length - 1] > 0.5); }); test('Ichimoku classic warmup is 77 and tenkan emits at bar 9', () => { const ichi = new wickra.Ichimoku(9, 26, 52, 26); assert.equal(ichi.warmupPeriod(), 77); const n = 30; const h = Array.from({ length: n }, (_, i) => 100 + i + 2); const l = Array.from({ length: n }, (_, i) => 100 + i - 2); const c = Array.from({ length: n }, (_, i) => 100 + i + 1); const out = ichi.batch(h, l, c); for (let i = 0; i < 8; i++) { assert.ok(Number.isNaN(out[i * 5]), `tenkan should be NaN at bar ${i}`); } assert.ok(!Number.isNaN(out[8 * 5]), 'tenkan should be defined at bar 9'); }); test('HeikinAshi first bar seeds from real open and close', () => { const ha = new wickra.HeikinAshi(); const out = ha.update(10, 12, 9, 11); assert.ok(Math.abs(out.open - (10 + 11) / 2) < 1e-12); assert.ok(Math.abs(out.close - (10 + 12 + 9 + 11) / 4) < 1e-12); }); test('PercentageTrailingStop seeds and ratchets', () => { const s = new wickra.PercentageTrailingStop(10); assert.ok(Math.abs(s.update(100) - 90) < 1e-9); assert.ok(Math.abs(s.update(110) - 99) < 1e-9); }); test('RenkoTrailingStop only advances after a full block', () => { const s = new wickra.RenkoTrailingStop(1); assert.ok(Math.abs(s.update(100) - 99) < 1e-9); assert.ok(Math.abs(s.update(100.5) - 99) < 1e-9); assert.ok(Math.abs(s.update(101) - 100) < 1e-9); }); test('DonchianStop window extremes', () => { const out = new wickra.DonchianStop(5).batch([1, 2, 3, 4, 5], [0, 1, 2, 3, 4]); // [long0, short0, long1, short1, ...]: idx 8 (=4*2) = long_5th, idx 9 = short_5th. assert.ok(Math.abs(out[8] - 0) < 1e-9); assert.ok(Math.abs(out[9] - 5) < 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); for (let i = 9; i < out.length; i++) { assert.ok(Math.abs(out[i] - 100) < 1e-9, `RVIVolatility[${i}] = ${out[i]}`); } }); test('ParkinsonVolatility zero-range bars yield zero', () => { const n = 30; const h = Array(n).fill(10); const l = Array(n).fill(10); const out = new wickra.ParkinsonVolatility(14, 252).batch(h, l); for (let i = 13; i < n; i++) { assert.ok(Math.abs(out[i]) < 1e-12, `Parkinson[${i}] = ${out[i]}`); } }); test('GarmanKlassVolatility zero-movement bars yield zero', () => { const n = 30; const flat = Array(n).fill(10); const out = new wickra.GarmanKlassVolatility(14, 252).batch(flat, flat, flat, flat); for (let i = 13; i < n; i++) { assert.ok(Math.abs(out[i]) < 1e-12, `GK[${i}] = ${out[i]}`); } }); test('RogersSatchellVolatility zero-movement bars yield zero', () => { const n = 30; const flat = Array(n).fill(10); const out = new wickra.RogersSatchellVolatility(14, 252).batch(flat, flat, flat, flat); for (let i = 13; i < n; i++) { assert.ok(Math.abs(out[i]) < 1e-12, `RS[${i}] = ${out[i]}`); } }); test('YangZhangVolatility zero-movement bars yield zero', () => { const n = 30; const flat = Array(n).fill(10); const out = new wickra.YangZhangVolatility(14, 252).batch(flat, flat, flat, flat); for (let i = 14; i < n; i++) { assert.ok(Math.abs(out[i]) < 1e-12, `YZ[${i}] = ${out[i]}`); } }); test('ZeroLagMACD on a flat series converges to zero', () => { const out = new wickra.ZeroLagMACD(3, 5, 3).batch(Array(60).fill(42)); // Last interleaved row: macd, signal, histogram all 0. const n = 60; assert.ok(Math.abs(out[(n - 1) * 3]) < 1e-12); assert.ok(Math.abs(out[(n - 1) * 3 + 1]) < 1e-12); assert.ok(Math.abs(out[(n - 1) * 3 + 2]) < 1e-12); }); test('AwesomeOscillatorHistogram on a flat median converges to zero', () => { const n = 50; const out = new wickra.AwesomeOscillatorHistogram(3, 5, 3).batch( Array(n).fill(11), Array(n).fill(9), ); // warmup = 5 + 3 - 1 = 7. for (let i = 6; i < n; i++) assert.ok(Math.abs(out[i]) < 1e-12); }); test('STC on a flat series stays at zero', () => { const out = new wickra.STC(3, 5, 4, 0.5).batch(Array(60).fill(42)); // Latest values must be exactly zero. for (let i = out.length - 5; i < out.length; i++) { if (Number.isNaN(out[i])) continue; assert.equal(out[i], 0); } }); test('ElderImpulse on a flat series stays neutral (0)', () => { const out = new wickra.ElderImpulse(13, 12, 26, 9).batch(Array(120).fill(42)); for (let i = 0; i < out.length; i++) { if (Number.isNaN(out[i])) continue; assert.equal(out[i], 0); } }); test('CFO(5) on a perfectly linear series yields zero', () => { const prices = Array.from({ length: 20 }, (_, i) => (i + 1) * 2); const out = new wickra.CFO(5).batch(prices); for (let i = 4; i < 20; i++) assert.ok(Math.abs(out[i]) < 1e-9); }); test('APO(3, 5) on a flat series converges to zero', () => { const out = new wickra.APO(3, 5).batch(Array(30).fill(42)); for (let i = 0; i < 4; i++) assert.ok(Number.isNaN(out[i])); for (let i = 4; i < 30; i++) assert.ok(Math.abs(out[i]) < 1e-12); }); test('Inertia(3, 4) on a constant RVI series equals that RVI', () => { const n = 60; // Every bar (open, high, low, close) = (10, 11, 9, 10.5) -> RVI = 0.25. const out = new wickra.Inertia(3, 4).batch( Array(n).fill(10), Array(n).fill(11), Array(n).fill(9), Array(n).fill(10.5), ); for (let i = 5; i < n; i++) assert.ok(Math.abs(out[i] - 0.25) < 1e-12); }); test('ConnorsRSI stays bounded in [0, 100]', () => { const prices = Array.from({ length: 250 }, (_, i) => 100 + 20 * Math.sin(i * 0.12)); const out = new wickra.ConnorsRSI(3, 2, 100).batch(prices); for (let i = 0; i < out.length; i++) { if (Number.isNaN(out[i])) continue; assert.ok(out[i] >= 0 && out[i] <= 100, `out[${i}] = ${out[i]}`); } }); test('LaguerreRSI on a flat series stays at the neutral 50', () => { const out = new wickra.LaguerreRSI(0.5).batch(Array(40).fill(42)); for (let i = 0; i < out.length; i++) assert.ok(Math.abs(out[i] - 50) < 1e-12); }); test('SMI with close at range centre emits zero after warmup', () => { const n = 60; const out = new wickra.SMI(5, 3, 3).batch(Array(n).fill(11), Array(n).fill(9), Array(n).fill(10)); // warmup_period = 5 + 3 + 3 - 2 = 9. for (let i = 8; i < n; i++) assert.ok(Math.abs(out[i]) < 1e-12); }); test('KST on a flat series emits zero after warmup', () => { const kst = new wickra.KST(10, 15, 20, 30, 10, 10, 10, 15, 9); const n = 80; const out = kst.batch(Array(n).fill(42)); const warmup = kst.warmupPeriod(); for (let i = warmup - 1; i < n; i++) { assert.ok(Math.abs(out[i * 2]) < 1e-12, `kst[${i}] = ${out[i * 2]}`); assert.ok(Math.abs(out[i * 2 + 1]) < 1e-12, `signal[${i}] = ${out[i * 2 + 1]}`); } }); test('PGO(5) on a flat close emits zero after warmup', () => { const n = 20; const out = new wickra.PGO(5).batch(Array(n).fill(11), Array(n).fill(9), Array(n).fill(10)); for (let i = 0; i < 4; i++) assert.ok(Number.isNaN(out[i])); for (let i = 4; i < n; i++) assert.ok(Math.abs(out[i]) < 1e-12, `out[${i}] = ${out[i]}`); }); test('RVI(2) reference value on two bars', () => { // Bars (open, high, low, close): (10, 11, 9, 10.5), (10.5, 11.5, 10, 11). const out = new wickra.RVI(2).batch([10, 10.5], [11, 11.5], [9, 10], [10.5, 11]); assert.ok(Number.isNaN(out[0])); assert.ok(Math.abs(out[1] - 1 / 3.5) < 1e-12); }); test('EVWMA(2) reference values on [10, 20, 30] with volumes [1, 3, 1]', () => { const out = new wickra.EVWMA(2).batch([10, 20, 30], [1, 3, 1]); assert.ok(Number.isNaN(out[0])); assert.ok(Math.abs(out[1] - 20) < 1e-12); assert.ok(Math.abs(out[2] - 22.5) < 1e-12); }); test('Alligator on a flat median price seeds to that median', () => { const n = 30; const out = new wickra.Alligator(13, 8, 5).batch(Array(n).fill(11), Array(n).fill(9)); // All three SMMAs see median (11 + 9) / 2 = 10 every bar. for (let i = 12; i < n; i++) { assert.ok(Math.abs(out[i * 3] - 10) < 1e-12, `jaw at ${i}: ${out[i * 3]}`); assert.ok(Math.abs(out[i * 3 + 1] - 10) < 1e-12); assert.ok(Math.abs(out[i * 3 + 2] - 10) < 1e-12); } }); test('JMA on a flat series reproduces the constant', () => { const out = new wickra.JMA(14, 0, 2).batch(Array(30).fill(42)); for (let i = 0; i < 30; i++) assert.ok(Math.abs(out[i] - 42) < 1e-12); }); test('VIDYA on a flat series holds the seed', () => { const out = new wickra.VIDYA(14, 4).batch(Array(20).fill(42)); for (let i = 0; i < 4; i++) assert.ok(Number.isNaN(out[i])); for (let i = 4; i < 20; i++) assert.ok(Math.abs(out[i] - 42) < 1e-12); }); test('FRAMA pure uptrend hugs the latest close', () => { const out = new wickra.FRAMA(4).batch([1, 2, 3, 4, 5, 6, 7, 8]); assert.ok(Math.abs(out[out.length - 1] - 8) < 0.05); }); test('McGinleyDynamic(3) seeds with SMA and recurses on the next price', () => { // Seed = SMA([10, 20, 30]) = 20. On 40: ratio = 2, divisor = 0.6*3*16 = 28.8. const out = new wickra.McGinleyDynamic(3).batch([10, 20, 30, 40]); assert.ok(Number.isNaN(out[0]) && Number.isNaN(out[1])); assert.ok(Math.abs(out[2] - 20) < 1e-12); const expected = 20 + 20 / (0.6 * 3 * 16); assert.ok(Math.abs(out[3] - expected) < 1e-12); }); test('ALMA(3, 0.85, 6) reference value on [10, 20, 30]', () => { // m = 0.85 * 2 = 1.7; s = 3 / 6 = 0.5; 2*s^2 = 0.5. const out = new wickra.ALMA(3, 0.85, 6).batch([10, 20, 30]); assert.ok(Number.isNaN(out[0]) && Number.isNaN(out[1])); const w = [0, 1, 2].map((i) => Math.exp(-Math.pow(i - 1.7, 2) / 0.5)); const s = w[0] + w[1] + w[2]; const expected = (10 * w[0] + 20 * w[1] + 30 * w[2]) / s; assert.ok(Math.abs(out[2] - expected) < 1e-12); // The heavy offset toward the newest sample lifts the average above the // simple mean of 20. assert.ok(out[2] > 20); }); test('Doji signed mode encodes dragonfly/gravestone/neutral direction', () => { // Default: direction-less detection flag (+1 doji / 0 otherwise). const flag = new wickra.Doji(); assert.equal(flag.isSigned(), false); assert.equal(flag.update(10, 11, 9, 10), 1); // body 0, range 2 -> doji assert.equal(flag.update(10, 12, 10, 12), 0); // body == range -> not a doji // Signed: classify a detected doji by its body position within the range. const d = new wickra.Doji(true); assert.equal(d.isSigned(), true); assert.equal(d.update(10, 10.05, 6, 10), 1); // dragonfly -> bullish +1 assert.equal(d.update(10, 14, 9.95, 10), -1); // gravestone -> bearish -1 assert.equal(d.update(10, 12, 8, 10), 0); // long-legged -> neutral 0 assert.equal(d.update(10, 12, 10, 12), 0); // not a doji -> 0 }); test('order-book indicators reference values', () => { // Top-1: (3 - 1) / (3 + 1) = 0.5. assert.equal(new wickra.OrderBookImbalanceTop1().update([100], [3], [101], [1]), 0.5); // Top-2: bidDepth 3, askDepth 2 -> (3 - 2) / 5 = 0.2. assert.ok( Math.abs(new wickra.OrderBookImbalanceTopN(2).update([100, 99], [2, 1], [101, 102], [1, 1]) - 0.2) < 1e-12, ); // Full: bidDepth 1, askDepth 3 -> -0.5. assert.equal(new wickra.OrderBookImbalanceFull().update([100], [1], [101, 102], [2, 1]), -0.5); // Microprice: (100*3 + 101*1) / 4 = 100.25. assert.equal(new wickra.Microprice().update([100], [1], [101], [3]), 100.25); // Quoted spread: 1 / 100.5 * 10000 ≈ 99.5025 bps. assert.ok(Math.abs(new wickra.QuotedSpread().update([100], [1], [101], [1]) - 99.50248756) < 1e-6); // Depth slope: each side distances 1,2 -> cumulative 1,3 -> OLS slope 2. assert.ok(Math.abs(new wickra.DepthSlope().update([99, 98], [1, 2], [101, 102], [1, 2]) - 2.0) < 1e-9); // Single level per side -> no slope -> 0. assert.equal(new wickra.DepthSlope().update([100], [1], [101], [1]), 0.0); }); test('order-book streaming update matches batch', () => { const snaps = Array.from({ length: 30 }, (_, i) => ({ bidPx: [100, 99], bidSz: [1 + (i % 5), 1], askPx: [101, 102], askSz: [1 + ((i + 1) % 3), 1], })); const batch = new wickra.Microprice().batch(snaps); const streamer = new wickra.Microprice(); 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(Math.abs(s - batch[i]) < 1e-12, `mismatch at ${i}: ${s} vs ${batch[i]}`); } }); test('order-book TopN rejects zero levels', () => { assert.throws(() => new wickra.OrderBookImbalanceTopN(0)); }); 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)); }); 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); // Sell at 99.95 vs mid 100.0: 2 * -1 * (99.95 - 100) / 100 * 10000 = 10 bps. assert.ok(Math.abs(new wickra.EffectiveSpread().update(99.95, 1, false, 100.0) - 10.0) < 1e-9); // A buy filled below the mid is price improvement -> negative. assert.ok(new wickra.EffectiveSpread().update(99.95, 1, true, 100.0) < 0.0); }); test('price-impact streaming update matches batch', () => { const n = 30; const mid = Array.from({ length: n }, (_, i) => 100 + 0.25 * Math.sin(i * 0.5)); const isBuy = Array.from({ length: n }, (_, i) => i % 3 !== 0); const price = Array.from({ length: n }, (_, i) => mid[i] + (isBuy[i] ? 0.03 : -0.03)); const size = Array.from({ length: n }, (_, i) => 1 + (i % 4)); const batch = new wickra.EffectiveSpread().batch(price, size, isBuy, mid); const streamer = new wickra.EffectiveSpread(); assert.equal(batch.length, n); for (let i = 0; i < n; i++) { const s = streamer.update(price[i], size[i], isBuy[i], mid[i]); assert.ok(Math.abs(s - batch[i]) < 1e-9, `mismatch at ${i}: ${s} vs ${batch[i]}`); } }); test('realized spread resolves against the future mid', () => { const rs = new wickra.RealizedSpread(1); assert.equal(rs.update(100.10, 1, true, 100.0), null); // buffered // 2 * (+1) * (100.10 - 100.20) / 100.0 * 10000 = -20 bps. assert.ok(Math.abs(rs.update(99.90, 1, false, 100.20) - -20.0) < 1e-9); }); test('realized spread streaming update matches batch', () => { const n = 30; const mid = Array.from({ length: n }, (_, i) => 100 + 0.25 * Math.sin(i * 0.5)); const isBuy = Array.from({ length: n }, (_, i) => i % 3 !== 0); const price = Array.from({ length: n }, (_, i) => mid[i] + (isBuy[i] ? 0.03 : -0.03)); const size = Array.from({ length: n }, (_, i) => 1 + (i % 4)); const batch = new wickra.RealizedSpread(4).batch(price, size, isBuy, mid); const streamer = new wickra.RealizedSpread(4); assert.equal(batch.length, n); for (let i = 0; i < n; i++) { const s = streamer.update(price[i], size[i], isBuy[i], mid[i]); const got = s === null ? NaN : s; assert.ok( (Number.isNaN(got) && Number.isNaN(batch[i])) || Math.abs(got - batch[i]) < 1e-9, `mismatch at ${i}: ${got} vs ${batch[i]}`, ); } }); test("kyle's lambda recovers a constant price-impact slope", () => { // Each trade moves the mid by exactly 0.5 per unit of signed volume. const impact = 0.5; let mid = 100; const price = []; const size = []; const isBuy = []; const mids = []; for (let i = 0; i < 20; i++) { const buy = i % 2 === 0; const sz = 1 + (i % 3); const signed = buy ? sz : -sz; mid += impact * signed; price.push(mid); size.push(sz); isBuy.push(buy); mids.push(mid); } const out = new wickra.KylesLambda(6).batch(price, size, isBuy, mids); assert.ok(Math.abs(out[out.length - 1] - 0.5) < 1e-9); }); test('price-impact rejects bad input', () => { assert.throws(() => new wickra.EffectiveSpread().update(100, 1, true, 0)); assert.throws(() => new wickra.RealizedSpread(0)); assert.throws(() => new wickra.KylesLambda(1)); }); test('footprint buckets buy and sell volume per price level', () => { const fp = new wickra.Footprint(1.0); fp.update(100.2, 2, true); // bucket 100 -> ask 2 fp.update(100.7, 3, false); // bucket 101 -> bid 3 const out = fp.update(100.1, 1, true); // bucket 100 -> ask 3 assert.equal(out.length, 2); assert.deepEqual( { price: out[0].price, bidVol: out[0].bidVol, askVol: out[0].askVol }, { price: 100.0, bidVol: 0.0, askVol: 3.0 }, ); assert.deepEqual( { price: out[1].price, bidVol: out[1].bidVol, askVol: out[1].askVol }, { price: 101.0, bidVol: 3.0, askVol: 0.0 }, ); }); test('footprint streaming update matches batch and rejects bad tick', () => { const n = 12; const price = Array.from({ length: n }, (_, i) => 100 + (i % 5) * 0.3); const size = Array.from({ length: n }, (_, i) => 1 + (i % 3)); const isBuy = Array.from({ length: n }, (_, i) => i % 2 === 0); const batch = new wickra.Footprint(1.0).batch(price, size, isBuy); const streamer = new wickra.Footprint(1.0); assert.equal(batch.length, n); for (let i = 0; i < n; i++) { const s = streamer.update(price[i], size[i], isBuy[i]); assert.deepEqual(s, batch[i], `mismatch at ${i}`); } assert.throws(() => new wickra.Footprint(0)); }); test('derivatives indicators reference values', () => { // Funding rate passes through (and may be negative). assert.equal(new wickra.FundingRate().update(0.0001), 0.0001); assert.equal(new wickra.FundingRate().update(-0.0003), -0.0003); // Rolling mean: window [0.001, 0.003] -> 0.002. const frm = new wickra.FundingRateMean(2); assert.equal(frm.update(0.001), null); // warming up assert.ok(Math.abs(frm.update(0.003) - 0.002) < 1e-12); // Z-score: window [0.001, 0.003] -> +1. const z = new wickra.FundingRateZScore(2); assert.equal(z.update(0.001), null); // warming up assert.ok(Math.abs(z.update(0.003) - 1.0) < 1e-9); // Basis: mark 100.5 vs index 100.0 -> 0.005. assert.ok(Math.abs(new wickra.FundingBasis().update(100.5, 100.0) - 0.005) < 1e-12); // OI delta: seeds then emits the change. const oid = new wickra.OpenInterestDelta(); assert.equal(oid.update(1000), null); assert.equal(oid.update(1250), 250); assert.equal(oid.update(1100), -150); }); test('derivatives streaming update matches batch', () => { const n = 30; const rate = Array.from({ length: n }, (_, i) => 0.0001 * Math.sin(i * 0.3)); const batch = new wickra.FundingRateMean(5).batch(rate); const streamer = new wickra.FundingRateMean(5); assert.equal(batch.length, n); for (let i = 0; i < n; i++) { const s = streamer.update(rate[i]); assert.ok( (s === null && Number.isNaN(batch[i])) || Math.abs(s - batch[i]) < 1e-12, `mismatch at ${i}: ${s} vs ${batch[i]}`, ); } }); test('derivatives reject bad input', () => { assert.throws(() => new wickra.FundingRateMean(0)); assert.throws(() => new wickra.FundingRateZScore(0)); assert.throws(() => new wickra.FundingBasis().update(100, 0)); }); test('OI / flow / liquidation indicators reference values', () => { // OI +10% while price flat -> divergence +0.1. const div = new wickra.OIPriceDivergence(1); assert.equal(div.update(1000, 100), null); // warming up assert.ok(Math.abs(div.update(1100, 100) - 0.1) < 1e-12); // OI-weighted: (100·10 + 110·30) / 40 = 107.5. const oiw = new wickra.OIWeighted(); assert.equal(oiw.update(100, 10), 100); assert.ok(Math.abs(oiw.update(110, 30) - 107.5) < 1e-12); // Long/short ratio. assert.ok(Math.abs(new wickra.LongShortRatio().update(600, 400) - 1.5) < 1e-12); assert.equal(new wickra.LongShortRatio().update(600, 0), 0); // Taker buy/sell ratio. assert.ok(Math.abs(new wickra.TakerBuySellRatio().update(60, 40) - 1.5) < 1e-12); assert.equal(new wickra.TakerBuySellRatio().update(60, 0), 0); // Liquidation features object. const liq = new wickra.LiquidationFeatures().update(30, 10); assert.equal(liq.net, 20); assert.equal(liq.total, 40); assert.equal(liq.imbalance, 0.5); }); test('liquidation features batch is flat n*5', () => { const longLiq = [10, 0, 30]; const shortLiq = [5, 20, 0]; const batch = new wickra.LiquidationFeatures().batch(longLiq, shortLiq); assert.equal(batch.length, 15); // Row 0: long 10, short 5, net 5, total 15. assert.equal(batch[0], 10); assert.equal(batch[1], 5); assert.equal(batch[2], 5); assert.equal(batch[3], 15); }); test('OI flow rejects bad input', () => { assert.throws(() => new wickra.OIPriceDivergence(0)); assert.throws(() => new wickra.OIWeighted().update(0, 100)); }); test('basis & calendar-spread reference values', () => { // futures 102 vs index 100 -> 0.02 contango. assert.ok(Math.abs(new wickra.TermStructureBasis().update(102, 100) - 0.02) < 1e-12); assert.ok(Math.abs(new wickra.TermStructureBasis().update(98, 100) + 0.02) < 1e-12); // futures 101 vs perpetual mark 100 -> 0.01. assert.ok(Math.abs(new wickra.CalendarSpread().update(101, 100) - 0.01) < 1e-12); }); test('basis streaming update matches batch', () => { const n = 20; const index = Array.from({ length: n }, (_, i) => 100 + Math.sin(i * 0.2)); const futures = Array.from({ length: n }, (_, i) => index[i] + 0.5); const batch = new wickra.TermStructureBasis().batch(futures, index); const streamer = new wickra.TermStructureBasis(); assert.equal(batch.length, n); for (let i = 0; i < n; i++) { assert.ok(Math.abs(streamer.update(futures[i], index[i]) - batch[i]) < 1e-12); } }); test('basis rejects bad input', () => { assert.throws(() => new wickra.TermStructureBasis().update(100, 0)); assert.throws(() => new wickra.CalendarSpread().update(100, 0)); });