用 Python 繪製效率前緣(Efficient Frontier)

什麼是效率前緣(Efficient Frontier)?

當今天投資人持有一投資組合,透過改變投資組合中資產的權重,能得出數種不同的可能,而在此集合中符合

1. 同樣的報酬率下風險最小

2. 同樣的風險下報酬率最高

以上兩個條件的投資組合,所連結繪出而成的部份即為效率前緣

投資組合理論至今仍是財務金融界中廣為被討論的主題,其中由Markowitz所提出的效率前緣(Efficient Frontier)也是財金系學生一定會學習到的知識

從封面圖中的曲線,可以發現到由下往上時,風險將逐漸的減少,同時報酬率也緩步著上升。

而經過了轉折點(最小變異數投資組合,星號的位置),上面曲線開始往右上方走,在轉折點的上方就具備著前面所提到的兩個特性,在同樣的風險下,曲線上緣的報酬率將高過下緣,而同樣報酬率的情況,曲線上緣的投資組合也承擔最小風險

介紹理論的同時,本文也將搭配著Python實作,來實現繪製效率前緣圖。

投資組合預期報酬率與風險

按照慣例先來導入需要用的套件

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import scipy.optimize as solver
from functools import reduce

比較特別的為第四行的solver,這個套件類似Excel中的規劃求解,可以求出極小化的狀況。

接著將利用台灣股票中隨機的10檔股票建構一個投資組合。

首先先讀取資料,並且計算股票的報酬率,然後將資料後移一天,這樣就能除去因為計算報酬率而第一天會出現的缺失值

df = pd.read_csv('efficient_frontier.csv',
                 index_col=0, parse_dates=True)

total_stocks = len(df.columns)

returns = df.pct_change()
returns = returns[1:]
returns.head()

成功讀取資料後,接著計算共變異數矩陣還有預期報酬率,乘上252的原因是因為原本的資料頻率為日資料,要進行年化,因此將計算出的結果乘上概約一年的交易日(252天)

再以平均權重,計算投資組合的報酬率及風險

covariance_matrix = returns.cov() * 252
stocks_expected_return = returns.mean() * 252
stocks_weights = np.array([.1, ]*total_stocks)

portfolio_return = sum(stocks_weights * stocks_expected_return)
portfolio_risk = np.sqrt(reduce(np.dot, [stocks_weights, covariance_matrix, stocks_weights.T]))

print('預期報酬率為: ' + str(round(portfolio_return, 4)))
print('風險為: ' + str(round(portfolio_risk, 4)))

比較特別的為reduce(),函數中的第一個元素np.dot,為矩陣相乘的函數,而reduce()能讓第二個元素中list中的三個變數,前兩個先套用np.dot矩陣相乘後,再將相乘結果和三個元素做矩陣相乘。

此計算過程即為衡量投資組合風險的公式,而stocks_weights.T 的.T就是轉置的意思,數學表示如下

$$ w^{T}\sum w $$

投資組合的預期報酬率為0.2034,本文範例簡易的假設未來一年股票的報酬率和過去一年相仿,預期報酬率並無最明確的計算方式,預期的結果,會因為假設和使用方法不同而有所差異

此範例將情況簡單討論,直接以去年的平均日報酬率乘上一年的交易日,作為每支股票未來一年的預期報酬率,讀者有機會不妨試試其他方法計算股票預期報酬率。

隨機亂數模擬

接著模擬各種不同權重,看不同權重下建構的投資組合風險和報酬率是如何分佈的

risk_list = []
return_list = []
simulations_target = 10**6
for _ in range(simulations_target):

    # random weighted
    weight = np.random.rand(total_stock)
    weight = weight / sum(weight)

    # calculate result
    ret = sum(stocks_expected_return * weight)
    risk = np.sqrt(reduce(np.dot, [weight, covariance_matrix, weight.T]))

    # record
    return_list.append(ret)
    risk_list.append(risk)


fig = plt.figure(figsize=(10, 6))
fig.suptitle('Stochastic Simulations', fontsize=18, fontweight='bold')

ax = fig.add_subplot()
ax.plot(risk_list, return_list, 'o')
ax.set_title(f'n={simulations_target}', fontsize=16)

fig.savefig('Stochastic_Simulations.png', dpi=300)

模擬完後發現結果呈現類似扇子的形狀,上頭的每一個點都代表著不同權重下所建構的投資組合

最小變異數投資組合(Minimum Variance PortfolioMVP)

除了模擬之外,也能夠透過極小化的方式求解,找出最小變異數的投資組合(Minimize Variance Portfolio,MVP)

