This vignette provides a “jump-start” for users that want to quickly
understand how to use entropy-pooling (EP) to construct *views*
on the market distribution. The methodology fits well in a variety of
scenarios: stress-testing, macro factors, non-linear payoffs, and
more.

To demonstrate entropy-pooling’s firepower, the current vignette uses
some of the cases Meucci (2008)^{1} leaves for the reader in the appendix. In
particular, the usage of the following functions are covered in
depth:

`view_on_mean()`

`view_on_volatility()`

`view_on_correlation()`

`view_on_rank()`

`view_on_marginal_distribution()`

`view_on_copula()`

Along with the examples, the `EuStockMarkets`

dataset -
that comes with the installation of `R`

- is used as a
*proxy* for “the market”:

```
library(ffp)
library(ggplot2)
set.seed(321)
# invariance / stationarity
<- diff(log(EuStockMarkets))
x
dim(x)
#> [1] 1859 4
head(x)
#> DAX SMI CAC FTSE
#> [1,] -0.009326550 0.006178360 -0.012658756 0.006770286
#> [2,] -0.004422175 -0.005880448 -0.018740638 -0.004889587
#> [3,] 0.009003794 0.003271184 -0.005779182 0.009027020
#> [4,] -0.001778217 0.001483372 0.008743353 0.005771847
#> [5,] -0.004676712 -0.008933417 -0.005120160 -0.007230164
#> [6,] 0.012427042 0.006737244 0.011714353 0.008517217
```

Assume an investor believes the `FTSE`

index return will
be \(20\%\) above average in the near
future. In order to process this *view*, the investor needs to
minimize the relative entropy:

\[ \sum_{j=1}^J x_j(ln(x_j) - ln(p_j)) \]

Subject to the restriction:

\[ \ x_j V_{j, k} = \mu_{k} \]

In which \(x_{j}\) is a yet to be
discovered *posterior* probability; \(V_{j,k}\) is a \(1859 \times 1\) vector with the
`FTSE`

returns; and \(\mu_{k}\) is a \(1 \times 1\) scalar with the investor
projected return for the `FTSE`

index. In this case, the
\(k\) subscript represents the fourth
column in `x`

^{2}.

*Views* on expected returns can be constructed with
`view_on_mean`

:

```
# Returns 20% higher than average
<- mean(x[ , "FTSE"]) * 1.2
mu
# ffp views constructor
<- view_on_mean(x = as.matrix(x[ , "FTSE"]), mean = mu)
views
views#> # ffp view
#> Type: View On Mean
#> Aeq : Dim 1 x 1859
#> beq : Dim 1 x 1
```

The `views`

object is a `list`

with two
components, `Aeq`

and `beq`

, that are equivalent
to the elements \(V_{j,k}\) transposed
and \(\mu_{k}\), respectively.

The investor also needs to formulate a vector of *prior*
probabilities, \(p_j\), which is
usually - but not necessary - a equal-weight vector:

```
# Prior probabilities
<- rep(1 / nrow(x), nrow(x)) p_j
```

Once the *prior* and the *views* are established, the
optimization can take place with `entropy_pooling`

:

```
# Solve Minimum Relative Entropy (MRE)
<- entropy_pooling(
ep p = p_j,
Aeq = views$Aeq,
beq = views$beq,
solver = "nlminb"
)
ep#> <ffp[1859]>
#> 0.0005425631 0.0005340012 0.000544236 0.0005418246 0.0005322989 ... 0.0005451271
```

What is the interpretation for `ep`

? Among all the
possible probability vectors, `ep`

is the one that can
satisfy the *views* in the “least” evasive way (in which the term
“least evasive” is direct reference to the *prior*). Therefore,
`ep`

is the best candidate for a *posterior*
distribution.

In real world, the investor can have many *views*. Extending
the current example, say the investor has a new belief: the
`CAC`

index returns will be \(10\%\) bellow average^{3}:

```
<- c(
mu mean(x[ , "FTSE"]) * 1.2, # Return 20% higher than average
mean(x[ , "CAC"]) * 0.9 # Return 10% bellow average
)
# ffp views constructor
<- view_on_mean(x = as.matrix(x[ , c("FTSE", "CAC")]), mean = mu)
views
views#> # ffp view
#> Type: View On Mean
#> Aeq : Dim 2 x 1859
#> beq : Dim 2 x 1
```

In which the elements `Aeq`

and `beq`

now have
\(2\) rows each, one for every
*view*.

The minimum relative entropy problem is solved in the exact same way:

```
# Solve MRE
<- entropy_pooling(
ep p = p_j,
Aeq = views$Aeq,
beq = views$beq,
solver = "nlminb"
)
ep#> <ffp[1859]>
#> 0.0005602371 0.0005473082 0.0005573006 0.0005384979 0.000531066 ... 0.0005434801
```

It’s easier to analyse the output of `ep`

visually.

```
class(ep)
#> [1] "ffp" "vctrs_vctr"
```

To that end, the `autoplot`

method is available for
objects of the `ffp`

class:

```
autoplot(ep) +
scale_color_viridis_c(option = "C", end = 0.75) +
labs(title = "Posterior Probability Distribution",
subtitle = "Views on Expected Returns",
x = NULL,
y = NULL)
```

The plot reinforces the intuition of \(x_j\) - the *posterior* probability
- as a distribution that distorts the *prior* in order to
accommodate the *views*.

It’s easy to double-check the investor *views* by computing
the ratio of the *conditional* vs. *unconditional*
returns:

```
<- ffp_moments(x, ep)$mu
conditional <- colMeans(x)
unconditional
/ unconditional - 1
conditional #> DAX SMI CAC FTSE
#> 0.01335825 0.02078130 -0.09999999 0.20000009
```

According to expectations, `CAC`

outlook is now \(10\%\) bellow average and `FTSE`

expected return is \(20\%\) above
average. *Violà!*

Say the investor is confident that markets will be followed by a
period of lull and bonanza. Without additional insights about expected
returns, the investor anticipates that market volatility for the
`CAC`

index will drop by \(10\%\).

To impose *views* on volatility, the investor needs to
minimize the expression:

\[ \sum_{j=1}^J x_j(ln(x_j) - ln(p_j)) \] Subject to the constraint:

\[ \sum_{j=1}^{J} x_j V_{j, k}^2 = \mu_{k}^2 + \sigma_{k}^2 \]

In which, \(x_j\) is a vector of yet
to be defined *posterior* probabilities; \(V_{j,k}^2\) is a \(1859 \times 1\) vector with
`CAC`

squared returns; \(\mu_{k}^2\) and \(\sigma_{k}^2\) are the `CAC`

sample mean and variance; and \(k\) is
the column position of `CAC`

in the panel \(V\).

The `view_on_volatility`

function can be used for this
purpose:

```
# opinion
<- sd(x[ , "CAC"]) * 0.9
vol_cond
# views
<- view_on_volatility(x = as.matrix(x[ , "CAC"]), vol = vol_cond)
views
views#> # ffp view
#> Type: View On Volatility
#> Aeq : Dim 1 x 1859
#> beq : Dim 1 x 1
```

The `views`

object holds a list with two components -
`Aeq`

and `beq`

- that are equivalent to the
elements \(V_{j,k}^2\) transposed and
\(\mu_{k}^2 + \sigma_{k}^2\),
respectively.

To solve the relative entropy use the `entropy_pooling`

function:

```
<- entropy_pooling(
ep p = p_j,
Aeq = views$Aeq,
beq = views$beq,
solver = "nlminb"
)
ep#> <ffp[1859]>
#> 0.0005227495 0.0004701207 0.0005609239 0.0005476659 0.0005631671 ... 0.0005349393
```

Once again, the `ep`

vector is what we call
*posterior*: the probability vector that causes the “least”
amount of distortion in the *prior*, but still obeys the
constraints (*views*).

The *posterior* probabilities can be observed with the
`autoplot`

method:

```
autoplot(ep) +
scale_color_viridis_c(option = "C", end = 0.75) +
labs(title = "Posterior Probability Distribution",
subtitle = "View on Volatility",
x = NULL,
y = NULL)
```

To check if the *posterior* probabilities are valid as a way
to display the investor’s *view*, compare the
*conditional* vs. *unconditional* volatilities:

```
<- apply(x, 2, sd)
unconditional <- sqrt(diag(ffp_moments(x, ep)$sigma))
conditional
/ unconditional - 1
conditional #> DAX SMI CAC FTSE
#> -0.08377407 -0.06701178 -0.09977906 -0.04552477
```

The *posterior* volatility for the `CAC`

index is
reduced by \(10\%\), in line with the
investor’s subjective judgment. However, by emphasizing tranquil periods
more often, other assets are also affected, but in smaller
magnitudes.

Assume the investor believes the correlation between
`FTSE`

and `DAX`

will increase by \(30\%\), from \(0.64\) to \(0.83\). To construct *views* on the
correlation matrix, the general expression has to be minimized:

\[ \sum_{j=1}^J x_j(ln(x_j) - ln(p_j)) \] Subject to the constraints:

\[ \sum_{j=1}^{J} x_j V_{j, k} V_{j, l} = \mu_{k} \mu_{l} + \sigma_{k} \sigma_{l} C_{k,l} \]

In which, the term \(V_{j, k} V_{j, l}\) on the left hand-side of the restriction consists of cross-correlations among assets; the terms \(\mu_{k} \mu_{l} + \sigma_{k} \sigma_{l} C_{k,l}\) carry the investor target correlation structure; and \(x_j\) is a yet to be defined probability vector.

To formulate this *view*, first compute the
*unconditional* correlation matrix. Second, add a “perturbation”
in the corresponding element that is consistent with the perceived
*view*:

```
<- cor(x) # unconditional correlation matrix
cor_view "FTSE", "DAX"] <- 0.83 # perturbation (investor belief)
cor_view["DAX", "FTSE"] <- 0.83 # perturbation (investor belief) cor_view[
```

Finally, pass the adjusted correlation matrix into the
`view_on_correlation`

function:

```
<- view_on_correlation(x = x, cor = cor_view)
views
views#> # ffp view
#> Type: View On Correlation
#> Aeq : Dim 10 x 1859
#> beq : Dim 10 x 1
```

In which `Aeq`

is equal to \(V_{j, k} V_{j, l}\) transposed and
`beq`

is a \(10 \times 1\)
vector with \(\mu_{k} \mu_{l} + \sigma_{k}
\sigma_{l} C_{k,l}\).

Notice that even though the investor has a *view* in just one
parameter^{4} the constraint vector has \(10\) rows, because every element of the
lower/upper diagonal of the correlation matrix has to match.

Once again, the minimum entropy is solved by
`entropy_pooling`

:

```
<- entropy_pooling(
ep p = p_j,
Aeq = views$Aeq,
beq = views$beq,
solver = "nlminb"
)
ep#> <ffp[1859]>
#> 6.709143e-05 0.000659403 0.001085763 0.0003254822 0.0005215729 ... 0.0004748052
```

And the `autoplot`

method is available:

```
autoplot(ep) +
scale_color_viridis_c(option = "C", end = 0.75) +
labs(title = "Posterior Probability Distribution",
subtitle = "View on Correlation",
x = NULL,
y = NULL)
```

To fact-check the investor *view*, compute the
*posterior* correlation matrix:

```
cov2cor(ffp_moments(x, p = ep)$sigma)
#> DAX SMI CAC FTSE
#> DAX 1.0000000 0.6932767 0.7317714 0.8346939
#> SMI 0.6932767 1.0000000 0.6260098 0.6113109
#> CAC 0.7317714 0.6260098 1.0000000 0.6535585
#> FTSE 0.8346939 0.6113109 0.6535585 1.0000000
```

And notice that the linear association between `FTSE`

and
`DAX`

is now \(0.83\)!

Assume the investor believes the `DAX`

index will
outperform the `SMI`

by some amount, but he doesn’t know by
how much. If the investor has only a mild *view* on the
performance of assets, he may want to minimize the following
expression:

\[ \sum_{j=1}^J x_j(ln(x_j) - ln(p_j)) \] Subject to the constraint:

\[ \sum_{j=1}^{J} x_j (V_{j, k} - V_{j,
l}) \ge 0 \] In which, the \(x_j\) is a yet to be determined probability
vector; \(V_{j, k}\) is a \(1859 \times 1\) column vector with
`DAX`

returns and \(V_{j,
l}\) is a \(1859 \times 1\)
column vector with the `SMI`

returns. In this case, the \(k\) and \(l\) subscripts refers to the column
positions of `DAX`

and `SMI`

in the object
`x`

, respectively.

*Views* on relative performance can be imposed with
`view_on_rank`

:

```
<- view_on_rank(x = x, rank = c(2, 1))
views
views#> # ffp view
#> Type: View On Rank
#> A : Dim 1 x 1859
#> b : Dim 1 x 1
```

Assets that are expected to outperform enter in the `rank`

argument with their column positions to the right: assets in the left
underperform and assets in the right outperform. Because the investor
believes that `DAX`

\(\geq\)
`SMI`

(the asset in the first column will outperform the
asset in the second column), we fill `rank = c(2, 1)`

^{5}.

The optimization is, once again, guided by the
`entropy_pooling`

function:

```
<- entropy_pooling(
ep p = p_j,
A = views$A,
b = views$b,
solver = "nloptr"
)
ep#> <ffp[1859]>
#> 0.0005145646 0.0005403155 0.0005470049 0.0005330238 0.0005446858 ... 0.0005469164
```

Two important observations:

- Inequalities constraint cannot be handled with the
`nlminb`

solver. Use`nloptr`

or`solnl`

, instead; - Inequalities constraint require the arguments
`A`

and`b`

to be fulfilled rather than`Aeq`

and`beq`

.

The *posterior* probabilities are presented bellow:

```
autoplot(ep) +
scale_color_viridis_c(option = "C", end = 0.75) +
labs(title = "Posterior Probability Distribution",
subtitle = "View on Ranking/Momentum",
x = NULL,
y = NULL)
```

To fact-check the ongoing *view*, compare the
*conditional* vs. *unconditional* returns:

```
<- ffp_moments(x, ep)$mu
conditional <- colMeans(x)
unconditional
/ unconditional - 1
conditional #> DAX SMI CAC FTSE
#> 0.17274275 -0.06507207 0.13576463 0.06261163
```

Indeed, returns for `DAX`

are adjusted upwards by \(17\%\), while returns for `SMI`

are revised downwards by \(6.5\%\).
Notice that returns for `CAC`

and `FTSE`

were also
affected. This behavior could be tamed by combining the ranking
condition with *views* on expected returns. See the function
`bind_views()`

for an example.

Assume the investor has a *view* on the entire marginal
distribution. One way to add *views* on marginal distributions is
by matching the first moments exactly, up to a given order.
Mathematically, the investor minimizes the relative entropy:

\[ \sum_{j=1}^J x_j(ln(x_j) - ln(p_j)) \] With, say, two equality constraints (one for \(\mu\) and one for \(\sigma^2\)):

\[ \sum_{j=1}^J x_j V_{j,k} = \sum_{j=z}^Z p_z \hat{V}_{z,k} \] \[ \sum_{j=1}^J x_j (V_{j,k})^2 = \sum_{z=1}^Z p_z (\hat{V}_{z,k})^2 \]

In which \(x_j\) is a yet to be
defined probability vector; \(V_{j,k}\)
is a matrix with the empirical marginal distributions; \(p_z\) is vector of *prior*
probabilities; and \(\hat{V}_{z,k}\) an
exogenous panel with simulations that are consistent with the investor’s
*views*.

When \(j = z\), the panels \(V_{j,k}\) and \(\hat{V}_{z,k}\) have the same number of
rows and the dimensions in both sides of the restrictions match.
However, it’s possible to set \(z \ge
j\) to simulate a larger panel for \(\hat{V}_{z, k}\). Keep in mind though that,
if \(z \ne j\), two *prior*
probabilities will have to be specified: one for \(p_j\) (the objective function) and one for
\(p_z\) (the *views*).

Continuing on the example, consider the margins of `x`

can
be approximated by a symmetric multivariate t-distribution. If this is
the case, the estimation can be conducted by the amazing
`ghyp`

