Curva SOFR¶
Se 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 aux
Data¶
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: int
initial_notional: float
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 BULLET
En 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 | nominal | amortizacion | interes | amort_es_flujo | flujo | moneda | 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 | nominal | amortizacion | interes | amort_es_flujo | flujo | moneda | 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 qcfinancial
que 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% |