发布时间:2026/9/1 9:16:15
Qlib 表达式引擎:一行表达式定义任意量化因子 Qlib 表达式引擎一行表达式定义任意量化因子【免费下载链接】qlibQlib is an AI-oriented Quant investment platform that aims to use AI tech to empower Quant Research, from exploring ideas to implementing productions. Qlib supports diverse ML modeling paradigms, including supervised learning, market dynamics modeling, and RL, and is now equipped with https://github.com/microsoft/RD-Agent to automate RD process.项目地址: https://gitcode.com/GitHub_Trending/qli/qlib从改一个参数重跑全链路说起你调了 MA 的窗口从 20 到 30结果发现因子计算写死在数据加载脚本里改完得重新跑整个 pipeline 才能看效果再想加一个收盘价高于 20 日均线时取成交量否则取 0的因子又得手写一遍数据读取和对齐代码。Qlib 表达式引擎Expression Engine源码在qlib/data/ops.py解决的就是这件事因子用一行字符串或几行 Python 代码定义解析、取数、缓存全部交给引擎。30 秒全景图Qlib 表达式引擎是 Qlib 数据层之上的因子计算层把一行表达式字符串解析成运算符对象树递归加载原始行情并完成计算输出pd.Series或pd.DataFrame。你写的是算什么它管怎么算、算了没算过。图中左侧数据层提供原始行情表达式引擎是中间的计算通道右侧模型层拿到的已经是算好的因子矩阵。parse_field eval负责把字符串变成对象load负责从存储层递归取数H[f]是全局内存缓存——这三个节点的行为是后面机制拆解的重点。机制拆解表达式字符串如何变成运算符对象树Qlib 没有自研词法/语法分析器而是复用 Python 的eval。qlib/data/data.py中ExpressionProvider.get_expression_instance的核心逻辑就一句把Mean($close, 20)转成合法 Python 代码后直接 eval得到的就是运算符对象。# qlib/data/data.py 中 get_expression_instance 的核心步骤 expression eval(parse_field(field)) # -- parse_field 把 $close 替换为 # Operators.Feature(close)Mean 等函数名在 eval 环境中映射到 Operators 注册表 self.expression_instance_cache[field] expression # 同一字符串只解析一次运算符类通过注册表Operatorsqlib/data/ops.py底部的OpsWrapper实例集中管理register_all_ops启动时把OpsList里约 50 个内置运算符注册进去__getattr__按名字取类。设计动机是零解析成本加可扩展eval 一行代码搞定用户还能通过C.custom_ops或Operators.register注册自定义运算符。反例如果像多数因子库那样自研 AST 解析器每加一个运算符都要改解析器字符串与实现的对应关系也更难维护。运算符树如何被递归执行所有运算符继承自qlib/data/base.py的Expression执行入口是模板方法load具体计算留给子类的_load_internal叶子节点Feature写法是$close字符串化后带$前缀直接读存储层Mean、Ref等窗口运算符先load自己的子表达式再套窗口计算。load的统一实现还干了两件子操作员感知不到的事全局内存缓存和错误日志后面生产实践部分细讲。窗口运算符Rolling._load_internal是整个引擎里最值得读的一段它用 8 行覆盖了三种窗口语义# qlib/data/ops.py 中 Rolling._load_internal 的核心分支 if isinstance(self.N, int) and self.N 0: series getattr(series.expanding(min_periods1), self.func)() # N0: 扩展窗口 elif isinstance(self.N, float) and 0 self.N 1: series series.ewm(alphaself.N, min_periods1).mean() # 0N1: 指数加权 else: series getattr(series.rolling(self.N, min_periods1), self.func)() # N 为正整数N 的取值行为对应 pandas 语义正整数如20固定 20 期滚动窗口rolling(20)0扩展窗口从序列起点算到当前expanding(0, 1)小数如0.1指数加权alpha0.1ewm(alpha0.1)把三种语义塞进同一个N参数好处是因子表达式里窗口类型可插拔Mean($close, 0)就是上市以来均值不需要单独发明运算符。反例如果仿照某些因子库把扩展窗口做成独立的ExpandingMean运算符数量会成倍膨胀且表达式文本无法通过改一个参数切换行为。注意min_periods1窗口不满 N 期时照常出值基于已有数据计算所以序列头部不会有一串 NaN。这一点和你直接用 pandas 手写结果会有差异调试时容易误判。运算符重载为什么你能用加号写因子Expression基类重写了__add__、__sub__、__truediv__、__gt__等全套运算符$close / $open - 1在 Python 里直接得到Sub(Div(Feature(close), Feature(open)), 1)的对象树str()就能印回表达式文本。这意味着两种写法字符串和 Python 对象产出同一棵树可以在 Python 里自由拼接、传参、组合。反例如果只有字符串入口像用同一个 20 日均线算两个因子这种复用就只能靠字符串拼接极易写错。动手实验前置准备安装 Qlib 并下载示例数据只需一次。git clone https://gitcode.com/GitHub_Trending/qli/qlib pip install -e qlib python qlib/scripts/get_data.py qlib_data cn_data实验一最小可运行的因子目标验证表达式字符串能直接算出单只股票的因子序列。import qlib qlib.init(provider_uri~/.qlib/qlib_data/cn_data, regioncn) # 指定数据目录 from qlib.data import D s D.feature(SH600519, ($close-$open)/$open, start_time2020-01-01, end_time2020-01-31) print(s) print(s.count())预期输出具体数值取决于行情数据2020-01-02 -0.0062 2020-01-03 0.0124 ... Freq: D, Name: ($close-$open)/$open, dtype: float32 22值得注意index 是交易日历非自然日1 月只有 22 个交易日series 的 name 就是表达式本身后续批量取数时会成为列名。变体把表达式换成Mean($close, 5)前 5 个交易日就能出值min_periods1的效应且前 5 个值是基于 1~5 期的均值而非 pandas 默认的 NaN。实验二批量取多因子并组合滚动与条件目标上一步只取了单因子这一步用dataset一次取多列并叠加滚动窗口和If条件分支。import qlib qlib.init(provider_uri~/.qlib/qlib_data/cn_data, regioncn) from qlib.data import D fields [ Mean($close, 20), # 20 日均价 Std($return, 10), # 10 日收益波动率 If($close Mean($close, 20), $volume, 0), # 站上均线才计成交量 ] df D.dataset(SH600519, fields, start_time2020-01-01, end_time2020-06-30) print(df.columns.tolist()) print(df.tail(3)) print(df[If($close Mean($close, 20), $volume, 0)].eq(0).mean())预期输出示例数值[Mean($close, 20), Std($return, 10), If($close Mean($close, 20), $volume, 0)] Mean($close, 20) Std($return, 10) If(...) 2020-06-29 731.25 0.021 1.82e07 2020-06-30 735.80 0.019 0.00e00 0.41值得注意列名就是表达式原文最后一行约 0.41 是收盘价跌破 20 日均线的天数占比说明If的 false 分支0真实生效了。变体把Mean($close, 20)改成Mean($close, 0)得到的是从该股上市首日算起的均值——窗口参数切换行为表达式结构不变。实验三Python 对象式组合与跨资产引用目标展示运算符重载和ChangeInstrument叠加用 Python 代码拼因子树并引用指数数据计算相对强度。import qlib qlib.init(provider_uri~/.qlib/qlib_data/cn_data, regioncn) from qlib.data import D from qlib.data.ops import Operators close Operators.Feature(close) ret close / Operators.Ref(close, 1) - 1 # 重载直接构树 idx Operators.ChangeInstrument(SH000300, close) # 换到沪深300指数 alpha Operators.Mean(ret, 20) - Operators.Mean(idx / Operators.Ref(idx, 1) - 1, 20) print(alpha) # 运算符树文本化可回传给 D.feature s D.feature(SH600519, str(alpha), 2023-01-01, 2023-03-31) print(s.tail(3))预期输出树结构示意具体数值取决于行情Sub(Mean(Sub(Div($close,Ref($close,1)),1),20), Mean(Sub(Div(ChangeInstrument(SH000300,$close), Ref(ChangeInstrument(SH000300,$close),1)),1),20)) 2023-03-29 0.0312 2023-03-30 -0.0045 2023-03-31 0.0108值得注意print(alpha)输出的文本可以直接作为字符串表达式回传D.feature两种写法闭环互通ChangeInstrument内部会忽略当前股票参数、改用指定标的取数这是相对强度类因子的关键。生产级实践当子表达式被重复使用时引擎的性能关键在Expression.load的缓存设计——缓存逻辑集中在基类所有运算符免费共享# 摘自 qlib/data/base.py 的 Expression.load cache_key str(self), instrument, start_index, end_index, *args if cache_key in H[f]: return H[f][cache_key] # -- 同文本子表达式命中内存缓存零重算 series self._load_internal(instrument, start_index, end_index, *args) series.name str(self) H[f][cache_key] series # -- 写回全局缓存供兄弟节点复用错误写法是把因子拆成多次D.feature调用、每次传不同时间范围(close-MA)/MA里 MA 只会在同一 instrument 且同一 index 范围的请求间复用范围一变就全部重算。正确写法是像实验二那样把多个表达式放进一次dataset调用对批量场景D.dataset默认还有disk_cache1的磁盘缓存DiskDatasetCache源码在qlib/data/cache.py相同标的池、字段、时间窗的二次请求直接读盘。一句话缓存键是表达式文本 标的 索引范围三元组三者保持一致才能命中。当窗口参数 N 取 0 或负数时Mean($close, 0)扩展窗口能跑但get_extended_window_size里明确标注了FIXME扩展窗口的边界数据不会精确计算源码原话 The Rolling(ATTR, 0) will not be accurately calculated涉及它的表达式在边界日期附近可能有偏差。正确做法需要全历史统计时用扩展窗口但在回测起始前预留充分数据或对精度敏感的场景改用足够大的固定窗口。Ref($close, -1)是另一个危险项Ref支持负 N 取未来数据用于研究没问题进生产就是前视偏差。get_longest_back_rolling的文档也明说Ref(Ref($close, -1), 1)这类嵌套无法正确处理。生产流水线里建议直接把负 N 视为 bug。如何确认你的表达式真的算对了import qlib qlib.init(provider_uri~/.qlib/qlib_data/cn_data, regioncn) from qlib.data import D from qlib.data.utils import parse_field field Mean($close, 20) - Ref($close, 1) print(eval(parse_field(field))) # 校验解析出的运算符树是否符合意图 s D.feature(SH600519, field, 2023-01-01, 2023-06-30) close D.feature(SH600519, $close, 2022-11-01, 2023-06-30) expect close.rolling(20, min_periods1).mean() - close.shift(1) # pandas 手算 print((s - expect.loc[s.index]).abs().max()) # 应为 0 或浮点精度量级三步定位法先print解析结果确认树没解析错比如括号位置再用 pandas 对单只股票手算对照注意对齐min_periods1这个细节最后怀疑性能问题时开 DEBUG 日志——load出错时会通过get_module_logger(data)打出 instrument、表达式、索引范围和异常详情这是排查某个标的数据缺了的第一现场。表达式算出的因子序列最终会进入 IC/收益分析环节上图为 Qlib 报告模块的 IC 分析输出样式如果数值和手算对不上问题一定在表达式或数据而不是评估环节。边界与取舍只做单标的的时间序列运算表达式引擎的操作单位是一只标的一列时间序列。横截面运算全市场 zscore、横截面 rank不在此列Rank($close, 20)是滚动窗口内的排名不是横截面排名。横截面标准化请用DataHandlerProcessor源码qlib/contrib/data/handler.py的ZScoreNorm、RobustZScoreNorm等两层各司其职。自定义运算符有明确成本写一个运算符需要继承ExpressionOps实现_load_internal、get_longest_back_rolling、get_extended_window_size三个抽象方法再经C.custom_ops或Operators.register注册注意同名会覆盖内置运算符并打 warning。如果你只是想调一个现成 Python 函数如 scipy 的某统计量更省事的路线是在dataset外面用 pandas/pipeline 处理而不是塞进表达式。已知不精确的场景源码自己承认两处扩展窗口N0边界不精确、负 N 嵌套的get_longest_back_rolling计算错误见前文。如果你的场景依赖精确的上市以来统计或未来函数组合建议绕开表达式引擎直接用存储层数据 pandas 实现。延伸与下一步通读运算符实现qlib/data/ops.py重点看Rolling和OpsWrapper两个类数据层完整文档表达式、DataHandler、PIT 数据docs/component/data.rst进阶点时数据库PIT运算符P/PRef与$$前缀字段见qlib/data/pit.py自定义运算符踩坑可直接在项目 Issues 区讨论速查附录名称语法一句话说明Feature$close叶子节点读原始行情字段RefRef($close, 1)取 N 期前数据负 N 为未来数据Mean / Sum / StdMean($close, 20)滚动均值 / 求和 / 标准差EMA / WMAEMA($close, 0.1)指数加权 / 加权移动平均Max / Min / Med / Rank / QuantileMax($high, 5)滚动极值、中位数、排名、分位数Slope / Rsquare / ResiSlope($close, 20)滚动回归斜率、拟合度、残差Corr / CovCorr($return, $idxret, 20)双序列滚动相关 / 协方差IdxMax / IdxMinIdxMax($volume, 10)窗口内最大 / 最小值的位置DeltaDelta($close, 5)当期与 N 期前之差IfIf(cond, a, b)按条件逐行取 a 或 bChangeInstrumentChangeInstrument(SH000300, $close)换用指定标的计算子表达式Log / Abs / Sign / PowerLog($close)逐元素一元运算MaskMask($close, SH000300)只保留指定标的的数据加减乘除$close / $open - 1运算符重载直接构树你现在可以打开qlib/data/ops.py对照本文验证每个运算符的窗口语义并用实验二的模板替换表达式跑通自己的第一个因子。【免费下载链接】qlibQlib is an AI-oriented Quant investment platform that aims to use AI tech to empower Quant Research, from exploring ideas to implementing productions. Qlib supports diverse ML modeling paradigms, including supervised learning, market dynamics modeling, and RL, and is now equipped with https://github.com/microsoft/RD-Agent to automate RD process.项目地址: https://gitcode.com/GitHub_Trending/qli/qlib创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

