"""C09/C11:债券合同假设与构造重放实验,非框架或真实市场回测。""" import json,math,statistics,sys from datetime import date from pathlib import Path def solve_price(price,flows): lo,hi=-.9,2. def value(r):return sum(cf/(1+r)**t for t,cf in flows) assert value(lo)>price>value(hi) for _ in range(150): mid=(lo+hi)/2 if value(mid)>price:lo=mid else:hi=mid y=(lo+hi)/2 assert math.isclose(value(y),price,abs_tol=1e-9) return y def stats(values,factor): mean=statistics.mean(values);sd=statistics.stdev(values) return {'count':len(values),'mean':mean,'sample_sd':sd,'annual_factor':factor,'sharpe_zero_rf':mean/sd*math.sqrt(factor) if sd else None} def drawdown(values): peak=values[0];rows=[] for v in values: peak=max(peak,v);rows.append(1-v/peak) return {'rows':rows,'maximum':max(rows)} def d30360(a,b):return (b.year-a.year)*360+(b.month-a.month)*30+min(b.day,30)-min(a.day,30) def calculate(): o={};face=10000;clean=100;ai=100*.036*90/365;fee=5 o['DJ081']={'face':face,'clean_per100':clean,'coupon_rate':.036,'days':90,'basis':365,'accrued_per100':ai,'clean_amount':face/100*clean,'accrued_amount':face/100*ai,'dirty_amount':face/100*(clean+ai),'fee':fee,'debit':face/100*(clean+ai)+fee} annual=[(1,4),(2,104)];semi=[(.5,2),(1,2),(1.5,2),(2,102)];a=solve_price(98,annual);s=solve_price(98,semi) o['DJ082']={'price':98,'annual_flows':annual,'semi_flows':semi,'annual_effective':a,'semi_effective':s,'semi_nominal':2*((1+s)**.5-1),'semi_price_at_annual_effective':sum(cf/(1+a)**t for t,cf in semi)} prev=date(2026,1,15);settle=date(2026,3,1);nextpay=date(2027,1,15);rows=[] for label,elapsed,remain,basis in [('ACT/365F',(settle-prev).days,(nextpay-settle).days,365),('30E/360',d30360(prev,settle),d30360(settle,nextpay),360)]: ai=4*elapsed/basis;dirty=100+ai;t=remain/basis;y=(104/dirty)**(1/t)-1 rows.append([label,elapsed,remain,basis,ai,dirty,t,y]) o['DJ083']={'previous_coupon':str(prev),'settlement':str(settle),'next_coupon':str(nextpay),'rows':rows} face=10000;clean=101.2;coupon=3;before=face/100*(clean+coupon);after=face/100*clean;received=face/100*coupon o['DJ084']={'face':face,'clean':clean,'coupon_per100':coupon,'before_dirty':before,'after_dirty':after,'cash_coupon':received,'after_total':after+received,'lower_clean_total':face/100*100.7+received} flows=[(t,3+(100 if t==5 else 0)) for t in range(1,6)];y=.04;price=sum(cf/(1+y)**t for t,cf in flows);duration=sum(t*cf/(1+y)**t for t,cf in flows)/price/(1+y);rows=[] for bp in [1,10,100,-100]: delta=bp/10000;approx=price*(1-duration*delta);exact=sum(cf/(1+y+delta)**t for t,cf in flows);rows.append([bp,delta,approx,exact,(approx-exact)/price]) o['DJ085']={'flows':flows,'base_yield':y,'base_price':price,'modified_duration':duration,'rows':rows} rows=[] for name,n,redemption in [('到期',5,100),('第2年赎回',2,101),('第3年赎回',3,100.5)]: flows=[(t,5+(redemption if t==n else 0)) for t in range(1,n+1)];rows.append({'name':name,'flows':flows,'redemption':redemption,'yield':solve_price(104,flows)}) o['DJ086']={'price':104,'rows':rows} p=100000;r1=.02;interest1=p*r1*3;rows=[] for r2 in [.01,.03]: for rollover in [False,True]: principal2=p+interest1 if rollover else p;interest2=principal2*r2*3;end=principal2+interest2+(0 if rollover else interest1) rows.append([r2,rollover,interest1,principal2,interest2,end]) o['DJ087']={'first_rate':r1,'rows':rows} a=date(2026,3,2);b=date(2026,4,2);d=(b-a).days;unit=100000*.018/365 o['DJ088']={'start':str(a),'end':str(b),'days':d,'daily_interest':unit,'rows':[[label,days,days*unit] for label,days in [('含起不含止',d),('首尾都含',d+1),('首尾都不含',d-1)]]} gross=100000*.03*3/365;fee=1;net=gross-fee o['DJ089']={'principal':100000,'quoted_rate':.03,'interest_days':3,'gross':gross,'fee':fee,'net':net,'same_use_3d':net/100000*365/3,'usable_4d':net/100000*365/4,'withdrawable_5d':net/100000*365/5,'timeline':[['T0','交易并冻结'],['T1','首次交收,计息起点'],['T2','构造休市,不可使用'],['T3','构造休市,不可使用'],['T4','到期交收/可用,停止计息'],['T5','按本练习账户假设可取']]} d=(date(2026,7,1)-date(2026,1,1)).days;withdraw=30000;remaining=70000;early=withdraw*.001*d/365;mature=remaining*.02 o['DJ090']={'start':'2026-01-01','partial':'2026-07-01','maturity':'2027-01-01','early_days':d,'early_principal':withdraw,'remaining_principal':remaining,'early_rate':.001,'contract_rate':.02,'early_interest':early,'remaining_interest':mature,'total_interest':early+mature,'no_withdraw_interest':100000*.02,'wrong_remaining_also_low':100000*.001*d/365} prices=[1,1.1,1.1,1.21];units=100;cash=0;rows=[] for k,p in enumerate(prices): deposit=110 if k==2 else 0 if deposit:units+=deposit/p rows.append([k,p,deposit,units,units*p, p/prices[0]]) o['DJ101']={'rows':rows,'amount_change':rows[-1][4]/100-1,'unit_return':prices[-1]/prices[0]-1,'profit':rows[-1][4]-210} n=100;cash=2000;last=8;rows=[] for label,residual in [('零残值',0),('每股2元',2),('按8元冻结待确认',None)]: proceeds=None if residual is None else n*residual;endcash=cash+(proceeds or 0);pending=n*last if residual is None else 0 rows.append({'scenario':label,'shares_after':n if residual is None else 0,'cash_after':endcash,'pending_mark':pending,'total_mark':endcash+pending,'recognized_loss_vs_last':None if residual is None else n*(residual-last)}) o['DJ102']={'prior_cash':cash,'shares':n,'last_quote':last,'prior_total':cash+n*last,'rows':rows} close=100;open_next=104;exit=106;n=100 o['DJ103']={'signal_close':close,'next_open':open_next,'exit':exit,'shares':n,'same_close_cost':n*close,'next_open_cost':n*open_next,'same_close_profit':n*(exit-close),'next_open_profit':n*(exit-open_next),'assumed_events':[['14:59:59','最终收盘价未知','不能使用最终价算信号'],['15:00:00','收盘事件','仅为构造事件时间'],['15:00:01','完整数据到达','此后才可计算'],['15:00:02','信号形成','此后才发订单'],['次日09:30:00','下一个假设成交机会','104元不保证实际可成交']]} cash=10000;price=37;weight=.6;target=cash*weight;continuous=target/price;lot=100;integer=math.floor(continuous/lot)*lot;newprice=40 o['DJ104']={'cash':cash,'price':price,'target_weight':weight,'lot':lot,'rows':[['连续股份',continuous,continuous*price,cash-continuous*price,(cash-continuous*price)+continuous*newprice],['整100股',integer,integer*price,cash-integer*price,cash-integer*price+integer*newprice]],'lot_one_shares':math.floor(continuous),'small_target_shares':math.floor((cash*.003/price)/lot)*lot} bars=[['09:30',10],['09:31',11],['09:31',11],['09:32',12]] def replay(data): cash=1000;shares=0;logs=[] for ts,p in data: if p>=11:shares+=10;cash-=10*p;logs.append([ts,p,10,cash,shares]) return {'orders':logs,'count':len(logs),'cash':cash,'shares':shares,'final':cash+shares*12} seen=set();unique=[] for row in bars: if row[0] not in seen:unique.append(row);seen.add(row[0]) o['DJ105']={'bars':bars,'raw':replay(bars),'dedup':replay(unique),'conflicting_duplicate':['09:31',11.2]} daily=[.01,-.004,.006,-.002,.003,-.008,.002,.001,.004,-.001,.005,.003,-.007,.002,.004,-.003,.006,-.002,.001,.005];weekly=[math.prod(1+r for r in daily[k:k+5])-1 for k in range(0,20,5)] o['DJ106']={'daily':daily,'weekly':weekly,'daily_stats':stats(daily,252),'weekly_stats':stats(weekly,52),'same_total_daily':math.prod(1+r for r in daily)-1,'same_total_weekly':math.prod(1+r for r in weekly)-1} prices=[1,1.2,.9,.8,1.1];units=100;rows=[] for k,p in enumerate(prices): dep=120 if k==2 else 0 if dep:units+=dep/p rows.append([k,p,dep,units,units*p]) amount=[r[4] for r in rows] o['DJ107']={'rows':rows,'amount_drawdown':drawdown(amount),'unit_drawdown':drawdown(prices),'no_deposit_amount':drawdown([100*p for p in prices])} prices=[100,102,101,103,103,104];returns=[prices[k]/prices[k-1]-1 for k in range(1,len(prices))];traded=[True,False,True,False,True];selected=[r for r,t in zip(returns,traded) if t];missing=[100,102,None,103,103,104];carried=[100,102,102,103,103,104];filled=[carried[k]/carried[k-1]-1 for k in range(1,6)] o['DJ108']={'prices':prices,'returns':returns,'traded':traded,'all_days':stats(returns,252),'trade_days_only':stats(selected,252),'missing_prices':missing,'carried_prices':carried,'filled_returns':filled,'filled_stats':stats(filled,252),'true_unknown_intervals':[2,3]} returns=[.01,-.02,.03,.005,-.01,.02,.01,-.005];windows=[list(range(1,5)),list(range(3,7)),list(range(5,9))];naive=[returns[d-1] for w in windows for d in w];unique=[returns[d-1] for d in range(1,9)];ownership=[] for d in range(1,9): owners=[i+1 for i,w in enumerate(windows) if d in w];ownership.append([d,','.join(map(str,owners)),max(owners),returns[d-1]]) o['DJ109']={'windows':windows,'returns':returns,'ownership':ownership,'naive_count':len(naive),'unique_count':len(unique),'naive_return':math.prod(1+r for r in naive)-1,'unique_return':math.prod(1+r for r in unique)-1,'duplicate_days':[d for d in range(1,9) if sum(d in w for w in windows)>1]} q=10;buy=20;sell=22;fxbuy=7;fxsell=7.2;feebuy=1;feesell=1.5;buyfee_fx=8;sellfee_fx=8.2;cost=q*buy*fxbuy+feebuy*buyfee_fx;proceeds=q*sell*fxsell-feesell*sellfee_fx o['DJ110']={'quantity':q,'buy_A':buy,'sell_A':sell,'buy_A_R':fxbuy,'sell_A_R':fxsell,'buy_fee_B':feebuy,'sell_fee_B':feesell,'buy_B_R':buyfee_fx,'sell_B_R':sellfee_fx,'cost_R':cost,'proceeds_R':proceeds,'profit_R':proceeds-cost,'wrong_fees_A':q*sell*fxsell-feesell*fxsell-(q*buy*fxbuy+feebuy*fxbuy),'price_profit_at_buyfx':q*(sell-buy)*fxbuy,'fx_profit_on_end':q*sell*(fxsell-fxbuy),'fees_total_R':feebuy*buyfee_fx+feesell*sellfee_fx} return o if __name__=='__main__': a=calculate() assert math.isclose(a['DJ084']['before_dirty'],a['DJ084']['after_total'],abs_tol=1e-8) assert math.isclose(a['DJ106']['same_total_daily'],a['DJ106']['same_total_weekly'],abs_tol=1e-12) assert math.isclose(a['DJ107']['unit_drawdown']['maximum'],a['DJ107']['no_deposit_amount']['maximum'],abs_tol=1e-12) assert math.isclose(a['DJ110']['price_profit_at_buyfx']+a['DJ110']['fx_profit_on_end']-a['DJ110']['fees_total_R'],a['DJ110']['profit_R'],abs_tol=1e-10) p=Path(__file__).with_suffix('.json');p.write_text(json.dumps({'as_of':'2026-10-23','python_version':sys.version,'sample_type':'构造合同与自行编写重放实验,未运行第三方回测框架','articles':a},ensure_ascii=False,indent=2),encoding='utf-8');print('20篇计算与独立勾稽通过')