1. 揭秘原始马科维茨资产组合模型(理论+Python实战)

0. 承前

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

1. 现代投资组合理论(MPT)优化模型

1.1 What is 现代投资组合理论优化模型

现代投资组合理论(Modern Portfolio Theory, MPT)是一种通过优化资产配置来实现投资组合风险收益平衡的投资策略模型。该模型通过最大化夏普比率(Sharpe Ratio)来寻找最优的资产配置权重,以在给定风险水平下获得最大的超额收益。

1.2 Why is 现代投资组合理论优化模型

假设你是一个投资组合经理,现在收到任务是使用指定的资金,对10个资产进行投资,你是直接每个资产平分10%?还是使用诺奖得主的金融模型来构建自己的投资组合?当然是后者!
因为我们在投资过程中,隐约会觉得需要考虑风险&收益,但是该怎么考虑?马科维茨资产组合模型,就为你提供了一个科学的思维框架。
以下因素,会影响单个或多个资产在马科维茨资产组合的占比:

  • 收益率:单个资产的收益率,及其预期收益率,会正向影响该资产的占比;
  • 波动率:单个资产的波动率,会反向影响该资产的占比;
  • 相关性:多个资产的相关性,会反向影响多个资产的占比;

大白话:马科维茨资产组合模型是一种追求收益(收益率),规避风险(波动率、相关性)的金融模型。

  • 优点概括
    1. 科学量化:通过数学模型实现投资组合的科学配置
    2. 分散投资:考虑资产间相关性,有效降低非系统性风险
    3. 风险收益平衡:在收益与风险之间寻找最优平衡点
    4. 客观决策:减少主观判断带来的决策偏差

1.3 How to 现代投资组合理论优化模型

  • 金融工程流程图
算法求解
资产组合权重
最大化夏普比优化函数
序列最小二乘规划算法
数据计算
计算预期收益:
均值方差
计算月度对数收益率
计算风险:
协方差矩阵
数据获取
数据预处理
获取行业名单
获取交易数据
  • 参数集设置

    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只股票
  • 数据准备

    1. 股票行业数据:通过tushare获取指定行业的股票列表
    2. 历史价格数据:获取指定时间段内的股票日线数据
    3. 无风险利率:设定无风险利率参数
    4. 持仓限制:设定投资组合的最大持仓数量
  • 计算流程

    1. 数据获取:获取行业股票列表和历史价格数据
    2. 收益率计算:计算月度对数收益率
    3. 统计指标计算:计算预期收益率和协方差矩阵
    4. 权重优化:最大化夏普比率得到最优权重
    5. 持仓筛选:选取权重最大的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.SH0.35523481104625515
002948.SZ0.20587305181720306
600016.SH0.15635926106496267
601398.SH0.07370383829427572
601997.SH0.061066273596321566
601288.SH0.03667856492576135
600036.SH0.035718928637484534
600919.SH0.031285868893261924
601998.SH0.029297209174020455
601166.SH0.014782192550453486

4. 反思

4.1 不足之处

  1. 假设条件理想化:假设收益率服从正态分布
  2. 历史数据依赖:过度依赖历史数据预测未来

4.2 提升思路

  1. 引入CAPM模型:考虑系统性风险
  2. 动态调整:添加时间序列分析
  3. 多目标优化:引入更多风险指标

5. 启后

本文章的马科维茨资产组合的最优组合仅是该系列的一个开端,有经验的资产组合经理应该能够轻松看出其中的不合理之处,比如使用过去的收益均值作为预期收益率,有可能会把该资产组合制作成为“追高杀低”的组合策略。因此,之后我们将会基于当前代码以及思路,持续进行优化推进。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI量金术师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值