Guía/Valorización y Sensibilidad

Valorización y Sensibilidad

Abrir en la demo

Configuración

Para ejecutar todos los ejemplos se debe importar la librería. Se sugiere utilizar siempre el alias qcf.

In [1]:
pythoncopy
import qcfinancial as qcf
import pickle
In [2]:
pythoncopy
qcf.id()
Out[2]:
'version: 1.11.3, build: 2026-06-29 16:00'

Librerías adicionales.

In [3]:
pythoncopy
import aux_functions as aux # Aquí se guardó la función leg_as_dataframe del notebook 3
from math import exp, log
import pandas as pd
import numpy as np
In [4]:
pythoncopy
pd.options.display.max_columns=300

Para formateo de pandas.DataFrames.

In [5]:
pythoncopy
format_dict = {
    'nominal': '{:,.2f}',
    'amort': '{:,.2f}',
    'interes': '{:,.2f}',
    'flujo': '{:,.2f}',
    'nocional': '{:,.2f}',
    'amortizacion': '{:,.2f}',
    'icp_inicial': '{:,.2f}',
    'icp_final': '{:,.2f}',
    'uf_inicial': '{:,.2f}',
    'uf_final': '{:,.2f}',
    'plazo': '{:,.0f}',
    'tasa': '{:,.4%}',
    'valor_tasa': '{:,.4%}',
    'valor_tasa_equivalente': '{:,.4%}',
    'spread': '{:,.4%}',
    'gearing': '{:,.2f}',
    'amort_moneda_pago': '{:,.2f}',
    'interes_moneda_pago': '{:,.2f}',
    'valor_indice_inicial': '{:,.2f}',
    'valor_indice_final': '{:,.2f}',
    'valor_indice_fx': '{:,.2f}',
    'flujo_en_clp': '{:,.2f}',
}

Construcción de la Curva

La construcción de una curva se hace en varios pasos.

Vectores de Float e Int

Este es un vector de números enteros (grandes, de ahí la l (long))

In [6]:
pythoncopy
lvec = qcf.long_vec()

Agregar un elemento.

In [7]:
pythoncopy
lvec.append(1000)

Este es un vector de números float.

In [8]:
pythoncopy
vec = qcf.double_vec()

Agregar un elemento.

In [9]:
pythoncopy
vec.append(.025)

Obtener ese elemento.

In [10]:
pythoncopy
print(f"Plazo: {lvec[0]:,.0f}")
print(f"Tasa: {vec[0]:,.2%}")
Out[10]:
Plazo: 1,000
Tasa: 2.50%

Objeto Curva

Es simplemente un long_vec que representa las abscisas de la curva y un double_vec que representa las ordenadas. Ambos vectores deben tener el mismo largo.

In [11]:
pythoncopy
curva = qcf.QCCurve(lvec, vec)

Un elemento de una curva se representa como un par abscisa, ordenada.

In [12]:
pythoncopy
curva.get_values_at(0)
Out[12]:
(1000, 0.025)

Se obtiene el plazo en una posición de la curva.

In [13]:
pythoncopy
curva.get_values_at(0)[0]
Out[13]:
1000

Se obtiene la tasa en una posición de la curva.

In [14]:
pythoncopy
curva.get_values_at(0)[1]
Out[14]:
0.025

Se agrega un par (plazo, valor) a la curva.

In [15]:
pythoncopy
curva.set_pair(100, .026)

Se verifica. Notar que se debe usar el índice 0 ya que la curva se ordena automáticamente por plazos ascendentes.

In [16]:
pythoncopy
curva.get_values_at(0)[0]
Out[16]:
100
In [17]:
pythoncopy
curva.get_values_at(0)[1]
Out[17]:
0.026

Se agrega un par más.

In [18]:
pythoncopy
curva.set_pair(370, .03)

Se itera sobre la curva mostrando sus valores

In [19]:
pythoncopy
for i in range(curva.get_length()):
    pair = curva.get_values_at(i)
    print("Tenor: {0:,.0f} Valor: {1:.4%}".format(pair[0], pair[1]))
Out[19]:
Tenor: 100 Valor: 2.6000%
Tenor: 370 Valor: 3.0000%
Tenor: 1,000 Valor: 2.5000%

Interpolador

Se agrega un interpolador. En este caso, un interpolador lineal.

In [20]:
pythoncopy
lin = qcf.QCLinearInterpolator(curva)

Se puede ahora obtener una tasa a un plazo cualquiera.

In [21]:
pythoncopy
plazo = 120
print(f"Tasa a {plazo:,.0f} días es igual a {lin.interpolate_at(plazo):.4%}")
Out[21]:
Tasa a 120 días es igual a 2.6296%

Curva Cero Cupón

Para completar el proceso se define un objeto de tipo QCInterestRate. Con este objeto, que representa la convención de las tasas de interés asociadas a la curva, se termina de dar de alta una curva cero cupón.

In [22]:
pythoncopy
yf = qcf.QCAct365()
wf = qcf.QCContinousWf()
tasa = qcf.QCInterestRate(.0, yf, wf)
In [23]:
pythoncopy
zcc = qcf.ZeroCouponCurve(lin, tasa)

El interpolador permite obtener una tasa a cualquier plazo.

In [24]:
pythoncopy
plazo = 300
print(f"Tasa en {plazo:,.0f} es igual a {zcc.get_rate_at(plazo):.4%}")
Out[24]:
Tasa en 300 es igual a 2.8963%

Otros métodos:

Discount factor.

In [25]:
pythoncopy
print(f"Discount factor at {plazo}: {zcc.get_discount_factor_at(plazo):.6%}")
print(f"Check: {exp(-zcc.get_rate_at(plazo) * plazo / 365):.6%}")
Out[25]:
Discount factor at 300: 97.647593%
Check: 97.647593%

Tasa Forward

In [26]:
pythoncopy
d1 = 30
d2 = 90
print(f"Tasa forward entre los días {d1:,.0f} y {d2:,.0f}: {zcc.get_forward_rate(d1, d2):.4%}")
Out[26]:
Tasa forward entre los días 30 y 90: 2.6000%

Se verifica el cálculo.

In [27]:
pythoncopy
df1 = zcc.get_discount_factor_at(d1)
df2 = zcc.get_discount_factor_at(d2)
df12 = df1 / df2
print(f"Check: {log(df12) * 365 / (d2 - d1):.4%}")
Out[27]:
Check: 2.6000%

Valorizar

Para valorizar es necesario dar de alta un objeto de tipo PresentValue.

In [28]:
pythoncopy
pv = qcf.PresentValue()

Depósito a Plazo

Se utilizará como instrumento un depósito a plazo en CLP o USD. Este instrumento se modela como un SimpleCashflow. Este, a su vez se construye con un monto, una fecha y una moneda.

In [29]:
pythoncopy
fecha_vcto = qcf.QCDate(13, 1, 2025)
monto = 10_000_000.0
clp = qcf.QCCLP()

# Se construye el depósito
depo = qcf.SimpleCashflow(fecha_vcto, monto, clp)
In [30]:
pythoncopy
print(f"Monto del depósito: {depo.amount():,.0f}")
Out[30]:
Monto del depósito: 10,000,000

Se define una fecha de valorización y se calcula el valor presente del depo.

In [31]:
pythoncopy
fecha_hoy = qcf.QCDate(31, 1, 2024)
print(f"Valor presente depo: {pv.pv(fecha_hoy, depo, zcc):,.2f}")
Out[31]:
Valor presente depo: 9,721,044.77

Se verifica a mano el resultado.

In [32]:
pythoncopy
plazo = fecha_hoy.day_diff(fecha_vcto)
print("Plazo:", plazo)
Out[32]:
Plazo: 348
In [33]:
pythoncopy
tasa_int = zcc.get_rate_at(plazo)
print(f"Tasa: {tasa_int:,.4%}")
Out[33]:
Tasa: 2.9674%
In [34]:
pythoncopy
valor_presente = monto * exp(-tasa_int * plazo / 365)
print(f"Valor presente a mano: {valor_presente:,.2f}")
Out[34]:
Valor presente a mano: 9,721,044.77

Renta Fija de Chile

Se muestra el ejemplo de valorización de un bono bullet a tasa fija con las convenciones de la Bolsa de Comercio de Santiago. Para el ejemplo usamos las características del BTU0150326.

Se dan de alta los parámetros requeridos para instanciar un objeto de tipo FixedRateLeg.

In [35]:
pythoncopy
rp = qcf.RecPay.RECEIVE
fecha_inicio = qcf.QCDate(1, 3, 2015)
fecha_final = qcf.QCDate(1, 3, 2026)
bus_adj_rule = qcf.BusyAdjRules.NO
periodicidad = qcf.Tenor('6M')
periodo_irregular = qcf.StubPeriod.NO
calendario = qcf.BusinessCalendar(fecha_inicio, 20)
lag_pago = 0
nominal = 100.0
amort_es_flujo = True
valor_tasa_fija = .015
tasa_cupon = qcf.QCInterestRate(
    valor_tasa_fija, 
    qcf.QC30360(),
    qcf.QCLinearWf()
)
moneda = qcf.QCCLP()
es_bono = True

