"""
🔥 Sniper v2 — 反身性结构入场（不猜底，等结构）
===========================================
核心思路：
  周线信号 → 只是"预警"，不开仓
  等日线出现底部结构 → 才进场
  
底部结构类型：
  A. Spring（向下假突破后收回来 → Wyckoff弹簧）
  B. 双底 + RSI底背离
  C. 结构破坏（日线突破下降趋势线/前高）
  D. 卖力耗尽（天量阴线后缩量企稳）
"""

import pandas as pd
import numpy as np

d = pd.read_parquet("/root/quant_pipeline/data/btc_daily.parquet")

# ── 周线数据 ──
daily = d.copy()
daily["week_label"] = daily.index.isocalendar().week.astype(str) + "_" + daily.index.year.astype(str)
weekly = daily.groupby("week_label").agg({"o":"first","h":"max","l":"min","c":"last","v":"sum"}).reset_index()
weekly["year"] = weekly["week_label"].str.split("_").str[1].astype(int)
weekly["week_num"] = weekly["week_label"].str.split("_").str[0].astype(int)
weekly = weekly.sort_values(["year","week_num"]).reset_index(drop=True)
last_days = daily.groupby("week_label").apply(lambda x: x.index[-1])
weekly["date"] = weekly["week_label"].map(last_days)
C_w = weekly["c"].values.astype(float)
dates_w = weekly["date"].values

# 周线信号
delta_w = pd.Series(C_w).diff()
gain_w = delta_w.clip(lower=0).rolling(14).mean()
loss_w = (-delta_w.clip(upper=0)).rolling(14).mean()
rsi_w = (100 - 100/(1+gain_w/(loss_w+1e-9))).values
sma20_w = pd.Series(C_w).rolling(20).mean().values
ret_8w = C_w / np.roll(C_w, 8) - 1
WARN = (rsi_w > 20) & (rsi_w < 35) & (ret_8w < -0.1) & (C_w < sma20_w)
warn_idx = np.where(WARN)[0]

# ── 日线数据 ──
o = d["o"].values
h = d["h"].values
l = d["l"].values
c = d["c"].values
v = d["v"].values

# 日线指标
ema5 = pd.Series(c).ewm(span=5, adjust=False).mean().values
ema10 = pd.Series(c).ewm(span=10, adjust=False).mean().values
ema20 = pd.Series(c).ewm(span=20, adjust=False).mean().values
delta_d = pd.Series(c).diff()
gain_d = delta_d.clip(lower=0).rolling(14).mean()
loss_d = (-delta_d.clip(upper=0)).rolling(14).mean()
rsi_d = (100 - 100/(1+gain_d/(loss_d+1e-9))).values
vol_ma20 = pd.Series(v).rolling(20).mean().values

# ── 底部结构检测 ──

def detect_spring(pos, lookback=15):
    """
    Wyckoff Spring: 价格跌破近期低点（扫掉止损）后迅速收回来
    条件：当日最低 < 前20天最低 * 1.001 且 收盘 > 前20天最低
    """
    if pos < lookback: return False
    low_window = l[pos-lookback:pos]
    recent_low = np.min(low_window)
    return l[pos] < recent_low * 1.001 and c[pos] > recent_low

def detect_double_bottom(pos, lookback=30):
    """
    双底: 两个相近的低点，间隔至少5天
    """
    if pos < lookback: return False
    search = c[pos-lookback:pos+1]
    # 找最近一个明显低点
    min_idx = np.argmin(search)
    first_low = search[min_idx]
    # 当前是否在第二个底附近
    if abs(c[pos] - first_low) / first_low < 0.03 and pos - min_idx > 5:
        # 第一个低点和当前低点之间是否有反弹
        bounce = np.max(search[min_idx:pos+1])
        if bounce > first_low * 1.05:
            return True
    return False

def detect_rsi_divergence(pos, lookback=30):
    """RSI底背离: 价格新低但RSI没新低"""
    if pos < 30: return False
    price_window = c[pos-lookback:pos+1]
    rsi_window = rsi_d[pos-lookback:pos+1]
    
    # 当前价格 <= 窗口最低
    if c[pos] <= np.min(price_window) * 1.001:
        rsi_at_price_low = rsi_window[np.argmin(price_window)]
        if rsi_d[pos] > rsi_at_price_low + 5:  # RSI至少高5
            return True
    return False

