#!/usr/bin/env python3
"""key_level_signal.py —Historical key level reversal.

Strategy: Identify key support/resistance levels from recent price action
(swing highs/lows, round numbers, prior day H/L). When price approaches
a key level with fading momentum, enter a reversal trade.

Mean-reversion type, designed for all regimes with appropriate confidence.
"""
from __future__ import annotations

# ── Adaptive price rounding (2-digit vs 3-digit brokers) ──────────
try:
    from src.price_digits import round_price as _round_price
except ImportError:
    def _round_price(price, **_kwargs):
        return round(float(price), 2)


import time, uuid
from typing import Any
from signal_generators.base import BaseSignalGenerator


class KeyLevelSignal(BaseSignalGenerator):
    """Key level reversal —fade at historical S/R zones."""

    SCHEMA_VERSION = "2.1.0"

    DEFAULT_PARAMS: dict[str, Any] = {
        "swing_lookback": 50,
        "level_tolerance_pct": 0.15,    # % of price —how close to level to trigger
        "round_number_step": 50,         # round number interval (e.g. 2600, 2650)
        "momentum_bars": 5,
        "atr_period": 14,
        "sl_atr_mult": 2.0,
        "tp_atr_mult": 3.5,
        "min_confidence": 0.55,
        "min_bars": 80,
        "_counter_direction_allowed": True,
    }

    @staticmethod
    def _atr(bars: list[dict], period: int = 14) -> float:
        n = len(bars)
        if n < period + 1: return 0.0
        trs = [max(bars[i]["high"] - bars[i]["low"], abs(bars[i]["high"] - bars[i-1]["close"]), abs(bars[i]["low"] - bars[i-1]["close"])) for i in range(1, n)]
        return sum(trs[-period:]) / period

    @staticmethod
    def _find_swing_levels(highs: list[float], lows: list[float], lookback: int) -> tuple[list[float], list[float]]:
        """Find recent swing highs and lows."""
        if len(highs) < lookback: return [], []
        h = highs[-lookback:]; l = lows[-lookback:]
        # Pivot highs: higher than both neighbors
        pivots_h = [h[i] for i in range(2, len(h)-2) if h[i] > h[i-1] and h[i] > h[i-2] and h[i] > h[i+1] and h[i] > h[i+2]]
        pivots_l = [l[i] for i in range(2, len(l)-2) if l[i] < l[i-1] and l[i] < l[i-2] and l[i] < l[i+1] and l[i] < l[i+2]]
        return pivots_h, pivots_l

    def analyze(self, bars: list[dict], params: dict | None = None) -> dict[str, Any]:
        p = {**self.DEFAULT_PARAMS, **(params or {})}
        tid = f"key_level_{uuid.uuid4().hex[:12]}"
        if len(bars) < p["min_bars"]: return self._hold(tid, "insufficient_bars", {})

        closes = [float(b["close"]) for b in bars]
        highs  = [float(b["high"]) for b in bars]
        lows   = [float(b["low"]) for b in bars]
        cp = closes[-1]
        atr_v = self._atr(bars, int(p["atr_period"]))
        if atr_v <= 0: return self._hold(tid, "no_atr", {})

        # Collect key levels
        levels: dict[str, list[float]] = {"resistance": [], "support": []}
        tolerance = cp * p["level_tolerance_pct"] / 100.0

        # 1. Swing highs/lows
        sh, sl = self._find_swing_levels(highs, lows, p["swing_lookback"])
        for h in sh: levels["resistance"].append(h)
        for l in sl: levels["support"].append(l)

        # 2. Prior day high/low
        if len(bars) >= 288:  # ~24h M5
            day_bars = bars[-288:]
            day_high = max(b["high"] for b in day_bars)
            day_low  = min(b["low"] for b in day_bars)
            levels["resistance"].append(day_high)
            levels["support"].append(day_low)

        # 3. Round numbers
        step = p["round_number_step"]
        nearest = round(cp / step) * step
        for offset in [-step, 0, step]:
            rn = nearest + offset
            if rn > cp: levels["resistance"].append(rn)
            if rn < cp: levels["support"].append(rn)

        # Check proximity to levels
        nearest_resistance = min(levels["resistance"]) if levels["resistance"] else float('inf')
        nearest_support = max(levels["support"]) if levels["support"] else 0

        action, side = "HOLD", "NONE"
        reason = ""
        mb = p["momentum_bars"]

        # Breakout guard: do not fade if price has clearly broken through the level
        _recent_high = max(highs[-mb:])
        _recent_low = min(lows[-mb:])

        if nearest_resistance < float("inf") and cp >= nearest_resistance - tolerance:
            _broke_out = _recent_high > nearest_resistance * 1.002
            if not _broke_out:
                up_bars = sum(1 for i in range(-mb, 0) if i < 0 and closes[i] > closes[i-1])
                if up_bars <= 1:
                    action, side = "SELL", "SELL"
                    reason = f"Resistance={nearest_resistance:.0f} fade"

        if action == "HOLD" and nearest_support > 0 and cp <= nearest_support + tolerance:
            _broke_down = _recent_low < nearest_support * 0.998
            if not _broke_down:
                down_bars = sum(1 for i in range(-mb, 0) if i < 0 and closes[i] < closes[i-1])
                if down_bars <= 1:
                    action, side = "BUY", "BUY"
                    reason = f"Support={nearest_support:.0f} bounce"
        if action == "HOLD": return self._hold(tid, "no_level_touch", {"nearest_R": nearest_resistance, "nearest_S": nearest_support})

        confidence = p["min_confidence"]
        entry = cp
        sl = _round_price(entry - atr_v * p["sl_atr_mult"]) if side == "BUY" else _round_price(entry + atr_v * p["sl_atr_mult"])
        tp = _round_price(entry + atr_v * p["tp_atr_mult"]) if side == "BUY" else _round_price(entry - atr_v * p["tp_atr_mult"])

        return {"schema_version": self.SCHEMA_VERSION, "trace_id": tid, "strategy_id": self.get_strategy_id(),
                "model_version": None, "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
                "_counter_direction_allowed": True,
                "payload": {"action": action, "side": side, "entry": round(entry, 3), "sl": sl, "tp": tp,
                            "confidence": round(confidence, 3), "reason": reason,
                            "indicators": {"nearest_R": nearest_resistance, "nearest_S": nearest_support, "atr": round(atr_v, 3)}}}

    def _hold(self, tid: str, r: str, ind: dict) -> dict:
        return {"schema_version": self.SCHEMA_VERSION, "trace_id": tid, "strategy_id": self.get_strategy_id(),
                "model_version": None, "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
                "payload": {"action": "HOLD", "confidence": 0.0, "reason": r, "indicators": ind}}

    def get_strategy_id(self) -> str: return "key_level"
    def get_params_schema(self) -> dict:
        return {"swing_lookback": {"type": "int", "default": 50, "range": [20, 100]},
                "level_tolerance_pct": {"type": "float", "default": 0.15, "range": [0.05, 0.30]},
                "round_number_step": {"type": "int", "default": 50, "range": [25, 100]},
                "momentum_bars": {"type": "int", "default": 5, "range": [3, 10]},
                "sl_atr_mult": {"type": "float", "default": 1.2, "range": [0.8, 2.0]},
                "tp_atr_mult": {"type": "float", "default": 2.0, "range": [1.0, 3.0]}}
