- 新增一次性翻译脚本 src/scripts/translate_paper_factors.py - 将 library_io.PAPER_FACTORS 中的 CamelCase DSL 公式替换为本地 DSL - 对使用未实现算子(Decay、TsLinRegSlope、TsLinRegResid、Resid、 Quantile、HMA、DEMA)的 16 个因子注释为 # TODO - 新增 test_factorminer_paper_factors.py 验证所有翻译后公式的 DSL 解析 - 更新整合计划中 step1 的状态
17 KiB
17 KiB
FactorMiner 本地框架整合实施计划(修订版)
目标:将
src/factorminer完全整合进 ProStock 项目。数据读取与因子计算全部复用本地FactorEngine,不再引入 FactorMiner 原生的数据加载、DSL 计算与(M,T)矩阵缓存。仅在因子生成、落库、指标分析时保留 FactorMiner 代码。本次修订核心变更:
- 删除 Step 1(LocalDataLoader):本地
FactorEngine.compute()已自带数据路由与读取能力,无需自行封装数据加载层。- 删除运行时 DSL 翻译器:不再维护
FmToLocalTranslator。改为一次性脚本把 110 个 paper factors 的 CamelCase DSL 翻译成本地 DSL,并直接回填到常量列表中;LLM Prompt 同步改造为直接输出本地 DSL。
代码风格与本地框架融合规范(全局约束)
所有新增/修改代码必须遵循 ProStock 代码风格,严禁出现 FactorMiner 原生的松散风格或外部项目风格。
-
命名规范
- 函数/方法/变量:
snake_case - 类名:
PascalCase - 常量:
UPPER_CASE - 私有方法/属性:
_leading_underscore
- 函数/方法/变量:
-
类型提示
- 所有公共函数必须标注参数类型和返回类型
- 可空类型使用
Optional[X]或X | None(Python 3.10+) - 复杂类型从
typing导入:Dict,List,Callable,Tuple,Any
-
文档字符串
- 中文 Google 风格
- 第一行为简短摘要
- 必须包含
Args:和Returns:段落
-
导入顺序
# 1. 标准库 import os from typing import Optional, Dict, List # 2. 第三方包 import numpy as np import polars as pl # 3. 本地模块(绝对导入) from src.data.storage import Storage from src.factors import FactorEngine -
错误处理
- 禁止裸
except: - 错误信息格式:
print(f"[ERROR] 上下文: {e}") - 记录上下文后重新抛出
raise
- 禁止裸
-
日志与输出
- 使用带前缀的
print:print("[模块名] 消息") - 循环进度使用
tqdm - 禁止 emoji
- 使用带前缀的
-
数据加载
- 查询模式必须使用
Storage(read_only=True) - 因子计算统一通过
FactorEngine
- 查询模式必须使用
-
测试
- 所有新模块必须配套
tests/test_*.py - 运行命令:
uv run pytest tests/test_xxx.py -v
- 所有新模块必须配套
Step 0: 统一模块引用风格为 src.*(已完成)
状态: [x] 已完成(通过脚本批量替换)
- 所有
from factorminer.xxx已替换为from src.factorminer.factorminer.xxx - 所有字符串形式的模块引用(如
"factorminer.xxx")已同步更新
Step 1: 一次性 Paper Factors DSL 迁移脚本
文件
- 新建:
scripts/translate_paper_factors.py
目标
- 将
src/factorminer/core/library_io.py中硬编码的 110 个PAPER_FACTORS的 CamelCase DSL 公式,一次性翻译为本地 snake_case DSL 字符串。 - 翻译结果直接替换回原常量列表,后续
import_from_paper()加载的公式已经是本地格式,无需运行时翻译。
映射规则(核心算子)
| FactorMiner | 本地 DSL |
|---|---|
Neg(X) |
-X |
Add(A, B) |
A + B |
Sub(A, B) |
A - B |
Mul(A, B) |
A * B |
Div(A, B) |
A / B |
Greater(A, B) |
A > B |
Square(X) |
X ** 2 |
CsRank(X) |
cs_rank(X) |
CsZscore(X) |
cs_zscore(X) |
TsMean(X, n) |
ts_mean(X, n) |
TsMax(X, n) |
ts_max(X, n) |
TsMin(X, n) |
ts_min(X, n) |
Std(X, n) |
ts_std(X, n) |
Delta(X, n) |
ts_delta(X, n) |
Delay(X, n) |
ts_delay(X, n) |
Corr(X, Y, n) |
ts_corr(X, Y, n) |
Cov(X, Y, n) |
ts_cov(X, Y, n) |
Sum(X, n) |
ts_sum(X, n) |
Return(X, n) |
ts_pct_change(X, n)(或 X / ts_delay(X, n) - 1) |
EMA(X, n) |
ts_ema(X, n) |
WMA(X, n) |
ts_wma(X, n) |
SMA(X, n) |
ts_mean(X, n)(FactorMiner 里的 SMA 即简单移动平均) |
Skew(X, n) |
ts_skew(X, n) |
Kurt(X, n) |
ts_kurt(X, n) |
Abs(X) |
abs(X) |
Sign(X) |
sign(X) |
Max(A, B) |
max_(A, B) |
Min(A, B) |
min_(A, B) |
IfElse(C, T, F) |
if_(C, T, F) |
$close |
close |
$volume |
vol |
$amt |
amount |
$vwap |
amount / vol |
$returns |
close / ts_delay(close, 1) - 1 |
未实现算子处理
本地框架缺少以下 FactorMiner 算子,翻译时将其整条公式替换为 # TODO: <原始公式>:
Decay(...)TsLinRegSlope(...)TsLinRegResid(...)Resid(...)Quantile(...)HMA(...)DEMA(...)
实现要点
- 脚本解析
PAPER_FACTORS中的字符串公式,使用括号递归栈做 AST 风格的拆分。 - 对
LeafNode(字段和数字常量)做直接映射;对OperatorNode做算子映射。 - 脚本输出新的 Python 列表代码,可直接复制并替换
library_io.py中的PAPER_FACTORS。 - 运行脚本后手动校验前 10 个公式的正确性,确保括号匹配。
代码风格检查点
- 脚本放
scripts/目录,使用snake_case命名。 - 带中文 docstring,打印统计:
print("[translate] 成功 {n}/110,TODO {m} 个")。
Step 2: 禁用 npz 并将库 I/O 对接本地 DSL
文件
- 修改:
src/factorminer/core/library_io.py - 修改:
src/factorminer/cli.py(如有save_signals参数则改为始终 False) - 测试:
tests/test_factorminer_library_io.py
目标
- 彻底禁止
.npz信号缓存落盘。 PAPER_FACTORS中的公式已通过 Step 1 变为本地 DSL,import_from_paper()直接加载即可,不再做运行时翻译。- 对于 Step 1 中标记为
# TODO的公式,在构建FactorLibrary时设置factor.metadata["unsupported"] = True。
修改要点
save_library(..., save_signals):无论传入什么,均忽略save_signals,不写.npz。load_library(path):恢复 JSON 后,若公式以# TODO开头,标记unsupported=True。import_from_paper():由于PAPER_FACTORS已本地化,直接构建FactorLibrary。- 移除
library_io.py中对ExpressionTree/canonicalizer的任何依赖(如果存在)。
代码风格检查点
- 废弃参数保留以兼容旧签名,但内部忽略。
- 打印日志说明 npz 已禁用:
print("[library_io] 信号缓存已禁用,仅保存 JSON 元数据")。
Step 3: LLM Prompt 改造(让 Agent 直接生成本地 DSL)
文件
- 修改:
src/factorminer/factorminer/agent/prompt_builder.py - 修改:
src/factorminer/factorminer/agent/output_parser.py - 修改:
src/factorminer/factorminer/agent/factor_generator.py(如有必要) - 测试:
tests/test_factorminer_prompt.py
目标
- 将 Prompt 中的 DSL 规范从 CamelCase +
$前缀改为本地 snake_case DSL。 - 所有示例公式替换为本地格式(如
cs_rank(close / ts_delay(close, 5) - 1))。 - 明确可用字段:
open,high,low,close,vol,amount,vwap(可用amount / vol计算)。 - LLM 输出直接是本地 DSL 字符串,解析层只需提取字符串,不再做
$替换或 CamelCase 转换。
修改要点
- 重写
SYSTEM_PROMPT中的 DSL 规则段落,列出现有函数名与字段名。 - 将所有 prompt 示例公式替换为本地 DSL。
OutputParser去掉$清洗逻辑;改为直接截取公式字符串(保留中文描述之外的纯公式部分)。factor_generator.py中的generate/try_parse不再调用 FactorMiner 的ExpressionTree.from_string,改为直接返回字符串(因为本地 DSL 由FactorEngine在计算时解析)。
代码风格检查点
- Prompt 内容易读、无 emoji。
- 单元测试验证 prompt 中包含的示例公式均为本地 DSL,且
OutputParser能正确提取。
Step 4: LocalFactorEvaluator(FactorEngine 执行封装)
文件
- 新建:
src/factorminer/factorminer/evaluation/local_engine.py - 测试:
tests/test_factorminer_local_engine.py
目标
- 封装
FactorEngine,提供与 FactorMinercompute_tree_signals兼容的输出接口。 - 输入:候选因子 DSL 列表(
(name, formula));输出:{name: (M, T) np.ndarray}。 - 无需外部数据加载器,直接利用
FactorEngine内建的数据路由读取pro_bar表。
类签名设计
class LocalFactorEvaluator:
def __init__(
self,
start_date: str,
end_date: str,
stock_codes: Optional[List[str]] = None,
) -> None:
"""初始化评估器。
Args:
start_date: 计算开始日期,YYYYMMDD 格式
end_date: 计算结束日期,YYYYMMDD 格式
stock_codes: 可选的股票代码列表,None 表示全量
"""
...
def evaluate(
self,
specs: List[Tuple[str, str]],
) -> Dict[str, np.ndarray]:
"""批量计算并返回 {name: (M, T) 矩阵}。
Args:
specs: (因子名, 本地 DSL 公式) 列表
Returns:
每个因子对应的 (asset, time) numpy 矩阵,缺失值填充 np.nan
"""
...
def evaluate_single(
self,
name: str,
formula: str,
) -> np.ndarray:
"""计算单个因子。"""
...
def evaluate_returns(
self,
periods: int = 1,
) -> np.ndarray:
"""计算收益率矩阵,用于后续 IC / quintile 分析。
Returns:
(M, T) 的 forward returns 矩阵
"""
...
实现要点
evaluate中一次性注册所有 specs,调用FactorEngine.compute(...)。- 返回的 Polars 长表按
ts_code(字母序)和trade_date(时间序)pivot为 numpy 矩阵。 - 缺失值填充
np.nan。 - 计算结束后调用
engine.clear()。 evaluate_returns计算ts_pct_change(close, periods)(或close / ts_delay(close, periods) - 1),同样 pivot 为矩阵。
代码风格检查点
- 严格的类型提示和中文 docstring。
- 日志打印:
print("[local_engine] 开始批量计算 {n} 个因子...")。
Step 5: 替换计算管线(pipeline.py / runtime.py)
文件
- 修改:
src/factorminer/factorminer/evaluation/pipeline.py - 修改:
src/factorminer/factorminer/evaluation/runtime.py - 修改:
src/factorminer/factorminer/data/loader.py(弃用标记,可选) - 修改:
src/factorminer/factorminer/data/preprocessor.py(弃用标记,可选) - 修改:
src/factorminer/factorminer/data/tensor_builder.py(弃用标记,可选) - 测试:
tests/test_factorminer_pipeline_integration.py
目标
- 移除
compute_tree_signals(..., data_dict)及其对 FactorMiner 原生(M,T)数据面板的依赖。 - 所有信号计算统一通过
LocalFactorEvaluator完成。 - 保留原有 IC、stats、quintile 分析逻辑。
修改 runtime.py 要点
EvaluationDataset不再持有data_dict和returns。evaluate_factors接收evaluator: LocalFactorEvaluator和returns: np.ndarray。- 不再需要
load_runtime_dataset做面版预处理;改为由调用方直接构造evaluator(指定日期范围)即可。 - 对每个 factor 调用
evaluator.evaluate_single(name, formula);若 formula 以# TODO开头,标记为reject。 - 保留 split-mask 和 stats 计算逻辑(它们只消费
(M,T)矩阵,无需改动)。
修改 pipeline.py 要点
ValidationPipeline.__init__改为接收evaluator: LocalFactorEvaluator和returns: np.ndarray。- 删除
compute_signals_fn参数(或保留为向后兼容的弃用参数)。 compute_tree_signals改为调用evaluator.evaluate_single(name, formula)。evaluate方法中,一次性批量计算所有候选因子的信号,再逐个进入 stats / correlation / replacement 阶段。
代码风格检查点
- 修改点精确定位,不改变评估函数的返回数据结构。
- 兼容测试通过后再提交。
Step 6: 内存优化——库中因子按需重算
文件
- 修改:
src/factorminer/factorminer/core/factor_library.py - 测试:
tests/test_factorminer_library_memory.py
目标
- 库内
Factor对象不再长期持有(M, T)numpy signals。 - 相关性检查改为按需调用
LocalFactorEvaluator重算。
修改要点
admit()时不再保存signals到Factor对象。compute_correlation签名改为接收evaluator: LocalFactorEvaluator, candidate_signals: np.ndarray。- 内部遍历库中因子,临时调用
evaluator.evaluate_single(name, formula)计算信号,再与候选信号求相关。 - 若 formula 为
# TODO则跳过(返回0.0)。 - 删除
_extend_correlation_matrix/_recompute_matrix_slot增量维护逻辑(改为动态求最大相关)。
代码风格检查点
- 废弃旧方法时保留空壳或私有方法,避免测试大面积报错。
- 中文注释说明为什么删除增量矩阵(本地引擎重算成本低,内存优先)。
Step 7: 端到端集成测试(110 Paper Factors)
文件
- 新建:
tests/test_factorminer_e2e.py
目标
- 验证迁移后的 110 个 paper factors 全部能在本地引擎上成功计算信号。
- 排除
# TODO公式,统计实际可运行因子的成功率。
测试逻辑
- 调用
import_from_paper()加载因子库。 - 实例化
LocalFactorEvaluator(start_date="20200101", end_date="20201231")。 - 过滤掉
unsupported=True的因子。 - 批量计算剩余因子,断言输出形状为
(M, T)且不含全 NaN。 - 打印统计:
print("[e2e] 成功 {x}/110,跳过 {y} 个未实现算子")。
代码风格检查点
- 使用
pytest.mark.slow标记(若运行时间 > 30 秒)。 - 不依赖外部 API Key。
Step 8: 清理所有 checkpoint 和 demo 中的 npz 保存逻辑
文件
- 修改:
src/factorminer/factorminer/core/ralph_loop.py - 修改:
src/factorminer/factorminer/core/helix_loop.py - 修改:
src/factorminer/run_demo.py - 修改:
src/factorminer/run_phase2_benchmark.py - 修改:
src/factorminer/factorminer/benchmark/*.py(如有save_signals调用)
目标
- 确保任何运行路径都不会意外触发
.npz信号缓存落盘。 - 移除或注释掉所有
library_io.save_library(..., save_signals=True)调用。
修改要点
- 搜索
save_signals=True和.npz关键字,逐一处理。 - 改为
save_signals=False或直接调用不带该参数的save_library。
Step 9: 代码风格审查、测试全量回归与提交
执行清单
- 运行
uv run pytest tests/test_factorminer_* -v,确保全部通过。 - 运行
uv run pytest tests/test_factor_engine.py tests/test_factor_integration.py -v,确保本地框架未受影响。 - 检查新增代码中是否混入 emoji。
- 检查新增代码的导入顺序和 docstring 完整性。
- 提交前做一次
git diff --stat,确认没有误删或大规模重写无关文件。
提交建议
- 按模块分几个 commit,而不是一个巨大的 commit。
- 使用 Conventional Commits 风格(
feat:/refactor:/perf:/test:)。
风险与 TODO
| 风险 | 应对 |
|---|---|
| FactorMiner 某些算子本地框架没有实现 | 一次性脚本翻译时标记 # TODO,unsupported=True 的因子在评估阶段直接 reject |
FactorEngine 在极宽表(>1000 列)时内存激增 |
LocalFactorEvaluator 以 batch 为单位分批计算,并配合 engine.clear() |
本地 pro_bar 表数据不完整或缺少某些日期 |
FactorEngine 本身有数据完整性校验;缺失率过高时会在计算阶段报错 |
OutputParser 对本地 DSL 的括号/逗号解析不兼容 |
修改 OutputParser 的清洗正则,增加单元测试 |
| 110 个 paper factors 中有大量使用未实现算子 | 统计 TODO 比例,若 >30% 则优先在本地框架补充 ts_linreg_slope、ts_decay 等高频算子 |
附:核心模块依赖关系(修订后)
┌─────────────────────────────┐
│ scripts/translate_paper_ │ ← 一次性脚本(跑完即删除/保留归档)
│ factors.py │
└─────────────┬───────────────┘
│ 替换 PAPER_FACTORS
▼
┌─────────────────────────────┐
│ library_io.py │ ← 禁用 npz,公式已本地 DSL 化
└─────────────┬───────────────┘
│ 加载 FactorLibrary
▼
┌─────────────────────────────┐
│ LocalFactorEvaluator │ ← FactorEngine (read_only 自动读取数据)
│ (local_engine.py) │
└─────────────┬───────────────┘
│
┌─────┴─────┐
▼ ▼
pipeline.py runtime.py ← 保留 FactorMiner 的 stats / metrics / admission 逻辑
│
▼
factor_library.py ← 按需重算,不保存 signals
│
▼
prompt_builder.py ← LLM 直接生成本地 DSL