Se da de alta el objeto.

In [36]:
pythoncopy
pata_bono = qcf.LegFactory.build_bullet_fixed_rate_leg(
    rp,
    fecha_inicio,
    fecha_final,
    bus_adj_rule,
    periodicidad,
    periodo_irregular,
    calendario,
    lag_pago,
    nominal,
    amort_es_flujo,
    tasa_cupon,
    moneda,
    es_bono
)

Se da de alta el valor de la TERA y luego se construye un objeto de tipo ChileanFixedRateBond.

In [37]:
pythoncopy
tera = qcf.QCInterestRate(.015044, qcf.QCAct365(), qcf.QCCompoundWf())
bono_chileno = qcf.ChileanFixedRateBond(pata_bono, tera)

Se valoriza al 2021-09-28 a una TIR de mercado del 1.61%.

In [38]:
pythoncopy
fecha_valor = qcf.QCDate(28, 9, 2021)
tir = qcf.QCInterestRate(.0161, qcf.QCAct365(), qcf.QCCompoundWf())

valor_presente = bono_chileno.present_value(fecha_valor, tir)
precio = bono_chileno.precio(fecha_valor, tir)
valor_par = bono_chileno.valor_par(fecha_valor)

print(f'Valor presente: {valor_presente:,.8f}')
print(f'Precio: {precio:,.2%}')
print(f'Valor par: {valor_par:,.18f}')
Out[38]:
Valor presente: 99.67188455
Precio: 99.56%
Valor par: 100.110516628864033351

Con esto el valor a pagar es:

In [39]:
pythoncopy
valor_uf = 30_080.37
valor_pago = precio * valor_par * valor_uf
print(f'Valor a pagar: {valor_pago:,.0f}')
Out[39]:
Valor a pagar: 2,998,111

Con 4 decimales en el precio (4 decimales porcentuales, 6 decimales en el número):

In [40]:
pythoncopy
precio2 = bono_chileno.precio2(fecha_valor, tir, 6)
print(f'Precio a 4 decmales: {precio2:.4%}')
Out[40]:
Precio a 4 decmales: 99.5619%

La función precio2 entrega el mismo resultado que la función precio cuando se utiliza con 2 decimales porcentuales.

In [41]:
pythoncopy
precio22 = bono_chileno.precio2(fecha_valor, tir, 4)
print(f'Precio a 4 decmales: {precio22:.4%}')
Out[41]:
Precio a 4 decmales: 99.5600%

Se muestran las diferencias con la convención de precio usual en mercados desarrollados.

In [42]:
pythoncopy
bono = qcf.FixedRateBond(pata_bono)
print(f'Valor presente: {bono.present_value(fecha_valor, tir):,.8f}')
print(f'Precio: {bono.price(fecha_valor, tir):,.8f}')
Out[42]:
Valor presente: 99.67188455
Precio: 99.55938455

Curvas Reales

Construyamos dos curvas a partir de data real. Primero una curva en CLP.

In [43]:
pythoncopy
curva_clp = pd.read_excel("./input/curva_clp.xlsx")
curva_clp.style.format(format_dict)
Out[43]:
  plazo tasa
0 1 1.7500%
1 4 1.7501%
2 96 1.4867%
3 188 1.3049%
4 279 1.2870%
5 369 1.3002%
6 553 1.3035%
7 734 1.2951%
8 1,099 1.4440%
9 1,465 1.6736%
10 1,830 1.9860%
11 2,195 2.2766%
12 2,560 2.5812%
13 2,926 2.8216%
14 3,291 3.0373%
15 3,656 3.2154%
16 4,387 3.4525%
17 5,482 3.7636%
18 7,309 4.1641%

Se da de alta un vector con los plazos (variable de tipo long) y un vector con las tasas (variable de tipo double).

In [44]:
pythoncopy
lvec1 = qcf.long_vec()
vec1 = qcf.double_vec()
for t in curva_clp.itertuples():
    lvec1.append(int(t.plazo))
    vec1.append(t.tasa)

Luego, con una curva, un interpolador y un objeto QCInterestRate(que indica la convención de las tasas de la curva) se construye una curva cupón cero.

In [45]:
pythoncopy
curva1 = qcf.QCCurve(lvec1, vec1)
lin1 = qcf.QCLinearInterpolator(curva1)
zcc_clp = qcf.ZeroCouponCurve(lin1, tasa)

Luego, una curva en USD.

In [46]:
pythoncopy
curva_usd = pd.read_excel("./input/curva_usd.xlsx")
curva_usd.style.format(format_dict)
Out[46]:
  plazo tasa
0 3 1.5362%
1 4 1.1521%
2 7 1.5536%
3 14 1.5850%
4 31 1.6595%
5 60 1.7698%
6 91 1.8010%
7 123 1.7711%
8 152 1.7542%
9 182 1.7394%
10 213 1.7288%
11 244 1.7183%
12 276 1.7027%
13 305 1.6917%
14 335 1.6820%
15 367 1.6722%
16 549 1.6207%
17 731 1.5947%
18 1,096 1.5788%
19 1,461 1.5906%
20 1,827 1.6190%
21 2,558 1.7028%
22 3,653 1.8533%
23 4,385 1.9547%
24 5,479 2.0893%
25 7,305 2.2831%
26 9,132 2.4306%
27 10,958 2.5576%

Se encapasulará todo el procedimiento anterior en una función que dado un DataFrame construya un objeto ZeroCouponCurve.

In [47]:
pythoncopy
def zcc_from_df(df: pd.DataFrame, tasa: qcf.QCInterestRate) -> qcf.ZeroCouponCurve:
    lvec = qcf.long_vec()
    vec = qcf.double_vec()
    for t in df.itertuples():
        lvec.append(int(t.plazo))
        vec.append(t.tasa)
    curva = qcf.QCCurve(lvec, vec)
    lin = qcf.QCLinearInterpolator(curva)
    return qcf.ZeroCouponCurve(lin, tasa)
In [48]:
pythoncopy
zcc_usd = zcc_from_df(curva_usd, tasa)

Finalmente, una curva en CLF.

In [49]:
pythoncopy
curva_clf = pd.read_excel("./input/curva_clf.xlsx")
curva_clf.style.format(format_dict)
Out[49]:
  plazo tasa
0 1 -5.6780%
1 4 -5.6744%
2 35 -0.9340%
3 64 -2.1183%
4 96 -2.0079%
5 126 -2.0762%
6 155 -1.9197%
7 188 -1.9347%
8 218 -1.7626%
9 249 -1.7987%
10 279 -1.9335%
11 309 -1.8159%
12 341 -1.5940%
13 369 -1.5994%
14 400 -1.5068%
15 428 -1.6115%
16 461 -1.5923%
17 491 -1.5780%
18 522 -1.5186%
19 553 -1.5533%
20 582 -1.5649%
21 734 -1.6594%
22 1,099 -1.4881%
23 1,465 -1.2740%
24 1,830 -1.0201%
25 2,195 -0.8009%
26 2,560 -0.5868%
27 2,926 -0.4145%
28 3,291 -0.3047%
29 3,656 -0.2242%
30 4,387 -0.1871%
31 5,482 -0.1056%
32 7,309 -0.0639%
In [50]:
pythoncopy
zcc_clf = zcc_from_df(curva_clf, tasa)
In [51]:
pythoncopy
curva_sofr = pd.read_excel("./input/sofr.xlsx")
curva_sofr.style.format(format_dict)
Out[51]:
  curva fecha plazo tasa
