01

这是什么?
当你输入一句话,背后发生了什么

从一条命令出发,认识 QuantaAlpha 的整个世界

你遇到了什么问题?

量化投资 的核心挑战只有一句话:在噪声中找到真正的信号。 股票市场每天产生海量数据,大部分都是随机涨跌的"噪声"——真正能预测未来走势的规律,往往藏得很深。

😓

传统方式

研究员手工阅读研报、设计公式、反复调参。一个有价值的因子可能需要数周甚至数月。

QuantaAlpha 的方式

你只需告诉系统一个方向,剩下的——提假设、写代码、跑回测、筛选结果——全部交给 AI 自动完成。

💡
QuantaAlpha 是什么?

一个由 LLM 驱动的 Alpha 因子 挖掘系统。你输入一个探索方向,它自动产出一批经过 回测 验证的因子,存入因子库等待下游模型使用。

什么是 Alpha 因子?

想象一位天气预报员。他不能控制明天的天气,但他能从气压、湿度、云图中找到"预兆"——这些预兆就是他的"信号"。

Alpha 因子 就是量化世界的"预兆"。它是一个数学公式,输入是历史价格、成交量等数据,输出是一个数字信号——这个信号越高,预示股票未来可能表现越好。

📐
一个真实的例子:动量因子

(昨日收盘 - 5日前收盘) / 5日前收盘

这个公式计算了最近5天的涨幅。背后的假设是:近期上涨的股票,短期内可能继续上涨。 这就是一个最简单的 动量 因子。

1
提出 假设

比如:"高成交量配合价格上涨,信号更可靠"

2
转化为 表达式

把文字描述变成可计算的数学公式

3
回测验证,看 IC 值

IC 值衡量因子预测能力:越高越好

一句命令,触发了什么?

当你运行 python launcher.py mine --direction "价量动量因子", 系统在幕后自动完成了 5 个环节:

👤
🗺️
Planning
🤖
AlphaAgent
📊
Qlib回测
🏦
因子库
点击"下一步"开始
Step 0 / 5

代码翻译:launcher.py 入口

你不需要看懂每一行代码,但看懂"入口"很重要。下面是 launcher.py 文件开头的用法注释——它告诉你这个工具能做什么:

launcher.py """ Usage: python launcher.py mine --direction "price-volume factor mining" python launcher.py mine --direction "momentum reversal" --config configs/experiment.yaml python launcher.py backtest --factor-source alpha158_20 """
逐行解读
用法一(核心):启动因子挖掘,--direction 后面跟你想探索的研究方向。方向用自然语言写,比如"价量动量因子"。
用法二(进阶):同时指定一个配置文件,控制实验参数(比如回测时间段、迭代次数)。
用法三(回测模式):对已有的因子集合(如 alpha158)重新跑回测,不做新的挖掘。
🎯
关键设计:方向用自然语言描述

--direction 是你和系统对话的界面。你不需要写公式、不需要改代码,只需要告诉系统"想往哪个方向找"——剩下的交给 LLM 来理解和展开。

组件对话:谁在幕后工作?

来看一次真实的挖掘过程中,各个组件之间是怎么"说话"的:

0 / 5 messages

理解检测

试试看你吸收了多少——3 道题,每题只选一个最佳答案。

Q1:你输入 --direction "价量动量因子" 后,系统第一步会做什么?

Q2:Alpha 因子的作用是什么?

Q3:如果你想让 QuantaAlpha 挖掘和财报相关的因子,你应该怎么做?

02

五位主角——谁在做什么?

认识 QuantaAlpha 的五个核心组件,理解每个人的职责,这样你就能告诉 AI "把这个逻辑放在 X 里"。

想象一家量化研究所

QuantaAlpha 不是一段"从上到下跑一遍"的脚本——它更像一家分工明确的量化研究所。 每位成员各司其职:有人负责提灵感,有人写代码,有人做实验,有人管质量,还有一位导师统筹全局。 五个组件,五种角色,缺一不可。

认识这五位"主角"之后,你就能准确地告诉 AI: "这段逻辑应该放在 FactorParser 里",而不是含糊地说"放在某个地方"。

🧠
HypothesisGen

提出研究假设
读取历史记录,调用 LLM,产出有具体内容的研究方向。就像研究所里那位灵感涌现的科学家,专门负责写"灵感笔记本"。

⚙️
FactorParser

把假设变成可运行的代码
把自然语言假设翻译成 Qlib 因子表达式,写出 factor.py。像把建筑师图纸翻译成施工图的工程师。

📊
FactorRunner

运行代码,拿到真实回测结果
执行 factor.py,合并数据,调用 Qlib 做历史回测,返回 IC、年化收益等指标。

🔄
EvolutionController

决定下一轮探索哪个方向
维护轨迹池,选出最优父代,触发变异或交叉策略。就像围棋教练,看完每盘棋决定下一步怎么练。

🛡️
FactorRegulator

确保生成的因子不是垃圾
在因子进入回测前做5重质量检查:冗余度、长度、自由参数、特征数量、语义一致性。

架构图——谁向谁汇报?

