"""fill_haircut — quanto il fill REALE a basso capitale erode il lead PREVDAY (e TP01)?

Lo scettico d'esecuzione (diario 2026-06-21) ha segnalato che il vol-target di PREVDAY fa ~8500
ribilanciamenti/anno, di cui 97-98% < $1 di nozionale a $600: a quel capitale NON puoi piazzare
quegli ordini (min_order $5), quindi il libro MODELED (ribilanciamento continuo, frictionless) è
una finzione. Il forward-monitor traccia MODELED-$2000 vs REAL-$600 per misurare il gap nei mesi
a venire — qui lo stimiamo SUBITO su tutto lo storico, replicando la STESSA logica di paper_prevday.

Due libri, identici tranne il fill:
  MODELED : ribilancia ad ogni barra alla posizione-bersaglio (fee proporzionale su ogni |Δ|).
  REAL-$C : capitale C, salta i ribilanciamenti con nozionale |Δpos|*leg_cap < min_order ($5)
            (posizione resta "stale" -> tracking error, ma niente fee sui trade infinitesimi).

Sweep capitale {600, 2000, 20000} per mostrare a quanto l'haircut svanisce. Poi la domanda-soldi:
il blend 80%TP01+20%PREVDAY conserva l'uplift hold-out (+0.56 modellato) usando PREVDAY-REAL-$600?

    uv run python scripts/research/intraday/fill_haircut.py
"""
import sys
from pathlib import Path

import numpy as np
import pandas as pd

ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
from src.backtest.harness import load                       # noqa: E402
from src.strategies.prevday_breakout import target as pv_target  # noqa: E402
from src.portfolio.portfolio import to_daily                # noqa: E402
from src.portfolio.sleeves import _tp01_returns             # noqa: E402

HOLD = pd.Timestamp("2025-01-01", tz="UTC")
FEE_SIDE = 0.0005          # 0.05%/side = 0.10% RT
MIN_ORDER = 5.0
WEIGHT = 0.5
ASSETS = ["BTC", "ETH"]


def _sh(x):
    x = x.dropna()
    return float(x.mean() / x.std() * np.sqrt(365.25)) if len(x) > 2 and x.std() > 0 else 0.0


def _dd(x):
    eq = (1 + x.fillna(0)).cumprod()
    return float(((eq - eq.cummax()) / eq.cummax()).min())


def simulate(targets: dict, rets: dict, idx_dt, capital):
    """Bar-by-bar 50/50 book. capital=None -> MODELED (continuous). Returns (daily_ret, stats)."""
    n = len(idx_dt)
    held = {a: 0.0 for a in ASSETS}
    net = np.zeros(n)
    exec_ct = {a: 0 for a in ASSETS}
    skip_ct = {a: 0 for a in ASSETS}
    fee_tot = 0.0
    for i in range(n):
        step = 0.0
        for a in ASSETS:
            tgt = float(targets[a][i]); r = float(rets[a][i]); h = held[a]
            if capital is None:                      # MODELED: always rebalance
                new_h = tgt; traded = abs(tgt - h)
                exec_ct[a] += 1 if traded > 1e-9 else 0
            else:                                    # REAL-$C: skip sub-min_order
                leg_cap = capital * WEIGHT
                if abs(tgt - h) * leg_cap >= MIN_ORDER:
                    new_h = tgt; traded = abs(tgt - h); exec_ct[a] += 1
                else:
                    new_h = h; traded = 0.0; skip_ct[a] += 1
            fee = FEE_SIDE * traded
            fee_tot += WEIGHT * fee
            step += WEIGHT * (h * r - fee)           # earn on position HELD into bar, pay fee on rebalance
            held[a] = new_h
        net[i] = step
    s = pd.Series(net, index=idx_dt)
    daily = s.groupby(s.index.floor("1D")).sum()
    yrs = (idx_dt[-1] - idx_dt[0]).days / 365.25
    ex = sum(exec_ct.values()); sk = sum(skip_ct.values())
    stats = dict(execs_per_yr=ex / yrs, skip_frac=sk / (ex + sk) if (ex + sk) else 0.0,
                 fee_drag_per_yr=fee_tot / yrs)
    return daily, stats