2026/9/1 9:16:15

基于OpenCV+QT的啤酒瓶口缺陷检测系统实现

简介:一套面向啤酒瓶口缺陷检测的C工程源码,采用OpenCV视觉库和Qt界面框架实现,适合有图像处理基础或希望入门工业检测的开发者学习使用。算法流程完整覆盖灰度化、高斯滤波、自适应阈值、数学形态学处理、连通区域查找、最大轮廓筛选&#x…

2026/9/1 9:16:15

STM32循迹避障小车完整实战:从硬件搭建到调参避坑

简介:基于STM32F103C8T6的循迹避障小车完整工程源码包,面向嵌入式入门学习者与电子设计竞赛备赛学生,能够帮助理解循迹、避障两种工作模式的代码组织与实时切换思路。压缩包共292个文件、约7.73MB,内部除.c/.h源码外,还…

2026/9/1 9:11:14

mpv 播放器快速上手:源码编译到脚本扩展的 6 步实操教程

mpv 播放器快速上手:源码编译到脚本扩展的 6 步实操教程 【免费下载链接】mpv 🎥 Command line media player 项目地址: https://gitcode.com/GitHub_Trending/mp/mpv 昨晚想在终端里直接播一个 .mkv,懒得开图形界面,顺手敲…

2026/9/1 9:31:17

