Files
ProStock/docs/factor_design.md

27 KiB
Raw Blame History

🚀 量化因子计算框架抽象设计与实施蓝图

一、 系统架构设计(四层解耦模型)

本系统采用严格的分层架构,每一层只需关注自己的输入与输出,层与层之间通过标准化的数据结构(如抽象语法树、需求清单、物理执行图)进行通信。

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节点类型体系

# 节点基类
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 基类中实现运算符重载:

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_*)

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_*)

cs_rank(field)                   # 截面排名0-1
cs_percentile(field)             # 截面分位数
cs_zscore(field)                 # Z-Score标准化
cs_mean(field)                   # 截面均值
cs_std(field)                    # 截面标准差

数学算子列表math_*)

math_log(field)                  # 自然对数
math_exp(field)                  # 指数
math_sqrt(field)                 # 平方根
math_abs(field)                  # 绝对值

表达式构建示例

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: 编译层详细设计

依赖提取器

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)
        )

数据需求规格

@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: 数据路由层详细设计

元数据注册表

@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对齐策略

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翻译器

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)

分组约束注入规则

# 时序算子:按股票分组,确保滚动窗口不跨股票
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

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
  • 编写单元测试

验收标准:

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. 元数据配置示例

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