点击任意组件,查看它的详细职责说明。注意最顶层的 settings.py——它是整个研究所的"人事档案",决定每个岗位由谁来担任。

配置层 · Configuration
⚙️ settings.py
主循环 · Main Loop
🔁 AlphaAgentLoop
核心组件 · Core Components
🧠 HypothesisGen ⚙️ FactorParser 📊 FactorRunner 🔄 EvolutionController 🛡️ FactorRegulator
← 点击上方任意组件,查看它的职责说明
💡
数据流向

HypothesisGen 产出假设 → FactorParser 写代码 → FactorRegulator 质检 → FactorRunner 回测 → EvolutionController 决定下一轮方向。这个循环会自动运行数百次,每次都在轨迹池里留下记录。

settings.py 的秘密——依赖注入

这是 QuantaAlpha 最聪明的设计之一:五个组件的具体实现类,全部通过字符串路径写在 settings.py 里。 主循环从不硬编码"用哪个类"——它只读配置,再用 Python 的 importlib 动态加载。

想换一个更好的 HypothesisGen?只改一行配置,不动任何业务逻辑。这就是依赖注入的魔法。

settings.py · line 48–57 class AlphaAgentFactorBasePropSetting(BasePropSetting): """Main experiment: LLM-driven factor mining.""" model_config = ExtendedSettingsConfigDict( env_prefix="QLIB_FACTOR_", protected_namespaces=() ) scen: str = "quantaalpha.factors.experiment.QlibAlphaAgentScenario" hypothesis_gen: str = "quantaalpha.factors.proposal.AlphaAgentHypothesisGen" hypothesis2experiment: str = "quantaalpha.factors.proposal.AlphaAgentHypothesis2FactorExpression" coder: str = "quantaalpha.factors.qlib_coder.QlibFactorParser" runner: str = "quantaalpha.factors.runner.QlibFactorRunner" summarizer: str = "quantaalpha.factors.feedback.AlphaAgentQlibFactorHypothesisExperiment2Feedback"
逐行解读
model_config — 读取环境变量前缀为 QLIB_FACTOR_,可以用环境变量覆盖任何配置项
scen — 实验场景类,定义"一次实验"的数据结构
hypothesis_gen — 👉 HypothesisGen 的完整类路径,这里写谁,主循环就用谁来生成假设
coder — 👉 FactorParser 的路径,负责把假设翻译成 factor.py
runner — 👉 FactorRunner 的路径,负责执行回测
summarizer — 实验完成后生成反馈摘要,供下一轮 HypothesisGen 参考
QlibFactorRunner 来自 quantaalpha/factors/runner.py — 执行 Docker 环境中的因子回测,结果写入 mlflow
QlibFactorParser 来自 quantaalpha/factors/qlib_coder/ — 使用 CoSTEER 迭代写出可执行代码
AlphaAgentHypothesisGen 来自 quantaalpha/factors/proposal.py — 核心方法 gen(trace) 接收Trace,返回假设对象
🔑
关键洞察

每个字段存的是字符串,不是类本身。主循环用 importlib.import_module() 在运行时动态加载。 这意味着你可以写自己的 HypothesisGen,只要把类路径填进去,整个系统就会用你的版本——不需要改任何其他代码。

组件对话——一个假设是怎么变成代码的

让我们跟着一个真实假设,看它如何在五个组件之间流转,最终变成一条有 IC 值的回测结果。 点击"下一条"逐步播放,或点击"全部展示"一次看完。

0 / 6 messages
📌
runner.py 的真实注释

QlibFactorRunner 在 Docker 容器里运行,每个因子的 factor.py、config.yaml、价量数据都放在独立文件夹,结果通过 mlflow 持久化——这就是那段注释说的 "Everything in a folder"

测验——把职责配对到正确的组件

把上方的组件名称拖拽到下方对应的职责描述里。全对了说明你已经认识这五位主角了!

HypothesisGen
FactorParser
FactorRunner
EvolutionController
FactorRegulator
根据回测历史和探索方向,调用 LLM 提出新的研究假设
Drop here
把自然语言假设翻译成 Qlib 表达式,写出可执行的 factor.py
Drop here
并行执行所有因子代码,合并结果,调用 Qlib 做历史回测
Drop here
管理轨迹池,决定下一轮用变异还是交叉策略
Drop here
检查因子是否重复、太复杂、或与假设不一致,守护因子库质量
Drop here
🎯
下一步

认识了这五位主角,下一个模块我们将跟随一轮完整的因子挖掘旅程——从"提出一个假设"到"拿到回测结果",每一步都有真实代码。

03

一轮挖掘的旅程

5步循环深度解析——跟着一个假设,从想法到因子值,再到反馈入库

想象一条汽车工厂流水线

汽车工厂里,一辆车从底盘到出厂,要经过焊接、喷漆、装配、检测、验收5道工位。每个工位只做自己的事,产出交给下一位,整条线循环不止。

AlphaAgentLoop 正是这样一条"因子工厂"流水线。每跑一圈,就完成1次完整的探索——

💡
每5步 = 1个 Loop

