ポートフォリオのリスクを分析する

「FinTech Advent Calendar 2018」の記事です。
Pythonのライブラリを使ってポートフォリオのリスクを分析してみます。

データの準備

手っ取り早くデータを取りたいので、今回はQuandlを使います。
株価のデータは有料なものがほとんどで世知辛いので、先物のデータを使います。

  • 天然ガス(1限月)
    なんとなく選んだ、特に意味はない
  • E-Mini S&P 500(1限月)
    ベンチマークとして使用
In [1]:
!pip install quandl
Requirement already satisfied: quandl in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (3.5.3)
Requirement already satisfied: python-dateutil in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from quandl) (2.8.1)
Requirement already satisfied: more-itertools in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from quandl) (8.6.0)
Requirement already satisfied: pandas>=0.14 in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from quandl) (0.25.3)
Requirement already satisfied: inflection>=0.3.1 in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from quandl) (0.5.1)
Requirement already satisfied: numpy>=1.8 in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from quandl) (1.19.4)
Requirement already satisfied: requests>=2.7.0 in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from quandl) (2.25.1)
Requirement already satisfied: six in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from quandl) (1.15.0)
Requirement already satisfied: pytz>=2017.2 in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from pandas>=0.14->quandl) (2020.5)
Requirement already satisfied: idna<3,>=2.5 in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from requests>=2.7.0->quandl) (2.10)
Requirement already satisfied: chardet<5,>=3.0.2 in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from requests>=2.7.0->quandl) (4.0.0)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from requests>=2.7.0->quandl) (1.26.2)
Requirement already satisfied: certifi>=2017.4.17 in /home/driller/work/pyfolio_env/lib/python3.8/site-packages (from requests>=2.7.0->quandl) (2020.12.5)
WARNING: You are using pip version 19.2.3, however version 20.3.3 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
In [2]:
import quandl

# 天然ガス(1限月)
ng_df = quandl.get("CHRIS/CME_NG1", start_date="1998-01-01", )
# E-Mini S&P 500(1限月)
es_df = quandl.get("CHRIS/CME_ES1", start_date="1998-01-01")

今後はデイリーのリターンを使うので事前に計算します。

In [3]:
ng_returns = ng_df["Settle"].pct_change()
es_returns = es_df["Settle"].pct_change()

empyrical

さまざまなリスクメトリクスを算出できます。

Alpha

ベンチマークをどれだけ超過できるか

$$ \mathrm{SCL} : R_{i,t} - R_{f} = \alpha_i + \beta_i\, ( R_{M,t} - R_{f} ) + \epsilon_{i,t} \frac{}{} $$
In [4]:
import empyrical

empyrical.alpha(ng_returns, es_returns)
Out[4]:
0.13808521665288276

Beta

ベンチマークに対する相関

$$ \beta = \frac {\mathrm{Cov}(r_a,r_b)}{\mathrm{Var}(r_b)}, $$
In [5]:
empyrical.beta(ng_returns, es_returns)
Out[5]:
0.11896838094042442

AlphaとBetaは算出時に共通する部分があるため、同時に算出する関数が用意されています。

In [6]:
empyrical.alpha_beta(ng_returns, es_returns)
Out[6]:
array([0.13808522, 0.11896838])

maximum drawdown

最大ドローダウン

In [7]:
empyrical.max_drawdown(ng_returns)
Out[7]:
-0.9036285602809208

annual growth rate of returns

年平均成長率

In [8]:
empyrical.annual_return(ng_returns)
Out[8]:
0.006871302123873768

annual volatility of a strategy

年間変動率

In [9]:
empyrical.annual_volatility(ng_returns)
Out[9]:
0.5674082218089809

Calmar ratio

最大損失率に対する年間平均収益の比率

In [10]:
empyrical.calmar_ratio(ng_returns)
Out[10]:
0.0076041223417480435

Omega ratio

負のリターンに対する正のリターンの比率 $$ \Omega(r) = \frac{\int_{r}^\infty (1-F(x))\,dx}{\int_{-\infty}^r F(x)dx} $$

In [11]:
empyrical.omega_ratio(ng_returns)
Out[11]:
1.053598485923448

Sharpe ratio

標準偏差に対するリターンの比率 $$ S_a = \frac{E[R_a-R_b]}{\sigma_a} = \frac{E[R_a-R_b]}{\sqrt{\mathrm{var}[R_a-R_b]}}, $$

