"""
eu_boost_mc_v1.py — Bennett's conditional EU boost, tested against FTMO
=======================================================================
Run from project root on HOME MACHINE:
  python src\\research\\eu_boost_mc_v1.py

THE RULE UNDER TEST (pre-stated, no lookahead):
  At EU entry: if NO UJ position is currently open -> EU notional x BOOST.
  If UJ is open -> EU normal size. UJ always trades normal tier size.
  Decision uses only information available at entry time.

KNOWN TAIL (what this test prices): EU holds 52h. A boosted EU entered
Monday can have UJ fire Tuesday 05:00 EET on top of it — stacked at a
size v4 never tested. The diagnostics section counts exactly this.

SWEEP: BOOST in {1.0 (baseline = v4-equivalent), 1.5, 2.0}
RUNS:  FULL   = all history
       QUIET  = 8 lowest-frequency EU years only (regime stand-in,
                real day sequences, no synthetic thinning)

Output per cell: Pass% / DailyFail% / MaxLoss% / P2Fail% / Median months.
NOTE: acceptance criteria were NOT pre-stated for this test — reader
beware of picking the prettiest cell. The honest read: compare each
boost row against its own baseline row, same run, and ask whether the
median gain is worth the pass-rate cost.
"""
import csv, sys
import numpy as np
from pathlib import Path
from datetime import datetime, timedelta
from collections import defaultdict

# ---- FTMO rules (identical to combined_ftmo_mc_v4) ----
ACCOUNT=100_000; P1_TARGET=10_000; P2_TARGET=5_000
DAILY_LIMIT=5_000; MAXLOSS_FLOOR=90_000
N_SIMS=5_000; SEED=42
BASE_NOTIONAL=300_000
EU_HOLD_H=52; UJ_HOLD_H=24
BOOSTS=[1.0, 1.5, 2.0]
N_QUIET_YEARS=8
rng=np.random.default_rng(SEED)

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_bars(path):
    bars=[]
    with open(path, newline="", encoding="utf-8-sig") as f:
        reader=csv.DictReader(f); cols={c.lower().strip():c for c in reader.fieldnames}
        dtc=next(cols[k] for k in cols if "time" in k or "date" in k)
        hc=cols["high"]; lc=cols["low"]
        for r in reader:
            dt=parse_dt(r.get(dtc))
            if dt is None: continue
            try: hi=float(r[hc]); lo=float(r[lc])
            except (ValueError,KeyError): continue
            bars.append((dt,hi,lo))
    bars.sort(key=lambda x:x[0])
    idx=defaultdict(list)
    for b in bars: idx[b[0].date()].append(b)
    return idx

def load_trades():
    eu_p=BASE/"data"/"processed"/"trades"/"trades_real_costs.csv"
    uj_p=BASE/"data"/"processed"/"usdjpy_trades_real_costs.csv"
    T=[]
    with open(eu_p, newline="", encoding="utf-8-sig") as f:
        for r in csv.DictReader(f):
            e=parse_dt(r.get("entry_time")); x=parse_dt(r.get("exit_time"))
            if e is None: continue
            if x is None: x=e+timedelta(hours=EU_HOLD_H)
            try:
                ep=float(r["entry_price"]); sig=int(float(r["signal"]))
                pnl=float(r["dollar_pnl_real"]); za=float(r.get("zscore_abs",2.0))
            except (ValueError,KeyError): continue
            tier = 2.0 if za>=3.5 else (1.5 if za>=3.0 else 1.0)
            T.append({"pair":"EU","entry":e,"exit":x,"ep":ep,"dir":sig,
                      "pnl_1x":pnl/tier,"tier":tier})
    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:
                ep=float(r["entry_price"]); d=int(float(r["direction"]))
                pnl=float(r["pnl_real"]); mult=float(r.get("mult",1.0))
            except (ValueError,KeyError): continue
            T.append({"pair":"UJ","entry":e,"exit":e+timedelta(hours=UJ_HOLD_H),
                      "ep":ep,"dir":d,"pnl_1x":pnl/mult,"tier":mult})
    T.sort(key=lambda t:t["entry"])
    return T

