refactor(factor): 完全重构因子计算框架 - 引入DSL表达式系统

- 删除旧因子框架:移除 base.py、composite.py、data_loader.py、data_spec.py
  及所有子模块(momentum、financial、quality、sentiment等)
- 新增DSL表达式系统:实现 factor DSL 编译器和翻译器
  - dsl.py: 领域特定语言定义
  - compiler.py: AST编译与优化
  - translator.py: Polars表达式翻译
  - api.py: 统一API接口
- 新增数据路由层:data_router.py 实现字段到表的动态路由
- 新增API封装:api_pro_bar.py 提供pro_bar数据接口
- 更新执行引擎:engine.py 适配新的DSL架构
- 重构测试体系:删除旧测试,新增 test_dsl_promotion.py、
  test_factor_integration.py、test_pro_bar.py
- 清理文档:删除8个过时文档(factor_design、db_sync_guide等)
This commit is contained in:
2026-02-27 22:22:23 +08:00
parent c3c20ed7ea
commit a56433e440
51 changed files with 667 additions and 11287 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,118 +0,0 @@
"""ProStock 因子计算框架
因子框架提供以下核心功能:
1. 类型安全的因子定义(截面因子、时序因子)
2. 数据泄露防护机制
3. 因子组合和运算
4. 高效的数据加载和计算引擎
基础数据类型Phase 1
- DataSpec: 数据需求规格
- FactorContext: 计算上下文
- FactorData: 数据容器
因子基类Phase 2
- BaseFactor: 抽象基类
- CrossSectionalFactor: 日期截面因子基类
- TimeSeriesFactor: 时间序列因子基类
- CompositeFactor: 组合因子
- ScalarFactor: 标量运算因子
因子分类目录:
- momentum/: 动量因子MA、收益率排名等
- financial/: 财务因子EPS、ROE等
- valuation/: 估值因子PE、PB、PS等
- technical/: 技术指标因子RSI、MACD、布林带等
- quality/: 质量因子(盈利能力、稳定性等)
- sentiment/: 情绪因子(换手率、资金流向等)
- volume/: 成交量因子OBV、成交量比率等
- volatility/: 波动率因子历史波动率、GARCH等
数据加载和执行Phase 3-4
- DataLoader: 数据加载器
- FactorEngine: 因子执行引擎
使用示例:
# 使用通用因子(参数化)
from src.factors import MovingAverageFactor, ReturnRankFactor
from src.factors import DataLoader, FactorEngine
ma5 = MovingAverageFactor(period=5) # 5日MA
ma10 = MovingAverageFactor(period=10) # 10日MA
ret5 = ReturnRankFactor(period=5) # 5日收益率排名
loader = DataLoader(data_dir="data")
engine = FactorEngine(loader)
result = engine.compute(ma5, stock_codes=["000001.SZ"], start_date="20240101", end_date="20240131")
"""
因子框架提供以下核心功能
1. 类型安全的因子定义截面因子时序因子
2. 数据泄露防护机制
3. 因子组合和运算
4. 高效的数据加载和计算引擎
基础数据类型Phase 1
- DataSpec: 数据需求规格
- FactorContext: 计算上下文
- FactorData: 数据容器
因子基类Phase 2
- BaseFactor: 抽象基类
- CrossSectionalFactor: 日期截面因子基类
- TimeSeriesFactor: 时间序列因子基类
- CompositeFactor: 组合因子
- ScalarFactor: 标量运算因子
动量因子momentum/
- MovingAverageFactor: 移动平均线时序因子
- ReturnRankFactor: 收益率排名截面因子
财务因子financial/
- 待添加
数据加载和执行Phase 3-4
- DataLoader: 数据加载器
- FactorEngine: 因子执行引擎
使用示例
# 使用通用因子(参数化)
from src.factors import MovingAverageFactor, ReturnRankFactor
from src.factors import DataLoader, FactorEngine
ma5 = MovingAverageFactor(period=5) # 5日MA
ma10 = MovingAverageFactor(period=10) # 10日MA
ret5 = ReturnRankFactor(period=5) # 5日收益率排名
loader = DataLoader(data_dir="data")
engine = FactorEngine(loader)
result = engine.compute(ma5, stock_codes=["000001.SZ"], start_date="20240101", end_date="20240131")
"""
from src.factors.data_spec import DataSpec, FactorContext, FactorData
from src.factors.base import BaseFactor, CrossSectionalFactor, TimeSeriesFactor
from src.factors.composite import CompositeFactor, ScalarFactor
from src.factors.data_loader import DataLoader
from src.factors.engine import FactorEngine
# 动量因子
from src.factors.momentum import MovingAverageFactor, ReturnRankFactor
__all__ = [
# Phase 1: 数据类型定义
"DataSpec",
"FactorContext",
"FactorData",
# Phase 2: 因子基类
"BaseFactor",
"CrossSectionalFactor",
"TimeSeriesFactor",
"CompositeFactor",
"ScalarFactor",
# Phase 3-4: 数据加载和执行引擎
"DataLoader",
"FactorEngine",
# 动量因子
"MovingAverageFactor",
"ReturnRankFactor",
]

View File

@@ -1,274 +0,0 @@
"""因子基类 - Phase 2 核心抽象类
本模块定义了因子框架的基类:
- BaseFactor: 抽象基类,定义通用接口和验证逻辑
- CrossSectionalFactor: 日期截面因子基类(防止日期泄露)
- TimeSeriesFactor: 时间序列因子基类(防止股票泄露)
"""
from abc import ABC, abstractmethod
from dataclasses import field
from typing import List
import polars as pl
from src.factors.data_spec import DataSpec, FactorData
class BaseFactor(ABC):
"""因子基类 - 定义通用接口
所有因子必须继承此类,并声明以下类属性:
- name: 因子唯一标识snake_case
- factor_type: "cross_sectional""time_series"
- data_specs: List[DataSpec] 数据需求列表
可选声明:
- category: 因子分类(默认 "default"
- description: 因子描述
示例:
>>> class MyFactor(CrossSectionalFactor):
... name = "my_factor"
... data_specs = [DataSpec("daily", ["close"], lookback_days=5)]
...
... def compute(self, data: FactorData) -> pl.Series:
... return data.get_column("close").rank()
"""
# 必须声明的类属性
name: str = ""
factor_type: str = "" # "cross_sectional" | "time_series"
data_specs: List[DataSpec] = field(default_factory=list)
# 可选声明的类属性
category: str = "default"
description: str = ""
def __init_subclass__(cls, **kwargs):
"""子类创建时验证必须属性
验证项:
1. name 必须是非空字符串
2. factor_type 必须是 "cross_sectional""time_series"
3. data_specs 必须是非空列表
"""
super().__init_subclass__(**kwargs)
# 跳过抽象基类和特殊因子类的验证
if cls.__name__ in (
"CrossSectionalFactor",
"TimeSeriesFactor",
"CompositeFactor",
"ScalarFactor",
):
return
# 验证 name - 必须直接定义在类中(不能继承)
if "name" not in cls.__dict__ or not cls.name:
raise ValueError(f"Factor {cls.__name__} must define 'name'")
if not isinstance(cls.name, str):
raise ValueError(f"Factor {cls.__name__}.name must be a string")
# 验证 factor_type - 必须有值(可以是继承的)
if not cls.factor_type:
raise ValueError(f"Factor {cls.__name__} must define 'factor_type'")
if cls.factor_type not in ("cross_sectional", "time_series"):
raise ValueError(
f"Factor {cls.__name__}.factor_type must be 'cross_sectional' "
f"or 'time_series', got '{cls.factor_type}'"
)
# 验证 data_specs
# 情况1: 完全没有定义 data_specs继承的空列表
if "data_specs" not in cls.__dict__:
raise ValueError(f"Factor {cls.__name__} must define 'data_specs'")
# 情况2: 定义了但为空列表
if not cls.data_specs or len(cls.data_specs) == 0:
raise ValueError(f"Factor {cls.__name__}.data_specs cannot be empty")
if not isinstance(cls.data_specs, list):
raise ValueError(f"Factor {cls.__name__}.data_specs must be a list")
def __init__(self, **params):
"""初始化因子参数
子类可通过 __init__ 接收参数化配置,如 MA(period=20)
注意data_specs 必须在类级别定义(类属性),
而非在 __init__ 中设置。data_specs 的验证在
__init_subclass__ 中完成(类创建时)。
Args:
**params: 因子参数,存储在 self.params 中
"""
self.params = params
def _validate_params(self):
"""验证参数有效性
子类可覆盖此方法进行自定义验证(需自行在子类 __init__ 中调用)。
基类实现为空,表示不执行任何验证。
注意:由于 data_specs 在类创建时通过 __init_subclass__ 验证,
不应在实例级别修改。如需动态 data_specs请使用参数化模式
>>> class ParamFactor(TimeSeriesFactor):
... name = "param_factor"
... data_specs = [] # 类级别定义
...
... def __init__(self, period: int = 20):
... super().__init__(period=period)
... # 通过参数化改变计算逻辑,而非 data_specs
...
... def compute(self, data: FactorData) -> pl.Series:
... return data.get_column("close").rolling_mean(self.params["period"])
"""
pass
@abstractmethod
def compute(self, data: FactorData) -> pl.Series:
"""核心计算逻辑 - 子类必须实现
Args:
data: 安全的数据容器,已根据因子类型裁剪
Returns:
计算得到的因子值 Series
"""
pass
# ========== 因子组合运算符 ==========
def __add__(self, other: "BaseFactor") -> "CompositeFactor":
"""因子相加f1 + f2要求同类型"""
from src.factors.composite import CompositeFactor
return CompositeFactor(self, other, "+")
def __sub__(self, other: "BaseFactor") -> "CompositeFactor":
"""因子相减f1 - f2要求同类型"""
from src.factors.composite import CompositeFactor
return CompositeFactor(self, other, "-")
def __mul__(self, other):
"""因子相乘f1 * f2 或 f1 * scalar"""
if isinstance(other, (int, float)):
from src.factors.composite import ScalarFactor
return ScalarFactor(self, float(other), "*")
elif isinstance(other, BaseFactor):
from src.factors.composite import CompositeFactor
return CompositeFactor(self, other, "*")
return NotImplemented
def __truediv__(self, other: "BaseFactor") -> "CompositeFactor":
"""因子相除f1 / f2要求同类型"""
from src.factors.composite import CompositeFactor
return CompositeFactor(self, other, "/")
def __rmul__(self, scalar: float) -> "ScalarFactor":
"""标量乘法0.5 * f1"""
from src.factors.composite import ScalarFactor
return ScalarFactor(self, scalar, "*")
def __repr__(self) -> str:
"""返回因子的字符串表示"""
return (
f"{self.__class__.__name__}(name='{self.name}', type='{self.factor_type}')"
)
class CrossSectionalFactor(BaseFactor):
"""日期截面因子基类
计算逻辑:在每个交易日,对所有股票进行横向计算
防泄露边界:
- ❌ 禁止访问未来日期的数据(日期泄露)
- ✅ 允许访问当前日期的所有股票数据
数据传入:
- compute() 接收的是 [T-lookback+1, T] 的数据
- 包含 lookback_days 的历史数据(用于时序计算后再截面)
示例:
>>> class PERankFactor(CrossSectionalFactor):
... name = "pe_rank"
... data_specs = [DataSpec("daily", ["pe"], lookback_days=1)]
...
... def compute(self, data: FactorData) -> pl.Series:
... cs = data.get_cross_section()
... return cs["pe"].rank()
"""
factor_type: str = "cross_sectional"
@abstractmethod
def compute(self, data: FactorData) -> pl.Series:
"""计算截面因子值
Args:
data: FactorData包含 [T-lookback+1, T] 的截面数据
格式DataFrame[ts_code, trade_date, col1, col2, ...]
Returns:
pl.Series: 当前日期所有股票的因子值(长度 = 该日股票数量)
示例:
>>> def compute(self, data):
... # 获取当前日期截面
... cs = data.get_cross_section()
... # 计算市值排名
... return cs['market_cap'].rank()
"""
pass
class TimeSeriesFactor(BaseFactor):
"""时间序列因子基类(股票截面)
计算逻辑:对每只股票,在其时间序列上进行纵向计算
防泄露边界:
- ❌ 禁止访问其他股票的数据(股票泄露)
- ✅ 允许访问该股票的完整历史数据
数据传入:
- compute() 接收的是单只股票的完整时间序列
- 包含该股票在 [start_date, end_date] 范围内的所有数据
示例:
>>> class MovingAverageFactor(TimeSeriesFactor):
... name = "ma"
...
... def __init__(self, period: int = 20):
... super().__init__(period=period)
... self.data_specs = [DataSpec("daily", ["close"], lookback_days=period)]
...
... def compute(self, data: FactorData) -> pl.Series:
... return data.get_column("close").rolling_mean(self.params["period"])
"""
factor_type: str = "time_series"
@abstractmethod
def compute(self, data: FactorData) -> pl.Series:
"""计算时间序列因子值
Args:
data: FactorData包含单只股票的完整时间序列
格式DataFrame[ts_code, trade_date, col1, col2, ...]
Returns:
pl.Series: 该股票在各日期的因子值(长度 = 日期数量)
示例:
>>> def compute(self, data):
... series = data.get_column("close")
... return series.rolling_mean(window_size=self.params['period'])
"""
pass

View File

@@ -1,201 +0,0 @@
"""组合因子 - Phase 2 因子组合和标量运算
本模块定义了因子组合相关的类:
- CompositeFactor: 组合因子,用于实现因子间的数学运算
- ScalarFactor: 标量运算因子,支持因子与标量的运算
"""
from typing import List
import polars as pl
from src.factors.data_spec import DataSpec, FactorData
from src.factors.base import BaseFactor
class CompositeFactor(BaseFactor):
"""组合因子 - 用于实现因子间的数学运算
约束:左右因子必须是同类型(同为截面或同为时序)
支持的运算符:'+', '-', '*', '/'
示例:
>>> f1 = SomeCrossSectionalFactor()
>>> f2 = AnotherCrossSectionalFactor()
>>> combined = f1 + f2 # 创建 CompositeFactor
"""
def __init__(self, left: BaseFactor, right: BaseFactor, op: str):
"""创建组合因子
Args:
left: 左操作数因子
right: 右操作数因子
op: 运算符,支持 '+', '-', '*', '/'
Raises:
ValueError: 左右因子类型不一致
ValueError: 不支持的运算符
"""
# 验证类型一致性
if left.factor_type != right.factor_type:
raise ValueError(
f"Cannot combine factors of different types: "
f"'{left.factor_type}' vs '{right.factor_type}'"
)
# 验证运算符
if op not in ("+", "-", "*", "/"):
raise ValueError(f"Unsupported operator: '{op}'")
self.left = left
self.right = right
self.op = op
# 设置类属性
self.factor_type = left.factor_type
self.name = f"({left.name}_{op}_{right.name})"
self.data_specs = self._merge_data_specs()
self.category = "composite"
self.description = f"Composite factor: {left.name} {op} {right.name}"
# 注意:不调用 super().__init__(),因为 CompositeFactor 是特殊因子
self.params = {
"left": left,
"right": right,
"op": op,
}
def _merge_data_specs(self) -> List[DataSpec]:
"""合并左右因子的数据需求
策略:
1. 相同 source 和 columns 的 DataSpec 合并
2. lookback_days 取最大值
Returns:
合并后的 DataSpec 列表
"""
merged = []
# 收集所有 specs
all_specs = list(self.left.data_specs) + list(self.right.data_specs)
# 按 (source, columns_tuple) 分组
spec_groups = {}
for spec in all_specs:
key = (spec.source, tuple(sorted(spec.columns)))
if key not in spec_groups:
spec_groups[key] = []
spec_groups[key].append(spec)
# 合并每组,取最大 lookback_days
for (source, columns_tuple), specs in spec_groups.items():
max_lookback = max(spec.lookback_days for spec in specs)
merged.append(
DataSpec(
source=source,
columns=list(columns_tuple),
lookback_days=max_lookback,
)
)
return merged
def compute(self, data: FactorData) -> pl.Series:
"""执行组合运算
流程:
1. 分别计算 left 和 right 的值
2. 根据 op 执行运算
3. 返回结果
Args:
data: 包含左右因子所需数据的 FactorData
Returns:
组合运算后的因子值 Series
"""
left_values = self.left.compute(data)
right_values = self.right.compute(data)
ops = {
"+": lambda a, b: a + b,
"-": lambda a, b: a - b,
"*": lambda a, b: a * b,
"/": lambda a, b: a / b,
}
return ops[self.op](left_values, right_values)
def _validate_params(self):
"""CompositeFactor 不需要额外验证"""
pass
class ScalarFactor(BaseFactor):
"""标量运算因子
支持scalar * factor, factor * scalar通过 __rmul__
示例:
>>> factor = SomeFactor()
>>> scaled = 0.5 * factor # 创建 ScalarFactor
"""
def __init__(self, factor: BaseFactor, scalar: float, op: str):
"""创建标量运算因子
Args:
factor: 基础因子
scalar: 标量值
op: 运算符,支持 '*', '+'
Raises:
ValueError: 不支持的运算符
"""
# 验证运算符
if op not in ("*", "+"):
raise ValueError(f"ScalarFactor only supports '*' and '+', got '{op}'")
self.factor = factor
self.scalar = scalar
self.op = op
# 设置类属性
self.factor_type = factor.factor_type
self.name = f"({scalar}_{op}_{factor.name})"
self.data_specs = factor.data_specs
self.category = "scalar"
self.description = f"Scalar factor: {scalar} {op} {factor.name}"
# 注意:不调用 super().__init__(),因为 ScalarFactor 是特殊因子
self.params = {
"factor": factor,
"scalar": scalar,
"op": op,
}
def compute(self, data: FactorData) -> pl.Series:
"""执行标量运算
Args:
data: 包含基础因子所需数据的 FactorData
Returns:
标量运算后的因子值 Series
"""
values = self.factor.compute(data)
if self.op == "*":
return values * self.scalar
elif self.op == "+":
return values + self.scalar
else:
# 不应该执行到这里,因为 __init__ 已经验证了 op
raise ValueError(f"Unsupported operation: '{self.op}'")
def _validate_params(self):
"""ScalarFactor 不需要额外验证"""
pass

View File

@@ -1,213 +0,0 @@
"""数据加载器 - Phase 3 数据加载模块
本模块负责从 DuckDB 安全加载数据:
- DataLoader: 数据加载器,支持多文件聚合、列选择、缓存
"""
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import pandas as pd
import polars as pl
from src.factors.data_spec import DataSpec
class DataLoader:
"""数据加载器 - 负责从 DuckDB 安全加载数据
功能:
1. 多文件聚合:合并多个表的数据
2. 列选择:只加载需要的列
3. 原始数据缓存:避免重复读取
4. 查询下推:利用 DuckDB SQL 过滤,只加载必要数据
示例:
>>> loader = DataLoader(data_dir="data")
>>> specs = [DataSpec("daily", ["ts_code", "trade_date", "close"], lookback_days=20)]
>>> df = loader.load(specs, date_range=("20240101", "20240131"))
"""
def __init__(self, data_dir: str):
"""初始化 DataLoader
Args:
data_dir: DuckDB 数据库文件所在目录
"""
self.data_dir = Path(data_dir)
self._cache: Dict[str, pl.DataFrame] = {}
def load(
self,
specs: List[DataSpec],
date_range: Optional[Tuple[str, str]] = None,
) -> pl.DataFrame:
"""加载并聚合多个 H5 文件的数据
流程:
1. 对每个 DataSpec
a. 检查缓存,命中则直接使用
b. 未命中则读取 HDF5通过 pandas
c. 转换为 Polars DataFrame
d. 按 date_range 过滤
e. 存入缓存
2. 合并多个 DataFrame按 trade_date 和 ts_code join
Args:
specs: 数据需求规格列表
date_range: 日期范围限制 (start_date, end_date),可选
Returns:
合并后的 Polars DataFrame
Raises:
FileNotFoundError: H5 文件不存在
KeyError: 列不存在于文件中
"""
dataframes = []
for spec in specs:
# 检查缓存
cache_key = f"{spec.source}_{','.join(sorted(spec.columns))}"
if cache_key in self._cache:
df = self._cache[cache_key]
else:
# 读取 H5 文件(传入日期范围以支持过滤)
df = self._read_h5(spec.source, date_range=date_range)
# 列选择 - 只保留需要的列
missing_cols = set(spec.columns) - set(df.columns)
if missing_cols:
raise KeyError(
f"Columns {missing_cols} not found in {spec.source}.h5. "
f"Available columns: {df.columns}"
)
df = df.select(spec.columns)
# 存入缓存
self._cache[cache_key] = df
# 按 date_range 过滤
if date_range:
start_date, end_date = date_range
df = df.filter(
(pl.col("trade_date") >= start_date)
& (pl.col("trade_date") <= end_date)
)
dataframes.append(df)
# 合并多个 DataFrame
if len(dataframes) == 1:
return dataframes[0]
else:
return self._merge_dataframes(dataframes)
def clear_cache(self):
"""清空缓存"""
self._cache.clear()
def _read_h5(
self,
source: str,
date_range: Optional[Tuple[str, str]] = None,
) -> pl.DataFrame:
"""读取数据 - 从 DuckDB 加载为 Polars DataFrame。
迁移说明:
- 方法名保持 _read_h5 以兼容现有代码(实际从 DuckDB 读取)
- 使用 Storage.load_polars() 直接返回 Polars DataFrame
- 支持零拷贝导出,性能优于 HDF5 + Pandas + Polars 转换
Args:
source: 表名(对应 DuckDB 中的表,如 "daily"
date_range: 日期范围限制 (start_date, end_date),可选
Returns:
Polars DataFrame
Raises:
Exception: 数据库查询错误
"""
from src.data.storage import Storage
from src.data.api_wrappers.api_trade_cal import get_trading_days
from src.data.utils import get_today_date
from src.factors.financial.utils import expand_period_to_trading_days
storage = Storage()
# 特殊处理财务数据:将报告期展开到交易日
if source == "financial_income":
# 确定日期范围
start_date = date_range[0] if date_range else "20180101"
end_date = date_range[1] if date_range else get_today_date()
# 1. 加载原始财务数据(报告期粒度),按日期范围过滤
# 注意financial_income 使用 end_date 字段作为报告期
df = storage.load_polars(
"financial_income",
start_date=start_date,
end_date=end_date,
)
if len(df) == 0:
return pl.DataFrame()
# 2. 获取交易日历从2018年开始到当前确保有足够的历史数据用于前向填充
# 需要从数据的最小日期开始,确保能获取到足够的交易日
trade_start = "20180101" if start_date > "20180101" else start_date
trade_dates = get_trading_days(trade_start, get_today_date())
# 3. 展开到交易日(前向填充)
return expand_period_to_trading_days(df, trade_dates)
# 其他数据源保持原有逻辑
return storage.load_polars(source)
def _merge_dataframes(self, dataframes: List[pl.DataFrame]) -> pl.DataFrame:
"""合并多个 DataFrame
策略:
1. 按 trade_date 和 ts_code join
2. 使用外连接保留所有数据
Args:
dataframes: DataFrame 列表
Returns:
合并后的 DataFrame
"""
result = dataframes[0]
for df in dataframes[1:]:
# 确定 join 键
join_keys = ["trade_date", "ts_code"]
# 检查 join 键是否存在
for key in join_keys:
if key not in result.columns or key not in df.columns:
raise KeyError(f"Join key '{key}' not found in DataFrames")
# 获取需要添加的列(排除重复的 join 键)
new_cols = [c for c in df.columns if c not in result.columns]
if new_cols:
# 选择必要的列进行 join
df_to_join = df.select(join_keys + new_cols)
# 执行 join
result = result.join(df_to_join, on=join_keys, how="full")
return result
def get_cache_info(self) -> Dict[str, int]:
"""获取缓存信息
Returns:
包含缓存条目数和总字节数的字典
"""
total_rows = sum(len(df) for df in self._cache.values())
return {
"entries": len(self._cache),
"total_rows": total_rows,
}

View File

@@ -1,242 +0,0 @@
"""数据类型定义 - Phase 1 核心数据模型
本模块定义了因子框架的基础数据类型:
- DataSpec: 数据需求规格,声明因子所需的数据源、列和回看窗口
- FactorContext: 计算上下文,由引擎自动注入,提供计算点信息
- FactorData: 数据容器,封装底层 Polars DataFrame提供安全的数据访问
"""
from dataclasses import dataclass, field
from typing import List, Optional
import polars as pl
@dataclass(frozen=True)
class DataSpec:
"""数据需求规格说明
用于声明因子计算所需的数据来源、列和回看窗口。
这是一个不可变对象,创建后不可修改。
Args:
source: H5 文件名(如 "daily", "fundamental"
columns: 需要的列名列表,必须包含 "ts_code""trade_date"
lookback_days: 需要回看的天数(包含当日)
- 1 表示只需要当日数据 [T]
- 5 表示需要 [T-4, T] 共5天
- 20 表示需要 [T-19, T] 共20天
Raises:
ValueError: 当参数不满足约束条件时
Examples:
>>> spec = DataSpec(
... source="daily",
... columns=["ts_code", "trade_date", "close"],
... lookback_days=20
... )
"""
source: str
columns: List[str]
lookback_days: int = 1
def __post_init__(self):
"""验证约束条件
验证项:
1. lookback_days >= 1至少包含当日
2. columns 必须包含 ts_code 和 trade_date
3. source 不能为空字符串
注意:由于 frozen=True实例创建后不可修改。
若需要在 __post_init__ 中修改字段(如有),可使用 object.__setattr__。
本类仅做验证,无需修改字段,因此直接 raise ValueError 即可。
"""
if self.lookback_days < 1:
raise ValueError(f"lookback_days must be >= 1, got {self.lookback_days}")
if not self.source:
raise ValueError("source cannot be empty string")
required_cols = {"ts_code", "trade_date"}
missing_cols = required_cols - set(self.columns)
if missing_cols:
raise ValueError(
f"columns must contain {required_cols}, missing: {missing_cols}"
)
@dataclass
class FactorContext:
"""因子计算上下文
由 FactorEngine 自动注入,因子开发者可通过 data.context 访问。
根据因子类型的不同,包含不同的上下文信息:
- CrossSectionalFactorcurrent_date 表示当前计算的日期
- TimeSeriesFactorcurrent_stock 表示当前计算的股票
Attributes:
current_date: 当前计算日期 YYYYMMDD截面因子使用
current_stock: 当前计算股票代码(时序因子使用)
trade_dates: 交易日历列表(可选,用于对齐)
Examples:
>>> context = FactorContext(current_date="20240101")
>>> context.current_date
'20240101'
"""
current_date: Optional[str] = None
current_stock: Optional[str] = None
trade_dates: Optional[List[str]] = None
class FactorData:
"""提供给因子的数据容器
封装底层 Polars DataFrame提供安全的数据访问接口。
根据因子类型的不同,包含不同的数据:
- CrossSectionalFactor当前日期及历史 lookback 的截面数据(所有股票)
- TimeSeriesFactor单只股票的完整时间序列数据
Args:
df: 底层的 Polars DataFrame
context: 计算上下文
Examples:
>>> df = pl.DataFrame({
... "ts_code": ["000001.SZ"],
... "trade_date": ["20240101"],
... "close": [10.0]
... })
>>> context = FactorContext(current_date="20240101")
>>> data = FactorData(df, context)
"""
def __init__(self, df: pl.DataFrame, context: FactorContext):
self._df = df
self._context = context
def get_column(self, col: str) -> pl.Series:
"""获取指定列的数据
适用于两种因子类型:
- 截面因子:获取当天所有股票的该列值
- 时序因子:获取该股票时间序列的该列值
Args:
col: 列名
Returns:
Polars Series
Raises:
KeyError: 列不存在于数据中
Examples:
>>> prices = data.get_column("close")
>>> print(prices)
"""
if col not in self._df.columns:
raise KeyError(
f"Column '{col}' not found in data. Available columns: {self._df.columns}"
)
return self._df[col]
def filter_by_date(self, date: str) -> "FactorData":
"""按日期过滤数据,返回新的 FactorData
主要用于截面因子获取特定日期的数据。
注意:无法获取未来日期的数据(引擎已经裁剪掉)。
Args:
date: YYYYMMDD 格式的日期
Returns:
过滤后的 FactorData新实例不修改原数据
Examples:
>>> today_data = data.filter_by_date("20240101")
>>> print(len(today_data))
"""
filtered = self._df.filter(pl.col("trade_date") == date)
return FactorData(filtered, self._context)
def get_cross_section(self) -> pl.DataFrame:
"""获取当前日期的截面数据
仅适用于截面因子,返回 current_date 当天的所有股票数据。
Returns:
DataFrame 包含当前日期的所有股票
Raises:
ValueError: current_date 未设置(非截面因子场景)
Examples:
>>> cs = data.get_cross_section()
>>> rankings = cs["pe"].rank()
"""
if self._context.current_date is None:
raise ValueError(
"current_date is not set in context. "
"get_cross_section() is only applicable for cross-sectional factors."
)
return self._df.filter(pl.col("trade_date") == self._context.current_date)
def to_polars(self) -> pl.DataFrame:
"""获取底层的 Polars DataFrame高级用法
返回原始 DataFrame允许进行自定义的 Polars 操作。
注意:直接操作底层数据可能绕过框架的防泄露保护,请谨慎使用。
Returns:
底层的 Polars DataFrame
Examples:
>>> df = data.to_polars()
>>> result = df.group_by("industry").agg(pl.col("pe").mean())
"""
return self._df
@property
def context(self) -> FactorContext:
"""获取计算上下文
Returns:
当前的 FactorContext 实例
Examples:
>>> date = data.context.current_date
>>> stock = data.context.current_stock
"""
return self._context
def __len__(self) -> int:
"""返回数据行数
Returns:
DataFrame 的行数
Examples:
>>> if len(data) > 0:
... result = data.get_column("close").mean()
"""
return len(self._df)
def __repr__(self) -> str:
"""返回 FactorData 的字符串表示
Returns:
包含类名、行数、列数和上下文信息的字符串
"""
cols = self._df.columns
context_info = []
if self._context.current_date:
context_info.append(f"date={self._context.current_date}")
if self._context.current_stock:
context_info.append(f"stock={self._context.current_stock}")
context_str = ", ".join(context_info) if context_info else "no context"
return f"FactorData(rows={len(self)}, cols={len(cols)}, {context_str})"

View File

@@ -1,20 +0,0 @@
"""财务因子模块
本模块提供财务类型的因子:
因子分类:
- financial: 财务因子
- EPSFactor: 每股收益排名因子
已添加因子:
- EPSFactor: 每股收益排名基于basic_eps
待添加因子:
- PERankFactor: 市盈率排名
- PBFactor: 市净率因子
- DividendFactor: 股息率因子
"""
from src.factors.financial.eps_factor import EPSFactor
__all__ = ["EPSFactor"]

View File

@@ -1,66 +0,0 @@
"""EPS因子
每股收益(EPS)排名因子实现
"""
from typing import List
import polars as pl
from src.factors.base import CrossSectionalFactor
from src.factors.data_spec import DataSpec, FactorData
class EPSFactor(CrossSectionalFactor):
"""每股收益(EPS)排名因子
计算逻辑使用最新报告期的basic_eps每天对所有股票进行截面排名
Attributes:
name: 因子名称 "eps_rank"
category: 因子分类 "financial"
data_specs: 数据需求规格
Example:
>>> from src.factors import FactorEngine, DataLoader
>>> from src.factors.financial.eps_factor import EPSFactor
>>> loader = DataLoader('data')
>>> engine = FactorEngine(loader)
>>> eps_factor = EPSFactor()
>>> result = engine.compute(eps_factor, start_date='20210101', end_date='20210131')
"""
name: str = "eps_rank"
category: str = "financial"
description: str = "每股收益截面排名因子"
data_specs: List[DataSpec] = [
DataSpec(
"financial_income", ["ts_code", "trade_date", "basic_eps"], lookback_days=1
)
]
def compute(self, data: FactorData) -> pl.Series:
"""计算EPS排名
Args:
data: FactorData包含当前日期的截面数据
Returns:
EPS排名的0-1标准化值0-1之间
"""
# 获取当前日期的截面数据
cs = data.get_cross_section()
if len(cs) == 0:
return pl.Series(name=self.name, values=[])
# 提取EPS值填充缺失值为0
eps = cs["basic_eps"].fill_null(0)
# 计算排名并归一化到0-1
if len(eps) > 1 and eps.max() != eps.min():
ranks = eps.rank(method="average") / len(eps)
else:
# 数据不足或全部相同返回0.5
ranks = pl.Series(name=self.name, values=[0.5] * len(eps))
return ranks

View File

@@ -1,82 +0,0 @@
"""财务因子工具函数
提供财务数据处理的工具函数:
- expand_period_to_trading_days: 将报告期数据展开到每个交易日(前向填充)
"""
from typing import List
import polars as pl
def expand_period_to_trading_days(
financial_df: pl.DataFrame,
trade_dates: List[str],
) -> pl.DataFrame:
"""将财务数据(报告期粒度)展开到每个交易日(前向填充)
核心逻辑:对于每个交易日,找到该日期之前最新的已公告报告期数据。
例如2020年报(20201231)公告于20210428则在2021-04-28之后的每个
交易日都使用该年报数据直到2021一季报公告。
Args:
financial_df: 财务数据DataFrame包含 ts_code, ann_date, end_date, ...
trade_dates: 交易日列表YYYYMMDD格式已排序
Returns:
DataFrame包含 trade_date, ts_code 和所有财务字段
Example:
>>> financial_df = pl.DataFrame({
... 'ts_code': ['000001.SZ'],
... 'ann_date': ['20210428'],
... 'end_date': ['20210331'],
... 'basic_eps': [0.5]
... })
>>> trade_dates = ['20210428', '20210429', '20210430']
>>> result = expand_period_to_trading_days(financial_df, trade_dates)
>>> print(result)
shape: (3, 5)
┌───────────┬───────────┬────────────┬────────────┬───────────┐
│ ts_code ┆ ann_date ┆ end_date ┆ basic_eps ┆ trade_date│
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ f64 ┆ str │
╞═══════════╪═══════════╪════════════╪════════════╪═══════════╡
│ 000001.SZ ┆ 20210428 ┆ 20210331 ┆ 0.5 ┆ 20210428 │
│ 000001.SZ ┆ 20210428 ┆ 20210331 ┆ 0.5 ┆ 20210429 │
│ 000001.SZ ┆ 20210428 ┆ 20210331 ┆ 0.5 ┆ 20210430 │
└───────────┴───────────┴────────────┴────────────┴───────────┘
"""
if len(financial_df) == 0:
return pl.DataFrame()
results = []
# 按股票分组处理
for ts_code in financial_df["ts_code"].unique():
stock_data = financial_df.filter(pl.col("ts_code") == ts_code)
# 按报告期排序end_date升序
stock_data = stock_data.sort("end_date")
rows = []
for trade_date in trade_dates:
# 找到该交易日之前最新的已公告报告期
# 条件1: end_date <= trade_date报告期不晚于交易日
# 条件2: ann_date <= trade_date已公告
applicable = stock_data.filter(
(pl.col("end_date") <= trade_date) & (pl.col("ann_date") <= trade_date)
)
if len(applicable) > 0:
# 取最新的一条end_date最大的
latest = applicable.tail(1).with_columns(
[pl.lit(trade_date).alias("trade_date")]
)
rows.append(latest)
if rows:
results.append(pl.concat(rows))
if results:
return pl.concat(results)
return pl.DataFrame()

View File

@@ -1,19 +0,0 @@
"""动量因子模块
本模块提供动量类型的因子:
- MovingAverageFactor: 移动平均线(时序因子)
- ReturnRankFactor: 收益率排名(截面因子)
因子分类:
- momentum: 动量因子
- ma: 移动平均线
- return_rank: 收益率排名
"""
from src.factors.momentum.ma import MovingAverageFactor
from src.factors.momentum.return_rank import ReturnRankFactor
__all__ = [
"MovingAverageFactor",
"ReturnRankFactor",
]

View File

@@ -1,78 +0,0 @@
"""动量因子 - 移动平均线
本模块提供通用移动平均线因子,支持参数化配置:
- MovingAverageFactor: 移动平均线(时序因子)
使用示例:
>>> from src.factors.momentum import MovingAverageFactor
>>> ma5 = MovingAverageFactor(period=5) # 5日MA
>>> ma10 = MovingAverageFactor(period=10) # 10日MA
>>> ma20 = MovingAverageFactor(period=20) # 20日MA
"""
from typing import List
import polars as pl
from src.factors.base import TimeSeriesFactor
from src.factors.data_spec import DataSpec, FactorData
class MovingAverageFactor(TimeSeriesFactor):
"""移动平均线因子
计算逻辑对每只股票计算其过去n日收盘价的移动平均值。
特点:
- 参数化因子:训练时通过 period 参数指定计算窗口
- 时序因子:每只股票单独计算,防止股票间数据泄露
Attributes:
period: MA计算期天数默认5
Example:
>>> ma5 = MovingAverageFactor(period=5)
>>> # 计算过去5日的收盘价均值
"""
name: str = "ma"
factor_type: str = "time_series"
category: str = "momentum"
description: str = "移动平均线因子计算过去n日收盘价的均值"
data_specs: List[DataSpec] = [
DataSpec("daily", ["ts_code", "trade_date", "close"], lookback_days=5)
]
def __init__(self, period: int = 5):
"""初始化因子
Args:
period: MA计算期天数默认5日
"""
super().__init__(period=period)
# 重新创建 DataSpec 以设置正确的 lookback_daysDataSpec 是 frozen 的)
self.data_specs = [
DataSpec(
"daily",
["ts_code", "trade_date", "close"],
lookback_days=period,
)
]
self.name = f"ma_{period}"
def compute(self, data: FactorData) -> pl.Series:
"""计算移动平均线
Args:
data: FactorData包含单只股票的完整时间序列
Returns:
移动平均值序列
"""
# 获取收盘价序列
close_prices = data.get_column("close")
# 计算移动平均
ma = close_prices.rolling_mean(window_size=self.params["period"])
return ma

View File

@@ -1,100 +0,0 @@
"""动量因子 - 收益率排名
本模块提供收益率排名因子:
- ReturnRankFactor: 过去n日收益率的rank因子截面因子
使用示例:
>>> from src.factors.momentum import ReturnRankFactor
>>> ret5 = ReturnRankFactor(period=5) # 5日收益率排名
>>> ret10 = ReturnRankFactor(period=10) # 10日收益率排名
"""
from typing import List
import polars as pl
from src.factors.base import CrossSectionalFactor
from src.factors.data_spec import DataSpec, FactorData
class ReturnRankFactor(CrossSectionalFactor):
"""过去n日收益率排名因子
计算逻辑每个交易日计算所有股票过去n日的收益率然后进行截面排名。
特点:
- 参数化因子:训练时通过 period 参数指定计算窗口
- 截面因子:每天对所有股票进行横向排名,防止日期泄露
Attributes:
period: 收益率计算期默认5日
Example:
>>> ret5 = ReturnRankFactor(period=5)
>>> # 每个交易日返回所有股票过去5日收益率的排名
"""
name: str = "return_rank"
factor_type: str = "cross_sectional"
category: str = "momentum"
description: str = "过去n日收益率的截面排名因子"
data_specs: List[DataSpec] = [
DataSpec("daily", ["ts_code", "trade_date", "close"], lookback_days=5)
]
def __init__(self, period: int = 5):
"""初始化因子
Args:
period: 收益率计算期(天数)
"""
super().__init__(period=period)
# 重新创建 DataSpec 以设置正确的 lookback_daysDataSpec 是 frozen 的)
self.data_specs = [
DataSpec(
"daily",
["ts_code", "trade_date", "close"],
lookback_days=period + 1,
)
]
self.name = f"return_{period}_rank"
def compute(self, data: FactorData) -> pl.Series:
"""计算过去n日收益率排名
Args:
data: FactorData包含过去n+1天的截面数据
Returns:
过去n日收益率的截面排名0-1之间
"""
# 获取当前日期的截面数据
cs = data.to_polars()
# 获取所有交易日期(已按日期排序)
trade_dates = cs["trade_date"].unique().sort()
if len(trade_dates) < 2:
# 数据不足,返回空排名
return pl.Series(name=self.name, values=[])
# 获取最新日期的数据
latest_date = trade_dates[-1]
current_data = cs.filter(pl.col("trade_date") == latest_date)
# 获取n天前的日期
n_days_ago = trade_dates[-(self.params["period"] + 1)]
past_data = cs.filter(pl.col("trade_date") == n_days_ago)
# 通过 ts_code join 计算收益率
merged = current_data.select(["ts_code", "close"]).join(
past_data.select(["ts_code", "close"]).rename({"close": "close_past"}),
on="ts_code",
how="inner",
)
# 计算收益率
returns = (merged["close"] - merged["close_past"]) / merged["close_past"]
# 返回排名0-1之间
return returns.rank(method="average") / len(returns)

View File

@@ -1,20 +0,0 @@
"""质量因子模块
本模块提供质量类因子:
- 盈利能力ROE、ROA、毛利率、净利率
- 盈利稳定性:盈利波动率、盈利持续性
- 财务健康度:资产负债率、流动比率等
使用示例:
>>> from src.factors.quality import ROEFactor
>>> factor = ROEFactor()
"""
# 在此处导入具体的质量因子
# from .roe import ROEFactor
# from .roa import ROAFactor
# from .profit_stability import ProfitStabilityFactor
__all__ = [
# 添加你的质量因子
]

View File

@@ -1,20 +0,0 @@
"""情绪因子模块
本模块提供市场情绪类因子:
- 换手率、换手率变化率
- 资金流向、主力净流入
- 波动率、振幅等
使用示例:
>>> from src.factors.sentiment import TurnoverFactor
>>> factor = TurnoverFactor(period=20)
"""
# 在此处导入具体的情绪因子
# from .turnover import TurnoverFactor
# from .money_flow import MoneyFlowFactor
# from .amplitude import AmplitudeFactor
__all__ = [
# 添加你的情绪因子
]

View File

@@ -1,20 +0,0 @@
"""技术指标因子模块
本模块提供技术分析类因子:
- 移动平均线(MA)、指数移动平均(EMA)
- 相对强弱指标(RSI)、MACD、KDJ
- 布林带(Bollinger Bands)等
使用示例:
>>> from src.factors.technical import RSIFactor
>>> factor = RSIFactor(period=14)
"""
# 在此处导入具体的技术指标因子
# from .rsi import RSIFactor
# from .macd import MACDFactor
# from .bollinger import BollingerFactor
__all__ = [
# 添加你的技术指标因子
]

View File

@@ -1,18 +0,0 @@
"""估值因子模块
本模块提供估值类因子:
- 市盈率(PE)、市净率(PB)、市销率(PS)等估值指标
- 估值排名、估值分位数等衍生因子
使用示例:
>>> from src.factors.valuation import PERankFactor
>>> factor = PERankFactor()
"""
# 在此处导入具体的估值因子
# from .pe_rank import PERankFactor
# from .pb_rank import PBRankFactor
__all__ = [
# 添加你的估值因子
]

View File

@@ -1,21 +0,0 @@
"""波动率因子模块
本模块提供波动率相关因子:
- 历史波动率(Historical Volatility)
- 实现波动率(Realized Volatility)
- GARCH类波动率预测
- 波动率风险指标等
使用示例:
>>> from src.factors.volatility import HistoricalVolFactor
>>> factor = HistoricalVolFactor(period=20)
"""
# 在此处导入具体的波动率因子
# from .historical_vol import HistoricalVolFactor
# from .realized_vol import RealizedVolFactor
# from .garch_vol import GARCHVolFactor
__all__ = [
# 添加你的波动率因子
]

View File

@@ -1,20 +0,0 @@
"""成交量因子模块
本模块提供成交量相关因子:
- 成交量移动平均
- 成交量比率(VR)、能量潮(OBV)
- 量价配合指标等
使用示例:
>>> from src.factors.volume import OBVFactor
>>> factor = OBVFactor()
"""
# 在此处导入具体的成交量因子
# from .obv import OBVFactor
# from .volume_ratio import VolumeRatioFactor
# from .volume_ma import VolumeMAFactor
__all__ = [
# 添加你的成交量因子
]