"""
parity_replay_v1.py — Macro Model Frequency Audit, backtest side
=================================================================
Run from project root on HOME MACHINE:
  python src/research/parity_replay_v1.py

The LIVE side has already been audited from live_monitor_v2.log
(2026-04-09 -> 2026-07-04). Findings embedded below as constants:

  EURUSD: 27 signal episodes seen live -> 8 traded.
          16 killed by "outside session" filter, 2 by Fib pull guard,
          1 silent non-arm (unexplained).
  USDJPY: 5 episodes -> all traded (8 orders incl. re-entries).
  Downtime: only ~40h non-weekend over 12.4 weeks (~2%) — NOT the cause.
  Stale rates: 3 FRED 500s (Apr 9/11 only), JP2Y stale 3 days — minor.

This script answers the remaining questions FROM THE BACKTEST TRADE LISTS,
with pre-stated verdict rules (bottom). No tuning, read-only.

  Q1 (convention): does 3.5/wk count re-entries? -> episode clustering.
  Q2 (regime):     per-year counts; do quiet years look like 2026?
  Q3 (defect):     does the backtest restrict entries to the live session
                   hours? If backtest entries occur at hours the live
                   executor blocks, the live session filter is an
                   UNVALIDATED restriction (= defect vs validated model).

TIMEZONES: live log timestamps are UTC+1 (verified vs state file UTC).
Backtest data is EET (EEST = UTC+3 in Apr–Jul). LOG + 2h = DATA time.
Adjust LOG_TO_DATA_H below if your data offset differs.
"""
import csv, sys, json
import numpy as np
from pathlib import Path
from datetime import datetime, timedelta
from collections import defaultdict

# ---------------- CONFIG ----------------
LOG_TO_DATA_H   = 2          # add to log times to get data (EET) time
EPISODE_GAP_H   = 6          # entries/signals within 6h = same episode
LIVE_WIN_START  = datetime(2026, 4, 9)    # log time
LIVE_WIN_END    = datetime(2026, 7, 4)
WEEKS_IN_WINDOW = (LIVE_WIN_END - LIVE_WIN_START).days / 7.0

# Live session windows inferred from the log (LOG time):
#   EURUSD entries allowed ~08:00-16:59, USDJPY ~06:00-16:59
LIVE_SESSION = {"EU": (8, 17), "UJ": (6, 17)}   # log-time hours [start, end)

# Live P&L aggregates (account_snapshot 2026-07-04, full live period):
LIVE_PNL = {"EU": (-763.07, 13), "UJ": (-2586.44, 8)}   # (total $, n trades)

# ---------------- EMBEDDED LIVE EVIDENCE (log time, UTC+1) ----------------
EPISODES = {"EURUSD": [["2026-04-17 14:01","2026-04-17 15:01",3.04,"TRADED"],
 ["2026-04-30 16:01","2026-04-30 20:01",3.03,"SESSION"],
 ["2026-05-14 13:46","2026-05-14 16:16",3.39,"TRADED"],
 ["2026-05-15 16:01","2026-05-15 16:01",3.02,"TRADED"],
 ["2026-05-18 07:31","2026-05-18 08:01",3.12,"SESSION"],
 ["2026-05-18 19:31","2026-05-18 19:31",2.90,"SESSION"],
 ["2026-05-19 21:31","2026-05-19 21:31",2.89,"SESSION"],
 ["2026-05-20 15:31","2026-05-20 16:46",4.24,"TRADED"],
 ["2026-05-21 15:16","2026-05-21 15:16",3.19,"SILENT"],
 ["2026-05-22 18:01","2026-05-22 18:16",3.02,"SESSION"],
 ["2026-05-24 22:01","2026-05-24 22:16",2.82,"SESSION"],
 ["2026-05-27 11:31","2026-05-27 19:16",3.70,"SESSION+SILENT"],
 ["2026-05-29 15:46","2026-05-29 16:46",3.38,"TRADED"],
 ["2026-06-01 15:01","2026-06-01 15:01",2.95,"FIBGUARD"],
 ["2026-06-02 18:31","2026-06-02 19:16",2.94,"SESSION"],
 ["2026-06-05 14:01","2026-06-05 15:46",3.36,"TRADED"],
 ["2026-06-09 17:16","2026-06-09 17:46",2.93,"SESSION"],
 ["2026-06-10 20:31","2026-06-10 20:31",3.04,"SESSION"],
 ["2026-06-11 20:16","2026-06-11 21:01",3.11,"SESSION"],
 ["2026-06-14 23:01","2026-06-14 23:31",3.33,"SESSION"],
 ["2026-06-17 12:31","2026-06-17 12:31",2.97,"FIBGUARD"],
 ["2026-06-17 19:01","2026-06-17 21:01",4.26,"SESSION"],
 ["2026-06-22 17:16","2026-06-22 17:16",2.83,"SESSION"],
 ["2026-06-23 13:01","2026-06-23 14:16",2.83,"TRADED"],
 ["2026-06-24 17:31","2026-06-24 17:31",2.82,"SESSION"],
 ["2026-07-01 20:01","2026-07-01 20:01",3.18,"SESSION"],
 ["2026-07-02 13:31","2026-07-02 19:31",3.88,"TRADED"]],
"USDJPY": [["2026-04-30 06:01","2026-05-01 05:01",3.32,"TRADED x3"],
 ["2026-05-05 06:01","2026-05-06 05:01",2.35,"TRADED"],
 ["2026-05-15 06:02","2026-05-17 22:02",2.17,"TRADED"],
 ["2026-06-18 06:02","2026-06-21 21:02",2.48,"TRADED x2"],
 ["2026-06-22 06:02","2026-06-23 05:02",2.21,"TRADED"]]}

