"""VOL01 — DVOL z-score risk on/off.

IDEA: Use Deribit DVOL (implied vol index) as a regime filter.
  - When DVOL z-score (expanding window, causal) < threshold => "calm" => go LONG vol-targeted
  - When DVOL z-score >= threshold => "high vol / fear" => flat
History starts 2021-03 (DVOL only available from then).

Strategy type: CONTINUOUS position (weights), long-flat, vol-targeted at 20%.

Grid: test two z-score thresholds (0 and 0.5) x two DVOL smoothing windows (30d, 60d).
Total cells: 4 param sets x 2 TFs (1d, 12h) x 2 assets = 16 backtests — within budget.
Pick best config by min-asset hold-out Sharpe.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np

# ------------------------------------------------------------------
# DVOL z-score signal builder
# ------------------------------------------------------------------
def make_vol01(zscore_thresh: float, dvol_smooth_days: int):
    """
    Returns a target_fn(df) for VOL01.

    Signal logic:
      1. Get DVOL for the asset (causal, aligned to bar timestamps).
      2. Smooth DVOL with an EMA of dvol_smooth_days bars.
      3. Compute an EXPANDING z-score of the smoothed DVOL.
         Expanding (not rolling) = fully causal, uses all history up to i.
      4. Direction = +1 if z-score < zscore_thresh, else 0 (flat).
      5. Apply vol_target scaling to direction.

    The expanding z-score naturally adapts to regime: low DVOL vs the full
    history = calm = invest; high DVOL vs history = fear = sideline.
    """
    def target_fn(df):
        # Step 1: get raw DVOL (causal forward-fill from daily Deribit data)
        # Detect which asset this df belongs to by checking close price range
        # We need to pass asset name — infer from close magnitude
        # BTC >> 1000, ETH >> 100 but < BTC. Use DVOL from both and pick best match.
        # Actually al.dvol needs the asset name. We'll pass it via closure.
        raise NotImplementedError("Asset name needed — use make_vol01_asset instead")
    return target_fn


def make_vol01_asset(asset: str, zscore_thresh: float, dvol_smooth_days: int):
    """VOL01 target function for a specific asset."""
    def target_fn(df):
        bpd = al.bars_per_day(df)

        # Step 1: get DVOL causally aligned to df bars
        dv = al.dvol(df, asset)  # float array, NaN before 2021-03

        # Step 2: smooth DVOL with EMA to reduce noise
        smooth_bars = dvol_smooth_days * bpd
        dv_smooth = al.ema(np.where(np.isfinite(dv), dv, np.nan), max(2, smooth_bars))

        # Step 3: expanding z-score (causal — uses all history up to i)
        s = pd.Series(dv_smooth)
        exp_mean = s.expanding(min_periods=30).mean()
        exp_std = s.expanding(min_periods=30).std()
        z = ((s - exp_mean) / exp_std.replace(0, np.nan)).values

        # Step 4: direction — long when z < threshold, flat otherwise
        direction = np.where(
            np.isfinite(z) & (z < zscore_thresh),
            1.0,
            0.0
        )

        # Step 5: vol-target scaling
        pos = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
        return pos

    return target_fn


# Need pandas for expanding z-score in the closure
import pandas as pd


# ------------------------------------------------------------------
# Small grid: 2 thresholds x 2 smoothing windows
# ------------------------------------------------------------------
param_grid = [
    (0.0,  30),   # strict: only enter below median DVOL, 30d smooth
    (0.5,  30),   # relaxed: enter below +0.5 sigma, 30d smooth
    (0.0,  60),   # strict: 60d smooth
    (0.5,  60),   # relaxed: 60d smooth
]

TFS = ("1d", "12h")

print("=== VOL01: DVOL z-score risk on/off ===")
print(f"Grid: {len(param_grid)} param sets x {len(TFS)} TFs x 2 assets")
print()

all_reps = []
for (zt, sd) in param_grid:
    name = f"VOL01_z{zt:.1f}_s{sd}d"

    # We need per-asset target functions since al.study_weights calls target_fn(df)
    # but doesn't pass asset name. Solution: run BTC and ETH separately using a
    # custom wrapper that uses asset-specific target functions.

    # Custom study that handles per-asset target functions:
    def run_study(name, zt=zt, sd=sd):
        cells = []
        for tf in TFS:
            per_asset = {}
            fee_ok_all = True
            for a in al.CERTIFIED:
                df = al.get(a, tf)
                tgt_fn = make_vol01_asset(a, zt, sd)
                tgt = tgt_fn(df)
                base = al.eval_weights(df, tgt, fee_side=al.FEE_SIDE)
                sweep = {f"{2*f*100:.2f}%RT": al.eval_weights(df, tgt, fee_side=f)["full"]["sharpe"]
                         for f in al.FEE_SWEEP}
                fee_ok = sweep.get("0.20%RT", -9) > 0
                fee_ok_all = fee_ok_all and fee_ok
                per_asset[a] = dict(
                    full=base["full"], holdout=base["holdout"],
                    tim=base["time_in_market"], turnover=base["turnover_per_year"],
                    fee_sweep=sweep, yearly=base["yearly"]
                )
            min_full = min(per_asset[a]["full"]["sharpe"] for a in al.CERTIFIED)
            min_hold = min(per_asset[a]["holdout"].get("sharpe", 0.0) for a in al.CERTIFIED)
            avg_full = np.mean([per_asset[a]["full"]["sharpe"] for a in al.CERTIFIED])
            cells.append(dict(
                tf=tf, per_asset=per_asset,
                min_asset_full_sharpe=round(min_full, 3),
                min_asset_holdout_sharpe=round(min_hold, 3),
                full_sharpe=round(float(avg_full), 3),
                fee_survives=fee_ok_all
            ))
        # compute verdict
        verdict = al._verdict(cells)
        return dict(name=name, kind="weights", cells=cells, verdict=verdict)

    rep = run_study(name)
    all_reps.append((zt, sd, rep))
    print(al.fmt(rep))
    print()

# ------------------------------------------------------------------
# Pick best config by min-asset hold-out Sharpe across best TF
# ------------------------------------------------------------------
best_entry = max(all_reps, key=lambda x: x[2]["verdict"].get("best_holdout_sharpe", -99))
best_zt, best_sd, best_rep = best_entry

print("=" * 60)
print(f"BEST CONFIG: z_thresh={best_zt}, smooth={best_sd}d")
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))
