feat(indicators): A5a Fibonacci tools (price-level) (#171)

Adds the six price-level Fibonacci tools as a new **Fibonacci** family (catalogue 367 -> 373, twenty-four families). All build on the internal `pattern_swing` ZigZag tracker, are parameter-free (baked 5% swing threshold), and emit `Candle -> struct` outputs via custom Python/Node/WASM bindings.

| Tool | Output |
|------|--------|
| `FibRetracement` | seven levels (0/23.6/38.2/50/61.8/78.6/100%) of the last swing leg |
| `FibExtension` | five extension ratios (127.2/141.4/161.8/200/261.8%) projected beyond the leg |
| `FibProjection` | A-B-C measured-move target zone (61.8/100/161.8/261.8%) |
| `AutoFib` | retracement anchored on the dominant (largest-magnitude) recent leg |
| `GoldenPocket` | the 0.618-0.65 optimal-trade-entry band (low/mid/high) |
| `FibConfluence` | densest cluster of retracement levels across recent legs (price + strength) |

Fully wired: core (100% unit-tested branches), Python/Node/WASM struct bindings, fuzz driver, reference + streaming-vs-batch tests, README/docs counter. The four geometric/time tools (Fan, Arcs, Channel, Time Zones) follow in A5b.

Verification: `cargo test --workspace` green, clippy `-D warnings` clean, node 450 tests, python 760 tests.
This commit is contained in:
kingchenc
2026-06-04 00:47:00 +02:00
committed by GitHub
parent 8115d3b33d
commit 716eb40206
20 changed files with 2615 additions and 18 deletions
@@ -0,0 +1,176 @@
//! Auto-Fibonacci — retracement of the most significant recent swing leg.
use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// How many recent pivots to consider when picking the dominant leg.
const PIVOT_HISTORY: usize = 6;
/// The seven canonical retracement ratios, in ascending order.
const RATIOS: [f64; 7] = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0];
/// Auto-Fibonacci retracement levels for the dominant recent swing leg.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AutoFibOutput {
/// 0.0% — the dominant leg's end.
pub level_0: f64,
/// 23.6% retracement.
pub level_236: f64,
/// 38.2% retracement.
pub level_382: f64,
/// 50% retracement.
pub level_500: f64,
/// 61.8% retracement.
pub level_618: f64,
/// 78.6% retracement.
pub level_786: f64,
/// 100% — the dominant leg's start.
pub level_1000: f64,
}
/// Auto-Fibonacci (`AutoFib`).
///
/// Like [`crate::indicators::FibRetracement`], but instead of always using the
/// immediate last leg it scans the last six confirmed pivots and anchors the
/// retracement on the single largest-magnitude leg among them — the dominant
/// swing the market is most likely respecting.
///
/// Parameter-free; construction is infallible. Returns `None` until two pivots
/// have confirmed.
///
/// See `crates/wickra-core/src/indicators/auto_fib.rs`.
#[derive(Debug, Clone)]
pub struct AutoFib {
swing: SwingTracker,
}
impl AutoFib {
/// Construct a new Auto-Fibonacci tracker.
#[must_use]
pub const fn new() -> Self {
Self {
swing: SwingTracker::new(SWING_THRESHOLD, PIVOT_HISTORY),
}
}
fn levels(&self) -> Option<AutoFibOutput> {
let dominant = self.swing.pivots().windows(2).max_by(|x, y| {
(x[0].price - x[1].price)
.abs()
.total_cmp(&(y[0].price - y[1].price).abs())
})?;
let (start, end) = (dominant[0].price, dominant[1].price);
let level = |r: f64| end + r * (start - end);
Some(AutoFibOutput {
level_0: level(RATIOS[0]),
level_236: level(RATIOS[1]),
level_382: level(RATIOS[2]),
level_500: level(RATIOS[3]),
level_618: level(RATIOS[4]),
level_786: level(RATIOS[5]),
level_1000: level(RATIOS[6]),
})
}
}
impl Default for AutoFib {
fn default() -> Self {
Self::new()
}
}
impl Indicator for AutoFib {
type Input = Candle;
type Output = AutoFibOutput;
fn update(&mut self, candle: Candle) -> Option<AutoFibOutput> {
self.swing.update(candle);
self.levels()
}
fn reset(&mut self) {
self.swing.reset();
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.swing.pivots().len() >= 2
}
fn name(&self) -> &'static str {
"AutoFib"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicators::pattern_swing::candles_for_pivots;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn accessors_and_metadata() {
let indicator = AutoFib::new();
assert_eq!(indicator.name(), "AutoFib");
assert_eq!(indicator.warmup_period(), 2);
assert!(!indicator.is_ready());
assert!(!AutoFib::default().is_ready());
}
#[test]
fn no_output_before_two_pivots() {
let mut indicator = AutoFib::new();
let outputs: Vec<_> = candles_for_pivots(&[120.0])
.into_iter()
.map(|c| indicator.update(c))
.collect();
assert!(outputs.iter().all(Option::is_none));
}
#[test]
fn anchors_on_the_largest_leg() {
// Pivots: 130 -> 120 (small, 10) -> 220 (large, 100) -> 200 (small, 20).
// The dominant leg is 120 -> 220; its retracement spans [120, 220].
let mut indicator = AutoFib::new();
let mut last = None;
for candle in candles_for_pivots(&[130.0, 120.0, 220.0, 200.0]) {
last = indicator.update(candle);
}
let v = last.unwrap();
assert!(indicator.is_ready());
// Largest leg 120 -> 220: 0% on 220 (end), 100% on 120 (start).
assert_relative_eq!(v.level_0, 220.0);
assert_relative_eq!(v.level_1000, 120.0);
assert_relative_eq!(v.level_500, 170.0);
assert_relative_eq!(v.level_618, 220.0 + 0.618 * (120.0 - 220.0));
}
#[test]
fn reset_clears_state() {
let mut indicator = AutoFib::new();
for candle in candles_for_pivots(&[200.0, 100.0]) {
let _ = indicator.update(candle);
}
assert!(indicator.is_ready());
indicator.reset();
assert!(!indicator.is_ready());
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
assert!(indicator.update(c).is_none());
}
#[test]
fn batch_equals_streaming() {
let candles = candles_for_pivots(&[130.0, 120.0, 220.0, 200.0]);
let mut a = AutoFib::new();
let mut b = AutoFib::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,181 @@
//! Fibonacci Confluence — the strongest retracement cluster across recent legs.
use crate::indicators::pattern_swing::{
approx_equal, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD,
};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// How many recent pivots to consider; six pivots yield up to five legs.
const PIVOT_HISTORY: usize = 6;
/// The retracement ratios contributed by each leg to the confluence search.
const RATIOS: [f64; 3] = [0.382, 0.5, 0.618];
/// The strongest Fibonacci confluence zone found across recent swing legs.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FibConfluenceOutput {
/// Mean price of the densest cluster of retracement levels.
pub price: f64,
/// Number of retracement levels that fall inside the cluster (its strength).
pub strength: f64,
}
/// Fibonacci Confluence (`FibConfluence`).
///
/// Computes the 38.2% / 50% / 61.8% retracement prices of every leg among the
/// last six confirmed pivots, then reports the densest price cluster — where
/// levels from different legs stack up, the zone the market is most likely to
/// react to. `price` is the cluster mean; `strength` is how many levels it
/// gathers.
///
/// Parameter-free; construction is infallible. Returns `None` until at least two
/// legs (three pivots) exist.
///
/// See `crates/wickra-core/src/indicators/fib_confluence.rs`.
#[derive(Debug, Clone)]
pub struct FibConfluence {
swing: SwingTracker,
}
impl FibConfluence {
/// Construct a new Fibonacci Confluence tracker.
#[must_use]
pub const fn new() -> Self {
Self {
swing: SwingTracker::new(SWING_THRESHOLD, PIVOT_HISTORY),
}
}
fn confluence(&self) -> Option<FibConfluenceOutput> {
let pivots = self.swing.pivots();
if pivots.len() < 3 {
return None;
}
let levels: Vec<f64> = pivots
.windows(2)
.flat_map(|leg| {
let (start, end) = (leg[0].price, leg[1].price);
RATIOS.map(|r| end + r * (start - end))
})
.collect();
// The `len < 3` guard guarantees at least two legs, hence a non-empty
// level set, so `max_by` always yields a cluster.
let (count, total) = levels
.iter()
.map(|&center| {
let members: Vec<f64> = levels
.iter()
.copied()
.filter(|&x| approx_equal(x, center, LEVEL_TOLERANCE))
.collect();
(members.len(), members.iter().sum::<f64>())
})
.max_by(|a, b| a.0.cmp(&b.0))
.expect("at least two legs guarantee a non-empty level set");
Some(FibConfluenceOutput {
price: total / count as f64,
strength: count as f64,
})
}
}
impl Default for FibConfluence {
fn default() -> Self {
Self::new()
}
}
impl Indicator for FibConfluence {
type Input = Candle;
type Output = FibConfluenceOutput;
fn update(&mut self, candle: Candle) -> Option<FibConfluenceOutput> {
self.swing.update(candle);
self.confluence()
}
fn reset(&mut self) {
self.swing.reset();
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.swing.pivots().len() >= 3
}
fn name(&self) -> &'static str {
"FibConfluence"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicators::pattern_swing::candles_for_pivots;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn accessors_and_metadata() {
let indicator = FibConfluence::new();
assert_eq!(indicator.name(), "FibConfluence");
assert_eq!(indicator.warmup_period(), 3);
assert!(!indicator.is_ready());
assert!(!FibConfluence::default().is_ready());
}
#[test]
fn no_output_before_two_legs() {
let mut indicator = FibConfluence::new();
let outputs: Vec<_> = candles_for_pivots(&[200.0, 100.0])
.into_iter()
.map(|c| indicator.update(c))
.collect();
assert!(outputs.iter().all(Option::is_none));
assert!(!indicator.is_ready());
}
#[test]
fn picks_the_densest_cluster() {
// Legs 200->100 and 100->160. The 38.2% of each (138.2 and ~137.08)
// sit within 3% of each other and form the densest cluster (strength 2).
let mut indicator = FibConfluence::new();
let mut last = None;
for candle in candles_for_pivots(&[200.0, 100.0, 160.0]) {
last = indicator.update(candle);
}
let v = last.unwrap();
assert!(indicator.is_ready());
assert_relative_eq!(v.strength, 2.0);
let want = (138.2 + (160.0 + 0.382 * (100.0 - 160.0))) / 2.0;
assert_relative_eq!(v.price, want, epsilon = 1e-9);
}
#[test]
fn reset_clears_state() {
let mut indicator = FibConfluence::new();
for candle in candles_for_pivots(&[200.0, 100.0, 160.0]) {
let _ = indicator.update(candle);
}
assert!(indicator.is_ready());
indicator.reset();
assert!(!indicator.is_ready());
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
assert!(indicator.update(c).is_none());
}
#[test]
fn batch_equals_streaming() {
let candles = candles_for_pivots(&[200.0, 100.0, 160.0, 120.0]);
let mut a = FibConfluence::new();
let mut b = FibConfluence::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,171 @@
//! Fibonacci Extension of the most recent confirmed swing leg.
use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// The five canonical extension ratios, in ascending order. Each is a multiple
/// of the swing leg measured from its origin, so `1.0` sits on the leg's end and
/// every ratio here projects further in the direction of the move.
const RATIOS: [f64; 5] = [1.272, 1.414, 1.618, 2.0, 2.618];
/// Fibonacci Extension levels for the most recent swing leg.
///
/// Each field is the price reached if the move continues to the matching
/// multiple of the leg, measured from the leg's start.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FibExtensionOutput {
/// 127.2% extension.
pub level_1272: f64,
/// 141.4% extension.
pub level_1414: f64,
/// 161.8% extension — the "golden" extension.
pub level_1618: f64,
/// 200% extension.
pub level_2000: f64,
/// 261.8% extension.
pub level_2618: f64,
}
/// Fibonacci Extension (`FibExtension`).
///
/// Tracks confirmed swing pivots with a baked-in 5% reversal threshold and, once
/// two pivots exist, projects the leg between them to the canonical extension
/// ratios — the price targets a continuation of the move would reach.
///
/// Parameter-free; construction is infallible. Returns `None` until the first
/// leg is complete.
///
/// See `crates/wickra-core/src/indicators/fib_extension.rs`.
#[derive(Debug, Clone)]
pub struct FibExtension {
swing: SwingTracker,
}
impl FibExtension {
/// Construct a new Fibonacci Extension tracker.
#[must_use]
pub const fn new() -> Self {
Self {
swing: SwingTracker::new(SWING_THRESHOLD, 2),
}
}
/// Extension price at ratio `e` for a leg from `start` to `end`: the total
/// move is `e` times the leg, measured from `start`.
fn level(start: f64, end: f64, e: f64) -> f64 {
start + e * (end - start)
}
fn levels(&self) -> Option<FibExtensionOutput> {
let pivots = self.swing.pivots();
let [start, end] = [pivots.first()?.price, pivots.get(1)?.price];
Some(FibExtensionOutput {
level_1272: Self::level(start, end, RATIOS[0]),
level_1414: Self::level(start, end, RATIOS[1]),
level_1618: Self::level(start, end, RATIOS[2]),
level_2000: Self::level(start, end, RATIOS[3]),
level_2618: Self::level(start, end, RATIOS[4]),
})
}
}
impl Default for FibExtension {
fn default() -> Self {
Self::new()
}
}
impl Indicator for FibExtension {
type Input = Candle;
type Output = FibExtensionOutput;
fn update(&mut self, candle: Candle) -> Option<FibExtensionOutput> {
self.swing.update(candle);
self.levels()
}
fn reset(&mut self) {
self.swing.reset();
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.swing.pivots().len() >= 2
}
fn name(&self) -> &'static str {
"FibExtension"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicators::pattern_swing::candles_for_pivots;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn accessors_and_metadata() {
let indicator = FibExtension::new();
assert_eq!(indicator.name(), "FibExtension");
assert_eq!(indicator.warmup_period(), 2);
assert!(!indicator.is_ready());
assert!(!FibExtension::default().is_ready());
}
#[test]
fn no_output_before_two_pivots() {
let mut indicator = FibExtension::new();
let outputs: Vec<_> = candles_for_pivots(&[120.0])
.into_iter()
.map(|c| indicator.update(c))
.collect();
assert!(outputs.iter().all(Option::is_none));
}
#[test]
fn extension_levels_of_a_down_leg() {
// Leg start = 200 (high), end = 100 (low): a 100-point drop continued.
let mut indicator = FibExtension::new();
let mut last = None;
for candle in candles_for_pivots(&[200.0, 100.0]) {
last = indicator.update(candle);
}
let v = last.unwrap();
assert!(indicator.is_ready());
// 161.8% extension projects 1.618 * (-100) below the 200 origin.
assert_relative_eq!(v.level_1272, 200.0 - 127.2);
assert_relative_eq!(v.level_1414, 200.0 - 141.4);
assert_relative_eq!(v.level_1618, 200.0 - 161.8);
assert_relative_eq!(v.level_2000, 0.0);
assert_relative_eq!(v.level_2618, 200.0 - 261.8);
}
#[test]
fn reset_clears_state() {
let mut indicator = FibExtension::new();
for candle in candles_for_pivots(&[200.0, 100.0]) {
let _ = indicator.update(candle);
}
indicator.reset();
assert!(!indicator.is_ready());
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
assert!(indicator.update(c).is_none());
}
#[test]
fn batch_equals_streaming() {
let candles = candles_for_pivots(&[200.0, 100.0, 150.0]);
let mut a = FibExtension::new();
let mut b = FibExtension::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,165 @@
//! Fibonacci Projection — a measured move from the last three swing pivots.
use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// The four canonical projection ratios, in ascending order. Each scales the
/// A→B leg and projects it from C; `1.0` is the classic AB=CD measured move.
const RATIOS: [f64; 4] = [0.618, 1.0, 1.618, 2.618];
/// Fibonacci Projection levels (the C→D target zone of a measured move).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FibProjectionOutput {
/// 61.8% projection of the A→B leg from C.
pub level_618: f64,
/// 100% projection — the AB=CD measured move.
pub level_1000: f64,
/// 161.8% projection.
pub level_1618: f64,
/// 261.8% projection.
pub level_2618: f64,
}
/// Fibonacci Projection (`FibProjection`).
///
/// Reads the last three confirmed swing pivots as the points A, B and C of a
/// measured move and projects the A→B leg from C at the canonical ratios — the
/// price targets for the C→D leg.
///
/// Parameter-free; construction is infallible. Returns `None` until three
/// pivots have confirmed.
///
/// See `crates/wickra-core/src/indicators/fib_projection.rs`.
#[derive(Debug, Clone)]
pub struct FibProjection {
swing: SwingTracker,
}
impl FibProjection {
/// Construct a new Fibonacci Projection tracker.
#[must_use]
pub const fn new() -> Self {
Self {
swing: SwingTracker::new(SWING_THRESHOLD, 3),
}
}
fn levels(&self) -> Option<FibProjectionOutput> {
let pivots = self.swing.pivots();
let [a, b, c] = [
pivots.first()?.price,
pivots.get(1)?.price,
pivots.get(2)?.price,
];
let project = |p: f64| c + p * (b - a);
Some(FibProjectionOutput {
level_618: project(RATIOS[0]),
level_1000: project(RATIOS[1]),
level_1618: project(RATIOS[2]),
level_2618: project(RATIOS[3]),
})
}
}
impl Default for FibProjection {
fn default() -> Self {
Self::new()
}
}
impl Indicator for FibProjection {
type Input = Candle;
type Output = FibProjectionOutput;
fn update(&mut self, candle: Candle) -> Option<FibProjectionOutput> {
self.swing.update(candle);
self.levels()
}
fn reset(&mut self) {
self.swing.reset();
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.swing.pivots().len() >= 3
}
fn name(&self) -> &'static str {
"FibProjection"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicators::pattern_swing::candles_for_pivots;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn accessors_and_metadata() {
let indicator = FibProjection::new();
assert_eq!(indicator.name(), "FibProjection");
assert_eq!(indicator.warmup_period(), 3);
assert!(!indicator.is_ready());
assert!(!FibProjection::default().is_ready());
}
#[test]
fn no_output_before_three_pivots() {
let mut indicator = FibProjection::new();
let outputs: Vec<_> = candles_for_pivots(&[200.0, 100.0])
.into_iter()
.map(|c| indicator.update(c))
.collect();
assert!(outputs.iter().all(Option::is_none));
assert!(!indicator.is_ready());
}
#[test]
fn measured_move_from_three_pivots() {
// A = 200 (high), B = 160 (low), C = 190 (high). A->B = -40, projected
// down from C.
let mut indicator = FibProjection::new();
let mut last = None;
for candle in candles_for_pivots(&[200.0, 160.0, 190.0]) {
last = indicator.update(candle);
}
let v = last.unwrap();
assert!(indicator.is_ready());
let (a, b, c) = (200.0, 160.0, 190.0);
assert_relative_eq!(v.level_618, c + 0.618 * (b - a));
assert_relative_eq!(v.level_1000, c + (b - a));
assert_relative_eq!(v.level_1618, c + 1.618 * (b - a));
assert_relative_eq!(v.level_2618, c + 2.618 * (b - a));
}
#[test]
fn reset_clears_state() {
let mut indicator = FibProjection::new();
for candle in candles_for_pivots(&[200.0, 160.0, 190.0]) {
let _ = indicator.update(candle);
}
assert!(indicator.is_ready());
indicator.reset();
assert!(!indicator.is_ready());
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
assert!(indicator.update(c).is_none());
}
#[test]
fn batch_equals_streaming() {
let candles = candles_for_pivots(&[200.0, 160.0, 190.0, 150.0]);
let mut a = FibProjection::new();
let mut b = FibProjection::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,201 @@
//! Fibonacci Retracement of the most recent confirmed swing leg.
use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// The seven canonical retracement ratios, in ascending order. `0.0` marks the
/// most recent swing extreme (the end of the leg) and `1.0` the swing origin
/// (its start); the interior ratios are the classic Fibonacci pullbacks.
const RATIOS: [f64; 7] = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0];
/// Fibonacci Retracement levels for the most recent swing leg.
///
/// Each field is the price at the matching retracement ratio, measured from the
/// leg's end (`level_0`, the latest confirmed extreme) back toward its start
/// (`level_1000`, the prior pivot).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FibRetracementOutput {
/// 0.0% — the most recent confirmed swing extreme.
pub level_0: f64,
/// 23.6% retracement.
pub level_236: f64,
/// 38.2% retracement.
pub level_382: f64,
/// 50% retracement (not a Fibonacci ratio, but conventionally drawn).
pub level_500: f64,
/// 61.8% retracement — the "golden ratio" pullback.
pub level_618: f64,
/// 78.6% retracement.
pub level_786: f64,
/// 100% — the swing origin.
pub level_1000: f64,
}
/// Fibonacci Retracement (`FibRetracement`).
///
/// Tracks confirmed swing pivots with a baked-in 5% reversal threshold (the
/// same non-repainting logic as [`crate::indicators::ZigZag`]) and, once two
/// pivots exist, reports the seven retracement levels of the leg between them.
///
/// The levels are recomputed each time a new pivot confirms; between
/// confirmations [`Indicator::update`] returns the locked levels of the current
/// leg. Before the first leg is complete it returns `None`.
///
/// Parameter-free: the threshold is a compile-time constant, mirroring the
/// chart- and harmonic-pattern detectors, so construction is infallible.
///
/// See `crates/wickra-core/src/indicators/fib_retracement.rs`.
#[derive(Debug, Clone)]
pub struct FibRetracement {
swing: SwingTracker,
}
impl FibRetracement {
/// Construct a new Fibonacci Retracement tracker.
#[must_use]
pub const fn new() -> Self {
Self {
swing: SwingTracker::new(SWING_THRESHOLD, 2),
}
}
/// Retracement price at ratio `r` for a leg from `start` to `end`: `0.0`
/// sits on `end`, `1.0` on `start`.
fn level(start: f64, end: f64, r: f64) -> f64 {
end + r * (start - end)
}
fn levels(&self) -> Option<FibRetracementOutput> {
let pivots = self.swing.pivots();
let [start, end] = [pivots.first()?.price, pivots.get(1)?.price];
Some(FibRetracementOutput {
level_0: Self::level(start, end, RATIOS[0]),
level_236: Self::level(start, end, RATIOS[1]),
level_382: Self::level(start, end, RATIOS[2]),
level_500: Self::level(start, end, RATIOS[3]),
level_618: Self::level(start, end, RATIOS[4]),
level_786: Self::level(start, end, RATIOS[5]),
level_1000: Self::level(start, end, RATIOS[6]),
})
}
}
impl Default for FibRetracement {
fn default() -> Self {
Self::new()
}
}
impl Indicator for FibRetracement {
type Input = Candle;
type Output = FibRetracementOutput;
fn update(&mut self, candle: Candle) -> Option<FibRetracementOutput> {
self.swing.update(candle);
self.levels()
}
fn reset(&mut self) {
self.swing.reset();
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.swing.pivots().len() >= 2
}
fn name(&self) -> &'static str {
"FibRetracement"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicators::pattern_swing::candles_for_pivots;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn accessors_and_metadata() {
let indicator = FibRetracement::new();
assert_eq!(indicator.name(), "FibRetracement");
assert_eq!(indicator.warmup_period(), 2);
assert!(!indicator.is_ready());
assert!(!FibRetracement::default().is_ready());
}
#[test]
fn no_output_before_two_pivots() {
let mut indicator = FibRetracement::new();
// A single confirmed pivot is not enough to define a leg.
let candles = candles_for_pivots(&[120.0]);
let outputs: Vec<_> = candles.into_iter().map(|c| indicator.update(c)).collect();
assert!(outputs.iter().all(Option::is_none));
assert!(!indicator.is_ready());
}
#[test]
fn retracement_levels_of_a_down_leg() {
// Leg start = 200 (high), end = 100 (low): a 100-point drop.
let mut indicator = FibRetracement::new();
let mut last = None;
for candle in candles_for_pivots(&[200.0, 100.0]) {
last = indicator.update(candle);
}
let v = last.unwrap();
assert!(indicator.is_ready());
// 0% on the low (end), 100% on the high (start).
assert_relative_eq!(v.level_0, 100.0);
assert_relative_eq!(v.level_1000, 200.0);
// 61.8% retracement of a 100-point drop, measured up from the low.
assert_relative_eq!(v.level_618, 161.8);
assert_relative_eq!(v.level_500, 150.0);
assert_relative_eq!(v.level_382, 138.2);
assert_relative_eq!(v.level_236, 123.6);
assert_relative_eq!(v.level_786, 178.6);
}
#[test]
fn levels_refresh_on_a_new_leg() {
// Four pivots, cap = 2: once the third and fourth confirm, the reported
// leg shifts to the latest pair (130 high -> 90 low).
let mut indicator = FibRetracement::new();
let mut last = None;
for candle in candles_for_pivots(&[200.0, 100.0, 130.0, 90.0]) {
last = indicator.update(candle);
}
let v = last.unwrap();
assert_relative_eq!(v.level_0, 90.0);
assert_relative_eq!(v.level_1000, 130.0);
assert_relative_eq!(v.level_618, 90.0 + 0.618 * 40.0);
}
#[test]
fn reset_clears_state() {
let mut indicator = FibRetracement::new();
for candle in candles_for_pivots(&[200.0, 100.0]) {
let _ = indicator.update(candle);
}
assert!(indicator.is_ready());
indicator.reset();
assert!(!indicator.is_ready());
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
assert!(indicator.update(c).is_none());
}
#[test]
fn batch_equals_streaming() {
let candles = candles_for_pivots(&[200.0, 100.0, 150.0]);
let mut a = FibRetracement::new();
let mut b = FibRetracement::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}
@@ -0,0 +1,175 @@
//! Golden Pocket — the 0.618-0.65 optimal-trade-entry zone of the last swing.
use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Lower bound of the golden pocket (the 61.8% retracement).
const RATIO_LOW: f64 = 0.618;
/// Upper bound of the golden pocket (the 65% retracement).
const RATIO_HIGH: f64 = 0.65;
/// The golden-pocket zone of the most recent swing leg.
///
/// `low`/`high` bracket the 0.618-0.65 retracement band (sorted, so `low <=
/// high` regardless of swing direction); `mid` is their midpoint.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GoldenPocketOutput {
/// Lower price of the golden-pocket band.
pub low: f64,
/// Midpoint of the band.
pub mid: f64,
/// Upper price of the golden-pocket band.
pub high: f64,
}
/// Golden Pocket (`GoldenPocket`).
///
/// The 0.618-0.65 retracement band of the most recent confirmed swing leg — the
/// "optimal trade entry" zone many swing traders watch for continuation.
///
/// Parameter-free; construction is infallible. Returns `None` until the first
/// leg is complete.
///
/// See `crates/wickra-core/src/indicators/golden_pocket.rs`.
#[derive(Debug, Clone)]
pub struct GoldenPocket {
swing: SwingTracker,
}
impl GoldenPocket {
/// Construct a new Golden Pocket tracker.
#[must_use]
pub const fn new() -> Self {
Self {
swing: SwingTracker::new(SWING_THRESHOLD, 2),
}
}
fn zone(&self) -> Option<GoldenPocketOutput> {
let pivots = self.swing.pivots();
let [start, end] = [pivots.first()?.price, pivots.get(1)?.price];
let span = start - end;
let edge_low = end + RATIO_LOW * span;
let edge_high = end + RATIO_HIGH * span;
let low = edge_low.min(edge_high);
let high = edge_low.max(edge_high);
Some(GoldenPocketOutput {
low,
mid: f64::midpoint(low, high),
high,
})
}
}
impl Default for GoldenPocket {
fn default() -> Self {
Self::new()
}
}
impl Indicator for GoldenPocket {
type Input = Candle;
type Output = GoldenPocketOutput;
fn update(&mut self, candle: Candle) -> Option<GoldenPocketOutput> {
self.swing.update(candle);
self.zone()
}
fn reset(&mut self) {
self.swing.reset();
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.swing.pivots().len() >= 2
}
fn name(&self) -> &'static str {
"GoldenPocket"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicators::pattern_swing::candles_for_pivots;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn accessors_and_metadata() {
let indicator = GoldenPocket::new();
assert_eq!(indicator.name(), "GoldenPocket");
assert_eq!(indicator.warmup_period(), 2);
assert!(!indicator.is_ready());
assert!(!GoldenPocket::default().is_ready());
}
#[test]
fn no_output_before_two_pivots() {
let mut indicator = GoldenPocket::new();
let outputs: Vec<_> = candles_for_pivots(&[120.0])
.into_iter()
.map(|c| indicator.update(c))
.collect();
assert!(outputs.iter().all(Option::is_none));
}
#[test]
fn zone_of_a_down_leg() {
// Leg 200 (high) -> 100 (low), span = 100.
let mut indicator = GoldenPocket::new();
let mut last = None;
for candle in candles_for_pivots(&[200.0, 100.0]) {
last = indicator.update(candle);
}
let v = last.unwrap();
assert!(indicator.is_ready());
// 61.8% = 161.8, 65% = 165 → sorted band [161.8, 165], mid 163.4.
assert_relative_eq!(v.low, 161.8);
assert_relative_eq!(v.high, 165.0);
assert_relative_eq!(v.mid, 163.4);
}
#[test]
fn band_is_sorted_for_an_up_leg() {
// Latest leg 100 (low) -> 250 (high): span negative, edges flip, but
// low <= high must still hold.
let mut indicator = GoldenPocket::new();
let mut last = None;
for candle in candles_for_pivots(&[200.0, 100.0, 250.0]) {
last = indicator.update(candle);
}
let v = last.unwrap();
assert!(v.low <= v.high);
assert_relative_eq!(v.mid, f64::midpoint(v.low, v.high));
}
#[test]
fn reset_clears_state() {
let mut indicator = GoldenPocket::new();
for candle in candles_for_pivots(&[200.0, 100.0]) {
let _ = indicator.update(candle);
}
indicator.reset();
assert!(!indicator.is_ready());
let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
assert!(indicator.update(c).is_none());
}
#[test]
fn batch_equals_streaming() {
let candles = candles_for_pivots(&[200.0, 100.0, 150.0]);
let mut a = GoldenPocket::new();
let mut b = GoldenPocket::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}
+24 -1
View File
@@ -34,6 +34,7 @@ mod aroon_oscillator;
mod atr;
mod atr_bands;
mod atr_trailing_stop;
mod auto_fib;
mod autocorrelation;
mod average_daily_range;
mod average_drawdown;
@@ -110,6 +111,10 @@ mod evening_doji_star;
mod evwma;
mod falling_three_methods;
mod fama;
mod fib_confluence;
mod fib_extension;
mod fib_projection;
mod fib_retracement;
mod fibonacci_pivots;
mod fisher_transform;
mod flag_pennant;
@@ -125,6 +130,7 @@ mod gain_loss_ratio;
mod gap_side_by_side_white;
mod garman_klass;
mod gartley;
mod golden_pocket;
mod granger_causality;
mod gravestone_doji;
mod hammer;
@@ -401,6 +407,7 @@ pub use aroon_oscillator::AroonOscillator;
pub use atr::Atr;
pub use atr_bands::{AtrBands, AtrBandsOutput};
pub use atr_trailing_stop::AtrTrailingStop;
pub use auto_fib::{AutoFib, AutoFibOutput};
pub use autocorrelation::Autocorrelation;
pub use average_daily_range::AverageDailyRange;
pub use average_drawdown::AverageDrawdown;
@@ -477,6 +484,10 @@ pub use evening_doji_star::EveningDojiStar;
pub use evwma::Evwma;
pub use falling_three_methods::FallingThreeMethods;
pub use fama::Fama;
pub use fib_confluence::{FibConfluence, FibConfluenceOutput};
pub use fib_extension::{FibExtension, FibExtensionOutput};
pub use fib_projection::{FibProjection, FibProjectionOutput};
pub use fib_retracement::{FibRetracement, FibRetracementOutput};
pub use fibonacci_pivots::{FibonacciPivots, FibonacciPivotsOutput};
pub use fisher_transform::FisherTransform;
pub use flag_pennant::FlagPennant;
@@ -492,6 +503,7 @@ pub use gain_loss_ratio::GainLossRatio;
pub use gap_side_by_side_white::GapSideBySideWhite;
pub use garman_klass::GarmanKlassVolatility;
pub use gartley::Gartley;
pub use golden_pocket::{GoldenPocket, GoldenPocketOutput};
pub use granger_causality::GrangerCausality;
pub use gravestone_doji::GravestoneDoji;
pub use hammer::Hammer;
@@ -1222,6 +1234,17 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"ThreeDrives",
],
),
(
"Fibonacci",
&[
"FibRetracement",
"FibExtension",
"FibProjection",
"AutoFib",
"GoldenPocket",
"FibConfluence",
],
),
];
#[cfg(test)]
@@ -1250,6 +1273,6 @@ mod family_tests {
// the actual indicator count is the early-warning signal that an
// indicator was added without being assigned a family.
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
assert_eq!(total, 367, "FAMILIES total drifted from indicator count");
assert_eq!(total, 373, "FAMILIES total drifted from indicator count");
}
}