"""
combined_ftmo_mc_v3_sweep.py - FTMO 2-Step Swing, NOTIONAL SWEEP
================================================================
Same accurate rules as v3, but sweeps notional per pair: 300k / 150k / 100k.

pnl and float_loss both scale LINEARLY with notional (a position twice as big
makes twice the $ and twice the $ floating loss). Base data is at 300k, so we
scale by factor = target_notional / 300000.

Reports CONSERVATIVE and SPREAD floating modes for each notional, focusing on
the key numbers: pass rate (A/B/C/D) and median months.
"""
import csv, sys
import numpy as np
from pathlib import Path
from datetime import datetime, timedelta
from collections import defaultdict

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
EU_HOLD_H=52; UJ_HOLD_H=24
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 parse_dt(s):
    s=(s or "").strip()
    for f in ("%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():
    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"))
            if e is None: continue
            try: pnl=float(r["dollar_pnl_real"]); mae=float(r.get("mae",0) or 0)
            except (ValueError,KeyError): continue
            T.append({"pair":"EU","entry":e,"hold_h":EU_HOLD_H,
                      "pnl":pnl,"float_loss":abs(mae)*BASE_NOTIONAL})
    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"]); mae=float(r.get("mae_pct",0) or 0)
            except (ValueError,KeyError): continue
            T.append({"pair":"UJ","entry":e,"hold_h":UJ_HOLD_H,
                      "pnl":pnl,"float_loss":(mae/100.0)*BASE_NOTIONAL})
    T.sort(key=lambda t:t["entry"])
    return T

def group_by_day(trades, spread, scale):
    day_real=defaultdict(float); day_float=defaultdict(float); day_n=defaultdict(int)
    for t in trades:
        d0=t["entry"].date()
        day_real[d0]+=t["pnl"]*scale
        day_n[d0]+=1
        fl=t["float_loss"]*scale
        if not spread:
            day_float[d0]=max(day_float[d0], fl)
        else:
            ndays=max(1,int(np.ceil(t["hold_h"]/24.0))); per=fl/ndays
            for k in range(ndays):
                dk=(t["entry"]+timedelta(days=k)).date()
                day_float[dk]=max(day_float[dk], per)
    alld=sorted(set(day_real)|set(day_float))
    return [(d, day_real.get(d,0.0), day_float.get(d,0.0), day_n.get(d,0)) for d in alld]

def run_phase(day_seq, target, start_idx=0):
    equity=ACCOUNT; trading_days=0; first=None; i=start_idx; n=len(day_seq)
    while i<n:
        d,day_pnl,day_float,ntr=day_seq[i]
        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):
    r1,idx1,el1=run_phase(day_seq,P1_TARGET,0)
    if r1!="pass": return (("fail_p1_"+r1),el1)
    r2,idx2,el2=run_phase(day_seq,P2_TARGET,idx1+1)
    if r2!="pass": return (("fail_p2_"+r2),el1+el2)
    return ("pass",el1+el2)

def summarize(outcomes, pass_days, n):
    passes=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
    return {"pass":100*passes/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,"n":n}

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):
    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 summarize(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 summarize(outs,pd,n_sims)

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

def sweep(trades):
    notionals=[300_000,150_000,100_000]
    print(f"\n{'='*78}")
    print(f"{'NOTIONAL SWEEP - FTMO 2-Step Swing $100k':^78}")
    print(f"{'='*78}")
    for spread in (False, True):
        tag="SPREAD (realistic)" if spread else "CONSERVATIVE (worst-case)"
        print(f"\n{'-'*78}\nFLOATING MODE: {tag}\n{'-'*78}")
        print(f"  {'Notional/pair':>14}{'Method':>10}{'Pass%':>8}{'DailyFail%':>12}"
              f"{'MaxLoss%':>10}{'P2Fail%':>9}{'MedMonths':>11}")
        for notl in notionals:
            scale=notl/BASE_NOTIONAL
            days=group_by_day(trades, spread, scale)
            mfl=max(d[2] for d in days)
            rA=m_seq(days); rB=m_block(days); rC=m_day(days)
            for name,r in [("A-seq",rA),("B-block",rB),("C-day",rC)]:
                print(f"  ${notl:>12,}{name:>10}{r['pass']:>8.1f}{r['daily']:>12.1f}"
                      f"{r['maxloss']:>10.1f}{r['p2']:>9.1f}{r['med']:>11.1f}")
            print(f"  {'':>14}{'(max 1-day float loss $'+format(mfl,',.0f')+')':>50}")
            print()

def main():
    print("="*78)
    print("FTMO 2-STEP SWING SIZING SWEEP | P1+10%/P2+5% | daily5% resets | maxloss10% static")
    print("="*78)
    trades=load_trades()
    print(f"Trades {len(trades):,}")
    sweep(trades)
    print("="*78)
    print("READ: find the notional where daily-DD failures drop acceptably while")
    print("median stays reasonable. Lower notional = safer but slower.")
    print("Daily limit $5,000. If max 1-day float loss < $5,000, daily breaches ~vanish.")
    print("="*78)

if __name__=="__main__":
    main()
