#!/usr/bin/env python3
"""
WyckoffSignalGenerator — Wyckoff 量价结构信号生成器

Wyckoff 三大定律（适配黄金外汇）：

  第一定律：供需决定价格
    - 供应扩大（价跌量增）→ 下跌趋势
    - 需求扩大（价涨量增）→ 上涨趋势
    - 缩量横盘 → 供需暂时平衡

  第二定律：因果定律
    - 任何主要趋势前都有蓄力（Accumulation/Distribution）
    - 蓄力时间长，趋势幅度大
    - 蓄力区间（Trading Range）越大，突破空间越大

  第三定律：努力与结果定律
    - 成交量放大但价格变动小 → 趋势即将反转
    - 价格快速移动但成交量萎缩 → 趋势即将衰竭

Wyckoff 核心结构（4阶段）：

  1. Accumulation（吸筹）:
     - 价格在区间内震荡，成交量逐步放大
     - SOS (Sign of Strength): 向上突破区间，成交量放大
     - 吸筹区间支撑（PS, SC, ST）
     - → 突破后买入（Phase B 入场）

  2. Markup（上涨）:
     - 价格持续上涨，创出新高
     - 回踩不破区间上沿（Spring / UT）
     - 动力足（价涨量增）

  3. Distribution（派发）:
     - 高位震荡，成交量不规则放大
     - SUP (Supply): 供给出现区域
     - UT (Up-thrust): 假突破后快速回落

  4. Markdown（下跌）:
     - 价格持续下跌，伴随恐慌抛售（Breadth Panic）
     - 价格创新低（Lowest低）

Schaff Trend Cycle 检测 Wyckoff 结构：
  - MFI (Money Flow Index) 量价综合指标
  - 结构突破确认（区间上轨/下轨突破）
  - 量价背离（CMO/Chaikin's Volume）
"""
import time
import uuid
import math
from typing import Optional, Tuple, List

from .base import BaseSignalGenerator


# ── Internal Indicators ────────────────────────────────────────────────────────

def _atr(bars, period=14) -> float:
    if len(bars) < 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, len(bars))]
    return sum(trs[-period:]) / period if len(trs) >= period else 0.0


def _sma(data: List[float], period: int) -> float:
    if len(data) < period:
        return sum(data) / len(data) if data else 0.0
    return sum(data[-period:]) / period


def _ema(data: List[float], period: int) -> float:
    if len(data) < period:
        return sum(data) / len(data) if data else 0.0
    k = 2 / (period + 1)
    ema = sum(data[:period]) / period
    for v in data[period:]:
        ema = v * k + ema * (1 - k)
    return ema


def _adx(bars, period=14) -> float:
    if len(bars) < period * 3:
        return 0.0

    def ema(data, n):
        if len(data) < n:
            return [0.0] * len(data)
        k = 2 / (n + 1)
        result = [sum(data[:n]) / n]
        for v in data[n:]:
            result.append(v * k + result[-1] * (1 - k))
        return result

    closes = [b["close"] for b in bars]
    highs = [b["high"] for b in bars]
    lows = [b["low"] for b in bars]
    volumes = [b.get("tick_volume", 1) for b in bars]

    plus_dm, minus_dm, trs = [], [], []
    for i in range(1, len(bars)):
        hd, ld = highs[i] - highs[i - 1], lows[i - 1] - lows[i]
        plus_dm.append(max(hd, 0) if hd > ld else 0)
        minus_dm.append(max(ld, 0) if ld > hd else 0)
        trs.append(max(highs[i] - lows[i], abs(highs[i] - closes[i - 1]), abs(lows[i] - closes[i - 1])))

    if len(trs) < period:
        return 0.0

    atr_s = ema(trs, period)
    dmp_s = ema(plus_dm, period)
    dmm_s = ema(minus_dm, period)

    di_range = min(len(atr_s), len(dmp_s), len(dmm_s))
    plus_di = [100 * dmp_s[i] / atr_s[i] if atr_s[i] > 0 else 0 for i in range(di_range)]
    minus_di = [100 * dmm_s[i] / atr_s[i] if atr_s[i] > 0 else 0 for i in range(di_range)]
    dx = [100 * abs(plus_di[i] - minus_di[i]) / (plus_di[i] + minus_di[i])
          if (plus_di[i] + minus_di[i]) > 0 else 0 for i in range(di_range)]
    adx_result = ema(dx, period)
    return adx_result[-1] if adx_result else 0.0