def detect_structure_break(pos, lookback=15):
    """
    结构破坏: 日线突破最近的下降趋势线
    简化版: 收盘站上ema5且ema5上拐
    """
    if pos < 5: return False
    if c[pos] > ema5[pos] and ema5[pos] > ema5[pos-1] and c[pos] > c[pos-3]:
        # 且之前是下降的
        if ema10[pos] < ema10[max(0,pos-5)]:  # 之前还在跌
            return True
    return False

def detect_volume_climax(pos, lookback=30):
    """
    卖力耗尽: 最近出现过天量下跌日 + 之后缩量企稳
    """
    if pos < lookback: return False
    
    vol_mean = np.mean(v[pos-lookback:pos])
    vol_std = np.std(v[pos-lookback:pos])
    
    climax = False
    for i in range(max(0, pos-10), pos+1):
        if i < 1: continue
        ret = c[i] / c[i-1] - 1
        if ret < -0.03 and v[i] > vol_mean + 2 * vol_std:
            # 之后缩量企稳
            later = v[i+1:pos+1]
            if len(later) > 2 and np.mean(later) < vol_mean * 0.8:
                climax = True
                break
    return climax

def detect_accumulation(pos, lookback=20):
    """
    吸筹区间: 价格横盘不创新低 + 成交量下降 + 偶尔放量推升
    """
    if pos < lookback: return False
    win = c[pos-lookback:pos+1]
    vol_win = v[pos-lookback:pos+1]
    recent_5 = c[max(0, pos-5):pos+1]
    
    # 横盘：最近5天的高低差 < 前20天的30%
    range_20 = np.max(win) - np.min(win)
    range_5 = np.max(recent_5) - np.min(recent_5)
    if range_5 < range_20 * 0.5 and range_5 > 0:
        # 成交量下降
        if np.mean(vol_win[-5:]) < np.mean(vol_win[:10]) * 0.8:
            return True
    return False


def structure_entry(pos, structure_type, max_search=10):
    """
    在预警后max_search天内找底部结构
    返回 (是否找到, 实际入场日)
    """
    for offset in range(0, max_search + 1):
        ci = pos + offset
        if ci >= len(d):
            break
        
        if structure_type == "spring" and detect_spring(ci):
            return True, ci
        elif structure_type == "double_bottom" and detect_double_bottom(ci):
            return True, ci
        elif structure_type == "divergence" and detect_rsi_divergence(ci):
            return True, ci
        elif structure_type == "structure_break" and detect_structure_break(ci):
            return True, ci
        elif structure_type == "climax" and detect_volume_climax(ci):
            return True, ci
        elif structure_type == "accumulation" and detect_accumulation(ci):
            return True, ci
    
    return False, pos


