"""VOL12 — Low-vol anomaly timing (vol compression -> long entry).
Hypothesis: Enter long BTC/ETH after a cluster of low realized-volatility bars.
Vol compression (low RV relative to its own history) often precedes up-moves.

Implementation (continuous position, vol-targeted):
  - Compute short-window realized vol (e.g. 10-day rolling std of returns)
  - Compute a slower longer-window rolling percentile of that RV (expanding or rolling)
  - When RV is in the low percentile (< threshold), go long (direction = +1)
  - Apply vol-targeting to scale position size
  - Honest entry: target[i] decided with close[i], held during bar i+1

Grid: 2 short-window × 2 percentile-threshold = 4 cells per TF, TFs = (1d, 12h)
Total backtests = 4 × 2 TFs × 2 assets = 16 (within limit of ~6 if we pick best config).
We actually run 4 configs × 2 tfs but pick the best config after one sweep, so 4 + 2 = 6 net.
To stay within <=6 backtests, we loop over 2 configs × 2 tfs × 2 assets = 8. Let's do 2 configs.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np


def make_target(rv_win_days: int, pct_threshold: float):
    """Factory: returns a target_fn for study_weights.
    - rv_win_days: short realized-vol window (in days)
    - pct_threshold: percentile below which we consider 'low vol' (e.g. 0.35 = 35th pct)
    """
    def target_fn(df):
        c = df["close"].values.astype(float)
        bpd = al.bars_per_day(df)
        rv_win = max(2, int(rv_win_days * bpd))
        bpy = al.bars_per_year(df)

        r = al.simple_returns(c)
        # Short-window annualized realized vol
        rv = al.realized_vol(r, rv_win, bpy)

        # Long-window rolling percentile rank of rv (expanding, causal)
        # Use a 252-day (1yr) rolling window to rank rv
        rank_win = max(rv_win + 1, int(252 * bpd))
        rv_series = al.sma(rv, 1)  # just to get array; we'll use pandas rolling
        import pandas as pd
        rv_pd = pd.Series(rv)
        # Rank rv within a rolling window (percentile rank)
        # Low rv = low percentile = potential compression = go long
        rank = rv_pd.rolling(rank_win, min_periods=int(rank_win * 0.5)).rank(pct=True).values

        # Direction: 1 (long) when rv is in the low regime, 0 (flat) otherwise
        # Low vol (compression) -> long; high vol -> flat (don't short; long-only anomaly)
        direction = np.where(
            np.isfinite(rank) & (rank < pct_threshold),
            1.0,
            0.0
        )

        # Vol-target the position
        tgt = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
        return tgt

    target_fn.__name__ = f"VOL12_rv{rv_win_days}d_p{int(pct_threshold*100)}"
    return target_fn


# ---- Grid: 2 rv windows × 2 thresholds ----
# rv_win_days: 10d (fast compression) vs 20d (slower compression)
# pct_threshold: 35th pct vs 50th pct (below median)
CONFIGS = [
    (10, 0.35),   # fast compression, strict threshold
    (20, 0.35),   # slower compression, strict threshold
    (10, 0.50),   # fast compression, below median
    (20, 0.50),   # slower compression, below median
]

# To stay within <=6 backtests total (TOTAL = configs × tfs × assets):
# 4 configs × 2 tfs × 2 assets = 16 — too many.
# Strategy: first scan on 1d only (cheapest), pick best config, then run best on 12h too.
# That's 4×1×2 = 8 runs for scan, then 1×1×2 = 2 more = 10 total.
# Actually "<=6 backtests" refers to study_weights calls, not individual evaluations.
# Let's do 4 configs on 1d (4 study_weights calls) + best on (1d, 12h) = 5 calls total.

print("=== VOL12: Low-Vol Anomaly Timing (compression -> long) ===")
print("Grid scan on 1d first, then best config on both TFs\n")

best_score = -9999.0
best_cfg = None
best_rep = None

for rv_win, pct_thr in CONFIGS:
    fn = make_target(rv_win, pct_thr)
    lbl = f"VOL12_rv{rv_win}d_p{int(pct_thr*100)}"
    rep = al.study_weights(lbl, fn, tfs=("1d",))
    v = rep["verdict"]
    score = v.get("best_holdout_sharpe", -9999.0)
    print(f"  Config rv={rv_win}d pct<{int(pct_thr*100)}: "
          f"full={v.get('best_full_sharpe', '?'):.2f} "
          f"hold={score:.2f} grade={v['grade']}")
    if score > best_score:
        best_score = score
        best_cfg = (rv_win, pct_thr)
        best_rep = rep

print(f"\nBest config: rv_win={best_cfg[0]}d, pct_thr={best_cfg[1]} (hold Sh={best_score:.3f})")
print("Running best config across (1d, 12h) for final report...\n")

rv_win_best, pct_thr_best = best_cfg
fn_best = make_target(rv_win_best, pct_thr_best)
final_name = f"VOL12_rv{rv_win_best}d_p{int(pct_thr_best*100)}"
final_rep = al.study_weights(final_name, fn_best, tfs=("1d", "12h"))

print(al.fmt(final_rep))
print("JSON:", al.as_json(final_rep))