In [12]:
empyrical.sharpe_ratio(ng_returns)
Out[12]:
0.289233273578207

Sortino ratio

下方リスクに対するリターンの比率 $$ S = \frac{R-T}{DR} $$

$$ DR = \sqrt{ \int_{-\infty}^T (T-r)^2f(r)\,dr } $$
In [13]:
empyrical.sortino_ratio(ng_returns)
Out[13]:
0.4485040587839132

downside deviation below a threshold

設定したしきい値(デフォルトは0)に対する下方リスク

In [14]:
empyrical.downside_risk(ng_returns)
Out[14]:
0.3659127141323593

R-squared of a linear fit to the cumulative log returns

対数収益を直線に回帰したときの決定係数

In [15]:
empyrical.stability_of_timeseries(ng_returns)
Out[15]:
0.0781384023632593

Tail ratio

5パーセンタイル値と95パーセンタイル値の比率

In [16]:
empyrical.tail_ratio(ng_returns)
Out[16]:
1.1041216303271517

compound annual growth rate

(CAGR)年平均成長率

In [17]:
empyrical.cagr(ng_returns)
Out[17]:
0.006871302123873768

pyfolio

empyricalは数値を算出するだけでしたが、pyfolioを使うとさまざまなリスク分析を可視化を交えてレポーティングしてくれます。

代表的な機能であるティアシートを作成してみます。

In [18]:
%matplotlib inline

import pyfolio

pyfolio.create_returns_tear_sheet(ng_returns, benchmark_rets=es_returns)
/home/driller/work/pyfolio_env/lib/python3.8/site-packages/pyfolio/pos.py:26: UserWarning: Module "zipline.assets" not found; mutltipliers will not be applied to position notionals.
  warnings.warn(
Start date1998-01-02
End date2020-12-24
Total months274
Backtest
Annual return 0.7%
Cumulative returns 17.0%
Annual volatility 56.7%
Sharpe ratio 0.29
Calmar ratio 0.01
Stability 0.08
Max drawdown -90.4%
Omega ratio 1.05
Sortino ratio 0.45
Skew NaN
Kurtosis NaN
Tail ratio 1.10
Daily value at risk -7.1%
Alpha 0.14
Beta 0.12
/home/driller/work/pyfolio_env/lib/python3.8/site-packages/numpy/core/fromnumeric.py:58: FutureWarning: 
The current behaviour of 'Series.argmin' is deprecated, use 'idxmin'
instead.
The behavior of 'argmin' will be corrected to return the positional
minimum in the future. For now, use 'series.values.argmin' or
'np.argmin(np.array(values))' to get the position of the minimum
row.
  return bound(*args, **kwds)
Worst drawdown periods Net drawdown in % Peak date Valley date Recovery date Duration
0 90.36 2005-12-13 2020-06-25 NaT NaN
1 81.66 2000-12-27 2001-09-26 2005-08-24 1216
2 39.46 1998-04-08 1999-02-26 1999-08-06 348
3 31.44 1999-10-27 1999-11-24 2000-04-17 124
4 23.95 2005-09-29 2005-11-28 2005-12-08 51
/home/driller/work/pyfolio_env/lib/python3.8/site-packages/pyfolio/plotting.py:784: FutureWarning: 
Passing list-likes to .loc or [] with any missing label will raise
KeyError in the future, you can use .reindex() as an alternative.

See the documentation here:
https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#deprecate-loc-reindex-listlike
  bmark_vol = factor_returns.loc[returns.index].std()
/home/driller/work/pyfolio_env/lib/python3.8/site-packages/numpy/core/fromnumeric.py:58: FutureWarning: 
The current behaviour of 'Series.argmin' is deprecated, use 'idxmin'
instead.
The behavior of 'argmin' will be corrected to return the positional
minimum in the future. For now, use 'series.values.argmin' or
'np.argmin(np.array(values))' to get the position of the minimum
row.
  return bound(*args, **kwds)

plot_ほげほげという関数を使うと個別にグラフを出せます。

In [19]:
pyfolio.plot_monthly_returns_heatmap(ng_returns)
Out[19]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fa3312811c0>

pyfolioはBayesian performance analysisやSector Mappingsなど、さまざまな機能があるのですが疲れ果てたのでここまでとします。