<jupyter_start><jupyter_text>Valuación de opciones con Black-ScholesEste trabajo utiliza fórmulas para determinar:* el precio de una opción dada su volatilidad.* la volatilidad implícita en el precio de una opción.* las variables "griegas" de una opción: su delta, gamma, theta, rho y vega.Estas luego se aplican a una implementación basada en objetos.<jupyter_code>import numpy as np
import pandas as pd
import pandas_datareader as web
import datetime as dt
import os
import scipy.stats as scs<jupyter_output><empty_output><jupyter_text>Estos son los paquetes a utilizar.  En caso de no poseer alguno de ellos, estos pueden ser instalados por medio del comando  ! pip install {nombre_del_paquete}<jupyter_code>cd = 'C:\\Users\\Alfred\\PycharmProjects\\Clases_Programacion\\Programacion_Finanzas\\options_dfs\\'<jupyter_output><empty_output><jupyter_text>Solución al problema de valuación de un Call de Black y Scholes ${\Large\ C(S,t) = S.N(d1)-K.e\,^{( -rf\,.\,TTM)}.N(d2)}$      donde  ${\Large\ d1 = \frac {ln(\,^{S}/_{K}\,)+ (rf+^{\,\sigma^2}/_{2}\,)*TTM}{\sigma \sqrt {TTM}} -\frac{\sigma*\sqrt{TTM}}{2}}$    ${\Large\ d2 = d1 - \sigma \sqrt{TTM}}$Siendo    $ C =$ valor de un call  $ S =$ precio del subyacente  $ K = $ precio de ejercicio (strike)  $ TTM = $ tiempo hasta el vencimiento (en años)  $ \sigma = $ volatilidad (anual)  $ rf = $ tasa libre de riesgo (anual)  $ N() = $ función de distribución normal<jupyter_code>def BSM_call_price(p_last:float, strike:float, TTM:float, rf:float, vol:float):
    d1 = (np.log(p_last / strike) + (rf + 1/2 * vol ** 2) * TTM) / (vol * np.sqrt(TTM))
    d2 = (np.log(p_last / strike) + (rf - 1/2 * vol ** 2) * TTM) / (vol * np.sqrt(TTM))
            
    call_price = (p_last * scs.norm.cdf(d1, 0, 1) - strike * np.exp(-rf * TTM) * scs.norm.cdf(d2, 0, 1))
    
    return call_price, d1, d2<jupyter_output><empty_output><jupyter_text>Esta es la función que toma como inputs las variables de la acción y calcula su valor teórico según Black Scholes.<jupyter_code>def BSM_imp_vol(call_price:float, p_last:float, strike:float, TTM:float, rf:float):
    iv = 0.4
    iters = 1
    
    while iters < 1000:
        result, d1, d2 = BSM_call_price(p_last, strike, TTM, rf, iv)
        
        if result > call_price:
            iv = iv-0.001
            iters = iters+1
        
        elif result < call_price:
            iv = iv+0.001
            iters = iters+1
        
        else:
            break
            
    if (call_price - result > call_price*0.05):
        raise Warning
    
    return iv<jupyter_output><empty_output><jupyter_text>Esta función es la que calcula la volatilidad implícita de una opción, tomando el precio como dado.<jupyter_code>def call_delta(d1):
    return scs.norm.cdf(d1, 0, 1)
def call_gamma(p_last, TTM, vol, d1):
    return 1/np.sqrt(2*np.pi*np.exp((-(d1)**2)/2))/(p_last*vol*TTM)
def call_theta(p_last, strike, TTM, rf, vol, d1, d2):
    return (vol * p_last * (1/np.sqrt(2*np.pi)*np.exp((-(d1)**2)/2))/(2*np.sqrt(TTM)) - rf*strike*np.exp(-rf*TTM)*scs.norm.cdf(d2, 0, 1))/100
def call_vega(p_last: float, TTM, d1):
    return (p_last * (1/np.sqrt(2*np.pi)*np.exp((-(d1)**2)/2)) * np.sqrt(TTM))/100
def call_rho(strike, TTM, rf, d2):
    return TTM * strike * np.exp(-rf*TTM)* scs.norm.cdf(d2, 0, 1)
