QuantLib · Errors and pitfalls
QuantLib 报错与结果异常排查:价格为 0、曲线超界、引擎不匹配
本页不写「跑通了」的示例,只写跑不通的东西:13 类真实报错的原文、根因与修复动作,以及 10 类不报错但结果可疑的陷阱——价格为 0、折现因子大于 1、隐含波动率反解失败、绑定函数根本调用不了。所有文本都在 QuantLib 1.43 / Python 3.11.9 上逐字跑出。
排查顺序:先判断「报错」还是「静默错值」,再把约定打印出来,然后用第二种方法复算,一次只改一个变量;示意图非实盘界面。
报错速查表:症状、原文、根因、修复
下表每一行的「报错原文」都是从本机运行结果里逐字抄下来的,未做改写。如果你的报错文本与之不同,先按类型判断,再到官方仓库检索。
| 症状 | 报错原文(类型 + 消息) | 根因 | 修复动作 |
|---|---|---|---|
| 读到空价格 | RuntimeError: null pricing engine | 未 setPricingEngine() 就直接调用 bond.cleanPrice() / NPV 一类需要引擎的方法 | 先挂引擎:bond.setPricingEngine(ql.DiscountingBondEngine(yts));只想按收益率算价就用 ql.BondFunctions.cleanPrice(bond, y, dc, ql.Compounded, ql.Annual) |
| 期权定价直接失败 | RuntimeError: not an European option | 把美式期权(AmericanExercise)交给了只支持欧式的解析引擎 | 换匹配引擎(二叉树 / 有限差分),或把行权方式改成欧式;引擎与行权方式必须成对选择 |
| 取希腊字母失败 | RuntimeError: vega not provided | 树引擎(BinomialVanillaEngine)不提供 vega,MC 引擎不提供 delta | 用解析引擎或有限差分引擎取 vega;或自行对波动率做差分(本机实测差分 vega 与解析值差 3.1e-09) |
| 曲线取早期日期报错 | RuntimeError: negative time (-4.03825) given | 向曲线请求参考日之前的折现因子,时间为负 | 把估值日与曲线参考日对齐;历史数据另建一条曲线,不要向同一条曲线索要过去的时间点 |
| 问远端期限失败 | RuntimeError: time (5.075) is past max curve time (1.02222) | 曲线只喂了 1 年报价,却请求 5 年零息;QuantLib 默认不外推 | 补齐报价节点,或显式 curve.enableExtrapolation()(外推是模型假设,要自己承担) |
| 报价倒挂后取远端失败 | RuntimeError: time (2.03056) is past max curve time (1.02222) | 6M 5% → 1Y 1% 这类倒挂报价会让 bootstrap 只建到最近的那个节点就停下 | 先检查报价单调性与量级;倒挂本身不报错,但会静默把曲线截断 |
| 构造日期报错 | TypeError: Wrong number or type of arguments for overloaded function 'new_Date'. | ql.Date 传入了浮点(如 ql.Date(2024.5, 1, 1)) | 日、月、年必须都是整数;字符串日期用 ql.Date('2025-01-15', '%Y-%m-%d') |
| 期权构造报错 | TypeError: in method 'new_EuropeanExercise', argument 1 of type 'Date const &' | 把 '2025-01-15' 这类字符串直接传给 EuropeanExercise | 先转成 ql.Date:ql.EuropeanExercise(ql.Date(15, 1, 2025)) |
| 估值日设到上限日期 | RuntimeError: Date's serial number (109575) outside allowed range [367-109574], i.e. [January 1st, 1901-December 31st, 2199] | evaluationDate 设为 2199-12-31 后,内部再叠加结算天数就越界 | 把估值日留在合理区间;上限日期 ql.Date.maxDate() 只是日期类型的边界,不是可用的估值日 |
| 模型取 delta 失败 | RuntimeError: delta not provided | AnalyticHestonEngine 不提供 delta | 自己用有限差分算 delta(对现货 ±h 重定价),或换用专门的数值引擎 |
| 类不存在 | AttributeError: module 'QuantLib' has no attribute 'BinomialHestonEngine' | 该类在 1.43 的 Python 绑定里不存在 | 先用 dir(ql) 确认可用类名,不要假设 C++ 文档里的类在 Python 侧一定可用 |
| 函数不存在 | AttributeError: module 'QuantLib' has no attribute 'bondYield' | 顶层 ql.bondYield / ql.bondPrice 在 1.43 的 Python 绑定里没有 | 改用 bond.bondYield(dc, ql.Compounded, ql.Annual) 或 ql.BondFunctions.bondYield(...) |
| 绑定整体不可用 | TypeError: Wrong number or type of arguments for overloaded function 'BondFunctions_zSpread'. | BondFunctions.zSpread 的 10 个 C++ 重载在 Python 侧都不可调用(参数类型 BondPrice 为 std::pair<Real,Real>,绑定未包装) | 别在 Python 里调它:自己实现 z-spread 求解(对利差做二分或牛顿迭代,直到价格匹配) |
| 隐含波动率反解失败 | RuntimeError: negative Put price (-6.5637) implied by put-call parity. No solution exists for Call strike 80, forward 105.142, price 17.3484, deflator 0.951099 | 给出的价格与远期/行权价不满足无套利边界,反解不存在解 | 先校验价格是否落在 max(F−K,0) 与远期价格之间;再检查远期与折现因子是否与定价时一致 |
RuntimeError 但消息不同)→ 把消息整段复制到官方仓库检索,仍以官方答复与你的本机输出为准。价格为 0 的四种成因:程序不报错,结果却是空的
「价格为 0」是本页实测中最难发现的一类问题——它不抛异常,容易被当成「便宜」而不是「错了」。下表四种成因都在本机复现过,判据可以直接照用。
| 成因 | 本机实测现象 | 判据(怎么确认) | 修复动作 |
|---|---|---|---|
| 期权已到期 | 到期日 2024-01-01(早于估值日 2024-01-15)时 NPV = 0.0、delta = 0.0,isExpired 为 True,不抛异常 | 打印 option.isExpired();为 True 就是到期,不是定价问题 | 把估值日移到到期日之前,或改用到期日的实现价值(payoff)而不是引擎 NPV |
| 到期日等于估值日 | 到期日与 valuationDate 同为 2024-01-15 时同样返回 0.0,isExpired 为 True | 检查 evaluationDate 与到期日是否相差 0 天 | 至少留出 1 天剩余期限;对当日到期的头寸用实现价值口径 |
| 债券结算日被推到到期日之后 | 1 年期债券设 settlementDays=300 → 结算日 2025-03-18 晚于到期日 2025-01-15,净价 / 全价 / 应计全部为 0.0,而 isExpired 仍为 False | 比较 bond.settlementDate() 与 bond.maturityDate();isExpired 在这个场景下不可靠,不能只用它判断 | 把 settlementDays 改回市场实际结算天数(本机默认示例为 2) |
| 同一成因换档位复现 | 同只债券把 settlementDays 改成 400 → 结算日 2025-08-08,净价依然为 0.0 | 说明不是某一档参数的偶发数值问题,而是结算日越界后的稳定行为 | 多档一起试、并把结算日与到期日写进日志,比只看一个数字更容易定位 |
| 债券到期日早于估值日 | 到期日 2023-01-15(估值日 2024-01-15)时净价与应计利息都是 0.0,也不报错 | 先打印 bond.maturityDate() 与估值日,确认债券是否还有存续现金流 | 换一只仍存续的债券;已到期债券的价格要用到期日口径另行计算 |
| 现金流为空(间接表现) | 周期设置为 1 年、区间只有 6 个月时,Schedule 只生成端点日期,不报错;若后续工具依赖中间现金流,价格会出现异常小值或 0 | 打印现金流:[str(cf.date()) for cf in bond.cashflows()] | 把周期改到小于区间长度,并核对生成的付息日与发行文件一致 |
isExpired()、settlementDate()、maturityDate()。本机实测说明:isExpired 只覆盖「工具自身已到期」,覆盖不了「结算日在到期日之后」和「现金流为空」这两类,所以不能只依赖它。不报错但结果可疑:十种「安静的错误」
这一节比报错更值得警惕:程序照常返回数字,数字却没有意义。下表每一条都是本机实测的原值。
| 场景 | 本机实测返回 | 为什么可疑 | 怎么处理 |
|---|---|---|---|
| 已到期期权 | NPV = 0.0 | 返回 0 而不是报错,容易被当成正常结果 | 先判 isExpired,再谈定价 |
| 结算日晚于到期日的债券 | 净价 / 全价 / 应计 = 0.0 | isExpired 仍为 False,没有任何提示 | 比较结算日与到期日,不依赖 isExpired |
| 负利率报价 | 折现因子 = 1.255230125523013 | 折现因子大于 1 是负利率下的正常数学结果,但和直觉冲突,容易被当成 bug | 负利率环境里把 DF 的检验区间放宽到大于 1;核对报价符号 |
| 波动率设为 0 | NPV = 4.890087197528255、delta = 1.0 | 没有任何提示,delta 恒为 1 说明模型退化成了远期现金流 | 检查波动率输入链路(报价、曲面插值、日历)是否把波动率清零 |
| Heston 违反 Feller 条件 + 深度虚值 | NPV = 1.5582964397103938e-05(2κv0−σ² = −2.248) | 参数不规范仍能出价,只是数值已失去金融含义 | 先验证 2κv0 − σ² > 0;不满足时改用全隐式格式或重新校准 |
| BondFunctions 的基点价值符号 | −0.05396566 | 负号与市场口径相反;直接当 DV01 用会把风险方向写反 | 市场口径用 (P(y−1bp) − P(y+1bp))/2,本机得 0.05396585 |
| 计息约定名字与直觉不符 | ActualActual(Bond) 的名字显示为 Actual/Actual (ISMA);Thirty360(German) 显示为 30E/360 (ISDA) day counter | 按名字反查实现会走错分支;对账双方可能各自以为「同名」 | 以 dayCounter().name() 的实际输出与官方实现说明为准,不要只看构造时写的关键字 |
| 报价倒挂 | 曲线仍能构造成功,只是后续取远端期限报 max curve time | 构造阶段不报错,错误延后到取值阶段才暴露 | 构造后立刻做自洽性检查:用曲线反算输入报价的 par rate,偏差应在 1bp 量级 |
| 日历选错 | 非假期边界上价格与对照组完全相同(本机实测 1/15、7/15 付息日) | 「没报错、价格也一样」不代表日历选对了,差异只在假期边界出现 | 构造假期边界日期(如 10/1、2/11)做针对性验证 |
| 估值日未显式设置 | 以运行当天为基准(本机 ql.Date.todaysDate() 返回运行日) | 同一份脚本每天结果不同,回测与对账无法复现 | 在脚本开头固定 ql.Settings.instance().evaluationDate |
曲线与 bootstrap 类故障:从构造到取值都可能出错
曲线的问题有一半不在构造阶段,而在取值阶段。下表把本机实测的六类曲线故障按「什么时候暴露」排列。
| 现象 | 暴露阶段 | 本机实测表现 | 处理顺序 |
|---|---|---|---|
| 时间超界(未外推) | 取值阶段 | time (5.075) is past max curve time (1.02222) | ① 补报价节点 ② 显式开启外推 ③ 确认你请求的期限确实在市场可交易范围内 |
| 向参考日之前取值 | 取值阶段 | negative time (-4.03825) given | ① 检查估值日与曲线参考日是否一致 ② 历史时点另建曲线 |
| 报价倒挂导致曲线截断 | 构造阶段不报错、取值阶段报错 | 6M 5% → 1Y 1% 的报价下,请求 2 年零息时报 time (2.03056) is past max curve time (1.02222) | ① 先做报价质量检查(单调性、量级、单位是百分数还是小数) ② 再做自洽性反算 |
| 负利率报价 | 构造阶段不报错 | −20% 存款报价下折现因子为 1.255230125523013 | ① 确认负利率是否真实 ② 若真实,把 DF 检查区间放宽到大于 1 ③ 检查取对数/开方类后续步骤 |
| 插值方法带来的偏差 | 使用阶段 | 同一组报价下,4 年期折现因子的极差为 2.5723 bp(四种插值方法对照) | ① 在报价节点上结果一致是正常的,要检查节点之间 ② 定价用的期限尽量落在节点上 |
| BondFunctions.zSpread 无法调用 | 调用阶段 | TypeError: Wrong number or type of arguments for overloaded function 'BondFunctions_zSpread'.(10 个重载全部失败) | ① 先确认不是参数写错 ② 换成自己实现 z-spread 求解 ③ 记录版本与失败方式便于检索 |
+0.018 bp 到 −1.364 bp 之间;如果你看到几十 bp 的偏差,说明报价、日历或计息约定至少有一项对不上。Python 绑定缺口与命名差异:C++ 文档里有的,Python 里可能没有
这一节是本站实测中最容易被教程误导的部分。很多中文示例来自更早的版本或 C++ 文档,直接复制到 1.43 会报 AttributeError。下表是逐项实测结果。
| 对象 | 本机实测结果 | 影响 | 替代写法 |
|---|---|---|---|
ql.bondYield | AttributeError: module 'QuantLib' has no attribute 'bondYield' | 沿用旧示例的脚本直接失败 | bond.bondYield(dc, ql.Compounded, ql.Annual) 或 ql.BondFunctions.bondYield(bond, price, dc, ql.Compounded, ql.Annual) |
ql.bondPrice | 同样不存在(同为顶层函数) | 无法用函数式计算指定收益率下的价格 | ql.BondFunctions.cleanPrice(bond, y, dc, ql.Compounded, ql.Annual) |
bond.cleanPrice()(无参) | RuntimeError: null pricing engine | 不挂引擎就没有价格 | 先 setPricingEngine;或走 BondFunctions 的按收益率算价路径 |
BondFunctions.zSpread | 10 个重载在 Python 侧全部不可调用(TypeError) | z-spread 无法直接用绑定计算 | 自己实现求解:对利差迭代,直到按曲线 + 利差折现得到的价格等于目标价格 |
BondFunctions.basisPointValue | 返回 −0.05396566(负号) | 符号与市场口径相反,直接用于风险统计会反向 | 用双点差分:(P(y−1bp) − P(y+1bp))/2 = 0.05396585 |
ql.Average 枚举 | 只有 Arithmetic 与 Geometric;没有 ql.AverageType | 按 AverageType 写的亚式期权脚本报 AttributeError | 用 ql.Average.Arithmetic / ql.Average.Geometric |
BinomialHestonEngine | AttributeError(1.43 无此类) | 想做 Heston 树定价时会先撞在类名上 | 先用 dir(ql) 列出可用类;能用的 Heston 引擎是解析引擎 |
AnalyticHestonEngine.delta() | RuntimeError: delta not provided | 模型定价可以,但拿不到解析希腊字母 | 对现货做 ±h 重定价求差分 delta |
ActualActual(Bond) 的显示名 | Actual/Actual (ISMA) | 按名字回查实现容易走错分支 | 以实际输出名为准,并在对账文档里写清构造方式 |
Thirty360(German) 的显示名 | 30E/360 (ISDA) day counter | German 与 ISDA 在本机为同一实现,名字与直觉不符 | 对账时写清构造关键字,不要只写显示名 |
dir(ql) 与 help(ql.BondFunctions) 确认对象是否存在、重载长什么样;函数文档字符串里会列出全部重载签名,比凭记忆写快得多。先判断「是不是安装问题」:四条判据与指向
很多「算不出价格」的问题其实出在环境上。安装步骤不在这里重复(见安装页),这一节只给判据:怎么在 1 分钟内判断故障属于环境层还是代码层。
| 判据 | 环境层故障的表现 | 代码层故障的表现 | 下一步 |
|---|---|---|---|
| 能不能导入 | import QuantLib 直接失败(ModuleNotFoundError 或 DLL 加载失败) | 导入正常,报错发生在构造或取值阶段 | 导入失败 → 先修环境;导入正常 → 进报错速查表 |
| 版本是否可见 | 打印 QuantLib.__version__ 与预期不符 | 版本正确但行为与教程不同 | 版本不同 → 先按本页绑定缺口表核对接口;版本正确 → 查参数 |
| 最小脚本能否跑通 | 只 import + 建一个日期就失败 | 最小脚本正常,加入自己的输入后才失败 | 先把最小脚本跑通再逐段加回你的代码 |
| 平台与 wheel | Windows 上安装的是 ABI 不匹配的 wheel,或源码编译缺编译器 / Boost | 平台与 wheel 正常,只是结果不对 | 环境问题看安装页;结果问题看本页第 2、3 节 |
| 多版本共存 | 同一脚本在不同终端表现不同(虚拟环境未激活) | 任何终端表现一致 | 固定使用同一个虚拟环境,并在报告里写明版本与路径 |
| 依赖与编码 | 中文路径、非 UTF-8 默认编码导致的文件名或读取异常 | 纯计算逻辑报错 | 文件读取统一指定 encoding='utf-8';路径尽量用英文目录 |
六步通用排查流程:从复现到定位
下面的顺序是按「先便宜后昂贵」排的:先记录最小复现,再分层,最后才动模型与参数。每一步都给出可执行动作与预期输出。
1. 先把最小复现脚本固定下来
把估值日、标的、报价全部写死在脚本里。预期输出:拿到一份能稳定复现同一报错的脚本——本页的复制脚本就是按这个要求写的,去掉所有随机与时间依赖。2. 分层:是环境问题还是计算问题
先跑import QuantLib 与 print(QuantLib.__version__)。预期输出:版本号(本机 1.43)。若这一步失败,直接去安装页,不要在定价代码里找原因。3. 把约定全部打印出来再下结论
打印evaluationDate、bond.settlementDate()、bond.dayCounter().name()、bond.calendar().name()、引擎类型。预期输出:五项约定一目了然——本机实测中,价格对不上多数源于这五项里的某一项。4. 判断是「报错」还是「静默错值」
看返回值:是抛异常,还是返回 0 / 小于 1e-4 / 恒为 1 的数。预期输出:分类结论。静默错值走本页第 2、3 节,报错走第 1 节。5. 用第二种方法复算一次
解析引擎对二叉树/有限差分/蒙特卡洛,或按收益率算价对按曲线折现。预期输出:两个结果的差;本机实测同参数下 CRR 801 步与解析解差 201.75 ppm、有限差分差 37.91 ppm——量级决定你该接受还是该继续查。6. 一次只改一个变量,并记录版本
只改日历、或只改计息约定、或只改结算天数,观察结果变化。预期输出:把「改了什么、从多少变到多少、在哪台环境」写成一行记录。本机实测:仅把应计约定换成 Actual/360,净价就从 99.75408467 变到 99.99419065。错误复现脚本:把这些报错在自己机器上跑一遍
下面这段脚本可以一次触发本页的多数报错,并打印类型与消息。建议在你的环境里跑一次,把输出与「报错速查表」对照——不同版本或平台的消息可能不同,以你的输出为准。
| 复现项 | 预期看到的类型 | 本机实测消息(片段) |
|---|---|---|
| 未设引擎读价格 | RuntimeError | null pricing engine |
顶层 bondYield | AttributeError | module 'QuantLib' has no attribute 'bondYield' |
zSpread 绑定 | TypeError | Wrong number or type of arguments for overloaded function 'BondFunctions_zSpread'. |
ql.Date 传浮点 | TypeError | Wrong number or type of arguments for overloaded function 'new_Date'. |
| 到期日传字符串 | TypeError | in method 'new_EuropeanExercise', argument 1 of type 'Date const &' |
| 向曲线索要过去 | RuntimeError | negative time (-4.03825) given |
| 索取超出末端的期限 | RuntimeError | time (5.075) is past max curve time (1.02222) |
| 已到期期权(静默) | 不报错 | NPV = 0.0,isExpired = True |
常见问题
为什么程序不报错,但价格是 0?
本机实测有四种成因:期权已到期(isExpired 为 True)、到期日等于估值日、债券结算日在到期日之后(此时 isExpired 仍是 False,不要只依赖它)、以及债券已到期(到期日早于估值日)。判据是先打印 isExpired()、settlementDate()、maturityDate() 三项。报错文本与行为随版本可能变化,以你本机输出与官方文档为准。
BondFunctions.zSpread 为什么调用不了?
本机逐项试过 handle、裸曲线对象、可重链接 handle、带结算日、带计息约定、以及传 (净价, 全价) 元组六种写法,10 个重载全部返回同一个 TypeError——参数类型 BondPrice 对应 C++ 的 std::pair<Real,Real>,Python 绑定里没有可用构造。替代做法是自己写求解:对利差迭代,直到按「曲线 + 利差」折现的价格等于目标价格。该结论只对 1.43 + Python 3.11 有效,换版本请重新验证。
报错里说 vega not provided,是我的参数写错了吗?
不是参数问题,是引擎能力边界。本机实测:树引擎(BinomialVanillaEngine)与蒙特卡洛引擎不提供 vega,AnalyticHestonEngine 不提供 delta。替代方案有两条:换成提供解析希腊字母的引擎,或者自己用有限差分算——本机实测差值 delta 8.6e-09、gamma 2.9e-10、vega 3.1e-09,对多数用途已经足够。具体引擎能力以官方文档中的引擎说明为准。
曲线报 past max curve time,只能加报价吗?
两条路:补齐报价节点,或者显式调用 enableExtrapolation()。本机实测的另一种情形是报价倒挂(6M 5% → 1Y 1%),曲线在构造阶段不报错,但实际只建到最近的那个节点,于是取值时才失败——所以构造完成后应立刻做自洽性检查(用曲线反算输入报价),而不是等到定价时才看结果。外推属于模型假设,用之前要写明依据。
把估值日设成 2199-12-31 为什么报 serial number 越界?
本机实测:ql.Date.maxDate() 是 December 31st, 2199,但把它设为 evaluationDate 后,内部还要叠加结算天数,于是越界报 Date's serial number (109575) outside allowed range [367-109574], i.e. [January 1st, 1901-December 31st, 2199]。日期类型的边界不等于可用的估值日,实际使用请留在合理区间。
能换一个版本绕过这些报错吗?
能换版本,但要自己验证而不是假设。本页所有结论都限定在 QuantLib 1.43 / Python 3.11.9 / Windows:绑定缺口(如 zSpread 不可调用)、类是否存在(如无 BinomialHestonEngine)、符号约定(basisPointValue 返回负值)都可能随版本变化。换版本后建议重跑本页的复现脚本,把新的输出作为你自己环境的事实记录;具体版本行为以官方发布说明与仓库 Issues 为准。
排查这些问题需要额外装什么工具吗?
不需要。本页的复现脚本只依赖 QuantLib 本身(本机由 pip install QuantLib 装到 1.43),不涉及行情数据、不涉及实盘接口。如果你更想用自然语言提问的方式做量化研究(例如查一只股票的指标),那属于另一条路线:EasyClaw 的本机技能可以免部署发起这类提问式研究,但 QuantLib 与 EasyClaw 无已证实集成,两者是独立的工具,本页只讨论 QuantLib 侧的报错与判据。
本页的报错能不能直接当成「官方 bug 列表」?
不能。本页记录的是本站实测行为,其中一部分是使用方式问题(如未设引擎、引擎与行权方式不匹配),一部分是版本相关的绑定限制(如 zSpread 不可调用),还有一部分是设计选择(如 basisPointValue 的符号)。要判断是否为缺陷,请到官方仓库检索同类报告并以其答复为准。