<jupyter_start><jupyter_text>Renta Fija Local En este notebook construiremos un valorizador de renta fija local similar al de la Bolsa de Comercio o RiskAmerica. Dado un nemotécnico y su TIR de mercado, obtendremos el precio, valor par, duración, convexidad y monto a pagar. Adicionalmente, con este valorizador podremos construir una funcionalidad adicional, valorizar un forward de renta fija.  Librerías<jupyter_code>from datetime import date
import pandas as pd
import numpy as np

import autograd.numpy as agnp
from autograd import grad

import my_functions as mf<jupyter_output><empty_output><jupyter_text>Data<jupyter_code>bonos = pd.read_excel('data/bonos_empresa_carga_inicial.xlsx')
bonos = bonos.append(pd.read_excel('data/bonos_estado_carga_inicial.xlsx'))
tablas_desarrollo = pd.read_csv('data/tablas_desarrollo.csv')
tablas_desarrollo<jupyter_output><empty_output><jupyter_text>Valorizador Nos gustaría poder reciclar las funciones que calculan el valor par y el valor presente, pero, nos damos cuenta, que las últimas versiones se han adaptado mucho al formato de la data disponible y ahora tenemos que hacer algunos cambios.Vamos a definir funciones `valor_presente` y `valor_par` que representen de la forma más general posible ambos cálculos. De esta manera, el formato particular de la data disponible se transforma en un problema totalmente separado del proceso de cálculo en sí.  Función `valor_presente`<jupyter_code>def valor_presente(fecha_valor, fechas, flujos, tasa):
    """
    Calcula el valor presente de un conjunto de flujos utilizando una única tasa de descuento.
    
    Parameters
    ----------
    
    fecha_valor: datetime.date
        Fecha a la cual se quiere obtener el valor presente.
        
    fechas: List[datetime.date]
        Fechas de pago de los flujos.
        
    flujos: List[float]
        Flujos a traer a valor presente. Deben corresponder a las fechas en el parámetro `fechas`.
        Los flujos cuyas fechas sean iguales o previas a `fecha_valor` no serán incluidos en el cálculo.
        
    tasa: float
        Tasa de descuento a utilizar. Debe estar en convención Com Act/365.
        
    Returns
    -------
    
    Un `float` que corresponde al valor presente de los flujos.
    """
    result = 0.0
    for fec, fl in zip(fechas, flujos):
        p = (fec - fecha_valor).days
        if p > 0:
            result += fl * (1 + tasa)**(-p / 365.0)
    return result<jupyter_output><empty_output><jupyter_text>Realicemos una prueba con la data que tenemos disponible en este notebook. Utilizaremos el bono BBNS-W0414.<jupyter_code>tabla = tablas_desarrollo[tablas_desarrollo.nemotecnico == 'BBNS-W0414']