def build_days(trades, bars_eu, bars_uj, boost):
    """Day records under Bennett's rule. Also returns diagnostics."""
    day_real=defaultdict(float); day_float=defaultdict(float); day_n=defaultdict(int)
    open_uj=[]   # exit datetimes of open UJ positions
    open_eu=[]   # (entry, exit, boosted?) for diagnostics
    diag={"eu_total":0,"eu_boosted":0,"eu_blocked_by_uj":0,"uj_onto_boosted":0}
    for t in trades:
        now=t["entry"]
        open_uj=[x for x in open_uj if x>now]
        if t["pair"]=="EU":
            diag["eu_total"]+=1
            if open_uj:
                mult=1.0; diag["eu_blocked_by_uj"]+=1; boosted=False
            else:
                mult=boost; boosted=(boost>1.0)
                if boosted: diag["eu_boosted"]+=1
            open_eu.append((t["entry"],t["exit"],boosted))
        else:
            mult=1.0
            open_uj.append(t["exit"])
            # did this UJ land on a boosted EU hold?
            if any(be<=now<=bx and bb for (be,bx,bb) in open_eu if bx>now):
                diag["uj_onto_boosted"]+=1
        eff=BASE_NOTIONAL*t["tier"]*mult
        day_real[t["exit"].date()] += t["pnl_1x"]*t["tier"]*mult
        bidx = bars_eu if t["pair"]=="EU" else bars_uj
        units = eff / t["ep"]
        d=t["entry"].date(); end=t["exit"].date()
        while d<=end:
            worst=0.0
            for (bt,hi,lo) in bidx.get(d,[]):
                if bt<t["entry"] or bt>t["exit"]: continue
                fl=(t["ep"]-lo)*units if t["dir"]>0 else (hi-t["ep"])*units
                if fl>worst: worst=fl
            if worst>0:
                day_float[d]+=worst; day_n[d]+=1
            d=d+timedelta(days=1)
    alld=sorted(set(day_real)|set(day_float))
    days=[(d, day_real.get(d,0.0), day_float.get(d,0.0), day_n.get(d,0)) for d in alld]
    return days, diag

# ---- FTMO challenge sim (identical logic to v4) ----
def run_phase(seq, target, start=0):
    eq=ACCOUNT; td=0; first=None; i=start; n=len(seq)
    while i<n:
        d,dp,df,ntr=seq[i]
        if first is None: first=d
        do=eq; floor=do-DAILY_LIMIT
        low=do+min(0.0,dp)-df
        if low<=MAXLOSS_FLOOR: return ("maxloss",i,(d-first).days)
        if low<=floor: return ("daily",i,(d-first).days)
        eq+=dp
        if ntr>0: td+=1
        if eq<=MAXLOSS_FLOOR: return ("maxloss",i,(d-first).days)
        if eq>=ACCOUNT+target and td>=4: return ("pass",i,(d-first).days)
        i+=1
    return ("incomplete",n-1,(seq[-1][0]-first).days if first else 0)

def run_challenge(seq):
    r1,i1,e1=run_phase(seq,P1_TARGET,0)
    if r1!="pass": return ("fail_p1_"+r1,e1)
    r2,i2,e2=run_phase(seq,P2_TARGET,i1+1)
    if r2!="pass": return ("fail_p2_"+r2,e1+e2)
    return ("pass",e1+e2)

def redate(seq):
    out=[]; d=datetime(2000,1,3).date()
    for (_od,p,f,n) in seq:
        out.append((d,p,f,n)); d+=timedelta(days=1)
        while d.weekday()>=5: d+=timedelta(days=1)
    return out

def summ(outs,pd_,n):
    p=sum(1 for o in outs if o=="pass")
    f1d=sum(1 for o in outs if o.startswith("fail_p1_daily"))
    f1m=sum(1 for o in outs if o.startswith("fail_p1_maxloss"))
    f2=sum(1 for o in outs if o.startswith("fail_p2"))
    med=float(np.median(np.array(pd_)/30.0)) if pd_ else 0
    return {"pass":100*p/n if n else 0,"daily":100*f1d/n if n else 0,
            "maxloss":100*f1m/n if n else 0,"p2":100*f2/n if n else 0,"med":med}

def m_seq(days):
    outs=[];pd_=[];n=len(days)
    for s in range(n):
        if n-s<8: break
        o,el=run_challenge(days[s:]); outs.append(o)
        if o=="pass": pd_.append(el)
    return summ(outs,pd_,len(outs))