0 SOFR 2024-06-11 00:00:00 1 5.3935%
1 SOFR 2024-06-11 00:00:00 2 5.3930%
2 SOFR 2024-06-11 00:00:00 9 5.3911%
3 SOFR 2024-06-11 00:00:00 16 5.3909%
4 SOFR 2024-06-11 00:00:00 32 5.3936%
5 SOFR 2024-06-11 00:00:00 63 5.3894%
6 SOFR 2024-06-11 00:00:00 94 5.3843%
7 SOFR 2024-06-11 00:00:00 124 5.3598%
8 SOFR 2024-06-11 00:00:00 155 5.3375%
9 SOFR 2024-06-11 00:00:00 185 5.3127%
10 SOFR 2024-06-11 00:00:00 216 5.2777%
11 SOFR 2024-06-11 00:00:00 247 5.2404%
12 SOFR 2024-06-11 00:00:00 275 5.2070%
13 SOFR 2024-06-11 00:00:00 367 5.0882%
14 SOFR 2024-06-11 00:00:00 550 4.8542%
15 SOFR 2024-06-11 00:00:00 732 4.6727%
16 SOFR 2024-06-11 00:00:00 1,097 4.4059%
17 SOFR 2024-06-11 00:00:00 1,463 4.2398%
18 SOFR 2024-06-11 00:00:00 1,828 4.1384%
19 SOFR 2024-06-11 00:00:00 2,193 4.0790%
20 SOFR 2024-06-11 00:00:00 2,558 4.0405%
21 SOFR 2024-06-11 00:00:00 2,924 4.0173%
22 SOFR 2024-06-11 00:00:00 3,289 4.0022%
23 SOFR 2024-06-11 00:00:00 3,654 3.9939%
24 SOFR 2024-06-11 00:00:00 4,385 3.9904%
25 SOFR 2024-06-11 00:00:00 5,480 3.9912%
26 SOFR 2024-06-11 00:00:00 7,307 3.9428%
27 SOFR 2024-06-11 00:00:00 9,133 3.8068%
28 SOFR 2024-06-11 00:00:00 10,959 3.6594%
In [52]:
pythoncopy
zcc_sofr = zcc_from_df(curva_sofr, tasa)
zcc_sofr.get_rate_at(10_959)
Out[52]:
0.0365940888755
In [53]:
pythoncopy
curva_sol = {
  "curve_code": "CSOFR",
  "type_of_rate": "CON_ACT/365",
  "process_date": "2024-06-11",
  "jacobians": {},
  "values": [
    {
      "tenor": "2D",
      "maturity": 2,
      "rate": 0.05393091948998677
    },
    {
      "tenor": "7D",
      "maturity": 9,
      "rate": 0.05391149528761137
    },
    {
      "tenor": "14D",
      "maturity": 16,
      "rate": 0.053909500139491626
    },
    {
      "tenor": "1M",
      "maturity": 34,
      "rate": 0.05392893501738643
    },
    {
      "tenor": "2M",
      "maturity": 63,
      "rate": 0.053894362943625894
    },
    {
      "tenor": "3M",
      "maturity": 94,
      "rate": 0.053843482977097415
    },
    {
      "tenor": "4M",
      "maturity": 125,
      "rate": 0.05359443451658483
    },
    {
      "tenor": "5M",
      "maturity": 155,
      "rate": 0.05337515689463274
    },
    {
      "tenor": "6M",
      "maturity": 185,
      "rate": 0.05312657866085667
    },
    {
      "tenor": "7M",
      "maturity": 216,
      "rate": 0.052777204713019304
    },
    {
      "tenor": "8M",
      "maturity": 247,
      "rate": 0.05240381230639302
    },
    {
      "tenor": "9M",
      "maturity": 275,
      "rate": 0.0520700981638382
    },
    {
      "tenor": "1Y",
      "maturity": 367,
      "rate": 0.05088167612943658
    },
    {
      "tenor": "18M",
      "maturity": 552,
      "rate": 0.04852936930616741
    },
    {
      "tenor": "2Y",
      "maturity": 734,
      "rate": 0.04659950874645961
    },
    {
      "tenor": "3Y",
      "maturity": 1098,
      "rate": 0.04394041235592892
    },
    {
      "tenor": "4Y",
      "maturity": 1463,
      "rate": 0.04231284987923743
    },
    {
      "tenor": "5Y",
      "maturity": 1828,
      "rate": 0.041317554448529636
    },
    {
      "tenor": "6Y",
      "maturity": 2193,
      "rate": 0.04073549138440949
    },
    {
      "tenor": "7Y",
      "maturity": 2558,
      "rate": 0.04035917885531376
    },
    {
      "tenor": "8Y",
      "maturity": 2925,
      "rate": 0.040119782655144265
    },
    {
      "tenor": "9Y",
      "maturity": 3289,
      "rate": 0.039975186953621844
    },
    {
      "tenor": "10Y",
      "maturity": 3654,
      "rate": 0.03989648083295584
    },
    {
      "tenor": "12Y",
      "maturity": 4385,
      "rate": 0.03986864130635106
    },
    {
      "tenor": "15Y",
      "maturity": 5480,
      "rate": 0.039884006370912016
    },
    {
      "tenor": "20Y",
      "maturity": 7307,
      "rate": 0.03940855790321542
    },
    {
      "tenor": "25Y",
      "maturity": 9134,
      "rate": 0.03804951829543225
    },
    {
      "tenor": "30Y",
      "maturity": 10961,
      "rate": 0.036573592658114495
    },
    {
      "tenor": "40Y",
      "maturity": 14612,
      "rate": 0.03329402953514909
    },
    {
      "tenor": "50Y",
      "maturity": 18264,
      "rate": 0.029736924965687982
    }
  ]
}
In [54]:
pythoncopy
df_curva_sol = pd.DataFrame(curva_sol["values"])
In [55]:
pythoncopy
df_curva_sol.columns = ['tenor', 'plazo', 'tasa']
zcc_sol = zcc_from_df(df_curva_sol, tasa)

Curvas para Sensibilidad

Para calcular sensibilidad a la curva cero cupón, se define qué vértice de la curva se quiere desplazar y el monto en puntos básicos del desplazamiento.

In [56]:
pythoncopy
vertice = 15
bp = 1

Se construyen las curvas con ese vértice 1 punto básico más arriba y 1 punto básico más abajo. Para esto se define una función auxiliar.

In [57]:
pythoncopy
def curvas_sens(
    df: pd.DataFrame, 
    tasa: qcf.QCInterestRate, 
    vertice: int, 
    bp: float
) -> tuple[qcf.ZeroCouponCurve, qcf.ZeroCouponCurve]:
    bp /= 10_000
    lvec = qcf.long_vec()
    vec_sens_up = qcf.double_vec()
    vec_sens_down = qcf.double_vec()
    for t in df.itertuples():
        lvec.append(int(t.plazo))
        if t.Index == vertice:
            vec_sens_up.append(t.tasa + bp)
            vec_sens_down.append(t.tasa - bp)
        else:
            vec_sens_up.append(t.tasa)
            vec_sens_down.append(t.tasa)

    zcc_sens_up = qcf.QCCurve(lvec, vec_sens_up)
    lin_sens_up = qcf.QCLinearInterpolator(zcc_sens_up)
    zz_sens_up = qcf.ZeroCouponCurve(lin_sens_up, tasa)

    zcc_sens_down = qcf.QCCurve(lvec, vec_sens_down)
    lin_sens_down = qcf.QCLinearInterpolator(zcc_sens_down)
    zz_sens_down = qcf.ZeroCouponCurve(lin_sens_down, tasa)
    
    return zz_sens_up, zz_sens_down
In [58]:
pythoncopy
zcc_clp_up, zcc_clp_down = curvas_sens(curva_clp, tasa, vertice, bp)
zcc_usd_up, zcc_usd_down = curvas_sens(curva_usd, tasa, vertice, bp)
zcc_clf_up, zcc_clf_down = curvas_sens(curva_clf, tasa, vertice, bp)

FixedRateCashflow Leg

Se da de alta una pata fija:

In [59]:
pythoncopy
rp = qcf.RecPay.RECEIVE
fecha_inicio = qcf.QCDate(13, 6, 2024)
fecha_final = qcf.QCDate(13, 6, 2026)
bus_adj_rule = qcf.BusyAdjRules.MODFOLLOW
periodicidad = qcf.Tenor('12M')
periodo_irregular = qcf.StubPeriod.SHORTFRONT
calendario = qcf.BusinessCalendar(fecha_inicio, 20)
lag_pago = 0
nominal = 1_000_000.0
amort_es_flujo = True
valor_tasa_fija = 0.048864
valor_tasa_fija = 0.047134
tasa_cupon = qcf.QCInterestRate(
    valor_tasa_fija, 
    qcf.QCAct360(), 
    qcf.QCLinearWf()
)
moneda = qcf.QCUSD()
es_bono = False

fixed_rate_leg = qcf.LegFactory.build_bullet_fixed_rate_leg(
    rp,
    fecha_inicio,
    fecha_final,
    bus_adj_rule,
    periodicidad,
    periodo_irregular,
    calendario,
    lag_pago,
    nominal,
    amort_es_flujo,
    tasa_cupon,
    moneda,
    es_bono
)
In [60]:
pythoncopy
aux.leg_as_dataframe(fixed_rate_leg).style.format(format_dict)
Out[60]:
  fecha_inicial fecha_final fecha_pago nocional amortizacion interes amort_es_flujo flujo moneda_nocional valor_tasa tipo_tasa
0 2024-06-13 2025-06-13 2025-06-13 1,000,000.00 0.00 47,788.64 True 47,788.64 USD 4.7134% LinAct360
1 2025-06-13 2026-06-15 2026-06-15 1,000,000.00 1,000,000.00 48,050.49 True 1,048,050.49 USD 4.7134% LinAct360

Se calcula ahora el valor presente:

In [61]:
pythoncopy
vp_fija = pv.pv(fecha_val:=qcf.QCDate(11, 6, 2024), fixed_rate_leg, zcc_sofr)
dias = fecha_val.day_diff(qcf.QCDate(13, 6, 2024))
print(f"Días: {dias}")
print(f"Valor presente de la pata fija es: {vp_fija / zcc_sofr.get_discount_factor_at(dias) :,.0f}")
Out[61]:
Días: 2
Valor presente de la pata fija es: 999,784

Al calcular el valor presente, también se calculan las derivadas del valor presente respecto a cada uno de los vértices de la curva.

In [62]:
pythoncopy
der = pv.get_derivatives()

Con esas derivadas, se puede calcular la sensibilidad a la curva cupón cero a un movimiento de 1 punto básico.

In [63]:
pythoncopy
i = 0
total = 0
for d in der:
    total += d * bp / 10_000
    print(f"Sensibilidad en {i}: {d * bp / 10_000:0,.0f}")
    i += 1
print(f"Sensibilidad total: {total:,.0f}")
Out[63]:
Sensibilidad en 0: 0
Sensibilidad en 1: 0
Sensibilidad en 2: 0
Sensibilidad en 3: 0
Sensibilidad en 4: 0
Sensibilidad en 5: 0
Sensibilidad en 6: 0
Sensibilidad en 7: 0
Sensibilidad en 8: 0
Sensibilidad en 9: 0
Sensibilidad en 10: 0
Sensibilidad en 11: 0
Sensibilidad en 12: 0
Sensibilidad en 13: -5
Sensibilidad en 14: 0
Sensibilidad en 15: -191
Sensibilidad en 16: -1
Sensibilidad en 17: 0
Sensibilidad en 18: 0
Sensibilidad en 19: 0
Sensibilidad en 20: 0
Sensibilidad en 21: 0
Sensibilidad en 22: 0
Sensibilidad en 23: 0
Sensibilidad en 24: 0
Sensibilidad en 25: 0
Sensibilidad en 26: 0
Sensibilidad en 27: 0
Sensibilidad en 28: 0
Sensibilidad total: -196

Se puede verificar la sensibilidad por diferencias finitas.

Se calcula el valor presente con las curvas desplazadas.

In [64]:
pythoncopy
vp_fija_sens_up = pv.pv(fecha_hoy, fixed_rate_leg, zcc_usd_up)
vp_fija_sens_down = pv.pv(fecha_hoy, fixed_rate_leg, zcc_usd_down)
print(f"Valor presente up de la pata fija es: {vp_fija_sens_up:,.4f}")
print(f"Valor presente down de la pata fija es: {vp_fija_sens_down:,.4f}")
Out[64]:
Valor presente up de la pata fija es: 1,056,008.6812
Valor presente down de la pata fija es: 1,056,012.1915

Finalmente, se calcula la sensibilidad (usando la aproximación central por diferencias finitas).

In [65]:
pythoncopy
print(f"Sensibilidad por diferencias finitas: {(vp_fija_sens_up - vp_fija_sens_down) / 2:,.0f}")
Out[65]:
Sensibilidad por diferencias finitas: -2

OvernightIndex Leg

Se da de alta una pata OvernightIndex.

In [66]:
pythoncopy
rp = qcf.RecPay.RECEIVE
fecha_inicio = qcf.QCDate(10, 1, 2019)
fecha_final = qcf.QCDate(10, 1, 2022)
bus_adj_rule = qcf.BusyAdjRules.FOLLOW
fix_adj_rule = qcf.BusyAdjRules.PREVIOUS
dates_for_eq_rate = qcf.DatesForEquivalentRate.ACCRUAL
periodicidad_pago = qcf.Tenor('6M')
periodo_irregular_pago = qcf.StubPeriod.SHORTFRONT
calendario = qcf.BusinessCalendar(fecha_inicio, 20)
lag_pago = 0
nominal = 38_000_000_000.0
amort_es_flujo = True
spread = .0
gearing = 1.0

overnight_index_leg = qcf.LegFactory.build_bullet_overnight_index_leg(
    rec_pay=rp,
    start_date=fecha_inicio,
    end_date=fecha_final,
    bus_adj_rule=bus_adj_rule,
    fix_adj_rule=fix_adj_rule,
    settlement_periodicity=periodicidad_pago,
    settlement_stub_period=periodo_irregular_pago,
    settlement_calendar=calendario,
    fixing_calendar=calendario,
    settlement_lag=lag_pago,
    initial_notional=nominal,
    amort_is_cashflow=amort_es_flujo,
    spread=spread,
    gearing=gearing,
    interest_rate=qcf.QCInterestRate(0.0, qcf.QCAct360(), qcf.QCLinearWf()),
    index_name='ICPCLP',
    eq_rate_decimal_places=4,
    notional_currency=qcf.QCCLP(),
    dates_for_eq_rate=dates_for_eq_rate,
    sett_lag_behaviour=qcf.SettLagBehaviour.DONT_MOVE,
)
In [67]:
pythoncopy
aux.leg_as_dataframe(overnight_index_leg).style.format(format_dict)
Out[67]:
  fecha_inicial fecha_final fecha_inicial_indice fecha_final_indice fecha_pago nocional amortizacion amort_es_flujo moneda_nocional nombre_indice valor_indice_inicial valor_indice_final valor_tasa tipo_tasa interes flujo spread gearing
0 2019-01-10 2019-07-10 2019-01-10 2019-07-10 2019-07-10 38,000,000,000.00 0.00 True CLP ICPCLP 1.00 1.00 0.0000% LinAct360 0.00 0.00 0.0000% 1.00
1 2019-07-10 2020-01-10 2019-07-10 2020-01-10 2020-01-10 38,000,000,000.00 0.00 True CLP ICPCLP 1.00 1.00 0.0000% LinAct360 0.00 0.00 0.0000% 1.00
2 2020-01-10 2020-07-10 2020-01-10 2020-07-10 2020-07-10 38,000,000,000.00 0.00 True CLP ICPCLP 1.00 1.00 0.0000% LinAct360 0.00 0.00 0.0000% 1.00
3 2020-07-10 2021-01-11 2020-07-10 2021-01-11 2021-01-11 38,000,000,000.00 0.00 True CLP ICPCLP 1.00 1.00 0.0000% LinAct360 0.00 0.00 0.0000% 1.00
4 2021-01-11 2021-07-12 2021-01-11 2021-07-12 2021-07-12 38,000,000,000.00 0.00 True CLP ICPCLP 1.00 1.00 0.0000% LinAct360 0.00 0.00 0.0000% 1.00
5 2021-07-12 2022-01-10 2021-07-12 2022-01-10 2022-01-10 38,000,000,000.00 38,000,000,000.00 True CLP ICPCLP 1.00 1.00 0.0000% LinAct360 0.00 38,000,000,000.00 0.0000% 1.00

Notar que al dar de alta un Leg con OvernightIndexCashflow, los valores futuros de los índeces son los default (=1.0). Por lo tanto, el primer paso para valorizar estos cashflows, es calcular los valores forward de los índices.

Se da de alta un objeto de tipo ForwardRates.

In [68]:
pythoncopy
fwd_rates = qcf.ForwardRates()

Se calculan los índices forward.

In [69]:
pythoncopy
icp_val = 18_890.34 # icp a la fecha de proceso
fecha_hoy = qcf.QCDate(8, 1, 2019)
index = 0

for i in range(overnight_index_leg.size()):
    cashflow = overnight_index_leg.get_cashflow_at(i)
    if cashflow.get_start_date() <= fecha_hoy <= cashflow.get_end_date():
        index = i

icp_fecha_inicio_cupon_vigente = 18_376.69
overnight_index_leg.get_cashflow_at(index).set_start_date_index(icp_fecha_inicio_cupon_vigente)

fwd_rates.set_rates_overnight_index_leg(fecha_hoy, icp_val, overnight_index_leg, zcc_clp)
In [70]:
pythoncopy
aux.leg_as_dataframe(overnight_index_leg).style.format(format_dict)
Out[70]:
  fecha_inicial fecha_final fecha_inicial_indice fecha_final_indice fecha_pago nocional amortizacion amort_es_flujo moneda_nocional nombre_indice valor_indice_inicial valor_indice_final valor_tasa tipo_tasa interes flujo spread gearing
