#!/usr/bin/env python3
"""adx_breakout_signal.py —ADX breakout with volume confirmation.

Strategy: Enter when ADX>30 + volume expanding + price breaks Donchian channel.
Trend-following, designed for STRONG_TREND / WEAK_TREND regimes.
"""
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 ADXBreakoutSignal(BaseSignalGenerator):
    """ADX-confirmed Donchian channel breakout."""

    SCHEMA_VERSION = "2.1.0"

    DEFAULT_PARAMS: dict[str, Any] = {
        "adx_period": 14,
        "adx_threshold": 30,
        "donchian_period": 20,
        "volume_expand_ratio": 1.3,
        "atr_period": 14,
        "sl_atr_mult": 1.8,
        "tp_atr_mult": 3.0,
        "min_confidence": 0.55,
        "min_bars": 80,
    }

    @staticmethod
    def _ema(data: list[float], period: int) -> list[float | None]:
        if len(data) < period: return [None] * len(data)
        k = 2.0 / (period + 1); r = [None] * (period - 1) + [sum(data[:period]) / period]
        for v in data[period:]: r.append(v * k + r[-1] * (1 - k))
        return r

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

    @staticmethod
    def _compute_adx(bars: list[dict], period: int = 14) -> float:
        n = len(bars)
        if n < period + 1: return 20.0
        tr_list, plus_dm, minus_dm = [], [], []
        for i in range(1, n):
            b, pb = bars[i], bars[i - 1]
            hl = b["high"] - pb["high"]; lh = pb["low"] - b["low"]
            plus_dm.append(max(hl, 0) if hl > lh else 0)
            minus_dm.append(max(lh, 0) if lh > hl else 0)
            tr_list.append(max(b["high"] - b["low"], abs(b["high"] - pb["close"]), abs(b["low"] - pb["close"])))
        atr_v = ADXBreakoutSignal._ema(tr_list, period)
        ep = ADXBreakoutSignal._ema(plus_dm, period)
        em = ADXBreakoutSignal._ema(minus_dm, period)
        dx = []
        for i in range(len(atr_v)):
            if atr_v[i] is None or atr_v[i] == 0 or ep[i] is None or em[i] is None: dx.append(None); continue
            di_sum = 100 * (ep[i] + em[i]) / atr_v[i]
            dx.append(100 * abs(ep[i] - em[i]) / di_sum if di_sum > 0 else 0)
        valid = [v for v in dx if v is not None]
        adx_e = ADXBreakoutSignal._ema(valid, period)
        return float(adx_e[-1]) if adx_e and adx_e[-1] is not None else 20.0

    def analyze(self, bars: list[dict], params: dict | None = None) -> dict[str, Any]:
        _llm_dir = str((params or {}).get("_llm_direction", "") or "").upper()
        _llm_conf = float((params or {}).get("_llm_confidence", 0.0) or 0.0)
        _llm_active = _llm_dir in {"BUY", "SELL"} and _llm_conf >= 0.4
        p = {**self.DEFAULT_PARAMS, **(params or {})}
        trace_id = f"adx_break_{uuid.uuid4().hex[:12]}"
        if len(bars) < p["min_bars"]: return self._hold(trace_id, "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]
        volumes = [float(b.get("tick_volume", 0) or 0) for b in bars]
        current_price = closes[-1]

        # ADX
        adx = self._compute_adx(bars, int(p["adx_period"]))
        if adx < p["adx_threshold"]: return self._hold(trace_id, f"ADX={adx:.0f}<{p['adx_threshold']}", {"adx": round(adx, 1)})

        # Donchian channel
        dp = int(p["donchian_period"])
        upper = max(highs[-dp:]) if len(highs) >= dp else 0
        lower = min(lows[-dp:]) if len(lows) >= dp else 0
        if upper <= 0 or lower <= 0: return self._hold(trace_id, "no_donchian", {})

        # Volume expansion
        avg_vol_20 = sum(volumes[-20:]) / 20 if len(volumes) >= 20 else 1
        recent_vol = sum(volumes[-5:]) / 5 if len(volumes) >= 5 else 1
        vol_expanding = recent_vol > avg_vol_20 * p["volume_expand_ratio"]
        if not vol_expanding: return self._hold(trace_id, "volume_not_expanding", {})

        atr_val = self._atr(bars, int(p["atr_period"]))
        if atr_val <= 0: return self._hold(trace_id, "no_atr", {})

        action, side = "HOLD", "NONE"
        if current_price >= upper: action, side = "BUY", "BUY"
        elif current_price <= lower: action, side = "SELL", "SELL"

        if action == "HOLD": return self._hold(trace_id, "no_breakout", {"adx": round(adx, 1), "upper": upper, "lower": lower})

        confidence = min(0.85, p["min_confidence"] + (adx - p["adx_threshold"]) * 0.01)
        if side == "BUY" and _llm_active and _llm_dir == "SELL":
            confidence = max(0.30, confidence * 0.5)
        elif side == "SELL" and _llm_active and _llm_dir == "BUY":
            confidence = max(0.30, confidence * 0.5)
        entry = current_price
        sl = _round_price(entry - atr_val * p["sl_atr_mult"]) if side == "BUY" else _round_price(entry + atr_val * p["sl_atr_mult"])
        tp = _round_price(entry + atr_val * p["tp_atr_mult"]) if side == "BUY" else _round_price(entry - atr_val * p["tp_atr_mult"])

        return {
            "schema_version": self.SCHEMA_VERSION, "trace_id": trace_id,
            "strategy_id": self.get_strategy_id(), "model_version": None,
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
            "payload": {"action": action, "side": side, "entry": round(entry, 3), "sl": sl, "tp": tp,
                        "confidence": round(confidence, 3),
                        "reason": f"ADX={adx:.0f} Donchian breakout {side}",
                        "indicators": {"adx": round(adx, 1), "upper": upper, "lower": lower, "atr": round(atr_val, 3)}},
        }

    def _hold(self, tid: str, reason: 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": reason, "indicators": ind}}

    def get_strategy_id(self) -> str: return "adx_breakout"
    def get_params_schema(self) -> dict:
        return {"adx_period": {"type": "int", "default": 14, "range": [7, 28]},
                "adx_threshold": {"type": "int", "default": 30, "range": [20, 40]},
                "donchian_period": {"type": "int", "default": 20, "range": [10, 50]},
                "volume_expand_ratio": {"type": "float", "default": 1.3, "range": [1.1, 2.0]},
                "sl_atr_mult": {"type": "float", "default": 1.8, "range": [1.0, 3.0]},
                "tp_atr_mult": {"type": "float", "default": 3.0, "range": [2.0, 5.0]}}
