"""R627:共同股票篮子,滞后波动率,真实现金/碎股/比例费用与权重漂移。""" from copy import deepcopy from math import isclose,isfinite from statistics import stdev RETURNS={'A':[.01,-.01,.02,.01,-.02,.03,.01,.01], 'B':[.02,-.02,.04,-.01,.03,-.01,.02,-.02], 'C':[0,0,0,0,0,0,0,0]} def targets(history,method): if method not in {'equal','inverse'}:raise ValueError('unknown weighting method') eligible={s:stdev(r) for s,r in history.items() if len(r)==3 and all(x is not None and isfinite(x) for x in r) and stdev(r)>1e-12} if not eligible:return {} raw={s:1 if method=='equal' else 1/vol for s,vol in eligible.items()} total=sum(raw.values()) return {s:w/total for s,w in raw.items()} def run(data,method): prices={s:[100.] for s in data} for s,series in data.items(): for r in series:prices[s].append(prices[s][-1]*(1+r)) cash=1000.;shares={s:0. for s in data};nav=[cash];ledger=[];total_cost=0.;traded=0. for t in range(3,8): open_prices={s:prices[s][t] for s in data} before=cash+sum(shares[s]*open_prices[s] for s in data) if t in [3,5,7]: history={s:data[s][t-3:t] for s in data} weights=targets(history,method) old={s:shares[s]*open_prices[s] for s in data} lo,hi=0.,before for _ in range(100): mid=(lo+hi)/2 amount=sum(abs(mid*weights.get(s,0)-old[s]) for s in data) if mid+.001*amount>before:hi=mid else:lo=mid invest=(lo+hi)/2 if weights else 0. target={s:invest*weights.get(s,0)/open_prices[s] for s in data} fills={s:target[s]-shares[s] for s in data} notional=sum(abs(fills[s])*open_prices[s] for s in data);cost=.001*notional cash=before-sum(target[s]*open_prices[s] for s in data)-cost assert cash>=-1e-8 shares=target;total_cost+=cost;traded+=notional assert isclose(cash+sum(shares[s]*open_prices[s] for s in data),before-cost,rel_tol=0,abs_tol=1e-8) assert not weights or isclose(sum(weights.values()),1,rel_tol=0,abs_tol=1e-12) ledger.append({'session':t,'weights':weights,'fills':fills,'cost':cost, 'cash':cash,'shares':dict(shares),'pretrade_equity':before}) ending=cash+sum(shares[s]*prices[s][t+1] for s in data) drift={s:shares[s]*prices[s][t+1]/ending for s in data} ledger[-1]['end_weights']=drift nav.append(ending) peak=nav[0];drawdown=0 for value in nav: peak=max(peak,value);drawdown=min(drawdown,value/peak-1) return {'ending':nav[-1],'net_return':nav[-1]/nav[0]-1,'cost':total_cost, 'turnover':traded/nav[0],'max_drawdown':drawdown,'ledger':ledger,'nav':nav} ew=run(RETURNS,'equal');iv=run(RETURNS,'inverse') assert ew['ledger'][0]['weights']=={'A':.5,'B':.5} assert isclose(iv['ledger'][0]['weights']['A'],2/3,rel_tol=0,abs_tol=1e-12) assert 'C' not in ew['ledger'][0]['weights'] and 'C' not in iv['ledger'][0]['weights'] assert targets({'Z':[0,0,0],'M':[.01,.02]},'equal')=={} assert targets({'Z':[0,0,0],'M':[.01,.02]},'inverse')=={} assert targets({'M':[None,.01,.02]},'equal')==targets({'M':[None,.01,.02]},'inverse')=={} cash_only=run({s:[0]*8 for s in RETURNS},'inverse') assert cash_only['ending']==1000 and cash_only['cost']==0 future=deepcopy(RETURNS);future['A'][7]=.5 assert run(future,'inverse')['ledger'][0]['weights']==iv['ledger'][0]['weights'] assert ew['cost']>0 and iv['cost']>0 assert isclose(ew['ending'],1023.715184,rel_tol=0,abs_tol=1e-6) assert isclose(iv['ending'],1018.583882,rel_tol=0,abs_tol=1e-6) assert not isclose(ew['ledger'][0]['end_weights']['A'],.5,rel_tol=0,abs_tol=1e-6) for label,result in [('equal',ew),('inverse',iv)]: print(label+' ending='+format(result['ending'],'.6f')+' net='+format(result['net_return'],'.6%') +' cost='+format(result['cost'],'.6f')+' turnover='+format(result['turnover'],'.6f') +' max_drawdown='+format(result['max_drawdown'],'.6%')) print(label+' first_weights='+str(result['ledger'][0]['weights'])+' rebalance_sessions=[3, 5, 7]') for row in result['ledger']: print(label+' session='+str(row['session'])+' target='+str({k:round(v,6) for k,v in row['weights'].items()}) +' fills='+str({k:round(v,6) for k,v in row['fills'].items()}) +' end_weights='+str({k:round(v,6) for k,v in row['end_weights'].items()}) +' cost='+format(row['cost'],'.6f')+' cash='+format(row['cash'],'.12f')) print('zero_vol_and_short_history=common_exclusion future_change_first_weights=unchanged self_financing=True')