技术文章

用 HotXLS 在 Delphi 中实现债券与国库券公式

HotXLS 在 Delphi 与 C++Builder 中原生计算 Excel 的债券与国库券金融函数:PRICEYIELDDURATIONMDURATION、六个票息日期基础函数、国库券三件套、贴现类函数,以及法式折旧函数。它们在库自身的公式引擎内部计算,因此一份为债券组合定价的工作簿,可以在一台既没有 Excel、也没有任何 Analysis ToolPak 加载项的服务器上重算

这些函数曾经属于 Analysis ToolPak,这也是它们至今仍带着"不好用"这个名声的原因。自 Excel 2007 起,它们已经成为标准函数集的一部分,并且在文件格式中以裸函数名写出,不带那些真正的新函数才有的 _xlfn. 前缀。使用它们的工作簿是一份普通的工作簿,唯一的问题在于负责重算它的那个引擎是否认识这些函数

票息日期引擎必须做对的事

每一个债券计算都依赖同一组基础量:距上一次付息过了多少天,本次付息周期共有多少天,距下一次付息还有多少天,这些付息日分别落在哪一天,以及还剩多少次付息。这些分别是 COUPDAYBSCOUPDAYSCOUPDAYSNCCOUPPCDCOUPNCDCOUPNUM,而每一个定价与收益率函数都会调用它们

让它们变得不简单的是基准参数。这里涉及五种计日惯例——30/360(美国)、实际/实际、实际/360、实际/365 和 30/360(欧洲)——它们在诸如"结算日落在某月 31 日会发生什么"或者"闰年里二月的付息日该如何处理"这类问题上给出不同答案。只把一种惯例做对、而对其余惯例做近似处理,得到的结果看起来合理,实际却偏差了几个基点,而在一笔大额头寸上,这就是真金白银

HotXLS 用一个自包含的嵌套票息日期引擎实现了全部五种惯例,由每一个需要它的函数共享,因此 PRICEDURATION 不可能在付息日到底落在哪一天这件事上产生分歧

HotXLS 在 Delphi 中的票息日期引擎:债券输入和选定的计日基准,输入六个由 PRICE、YIELD、DURATION 和 MDURATION 共享的 COUP 基础函数
全部五种计日基准都输入同一个嵌套票息日期引擎,由 HotXLS 中每一个金融函数共享,因此 PRICE 和 DURATION 在付息日的判断上永远不会产生分歧

在工作簿中定价与计算收益率

调用代码本身没有任何特别之处:写公式、重算、读取数值。这正是这项设计的意义所在——工作簿就是模型,你的 Delphi 应用只是计算器:

uses
  lxHandleX;

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Bond');
    Sheet.Cells[1, 1].Value := EncodeDate(2026, 3, 15);   // 结算日
    Sheet.Cells[2, 1].Value := EncodeDate(2034, 11, 15);  // 到期日
    Sheet.Cells[3, 1].Value := 0.0425;                    // 票息利率
    Sheet.Cells[4, 1].Value := 0.0391;                    // 要求收益率
    Sheet.Cells[5, 1].Value := 100;                       // 赎回价
    Sheet.Cells[6, 1].Value := 2;                         // 付息频率
    Sheet.Cells[7, 1].Value := 1;                         // 基准:实际/实际

    Sheet.Cells[9, 1].Formula  := '=PRICE(A1,A2,A3,A4,A5,A6,A7)';
    Sheet.Cells[10, 1].Formula := '=DURATION(A1,A2,A3,A4,A6,A7)';
    Sheet.Cells[11, 1].Formula := '=MDURATION(A1,A2,A3,A4,A6,A7)';
    Sheet.Cells[12, 1].Formula := '=COUPNUM(A1,A2,A6,A7)';

    Book.Recalculate;

    Writeln(Format('price %.6f  Macaulay %.6f  modified %.6f  coupons %d',
      [Double(Sheet.Cells[9, 1].Value), Double(Sheet.Cells[10, 1].Value),
       Double(Sheet.Cells[11, 1].Value),
       Integer(Sheet.Cells[12, 1].Value)]));
  finally
    Book.Free;
  end;
end;

日期应作为真正的日期值输入,而不是字符串。文本形式的日期在 Excel 中能用,是因为 Excel 会做类型强制转换,而这种转换依赖于区域设置,这正是服务端重算不应该有的一种依赖。底层的日期系统规则见 日期序列号与 1904 系统

收益率没有解析解,这带来了一些后果

PRICE 是一次直接计算:把各期现金流贴现后求和即可。YIELD 是它的逆运算,对于剩余付息次数超过一次的债券没有解析解,因此必须用数值方法求解。HotXLS 使用二分法求解器,它收敛得可靠,但不算快

