"""
V33 Diagnostic: QQQ ORB Candle Quality as Market-Level Regime Signal

Hypothesis: on days when QQQ's own 5-min ORB bar is bullish (strong body, closes near high),
individual gap-up stock ORB breakouts have better follow-through.

The current V24 regime filter requires QQQ to open > +0.15% (daily gap) but doesn't check
whether QQQ's ORB CANDLE ITSELF is bullish. A day where QQQ opens +0.2% but the ORB bar is
red (intraday fade) may be a trap day. A day where QQQ opens +0.2% AND the ORB bar is strongly
bullish is a cleaner market-wide breakout day.

Features (all from QQQ's 9:30-9:35 ET bar — available at 9:35 AM before any entry):
  qqq_orb_body_pct  : (close - open) / (high - low). +1.0 = full bullish candle, 0 = doji
  qqq_orb_return    : (close - open) / open. Raw return of QQQ's ORB bar.
  qqq_orb_close_loc : (close - low) / (high - low). Close position in bar range (1 = at high)

Source: V24 400d trade set (n=~304 trades, ~120-140 unique trade days).
Note: day-level feature (all trades on same day share same QQQ ORB value) — effective n for
correlation is unique trading days, but trade-level Pearson is also reported.

Gates (same as V25-V31):
  G1: |Pearson| >= 0.07 on >= 120 trades (or >= 80 unique days)
  G2: |top - bottom tercile avg_R| >= 0.30R
  G3: |top - bottom tercile WR| >= 5pp
  G5a: |rho(qqq_feature, V24 obv_slope per trade)| < 0.70 (redundancy check)
"""
from __future__ import annotations

import datetime as dt
import json
import os
import sys
from pathlib import Path

import pyarrow.parquet as pq

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")))

from zoneinfo import ZoneInfo

_ET = ZoneInfo("America/New_York")
_MKT_OPEN = dt.time(9, 30)

INTRADAY_CACHE_DIR = "data/cache/intraday"
# Use most recent V24 400d run (trail=0.8, risk=0.05, OBV-slope=0.05)
V24_400D_RUN = "runs/intraday_orb/intraday_20260422_011012_06f59ede.json"


def _parse_ts(ts_raw: object) -> dt.datetime:
    s = str(ts_raw)
    if s.endswith("Z"):
        s = s[:-1] + "+00:00"
    return dt.datetime.fromisoformat(s).astimezone(_ET)


def load_qqq_orb_bar(date: str) -> dict | None:
    """Load QQQ's 5-minute ORB bar (9:30-9:35 ET) for a given date."""
    path = Path(INTRADAY_CACHE_DIR) / "QQQ" / f"{date}.parquet"
    if not path.exists():
        return None
    try:
        table = pq.read_table(str(path))
        rows = table.to_pydict()
    except Exception:
        return None
    for i, ts_raw in enumerate(rows.get("timestamp", [])):
        try:
            ts = _parse_ts(ts_raw)
        except Exception:
            continue
        if ts.time() == _MKT_OPEN:
            o = float(rows["open"][i] or 0)
            h = float(rows["high"][i] or 0)
            lo = float(rows["low"][i] or 0)
            c = float(rows["close"][i] or 0)
            if o > 0 and h > lo:
                return {"open": o, "high": h, "low": lo, "close": c}
    return None


def pearson(xs: list[float], ys: list[float]) -> float | None:
    n = len(xs)
    if n < 2:
        return None
    xm, ym = sum(xs) / n, sum(ys) / n
    num = sum((xs[i] - xm) * (ys[i] - ym) for i in range(n))
    dx = sum((x - xm) ** 2 for x in xs) ** 0.5
    dy = sum((y - ym) ** 2 for y in ys) ** 0.5
    if dx <= 0 or dy <= 0:
        return None
    return num / (dx * dy)