def m_block(days,n_sims=N_SIMS,blk=21):
    outs=[];pd_=[];n=len(days);tl=min(n,24*21)
    for _ in range(n_sims):
        seq=[]
        while len(seq)<tl:
            st=rng.integers(0,max(1,n-blk)); seq.extend(days[st:st+blk])
        seq=redate(seq); o,el=run_challenge(seq); outs.append(o)
        if o=="pass": pd_.append(el)
    return summ(outs,pd_,n_sims)

def main():
    print("="*74)
    print("EU CONDITIONAL BOOST vs FTMO 2-Step Swing | base $300k=1x tier")
    print("Rule: EU x BOOST only when no UJ position open at EU entry")
    print("="*74)
    print("Loading bars + trades...")
    bars_eu=load_bars(next(BASE.rglob("EURUSD_15M*.csv")))
    bars_uj=load_bars(next(BASE.rglob("USDJPY_15M*.csv")))
    trades=load_trades()
    print(f"Trades: {len(trades):,} "
          f"(EU {sum(1 for t in trades if t['pair']=='EU'):,} / "
          f"UJ {sum(1 for t in trades if t['pair']=='UJ'):,})")

    # quiet-year selection by EU frequency (full years only)
    eu_by_year=defaultdict(int)
    for t in trades:
        if t["pair"]=="EU": eu_by_year[t["entry"].year]+=1
    full_years=[y for y in sorted(eu_by_year) if 2005<=y<=2025]
    quiet=sorted(sorted(full_years,key=lambda y:eu_by_year[y])[:N_QUIET_YEARS])
    print(f"\nQUIET regime = {N_QUIET_YEARS} lowest-EU-frequency years: {quiet}")
    print(f"  ({', '.join(f'{y}:{eu_by_year[y]}' for y in quiet)} EU trades/yr; "
          f"2026 partial runs {eu_by_year.get(2026,0)})")

    header=(f"  {'Run':>6}{'Boost':>7}{'Meth':>8}{'Pass%':>8}{'DailyF%':>9}"
            f"{'MaxL%':>7}{'P2F%':>7}{'MedMo':>7}")
    for boost in BOOSTS:
        days_all, diag = build_days(trades, bars_eu, bars_uj, boost)
        days_quiet=[d for d in days_all if d[0].year in quiet]
        tag="baseline (=v4 $300k)" if boost==1.0 else f"EU x{boost} when UJ flat"
        print(f"\n{'─'*74}\nBOOST {boost}x — {tag}")
        if boost>1.0:
            print(f"  Diagnostics: EU entries boosted {diag['eu_boosted']}/{diag['eu_total']} "
                  f"({100*diag['eu_boosted']/max(1,diag['eu_total']):.0f}%) | "
                  f"blocked by open UJ {diag['eu_blocked_by_uj']} | "
                  f"UJ landed ON a boosted EU hold {diag['uj_onto_boosted']} times")
        mx=max(d[2] for d in days_all)
        print(f"  Worst single-day concurrent float: ${mx:,.0f} "
              f"(99th pctile ${np.percentile([d[2] for d in days_all if d[2]>0],99):,.0f}) "
              f"vs $5,000 daily limit")
        print(header)
        for run,days in (("FULL",days_all),("QUIET",days_quiet)):
            rS=m_seq(days); rB=m_block(days)
            print(f"  {run:>6}{boost:>7.1f}{'A-seq':>8}{rS['pass']:>8.1f}{rS['daily']:>9.1f}"
                  f"{rS['maxloss']:>7.1f}{rS['p2']:>7.1f}{rS['med']:>7.1f}")
            print(f"  {'':>6}{'':>7}{'B-block':>8}{rB['pass']:>8.1f}{rB['daily']:>9.1f}"
                  f"{rB['maxloss']:>7.1f}{rB['p2']:>7.1f}{rB['med']:>7.1f}")

    print(f"\n{'='*74}")
    print("READ THE TABLE HONESTLY:")
    print("  1. Compare each boost row to the SAME-RUN baseline row.")
    print("  2. QUIET rows are the current-regime estimate — weight those.")
    print("  3. The cost of a failed challenge = fee + restart; the cost of")
    print("     a slow pass = patience only (Swing has no time limit).")
    print("  4. 'UJ landed on boosted hold' count = the untested-stack tail.")
    print("="*74)

if __name__=="__main__":
    main()
