QuantLib · Conventions audit

QuantLib 结果为什么不一样?金融约定逐项审计

同一只债券、同一组现金流,换一个 DayCounter、换一种日历、把 settlementDays 从 T+2 改成 T+90,价格和收益率都会变。本页把 evaluationDate、Calendar、BusinessDayConvention、DayCounter、Schedule、settlementDays、clean/dirty/accrued 七项约定逐个替换,用 QuantLib 1.43 的实测输出告诉你每一项到底改动了多少。

依据:本机 QuantLib 1.43 实测 + 官方文档覆盖约定:7 类误差量级:0.0001 ~ 0.24(每 100 元面值)
估值日evaluationDate / Settings
日历Calendar + BusinessDay
计息DayCounter
价格口径clean / dirty / accrued

四项先定、价格后算:估值日决定「算哪一天」,日历决定「现金流落在哪天」,计息约定决定「每期算多少天」,价格口径决定「报出来的是哪个价」。示意图非实盘界面。

测试版本QuantLib 1.43(PyPI wheel `quantlib-1.43-cp39-abi3-win_amd64`)Python 版本3.11.9(Windows,Python 官方 CPython)最后核验2026-10-09(全部数值为本机一次运行输出)估值日2024-01-15(`ql.Settings.instance().evaluationDate`)日历TARGET / China.SSE / China.IB / UnitedStates.NYSE 四种对照计息约定14 种 DayCounter 对照(Actual 系 / 30-360 系 / Business-252)可直接复现是(每条结论都给出参数、代码与实测输出)本站实测范围仅约定与价格口径;不含行情数据、不接实盘、不构成投资建议
实验环境QuantLib 1.43 / Python 3.11.9 / Windows
对照项14 种 DayCounter · 4 种日历
核心结论应计口径可让净价差 0.24 元
证据等级本站实测(代码与输出均在本页)
Clean vs dirty

净价、全价、应计利息为什么对不上?

债券报价习惯用净价(clean price),但真正付钱的是全价(dirty price)。QuantLib 把三者拆成三个方法,先看它们在同一只债券上的实测值,再看为什么核对时常出错。

同一只债券(票息 3%、2020-01-15 发行、2030-01-15 到期、年付、TARGET 日历、A/A ISDA、平曲线 3%)
量本机实测值(面值 100)口径说明核对时要注意
净价 cleanPrice()99.75408467不含应计利息的报价口径交易日习惯报这个价,但它不是结算金额
应计利息 accruedAmount()0.01639344上一付息日到结算日之间应计的部分随结算日每天变化,T+0 时为 0
全价 dirtyPrice()99.77047812净价 + 应计利息,等于结算金额口径现金流折现得到的是这个价
净价 + 应计99.77047812与全价完全相同(差 < 1e-10)对不上时先查 settlementDays 与 accrued 的计息约定
NPV()99.75412364引擎口径的净现值与 dirtyPrice 差一个应计利息量级,别混用
结算日 settlementDateJanuary 17th, 2024T+2 由 settlementDays=2 决定它同时决定应计天数与折现起点
核对顺序建议:先确认 evaluationDate 与 settlementDays,再看 accruedAmount() 用的是哪条应计约定,最后才比较价格。本页实测中,改应计约定会让净价在 99.7535 ~ 99.9942 之间移动,收益率在 3.0004% ~ 3.0456% 之间移动——这两组数就是「价格对不上」最常见的来源。
DayCounter

DayCounter 计息约定逐项实测:同样 182 天,能算出 0.4959 到 0.5056

同一个区间(2024-01-15 → 2024-07-15,实际 182 天),14 种 DayCounter 给出的年化分数并不相同。下表全部是本机实测输出,名字列使用 QuantLib 自己返回的名称。

