"""Fibonacci-based technical analysis indicators.
This module provides Fibonacci retracement, extension, fan, time zone,
pivot point, and auto-detection indicators. All functions accept
``pd.Series`` inputs and return ``dict`` or ``list`` outputs.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
__all__ = [
"fibonacci_retracements",
"fibonacci_extensions",
"fibonacci_fans",
"fibonacci_time_zones",
"fibonacci_pivot_points",
"auto_fibonacci",
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
from wraquant.ta._validators import validate_series as _validate_series
# ---------------------------------------------------------------------------
# Fibonacci Retracements
# ---------------------------------------------------------------------------
[docs]
def fibonacci_retracements(
swing_high: float,
swing_low: float,
direction: str = "up",
) -> dict[str, float]:
"""Compute Fibonacci retracement levels from a swing high/low pair.
Given a swing high and swing low, computes the standard Fibonacci
retracement levels at 23.6%, 38.2%, 50%, 61.8%, and 78.6%.
Interpretation:
- **23.6%**: Shallow retracement -- strong trend continuation
likely. Common in fast-moving markets.
- **38.2%**: Moderate retracement -- healthy pullback in a
strong trend.
- **50%**: Not a Fibonacci ratio but widely watched. A 50%
retracement is considered normal.
- **61.8%**: The "golden ratio" -- the most important level.
If price holds here, the trend is likely to resume.
- **78.6%**: Deep retracement -- the trend is under pressure.
If this level breaks, the trend may be over.
Trading rules:
- Look for buy signals (candlestick patterns, divergence) at
38.2%-61.8% retracement levels in an uptrend.
- Place stops below the 78.6% level.
- The stronger the trend, the shallower the retracement
(23.6%-38.2%).
Parameters
----------
swing_high : float
The swing high price.
swing_low : float
The swing low price.
direction : str, default "up"
If ``"up"``, retracements are measured from the high downward
(pullback in an uptrend). If ``"down"``, retracements are
measured from the low upward (pullback in a downtrend).
Returns
-------
dict[str, float]
Level names (e.g. ``"23.6%"``) as keys and price values.
Example
-------
>>> result = fibonacci_retracements(swing_high=110.0, swing_low=100.0)
>>> result["50.0%"]
105.0
"""
if swing_high <= swing_low:
raise ValueError(f"swing_high ({swing_high}) must be > swing_low ({swing_low})")
diff = swing_high - swing_low
ratios = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0]
labels = ["0.0%", "23.6%", "38.2%", "50.0%", "61.8%", "78.6%", "100.0%"]
if direction == "up":
# Retracing down from the high
levels = {label: swing_high - r * diff for label, r in zip(labels, ratios)}
elif direction == "down":
# Retracing up from the low
levels = {label: swing_low + r * diff for label, r in zip(labels, ratios)}
else:
raise ValueError(f"direction must be 'up' or 'down', got {direction!r}")
return levels
# ---------------------------------------------------------------------------
# Fibonacci Extensions
# ---------------------------------------------------------------------------
[docs]
def fibonacci_extensions(
swing_low: float,
swing_high: float,
pullback_low: float,
) -> dict[str, float]:
"""Compute Fibonacci extension levels from three price points.
Uses a swing low, swing high, and pullback low to project
extension levels at 100%, 127.2%, 161.8%, 200%, and 261.8%.
Interpretation:
- Extension levels project where price might go AFTER a
retracement completes.
- **100%**: The most common initial target (move equals the
first swing).
- **127.2%**: Common target for corrective waves.
- **161.8%**: The golden extension -- a key profit target.
- **200% and 261.8%**: Extended targets for strong trends.
- Use for setting profit targets and identifying potential
resistance levels in a trend.
Parameters
----------
swing_low : float
The initial swing low price.
swing_high : float
The swing high price.
pullback_low : float
The pullback low price (retracement point).
Returns
-------
dict[str, float]
Extension level names as keys and projected price values.
Example
-------
>>> result = fibonacci_extensions(100.0, 110.0, 105.0)
>>> result["100.0%"]
115.0
"""
if swing_high <= swing_low:
raise ValueError(f"swing_high ({swing_high}) must be > swing_low ({swing_low})")
diff = swing_high - swing_low
ratios = [1.0, 1.272, 1.618, 2.0, 2.618]
labels = ["100.0%", "127.2%", "161.8%", "200.0%", "261.8%"]
levels = {label: pullback_low + r * diff for label, r in zip(labels, ratios)}
return levels
# ---------------------------------------------------------------------------
# Fibonacci Fans
# ---------------------------------------------------------------------------
[docs]
def fibonacci_fans(
pivot_x: int,
pivot_y: float,
target_x: int,
target_y: float,
) -> dict[str, float]:
"""Compute Fibonacci fan line slopes from two pivot points.
Draws fan lines from ``(pivot_x, pivot_y)`` through Fibonacci
retracement levels of the vertical distance to
``(target_x, target_y)``.
Parameters
----------
pivot_x : int
Bar index of the pivot (start) point.
pivot_y : float
Price at the pivot point.
target_x : int
Bar index of the target (end) point.
target_y : float
Price at the target point.
Returns
-------
dict[str, float]
Fan line labels as keys and slope values.
Example
-------
>>> result = fibonacci_fans(0, 100.0, 10, 110.0)
>>> abs(result["50.0%"] - 0.5) < 1e-10
True
"""
dx = target_x - pivot_x
if dx == 0:
raise ValueError("pivot_x and target_x must be different")
dy = target_y - pivot_y
ratios = {"38.2%": 0.382, "50.0%": 0.5, "61.8%": 0.618}
slopes: dict[str, float] = {}
for label, ratio in ratios.items():
fan_y = pivot_y + ratio * dy
slopes[label] = (fan_y - pivot_y) / dx
return slopes
# ---------------------------------------------------------------------------
# Fibonacci Time Zones
# ---------------------------------------------------------------------------
[docs]
def fibonacci_time_zones(
start_index: int,
max_index: int,
) -> list[int]:
"""Compute Fibonacci time zone indices from a start bar.
Generates a sequence of bar indices at Fibonacci intervals
(1, 1, 2, 3, 5, 8, 13, 21, ...) from the given start index,
up to ``max_index``.
Parameters
----------
start_index : int
The bar index to begin the Fibonacci time zones from.
max_index : int
The maximum bar index (exclusive) to generate zones up to.
Returns
-------
list[int]
List of bar indices at Fibonacci time intervals.
Example
-------
>>> fibonacci_time_zones(0, 50)
[1, 2, 3, 5, 8, 13, 21, 34]
"""
if max_index <= start_index:
raise ValueError(
f"max_index ({max_index}) must be > start_index ({start_index})"
)
zones: list[int] = []
a, b = 1, 1
while start_index + a < max_index:
idx = start_index + a
if not zones or zones[-1] != idx:
zones.append(idx)
a, b = b, a + b
return zones
# ---------------------------------------------------------------------------
# Fibonacci Pivot Points
# ---------------------------------------------------------------------------
[docs]
def fibonacci_pivot_points(
high: pd.Series,
low: pd.Series,
close: pd.Series,
) -> dict[str, pd.Series]:
"""Pivot points using Fibonacci ratios.
Computes the standard pivot ``P = (H + L + C) / 3`` and derives
support/resistance using Fibonacci ratios applied to the
prior bar's range.
Parameters
----------
high : pd.Series
High prices.
low : pd.Series
Low prices.
close : pd.Series
Close prices.
Returns
-------
dict[str, pd.Series]
``pivot``, ``s1``, ``s2``, ``s3``, ``r1``, ``r2``, ``r3``.
Example
-------
>>> import pandas as pd
>>> h = pd.Series([12, 13, 14, 13, 12], dtype=float)
>>> lo = pd.Series([10, 11, 12, 11, 10], dtype=float)
>>> c = pd.Series([11, 12, 13, 12, 11], dtype=float)
>>> result = fibonacci_pivot_points(h, lo, c) # doctest: +SKIP
"""
high = _validate_series(high, "high")
low = _validate_series(low, "low")
close = _validate_series(close, "close")
h_prev = high.shift(1)
l_prev = low.shift(1)
c_prev = close.shift(1)
pp = (h_prev + l_prev + c_prev) / 3.0
diff = h_prev - l_prev
r1 = pp + 0.382 * diff
r2 = pp + 0.618 * diff
r3 = pp + 1.000 * diff
s1 = pp - 0.382 * diff
s2 = pp - 0.618 * diff
s3 = pp - 1.000 * diff
return {
"pivot": pp.rename("fib_pivot"),
"r1": r1.rename("fib_r1"),
"r2": r2.rename("fib_r2"),
"r3": r3.rename("fib_r3"),
"s1": s1.rename("fib_s1"),
"s2": s2.rename("fib_s2"),
"s3": s3.rename("fib_s3"),
}
# ---------------------------------------------------------------------------
# Auto Fibonacci
# ---------------------------------------------------------------------------
[docs]
def auto_fibonacci(
data: pd.Series,
lookback: int = 50,
direction: str = "up",
) -> dict[str, object]:
"""Automatically detect swing high/low and compute Fibonacci retracements.
Scans the most recent *lookback* bars to find the highest high
and lowest low, then computes Fibonacci retracement levels.
Parameters
----------
data : pd.Series
Price series (typically close).
lookback : int, default 50
Number of recent bars to scan for swing points.
direction : str, default "up"
Trend direction assumption: ``"up"`` retraces from high
downward, ``"down"`` retraces from low upward.
Returns
-------
dict[str, object]
``swing_high`` (float), ``swing_high_idx`` (index label),
``swing_low`` (float), ``swing_low_idx`` (index label),
``levels`` (dict of retracement levels).
Example
-------
>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> close = pd.Series(100 + np.cumsum(np.random.randn(100) * 0.5))
>>> result = auto_fibonacci(close, lookback=30) # doctest: +SKIP
"""
data = _validate_series(data)
if lookback < 2:
raise ValueError(f"lookback must be >= 2, got {lookback}")
window = data.iloc[-lookback:]
swing_high_idx = window.idxmax()
swing_low_idx = window.idxmin()
swing_high_val = float(window.loc[swing_high_idx])
swing_low_val = float(window.loc[swing_low_idx])
if swing_high_val <= swing_low_val:
# Flat price — return degenerate levels
levels = {
"0.0%": swing_high_val,
"23.6%": swing_high_val,
"38.2%": swing_high_val,
"50.0%": swing_high_val,
"61.8%": swing_high_val,
"78.6%": swing_high_val,
"100.0%": swing_high_val,
}
else:
levels = fibonacci_retracements(swing_high_val, swing_low_val, direction)
return {
"swing_high": swing_high_val,
"swing_high_idx": swing_high_idx,
"swing_low": swing_low_val,
"swing_low_idx": swing_low_idx,
"levels": levels,
}