F13c: restructure the indicator catalogue into eight families
The original taxonomy was four classical families plus a statistics group, with the F1-F12 expansion slotted in as sub-categories. This regroups the whole 71-indicator catalogue into eight top-level families, each with at least five members: Moving Averages (12), Momentum Oscillators (13), Trend & Directional (9), Price Oscillators (5), Volatility & Bands (12), Trailing Stops (5), Volume (9), Price Statistics (7). - Wiki: docs/wiki/indicators/ reorganised into eight family folders; all 71 indicator pages moved with `git mv`. Every internal cross-link is normalised to `../<family>/Indicator-X.md`, each page's `Family` field is set to its new family, and two pre-existing `../Indicator-Chaining.md` links (should have been `../../`) are corrected. A link check confirms every relative wiki link resolves. - Indicators-Overview.md fully rewritten around the eight families; Home.md indicator reference and the README family table follow suit. - Warmup-Periods.md gains the eight F13 indicators; CHANGELOG records the 46-indicator expansion (25 -> 71) and the eight-family taxonomy. - Tests: Node indicators.test.js and Python test_new_indicators.py cover all eight new indicators (Node 91/91, Python 117/117 green). cargo fmt + clippy (core/wickra/data/wasm/node) clean; 508 core tests, 25 data tests and 74 doctests green.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Comprehensive tests for the Wickra Node bindings: streaming-vs-batch
|
||||
// equivalence, reference values, and lifecycle methods across all 63
|
||||
// equivalence, reference values, and lifecycle methods across all 71
|
||||
// indicators. Ported from the Python test_streaming_vs_batch / test_known_values
|
||||
// suites.
|
||||
|
||||
@@ -13,6 +13,7 @@ const close = Array.from({ length: N }, (_, i) => 100 + Math.sin(i * 0.2) * 10 +
|
||||
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);
|
||||
@@ -55,6 +56,9 @@ const scalarFactories = {
|
||||
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),
|
||||
};
|
||||
|
||||
for (const [name, make] of Object.entries(scalarFactories)) {
|
||||
@@ -95,6 +99,11 @@ const candleScalar = {
|
||||
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) },
|
||||
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) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(candleScalar)) {
|
||||
@@ -232,3 +241,19 @@ test('SuperTrend flat market holds the lower band and an uptrend', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -60,6 +60,9 @@ SCALAR = [
|
||||
(ta.PercentB, (20, 2.0)),
|
||||
(ta.LinearRegression, (14,)),
|
||||
(ta.LinRegSlope, (14,)),
|
||||
(ta.VerticalHorizontalFilter, (28,)),
|
||||
(ta.ZScore, (20,)),
|
||||
(ta.LinRegAngle, (14,)),
|
||||
]
|
||||
|
||||
|
||||
@@ -131,6 +134,28 @@ CANDLE_SCALAR = {
|
||||
lambda: ta.WeightedClose(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"AcceleratorOscillator": (
|
||||
lambda: ta.AcceleratorOscillator(5, 34, 5),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
),
|
||||
"BalanceOfPower": (
|
||||
# The streaming 6-tuple feeds open == close, so batch matches with
|
||||
# the close column standing in for open.
|
||||
lambda: ta.BalanceOfPower(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ChoppinessIndex": (
|
||||
lambda: ta.ChoppinessIndex(14),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"TrueRange": (
|
||||
lambda: ta.TrueRange(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"ChaikinVolatility": (
|
||||
lambda: ta.ChaikinVolatility(10, 10),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -239,6 +264,31 @@ def test_linreg_slope_reference():
|
||||
assert out[2] == pytest.approx(4.0)
|
||||
|
||||
|
||||
def test_balance_of_power_reference():
|
||||
# (close - open) / (high - low) = (12 - 10) / (14 - 10) = 0.5.
|
||||
bop = ta.BalanceOfPower()
|
||||
assert bop.update((10.0, 14.0, 10.0, 12.0, 1.0, 0)) == pytest.approx(0.5)
|
||||
|
||||
|
||||
def test_true_range_reference():
|
||||
tr = ta.TrueRange()
|
||||
assert tr.update((11.0, 12.0, 8.0, 11.0, 1.0, 0)) == pytest.approx(4.0)
|
||||
assert tr.update((9.5, 10.0, 9.0, 9.5, 1.0, 1)) == pytest.approx(2.0)
|
||||
|
||||
|
||||
def test_linreg_angle_reference():
|
||||
# A series rising by 1 per step has slope 1, and atan(1) = 45 degrees.
|
||||
out = ta.LinRegAngle(5).batch(np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]))
|
||||
assert out[4] == pytest.approx(45.0)
|
||||
|
||||
|
||||
def test_z_score_reference():
|
||||
# Window [1, 3]: mean 2, population stddev 1; latest 3 -> z = 1.
|
||||
out = ta.ZScore(2).batch(np.array([1.0, 3.0]))
|
||||
assert math.isnan(out[0])
|
||||
assert out[1] == pytest.approx(1.0)
|
||||
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user