"""LiquidityLevels ES/NQ overnight session study (2026-10-06). Descriptive statistics only; not trading advice.

Reproduce:
  curl -A "Mozilla/5.0" "https://query1.finance.yahoo.com/v8/finance/chart/ES=F?interval=1h&range=730d" -o es1h.json
  curl -A "Mozilla/5.0" "https://query1.finance.yahoo.com/v8/finance/chart/NQ=F?interval=1h&range=730d" -o nq1h.json
  python overnight-session-study.py es1h.json nq1h.json

Sessions (America/New_York, hourly bars by start time): Asia 20:00-01:59, London 02:00-07:59,
US 09:00-15:59; Tuesday-Friday only. Python 3.9+, standard library only (zoneinfo needs tz data on Windows: pip install tzdata).
Yahoo Finance data is unaudited and the window rolls forward, so re-running later gives different (similar) numbers.
"""
import json, datetime, collections, math, sys
from zoneinfo import ZoneInfo
ET=ZoneInfo('America/New_York')
def load(fn):
    r=json.load(open(fn))['chart']['result'][0]; q=r['indicators']['quote'][0]
    bars=[]
    for i,t in enumerate(r['timestamp']):
        o,h,l,c=q['open'][i],q['high'][i],q['low'][i],q['close'][i]
        if None in (o,h,l,c): continue
        dt=datetime.datetime.fromtimestamp(t,datetime.UTC).astimezone(ET)
        bars.append((dt,o,h,l,c))
    return bars
def sessions(bars):
    # trading day D: Asia = D-1 20:00 .. D 01:59 ; London = D 02:00..07:59 ; US = D 09:00..15:59 ; prior RTH = D-1 09:00..15:59
    by=collections.defaultdict(list)
    for b in bars: by[b[0].date()].append(b)
    days=[]
    dates=sorted(by)
    for d in dates:
        if d.weekday()>4: continue
        prev=d-datetime.timedelta(days=1)
        # find previous trading date with RTH data
        pd=None
        for k in range(1,5):
            c=d-datetime.timedelta(days=k)
            if c.weekday()<5 and c in by and any(9<=x[0].hour<16 for x in by[c]): pd=c; break
        if pd is None: continue
        asia=[x for x in by.get(pd,[]) if x[0].hour>=20]+[x for x in by[d] if x[0].hour<2]
        lon=[x for x in by[d] if 2<=x[0].hour<8]
        us=[x for x in by[d] if 9<=x[0].hour<16]
        prth=[x for x in by[pd] if 9<=x[0].hour<16]
        if len(asia)<5 or len(lon)<5 or len(us)<5 or len(prth)<5: continue
        days.append(dict(date=d,
            ah=max(x[2] for x in asia),al=min(x[3] for x in asia),
            lh=max(x[2] for x in lon),ll=min(x[3] for x in lon),lc=lon[-1][4],lo=lon[0][1],
            uo=us[0][1],uc=us[-1][4],uh=max(x[2] for x in us),ul=min(x[3] for x in us),
            ph=max(x[2] for x in prth),pl=min(x[3] for x in prth),pc=prth[-1][4]))
    return days
def wilson(k,n,z=1.96):
    if n==0: return (0,0)
    p=k/n; den=1+z*z/n; c=p+z*z/(2*n); a=z*math.sqrt(p*(1-p)/n+z*z/(4*n*n)); return ((c-a)/den,(c+a)/den)
def pct(k,n): return f"{100*k/n:.1f}%"
def run(name,fn):
    D=sessions(load(fn)); n=len(D); out={'name':name,'n':n,'from':str(D[0]['date']),'to':str(D[-1]['date'])}
    hi=[d for d in D if d['lh']>d['ah']]; lo=[d for d in D if d['ll']<d['al']]
    both=[d for d in D if d['lh']>d['ah'] and d['ll']<d['al']]; neither=[d for d in D if d['lh']<=d['ah'] and d['ll']>=d['al']]
    out['london_above_asia_high']=len(hi); out['london_below_asia_low']=len(lo); out['both']=len(both); out['neither']=len(neither)
    # sweep-and-return: London exceeds Asia high but London closes back below it
    sh=[d for d in D if d['lh']>d['ah'] and d['lc']<=d['ah']]; acc=[d for d in D if d['lh']>d['ah'] and d['lc']>d['ah']]
    sl=[d for d in D if d['ll']<d['al'] and d['lc']>=d['al']]; accl=[d for d in D if d['ll']<d['al'] and d['lc']<d['al']]
    out['sweep_high_return']=len(sh); out['accept_above']=len(acc); out['sweep_low_return']=len(sl); out['accept_below']=len(accl)
    def us_dir(L,sign):  # share of days where US session (9:00 open -> 15:00 bar close) moved in sign
        up=sum(1 for d in L if d['uc']>d['uo']); return up,len(L)
    out['us_up_all']=us_dir(D,1)
    out['us_up_after_sweep_high_return']=us_dir(sh,1); out['us_up_after_accept_above']=us_dir(acc,1)
    out['us_up_after_sweep_low_return']=us_dir(sl,1); out['us_up_after_accept_below']=us_dir(accl,1)
    # prior RTH high/low taken overnight (Asia+London = use ah/lh)
    ovh=[d for d in D if max(d['ah'],d['lh'])>d['ph']]; ovl=[d for d in D if min(d['al'],d['ll'])<d['pl']]
    out['overnight_above_prior_rth_high']=len(ovh); out['overnight_below_prior_rth_low']=len(ovl)
    # ranges
    ar=[d['ah']-d['al'] for d in D]; lr=[d['lh']-d['ll'] for d in D]; pr=[d['ph']-d['pl'] for d in D]
    out['median_asia_range']=sorted(ar)[n//2]; out['median_london_range']=sorted(lr)[n//2]; out['median_prior_rth_range']=sorted(pr)[n//2]
    # by weekday london sweep any side
    wd=collections.defaultdict(lambda:[0,0])
    for d in D:
        k=d['date'].weekday(); wd[k][1]+=1
        if d['lh']>d['ah'] or d['ll']<d['al']: wd[k][0]+=1
    out['weekday_any']={k:wd[k] for k in sorted(wd)}
    out['_D']=D
    return out
res=[run('ES',sys.argv[1]),run('NQ',sys.argv[2])]
for r in res:
    D=r.pop('_D'); print(json.dumps(r,default=str))
    for k in ('london_above_asia_high','london_below_asia_low','both','neither','sweep_high_return','accept_above','sweep_low_return','accept_below','overnight_above_prior_rth_high','overnight_below_prior_rth_low'):
        print('  ',k,r[k],pct(r[k],r['n']))
json.dump(res,open('study_out.json','w'),default=str)
