Portforlios and Risk

Portforlios and Risk

本文是学习 Mastering pandas for finance 一书最后一章的笔记。关于有效前沿的内容建议参考我的另一篇博客:马科维茨有效前沿实现(Python 版本)

准备工作

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# !/anaconda3/bin/python
# -*- coding: utf-8 -*-
"""
标题 : Portforlios and Risk
作者 : 程振兴
创建日期: 2019-03-25
"""
import pandas as pd
import numpy as np
import tushare as ts
import matplotlib.pyplot as plt
from datetime import datetime
from matplotlib import rcParams

pro = ts.pro_api('你的 Tushare 密钥')
from matplotlib.font_manager import FontProperties

# 中文字体
cnfont = FontProperties(fname='/Library/Fonts/Songti.ttc', size=14)
# 英文字体
enfont = FontProperties(fname='/Users/czx/Library/Fonts/RobotoSlab-Regular.ttf', size=14)
# 解决负号'-'显示为方块的问题
rcParams['axes.unicode_minus'] = False
rcParams['savefig.dpi'] = 300 # 图片像素
rcParams['figure.dpi'] = 300 # 分辨率

#==========================================================#

import scipy as sp
import scipy.optimize as scopt
import scipy.stats as spstats
import matplotlib.mlab as mlab

Modeling a portfolio with pandas

一个创建资产组合的函数:

Python
1
2
3
4
5
6
7
8
9
10
11
def create_portfolio(tickers, weight = None):
if(weight is None):
shares = np.ones(len(tickers)) . len(tickers)
portfolio = pd.DataFrame({'Tickers': tickers, 'Weights': weight}, index = tickers)
return portfolio

portfolio = create_portfolio(['Stock A', 'Stock B'], [1, 1])
portfolio
#> Tickers Weights
#> Stock A Stock A 1
#> Stock B Stock B 1

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
returns = pd.DataFrame(
{
'Stock A': [0.1, 0.24, 0.05, -0.02, 0.2],
'Stock B': [-0.15, -0.2, -0.01, 0.04, -0.15]
}
)

returns
#> Stock A Stock B
#> 0 0.10 -0.15
#> 1 0.24 -0.20
#> 2 0.05 -0.01
#> 3 -0.02 0.04
#> 4 0.20 -0.15
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def calculate_weighted_portfolio_value(portfolio, returns, name = 'Value'):
total_weights = portfolio.Weights.sum()
weighted_returns = returns * (portfolio.Weights / total_weights)
return pd.DataFrame({name: weighted_returns.sum(axis = 1)})

wr = calculate_weighted_portfolio_value(portfolio, returns, "Value")
with_value = pd.concat([returns, wr], axis = 1)
with_value
#> Stock A Stock B Value
#> 0 0.10 -0.15 -0.025
#> 1 0.24 -0.20 0.020
#> 2 0.05 -0.01 0.020
#> 3 -0.02 0.04 0.010
#> 4 0.20 -0.15 0.025

with_value.std()
#> Stock A 0.106677
#> Stock B 0.103102
#> Value 0.020310
#> dtype: float64

可以看出两个收益负相关的股票组合波动率更小。

Python
1
2
3
4
5
6
7
8
9
def plot_portfolio_returns(returns, title = None):
returns.plot(figsize = (12, 8))
plt.xlabel('Year')
plt.ylabel('Returns')
if (title is not None): plt.title(title)
plt.savefig('20180410a1.svg')
plt.show()

plot_portfolio_returns(with_value)

Python
1
2
3
4
returns.corr()
#> Stock A Stock B
#> Stock A 1.000000 -0.925572
#> Stock B -0.925572 1.000000

Constructing an efficient portfolio

  1. Gathering of historical returns on the assets in the portfolio;
  2. Formulation of portfolio risk based on historical returns;
  3. Determining the Sharpe ratio for a portfolio;
  4. Selecting optimal portfolios based upon Sharpe ratios.

Gathering historical returns for a portfolio

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def get_historical_closes(ticker, start_date, end_date):
df1 = pro.daily(ts_code = ticker[0], start_date = start_date, end_date = end_date)
df1.index = pd.to_datetime(df1.trade_date)
df = pd.DataFrame(index=df1.index)
df = pd.concat([df, df1.close], axis=1)
df.rename(columns={'close': ticker[0]}, inplace=True)
for code in ticker:
if code is not ticker[0]:
temp = pro.daily(ts_code = code, start_date = start_date, end_date = end_date)
temp.index = pd.to_datetime(temp.trade_date)
df = pd.concat([df, temp.close], axis=1)
df.rename(columns={'close': code}, inplace=True)
return df

