QuantLib
QuantLib copied to clipboard
Add rounding to coupons and accrued amounts
Hi. In Russia local bonds are traded not in notional but in quantities by RUB 1000 amount for every 1 bond. It imlies coupon and accrued amounts are rounded per 2 digits for every RUB 1000. It differs from eurubond conventions, but that's it. As a results, CFs are not precisely correct when constructing via qlBond object. For big amount (millions, billions) that rounding actually gives some distortions in yields, spreads. Is there a way to fix this bug? Somehow force qlXL to round coupon and accrued amount for every sigle bond?
For example, 3% coupon rate act/365 with 182D for 1000 gives correct coupon 14.96 and not 14,9589. Accrued for 1st day is 0.82 and not 0.82.
Thanks for posting! It might take a while before we look at your issue, so don't worry if there seems to be no feedback. We'll get to it.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Hi. If my post was not clear enough, pls let me know and I will demonstrate this in more details. But it's specific of Russian bonds market from the very beginning, and such appoach to rounding of accrued and coupon will persist. Thx.
It's clear enough, thanks. I've labelled the issue so it doesn't get marked as stale.
@Vangerok I would like to understand the calculations more, would you please see if my understanding below is correct?
Currently: 182/365 * 0.03 * 1000 = 14.9589
Expected: 182/365 * 0.03 * 1000 ~ 14.96
Currently: 185/365 * 0.03 * 1000 = 15.20548
Expected: 185/365 * 0.03 * 1000 ~ 15.21
Currently: 191/365 * 0.03 * 1000 = 15.69863
Expected: 191/365 * 0.03 * 1000 ~ 15.70
Currently: 192/365 * 0.03 * 1000 = 15.78082
Expected: 192/365 * 0.03 * 1000 ~ 15.78
@Vangerok I would like to understand the calculations more, would you please see if my understanding below is correct?
Currently: 182/365 * 0.03 * 1000 = 14.9589 Expected: 182/365 * 0.03 * 1000 ~ 14.96 Currently: 185/365 * 0.03 * 1000 = 15.20548 Expected: 185/365 * 0.03 * 1000 ~ 15.21 Currently: 191/365 * 0.03 * 1000 = 15.69863 Expected: 191/365 * 0.03 * 1000 ~ 15.70 Currently: 192/365 * 0.03 * 1000 = 15.78082 Expected: 192/365 * 0.03 * 1000 ~ 15.78
Yes, exactly, math rounding rule is applied per each 1 bond (and only then multipled by quantity) - this directly stated in the bond prospectus.
@Vangerok as this is new to me I took the prospectus and found this in 7(a): "Principal... rounded downwards, if necessary, to the nearest cent." From my understanding, this should only apply to the principal and it shouldn't round up but only down? Source: https://minfin.gov.ru/common/upload/library/2018/12/main/Russia-2029_Russia-2047tap_Prospectus_final.pdf
@Vangerok as this is new to me I took the prospectus and found this in 7(a): "Principal... rounded downwards, if necessary, to the nearest cent." From my understanding, this should only apply to the principal and it shouldn't round up but only down? Source: https://minfin.gov.ru/common/upload/library/2018/12/main/Russia-2029_Russia-2047tap_Prospectus_final.pdf
Yes, you are right. This specifics is applied only to bonds, issues under Russial cocal regulations. You mentions eurobond and its not traded by RUB 1000 pieces, but instead traded by notional.
E.g. lets take russial local bond RU000A0JW0S4. Accrued for March 2, 2021 is calced as 5% * 77 / 365 * 1000 = 10,54795. But the bond traded by piece of RUB 1000 notional, so math rounding is applied and we have AI=10,55.
https://www.rusbonds.ru/BondCalc.aspx?bond_state=Market&ftid=121744&BondCalcDate=02.03.2021&bondCalcRate=0&bondCalcType=0&Price_Type=0&Price_Clear=100&Price_Full=0&Yield_Type=0&Yield_1=0
Screen https://ibb.co/0CqxNfH
Because this math rounding per every piece of bond (usually RUB 1000) is applied to russian local bonds, so official prospectus also only in Russian. Below is exact Russian wording and it says (in bold) that accrued is determined with an accuracy of one penny and (in brackets) rule of mathematical rounding is applied with explanatioin:
Начиная со второго дня размещения Облигаций, покупатель при совершении сделки купли-продажи Облигаций также уплачивает накопленный купонный доход по Облигациям (далее - НКД), определяемый по следующей формуле: НКД = Nom * C * ((T - T0) / 365)/ 100%, где НКД - накопленный купонный доход, руб. Nom - номинальная стоимость одной Облигации, руб.; С - размер процентной ставки купона на первый купонный период, проценты годовых; T - дата размещения Облигаций; T0 - дата начала размещения Облигаций. Величина НКД в расчете на одну Облигацию определяется с точностью до одной копейки (округление производится по правилам математического округления. При этом под правилом математического округления следует понимать метод округления, при котором значение целой копейки (целых копеек) не изменяется, если первая за округляемой цифра равна от 0 до 4, и изменяется, увеличиваясь на единицу, если первая за округляемой цифра равна 5 - 9).
The rounding conventions for ruble-denominated bonds are a little more complicated than the above discussion. To quote Bloomberg "Vald calc types" document:
calc type 1155: Russia: 91/182-Pay
...
Price, yield are rounded to two decimal places.
Accrued interest is rounded to 2 decimals _ [i.e. to the nearest whole kopek = 0.01 ruble] on 1000 par before being multiplied by face amount._
However, the methodology is slightly different between government and corporate securities.
Government bond [i.e. OFZ] accrued formula:
periodic coupon = annual coupon rate × days in period /365 × 1000 × principal factor rounded to 2 decimals
accrued interest = periodic coupon × accrued days /days in period rounded to 2 decimals
Corporate bond accrued formula:
accrued interest = annual coupon rate × accrued days /365 × 1000 × principal factor rounded to 2 decimals
A similar note is found on calc type 1151.
Also, here is a quote from a example prospectus of a ruble bond in English https://mkb.ru/en/investor/emitent-news/finance-info/16032
Starting from the second day of placement of the Bonds, any buyer thereof shall, in addition to the placement price, also pay the accrued coupon income thereon («ACI») calculated using the following formula:
ACI = Nom * C1 * ((T — T0)/ 365)/ 100%, where:
ACI means the accrued coupon income, RUB;
Nom means the par value of one Bond at the placement starting date, RUB;
С1 means the first coupon rate, percent per annum, not to exceed the level set in the Bank of Russia’s Regulation dated 28.12.2012 No. 395-P «On the Method of Calculating the Amount, and Assessing the Adequacy of, Credit Institutions’ Equity (Capital) („Basel III“)» for subordinated loans (bond issues) qualifying as a source of a credit institution’s additional capital;
T means the placement date of the Bonds as at which ACI is calculated;
T0 means the placement starting date of the Bonds;
The ACI per one Bond shall be determined to one cent (rounding mathematically, «mathematically» meaning that any integral cent amount increases by one digit if the next following digit lies between 5 and 9, and remains unchanged if not).
and further:
Each coupon period shall have a duration of 182 (one hundred eighty two) days. The first coupon rate shall be determined by the Issuer at least 1 (one) business day before the Placement Starting Date. The interest rate for the second and subsequent coupon periods up to and including the eleventh coupon period is equal to the first coupon rate.
The amount of coupon income per one Bond payable at the end of a coupon period shall be calculated using the following formula:
Kj = Nom * Сj * (T(j) — T(j-1)) / 365 / 100%, where
Kj means the amount of coupon payment per each Bond, in roubles;
j means the ordinal number of the current coupon period;
Nom means the par value of one Bond or, if the Issuer’s obligation to repay the par value to Bond holders were terminated upon occurrence of any of the Termination Events specified in cl. 10.4.1. of the Issue Resolution, before or during the j-th coupon period, the then outstanding portion of the par value of one Bond, RUB;
Сj means the j-th coupon rate;
T(j) means the last day of the j-th coupon period;
T(j-1) means the last day of the (j-1)-th coupon period (or, for the first coupon period, the placement starting date).
The interest (coupon) income per one Bond shall be determined to one kopeck rounding mathematically, «mathematically» meaning that any integral kopeck amount increases by one digit if the next following digit lies between 5 and 9, and remains unchanged if not.
Ciao all, the calculations of coupon and accrued amount are performed in class FixedRateCoupon, in member functions amount and accruedAmount. In order to implement this logic for domestic Russian bonds maybe we could overload those two functions and pass them the "minimum lot size" (in the specific Russian case it will be for instance 1000 RUB) and also the other 3 parameters needed to invoke the Rounding function, therefore Integer precision=2, Type type=Closest, Integer digit=4. We can then perform the desired rounding on the coupon/accrual compoundFactor value and then multiply it for (nominal() / "lot size"):
Real FixedRateCoupon::amount() const {
return nominal()*(rate_.compoundFactor(accrualStartDate_,
accrualEndDate_,
refPeriodStart_,
refPeriodEnd_) - 1.0);
}
Maybe this could be a way to implement the Russian domestic bond requirement and at the same time keep the software design general and flexible enough to manage similar cases for other countries. It would be very interesting to hear the opinion of @lballabio or other QuantLib experienced developers about how to approach this requirement. I attach the source code used to replicate the case for the real domestic Russian bond RU000A0JW0S4. https://www.moex.com/en/issue.aspx?board=TQCB&code=RU000A0JW0S4&utm_source=www.moex.com&utm_term=ru000a0jw0s4#/bond_1 testRussiaCoupon.cpp.txt
Hi all, if @lballabio agrees with the analysis above I can try to code a PR for this. Of course if any QuantLib experienced developer has suggestions/ideas about this topic I am more than happy to discuss them.
A couple of quick thoughts:
-
Passing the additional info as parameters to
amountandaccruedAmount, even with defaults, changes the interface in a non backward-compatible way (it would break user code). If we need to pass rounding information, I would do it in the constructor instead. -
Currently, bonds assume that their price is quoted around 100, even if the face amount is different. This should be generalized.
While most bonds are quoted as a percentage of face value, a few are not. For example, Mexican Cetes (Certificados de la Tesorería de la Federación - zero-coupon bonds, MCET Govt on Boomberg) are quoted so 10 is par. Brazil NTN-F's (BNTNF Govt on Bloomberg) are quoted so 1,000 is par. There are other examples of unusual bond price quoting conventons, but probably they belong in a separate discussion.
@dvulis thanks for pointing it out. If you have other examples please provide them. If you could provide the ISIN of each example it would be really great. Having more examples will help to collect the requirement and eventually find a common solution to generalize the code enough to manage them all. @lballabio thanks for your suggestion. WIll study the code of FixedRateCoupon to see if I can figure out a solution.
Sure, some examples:
MCET 0 2/10/2022, ISIN: MXBIGO000R74
BNTN-F 10% 1/1/2029, ISIN: BRSTNCNTF1Q6
Hi @jakeheke75,
another approach would be to have a derived class AmountRoundingFixedRateCoupon : public class FixedRateCoupon (or some better naming for it) with the relevant rounding information in its constructor which overwrites FixedRateCoupon::amount() and FixedRateCoupon::accruedAmount() accordingly.
And to have an extra FixedRateLeg& FixedRateLeg::withAmountRounding(...) which would return an AmountRoundingFixedRateCoupon if set and the "classical" FixedRateCoupon if not.
This gives you flexibility to implement the rounding feature as it would keep the FixedRateCoupon interface as it is. I guess in 99.99% the rounding in FixedRateCoupon::amount() would be not used anyhow so we avoid a performance impact here.
Regards Ralf
Hmm, I'm afraid that would lead to AmountRoundingFixedRateCoupon, AmountRoundingIborCoupon, AmountRoundingInflationCoupon and the like. If we write the logic in Coupon instead, the performance hit in the default case can probably be brought down to just checking a bool, so I wouldn't be too concerned.
It might also possible to have AmountRounding<FixedRateCoupon>, AmountRounding<IborCoupon> etc which would solve the duplication problem, but that might require fancier code than I like 😄
Okay, good point.
On the other hand currently in Coupon neither amount() nor accruedAmount() are implemented?!
It is virtual Real CashFlow::amount() const = 0; and virtual Real Coupon::accruedAmount(const Date&) const = 0;
Hi @lballabio and @ralfkonrad, thanks for your suggestions. As a newbie C++ developer I am following this discussion with attention and interest. There's a lot to be learned here! :smile: I tried to read a bit about the history of the Coupon class: http://www.implementingquantlib.com/2014/01/chapter-4-part-1-of-5-cash-flows-and.html and I understood that both member functions amount and accruedAmount have been created as pure virtual in order to demand a more specific implementation to the derived classes. In hindsight I think that it was a good choice. In fact the function amount has been implemented in 3 different Coupon derived classes: FixedRateCoupon, FloatingRateCoupon and InflationCoupon. The function accruedAmount has been implemented in 4 different Coupon derived classes: FixedRateCoupon, FloatingRateCoupon, InflationCoupon and OvernightIndexedCoupon. All the implementations are fair specific... I think it would have been very hard to figure out a common implementation to be done directly in the Coupon class.
With that said, what to do with this rounding requirement? IMHO we have a couple of choices:
- We implement new member functions
amountRoundingandaccruedAmountRoundingin theCouponclass. If we do this, it will still be necessary to differentiate the implementation logic (maybe with aenumparameter) in order to apply the correct calculations for each coupon type. So it will not be a real "common" implementation, but we will simply have all the different logics grouped in the same class. - We keep the logic as it is and we implement the new member functions directly in each derived class, but only if it makes sense from a functional point of view. I mean, if we hang around here we probably all love coding, but the best code is probably the one that we have not implemented because it was not strictly required in the beginning. With this I want to say that for the moment we have a real requirement of rounding rule and different par amount for Russian domestic bonds which are managed in
FixedRateCoupon. Is this requirement existing in the reality also for the otherCouponderived classes different thanFixedRateCouponor not? If not then we can implement the rounding functionality only in theFixedRateCouponclass and be ok with it, because we know that in the real world it will not be applicable in other cases. We can eventually add more implementations if and when new real user cases arise. Sorry for the long post and I apologize if I wrote something completely wrong :smile:
Hi @jakeheke75,
regarding your alternative having amountRounding and accruedAmountRounding:
The downside of this approach is that e.g. the pricing engines will not take this new members into account. The DiscountingBondEngine uses CashFlows::npv(...) which itself uses amount().
So to be consistent within the library I think we have to do the changes within amount() and accruedAmount() itself.
Regards Ralf
Hmm, it's tricky. In principle all coupons should be rounded, not just Russian ones; a bond in EUR or USD can't pay a fraction of cent either unless I'm mistaken. So if we look for a solution, it would be nice to have a common one. Another solution would be to rely on the users to consider rounding after the fact, as we're practically doing now.
@Vangerok — do you have an example of distortions in yields and spreads? I would have thought that the functions in the library work with quoted prices (corresponding to one lot), and so millions or billions of face amount shouldn't multiply the problem. What am I missing?