"""SKH2_ENS_PARAM — within-sleeve PARAM ENSEMBLE for Skyhook DD reduction.

Family: equal-weight the DAILY returns of K diverse skyhook param sets (incl. the V2 winner),
varying ptn_n {25,45,90}, exits, sl/tp. Diversification across configs smooths equity and cuts
standalone DD without killing hold-out. We:
  * build each config's per-asset 230m equity (sk.run_asset) -> daily returns,
  * equal-weight average the configs' daily returns PER ASSET -> ensemble per-asset equity ->
    standalone DD (max over BTC/ETH) and per-asset/year/full/hold Sharpe via the SAME _split logic,
  * fee sweep: re-run each config at fee f, average daily, recompute Sharpe (fee_ok = Sharpe>0 @0.30% RT),
  * causality: every member is a pure SkyhookParams variant -> sk.causality on each (must be ok),
  * marginal: feed the 50/50 ensemble daily series to altlib.marginal_vs_tp01.

Standalone max_dd for the ensemble = max-DD of the COMBINED (averaged) per-asset equity curve.
All causal/leak-free: ensemble is a linear combo of leak-free member equities; no future data used.
"""
from __future__ import annotations

import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/skyhook")

import numpy as np
import pandas as pd

import skyhooklib as sk
import altlib as al
from src.strategies.skyhook import SkyhookParams

HOLDOUT = sk.HOLDOUT
FEE = sk.FEE_RT
CERTIFIED = ("BTC", "ETH")
ANN = np.sqrt(365.25)

# The verified V2 winner (must be a member of every ensemble).
WINNER = SkyhookParams(ptn_n=45, sl_atr=2.5, tp_atr=7.0, uscitalong=24, uscitashort=16,
                       vola_lo=35.0, vola_hi=95.0, vol_lo=0.0)


def _sharpe(r: np.ndarray) -> float:
    r = r[np.isfinite(r)]
    return float(np.mean(r) / np.std(r) * ANN) if len(r) > 2 and np.std(r) > 0 else 0.0


def _dd_from_eq(eq: np.ndarray) -> float:
    pk = np.maximum.accumulate(eq)
    return float(np.max((pk - eq) / pk)) if len(eq) else 0.0


# ---------------------------------------------------------------------------
# Per-config DAILY equity-return series per asset, cached by (config-id, asset, fee).
# We use sk.run_asset to get the leak-free 230m equity, then resample to daily LAST and
# take pct_change -> daily returns. Aligning all members on the same daily index lets us
# equal-weight-average their daily returns (an equal-capital rebalanced ensemble).
# ---------------------------------------------------------------------------
_DAILY_CACHE: dict = {}
_NTR_CACHE: dict = {}


def _config_daily(p: SkyhookParams, asset: str, fee: float) -> pd.Series:
    key = (id(p), asset, fee)
    if key in _DAILY_CACHE:
        return _DAILY_CACHE[key]
    r = sk.run_asset(asset, p, fee)
    s = pd.Series(r["_eq"], index=r["_idx"])
    daily = s.resample("1D").last().ffill().pct_change().dropna()
    _DAILY_CACHE[key] = daily
    _NTR_CACHE[(id(p), asset)] = r["full"]["n_trades"]
    return daily


def _ensemble_daily_asset(members, asset: str, fee: float) -> pd.Series:
    """Equal-weight average of members' daily returns for one asset (common dates)."""
    cols = {f"m{i}": _config_daily(p, asset, fee) for i, p in enumerate(members)}
    J = pd.concat(cols, axis=1, join="inner").fillna(0.0)
    return J.mean(axis=1)


