量化小白也能自动化挖掘出6万+因子

最近逛某乎,碰到了这个问题:如何看待量化交易WorldQuant世坤大赛北大牛人提交了6万+因子?

我的第一直觉,这肯定不是纯手工挖出来的,6w个因子,一天挖一个,节假日都不休息的话,需要164年,一个小时挖一个的话,也需要7年,一分钟一个,需要42天。

这明显不是一个纯人工手工的速度,麒麟臂也不行,肯定是有机器参与了,除非这个北大学子也是个少爷,雇了100来人,每个人搞来几百个因子就行了。

如果可以用机器,只注重因子数量的话,短时间内挖6w+因子也不是什么难事,量化小白也可以做到,本来这方法是我之前给PM支招对付恶毒私募那种3周要挖4000因子的要求,核心代码只有4行,虽然因子质量不一定能保证,但因子数量绝对是足斤足两的,我刚才试了一下,挖6w+因子,一共花了不到3个小时。

不藏着掖着,这个牛掰的量化因子自动挖掘神器就是tsfresh**,只要你基础因子和电脑配置足够,真的是想挖多少就挖多少。**

tsfresh实际上是一个python库,在电脑终端使用"pip install tsfresh"就可以自动安装,它最主要的功能就是能对证券价格这类型的时间序列数据自动挖掘其中的特征,并且还可以对有效特征进行筛选,详细说明请见官网:https://tsfresh.com/。

特征(feature)一般是在人工智能/机器学习/深度学习领域的叫法,在量化交易/量化投资领域一般叫因子(factor),例如相对强弱指标RSI在机器学习领域就叫RSI特征,在量化选股领域就叫RSI因子,其实都是一样的东西,大伙儿注意分辨语境就好了。

废话不多说,咱可以先来快速体验一下挖6w+因子,详细原理下文再细说,在成功安装tsfresh库后,只需要以下4行核心代码(第5~8行),便可以将原有的84个基础因子,扩展到65772个因子,耗时9730秒,约2.7小时。

python 复制代码
import datetime
start_time = datetime.datetime.now() #开始时间

# 核心代码部分
from tsfresh import extract_features
from tsfresh.utilities.dataframe_functions import roll_time_series
data_roll = roll_time_series(data, column_id='code', column_sort='date', max_timeshift=20, min_timeshift=5).drop(columns=['code'])
data_feat = extract_features(data_roll, column_id='id', column_sort='date') 

end_time = datetime.datetime.now() #结束时间
print('开始时间:', start_time.strftime('%Y-%m-%d %H:%M:%S'))
print('结束时间:', end_time.strftime('%Y-%m-%d %H:%M:%S'))
print('耗时:%d 秒' %(end_time-start_time).seconds)
print('原始数据维度:', data.shape)
print('特征提取后维度:', data_feat.shape)

输出结果:

python 复制代码
开始时间: 2024-07-30 23:10:01
结束时间: 2024-07-31 01:52:11
耗时:9730 秒
原始数据维度: (4651, 86)
特征提取后维度: (4646, 65772)

补充说明一下,其中输入数据data是预先生成的数据(下文会说明获取方式),第1列是日期(date),第2列是证券代码(code),之后的84列都是因子列;特征提取之后行数少了5行,是因为设置了前面至少要有5行数据才开始提取当日特征;耗时大小跟电脑配置有关,本次试验所用的电脑处理器是第12代i7(主频2.10 GHz),电脑内存大小是32G。

咱不能是为了提取因子而提取因子,必定是用来做些什么的吧,那咱就可以沿着之前机器学习指数量化择时的策略思路,构建一个因子增强版的策略,整体策略思路详见之前的文章《手把手教你,利用机器学习模型,构建量化择时策略》

整个建模流程分为收集数据、准备数据、选择/建立模型、训练模型、测试模型和调节参数这6大步骤,这次咱主要是利用tsfresh库生成更多的有效因子进行建模,因此只需改变收集数据和准备数据这2个步骤,后面4个步骤与之前的文章基本一致,无须大变。