微信消息防撤回终极指南:从底层原理到避坑全解析

微信消息防撤回终极指南:从底层原理到避坑全解析 【免费下载链接】RevokeMsgPatcher :trollface: A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁(我已经看到了,撤回也没用了) 项目地址: https://gitcode.com/Git…

2026/9/1 9:31:17

基于Java与YOLOv5的柑橘病虫害检测系统实战解析

简介:本资源是一款面向农业信息化开发者与高校计算机专业学生的柑橘病虫害智能检测系统完整Java实现,聚焦农作物病虫害快速识别这一实际农业植保需求,适用于课程设计、毕业设计及轻量级智慧农业工具开发场景。压缩包共30个文件,总…

2026/9/1 9:31:17

机器人开发实战:从ROS2环境搭建到核心代码实现

最近,机器人领域似乎进入了一个“静默期”。新闻头条不再被“人形机器人颠覆世界”的宏大叙事占据,取而代之的是工厂里机械臂的精度提升了0.1%、物流分拣机器人如何应对双十一爆仓、以及某款服务机器人又因为一个简单的台阶而“罢工”的报道。这背后传递…

2026/8/31 1:05:20

vSound小提琴数字处理器实操指南:从接线到演出的完整配置

电小提琴或者原声小提琴插电演出,第一个绕不开的坎就是声音难听。原声琴的共鸣和空气感一旦进了拾音器,出来的往往是一坨干瘪、发尖、带着奇怪塑料味的信号。我当初第一次把琴接上乐队调音台,直接被主唱吐槽"你这声音像在锯钢丝"。…

