feat(family-13): add Ichimoku + Heikin-Ashi (#50)
Two new indicators in a brand-new "Ichimoku & alternative charts" family: - `Ichimoku` (Ichimoku Kinko Hyo): the full five-line cloud system (Tenkan-sen, Kijun-sen, Senkou Span A/B, Chikou Span). Classic (9, 26, 52, 26) defaults; configurable. Forward displacement is handled in an O(1) ring buffer so the visible Senkou A/B at bar n are the values computed at bar n-displacement. - `HeikinAshi`: recursive candle smoothing transform emitting a four-field synthetic candle. Seeds ha_open from (open+close)/2 on the first bar. Touchpoints: core + unit tests, mod.rs/lib.rs re-exports, Python + Node + WASM bindings (multi-output via PyArray2 / interleaved Vec<f64> / Object+Float64Array), Python tests across smoke/new-indicators/ input-validation, Node parity tests, fuzz target (Candle), benches, README family table + counter (71 -> 73, 8 -> 9 families), CHANGELOG. Note: Renko, Kagi, and Point & Figure from the family-13 ideas list are intentionally skipped. They are bar generators (the bar boundary is defined by price moves, not by a fixed time interval) rather than indicators that consume a candle stream, and belong in wickra-data as candle/tick transforms alongside the existing tick-to-candle aggregator and resampler.
This commit is contained in:
@@ -231,6 +231,9 @@ const multi = {
|
||||
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)) {
|
||||
@@ -357,6 +360,27 @@ test('LinRegAngle of a unit-slope series is 45 degrees', () => {
|
||||
assert.ok(Math.abs(out[4] - 45) < 1e-9);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user