Collateralized Cash Price — A consistent pricing framework (part II)

We discuss the calibration of a consistent model to price cash IRR, CCP and physical settled swaptions.

Posted by Oliver Kahl on Sat, May 26, 2018
Tags: swaptions, python, cash vs. physical
Series: Cash vs. Physical Swaptions

Overview

In the previous article, we gathered all the data needed. We will now utilize that information to calibrate our model. As it takes some functions and dependencies, our approach will be to put all the calibration code into three classes. In that regard we will have an "ImpliedVol" class, which does the vol implication from premiums. Also we have a superclass "CashPhyATM", doing the basic calibration for ATM data. There is a subclass to "CashPhyATM", called "CashPhyOTM", calculating all the OTM values. The rest of our work will be done in a Jupyter Notebook. For our modelling we use Pietersz and Sengers "Cash-settled swaptions: A new pricing model".

Vol Implication

The first step will be the implying of a shifted-log-normal volatility for all the premiums we collected this far. For this we will use the paper from Jaeckel (see references below). As a reminder our input DataFrame, "cash_prem", looks like this:

Master DataFrame
Figure 1. Cash premiums going into vol implication.

In principal we now iterate over our "cash_prem" DataFrame, to transform all the premiums into vols and print it back to a new, "vols", DataFrame. The following piece will do this:

for line in cash_prem.itertuples():
    for col in cash_prem.columns[5:]:
        if np.isnan(cash_prem[col][line.Index]):
            continue
        if type(col) == float:
            if col < 0:
                vols[col][line.Index] = ImpliedVol(
                                            (cash_prem[col][line.Index]
                                            /\ cash_prem['Ann Cash'][line.Index]
                                                 * cash_prem['DF'][line.Index]),
                                             cash_prem['Fwd'][line.Index],
                                             cash_prem['Shift'][line.Index],
                                             cash_prem['Fwd'][line.Index] + col / 10000,
                                             line.Index[0] / 12,
                                             'put'
                                                ).implied_volatility()
            else:
                vols[col][line.Index] = ImpliedVol(
                                            (cash_prem[col][line.Index]\
                                            / cash_prem['Ann Cash'][line.Index]
                                                 * cash_prem['DF'][line.Index]),
                                             cash_prem['Fwd'][line.Index],
                                             cash_prem['Shift'][line.Index],
                                             cash_prem['Fwd'][line.Index] + col / 10000,
                                             line.Index[0] / 12,
                                             'call'
                                                ).implied_volatility()
        else:
            if 'ATM Cash Rec' in col:
                vols[col][line.Index] = ImpliedVol(
                                            (cash_prem[col][line.Index]\
                                            / cash_prem['Ann Cash'][line.Index]
                                                 * cash_prem['DF'][line.Index]),
                                             cash_prem['Fwd'][line.Index],
                                             cash_prem['Shift'][line.Index],
                                             cash_prem['Fwd'][line.Index],
                                             line.Index[0] / 12,
                                             'put'
                                                ).implied_volatility()
            if 'ATM Cash Pay' in col:
                vols[col][line.Index] = ImpliedVol(
                                            (cash_prem[col][line.Index]\
                                            / cash_prem['Ann Cash'][line.Index]
                                                 * cash_prem['DF'][line.Index]),
                                             cash_prem['Fwd'][line.Index],
                                             cash_prem['Shift'][line.Index],
                                             cash_prem['Fwd'][line.Index],
                                             line.Index[0] / 12,
                                             'call'
                                                ).implied_volatility()
            if 'ATM Phy' in col:
                vols[col][line.Index] = ImpliedVol(
                                            (cash_prem[col][line.Index]\
                                            / cash_prem['PVBP'][line.Index]
                                                 * cash_prem['DF'][line.Index]),
                                             cash_prem['Fwd'][line.Index],
                                             cash_prem['Shift'][line.Index],
                                             cash_prem['Fwd'][line.Index],
                                             line.Index[0] / 12,
                                             'put'
                                                ).implied_volatility()

With that being done, we have a full ATM vol surface for ATM physical, ATM IRR receiver, and ATM IRR payer swaptions (all at the physical forward). Note that we have also calibrated vols for OTM strikes in IRR swaptions, but due to the lack of more information those surfaces are far from being "complete". At this stage we also have no information on physical smile data. But let’s inspect something, which is already complete, namely the ATM physical swaption surface:

ATM physical swaption surface
Figure 2. ATM physical swaption surface.

Align cash IRR and physical (cash CCP) vols

Description of the approach

Thus far we did nothing more than a regular (shifted) Black log-normal vol implication. The "CashPhyATM" superclass and it’s "CashPhyOTM" subclass will now be our working horse to get all swaptions, namely cash IRR and physical (and CCP as being equivalent), aligned in an arbitrage free fashion.

But let’s have a look at the basic idea of Pietersz and Sengers for modeling the above. They start with two processes, which financially turn out to be the sum of discount factors (present value of a basis point (PVBP)) and the EURIBOR forwards (y) for all the considered periods. On top of that, they consider another process, y*, such that the PVBP process is a constant scalar times the cash-annuity of y*. As outlined by Pietersz and Sengers, we start the PVBP process with it’s value as of today and model it with shifted log-normal dynamics. In the following we refer to y and y* — in analogy to Pietersz and Sengers — with the terms forecast forward rate and discount forward rate respectively. The forthcoming modeling will be done with 4 parameters in our shifted log-normal framework: forecast sigma and forecast shift for y and discount sigma and discount shift for y*.

From that framework a new put-call-parity relation for for IRR swaptions at the convexity adjusted forward/ PVBP (Fwd*/ PVBP*) can be obtained. We use that relation to arrive at prices for the various swaptions not observed in the market. Eventually, we get vols for IRR payer/ receiver and physical, which can then be further processed by a SABR model (next article) to complete the vol cube.

Let’s sum up what we already have before calibration:

  • ATM physical-settled vols

  • ATM IRR settled vols

  • OTM IRR settled vols

And this is what we will compute in the following:

  • Discount/ cash-settled forwards (where put-call-parity holds for IRR settlement)

  • Discount/ cash-settled displacement

  • Discount/ cash-settled ATM vol (at the discount/ cash-settled forward)

  • Discount/ cash-settled PVBP

  • OTM physical-settled vols

  • ITM physical-settled vols

  • ITM IRR settled vols

The "vols" DataFrame representation of the above now looks like this:

Cash Phy ATM
Figure 3. Cash/ physical ATM calibration input.

ATM calibration

The following shows, how we iterate over the "vols" DataFrame and call the respective methods from the "CashPhyATM" class, to obtain the above sketched out measures with regard to the ATM point:

for line in vols.itertuples():
    if line[11] == line[12]:
        vols['ATM Cash*'][line.Index[0]][line.Index[1]] = line[11]
        vols['Shift*'][line.Index[0]][line.Index[1]] = line[4]
        vols['PVBP*'][line.Index[0]][line.Index[1]] = line[6]
        vols['Fwd*'][line.Index[0]][line.Index[1]] = line[2]
        vols['ATM PCC'][line.Index[0]][line.Index[1]] = 0
    else:
        cash_phy_atm_calib = CashPhyATM(line[12], line[11], line.Index[0] / 12, line[4],
                                        line[2], line[8], line[9], int(line.Index[1] / 12))
        vols['ATM Cash*'][line.Index[0]][line.Index[1]]\
            = cash_phy_atm_calib.vol_atm_cash
        vols['Shift*'][line.Index[0]][line.Index[1]]\
            = cash_phy_atm_calib.cash_settled_displacement
        vols['PVBP*'][line.Index[0]][line.Index[1]]\
            = cash_phy_atm_calib.get_convexity_adjusted_pvbp()
        vols['Fwd*'][line.Index[0]][line.Index[1]]\
            = cash_phy_atm_calib.get_convexity_adjusted_forward()
        vols['ATM PCC'][line.Index[0]][line.Index[1]]\
            = cash_phy_atm_calib.get_put_call_combo_value()

Now we have added all calibrated ATM parameters to our DataFrame.

OTM calibration