由此带来两个实际影响。计算收益率明显比计算价格更昂贵,因此一次对一万个头寸计算收益率的组合重估,成本会明显高于只计算价格的重估,值得先弄清楚你的模型实际需要哪一种。另外,一个无法被区间夹逼的收益率——因为输入描述的这只债券在给定价格下根本不存在对应的收益率——会返回一个错误而不是一个错误的数字,这才是正确的行为,也是应该测试到的情形

在 Delphi 的 HotXLS 中,PRICE 是一次直接的现金流贴现计算,而 YIELD 没有解析解,靠二分法循环求解,在不存在夹逼区间时返回错误
PRICE 是对贴现现金流的一次直接计算,而 YIELD 没有解析解,需要先夹逼区间再二分求解,在输入无法定价时返回错误而不是一个错误的数字

国库券是自成一体的一类

国库券是没有票息的贴现工具,因此使用 TBILLPRICETBILLYIELDTBILLEQ,而不是债券函数。TBILLEQ 把贴现率转换为债券等价收益率,它有一个值得了解的既定怪癖:对于期限超过 182 天的情形,这种转换是二次的而不是线性的,因为该证券跨越了不止一个半年期

如果改用零票息、带频率参数的方式把一张国库券喂给 PRICE,得到的数字并不相同。贴现工具家族——DISCINTRATERECEIVEDPRICEDISCYIELDDISC——正是因为这些工具的报价和结算方式不同才存在

其余函数,一并列出

除了债券和国库券之外,同一批函数还覆盖了真实模型中围绕它们的其他函数。PRICEMATYIELDMAT 处理到期一次性付息的证券。ACCRINTM 计算这类证券的应计利息。EFFECTNOMINAL 在实际年利率和名义年利率之间转换,这是手工计算时最常出错的一种换算。CUMIPMTCUMPRINC 给出某一期间范围内的累计利息与累计本金。FVSCHEDULE 依次应用一系列不同的利率,参数可以是一个区域,也可以是一个标量

DOLLARDEDOLLARFR 在小数和分数形式的美元报价之间转换,这对任何仍以三十二分之一为单位报价的工具都很重要。而 AMORDEGRCAMORLINC 实现了法式折旧方法,这是法国会计准则的强制要求,而不是可有可无的便利功能

// 一种人们手工计算时经常出错的利率换算
Sheet.Cells[1, 3].Formula := '=EFFECT(0.0425,12)';   // 名义利率转实际利率
Sheet.Cells[2, 3].Formula := '=NOMINAL(0.043338,12)'; // 再转换回去

// 一笔 25 年期贷款前两年的累计利息
Sheet.Cells[4, 3].Formula :=
  '=CUMIPMT(0.0389/12,300,340000,1,24,0)';

// 浮动利率预测:先 3%,再 3.5%,再 4%
Sheet.Cells[6, 3].Formula := '=FVSCHEDULE(10000,{0.03;0.035;0.04})';

校验一份非自己所写的实现

金融函数是"能返回一个数字"完全不能说明任何问题的一个领域。这个数字从构造上看就是合理的,而计日惯例上的一处错误,会让结果偏移一个小到不易察觉、却大到不可接受的幅度

真正有效的测试是差分测试。搭建一张具有代表性的工具清单——每种计日基准配一只债券、一只在月末结算的债券、一只跨越闰年二月的债券、一只 182 天以内和一只 182 天以上的国库券——先在 Excel 里计算一次数值,把它们存为期望结果,此后每次库升级后都拿它们做断言。横跨某个付息日的头寸和 31 日结算的情形,正是各实现容易出现分歧的地方,因此要刻意把两者都纳入

Delphi 中 HotXLS 债券与国库券函数的差分校验集:具有代表性的工具(含闰年和月末情形)先在 Excel 中计算一次,此后每次库升级都对其做断言
差分测试集在 Excel 中对具有代表性的工具定价一次,把数值存为期望结果,并在每次 HotXLS 升级后加以断言,使基点级的偏移无法藏在看似合理的输出背后

就工作簿本身而言,与这项特性相配的另外两块内容分别是决定重算范围的计算引擎,见 增量重算与依赖图,以及统计函数家族,见 统计分布函数。二者结合起来,正是让一个金融模型能够在服务器上无人值守运行的关键,这也是从一开始就要原生实现它们的全部理由

债券金融函数、统计函数、工程函数和重算引擎,都打包在同一个面向 Delphi 与 C++Builder 的库中;完整功能列表见 HotXLS Delphi 电子表格组件页面