QuantLib · Instruments
QuantLib 债券与利率工具:从现金流到净价、久期与 DV01
债券定价不是一行 NPV():发行日、到期日、付息频率、日历、调整规则、计息约定、结算天数、折现曲线,每一项都会改变结果。本页把一只虚拟债券的 11 条真实现金流、净价/全价/应计、收益率、久期、凸性、DV01 全部跑出来,并给出把利率掉期定价跑起来时逐步暴露的三个真实报错。
工具定价链:先把工具拆成现金流,再决定用曲线还是用单一收益率折现,最后才产出价格与风险指标。本页所有数字都来自这条链的本机实测。
QuantLib 有哪些金融工具?能定价到什么程度
下表按官方类名整理,并如实标注本站的核验程度:只有债券与利率掉期链在本机跑出过价格,其余工具只核对了类名存在性。
| 工具类别 | QuantLib 类 | 本站核验程度 | 适用场景 | 注意点 |
|---|---|---|---|---|
| 固定利率债券 | FixedRateBond | 已实测定价(本页全部指标) | 国债、政金债、企业债的现金流与风险计量 | 必须自己给定日历、计息约定与结算天数 |
| 零息债券 | ZeroCouponBond | 已实测构造(未做风险指标) | 贴现债、短期融资券 | 到期一次还本,应计逻辑与附息债不同 |
| 浮动利率债券 | FloatingRateBond | 只核对类名 | 挂钩基准利率的浮息债 | 需要基准指数与预测曲线,否则定价会抛缺失定盘价 |
| 利率掉期 | VanillaSwap | 已实测定价(含三个真实报错) | 互换报价与曲线自洽性检验 | 需要指数定盘价 + 预测曲线,见本页实测报错链 |
| 互换期权 | Swaption + JamshidianSwaptionEngine | 已实测定价(利率模型页) | 利率波动率交易的定价与风险 | 结果依赖模型(Hull-White 参数),不是无模型价格 |
| 信用违约互换 | CreditDefaultSwap | 只核对类名,本站未实测价格 | 信用风险定价与保护成本计量 | 需要违约概率曲线与回收率假设,缺一不可 |
| 可转债 | ConvertibleFixedCouponBond / ConvertibleZeroCouponBond | 只核对类名,本站未实测价格 | 转债的股债混合定价 | 需要股价过程与转股条款,模型选择影响大 |
| 上限/下限、回望等 | CapFloor 等 | 只核对类名 | 利率上下限类结构 | 引擎选择与波动率曲面口径必须写清 |
债券定价六步:每一步都有可核对的产物
下面六步是本站实际执行的顺序。每步都给出真实代码与预期产物,最后一步的数值应当与本页表格一致。
1. 固定估值日
先设ql.Settings.instance().evaluationDate。本页用 2024-01-15;不设的话,价格会随运行当天变化,无法对账。2. 建日历与 Schedule
ql.Schedule(发行日, 到期日, ql.Period(ql.Annual), cal, ql.ModifiedFollowing, ql.ModifiedFollowing, ql.DateGeneration.Forward, False);实测显示本页债券的付息日会出现 1/15、1/16、1/17 三种落点,就是调整规则在起作用。3. 建债券并指定应计计息约定
ql.FixedRateBond(2, 100.0, sched, [0.03], ql.ActualActual(ql.ActualActual.ISDA))。第一个参数是结算天数(T+2),它会同时影响应计利息与折现起点。4. 指定价引擎
两条路:收益率的ql.BondFunctions.*,或曲线的 ql.DiscountingBondEngine(...)。本页实测中,不设引擎直接调用 bond.cleanPrice() 会抛 RuntimeError: null pricing engine。5. 取价格与应计
实测输出:净价 99.75408467、全价 99.77047812、应计 0.01639344,且净价 + 应计 = 全价(差为 0)。6. 算风险指标
由净价反解收益率 3.0454534%,再用ql.BondFunctions.duration / convexity / basisPointValue 得修正久期、凸性与 BPV;注意 basisPointValue 返回负值,见下页说明。11 条真实现金流:为什么每期票息金额不一样
这是本页最容易忽略、也最能说明问题的一张表:同一只 3% 票息的债券,各期票息金额并不相同。原因不是数据错,而是 Actual/Actual (ISDA) 在每个付息期间给出的年化分数不同;同时可以看到营业日调整把 1/15 推到了 1/16、1/17。
| 付息日(实测) | 金额(每 100 面值) | 与名义 3.0 的差 | 该期发生了什么 |
|---|---|---|---|
| 2021-01-15 | 3.00031439 | +0.00031439 | 首期,2020 为闰年,年化分数略大于 1 |
| 2022-01-17 | 3.01643836 | +0.01643836 | 1/15 是周六,顺延到 1/17;区间含闰日 |
| 2023-01-16 | 2.99178082 | −0.00821918 | 1/15 是周日,顺延到 1/16 |
| 2024-01-15 | 2.99146643 | −0.00853357 | 区间跨越 2023/2024 两个年度 |
| 2025-01-15 | 3.00031439 | +0.00031439 | 回到正常落点 |
| 2026-01-15 | 3.00000000 | 0.00000000 | 整年,分数恰好为 1 |
| 2027-01-15 | 3.00000000 | 0.00000000 | 整年 |
| 2028-01-17 | 3.01607905 | +0.01607905 | 1/15 顺延;区间含 2028 闰日 |
| 2029-01-15 | 2.98392095 | −0.01607905 | 与上一期互补 |
| 2030-01-15 | 3.00000000 | 0.00000000 | 末期票息 |
| 2030-01-15 | 100.00000000 | — | 本金偿还(同一日两笔现金流) |
八项指标一次看全:价格、收益率、久期、凸性、DV01
同一只债券在 2024-01-17 结算日的实测指标。把它们放在一起看,才能发现「同一天、同一只债」在不同折现口径下会给出不同的久期。
| 指标 | 实测值 | 计算口径 | 注意点 |
|---|---|---|---|
| 净价 clean price | 99.75408467 | 现金流折现后扣掉应计 | 报价习惯用净价,但不是结算金额 |
| 全价 dirty price | 99.77047812 | 净价 + 应计,等于结算金额口径 | 与折现的现值口径一致 |
| 应计利息 | 0.01639344 | 上一付息日至结算日按 A/A ISDA 计息 | T+0 时为 0;随结算日线性增长 |
| 由净价反解收益率 | 3.0454534% | 年化复利、年付(ql.Compounded, ql.Annual) | 同一价格换成半年复利口径会得到不同数字 |
| 修正久期 | 5.40899919 | 在债券自身收益率(3.0455%)处计算 | 久期是收益率与计息约定的函数,不是常数 |
| 麦考利久期 | 5.57372774 | 同一收益率处计算 | 与修正久期差一个 (1+y/f) 因子 |
| 凸性 | 35.81120973 | 在债券自身收益率处计算 | 换成 3.00% 收益率计算得 35.84789852,不是同一个数 |
| BPV(库函数返回值) | −0.05396566 | ql.BondFunctions.basisPointValue 直接返回 | 本机实测带负号,市场口径应取绝对值 |
| DV01(市场口径,每 100 面值) | 0.05396585 | (P(y−1bp) − P(y+1bp)) / 2 | 收益率 +1bp → 净价 99.70013669;−1bp → 99.80806839 |
折现用曲线还是用单一收益率?两者给出不同的价格体系
债券有两条定价路径:一条用单一收益率(BondFunctions 系列),一条用曲线折现(DiscountingBondEngine)。下表是同一只债券在不同曲线下的实测结果——最后一行说明了「为什么单一收益率复现不了曲线价」。
| 折现方式 | 净价 | 全价 | 由净价反解的单一收益率 | DV01(每 100 面值) | 说明 |
|---|---|---|---|---|---|
| 平曲线 2% | 105.48014237 | 105.49653581 | 2.020134% | 0.05777747 | 曲线水平低于票息,债券溢价 |
| 平曲线 3% | 99.75408467 | 99.77047812 | 3.0454534% | 0.05396585 | 基准情形 |
| 平曲线 4% | 94.35189611 | 94.36828955 | 4.08107742% | 0.05040818 | 折价;收益率高于票息 |
| 递增零息曲线(2.65% → 7.15%,31 个节点) | 97.69124475 | 97.70763819 | 3.43261594% | 0.05260284 | 曲线有斜率:反解出的单一收益率无法复现同一价格 |
利率掉期定价:三个真实报错,一条可通行的路径
把 5 年期 Payer 掉期(固定 4.5% 年付 30/360 对 USD-6M 浮动)在本机跑通的过程里,QuantLib 依次抛出三个报错。它们不是 bug,而是掉期定价必须补齐的三件输入。
| 步骤 | 缺少什么 | 实测报错原文 | 修复动作 |
|---|---|---|---|
| A | 指数定盘价与预测曲线都没有 | RuntimeError: 2nd leg: Missing USD-6M6M Actual/360 fixing for January 11th, 2024 | 为指数补上历史定盘价(浮动腿的起点要能确定) |
| B | 有定盘价,但指数没有预测曲线 | RuntimeError: 2nd leg: null term structure set to this instance of USD-6M6M Actual/360 | 给指数传入预测曲线句柄,浮动腿才能外推未来定盘价 |
| C | 两者齐备 | 不再报错,可得到价格 | 设置折现引擎后取 NPV / fairRate / BPS 等量 |
| 输出量 | Payer 固定 4.5% | Payer 固定 3.0% | 说明 |
|---|---|---|---|
| NPV | −5.36573658(100 名义) | +1.49489970 | 固定利率高于均衡利率时为负 |
| fairRate | 3.3268428% | 3.3268428% | 与固定利率无关,由曲线和合约结构决定 |
| fixed leg NPV | −20.58190885 | −13.72127256 | 固定腿现值随固定利率变化 |
| floating leg NPV | +15.21617226 | +15.21617226 | 浮动腿不受固定利率影响 |
| fixed leg BPS | −0.04573758 | −0.04573758 | 固定腿 1bp 价值 |
| floating leg BPS | +0.04642366 | +0.04642366 | 两腿 BPS 不相等,差额来自计息约定差异 |
中国债券口径:能用什么、必须自己补什么
中国市场相关能力必须分开看:日历可以直接用,计息口径要自己选,估值规则与数据源要自己补。下表是本机实测的边界。
| 环节 | QuantLib 1.43 提供什么 | 本站实测结果 | 你必须自己补什么 |
|---|---|---|---|
| 交易日历 | ql.China(ql.China.SSE) / ql.China(ql.China.IB) | 名字分别为 Shanghai stock exchange / China inter bank market;2024 年非营业日 SSE 124 天、IB 115 天 | 与当年实际放假安排核对(调休、临时休市) |
| 计息约定 | ActualActual 枚举:ISDA / Bond / Historical / AFB / Euro / Actual365 / ISMA | 枚举里没有 China 口径 | 按发行文件选定(国债常用 A/A 或 A/365,须逐只确认) |
| 债券现金流 | Schedule + Calendar + DayCounter 可完整搭建 | CGB 口径债券(2.75% 半年付、SSE、A/A ISDA、T+1)实测净价 98.51481374、应计 0.00751366、收益率 3.02261292%、修正久期 5.48254592 | 付息频率、闰年规则、净价/全价口径按发行文件核对 |
| 银行间 vs 交易所 | 两套日历(IB / SSE) | 2024 年非营业日相差 9 天(115 vs 124) | 按你的结算场所选日历 |
| 行情与中债估值 | 不提供行情、不提供中债估值规则 | 本站未测试中债数据导入(无数据授权) | 数据下载口径、字段映射、日期与计息字段需自行解析并留痕 |
| 日历是否影响价格 | 会(通过现金流日期) | 本组 1/15、7/15 付息日在 SSE 与 TARGET 下都是营业日,实测价格完全相同(差为 0) | 结论只对本组日期成立:假期边界(如 10/1 起算)必须单独验证 |
工具建模出错的排查顺序(含真实报错原文)
从本页实测的三个报错出发,总结出债券与掉期建模的排查顺序:先确认输入齐全,再确认口径一致,最后才查数值。
| 现象 / 报错 | 根因 | 下一步该查什么 |
|---|---|---|
| RuntimeError: null pricing engine | 调用了需要引擎的 cleanPrice() 却没设引擎 | 改走 BondFunctions 收益率路径,或给债券 setPricingEngine |
| AttributeError: module 'QuantLib' has no attribute 'bondYield' | 本机 1.43 没有顶层 ql.bondYield 函数 | 改用 bond.bondYield(...) 或 ql.BondFunctions.bondYield(...) |
| Missing USD-6M6M Actual/360 fixing ... | 浮动腿缺历史定盘价 | 先补定盘价,再谈价格 |
| null term structure set to this instance of ... | 指数没有预测曲线句柄 | 把曲线句柄传给指数 |
| 净价与全价差值和应计对不上 | 结算天数或应计计息约定不同 | 打印 settlementDate 与 dayCounter().name() 比对 |
| 久期/凸性与对方差在第三位 | 计算所用的收益率不同 | 确认双方用的是债券自身收益率还是曲线水平 |
| BPV 符号为负 | 库函数按自身约定返回 | 市场口径取 ×(−1) 或改用双侧 bump 定义 |
| 掉期 fairRate 不等于曲线水平 | 两腿计息约定不同 | 核对固定腿频率/计息约定与浮动腿指数约定 |
常见问题
为什么我的 DV01 是负的?
本机实测 ql.BondFunctions.basisPointValue 返回 −0.05396566,而市场口径的 DV01 是 (P(y−1bp) − P(y+1bp))/2 = 0.05396585。这不是债券方向,而是库函数的符号约定。建议统一用双侧 bump 自己定义 DV01,或在报告里注明所用口径。以官方实现为准。
YTM 和曲线折现价为什么不同?
两者是不同的价格体系:YTM 把整条现金流压成一个收益率,曲线折现按每个期限单独取折现因子。本站实测:递增零息曲线下净价 97.69124475,反解出的单一收益率为 3.43261594%,把它代回平曲线公式得不到同一价格。做对账时先统一口径,再比数字。以官方文档为准。
中国国债能不能直接用 QuantLib 定价?
框架可以,规则要自己补。实测:ql.China(ql.China.SSE) 与 ql.China(ql.China.IB) 可用(2024 年非营业日分别是 124 与 115 天),但 ActualActual 枚举里没有 China 计息口径,也没有中债估值规则与行情数据。本站的中国案例用的是虚构票息与示意曲线,真实债券必须按发行文件与市场规则逐只核对。以官方文档与你所在市场的规则为准。
为什么每期票息金额不一样?
因为计息约定按「实际天数 / 实际天数」计算,每个付息期间的年化分数不同。本站实测 11 条现金流中,票息金额在 2.98392095 到 3.01643836 之间波动,还有两期因为营业日调整把 1/15 推到了 1/16、1/17。这不是数据错误,而是 Schedule 与 DayCounter 的正常输出。以发行文件的计息条款为准。
掉期为什么跑不起来?
本机实测的报错链很明确:先是 Missing USD-6M6M Actual/360 fixing for January 11th, 2024(缺定盘价),补上后变成 null term structure set to this instance of USD-6M6M Actual/360(指数缺预测曲线),两者齐备才能定价。掉期不是「有类和曲线就能算」,浮动腿的两项输入都是必需的。详细报错目录见错误与排查页;QuantLib 与 EasyClaw 无已证实集成,这类定位需要你自己完成。
为什么同一只债券在不同工具里久期不同?
因为久期要在某个指定收益率处计算。本机实测:同一只债券在自身反解收益率(3.0454534%)处修正久期为 5.40899919,在曲线水平 3.00% 处为 5.41197919,凸性则是 35.81120973 与 35.84789852。对账时必须先对齐「用哪个收益率算的」。以官方文档为准。
CDS 和可转债本页为什么没有数字?
因为本站没有实测它们:CDS 需要违约概率曲线与回收率假设,可转债需要股价过程与转股条款,这些输入不在本页的实验范围内。写不出实测数字时,本页只给类名与核验状态,不给「看起来像结果」的示例。需要时请以官方示例与你自己市场的假设为准(QuantLib 与 EasyClaw 无已证实集成,本页不做集成暗示)。
看完这页应该先去哪?
如果要构造或校准曲线,去期限结构页;如果价格或指标对不上,先去金融约定审计页把日期与计息口径对齐;如果程序报错或结果为 0,直接去错误与排查页按报错原文定位。