读懂 Vibe-Trading 开源项目的因子库来源:一份 NOTICE 与四份子目录许可证
本文基于 Vibe-Trading 仓库 commit 3a752d5(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/HKUDS/Vibe-Trading 最新代码与文档为准。
Vibe-Trading(HKUDS 放出的那个开源交易 Agent 项目,不是”凭感觉交易”这种说法)最值得抄作业的地方,可能不是它的多智能体编制,而是它把第三方来源的归属问题当成代码问题来处理。 它的因子库不是自研的,仓库根目录一份 NOTICE 把每一组公式的上游来源和许可写在明处,agent/src/factors/zoo/ 下面又有四个子目录各带一份 LICENSE.md,把”我复现了什么、我没复现什么、我为什么认为可以复现”逐条摊开。更关键的是,这些声明不只停在文档里,它们在测试和 CI 的 grep 门里有对应的强制项。
这篇只讲工程与合规结构,不讲任何因子好不好用。历史表现不代表未来,本文只讨论工程实现。本文也不提供法律意见,涉及能否商用、能否再分发的判断一律以许可证原文为准。
站内已有的 开源项目选型方法 讲的是你怎么挑一个项目、开源 AI 工具盘点 讲的是有哪些东西可选、AI 写的代码能不能上生产 讲的是生成代码的质量门;这篇的分工是往下钻一层,看一个具体项目怎么把”这段代码从哪来”变成可被机器检查的约束。
一、根 NOTICE 先把话说死
打开仓库根目录的 NOTICE,头几行是常规的项目归属(Copyright 2026 HKUDS contributors),接着分成泾渭分明的两块。
第一块是走标准 Apache 2.0 通道的:Microsoft Qlib 的特征定义以 Apache 2.0 许可被 bundle 进来,指向 agent/src/factors/zoo/qlib158/ 下的 NOTICE 与 LICENSE.md。第二块是它自己给出的一个立场声明,原文这样写:
Mathematical formulas from the following sources are reimplemented in this
repository as factual mathematical content (formulas are not copyrightable):
- Kakushadze, Z. (2015) "101 Formulaic Alphas" (arXiv:1601.00991)
See agent/src/factors/zoo/alpha101/LICENSE.md
- Guotai Junan Securities (2014) "191 Short-period Trading Alpha Factors"
See agent/src/factors/zoo/gtja191/LICENSE.md
- Fama-French five-factor model, Carhart momentum, Hou-Xue-Zhang q-factor
See agent/src/factors/zoo/academic/LICENSE.md
紧跟着一句划边界的话:源论文和研报里的叙述文字、表格、图,一律不在本仓库中复现,只有数学公式被重实现。文件最后还有一条容易被忽略的:前端用的 Inter 与 JetBrains Mono 字体来自 @fontsource,走 SIL Open Font License 1.1,指向 frontend/public/fonts/LICENSE。
读到这里你就能看出这份 NOTICE 的写法特点:它不写”我们的因子库”,而是每一组都点名上游、点名年份、点名文献编号,再给一条到子目录的指路。任何人拿到仓库都能顺着这条路往下核。
二、四个子目录,四种不同的姿态
agent/src/factors/zoo/ 下面有五个子包:academic、alpha101、fundamental、gtja191、qlib158。其中四个带 LICENSE.md,只有 fundamental(里面是 roe.py、gross_profitability.py、asset_growth.py、earnings_yield.py 这几个基础财务因子)没有——因为它没有外部上游可归属。
| 组成部分 | 它负责什么 | 仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 根 NOTICE | 汇总所有第三方来源与许可,给出到各子目录的指路 | NOTICE | 你想再分发、或要判断能不能商用时,第一个该读的文件 |
| Kakushadze 101 | 101 个独立 Python 模块,一 alpha 一文件,按论文附录公式表重写 | agent/src/factors/zoo/alpha101/(含 LICENSE.md,共 103 个文件) | 你要加自定义 alpha,或想看单文件纯函数怎么写 |
| GTJA Alpha 191 | 国泰君安 2014 研报的 191 条公式,用本项目算子代数重表达 | agent/src/factors/zoo/gtja191/(含 LICENSE.md,共 193 个文件) | 你要核对某条公式的算子替换是否可接受 |
| qlib158 | 对 Microsoft Qlib Alpha158 特征目录的净室重表达 | agent/src/factors/zoo/qlib158/(含 LICENSE.md 与独立 NOTICE,共 157 个文件) | 你要满足 Apache 2.0 的归属义务时 |
| academic 基线 | 经典学术因子的价格代理实现,另含一个来自本仓技能库的因子 | agent/src/factors/zoo/academic/(含 LICENSE.md,共 14 个文件) | 你以为拿到的是原版因子序列时——它不是 |
| 注册表 | AST 扫描 zoo 模块、校验元数据、按需惰性导入 | agent/src/factors/registry.py | 你新增因子后发现它没被加载时 |
四份 LICENSE.md 的姿态明显不同,这个差异本身就是信息。
qlib158/LICENSE.md 走的是最正规的 Apache 2.0 流程:写明上游仓库、上游许可、锁定的 commit(d5379c520f66a39953bad76234a7019a72796fd0)和锁定的路径(qlib/contrib/data/handler.py 里的 Alpha158 类、loader.py 里的 Alpha158DL),然后逐条对照 Apache 2.0 §4 的义务解释自己怎么满足。目录里每个 .py 的文件头都带同一行引用:
# Adapted from microsoft/qlib@d5379c520f66a39953bad76234a7019a72796fd0:qlib/contrib/data/handler.py
# (Apache-2.0). Copyright (c) Microsoft Corporation.
它还专门回答了一个问题:这里的”作品”到底是什么?单条公式(比如 KMID = (close - open) / open)是算术事实,不受版权保护;真正被采用的是那份策展目录——挑哪 9 个 K 线特征、哪 29 个滚动特征乘 5 个窗口打包成 Alpha158,以及 KMID、KLEN、ROC、MA 这套规范短名。这份策展才是创造性贡献,所以归属给它。同一份文件还诚实交代了刻意跳过的 4 个字段(窗口 0 的 OPEN/HIGH/LOW/VWAP,属于原始输入而非信号),所以最终是 9 + 29×5 = 154 个。
旁边那份 qlib158/NOTICE 更有意思:它记录的是一个缺席。上游 microsoft/qlib 在锁定 commit 上根目录并没有 NOTICE 文件(文中写明是用 gh api repos/microsoft/qlib/contents 核实的),所以没有上游归属行可以照抄,这份文件专门把这个事实写下来,并给下游消费者留一个稳定的归属面。
alpha101/LICENSE.md 和 gtja191/LICENSE.md 走的是另一条路,因为上游是论文和研报,不是带许可的代码。它们的共同结构是三段式:源头是什么(文献、年份、编号)、复现了什么(只有公式)、没复现什么(叙述、图、表、样本内外的表现讨论、专有评述)。gtja191/LICENSE.md 里有一句相当克制的表述,大意是:不援引任何美国法下的抗辩理由来为这次复现开脱,立场更简单——只复现属于事实性数学的公式,研报的叙述、框架和图一概不要。
academic/LICENSE.md 则把重点放在了另一件事上:防止误解。它逐条列出 Sharpe 1964、Fama-French 1993 与 2015、Carhart 1997、Frazzini-Pedersen 2014 的出处,然后拉出一整段披露,说明这些实现只吃 OHLCV 面板,原始因子要用的基本面数据(账面权益、营业利润、资产增长)根本不在面板里,所以每个基本面输入都被换成了价格或成交量代理——比如 SMB 用 60 日平均成交额的负对数代替市值,HML 用 252 日的区间涨跌取负代替账面市值比。Hou-Xue-Zhang 的 q-factor 被列出来但没有单独实现,理由写得很直白:在只有 OHLCV 的设定下,它的投资腿和盈利腿与 RMW / CMA 重叠了。想要研究级序列的人,文件直接指向 Kenneth French 的公开数据库,并说明仓库只给链接、不拷数据文件。
这一段有一个非常实用的工程习惯:披露不只写在 LICENSE.md 里,每个模块的 notes 字段还会把同样的话再说一遍。文档会被人跳过,代码里的元数据不会。
三、声明是怎么落到代码里的
到这一步为止讲的都还是文档。这个项目真正把结构立住的地方,是让声明变成可执行的检查。
第一层是元数据即事实来源。每个 alpha 模块里有一个 __alpha_meta__ 字典字面量,registry.py 的文档写得很清楚:这个字典是 source of truth,_meta.yaml 只是由导出命令生成给外部消费者(wiki)用的,不在加载路径上;模块路径由 f"src.factors.zoo.{zoo_id}.{alpha_id_short}" 推导,永远不采信任何数据文件里的 py_module 字段。实际的字段长这样:
ALPHA_ID = "alpha101_001"
__alpha_meta__ = {
'id': 'alpha101_001',
'nickname': 'Kakushadze Alpha #1',
'theme': ['reversal', 'volatility'],
'formula_latex': 'rank(ts_argmax(SignedPower((returns<0)?stddev(returns,20):close, 2.), 5)) - 0.5',
'columns_required': ['close'],
'extras_required': [],
'requires_sector': False,
'universe': ['equity_us', 'equity_in', 'equity_kr'],
'frequency': ['1D'],
'decay_horizon': 5,
'min_warmup_bars': 25,
'notes': '',
}
formula_latex 存的是论文附录里那条公式本身,模块 docstring 里还有一行 arXiv 引用。有个容易看岔的细节:这个模块的文件名是 alpha_001.py,而 id 是 alpha101_001——注册表拼模块路径时取的是文件名去掉后缀的部分,再前置 zoo 目录名,id 只是元数据里的标识符,不参与定位;两者都必须匹配 ^[a-z][a-z0-9_]{0,31}$,不合规的直接进加载错误列表而不是让整个注册表崩掉。alpha101/LICENSE.md 明确要求:再分发这个子目录时,请保留每个模块 docstring 里的 arXiv 引用,保留 Kakushadze 101 Formulaic Alphas 这个显示名。
第二层是 notes 字段承载”我改了哪”。重实现公开公式最容易翻车的地方不是抄错,而是原公式用了你没有的算子或数据,你随手换了一个近似却没记录。这两份 LICENSE.md 都把这类偏差摊开写了:alpha101 那边,公式引用市值 cap 或行业分类时,要么代入降级值、要么用 requires_sector=True 把这个 alpha 门控起来,每一处降级都写进 __alpha_meta__ 的 notes;gtja191 那边列了一整节”算子可得性偏差”,WMA 近似成 decay_linear、SMA(x, n, m) 映射成 x.ewm(alpha=m/n, adjust=False).mean()、REGBETA 与 REGRESI 用 ts_cov(x, y, n) / ts_std(y, n) ** 2 与残差近似、FILTER 实现成 x.where(cond, np.nan) 让不满足条件的行传播 NaN 而不是悄悄填零、HIGHDAY / LOWDAY 用 0 基的 ts_argmax / ts_argmin 并按需重新对齐到报告的 1 基约定。基准指数序列拿不到时回退成当日 close 的横截面均值,同样记在 notes 里。
第三层是测试与 CI 门。agent/tests/factors/test_alpha_purity.py 是一个基于 AST 的纯度门,只允许 pandas、numpy、scipy、__future__、typing、math、dataclasses 这几个导入根,加上本仓唯一的算子入口 src.factors.base;os、sys、subprocess、socket、urllib、requests、httpx、pathlib、open、eval、exec、__import__ 这些名字在模块里任何位置出现都算违规,连 getattr 的字符串参数都查。它对模块级语句也有要求:除了导入、函数定义、模块 docstring,以及 ALPHA_ID 与 __alpha_meta__ 这两个赋值之外,顶层不许出现类定义、if 或任何调用。
守前视的是另一个文件 agent/tests/factors/test_lookahead.py,用的是扰动法而不是静态扫描:先在一块合成面板上把因子算一遍、记下某一行的取值,再把这一行之后隔开若干行起的整片数据(面板里每一列都算上)改成 NaN 或极端值重算一次,两次在那一行必须一致(容差 1e-9),否则就说明这个因子看了未来的数据。而”delta 只接受 d >= 1、不提供负向位移”这条更硬的约束不在测试里,在算子层——agent/src/factors/base.py 的 delta 遇到 d < 1 直接抛错,前视在这一层就写不出来。
tools/ci_grep_gates.sh 则是仓库级的安全底线,五道门顺序跑,任何一道失败就非零退出并点名文件。其中一道门直接服务于前面的商标立场:alpha101/LICENSE.md 提到,某个常与论文作者机构关联的商标字符串在这个仓库里一处都不出现,而这条约束正是由这个 grep 门强制的。看脚本会发现它扫的是 .py、.md、.html、.json 这些会被分发出去的文件,唯独放过讨论这条政策本身的内部规划文档目录——策略本身要写清楚,产物里不能带。其余几道分别管不安全的 yaml.load(、wiki 目录下的逐股票代码数据批量泄漏、废弃的时间 API 用法,以及集中化配置层之外的环境变量直读。
把三层连起来看,这套结构的逻辑是:文档写立场,元数据写偏差,测试和 grep 门保证前两者不被人不小心违反。这跟工程上做 代码安全审计 的思路是同一套——能自动查的事,别指望靠人自觉。
四、边界与代价:这套结构不管什么
这个设计不是免费的,也不是万能的。把它抄回自己项目之前,先看清它放弃了什么。
它不管”你能不能这么用”。 所有这些文件给的是来源披露和归属,不是法律结论。gtja191/LICENSE.md 甚至主动声明不援引任何法域下的抗辩理由,只陈述事实。你要判断能不能商用、能不能再分发、能不能对外提供服务,得读许可证原文,必要时找你自己的法务,以许可证原文为准。
它换来的是可用性上的损失。 为了守住纯函数与无前视这两条契约,因子模块被限制得很死:不能读文件、不能发网络请求、不能用表达式编译器、也没有缓存层。qlib158/LICENSE.md 里明写它放弃了上游 qlib 的长格式表达式树,改用宽表 DataFrame 面板加纯 compute(panel) 函数。这让代码好审、好测、好并行,代价是你想做跨截面的复杂依赖、想做增量缓存、想引入外部数据源,都得跳出这一层另建。
代理实现不等于原始因子。 academic 目录最需要盯的就是这一点。它给你的是 OHLCV 价格代理,不是学术论文里那些因子本身。你要是把它当成论文序列去做任何对照,结论从一开始就不成立——这也是那份 LICENSE.md 反复披露、并且在每个模块 notes 里再说一遍的原因。
它完全不管你与真实资金之间那一段。 因子库这几层是纯计算,往下走到 agent/src/trading/connectors/(12 家券商连接器子目录)就是另一个风险量级了:一旦你把凭据放进去,凭据的暴露面就从”你的开发机”扩大到整条 Agent 链路,包括日志、上下文、任何被模型看见的地方;下单指令一旦发出去通常不可撤销,没有”回滚一次交易”这种操作;程序化交易本身在不同司法辖区有不同的报备与合规义务。这套因子库的许可证结构一个字都没打算解决这些问题,能不能这么用一律以你所在司法辖区的监管要求与券商协议为准。
它也不管上游变了怎么办。 锁 commit 是好习惯,但锁完之后上游 Alpha158 若有修订,这边不会自动跟进,也没有承诺跟进。你把它当成一份 2026 年某个时间点的快照来用比较稳妥。
五、上手与避坑清单
别把因子库当成”项目自研的资产”来对外描述。 会踩是因为 README 和代码看起来浑然一体,很容易顺口说成”它内置了几百个自研因子”。避法是:任何对外材料里提到这几个 zoo,都按 NOTICE 的口径写清上游——Qlib 特征目录走 Apache 2.0,另外几组是对公开论文与研报公式的重实现。你要是在这上面说错话,风险是你自己的,不是上游的。
再分发子目录前,先把 LICENSE.md 的三条要求逐条对一遍。 会踩是因为很多人复制目录时只带 .py,把 LICENSE.md 和文件头注释当成噪声删了。alpha101/LICENSE.md 要求保留 docstring 里的 arXiv 引用、保留显示名、不要引入它刻意规避的商标串;qlib158 的每个文件头那行上游 commit 引用同理,删掉就等于破坏了 Apache 2.0 §4 的归属义务。避法是把这些文件纳入你自己的分发清单检查,别靠手动记。
新增自定义因子时,先读纯度门再动手。 会踩的典型是:你想在因子里读一个本地 CSV 或调一次接口,import pathlib 一写,test_alpha_purity.py 直接红。这不是测试太严,是这一层的契约就是纯函数。避法是把数据获取放到面板构建那一层,因子只接收 panel 这个字典,出去只返回一个宽表。
任何近似替换都必须写进 notes,哪怕你觉得等价。 会踩是因为半年后没人记得 WMA 被换成了线性衰减,某条 alpha 的行为与公式表对不上时就得从头 reverse。避法照抄 gtja191/LICENSE.md 的做法:偏差写在 LICENSE.md 里一次,写在每个受影响模块的 notes 里一次,两处都留。
别拿 _meta.yaml 当加载入口。 会踩是因为 registry.py 的模块 docstring 里提到过这么一类 yaml,容易被理解成配置源;实际上它写得很明白:_meta.yaml 是 export-manifest 导出给 wiki 这类外部消费者的产物,不在加载路径上(当前源码树里 zoo 各子目录也确实找不到这种文件)。真正的事实来源是 .py 里的 __alpha_meta__ 字典字面量,注册表是用 AST 扫出来的、扫的时候不导入模块。同一段还写了一句更硬的:任何数据文件里的 py_module 字段一概不采信,模块路径只由 zoo 目录名加 .py 文件名推出来。
接券商之前,先把凭据这条链路单独设计一遍。 会踩是因为 Agent 项目的默认习惯是”配置文件里塞环境变量就完事”,但 Agent 会把上下文喂给模型、会写日志、会被人截图。这个仓库的 CI 门里就有一条禁止在集中化配置层之外直读环境变量,可以当成一个提示:凭据的读取点越少越好。避法是先只跑纸面模式、把因子和回测这部分完全跑通再谈连接,并且明确到底谁有权限发出可执行的指令。关于权限该收到多紧,最小权限设计 里讲得更细。
收束
这套结构值得抄的地方,是它把一个通常靠人自觉的问题(这段代码从哪来、我改了什么、我不敢声称什么)拆成了三层可检查的东西:根 NOTICE 给全景,子目录 LICENSE.md 给逐条立场,__alpha_meta__ 的 notes 加纯度门和 grep 门保证代码不会偷偷偏离声明。你的项目里但凡有从论文、研报、别的仓库搬来的东西,这三层都能照搬。
接下来该读哪个文件,按你的目的选:想判断能不能再分发,从根 NOTICE 读到对应子目录的 LICENSE.md;想理解因子怎么被加载和校验,读 agent/src/factors/registry.py 的模块 docstring,它把设计契约写在最前面;想知道自己写的因子会被怎么卡,读 agent/tests/factors/test_alpha_purity.py 的白名单和 tools/ci_grep_gates.sh 的门列表。
最后重复一遍前面那句:本文只拆工程实现与归属结构,不涉及任何投资判断,历史表现不代表未来;能不能用、怎么用,以许可证原文、以及你所在司法辖区的监管要求与券商协议为准。
本篇属于一个把开源个人交易 Agent 项目 Vibe-Trading逐层拆开讲的系列,整体地图见 Vibe-Trading 是什么:HKUDS 这个开源交易 Agent 项目的工程全景与边界;沿着这条线往下,还可以看 拆开 Vibe-Trading 开源项目的因子引擎:算子层、注册表与批量跑分 和 Vibe-Trading 影子账户:从交易记录抽规则到生成回测代码的流水线。