|
From: Aditya G. <adi...@co...> - 2022-08-15 03:40:28
|
Hi Peter, Regarding Bloomberg's model, it seems that it isn't very clear in their documentation what engine is being used, but the Sabr cube is used for the volatilities. I will try to contact support and ask them for more details. In any case, it seems that with a constant or non constant volatility, the pricing is not accurate. What could be the error in the code? If Bloomberg has a different model to the Quantlib one, the Quantlib model should still be able to price within some reasonable error percentage. Since this is not the case, where in the code could the mistake be? I am happy to send any updated code or fill in the missing lines. Thank you for your time. Regards, Aditya Gupta. On Aug 12, 2022 8:22 PM, Peter Caspers <pca...@gm...> wrote: You don't often get email from pca...@gm.... Learn why this is important<https://aka.ms/LearnAboutSenderIdentification> CAUTION: EXTERNAL Sender Hi Aditya 2 the error shouldn't show up unless you pass a volatilityType to the pricer. Might have to do with how SWIG calls the constructor, not sure at the moment. 3 what I meant was: the cms price depends on the volatilities outside the quoted part ATM-200 / ATM+200 as well, so maybe BBG uses different extrapolated vols than you do. And as I said, the model itself (and the mean reversion value) have a big impact, so you might first want check what BBG is doing Thanks Peter On Fri, 12 Aug 2022 at 10:43, Aditya Gupta <adi...@co...<mailto:adi...@co...>> wrote: 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...<mailto:pca...@gm...>> Sent: Friday, August 12, 2022 4:20 PM To: Aditya Gupta <adi...@co...<mailto:adi...@co...>> Cc: qua...@li...<mailto: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 |