由于一次性同时挖掘6w+因子耗时就不说了,还非常吃内存,我自己这台32G内存的老机子都差点儿扛不下来,因此为了照顾到更多的小伙伴能跑通程序,建模的时候使用的是8k+因子,但同时也会说明如何升级到6w+因子。

Step1:收集数据

现在开始第1个步骤,咱就来获取沪深300指数从2005年4月8日上市以来至今的全部行情数据,这里使用的是股票量化开源库qstock,"pip install qstock"安装后,基本的功能无需注册便可以使用,对萌新来说非常方便,详情请见:https://github.com/tkfy920/qstock

python 复制代码
import qstock as qs

# 获取沪深300指数高开低收、成交量、成交金额、换手率数据,index是日期
data = qs.get_data(code_list=['HS300'], start='20050408', end='20240730', freq='d')
# 删除名称列、排序并去除空值
data = data.drop(columns=['name']).sort_index().fillna(method='ffill').dropna()
# 插入日期列
data.insert(0, 'date', data.index)
# 将日期从datetime格式转换为str格式
data['date'] = data['date'].apply(lambda x: x.strftime('%Y-%m-%d'))
data = data.reset_index(drop=True)

print(data.shape)
data.tail(10)

这里使用qstock获取数据只是举例,大家使用相同功能的库或API都可以,要注意的是不要带名称列,而且日期列是str格式,不是datetime格式。

Step2:准备数据(本期重点)

第2个步骤就是准备数据,生成/选取哪些特征/因子作为将来SVM模型的输入,在这里咱额外计算的因子是价格斜率(slope)、相对强弱指标(rsi)、威廉指标值(wr)、MACD快线(dif)、MACD慢线(dea)、MACD柱(macd)和抛物线指标(sar)这7个因子,再加上之前的收盘价、成交量、成交金额和换手率,一共是11个因子。

python 复制代码
import talib

# 收盘价的斜率
data['slope'] = talib.LINEARREG_SLOPE(data['close'].values, timeperiod=5)
# 相对强弱指标
data['rsi'] = talib.RSI(data['close'].values, timeperiod = 14)
# 威廉指标值
data['wr'] = talib.WILLR(data['high'].values, data['low'].values, data['close'].values, timeperiod=7)
# MACD中的DIF、DEA和MACD柱
data['dif'], data['dea'], data['macd'] = talib.MACD(data['close'].values, fastperiod=12, slowperiod=26, signalperiod=9)
# 抛物线指标
data['sar'] = talib.SAR(data['high'].values, data['low'].values)
# 删除开盘价、最高价和最低价
data = data.drop(columns=['open','high','low']).fillna(method='ffill').dropna().reset_index(drop=True)

print(data.shape)
data.tail(10)

注意这里的行数比之前的少了33(4694->4661),那是因为在计算MACD指标时,底层需要计算EMA26并且在这基础上还要计算EMA9,期初的数据由于长度不足,指标是空值被剔除了,(26-1)+(9-1)=33,下面出现行数减少的原因也是同样的原理,不再赘述。

重点来了,在这里咱要基于这11个基础因子,生成8k+个因子,这里就要先说明一下tsfresh的提取特征原理了。

tsfresh是对序列数据提取特征,输入一个时间序列数据就会转换为一个数值,例如输入一段时间的股价序列,那么可以转化成最大值、最小值、平均值、中位数、波峰数这样的标量数值。

如果不做特殊处理,直接把沪深300因子数据输入进去,那么它只会把每个因子序列数据转化为单个值,这样一个4661x13的因子矩阵,最终就会变为1x13的因子向量,相当于只是一个样本,这样是无法建模的。

咱需要的是每个交易日都要有一个样本,这就需要用到tsfresh的滚动(roll)技术了,请看下面的变换。

python 复制代码
from tsfresh.utilities.dataframe_functions import roll_time_series

data_roll = roll_time_series(data, column_id='code', column_sort='date', max_timeshift=20, min_timeshift=5).drop(columns=['code'])

print(data_roll.shape)
data_roll.head(15)