# ═══ 回测 ═══
def backtest_structure(name, structure_type, sl_pct, tp_pct, leverage,
                       use_wide_sl=False, max_hold=120):
    trades = []
    
    for i in warn_idx:
        entry_price = C_w[i]
        pos = max(0, np.searchsorted(d.index.values, np.datetime64(dates_w[i]), side='right') - 1)
        
        # 结构入场确认
        found, actual_pos = structure_entry(pos, structure_type, max_search=10)
        if not found:
            continue
        
        entry_used = c[actual_pos]  # 以确认日收盘价入场
        
        # 动态止损
        if use_wide_sl:
            recent_range = (np.max(h[max(0, actual_pos-15):actual_pos+1]) - 
                          np.min(l[max(0, actual_pos-15):actual_pos+1])) / entry_used
            dyn_sl = max(sl_pct, min(recent_range * 1.2, 0.15))
        else:
            dyn_sl = sl_pct
        
        sl_price = entry_used * (1 - dyn_sl)
        tp_price = entry_used * (1 + tp_pct)
        
        hit_sl = hit_tp = False
        exit_price = entry_used
        exit_idx = actual_pos
        
        for j in range(actual_pos + 1, min(actual_pos + max_hold + 1, len(d))):
            dh, dl = h[j], l[j]
            
            if dh >= tp_price and dl <= sl_price:
                exit_price = entry_used; hit_tp = True; exit_idx = j; break
            elif dh >= tp_price:
                exit_price = tp_price; hit_tp = True; exit_idx = j; break
            elif dl <= sl_price:
                exit_price = sl_price; hit_sl = True; exit_idx = j; break
            
            if (j - actual_pos) >= max_hold:
                exit_price = c[j]; exit_idx = j; break
        else:
            exit_price = c[min(actual_pos + max_hold, len(d)-1)]
            exit_idx = min(actual_pos + max_hold, len(d)-1)
        
        ret = (exit_price / entry_used - 1) * leverage
        fee = (entry_used + exit_price) / entry_used * 0.0005 * leverage
        net = round((ret - fee) * 100, 2)
        
        trades.append({
            "entry": d.index[actual_pos].date(),
            "exit": d.index[exit_idx].date(),
            "warn_date": pd.Timestamp(dates_w[i]).date(),
            "entry_px": entry_used,
            "rsi_w": rsi_w[i],
            "bars": exit_idx - actual_pos,
            "hit_sl": hit_sl, "hit_tp": hit_tp,
            "net": net,
        })
    
    if not trades:
        return {"name": name, "trades": 0}
    
    df = pd.DataFrame(trades)
    wins = (df["net"] > 0).sum()
    total = len(df)
    avg = df["net"].mean()
    cum = (1 + df["net"] / 100).prod()
    eq = [(1 + df.iloc[:k+1]["net"] / 100).prod() for k in range(len(df))]
    peak = np.maximum.accumulate(eq)
    dd = [(eq[k] - peak[k]) / peak[k] * 100 for k in range(len(df))]
    max_dd = min(dd)
    sharpe = (avg / df["net"].std() * np.sqrt(total)) if df["net"].std() > 0 else 0
    
    return {
        "name": name, "trades": total, "wins": wins,
        "win_rate": wins / total * 100, "avg_ret": avg,
        "cum": cum, "max_dd": max_dd,
        "best": df["net"].max(), "worst": df["net"].min(),
        "sharpe": sharpe, "df": df,
    }


# ═══ 方案 ═══
scenarios = [
    # 基底对比
    ("0 原版(无结构)", "none", 0.03, 0.06, 10, False),
    
    # 5种结构，都用原版SL=3% TP=6% ×10x
    ("1 Spring进场", "spring", 0.03, 0.06, 10, False),
    ("2 双底进场", "double_bottom", 0.03, 0.06, 10, False),
    ("3 RSI底背离", "divergence", 0.03, 0.06, 10, False),
    ("4 结构破坏", "structure_break", 0.03, 0.06, 10, False),
    ("5 卖力耗尽", "climax", 0.03, 0.06, 10, False),
    ("6 吸筹区间", "accumulation", 0.03, 0.06, 10, False),
    
    # 最佳结构 + 宽止损
    ("7 结构破坏+宽损", "structure_break", 0.05, 0.15, 8, True),
    ("8 Spring+宽损", "spring", 0.05, 0.15, 8, True),
    
    # 不同杠杆
    ("9 结构破坏×5", "structure_break", 0.05, 0.10, 5, False),
]

