#!/usr/bin/env python3
"""boll_band_signal.py — Bollinger Band mean-reversion signal generator.

Strategy: Buy when price touches/crosses below lower band,
          sell when price touches/crosses above upper band.
Designed for RANGING markets — assumes price reverts to the midline.

Uses the base Signal Generator interface (BaseSignalGenerator.analyze).
"""
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
import uuid
from typing import Any

from signal_generators.base import BaseSignalGenerator


class BollBandSignal(BaseSignalGenerator):
    """Bollinger Band mean-reversion strategy.

    Long  entry: price <= lower_band  (2σ below SMA → oversold → buy)
    Short entry: price >= upper_band  (2σ above SMA → overbought → sell)

    SL at opposite band, TP at midline (SMA). RR ~1:1.
    """

    SCHEMA_VERSION = "2.1.0"

    DEFAULT_PARAMS: dict[str, Any] = {
        "bb_period": 20,
        "bb_std_mult": 2.0,
        "atr_period": 14,
        "sl_atr_mult": 2.0,
        "tp_atr_mult": 3.5,
        "min_confidence": 0.55,
        "min_bars": 50,
        "_counter_direction_allowed": True,
    }

    # ── Helpers ──────────────────────────────────────────────────

    @staticmethod
    def _sma(data: list[float], period: int) -> list[float | None]:
        result: list[float | None] = [None] * (period - 1)
        for i in range(period - 1, len(data)):
            result.append(sum(data[i - period + 1:i + 1]) / period)
        return result

    @staticmethod
    def _stddev(data: list[float], sma_vals: list[float | None], period: int) -> list[float | None]:
        result: list[float | None] = [None] * (period - 1)
        for i in range(period - 1, len(data)):
            sma = sma_vals[i]
            if sma is None:
                result.append(None)
                continue
            variance = sum((data[j] - sma) ** 2 for j in range(i - period + 1, i + 1)) / period
            result.append(variance ** 0.5)
        return result

    @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]
            h, l, pc = b["high"], b["low"], pb["close"]
            trs.append(max(h - l, abs(h - pc), abs(l - pc)))
        return sum(trs[-period:]) / period

    # ── Core ─────────────────────────────────────────────────────

    def analyze(self, bars: list[dict], params: dict | None = None) -> dict[str, Any]:
        p = {**self.DEFAULT_PARAMS, **(params or {})}
        trace_id = f"boll_band_{uuid.uuid4().hex[:12]}"

        if len(bars) < max(p["min_bars"], p["bb_period"] + 1):
            return self._hold(trace_id, "insufficient_bars", {})

        closes = [float(b["close"]) for b in bars]
        current_price = closes[-1]

        sma_vals = self._sma(closes, int(p["bb_period"]))
        std_vals = self._stddev(closes, sma_vals, int(p["bb_period"]))

        mid = sma_vals[-1]
        std = std_vals[-1]
        if mid is None or std is None or std <= 0:
            return self._hold(trace_id, "insufficient_data", {})

        mult = p["bb_std_mult"]
        upper = round(mid + mult * std, 3)
        lower = round(mid - mult * std, 3)
        bandwidth = (upper - lower) / mid if mid > 0 else 0

        atr_val = self._atr(bars, int(p["atr_period"]))

        action = "HOLD"
        side = "NONE"
        reason = ""
        confidence = 0.0

        if current_price <= lower:
            action, side = "BUY", "BUY"
            # Confidence: deeper below band → higher
            penetration = (lower - current_price) / max(0.01, std)
            confidence = min(0.85, p["min_confidence"] + penetration * 0.05)
            reason = f"Price below lower band ({current_price:.2f}<{lower:.2f})"
        elif current_price >= upper:
            action, side = "SELL", "SELL"
            penetration = (current_price - upper) / max(0.01, std)
            confidence = min(0.85, p["min_confidence"] + penetration * 0.05)
            reason = f"Price above upper band ({current_price:.2f}>{upper:.2f})"
        else:
            return self._hold(trace_id, f"Price within bands ({lower:.2f}-{upper:.2f})",
                              {"upper": upper, "lower": lower, "mid": round(mid, 3)})

        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": reason,
                "indicators": {
                    "upper": upper,
                    "mid": round(mid, 3),
                    "lower": lower,
                    "bandwidth": round(bandwidth, 4),
                    "atr": round(atr_val, 3),
                    "current_price": round(current_price, 3),
                },
            },
            "_counter_direction_allowed": bool(p.get("_counter_direction_allowed", True)),
        }

    def _hold(self, trace_id: str, reason: str, indicators: dict) -> dict:
        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": "HOLD",
                "confidence": 0.0,
                "reason": reason,
                "indicators": indicators,
            },
        }

    def get_strategy_id(self) -> str:
        return "boll_band"

    def get_params_schema(self) -> dict:
        return {
            "bb_period":     {"type": "int",   "default": 20, "range": [10, 50]},
            "bb_std_mult":   {"type": "float", "default": 2.0, "range": [1.5, 3.0]},
            "atr_period":    {"type": "int",   "default": 14, "range": [7, 28]},
            "sl_atr_mult":   {"type": "float", "default": 2.0, "range": [0.8, 4.0]},
            "tp_atr_mult":   {"type": "float", "default": 3.5, "range": [1.5, 6.0]},
            "min_confidence": {"type": "float", "default": 0.55, "range": [0.45, 0.70]},
        }
