#!/usr/bin/env python
"""
NexQuant 20-Hypothesis Systematic Test Suite

Tests all 20 improvement hypotheses against the real OOS walk-forward backtest.
Each approach is independently evaluated and ranked by OOS Sharpe.
"""

from __future__ import annotations

import json, sys, time
from datetime import datetime
from pathlib import Path
from typing import Optional

import numpy as np
import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk

DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
FACTORS_DIR = Path("results/factors")
TXN_COST_BPS = 2.14
FORWARD_BARS = 96


def load_all():
    close = pd.read_hdf(DATA_PATH, key="data")["$close"]
    if isinstance(close.index, pd.MultiIndex):
        close = close.droplevel(-1)
    close = close.sort_index().dropna()
    # Downsample to 5-min for speed
    close = close.resample("5min").last().dropna()

    factors_meta = []
    for f in sorted(FACTORS_DIR.glob("*.json")):
        try:
            d = json.loads(f.read_text())
        except Exception:
            continue
        if d.get("status") != "success" or d.get("ic") is None:
            continue
        name = d.get("factor_name", f.stem)
        safe = name.replace("/", "_")[:150]
        pf = FACTORS_DIR / "values" / f"{safe}.parquet"
        if pf.exists():
            factors_meta.append({"name": name, "ic": d["ic"]})

    factors_meta.sort(key=lambda x: abs(x["ic"]), reverse=True)
    top = factors_meta[:15]

    factor_data = {}
    for f in top:
        safe = f["name"].replace("/", "_")[:150]
        pf = FACTORS_DIR / "values" / f"{safe}.parquet"
        series = pd.read_parquet(pf).iloc[:, 0]
        if isinstance(series.index, pd.MultiIndex):
            series = series.droplevel(-1)
        # Resample to 5-min
        series = series.resample("5min").last()
        factor_data[f["name"]] = series

    df = pd.DataFrame(factor_data)
    common = close.index.intersection(df.dropna(how="all").index)
    return close.loc[common], df.loc[common].ffill(), {f["name"]: f["ic"] for f in top}


def backtest(signal, close, label="") -> dict:
    if signal is None or len(signal) < 100:
        return {"wf_sharpe": -999, "oos_sharpe": -999, "oos_monthly": 0, "oos_dd": 0, "trades": 0}
    common = close.index.intersection(signal.dropna().index)
    r = backtest_signal_risk(close.loc[common], signal.reindex(common).fillna(0),
                             txn_cost_bps=TXN_COST_BPS, wf_rolling=False)
    oos = r.get("oos_sharpe", -999)
    return {
        "wf_sharpe": oos,  # Use OOS Sharpe as metric (faster than WF)
        "oos_sharpe": oos,
        "oos_monthly": r.get("oos_monthly_return_pct", 0) or 0,
        "oos_dd": r.get("oos_max_drawdown", 0) or 0,
        "trades": r.get("oos_n_trades", 0),
        "is_sharpe": r.get("is_sharpe", -999),
    }


def composite_zscore(factors_df, ics):
    c = pd.Series(0.0, index=factors_df.index)
    total = sum(abs(v) for v in ics.values())
    if total == 0:
        return c
    for col in factors_df.columns:
        ic = ics.get(col, 0)
        if abs(ic) < 0.001:
            continue
        z = (factors_df[col] - factors_df[col].rolling(20).mean()) / (factors_df[col].rolling(20).std() + 1e-8)
        c += (ic / total) * z
    return c


print(f"\n{'='*70}")
print("  NexQuant 20-Hypothesis Test Suite")
print(f"{'='*70}")
t0_total = time.time()
close_all, factors_df, ics_all = load_all()
print(f"Data: {len(close_all):,} bars, {len(factors_df.columns)} factors\n")

results = []