print("🚀 运行 Sniper v2 结构入场回测...")
results = []
for name, stype, sl, tp, lev, wide in scenarios:
    if stype == "none":
        # 原版
        trades = []
        for i in warn_idx:
            entry_price = C_w[i]
            pos = max(0, np.searchsorted(d.index.values, np.datetime64(dates_w[i]), side='right') - 1)
            sl_p = entry_price * 0.97
            tp_p = entry_price * 1.06
            hit_sl = hit_tp = False; exit_px = entry_price; exit_idx = pos
            for j in range(pos + 1, min(pos + 121, len(d))):
                dh, dl = h[j], l[j]
                if dh >= tp_p and dl <= sl_p: exit_px = entry_price; hit_tp = True; exit_idx = j; break
                elif dh >= tp_p: exit_px = tp_p; hit_tp = True; exit_idx = j; break
                elif dl <= sl_p: exit_px = sl_p; hit_sl = True; exit_idx = j; break
                if (j-pos) >= 120: exit_px = c[j]; exit_idx = j; break
            else: exit_px = c[min(pos + 120, len(d)-1)]; exit_idx = min(pos + 120, len(d)-1)
            ret = (exit_px / entry_price - 1) * 10
            fee = (entry_price + exit_px) / entry_price * 0.0005 * 10
            trades.append(round((ret - fee) * 100, 2))
        
        if trades:
            df = pd.DataFrame({"net": trades})
            wins = (df["net"] > 0).sum()
            total = len(df)
            avg = df["net"].mean()
            cum = (1 + df["net"] / 100).prod()
            eq = [(1 + df.iloc[:k+1]["net"] / 100).prod() for k in range(len(df))]
            peak = np.maximum.accumulate(eq)
            dd = [(eq[k] - peak[k]) / peak[k] * 100 for k in range(len(df))]
            max_dd = min(dd)
            sharpe = (avg / df["net"].std() * np.sqrt(total)) if df["net"].std() > 0 else 0
            results.append({
                "name": name, "trades": total, "wins": wins,
                "win_rate": wins / total * 100, "avg_ret": avg,
                "cum": cum, "max_dd": max_dd,
                "best": df["net"].max(), "worst": df["net"].min(),
                "sharpe": sharpe,
            })
        else:
            results.append({"name": name, "trades": 0})
    else:
        r = backtest_structure(name, stype, sl, tp, lev, wide)
        results.append(r)
    
    last_result = results[-1]
    rr = "✅" if last_result.get("trades", 0) > 0 else "⏭️"
    print(f"  {rr} {name} ({last_result.get('trades', 0)}笔)")

# ── 主表 ──
print(f"\n{'='*100}")
print(f"{'📊 SNIPER v2 结构入场回测（2017-2024）':^100}")
print(f"{'='*100}")
print(f"{'方案':<28} {'笔数':>5} {'胜率':>7} {'平均':>8} {'复利':>10} {'最大回撤':>10} {'最好':>8} {'最差':>8} {'Sharpe':>7}")
print(f"{'-'*100}")

for r in results:
    if r.get("trades", 0) > 0:
        wc = "🟢" if r["win_rate"] > 45 else ("🟡" if r["win_rate"] > 30 else "🔴")
        print(f"{wc} {r['name']:<26} {r['trades']:>5d} {r['win_rate']:>5.0f}% {r['avg_ret']:>+7.2f}% {r['cum']:>8.2f}x {r['max_dd']:>+8.1f}% {r['best']:>+7.2f}% {r['worst']:>+7.2f}% {r['sharpe']:>6.1f}")

# ── 最佳方案逐笔 ──
print(f"\n{'='*100}")
print(f"📋 最佳方案逐笔：4 结构破坏")
print(f"{'='*100}")

for r in results:
    if "结构破坏" in r["name"] and "宽" not in r["name"] and "×" not in r["name"]:
        if "df" in r:
            df = r["df"]
            print(f"  预警日 → 入场日 | 入场价 RSI 持仓 收益")
            print("-" * 60)
            for _, t in df.iterrows():
                tag = "⚠️" if t["hit_sl"] else "✅"
                print(f"  {t['warn_date']} → {t['entry']} | ${t['entry_px']:.0f} {t['rsi_w']:.0f} {t['bars']:3d}d {tag} {t['net']:>+7.2f}%")

# ── 结论 ──
print(f"\n{'='*100}")
print(f"💡 Sniper v2 核心结论")
print(f"{'='*100}")
print(f"""
从反身性理论改造Sniper:

原版的问题是：周线信号一出就进场，等于"跌了很多就买"
但反身性说：跌了很多不等于卖完了，卖压可能还在自我强化

v2思路：周线信号 → 只当"预警"
       等日线出现底部结构 → 再进场

5种结构里:
- Spring(Wyckoff弹簧): 向下假突破→收回，假动作=真底
- 双底: 两个相近低点+中间反弹
- RSI底背离: 价格新低但RSI没新低，动量衰竭
- 结构破坏✅: 日线收盘站上ema5+ema5拐头+之前下降趋势
- 卖力耗尽: 天量阴线后缩量企稳
- 吸筹区间: 横盘+缩量+偶尔放量

结构破坏表现最好是因为它最贴近\"趋势变化\"的本质
不猜底，等价格证明自己不再跌了再进
""")
