2. 马科维茨资产组合模型+CAMP优化方案(理论+Python实战)

目录

    • 0. 承前
    • 1. 资本资产定价模型(CAPM)优化的现代投资组合理论
      • 1.1 What is CAPM优化的现代投资组合理论
      • 1.2 Why is CAPM优化的现代投资组合理论
      • 1.3 How to CAPM优化的现代投资组合理论
    • 2. 数据要素&计算流程
      • 2.1 参数集设置
      • 2.2 数据获取&预处理
      • 2.3 收益率计算
      • 2.4 CAPM预期收益率计算
      • 2.5 协方差矩阵计算
      • 2.6 投资组合表现计算
      • 2.7 夏普比率优化
      • 2.8 持仓筛选
    • 3. 汇总代码
    • 4. 反思
      • 4.1 不足之处
      • 4.2 提升思路
    • 5. 启后

0. 承前

本篇博文是对上一篇文章,链接:
1. 揭秘原始马科维茨资产组合模型(理论+Python实战)
预期收益计算方式进行反思与优化。为什么直接使用历史平均收益,作为预期收益,存在较大的问题,主要原因有以下几点:

  • 长期上违背涨跌规律:设想某个股票在过去一段较长时间处于单边上涨,则会因此提高了预期收益,从而提供了资产组合中的占比。但以往经验我们知道,长期单边上涨后,不仅减少了未来收益的可能性,亦加深了风险的积累。因此,我们应该不希望过去涨势过好的、脱离了正常范围的股票占较高占比。
  • 收益率脱离参考物:仅考虑单独的涨跌势,而忽视当时大环境的资产组合经理,是不及格的。就正如你不能因为看见猪在风口被吹起来,就得出“猪可以飞”的结论。所有的单个资产收益,都应该结合大盘进行思考。因此,我们在此方案中,使用沪深300作为标的物,使每个资产的涨跌有归一的参考物。

注意:本文是在前一篇文章的基础上,使用CAMP对原金融资产组合模型进行预期收益计算方式的优化,大框架仍然是马科维茨资产组合模型,因此,以下文章称MPT-CAPM组合优化模型。

如果想更加全面清晰地了解金融资产组合模型进化论的体系架构,可参考:
0. 金融资产组合模型进化全图鉴

1. 资本资产定价模型(CAPM)优化的现代投资组合理论

1.1 What is CAPM优化的现代投资组合理论

这是一个在现代投资组合理论(MPT)框架下,使用资本资产定价模型(CAPM)优化预期收益率估计的投资组合模型。MPT通过分散投资来平衡风险和收益,而CAPM则通过考虑资产与市场的系统性风险(Beta)来提供更准确的预期收益率估计。该模型保持了MPT的投资组合优化框架,同时引入CAPM来改进收益预测的准确性。

1.2 Why is CAPM优化的现代投资组合理论

CAPM 相较于原始版本的均值方差预期收益的两大优势:

  • 更全面的风险衡量:CAPM 加入了市场整体的风险(系统性风险)。
  • 区分两类风险:CAPM 明确区分了两种风险——市场风险和个股特有的风险(非系统性风险)。它指出,只有市场风险才值得额外的回报,而个股特有的风险可以通过分散投资来消除。这使得投资者可以更清楚地知道哪些风险是值得承担的。

大白话:考虑的风险更加全面,而且预期收益率的计算更加合理。

  • MPT框架优势

    1. 分散投资:通过资产组合降低非系统性风险
    2. 量化优化:使用夏普比率进行组合权重优化
    3. 风险平衡:在收益与风险之间寻找最优平衡点
    4. 科学配置:基于数学模型的客观资产配置方法
  • CAPM改进

    1. 系统性风险:通过Beta系数量化资产与市场的关联性
    2. 理论支撑:结合CAPM定价理论提高预期收益估计的准确性
    3. 风险溢价:考虑市场风险溢价对预期收益的影响
    4. 市场基准:引入市场指数提供更客观的收益预测基准

1.3 How to CAPM优化的现代投资组合理论

  • 参数集设置

    1. ts.set_token:设置Tushare的API访问令牌
    2. industry:选择目标行业,如"银行"
    3. end_date:回测结束日期,格式为’YYYYMMDD’
    4. years:回测年限,默认5年
    5. risk_free_rate:无风险利率,默认0.03
    6. top_holdings:投资组合持仓数量,默认10只股票
    7. index_code:市场指数代码,默认’000300.SH’
  • 数据准备

    1. 股票行业数据:通过tushare获取指定行业的股票列表
    2. 历史价格数据:获取指定时间段内的股票日线数据
    3. 市场指数数据:获取指定时间段内的市场指数数据
    4. 无风险利率:设定无风险利率参数
  • 计算流程

    1. 数据获取:获取股票和市场指数的历史数据
    2. 收益率计算:计算月度对数收益率
    3. Beta计算:计算每只股票相对于市场的Beta系数
    4. CAPM预期收益:使用CAPM模型计算预期收益率
    5. 组合优化:最大化夏普比率得到最优权重
    6. 持仓筛选:选取权重最大的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      # 持仓数量参数
index_code = '000300.SH'  # 市场指数代码参数

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

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

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 CAPM预期收益率计算

使用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

2.5 协方差矩阵计算

计算收益率的协方差矩阵,用于评估资产间的相关性和波动性。

def calculate_covariance_matrix(monthly_log_returns):
    return monthly_log_returns.cov()

2.6 投资组合表现计算

计算给定权重下投资组合的预期收益率和波动率。

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

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
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

4. 反思

4.1 不足之处

  1. 线性关系:假设收益与Beta呈线性关系
  2. 单一因子:仅考虑市场系统性风险

4.2 提升思路

  1. 多因子扩展:引入规模、价值等其他因子
  2. 非线性关系:考虑收益率与风险的非线性关系
  3. 条件CAPM:考虑市场状态的变化

5. 启后

本文章的MPT+CAMP所考虑的因子只有一个市场因子(Beta),即个股收益与市场指数(本文使用沪深300指数)走势呈线性相关。但是这种仅考虑个股历史收益,与大盘指数历史收益的思路,忽略了个股自身的指标问题。比如说如果个股突然财务恶化,但历史收益非常好,该资产组合仍会因为历史数据好看而继续给该个股较高的持仓占比,这是我们不希望看到的。因此,我们不仅仅要考虑个股历史走势与大盘指数走势,还要考虑个股自身财务指标。
由此引进,Fama-French三因子模型,对预期收益率进行进一步优化:

  • 优化,预期收益率的优化方案:,可参考下一篇文章:
    3. 马科维茨资产组合模型+Fama-French三因子优化方案(理论+Python实战)

  • 量化回测实现,可参考下一篇文章:
    2.1 对MPT+CAMP优化方案实现Backtrader量化回测(理论+Python实战)

你可能感兴趣的:(金融资产组合模型进化论,人工智能,大数据,金融,python,数据库,机器学习)