"""DJ001—DJ020 构造算例复算;仅用标准库,运行后输出同目录 JSON。""" from datetime import date, timedelta from decimal import Decimal, ROUND_HALF_UP from pathlib import Path import json,csv,io def npv(rate, flows): start = min(date.fromisoformat(d) for d, _ in flows) return sum(c / (1 + rate) ** ((date.fromisoformat(d)-start).days/365) for d,c in flows) def root(flows, lo=-.99, hi=10): assert npv(lo,flows)*npv(hi,flows) <= 0 for _ in range(200): mid=(lo+hi)/2 if npv(lo,flows)*npv(mid,flows)<=0: hi=mid else: lo=mid return (lo+hi)/2 def newton(flows, guess, limit=100): r=guess t=[((date.fromisoformat(d)-date.fromisoformat(flows[0][0])).days/365,c) for d,c in flows] for k in range(limit): if r<=-1: return {'status':'越过定义域','iterations':k,'last_rate':r} v=sum(c/(1+r)**x for x,c in t) if abs(v)<1e-9:return {'status':'收敛','iterations':k,'rate':r,'residual':v} der=sum(-x*c/(1+r)**(x+1) for x,c in t) if abs(der)<1e-14:return {'status':'导数过小','iterations':k,'last_rate':r} r-=v/der return {'status':'达到迭代上限','last_rate':r} def money(x):return round(float(x),6) out={} r=.005 rows=[{'month':m,'start_periods':13-m,'end_periods':12-m,'start_fv':1000*(1+r)**(13-m),'end_fv':1000*(1+r)**(12-m)} for m in range(1,13)] out['DJ001']={'monthly_rate':r,'rows':rows,'start':sum(x['start_fv'] for x in rows),'end':sum(x['end_fv'] for x in rows),'negative_start':sum(1000*.995**(13-m) for m in range(1,13)),'negative_end':sum(1000*.995**(12-m) for m in range(1,13))} eff=.12; exact=(1+eff)**(1/12)-1; wrong=eff/12 b=c=0; rows=[] for m in range(1,13): b=b*(1+exact)+1000;c=c*(1+wrong)+1000 rows.append({'month':m,'exact_balance':b,'divide_balance':c,'difference':c-b}) out['DJ002']={'effective':eff,'exact_monthly':exact,'nominal_monthly':wrong,'equivalent_nominal':12*exact,'wrong_effective':(1+wrong)**12-1,'rows':rows} end=date(2026,12,31);rows=[] for m in range(1,13): d=date(2026,m,15);base=1000;actual=0 if m in (4,5) else (3000 if m==6 else 1000) factor=1.06**((end-d).days/365) rows.append({'date':str(d),'days':(end-d).days,'plan':base,'no_catchup':0 if m in (4,5) else 1000,'catchup':actual,'factor':factor}) out['DJ003']={'rows':rows,'plan_fv':sum(x['plan']*x['factor'] for x in rows),'no_fv':sum(x['no_catchup']*x['factor'] for x in rows),'catchup_fv':sum(x['catchup']*x['factor'] for x in rows)} b=0;rows=[] for y,p in enumerate((1000,1200,1500),1): opening=b for _ in range(12):b=b*1.005+p rows.append({'year':y,'payment':p,'opening':opening,'contribution':12*p,'closing':b,'profit':b-opening-12*p}) rev=0 for p in (1500,1200,1000): for _ in range(12):rev=rev*1.005+p out['DJ004']={'rows':rows,'total_contribution':sum(x['contribution'] for x in rows),'reverse_fv':rev} units=0;cash=0;rows=[] for d,p,c,red in [('2026-01-15',1,1000,0),('2026-02-15',.8,1000,0),('2026-03-15',1.2,0,600),('2026-04-15',1,1000,0)]: buy=c/p;units+=buy-red;cash+=red*p rows.append({'date':d,'nav':p,'payment':c,'buy':buy,'redemption_units':red,'balance_units':units,'received_cash':cash,'market_value':units*p}) out['DJ005']={'rows':rows,'end_nav':1.1,'end_value':units*1.1,'received':cash,'profit':units*1.1+cash-3000,'fee_case_received':cash-6} u=1000/1+1000/1.25;div=u*.1 out['DJ006']={'units':u,'before_nav':1.4,'after_nav':1.3,'dividend_per_unit':.1,'dividend':div,'before_value':u*1.4,'after_value':u*1.3,'end_nav':1.35,'end_value':u*1.35,'profit':u*1.35+div-2000,'down_profit':u*1.05+div-2000} u=Decimal('1234.56');dv=u*Decimal('.037');price=Decimal('1.0837');new=dv/price;rounded=new.quantize(Decimal('.01'),rounding=ROUND_HALF_UP) out['DJ007']={'old_units':str(u),'dividend':str(dv),'confirm_nav':str(price),'exact_new_units':str(new),'rounded_new_units':str(rounded),'unit_tail':str(new-rounded),'cash_tail':str(dv-rounded*price),'wrong_nav_units':str(dv/Decimal('1.12')),'total_units':str(u+rounded),'end_value':str((u+rounded)*Decimal('1.2'))} rows=[] for m,(a,b) in enumerate(zip([1,.8,.6,.8,1,1.2],[1,1.1,1.4,1.5,1.3,1.2]),1):rows.append({'month':m,'a_nav':a,'b_nav':b,'a_units':1000/a,'b_units':1000/b}) out['DJ008']={'rows':rows,'a_units':sum(x['a_units'] for x in rows),'b_units':sum(x['b_units'] for x in rows),'a_value':sum(x['a_units'] for x in rows)*1.2,'b_value':sum(x['b_units'] for x in rows)*1.2,'a_down':sum(x['a_units'] for x in rows)*.6,'b_down':sum(x['b_units'] for x in rows)*.6} rows=[] for m,p in enumerate([1,.9,1.1,1.05],1): net=Decimal('1000')/Decimal('1.01');fee=Decimal('1000')-net rows.append({'month':m,'nav':p,'debit':1000,'net':float(net),'fee':float(fee),'units':float(net)/p}) out['DJ009']={'rows':rows,'total_debit':4000,'total_net':sum(x['net'] for x in rows),'total_fee':sum(x['fee'] for x in rows),'units':sum(x['units'] for x in rows),'end_value':sum(x['units'] for x in rows)*1.1,'wrong_value':sum(1000/x['nav'] for x in rows)*1.1} out['DJ010']={'cases':[{'n':n,'rate':rate,'end':sum(1000*(1+rate)**k for k in range(n)),'wrong':sum(1000*(1+rate)**(k+1) for k in range(n))} for n,rate in [(0,.01),(1,.01),(2,.01),(2,0)] ]} f=[('2026-01-01',-10000),('2026-07-01',-3000),('2026-07-01',1000),('2027-01-01',13500)];g=[('2026-01-01',-10000),('2026-07-01',-2000),('2027-01-01',13500)] rt=root(f);out['DJ011']={'split':f,'net':g,'rate':rt,'split_residual':npv(rt,f),'net_residual':npv(rt,g),'shifted_rate':root([('2026-01-01',-10000),('2026-07-01',-3000),('2026-07-02',1000),('2027-01-01',13500)])} a=[('2026-01-01',-10000),('2026-07-01',4000),('2027-01-01',6600)];b=[('2026-07-01',-4000),('2027-01-01',4400)];total=[('2026-01-01',-10000),('2027-01-01',11000)] out['DJ012']={'a':a,'b':b,'total':total,'a_rate':root(a),'b_rate':root(b),'total_rate':root(total),'one_sided_error':root([('2026-01-01',-10000),('2026-07-01',4000),('2027-01-01',11000)])} out['DJ013']={'opening':-10000,'cash':500,'units':800,'price':13,'terminal':500+800*13,'rate':root([('2026-01-01',-10000),('2027-01-01',10900)]),'omit_cash_rate':root([('2026-01-01',-10000),('2027-01-01',10400)]),'old_price':12.5,'old_terminal':500+800*12.5,'double_cash_rate':root([('2026-01-01',-10000),('2027-01-01',11400)])} d0=date(2024,1,1);d1=date(2025,1,1);days=(d1-d0).days out['DJ014']={'dates':[str(d0),str(d1)],'days':days,'exponent':days/365,'rate':1.1**(365/days)-1,'calendar_rate':.1,'npv_at_10pct':-10000+11000/1.1**(days/365),'short_days':(date(2024,3,1)-date(2024,2,28)).days,'short_rate':1.001**(365/2)-1} f=[('2025-01-01',-100),('2026-01-01',230),('2027-01-01',-132)] out['DJ015']={'flows':f,'guesses':{str(g):newton(f,g) for g in [.05,.15,.3,.8]},'roots':[root(f,.05,.15),root(f,.15,.3)],'curve':[{'rate':r,'xnpv':npv(r,f)} for r in [0,.05,.1,.15,.2,.25,.3,.5]]} no=[('2025-01-01',-100),('2026-01-01',50),('2027-01-01',-100)];yes=[('2026-01-01',-100),('2027-01-01',110)] out['DJ016']={'no_root':no,'has_root':yes,'scan':[{'rate':r,'no_root_npv':npv(r,no),'has_root_npv':npv(r,yes)} for r in [-.9,-.5,0,.1,.5,1,10]],'failed_newton':newton(yes,10),'recovered_root':root(yes,-.5,.5)} out['DJ016']['discriminant']=50**2-4*100*100 out['DJ017']={'raw':[{'date':'2026-01-01','amount':'-10000','python_type':'str'},{'date':'2027-01-01','amount':'11000','python_type':'str'}],'normalized':[{'date':str(date.fromisoformat(d)),'amount':float(c),'elapsed_days':(date.fromisoformat(d)-date(2026,1,1)).days} for d,c in [('2026-01-01',-10000),('2027-01-01',11000)]],'rate':root([('2026-01-01',-10000),('2027-01-01',11000)]),'ambiguous_days':(date(2026,3,4)-date(2026,4,3)).days} raw_csv='date,amount\n2026-01-01,-10000\n2027-01-01,11000\n' parsed=list(csv.DictReader(io.StringIO(raw_csv))) out['DJ017']['csv_original']=raw_csv out['DJ017']['csv_raw_types']=[{'date':type(x['date']).__name__,'amount':type(x['amount']).__name__} for x in parsed] out['DJ017']['csv_normalized_types']=[{'date':type(date.fromisoformat(x['date'])).__name__,'amount':type(float(x['amount'])).__name__} for x in parsed] f=[('2026-01-01',-10000),('2026-04-01',-2000),('2026-09-01',1000),('2027-01-01',12500)];rt=root(f) out['DJ018']={'flows':f,'rate':rt,'rows':[{'date':d,'amount':c,'days':(date.fromisoformat(d)-date(2026,1,1)).days,'discounted':c/(1+rt)**((date.fromisoformat(d)-date(2026,1,1)).days/365)} for d,c in f],'residual':npv(rt,f),'rounded_rate':round(rt,4),'rounded_residual':npv(round(rt,4),f),'wrong_residual':npv(.1,f),'scaled_rate':root([(d,c*100) for d,c in f])} f=[('2026-01-01',-1000*7),('2026-07-01',-500*7.2),('2027-01-01',1600*6.8)];usd=[('2026-01-01',-1000),('2026-07-01',-500),('2027-01-01',1600)] out['DJ019']={'usd':usd,'rates':[7,7.2,6.8],'base_flows':f,'base_rate':root(f),'usd_rate':root(usd),'flat_fx_rate':root([(d,c*7) for d,c in usd]),'wrong_mixed_rate':root([('2026-01-01',-1000),('2026-07-01',-500),('2027-01-01',10880)])} dates=['2026-01-01','2026-02-01','2026-03-01'] first_period_balance=1000*1.1 small_terminal=(first_period_balance+100)*.9 large_terminal=(first_period_balance+10000)*.9 small=[(dates[0],-1000),(dates[1],-100),(dates[2],small_terminal)] large=[(dates[0],-1000),(dates[1],-10000),(dates[2],large_terminal)] out['DJ020']={'dates':dates,'first_period_balance':first_period_balance,'small_after_addition':first_period_balance+100,'large_after_addition':first_period_balance+10000,'small_flows':small,'large_flows':large,'small_terminal':small_terminal,'large_terminal':large_terminal,'small_rate':root(small),'large_rate':root(large),'month1_annual':1.1**(365/31)-1,'month2_annual':.9**(365/28)-1,'linked_period_return':1.1*.9-1,'small_year_end_rate':root([(dates[0],-1000),(dates[1],-100),('2027-01-01',small_terminal)]),'large_year_end_rate':root([(dates[0],-1000),(dates[1],-10000),('2027-01-01',large_terminal)])} target=Path(__file__).with_suffix('.json') target.write_text(json.dumps({'as_of':'2026-10-23','notice':'全部为构造算例,非历史或实盘;日数由datetime计算;金额展示前保留全精度。','articles':out},ensure_ascii=False,indent=2),encoding='utf-8') print(target) for key,value in out.items(): print(key,json.dumps(value,ensure_ascii=False))