2
$\begingroup$

I am trying to price the BRL zero coupon swap. As we know that ZC swaps fixed payer pays a single payment at maturity and the float payer pays the interim payments till maturity. So in this case, where is the payment frequency or schedule attribute for pricing this swap? If I follow the below-mentioned way, I am getting a huge variation in swap.NPV compared to the given NPV. Please guide me if I am following the right approach.

maturity = []
discountfactor = []
day_count = ql.Actual360()
 calendar = ql.JointCalendar(ql.UnitedStates(), ql.UnitedKingdom())
  yield_curve = ql.DiscountCurve(maturity, discountfactor, day_count, calendar)
 yield_curve.enableExtrapolation()
 curve_handle = ql.YieldTermStructureHandle(yield_curve)


 # BRL index
  BRL_index = ql.OvernightIndex('cdi', 0, ql.BRLCurrency(), ql.Brazil(), ql.Business252())
  notional = 20000000
  notional currency = 'BRL'
  fixed_rate = 0.05
  fixed_leg_daycount = ql.Business252()
  float_spread = 0.0
  float_leg_daycount = ql.Business252()
  payment frequency = ql.Once

 #applying zero coupon swap function
 swap = ql.ZeroCouponSwap(ql.Swap.Receiver, notional, start_date, end_date,
                     fixed_rate, fixed_leg_daycount, BRL_index, ql.Brazil())

 engine = ql.DiscountingSwapEngine(yield_curve)
 swap.setPricingEngine(engine)
  npv = swap.NPV
$\endgroup$
2

1 Answer 1

1
$\begingroup$

"The short answer is that the frequency of the floating payments is determined by the Schedule of an IborIndex (or OvernightIndex). Thus, if we define the following

ibor_index= ql.IborIndex(
    "MyIndex",
    ql.Period("1Y"),
    0,
    ql.SEKCurrency(),
    ql.Sweden(),
    ql.ModifiedFollowing,
    False,
    ql.Actual360(),
    ts_handle,
)

It can be interpreted as the coupons of the ZeroCouponSwap will have a frequency of 1Y, i.e. each coupon payment day will occur 1Y between.


If we consider an more in-depth answer. Then I found the definition of ZeroCouponSwap to be very helpful when researching this particular class. Furthermore, assume that we want to replicate the following amount of this particular ZeroCouponSwap to fully grasp how the floating leg of ZeroCouponSwap is priced:

import QuantLib as ql
import pandas as pd
# Set the reference date
ref_date = ql.Date(1, 1, 2022)
ql.Settings.instance().evaluationDate = ql.Date(1, 1, 2022)

dfs = [1, 0.965, 0.94]  # discount factors
dates = [
    ql.Date(1, 1, 2022),
    ql.Date(1, 1, 2023),
    ql.Date(1, 1, 2025),
]  # maturity dates of the discount factors

day_counter = ql.Actual360()

# Create the discount curve
curve = ql.DiscountCurve(dates, dfs, day_counter)

# The curve will note be linked in case we want to update the quotes later on
ts_handle = ql.YieldTermStructureHandle(curve)

custom_index= ql.IborIndex(
    "MyIndex",
    ql.Period("1Y"),
    0,
    ql.SEKCurrency(),
    ql.Sweden(),
    ql.ModifiedFollowing,
    False,
    ql.Actual360(),
    ts_handle,
)

start_date= ql.Date(1,1,2022)
end_date = ql.Date(1,1,2024)
notional= 10e6
swap = ql.ZeroCouponSwap(
    ql.Swap.Payer,
    notional,
    start_date,
    end_date,
    0.02,
    day_counter,
    custom_index,
    ql.Sweden(),
)

engine = ql.DiscountingSwapEngine(ts_handle)
swap.setPricingEngine(engine)

pd.DataFrame([{
    'fixingDate': cf.fixingDate().ISO(),
    'accrualStart': cf.accrualStartDate().ISO(),
    'accrualEnd': cf.accrualEndDate().ISO(),
    'accrualPeriod': cf.accrualPeriod(),
    "paymentDate": cf.date().ISO(),
    'forward': cf.indexFixing(),
    'rate': cf.rate(),
    "amount": cf.amount(),
} for cf in map(ql.as_floating_rate_coupon, swap.leg(1))])

The code above will result in the following values:

