"""python catchup_confirmation.py inputs.json.gz research-v2.json extension.json Retrospective confirmation policy comparison; frozen v2 complete mature cohort. """ import gzip,json,sys,statistics as st from collections import Counter,defaultdict from altcoin_catchup_study import DAY,interval,iso from broad_altcoin_catchup import features VERSION='altcoin-catchup-confirmation-3.0' POLICIES=['immediate','relative_trend','breakout_volume','btc'] def candidate(data,btc,t): f=features(data,btc,t) f['breakout20']=data[t]['c']>max(data[t-i*DAY]['c'] for i in range(1,21)) f['relative_trend']=f['relative_momentum3']>0 and f['above_sma20'] f['breakout_volume']=f['relative_trend'] and f['breakout20'] and f['volume_ratio20'] is not None and f['volume_ratio20']>=1.5 return f def policy(data,btc,t,end,kind,scan): signal=t if kind in ('immediate','btc') else next((x['time'] for x in scan if x[kind]),None) if signal is None: return dict(status='cash_no_confirmation',signal_time=None,entry_time=None,exit_time=end,net=0., wait_days=None,entry_move_from_initial=None,mae_net=0.,mfe_net=0.,entry_price=None,exit_price=None) entry=signal+DAY;d=btc if kind=='btc' else data;price=d[entry]['o'];factor=.998/1.002 lows=[d[u]['l'] for u in range(entry,end,DAY)]+[d[end]['o']] highs=[d[u]['h'] for u in range(entry,end,DAY)]+[d[end]['o']] return dict(status='entered',signal_time=signal,entry_time=entry,exit_time=end, entry_price=price,exit_price=d[end]['o'],net=d[end]['o']/price*factor-1, wait_days=(entry-t-DAY)//DAY,entry_move_from_initial=price/d[t+DAY]['o']-1, mae_net=min(lows)/price*factor-1,mfe_net=max(highs)/price*factor-1) def path_type(touched,recovered): return 'held' if touched and recovered else 'faded' if touched else 'exit_only' if recovered else 'never' def summary(rows): n=len(rows);result={} for kind in POLICIES: entered=[x['policies'][kind] for x in rows if x['policies'][kind]['status']=='entered'] by=defaultdict(list) for row in rows: by[row['event_time']].append(row) ev=[dict(event_time=t,net=st.mean(x['policies'][kind]['net'] for x in rr), vs_immediate=st.mean(x['policies'][kind]['net']-x['policies']['immediate']['net'] for x in rr), vs_btc=st.mean(x['policies'][kind]['net']-x['policies']['btc']['net'] for x in rr)) for t,rr in sorted(by.items())] result[kind]=dict(n=n,entered=len(entered),cash=n-len(entered),events=len(by), positive=sum(x['policies'][kind]['net']>0 for x in rows), median_net=st.median(x['policies'][kind]['net'] for x in rows) if n else None, median_wait=st.median(x['wait_days'] for x in entered) if entered else None, median_entry_move=st.median(x['entry_move_from_initial'] for x in entered) if entered else None, median_entered_mae=st.median(x['mae_net'] for x in entered) if entered else None, median_entered_mfe=st.median(x['mfe_net'] for x in entered) if entered else None, event_mean={k:st.mean(x[k] for x in ev) if ev else None for k in ['net','vs_immediate','vs_btc']}, ci95={k:interval(ev,k) for k in ['net','vs_immediate','vs_btc']}) return result def run(raw,original): series={s:{int(t):v for t,v in d.items()} for s,d in raw['series'].items()};btc=series['BTCUSDT'] source=[r for r in original['main']['ledger'] if r['cohort']=='mature90' and r.get('lagger') and r['status']=='complete'] rows=[] for r in source: t=r['signal_time'];end=t+31*DAY;d=series[r['symbol']] baseline=d[t-7*DAY]['c']/btc[t-7*DAY]['c'] ratios=[(u,d[u]['c']/btc[u]['c']) for u in range(t+DAY,end,DAY)] touches=[u for u,v in ratios if v>=baseline];first=touches[0] if touches else None recovered=d[end]['o']/btc[end]['o']>=baseline scan=[dict(time=u,known_at=u+DAY,**candidate(d,btc,u)) for u in range(t,t+7*DAY,DAY)] prior_relative=(d[t-7*DAY]['c']/btc[t-7*DAY]['c'])/(d[t-37*DAY]['c']/btc[t-37*DAY]['c'])-1 followup={} for horizon in [3,7]: available=first is not None and first+horizon*DAY0,above_sma20=r['above_sma20'],rsi=r['cutler_rsi14'], lag_pp=r['lag_pp'],type=path_type(bool(touches),recovered),recovered=recovered, first_recovery_day=first,first_recovery_known_at=first+DAY if first is not None else None, days_to_first_recovery=(first-t)//DAY if first is not None else None, followup=followup,scan=scan,policies={k:policy(d,btc,t,end,k,scan) for k in POLICIES}, excess30=r['outcomes']['30']['excess_net'],original_observation=r)) groups={} for strength in [False,True]: for trend in [False,True]: rr=[r for r in rows if r['prior_strength']==strength and r['above_sma20']==trend] groups[f'prior_{strength}_trend_{trend}']=dict(n=len(rr),events=len({r['event_time'] for r in rr}), recovered=sum(r['recovered'] for r in rr),summary=summary(rr)) selected=[];used=set() for kind in ['held','faded','never']: pool=[r for r in rows if r['type']==kind];median=st.median(r['excess30'] for r in pool) count=0 for r in sorted(pool,key=lambda r:(abs(r['excess30']-median),r['id'])): if r['symbol'] in used:continue selected.append(r['id']);used.add(r['symbol']);count+=1 if count==6:break assert count==6,'Insufficient distinct case symbols' followup={} for h in ['3','7']: rr=[r for r in rows if r['followup'][h]['complete']] followup[h]=dict(n=len(rr),failed=sum(r['followup'][h]['failed'] for r in rr), touched_without_full_followup=sum(r['first_recovery_day'] is not None and not r['followup'][h]['complete'] for r in rows)) return dict(version=VERSION,source_version=original['version'],source_observed_at=raw['observed_at'], parameters=dict(confirmation_scan_days=7,common_exit_day=31,fee_per_side=.002,phase_boundary='2024-01-01T00:00:00Z', phase_claim='chronological robustness; later data not genuinely untouched',selection='held,faded,never; six distinct symbols each; distance to within-type median excess30 then ID; global symbol deduplication'), counts=dict(Counter(r['type'] for r in rows)),followup=followup, summary=summary(rows),phases={p:summary([r for r in rows if r['phase']==p]) for p in ['2020–2023','2024–cutoff']}, background=groups,cases=selected,rows=rows) if __name__=='__main__': with gzip.open(sys.argv[1],'rt') as f:raw=json.load(f) with open(sys.argv[2]) as f:original=json.load(f) with open(sys.argv[3],'w') as f:json.dump(run(raw,original),f,ensure_ascii=False,separators=(',',':'),allow_nan=False)