Skip to main content

symbol expression to polars expression tool

Project description

expr_codegen 符号表达式代码生成器

表达式转代码工具

项目背景

在本人新推出polars_ta这个库后,再回头反思expr_codegen是什么。

expr_cdegen本质是DSL,领域特定语⾔(Domain Specific Language)。但它没有定义新的语法

它解决了两个问题:

  1. polars_ta已经能很方便的写出特征计算表达式,但遇到混用时序与截面的表达式,利用expr_codegen能自动分组大大节省工作
  2. expr_codegen利用了Common Subexpression Elimination公共子表达式消除,大量减少重复计算,提高效率

就算在量化领域,初级研究员局限于时序指标,仅用polars_ta即可,中高级研究员使用截面指标,推荐用expr_codegen

虽然现在此项目与polars_ta依赖非常紧密,但也是支持翻译成其它库,如pandas / cudf.pandas,只是目前缺乏一个比较简易的库

在线演示

https://exprcodegen.streamlit.app

初级用户可以直接访问此链接进行表达式转译,不需要另外安装软件。(此工具免费部署在国外,打开可能有些慢)

更完整示例访问alpha_examples

使用示例

import sys

# from polars_ta.prefix.talib import *  # noqa
from polars_ta.prefix.cdl import *  # noqa
from polars_ta.prefix.ta import *  # noqa
from polars_ta.prefix.tdx import *  # noqa
from polars_ta.prefix.wq import *  # noqa

from expr_codegen.tool import codegen_exec


def _code_block_1():
    # 因子编辑区,可利用IDE的智能提示在此区域编辑因子
    LOG_MC_ZS = cs_mad_zscore(log1p(market_cap))


def _code_block_2():
    # 模板中已经默认导入了from polars_ta.prefix下大量的算子,但
    # talib在模板中没有默认导入。这种写法可实现在生成的代码中导入
    from polars_ta.prefix.talib import ts_LINEARREG_SLOPE  # noqa

    # 1. 下划线开头的变量只是中间变量,会被自动更名,最终输出时会被剔除
    # 2. 下划线开头的变量可以重复使用。多个复杂因子多行书写时有重复中间变时不再冲突
    _avg = ts_mean(corr, 20)
    _std = ts_std_dev(corr, 20)
    _beta = ts_LINEARREG_SLOPE(corr, 20)

    # 3. 下划线开头的变量有环循环赋值。在调试时可快速用注释进行切换
    _avg = cs_mad_zscore_resid(_avg, LOG_MC_ZS, ONE)
    _std = cs_mad_zscore_resid(_std, LOG_MC_ZS, ONE)
    # _beta = cs_mad_zscore_resid(_beta, LOG_MC_ZS, ONE)

    _corr = cs_zscore(_avg) + cs_zscore(_std)
    CPV = cs_zscore(_corr) + cs_zscore(_beta)


df = None  # 替换成真实的polars数据
df = codegen_exec(df, _code_block_1, _code_block_2, output_file=sys.stdout)  # 打印代码
df = codegen_exec(df, _code_block_1, _code_block_2, output_file="output.py")  # 保存到文件
df = codegen_exec(df, _code_block_1, _code_block_2)  # 只执行,不保存代码

目录结构

│  requirements.txt # 通过`pip install -r requirements.txt`安装依赖
├─data
│      prepare_date.py # 准备数据
├─examples
│      demo_express.py # 速成示例。演示如何将表达式转换成代码
│      demo_exec_pl.py # 演示调用转换后代码并绘图
│      demo_transformer.py # 演示将第三方表达式转成内部表达式
│      output.py # 结果输出。可不修改代码,直接被其它项目导入
│      show_tree.py # 画表达式树形图。可用于分析对比优化结果
│      sympy_define.py # 符号定义,由于太多地方重复使用到,所以统一提取到此处
├─expr_codegen
│   │  expr.py # 表达式处理基本函数
│   │  tool.py # 核心工具代码。一般不需修改
│   ├─polars
│   │  │  code.py # 针对polars语法的代码生成功能
│   │  │  template.py.j2 # `Jinja2`模板。用于生成对应py文件,一般不需修改
│   │  │  printer.py # 继承于`Sympy`中的`StrPrinter`,添加新函数时可能需修改此文件

工作原理

本项目依赖于sympy项目。所用到的主要函数如下:

  1. simplify: 对复杂表达式进行化简
  2. cse: Common Subexpression Elimination公共子表达式消除
  3. StrPrinter: 根据不同的函数输出不同字符串。定制此代码可以支持其它语种或库

因为groupby,sort都比较占用时间。如果提前将公式分类,不同的类别使用不同的groupby,可以减少计算时间。

  1. ts_xxx(ts_xxx): 可在同一groupby中进行计算
  2. cs_xxx(cs_xxx): 可在同一groupby中进行计算
  3. ts_xxx(cs_xxx): 需在不同groupby中进行计算
  4. cs_xxx(ts_xxx(cs_xxx)): 需三不同groupby中进行计算
  5. gp_xxx(aa, )+gp_xxx(bb, ): 因aa,bb不同,需在两不同groupby中进行计算

所以

  1. 需要有一个函数能获取当前表达式的类别(get_current)和子表达式的类别(get_children)
  2. 如果当前类别与子类别不同就可以提取出短公式(extract)。不同层的同类别表达式有先后关系,不能放同一groupby
  3. 利用cse的特点,将长表达式替换成前期提取出来的短表达式。然后输入到有向无环图(DAG)
  4. 利用有向无环图的流转,进行分层。同一层的ts,cs,gp不区分先后
  5. 同一层对ts,cs,gp分组,然后生成代码(codegen)即可

