Skip to content
All library documents

Swap Forward-Rate Differences from Curve Interpolation Choices

Article Quant Q&A · Author: Gloomy

Summary

The document examines a mismatch between a QuantLib valuation and a Bloomberg valuation for a CZK swap. Most payment dates match, but a reset date associated with an interpolated forward rate differs. The questioner suspects FRA setup, while the response points to interpolation: when curve nodes have a steep slope between them, the chosen method can materially change the implied forward rate and cash flow.

The suggested diagnostic is to rebuild the curve with several interpolation schemes, including log-linear discount factors, cubic discount factors, linear or cubic zero rates, linear forwards, and spline cubic discount factors. Then compare the relevant floating coupon’s rate and amount under each curve and inspect forward rates across the interval. This helps identify whether the discrepancy is driven by interpolation. The document does not establish which method Bloomberg uses or resolve the specific valuation; matching conventions, inputs, calendars, and curve construction still need to be checked.

Key ideas

  • Forward rates between curve nodes depend on the curve interpolation method.
  • Steep changes between neighboring nodes can make interpolation choice especially consequential.
  • Reprice the same swap under several interpolation schemes and compare the affected coupon rate and amount.
  • Plotting forward rates across the interval can reveal how curve methods differ between market inputs.
  • The example does not identify Bloomberg’s settings or prove interpolation is the only possible source of mismatch.

Tags

Full text
# Quantlib FRA and interpolated rate in Swaps vs BBG valuation


# Quantlib FRA and interpolated rate in Swaps vs BBG valuation












I am building a CZK swap pricer on quantlib, and I am trying to understand my differences with Bloomberg pricing.

I believe the way I set up my FRA is wrong, the reason is because even though I match perfectly Bloomberg's pay leg after the FRA periods, I don't match bloomberg at all on one date that should be interpolated by FRA pricing,

Here is an example, I am trying to price a 1m forward 10 year maturity CZK swap, as of 31/01/2023. Both bloomberg and quantlib match on each payment date - except one on 04/03/2023. It's interesting because when I checked, the difference is indeed in the reset rate value (6.54753% for BBG and 6.02% for quantlib), which is relatively big

this is my raw curve data