# ---------------- paths / loading ----------------
def resolve_base():
    for b in [Path(r"C:\Users\Administrator\OneDrive\fx_macro_intraday"),
              Path(r"C:\Users\paul_\OneDrive\fx_macro_intraday"),
              Path(__file__).resolve().parents[2]]:
        if b.exists(): return b
    sys.exit("base not found")
BASE = resolve_base()

def parse_dt(s):
    s = (s or "").strip()
    for f in ("%Y-%m-%d %H:%M:%S","%Y-%m-%d %H:%M","%Y.%m.%d %H:%M:%S",
              "%Y.%m.%d %H:%M","%d/%m/%Y %H:%M"):
        try: return datetime.strptime(s[:19], f)
        except ValueError: continue
    return None

def load_trades():
    """Returns {'EU': [(entry_dt, pnl)], 'UJ': [(entry_dt, pnl)]} in DATA time."""
    T = {"EU": [], "UJ": []}
    eu_p = BASE/"data"/"processed"/"trades"/"trades_real_costs.csv"
    uj_p = BASE/"data"/"processed"/"usdjpy_trades_real_costs.csv"
    with open(eu_p, newline="", encoding="utf-8-sig") as f:
        for r in csv.DictReader(f):
            e = parse_dt(r.get("entry_time"))
            if e is None: continue
            try: pnl = float(r["dollar_pnl_real"])
            except (ValueError, KeyError): pnl = np.nan
            T["EU"].append((e, pnl))
    with open(uj_p, newline="", encoding="utf-8-sig") as f:
        for r in csv.DictReader(f):
            e = parse_dt(r.get("entry_time"))
            if e is None: continue
            try: pnl = float(r["pnl_real"])
            except (ValueError, KeyError): pnl = np.nan
            T["UJ"].append((e, pnl))
    for k in T: T[k].sort(key=lambda x: x[0])
    return T

def cluster(entries, gap_h=EPISODE_GAP_H):
    """Group entry datetimes into episodes."""
    eps = []
    for e in entries:
        if eps and (e - eps[-1][-1]).total_seconds() <= gap_h*3600:
            eps[-1].append(e)
        else:
            eps.append([e])
    return eps

