"""C07完整构造模型;Python3.10+标准库;无网络/仓库依赖。 金额Decimal内部精度50,金额显示2位,收益/权重/天数显示6位,ROUND_HALF_UP。 默认在本脚本同目录写纯11ID UTF-8/LF JSON。百分比与百分点均显式乘100。 """ from pathlib import Path from datetime import date from decimal import Decimal,getcontext,ROUND_HALF_UP from functools import reduce import json getcontext().prec=50 D=Decimal def d(x): if x is None:raise ValueError('未知不等于零') if isinstance(x,float):raise TypeError('须输入十进制字符串') return D(x) def f(x,n=6): v=d(x).quantize(D(1).scaleb(-n),rounding=ROUND_HALF_UP) return format(abs(v) if v==0 else v,'f') def pct(x):return f(d(x)*100)+'%' def money(x):return f(x,2) def prod(values):return reduce(lambda a,b:a*b,values,D(1)) def compound(values):return prod(1+d(r) for r in values)-1 def table(header,rows):return {'header':header,'rows':rows} def rec(inputs,tables,assumptions,checks):return {'inputs':inputs,'precision':'Decimal精度50,金额2位,比例/百分点6位,ROUND_HALF_UP;计算中不逐行舍入','tables':tables,'assumptions':assumptions,'self_checks':checks} def close(a,b):return abs(a-b)0 if side=='up' else r<0)] if not ids:return None,'empty_group' exponent=D(12)/len(ids);br=prod(1+b[k] for k in ids)**exponent-1;pr=prod(1+p[k] for k in ids)**exponent-1 if br==0:return None,'zero_benchmark_denominator' return pr/br,'computed' def quantity_status(total,sold): if total is None or sold is None:return 'unknown_quantity' if d(total)<0 or d(sold)<0:return 'invalid_negative_quantity' return 'oversell_rejected' if d(sold)>d(total) else 'valid' def dietz(bv,ev,events,days,bod=False): if bv is None or ev is None or any(c is None for _,c in events):return None,'unknown' if days<=0 or any(k<0 or k>days for k,_ in events):return None,'invalid_date' capital=d(bv)+sum((d(c)*D(days-k+int(bod))/days for k,c in events),D(0)) if capital<=0:return None,'nonpositive_average_capital' return (d(ev)-d(bv)-sum((d(c) for _,c in events),D(0)))/capital,'computed' def brinson(wp,wb,returns,labels): if any(x is None for x in labels):raise ValueError('缺映射不填零') assert sum(wp)==sum(wb)==1 rows=[];total=[D(0)]*3 for group in sorted(set(labels)): ids=[i for i,g in enumerate(labels) if g==group] p=sum(wp[i] for i in ids);b=sum(wb[i] for i in ids) if p<=0 or b<=0:raise ValueError('本模型未定义空组回报替代') rp=sum(wp[i]*returns[i] for i in ids)/p;rb=sum(wb[i]*returns[i] for i in ids)/b effects=[(p-b)*rb,b*(rp-rb),(p-b)*(rp-rb)] total=[x+y for x,y in zip(total,effects)] rows.append([group,pct(p),pct(b),pct(rp),pct(rb)]+[f(x*100) for x in effects]) rp=sum(a*r for a,r in zip(wp,returns));rb=sum(a*r for a,r in zip(wb,returns)) assert close(sum(total),rp-rb) return rows,total,rp,rb def main_model(): out={} # 067:同端点流量、两条不同中间路径。 bv,ev=D(100),D(145);events=[(10,'60'),(20,'-30')];days=30 md,status=dietz(bv,ev,events,days);capital=bv+60*D(20)/30-30*D(10)/30 paths=[] for name,pre1,pre2 in [('A','110','150'),('B','90','135')]: a,b=map(D,[pre1,pre2]);after1=a+60;after2=b-30 returns=[a/bv-1,b/after1-1,ev/after2-1];twr=compound(returns) paths.append([name,money(a),money(after1),money(b),money(after2)]+[pct(r) for r in returns]+[pct(twr),f((md-twr)*100)]) assert paths[0][-2]!=paths[1][-2] out['DJ3-067']=rec({'单位':'人民币万元','期初':'2025-05-31','期末':'2025-06-30','BV':'100','EV':'145','外部现金流':[['2025-06-10','60'],['2025-06-20','-30']],'日末政策':True}, {'现金流权重':table(['现金流日','距期初日数','流量万元','剩余日数','日权重','加权资本万元'],[['2025-06-10',10,'60.00',20,f(D(20)/30),money(60*D(20)/30)],['2025-06-20',20,'-30.00',10,f(D(10)/30),money(-30*D(10)/30)]]), '近似结果':table(['项目','数值'],[['净流入万元','30.00'],['金额利润万元',money(ev-bv-30)],['平均资本万元',money(capital)],['Modified Dietz',pct(md)],['日初政策对照',pct(dietz(bv,ev,events,30,True)[0])]]), '中间估值参照':table(['路径','入金前','入金后','提款前','提款后','段1回报','段2回报','段3回报','精确TWR','MD减TWR百分点'],paths)}, ['30自然日期间;现金流日末进入,日权重(30-日序)/30;正为边界外流入','中间估值路径只是构造参照,不是仅凭端点复原;无费用与分红','不是XIRR、非年化,不宣称GIPS合规'],{'端点日差30':(date(2025,6,30)-date(2025,5,31)).days==30,'零平均资本拒绝':dietz('0','1',[],30)[1],'负资本拒绝':dietz('100','20',[(0,'-200')],30)[1],'未知拒绝':dietz(None,'1',[],30)[1],'零外流可算':dietz('100','110',[],30)[0]==D('.1')}) # 068:代数望远镜链接,交叉项按明确顺序分配。 rp=list(map(D,['.10','-.05','.08']));rb=list(map(D,['.06','-.02','.04']));alloc=list(map(D,['.02','-.01','.015']));select=[p-b-a for p,b,a in zip(rp,rb,alloc)] weights=[prod(1+rp[k] for k in range(t))*prod(1+rb[k] for k in range(t+1,3)) for t in range(3)] reverse=[prod(1+rb[k] for k in range(t))*prod(1+rp[k] for k in range(t+1,3)) for t in range(3)] linked_a=sum(a*w for a,w in zip(alloc,weights));linked_s=sum(a*w for a,w in zip(select,weights));excess=compound(rp)-compound(rb) assert linked_a+linked_s==excess out['DJ3-068']=rec({'期间':['2025-01-01至04-30','2025-05-01至08-31','2025-09-01至12-31'],'组合收益':list(map(str,rp)),'基准收益':list(map(str,rb)),'配置':list(map(str,alloc)),'选择':list(map(str,select))}, {'单期与权重':table(['四个月报告段','组合','基准','配置百分点','选择百分点','单期超额百分点','链接权重','链接配置百分点','链接选择百分点'],[[j+1,pct(rp[j]),pct(rb[j]),f(alloc[j]*100),f(select[j]*100),f((rp[j]-rb[j])*100),f(weights[j]),f(alloc[j]*weights[j]*100),f(select[j]*weights[j]*100)] for j in range(3)]), '全年对照':table(['方法','配置百分点','选择百分点','总超额百分点','距累计差残差百分点'],[['直接相加',f(sum(alloc)*100),f(sum(select)*100),f(sum(p-b for p,b in zip(rp,rb))*100),f((excess-sum(p-b for p,b in zip(rp,rb)))*100)],['望远镜链接',f(linked_a*100),f(linked_s*100),f(excess*100),'0.000000'],['反向交叉项分配',f(sum(a*w for a,w in zip(alloc,reverse))*100),f(sum(a*w for a,w in zip(select,reverse))*100),f(excess*100),'0.000000']]),'累计回报':table(['组合','基准','回报差百分点'],[[pct(compound(rp)),pct(compound(rb)),f(excess*100)]])}, ['3期各为4个月,覆盖全年,不拿3个月叫全年;单期归因已确认闭合','权重=本期之前组合增长因子乘本期之后基准增长因子;分项不是唯一因果','不把(1+单期差)连乘当累计回报差'],{'单期闭合':all(a+s==p-b for a,s,p,b in zip(alloc,select,rp,rb)),'链接精确闭合':True,'反向分配也闭合':sum((p-b)*w for p,b,w in zip(rp,rb,reverse))==excess,'零收益权重为1':prod([D(1),D(1)])==1}) # 069:有效日分类与统一重述;六证券、两期价格。 names=list('ABCDEF');wp=list(map(D,['.30','.20','.10','.15','.15','.10']));wb=list(map(D,['.20','.20','.20','.10','.15','.15'])) returns=[list(map(D,['.10','0','-.05','.04','.02','-.02'])),list(map(D,['-.02','.06','.08','0','-.04','.03']))] old=['X','X','X','Y','Y','Y'];new=['X','X','Y','Y','Y','Y'];price0=[D(100)]*6;price1=[p*(1+r) for p,r in zip(price0,returns[0])];price2=[p*(1+r) for p,r in zip(price1,returns[1])] mapping=[[name,old[k],new[k],'2025-07-01' if k==2 else '全期不变',pct(wp[k]),pct(wb[k]),money(price0[k]),money(price1[k]),money(price2[k])] for k,name in enumerate(names)] effectrows=[];group_rows=[] for period in range(2): for policy,labels in [('当时分类',old if period==0 else new),('统一新分类重述',new)]: rows,effects,p,b=brinson(wp,wb,returns[period],labels) effectrows.append([period+1,policy,pct(p),pct(b)]+[f(x*100) for x in effects]+[f((p-b)*100),'0.000000']) group_rows.extend([[period+1,policy]+r for r in rows]) missing_rejected=False try:brinson(wp,wb,returns[0],['X',None,'X','Y','Y','Y']) except ValueError:missing_rejected=True out['DJ3-069']=rec({'期间':['2025-01-01至06-30','2025-07-01至12-31'],'分类生效':'2025-07-01','证券':names,'组合权重':list(map(str,wp)),'基准权重':list(map(str,wb)),'两期收益':[[str(x) for x in r] for r in returns]}, {'历史交叉映射':table(['证券','原分类','新分类','生效边界','组合权重','基准权重','期初价','6月末价','12月末价'],mapping), '行业中间账':table(['半年段','分类政策','行业','组合组权重','基准组权重','组内组合回报','组内基准回报','配置百分点','选择百分点','交互百分点'],group_rows), '映射前后归因':table(['半年段','政策','总组合','总基准','配置百分点','选择百分点','交互百分点','总差百分点','残差百分点'],effectrows)}, ['单币种、完全投资、六证券同口径价格无分配;年中按给定权重再平衡、无费用','X/Y为自定义教学类别;C从7月起跨类,统一重述是另一版本非当时可得标签','BHB:配置=(Wp-Wb)Rb,选择=Wb(Rp-Rb),交互=(Wp-Wb)(Rp-Rb);组内回报按组权重归一'],{'两政策总回报各期相同':all(effectrows[k][2:4]==effectrows[k+1][2:4] for k in [0,2]),'缺映射拒绝':missing_rejected,'证券权重均合1':sum(wp)==sum(wb)==1}) # 070:交易重放和静态持仓残差。 initial={'A':D(100),'B':D(50),'C':D(0)};p0=dict(zip('ABC',map(D,['10','20','5'])));p1=dict(zip('ABC',map(D,['11','18','7']))) cash0=D(1000);cash1=cash0+D(40)*12-4-D(60)*6-3+50+5 final={'A':D(60),'B':D(50),'C':D(60)};v0=cash0+sum(initial[k]*p0[k] for k in initial);v1=cash1+sum(final[k]*p1[k] for k in final);pnl=v1-v0 static=sum(initial[k]*(p1[k]-p0[k]) for k in initial);trade=D(40)*(12-p1['A'])+D(60)*(p1['C']-6) assert static+trade+50-7+5==pnl out['DJ3-070']=rec({'期初':'2025-03-31','期末':'2025-06-30','单位':'人民币元,证券数量单位股(构造)','期初数量':{k:str(v) for k,v in initial.items()},'期初价格':{k:str(v) for k,v in p0.items()},'期末价格':{k:str(v) for k,v in p1.items()},'期初现金':'1000','交易':[['2025-05-15','卖A','40','12','4'],['2025-05-15','买C','60','6','3']],'分红':['2025-06-01','B每股1,50股','50'],'现金利息':['2025-06-30','5']}, {'持仓与终值':table(['证券','期初数量','期初价','期末数量','期末价','期末市值'],[[k,f(initial[k],0),money(p0[k]),f(final[k],0),money(p1[k]),money(final[k]*p1[k])] for k in initial]), '现金流水':table(['事件','现金增减元','余额元'],[['期初','1000.00','1000.00'],['卖A毛额','480.00','1480.00'],['卖出费','-4.00','1476.00'],['买C','-360.00','1116.00'],['买入费','-3.00','1113.00'],['B现金分红','50.00','1163.00'],['现金利息','5.00',money(cash1)]]), '静态到真实桥':table(['项目','损益元'],[['期初数量静态价格变动',money(static)],['卖A后避免的期末下跌',money(40*(12-p1['A']))],['新增C持有段',money(60*(p1['C']-6))],['分红','50.00'],['费用','-7.00'],['现金利息','5.00'],['真实总损益',money(pnl)]]), '资产核对':table(['期初资产','期末资产','净外部流量','损益','静态价格加分红','未解释残差'],[[money(v0),money(v1),'0.00',money(pnl),money(static+50),money(pnl-static-50)]])}, ['无外部申赎、税、拆并股;成交量已给定且无负仓;价格含义不复权,分红仅现金一次','真实流水完整为本例构造,不能从季报反推实际调仓','无交易情景时交易贡献归零,仍保留分红/费用/利息的各自条件'],{'数量守恒':final['A']==100-40 and final['C']==60,'金额桥闭合':True,'现金1168':cash1==1168,'真实净回报':pct(pnl/v0)}) # 071:前4月A后4月B;原始指数基点不同。 pr=list(map(D,['.02','-.01','.03','.01','0','.02','-.02','.04']));aa=list(map(D,['.01','0','.02','-.01','.01','.01','-.01','.02']));bb=list(map(D,['.03','-.02','.01','.02','-.01','0','.03','.01'])) chains=[];port=D(100);chain=D(100);fullb=D(100);rawa=D(1000);rawb=D(2000) for k in range(8): port*=1+pr[k];chain*=1+(aa[k] if k<4 else bb[k]);fullb*=1+bb[k];rawa*=1+aa[k];rawb*=1+bb[k] chains.append([f'2025-{k+1:02d}',pct(pr[k]),pct(aa[k]),pct(bb[k]),'A-v1' if k<4 else 'B-v2',f(port),f(chain),f(fullb),f(rawa),f(rawb)]) out['DJ3-071']=rec({'单位':'归一化点位/百分点','观察':'2024-12-31至2025-08-31','事前决定日':'2025-04-15','变更生效日':'2025-05-01','变更理由':'构造授权范围改变,预先决定','组合月回报':list(map(str,pr)),'A月回报':list(map(str,aa)),'B月回报':list(map(str,bb)),'A原始起点':'1000','B原始起点':'2000'}, {'版本与曲线':table(['月末','组合月回报','A月回报','B月回报','生效版本','组合归一点位','分段基准','全段B重述','A原始点位','B原始点位'],chains), '版本结果':table(['口径','基准累计','组合减基准百分点'],[['前A后B自定义链',pct(chain/100-1),f((port-chain))],['全段追溯B',pct(fullb/100-1),f(port-fullb)],['错误原始点位直接拼接',pct(rawb/1000-1),f((port/100-rawb/1000)*100)]]), '变更生效表':table(['版本','适用收益区间','记录性质'],[['A-v1','2025-01至04月','保留旧报告'],['B-v2','2025-05至08月','前瞻生效'],['B全段重述','2025-01至08月','另列对照,不覆盖旧链']])}, ['8个月仅报区间累计,不年化;两基准均无税含分配的构造回报、同币种','变更发生在月界;实际规则缺失时不能替真实合同决定生效','分段乘增长因子,不直接拼1000/2000基点;自定义链不是B全历史'],{'分段连乘相等':close(chain/100,prod(1+x for x in aa[:4])*prod(1+x for x in bb[4:])),'各序列8行':len(chains)==8,'边界无重复五月':chains[3][4]=='A-v1' and chains[4][4]=='B-v2'}) # 072:同币种内部转账抵销。 a0,b0=D(900),D(100);a_mid=a0*D('1.1');b_mid=b0;transfer=D(200);a_after=a_mid-transfer;b_after=b_mid+transfer;a_end=a_after;b_end=b_after*D('.95') total0=a0+b0;mid=a_mid+b_mid;end=a_end+b_end;first=mid/total0-1;second=end/mid-1 actual=end/total0-1 assert close((1+first)*(1+second)-1,actual) out['DJ3-072']=rec({'期间':'2025-01-01至2025-12-31','切段时点':'2025-06-30日末','币种':'CNY','单位':'元','期初A':'900','期初B':'100','第一段A':'0.10','第一段B':'0','第二段A':'0','第二段B':'-0.05','内部转账A至B':'200','外部流量':'0'}, {'账户金额账':table(['账户','期初','转账前估值','内部流量','转账后','期末','金额损益','账户TWR'],[['A',money(a0),money(a_mid),money(-transfer),money(a_after),money(a_end),money(a_end-a0+transfer),'10.000000%'],['B',money(b0),money(b_mid),money(transfer),money(b_after),money(b_end),money(b_end-b0-transfer),'-5.000000%'],['合并',money(total0),money(mid),'0.00',money(mid),money(end),money(end-total0),pct(actual)]]), '合并切段':table(['区间','起值','终值','外部现金','回报'],[['上半年',money(total0),money(mid),'0.00',pct(first)],['下半年',money(mid),money(end),'0.00',pct(second)],['全年连乘',money(total0),money(end),'0.00',pct(actual)]]), '错误权重与对照':table(['方法','回报'],[['账户简单平均',pct((D('.1')-D('.05'))/2)],['期初权重乘全年账户率',pct((a0*D('.1')+b0*D('-.05'))/total0)],['期末资产权重',pct((a_end*D('.1')+b_end*D('-.05'))/end)],['正确分段合并',pct(actual)],['取消内部转账条件',pct((a_mid+b0*D('.95'))/total0-1)]])}, ['两账户同一投资者评价边界、同估值截点、无汇率、费用与负债','内部转账子账户一进一出,整体无外部流量;转账不计交易利润','月中实际流量需另切段;本例切点前后估值全知'],{'内部流量为零':-transfer+transfer==0,'合并利润75':end-total0==75,'转账不改变即时总资产':a_after+b_after==mid,'链接与端点相等':True}) # 073:四账户月度资格与余额递推;不把退出者历史删去。 raw_returns={'A':['.01']*4,'B':['-.10','-.10',None,None],'C':[None,'.02','-.01','.01'],'D':[None,None,None,'-.04']} starts={'A':D(100),'B':D(100),'C':D(200),'D':D(50)};balances=dict(starts);monthrows=[];detail=[];survivor=[] for k in range(4): members=[name for name in starts if raw_returns[name][k] is not None];total=sum(balances[n] for n in members);pnl=sum(balances[n]*D(raw_returns[n][k]) for n in members);ret=pnl/total remaining=[n for n in members if n!='B'];surv_total=sum(balances[n] for n in remaining);surv_ret=sum(balances[n]*D(raw_returns[n][k]) for n in remaining)/surv_total survivor.append(surv_ret) monthrows.append([f'2025-{k+1:02d}',','.join(members),money(total),money(pnl),pct(ret),pct(surv_ret)]) for name in members: value=balances[name];r=D(raw_returns[name][k]);detail.append([f'2025-{k+1:02d}',name,money(value),pct(value/total),pct(r),money(value*r),money(value*(1+r))]);balances[name]*=1+r # 用明细精度而非已显示六位收益重建每月综合体。 balances=dict(starts);returns=[] for k in range(4): valid=[n for n in starts if raw_returns[n][k] is not None];total=sum(balances[n] for n in valid);returns.append(sum(balances[n]*D(raw_returns[n][k]) for n in valid)/total) for n in valid:balances[n]*=1+D(raw_returns[n][k]) out['DJ3-073']=rec({'月份':'2025-01至04月','政策':'从第一个完整在管月纳入,退出保留最后完整在管月;不跨断档','成员日期':[['A','2024-12-01','在管'],['B','2024-12-01','2025-02-28日末退出'],['C','2025-01-15','在管'],['D','2025-03-15','在管']],'首个完整月资产':{k:str(v) for k,v in starts.items()},'各月收益':raw_returns}, {'完整月成员政策':table(['账户','进入日','退出边界','参与完整月'],[['A','2024-12-01','未退出','1,2,3,4'],['B','2024-12-01','2025-02-28日末','1,2'],['C','2025-01-15','未退出','2,3,4'],['D','2025-03-15','未退出','4']]), '每月权重明细':table(['月','账户','月初资产','权重','月收益','本月损益','月末资产'],detail), '历史与幸存者':table(['月','真实成员','月初合计','损益','完整历史回报','只留期末账户'],monthrows), '累计对照':table(['完整政策链','删B历史链','差百分点'],[[pct(compound(returns)),pct(compound(survivor)),f((compound(survivor)-compound(returns))*100)]])}, ['仅4个月构造,不声称GIPS报告;C/D不完整进入月不造回报','各账户完整纳入月无内部现金流;新成员首月资产单独输入,不能从别的账户复制','无成员月输出null并断开链接,绝不补0或补基准'],{'B保留1月和2月':sum(1 for x in detail if x[1]=='B')==2,'C不填1月收益':raw_returns['C'][0] is None,'每月权重合1':all(sum(D(x[3].rstrip('%')) for x in detail if x[0]==m[0]).quantize(D('.00001'))==D(100).quantize(D('.00001')) for m in monthrows),'空集合不得算回报':None}) # 074:横截面年度离散度与A月度时序。 ar=list(map(D,['.04','.06','.08','.10','.12','.14']));assets=list(map(D,['100','100','200','200','300','100']));weights=[x/sum(assets) for x in assets];mean=sum(ar)/6;var=sum((r-mean)**2 for r in ar)/6;weighted=sum(r*w for r,w in zip(ar,weights));wvar=sum(w*(r-weighted)**2 for r,w in zip(ar,weights));monthly=[D(0)]*11+[D('.04')];mm=sum(monthly)/12;mv=sum((r-mm)**2 for r in monthly)/12 out['DJ3-074']=rec({'年度':'2025-01-01至12-31','口径':'人民币元、费用后账户回报;横截面总体分母n=6','全年度A至F回报':list(map(str,ar)),'期初资产':list(map(str,assets)),'G':'2025-07-01进入,半年度回报30%','H':'2025-09-30退出,不完整年回报-20%','A月回报':list(map(str,monthly))}, {'年度成员资格':table(['账户','全年资格','年/部分期回报','期初权重资产','采用权重'],[[chr(65+k),'完整年',pct(ar[k]),money(assets[k]),pct(weights[k])] for k in range(6)]+[['G','非完整年','30.000000%','不参与','不参与'],['H','非完整年','-20.000000%','不参与','不参与']]), '离散度与范围':table(['对象/定义','均值','标准差百分点','最低','最高','范围百分点'],[['6账户年度等权总体',pct(mean),f(var.sqrt()*100),pct(min(ar)),pct(max(ar)),f((max(ar)-min(ar))*100)],['6账户年度期初资产权重',pct(weighted),f(wvar.sqrt()*100),pct(min(ar)),pct(max(ar)),f((max(ar)-min(ar))*100)],['A的12个月总体波动',pct(mm),f(mv.sqrt()*100),pct(min(monthly)),pct(max(monthly)),f((max(monthly)-min(monthly))*100)]]), 'A月度时序':table(['月','A月收益'],[[f'2025-{k+1:02d}',pct(r)] for k,r in enumerate(monthly)]), '样本分母敏感性':table(['年度等权总体n=6','年度等权样本n-1=5','A年回报'],[[f(var.sqrt()*100),f((var*6/5).sqrt()*100),pct(compound(monthly))]])}, ['6个完整年度账户;G/H不混入全年离散度,未将半年率年化成全年','资产权重使用期初金额,非GIPS唯一指定;方法要披露并一致','横截面年收益标准差不年化;A12个月标准差为时序对象,不是另一种内部离散度'],{'A月复利等于4%':compound(monthly)==ar[0],'六成员资格':len(ar)==6,'相同年收益离散为零':sum((D('.1')-D('.1'))**2 for _ in range(6))==0,'仅一成员统计边界':'总体标准差可为0,不能作广泛一致性证据'}) # 075:上6下4零2,年化指数为整数,避免非整数幂混精度。 br=list(map(D,['.02','-.02','.01','0','-.03','.03','-.01','.02','0','-.02','.01','.04']));fr=list(map(D,['.03','.01','.02','.05','.01','.04','.01','.01','-.05','.01','.02','.03'])) groups={key:[k for k,x in enumerate(br) if (x>0 if key=='上行' else x<0)] for key in ['上行','下行']};capture=[] for key,ids in groups.items(): b=compound(br[k] for k in ids);p=compound(fr[k] for k in ids);exp=12//len(ids);assert 12%len(ids)==0 ba=(1+b)**exp-1;pa=(1+p)**exp-1;cap=pa/ba capture.append([key,len(ids),pct(p),pct(b),exp,pct(pa),pct(ba),f(cap*100)]) out['DJ3-075']=rec({'期间':'2025-01至12月','月基准':list(map(str,br)),'月组合':list(map(str,fr)),'政策':'按基准正/负分组,零基准月不进入两组;年化指数12/n;捕获率=两者年化回报比×100','数据口径':'同月同币种含分配、费用后组合,对照基准为构造'}, {'逐月标签':table(['月','基准月回报','组合月回报','分组'],[[f'2025-{k+1:02d}',pct(b),pct(fr[k]),'上行' if b>0 else '下行' if b<0 else '零月排除'] for k,b in enumerate(br)]), '子样本与捕获率':table(['组','月份数','组合子样本累计','基准子样本累计','年化指数','组合年化','基准年化','捕获率%'],capture), '边界状态':table(['条件','输出'],[['空子样本','null,样本不足'],['基准年化分母0','null,不定义'],['月回报低于-100%','拒绝普通正净值复利'],['缺失月份','未知,不补0'],['本例下行组组合上涨','负捕获率,只作历史描述']])}, ['12/n为本文明确年化约定,不声称2014教育页已给出Direct全部实现细节','上行6月、下行4月、零2月,零月不改变组内分母;子样本不是连续市场一年','不以负捕获率推断未来抗跌能力或择时因果'],{'上行6':len(groups['上行'])==6,'下行4':len(groups['下行'])==4,'零2':sum(x==0 for x in br)==2,'下行负捕获率':D(capture[1][-1])<0,'月度齐12':len(fr)==len(br)==12}) # 076:24个月末观测,23个月区间;12个完整12月持有窗。 dates=[date(2024+k//12,k%12+1,[31,29 if 2024+k//12==2024 else 28,31,30,31,30,31,31,30,31,30,31][k%12]).isoformat() for k in range(24)] nav=list(map(D,['100','102','104','103','105','107','106','108','110','109','111','112','110','108','107','106','105','104','103','102','101','100','99','98'])) windows=[];coverage=[0]*23;valid=[] for k in range(24): target_year=2024+(k+12)//12;target_month=(k+12)%12+1;target_date=f'{target_year}-{target_month:02d}-'+('29' if target_year%4==0 and target_month==2 else '28' if target_month==2 else '30' if target_month in [4,6,9,11] else '31') if k+12<24: r=nav[k+12]/nav[k]-1;valid.append(r);windows.append([dates[k],target_date,money(nav[k]),money(nav[k+12]),pct(r),'完整12月']) for j in range(k,k+12):coverage[j]+=1 else:windows.append([dates[k],target_date,money(nav[k]),'未知','不计算','不足期']) pos=sum(r>0 for r in valid);zero=sum(r==0 for r in valid);negative=sum(r<0 for r in valid) out['DJ3-076']=rec({'单位':'归一化总回报净值点位','观察日期':dates,'净值':list(map(str,nav)),'持有期':'12个自然月月末到月末,非固定365天','正收益标准':'>0,零单列,缺失不当亏损'}, {'完整与不足期网格':table(['起点','12月目标终点','起点净值','终点净值','持有回报','有效性'],windows), '重叠覆盖':table(['月区间结束','被几个窗口覆盖'],[[dates[j+1],coverage[j]] for j in range(23)]), '分母审计':table(['字段','结果'],[['月末观测',24],['月区间',23],['有效12月窗口',len(valid)],['不足期起点',24-len(valid)],['正收益',pos],['零收益',zero],['负收益',negative],['历史正比例',pct(D(pos)/len(valid))],['重复覆盖总计',sum(coverage)],['相邻窗口共同月区间',11]])}, ['24个净值点不是24个收益区间;本例日期为月末观察,全部构造无未来补值','12个窗口各覆盖12区间,共144次区间覆盖,非144个独立月份','不把描述比例当未来盈利概率,不作二项独立置信度'],{'窗口数24减12':len(valid)==12,'覆盖合144':sum(coverage)==144,'正零负合12':pos+zero+negative==12,'日历跨闰年非365固定':'2024-01-31至2025-01-31为366天','净值正数':all(x>0 for x in nav)}) # 077:费用嵌入买入成本,现金分红独立;FIFO与移动平均。 buys=[('2025-01-10',D(100),D(10),D(10)),('2025-03-10',D(100),D(14),D(10))];qty=sum(q for _,q,_,_ in buys);totalcost=sum(q*p+fee for _,q,p,fee in buys);sold=D(150);saleprice=D(16);salefee=D(15);mark=D(15);dividend=qty*D('.5');sale=sold*saleprice;net=sale-salefee;remaining=qty-sold;market=remaining*mark lotrows=[];need=sold;fifo_cost=D(0) for dt,q,p,fee in buys: unitcost=(q*p+fee)/q;use=min(q,need);cost=use*unitcost;need-=use;fifo_cost+=cost;lotrows.append([dt,f(q,0),money(p),money(fee),money(q*p+fee),f(unitcost),f(use,0),money(cost),f(q-use,0),money((q-use)*unitcost)]) average=totalcost/qty;avg_cost=sold*average result=[] for name,cost in [('FIFO内部成本归属',fifo_cost),('移动平均内部成本归属',avg_cost)]: leftcost=totalcost-cost;real=net-cost;unreal=market-leftcost;total=real+unreal+dividend assert total==market+net+dividend-totalcost and cost+leftcost==totalcost result.append([name,money(cost),money(leftcost),money(real),money(unreal),money(dividend),money(total)]) out['DJ3-077']=rec({'单位':'人民币元、构造份额','两笔买入':[[dt,str(q),str(p),str(fee)] for dt,q,p,fee in buys],'分红日':'2025-04-10','每份分红':'0.50','分红资格数量':'200','卖出日':'2025-05-10','卖出数量':'150','卖价':'16','卖出费':'15','估值日':'2025-06-30','期末价':'15'}, {'FIFO批次成本账':table(['买入日','份额','价格','买入费','总成本','单位成本','卖出分配份额','卖出分配成本','剩余份额','剩余成本'],lotrows), '移动平均成本账':table(['时点','数量','累计成本','平均成本','本次转出成本','剩余成本'],[['首笔买入',100,money(D(1010)),f(D('10.1')),'0.00',money(D(1010))],['第二笔后',200,money(totalcost),f(average),'0.00',money(totalcost)],['卖出150后',50,money(totalcost-avg_cost),f(average),money(avg_cost),money(totalcost-avg_cost)]]), '两方法损益拆分':table(['内部方法','卖出成本','剩余成本','已实现含卖费','未实现','分红','总损益'],result), '总资产守恒':table(['总投入含买费','卖出毛额','卖出费','净现金','现金分红','剩余市值','期末资产','总损益'],[[money(totalcost),money(sale),money(salefee),money(net),money(dividend),money(market),money(net+dividend+market),money(net+dividend+market-totalcost)]]), '边界对照':table(['条件','卖出成本','剩余成本','剩余数量','未实现','总损益含分红'],[['完全不卖',money(0),money(totalcost),200,money(qty*mark-totalcost),money(qty*mark-totalcost+dividend)],['全部卖200且卖费20',money(totalcost),money(0),0,money(0),money(qty*saleprice-20-totalcost+dividend)]])}, ['仅内部教学成本归属,不是财务或税务方法选择建议;无真实产品合同','买费已资本化到内部成本,卖费已扣到实现损益,分红现金仅记一次','卖出现金保留于账户,不再作外部收回加第二遍;无再投、税、汇率和拆并份额','移动平均逐次买入更新,卖出时使用卖前均价;本例非任意期末总成本平均'],{'份额200减150剩50':remaining==50,'两法总损益815':all(r[-1]=='815.00' for r in result),'FIFO已实现多100未实现少100':fifo_cost==avg_cost-100,'超卖拒绝':sold<=qty,'原值成本守恒':True}) # 执行未知/零/负数与空集合边界,保留实际状态。 assert weighted_return([],[])[1]=='empty_members' assert weighted_return(['.1'],['0'])[1]=='invalid_capital' assert weighted_return([None],['1'])[1]=='unknown_or_unaligned' assert weighted_return(['0'],['1'])[0]==0 out['DJ3-073']['self_checks']['空集合不得算回报']=weighted_return([],[])[1] out['DJ3-073']['self_checks']['未知回报不得补零']=weighted_return([None],['1'])[1] assert capture_ratio(['0'],['.1'],'up')[1]=='empty_group' assert capture_ratio(['.1'],[None],'up')[1]=='unknown_or_unaligned' assert capture_ratio(['-.01'],['.01'],'down')[0]<0 assert capture_ratio(['-1'],['.01'],'down')[1]=='nonpositive_growth_factor' # 零分母的显式保护也用于计算完成后的捕获率相除,不能以默认0代替。 def divide_if_nonzero(a,b):return None if b==0 else a/b assert divide_if_nonzero(D(1),D(0)) is None out['DJ3-075']['self_checks']['空组实测']=capture_ratio(['0'],['.1'],'up')[1] out['DJ3-075']['self_checks']['缺月实测']=capture_ratio(['.1'],[None],'up')[1] out['DJ3-075']['self_checks']['非正净值实测']=capture_ratio(['-1'],['.01'],'down')[1] out['DJ3-075']['self_checks']['零分母实测']=divide_if_nonzero(D(1),D(0)) for side,row in zip(['up','down'],out['DJ3-075']['tables']['子样本与捕获率']['rows']): assert f(capture_ratio([str(x) for x in br],[str(x) for x in fr],side)[0]*100)==row[-1] assert quantity_status('200','201')=='oversell_rejected' assert quantity_status('200','0')=='valid' assert quantity_status('200','-1')=='invalid_negative_quantity' assert quantity_status('200',None)=='unknown_quantity' out['DJ3-077']['self_checks']['超卖拒绝实测']=quantity_status('200','201') out['DJ3-077']['self_checks']['零卖出实测']=quantity_status('200','0') out['DJ3-077']['self_checks']['负卖出实测']=quantity_status('200','-1') out['DJ3-077']['self_checks']['未知数量实测']=quantity_status('200',None) assert list(out)==[f'DJ3-{n:03d}' for n in range(67,78)] # 通用模型边界自检:未知、零、负数、日期次序、币种上下文明确分开。 try:d(None);raise AssertionError('未拒绝未知') except ValueError:pass assert d('0')==0 and d('-1')<0 assert dietz('100','100',[(31,'1')],30)[1]=='invalid_date' assert date(2025,1,31)-date(2024,1,31)==date(2024,12,31)-date(2023,12,31) return out if __name__=='__main__': with Path(__file__).resolve().with_suffix('.json').open('w',encoding='utf-8',newline='\n') as handle:handle.write(json.dumps(main_model(),ensure_ascii=False,indent=2)+'\n')