See below my code with the curve, (I changed it slightly so it's more readable). Mapping below

```
'CZK': {
    'ql_index' : ql.IborIndex('PRIBOR6M',ql.Period('6M'),2,ql.CZKCurrency(),ql.CzechRepublic(),ql.ModifiedFollowing,False,
                              ql.Actual360()),
    'ql_settle_days' : 2,
    'ql_swap_calendar' : ql.CzechRepublic(),
    'ql_payment_convention_floating' : ql.ModifiedFollowing,
    'ql_payment_convention_fixed' : ql.ModifiedFollowing,
    'ql_currency' : ql.CZKCurrency(),
    'ql_payment_lag' : 0,
    'ql_payment_frequency_floating' : ql.Semiannual,
    'ql_payment_frequency_fixed' : ql.Annual,
    'ql_daycount_float' : ql.Actual360(),
    'ql_daycount_fixed' : ql.Actual360(),
    'ql_fixed_tenor' : ql.Period('1Y'),
```

and code here

```
df_mapping = 'get table above'

helpers = []

for index,row in df_mapping.iterrows():
    if row['Type'] == 'Depo':
        rate_ql = ql.QuoteHandle(ql.SimpleQuote(row[date_str]/100))
        helpers.append(ql.DepositRateHelper(rate_ql,index_ql))
    elif row['Type'] == 'FRA':
        month_to_start = row['FRAstart']
        month_end = row['FRAend']
        rate_ql = ql.QuoteHandle(ql.SimpleQuote(row[date_str]/100))
        helpers.append(ql.FraRateHelper(rate_ql,int(month_to_start),index_ql))
    elif row['Type'] == 'swap':
        tenor = row['Tenor']
        rate_ql = ql.QuoteHandle(ql.SimpleQuote(row[date_str]/100))
        helpers.append(ql.SwapRateHelper(rate_ql,ql.Period(tenor),calendar_ql,fixed_paymentFrequency_ql,paymentconvention_ql_fixed,fixed_daycount_ql,index_ql))

curve_ql = ql.PiecewiseLogLinearDiscount(date_ql,helpers,fixed_daycount_ql)
```

I then create engine / price swap with makevanillaswap func

```
yts = ql.RelinkableYieldTermStructureHandle(curve_ql)
index_ql = index_ql.clone(yts)
engine = ql.DiscountingSwapEngine(yts)
fixed_tenor = param.dict_curve_conv[curve]['ql_fixed_tenor']
new_swap = ql.MakeVanillaSwap(ql.Period('10Y'), index_ql, 0.0435, ql.Period('1M'), swapType=ql.VanillaSwap.Receiver,pricingEngine=engine,
                           Nominal=10e6,fixedLegTenor=fixed_tenor,fixedLegDayCount=fixed_daycount_ql)
```

Looking at pay leg cashflow table I see everything matches but one row ...

this is quantlib

and this is bloomberg

Very curious to hear your thoughts on this, note that I believe it is because of FRAs because I don't have this issue for curves without FRA interpolation.

Thanks again for your help, and obviously happy to answer any questions

## Answer by David Duarte (score 5)

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

I don't know what are your Bloomberg setting for the CZK, but I'm pretty sure it has to do with interpolation method.

The slope between the two nodes for determining the forward you mention is considerable, and you can try different interpolation methods to see the differences.

```
ryts = ql.RelinkableYieldTermStructureHandle()
index = index.clone(ryts)
engine = ql.DiscountingSwapEngine(ryts)

new_swap = ql.MakeVanillaSwap(
    ql.Period('10Y'),
    index, 
    0.0435, 
    ql.Period('1M'), 
    swapType=ql.VanillaSwap.Receiver,
    pricingEngine=engine,
    Nominal=10e6,
    fixedLegTenor=ql.Period('1Y'),
    fixedLegDayCount=ql.Actual360()
)

piecewiseMethods = {
    'logLinearDiscount': ql.PiecewiseLogLinearDiscount,
    'logCubicDiscount': ql.PiecewiseLogCubicDiscount,
    'linearZero': ql.PiecewiseLinearZero,
    'cubicZero': ql.PiecewiseCubicZero,
    'linearForward': ql.PiecewiseLinearForward,
    'splineCubicDiscount': ql.PiecewiseSplineCubicDiscount,
}

for name, piecewise in piecewiseMethods.items():
    crv = piecewise(2, calendar, helpers, ql.Actual365Fixed())
    ryts.linkTo(crv)

    floating_cashflows = list(map(ql.as_floating_rate_coupon, new_swap.floatingLeg()))
    fwd = floating_cashflows[1].rate()
    amount = floating_cashflows[1].amount()
    print(f"{name:>20}\t{fwd:.3%}\t{amount:,.2f}")
```

As you might know, the particular interpolation method you use can greatly influence the behavior of the forwards between your data points.

```
import matplotlib.pyplot as plt

fig, ax = plt.subplots(2, 3, figsize=(8,4), sharey=True, sharex=True)
ax = ax.flatten()
dates = [ql.Date(4,3,2023) + ql.Period(n, ql.Days) for n in range(360)]
start_dates = dates[:180]
end_dates = dates[-180:]

dc = ql.Actual360()
for idx, (name, piecewise) in enumerate(piecewiseMethods.items()):
    crv = piecewise(2, calendar, helpers, ql.Actual365Fixed())
    ryts.linkTo(crv)
    rates = [crv.forwardRate(start, end, dc, ql.Simple).rate() for start, end in zip(start_dates, end_dates)]
    ax[idx].plot(rates)
    ax[idx].set_title(name)
    ax[idx].scatter([0,180], [rates[0], rates[179]])
```

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.