def main():
    print("="*72)
    print("PARITY REPLAY v1 — backtest side of the frequency audit")
    print(f"Live window: {LIVE_WIN_START.date()} -> {LIVE_WIN_END.date()}"
          f"  ({WEEKS_IN_WINDOW:.1f} wk) | LOG+{LOG_TO_DATA_H}h = DATA time")
    print("="*72)
    T = load_trades()

    verdict_bits = []

    for pair, name in (("EU","EURUSD"), ("UJ","USDJPY")):
        tr = T[pair]
        entries = [e for e,_ in tr]
        pnls    = np.array([p for _,p in tr if not np.isnan(p)])
        print(f"\n{'─'*72}\n{name} — {len(tr):,} backtest trades "
              f"({entries[0].date()} -> {entries[-1].date()})\n{'─'*72}")

        # Q2 REGIME: per-year counts -------------------------------------
        by_year = defaultdict(int)
        for e in entries: by_year[e.year] += 1
        years = sorted(by_year)
        counts = [by_year[y] for y in years]
        print("  Per-year trade counts:")
        for y in years:
            n = by_year[y]; wk = n/52.0
            bar = "#"*int(n/2)
            print(f"    {y}: {n:4d}  ({wk:4.2f}/wk)  {bar}")
        mean_wk = np.mean(counts)/52.0
        q1 = np.percentile(counts, 25)
        print(f"  Mean {mean_wk:.2f}/wk | quietest year {min(counts)} "
              f"({min(counts)/52:.2f}/wk) | 25th pct {q1:.0f}/yr ({q1/52:.2f}/wk)")

        # Q1 CONVENTION: episode clustering ------------------------------
        eps = cluster(entries)
        mult = len(entries)/len(eps) if eps else 0
        print(f"\n  Episode clustering (gap<={EPISODE_GAP_H}h):")
        print(f"    trades={len(entries):,}  episodes={len(eps):,}"
              f"  re-entry multiplier={mult:.2f}x")
        print(f"    -> {mean_wk:.2f} trades/wk = {mean_wk/mult:.2f} episodes/wk")

        # Q3 DEFECT: entry-hour histogram vs live session -----------------
        lo, hi = LIVE_SESSION[pair]
        lo_d, hi_d = (lo+LOG_TO_DATA_H) % 24, (hi+LOG_TO_DATA_H) % 24
        hours = defaultdict(int)
        for e in entries: hours[e.hour] += 1
        in_sess = sum(v for h,v in hours.items() if lo_d <= h < hi_d) \
                  if lo_d < hi_d else \
                  sum(v for h,v in hours.items() if h >= lo_d or h < hi_d)
        pct_in = 100*in_sess/len(entries)
        print(f"\n  Entry hours (DATA/EET time). Live session = "
              f"{lo:02d}-{hi:02d} log = {lo_d:02d}-{hi_d:02d} data:")
        row = "    " + " ".join(f"{h:02d}:{hours.get(h,0)}" for h in range(24))
        print(row)
        print(f"    Backtest entries INSIDE live session window: {pct_in:.0f}%")
        if pct_in < 90:
            verdict_bits.append(
                f"{name}: only {pct_in:.0f}% of validated backtest entries fall "
                f"inside the live session window -> live session filter is an "
                f"UNVALIDATED restriction (DEFECT vs validated model). "
                f"Expected live count should be scaled by ~{pct_in:.0f}%.")
        else:
            verdict_bits.append(
                f"{name}: backtest already restricted to live session hours "
                f"({pct_in:.0f}% inside) -> session filter matches; frequency "
                f"gap must come from convention/regime.")

        # Live-window overlay ---------------------------------------------
        ws = LIVE_WIN_START + timedelta(hours=LOG_TO_DATA_H)
        we = LIVE_WIN_END   + timedelta(hours=LOG_TO_DATA_H)
        in_win = [e for e in entries if ws <= e <= we]
        if in_win:
            print(f"\n  Backtest trades INSIDE the live window: {len(in_win)}"
                  f"  ({len(in_win)/WEEKS_IN_WINDOW:.2f}/wk)")
            for e in in_win: print(f"    {e}")
        else:
            print(f"\n  (Backtest trade list ends {entries[-1].date()} — "
                  f"no overlap with live window; use per-year rates above.)")

        # Live episodes for this pair --------------------------------------
        lname = name
        leps = EPISODES[lname]
        traded = sum(1 for e in leps if e[3].startswith("TRADED"))
        print(f"\n  LIVE (from log): {len(leps)} episodes"
              f" ({len(leps)/WEEKS_IN_WINDOW:.2f}/wk), {traded} traded")

        # Secondary check (a): per-trade P&L on-model? ---------------------
        tot, n = LIVE_PNL[pair]
        live_avg = tot/n
        if len(pnls) > 50:
            boots = []
            rng = np.random.default_rng(42)
            for _ in range(10000):
                boots.append(rng.choice(pnls, size=n, replace=True).mean())
            boots = np.array(boots)
            pctile = 100*(boots < live_avg).mean()
            print(f"\n  P&L check: live avg ${live_avg:,.0f}/trade (n={n}) vs "
                  f"backtest avg ${pnls.mean():,.0f}")
            print(f"    Live sample mean sits at the {pctile:.0f}th percentile of "
                  f"{n}-trade bootstrap means from the backtest.")
            if 2.5 < pctile < 97.5:
                print("    -> WITHIN normal variation. Edge not falsified at n="
                      f"{n}.")
            else:
                print("    -> OUTSIDE 95% band. Investigate fills/costs, not "
                      "just frequency.")

    # ---------------- VERDICT ----------------
    print(f"\n{'='*72}\nVERDICT INPUTS (pre-stated rules)\n{'='*72}")
    for b in verdict_bits: print("  •", b)
    print("""
  RULES:
  1) If backtest entry hours span outside the live session window
     -> DEFECT: live executor enforces a session filter the validated
        model never had. (Live log shows it killed 16/27 EURUSD episodes.)
  2) Re-entry multiplier > 1.3 -> CONVENTION: the 3.5/wk figure counts
     clustered re-entries; compare episodes/wk to live episodes/wk instead.
  3) If quietest backtest years run at <= the live episode rate
     -> REGIME contributes; recalibrate the expectation, don't touch code.
  Multiple can be true. The Fib pull guard (2 blocks) and arm/touch 6h
  expiry (2 blocks) must also each exist in the validated backtest with
  identical parameters, or they are additional unvalidated restrictions.
""")

if __name__ == "__main__":
    main()
