这是用户在 2024-6-6 13:49 为 https://zhuanlan.zhihu.com/p/701826617 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

机器学习模拟投资者预期分歧

11 人赞同了该文章
发布于 2024-06-05 20:50・IP 属地上海 ,编辑于 2024-06-05 20:54・IP 属地上海
目录
收起
引言
研究设计
数据样本
实证分析
A股复现
结语
参考文献

最近刷到一篇比较有趣的working paper——《Machine Forecast Disagreement》,用机器学习模型模拟市场投资者的预期分歧,进而预测股票的横截面收益。读了之后我挺兴奋的,立马给好兄弟安利了一波,但可惜被拒绝了。既然如此,那咱就自己PY吧,以下是这篇论文的解读以及在A股市场的简单复现。

引言

信念分歧(Belief disagreement)是驱动投资交易的重要因素,对分歧的深入洞察对于理解金融市场的动态行为至关重要。在理论层面,学者们致力于探究投资者间意见差异如何塑造市场价格和交易量。Miller(1977)提出,当投资者对股票价值存在不同看法,特别是在悲观投资者受限于卖空约束时,股票价格往往会出现向上偏差。然而,鉴于投资者信念的难以量化,关于信仰分歧的实证研究相对较少。后来,Diether等(2002)利用股票分析师的收益预测数据,对投资者信念的异质性进行了开创性的实证分析,研究发现,在个股的横截面上,分析师预测离散度(AFD)较高时,往往预示着较低的未来收益。然而,Johnson(2004)对Dietheret al.(2002)的结论提出了质疑,他认为AFD实际上可能更多地反映了企业特有的风险。

本研究的内容主要包括三个方面。首先,提出了一种在资产层面上衡量投资者信念分歧的新方法。鉴于投资者信念的分布难以直接观测,因此设计了一个统计代理作为解决方案。在这个框架中,每个投资者被视作一个独立的预测模型,他们的信念基于对未来回报的预测而形成。这些假设的投资者共享一组通用的预测信息,但各自以不同的方式利用这些信息。为了模拟这种信念分布,为每个投资者分配了一个机器学习模型,并在投资者之间引入了模型的随机变化,通过随机化的模型,能够捕捉了投资者可能存在的先验信念、信息摩擦和偏见分布的差异。值得一提的是,尽管模型中的所有投资者都具备高度的复杂性和技巧性——每个模型都能够灵活且非线性地使用预测数据集——但它们并非完美无缺。因为现实中,没有哪个投资者能拥有完全准确的模型,他们的模型都是对真实数据生成过程的近似模拟,且不尽相同。每个投资者在给定其模型特性的前提下,利用可得数据来估计模型参数并形成预测。基于这种对投资者信念的构建,将股票层面的分歧量化为投资者回报预测的离散程度,并将其命名为“机器预测分歧”(MFD)。MFD的一个显著优点是,通过回避直接、可靠地调查投资者信念的难题,能够应用于任何时间点的所有股票,而不会受限于分析师预测的样本,因此在数据覆盖面上远超先前的文献。

第二是发现了具有较高MFD(机器预测分歧)的股票的未来收益显著低于其他股票。价值加权(等加权)的多空组合收益每月1.14% (1.32%),newey-west t统计量为4.33(5.61)。与其他衡量投资者分歧的代理指标相比,如分析师预测离散度(AFD),MFD展现出与投资者分歧更高的相关性。研究还发现,MFD的横截面收益预测能力能够扩展到国际股票市场。

第三是深入探究了MFD alpha(机器预测分歧的alpha值)背后的经济基础。发现高MFD股票的过高定价在卖空成本较高的股票中尤为显著,在卖空限制更为严格的情况下,投资者信念的分歧会导致资产被高估。高散户持股股票组合的MFD alpha更加显著,即散户投资者在面临较大分歧时更容易出现过度乐观的偏差。为了提供更充分的证据,依据Stambaugh et al.(2015)的股票水平错误定价(MISP)定义,分析了MFD溢价与高MFD股票的错误定价有关。通过观察收益公告期间的股价反应,为MFD alpha由股票水平错误定价驱动提供了额外支持。对于套利限制更为严重的股票,MFD alpha的显著性更强。

