1
$\begingroup$

I'm trying to price a CZK swap via Quantlib with BBG data, so far nothing complicated but I can't seem to match the floating leg cashflows, and NPV, when I price my swaps, even if I find the right Par rate.

I am trying to bootstrap CZK curve by creating iborindex and then Depo / FRA / Swap RateHelper

so first I create the index,

index_ql= ql.IborIndex('PRIBOR6M',ql.Period('6M'),2,ql.CZKCurrency(),ql.CzechRepublic(),ql.ModifiedFollowing,False,
                              ql.Actual360())

then I create a helpers and add the Depo/FRA and swap data. Note that df_mapping is just a df that has the raw data (I will post it later)

helpers = []
for index,row in df_mapping.iterrows():
    if row['Type'] == 'Depo':
        rate_ql = ql.QuoteHandle(ql.SimpleQuote(row['Level']/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['Level']/100))
        #helpers.append(ql.FraRateHelper(rate_ql,int(month_to_start),index_ql))
        helpers.append(ql.FraRateHelper(rate_ql,int(month_to_start),index_ql))
    elif row['Type'] == 'swap':
        rate_ql = ql.QuoteHandle(ql.SimpleQuote(row['Level']/100))
        helpers.append(ql.SwapRateHelper(rate_ql,ql.Period(index),calendar_ql,fixed_paymentFrequency_ql,paymentconvention_ql_fixed,fixed_daycount_ql,index_ql))

then I create curve / yts / link index / engine etc ...

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

yts = ql.RelinkableYieldTermStructureHandle(curve_ql)

# Link index to discount curve
index_ql = index_ql.clone(yts)   
engine = ql.DiscountingSwapEngine(yts)

once it's created I just create a simple spot starting 10y maturity swap coupon 4%

new_swap = ql.MakeVanillaSwap(ql.Period('10Y'), index_ql, 
0.04, ql.Period('0D'), swapType=ql.VanillaSwap.Receiver,pricingEngine=engine,
                            Nominal=10e6,fixedLegTenor=ql.Period('1Y'),fixedLegDayCount=fixed_daycount_ql)

with the same data, I find NPV = -471426 vs BBG -476585, I match the dates / cashflow on fixed leg, but I don't match reset rate and floating leg amounts (but I still match the dates)