实测:区间 2024-01-15 → 2024-07-15(182 天),QuantLib 1.43
DayCounter(构造方式)dayCountyearFractionQuantLib 返回的名字典型用途 / 注意点
ql.Actual360()1820.5055555556Actual/360货币市场基准;比 A/365 多算约 1.4%
ql.Actual365Fixed()1820.4986301370Actual/365 (Fixed)固收常用近似;闰年不做特殊处理
ql.Actual365Fixed(NoLeap)1810.4958904110Actual/365 (No Leap)跳过 2 月 29 日,天数会比实际少 1 天
ql.Actual366()1820.4972677596Actual/366闰年口径;与 A/A ISDA 在本区间相同
ql.ActualActual(ISDA)1820.4972677596Actual/Actual (ISDA)跨国债券与互换最常用的口径
ql.ActualActual(Bond)1820.5000000000Actual/Actual (ISMA)按期数平分;半年期正好 0.5
ql.ActualActual(Historical)1820.4972677596Actual/Actual (ISDA)本区间与 ISDA 同值,跨世纪区间才会分叉
ql.ActualActual(AFB)1820.4972677596Actual/Actual (AFB)南非/欧洲旧口径
ql.Thirty360(BondBasis)1800.500000000030/360 (Bond Basis)每月按 30 天算,日历年化常用
ql.Thirty360(European)1800.500000000030E/360 (Eurobond Basis)欧债口径
ql.Thirty360(ISDA)1800.500000000030E/360 (ISDA)German 变体与本口径同实现
ql.Thirty360(USA)1800.500000000030/360 (US)美国 NASD 口径
ql.SimpleDayCounter()1800.5000000000Simple教学用,实盘少用
ql.Business252()1250.4960317460Business/252(Brazil)按营业日计数(这里是巴西日历)
边界区间实测:月末与闰年会把「同一区间」的年化分数拉开 8% 以上(0.0767 vs 0.0833)
边界区间实际天数Actual/365 (Fixed)Actual/365 (NoLeap)Actual/Actual (ISDA)Actual/Actual (Bond)30/360 (Bond Basis)30E/360 (ISDA)
2023-08-15 → 2024-08-153661.002739731.000000001.001040501.000000001.000000001.00000000
2024-02-15 → 2025-02-153661.002739731.000000001.000336851.000000001.000000001.00000000
2024-01-31 → 2024-02-29290.079452050.076712330.079234970.083333330.080555560.08333333
2024-01-31 → 2024-03-31600.164383560.161643840.163934430.166666670.166666670.16666667
要点:不要用「都是实际天数」来判断计息结果一致。本机实测中,2024-01-31 → 2024-02-29 这 29 天,Actual/365 (NoLeap) 给 0.07671、Actual/Actual (Bond) 给 0.08333,相对差 8.6%;放在票息 3%、面值 1 亿元的债券上就是几万元的利息差。
Price impact

换计息约定,债券价格和收益率各变多少?

把同一只债券(票息 3%、年付、TARGET、平曲线 3%)的应计计息约定依次替换,其余参数完全不动,净价、票息金额、收益率的变化如下——这是「价格对不上」的第二大来源。

实测:同一只债券、同一估值日,只替换应计计息约定
应计 DayCounter应计天数首期票息金额应计利息净价全价由净价反解的收益率
Actual/Actual (ISDA)3663.016438360.0163934499.7540846799.770478123.0454534%
Actual/365 (Fixed)3663.016438360.0164383699.7687969799.785235333.0426513%
Actual/3603663.058333330.0166666799.99419065100.010857313.0003516%
30/360 (Bond Basis)3603.016666670.0166666799.7535216299.770188293.0455591%
30E/360 (Eurobond)3603.016666670.0166666799.7535216299.770188293.0455591%
读法:同一只债券在两种口径下的净价差 0.24 元(每 100 元面值),收益率差 4.5bp;换成 Actual/360 后票息金额从 3.0164 变成 3.0583,收益率掉到 3.0004%。如果对账双方各自用了不同口径,就会出现「明明是同一只债,价格差几毛、收益率差几个 bp」。
Calendar

日历与营业日调整:同一天,四个日历给出四种答案

日历决定两件事:某一天是否可交割,以及落在非营业日的日期往哪调。下表是本机实测的两种检查——同一批日期在不同日历下是否营业,以及非营业日调整后的落点。

实测 1:isBusinessDay 判定(休 = 非营业日)
日期星期TARGETChina.SSEChina.IBUS.NYSE说明
2024-01-01一休休休休四个日历都是元旦
2024-02-09五营业休营业营业除夕:SSE 休市,银行间(IB)仍营业
2024-02-12一营业休休营业春节假期内
2024-04-04四营业休休营业清明
2024-05-01三休休休营业TARGET 也休(劳动节)
2024-06-10一营业休休营业端午
2024-10-01二营业休休营业国庆:TARGET 是营业日,SSE 休市
2024-12-25三休营业营业休圣诞:中国市场正常交易
实测 2:营业日调整(adjust)落点
原始日期星期China.SSE + FollowingChina.SSE + ModifiedFollowingChina.SSE + PrecedingTARGET + Following注意点
2024-10-01二October 8th, 2024October 8th, 2024September 30th, 2024October 1st, 2024国庆连休 7 天,向前/向后调整差 8 天
2024-10-05六October 8th, 2024October 8th, 2024September 30th, 2024October 7th, 2024调休后的工作日不等于自然营业日
2024-02-11日February 19th, 2024February 19th, 2024February 8th, 2024February 12th, 2024春节调整跨度 8~11 天
2024-09-15日September 18th, 2024September 18th, 2024September 13th, 2024September 16th, 2024中秋
2024-01-31三January 31st, 2024January 31st, 2024January 31st, 2024January 31st, 2024本来就是营业日:三种约定结果相同
2024 年实测计数:China.SSE 营业日 241 天(非营业 124 天),TARGET 营业日 255 天。同一年相差 14 个可交割日,直接决定现金流能否真的在那天发生。ql.China(ql.China.SSE) 的名字是「Shanghai stock exchange」,ql.China(ql.China.IB) 是「China inter bank market」——两者假期不同,选错会错开日期。
Schedule

