|
From: Aditya G. <adi...@co...> - 2022-08-12 08:58:36
|
Hi Peter,
For point 2) the LinearTsrPricer can take the input as ql.ConstantSwaptionVolatilityStructure, but the Lognormal pricer complains by saying: " if only an atm surface is given, the volatility type must be inherited" So I guess it is not possible. To create a ‘constant volatility cube’, I instantiated an atmMatrix with a constant value, but all the OTM and ITM swaptions had a volatility of 0. This way, for every triplet (strike, expiry, tenor), QL would return a constant volatility. It is a workaround, but it sufficed for my purpose.
For 3) The code and data for the extrapolation is below. Some notes:
volHandle at the end is passed into the pricer. The data is not complete but should be enough for debugging purposes. The BBG/QL volatility cubes seem to match to a decent degree (i.e. I don’t believe it is the source of the error). The volatilities are within 10 bps when testing random data points. However, the atm strike does not change (even if the volatility is constant) and does not agree with Bloomberg.
normalVolCube =
Expiry
1Yr
2Yr
3Yr
4Yr
5Yr
7Yr
10Yr
12Yr
15Yr
20Yr
25Yr
30Yr
1Mo
153.65
160.51
151.81
143.8
141.03
129.88
123.16
118.88
110.61
108.9
104.21
101.91
3Mo
150.06
152.12
149.09
140.08
134.79
125.74
118.65
115.21
112.26
107.13
103.41
99.43
6Mo
154.30
150.74
143.9
137.09
132.84
123.31
115.03
111.31
107.88
103.41
100.5
96.31
9Mo
155.29
149.86
142.87
135.17
130.97
121.33
112.06
108.57
105.04
99.95
97.12
93.19
1Yr
155.98
149.11
140.78
133.39
127.82
118.13
109.42
106.32
102.06
97.15
94.05
90.8
2Yr
142.61
135.71
129.64
123.34
118.42
110
102.05
99.13
95.18
89.95
86.93
84.38
3Yr
130.05
124.03
117.71
113.72
109.87
102.87
96.37
93.46
89.5
84.21
81.15
79.07
4Yr
118.85
113.22
108.13
104.87
102.41
97.49
91.33
88.78
85.31
79.42
77.09
74.93
5Yr
109.73
104.94
102.2
99.09
96.85
92.53
87.38
84.84
81.37
76.05
73.36
72.24
6Yr
102.35
99.42
97.38
94.42
92.18
88.36
83.64
81.19
77.88
72.88
70.54
69.33
7Yr
96.52
93.55
91.44
89.36
87.41
84.15
79.99
77.68
74.58
70.13
68.25
66.85
8Yr
91.55
89.03
87.38
85.58
83.78
80.83
77.08
74.82
71.81
67.42
65.22
64.37
9Yr
86.91
85.11
83.57
81.89
80.19
77.48
74.07
71.89
68.99
64.76
62.82
62.05
10Yr
83.39
81.29
79.55
77.84
76.4
74.03
71.19
69.2
66.54
62.56
60.74
60.1
12Yr
77.24
75.44
74
72.38
70.97
69.1
66.47
64.68
62.27
58.88
57.3
56.85
15Yr
68.34
67.24
65.61
64.58
64.34
62.11
60.25
58.73
56.7
53.89
52.72
52.28
20Yr
60.48
60.35
59.15
58.46
57.75
56.64
55.07
53.74
51.97
49.87
48.96
48.63
25Yr
58.04
56.62
55.74
55.16
54.62
53.59
52.25
51.07
49.5
47.49
46.72
46.08
30Yr
54.83
53.31
52.73
52.34
51.96
51.06
49.7
48.76
47.52
45.21
44.11
44.1
strikeSpreadData Snippet:
Term x Tenor
-200bps
-100bps
-50bps
-25bps
ATM
25bps
50bps
100bps
200bps
3Mo X 1Yr
163.06
147.61
145.65
147.02
150.06
154.64
160.47
174.74
208.13
3Mo X 2Yr
168.32
152.63
149.59
150.1
152.12
155.6
160.36
172.72
203.32
3Mo X 3Yr
165.94
150.14
146.87
147.22
149.09
152.42
157.05
169.18
199.38
3Mo X 5Yr
154.44
137.68
133.49
133.36
134.79
137.76
142.09
153.73
182.89
3Mo X 7Yr
145.98
128.92
124.5
124.31
125.74
128.76
133.16
144.93
173.96
3Mo X 10Yr
141.64
123.6
118.33
117.66
118.65
121.3
125.42
136.78
165.14
3Mo X 12Yr
124.87
115.74
113.96
114.17
115.21
117.06
119.67
126.74
145.33
3Mo X 15Yr
137.53
118.56
112.58
111.56
112.26
114.72
118.73
130
158.17
3Mo X 20Yr
133.96
114.4
107.91
106.63
107.13
109.46
113.42
124.65
152.72
3Mo X 25Yr
130.47
110.79
104.19
102.88
103.41
105.83
109.9
121.34
149.56
3Mo X 30Yr
126.66
106.9
100.19
98.87
99.43
101.94
106.13
117.77
146.12
6Mo X 1Yr
153.08
147.26
148.88
151.13
154.3
158.3
163.02
174.1
199.93
6Mo X 2Yr
155.61
147.49
147.3
148.56
150.74
153.82
157.69
167.34
191.18
6Mo X 3Yr
151.41
141.74
140.85
141.85
143.9
146.93
150.84
160.73
185.27
6Mo X 5Yr
144.96
132.89
130.7
131.19
132.84
135.64
139.46
149.43
174.44
6Mo X 7Yr
137.51
124.48
121.67
121.87
123.31
125.95
129.67
139.51
164.24
6Mo X 10Yr
131.51
117.49
113.98
113.86
115.03
117.47
121.05
130.73
155.14
6Mo X 12Yr
119.63
111.38
109.93
110.24
111.31
113.15
115.68
122.45
140.14
6Mo X 15Yr
126.39
111.49
107.36
106.95
107.88
110.16
113.64
123.22
147.39
6Mo X 20Yr
123.32
107.85
103.29
102.66
103.41
105.58
109
118.53
142.61
6Mo X 25Yr
120.86
105.2
100.48
99.78
100.5
102.67
106.1
115.7
139.82
# ATM Volatility matrix
volType = ql.Normal
flatExtrapolation = False
atmSwapTenors = [ql.Period(tenor[:-1]) for tenor in normalVolCube.columns[1:]]
atmOptionTenors = [ql.Period(tenor[:-1]) for tenor in normalVolCube['Expiry']]
normalVols = normalVolCube[normalVolCube.columns[1:]].apply(
lambda x: x * 1e-4).values.tolist()
swaptionVolMatrix = ql.SwaptionVolatilityMatrix(calendar, convention,
atmOptionTenors, atmSwapTenors,
ql.Matrix(normalVols),
dayCounter, flatExtrapolation,
volType)
swaptionVolMatrixHandle = ql.SwaptionVolatilityStructureHandle(
swaptionVolMatrix)
strikeSpreads = [-200, -100, -50, -25, 0, 25, 50, 100, 200]
strikeSpreads = [x * 1e-4 for x in strikeSpreads]
optionTenors = [
ql.Period(3, ql.Months),
ql.Period(6, ql.Months),
ql.Period(9, ql.Months),
ql.Period(1, ql.Years),
ql.Period(3, ql.Years),
ql.Period(5, ql.Years),
ql.Period(7, ql.Years),
ql.Period(10, ql.Years),
ql.Period(15, ql.Years),
ql.Period(20, ql.Years),
ql.Period(30, ql.Years)
]
swapTenors = [
ql.Period(1, ql.Years),
ql.Period(2, ql.Years),
ql.Period(3, ql.Years),
ql.Period(5, ql.Years),
ql.Period(7, ql.Years),
ql.Period(10, ql.Years),
ql.Period(12, ql.Years),
ql.Period(15, ql.Years),
ql.Period(20, ql.Years),
ql.Period(25, ql.Years),
ql.Period(30, ql.Years)
]
nRows = len(optionTenors) * len(swapTenors)
nCols = len(strikeSpreads)
volSpreadsMatrix = strikeSpreadData[strikeSpreadData.columns[1:]].apply(
lambda x: x).values.tolist()
volSpreads = []
for i in range(nRows):
volSpreadsRow = []
for j in range(nCols):
volSpreadsRow.append(
ql.QuoteHandle(ql.SimpleQuote(volSpreadsMatrix[i][j])))
volSpreads.append(volSpreadsRow)
vegaWeightedSmileFit = False
volCube = ql.SwaptionVolCube2(swaptionVolMatrixHandle, optionTenors,
swapTenors, strikeSpreads, volSpreads,
swapIndex1, swapIndex2, vegaWeightedSmileFit)
volHandle = ql.SwaptionVolatilityStructureHandle(volCube)
volHandle.enableExtrapolation()
Thank you!
-----Original Message-----
From: Peter Caspers <pca...@gm...>
Sent: Friday, August 12, 2022 4:20 PM
To: Aditya Gupta <adi...@co...>
Cc: qua...@li...
Subject: Re: [Quantlib-users] CMS Spread Cap Pricing
[You don't often get email from pca...@gm...<mailto:pca...@gm...>. Learn why this is important at https://aka.ms/LearnAboutSenderIdentification ]
CAUTION: EXTERNAL Sender
Hi Aditya
1. correct
2. the pricer takes any SwaptionVolatilityStructure, i.e. a ConstantSwaptionVolatilityStructure should work. Is that what you mean?
3. this might be related to smile extrapolation, which strike range do you cover in your swaption cube and how do you set up the extrapolation? The pricer uses an integration over [-200%, +200%] by default.
4. I agree 500% is much to high, let's discuss further once 3 is resolved, i.e. QL / BBG matches
In addition, the linear TSR model might not work well when the time to expiry is large. Do you have more info on which model BBG uses?
Thank you
Peter
On Fri, 12 Aug 2022 at 09:36, Aditya Gupta <adi...@co...<mailto:adi...@co...>> wrote:
>
> Hi,
>
>
>
> I am trying to price a CMS Spread Cap using Quantlib. I am using data from Bloomberg, and I am using the following classes:
>
>
>
> yts is a bootstrapped Sofr curve
>
> Instrument = ql.CappedFlooredCmsSpreadCoupon()
>
> volCube = ql.SwaptionVolCube2() with the correct data and volType =
> ql.Normal
>
> cmsPricer = ql.LinearTSRPricer(volHandle, meanReversion, yts)
>
> pricer = ql.LognormalCmsSpreadPricer(cmsPricer, correlation, yts, 16)
>
>
>
> I have a few questions.
>
>
>
> It seems to me that to price a ql. CappedFlooredCmsSpreadCoupon, the only option is to use a ql.LognormalCmsSpreadPricer, and hence if we use Normal volatility, the input cmsPricer must be the LinearTSRPricer (Hagan cannot be used). Is my assessment correct?
> In order to check the pricing, I used a ‘constant’ volatility cube (set the atm matrix to a constant, rest to 0) and tested the pricing. The instrument was priced correctly (within the error from the discount curve). Can the pricer accept a constant volatility otherwise?
> The volCube seems to be returning the correct volatilities (within reason) when compared to Bloomberg, and is being passed to the pricer without error. But for some reason, the price is completely different (about half of what it should be). What could be the cause for the error, or how could I go about debugging this?
> If I use scipy.optimize to ‘artificially’ optimize the price as a function of the mean reversion, for a value of around 5 (which is way too high for the mean correlation), I was able to bring the error down to less than 1 dollar. Clearly this approach is not sensible. So I used the HullWhite model on the swaption matrix, and calibrated the mean reversion. But the error for this value of the mean reversion is much too high. How can I determine a good value for the mean reversion?
>
>
>
> Right now I am content with there being some error compared to Bloomberg, but I am unable to understand the blackbox behind the pricing engine and the mean reversion calibration, and if there is any way to solve these issues. Any and all help would be greatly appreciated. Thank you.
>
>
>
> Some of the code and data is as follows:
>
>
>
> today = ql.Date(25, 7, 2022)
>
> ql.Settings.instance().evaluationDate = today
>
>
>
> #Global variables
>
> calendar = ql.UnitedStates(ql.UnitedStates.FederalReserve)
>
> convention = ql.ModifiedFollowing
>
> endOfMonth = False
>
> dayCounter = ql.Actual360()
>
> fixingDays = 0
>
> compounding = ql.Compounded
>
> compoundingFrequency = ql.Annual
>
>
>
> yts = ql.RelinkableYieldTermStructureHandle()
>
> index = ql.Sofr(yts)
>
>
>
> helper = []
>
>
>
> # sofrData are just the rates from a dataframe
>
>
>
> helper += [
>
> ql.DepositRateHelper(
>
> ql.QuoteHandle(ql.SimpleQuote(sofrData['Shifted Rate'][0] /
> 100.0)),
>
> index)
>
> ]
>
>
>
> helper += [
>
> ql.OISRateHelper(0,
>
> ql.Period(expiration),
>
> ql.QuoteHandle(ql.SimpleQuote(rate / 100.0)),
>
> index,
>
> paymentLag=0,
>
> paymentConvention=convention,
>
> paymentFrequency=ql.Annual,
>
> paymentCalendar=calendar,
>
> averagingMethod=ql.RateAveraging.Compound) for
> rate,
>
> expiration in zip(sofrData['Shifted Rate'][1:],
> sofrData["Term"][1:])
>
> ]
>
>
>
> curve = ql.PiecewiseSplineCubicDiscount(today, helper, ql.Actual360())
>
> curve.enableExtrapolation()
>
> yts.linkTo(curve)
>
>
>
> meanReversion = ql.QuoteHandle(ql.SimpleQuote(5.037702498779181))
> #this is the ‘optimised’ value but I need further help
>
> correlation = ql.QuoteHandle(ql.SimpleQuote(36.6 * 1e-4))
>
>
>
> nominal = 1e9
>
> gearing = 1.0
>
> spread = 0.0
>
> cap = -8 * 1e-4
>
> floor = ql.nullDouble() #infinite floor
>
>
>
> startDate = ql.Date(26, 9, 2022)
>
> endDate = ql.Date(26, 9, 2023)
>
> paymentDate = ql.Date(26, 9, 2023)
>
>
>
> isInArrears = False
>
> exCouponDate = ql.Date()
>
>
>
> fixingDays = 0
>
>
>
> swapIndex1 = ql.UsdLiborSwapIsdaFixAm(ql.Period('30y'), yts, yts)
>
> ql.IndexManager.instance().clearHistory(swapIndex1.name())
>
>
>
> swapIndex2 = ql.UsdLiborSwapIsdaFixAm(ql.Period('10y'), yts, yts)
>
> ql.IndexManager.instance().clearHistory(swapIndex2.name())
>
>
>
> spreadIndex = ql.SwapSpreadIndex("Joint Index", swapIndex1,
> swapIndex2)
>
>
>
> instrument = ql.CappedFlooredCmsSpreadCoupon(paymentDate, nominal,
> startDate,
>
> endDate, fixingDays,
> spreadIndex,
>
> gearing, spread, cap,
> floor,
>
> ql.Date(), ql.Date(),
> dayCounter,
>
> isInArrears,
> exCouponDate)
>
>
>
> cmsPricer = ql.LinearTsrPricer(volHandle, meanReversion, yts)
>
>
>
> # cmsPricer = ql.NumericHaganPricer(volHandle,
> ql.GFunctionFactory.ParallelShifts , meanReversion)#, Rate
> lowerLimit=0.0, Rate upperLimit=1.0, Real precision=1.0e-6, Real
> hardUpperLimit=QL_MAX_REAL)
>
>
>
> integrationPoints = 16
>
>
>
> pricer = ql.LognormalCmsSpreadPricer(cmsPricer, correlation, yts,
>
> integrationPoints)
>
>
>
> instrument.setPricer(pricer)
>
>
>
> Regards,
>
> Aditya Gupta.
>
>
>
> ________________________________
> Complus Asset Management Limited ("CAML") is licensed by the Securities and Futures Commission of Hong Kong (“SFC”) for the Regulated Activities of Advising on Securities and Asset Management with CE number AWX256. CAML is subject to certain SFC codes and regulations, details of which can be found on the SFC's website at http://www.sfc.hk. CAML is a company incorporated in Hong Kong with its registered office and principal place of business as indicated above.
>
> Unless specifically indicated, this message should not be construed as an offer to sell or solicitation to purchase any securities, financial products or services or the giving of investment advice within or outside Hong Kong. This message is intended solely for the person(s) to whom it is addressed. If you receive this message in error, please immediately delete it and all copies of it from your computer network or hard copies, and notify the sender.
> _______________________________________________
> QuantLib-users mailing list
> Qua...@li...<mailto:Qua...@li...>
> https://lists.sourceforge.net/lists/listinfo/quantlib-users
|