d43bc9ddf3
* feat: add doji-star, gap, high-wave and hikkake candlestick patterns Five patterns, all `Input = Candle`, `Output = f64`: - Evening Doji Star (CDLEVENINGDOJISTAR) — bearish top reversal: long white bar, a doji gapping up, then a black bar closing deep into the first body; -1 (penetration configurable, default 0.3). - Morning Doji Star (CDLMORNINGDOJISTAR) — bullish bottom reversal mirror; +1. - Gap Side-by-Side White (CDLGAPSIDESIDEWHITE) — two similar white candles opening side by side after a gap, a continuation; gap up +1, gap down -1. - High-Wave (CDLHIGHWAVE) — a small body with very long shadows on both sides, an extreme indecision flag; +1 on detection. - Hikkake (CDLHIKKAKE) — an inside bar followed by a failed breakout (a trap); bullish +1, bearish -1. Counter 259 -> 264 (mod-count == lib counted block; FAMILIES total 254 -> 259). * chore: sync indicator count to 264 --------- Co-authored-by: wickra-bot <wickra-bot@users.noreply.github.com>
1223 lines
66 KiB
JavaScript
1223 lines
66 KiB
JavaScript
// 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) },
|
||
};
|
||
|
||
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));
|
||
});
|