Files
tick-stock-panel/docs/factor-system-design.md
T
shy3130 e0cd625ef4 feat(platform): 因子平台与因子↔策略双向联动 v0.2.3
- 因子平台: /factors 一级页(检验/因子库/编辑器/组合/挖掘), DSL 公式因子(25 算子点选、双语字段、我的因子模板、脏公式守卫), 版本与生命周期, 自动挖掘 L1 统计筛选
- 因子↔策略四条桥: 触发器 Zap 快建因子条件信号、因子一键生成排名策略、自定义信号 AI 提示词接入因子分组、策略回测因子归因(胜/败单入场信号日因子均值, 独立 tab, 双语因子名)
- 回测: 统计卡新增盈亏比(≥1 红/<1 绿), 蒙卡回撤合并为中位/95% 双值卡(自适应字号), 高级设置基础过滤与策略编辑器参数对齐(5 组区间)
- 信号库独立页 /signals(原设置 tab 迁出), 持仓提醒入导航; 挖掘并入因子页第 5 tab, /mining 旧链接重定向
- 研究线配套: 因子目录 61→77(评分/矩阵双内核), stats_v2(Newey-West/BH-FDR/DSR), enriched 管道与异动/报价服务配套调整
- 文档: README 导航与特性表、features.md 因子平台章节、操作说明书 9.2、factor-platform-plan 执行状态与 §5、二开文档桥接说明; 交流与支持节改版
- 版本 0.2.2 → 0.2.3; 后端全量 1625 passed(1 例环境性跳过), 前端 build 通过
2026-09-05 15:41:15 +08:00

