Skip to content
All library documents

Decomposing Swaption Bid-Offer Liquidity Costs into SABR Risks

Article Quant Q&A · Author: Jonita D'souza

Summary

The document outlines ways to allocate swaption bid-offer liquidity costs across SABR parameters. With beta held fixed, alpha represents the at-the-money volatility level, nu captures smile curvature, and rho captures skew. First estimate how the option value responds to each parameter, using finite differences and repricing. A top-down method calibrates the model separately to bid and offer volatility quotes, then multiplies parameter differences by the position’s sensitivities. A bottom-up method uses a strike-by-parameter Jacobian to relate quote spreads to parameter effects.

A worked example illustrates the parameter charges and shows how different strikes emphasize different components; wing quotes can make nu more influential, while rho reflects skew. These decompositions are first-order approximations: nonlinear calibration, interactions, an overdetermined system, and imperfect fit to the market smile can leave residual spread unexplained. The document notes that calibration can be unstable, especially for nu and rho, so parameter constraints or a residual bucket may be appropriate. The example is illustrative and does not establish a universal allocation.

Key ideas

  • SABR alpha, nu, and rho describe volatility level, smile curvature, and skew, respectively.
  • Finite-difference repricing can estimate option value sensitivity to each parameter.
  • Top-down allocation compares parameters calibrated to bid and offer quotes.
  • Bottom-up allocation maps quote spreads into parameter effects through a Jacobian.
  • First-order allocations may not sum to the full spread because of nonlinearities, interactions, and model fit error.

Tags

Full text
# Charges to parameters of SABR model for a swaption


# Charges to parameters of SABR model for a swaption












I have forward premium for a Swaption. How will I allocate the charges to parameters of SABR model for risk based Liquidity Reserve Calculation?

So, I just want to understand how forward bid-offer spreads quoted by brokers are allocated to SABR parameter, to understand the liquidity cost.

## Answer by Dimitri Vulis (score 1)

https://quant.stackexchange.com/a/85536

SABR model uses 3 model parameters: alpha is the at the money vol, rho is the skew, nu is the vol of vol. But this discussion can be generalized to other pricing models.

The brokers quote bid-offer spreads for different strikes - out of the money, at the money, in the money.

The model fair value of a swaption depends on these 3 model parameters and on the forward rate.

Calculate the sensitivities of the model fair value to each model parameter. These are sometimes called "model greeks". You can calculate these numerically by bumping the model parameters up and down a little and re-running the pricing model. The sensitivity of the fair value to the alpha is like vega.

Having the sensitivities, you have (at least) two methodologies to set the liquidity reserve. The result shouldn't be very different between them.

- bottom up - invert the Jacobian matrix that relates the bid-offer spreads quoted by brokers and the sensitivities to the model parameters.

- top down - from broker quotes, calibrate the model parameters from all bid side quotes and all offer side quotes. The liquidity reserve for each model parameter is the sensitivity to this parameter times the difference between "offer" and "bid" values for this parameter.

## Answer by pandashark (score 1)

https://quant.stackexchange.com/a/85569

In short: calibrate SABR to bid and offer quotes separately, compare the fitted parameters, and multiply each parameter difference by the position's sensitivity to that parameter. The worked example below shows how.

Building on Dimitri Vulis's answer, here is a worked example using QuantLib.

#### Setup

In SABR, $\beta$ is typically fixed by convention (commonly 0 or 0.5 for rates), leaving three free parameters:

- $\alpha$ — ATM vol level

- $\nu$ — vol-of-vol, controls smile curvature (butterfly)

- $\rho$ — correlation, controls skew (risk reversal)

Given broker-quoted bid-offer spreads across strikes, how much of the liquidity cost is attributable to each parameter?

#### Approach 1: Top-Down (Separate Bid/Offer Calibration)

Calibrate SABR to bid vols and offer vols independently. The parameter differences give per-parameter uncertainty bands. Using QuantLib's `SABRInterpolation` on a stylized 5Y10Y swaption smile ($\beta=0.5$ fixed):

```
              alpha      nu       rho
  Mid:      0.0284    0.4473   -0.0478
  Bid:      0.0288    0.3920   -0.0888
  Offer:    0.0281    0.4996   -0.0130
  ─────────────────────────────────────
  Δ(param): -0.0007   0.1076    0.0757
```

The liquidity charge per parameter is $\left|\frac{\partial V}{\partial \theta_j}\right| \times |\Delta\theta_j|$, where the model greeks are computed by bumping each SABR parameter and repricing via `sabrVolatility()` + `blackFormula()`:

```
ATM liquidity reserve decomposition (per unit notional):
  alpha charge = 0.0115 bp  (38%)
  nu charge    = 0.0170 bp  (56%)
  rho charge   = 0.0019 bp  ( 6%)
  TOTAL        = 0.0304 bp
```

Caveat: This approach can be fragile. SABR calibration is nonlinear, and small perturbations in input vols can produce disproportionate parameter shifts, especially for $\nu$ and $\rho$ which are sensitive to wing data. Some desks regularize by constraining certain parameters across bid/offer.

#### Approach 2: Bottom-Up (Jacobian Decomposition)

Calibrate SABR to mid vols once, then compute the Jacobian $J_{ij} = \frac{\partial \sigma_i}{\partial \theta_j}$ at each strike. Convert to price space via vega:

```
  Strike   BidOffer$   Alpha$     Nu$       Rho$     Total$
  ──────────────────────────────────────────────────────────
  1.0%     0.000160   0.000026  0.000239  0.000040  0.000305
  2.0%     0.000151   0.000068  0.000296  0.000069  0.000433
  2.5%     0.000131   0.000098  0.000229  0.000048  0.000374
  3.0%     0.000105   0.000115  0.000170  0.000019  0.000304  ← ATM
  3.5%     0.000157   0.000108  0.000220  0.000088  0.000416
  4.0%     0.000233   0.000088  0.000305  0.000118  0.000511
  5.0%     0.000508   0.000056  0.000374  0.000111  0.000542
```

#### Interpretation

The table shows the SABR structure at work:



- Wings (deep OTM): $\nu$ charge dominates — vol-of-vol controls smile curvature

- Skew (asymmetry between low/high strikes): $\rho$ contributes — it controls the tilt

This decomposition is a first-order approximation. The sum of per-parameter charges won't exactly equal the total bid-offer spread due to: (1) cross-terms in the Taylor expansion, (2) the overdetermined system (more strikes than parameters) leaves a least-squares residual, and (3) the market smile is not exactly SABR-shaped. Many desks add an "unexplained residual" bucket. A practical alternative is bump-and-reprice, which captures nonlinearities automatically.

#### Regulatory Context

This decomposition applies to Prudent Valuation (PVA) under CRR2 Article 105, where banks compute close-out cost AVAs from bid-offer spreads. It also informs FRTB IMA liquidity horizon assignments.

#### QuantLib Code

```
#include <ql/quantlib.hpp>
using namespace QuantLib;

// Market data: 5Y10Y swaption smile
Real forward = 0.03;
Time expiry  = 5.0;
std::vector<Real> strikes = {0.01, 0.02, 0.025, 0.03, 0.035, 0.04, 0.05};
std::vector<Real> midVols = {0.35, 0.22, 0.195, 0.18, 0.175, 0.178, 0.195};

// Calibrate SABR (beta=0.5 fixed)
SABRInterpolation sabr(
    strikes.begin(), strikes.end(), midVols.begin(),
    expiry, forward,
    Null<Real>(), 0.5, Null<Real>(), Null<Real>(),
    false, true, false, false,  // alpha,nu,rho free; beta fixed
    true,                        // vega-weighted
    ext::make_shared<EndCriteria>(100000, 100, 1e-8, 1e-8, 1e-8));
sabr.update();
// sabr.alpha(), sabr.nu(), sabr.rho() now hold calibrated params

// Model greeks by finite difference
Real bump = 1e-4;
Real volUp = sabrVolatility(K, forward, expiry, alpha+bump, beta, nu, rho);
Real volDn = sabrVolatility(K, forward, expiry, alpha-bump, beta, nu, rho);
Real dVoldAlpha = (volUp - volDn) / (2.0 * bump);

// Convert to price: dPrice/dParam = vega * dVol/dParam
Real vega = blackFormulaStdDevDerivative(K, forward, midVol*sqrt(expiry))
            * sqrt(expiry);
Real chargeAlpha = fabs(vega * dVoldAlpha * deltaAlpha);
```

