ポートフォリオ最適化問題に対する定式化

準備

import pandas as pd
import random
import math
from gurobipy import Model, quicksum, GRB, multidict
# from mypulp import Model, quicksum, GRB, multidict
from scipy.stats import norm
import numpy as np
import yfinance as yf
import warnings
import riskfolio as rp
import riskfolio.PlotFunctions as plf

Markowitzモデル

Markowitzモデルは,ポートフォリオ最適化問題の古典であり,以下のように定義される.

資産 $M$ 円を持つとき, $1$ 年後の期待資産価値が $\alpha M$ 円以上(ただし $\alpha >1$)という制約のもとで, 「リスク」を最小化することを考える. ただし,株を $i=1,2,\ldots,n $ で表し, 株 $i$ の $1$ 年後の価値は期待値 $r_i$, 分散 $\sigma^2_i$ の確率分布に したがうと仮定する.

株を $1, 2, \ldots, n$ とし, 各株 $i$ の $1$ 年後の価格を,確率変数 $R_i$ で表す. ただし,$R_i$ の期待値は $r_i$, 分散は $\sigma_i^2$ とする. 期待値をとる操作を $\mathbf{E}[\cdot]$ で書けば,次の関係が成り立っている. $$ \mathbf{E}[R_i] = r_i, \ \mathbf{E}[(R_i- r_i)^2] = \sigma_i^2 \ \ \forall i=1,2,\ldots,n $$

株 $i$ に投資する割合を $x_i$ とする.すると $x_1, x_2, \ldots, x_n$ は 非負で和が $1$ になっているはずである. $$ \sum_{i=1}^n x_i =1, \quad x_i\geq 0, \ \forall i=1,2,\ldots, n $$ この投資比率で投資したとき, $1$ 年後の財産価格は確率変数 $R_i$ を用いて $M \sum_{i=1}^n R_i x_i$ と書けるので,期待値が $ \alpha M$以上という制約は次のようになる. $$ M \sum_{i=1}^n r_i x_i \geq \alpha M $$

$M$ は両辺に現れるので消去でき,結局以下のようになる. $$ \sum_{i=1}^n r_i x_i \geq \alpha $$

最後に「リスク」とは何かを考えなければならない. Markowitz は「リスク」とは期待値からのずれと解釈し, 確率でいう「分散」をリスクと定義した.

$$ \begin{array}{ll} minimize &\displaystyle\sum_{i=1}^n \sigma_i^2 x_i^2 \\ s.t. & \displaystyle\sum_{i=1}^n r_i x_i \geq \alpha \\ & \displaystyle\sum_{i=1}^n x_i = 1 \\ & x_i \geq 0 \ \ \ \forall i=1, 2, \ldots, n \end{array} $$

以下に定式化のコードと求解例を示す.

def markowitz(I, sigma, r, alpha):
    """markowitz -- simple markowitz model for portfolio optimization.
    Parameters:
        - I: set of items
        - sigma[i]: standard deviation of item i
        - r[i]: revenue of item i
        - alpha: acceptance threshold
    Returns a model, ready to be solved.
    """

    model = Model("markowitz")
    x = {}
    for i in I:
        x[i] = model.addVar(vtype="C", name="x(%s)" % i)  # quantity of i to buy
    model.update()

    model.addConstr(quicksum(r[i] * x[i] for i in I) >= alpha)
    model.addConstr(quicksum(x[i] for i in I) == 1)

    model.setObjective(quicksum(sigma[i] ** 2 * x[i] * x[i] for i in I), GRB.MINIMIZE)

    model.update()
    model.__data = x
    return model
I, sigma, r = multidict(
    {1: [0.07, 1.01], 2: [0.09, 1.05], 3: [0.1, 1.08], 4: [0.2, 1.10], 5: [0.3, 1.20]}
)
alpha = 1.05

model = markowitz(I, sigma, r, alpha)
model.optimize()

x = model.__data
EPS = 1.0e-6
print("%5s\t%8s" % ("i", "x[i]"))
for i in I:
    print("%5s\t%8g" % (i, x[i].X))
print("sum:", sum(x[i].X for i in I))
print("Obj:", model.ObjVal)
Academic license - for non-commercial use only - expires 2023-06-24
Using license file /Users/mikiokubo/gurobi.lic
Gurobi Optimizer version 9.1.1 build v9.1.1rc0 (mac64)
Thread count: 8 physical cores, 16 logical processors, using up to 16 threads
Optimize a model with 2 rows, 5 columns and 10 nonzeros
Model fingerprint: 0x16b2f4af
Model has 5 quadratic objective terms
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  Objective range  [0e+00, 0e+00]
  QObjective range [1e-02, 2e-01]
  Bounds range     [0e+00, 0e+00]
  RHS range        [1e+00, 1e+00]