在这个变换中,新生成了一个id列,数值是一个元组,元素1是证券代码,元素2是日期,红框中id日期表明,2005-06-08的因子计算需要用到2005-06-01、2005-06-02、2005-06-03、2005-06-06、2005-06-07和2005-06-08这6个数值组成的时间序列数据,用多长的序列数据,由参数max_timeshift和min_timeshift决定。

max_timeshift=n表示每个交易日最多 使用包含当日在内的n+1个数据点序列,min_timeshift=m表示每个交易日至少使用包含当日在内的m+1个数据点序列,这也就说明了数据id为什么从2005-06-08开始,因为之前至少需要5个数据点,再给大伙儿换一种方式展示就清楚更多了,如下。

python 复制代码
gg = data_roll.groupby('id').agg({'date':['count', min, max]})

print(gg.shape)
gg.head(20)

看到这里就应该明白了,数据长度在min_timeshift+1 ~ max_timeshift+1之间时,有多少数据用多少数据,当超出这个范围后,就每个交易日滚动(roll)使用max_timeshift+1个数据点的序列数据,因此原始因子数量不变,但行数被扩展为9万多行,因为部分数据被重复滚动使用。

在data_roll之上,就可以使用tsfresh的extract_features函数在基础因子之上衍生出众多因子,这一步较为耗时,不同电脑配置执行时间不一样,须耐心等待。

python 复制代码
from tsfresh import extract_features

data_feat = extract_features(data_roll, column_id='id', column_sort='date')
# 对单独标的而言,将日期作为index
data_feat.index = [v[1] for v in data_feat.index] 

print(data_feat.shape)

输出结果:

python 复制代码
Feature Extraction: 100%|██████████████████████████████████████████████████████████████| 50/50 [14:50<00:00, 17.81s/it]
(4656, 8613)

从结果中看出,最终得到了4656x8613的因子矩阵,4656对应着交易日数,8613对应着在11个基础因子之上衍生出来的因子数。

因子生成的原理是,tsfresh预置了783个算子(calculator),会逐个用在每一个交易日的基础因子数值序列上,这就相当于孙悟空拔一根猴毛下来,能变出783个形态各异的化身,相同计算公式不同单参数也算1个,11乘以783就是8613,具体的算子计算公式请见官方文档:

https://tsfresh.readthedocs.io/en/latest/text/list_of_features.html

看到这里,挖出6w+因子的原理就很明显了,60000 / 783 ≈ 77,只需要有77个基础因子,就能衍生出6w个因子,只需要将之前生成11个基础因子的部分,替换为以下代码,就可以生成84个基础因子,能衍生出65772个因子,开头2.7小时挖6w+因子的试验就是用这个方法。

python 复制代码
import talib

for period in range(5,15):
    # 收盘价的斜率
    data[f'slope{period}'] = talib.LINEARREG_SLOPE(data['close'].values, timeperiod=period)
    # 相对强弱指标
    data[f'rsi{period}'] = talib.RSI(data['close'].values, timeperiod=period)
    # 威廉指标值
    data[f'wr{period}'] = talib.WILLR(data['high'].values, data['low'].values, data['close'].values, timeperiod=period)
    # 均幅指标
    data[f'atr{period}'] = talib.ATR(data['high'].values, data['low'].values, data['close'].values, timeperiod=period)
    # 平均趋向指标
    data[f'adx{period}']  = talib.ADX(data['high'].values, data['low'].values, data['close'].values, timeperiod=period)
for fperiod,speriod,mperiod in zip(range(8,18),range(22,32),range(5,15)):
    # MACD中的DIF、DEA和MACD柱
    data[f'dif{fperiod}'], data[f'dea{fperiod}'], data[f'macd{fperiod}'] = talib.MACD(data['close'].values, fastperiod=fperiod, slowperiod=speriod, signalperiod=mperiod)

# 删除开盘价、最高价和最低价
data = data.drop(columns=['open','high','low']).fillna(method='ffill').dropna().reset_index(drop=True)

print(data.shape)
data.tail(10)