研究设计

简单来说,就是假设市场上有很多个投资者,每个投资者会利用市场上的部分信息,不同的投资者会利用市场上的部分信息进行股票预测,且投资者的水平良莠不齐,既有自称“堪比巴菲特”的投资大牛,也有投资业绩比不过大猩猩的基金经理,更不缺被套的韭菜们。所有投资者对于股票的预期的不一致,这就是投资者分歧。所以每个g()可以代表一个投资者,不同的g()输入的特征因子不完全相同,代表使用了不同的信息。由于使用的信息不同,所以g()的预测值也不同,有的g()预测效果比较好,同时也有的g()预测效果比较差。这些g()的预测值,就代表了投资者的分歧。构建的分歧度量MFD遵循了以下具体步骤。首先,将投资者数量设定为K=100,不完全信息的维度则固定为76,即对于所有K =1,2......100,其信息维度dk均为76。接着,选择了随机森林回归模型,为了模型性能与计算效率的平衡,参考Gu et al. (2020)和 Bali et al.(2023),设定了最大树深度为3,集合中树的个数为2000,并且设置了特征和样本的抽样比例为0.05,横截面收益缩尾1%。

数据样本

使用Jensen et al.(2022)的数据集,这是一个公开的股票收益和特征数据集基础收益数据来自证券价格研究中心(CRSP),会计数据来自Compustat。将样本限制为在纽约证券交易所、美国证券交易所和纳斯达克交易的普通股。不包括金融和公用事业公司。为了减少小型和非流动性股票的影响,还排除了交易价格低于每股5美元的低价股票。为了预测收益,使用153个股票特征作为完整的信息集。对所有股票特征逐期进行横截面排序,并根据Kelly et al.(2019)、Gu et al.(2020)和Freyberger et al.(2020)的方法,将这些排序映射至[-1, 1]的标准化区间。样本时间跨度从1966年7月至2022年12月,利用10年滚动窗口来估计随机森林回归量。使用前一个月(t−1)的特征计算了第t个月的MFD。随后,对1976年8月至2022年12月期间进行了样本外横断面资产定价测试。

实证分析

MFD排序的投资组合的超额收益和阿尔法从十分位数1到十分位数10递减。卖空MFD最高的第10个百分位(十分位数10)的股票,买入MFD最低的第10个百分位(十分位数1)的股票的多空组合,每月的价值加权(等加权)平均回报率为1.14% (1.32%),t统计量为4.33(5.61),换算成年化回报率为13.68% (15.84%)。对于大多数因子模型,控制风险并不会改变MFD投资组合的收益差的大小和统计显著性。在十分位数的股票中,alpha在统计上显著为负且绝对值较大。

随后进行了Fama-MacBeth回归分析,控制了市场beta、规模、账面市值比、动量、营业盈利能力、资产增长、盈利意外、短期回报逆转、非流动性、换手率、特殊波动率和彩票需求等变量。结果表明,MFD与未来一个月的收益之间存在显著的负相关关系。

为了探究MFD收益的经济来源,作者进行了一系列的检验。

高MFD股票的过高定价在卖空成本高的股票中尤为明显,即在存在更严格的卖空限制的情况下,分歧会导致资产被高估。

高散户持股股票组合的alpha价差显著大于主要由机构投资者持有的股票组合的alpha,进一步支持了Miller(1977)的假设。

通过Stambaughet al.(2015)的股票水平错误定价(MISP)指标,发现高MFD股票的错误定价得分显著高于低MFD股票。

通过检验收益公告周围的股价反应,收益公告期间的收益预测超过非收益期间的收益预测,为MFD alpha由股票水平错误定价驱动的解释提供了额外的支持。

对于套利限制更严重的股票,MFD alpha显著更强,这与套利限制加剧了资产错误定价一致。


A股复现

笔者选择2005至2022年的研究区间,样本池为全部A股,剔除了ST、停牌、上市不满一年的样本,选取了95个股票因子。设置K=100,dk=50。选择的模型为LightGBM(主要是因为这个比较快)。同样基于滚动窗口法进行研究,训练集长度为12个月,测试集为1个月(只是做个简单的研究,没有设置验证集调整超参数)。