0 2019-01-10 2019-07-10 2019-01-10 2019-07-10 2019-07-10 38,000,000,000.00 0.00 True CLP ICPCLP 18,892.15 19,015.28 1.3000% LinAct360 248,372,222.00 248,372,222.00 0.0000% 1.00
1 2019-07-10 2020-01-10 2019-07-10 2020-01-10 2020-01-10 38,000,000,000.00 0.00 True CLP ICPCLP 19,015.28 19,138.86 1.2700% LinAct360 246,662,222.00 246,662,222.00 0.0000% 1.00
2 2020-01-10 2020-07-10 2020-01-10 2020-07-10 2020-07-10 38,000,000,000.00 0.00 True CLP ICPCLP 19,138.86 19,264.34 1.3000% LinAct360 249,744,444.00 249,744,444.00 0.0000% 1.00
3 2020-07-10 2021-01-11 2020-07-10 2021-01-11 2021-01-11 38,000,000,000.00 0.00 True CLP ICPCLP 19,264.34 19,388.80 1.2600% LinAct360 246,050,000.00 246,050,000.00 0.0000% 1.00
4 2021-01-11 2021-07-12 2021-01-11 2021-07-12 2021-07-12 38,000,000,000.00 0.00 True CLP ICPCLP 19,388.80 19,550.81 1.6500% LinAct360 316,983,333.00 316,983,333.00 0.0000% 1.00
5 2021-07-12 2022-01-10 2021-07-12 2022-01-10 2022-01-10 38,000,000,000.00 38,000,000,000.00 True CLP ICPCLP 19,550.81 19,728.77 1.8000% LinAct360 345,800,000.00 38,345,800,000.00 0.0000% 1.00

Con esto, podemos calcular el valor presente.

In [71]:
pythoncopy
vp_on_index_leg = pv.pv(fecha_hoy, overnight_index_leg, zcc_clp)
print(f"Valor presente pata ON Index Leg: {vp_on_index_leg:,.0f}")
Out[71]:
Valor presente pata ON Index Leg: 37,996,356,295
In [72]:
pythoncopy
print(f"{nominal * zcc_clp.get_discount_factor_at(2):,.0f}")
Out[72]:
37,996,356,295

También en este caso es posible calcular la sensibilidad a la curva de descuento.

In [73]:
pythoncopy
der = pv.get_derivatives()
i = 0
for d in der:
    print(f"Sensibilidad en {i:}: {d * bp / 10_000:0,.0f}")
    i += 1
sens_disc = [d * bp / 10_000 for d in der]
print()
print("Sensibilidad de descuento: {0:,.0f} CLP".format(sum(sens_disc)))
Out[73]:
Sensibilidad en 0: 0
Sensibilidad en 1: 0
Sensibilidad en 2: -670
Sensibilidad en 3: -11,665
Sensibilidad en 4: -545
Sensibilidad en 5: -24,764
Sensibilidad en 6: -35,947
Sensibilidad en 7: -116,962
Sensibilidad en 8: -11,053,193
Sensibilidad en 9: 0
Sensibilidad en 10: 0
Sensibilidad en 11: 0
Sensibilidad en 12: 0
Sensibilidad en 13: 0
Sensibilidad en 14: 0
Sensibilidad en 15: 0
Sensibilidad en 16: 0
Sensibilidad en 17: 0
Sensibilidad en 18: 0

Sensibilidad de descuento: -11,243,745 CLP

La estructura es la misma que para una pata fija, lo que indica que se debe también incluir la sensibilidad a la curva de proyección.

In [74]:
pythoncopy
result = []
for i in range(overnight_index_leg.size()):
    cshflw = overnight_index_leg.get_cashflow_at(i)
    amt_der = cshflw.get_amount_derivatives()
    df = zcc_clp.get_discount_factor_at(fecha_hoy.day_diff(cshflw.get_settlement_date()))
    amt_der = [a * bp / 10_000  * df for a in amt_der]
    if len(amt_der) > 0:
        result.append(np.array(amt_der))
total = result[0] * 0

for r in result:
    total += r

for i in range(len(total)):
    print("Sensibilidad en {0:}: {1:0,.0f}".format(i, total[i]))

print()
print("Sensibilidad de proyección: {0:,.0f} CLP".format(sum(total)))
Out[74]:
Sensibilidad en 0: -13,880
Sensibilidad en 1: -6,940
Sensibilidad en 2: 670
Sensibilidad en 3: 11,665
Sensibilidad en 4: 545
Sensibilidad en 5: 24,764
Sensibilidad en 6: 35,947
Sensibilidad en 7: 116,962
Sensibilidad en 8: 11,053,193
Sensibilidad en 9: 0
Sensibilidad en 10: 0
Sensibilidad en 11: 0
Sensibilidad en 12: 0
Sensibilidad en 13: 0
Sensibilidad en 14: 0
Sensibilidad en 15: 0
Sensibilidad en 16: 0
Sensibilidad en 17: 0
Sensibilidad en 18: 0

Sensibilidad de proyección: 11,222,925 CLP

Como se espera de una pata OvernightIndex (con lag de pago igual a 0 y spread igual a 0), ambas sensibilidades se cancelan.

Se verifica la sensibilidad de proyección por diferencias finitas:

In [75]:
pythoncopy
fwd_rates.set_rates_overnight_index_leg(fecha_hoy, icp_val, overnight_index_leg, zcc_clp_up)
vp_on_index_leg_up = pv.pv(fecha_hoy, overnight_index_leg, zcc_clp)

fwd_rates.set_rates_overnight_index_leg(fecha_hoy, icp_val, overnight_index_leg, zcc_clp_down)
vp_on_index_leg_down = pv.pv(fecha_hoy, overnight_index_leg, zcc_clp)

print(f"Sensibilidad en vértice {vertice}: {(vp_on_index_leg_up - vp_on_index_leg_down) / 2:,.0f} CLP")
Out[75]:
Sensibilidad en vértice 15: 0 CLP

IborCashflow Leg

Se da de alta una pata de tipo IborCashflow.

In [76]:
pythoncopy
rp = qcf.RecPay.RECEIVE
fecha_inicio = qcf.QCDate(12, 11, 2019)
fecha_final = qcf.QCDate(12, 5, 2021)
bus_adj_rule = qcf.BusyAdjRules.MODFOLLOW
periodicidad_pago = qcf.Tenor('6M')
periodo_irregular_pago = qcf.StubPeriod.NO
calendario = qcf.BusinessCalendar(fecha_inicio, 20)
lag_pago = 0
periodicidad_fijacion = qcf.Tenor('6M')
periodo_irregular_fijacion = qcf.StubPeriod.NO

# vamos a usar el mismo calendario para pago y fijaciones
lag_de_fijacion = 2

# Definición del índice
codigo = 'TERMSOFR6M'
lin_act360 = qcf.QCInterestRate(.0, qcf.QCAct360(), qcf.QCLinearWf())
fixing_lag = qcf.Tenor('2d')
tenor = qcf.Tenor('6m')
fixing_calendar = calendario
settlement_calendar = calendario
usd = qcf.QCUSD()
termsofr_6m = qcf.InterestRateIndex(
    codigo,
    lin_act360,
    fixing_lag,
    tenor,
    fixing_calendar,
    settlement_calendar,
    usd
)
# Fin índice

nominal = 20_000_000.0
amort_es_flujo = True
moneda = usd
spread = .02
gearing = 1.0

ibor_leg = qcf.LegFactory.build_bullet_ibor_leg(
    rp, 
    fecha_inicio, 
    fecha_final, 
    bus_adj_rule, 
    periodicidad_pago,
    periodo_irregular_pago, 
    calendario, 
    lag_pago,
    periodicidad_fijacion, 
    periodo_irregular_fijacion,
    calendario, 
    lag_de_fijacion, 
    termsofr_6m,
    nominal, 
    amort_es_flujo, 
    moneda, 
    spread, 
    gearing
)
In [77]:
pythoncopy
aux.leg_as_dataframe(ibor_leg).style.format(format_dict)
Out[77]:
  fecha_inicial fecha_final fecha_fixing fecha_pago nocional amortizacion interes amort_es_flujo flujo moneda_nocional codigo_indice_tasa valor_tasa spread gearing tipo_tasa
0 2019-11-12 2020-05-12 2019-11-08 2020-05-12 20,000,000.00 0.00 202,222.22 True 202,222.22 USD TERMSOFR6M 0.0000% 2.0000% 1.00 LinAct360
1 2020-05-12 2020-11-12 2020-05-08 2020-11-12 20,000,000.00 0.00 204,444.44 True 204,444.44 USD TERMSOFR6M 0.0000% 2.0000% 1.00 LinAct360
2 2020-11-12 2021-05-12 2020-11-10 2021-05-12 20,000,000.00 20,000,000.00 201,111.11 True 20,201,111.11 USD TERMSOFR6M 0.0000% 2.0000% 1.00 LinAct360
In [78]:
pythoncopy
valor_termsofr_6m = 0.02
fecha_hoy = qcf.QCDate(8, 5, 2020)
fwd_rates.set_rates_ibor_leg1(fecha_hoy, ibor_leg, zcc_usd)
In [79]:
pythoncopy
aux.leg_as_dataframe(ibor_leg).style.format(format_dict)
Out[79]:
  fecha_inicial fecha_final fecha_fixing fecha_pago nocional amortizacion interes amort_es_flujo flujo moneda_nocional codigo_indice_tasa valor_tasa spread gearing tipo_tasa