843 lines
61 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.
# 因子体系专业化设计(提案)
> **状态声明**:本文是设计提案,**尚未实现**。凡标注【现状】的条目引用当前仓库真实代码(基于 main@2ce8b4b1),可直接核对;凡标注【设计】的条目是目标契约,**不得当作已存在的 API 导入或调用**(遵循 `docs/secondary-development.md` 第 1 节的状态区分要求)。
>
> 全部【现状】引用已于 2026-09-04 逐条核对,Polars API 与依赖可行性已实测(polars 1.40.1),验证记录见 §17;一处初稿引用错误(pipeline.py:1738→1795)已修正。
>
> 涉及改动均按二次开发分级标注(L1 配置 / L2 扩展点 / L3 核心源码修改,见 `docs/secondary-development.md` 第 2 节)。
---
## 0. 设计目标
1. **因子定义单一事实源**:公式、元数据、计算、测试同处一地,可审计、可版本化。
2. **研究结论可辩护**:宇宙可解释、风险调整显式、统计检验完备、指标口径唯一。
3. **策略接入零摩擦**【用户核心诉求】:因子研究成果(含用户自定义因子、复合因子、挖掘产物)以统一形态被策略评分、选股、回测、监控四端消费,一处定义、处处生效。
4. **不推倒重来**:挖掘框架(purge/embargo/嵌套样本外)、enriched 列体系、虚拟评分机制全部保留,只做补层和收口。
---
## 1. 分层总览与现状映射
| 层 | 目标模块 | 现状代码 | 动作 | 分级 |
| --- | --- | --- | --- | --- |
| L-REG 因子注册表 | `app/factors/registry.py`【设计】 | `backtest/factor.py:36` FACTOR_COLUMNS + `strategy/scoring.py` 虚拟因子 + `indicators/pipeline.py` ENRICHED_COLUMNS | 三处合一收口 | L3(重构) |
| L-DSL 表达式层 | `app/factors/dsl/`【设计】 | 无(`strategy/custom_signals.py` 白名单模式可借鉴) | 新增 | L2 |
| L-UNI 宇宙构建 | `app/factors/universe.py`【设计】 | 无(tradable/limit_up_locked/listing_date 素材已存在) | 新增 | L2 |
| L-NEU 风险调整 | `app/factors/neutralize.py`【设计】 | 无(`get_index_daily`、行业 preset、`share_capital.py` 素材已存在) | 新增 | L2 |
| L-INF 统计检验 | `app/factors/stats.py`【设计】 | 无 | 新增 | L2 |
| L-MET 指标统一 | `app/factors/metrics.py`【设计】 | `backtest/engine.py` 三种 Sharpe:2899/:2993/:3116 | 收敛 + 版本化 | L3(热点) |
| L-CMP 复合因子→策略 | `app/factors/composite.py`【设计】 | `strategy/scoring.py` 虚拟评分字段机制【现状·已可用】 | 扩展既有机制 | L2→L3 接线 |
| 数据契约 | provider dataset 声明 | 无 ST 历史/退市股/点时行业 | 新增 dataset | L1YAML+provider 实现 |
模块落点说明:新建 `app/factors/` 包而不是塞进 `backtest/`,因为因子目录、宇宙、中性化被选股(`strategy/`)、回测(`backtest/`)、挖掘(`backtest/mining.py`)三方消费,放任一方都会造成反向依赖(违反 CONTRIBUTING 2.3 模块边界)。
---
## 2. 因子注册表(L-REG
### 2.1 现状问题
因子元数据目前分散在四处,互相漂移无感知:
| 位置 | 内容 | 缺陷 |
| --- | --- | --- |
| `backtest/factor.py:36-109` | 62+ 因子目录(id/label/group/desc | desc 是自然语言,与计算无绑定 |
| `strategy/scoring.py:13-51` | VIRTUAL_SCORING_DEPENDENCIES | 虚拟因子的依赖声明,但与因子目录是两套清单 |
| `strategy/scoring.py:80+` | `scoring_value_expr` | 虚拟因子的 Polars 表达式,硬编码 if-else 分发 |
| `strategy/scoring.py:53-66` | `_ROLLING_SCORING_WARMUP` | 预热窗口第三套清单 |
### 2.2 FactorSpec 完整 schema【设计】
```python
@dataclass(frozen=True)
class FactorSpec:
id: str # 全局唯一,^f_[a-z0-9_]{1,40}$;内置因子保持现有列名不变(如 momentum_20d
version: int # 因子语义版本;公式变更必须 +1,进缓存键
label: str # 中文显示名
group: str # 展示分组(沿用现有:动量/均线偏离/超买超卖/趋势/波动率/量价/…)
kind: Literal["base", "virtual", "composite", "custom"]
# base: 已物化在 enriched parquet
# virtual: 按需由 base 列编译计算(如 ma5_bias
# composite: 复合因子(见 §8
# custom: 用户 DSL 因子(见 §3
expr_factory: Callable[[frozenset[str]], pl.Expr | None] | None
# virtual/custom 的计算:输入可用列集合,依赖不完整返回 None(fail-closed
formula_text: str # 人类可读公式;virtual 由表达式自动生成,base 手写并配特征化测试锁定
dependencies: frozenset[str] # 展开到 enriched base 列(自递归展开 composite/custom 依赖)
direction: Literal["high", "low", "none"]
# 预期信号方向;进复合因子默认权重与 UI 排序展示
unit: Literal["ratio", "pct", "score", "count", "days", "currency", "none"]
# 单位口径,UI 格式化与 sanity check 用(禁止"数值<1 乘 100"启发式)
warmup_bars: int # 历史窗口需求(交易日数);对齐 _ROLLING_SCORING_WARMUP 语义
pit: bool # 是否点时数据依赖(财务因子 = True)
pit_source: Literal["financial_announce", "share_capital_announce", "none"]
asset_types: frozenset[Literal["stock", "etf"]]
incremental_safe: bool # 盘中增量路径(pipeline.py:1795)能否复算;False 则盘中不含该列
scale_free: bool # 跨标的可比(可直接截面排序);如 atr_14 原值 = Falseatr_pct = True
null_policy: Literal["keep", "drop_row"]
# 研究路径默认 keep(不填零,沿用 fundamentals.py 纪律)
stability: Literal["stable", "experimental", "deprecated"]
tags: tuple[str, ...] # 风格标签:"momentum"/"value"/"size"/"lottery"/"liquidity"/…
```
注册表 API(仅内部 Python 接口,不新增 HTTP):
```python
register_factor(spec) # 启动期注册;重复 id 且 version 未增 → 拒绝启动(fail-closed
get_factor(fid) -> FactorSpec
all_factors(asset_type=None, stable_only=False) -> list[FactorSpec]
factor_dependencies(fids) -> frozenset[str] # 递归展开
factor_value_exprs(available_cols) -> dict[str, pl.Expr | None]
```
### 2.3 迁移策略【设计】
1. **特征化测试先行**:固定样本(≥50 只股票 × 含除权日、停牌日、涨跌停日的窗口)快照当前全部 62+ 因子在 enriched 与 `scoring_value_expr` 两条路径的输出,重构后断言逐位一致。扩展 `backend/tests/backtest/test_factor_library_v2.py`
2. `VIRTUAL_SCORING_DEPENDENCIES``scoring_value_expr` 的 if-else 分发、`_ROLLING_SCORING_WARMUP` 逐一改读注册表,**函数签名不变**(`scoring.py` 对外契约保持)。
3. `FACTOR_COLUMNS` 改由注册表生成,`factor.py` 对外常量保留为兼容别名。
4. desc 公式与 `formula_text` 不一致处,以特征化测试输出的实际计算为准修正文档。
### 2.4 因子分类学与补全清单【设计】
现有 11 组保留;补全以下专业常用因子(标注数据依赖,缺数据不注册、不静默):
| 族 | 建议新增 | 公式要点 | 依赖 |
| --- | --- | --- | --- |
| 动量 | 特质动量 `f_idio_mom_20d` | 个股日收益对基准收益回归残差的 20 日累计 | 指数日K(已有) |
| 动量 | 52 周新高接近度 `f_near_high_52w` | close / 250 日最高 close 1 | 已有 |
| 反转 | 短期反转 `f_rev_5d` | momentum_5ddirection=low 的语义化封装) | 已有 |
| 波动 | 已实现波动偏度差、高低频波动分解 | 简化:`f_vol_ratio_short_long` = vol_5d/vol_60d | 已有 |
| 波动(条件) | 条件波动率 `f_ewma_vol` | RiskMetrics EWMA(λ=0.94) 条性日波动年化;比等权 rolling_std 对近端冲击响应更快,低成本低争议 | 已有(`ewm_std` 向量化) |
| 波动(条件) | 波动的波动 `f_vol_of_vol_60d` / 波动区制 `f_vol_regime` | 波动率的滚动 std / EWMA 波动 ÷ 长期波动;区分"高波市场"与"波动突变"A 股风格切换敏感因子 | 已有 |
| 波动(条件) | GARCH(1,1) 条件波动 `f_garch_vol` | **标记 experimental、按需实现**:逐 symbol 递归拟合与全向量化管线冲突,若引入必须走 `numba_runtime.py`【现状】路径或 numba/arch 依赖,先以 EWMA 交付(日频下 EWMA ≈ GARCH 的 90% 价值) | 已有 + numba |
| 量价 | 量价背离 `f_pv_divergence_20d` | vol_price_corr_20d 语义化 | 已有 |
| 流动性 | 非流动性变化 `f_amihud_chg` | amihud_20d / amihud_60d 1 | 已有 |
| 规模 | 流通市值对数 `f_log_float_mv` | ln(历史流通股本 × raw_close)【点时股本,share_capital.py 已有】 | 已有 |
| 价值 | `f_ep_latest``f_ep_ttm` | 1/PE 口径(E/P 比 PE 统计性质更好);ttm 需财务四表滚动 | 财务(已有);ttm 需扩展 |
| 质量 | 应收/存货增速差、商誉/净资产 | 财务表字段 | 财务(需字段核对) |
| 涨停 | 首板/连板区分、炸板后回封率 | 基于 consecutive_limit_ups、炸板列 | 已有 |
| 财务 | SUE(盈余惊喜) | (E_t E_{t-4}) / σ(ΔE, 4期),公告日口径 | 财务历史(已有 `_merge_report_history` |
---
### 2.5 全量因子目录映射表(代码推导,PR-6 注册蓝本)
下表覆盖现有全部 61 个因子【现状:`factor.py:36-109`】,kind 与依赖由 `scoring.py:13-51` VIRTUAL_SCORING_DEPENDENCIES 逐字推导(virtual = 表中出现的键,base = 未出现即已物化列),运行时预热取自 `scoring.py:53-66` `_ROLLING_SCORING_WARMUP` 代码值。direction 列为**建议初值**high=因子值大预期收益高;"待标定"= 振荡类/方向依市场状态,PR-6 注册时依 IC 实证方向标定并允许研究层覆盖)。
| 因子 | 组 | kind | 依赖(virtual 展开) | 预热 | direction |
| --- | --- | --- | --- | --- | --- |
| momentum_5d/10d/20d/30d/60d | 动量 | base | — | 全局120日 | high |
| change_pct | 动量 | base | — | 全局 | high |
| ma5..60_bias5个) | 均线偏离 | virtual | {close, maN} | 全局 | high |
| ema5..60_bias5个) | 均线偏离 | virtual | {close, emaN} | 全局 | high |
| rsi_6 / rsi_14 / rsi_24 | 超买超卖 | base | — | 全局 | 待标定 |
| macd_hist | 趋势 | base | — | 全局 | 待标定 |
| macd_dif_pct / macd_dea_pct / macd_hist_pct | 趋势 | virtual | {close, macd_dif/dea/hist} | 全局 | high |
| kdj_k / kdj_d / kdj_j | 趋势 | base | — | 全局 | 待标定 |
| boll_position | 趋势 | virtual | {close, boll_upper, boll_lower} | 全局 | high |
| annual_vol_20d | 波动率 | base | — | 全局 | low |
| atr_14 | 波动率 | base | — | 全局 | 待标定 |
| atr_pct | 波动率 | virtual | {close, atr_14} | 全局 | low |
| amplitude | 波动率 | base | — | 全局 | low |
| boll_width | 波动率 | virtual | {ma20, boll_upper, boll_lower} | 全局 | low |
| vol_ratio_5d | 量价 | base | — | 全局 | 待标定 |
| vol_ratio_10d | 量价 | virtual | {volume} | 11 | 待标定 |
| vol_trend_5_10 | 量价 | virtual | {vol_ma5, vol_ma10} | 全局 | high |
| turnover_rate | 量价 | base | — | 全局 | low |
| turnover_ratio_5d | 量价 | virtual | {turnover_rate} | 6 | high |
| log_amount | 量价 | virtual | {amount} | 全局 | 待标定 |
| amount_ratio_5d | 量价 | virtual | {amount} | 6 | high |
| gap_return | 价格位置 | virtual | {open, prev_close} | 全局 | 待标定 |
| intraday_return | 价格位置 | virtual | {open, close} | 全局 | 待标定 |
| close_position | 价格位置 | virtual | {high, low, close} | 全局 | 待标定 |
| distance_to_high_60d | 价格位置 | virtual | {close, high_60d} | 全局 | high |
| distance_from_low_60d | 价格位置 | virtual | {close, low_60d} | 全局 | high |
| vwap_bias | 价格位置 | virtual | {close, volume, amount} | 全局 | 待标定 |
| max_ret_20d | 收益形态 | virtual | {close} | 21 | low |
| ret_skew_20d | 收益形态 | virtual | {close} | 21 | low |
| up_days_20d | 收益形态 | virtual | {close} | 21 | 待标定 |
| amihud_20d | 流动性 | virtual | {close, amount} | 21 | low |
| turnover_z_60d | 流动性 | virtual | {turnover_rate} | 61 | 待标定 |
| vol_price_corr_20d | 量价 | virtual | {close, volume} | 21 | 待标定 |
| vol_trend_5_60 | 量价 | virtual | {volume} | 60 | high |
| limit_up_count_20d | 涨停基因 | virtual | {consecutive_limit_ups} | 21 | high |
| limit_up_count_60d | 涨停基因 | virtual | {consecutive_limit_ups} | 61 | high |
| pb_latest | 财务 | base(点时联表) | — | 公告日机制 | low |
| roe_latest | 财务 | base(点时联表) | — | 公告日机制 | high |
| gross_margin_latest | 财务 | base(点时联表) | — | 公告日机制 | high |
| net_margin_latest | 财务 | base(点时联表) | — | 公告日机制 | high |
| revenue_yoy_latest | 财务 | base(点时联表) | — | 公告日机制 | high |
| net_income_yoy_latest | 财务 | base(点时联表) | — | 公告日机制 | high |
| debt_ratio_latest | 财务 | base(点时联表) | — | 公告日机制 | low |
计数核对:virtual 35 + base 非财务 19 + 财务 7 = 61,与 FACTOR_COLUMNS 一致。base 因子的研究预热由 `FACTOR_WARMUP_DAYS=120``factor.py:111`【现状】)统一承担;财务因子 pit=true、pit_source=financial_announce。
## 3. 因子表达式层(L-DSL
### 3.1 语法与算子表【设计】
表达式 = `expr ::= operand | expr op expr | func(expr[, expr[, const]])`;中缀 + 函数调用,无变量赋值、无循环。
**操作数**:基准列(open/high/low/close/volume/amount/turnover_rate/prev_close/raw_close)、白名单指标列(注册表中 base 因子)、已注册因子 idvirtual/composite/custom,递归内联展开)、数值常量。
**时序算子**`over("symbol")`,窗口 n ∈ [2, 512],全部只向后看):
| 算子 | 语义 | Polars 编译 |
| --- | --- | --- |
| `ts_mean(x,n)` / `ts_std(x,n)` / `ts_sum(x,n)` | 滚动均值/样本标准差/求和 | `rolling_mean/std/sum(n)` |
| `ts_max(x,n)` / `ts_min(x,n)` | 滚动极值 | `rolling_max(n)` / `rolling_min(n)` |
| `ts_delta(x,n)` | x ts_delay(x,n) | `x - x.shift(n)` |
| `ts_delay(x,n)` | n 期前的值(n ∈ [1, 512],**禁止负数**——负数即未来函数,编译期报错) | `x.shift(n)` |
| `ts_rank(x,n)` | 当期值在滚动窗口内的分位 | `rolling_rank(n)`【已验证:polars 1.40.1 存在且行为正确,§17】 |
| `ts_zscore(x,n)` | (x ts_mean)/ts_std | 组合表达式 |
| `ts_corr(x,y,n)` / `ts_cov(x,y,n)` | 滚动相关/协方差 | 顶层函数 `pl.rolling_corr(x,y,window_size=n)` / `pl.rolling_cov`【已验证:Expr 上无此方法,必须走顶层函数,§17】 |
| `ts_quantile(x,n,q)` | 滚动分位(q ∈ (0,1) 常量) | `rolling_quantile` |
| `decay_linear(x,n)` | 线性衰减加权均值(近端权重大) | 手写权重组合表达式 |
**v1 不提供的时序算子及原因**`ts_argmax/ts_argmin`——Polars 无向量化实现(`rolling_map` 为逐窗 Python 回调,违反向量化约束,已验证 Expr 无 `rolling_arg_max`);"距极值天数"类需求以具体因子的组合表达式实现(如 `distance_to_high_60d` 模式),确有高频需求再经 numba 扩展。
**截面算子**(按日期分组,逐日横截面):
| 算子 | 语义 | 说明 |
| --- | --- | --- |
| `rank(x)` | 横截面百分位排名 ∈ (0,1] | null 不参与排名 |
| `zscore(x)` | 横截面 (x−μ)/σ | σ=0 → null |
| `winsorize(x,k)` | 截尾至 μ±kσ(k ∈ [1,6] 常量,默认 3) | 截面口径 |
**算术/工具**`+ * /`(除零 → null)、`log abs sign sqrt min max power(x,c) clamp(x,lo,hi)`、三元 `if_else(cond, a, b)`、比较与逻辑 `> >= < <= == != and or not`(产出布尔,配合 if_else)。
### 3.2 校验规则(编译期全部强制)【设计】
1. 标识符必须在基准列/白名单/已注册因子内,否则报错(防注入,沿用 `custom_signals.py` 白名单哲学)。
2. `ts_delay`/`ts_delta` 的 n ≥ 0;任何窗口 n ∈ [2, 512]`power` 指数 |c| ≤ 4AST 深度 ≤ 12;表达式 token 数 ≤ 200。
3. 常量折叠后若产生 `x/0` 类静态除零 → 编译失败。
4. 依赖列集合 = 递归展开;warmup_bars = max(各 ts 算子窗口);超出即注册表标记,研究 UI 提示所需历史长度。
5. 产出类型必须为数值或布尔(布尔经 `cast` 视为 0/1)。
6. **禁止未来引用的总闸**:所有时序算子 shift 语义已内建,语法层不存在负 shiftcode review checklist 补一条"新增算子必须只向后看"。
### 3.3 编译流水线【设计】
`text → tokenizer → Pratt 解析 → AST → 语义检查(§3.2) → 依赖/预热推导 → Polars Expr 工厂`。产出缓存(表达式文本 → 编译产物 LRU,键含依赖列版本);编译失败返回结构化错误(位置 + 原因),不抛裸异常。
### 3.4 形式文法(EBNF)与错误码目录【设计】
```ebnf
expr = or_expr ;
or_expr = and_expr { "or" and_expr } ;
and_expr = cmp_expr { "and" cmp_expr } ;
cmp_expr = add_expr [ (">" | ">=" | "<" | "<=" | "==" | "!=") add_expr ] ;
add_expr = mul_expr { ("+" | "-") mul_expr } ;
mul_expr = unary { ("*" | "/") unary } ;
unary = "-" unary | primary ;
primary = NUMBER | IDENT | func_call | "(" expr ")" ;
func_call = IDENT "(" [ arglist ] ")" ;
arglist = expr { "," expr } ;
(* IDENT:基准列/白名单指标列/已注册因子 id/算子名;NUMBER:十进制与负号经 unary 处理 *)
```
运算符优先级由产生式层级固定(or < and < 比较 < 加减 < 乘除 < 一元负号 < 原子),与 Python/JS 语义一致,降低用户迁移成本。
**错误码目录**(编译与运行校验的唯一错误词汇表,API/编辑器/UI 共用):
| 码 | 含义 | 触发 |
| --- | --- | --- |
| E001 | 未知标识符 | IDENT 不在白名单/注册表 |
| E002 | 未知函数 | 函数名不在算子表 |
| E003 | 参数数量/类型不符 | 算子签名不匹配(含常量参数位置) |
| E004 | 窗口越界 | n∉[2,512] 或 q∉(0,1) |
| E005 | 负 shift | ts_delay/ts_delta 的 n<0 |
| E006 | 嵌套深度超限 | AST 深度>12 |
| E007 | 规模超限 | token 数>200 |
| E008 | 静态除零 | 常量折叠检出分母恒 0 |
| E009 | 产出类型非法 | 非数值/布尔 |
| E010 | power 指数越界 | \|c\|>4 |
| E011 | winsorize k 越界 | k∉[1,6] |
| E012 | 循环引用 | 因子依赖成环(含自定义因子链) |
| E013 | 依赖列不可用 | 面板缺列(运行时) |
| E014 | 语法错误 | 解析失败(附位置) |
| E015 | 预热不足 | warmup > 研究窗口(运行时) |
| E016 | 常量表达式 | 无任何标识符,拒绝保存 |
错误响应统一结构:`{"code": "E001", "message": "未知标识符: clos", "position": {"offset": 12, "line": 1}, "detail": {...}}`
### 3.5 用户因子生命周期与存储【设计】
- 存储路径:`data/user_data/custom_factors/*.json`(对齐 custom_signals 目录约定),schema
```json
{
"id": "uf_my_rev", // ^uf_[a-z0-9_]{1,40}$,前缀与 csg_ 同哲学
"version": 1,
"label": "我的反转因子",
"formula": "rank(-ts_sum(change_pct, 5))",
"direction": "low",
"description": "5 日累计涨幅的截面倒数",
"created_at": "2026-09-04T00:00:00",
"updated_at": "2026-09-04T00:00:00"
}
```
- 生命周期:草稿(编辑器内试算,不落盘)→ 保存(编译通过 + 试算有非空输出才可保存,fail-closed)→ 引用(策略 scoring / 因子研究 / 复合因子)→ 版本化(公式变更 version+1,旧结果按 version 键隔离)→ 删除(有引用时列出引用方并二次确认,对齐策略删除的 fail-closed 要求)。
- 加载失败的单个文件只禁用该因子并提示,不影响启动与其他因子(对齐 plugins 隔离要求,CONTRIBUTING 第 4 节)。
---
## 4. 宇宙构建(L-UNI
### 4.1 UniverseSpec 完整 schema【设计】
```python
@dataclass(frozen=True)
class UniverseSpec:
exclude_suspended: bool = True # 停牌(tradable 矩阵口径,matrix.py:1476+
exclude_limit_locked: bool = True # 调仓时点一字涨停不可买入者(buy_limit_up 口径)
exclude_st: bool = False # 非点时(今日名称),开启时报告中必须出现降级注记
min_listing_days: int = 0 # 次新剔除;listing_date 已入库未使用(api/data.py:765
min_amount_quantile: float | None = None # 流动性过滤:当日成交额截面分位下限 (0,1)
cap_quantile_range: tuple[float, float] | None = None # 市值分位区间;依赖 f_log_float_mv
max_names: int | None = None # 截面数量上限;排序键由研究上下文显式传入(因子值或复合分,不隐式默认),用于微型宇宙研究
```
### 4.2 执行语义【设计】
- 按日生成 `universe[date] -> set[symbol]`,**每个过滤条件独立短路、独立计数**,产出 `filter_stats`(每日各过滤器剔除数),研究报告展示"宇宙从 5200 → 4980 → 4890"漏斗。
- `universe_id = sha256(canonical_json(spec))[:12]`,进一切下游缓存键。
- 过滤顺序固定(先便宜的列过滤,后需联表的),顺序本身进 canonical_json。
- **as_of 语义**:宇宙内一切判定只用当日及以前数据。ST 例外必须显式标注 `degraded: ["st_not_point_in_time"]` 并在报告 UI 渲染黄条。
### 4.3 数据缺口降级矩阵【设计】
| 过滤器 | 数据缺失时行为 |
| --- | --- |
| exclude_suspended / exclude_limit_locked | 素材必在(enriched 必算列);缺失 = 数据本身异常 → fail 报错 |
| exclude_st | instruments 名称缺失 → 过滤器跳过 + 降级注记(不静默假装过滤了) |
| min_listing_days | listing_date 缺失的标的视为"不满足"剔除(保守),计数展示 |
| cap_quantile_range | 历史股本缺失标的退出该过滤(不参与分位),降级注记 |
| 退市股(未来) | 依赖新 dataset(§9);无数据源时 universe 定义退化为"当前上市 ∪ 本地历史",报告中永久注记幸存者偏差警示 |
---
## 5. 风险调整与基准(L-NEU
### 5.1 NeutralizationSpec【设计】
```python
@dataclass(frozen=True)
class NeutralizationSpec:
benchmark: str | None = "000001.SH" # 上证指数(index_const.py:12【现状】核心四只之一);扩展指数见 §9
return_basis: Literal["raw", "excess"] = "excess" # IC/分层收益口径
method: Literal["none", "industry_demean", "industry_zscore", "regression_industry_size"] = "none"
winsorize_sigma: float | None = 3.0 # 因子值截面截尾;None = 不截尾
# neutralization_id = sha256(canonical_json)[:12],进缓存键
```
### 5.2 方法规格【设计】
- 超额收益:`r_ex = r_stock r_bench`(基准同日收益;基准停市日沿用最近交易日,日历由数据轴驱动)。
- `industry_demean``f' = f mean_ind(f)`THS 行业一级,ext preset 快照)。
- `industry_zscore`:组内标准化 `f' = (f μ_ind)/σ_ind`(σ=0 组 → null)。
- `regression_industry_size``f ~ 1 + 行业哑变量 + log_float_mv` 的残差(逐日 OLSPolars 表达式实现,n<30 或共线 → 回退 demean + 注记)。
- 固定管线顺序:`宇宙过滤 → winsorize → 中性化 → 标准化(zscore 或 rank)`;顺序进 spec 哈希。
- **行业快照局限**(当前归属回填历史)写入 `degraded` 注记并在报告显示;点时行业表到位后(§9)仅切换数据源,spec 不变。
### 5.3 报告口径并列【设计】
IC 报告同时输出三列:`原始 / 超额 / 超额+中性化`,默认排序以最后一列为准——旧结论可查,新结论更严,不静默替换。
---
## 6. 统计检验(L-INF
**依赖原则**:后端当前无 scipy/statsmodels(已验证,§17),运行时保持零新增第三方依赖——NW/BH-FDR/DSR 全部以 numpy 手写实现(各约 20-40 行);statsmodels 仅允许加入 uv dev 依赖组用于测试对拍,不进运行时 import。
### 6.1 统计量精确定义【设计】
| 统计量 | 定义 | 备注 |
| --- | --- | --- |
| Rank IC | 逐日 Spearman(factor_t, fwd_ret_{t→t+h}),现有口径不变(factor.py:740-750 | — |
| IC t 值(朴素) | `t = mean(IC) / (std(IC, ddof=1)/√N)` | 仅作对照展示 |
| IC t 值(NW | Newey-West HAC 稳健标准误,滞后 `L = h`(h 日前瞻收益使 IC 序列存在 h−1 阶移动平均自相关) | **主口径**numpy 手写 Bartlett 核加权,测试用固定黄金参考向量 + 可选 dev 组 statsmodels 对拍 |
| ICIR | mean(IC)/std(IC),已有 | — |
| IC 自相关 & 半衰期 | ACF(1..10);半衰期 = ACF 首次 < 0.5 的滞后(线性插值);无收敛 → null | 换手率预期管理 |
| 分层单调性 | Spearman(组序号, 组均超额收益) + 线性趋势斜率 t 值 | 判定"梯子是否成立" |
| 多空 t 值 | 顶组−底组日超额收益序列的 NW t | 滞后 = 调仓周期的收益重叠阶数 |
| BH-FDR q 值 | 对 optimizer/mining 排行榜全体 p 值(每行 = 其 OOS/IS 最优组合的 IC 或收益 t 值双尾 p)做 Benjamini-Hochbergq_i = min_{j≥i}(N·p_j/j) 单调化 | 排行榜级,不进单因子报告 |
| Deflated Sharpe | Bailey-López de Prado:以试验次数 N(挖掘 trial 预算已计数,mining.py:1041+)与偏度峰度校正 SR₀,DSR = Φ((SRSR₀)·√(T1) / √(1−γ̂₃SR+((γ̂₄−1)/4)SR²)) | 挖掘晋升报告展示"考虑搜索后的置信" |
| 覆盖率/换手率 | 已有(factor.py:1123-1166),补充宇宙过滤后口径 | — |
### 6.2 显著性标注约定【设计】
|t| < 1.645 无标注;≥1.645 `*`(10%);≥1.96 `**`(5%);≥2.576 `***`(1%)。报告 UI 图标化,q ≥ 0.10 的挖掘候选禁止晋升(现有晋升门槛 mining.py:26-31 之上叠加,未达标给出具体差值)。
### 6.3 版本命名规则与报告完整字段【设计】
**方法论版本命名**(单一规则,全文档统一):
- `factor_v3` = 本设计交付的因子研究方法论(三口径 + 统计检验 + 宇宙/中性化 spec 进键);现有 `factor_v2`factor.py:112【现状】)结果按旧版本读取展示,不重算。
- `metrics_v2` = §7 指标统一后的口径;与 `factor_v3` 独立演进,报告分别携带。
- 因子个体 `version`(FactorSpec)与研究方法论版本正交:因子公式变更不改方法论版本,反之亦然。
**IC 研究报告完整字段定义**`POST /api/factor-research/ic` 响应,§10 示例为其节选):
```text
methodology_version: str # "factor_v3"
factor_id / factor_version: str / int
universe_id / neutralization_id: str # 两 spec 哈希
universe: object # 回显生效 UniverseSpec
neutralization: object # 回显生效 NeutralizationSpec
date_range: {start, end, rebalance, n_groups}
ic: {mean, std, icir, t_naive, t_newey_west, nw_lag, significance,
half_life_days|null, acf: float[10], win_rate, coverage, n_days}
ic_decay: [{horizon, ic_mean, icir}] # 沿用现有 1/3/5 日结构
ic_yearly: [{year, ic_mean, icir, n_days}] # 沿用现有结构
ic_by_basis: {raw, excess, excess_neutralized} × {mean, t_newey_west}
monotonicity: {spearman, trend_t, verdict} # verdict ∈ 成立/弱/不成立
groups: [{group, excess_return_annual, nav, turnover, n_names,
t_stat, avg_name_count}] # 每组含 t 值
long_short: {annual_return, t_newey_west, max_drawdown, executable_short: false}
turnover_top_group: float
costs: {commission_pct, stamp_tax_pct, slippage_bps, round_trip}
universe_funnel: [{date, raw, after_suspended, after_limit_locked,
after_st|null, after_new_listing|null, after_filters}]
degraded: [str] # 如 st_not_point_in_time / industry_snapshot
warnings: [str] # 非降级类提示(预热边界、覆盖不足等)
```
新增字段全部带默认值,历史(factor_v2)缓存结果缺字段时前端显示为空,不报错。
---
## 7. 绩效指标统一(L-MET
### 7.1 唯一口径【设计】
- **Sharpe**:净值曲线日收益 `r_t = nav_t/nav_{t1} 1``Sharpe = mean(r)/std(r, ddof=1) × √A`;无风险利率参数 `rf_annual`(默认 0,单位/年,日化按 A 折算)。引擎三种旧口径(逐笔/仅卖出日/净值)收敛为净值口径;前两者字段保留一个版本周期并标 `deprecated_mode`
- **年化天数 A**:默认 243(近五年 A 股实际均值区间),`metrics_methodology_version = "metrics_v2"`;报告展示口径徽章。
- **年化收益**`(nav_T/nav_0)^(A/n_bars) 1`(统一按 K 线数折算,废除 365.25 自然日混用,engine.py:2871-2874 收敛)。
- **MaxDD**:现有算法(峰值下限 1.0)不变。
- **基准相对新增**`excess_annual``tracking_error = std(rr_b)×√A``information_ratio = mean(rr_b)/std(rr_b)×√A``beta/alpha`OLSrf 处理同上)、`excess_win_rate`
- `engine.py`/`strategy.py`/`factor.py` 全部改 import `app/factors/metrics.py`,禁止本地重算(Ruff 检查加入 noqa 禁用清单之外无豁免)。
---
## 8. 复合因子与策略接入(L-CMP)——核心章节
### 8.1 现有桥(【现状·已可用】,设计的锚点)
- 策略配置:`"scoring": {"factor_name": weight}` + `scoring_directions` 覆盖高低方向(`builtin/*.py` 均此形态)。
- `scoring.py`:虚拟因子按需编译 Polars 表达式(`scoring_value_expr`)、依赖展开(`scoring_dependencies`)、预热推导(`scoring_warmup_bars`)。
- 回测矩阵按 `score` 排序建仓(`engine.py` `max_positions` + `score_min/max`)。
- 挖掘产物 = 因子排名组合(mining.py),候选库 `candidates.py` 已有 `factor`/`strategy` 双形态。
### 8.2 FactorCompositeSpec【设计】
```python
@dataclass(frozen=True)
class FactorCompositeSpec:
id: str # ^cf_[a-z0-9_]{1,40}$;策略 scoring 里以 "cf_xxx" 引用
version: int
label: str
factors: tuple[CompositeMember, ...] # 1..10 个成员
transform: Literal["rank", "zscore"] = "rank" # 成员标准化方式(截面)
weighting: Literal["manual", "equal", "icir", "max_ic"] = "manual"
auto_weight_window: int = 504 # 自动权重的滚动窗口(交易日)
direction: Literal["high", "low"] = "high"
@dataclass(frozen=True)
class CompositeMember:
factor: str # 任意已注册因子 id(含 uf_/cf_ 前缀,禁止自引用,环检测)
weight: float | None # manual 模式必填;自动模式忽略
direction_override: Literal["high", "low"] | None = None
```
**计算管线(顺序固定,进 spec 哈希)**
```
宇宙(可选, 默认不过滤以兼容现有策略)
→ 各成员因子值(注册表展开,含 warmup 检查)
→ winsorize(3σ, 可关)
→ 截面 transformrank/zscore
→ 方向统一(low → 取负)
→ 加权求和(manual 权重归一化校验 |Σw1|<1e-9
icir → w_i ∝ max(ICIR_i, 0)ICIR 取 (tauto_weight_window, t1] 窗口——**权重只用于过去,严禁 t 日数据参与 t 日权重**;
max_ic → 同窗口 mean(rank IC) 单调权重)
→ 输出复合分 cf_xxxrank 基础下近似 ∈ [−1,1],文档声明分布性质)
```
**接入策略(零引擎改动)**
- 复合因子注册进注册表(kind="composite"),`scoring_value_expr` 机制天然支持:策略写 `"scoring": {"cf_hotmom": 0.6, "vol_ratio_5d": 0.2, "amount": 0.2}` 即生效;依赖/预热自动递归展开进矩阵构建,选股/回测/监控三端无需感知"这是复合因子"。
- 挖掘产物一键导出为 CompositeSpecmining 的排名组合本来 = factors+weights,导出即 `cf_mined_<run>`,闭合"挖掘 → 复合因子 → 策略"回路。
- 前置校验:成员因子任一 warmup 超研究窗口 → 启动期注册成功但使用时返回明确"预热不足"错误(不产出半截分数)。
**策略侧引用形态(确切 JSON)**——复合因子编辑器"导出 scoring 片段"产出,直接粘贴进策略配置:
```json
{
"scoring": {"cf_hotmom": 0.6, "vol_ratio_5d": 0.2, "amount": 0.2},
"scoring_directions": {"cf_hotmom": "high", "amount": "high"}
}
```
复合因子与普通字段混用、权重语义不变;`scoring_dependencies`/`scoring_warmup_bars` 自动递归展开(`scoring.py:91-104`【现状】机制不动,仅数据源换成注册表)。
### 8.3 一致性契约【设计】
同一 `cf_xxx` 在**因子研究(IC/分层)、选股、回测、监控**四端必须逐位同值——单测直接断言四路径对同一 (date,symbol) 的输出相等。这是 CONTRIBUTING 5.3"同一候选集和排序方向"的推广。
### 8.4 监控端数据流澄清【设计】
监控不重算复合因子:`monitor.py:1314-1322`【现状】消费的是**策略结果缓存**里的 `result.scores``score_min/score_max` 过滤)。因此复合因子进监控的路径 = 策略执行时算好分 → 结果缓存 → 监控读缓存。**实时行情线程零新增计算**(CONTRIBUTING 6.3 硬约束)。推论:修改复合因子定义后,必须走策略参数变更的既有失效链路(重算策略结果缓存 → 监控实例刷新),该链路已存在(CONTRIBUTING 5.1),设计只复用不新造。
### 8.5 自定义/复合因子的盘中行为【设计】
- 选股(盘后批量):`incremental_safe=True` 的成员因子照常参与当日计算。
- `incremental_safe=False` 成员(若有):当日选股对该因子返回"预热/路径不足"的明确不可计算状态(对齐 CONTRIBUTING 5.1"空值不得伪装成零分"),UI 标注原因;**不降级用部分成员算半截复合分**。
- 盘中增量路径(`pipeline.py:1795` `compute_enriched_today`【现状】):自定义与复合因子默认不进入(§12 缓存策略),分时选股若引用则同样返回不可计算状态,盘后恢复。
---
## 9. 数据契约扩展(provider dataset
新增 dataset 声明(capabilities.py 注册表 + 对应 provider 实现,均【设计】):
| dataset | 内容 | 解锁能力 | 无数据源时 |
| --- | --- | --- | --- |
| `st_history` | 点时风险警示状态 | 历史涨跌停幅度修正、宇宙 ST 点时过滤 | 涨跌停用当前名推断 + 注记(现状) |
| `delisted_kline` | 退市标的日 K + 退市维表 | 幸存者偏差修复(宇宙回补) | 报告永久幸存者注记 |
| `industry_pit` | 点时行业归属 | 中性化升级为点时 | 行业快照 + 注记(现状) |
| `index_ext` | 扩展指数日 K | 基准升级(当前限核心四只,index_const.py:12-15【现状】) | 基准限核心四只 |
各 dataset 完整字段 schemaprovider 归一后落 Parquet,命名对齐现有 normalized 契约):
**`st_history`**(分区 `data/parquet/st_history/`,按年):
| 字段 | 类型 | 语义 |
| --- | --- | --- |
| symbol | str | 标准代码(如 600000.SH |
| flag_date | date | 状态生效日(戴帽/摘帽公告后的首个交易日) |
| st_flag | bool | true=风险警示(ST/*ST),false=摘帽;行区间语义:自 flag_date 起至下一条记录 |
| flag_type | str | "ST" / "*ST" / "摘帽";缺失填 "ST" |
| source | str | provider 标识 |
查询语义:`st_at(symbol, t) = flag_date ≤ t 的最后一条记录的 st_flag`asof-backward)。同步:全量快照 + 增量 append,`(symbol, flag_date)` 去重幂等(对齐 kline_sync:358-367【现状】模式)。
**`delisted_kline`**K 线部分复用 `daily` dataset 完整 schemasymbol/date/OHLC/volume/amount/…);另需维表 `delisted_instruments`
| 字段 | 类型 | 语义 |
| --- | --- | --- |
| symbol / name | str | 代码/退市前简称 |
| list_date / delist_date | date | 上市/退市日 |
| delist_reason | str | "面值"/"财务"/"重组"/"主动"/"其他";缺失填 "其他" |
宇宙回补语义:`as_of=t 的可交易池 = instruments(上市≤t<退市) delisted(上市≤t<退市)``_resolve_universe`daily_pipeline.py:92-125【现状】)扩展为两源合并。
**`industry_pit`**(分区 `data/parquet/industry_pit/`):
| 字段 | 类型 | 语义 |
| --- | --- | --- |
| symbol | str | 标准代码 |
| effective_date | date | 归属生效日 |
| industry_l1 / industry_l2 | str | 一级/二级行业名(如 计算机/软件开发,对齐 market_mainline.py:35【现状】的两级口径) |
| source | str | provider(如 ths |
查询语义:asof-backward join(同 fundamentals.py:107-114【现状】模式);effective_date 缺失 = 供应商不提供历史,整表降级为快照并触发 §5 注记。
**`index_ext`**schema 与 `kline_index_daily` 完全一致(symbol/date/OHLC/volume/amount),仅标的白名单扩展(默认建议:000300.SH 沪深300、000905.SH 中证500、000852.SH 中证1000、000985.SH 中证全指);白名单由 preset 配置声明,不进代码硬编码(对齐 ext_presets 模式)。
**能力注册示例**provider 侧 `plugin.yaml` datasets 声明,对齐 `docs/plugin-development.md` 契约):
```yaml
datasets:
st_history:
enabled: true
description: 点时风险警示状态(戴帽/摘帽区间)
delisted_kline:
enabled: true
description: 退市标的日K + 退市维表
industry_pit:
enabled: false # 供应商无历史归属时声明 false,不注册能力
description: 点时行业归属
```
能力矩阵(`capabilities.py` 注册表)同步各 dataset 的展示元数据与路由偏好字段;provider 未声明 = 该能力全局不可用,研究路径按 §4.3 降级矩阵处理,不静默。
---
## 10. API 契约【设计】
新路由前缀 `/api/factors`(薄层,重计算在 services/factors_research.py 编排层):
| 端点 | 方法 | 请求要点 | 响应要点 |
| --- | --- | --- | --- |
| `/api/factors/catalog` | GET | asset_type, group, stability 过滤 | 因子清单(含 formula_text/warmup/direction/pit/scale_free/usage_count |
| `/api/factors/validate` | POST | formula 文本 | 编译错误(位置+原因)或成功(依赖/预热推导) |
| `/api/factors/preview` | POST | formula + symbols + date_range | 试算表格(最新 5 日 × 前 20 标的)+ 非空率 |
| `/api/factors/custom` | GET/POST/DELETE | §3.4 JSON | CRUD;删除带引用清单 |
| `/api/factor-research/ic` | POST | factor_id, universe_id/UniverseSpec, NeutralizationSpec, start/end, rebalance, n_groups | §6 全套统计 + 三口径并列 + 漏斗 filter_stats + degraded 注记 |
| `/api/factor-research/composite` | GET/POST/DELETE | CompositeSpec | CRUD + 一键"作为评分字段试策略"跳转链接 |
| `/api/factor-research/universes` | GET/POST | UniverseSpec 存档 | 命名宇宙 CRUD(研究配置复用) |
全部响应新增字段带默认值;错误响应含 `code/message/detail`,不泄漏内部栈(CONTRIBUTING 第 8 节)。SSE 进度复用现有回测 SSE 模式(长任务:批量 IC 扫描)。
**鉴权**:新路由经 `api/routes.py`【现状】注册,继承应用级部署口令鉴权(`docs/deploy-password.md` 模式),不引入独立权限模型。写操作(自定义因子/复合因子/宇宙存档 CRUD)只落 `data/user_data/`,路径校验沿用策略目录的防穿越规则(CONTRIBUTING 5.1 删除策略 fail-closed 要求同样适用)。
**并发**:批量 IC 扫描与复合分批量计算走 `services/heavy_job_limiter.py`【现状】限流,SSE 进度事件结构复用回测现有契约;用户取消走现有回测 worker 取消机制。
**核心端点示例**(其余端点按同构风格推导):
`GET /api/factors/catalog?group=动量&asset_type=stock`
```json
{
"factors": [
{
"id": "momentum_20d", "version": 1, "label": "20日动量", "group": "动量",
"kind": "base", "formula_text": "20个交易日累计收益率",
"direction": "high", "unit": "ratio", "warmup_bars": 20,
"pit": false, "scale_free": true, "stability": "stable",
"tags": ["momentum"], "usage_count": 7, "custom": false
}
],
"total": 61, "degraded": []
}
```
`POST /api/factors/validate` `{"formula": "rank(ts_delta(close, -5))"}`
```json
{"ok": false, "errors": [{"code": "E005", "message": "负 shift: ts_delay 的 n 必须 ≥ 0(负数即未来函数)", "position": {"offset": 18, "line": 1}, "detail": {"n": -5}}]}
```
`POST /api/factor-research/ic`
```json
{
"factor_id": "momentum_20d",
"universe": {"exclude_suspended": true, "exclude_limit_locked": true, "exclude_st": true, "min_listing_days": 60},
"neutralization": {"benchmark": "000001.SH", "return_basis": "excess", "method": "industry_demean", "winsorize_sigma": 3.0},
"start": "2023-01-01", "end": "2025-12-31",
"rebalance": "monthly", "n_groups": 5
}
```
响应(节选,完整字段见 §6):
```json
{
"methodology_version": "factor_v3",
"universe_id": "a1b2c3d4e5f6", "neutralization_id": "9f8e7d6c5b4a",
"ic": {"mean": 0.031, "icir": 0.42, "t_naive": 2.9, "t_newey_west": 1.87, "nw_lag": 1,
"significance": "*", "half_life_days": 4, "acf": [0.21, 0.08, ...], "coverage": 0.97},
"ic_by_basis": {"raw": {"mean": 0.041, "t_newey_west": 2.2}, "excess": {"mean": 0.031, "t_newey_west": 1.87},
"excess_neutralized": {"mean": 0.019, "t_newey_west": 1.02}},
"monotonicity": {"spearman": 0.9, "trend_t": 2.4, "verdict": "成立"},
"universe_funnel": [{"date": "2025-12-31", "raw": 5412, "after_suspended": 5390, "after_limit_locked": 5320, "after_st": 5180, "after_new_listing": 5090}],
"degraded": ["st_not_point_in_time", "industry_snapshot"],
"turnover_top_group": 0.31, "costs_round_trip": 0.0013
}
```
---
## 11. 前端界面【设计】
按此前结论:**不新增顶层页面**,组件级落点。每个组件给出区块级线框与交互流:
1. **ResearchProfile 共享面板**(新组件,因子回测/挖掘/验证三视图共用):UniverseSpec + NeutralizationSpec 的受控表单,可存档命名(对应 `/api/factor-research/universes`);degraded 注记黄条;查询键含两 spec 哈希(queryKeys.ts 集中新增 `factorResearch` 键族)。
线框:`[存档下拉 ▾] [另存为] | 折叠区1·宇宙(6 个过滤器开关/输入 + 漏斗摘要行) | 折叠区2·调整(基准/口径/方法/截尾) | [重置] [应用到当前视图]`;spec 哈希变化即触发查询键切换。
2. **因子目录对话框**(因子回测 tab 内,仿 ResearchCandidatesDialog):分组树 + 搜索 + 公式/方向/预热/PIT 徽章/引用数;"研究此因子"按钮回填选择器。
线框:`左侧分组树(带计数) | 右侧表格[因子/公式/方向/预热/PIT/引用] | 底部[研究此因子][加入复合候选]`;"加入复合候选"把因子暂存到复合编辑器的选择篮(跨组件轻状态,放 TanStack Query 缓存而非全局 store)。
3. **IC 报告增强**:t 值列(NW 主口径,显著性星标)、三口径并列、单调性判定、IC 半衰期、宇宙漏斗、降级注记条。
线框:IC 摘要卡新增 `t(NW)=2.31** 半衰期=4d 单调性=成立(ρ=0.9)` 一行;分层表头新增口径切换 tab(原始/超额/超额+中性化),切换不改数据只换列;宇宙漏斗为横向递减条形(5200→4980→4890hover 显示过滤器名)。
4. **自定义因子编辑器**Settings 新面板"因子库",与信号库并列):公式输入 + 算子速查侧栏 + 实时校验 + 试算预览 + 版本列表 + 引用关系展示。
线框:`左列: 版本列表(当前高亮)+元信息表单 | 中列: 公式输入框(等宽,校验错误行内红标+光标定位) + 算子速查(点击插入) | 右列: 试算预览表(最新5日×前20标的+非空率) [校验] [试算] [保存]`;保存按钮在校验+试算双绿前禁用。
5. **复合因子编辑器**(同 Settings 面板内 tab):成员表(因子搜索、权重、方向)、自动权重开关与窗口、管线预览图;"试用于策略"向导生成 scoring 片段。
线框:`上: 成员表[因子搜索器|方向|权重|剔除] + weighting 单选 + 窗口输入 | 中: 成员相关性热力图(§11-8, >0.8 对红标提示去重) + 管线预览(过滤→截尾→中性化→标准化→加权) | 下: [导出 scoring 片段] [试用于策略]`
6. **因子相关性探索器**(复合编辑器内嵌 + 因子回测 tab 的独立对话框):任选 2-10 个因子,展示区间内日均截面秩相关矩阵热力图。后端复用 `mining.py:446` `compute_rank_correlation`【现状】抽出的公共函数,不新建第二套计算。
7. **挖掘工作台**:排行榜加 t/q 值列与 DSR;候选卡新增"导出为复合因子"。
8. 全部新组件覆盖 加载/空/错误/禁用/无权限 五态(CONTRIBUTING 第 7 节)**并在 1280px 常用宽度与窄屏(≤768px)检查截断、遮挡、弹窗可操作性**;前端类型同步进 `lib/api.ts`;所有轮询/长任务按钮带进行中禁用态。
**查询键新增**`queryKeys.ts` 集中定义,spec 哈希必须进键):
```text
factorCatalog({assetType, group}) // 目录
factorValidate() // mutation,无需键
factorCustomList() / factorCustomPreview({formulaHash}) // CRUD / 试算
factorIcReport({factorId, factorVersion, universeId, neuId,
methodology, rangeHash, rebalance, nGroups})
factorCompositeList() / factorCompositeEval({cmpSpecHash, matrixGeneration})
factorCorrelation({factorIds[], rangeHash})
researchUniverses()
```
---
## 12. 缓存与性能【设计】
| 缓存 | 键 | 失效 |
| --- | --- | --- |
| 因子 IC 报告 | `fr:ic:{factor_id}:{v}:{universe_id}:{neu_id}:{methodology}:{range_hash}:{rebalance}:{n_groups}` | enriched generation 变更或键任一分量变 |
| 复合因子定义 | `fr:cmp:{id}:{spec_hash}` | 定义编辑 |
| 复合分值(研究期) | `fr:cmpv:{cmp_spec_hash}:{matrix_generation}` | 矩阵重建 |
| DSL 编译产物 | 进程内 LRU(表达式文本 → Expr) | 进程重启 |
| 宇宙快照 | `fr:uni:{universe_id}:{matrix_generation}` | 矩阵重建 |
约束:复合/自定义因子**默认不物化进 enriched parquet**(避免用户定义污染核心管道与增量路径);只在研究/评分请求期计算并按上表缓存。`incremental_safe=False` 的因子盘中路径直接缺失而非降级计算(对齐 pipeline 增量路径现有行为)。中性化逐日截面计算全部 Polars 表达式化;统计层 O(N·G) 极小。禁止任何新增逻辑进入实时行情线程(CONTRIBUTING 6.3)。
**性能预算(实现验收线,超线必须先优化再合入)**
| 操作 | 预算 | 基准场景 |
| --- | --- | --- |
| 单因子 IC 全报告(含三口径+t 值+分层) | ≤ 现有报告耗时 × 1.3 | 全 A 股 × 3 年日线(现有 `factor.py` 同窗基线,PR 里附前后数据,CONTRIBUTING 6.3 |
| DSL 编译(含校验) | ≤ 5ms/表达式 | 深度 12、token 200 上限样例 |
| 复合因子单期截面计算 | ≤ 成员因子独立计算耗时之和 × 1.2 | 10 成员 × 全 A 股 |
| 宇宙过滤全期 | ≤ 全期 IC 计算的 10% | 同上基准 |
| 因子目录接口 | ≤ 50ms | 全量 62+ 因子元数据 |
| 相关性探索器 | ≤ 现有 mining 同规模秩相关耗时 × 1.1 | 10 因子 × 1 年 |
---
## 13. 测试矩阵(最低要求清单)【设计】
| 模块 | 必测 |
| --- | --- |
| 注册表重构 | 特征化快照(62+ 因子两条计算路径逐位一致);重复 id/未增版本拒绝启动 |
| DSL | 每算子黄金用例(含 null/除零/σ=0/全常数);负 shift 编译失败;深度/窗口/白名单越界拒绝;与手写 Polars 等价性;注入样例(`__import__`、列名穿越)拒绝 |
| 宇宙 | 每过滤器独立单测(构造含 ST/停牌/涨停锁死/次新/微额的合成面板);漏斗计数;降级注记触发 |
| 中性化 | 合成数据数值断言(demean/zscore/回归残差 vs statsmodels 对拍);行业缺组回退;基准停市日 |
| 统计 | NW t:黄金参考向量(离线计算硬编码期望值)+ dev 组 statsmodels 对拍(可选);FDR:BH 已知 p 向量解析解;DSR:已构造解析例(对称正态收益 + 已知试验数);单调性边界(平梯/倒梯) |
| 指标 | Sharpe/年化/超额/IR 已知序列解析解;243 口径回归 |
| 复合因子 | 权重归一;**自动权重无未来函数**(t 日权重不随 t 日数据变化——篡改 t 日数据断言权重不变);四端同值断言(§8.3);环引用拒绝 |
| API | 成功/空数据/编译错误/预热不足/无权限 |
| 缓存 | 键覆盖测试(改 spec 必换键);generation 失效 |
| 前端 | pnpm build + 五态检查 |
---
## 14. 实施路线图(PR 粒度,每 PR 独立可合)
| PR | 内容 | 依赖 | 主要文件 | 分级 |
| --- | --- | --- | --- | --- |
| 1 | stats 模块:IC t(NW)/单调性/半衰期 + IC 报告新字段 | 无 | 新 `app/factors/stats.py` + factor.py 增量 | L2 |
| 2 | metrics 统一 + metrics_v2 版本化 | PR-1 | 新 metrics.pyengine/strategy/factor 改引用 | L3(热点,最小接线) |
| 3 | 宇宙构建器 + 过滤器 + IC 接入 + 缓存键 | PR-1 | 新 universe.py + factor.py | L2 |
| 4 | 基准超额 + 中性化 + 三口径并列 | PR-3 | 新 neutralize.py + factor.py | L2 |
| 5 | FDR + DSR 进 optimizer/mining 排行榜与晋升门槛 | PR-1 | optimizer/mining 增量 | L3 |
| 6 | 因子注册表重构(特征化测试先行) | 无(可与 1-5 并行) | 新 registry.pyfactor.py/scoring.py/pipeline.py 收口 | L3 |
| 7 | 复合因子 + 策略 scoring 桥 + 挖掘导出 | PR-6 | 新 composite.pyscoring.py 最小接线 | L2→L3 |
| 8 | DSL 编译器 + 自定义因子 CRUD + Settings 因子库 UI | PR-6 | 新 dsl/ + api + settings 前端 | L2 |
| 9 | 前端:ResearchProfile + 目录 + 报告增强 | PR-3/4 后端就绪 | 前端组件族 | 前端 |
| 10+ | 数据契约:st_history / delisted / industry_pit / index_ext | provider 侧 | capabilities + 各 provider | L1+L2 |
每个 PR 按 CONTRIBUTING 第 10 节模板出描述(问题/根因/方案/兼容/性能/验证/界面证据/回滚)。
**配置白名单联动**`backtest/candidates.py:28-49`【现状】的 `_CONFIG_FIELDS["factor"]` 是冻结字段集,PR-3/PR-4 必须同步扩展 `universe``neutralization` 两个配置字段(沿用 `_MINING_SOURCE_CONFIG_FIELDS` 的 frozenset 合并模式),否则保存候选会静默丢弃 spec——这是缓存一致性之外的第二个容易漏的接线点,测试须覆盖"保存→载入→spec 哈希不变"。
**各 PR 回滚要点**PR-1/3/4/5 新增模块 + 增量字段,回滚 = revert 即可(旧缓存键不含新分量,自动回旧路径);PR-2 指标统一保留 `metrics_methodology` 开关,回滚 = 切回 v1 计算分支并保留数据;PR-6 注册表重构通过特征化测试保证行为等价,回滚 = revert(无持久化迁移);PR-7/8 用户数据(自定义因子/复合因子 JSON)为新增目录,回滚代码后文件残留但不再加载,重新部署即恢复——**不存在任何需要用户手动清数据的回滚**(CONTRIBUTING 第 12 节红线)。
**文档同步任务**(各 PR 内完成,不单开):PR-1/3/4 更新 `docs/features.md` 因子回测章节;PR-5 更新 `docs/mining.md` 门槛说明;PR-7 更新 `docs/strategy.md` 评分字段说明与 `操作说明书.md`PR-8 更新 `docs/custom-data-source.md` 无关则跳过;本设计文档在每个 PR 合入后把对应条目从【设计】改标【已实现】。
---
## 15. 兼容性影响与风险清单
| 变更 | 功能影响 | 结果口径影响 | 缓解 |
| --- | --- | --- | --- |
| PR-1/5 统计字段 | 无 | 无(纯新增) | 字段默认值 |
| PR-2 指标统一 | 无 | **Sharpe/年化数字变化(有意)** | metrics_v2 版本徽章;旧字段一版周期弃用 |
| PR-3/4 宇宙/中性化 | 无 | IC/分层数字变化(有意,通常回落) | 默认开关显式;三口径并列;缓存键含 spec |
| PR-6 注册表 | 无 | 要求逐位一致 | 特征化测试是合入硬门槛 |
| PR-7/8 复合/DSL | 无(纯新增能力) | 无 | 注入列模式,custom_signals 先例 |
| 数据契约 | 无 | 退市股回补后回测数字变化(修复) | 独立 dataset,无源时明确降级注记 |
剩余风险:~~① ts_rank 的 Polars 原生可用性需实现期确认~~【已解决:§17 验证 polars 1.40.1 `Expr.rolling_rank` 存在且行为正确】;② 行业快照回填历史的偏差在点时表到位前无法消除(注记透明化);③ 幸存者偏差的根本修复依赖数据源,代码侧已尽(注记 + 回补接口预留);④ ST 非点时在 st_history dataset 到位前仅能注记;⑤ 统计函数运行时零新增第三方依赖(后端当前无 scipy/statsmodels,已验证),NW/BH-FDR/DSR 以 numpy 手写实现,statsmodels 仅允许加入 uv dev 依赖组做测试对拍,不进运行时。
---
## 16. 明确不做清单(YAGNI 边界)
以下能力**刻意不在本设计范围内**,防止范围蔓延(依据 `docs/secondary-development.md` 第 10 节:不为未来可能出现的需求预埋框架)。出现真实需求时再按需立项:
| 不做项 | 理由 |
| --- | --- |
| 因子市场/分享/导入导出社区 | 单用户自托管定位,无真实需求 |
| 全 Barra 风格回归(Beta/动量/流动性/非线性市值等十因子) | 数据与维护成本高;industry+size lite 已覆盖主要混杂,收益边际低 |
| 自动机器学习/遗传规划因子搜索 | 与现有 beam search + 嵌套样本外定位重叠,且加剧多重检验问题 |
| 港美股/加密资产因子 | 数据源与交易规则(T+0/无涨跌停)完全是另一套引擎 |
| Tick 级/高频因子 | 分钟数据集能力有限,且与现有日线研究框架口径不同 |
| 因子值的实时盘中推送(SSE 逐笔更新) | 违反实时热路径约束;监控经由策略结果缓存已覆盖时效需求 |
| 复合因子权重在线学习/逐日再优化 | 自动权重窗口已是点时滚动;更细粒度会显著推高换手且引入过拟合面 |
| ARIMA/VAR 预测、协整与配对交易 | 本平台定位是**横截面因子研究**;时间序列预测与统计套利是另一条业务线(指数择时/配对),数据、引擎与交易规则均不同,混入即范围蔓延 |
| GARCH 全族 / 卡尔曼滤波 / 时变 Beta 状态空间模型 | EWMA 条件波动已覆盖日频主要价值;逐 symbol 递归拟合与全向量化管线冲突,机构级边际收益不抵维护成本(单 GARCH(1,1) 为 §2.4 的 experimental 按需项,不在冲突内) |
| HMM/马尔可夫区制检测 | 现有情绪周期 6 阶段(启发式)+ 分环境 ICfactor.py:813-903【现状】)已覆盖区制条件分析;统计区制模型列为未来探索项不进本期 |
| 独立权限体系(多用户/角色) | 应用级部署口令已满足自托管场景 |
---
## 17. 验证附录(本设计的验证记录)
> 验证日期 2026-09-04,基准 main@2ce8b4b1,后端 polars 1.40.1。分三部分:代码引用逐条核对、技术可行性实测、内部一致性检查。结论:**全部引用属实或已修正,可行性风险清零或已有替代方案,一致性检查通过**。
### 17.1 代码引用核对(【现状】条目逐条对账)
| 引用 | 核对内容 | 结果 |
| --- | --- | --- |
| factor.py:36-109 | FACTOR_COLUMNS 61 因子目录 | ✓(全文读取) |
| factor.py:111/112/122 | warmup 120 / factor_v2 / n_groups=5 | ✓ |
| factor.py:740-750 / 752-778 / 780-811 / 813-903 | Rank IC / 分年 / 衰减 / 分环境 | ✓(函数定义与实现均在引用区间) |
| factor.py:1059-1067 / 1095-1098 | 双边佣金+印花税+滑点成本 / 每调仓期扣减 | ✓ |
| factor.py:1123-1166 / 1001-1035 / 1294-1295 | 换手率 / tie-aware 分层 / executable_short=False | ✓ |
| factor.py:1184-1186 / 1274-1277 | 年化系数匹配调仓频率的注释与实现 | ✓(注释原文核实) |
| mining.py:26-31 / 91-100 / 1041-1073 / 1503-1530 / 446 | 晋升门槛 / purge30+embargo5 / trial 预算 / 折构造 / 秩相关 | ✓ |
| mining.py:492-493 | `pl.corr(..., method="spearman")` 可用性 | ✓(代码在用,即证 API 存在) |
| engine.py:2871-2874 / 2899-2903 / 2993-3030 / 3116-3122 | 365.25 年化 / 逐笔 Sharpe(含"非严格正确"注释)/ 仅卖出日聚合 / 净值口径 | ✓(四种口径全部原文核实) |
| engine.py:51 / 54-94 / 903-933 | matching 默认 close_t / 成本模型 / 涨跌停与停牌成交闸 | ✓(全文精读) |
| pipeline.py:970-984 | filter_halt_days(函数头 973 | ✓ |
| pipeline.py:1795 | compute_enriched_today 盘中增量入口 | ✓ **(修正:初稿误引 1738+,该行实为复权因子读取;已改)** |
| matrix.py:1476-1495 | _write_tradable_matrix | ✓ |
| repository.py:1488 | get_index_daily | ✓ |
| api/data.py:765 | listing_date 已暴露未用于研究 | ✓ |
| fundamentals.py:107-115 | join_asof backward + date>_announce 严格公告日后 | ✓ |
| share_capital.py:54-56 | announce_date 优先、period_end 兜底 | ✓ |
| price_limits.py:87-101 | numpy_limit_pct_vectors(当前名推断) | ✓ |
| capabilities.py:44-45 | 复权口径一致性"不做路由耦合"注释 | ✓ |
| index_const.py:12-15 | 核心四只代码(000001.SH/399001.SZ/399006.SZ/000680.SH | ✓ |
| daily_pipeline.py:92-125 | _resolve_universeCN_Equity_A 当前池) | ✓ |
| kline_sync.py:358-367 | (symbol, trade_date) 去重 keep=last 原子合并 | ✓ |
| scoring.py:13-51 / 53-66 / 91-104 / 108+ | VIRTUAL 依赖 35 项 / 预热表 / 依赖展开与预热推导 / scoring_value_expr | ✓(全文读取,附录A 由其逐字推导) |
| monitor.py:1314-1322 | score_min/max 消费 result.scores | ✓ |
| strategy/builtin/*.py | "scoring": {字段: 权重} 配置形态 | ✓(8 个内置策略抽样) |
| candidates.py | factor/strategy 双形态候选配置字段 | ✓ |
| services/heavy_job_limiter.py、backtest/numba_runtime.py、services/ext_presets.py、services/market_mainline.py:35 | 模块存在性 / 行业两级口径 | ✓ |
### 17.2 技术可行性实测(`uv run python` 于 backend 环境)
| 项 | 实测结果 | 设计影响 |
| --- | --- | --- |
| polars 版本 | 1.40.1pyproject pin >=1.0 | — |
| `Expr.rolling_rank` | **存在**,递增序列 4 点窗输出 [null,null,null,4,4,4] 行为正确 | §15 风险①**解除**,ts_rank 用原生实现 |
| `Expr.ewm_std` / `ewm_var` | 存在 | f_ewma_vol 直接可实现 |
| `pl.rolling_corr`(顶层)/ `pl.rolling_cov` | 存在且可算出正确相关值(3 点窗样例 0.6547 | ts_corr/ts_cov 编译目标为顶层函数 |
| `pl.rolling_corr(...).over("symbol")` 分组组合 | **实测通过**:A/B 两组各自窗口内相关(0.6547 / 1.0),无串组 | ts_corr 多标的面板场景确认可行(初稿未验证,本轮补测) |
| `Expr.rolling_corr` | **不存在**(仅顶层函数) | 算子表已按顶层函数修正 |
| `Expr.rolling_arg_max` | **不存在**rolling_map 为 Python 回调 | ts_argmax/ts_argmin v1 移除(§3.1 已注) |
| rolling_quantile/var/mean/std/max/min/sum、shift、pct_change、diff、pow、sign、clip、log、cum_prod | 全部存在 | 其余算子无阻碍 |
| `pl.corr` spearman | mining.py:492 在用 | 无阻碍 |
| numba | 已安装(numba_runtime 真实存在) | GARCH/矩阵核扩展路径成立 |
| statsmodels / scipy | **均未安装** | §6 零依赖原则:运行时 numpy 手写 NW/BH-FDR/DSRstatsmodels 仅可进 dev 依赖组 |
### 17.3 内部一致性检查
1. 缓存键 ↔ Spec 字段:UniverseSpec 7 字段、NeutralizationSpec 5 字段(含 winsorize_sigma 与管线顺序)全部进入各自 canonical_json/哈希 → 键覆盖完整(§4.2/§5.2 ↔ §12)。✓
2. API ↔ 前端组件:catalog↔目录对话框、validate/preview/custom↔因子编辑器、ic↔报告增强、composite↔复合编辑器、universes↔ResearchProfile,无孤立端点或无后端组件。✓
3. 测试矩阵 ↔ 模块:§13 十行覆盖 §2-§12 全部新增模块,无模块缺测试项。✓
4. 路线图 ↔ 章节:PR-1↔§6.1、PR-2↔§7、PR-3↔§4、PR-4↔§5、PR-5↔§6.1(FDR/DSR)、PR-6↔§2、PR-7↔§8、PR-8↔§3、PR-9↔§11、PR-10↔§9,全部章节有落点 PR。✓
5. §2.4 GARCH(1,1) experimental 与 §16"GARCH 全族不做"边界一致(单一按需项 vs 全族排除,§16 已加注)。✓
6. EBNF ↔ 算子表:if_else/比较/逻辑均以函数调用或中缀产生式覆盖;优先级链 or<and<cmp<add<mul<unary 明确无歧义。✓
7. 错误码目录 ↔ §3.2 校验规则:六条编译规则 + 三条运行时规则(E013/E015/E016)全部有码可映射。✓
8. 附录A 计数:virtual 35 + base 非财务 19 + 财务 7 = 61,与 FACTOR_COLUMNS 行数一致;方向"待标定"仅用于振荡/环境依赖因子,不虚构。✓
9. 三口径并列(§5.3)↔ API 响应 ic_by_basis 三键(§10)↔ 前端口径切换 tab(§11-3)三处一致。✓
10. §16 不做清单 8 项与 §0 设计目标无冲突。✓