- 新增一次性翻译脚本 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 的状态
425 lines
17 KiB
Markdown
425 lines
17 KiB
Markdown
# FactorMiner 本地框架整合实施计划(修订版)
|
||
|
||
> 目标:将 `src/factorminer` 完全整合进 ProStock 项目。数据读取与因子计算全部复用本地 `FactorEngine`,不再引入 FactorMiner 原生的数据加载、DSL 计算与 `(M,T)` 矩阵缓存。仅在因子生成、落库、指标分析时保留 FactorMiner 代码。
|
||
>
|
||
> 本次修订核心变更:
|
||
> 1. **删除 Step 1(LocalDataLoader)**:本地 `FactorEngine.compute()` 已自带数据路由与读取能力,无需自行封装数据加载层。
|
||
> 2. **删除运行时 DSL 翻译器**:不再维护 `FmToLocalTranslator`。改为一次性脚本把 110 个 paper factors 的 CamelCase DSL 翻译成本地 DSL,并直接回填到常量列表中;LLM Prompt 同步改造为直接输出本地 DSL。
|
||
|
||
---
|
||
|
||
## 代码风格与本地框架融合规范(全局约束)
|
||
|
||
所有新增/修改代码必须遵循 ProStock 代码风格,严禁出现 FactorMiner 原生的松散风格或外部项目风格。
|
||
|
||
1. **命名规范**
|
||
- 函数/方法/变量:`snake_case`
|
||
- 类名:`PascalCase`
|
||
- 常量:`UPPER_CASE`
|
||
- 私有方法/属性:`_leading_underscore`
|
||
|
||
2. **类型提示**
|
||
- 所有公共函数必须标注参数类型和返回类型
|
||
- 可空类型使用 `Optional[X]` 或 `X | None`(Python 3.10+)
|
||
- 复杂类型从 `typing` 导入:`Dict`, `List`, `Callable`, `Tuple`, `Any`
|
||
|
||
3. **文档字符串**
|
||
- **中文** Google 风格
|
||
- 第一行为简短摘要
|
||
- 必须包含 `Args:` 和 `Returns:` 段落
|
||
|
||
4. **导入顺序**
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
5. **错误处理**
|
||
- 禁止裸 `except:`
|
||
- 错误信息格式:`print(f"[ERROR] 上下文: {e}")`
|
||
- 记录上下文后重新抛出 `raise`
|
||
|
||
6. **日志与输出**
|
||
- 使用带前缀的 `print`:`print("[模块名] 消息")`
|
||
- 循环进度使用 `tqdm`
|
||
- **禁止 emoji**
|
||
|
||
7. **数据加载**
|
||
- 查询模式必须使用 `Storage(read_only=True)`
|
||
- 因子计算统一通过 `FactorEngine`
|
||
|
||
8. **测试**
|
||
- 所有新模块必须配套 `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`,提供与 FactorMiner `compute_tree_signals` 兼容的输出接口。
|
||
- 输入:候选因子 DSL 列表(`(name, formula)`);输出:`{name: (M, T) np.ndarray}`。
|
||
- 无需外部数据加载器,直接利用 `FactorEngine` 内建的数据路由读取 `pro_bar` 表。
|
||
|
||
**类签名设计**
|
||
```python
|
||
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` 公式,统计实际可运行因子的成功率。
|
||
|
||
**测试逻辑**
|
||
1. 调用 `import_from_paper()` 加载因子库。
|
||
2. 实例化 `LocalFactorEvaluator(start_date="20200101", end_date="20201231")`。
|
||
3. 过滤掉 `unsupported=True` 的因子。
|
||
4. 批量计算剩余因子,断言输出形状为 `(M, T)` 且不含全 NaN。
|
||
5. 打印统计:`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: 代码风格审查、测试全量回归与提交
|
||
|
||
**执行清单**
|
||
1. 运行 `uv run pytest tests/test_factorminer_* -v`,确保全部通过。
|
||
2. 运行 `uv run pytest tests/test_factor_engine.py tests/test_factor_integration.py -v`,确保本地框架未受影响。
|
||
3. 检查新增代码中是否混入 emoji。
|
||
4. 检查新增代码的导入顺序和 docstring 完整性。
|
||
5. 提交前做一次 `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
|
||
```
|
||
|