0 2019-11-12 2020-05-12 2019-11-08 2020-05-12 20,000,000.00 0.00 202,222.22 True 202,222.22 USD TERMSOFR6M 0.0000% 2.0000% 1.00 LinAct360
1 2020-05-12 2020-11-12 2020-05-08 2020-11-12 20,000,000.00 0.00 381,670.26 True 381,670.26 USD TERMSOFR6M 1.7337% 2.0000% 1.00 LinAct360
2 2020-11-12 2021-05-12 2020-11-10 2021-05-12 20,000,000.00 20,000,000.00 360,756.76 True 20,360,756.76 USD TERMSOFR6M 1.5876% 2.0000% 1.00 LinAct360
In [80]:
pythoncopy
cci = ibor_leg.get_current_cashflow_index(fecha_hoy)
In [81]:
pythoncopy
ibor_leg.get_cashflow_at(cci).set_interest_rate_value(valor_termsofr_6m)
In [82]:
pythoncopy
termsofr_6m.get_rate().get_value()
Out[82]:
0.02
In [83]:
pythoncopy
aux.leg_as_dataframe(ibor_leg).style.format(format_dict)
Out[83]:
  fecha_inicial fecha_final fecha_fixing fecha_pago nocional amortizacion interes amort_es_flujo flujo moneda_nocional codigo_indice_tasa valor_tasa spread gearing tipo_tasa
0 2019-11-12 2020-05-12 2019-11-08 2020-05-12 20,000,000.00 0.00 404,444.44 True 404,444.44 USD TERMSOFR6M 2.0000% 2.0000% 1.00 LinAct360
1 2020-05-12 2020-11-12 2020-05-08 2020-11-12 20,000,000.00 0.00 381,670.26 True 381,670.26 USD TERMSOFR6M 1.7337% 2.0000% 1.00 LinAct360
2 2020-11-12 2021-05-12 2020-11-10 2021-05-12 20,000,000.00 20,000,000.00 360,756.76 True 20,360,756.76 USD TERMSOFR6M 1.5876% 2.0000% 1.00 LinAct360
In [84]:
pythoncopy
ibor_leg.get_cashflow_at(cci).accrued_interest(fecha_hoy)
Out[84]:
395555.55555555347
In [85]:
pythoncopy
qcf.QCDate(12, 11, 2019).day_diff(fecha_hoy) * (valor_termsofr_6m * gearing + spread) / 360 * nominal
Out[85]:
395555.55555555556
In [86]:
pythoncopy
ibor_leg.get_cashflow_at(cci).get_interest_rate_value()
Out[86]:
0.02
In [87]:
pythoncopy
ibor_leg.get_cashflow_at(cci).get_spread()
Out[87]:
0.02
In [88]:
pythoncopy
termsofr_6m.get_rate().get_value()
Out[88]:
0.02

Se verifica la tasa forward del segundo cashflow.

In [89]:
pythoncopy
which_cashflow = 1
d1 = fecha_hoy.day_diff(ibor_leg.get_cashflow_at(which_cashflow).get_start_date())
d2 = fecha_hoy.day_diff(ibor_leg.get_cashflow_at(which_cashflow).get_end_date())
print(f"d1: {d1:,.0f}")
print(f"d2: {d2:,.0f}")
crv = zcc_usd
w1 = 1 / crv.get_discount_factor_at(d1)
w2 = 1 / crv.get_discount_factor_at(d2)
print(f"Factor forward: {w2 / w1:.4%}")
print(f"Tasa forward: {(w2 / w1 - 1) * 360 / (d2 - d1):.4%}")
print(f"Curve method {crv.get_forward_rate_with_rate(termsofr_6m.get_rate(), d1, d2):.4%}")
Out[89]:
d1: 4
d2: 188
Factor forward: 100.8861%
Tasa forward: 1.7337%
Curve method 1.7337%

Cálculo de valor presente.

In [90]:
pythoncopy
vp_ibor = pv.pv(fecha_hoy, ibor_leg, zcc_usd)
print(f"Valor presente pata IBOR: {vp_ibor:,.0f}")
Out[90]:
Valor presente pata IBOR: 20,802,232

Derivadas del valor presente.

In [91]:
pythoncopy
der = pv.get_derivatives()
i = 0
for d in der:
    print(f"Sensibilidad en {i}: {d * bp / 10_000:0,.0f}")
    i += 1
print()
print(f"Sensibilidad de descuento: {sum(der) * bp / 10_000:,.0f} USD")
Out[91]:
Sensibilidad en 0: 0
Sensibilidad en 1: -0
Sensibilidad en 2: 0
Sensibilidad en 3: 0
Sensibilidad en 4: 0
Sensibilidad en 5: 0
Sensibilidad en 6: 0
Sensibilidad en 7: 0
Sensibilidad en 8: 0
Sensibilidad en 9: -16
Sensibilidad en 10: -4
Sensibilidad en 11: 0
Sensibilidad en 12: 0
Sensibilidad en 13: 0
Sensibilidad en 14: 0
Sensibilidad en 15: -2,002
Sensibilidad en 16: -22
Sensibilidad en 17: 0
Sensibilidad en 18: 0
Sensibilidad en 19: 0
Sensibilidad en 20: 0
Sensibilidad en 21: 0
Sensibilidad en 22: 0
Sensibilidad en 23: 0
Sensibilidad en 24: 0
Sensibilidad en 25: 0
Sensibilidad en 26: 0
Sensibilidad en 27: 0

Sensibilidad de descuento: -2,044 USD

Se verifica la sensibilidad de descuento por diferencias finitas.

In [92]:
pythoncopy
vp_ibor_up = pv.pv(fecha_hoy, ibor_leg, zcc_usd_up)
print(f"Valor presente up pata IBOR: {vp_ibor_up:,.0f}")

vp_ibor_down = pv.pv(fecha_hoy, ibor_leg, zcc_usd_down)
print(f"Valor presente down pata IBOR: {vp_ibor_down:,.0f}")

print(f"Sensibilidad de descuento en el vértice {vertice}: {(vp_ibor_up - vp_ibor_down) / 2:,.0f}")
Out[92]:
Valor presente up pata IBOR: 20,800,231
Valor presente down pata IBOR: 20,804,234
Sensibilidad de descuento en el vértice 15: -2,002

Se calcula también la sensibilidad a la curva de proyección.

In [93]:
pythoncopy
result = []

for i in range(ibor_leg.size()):
    cshflw = ibor_leg.get_cashflow_at(i)
    df = zcc_usd.get_discount_factor_at(fecha_hoy.day_diff(cshflw.get_settlement_date()))
    amt_der = cshflw.get_amount_derivatives()
    if len(amt_der) > 0:
        amt_der = [a * bp / 10_000 * df for a in amt_der]
        result.append(np.array(amt_der))

total = result[0] * 0
for r in result:
    total += r

for i in range(len(total)):
    print(f"Sensibilidad en {i}: {total[i]:0,.0f}")
print()
print(f"Sensibilidad de proyección: {sum(total):,.0f} USD")
Out[93]:
Sensibilidad en 0: 0
Sensibilidad en 1: -22
Sensibilidad en 2: 0
Sensibilidad en 3: 0
Sensibilidad en 4: 0
Sensibilidad en 5: 0
Sensibilidad en 6: 0
Sensibilidad en 7: 0
Sensibilidad en 8: 0
Sensibilidad en 9: 7
Sensibilidad en 10: 2
Sensibilidad en 11: 0
Sensibilidad en 12: 0
Sensibilidad en 13: 0
Sensibilidad en 14: 0
Sensibilidad en 15: 1,982
Sensibilidad en 16: 22
Sensibilidad en 17: 0
Sensibilidad en 18: 0
Sensibilidad en 19: 0
Sensibilidad en 20: 0
Sensibilidad en 21: 0
Sensibilidad en 22: 0
Sensibilidad en 23: 0
Sensibilidad en 24: 0
Sensibilidad en 25: 0
Sensibilidad en 26: 0
Sensibilidad en 27: 0

Sensibilidad de proyección: 1,991 USD

Se verifica la sensibilidad de proyección por diferencias finitas.

In [94]:
pythoncopy
fwd_rates.set_rates_ibor_leg1(fecha_hoy, ibor_leg, zcc_usd_up)
vp_ibor_up = pv.pv(fecha_hoy, ibor_leg, zcc_usd)
print(f"Valor presente up pata IBOR: {vp_ibor_up:,.0f}")

fwd_rates.set_rates_ibor_leg1(fecha_hoy, ibor_leg, zcc_usd_down)
vp_ibor_down = pv.pv(fecha_hoy, ibor_leg, zcc_usd)
print(f"Valor presente down pata IBOR: {vp_ibor_down:,.0f}")

print(f"Sensibilidad de proyección en el vértice {vertice}: {(vp_ibor_up - vp_ibor_down) / 2:,.0f}")
Out[94]:
Valor presente up pata IBOR: 20,804,214
Valor presente down pata IBOR: 20,800,251
Sensibilidad de proyección en el vértice 15: 1,982

