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
@@ -391,6 +391,10 @@ const multi = {
AutoFib: { make: () => new wickra.AutoFib(), fields: ['level0', 'level236', 'level382', 'level500', 'level618', 'level786', 'level1000'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
GoldenPocket: { make: () => new wickra.GoldenPocket(), fields: ['low', 'mid', 'high'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
FibConfluence: { make: () => new wickra.FibConfluence(), fields: ['price', 'strength'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
FibFan: { make: () => new wickra.FibFan(), fields: ['fan382', 'fan500', 'fan618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
FibArcs: { make: () => new wickra.FibArcs(), fields: ['arc382', 'arc500', 'arc618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
FibChannel: { make: () => new wickra.FibChannel(), fields: ['base', 'level618', 'level1000', 'level1618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
FibTimeZones: { make: () => new wickra.FibTimeZones(), fields: ['onZone', 'barsToNext'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
};
for (const [name, d] of Object.entries(multi)) {
+56
View File
@@ -402,6 +402,26 @@ export interface FibConfluenceValue {
price: number
strength: number
}
export interface FibFanValue {
fan382: number
fan500: number
fan618: number
}
export interface FibArcsValue {
arc382: number
arc500: number
arc618: number
}
export interface FibChannelValue {
base: number
level618: number
level1000: number
level1618: number
}
export interface FibTimeZonesValue {
onZone: number
barsToNext: number
}
export type SmaNode = SMA
export declare class SMA {
constructor(period: number)
@@ -3926,3 +3946,39 @@ export declare class FibConfluence {
isReady(): boolean
warmupPeriod(): number
}
export type FibFanNode = FibFan
export declare class FibFan {
constructor()
update(high: number, low: number): FibFanValue | null
batch(high: Array<number>, low: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type FibArcsNode = FibArcs
export declare class FibArcs {
constructor()
update(high: number, low: number): FibArcsValue | null
batch(high: Array<number>, low: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type FibChannelNode = FibChannel
export declare class FibChannel {
constructor()
update(high: number, low: number): FibChannelValue | null
batch(high: Array<number>, low: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type FibTimeZonesNode = FibTimeZones
export declare class FibTimeZones {
constructor()
update(high: number, low: number): FibTimeZonesValue | null
batch(high: Array<number>, low: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
File diff suppressed because one or more lines are too long
+276
View File
@@ -14482,3 +14482,279 @@ impl Default for FibConfluenceNode {
Self::new()
}
}
#[napi(object)]
pub struct FibFanValue {
pub fan_382: f64,
pub fan_500: f64,
pub fan_618: f64,
}
#[napi(js_name = "FibFan")]
pub struct FibFanNode {
inner: wc::FibFan,
}
#[napi]
impl FibFanNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::FibFan::new(),
}
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibFanValue>> {
Ok(self
.inner
.update(swing_cnd(high, low)?)
.map(|o| FibFanValue {
fan_382: o.fan_382,
fan_500: o.fan_500,
fan_618: o.fan_618,
}))
}
#[napi]
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
if high.len() != low.len() {
return Err(NapiError::from_reason(
"high and low must be equal length".to_string(),
));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 3];
for i in 0..n {
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
out[i * 3] = o.fan_382;
out[i * 3 + 1] = o.fan_500;
out[i * 3 + 2] = o.fan_618;
}
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
impl Default for FibFanNode {
fn default() -> Self {
Self::new()
}
}
#[napi(object)]
pub struct FibArcsValue {
pub arc_382: f64,
pub arc_500: f64,
pub arc_618: f64,
}
#[napi(js_name = "FibArcs")]
pub struct FibArcsNode {
inner: wc::FibArcs,
}
#[napi]
impl FibArcsNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::FibArcs::new(),
}
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibArcsValue>> {
Ok(self
.inner
.update(swing_cnd(high, low)?)
.map(|o| FibArcsValue {
arc_382: o.arc_382,
arc_500: o.arc_500,
arc_618: o.arc_618,
}))
}
#[napi]
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
if high.len() != low.len() {
return Err(NapiError::from_reason(
"high and low must be equal length".to_string(),
));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 3];
for i in 0..n {
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
out[i * 3] = o.arc_382;
out[i * 3 + 1] = o.arc_500;
out[i * 3 + 2] = o.arc_618;
}
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
impl Default for FibArcsNode {
fn default() -> Self {
Self::new()
}
}
#[napi(object)]
pub struct FibChannelValue {
pub base: f64,
pub level_618: f64,
pub level_1000: f64,
pub level_1618: f64,
}
#[napi(js_name = "FibChannel")]
pub struct FibChannelNode {
inner: wc::FibChannel,
}
#[napi]
impl FibChannelNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::FibChannel::new(),
}
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibChannelValue>> {
Ok(self
.inner
.update(swing_cnd(high, low)?)
.map(|o| FibChannelValue {
base: o.base,
level_618: o.level_618,
level_1000: o.level_1000,
level_1618: o.level_1618,
}))
}
#[napi]
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
if high.len() != low.len() {
return Err(NapiError::from_reason(
"high and low must be equal length".to_string(),
));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 4];
for i in 0..n {
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
out[i * 4] = o.base;
out[i * 4 + 1] = o.level_618;
out[i * 4 + 2] = o.level_1000;
out[i * 4 + 3] = o.level_1618;
}
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
impl Default for FibChannelNode {
fn default() -> Self {
Self::new()
}
}
#[napi(object)]
pub struct FibTimeZonesValue {
pub on_zone: f64,
pub bars_to_next: f64,
}
#[napi(js_name = "FibTimeZones")]
pub struct FibTimeZonesNode {
inner: wc::FibTimeZones,
}
#[napi]
impl FibTimeZonesNode {
#[napi(constructor)]
pub fn new() -> Self {
Self {
inner: wc::FibTimeZones::new(),
}
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<FibTimeZonesValue>> {
Ok(self
.inner
.update(swing_cnd(high, low)?)
.map(|o| FibTimeZonesValue {
on_zone: o.on_zone,
bars_to_next: o.bars_to_next,
}))
}
#[napi]
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
if high.len() != low.len() {
return Err(NapiError::from_reason(
"high and low must be equal length".to_string(),
));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
if let Some(o) = self.inner.update(swing_cnd(high[i], low[i])?) {
out[i * 2] = o.on_zone;
out[i * 2 + 1] = o.bars_to_next;
}
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
impl Default for FibTimeZonesNode {
fn default() -> Self {
Self::new()
}
}