fixingDate accrualStart accrualEnd accrualPeriod paymentDate forward rate amount
2023-06-05 2022-02-03 2024-06-03 2.363888889 2024-06-03 0.0130119 0.021911 517947.6

Then in reference to the aforementioned link the floating rate of the coupons for ZeroCouponSwap are calculated by:

$$R^{FLT} = \left[ \prod_{k=0}^{K-1} (1+\alpha(T_{k},T_{k+1}) L(T_{k},T_{k+1})) -1 \right]$$

Thus, we aim to identify the attributes that QuantLib utilizes to calculate the floating rate amount. However, upon inspecting the source code of QuantLib, one will observe that the floating leg of the ZeroCouponSwap comprises simply of SubPeriodsCoupon. To simplify this process, we can use the following code to determine the exact attributes that are employed (as I have understood the floating coupon payments can not be found in the original ZeroCouponSwap class):

import QuantLib as ql
import pandas as pd
# Set the reference date
ql.Settings.instance().evaluationDate = ql.Date(1, 1, 2022)

dfs = [1, 0.965, 0.94]  # discount factors
dates = [
    ql.Date(1, 1, 2022),
    ql.Date(1, 1, 2023),
    ql.Date(1, 1, 2025),
]  # maturity dates of the discount factors

day_counter = ql.Actual360()

# Create the discount curve
curve = ql.DiscountCurve(dates, dfs, day_counter)

# The curve will note be linked in case we want to update the quotes later on
ts_handle = ql.YieldTermStructureHandle(curve)

custom_index= ql.IborIndex(
    "MyIndex",
    ql.Period("1Y"),
    0,
    ql.SEKCurrency(),
    ql.Sweden(),
    ql.ModifiedFollowing,
    False,
    ql.Actual360(),
    ts_handle,
)

start_date= ql.Date(1,1,2022)
end_date = ql.Date(1,1,2024)



coupon = ql.SubPeriodsCoupon(ql.Date(3,6,2024), 1e7, start_date, end_date, 0, custom_index)

coupon.setPricer(ql.CompoundingRatePricer())
print(f'{coupon.amount() = }')

This will give us the identical amount as ZeroCouponSwap. Thus, given the equation that was found in the documentation we arrive at

coupon_fixing_dates = coupon.fixingDates()
sub_period_fixings = []
for fixing_date in coupon_fixing_dates:
    sub_period_fixings.append(custom_index.fixing(fixing_date))
sub_period_fractions = coupon.dt()
compound_factor = 1 
for fixing, frac in zip(sub_period_fixings, sub_period_fractions):
    compound_factor *= 1+fixing * frac 
rate = ((compound_factor-1) / coupon.accrualPeriod()) 

Which produces the number 0.021910825388354933. Which is the number that is used to produce the amount of the floating leg. Using this value multiplied by $10^6$ and accrualPeriod we arrive at the exact amount.


I expect the most efficient way to view coupon payment days is to use coupon.valueDates() which is created with ql.MakeSchedule using the start_date, end_date and ql.Period to see the cashflows of the floating leg.

$\endgroup$
4
  • $\begingroup$ Xiapedia Thank you for the effort and time. Based on your reply, the key takeaways are: 1) My approach to price BRL zero coupon swap seems correct. 2) The payment frequency is irrelevant if I assume BRL as overnight index. However, I can always set the frequency to ql.Once in the iborIndex $\endgroup$
    – John83
    Nov 29, 2023 at 2:35
  • $\begingroup$ 3) The BRL can be defined as ql.OvernightIndex('brlcdi',0,ql.BRLCurrency,ql.Brazil(),ql.Business252()) or ql.IborIndex( "brlcdi",ql.Period("0D"),0,ql.BRLCurrency(),ql.Brazil(),ql.ModifiedFollowing,False,ql.Business252(),ts_handle) 4) The Quantlib zerocouponswap class attributes have the required setting to price and there is no need to compute Float or Fixed Leg separately. $\endgroup$
    – John83
    Nov 29, 2023 at 2:35
  • $\begingroup$ I am glad that you found my answer helpful. Please consider marking it as the solution for future users. $\endgroup$
    – Xiarpedia
    Nov 29, 2023 at 8:26
  • $\begingroup$ @ Xiarpedia. Sure thing. In my next post, I will discuss achieving zero coupon swap pricing by NOT using ql.ZeroCouponSwap class, instead by applying other ways. $\endgroup$
    – John83
    Nov 29, 2023 at 19: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.