closes = get_historical_closes(['000001.SZ', '000662.SZ', '000950.SZ'], '20150101', '20181231')
closes[:5]
#> 000001.SZ 000662.SZ 000950.SZ
#> trade_date
#> 2015-01-05 16.02 6.91 5.49
#> 2015-01-06 15.78 7.60 5.54
#> 2015-01-07 15.48 8.36 5.74
#> 2015-01-08 14.96 9.20 5.68
#> 2015-01-09 15.08 9.00 5.70

计算日收益率:

Python
1
2
3
4
5
6
7
8
9
10
11
def  calc_daily_returns(closes):
return np.log(closes / closes.shift(1))
daily_returns = calc_daily_returns(closes)
daily_returns[: 5]
#> 000001.SZ 000662.SZ 000950.SZ
#> trade_date
#> 2015-01-05 NaN NaN NaN
#> 2015-01-06 -0.015095 0.095179 0.009066
#> 2015-01-07 -0.019194 0.095310 0.035465
#> 2015-01-08 -0.034169 0.095745 -0.010508
#> 2015-01-09 0.007989 -0.021979 0.003515

计算年度收益率:

Python
1
2
3
4
5
6
7
8
9
10
def calc_annual_returns(daily_returns):
grouped = np.exp(daily_returns.groupby(lambda trade_date: trade_date.year).sum()) - 1
return grouped
annual_returns = calc_annual_returns(daily_returns)
annual_returns
#> 000001.SZ 000662.SZ 000950.SZ
#> 2015 -0.251561 2.516428 0.755863
#> 2016 -0.241034 -0.131284 -0.065271
#> 2017 0.461538 -0.350890 0.144077
#> 2018 -0.294737 -0.538220 -0.500994

Formulation of portfolio risks

Python
1
2
3
4
5
6
7
8
def calc_portfolio_var(returns, weights = None):
if (weights is None):
weights = np.ones(returns.columns.size) / returns.columns.size
sigma = np.cov(returns.T, ddof = 0)
var = (weights * sigma * weights.T).sum()
return var
calc_portfolio_var(annual_returns)
#> 0.2943101494198389

The Sharpe ratio

Python
1
2
3
4
5
6
7
8
9
def sharpe_ratio(returns, weights = None, risk_free_rate = 0.015):
n = returns.columns.size
if weights is None: weights = np.ones(n) / n
var = calc_portfolio_var(returns, weights)
means = returns.mean()
return (means.dot(weights) - risk_free_rate) / np.sqrt(var)

sharpe_ratio(returns)
#> -0.2752409412815904

Optimization and minimization

Python
1
2
3
4
5
6
7
8
9
# 例如最小化 y = 2 + x^2
def y_f(x): return 2 + x**2
# 1000是随机数种子
scopt.fmin(y_f, 1000)
#> Optimization terminated successfully.
#> Current function value: 2.000000
#> Iterations: 27
#> Function evaluations: 54
#> array([0.])

Constructing an optimal portfolio

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def negative_sharpe_ratio_n_minus_1_stock(weights, returns, risk_free_rate):
"""
Given n-1 weights, return a negative sharpratio
"""
weights2 = sp.append(weights, 1 - np.sum(weights))
return -sharpe_ratio(returns, weights2, risk_free_rate)

def optimize_portfolio(returns, risk_free_rate):
w0 = np.ones(returns.columns.size - 1, dtype = float) * 1.0 / returns.columns.size
w1 = scopt.fmin(negative_sharpe_ratio_n_minus_1_stock, w0, args = (returns, risk_free_rate))
final_w = sp.append(w1, 1 - np.sum(w1))
final_sharpe = sharpe_ratio(returns, final_w, risk_free_rate)
return (final_w, final_sharpe)

optimize_portfolio(annual_returns, 0.03)
#> (array([-1.19507560e+15, 6.04682149e+14, 5.90393456e+14]),
#> 0.3823195500485456)

