ml-finance-python

python scripts for finance machine learning

git clone https://9o.is/git/ml-finance-python.git

lab_23.ipynb

(58370B)


      1 {
      2  "cells": [
      3   {
      4    "cell_type": "markdown",
      5    "metadata": {},
      6    "source": [
      7     "# An Introduction to the Black-Litterman in Python\n",
      8     "\n",
      9     "## Introduction\n",
     10     "### Background and Theory\n",
     11     "\n",
     12     "The Black-Litterman asset allocation model \\cite{black1992global}, \\cite{he1999intuition} provides a methodical way of combining an investors subjective views of the future performance of a risky investment asset with the views implied by the market equilibrium. The method has seen wide acceptance amongst practitioners as well as academics in spite of the fact that it originated as an internal Goldman Sachs working paper, rather than as a piece of research from academia.\n",
     13     "\n",
     14     "The Black Litterman procedure can be viewed as a bayesian shrinkage method, that shrinks the expected returns constructed from an investor's views on asset returns towards asset returns implied by the market equilibrium. The procedure computes a set of expected returns that uses the market equilibrium implied  as a prior. This is then combined with returns implied by subjective investor views to produce a set of posterior expected returns $\\mu^{BL}$ and covariances $\\Sigma^{BL}$.\n",
     15     "\n",
     16     "Besides the obvious attraction of being able to incorporate subjective investor views, the Black-Litterman procedure has a second feature that makes it extremely attractive to portfolio optimization. It is well known that the Markowitz optimization procedure is highly sensitive to estimation errors in Expected Returns and Covariances, and this _error maximizing_ nature of the Markowitz procedure causes unstable portfolios with extreme weights that diverge rapidly from the market equilibrium portfolio even with minor changes to the inputs (e.g. \\cite{chopra1993effect}, \\cite{michaud1989markowitz}). However, the posterior parameters $\\mu^{BL}, \\Sigma^{BL}$ computed by the Black Litterman procedure are derived in part from the market portfolio, and therefore are much more pragmatic inputs for purposes of portfolio optimization. Specifically, when $\\mu^{BL}, \\Sigma^{BL}$ as used as as inputs to a Markowitz Optimizer, they produce optimized weights that diverge from the market portfolio in limited ways, and only to the extent of the confidence that the investor expresses in the views. Consequently the optimized portfolios are more stable portfolios than with pure Markowitz optimization with sample estimates. In the extreme, with appropriately set parameters, the Markowitz portfolio computed from the Black-Litterman parameters when there are no subjective investor views exactly coincides and is able to recover the market equilibrium portfolio."
     17    ]
     18   },
     19   {
     20    "cell_type": "markdown",
     21    "metadata": {},
     22    "source": [
     23     "### The Black Litterman Formulas\n",
     24     "\n",
     25     "Assume that we have $N$ assets, and $K$ views. There are two sets of inputs to the procedure. The first set of inputs relate to market parameters and these are:\n",
     26     "\n",
     27     "\\begin{array}{ll}\n",
     28     "w & \\mbox{A Column Vector ($N \\times 1$) of Equilibrium Market Weights of the Assets} \\\\\n",
     29     "\\Sigma & \\mbox{A Covariance Matrix ($N \\times N$) of the Assets} \\\\\n",
     30     "R_f & \\mbox{The Risk Free Rate} \\\\\n",
     31     "\\delta & \\mbox{The investor's Risk Aversion parameter}  \\\\\n",
     32     "\\tau & \\mbox{A scalar indicating the uncertainty of the prior (details below)}\n",
     33     "\\end{array}\n",
     34     "\n",
     35     "\n",
     36     "Some of these parameters can be inferred from other parameters if they are not explicitly specified. For instance, the risk aversion parameter can be set arbitrarily. For instance, some authors use $\\delta = 2.5$ while others use the value of $\\delta = 2.14$ in order to be consistent with the value calculated in \\cite{dimson2008triumph}.\n",
     37     "\n",
     38     "\\cite{beach2007application} suggest using $2.65$. Another common approach is to set $\\delta$ to the Market Price of Risk (i.e. a measure of the risk aversion of the Representative Investor, which is computed as $\\delta = \\mu_M/\\sigma^2_M$ where $\\mu_M$ and $\\sigma^2_M$ are estimates of the mean and variance of the returns of the market portfolio. Frequently, a broad market index such as the S\\&P500 is taken as a proxy for the market in order to compute the market price of risk from $\\mu_M$ and $\\sigma^2_M$.\n",
     39     "\n",
     40     "The treatment of $\\tau$ is the source of some confusion. As we will explain in the following section, some implementors have done away with $\\tau$ by setting it to $1$ or to calibrate the model to $tau$. In the original model, Black and Litterman suggest using a small number. A common technique is to set $\\tau = 1/T$ where $T$ is the number of periods of data used. Thus, for $T=5$ you would use $1/(5 \\times 12)$ which yields a value of approximately $\\tau=.02$.\n",
     41     "\n",
     42     "The second set of inputs that the procedure needs is a representation of the investors views. These are specified via:\n",
     43     "\n",
     44     "\\begin{array}{ll}\n",
     45     "Q & \\mbox{An $K \\times 1$ ``Qualitative Views'' or simply, Views matrix} \\\\\n",
     46     "P & \\mbox{A $K \\times N$ ``Projection'' or ``Pick'' matrix, linking each view to the assets} \\\\\n",
     47     "\\Omega & \\mbox{A Covariance matrix representing the uncertainty of views}\n",
     48     "\\end{array}\n",
     49     "\n",
     50     "\n",
     51     "Views are represented in $Q$ and $P$ as follows:\n",
     52     "\n",
     53     "If the $k$-th view is an absolute view, it is represented by setting $Q_k$ to the expected return of asset $k$ and setting $P_{ki}$ to 1 and all other elements of row $k$ in $P$ to zero.\n",
     54     "\n",
     55     "If the $k$-th view is an relative view, between assets $i$ and $j$ it is represented by setting $Q_k$ to the expected difference of returns between assets $i$ and $j$, and setting $P_{ki}$ to $-1$ for the underperforming asset, $P_{kj}$ to $+1$ and all other elements of row $k$ in $P$ to zero. $\\Omega$ is either set to the specified uncertainty or is inferred from the user or from the data.\n",
     56     "\n",
     57     "The uncertainty of the views $\\Omega$ is either set by the user, or inferred (e.g. via statements of confidence, from market data, from the variance of residuals from a prediction model used to generate the views etc, we shall see examples in sections below). In particular, \\cite{he1999intuition} suggest setting it to be the diagonal matrix obtained from the diagonal elements of $P \\tau \\Sigma P^T$, which is what we shall do for some of our initial tests. In my implementation the code accepts a matrix, but uses this assumption as the default if the user does not specify a matrix to use as $\\Omega$.\n",
     58     "\n",
     59     "#### The Master Formula\n",
     60     "\n",
     61     "The first step of the procedure is a _reverse-optimization_ step that infers the implied returns vector $\\pi$ that are implied by the equilibrium weights $w$ using the formula:\n",
     62     "\n",
     63     "$$\\pi = \\delta\\Sigma w$$\n",
     64     "\n",
     65     "Next, the posterior returns and covariances are obtained from the _Black-Litterman Master Formula_ which is the following set of equations:\n",
     66     "\n",
     67     "\\begin{equation}\n",
     68     "\\label{eq:blMuOrig}\n",
     69     "\\mu^{BL} = [(\\tau\\Sigma)^{-1} + P \\Omega^{-1} P]^{-1}[(\\tau\\Sigma)^{-1} \\pi + P \\Omega^{-1} Q]\n",
     70     "\\end{equation}\n",
     71     "\n",
     72     "\\begin{equation}\n",
     73     "\\label{eq:blSigmaOrig}\n",
     74     "\\Sigma^{BL} = \\Sigma + [(\\tau\\Sigma)^{-1} + P \\Omega^{-1} P]^{-1}\n",
     75     "\\end{equation}\n",
     76     "\n",
     77     "#### Inverting $\\Omega$\n",
     78     "\n",
     79     "While the master formulas identified in Equation \\ref{eq:blMuOrig} and Equation \\ref{eq:blSigmaOrig} are frequently easy to implement, they do involve the term $\\Omega^{-1}$. Unfortuantely, $\\Omega$ is sometimes non-invertible, which poses difficulties to implement the equations as-is. Fortunately the equations are easily transformed to a form that does not require this troublesome inversion. Therefore, frequently, implementations use the following equivalent versions of these equations which are sometimes computationally more stable, since they do not involve inverting $\\Omega$. Derivations of these alternate forms are provided in the appendices of \\cite{walters2011black}:\n",
     80     "\n",
     81     "\\begin{equation}\n",
     82     "\\label{eq:blMu}\n",
     83     "\\mu^{BL} = \\pi + \\tau \\Sigma P^T[(P \\tau \\Sigma P^T) + \\Omega]^{-1}[Q - P \\pi]\n",
     84     "\\end{equation}\n",
     85     "\n",
     86     "\\begin{equation}\n",
     87     "\\label{eq:blSigma}\n",
     88     "\\Sigma^{BL} = \\Sigma + \\tau \\Sigma - \\tau\\Sigma P^T(P \\tau \\Sigma P^T + \\Omega)^{-1} P \\tau \\Sigma\n",
     89     "\\end{equation}\n"
     90    ]
     91   },
     92   {
     93    "cell_type": "markdown",
     94    "metadata": {},
     95    "source": [
     96     "### Flavors of Black-Litterman\n",
     97     "\n",
     98     "The original method described above has also seen a number of modifications and extensions (e.g. see \\cite{walters2011black} for an extensive and detailed summary) to the point where there is some confusion about exactly what comprises the true _Black-Litterman_ model.\n",
     99     "\n",
    100     "I shall use a nomenclature that is consistent with \\cite{walters2011black}. Walters classifies implementations in two broad categories. The first category was implemented by \\cite{black1992global} and \\cite{he1999intuition}, and Walters refers to these as the _Reference Model_. The second category consists of well known implementations described in \\cite{satchell2000demystification} and a series of papers by Meucci (e.g. \\cite{meucci2005beyond}, \\cite{meucci2009enhancing}, \\cite{meucci2012fully}). In these models, the $\\tau$ parameter is eliminated, either by setting it to 1 or by incorporating it into the $\\Omega$ matrix.\n",
    101     "\n",
    102     "For the rest of this document, I shall be restricting myself to the _Reference Model_ as originally described in \\cite{black1992global} and \\cite{he1999intuition}, and I shall not be implementing the extensions of Meucci and others.\n",
    103     "\n",
    104     "### Implementation Overview\n",
    105     "\n",
    106     "The rest of this notebook proceeds as follows. In the following section, I shall implement the Black Litterman procedure in Python and annotate the code as I proceed, to illustrate each step. I then use the code to exactly reproduce the results in \\cite{he1999intuition}.\n",
    107     " \n",
    108     "Having established that the code accurately implements the Black Litterman procedure, I shall get down apply the procedure to the Fama French 6-portfolio allocation problem. Along the way, my tests will impose absolute views as well as relative views, and test the impact of the procedure on portfolios using a range of Seven different prediction strategies to obtain views. I also backtest these strategies over time and examine various portfolio metrics, while comparing the Black Litterman derived (BL) expected returns being supplied to an optimizer with weights obtained from Naive Mean-Variance optimization using expected returns and covariance matrixes directly from the prediction strategy. Finally, I conclude the section by examining the impact of these portfolios on transaction costs. \n",
    109     " \n",
    110     "## Annotated Implementation of Black-Litterman\n",
    111     "### The Code\n",
    112     "\n",
    113     "The Black Litterman procedure is implemented in Python in the function `bl`. Before we implement the body of `bl`, let's build a few helper functions that will hopefully make the code a bit easier to understand and deal with.\n",
    114     "\n",
    115     "numpy treats a column vector differently from a 1 dimensional array. In order to consistently use column vectors, the following helper function takes either a numpy array or a numpy one-column matrix (i.e. a column vector) and returns the data as a column vector. Let's call this function `as_colvec`\n"
    116    ]
    117   },
    118   {
    119    "cell_type": "code",
    120    "execution_count": 1,
    121    "metadata": {},
    122    "outputs": [],
    123    "source": [
    124     "import numpy as np\n",
    125     "import pandas as pd\n",
    126     "\n",
    127     "def as_colvec(x):\n",
    128     "    if (x.ndim == 2):\n",
    129     "        return x\n",
    130     "    else:\n",
    131     "        return np.expand_dims(x, axis=1)\n"
    132    ]
    133   },
    134   {
    135    "cell_type": "code",
    136    "execution_count": 2,
    137    "metadata": {},
    138    "outputs": [
    139     {
    140      "data": {
    141       "text/plain": [
    142        "array([0, 1, 2, 3])"
    143       ]
    144      },
    145      "execution_count": 2,
    146      "metadata": {},
    147      "output_type": "execute_result"
    148     }
    149    ],
    150    "source": [
    151     "np.arange(4)"
    152    ]
    153   },
    154   {
    155    "cell_type": "code",
    156    "execution_count": 3,
    157    "metadata": {},
    158    "outputs": [
    159     {
    160      "data": {
    161       "text/plain": [
    162        "array([[0],\n",
    163        "       [1],\n",
    164        "       [2],\n",
    165        "       [3]])"
    166       ]
    167      },
    168      "execution_count": 3,
    169      "metadata": {},
    170      "output_type": "execute_result"
    171     }
    172    ],
    173    "source": [
    174     "as_colvec(np.arange(4))"
    175    ]
    176   },
    177   {
    178    "cell_type": "markdown",
    179    "metadata": {},
    180    "source": [
    181     "Recall that the first step in the Black Litterman procedure was to reverse engineer the implied returns vector $\\pi$ from a set of portfolio weights $w$. \n",
    182     "\n",
    183     "$$\\pi = \\delta\\Sigma w$$\n",
    184     "\n",
    185     "This is performed by the following code:"
    186    ]
    187   },
    188   {
    189    "cell_type": "code",
    190    "execution_count": 4,
    191    "metadata": {},
    192    "outputs": [],
    193    "source": [
    194     "def implied_returns(delta, sigma, w):\n",
    195     "    \"\"\"\n",
    196     "Obtain the implied expected returns by reverse engineering the weights\n",
    197     "Inputs:\n",
    198     "delta: Risk Aversion Coefficient (scalar)\n",
    199     "sigma: Variance-Covariance Matrix (N x N) as DataFrame\n",
    200     "    w: Portfolio weights (N x 1) as Series\n",
    201     "Returns an N x 1 vector of Returns as Series\n",
    202     "    \"\"\"\n",
    203     "    ir = delta * sigma.dot(w).squeeze() # to get a series from a 1-column dataframe\n",
    204     "    ir.name = 'Implied Returns'\n",
    205     "    return ir\n"
    206    ]
    207   },
    208   {
    209    "cell_type": "markdown",
    210    "metadata": {},
    211    "source": [
    212     "As we noted previously, \\cite{he1999intuition} suggest that if the investor does not have a specific way to explicitly quantify the uncertaintly associated with the view in the $\\Omega$ matrix, one could make the simplifying assumption that $\\Omega$ is proportional to the variance of the prior.\n",
    213     "\n",
    214     "Specifically, they suggest that:\n",
    215     "\n",
    216     "$$\\Omega = diag(P (\\tau \\Sigma) P^T) $$\n",
    217     "\n",
    218     "This is implemented in Python as:"
    219    ]
    220   },
    221   {
    222    "cell_type": "code",
    223    "execution_count": 5,
    224    "metadata": {},
    225    "outputs": [],
    226    "source": [
    227     "# Assumes that Omega is proportional to the variance of the prior\n",
    228     "def proportional_prior(sigma, tau, p):\n",
    229     "    \"\"\"\n",
    230     "    Returns the He-Litterman simplified Omega\n",
    231     "    Inputs:\n",
    232     "    sigma: N x N Covariance Matrix as DataFrame\n",
    233     "    tau: a scalar\n",
    234     "    p: a K x N DataFrame linking Q and Assets\n",
    235     "    returns a P x P DataFrame, a Matrix representing Prior Uncertainties\n",
    236     "    \"\"\"\n",
    237     "    helit_omega = p.dot(tau * sigma).dot(p.T)\n",
    238     "    # Make a diag matrix from the diag elements of Omega\n",
    239     "    return pd.DataFrame(np.diag(np.diag(helit_omega.values)),index=p.index, columns=p.index)\n"
    240    ]
    241   },
    242   {
    243    "cell_type": "markdown",
    244    "metadata": {},
    245    "source": [
    246     "We use this function to compute the posterior expected returns as follows:"
    247    ]
    248   },
    249   {
    250    "cell_type": "code",
    251    "execution_count": 6,
    252    "metadata": {},
    253    "outputs": [],
    254    "source": [
    255     "from numpy.linalg import inv\n",
    256     "\n",
    257     "def bl(w_prior, sigma_prior, p, q,\n",
    258     "                omega=None,\n",
    259     "                delta=2.5, tau=.02):\n",
    260     "    \"\"\"\n",
    261     "# Computes the posterior expected returns based on \n",
    262     "# the original black litterman reference model\n",
    263     "#\n",
    264     "# W.prior must be an N x 1 vector of weights, a Series\n",
    265     "# Sigma.prior is an N x N covariance matrix, a DataFrame\n",
    266     "# P must be a K x N matrix linking Q and the Assets, a DataFrame\n",
    267     "# Q must be an K x 1 vector of views, a Series\n",
    268     "# Omega must be a K x K matrix a DataFrame, or None\n",
    269     "# if Omega is None, we assume it is\n",
    270     "#    proportional to variance of the prior\n",
    271     "# delta and tau are scalars\n",
    272     "    \"\"\"\n",
    273     "    if omega is None:\n",
    274     "        omega = proportional_prior(sigma_prior, tau, p)\n",
    275     "    # Force w.prior and Q to be column vectors\n",
    276     "    # How many assets do we have?\n",
    277     "    N = w_prior.shape[0]\n",
    278     "    # And how many views?\n",
    279     "    K = q.shape[0]\n",
    280     "    # First, reverse-engineer the weights to get pi\n",
    281     "    pi = implied_returns(delta, sigma_prior,  w_prior)\n",
    282     "    # Adjust (scale) Sigma by the uncertainty scaling factor\n",
    283     "    sigma_prior_scaled = tau * sigma_prior  \n",
    284     "    # posterior estimate of the mean, use the \"Master Formula\"\n",
    285     "    # we use the versions that do not require\n",
    286     "    # Omega to be inverted (see previous section)\n",
    287     "    # this is easier to read if we use '@' for matrixmult instead of .dot()\n",
    288     "    #     mu_bl = pi + sigma_prior_scaled @ p.T @ inv(p @ sigma_prior_scaled @ p.T + omega) @ (q - p @ pi)\n",
    289     "    mu_bl = pi + sigma_prior_scaled.dot(p.T).dot(inv(p.dot(sigma_prior_scaled).dot(p.T) + omega).dot(q - p.dot(pi).values))\n",
    290     "    # posterior estimate of uncertainty of mu.bl\n",
    291     "#     sigma_bl = sigma_prior + sigma_prior_scaled - sigma_prior_scaled @ p.T @ inv(p @ sigma_prior_scaled @ p.T + omega) @ p @ sigma_prior_scaled\n",
    292     "    sigma_bl = sigma_prior + sigma_prior_scaled - sigma_prior_scaled.dot(p.T).dot(inv(p.dot(sigma_prior_scaled).dot(p.T) + omega)).dot(p).dot(sigma_prior_scaled)\n",
    293     "    return (mu_bl, sigma_bl)\n"
    294    ]
    295   },
    296   {
    297    "cell_type": "markdown",
    298    "metadata": {},
    299    "source": [
    300     "### A Simple Example: Absolute Views\n",
    301     "\n",
    302     "We start with a simple 2-Asset example. Let's start with an example from _Statistical Models and Methods for Financial Markets (Springer Texts in Statistics) 2008th Edition, Tze Lai and Haipeng Xing_.\n",
    303     "\n",
    304     "Consider the portfolio consisting of just two stocks: Intel (INTC) and Pfizer (PFE).\n",
    305     "\n",
    306     "From Table 3.1 on page 72 of the book, we obtain the covariance matrix (multipled by $10^4$)\n",
    307     "\n",
    308     "\\begin{array}{lcc}\n",
    309     "INTC & 46.0 & 1.06 \\\\\n",
    310     "PFE   & 1.06 & 5.33\n",
    311     "\\end{array}\n",
    312     "\n",
    313     "Assume that Intel has a market capitalization of approximately USD 80B and that of Pfizer is approximately USD 100B (this is not quite accurate, but works just fine as an example!).\n",
    314     "Thus, if you held a market-cap weighted portfolio you would hold INTC and PFE with the following weights: $W_{INTC} = 80/180 = 44\\%, W_{PFE} = 100/180 = 56\\%$. These appear to be reasonable weights without an extreme allocation to either stock, even though Pfizer is slightly overweighted.\n",
    315     "\n",
    316     "We can compute the equilibrium implied returns $\\pi$ as follows:\n"
    317    ]
    318   },
    319   {
    320    "cell_type": "code",
    321    "execution_count": 7,
    322    "metadata": {},
    323    "outputs": [
    324     {
    325      "data": {
    326       "text/plain": [
    327        "INTC    0.052084\n",
    328        "PFE     0.008628\n",
    329        "Name: Implied Returns, dtype: float64"
    330       ]
    331      },
    332      "execution_count": 7,
    333      "metadata": {},
    334      "output_type": "execute_result"
    335     }
    336    ],
    337    "source": [
    338     "tickers = ['INTC', 'PFE']\n",
    339     "s = pd.DataFrame([[46.0, 1.06], [1.06, 5.33]], index=tickers, columns=tickers) *  10E-4\n",
    340     "pi = implied_returns(delta=2.5, sigma=s, w=pd.Series([.44, .56], index=tickers))\n",
    341     "pi"
    342    ]
    343   },
    344   {
    345    "cell_type": "markdown",
    346    "metadata": {},
    347    "source": [
    348     "Thus the equilibrium implied returns for INTC are a bit more than 5\\% and a bit less than 1\\% for PFE.\n",
    349     "\n",
    350     "Assume that the investor thinks that Intel will return 2\\% and that Pfizer is poised to rebounce, and will return 4\\% . We can now examine the optimal weights according to the Markowitz procedure.\n",
    351     "What would happen if we used these expected returns to compute the Optimal Max Sharpe Ratio portfolio?\n",
    352     "\n",
    353     "The Max Sharpe Ratio (MSR) Portfolio weights are easily computed in explicit form if there are no constraints on the weights.\n",
    354     "The weights are given by the expression (e.g. See  \\cite{campbell1996econometrics} page 188 Equation 5.2.28):\n",
    355     "\n",
    356     "$$ W_{MSR} = \\frac{\\Sigma^{-1}\\mu_e}{\\bf{1}^T \\Sigma^{-1}\\mu_e} $$\n",
    357     "\n",
    358     "where $\\mu_e$ is the vector of expected excess returns and $\\Sigma$ is the variance-covariance matrix.\n",
    359     "\n",
    360     "This is implemented as follows:\n",
    361     "\n"
    362    ]
    363   },
    364   {
    365    "cell_type": "code",
    366    "execution_count": 8,
    367    "metadata": {},
    368    "outputs": [],
    369    "source": [
    370     "# for convenience and readability, define the inverse of a dataframe\n",
    371     "def inverse(d):\n",
    372     "    \"\"\"\n",
    373     "    Invert the dataframe by inverting the underlying matrix\n",
    374     "    \"\"\"\n",
    375     "    return pd.DataFrame(inv(d.values), index=d.columns, columns=d.index)\n",
    376     "\n",
    377     "def w_msr(sigma, mu, scale=True):\n",
    378     "    \"\"\"\n",
    379     "    Optimal (Tangent/Max Sharpe Ratio) Portfolio weights\n",
    380     "    by using the Markowitz Optimization Procedure\n",
    381     "    Mu is the vector of Excess expected Returns\n",
    382     "    Sigma must be an N x N matrix as a DataFrame and Mu a column vector as a Series\n",
    383     "    This implements page 188 Equation 5.2.28 of\n",
    384     "    \"The econometrics of financial markets\" Campbell, Lo and Mackinlay.\n",
    385     "    \"\"\"\n",
    386     "    w = inverse(sigma).dot(mu)\n",
    387     "    if scale:\n",
    388     "        w = w/sum(w) # fix: this assumes all w is +ve\n",
    389     "    return w\n"
    390    ]
    391   },
    392   {
    393    "cell_type": "markdown",
    394    "metadata": {},
    395    "source": [
    396     "Recall that the investor expects that Intel will return 2\\% and Pfizer will return 4\\% . We can now examine the optimal weights obtained by naively implementing the Markowitz procedure with these expected returns."
    397    ]
    398   },
    399   {
    400    "cell_type": "code",
    401    "execution_count": 9,
    402    "metadata": {},
    403    "outputs": [
    404     {
    405      "data": {
    406       "text/plain": [
    407        "INTC     3.41\n",
    408        "PFE     96.59\n",
    409        "dtype: float64"
    410       ]
    411      },
    412      "execution_count": 9,
    413      "metadata": {},
    414      "output_type": "execute_result"
    415     }
    416    ],
    417    "source": [
    418     "mu_exp = pd.Series([.02, .04],index=tickers) # INTC and PFE\n",
    419     "np.round(w_msr(s, mu_exp)*100, 2)\n"
    420    ]
    421   },
    422   {
    423    "cell_type": "markdown",
    424    "metadata": {},
    425    "source": [
    426     "Consistent with the poor reputation of naive Markowitz optimization, the Markwitz procedure places an unrealistic weight of more than 96\\% in Pfizer and less than 4\\% in Intel. This is completely impractical and no reasonable investor would make such dramatic bets.\n",
    427     "\n",
    428     "In contrast, let us now find the weights that the Black Litterman procedure would place. We allow $\\Omega$ to be computed automatically, and are willing to use all the other defaults. We find the Black Litterman weights as follows:"
    429    ]
    430   },
    431   {
    432    "cell_type": "code",
    433    "execution_count": 10,
    434    "metadata": {},
    435    "outputs": [
    436     {
    437      "data": {
    438       "text/plain": [
    439        "INTC    0.037622\n",
    440        "PFE     0.024111\n",
    441        "dtype: float64"
    442       ]
    443      },
    444      "execution_count": 10,
    445      "metadata": {},
    446      "output_type": "execute_result"
    447     }
    448    ],
    449    "source": [
    450     "# Absolute view 1: INTC will return 2%\n",
    451     "# Absolute view 2: PFE will return 4%\n",
    452     "q = pd.Series({'INTC': 0.02, 'PFE': 0.04})\n",
    453     "\n",
    454     "# The Pick Matrix\n",
    455     "# For View 2, it is for PFE\n",
    456     "p = pd.DataFrame([\n",
    457     "# For View 1, this is for INTC\n",
    458     "    {'INTC': 1, 'PFE': 0},\n",
    459     "# For View 2, it is for PFE\n",
    460     "    {'INTC': 0, 'PFE': 1}\n",
    461     "    ])\n",
    462     "\n",
    463     "# Find the Black Litterman Expected Returns\n",
    464     "bl_mu, bl_sigma = bl(w_prior=pd.Series({'INTC':.44, 'PFE':.56}), sigma_prior=s, p=p, q=q)\n",
    465     "# Black Litterman Implied Mu\n",
    466     "bl_mu\n"
    467    ]
    468   },
    469   {
    470    "cell_type": "markdown",
    471    "metadata": {},
    472    "source": [
    473     "The posterior returns returned by the procedure are clearly weighted between that of the equilibrium implied expected returns (in the range of 5\\% and 1\\%) and that of the investor (2\\% and 4\\%). The question is are these weights likely to yield more realistic portfolios? To answer that question we supply the Black Litterman expected returns and covariance matrix to the optimizer:"
    474    ]
    475   },
    476   {
    477    "cell_type": "code",
    478    "execution_count": 11,
    479    "metadata": {},
    480    "outputs": [
    481     {
    482      "data": {
    483       "text/plain": [
    484        "INTC    0.140692\n",
    485        "PFE     0.859308\n",
    486        "dtype: float64"
    487       ]
    488      },
    489      "execution_count": 11,
    490      "metadata": {},
    491      "output_type": "execute_result"
    492     }
    493    ],
    494    "source": [
    495     "# Use the Black Litterman expected returns to get the Optimal Markowitz weights\n",
    496     "w_msr(bl_sigma, bl_mu)"
    497    ]
    498   },
    499   {
    500    "cell_type": "markdown",
    501    "metadata": {},
    502    "source": [
    503     "We see that we get much more reasonable weights than we did with naive optimization. These weights are also much closer to the 45-55 mix in the cap weighted portfolio.\n",
    504     "On the other hand, they respect the investor's view that expects Pfizer to rebound, and places a higher weight on Pfizer relative to the cap weighted portfolio.\n",
    505     "\n",
    506     "### A Simple Example: Relative Views\n",
    507     "\n",
    508     "In this example, we examine relative views. We stick with our simple 2-stock example. Recall that the Cap-Weighted implied expected returns are:\n"
    509    ]
    510   },
    511   {
    512    "cell_type": "code",
    513    "execution_count": 12,
    514    "metadata": {},
    515    "outputs": [
    516     {
    517      "data": {
    518       "text/plain": [
    519        "INTC    0.052084\n",
    520        "PFE     0.008628\n",
    521        "Name: Implied Returns, dtype: float64"
    522       ]
    523      },
    524      "execution_count": 12,
    525      "metadata": {},
    526      "output_type": "execute_result"
    527     }
    528    ],
    529    "source": [
    530     "# Expected returns inferred from the cap-weights\n",
    531     "pi\n"
    532    ]
    533   },
    534   {
    535    "cell_type": "markdown",
    536    "metadata": {},
    537    "source": [
    538     "Recall also that the cap-weighted portfolio is approximately a 45-55 mix of Intel and Pfizer.\n",
    539     "\n",
    540     "Assume instead that the investor feels that the Intel will outperform Pfizer by only 2\\%. This view is implemented as follows:"
    541    ]
    542   },
    543   {
    544    "cell_type": "code",
    545    "execution_count": 13,
    546    "metadata": {},
    547    "outputs": [
    548     {
    549      "data": {
    550       "text/plain": [
    551        "INTC    0.041374\n",
    552        "PFE     0.009646\n",
    553        "dtype: float64"
    554       ]
    555      },
    556      "execution_count": 13,
    557      "metadata": {},
    558      "output_type": "execute_result"
    559     }
    560    ],
    561    "source": [
    562     "q = pd.Series([\n",
    563     "# Relative View 1: INTC will outperform PFE by 2%\n",
    564     "  0.02\n",
    565     "    ]\n",
    566     ")\n",
    567     "# The Pick Matrix\n",
    568     "p = pd.DataFrame([\n",
    569     "  # For View 1, this is for INTC outperforming PFE\n",
    570     "  {'INTC': +1, 'PFE': -1}\n",
    571     "])\n",
    572     "\n",
    573     "# Find the Black Litterman Expected Returns\n",
    574     "bl_mu, bl_sigma = bl(w_prior=pd.Series({'INTC': .44, 'PFE': .56}), sigma_prior=s, p=p, q=q)\n",
    575     "# Black Litterman Implied Mu\n",
    576     "bl_mu\n"
    577    ]
    578   },
    579   {
    580    "cell_type": "markdown",
    581    "metadata": {},
    582    "source": [
    583     "Once again we see that the Black Litterman expected returns are a blend between the cap-weight implied weights and the investor view. The outperformance of Intel in the implied returns is:"
    584    ]
    585   },
    586   {
    587    "cell_type": "code",
    588    "execution_count": 14,
    589    "metadata": {},
    590    "outputs": [
    591     {
    592      "data": {
    593       "text/plain": [
    594        "0.043456"
    595       ]
    596      },
    597      "execution_count": 14,
    598      "metadata": {},
    599      "output_type": "execute_result"
    600     }
    601    ],
    602    "source": [
    603     "pi[0]-pi[1]"
    604    ]
    605   },
    606   {
    607    "cell_type": "markdown",
    608    "metadata": {},
    609    "source": [
    610     "In contrast, the investor felt it only would be 2\\%. The expected returns returned by the Black Litterman procedure show a spread that is a blend between the cap-weight implied returns and that of the investor:"
    611    ]
    612   },
    613   {
    614    "cell_type": "code",
    615    "execution_count": 15,
    616    "metadata": {},
    617    "outputs": [
    618     {
    619      "data": {
    620       "text/plain": [
    621        "0.031728"
    622       ]
    623      },
    624      "execution_count": 15,
    625      "metadata": {},
    626      "output_type": "execute_result"
    627     }
    628    ],
    629    "source": [
    630     "bl_mu[0]-bl_mu[1]"
    631    ]
    632   },
    633   {
    634    "cell_type": "markdown",
    635    "metadata": {},
    636    "source": [
    637     "And, the weights in the Optimized portfolio when we use these expected returns are:\n"
    638    ]
    639   },
    640   {
    641    "cell_type": "code",
    642    "execution_count": 16,
    643    "metadata": {},
    644    "outputs": [
    645     {
    646      "data": {
    647       "text/plain": [
    648        "INTC    0.347223\n",
    649        "PFE     0.652777\n",
    650        "dtype: float64"
    651       ]
    652      },
    653      "execution_count": 16,
    654      "metadata": {},
    655      "output_type": "execute_result"
    656     }
    657    ],
    658    "source": [
    659     "# Use the Black Litterman expected returns and covariance matrix\n",
    660     "w_msr(bl_sigma, bl_mu)"
    661    ]
    662   },
    663   {
    664    "cell_type": "markdown",
    665    "metadata": {},
    666    "source": [
    667     "These seem like reasonable weights, and demonstrates the power of using the Black Litterman procedure. In contrast, consider the weights we would get if we implemented the same view without Black Litterman. We set the returns of Intel and Pfizer to be 3\\% and 1\\% respectively."
    668    ]
    669   },
    670   {
    671    "cell_type": "code",
    672    "execution_count": 17,
    673    "metadata": {},
    674    "outputs": [
    675     {
    676      "data": {
    677       "text/plain": [
    678        "INTC    0.258528\n",
    679        "PFE     0.741472\n",
    680        "dtype: float64"
    681       ]
    682      },
    683      "execution_count": 17,
    684      "metadata": {},
    685      "output_type": "execute_result"
    686     }
    687    ],
    688    "source": [
    689     "w_msr(s, [.03, .01])"
    690    ]
    691   },
    692   {
    693    "cell_type": "markdown",
    694    "metadata": {},
    695    "source": [
    696     "The weights are significantly more dramatic than one might be willing to implement, and are likely unwarranted given the relatively weak view. In fact, if the same view were implemented as Intel and Pfizer returning 2\\% and 0\\%, the results are even more extreme:"
    697    ]
    698   },
    699   {
    700    "cell_type": "code",
    701    "execution_count": 18,
    702    "metadata": {},
    703    "outputs": [
    704     {
    705      "data": {
    706       "text/plain": [
    707        "INTC    1.248244\n",
    708        "PFE    -0.248244\n",
    709        "dtype: float64"
    710       ]
    711      },
    712      "execution_count": 18,
    713      "metadata": {},
    714      "output_type": "execute_result"
    715     }
    716    ],
    717    "source": [
    718     "w_msr(s, [.02, .0])"
    719    ]
    720   },
    721   {
    722    "cell_type": "markdown",
    723    "metadata": {},
    724    "source": [
    725     "In this case, the Markowitz recommends shorting Pfizer to the extent of nearly 25\\% of the portfolio and leveraging Intel to 125\\%. Clearly this is not a plausible allocation based on the simple view expressed above.\n",
    726     "\n",
    727     "## Reproducing the He-Litterman (1999) Results\n",
    728     "\n",
    729     "We now reproduce the results in the He-Litterman paper that first detailed the steps in the procedure. We obtained the data by typing it in from the He-Litterman tables, and used it to test the implementation.\n",
    730     "\n",
    731     "The He-Litterman example involves an international allocation between 7 countries. The data is as follows:"
    732    ]
    733   },
    734   {
    735    "cell_type": "code",
    736    "execution_count": 19,
    737    "metadata": {},
    738    "outputs": [
    739     {
    740      "data": {
    741       "text/plain": [
    742        "AU    3.9\n",
    743        "CA    6.9\n",
    744        "FR    8.4\n",
    745        "DE    9.0\n",
    746        "JP    4.3\n",
    747        "UK    6.8\n",
    748        "US    7.6\n",
    749        "Name: Implied Returns, dtype: float64"
    750       ]
    751      },
    752      "execution_count": 19,
    753      "metadata": {},
    754      "output_type": "execute_result"
    755     }
    756    ],
    757    "source": [
    758     "# The 7 countries ...\n",
    759     "countries  = ['AU', 'CA', 'FR', 'DE', 'JP', 'UK', 'US'] \n",
    760     "# Table 1 of the He-Litterman paper\n",
    761     "# Correlation Matrix\n",
    762     "rho = pd.DataFrame([\n",
    763     "    [1.000,0.488,0.478,0.515,0.439,0.512,0.491],\n",
    764     "    [0.488,1.000,0.664,0.655,0.310,0.608,0.779],\n",
    765     "    [0.478,0.664,1.000,0.861,0.355,0.783,0.668],\n",
    766     "    [0.515,0.655,0.861,1.000,0.354,0.777,0.653],\n",
    767     "    [0.439,0.310,0.355,0.354,1.000,0.405,0.306],\n",
    768     "    [0.512,0.608,0.783,0.777,0.405,1.000,0.652],\n",
    769     "    [0.491,0.779,0.668,0.653,0.306,0.652,1.000]\n",
    770     "], index=countries, columns=countries)\n",
    771     "\n",
    772     "# Table 2 of the He-Litterman paper: volatilities\n",
    773     "vols = pd.DataFrame([0.160,0.203,0.248,0.271,0.210,0.200,0.187],index=countries, columns=[\"vol\"]) \n",
    774     "# Table 2 of the He-Litterman paper: cap-weights\n",
    775     "w_eq = pd.DataFrame([0.016,0.022,0.052,0.055,0.116,0.124,0.615], index=countries, columns=[\"CapWeight\"])\n",
    776     "# Compute the Covariance Matrix\n",
    777     "sigma_prior = vols.dot(vols.T) * rho\n",
    778     "# Compute Pi and compare:\n",
    779     "pi = implied_returns(delta=2.5, sigma=sigma_prior, w=w_eq)\n",
    780     "(pi*100).round(1)"
    781    ]
    782   },
    783   {
    784    "cell_type": "markdown",
    785    "metadata": {},
    786    "source": [
    787     "The values of $\\pi$ computed by the Python code exactly matches column 3 of Table 2"
    788    ]
    789   },
    790   {
    791    "cell_type": "markdown",
    792    "metadata": {},
    793    "source": [
    794     "### View 1: Germany vs Rest of Europe\n",
    795     "\n",
    796     "Next, we impose the view that German equities will outperform the rest of European equities by 5\\%.\n",
    797     "\n",
    798     "The other European equities are France and the UK. We split the outperformance proportional to the Market Caps of France and the UK.\n"
    799    ]
    800   },
    801   {
    802    "cell_type": "code",
    803    "execution_count": 20,
    804    "metadata": {},
    805    "outputs": [
    806     {
    807      "data": {
    808       "text/html": [
    809        "<div>\n",
    810        "<style scoped>\n",
    811        "    .dataframe tbody tr th:only-of-type {\n",
    812        "        vertical-align: middle;\n",
    813        "    }\n",
    814        "\n",
    815        "    .dataframe tbody tr th {\n",
    816        "        vertical-align: top;\n",
    817        "    }\n",
    818        "\n",
    819        "    .dataframe thead th {\n",
    820        "        text-align: right;\n",
    821        "    }\n",
    822        "</style>\n",
    823        "<table border=\"1\" class=\"dataframe\">\n",
    824        "  <thead>\n",
    825        "    <tr style=\"text-align: right;\">\n",
    826        "      <th></th>\n",
    827        "      <th>AU</th>\n",
    828        "      <th>CA</th>\n",
    829        "      <th>FR</th>\n",
    830        "      <th>DE</th>\n",
    831        "      <th>JP</th>\n",
    832        "      <th>UK</th>\n",
    833        "      <th>US</th>\n",
    834        "    </tr>\n",
    835        "  </thead>\n",
    836        "  <tbody>\n",
    837        "    <tr>\n",
    838        "      <th>0</th>\n",
    839        "      <td>0.0</td>\n",
    840        "      <td>0.0</td>\n",
    841        "      <td>-29.5</td>\n",
    842        "      <td>100.0</td>\n",
    843        "      <td>0.0</td>\n",
    844        "      <td>-70.5</td>\n",
    845        "      <td>0.0</td>\n",
    846        "    </tr>\n",
    847        "  </tbody>\n",
    848        "</table>\n",
    849        "</div>"
    850       ],
    851       "text/plain": [
    852        "    AU   CA    FR     DE   JP    UK   US\n",
    853        "0  0.0  0.0 -29.5  100.0  0.0 -70.5  0.0"
    854       ]
    855      },
    856      "execution_count": 20,
    857      "metadata": {},
    858      "output_type": "execute_result"
    859     }
    860    ],
    861    "source": [
    862     "# Germany will outperform other European Equities (i.e. FR and UK) by 5%\n",
    863     "q = pd.Series([.05]) # just one view\n",
    864     "# start with a single view, all zeros and overwrite the specific view\n",
    865     "p = pd.DataFrame([0.]*len(countries), index=countries).T\n",
    866     "# find the relative market caps of FR and UK to split the\n",
    867     "# relative outperformance of DE ...\n",
    868     "w_fr =  w_eq.loc[\"FR\"]/(w_eq.loc[\"FR\"]+w_eq.loc[\"UK\"])\n",
    869     "w_uk =  w_eq.loc[\"UK\"]/(w_eq.loc[\"FR\"]+w_eq.loc[\"UK\"])\n",
    870     "p.iloc[0]['DE'] = 1.\n",
    871     "p.iloc[0]['FR'] = -w_fr\n",
    872     "p.iloc[0]['UK'] = -w_uk\n",
    873     "(p*100).round(1)\n"
    874    ]
    875   },
    876   {
    877    "cell_type": "markdown",
    878    "metadata": {},
    879    "source": [
    880     " The results of implementing this view appear in the He-Litterman paper in Table 4. This exactly reproduces column 1 of Table 4. Next, we examine the values of $\\mu^{BL}$:\n"
    881    ]
    882   },
    883   {
    884    "cell_type": "code",
    885    "execution_count": 21,
    886    "metadata": {},
    887    "outputs": [
    888     {
    889      "data": {
    890       "text/plain": [
    891        "AU     4.3\n",
    892        "CA     7.6\n",
    893        "FR     9.3\n",
    894        "DE    11.0\n",
    895        "JP     4.5\n",
    896        "UK     7.0\n",
    897        "US     8.1\n",
    898        "dtype: float64"
    899       ]
    900      },
    901      "execution_count": 21,
    902      "metadata": {},
    903      "output_type": "execute_result"
    904     }
    905    ],
    906    "source": [
    907     "delta = 2.5\n",
    908     "tau = 0.05 # from Footnote 8\n",
    909     "# Find the Black Litterman Expected Returns\n",
    910     "bl_mu, bl_sigma = bl(w_eq, sigma_prior, p, q, tau = tau)\n",
    911     "(bl_mu*100).round(1)\n"
    912    ]
    913   },
    914   {
    915    "cell_type": "markdown",
    916    "metadata": {},
    917    "source": [
    918     "The  Black Litterman expected returns computed by the code exactly reproduces column 2 of Table 4.\n",
    919     "\n",
    920     "He-Litterman compute the optimal portfolio $w^*$ as follows (this is Equation (13) on page 6 of their paper)\n"
    921    ]
    922   },
    923   {
    924    "cell_type": "code",
    925    "execution_count": 22,
    926    "metadata": {},
    927    "outputs": [
    928     {
    929      "data": {
    930       "text/plain": [
    931        "AU     1.5\n",
    932        "CA     2.1\n",
    933        "FR    -4.0\n",
    934        "DE    35.4\n",
    935        "JP    11.0\n",
    936        "UK    -9.5\n",
    937        "US    58.6\n",
    938        "dtype: float64"
    939       ]
    940      },
    941      "execution_count": 22,
    942      "metadata": {},
    943      "output_type": "execute_result"
    944     }
    945    ],
    946    "source": [
    947     "def w_star(delta, sigma, mu):\n",
    948     "    return (inverse(sigma).dot(mu))/delta\n",
    949     "\n",
    950     "wstar = w_star(delta=2.5, sigma=bl_sigma, mu=bl_mu)\n",
    951     "# display w*\n",
    952     "(wstar*100).round(1)\n"
    953    ]
    954   },
    955   {
    956    "cell_type": "markdown",
    957    "metadata": {},
    958    "source": [
    959     "The computed $w^*$ exactly replicates column 3 ($w^*$) of Table 4. Finally, they compute $w^* - \\frac{w_{eq}}{1+\\tau}$ which is the difference in weights between the optimal portfolio and the equilibrium portfolio (they use unscaled weights) in column 4. We replicate that column as follows:"
    960    ]
    961   },
    962   {
    963    "cell_type": "code",
    964    "execution_count": 23,
    965    "metadata": {},
    966    "outputs": [
    967     {
    968      "data": {
    969       "text/plain": [
    970        "AU     0.0\n",
    971        "CA    -0.0\n",
    972        "FR    -8.9\n",
    973        "DE    30.2\n",
    974        "JP     0.0\n",
    975        "UK   -21.3\n",
    976        "US     0.0\n",
    977        "dtype: float64"
    978       ]
    979      },
    980      "execution_count": 23,
    981      "metadata": {},
    982      "output_type": "execute_result"
    983     }
    984    ],
    985    "source": [
    986     "w_eq  = w_msr(delta*sigma_prior, pi, scale=False)\n",
    987     "# Display the difference in Posterior and Prior weights\n",
    988     "np.round(wstar - w_eq/(1+tau), 3)*100\n"
    989    ]
    990   },
    991   {
    992    "cell_type": "markdown",
    993    "metadata": {},
    994    "source": [
    995     "which exactly matches Column 4 of Table 4. This completes our reproduction of the first view in He-Litterman (1999).\n",
    996     "\n",
    997     "Note that this demonstrates the power of the approach. The weights for assets that do not involve the view remain unchanged. The two underperforming countries (according to the view) are underweighted, while the overperforming country is overweighted, but not to the extreme extent that a naive portfolio optimizer would have produced."
    998    ]
    999   },
   1000   {
   1001    "cell_type": "markdown",
   1002    "metadata": {},
   1003    "source": [
   1004     "### View 2: Canada vs US\n",
   1005     "\n",
   1006     "For their second case, He and Litterman implement the additional view that Canadian Equities will outperform US Equities by 3\\%. The results are in (their) Table 5, which we shall now reproduce."
   1007    ]
   1008   },
   1009   {
   1010    "cell_type": "code",
   1011    "execution_count": 24,
   1012    "metadata": {},
   1013    "outputs": [
   1014     {
   1015      "data": {
   1016       "text/html": [
   1017        "<div>\n",
   1018        "<style scoped>\n",
   1019        "    .dataframe tbody tr th:only-of-type {\n",
   1020        "        vertical-align: middle;\n",
   1021        "    }\n",
   1022        "\n",
   1023        "    .dataframe tbody tr th {\n",
   1024        "        vertical-align: top;\n",
   1025        "    }\n",
   1026        "\n",
   1027        "    .dataframe thead th {\n",
   1028        "        text-align: right;\n",
   1029        "    }\n",
   1030        "</style>\n",
   1031        "<table border=\"1\" class=\"dataframe\">\n",
   1032        "  <thead>\n",
   1033        "    <tr style=\"text-align: right;\">\n",
   1034        "      <th></th>\n",
   1035        "      <th>0</th>\n",
   1036        "      <th>1</th>\n",
   1037        "    </tr>\n",
   1038        "  </thead>\n",
   1039        "  <tbody>\n",
   1040        "    <tr>\n",
   1041        "      <th>AU</th>\n",
   1042        "      <td>0.0</td>\n",
   1043        "      <td>0.0</td>\n",
   1044        "    </tr>\n",
   1045        "    <tr>\n",
   1046        "      <th>CA</th>\n",
   1047        "      <td>0.0</td>\n",
   1048        "      <td>100.0</td>\n",
   1049        "    </tr>\n",
   1050        "    <tr>\n",
   1051        "      <th>FR</th>\n",
   1052        "      <td>-29.5</td>\n",
   1053        "      <td>0.0</td>\n",
   1054        "    </tr>\n",
   1055        "    <tr>\n",
   1056        "      <th>DE</th>\n",
   1057        "      <td>100.0</td>\n",
   1058        "      <td>0.0</td>\n",
   1059        "    </tr>\n",
   1060        "    <tr>\n",
   1061        "      <th>JP</th>\n",
   1062        "      <td>0.0</td>\n",
   1063        "      <td>0.0</td>\n",
   1064        "    </tr>\n",
   1065        "    <tr>\n",
   1066        "      <th>UK</th>\n",
   1067        "      <td>-70.5</td>\n",
   1068        "      <td>0.0</td>\n",
   1069        "    </tr>\n",
   1070        "    <tr>\n",
   1071        "      <th>US</th>\n",
   1072        "      <td>0.0</td>\n",
   1073        "      <td>-100.0</td>\n",
   1074        "    </tr>\n",
   1075        "  </tbody>\n",
   1076        "</table>\n",
   1077        "</div>"
   1078       ],
   1079       "text/plain": [
   1080        "        0      1\n",
   1081        "AU    0.0    0.0\n",
   1082        "CA    0.0  100.0\n",
   1083        "FR  -29.5    0.0\n",
   1084        "DE  100.0    0.0\n",
   1085        "JP    0.0    0.0\n",
   1086        "UK  -70.5    0.0\n",
   1087        "US    0.0 -100.0"
   1088       ]
   1089      },
   1090      "execution_count": 24,
   1091      "metadata": {},
   1092      "output_type": "execute_result"
   1093     }
   1094    ],
   1095    "source": [
   1096     "view2 = pd.Series([.03], index=[1])\n",
   1097     "q = q.append(view2)\n",
   1098     "pick2 = pd.DataFrame([0.]*len(countries), index=countries, columns=[1]).T\n",
   1099     "p = p.append(pick2)\n",
   1100     "p.iloc[1]['CA']=+1\n",
   1101     "p.iloc[1]['US']=-1\n",
   1102     "np.round(p.T, 3)*100"
   1103    ]
   1104   },
   1105   {
   1106    "cell_type": "markdown",
   1107    "metadata": {},
   1108    "source": [
   1109     "This matches columns 1 and 2 of Table 5. We now compute the Black Litterman weights as\n",
   1110     "before:"
   1111    ]
   1112   },
   1113   {
   1114    "cell_type": "code",
   1115    "execution_count": 25,
   1116    "metadata": {},
   1117    "outputs": [
   1118     {
   1119      "data": {
   1120       "text/plain": [
   1121        "AU     4.4\n",
   1122        "CA     8.7\n",
   1123        "FR     9.5\n",
   1124        "DE    11.2\n",
   1125        "JP     4.6\n",
   1126        "UK     7.0\n",
   1127        "US     7.5\n",
   1128        "dtype: float64"
   1129       ]
   1130      },
   1131      "execution_count": 25,
   1132      "metadata": {},
   1133      "output_type": "execute_result"
   1134     }
   1135    ],
   1136    "source": [
   1137     "bl_mu, bl_sigma = bl(w_eq, sigma_prior, p, q, tau = tau)\n",
   1138     "np.round(bl_mu*100, 1)"
   1139    ]
   1140   },
   1141   {
   1142    "cell_type": "markdown",
   1143    "metadata": {},
   1144    "source": [
   1145     "The Black Litterman expected returns computed by the Python code exactly reproduces column 3 of\n",
   1146     "Table 5.\n",
   1147     "He-Litterman compute the optimal portfolio w ∗ as follows (this is Equation (13) on page 6 of\n",
   1148     "their paper)"
   1149    ]
   1150   },
   1151   {
   1152    "cell_type": "code",
   1153    "execution_count": 26,
   1154    "metadata": {},
   1155    "outputs": [
   1156     {
   1157      "data": {
   1158       "text/plain": [
   1159        "AU     1.5\n",
   1160        "CA    41.9\n",
   1161        "FR    -3.4\n",
   1162        "DE    33.6\n",
   1163        "JP    11.0\n",
   1164        "UK    -8.2\n",
   1165        "US    18.8\n",
   1166        "dtype: float64"
   1167       ]
   1168      },
   1169      "execution_count": 26,
   1170      "metadata": {},
   1171      "output_type": "execute_result"
   1172     }
   1173    ],
   1174    "source": [
   1175     "wstar = w_star(delta=2.5, sigma=bl_sigma, mu=bl_mu)\n",
   1176     "# display w*\n",
   1177     "(wstar*100).round(1)"
   1178    ]
   1179   },
   1180   {
   1181    "cell_type": "markdown",
   1182    "metadata": {},
   1183    "source": [
   1184     "The computed $w^*$ exactly replicates column 4 ($w^*$) of Table 5. Finally, as in the previous case, they compute $w^* - \\frac{w_{eq}}{1+\\tau}$ in column 5. We replicate that column as follows:"
   1185    ]
   1186   },
   1187   {
   1188    "cell_type": "code",
   1189    "execution_count": 27,
   1190    "metadata": {},
   1191    "outputs": [
   1192     {
   1193      "data": {
   1194       "text/plain": [
   1195        "AU     0.0\n",
   1196        "CA    39.8\n",
   1197        "FR    -8.4\n",
   1198        "DE    28.4\n",
   1199        "JP    -0.0\n",
   1200        "UK   -20.0\n",
   1201        "US   -39.8\n",
   1202        "dtype: float64"
   1203       ]
   1204      },
   1205      "execution_count": 27,
   1206      "metadata": {},
   1207      "output_type": "execute_result"
   1208     }
   1209    ],
   1210    "source": [
   1211     "w_eq  = w_msr(delta*sigma_prior, pi, scale=False)\n",
   1212     "# Display the difference in Posterior and Prior weights\n",
   1213     "np.round(wstar - w_eq/(1+tau), 3)*100"
   1214    ]
   1215   },
   1216   {
   1217    "cell_type": "markdown",
   1218    "metadata": {},
   1219    "source": [
   1220     "Which exactly reproduces the last column of Table 5 of their paper.\n",
   1221     "\n",
   1222     "Once again, we see the power of the approach. The weights for assets that do not involve the view (AU, JP) remain unchanged. The two underperforming countries (FR, UK, US, according to the view) are underweighted, while the overperforming countries (CA, DE) are overweighted, but not to the extreme extent that a naive portfolio optimizer would have produced.\n"
   1223    ]
   1224   },
   1225   {
   1226    "cell_type": "markdown",
   1227    "metadata": {},
   1228    "source": [
   1229     "### View 3: More Bullish Canada vs US\n",
   1230     "\n",
   1231     "For their third case, He and Litterman alter the second view that Canadian Equities will outperform US Equities by increasing the expected out-performance from the previously stated 3\\% to 4\\%. The results are in Table 6 of their paper, which we shall now reproduce.\n"
   1232    ]
   1233   },
   1234   {
   1235    "cell_type": "code",
   1236    "execution_count": 28,
   1237    "metadata": {},
   1238    "outputs": [
   1239     {
   1240      "data": {
   1241       "text/plain": [
   1242        "0    0.05\n",
   1243        "1    0.04\n",
   1244        "dtype: float64"
   1245       ]
   1246      },
   1247      "execution_count": 28,
   1248      "metadata": {},
   1249      "output_type": "execute_result"
   1250     }
   1251    ],
   1252    "source": [
   1253     "q[1] = .04\n",
   1254     "q"
   1255    ]
   1256   },
   1257   {
   1258    "cell_type": "markdown",
   1259    "metadata": {},
   1260    "source": [
   1261     "Note that P remains unchanged since we have only altered Q, not P"
   1262    ]
   1263   },
   1264   {
   1265    "cell_type": "code",
   1266    "execution_count": 29,
   1267    "metadata": {},
   1268    "outputs": [
   1269     {
   1270      "data": {
   1271       "text/html": [
   1272        "<div>\n",
   1273        "<style scoped>\n",
   1274        "    .dataframe tbody tr th:only-of-type {\n",
   1275        "        vertical-align: middle;\n",
   1276        "    }\n",
   1277        "\n",
   1278        "    .dataframe tbody tr th {\n",
   1279        "        vertical-align: top;\n",
   1280        "    }\n",
   1281        "\n",
   1282        "    .dataframe thead th {\n",
   1283        "        text-align: right;\n",
   1284        "    }\n",
   1285        "</style>\n",
   1286        "<table border=\"1\" class=\"dataframe\">\n",
   1287        "  <thead>\n",
   1288        "    <tr style=\"text-align: right;\">\n",
   1289        "      <th></th>\n",
   1290        "      <th>0</th>\n",
   1291        "      <th>1</th>\n",
   1292        "    </tr>\n",
   1293        "  </thead>\n",
   1294        "  <tbody>\n",
   1295        "    <tr>\n",
   1296        "      <th>AU</th>\n",
   1297        "      <td>0.0</td>\n",
   1298        "      <td>0.0</td>\n",
   1299        "    </tr>\n",
   1300        "    <tr>\n",
   1301        "      <th>CA</th>\n",
   1302        "      <td>0.0</td>\n",
   1303        "      <td>100.0</td>\n",
   1304        "    </tr>\n",
   1305        "    <tr>\n",
   1306        "      <th>FR</th>\n",
   1307        "      <td>-29.5</td>\n",
   1308        "      <td>0.0</td>\n",
   1309        "    </tr>\n",
   1310        "    <tr>\n",
   1311        "      <th>DE</th>\n",
   1312        "      <td>100.0</td>\n",
   1313        "      <td>0.0</td>\n",
   1314        "    </tr>\n",
   1315        "    <tr>\n",
   1316        "      <th>JP</th>\n",
   1317        "      <td>0.0</td>\n",
   1318        "      <td>0.0</td>\n",
   1319        "    </tr>\n",
   1320        "    <tr>\n",
   1321        "      <th>UK</th>\n",
   1322        "      <td>-70.5</td>\n",
   1323        "      <td>0.0</td>\n",
   1324        "    </tr>\n",
   1325        "    <tr>\n",
   1326        "      <th>US</th>\n",
   1327        "      <td>0.0</td>\n",
   1328        "      <td>-100.0</td>\n",
   1329        "    </tr>\n",
   1330        "  </tbody>\n",
   1331        "</table>\n",
   1332        "</div>"
   1333       ],
   1334       "text/plain": [
   1335        "        0      1\n",
   1336        "AU    0.0    0.0\n",
   1337        "CA    0.0  100.0\n",
   1338        "FR  -29.5    0.0\n",
   1339        "DE  100.0    0.0\n",
   1340        "JP    0.0    0.0\n",
   1341        "UK  -70.5    0.0\n",
   1342        "US    0.0 -100.0"
   1343       ]
   1344      },
   1345      "execution_count": 29,
   1346      "metadata": {},
   1347      "output_type": "execute_result"
   1348     }
   1349    ],
   1350    "source": [
   1351     "np.round(p.T*100, 1)"
   1352    ]
   1353   },
   1354   {
   1355    "cell_type": "markdown",
   1356    "metadata": {},
   1357    "source": [
   1358     "This matches columns 1 and 2 of Table 6. We now compute the Black Litterman weights as before:"
   1359    ]
   1360   },
   1361   {
   1362    "cell_type": "code",
   1363    "execution_count": 30,
   1364    "metadata": {},
   1365    "outputs": [
   1366     {
   1367      "data": {
   1368       "text/plain": [
   1369        "AU     4.4\n",
   1370        "CA     9.1\n",
   1371        "FR     9.5\n",
   1372        "DE    11.3\n",
   1373        "JP     4.6\n",
   1374        "UK     7.0\n",
   1375        "US     7.3\n",
   1376        "dtype: float64"
   1377       ]
   1378      },
   1379      "execution_count": 30,
   1380      "metadata": {},
   1381      "output_type": "execute_result"
   1382     }
   1383    ],
   1384    "source": [
   1385     "bl_mu, bl_sigma = bl(w_eq, sigma_prior, p, q, tau = tau)\n",
   1386     "np.round(bl_mu, 3)*100"
   1387    ]
   1388   },
   1389   {
   1390    "cell_type": "markdown",
   1391    "metadata": {},
   1392    "source": [
   1393     "The  Black Litterman expected returns computed by my code exactly reproduces column 3 of Table 6.\n",
   1394     "\n",
   1395     "He-Litterman compute the optimal portfolio $w^*$ as follows (this is Equation (13) on page 6 of their paper)\n"
   1396    ]
   1397   },
   1398   {
   1399    "cell_type": "code",
   1400    "execution_count": 31,
   1401    "metadata": {},
   1402    "outputs": [
   1403     {
   1404      "data": {
   1405       "text/plain": [
   1406        "AU     1.5\n",
   1407        "CA    53.3\n",
   1408        "FR    -3.3\n",
   1409        "DE    33.1\n",
   1410        "JP    11.0\n",
   1411        "UK    -7.8\n",
   1412        "US     7.3\n",
   1413        "dtype: float64"
   1414       ]
   1415      },
   1416      "execution_count": 31,
   1417      "metadata": {},
   1418      "output_type": "execute_result"
   1419     }
   1420    ],
   1421    "source": [
   1422     "wstar = w_star(delta=2.5, sigma=bl_sigma, mu=bl_mu)\n",
   1423     "# display w*\n",
   1424     "(wstar*100).round(1)"
   1425    ]
   1426   },
   1427   {
   1428    "cell_type": "markdown",
   1429    "metadata": {},
   1430    "source": [
   1431     "The computed $w^*$ exactly replicates column 4 ($w^*$) of Table 7. Finally, as in the previous case, they compute $w^* - \\frac{w_{eq}}{1+\\tau}$ in column 6. We replicate that column as follows:"
   1432    ]
   1433   },
   1434   {
   1435    "cell_type": "code",
   1436    "execution_count": 32,
   1437    "metadata": {},
   1438    "outputs": [
   1439     {
   1440      "data": {
   1441       "text/plain": [
   1442        "AU     0.0\n",
   1443        "CA    51.3\n",
   1444        "FR    -8.2\n",
   1445        "DE    27.8\n",
   1446        "JP    -0.0\n",
   1447        "UK   -19.6\n",
   1448        "US   -51.3\n",
   1449        "dtype: float64"
   1450       ]
   1451      },
   1452      "execution_count": 32,
   1453      "metadata": {},
   1454      "output_type": "execute_result"
   1455     }
   1456    ],
   1457    "source": [
   1458     "w_eq  = w_msr(delta*sigma_prior, pi, scale=False)\n",
   1459     "# Display the difference in Posterior and Prior weights\n",
   1460     "np.round(wstar - w_eq/(1+tau), 3)*100"
   1461    ]
   1462   },
   1463   {
   1464    "cell_type": "markdown",
   1465    "metadata": {},
   1466    "source": [
   1467     "Which exactly reproduces the last column of Table 6 of their paper. Again, we see how the weights increase allocations consistent with the view, but keep allocations from getting extreme."
   1468    ]
   1469   },
   1470   {
   1471    "cell_type": "markdown",
   1472    "metadata": {},
   1473    "source": [
   1474     "### View 4: Increasing View Uncertainty\n",
   1475     "\n",
   1476     "As a final step, He and Litterman demonstrate the effect of $\\Omega$. They increase the uncertainty associated with the first of the two views (i.e. the one that Germany will outperform the rest of Europe). First we compute the default value of $\\Omega$ and then increase the uncertainty associated with the first view alone."
   1477    ]
   1478   },
   1479   {
   1480    "cell_type": "code",
   1481    "execution_count": 33,
   1482    "metadata": {},
   1483    "outputs": [
   1484     {
   1485      "data": {
   1486       "text/html": [
   1487        "<div>\n",
   1488        "<style scoped>\n",
   1489        "    .dataframe tbody tr th:only-of-type {\n",
   1490        "        vertical-align: middle;\n",
   1491        "    }\n",
   1492        "\n",
   1493        "    .dataframe tbody tr th {\n",
   1494        "        vertical-align: top;\n",
   1495        "    }\n",
   1496        "\n",
   1497        "    .dataframe thead th {\n",
   1498        "        text-align: right;\n",
   1499        "    }\n",
   1500        "</style>\n",
   1501        "<table border=\"1\" class=\"dataframe\">\n",
   1502        "  <thead>\n",
   1503        "    <tr style=\"text-align: right;\">\n",
   1504        "      <th></th>\n",
   1505        "      <th>0</th>\n",
   1506        "      <th>1</th>\n",
   1507        "    </tr>\n",
   1508        "  </thead>\n",
   1509        "  <tbody>\n",
   1510        "    <tr>\n",
   1511        "      <th>AU</th>\n",
   1512        "      <td>0.0</td>\n",
   1513        "      <td>0.0</td>\n",
   1514        "    </tr>\n",
   1515        "    <tr>\n",
   1516        "      <th>CA</th>\n",
   1517        "      <td>0.0</td>\n",
   1518        "      <td>100.0</td>\n",
   1519        "    </tr>\n",
   1520        "    <tr>\n",
   1521        "      <th>FR</th>\n",
   1522        "      <td>-29.5</td>\n",
   1523        "      <td>0.0</td>\n",
   1524        "    </tr>\n",
   1525        "    <tr>\n",
   1526        "      <th>DE</th>\n",
   1527        "      <td>100.0</td>\n",
   1528        "      <td>0.0</td>\n",
   1529        "    </tr>\n",
   1530        "    <tr>\n",
   1531        "      <th>JP</th>\n",
   1532        "      <td>0.0</td>\n",
   1533        "      <td>0.0</td>\n",
   1534        "    </tr>\n",
   1535        "    <tr>\n",
   1536        "      <th>UK</th>\n",
   1537        "      <td>-70.5</td>\n",
   1538        "      <td>0.0</td>\n",
   1539        "    </tr>\n",
   1540        "    <tr>\n",
   1541        "      <th>US</th>\n",
   1542        "      <td>0.0</td>\n",
   1543        "      <td>-100.0</td>\n",
   1544        "    </tr>\n",
   1545        "  </tbody>\n",
   1546        "</table>\n",
   1547        "</div>"
   1548       ],
   1549       "text/plain": [
   1550        "        0      1\n",
   1551        "AU    0.0    0.0\n",
   1552        "CA    0.0  100.0\n",
   1553        "FR  -29.5    0.0\n",
   1554        "DE  100.0    0.0\n",
   1555        "JP    0.0    0.0\n",
   1556        "UK  -70.5    0.0\n",
   1557        "US    0.0 -100.0"
   1558       ]
   1559      },
   1560      "execution_count": 33,
   1561      "metadata": {},
   1562      "output_type": "execute_result"
   1563     }
   1564    ],
   1565    "source": [
   1566     "# This is the default \"Proportional to Prior\" assumption\n",
   1567     "omega = proportional_prior(sigma_prior, tau, p)\n",
   1568     "# Now, double the uncertainty associated with View 1\n",
   1569     "omega.iloc[0,0] = 2*omega.iloc[0,0]\n",
   1570     "np.round(p.T*100, 1)"
   1571    ]
   1572   },
   1573   {
   1574    "cell_type": "markdown",
   1575    "metadata": {},
   1576    "source": [
   1577     "This matches columns 1 and 2 of Table 7 (which is if course, unchanged, since we have only altered $\\Omega$, not Q or P). We now compute the Black Litterman weights as before, but supplying the value of $\\Omega$ we just adjusted:"
   1578    ]
   1579   },
   1580   {
   1581    "cell_type": "code",
   1582    "execution_count": 34,
   1583    "metadata": {},
   1584    "outputs": [
   1585     {
   1586      "data": {
   1587       "text/plain": [
   1588        "AU     4.3\n",
   1589        "CA     8.9\n",
   1590        "FR     9.3\n",
   1591        "DE    10.6\n",
   1592        "JP     4.6\n",
   1593        "UK     6.9\n",
   1594        "US     7.2\n",
   1595        "dtype: float64"
   1596       ]
   1597      },
   1598      "execution_count": 34,
   1599      "metadata": {},
   1600      "output_type": "execute_result"
   1601     }
   1602    ],
   1603    "source": [
   1604     "bl_mu, bl_sigma = bl(w_eq, sigma_prior, p, q, tau = tau, omega=omega)\n",
   1605     "np.round(bl_mu, 3)*100"
   1606    ]
   1607   },
   1608   {
   1609    "cell_type": "markdown",
   1610    "metadata": {},
   1611    "source": [
   1612     "The  Black Litterman expected returns computed by the code exactly reproduces column 3 of Table 7.\n",
   1613     "\n",
   1614     "He-Litterman compute the optimal portfolio $w^*$ as follows (this is Equation (13) on page 6 of their paper)"
   1615    ]
   1616   },
   1617   {
   1618    "cell_type": "code",
   1619    "execution_count": 35,
   1620    "metadata": {},
   1621    "outputs": [
   1622     {
   1623      "data": {
   1624       "text/plain": [
   1625        "AU     1.5\n",
   1626        "CA    53.9\n",
   1627        "FR    -0.5\n",
   1628        "DE    23.6\n",
   1629        "JP    11.0\n",
   1630        "UK    -1.1\n",
   1631        "US     6.8\n",
   1632        "dtype: float64"
   1633       ]
   1634      },
   1635      "execution_count": 35,
   1636      "metadata": {},
   1637      "output_type": "execute_result"
   1638     }
   1639    ],
   1640    "source": [
   1641     "wstar = w_star(delta=2.5, sigma=bl_sigma, mu=bl_mu)\n",
   1642     "# display w*\n",
   1643     "(wstar*100).round(1)"
   1644    ]
   1645   },
   1646   {
   1647    "cell_type": "markdown",
   1648    "metadata": {},
   1649    "source": [
   1650     "The computed $w^*$ exactly replicates column 4 ($w^*$) of Table 7. Finally, as in the previous case, they compute $w^* - \\frac{w_{eq}}{1+\\tau}$ in column 6. We replicate that column as follows:\n"
   1651    ]
   1652   },
   1653   {
   1654    "cell_type": "code",
   1655    "execution_count": 36,
   1656    "metadata": {},
   1657    "outputs": [
   1658     {
   1659      "data": {
   1660       "text/plain": [
   1661        "AU    -0.0\n",
   1662        "CA    51.8\n",
   1663        "FR    -5.4\n",
   1664        "DE    18.4\n",
   1665        "JP     0.0\n",
   1666        "UK   -13.0\n",
   1667        "US   -51.8\n",
   1668        "dtype: float64"
   1669       ]
   1670      },
   1671      "execution_count": 36,
   1672      "metadata": {},
   1673      "output_type": "execute_result"
   1674     }
   1675    ],
   1676    "source": [
   1677     "w_eq  = w_msr(delta*sigma_prior, pi, scale=False)\n",
   1678     "# Display the difference in Posterior and Prior weights\n",
   1679     "np.round(wstar - w_eq/(1+tau), 3)*100"
   1680    ]
   1681   },
   1682   {
   1683    "cell_type": "markdown",
   1684    "metadata": {},
   1685    "source": [
   1686     "Which exactly reproduces the last column of Table 7 of their paper. Again, we see how the weights increase allocations consistent with the view, but keep allocations from getting extreme.\n",
   1687     "\n",
   1688     "That concludes our reproduction of the paper. Note that He and Litterman also produce an extra table (Table 8) which demonstrates the value of adding a third view. However, the third view is identical to the values implied by the equilibrium and as a result, they produce exactly the same results as Table 7. I do not bother reproduce it here since the results are exactly the same as Table 7.\n",
   1689     "\n",
   1690     "## Try it on Industry Data ...\n",
   1691     "\n",
   1692     "Now that you've reproduced the results, you should be able to run the code on the Fama-French Industry Portfolios ...\n",
   1693     "\n",
   1694     "Start out by loading the data as follows, and then play around!"
   1695    ]
   1696   },
   1697   {
   1698    "cell_type": "code",
   1699    "execution_count": 37,
   1700    "metadata": {},
   1701    "outputs": [],
   1702    "source": [
   1703     "import edhec_risk_kit_206 as erk\n",
   1704     "\n",
   1705     "ind49_rets = erk.get_ind_returns(weighting=\"vw\", n_inds=49)[\"2014\":]\n",
   1706     "ind49_mcap = erk.get_ind_market_caps(49, weights=True)[\"2014\":]"
   1707    ]
   1708   }
   1709  ],
   1710  "metadata": {
   1711   "kernelspec": {
   1712    "display_name": "Python 3",
   1713    "language": "python",
   1714    "name": "python3"
   1715   },
   1716   "language_info": {
   1717    "codemirror_mode": {
   1718     "name": "ipython",
   1719     "version": 3
   1720    },
   1721    "file_extension": ".py",
   1722    "mimetype": "text/x-python",
   1723    "name": "python",
   1724    "nbconvert_exporter": "python",
   1725    "pygments_lexer": "ipython3",
   1726    "version": "3.8.8"
   1727   }
   1728  },
   1729  "nbformat": 4,
   1730  "nbformat_minor": 2
   1731 }