简介:这份PDF文档系统梳理了WorldQuant BRAIN平台中用于量化交易的核心算子,涵盖算术运算、逻辑判断、时间序列分析、截面分析与向量变换五大类,按基础级与专家级分级讲解,适合量化分析师、算法交易员以及金融工程研究人员在构建和优化交易模型时查阅。文档以表格形式逐一说明abs、add、log、power、divide等算术算子,以及if_else逻辑判断、移动平均、延迟、协方差、ts_arg_max等时间序列算子的输入输出与使用要点,并补充了组内中性化、排名、标准化和归一化、分位数、正交化等截面与向量处理工具,可直接支撑数据预处理、特征工程和策略开发调试。资源为单份PDF文件,大小仅94KB,内容紧凑便于检索。这些算子可围绕WorldQuant BRAIN快速搭建从数据清洗、特征构造到策略评估的完整链路。已有1908人学习下载,适合希望系统掌握BRAIN算子用法、提升量化策略编写效率的读者。
1. 从积木思维到alpha信号:WorldQuant BRAIN算子解决的是什么
拿到"WorldQuant BRAIN算子详解"这个标题,多数人以为是查文档,其实真正的问题是:怎么把"动量""反转""放量"这些模糊想法,变成一段能在回测里跑出正收益的表达式。BRAIN算子是这套表达式语言的最小积木——算术算子负责数值合成,逻辑算子负责条件筛选,时间序列算子沿时间轴滚动取统计量,向量算子在同一截面上做横向比较。你不需要自己从零写指标库,只需要把几块算子叠起来,平台会自动完成并行计算、回测和绩效归因。这一篇按"算子怎么用、参数怎么设、坑在哪里"展开,适合已经在用BRAIN写alpha、但频繁遇到信号全为NaN或表现不稳定的研究者,也适合想快速验证选股逻辑的新手。读完后你能拿到一个可以直接抄的完整表达式模板。
2. 算术与逻辑算子:alpha表达式的最底层地基
2.1 算术算子的维度规则:标量、时间序列与截面的混合运算
BRAIN表达式的大部分算术算子(add、sub、mul、div、abs、power、log、sign)遵循逐元素语义。这里的"逐元素"包含两个维度:沿着时间轴对同一只股票做运算,同时沿着截面对同一个交易日的不同股票做运算。表达式引擎把输入信号看成一张"时间 × 股票"的大表,算术操作在同一时间点、同一只股票上进行数值计算;只有当其中一边是标量时,才会发生广播。所以"close - open"是当日单只股票的价差,"close / market_cap"在语义上也是逐股票运算,并不存在跨股票的隐式聚合。
真正容易出问题的是把两个"截面统计量"混进逐元素计算。比如你想用"过去60天的平均排名"做信号,常见错误是把rank(close)直接包进ts_mean,或者把ts_mean后的结果再交给rank。两者排序不同:rank(ts_mean(close, 60))是在每一天对"60日均线"作横向排名,反映的是股票长期成本的位置;ts_mean(rank(close), 60)则是先对每日收盘价作截面排名,再取排名的时间均值,反映的是相对位置的稳定度。这个差异在趋势市和震荡市里会给出截然不同的分组结果,也是新手最容易忽略的维度问题。
# BRAIN 表达式语言示例:算子的嵌套顺序决定语义 rank_avg = ts_mean(rank(close), 60) # 过去60天平均截面排名 avg_rank = rank(ts_mean(close, 60)) # 60日均线截面的排名 alpha = 0.6 * rank_avg + 0.4 * avg_rank # 两个版本做线性合成逻辑说明:第一行先把收盘价在截面上排名,压缩掉绝对价格的影响,再沿时间取均值,衡量的是股票在市场中相对位置的历史平均水平。第二行先做时间平滑,再在截面上比较绝对价位,衡量的是股票当前长期成本的高低。0.6和0.4是两者的权重系数,合成后的alpha同时包含了"相对稳定性"和"绝对位置"两种信息。参数上,60日窗口适合月度调仓或周度调仓的风格;如果你的组合平均换手周期只有5天,窗口缩短到10日和30日会更贴近持仓实际。
接着要处理的是除零。浮点除零在BRAIN里不会直接让表达式报错,但会产生无穷大或NaN,等到外层套上rank时,NaN的排名会被当作缺失值处理,整个截面的分布被挤到一边。我的习惯是所有分母先用abs或max保护,这个习惯在后来处理"成交量恰好为0"或者"市值缺失"的停牌交易日时救过很多次。用log函数压缩动态范围也是常见做法,BRAIN没有单独的log1p算子,替代方案是log(add(x, 1)),它对小幅值的压缩更温和,对极端大值的压制效果比log(x)更稳定。
2.2 逻辑算子与if_else:把交易规则变成可回测的条件表达式
逻辑算子在BRAIN里承担两类功能:一是产出布尔掩码,用于后续合成中的条件过滤;二是作为if_else的条件端,控制信号在两个分支之间切换。比较运算>、<、==、>=、<=都会返回布尔值,可以与and、or、not连接。不同版本的表达式编辑器对"与或非"的写法支持略有差异,有的接受关键字,有的要求两字符符号,保险起见在编辑器里直接点选算子插入,手写关键字要注意全半角符号混用。
业务上最常用的模式是"量价确认":当放量时给动量信号更高权重,当缩量时保留原信号或降权。这类写法比单纯把信号乘起来更接近真实交易逻辑,因为放量与否本身是离散事件,用一个连续的涨跌幅去乘"放量倍数",语义上会变成"量越大涨得越多",这在逻辑上站不住脚。if_else则能把条件从信号值里剥离出来,单独做一层开关控制。
# BRAIN 表达式语言示例:if_else 做量价确认 vol_ratio = ts_mean(volume, 5) / ts_mean(volume, 20) momentum = rank(close / delay(close, 10) - 1) alpha2 = if_else(vol_ratio > 1.5, momentum, 0.1 * momentum)逻辑说明:当近5日平均成交量是20日平均的1.5倍以上时,输出完整的动量信号;否则输出动量信号的十分之一作为保底权重。保底权重用来防止持仓在缩量期全部归零,也便于组合的多空平衡。参数1.5是放量阈值,可按标的数据分布调整,阈值越高选出的标的越少、信号越集中;0.1是缩量期的降权系数,过大会让策略在震荡市频繁出手,过小则会让信号在量能不足时几乎失效。
这里有个容易踩的坑:if_else的两个分支都会被完整计算,引擎不会做短路求值。表面上看条件不满足时不会执行另一段逻辑,实际两段信号都已经算完,只做了输出选择。这个设计在逻辑上无害,但在表达式很长、嵌套很深时,会明显增加内存占用和计算时间。大量使用算子供硬件性能带来的压力,在后面第5章的排查清单里还会专门展开。
3. 时间序列算子:把"过去N天"折叠成alpha因子
时间序列算子是BRAIN里使用频率最高、也是新手最先碰壁的一类。它的本质是在时间轴上开一个滚动窗口,对窗口内的数据做统计变换,再输出一个与原始序列等长的结果。拉普拉斯算子在图卷积里负责"邻域差分",时间序列算子里的差分、均值、排名做的其实是同一件事——把相邻交易日之间的关系变成可量化的特征;区别在于图上的邻域是空间关系,而时序窗口默认对所有历史点等权处理,除非你使用ts_decay_exp这类带衰减权重的函数。
3.1 从ts_mean到ts_rank:动量和相对强度怎么选窗口
时间序列算子的函数名基本遵循ts_前缀加统计名,最常见的有ts_mean、ts_sum、ts_std_dev、ts_max、ts_min、ts_delta、ts_decay_exp、ts_rank。其中ts_rank的返回值是窗口内该值所处的百分位,输出范围0到1,天然适合再做截面排名。它相比ts_mean更能捕捉相对位置而非绝对数值:用ts_mean(close, 20)表示20日均价时,高价股天然占优;改用ts_rank(close, 20),含义变成"当前收盘价在过去20天里处于什么分位",绝对价格影响被消除,信号更干净。
# BRAIN 表达式语言示例:动量、波动率调整与相对强度 mom10 = close / delay(close, 10) - 1 vol = ts_std_dev(ts_delta(close, 1), 20) alpha3 = rank(mom10 / vol) rel_strength = ts_rank(close / ts_mean(close, 20), 20) alpha4 = rank(rel_strength)逻辑说明:mom10是10日简单动量,vol用20日窗口对日收益序列做标准差,mom10 / vol等于单位风险的动量;rel_strength先把收盘价相对20日均值的比例算出来,再在过去20天窗口里对这个比例排名,衡量强度是否处于近期高位。alpha3和alpha4可单独回测,或按0.6/0.4加权合并。参数上,10日窗口捕捉短期趋势,20日窗口用于估计波动率,持有周期越长这两个参数应该同步放大;如果是高频量价数据,10可以改成5,20可以改成10。
这个例子里最值得留意的是vol的计算方式:先ts_delta(close, 1)生成日收益率再求标准差,而不是直接用ts_std_dev(close, 20)。ts_std_dev包在价格序列上得到的是价格波动,受绝对价格水平影响严重;对日收益率求波动率才是真正的"每单位风险"。很多第一次写波动率因子的人都栽在这里,结果alpha在高价股上天然权重偏大。如果想用对数收益率,BRAIN里没有现成的diff(log(close))写法,用log(close / delay(close, 1))加ts_std_dev包装,效果一样,只是表达式更长。
窗口参数的选择直接影响信号的换手率和稳定性。下面这张表是我自己调参时常用的参考基准,按持仓周期倒推窗口长度,比按感觉拍数字要可靠得多。
| 持有周期 | 动量窗口 | 波动率窗口 | 适用场景 | 注意点 |
|---|---|---|---|---|
| 日内或隔日 | 3~5 | 10 | 高频量价、日内反转 | 信号对噪声敏感,必须配winsorize |
| 周度调仓 | 5~10 | 20 | 周频动量、板块轮动 | 窗口短,次新股缺失值多 |
| 月度调仓 | 20~30 | 60 | 月度选股、基本面动量 | 趋势市中信号表现好,震荡市换手偏高 |
| 季度调仓 | 60~120 | 120 | 低换手、长期持有 | 参数过长容易变成缓慢的趋势跟踪,注意时滞 |
3.2 窗口参数与边界行为:NaN、极值和你的第一道防线
任何时序算子在每只股票的头几个交易日都缺数据。ts_mean(close, 60)在股票上市前59天返回NaN,这不是报错,而是给后续所有算子传递一个缺失状态。多数新手最容易犯的错是在回测里忽略这些缺失值,等发现组合收益在次新股上全是0或者被新上市股票带偏,才意识到要手动处理。常规做法是在入池条件加一个过滤,比如ts_count(close, 252) >= 60,把上市不足一年的股票屏蔽掉,再用逻辑运算符合并进入alpha主表达式。
窗口长度本身值得做一轮敏感度测试。ts_rank(close, 20)和ts_rank(close, 40)在趋势市差异不大,但在震荡市里20日版本频繁翻转,信号换手率可能高出30%以上。遇到这种情况,不必急着把窗口拉长,先在表达式输出端加一个延迟衰减参数,等价于给信号做平滑,能显著降低换手。这种"先保留敏感窗口、再做输出平滑"的做法,比直接改窗口更能保留信号的原始含义。
关于未来函数,BRAIN平台在算子内部已经做了大量防护,不允许算子引用未来时间的数据。但如果你把当日收盘价和次日开盘价放进同一个表达式,平台不会替你纠错,因为这两个序列在时间维度上是对齐的,只是语义上存在真实的时滞。规避方式很简单:所有用到open的alpha,一律配上delay(open, 1)或者直接用前收盘价。这一点在不少线下交流里被反复提起,属于"看起来没毛病、实盘就翻车"的典型黑匣子。
4. 向量算子:在截面上做横向排名的标准化工具
向量算子和时间序列算子最大的区别在于计算方向。时间序列算子沿时间轴滚动,向量算子则对同一个交易日的所有股票做横向计算。标题里说的"向量运算",在BRAIN语境下就是rank、scale、winsorize以及带group_前缀的分组算子。它们的输入是某一时刻全部股票的值,输出是同一截面重新标度后的值。理解这个方向性,你才会明白为什么rank不能随便塞进时间序列算子内部:rank(ts_mean(close, 20))是合法且有意义的,但ts_rank(rank(close), 20)的语义会变得很模糊,因为rank输出本身只含次序信息,再对它做时间窗口排名,得到的是"排名位置的滚动分位",这个信号在绝大多数策略里都很难解释。
4.1 rank、winsorize与scale:使用顺序不同,回测结果天差地别
rank(x)把截面值转换为0到1的经验分布排名,输出非线性,对极值天然不敏感;winsorize(x, 3)把超过3倍标准差的值截断到边界,保留数值大小但削掉尾部;scale(x)一般用于把信号的绝对值之和缩放为1,经常出现在仓位权重计算处。三个算子最常见的错误不在选哪个,而在顺序:先winsorize再rank,排名结果稳定;先rank再winsorize几乎无效,因为rank输出已经限定在0到1区间,没有极值可以再被削减。
# BRAIN 表达式语言示例:三种截面算子的顺序组合 raw = ts_delta(close, 10) / ts_std_dev(ts_delta(close, 1), 20) clean = winsorize(raw, 3) pct = rank(clean) alpha5 = scale(pct - 0.5) # 中心化后再缩放,形成多空权重逻辑说明:raw是10日价格变化与20日波动率的比值,含有明显尾部极值;clean用3倍标准差截断掉极端尾部;pct把清洗后的值映射为横截面排名;最后用scale把中心化后的排名映射成可执行权重,使得多头权重与空头权重绝对值之和各自可控。参数3是标准差倍数,如果因子分布本身厚尾,可以放到4,但超过5的阈值基本不会改变结果,因为极端值本来就不常见。
如果你的目标是纯多头组合,直接scale(pct)就行;如果想做多空对冲,一定要在rank之后减去0.5再做scale,否则所有输出都是正数,scale无法生成负权重。这个细节在界面回测上体现为"多空收益不对称"或者"策略只有多头没有空头"。另外,winsorize的参数在平台里也支持MAD版本,对含跳跃数据的因子更稳健,但MAD的运算开销比标准差大,样本量小的时候不建议用。
4.2 行业中性化:用group_rank和group_mean避免单一行业主导
行业聚集和风格暴露是横截面因子最常翻车的地方。在一个由科技股主导的股票池里做动量排名,信号排名靠前的标的大概率集中在几个高beta行业,真正被捕捉到的不是个股alpha,而是行业beta。行业中性化的标准做法是分组:按行业字段分桶,在桶内做排名或均值,再拼回全市场截面。BRAIN的group_家族算子就是为此设计的,group_rank(x, sector)在行业内排名,group_mean在行业内求均值后再广播回原维度。
# BRAIN 表达式语言示例:行业组内排名的典型写法 mom = ts_delta(close, 20) / ts_std_dev(ts_delta(close, 1), 20) ind_adj = group_rank(mom, sector) alpha6 = rank(ind_adj) * group_rank(volume, sector) # 组内排名叠乘组内流动性逻辑说明:先对每个行业把mom在行业内横向排名,抵消行业间差异;再对全市场做一次rank,输出标准化的0到1信号;最后乘以group_rank后的流动性因子,给流动性加权,避免小市值股票在组合里权重过大却无法成交。sector可以换成subindustry,分组粒度越细中性化越彻底,但每组股票数量越少,排名统计量越不稳定。按我的经验,全市场分行业时每组如果少于20只,group_rank的方差会明显变大,这时候退回sector粒度比硬扛subindustry更实际。
另一种场景是用group_mean做风格对冲。如果你想构建一个剔除行业收益的纯个股alpha,直接用x减去group_mean(x, sector)是比rank更严格的做法,因为它保留信号值的大小差异而不仅限于名次。缺点是极端值仍然存在,所以减法模式必须搭配winsorize使用。有的平台文档会把group_mean的结果输出为"组内均值广播",也就是每只股票拿到的是自己所在组的均值,这个行为在做截面去均值时非常方便,不要误以为它返回的是每组一个标量。
5. BRAIN算子避坑指南:5个翻车现场与排查方法
这一章写的是我在使用过程中被反复教育的点,每条按"现象、原因、解决"展开。遇到同名问题可以直接对照排查顺序来。
5.1 现象一:整个alpha回测结果全是NaN,或者组合仓位长期为空
新人在平台上写完表达式,回测跑完发现收益曲线是一条横线,打开持仓明细,里面一个标的都没有。最先怀疑表达式语法,但编辑器没有报错,这时问题基本出在数据缺失而不是语法。时序算子窗口长于股票上市历史,导致大量股票在样本期前段返回NaN;如果这些NaN没有经过任何过滤就被送进rank,排名函数会把缺失值当作无效输入,整个截面输出就被清空。
原因包括三类:一是新上市股票历史长度不足;二是除零产生Inf后被后续算子逐层放大;三是使用了没有提前过滤停牌数据的原始字段。排查时先删掉最外层rank,把内部信号单独导出,观察缺失值的比例和时间分布,确认是哪一个算子引入了第一处NaN。解决办法是在所有除法分母上加上max保护,在入池条件里用ts_count(close, N)加历史长度过滤,并把窗口参数缩短到能覆盖大部分股票的水平。养成"每个中间信号导出后先看缺失率"的习惯,可以省掉大量无头绪的调试时间。
5.2 现象二:ts_rank窗口太短,信号在次新股上全是极值
把窗口参数设成5或者3之后,回测分组里收益最高的那一组总是集中在上市不足半年的股票上,换手率异常高。原因在于短窗口可用的历史样本太少,只要中间出现一根异常K线(比如涨停板开板),窗口内的排名直接顶到1或0,加上次新股本身波动大、流动性不稳定,信号被噪声主导。这个问题在财报季和指数调整期尤其明显。
解决办法有三条:把ts_rank窗口从5拉长到10或20;如果必须保留短窗口来捕捉短线动量,则在外层加winsorize把极值截断;再给次新股单独做权重衰减,常见写法是用group_rank结合上市天数分组,让上市时间短的股票在组内排名时天然靠后。需要明白一点:ts_rank的统计功效随窗口增大而提升,但窗口拉长会增加信号滞后,所以不要盲目追求长窗口,先确认你的持有周期再定窗口长度。
5.3 现象三:winsorize和scale顺序颠倒,调参数调一天都没效果
在界面上反复改winsorize的倍数,从2改到6,回测结果几乎不变,说明极值裁剪本身没有触达真正的尾部。原因是你先执行了scale,把整个信号的数值压缩到很小的区间,之后再做winsorize时,分布已经没有可裁剪的尾部了。scale的输出天然受限,后续所有依赖数值大小的操作都会被抵消。
解决办法是把顺序固定成"去极值 → 中心化 → 排名 → 缩放",也就是先在原始因子上做winsorize,再中心化或rank,最后才做scale生成权重。如果想验证自己的顺序是否正确,把两种顺序产生的信号做相关性检验,相关系数通常低于0.5,这说明顺序本身已经改变了信号的解释含义。之后每次调参,优先检查表达式链路的顺序,不要一上来就动倍数参数。
5.4 现象四:group_rank的组内股票太少,结果噪声极大且参数不稳定
把分组粒度选到subindustry后,回测日志显示某些组只有3到5只股票,排名结果几乎每隔几天就完全翻转,分组收益的IC也忽高忽低。原因是细分行业里样本太少,排名实际上被个别股票的随机波动决定,统计功效不够。
解决办法是在固定分组粒度前,先按年统计每个分组的股票数量中位数;低于20只就退回sector。如果你需要保留细分行业的信息,可以用group_mean替代group_rank,均值对个别极端值不那么敏感。这个坑在季度调仓时更隐蔽,因为季度窗口天然掩盖了短期噪声,等你看到年度绩效报告时才意识到行业配置比例早就被少数几家公司绑架了。
5.5 现象五:表达式嵌套太多,平台反馈计算缓慢或直接超时
大量使用算子对硬件性能的挑战在BRAIN上是真实存在的。单个表达式里堆上二三十个算子,多层ts_window内嵌,if_else的每个分支都是完整计算图,最终生成的中间张量数量会指数膨胀。平台端的直接表现是从提交到回测结果返回的等待时间变长,严重时表达式被拒绝执行。
解决办法是把表达式拆成多个中间信号逐层定义,不要追求一段代码写完所有逻辑;固定的公共子表达式(比如ts_std_dev(ts_delta(close, 1), 20))抽出来单独命名,避免在多个分支里重复计算。如果编辑器提示超时,先把最外层嵌套拆掉,跑通后再一层一层加回去,逐层确认增加的延迟是否可接受。排查时打开平台的性能面板,看哪个算子占用了最多计算时间,通常是ts_decay_exp或ts_corr这类计算复杂度高的函数,优先替换成等效的低成本写法。
6. 从零拼装一个完整的alpha表达式:参数设置与验证技巧
6.1 一个包含四类算子的完整组合表达式
下面这个表达式在结构上覆盖了算术、逻辑、时间序列和向量四类算子,可以作为你搭建自己alpha的起点模板,直接替换字段和参数就能跑。
# BRAIN 表达式语言:完整alpha组合示例 close_ma = ts_mean(close, 20) trend = close / delay(close_ma, 20) - 1 vol_signal = ts_std_dev(ts_delta(close, 1), 20) strength = ts_rank(close, 60) / (vol_signal + 0.01) liquid = group_rank(volume, sector) alpha = rank(trend + 0.2 * strength) * liquid逻辑说明:trend是均线动量,strength是60日价格位置与波动率调整后的强度,liquid是行业内流动性排名,最终把动量、强度和流动性合成为一个综合信号。参数0.2是强度信号的比例系数,建议初值取0.2,再用0到0.5之间的网格做二次搜索;0.01是vol_signal的防零保护。注意这个alpha在牛市中会暴露一定趋势风险,必须配合下面的验证步骤再决定是否实盘。
6.2 用分段验证替代"一个回测走天下"
拿到组合表达式之后,第一步先做分年度回测,把2020年、2023年、2024年这些市场结构差异明显的年份单独看,如果某一年IC极差,说明alpha对市场状态敏感,实盘时就要警惕环境变化。第二步做换手率检查,一个收益再好的alpha,如果月度换手超过500%,实际执行的滑点和手续费会吃掉大部分收益,这时候优先加延迟衰减而不是加权重。第三步做参数邻域检验,把窗口参数换成一串相近值,观察是否全部失效;如果只改一个数字结果就崩,说明过拟合很严重,需要用更大的样本外区间重新验证。
我最后的调参习惯是保留一小段out-of-sample样本,只在前半段调参,后半段完全不动,等样本外验证通过再固化参数。这套做法虽然牺牲了一点回测拟合度,但能明显降低实盘翻车的概率。希望这些经验能帮你在WorldQuant BRAIN上把算子用得更稳,回测顺利。
本文还有配套的精品资源,点击获取