Estoy intentando fijar el precio de swaps SOFR en dos fechas diferentes (los mismos swaps, sólo que con curvas y fechas diferentes)
Estos son mis parámetros iniciales:
curve_date =ql.Date (9,5,2022)
ql.Settings.instance().evaluationDate = curve_date
sofr = ql.Sofr() #overnightIndex
swaps_calendar = ql.UnitedStates(ql.UnitedStates.FederalReserve)#calendar
day_count = ql.Actual360() #day count convention
settlement_days = 2 #t+2 settlement convention for SOFR swaps
esta es la curva SOFR a 9 de mayo de 2022:
índice
ticker
n
tenor
cita
0
USOSFR1Z CBBT Curncy
1
1
0.79
1
USOSFR2Z CBBT Curncy
2
1
0.81
2
USOSFR3Z CBBT Curncy
3
1
0.79
3
USOSFRA CBBT Curncy
1
2
0.8
4
USOSFRB CBBT Curncy
2
2
1.01
5
USOSFRC CBBT Curncy
3
2
1.19
6
USOSFRD CBBT Curncy
4
2
1.34
7
USOSFRE CBBT Curncy
5
2
1.47
8
USOSFRF CBBT Curncy
6
2
1.61
9
USOSFRG CBBT Curncy
7
2
1.71
10
USOSFRH CBBT Curncy
8
2
1.82
11
USOSFRI CBBT Curncy
9
2
1.93
12
USOSFRJ CBBT Curncy
10
2
2.01
13
USOSFRK CBBT Curncy
11
2
2.09
14
USOSFR1 CBBT Curncy
12
2
2.17
15
USOSFR1F CBBT Curncy
18
2
2.48
16
USOSFR2 CBBT Curncy
2
3
2.62
17
USOSFR3 CBBT Curncy
3
3
2.69
18
USOSFR4 CBBT Curncy
4
3
2.72
19
USOSFR5 CBBT Curncy
5
3
2.73
20
USOSFR7 CBBT Curncy
7
3
2.77
21
USOSFR8 CBBT Curncy
8
3
2.78
22
USOSFR9 CBBT Curncy
9
3
2.8
23
USOSFR10 CBBT Curncy
10
3
2.81
24
USOSFR12 CBBT Curncy
12
3
2.83
25
USOSFR15 CBBT Curncy
15
3
2.85
26
USOSFR20 CBBT Curncy
20
3
2.81
27
USOSFR25 CBBT Curncy
25
3
2.71
28
USOSFR30 CBBT Curncy
30
3
2.6
29
USOSFR40 CBBT Curncy
40
3
2.4
30
USOSFR50 CBBT Curncy
50
3
2.23
Estos datos se almacenan en un df llamado: swap_data
y lo utilizo para construir tuplas (tarifa, (tenor)) para el OISRateHelper
objetos
swaps= [(row.quote,(row.n, row.tenor)) for row in swap_data.itertuples(index=True, name='Pandas')]
def zero_curve(settlement_days,swaps,day_count):
ois_helpers = [ ql.OISRateHelper(settlement_days, #settlementDays
ql.Period(*tenor), #tenor -> note that `tenor` in the list comprehension are (n,units), so uses * to unpack when calling ql.Period(n, units)
ql.QuoteHandle(ql.SimpleQuote(rate/100)), #fixedRate
sofr) #overnightIndex
for rate, tenor in swaps]
#for now I have chosen to use a logCubicDiscount term structure to ensure continuity in the inspection
sofrCurve = ql.PiecewiseLogCubicDiscount(settlement_days, #referenceDate
swaps_calendar,#calendar
ois_helpers, #instruments
day_count, #dayCounter
)
sofrCurve.enableExtrapolation() #allows for extrapolation at the ends
return sofrCurve
con esta función construyo una curva cero, un objeto sofr vinculado a esa curva y un motor de precios swap
sofrCurve = zero_curve(settlement_days,swaps,day_count)
valuation_Curve = ql.YieldTermStructureHandle(sofrCurve)
sofrIndex = ql.Sofr(valuation_Curve)
swapEngine = ql.DiscountingSwapEngine(valuation_Curve)
Con esto creo intercambios OIS y les pongo precio usando esta curva para asegurarme de que está correctamente calibrada:
effective_date = swaps_calendar.advance(curve_date, settlement_days, ql.Days)
notional = 10_000_000
ois_swaps = []
for rate, tenor in swaps:
schedule = ql.MakeSchedule(effective_date,
swaps_calendar.advance(effective_date, ql.Period(*tenor)),
ql.Period('1Y'),
calendar = swaps_calendar)
fixedRate = rate/100
oisSwap = ql.MakeOIS(ql.Period(*tenor), sofrIndex, fixedRate, nominal=notional)
oisSwap.setPricingEngine(swapEngine)
ois_swaps.append(oisSwap)
el VAN de todos los swaps es cero por lo que parece. Fui un paso más allá para confirmar que estaba obteniendo el VP de las patas correctamente mediante la construcción de una función que produce una tabla con la información relevante de la pata
def leg_information(effective_date, day_count,ois_swap, leg_type, sofrCurve):
leg_df=pd.DataFrame(columns=['date','yearfrac','CF','discountFactor','PV','totalPV'])
cumSum_pv= 0
leg = ois_swap.leg(0) if leg_type == "fixed" else ois_swap.leg(1)
for index, cf in enumerate(leg):
yearfrac = day_count.yearFraction(effective_date,cf.date())
df = sofrCurve.discount(yearfrac)
pv = df * cf.amount()
cumSum_pv += pv
row={'date':datetime.datetime(cf.date().year(), cf.date().month(), cf.date().dayOfMonth()),'yearfrac':yearfrac, 'CF':cf.amount() ,'discountFactor':df,'PV':pv,'totalPV':cumSum_pv}
leg_df.loc[index]=row
return leg_df
Luego procedí a ver las patas fijas y flotantes para el intercambio de 30y:
fixed_leg = leg_information(effective_date, day_count,ois_swaps[-3], 'fixed', sofrCurve)
fixed_leg.tail()
fecha
yearfrac
CF
factorDescuento
FV
totalPV
2048-05-11
26.38
263343.89
0.5
132298.29
4821684
2049-05-11
27.39
264067.36
0.49
130173.38
4951857.39
2050-05-11
28.41
264067.36
0.48
127789.7
5079647.08
2051-05-11
29.42
264067.36
0.48
125514.12
5205161.2
2052-05-13
30.44
266237.78
0.47
124346.16
5329507.36
float_leg = leg_information(effective_date, day_count,ois_swaps[-3], 'Float', sofrCurve)
float_leg.tail()
fecha
yearfrac
CF
factorDescuento
FV
totalPV
2048-05-11
26.38
194630.64
0.5
97778.23
4976215.78
2049-05-11
27.39
191157.4
0.49
94232.04
5070447.82
2050-05-11
28.41
186532.08
0.48
90268.17
5160715.99
2051-05-11
29.42
181300.34
0.48
86174.05
5246890.04
2052-05-13
30.44
176892.09
0.47
82617.32
5329507.36
Además, el DV01 en el intercambio se alinea con lo que veo en bloomberg:
ois_swaps[-3].fixedLegBPS()
= $20462.68. Así que en este punto, me siento cómodo con lo que el objeto de intercambio, ya que parece coincidir con lo que veo en Bloomberg utilizando SWPM
Ahora, cuando cambio la fecha:
curve_date =ql.Date (9,5,2023)
ql.Settings.instance().evaluationDate = curve_date
effective_date = swaps_calendar.advance(curve_date, settlement_days, ql.Days)
y tira de la nueva curva:
índice
ticker
n
tenor
cita
0
USOSFR1Z CBBT Curncy
1
1
5.06
1
USOSFR2Z CBBT Curncy
2
1
5.06
2
USOSFR3Z CBBT Curncy
3
1
5.06
3
USOSFRA CBBT Curncy
1
2
5.07
4
USOSFRB CBBT Curncy
2
2
5.1
5
USOSFRC CBBT Curncy
3
2
5.11
6
USOSFRD CBBT Curncy
4
2
5.11
7
USOSFRE CBBT Curncy
5
2
5.09
8
USOSFRF CBBT Curncy
6
2
5.06
9
USOSFRG CBBT Curncy
7
2
5.03
10
USOSFRH CBBT Curncy
8
2
4.97
11
USOSFRI CBBT Curncy
9
2
4.92
12
USOSFRJ CBBT Curncy
10
2
4.87
13
USOSFRK CBBT Curncy
11
2
4.81
14
USOSFR1 CBBT Curncy
12
2
4.74
15
USOSFR1F CBBT Curncy
18
2
4.28
16
USOSFR2 CBBT Curncy
2
3
3.96
17
USOSFR3 CBBT Curncy
3
3
3.58
18
USOSFR4 CBBT Curncy
4
3
3.39
19
USOSFR5 CBBT Curncy
5
3
3.3
20
USOSFR7 CBBT Curncy
7
3
3.24
21
USOSFR8 CBBT Curncy
8
3
3.23
22
USOSFR9 CBBT Curncy
9
3
3.24
23
USOSFR10 CBBT Curncy
10
3
3.24
24
USOSFR12 CBBT Curncy
12
3
3.27
25
USOSFR15 CBBT Curncy
15
3
3.3
26
USOSFR20 CBBT Curncy
20
3
3.28
27
USOSFR25 CBBT Curncy
25
3
3.2
28
USOSFR30 CBBT Curncy
30
3
3.12
29
USOSFR40 CBBT Curncy
40
3
2.93
30
USOSFR50 CBBT Curncy
50
3
2.73
almacenar los datos anteriores en swap_data
y proceda de nuevo a recalibrar la curva cero:
swaps= [(row.quote,(row.n, row.tenor)) for row in swap_data.itertuples(index=True, name='Pandas')]
sofrCurve_2023 = zero_curve(settlement_days,swaps,day_count)
valuation_Curve2023 = ql.YieldTermStructureHandle(sofrCurve_2023)
sofrIndex2023 = ql.Sofr(valuation_Curve2023)
swapEngine2023 = ql.DiscountingSwapEngine(valuation_Curve2023)
ois_swaps[-3].setPricingEngine(swapEngine2023)
e intentar obtener el VAN del swap
ois_swaps[-3].NPV()
Se obtiene un valor de 60968,42 dólares.
Sé que el VAN después de cambiar la fecha hacia adelante es incorrecto. He hecho un cálculo sencillo: el tipo del swap a 30 años ha pasado de 2,60 a 3,12 (ya sé que es un swap a 29 años 1 año más tarde, pero a efectos ilustrativos las pérdidas y ganancias son más -20k* 52bps = -1.040.000 $).
y si intento ver el tramo flotante llamando a:
float_leg = leg_information(effective_date, day_count,ois_swaps[-3], 'Float', sofrCurve)
float_leg.tail()
Me sale lo siguiente:
RuntimeError: Missing SOFRON Actual/360 fixing for May 11th, 2022
Lo que me hace pensar que necesito volver a enlazar con el OvernightIndex para sofrIndex2023
en ese swap de 30y (es que no se como hacerlo, he mirado la documentación y no hay pistas de como hacerlo)
¿Qué estoy haciendo mal?