def _volume_profile(bars) -> Tuple[float, float, float]:
    """
    量价分析：
    返回 (avg_vol, current_vol_ratio, vol_trend)
    vol_ratio > 1: 当前成交量高于平均（趋势确认或派发）
    vol_ratio < 1: 当前成交量低于平均（趋势可能衰竭）
    """
    if len(bars) < 20:
        return 1.0, 1.0, 0.0

    volumes = [float(b.get("tick_volume", 1)) for b in bars]
    recent_vol = sum(volumes[-5:]) / 5
    avg_vol = sum(volumes[-20:]) / 20

    vol_ratio = recent_vol / (avg_vol + 0.0001)

    # 成交量趋势：近期成交量是否在放大
    mid_vol = sum(volumes[-10:-5]) / 5 if len(volumes) >= 10 else avg_vol
    vol_trend = (recent_vol - mid_vol) / (mid_vol + 0.0001)  # >0 = 放大

    return avg_vol, vol_ratio, vol_trend


def _mfi(bars, period=14) -> float:
    """
    Money Flow Index (MFI): 量价版 RSI
    MFI > 80: 超买（可能派发/下跌前兆）
    MFI < 20: 超卖（可能吸筹/上涨前兆）
    """
    if len(bars) < period + 1:
        return 50.0

    typical_prices = [(b["high"] + b["low"] + b["close"]) / 3 for b in bars]
    volumes = [float(b.get("tick_volume", 1)) for b in bars]

    raw_mf = [tp * vol for tp, vol in zip(typical_prices, volumes)]

    pos_mf, neg_mf = 0.0, 0.0
    for i in range(1, len(typical_prices)):
        if typical_prices[i] > typical_prices[i - 1]:
            pos_mf += raw_mf[i]
        else:
            neg_mf += raw_mf[i]

    if neg_mf == 0:
        return 100.0
    mr = pos_mf / neg_mf
    return 100 - (100 / (1 + mr))


def _supply_demand_index(bars) -> Tuple[float, float, float]:
    """
    简化的供需指数：
    - cum_delta > 0 → 需求主导（上涨）
    - cum_delta < 0 → 供给主导（下跌）
    返回: (supply_demand_ratio, cum_delta_pct, force)
    """
    if len(bars) < 20:
        return 1.0, 0.0, 0.0

    closes = [b["close"] for b in bars]
    volumes = [float(b.get("tick_volume", 1)) for b in bars]

    # 计算每根K线的供需力
    deltas = []
    for i in range(1, len(bars)):
        price_change = closes[i] - closes[i - 1]
        vol = volumes[i]
        # 价涨量增 = 需求（正），价跌量增 = 供给（负）
        delta = price_change * vol
        deltas.append(delta)

    if len(deltas) < 10:
        return 1.0, 0.0, 0.0

    # EMA 平滑
    cum_delta = _ema(deltas, 10)
    avg_delta = _ema([abs(d) for d in deltas], 10)

    if avg_delta == 0:
        return 1.0, 0.0, 0.0

    ratio = (cum_delta + avg_delta) / (2 * avg_delta)  # >1 需求, <1 供给
    cum_delta_pct = cum_delta / (avg_delta + 0.0001)

    # Wyckoff 努力与结果：成交量 vs 价格变动
    recent_force = cum_delta

    return ratio, cum_delta_pct, recent_force


