"""R11构造借款底稿:仅用标准库复算,借款人收到为正、付出为负。""" from datetime import date, timedelta from decimal import Decimal, ROUND_HALF_UP from pathlib import Path import json import math import re import sys HERE = Path(__file__).resolve().parent START = '2026-11-01' def cash(x): return float(Decimal(str(x)).quantize(Decimal('.01'), rounding=ROUND_HALF_UP)) def month(n): y, m = divmod(2026 * 12 + 10 + n, 12) return date(y, m + 1, 1).isoformat() def flow(day, amount, event): return {'date': day, 'amount': cash(amount), 'event': event} def npv(flows, r): base = min(date.fromisoformat(f['date']) for f in flows) return math.fsum(f['amount'] / (1+r)**((date.fromisoformat(f['date'])-base).days/365) for f in flows) def rate(flows): assert any(f['amount']>0 for f in flows) and any(f['amount']<0 for f in flows) assert len({f['date'] for f in flows}) > 1 lo, hi = -.9, 10. fl = npv(flows, lo) assert fl*npv(flows, hi)<0, '示例求根区间没有变号' for _ in range(180): mid = (lo+hi)/2 fm = npv(flows, mid) if fl*fm <= 0: hi=mid else: lo, fl=mid, fm root = (lo+hi)/2 residual = npv(flows, root) assert abs(residual) < .000001 return {'xirr_pct': root*100, 'xnpv_residual_yuan': residual, 'relative_residual': abs(residual)/sum(abs(f['amount']) for f in flows), 'basis': '实际天数/固定365天;有限区间二分所得一个根,不宣称枚举所有根或监管口径认定'} def periodic(p, payment, n): lo, hi=0., 1. for _ in range(180): r=(lo+hi)/2 v=sum(payment/(1+r)**k for k in range(1,n+1))-p if v>0: lo=r else: hi=r r=(lo+hi)/2 assert abs(sum(payment/(1+r)**k for k in range(1,n+1))-p)<.000001 return {'monthly_irr_pct':r*100, 'period_rate_times_12_pct':r*1200, 'annual_compound_pct':((1+r)**12-1)*100, 'periodic_residual_yuan':sum(payment/(1+r)**k for k in range(1,n+1))-p} def run(): out={} # 091:每期按原始本金收费,先扣一笔费用。 rows=[] for k in range(1,13): rows.append({'period':k,'date':month(k),'opening_principal':24000-(k-1)*2000, 'principal_paid':2000,'fee_base':24000,'fee':96,'payment':2096, 'closing_principal':24000-k*2000}) flows=[flow(START,23760,'名义本金24000减已确认直接费用240')]+[flow(r['date'],-r['payment'],'分期还款') for r in rows] out['DJ2-091']={'inputs':{'nominal_principal':24000,'upfront_direct_fee':240,'monthly_fee_pct':.4,'months':12}, 'rows':rows,'flows':flows,'results':{'net_available':23760,'total_period_payments':25152,'total_cost':1392, 'fee_rate_times_12_pct':4.8,'sum_opening_principals':156000,**periodic(23760,2096,12),**rate(flows)}, 'without_upfront':{**periodic(24000,2096,12),'total_cost':1152}} # 092:退款只按本构造合同的未履行期数×80%,结清费100。 cases=[] for k in (3,6,9,12): refund=0 if k==12 else 1200*(12-k)/12*.8 settlement_fee=0 if k==12 else 100 balance=24000-2000*k fs=[flow(START,22800,'扣除1200预付费用后到账')] fs += [flow(month(i),-2000,'正常本金') for i in range(1,k+1)] if balance: fs.append(flow(month(k),-balance-settlement_fee+refund,'余额结清并同期退款')) cases.append({'period':k,'settlement_date':month(k),'paid_principal':2000*k,'balance_after_normal':balance, 'unexpired_fee':1200*(12-k)/12,'refund':refund,'settlement_fee':settlement_fee, 'extra_net_settlement':balance+settlement_fee-refund,'total_historical_cost':1200+settlement_fee-refund, 'future_cash_saving_after_normal_payment':refund-settlement_fee,'flows':fs,**rate(fs)}) delayed=[dict(f) for f in cases[1]['flows']] delayed[-1]['amount']=cash(delayed[-1]['amount']-480) delayed.append(flow((date.fromisoformat(month(6))+timedelta(days=7)).isoformat(),480,'退款晚7天到账')) out['DJ2-092']={'inputs':{'nominal_principal':24000,'upfront_fee':1200,'months':12,'monthly_principal':2000, 'refund_unexpired_fraction':.8,'early_settlement_fee':100},'cases':cases, 'refund_delayed_7_days':{'flows':delayed,'gross_extra_cash_needed':12100,'refund':480,**rate(delayed)}} # 093:六期先息后本,储备为内部用途分配。 rows=[] for k in range(1,7): rows.append({'period':k,'date':month(k),'principal_due':60000 if k==6 else 0,'interest_due':300, 'debt_payment':60300 if k==6 else 300,'reserve_added':10000,'reserve_before_redemption':10000*k, 'reserve_after_redemption':0 if k==6 else 10000*k,'salary_budget_needed':10300, 'delayed_start_reserve':max(0,k-1)*10000}) fs=[flow(START,60000,'借款到账')]+[flow(r['date'],-r['debt_payment'],'利息及到期还本') for r in rows] out['DJ2-093']={'inputs':{'principal':60000,'months':6,'monthly_interest':300,'monthly_reserve':10000, 'reserve_interest':0,'initial_reserve':0},'rows':rows,'flows':fs, 'results':{'total_interest':1800,'total_debt_payments':61800,'late_start_shortfall':10000, 'only_8000_each_month_shortfall':12000,**rate(fs)}} # 094:车价差和贷款融资成本分别记录。 rows=[] for k in range(1,13): rows.append({'period':k,'date':month(k),'regular_payment':6000,'regular_principal':5000,'financing_fee':1000, 'tail_principal':60000 if k==12 else 0,'total_payment':66000 if k==12 else 6000, 'closing_principal':0 if k==12 else 120000-k*5000,'tail_reserve_before_payment':5000*k}) fs=[flow(START,117000,'120000名义本金减3000直接融资费')]+[flow(r['date'],-r['total_payment'],'月供及尾款') for r in rows] out['DJ2-094']={'inputs':{'cash_car_price':180000,'finance_car_price':176000,'down_payment':56000, 'loan_principal':120000,'upfront_fee':3000,'tail':60000,'months':12,'regular_payment':6000},'rows':rows,'loan_flows':fs, 'results':{'initial_cash_out':59000,'regular_payments_total':72000,'total_repayments':132000, 'financing_cost':15000,'purchase_cash_total':191000,'cash_vs_finance_difference':11000, 'omitting_tail_false_total':131000,'monthly_tail_reserve':5000,'monthly_budget_including_reserve':11000,**rate(fs)}} # 095:相同保障的构造报价,额外费用为两年、实际日期折现。 cash_rows=[{'date':START,'event':'裸车','cash':180000,'finance':52000}, {'date':START,'event':'首年同等保障保费','cash':5000,'finance':6500}, {'date':START,'event':'贷款服务费','cash':0,'finance':4000}] cash_rows += [{'date':month(k),'event':'分期本金','cash':0,'finance':10000} for k in range(1,13)] cash_rows += [{'date':month(12),'event':'次年同等保障保费','cash':5000,'finance':6500}] for r in cash_rows: days=(date.fromisoformat(r['date'])-date.fromisoformat(START)).days r['days']=days r['cash_pv_3pct']=r['cash']/1.03**(days/365) r['finance_pv_3pct']=r['finance']/1.03**(days/365) pv=[] for disc in (0,.03,.06): c=sum(r['cash']/(1+disc)**(r['days']/365) for r in cash_rows) f=sum(r['finance']/(1+disc)**(r['days']/365) for r in cash_rows) pv.append({'discount_pct':disc*100,'cash_pv':c,'finance_pv':f,'cash_minus_finance_pv':c-f}) out['DJ2-095']={'inputs':{'cash_car_price':180000,'finance_car_price':172000,'loan_principal':120000, 'finance_discount':8000,'service_fee':4000,'cash_annual_insurance':5000,'finance_annual_insurance':6500, 'insurance_years':2,'discount_rate_pct':3},'rows':cash_rows,'present_values':pv, 'results':{'cash_total':190000,'finance_total':189000,'incremental_insurance':3000,'net_nominal_advantage':1000}} # 096:已支付期末作为决策起点,历史与未来拆分。 fs=[flow(START,36000,'分期购买力视为融资流入')]+[flow(month(k),-3180,'正常本金与费用') for k in range(1,5)] fs += [flow(month(4),-24480,'正常付款后额外结清')] full=[flow(START,36000,'分期购买力')]+[flow(month(k),-3180,'全期付款') for k in range(1,13)] out['DJ2-096']={'inputs':{'principal':36000,'monthly_principal':3000,'monthly_fee':180,'months':12, 'paid_periods':4,'waived_remaining_fee':1080,'settlement_charge':120,'current_cash':30000,'emergency_floor':10000}, 'reconciliation':{'paid_principal':12000,'paid_fees':720,'remaining_principal':24000,'scheduled_remaining_fees':1440, 'waived':1080,'payable_remaining_fee':360,'settlement_charge':120,'extra_settlement_payment':24480, 'continue_future_payments':25440,'future_saving':960,'total_historical_cost_early':1200,'full_cost':2160, 'cash_after_settlement':5520,'allowed_cash_above_floor':20000,'cash_floor_shortfall':4480}, 'early_flows':fs,'full_flows':full,'early_rate':rate(fs),'full_rate':rate(full)} # 097:合同日起算与实际到账日起算只变一行。 cases=[] for receipt in (START,'2026-11-11','2026-11-21'): fs=[flow(receipt,24000,'实际可用到账')]+[flow(month(k),-2100,'相同约定还款') for k in range(1,13)] cases.append({'receipt_date':receipt,'first_payment_date':month(1), 'first_gap_days':(date.fromisoformat(month(1))-date.fromisoformat(receipt)).days, 'total_cost':1200,'flows':fs,**rate(fs)}) out['DJ2-097']={'inputs':{'contract_date':START,'principal':24000,'payment':2100,'periods':12},'cases':cases, 'equal_period_reference':periodic(24000,2100,12)} # 098:按前次事件后余额实际日计息,每段收息一次。 events=[(START,30000,0),('2026-11-16',20000,0),('2026-12-01',0,10000), ('2026-12-21',10000,0),('2027-01-10',0,25000),('2027-02-01',0,25000)] rows=[]; balance=0; prev=date.fromisoformat(START); fs=[] for day,draw,repay in events: days=(date.fromisoformat(day)-prev).days interest=cash(Decimal(balance)*Decimal('.06')*days/365) before=balance balance += draw-repay assert balance>=0 row={'date':day,'days_since_previous':days,'interest_base':before,'draw':draw,'principal_repaid':repay, 'interest':interest,'closing_balance':balance,'net_cash':cash(draw-repay-interest)} rows.append(row);fs.append(flow(day,row['net_cash'],'提款、还本及分段结息'));prev=date.fromisoformat(day) assert balance==0 and sum(r['draw'] for r in rows)==sum(r['principal_repaid'] for r in rows)==60000 out['DJ2-098']={'inputs':{'credit_limit':100000,'annual_simple_rate_pct':6,'day_basis':365, 'interest_rounding':'每事件区间末期四舍五入到分','unused_limit_fee':0},'rows':rows,'flows':fs, 'results':{'total_draws':60000,'total_principal_repaid':60000,'peak_balance':max(r['closing_balance'] for r in rows), 'total_interest':cash(sum(r['interest'] for r in rows)),'wrong_full_limit_interest':cash(100000*.06*92/365), 'span_days':92,**rate(fs)}} # 为说明“每次提款”另建先进先还的构造归属账;不冒充机构还款分配政策。 lots=[]; allocation=[] for r in rows: active=[lot for lot in lots if lot['balance']] assigned=0 for j,lot in enumerate(active): part=cash(r['interest']-assigned) if j==len(active)-1 else cash(Decimal(str(r['interest']))*lot['balance']/r['interest_base']) lot['interest']=cash(lot['interest']+part);assigned=cash(assigned+part) allocation.append({'end_date':r['date'],'draw_id':lot['id'],'interest_base':lot['balance'],'interest_allocated':part}) remaining=r['principal_repaid'] for lot in lots: paid=min(lot['balance'],remaining);lot['balance']-=paid;remaining-=paid assert remaining==0 if r['draw']: lots.append({'id':chr(65+len(lots)),'date':r['date'],'draw':r['draw'],'balance':r['draw'],'interest':0}) assert all(lot['balance']==0 for lot in lots) assert cash(sum(lot['interest'] for lot in lots))==out['DJ2-098']['results']['total_interest'] out['DJ2-098']['fifo_allocation']=allocation out['DJ2-098']['draw_costs_fifo']=lots out['DJ2-098']['inputs']['repayment_allocation']='仅本例构造先进先还,分段利息先汇总四舍五入,再按本金分配,末批承担分位尾差' # 099:原始基数、还款前剩余基数、还款后剩余基数并列。 rows=[] for k in range(1,7): opening=18000-(k-1)*3000;closing=18000-k*3000 rows.append({'period':k,'date':month(k),'opening_principal':opening,'principal_paid':3000,'closing_principal':closing, 'original_base_fee':90,'opening_base_fee':cash(opening*.005),'closing_base_fee':cash(closing*.005), 'original_base_payment':3090,'opening_base_payment':cash(3000+opening*.005)}) fs1=[flow(START,18000,'本金到账')]+[flow(r['date'],-r['original_base_payment'],'原始本金计费') for r in rows] fs2=[flow(START,18000,'本金到账')]+[flow(r['date'],-r['opening_base_payment'],'期初剩余本金计费') for r in rows] out['DJ2-099']={'inputs':{'principal':18000,'monthly_principal':3000,'periods':6,'monthly_fee_pct':.5},'rows':rows, 'results':{'original_total_fee':540,'opening_total_fee':315,'closing_total_fee':225,'original_minus_opening':225, 'opening_minus_closing':90},'original_flows':fs1,'opening_flows':fs2, 'original_rate':rate(fs1),'opening_rate':rate(fs2)} assert len(out)==9 for item in out.values(): item['disclosure']='全部金额、日期、退款、优惠、保险和收费条款为已确认条件的教学构造,不代表真实机构政策。' return out def expected_tables(d): """按计算字段独立建立正文表格的数值序列,不读取正文反推任何结果。""" t={} a=d['DJ2-091'];r=a['results'] t['DJ2-091']=[[[x[k] for k in ('period','date','opening_principal','principal_paid','fee','payment','closing_principal')] for x in a['rows']], [[r['fee_rate_times_12_pct']],[r['monthly_irr_pct']],[r['period_rate_times_12_pct']],[r['annual_compound_pct']],[365,r['xirr_pct']]]] a=d['DJ2-092'] t['DJ2-092']=[[[x[k] for k in ('period','settlement_date','balance_after_normal','unexpired_fee','refund','settlement_fee','extra_net_settlement','total_historical_cost','xirr_pct')] for x in a['cases']], [[START,22800,24000,1200],['2026-12-01至2027-04-01',-2000],['2027-05-01',-2000],['2027-05-01',-12100,12000,100],['2027-05-01',480,11620]]] a=d['DJ2-093'] t['DJ2-093']=[[[x[k] for k in ('period','date','interest_due','principal_due','reserve_added','reserve_before_redemption','reserve_after_redemption','salary_budget_needed')] for x in a['rows']], [[10000,6,60000,0],[10000,5,50000,10000],[8000,6,48000,12000]]] a=d['DJ2-094'];r=a['results'];i=a['inputs'] t['DJ2-094']=[[[x[k] for k in ('period','date','regular_principal','financing_fee','tail_principal','total_payment','closing_principal','tail_reserve_before_payment')] for x in a['rows']], [[i['cash_car_price'],i['finance_car_price']],[i['cash_car_price'],i['down_payment']],[0,i['upfront_fee']],[0,r['regular_payments_total']],[0,i['tail']],[i['cash_car_price'],r['purchase_cash_total']]]] a=d['DJ2-095'] t['DJ2-095']=[[[x[k] for k in ('date','event','cash','finance','cash_pv_3pct','finance_pv_3pct')] for x in a['rows']], [[x[k] for k in ('discount_pct','cash_pv','finance_pv','cash_minus_finance_pv')] for x in a['present_values']]] a=d['DJ2-096'];r=a['reconciliation'];i=a['inputs'] t['DJ2-096']=[[[36000],[12000,3000],[720,180],[24000,36000,12000],[1440,180],[1080],[360,1440,1080],[120],[24480,24000,360,120]], [[25440,24480],[2160,1200],[365,a['full_rate']['xirr_pct'],a['early_rate']['xirr_pct']],[30000,5520],[10000,20000,20000],[4480]]] a=d['DJ2-097'] t['DJ2-097']=[[[START,24000,0,0],['2026-11-11',0,24000,0],['2026-11-21',0,0,24000]]+ [[x['date'],x['amount'],x['amount'],x['amount']] for x in a['cases'][0]['flows'][1:]], [[x[k] for k in ('receipt_date','first_payment_date','first_gap_days','total_cost','xirr_pct')] for x in a['cases']]] a=d['DJ2-098'] t['DJ2-098']=[[[x[k] for k in ('date','days_since_previous','interest_base','draw','principal_repaid','interest','closing_balance','net_cash')] for x in a['rows']], [[x[k] for k in ('end_date','draw_id','interest_base','interest_allocated')] for x in a['fifo_allocation']], [[x[k] for k in ('id','date','draw','balance','interest')] for x in a['draw_costs_fifo']]] a=d['DJ2-099'];r=a['results'] t['DJ2-099']=[[[x[k] for k in ('period','date','opening_principal','principal_paid','closing_principal','original_base_fee','opening_base_fee','closing_base_fee')] for x in a['rows']], [[x[k] for k in ('period','original_base_payment','opening_base_payment')] for x in a['rows']], [[540,315],[18000,18000],[18540,18315],[365,a['original_rate']['xirr_pct'],a['opening_rate']['xirr_pct']]]] return t def verify_article_tables(d): root=HERE.parents[4] plan=json.loads((HERE.parent/'content-plan.json').read_text('utf-8')) refs=expected_tables(d);reports={} for item in plan['articles']: if item['id'] not in refs: continue path=root/'sites/daojijun.com/src/content'/ (item['planned_path'].strip('/')+'.md') body=path.read_text('utf-8').split('---',2)[2].split('')[0] actual=[] for block in re.findall(r'(?:^\|[^\n]*\|\s*\n)+',body,re.M): lines=block.strip().splitlines()[2:] actual.append([re.findall(r'[+-]?\d+(?:\.\d+)?',line) for line in lines]) expected=refs[item['id']] assert len(actual)==len(expected),(item['id'],'表数不一致',len(actual),len(expected)) checks=[] for idx,(a,e) in enumerate(zip(actual,expected),1): assert len(a)==len(e),(item['id'],idx,'行数不一致') cells=0 for rownum,(ar,er) in enumerate(zip(a,e),1): et=[] for v in er: if isinstance(v,(int,float)): et.append(float(v)) else: et += [float(x) for x in re.findall(r'[+-]?\d+(?:\.\d+)?',str(v))] assert len(ar)==len(et),(item['id'],idx,rownum,ar,et) for av,ev in zip(ar,et): places=len(av.split('.')[1]) if '.' in av else 0 tolerance=.5*10**(-places)+1e-9 assert abs(float(av)-ev)<=tolerance,(item['id'],idx,rownum,av,ev) cells+=1 checks.append({'table':idx,'rows':len(a),'numeric_cells':cells,'passed':True, 'method':'独立计算字段逐行比对正文;仅允许正文显示精度的一半单位舍入误差'}) reports[item['id']]=checks assert len(reports)==9 return reports if __name__=='__main__': output=run() (HERE/'calc_r11.json').write_text(json.dumps(output,ensure_ascii=False,indent=2,allow_nan=False)+'\n',encoding='utf-8') print('R11:9个构造底稿、日期现金流与残差核验通过。输出:'+str(HERE/'calc_r11.json')) if '--check-articles' in sys.argv: reports=verify_article_tables(output) print('正文数值核验通过:'+str(sum(len(v) for v in reports.values()))+'张表,'+str(sum(t['numeric_cells'] for v in reports.values() for t in v))+'个数值单元。')