隐含信息

  1. ts: sort(by=[ASSET, DATE]).groupby(by=[ASSET], maintain_order=True)
  2. cs: sort(by=[DATE]).groupby(by=[DATE], maintain_order=False)
  3. gp: sort(by=[DATE, GROUP]).groupby(by=[DATE, GROUP], maintain_order=False)

  1. 时序函数隐藏了两个字段ASSET, DATE,横截面函数了隐藏了一个字段DATE
  2. 分组函数转入了一个字段GROUP,同时隐藏了一个字段DATE

两种分类方法

  1. 根据算子前缀分类(get_current_by_prefix),限制算子必需以ts_cs_gp_开头
  2. 根据算子全名分类(get_current_by_name), 不再限制算子名。比如cs_rank可以叫rank

二次开发

  1. 备份后编辑demo_express.py, import需要引入的函数
  2. 然后printer.py有可能需要添加对应函数的打印代码
    • 注意:需要留意是否要加括号(),不加时可能优先级混乱,可以每次都加括号,也可用提供的parenthesize简化处理

expr_codegen局限性

  1. DAG只能增加列无法删除。增加列时,遇到同名列会覆盖
  2. 不支持删除行,但可以添加删除标记列,然后在外进行删除行。删除行影响了所有列,不满足DAG
  3. 不支持重采样,原理同不支持删除行。需在外进行
  4. 可以将删除行重采样做为分割线,一大块代码分成多个DAG串联。复杂不易理解,所以最终没有实现

小技巧

  1. sympy不支持==,而是当成两个对象比较。例如:

    1. if_else(OPEN==CLOSE, HIGH, LOW), 一开始就变成了if_else(False, HIGH, LOW)
    2. 可以用Eq来代替,if_else(Eq(OPEN, CLOSE), HIGH, LOW)。具体示例请参考Alpha101中的alpha_021
  2. sympy不支持boolint。例如:

    1. (OPEN < CLOSE) * -1报错 TypeError: unsupported operand type(s) for *: 'StrictLessThan' and 'int'
    2. 可以用if_else代替。if_else(OPEN<CLOSE, 1, 0)*-1。具体示例请参考Alpha101中的alpha_064
  3. Python不支持?:三元表达式,只支持if else, 而在本项目中需要转成if_else

以上三种问题本项目都使用ast进行了处理,可以简化使用

下划线开头的变量

  1. 输出的数据,所有以_开头的列,最后会被自动删除。所以需要保留的变量一定不要以_开头
  2. 为减少重复计算,自动添加了了中间变量,以_x_开头,如_x_0_x_1等。最后会被自动删除
  3. 单行表达式过长,或有重复计算,可以通过中间变量,将单行表达式改成多行。如果中间变量使用_开头,将会自动添加数字后缀,形成不同的变量,如_A_0__A_1_等。使用场景如下:
    1. 同一变量名,重复使用。本质是不同的变量
    2. 循环赋值,但DAG不支持有环。=号左右的同名变量其实是不同变量

转译结果示例

转译后的代码片段,详细代码请参考Polars版

def func_0_ts__asset(df: pl.DataFrame) -> pl.DataFrame:
    df = df.sort(by=[_DATE_])
    # ========================================
    df = df.with_columns(
        _x_0=1 / ts_delay(OPEN, -1),
        LABEL_CC_1=(-CLOSE + ts_delay(CLOSE, -1)) / CLOSE,
    )
    # ========================================
    df = df.with_columns(
        LABEL_OO_1=_x_0 * ts_delay(OPEN, -2) - 1,
        LABEL_OO_2=_x_0 * ts_delay(OPEN, -3) - 1,
    )
    return df

转译后的代码片段,详细代码请参考Pandas版

def func_2_cs__date(df: pd.DataFrame) -> pd.DataFrame:
    # expr_4 = cs_rank(x_7)
    df["expr_4"] = (df["x_7"]).rank(pct=True)
    return df


def func_3_ts__asset__date(df: pd.DataFrame) -> pd.DataFrame:
    # expr_5 = -ts_corr(OPEN, CLOSE, 10)
    df["expr_5"] = -(df["OPEN"]).rolling(10).corr(df["CLOSE"])
    # expr_6 = ts_delta(OPEN, 10)
    df["expr_6"] = df["OPEN"].diff(10)
    return df

本地部署交互网页

只需运行streamlit run streamlit_app.py

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

expr_codegen-0.8.0.tar.gz (31.7 kB view details)

Uploaded Source

Built Distribution

expr_codegen-0.8.0-py3-none-any.whl (35.3 kB view details)

Uploaded Python 3

File details

Details for the file expr_codegen-0.8.0.tar.gz.

File metadata

  • Download URL: expr_codegen-0.8.0.tar.gz
  • Upload date:
  • Size: 31.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.9.19

File hashes

Hashes for expr_codegen-0.8.0.tar.gz
Algorithm Hash digest
SHA256 b0e0a82cb4a4b4d36346d69cc3a9c4fdbac3d98e276b62e66fc3c516af0dba80
MD5 d05af6f13e9b448369bbd2340fd320af
BLAKE2b-256 1ae4ba2e537ef891d62e3df6ebd93499d0f6d65e421ffd9729edd9e2ee653a10

See more details on using hashes here.

File details

Details for the file expr_codegen-0.8.0-py3-none-any.whl.

File metadata

  • Download URL: expr_codegen-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 35.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.9.19

File hashes

Hashes for expr_codegen-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 79a457821fa9f75ad90f4897e200207cd0d42578e9c25e4b0ec8c1e0d598993f
MD5 b4a1a8f725d1f0a5c65e6f09cadf2513
BLAKE2b-256 85035c2a988883397f275fd2e3c4bf398f8bf8f888226233a1bf3149b5861a21

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page