HotXLS 可在 Delphi 與 C++Builder 中原生求值 Excel 的債券與國庫券理財函式:PRICE、YIELD、DURATION、MDURATION、六個票息日期基本函式、三個國庫券函式、折現系列函式,以及法式折舊函式。這些函式在函式庫自身的公式引擎中計算,因此一份為債券投資組合定價的活頁簿,可以在完全沒有 Excel、也沒有 Analysis ToolPak 增益集的伺服器上重新計算
這些函式過去存在於 Analysis ToolPak 中,這也是它們至今仍給人一種難用印象的原因。自 Excel 2007 起,它們已成為標準函式集的一部分,在檔案格式中是以「裸」形式寫入,不帶真正較新函式才會有的 _xlfn. 前綴。使用它們的活頁簿就是一份普通的活頁簿;唯一的問題是負責重新計算它的工具是否認得這些函式
票息日期引擎必須做對的事
每一項債券計算都建立在同一組基本函式之上:距上次配息已過多少天、目前配息期共有多少天、距下次配息還有多少天、各次配息落在哪一天,以及還剩幾次配息。這些正是 COUPDAYBS、COUPDAYS、COUPDAYSNC、COUPPCD、COUPNCD 與 COUPNUM,每一個計算價格與殖利率的函式都會呼叫它們
讓這些函式不簡單的地方在於 basis 引數。實際運作中有五種日數計算慣例——美式 30/360、實際天數/實際天數、實際天數/360、實際天數/365,以及歐式 30/360——它們對於「結算日落在月底 31 日時該怎麼處理」或「二月的配息日在閏年該如何表現」這類問題,答案並不一致。只把其中一種慣例做對、其餘用近似值處理,會得到看似合理、實際卻偏差幾個基點的結果,而在大額部位上,這幾個基點就是實實在在的金錢
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; // basis:實際天數/實際天數
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 使用二分法求解器,它收斂得可靠,而不是快
由此帶來兩項實務上的影響。殖利率的計算成本明顯高於價格,因此為一萬個部位計算殖利率的投資組合重新估值,成本會明顯高於只計算價格,值得先確認你的模型實際需要哪一種。而當一個殖利率無法被夾定範圍——因為輸入值描述的債券,在給定價格下根本不存在任何殖利率——時,函式會回傳錯誤,而不是一個錯誤的數字,這才是正確行為,也是應該測試的情況
國庫券是獨立的一組函式家族
國庫券是不含配息的折現工具,因此使用的是 TBILLPRICE、TBILLYIELD 與 TBILLEQ,而不是債券函式。TBILLEQ 會把折現率轉換為債券等值殖利率,並有一項有文件記載、值得了解的特殊行為:當到期日超過 182 天時,轉換公式是二次式而非線性式,因為該證券橫跨了不只一個半年期
若改以零票息與某個配息頻率,把國庫券資料餵給 PRICE,得到的數字並不會相同。折現工具函式家族——DISC、INTRATE、RECEIVED、PRICEDISC 與 YIELDDISC——之所以存在,正是因為這類工具的報價與結算方式本就不同
其餘函式,集中一處說明
除了債券與國庫券之外,同一批函式還涵蓋了實際模型中經常伴隨出現的其他函式。PRICEMAT 與 YIELDMAT 處理到期時一次付息的證券。ACCRINTM 為這類證券計算應計利息。EFFECT 與 NOMINAL 在有效年利率與名目年利率之間互相轉換,這也是手動計算時最常出錯的一種換算。CUMIPMT 與 CUMPRINC 給出某段期間範圍內的累計利息與累計本金。FVSCHEDULE 則依序套用一系列不同的利率,可接受範圍或純量作為輸入
DOLLARDE 與 DOLLARFR 在十進位與分數美元報價之間互相轉換,這對任何仍以 1/32 為單位報價的工具都很重要。而 AMORDEGRC 與 AMORLINC 實作了法式折舊方法,這是法國會計規則所要求的,而不是選用的便利功能
// 人們手動計算時經常出錯的一種利率換算
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 與 C++Builder 中同屬一套函式庫;完整功能清單請見HotXLS Delphi 試算表元件頁面