def tercile_stats(vals: list[float], rs: list[float]) -> dict:
    if len(vals) < 9:
        return {}
    pairs = sorted(zip(vals, rs), key=lambda p: p[0])
    n = len(pairs)
    t = n // 3
    def stats(sub):
        ys = [p[1] for p in sub]
        return {"n": len(ys), "wr": sum(1 for y in ys if y > 0) / len(ys), "avg_r": sum(ys) / len(ys)}
    return {"low": stats(pairs[:t]), "mid": stats(pairs[t:2*t]), "high": stats(pairs[2*t:])}


def check_gates(label: str, vals: list[float], r_mult: list[float]) -> None:
    n = len(vals)
    p = pearson(vals, r_mult)
    ts = tercile_stats(vals, r_mult)
    if not ts:
        print(f"  {label}: n={n}, insufficient data")
        return
    low, mid, high = ts["low"], ts["mid"], ts["high"]
    g1 = p is not None and abs(p) >= 0.07 and n >= 80
    g2 = abs(high["avg_r"] - low["avg_r"]) >= 0.30
    g3 = abs(high["wr"] - low["wr"]) >= 0.05
    print(f"\n  [{label}]  n={n}  Pearson={p:.3f}")
    print(f"    G1: {'PASS' if g1 else 'FAIL'} (|{p:.3f}| {'≥' if g1 else '<'} 0.07, n={n})")
    print(f"    G2: {'PASS' if g2 else 'FAIL'} (|top-bot avg_R| = {abs(high['avg_r']-low['avg_r']):.3f}R)")
    print(f"    G3: {'PASS' if g3 else 'FAIL'} (|top-bot WR| = {abs(high['wr']-low['wr'])*100:.1f}pp)")
    print(f"    Bottom tercile: WR {low['wr']*100:.1f}%  avg_R {low['avg_r']:.3f}  n={low['n']}")
    print(f"    Middle tercile: WR {mid['wr']*100:.1f}%  avg_R {mid['avg_r']:.3f}  n={mid['n']}")
    print(f"    Top tercile:    WR {high['wr']*100:.1f}%  avg_R {high['avg_r']:.3f}  n={high['n']}")
    status = "PASS ALL" if (g1 and g2 and g3) else "FAIL"
    print(f"    Status: {status}")