大致的原理是,使用价格斜率(slope)、相对强弱指标(rsi)、威廉指标值(wr)、均幅指标(atr)、平均趋向指标(adx)、MACD快线(dif)、MACD慢线(dea)和MACD柱(macd)这8个指标,每个指标都分别使用10个不同的参数,那就是80个不同的因子,再加上之前的收盘价、成交量、成交金额和换手率,一共是84个因子。这是一种偷懒的写法,大伙儿别学我,根据自己的实际需要加入基础因子哈

说回正题,这8613维里面都是衍生出来的因子,是不包含原始的基础因子的,记得也把基础因子添加回去。跟以前的做法一样,也要给每个交易日的数据打上标签,每个样本标签对应的是第二个交易日的涨跌情况,计算出每个样本第二天的涨幅(pct),如果第二天上涨,则设置标签(rise)为1,反之为0。

python 复制代码
import pandas as pd

# 将原始因子加入因子矩阵当中
data_feat = pd.merge(data_feat, data.set_index('date', drop=True).drop(columns=['code']), 
                     how='left', left_index=True, right_index=True)

# 给数据打标签
data_feat['pct'] = data_feat['close'].shift(-1) / data_feat['close'] - 1.0
data_feat['rise'] = data_feat['pct'].apply(lambda x: 1 if x>0 else 0)
data_feat = data_feat.dropna(subset=['pct'])

print(data_feat.shape)

Step3: 选择/建立模型

还是跟之前一样,使用SVM模型,原因和原理不再赘述,详见之前的文章《手把手教你,利用机器学习模型,构建量化择时策略》,为了方(tou)便(lan)实现和建立模型,咱还是直接从Scikit-learn(简称sklearn)中导入,它是非常流行的Python免费机器学习库,具有各种分类、回归和聚类算法,一般配合numpy数据格式使用。

Step4: 训练模型

在这里,咱需要把整个数据集分拆为训练集和测试集,因为除了训练模型之外,咱还要留出一部分数据来验证训练出来模型的优劣。

一般来说,将完整数据集80%的样本作为训练集,剩余20%的样本作为测试集,要注意的是,在这里使用tsfresh的select_features函数对之前的8000+因子进行有效因子筛选,最终有效入选因子有200多个,这个只能在训练集上操作,不能在全部数据集上操作,否则就会变相引入"未来函数"。

python 复制代码
from tsfresh import select_features

# 划分训练集和测试集
num_train = round(len(data_feat)*0.8)
data_train = data_feat.iloc[:num_train, :]
y_train = data_feat.iloc[:num_train, :]['rise']
data_test = data_feat.iloc[num_train:, :]
y_test = data_feat.iloc[num_train:, :]['rise']

# 特征选择
data_train0 = select_features(data_train.drop(columns=['pct','rise']).dropna(axis=1, how='any'), y_train)
select_columns = list(data_train0.columns) + ['pct','rise']
data_train = data_train[select_columns]
data_test = data_test[select_columns]

print(data_train.shape)

输出结果:

python 复制代码
(3724, 242)

接下来,按照老方法将因子数据标准化后,用于模型训练,从sklearn的svm模块当中导入SVM分类器SVC,创建实例对象后,将训练集因子数据和对应标签塞进fit函数就行了,SVM模型的惩罚系数使用默认值1.0,核函数也用默认的RBF核函数。

python 复制代码
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler

# 转化为numpy的ndarray数组格式
X_train = data_train.drop(columns=['pct','rise']).values
X_test = data_test.drop(columns=['pct','rise']).values

# 对数据进行标准化
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# 训练模型
classifier = SVC(C=1.0, kernel='rbf')
classifier.fit(X_train, y_train)

Step5:测试模型

至此,SVM分类器已经训练好了,把因子数据塞进predict函数,就能输出每个样本的预测值,咱分别把训练集和测试集的预测标签插回到原来的数据集当中,用来计算预测的准确率。

python 复制代码
y_train_pred = classifier.predict(X_train)
y_test_pred = classifier.predict(X_test)
data_train['pred'] = y_train_pred
data_test['pred'] = y_test_pred
accuracy_train = 100 * data_train[data_train.rise==data_train.pred].shape[0] / data_train.shape[0]
accuracy_test = 100 * data_test[data_test.rise==data_test.pred].shape[0] / data_test.shape[0]
print('训练集预测准确率:%.2f%%' %accuracy_train)
print('测试集预测准确率:%.2f%%' %accuracy_test)