package, that covers the entire family of generalized
hyperbolic distributions:

```
library(ghyp)
# multivariate t distribution
<- fit.tmv(data = x, symmetric = TRUE, silent = TRUE)
dist_fit
dist_fit#> Symmetric Student-t Distribution:
#>
#> Parameters:
#> nu
#> 6.151241
#>
#> mu:
#> [1] 0.0007899665 0.0009594760 0.0004790250 0.0003811669
#>
#> sigma:
#> DAX SMI CAC FTSE
#> DAX 1.000013e-04 6.046969e-05 7.933384e-05 5.072439e-05
#> SMI 6.046969e-05 8.062728e-05 5.869208e-05 4.119624e-05
#> CAC 7.933384e-05 5.869208e-05 1.216927e-04 5.715865e-05
#> FTSE 5.072439e-05 4.119624e-05 5.715865e-05 6.398099e-05
#>
#> gamma:
#> [1] 0 0 0 0
#>
#> log-likelihood:
#> 26370.73
#>
#>
#> Call:
#> fit.tmv(data = x, symmetric = TRUE, silent = TRUE)
```

The investor then, can construct a large panel with statistical properties similar to the margins he envisions:

```
set.seed(123)
# random numbers from `dist_fit`
<- rghyp(n = 100000, object = dist_fit) simul
```

Where the \(100.000 \times 4\)
matrix is used to match the *views* with
`view_on_marginal_distribution`

:

```
<- rep(1 / 100000, 100000)
p_z <- view_on_marginal_distribution(
views x = x,
simul = simul,
p = p_z
)
views#> # ffp view
#> Type: View On Marginal Distribution
#> Aeq : Dim 8 x 1859
#> beq : Dim 8 x 1
```

The objects `simul`

and `p_z`

corresponds to
the terms \(\hat{V}_{z,k}\) transposed
and \(p_z\), respectively. Note that
\(8\) restrictions are created, four
for each moment of the marginal distribution (there are four assets in
“the market”).

With the *prior* and the *views* at hand, the
optimization can be quickly implemented:

```
<- entropy_pooling(
ep p = p_j,
Aeq = views$Aeq,
beq = views$beq,
solver = "nloptr"
)
ep#> <ffp[1859]>
#> 0.000528748 0.0005678894 0.000536657 0.0005231905 0.0005349765 ... 0.0005324979
```

As the visual inspection of the vector `ep`

:

```
autoplot(ep) +
scale_color_viridis_c(option = "C", end = 0.75) +
labs(title = "Posterior Probability Distribution",
subtitle = "View on Marginal Distribution",
x = NULL,
y = NULL)
```

It’s easy to extend the current example. For instance, the investor could “tweak” some of the fitted parameters for stress-testing (i.e. change the degrees of freedom, increase/decrease expected returns, etc) to verify the immediate impact on the P&L (VaR, CVaR, etc).

Acknowledge that when the restrictions are to harsh, the scenarios
will be heavily distorted to satisfy the *views*. In this case,
one may need to compute the effective number of scenarios (ENS). This is
very easy to do with `ens()`

:

```
ens(ep)
#> [1] 1857.356
```

Rest on the investor to calibrate this statistic in order to get a
desired confidence level^{6}.

Assume the investor would like to simulate a different dependence
structure for the market. *Views* that change the interdependence
of assets can be implemented by minimizing the relative entropy:

\[ \sum_{j=1}^J x_j(ln(x_j) - ln(p_j)) \] Subject to the following restrictions:

\[ \sum_{j=1}^J x_i U_{j,k} = 0.5\] \[ \sum_{j=1}^J x_j U_{j,k}U_{j,l} = \sum_{z=1}^Z p_z \hat{U}_{z,k}\hat{U}_{z,l} \]

\[ \sum_{j=1}^J x_j U_{j,k}U_{j,l}U_{j,i} = \sum_{j=1}^J p_j \hat{U}_{j,k}\hat{U}_{j,l}\hat{U}_{j,i} \]