看起来这三只股票不太适合构建投资组合,再复习一下 QUANTAXIS:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import QUANTAXIS as QA
# 这里需要先启动 mongo数据库:brew services srart mongo
code = QA.QA_fetch_stock_block_adv().get_block('上证50').code[0:4]
code
data = QA.QA_fetch_stock_day_adv(code, '2010-01-01', '2018-12-31').to_qfq()
close = data.pivot('close')
close.head()
#> code 600000 600016 600019 600028
#> date
#> 2010-01-04 6.628596 3.562101 7.068860 7.227268
#> 2010-01-05 6.678647 3.624988 6.994058 7.269104
#> 2010-01-06 6.547264 3.553117 7.098782 7.112217
#> 2010-01-07 6.400240 3.458787 6.754689 6.923953
#> 2010-01-08 6.472188 3.494722 6.792090 6.850739
Python
1
2
3
4
5
6
7
8
annual_returns = calc_annual_returns(calc_daily_returns(close))
optimize_portfolio(annual_returns, 0.03)
#> Optimization terminated successfully.
#> Current function value: -0.171788
#> Iterations: 81
#> Function evaluations: 142
#> (array([ 1.5598406 , 1.38619637, -0.64026596, -1.30577101]),
#> 0.17178781996744713)

Visualizing the efficient frontier

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def objfun(W, R, target_ret):
stock_mean = np.mean(R, axis = 0)
port_mean = np.dot(W, stock_mean)
cov = np.cov(R.T)
port_var = np.dot(np.dot(W, cov), W.T)
penalty = 2000 * abs(port_mean - target_ret)
return np.sqrt(port_var) + penalty

def calc_efficient_frontier(returns):
result_means = []
result_stds = []
result_weights = []
means = returns.mean()
min_mean, max_mean = means.min(), means.max()
nstocks = returns.columns.size
for r in np.linspace(min_mean, max_mean, 100):
weights = np.ones(nstocks) / nstocks
bounds = [(0, 1) for i in np.arange(nstocks)]
constraints = [{'type': 'eq', 'fun': lambda W: np.sum(W) - 1}]
results = scopt.minimize(objfun, weights, (returns, r),
method = 'SLSQP',
constraints = constraints,
bounds = bounds)
if not results.success:
raise Exception(results.message)
result_means.append(np.round(r, 4))
std_ = np.round(np.std(np.sum(returns * results.x, axis = 1)), 6)
result_stds.append(std_)
result_weights.append(np.round(results.x, 5))
return {'Means': result_means,
'Stds': result_stds,
'Weights': result_weights}

frontier_data = calc_efficient_frontier(annual_returns)
frontier_data['Stds'][:5]
#> [0.250647, 0.250308, 0.249987, 0.249683, 0.249397]

frontier_data['Weights'][:5]
#> [array([0., 0., 0., 1.]),
#> array([0. , 0.01122, 0. , 0.98878]),
#> array([0. , 0.02244, 0. , 0.97756]),
#> array([0. , 0.03366, 0. , 0.96634]),
#> array([0. , 0.04488, 0. , 0.95512])]

def plot_efficient_frontier(ef_data):
plt.title('Efficient Frontier')
plt.xlabel('Standard Deviation of the portfolio (Risk)')
plt.ylabel('Return of the portfolio')
plt.plot(ef_data['Stds'], ef_data['Means'], '--')
plt.savefig('efficient_frontier.svg')
plt.show()

plot_efficient_frontier(frontier_data)

Value at Risk

Python
1
2
3
4
5
6
7
pingan_closes = get_historical_closes(['000001.SZ'], '20000101', '20190410')
returns = calc_daily_returns(pingan_closes)
plt.hist(returns.values[1:], bins = 100)
plt.xlim(-0.2, 0.2)
plt.title('平安银行日收益率分布', fontproperties = cnfont)
plt.savefig('pinganhist2.svg')
plt.show()

Python
1
2
3
4
5
6
7
8
9
10
# 正态分布的95%分位数为:
z = spstats.norm.ppf(0.95)
# 假如我们在2018年12月29日持有1000股的平安银行:
position = 1000 * pingan_closes.loc['2018-12-28']['000001.SZ']
position
VaR = position * (z * returns['000001.SZ'].std())
VaR
#> trade_date
#> 2018-12-28 420.107616
#> Name: 000001.SZ, dtype: float64

VaR 为 420 元,这意味着,在 95%的置信水平上,我们下一年的最大损失是 420 元。

# Python

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×