In the next step, we move away from the ATM point and calibrate all our grid points for OTM, where we observe market quotes. By using put-call-parity we also compute ITM swaptions for these points. Additionally, we calculate physical smile data, an area where market prices are not observable. Before we start, we reorganise the target DataFrame a bit, to better serve our needs. We call this DataFrame "vols_sabr_in" as it is the basis for applying the SABR model in the next article. Basically we add a type flag to our MultiIndex, which indicates whetver we have a "cash IRR receiver", a "cash IRR payer" or a "physical" vol. In it’s empty form the DataFrame now looks like this:

Cash Phy ATM
Figure 4. Cash/ physical OTM calibration input (goes into SABR calibration later)

The following piece of code in combination with the "CashPhyOTM" class will fill the "vols_sabr_in" DataFrame for us:

for line in vols.itertuples():
    for strike_number, strike_offset in enumerate(vols_sabr_in.columns[2:].values):
        if strike_offset < 0:
            if np.isnan(line[14 + strike_number]):
                pass
            else:
                cash_phy_otm_calib = CashPhyOTM(line.Index[0] / 12, line[4], line[2],
                                                line[8], line[9],
                                                int(line.Index[1] / 12), 'put',
                                                strike_offset / 10000,
                                                line[14 + strike_number],
                                                line[10], line[5], line[13], line[7])
                vols_sabr_in[strike_offset]['Phy'][line.Index[0]][line.Index[1]],\
                vol_cash_star = cash_phy_otm_calib.calibrate_cash_model()
                vols_sabr_in[strike_offset]['Pay'][line.Index[0]][line.Index[1]] =
                cash_phy_otm_calib.get_implied_vol(
                                        'call', line[5], vol_cash_star,
                    vols_sabr_in[strike_offset]['Phy'][line.Index[0]][line.Index[1]]
                                                                    )
        if strike_offset > 0:
            if np.isnan(line[13 + strike_number]):
                pass
            else:
                cash_phy_otm_calib = CashPhyOTM(line.Index[0] / 12, line[4], line[2],
                                                line[8], line[9], int(line.Index[1] / 12),
                                                'call', strike_offset / 10000,
                                                line[13 + strike_number],
                                                line[10], line[5], line[13], line[7])
                vols_sabr_in[strike_offset]['Phy'][line.Index[0]][line.Index[1]],\
                vol_cash_star = cash_phy_otm_calib.calibrate_cash_model()
                try:
                    vols_sabr_in[strike_offset]['Rec'][line.Index[0]][line.Index[1]] =\
                    cash_phy_otm_calib.get_implied_vol(
                                            'put', line[5], vol_cash_star,
                        vols_sabr_in[strike_offset]['Phy'][line.Index[0]][line.Index[1]]
                                                                        )
                except ValueError:
                    vols_sabr_in[strike_offset]['Rec'][line.Index[0]][line.Index[1]] = np.nan
                    print(line.Index[0] / 12, int(line.Index[1] / 12),
                          strike_offset, "Receiver value through intrinsic")

Now we have milked our model dry in a sense that we have calibrated as many vols as we could get from market premiums in combination with what the model is able to do for us.

Visual inspection of the calibrated data

Now we essentially have three types of vols at the physical measure: cash IRR receiver, cash IRR payer and physical (cash CCP). That is exactly what we were after. So let’s inspect how the different vols relate to each other:

10y10y swaption smile
Figure 5. 10y10y swaption smile with different settlement types.

Note that if we would move to the cash IRR measure, we would observe different vols for payer and receiver for physical settlement. Therefore another interesting comparison in that space is, how physical and cash IRR forwards relate to each other. The following is a table of cash IRR forwards over physical forwards in BP:

cash over phy forwards
Figure 6. Convexity adjusted cash IRR Forwards over physical forwards in BP.

We see that the convexity adjustment is most pronounced on the bottom right of the table (which corresponds well with how zero-wide-collar prices are distributed).

As we have now completed the calibration for every spot where a market price has been observed the next logical step would be to complete the whole vol cube. As "regular" interpolation techniques are known to fail with respect to arbitrage concerns, a typical choice to arrive at a robust cube, is the SABR model. This is exactly what will be done in the next article.

References

Pietersz and Sengers: "Cash-settled swaptions: A new pricing model"

Jaeckel: "By Implication"


This is a post in the Cash vs. Physical Swaptions series.
Other posts in this series: