Neste projeto vou implementar o método de diferenças finitas explícito para precificar diferentes derivativos. Como o preço de opções geralmente é descrito por equações diferenciais de difusão ou parabólicas, o método de diferenças finitas se mostra adequado, uma vez que é utilizado justamente para encontrar soluções numéricas para equações diferencias. Depois de implementar o modelo, vou compará-lo com os resultados obtidos pela solução analítica de cada instrumento.
Nesta seção dou uma breve descrição sobre o método de diferenças finitas e declaro o problema que será abordado.
Como colocado por Wilmott, encontrar soluções fechadas para precificação de muitos derivativos pode ser difícil, ou mesmo inviável. Como o preço destes instrumentos frequentemente são descritos por equações diferencias parciais, frequentemente utilizam-se métodos numéricos para encontrar uma solução, como árvores binomiais, simulação de monte carlo e diferenças finitas.
O método de diferenças finitas encontra o valor de um derivativo calculando-o em todo domínio (factivel) de preços do instrumento base, incluíndo a passagem de tempo até seu vencimento. Assim, são similares às árvores binomiais. Porém, ao invéz e discretizar os preços do ativo e a passagem do tempo em uma estrutura de árvore, discretiza em um grid.
Diferenças finitas também são muitos boas para lidar com problemas com poucas dimensões e equações diferenciais não lineares (o preço e tempo já são duas, e o Wilmott sugere até quatro). Para muitas dimensões, a implementação começa a ficar complicada e pouco eficiente, coisa que o Monte Carlo lida melhor. Porém, se há exercício antecipado e não linearidade, o método de diferenças finitas acaba se mostrando como solução mais viável.
Considerando um ativo-objeto cuja dinâmica do preço satisfaz a seguinte EDE:
dStSt=μ⋅dt+σ⋅dWtDeseja-se calcular o preço justo de um derivativo um derivativo com característica europeia cujo payoff é descrito por uma função qualquer VT=V(T,ST), onde T é o vencimento do derivativo. ST é o preço do ativo-objeto em T e é possível negociar qualquer quantidade dele em qualquer instante. Não há custo de transação (corretagem, emolumento, bid-ask spread, etc) e posições vendidas a descoberto no subjacente são permitidas, não havendo custos associados.
Pede-se que se implemente o algoritmo de diferenças finitas explícito e se calcule o preço justo, o Delta e o Gamma de cada instrumento abaixo. Os valores encontrados devem ser comparados com o resultado de suas expressões analítica. A simulação deve ser feita para os payoffs abaixo. K é o Strike da opção.
Abaixo, apresentarei os elementos necessários para implementar o método diferenças finitas, sendo eles: a diferenciação das derivadas parciais necessárias usando o Grid; a discretização da condição final para cada derivativo; e suas respectivas condições de contorno (nos limites do grid).
De acordo com Wilmott, o grid utilizado pelo método de diferenças finitas tem passos de tempo e de preço (ou log do preço) geralmente homogênios. Porém, não há restrição para a forma do Grid, desde que os ajustes necessários sejam feitos. Como exposto por
Considerando que podemos discretizar t como t=T−kδt e do S como S=iδS, onde i e k são seus respectivos passos no grid, podemos escrever o valor da opção em cada ponto do grig como sendo:
Vki=(iδS,T−kδt)Seguindo notas de aula, aplicando Taylor ao preço V(T,ST) de um derivativo genérico, posso escrever a seguinte equação diferencial parabólica:
∂V∂t+a(S,t)⋅∂V2∂S2+b(S,t)⋅∂V∂S+c(S,t)⋅V=0Sendo que ∂V∂t também é chamado de theta (θ), ∂V∂S de delta (Δ) e ∂V2∂S2 de gamma (Γ). Como demostrado por Wilmott, como a definição da primeira derivada V em relação a t é dado por
∂V∂t=limh→0V(S,t+h)−V(S,t)hEntão podemos aproximar θ como sendo ∂V∂t(S,t)≈Vki−Vk+1iδt. Note que i, que é relacionado ao passo do ativo, ficou fixo. Também estou ignorando os erros de aproximação, como ignorarei em todas as outras diferenciações.
O mesmo raciocínio pode ser utilizado para aproximar o Δ. Porém, Wilmott ainda sugere que se utilize a diferença centrada, que é dada por ∂V∂S(S,t)≈Vki+1−Vki−12δS. A discretização anterior também poderia ser utilizada, mas esta oferece um erro de aproximação menor. O único problema desta abordagem é que preciso saber os valores S+δS e S−δS e nas fronteiras do Grid não terei esta informação. Por tanto, nestes casos utilizarei a discretrização utilizando apenas um dos lados.
Por fim, para achar o γ, Wilmott subtrai o delta forward do delta backward e divide o resultado por δS, chegando na aproximação ∂V2∂S2(S,t)≈Vki+1−2Vki+Vki−1δS2. Como demonstrado em notas de aula, todos estes resultados podem ser checados aplicando a expansão de Taylor.
O valor da opção no vencimento é simplemente seu payoff. Assim, não é necessário resolver nada para T, apenas para S. Usando a notação de diferenças finitas, os payoffs desejados ficam sendo da forma:
Como o Wilmott explica, o método de diferenças finitas começa de trás para frente, será destes valores que a iteração começará, como se estivéssemos calculando o preço de um derivativo por árvores binomiais, também de trás para frente.
Quando estivermos percorrendo o Grid, necessitaremos definir o preço do derivativo em seus extremos, quando S=0 e S=IδS, onde I é o ponto mais alto do Grid. Esta condição depende do instrumento que está sendo precificado. Para todos dos contratos, utilizaremos a seguinte condição para quando S=0:
Vk0=(1−rδt)Vk−10Para a condição de contorno superior, utilizaremos para a maioria dos contratos:
VkI=2VkI−1−VkI−2Esta condição é adequada pois, a medida que S→∞, o Γ da maioria dos contratos tende a zero. Porém, isso não é verdade para o contrato cujo payoff é descrito por V0i=(iδS−K)2. Diferenciando novamente o delta deste contrato, chegamos que o gamma será dado por 2⋅e(r+σ2)(T−t). Neste caso, partindo da equação discretizada do Γ e resolvendo para VkI, tenho que:
Γ=VkI+1−2VkI+VkI−1δS22⋅e(r+σ2)(δt)=VkI+1−2VkI+VkI−1δS2VkI=2δS2e(r+σ2)(δt)+2VkI−1−VkI−2VkI≈2δS2+2VkI−1−VkI−2Nesta seção vou decsrever o método de diferenças finitas explícito e implementar os códigos necessários.
Seguindo notas de aula, substituindo as diferenciações encontradas na equação parabólica mencionada anteriormente e reescrevendo os outros termos com a notação de diferenças finitas, temos que:
∂V∂t+a(S,t)⋅∂V2∂S2+b(S,t)⋅∂V∂S+c(S,t)⋅V=0Vki−Vk+1iδt+aki⋅Vki+1−2Vki+Vki−1δS2+bki⋅Vki+1−Vki−12δS+cki⋅Vki=0Rearranjando equação acima para isolar Vk+1i e renomeando alguns termos, ficamos com:
Vk+1i=AkiVki−1+(1+Bki)Vki+CkiVki+1Onde:
Aki=ν1aki−0.5ν2bkiBki=−2ν1aki−δtckiCki=ν1aki+0.5ν2bkiSendo que ν1=δtδS2 e ν2=δtδS. A equação acima está definida apenas entre i=1,...,I−1. OS pontos restantes necessários para discretização vem das condições de contorno. Como nós conhecemos o valor terminal em V0i, podemos calcular o valor de V1i e assim por diante. Como o valor do instrumento em k+1 só depende dos valores dele em k, chamamos este método de método explícito.
Para que a solução deste método seja estável, é necessário que satisfaça δt⩽δS22σ2S2 e δS⩽2a|b|. Vou utilizar a primeira delas explicitamente na implementação, porém vou fazer a seguinte transformação:
δt⩽δS22σ2S2=1σ2(δSS)2=1σ2I2Onde I=δS×S.
Devido a discretização do modelo, pode ser que seja necessário obter um valor que esteja entre os nós do Grid. Nestes casos, adotarei uma interpolação de duas dimensões (Preço e tempo) chamada interpolação bilinear. Ela consiste em realizar uma interpolação linear primeiro em uma direção e depois na outra. Considerando um quadrante específico do Grid, em que há quatro preços de opção, basicamente se divide o quadrante em quatro (onde os pontos de divisão são os valores desejados) e se utiliza a área de cada um deles como peso para os valores das opções. Assim, terminamos com a seguinte fórmula:
∑4i=1AiVi∑4i=1AiOnde A é a área dos retângulos e V o preço da opção em cada canto do quadrante principal. Cada Ai é oposto ao Vi correspondente (o A2 é oposto ao V2, por exemplo).
Primeiro, vou definir as classes para a criação do Grid. Como quero acessar os valores posteriormente, vou manter todos em uma estrutura, ainda que este provavelmente não seja o método mais adequado pensando em velocidade.
import finite_difference; reload(finite_difference);
x = finite_difference.Grid(f_vol=0.5,
f_value=100.,
f_time=1.,
i_nas=10,
i_nts=10)
print x
0 1 2 3 4 5 6 7 8 9 0 0,0 0,1 0,2 0,3 0,4 0,5 0,6 0,7 0,8 0,9 1 1,0 1,1 1,2 1,3 1,4 1,5 1,6 1,7 1,8 1,9 2 2,0 2,1 2,2 2,3 2,4 2,5 2,6 2,7 2,8 2,9 3 3,0 3,1 3,2 3,3 3,4 3,5 3,6 3,7 3,8 3,9 4 4,0 4,1 4,2 4,3 4,4 4,5 4,6 4,7 4,8 4,9 5 5,0 5,1 5,2 5,3 5,4 5,5 5,6 5,7 5,8 5,9 6 6,0 6,1 6,2 6,3 6,4 6,5 6,6 6,7 6,8 6,9 7 7,0 7,1 7,2 7,3 7,4 7,5 7,6 7,7 7,8 7,9 8 8,0 8,1 8,2 8,3 8,4 8,5 8,6 8,7 8,8 8,9 9 9,0 9,1 9,2 9,3 9,4 9,5 9,6 9,7 9,8 9,9 10 10,0 10,1 10,2 10,3 10,4 10,5 10,6 10,7 10,8 10,9
%timeit x = Grid(f_vol=0.2, f_value=100., f_time=1., i_nas=100)
1000 loops, best of 3: 344 µs per loop
Como comentei, não é uma estrtutura eficiente. Demorou quase 5 segundos para criar uma estrtutura de 10.000 nós. Porém acredito que isso me ajudará adiante.
Seguindo implementação do livro, também há outra opção para se encontrar Vk+1i. Substituindo a notação da fórmula de precificação e assumindo que possuimos os valores para Δ, Γ e Vki, podemos isolar o θ, ficando com
θ+aki⋅Γ+bki⋅Δ+cki⋅Vki=0−aki⋅Γ−bki⋅Δ−cki⋅Vki=θAssim, para uma opção que não paga dividendos, que o ativo base segue um movimento brawniano geométrico, e esta sendo avaliado no mundo neutro a risco, temos que o parâmetro c=−r, b=rS e a=0.5⋅σ2S2. Assim, fico com o theta sendo da forma:
θ=rVki−rSkiΔ−0.5σ2S2ΓEntão, o preço do derivativo em k+1 pode ser dado por:
Vk+1i=Vki−δtθPara comparar os resultados obtidos, também serão calculados os preços e as gregas de cada opção analiticamente, seguindo as equações de preficição demostradas em trabalho anterior. Abaixo vou imprimir os resultados obtidos para uma Call Européia para comparação com exemplos do Wilmott.
import finite_difference; reload(finite_difference);
d_param = {"f_St": 100., # preco do ativo
"f_sigma": 0.2, # desvio padra do ativo objeto
"f_time": 1., # tempo para vencimento em anos
"f_r": 0.05, # taxa de juros anual
"i_nas": 20, # passos que o ativo sera discretizado
"i_nts": 10, # passos que o tempo sera discretizado (opcional)
"f_K": 100. # strike da opcao
}
%time self = finite_difference.EuropianCall(**d_param)
Wall time: 582 ms
# imprime apenas a parte da matriz demonstrada no exemplo do livro, pag 1216, tabela 77.1
df = self.df_opt_prices.copy()
df.columns = ["{:.3f}".format(x) for x in df.columns]
df.tail(15).head(9)
0.000 | 0.111 | 0.222 | 0.333 | 0.444 | 0.556 | 0.667 | 0.778 | 0.889 | 1.000 | |
---|---|---|---|---|---|---|---|---|---|---|
60.0 | 0.0 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.001046 | 0.004769 | 0.012912 | 0.027040 | 0.048408 |
70.0 | 0.0 | 0.000000 | 0.000000 | 0.000000 | 0.010816 | 0.040306 | 0.092362 | 0.168004 | 0.266547 | 0.386414 |
80.0 | 0.0 | 0.000000 | 0.000000 | 0.084278 | 0.248619 | 0.475041 | 0.746832 | 1.051237 | 1.379044 | 1.723707 |
90.0 | 0.0 | 0.000000 | 0.512500 | 1.148000 | 1.807437 | 2.461115 | 3.100759 | 3.724696 | 4.333380 | 4.927851 |
100.0 | 0.0 | 2.500000 | 4.013889 | 5.200154 | 6.223712 | 7.149693 | 8.008182 | 8.816223 | 9.584443 | 10.320129 |
110.0 | 10.0 | 10.555556 | 11.571451 | 12.561620 | 13.500820 | 14.389205 | 15.235188 | 16.045095 | 16.824334 | 17.576942 |
120.0 | 20.0 | 20.555556 | 21.108025 | 21.790273 | 22.510062 | 23.243401 | 23.974649 | 24.698653 | 25.412183 | 26.114374 |
130.0 | 30.0 | 30.555556 | 31.108025 | 31.657425 | 32.248867 | 32.862026 | 33.491183 | 34.127231 | 34.766578 | 35.405282 |
140.0 | 40.0 | 40.555556 | 41.108025 | 41.657425 | 42.203772 | 42.764972 | 43.335178 | 43.915298 | 44.500544 | 45.090213 |
O título das colunas se refere ao tempo, começando no vencimento (tempo zero) até t (o dia em que a opção está sendo avaliada). As linhas se referem ao preço do ativo objeto. Note que começa a aparecer uma diferença em relação ao livro quando o ativo está valendo R$110.00 ou menos. Como o método utiliza os três nós subjacentes anteriores para calcular o valor do nó posterior, o nó (90.0;0.111) não deveria apresentar valor algum de fato. Porém, no exemplo do Wilmott, apresentou.
Nesta seção vou precificar alguns instrumentos e comparar com suas soluções analíticas.
Partindo de implementação anterior, apenas resta imeplementar o gamma analítico da equação, que é dado abaixo: ∂V2∂S2=Δ∂S=N′(d1)Sσ√t
Abaixo vou comparar os resultados analíticos com o que obtive do método de diferenças finitas.
import finite_difference; reload(finite_difference);
d_param = {"f_St": 100., # preco do ativo
"f_sigma": 0.5, # desvio padra do ativo objeto
"f_time": .5, # tempo para vencimento em anos
"f_r": 0.10, # taxa de juros anual
"i_nas": 20, # passos que o ativo sera discretizado
"f_K": 100. # strike da opcao
}
%time my_option = finite_difference.EuropianCall(**d_param)
CPU times: user 2.24 s, sys: 23.1 ms, total: 2.26 s Wall time: 2.28 s
# plotando graficos do inicio do periodo
l_prices = np.arange(50., 151., 1.)
my_option.compare_to_analytical_solutions(l_prices, d_param['f_time'])
Derivando gamma analítico da equação a paritr do Delta Δ=e−r⋅tS, tenho que:
∂V2∂S2=Δ∂S=−e−r⋅tS2Abaixo vou comparar os resultados analíticos com o que obtive do método de diferenças finitas. A única diferença que é necessária tomar nesta implementação é quando o valor do ativo no final é 0. Nestes casos, fiz o valor da opção também ser 0.
import finite_difference; reload(finite_difference);
d_param = {"f_St": 100., # preco do ativo
"f_sigma": 0.5, # desvio padra do ativo objeto
"f_time": .5, # tempo para vencimento em anos
"f_r": 0.10, # taxa de juros anual
"i_nas": 20, # passos que o ativo sera discretizado
"f_K": 100. # strike da opcao
}
%time my_option = finite_difference.LogContract(**d_param)
CPU times: user 1.83 s, sys: 11.1 ms, total: 1.84 s Wall time: 1.84 s
# plotando graficos do inicio do periodo
l_prices = np.arange(50., 151., 1.)
my_option.compare_to_analytical_solutions(l_prices, d_param['f_time'])
Derivando gamma analítico da equação e aplicando a regra de quociente, tenho que, se Δ=2e−r⋅tS[ln(S)+(r−σ22)⋅t]
Então
∂V2∂S2=Δ∂S=2e−r⋅t∂∂S[ln(S)+t(r−σ22)S]=2e−r⋅tS∂∂S(ln(S)+t(r−σ22))−(ln(S)+t(r−σ22))∂∂S(S)S2=2e−r⋅tS⋅1S−ln(S)−tr+tσ22S2=e−rtS2[2+σ2t−2ln(S)−2rt]Abaixo vou comparar os resultados analíticos com o que obtive do método de diferenças finitas.
import finite_difference; reload(finite_difference);
d_param = {"f_St": 100., # preco do ativo
"f_sigma": 0.5, # desvio padra do ativo objeto
"f_time": .5, # tempo para vencimento em anos
"f_r": 0.10, # taxa de juros anual
"i_nas": 20, # passos que o ativo sera discretizado
"f_K": 100. # strike da opcao
}
%time my_option = finite_difference.SquaredLogContract(**d_param)
CPU times: user 2.02 s, sys: 85.9 ms, total: 2.11 s Wall time: 2.09 s
# plotando graficos do inicio do periodo
l_prices = np.arange(50., 151., 1.)
my_option.compare_to_analytical_solutions(l_prices, d_param['f_time'])
Derivando gamma analítico da equação a partir de Δ=2Se(r+σ2)−2K, tenho que:
∂V2∂S2=2e(r+σ2)Neste caso, também preciso mudar as condições de contorno. Abaixo vou comparar os resultados analíticos com o que obtive do método de diferenças finitas.
import finite_difference; reload(finite_difference);
d_param = {"f_St": 100., # preco do ativo
"f_sigma": 0.5, # desvio padra do ativo objeto
"f_time": .5, # tempo para vencimento em anos
"f_r": 0.10, # taxa de juros anual
"i_nas": 20, # passos que o ativo sera discretizado
"f_K": 100. # strike da opcao
}
%time my_option = finite_difference.SquaredExotic(**d_param)
CPU times: user 1.82 s, sys: 34.3 ms, total: 1.86 s Wall time: 1.85 s
# plotando graficos do inicio do periodo
l_prices = np.arange(50., 151., 1.)
my_option.compare_to_analytical_solutions(l_prices, d_param['f_time'])
import finite_difference; reload(finite_difference);
d_param = {"f_St": 100., # preco do ativo
"f_sigma": 0.5, # desvio padra do ativo objeto
"f_time": .5, # tempo para vencimento em anos
"f_r": 0.10, # taxa de juros anual
"i_nas": 20, # passos que o ativo sera discretizado
"f_K": 100. # strike da opcao
}
%time my_option = finite_difference.DigitalOption(**d_param)
CPU times: user 2.46 s, sys: 92.1 ms, total: 2.55 s Wall time: 2.61 s
# plotando graficos do inicio do periodo
# plotando graficos do inicio do periodo
l_prices = np.arange(50., 151., 1.)
my_option.compare_to_analytical_solutions(l_prices, d_param['f_time'])
A solução não aderiu muito bem às soluções analíticas. Vou tentar melhorar a quantidade de discretizaçòes do ativo subjacente para tentar melhorar este resultado
import finite_difference; reload(finite_difference);
d_param = {"f_St": 100., # preco do ativo
"f_sigma": 0.5, # desvio padra do ativo objeto
"f_time": .5, # tempo para vencimento em anos
"f_r": 0.10, # taxa de juros anual
"i_nas": 100, # passos que o ativo sera discretizado
"f_K": 100. # strike da opcao
}
%time my_option = finite_difference.DigitalOption(**d_param)
CPU times: user 4min 26s, sys: 1.62 s, total: 4min 28s Wall time: 4min 32s
# plotando graficos do inicio do periodo
# plotando graficos do inicio do periodo
l_prices = np.arange(50., 151., 1.)
my_option.compare_to_analytical_solutions(l_prices, d_param['f_time'])
Curiosamente, a melhorar foi marginal. A implementação que fiz também se mostrou inadequada, uma vez que quando criei um grid com 100 passos no ativo, demorou 4 minutos e meio para realizar todos os cálculos.
Neste trabalho o método de diferenças finitas explícito foi comparado à precificação de instrumentos com solução analítica. De maneira geral, o método conseguiu aproximar bem o preço das opções e se mostrou bastante flexível, precisando de poucos ajustes para precificar cada instrumento. Apesar de não testado aqui, provavelmente a inclusão de não linearidades nos contratos também não exigiria muitos ajustes, como a utilização de parâmetros incertos, ou mesmo exercício antecipado.
A decisão de usar classes no lugar de um array dificultou a implementação. A aplicação do método criando um objeto para cada nó do Grid também se mostrou muito lenta. Porém, foi interessante notar que o simples aumento da discretização do grid não melhorou tanto a aderência aos valores calculados pelas soluções analíticas. Talvez a mudança do formato do grid mostrasse melhores resultados.
Style notebook and change matplotlib defaults
#loading style sheet
from IPython.core.display import HTML
HTML(open('ipython_style.css').read())
#changing matplotlib defaults
%matplotlib inline
import seaborn as sns
sns.set_palette("deep", desat=.6)
sns.set_context(rc={"figure.figsize": (8, 4)})
sns.set_style("whitegrid")
sns.set_palette(sns.color_palette("Set2", 10))