tabla<jupyter_output><empty_output><jupyter_text>Vemos que los flujos corresponden a la suma de las columnas `interes` y `amortizacion` y las fechas están en la columna `fecha_vcto_cupon`.<jupyter_code>flujos = tabla['interes'] + tabla['amortizacion']<jupyter_output><empty_output><jupyter_text>El tipo de la variables `flujos` es:<jupyter_code>type(flujos)<jupyter_output><empty_output><jupyter_text>Para tener un `np.array` con los valores aplicaremos:<jupyter_code>flujos.values<jupyter_output><empty_output><jupyter_text>Obtenemos un `numpy.array` que es un tipo similar a una `List`, pero optimizado para cálculos.Para las fechas aplicamos un procedimiento similar:<jupyter_code>fechas = tabla['fecha_vcto_cupon'].values
fechas<jupyter_output><empty_output><jupyter_text>Vemos que tenemos que obtenemos un `numpy.array` de `str`, pero queremos `datetime.date`.<jupyter_code>import datetime
fechas = np.asarray([datetime.datetime.strptime(f, "%Y-%m-%d").date() for f in fechas])
fechas<jupyter_output><empty_output><jupyter_text>Ahora aplicamos la función:<jupyter_code>valor_presente(date(2014,4,1), fechas, flujos, .0302)<jupyter_output><empty_output><jupyter_text>Función `valor_par` Para el cálculo del `valor_par` necesitamos la fecha valor, las fechas iniciales de los cupones y la tera.<jupyter_code>def valor_par(fecha_valor, fechas_iniciales, saldos_insolutos, tera):
    """
    Calcula el valor par en base 100 para un bono de renta fija local.
    
    Parameters
    ----------
    
    fecha_valor: datetime.date
        Fecha a la cual se quiere obtener el valor par.
        
    fechas_iniciales: List[datetime.date]
        Fechas de inicio de devengo de los flujos del bono.
        
    saldos_insolutos: List[float]
        Saldos insolutos de cada cupón del bono.
        
    tera: float
        Tasa efectiva real anual del bono según las convenciones de la Bolsa de Comercio de Santiago.
        
    Returns
    -------
    
    Un `float` que corresponde al valor par del bono.
    """
    fecha = mf.find_le(fechas_iniciales, fecha_valor)
    i = fechas_iniciales.index(fecha)
    saldo = saldos_insolutos[i]
    d = (fecha_valor - fecha).days
    return saldo * (1 + tera)**(d / 365.0)<jupyter_output><empty_output><jupyter_text>Probemos. La data que ahora estamos utilizando no está en el formato astuto de RiskAmerica que agrega un cupón 0 a la tabla de desarrollo. Vamos a tomar las fechas de la tabla de desarrollo y luego vamos a insertar al inicio la fecha de *emisión* desde la tabla de características.Para este ejemplo vamos a usar el nemotécnico BCCA-F0919.<jupyter_code>fechas_iniciales = tablas_desarrollo[tablas_desarrollo.nemotecnico == 'BCCA-F0919']['fecha_vcto_cupon'].values
fecha_emision = bonos[bonos.nemotecnico == 'BCCA-F0919']['fecha_emision'].iloc[0]
np.insert(fechas_iniciales, 0, [str(fecha_emision)])
fechas_iniciales = [datetime.datetime.strptime(f, "%Y-%m-%d").date() for f in fechas_iniciales]
fechas_iniciales
saldos_insolutos = list(tablas_desarrollo[tablas_desarrollo.nemotecnico == 'BCCA-F0919']['saldo_insoluto'].values)
tera = bonos[bonos.nemotecnico == 'BCCA-F0919']['tasa_descuento'].iloc[0] / 100.0
tera
valor_par(date(2019, 12, 31), fechas_iniciales, saldos_insolutos, tera)<jupyter_output><empty_output><jupyter_text>Primera Versión Con las definiciones anteriores estamos en condiciones de construir el valorizador, al menos la funcionalidad relacionada con el valor presente y valor par.Vamos a definir una función que aproveche la funcionalidad definida y abstraiga los detalles relacionados con la obtención de la data específica de un bono.**IMPORTANTE:** Esta función captura desde el contexto global los `DataFrame` `bonos` y `tablas`, evitando así tener que pasar estos argumentos a cada llamada de la función. Al finalizar veremos una manera más elegante y explícita de lograr el mismo efecto.<jupyter_code>def valorizador_rf(fecha_valor, nemotecnico, tir, monto):
    """
    Valoriza un instrumento de renta fija local a una fecha, a partir de su nemotécnico y la tir de mercado.
    
    Arguments
    ---------
    
    nemotecnico: str
        Es un nemotécnico válido de renta fija local.
        
    fecha_valor: datetime.date
        Fecha a la cual se requiere el cálculo.
        
    tir: float
        Tir de mercado del bono.
        
    monto: float
        Monto total del bono.
        
    Returns
    -------
    
    El precio, el valor par, el valor presente y el valor de pago del bono.
    
    """
    # Busca las características del bono
    caracteristicas = bonos[bonos.nemotecnico == nemotecnico]
    
    # Busca la tabla de desarrollo
    tabla = tablas_desarrollo[tablas_desarrollo.nemotecnico == nemotecnico]

    
    # Establece el valor de la TERA,
    tera = round(caracteristicas['tasa_descuento'].iloc[0] / 100.0, 6)
    
    # la fecha de inicio del primer cupón,
    primera_fecha = caracteristicas['fecha_emision'].iloc[0]
    primera_fecha = datetime.datetime.strptime(str(primera_fecha)[0:10], "%Y-%m-%d").date()
    
    # las fechas de pago de cupón
    fechas_pago = tabla['fecha_vcto_cupon'].values
    fechas_pago = [datetime.datetime.strptime(f, "%Y-%m-%d").date() for f in fechas_pago]
    
    # y los cupones.
    flujos = (tabla['interes'] + tabla['amortizacion']).values
    
    # Se calcula el valor presente
    vpresente = valor_presente(fecha_valor, fechas_pago, flujos, tir)
    
    # Se establecen las fechas de inicio de los cupones.
    np.insert(fechas_pago, 0, [primera_fecha])
    
    # Se establecen los saldos insolutos
    saldos_insolutos = list(tabla['saldo_insoluto'])
    
    # Se calcula el valor par.
    vpar = valor_par(fecha_valor, fechas_pago, saldos_insolutos, tera)
    
    # Se calcula el precio a 4 decimales
    precio = round(vpresente / vpar, 6)
    
    # Se calcula el valor a pagar en unidades monetarias.
    valor_pago = precio * vpar * monto / 100.0
    
    return {
        'precio': precio,
        'valor_par': vpar,
        'valor_presente': vpresente,
        'valor_pago': valor_pago
    }
