本篇博文是对上一篇文章,链接:
1. 揭秘原始马科维茨资产组合模型(理论+Python实战)
的预期收益计算方式进行反思与优化。为什么直接使用历史平均收益,作为预期收益,存在较大的问题,主要原因有以下几点:
注意:本文是在前一篇文章的基础上,使用CAMP对原金融资产组合模型进行预期收益计算方式的优化,大框架仍然是马科维茨资产组合模型,因此,以下文章称MPT-CAPM组合优化模型。
如果想更加全面清晰地了解金融资产组合模型进化论的体系架构,可参考:
0. 金融资产组合模型进化全图鉴
这是一个在现代投资组合理论(MPT)框架下,使用资本资产定价模型(CAPM)优化预期收益率估计的投资组合模型。MPT通过分散投资来平衡风险和收益,而CAPM则通过考虑资产与市场的系统性风险(Beta)来提供更准确的预期收益率估计。该模型保持了MPT的投资组合优化框架,同时引入CAPM来改进收益预测的准确性。
CAPM 相较于原始版本的均值方差预期收益的两大优势:
大白话:考虑的风险更加全面,而且预期收益率的计算更加合理。
MPT框架优势:
CAPM改进:
参数集设置:
数据准备:
计算流程:
设置模型所需的基本参数,包括数据获取、回测区间和优化约束等。
# 参数集
ts.set_token('token')
pro = ts.pro_api()
industry = '银行'
end_date = '20240101'
years = 5 # 数据时长
risk_free_rate = 0.03 # 无风险利率参数
top_holdings = 10 # 持仓数量参数
index_code = '000300.SH' # 市场指数代码参数
获取股票和市场指数数据,并进行必要的数据清洗和格式转换。
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
def get_market_data(index_code='000300.SH', start_date=None, end_date=None):
df_market = pro.index_daily(ts_code=index_code,
start_date=start_date,
end_date=end_date,
fields=['trade_date', 'close'])
df_market['date'] = pd.to_datetime(df_market['trade_date'])
df_market.set_index('date', inplace=True)
monthly_last_close = df_market['close'].resample('M').last()
monthly_log_returns = np.log(monthly_last_close).diff().dropna()
return monthly_log_returns
计算月度对数收益率,为后续的优化计算做准备。
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
使用CAPM模型计算预期收益率,考虑Beta系数和市场风险溢价。
def calculate_expected_returns(monthly_log_returns):
start_date = monthly_log_returns.index.min().strftime('%Y%m%d')
end_date = monthly_log_returns.index.max().strftime('%Y%m%d')
# 获取市场收益率
market_returns = get_market_data(index_code, start_date, end_date)
# 对齐数据
aligned_dates = monthly_log_returns.index.intersection(market_returns.index)
market_returns = market_returns[aligned_dates]
stock_returns = monthly_log_returns.loc[aligned_dates]
# 计算Beta值
betas = {}
for stock in stock_returns.columns:
X = sm.add_constant(market_returns)
y = stock_returns[stock]
model = sm.OLS(y, X).fit()
betas[stock] = model.params[1]
# 计算市场风险溢价
market_risk_premium = market_returns.mean() - risk_free_rate
# CAPM预期收益率
expected_returns = pd.Series({
stock: risk_free_rate + beta * market_risk_premium
for stock, beta in betas.items()
})
return expected_returns
计算收益率的协方差矩阵,用于评估资产间的相关性和波动性。
def calculate_covariance_matrix(monthly_log_returns):
return monthly_log_returns.cov()
计算给定权重下投资组合的预期收益率和波动率。
def portfolio_performance(weights, mean_returns, cov_matrix):
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):
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
选取权重最大的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
以下即为全量代码,修改参数集中内容即可跑出个性化数据。
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
import statsmodels.api as sm
# 参数集##############################################################################
ts.set_token('token')
pro = ts.pro_api()
industry = '银行'
end_date = '20240101'
years = 5 # 数据时长
risk_free_rate = 0.03 # 无风险利率参数
top_holdings = 10 # 持仓数量参数
index_code = '000300.SH' # 市场指数代码参数
# 参数集##############################################################################
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 get_market_data(index_code='000300.SH', start_date=None, end_date=None):
"""获取市场指数数据用于计算贝塔"""
df_market = pro.index_daily(ts_code=index_code,
start_date=start_date,
end_date=end_date,
fields=['trade_date', 'close'])
df_market['date'] = pd.to_datetime(df_market['trade_date'])
df_market.set_index('date', inplace=True)
df_market = df_market.sort_index()
# 计算月度收益率
monthly_last_close = df_market['close'].resample('M').last()
monthly_log_returns = np.log(monthly_last_close).diff().dropna()
return monthly_log_returns
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):
"""
使用CAPM模型计算各股票的预期收益率。
参数:
monthly_log_returns (pd.DataFrame): 月度对数收益率数据框
返回:
pd.Series: 各股票的预期收益率
"""
# 计算开始日期和结束日期
start_date = monthly_log_returns.index.min().strftime('%Y%m%d')
end_date = monthly_log_returns.index.max().strftime('%Y%m%d')
# 获取市场收益率
market_returns = get_market_data(index_code, start_date, end_date)
# 确保市场收益率和股票收益率的日期对齐
aligned_dates = monthly_log_returns.index.intersection(market_returns.index)
market_returns = market_returns[aligned_dates]
stock_returns = monthly_log_returns.loc[aligned_dates]
# 使用OLS回归计算每个股票的beta值
betas = {}
for stock in stock_returns.columns:
# 添加常数项以拟合截距
X = sm.add_constant(market_returns)
y = stock_returns[stock]
# 使用OLS回归
model = sm.OLS(y, X).fit()
# 提取beta值(市场因子的系数)
betas[stock] = model.params[1]
# 计算市场风险溢价(使用历史平均收益率)
market_risk_premium = market_returns.mean() - risk_free_rate
# 使用CAPM计算预期收益率
expected_returns = pd.Series({
stock: risk_free_rate + beta * market_risk_premium
for stock, beta in betas.items()
})
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)
# 使用CAPM计算预期收益率
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只最优配置的股票及其权重。
股票代码 | 占比 |
---|---|
600908.SH | 0.14546516622150177 |
002839.SZ | 0.13519189871249462 |
601398.SH | 0.12375994065762526 |
601288.SH | 0.11294451004384103 |
002936.SZ | 0.10621692230066024 |
600919.SH | 0.10129082773382357 |
600036.SH | 0.08097621713692311 |
600015.SH | 0.07869329345698277 |
601328.SH | 0.058865170485181945 |
600016.SH | 0.05659605325096555 |
本文章的MPT+CAMP所考虑的因子只有一个市场因子(Beta),即个股收益与市场指数(本文使用沪深300指数)走势呈线性相关。但是这种仅考虑个股历史收益,与大盘指数历史收益的思路,忽略了个股自身的指标问题。比如说如果个股突然财务恶化,但历史收益非常好,该资产组合仍会因为历史数据好看而继续给该个股较高的持仓占比,这是我们不希望看到的。因此,我们不仅仅要考虑个股历史走势与大盘指数走势,还要考虑个股自身财务指标。
由此引进,Fama-French三因子模型,对预期收益率进行进一步优化:
优化,预期收益率的优化方案:,可参考下一篇文章:
3. 马科维茨资产组合模型+Fama-French三因子优化方案(理论+Python实战)
量化回测实现,可参考下一篇文章:
2.1 对MPT+CAMP优化方案实现Backtrader量化回测(理论+Python实战)