def study_ensemble(name: str, members) -> dict:
    """FULL+HOLD+fee-sweep+per-year on BOTH assets for the equal-weight param ensemble.
    Standalone DD = max-DD of the averaged per-asset equity curve."""
    per_asset = {}
    fee_ok_all = True
    for a in CERTIFIED:
        ens = _ensemble_daily_asset(members, a, FEE)
        eq = np.cumprod(1.0 + ens.values)
        idx = ens.index
        full_sh = _sharpe(ens.values)
        full_dd = _dd_from_eq(eq)
        full_ret = float(eq[-1] / eq[0] - 1) if len(eq) else 0.0
        hmask = idx >= HOLDOUT
        rh = ens.values[hmask]
        eqh = np.cumprod(1.0 + rh) if rh.size else np.array([1.0])
        hold_sh = _sharpe(rh)
        hold_ret = float(eqh[-1] / eqh[0] - 1) if eqh.size else 0.0
        hold_dd = _dd_from_eq(eqh)
        # per-year Sharpe-equivalent return
        yearly = {}
        for y in sorted(set(idx.year)):
            ry = ens.values[idx.year == y]
            eqy = np.cumprod(1.0 + ry) if ry.size else np.array([1.0])
            yearly[int(y)] = float(eqy[-1] - 1.0) if eqy.size else 0.0
        # fee sweep
        sweep = {}
        for f in (0.0, 0.001, 0.002, 0.003):
            ensf = _ensemble_daily_asset(members, a, f)
            sweep[f"{f*100:.2f}%"] = round(_sharpe(ensf.values), 3)
        fee_ok_all = fee_ok_all and (sweep["0.30%"] > 0)
        # n_trades = sum across members (the ensemble trades all of them)
        ntr = sum(_NTR_CACHE.get((id(p), a), 0) for p in members)
        per_asset[a] = dict(
            full=dict(sharpe=round(full_sh, 3), ret=round(full_ret, 4), maxdd=round(full_dd, 4),
                      n_trades=int(ntr)),
            hold=dict(sharpe=round(hold_sh, 3), ret=round(hold_ret, 4), maxdd=round(hold_dd, 4)),
            yearly=yearly, fee_sweep=sweep)
    mf = min(per_asset[a]["full"]["sharpe"] for a in per_asset)
    mh = min(per_asset[a]["hold"]["sharpe"] for a in per_asset)
    mt = min(per_asset[a]["full"]["n_trades"] for a in per_asset)
    mdd = max(per_asset[a]["full"]["maxdd"] for a in per_asset)
    grade = "PASS" if (mt >= 20 and mf >= 0.5 and mh >= 0.2 and fee_ok_all) else \
            ("WEAK" if (mt >= 20 and mf >= 0.3 and mh >= 0.0) else "FAIL")
    print(f"\n=== {name} -> {grade}  (minFull={mf:+.2f} minHold={mh:+.2f} minTr={mt} maxDD={mdd*100:.0f}% feeOK={fee_ok_all})")
    for a in per_asset:
        pa = per_asset[a]
        yr = " ".join(f"{y}:{r*100:+.0f}%" for y, r in pa["yearly"].items())
        print(f"  {a}: FULL Sh={pa['full']['sharpe']:+.2f} ret={pa['full']['ret']*100:+.0f}% DD={pa['full']['maxdd']*100:.0f}%"
              f" n={pa['full']['n_trades']} | HOLD Sh={pa['hold']['sharpe']:+.2f} ret={pa['hold']['ret']*100:+.0f}% DD={pa['hold']['maxdd']*100:.0f}%")
        print(f"      fee: " + " ".join(f"{k}={v:+.2f}" for k, v in pa["fee_sweep"].items()))
        print(f"      yr: {yr}")
    return dict(grade=grade, minFull=mf, minHold=mh, minTr=mt, maxDD=mdd, fee_ok=fee_ok_all,
                per_asset=per_asset)


def ensemble_5050_daily(members, fee: float = FEE) -> pd.Series:
    """50/50 BTC+ETH ensemble daily series (same convention as altlib baseline) for marginal."""
    sb = _ensemble_daily_asset(members, "BTC", fee)
    se = _ensemble_daily_asset(members, "ETH", fee)
    J = pd.concat({"BTC": sb, "ETH": se}, axis=1, join="inner").fillna(0.0)
    return 0.5 * J["BTC"] + 0.5 * J["ETH"]