Presolve time: 0.01s
Presolved: 2 rows, 5 columns, 10 nonzeros
Presolved model has 5 quadratic objective terms
Ordering time: 0.00s

Barrier statistics:
 AA' NZ     : 1.000e+00
 Factor NZ  : 3.000e+00
 Factor Ops : 5.000e+00 (less than 1 second per iteration)
 Threads    : 1

                  Objective                Residual
Iter       Primal          Dual         Primal    Dual     Compl     Time
   0   1.12329054e+06 -1.12329054e+06  2.50e+03 0.00e+00  1.00e+06     0s
   1   6.14351792e+04 -6.16258522e+04  3.06e+02 0.00e+00  1.29e+05     0s
   2   5.76881996e+01 -3.29740683e+02  4.37e+00 0.00e+00  1.94e+03     0s
   3   4.59130385e-02 -2.60672741e+02  4.37e-06 0.00e+00  4.35e+01     0s
   4   4.58729015e-02 -3.07134775e-01  1.55e-09 0.00e+00  5.88e-02     0s
   5   2.03720363e-02 -2.25446605e-02  6.89e-11 0.00e+00  7.15e-03     0s
   6   5.90436479e-03 -1.22651799e-02  5.55e-17 8.67e-18  3.03e-03     0s
   7   2.89216605e-03  8.20752077e-05  0.00e+00 0.00e+00  4.68e-04     0s
   8   2.23962706e-03  1.88182071e-03  2.78e-17 3.47e-18  5.96e-05     0s
   9   2.19020764e-03  2.15142085e-03  0.00e+00 1.04e-17  6.46e-06     0s
  10   2.18951463e-03  2.18928902e-03  1.39e-17 5.20e-18  3.76e-08     0s
  11   2.18949484e-03  2.18949461e-03  0.00e+00 1.04e-17  3.76e-11     0s

Barrier solved model in 11 iterations and 0.02 seconds
Optimal objective 2.18949484e-03

    i	    x[i]
    1	0.391756
    2	0.270308
    3	0.239191
    4	0.0631714
    5	0.0355731
sum: 1.0
Obj: 0.0021894948394944346

損をする確率を抑えるモデル

Markowitzのモデルは,リスクを分散で表現し, これを最小化する問題であった. しかし分散は,プラスの方向にもマイナスの方向にも等しく 作用するので,たくさん損することを避けるために, たくさん得することも避けるモデルとなっている. これは少し納得しづらい状況である.

そこで少し考え方を変える.もっと直接的に確率を扱い, 例えば $1$ 年後の資産価値が $0.95 M$ 円以下になる確率を $5\%$ 以下にしたいと考えてみよう. つまり,$95\%$ 以上の確率で 9割5分の資産が残る,という条件のもと, 期待資産価値を最大化する問題である.

上の問題を一般的に表すと次のようになる.

$1$ 年後の資産価値が $\alpha M$円以下になる「確率」を $\beta \%$ 以下に抑えながら, $1$ 年後の期待資産価値を最大化せよ.

先と同じように株式を $i=1,2,\ldots,n $ で表し, 株式 $i$ の一年後の価値が期待値 $r_i$, 分散 $\sigma_i$ の正規分布をすると仮定する. この確率変数を $R_i$ と書く. 各株式 $i$ に投資する割合を $x_i$ とすれば,やはり次を満たしている. $$ \sum_{i=1}^n x_i =1, \quad x_i\geq 0\ \ \ \forall i=1, 2, \ldots, n $$ 今回の場合,期待利益を最大化したいので,目的関数は $ \sum_{i=1}^n r_i x_i $ である.

難しいのは,「資産価値が $\alpha M$ 円以下になる確率を $\beta$ 以下に押さえる」という条件である.

確率変数 $R_i$ は平均 $r_i$, 分散 $\sigma_i^2$ の正規分布にしたがうと仮定したので, $R_i x_i$ は平均 $r_i x_i$, 分散 $\sigma_i^2 x_i^2$ の正規分布にしたがい, さらにその和 $ \sum_{i=1}^n R_i x_i $ は 平均 $\mu = \sum_{i=1}^n r_i x_i$, 分散 $\sigma^2 = \sum_{i=1}^n r_i^2 x_i^2$ の正規分布にしたがう. よって,新たな確率変数を $$ R = \frac{\sum_{i=1}^n R_i x_i -\mu}{\sigma} $$ で定義すれば $R$ は平均 $0$, 分散 $1$ の正規分布にしたがうことになる.

