HotXLS вычисляет финансовые функции облигаций и казначейских векселей Excel нативно в Delphi и C++Builder: PRICE, YIELD, DURATION, MDURATION, шесть примитивов купонных дат, тройку казначейских векселей, семейство дисконтных функций и французские функции амортизации. Они вычисляются внутри собственного движка формул библиотеки, поэтому книга, оценивающая портфель облигаций, пересчитывается на сервере без Excel и без надстройки Analysis ToolPak где бы то ни было поблизости
Эти функции когда-то жили в Analysis ToolPak, поэтому за ними до сих пор тянется репутация неудобных. Начиная с Excel 2007 они входят в стандартный набор функций и записываются в формате файла напрямую, без префикса _xlfn., который несут по-настоящему новые функции. Книга, использующая их, — обычная книга; единственный вопрос в том, знает ли эти функции то, что её пересчитывает
Что обязан правильно делать движок купонных дат
Каждый расчёт облигации опирается на одни и те же примитивы: сколько дней прошло с последнего купона, сколько дней в текущем купонном периоде, сколько дней до следующего купона, когда наступают эти купоны и сколько их осталось. Это COUPDAYBS, COUPDAYS, COUPDAYSNC, COUPPCD, COUPNCD и COUPNUM, и их вызывает каждая функция цены и доходности
Нетривиальными их делает аргумент базы расчёта. В игре пять соглашений о подсчёте дней — 30/360 US, actual/actual, actual/360, actual/365 и 30/360 European, — и они расходятся в таких вопросах, как поведение при дате расчёта, приходящейся на 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; // база: actual/actual
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 преобразуют между десятичной и дробной долларовой котировкой, что важно для любого инструмента, до сих пор котируемого в тридцать вторых долях. А 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 дней и один свыше, — один раз вычислите значения в Excel, сохраните их как ожидаемые результаты и сверяйте с ними после каждого обновления библиотеки. Позиции, охватывающие купонную дату, и расчёты на 31-е число — это места, где реализации расходятся, поэтому включите оба случая намеренно
Что касается самих книг, два спутника этой возможности — движок расчёта, решающий, что пересчитывать, описанный в статье инкрементный пересчёт и граф зависимостей, и статистическое семейство, описанное в статье функции статистических распределений. Вместе они и есть то, что позволяет финансовой модели работать без присмотра на сервере, — собственно ради этого функции и реализовывались нативно
Финансы облигаций, статистика, инженерные функции и движок пересчёта поставляются в одной библиотеке для Delphi и C++Builder; полный список возможностей — на странице компонента HotXLS Delphi Spreadsheet Component