def marginal_ensemble(members) -> dict:
    return al.marginal_vs_tp01(ensemble_5050_daily(members))


def report(tag, members, names):
    r = study_ensemble(tag, members)
    caus = {}
    for i, p in enumerate(members):
        cb = sk.causality(p, "BTC")
        ce = sk.causality(p, "ETH")
        caus[names[i]] = (cb["ok"], ce["ok"])
    caus_ok = all(b and e for b, e in caus.values())
    mg = marginal_ensemble(members)
    w25 = mg.get("blends", {}).get("w25", {})
    w50 = mg.get("blends", {}).get("w50", {})
    earns = (r["grade"] != "FAIL" and mg.get("marginal_verdict") == "ADDS"
             and mg.get("robust_oos") is True and mg.get("is_hedge") is False)
    beats = (earns and r["maxDD"] < 0.30 and (w25.get("uplift_hold") or -9) >= 0.55
             and r["minHold"] >= 0.65)
    print(f"\n----- MARGINAL [{tag}] -----")
    print(f"  members: {names}")
    print(f"  causality per member (BTC,ETH): {caus}  -> all_ok={caus_ok}")
    print(f"  corr_full={mg.get('corr_full')} corr_hold={mg.get('corr_hold')} verdict={mg.get('marginal_verdict')}")
    print(f"  has_insample_edge={mg.get('has_insample_edge')} cand_insample_sharpe={mg.get('cand_insample_sharpe')}"
          f" is_hedge={mg.get('is_hedge')} robust_oos={mg.get('robust_oos')} multicut_persistent={mg.get('multicut_persistent')}")
    print(f"  clean_year_uplift={mg.get('clean_year_uplift')} jackknife_min_uplift={mg.get('jackknife_min_uplift')}"
          f" multicut_uplift={mg.get('multicut_uplift')}")
    print(f"  w25={w25}")
    print(f"  w50={w50}")
    print(f"  => earns_slot={earns}  BEATS_WINNER={beats}  (DD={r['maxDD']*100:.0f}% minHold={r['minHold']:+.2f} w25_up_hold={w25.get('uplift_hold')})")
    return dict(study=r, marginal=mg, caus_ok=caus_ok, earns=earns, beats=beats, w25=w25, w50=w50)