valorizador_rf(date(2021, 8, 13), 'BWATT-Q', 0.0673, 1000)<jupyter_output><empty_output><jupyter_text>Duración y Convexidad ¿Qué es la duración de un bono? Típicamente la primera respuesta es *el promedio ponderado por el valor presente de los flujos de los plazos residuales de los flujos*. Una segunda forma, más robusta y que se extiende a otros contextos es pensar en la duración como la derivada primera del valor del bono respecto a la TIR de mercado.$$V\left(TIR\right)=\sum_{i=1}^N C_i\cdot \exp\left(-TIR\cdot yf_i\right)$$$$\frac{dV}{dTIR}=-\sum_{i=1}^N yf_i\cdot C_i\cdot \exp\left(-TIR\cdot yf_i\right)=-D\cdot V\left(TIR\right)$$En esta convención de tasa, la duración es, a menos del valor presente del bono y el signo, exactamente igual a la derivada del valor del bono. Si usamos la convención de la TIR de mercado de la renta fija local tenemos:$$V\left(TIR\right)=\sum_{i=1}^N C_i\cdot\left(1+TIR\right)^{-yf_i}$$$$\frac{dV}{dTIR}=-\frac{\sum_{i=1}^N yf_i\cdot C_i\cdot\left(1+TIR\right)^{-yf_i}}{1+TIR}=-D_M\cdot V\left(TIR\right)$$Vemos que, en esta convención de tasa, la derivada del valor del bono coincide con la duración modificada.Para la convexidad se puede hacer un razonamiento del todo análogo al anterior:$$\frac{d^2V}{dTIR^2}=\frac{\sum_{i=1}^N yf_i\cdot \left(yf_i+1\right)\cdot C_i\cdot\left(1+TIR\right)^{-yf_i}}{\left(1+TIR\right)^2}$$ Podríamos utilizar las últimas fórmulas y escribir funciones que calculen la duración y convexidad de un bono, pero vamos a adoptar un acercamiento al problema más extendible a situaciones futuras de mayor compeljidad.Vamos a utilizar la librería `autograd` que es capaz de calcular la derivada de una función. No es una aproximación por diferencias finitas, `autograd` calcula la derivada del código Python que define la función.<jupyter_code>dvdtir = grad(valor_presente, 3) # Derivada de la función valor_presente respecto a la TIR (3 variable)
type(dvdtir)
dur = -(1 + .0002) * dvdtir(date(2021,8,13), fechas, flujos, .0002)
dur /= valor_presente(date(2021,8,13), fechas, flujos, .0002)
dur
d2vdtir2 = grad(dvdtir, 3)
conv = d2vdtir2(date(2021,8,13), fechas, flujos, .0002)
conv /= valor_presente(date(2021,8,13), fechas, flujos, .0002) * (1 + .0002)**2
conv<jupyter_output><empty_output><jupyter_text>Dado lo anterior, podemos hacer una segunda versión de la función `valorizador_rf` que retorne también la duración y la convexidad.<jupyter_code>def valorizador_rf(fecha_valor, nemotecnico, tir, monto):
    """
    Valoriza un instrumento de renta fija local a una fecha, a partir de su nemotécnico y la tir de mercado.

    Arguments
    ---------

    nemotecnico: str
        Es un nemotécnico válido de renta fija local.

    fecha_valor: datetime.date
        Fecha a la cual se requiere el cálculo.

    tir: float
        Tir de mercado del bono.

    monto: float
        Monto total del bono.

    Returns
    -------

    El precio, el valor par, el valor presente, el valor de pago, la duración y la convexidad del bono.

    """
    # Busca las características del bono
    caracteristicas = bonos[bonos.nemotecnico == nemotecnico]

    # Busca la tabla de desarrollo
    tabla = tablas_desarrollo[tablas_desarrollo.nemotecnico == nemotecnico]

    # Establece el valor de la TERA,
    tera = round(caracteristicas['tasa_descuento'].iloc[0] / 100.0, 6)

    # la fecha de inicio del primer cupón,
    primera_fecha = caracteristicas['fecha_emision'].iloc[0]
    primera_fecha = datetime.datetime.strptime(
        str(primera_fecha)[0:10], "%Y-%m-%d").date()

    # las fechas de pago de cupón
    fechas_pago = tabla['fecha_vcto_cupon'].values
    fechas_pago = [datetime.datetime.strptime(
        f, "%Y-%m-%d").date() for f in fechas_pago]

    # y los cupones.
    flujos = (tabla['interes'] + tabla['amortizacion']).values

    # Se calcula el valor presente
    vpresente = valor_presente(fecha_valor, fechas_pago, flujos, tir)

    # Se calcula la duración.
    dvdtir = grad(valor_presente, 3)
    dur = -(1 + tir) * dvdtir(fecha_valor,
                              fechas_pago, flujos, tir) / vpresente

    # Se calcula la convexidad
    d2dvdtir2 = grad(dvdtir, 3)
    conv = d2dvdtir2(fecha_valor, fechas_pago, flujos, tir)
    conv /= vpresente

    # Se establecen las fechas de inicio de los cupones.
    np.insert(fechas_pago, 0, [primera_fecha])
    
    # Se establecen los saldos insolutos
    saldos_insolutos = list(tabla['saldo_insoluto'])

    # Se calcula el valor par.
    vpar = valor_par(fecha_valor, fechas_pago, saldos_insolutos, tera)

    # Se calcula el precio a 4 decimales
    precio = round(vpresente / vpar, 6)

    # Se calcula el valor a pagar en unidades monetarias.
    valor_pago = precio * vpar * monto / 100.0

    return {
        'precio': precio,
        'valor_par': vpar,
        'valor_presente': vpresente,
        'valor_pago': valor_pago,
        'duracion': dur,
        'convexidad': conv
    }<jupyter_output><empty_output><jupyter_text>Probemos con los mismos valores anteriores.<jupyter_code>valorizador_rf(date(2021, 8, 13), 'BWATT-Q', 0.0673, 1000000)<jupyter_output><empty_output><jupyter_text>Segunda Versión Sin captura global de la data, pero con la misma usabilidad.<jupyter_code>def valorizador_rf(fecha_valor, nemotecnico, tir, monto, bonos, tablas_desarrollo):
    """
    Valoriza un instrumento de renta fija local a una fecha, a partir de su nemotécnico y la tir de mercado.
    
    Arguments
    ---------
    
    nemotecnico: str
        Es un nemotécnico válido de renta fija local.
        
    fecha_valor: datetime.date
        Fecha a la cual se requiere el cálculo.
        
    tir: float
        Tir de mercado del bono.
        
    monto: float
        Monto total del bono.
        
    bonos: pandas.DataFrame
        Contiene la información de cabecera de los bonos. En particular tiene las columnas `nemotecnico`,
        `tasa_descuento` que representa la TERA del bono y `fecha_emision`.
        
    tablas_desarrollo: pandas.DataFrame
        Contiene las columnas `nemotecnico`, `interes`, `amortizacion` y `fecha_vcto_cupon`.
        
    Returns
    -------
    
    El precio, el valor par, el valor presente, el valor de pago, la duración y la convexidad del bono.
    
    """
    # Busca las características del bono
    caracteristicas = bonos[bonos.nemotecnico == nemotecnico]
    
    # Busca la tabla de desarrollo
    tabla = tablas_desarrollo[tablas_desarrollo.nemotecnico == nemotecnico]

    
    # Establece el valor de la TERA,
    tera = round(caracteristicas['tasa_descuento'].iloc[0] / 100.0, 6)
    
    # la fecha de inicio del primer cupón,
    primera_fecha = caracteristicas['fecha_emision'].iloc[0]
    primera_fecha = datetime.datetime.strptime(str(primera_fecha)[0:10], "%Y-%m-%d").date()
    
    # las fechas de pago de cupón
    fechas_pago = tabla['fecha_vcto_cupon'].values
    fechas_pago = [datetime.datetime.strptime(f, "%Y-%m-%d").date() for f in fechas_pago]
    
    # y los cupones.
    flujos = (tabla['interes'] + tabla['amortizacion']).values
    
    # Se calcula el valor presente
    vpresente = valor_presente(fecha_valor, fechas_pago, flujos, tir)
    
    # Se calcula la duración.
    dvdtir = grad(valor_presente, 3)
    dur = -(1 + tir) * dvdtir(fecha_valor, fechas_pago, flujos, tir) / vpresente
    
    # Se calcula la convexidad
    d2dvdtir2 = grad(dvdtir, 3)
    conv = d2dvdtir2(fecha_valor, fechas_pago, flujos, tir)
    conv /= vpresente 
    
    # Se establecen las fechas de inicio de los cupones.
    np.insert(fechas_pago, 0, [primera_fecha])
    
    # Se establecen los saldos insolutos
    saldos_insolutos = list(tabla['saldo_insoluto'])
    
    # Se calcula el valor par.
    vpar = valor_par(fecha_valor, fechas_pago, saldos_insolutos, tera)
    
    # Se calcula el precio a 4 decimales
    precio = round(vpresente / vpar, 6)
    
    # Se calcula el valor a pagar en unidades monetarias.
    valor_pago = precio * vpar * monto / 100.0
    
    return {
        'precio': precio,
        'valor_par': vpar,
        'valor_presente': vpresente,
        'valor_pago': valor_pago,
        'duracion': dur,
        'convexidad': conv
    }<jupyter_output><empty_output><jupyter_text>Est función hace explícita la dependencia de la data relacionada con los bonos y permite, por lo tanto, que sea reutilizable en otros contextos. Sin embargo, el código se hace más verboso si tenemos que pasar los `DataFrames` con la data cada vez que llamamos la función. Sin embargo, esto último se puede solucionar con la siguiente captura.<jupyter_code>import this
def get_valorizador_with_data(bonos, tablas_desarrollo):
    def wrapper(fecha_valor, nemotecnico, tir, monto):
        return valorizador_rf(
            fecha_valor,
            nemotecnico,
            tir, monto,
            bonos,
            tablas_desarrollo
        )
    return wrapper
val_rf = get_valorizador_with_data(bonos, tablas_desarrollo)
val_rf(date(2021, 8, 13), 'BWATT-Q', 0.0673, 1000000)<jupyter_output><empty_output><jupyter_text>Un Ejemplo Típico Como fijar el valor de una variable de una función.<jupyter_code>def suma(a, b):
    return a + b
def suma_x(x):
    def wrapper(a):
        return suma(a, x)
    return wrapper
suma2 = suma_x(2)
suma2(10)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09195593201572275, 'fraction_numerical': 0.015113768476997176, 'mean_word_length': 3.558163007822357, 'pattern_counts': {'":': 0, '<': 77, '<?xml version=': 0, '>': 78, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '259936', 'license': 'MIT', 'n_tokens_mistral': 6799, 'n_tokens_neox': 6573, 'n_words': 1826, 'path': '03-01_a_renta_fija.ipynb', 'repo_name': 'adiaz-efaa/intro-fin-python', 'size': '18063'}