Schedule 生成规则实测:起点、调整约定、周期谁先谁后

Schedule 把「起止日期 + 周期 + 日历 + 调整约定」变成一串现金流日期。下面四组是本机实测的日期串,用来判断你的现金流日期为什么和报价表不一样。

实测:同一组参数只改一项,日期串如何变化
实验条件实测生成的日期串该关注的约定
起始 2024-10-01(中国国庆,SSE 休市 / TARGET 营业)+ 半年付 + SSE + ModifiedFollowingOct 8, 2024 / Apr 1, 2025 / Oct 9, 2025 / Apr 1, 2026 / Oct 8, 2026起点在假期内会被整体顺延,后续付息日跟着平移
同上,仅把日历换成 TARGETOct 1, 2024 / Apr 1, 2025 / Oct 1, 2025 / Apr 1, 2026 / Oct 1, 2026TARGET 不知道中国假期,日期看起来「干净」但与市场不符
付息日落在周六 2024-06-15 + TARGET + FollowingJun 17, 2024 / Dec 16, 2024 / Jun 16, 2025 / Dec 15, 2025 / …每个落在周末的付息日都向后顺延
同上一组,改为 UnadjustedJun 15, 2024 / Dec 15, 2024 / Jun 15, 2025 / Dec 15, 2025 / …日期保持日历上的名义日,需自行处理非营业日
春节起点 2024-02-11 + SSE + ModifiedFollowing + 半年付Feb 19, 2024 / Aug 12, 2024 / Feb 11, 2025 / Aug 11, 2025 / …首期实际长度 ≠ 半年,会影响首期利息计算
周期 1 年 > 区间 6 个月(Forward 生成)Jan 15, 2024 / Jun 17, 2024QuantLib 不报错,只按端点生成;周期设置错误不会提示你
2024-01-31 起半年付,EndOfMonth=FalseJan 31, 2024 / Jul 31, 2024 / Jan 31, 2025 / Jul 31, 2025本组日期恰好都落在月末,与 EndOfMonth=True 输出相同
2024-01-31 起半年付,EndOfMonth=TrueJan 31, 2024 / Jul 31, 2024 / Jan 31, 2025 / Jul 31, 2025月末规则只在月份天数不同时才显出差异(如 8/31 → 2/28)
诚实说明:上表最后两行在本次实验里没有差异——因为 1/31 与 7/31 在两个设置下都是月末。月末规则要到「8 月 31 日起半年付」这类情形才会与 2 月末对齐规则分叉。本站没有把它当作「有差异」来写,只如实报告:本组参数下 EndOfMonth 开关不改变输出。
settlementDays

settlementDays 与 evaluationDate:最容易被忽略的两个日期

settlementDays 决定结算日,结算日又决定应计利息与折现起点。下面这张表把同一只债券的 settlementDays 从 T+0 拉到 T+90,可以看到价格与风险指标如何被「日期」而不是「利率」推动。

