feat(indicators): A5b Fibonacci tools (geometric) (#172)

Completes the **Fibonacci** family with the four geometric/time tools (catalogue 373 -> 377). All extend the internal `pattern_swing` ZigZag tracker with a per-pivot bar index and a current-bar counter (additive — the chart/harmonic detectors are unaffected), and emit `Candle -> struct` outputs via custom Python/Node/WASM bindings.

| Tool | Output |
|------|--------|
| `FibFan` | three trendlines fanning from a swing start through its 38.2/50/61.8% retracement levels, extended to the current bar |
| `FibArcs` | semicircular retracement levels centred on the swing end, normalised by the leg's bar-width (chart-scale-free) |
| `FibChannel` | a sloped base trendline plus parallel lines at Fibonacci multiples of the channel width |
| `FibTimeZones` | markers at Fibonacci bar-distances (1/2/3/5/8/...) from the latest swing pivot |

The geometric tools are novel as streaming indicators; each normalises its geometry to the swing leg's bar-width so the output is chart-scale-free. Formulas are documented in each module and deep-dive.

Fully wired: core (100% unit-tested branches incl. the new `pattern_swing` bar tracking), Python/Node/WASM struct bindings, fuzz, reference + streaming-vs-batch tests.

Verification: `cargo test --workspace` green, clippy `-D warnings` clean, node 454 tests, python 768 tests.
This commit is contained in:
kingchenc
2026-06-04 01:12:09 +02:00
committed by GitHub
parent ea9da12d86
commit 5a1d607807
19 changed files with 1757 additions and 13 deletions
@@ -860,6 +860,26 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
# --- Candle-input, multi-output indicators --------------------------------
MULTI = {
"FibFan": (
lambda: ta.FibFan(),
lambda ind, h, l, c, v: ind.batch(h, l),
3,
),
"FibArcs": (
lambda: ta.FibArcs(),
lambda ind, h, l, c, v: ind.batch(h, l),
3,
),
"FibChannel": (
lambda: ta.FibChannel(),
lambda ind, h, l, c, v: ind.batch(h, l),
4,
),
"FibTimeZones": (
lambda: ta.FibTimeZones(),
lambda ind, h, l, c, v: ind.batch(h, l),
2,
),
"FibRetracement": (
lambda: ta.FibRetracement(),
lambda ind, h, l, c, v: ind.batch(h, l),
@@ -2652,6 +2672,41 @@ def test_fib_confluence_reference():
assert t.update((101.0, 160.0, 101.0, 101.0, 1.0, 2)) is None
assert t.update((144.0, 158.4, 144.0, 144.0, 1.0, 3)) == pytest.approx((137.64, 2.0))
def test_fib_fan_reference():
t = ta.FibFan()
assert t.update((199.0, 200.0, 199.0, 199.0, 1.0, 0)) is None
assert t.update((160.0, 190.0, 160.0, 160.0, 1.0, 1)) is None
assert t.update((100.0, 150.0, 100.0, 100.0, 1.0, 2)) is None
assert t.update((105.0, 110.0, 105.0, 105.0, 1.0, 3)) == pytest.approx((142.7, 125.0, 107.3))
def test_fib_arcs_reference():
t = ta.FibArcs()
assert t.update((199.0, 200.0, 199.0, 199.0, 1.0, 0)) is None
assert t.update((160.0, 190.0, 160.0, 160.0, 1.0, 1)) is None
assert t.update((100.0, 150.0, 100.0, 100.0, 1.0, 2)) is None
assert t.update((105.0, 110.0, 105.0, 105.0, 1.0, 3)) == pytest.approx((133.082181, 143.30127, 153.52037))
def test_fib_channel_reference():
t = ta.FibChannel()
assert t.update((199.0, 200.0, 199.0, 199.0, 1.0, 0)) is None
assert t.update((100.0, 190.0, 100.0, 100.0, 1.0, 1)) is None
assert t.update((108.0, 110.0, 108.0, 108.0, 1.0, 2)) is None
assert t.update((210.0, 220.0, 210.0, 210.0, 1.0, 3)) is None
assert t.update((150.0, 200.0, 150.0, 150.0, 1.0, 4)) == pytest.approx((226.666667, 160.746667, 120.0, 54.08))
def test_fib_time_zones_reference():
t = ta.FibTimeZones()
assert t.update((199.0, 200.0, 199.0, 199.0, 1.0, 0)) is None
assert t.update((150.0, 190.0, 150.0, 150.0, 1.0, 1)) == pytest.approx((1.0, 1.0))
assert t.update((151.0, 155.0, 151.0, 151.0, 1.0, 2)) == pytest.approx((1.0, 1.0))
assert t.update((151.0, 155.0, 151.0, 151.0, 1.0, 3)) == pytest.approx((1.0, 2.0))
assert t.update((151.0, 155.0, 151.0, 151.0, 1.0, 4)) == pytest.approx((0.0, 1.0))
assert t.update((151.0, 155.0, 151.0, 151.0, 1.0, 5)) == pytest.approx((1.0, 3.0))
# --- Lifecycle ------------------------------------------------------------