输出结果:

python 复制代码
训练集预测准确率:65.01%
测试集预测准确率:52.42%

光看准确率还是不够直观,咱还要看一下如果纯粹按照这个择时模型的预测结果进行投资,能获得多少收益。假设指数可以多空交易,如果模型预测为1(上涨),第二天策略的收益率就是指数的涨幅,如果模型预测为0(下跌),第二天策略的收益率就是指数的涨幅的相反数,有了每天的日收益率之后,通过dataframe自带的累乘函数cumprod,就可以得到择时策略和沪深300指数的净值曲线,为了方(tou)便(lan)起见,不考虑交易费率,以及按照收盘价成交。

python 复制代码
import matplotlib.pyplot as plt

#策略日收益率
data_test['strategy_pct'] = data_test.apply(lambda x: x.pct if x.pred>0 else -x.pct, axis=1)

#策略和沪深300的净值
data_test['strategy'] = (1.0 + data_test['strategy_pct']).cumprod()
data_test['hs300'] = (1.0 + data_test['pct']).cumprod()

# 粗略计算年化收益率
annual_return = 100 * (pow(data_test['strategy'].iloc[-1], 250/data_test.shape[0]) - 1.0)
print('SVM 沪深300指数择时策略的年化收益率:%.2f%%' %annual_return)

#将索引从字符串转换为日期格式,方便展示
data_test.index = pd.to_datetime(data_test.index)
ax = data_test[['strategy','hs300']].plot(figsize=(16,9), color=['SteelBlue','Red'],
                                          title='SVM 沪深300指数择时策略净值  by 公众号【量化君也】')
plt.show()

Step6:调节参数

如果你觉得预测准确率和策略收益还达不到预期,可以对每一个步骤的参数进行调整改进。

就好比这次建模,跟原始版SVM择时模型比起来,最大的不同点就是使用了不同的基础因子,还在基础因子上提取了众多的衍生因子,并且进行了有效因子筛选,训练集预测准确率从之前的57.23%提升到了65.01%,测试集预测准确率从48.34%提升到了52.42%,策略年化收益也从-5.98%提高到了18.34%。

原始版SVM择时模型测试集的策略净值

这就是改进了"因子参数"的结果,你还可以调整SVM模型参数、因子标准化方法、因子中性化/正交化方法等等。

~~~~~~建模结束分割线~~~~~~

荀子的《劝学》中有云:君子性非异也,善假于物也。有些事情看上去很牛掰,也许只是善于利用工具的缘故罢了,你会用了,你也可以很牛掰,10w+因子都不在话下。

相关推荐
疯狂飙车的蜗牛几秒前
从零玩转CanMV-K230(4)-小核Linux驱动开发参考
linux·运维·驱动开发
ZHOU_WUYI24 分钟前
4.metagpt中的软件公司智能体 (ProjectManager 角色)
人工智能·metagpt
恩爸编程1 小时前
探索 Nginx:Web 世界的幕后英雄
运维·nginx·nginx反向代理·nginx是什么·nginx静态资源服务器·nginx服务器·nginx解决哪些问题
靴子学长1 小时前
基于字节大模型的论文翻译(含免费源码)
人工智能·深度学习·nlp
梧桐树04292 小时前
python常用内建模块:collections
python
AI_NEW_COME2 小时前
知识库管理系统可扩展性深度测评
人工智能
Michaelwubo2 小时前
Docker dockerfile镜像编码 centos7
运维·docker·容器
Dream_Snowar2 小时前
速通Python 第三节
开发语言·python
海棠AI实验室2 小时前
AI的进阶之路:从机器学习到深度学习的演变(一)
人工智能·深度学习·机器学习
hunteritself2 小时前
AI Weekly『12月16-22日』:OpenAI公布o3,谷歌发布首个推理模型,GitHub Copilot免费版上线!
人工智能·gpt·chatgpt·github·openai·copilot