```
/* Demonstrates how to decompose swaption bid-offer spreads into
   SABR parameter charges for liquidity reserve calculation.

   Two approaches:
   1. Top-down: calibrate SABR to bid vols and offer vols separately,
      then compute per-parameter reserves from the parameter differences.
   2. Bottom-up (Jacobian): compute sensitivities of option prices to
      SABR parameters, invert to map bid-offer price spreads back to
      parameter space.
*/

#include <ql/quantlib.hpp>
#include <iostream>
#include <iomanip>
#include <vector>
#include <cmath>

using namespace QuantLib;

int main() {
    std::cout << std::fixed << std::setprecision(6);

    // ---------------------------------------------------------------
    // Market data: 5Y10Y swaption smile (lognormal vols)
    // ---------------------------------------------------------------
    Real forward = 0.03;          // 3% forward swap rate
    Time expiry  = 5.0;           // 5Y option expiry

    // Strikes: -200bp to +200bp around forward
    std::vector<Real> strikes = {0.01, 0.02, 0.025, 0.03, 0.035, 0.04, 0.05};

    // Mid-market vols (lognormal)
    std::vector<Real> midVols = {0.35, 0.22, 0.195, 0.18, 0.175, 0.178, 0.195};

    // Bid-offer half-spreads in vol points (wider for wings)
    std::vector<Real> halfSpreadVol = {0.015, 0.005, 0.003, 0.002, 0.003, 0.005, 0.015};

    // Construct bid and offer vol vectors
    std::vector<Real> bidVols(strikes.size()), offerVols(strikes.size());
    for (Size i = 0; i < strikes.size(); ++i) {
        bidVols[i]   = midVols[i] - halfSpreadVol[i];
        offerVols[i] = midVols[i] + halfSpreadVol[i];
    }

    // ---------------------------------------------------------------
    // SABR calibration: mid, bid, offer
    // ---------------------------------------------------------------
    // Fix beta = 0.5 (common convention for rates)
    Real betaFixed = 0.5;

    auto calibrate = [&](const std::vector<Real>& vols, const std::string& label) {
        SABRInterpolation sabr(
            strikes.begin(), strikes.end(), vols.begin(),
            expiry, forward,
            Null<Real>(), betaFixed, Null<Real>(), Null<Real>(),
            false, true, false, false,   // alpha,nu,rho free; beta fixed
            true,                         // vega weighted
            ext::shared_ptr<EndCriteria>(new EndCriteria(100000, 100, 1e-8, 1e-8, 1e-8)),
            ext::shared_ptr<OptimizationMethod>(),
            0.0020, false, 50, 0.0,
            VolatilityType::ShiftedLognormal);
        sabr.update();

        std::cout << label << ":\n"
                  << "  alpha = " << sabr.alpha()
                  << "  beta = " << sabr.beta()
                  << "  nu = " << sabr.nu()
                  << "  rho = " << sabr.rho()
                  << "  (rms err = " << sabr.rmsError() << ")\n";

        return std::make_tuple(sabr.alpha(), sabr.beta(), sabr.nu(), sabr.rho());
    };

    std::cout << "=== SABR Calibration Results ===\n\n";

    auto [alphaMid, betaMid, nuMid, rhoMid]     = calibrate(midVols, "Mid");
    auto [alphaBid, betaBid, nuBid, rhoBid]      = calibrate(bidVols, "Bid");
    auto [alphaOffer, betaOffer, nuOffer, rhoOffer] = calibrate(offerVols, "Offer");

    // ---------------------------------------------------------------
    // Approach 1: Top-down parameter charge decomposition
    // ---------------------------------------------------------------
    std::cout << "\n=== Approach 1: Top-Down (Calibrate Bid & Offer Separately) ===\n\n";

    Real dAlpha = alphaOffer - alphaBid;
    Real dNu    = nuOffer - nuBid;
    Real dRho   = rhoOffer - rhoBid;

    std::cout << "Parameter bid-offer ranges:\n"
              << "  delta(alpha) = " << dAlpha << "\n"
              << "  delta(nu)    = " << dNu << "\n"
              << "  delta(rho)   = " << dRho << "\n";

    // Compute model greeks (sensitivities) by finite difference on ATM price
    Real bump = 1e-4;
    Real atmMidVol = sabrVolatility(forward, forward, expiry,
                                     alphaMid, betaMid, nuMid, rhoMid);
    Real atmPrice = blackFormula(Option::Call, forward, forward,
                                  atmMidVol * std::sqrt(expiry));

    // d(price)/d(alpha)
    Real volUp = sabrVolatility(forward, forward, expiry,
                                 alphaMid + bump, betaMid, nuMid, rhoMid);
    Real volDn = sabrVolatility(forward, forward, expiry,
                                 alphaMid - bump, betaMid, nuMid, rhoMid);
    Real dPdAlpha = (blackFormula(Option::Call, forward, forward, volUp * std::sqrt(expiry))
                   - blackFormula(Option::Call, forward, forward, volDn * std::sqrt(expiry)))
                   / (2.0 * bump);

    // d(price)/d(nu)
    volUp = sabrVolatility(forward, forward, expiry,
                            alphaMid, betaMid, nuMid + bump, rhoMid);
    volDn = sabrVolatility(forward, forward, expiry,
                            alphaMid, betaMid, nuMid - bump, rhoMid);
    Real dPdNu = (blackFormula(Option::Call, forward, forward, volUp * std::sqrt(expiry))
                - blackFormula(Option::Call, forward, forward, volDn * std::sqrt(expiry)))
                / (2.0 * bump);

    // d(price)/d(rho)
    volUp = sabrVolatility(forward, forward, expiry,
                            alphaMid, betaMid, nuMid, std::min(rhoMid + bump, 0.9999));
    volDn = sabrVolatility(forward, forward, expiry,
                            alphaMid, betaMid, nuMid, std::max(rhoMid - bump, -0.9999));
    Real dPdRho = (blackFormula(Option::Call, forward, forward, volUp * std::sqrt(expiry))
                 - blackFormula(Option::Call, forward, forward, volDn * std::sqrt(expiry)))
                 / (2.0 * bump);

    std::cout << "\nATM model greeks (sensitivities to SABR params):\n"
              << "  dPrice/dAlpha = " << dPdAlpha << "\n"
              << "  dPrice/dNu    = " << dPdNu << "\n"
              << "  dPrice/dRho   = " << dPdRho << "\n";

    Real chargeAlpha = std::fabs(dPdAlpha * dAlpha);
    Real chargeNu    = std::fabs(dPdNu * dNu);
    Real chargeRho   = std::fabs(dPdRho * dRho);
    Real totalCharge = chargeAlpha + chargeNu + chargeRho;

    std::cout << "\nLiquidity reserve decomposition (ATM, per unit notional):\n"
              << "  alpha charge = " << chargeAlpha
              << "  (" << 100.0 * chargeAlpha / totalCharge << "%)\n"
              << "  nu charge    = " << chargeNu
              << "  (" << 100.0 * chargeNu / totalCharge << "%)\n"
              << "  rho charge   = " << chargeRho
              << "  (" << 100.0 * chargeRho / totalCharge << "%)\n"
              << "  TOTAL        = " << totalCharge << "\n";

    // ---------------------------------------------------------------
    // Approach 2: Bottom-up Jacobian at multiple strikes
    // ---------------------------------------------------------------
    std::cout << "\n=== Approach 2: Bottom-Up (Jacobian at Each Strike) ===\n\n";

    std::cout << std::setw(10) << "Strike"
              << std::setw(12) << "BidOffer"
              << std::setw(12) << "Alpha$"
          << std::setw(12) << "Nu$"
              << std::setw(12) << "Rho$"
          << std::setw(12) << "Total$" << "\n";
    std::cout << std::string(70, '-') << "\n";

    for (Size i = 0; i < strikes.size(); ++i) {
        Real K = strikes[i];
        Real bidOfferPrice = blackFormula(Option::Call, K, forward,
                                           offerVols[i] * std::sqrt(expiry))
                           - blackFormula(Option::Call, K, forward,
                                           bidVols[i] * std::sqrt(expiry));

        // Jacobian row: d(vol)/d(param) at this strike, using mid params
        auto sabrVol = [&](Real a, Real n, Real r) {
            return sabrVolatility(K, forward, expiry, a, betaMid, n, r);
        };

        Real dvdA = (sabrVol(alphaMid + bump, nuMid, rhoMid)
                   - sabrVol(alphaMid - bump, nuMid, rhoMid)) / (2.0 * bump);
        Real dvdN = (sabrVol(alphaMid, nuMid + bump, rhoMid)
                   - sabrVol(alphaMid, nuMid - bump, rhoMid)) / (2.0 * bump);
        Real dvdR = (sabrVol(alphaMid, nuMid, std::min(rhoMid + bump, 0.9999))
                   - sabrVol(alphaMid, nuMid, std::max(rhoMid - bump, -0.9999))) / (2.0 * bump);

        // Convert vol sensitivity to price sensitivity via vega
        Real midVol_i = sabrVol(alphaMid, nuMid, rhoMid);
        Real vega = blackFormulaStdDevDerivative(K, forward,
                                                   midVol_i * std::sqrt(expiry))
                    * std::sqrt(expiry);

        Real chA = std::fabs(vega * dvdA * dAlpha);
        Real chN = std::fabs(vega * dvdN * dNu);
        Real chR = std::fabs(vega * dvdR * dRho);

        std::cout << std::setw(10) << K
                  << std::setw(12) << bidOfferPrice
                  << std::setw(12) << chA
                  << std::setw(12) << chN
                  << std::setw(12) << chR
                  << std::setw(12) << chA + chN + chR << "\n";
    }

    std::cout << "\nDone.\n";
    return 0;
}
```

Compile against QuantLib: `clang++ -std=c++17 -O2 -I/path/to/quantlib -lQuantLib -o sabr_reserve sabr_liquidity_reserve.cpp`

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.