if __name__ == "__main__":
    # ---- Diverse member pool (all pure SkyhookParams variants, all causal) ----
    # WINNER (ptn_n=45, sl2.5/tp7.0, exits 24/16, vola 35-95, vol_lo 0)
    P_WIN = WINNER
    # Faster pattern, tighter stop, shorter TP (different turnover/regime sensitivity)
    P_FAST = SkyhookParams(ptn_n=25, sl_atr=2.0, tp_atr=5.0, uscitalong=18, uscitashort=12,
                           vola_lo=35.0, vola_hi=95.0, vol_lo=0.0)
    # Slow pattern, wider stop, longer TP (smoother, fewer trades)
    P_SLOW = SkyhookParams(ptn_n=90, sl_atr=3.0, tp_atr=9.0, uscitalong=30, uscitashort=20,
                           vola_lo=35.0, vola_hi=95.0, vol_lo=0.0)
    # Mid pattern, percent exits (structurally different exit mode) + tighter vola band
    P_PCT = SkyhookParams(ptn_n=45, exit_mode="pct", sl_pct=0.04, tp_pct=0.10,
                          uscitalong=24, uscitashort=16, vola_lo=30.0, vola_hi=90.0, vol_lo=0.0)
    # Low-vol gate variant: add a vol floor + slightly different vola band (regime diversity)
    P_GATE = SkyhookParams(ptn_n=45, sl_atr=2.5, tp_atr=6.0, uscitalong=24, uscitashort=16,
                           vola_lo=40.0, vola_hi=95.0, vol_lo=40.0)
    # DEFENSIVE members: tight stop cuts losers fast -> shallow per-trade DD (the DD-cutters).
    P_TIGHT = SkyhookParams(ptn_n=45, sl_atr=1.5, tp_atr=4.5, uscitalong=18, uscitashort=12,
                            vola_lo=35.0, vola_hi=95.0, vol_lo=0.0)
    P_TIGHT2 = SkyhookParams(ptn_n=25, sl_atr=1.3, tp_atr=4.0, uscitalong=14, uscitashort=10,
                             vola_lo=35.0, vola_hi=95.0, vol_lo=0.0)
    # Calm-regime gate (sit out high-vola tails) + tight stop -> lowest DD contributor
    P_CALM = SkyhookParams(ptn_n=45, sl_atr=1.8, tp_atr=5.0, uscitalong=20, uscitashort=14,
                           vola_lo=20.0, vola_hi=70.0, vol_lo=0.0)
    # Calm variants: narrower / different vola windows -> diverse DD timing among defenders.
    P_CALM2 = SkyhookParams(ptn_n=90, sl_atr=1.8, tp_atr=5.5, uscitalong=24, uscitashort=16,
                            vola_lo=25.0, vola_hi=65.0, vol_lo=0.0)
    P_CALM3 = SkyhookParams(ptn_n=45, sl_atr=2.0, tp_atr=6.0, uscitalong=24, uscitashort=16,
                            vola_lo=15.0, vola_hi=60.0, vol_lo=0.0)
    # CALM4: strong hold-out defensive (wider TP like winner but calm band) — uplift booster
    P_CALM4 = SkyhookParams(ptn_n=45, sl_atr=2.2, tp_atr=7.0, uscitalong=24, uscitashort=16,
                            vola_lo=15.0, vola_hi=65.0, vol_lo=0.0)
    # CALM5: ptn_n=90 calm + wide TP (smooth, strong) for uplift+DD balance
    P_CALM5 = SkyhookParams(ptn_n=90, sl_atr=2.0, tp_atr=7.0, uscitalong=28, uscitashort=18,
                            vola_lo=15.0, vola_hi=62.0, vol_lo=0.0)

    POOL = {"WIN": P_WIN, "FAST": P_FAST, "SLOW": P_SLOW, "PCT": P_PCT, "GATE": P_GATE,
            "TIGHT": P_TIGHT, "TIGHT2": P_TIGHT2, "CALM": P_CALM,
            "CALM2": P_CALM2, "CALM3": P_CALM3, "CALM4": P_CALM4, "CALM5": P_CALM5}

    # ---- First: standalone DD of each member (diagnostic) ----
    print("\n===== STANDALONE per-member DD (max over BTC/ETH) =====")
    for k, p in POOL.items():
        dds, fhs, hhs = {}, {}, {}
        for a in CERTIFIED:
            r = sk.run_asset(a, p, FEE)
            dds[a] = r["full"]["maxdd"]; fhs[a] = r["full"]["sharpe"]; hhs[a] = r["holdout"]["sharpe"]
        print(f"  {k:7s}: maxDD={max(dds.values())*100:.0f}% (BTC {dds['BTC']*100:.0f}/ETH {dds['ETH']*100:.0f})"
              f"  minFull={min(fhs.values()):+.2f} minHold={min(hhs.values()):+.2f}")

    results = {}

    # Smallest K first: K=3, then K=4, K=5 mixes (winner always included).
    # Focus on DEFENSIVE-heavy mixes to drive standalone DD below 30%.
    mixes = {
        # maximize w25 uplift_hold (>=0.55) while keeping DD<30 -> strong-holdout calm members
        "K3_WIN_CALM3_CALM4":      ["WIN", "CALM3", "CALM4"],
        "K3_WIN_CALM4_CALM5":      ["WIN", "CALM4", "CALM5"],
        "K3_WIN_CALM3_CALM5":      ["WIN", "CALM3", "CALM5"],
        "K3_WIN_PCT_CALM3":        ["WIN", "PCT", "CALM3"],       # PCT has hold 1.0 (uplift booster) but DD 43
        "K4_WIN_CALM3_CALM4_CALM5":["WIN", "CALM3", "CALM4", "CALM5"],
        "K3_WIN_CALM3_CALM2":      ["WIN", "CALM3", "CALM2"],     # prior: DD 24, uplift 0.508
        "K3_WIN_CALM_CALM3":       ["WIN", "CALM", "CALM3"],      # prior: DD 23, uplift 0.493
        "K4_WIN_GATE_CALM3_CALM4": ["WIN", "GATE", "CALM3", "CALM4"],
        "K3_WIN_GATE_CALM3":       ["WIN", "GATE", "CALM3"],
    }

    for tag, keys in mixes.items():
        members = [POOL[k] for k in keys]
        print(f"\n########## {tag}  members={keys} ##########")
        results[tag] = report(tag, members, keys)

    # ---- pick best: prefer BEATS_WINNER, else best by (earns, low DD, hold) ----
    def score(res):
        r, w25 = res["study"], res["w25"]
        uh = (w25.get("uplift_hold") or -9)
        dd_ok = 1 if r["maxDD"] < 0.30 else 0          # goal #1 first
        hold_ok = 1 if r["minHold"] >= 0.65 else 0     # goal #2
        return (1 if res["beats"] else 0, 1 if res["earns"] else 0,
                dd_ok, hold_ok, uh, -r["maxDD"])
    best_tag = max(results, key=lambda t: score(results[t]))
    bres = results[best_tag]
    br, bw25, bw50, bmg = bres["study"], bres["w25"], bres["w50"], bres["marginal"]

    print("\n\n##################### BEST ENSEMBLE #####################")
    print(f"BEST = {best_tag}  members={mixes[best_tag]}")
    print(f"grade={br['grade']} minFull={br['minFull']:+.3f} minHold={br['minHold']:+.3f}"
          f" max_dd={br['maxDD']:.4f} n_trades_min={br['minTr']} fee_ok(@0.30%)={br['fee_ok']}")
    print(f"causality_ok={bres['caus_ok']}")
    print(f"marginal: corr_full={bmg.get('corr_full')} verdict={bmg.get('marginal_verdict')}"
          f" has_insample_edge={bmg.get('has_insample_edge')} is_hedge={bmg.get('is_hedge')}"
          f" robust_oos={bmg.get('robust_oos')} multicut_persistent={bmg.get('multicut_persistent')}"
          f" clean_year_uplift={bmg.get('clean_year_uplift')}")
    print(f"blend w25 uplift_hold={bw25.get('uplift_hold')} uplift_full={bw25.get('uplift_full')}")
    print(f"blend w50 full={bw50.get('full')} hold={bw50.get('hold')} dd={bw50.get('dd')}")
    print(f"earns_slot={bres['earns']}  BEATS_WINNER={bres['beats']}")

    # Emit a machine-readable line so the agent can lift exact numbers.
    import json
    print("\nRESULT_JSON " + json.dumps({
        "best_tag": best_tag, "members": mixes[best_tag],
        "grade": br["grade"], "minFull": br["minFull"], "minHold": br["minHold"],
        "max_dd": br["maxDD"], "n_trades_min": br["minTr"], "fee_ok": br["fee_ok"],
        "causality_ok": bres["caus_ok"],
        "corr_full": bmg.get("corr_full"), "verdict": bmg.get("marginal_verdict"),
        "has_insample_edge": bmg.get("has_insample_edge"), "is_hedge": bmg.get("is_hedge"),
        "robust_oos": bmg.get("robust_oos"), "multicut_persistent": bmg.get("multicut_persistent"),
        "clean_year_uplift": bmg.get("clean_year_uplift"),
        "w25_uplift_hold": bw25.get("uplift_hold"), "w50_full": bw50.get("full"),
        "w50_hold": bw50.get("hold"), "w50_dd": bw50.get("dd"),
        "earns_slot": bres["earns"], "beats_winner": bres["beats"],
        "cand_insample_sharpe": bmg.get("cand_insample_sharpe"),
    }, default=str))
