1.1 一些思考
- 世坤的alpha101给大家展示了因子如何变成表达式,那怎么快速生成表达式是一个问题
- 普通的量价因子和基本面因子效果一般,能不能通过遗传的方法让他们产生新的因子;
1.2 遗传算法基础
-
背景概念
遗传规划是一种启发式的公式演化技术,通过模拟自然界中遗传进化的过程来逐渐生成契合特定目标的公式群体,适合进行特征工程。将遗传规划运用于选股因子挖掘时,可以充分利用计算机的强大算力,同时突破人类的思维局限,挖掘出某些隐藏的、难以通过人脑构建的因子。在量化多因子选股里,大家最关心的一直就是“因子”怎么找。以前的研究方法,一般是先根据市场上的规律和投资经验想出一个逻辑,再把它写成公式,像常见的估值、成长性、财务质量、波动率这些因子,基本都是这么来的。但现在数据越来越多,加上机器学习等新技术,我们可以用“遗传规划”这种方法,在海量数据里让计算机自己去“进化”出一些有效的因子,再回过头来解释这些因子为什么有用。也就是说,过去是“先有逻辑,再有公式”;而现在也可以“先有公式,再找逻辑”。这两种思路,其实就是“演绎法”和“归纳法”。前一种靠人脑经验,后一种靠机器算力。后一种的优势是,它能突破人类思维的限制,从数据里挖出一些人想不到的因子,给因子研究带来更多新可能。 -
因子表达式
遗传规划在生成因子时,本质上就是让计算机自动组合各种数学算子和金融数据字段,形成一条条“公式候选”。这些公式就是因子表达式。
一个因子表达式通常是 输入+运算符+组合方式 的结果。
•输入:常见是 K 线数据(开盘价、收盘价、最高价、最低价、成交量、成交额)、财务指标(市盈率、市净率、ROE、现金流)、衍生指标(波动率、换手率、行业收益率等)。
•运算符:包括加、减、乘、除、log、sqrt、abs、rank、zscore、rolling_mean、rolling_std 等常用算子。
•组合方式:通过树状结构进行嵌套组合,比如 (log(volume) - mean(close, 10)) / std(close, 20)。
这样生成的因子表达式可能是:
•简单型:(close - open) / open (收盘价相对开盘价的涨幅)
•复杂型:rank((log(volume) * (close - low)) / (high - low))
•进化型:计算机经过多轮迭代,可能会进化出一些人类研究者想不到的复杂组合,比如 abs((rolling_mean(close, 5) - open) / sqrt(rolling_std(volume, 10)))。
遗传规划会不断筛选表现好的表达式(比如在回测中IC高、RankIC稳定),保留它们当作“父代”,再继续交叉、变异,生成新一代公式。经过多轮进化后,最终得到一批在历史数据中表现优异的因子表达式。这些表达式再进入后续的检验和解释阶段。
数据准备:
3.遗传算法整体流程
如下图所示,遗传算法主要是挑选一批强的父代用于进化,第一步是随机生成第一代的公式然后通过fitness最高的作为父代进行进化,适应度可以用rankic来进行,最终一直跌到到选出公式。
4.对传统GPlean的改造
扩充算子,也就是function_set
类型 | 名称 | 定义 |
---|---|---|
自定义 | rank(X) | 返回值为向量,其中第 i 个元素为𝑋𝑖在向量 X 中的分位数。 |
自定义 | delay(X, d) | 返回值为向量,d 天以前的 X 值。 |
自定义 | correlation(X, Y, d) | 返回值为向量,其中第 i 个元素为过去 d 天𝑋𝑖值构成的时序数列和𝑌𝑖值构成的时序数列的相关系数 |
自定义 | scale(X, a) | 返回值为向量 a*X/sum(abs(x)),a 的缺省值为 1,一般 a 应为正数。 |
def _delta(data):
value = np.diff(data.flatten())
value = np.append(0, value)
return value
def _sma(data):
window=10
value = np.array(pd.Series(data.flatten()).rolling(window).mean().tolist())
value = np.nan_to_num(value)
return value
def _stddev(data):
window=10
value = np.array(pd.Series(data.flatten()).rolling(window).std().tolist())
value = np.nan_to_num(value)
return value
stddev = make_function(function=_stddev, name='stddev', arity=1)
delta = make_function(function=_delta, name='delta', arity=1)
sma = make_function(function=_sma, name='sma', arity=1)
5.对fitness的自定义
由于GPlean只能做二维数据的学习,我们这种时间+标的+特征的模式,我们要做一次标记,对不同标的结束的时候做一个nan标记,在计算多空里面的时候根据这个标记还原数据进行分组计算,下面代码是多空计算代码。
def _my_metric_ud(y, y_pred, w):
if len(y_pred)<100:
return 0
for i in del_index:
try:
y_pred[i:i+window*5] = np.nan # 删除y
except:
break
df = pd.DataFrame({
'y_pred': y_pred,
'y': y
}, index=gp_factor_train_index)
y_pred = df['y_pred'].unstack('symbol').dropna(how = 'all')
y = df['y'].unstack('symbol')
y_pred_g = y_pred.apply(lambda x: pd.qcut(x, q=5, labels=False, duplicates="drop"),axis = 1)
group_top = y[y_pred_g == 0].mean(axis = 1)
group_bottom = y[y_pred_g == 4].mean(axis = 1)
group_up = (group_top - group_bottom)
group_up_sharpe = group_up.mean() / group_up.std() * np.sqrt(252)
if np.isnan(group_up_sharpe):
return 0
else:
print(f'\r {group_up_sharpe}',end = '')
return group_up_sharpe
my_metric_ud = make_fitness(function=_my_metric_ud, greater_is_better=True)
6.通过训练后会得到一堆的因子特征表达式,我们对要这些特征进行进行线性模型合成或者非线性合成,首先可以利用画图的方法看看因子是否样本内外都有效,例如下图:
训练是2023年到2024年,样本内网其实都比较有效,下一步就是做因子合成。用简单加权因子合成就行了,然后看一下最终的结果。做合成之前为了解决共线性问题,先把相关性卡掉,如下图:
最终加权合成得到以下效果:
其实也蛮好的,我们训练的数据比较普通,能有这个效果也还可以。