提假设 → 造表达式 → 写代码跑计算 → 回测 → 反馈归档,环环相扣,缺一不可

🔄
每轮产出 0—N 个候选因子

代码可能失败、因子可能被质量门控过滤,最终存活的才进入因子库

🧠
Trace 贯穿始终

每轮的假设、结果、反馈都追加到 Trace,下一轮 LLM 提假设时会读取它,形成真正的记忆闭环

🏭
本模块目标

我们将跟踪"一个假设从想法到因子值"的完整路径,理解每一步做了什么、交出了什么、为什么这样设计。

Step 1 & 2:从想法到因子表达式

流水线的前两个工位负责把一个模糊的"研究直觉"变成一段精确的数学表达式。

1
factor_propose() — 提假设

调用 Trace(最多6条历史记录)作为上下文,由 HypothesisGen 向 LLM 提出新假设。 输出一个 AlphaAgentHypothesis 对象,包含 hypothesis(假设陈述)、observation(市场观察)、justification(逻辑依据)、specification(实现规格)四个字段。

2
factor_construct() — 造表达式

AlphaAgentHypothesis2FactorExpression.convert() 把假设拆解为 FactorTask 列表。每个 FactorTask 包含: factor_name、factor_description、factor_formulation(数学公式)、factor_expression(可执行表达式)。 质量门控 FactorRegulator 在此触发——不合规的表达式直接拦截,不进入后续步骤。

🧠
Trace:LLM 的实验日志

Trace 是一个列表,每条记录是 (hypothesis, experiment, feedback) 三元组。LLM 每次提假设前都会读取这份日志——它知道上几轮探索了什么、结果怎样,从而自然地避开已走过的死路,向未探索的方向前进。

loop.py · L141–149 @measure_time @stop_event_check def factor_propose(self, prev_out: dict[str, Any]): """Propose hypothesis as the basis for factor construction.""" with logger.tag("r"): idea = self.hypothesis_generator.gen(self.trace) logger.log_object(idea, tag="hypothesis generation") self._last_hypothesis = idea return idea
逐行解读
@measure_time — 自动计时,记录这一步耗时多少秒
@stop_event_check — 收到停止信号时可以优雅中断,不强制杀进程
hypothesis_generator.gen(self.trace) — 把 Trace(历史记忆)传给 LLM,让它提出新假设
log_object(idea, tag="hypothesis generation") — 把假设写入结构化日志,便于事后审查
self._last_hypothesis = idea — 暂存本轮假设,后续步骤通过 prev_out 字典传递

Step 3:代码执行的秘密

这是5步中技术细节最密集的一步。factor_calculate() 会为每个 FactorTask 生成一个独立的 factor.py,在隔离的 workspace 文件夹里运行它。

📥
输入:daily_pv.h5

HDF5 格式的日线价量数据。factor.py 通过符号链接读取,不复制原始文件,节省磁盘空间。

⚙️
执行:CoSTEER 自修复

代码失败时,CoSTEER 分析错误信息,自动修改代码重试,最多若干次,不让整个流程崩溃。

📤
输出:result.h5

每个因子输出独立的 result.h5,存储因子值——MultiIndex(datetime × instrument)格式的二维表,是下一步回测的输入。

factor.py · L104–113 def execute(self, data_type: str = "Debug") -> Tuple[str, pd.DataFrame]: """ execute the implementation and get the factor value by the following steps: 1. make the directory in workspace path 2. write the code to the file in the workspace path 3. link all the source data to the workspace path folder if call_factor_py is True: 4. execute the code else: 4. generate a script from template to import the factor.py 5. read the factor value from the output file """
5步执行流程
① 为这个因子创建专属 workspace 目录——隔离环境,防止文件冲突
② 把 LLM 生成的因子代码写入该目录下的 factor.py
③ 用符号链接把 daily_pv.h5 接入 workspace,无需复制原始数据
④ 直接执行 factor.py(或生成导入脚本间接调用)
⑤ 从输出文件读取因子值——这就是 result.h5
🔁
失败是正常的

LLM 生成的代码不可能每次都完美。CoSTEER 的存在让"失败→分析→修复"变成自动化循环。就像一位会自我纠错的实习生——不完美,但会学习。

Step 4 & 5:回测与反馈闭环

前3步把假设变成了因子值,后2步负责"审判"这些因子——量化它们的预测能力,并把结论写回记忆,指导下一轮探索。