2026/9/1 8:27:47

传感器接口IC如何攻克生物化学传感的微弱信号难题?

1. 从电极到比特流:为什么生物化学传感必须依赖专用接口IC 做生物化学传感的人都有过类似的经历:明明传感器本身性能很好,信号输出却一塌糊涂——噪声大、漂移明显、重复性差,怎么调都达不到预期。很多时候问题并不在传感器&#…

2026/9/1 7:04:43

STM32F411CEU6多通道ADC采集:扫描模式+DMA实现详解

1. 多通道 ADC 的用武之地把“Multichannel ADC”和“STM32F411CEU6”这两个关键字放在一起,其实就是嵌入式开发里最常遇到的一类需求:用一块不算贵的 MCU,同时采集多路模拟信号。STM32F411CEU6 是 48 引脚的 Cortex-M4F 主控,主频…

2026/9/1 0:00:42

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/1 0:00:42

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/1 0:00:42

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

2026/9/1 0:00:42

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行&#xff0c;Type-C接口算是典型的“看着简单&#xff0c;做起来全坑”的东西。光引脚就24个&#xff0c;高低速信号、电源、控制线全部塞在一个小小的连接器里&#xff0c;如果PCB布局不做规划&#xff0c;打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/1 0:00:42

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/1 0:00:42

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…