"""
combined_ftmo_mc_v4.py - DEFINITIVE FTMO 2-Step Swing test
==========================================================
Reads REAL per-day floating drawdown (floating_dd_by_day.json, built from 15M
bars) and applies the exact FTMO 2-Step Swing rules. No CONSERVATIVE/SPREAD
guess - we have the true intraday floating path now.

Each day record (at $300k notional):
  realized_pnl_300k          - closed P&L that day
  worst_float_loss_300k      - worst single open position underwater that day
  concurrent_float_loss_300k - SUM of all concurrent open positions underwater
  n_open                     - open positions that day

FTMO 2-Step Swing $100k:
  P1 +10% (+$10k), P2 +5% (+$5k), sequential, pass=both
  Daily loss 5% ($5k) RESETS nightly from day's opening balance, incl floating
  Max loss 10% static $90k floor, incl floating
  No time limit; min 4 trading days/phase

We use CONCURRENT floating loss for the daily/max-loss checks (the honest FTMO
measure - total equity includes ALL open positions). Notional scalable.

Methods: A sequential replay, B block bootstrap, C day-level MC, D intervention.
Sweeps notional 300k/150k/100k so you can see exactly where 300k lands.
"""
import json, sys
import numpy as np
from pathlib import Path
from datetime import datetime, timedelta

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
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 load_days():
    p=BASE/"data"/"processed"/"floating_dd_by_day.json"
    recs=json.loads(p.read_text(encoding="utf-8"))
    days=[]
    for r in recs:
        d=datetime.fromisoformat(r["date"]).date()
        days.append((d, r["realized_pnl_300k"], r["concurrent_float_loss_300k"],
                     r.get("n_open",0)))
    days.sort(key=lambda x:x[0])
    return days

def run_phase(day_seq, target, scale, start_idx=0):
    equity=ACCOUNT; trading_days=0; first=None; i=start_idx; n=len(day_seq)
    while i<n:
        d,day_pnl_b,day_float_b,ntr=day_seq[i]
        day_pnl=day_pnl_b*scale; day_float=day_float_b*scale
        if first is None: first=d
        day_open=equity; daily_floor=day_open-DAILY_LIMIT
        intraday_low=day_open+min(0.0,day_pnl)-day_float
        if intraday_low<=MAXLOSS_FLOOR: return ("maxloss",i,(d-first).days)
        if intraday_low<=daily_floor:   return ("daily",i,(d-first).days)
        equity+=day_pnl
        if ntr>0: trading_days+=1
        if equity<=MAXLOSS_FLOOR: return ("maxloss",i,(d-first).days)
        if equity>=ACCOUNT+target and trading_days>=4: return ("pass",i,(d-first).days)
        i+=1
    return ("incomplete",n-1,(day_seq[-1][0]-first).days if first else 0)

def run_challenge(day_seq, scale):
    r1,idx1,el1=run_phase(day_seq,P1_TARGET,scale,0)
    if r1!="pass": return (("fail_p1_"+r1),el1)
    r2,idx2,el2=run_phase(day_seq,P2_TARGET,scale,idx1+1)
    if r2!="pass": return (("fail_p2_"+r2),el1+el2)
    return ("pass",el1+el2)

def summ(outcomes, pass_days, n):
    p=sum(1 for o in outcomes if o=="pass")
    f1d=sum(1 for o in outcomes if o.startswith("fail_p1_daily"))
    f1m=sum(1 for o in outcomes if o.startswith("fail_p1_maxloss"))
    f2=sum(1 for o in outcomes if o.startswith("fail_p2"))
    med=float(np.median(np.array(pass_days)/30.0)) if pass_days else 0
    p25=float(np.percentile(np.array(pass_days)/30.0,25)) if pass_days else 0
    p75=float(np.percentile(np.array(pass_days)/30.0,75)) if pass_days 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,"p25":p25,"p75":p75}

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

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

def m_block(days,scale,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,scale); outs.append(o)
        if o=="pass": pd.append(el)
    return summ(outs,pd,n_sims)

def m_day(days,scale,n_sims=N_SIMS):
    outs=[];pd=[];n=len(days);tl=min(n,24*21);pool=np.arange(n)
    for _ in range(n_sims):
        pick=rng.choice(pool,size=tl,replace=True)
        seq=redate([days[i] for i in pick]); o,el=run_challenge(seq,scale); outs.append(o)
        if o=="pass": pd.append(el)
    return summ(outs,pd,n_sims)

def m_interv(days,scale,n_sims=2000,n_events=3,extra=2):
    outs=[];pd=[];n=len(days);tl=min(n,24*21)
    dp=sorted(d[1] for d in days); med=dp[len(dp)//2]
    bad=[d for d in days if d[1]<med]
    for _ in range(n_sims):
        pick=rng.choice(np.arange(n),size=tl,replace=True)
        seq=[list(days[i]) for i in pick]
        for _e in range(n_events):
            pos=rng.integers(0,len(seq)); sp=seq[pos][1]; sf=seq[pos][2]; sn=seq[pos][3]
            for _c in range(extra):
                bd=bad[rng.integers(0,len(bad))]; sp+=bd[1]; sf+=bd[2]; sn+=bd[3]
            seq[pos][1]=sp; seq[pos][2]=sf; seq[pos][3]=sn
        seq=redate([tuple(s) for s in seq]); o,el=run_challenge(seq,scale); outs.append(o)
        if o=="pass": pd.append(el)
    return summ(outs,pd,n_sims)

def main():
    print("="*80)
    print("FTMO 2-STEP SWING v4 - DEFINITIVE (real intraday floating DD from 15M bars)")
    print("P1+10%/P2+5% | daily5% resets | maxloss10% static | concurrent floating used")
    print("="*80)
    days=load_days()
    print(f"Loaded {len(days):,} real trading-day records")
    mc=max(d[2] for d in days)
    print(f"Max concurrent floating loss (300k): ${mc:,.0f}")
    print(f"NOTE: UJ exit approximated entry+24h (no exit_time col); EU exact.\n")

    for notl in (300_000,150_000,100_000):
        scale=notl/BASE_NOTIONAL
        mc_s=mc*scale
        print(f"\n{'='*80}")
        print(f"NOTIONAL ${notl:,}/pair  (max concurrent float loss ${mc_s:,.0f} vs $5,000 daily limit)")
        print(f"{'='*80}")
        rA=m_seq(days,scale); print("  A done",flush=True)
        rB=m_block(days,scale); print("  B done",flush=True)
        rC=m_day(days,scale); print("  C done",flush=True)
        rD=m_interv(days,scale); print("  D done",flush=True)
        print(f"\n  {'Method':>10}{'Pass%':>8}{'DailyF%':>9}{'MaxL%':>7}{'P2F%':>7}"
              f"{'Med':>6}{'P25':>6}{'P75':>6}")
        for nm,r in [("A-seq",rA),("B-block",rB),("C-day",rC),("D-interv",rD)]:
            print(f"  {nm:>10}{r['pass']:>8.1f}{r['daily']:>9.1f}{r['maxloss']:>7.1f}"
                  f"{r['p2']:>7.1f}{r['med']:>6.1f}{r['p25']:>6.1f}{r['p75']:>6.1f}")
    print(f"\n{'='*80}")
    print("This is the accurate test. Daily/maxloss use REAL concurrent floating loss.")
    print("Find the notional with high pass% and acceptable median months.")
    print("="*80)

if __name__=="__main__":
    main()