实测:同一只债券,只改 settlementDays(BPV 为 BondFunctions 直接返回原值,符号见下页说明)
settlementDays结算日应计利息净价全价由净价反解的收益率修正久期BPV
0(T+0)2024-01-150.0000000099.7541236499.754123643.0454534%5.414302-0.05400972
12024-01-160.0081967299.7541038299.762300543.0454534%5.411651-0.05398769
2(默认)2024-01-170.0163934499.7540846799.770478123.0454534%5.408999-0.05396566
32024-01-180.0245901699.7540662099.778656363.0454534%5.406348-0.05394363
52024-01-220.0573770599.7539989999.811376043.0454534%5.395742-0.05385546
102024-01-290.1147541099.7539071999.868661293.0454534%5.377181-0.05370101
302024-02-260.3442623099.75386899100.098131283.0454534%5.302940-0.05308126
902024-05-231.0573770599.75711999100.814497043.0454534%5.072260-0.05113557
evaluationDate 的两条硬边界(本机实测的报错原文)import QuantLib as ql # 边界 1:曲线取值早于参考日 -> 直接报错 ts = ql.FlatForward(ql.Date(15, 1, 2024), 0.03, ql.Actual365Fixed()) ts.discount(ql.Date(1, 1, 2020)) # RuntimeError: negative time (-4.03825) given # 边界 2:evaluationDate 设到最大日期,QuantLib 内部 +结算天数后越界 ql.Settings.instance().evaluationDate = ql.Date(31, 12, 2199) # RuntimeError: Date's serial number (109575) outside allowed range [367-109574], # i.e. [January 1st, 1901-December 31st, 2199]
读法:从 T+0 到 T+90,净价只动了 0.003 元,但应计利息从 0 涨到 1.0574、全价从 99.7541 涨到 100.8145、修正久期从 5.4143 落到 5.0723。也就是说:日期决定的是「你现在站在现金流的哪个位置」,不是「利率变了多少」。
Checklist

拿到一条「对不上」的价格,按这 6 步核对

