Diagnosing Swap Pricing Differences Through Curve and Leg Conventions
Summary
This exchange examines a mismatch between QuantLib and Bloomberg pricing for a Czech koruna interest rate swap. The questioner reports matching fixed-leg dates and the par rate but differing floating reset rates, cash flows, and net present value. A respondent offers a separate valuation using a bootstrapped curve built from deposit, FRA, and swap quotes, then prices a ten-year swap with annual fixed payments and semiannual floating payments.
That calculation produces a value close to the questioner’s QuantLib result, but it assumes a different holiday calendar and specific payment frequencies and day-count conventions. It therefore illustrates why matching a quoted par rate alone does not establish that two systems use identical market conventions or curve inputs. The response does not identify the precise source of the Bloomberg discrepancy, and its result depends on assumptions rather than a confirmed Czech market setup. The exchange is a diagnostic example, not a general proof that one pricing implementation is correct.
Key ideas
- Matching a swap’s par rate does not guarantee matching floating cash flows or present value.
- Curve construction depends on the market instruments and conventions used for deposits, FRAs, and swaps.
- Payment frequency, day-count basis, calendar, and fixing method can affect swap valuation.
- The alternative valuation is close to the reported result but relies on an assumed calendar and leg conventions.
- The exchange does not establish the exact cause of the Bloomberg mismatch.
Tags
Full text
# Quantlib - mismatch with BBG Swap
# Quantlib - mismatch with BBG Swap
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)
BBG's floating leg (see payment column)
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,
## Answer by Attack68 (score 1)
https://quant.stackexchange.com/a/77295
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)
```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.