#!/usr/bin/env python3
"""Python3: --release URL-or-local-file [--inputs URL-or-file] [--bootstrap].
Core uses stdlib; optional interval reproduction requires numpy. Does not
certify statistical suitability, rights, prospective performance or execution.
"""
import argparse,csv,hashlib,io,json,math,statistics,urllib.request
from pathlib import Path

def read(p):
    return urllib.request.urlopen(p,timeout=90).read() if p.startswith(('http://','https://')) else Path(p).read_bytes()

def check(a,b):
    if a is None or b is None:assert a is b,(a,b)
    elif isinstance(a,bool) or isinstance(b,bool):assert a is b,(a,b)
    elif isinstance(a,(float,int)) and isinstance(b,(float,int)):assert math.isclose(a,b,rel_tol=1e-9,abs_tol=1e-9),(a,b)
    else:assert a==b,(a,b)

def smooth(v,n,alpha=None,start=0):
    out=[None]*len(v)
    if len(v)<start+n:return out
    out[start+n-1]=sum(v[start:start+n])/n;weight=alpha or 1/n
    for j in range(start+n,len(v)):out[j]=weight*v[j]+(1-weight)*out[j-1]
    return out

def verify(d,raw):
    assert hashlib.sha256(raw).hexdigest()==d['input_sha256'];groups={}
    for r in csv.DictReader(io.StringIO(raw.decode())):groups.setdefault(r['symbol'],[]).append({k:float(v) if v else None for k,v in r.items() if k not in ('symbol','source_sha256')})
    expected=[];checked=0
    for symbol,bars in groups.items():
        bars.sort(key=lambda r:r['opened']);assert len({r['opened'] for r in bars})==len(bars)
        c=[r['c'] for r in bars];n=len(c);idx={r['opened']+3600:i for i,r in enumerate(bars)}
        delta=[0]+[c[i]-c[i-1] for i in range(1,n)];gain=smooth([max(0,x) for x in delta],14,start=1);loss=smooth([max(0,-x) for x in delta],14,start=1)
        rsi=[None if gain[i] is None else 50 if gain[i]==loss[i]==0 else 100 if loss[i]==0 else 100-100/(1+gain[i]/loss[i]) for i in range(n)]
        fast=smooth(c,12,2/13);slow=smooth(c,26,2/27);ema=smooth(c,21,2/22);line=[fast[i]-slow[i] if i>=25 else None for i in range(n)];signal=[None]*25+smooth(line[25:],9,2/10);hist=[line[i]-signal[i] if signal[i] is not None else None for i in range(n)]
        tr=[];pd=[0];md=[0]
        for i,r in enumerate(bars):
            assert 0<r['l']<=min(r['o'],r['c'])<=max(r['o'],r['c'])<=r['h']
            tr.append(max(r['h']-r['l'],abs(r['h']-c[i-1]),abs(r['l']-c[i-1])) if i else r['h']-r['l'])
            if i:
                up=r['h']-bars[i-1]['h'];down=bars[i-1]['l']-r['l'];pd.append(up if up>down and up>0 else 0);md.append(down if down>up and down>0 else 0)
        atr=smooth(tr,14);trs,ps,ms=[smooth(v,14,start=1) for v in (tr,pd,md)];plus=[100*ps[i]/trs[i] if trs[i] else 0 for i in range(n)];minus=[100*ms[i]/trs[i] if trs[i] else 0 for i in range(n)];dx=[100*abs(a-b)/(a+b) if a+b else 0 for a,b in zip(plus,minus)];adx=smooth(dx,14,start=14)
        ledger=[];last=-math.inf;blocker=None;retained=[]
        for i in range(200,n):
            r=bars[i];t=r['opened']+3600
            if t<d['observation_start'] or t>d['observation_end'] or r['opened']-bars[i-200]['opened']!=200*3600:continue
            dd=(1-c[i]/max(x['h'] for x in bars[i-168:i]))*100
            if r['l']>=min(x['l'] for x in bars[i-48:i]) or dd<5:continue
            oid=f'{symbol}:{int(t)}';keep=t-last>=180*3600
            ledger.append((oid,keep,None if keep else blocker))
            if keep:retained.append(oid);last=t;blocker=oid
        actual=[(r['observation_id'],r['retained'],r['blocking_id']) for r in d['raw_event_ledger'] if r['symbol']==symbol];assert actual==ledger
        events=[e for e in d['observations'] if e['symbol']==symbol];assert [e['observation_id'] for e in events]==retained
        for e in events:
            i=idx[e['event_time']];j=i+12;L=bars[i]['l'];A=atr[i-1];b=.1*A;t=e['event_time'];check(L,e['reference_low']);check(A,e['atr_prior']);check(t+12*3600,e['decision_time'])
            wait=bars[i+1:j+1];ready=len(wait)==12 and wait[-1]['opened']-bars[i]['opened']==12*3600
            def fail(path):return next((k for k in range(1,len(path)) if path[k-1]['c']<L-b and path[k]['c']<L-b),None)
            early=fail(wait) is not None if ready else None;check(early,e['early_failure']);check(bool(ready and not early and c[j]>L),e['eligible'])
            if ready:
                q=sum(r['qv'] for r in wait);mean=sum(r['qv'] for r in bars[i-24:i])/24;valid=q>0 and all(r['taker_buy_valid']==1 and r['taker_buy_qv'] is not None and 0<=r['taker_buy_qv']<=r['qv'] for r in wait);buy=sum(r['taker_buy_qv'] for r in wait) if valid else None
                sess=[r for r in bars[max(0,j-23):j+1] if r['opened']//86400==bars[j]['opened']//86400];whole=sess[0]['opened']%86400==0 and len(sess)==int(bars[j]['opened']%86400/3600)+1 and all(r['v'] is not None and r['v']>0 and r['qv']>=0 for r in sess);qv=sum(r['qv'] for r in sess) if whole else None;v=sum(r['v'] for r in sess) if whole else None;vw=qv/v if v else None
                prev=min(range(i-48,i),key=lambda k:(c[k],k));lo1=min(r['l'] for r in wait[:6]);lo2=min(r['l'] for r in wait[6:])
                values=dict(rsi_trigger=rsi[i],rsi_T=rsi[j],macd_trigger=hist[i],macd_T=hist[j],ema21_T=ema[j],adx_trigger=adx[i],adx_T=adx[j],minus_di_trigger=minus[i],minus_di_T=minus[j],first6_low=lo1,last6_low=lo2,wait_quote=q,prior_quote_mean=mean,wait_buy_quote=buy,vwap_quote=qv,vwap_base=v,vwap_T=vw,prior_closing_low=c[prev],prior_closing_low_rsi=rsi[prev],wait_change_pct=(c[j]/c[i]-1)*100,low_distance_pct=(c[j]/L-1)*100,distance_atr=(c[j]-L)/A)
                for k,value in values.items():check(value,e[k])
                flags=dict(price_hold=lo2>lo1 and lo1>=L-b,rsi_repair=rsi[j]>rsi[i] and rsi[j]>30,macd_repair=hist[j]>hist[i],divergence=c[i]<c[prev] and rsi[i]>rsi[prev],volume=q/12>=1.5*mean if mean>0 else None,buy_flow=buy/q>=.55 if valid else None,ema_reclaim=c[j]>ema[j],vwap_reclaim=c[j]>vw if vw else None,adx_easing=adx[j]<adx[i] and minus[j]<minus[i])
                for key,parts in [('price_macd',['price_hold','macd_repair']),('macd_volume',['macd_repair','volume']),('price_buy',['price_hold','buy_flow']),('triple',['price_hold','macd_repair','volume'])]:
                    vals=[flags[k] for k in parts];flags[key]=False if False in vals else None if None in vals else True
                for k,value in flags.items():check(value,e[k])
            else:assert all(e[k] is None for k in d['contract']['rules'])
            for h in (24,72,168):
                path=bars[i:i+13+h];future=bars[j+1:j+h+1];complete=len(path)==13+h and all(z['opened']-a['opened']==3600 for a,z in zip(path,path[1:])) and t+(12+h)*3600<=d['observation_end'];status='complete' if complete else 'gap' if t+(12+h)*3600<=d['observation_end'] else 'pending';check(status,e[f'status_{h}h'])
                f=fail(future) if complete else None;check(f is not None if complete else None,e[f'rebreak_{h}h'])
                for metric,value in [('return',(future[-1]['c']/c[j]-1)*100 if complete else None),('mfe',max(0,(max(r['h'] for r in future)/c[j]-1)*100) if complete else None),('mae',min(0,(min(r['l'] for r in future)/c[j]-1)*100) if complete else None)]:check(value,e[f'{metric}_{h}h_pct'])
                check(any(r['c']>=L+2*A for r in future) if complete else None,e[f'rebound_{h}h'])
                reclaim=any(future[k-1]['c']>L+b and future[k]['c']>L+b for k in range(f+2,len(future))) if complete and f is not None else False if complete else None;check(reclaim,e[f'reclaim_{h}h'])
            checked+=1
    for section in ('items','incremental','strata'):
        for r in d[section]:
            pool=pool_for(d,r)
            check(len(pool),r['pool_n']);check(sum(e[r['method']] is None for e in pool),r['unavailable_n'])
            for name,flag in [('yes',True),('no',False)]:
                group=[e for e in pool if e[r['method']] is flag];check(len(group),r[name+'_n'])
                for metric,field in [('wait','wait_change_pct'),('low_distance','low_distance_pct')]:check(statistics.median([e[field] for e in group]) if group else None,r[f'{name}_{metric}_median_pct'])
                for h in (24,72,168):
                    fail=sum(e[f'rebreak_{h}h'] for e in group);check(fail,r[f'{name}_failures_{h}h']);check(fail/len(group)*100 if group else None,r[f'{name}_failure_{h}h_pct'])
                    for metric in ('return','mfe','mae'):check(statistics.median([e[f'{metric}_{h}h_pct'] for e in group]) if group else None,r[f'{name}_{metric}_{h}h_median_pct'])
                    check(sum(e[f'rebound_{h}h'] for e in group),r[f'{name}_rebound_{h}h_n'])
    print(f'PASS: input hash, {len(d["raw_event_ledger"])} candidates, {checked} retained observations, raw features, flags, paths and all summary/incremental/stratum counts, rates and medians.')

def pool_for(d,r):
    pool=[e for e in d['observations'] if e['symbol']==r['symbol'] and e['eligible'] and e['status_168h']=='complete']
    if 'calendar_start' in r:pool=[e for e in pool if r['calendar_start']<=e['event_time'] and e['event_time']+180*3600<=r['calendar_end']]
    if 'base_condition' in r:pool=[e for e in pool if e[r['base_condition']] is True]
    if 'stratum' in r:
        lo,hi=map(float,r['range'][1:-1].split(','));pool=[e for e in pool if lo<=e[r['stratum']]<hi]
    return pool

def bootstrap(d):
    import numpy as np
    cache={};count=0
    for r in d['items']+d['incremental']:
        test=r['method'] if 'base_condition' in r else 'main';seed=int.from_bytes(hashlib.sha256(f'{d["calculation_version"]}|{r["symbol"]}|{r["period"]}|{test}|14'.encode()).digest()[:4],'big') or 1;check(seed,r['bootstrap_seed'])
        if min(r['yes_n'],r['no_n'])<20:check(None,r['difference_ci_low']);check(None,r['difference_ci_high']);continue
        pool=pool_for(d,r);key=(r['symbol'],r['period'],test)
        if key not in cache:
            days=math.ceil((r['calendar_end']-r['calendar_start'])/86400);di=[int((e['event_time']-r['calendar_start'])//86400) for e in pool];state=seed;weights=[]
            for _ in range(2000):
                picks=[]
                while len(picks)<days:
                    state^=(state<<13)&0xffffffff;state^=state>>17;state^=(state<<5)&0xffffffff;state&=0xffffffff;picks.extend((state%days+j)%days for j in range(14))
                counts=np.bincount(picks[:days],minlength=days);weights.append(counts[di])
            cache[key]=np.array(weights)
        w=cache[key];yes=np.array([e[r['method']] is True for e in pool]);no=np.array([e[r['method']] is False for e in pool]);f=np.array([e['rebreak_72h'] for e in pool],float)
        with np.errstate(divide='ignore',invalid='ignore'):diff=(w[:,yes]*f[yes]).sum(1)/w[:,yes].sum(1)*100-(w[:,no]*f[no]).sum(1)/w[:,no].sum(1)*100
        good=diff[np.isfinite(diff)];bounds=np.quantile(good,[.025,.975]).tolist() if len(good)>=1900 else [None,None];check(bounds[0],r['difference_ci_low']);check(bounds[1],r['difference_ci_high']);check(len(good),r['bootstrap_valid']);count+=1
    print(f'PASS: {count} primary72h difference intervals; all remaining sparse intervals correctly withheld.')

if __name__=='__main__':
    p=argparse.ArgumentParser(description=__doc__);p.add_argument('--release',required=True);p.add_argument('--inputs');p.add_argument('--bootstrap',action='store_true');a=p.parse_args();d=json.loads(read(a.release));verify(d,read(a.inputs or 'https://coinnudge.site'+d['inputs_url']))
    if a.bootstrap:bootstrap(d)
