最近在整理和复习一些经典量化因子的时候,萌生了一个想法:
干脆开一个小系列,把这101个常见的量化因子,一篇篇拆开来聊一遍。
每篇文章,我都会选一个因子,从最原始的公式出发,带着大家一起看看它到底想表达什么,适合用在哪些场景,又有哪些地方值得改进或优化。
今天是这个系列的第一篇,就从 Alpha#1 开始。
公式长这样:
Alpha#1 = (rank(Ts_ArgMax(SignedPower(((returns < 0) ? stddev(returns, 20) : close), 2.), 5)) - 0.5)别急,看着复杂,实际上逻辑并不难。今天就来跟大家聊聊,这个Alpha#1到底想表达什么,它背后又隐含了怎样的交易思路。
一步步拆开来看
首先,从最外层看,整个因子的大体流程是这样的:
- 处理数据: 根据收益率情况,选取不同的指标(波动率或收盘价)。
- 加工数据: 进行平方运算,放大差异。
- 时间窗口内寻找极值: 在最近5天内找出最极端的一天。
- 排名归一化: 对所有股票进行横向排名,并稍微中心化结果。
下面详细讲讲每一块的逻辑。
1. returns < 0 ? stddev(returns, 20) : close
这一段是个简单的条件判断:
- 如果今天收益率是负的(跌了),就取过去20天收益率的标准差(也就是波动率)。
- 如果今天收益率是正的或0(没跌甚至涨了),就取今天的收盘价。
所以这个因子有一个很明显的思路:
下跌关注波动,上涨关注价格本身。
亏的时候看“抖动得厉不厉害”,赚的时候直接盯价格。
这种思路挺符合实际交易的:
如果一只股票在跌,而且波动剧烈,通常意味着风险加大;
如果在涨,价格越高,趋势可能越强。
2. SignedPower(..., 2.)
接着,把上面选出来的那个值平方,而且保留正负号。
平方的作用很简单:放大强信号,让差异更加明显。
而保留正负号,则避免丢失方向感(涨还是跌)。
这一步可以理解为:“强调那些真正特别的股票,不太关注中庸的走势”。
3. Ts_ArgMax(..., 5)
然后,在最近5天内,看这个加工后的值,在第几天达到最大。
举个例子,如果今天是周五,那么就看本周五天里面,哪一天表现最极端。
这个操作的核心思想是:
看谁最近最“出圈”。
不一定要求一直优秀,只要有那么一两天特别突出,也能上榜。
4. rank(...) - 0.5
最后,对所有股票的这个极值位置进行横向排名。
排名靠前的(意味着最近某天特别活跃或者极端的)股票得高分。
再减去0.5,相当于把分布平移到中心点附近,方便后续建模或者组合优化。
总结一下
这个Alpha#1实际上在做的事情很有趣:
- 下跌时关注风险(波动性)
- 上涨时关注趋势(价格本身)
- 强调最近5天内有没有特别极端的表现
- 把极端程度在股票之间排序,筛选出表现突出的标的
通俗说,就是:
“跌的时候看慌不慌,涨的时候看高不高;过去五天谁最猛,谁就优先。”
这样的因子在实际策略里可以搭配很多玩法,比如动量策略里的选股过滤器,或者作为多因子模型中的一环。
Alpha#1 因子计算代码
import pandas as pd
import numpy as np
def calc_alpha1(df):
"""
计算Alpha#1因子
输入: df,一个包含 'close' 和 'returns' 列的 DataFrame
输出: df,新增 'alpha1' 列
"""
df = df.copy()
# 计算过去20日收益率标准差
df['stddev_20'] = df['returns'].rolling(window=20, min_periods=1).std()
# 根据条件选择
condition = df['returns'] < 0
df['selected_value'] = np.where(condition, df['stddev_20'], df['close'])
# SignedPower(x, 2),即平方,同时保留符号
df['signed_power'] = np.sign(df['selected_value']) * (np.abs(df['selected_value']) ** 2)
# 计算最近5日内 signed_power 最大值出现在哪一天(0是今天,1是昨天...)
def ts_argmax(series):
return np.argmax(series[::-1])
df['ts_argmax_5'] = df['signed_power'].rolling(window=5, min_periods=1).apply(ts_argmax, raw=True)
# 在横截面上进行排名(每天对所有股票排名)
# 如果是单只股票的单一序列,这里演示简单版
# 如果是多股票的话,需要 groupby date 后 rank
# 简单处理,归一化到 [0,1] 区间
df['rank'] = df['ts_argmax_5'].rank(pct=True)
# 减去0.5,中心化
df['alpha1'] = df['rank'] - 0.5
return df
# 示例:假设你已经有一个DataFrame,包含了close和returns
# df = pd.DataFrame({
# 'close': [...],
# 'returns': [...],
# 'date': [...]
# })
# 直接调用
# result = calc_alpha1(df)
# print(result[['close', 'returns', 'alpha1']])
rolling(window=5) 这种滑动窗口默认是向前看的(不包含未来信息),所以适合真实的回测环境。另外,这里用了 np.argmax(series[::-1]) 是因为在rolling窗口里,pandas默认顺序是从老到新,我们倒过来看最近的日子。
如果是多只股票的panel数据(比如不同股票代码+不同日期),要在date那一列上分组排名,比如用:
df['rank'] = df.groupby('date')['ts_argmax_5'].rank(pct=True)
可能的改进方向
讲到这里,其实我们可以顺便思考一下:
这个Alpha#1,虽然逻辑清晰,但里面的一些细节参数,并不一定对所有市场、所有品种都最优。想要让它真正适配自己的交易策略,动手微调和探索是非常有必要的。
比如,可以从下面几个角度去改造和优化:
1. 调整参数细节
stddev(returns, 20)这里用了20天的标准差,作为衡量下跌时的波动。- ArgMax 5天窗口是找最近5天的局部极值。
但问题是,不同品种的波动周期差异很大。
比如短线热门股5天就风云变幻,而大盘蓝筹可能20天都波澜不惊。
可以尝试调成10天、30天、60天,甚至动态调整,看对整体因子效果有什么变化。
2. 换一种波动测量方式
目前默认是用标准差(stddev)来量波动,但其实还有很多其他选项,比如:
- ATR(Average True Range),它在金融交易里更能反映“真实的价格波动”。
- 收益率均方根(RMS),或者一些自定义的低延迟波动指标。
换一种波动的定义,可能让因子更敏感、更早捕捉到拐点。
3. 根据市值等属性分组优化
还有一个思路,就是不是一刀切用一个因子去套所有股票。
比如小盘股波动大、流动性差,大盘股波动小、趋势稳定——在同一个因子公式下,它们的行为模式可能完全不一样。
可以试试看:
- 把股票按市值、成交额、波动率做分组。
- 在不同组别里分别计算和测试因子效果。
有时候,单一因子在全部股票上效果一般,但在某一小群体里能发挥非常好的效果。