# PUF model and probability distributions

We use the temperature-dependent model for SRAM-PUF as defined by Roel Maes.
\begin{align*}
r_i^{(j)}=\left\{\begin{matrix}
0 , &\text{if } m_i+d_i\cdot t^{(j)} +w\leq \tau,\\
1 , &\text{if } m_i+d_i \cdot t^{(j)} +w> \tau.
\end{matrix}\right.
\end{align*}
With process variable $m_i \sim \mathcal{N}(\mu_M,\sigma_M^2)$, temperature dependence variable $d_i \sim \mathcal{N}(0,\sigma_D^2)$, noise variable $w_i^{(j)} \sim \mathcal{N}(0,\sigma_N^2)$, and temperature $t^{(j)}$ at time $j$.
Roel also defines some parameters $\lambda_1 = \sigma_N/\sigma_M$, $\lambda_2 = (\tau-\mu_M)/\sigma_M$, and $\theta= \sigma_N / \sigma_D $.

We refer to the one-probability of cell $i$ as $\xi_i$ and in case of known state-variables, this is given as $\xi_i = \Pr(X=1) = 1- cdf_N(\tau - m_i-d_i \cdot t^{(j)} )=\Phi(\frac{-\tau + m_i + d_i \cdot t^{(j)} }{\sigma_N}) $.

It is our goal to estimate $\lambda_1,\lambda_2,\theta$ for our dataset.

We will use maximum-likelihood estimation to achieve this.

## Settings of parameters

```python
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
import srampufparest
# set the parameters
lambda12 = [0.1212,-0.0005] # settings from Roel: 0.1213,0.021,45
theta = 45;
```

# verify srampufparest.loadUnique()
Does it give the same histogram as the code that I used before ?

```python
Nsamples = 60
Ndevices = 96
datapath = r'C:\Users\lkusters\surfdrive\python\server_008104\20181125\Unique'
folder = datapath+'\\002_Temp_025'
_,merged2 = srampufparest.loadUnique(folder)
import os
data25 = [[] for i in range(Nsamples)]
for ii in range(Ndevices):
    count = 0;
    for fname in os.listdir(folder):
        if fname.startswith("Unique_dev%03d"%(ii+1) ):
            data =  np.fromfile(folder+'\\'+fname, np.uint8)
            data_bits = np.unpackbits(data)
            data25[count].extend(data_bits)
            count += 1;
data25 = np.swapaxes(data25,0,1) # swap axes, s.t. 1st dim is cells, and second is observations
folder = datapath+'\\001_Temp_m40'
_,merged1 = srampufparest.loadUnique(folder)
```

    Finished loading data for 96 devices, with 60 observations of 65536 cells
    returning observations [96,60,65536] and merged observations [6291456,60]
    Finished loading data for 96 devices, with 40 observations of 65536 cells
    returning observations [96,40,65536] and merged observations [6291456,40]

```python
mse = 0
for rowd,rowm in zip(data25,merged2):
    # compare the rows
    mse = mse+sum([(i-j)**2 for i,j in zip(rowd,rowm)])
print('load data: MSE(old method,new method) = %0.4f'%mse)
```

    0

```python
# from srampufparest
from numpy.random import uniform
print(len(merged2))
N25 = [len(d) for d in merged2] # number of measurements per cell
sums25 = [sum(n) for n in merged2 ]
print(set(N25))
# now generate errorcount
errorcount25 = [sum25 if uniform(0,1,1)>(sum25/Nsamples) else Nsamples-sum25 for sum25 in sums25]
```

    6291456
    {60}

```python
# original method
from numpy.random import uniform
print(len(data25))
N25 = [len(d) for d in data25] # number of measurements per cell
sums25 = [sum(n) for n in data25 ]
print(set(N25))
# now generate errorcount
errorcount25 = [sum25 if uniform(0,1,1)>(sum25/Nsamples) else Nsamples-sum25 for sum25 in sums25]
```

    6291456
    {60}

```python
# ORIGINAL
# plot the histogram of the number of observed ones in each cell
from scipy.special import comb
pmfdata_sum25,edges = np.histogram(sums25,bins=Nsamples+1,range=(-.5,Nsamples+.5))
plt.plot(range(Nsamples+1),[count/len(data25) for count in pmfdata_sum25] ,'o', fillstyle='none')
plt.yscale('log')
plt.xlabel('k Number of ones for total %d measurements'%(Nsamples))
plt.ylabel('#cells with Number of ones = k ')
plt.title('$T=25^oC$')

# Functions srampuf
pmfhist,centers = srampufparest.getcounts1D(merged2)
plt.plot(centers,[count/len(merged1) for count in pmfhist] )

plt.show()
```

```python
mse = 0
mse = mse+sum([(i-j)**2 for i,j in zip(pmfdata_sum25,pmfhist)])
print('find histogram k-ones: MSE(old method,new method) = %0.4f'%mse)
```

    0

# Verify fitting method

# Pdf of one-probability (no temperature)

We find that apparently the approximations by python (in integrator?) are not accurate enough, since the pdf does not sum to 1 (but instead 0.38). Therefore, we instead use the equations to approximate the cdf and then calculate the pdf by integrating (sum over small intervals).

## Pdf of the one-probability
\begin{equation}
        p_0(\xi|\lambda_1,\lambda_2)
        = \frac{\lambda_1 \cdot \phi( \Phi^{-1}(\xi) \cdot \lambda_1  +\lambda_2) }{\phi(\Phi^{-1}(\xi))}
\end{equation}

First, we show that indeed the total probability does not sum to 1:

## Cdf of the one-probability

The PDF can be approximated as
\begin{equation}
        p_0(\xi|\lambda_1,\lambda_2)
        = \Pr(\Xi \leq \xi + \Delta\xi) - \Pr(\Xi \leq \xi - \Delta\xi) ,
\end{equation}
with
\begin{equation}
        \Pr(\Xi \leq \xi)
        = \Phi( \Phi^{-1}(\xi) \cdot \lambda_1  +\lambda_2)
\end{equation}

Second show that we can use cdf and integrate to improve the approximation

```python
# First plot the pdf
l1 = lambda12[0]
l2 = lambda12[1]
NX = 1000 # number of steps

xx = [i for i in np.linspace(0,1,NX)]
yy= [norm.cdf(l1*norm.ppf(xi)+l2) for xi in xx]
pdf_one = np.diff(yy)
pdf_x = [x+0.5/NX for x in xx[:-1]]
plt.plot(pdf_x,pdf_one,'o', fillstyle='none')

NX = 1000 # number of steps

p0_p, p0_xi = srampufparest.pdfp0(l1,l2,NX)

plt.plot(p0_xi,p0_p)
plt.yscale('log')
plt.xlabel('Pr(X=1)=xi')
plt.ylabel('Pr(Xi=xi)')
plt.show()
pred = sum(p0_p) # should be 1
print(pred, 'should be equal to ONE ')
```

```python
# Then verify that we can reproduce the histogram of the one counts
# plot the histogram of the number of observed ones in each cell
from scipy.special import comb
pmfhist,centers = srampufparest.getcounts1D(merged2)
plt.plot(centers,[count/len(merged1) for count in pmfhist] ,'o', fillstyle='none')
plt.yscale('log')
plt.xlabel('k Number of ones for total %d measurements'%(Nsamples))
plt.ylabel('#cells with Number of ones = k ')
plt.title('$T=25^oC$')
# Now the fit
l1 = lambda12[0]
l2 = lambda12[1]
NX = 10000 # number of steps

pmf_pred, pmf_pred_xi = srampufparest.pmfpkones(NX, l1, l2, 60)

plt.plot(pmf_pred_xi,pmf_pred)
plt.legend(['empirical','prediction'])
plt.show()
pred = sum(p0_p) # should be 1
print('Area under curve = %0.6f (should be equal to 1) '%pred)
print('\lambda_1 = %0.4f, \lambda_2 = %0.4f'%(l1,l2))

pmf_emp = [count/len(merged1) for count in pmfhist]
MSE = sum([(x-y)**2 for (x,y) in zip(pmf_emp,pmf_pred)])
print('pdf of k-ones: MSE(empricial,predicted) = %0.6f'%MSE)
```

# Pdf of one-probability (with temperature)

## Pdf of the one-probability
\begin{equation}
        p_1(\xi_2|\xi_1,t_1,t_2,\lambda_1,\lambda_2,\theta)
        = \phi \left(\frac{\theta}{t_2-t_1} \cdot (\Phi^{-1}(\xi_{t_2})-\Phi^{-1}(\xi_{t_1})) \right)   \cdot \frac{\theta}{t_2-t_1}  \cdot  \frac{1 }{\phi(\Phi^{-1}(\xi_{t_2} )  ) }
\end{equation}

First, we show that indeed the total probability does not sum to 1:

## Cdf of the one-probability

The PDF can be approximated as
\begin{equation}
        p_1(\xi_2|\xi_1,t_1,t_2,\lambda_1,\lambda_2,\theta)
        = \Pr(\Xi_{t_2} \leq \xi_{t_2}+ \Delta\xi|\Xi_{t_1}=\xi_{t_1})  - \Pr(\Xi_{t_2} \leq \xi_{t_2}- \Delta\xi|\Xi_{t_1}=\xi_{t_1}) ,
\end{equation}
with
\begin{equation}
        \Pr(\Xi_{t_2} \leq \xi_{t_2}|\Xi_{t_1}=\xi_{t_1})
        = \Phi \left(\frac{\theta}{t_2-t_1} \cdot (\Phi^{-1}(\xi_{t_2})-\Phi^{-1}(\xi_{t_1})) \right)
\end{equation}

We show that we can use cdf and integrate to improve the approximation

```python
# Now the temperature dependent behavior
# settings from Roel: 0.12,0.02,45
l1 = 0.12
l2 = 0.02
theta = 45
NX = 1000 # number of steps

idx = srampufparest.getcellskones(merged2,5); # select cells that observed 5 ones in T2
merged1_sel = [merged1[i] for i in idx] # find observations of these cells at T1
hist,centerpoints = srampufparest.getcounts1D(merged1_sel); # generate the histogram

p1 = 5/60 # one probability
L = 40 # number of observations at T1
dT = 65 # temperature difference between T1 and T2
predict,x = srampufparest.pmfpkones_temperature(NX,dT,l1,l2,theta,p1,L); # predict the histogram

plt.plot(x,predict)
plt.plot(centerpoints,[h/sum(hist) for h in hist],'o', fillstyle='none') # actual values
plt.yscale('log')
plt.xlabel('k Number of ones for total %d measurements'%(Nsamples))
plt.ylabel('#cells with Number of ones = k ')
plt.show()

# REPEAT FOR DIFFERENT init
p1 = 10/60 # one probability
predict,x = srampufparest.pmfpkones_temperature(NX,dT,l1,l2,theta,p1,L); # predict the histogram

plt.plot(x,predict)
plt.plot(centerpoints,[h/sum(hist) for h in hist],'o', fillstyle='none') # actual values
plt.yscale('log')
plt.xlabel('k Number of ones for total %d measurements'%(Nsamples))
plt.ylabel('#cells with Number of ones = k ')
plt.show()
```

```python

```

```python
# use the CDF
l1 = lambda12[0]
l2 = lambda12[1]
th = theta
NX = 100 # number of steps

dT = 25 # temperature difference

# first we choose single value for pi
pi = 0.1 # xit1
p1_p, p1_xi = srampufparest.pdfp1(pi,dT,l1,l2,th,NX)
plt.plot(p1_xi,p1_p)
plt.yscale('log')
plt.xlabel('Pr(X=1)=xi')
plt.ylabel('Pr(Xi=xi|pi=%0.2f,dT=%d)'%(pi,dT))
plt.show()
pred = sum(p1_p)
xx[0]

print(pred, 'should be equal to ONE ')

# second we integrate over all possible pi, to get the average distribution
# NOT IMPLEMENTED YET FOR NEW METHOD
# we re-use p0_p and p0_xi
ytotal = [ sum( [ Pdfpi*norm.cdf( (th/dT) * (norm.ppf(xi)-norm.ppf(pi)) )  for (Pdfpi,pi) in zip(p0_p,p0_xi) ])   for xi in xx]
ytotal = np.diff(ytotal)
ytotalx = [x+0.5/NX for x in xx[:-1]]

plt.plot(ytotalx,ytotal)
plt.yscale('log')
plt.xlabel('Pr(X=1)=xi')
plt.ylabel('Pr(Xi=xi|dT=%d)'%dT)
plt.show()
pred = sum(ytotal)

print(pred, 'should be equal to ONE ')

```

# Compare the ML estimation

Compare old ML estimation method with new method from srampufparest module.

```python
# from the old method
import numpy as np
from scipy.stats import norm
from scipy.special import comb
def calculatePmf_kones_noT(l12,nsamples,NX): # USED for ML estimation without temperature
    # pmf of sum(x)
    # parameters l12 = lambda1, lambda2
    # nsamples is total number of samples
    # out: probability distribution of k (# ones)
    #NX precision of the integrator
    pmf_ones = [];
    l1 = l12[0];l2=l12[1];
    # now the integrator
    xx = [i for i in np.linspace(0,1,NX)]
    yy= [norm.cdf(l1*norm.ppf(xi)+l2) for xi in xx]
    pdfpx = [xi+0.5/NX for xi in xx[:-1]]
    pdfpy = np.diff(yy)

    for nones in range(nsamples+1): # loop over number of ones
        pred = [(xi**nones)*((1-xi)**(nsamples-nones))*pdfp for (xi,pdfp) in zip(pdfpx,pdfpy) ]
        pmf_ones.append(sum(pred))
    return pmf_ones
```

```python
# load the data
import srampufparest
datapath = r'C:\Users\lkusters\surfdrive\python\server_008104\20181125\Unique'
folder = datapath+'\\002_Temp_025'
_,merged2 = srampufparest.loadUnique(folder)
pmfhist,centers = srampufparest.getcounts1D(merged2)
```

    Finished loading data for 96 devices, with 60 observations of 65536 cells
    returning observations [96,60,65536] and merged observations [6291456,60]
    Finished generating histogram, with 60 max observations and 6291456 total cells

```python
#Make the grid
import numpy as np
nl1 = 18;
nl2 = 61;
ll1 = np.linspace(0.118,0.125,nl1);
ll2 = np.linspace(-0.01,0.005,nl2);

Nsamples = 60;
NX = 10000;
```

```python
LL_old = [[0  for j in range(len(ll2))] for i in range(nl1)]
LL_new = [[0  for j in range(len(ll2))] for i in range(nl1)]
i=0;
for i in range(len(ll1)):
    print(i)
    l1 = ll1[i]
    for j in range(len(ll2)):
        l2 = ll2[j];
        LLloop_old = 0;
        p_k = calculatePmf_kones_noT([l1,l2],Nsamples,NX)
        for (pk,nk) in zip(p_k,pmfhist):
            LLloop_old += nk*np.log10(pk)
        LL_old[i][j] = LLloop_old;
        LL_new[i][j] = srampufparest.loglikelihood(pmfhist,centers,NX, l1, l2)
```

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

```python
import matplotlib.pyplot as plt
legenda = ()
for i in range(0,len(ll1),2):
    plt.plot(ll2,LL_old[i]);
    legenda = legenda + (str(ll1[i]),);

plt.ylabel('Log-Likelihood')
plt.xlabel('$\lambda_2$')
plt.title('LL (old) for various $\lambda_1$')
plt.legend(legenda)
plt.show()
```

```python
import matplotlib.pyplot as plt
legenda = ()
for i in range(0,len(ll1),2):
    plt.plot(ll2,LL_new[i]);
    legenda = legenda + (str(ll1[i]),);

plt.ylabel('Log-Likelihood')
plt.xlabel('$\lambda_2$')
plt.title('LL (new) for various $\lambda_1$')
plt.legend(legenda)
plt.show()
```

```python
l1 = 0.12
l2 = -0.001
LLloop_old = 0;
p_k = calculatePmf_kones_noT([l1,l2],Nsamples,NX)
for (pk,nk) in zip(p_k,pmfhist):
    LLloop_old += nk*np.log10(pk)
compare = srampufparest.loglikelihood(pmfhist,centers,NX, l1, l2)
print(compare)
print(LLloop_old)
```

    -18131839.446644746
    -18131839.446644746

```python
#Make the grid
import numpy as np
nl1 = 6;
nl2 = 21;
ll1 = np.linspace(0.118,0.122,nl1);
ll2 = np.linspace(-0.005,0.005,nl2);

Nsamples = 60;
NX = 10000;
```

```python
LL_old2 = [[0  for j in range(len(ll2))] for i in range(nl1)]
LL_new2 = [[0  for j in range(len(ll2))] for i in range(nl1)]
i=0;
for i in range(len(ll1)):
    print(i)
    l1 = ll1[i]
    for j in range(len(ll2)):
        l2 = ll2[j];
        LLloop_old = 0;
        p_k = calculatePmf_kones_noT([l1,l2],Nsamples,NX)
        for (pk,nk) in zip(p_k,pmfhist):
            LLloop_old += nk*np.log10(pk)
        LL_old2[i][j] = LLloop_old;
        LL_new2[i][j] = srampufparest.loglikelihood(pmfhist,centers,NX, l1, l2)
```

    0
    1
    2
    3
    4
    5

```python
import matplotlib.pyplot as plt
legenda = ()
for i in range(0,len(ll1)):
    plt.plot(ll2,LL_old2[i]);
    legenda = legenda + (str(ll1[i]),);

plt.ylabel('Log-Likelihood')
plt.xlabel('$\lambda_2$')
plt.title('LL (old) for various $\lambda_1$')
plt.legend(legenda)
plt.show()
```

```python
import matplotlib.pyplot as plt
legenda = ()
for i in range(0,len(ll1)):
    plt.plot(ll2,LL_new2[i]);
    legenda = legenda + (str(ll1[i]),);

plt.ylabel('Log-Likelihood')
plt.xlabel('$\lambda_2$')
plt.title('LL (old) for various $\lambda_1$')
plt.legend(legenda)
plt.show()
```

```python
LL_new3 = srampufparest.looploglikelihood(pmfhist,centers,NX, ll1, ll2)
```

```python
print('pdf of k-ones: MSE(empricial,predicted) = %0.6f'%MSE)
```

    pdf of k-ones: MSE(empricial,predicted) = 0.000003

```python

```


# 1. Estatísticas de Ordem

Nesta aula, estudaremos assuntos relacionados à temática de estatísticas de ordem.

## 1.1. Problema de Seleção

Problemas de **seleção** são comumente encontrados em nosso cotidiano. Suponha que estejamos interessados em determinar o $i$-ésimo menor elemento de um conjunto $A$ com $n$ elementos. Como alguns casos particulares importantes, podemos citar:
* Menor elemento: $i=1$
* Maior elemento: $i=n$

Para obter o **menor** elemento de um conjunto de dados, podemos considerar a seguinte implementação:

```python
import numpy
global counter
```

```python
def Min(A):

    n = len(A)
    min = A[0]

    for i in range(1,n):
        if(A[i] < min):
            min = A[i]

    return min
```

No algoritmo acima, o temos $n-1$ comparações, ou seja, encontrar o menor elemento em um conjunto de dados quaisquer possui complexidade de tempo de $\theta(n)$. Exemplo de funcionamento:

```python
A = numpy.random.randint(0, 20, 20)
print('Vetor de entrada: '+ str(A))
print('Menor elemento: '+ str(Min(A)))
```

    Vetor de entrada: [16  1 15 14 12 19 13  5 10  7 12  9 10 16  2  4 16  4  8 14]
    20
    Menor elemento: 1

Suponha, agora, que desejemos encontrar o **menor** e o **maior** valor de um conjunto de dados quaisquer $A$ com $n$ elementos. Podemos considerar a seguinte implementação:

```python
def Min_Max(A):

    n = len(A)
    min = max = A[0]
    counter = 0 # armazena o número de comparações

    for i in range(1, n):

        counter = counter + 1
        if(A[i] < min):
            min = A[i]

        counter = counter + 1
        if(A[i] > max):
            max = A[i]

    return min,max,counter
```

No algoritmo acima, temos $2(n-1)\in\theta(n)$ comparações, ou seja, encontrar o menor e o maior elemento em um conjunto de dados quaisquer possui complexidade de tempo de $\theta(n)$. Exemplo de funcionamento:

```python
A = numpy.random.randint(0, 20, 20)
print('Vetor de entrada: '+ str(A))
min, max, counter = Min_Max(A)
print('Menor elemento: '+ str(min))
print('Maior elemento: '+ str(max))
print('Número de elementos: '+ str(len(A)))
print('Número de comparações: '+ str(counter))
```

    Vetor de entrada: [ 6 11  1  2  3  5 10 19  2  7 17 18 16 12  5  9  0  4 17  9]
    Menor elemento: 0
    Maior elemento: 19
    Número de elementos: 20
    Número de comparações: 38

```python
def Min_Max_Melhorado(A):

    n = len(A)
    counter = 0 # armazena o número de compações

    if(n % 2 == 1):
        min = max = A[0]
        i = 1
    else:
        i = 2
        if(A[0] < A[1]):
            min = A[0]
            max = A[1]
        else:
            min = A[1]
            max = A[0]

    while (i+1 < n):

        counter = counter + 1
        if(A[i] > A[i+1]):
            counter = counter + 1
            if(A[i] > max):
                max = A[i]
        else:
            counter = counter + 1
            if(A[i] < min):
                min = A[i]

        i = i+2

    return min,max,counter
```

Entretanto, podemos projetar uma versão mais rápida do algoritmo acima, na qual processamos os elementos por **pares**. A ideia consiste em comparar o menor atual com o menor elemento do par, bem como o maior valor atual com o maior elemento do par. Quando $n$ é **ímpar**, inicializamos o menor e maior valores como sendo o primeiro elemento do conjunto de dados. Caso contrário, inicializamos o máximo e mínimo comparando os dois primeiros elementos do conjunto de dados.

Nesta versão melhorada, realizamos $2n/2-2=n-2\in\theta(n)$ comparações, ou seja, como o índice $i$ é incrementado em duas unidades, o laço executa $n/2$ vezes, sendo que temos $2$ comparações em cada iteração do mesmo. Desta forma, perfazemos $2n/2$ comparações. Como o primeiro par, em geral, já foi comparado antes da execução do laço, subamtraímos duas unidades do número de comparações anterior, ou seja, $2n/2-2 = n-2$. Muito embora ambas versões possuam a mesma complexidade nominal, o algoritmo `Min_Max_Melhorado` realiza um número menor de comparações na prática.

```python
A = numpy.random.randint(0, 20, 20)
print('Vetor de entrada: '+ str(A))
min, max, counter = Min_Max_Melhorado(A)
print('Menor elemento: '+ str(min))
print('Maior elemento: '+ str(max))
print('Número de elementos: '+ str(len(A)))
print('Número de comparações: '+ str(counter))
```

    Vetor de entrada: [14 19  8  4  6 18 17 16  0  4  5 17 12 12  4  9 18  9 11  3]
    Menor elemento: 0
    Maior elemento: 19
    Número de elementos: 20
    Número de comparações: 18

Suponha, agora, que desejemos encontrar o $i$-ésimo menor elemento do vetor $A$. Uma solução simples seria a seguinte:

1. Ordene o vetor A.
2. Retorne A[i]

A complexidade desta solução depende, então, da complexidade do algoritmo de ordenação utilizado no passo 1. Opções boas são o Mergesort e o Heapsort, os quais possuem complexidade de tempo de $\theta(n\log n)$. Como o passo 2 possui complexidade $\theta(1)$, então a complexidade de tempo final da ideia acima é de $\theta(n\log n)$.

No entanto, existem outras soluções que melhoram essa complexidade. Retomemos ao **problema da partição**, em que a ideia é: dado um vetor $A[i\ldots f]$, devolver um índice $p$ tal que $A[e\ldots p-1]\leq A[p]\leq A[p+1\ldots d]$. A Figura 1 ilustra essa situação.

Figura 1: Problema de partição.

O algoritmo abaixo implementação a função de partição que foi utilizada na técnica Quicksort.

```python
def Partition(A, e, d):
    pivot = A[d]
    p_index = e

    for i in range(e,d):
        if (A[i] <= pivot):
            A[i], A[p_index] = A[p_index], A[i]
            p_index = p_index + 1

    A[p_index], A[d] = A[d], A[p_index]

    return p_index

n = 10 # número de elementos do vetor A
A = numpy.random.randint(-20, 20, n)
print('Vetor de entrada: '+ str(A))
p = Partition(A, 0, n-1)
print('Vetor de saída: '+ str(A))
print('Posição do pivô: '+ str(p))
```

    Vetor de entrada: [-20  -4   5 -13   0   4 -20  15  15  19]
    Vetor de saída: [-20  -4   5 -13   0   4 -20  15  15  19]
    Posição do pivô: 9

Note que o algoritmo acima possui um único laço que roda $\theta(n)$ vezes, como vimos na aula de Quicksort. Podemos, então, utilizá-lo para resolver o problema de selecionar o $i$-ésimo elemento de um vetor de elementos quaisquer. Desta forma, temos as seguintes situações após a execução da função `Partition` acima:
1. Se $p=i$, então problema resolvido!
2. Se $i
p$, então a solução do problema está em encontrar o $(i-p)$-ésimo menor elemento em $A[p+1\ldots d]$.

Para os casos descritos nos itens 2 e 3, basta executar o algoritmo de `Partition` recursivamente até o índice desejado for atingido, conforme implementado abaixo.

```python
def Select(A, e, d, i):
    global counter

    if(e == d):
        return A[e]

    p = Partition(A, e, d)

    k = p-e+1 # p está na k-ésima posição de A
    if(k == i):
        return A[p]
    elif(i < k):
        counter = counter + 1
        return Select(A, e, p-1, i)
    else:
        counter = counter + 1
        return Select(A, p+1, d, i-k)

```

Exemplo de funcionamento:

```python
counter = 1 # conta quantas chamadas recursivas foram realizadas
n = 10 # número de elementos do vetor A
i = 3 # posição do i-ésimo menor elemento
A = numpy.random.randint(-20, 20, n)
print('Vetor de entrada: '+ str(A))
x = Select(A, 0, n-1, i)
print('Elemento desejado: '+ str(x))
```

    Vetor de entrada: [ -1   8   0 -13   9 -10  19  11   8  19]
    Elemento desejado: -1

A função `Select` possui uma chamada à função `Partition` que, como descrito anteriormente, possui complexidade de tempo de $\theta(n)$. Em seguida, caso a execução do algoritmo continue na condição $i<k$, temos que a função `Select` pode ser chamada $O(k-1)$ vezes, que seria o **tamanho do lado esquerdo** do vetor, ou seja, o lado que possui elementos menores ou iguais ao pivô. Caso contrário, a função `Select` poderia ser chamada $O(n-k)$ vezes, que corresponde ao **tamanho do lado direito** do vetor, ou seja, o lado que possui elementos maiores ou iguais ao pivô.

Assim sendo, a complexidade de tempo da função `Select` corresponde ao pior dos casos entre $O(k-1)$ e $O(n-k)$, ou seja, temos a seguinte relação de recorrência:

\begin{equation}
\label{e.recurrence_select_i}
\tag{1}
T(n) = \max_{1\leq k\leq n}\{T(k-1),T(n-k)\}+\theta(n).
\end{equation}

Note que, de maneira semelhante ao algoritmo Quicksort, a complexidade depende da posição do pivô. No pior caso, isto é, quando $k=n-1$, ou seja, quando o pivô está localizado na última posição do pivô, temos que a relação de recorrência é dada por:

\begin{equation}
\label{e.recurrence_worst_select_i}
\tag{2}
T(n) = T(n-1)+\theta(n),
\end{equation}
a qual, como visto anteriormente, resulta em uma complexidade $O(n^2)$.

Entretanto, podemos mostrar que, no **caso médio**, a função `Select` tem complexidade de $O(n)$. A ideia é partir da premissa que o pivô possui probabilidade de estar localizado em qualquer posição do vetor, mas que na maioria das vezes ele acaba dividindo o conjunto de dados em dois subconjuntos com tamanhos similares.

Agora, vamos realizar uma pequena alteração na função `Partition` para que ela escolha o pivô de maneira **aleatória**, e não como sendo o **último** elemento do vetor. Chamaremos essa função de `Partition_Random`, bem como chamaremos de `Select_Random` sua versão modificada que faz uso do pivô escolhido de maneira aleatória.

```python
def Partition_Random(A, e, d):

    # aqui estão as mudanças *****
    j = numpy.random.randint(e, d)
    pivot = A[j]
    A[j], A[d] = A[d], A[j]
    # ****************************

    p_index = e

    for i in range(e,d):
        if (A[i] <= pivot):
            A[i], A[p_index] = A[p_index], A[i]
            p_index = p_index + 1

    A[p_index], A[d] = A[d], A[p_index]

    return p_index

def Select_Random(A, e, d, i):
    global counter

    if(e == d):
        return A[e]

    p = Partition_Random(A, e, d)

    k = p-e+1 # p está na k-ésima posição de A
    if(k == i):
        return A[p]
    elif(i < k):
        counter = counter + 1
        return Select_Random(A, e, p-1, i)
    else:
        counter = counter + 1
        return Select_Random(A, p+1, d, i-k)
```

Exemplo de funcionamento:

```python
counter = 1 # conta quantas chamadas recursivas foram realizadas
n = 10 # número de elementos do vetor A
i = 3 # posição do i-ésimo menor elemento
A = numpy.random.randint(-20, 20, n)
print('Vetor de entrada: '+ str(A))
x = Select(A, 0, n-1, i)
print('Número de chamadas recursivas para a função Select: '+ str(counter))
print("Elemento: "+ str(x))
counter = 1
x = Select_Random(A, 0, n-1, i)
print('Número de chamadas recursivas para a função Select_Random: '+ str(counter))
print("Elemento: "+ str(x))
```

    Vetor de entrada: [  8  -7 -19  13 -18 -17 -14  -5 -11   8]
    Número de chamadas recursivas para a função Select: 4
    Elemento: -17
    Número de chamadas recursivas para a função Select_Random: 1
    Elemento: -17

Pode se constatar que, na maioria das vezes, o número de chamadas recursivas à função `Select_Random` é menor do que sua versão tradicional `Select`.


# Benchmarking the Hafnian
This tutorial shows how to use Hafnian, a C (masquerading as Python) library to calculate the Hafnian.

### The Hafnian
The hafnian of an $n$-by-$n$ symmetric matrix $A = A^T$ is defined as

\begin{align}\label{eq:hafA}
\text{haf}(A) = \sum_{M \in \text{PMP}(n)} \prod_{\scriptscriptstyle (i, j) \in M} A_{i, j}
% = \sum_{\mu \in \text{PMP}(n)} \prod_{j=1}^n A_{\mu(2j-1),\mu(2j)}
\end{align}
where PMP$(n)$ stands for the set of perfect matching permutations of $n$ (even) objects.

### Using the library

Import the library in the usual way:

```python
from thewalrus import hafnian
```

To use it  we need to pass square numpy arrays as arguments, thus we also must import NumPy:

```python
import numpy as np
```

The library provides functions to compute hafnians of real and complex matrices. The functions arguments must be passed as the NumPy arrays or matrices.

```python
size = 10
nth = 4
matrix = np.ones([size,size])
hafnian(matrix)
```

    945.0000000000427

```python
size = 10
nth = 4
matrix = 1j*np.ones([size,size])
hafnian(matrix)
```

    (6.5888617468912e-11+944.9999999999968j)

Not surprisingly, the hafnian of a matrix containing only ones is given by $(n-1)!! = \frac{n!}{(n/2)! 2^{n/2}}$

```python
from math import factorial
factorial(size)/(factorial(size//2)*2**(size//2))
```

    945.0

Note that when doing floating point computations with large numbers, precision can be lost.

### Benchmarking the performance of the code

For sizes $n=2,30$ we will generate random symmetric matrices and measure the (average) amount of time it takes to calculate their hafnian. The number of samples for each will be geometrically distributed, with 1000 samples for size $n=2$ and 10 samples for $n=30$. The unitaries will be random Haar distributed.

```python
a0 = 1000.
anm1 = 2.
n = 20
r = (anm1/a0)**(1./(n-1))
nreps = [(int)(a0*(r**((i)))) for i in range(n)]
```

```python
nreps
```

    [1000,
     721,
     519,
     374,
     270,
     194,
     140,
     101,
     73,
     52,
     37,
     27,
     19,
     14,
     10,
     7,
     5,
     3,
     2,
     2]

The following function generates random Haar unitaries of dimensions $n$

```python
from scipy import diagonal, randn
from scipy.linalg import qr
def haar_measure(n):
    '''A Random matrix distributed with Haar measure
    See https://arxiv.org/abs/math-ph/0609050
    How to generate random matrices from the classical compact groups
    by Francesco Mezzadri '''
    z = (randn(n,n) + 1j*randn(n,n))/np.sqrt(2.0)
    q,r = qr(z)
    d = diagonal(r)
    ph = d/np.abs(d)
    q = np.multiply(q,ph,q)
    return q
```

Now let's benchmark the scaling of the calculation with the matrix size

```python
import time
times = np.empty(n)
for ind,reps in enumerate(nreps):
    start = time.time()
    for i in range(reps):
        size = 2*(ind+1)
        nth = 1
        matrix = haar_measure(size)
        A = matrix @ matrix.T
        A = 0.5*(A+A.T)
        res = hafnian(A)
    end = time.time()
    times[ind] = (end - start)/reps
    print(2*(ind+1), times[ind])
```

    2 0.00023733234405517577
    4 0.0003898074326005954
    6 0.0005447915064346813
    8 0.00046417611168030114
    10 0.0010115535170943648
    12 0.001820592536139734
    14 0.0031131046158926826
    16 0.01897731393870741
    18 0.035656063524011065
    20 0.0786897219144381
    22 0.11523576040525695
    24 0.20640154238100406
    26 0.41349315643310547
    28 0.894156779561724
    30 2.0350144147872924
    32 4.544636079243252
    34 10.250679206848144
    36 23.105360349019367
    38 50.57243990898132
    40 113.49874126911163

We can now plot the (average) time it takes to calculate the hafnian vs. the size of the matrix:

```python
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_formats=['svg']
```

```python
plt.semilogy(2*np.arange(1,n+1),times,"+")
plt.xlabel(r"Matrix size $n$")
plt.ylabel(r"Time in seconds for 4 threads")
```

    <matplotlib.text.Text at 0x7f1ed80c4b00>

The specs of the computer on which this benchmark was performed are:

```python
!cat /proc/cpuinfo|head -19
```

    processor	: 0
    vendor_id	: AuthenticAMD
    cpu family	: 21
    model		: 101
    model name	: AMD A12-9800 RADEON R7, 12 COMPUTE CORES 4C+8G
    stepping	: 1
    microcode	: 0x6006118
    cpu MHz		: 3771.211
    cache size	: 1024 KB
    physical id	: 0
    siblings	: 4
    core id		: 0
    cpu cores	: 2
    apicid		: 16
    initial apicid	: 0
    fpu		: yes
    fpu_exception	: yes
    cpuid level	: 13
    wp		: yes

If you use this library in your research please cite it:

```python
!cat hafnian.bib
```

    @ARTICLE{2018arXiv180512498B,
       author = {{Bj{\"o}rklund}, A. and {Gupt}, B. and {Quesada}, N.},
       title = "{A faster hafnian formula for complex matrices and its benchmarking on the Titan supercomputer}",
       journal = {ArXiv e-prints},
       archivePrefix = "arXiv",
       eprint = {1805.12498},
       primaryClass = "cs.DS",
       keywords = {Computer Science - Data Structures and Algorithms},
       year = 2018
    }


# Derivativos de Taxa de Juros
## Modelo de Kalotay-Williams-Fabozzi

Renato Mori, Uirá Caiado. 19 de Junho, 2016

**Resumo**

*Neste projeto vamos implementar um modelo de um fator chamado [Kalotay-Williams-Fabozzi](https://en.wikipedia.org/wiki/Short-rate_model#One-factor_short-rate_models), onde apenas um único fator estocástico - a taxa de juros instantânea - determina a evolução de toda estrutura a termo da taxa de juros. Vamos descrever brevemente o modelo, detalhar sua implementação, estimar seus parâmetros e demonstrar sua utilização para precificação de alguns títulos que sejam possíveis de compara com preços de mercado.*

## 1. Introdução

Nesta sessão vamos introduzir o modelo discutido neste trabalho e descrever o problema que será abordado aqui.

### 1.1. Modelagem da Curva de Juros

Como colocado por Fabozzi, a Taxa de Juros frequentemente é modelada usando equações diferenciais estocásticas ([SDEs](https://en.wikipedia.org/wiki/Stochastic_differential_equation) é a abreviação em inglês). Os modelos mais comumente utilizados são os modelos de um ou dois fatores (estocástios), sendo que o de um usa uma SDE para representar a taxa de juros de curto prazo e a segunda representa tanto a taxa de curto como a de longo prazo. É na dinâmica escolhida para o modelo que devem ser refletidas as características desejadas para a curva, como reversão a média e volatilidade dependente do nível da taxa.

Existe duas abordagens para na utilizaçtão de equações estocásticas na modelagem da curva a termo de juros: modelos de equilíbrio e modelos de não arbitragem, sendo que ambos são usados para precificar *Bonds* e derivativos sobre taxa de juros.

Modelos de equilíbrio começam com uma SDE e desenvolvem uma estrutura que não necessariamente correspondem aos preços observados no mercado. Alguns exemplos de modelos desta classe são o Vasicek, Brenn and Schwartz e o Cox, Ingersoll and Ross.

Os modelos de não-arbitragem também utilizam uma SDE, porém usam os preços de mercado para gerar os caminhos possíveis para taxa de juros. Esta estrutura segue as características impostas pela SDE. As taxas de juros encontradas, quando usadas para precificar um bond qualquer, resultam na mesma taxa de juros observada no mercado. Exemplos são o Ho and Lee, Black, Derman and Toy e o Hull and White.

Os modelos de não arbitragem geralmente são escolhidos para precificação de Bonds e derivativos pois garantem que as taxas atuais correspondam ao mercado. Este trabalho explorará um modelo de não arbitragem chamado Kalotay-Williams-Fabozzi.

### 1.2. Modelo Kalotay-Williams-Fabozzi

O modelo Kalotay-Williams-Fabozzi (KWF) assume que as mudanças na taxa de juros de curto prazo segue um certo [processo estocástico](https://en.wikipedia.org/wiki/Stochastic_process) que pode ser descrito pela seguinte SDE:

$$ \mathrm{d} \ln \left (  r(t) \right ) = \theta(t) \mathrm{d} t + \sigma\mathrm{d}Z$$

Onde $Z$ é um processo de [Wiener](https://en.wikipedia.org/wiki/Wiener_process), $\theta$ é o *drift* do processo e $\sigma$ é a volatilidade da primeira diferença das taxas de curto prazo.  Como o que a equação descreve é a dinâmica do **logarítimo natural** de taxa $r$ e $W$ tem distribuição normal, o $\ln(r)$ terá distribuição normal e o $r$, log normal. Desta maneira, embora $\ln(r)$ possa assumir valores negativos, $r$ não assumirá.

No trabalho original em que o modelo KWF foi apresentado, os autores não incluiram explicitamente o *drift* na dinâmica do processo na modelagem e, por tanto, também não utilizaremos ele aqui. Assim, assumindo $\theta=0$, integrando ambos os lados da SDE, chamando $\ln(r)$ de $f(t)$ e sabendo que $W_t - W_s = W_{t-s}$, ficamos com:

\begin{equation}
    \begin{aligned}
    \int_{t}^{T}{\mathrm{d} f(t)} &=  \int_{t}^{T}{\mathrm{d}W_{t}} + \sigma \int_{t}^{T}{\mathrm{d}W_{t}} \\
    f(T) - f(t) &= \sigma \left (  W_{T} - W_{t} \right ) \\
    \ln \left (  r(T) \right ) - \ln \left (  r(t) \right ) &= \sigma \left (  W_{T} - W_{t} \right )\\
    r(T) &= r(t) \cdot e^{\sigma \cdot W_{T-t}}
    \end{aligned}
\end{equation}

Na proxima seção, discretizaremos este processo e implementaremos o modelo.

## 2. Implementando o Modelo

Nesta sessão detalharemos e implementaremos o modelo, utilizando como referência o artigo original. Calibraremos ele para que bata com os dados de mercado.

### 2.1. Aproximação da SDE por Árvore Binomial

As equações diferenciais estocásticas podem ter sua solução aproximada numericamente utilizando uma árvore binomial. Este método assume que a taxa de juros de curto prazo pode assumir apenas dois valores no período seguinte da discretização, $r_{u}$ ou $r_{d}$, onde $r_{u} > r_{d}$. Como cada nó da árvore dá origem à dois novos nós, a medida que se aumenta os passos de discretização, o número de nós aumenta rapidamente, resultando em uma árvore com $2^{\,\,passos-1}$ nós no último passo da árvore, cada um representando uma evolução específica da taxa.

Para evitar este crescimento exponencial, é imposto ao modelo que a árvore seja recombinante, ou seja, um movimento de subida seguido por uma queda resulte no mesmo nó de uma queda seguida de uma alta. Isto reduz a árvore, implicando que tenha a mesma quantidade de nós que tem de passos no último estágio. Um movimento de alta na taxa de juros tem probabilidade $q$ de ocorrer, enquanto um de queda tem probabilidade de $1 - q$. Combinando cada nó com sua respectiva probabilidade, esta árvore é eqivalente à árvore não recombinante.

Utilizaremos probabilidade de $q=0.5$ para garantir a neutralidade a risco na solução da SDE. É importante frisar que isso não implica na probabilidade real da taxa de juros cair. A probabilidade $q$ representa apenas a probabilidade no mundo [*neutro a risco*](https://en.wikipedia.org/wiki/Risk-neutral_measure). Abaixo, vamos testar a estrtutura criada para representar uma árvore binomial. Como será demonstrado, o total de nós criados é equivalente a $\sum_{i=1}^{n+1}{i}$, onde $n$ é a quantidade de passos.

```python
# bibliotecas necessarias
import sys
sys.path.append("../")  # include the root directory as the main
import numpy as np
import math
import pandas as pd
import seaborn as sns
import matplotlib.pylab as plt
```

```python
import time
import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);

def foo(i):
    f_start = time.time()
    x = binomial_tree.BinomialTree(i)
    x._go_foward(i)
    i_rtn = len(x.set_of_nodes)
    return i_rtn, time.time() - f_start

print "Tempo para inicar toda a arvore: "
for i in [10, 100, 250, 500, 1000]:
    # mais de 1000 nodes trava
    i_nodes, f_time = foo(i)
    print u"- Com {} passos: Criados {:0,.0f} nós em {:.2f} segundos".format(i, i_nodes, f_time)
```

    Tempo para inicar toda a arvore:
    - Com 10 passos: Criados 66 nós em 0.00 segundos
    - Com 100 passos: Criados 5,151 nós em 0.12 segundos
    - Com 250 passos: Criados 31,626 nós em 0.85 segundos
    - Com 500 passos: Criados 125,751 nós em 3.28 segundos
    - Com 1000 passos: Criados 501,501 nós em 14.21 segundos

A propriedade da recombinação pode ser visualizada no diagrama abaixo:

```python
import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
x = binomial_tree.BinomialTree(12)
x._go_foward(12)
print 'Primeiros passos da arvore criada:\n========================================\n\n'
print x
```

    Primeiros passos da arvore criada:
    ========================================

    								UUUUUUUU
    							UUUUUUU
    						UUUUUU		DUUUUUUU
    					UUUUU		DUUUUUU
    				UUUU		DUUUUU		DDUUUUUU
    			UUU		DUUUU		DDUUUUU
    		UU		DUUU		DDUUUU		DDDUUUUU
    	U		DUU		DDUUU		DDDUUUU
    _		DU		DDUU		DDDUUU		DDDDUUUU
    	D		DDU		DDDUU		DDDDUUU
    		DD		DDDU		DDDDUU		DDDDDUUU
    			DDD		DDDDU		DDDDDUU
    				DDDD		DDDDDU		DDDDDDUU
    					DDDDD		DDDDDDU
    						DDDDDD		DDDDDDDU
    							DDDDDDD
    								DDDDDDDD

    Plotted 8 from 12 steps

### 2.2. Discretização o Modelo

Como pode ser visto no diagrama acima, a taxa $r_U$ está sempre $2$ nós distante da taxa $r_D$  (há um nó vazio entre elas). Como a dinâmica proposta é dinâmica $r(T) = r(t) \cdot e^{\sigma \cdot W_{T-t}}$, considerando que já possuímos o nó mais baixo, se utilizarmos $r_D$ como *proxy*, a taxa logo acima dela será $r_U=r_D\, e ^{\,2\sigma}$, onde $\sigma$ é a volatilidade constante da taxa [*forward*](http://www.investopedia.com/terms/f/forwardrate.asp) de um ano.

Assim o modelo segue de maneira bem direta: itera os nós mais baixos de maneira que, ao atualizar os nós acima no mesmo passo e trazer o valor de um bond hipotético a valor presente, o preço seja seja markoviado, ou seja, que $E\,[\,X_{t+1}] = X_t$. A iteração é feita chutando valores ao redor de um ponto inicial (o juros de mercado) e minimizando os [erros quadrados](http://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.leastsq.html) entre os preços encontrados pelo modelo e o preço observado no mercado.

```python
# dados originais do paper
import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
l_txindicativa = [0.035, 0.04, 0.045]
l_prazo = [1., 2., 3.]

my_tree = binomial_tree.KWFTree(l_txindicativa, l_prazo)
my_tree.fit_foward_curve(f_sigma=0.1, f_faceval=100.)
print my_tree
```

    			UUU
    		UU
    	U		DUU
    _		DU
    	D		DDU
    		DD
    			DDD

Onde as taxas nos pontos D, DD e DDD foram obtidas por iteração. Utilizando a curva obtida acima, pode-se então calcular o preço de um bond hipotético que vale $\$\,100$ no vencimento e paga cupons de $\$\,5.25$ por ano.

```python
import kwf_model.instruments as instruments; reload(instruments);
bond_example = instruments.Bond(f_face_value=100., f_cupon=5.25)
print "Preço do Bond: {:.2f}".format(bond_example.get_current_value(my_tree, 3))
```

    Preço do Bond: 102.07

Abaixo realizamos o mesmo procedimento para fitar uma curva de LTNs. Utilizamos como referência os preços dos bonds no dia $13$ de Junho, que foram levantados pelo site da [ANBIMA](http://portal.anbima.com.br/Pages/home.aspx). Utilizamos $12$ vértices no total.

O juros de um nó para outro e a volatilidade foram ajustados ao vencimento de cada ponto, de maneira que os nós se relacionam pela seguinte equação:

$$r_U = r_D e^{2 \cdot \sigma \cdot \frac{t}{s}}$$

Onde $t$ equivale a maturidade do nó e $s$ a quantidade de passos dados até então. Primeiro, vamos checar quanto temo a estrtutura criada leva para se ajustar aos vértices passados, utilizando bons hipotéticos com cupons nos juros de cada vértice.

```python
import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
import pandas as pd
# definindo parametros
l_txindicativa = [14.152, 14.0265, 13.7596, 13.4663, 13.1995,
                  12.9729, 12.8001, 12.7250, 12.6541, 12.5649,
                  12.5653, 12.6005]
l_prazo = [9., 74., 136., 199., 260., 324., 385., 446., 509, 635., 758., 888.]
l_txindicativa = [x/100. for x in l_txindicativa]
```

```python
# testa tempo
def foo():
    my_tree = binomial_tree.KWFTree([x for x in l_txindicativa],
                                    [x/252. for x in l_prazo])
    my_tree.fit_foward_curve(f_sigma=0.1)

%timeit foo()
```

    1 loop, best of 3: 799 ms per loop

```python
#cria arvore
import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
my_tree = binomial_tree.KWFTree([x for x in l_txindicativa],
                                [x/252. for x in l_prazo])
my_tree.fit_foward_curve(f_sigma=0.1, f_faceval=1000.)
```

```python
my_tree.get_description()
```

                     cupon     valor     taxa      prazo     venc      sigma
    _                 0.00  1000.000    14.152    0.0357    0.0000    0.0000
    D                 4.25  1032.765    12.622    0.0357    0.0357   10.0000
    U                 4.25   968.216    15.417    0.0357    0.0357   10.0000
    DD               31.08  1062.316    10.907    0.2579    0.2937   10.0000
    DU               31.08  1005.360    13.321    0.2579    0.2937   10.0000
    UU               31.08   941.862    16.271    0.2579    0.2937   10.0000
    DDD              29.63  1084.957     9.373    0.2460    0.5397   10.0000
    DDU              29.63  1035.224    11.448    0.2460    0.5397   10.0000
    DUU              29.63   979.065    13.983    0.2460    0.5397   10.0000
    UUU              29.63   916.580    17.079    0.2460    0.5397   10.0000
    DDDD             30.11  1100.834     8.103    0.2500    0.7897   10.0000
    ...

Com a curva ajustada, vamos calcular os preços atuais de zeros-cupons que tem principal de $\$\,1.000$ para cada um dos vértices e comparar com os seguintes PUs:

```python
for f_r, f_prazo in zip(l_txindicativa, l_prazo):
    f_aux = 1000./(1+f_r)**(f_prazo/252.)
    print "PU do prazo {:4.0f}: {:.2f}".format(f_prazo, f_aux)
```

    PU do prazo    9: 995.28
    PU do prazo   74: 962.19
    PU do prazo  136: 932.79
    PU do prazo  199: 905.05
    PU do prazo  260: 879.93
    PU do prazo  324: 854.85
    PU do prazo  385: 831.92
    PU do prazo  446: 808.97
    PU do prazo  509: 786.10
    PU do prazo  635: 742.12
    PU do prazo  758: 700.45
    PU do prazo  888: 658.24

```python
# calcula precos dos bonds
import kwf_model.instruments as instruments; reload(instruments);
bond_example = instruments.Bond(f_face_value=1000., f_cupon=0.)
for i in xrange(1, 13):
    print "{}: {:5.2f}".format(i, bond_example.get_current_value(my_tree, i_steps=i))
```

    1: 995.28
    2: 962.19
    3: 932.81
    4: 905.13
    5: 880.12
    6: 855.17
    7: 832.39
    8: 809.52
    9: 786.74
    10: 742.89
    11: 701.18
    12: 658.79

Como observado, os valores ficam muito próximos, porém ainda não condizem com os valores de mercado. Para garantir que o modelo convirja para os preços desejados, ao invés de calcularmos um bond hipotético que paga cupons para fazermos a aproximação, como feito no paper, faremos com um zero cupon e imporemos que o modelo se iguale aos PUs originais.

```python
#cria arvore
import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
my_tree = binomial_tree.KWFTreePU([x for x in l_txindicativa],
                                [x/252. for x in l_prazo])
my_tree.fit_foward_curve(f_sigma=0.1, f_faceval=1000.)
my_tree.get_description()
```

                     cupon     valor     taxa      prazo     venc      sigma
    _                 0.00   658.236    14.152    0.0357    0.0000    0.0000
    D                 0.00   666.684    13.959    0.0357    0.0357    0.3571
    U                 0.00   656.026    14.059    0.0357    0.0357    0.3571
    DD                0.00   694.906    13.051    0.2579    0.2937    1.4683
    DU                0.00   684.169    13.439    0.2579    0.2937    1.4683
    UU                0.00   673.166    13.840    0.2579    0.2937    1.4683
    DDD               0.00   721.363    12.157    0.2460    0.5397    1.7989
    DDU               0.00   711.032    12.602    0.2460    0.5397    1.7989
    DUU               0.00   700.423    13.064    0.2460    0.5397    1.7989
    UUU               0.00   689.536    13.542    0.2460    0.5397    1.7989
    DDDD              0.00   747.234    11.390    0.2500    0.7897    1.9742
    ...

```python
bond_example = instruments.Bond(f_face_value=1000., f_cupon=0.)
for i in xrange(1, 13):
    print "{}: {:5.2f}".format(i, bond_example.get_current_value(my_tree, i_steps=i))
```

    1: 995.28
    2: 962.19
    3: 932.79
    4: 905.05
    5: 879.93
    6: 854.85
    7: 831.92
    8: 808.97
    9: 786.10
    10: 742.12
    11: 700.45
    12: 658.24

### 2.3. Por que Árvores Binomiais Funcionam ?

É interessante notar que os modelos de árvore binomial, de maneira geral, usam a recombinação para diminuir a quantidade de simulações executadas. Porém, mantém as características da distribuição resultante se todos os caminhos possíveis fossem criados. Por exemplo, se criarmos todos os caminhos possíveis para a árvore atual com $12$ vértices teremos:

```python
df = bond_example.get_range_of_values(my_tree, i_steps=12)
print "Quantidade de caminhos gerados: {}".format(df.shape[0])
```

    Quantidade de caminhos gerados: 2048

A média dos valores gerados será igual ao valor encontrado utilizando apenas a estrutura original, como pode ser visto abaixo para o último vértice:

```python
print "Preço encontrado do vértice {}: {:5.2f}".format(12, df.mean())
```

    Preço encontrado do vértice 12: 658.24

Todos os caminhos gerados podem ser visualizados na treliça abaixo:

```python
#cria arvore e calcula taxas
bond_example = instruments.Bond(f_face_value=1000., f_cupon=0.)
d_rtn = {}
for i in xrange(1, 13):
    df_aux = bond_example.get_range_of_values(my_tree, i_steps=i)
    f_time = l_prazo[i-1]/252.
    df_aux = (1000./df_aux) ** (1./ f_time) - 1
    d_rtn[f_time] = list(df_aux.values)
```

```python
# plota todas as curva geradas
df_curves = pd.DataFrame(d_rtn)
ax1 = df_curves.T.plot(legend=False)
ax1.set_xlabel('Prazos em anos')
ax1.set_ylabel('Taxas')
ax1.set_title(u'Curvas Geradas pela Árvore\n', fontsize=16);
```

Agora, comparando a média de todas as curvas geradas com os preços de mercado, temos:

```python
# compara curva media e curva de mercado
df_plot = pd.DataFrame(df_curves.mean())
df_plot.columns = [u"Média das Simulações"]
ax = df_plot.plot()
df_plot = pd.DataFrame(l_txindicativa, index=[x/252. for x in l_prazo])
df_plot.columns = ["Mercado"]
df_plot.plot(style='ro', alpha = 0.7, ax=ax)
ax.set_title(u"Comparação de Curvas\n", fontsize=16)
ax.set_xlabel('Prazos em anos')
ax.set_ylabel('Taxas');
```

Por fim, ainda podemos visualizar todas os preços possíveis para o bond do último vértice. A distribuição é centrada no preço de mercado do bond, por construção.

```python
ax = df.hist(bins=20)
ax.set_xlabel("Valor Atual")
ax.set_title(u"Valores Atuais Possíveis Para o Bond Precificado\n", fontsize=15);
```

O curioso é que com apenas $91$ nós criados ao longo da árvore, é possível se obter o mesmo preço gerado com os $2048$ caminhos criados.

## 3. Aplicação do Modelo

Nesta seção iremos ajustar a volatilidade do modelo e usar a curva criada para precificar diversos instrumentos.

### 3.1. A importância da Volatilidade

A volatilidade é o único parâmetro do modelo e se refere ao $\sigma$ da taxa *forward* de 1 ano. Para demonstrar sua importância, abaixo iremos plotar a distribuição dos valores encontrados para cada caminho do último vértice da curva. Vamos calcular a distribuição para os $\sigma=\{2\%, 5\%, 10\%, 25\%, 50\%, 95\%\}$. Assim, primeiro vamos calcular as médias da distribuição.

```python
# TODO: criar dispersão para diferentes volatilidades. mostrar que eh importante ter
# uma estimacaso crivel dela
#cria arvore
import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
d_vals = {}
bond_example = instruments.Bond(f_face_value=1000., f_cupon=0.)
for f_sig in [0.02, 0.05, 0.1, 0.25, 0.5, 0.95]:
    my_tree.fit_foward_curve(f_sigma=f_sig, f_faceval=1000.)
    df = bond_example.get_range_of_values(my_tree, i_steps=12)
    d_vals[f_sig] = list(df.values)
```

```python
df_sig = pd.DataFrame(d_vals)
df_sig.mean()
```

    0.02    658.236155
    0.05    658.236155
    0.10    658.236155
    0.25    658.236153
    0.50    658.236148
    0.95    658.236122
    dtype: float64

Como esperado, são todas "iguais". Agora, vamos plotar o histograma de cada uma das distribuições. Observe que o range de valores do eixo X aumenta em cada gráfico, além da média ficar cada vez mais deslocada para direita.

```python
df2 = df_sig.stack().reset_index()
df2.columns = ["index", "$\sigma$", u"Price"]
g = sns.FacetGrid(df2, col="$\sigma$", col_wrap=3, margin_titles=True,
                  sharex=False, sharey=True, size=3)
g.map(plt.hist, 'Price', edgecolor='white')
g.fig.suptitle(u"Distribuição de Preços Atuais Para Diferentes Sigmas",
               fontsize=16, y=1.04);
```

Para ressaltar a dispersão para cada um dos sigmas, abaixo vamos plotar a descrição deste dados.

```python
df3 = df2.copy()
df3.drop("index", axis=1, inplace=True)
df3.Price = ((1000./df3.Price)**(1/(l_prazo[-1]/252.)) - 1) * 100.
df_stat = df3.groupby("$\sigma$").describe().unstack().T
df_stat
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>$\sigma$</th>
      <th>0.02</th>
      <th>0.05</th>
      <th>0.1</th>
      <th>0.25</th>
      <th>0.5</th>
      <th>0.95</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th rowspan="8" valign="top">Price</th>
      <th>count</th>
      <td>2048.000000</td>
      <td>2048.000000</td>
      <td>2048.000000</td>
      <td>2048.000000</td>
      <td>2048.000000</td>
      <td>2048.000000</td>
    </tr>
    <tr>
      <th>mean</th>
      <td>12.600822</td>
      <td>12.602511</td>
      <td>12.608550</td>
      <td>12.651168</td>
      <td>12.808162</td>
      <td>13.409541</td>
    </tr>
    <tr>
      <th>std</th>
      <td>0.126563</td>
      <td>0.316510</td>
      <td>0.633743</td>
      <td>1.597096</td>
      <td>3.287308</td>
      <td>6.905992</td>
    </tr>
    <tr>
      <th>min</th>
      <td>12.212594</td>
      <td>11.658470</td>
      <td>10.804274</td>
      <td>8.690043</td>
      <td>6.281002</td>
      <td>3.958541</td>
    </tr>
    <tr>
      <th>25%</th>
      <td>12.512740</td>
      <td>12.380292</td>
      <td>12.158345</td>
      <td>11.491909</td>
      <td>10.397421</td>
      <td>8.570103</td>
    </tr>
    <tr>
      <th>50%</th>
      <td>12.599974</td>
      <td>12.597249</td>
      <td>12.587588</td>
      <td>12.522114</td>
      <td>12.305924</td>
      <td>11.660371</td>
    </tr>
    <tr>
      <th>75%</th>
      <td>12.688215</td>
      <td>12.819759</td>
      <td>13.036637</td>
      <td>13.676681</td>
      <td>14.669622</td>
      <td>16.249875</td>
    </tr>
    <tr>
      <th>max</th>
      <td>13.003875</td>
      <td>13.639262</td>
      <td>14.784376</td>
      <td>18.965234</td>
      <td>29.174831</td>
      <td>63.345630</td>
    </tr>
  </tbody>
</table>
</div>

### 3.2. Ajustando a Volatilidade

Para estimar a volatilidade, vamos calcular a volatilidade diária da primeira diferença da taxa *forward* entre os DIs com maior liquidez, que são os contratos com vencimento em janeiro de 2017 e 2018. Esta volatilidade será anualizada para 1 ano. Vamos pegar a curva ao meio dia dos últimos três meses para realizar os cálculos. Assim, primeiro vamos calcular as taxas forwards anualizadas e filtrar apenas o horário desejado. Os forwards vão ser calculados da seguinte maneira:

$r_{forward}=\frac{PU_{c}}{PU_{l}}^{252/(DU_{l}-DU_{c})}-1$

Onde $c$ se refere à taxa curta e $l$ à taxa longa.

```python
df_di_prices = pd.read_csv("../data/di_fut_price.tsv", index_col="DATE", parse_dates=["DATE"])
df_di_du = pd.read_csv("../data/di_fut_maturity.tsv", index_col="DATE", parse_dates=["DATE"])
df_di_prices.ix[:, ['DI1F17', 'DI1F18']].head()
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>DI1F17</th>
      <th>DI1F18</th>
    </tr>
    <tr>
      <th>DATE</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>2016-04-04 09:20:00</th>
      <td>13.74</td>
      <td>13.47</td>
    </tr>
    <tr>
      <th>2016-04-04 09:25:00</th>
      <td>13.75</td>
      <td>13.48</td>
    </tr>
    <tr>
      <th>2016-04-04 09:30:00</th>
      <td>13.77</td>
      <td>13.51</td>
    </tr>
    <tr>
      <th>2016-04-04 09:35:00</th>
      <td>13.77</td>
      <td>13.53</td>
    </tr>
    <tr>
      <th>2016-04-04 09:40:00</th>
      <td>13.79</td>
      <td>13.56</td>
    </tr>
  </tbody>
</table>
</div>

```python
# calcula partes da formula
df_pu = df_di_prices.ix[:, ['DI1F17', 'DI1F18']]
df_du = df_di_du.ix[:, ['DI1F17', 'DI1F18']]
df_prazo_norm = 252./(df_du['DI1F18']-df_du['DI1F17'])
df_pu = df_pu/100. + 1
df_pu = 10**5/(df_pu)**(df_du/252.)
# calcula taxa forward
df_fwrd = (df_pu['DI1F17']/df_pu['DI1F18'])**df_prazo_norm-1
# cria lista de filtros da taxa
l_filtros_meio_dia = [(x.hour==12)&(x.minute==0)&(x.second==0) for x in df_fwrd.index]
# filtra dados
df_fwrd = df_fwrd[l_filtros_meio_dia]
df_fwrd.head()
```

    DATE
    2016-04-04 12:00:00    0.134209
    2016-04-05 12:00:00    0.135644
    2016-04-06 12:00:00    0.135500
    2016-04-07 12:00:00    0.136704
    2016-04-08 12:00:00    0.134342
    dtype: float64

```python
print "Quantidade de pontos faltantes de {}: {}".format(df_fwrd.shape[0], df_fwrd.isnull().sum())
```

    Quantidade de pontos faltantes de 55: 4

Vamos tirar estes pontos da série. Provavelmente seria mais apropriado usar alguma interpolação entre eles

```python
df_fwrd.dropna(inplace=True)
print "Quantidade de pontos faltantes de {}: {}".format(df_fwrd.shape[0], df_fwrd.isnull().sum())
```

    Quantidade de pontos faltantes de 51: 0

Agora, vamos calcular a dispersão da [primeira diferença](https://en.wikipedia.org/wiki/First-difference_estimator) da taxa forward

```python
df_first_difference = (df_fwrd - df_fwrd.shift())
```

```python
f, (ax1, ax2) = plt.subplots(1, 2)
df_first_difference.plot(ax=ax1)
df_first_difference.hist(ax=ax2);
plt.suptitle(u'Primeira Diferença da Taxa Forward\n', fontsize=16, y=1.04);
f.tight_layout()
```

```python
# calcula a volatilidade anualizada
f_sigma = df_first_difference.std() * (252.)**.5
print "A vol anualizada do forward de 1 ano entre o DI1F17 e DI1F18 foi de {:01.2f}%".format(f_sigma*100)
```

    A vol anualizada do forward de 1 ano entre o DI1F17 e DI1F18 foi de 1.92%

Olhando para volatilidade obtida,  o parâmetro encontrado parece muito baixo. Vamos calcular a estrutura inteira para checar como é o $\sigma$ das forwards de um ano utilizando outros vencimentos.

```python
def cal_col(df_di1, df_du, s_short, s_long):
    '''
    Return anuallized volatility of forward rate using daily data
    '''
    # calcula partes da formula
    df_pu = df_di1.ix[:, [s_short, s_long]]
    df_du = df_du.ix[:, [s_short, s_long]]
    df_prazo_norm = 252./(df_du[s_long]-df_du[s_short])
    df_pu = df_pu/100. + 1
    df_pu = 10**5/(df_pu)**(df_du/252.)
    # calcula taxa forward
    df_fwrd = (df_pu[s_short]/df_pu[s_long])**df_prazo_norm-1
    # cria lista de filtros da taxa
    l_filtros_meio_dia = [(x.hour==12)&(x.minute==0)&(x.second==0)
                          for x in df_fwrd.index]
    df_fwrd = df_fwrd[l_filtros_meio_dia]
    df_first_difference = (df_fwrd - df_fwrd.shift())
    f_sigma = df_first_difference.std() * (252.)**.5
    return f_sigma
```

```python
l_col = [u'DI1F17', u'DI1F18', u'DI1F19', u'DI1F20', u'DI1F21',
         u'DI1F22', u'DI1F23', u'DI1F24', u'DI1F25', u'DI1F26',
         u'DI1F27']
l_sigma = []
l_leg = []
for idx in xrange(len(l_col)-1):
    f_sigma = cal_col(df_di_prices, df_di_du, l_col[idx], l_col[idx+1])
    l_leg.append("{}-{}".format(l_col[idx][3:], l_col[idx+1][3:]))
    l_sigma.append(f_sigma)
```

```python
ax = pd.DataFrame(l_sigma, index=l_leg).plot(legend=False);
ax.set_title("Estrutura de Vol das Forwards de DI1 de $1$ ano\n", fontsize=16);
```

Como pode ser visto, a volatilidade das *forwards* ficou no mesmo nível em vários vencimentos. Vamos utilizar a média das volatilidades destes vértices como a volatilidade do modelo daqui para frente.

```python
f_sigma = np.mean(l_sigma[1:-2])
print u"A volatilidade utilizada será de {:0.2f}%".format(f_sigma*100)
```

    A volatilidade utilizada será de 3.28%

### 3.3. Precificando Borboletas de DI1

Uma das limitaões que aparentemente as árvores binomiais possuem é quando precisamos precificar pontos com maturidades que estejam entre dois passos da árvore. Como a estrutura do modelo impõe que as taxas forwards sejam constantes entre os nós dela, uma opção é procurar o primeiro nó com vencimento depois do que queremos e começar o cálculo a partir do passo anterior, como em uma interpolação. *Isso se torna um problema se estamos interessados justamente nos forwards destes pontos*.

Por exemplo, digamos que estamos interessados em precificar uma borboleta de DI1. Esta estratégia consiste em combinar três vencimentos diferentes de maneira a se proteger de movimentos paralelos na curva de juros. Apesar de existir muitas maneiras de se [balancear](http://www.jamesgoulding.com/Research_II/Butterfly/Butterfly%20%28Understanding%29.pdf) cada ponta da operação para privilegiar movimentos específicos da curva, os preços são calculados sempre da mesma maneira pelo mercado. Basicamente, é uma diferença de taxas forwards, cotada em *basispoints*. Assim, o preço é dado por:

$P_{fly} = \left (  \left (PU_{m}/PU_{l}\right )^{252/(DU_{l}-DU_{m})} - \left (PU_{c}/PU_{m}\right )^{252/(DU_{m}-DU_{c})} \right ) \times 10^{\,4}$

Onde $c$ é a taxa curta, $l$ a longa e $m$ a taxa do miolo. Para denotar esta característica de taxas forwards constantes, vamos calibrar a árvore apenas com os vencimentos $F$ (janeiro) e utilizar esta estrutura para achar o vencimento $N17$. Então, calcularemos o preço da borboleta $F17-N17-F18$ (janeiro, julho, janeiro). Utilizando apenas as $F$ e a volatilidade acima, nossa treliça fica da seguinte maneira:

```python
l_filtros_cinco_maio = [(x.day==5)&(x.month==5)&(x.hour>=10)&(x.hour<=15) for x in df_di_prices.index]
```

```python
df_maio_di = df_di_prices[l_filtros_cinco_maio]
df_maio_du = df_di_du[l_filtros_cinco_maio]
l_col = [u'DI1F17', u'DI1F18', u'DI1F19', u'DI1F20', u'DI1F21',
         u'DI1F22', u'DI1F23', u'DI1F24', u'DI1F25']
#pontos utilizados
l_tx_di= list((df_maio_di[l_col].head(1)/100.).values[0])
l_du_di= list((df_maio_du[l_col].head(1)/252).values[0])

import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
my_tree = binomial_tree.KWFTreePU(l_tx_di, l_du_di)
my_tree.fit_foward_curve(f_sigma=f_sigma, f_faceval=100000.)
```

```python
#cria arvore e calcula taxas
import kwf_model.instruments as instruments; reload(instruments);
bond_example = instruments.Bond(f_face_value=100000., f_cupon=0.)
d_rtn = {}
for i in xrange(1, len(l_du_di) + 1):
    df_aux = bond_example.get_range_of_values(my_tree, i_steps=i)
    f_time = l_du_di[i-1]
    df_aux = (100000./df_aux) ** (1./ f_time) - 1
    d_rtn[f_time] = list(df_aux.values)
```

```python
f, (ax1, ax2) = plt.subplots(1, 2,sharey=True, sharex=True)
# plota todas as curva geradas
df_curves = pd.DataFrame(d_rtn)
ax1 = df_curves.T.plot(legend=False, ax = ax1)
ax1.set_xlabel('Prazos em anos')
ax1.set_ylabel('Taxas')
ax1.set_title(u'Curvas Geradas pela Árvore\n', fontsize=14);

# compara curva media e curva de mercado
df_plot = pd.DataFrame(df_curves.mean())
df_plot.columns = [u"Média das Simulações"]
df_plot.plot(ax=ax2)
df_plot = pd.DataFrame(l_tx_di, index=l_du_di)
df_plot.columns = ["Mercado"]
df_plot.plot(style='ro', alpha = 0.7, ax=ax2)
ax2.set_title(u"Comparação de Curvas\n", fontsize=14)
ax2.set_xlabel('Prazos em anos')
ax2.set_ylabel('Taxas');

f.tight_layout()
```

Agora vamos calcular os preços da estratégia com os dados de mercado deste dia

```python
#pontos utilizados
l_fnf = [u'DI1F17', u'DI1N17', u'DI1F18']
df_fnf = df_maio_di[l_fnf]
df_fnf_du =  df_maio_du[l_fnf]
```

```python
def calc_forward(l_cmm, df_prices=df_maio_di, df_maturity=df_maio_du):
    '''
    Return a data frame with the the foward rate
    between two points
    :param l_fnf: list. The two instrument names
    :param df_prices: data frame. Prices matrix
    :param df_maturity: data frame. Maturity matrix
    '''
    df_p = df_prices[l_cmm]
    df_d =  df_maturity[l_cmm]

    # calcula partes da formula
    df_prazo_norm = 252./(df_d[l_cmm[1]]-df_d[l_cmm[0]])
    df_pu = df_p/100. + 1
    df_pu = 10**5/(df_pu)**(df_d/252.)
    # calcula taxa forward
    df_fwrd1 = (df_pu[l_cmm[0]]/df_pu[l_cmm[1]])**df_prazo_norm
    return df_fwrd1
```

```python
# calcula preco da fly
df_fly = (calc_forward(l_fnf[1:])-calc_forward(l_fnf[:2]))*10**4
```

```python
ax = df_fly.hist()
ax.set_title(u"Distribuição dos preços da F17-N17-F18\n", fontsize=16);
```

Vamos comparar esta distribuição de preços com os preços gerados pelo modelo sobrepondo alguns pontos fora da discretização sobre a curva de mercado. Também vamos plotar apenas a curva das $F$.

```python
# TODO: preciso corrigir prazo do primeiro noh das arvores
```

```python
# calcula os vencimentos utilizando a arvore fitada apenas
# com os contratos F
import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
import kwf_model.instruments as instruments; reload(instruments);
# l_value = []
l_value2 = []
for f_du in [291., 540., 789.]:
    DI1 = instruments.BondBetween(f_face_value=10.**5,
                                  f_maturity=f_du/252,  # prazo do N17
                                  f_cupon=0.)
#     f_pu = DI1.get_range_of_values(my_tree).mean()
#     l_value.append((10**5/f_pu)**(252./f_du)-1)
    f_pu = DI1.get_current_value(my_tree)
    l_value2.append((10**5/f_pu)**(252./f_du)-1)

```

```python
# filtra os preços de mercado incluindo os contratos F e os N mencionados
l_col3 = [u'DI1F17', u'DI1N17', u'DI1F18',  u'DI1N18', u'DI1F19', u'DI1N19',
         u'DI1F20', u'DI1F21', u'DI1F22', u'DI1F23', u'DI1F24', u'DI1F25']
l_tx_di2= list((df_maio_di[l_col3].head(1)/100.).values[0])
l_du_di2= list((df_maio_du[l_col3].head(1)/252).values[0])
```

```python
# compara pontos calculados e a curva de mercado
# mercado
df_plot = pd.DataFrame(l_tx_di2, index=l_du_di2)
df_plot.columns = [u"Curva de Mercado"]
ax = df_plot.plot()

# mercado so os F
df_plot = pd.DataFrame(l_tx_di, index=l_du_di)
df_plot.columns = [u"Curva Apenas dos F"]
ax = df_plot.plot(ax=ax, style='g--', alpha = 0.7)

#pontos calculados
df_plot = pd.DataFrame(l_value2,
                       index=[x/252.
                              for x in [291., 540., 789.]])
df_plot.columns = [u"Pontos fora da árvore"]
df_plot.plot(style='ro', alpha = 0.7, ax=ax)

ax.set_title(u"Comparação de Curvas\n", fontsize=14)
ax.set_xlabel('Prazos em anos')
ax.set_ylabel('Taxas');
```

É interessante notar que a utilização da árvore com os forwards constantes entre os nós tem um efeito parecido com o de uma interpolação cúbica - ela impõe uma curvatura à estrutura
às taxas entre os vértices. Agora, vamos calcular os preços da estartégia pelo modelo.

```python
import kwf_model.instruments as instruments; reload(instruments);
d_values = {}
l_du = list(df_fnf_du.head(1).values[0])
l_name = [u'F17', u'N18', u'F19']
d_valuesAll = {}
for s_name, f_du in zip(l_name, l_du):
    DI1 = instruments.BondBetween(f_face_value=10.**5,
                                  f_maturity=f_du/252,
                                  f_cupon=0.)
    f_pu = DI1.get_current_value(my_tree)
    d_values[s_name] = {"PU": f_pu, "DU": f_du}
    d_valuesAll[s_name] = DI1.get_range_of_values(my_tree)

df_values = pd.DataFrame(d_valuesAll)
df_miolo_longo = (df_values[l_name[1]]/df_values[l_name[2]])**(252./(d_values[l_name[2]]['DU'] - d_values[l_name[1]]['DU']))
df_curto_miolo = (df_values[l_name[0]]/df_values[l_name[1]])**(252./(d_values[l_name[1]]['DU'] - d_values[l_name[0]]['DU']))
df_values = (df_miolo_longo-df_curto_miolo)*10**4
df_values.describe()
```

    count    2.560000e+02
    mean    -1.110223e-12
    std      1.112398e-12
    min     -2.220446e-12
    25%     -2.220446e-12
    50%     -1.110223e-12
    75%      0.000000e+00
    max      0.000000e+00
    dtype: float64

Como pode-se observar acima, **não deu certo**. Todos valores ficaram perto de $0$. Porém, considerando que a Fly de Di é uma diferenças de *forwards* e estamos usando a mesma forward  do $F17$ para o $N17$ e, consequentemente, do $N17$ para o $F18$, ao menos agora sabemos que a modelagem está correta, mas não é adequada para este caso.

Agora vamos fazer um pouco diferente. Vamos calcular os cenários para pontos que já conhecemos, fixando a curva das 10:00 para calibração da árvore. Abaixo, vamos descrever os preços observados para a borboleta $F22-F23-F24$.

```python
#pontos utilizados
l_fnf = [u'DI1F22', u'DI1F23', u'DI1F24']
df_fnf = df_maio_di[l_fnf]
df_fnf_du =  df_maio_du[l_fnf]
# calcula preco da fly
df_fly = (calc_forward(l_fnf[1:], df_prices=df_fnf, df_maturity=df_fnf_du)
          -calc_forward(l_fnf[:2], df_prices=df_fnf, df_maturity=df_fnf_du))*10**4

```

```python
df_fly.describe()
```

    count    62.000000
    mean    -18.323970
    std      10.282591
    min     -47.038939
    25%     -20.191773
    50%     -14.501875
    75%     -14.501844
    max      -1.079785
    dtype: float64

Agora, vamos calcular a distribuição dos preços encontrados pelo modelo

```python
# fita a arvore com todos os pontos validos
i_curve = 1
df_maio_di = df_di_prices[l_filtros_cinco_maio]
df_maio_du = df_di_du[l_filtros_cinco_maio]
l_col = [u'DI1N16', u'DI1V16', u'DI1F17', u'DI1J17', u'DI1N17', u'DI1V17',
         u'DI1F18', u'DI1J18', u'DI1N18', u'DI1V18', u'DI1F19', u'DI1J19',
         u'DI1N19', u'DI1V19', u'DI1F20', u'DI1J20', u'DI1N20', u'DI1V20',
         u'DI1F21', u'DI1J21', u'DI1N21', u'DI1V21', u'DI1F22', u'DI1F23',
         u'DI1F24', u'DI1F25', u'DI1F26', u'DI1F27']
l_col = list(df_maio_di[l_col].head(i_curve).dropna(axis=1).columns)
#pontos utilizados
l_tx_di= list((df_maio_di[l_col].head(i_curve)/100.).values[0])
l_du_di= list((df_maio_du[l_col].head(i_curve)/252).values[0])

import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
my_tree = binomial_tree.KWFTreePU(l_tx_di, l_du_di)
my_tree.fit_foward_curve(f_sigma=f_sigma, f_faceval=100000.)
```

```python
# calcula precos
import kwf_model.instruments as instruments; reload(instruments);
d_values = {}
l_du = list(df_fnf_du.head(i_curve).values[0])
l_name = [u'F22', u'F23', u'F24']
d_valuesAll = {}
for s_name, f_du in zip(l_name, l_du):
    DI1 = instruments.BondBetween(f_face_value=10.**5,
                                  f_maturity=f_du/252,
                                  f_cupon=0.)
    f_pu = DI1.get_current_value(my_tree)
    d_values[s_name] = {"PU": f_pu, "DU": f_du}
    d_valuesAll[s_name] = DI1.get_range_of_values(my_tree)

df_values = pd.DataFrame(d_valuesAll)
df_miolo_longo = (df_values[l_name[1]]/df_values[l_name[2]])**(252./(d_values[l_name[2]]['DU'] - d_values[l_name[1]]['DU']))
df_curto_miolo = (df_values[l_name[0]]/df_values[l_name[1]])**(252./(d_values[l_name[1]]['DU'] - d_values[l_name[0]]['DU']))
df_values = (df_miolo_longo-df_curto_miolo)*10**4
df_values.describe()
```

    count    32768.000000
    mean       -25.243492
    std         22.366775
    min        -56.155439
    25%        -47.342162
    50%        -26.362649
    75%         -3.740642
    max         21.087397
    dtype: float64

Os preços encontrados ficaram condizentes com o mercado, mesmo fixando a curva no início do dia.

### 3.4. Precificando Uma NTN-F

O título [NTN-F](http://carodinheiro.blogfolha.uol.com.br/2012/12/07/saiba-como-o-titulo-prefixado-ntn-f-funciona-parte-2/) é um título pré-fixado que possui cupons semestrais, além do pagamento do valor nominal, realizado na data de vencimento. O investidor recebe juros semestralmente de acordo com estabelecido pelo Tesouro na data de emissão. Para sua precificação, vamos utilizar a curva de LTN-F. Porém, faremos uma modificação. Calcularemos os valores da LTN-F semestralmente e então recalibraremos a curva com os PUs encontrados. Comparando a curva original com a curva obtida, temo que:

```python
# definindo parametros inciais
l_txindicativa = [14.152, 14.0265, 13.7596, 13.4663, 13.1995,
                  12.9729, 12.8001, 12.7250, 12.6541, 12.5649,
                  12.5653, 12.6005]
l_prazo = [9., 74., 136., 199., 260., 324., 385., 446., 509, 635., 758.,
           888.]
```

```python
#pontos utilizados
l_tx= [x/100. for x in l_txindicativa]
l_du= [x/252. for x in l_prazo]

import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
my_tree = binomial_tree.KWFTreePU(l_tx, l_du)
my_tree.fit_foward_curve(f_sigma=f_sigma, f_faceval=1000.)
```

```python
# inicia variaveis
import kwf_model.instruments as instruments; reload(instruments);
import kwf_model.binomial_tree as binomial_tree; reload(binomial_tree);
d_values = {}
l_prazo = [9., 74., 126., 136., 199., 252., 260., 324., 378., 385., 446.,
           504., 509., 630., 635., 756., 758., 882., 888.]
# calcula valores itermediarios
for f_du in l_prazo:
    LFT = instruments.BondBetween(f_face_value=1000.,
                                  f_maturity=f_du/252.,
                                  f_cupon=0.)
    f_pu = LFT.get_current_value(my_tree)
    d_values[f_du/252.] = {"DI": (1000./f_pu)**(252./f_du)-1}

# fita nova arvore
df_aux = pd.DataFrame(d_values)
my_tree2 = binomial_tree.KWFTreePU(list(df_aux.values[0]), list(df_aux.keys()))
my_tree2.fit_foward_curve(f_sigma=f_sigma, f_faceval=1000.)

# calcula valores com nova arvore
d_values = {}

for f_du in l_prazo:
    LFT = instruments.BondBetween(f_face_value=1000.,
                                  f_maturity=f_du/252.,
                                  f_cupon=0.)
    f_pu = LFT.get_current_value(my_tree2)
    d_values[f_du/252.] = {"DI": (1000./f_pu)**(252./f_du)-1}
```

```python
# plota todas as curva geradas
ax = pd.Series(dict(zip(l_du, l_tx))).plot()
# compara curva media e curva de mercado
pd.DataFrame(d_values).T.plot(style="ro",  ax=ax, legend=False)
ax.set_xlabel('Prazos em anos')
ax.set_ylabel('Taxas')
ax.set_title(u'comparação de Curvas de LTNs\n', fontsize=14);

```

Com esta curva, vamos calcular o valor de uma NTN-F com vencimento em 2019, que tem PU segundo a ANBIMA de $R\$\,996.28$.

```python
import kwf_model.instruments as instruments; reload(instruments);
l_du_cupons = [x/252. for x in [9., 136., 260., 385., 509., 635., 758.]]
NTN = instruments.NTN_F(f_maturity=758./252., l_du_cupons=l_du_cupons)
print "Preço encontrado pelo modelo: R$ {:.2f}".format(NTN.get_current_value(my_tree2))
```

    Preço encontrado pelo modelo: R$ 986.89

```python
%time df_prices = NTN.get_range_of_values(my_tree2)
```

    CPU times: user 1min 27s, sys: 768 ms, total: 1min 28s
    Wall time: 1min 27s

```python
df_prices.describe()
```

    count    262144.000000
    mean        986.891445
    std           3.281032
    min         975.053980
    25%         984.642447
    50%         986.904364
    75%         989.155484
    max         998.320501
    dtype: float64

Pela curva de LTN, chegamos que a NTN-F deveria valer $R\$\,986.89$, pouco menos de $R\$\,10.00$ do valor registrado pela ANBIMA. Olhando ainda para dispersão dos valores da simulação, podemos concluir que este valor está "caro" (acima do quartil terceiro quartil) quando comparado às LTNs.

## 4. Conclusão

O modelo KWF, apesar de simples, mostrou-se bastante útil e demonstrou um método simplificado para se ajustar ao mercado. Alguns ajustes tiveram que ser feitos para que se adequasse aos dados utilizados, como a adaptação do $\sigma$ e a utilização de PUs para calibragem, e não de taxas como feito pelo modelo.

O KWF, assim como outros modelos de árvore binomial, possui carcaterísticas interessantes que poderiam ser utilizados em outros contextos além da precificação. Poderia, por exemplo, ser utilizado de maneira relevatnte para tomada de decisão em *trading*, desde que se achasse outro método para criar uma curva crível onde se basear.

## 5. Últimas Considerações

A estrutura criada não é eficiente. Era esperado que a escolha em separar a árvore e os nós em classes diferentes facilitasse a implementação do modelo, mas moutrou-se extamanete o contrário. Porém, ganhamos em clareza e em uma estrtutura que poderíamos utilizar para implementar outros modelos.

Talvez se não mantessemos todos os nós em memória fosse melhor, porém cremos que a complexidade aumentaria bastante. Também criamos todos os caminhos na força bruta para plotar os histogramas, porém isso é inviável em árvores maiores. Seria melhor criar a distribuição ponderando cada valor pela respectiva probabilidade e criando uma histograma hipotético.

*Style notebook and change matplotlib defaults*

```python
#loading style sheet
from IPython.core.display import HTML
HTML(open('ipython_style.css').read())
```

<style>
    @font-face {
        font-family: "Computer Modern";
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunss.otf');
    }
    @font-face {
        font-family: "Computer Modern";
        font-weight: bold;
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunsx.otf');
    }
    @font-face {
        font-family: "Computer Modern";
        font-style: oblique;
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunsi.otf');
    }
    @font-face {
        font-family: "Computer Modern";
        font-weight: bold;
        font-style: oblique;
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunso.otf');
    }
    div.cell{
        width:800px;
        margin-left:16% !important;
        margin-right:auto;
    }
    h1 {
        font-family: Helvetica, serif;
    }
    h4{
        margin-top:12px;
        margin-bottom: 3px;
       }
    div.text_cell_render{
        font-family: Computer Modern, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
        line-height: 145%;
        font-size: 130%;
        width:800px;
        margin-left:auto;
        margin-right:auto;
    }
    .CodeMirror{
            font-family: "Source Code Pro", source-code-pro,Consolas, monospace;
    }
    .prompt{
        display: None;
    }
    .text_cell_render h5 {
        font-weight: 300;
        font-size: 22pt;
        color: #4057A1;
        font-style: italic;
        margin-bottom: .5em;
        margin-top: 0.5em;
        display: block;
    }

    .warning{
        color: rgb( 240, 20, 20 )
        }
</style>

<style>
    table {
        overflow:hidden;
        font-family: Computer Modern, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
        font-size: 12px;
        margin: 10px;
        width: 480px;
        text-align: left;
        border-collapse: collapse;
        border: 1px solid #d3d3d3;
        -moz-border-radius:5px; FF1+;
        -webkit-border-radius:5px; Saf3-4;
        border-radius:5px;
        -moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.01);
    }
    th
    {
        padding: 12px 17px 12px 17px;
        font-weight: normal;
        font-size: 14px;
        border-bottom: 1px dashed #69c;
    }

    td
    {
        padding: 7px 17px 7px 17px;

    }

    tbody tr:hover th
    {

        background:  #E9E9E9;
    }

    tbody tr:hover td
    {

        background:  #E9E9E9;
    }

</style>

```python
#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("PuBuGn_d", 10))
```

```python

```


```python
# ML_in_Finance_ARIMA-HFT
# Author: Matthew Dixon
# Version: 1.0 (24.7.2019)
# License: MIT
# Email: matthew.dixon@iit.edu
# Notes: tested on Mac OS X with Python 3.6 and Tensorflow 1.3.0
# Citation: Please cite the following reference if this notebook is used for research purposes:
# Dixon M.F., I. Halperin and P. Bilokon, Machine Learning in Finance: From Theory to Practice, Springer Graduate textbook Series, 2020.
```

# Overview
The purpose of this notebook is to demonstrate the application of ARIMA time series modeling to high frequency data. The model predicts changes in the VWAP (volume weighted average prices) based on historical observations of the VWMAP and an exogenous variable, the current Order Flow Imbalance (OFI). See https://arxiv.org/abs/1011.6402 for more details on OFI.

The notebook describes many of the steps in Chapter 6 on applying the Dickey-Fuller test to establish stationarity of the endogenous series, followed by use of the ACF and PACF to identify the ARIMA model order. Typically, addition diagnostic tests are performed on the model residual but omitted here. Instead we simply compare the forecast with the actual VWAP change.

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
from ofi import ofi, time_as_index, smart_price
from statsmodels.regression.linear_model import OLS

%matplotlib inline
plt.rcParams['figure.figsize'] = (12, 9)
```

    /Users/dez/miniconda3/envs/MLFenv/lib/python3.6/site-packages/statsmodels/tools/_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.
      import pandas.util.testing as tm

Load the data

```python
sp500quotes = pd.read_pickle('../data/09012013_quotes.pickle')
time_as_index(sp500quotes)

sp500ofi = ofi(sp500quotes['Bid Size'], sp500quotes['Ask Size'],
               sp500quotes['Bid Price'], sp500quotes['Ask Price'])
sp500smart = smart_price(sp500quotes['Bid Price'], sp500quotes['Bid Size'],
                         sp500quotes['Ask Price'], sp500quotes['Ask Size'])

sp500smart_diff = sp500smart.diff(1)
pd.concat([sp500ofi, sp500smart_diff], axis=1).plot(kind='scatter',
                                                    x='OFI', y='VWAP', alpha=0.1);
```

### Stationarity
It is essential to determine whether the time series is "stationary". Informally, stationarity is when the auto-covariance is independent of time. Failure to establish stationarity will almost certainly lead to misinterpretation of model identification and diagnostics tests. Moreover, stationarity is decisive in characterizing the prediction problem and whether to use a more advanced architecture. In particular, we can expect a plain RNN to perform poorly if the data is non-stationary as the RNN exhibits fixed auto-covariance.

We perform an Augmented Dickey-Fuller test to establish stationarity under the assumption that the time series has a constant bias but does not exhibit a time trend. In other words, we assume that the time series is already de-trended.

If the stationarity test fails, even after first de-trending the time series, then one potential recourse is to simply take differences of time series and predict $\Delta y_t$.

The null hypothesis of the Augmented Dickey-Fuller is that there is a unit root, with the alternative that there is no unit root. If the p-value is above $(1-\alpha)$, then we cannot reject that there is a unit root. Note that a subset of the time series is used to reduce the computation time of the test. Test separately on both the dependent and the independent variable.

```python
sample = sp500smart_diff[1:]
adf, p, usedlag, nobs, cvs, aic = sm.tsa.stattools.adfuller(sample)
```

```python
adf_results_string = 'ADF: {}\np-value: {},\nN: {}, \ncritical values: {}'
print(adf_results_string.format(adf, p, nobs, cvs))
```

    ADF: -51.65350270603796
    p-value: 0.0,
    N: 82102,
    critical values: {'1%': -3.4304296509764045, '5%': -2.861575204399113, '10%': -2.566788738084499}

We reject the Null hypothesis at the 99% level - the difference in smart prices contain no unit roots and are stationary.

## Determine the order p of the AR model from the PACF plot

#### Autoregressive Model Identification: The partial auto-correlation
It is important to determine the number of lags, the sequence length, required in the RNN by statistical analysis. A brute-force approach will in general be too time-consuming.

A partial auto-correlation at lag $h\geq 2$ is a conditional auto-correlation between a variable, $X_t$, and its $h^{th}$ lag, $X_{t-h}$ under the assumption that we control for the values of the intermediate lags, $X_{t-1},\dots, X_{t-h+1}$:

$$\begin{align}\tau_h&:=\tau(X_t, X_{t-h}; X_{t-1},\dots, X_{t-h+1})\\
&:=\frac{\gamma(X_t, X_{t-h}; X_{t-1},\dots, X_{t-h+1})}{\sqrt{\gamma(X_t |X_{t-1},\dots, X_{t-h+1})\gamma(X_{t-h} |X_{t-1},\dots, X_{t-h+1}))}}
,\end{align}$$
where $\gamma_h:=\gamma(X_tX_{t-h})$ is the lag-$h$ autocovariance. The partial autocorrelation function $\tau_h:\mathbb{N} \rightarrow [-1,1]$ is a map $h:\mapsto \tau_h$.

The estimated partial auto-correlation function (PACF) can be used to identify the order of an autoregressive time series model. Values of $|\tau_h|$ greater or equal to $\frac{\Phi^{-1}(\alpha)}{\sqrt{T}}$, where $T$ is the number of observations and $\Phi(z)$ is the standard normal CDF, are significant lag $h$ partial autocorelations at the $\alpha$ confidence level.

We use the stattools package to estimat the PACF. The `nlags` parameter is the maximum number of lags used for PACF estimation of the endogeneous variable - SP500 mid-prices.

```python
pacf = sm.tsa.stattools.pacf(sp500smart_diff[1:], nlags=30)
```

Since $\Phi^{-1}(0.99) \simeq 2.58$:

```python
T = len(sp500smart_diff)

sig_test = lambda tau_h: np.abs(tau_h) > 2.58/np.sqrt(T)
```

We find the first lag which isn't significant at the 99% level and automatically determine the number of lags needed in our autoregressive model as one below this value.

```python
for i in range(len(pacf)):
    if sig_test(pacf[i]) == False:
        p = i - 1
        print('p set to', p)
        break
```

    p set to 17

This may lead to a high order model, with more lags than strictly necessary. We could view this value, informally, as an upper bound on the number of lags needed. We can also simply identify the order of the model based on the plot of the PACF. In this case, a minimum of approximately 10 lags appears satisfactory, although more may be needed.  We plot the PACF function.

```python
plt.plot(pacf, label='pacf')
plt.plot([2.58/np.sqrt(T)]*30, label='99% confidence interval (upper)')
plt.plot([-2.58/np.sqrt(T)]*30, label='99% confidence interval (lower)')
plt.xlabel('number of lags')
plt.xticks(np.arange(0, 30, 2))
plt.legend();
```

## Determine the order $q$ of the MA model from the ACF plot
Evaluate the ACF on the endogenous variable - SP500 mid-prices

```python
acf = sm.tsa.stattools.acf(sp500smart_diff[1:], nlags=30)
```

    /Users/dez/miniconda3/envs/MLFenv/lib/python3.6/site-packages/statsmodels/tsa/stattools.py:541: FutureWarning: fft=True will become the default in a future version of statsmodels. To suppress this warning, explicitly set fft=False.
      warnings.warn(msg, FutureWarning)

```python
T = len(sp500smart)

sig_test = lambda tau_h: np.abs(tau_h) > 2.58/np.sqrt(T)

q = 0
```

```python
for i in range(len(acf)):
    if sig_test(acf[i]) == False:
        q = i - 1
        print('q set to', q)
        break
```

    q set to 8

```python
plt.plot(acf, label='acf')
plt.plot([2.58/np.sqrt(T)]*30, label='99% confidence interval (upper)')
plt.plot([-2.58/np.sqrt(T)]*30, label='99% confidence interval (lower)')
plt.xlabel('number of lags')
plt.xticks(np.arange(0,30,2))
plt.legend();
```

train on the first 10,000 samples

```python
n_train = 10000
p = 10
model = sm.tsa.ARIMA(endog=sp500smart_diff[1:(n_train+1)],
                     order=(p, 0, q), exog=sp500ofi[0:n_train])
```

    /Users/dez/miniconda3/envs/MLFenv/lib/python3.6/site-packages/statsmodels/tsa/base/tsa_model.py:219: ValueWarning: A date index has been provided, but it has no associated frequency information and so will be ignored when e.g. forecasting.
      ' ignored when e.g. forecasting.', ValueWarning)

```python
model_fit = model.fit(disp=0)
```

    /Users/dez/miniconda3/envs/MLFenv/lib/python3.6/site-packages/statsmodels/base/model.py:492: HessianInversionWarning: Inverting hessian failed, no bse or cov_params available
      'available', HessianInversionWarning)
    /Users/dez/miniconda3/envs/MLFenv/lib/python3.6/site-packages/statsmodels/base/model.py:512: ConvergenceWarning: Maximum Likelihood optimization failed to converge. Check mle_retvals
      "Check mle_retvals", ConvergenceWarning)

```python
model_fit.summary()
```

    /Users/dez/miniconda3/envs/MLFenv/lib/python3.6/site-packages/statsmodels/tsa/arima_model.py:1441: RuntimeWarning: invalid value encountered in sqrt
      return np.sqrt(np.diag(-inv(hess)))
    /Users/dez/miniconda3/envs/MLFenv/lib/python3.6/site-packages/scipy/stats/_distn_infrastructure.py:903: RuntimeWarning: invalid value encountered in greater
      return (a < x) & (x < b)
    /Users/dez/miniconda3/envs/MLFenv/lib/python3.6/site-packages/scipy/stats/_distn_infrastructure.py:903: RuntimeWarning: invalid value encountered in less
      return (a < x) & (x < b)
    /Users/dez/miniconda3/envs/MLFenv/lib/python3.6/site-packages/scipy/stats/_distn_infrastructure.py:1912: RuntimeWarning: invalid value encountered in less_equal
      cond2 = cond0 & (x <= _a)

ARMA Model Results

  Dep. Variable:	       VWAP	         No. Observations:  	    10000

  Model:	            ARMA(10, 8)	     Log Likelihood     	  9235.095

  Method:	             css-mle	       S.D. of innovations	    0.096

  Date:	          Thu, 28 May 2020	   AIC                	 -18428.190

  Time:	              13:02:07	       BIC                	 -18276.772

  Sample:	                0	          HQIC               	 -18376.936

       	          coef	     std err	      z	      P>|z|	  [0.025	    0.975]

  const	           0.0002	     0.000	     0.719	  0.472	    -0.000	     0.001

  OFI	             0.0006	  2.29e-05	    24.093	  0.000	     0.001	     0.001

  ar.L1.VWAP	     -0.0505	     0.214	    -0.236	  0.814	    -0.470	     0.369

  ar.L2.VWAP	      0.4020	     0.194	     2.070	  0.038	     0.021	     0.783

  ar.L3.VWAP	      0.4948	     0.203	     2.436	  0.015	     0.097	     0.893

  ar.L4.VWAP	     -0.2672	     0.600	    -0.446	  0.656	    -1.443	     0.908

  ar.L5.VWAP	     -0.3877	     0.143	    -2.705	  0.007	    -0.669	    -0.107

  ar.L6.VWAP	     -0.1725	     0.349	    -0.495	  0.621	    -0.856	     0.511

  ar.L7.VWAP	      0.3912	     0.240	     1.629	  0.103	    -0.079	     0.862

  ar.L8.VWAP	     -0.0159	     0.117	    -0.136	  0.892	    -0.246	     0.214

  ar.L9.VWAP	     -0.0007	     0.013	    -0.055	  0.956	    -0.027	     0.025

  ar.L10.VWAP	    -0.0502	     0.011	    -4.434	  0.000	    -0.072	    -0.028

  ma.L1.VWAP	     -0.1451	     0.215	    -0.676	  0.499	    -0.566	     0.276

  ma.L2.VWAP	     -0.5515	     0.140	    -3.942	  0.000	    -0.826	    -0.277

  ma.L3.VWAP	     -0.5018	     0.280	    -1.795	  0.073	    -1.050	     0.046

  ma.L4.VWAP	      0.3357	     0.583	     0.576	  0.565	    -0.807	     1.479

  ma.L5.VWAP	      0.3639	       nan	       nan	    nan	       nan	       nan

  ma.L6.VWAP	      0.0997	     0.451	     0.221	  0.825	    -0.783	     0.983

  ma.L7.VWAP	     -0.4640	     0.270	    -1.718	  0.086	    -0.993	     0.065

  ma.L8.VWAP	      0.0352	     0.150	     0.235	  0.814	    -0.259	     0.329

Roots

    	                Real	           Imaginary	          Modulus	          Frequency

  AR.1	            -0.9838	           -0.4982j	            1.1028	           -0.4254

  AR.2	            -0.9838	           +0.4982j	            1.1028	            0.4254

  AR.3	            -0.4090	           -1.0372j	            1.1149	           -0.3098

  AR.4	            -0.4090	           +1.0372j	            1.1149	            0.3098

  AR.5	             0.9763	           -0.7222j	            1.2143	           -0.1014

  AR.6	             0.9763	           +0.7222j	            1.2143	            0.1014

  AR.7	             1.3953	           -0.3335j	            1.4346	           -0.0373

  AR.8	             1.3953	           +0.3335j	            1.4346	            0.0373

  AR.9	            -0.9861	           -1.8350j	            2.0832	           -0.3285

  AR.10	           -0.9861	           +1.8350j	            2.0832	            0.3285

  MA.1	            -0.9722	           -0.5168j	            1.1010	           -0.4222

  MA.2	            -0.9722	           +0.5168j	            1.1010	            0.4222

  MA.3	            -0.4022	           -1.0249j	            1.1010	           -0.3095

  MA.4	            -0.4022	           +1.0249j	            1.1010	            0.3095

  MA.5	             1.0754	           -0.0000j	            1.0754	           -0.0000

  MA.6	             0.9796	           -0.6593j	            1.1808	           -0.0943

  MA.7	             0.9796	           +0.6593j	            1.1808	            0.0943

  MA.8	            12.8807	           -0.0000j	           12.8807	           -0.0000

# Forecast

```python
start_index = n_train
horizon = 500
end_index = start_index + horizon
forecast = model_fit.predict(start=start_index, end=end_index, exog=sp500ofi)
```

    /Users/dez/miniconda3/envs/MLFenv/lib/python3.6/site-packages/statsmodels/tsa/base/tsa_model.py:576: ValueWarning: No supported index is available. Prediction results will be given with an integer index beginning at `start`.
      ValueWarning)

```python
forecast.index = sp500smart_diff[n_train:n_train + horizon + 1].index
```

```python
plt.plot(sp500smart_diff[n_train:(n_train + horizon + 1)], color='black', label='Delta VWAP')
plt.plot(forecast, color='blue', label='forecast')
plt.title('Forecast of VWAP changes versus Actual')
plt.xlabel('time')
plt.ylabel('delta VWAP')
plt.legend();
```