from pathlib import Path from datetime import datetime from zoneinfo import ZoneInfo import json,hashlib import numpy as np import pandas as pd OUT=Path(__file__).resolve().parent def read(p):return json.loads(p.read_text(encoding='utf-8-sig')) def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest() cfg=read(OUT/'价格对照实验设定.json');risk=read(OUT/'价格与组合风险诊断-r2.json') src=Path(risk['source_path']);supp=Path(risk['supplement_path']) assert sha(src)==risk['source_sha256'] and sha(supp)==risk['supplement_sha256'] cols=['SPY','IEF','GLD','BIL','XLK','XLI','XLE','XLV','XLP','XLU'];universe=cols[4:] p=pd.read_csv(src).pivot(index='date',columns='symbol',values='adjusted_close').sort_index().loc['2023-10-30':'2026-11-04'] new=pd.read_csv(supp).pivot(index='date',columns='symbol',values='adjusted_close') for s in ['XLV','XLP']:p[s]=new[s] p=p[cols];assert len(p)==757 and p.notna().all().all() and np.isfinite(p.values).all() end_month=[i for i in range(len(p)-1) if p.index[i][:7]!=p.index[i+1][:7] and p.index[i]>='2024-12-01'] assert p.index[end_month[0]]=='2024-12-31' selection_log=[];targets={};selected=None for i in end_month: if p.index[i][5:7]=='12': moment=(p.iloc[i-21][universe]/p.iloc[i-252][universe]-1).to_dict() selected=sorted(universe,key=lambda s:(-moment[s],s))[:2] selection_log.append({'signal':p.index[i],'execution':p.index[i+1],'ranking':sorted(moment.items(),key=lambda x:(-x[1],x[0])),'selected':selected,'latest_price_used':p.index[i-21],'lookback_start':p.index[i-252]}) assert selected is not None basic={'SPY':.30,'IEF':.20,'GLD':.25,'BIL':.25} target={'base_AI8_static':basic,'add_SPY_10pct':basic|{'SPY':.40,'BIL':.15}, 'equal_sector_10pct':basic|{'BIL':.15,**{s:.10/6 for s in universe}}, 'annual_price_top2_10pct':basic|{'BIL':.15,**{s:.05 for s in selected}}} for w in target.values():assert abs(sum(w.values())-1)<1e-12 targets[i+1]={'signal':p.index[i],'weights':target} assert len(selection_log)==2 def net_trade_nav(holdings,cash,w,c): nav=float(holdings.sum()+cash);low,high=nav/(1+2*c),nav for _ in range(60): mid=(low+high)/2 if mid+c*np.abs(mid*w-holdings).sum()>nav:high=mid else:low=mid after=(low+high)/2;notional=float(np.abs(after*w-holdings).sum());fee=notional*c assert abs(after+fee-nav)<1e-7 return after,notional,fee # Independent closed-form cases: initial purchase and a 100/25 portfolio rebalanced 50/50. assert abs(net_trade_nav(np.array([0.,0.]),100.,np.array([.5,.5]),.0005)[0]-100/1.0005)<1e-10 assert abs(net_trade_nav(np.array([100.,25.]),0.,np.array([.5,.5]),.001)[0]-124.925)<1e-10 # Future prices must not change the annual selection made at the signal date. for log in selection_log: i=p.index.get_loc(log['signal']);perturbed=p.copy();perturbed.iloc[i+1:]*=7 score=perturbed.iloc[i-21][universe]/perturbed.iloc[i-252][universe]-1 assert sorted(universe,key=lambda s:(-score[s],s))[:2]==log['selected'] start=min(targets);results=[];curves={};trades=[] for bps in cfg['costs_one_way_bps']: c=bps/10000 for strategy in list(next(iter(targets.values()))['weights']): holdings=np.zeros(len(cols));cash=100000.;navs=[cash];fees=0.;turnover=0.;orders=[] for i in range(start,len(p)): if i>start:holdings*=p.iloc[i].values/p.iloc[i-1].values nav=float(holdings.sum()+cash) if i in targets: signal=targets[i]['signal'];assert signal=0) holdings=after*w;cash=0.;fees+=fee;turnover+=notional/nav orders.append({'signal':signal,'execution':p.index[i],'fee':fee,'traded_notional':notional,'weights':dict(zip(cols,w.tolist()))}) navs.append(float(holdings.sum()+cash)) arr=np.array(navs);dd=1-arr/np.maximum.accumulate(arr);rets=arr[1:]/arr[:-1]-1 years=(pd.Timestamp(p.index[-1])-pd.Timestamp(p.index[start])).days/365.25 record={'strategy':strategy,'one_way_bps':bps,'first_trade':p.index[start],'last_valuation':p.index[-1],'sessions':len(arr)-1,'annual_decisions':len(selection_log) if strategy=='annual_price_top2_10pct' else 0,'cumulative_return':arr[-1]/100000-1,'short_sample_cagr':(arr[-1]/100000)**(1/years)-1,'max_drawdown':float(dd.max()),'realized_volatility':float(np.std(rets[1:],ddof=1)*np.sqrt(252)),'fees_USD':fees,'summed_two_way_traded_notional_over_nav':turnover,'rebalance_events':len(orders)} results.append(record);curves[strategy+'_'+str(bps)]={'seed_cash':100000,'dates':p.index[start:].tolist(),'nav':arr[1:].tolist()} if bps==5:trades.append({'strategy':strategy,'orders':orders}) # Check event alignment and equal-equity comparator risk budgets, without reading future prices in selection. assert all(sum(o['weights'][s] for s in ['SPY']+universe)<=.40000000001 for t in trades for o in t['orders']) for strategy in set(r['strategy'] for r in results): seq=[r for r in results if r['strategy']==strategy] assert seq[0]['cumulative_return']>=seq[1]['cumulative_return']>=seq[2]['cumulative_return'] out={'created_at':datetime.now(ZoneInfo('Asia/Taipei')).isoformat(),'experiment':cfg,'settings_sha256':sha(OUT/'价格对照实验设定.json'),'source_sha256':sha(src),'supplement_sha256':sha(supp),'selection_log':selection_log,'results':results,'AI_strategy_performance':None,'account_action':False,'verification':{'annual_signal_count':2,'signal_strictly_before_trade':True,'budget_and_cash_checks':True,'cost_monotonicity':True,'closed_form_fee_tests':True,'future_price_perturbation_invariant':True,'parameter_search':False}} (OUT/'价格对照实验结果.json').write_text(json.dumps(out,ensure_ascii=False,indent=2),encoding='utf-8') (OUT/'价格对照实验路径.json').write_text(json.dumps({'curves':curves,'trades_5bps':trades},ensure_ascii=False,indent=2),encoding='utf-8') print(json.dumps({'selection':selection_log,'base_cost_results':[r for r in results if r['one_way_bps']==5]},ensure_ascii=False,indent=2))