def build_targets():
    targets, rets, ts_sets = {}, {}, {}
    dts = {}
    for a in ASSETS:
        df = load(a, "1h").reset_index(drop=True)
        c = df["close"].values.astype(float)
        r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
        targets[a] = pv_target(df); rets[a] = r
        ts = df["timestamp"].values.astype("int64")
        ts_sets[a] = ts
        dts[a] = pd.to_datetime(df["datetime"], utc=True).values
    common = sorted(set(ts_sets["BTC"]).intersection(ts_sets["ETH"]))
    pos = {a: {int(t): i for i, t in enumerate(ts_sets[a])} for a in ASSETS}
    T, R = {a: [] for a in ASSETS}, {a: [] for a in ASSETS}
    dt_out = []
    for t in common:
        i_btc = pos["BTC"][int(t)]
        dt_out.append(dts["BTC"][i_btc])
        for a in ASSETS:
            i = pos[a][int(t)]
            T[a].append(targets[a][i]); R[a].append(rets[a][i])
    idx = pd.to_datetime(dt_out, utc=True)
    return {a: np.array(T[a]) for a in ASSETS}, {a: np.array(R[a]) for a in ASSETS}, idx


def row(label, daily):
    J = daily.dropna(); JH = J[J.index >= HOLD]
    yrs = (J.index[-1] - J.index[0]).days / 365.25
    cagr = (1 + J).prod() ** (1 / yrs) - 1
    return (f"  {label:<18s} FULL Sh {_sh(J):+5.2f}  HOLD Sh {_sh(JH):+5.2f}  "
            f"CAGR {cagr*100:+5.1f}%  DD {_dd(J)*100:4.0f}%")


def main():
    print("=" * 92)
    print("  FILL-HAIRCUT — PREVDAY: libro MODELED (continuo) vs REAL-$C (skip < $5 min-order)")
    print("=" * 92)
    T, R, idx = build_targets()
    print(f"  path 1h: {len(idx)} barre  {idx[0].date()} -> {idx[-1].date()}\n")

    books = {}
    for cap, lab in [(None, "MODELED ($∞)"), (20000, "REAL-$20k"), (2000, "REAL-$2000"), (600, "REAL-$600")]:
        daily, st = simulate(T, R, idx, cap)
        books[lab] = daily
        print(row(lab, daily) +
              f"   | rebal/yr {st['execs_per_yr']:6.0f}  skip {st['skip_frac']*100:4.1f}%  "
              f"fee-drag/yr {st['fee_drag_per_yr']*100:4.2f}%")

    mod = books["MODELED ($∞)"]; real = books["REAL-$600"]
    hc_full = _sh(mod.dropna()) - _sh(real.dropna())
    JHm = mod[mod.index >= HOLD]; JHr = real[real.index >= HOLD]
    hc_hold = _sh(JHm) - _sh(JHr)
    print(f"\n  >> HAIRCUT $600 (MODELED - REAL): FULL Sharpe {hc_full:+.2f}  |  HOLD-OUT Sharpe {hc_hold:+.2f}")

    # money question: does the blend uplift survive at REAL-$600?
    print("\n" + "-" * 92)
    print("  BLEND 80%TP01 + 20%PREVDAY — sopravvive l'uplift hold-out col fill reale?")
    tp = to_daily(_tp01_returns())
    for lab, pv in [("MODELED", mod), ("REAL-$600", real)]:
        J = pd.concat({"TP": tp, "PV": pv}, axis=1).dropna(); JH = J[J.index >= HOLD]
        for w in (0.20, 0.30):
            b = (1 - w) * J["TP"] + w * J["PV"]; bh = (1 - w) * JH["TP"] + w * JH["PV"]
            print(f"  PV={lab:<9s} w={w:.0%}  FULL {_sh(b):+.2f} (upl {_sh(b)-_sh(J['TP']):+.2f})  "
                  f"HOLD {_sh(bh):+.2f} (upl {_sh(bh)-_sh(JH['TP']):+.2f})")
    print(f"  [TP01 solo: FULL {_sh(tp.dropna()):+.2f}  HOLD {_sh(tp[tp.index>=HOLD]):+.2f}]")
    print("=" * 92)


if __name__ == "__main__":
    main()
