"""Adversarial parameter-perturbation harness for the 3 blind survivors.
Re-implements each signal parameterized; perturbs each key param +/-25% (and larger
jumps), re-evaluates OOS (test slice, A & B) and train. Reports min/median/max OOS
Sharpe across the grid and the train->test Sharpe decay. Also a fee bump to 0.20% RT.
"""
import sys
import numpy as np
import pandas as pd

sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/blind")
import blindlib as bl

FEE_BASE = 0.0005   # 0.10% RT
FEE_BUMP = 0.001    # 0.20% RT


def _masks(series):
    df = bl.load(series, "full")
    cut = bl.split_cut(series)
    test = np.zeros(len(df), bool); test[cut:] = True
    train = np.zeros(len(df), bool); train[:cut] = True
    return df, train, test


# ---------------- agent_04 MACD ----------------
def macd_signal(df, FAST=26, SLOW=52, SIGNAL=9, SLOPE_W=0.20, SHORT_W=0.5,
                TARGET_VOL=0.20, VOL_WIN=30, LEV_CAP=1.0):
    c = df["close"].values.astype(float)
    macd = bl.ema(c, FAST) - bl.ema(c, SLOW)
    signal_line = bl.ema(macd, SIGNAL)
    hist = macd - signal_line
    base = np.where(np.sign(hist) == np.sign(macd), np.sign(macd), 0.0)
    slope = np.sign(np.diff(hist, prepend=hist[0]))
    raw = (1.0 - SLOPE_W) * base + SLOPE_W * slope
    raw = np.clip(raw, -1.0, 1.0)
    raw = np.where(raw < 0, raw * SHORT_W, raw)
    raw = np.nan_to_num(raw, nan=0.0)
    pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
                        leverage_cap=LEV_CAP)
    return np.clip(pos, -1.0, 1.0)


# ---------------- agent_06 accel ----------------
def _lagged_diff(x, lag):
    out = np.zeros(len(x))
    if lag < len(x):
        out[lag:] = x[lag:] - x[:-lag]
    return out


def accel_signal(df, FAST=28, LAG=30, Z_WIN=200, KV=1.5, KA=1.5, W_VEL=0.4,
                 W_ACC=0.6, SHORT_W=0.0, TARGET_VOL=0.27, VOL_WIN=25, LEV_CAP=1.5):
    c = df["close"].values.astype(float)
    lr = np.zeros(len(c)); lr[1:] = np.log(c[1:] / c[:-1])
    vel = bl.ema(lr, FAST)
    acc = _lagged_diff(vel, LAG)
    zv = np.nan_to_num(bl.zscore(vel, Z_WIN), nan=0.0)
    za = np.nan_to_num(bl.zscore(acc, Z_WIN), nan=0.0)
    raw = W_VEL * np.tanh(KV * zv) + W_ACC * np.tanh(KA * za)
    raw = np.clip(raw, -1.0, 1.0)
    raw = np.where(raw >= 0.0, raw, raw * SHORT_W)
    pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
                        leverage_cap=LEV_CAP)
    return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)


# ---------------- agent_23 vol_of_vol ----------------
def _expanding_pctl_rank(x, min_hist):
    n = len(x); rank = np.full(n, np.nan); seen = []
    for i in range(n):
        v = x[i]
        if np.isfinite(v):
            seen.append(v)
            if len(seen) >= min_hist:
                rank[i] = float(np.mean(np.asarray(seen) <= v))
    return rank


def _tsmom_sign(c, h):
    out = np.zeros(len(c))
    if h < len(c):
        out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
    return out


