lballabio / QuantLib

The QuantLib C++ library
http://quantlib.org
Other
5.21k stars 1.78k forks source link

Swap floating leg basis-point sensitivity includes coupons based on cashflow date - should it be fixing date? #1779

Open tomwhoiscontrary opened 1 year ago

tomwhoiscontrary commented 1 year ago

If you have a VanillaSwap which started in the past, and you ask for the floatingLegBPS, then the value returned includes the effect of a rate shift on all the coupons which have yet to be paid, even if they have already fixed.

The same holds for overnight index swaps - coupons drop out atomically on the cashflow date, rather than diminishing in effect as they progressively fix.

That means you can't use this number to estimate change in NPV for a shift in the curve. Was that the intention? What are these numbers for?

I would like to be able to estimate change in NPV like this. Is there any scope for changing QuantLib's behaviour here? Perhaps simply changing it is out for backwards compatibility reasons. But this could be controlled via a boolean option in the same way as includeSettlementDateFlows / includeReferenceDateEvents?

lballabio commented 1 year ago

Hello Tom, I think the idea was not to use that figure to estimate changes for a shift but rather to calculate a spread to add to the coupon to make the swap fair. I'm not sure if the latter calculation makes a lot of sense in the case of seasoned swaps, though, so we might try and see if we can support both use cases without making the code too complex.

tomwhoiscontrary commented 1 year ago

I am working on implementing it in my fork - i have raised a PR for an enabling change, and will perhaps raise more, and will show you what i end up with for this change.

tomwhoiscontrary commented 1 year ago

I had a look at this, and i think there is a fairly simple change. In CashFlows::npvbps, we currently have:

                if (cp != nullptr)
                    bps += cp->nominal() * cp->accrualPeriod() * df;

We pull out the accrual period into a variable, and set it a bit more conditionally, based on the specific runtime type of the coupon. If it is an IborCoupon (dynamic_cast into icp), set it to icp->hasFixed() ? 0 : cp->accrualPeriod() (). If it is an OvernightIndexCoupon, set it to cp->accrualPeriod() - cp->accruedPeriod(settlementDate).

We could pull out a accrualPeriodRemaining() method on the coupon to make this more uniform.

tomwhoiscontrary commented 1 year ago

Edits:

There are probably more errors on the details here, but hopefully the gist of the idea should be clear.

github-actions[bot] commented 10 months ago

This issue was automatically marked as stale because it has been open 60 days with no activity. Remove stale label or comment, or this will be closed in two weeks.

tomwhoiscontrary commented 10 months ago

PR #1781 is related to this, and i would still like to work on it, as we are carrying a patch locally at the moment.

github-actions[bot] commented 8 months ago

This issue was automatically marked as stale because it has been open 60 days with no activity. Remove stale label or comment, or this will be closed in two weeks.

tomwhoiscontrary commented 3 months ago

The npv, bps, and npvbps methods in Cashflows all take optional settlement date and NPV date parameters. What are those supposed to mean? I think i know, but i would like to confirm before i go on.

I understand that the settlement date is when a trade in the instrument settles, and that the NPV date is the date we are asking for the NPV as of.

So the evaluation date might be today, 14 June 2024. If we have a one-year IMM-rolls swap, it could start on 18 September 2024 and end on 17 September 2025. We might consider the situation where we trade it on the 31st of October 2024 (as a seasoned swap), and for some reason want to know what that would mean for the NPV as of the 1st of July 2024.

The current code excludes coupons if they have already been paid as of the settlement date. That makes sense, because those payments would not be exchanged if the swap was traded after their payment dates.

I'm trying to work out how these dates interact with BPS calculation. BPS is how much the NPV will change for a one basis point change in coupon rates. My rule of thumb is that BPS should match what happens if you bump the curve enough to produce a one basis point change in coupons, then calculate NPV again and measure the change in that. With a bumped curve:

But changing the settlement date would not have any effect on which coupons used a historical fixing, and which used a forecast fixing.

So i think that in the cashflows functions, fixed coupons should be excluded on the basis of the evaluation date, not the settlement date. Paid coupons should still be excluded on the basis of the settlement date, of course.

Does this make sense?

lballabio commented 2 months ago

As for settlement date and npv date, they're probably more useful for things like bonds. The settlement date is the settlement date; passing it as the npv date as well gives you the price of the bond, passing today's date instead gives you the value as of today of holding the bond once you bought it.

This said: thinking about it, I'm getting the idea that what you need might be a separate method. BPS as implemented today is not what happens if you bump the curve enough to produce a one basis point change in coupons, then calculate NPV again and measure the change in that; because if you bump the curve, you also change the discounting (at least in OIS), and the BPS calculation doesn't do that. It's what happens if your fixed-rate coupons pay 1 bp more, or if your floating-rate coupons have an added spread of 1 bp, with the curve staying the same. That's not the sensitivity to movements of the curve(s).

The sensitivity to a bump in the curve has value, of course! But it's a different calculation.

tomwhoiscontrary commented 2 months ago

What you say about the dates makes sense, so i will not write those up to the has-fixed logic.

I take your point about this method not really simulating a bump for OISs. We do indeed do that different calculation of bumping the curve. But this bump-the-coupons number is also useful, and it would be most useful if its treatment of fixings was similar to that of bumping.

lballabio commented 2 months ago

What if we provided some helper functions to get those numbers more easily from the existing functions? Something like

CashFlows::bps(forecast_cashflows(leg), discount_curve, ...);

where forecast_cashflows returns the vector of coupons from and not including the current?

tomwhoiscontrary commented 2 months ago

At the moment, in my code we ultimately get the BPS by calling swap.floatingLegBPS(). For me, the ideal solution would change (IMHO, fix) the behaviour of that method. But it would be possible to do CashFlows::bps(forecast_cashflows(swap.floatingLeg), etc) - just awkward because i would have to get all the parameters together for each swap, when that is already done by QuantLib during pricing.

Would you prefer not to change the current behaviour?

lballabio commented 2 months ago

fairSpread relies on the current behavior.

lballabio commented 2 months ago

Also, for consistency, floatingLegBPS should work like fixedLegBPS. And for the latter, there's no difference between the current coupon and the future ones—they're all already fixed anyway.

tomwhoiscontrary commented 2 months ago

Fair enough.

I would also like to get the "right" result for OISs. I'm not sure that can be done just by filtering the cashflows, because it needs to account for the BPS of the first cashflow gradually decreasing as it incrementally fixes. I suppose i could make a copy of the leg with the first cashflow modified in some way?

lballabio commented 2 months ago

The easier way is probably to use two different handles for OIS forecasting and OIS discounting so you can bump the one and not the other.