📝
factor.py
💾
daily_pv.h5
🔢
result.h5
📦
combined.parquet
📈
Qlib回测
点击"下一步"开始追踪数据流
IC / ICIR 因子预测能力——与未来收益的相关性及其稳定性,是最核心的评价指标
ARR 策略年化收益率——使用 TopkDropoutStrategy(持仓50只,每次换5只)模拟的年化回报
MDD 最大回撤——策略在历史区间内从高点到低点的最大亏损幅度,衡量风险
LightGBM 将所有因子值作为特征,训练一个股票收益预测模型,驱动选股决策
loop.py · feedback() def feedback(self, prev_out: dict[str, Any]): feedback = self.summarizer.generate_feedback( prev_out["factor_backtest"], prev_out["factor_propose"], self.trace ) with logger.tag("ef"): logger.log_object(feedback, tag="feedback") self.trace.hist.append(( prev_out["factor_propose"], prev_out["factor_backtest"], feedback ))
反馈闭环解读
generate_feedback(...) — LLM 同时看到:回测指标 + 本轮假设 + 历史 Trace,生成自然语言反馈
prev_out["factor_backtest"] — 本轮回测对象(含 IC、ARR、MDD 等指标)
prev_out["factor_propose"] — 本轮提出的假设对象
self.trace.hist.append(...) — 把(假设, 实验, 反馈)三元组追加到 Trace 历史
下一轮 factor_propose() 调用 gen(self.trace) 时,会读到这条新记录
📚
因子库:有价值的成果永久保存

通过质量门控的因子会被保存到 all_factors_library.json(FactorLibrary),累积成为后续模型训练的知识资产。

测验:追踪一个假设

三道场景题,检验你对5步循环的理解。

场景

AlphaAgent 刚运行完第3轮,IC 一直在 0.02 左右,没什么进步。你想让下一轮换个角度探索。

最有效的做法是?

场景

factor.py 运行失败了(比如 LLM 生成了含语法错误的代码)。

系统会怎么做?

数据结构

Step 3 执行完毕,workspace 目录下出现了 result.h5。

result.h5 里存的是什么?

04

进化的秘密

从 Original 到 Mutation 到 Crossover——理解 EvolutionController 如何把随机探索变成有记忆的定向搜索

为什么不用随机搜索?

想象你在培育冠军赛马。你不会每次都从马圈里随机挑一匹野马配种——你会找到跑得最快的那两匹,让它们的后代继承双方的优良基因。几代之后,马群整体的速度就会远超随机配种的结果。

量化因子挖掘面临同样的问题:因子的可能空间无穷大,如果每次都从头随机生成,大部分尝试都是在浪费时间。进化算法的核心思想是:记住什么管用,然后在这个基础上继续改进。

🎲
纯随机搜索

每次生成全新因子,没有记忆,没有方向。就像每次都从野生马里随机挑——偶尔运气好,但效率极低。

🧬
定向进化(QuantaAlpha 的做法)

每一轮都记录哪些因子表现最好,下一轮在这些"优秀父代"的基础上探索变体。搜索空间逐渐收窄到有价值的区域。

📚
TrajectoryPool 是进化的记忆

每个因子的表现都被存入轨迹池,就像赛马的血统档案。EvolutionController 每轮都会查阅这份档案,决定下一步怎么走。

💡
在软件里,这叫进化算法

Evolutionary Algorithm——一个能自我改进的系统。它不需要人类专家告诉它"往哪个方向改",而是通过积累的实验历史,自动发现有效的探索路径。QuantaAlpha 把这个思想用在了量化因子挖掘上。

三个阶段:Original → Mutation → Crossover

每一轮进化,EvolutionController 会根据历史进展,选择三种策略之一。点击"下一步"追踪一次完整的进化轮次数据流。

🌱
Original — 白板探索

LLM 从零开始提出全新的因子假设。没有任何父代约束,代表"种群初始化"阶段。通常在前几轮使用,建立初始基因库。

🔧
Mutation — 微调变异

取当前最优因子,让 LLM 在它的基础上微调:延长/缩短时间窗口、替换运算符、改变价量变量组合。像给冠军马的后代做定向训练。

🔀
Crossover — 基因杂交

选两个表现都很好的因子,让 LLM 把它们的子表达式组合在一起。就像让两匹冠军马配种,期望后代继承双方优点。

🎯
Evolution
Controller
💡
Hypothesis
Gen
🔁
Alpha
AgentLoop
📚
Trajectory
Pool
🔀
Crossover
Result
点击"下一步"追踪一轮完整进化的数据流

代码解读:进化循环的核心逻辑

这两段代码来自 quantaalpha/pipeline/factor_mining.py,是整个进化系统的骨架。左边是真实代码,右边是对应的中文解读。