よって, $\Phi$ を正規分布の分布関数としたとき, $$ \begin{array}{l c l} Prob\left{ R \leq \frac{\alpha-\mu}{\sigma} \right} = \Phi\left(R \leq \frac{\alpha-\mu}{\sigma}\right) \leq \beta & \Leftrightarrow &  \frac{\alpha - \mu}{\sigma} \leq \Phi^{-1}(\beta) \ & \Leftrightarrow &

  • \Phi^{-1}(\beta)\sqrt{\sum_{i=1}^n\sigma_i^2 xi^2} \leq -\alpha + \sum{i=1}^n r_i x_i \end{array} $$ となる. $\beta<1/2$ のとき,$\Phi^{-1}(\beta)<0$ であり, 上の制約は2次錐制約となる.

結局,以下の最適化問題に定式化されることになる. $$ \begin{array}{ll} maximize & \rho \\ s.t. & \displaystyle\sum_{i=1}^n r_i x_i = \rho \\ & \displaystyle\sum_{i=1}^n x_i = 1 \\ & x_i \ge 0 \ \ \forall i=1,\ldots,n \\ & \sqrt{\displaystyle\sum_{i=1}^n \bar\sigma_i^2 x_i^2} \leq \displaystyle\frac{\alpha - \rho}{\Phi^{-1}(\beta)} \end{array} $$

これは2次錐最適化問題なので,Gurobiで解くことができる.

def p_portfolio(I, sigma, r, alpha, beta):
    """p_portfolio -- modified markowitz model for portfolio optimization.
    Parameters:
        - I: set of items
        - sigma[i]: standard deviation of item i
        - r[i]: revenue of item i
        - alpha: acceptance threshold
        - beta: desired confidence level
    Returns a model, ready to be solved.
    """

    model = Model("p_portfolio")
    x = {}
    for i in I:
        x[i] = model.addVar(vtype="C", name="x(%s)" % i)  # quantity of i to buy
    rho = model.addVar(vtype="C", name="rho")
    rhoaux = model.addVar(vtype="C", name="rhoaux")
    model.update()

    model.addConstr(rho == quicksum(r[i] * x[i] for i in I))
    model.addConstr(quicksum(x[i] for i in I) == 1)

    model.addConstr(rhoaux == (alpha - rho) / norm.ppf(beta))
    model.addConstr(quicksum(sigma[i] ** 2 * x[i] * x[i] for i in I) <= rhoaux * rhoaux)

    model.setObjective(rho, GRB.MAXIMIZE)
    model.update()
    model.__data = x
    return model
I, sigma, r = multidict(
    {1: [0.07, 1.01], 2: [0.09, 1.05], 3: [0.1, 1.08], 4: [0.2, 1.10], 5: [0.3, 1.20]}
)
alpha = 0.95
beta = 0.1

print("\n\n\nbeta:", beta, "phi inv:", phi_inv(beta))
model = p_portfolio(I, sigma, r, alpha, beta)
model.optimize()

x = model.__data
EPS = 1.0e-6
print("Investment:")
print("%5s\t%8s" % ("i", "x[i]"))
for i in I:
    print("%5s\t%8g" % (i, x[i].X))
print("Obj:", model.ObjVal)