class Call:
    def __init__(self, ticker:str, strike:float, opex, rf:float, call_price=None, vol=None):
        self.underlying = ticker
        self.strike = strike
        self.opex = dt.datetime.strptime(opex, '%d/%m/%Y').date()
        self.rf = rf
        self.TTM = (self.opex - dt.date.today()).days/365.25
        
        # Descarga de datos históricos
        if not os.path.exists(cd+ticker+'.csv'):
            df = web.DataReader(ticker, 'yahoo', 
                                start= (dt.date.today()-dt.timedelta(366)),  
                                end=(dt.date.today()-dt.timedelta(1)))
            df.to_csv(cd+ticker+'.csv')
            self.p_underlying = df['Adj Close']
            
        else:
            df = pd.read_csv(cd+ticker+'.csv')
            self.p_underlying = df['Adj Close']
            
        
        self.returns = self.p_underlying/self.p_underlying.shift(1)
        self.p_last = self.p_underlying.iloc[-1]
        
        
        if (call_price == None) and (vol != None):
            # calculo de precio teórico según volatilidad dada
            self.vol = vol
            self.call_price, self.d1, self.d2 = BSM_call_price(self.p_last, self.strike, self.TTM, self.rf, self.vol)
            self.method = 'Black Scholes Fair Value from input vol'

        elif (call_price==None) and (vol==None):
            # calculo de precio teórico según volatilidad histórica
            self.vol = ((self.returns.std()* np.sqrt(252))**2) 
            self.call_price, self.d1, self.d2 = BSM_call_price(self.p_last, self.strike, self.TTM, self.rf, self.vol)
            self.method = 'Black Scholes Fair Value from 1 year vol'
        
        elif (call_price != None):
            # calculo de volatilidad implícita según precio dado
            self.call_price = call_price
            self.vol = BSM_imp_vol(self.call_price, self.p_last, self.strike, self.TTM, self.rf)
            self.method = 'Implied volatility from price'
            # Llamamos nuevamente a la función inversa que hicimos para obtener los valores de d1 y d2 que vamos a utilizar en el cálculo de las griegas
            _, self.d1, self.d2 = BSM_call_price(self.p_last, self.strike, self.TTM, self.rf, self.vol)

        else:
            raise AttributeError
       
    
    def calculate_greeks(self):
        self.delta = call_delta(self.d1)
        self.gamma = call_gamma(self.p_last, self.TTM, self.vol, self.d1)
        self.theta = call_theta(self.p_last, self.strike, self.TTM, self.rf, self.vol, self.d1, self.d2)        
        self.vega = call_vega(self.p_last, self.TTM, self.d1)
        self.rho = call_rho(self.strike, self.TTM, self.rf, self.d2)
        
    def attributes(self):
        aux_df = pd.DataFrame(index=['Ticker','Last Underlying price','Strike price','Expiry date',
                                     'Time to maturity', 'Risk free rate used', 
                                     'Call price', 'Volatility', 'Pricing method'])
        
        aux_df['attribute'] = [self.underlying, self.p_last, self.strike, self.opex,
                              self.TTM, self.rf, self.call_price, self.vol, self.method]
        
        try:
            greek_df = pd.DataFrame(index=['Delta', 'Gamma', 'Theta', 'Vega', 'Rho'])
            greek_df['attribute']=[self.delta, self.gamma, self.theta, self.vega, self.rho]
            aux_df = aux_df.append(greek_df)
            
            return aux_df.transpose()
            
            
        except:
            return aux_df.transpose()<jupyter_output><empty_output><jupyter_text>Esta es la clase del Objeto Call que vamos a utilizar.  El objetivo es que este objeto contenga los datos fijos del contrato: identificación del subyacente, precio de ejercicio, fecha de vencimiento y la tasa de interés a utilizar.      ***        Pricing y asignación de volatilidad  Ejemplo 1: volatilidad conocida Primero veremos el ejemplo típico teórico: tenemos una opción, conocemos su volatilidad esperada, y buscamos determinar el precio teórico segun el modelo.  Determinamos que esta será del orden del 50% anual<jupyter_code>GFGC160_OC = Call('GGAL.BA', 160, '16/10/2020', 0.2, vol=0.5)<jupyter_output><empty_output><jupyter_text>Buscamos conocer algunos atributos de la opción, como su tiempo hasta el vencimiento.<jupyter_code>GFGC160_OC.TTM
GFGC160_OC.attributes()<jupyter_output><empty_output><jupyter_text>Las funciones determinan el precio del call en approx $ \$2.5$.  Vemos que el método de precio es determinado por la volatilidad introducida.    ***      Ejemplo 2: Volatilidad históricaSi introducimos a la opción sin precio de opción ni volatilidad, el objeto estimará la volatilidad resultante del último año de operatoria, y utilizará este valor para el cálculo del precio de la opción.<jupyter_code>GFGC160_OC = Call('GGAL.BA',160, '16/10/2020', 0.2)

GFGC160_OC.attributes()<jupyter_output><empty_output><jupyter_text>Nuevamente observamos que el método de valuación resalta la fuente como la vol. histórica.  Vemos así que la volatilidad del último año es mayor a nuestro supuesto de 50% anual, lo que resulta en una valuación superior a la opción (approx el doble).  Dado que el único cambio en estas valuaciones corresponde a la volatilidad implícita (50% vs. 62%) podemos hacer una aproximación lineal por medio del vega ($ \nu $) de la opción.<jupyter_code>GFGC160_OC.calculate_greeks()

GFGC160_OC.attributes()<jupyter_output><empty_output><jupyter_text>El vega mide el cambio en el precio de la opción respecto al cambio de 1 punto de volatilidad.  Por lo tanto, una aproximación lineal sería de $(62\%-50\%)*0.1528 = 1.834$ de diferencia entre una opcion y la otra.  En cuanto a los calculos precisos, la diferencia entre uno y otro es de $4.1676-2.4854 = 1.6822$.Resulta una aproximación razonable, dentro de un 10% del valor real.  La diferencia se debe a que el valor de vega, va cambiando con el mismo.  Es decir $$\Large\ \nu = \frac{\partial C}{\partial \sigma}$$  Pero a su vez, este valor es variable con respecto al precio del subyacente, es decir, para una aproximación de segundo orden deberemos calcular el $vanna$ de la opción: $$ \large\ \frac{\partial \nu}{\partial S}= \frac{\partial^2 C}{\partial S \, \partial \sigma}$$      ***  Ejemplo 3: Volatilidad implícitaCuando introducimos un precio de opción sin incluir una volatilidad, el objeto interpreta que buscamos la volatilidad implícita. Es decir, este es el ejemplo converso al Ejemplo 1.<jupyter_code>GFGC160_OC = Call('GGAL.BA', 160, '16/10/2020', 0.2, call_price=2.48542)

GFGC160_OC.calculate_greeks()

GFGC160_OC.attributes()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11688442211055276, 'fraction_numerical': 0.02371859296482412, 'mean_word_length': 2.966121960940614, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 43, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '665781', 'license': 'MIT', 'n_tokens_mistral': 3696, 'n_tokens_neox': 3533, 'n_words': 962, 'path': 'Option_Calls_as_Objects_BSM.ipynb', 'repo_name': 'alfsn/derivatives', 'size': '9950'}