# -*- coding: utf-8 -*-

import os
import numpy as np
import pandas as pd
import random
random.seed(42)
import warnings
warnings.filterwarnings("ignore")
import lightgbm as lgb

path = r'D:'
os.chdir(path)

# %% 主要代码

# 定义一个函数来随机选择50列
def extract_random_columns(dataframe, num_cols, num_iterations):
    # dataframe = ch.copy()
    all_columns = dataframe.columns.tolist()[3:]
    random_column_lists = []
    
    for _ in range(num_iterations):
        # 随机选择50列,确保每次选择不重复
        selected_columns = random.sample(all_columns, num_cols)
        random_column_lists.append(selected_columns)
    
    return random_column_lists

def calculate_mdf(ch,random_column_selections,trw,viw,tew,models='ols',param=None):
    # length为滑动窗口长度:取值{12}
    month_list = sorted(list(set(ch['month'])))
    
    mfd_temp = []

    for i in range(len(month_list)-trw-viw):
        # i = 0
        print(i)
        # month = month_list[i]
        month_train_start = month_list[i]
        month_train_end = month_list[i+trw-1]
        month_valid_start = month_list[i+trw]
        month_valid_end = month_list[i+trw+viw-1]        
        month_test = month_list[i+trw+viw]
        
        
        train_data = ch[(ch['month']>=month_train_start)&(ch['month']<=month_train_end)].iloc[:,2:]
        valid_data = ch[(ch['month']>=month_valid_start)&(ch['month']<=month_valid_end)].iloc[:,2:]
        test_data = ch[ch['month']==month_test].iloc[:,2:]
        
        stk_pool = ch[ch['month']==month_test]['stkcd'].to_list()
        
        predict = []

        for j in range(len(random_column_selections)):
            # j=0
            select_feature = random_column_selections[j]
        
            x_train = np.array(train_data[select_feature]) # 训练集 输入
            y_train = np.array(train_data['ret'])  # 训练集 输出
            
            x_valid = np.array(valid_data[select_feature]) # 验证集 输入
            y_valid = np.array(valid_data['ret'])  # 验证集 输出        
            
            x_test = np.array(test_data[select_feature])  # 测试集 输入
   
            lgb_train = lgb.Dataset(x_train, y_train) # 将数据保存到LightGBM二进制文件将使加载更快
            #lgb_eval = lgb.Dataset(x_test, y_test, reference=lgb_train)  # 创建验证数据
            
            params = {
                'task': 'train',
                'boosting_type': 'gbdt',  # 设置提升类型
                'objective': 'regression',  # 目标函数
                'metric':  'rmse',  # 评估函数#auc,mape,rmse,mae
                'n_jobs': 14,
                #'subsample': 0.5,
                #'subsample_freq': 1,
                'num_leaves': 31,   # 叶子节点数
                'learning_rate': param['learning_rate'],  # 学习速率
                'feature_fraction': 0.9,  # 建树的特征选择比例
                'bagging_fraction': 0.8,  # 建树的样本采样比例
                'bagging_freq': 5,  # k 意味着每 k 次迭代执行bagging
                'n_estimators': param['n_estimators'],  # 基学习器个数
                'verbose': -1,  # <0 显示致命的, =0 显示错误 (警告), >0 显示信息
            } #这里目前还没有验证集,之后可以考虑加入验证集进行训练
        
            model = lgb.train(params,lgb_train,verbose_eval = 5)
            y_pred = model.predict(x_test)

            predict.append(y_pred)
        
        y_pred = pd.DataFrame(predict, columns=stk_pool)
        y_pred_std = 1/y_pred.std(axis=0)

        mfd_temp.append(y_pred_std)
        
    mfd_df = pd.concat(mfd_temp, axis=1).T
    mfd_df.index = month_list[trw+viw:]

    return mfd_df

# %% 启动程序
if __name__ == '__main__':
    
    start_month = 200501 #回测开始年份
    end_month = 202212 #回测结束年份
    

    # 读取数据
    ch = pd.read_parquet(os.path.join(path, '过程数据', 'factor_ret.parquet'