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