my floating leg (not PV'd)

floating leg quantlib

BBG's floating leg (see payment column)

BBG floating leg

Can you please advise? I think the issue is somewhere in the calculation of the forward rate, which doesn't seem to match, but I have no idea why. I tried to change the interepolation method but it doesn't change much.

See below the raw data, please let me know if you have any questions, thanks again for all your help, raw data

$\endgroup$
6
  • $\begingroup$ In the CZK curve construction, CZK swaps (like EUR) are bootstrapped vs 3m pribor out to 2y, then vs 6m pribor for longer maturities - this may be one reason for the differences (if you're using the pribor for both fwds and discounting) $\endgroup$
    – user35980
    Nov 8, 2023 at 7:04
  • $\begingroup$ Thanks for your reply - and yes I am using Pribor for both discounting and fwds Are you sure about the tenor difference? When I check BBG (curve #320 CZK vs 6M PRIBOR) I see depo 6M Pribor and the FRAs are all 1x7,2x8,3x9,4x10,5x11,6x12,12x18. Is there a way to grab this data from BBG? $\endgroup$
    – Gloomy
    Nov 8, 2023 at 8:51
  • $\begingroup$ In addition to my answer below, BBG seems to value Czech swaps with an OIS discounting curve. Your calculations (and mine below) have discount at 6M IBOR. Using an OIS curve which is lower than the IBOR curve will make the absolute value of the NPV higher, i.e. move it from -471k towards -476k. $\endgroup$
    – Attack68
    Nov 8, 2023 at 9:20
  • $\begingroup$ So basically 2 things I had to change to match BBG 1. Untick OIS DC Stripping, which basically creates a new Libor 6m curve from OIS dual curve stripping, so you get the same Cashflow amount 2. Set no CSA Coll Ccy (by default set to CZK OIS), so you use the same 6m libor curve to PV your coupons $\endgroup$
    – Gloomy
    Nov 8, 2023 at 11:36
  • $\begingroup$ Now that I match BBG with these settings - the question is how do they compute the new 6m Libor by OIS DC Stripping? Is there a documentation for this? I will ask BBG but checking if someone has done this here And I guess regarding point #2 - one way to fix this is to price the same swap as above but with the OIS disco curve? I will check this too and come back $\endgroup$
    – Gloomy
    Nov 8, 2023 at 11:36

1 Answer 1

1
$\begingroup$

For what its worth I priced this in my own library and got -471,275 NPV, valued as of 2nd October 2023.

I dont have a Czech holiday calendar and I assumed the convention of the swaps is Annual Act360 vs Semi Act360.

from rateslib import *

curve = Curve(
    nodes={
        dt(2023, 10, 2): 1.0, dt(2024, 4, 4): 1.0, dt(2024, 5, 4): 1.0,
        dt(2024, 6, 4): 1.0, dt(2024, 7, 4): 1.0, dt(2024, 8, 4): 1.0,
        dt(2024, 9, 4): 1.0, dt(2024, 10, 4): 1.0, dt(2025, 4, 4): 1.0,
        dt(2025, 10, 4): 1.0, dt(2026, 10, 4): 1.0, dt(2027, 10, 4): 1.0,
        dt(2028, 10, 4): 1.0, dt(2029, 10, 4): 1.0, dt(2030, 10, 4): 1.0,
        dt(2031, 10, 4): 1.0, dt(2032, 10, 4): 1.0, dt(2033, 10, 4): 1.0,
        dt(2035, 10, 4): 1.0, dt(2038, 10, 4): 1.0,
    },
    calendar="tgt",
    convention="act360",
    id="crv",
)

args = dict(curves="crv", frequency="S", termination="6m", calendar="tgt")
args2 = dict(
    curves="crv", 
    frequency="A", calendar="tgt", convention="Act360",
    leg2_frequency="S", leg2_convention="act360", leg2_fixing_method="ibor",
)
solver = Solver(
    curves=[curve],
    instruments=[
        FRA(dt(2023, 10, 4), **args),
        FRA(dt(2023, 11, 4), **args),
        FRA(dt(2023, 12, 4), **args),
        FRA(dt(2024, 1, 4), **args),
        FRA(dt(2024, 2, 4), **args),
        FRA(dt(2024, 3, 4), **args),
        FRA(dt(2024, 4, 4), **args),
        FRA(dt(2024, 10, 4), **args),
        IRS(dt(2023, 10, 4), "2y", **args2),
        IRS(dt(2023, 10, 4), "3y", **args2),
        IRS(dt(2023, 10, 4), "4y", **args2),
        IRS(dt(2023, 10, 4), "5y", **args2),
        IRS(dt(2023, 10, 4), "6y", **args2),
        IRS(dt(2023, 10, 4), "7y", **args2),
        IRS(dt(2023, 10, 4), "8y", **args2),
        IRS(dt(2023, 10, 4), "9y", **args2),
        IRS(dt(2023, 10, 4), "10y", **args2),
        IRS(dt(2023, 10, 4), "12y", **args2),
        IRS(dt(2023, 10, 4), "15y", **args2),
    ],
    s=[
        7.01, 6.955, 6.805, 6.52, 6.365, 5.988,
        5.605, 4.268, 5.355, 4.9625, 4.7853, 4.69,
        4.64068, 4.6125, 4.60105, 4.59690, 4.59501,
        4.5750, 4.56
    ],
)

And the IRS:

irs = IRS(dt(2023, 10, 4), "10Y", notional=-10e6, fixed_rate=4.0, **args2)
irs.npv(solver=solver)
# -471,275.78
irs.cashflows(solver=solver)

enter image description here

$\endgroup$
3
  • 1
    $\begingroup$ "I dont have a Czech holiday calendar" fwiw publicholidays.cz/2023-dates :) $\endgroup$ Nov 7, 2023 at 20:36
  • $\begingroup$ Thanks - maybe one thing I would add, I believe the fixed leg pays annually with a daycount of Act/360 (or at least that's what BBG has!) $\endgroup$
    – Gloomy
    Nov 8, 2023 at 8:49
  • $\begingroup$ I have updated the answer for the updated conventions. The result is closer to Quantlib $\endgroup$
    – Attack68
    Nov 8, 2023 at 9:14

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.