"""Reproduce with Python stdlib: broad_altcoin_catchup.py inputs.json.gz output.json. Historical-directory-discovered Binance USDT markets; not a complete listing registry. """ import gzip import json import sys import statistics as st from collections import Counter, defaultdict from altcoin_catchup_study import DAY, iso, contiguous, outcomes, summarize, interval VERSION = 'altcoin-catchup-broad-2.0' def features(data, btc, t): changes=[data[t-i*DAY]['c']-data[t-(i+1)*DAY]['c'] for i in range(14)] up=st.mean(max(x,0) for x in changes); down=st.mean(max(-x,0) for x in changes) rsi=100*up/(up+down) if up+down else 50. sma=st.mean(data[t-i*DAY]['c'] for i in range(20)) median=st.median(data[t-i*DAY]['qv'] for i in range(1,21)) v=data[t]['qv']/median if median>0 else None relative=(data[t]['c']/btc[t]['c'])/(data[t-3*DAY]['c']/btc[t-3*DAY]['c'])-1 return dict(sma20=sma,above_sma20=data[t]['c']>sma,cutler_rsi14=rsi, volume_ratio20=v,relative_momentum3=relative, combined_confirmation=data[t]['c']>sma and v is not None and v>=1.5 and relative>0 and 50<=rsi<=70) def event_candidates(btc, threshold): last = None result = [] for t in sorted(btc): if not contiguous(btc, t-7*DAY, t): continue change = btc[t]['c']/btc[t-7*DAY]['c']-1 if change < threshold: continue r = dict(id=f'btc-{t}', signal_time=t, known_at=t+DAY, btc_return7=change) if last is not None and t-last < 30*DAY: r.update(status='excluded_cooldown', blocked_by=last) else: last=t; r['status']='retained' result.append(r) return result def eligibility(data, t): if t not in data: return 'unobserved_signal_day', None if not contiguous(data,t-29*DAY,t): return 'insufficient_prior30', None cohort = 'mature90' if contiguous(data,t-89*DAY,t) else 'short_history30to89' if data[t]['qv'] <= 0: return 'no_signal_day_turnover', cohort if st.median(data[t-i*DAY]['qv'] for i in range(30)) < 1e6: return 'below_liquidity_floor', cohort return 'eligible', cohort def future_status(data, btc, t, cutoff): if t+31*DAY >= cutoff: return 'pending_30d' for u in [t+DAY,t+8*DAY,t+15*DAY,t+31*DAY]: if u not in data or u not in btc: return 'missing_execution_bar' if data[u]['qv']<=0 or btc[u]['qv']<=0: return 'no_execution_day_turnover' if not contiguous(data,t+DAY,t+31*DAY) or not contiguous(btc,t+DAY,t+31*DAY): return 'missing_future_path' if any(data[u]['qv']<=0 for u in range(t+DAY,t+31*DAY,DAY)): return 'zero_turnover_future_path' return 'complete' def paired_baskets(rows, events): grouped=defaultdict(list) for r in rows: grouped[r['btc_event']].append(r) baskets=[] for e in events: if e['status']!='retained': continue a=[r for r in grouped[e['id']] if r['lagger']] b=[r for r in grouped[e['id']] if not r['lagger']] r=dict(btc_event=e['id'], event_time=e['known_at'], lagger_n=len(a), comparison_n=len(b)) if not a or not b: r['status']='unpaired' else: r.update(status='paired',outcomes={}) for h in ['7','14','30']: la=st.mean(x['outcomes'][h]['alt_net'] for x in a) co=st.mean(x['outcomes'][h]['alt_net'] for x in b) r['outcomes'][h]=dict(lagger_mean=la,comparison_mean=co,difference=la-co) baskets.append(r) paired=[r for r in baskets if r['status']=='paired']; summary={} for h in ['7','14','30']: rr=[dict(event_time=r['event_time'],delta=r['outcomes'][h]['difference']) for r in paired] summary[h]=dict(n=len(rr),positive_n=sum(r['delta']>0 for r in rr), mean_difference=st.mean(r['delta'] for r in rr) if rr else None,ci95=interval(rr,'delta')) return baskets,summary def bounds(rows): # Bounds only for eligible, matured laggards: unknown != zero or delisted loss. matured=[r for r in rows if r.get('lagger') and r['status']!='pending_30d'] complete=[r for r in matured if r['status']=='complete'] n=len(matured); known=sum(r['outcomes']['30']['recovered'] for r in complete) missing=n-len(complete) return dict(matured_eligible_laggers=n,complete=len(complete),unknown=missing,recovered=known, lower=known/n if n else None,upper=(known+missing)/n if n else None, pending=sum(r.get('lagger',False) and r['status']=='pending_30d' for r in rows)) def experiment(series, metadata, cutoff, threshold=.1, lag=.05, details=True): btc=series['BTCUSDT']; events=event_candidates(btc,threshold); ledger=[] for e in events: if e['status']!='retained': continue t=e['signal_time'] for symbol,data in sorted(series.items()): if symbol=='BTCUSDT': continue status,cohort=eligibility(data,t) r=dict(id=f'{symbol}-{t}',symbol=symbol,btc_event=e['id'],signal_time=t,event_time=t+DAY, btc_return7=e['btc_return7'],cohort=cohort,status=status, current_status=metadata[symbol]['current_status'],fee_per_side=.002) if status=='eligible': ar=data[t]['c']/data[t-7*DAY]['c']-1 r.update(alt_return7=ar,lag_pp=e['btc_return7']-ar,lagger=e['btc_return7']-ar>=lag, median_quote30=st.median(data[t-i*DAY]['qv'] for i in range(30)),**features(data,btc,t)) r['status']=future_status(data,btc,t,cutoff) if r['status']=='complete': r['outcomes']=outcomes(data,btc,t,.002) ledger.append(r) main=[r for r in ledger if r['cohort']=='mature90' and 'lagger' in r] short=[r for r in ledger if r['cohort']=='short_history30to89' and 'lagger' in r] complete=[r for r in main if r['status']=='complete']; lags=[r for r in complete if r['lagger']] others=[r for r in complete if not r['lagger']]; baskets,pairs=paired_baskets(complete,events) result=dict(parameters=dict(btc_return7_min=threshold,lag_pp_min=lag,fee_per_side=.002), status_counts=dict(Counter(r['status'] for r in ledger)),laggers=summarize(lags,True), comparison=summarize(others),paired_baskets=pairs,bounds=bounds(main), short_history=dict(summary=summarize([r for r in short if r['status']=='complete' and r['lagger']]),bounds=bounds(short)), markets_with_eligible_main=len({r['symbol'] for r in main}), markets_with_complete_laggers=len({r['symbol'] for r in lags})) if details: groups=defaultdict(list) for r in lags: y=int(iso(r['event_time'])[:4]) for key in ['symbol:'+r['symbol'], 'current:'+('TRADING' if r['current_status']=='TRADING' else 'not_TRADING_or_absent'), 'period:'+('2020–2022' if y<=2022 else '2023–2024' if y<=2024 else '2025–cutoff'), 'sma20:'+str(r['above_sma20']),'volume:'+str(r['volume_ratio20']>=1.5 if r['volume_ratio20'] is not None else 'missing'), 'relative3:'+str(r['relative_momentum3']>0),'rsi50to70:'+str(50<=r['cutler_rsi14']<=70), 'combined:'+str(r['combined_confirmation'])]: groups[key].append(r) result.update(events=events,ledger=ledger,baskets=baskets,groups={k:summarize(v) for k,v in groups.items()}) return result def run(raw): series={s:{int(t):r for t,r in d.items()} for s,d in raw['series'].items()} metadata={r['symbol']:r for r in raw['universe']['records']} main=experiment(series,metadata,raw['cutoff']) sensitivity={name:experiment(series,metadata,raw['cutoff'],b,l,False) for name,b,l in [('btc5',.05,.05),('btc15',.15,.05),('lag2.5',.10,.025),('lag10',.10,.10)]} lags=[r for r in main['ledger'] if r['cohort']=='mature90' and r.get('lagger') and r['status']=='complete'] costs={str(f):summarize([dict(r,outcomes=outcomes(series[r['symbol']],series['BTCUSDT'],r['signal_time'],f)) for r in lags]) for f in [.001,.003,.005]} return dict(version=VERSION,cutoff=iso(raw['cutoff']),observed_at=raw['observed_at'], input_rows=sum(len(d) for d in series.values()),symbols=sorted(series), temporal_coverage=min(iso(t) for d in series.values() for t in d)+'/'+iso(raw['cutoff']), universe=raw['universe'],main=main,threshold_sensitivity=sensitivity,cost_sensitivity=costs) if __name__=='__main__': with gzip.open(sys.argv[1],'rt') as f: raw=json.load(f) with open(sys.argv[2],'w') as f: json.dump(run(raw),f,ensure_ascii=False,allow_nan=False,separators=(',',':'))