"""P21-pre: Distribution-day shorts diagnostic — pandas-only R/R check.

CAUSAL definition (no look-ahead):
  Day d is "tradeable" iff:
    - QQQ full-day ret on day d-1 < -0.5%   (prior session was a down day)
    - Breadth on day d-1 < 40%               (prior session had bad breadth)
  These are known by close of day d-1, so trading day d at 9:30 is causal.

For each tradeable day d, find the N "weakest" tickers by:
  - Largest negative gap (open[d]/close[d-1] - 1)  ← known by 9:30 of day d
  - AND below 20d MA on day d-1
  - AND ADV20 ≥ $25M, price ≥ $10
  - AND |gap| ≤ 10% (HTB / borrow / halt risk filter — exclude extreme gaps)

Trade: short at open[d], cover at close[d]. open-to-close return as proxy.

KILL GATE: avg_win < avg_loss → adverse R/R, abort engine build.
"""
from __future__ import annotations

import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd

UNIVERSE_FILE = "data/cache/screener_universe/c71fae94f9066a528905bba18f8a9b307c95f126.json"
DAILY_DIR = Path("data/cache/daily_snapshots/orb_daily_v46_20260423")
QQQ_PARQUET = "data/cache/daily/QQQ.parquet"
WINDOW_START = "2025-07-09"
WINDOW_END = "2026-04-23"
MIN_PRICE = 10.0
MIN_AVG_DOLLAR_VOL = 25_000_000
QQQ_DOWN_THR = -0.005  # -0.5% full-day
BREADTH_THR = 0.40
TOP_N_WEAKEST = 10


