"""prevday_turnover — la fee di PREVDAY viene dai FLIP, non dai micro-ribilanciamenti. Si può tagliare?

Da fill_haircut.py: il libro REAL-$600 salta il 98.4% dei ribilanciamenti del vol-target e la
fee-drag scende solo 2.49% -> 2.39%/anno. Quindi la fee (~2.4%/anno) e' dominata dai ~50 FLIP di
direzione/anno, non dal churn sub-dollaro. Un deadband d'esecuzione e' inutile; la leva e' ridurre i
flip a LIVELLO DI SEGNALE. Qui sweepiamo le leve che riducono i flip e misuriamo il trade-off
turnover <-> edge:
  * BUFFER_K (break piu' deciso = meno flip)              {0.30 base, 0.50, 0.75, 1.00}
  * ANCHOR_DAYS (range multi-giorno = livelli piu' larghi){1 base, 2, 3, 5}
  * MIN_HOLD (non flippare entro N ore dall'ultimo flip)  {0 base, 24, 72}
e in piu' LONG-ONLY vs LONG-SHORT (isola la gamba short = l'hedge del blocker #1).

Per ogni config: flip/anno, fee-drag/anno, FULL/HOLD Sharpe, corr a TP01, e l'uplift hold-out del
blend 80%TP01+20%PV (la metrica che conta). Libro MODELED (l'haircut di fill e' +0.01, irrilevante).

    uv run python scripts/research/intraday/prevday_turnover.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 import prevday_breakout as pb           # 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
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 _min_hold(direction, min_hold_bars):
    """Sopprime i flip entro min_hold_bars dall'ultimo cambio di segno (riduce il churn di segnale)."""
    if min_hold_bars <= 0:
        return direction
    out = direction.copy()
    last_flip = -10**9
    cur = out[0]
    for i in range(len(out)):
        if np.sign(out[i]) != np.sign(cur):
            if i - last_flip >= min_hold_bars:
                cur = out[i]; last_flip = i
            else:
                out[i] = cur               # mantieni la posizione (flip soppresso)
        else:
            cur = out[i]
    return out


def build(dfs, anchor, k, min_hold_bars, allow_short):
    """Ritorni daily 50/50 + flip/anno + fee-drag/anno per una config di segnale (libro modeled)."""
    legs, idx_ref = [], None
    flips = 0; fee_tot = 0.0; yrs = None
    daily_sum = None
    for a in ASSETS:
        df = dfs[a]
        c = df["close"].values.astype(float)
        r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0
        direction = pb._breakout_direction(df, anchor, k, allow_short)
        direction = _min_hold(direction, min_hold_bars)
        tgt = pb._vol_target(direction, df, pb.TARGET_VOL, pb.VOL_WIN_DAYS, pb.LEV_CAP)
        tgt = np.nan_to_num(tgt, nan=0.0)
        held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
        turn = np.abs(np.diff(tgt, prepend=tgt[0]))
        net = held * r - FEE_SIDE * turn
        dt = pd.to_datetime(df["datetime"], utc=True)
        s = pd.Series(net, index=dt)
        d = s.groupby(s.index.floor("1D")).sum()
        daily_sum = d if daily_sum is None else daily_sum.add(d, fill_value=0)
        flips += int((np.sign(direction[1:]) != np.sign(direction[:-1])).sum())
        fee_tot += float((FEE_SIDE * turn).sum())
        yrs = (dt.iloc[-1] - dt.iloc[0]).days / 365.25
    daily = WEIGHT * daily_sum
    return daily, flips / 2 / yrs, WEIGHT * fee_tot / yrs    # flips mediati sulle 2 gambe


def main():
    print("=" * 104)
    print("  PREVDAY turnover-reduction — la fee viene dai FLIP. Si taglia senza perdere l'edge?")
    print("=" * 104)
    dfs = {a: load(a, "1h").reset_index(drop=True) for a in ASSETS}
    tp = to_daily(_tp01_returns())

    def line(label, daily, flips, fee):
        J = pd.concat({"TP": tp, "PV": daily}, axis=1, sort=True).dropna(); JH = J[J.index >= HOLD]
        b = 0.8 * J["TP"] + 0.2 * J["PV"]; bh = 0.8 * JH["TP"] + 0.2 * JH["PV"]
        upl_h = _sh(bh) - _sh(JH["TP"])
        print(f"  {label:<26s} flip/yr {flips:5.0f}  fee {fee*100:4.2f}%  "
              f"FULL {_sh(J['PV']):+5.2f}  HOLD {_sh(JH['PV']):+5.2f}  DD {_dd(J['PV'])*100:4.0f}%  "
              f"corrTP {J['PV'].corr(J['TP']):+.2f}  blendHOLDupl {upl_h:+.2f}")

    print(f"  [TP01 solo: FULL {_sh(tp.dropna()):+.2f}  HOLD {_sh(tp[tp.index>=HOLD]):+.2f}]\n")

    print("  -- BASE (congelato: anchor=1, k=0.30, no min-hold, long-short) --")
    d, f, fee = build(dfs, 1, 0.30, 0, True); line("BASE", d, f, fee)

    print("\n  -- BUFFER_K piu' ampio (break piu' deciso) --")
    for k in (0.50, 0.75, 1.00):
        d, f, fee = build(dfs, 1, k, 0, True); line(f"k={k:.2f}", d, f, fee)

    print("\n  -- ANCHOR_DAYS multi-giorno (range piu' largo) --")
    for an in (2, 3, 5):
        d, f, fee = build(dfs, an, 0.30, 0, True); line(f"anchor={an}", d, f, fee)

    print("\n  -- MIN_HOLD (no flip entro N ore) --")
    for mh in (24, 72):
        d, f, fee = build(dfs, 1, 0.30, mh, True); line(f"min_hold={mh}h", d, f, fee)

    print("\n  -- combo low-turnover (k=0.75 + anchor=2 + min_hold=24h) --")
    d, f, fee = build(dfs, 2, 0.75, 24, True); line("combo-LT", d, f, fee)

    print("\n  -- LONG-ONLY vs LONG-SHORT (isola la gamba short = hedge del blocker #1) --")
    d, f, fee = build(dfs, 1, 0.30, 0, False); line("long-only (no short)", d, f, fee)
    d, f, fee = build(dfs, 1, 0.30, 0, True);  line("long-short (BASE)", d, f, fee)
    print("=" * 104)


if __name__ == "__main__":
    main()
