2 votos

Reevaluación de swaps SOFR de Quantlib en 2 fechas diferentes

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?

1voto

dotnetcoder Puntos 1262

Me doy cuenta de que la pregunta es específicamente sobre Quantlib, pero quería destacar una respuesta utilizando Rateslib para Python, la respuesta es alrededor de 1,05 mm USD como usted predijo.

Configure su curva inicial (tenga en cuenta que he ignorado la mayoría de los intercambios para aproximarme al intercambio de prueba de 30 años).

from rateslib import Curve, IRS, dt, Solver
curve = Curve(
    nodes={
        dt(2022, 5, 9): 1.0,
        dt(2047, 5, 9): 1.0,
        dt(2052, 5, 9): 1.0,
    },
    id="sofr"
)
sofr_kws = dict(
    payment_lag=2,
    frequency="A",
    convention="act360",
    effective=dt(2022, 5, 11),
    calendar="nyc",
    curves="sofr",
)
instruments = [
    IRS(termination="25y", **sofr_kws),
    IRS(termination="30y", **sofr_kws),
]
solver = Solver(
    curves=[curve], 
    instruments=instruments,
    s=[2.71, 2.60],
    instrument_labels=["25Y", "30Y"],
    id="SOFR"
)

A continuación, creamos su tess IRS y comprobar su VAN.

>>> test_irs = IRS(termination="30Y", **sofr_kws, fixed_rate=2.60, notional=10e6)
>>> test_irs.npv(solver=solver)
<Dual: 0.000932, ('sofr0', 'sofr1', 'sofr2'), [  7464130.57816169  -4800825.36772211 -10833705.54868424]>

A continuación, construimos una segunda curva con nuevas fechas y tipos.

curve2 = Curve(
    nodes={
        dt(2023, 5, 9): 1.0,
        dt(2048, 5, 9): 1.0,
        dt(2053, 5, 9): 1.0,
    },
    id="sofr"
)
sofr_kws2 = dict(
    payment_lag=2,
    frequency="A",
    convention="act360",
    effective=dt(2023, 5, 11),
    calendar="nyc",
    curves="sofr",
)
instruments = [
    IRS(termination="25y", **sofr_kws2),
    IRS(termination="30y", **sofr_kws2),
]
solver2 = Solver(
    curves=[curve2], 
    instruments=instruments,
    s=[3.2, 3.12],
    instrument_labels=["25Y", "30Y"],
    id="SOFR"
)

Tenemos que añadir las fijaciones para el IRS de prueba, ya que tiene una liquidación de pago por venir. He echado un vistazo a las fijaciones históricas y la media durante el año pasado ha sido de alrededor del 3%.

test_irs = IRS(termination="30Y", **sofr_kws, fixed_rate=2.60, notional=10e6, leg2_fixings=3.0)
test_irs.npv(solver=solver2)
<Dual: 1,049,569.356238, ('sofr0', 'sofr1', 'sofr2'), [ 7596484.75971786 -6798185.22482419 -8777872.59764332]>

Finanhelp.com

FinanHelp es una comunidad para personas con conocimientos de economía y finanzas, o quiere aprender. Puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X