Curva SOFR
Abrir en la demoSe construye la curva cupón cero asociada a los swaps de SOFR vs tasa fija:
Se utiliza el procedimiento clásico que consiste en:
- Resolver el sistema de ecuaciones que iguala el valor presente de las patas fijas (en
start_date) con el valor del nocional. - Se considera como flujo el nocional al vencimiento.
- Es importante notar que para que estas ecuaciones sean válidas se debe suponer que el settlement lag es siempre igual a 0.
import qcfinancial as qcf
import pandas as pd
import aux_functions as auxData
La data se obtiene del asiguiente archivo Excel. En él, además de las tasas de los swaps, se ha registrado las características principales de estos contratos.
data = pd.read_excel("./input/20240621_sofr_data.xlsx")data.style.format({'rate':'{:.4%}'})| ticket | start_date | tenor | stub_period | pay_freq | settlement_lag | bus_adj_rule | yf | wf | rate | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | USOSFR1Z BGN Curncy | 2024-06-25 00:00:00 | 7D | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.3342% |
| 1 | USOSFR2Z BGN Curncy | 2024-06-25 00:00:00 | 14D | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.3375% |
| 2 | USOSFR3Z BGN Curncy | 2024-06-25 00:00:00 | 21D | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.3415% |
| 3 | USOSFRA BGN Curncy | 2024-06-25 00:00:00 | 1M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.3442% |
| 4 | USOSFRB BGN Curncy | 2024-06-25 00:00:00 | 2M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.3454% |
| 5 | USOSFRC BGN Curncy | 2024-06-25 00:00:00 | 3M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.3426% |
| 6 | USOSFRD BGN Curncy | 2024-06-25 00:00:00 | 4M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.3171% |
| 7 | USOSFRE BGN Curncy | 2024-06-25 00:00:00 | 5M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.2955% |
| 8 | USOSFRF BGN Curncy | 2024-06-25 00:00:00 | 6M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.2714% |
| 9 | USOSFRG BGN Curncy | 2024-06-25 00:00:00 | 7M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.2366% |
| 10 | USOSFRH BGN Curncy | 2024-06-25 00:00:00 | 8M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.1986% |
| 11 | USOSFRI BGN Curncy | 2024-06-25 00:00:00 | 9M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.1667% |
| 12 | USOSFRK BGN Curncy | 2024-06-25 00:00:00 | 11M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.0841% |
| 13 | USOSFR1 BGN Curncy | 2024-06-25 00:00:00 | 12M | SHORT_FRONT | 2Y | 2 | MOD_FOLLOW | Act360 | Lin | 5.0477% |
| 14 | USOSFR1F BGN Curncy | 2024-06-25 00:00:00 | 18M | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 4.7545% |
| 15 | USOSFR2 BGN Curncy | 2024-06-25 00:00:00 | 2Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 4.5629% |
| 16 | USOSFR3 BGN Curncy | 2024-06-25 00:00:00 | 3Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 4.2799% |
| 17 | USOSFR4 BGN Curncy | 2024-06-25 00:00:00 | 4Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 4.1110% |
| 18 | USOSFR5 BGN Curncy | 2024-06-25 00:00:00 | 5Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 4.0095% |
| 19 | USOSFR6 BGN Curncy | 2024-06-25 00:00:00 | 6Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.9498% |
| 20 | USOSFR7 BGN Curncy | 2024-06-25 00:00:00 | 7Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.9114% |
| 21 | USOSFR8 BGN Curncy | 2024-06-25 00:00:00 | 8Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.8869% |
| 22 | USOSFR9 BGN Curncy | 2024-06-25 00:00:00 | 9Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.8721% |
| 23 | USOSFR10 BGN Curncy | 2024-06-25 00:00:00 | 10Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.8639% |
| 24 | USOSFR12 BGN Curncy | 2024-06-25 00:00:00 | 12Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.8604% |
| 25 | USOSFR15 BGN Curncy | 2024-06-25 00:00:00 | 15Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.8615% |
| 26 | USOSFR20 BGN Curncy | 2024-06-25 00:00:00 | 20Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.8278% |
| 27 | USOSFR25 BGN Curncy | 2024-06-25 00:00:00 | 25Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.7356% |
| 28 | USOSFR30 BGN Curncy | 2024-06-25 00:00:00 | 30Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.6388% |
| 29 | USOSFR40 BGN Curncy | 2024-06-25 00:00:00 | 40Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.4272% |
| 30 | USOSFR50 BGN Curncy | 2024-06-25 00:00:00 | 50Y | SHORT_FRONT | 1Y | 2 | MOD_FOLLOW | Act360 | Lin | 3.2120% |
Input
Se definen los inputs que son comunes a todas las operaciones. Notar que, contrariamente a lo que indican los datos, se establece que el settlement lag sea igual a 0. Esto para poder aplicar la condición que iguala el valor prersente de la pata fija en start_date al nocional.
# Debe coincidir con la fecha de los datos
trade_date = qcf.QCDate(21, 6, 2024)# Convención de las tasas de las patas fijas
yf = qcf.QCAct360()
wf = qcf.QCLinearWf()# Los parámetros se organizan en un dict.
common_params = {
"rec_pay": qcf.RecPay.RECEIVE,
"start_date": qcf.QCDate(25, 6, 2024),
"bus_adj_rule": qcf.BusyAdjRules.MODFOLLOW,
"settlement_stub_period": qcf.StubPeriod.SHORTFRONT,
"settlement_calendar": qcf.BusinessCalendar(trade_date, 50),
"settlement_lag": 0, # Se impone = 0
"initial_notional": 1_000_000,
"amort_is_cashflow": True,
"notional_currency": qcf.QCUSD(),
"is_bond": False,
"sett_lag_behaviour": qcf.SettLagBehaviour.DONT_MOVE,
}La siguiente celda es para facilitar la escritura del código que viene ya que nos recuerda cuáles son los argumentos de la función que construye patas fijas.
for p in qcf.LegFactory.build_bullet_fixed_rate_leg.__doc__.split(','):
print(p)build_bullet_fixed_rate_leg(rec_pay: qcfinancial.RecPay
start_date: qcfinancial.QCDate
end_date: qcfinancial.QCDate
bus_adj_rule: qcfinancial.BusyAdjRules
settlement_periodicity: qcfinancial.Tenor
settlement_stub_period: qcfinancial.StubPeriod
settlement_calendar: qcfinancial.BusinessCalendar
settlement_lag: typing.SupportsInt
initial_notional: typing.SupportsFloat
amort_is_cashflow: bool
interest_rate: qcfinancial.QCInterestRate
notional_currency: qcfinancial.QCCurrency
is_bond: bool
sett_lag_behaviour: qcfinancial.SettLagBehaviour = <SettLagBehaviour.DONT_MOVE: 1>) -> qcfinancial.Leg
Builds a Leg containing only cashflows of type FixedRateCashflow. Amortization is BULLETEn el siguiente loop, se construyen todas las patas fijas.
# Aquí se almacenarán los resultados
fixed_rate_legs = []
# Se recorre el DataFrame con la data
for t in data.itertuples():
# Madurez del contrato
tenor = qcf.Tenor(t.tenor)
# Se calcula el número de meses de la madurez
months = tenor.get_months() + 12 * tenor.get_years()
# Se calcula la fecha final del swap sin aplicar todavía ajustes de calendario
if (days:=tenor.get_days()) > 0:
end_date = common_params["start_date"].add_days(days)
else:
end_date = common_params["start_date"].add_months(months)
# Se define un dict con los parámetros propios de cada contrato
other_params = {
"end_date": end_date,
"settlement_periodicity": qcf.Tenor(t.pay_freq),
"interest_rate": qcf.QCInterestRate(t.rate, yf, wf),
}
# Se construye y almacena la pata fija correspondiente
fixed_rate_legs.append(
qcf.LegFactory.build_bullet_fixed_rate_leg(
**(common_params | other_params),
)
)Se muestra la estructura de un par de patas fijas.
aux.leg_as_dataframe(fixed_rate_legs[0]).style.format(aux.format_dict)| fecha_inicial | fecha_final | fecha_pago | nocional | amortizacion | interes | amort_es_flujo | flujo | moneda_nocional | valor_tasa | tipo_tasa | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2024-06-25 | 2024-07-02 | 2024-07-02 | 1,000,000.00 | 1,000,000.00 | 1,037.21 | True | 1,001,037.21 | USD | 5.3342% | LinAct360 |
aux.leg_as_dataframe(fixed_rate_legs[14]).style.format(aux.format_dict)| fecha_inicial | fecha_final | fecha_pago | nocional | amortizacion | interes | amort_es_flujo | flujo | moneda_nocional | valor_tasa | tipo_tasa | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2024-06-25 | 2024-12-25 | 2024-12-25 | 1,000,000.00 | 0.00 | 24,168.71 | True | 24,168.71 | USD | 4.7545% | LinAct360 |
| 1 | 2024-12-25 | 2025-12-25 | 2025-12-25 | 1,000,000.00 | 1,000,000.00 | 48,205.35 | True | 1,048,205.35 | USD | 4.7545% | LinAct360 |
Curva Inicial
La curva cero cupón se construye usando bootstrapping y el algoritmo de Newton-Raphson. En el siguiente loop se construye la curva inicial. Newton-Raphson comenzará sus iteraciones desde cada punto de esta curva.
# Se define los vectores de plazos y tasas
plazos = qcf.long_vec()
tasas = qcf.double_vec()
# Para rellenarlos se utiliza la información contenida
# en las patas fijas.
for leg in fixed_rate_legs:
# Número de cupones de la pata
num_cup = leg.size()
# Último cashflow de la pata
cashflow = leg.get_cashflow_at(num_cup - 1)
# Se calcula el número de días desde start_date
# hasta la última settlement_date
plazo = common_params["start_date"].day_diff(cashflow.get_settlement_date())
plazos.append(plazo)
# Se obtiene el valor de la tasa fija
tasa = cashflow.get_rate().get_value()
tasas.append(tasa)
# Con la información anterior, se termina de construir la curva
curva = qcf.QCCurve(plazos, tasas)
interpolator = qcf.QCLinearInterpolator(curva)
initial_zcc = qcf.ZeroCouponCurve(
interpolator,
rate:=(qcf.QCInterestRate(
0.0,
qcf.QCAct365(),
qcf.QCContinousWf()
))
)Bootstrapping
Se procede ahora a aplicar el bootstrapping. Se comienza dando de alta el objeto PresentValue de qcfinancialque permite valorizar todo tipo de patas.
pv = qcf.PresentValue()El siguiente loop ejecuta el bootstrapping.
# Se resuelve la ecuación:
# VP(pata_fija(i), z1,...,z(i),...,zN) - nocional = 0, para todo i
for i, leg in enumerate(fixed_rate_legs):
# Se define la función objetivo
def obj(zcc):
# VP - nocional
return pv.pv(common_params["start_date"], leg, zcc) - common_params["initial_notional"]
# Aquí comienza la resolución
error = 1_000
epsilon = .00001
x = initial_zcc.get_rate_at(i) # Valor inicial para Newton-Raphson
new_zcc = initial_zcc # En new_zcc se almacena el resultado
# Se aplica Newton-Raphson
while error > epsilon:
x = x - obj(new_zcc) / pv.get_derivatives()[i] # La derivada del VP se calcula al momento de valorizar
tasas[i] = x
# Se reconstruye la curva con el valor de la iteración
curva = qcf.QCCurve(plazos, tasas)
interpolator = qcf.QCLinearInterpolator(curva)
new_zcc = qcf.ZeroCouponCurve(
interpolator,
rate,
)
# Se calcula el nuevo error
error = abs(obj(new_zcc))Una vez ejecutado el bootstrapping, verificamos que, para cada pata, se cumple la condición deseada.
check = []
for i, leg in enumerate(fixed_rate_legs):
check.append({
"leg_number": i,
"present_value": pv.pv(common_params['start_date'], leg, new_zcc),
})
df_check = pd.DataFrame(check)
df_check.style.format({"present_value": "{:,.4f}"})| leg_number | present_value | |
|---|---|---|
| 0 | 0 | 1,000,000.0000 |
| 1 | 1 | 1,000,000.0000 |
| 2 | 2 | 1,000,000.0000 |
| 3 | 3 | 1,000,000.0000 |
| 4 | 4 | 1,000,000.0000 |
| 5 | 5 | 1,000,000.0000 |
| 6 | 6 | 1,000,000.0000 |
| 7 | 7 | 1,000,000.0000 |
| 8 | 8 | 1,000,000.0000 |
| 9 | 9 | 1,000,000.0000 |
| 10 | 10 | 1,000,000.0000 |
| 11 | 11 | 1,000,000.0000 |
| 12 | 12 | 1,000,000.0000 |
| 13 | 13 | 1,000,000.0000 |
| 14 | 14 | 1,000,000.0000 |
| 15 | 15 | 1,000,000.0000 |
| 16 | 16 | 1,000,000.0000 |
| 17 | 17 | 1,000,000.0000 |
| 18 | 18 | 1,000,000.0000 |
| 19 | 19 | 1,000,000.0000 |
| 20 | 20 | 1,000,000.0000 |
| 21 | 21 | 1,000,000.0000 |
| 22 | 22 | 1,000,000.0000 |
| 23 | 23 | 1,000,000.0000 |
| 24 | 24 | 1,000,000.0000 |
| 25 | 25 | 1,000,000.0000 |
| 26 | 26 | 1,000,000.0000 |
| 27 | 27 | 1,000,000.0000 |
| 28 | 28 | 1,000,000.0000 |
| 29 | 29 | 1,000,000.0000 |
| 30 | 30 | 1,000,000.0000 |
Finalmente, se despliega los valores de la curva obtenida.
df_curva = pd.concat([pd.DataFrame(plazos), pd.DataFrame(tasas)], axis=1)
df_curva.columns = ['plazo', 'tasa']
df_curva.style.format({'tasa':'{:.4%}'})| plazo | tasa | |
|---|---|---|
| 0 | 7 | 5.4055% |
| 1 | 14 | 5.4060% |
| 2 | 21 | 5.4073% |
| 3 | 30 | 5.4064% |
| 4 | 62 | 5.3948% |
| 5 | 92 | 5.3802% |
| 6 | 122 | 5.3430% |
| 7 | 153 | 5.3095% |
| 8 | 183 | 5.2743% |
| 9 | 216 | 5.2276% |
| 10 | 245 | 5.1797% |
| 11 | 273 | 5.1384% |
| 12 | 335 | 5.0365% |
| 13 | 365 | 4.9912% |
| 14 | 548 | 4.7223% |
| 15 | 730 | 4.5116% |
| 16 | 1095 | 4.2290% |
| 17 | 1462 | 4.0595% |
| 18 | 1826 | 3.9577% |
| 19 | 2191 | 3.8983% |
| 20 | 2556 | 3.8604% |
| 21 | 2922 | 3.8367% |
| 22 | 3289 | 3.8231% |
| 23 | 3653 | 3.8163% |
| 24 | 4383 | 3.8166% |
| 25 | 5480 | 3.8225% |
| 26 | 7307 | 3.7822% |
| 27 | 9131 | 3.6529% |
| 28 | 10957 | 3.5115% |
| 29 | 14610 | 3.1854% |
| 30 | 18262 | 2.8431% |