def _vol_of_vol(rv, win):
    rv_s = pd.Series(rv)
    logrv = np.log(rv_s.where(rv_s > 0))
    dlog = logrv.diff()
    return dlog.rolling(win, min_periods=max(5, win // 2)).std().values


def vov_signal(df, RV_WIN=30, VOV_WIN=40, PCTL=0.80, HORIZONS=(25, 60, 120),
               TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5, MIN_HIST=60):
    c = df["close"].values.astype(float)
    bpy = bl.bars_per_day(df) * 365.25
    rv = bl.realized_vol(bl.simple_returns(c), RV_WIN, bpy)
    vov = _vol_of_vol(rv, VOV_WIN)
    rank = _expanding_pctl_rank(vov, MIN_HIST)
    stable = np.isfinite(rank) & (rank <= PCTL)
    sig = np.zeros(len(c))
    for h in HORIZONS:
        sig += _tsmom_sign(c, h)
    sig /= len(HORIZONS)
    raw = np.where(stable, sig, 0.0)
    pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
                        leverage_cap=LEV_CAP)
    return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)


def score(sig_fn, kwargs, fee=FEE_BASE):
    """Return dict of train & test sharpe/pnl, averaged over A&B (min/mean)."""
    out = {}
    for s in ("A", "B"):
        df, train, test = _masks(s)
        tgt = sig_fn(df, **kwargs)
        rtr = bl.eval_target(df, tgt, fee_side=fee, metric_mask=train)
        rte = bl.eval_target(df, tgt, fee_side=fee, metric_mask=test)
        out[s] = dict(tr_sh=rtr["sharpe"], tr_pnl=rtr["pnl"],
                      te_sh=rte["sharpe"], te_pnl=rte["pnl"], te_dd=rte["maxdd"])
    # combined: min across A,B (the agents tuned on sharpe_min)
    te_sh_min = min(out["A"]["te_sh"], out["B"]["te_sh"])
    tr_sh_min = min(out["A"]["tr_sh"], out["B"]["tr_sh"])
    te_sh_mean = 0.5 * (out["A"]["te_sh"] + out["B"]["te_sh"])
    te_pnl_mean = 0.5 * (out["A"]["te_pnl"] + out["B"]["te_pnl"])
    return dict(out=out, te_sh_min=te_sh_min, tr_sh_min=tr_sh_min,
                te_sh_mean=te_sh_mean, te_pnl_mean=te_pnl_mean)


def perturb_grid(sig_fn, base, grid):
    """grid: {param: [values]}. Sweep one param at a time around base."""
    base_sc = score(sig_fn, base)
    rows = []
    for p, vals in grid.items():
        for v in vals:
            kw = dict(base); kw[p] = v
            sc = score(sig_fn, kw)
            rows.append(dict(param=p, val=v, te_sh_min=sc["te_sh_min"],
                             te_sh_mean=round(sc["te_sh_mean"], 3),
                             te_pnl_mean=round(sc["te_pnl_mean"], 3),
                             tr_sh_min=sc["tr_sh_min"]))
    return base_sc, rows