def main() -> None:
    print("=== V33 QQQ ORB Candle Quality Diagnostic ===\n")

    with open(V24_400D_RUN) as f:
        run_data = json.load(f)
    trades = run_data.get("trades", [])
    m = run_data.get("metrics", {})
    print(f"Loaded V24 400d run: {m.get('total_trades')} trades, "
          f"{m.get('start_date')} → {m.get('end_date')}")
    print(f"Return: {m.get('total_return_pct', 0)*100:.2f}%  "
          f"DD: {m.get('max_drawdown_pct', 0)*100:.2f}%  "
          f"Sharpe: {m.get('sharpe_ratio', 0):.3f}\n")

    # Build per-trade data
    trade_records = [
        {"date": t["date"], "r_multiple": t.get("r_multiple_at_exit") or 0.0}
        for t in trades
        if t.get("r_multiple_at_exit") is not None
    ]
    dates_needed = sorted(set(r["date"] for r in trade_records))
    print(f"Trade records: {len(trade_records)} with r_multiple, across {len(dates_needed)} unique dates")

    # Load QQQ ORB bars for each date
    print("Loading QQQ ORB bars...")
    qqq_bars: dict[str, dict] = {}
    missing = 0
    for date in dates_needed:
        bar = load_qqq_orb_bar(date)
        if bar:
            qqq_bars[date] = bar
        else:
            missing += 1
    print(f"  Loaded: {len(qqq_bars)}/{len(dates_needed)} dates (missing: {missing})")

    # Compute features per trade
    body_vals, return_vals, closeloc_vals, r_mults = [], [], [], []
    skipped = 0
    for rec in trade_records:
        bar = qqq_bars.get(rec["date"])
        if not bar:
            skipped += 1
            continue
        o, h, lo, c = bar["open"], bar["high"], bar["low"], bar["close"]
        body_range = h - lo
        if body_range <= 0:
            skipped += 1
            continue
        body_pct = (c - o) / body_range  # [-1, 1], +1 = full bullish
        orb_ret = (c - o) / o           # raw return of ORB bar
        close_loc = (c - lo) / body_range  # [0, 1], 1 = closed at high

        body_vals.append(body_pct)
        return_vals.append(orb_ret)
        closeloc_vals.append(close_loc)
        r_mults.append(rec["r_multiple"])

    n_valid = len(r_mults)
    coverage = n_valid / len(trade_records) if trade_records else 0
    print(f"  Valid trades: {n_valid}/{len(trade_records)} (coverage {coverage*100:.1f}%)")
    print(f"  Skipped: {skipped}\n")

    if n_valid < 30:
        print("Insufficient data for analysis.")
        return

    print("=" * 60)
    print("GATE RESULTS (G1: |Pearson|>=0.07 & n>=80; G2: avg_R gap>=0.30R; G3: WR gap>=5pp)")
    check_gates("qqq_orb_body_pct", body_vals, r_mults)
    check_gates("qqq_orb_return", return_vals, r_mults)
    check_gates("qqq_orb_close_loc", closeloc_vals, r_mults)

    # Redundancy check vs OBV-slope (from trade obv_slope_20 stored in enrichment)
    # We don't have per-trade obv_slope but check pairwise between qqq features
    p_body_ret = pearson(body_vals, return_vals)
    p_body_cloc = pearson(body_vals, closeloc_vals)
    p_ret_cloc = pearson(return_vals, closeloc_vals)
    print("\n  Inter-feature correlations:")
    print(f"    ρ(body_pct, orb_return) = {p_body_ret:.3f}")
    print(f"    ρ(body_pct, close_loc)  = {p_body_cloc:.3f}")
    print(f"    ρ(orb_ret,  close_loc)  = {p_ret_cloc:.3f}")

    # Day-level analysis (aggregate by unique date)
    day_data: dict[str, dict] = {}
    for i, rec in enumerate(trade_records):
        if rec["date"] not in qqq_bars:
            continue
        d = rec["date"]
        if d not in day_data:
            bar = qqq_bars[d]
            o, h, lo, c = bar["open"], bar["high"], bar["low"], bar["close"]
            body_range = h - lo
            if body_range <= 0:
                continue
            day_data[d] = {
                "qqq_body": (c - o) / body_range,
                "qqq_return": (c - o) / o,
                "qqq_close_loc": (c - lo) / body_range,
                "r_mults": [],
            }
        day_data[d]["r_mults"].append(rec["r_multiple"])
    day_avg_r = {d: sum(v["r_mults"]) / len(v["r_mults"]) for d, v in day_data.items()}
    day_body = [v["qqq_body"] for d, v in day_data.items()]
    day_ret = [v["qqq_return"] for d, v in day_data.items()]
    day_cloc = [v["qqq_close_loc"] for d, v in day_data.items()]
    day_r = [day_avg_r[d] for d in day_data]
    p_day_body = pearson(day_body, day_r)
    p_day_ret = pearson(day_ret, day_r)
    p_day_cloc = pearson(day_cloc, day_r)
    print(f"\n  Day-level correlation (n_days={len(day_data)}, using avg_R per day):")
    print(f"    ρ(qqq_body,     avg_day_R) = {p_day_body:.3f}")
    print(f"    ρ(qqq_orb_ret,  avg_day_R) = {p_day_ret:.3f}")
    print(f"    ρ(qqq_close_loc, avg_day_R) = {p_day_cloc:.3f}")

    print("\n=== Summary ===")
    print("Signal library context: 7 prior axes failed G2 >= 0.30R (only OBV-slope 0.394R passed).")
    print("QQQ ORB body_pct, return, close_loc are day-level features (market-wide confirmation).")
    print("These cannot be used for candidate ranking (same value for all stocks on same day).")
    print("Use case: day-level gate (skip all ORB trades when QQQ ORB candle is weak/bearish).")


if __name__ == "__main__":
    main()
