Rough volatility with Python¶

Jim Gatheral

For Python Quants, New York, Friday May 6, 2016

Acknowledgements¶

The code in this iPython notebook used to be in R.  I am very grateful to Yves Hilpisch and Michael Schwed for translating my R-code to Python.

For slideshow functionality I use RISE by Damián Avila.

$$
\newcommand{\beas}{\begin{eqnarray*}}
\newcommand{\eeas}{\end{eqnarray*}}
\newcommand{\bea}{\begin{eqnarray}}
\newcommand{\eea}{\end{eqnarray}}
\newcommand{\ben}{\begin{enumerate}}
\newcommand{\een}{\end{enumerate}}
\newcommand{\bi}{\begin{itemize}}
\newcommand{\ei}{\end{itemize}}
\newcommand{\beq}{\begin{equation}}
\newcommand{\eeq}{\end{equation}}
\newcommand{\bv}{\begin{verbatim}}
\newcommand{\ev}{\end{verbatim}}
\newcommand{\E}{\mathbb{E}}
\newcommand{\R}{\mathbb{R}}
\newcommand{\mP}{\mathbb{P}}
\newcommand{\mQ}{\mathbb{Q}}
\newcommand{\sigl}{\sigma_L}
\newcommand{\BS}{\rm BS}
\newcommand{\vix}{\text{VIX}}
\newcommand{\p}{\partial}
\newcommand{\var}{{\rm var}}
\newcommand{\cov}{{\rm cov}}
\newcommand{\mt}{\mathbf{t}}
\newcommand{\mS}{\mathbf{S}}
\newcommand{\tC}{\widetilde{C}}
\newcommand{\hC}{\widehat{C}}
\newcommand{\cE}{\mathcal{E}}
\newcommand{\tH}{\widetilde{H}}
\newcommand{\cD}{\mathcal{D}}
\newcommand{\cM}{\mathcal{M}}
\newcommand{\cS}{\mathcal{S}}
\newcommand{\cR}{\mathcal{R}}
\newcommand{\cF}{\mathcal{F}}
\newcommand{\cV}{\mathcal{V}}
\newcommand{\cG}{\mathcal{G}}
\newcommand{\cv}{\mathcal{v}}
\newcommand{\cg}{\mathcal{g}}
\newcommand{\cL}{\mathcal{L}}
\newcommand{\cO}{\mathcal{O}}
\newcommand{\dt}{\Delta t}
\newcommand{\tr}{{\rm tr}}
\newcommand{\sgn}{\mathrm{sign}}
\newcommand{\ee}[1]{{\mathbb{E}\left[{#1}\right]}}
\newcommand{\eef}[1]{{\mathbb{E}\left[\left.{#1}\right|\cF_t\right]}}
\newcommand{\eefm}[2]{{\mathbb{E}^{#2}\left[\left.{#1}\right|\cF_t\right]}}
\newcommand{\angl}[1]{{\langle{#1}\rangle}}
$$

Outline of presentation¶

The time series of historical volatility

Scaling properties

The RFSV model

Pricing under rough volatility

Forecasting realized variance

The time series of variance swaps

Relating historical and implied

The time series of realized variance¶

Assuming an underlying variance process $v_s$, integrated variance $
\frac 1 \delta \,\int_t^{t+\delta}\,v_s\,ds$ may (in principle) be estimated arbitrarily accurately given enough price data.

In practice, market microstructure noise makes estimation harder at very high frequency.

Sophisticated estimators of integrated variance have been developed to adjust for market microstructure noise.  See Gatheral and Oomen [6] (for example) for details of these.

The Oxford-Man Institute of Quantitative Finance makes historical realized variance (RV) estimates freely available at http://realized.oxford-man.ox.ac.uk.  These estimates are updated daily.

Each day, for 21 different indices, all trades and quotes are used to estimate realized (or integrated) variance over the trading day from open to close.

Using daily RV estimates as proxies for instantaneous variance, we may investigate the time series properties of $v_t$ empirically.

First load all necessary Python libraries.

import warnings; warnings.simplefilter('ignore')
import datetime as dt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.mlab as mlab
from matplotlib.mlab import stineman_interp
import pandas as pd
import pandas.io.data as web
import requests
import zipfile as zi
import StringIO as sio
from sklearn import datasets, linear_model
import scipy.special as scsp
import statsmodels.api as sm
import math
import seaborn as sns; sns.set()
%matplotlib inline

Then update and save the latest Oxford-Man data.

url = 'http://realized.oxford-man.ox.ac.uk/media/1366/'
url += 'oxfordmanrealizedvolatilityindices.zip'
data = requests.get(url, stream=True).content
z = zi.ZipFile(sio.StringIO(data))
z.extractall()

There are many different estimates of realized variance, all of them very similar.  We will use the realized kernel estimates denoted by ".rk".

df = pd.read_csv('OxfordManRealizedVolatilityIndices.csv', index_col=0, header=2 )
rv1 = pd.DataFrame(index=df.index)
for col in df.columns:
    if col[-3:] == '.rk':
        rv1[col] = df[col]
rv1.index = [dt.datetime.strptime(str(date), "%Y%m%d") for date in rv1.index.values]

```julia
print("Hello world")
```

    Hello world

Let's plot SPX realized variance.

```julia

spx = pd.DataFrame(rv1['SPX2.rk'])
spx.plot(color='red', grid=True, title='SPX realized variance',
         figsize=(16, 9), ylim=(0,0.003));

```

Figure 1: Oxford-Man KRV estimates of SPX realized variance from January 2000 to the current date.

```julia

spx.head()

```

```julia

spx.tail()

```

We can get SPX data from Yahoo using the DataReader function:

```julia

SPX = web.DataReader(name = '^GSPC',data_source = 'yahoo', start='2000-01-01')
SPX = SPX['Adj Close']
SPX.plot(title='SPX',figsize=(14, 8));

```

The smoothness of the volatility process¶

For $q\geq 0$, we define the $q$th sample moment of differences of log-volatility at a given lag $\Delta$.($\angl{\cdot}$ denotes the sample average):

$$
m(q,\Delta)=\angl{\left|\log \sigma_{t+\Delta} -\log \sigma_{t} \right|^q}
$$
For example

$$
m(2,\Delta)=\angl{\left(\log \sigma_{t+\Delta} -\log \sigma_{t} \right)^2}
$$
is just the sample variance of differences in log-volatility at the lag $\Delta$.

Scaling of $m(q,\Delta)$ with lag $\Delta$¶

```julia

spx['sqrt']= np.sqrt(spx['SPX2.rk'])
spx['log_sqrt'] = np.log(spx['sqrt'])

def del_Raw(q, x):
    return [np.mean(np.abs(spx['log_sqrt'] - spx['log_sqrt'].shift(lag)) ** q)
            for lag in x]

```

```julia

plt.figure(figsize=(8, 8))
plt.xlabel('$log(\Delta)$')
plt.ylabel('$log\  m(q.\Delta)$')
plt.ylim=(-3, -.5)

zeta_q = list()
qVec = np.array([.5, 1, 1.5, 2, 3])
x = np.arange(1, 100)
for q in qVec:
    plt.plot(np.log(x), np.log(del_Raw(q, x)), 'o')
    model = np.polyfit(np.log(x), np.log(del_Raw(q, x)), 1)
    plt.plot(np.log(x), np.log(x) * model[0] + model[1])
    zeta_q.append(model[0])

print zeta_q

```

Figure 2: $\log m(q,\Delta)$ as a function of $\log \Delta$, SPX.

Monofractal scaling result¶

From the above log-log plot, we see that for each $q$, $m(q,\Delta) \propto \Delta ^{\zeta_q}$.

How does $\zeta_q$ scale with $q$?

Scaling of $\zeta_q$ with $q$¶

```julia

plt.figure(figsize=(8,8))
plt.xlabel('q')
plt.ylabel('$\zeta_{q}$')
plt.plot(qVec, zeta_q, 'or')

line = np.polyfit(qVec[:4], zeta_q[:4],1)
plt.plot(qVec, line[0] * qVec + line[1])
h_est= line[0]
print(h_est)

```

Figure 3: Scaling of $\zeta_q$ with $q$.

We find the monofractal scaling relationship

$$
\zeta_q = q\,H
$$
with $H \approx 0.13$.

Note however that $H$ does vary over time, in a narrow range.

Note also that our estimate of $H$ is biased high because we proxied instantaneous variance $v_t$ with its average over each day $\frac 1T\,\int_0^T\,v_t\,dt$, where $T$ is one trading day.

Estimated $H$ for all indices¶

We now repeat this analysis for all 21 indices in the Oxford-Man dataset.

```julia

def dlsig2(sic, x, pr=False):
    if pr:
        a= np.array([(sig-sig.shift(lag)).dropna() for lag in x])
        a=a ** 2
        print a.info()
    return [np.mean((sig-sig.shift(lag)).dropna() ** 2) for lag in x]

```

```julia

h = list()
nu = list()

for col in rv1.columns:
    sig = rv1[col]
    sig = np.log(np.sqrt(sig))
    sig = sig.dropna()
    model = np.polyfit(np.log(x), np.log(dlsig2(sig, x)), 1)
    nu.append(np.sqrt(np.exp(model[1])))
    h.append(model[0]/2.)

OxfordH = pd.DataFrame({'names':rv1.columns, 'h_est': h, 'nu_est': nu})

```

```julia

OxfordH

```

Distributions of $(\log \sigma_{t+\Delta}-\log \sigma_t)$ for various lags $\Delta$¶

Having established these beautiful scaling results for the moments, how do the histograms look?

```julia

def plotScaling(j, scaleFactor):
    col_name = rv1.columns[j]
    v = rv1[col_name]
    x = np.arange(1,101)

    def xDel(x, lag):
        return x-x.shift(lag)

    def sdl(lag):
        return (xDel(np.log(v), lag)).std()

    sd1 = (xDel(np.log(v), 1)).std()
    h = OxfordH['h_est'][j]
    f, ax = plt.subplots(2,2,sharex=False, sharey=False, figsize=(10, 10))

    for i_0 in range(0, 2):
        for i_1 in range(0, 2):
            la = scaleFactor ** (i_1*1+i_0*2)

            hist_val = xDel(np.log(v), la).dropna()
            std = hist_val.std()
            mean = hist_val.mean()

            ax[i_0][i_1].set_title('Lag = %s Days' %la)
            n, bins, patches = ax[i_0][i_1].hist(hist_val.values, bins=100,
                                   normed=1, facecolor='green',alpha=0.2)
            ax[i_0][i_1].plot(bins, mlab.normpdf(bins,mean,std), "r")
            ax[i_0][i_1].plot(bins, mlab.normpdf(bins,0,sd1 * la ** h), "b--")
            hist_val.plot(kind='density', ax=ax[i_0][i_1])

```

```julia

 plotScaling(1,5)

```

Figure 4: Histograms of $(\log \sigma_{t+\Delta}-\log \sigma_t)$ for various lags $\Delta$; normal fit in red; $\Delta=1$ normal fit scaled by $\Delta^{0.14}$ in blue.

Universality?¶

[Gatheral, Jaisson and Rosenbaum][5]</a></sup> compute daily realized variance estimates over one hour windows for DAX and Bund futures contracts, finding similar scaling relationships.

We have also checked that Gold and Crude Oil futures scale similarly.

Although the increments $(\log \sigma_{t+\Delta}-\log \sigma_t)$ seem to be fatter tailed than Gaussian.

A natural model of realized volatility¶

As noted originally by [Andersen et al.][1]</a></sup>, distributions of differences in the log of realized volatility are close to Gaussian.

This motivates us to model $\sigma_t$ as a lognormal random variable.

Moreover, the scaling property of variance of RV differences suggests the model:

(1)
$$
\log \sigma_{t+\Delta} - \log \sigma_t =\nu\,\left( W^H_{t+\Delta}-W^H_t\right)
$$

where $W^H$ is fractional Brownian motion.

In  [Gatheral, Jaisson and Rosenbaum][5]</a></sup>, we refer to a stationary version of (1) as the RFSV (for Rough Fractional Stochastic Volatility) model.

Fractional Brownian motion (fBm)¶

Fractional Brownian motion (fBm) $\{W^H_t; t \in \mathbb{R}\}$ is the unique Gaussian process with mean zero and autocovariance function
$$
\ee{ W^H_t\,W^H_s  } = \frac12\,\left\{ |t|^{2\,H}+|s|^{2\,H}-|t-s|^{2\,H}  \right\}
$$
where $H \in (0,1)$ is called the Hurst index or parameter.

In particular, when $H=1/2$, fBm is just Brownian motion.

If $H>1/2$, increments are positively correlated.% so the process is trending.

If $H<1/2$, increments are negatively correlated.% so the process is reverting.

Representations of fBm¶

There are infinitely many possible representations of fBm in terms of Brownian motion.  For example, with $\gamma = \frac 12 - H$,

Mandelbrot-Van Ness

$$
W^H_t ={C_H}\,\left\{\int_{-\infty}^t \,\frac{dW_s}{(t-s)^\gamma} - \int_{-\infty}^0 \,\frac{dW_s}{(-s)^\gamma}\right\}.
$$

The choice

$$
C_H = \sqrt{ \frac{2\,H\,\Gamma(3/2-H)}{\Gamma(H+1/2)\,\Gamma(2-2\,H)}}
$$
ensures that

$$
\ee{W^H_t\,W^H_s }= \frac{1}{2}\,\left\{t^{2 H} + s^{2 H} - |t-s|^{2 H}\right\}.
$$

Does simulated RSFV data look real?¶

Figure 8: Volatility of SPX (above) and of the RFSV model (below).

Remarks on the comparison¶

The simulated and actual graphs look very alike.

Persistent periods of high volatility alternate with low volatility periods.

$H \sim 0.1$ generates very rough looking sample paths (compared with $H=1/2$ for Brownian motion).

Hence rough volatility.

On closer inspection, we observe fractal-type behavior.

The graph of volatility over a small time period looks like the same graph over a much longer time period.

This feature of volatility has been investigated both empirically and theoretically in, for example, [Bacry and Muzy][3]</a></sup>
.

In particular, their Multifractal Random Walk (MRW) is related to a limiting case of the RSFV model as $H \to 0$.

Pricing under rough volatility¶

The foregoing behavior suggest the following model (see [Bayer et al.][2]</a></sup> for volatility under the real (or historical or physical) measure $\mP$:

$$
 \log \sigma_t =\nu\,W^H_t.
$$
Let $\gamma=\frac{1}{2}-H$.  We choose the Mandelbrot-Van Ness representation of fractional Brownian motion $W^H$ as follows:

$$
W^H_t ={C_H}\,\left\{\int_{-\infty}^t \,\frac{dW^{\mP}_s}{(t-s)^\gamma} - \int_{-\infty}^0 \,\frac{dW^{\mP}_s}{(-s)^\gamma}\right\}.
$$

Then

$$
\begin{eqnarray}
&&\log v_u - \log v_t \nonumber\\
&=&\nu\,C_H\,\left\{ \int_t^u\,\frac{1}{(u-s)^\gamma}\,d{W}^{\mP}_s  +\int_{-\infty}^t\,\left[ \frac{1}{(u-s)^\gamma}-\frac{1}{(t-s)^\gamma} \right]\,d{W}^{\mP}_s\right\}\nonumber\\
&=:& 2\,\nu\,C_H\,\left[M_t(u)+ Z_t(u)\right].
\end{eqnarray}
$$

Note that $\eefm{M_t(u)}{\mP}=0$ and $Z_t(u)$ is $\cF_t$-measurable.

To price options, it would seem that we would need to know $\cF_t$, the entire history of the Brownian motion $W_s$ for $s<t>
</t>

Pricing under $\mP$¶

Let

$$
\tilde W^{\mP}_t(u) := \sqrt{2\,H}\,\int_t^u\,\frac{dW^{\mP}_s}{(u-s)^\gamma}
$$
With
$\eta := 2\,\nu\,C_H/\sqrt{2\,H}$ we have $2\,\nu\,C_H\, M_t(u)
= \eta\, \tilde W^{\mP}_t(u)$ so denoting the stochastic exponential by $\cE(\cdot)$, we may write

$$
\begin{eqnarray}
v_u &=& v_t \exp\left\{  \eta \tilde W^{\mP}_t(u) +
 2\,\nu\,C_H\,
Z_t(u) \right\}\nonumber\\
&=& \eefm{v_u}{\mP}\,\cE \left(\eta\,\tilde W^{\mP}_t(u) \right).
%\label{eq:rBergomiP}
\end{eqnarray}
$$

The conditional distribution of $v_u$ depends on $\cF_t$ only through the variance forecasts $\eefm{v_u}{\mP}$,

To price options, one does not need to know $\cF_t$, the entire history of the Brownian motion $W_s^{\mP}$ for $s<t>
</t>

Pricing under $\mQ$¶

Our model under $\mP$ reads:

(2)
$$
v_u =\eefm{v_u}{\mP}\,\cE\left(\eta\,\tilde W^{\mP}_t(u) \right).
%\label{eq:Pmodel}
$$

Consider some general change of measure

$$
dW^{\mP}_s = dW^{\mQ}_s + \lambda_s\,ds,
%\label{eq:dQdP}
$$
where $\{ \lambda_s: s > t \}$  has a natural interpretation as the price of volatility risk.

We may then rewrite (2) as

$$
v_u
=  \eefm{v_u}{\mP}\,\cE\left(\eta\,\tilde W^{\mQ}_t(u) \right)
\exp \left\{ \eta\,\sqrt{2\,H}\, \int_t^u\,\frac{\lambda_s}{(u-s)^\gamma}\,ds\right\}.
%\label{eq:explicitBergomiQ1}
$$

Although the conditional distribution of $v_u$ under $\mP$ is lognormal, it will not be lognormal in general under $\mQ$.

The upward sloping smile in VIX options means $\lambda_s$ cannot be deterministic in this picture.

The rough Bergomi (rBergomi) model¶

Let's nevertheless consider the simplest change of measure

$$
d{W}^{\mP}_s = d{W}^{\mQ}_s + \lambda(s)\,ds,
$$
where $\lambda(s)$ is a deterministic function of $s$.  Then from (2), we would have

$$
\begin{eqnarray}
v_u
&=&  \eefm{v_u}{\mP}\,\cE\left(\eta\,\tilde W^{\mQ}_t(u) \right)
\exp \left\{ \eta\,\sqrt{2\,H}\,  \int_t^u\,\frac{1}{(u-s)^\gamma}\,\lambda(s)\,ds\right\}\nonumber\\
&=& \xi_t(u) \,\cE\left(\eta\,\tilde W^{\mQ}_t(u) \right)%\label{eq:explicitBergomiQ}
\end{eqnarray}
$$
where the forward variances $\xi_t(u) =  \eefm{v_u}{\mQ}$ are (at least in principle) tradable and  observed in the market.

$\xi_t(u)$ is the product of two terms:

$ \eefm{v_u}{\mP}$ which depends on the historical path $\{W_s, s<t brownian="" motion="" of="" the="">

a term which depends on the price of risk $\lambda(s)$.

</t>

Features of the rough Bergomi model¶

The rBergomi model is a non-Markovian generalization of the Bergomi model:
$$
\eef{v_u}\neq \E[v_u|v_t].
$$

The rBergomi model is Markovian in the (infinite-dimensional) state vector $\eefm{v_u}{\mQ}=\xi_t(u)$.

We have achieved our aim from Session 1 of replacing the exponential kernels in the Bergomi model with a power-law kernel.

We may therefore expect that the rBergomi model will generate a realistic term structure of ATM volatility skew.

Re-interpretation of the conventional Bergomi model¶

A conventional $n$-factor Bergomi model is not self-consistent for an arbitrary choice of the initial forward variance curve $\xi_t(u)$.

$\xi_t(u)=\eef{v_u}$ should be consistent with the assumed dynamics.

Viewed from the perspective of the fractional Bergomi model however:

The initial curve $\xi_t(u)$ reflects the history $\{W_s; s<t brownian="" driving="" motion="" of="" the="" time="" to="" up="">

The exponential kernels in the exponent of the conventional Bergomi model approximate more realistic power-law kernels.

</t>

The conventional two-factor Bergomi model is then justified in practice as a tractable Markovian engineering approximation to a more realistic fractional Bergomi model.

The stock price process¶

The observed anticorrelation between price moves and volatility moves may be  modeled naturally by anticorrelating the Brownian motion $W$ that drives the volatility process with the Brownian motion driving the price process.

Thus
$$
\frac{dS_t}{S_t}=\sqrt{v_t}\,dZ_t
$$
with
$$
dZ_t = \rho\,dW_t + \sqrt{1-\rho^2}\,dW^\perp_t
$$
where $\rho$ is the correlation between volatility moves and price moves.

Simulation of the  rBergomi model¶

We simulate the rBergomi model as follows:

Construct the  joint covariance matrix for the Volterra process $\tilde
W$ and the Brownian motion $Z$ and compute its Cholesky decomposition.

For each time, generate iid normal random vectors {and
  multiply them by the lower-triangular matrix obtained by the Cholesky
  decomposition} to get a $m \times 2\,n$ matrix of paths of $\tilde W$     and $Z$ with the correct joint marginals.

With these paths held in memory, we may evaluate the expectation under $\mQ$ of any payoff of interest.

This procedure is very slow!

Speeding up the simulation is work in progress.

Guessing rBergomi model parameters¶

The rBergomi model has only three parameters: $H$, $\eta$ and $\rho$.

If we had a fast simulation, we could just iterate on these parameters to find the best fit to observed option prices.  But we don't.

However, the model parameters $H$, $\eta$ and $\rho$ have very direct interpretations:

$H$ controls the decay of ATM skew $\psi(\tau)$ for very short expirations.

The product $\rho\,\eta$ sets the level of the ATM skew for longer expirations.

Keeping  $\rho\,\eta$ constant but decreasing $\rho$ (so as to make it more negative) pushes the minimum of each smile towards higher strikes.

So we can guess parameters in practice.

As we will see, even without proper calibration (i.e. just guessing parameters), rBergomi model fits to the volatility surface are amazingly good.

SPX smiles in the rBergomi model¶

In Figures 9 and 10, we show how well a rBergomi model simulation with guessed parameters fits the SPX option market as of February 4, 2010, a day when the ATM volatility term structure happened to be pretty flat.

rBergomi parameters were: $H=0.07$, $\eta=1.9$, $\rho=-0.9$.

Only three parameters to get a very good fit to the whole SPX volatility surface!

rBergomi fits to SPX smiles as of 04-Feb-2010¶

Figure 9: Red and blue points represent bid and offer SPX implied volatilities; orange smiles are from the rBergomi simulation.

Shortest dated smile as of February 4, 2010¶

Figure 10: Red and blue points represent bid and offer SPX implied volatilities; orange smile is from the rBergomi simulation.

ATM volatilities and skews¶

In Figures 11 and 12, we see just how well the rBergomi model can match empirical skews and vols.  Recall also that the parameters we used are just guesses!

Term structure of ATM skew as of February 4, 2010¶

Figure 11: Blue points are empirical skews; the red line is from the rBergomi simulation.

Term structure of ATM vol as of February 4, 2010¶

Figure 12: Blue points are empirical ATM volatilities; the red line is from the rBergomi simulation.

Another date¶

Now we take a look at another date: August 14, 2013, two days before the last expiration date in our dataset.

Options set at the open of August 16, 2013 so only one trading day left.

Note in particular that the extreme short-dated smile is well reproduced by the rBergomi model.

There is no need to add jumps!

SPX smiles as of August 14, 2013¶

Figure 13: Red and blue points represent bid and offer SPX implied volatilities; orange smiles are from the rBergomi simulation.

The forecast formula¶

In the RFSV model (1), $\log \sigma_t \approx \nu\,W^H_t+C$ for some constant $C$.

[Nuzman and Poor][7]</a></sup> show that $W^H_{t+\Delta}$ is conditionally Gaussian with conditional expectation

$$\E[W^H_{t+\Delta}|\cF_t]=\frac{\cos(H\pi)}{\pi} \Delta^{H+1/2} \int_{-\infty}^t \frac{W^H_s}{(t-s+\Delta)(t-s)^{H+1/2}} ds
$$

and conditional variance

$$
\text{Var}[W^H_{t+\Delta}|\cF_t]=c\,\Delta^{2H}.
$$
where $$
c = \frac{\Gamma(3/2-H)}{\Gamma(H+1/2)\,\Gamma(2-2H)}.
$$

The forecast formula¶

Thus, we obtain

Variance forecast formula

(3)
$$
\eefm{v_{t+\Delta}}{\mP}=\exp\left\{\eefm{\log(v_{t+\Delta})}{\mP}+2\, c\,\nu^2\Delta^{2\,H}\right\}
%\label{eq:vForecast}
$$

where

$$
\beas
&&\eefm{\log v_{t+\Delta}}{\mP}\\
&&= \frac{\cos(H\pi)}{\pi} \Delta^{H+1/2} \int_{-\infty}^t \frac{\log v_s}{(t-s+\Delta)(t-s)^{H+1/2}} ds.
\eeas
$$

Implement variance forecast in Python¶

```julia

def c_tilde(h):
    return scsp.gamma(3. / 2. - h) / scsp.gamma(h + 1. / 2.) * scsp.gamma(2. - 2. * h)

def forecast_XTS(rvdata, h, date, nLags, delta, nu):
    i = np.arange(nLags)
    cf = 1./((i + 1. / 2.) ** (h + 1. / 2.) * (i + 1. / 2. + delta))
    ldata = rvdata.truncate(after=date)
    l = len(ldata)
    ldata = np.log(ldata.iloc[l - nLags:])
    ldata['cf'] = np.fliplr([cf])[0]
    # print ldata
    ldata = ldata.dropna()
    fcst = (ldata.iloc[:, 0] * ldata['cf']).sum() / sum(ldata['cf'])

    return math.exp(fcst + 2 * nu ** 2 * c_tilde(h) * delta ** (2 * h))

```

SPX actual vs forecast variance¶

```julia

rvdata = pd.DataFrame(rv1['SPX2.rk'])
nu  = OxfordH['nu_est'][0] # Vol of vol estimate for SPX
h = OxfordH['h_est'][0]
n = len(rvdata)
delta = 1
nLags = 500
dates = rvdata.iloc[nLags:n-delta].index
rv_predict = [forecast_XTS(rvdata, h=h, date=d, nLags=nLags,
                           delta=delta, nu=nu) for d in dates]
rv_actual = rvdata.iloc[nLags+delta:n].values

```

Scatter plot of delta days ahead predictions¶

```julia

plt.figure(figsize=(8, 8))
plt.plot(rv_predict, rv_actual, 'bo');

```

Figure 14: Actual vols vs predicted vols.

Superimpose actual and predicted vols¶

```julia

plt.figure(figsize=(11, 6))
vol_actual = np.sqrt(np.multiply(rv_actual,252))
vol_predict = np.sqrt(np.multiply(rv_predict,252))
plt.plot(vol_actual, "b")
plt.plot(vol_predict, "r");

```

Figure 15: Actual volatilities in blue; predicted vols in red.

Forecasting the variance swap curve¶

Finally, we forecast the whole variance swap curve using the variance forecasting formula (3).

```julia

def xi(date, tt, nu,h, tscale):  # dt=(u-t) is in units of years
    rvdata = pd.DataFrame(rv1['SPX2.rk'])
    return [ forecast_XTS(rvdata,h=h,date=date,nLags=500,delta=dt*tscale,nu=nu) for dt in tt]

nu = OxfordH["nu_est"][0]
h = OxfordH["h_est"][0]

def varSwapCurve(date, bigT, nSteps, nu, h, tscale, onFactor):
  # Make vector of fwd variances
  tt = [ float(i) * (bigT) / nSteps for i in range(nSteps+1)]
  delta_t = tt[1]
  xicurve = xi(date, tt, nu, h, tscale)
  xicurve_mid = (np.array(xicurve[0:nSteps]) + np.array(xicurve[1:nSteps+1])) / 2
  xicurve_int = np.cumsum(xicurve_mid) * delta_t
  varcurve1 = np.divide(xicurve_int, np.array(tt[1:]))
  varcurve = np.array([xicurve[0],]+list(varcurve1))
  varcurve = varcurve * onFactor * tscale #  onFactor is to compensate for overnight moves
  res = pd.DataFrame({"texp":np.array(tt), "vsQuote":np.sqrt(varcurve)})
  return(res)

```

```julia

def varSwapForecast(date,tau,nu,h,tscale,onFactor):
  vsc = varSwapCurve(date, bigT=2.5, nSteps=100, nu=nu, h=h,
                    tscale=tscale, onFactor=onFactor) # Creates the whole curve
  x = vsc['texp']
  y = vsc['vsQuote']
  res = stineman_interp(tau,x,y,None)

  return(res)

# Test the function

tau = (.25,.5,1,2)
date = dt.datetime(2008,9,8)
varSwapForecast(date,tau,nu=nu,h=h,tscale=252,onFactor=1)

```

'Constructing a time series of variance swap curves¶

For each of 2,658 days from Jan 27, 2003 to August 31, 2013:

We compute proxy variance swaps from closing prices of SPX options sourced from OptionMetrics (www.optionmetrics.com) via WRDS.

We form the forecasts $\eefm{v_u}{\mP}$ using (3) with 500 lags of SPX RV data sourced from The Oxford-Man Institute of Quantitative Finance (http://realized.oxford-man.ox.ac.uk).

We note that the actual variance swap curve is a factor (of roughly 1.4) higher than the forecast, which we may attribute to a combination of overnight movements of the index and the price of volatility risk.

Forecasts must therefore be rescaled to obtain close-to-close realized variance forecasts.

3-month forecast vs actual variance swaps¶

Figure 16: Actual (proxy) 3-month variance swap quotes in blue vs forecast in red (with no scaling factor).

Ratio of actual to forecast¶

Figure 17: The ratio between 3-month actual variance swap quotes and 3-month forecasts.

The Lehman weekend¶

Empirically, it seems that the variance curve is a simple scaling factor times the forecast, but that this scaling factor is time-varying.

We can think of this factor as having two multiplicative components: the overnight factor, and the price of volatility risk.

Recall that as of the close on Friday September 12, 2008, it was widely believed that Lehman Brothers would be rescued over the weekend. By Monday morning, we knew that Lehman had failed.

In Figure 18, we see that variance swap curves just before and just after the collapse of Lehman are just rescaled versions of the RFSV forecast curves.

We need variance swap estimates for 12-Sep-2008 and 15-Sep-2008¶

We proxy these by taking SVI fits for the two dates and computing the log-strips.

```julia

varSwaps12 =(
    0.2872021, 0.2754535, 0.2601864, 0.2544684, 0.2513854, 0.2515314,
    0.2508418, 0.2520099, 0.2502763, 0.2503309, 0.2580933, 0.2588361,
    0.2565093)

texp12 = (
    0.01916496, 0.04654346, 0.09582478, 0.19164956, 0.26830938, 0.29842574,
    0.51745380, 0.54483231, 0.76659822, 0.79397673, 1.26488706, 1.76317591,
    2.26146475)

varSwaps15 = (
    0.4410505, 0.3485560, 0.3083603, 0.2944378, 0.2756881, 0.2747838,
    0.2682212, 0.2679770, 0.2668113, 0.2706713, 0.2729533, 0.2689598,
    0.2733176)

texp15 = (
    0.01095140, 0.03832991, 0.08761123, 0.18343600, 0.26009582, 0.29021218,
    0.50924025, 0.53661875, 0.75838467, 0.78576318, 1.25667351, 1.75496235,
    2.25325120)

```

Actual vs predicted over the Lehman weekend¶

```julia

nu = OxfordH['nu_est'][0]
h = OxfordH['h_est'][0]
date1 = dt.datetime(2008, 9, 12)
date2 = dt.datetime(2008, 9, 15)

# Variance curve fV model forecasts
tau1000 = [ float(i) * 2.5 / 1000. for i in range(1,1001)]
vs1 = varSwapForecast(date1, tau1000, nu=nu,h=h, tscale=252, onFactor=1.29)
vs2 = varSwapForecast(date2, tau1000, nu=nu,h=h, tscale=252, onFactor=1.29)

```

```julia

plt.figure(figsize=(11, 6))
plt.plot(texp12, varSwaps12, "r")
plt.plot(texp15, varSwaps15, "b")
plt.plot(tau1000, vs1, "r--")
plt.plot(tau1000, vs2, "b--");

```

Figure 18: SPX variance swap curves as of September 12, 2008 (red) and September 15, 2008 (blue). The dashed curves are RFSV model forecasts rescaled by the 3-month ratio ($1.29$) as of the Friday close.

Remarks¶

We note that

The actual variance swaps curves are very close to the forecast curves, up to a scaling factor.

We are able to explain the change in the variance swap curve with only one extra observation: daily variance over the trading day on Monday 15-Sep-2008.

The SPX options market appears to be backward-looking in a very sophisticated way.

The Flash Crash¶

The so-called Flash Crash of Thursday May 6, 2010 caused  intraday realized variance to be much higher than normal.

In Figure 19, we plot the actual variance swap curves as of the Wednesday and Friday market closes together with forecast curves rescaled by the 3-month ratio as of the close on Wednesday May 5 (which was $2.52$).

We see that the actual variance curve as of the close on Friday is consistent with a forecast from the time series of realized variance that includes the anomalous price action of Thursday May 6.

Variance swap estimates¶

We again proxy variance swaps for 05-May-2010, 07-May-2010 and 10-May-2010 by taking SVI fits (see [Gatheral and Jacquier][4]</a></sup> ) for the three dates and computing the log-strips.

```julia

varSwaps5 = (
    0.4250369, 0.2552473, 0.2492892, 0.2564899, 0.2612677, 0.2659618, 0.2705928, 0.2761203,
    0.2828139, 0.2841165, 0.2884955, 0.2895839, 0.2927817, 0.2992602, 0.3116500)

texp5 = (
    0.002737851, 0.043805613, 0.120465435, 0.150581793, 0.197125257, 0.292950034,
    0.369609856, 0.402464066, 0.618754278, 0.654346338, 0.867898700, 0.900752909,
    1.117043121, 1.615331964, 2.631074606)

varSwaps7 = (
    0.5469727, 0.4641713, 0.3963352, 0.3888213, 0.3762354, 0.3666858, 0.3615814, 0.3627013,
    0.3563324, 0.3573946, 0.3495730, 0.3533829, 0.3521515, 0.3506186, 0.3594066)

texp7 = (
    0.01642710, 0.03832991, 0.11498973, 0.14510609, 0.19164956, 0.28747433, 0.36413415,
    0.39698836, 0.61327858, 0.64887064, 0.86242300, 0.89527721, 1.11156742, 1.60985626,
    2.62559890)

varSwaps10 = (
    0.3718439, 0.3023223, 0.2844810, 0.2869835, 0.2886912, 0.2905637, 0.2957070, 0.2960737,
    0.3005086, 0.3031188, 0.3058492, 0.3065815, 0.3072041, 0.3122905, 0.3299425)

texp10 = (
    0.008213552, 0.030116359, 0.106776181, 0.136892539, 0.183436003, 0.279260780,
    0.355920602, 0.388774812, 0.605065024, 0.640657084, 0.854209446, 0.887063655,
    1.103353867, 1.601642710, 2.617385352)

```

```julia

date1 = dt.datetime(2010, 5, 5)
date2 = dt.datetime(2010, 5, 7)

vsf5 = varSwapCurve(date1, bigT=2.5, nSteps=100, nu=nu, h=h, tscale=252, onFactor=2.52)
vsf7 = varSwapCurve(date2, bigT=2.5, nSteps=100, nu=nu, h=h, tscale=252, onFactor=2.52)

```

```julia

plt.figure(figsize=(11, 6))
plt.plot(texp5, varSwaps5, "r", label='May 5')
plt.plot(texp7, varSwaps7, "g", label='May 7')

plt.legend()
plt.xlabel("Time to maturity")
plt.ylabel("Variance swap quote")

plt.plot(vsf5['texp'], vsf5['vsQuote'], "r--")
plt.plot(vsf7['texp'], vsf7['vsQuote'], "g--");

```

Figure 19: SPX variance swap curves as of May 5, 2010 (red) and May 7, 2010 (green). The dashed curves are RFSV model forecasts rescaled by the 3-month ratio ($2.52$) as of the close on Wednesday May 5.  The curve as of the close on May 7 is consistent with the forecast including the crazy moves on May 6.

The weekend after the Flash Crash¶

Now we plot forecast and actual variance swap curves as of the close on Friday May 7 and Monday May 10.

```julia

date1 = dt.datetime(2010,5,7)
date2 = dt.datetime(2010,5,10)

vsf7 = varSwapCurve(date1, bigT=2.5, nSteps=100, nu=nu, h=h, tscale=252, onFactor=2.52)
vsf10 = varSwapCurve(date2, bigT=2.5, nSteps=100, nu=nu, h=h, tscale=252, onFactor=2.52)

```

```julia

plt.figure(figsize=(11, 6))
plt.plot(texp7, varSwaps7, "g", label='May 7')
plt.plot(texp10, varSwaps10, "m", label='May 10')

plt.legend()
plt.xlabel("Time to maturity")
plt.ylabel("Variance swap quote")

plt.plot(vsf7['texp'], vsf7['vsQuote'], "g--")
plt.plot(vsf10['texp'], vsf10['vsQuote'], "m--");

```

Figure 20: The May 10 actual curve is  inconsistent with a forecast that includes the Flash Crash.

Now let's see what happens if we exclude the Flash Crash from the time series used to generate the variance curve forecast.

```julia

plt.figure(figsize=(11, 6))
ax = plt.subplot(111)
rvdata_p = rvdata.drop((dt.datetime(2010, 5, 6)), axis=0)
rvdata.loc["2010-05-04":"2010-05-10"].plot(ax=ax, legend=False)
rvdata_p.loc["2010-05-04":"2010-05-10"].plot(ax=ax, legend=False);

```

Figure 21: rvdata_p has the May 6 realized variance datapoint eliminated (green line).  Notice the crazy realized variance estimate for May 6!

We need a new variance curve forecast function that uses the new time series.

```julia

def xip(date, tt, nu,h, tscale):  # dt=(u-t) is in units of years
    rvdata = pd.DataFrame(rv1['SPX2.rk'])
    rvdata_p = rvdata.drop((dt.datetime(2010, 5, 6)), axis=0)
    return [ forecast_XTS(rvdata_p, h=h, date=date,nLags=500,
                          delta=delta_t * tscale, nu=nu) for delta_t in tt]

nu = OxfordH["nu_est"][0]
h = OxfordH["h_est"][0]

def varSwapCurve_p(date, bigT, nSteps, nu, h, tscale, onFactor):
  # Make vector of fwd variances
  tt = [ float(i) * (bigT) / nSteps for i in range(nSteps+1)]
  delta_t = tt[1]
  xicurve = xip(date, tt, nu, h, tscale)
  xicurve_mid = (np.array(xicurve[0:nSteps]) + np.array(xicurve[1:nSteps + 1])) / 2
  xicurve_int = np.cumsum(xicurve_mid) * delta_t
  varcurve1 = np.divide(xicurve_int, np.array(tt[1:]))
  varcurve = np.array([xicurve[0],]+list(varcurve1))
  varcurve = varcurve * onFactor * tscale #  onFactor is to compensate for overnight moves
  res = pd.DataFrame({"texp":np.array(tt), "vsQuote":np.sqrt(varcurve)})
  return(res)

def varSwapForecast_p(date, tau, nu, h, tscale, onFactor):
  vsc = varSwapCurve_p(date, bigT=2.5, nSteps=100, nu=nu, h=h,
                    tscale=tscale, onFactor=onFactor) # Creates the whole curve
  x = vsc['texp']
  y = vsc['vsQuote']
  res = stineman_interp(tau, x, y, None)

  return(res)

# Test the function

tau = (.25, .5 ,1, 2)
date = dt.datetime(2010, 5, 10)
varSwapForecast_p(date, tau, nu=nu, h=h, tscale=252, onFactor=1. / (1 - .35))

```

Finally, we compare our new forecast curves with the actuals.

```julia

date1 = dt.datetime(2010, 5, 7)
date2 = dt.datetime(2010, 5, 10)

vsf7 = varSwapCurve(date1, bigT=2.5, nSteps=100, nu=nu, h=h, tscale=252, onFactor=2.52)
vsf10p = varSwapCurve_p(date2, bigT=2.5, nSteps=100, nu=nu, h=h, tscale=252, onFactor=2.52)

```

```julia

plt.figure(figsize=(11, 6))
plt.plot(texp7, varSwaps7, "g", label='May 7')
plt.plot(texp10, varSwaps10, "m", label='May 10')

plt.legend()
plt.xlabel("Time to maturity")
plt.ylabel("Variance swap quote")

plt.plot(vsf7['texp'], vsf7['vsQuote'], "g--")
plt.plot(vsf10p['texp'], vsf10p['vsQuote'], "m--");

```

Figure 22: The May 10 actual curve is consistent with a forecast that excludes the Flash Crash.

Resetting of expectations over the weekend¶

In Figures 20 and 22, we see that the actual variance swap curve on Monday, May 10  is consistent with a  forecast that excludes the  Flash Crash.

Volatility traders realized that the Flash Crash should not influence future realized variance projections.

Summary¶

We uncovered a remarkable monofractal scaling relationship in historical volatility.

A corollary is that volatility is not a long memory process, as widely believed.

This leads to a natural non-Markovian stochastic volatility model under $\mP$.

The simplest specification of $\frac{d\mQ}{d\mP}$ gives a non-Markovian generalization of the Bergomi model.

The history of the Brownian motion $\lbrace W_s, s<t curve="" encoded="" for="" forward="" in="" is="" market.="" observed="" pricing="" required="" the="" variance="" which="">
</t>

This model fits the observed volatility surface surprisingly well with very few parameters.

For perhaps the first time, we have a simple consistent model of historical and implied volatility.

References¶

^
Torben G Andersen, Tim Bollerslev, Francis X Diebold, and Heiko Ebens, The distribution of realized stock return volatility, *Journal of Financial Economics* **61**(1) 43-76 (2001).

^
Christian Bayer, Peter Friz and Jim Gatheral, Pricing under rough volatility, *Quantitative Finance* forthcoming, available at http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2554754, (2015).

^
Emmanuel Bacry and Jean-François Muzy, Log-infinitely divisible multifractal processes,
*Communications in Mathematical Physics* **236**(3) 449-475 (2003).

^  Jim Gatheral and Antoine Jacquier, Arbitrage-free SVI volatility
surfaces, *Quantitative Finance* **14**(1) 59-71 (2014).

^ Jim Gatheral, Thibault Jaisson and Mathieu Rosenbaum, Volatility is rough, available at http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2509457, (2014).

^ Jim Gatheral and Roel Oomen, Zero-intelligence realized variance estimation, *Finance and Stochastics* **14**(2) 249-283 (2010).

^ Carl J. Nuzman and H. Vincent Poor, Linear estimation of self-similar processes via Lamperti’s transformation, *Journal of Applied Probability* **37**(2) 429-452 (2000).


```python
%load_ext autoreload
%autoreload 2
import tensorflow as tf
import qutip as q
from spylind import spylind as spl
from spylind import spyIVP as spi
from spylind.tf_misc import InterpolatorMask, InterpolatorMaskArgs

from math import *
import numpy as np
import sympy as sm
from matplotlib.pyplot import *
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

## Qutip style problem definition

```python

#problem definition
K1, K2  = q.basis(2,0), q.basis(2,1),
tS, DeltaS = sm.symbols('t, Delta', real=True)
ES_r, ES_i = sm.symbols("E_r, E_i", real=True)
H0 = 2*pi*K2*K2.dag()
c_opL = [sqrt(.1)*q.sigmap()]

H_desc = [H0, [ES_r, q.sigmax()], [ES_i, q.sigmay()], [DeltaS, K2*K2.dag()] ]
eq, ex_vals=spl.makeMESymb(H_desc, c_opL=c_opL, e_opL = [], bReturnMatrixEquation=True)
lhsL, rhsL = spl.seperate_DM_equation(eq)

ode_s = spi.ODESolver(dict(zip(lhsL, rhsL) ),  dims={DeltaS:np.linspace(-2,2,100)}, driving_syms=[ES_r, ES_i], bDecompose_to_re_im=True, backend='tensorflow')
ode_s.set_initial_conditions(np.array([1,0,0]) )

tDrv = np.linspace(0,2.0,200) #Times where the value of the driving field is defined ("control points")
driving =  np.exp(1j*2*np.pi* tDrv)
driving_interpolator_r = InterpolatorMask(tDrv, driving.real) #These will interpolate between the supplied control points
driving_interpolator_i = InterpolatorMask(tDrv, driving.imag)
ode_s.set_driving({ES_r:driving_interpolator_r, ES_i:driving_interpolator_i}) # map the symbols ES_r and ES_i to the driving functions
ode_s.setup(bForceStateDimensions=True)

#Now run
tSteps = np.linspace(0,2.0, 100) # Times when values will be returned
%time res1= ode_s.integrate(tSteps)
print("\n")
print("after JIT compiling, subsequent runs are much faster:")
%time res2= ode_s.integrate(tSteps)
print('\n')
print("To avoid recompilation, change the driving fields using the methods on the interpolation oobjects (uses tensorflow variables)")
driving =  np.exp(2j*2*np.pi* tDrv)
driving_interpolator_r.set_y(driving.real)
driving_interpolator_i.set_y(driving.imag)
%time res3= ode_s.integrate(tSteps)

#Plotting
plot(res1[:,1].numpy().mean(axis=-1))
plot(res2[:,1].numpy().mean(axis=-1))
plot(res3[:,1].numpy().mean(axis=-1))
legend(np.arange(3))
title('excited state pop')

```

Note that changing 'tSteps', ie the time that results are returned at, triggers a recompile.
Also note that having lots of 'tSteps' is expensive, but you usually don't need them if you only care about the final state.
In the below, the final state is essentially the same, despite only returning 5 steps

```python
tSteps = np.linspace(0,2.0, 5)#tDrv[:-1].copy()
res4 = ode_s.integrate(tSteps)
res3[-1,1].numpy().mean(), res4[-1,1].numpy().mean() #The last evaluated point is almost the same

```

    /home/morgan/anaconda3/envs/spylind/lib/python3.8/site-packages/tensorflow/python/framework/constant_op.py:98: ComplexWarning: Casting complex values to real discards the imaginary part
      return ops.EagerTensor(value, ctx.device_name, dtype)

    WARNING:tensorflow:6 out of the last 13 calls to <function ODESolver.integrate_tf at 0x7c8d2df044c0> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/tutorials/customization/performance#python_or_tensor_args and https://www.tensorflow.org/api_docs/python/tf/function for  more details.

    WARNING:tensorflow:6 out of the last 13 calls to <function ODESolver.integrate_tf at 0x7c8d2df044c0> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/tutorials/customization/performance#python_or_tensor_args and https://www.tensorflow.org/api_docs/python/tf/function for  more details.

    num function evaluations: 12006

    (0.05115718623768774, 0.051170741903498414)

## Different ways of defining the control points
Here we'll use modulation envelopes for the control points

```python
#Needs to be a function that takes a time tensor as arguemnt and returns a tf.tensor. May not work with non-tensorflow operations
drv_freq = tf.Variable(0.7, dtype='float64')
def f_r(t): #real field
    return driving_interpolator_r(t)*tf.cos(drv_freq*2*pi*t)
def f_i(t): #imaginary
    return driving_interpolator_i(t)*tf.sin(drv_freq*2*pi*t)
ode_s = spi.ODESolver(dict(zip(lhsL, rhsL) ),  dims={DeltaS:np.linspace(-2,2,100)}, driving_syms=[ES_r, ES_i], bDecompose_to_re_im=True, backend='tensorflow')
ode_s.set_initial_conditions(np.array([1,0,0]) )

tDrv = np.linspace(0,2.0,200) #Times where the value of the driving field is defined ("control points")

ode_s.set_driving({ES_r:f_r, ES_i:f_i})
ode_s.setup(bForceStateDimensions=True)
driving = np.ones(tDrv.size)*(0.5 + 0.5j)
driving_interpolator_r.set_y(driving.real)
driving_interpolator_i.set_y(driving.imag)

tSteps = np.linspace(0,2,200)
%time states= ode_s.integrate(tSteps)
plot(states[:,:,50])
```

```python
driving = np.ones(tDrv.size)*(1. + 0.5j)
driving_interpolator_r.set_y(driving.real)
driving_interpolator_i.set_y(driving.imag)
drv_freq.assign(0.2)
tSteps = np.linspace(0,2,200)
%time states= ode_s.integrate(tSteps)
plot(states[:,:,50])
```

# Calculating gradients:
Easiest way is to wrap it all in a "GradientTape"

```python
import MT
with tf.GradientTape(watch_accessed_variables=False) as tape:
    tape.watch(driving_interpolator_r.yOrig)
    tape.watch(driving_interpolator_i.yOrig)
    tape.watch(drv_freq)

    %time res2= ode_s.integrate(tSteps)#, grad_pair=grad_pair
    %time grad_r, grad_i=tape.gradient(tf.reduce_mean(res2[-1][1]), (driving_interpolator_r.yOrig, driving_interpolator_i.yOrig))
grad = grad_r.numpy() + 1j*grad_i.numpy()

title('loss gradients')
plot(grad.real, label='real')
plot(grad.imag, label='imag')
#xlim([25, 30])
```

Because of the adaptive stepping, gradients can be a little jagged due to some steps sample more than others. Doing a moving average of 2 points gets rid of some of it. Not sure how sustainable this is

```python
import MT
smth = lambda y: MT.smooth(y,2+1) # This actually should be a moving average of 2 (dont' ask why)
plot(smth(grad.real) )
plot(smth(grad.imag))
plot(grad.imag)
plot(grad.real )
xlim([75,125])

```

## Optimize:

```python
%load_ext autoreload
%autoreload 2
import tensorflow as tf
import tensorflow_probability as tfp
import qutip as q
from spylind import spylind as spl
from spylind import spyIVP as spi
from spylind.tf_misc import InterpolatorMask, InterpolatorMaskArgs

from math import *
import numpy as np
import sympy as sm
from matplotlib.pyplot import *

#problem definition
K1, K2  = q.basis(2,0), q.basis(2,1),
tS, DeltaS = sm.symbols('t, Delta', real=True)
ES_r, ES_i = sm.symbols("E_r, E_i", real=True)
H0 = 0*pi*K2*K2.dag()
c_opL = [sqrt(.1)*q.sigmap()]

H_desc = [H0, [ES_r, q.sigmax()], [ES_i, q.sigmay()], [DeltaS, K2*K2.dag()] ]
eq, ex_vals=spl.makeMESymb(H_desc, c_opL=c_opL, e_opL = [], bReturnMatrixEquation=True)
lhsL, rhsL = spl.seperate_DM_equation(eq)

ode_s = spi.ODESolver(dict(zip(lhsL, rhsL) ),  dims={DeltaS:np.linspace(-.01,.01,100)}, driving_syms=[ES_r, ES_i], bDecompose_to_re_im=True, backend='tensorflow')
ode_s.set_initial_conditions(np.array([1,0,0]) )

tDrv = np.linspace(0,2.0,20)
tSteps = np.linspace(0,2.0, 100)#tDrv[:-1].copy()

tDrv -= (tDrv[1]-tDrv[0])/2

driving =  np.exp(1j*2*np.pi* tDrv)

driving_interpolator_r = InterpolatorMask(tDrv, driving.real)
driving_interpolator_i = InterpolatorMask(tDrv, driving.imag)

def f_r(t):
    return driving_interpolator_r(t)*tf.cos(2*pi*t)
def f_i(t):
    return driving_interpolator_i(t)*tf.sin(2*pi*t)

#ode_s.set_driving({ES_r:driving_interpolator_r, ES_i:driving_interpolator_i})
ode_s.set_driving({ES_r:f_r, ES_i:f_i})

ode_s.setup(bForceStateDimensions=True)

```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload
    Munch({'tSym': t, 'dimSyms': [Delta], 'prop_state_syms': [\rho_{0|0}, \rho_{1|1}, \rho_{0|1}^r, \rho_{0|1}^i], 'stationary_state_syms': [], 'driving_syms': [E_r, E_i], 'state_dep_syms': [Delta]})
    state dependent functions should have signature <itertools.chain object at 0x7c8d3eba28b0>

## Problem:
going back go adaptive stepping seems to be screwing things up as they cause noisy gradients. If you 'encourage' similar step sizes, things improve, but it still gets stuck in local minima due to the stepping noise.

```python
import MT
from scipy import optimize
smth = lambda x: x
def stack(cmplx):
    return np.hstack([cmplx.real,cmplx.imag])
def unstack(stacked):
    return stacked[:N] + 1j * stacked[N:]

N = tDrv.size
drv0 = np.where(tDrv<1, 0, 5.0)*np.exp(0*pi*1j*tDrv)#np.ones(tDrv.size)*(.1 + 0j)

def just_run(driving):
    drv_r, drv_i = driving[:N], driving[N:]
    driving_interpolator_r.set_y(drv_r)
    driving_interpolator_i.set_y(drv_r)
    res= ode_s.integrate(tSteps)
    return res
def get_loss(driving):
    drv_r, drv_i = driving[:N], driving[N:]
    driving_interpolator_r.set_y(drv_r)
    driving_interpolator_i.set_y(drv_r)

    with tf.GradientTape(watch_accessed_variables=False) as tape:
        tape.watch(driving_interpolator_r.yOrig)
        tape.watch(driving_interpolator_i.yOrig)

        res= ode_s.integrate(tSteps, first_step_size=1e-3, safety_factor=0.01, max_step_size_factor=2.0, atol=1e-12, rtol=1e-8)#, first_step_size=0.01, safety_factor=0.1, min_step_size_factor =1-1e-5, max_step_size_factor=1 + 1e-4, atol=1e10, rtol=1e10)#, grad_pair=grad_pair
                res= ode_s.integrate(tSteps, first_step_size=1e-3, safety_factor=0.01, max_step_size_factor=2.0, atol=1e-12, rtol=1e-8)#, first_step_size=0.01, safety_factor=0.1, min_step_size_factor =1-1e-5, max_step_size_factor=1 + 1e-4, atol=1e10, rtol=1e10)#, grad_pair=grad_pair

        L = tf.reduce_mean(1-res[-1,1]**2)
       # tf.reduce_mean(1-arr[-1,2]**2)
        grad_r, grad_i=tape.gradient(L, (driving_interpolator_r.yOrig, driving_interpolator_i.yOrig))
    L =L.numpy()
    print("Loss: ", L)
    return L, np.hstack([smth(grad_r.numpy()), 1*smth(grad_i.numpy()) ])
#driving
#get_loss(stack(drv0) )
res=optimize.minimize(get_loss, stack(drv0), jac=True) #jac=True means the second return value from "get_loss" is the gradient
```

```python
#continue from above
res=optimize.minimize(get_loss, res.x, method='CG', jac=True, options={'maxiter':20})
```

    /home/morgan/anaconda3/envs/spylind/lib/python3.8/site-packages/tensorflow/python/framework/constant_op.py:98: ComplexWarning: Casting complex values to real discards the imaginary part
      return ops.EagerTensor(value, ctx.device_name, dtype)

    num function evaluations: 1536
    0.050040093402770956
    num function evaluations: 1512
    0.049911503093696304
    num function evaluations: 1458
    0.04970823346384359
    num function evaluations: 1428
    0.04954958920786658
    num function evaluations: 1548
    0.04991445468139654
    num function evaluations: 1422
    0.04954739300197139
    num function evaluations: 1500
    0.049307154907549645
    num function evaluations: 1590
    0.04999766839604881
    num function evaluations: 1416
    0.04967728307095201
    num function evaluations: 1482
    0.04895304066711273
    num function evaluations: 1458
    0.04884561873096348
    num function evaluations: 1500
    0.049278782101448385
    num function evaluations: 1464
    0.04892438292755687
    num function evaluations: 1458
    0.048846168314706145
    num function evaluations: 1458
    0.04884562549452692
    num function evaluations: 1458
    0.04884562182786357
    num function evaluations: 1458
    0.04884562142034939
    num function evaluations: 1458
    0.04884561873096348

    /home/morgan/anaconda3/envs/spylind/lib/python3.8/site-packages/tensorflow/python/framework/constant_op.py:98: ComplexWarning: Casting complex values to real discards the imaginary part
      return ops.EagerTensor(value, ctx.device_name, dtype)

    num function evaluations: 1512
    0.049911503093696304
    num function evaluations: 1500
    0.04990876396615084
    num function evaluations: 1536
    0.04975128928536424
    num function evaluations: 1500
    0.04959003030433808
    num function evaluations: 1548
    0.04987252957802488
    num function evaluations: 1464
    0.04982035788826068
    num function evaluations: 1518
    0.04981142698318447
    num function evaluations: 1500
    0.04964350646314117
    num function evaluations: 1470
    0.04978218443616211
    num function evaluations: 1494
    0.049874910160665725
    num function evaluations: 1470
    0.04994153031530732
    num function evaluations: 1476
    0.04958162013542515
    num function evaluations: 1470
    0.04993614958689612
    num function evaluations: 1470
    0.04993344211040974
    num function evaluations: 1476
    0.04958290390005054
    num function evaluations: 1476
    0.049582463766757165

```python
jac = unstack(res.jac)
plot(jac.real)
plot(jac.imag)
```

```python
drv = unstack(res.x)
plot(drv.real, label='real')
plot(drv.imag, label='imag')
title('driving field')
legend()

states = just_run(drv)
figure()
plot(states[:,:,50])
title('state evolution')
legend(np.arange(4))
```

```python

```

```python

```


# Selección óptima de portafolios I

En la clase pasada vimos que:
- La LAC describe las posibles selecciones de riesgo-rendimiento entre un activo libre de riesgo y un activo riesgoso.
- Su pendiente es igual al radio de Sharpe del activo riesgoso.
- La asignación óptima de capital para cualquier inversionista es el punto tangente de la curva de indiferencia del inversionista con la LAC (depende de las preferencias particulares - aversión al riesgo).

Para todo lo anterior, supusimos que ya teníamos el portafolio óptimo (activo riesgoso).

En el siguiente análisis:

**Objetivos:**
- ¿Cuál es el portafolio óptimo de activos riesgosos?
 - ¿Cuál es el mejor portafolio de activos riesgosos?
 - Es un portafolio eficiente en media-varianza.
- Problema: dado un conjunto de activos riesgosos, ¿cómo construimos la mejor combinación?

*Referencia:*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Maximizando el radio de Sharpe

### ¿Qué pasa si tenemos dos activos riesgosos?

Cuando tenemos dos o más activos riesgosos, tenemos disponibles diferentes LAC. ¿Qué significan sus pendientes?

 Ver en el tablero.

Pregunta:
- ¿Qué es lo que se quiere?

**Conclusión:**
- El mejor portafolio de activos no depende de las preferencias individuales, y por tanto va a ser igual para todos.
- Dicho mejor portafolio maximiza el radio de Sharpe.
- A este portafolio lo llamaremos el portafolio eficiente en media-varianza (EMV)

**Idea principal: el portafolio óptimo de activos riesgosos es independiente de las preferencias del inversionista.**
- El portafolio EMV determina el portafolio óptimo de activos riesgosos.
- Todos tendremos el mismo portafolio de activos riesgosos (EMV), y lo combinaremos con el activo libre de reisgo de acuerdo con las preferencias de cada uno de nosotros (aversión al riesgo).
- La LAC combinando el activo libre de riesgo y el portafolio EMV, se vuelve el conjunto de portafolios eficientes.

Entonces, se deben seguir los siguientes pasos:
1. Crear la frontera media-varianza.
2. Encontrar el portafolio que maximize el radio de Sharpe (portafolio EMV).
3. Construir la frontera eficiente (LAC) del punto $(0,r_f)$ al punto $(\sigma_s,E[r_s])$ del portafolio EMV.
4. Combinar de acuerdo a sus preferencias.
___

## 2. Solución analítica del portafolio EMV: caso con dos activos.

Queremos solucionar el siguiente problema:

\begin{align}
\max_{w_1,w_2} &\quad \frac{E[r_p]-r_f}{\sigma_p}\\
\text{s.a.}    &\quad E[r_p]=w_1E[r_1]+w_2E[r_2]\\
               &\quad \sigma_p=\sqrt{w_1^2\sigma_1^2+w_2^2\sigma_2^2+2w_1w_2\rho_{12}\sigma_1\sigma_2}\\
               &\quad w_1+w_2=1, \quad w_1,w_2\geq0
\end{align}

el cual es equivalente a

\begin{align}
\max_{w_1} &\quad \frac{w_1E[r_1]+(1-w_1)E[r_2]-r_f}{\sqrt{w_1^2\sigma_1^2+(1-w_1)^2\sigma_2^2+2w_1(1-w_1)\rho_{12}\sigma_1\sigma_2}}\\
\text{s.a.}    &\quad 0\leq w_1\leq1
\end{align}

**Actividad.**
El anterior es un problema de maximizar una función de una variable en un dominio cerrado. No debaría representar dificultad.

Encontrar la solución analítica a este problema.

Quien primero lo haga, y salga a explicarlo al tablero, le subo alguna tarea, quiz o el primer examen a 100.

Deben llegar a:

$$w_{1,EMV}=\frac{(E[r_1]-r_f)\sigma_2^2-(E[r_2]-r_f)\sigma_{12}}{(E[r_2]-r_f)\sigma_1^2+(E[r_1]-r_f)\sigma_2^2-((E[r_1]-r_f)+(E[r_2]-r_f))\sigma_{12}}.$$

Si nadie lo ha hecho en 30 min., procederé a hacerlo yo.

**Nota:**
- así como obtuvimos una expresión para el peso del portafolio de mínima varianza con dos activos, obtenemos una expresión para el peso del portafolio Eficiente en Media-Varianza.
- Estas actividades son sin duda un buen ejercicio, y se pueden replicar usando técnicas de varias variables (multiplicadores de Lagrange) cuando se tengan más de dos activos.
- Sin embargo, la complejidad del problema crece considerablemente con el número de variables, y la solución analítica deja de ser viable cuando mencionamos que un portafolio bien diversificado consta aproximadamente de 50-60 activos.
- En esos casos, este problema se soluciona con rutinas numéricas que hagan la optimización por nosotros.
- Por eso, les enseño cómo resolver este problema con optimizadores numéricos, porque son una solución viable y escalable a más variables.

## 3. Ejemplo ilustrativo.

Retomamos el ejemplo de mercados de acciones en los países integrantes del $G5$: EU, RU, Francia, Alemania y Japón.

```python
# Importamos pandas y numpy
import pandas as pd
import numpy as np
```

```python
# Resumen en base anual de rendimientos esperados y volatilidades
annual_ret_summ = pd.DataFrame(columns=['EU', 'RU', 'Francia', 'Alemania', 'Japon'], index=['Media', 'Volatilidad'])
annual_ret_summ.loc['Media'] = np.array([0.1355, 0.1589, 0.1519, 0.1435, 0.1497])
annual_ret_summ.loc['Volatilidad'] = np.array([0.1535, 0.2430, 0.2324, 0.2038, 0.2298])

annual_ret_summ.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      Media
      0.1355
      0.1589
      0.1519
      0.1435
      0.1497

      Volatilidad
      0.1535
      0.243
      0.2324
      0.2038
      0.2298

```python
# Matriz de correlación
corr = pd.DataFrame(data= np.array([[1.0000, 0.5003, 0.4398, 0.3681, 0.2663],
                                    [0.5003, 1.0000, 0.5420, 0.4265, 0.3581],
                                    [0.4398, 0.5420, 1.0000, 0.6032, 0.3923],
                                    [0.3681, 0.4265, 0.6032, 1.0000, 0.3663],
                                    [0.2663, 0.3581, 0.3923, 0.3663, 1.0000]]),
                    columns=annual_ret_summ.columns, index=annual_ret_summ.columns)
corr.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      EU
      1.0000
      0.5003
      0.4398
      0.3681
      0.2663

      RU
      0.5003
      1.0000
      0.5420
      0.4265
      0.3581

      Francia
      0.4398
      0.5420
      1.0000
      0.6032
      0.3923

      Alemania
      0.3681
      0.4265
      0.6032
      1.0000
      0.3663

      Japon
      0.2663
      0.3581
      0.3923
      0.3663
      1.0000

Supondremos, además, que la tasa libre de riesgo es $r_f=5\%$.

```python
# Tasa libre de riesgo
rf= 0.05
```

Entonces, supondremos que tenemos disponibles los activos correspondientes a los mercados de acciones de EU y Japón, y en adición el activo libre de riesgo.

#### 1. Construir la frontera de mínima varianza

```python
# Vector de w variando entre 0 y 1 con n pasos
n=101
w= np.linspace(0,1,n)
# Rendimientos esperados individuales
# Activo1: EU, Activo2:Japon
E1=annual_ret_summ.loc['Media', 'EU']
E2=annual_ret_summ.loc['Media', 'Japon']
# Volatilidades individuales
s1=annual_ret_summ.loc['Volatilidad', 'EU']
s2=annual_ret_summ.loc['Volatilidad', 'Japon']
# Correlacion
r12= corr.loc['EU','Japon']
```

```python
# DataFrame de portafolios:
# 1. Índice: i
# 2. Columnas 1-2: w, 1-w
# 3. Columnas 3-4: E[r], sigma
# 4. Columna 5: Sharpe ratio
portafolios= pd.DataFrame(index=range(n),
                         data={'w':w,
                              '1-w': 1-w,#w_2
                              'Media': w*E1+(1-w)*E2,
                              'vol':np.sqrt((w*s1)**2+2*w*(1-w)*r12*s1*s2+((1-w)*s2)**2)})
portafolios['RS']=((portafolios['Media']-rf)/portafolios['vol'])
portafolios.head()
```

      w
      1-w
      Media
      vol
      RS

      0
      0.00
      1.00
      0.149700
      0.229800
      0.433856

      1
      0.01
      0.99
      0.149558
      0.227916
      0.436820

      2
      0.02
      0.98
      0.149416
      0.226041
      0.439814

      3
      0.03
      0.97
      0.149274
      0.224176
      0.442839

      4
      0.04
      0.96
      0.149132
      0.222322
      0.445894

```python
portafolios.tail()
```

      w
      1-w
      Media
      vol
      RS

      96
      0.96
      0.04
      0.136068
      0.150070
      0.573521

      97
      0.97
      0.03
      0.135926
      0.150877
      0.569509

      98
      0.98
      0.02
      0.135784
      0.151719
      0.565415

      99
      0.99
      0.01
      0.135642
      0.152593
      0.561244

      100
      1.00
      0.00
      0.135500
      0.153500
      0.557003

```python
# Importar librerías de gráficos
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a SR
plt.figure(figsize=(5,4))
plt.scatter(portafolios['vol'], portafolios['Media'], c=portafolios['RS'],cmap='autumn')
plt.xlabel('Volatilidad ')
plt.ylabel('Media')
plt.grid()
plt.colorbar()
plt.show()
```

#### 2. Encontrar el portafolio que maximiza el radio de Sharpe (EMV)

Primero, encontramos este portafolio con la fórmula que obtuvimos:

```python
# Fórmula que obtuvimos
s12=r12*s1*s2
wEMV= ((E1-rf)*s2**2-(E2-rf)*s12)/((E2-rf)*s1**2+(E1-rf)*s2**2-(((E1-rf)+(E2-rf))*s12))
EEMV= wEMV*E1+(1-wEMV)*E2
#np.sqrt((w*s1)**2+2*w*(1-w)*r12*s1*s2+((1-w)*s2)**2)
sEMV= ((wEMV*s1)**2+2*wEMV*(1-wEMV)*s12+((1-wEMV)*s2)**2)**0.5
RSmax= (EEMV-rf)/sEMV
wEMV, 1-wEMV,EEMV,sEMV,RSmax
```

    (0.6983139170512034,
     0.3016860829487966,
     0.13978394237787292,
     0.14231708951606933,
     0.6308725303698346)

Ahora sí, con la función scipy.optimize.minimize

```python
# Importar el módulo optimize
import scipy.optimize as opt
```

```python
## Construcción de parámetros
## Activo 1: EU, Activo 2: Japon
# 1. Sigma: matriz de varianza-covarianza
Sigma=np.array([[s1**2,r12*s1*s2],
              [r12*s1*s2,s2**2]])
# 2. Eind: rendimientos esperados activos individuales
Eind= np.array([E1,E2])
```

La librería de optimize no puede obtener máximos, sólo calcula mínimos. Por lo tanto debemos obtener el mínimo del negativo de nuestra función objetivo.

```python
# Función objetivo (-SR)
def minus_RS(w, Eind, Sigma,rf):
    Ep= Eind.dot(w)
    sp= ((w.dot(Sigma)).dot(w))**0.5
    return (rf-Ep)/sp
```

```python
# Cantidad de activos
N= len(Eind)
# Dato inicial
w0= np.ones((2,))/N
# Cotas de las variables
bnds= ((0,1),)*N
#bnds = ((0,1),(0,1))#
# Restricciones
cons= {'type':'eq','fun':lambda w:w.sum()-1}
```

```python
# Optimización numérica
EMV= opt.minimize(minus_RS,
                  x0=w0,
                  args=(Eind,Sigma,rf),
                  bounds= bnds,
                  constraints=cons)
# Resultado
EMV
```

         fun: -0.6308725303331167
         jac: array([-0.35171176, -0.35029621])
     message: 'Optimization terminated successfully.'
        nfev: 17
         nit: 4
        njev: 4
      status: 0
     success: True
           x: array([0.69830749, 0.30169251])

Con lo anterior, podemos obtener datos de rendimiento esperado y volatilidad del portafolio EMV

Debemos invertir 69.8% en EU y 30.16% en Japon

```python
# Rendimiento esperado y volatilidad del portafolio EMV
wEMV2 = EMV.x
Ep = Eind.dot(wEMV2)
sp= (wEMV2.dot(Sigma).dot(wEMV2))**0.5
RSmax= (Ep-rf)/sp
Ep, sp, RSmax
```

    (0.1397840336419269, 0.14231723418757614, 0.6308725303331167)

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a SR, y portafolio EMV
plt.figure(figsize=(5,4))
plt.scatter(portafolios['vol'], portafolios['Media'], c=portafolios['RS'],cmap='autumn')
plt.plot(sp, Ep, 'ok')
plt.xlabel('Volatilidad ')
plt.ylabel('Rendimiento esperado $E[r]$')
plt.axis([0.14, 0.18, 0.135, 0.145])
plt.grid()
plt.colorbar()
plt.show()
```

#### 3. Construir LAC

Ahora, dibujamos la LAC, combinando el portafolio EMV con el activo libre de riesgo:

```python
# Vector de wp variando entre 0 y 1.5 con n pasos
wp = np.linspace(0,1.5, 50)
```

```python
# DataFrame de CAL:
# 1. Índice: i
# 2. Columnas 1-2: wp, wrf
# 3. Columnas 3-4: E[r], sigma
# 4. Columna 5: Sharpe ratio
LAC= pd.DataFrame(index= range(len(wp)),
                 data={'wp': wp,
                      'wrf': 1-wp,
                      'Media': wp*Ep+(1-wp)*rf,
                      'vol': wp*sp})
LAC['RS']=(LAC['Media']-rf)/LAC['vol']
LAC.head()
```

      wp
      wrf
      Media
      vol
      RS

      0
      0.000000
      1.000000
      0.050000
      0.000000
      NaN

      1
      0.030612
      0.969388
      0.052748
      0.004357
      0.630873

      2
      0.061224
      0.938776
      0.055497
      0.008713
      0.630873

      3
      0.091837
      0.908163
      0.058245
      0.013070
      0.630873

      4
      0.122449
      0.877551
      0.060994
      0.017427
      0.630873

```python
LAC.tail()
```

      wp
      wrf
      Media
      vol
      RS

      45
      1.377551
      -0.377551
      0.173682
      0.196049
      0.630873

      46
      1.408163
      -0.408163
      0.176431
      0.200406
      0.630873

      47
      1.438776
      -0.438776
      0.179179
      0.204763
      0.630873

      48
      1.469388
      -0.469388
      0.181928
      0.209119
      0.630873

      49
      1.500000
      -0.500000
      0.184676
      0.213476
      0.630873

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a SR, portafolio EMV y LAC
plt.figure(figsize=(5,4))
plt.scatter(portafolios['vol'], portafolios['Media'], c=portafolios['RS'],cmap='autumn')
plt.plot(sp, Ep, 'ok')
plt.plot(LAC['vol'],LAC['Media'], 'b-')
plt.xlabel('Volatilidad ')
plt.ylabel('Rendimiento esperado $E[r]$')
#plt.axis([0.10, 0.18, 0.135, 0.145])
plt.grid()
plt.colorbar()
plt.show()
```

#### 4. Combinación óptima de acuerdo a preferencias

Con los datos anteriores, y la caracterización de aversión al riesgo, se escoge la combinación óptima entre el portafolio EMV y el activo libre de riesgo de acuerdo a:

$$w^\ast=\frac{E[r_s-r_f]}{\gamma\sigma_s^2}.$$

```python
# Para gamma=7
gamma=7
w_opt= (Ep-rf)/(gamma*sp**2)
w_opt
```

    0.6332658704683628

```python
1-w_opt, w_opt*wEMV2
```

    (0.3667341295316372, array([0.4422143 , 0.19105157]))

Con esto decidimos nuestra posición dentro de la linea LAC. Debemos invertir 1-w_opt en el activo libre de riesgo.

En este caso debemos de invertir 36.7% en los activos libres de riesgo y el resto en los activos del portafolio; 44.22% y 19.1% en EU y Japon respectivamente

Created with Jupyter by Esteban Jiménez Rodríguez.


```python
from logicqubit.logic import *
from cmath import *
import numpy as np
import sympy as sp
import scipy
from random import randrange
from scipy.optimize import *
import matplotlib.pyplot as plt
```

    Cuda is not available!
    logicqubit version 1.5.8

```python
gates = Gates()

ID = gates.ID()
X = gates.X()
Y = gates.Y()
Z = gates.Z()
```

```python
IIII = ID.kron(ID).kron(ID).kron(ID)
XXXX = X.kron(X).kron(X).kron(X)
YYYY = Y.kron(Y).kron(Y).kron(Y)
ZZZZ = Z.kron(Z).kron(Z).kron(Z)
signals = [ZZZZ.get()[i,i] for i in range(len(ZZZZ.get()))]
signals # used in <psi|ZZZZ|psi>
```

    [1, -1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, -1, 1]

```python
H = IIII*3 + ZZZZ*8

min(scipy.linalg.eig(H.get())[0])
```

    (-5+0j)

```python
def _ansatz(reg, params):
    n_qubits = len(reg)
    depth = n_qubits
    for i in range(depth):
        reg[1].CNOT(reg[0])
        for j in range(n_qubits):
            reg[i].RY(params[j])

def ansatz(reg, params):
    n_qubits = len(reg)
    depth = n_qubits
    for i in range(depth):
        for j in range(n_qubits):
            if(j < n_qubits-1):
                reg[j+1].CNOT(reg[j])
            reg[i].RY(params[j])

def ansatz_4q(q1, q2, q3, q4, params):
    q1.RY(params[0])
    q2.RY(params[1])
    q3.RY(params[2])
    q4.RY(params[3])
    q2.CNOT(q1)
    q3.CNOT(q2)
    q4.CNOT(q3)
    q1.RX(params[4])
    q2.RX(params[5])
    q3.RX(params[6])
    q4.RX(params[7])
    q2.CNOT(q1)
    q3.CNOT(q2)
    q4.CNOT(q3)
    q1.RY(params[8])
    q2.RY(params[9])
    q3.RY(params[10])
    q4.RY(params[11])
    q2.CNOT(q1)
    q3.CNOT(q2)
    q4.CNOT(q3)
    q1.RY(params[12])
    q2.RY(params[13])
    q3.RY(params[14])
    q4.RY(params[15])
    q2.CNOT(q1)
    q3.CNOT(q2)
    q4.CNOT(q3)
```

```python
def expectation_4q(params):
    logicQuBit  = LogicQuBit(4)
    q1 = Qubit()
    q2 = Qubit()
    q3 = Qubit()
    q4 = Qubit()

    ansatz_4q(q1,q2,q2,q4,params)
    #ansatz([q1,q2,q3,q4],params)
    psi = logicQuBit.getPsi()

    return (psi.adjoint()*H*psi).get()[0][0]

initial_values = np.random.uniform(-np.pi, np.pi, 16)
minimum = minimize(expectation_4q, initial_values, method='Nelder-Mead',options={'xtol': 1e-10, 'ftol': 1e-10})
print(minimum)
```

     final_simplex: (array([[ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633],
           [ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
             1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
            -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
            -1.57079633]]), array([-5., -5., -5., -5., -5., -5., -5., -5., -5., -5., -5., -5., -5.,
           -5., -5., -5., -5.]))
               fun: -5.000000000000004
           message: 'Optimization terminated successfully.'
              nfev: 1735
               nit: 1043
            status: 0
           success: True
                 x: array([ 0.97108118, -0.26330511, -1.30749121, -0.6436676 , -3.14159267,
            1.23238916,  0.63362969, -1.27557381,  0.24058204,  0.61156406,
           -0.61156407,  2.49663256,  0.96096669, -0.58769253,  1.62413606,
           -1.57079633])

```python
def expectation_value(measurements):
    probabilities = np.array(measurements)
    states = signals
    expectation = np.sum(states * probabilities) # <psi|ZZZZ|psi>
    return expectation

def sigma_xxxx(params):
    logicQuBit  = LogicQuBit(4, first_left = False)
    q1 = Qubit()
    q2 = Qubit()
    q3 = Qubit()
    q4 = Qubit()

    ansatz_4q(q1,q2,q3,q4,params)

    # medidas em XX
    q1.RY(-pi/2)
    q2.RY(-pi/2)
    q3.RY(-pi/2)
    q4.RY(-pi/2)

    result = logicQuBit.Measure([q1,q2,q3,q4])
    result = expectation_value(result)
    return result

def sigma_yyyy(params):
    logicQuBit  = LogicQuBit(4, first_left = False)
    q1 = Qubit()
    q2 = Qubit()
    q3 = Qubit()
    q4 = Qubit()

    ansatz_4q(q1,q2,q3,q4,params)

    # medidas em YY
    q1.RX(pi/2)
    q2.RX(pi/2)
    q3.RX(pi/2)
    q4.RX(pi/2)

    result = logicQuBit.Measure([q1,q2,q3,q4])
    result = expectation_value(result)
    return result

def sigma_zzzz(params):
    logicQuBit  = LogicQuBit(4, first_left = False)
    q1 = Qubit()
    q2 = Qubit()
    q3 = Qubit()
    q4 = Qubit()

    ansatz_4q(q1,q2,q3,q4,params)

    result = logicQuBit.Measure([q1,q2,q3,q4])
    result = expectation_value(result)
    return result

def expectation_energy(params):
    xxxx =  sigma_xxxx(params)
    yyyy =  sigma_yyyy(params)
    zzzz =  sigma_zzzz(params)

    result = 3 + 8*zzzz
    return result
```

```python
initial_values = np.random.uniform(-np.pi, np.pi, 16)
minimum = minimize(expectation_energy, initial_values, method='Nelder-Mead')
print(minimum)
```

     final_simplex: (array([[-3.14158631e+00, -1.56722459e+00,  4.30413762e-01,
             9.60212067e-02,  3.14158852e+00,  1.56202882e+00,
            -2.35871198e+00,  3.21365907e+00,  1.15642881e-06,
             1.57489919e+00, -9.75093207e-01,  3.13672123e+00,
             2.87586517e+00, -2.69623135e-03, -5.14798803e-02,
            -1.57040064e+00],
           [-3.14159290e+00, -1.56719816e+00,  4.30399183e-01,
             9.60238298e-02,  3.14158941e+00,  1.56201745e+00,
            -2.35870998e+00,  3.21363982e+00,  2.77304237e-06,
             1.57491768e+00, -9.75103211e-01,  3.13669459e+00,
             2.87585784e+00, -2.69622332e-03, -5.14830310e-02,
            -1.57040753e+00],
           [-3.14159594e+00, -1.56716079e+00,  4.30383177e-01,
             9.60275892e-02,  3.14159095e+00,  1.56197556e+00,
            -2.35872335e+00,  3.21365304e+00, -2.24314318e-06,
             1.57496858e+00, -9.75105170e-01,  3.13663393e+00,
             2.87590226e+00, -2.69620394e-03, -5.14862760e-02,
            -1.57040722e+00],
           [-3.14158731e+00, -1.56721675e+00,  4.30410025e-01,
             9.60208689e-02,  3.14159749e+00,  1.56200806e+00,
            -2.35872571e+00,  3.21365441e+00,  7.88620818e-06,
             1.57492017e+00, -9.75093757e-01,  3.13670683e+00,
             2.87586357e+00, -2.69622854e-03, -5.14815240e-02,
            -1.57040637e+00],
           [-3.14159070e+00, -1.56729464e+00,  4.30431946e-01,
             9.60176831e-02,  3.14158277e+00,  1.56202599e+00,
            -2.35872666e+00,  3.21366770e+00, -1.91714491e-06,
             1.57484386e+00, -9.75089789e-01,  3.13681646e+00,
             2.87590312e+00, -2.69621071e-03, -5.14734134e-02,
            -1.57041014e+00],
           [-3.14158315e+00, -1.56728858e+00,  4.30435126e-01,
             9.60172187e-02,  3.14159478e+00,  1.56204559e+00,
            -2.35870798e+00,  3.21366250e+00, -5.05876665e-07,
             1.57484518e+00, -9.75090183e-01,  3.13680752e+00,
             2.87587776e+00, -2.69623599e-03, -5.14730307e-02,
            -1.57040580e+00],
           [-3.14157816e+00, -1.56719712e+00,  4.30411769e-01,
             9.60207428e-02,  3.14159264e+00,  1.56200536e+00,
            -2.35872259e+00,  3.21366700e+00,  1.04248414e-05,
             1.57492879e+00, -9.75087896e-01,  3.13668979e+00,
             2.87586088e+00, -2.69624670e-03, -5.14818850e-02,
            -1.57040181e+00],
           [-3.14160136e+00, -1.56725066e+00,  4.30409984e-01,
             9.60226334e-02,  3.14158674e+00,  1.56202695e+00,
            -2.35870787e+00,  3.21364738e+00, -7.22034267e-06,
             1.57487374e+00, -9.75104499e-01,  3.13675810e+00,
             2.87589110e+00, -2.69620398e-03, -5.14782178e-02,
            -1.57040323e+00],
           [-3.14158783e+00, -1.56724599e+00,  4.30408936e-01,
             9.60224538e-02,  3.14159065e+00,  1.56203393e+00,
            -2.35871759e+00,  3.21361216e+00, -4.46921288e-06,
             1.57488294e+00, -9.75099471e-01,  3.13676144e+00,
             2.87589427e+00, -2.69618524e-03, -5.14806801e-02,
            -1.57041244e+00],
           [-3.14159147e+00, -1.56725034e+00,  4.30417971e-01,
             9.60196423e-02,  3.14157799e+00,  1.56204219e+00,
            -2.35871434e+00,  3.21365347e+00,  4.36422831e-06,
             1.57487782e+00, -9.75097877e-01,  3.13675142e+00,
             2.87584030e+00, -2.69623005e-03, -5.14774338e-02,
            -1.57041203e+00],
           [-3.14159400e+00, -1.56725668e+00,  4.30420379e-01,
             9.60200547e-02,  3.14159675e+00,  1.56204103e+00,
            -2.35870832e+00,  3.21364758e+00, -4.09476034e-06,
             1.57487155e+00, -9.75093423e-01,  3.13676714e+00,
             2.87588052e+00, -2.69621979e-03, -5.14781746e-02,
            -1.57039645e+00],
           [-3.14158998e+00, -1.56719152e+00,  4.30394447e-01,
             9.60257389e-02,  3.14159518e+00,  1.56202714e+00,
            -2.35870377e+00,  3.21363012e+00, -3.88268472e-06,
             1.57492483e+00, -9.75106292e-01,  3.13667522e+00,
             2.87586945e+00, -2.69621209e-03, -5.14835957e-02,
            -1.57040334e+00],
           [-3.14158936e+00, -1.56723445e+00,  4.30400634e-01,
             9.60243830e-02,  3.14158351e+00,  1.56200936e+00,
            -2.35872892e+00,  3.21362481e+00, -3.33030526e-06,
             1.57490035e+00, -9.75107650e-01,  3.13673237e+00,
             2.87590532e+00, -2.69617691e-03, -5.14796240e-02,
            -1.57040644e+00],
           [-3.14159040e+00, -1.56720078e+00,  4.30402090e-01,
             9.60229989e-02,  3.14158132e+00,  1.56202323e+00,
            -2.35870847e+00,  3.21364298e+00,  5.83893432e-06,
             1.57491410e+00, -9.75102822e-01,  3.13669864e+00,
             2.87584049e+00, -2.69623280e-03, -5.14821907e-02,
            -1.57040586e+00],
           [-3.14159408e+00, -1.56716667e+00,  4.30392228e-01,
             9.60254556e-02,  3.14158885e+00,  1.56201798e+00,
            -2.35869895e+00,  3.21364138e+00, -1.79744302e-06,
             1.57494772e+00, -9.75099205e-01,  3.13666209e+00,
             2.87585853e+00, -2.69623035e-03, -5.14870897e-02,
            -1.57038950e+00],
           [-3.14159525e+00, -1.56727306e+00,  4.30421870e-01,
             9.60192563e-02,  3.14159534e+00,  1.56200342e+00,
            -2.35873442e+00,  3.21366520e+00,  4.92719365e-06,
             1.57485962e+00, -9.75097477e-01,  3.13677827e+00,
             2.87589651e+00, -2.69620920e-03, -5.14751370e-02,
            -1.57040971e+00],
           [-3.14158461e+00, -1.56722266e+00,  4.30413830e-01,
             9.60223626e-02,  3.14159317e+00,  1.56200423e+00,
            -2.35871096e+00,  3.21367190e+00, -3.31131891e-06,
             1.57488523e+00, -9.75094012e-01,  3.13673916e+00,
             2.87591367e+00, -2.69623017e-03, -5.14784284e-02,
            -1.57040153e+00]]), array([-4.99997456, -4.99997456, -4.99997456, -4.99997456, -4.99997456,
           -4.99997456, -4.99997456, -4.99997456, -4.99997456, -4.99997456,
           -4.99997456, -4.99997456, -4.99997456, -4.99997456, -4.99997456,
           -4.99997456, -4.99997456]))
               fun: -4.999974564586708
           message: 'Optimization terminated successfully.'
              nfev: 1774
               nit: 1286
            status: 0
           success: True
                 x: array([-3.14158631e+00, -1.56722459e+00,  4.30413762e-01,  9.60212067e-02,
            3.14158852e+00,  1.56202882e+00, -2.35871198e+00,  3.21365907e+00,
            1.15642881e-06,  1.57489919e+00, -9.75093207e-01,  3.13672123e+00,
            2.87586517e+00, -2.69623135e-03, -5.14798803e-02, -1.57040064e+00])

```python
def gradient(params, evaluate):
    n_params = params.shape[0]
    shift = pi/2
    gradients = np.zeros(n_params)

    for i in range(n_params):
        #parameter shift rule
        shift_vect = np.array([shift if j==i else 0 for j in range(n_params)])
        shift_right = params + shift_vect
        shift_left = params - shift_vect

        expectation_right = evaluate(shift_right)
        expectation_left = evaluate(shift_left)

        gradients[i] = expectation_right - expectation_left

    return gradients
```

```python
params = np.random.uniform(-np.pi, np.pi, 16)
last_params = np.zeros(16)
```

```python
lr = 0.05
err = 1
while err > 1e-15:
    grad = gradient(params, expectation_energy)
    params = params - lr*grad
    err = abs(sum(params - last_params))
    last_params = np.array(params)
    print(err)
```

    <ipython-input-24-cef20e405c15>:15: ComplexWarning: Casting complex values to real discards the imaginary part
      gradients[i] = expectation_right - expectation_left

    0.08546257568133853
    0.15416069170439184
    0.0010030077885998523
    0.005848087390222023
    0.0014074649937678707
    0.00037367540024457746
    0.000525058931988287
    0.0003059735653656581
    0.00021555852567467504
    0.00013823997861606152
    9.094510252460886e-05
    5.9029449401171163e-05
    3.842826257582921e-05
    2.4959655773759692e-05
    1.6213877932491627e-05
    1.0527286879780107e-05
    6.83443283255869e-06
    4.43633101898655e-06
    2.8795013184979013e-06
    1.868905151303224e-06
    1.2129531050675268e-06
    7.872115472817853e-07
    5.108967460198954e-07
    3.315667485015439e-07
    2.151822029450301e-07
    1.396497364414273e-07
    9.063018180377469e-08
    5.881727571654949e-08
    3.81712628172437e-08
    2.4772387252625094e-08
    1.607677746484626e-08
    1.0433500063911083e-08
    6.771128102656121e-09
    4.3943220173758846e-09
    2.8518238881503066e-09
    1.8507753107854796e-09
    1.201114319115959e-09
    7.794980216857539e-10
    5.058791163747856e-10
    3.2830427265650997e-10
    2.1306334474502364e-10
    1.3827272660194012e-10
    8.973621845598245e-11
    5.823808102434214e-11
    3.779376811507973e-11
    2.452837932764851e-11
    1.5917933637865644e-11
    1.0330625244137082e-11
    6.70330457808177e-12
    4.351852211925689e-12
    2.823519196226698e-12
    1.8316459460265833e-12
    1.1899370377932428e-12
    7.709388682997087e-13
    5.015987625256457e-13
    3.2462921240039577e-13
    2.107203300738547e-13
    1.3700152123874432e-13
    8.859579736508749e-14
    5.81756864903582e-14
    3.708144902248023e-14
    2.4646951146678475e-14
    1.6653345369377348e-14
    9.769962616701378e-15
    6.661338147750939e-15
    3.3306690738754696e-15
    3.3306690738754696e-15
    1.9984014443252818e-15
    8.881784197001252e-16

```python
expectation_energy(params)
```

    (-4.999999999999998+0j)

```python

```


```python

```

メモ

とりあえず sympy document の fpgroups を読んでみよう。 手を動かす、という意味で。

山括弧

https://docs.sympy.org/latest/modules/combinatorics/fp_groups.html

```python
from sympy.combinatorics.free_groups import free_group, vfree_group, xfree_group
from sympy.combinatorics.fp_groups import FpGroup, CosetTable, coset_enumeration_r

```

とりあえず import はできた。

```python
F,a,b = free_group("a,b")
G=FpGroup(F,[a**2,b**3,(a*b)**4])
G
```

$\displaystyle \mathtt{\text{<fp group on the generators (a, b)>}}$

```python
F,r,s,t = free_group("r,s,t")
G=FpGroup(F,[r**2,s**2,t**2,r*s*t*r**-1*t**-1*s**-1,s*t*r*s**-1*r**-1*t**-1])
```

$\displaystyle \mathtt{\text{<free group on the generators (r, s, t)>}}$

```python
F,x,y=free_group("x,y")
F
```

$\displaystyle \mathtt{\text{<free group on the generators (x, y)>}}$

```python
F=vfree_group("x,y")
F
```

$\displaystyle \mathtt{\text{<free group on the generators (x, y)>}}$

```python
F=xfree_group("x,y")
print(F)
x**2
```

    (<free group on the generators (x, y)>, (x, y))

$\displaystyle \left( \left( x, \  2\right)\right)$

```python
from sympy.combinatorics.free_groups import free_group, vfree_group, xfree_group
from sympy.combinatorics.fp_groups import FpGroup, CosetTable, coset_enumeration_r
F, a, b = free_group("a, b")
Cox = FpGroup(F, [a**6, b**6, (a*b)**2, (a**2*b**2)**2, (a**3*b**3)**5])
# C_r = coset_enumeration_r(Cox, [a], max_cosets=50)
```

# 途中だけどここまで

群論の group 表記の山括弧が云々、なのでとりあえずストップ。

```python

```


# Building and using a 0D emulator

This notebook demonstrates how to use the `cardioemulator` library to:

- build a zero-dimensional emulator ($\mathcal{M}_{\text{0D}}$) of a three-dimensional left ventricular model ($\mathcal{M}_{\text{3D}}$);

- perform numerical simulations through the zero-dimensional emulator;

- use the zero-dimensional emulator to detect a limit cycle.

The file used in this demo are contained in the folder `example` of the [repository](https://github.com/FrancescoRegazzoni/cardioemulator).

## Building the 0D emulator

Let us first import the needed dependencies

```python
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import sys

# you don't need this line if you have installed cardioemulator correctly!
sys.path.insert(0, '..')

import cardioemulator
```

The file `example/data/sample_PV_loops.csv` contains five PV loops obtained through the 3D left ventricle (LV) model, coupled with a circulation model ($\mathcal{M}_{\text{3D}}$-$\mathcal{C}$).
The file `example/data/simulated_EDPVR.csv` contains pressure-volume pairs in relaxed conditions, obtained with the same 3D left ventricle model ($\mathcal{M}_{\text{3D}}$).

With the following commands, we build two emulators. The first one is based on the *fitted EDPVR approach* (that is, the EDPV relationship is obtained by fitting the Klotz's curve on the sample PV loops).

```python
emulator_fitted_EDPV = cardioemulator.build_emulator(
                            file_PV_loops = 'data/sample_PV_loops.csv',
                            period = 0.8, # [s]
                            num_cycles = 3)
```

    Building 0D emulator:
       file PV loops:         data/sample_PV_loops.csv
       EDPV approach:         fitted
       period:                0.800 s
       num cycles:            3
       EDPV V_0:              -1.19 mL
       EDPV V_30:             167.47 mL
       ESPV V_0:              11.45 mL
       ESPV elastance:        1.645 mmHg/mL
       EDPV elastance:        0.132 mmHg/mL

The second one is based on the *simulated EDPVR approach* (that is the EDPV relationship is obtained through the 3D model).

```python
emulator_simulated_EDPV = cardioemulator.build_emulator(
                            file_PV_loops = 'data/sample_PV_loops.csv',
                            file_EDPV = 'data/simulated_EDPVR.csv',
                            period = 0.8, # [s]
                            num_cycles = 3)
```

    Building 0D emulator:
       file PV loops:         data/sample_PV_loops.csv
       EDPV approach:         simulated
       file EDPV:             data/simulated_EDPVR.csv
       period:                0.800 s
       num cycles:            3
       ESPV V_0:              11.45 mL
       ESPV elastance:        1.645 mmHg/mL
       EDPV elastance:        0.132 mmHg/mL

**Note**

These commands assume that pressures and volumes are contained in columns named "pressure" and "volume", respectively.
If this is not the case, you can specify the column names through the arguments `label_pressure_PV_loops` and `label_volume_PV_loops` (for `file_PV_loops`), or `label_pressure_EDPV` and `label_volume_PV_EDPV` (for `file_EDPV`).
Similarly, you can change the dafault measure units (mmHg and mL, respectively), through the arguments `pressure_unit_PV_loops`, `volume_unit_PV_loops`, `pressure_unit_EDPV` and `volume_unit_EDPV`.

Please notice that the same results can be obtained by running the bash command:
```bash
$ cardioemulator_build data/sample_PV_loops.csv 0.8 --num-cycles 3 \
                       -o emulator_fitted_EDPV.json
$ cardioemulator_build data/sample_PV_loops.csv 0.8 --num-cycles 3 \
                       --EDPV data/simulated_EDPVR.csv \
                       -o emulator_simulated_EDPV.json
```

and then loading the emulators from file within a Python script:
```python
emulator_fitted_EDPV    = cardioemulator.Emulator('emulator_fitted_EDPV.json')
emulator_simulated_EDPV = cardioemulator.Emulator('emulator_simulated_EDPV.json')
```

**Hint**

The option ``-d`` of the app ``cardioemulator_build`` provides on-the-fly plots of the emulator built.
Similarly, the argument ``-f`` allows to export the figure to file.

## Visualizing the 0D emulator

Let us plot the functions defining the two emulators $\mathcal{M}_{\text{0D}}$, together with the PV loops we used for their construction.

```python
data_loop = pd.read_csv('data/sample_PV_loops.csv')
VV = np.linspace(20, 200, 1000)
times = np.linspace(0, 0.8, 1000)

_, axs = plt.subplots(1,2,figsize=(8,4))
axs[0].plot(data_loop.volume, data_loop.pressure, 'k', linewidth = 0.8)
axs[0].plot(VV, emulator_fitted_EDPV.EDPV(VV), label = 'EDPVR (fitted)')
axs[0].plot(VV, emulator_simulated_EDPV.EDPV(VV), label = 'EDPVR (simulated)')
axs[0].plot(VV, emulator_fitted_EDPV.ESPV(VV), label = 'ESPVR')
axs[0].set_xlabel('V [mL]')
axs[0].set_ylabel('p [mmHg]')
axs[0].set_xlim([50, 170])
axs[0].set_ylim([-10, 150])
axs[0].legend(bbox_to_anchor=(1, 1), loc=1, frameon=False, fontsize=7, labelspacing = 0.2)
axs[1].plot(times, emulator_fitted_EDPV.activation(times), label = 'fitted EDPVR')
axs[1].plot(times, emulator_simulated_EDPV.activation(times), label = 'simulated EDPVR')
axs[1].set_xlabel('time [s]')
axs[1].set_ylabel('activation function [-]')
axs[1].legend(bbox_to_anchor=(1, 1), loc=1, frameon=False, fontsize=7, labelspacing = 0.2)

```

## Using the 0D emulator

The 0D emulator consists of a time-dependent pressure-volume relationship:
\begin{equation}
p = \mathcal{P}(V, t)
\end{equation}
The pressure-volume function $\mathcal{P}$ is accessible through the member [PV](../docs/_autosummary/_autosummary/cardioemulator.Emulator.html#cardioemulator.Emulator.PV) of the class [Emulator](../_autosummary/_autosummary/cardioemulator.Emulator.html). The following script computes e.g. $\mathcal{P}(100 \text{ mL}, 0.4 \text{ s})$

```python
volume = 100 # [ml]
time = 0.4 # [s]
pressure = emulator_fitted_EDPV.PV(volume, time)
print('pressure = %f mmHg' % pressure)
```

    pressure = 91.256128 mmHg

A useful feature of the 0D emulator is the possibilty of being coupled with a blood circulation model ($\mathcal{C}$).
For illutrative purposes, this example contains a Python class implementing the 0D closed-loop circulation model introduced in [this paper](https://arxiv.org/abs/2011.15040). To couple the 0D emulator with this circulation model, all we need to do is simply replacing the LV pressure-volume relationship (therein represented by an elastance-based model) by the function $\mathcal{P}$ and then running a simulation:

```python
from circulation_closed_loop import circulation_closed_loop

# initialize the circulation model
circulation = circulation_closed_loop(options = 'data/params_bsln.json')
# replace the original LV PV relationship with the one defined by the emulator
circulation.p_LV_func = emulator_fitted_EDPV.PV
# run a 5 heartbeats simulation
history = circulation.solve(num_cycles = 5, initial_state = 'data/state_bsln.json')
```

    Circulation model - running simulation...
    Circulation model - elapsed time 2.0310 s

Let us now visually compare the results obtanined with the $\mathcal{M}_{\text{0D}}$-$\mathcal{C}$ model and with the $\mathcal{M}_{\text{3D}}$-$\mathcal{C}$ model:

```python
_, axs = plt.subplots(1,1,figsize=(4,4))
axs.plot(data_loop.volume, data_loop.pressure, label = '3D')
axs.plot(history.VLV, history.pLV, label = '0D')
axs.legend(bbox_to_anchor = (1, 1), loc = 1, frameon = False)
axs.set_xlabel('V [mL]')
axs.set_ylabel('p [mmHg]')
```

## Detecting the limit cycle by means of the 0D emulator

Let us now run 100 heartbeats by means of the 0D emulator coupled with the circulation model ($\mathcal{M}_{\text{0D}}$-$\mathcal{C}$).
Then, we store the final state of this simulation in the file `limit_cycle.json`.
Finally, to check that the simulation actually reached a limit cycle, we perform a second and shorter simulation starting from the stored state.

```python
history_transient = circulation.solve(num_cycles = 100, initial_state = 'data/state_bsln.json')
circulation.save_state('limit_cycle.json')
history_limit_cycle = circulation.solve(num_cycles = 3, initial_state = 'limit_cycle.json')
```

    Circulation model - running simulation...
    Circulation model - elapsed time 39.8512 s
    Circulation model - running simulation...
    Circulation model - elapsed time 1.2790 s

Let us plot the full transient and the limit cycle:

```python
_, axs = plt.subplots(1,1,figsize=(4,4))
axs.plot(history_transient.VLV, history_transient.pLV, label = 'transient')
axs.plot(history_limit_cycle.VLV, history_limit_cycle.pLV, label = 'limit cycle')
axs.legend(bbox_to_anchor = (1, 1), loc = 1, frameon = False)
axs.set_xlabel('V [mL]')
axs.set_ylabel('p [mmHg]')
```

In conclusion, the final state of the simulation obtained through the $\mathcal{M}_{\text{0D}}$-$\mathcal{C}$ model (stored in the file `limit_cycle.json`) can be used to provide an initial state for the $\mathcal{M}_{\text{3D}}$-$\mathcal{C}$ model. Specifically, the state variables of the circulation model allow to initialize the circulation model itself, while the pressure or volume of the LV allow to initialize the $\mathcal{M}_{\text{3D}}$ model. The results are reported in \[1\].

### References

\[1\] F. Regazzoni, A. Quarteroni "[Accelerating the convergence to a limit cycle in 3D cardiac electromechanical simulations through a data-driven 0D emulator](https://doi.org/10.1016/j.compbiomed.2021.104641)", *Computers in Biology and Medicine* 135 (2021) 104641