def main() -> None:
    universe = json.load(open(UNIVERSE_FILE))["symbols"]
    ws = pd.Timestamp(WINDOW_START)
    we = pd.Timestamp(WINDOW_END)

    # 1) Load all liquid tickers' daily data once into a long DataFrame
    print("Loading universe daily data...")
    rows = []
    for sym in universe:
        path = DAILY_DIR / f"{sym}.parquet"
        if not path.exists():
            continue
        try:
            df = pd.read_parquet(path)
        except Exception:
            continue
        if len(df) < 25:
            continue
        df = df.sort_values("date").reset_index(drop=True)
        df["date"] = pd.to_datetime(df["date"])
        df["prev_close"] = df["close"].shift(1)
        df["full_ret"] = (df["close"] / df["prev_close"]) - 1.0
        df["gap"] = (df["open"] / df["prev_close"]) - 1.0
        df["oc_ret"] = (df["close"] / df["open"]) - 1.0
        df["ma20"] = df["close"].rolling(20).mean()
        df["dollar_vol"] = df["close"] * df["volume"]
        df["adv20"] = df["dollar_vol"].rolling(20).mean()
        df["below_ma20_prev"] = df["close"].shift(1) < df["ma20"].shift(1)
        sub = df[(df["date"] >= ws) & (df["date"] <= we)].copy()
        sub["ticker"] = sym
        rows.append(sub[["ticker", "date", "open", "close", "prev_close",
                         "full_ret", "gap", "oc_ret", "ma20",
                         "below_ma20_prev", "adv20", "volume"]])

    big = pd.concat(rows, ignore_index=True)
    print(f"Total ticker-days in window: {len(big)}")

    # 2) Compute breadth per date (across liquid universe)
    liquid = big[big["adv20"] >= MIN_AVG_DOLLAR_VOL].copy()
    liquid = liquid[liquid["close"] >= MIN_PRICE]
    print(f"Liquid ticker-days: {len(liquid)}")

    breadth = liquid.groupby("date").apply(
        lambda g: pd.Series({
            "n": len(g),
            "n_pos": (g["full_ret"] > 0).sum(),
            "n_neg": (g["full_ret"] < 0).sum(),
            "frac_pos": (g["full_ret"] > 0).sum() / len(g) if len(g) else 0,
        })
    ).reset_index()

    # 3) Load QQQ
    qqq = pd.read_parquet(QQQ_PARQUET)
    qqq["date"] = pd.to_datetime(qqq["date"])
    qqq = qqq.sort_values("date").reset_index(drop=True)
    qqq["qqq_full_ret"] = (qqq["close"] / qqq["close"].shift(1)) - 1.0
    qqq = qqq[["date", "qqq_full_ret"]]

    # 4) Identify distribution days
    day_summary = breadth.merge(qqq, on="date", how="left")
    day_summary = day_summary.sort_values("date").reset_index(drop=True)
    day_summary["is_distribution"] = (
        (day_summary["qqq_full_ret"].shift(1) < QQQ_DOWN_THR)
        & (day_summary["frac_pos"].shift(1) < BREADTH_THR)
    )

    n_window = len(day_summary)
    n_dist = day_summary["is_distribution"].sum()
    print(f"\n=== Distribution day stats ===")
    print(f"Total trading days: {n_window}")
    print(f"Distribution days (QQQ < {QQQ_DOWN_THR*100}% AND breadth < {BREADTH_THR*100}%): {n_dist}")

    if n_dist < 5:
        print(f"INSUFFICIENT n: only {n_dist} distribution days. Loosen thresholds.")
        # Loosen (CAUSAL: shifted regime signals)
        for q_thr, b_thr in [(-0.003, 0.45), (-0.002, 0.50), (0.0, 0.50)]:
            day_summary[f"d_{q_thr}_{b_thr}"] = (
                (day_summary["qqq_full_ret"].shift(1) < q_thr)
                & (day_summary["frac_pos"].shift(1) < b_thr)
            )
            print(f"  q<{q_thr*100:+.2f}%, breadth<{b_thr*100:.0f}%: {day_summary[f'd_{q_thr}_{b_thr}'].sum()} days")

    dist_dates = day_summary.loc[day_summary["is_distribution"], "date"].tolist()
    if not dist_dates:
        print("No distribution days at primary thresholds — switching to looser thresholds.")
        loose_mask = (day_summary["qqq_full_ret"].shift(1) < -0.003) & (day_summary["frac_pos"].shift(1) < 0.45)
        dist_dates = day_summary.loc[loose_mask, "date"].tolist()
        print(f"Loose distribution days: {len(dist_dates)}")

    if not dist_dates:
        print("Aborting: no usable distribution days.")
        return

    # 5) For each distribution day, find top N weakest tickers
    weakest_rows = []
    for d in dist_dates:
        on_d = liquid[liquid["date"] == d].copy()
        # Weakness: negative gap, below MA20 prior day, |gap| ≤ 10% (HTB filter)
        on_d = on_d[
            (on_d["gap"] < 0)
            & (on_d["gap"] >= -0.10)
            & (on_d["below_ma20_prev"])
        ]
        # Sort by most negative gap
        on_d = on_d.sort_values("gap").head(TOP_N_WEAKEST)
        weakest_rows.append(on_d)

    if not weakest_rows:
        print("No weakest candidates on distribution days.")
        return
    cand = pd.concat(weakest_rows, ignore_index=True)
    print(f"\nTotal short candidates: {len(cand)} (across {len(dist_dates)} distribution days)")

    # 6) R/R measurement: short at open, cover at close (open-to-close ret)
    short_pnl = -cand["oc_ret"]  # short profit
    n = len(short_pnl)
    wins = short_pnl[short_pnl > 0]
    losses = short_pnl[short_pnl < 0]
    wr = len(wins) / n if n else 0
    avg_win = wins.mean() if len(wins) else 0
    avg_loss = -losses.mean() if len(losses) else 0
    expectancy = short_pnl.mean()

    print(f"\n=== HYPOTHETICAL SHORT (open-to-close, no slippage/fees) ===")
    print(f"n: {n} (wins={len(wins)}, losses={len(losses)})")
    print(f"WR: {wr*100:.2f}%")
    print(f"Avg WIN  (price went DOWN): +{avg_win*100:.3f}%")
    print(f"Avg LOSS (price went UP):   -{avg_loss*100:.3f}%")
    print(f"Expectancy / trade:         {expectancy*100:+.4f}%")
    print(f"Total cumulative ret:       {short_pnl.sum()*100:+.2f}%")

    if avg_loss > 0:
        ratio = avg_win / avg_loss
        print(f"\nR/R asymmetry: avg_win / avg_loss = {ratio:.3f}")
    if n >= 30:
        if avg_win < avg_loss:
            print("VERDICT: KILL — adverse R/R asymmetry (consistent with prior gap-down shorts research).")
        elif expectancy < 0:
            print("VERDICT: KILL — negative expectancy.")
        else:
            print("VERDICT: PROCEED — favorable R/R, positive expectancy.")
    else:
        print(f"VERDICT: INSUFFICIENT n ({n} < 30).")

    # 7) Show top winners and losers
    print(f"\n=== Top 5 short wins ===")
    cand["short_pnl"] = short_pnl
    print(cand.sort_values("short_pnl", ascending=False).head(5)[["ticker", "date", "gap", "oc_ret", "short_pnl"]].to_string(index=False))
    print(f"\n=== Top 5 short losses ===")
    print(cand.sort_values("short_pnl", ascending=True).head(5)[["ticker", "date", "gap", "oc_ret", "short_pnl"]].to_string(index=False))

    # 8) Threshold sensitivity sweep
    print(f"\n=== Threshold sensitivity (distribution day def + top_n) ===")
    print(f"{'qqq_thr':>10} | {'breadth':>8} | {'top_n':>5} | {'days':>5} | {'n_cand':>7} | {'WR%':>7} | {'avg_win%':>9} | {'avg_loss%':>10} | {'expect%':>9} | {'verdict':>10}")
    for q_thr in [-0.005, -0.003, -0.001]:
        for b_thr in [0.35, 0.45, 0.55]:
            for tn in [5, 10, 20]:
                mask = (day_summary["qqq_full_ret"].shift(1) < q_thr) & (day_summary["frac_pos"].shift(1) < b_thr)
                ddates = day_summary.loc[mask, "date"].tolist()
                if not ddates:
                    continue
                rr = []
                for d in ddates:
                    o = liquid[liquid["date"] == d]
                    o = o[
                        (o["gap"] < 0)
                        & (o["gap"] >= -0.10)
                        & (o["below_ma20_prev"])
                    ].sort_values("gap").head(tn)
                    rr.append(o["oc_ret"])
                if not rr:
                    continue
                ret = pd.concat(rr, ignore_index=True)
                sp = -ret
                nn = len(sp)
                if nn < 10:
                    continue
                w = sp[sp > 0]
                l = sp[sp < 0]
                wrv = len(w) / nn
                aw = w.mean() if len(w) else 0
                al = -l.mean() if len(l) else 0
                ex = sp.mean()
                v = "PROCEED" if (aw >= al and ex > 0 and nn >= 30) else "KILL"
                print(f"{q_thr*100:>+9.2f}% | {b_thr*100:>7.0f}% | {tn:>5d} | {len(ddates):>5d} | {nn:>7d} | {wrv*100:>6.2f} | {aw*100:>8.3f} | {al*100:>9.3f} | {ex*100:>+8.4f} | {v:>10}")


if __name__ == "__main__":
    main()