In which, the first restriction matches the first moment of the
uniform distribution; the second and third restrictions pair the
cross-moments of the empirical copula, \(U\), with the simulated copula, \(\hat{U}\); \(x_j\) is a yet to be discovered
*posterior* distribution; \(p_z\) is a *prior* probability; When
\(j = z\), the dimensions of \(p_j\) and \(p_z\) match.

Among many of the available copulas, say the investor wants to model
the dependence of the market as a clayton
copula to ensure the lower tail dependency
does not go unnoticed. The estimation is simple to implement with the
package `copula`

:

```
library(copula)
# copula (pseudo-observation)
<- pobs(x)
u
# copula estimation
<- fitCopula(
cop copula = claytonCopula(dim = ncol(x)),
data = u,
method = "ml"
)
# simulation
<- rCopula(
r_cop n = 100000,
copula = claytonCopula(param = cop@estimate, dim = 4)
)
```

First, the copulas are computed component-wise by applying the
marginal empirical distribution in every \(k\) column of the dataset. Second, the
pseudo-observations are used to fit a Clayton copula by the
Maximum-Likelihood (ML) method. Finally, a large panel with dimension
\(100.000 \times 4\) is constructed to
match a Clayton copula with \(\hat \alpha =
1.06\), as in the object `cop`

.

The *views* on the market is constructed with the
`view_on_copula`

:

```
<- view_on_copula(x = u, simul = r_cop, p = p_z)
views
views#> # ffp view
#> Type: View On Copula
#> Aeq : Dim 34 x 1859
#> beq : Dim 34 x 1
```

Once again, the solution of the minimum relative entropy can be found in one line of code:

```
<- entropy_pooling(
ep p = p_j,
Aeq = views$Aeq,
beq = views$beq,
solver = "nloptr"
)
ep#> <ffp[1859]>
#> 0.0001990935 0.0004161479 0.0008166059 0.0006512624 0.0003776943 ... 0.0003269579
```

And the `autoplot`

method is available for the objects of
the `ffp`

class:

```
autoplot(ep) +
scale_color_viridis_c(option = "C", end = 0.75) +
labs(title = "Posterior Probability Distribution",
subtitle = "View on Copulas",
x = NULL,
y = NULL)
```

To stretch the current example, assume the investor stresses the
parameter \(\hat \alpha = 1.06\)^{7}. Higher
\(\alpha\) values are linked to extreme
occurrences on the left tails of the distribution. Whence, consider the
case where \(\hat \alpha = 5\):

`@estimate <- 5 cop`

Rerun the previous steps (simulation, *views* and
optimization):

```
# simulation
<- rCopula(
r_cop n = 100000,
copula = claytonCopula(param = cop@estimate, dim = 4)
)
# #views
<- view_on_copula(x = u, simul = r_cop, p = p_z)
views
# relative entropy
<- entropy_pooling(
ep p = p_j,
Aeq = views$Aeq,
beq = views$beq,
solver = "nloptr"
)
```

To find the new *posterior* probability vector the satisfy the
stress-test condition:

```
autoplot(ep) +
scale_color_viridis_c(option = "C", end = 0.75) +
labs(title = "Posterior Probability Distribution",
subtitle = "View on Copulas",
x = NULL,
y = NULL)
```

Meucci, Attilio, Fully Flexible Views: Theory and Practice (August 8, 2008). Fully Flexible Views: Theory and Practice, Risk, Vol. 21, No. 10, pp. 97-102, October 2008, Available at SSRN: https://www.ssrn.com/abstract=1213325↩︎

The commands

`x[ , 4]`

or`x[ , "FTSE"]`

yield the same results.↩︎An ARIMA, GARCH or VAR model could be used as well. The focus is not on the best forecasting method, but how to input the

*view*once the forecast has already been made.↩︎`cor(ftse, dax) = 0.83`

.↩︎In the

`x`

object,`DAX`

it’s in the first column and the`SMI`

it’s in the second column.↩︎Meucci, Attilio, Effective Number of Scenarios in Fully Flexible Probabilities (January 30, 2012). GARP Risk Professional, pp. 32-35, February 2012, Available at SSRN: https://www.ssrn.com/abstract=1971808.↩︎

`cop@estimate`

↩︎