目录
0. 承前
如果想更加全面清晰地了解金融资产组合模型进化论的体系架构,可参考:
0. 金融资产组合模型进化全图鉴
1. 现代投资组合理论(MPT)优化模型
1.1 What is 现代投资组合理论优化模型
现代投资组合理论(Modern Portfolio Theory, MPT)是一种通过优化资产配置来实现投资组合风险收益平衡的投资策略模型。该模型通过最大化夏普比率(Sharpe Ratio)来寻找最优的资产配置权重,以在给定风险水平下获得最大的超额收益。
1.2 Why is 现代投资组合理论优化模型
假设你是一个投资组合经理,现在收到任务是使用指定的资金,对10个资产进行投资,你是直接每个资产平分10%?还是使用诺奖得主的金融模型来构建自己的投资组合?当然是后者!
因为我们在投资过程中,隐约会觉得需要考虑风险&收益,但是该怎么考虑?马科维茨资产组合模型,就为你提供了一个科学的思维框架。
以下因素,会影响单个或多个资产在马科维茨资产组合的占比:
- 收益率:单个资产的收益率,及其预期收益率,会正向影响该资产的占比;
- 波动率:单个资产的波动率,会反向影响该资产的占比;
- 相关性:多个资产的相关性,会反向影响多个资产的占比;
大白话:马科维茨资产组合模型是一种追求收益(收益率),规避风险(波动率、相关性)的金融模型。
- 优点概括:
- 科学量化:通过数学模型实现投资组合的科学配置
- 分散投资:考虑资产间相关性,有效降低非系统性风险
- 风险收益平衡:在收益与风险之间寻找最优平衡点
- 客观决策:减少主观判断带来的决策偏差
1.3 How to 现代投资组合理论优化模型
- 金融工程流程图:
-
参数集设置:
- ts.set_token:设置Tushare的API访问令牌
- industry:选择目标行业,如"银行"
- end_date:回测结束日期,格式为’YYYYMMDD’
- years:回测年限,默认5年
- risk_free_rate:无风险利率,默认0.03
- top_holdings:投资组合持仓数量,默认10只股票
-
数据准备:
- 股票行业数据:通过tushare获取指定行业的股票列表
- 历史价格数据:获取指定时间段内的股票日线数据
- 无风险利率:设定无风险利率参数
- 持仓限制:设定投资组合的最大持仓数量
-
计算流程:
- 数据获取:获取行业股票列表和历史价格数据
- 收益率计算:计算月度对数收益率
- 统计指标计算:计算预期收益率和协方差矩阵
- 权重优化:最大化夏普比率得到最优权重
- 持仓筛选:选取权重最大的N只股票并归一化
2. 数据要素&计算流程
2.1 参数集设置
设置模型所需的基本参数,包括数据获取、回测区间和优化约束等。
# 参数集
ts.set_token('token')
pro = ts.pro_api()
industry = '银行'
end_date = '20240101'
years = 5 # 数据时长
risk_free_rate = 0.03 # 无风险利率参数
top_holdings = 10 # 持仓数量参数
2.2 数据获取&预处理
获取指定行业的股票列表和历史价格数据,并进行必要的数据清洗和格式转换。
def get_industry_stocks(industry):
df = pro.stock_basic(fields=["ts_code", "name", "industry"])
industry_stocks = df[df["industry"]==industry].copy()
industry_stocks.sort_values(by='ts_code', inplace=True)
industry_stocks.reset_index(drop=True, inplace=True)
return industry_stocks['ts_code'].tolist()
def get_data(code_list, end_date, years):
# 计算时间区间
end_date_dt = datetime.strptime(end_date, '%Y%m%d')
start_date_dt = end_date_dt - timedelta(days=years*365)
start_date = start_date_dt.strftime('%Y%m%d')
# 获取历史数据
all_data = []
for stock in code_list:
df = pro.daily(ts_code=stock, start_date=start_date, end_date=end_date)
all_data.append(df)
combined_df = pd.concat(all_data).sort_values(by=['ts_code', 'trade_date'])
return combined_df
2.3 收益率计算
计算月度对数收益率,为后续的优化计算做准备。
def calculate_monthly_log_returns(df):
df['date'] = pd.to_datetime(df['date'])
monthly_last_close = df.groupby(['ts_code', pd.Grouper(key='date', freq='M')])['close'].last().unstack(level=-1)
monthly_log_returns = np.log(monthly_last_close).diff().dropna()
return monthly_log_returns.T
2.4 预期收益率计算(重点)
计算各个股票的预期收益率,使用历史平均月度对数收益率作为预期收益的估计。
def calculate_expected_returns(monthly_log_returns):
"""
计算各股票的预期收益率。
参数:
monthly_log_returns (pd.DataFrame): 月度对数收益率数据框
返回:
pd.Series: 各股票的预期收益率(平均月度对数收益率)
"""
expected_returns = monthly_log_returns.mean()
return expected_returns
2.5 协方差矩阵计算(重点)
计算收益率的协方差矩阵,用于评估资产间的相关性和波动性。
def calculate_covariance_matrix(monthly_log_returns):
"""
计算收益率协方差矩阵。
参数:
monthly_log_returns (pd.DataFrame): 月度对数收益率数据框
返回:
pd.DataFrame: 收益率协方差矩阵
"""
return monthly_log_returns.cov()
2.6 投资组合表现计算
计算给定权重下投资组合的预期收益率和波动率。
def portfolio_performance(weights, mean_returns, cov_matrix):
"""
计算投资组合的表现:预期收益率和波动率。
参数:
weights (array-like): 资产权重数组
mean_returns (pd.Series): 各资产的平均收益率
cov_matrix (pd.DataFrame): 收益率协方差矩阵
返回:
tuple: 预期收益率和波动率
"""
returns = np.sum(mean_returns * weights)
std_dev = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
return returns, std_dev
2.7 夏普比率优化
通过最大化夏普比率来寻找最优权重配置。
def negative_sharpe_ratio(weights, mean_returns, cov_matrix, risk_free_rate):
"""
计算负夏普比率(用于最小化)。
"""
p_ret, p_std = portfolio_performance(weights, mean_returns, cov_matrix)
sharpe_ratio = (p_ret - risk_free_rate) / p_std
return -sharpe_ratio
def max_sharpe_ratio(mean_returns, cov_matrix, risk_free_rate):
"""
计算最大夏普比率的投资组合权重。
"""
num_assets = len(mean_returns)
args = (mean_returns, cov_matrix, risk_free_rate)
constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bounds = tuple((0, 1) for asset in range(num_assets))
result = minimize(negative_sharpe_ratio, num_assets*[1./num_assets], args=args,
method='SLSQP', bounds=bounds, constraints=constraints)
return result.x
2.8 持仓筛选
选取权重最大的N只股票并重新归一化权重。
def calculate_top_holdings_weights(optimal_weights, monthly_log_returns_columns, top_n):
result_dict = {asset: weight for asset, weight in zip(monthly_log_returns_columns, optimal_weights)}
top_n_holdings = sorted(result_dict.items(), key=lambda item: item[1], reverse=True)[:top_n]
top_n_sum = sum(value for _, value in top_n_holdings)
updated_result = {key: value / top_n_sum for key, value in top_n_holdings}
return updated_result
3. 汇总代码
以下即为全量代码,修改参数集中内容即可跑出个性化数据。
import tushare as ts
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from scipy.optimize import minimize
import backtrader as bt
# 参数集##############################################################################
ts.set_token('token')
pro = ts.pro_api()
industry = '银行'
end_date = '20240101'
years = 5 # 数据时长
risk_free_rate = 0.03 # 无风险利率参数
top_holdings = 10 # 持仓数量参数
# 参数集##############################################################################
def get_industry_stocks(industry):
"""
获取指定行业的股票列表。
参数:
industry (str): 行业名称,如"银行"
返回:
list: 该行业的股票代码列表
pd.DataFrame: 包含股票代码和名称的数据框
"""
# 获取指定行业名称的代码列表
df = pro.stock_basic(fields=["ts_code", "name", "industry"])
industry_stocks = df[df["industry"]==industry].copy()
# 排序并重置索引
industry_stocks.sort_values(by='ts_code', inplace=True)
industry_stocks.reset_index(drop=True, inplace=True)
return industry_stocks['ts_code'].tolist()
def get_data(code_list, end_date, years):
"""
获取指定行业名称的历史收盘价数据。
参数:
industry (str): 行业名称,当前指定输入"银行"
end_date (str): 结束日期,格式为 'YYYYMMDD'
years (int): 时间长度(年)
返回:
pd.DataFrame: 各股票的每日收益率
"""
# 获取行业股票列表
ts_code_list = code_list
# 计算开始日期
end_date_dt = datetime.strptime(end_date, '%Y%m%d')
start_date_dt = end_date_dt - timedelta(days=years*365)
start_date = start_date_dt.strftime('%Y%m%d')
# 获取历史收盘价
all_data = []
for stock in ts_code_list:
df = pro.daily(ts_code=stock, start_date=start_date, end_date=end_date)
all_data.append(df)
# 合并所有数据
combined_df = pd.concat(all_data).sort_values(by=['ts_code', 'trade_date'])
# 重置索引并按ts_code分组
combined_df.reset_index(drop=True, inplace=True)
combined_df.rename(columns={'trade_date': 'date'}, inplace=True)
return combined_df
def calculate_monthly_log_returns(df):
"""
根据每日收盘价计算每月的对数收益率。
参数:
df (pd.DataFrame): 包含多个资产每日收盘价的数据框
返回:
pd.DataFrame: 包含每月对数收益率的新数据框
"""
# 确保日期列为 datetime 类型
df['date'] = pd.to_datetime(df['date'])
# 按月份分组并计算每月最后一个交易日的收盘价
monthly_last_close = df.groupby(['ts_code', pd.Grouper(key='date', freq='M')])['close'].last().unstack(level=-1)
# 计算月度对数收益率
monthly_log_returns = np.log(monthly_last_close).diff().dropna()
return monthly_log_returns.T
def calculate_expected_returns(monthly_log_returns):
"""
计算各股票的预期收益率。
参数:
monthly_log_returns (pd.DataFrame): 月度对数收益率数据框
返回:
pd.Series: 各股票的预期收益率(平均月度对数收益率)
"""
# 计算各资产的平均月度对数收益率作为预期收益率
expected_returns = monthly_log_returns.mean()
return expected_returns
def calculate_covariance_matrix(monthly_log_returns):
"""
计算收益率协方差矩阵。
参数:
monthly_log_returns (pd.DataFrame): 月度对数收益率数据框
返回:
pd.DataFrame: 收益率协方差矩阵
"""
return monthly_log_returns.cov()
def portfolio_performance(weights, mean_returns, cov_matrix):
"""
计算投资组合的表现:预期收益率和波动率。
参数:
weights (array-like): 资产权重数组
mean_returns (pd.Series): 各资产的平均收益率
cov_matrix (pd.DataFrame): 收益率协方差矩阵
返回:
tuple: 预期收益率和波动率
"""
returns = np.sum(mean_returns * weights)
std_dev = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
return returns, std_dev
def negative_sharpe_ratio(weights, mean_returns, cov_matrix, risk_free_rate):
"""
计算负夏普比率(用于最小化)。
参数:
weights (array-like): 资产权重数组
mean_returns (pd.Series): 各资产的平均收益率
cov_matrix (pd.DataFrame): 收益率协方差矩阵
risk_free_rate (float): 无风险利率
返回:
float: 负夏普比率
"""
p_ret, p_std = portfolio_performance(weights, mean_returns, cov_matrix)
sharpe_ratio = (p_ret - risk_free_rate) / p_std
return -sharpe_ratio
def max_sharpe_ratio(mean_returns, cov_matrix, risk_free_rate):
"""
计算最大夏普比率的投资组合权重。
参数:
mean_returns (pd.Series): 各资产的平均收益率
cov_matrix (pd.DataFrame): 收益率协方差矩阵
risk_free_rate (float): 无风险利率
返回:
dict: 最优权重
"""
num_assets = len(mean_returns)
args = (mean_returns, cov_matrix, risk_free_rate)
constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bounds = tuple((0, 1) for asset in range(num_assets))
result = minimize(negative_sharpe_ratio, num_assets*[1./num_assets], args=args,
method='SLSQP', bounds=bounds, constraints=constraints)
return result.x
def calculate_top_holdings_weights(optimal_weights, monthly_log_returns_columns, top_n):
"""
计算前N大持仓的权重占比。
参数:
optimal_weights (array-like): 优化后的权重数组
monthly_log_returns_columns: 股票代码列表
top_n (int): 需要保留的前N个持仓数量
返回:
dict: 归一化后的前N大持仓权重
"""
# 创建结果字典
result_dict = {asset: weight for asset, weight in zip(monthly_log_returns_columns, optimal_weights)}
# 提取前N大占比值
top_n_holdings = sorted(result_dict.items(), key=lambda item: item[1], reverse=True)[:top_n]
# 计算前N大值的总和
top_n_sum = sum(value for _, value in top_n_holdings)
# 更新保留的值
updated_result = {key: value / top_n_sum for key, value in top_n_holdings}
return updated_result
def main():
# 获取数据
code_list = get_industry_stocks(industry)
df = get_data(code_list, end_date, years)
# 计算每月的对数收益率
monthly_log_returns = calculate_monthly_log_returns(df)
# 计算预期收益率
mean_returns = calculate_expected_returns(monthly_log_returns)
# 计算收益率协方差矩阵
cov_matrix = calculate_covariance_matrix(monthly_log_returns)
# 优化权重
optimal_weights = max_sharpe_ratio(mean_returns, cov_matrix, risk_free_rate)
# 计算前N大持仓权重
updated_result = calculate_top_holdings_weights(
optimal_weights,
monthly_log_returns.columns,
top_holdings
)
# 打印更新后的资产占比
print(f"\n{end_date}最优资产前{top_holdings}占比:")
print(updated_result)
if __name__ == "__main__":
main()
运行结果:
根据参数设置,程序会输出指定行业前N只最优配置的股票及其权重。
股票代码 | 占比 |
---|---|
601328.SH | 0.35523481104625515 |
002948.SZ | 0.20587305181720306 |
600016.SH | 0.15635926106496267 |
601398.SH | 0.07370383829427572 |
601997.SH | 0.061066273596321566 |
601288.SH | 0.03667856492576135 |
600036.SH | 0.035718928637484534 |
600919.SH | 0.031285868893261924 |
601998.SH | 0.029297209174020455 |
601166.SH | 0.014782192550453486 |
4. 反思
4.1 不足之处
- 假设条件理想化:假设收益率服从正态分布
- 历史数据依赖:过度依赖历史数据预测未来
4.2 提升思路
- 引入CAPM模型:考虑系统性风险
- 动态调整:添加时间序列分析
- 多目标优化:引入更多风险指标
5. 启后
本文章的马科维茨资产组合的最优组合仅是该系列的一个开端,有经验的资产组合经理应该能够轻松看出其中的不合理之处,比如使用过去的收益均值作为预期收益率,有可能会把该资产组合制作成为“追高杀低”的组合策略。因此,之后我们将会基于当前代码以及思路,持续进行优化推进。
-
优化:预期收益率的优化方案,可参考下一篇文章:
2. 马科维茨资产组合模型+CAMP优化方案(理论+Python实战) -
量化回测实现,可参考下一篇文章:
1.1 对马科维茨资产组合模型实现Backtrader量化回测(理论+Python实战)