Files
ProStock/docs/factor_design.md

708 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🚀 量化因子计算框架抽象设计与实施蓝图
## 一、 系统架构设计(四层解耦模型)
本系统采用严格的分层架构,每一层只需关注自己的输入与输出,层与层之间通过标准化的数据结构(如抽象语法树、需求清单、物理执行图)进行通信。
### 1. 领域特定语言层DSL Layer / 用户层)
* **职责**:提供对量化研究员极度友好的因子表达式编写接口,屏蔽所有底层计算引擎和数据库的痕迹。
* **输入**:研究员编写的数学与逻辑表达式。
* **输出**:纯粹的、无状态的**抽象语法树AST**。
* **边界约束**:本层绝对不允许依赖任何外部数据处理库。它只负责描述“计算逻辑是什么”,不涉及“怎么算”和“数据在哪”。
### 2. 编译与分析层Compiler Layer / 解析层)
* **职责**:接收 DSL 层生成的 AST进行语法树分析与优化。
* **核心动作 1依赖提取**。遍历语法树,找出所有的“叶子节点”(即基础数据字段),生成全局数据需求清单。
* **核心动作 2图优化可选**。识别重复的子表达式结构,进行合并计算标记。
* **输出**结构化的数据依赖清单Set/List和经过校验的 AST。
### 3. 动态数据路由层Data Router Layer / IO 层)
* **职责**:充当量化系统与底层多表数据库之间的桥梁。
* **核心逻辑**:基于元数据字典(记录字段所属的数据库表及数据频度),将分析层传递的“数据需求清单”转化为对数据库的最优查询指令。
* **输出**在内存中组装好的、经过严格时间对齐与防未来函数处理的、极简的数据上下文Data Context
### 4. 物理执行引擎层Execution Engine / 计算层)
* **职责**:将抽象的计算逻辑映射到具体的硬件或高性能计算库(如 Polars/向量化引擎)上并执行。
* **核心逻辑**:遍历 AST将其翻译为物理引擎的执行算子。在这个翻译过程中**系统隐式地强制注入量化计算的安全规则**(如截面分组、时序分组)。
* **输出**:最终的因子计算结果(面板数据表)。
---
## 二、 核心机制的具体实现逻辑(非代码描述)
为了让 AI 准确理解你的意图,你需要向 AI 阐明以下四个核心逻辑的运作机制:
### 1. 表达式树的生成机制 (符号化运算)
* **逻辑说明**:定义基础的变量节点(代表底层字段)和操作节点(代表加减乘除或函数)。通过重载面向对象语言的原生运算符(如算术运算符、比较运算符),使得变量节点参与运算时,不会抛出错误或执行计算,而是生成一个新的、包含左右子节点和操作符的父节点。
* **结果**:一个复杂的数学公式最终在内存里会变成一棵树状的数据结构。
### 2. 动态 SQL 生成与按需加载机制
* **逻辑说明**:系统初始化时,加载一次数据库元数据(表名、列名、更新频率),形成路由字典。当收到需求清单时,系统不使用 `SELECT *`,而是通过路由字典找到字段对应的表,动态拼接 `SELECT [必要关联键], [需求字段] FROM [表名] WHERE[时间与股票池过滤]`
* **结果**:极大降低数据库的 I/O 压力和网络传输负载。
### 3. 数据对齐与防未来函数机制(极其重要)
数据在内存中合并时,必须根据表的“频度属性”采取不同的关联策略:
* **同行频表(如日频基础与日频行情)**:以基准时间轴为左表,严格按照 `[资产标识, 交易日]` 进行精确匹配连接。
* **低频事件表(如财务报表)**:绝不能按自然日期或报告期关联。必须以“财报实际披露日”作为右表时间键,采用**“就近向后寻找匹配Asof Join / Point-in-Time Join”**策略。即某一天的财务数据,只能使用该日期之前(含当天)最新发布的那份财报。
* **防错铁律**:拼表完成后,必须强制按照 `[资产标识, 交易日]` 的优先级进行升序排序,为后续的滑动窗口计算提供物理连续性保障。
### 4. 算子翻译与引擎方言注入机制
物理层在将 AST 翻译为引擎执行图时,必须自动附加以下安全约束,这是研究员无需关心但系统必须保证的:
* **时序算子(如移动平均、动量)**:翻译时,必须向引擎下达强制指令——“本计算窗口必须被严格限制在单一资产的边界内”。
* **截面算子(如截面排名、行业中性化)**:翻译时,必须向引擎下达强制指令——“本计算必须在同一个交易日切片内横向展开”。
---
## 三、 Vibe Coding 实施与 Prompt 投喂计划
在利用 AI 编写代码时,建议按照以下阶段逐步进行(可作为每个阶段发给 AI 的指令纲要):
### 里程碑 1构建抽象语法树引擎 (DSL & AST)
* **任务指派**:要求 AI 设计一套纯粹的表达式树数据结构。包含基础节点类、变量节点类、二元/一元操作节点类、以及函数调用节点类。
* **验收标准**:通过重载运算符,可以随意组合变量(如 A, B, C并且编写一个简单的打印函数能够以可视化的方式或 JSON 结构)输出这棵树的层次关系。绝不包含任何第三方数据处理库。
### 里程碑 2实现依赖解析器 (Compiler)
* **任务指派**:要求 AI 编写一个树遍历器(如使用 Visitor 模式)。该遍历器接收里程碑 1 产生的树根节点,递归访问所有分支,收集所有叶子节点(变量节点)的名称。
* **验收标准**:输入一个深层嵌套的复杂公式树,解析器能够准确、去重地返回该公式依赖的所有底层基础字段名称列表。
### 里程碑 3构建元数据路由与动态组装器 (Data Router)
* **任务指派**:要求 AI 设计一个数据上下文管理器。
1. 实现注册机制,能接收不同表的数据字典和频度属性(日频或 PIT 低频)。
2. 根据里程碑 2 提取的依赖列表,自动分配表归属,并生成最小化拉取数据的伪代码或抽象 SQL 查询计划。
3. 阐明并在代码结构中实现不同频度数据的合并对齐逻辑(精确连接与就近前向连接),以及最后的全局强制排序逻辑。
* **验收标准**:输入几个测试字段,管理器能正确输出不同表的查询指令清单,并展现合并逻辑的抽象流程。
### 里程碑 4构建物理引擎翻译器 (Translator)
* **任务指派**:指定一个高性能计算库(如 Polars。要求 AI 编写一个翻译层,接收里程碑 1 的树节点,递归转化为该计算库的原生表达式对象。
* **验收约束**:在这个环节,要求 AI 必须在翻译时序函数时自动附加资产分组属性,在翻译截面函数时自动附加日期分组属性。
* **验收标准**:输入的抽象树被成功转化为计算引擎可以识别的执行计划对象,且分组属性被正确挂载。
### 里程碑 5系统顶层编排与端到端测试 (Orchestrator)
* **任务指派**:要求 AI 编写一个对外的 `FactorEngine` 类,作为系统的统一入口。
* **执行流编排**:接收研究员的表达式 -> 调用编译器解析依赖 -> 调用路由器连接数据库拉取并组装核心宽表 -> 调用翻译器生成物理执行计划 -> 将计划提交给计算引擎执行并行运算。
* **验收标准**:模拟少量的内存数据作为假数据库,完整跑通一条“从表达式注册,到自动按需取数,最终输出包含因子结果数据表”的全流程链路。
---
## 四、 详细设计规范(新增)
### 4.1 五层架构总览
```
┌─────────────────────────────────────────────────────────────────┐
│ Layer 5: 编排层 (Orchestrator) │
│ - FactorEngine: 统一入口 │
│ - 协调各层工作流 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Layer 4: 物理执行引擎层 (Execution Engine) │
│ - PolarsTranslator: AST → Polars表达式 │
│ - 自动注入分组约束(截面/时序) │
│ - 执行计算并返回结果 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Layer 3: 动态数据路由层 (Data Router) │
│ - MetadataRegistry: 字段→表映射 │
│ - QueryPlanner: 生成最优查询计划 │
│ - DataAligner: PIT对齐与防未来函数处理 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Layer 2: 编译与分析层 (Compiler) │
│ - DependencyExtractor: 提取数据依赖 │
│ - GraphOptimizer: 子表达式合并(预留接口) │
│ - 输出: 数据需求清单 + 优化后的AST │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Layer 1: DSL层 (领域特定语言) │
│ - AST节点: Field, BinaryOp, UnaryOp, FunctionCall, Constant │
│ - 算子库: ts_* (时序), cs_* (截面), math_* (数学) │
│ - 运算符重载: +, -, *, /, >, <, == 等 │
└─────────────────────────────────────────────────────────────────┘
```
---
### 4.2 Layer 1: DSL层详细设计
#### 核心设计原则
- **算子与数据解耦**:算子只描述计算逻辑,不绑定具体数据
- **纯表达式树**输出无状态的AST不涉及任何外部库
- **延迟执行**:表达式构建时不执行计算,只生成树结构
#### AST节点类型体系
```python
# 节点基类
class ASTNode(ABC):
"""AST节点基类"""
@abstractmethod
def accept(self, visitor: "NodeVisitor") -> Any:
"""接受访问者"""
pass
@abstractmethod
def get_children(self) -> List["ASTNode"]:
"""获取子节点列表"""
pass
# 1. 字段节点(叶子节点)
class Field(ASTNode):
"""
字段节点 - 代表底层数据字段
示例: close, volume, pe, pb
"""
name: str # 字段名
dtype: Optional[str] = None # 数据类型提示
# 2. 常量节点(叶子节点)
class Constant(ASTNode):
"""
常量节点 - 代表常量值
示例: 5, 10.5, "20240101"
"""
value: Union[int, float, str]
dtype: str
# 3. 二元操作节点
class BinaryOp(ASTNode):
"""
二元操作节点
支持的运算符: +, -, *, /, //, %, **, >, >=, <, <=, ==, !=, &, |
"""
op: str # '+', '-', '*', '/', '>', etc.
left: ASTNode
right: ASTNode
# 4. 一元操作节点
class UnaryOp(ASTNode):
"""
一元操作节点
支持的运算符: -, +, ~, abs
"""
op: str # '-', '+', '~', 'abs'
operand: ASTNode
# 5. 函数调用节点
class FunctionCall(ASTNode):
"""
函数调用节点 - 代表算子调用
示例: ts_mean(close, 20), cs_rank(pe)
"""
name: str # 函数名
args: List[ASTNode]
kwargs: Dict[str, Any]
func_type: str # "timeseries" | "cross_sectional" | "math"
```
#### 运算符重载规则
在 ASTNode 基类中实现运算符重载:
```python
class ASTNode:
# 算术运算符
def __add__(self, other) -> BinaryOp:
return BinaryOp("+", self, _ensure_node(other))
def __sub__(self, other) -> BinaryOp:
return BinaryOp("-", self, _ensure_node(other))
def __mul__(self, other) -> BinaryOp:
return BinaryOp("*", self, _ensure_node(other))
def __truediv__(self, other) -> BinaryOp:
return BinaryOp("/", self, _ensure_node(other))
# 反向运算符(支持 5 * field
def __radd__(self, other) -> BinaryOp:
return BinaryOp("+", _ensure_node(other), self)
def __rmul__(self, other) -> BinaryOp:
return BinaryOp("*", _ensure_node(other), self)
# 比较运算符
def __gt__(self, other) -> BinaryOp:
return BinaryOp(">", self, _ensure_node(other))
def __lt__(self, other) -> BinaryOp:
return BinaryOp("<", self, _ensure_node(other))
# 一元运算符
def __neg__(self) -> UnaryOp:
return UnaryOp("-", self)
```
#### 算子库规范
算子按功能分为三类:
| 前缀 | 类别 | 说明 | 示例 |
|------|------|------|------|
| `ts_` | 时序算子 | 在时间序列上计算,需按股票分组 | `ts_mean`, `ts_std`, `ts_sum` |
| `cs_` | 截面算子 | 在截面上计算,需按日期分组 | `cs_rank`, `cs_zscore`, `cs_percentile` |
| `math_` | 数学算子 | 逐元素计算,无需分组 | `math_log`, `math_exp`, `math_sqrt` |
**时序算子列表ts_*)**
```python
ts_mean(field, window: int) # 移动平均
ts_std(field, window: int) # 移动标准差
ts_sum(field, window: int) # 移动求和
ts_max(field, window: int) # 移动最大值
ts_min(field, window: int) # 移动最小值
ts_delta(field, period: int = 1) # 差分
ts_pct_change(field, period: int = 1) # 百分比变化
ts_corr(f1, f2, window: int) # 滚动相关系数
```
**截面算子列表cs_*)**
```python
cs_rank(field) # 截面排名0-1
cs_percentile(field) # 截面分位数
cs_zscore(field) # Z-Score标准化
cs_mean(field) # 截面均值
cs_std(field) # 截面标准差
```
**数学算子列表math_*)**
```python
math_log(field) # 自然对数
math_exp(field) # 指数
math_sqrt(field) # 平方根
math_abs(field) # 绝对值
```
#### 表达式构建示例
```python
from src.factors.dsl import Field, ts_mean, cs_rank
# ========== 示例 1: 简单移动平均线因子 ==========
close = Field("close")
ma20 = ts_mean(close, 20)
factor1 = ma20
# ========== 示例 2: 双均线差值因子 ==========
close = Field("close")
ma20 = ts_mean(close, 20)
ma5 = ts_mean(close, 5)
factor2 = (ma20 - ma5) / close
# ========== 示例 3: 复杂多因子组合 ==========
close = Field("close")
volume = Field("volume")
pe = Field("pe")
price_momentum = ts_pct_change(close, 20)
vol_ma = ts_mean(volume, 20)
vol_ratio = volume / vol_ma
pe_rank = cs_rank(pe)
factor3 = price_momentum * 0.4 + vol_ratio * 0.3 + pe_rank * 0.3
```
---
### 4.3 Layer 2: 编译层详细设计
#### 依赖提取器
```python
class DependencyExtractor(NodeVisitor):
"""
依赖提取器 - 遍历AST收集数据依赖
输出: DataRequirement
- fields: Set[str] 需要的字段列表
- min_lookback: Dict[str, int] 每个字段的最小回看天数
"""
def __init__(self):
self.fields: Set[str] = set()
self.field_lookback: Dict[str, int] = defaultdict(int)
def visit_field(self, node: Field) -> None:
"""记录字段依赖"""
self.fields.add(node.name)
self.field_lookback[node.name] = max(
self.field_lookback[node.name], 1
)
def visit_function_call(self, node: FunctionCall) -> None:
"""处理函数调用,提取窗口参数"""
for arg in node.args:
arg.accept(self)
if node.func_type == "timeseries":
window = self._extract_window(node)
self._update_lookback(node.args[0], window)
def extract(self, root: ASTNode) -> DataRequirement:
"""执行提取"""
root.accept(self)
return DataRequirement(
fields=self.fields,
lookback=dict(self.field_lookback)
)
```
#### 数据需求规格
```python
@dataclass
class DataRequirement:
"""
数据需求规格
属性:
fields: 需要的字段集合
lookback: 每个字段需要回看的天数
date_range: 计算日期范围 (start, end)
"""
fields: Set[str]
lookback: Dict[str, int]
date_range: Optional[Tuple[str, str]] = None
def get_max_lookback(self) -> int:
"""获取最大回看天数"""
return max(self.lookback.values()) if self.lookback else 1
```
---
### 4.4 Layer 3: 数据路由层详细设计
#### 元数据注册表
```python
@dataclass
class FieldMetadata:
"""
字段元数据
属性:
name: 字段名
table: 所属表名
dtype: 数据类型
freq: 数据频度 ("daily", "quarterly", "pit")
announce_date_field: 公告日字段名PIT数据使用
"""
name: str
table: str
dtype: str
freq: str
announce_date_field: Optional[str] = None
class MetadataRegistry:
"""
元数据注册表 - 管理字段到表的映射
单例模式,系统启动时加载配置
"""
def register(self, metadata: FieldMetadata) -> None:
"""注册字段元数据"""
pass
def get_table(self, field: str) -> str:
"""获取字段所属表"""
pass
def group_by_table(self, fields: Set[str]) -> Dict[str, Set[str]]:
"""按表分组字段"""
pass
```
#### PIT对齐策略
```python
class DataAligner:
"""
数据对齐器 - 处理多表数据合并与PIT对齐
"""
def align(
self,
dataframes: Dict[str, pl.DataFrame],
plans: List[QueryPlan]
) -> pl.DataFrame:
"""
对齐并合并多个数据表
步骤:
1. 分离日频表和PIT表
2. 日频表直接join
3. PIT表使用asof join
4. 最终排序
"""
pass
def _asof_join(
self,
left: pl.DataFrame,
right: pl.DataFrame,
announce_date_field: str
) -> pl.DataFrame:
"""
执行PIT asof join
策略: 对于每个交易日,使用最新公告的数据
"""
return left.join_asof(
right,
left_on="trade_date",
right_on=announce_date_field,
by="ts_code",
strategy="backward"
)
```
---
### 4.5 Layer 4: 执行引擎层详细设计
#### Polars翻译器
```python
class PolarsTranslator(NodeVisitor):
"""
Polars翻译器 - 将AST翻译为Polars表达式
"""
def __init__(self, df: pl.LazyFrame):
self.df = df
def translate(self, root: ASTNode) -> pl.Expr:
"""翻译AST为Polars表达式"""
return root.accept(self)
def visit_field(self, node: Field) -> pl.Expr:
"""字段 → pl.col()"""
return pl.col(node.name)
def visit_binary_op(self, node: BinaryOp) -> pl.Expr:
"""二元操作 → Polars运算符"""
left = node.left.accept(self)
right = node.right.accept(self)
ops = {
"+": lambda a, b: a + b,
"-": lambda a, b: a - b,
"*": lambda a, b: a * b,
"/": lambda a, b: a / b,
}
return ops[node.op](left, right)
def visit_function_call(self, node: FunctionCall) -> pl.Expr:
"""
函数调用 → Polars窗口函数
关键根据func_type注入分组约束
"""
args = [arg.accept(self) for arg in node.args]
impl = self._get_impl(node.name)
if node.func_type == "timeseries":
return impl(*args).over("ts_code")
elif node.func_type == "cross_sectional":
return impl(*args).over("trade_date")
else:
return impl(*args)
```
#### 分组约束注入规则
```python
# 时序算子:按股票分组,确保滚动窗口不跨股票
def inject_timeseries_constraint(expr: pl.Expr) -> pl.Expr:
return expr.over("ts_code")
# 截面算子:按日期分组,确保排名在每天内部进行
def inject_cross_sectional_constraint(expr: pl.Expr) -> pl.Expr:
return expr.over("trade_date")
```
---
### 4.6 Layer 5: 编排层详细设计
#### FactorEngine
```python
class FactorEngine:
"""
因子执行引擎 - 系统统一入口
"""
def __init__(
self,
data_source: DataSource,
registry: MetadataRegistry
):
self.data_source = data_source
self.registry = registry
self.compiler = Compiler()
self.planner = QueryPlanner(registry)
self.aligner = DataAligner()
def compute(
self,
expression: ASTNode,
start_date: str,
end_date: str,
stock_codes: Optional[List[str]] = None
) -> pl.DataFrame:
"""
计算因子表达式
执行流程:
1. 编译:提取数据依赖
2. 规划:生成查询计划
3. 加载:从数据源获取数据
4. 对齐PIT对齐与合并
5. 翻译AST → Polars表达式
6. 执行:计算并返回结果
"""
# Step 1: 编译
requirement = self.compiler.extract_dependency(expression)
requirement.date_range = (start_date, end_date)
# Step 2: 规划
plans = self.planner.plan(requirement)
# Step 3: 加载
raw_data = {}
for plan in plans:
df = self.data_source.load(...)
raw_data[plan.table] = df
# Step 4: 对齐
aligned_data = self.aligner.align(raw_data, plans)
# Step 5: 翻译
translator = PolarsTranslator(aligned_data.lazy())
polars_expr = translator.translate(expression)
# Step 6: 执行
result = aligned_data.with_columns(
polars_expr.alias("factor_value")
)
return result
```
---
## 五、 实施路线图(详细版)
### 阶段1: 基础架构Layer 1 + Layer 2
**目标**: 实现DSL表达式树和依赖提取
**任务清单**:
- [ ] 实现AST节点类Field, Constant, BinaryOp, UnaryOp, FunctionCall
- [ ] 实现运算符重载
- [ ] 实现基础算子库ts_mean, ts_std, cs_rank等
- [ ] 实现DependencyExtractor
- [ ] 编写单元测试
**验收标准**:
```python
close = Field("close")
factor = ts_mean(close, 20) / close
deps = extract_dependencies(factor)
assert deps.fields == {"close"}
assert deps.lookback == {"close": 20}
```
### 阶段2: 数据层Layer 3
**目标**: 实现元数据管理和PIT对齐
**任务清单**:
- [ ] 实现MetadataRegistry
- [ ] 实现QueryPlanner
- [ ] 实现DataAligner含asof join
- [ ] 集成DuckDB数据源
### 阶段3: 执行层Layer 4
**目标**: 实现Polars翻译和执行
**任务清单**:
- [ ] 实现PolarsTranslator
- [ ] 实现算子到Polars的映射
- [ ] 实现分组约束注入
### 阶段4: 编排层Layer 5
**目标**: 实现FactorEngine统一入口
**任务清单**:
- [ ] 实现FactorEngine
- [ ] 整合各层组件
- [ ] 编写端到端测试
---
## 六、 关键设计决策
### 6.1 为什么使用Visitor模式
- **扩展性**: 新增节点类型只需添加visit方法
- **分离关注点**: 遍历逻辑与处理逻辑分离
- **类型安全**: 每个节点类型有明确的处理函数
### 6.2 为什么算子需要分类ts_/cs_/math_
- **显式分组**: 用户明确知道计算维度
- **约束注入**: 系统根据前缀自动注入正确的分组
- **错误预防**: 避免截面/时序算子混用导致的逻辑错误
### 6.3 向后兼容性
**决策**: 完全重构不保留旧API
**理由**:
- 新旧架构差异过大绑定vs解耦
- 保持旧API会增加维护负担
- 量化策略代码通常是一次性编写,迁移成本可控
---
## 七、 附录
### A. 完整算子列表
**时序算子 (ts_*)**: ts_mean, ts_std, ts_var, ts_sum, ts_max, ts_min, ts_product, ts_median, ts_argmax, ts_argmin, ts_skew, ts_kurt, ts_delta, ts_pct_change, ts_corr, ts_cov, ts_rank
**截面算子 (cs_*)**: cs_rank, cs_percentile, cs_zscore, cs_mean, cs_std, cs_median, cs_max, cs_min
**数学算子 (math_*)**: math_log, math_log1p, math_exp, math_sqrt, math_abs, math_sign, math_power
### B. 元数据配置示例
```python
METADATA = [
{"name": "close", "table": "daily", "dtype": "float64", "freq": "daily"},
{"name": "volume", "table": "daily", "dtype": "float64", "freq": "daily"},
{"name": "pe", "table": "daily", "dtype": "float64", "freq": "daily"},
{"name": "eps", "table": "financial_income", "dtype": "float64",
"freq": "pit", "announce_date_field": "ann_date"},
]
```
### C. 与现有代码对比
| 维度 | 现有实现 | 新设计 |
|------|---------|--------|
| 因子定义 | 类继承 | 表达式 |
| 数据绑定 | data_specs硬编码 | 元数据注册表 |
| 组合方式 | CompositeFactor包装 | AST节点自然组合 |
| 执行时机 | 立即执行 | 延迟执行 |
| 防泄露 | 手动控制 | 自动注入分组约束 |
| 可优化性 | 低 | 高 |
---
**文档版本**: 2.0 | **更新日期**: 2026-02-26