beta: 0.1 phi inv: -1.281728756502709
Gurobi Optimizer version 9.1.1 build v9.1.1rc0 (mac64)
Thread count: 8 physical cores, 16 logical processors, using up to 16 threads
Optimize a model with 3 rows, 7 columns and 13 nonzeros
Model fingerprint: 0xa2bd34d7
Model has 1 quadratic constraint
Coefficient statistics:
  Matrix range     [8e-01, 1e+00]
  QMatrix range    [5e-03, 1e+00]
  Objective range  [1e+00, 1e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [7e-01, 1e+00]
Presolve removed 1 rows and 1 columns
Presolve time: 0.01s
Presolved: 7 rows, 6 columns, 16 nonzeros
Presolved model has 1 second-order cone constraint
Ordering time: 0.00s

Barrier statistics:
 Dense cols : 1
 AA' NZ     : 1.800e+01
 Factor NZ  : 3.600e+01
 Factor Ops : 2.040e+02 (less than 1 second per iteration)
 Threads    : 1

                  Objective                Residual
Iter       Primal          Dual         Primal    Dual     Compl     Time
   0   1.09384740e+00  1.02759245e+00  6.16e-03 1.05e+00  4.17e-02     0s
   1   1.14345898e+00  1.09197135e+00  1.60e-02 3.10e-01  7.44e-03     0s
   2   1.11975956e+00  1.16129009e+00  3.46e-04 2.33e-03  3.95e-03     0s
   3   1.13919013e+00  1.14000962e+00  1.71e-11 8.40e-05  7.64e-05     0s
   4   1.13932822e+00  1.13934967e+00  2.21e-14 9.27e-07  1.97e-06     0s
   5   1.13934093e+00  1.13934125e+00  2.86e-13 7.15e-09  2.89e-08     0s

Barrier solved model in 5 iterations and 0.02 seconds
Optimal objective 1.13934093e+00

Investment:
    i	    x[i]
    1	2.77054e-08
    2	3.26614e-06
    3	0.309915
    4	0.234687
    5	0.455394
Obj: 1.1393409341049061

様々なモデルを解くためのパッケージ

以下のパッケージを使うと,様々なポートフォリオ最適化問題を解くことができる.

https://github.com/dcajasn/Riskfolio-Lib

  • データはYahoo Financeデータを yfinance パッケージを利用して入手している.ただし,現在はデータスクレイピングが禁止されたため,使えなくなっている. 使用する際には,自分でデータを準備する必要がある.

  • 最適化はオープンソースの凸最適化パッケージ cvxopt を利用している.

最適化と主な引数

最適化は以下の形式で行う.

import riskfolio.Portfolio as pf
portofolio = pf.Portfolio()
portofolio.optimization()

主な引数は以下の通り.

model: 平均と共分散のためのモデル

  • 'Classic': 過去の履歴(既定値)
  • 'BL': Black Littermanモデル
  • 'FM': リスクファクターモデル

rm: リスク尺度

  • 'MV': 標準偏差 (既定値)

  • 平均絶対偏差 (Mean Absolute Deviation: MAD)

$$ \text{MAD}(X) = \frac{1}{T}\sum_{t=1}^{T}| X_{t} - \mathbb{E}(X_{t}) | $$
  • 'MSV': セミ標準偏差
$$ \text{SemiDev}(X) = \left [ \frac{1}{T-1}\sum_{t=1}^{T} (X_{t} - \mathbb{E}(X_{t}))^2 \right ]^{1/2} $$
  • 'FLPM': First Lower Partial Moment (Omega Ratio)
$$ \text{LPM}(X, \text{MAR}, 1) = \frac{1}{T}\sum_{t=1}^{T} \max(\text{MAR} - X_{t}, 0) $$
  • 'SLPM': Second Lower Partial Moment (Sortino Ratio)
$$ \text{LPM}(X, \text{MAR}, 2) = \left [ \frac{1}{T-1}\sum_{t=1}^{T} \max(\text{MAR} - X_{t}, 0)^{2} \right ]^{\frac{1}{2}} $$
  • 'CVaR': 条件付きVaR
$$ \text{CVaR}_{\alpha}(X) = \text{VaR}_{\alpha}(X) + \frac{1}{\alpha T} \sum_{t=1}^{T} \max(-X_{t} - \text{VaR}_{\alpha}(X), 0) $$
  • 'EVaR': Entropic Value at Risk
$$ \text{EVaR}_{\alpha}(X) = \inf_{z>0} \left \{ z \ln \left (\frac{M_X(z^{-1})}{\alpha} \right ) \right \} $$
  • 'WR': Worst Realization (Minimax)
$$ \text{WR}(X) = \max(-X) $$
  • 'MDD': Maximum Drawdown of uncompounded cumulative returns (Calmar Ratio)
$$ \text{MDD}(X) = \max_{j \in (0,T)} \left [\max_{t \in (0,j)} \left ( \sum_{i=0}^{t}X_{i} \right ) - \sum_{i=0}^{j}X_{i} \right ] $$
  • 'ADD': Average Drawdown of uncompounded cumulative returns
$$ \text{ADD}(X) = \frac{1}{T}\sum_{j=0}^{T}\left [ \max_{t \in (0,j)} \left ( \sum_{i=0}^{t}X_{i} \right ) - \sum_{i=0}^{j}X_{i} \right ] $$
  • 'CDaR': Conditional Drawdown at Risk of uncompounded cumulative returns
$$ \text{CDaR}_{\alpha}(X) = \text{DaR}_{\alpha}(X) + \frac{1}{\alpha T} \sum_{j=0}^{T} \max \left [ \max_{t \in (0,j)} \left ( \sum_{i=0}^{t}X_{i} \right ) - \sum_{i=0}^{j}X_{i} - \text{DaR}_{\alpha}(X), 0 \right ] $$

ただし, $$ \text{DaR}_{\alpha}(X) = \max_{j \in (0,T)} \left \{ \text{DD}(X,j) \in \mathbb{R}: F_{\text{DD}} \left ( \text{DD}(X,j) \right )< 1-\alpha \right \} $$

$$ \text{DD}(X,j) = \max_{t \in (0,j)} \left ( \sum_{i=0}^{t}X_{i} \right )- \sum_{i=0}^{j}X_{i} $$
  • 'EDaR': Entropic Drawdown at Risk of uncompounded cumulative returns
$$ \text{EDaR}_{\alpha}(X) = \inf_{z>0} \left \{ z \ln \left (\frac{M_{\text{DD}(X)}(z^{-1})}{\alpha} \right ) \right \} $$$$ \text{DD}(X,j) = \max_{t \in (0,j)} \left ( \sum_{i=0}^{t}X_{i} \right )- \sum_{i=0}^{j}X_{i} $$
  • 'UCI': Ulcer Index of uncompounded cumulative returns
$$ \text{UCI}(X) =\sqrt{\frac{1}{T}\sum_{j=0}^{T} \left [ \max_{t \in (0,j)} \left ( \sum_{i=0}^{t}X_{i} \right ) - \sum_{i=0}^{j}X_{i} \right ] ^2} $$

obj: 目的関数

  • 'MinRisk': リスク最小化
  • 'Utility': 効用関数 $\mu w - l \phi_{i}(w)$ 最大化 ($\phi_{i}$ は上の13のリスク尺度から選択, $l$ はリスク回避率)
  • 'Sharpe': リスク調整済みリターン(既定値); (リターン - 無リスク金利)/$\phi_{i}$ で定義される.
  • 'MaxRet': 期待リターン最大化

まず,データを読み込む.

warnings.filterwarnings("ignore")
pd.options.display.float_format = '{:.4%}'.format

# Date range
start = '2016-01-01'
end = '2019-12-30'

# Tickers of assets
assets = ['JCI', 'TGT', 'CMCSA', 'CPB', 'MO', 'APA', 'MMC', 'JPM',
          'ZION', 'PSA', 'BAX', 'BMY', 'LUV', 'PCAR', 'TXT', 'TMO',
          'DE', 'MSFT', 'HPQ', 'SEE', 'VZ', 'CNP', 'NI', 'T', 'BA']
assets.sort()

# Downloading data
data = yf.download(assets, start = start, end = end)
data = data.loc[:,('Adj Close', slice(None))]
data.columns = assets

Y = data[assets].pct_change().dropna()
Y.head()
[*********************100%***********************]  25 of 25 completed
APA BA BAX BMY CMCSA CNP CPB DE HPQ JCI ... NI PCAR PSA SEE T TGT TMO TXT VZ ZION
Date
2016-01-04 -0.0900% -2.8287% -2.5688% -2.5585% -0.9612% -0.4902% -1.6936% -0.2491% -2.0270% -0.3136% ... 0.0512% -0.5063% -1.3444% -3.4978% -0.1744% 1.2946% -2.1854% -1.2139% -0.7573% -2.1612%
2016-01-05 -2.0256% 0.4057% 0.4035% 1.9693% 0.0180% 0.9305% 0.3678% 0.5783% 0.9482% -1.1953% ... 1.5881% 0.0212% 2.8236% 0.9758% 0.6987% 1.7539% -0.1730% 0.2409% 1.3734% -1.0857%
2016-01-06 -11.4863% -1.5879% 0.2412% -1.7556% -0.7727% -1.2473% -0.1736% -1.1239% -3.5867% -0.9551% ... 0.5547% 0.0212% 0.1592% -1.5647% 0.3108% -1.0155% -0.7653% -3.0048% -0.9035% -2.9145%
2016-01-07 -5.1389% -4.1922% -1.6573% -2.7699% -1.1047% -1.9770% -1.2206% -0.8856% -4.6058% -2.5393% ... -2.2066% -3.0310% -1.0411% -3.1557% -1.6148% -0.2700% -2.2845% -2.0570% -0.5492% -3.0020%
2016-01-08 0.2736% -2.2705% -1.6037% -2.5425% 0.1099% -0.2241% 0.5706% -1.6402% -1.7641% -0.1649% ... -0.1539% -1.1366% -0.7308% -0.1448% 0.0896% -3.3839% -0.1117% -1.1387% -0.9719% -1.1254%

5 rows × 25 columns

ポートフォリオを求める.

port = rp.Portfolio(returns=Y)

method_mu='hist' # 過去の履歴から平均値を計算(ewma1 or ewma2にすると,指数平滑法)
method_cov='hist' # 過去の履歴から共分散を計算

port.assets_stats(method_mu=method_mu, method_cov=method_cov, d=0.94) #dは指数平滑法のパラメータ

# 最適ポートフォリオを求める

model='Classic' # Classic (historical), BL (Black Litterman) or FM (Factor Model)
rm = 'MV' # リスク尺度は標準偏差
obj = 'Sharpe' # 目的関数 MinRisk, MaxRet, Utility or Sharpe
hist = True # Use historical scenarios for risk measures that depend on scenarios
rf = 0 # Risk free rate
l = 0 # Risk aversion factor, only useful when obj is 'Utility'

w = port.optimization(model=model, rm=rm, obj=obj, rf=rf, l=l, hist=hist)

display(w.T)
APA BA BAX BMY CMCSA CNP CPB DE HPQ JCI ... NI PCAR PSA SEE T TGT TMO TXT VZ ZION
weights 0.0000% 5.5551% 10.5178% 0.0000% 0.0000% 8.8001% 0.0000% 4.7778% 0.0000% 0.0000% ... 11.2495% 0.0000% 0.0000% 0.0000% 0.0000% 7.9788% 0.0000% 0.0000% 4.1383% 0.0000%

1 rows × 25 columns

結果を描画する.

ax = plf.plot_pie(w=w, title='Sharpe Mean Variance', others=0.05, nrow=25, cmap = "tab20",
                  height=6, width=10, ax=None)
points = 50 # Number of points of the frontier

frontier = port.efficient_frontier(model=model, rm=rm, points=points, rf=rf, hist=hist)

display(frontier.T.head())
APA BA BAX BMY CMCSA CNP CPB DE HPQ JCI ... NI PCAR PSA SEE T TGT TMO TXT VZ ZION
0 0.0000% 0.0000% 5.1206% 4.3334% 2.1718% 7.0667% 3.1868% 0.1850% 0.0000% 2.9728% ... 11.5764% 0.0000% 14.8984% 0.0396% 6.7154% 4.2092% 0.0000% 0.0000% 8.2638% 0.0020%
1 0.0000% 1.7915% 8.1835% 0.6778% 1.7443% 8.6959% 2.0606% 1.6004% 0.0000% 1.3086% ... 13.6967% 0.0000% 9.1544% 0.0000% 5.8687% 5.7768% 0.0000% 0.0000% 8.7102% 0.0000%
2 0.0000% 2.5434% 8.9733% 0.0000% 1.2427% 9.3156% 1.5796% 2.0330% 0.0000% 0.3715% ... 14.5510% 0.0000% 6.3250% 0.0000% 5.4185% 6.3699% 0.0000% 0.0000% 8.9551% 0.0000%
3 0.0000% 3.1112% 9.4778% 0.0000% 0.6498% 9.7657% 0.9240% 2.3124% 0.0000% 0.0000% ... 15.1335% 0.0000% 3.4855% 0.0000% 4.6374% 6.8032% 0.0000% 0.0000% 9.0775% 0.0000%
4 0.0000% 3.5683% 9.8654% 0.0000% 0.0796% 10.1250% 0.2218% 2.5164% 0.0000% 0.0000% ... 15.5460% 0.0000% 0.8877% 0.0000% 3.7856% 7.1433% 0.0000% 0.0000% 9.1219% 0.0000%

5 rows × 25 columns

label = 'Max Risk Adjusted Return Portfolio' # Title of point
mu = port.mu # Expected returns
cov = port.cov # Covariance matrix
returns = port.returns # Returns of the assets

ax = plf.plot_frontier(w_frontier=frontier, mu=mu, cov=cov, returns=returns, rm=rm,
                       rf=rf, alpha=0.05, cmap='viridis', w=w, label=label,
                       marker='*', s=16, c='r', height=6, width=10, ax=None)
ax = plf.plot_frontier_area(w_frontier=frontier, cmap="tab20", height=6, width=10, ax=None)