def _detect_trading_range(bars, lookback=60) -> Tuple[Optional[float], Optional[float], str]:
    """
    检测交易区间（蓄力/派发区间）：
    - high = 区间的上轨（PSY/SOW上轨）
    - low = 区间的下轨（PS/SC/LPS下轨）
    - phase = "accumulation" | "distribution" | "none"
    """
    if len(bars) < lookback:
        return None, None, "none"

    recent = bars[-lookback:]

    highs = [b["high"] for b in recent]
    lows = [b["low"] for b in recent]
    closes = [b["close"] for b in recent]
    volumes = [float(b.get("tick_volume", 1)) for b in recent]

    tr_high = max(highs)
    tr_low = min(lows)
    tr_mid = (tr_high + tr_low) / 2
    tr_range = tr_high - tr_low
    atr_val = _atr(bars, 14)
    tr_width_atr = tr_range / (atr_val + 0.0001)

    # 判断区间内量价行为
    first_half_vol = sum(volumes[:lookback // 2]) / (lookback // 2)
    second_half_vol = sum(volumes[lookback // 2:]) / (lookback - lookback // 2)
    vol_change = (second_half_vol - first_half_vol) / (first_half_vol + 0.0001)

    # 判断区间方向
    # 区间内价格倾向于区间上半部 → 吸筹可能性大
    recent_closes = closes[-lookback // 3:]
    avg_recent_close = sum(recent_closes) / len(recent_closes)
    close_in_range = (avg_recent_close - tr_low) / (tr_range + 0.0001)  # 0~1

    # 吸筹 vs 派发判定
    # 吸筹：成交量逐步放大 + 价格在区间中上部
    # 派发：成交量不规则放大 + 价格在区间中下部
    if close_in_range > 0.55 and vol_change > 0:
        phase = "accumulation"
    elif close_in_range < 0.45:
        phase = "distribution"
    else:
        phase = "none"  # 区间不明显

    return tr_high, tr_low, phase


def _wyckoff_spring_test(bars, tr_high: float, tr_low: float,
                         atr_val: float) -> Tuple[str, float, float, float, float, str]:
    """
    Wyckoff Spring（弹簧效应）和 Up-Thrust 检测：
    Spring: 价格短暂跌破支撑（TR Low）后快速收回 → 做多信号
    Up-Thrust: 价格短暂突破阻力（TR High）后快速回落 → 做空信号

    条件：
    - 假突破幅度 <= 0.8 * ATR
    - 收回时间 <= 3 根K线
    - 突破时成交量萎缩（努力小但结果大）
    """
    if len(bars) < 10:
        return "HOLD", 0.0, None, None, None, "bars不足"

    recent = bars[-10:]

    # 找最近的低点（是否有跌破 tr_low）
    spring_found = False
    for i in range(len(recent) - 1, -1, -1):
        if recent[i]["low"] < tr_low:
            # 检查是否在3根内收回
            for j in range(i, min(i + 3, len(recent))):
                if recent[j]["close"] > tr_low:
                    spring_depth = (tr_low - recent[i]["low"]) / (atr_val + 0.0001)
                    vol_spring = float(recent[i].get("tick_volume", 1))
                    avg_vol = sum(float(b.get("tick_volume", 1)) for b in recent[-5:]) / 5
                    vol_ratio_spring = vol_spring / (avg_vol + 0.0001)
                    if spring_depth < 0.8 and vol_ratio_spring < 1.0:
                        spring_found = True
                        entry = recent[j]["close"]
                        sl = recent[i]["low"] - 0.5 * atr_val
                        tp = tr_high + 0.5 * atr_val
                        conf = 0.75 - spring_depth * 0.1
                        return "BUY", conf, entry, sl, tp, (
                            f"Wyckoff Spring 弹簧效应 | "
                            f"假跌破={spring_depth:.2f}ATR | "
                            f"区间下轨={tr_low:.2f}"
                        )
                    break

    # 找最近的假突破 tr_high (Up-Thrust)
    for i in range(len(recent) - 1, -1, -1):
        if recent[i]["high"] > tr_high:
            for j in range(i, min(i + 3, len(recent))):
                if recent[j]["close"] < tr_high:
                    thrust_depth = (recent[i]["high"] - tr_high) / (atr_val + 0.0001)
                    vol_thrust = float(recent[i].get("tick_volume", 1))
                    avg_vol = sum(float(b.get("tick_volume", 1)) for b in recent[-5:]) / 5
                    vol_ratio_thrust = vol_thrust / (avg_vol + 0.0001)
                    if thrust_depth < 0.8 and vol_ratio_thrust < 1.0:
                        entry = recent[j]["close"]
                        sl = recent[i]["high"] + 0.5 * atr_val
                        tp = tr_low - 0.5 * atr_val
                        conf = 0.75 - thrust_depth * 0.1
                        return "SELL", conf, entry, sl, tp, (
                            f"Wyckoff Up-Thrust 假突破 | "
                            f"假突破={thrust_depth:.2f}ATR | "
                            f"区间上轨={tr_high:.2f}"
                        )
                    break

    return "HOLD", 0.0, None, None, None, "无弹簧/假突破结构"


def _wyckoff_trend_signal(bars, tr_high: float, tr_low: float,
                          atr_val: float, adx_val: float) -> Tuple[str, float, float, float, float, str]:
    """
    Wyckoff 趋势跟随信号（区间突破）：
    - SOS (Sign of Strength): 向上突破 TR 区间 + 成交量放大 → 做多
    - SOW (Sign of Weakness): 向下突破 TR 区间 + 成交量放大 → 做空
    """
    if len(bars) < 5 or tr_high is None:
        return "HOLD", 0.0, None, None, None, None, "数据不足"

    latest_close = bars[-1]["close"]
    prev_close = bars[-2]["close"]
    latest_vol = float(bars[-1].get("tick_volume", 1))
    avg_vol = sum(float(b.get("tick_volume", 1)) for b in bars[-20:-1]) / 20
    vol_ratio = latest_vol / (avg_vol + 0.0001)

    tr_range = tr_high - tr_low
    breakout_threshold = 0.3 * tr_range  # 突破 30% 区间幅度

    # SOS: 向上突破区间 + 成交量放大
    if latest_close > tr_high and prev_close <= tr_high:
        if vol_ratio > 1.0:
            sl = tr_high - 0.5 * atr_val
            entry = latest_close
            tp = tr_high + 1.618 * atr_val
            conf = min(0.72 + (vol_ratio - 1.0) * 0.1, 0.88)
            return "BUY", conf, entry, sl, tp, (
                f"Wyckoff SOS 强势信号 | 向上突破区间 | "
                f"区间={tr_low:.2f}~{tr_high:.2f} | 成交量×{vol_ratio:.1f}"
            )

    # SOW: 向下突破区间 + 成交量放大
    if latest_close < tr_low and prev_close >= tr_low:
        if vol_ratio > 1.0:
            sl = tr_low + 0.5 * atr_val
            entry = latest_close
            tp = tr_low - 1.618 * atr_val
            conf = min(0.72 + (vol_ratio - 1.0) * 0.1, 0.88)
            return "SELL", conf, entry, sl, tp, (
                f"Wyckoff SOW 弱势信号 | 向下突破区间 | "
                f"区间={tr_low:.2f}~{tr_high:.2f} | 成交量×{vol_ratio:.1f}"
            )

    return "HOLD", 0.0, None, None, None, "无区间突破信号"


# ── Signal Generator ──────────────────────────────────────────────────────────

class WyckoffSignalGenerator(BaseSignalGenerator):

    DEFAULT_PARAMS = {
        "wyckoff_lookback": 60,       # 交易区间检测回溯K线数
        "wyckoff_vol_threshold": 1.0,  # 成交量放大门槛（vs 平均）
        "wyckoff_adx_min": 20,         # ADX 最低门槛（极弱趋势不用）
        "wyckoff_mfi_oversold": 20,    # MFI 超卖门槛（吸筹信号）
        "wyckoff_mfi_overbought": 80,  # MFI 超买门槛（派发信号）
        "wyckoff_confidence_weight": 0.78,
        "wyckoff_phase_filter": "all",  # "all" | "accumulation" | "distribution"
    }

    def __init__(self, params: dict = None):
        self.params = {**self.DEFAULT_PARAMS, **(params or {})}

    def analyze(self, bars: list, params: dict = None) -> dict:
        p = {**self.params, **(params or {})}
        trace_id = str(uuid.uuid4())[:8]

        lookback = int(p["wyckoff_lookback"])
        if len(bars) < lookback + 10:
            return self._hold(trace_id, f"bars不足（需要{lookback + 10}根以上）")

        atr_val = _atr(bars, period=14)
        adx_val = _adx(bars, period=14)
        mfi_val = _mfi(bars, period=14)
        avg_vol, vol_ratio, vol_trend = _volume_profile(bars)
        ratio, cum_delta_pct, force = _supply_demand_index(bars)

        if atr_val <= 0:
            return self._hold(trace_id, "ATR计算异常")

        # ADX 过滤
        if adx_val < p["wyckoff_adx_min"]:
            return self._hold(trace_id,
                f"ADX={adx_val:.1f}<{p['wyckoff_adx_min']}（趋势极弱，Wyckoff信号不可靠）",
                indicators={"adx": round(adx_val, 2), "mfi": round(mfi_val, 1),
                            "vol_ratio": round(vol_ratio, 2)})

        # 检测交易区间
        tr_high, tr_low, phase = _detect_trading_range(bars, lookback=lookback)

        # Phase filter
        phase_filter = p.get("wyckoff_phase_filter", "all")
        if phase_filter != "all" and phase != phase_filter:
            return self._hold(trace_id,
                f"Wyckoff阶段={phase}（过滤器: {phase_filter}）",
                indicators={"phase": phase, "adx": round(adx_val, 2),
                            "tr_high": tr_high, "tr_low": tr_low,
                            "mfi": round(mfi_val, 1), "vol_ratio": round(vol_ratio, 2)})

        if tr_high is None:
            return self._hold(trace_id, "无有效交易区间（Wyckoff结构需区间蓄力）")

        # ── 信号合成 ──────────────────────────────────────────────────────────

        # 1. Spring / Up-Thrust（弹簧/假突破）
        action1, conf1, entry1, sl1, tp1, reason1 = _wyckoff_spring_test(
            bars, tr_high, tr_low, atr_val
        )

        # 2. 趋势区间突破（SOS/SOW）
        action2, conf2, entry2, sl2, tp2, reason2 = _wyckoff_trend_signal(
            bars, tr_high, tr_low, atr_val, adx_val
        )

        # 3. MFI 超卖/超买（吸筹/派发极值）
        action3, conf3, entry3, sl3, tp3, reason3 = "HOLD", 0.0, None, None, None, ""
        latest_close = bars[-1]["close"]

        if mfi_val < p["wyckoff_mfi_oversold"] and phase == "accumulation":
            action3 = "BUY"
            conf3 = 0.65 + (20 - mfi_val) / 20 * 0.15
            entry3 = latest_close
            sl3 = tr_low - 0.5 * atr_val
            tp3 = tr_high + 0.618 * atr_val
            reason3 = f"MFI超卖={mfi_val:.0f} | 吸筹区间 | 低吸机会"
        elif mfi_val > p["wyckoff_mfi_overbought"] and phase == "distribution":
            action3 = "SELL"
            conf3 = 0.65 + (mfi_val - 80) / 20 * 0.15
            entry3 = latest_close
            sl3 = tr_high + 0.5 * atr_val
            tp3 = tr_low - 0.618 * atr_val
            reason3 = f"MFI超买={mfi_val:.0f} | 派发区间 | 高空机会"

        # 选择最强信号（置信度最高）
        candidates = [
            (action1, conf1, entry1, sl1, tp1, reason1),
            (action2, conf2, entry2, sl2, tp2, reason2),
            (action3, conf3, entry3, sl3, tp3, reason3),
        ]

        best = max(candidates, key=lambda x: x[1] if x[0] != "HOLD" else 0)
        action, confidence, entry, sl, tp, reason = best

        confidence *= float(p["wyckoff_confidence_weight"])

        side = None
        if action in ("BUY", "SELL"):
            side = action

        return {
            "schema_version": "2.1.0",
            "trace_id": trace_id,
            "strategy_id": "wyckoff_v1",
            "model_version": None,
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
            "payload": {
                "action": action,
                "confidence": round(confidence, 4),
                "reason": reason,
                "indicators": {
                    "wyckoff_phase": phase,
                    "tr_high": round(tr_high, 2) if tr_high else None,
                    "tr_low": round(tr_low, 2) if tr_low else None,
                    "tr_range_atr": round((tr_high - tr_low) / (atr_val + 0.0001), 2) if tr_high else None,
                    "mfi": round(mfi_val, 1),
                    "vol_ratio": round(vol_ratio, 2),
                    "vol_trend": round(vol_trend, 3),
                    "supply_demand_ratio": round(ratio, 3),
                    "cum_delta_pct": round(cum_delta_pct, 4),
                    "adx": round(adx_val, 2),
                    "atr": round(atr_val, 2),
                },
                "side": side,
                "entry": round(entry, 2) if entry else None,
                "sl": round(sl, 2) if sl else None,
                "tp": round(tp, 2) if tp else None,
            }
        }

    def _hold(self, trace_id: str, reason: str, indicators: dict = None) -> dict:
        return {
            "schema_version": "2.1.0",
            "trace_id": trace_id,
            "strategy_id": "wyckoff_v1",
            "model_version": None,
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
            "payload": {
                "action": "HOLD",
                "confidence": 0.0,
                "reason": reason,
                "indicators": indicators or {},
                "side": None,
                "entry": None,
                "sl": None,
                "tp": None,
            }
        }

    def get_params_schema(self) -> dict:
        return {
            "wyckoff_lookback": {"type": "int", "default": 60, "range": [30, 120]},
            "wyckoff_vol_threshold": {"type": "float", "default": 1.0, "range": [0.5, 2.0]},
            "wyckoff_adx_min": {"type": "int", "default": 20, "range": [15, 30]},
            "wyckoff_mfi_oversold": {"type": "int", "default": 20, "range": [10, 30]},
            "wyckoff_mfi_overbought": {"type": "int", "default": 80, "range": [70, 90]},
            "wyckoff_confidence_weight": {"type": "float", "default": 0.78, "range": [0.5, 1.0]},
            "wyckoff_phase_filter": {"type": "str", "default": "all",
                                      "options": ["all", "accumulation", "distribution"]},
        }


def create_generator(strategy_type: str = "wyckoff", params: dict = None) -> BaseSignalGenerator:
    if strategy_type in ("wyckoff", "wyck"):
        return WyckoffSignalGenerator(params)
    raise ValueError(f"Unknown Wyckoff strategy: {strategy_type}")
