0
$\begingroup$

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

enter image description here

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

quantlib payleg cashflow table

and this is bloomberg

enter image description here

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

$\endgroup$
1
  • 1
    $\begingroup$ I see many values in the same column are also different from Bloomberg. Would you explain those as well? $\endgroup$
    – Cloud Cho
    Commented Nov 11, 2023 at 8:22

1 Answer 1

5
$\begingroup$

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}")    

enter image description here

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]])

enter image description here

$\endgroup$
1
  • $\begingroup$ Very insightful and helpful as always. $\endgroup$ Commented Nov 11, 2023 at 21:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.