Renta Fija Local: Escenarios#
En este notebook aprenderemos a valorizar un portfolio de papeles en distintos escenarios, tanto a fecha de hoy como en una fecha futura
Librerías#
from datetime import date
import datetime
import bisect
import pandas as pd
import numpy as np
import autograd.numpy as agnp
from autograd import grad
import my_functions as mf
Data de Bonos#
Vamos a utilizar la misma base de datos de la sesión anterior.
bonos = pd.read_excel('data/bonos_empresa_carga_inicial.xlsx')
Al hacer el append, se regenera el índice. Con el valor por default ignore_index=True
se matienen los índices originales de ambos DataFrame
.
bonos = bonos.append(pd.read_excel('data/bonos_estado_carga_inicial.xlsx'), ignore_index=True)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
/tmp/ipykernel_2231/2206367077.py in ?()
----> 1 bonos = bonos.append(pd.read_excel('data/bonos_estado_carga_inicial.xlsx'), ignore_index=True)
/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/pandas/core/generic.py in ?(self, name)
6295 and name not in self._accessors
6296 and self._info_axis._can_hold_identifiers_and_holds_name(name)
6297 ):
6298 return self[name]
-> 6299 return object.__getattribute__(self, name)
AttributeError: 'DataFrame' object has no attribute 'append'
tablas_desarrollo = pd.read_csv('data/tablas_desarrollo.csv')
tablas_desarrollo.head()
nemotecnico | numero_cupon | fecha_vcto_cupon | interes | amortizacion | saldo_insoluto | |
---|---|---|---|---|---|---|
0 | BC18-A0719 | 1 | 2019-10-31 | 0.98534 | 0.00 | 100.00 |
1 | BC18-A0719 | 2 | 2020-01-31 | 0.98534 | 0.00 | 100.00 |
2 | BC18-A0719 | 3 | 2020-04-30 | 0.98534 | 0.00 | 100.00 |
3 | BC18-A0719 | 4 | 2020-07-31 | 0.98534 | 4.65 | 100.00 |
4 | BC18-A0719 | 5 | 2020-10-31 | 0.93952 | 0.00 | 95.35 |
Valorización de un Portfolio#
Vamos a definir un portfolio de bonos con el cual realizar los cálculos. La posición repetida en BTP0470930 es para considerar la práctica usual en Chile de considerar palo a palo las transacciones y no el precio promedio de toda la posición.
portfolio = [
{
'nemotecnico': 'BTP0470930',
'monto': 1000000000,
'fecha_compra': date(2021, 3, 18),
'tir_compra': .0321,
'monto_compra': 1124753559,
},
{
'nemotecnico': 'BTP0470930',
'monto': 2000000000,
'fecha_compra': date(2021, 2, 23),
'tir_compra': .0293,
'monto_compra': 2340842895,
},
{
'nemotecnico': 'BTU0200335',
'monto': 300000,
'fecha_compra': date(2021, 7, 7),
'tir_compra': .0225,
'monto_compra': 8732178169,
},
{
'nemotecnico': 'BBNS-W0414',
'monto': 100000,
'fecha_compra': date(2020, 9, 3),
'tir_compra': -.0009,
'monto_compra': 3657255336,
},
{
'nemotecnico': 'BSTDW11218',
'monto': 300000,
'fecha_compra': date(2020, 11, 13),
'tir_compra': -.0045,
'monto_compra': 9533881736,
},
{
'nemotecnico': 'BCCA-F0919',
'monto': 1000000000,
'fecha_compra': date(2021, 3, 24),
'tir_compra': .0721 ,
'monto_compra': 1013970102,
}
]
La fecha de valorización.
fecha_valor = date(2021, 8, 23)
Y los datos de mercado correspondientes.
ufs = {fecha_valor: 29850.56}
mkt = {
'BTP0470930': {
'tir_mcdo': .01,
'base': .0028,
'spread': 0.0072
},
'BTU0200335': {
'tir_mcdo': .0196,
'base': .0196,
'spread': 0.0
},
'BBNS-W0414': {
'tir_mcdo': .0016,
'base': -.0046,
'spread': 0.0062
},
'BSTDW11218': {
'tir_mcdo': .01,
'base': .0028,
'spread': 0.0072
},
'BCCA-F0919': {
'tir_mcdo': .09,
'base': .0326,
'spread': 0.0574
}
}
def get_valorizador_with_data(bonos, tablas_desarrollo, ufs):
def wrapper(fecha_valor, nemotecnico, tir, monto):
return mf.valorizador_rf(
fecha_valor,
nemotecnico,
tir,
monto,
bonos,
tablas_desarrollo,
ufs
)
return wrapper
valorizador = get_valorizador_with_data(bonos, tablas_desarrollo, ufs)
def valor_portfolio(fecha_val, escenario, valorizador):
return [
valorizador(fecha_valor, nemo, escenario[nemo]['tir_mcdo'], 100) for nemo in mkt
]
result = valor_portfolio(fecha_valor, mkt, valorizador)
result
[{'nemotecnico': 'BTP0470930',
'fecha_valor': datetime.date(2021, 8, 23),
'precio': 1.311553,
'valor_par': 102.25004038844081,
'valor_presente': 134.10639740039392,
'valor_pago': 134.1063472215807,
'duracion': 7.572087109563799,
'convexidad': 70.43408834145194},
{'nemotecnico': 'BTU0200335',
'fecha_valor': datetime.date(2021, 8, 23),
'precio': 1.00566,
'valor_par': 100.95799598568694,
'valor_presente': 101.52938308528377,
'valor_pago': 3030709.991026749,
'duracion': 11.814434787227066,
'convexidad': 158.25644977322216},
{'nemotecnico': 'BBNS-W0414',
'fecha_valor': datetime.date(2021, 8, 23),
'precio': 1.072975,
'valor_par': 101.18073158224423,
'valor_presente': 108.56439638814848,
'valor_pago': 3240708.0008247993,
'duracion': 2.504589715818105,
'convexidad': 8.928432876386603},
{'nemotecnico': 'BSTDW11218',
'fecha_valor': datetime.date(2021, 8, 23),
'precio': 1.020312,
'valor_par': 100.35149796108647,
'valor_presente': 102.3898545736525,
'valor_pago': 3056393.9903010605,
'duracion': 3.670643928974249,
'convexidad': 17.053259165667136},
{'nemotecnico': 'BCCA-F0919',
'fecha_valor': datetime.date(2021, 8, 23),
'precio': 0.976063,
'valor_par': 98.13871071693468,
'valor_presente': 95.78953587514249,
'valor_pago': 95.7895643985034,
'duracion': 2.279816295851228,
'convexidad': 6.684113183332923}]
Vamos a procesar un poco este resultado para que resulte más legible y contenga también la información del portfolio.
def genera_informe(resultado_valorizacion, portfolio, escenario):
"""
"""
df_valor = pd.DataFrame.from_dict(resultado_valorizacion)
df_portfolio = pd.DataFrame.from_dict(portfolio)
df_portfolio['tir_mcdo'] = df_portfolio.apply(
lambda row: escenario[row['nemotecnico']]['tir_mcdo'], axis=1)
df_portfolio = df_portfolio.merge(
df_valor[['nemotecnico', 'valor_pago', 'precio', 'duracion', 'convexidad']])
df_portfolio['valor_pago'] = df_portfolio['valor_pago'] * df_portfolio['monto'] / 100.0
return df_portfolio
df_portfolio = genera_informe(result, portfolio, mkt)
df_portfolio.style.format({
'tir_compra': '{:.4%}',
'tir_mcdo': '{:.4%}',
'precio': '{:.4%}',
'monto': '{:,.0f}',
'monto_compra': '{:,.0f}',
'valor_pago': '{:,.0f}',
'duracion': '{:,.2f}',
'convexidad': '{:,.2f}',
})
nemotecnico | monto | fecha_compra | tir_compra | monto_compra | tir_mcdo | valor_pago | precio | duracion | convexidad | |
---|---|---|---|---|---|---|---|---|---|---|
0 | BTP0470930 | 1,000,000,000 | 2021-03-18 | 3.2100% | 1,124,753,559 | 1.0000% | 1,341,063,472 | 131.1553% | 7.57 | 70.43 |
1 | BTP0470930 | 2,000,000,000 | 2021-02-23 | 2.9300% | 2,340,842,895 | 1.0000% | 2,682,126,944 | 131.1553% | 7.57 | 70.43 |
2 | BTU0200335 | 300,000 | 2021-07-07 | 2.2500% | 8,732,178,169 | 1.9600% | 9,092,129,973 | 100.5660% | 11.81 | 158.26 |
3 | BBNS-W0414 | 100,000 | 2020-09-03 | -0.0900% | 3,657,255,336 | 0.1600% | 3,240,708,001 | 107.2975% | 2.50 | 8.93 |
4 | BSTDW11218 | 300,000 | 2020-11-13 | -0.4500% | 9,533,881,736 | 1.0000% | 9,169,181,971 | 102.0312% | 3.67 | 17.05 |
5 | BCCA-F0919 | 1,000,000,000 | 2021-03-24 | 7.2100% | 1,013,970,102 | 9.0000% | 957,895,644 | 97.6063% | 2.28 | 6.68 |
El valor total del portfolio es:
print(f"Valor total: {df_portfolio['valor_pago'].sum(): ,.0f}")
Valor total: 26,483,106,005
Definir Escenarios de Valorización#
Supongamos que queremos saber cuánto cambia el valor del portfolio si las tasas base suben N puntos básicos. Vamos a definir una función que dado un número puntos básicos y un escenario base de valorización construya el escenario deseado.
def escenario_tasa_base(n, escenario_base):
return {
k: {
'tir_mcdo': round(escenario_base[k]['tir_mcdo'] + n / 10000., 6),
'base': round(escenario_base[k]['base'] + n / 10000., 6),
'spread': escenario_base[k]['spread']
} for k in escenario_base
}
mkt_10pb_mas_base = escenario_tasa_base(10, mkt)
mkt_10pb_mas_base
{'BTP0470930': {'tir_mcdo': 0.011, 'base': 0.0038, 'spread': 0.0072},
'BTU0200335': {'tir_mcdo': 0.0206, 'base': 0.0206, 'spread': 0.0},
'BBNS-W0414': {'tir_mcdo': 0.0026, 'base': -0.0036, 'spread': 0.0062},
'BSTDW11218': {'tir_mcdo': 0.011, 'base': 0.0038, 'spread': 0.0072},
'BCCA-F0919': {'tir_mcdo': 0.091, 'base': 0.0336, 'spread': 0.0574}}
Podemos generar un informe de valorización con este escenario:
nuevo_valor = valor_portfolio(fecha_valor, mkt_10pb_mas_base, valorizador)
df_portfolio_10pb_mas_base = genera_informe(
nuevo_valor,
portfolio,
mkt_10pb_mas_base
)
df_portfolio_10pb_mas_base.style.format({
'tir_compra': '{:.4%}',
'tir_mcdo': '{:.4%}',
'precio': '{:.4%}',
'monto': '{:,.0f}',
'monto_compra': '{:,.0f}',
'valor_pago': '{:,.0f}',
'duracion': '{:,.2f}',
'convexidad': '{:,.2f}',
})
nemotecnico | monto | fecha_compra | tir_compra | monto_compra | tir_mcdo | valor_pago | precio | duracion | convexidad | |
---|---|---|---|---|---|---|---|---|---|---|
0 | BTP0470930 | 1,000,000,000 | 2021-03-18 | 3.2100% | 1,124,753,559 | 1.1000% | 1,331,057,283 | 130.1767% | 7.57 | 70.21 |
1 | BTP0470930 | 2,000,000,000 | 2021-02-23 | 2.9300% | 2,340,842,895 | 1.1000% | 2,662,114,567 | 130.1767% | 7.57 | 70.21 |
2 | BTU0200335 | 300,000 | 2021-07-07 | 2.2500% | 8,732,178,169 | 2.0600% | 8,987,489,923 | 99.4086% | 11.80 | 157.73 |
3 | BBNS-W0414 | 100,000 | 2020-09-03 | -0.0900% | 3,657,255,336 | 0.2600% | 3,232,619,633 | 107.0297% | 2.50 | 8.91 |
4 | BSTDW11218 | 300,000 | 2020-11-13 | -0.4500% | 9,533,881,736 | 1.1000% | 9,135,940,370 | 101.6613% | 3.67 | 17.02 |
5 | BCCA-F0919 | 1,000,000,000 | 2021-03-24 | 7.2100% | 1,013,970,102 | 9.1000% | 955,894,596 | 97.4024% | 2.28 | 6.67 |
print(f"Valor total +10 pb en base: {df_portfolio_10pb_mas_base['valor_pago'].sum(): ,.0f}")
Valor total +10 pb en base: 26,305,116,373
Ejercicio#
Hacer un escenario que mueva sólo el spread.
def escenario_spread(n, escenario_base):
def is_gov(nemo):
return nemo[0:3] in ['BTP', 'BTU', 'BCU', 'BCP']
def tir(nemo):
return (round(escenario_base[nemo]['tir_mcdo'] + n / 10000., 6) if
not is_gov(nemo) else escenario_base[nemo]['tir_mcdo'])
def spread(nemo):
return (round(escenario_base[nemo]['spread'] + n / 10000., 6) if
not is_gov(nemo) else escenario_base[nemo]['spread'])
return {
k: {
'tir_mcdo': tir(k),
'base': escenario_base[k]['base'],
'spread': spread(k),
} for k in escenario_base
}
mkt_10pb_mas_spread = escenario_spread(10, mkt)
mkt_10pb_mas_spread
{'BTP0470930': {'tir_mcdo': 0.01, 'base': 0.0028, 'spread': 0.0072},
'BTU0200335': {'tir_mcdo': 0.0196, 'base': 0.0196, 'spread': 0.0},
'BBNS-W0414': {'tir_mcdo': 0.0026, 'base': -0.0046, 'spread': 0.0072},
'BSTDW11218': {'tir_mcdo': 0.011, 'base': 0.0028, 'spread': 0.0082},
'BCCA-F0919': {'tir_mcdo': 0.091, 'base': 0.0326, 'spread': 0.0584}}
nuevo_valor_2 = valor_portfolio(fecha_valor, mkt_10pb_mas_spread, valorizador)
df_portfolio_10pb_spread = genera_informe(
nuevo_valor_2,
portfolio,
mkt_10pb_mas_spread
)
df_portfolio_10pb_spread.style.format({
'tir_compra': '{:.4%}',
'tir_mcdo': '{:.4%}',
'precio': '{:.4%}',
'monto': '{:,.0f}',
'monto_compra': '{:,.0f}',
'valor_pago': '{:,.0f}',
'duracion': '{:,.2f}',
'convexidad': '{:,.2f}',
})
nemotecnico | monto | fecha_compra | tir_compra | monto_compra | tir_mcdo | valor_pago | precio | duracion | convexidad | |
---|---|---|---|---|---|---|---|---|---|---|
0 | BTP0470930 | 1,000,000,000 | 2021-03-18 | 3.2100% | 1,124,753,559 | 1.0000% | 1,341,063,472 | 131.1553% | 7.57 | 70.43 |
1 | BTP0470930 | 2,000,000,000 | 2021-02-23 | 2.9300% | 2,340,842,895 | 1.0000% | 2,682,126,944 | 131.1553% | 7.57 | 70.43 |
2 | BTU0200335 | 300,000 | 2021-07-07 | 2.2500% | 8,732,178,169 | 1.9600% | 9,092,129,973 | 100.5660% | 11.81 | 158.26 |
3 | BBNS-W0414 | 100,000 | 2020-09-03 | -0.0900% | 3,657,255,336 | 0.2600% | 3,232,619,633 | 107.0297% | 2.50 | 8.91 |
4 | BSTDW11218 | 300,000 | 2020-11-13 | -0.4500% | 9,533,881,736 | 1.1000% | 9,135,940,370 | 101.6613% | 3.67 | 17.02 |
5 | BCCA-F0919 | 1,000,000,000 | 2021-03-24 | 7.2100% | 1,013,970,102 | 9.1000% | 955,894,596 | 97.4024% | 2.28 | 6.67 |
print(f"Valor total +10 pb en spread: {df_portfolio_10pb_spread['valor_pago'].sum(): ,.0f}")
Valor total +10 pb en spread: 26,439,774,989