先定義一個standard_deviation函式計算投資組合的風險,等等會使用到

def standard_deviation(weights):
    return np.sqrt(reduce(np.dot, [weights, covariance_matrix, weights.T]))

接著使用solverminimize方法求出最小變異數的投資組合

x0 = stocks_weights
bounds = tuple((0, 1) for x in range(total_stocks))
constraints = [{'type': 'eq', 'fun': lambda x: sum(x) - 1}]
minimize_variance = solver.minimize(
    standard_deviation, x0=x0, constraints=constraints, bounds=bounds
)

mvp_risk = minimize_variance.fun
mvp_return = sum(minimize_variance.x * stocks_expected_return)

print('風險最小化投資組合預期報酬率為:' + str(round(mvp_return, 2)))
print('風險最小化投資組合風險為:' + str(round(mvp_risk, 2)))

上方程式碼中的x0為極小化過程的變數,也就是各個股票的投資權重

bounds則是每個變數(權重)的上下界,因為有10檔股票,所以後面加了迴圈。

constraints為限制式,設定所有的變數總和為1(不考慮放空情況)

最後利用預先定義的投資組合風險函式來極小化結果,得到報酬率為0.12,風險則為0.08。

for i in range(total_stocks):
    stock_symbol = str(df.columns[i])
    weighted = str(format(minimize_variance.x[i], '.4f'))
    print(f'{stock_symbol} 佔投資組合權重 : {weighted}')

最小化變異數的投資組合中,連台積電(2330)也不配置權重,猜想因為波動過太大的緣故,所以沒辦法配置選入。

效率前緣程式碼

接著繪製效率前緣,過程如下

  1. 給定報酬率
  2. 透過改變股票權重,求該報酬率下風險最小值

constraints第二行多加了一個限制{'type': 'eq', 'fun': lambda x: sum(x* stocks_expected_return) - i}就是因為給定報酬率的緣故。

x0 = stocks_weights
bounds = tuple((0, 1) for x in range(total_stocks))

efficient_fronter_return_range = np.arange(0.05, 0.35, .005)
efficient_fronter_risk_list = []

for i in efficient_fronter_return_range:
    constraints = [{'type': 'eq', 'fun': lambda x: sum(x) - 1},
                   {'type': 'eq', 'fun': lambda x: sum(x * stocks_expected_return) - i}]
    efficient_fronter = solver.minimize(
        standard_deviation, x0=x0, constraints=constraints, bounds=bounds
    )
    efficient_fronter_risk_list.append(efficient_fronter.fun)

資料視覺化程式碼

求出結果後,再將資料視覺化

risk_free = 0.01

fig = plt.figure(figsize=(12, 6))
fig.subplots_adjust(top=0.85)
ax = fig.add_subplot()

fig.subplots_adjust(top=0.85)
ax0 = ax.scatter(risk_list, return_list,
                 c=(np.array(return_list)-risk_free)/np.array(risk_list),
                 marker='o')
ax.plot(
    efficient_fronter_risk_list, 
    efficient_fronter_return_range, 
    linewidth=1, 
    color='#251f6b', 
    marker='o',
    markerfacecolor='#251f6b', 
    markersize=5
)
ax.plot(
    mvp_risk, 
    mvp_return, 
    '*', 
    color='r', 
    markerfacecolor='#ed1313',  
    markersize=25
)

ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.set_title('Efficient Frontier', fontsize=22, fontweight='bold')
ax.set_xlabel('Risk')
ax.set_ylabel('Return')

fig.colorbar(ax0, ax=ax, label='Sharpe Ratio')
plt.savefig('Efficient_Frontier.png', dpi=300)

非常完美的弧線,在深藍色線上的投資組合就符合了前面提到的兩個要點,同樣報酬率之下風險最低,同樣風險之下報酬率最高,紅色星星即為最小變異數投資組合,紅色星星以上的藍色區塊,即為效率前緣(Efficient Frontier)

圖中也把每個投資組合的夏普值(sharpe ratio)求出,並用顏色標註,越往左上方的顏色越亮,是因為越往左上的報酬率越高,風險越小,資產運用效率越佳

結語

投資人可以試著利用這樣的工具來計算手上投資組合適合的配置權重,但重點在於要如何衡量資產的預期報酬率,本篇將情況簡化,直接以前一年的資訊作為預測,但投資人在操作時可以將預期報酬率多加入些主觀看法。

Tags:
# python
# finance
# investing
# pandas
# numpy