"""Frozen, standard-library-only analysis. Run with INPUT_DIRECTORY OUTPUT.json. No network, credentials or production writes. Inputs: candles.json, derivatives.json. All rates named *_pct are percentage points; returns and other ratios are fractions. """ import json import math import random import statistics as st import sys from collections import defaultdict from datetime import datetime, timezone from pathlib import Path DAY = 86400 STEP = 900 COST = .002 SEED = 18092026 VERSION = 'three-decision-studies-1.0' def iso(t): return datetime.fromtimestamp(t, timezone.utc).isoformat().replace('+00:00', 'Z') def quantile(v, q): if not v: return None a = sorted(v); z = (len(a)-1)*q; i = int(z) return a[i] + (a[min(i+1, len(a)-1)]-a[i])*(z-i) def metrics(v): return dict(n=len(v), mean=st.mean(v) if v else None, median=st.median(v) if v else None, positive_n=sum(x > 0 for x in v), negative_n=sum(x < 0 for x in v), p10=quantile(v, .1), p90=quantile(v, .9)) def quarter(t): d = datetime.fromtimestamp(t, timezone.utc) return f'{d.year}-Q{(d.month-1)//3+1}' def paired_ci(rows, key): groups = defaultdict(list) for r in rows: groups[quarter(r['event_time'])].append(r['policies'][key]['return']-r['policies']['immediate']['return']) keys = sorted(groups) if len(rows) < 20 or len(keys) < 8: return None rng = random.Random(SEED); values = [] for _ in range(1000): draw = [v for k in rng.choices(keys, k=len(keys)) for v in groups[k]] values.append(st.mean(draw)) return [quantile(values, .025), quantile(values, .975)] def policy_summary(rows, keys, ci=False): out = {} for k in keys: v = [r['policies'][k]['return'] for r in rows] a = metrics(v) a.update(cash_n=sum(not r['policies'][k]['entered'] for r in rows), beats_immediate_n=sum(r['policies'][k]['return'] > r['policies']['immediate']['return']+1e-12 for r in rows), mean_difference=st.mean(r['policies'][k]['return']-r['policies']['immediate']['return'] for r in rows) if rows else None, mean_difference_ci95=paired_ci(rows, k) if ci else None) if rows and 'max_drawdown' in rows[0]['policies'][k]: a['median_max_drawdown'] = st.median(r['policies'][k]['max_drawdown'] for r in rows) out[k] = a return out def complete_grid(data, start, end, step=DAY): return all(t in data for t in range(start, end+step, step)) def dca_policy(data, start, count, horizon=365, cost=COST, points=False): cash=1.; units=0.; peak=1.; dd=0.; purchases=[]; path=[] end=start+horizon*DAY for offset in range(horizon+1): t=start+offset*DAY; b=data[t] if offset % 7 == 0 and offset//7 < count: spend=1/count; cash-=spend; bought=spend/(b['o']*(1+cost)); units+=bought purchases.append(dict(time=t, price=b['o'], spend=spend, units=bought)) mark=b['o'] if t==end else b['c'] value=cash+units*mark*(1-cost) peak=max(peak,value); dd=min(dd,value/peak-1) if points:path.append(dict(time=t, utc=iso(t), equity=value, price=mark, cash=max(0,cash))) return dict(entered=True, return_=value-1, max_drawdown=dd, purchases=purchases, path=path) def dca(candles): data=candles['BTCUSDT']; ledger=[]; rows=[] for t in sorted(data): if datetime.fromtimestamp(t,timezone.utc).day != 1:continue row=dict(id='BTC-'+iso(t)[:10],symbol='BTCUSDT',event_time=t,utc=iso(t),end_time=t+365*DAY) if not complete_grid(data,t,t+365*DAY): row['status']='pending' if t+365*DAY>max(data) else 'missing'; ledger.append(row);continue row.update(status='complete',policies={},horizon180={}) for k,n in [('immediate',1),('weekly4',4),('weekly12',12),('weekly26',26)]: p=dca_policy(data,t,n);p['return']=p.pop('return_');p.pop('path');row['policies'][k]=p p=dca_policy(data,t,n,180);p['return']=p.pop('return_');p.pop('path');row['horizon180'][k]=p rows.append(row);ledger.append(row) keys=['immediate','weekly4','weekly12','weekly26'] years={str(y):policy_summary([r for r in rows if iso(r['event_time']).startswith(str(y))],keys) for y in sorted({int(iso(r['event_time'])[:4]) for r in rows})} annual=[r for r in rows if iso(r['event_time'])[5:10]=='01-01'] return dict(candidates=ledger,observations=rows,summary=policy_summary(rows,keys),by_start_year=years, nonoverlapping_january_starts=policy_summary(annual,keys), horizon180=policy_summary([dict(r,policies=r['horizon180']) for r in rows],keys)) def wilder_rsi(data): ts=sorted(data); out={}; gain=loss=None for i,t in enumerate(ts): if i<14:continue if i==14: ds=[data[ts[j]]['c']-data[ts[j-1]]['c'] for j in range(1,15)] gain=sum(max(0,d) for d in ds)/14;loss=sum(max(0,-d) for d in ds)/14 else: change=data[t]['c']-data[ts[i-1]]['c'] gain=(gain*13+max(change,0))/14;loss=(loss*13+max(-change,0))/14 out[t]=100-100/(1+gain/loss) if loss else (100 if gain else 50) return out def buy_share(b): return b['taker_buy_qv']/b['qv'] if b.get('taker_buy_valid')==1 and b['qv']>0 else None def dip_entries(data,t): end=t+31*DAY entries={'immediate':t+DAY,'breakout':None,'buy_confirmation':None} for u in range(t+DAY,t+8*DAY,DAY): if not complete_grid(data,u-3*DAY,u):continue if data[u]['c']>max(data[u-i*DAY]['h'] for i in range(1,4)): if entries['breakout'] is None:entries['breakout']=u+DAY share=buy_share(data[u]) if entries['buy_confirmation'] is None and share is not None and share>=.5: entries['buy_confirmation']=u+DAY return entries,end def dip_policies(data,entries,end,cost=COST): return {k:dict(entered=t is not None,entry_time=t,entry_price=data[t]['o'] if t else None, return_=(data[end]['o']*(1-cost)/(data[t]['o']*(1+cost))-1) if t else 0.) for k,t in entries.items()} def dip(candles): candidates=[];rows=[] for symbol,data in candles.items(): ts=sorted(data);rsi=wilder_rsi(data);last=-10**20 for i,t in enumerate(ts): if i<200 or not complete_grid(data,t-200*DAY,t):continue close=data[t]['c']; prev=data[t-DAY]['c'] dd=close/max(data[t-j*DAY]['c'] for j in range(1,21))-1 prev_dd=prev/max(data[t-j*DAY]['c'] for j in range(2,22))-1 if not (dd<=-.10 and prev_dd>-.10):continue row=dict(id=symbol+'-'+iso(t)[:10],symbol=symbol,signal_time=t,event_time=t+DAY,utc=iso(t+DAY),drawdown=dd, sma200=st.mean(data[t-j*DAY]['c'] for j in range(200)),close=close,rsi14=rsi[t], volume_ratio=data[t]['qv']/st.median(data[t-j*DAY]['qv'] for j in range(1,21)),buy_share=buy_share(data[t])) row['above_sma200']=close>row['sma200'] if t-last<30*DAY:row['status']='cooldown';candidates.append(row);continue last=t if not complete_grid(data,t,t+31*DAY): row['status']='pending' if t+31*DAY>max(ts) else 'missing';candidates.append(row);continue entries,end=dip_entries(data,t); policies=dip_policies(data,entries,end) for p in policies.values():p['return']=p.pop('return_') row.update(status='complete',exit_time=end,exit_price=data[end]['o'],policies=policies) rows.append(row);candidates.append(row) rows.sort(key=lambda r:(r['event_time'],r['symbol']));keys=['immediate','breakout','buy_confirmation'] groups={ 'above_sma200':[r for r in rows if r['above_sma200']], 'below_or_equal_sma200':[r for r in rows if not r['above_sma200']], 'rsi_le30':[r for r in rows if r['rsi14']<=30], 'rsi_gt30':[r for r in rows if r['rsi14']>30], 'volume_ge2':[r for r in rows if r['volume_ratio']>=2], 'volume_lt2':[r for r in rows if r['volume_ratio']<2], 'trend_and_buying':[r for r in rows if r['above_sma200'] and r['buy_share'] is not None and r['buy_share']>=.5], 'btc_only':[r for r in rows if r['symbol']=='BTCUSDT']} sensitivity={} for cost in [.001,.005]: rerun=[] for row in rows: r=dict(row);entries={k:p['entry_time'] for k,p in r['policies'].items()} p=dip_policies(candles[r['symbol']],entries,r['exit_time'],cost) for x in p.values():x['return']=x.pop('return_') r['policies']=p;rerun.append(r) sensitivity[str(cost)]=policy_summary(rerun,keys) return dict(candidates=candidates,observations=rows,summary=policy_summary(rows,keys,True), groups={k:policy_summary(v,keys) for k,v in groups.items()},cost_sensitivity=sensitivity, by_symbol={s:policy_summary([r for r in rows if r['symbol']==s],keys) for s in candles}) def funding_group(rate,oi): if rate>=.01:return 'positive_rising' if oi>=.02 else 'positive_other' if rate<=-.01:return 'negative_rising' if oi>=.02 else 'negative_other' return 'ordinary_rising' if oi>=.02 else None def funding(inputs): series=defaultdict(dict) for r in inputs['rows']:series[r['contract']][r['ts']]=r ledger=[];rows=[];coverage=[] for contract,data in sorted(series.items()): last=-10**20;times=sorted(data) coverage.append(dict(contract=contract,n=len(times),start=iso(min(times)),end=iso(max(times)))) for t in times: r=data[t] if r['oi_usd']<1_000_000 or not complete_grid(data,t-3600,t,STEP):continue a=data[t-3600];q=r['oi_usd']/r['price'];q0=a['oi_usd']/a['price'];change=q/q0-1 group=funding_group(r['funding_hourly_pct'],change) if group is None:continue row=dict(id=contract+'-'+str(t),contract=contract,symbol=r['symbol'],group=group, event_time=t,decision_boundary=t+STEP,utc=iso(t+STEP), archived_observed_at=r['observed_at'],funding_hourly_pct=r['funding_hourly_pct'], oi_usd=r['oi_usd'],oi_quantity=q,prior_oi_quantity=q0,oi_change_1h=change, price=r['price'],price_change_1h=r['price']/a['price']-1,outcomes={}) if t-lastmax(times) else 'missing',return_=None) else: ref=data[entry]['price'];values=[data[u]['price']/ref-1 for u in range(entry,end+STEP,STEP)] row['outcomes'][str(hours)]=dict(status='complete',return_=values[-1],mfe=max(values),mae=min(values), reference_price=ref,end_price=data[end]['price'],end_time=end) p=row['outcomes'][str(hours)];p['return']=p.pop('return_') row['status']=row['outcomes']['24']['status'];ledger.append(row);rows.append(row) group_names=['positive_rising','positive_other','negative_rising','negative_other','ordinary_rising'] summary={} for group in group_names: subset=[r for r in rows if r['group']==group];s=dict(retained_n=len(subset),symbols=len({r['symbol'] for r in subset}),hours={}) for h in ['4','24']: complete=[r for r in subset if r['outcomes'][h]['status']=='complete'];ms=metrics([r['outcomes'][h]['return'] for r in complete]) ms.update(pending_n=sum(r['outcomes'][h]['status']=='pending' for r in subset),missing_n=sum(r['outcomes'][h]['status']=='missing' for r in subset), utc_days=len({iso(r['decision_boundary'])[:10] for r in complete}),median_mfe=st.median(r['outcomes'][h]['mfe'] for r in complete) if complete else None, median_mae=st.median(r['outcomes'][h]['mae'] for r in complete) if complete else None) s['hours'][h]=ms summary[group]=s return dict(candidates=ledger,observations=rows,summary=summary,coverage=coverage, start=iso(min(r['ts'] for r in inputs['rows'])),end=iso(max(r['ts'] for r in inputs['rows'])+STEP), rows=len(inputs['rows']),symbols=len(series),status='short-history exploratory pilot') def analyze(folder): folder=Path(folder);raw=json.loads((folder/'candles.json').read_text());deriv=json.loads((folder/'derivatives.json').read_text()) series={k:{int(t):v for t,v in data.items()} for k,data in raw['series'].items()} for data in series.values(): assert complete_grid(data,min(data),max(data)) for t,b in data.items(): assert t%DAY==0 and 0