"""
VOL & RANGE FORECAST v3 — daily tool (Parkinson-EWMA + event & weekday conditional multipliers)
Run on the VPS (MT5 terminal running):
    python vol_range_forecast.py EURUSD GBPUSD XAUUSD

Out-of-sample scoring vs the v1 close-close engine (2018-2026, EURUSD/GBPUSD):
  pinball loss -17..19%, MAE -17..19%, corr with realized range 0.39->0.54 / 0.42->0.52,
  weekday coverage healed from 64-78% spread to 72-76% across all days.
Method:
  - Parkinson daily variance ln(H/L)^2 / 4ln2, EWMA lambda=0.94 -> next-session sigma
  - multipliers = median & 75th pct of (range%/sigma) over the trailing 150 sessions
    OF THE TARGET WEEKDAY (captures Mon-quiet / midweek-event structure)
"""
import sys, math, statistics
from datetime import datetime, timezone
import MetaTrader5 as mt5

LAMBDA=0.94; PULL=1500; WD_ROLL=150; EVENTS_CSV="calendar_events.csv"   # optional; weekday-only if absent
EVENT_CLASSES={"FOMC":("United States","Fed Interest Rate Decision"),
               "NFP":("United States","Payroll Jobs Growth"),
               "USCPI":("United States","Inflation Rate Year-over-Year"),
               "ECB":("Euro Area","ECB Interest Rate Decision"),
               "BOE":("United Kingdom","BoE Interest Rate Decision")}
PAIR_EVENT_CLASSES={"EURUSD":["FOMC","ECB","NFP","USCPI"],"GBPUSD":["FOMC","BOE","NFP","USCPI"]}

def load_events():
    import csv, os
    out={k:set() for k in EVENT_CLASSES}
    if not os.path.exists(EVENTS_CSV): return {}
    try:
        with open(EVENTS_CSV,encoding="utf-8",errors="replace") as f:
            for row in csv.DictReader(f):
                for cls,(cty,name) in EVENT_CLASSES.items():
                    if row.get("country")==cty and row.get("event")==name:
                        out[cls].add(str(row.get("date",""))[:10])
        return out
    except Exception:
        return {}

def q(v,p):
    s=sorted(v); k=(len(s)-1)*p; f=int(k)
    return s[f]+(s[min(f+1,len(s)-1)]-s[f])*(k-f)

def forecast(symbol, target_wd, ev=None, today=None):
    rates=mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_D1, 0, PULL)
    if rates is None or len(rates)<400:
        return f"──── {symbol} ─────────────────\n  insufficient D1 history"
    o=[r['open'] for r in rates]; h=[r['high'] for r in rates]
    l=[r['low'] for r in rates];  c=[r['close'] for r in rates]
    wd=[datetime.fromtimestamp(r['time'],timezone.utc).weekday() for r in rates]
    ds=[datetime.fromtimestamp(r['time'],timezone.utc).strftime("%Y-%m-%d") for r in rates]
    classes=[c for c in PAIR_EVENT_CLASSES.get(symbol,["FOMC","NFP","USCPI"]) if ev and c in ev]
    def day_class(dstr):
        for cls in classes:
            if dstr in ev[cls]: return cls
        return None
    park=[(math.log(h[i]/l[i])**2)/(4*math.log(2)) for i in range(len(o))]
    var=statistics.fmean(park[1:31]); sig=[]
    for i in range(1,len(o)):
        sig.append(math.sqrt(var))            # sigma forecast FOR day i (info to i-1)
        var=LAMBDA*var+(1-LAMBDA)*park[i]
    sig_next=math.sqrt(var)
    xr_w={w:[] for w in range(7)}; xo_w={w:[] for w in range(7)}
    hist={cls:[] for cls in classes}
    for i in range(1,len(o)):
        s=sig[i-1]
        if s<=0: continue
        pair=((h[i]-l[i])/o[i]/s, abs(c[i]-o[i])/o[i]/s)
        cls=day_class(ds[i])
        if cls: hist[cls].append(pair)
        else:
            xr_w[wd[i]].append(pair[0]); xo_w[wd[i]].append(pair[1])
    label="weekday"; tcls=day_class(today) if today else None
    if tcls and len(hist[tcls])>=24:
        pool=hist[tcls][-150:]; label=tcls
        xr=[a for a,b in pool]; xo=[b for a,b in pool]
    else:
        if tcls: label=f"{tcls} (thin history — weekday bands)"
        xr=xr_w.get(target_wd,[])[-WD_ROLL:]; xo=xo_w.get(target_wd,[])[-WD_ROLL:]
        if len(xr)<40:   # weekend/holiday fallback: unconditional
            xr=[v for w in range(5) for v in xr_w[w]][-750:]
            xo=[v for w in range(5) for v in xo_w[w]][-750:]
    ann=sig_next*math.sqrt(252)*100
    f50r,f75r=sig_next*q(xr,.5),sig_next*q(xr,.75)
    f50o,f75o=sig_next*q(xo,.5),sig_next*q(xo,.75)
    px=c[-1]
    evline="" if label=="weekday" else f"⚠️ {label} day — event-conditioned bands\n"
    return (f"──── {symbol} ─────────────────\n{evline}"
            f"Volatility (annualized) : {ann:.2f}%\n"
            f"High to Low range       : {f50r*100:.2f}% median · {f75r*100:.2f}% 75th Percentile\n"
            f"Open to Close move      : {f50o*100:.2f}% median · {f75o*100:.2f}% 75th Percentile\n"
            f"(ref close {px:.5f} → 75th-pct range ≈ {f75r*px:.5f} price units · weekday-conditioned)")

if __name__=="__main__":
    symbols=sys.argv[1:] or ["EURUSD","GBPUSD"]
    if not mt5.initialize(): raise SystemExit(f"MT5 initialize failed: {mt5.last_error()}")
    try:
        now=datetime.now()
        print("**VOL & RANGE FORECAST v2**")
        print(f"**For session: {now:%A, %B %d, %Y}**")
        ev=load_events()
        today=now.strftime("%Y-%m-%d")
        if not ev: print("(no calendar_events.csv found — weekday-only bands)")
        for s in symbols:
            mt5.symbol_select(s,True)
            print(forecast(s, now.weekday(), ev, today))
    finally:
        mt5.shutdown()
