1. 前言最近要工作了,工作好难找啊。看到好多要求要有机器学习这方面的经验的,虽然我对传统因子模型这块做了很多工作,但是机器学习却没怎么接触。
从我自身的理解来看,在股票的多因子模型上,机器学习或者深度学习的模型依然较难打败传统的线性加权的方法,而且也存在动量的特征,当某类长期稳健的底层因子突然失效,机器学习得到的Alpha也难以规避这样的风险。本身Alpha因子与收益之间的线性关系就比较好,另一方面很多Alpha模型是低频率的,样本数据不够也可能导致一些机器学习的非线性模型样本外表现不佳。现在很多私募都是大量的堆技术因子,或者将一些模型用在高频数据上。这样可能会好一点,但不管是技术因子还是高频数据,都会给模型增加了交易的成本,我虽然没研究过这块,但总感觉有点粗暴,甚至了解的一家私募雇中科院的实习生写因子,三年积累了快1万个。
现在不少基本面因子其实和股票的收益之间的线性关系并不好,例如它可能是一个倒U形的关系。如果我们能用机器学习的方法建立一个模型,捕捉到因子和股票收益之间的这种非线性关系,那就非常具有意义,其实简单来讲就是一个非线性的变换。尽管这样很难,一方面,这种关系可能并不是稳定,同时也可能不具有普适性。
但是既然是为了找工作,所以还是要学点这方面的知识,所以尝试建立一个机器学习来进行因子合成的框架,虽然最终结果未必要好于线性加权的方式,旨在学习交流为主。
2. 准备工作限于硬件水平,只有一台老破笔记本,所以没有选择太多的特征,选择了如下这些因子:
bp_lr, current_ratio, debt_2_equity, ep_ttm, eps_basic_ttm, gross_margin_ttm, illiq_1m, illiq_3m, ncf_operate_a_lr_yoy, ncf_operate_a_ttm_yoy, ncfp_ttm, net_profit_lr_yoy, net_profit_margin_ttm, net_profit_sq_zscore, net_profit_ttm_yoy, ret_5d, revenue_lr_yoy, revenue_sq_yoy, revenue_sq_zscore, revenue_ttm_yoy, roa_sq, roa_ttm_avg, roe_sq, roe_ttm_avg, sp_ttm, sue, sur, turnover_1m, turnover_3m, vol_20d毫不犹豫的贡献了自己因子库的一部分,这些因子在卖方的报告当中都有,所以我这里不再赘述它们的含义,基本上包括了估值,成长,动量,质量,规模,波动这几类。
我们需要对这些因子做去极值,标准化处理,由于机器运算能力的限制,我们先不做中性化。
因子缺失数据需要进行填充,这里也先最简单的方式,用一个固定数0来处理。
import pandas as pd
import sys
sys.path.append('E:\Heat')
from acrab.utils.factor_function import FactorUtil
from acrab.utils.data_function import DataAPI
from acrab.utils.config import factor_object
2.1 因子数据
因子计算和生产已经在本地做好了,这里只是调用了我自己写的一些函数来读取这些数据
start, end = '20091201', '20191231'
month_end_list = DataAPI.get_date_list(start, end, 'm')
all_factor = list()
for date in month_end_list:
month_factor = dict()
for name in factor_object.keys():
date_factor = DataAPI.get_factor(name, start=date, end=date, freq='d').set_index('ticker')[name]
date_factor = date_factor[~date_factor.index.str.startswith('68')] # 剔除科创板
date_factor = FactorUtil.winsorize(date_factor, win_type='percentile', p_value=(0.01, 0.01))
date_factor = FactorUtil.winsorize(date_factor, win_type='mad', limits=(-3, 3))
date_factor = FactorUtil.standardize(date_factor, method=1, date=date, estu='90cap')
month_factor[name] = date_factor
month_factor = pd.DataFrame(month_factor)
month_factor['trade_date'] = date
all_factor.append(month_factor)
E:\anaconda\lib\importlib\_bootstrap.py:219: RuntimeWarning: numpy.ufunc size changed, may indicate binary incompatibility. Expected 216, got 192
return f(*args, **kwds)
all_factor = pd.concat(all_factor, axis=0)
all_factor.head()
2.2 股票收益数据
由于我们是预测一个月的股票收益,所以我们先获得每个月月末的复权价格,然后再转换成收益率
price = DataAPI.get_stock_quote(start, end, ['closePrice'])
price = price.pivot(index='trade_date', columns='ticker', values='closePrice')
price = price.reindex(month_end_list)
forward_1m_ret = price.pct_change().shift(-1)
2.3 训练集的构造为了避免噪声的影响,我们在构建训练集的时候并不会拿所有股票进行训练,这里我们选择每个月在市场上涨幅排名前20%的股票和跌幅排名后20%的股票作为训练集
虽然我们的目标也不是为了预测股票,但这很难。模型即使训练出来效果肯定是一团糟的,所以我们改为预测涨跌,但最终的Alpha取的是预测涨跌的概率
train_set = list()
for date in month_end_list:
month_ret = forward_1m_ret.loc[date].dropna()
month_factor = all_factor[all_factor['trade_date'] == date]
upper = month_ret[month_ret >= month_ret.quantile(0.8)]
lower = month_ret[month_ret <= month_ret.quantile(0.2)]
label = pd.Series(1, index=upper.index).append(pd.Series(-1, index=lower.index))
month_train_set = pd.concat((label.to_frame('label'), month_factor.reindex(label.index).fillna(0)), axis=1)
train_set.append(month_train_set)
train_set = pd.concat(train_set, axis=0)
train_set.head()
可以看到,这里增加了label列,即为我们前面所定义的标签
3. 模型选择我们选择xgboost模型来进行因子合成
使用过去一年的样本进行滚动训练
交叉验证方式采用K折交叉验证,交叉验证在每年年初进行,这块对我老破笔记本来说确实太耗时了,所以把网格寻参的范围缩小一些
from sklearn.model_selection import KFold, GridSearchCV
from xgboost import XGBClassifier
para_set = {
'learning_rate': [0.025, 0.05],
'max_depth': [6, 12],
'sub_sample': [0.8, 0.9]
}
signal = dict()
for i, date in enumerate(month_end_list[13:]):
# 1. 由于我们的标签是未来一个月的收益,所以我们训练集要往前漂移一期
train = train_set[(train_set['trade_date'] >= month_end_list[i]) & (train_set['trade_date'] <= month_end_list[i+12])]
predict = all_factor[all_factor['trade_date'] == date]
train_x = train[list(factor_object.keys())]
train_y = train['label']
predict_x = predict[list(factor_object.keys())]
if date[-4:-2] == '01':
init_model = XGBClassifier(random_state=10, njobs=2)
cv_method = KFold(n_splits=4, shuffle=True, random_state=10)
grid = [para_set]
cv = GridSearchCV(init_model, grid, scoring='roc_auc', cv=cv_method, verbose=10)
cv_result = cv.fit(train_x, train_y)
init_best_model = XGBClassifier(learning_rate=cv_result.best_params_['learning_rate'],
max_depth=cv_result.best_params_['max_depth'],
sub_sample=cv_result.best_params_['sub_sample'],
random_state=10)
best_model = init_best_model.fit(train_x, train_y)
date_signal = pd.Series(best_model.predict_proba(predict_x.fillna(0))[:, 1] - 0.5, index=predict_x.index)
signal[date] = date_signal
signal = pd.DataFrame(signal).T
signal = signal.unstack().to_frame('xgb_alpha').dropna().reset_index()
signal.columns = ['ticker', 'trade_date', 'xgb_alpha']
signal.to_csv(r'./xgb_alpha.csv', index=False)
4. 因子分析
将合成的因子进行中性化后再进行因子分析,顺便展示下最近开发的单因子分析框架,功能还不够完全,部分结果如下:机器学习的因子会自动进行方向上的选择,整体来看最终合成的因子的分布也接近正态,同时也能看到,共107期的测试,92期Alpha的IC都为正,展现了不错的稳定性
由于进行了填0处理,所以因子的覆盖度水平较好
从与风格一结果上来看,如果我们一股脑把因子都丢到模型里面,那么难以避免的模型会把很高的权重集中在某类因子上,像这里,就是流动性因子
从本文展示的内容来说,将机器学习应用在因子合成上,仍然有很多问题需要解决