-
Notifications
You must be signed in to change notification settings - Fork 41
expr_codegen简易教程
仿WorldQuant Alpha101表达式转译工具。简单!高效!开源!
量化研究员一定很羡慕WorldQuant表达式在因子挖掘的便捷,特别是用pandas来实现时序与截面混合的因子。到Github上找了不少Alpha101的代码,但都要对表达式手工翻译,工作量大易出错。
有一次忽然灵光一闪,想到用装饰器可以实现时序与截面算子的提前分组。这就是ta_cn项目的来历。但使用了一段时间后,还是发现很大的不足。
- alpha101中有大量重复表达式,希望能化简避免重复计算
- 装饰器中有
groupby,每个算子都分组一次,应当归集起来一起groupby
由于pandas+ta_cn速度还是不够理想,开始转向polars。以前的表达式翻译成polars又是巨大的工作量。
又一次偶然想到sympy有表达式化简功能,开始翻阅文档,发现还有cse功能,开始验证可行性,最终推出expr_codegen,完全解决了ta_cn的缺点。
并将其中的算子代码独立成了polars_ta。
一开始算子名都按Alpha101,比如rank,但后来发现自定义因子比较麻烦,所以约定算子有前缀。
官方源
pip install expr_codegen -i https://pypi.org/simple -U
pip install polars_ta -i https://pypi.org/simple -U
国内镜像
pip install expr_codegen -i https://mirrors.aliyun.com/pypi/simple -U
pip install polars_ta -i https://mirrors.aliyun.com/pypi/simple -U
免安装使用
常用函数只有一个codegen_exec
- 代码生成(codegen)
- output_file: 用于调试,或脱离codegen直接运行
- None: 生成的代码只存在于内存中
- sys.stdout: 打印到标准输出
- path: 保存到文件
- date/asset: 日期和资产字段名。用于分组和排序
- style: 代码风格。
- pandas: 使用
pd.DataFrame.groupby - polars(推荐): 使用
Expr.over - sql: 生成
sql语句
- pandas: 使用
- codes: 需转译的代码块。支持传入多块代码
- 函数块:可以利用
IDE的智能补全,方便编写 - 字符串:可利用循环拼接表达式。支持无法过Python语法检查的三元表达式
- 函数块:可以利用
- extra_codes: 原样复制到目标代码中的字符串
- output_file: 用于调试,或脱离codegen直接运行
- 执行生成的代码(exec)
-
df不为None就会exec执行生成的代码 -
df可以为pd.DataFrame/pl.DataFrame/pl.LazyFrame之一- date: 日期时间。对时间粒度无要求,可以日月年,也可以秒分时。在横截面计算时用于分组,在时序计算时用于排序
- asset: 资产。在时序指标计算前用于分组。
- other: 其他特征/因子
- run_file: 可跳过转译,直接运行文件
- True: 直接运行
output_file文件 -
out.py: 直接运行out.py文件 -
out: 直接运行out模块
- True: 直接运行
-
-
ts_xxx()时序算子: 先groupby(asset),然后sort(date),最后调用ts_xxx -
cs_xxx()截面算子: 先groupby(date),然后调用cs_xxx -
gp_xxx(group,)分组算子: 先groupby(date, group),然后调用cs_xxx -
xxx():不用groupby直接调用的算子 -
_xxx临时特征: 提取公共子表达式时会自动生成,参与特殊的语法调用等功能,事后会被删除 -
C?T:F支持C语言的三元表达式。注意:仅能通过字符串输入 -
A,B,C=MACD()。支持多输出的算子 -
CLOSE[1]。表示昨天收盘价,底层会转换成ts_delay(CLOSE,1)
alpha003=-1 * ts_corr(cs_rank(open), cs_rank(volume), 10)
相当简单,直接复制到https://exprcodegen.streamlit.app,然后下载到本地,导入到项目中
from output import main
# ...
df = main(df)
#...注意:open这是原始价?还是后复权价?
自己开发了一个算子库,如何使用?
- 保证
import到项目中后能被Python调用成功 - 保证函数名符合
ts_/cs_等前缀规则
- 通过代码区传入
def _code_block_1():
from my_lib import my_func as ts_func
A = ts_func(OPEN, CLOSE, 10)
df = codegen_exec(df, _code_block_1, output_file='output.py')可以检查output.py中的代码是否正确。之后可直接调用省去转译过程。
- 通过修改模板,备份
polars_over/template.py.j2,修改template2.py.j2,文件前插入from my_lib import my_func as ts_func
def _code_block_1():
A = ts_func(OPEN, CLOSE, 10)
df = codegen_exec(df, _code_block_1, output_file='output.py',template_file='template2.py.j2')所以,polars_ta本质只是一个符合前缀规则的算子库。用户可以开发自己的算子库
- 如果只有少量代码要插入,可以更简洁
def _code_block_1():
def cs_rank_if(condition, factor):
"""可实现动态票池功能。但无法实现仅算票池中的时序指标"""
# 不支持expr_codegen语法,只支持Python原生语法
# 所以没有[1],只能用ts_delay(,1)等
return cs_rank(if_else(condition, factor, None))
df = codegen_exec(df, _code_block_1, output_file='output.py')聚宽数据字段名与本工具默认设置不同
- 提前规范字段名
df = df.rename({'time': 'date', 'code': 'asset'})- 转译时指定
df = codegen_exec(df, _code_block_1, date='time', asset='code')除了生成因子,是否能生成机器学习的标签?
标签是未来数据,本质上只要平移即可,[-1]最简洁,底层会替换成ts_delay(,-1)
收益率_第一天开盘入场_第二天收盘出场 = CLOSE[-2]/OPEN[-1]-1Alpha191一条条翻译成指定前缀规则的算子工作量太大了
已经提供了ast翻译代码的示例供研究
https://github.com/wukan1986/alpha_examples/blob/main/transformer/alpha191_transformer.py
生成的代码如何下断点调试?分两步
# 生成文件
df = codegen_exec(df, _code_block_1, output_file="output1.py", run_file=False)
# 跳过转译,以模块方式调用,run_file后缀名非`.py`
df = codegen_exec(df, _code_block_1, output_file="output1.py", run_file="output1")公式已经定型不用再改,如何最小改动跳过转译,长期使用?
# 生成文件
df = codegen_exec(df, _code_block_1, output_file="output1.py", run_file=False)
# 跳过转译,以文件方式运行
df = codegen_exec(df, _code_block_1, output_file="output1.py", run_file=True)如何使用三元表达式?
def _code_block_1():
A = if_else(OPEN>CLOSE,1,0)
codes="""
B=OPEN>CLOSE?1:0
"""
df = codegen_exec(df, _code_block_1, codes)以上两种方法是等价的
不少函数计算的结果会有异常值,如果多个函数嵌套,异常值会越来越多,异常值是每步计算都处理,还是最后一步再处理?如ts_mean(ts_std_dev(CLOSE, 10), 5)
有两种方案:
- 修改
polars_ta代码,保证每个函数在输出前的数据都修正了异常值,但这会导致效率低下。再者异常值是用0/None/mean等值填充都是看情况决定的 - 利用装饰器
extra_codes = """
from functools import wraps
def fill_null_decorator(func):
@wraps(func)
def decorated(*args, **kwargs):
return fill_null(func(*args, **kwargs), 0)
return decorated
# 对部分函数套装饰器
ts_mean = fill_null_decorator(ts_mean)
ts_std_dev = fill_null_decorator(ts_std_dev)
"""
def _code_block_1():
alpha_001 = ts_mean(ts_std_dev(CLOSE, 10), 5)
df = codegen_exec(df, _code_block_1, extra_codes=extra_codes, output_file="1_out.py")可以观察生成的"1_out.py"
表达式最后一步是截面计算,是否可以只算最后两天?因为实盘时我只需要最后一行,这样能加快速度。
df = codegen_exec(df, _code_block_1, output_file="output1.py", ge_date_idx=-2)它的效果是
df = func_0_ts__asset(df.sort(_ASSET_, _DATE_)).drop(*[])
df = _filter_last(df, ge_date_idx)
df = func_0_cs__date(df.sort(_DATE_)).drop(*[])
df = func_1_cl(df).drop(*[])其中df = _filter_last(df, ge_date_idx)是新插入的语句。插入位置的原则是:
- 最后一个
ts函数之后 - 没有
ts函数,插入在第一位
例如:
A = cs_rank(CLOSE, True)
B = ts_returns(CLOSE, 5)它两是并行的,先算后算无所谓,但工具会调整成先算ts,然后filter,再算cs