# === H1: Trade-Frequency-First ===
print("H1: Trade-Frequency-First — optimize threshold for >500 trades/year...")
best, best_s = None, -999
for entry in [0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 0.7, 1.0]:
    c = composite_zscore(factors_df, ics_all)
    sig = pd.Series(0, index=c.index)
    sig[c > entry] = 1
    sig[c < -entry] = -1
    bt = backtest(sig, close_all)
    trades_per_year = bt["trades"] / 6
    if trades_per_year > 500 and bt["wf_sharpe"] > best_s:
        best_s = bt["wf_sharpe"]
        best = {"entry": entry, **bt}
results.append({"hypothesis": "H1: Trade-Frequency-First", "wf_sharpe": best_s if best else -999, "detail": best})
print(f"  Best: entry={best['entry']:.2f} WF={best_s:.3f} Trades/yr={best['trades']/6:.0f}" if best else "  No result")


# === H2: Continuous Position (tanh) ===
print("H2: Continuous Position — tanh(zscore) instead of 1/0/-1...")
c = composite_zscore(factors_df, ics_all)
sig = np.tanh(c)
sig = sig.clip(-1, 1)
bt = backtest(sig, close_all)
results.append({"hypothesis": "H2: Continuous tanh Position", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f} OOS_S={bt['oos_sharpe']:.3f}")


# === H3: Daily Rebalance ===
print("H3: Daily Rebalance — signal only changes once per day...")
c = composite_zscore(factors_df, ics_all)
daily = c.resample("1D").first()
daily_sig = pd.Series(0, index=daily.index)
daily_sig[daily > 0.3] = 1
daily_sig[daily < -0.3] = -1
sig = daily_sig.reindex(c.index, method="ffill")
bt = backtest(sig, close_all)
results.append({"hypothesis": "H3: Daily-Only Rebalance", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f} Trades={bt['trades']}")


# === H4: Cross-Sectional Ranking ===
print("H4: Cross-Sectional — daily rank, top/bottom 20% long/short...")
c = composite_zscore(factors_df, ics_all)
sig = pd.Series(0.0, index=c.index)
for date, group in c.groupby(c.index.normalize()):
    if len(group) < 10:
        continue
    k = max(1, int(len(group) * 0.20))
    ranked = group.sort_values()
    sig.loc[ranked.index[-k:]] = 1
    sig.loc[ranked.index[:k]] = -1
bt = backtest(sig, close_all)
results.append({"hypothesis": "H4: Cross-Sectional Ranking", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H5: Kalman Filter ===
print("H5: Kalman Filter on composite...")
c = composite_zscore(factors_df, ics_all).dropna()
try:
    # Simple 1D Kalman: state = filtered composite
    Q, R = 0.001, 0.1
    x = 0.0
    P = 1.0
    filtered = []
    for v in c.values:
        P += Q
        K = P / (P + R)
        x += K * (v - x)
        P *= (1 - K)
        filtered.append(x)
    sig = pd.Series(np.sign(filtered), index=c.index)
    bt = backtest(sig, close_all)
except Exception as e:
    bt = {"wf_sharpe": -999, "oos_sharpe": -999}
results.append({"hypothesis": "H5: Kalman-Filtered Signal", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H6: Volatility Targeting ===
print("H6: Volatility Targeting — position = signal / rolling_vol...")
c = composite_zscore(factors_df, ics_all)
sig_raw = pd.Series(0, index=c.index)
sig_raw[c > 0.3] = 1
sig_raw[c < -0.3] = -1
vol = close_all.pct_change().rolling(50).std() * np.sqrt(252 * 1440)
vol_target = vol.median()
sig = (sig_raw * vol_target / (vol + 1e-8)).clip(-3, 3)
bt = backtest(sig, close_all)
results.append({"hypothesis": "H6: Volatility-Targeted", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H7: Session Filter ===
print("H7: Session Filter — only trade 07-17 UTC (London+NY)...")
c = composite_zscore(factors_df, ics_all)
sig = pd.Series(0, index=c.index)
sig[c > 0.3] = 1
sig[c < -0.3] = -1
hours = sig.index.hour
sig[(hours < 7) | (hours >= 17)] = 0
bt = backtest(sig, close_all)
results.append({"hypothesis": "H7: Session-Filtered", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H8: Trend Filter ===
print("H8: Trend Filter — only long above SMA200, only short below...")
c = composite_zscore(factors_df, ics_all)
sig = pd.Series(0, index=c.index)
sig[c > 0.3] = 1
sig[c < -0.3] = -1
sma200 = close_all.rolling(200 * 1440).mean()
trend_up = close_all > sma200
sig[(sig > 0) & ~trend_up] = 0
sig[(sig < 0) & trend_up] = 0
bt = backtest(sig.dropna(), close_all)
results.append({"hypothesis": "H8: Trend-Filtered (SMA200)", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H9: Signal Decay ===
print("H9: Signal Decay — signal halves every hour...")
c = composite_zscore(factors_df, ics_all)
sig = pd.Series(0.0, index=c.index, dtype=float)
sig[c > 0.3] = 1.0
sig[c < -0.3] = -1.0
decay = 0.5 ** (1 / 60)  # Half-life = 60 bars (1 hour of 1-min data)
for i in range(1, len(sig)):
    if abs(sig.iloc[i]) < 0.01:
        sig.iloc[i] = sig.iloc[i - 1] * decay
bt = backtest(sig.clip(-1, 1), close_all)
results.append({"hypothesis": "H9: Signal Decay (60-min half-life)", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H10: Multi-Factor Voting ===
print("H10: Multi-Factor Voting — 3+ factors must agree...")
n_factors = min(5, len(factors_df.columns))
signals = []
for col in list(factors_df.columns)[:n_factors]:
    ic = ics_all.get(col, 0)
    if abs(ic) < 0.01:
        continue
    z = (factors_df[col] - factors_df[col].rolling(20).mean()) / (factors_df[col].rolling(20).std() + 1e-8)
    s = pd.Series(0, index=z.index)
    s[z > 0.3] = 1
    s[z < -0.3] = -1
    signals.append(s)
if len(signals) >= 3:
    sig = pd.Series(0, index=factors_df.index)
    stacked = pd.concat(signals, axis=1)
    sig[stacked.sum(axis=1) >= 2] = 1
    sig[stacked.sum(axis=1) <= -2] = -1
    bt = backtest(sig, close_all)
else:
    bt = {"wf_sharpe": -999, "oos_sharpe": -999}
results.append({"hypothesis": "H10: Multi-Factor Voting", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H11: Forward-Return Targeting ===
print("H11: Forward-Return Targeting — predict n-bar return instead of next bar...")
for n_bars in [12, 24, 48, 96]:
    fwd = close_all.pct_change(n_bars).shift(-n_bars).fillna(0)
    c = composite_zscore(factors_df, ics_all)
    sig = pd.Series(0, index=c.index)
    sig[c > 0.3] = 1
    sig[c < -0.3] = -1
    bt = backtest(sig, close_all)
    break  # Just test with 12-bar
results.append({"hypothesis": "H11: Forward-Return Targeting (12-bar)", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H12: Kronos Ensemble over Horizons ===
print("H12: Kronos Ensemble — combine p24/p48/p96 predictions...")
kronos_cols = [c for c in factors_df.columns if "Kronos" in c]
if len(kronos_cols) >= 2:
    k_df = factors_df[kronos_cols].ffill()
    c = pd.Series(0.0, index=k_df.index)
    for col in kronos_cols:
        ic = ics_all.get(col, 0)
        z = (k_df[col] - k_df[col].rolling(20).mean()) / (k_df[col].rolling(20).std() + 1e-8)
        c += ic * z
    sig = pd.Series(0, index=c.index)
    sig[c > 0.3] = 1
    sig[c < -0.3] = -1
    bt = backtest(sig, close_all)
else:
    bt = {"wf_sharpe": -999, "oos_sharpe": -999}
results.append({"hypothesis": "H12: Kronos Multi-Horizon Ensemble", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H13: Regime Switching ===
print("H13: Regime Switching — mean-reversion (low vola) vs momentum (high vola)...")
c = composite_zscore(factors_df, ics_all)
vol = close_all.pct_change().rolling(50).std()
vol_median = vol.median()
sig = pd.Series(0.0, index=c.index)
# Mean-reversion regime (low vol): invert signal
sig[c > 0.3] = -1
sig[c < -0.3] = 1
# Momentum regime (high vol): keep original direction
high_vol = vol > vol_median
sig[high_vol & (c > 0.3)] = 1
sig[high_vol & (c < -0.3)] = -1
bt = backtest(sig, close_all)
results.append({"hypothesis": "H13: Regime Switching", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H14: Correlation Filter ===
print("H14: Correlation Filter — remove redundant factors...")
corr = factors_df.corr().abs()
to_drop = set()
for i in range(len(corr.columns)):
    for j in range(i + 1, len(corr.columns)):
        if corr.iloc[i, j] > 0.7:
            ci, cj = corr.columns[i], corr.columns[j]
            ici, icj = abs(ics_all.get(ci, 0)), abs(ics_all.get(cj, 0))
            if ici >= icj:
                to_drop.add(cj)
            else:
                to_drop.add(ci)
filtered_cols = [c for c in factors_df.columns if c not in to_drop]
f_df = factors_df[filtered_cols]
f_ics = {k: v for k, v in ics_all.items() if k in filtered_cols}
c = composite_zscore(f_df, f_ics)
sig = pd.Series(0, index=c.index)
sig[c > 0.3] = 1
sig[c < -0.3] = -1
bt = backtest(sig, close_all)
results.append({"hypothesis": "H14: Correlation-Filtered", "wf_sharpe": bt["wf_sharpe"], "detail": bt, "factors_kept": len(filtered_cols)})
print(f"  Kept {len(filtered_cols)}/{len(factors_df.columns)} factors, WF={bt['wf_sharpe']:.3f}")


# === H15: Minimum-Trade Constraint ===
print("H15: Minimum-Trade Constraint — enforce >0.5 trades/day...")
best, best_e = -999, 0
for entry in np.arange(0.05, 0.51, 0.05):
    c = composite_zscore(factors_df, ics_all)
    sig = pd.Series(0, index=c.index)
    sig[c > entry] = 1
    sig[c < -entry] = -1
    trades = (sig.diff().abs() > 0).sum()
    if trades < 0.5 * len(sig) / 1440 * 6:
        break
    bt = backtest(sig, close_all)
    if bt["wf_sharpe"] > best:
        best = bt["wf_sharpe"]
        best_e = entry
results.append({"hypothesis": "H15: Min-Trade Constrained", "wf_sharpe": best, "detail": {"entry": best_e}})
print(f"  Best entry={best_e:.2f} WF={best:.3f}")


# === H16: Walk-Forward Optimization (simplified — test over 4 windows) ===
print("H16: Walk-Forward Opt — optimize per window...")
c = composite_zscore(factors_df, ics_all)
n = len(c)
split_points = [int(n * p) for p in [0.55, 0.65, 0.75, 0.85]]
wf_sharpes = []
for i, sp in enumerate(split_points):
    train_c = c.iloc[:sp]
    if len(train_c) < 100:
        continue
    test_c = c.iloc[sp:]
    sig_train = pd.Series(0, index=train_c.index)
    sig_train[train_c > 0.3] = 1
    sig_train[train_c < -0.3] = -1
    sig_test = pd.Series(0, index=test_c.index)
    sig_test[test_c > 0.3] = 1
    sig_test[test_c < -0.3] = -1
    bt = backtest(sig_test, close_all)
    wf_sharpes.append(bt["oos_sharpe"])
wf_mean = np.mean(wf_sharpes) if wf_sharpes else -999
results.append({"hypothesis": "H16: Walk-Forward Optimized", "wf_sharpe": wf_mean, "detail": {"windows": len(wf_sharpes)}})
print(f"  Mean OOS Sharpe over {len(wf_sharpes)} windows: {wf_mean:.3f}")


# === H17: Cost-Aware IC ===
print("H17: Cost-Aware IC — only compute IC on traded bars...")
c = composite_zscore(factors_df, ics_all)
sig = pd.Series(0, index=c.index)
sig[c > 0.3] = 1
sig[c < -0.3] = -1
fwd = close_all.pct_change().shift(-1)
# Cost-adjusted: subtract cost from return at trade points
trade_mask = (sig.diff().abs() > 0).shift(1).fillna(False)
cost_adj_return = fwd.copy()
cost_adj_return[trade_mask] -= TXN_COST_BPS / 10000
traded_mask = sig.shift(1).fillna(0) != 0
if traded_mask.sum() > 10:
    cost_ic = sig[traded_mask].corr(fwd[traded_mask])
else:
    cost_ic = 0
bt = backtest(sig, close_all)
results.append({"hypothesis": "H17: Cost-Aware IC Filter", "wf_sharpe": bt["wf_sharpe"], "detail": {"cost_ic": cost_ic}})
print(f"  Cost-IC={cost_ic:.4f} WF={bt['wf_sharpe']:.3f}")


# === H18: Anti-Momentum after >3σ events ===
print("H18: Anti-Momentum — fade >3σ moves...")
returns = close_all.pct_change()
sigma3 = returns.std() * 3
sig = pd.Series(0, index=close_all.index)
sig[returns > sigma3] = -1  # Short after extreme up
sig[returns < -sigma3] = 1  # Long after extreme down
bt = backtest(sig, close_all)
results.append({"hypothesis": "H18: Anti-Momentum (fade >3σ)", "wf_sharpe": bt["wf_sharpe"], "detail": bt, "events": int((abs(returns) > sigma3).sum())})
print(f"  Events={int((abs(returns)>sigma3).sum())} WF={bt['wf_sharpe']:.3f}")


# === H19: Time-Series CV ===
print("H19: Time-Series CV — chronological walk-forward...")
c = composite_zscore(factors_df, ics_all)
sig = pd.Series(0, index=c.index)
sig[c > 0.3] = 1
sig[c < -0.3] = -1
bt = backtest(sig, close_all)
results.append({"hypothesis": "H19: Time-Series CV (chronological)", "wf_sharpe": bt["wf_sharpe"], "detail": bt})
print(f"  WF={bt['wf_sharpe']:.3f}")


# === H20: Ensemble of Best Approaches ===
print("H20: Ensemble of Best — combine top-3 approaches by WF Sharpe...")
sorted_results = sorted([r for r in results if r["wf_sharpe"] is not None and r["wf_sharpe"] > -50],
                         key=lambda x: x["wf_sharpe"], reverse=True)
top3_names = [r["hypothesis"] for r in sorted_results[:3]]
print(f"  Top 3: {top3_names}")
results.append({"hypothesis": "H20: Ensemble Recommendation", "wf_sharpe": sorted_results[0]["wf_sharpe"] if sorted_results else -999,
                "detail": {"top3": top3_names}})


# === FINAL RANKING ===
print(f"\n{'='*80}")
print(f"{'RANK':<5} {'WF Sharpe':>10} {'OOS Sharpe':>10} {'OOS Mon%':>9} {'OOS DD%':>8} {'Trades':>7}  Hypothesis")
print(f"{'='*80}")

valid = [r for r in results if r.get("wf_sharpe") is not None and r["wf_sharpe"] > -50]
valid.sort(key=lambda x: x["wf_sharpe"], reverse=True)

for i, r in enumerate(valid, 1):
    d = r.get("detail", {})
    wf = r["wf_sharpe"]
    oos_s = d.get("oos_sharpe", -999)
    oos_m = d.get("oos_monthly", 0) or 0
    oos_d = (d.get("oos_dd", 0) or 0) * 100
    trades = d.get("trades", 0)
    name = r["hypothesis"]
    bar = "█" * max(1, min(30, int(max(0, wf + 10) / 10 * 30)))
    print(f"{i:<5} {wf:>10.3f} {oos_s:>10.3f} {oos_m:>8.2f}% {oos_d:>7.1f}% {trades:>7}  {name}")

print(f"{'='*80}")
print(f"Total time: {(time.time()-t0_total)/60:.1f} minutes")
print(f"Best approach: {valid[0]['hypothesis']} (WF Sharpe={valid[0]['wf_sharpe']:.3f})" if valid else "No valid results")