顺序很重要:先定点(日期),再点口径(日历与计息),最后才怀疑模型。以下每一步都给出本页实测过的判据。

  • 1. 先确认估值日与结算日

    打印 ql.Settings.instance().evaluationDate 与 bond.settlementDate()。若结算日跨过付息日,应计利息会归零重算,看起来就像价格突然跳了一档。
  • 2. 再确认应计利息的计息约定

    本页实测:换应计约定会让净价移动 0.24 元、收益率移动 4.5bp。核对 bond.dayCounter().name() 与对方报价单是否一致。
  • 3. 分清报的是净价还是全价

    现金结算用全价,报价习惯用净价。实测中两者差 0.0164(T+2),T+90 时差 1.0574,差额随时间线性增长。
  • 4. 换一种方式复算收益率

    用 ql.BondFunctions.bondYield(...) 与 ql.BondFunctions.cleanPrice(...) 互相反解:本机实测二者在同组参数下互为逆运算,如果对不上,说明有一侧用的不是同一组约定。
  • 5. 检查日历是否属于该市场

    中国市场用 ql.China(ql.China.SSE) 或 ql.China(ql.China.IB),不要用 TARGET 代替:实测同一日期串会错开 1~8 天。
  • 6. 最后才怀疑模型与曲线

    曲线折现与单一收益率并不等价:本站实测同一只债券在递增零息曲线下净价 97.6912,而「反解出的单一收益率」只有 3.4326%——这个数字在平曲线下无法复现同一价格。
  • Reproduce

    自己复现本页全部结论:一条命令 + 固定参数

    下面这段脚本就是本站生成上表数值的写法(去掉打印格式后的核心部分),把它存成文件、在装好 QuantLib 的环境里运行,应当得到同样的数字。约定不同(比如换了日历或计息约定)就会得到不同的数字,这正是本页要说明的事。

    conventions_check.py(QuantLib 1.43 / Python 3.11.9 实测通过)import QuantLib as ql today = ql.Date(15, 1, 2024) ql.Settings.instance().evaluationDate = today # ① 估值日 cal, dc = ql.TARGET(), ql.ActualActual(ql.ActualActual.ISDA) # ② 日历 + ③ 应计计息约定 sched = ql.Schedule(ql.Date(15, 1, 2020), ql.Date(15, 1, 2030), ql.Period(ql.Annual), cal, ql.ModifiedFollowing, ql.ModifiedFollowing, ql.DateGeneration.Forward, False) # ④ Schedule 生成规则 bond = ql.FixedRateBond(2, 100.0, sched, [0.03], dc) # ⑤ settlementDays = 2 bond.setPricingEngine(ql.DiscountingBondEngine( ql.YieldTermStructureHandle(ql.FlatForward(today, 0.03, dc)))) # ⑥ 折现曲线 print(bond.cleanPrice(), bond.dirtyPrice(), bond.accruedAmount()) # ⑦ clean / dirty / accrued y = bond.bondYield(dc, ql.Compounded, ql.Annual) # ⑧ 由净价反解收益率 print(y * 100, ql.BondFunctions.duration(bond, y, dc, ql.Compounded, ql.Annual, ql.Duration.Modified))
    复现时的四个替换开关(本页表格全部来自这些开关的不同组合)
    替换项改哪里本页实测的后果
    计息约定第 ② 行的 dc净价与收益率同时变化(净价区间 99.7535 ~ 99.9942)
    日历第 ② 行的 cal(换 SSE/IB)现金流日期错开 1~8 天;本组 1/15 日期恰好不变
    结算天数第 ⑤ 行的 2应计利息与全价变化,净价几乎不变
    估值日第 ① 行的 today整条现金流的时间坐标平移
    折现曲线第 ⑥ 行的 FlatForward(today, 0.03, dc)价格随曲线水平移动;斜率变化时单一收益率无法对应
    FAQ

    常见问题

    为什么同一只债券,我的价格和别人差 0.2 元?

    最常见的是应计计息约定不同。本站实测:应计约定从 Actual/Actual (ISDA) 换成 Actual/360,净价从 99.75408467 变成 99.99419065(差 0.24),收益率从 3.0455% 掉到 3.0004%。先对齐 dayCounter,再对齐 settlementDays,最后才怀疑曲线;以官方文档与报价单口径为准。QuantLib 与 EasyClaw 无已证实集成,这类口径核对需要你自己掌握。

    clean price 和 dirty price 哪个才是要付的钱?

    现金结算对应全价(dirty price = 净价 + 应计利息)。本站实测二者关系为 99.75408467 + 0.01639344 = 99.77047812,完全相等。报价习惯报净价,所以对账时必须先问清「报的是哪个价」。具体会计口径以你所在市场的结算规则为准。

    Actual/365 Fixed 和 Actual/Actual (ISDA) 什么时候会分叉?

    跨闰年或跨月短区间才明显。实测:2023-08-15 → 2024-08-15 这 366 天,A/365 Fixed 给 1.00273973、A/A ISDA 给 1.00104050;而 2024-01-31 → 2024-02-29 只有 29 天时,两者分别是 0.07945205 与 0.07923497,Actual/Actual (Bond) 则是 0.08333333。区间越短、月末越特殊,差异占比越大。以官方实现与市场惯例为准。

    QuantLib 里有没有中国市场的计息约定?

    没有。本机实测 ql.ActualActual 的枚举是 ISDA / Bond / Historical / AFB / Euro / Actual365 / ISMA,不含 China 口径;日历有 ql.China(ql.China.SSE) 与 ql.China(ql.China.IB)。也就是说:日历可以直接用,计息约定要你自己按市场规则选定,别默认任意一个。以官方文档为准。

    settlementDays 应该填几?

    按你所处市场的结算规则来,不要沿用示例里的 2。本站实测 T+0 到 T+90 的应计利息从 0 变到 1.05737705,修正久期从 5.414302 变到 5.072260——日期设定的影响不小于利率变动。QuantLib 不校验这个值是否合理,填错不会报错,只会让价格悄悄偏离。以你的结算制度为准。

    为什么日历换掉之后,这次实验的价格没变?

    因为本组现金流日期(每年 1/15、7/15)在 TARGET 与 SSE 下都是营业日,所以日期串完全一致,价格自然相同。本机实测的差异只出现在假期边界上(如 10/1、2/11 起算,日期会错开 1~8 天)。这也是一个提醒:没有报错、价格也没变,不代表日历选对了,要针对性构造边界日期去验证。以官方日历数据为准。

    evaluationDate 可以和 today 不一样吗?

    可以,而且应该显式设置。ql.Date.todaysDate() 在本机返回运行当天(2026-10-09),如果不设置,所有定价都以运行日为基准,回测或对账时就会得到「每天都在变」的结果。但注意边界:把 evaluationDate 设到 2199-12-31 会立即报错(内部还要加结算天数)。以官方文档为准。

    本页的数值能不能直接当成市场数据用?

    不能。本页全部是受控实验:票息 3%、曲线固定 3%、标的为虚构债券,目的是让口径差异可比较、可复现。真实债券的票息、付息频率、日历与应计规则都要按发行文件取值;结果仅供研究与对账方法参考,不构成投资建议。

    本页结论定价结果对约定的敏感度是可量化的:应计约定让净价移动 0.24 元 / 收益率移动 4.5bp,settlementDays 让应计利息从 0 变到 1.0574,日历在假期边界上让现金流日期错开 1~8 天。先定约定、再谈价格。
    适用边界所有数值来自虚构债券与固定 3% 平曲线,估值日固定 2024-01-15、环境为 QuantLib 1.43 / Python 3.11.9;真实债券需按发行文件与市场惯例取值,跨版本可能与本页数字有差异。
    风险提示本页只讨论计算口径,不涉及任何标的估值结论;结果不构成投资建议。把本页数字直接套到真实债券上会产生偏差。

    约定对上之后,下一步做什么?

    如果结果仍然异常(价格为 0、曲线不收敛、导入失败),直接进错误与排查;要把约定用到真实的曲线构造与债券定价上,去期限结构与金融工具两页。