"""Fibonacci retracement signal evaluator. Computes retracement levels using ``L(r) = SH - r * (SH - SL)`` for the standard ratios [0.236, 0.382, 0.5, 0.618, 0.786] and produces a signal based on the proximity of the current price to the nearest level. """ from __future__ import annotations from services.signal_engine.models import OHLCVBar, SignalDirection, SignalResult from services.signal_engine.signals.base import ( find_swing_high, find_swing_low, validate_lookback, ) # Standard Fibonacci retracement ratios RETRACEMENT_RATIOS: list[float] = [0.236, 0.382, 0.5, 0.618, 0.786] # Ratios considered "key" levels — proximity to these yields higher confidence _KEY_RATIOS: set[float] = {0.5, 0.618} # Default minimum number of bars required for evaluation DEFAULT_MIN_BARS: int = 20 class FibonacciEvaluator: """Fibonacci retracement signal evaluator. Satisfies the :class:`~services.signal_engine.signals.base.SignalEvaluator` protocol. Parameters ---------- min_bars: Minimum number of OHLCV bars required before the evaluator will produce a signal. Defaults to ``20``. """ def __init__(self, min_bars: int = DEFAULT_MIN_BARS) -> None: self.min_bars = min_bars # ------------------------------------------------------------------ # Public API (SignalEvaluator protocol) # ------------------------------------------------------------------ def evaluate( self, bars: list[OHLCVBar], timeframe: str, ) -> SignalResult | None: """Evaluate Fibonacci retracement on *bars* for *timeframe*. Returns ``None`` when there are fewer than :pyattr:`min_bars` bars, or when the swing high equals the swing low (flat market — no valid retracement). """ if not validate_lookback(bars, self.min_bars): return None # Detect swing high / swing low within the evaluation window sh_result = find_swing_high(bars, self.min_bars) sl_result = find_swing_low(bars, self.min_bars) if sh_result is None or sl_result is None: return None _sh_idx, sh_price = sh_result _sl_idx, sl_price = sl_result # SH must be strictly greater than SL for a valid retracement range if sh_price <= sl_price: return None price_range = sh_price - sl_price current_price = bars[-1].close # Compute retracement levels: L(r) = SH - r * (SH - SL) levels: dict[float, float] = { r: sh_price - r * price_range for r in RETRACEMENT_RATIOS } # Find the nearest retracement level to the current price nearest_ratio: float = RETRACEMENT_RATIOS[0] nearest_level: float = levels[nearest_ratio] min_distance: float = abs(current_price - nearest_level) for ratio in RETRACEMENT_RATIOS[1:]: distance = abs(current_price - levels[ratio]) if distance < min_distance: min_distance = distance nearest_ratio = ratio nearest_level = levels[ratio] # Signal strength: 1.0 - (distance / range), clamped to [0, 1] raw_strength = 1.0 - (min_distance / price_range) strength = max(0.0, min(1.0, raw_strength)) # Direction: BULLISH if price is near a retracement level and above SL # (potential bounce off support). Otherwise BEARISH. if current_price >= sl_price: direction = SignalDirection.BULLISH else: direction = SignalDirection.BEARISH # Confidence: higher when the nearest level is a key ratio (0.618, 0.5) if nearest_ratio in _KEY_RATIOS: confidence = min(1.0, strength * 1.2) else: confidence = strength * 0.8 return SignalResult( signal_type="fibonacci", timeframe=timeframe, strength=strength, direction=direction, confidence=confidence, metadata={ "swing_high": sh_price, "swing_low": sl_price, "retracement_levels": levels, "nearest_ratio": nearest_ratio, "nearest_level": nearest_level, "distance_to_nearest": min_distance, "current_price": current_price, }, )