if __name__ == "__main__":
    import json
    pd.set_option("display.width", 160)
    pd.set_option("display.max_rows", 300)

    print("="*70)
    print("AGENT 04 — MACD")
    print("="*70)
    base04 = dict(FAST=26, SLOW=52, SIGNAL=9, SLOPE_W=0.20, SHORT_W=0.5,
                  TARGET_VOL=0.20, VOL_WIN=30, LEV_CAP=1.0)
    b, rows = perturb_grid(macd_signal, base04, dict(
        FAST=[20, 22, 26, 30, 32, 39],          # +/-25% + bigger
        SLOW=[39, 45, 52, 60, 65, 78],
        SIGNAL=[5, 7, 9, 11, 13, 18],
        SLOPE_W=[0.10, 0.15, 0.20, 0.25, 0.30, 0.40],
        SHORT_W=[0.0, 0.25, 0.375, 0.5, 0.625, 0.75, 1.0],
        VOL_WIN=[15, 22, 30, 38, 45, 60],
        TARGET_VOL=[0.15, 0.20, 0.25, 0.30],
    ))
    print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
    print("  per-series base:", b["out"])
    print(pd.DataFrame(rows).to_string(index=False))

    print("\n" + "="*70)
    print("AGENT 06 — ACCEL")
    print("="*70)
    base06 = dict(FAST=28, LAG=30, Z_WIN=200, KV=1.5, KA=1.5, W_VEL=0.4,
                  W_ACC=0.6, SHORT_W=0.0, TARGET_VOL=0.27, VOL_WIN=25, LEV_CAP=1.5)
    b, rows = perturb_grid(accel_signal, base06, dict(
        FAST=[21, 24, 28, 32, 35, 42],
        LAG=[20, 26, 30, 36, 40, 50],
        Z_WIN=[140, 160, 200, 240, 260, 320],
        KV=[1.0, 1.2, 1.5, 1.8, 2.0, 3.0],
        KA=[1.0, 1.2, 1.5, 1.8, 2.0, 3.0],
        W_ACC=[0.3, 0.45, 0.6, 0.75, 0.9, 1.0],
        TARGET_VOL=[0.18, 0.22, 0.27, 0.32],
        VOL_WIN=[18, 22, 25, 30, 35],
    ))
    print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
    print("  per-series base:", b["out"])
    print(pd.DataFrame(rows).to_string(index=False))

    print("\n" + "="*70)
    print("AGENT 23 — VOL_OF_VOL")
    print("="*70)
    base23 = dict(RV_WIN=30, VOV_WIN=40, PCTL=0.80, HORIZONS=(25, 60, 120),
                  TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5, MIN_HIST=60)
    b, rows = perturb_grid(vov_signal, base23, dict(
        RV_WIN=[22, 26, 30, 34, 38, 45],
        VOV_WIN=[30, 35, 40, 45, 50, 60],
        PCTL=[0.60, 0.70, 0.76, 0.80, 0.84, 0.90, 1.00],
        TARGET_VOL=[0.18, 0.22, 0.26, 0.30],
        VOL_WIN=[34, 40, 45, 55, 60],
        MIN_HIST=[40, 60, 90],
    ))
    print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
    print("  per-series base:", b["out"])
    # horizons sweep separately (tuple param)
    hz_rows = []
    for hz in [(20,50,100),(25,60,120),(30,70,140),(20,40,80),(40,90,180),(15,30,60)]:
        kw = dict(base23); kw["HORIZONS"] = hz
        sc = score(vov_signal, kw)
        hz_rows.append(dict(param="HORIZONS", val=str(hz), te_sh_min=sc["te_sh_min"],
                            te_sh_mean=round(sc["te_sh_mean"],3),
                            te_pnl_mean=round(sc["te_pnl_mean"],3), tr_sh_min=sc["tr_sh_min"]))
    print(pd.DataFrame(rows + hz_rows).to_string(index=False))

    # ---- FEE BUMP to 0.20% RT, base params ----
    print("\n" + "="*70)
    print("FEE BUMP 0.10% -> 0.20% RT (base params)")
    print("="*70)
    for name, fn, base in [("MACD", macd_signal, base04),
                           ("ACCEL", accel_signal, base06),
                           ("VOV", vov_signal, base23)]:
        lo = score(fn, base, fee=FEE_BASE)
        hi = score(fn, base, fee=FEE_BUMP)
        print(f"{name:6s} te_sh_min {lo['te_sh_min']:+.3f} -> {hi['te_sh_min']:+.3f} | "
              f"te_sh_mean {lo['te_sh_mean']:+.3f} -> {hi['te_sh_mean']:+.3f} | "
              f"te_pnl_mean {lo['te_pnl_mean']:+.3f} -> {hi['te_pnl_mean']:+.3f}")
        print(f"       per-series @0.20%: A te_sh {score(fn,base,fee=FEE_BUMP)['out']['A']['te_sh']} "
              f"B te_sh {score(fn,base,fee=FEE_BUMP)['out']['B']['te_sh']}")