factor_mining.py · run_evolution_loop def run_evolution_loop(cfg, trace): controller = EvolutionController(cfg) for round_idx in range(cfg.evolution.max_rounds): tasks = controller.schedule_tasks(trace) results = _run_tasks_parallel(tasks, cfg) for task, result in zip(tasks, results): trace.update_with_result(task, result)
逐行解读
def run_evolution_loop(cfg, trace): — 定义主进化函数,接收配置(cfg)和共享记忆(Trace
controller = EvolutionController(cfg) — 创建"指挥官",它根据历史决定每轮跑什么任务
for round_idx in range(cfg.evolution.max_rounds): — 循环指定轮数(默认 3 轮),每轮是一次完整的进化迭代
tasks = controller.schedule_tasks(trace) — 问指挥官:这一轮应该跑哪些实验?返回任务列表
results = _run_tasks_parallel(tasks, cfg) — 同时并行运行所有任务,充分利用计算资源
trace.update_with_result(task, result) — 把每个任务的结果写回共享记忆,供下一轮决策使用

下面是 schedule_tasks 的核心逻辑——它决定"这一轮用哪种策略":

factor_mining.py · schedule_tasks def schedule_tasks(self, trace): phase = self._determine_phase(trace) if phase == "original": return self._create_original_tasks() elif phase == "mutation": parents = trace.get_top_factors(n=1) return self._create_mutation_tasks(parents) else: # crossover parents = trace.get_top_factors(n=2) return self._create_crossover_tasks(parents)
逐行解读
phase = self._determine_phase(trace) — 根据 Trace 历史(跑了几轮、通过了多少因子)判断当前应处于哪个阶段
if phase == "original": — Original 阶段 → 不需要父代,直接从零生成新假设任务
return self._create_original_tasks() — 返回全新任务列表,LLM 用白板思维探索
parents = trace.get_top_factors(n=1) — Mutation 阶段 → 取 IC 最高的 1 个父代因子
return self._create_mutation_tasks(parents) — 把这个最优因子交给 LLM,让它生成变异版本
parents = trace.get_top_factors(n=2) — Crossover 阶段 → 取 IC 最高的 2 个父代因子
return self._create_crossover_tasks(parents) — 把两个父代交给 LLM,让它合并各自的子表达式
🔑
关键设计:父代数量决定阶段类型

注意 get_top_factors(n=1)get_top_factors(n=2) 的区别——这一个数字的差异,决定了是"单亲变异"还是"双亲杂交"。Mutation 只需要一个榜样,Crossover 需要两个互补的来源。

进化委员会开会——Mutation 阶段现场

看看当系统进入 Mutation 阶段时,这几个组件之间是怎么协作的。点击"下一条"逐步播放,或点击"全部展示"一次看完。

0 / 9 messages
🛡️
FactorRegulator 也参与进化过程

注意消息 8:即使在 Mutation 阶段生成的因子,也要经过 FactorRegulator 的质检。重复的因子(AST 相似度过高)会被直接拒绝,确保每次变异都是真正有价值的探索。

进化为什么有效?——以及你学到了什么

🧬
进化 vs 随机搜索

随机搜索每次都是在整个因子空间里抓彩票,而进化搜索利用 TrajectoryPool 积累的知识,把探索方向不断收窄到"已知有价值的邻域"。每一轮,搜索空间都在收缩,而不是保持不变。这就是为什么经过几轮进化后,新发现的因子质量会系统性地高于随机生成的结果——不是运气,是超参数驱动的定向搜索。

Original 建立初始种群 — LLM 自由探索,不受历史约束,覆盖广泛的假设空间
Mutation 单点改进 — 在最优因子的邻域深挖,微调参数和运算符,寻找局部最优
Crossover 优势融合 — 结合两个不同维度的优质因子,可能跳出局部最优,发现全新组合
TrajectoryPool 进化的记忆 — 每轮成果的积累,让下一轮的决策越来越准确,形成良性循环

测验:进化机制你掌握了吗?

三道场景题,检验你对进化循环的理解。

如果第一轮跑了 3 个原始因子,最佳 IC 是 0.05,第二轮会发生什么?

Crossover 阶段需要几个父代因子?

你发现系统运行了很多轮但效果没有提升。你会怎么调整进化参数?

🎯
你现在掌握了什么

Original → Mutation → Crossover 三阶段进化循环;EvolutionController 如何用 TrajectoryPool 驱动父代选择;以及为什么定向进化比随机搜索在因子挖掘中更高效。下一个模块将深入探索因子质量管控——FactorRegulator 的五道质检门控。

05

质量守门员
The Quality Gatekeeper

每一个因子在进入回测之前,都必须通过 FactorRegulator 的五道安检。了解这些规则,你就能更好地引导 AI 生成高质量的候选因子。

为什么需要质量门控?

想象一座繁忙机场的安检通道。每一位旅客——不管是外国政要还是背包客——都必须通过同样的安检流程,没有例外,没有快速通道。 QuantaAlpha 里,每一个 LLM 提出的候选因子也是如此——在进入昂贵的回测之前,必须通过 FactorRegulator 的五道安检。

🗑️

没有质量门控会怎样?

LLM 会反复生成"换汤不换药"的重复公式,浪费大量回测算力,同时让因子库充满冗余,最终污染所有下游分析。

🛡️

有了质量门控

低质量因子在进入昂贵回测之前就被拦截,只有真正新颖、结构合理的候选因子才能"登机",保持因子库的高价值密度。

🔍
FactorRegulator 是因子从提案到测试的最后防线

它位于 quantaalpha/factors/regulator/factor_regulator.py, 在每一个新因子提交给 Qlib 回测之前执行检查。通过全部五项检查才放行,一项不过即告拒绝。

五道安检——逐条解析

以下是 is_expression_acceptable() 方法中的五项判断条件。每一项都对应一类常见问题,缺少任何一项,因子库就会被某种"坏因子"悄悄污染。

1
去重检测 cond1

与已有因子共享的最大公共AST子树不超过 8 个节点。 防止"换汤不换药"——即使公式写法不同,只要数学逻辑相同,就会被识别为重复。

2
自由参数比例 cond2

公式中的"魔法数字"(自由参数)占比不超过 50%。 一个到处是神秘数字的公式如 0.382 * close + 0.618 * volume * 1.41 很可能是在过拟合历史数据。

3
变量多样性 cond3

独特变量(不同的运算符和函数)占所有符号的比例不超过 50%。 这保证公式有足够的"逻辑骨架"——不是一堆随机嵌套,而是有结构意义的表达式。

4
符号长度 cond4

因子表达式字符串长度不超过 300 个字符。 过于复杂的嵌套表达式不仅难以解释,而且几乎肯定是在记忆历史噪声。简洁的因子往往更稳健——想想最经典的动量因子,不超过 30 个字符。

5
基础特征数量 cond5

使用的原始基础特征(如 close、volume、high)不超过 6 个。 一个依赖太多不同数据维度的因子往往是"拼凑"出来的,而不是捕捉到了真正的市场逻辑。

💡
所有五项必须同时满足

代码是 return cond1 and cond2 and cond3 and cond4 and cond5——任何一项不过,因子直接被拒绝,不会给出部分通过的机会。这是一个"AND 门",不是"OR 门"。

深度解析:AST 去重是怎么工作的?

五项检查中,最技术也最精妙的是第一项——AST 去重。 普通的字符串比较很容易被骗:close/Ref(close,5) - 1(close - Ref(close,5)) / Ref(close,5) 是同一个动量公式,但字符串完全不同。 AST 比较则能看穿表面差异,找到数学本质。

食谱比喻: 想象每个因子是一份菜谱。AST 去重不是对比菜名或配料表格式,而是逐步骤比对操作流程——如果两份菜谱共享了超过 8 个相同的操作步骤,系统就认定它们"本质上是同一道菜"。

示例:将 (close - Ref(close,5)) / Ref(close,5) 解析为 AST

÷ (根节点:除法)
(减法)
close
Ref(close,5)
Ref(close,5) (右叶节点)

蓝色框 Ref(close,5) 在树中出现了 两次(左子树的右叶 + 右子树),这是一个内部重复。 对外去重时,系统会比较整棵树与已有因子的最大公共子树大小。

factor_ast.py def find_largest_common_subtree(root1, root2): if root1 is None or root2 is None: return 0 if root1.node_type != root2.node_type: return 0 if root1.value != root2.value: return 0 size = 1 for c1, c2 in zip(root1.children, root2.children): size += find_largest_common_subtree(c1, c2) return size
逐行解读
第 1 行:比较两棵因子表达式树,返回它们共享多少个节点
第 2-3 行:如果任意一棵树是空的,它们共享 0 个节点
第 4-5 行:如果节点类型不匹配(比如一个是函数、另一个是运算符),直接不匹配
第 6-7 行:如果节点值不匹配(比如 Ref 对比 Mean),直接不匹配
第 8 行:当前节点匹配!计分从 1 开始
第 9-10 行递归检查所有子节点,累加匹配数量
第 11 行:返回这两棵子树的总重叠得分
🎯
如果得分 > 8,新因子被拒绝

现有因子已经覆盖了这片数学领域。继续放行只会浪费算力,并增加因子库中的噪声。 这个阈值(默认 8)可以通过配置调整——阈值越大,限制越严格,因子库越干净但也越小。

安检现场:一次真实的检查过程

来看看当 HypothesisGen 提交两个候选因子时,FactorRegulator 是怎么处理的:

0 / 11 messages

实际意义:为什么这对你很重要?

⚖️
质量门控的双重价值

① 节省算力:拒绝一个候选因子的成本接近于零,而一次完整回测需要分钟到小时级的计算时间。早拒绝 = 大省算力。

② 保持因子库的信息密度:下游的IC去重和因子组合优化,需要一个"干净"的因子库作为基础。如果因子库里 80% 是重复,后续所有分析的有效性都会大打折扣。

作为使用这个系统的人,你可以通过调整以下配置参数来控制质量门槛的松紧。了解这些参数的含义,能帮你在"严格"和"宽松"之间找到合适的平衡:

duplication_threshold: 8 去重树节点阈值——越大越严格(允许更少的结构相似性)
symbol_length_threshold: 300 符号长度上限——降低此值可以强制因子更简洁
base_features_threshold: 6 最大基础特征数——限制因子依赖的原始数据维度
free_args_ratio: < 0.5 自由参数比例上限——防止因子中充满"魔法数字"
🛠️
你能用这些知识做什么?

当你发现系统一直在拒绝因子时,先看拒绝原因。如果大多数是 cond1(AST重叠),说明 LLM 在生成同质化的公式——你应该在提示词中引导它探索不同的算子组合、不同的时间窗口或不同的价量关系,而不是盲目降低阈值。

理解检测

3 道题,每题选一个最佳答案。

Q1:你发现系统一直拒绝 LLM 生成的因子,大多是因为 AST 重叠。你会怎么做?

Q2:为什么 FactorRegulator 要限制公式长度(符号长度 ≤ 300)?

Q3:FactorRegulator 的哪项检查最能防止"换汤不换药"的重复因子?

06

如何驾驭它

从指挥台到配置文件——学会运行、调参、排错,真正把 QuantaAlpha 用起来

你现在站在指挥台上

前五个模块,你一直在机器内部探索:看懂了主角、跟完了循环、理解了进化、见识了质量门控。 现在是时候走出机器间,走进指挥台

🎼
你的控制台:configs/experiment.yaml

这个文件是你和 QuantaAlpha 对话的主界面。不需要改代码,只需要调整这里的参数,就能控制系统挖掘的广度、深度、质量标准。

⌨️
你的启动命令

一切从这一行开始:

终端 / Terminal python launcher.py mine \ --direction "量价动量" \ --config_path configs/experiment.yaml
🚀
三行命令,启动一台自动挖掘 alpha 因子的机器

launcher.py 是整个系统的入口,--direction 是你给 LLM 的研究方向(自然语言即可),--config_path 指向控制台文件。 改变 --direction 就像告诉指挥家"今晚演莫扎特",改变 experiment.yaml 就像决定乐团规模和排练轮数。

launcher.py 读取 .env 环境变量(API Key 等),然后调用 cli.py 执行主流程
cli.py 解析命令行参数,读取 experiment.yaml,构建 EvolutionController 并启动 factor_mining.main()
experiment.yaml 你唯一需要编辑的文件——控制探索广度、进化轮数、质量门控开关等所有关键参数

控制面板——你最需要了解的参数

configs/experiment.yaml 有数十个参数,但真正需要调整的只有这几组。 下面是配置文件的真实结构,配上每个参数的中文解释。

configs/experiment.yaml(核心节选) planning: num_directions: 2 # 论文使用 10 direction_template: "价量因子挖掘,专注于{direction}" execution: max_loops: 2 # 论文使用 5 evolution: enabled: true max_rounds: 3 factor: factors_per_hypothesis: 1 # 论文使用 3 quality_gate: redundancy_enabled: true complexity_enabled: true
逐参数解读
planning.num_directions — 每次运行要探索几个研究子方向。2 个方向 = 2 条并行探索线。数字越大,覆盖越广,但 API 消耗也越多。
direction_template — 发给 LLM 的方向模板,{direction} 会被你的 --direction 参数替换。可以用来限定 LLM 的探索语境。
execution.max_loops — 每个方向运行几轮完整的"假设→构建→测试→反馈"循环。每一轮都消耗 LLM 调用,也都会有新的因子产出(或被拒绝)。
evolution.enabled — 是否启用进化框架(Original→Mutation→Crossover 循环)。关闭后系统仅做随机探索,不会从历史结果中学习和改进。
evolution.max_rounds — 进化循环的总轮数(含 Original 轮)。第 0 轮是原始探索,后续轮是变异和交叉。
factor.factors_per_hypothesis — 每个假设生成几个因子。设为 3 时,每轮每个方向最多产出 3 个候选因子,探索密度更高。
quality_gate.redundancy_enabled — 是否运行 AST 去重检查。关闭后相似因子会进入因子库,可能导致因子库中有大量冗余。
quality_gate.complexity_enabled — 是否强制执行符号长度和基础特征数量的限制。关闭后可能产生极度复杂的因子,容易 过拟合
💡
代码默认值 vs 论文配置

你可能注意到代码里的默认值(2, 2, 1)比论文中的配置(10, 5, 3)小很多。 这是有意为之的——开发者用小值作为快速调试的默认配置。 完整复现论文结果需要使用论文中的 超参数, 详见论文 Appendix B。

实战调参指南:三种场景

根据你的目标选择合适的配置。没有"正确配置",只有"适合当前目标的配置"。

1
场景一:快速原型验证(低成本)

目标:确认系统能跑通,花最少的 API 配额。

num_directions: 1 只跑一个方向,最小化 LLM 调用
max_loops: 2 每个方向只跑2轮循环,够验证流程
evolution.enabled: false 关闭进化,跳过 Mutation/Crossover
factors_per_hypothesis: 1 每个假设只生成1个因子

适用时机:新环境首次安装测试、Backtest 数据路径刚配好、API Key 刚到位。 只要系统能跑通一个完整的 propose→build→test→feedback 循环,基础设施就正常了。

2
场景二:标准探索(日常挖掘)

目标:探索一个新的研究方向主题,平衡效果与成本。

num_directions: 3 三个子方向并行探索,覆盖更广
max_loops: 3 每方向3轮循环,让 Trace 有内容可学习
evolution.enabled: true 启用进化,让系统从好因子中学习
evolution.max_rounds: 3 Original + Mutation + Crossover 各一轮
factors_per_hypothesis: 2 每假设2个因子,提高单轮产出

适用时机:尝试新的方向主题(比如"日内成交量结构")、常规周期性挖掘、IC 结果评估研究。

3
场景三:论文复现(全功率)

目标:复现论文中报告的结果,需要大量计算资源。

num_directions: 10 论文配置:10个方向并行探索
max_loops: 5 每方向5轮完整循环
evolution.enabled: true 必须启用进化框架
evolution.max_rounds: 5 更多进化轮次,让优秀因子充分迭代
factors_per_hypothesis: 3 每假设3个因子,最大化探索密度
⚠️

这是论文中使用的配置,需要大量 LLM API 调用和计算资源。 建议先用场景一确认系统正常运行后,再逐步扩大配置规模。

从命令到结果:完整数据流

你输入一条命令,背后触发了12个步骤。点击"下一步"跟随数据流,理解每个环节交出了什么、传给了谁。

⌨️
用户 / CLI
🔄
EvolutionController
🤖
AlphaAgentLoop
📊
结果库 / Qlib
点击"下一步"开始追踪完整数据流
Step 0 / 12
📦
最终产物:factors_df.parquet

整个流程的终点是一个 Parquet 文件,存储所有通过质量门控的因子。这个文件可以直接被量化模型读取,作为选股特征输入。 你挖到的因子就在这里——打开它,读懂它,才是理解 QuantaAlpha 输出的最后一步。

当出现问题时——排错指南

这些是最常见的四种故障模式,以及对应的排查思路。

🛡️
因子大量被 FactorRegulator 拒绝

可能原因:AST 去重阈值过严,或 LLM 生成的因子结构雷同。
排查: 降低 factor.duplication.threshold(默认值 5),或修改 direction 提示词让 LLM 更有创意。也可以暂时将 quality_gate.redundancy_enabled: false 观察是否能通过。

📉
Backtest 结果 IC 都很低

可能原因:数据质量问题,或 direction 太宽泛。
排查: 优先检查 Qlib 数据路径配置,确认 daily_pv.h5 数据不陈旧;尝试更具体的方向主题(如"开盘后30分钟的成交量模式"而非"成交量因子");查看 IC 是接近 0 还是接近 -0.05(负 IC 可能说明方向反了)。

🐢
运行速度太慢

可能原因:配置过大,或 LLM API 网络延迟高。
排查: 降低 num_directionsfactors_per_hypothesis;设置 execution.parallel_execution: true(需要多核环境);用 ping 测试 API 端点延迟;检查 Backtesttimeout 参数是否设置合理。

LLM 生成格式错误的因子表达式

可能原因:LLM 使用了不支持的操作符,或输出不符合解析器预期的格式。
排查: 查看 quantaalpha/factors/coder/factor_ast.py 中定义的支持操作符列表;检查 llm.json_mode_strict: true 是否已启用;查看 LLM 返回的原始输出日志(运行时加 --verbose 参数)。

🎯
你现在了解了 QuantaAlpha 的每一层

从哲学到代码,从进化框架到质量控制,从命令行到配置文件——你已经有了完整的地图。 下一步:运行它,读因子库,理解它为什么选择了那些因子。

模块测验

Q1:你想要快速验证 QuantaAlpha 系统是否正常工作,同时最小化 API 成本。你会怎么设置配置?

Q2:你发现系统挖到的因子 IC 都在 0.01 以下,远低于论文的 0.15。哪个是最不可能的原因?

Q3:论文中的配置(num_directions=10, max_loops=5, factors_per_hypothesis=3)和代码默认值(2, 2, 1)差距很大。这说明什么?

🎓

课程完成!

你从一个完全陌生的代码库出发,现在已经能够理解、运行并定制 QuantaAlpha 的每一个核心部分。

01
你学会了什么是 Alpha 因子

量化信号的本质、为什么需要挖掘它、QuantaAlpha 如何用 LLM 自动化这个过程。

02
你认识了五位主角

HypothesisGen、FactorParser、FactorRunner、EvolutionController、FactorRegulator——每位的职责和协作方式。

03
你跟完了一轮完整循环

从假设提出到因子值计算,再到 Backtest 和反馈写入 Trace,理解了5步循环的每个细节。

04
你理解了进化的秘密

Original→Mutation→Crossover 进化框架如何让系统从历史结果中自我改进,越跑越好。

05
你见识了质量守门员

FactorRegulator 的5重质量门控如何防止垃圾因子污染因子库,以及 过拟合 风险的控制逻辑。

06
你现在能驾驭它

如何运行、调参、选择场景配置、排查常见问题——你有了完整的操作指南。

接下来探索什么?

运行系统,读因子库

用场景一配置(快速原型)跑一次,然后打开 factors_df.parquet,用 pandas 读取它,看看系统挖出来的因子长什么样——这才是真正的理解。

定制你的 HypothesisGen

依赖注入设计让你可以替换任何组件。在 settings.py 里修改 hypothesis_gen 字段,指向你自己写的类——这是扩展 QuantaAlpha 最干净的方式。

阅读论文原文

代码来自一篇量化研究论文。理解了代码之后,读论文会变得容易很多——你会认出每个公式对应哪个函数,每个实验设置对应哪个 超参数

最后一句话

QuantaAlpha 的设计哲学是:让研究员专注于"方向",让机器负责"探索"。 你现在已经同时理解了这两端——你知道怎么给出好的方向,也知道机器背后发生了什么。 这就是驾驭它的完整能力。