IcpClfCashflow Leg

Se da de alta una pata de tipo IcpClfCashflow.

In [95]:
pythoncopy
rp = qcf.RecPay.RECEIVE
fecha_inicio = qcf.QCDate(31, 5, 2018)
fecha_final = qcf.QCDate(31, 3, 2021) 
bus_adj_rule = qcf.BusyAdjRules.FOLLOW
periodicidad_pago = qcf.Tenor('6M')
periodo_irregular_pago = qcf.StubPeriod.SHORTFRONT
calendario = qcf.BusinessCalendar(fecha_inicio, 20)
lag_pago = 0
nominal = 300_000.0
amort_es_flujo = True 
spread = .0
gearing = 1.0

icp_clf_leg = qcf.LegFactory.build_bullet_icp_clf_leg(
    rp, 
    fecha_inicio, 
    fecha_final, 
    bus_adj_rule, 
    periodicidad_pago,
    periodo_irregular_pago, 
    calendario, 
    lag_pago,
    nominal, 
    amort_es_flujo, 
    spread, 
    gearing
)
In [96]:
pythoncopy
aux.leg_as_dataframe(icp_clf_leg).style.format(format_dict)
Out[96]:
  fecha_inicial fecha_final fecha_pago nocional amortizacion amort_es_flujo flujo moneda_nocional icp_inicial icp_final uf_inicial uf_final valor_tasa interes spread gearing tipo_tasa flujo_en_clp
0 2018-05-31 2018-10-01 2018-10-01 300,000.00 0.00 True 0.00 CLF 10,000.00 10,000.00 35,000.00 35,000.00 0.0000% 0.00 0.0000% 1.00 LinAct360 0.00
1 2018-10-01 2019-04-01 2019-04-01 300,000.00 0.00 True 0.00 CLF 10,000.00 10,000.00 35,000.00 35,000.00 0.0000% 0.00 0.0000% 1.00 LinAct360 0.00
2 2019-04-01 2019-09-30 2019-09-30 300,000.00 0.00 True 0.00 CLF 10,000.00 10,000.00 35,000.00 35,000.00 0.0000% 0.00 0.0000% 1.00 LinAct360 0.00
3 2019-09-30 2020-03-31 2020-03-31 300,000.00 0.00 True 0.00 CLF 10,000.00 10,000.00 35,000.00 35,000.00 0.0000% 0.00 0.0000% 1.00 LinAct360 0.00
4 2020-03-31 2020-09-30 2020-09-30 300,000.00 0.00 True 0.00 CLF 10,000.00 10,000.00 35,000.00 35,000.00 0.0000% 0.00 0.0000% 1.00 LinAct360 0.00
5 2020-09-30 2021-03-31 2021-03-31 300,000.00 300,000.00 True 300,000.00 CLF 10,000.00 10,000.00 35,000.00 35,000.00 0.0000% 0.00 0.0000% 1.00 LinAct360 10,500,000,000.00
In [97]:
pythoncopy
icp_hoy = 18_882.07
uf_hoy = 28_440.19
fwd_rates.set_rates_icp_clf_leg(fecha_hoy, icp_hoy, uf_hoy, icp_clf_leg, zcc_clp, zcc_clp, zcc_clf)
cshflw = icp_clf_leg.get_cashflow_at(3)
cshflw.set_start_date_uf(28_080.26)
cshflw.set_start_date_icp(18_786.13)
In [98]:
pythoncopy
aux.leg_as_dataframe(icp_clf_leg).style.format(format_dict)
Out[98]:
  fecha_inicial fecha_final fecha_pago nocional amortizacion amort_es_flujo flujo moneda_nocional icp_inicial icp_final uf_inicial uf_final valor_tasa interes spread gearing tipo_tasa flujo_en_clp
0 2018-05-31 2018-10-01 2018-10-01 300,000.00 0.00 True 0.00 CLF 10,000.00 10,000.00 35,000.00 35,000.00 0.0000% 0.00 0.0000% 1.00 LinAct360 0.00
1 2018-10-01 2019-04-01 2019-04-01 300,000.00 0.00 True 0.00 CLF 10,000.00 10,000.00 35,000.00 35,000.00 0.0000% 0.00 0.0000% 1.00 LinAct360 0.00
2 2019-04-01 2019-09-30 2019-09-30 300,000.00 0.00 True 0.00 CLF 10,000.00 10,000.00 35,000.00 35,000.00 0.0000% 0.00 0.0000% 1.00 LinAct360 0.00
3 2019-09-30 2020-03-31 2020-03-31 300,000.00 0.00 True -171,885.95 CLF 18,786.13 10,000.00 28,080.26 35,000.00 -112.7121% -171,885.95 0.0000% 1.00 LinAct360 -6,016,008,337.00
4 2020-03-31 2020-09-30 2020-09-30 300,000.00 0.00 True 391,681.30 CLF 10,000.00 18,986.62 35,000.00 28,822.76 256.8402% 391,681.30 0.0000% 1.00 LinAct360 11,289,337,594.00
5 2020-09-30 2021-03-31 2021-03-31 300,000.00 300,000.00 True 297,808.27 CLF 18,986.62 19,102.24 28,822.76 29,211.03 -1.4451% -2,191.73 0.0000% 1.00 LinAct360 8,699,285,743.00
In [99]:
pythoncopy
vp_icp_clf = pv.pv(fecha_hoy, icp_clf_leg, zcc_clf)
print(f"Valor presente en UF: {vp_icp_clf:,.2f}")
print(f"Valor presente en CLP: {vp_icp_clf * uf_hoy:,.0f}")
Out[99]:
Valor presente en UF: 697,118.18
Valor presente en CLP: 19,826,173,500

Sensibilidad de Descuento

In [100]:
pythoncopy
der = pv.get_derivatives()
i = 0
for d in der:
    print(f"Sensibilidad en {i}: {d * bp / 10_000:0,.2f}")
    i += 1
print()
print(f"Sensibilidad de descuento: {sum(der) * bp / 10_000:,.2f} CLF")
Out[100]:
Sensibilidad en 0: 0.00
Sensibilidad en 1: 0.00
Sensibilidad en 2: 0.00
Sensibilidad en 3: 0.00
Sensibilidad en 4: 0.00
Sensibilidad en 5: -5.41
Sensibilidad en 6: -10.27
Sensibilidad en 7: 0.00
Sensibilidad en 8: 0.00
Sensibilidad en 9: 0.00
Sensibilidad en 10: 0.00
Sensibilidad en 11: -11.85
Sensibilidad en 12: -15.24
Sensibilidad en 13: 0.00
Sensibilidad en 14: 0.00
Sensibilidad en 15: 0.00
Sensibilidad en 16: 0.00
Sensibilidad en 17: 0.00
Sensibilidad en 18: 0.00
Sensibilidad en 19: 0.00
Sensibilidad en 20: 0.00
Sensibilidad en 21: 0.00
Sensibilidad en 22: 0.00
Sensibilidad en 23: 0.00
Sensibilidad en 24: 0.00
Sensibilidad en 25: 0.00
Sensibilidad en 26: 0.00
Sensibilidad en 27: 0.00
Sensibilidad en 28: 0.00
Sensibilidad en 29: 0.00
Sensibilidad en 30: 0.00
Sensibilidad en 31: 0.00
Sensibilidad en 32: 0.00

Sensibilidad de descuento: -42.77 CLF

Sensibilidad de Proyección

In [101]:
pythoncopy
result = []
for i in range(icp_clf_leg.size()):
    cshflw = icp_clf_leg.get_cashflow_at(i)
    df = zcc_clf.get_discount_factor_at(fecha_hoy.day_diff(cshflw.date()))
    amt_der = cshflw.get_amount_ufclf_derivatives()
    if len(amt_der) > 0:
        amt_der = [a * bp / 10_000 * df for a in amt_der]
        result.append(np.array(amt_der))

total = result[0] * 0
for r in result:
    total += r

for i in range(len(total)):
    print(f"Sensibilidad en {i}: {total[i]:0,.2f}")
Out[101]:
Sensibilidad en 0: 0.00
Sensibilidad en 1: 0.00
Sensibilidad en 2: 0.00
Sensibilidad en 3: 0.00
Sensibilidad en 4: 0.00
Sensibilidad en 5: 5.41
Sensibilidad en 6: 10.27
Sensibilidad en 7: 0.00
Sensibilidad en 8: 0.00
Sensibilidad en 9: 0.00
Sensibilidad en 10: 0.00
Sensibilidad en 11: 11.85
Sensibilidad en 12: 15.24
Sensibilidad en 13: 0.00
Sensibilidad en 14: 0.00
Sensibilidad en 15: 0.00
Sensibilidad en 16: 0.00
Sensibilidad en 17: 0.00
Sensibilidad en 18: 0.00
Sensibilidad en 19: 0.00
Sensibilidad en 20: 0.00
Sensibilidad en 21: 0.00
Sensibilidad en 22: 0.00
Sensibilidad en 23: 0.00
Sensibilidad en 24: 0.00
Sensibilidad en 25: 0.00
Sensibilidad en 26: 0.00
Sensibilidad en 27: 0.00
Sensibilidad en 28: 0.00
Sensibilidad en 29: 0.00
Sensibilidad en 30: 0.00
Sensibilidad en 31: 0.00
Sensibilidad en 32: 0.00

CompoundedOvernightRate Leg

Se da de alta una pata de tipo CompoundedOvernightRate.

In [102]:
pythoncopy
rp = qcf.RecPay.PAY
fecha_inicio = qcf.QCDate(13, 6, 2024)
fecha_final = qcf.QCDate(13, 12, 2025)
bus_adj_rule = qcf.BusyAdjRules.MODFOLLOW
periodicidad_pago = qcf.Tenor('12M')
periodo_irregular_pago = qcf.StubPeriod.SHORTFRONT
calendario = qcf.BusinessCalendar(fecha_inicio, 20)
lag_pago = 0
lookback = 0

######################################################################
# Definición del índice

codigo = 'OISTEST'
lin_act360 = qcf.QCInterestRate(.0, qcf.QCAct360(), qcf.QCLinearWf())
fixing_lag = qcf.Tenor('0d')
tenor = qcf.Tenor('1d')
fixing_calendar = calendario
settlement_calendar = calendario
usd = qcf.QCUSD()
oistest = qcf.InterestRateIndex(
    codigo,
    lin_act360,
    fixing_lag,
    tenor,
    fixing_calendar,
    settlement_calendar,
    usd)

# Fin índice
######################################################################

nominal = 1_000_000.0
amort_es_flujo = True
moneda = usd
spread = .0
gearing = 1.0

cor_leg = qcf.LegFactory.build_bullet_compounded_overnight_rate_leg_2(
    rp,
    fecha_inicio,
    fecha_final,
    bus_adj_rule,
    periodicidad_pago,
    periodo_irregular_pago,
    calendario,
    lag_pago,
    calendario,
    oistest,
    nominal,
    amort_es_flujo,
    usd,
    spread,
    gearing,
    qcf.QCInterestRate(0.0, qcf.QCAct360(), qcf.QCLinearWf()),
    10,
    lookback,
    0
)

Valor Presente

In [103]:
pythoncopy
ts = qcf.time_series()
fwd_rates.set_rates_compounded_overnight_leg2(
    fecha_inicio,
    cor_leg,
    zcc_sofr,
    ts
)
In [104]:
pythoncopy
aux.leg_as_dataframe(cor_leg).style.format(format_dict)
Out[104]:
  fecha_inicial fecha_final fecha_pago nocional amortizacion interes amort_es_flujo flujo moneda_nocional codigo_indice_tasa tipo_tasa valor_tasa spread gearing
0 2024-06-13 2024-12-13 2024-12-13 -1,000,000.00 0.00 -27,002.51 True -27,002.51 USD OISTEST LinAct360 5.3120% 0.0000% 1.00
1 2024-12-13 2025-12-15 2025-12-15 -1,000,000.00 -1,000,000.00 -47,599.76 True -1,047,599.76 USD OISTEST LinAct360 4.6692% 0.0000% 1.00
In [105]:
pythoncopy
print(f'Valor presente: {pv.pv(fecha_inicio, cor_leg, zcc_sofr):,.0f}')
Out[105]:
Valor presente: -1,000,000

Sensibilidad de Proyección

In [106]:
pythoncopy
proj_sens_by_cashflow = np.array([np.array(
    np.array(cor_leg.get_cashflow_at(i).get_amount_derivatives()) *
    zcc_usd.get_discount_factor_at(fecha_hoy.day_diff(cor_leg.get_cashflow_at(i).get_settlement_date())) * bp / 10_000)
                             for i in range(cor_leg.size())])
proj_sens = np.sum(proj_sens_by_cashflow, axis=0)
for i, s in enumerate(proj_sens):
     print(f"Sensibilidad en {i}: {s:0,.2f}")
Out[106]:
Sensibilidad en 0: 0.00
Sensibilidad en 1: 0.00
Sensibilidad en 2: 0.00
Sensibilidad en 3: 0.00
Sensibilidad en 4: 0.00
Sensibilidad en 5: 0.00
Sensibilidad en 6: 0.00
Sensibilidad en 7: 0.00
Sensibilidad en 8: 0.01
Sensibilidad en 9: 0.07
Sensibilidad en 10: 0.00
Sensibilidad en 11: 0.00
Sensibilidad en 12: 0.00
Sensibilidad en 13: 0.00
Sensibilidad en 14: -143.95
Sensibilidad en 15: 0.00
Sensibilidad en 16: 0.00
Sensibilidad en 17: 0.00
Sensibilidad en 18: 0.00
Sensibilidad en 19: 0.00
Sensibilidad en 20: 0.00
Sensibilidad en 21: 0.00
Sensibilidad en 22: 0.00
Sensibilidad en 23: 0.00
Sensibilidad en 24: 0.00
Sensibilidad en 25: 0.00
Sensibilidad en 26: 0.00
Sensibilidad en 27: 0.00
Sensibilidad en 28: 0.00

Verifica sensibilidad de proyección.

In [107]:
pythoncopy
fwd_rates.set_rates_compounded_overnight_leg2(fecha_hoy, cor_leg, zcc_usd_up, ts)
vp_cor_up = pv.pv(fecha_hoy, cor_leg, zcc_usd)
print(f"Valor presente up pata CompoundedOvernightRate: {vp_cor_up:,.0f}")

fwd_rates.set_rates_compounded_overnight_leg2(fecha_hoy, cor_leg, zcc_usd_down, ts)
vp_cor_down = pv.pv(fecha_hoy, cor_leg, zcc_usd)
print(f"Valor presente down pata CompoundedOvernightRate: {vp_cor_down:,.0f}")

print(f"Sensibilidad de proyección en el vértice {vertice}: {(vp_cor_up - vp_cor_down) / 2:,.2f}")
Out[107]:
Valor presente up pata CompoundedOvernightRate: -936,739
Valor presente down pata CompoundedOvernightRate: -936,739
Sensibilidad de proyección en el vértice 15: 0.00

Sensibilidad de Descuento

In [108]:
pythoncopy
disc_der = np.array(pv.get_derivatives()) * bp / 10_000
for i, s in enumerate(disc_der):
    print(f"Sensibilidad en {i}: {s:0,.2f}")
Out[108]:
Sensibilidad en 0: 0.00
Sensibilidad en 1: 0.00
Sensibilidad en 2: 0.00
Sensibilidad en 3: 0.00
Sensibilidad en 4: 0.00
Sensibilidad en 5: 0.00
Sensibilidad en 6: 0.00
Sensibilidad en 7: 0.00
Sensibilidad en 8: 0.00
Sensibilidad en 9: 0.00
Sensibilidad en 10: 0.00
Sensibilidad en 11: 0.00
Sensibilidad en 12: 0.00
Sensibilidad en 13: 0.00
Sensibilidad en 14: 0.00
Sensibilidad en 15: 0.00
Sensibilidad en 16: 0.00
Sensibilidad en 17: 0.00
Sensibilidad en 18: 0.00
Sensibilidad en 19: 1.49
Sensibilidad en 20: 366.30
Sensibilidad en 21: 156.75
Sensibilidad en 22: 0.00
Sensibilidad en 23: 0.00
Sensibilidad en 24: 0.00
Sensibilidad en 25: 0.00
Sensibilidad en 26: 0.00
Sensibilidad en 27: 0.00

Verifica la sensibilidad de descuento.

In [109]:
pythoncopy
fwd_rates.set_rates_compounded_overnight_leg2(fecha_hoy, cor_leg, zcc_usd, ts)
vp_cor_up = pv.pv(fecha_hoy, cor_leg, zcc_usd_up)
print(f"Valor presente up pata CompoundedOvernightRate: {vp_cor_up:,.2f}")

fwd_rates.set_rates_compounded_overnight_leg2(fecha_hoy, cor_leg, zcc_usd, ts)
vp_cor_down = pv.pv(fecha_hoy, cor_leg, zcc_usd_down)
print(f"Valor presente down pata CompoundedOvernightRate: {vp_cor_down:,.2f}")

print(f"Sensibilidad de descuento en el vértice {vertice}: {(vp_cor_up - vp_cor_down) / 2:,.2f}")
Out[109]:
Valor presente up pata CompoundedOvernightRate: -936,738.63
Valor presente down pata CompoundedOvernightRate: -936,738.63
Sensibilidad de descuento en el vértice 15: 0.00
In [110]:
pythoncopy
In [111]:
pythoncopy
In [112]:
pythoncopy
In [113]:
pythoncopy
In [114]:
pythoncopy
In [115]:
pythoncopy
In [116]:
pythoncopy
In [117]:
pythoncopy