项目

一般

简介

路线图

FSS-Sprint2

打开

截止日期到 16 天 (2026-05-03)

0002-数据基建

93%

15 问题   (14 已关闭1 打开)

数据基建

周次 Story ID Story 描述 规约硬性映射 验收标准 工时
W2 S2-01 配置扩展与 DuckDB 粗筛降维
config.yaml 新增数据源路径配置;实现 DuckDB 读取 CSV、显式列投影(丢弃 id+4列冗余)、周频聚合、白名单 JOIN,输出精简 Arrow。
规约 1.2 (周聚合/类型过滤);章程 R-5 (显式丢弃4列);FW-1/FW-2 config.yaml 新增 data.raw_paths 节点,data_loader.py 从配置读取路径;
② DuckDB SQL 禁止 SELECT *,必须显式列出 fund_id, net_value_date, cumulative_net_value
③ JOIN fund_basic_info 后,Arrow Schema 仅含上述 3 列 + fund_type(临时审计用,最终落盘前丢弃);
④ 单测:Mock 小 CSV 验证列裁剪正确。
2天
W2 S2-02 Polars 时序状态机(核心攻坚)
接收全量 Arrow,实现 ffill、>4周缺失截断并打 segment_id、建仓期硬删、异常值处理。
规约 1.2 (缺失填充/截断/建仓期/异常值);CR-001 (segment_id);全量 Arrow 破局方案 segment_id 截断处递增,同基金内从 0 开始;
② 建仓期硬删取 segment_id == 0 首条日期减 12 周(非全局首条);
cumulative_net_value ≤ 0 的行被剔除(不进入 Parquet);
④ 单周涨跌幅 >50% 的行 cumulative_net_value 置 NULL(保留行,保留时间连续性);
单测必覆盖:跨年 ffill、刚好第 5 周缺失触发截断、单基金仅 10 周被建仓期完全剔除、异常值 NULL 化。
3天
W3 S2-03 存活期过滤与按年分区落盘
统计各基金有效周数,剔除 <52 周,Polars 内按年切片写 Parquet,创建 DuckDB VIEW。
规约 1.2 (存活期过滤/清盘基金);规约 1.3 (存储方案/row_group_size);FW-4/FW-5 ① 存活期计算 = 该基金所有段有效周数之和,< 52 周整体剔除;
data/processed/ 下产出 net_value_YYYY.parquet(覆盖写入,不追加);
③ PyArrow write_parquet() 显式指定 row_group_size=100000
create_duckdb_view() 函数拼接所有年份文件创建 v_net_value_processed
⑤ 单测:Mock 含短生命周期基金的 DataFrame,验证其被完整剔除。
2天
W3 S2-04 数据血缘、审计日志与内存压测
计算处理后 Hash,生成结构化审计 JSON,在 64GB 实体机执行全量压测并记录内存峰值。
章程 M2 Done 标准(更新版);规约 7.3 (数据血缘) ① 产出 data/metadata/processed_hash.json(按年份文件逐个计算 SHA-256);
② 产出 data/metadata/data_audit.json,Schema 严格符合下方定义;
③ 压测日志包含 peak_memory_gb,证明 ≤ 40GB;
④ 若 whitelist_retention_rate < 15%,触发 R-6 预警流程,向 PM 汇报。
2天
W3 S2-05 Gitea /docs 数据契约文档与 Redmine 验收
编写 data_contract.md 沉淀为技术真相源,在 Redmine 录入 M2 验收 CheckList。
架构基线 3 (双轨制);章程 M2 docs/data_contract.md 内容包含完整 Parquet Schema 定义与 data_audit.json Schema 定义;
② Redmine M2 任务状态可更新为"待验收";
③ CI 软预警检查通过(src/ 变更已同步 docs/ 变更)。
1天

data_audit.json 标准 Schema 定义

此文件为 M2 验收的机器可解析审计凭证,S2-04 必须严格按此 Schema 输出:

{
  "pipeline": {
    "step": "step2_data_loading",
    "run_time": "2026-04-11T10:00:00+08:00",
    "python_version": "3.12.x"
  },
  "input": {
    "fund_net_info": {
      "file_path": "data/raw/fund_net_info.csv",
      "sha256": "a1b2c3d4...",
      "total_rows": 23000000,
      "date_range": ["2015-12-07", "2026-03-28"]
    },
    "fund_basic_info": {
      "file_path": "data/raw/fund_basic_info.csv",
      "sha256": "e5f6g7h8...",
      "total_records": 26000,
      "columns_actually_read": ["fund_id", "fund_name", "fund_type", "create_date"],
      "columns_discarded": ["insert_datetime", "update_datetime", "insert_operator", "update_operator"]
    }
  },
  "filtering": {
    "whitelist_total_funds": 26000,
    "whitelist_retained_funds": 8000,
    "whitelist_filtered_funds": 18000,
    "whitelist_retention_rate": 0.3077
  },
  "cleaning": {
    "weekly_aggregation_dedup_rows": 500000,
    "nav_le_zero_removed_rows": 1200,
    "weekly_change_over_limit_nullified": 3500,
    "building_period_dropped_rows": 96000,
    "truncated_rows_by_gap": 42000,
    "survival_filtered_funds": 1500,
    "survival_filtered_rows": 78000
  },
  "output": {
    "parquet_files": [
      {"filename": "net_value_2016.parquet", "sha256": "...", "rows": 450000},
      {"filename": "net_value_2017.parquet", "sha256": "...", "rows": 480000}
    ],
    "total_rows_after_cleaning": 8500000,
    "total_funds_after_cleaning": 6500,
    "date_range": ["2016-01-04", "2026-03-28"],
    "schema": ["fund_id:String", "net_value_date:Date", "cumulative_net_value:Float64", "segment_id:UInt32"],
    "peak_memory_gb": 35.2,
    "duckdb_view_name": "v_net_value_processed"
  }
}

PM 卡控说明:上述数值为占位示例,研发必须使用 settings.filter.fund_type_whitelist 等配置参数驱动计算,严禁硬编码whitelist_retention_rate 的分母必须是 fund_basic_info 的原始总行数。


可优化与可复用逻辑提示

1. ffill + 截断的 Polars 向量化实现(复用级)

# 推荐实现思路(伪代码,非最终交付)
# 先按 fund_id 排序,再用 Polars 原生 forward fill
df = df.sort(["fund_id", "net_value_date"])
df = df.with_columns(
    pl.col("cumulative_net_value").forward_fill().over("fund_id")
)
# 然后 Polars 原生检测连续 NULL(即原始缺失超过 max_gap_weeks 的位置)
# 利用 diff 计算相邻日期间隔,间隔 > (max_gap_weeks+1)*7 天即为截断点

2. DuckDB memory_limit 应从配置读取

S2-01 中 DuckDB 连接应设置 memory_limit=str(settings.duckdb_memory_limit),建议在 config.yaml 中新增:

duckdb:
  memory_limit: "40GB"

这样后续压测时可通过环境变量 DUCKDB__MEMORY_LIMIT=20GB 快速模拟低内存环境。

3. 审计日志的"率"统一精度

建议在 src/utils/config.py 中新增一个审计工具函数:

def calc_rate(numerator: int, denominator: int, decimals: int = 4) -> float:
    """通用比率计算,分母为 0 时返回 0.0"""

data_loader.py 调用,保证全项目比率计算逻辑一致。


新增/修改文件清单

操作 文件路径 简介与企业级约束
修改 config/config.yaml data: 节点下新增 raw_paths 字典(fund_net_info / fund_basic_info 路径);② 新增 duckdb.memory_limit: "40GB"
修改 src/utils/config.py 对应新增 data.raw_pathsduckdb.memory_limit 的强类型属性
新增 src/data_loader.py 核心模块。包含 5 个公开函数:
extract_raw_to_arrow() — DuckDB 粗筛
process_timeseries() — Polars 状态机(ffill/截断/segment_id/建仓期/异常值)
filter_and_sink_parquet() — 存活期过滤 + 按年写 Parquet
create_duckdb_view() — 创建 v_net_value_processed
generate_audit_report() — 产出 data_audit.json
严禁魔法数字,全部从 settings 读取
新增 tests/test_data_loader.py 时序状态机专项单测。必覆盖:跨年 ffill、>4 周截断 + segment_id 递增、建仓期仅取 segment_0、异常值 NULL 化、短生命周期基金剔除
新增 docs/data_contract.md 技术真相源。定义 net_value_YYYY.parquet 的严格 Schema(含 segment_id 语义)、data_audit.json 的 JSON Schema、v_net_value_processed VIEW 的列定义
修改 docs/architecture_baseline.md 同步更新:① FW-3 章节补充"Step 2 全量 Arrow 破局方案"的例外说明;② 新增 segment_id 在防火墙架构中的数据流位置
修改 .gitea/workflows/ci.yml 增加 pytest tests/test_data_loader.py -v 步骤,纳入 CI 强阻断
产出 data/metadata/data_audit.json 运行时产物,不由代码模板生成,由 data_loader.py 动态写入
产出 data/metadata/processed_hash.json 运行时产物,由 src/utils/hash.py 计算
产出 data/processed/net_value_YYYY.parquet 运行时产物,Step 3/4 的直接输入

下级目录

CR-001-M2里程碑_Parquet_Schema_变更

FSS-Sprint3

打开

截止日期到 23 天 (2026-05-10)

0003-全量数据基准预览

4%

6 问题   (1 已关闭5 打开)

全量数据基准预览(仅作审计与特征字典参考)

周次 Story ID Story 描述 规约硬性映射 验收标准 工时
W4 S3-01 基础滚动特征引擎(A/B/C类)
实现均值回归、波动、趋势动量类特征的 Polars 向量化计算。
规约 4.1 (A/B/C类);FW-3 (按年串行);CR-003 (_w 命名) ① 产出约 20 维基础特征(如 momentum_w4, max_drawdown_w26);
强制隔离:滚动计算必须按 group_by(['fund_id', 'segment_id']) 隔离,严禁跨段计算;
③ 所有窗口参数从 config.yaml 读取;
④ 单测验证特定 Mock 数据下的滚动值精确度。
1.5天
W4 S3-02 定投特有与风险调整特征引擎(D/E类)
实现 DCA 特征与 Sharpe/Sortino,包含 OLS 线性回归斜率计算。
规约 4.1 (D/E类);规约 4.2 (min_valid_ratio);规约 4.1E (weekly_rf 公式) ① 产出约 20 维高阶特征(如 dca_return_w12, rolling_sharpe_w26);
有效值判定:所有滚动特征必须实现 min_valid_ratio=0.20 卡控,不足时输出 NULL;
weekly_rf 计算公式必须严格为 (1 + 0.02)^(1/52) - 1,严禁硬编码数值;
④ OLS 回归单测验证斜率与 R² 极限情况。
1天
W4 S3-03 标准化与共线性工具函数(纯逻辑抽离)
实现 T-1 截面标准化与共线性剔除的纯函数,并在全量数据上跑一遍产出预览文件。
规约 4.3 (T-1模式/MAD边界3.0);规约 4.4 (IC计算/阈值0.90);CR-002 (函数级单测) ① 暴露 calculate_stats(), apply_standardization(), remove_collinear_features() 三个公开函数,带完整 Type Hints
单测强卡点:构造 2 天数据的 DataFrame,调用 apply_standardization(),断言第 1 天所有特征输出 NULL;
③ 产出 standardization_params.parquet(标记为预览)与 final_feature_list.json
1.5天
W4 S3-04 特征落盘、血缘与双轨制文档
按年分区写长表特征 Parquet,更新 Hash 与数据契约文档。
规约 7.4 (严禁Pivot宽表);FW-4 (按年覆盖写回);架构基线 3 data/features/features_YYYY.parquet 产出,Schema 为长表:fund_id, net_value_date, segment_id, feature_1, ..., feature_K
② PyArrow 写入指定 row_group_size=100000
③ 产出 data/metadata/features_hash.json
docs/data_contract.md 同步更新特征字典(含 _w 命名示例)。
1天

可优化与可复用逻辑提示

1. 滚动窗口的“有效值判定”通用包装器(复用级)

规约 4.2 要求每个滚动特征都要判断 min_valid_ratio。如果在 40 个特征里各自写一遍 .is_not_null().sum() 逻辑,代码极度冗余且易错。
建议实现:在 feature_engineering.py 内部实现一个高阶函数或装饰器:

def with_validity_check(df: pl.Expr, window_size: int, min_ratio: float) -> pl.Expr:
    # 统一包装:先算 rolling,再算窗口内非空数,不足比例则返回 NULL
    ...

所有特征计算只需调用此包装器,保证全项目有效值判定逻辑绝对一致。

2. OLS 回归的 NumPy 向量化加速

规约要求计算 trend_slopetrend_r_squared。Polars 原生不支持直接在 group_by 内做复杂的 OLS 矩阵运算。
建议实现:提取每只基金(每段)的窗口数据为极小的 NumPy 数组(最大 52x2 矩阵),使用 np.linalg.lstsq 批量求解。这不仅规避了 Polars 的限制,还能通过 map_elements 在底层实现极高的向量化性能。


新增/修改文件清单

操作 文件路径 简介与企业级约束
新增 src/feature_engineering.py 核心模块。包含 3 个层级:
calculate_raw_features(df) — 按年调用,产出 A~E 类原始特征;
calculate_stats(df), apply_standardization(df, stats) — 标准化纯函数;
remove_collinear_features(df, labels) — 共线性剔除纯函数。严禁出现魔法数字。
新增 tests/test_feature_engineering.py 防线级单测。必含:
① OLS 斜率精度测试;
min_valid_ratio NULL 化测试;
T-1 标准化首日 NULL 测试(构造 2 天数据,验证函数输出)。
修改 docs/data_contract.md 新增《特征矩阵接口契约单》:
① 明确 features_YYYY.parquet 的 Schema(重点标注含 segment_id 且特征为原始值);
② 列出完整的特征字典(采用 _w 风格 2 命名,约 40+ 行);
③ 标注 standardization_params.parquet 的“预览”语义。
修改 docs/architecture_baseline.md 更新数据流图:明确 feature_engineering.pytrainer.py 暴露标准化“纯函数”的虚线依赖关系。
修改 .gitea/workflows/ci.yml 增加 pytest tests/test_feature_engineering.py -v 步骤,纳入 CI 强阻断。
产出 data/features/features_YYYY.parquet 运行时产物。长表形态,包含原始特征值。
产出 data/models/standardization_params.parquet 运行时产物(全量基准预览)。Schema 符合规约 4.3,但增加审计备注。
产出 data/models/final_feature_list.json 运行时产物(预览列表)。格式:["price_vs_ma_ratio_w12", ..., "rolling_sharpe_w52"]
产出 data/metadata/features_hash.json 运行时产物。基于 src/utils/hash.py 计算。

下级目录

CR-002-M3a里程碑交付物语义变更

CR-003-多窗口特征列名规范化


FSS-Sprint4

打开

截止日期到 30 天 (2026-05-17)

0004-标签生成器

0%

4 问题   (0 已关闭 — 4 打开)

标签生成器

由于 Step 4 业务逻辑高度内聚且规约给出了标准伪代码,一周时间安排相对紧凑,核心攻坚集中在向量化实现与边界防御。

周次 Story ID Story 描述 规约硬性映射 验收标准 工时
W4 S4-01 DCA 标签向量化核心引擎
实现基于 Polars group_by + NumPy 的标签计算逻辑。
规约 3.2 (算法伪代码);CR-001 (segment_id隔离) ① 按 group_by(['fund_id', 'segment_id']) 提取极小数组,严禁跨 segment 滑动窗口
② 有效窗口 len < 2 时输出 label = 0
③ 严格使用 np.cumsumnp.arange 计算简单平均成本;
④ 使用 np.argmax(mask) + 1 计算耗费周数,映射为 150 - weeks
性能卡控:全量数据跑批耗时需在可接受范围内(预估 10-20 分钟)。
2天
W4 S4-02 样本平衡与落盘机制
实现正负样本的等距抽样限制与长表 Parquet 写入。
规约 3.3 (等距抽样/上限50/15);规约 7.4 (严禁Pivot宽表) ① 正样本(label>0)单基金上限 50 条;
② 负样本(label=0)单基金上限 15 条(50 * 0.3);
强制使用 np.linspace(0, len(samples)-1, N, dtype=int) 进行等距抽样;
④ 产出 labels.parquet,Schema 为长表:fund_id, net_value_date, segment_id, label (Int32);
⑤ 产出 data/metadata/labels_hash.json
1天
W4 S4-03 7 场景极限防御单测与 CI 闭环
针对规约第九节的 7 个场景编写单测,并补充 segment 边界测试。
章程 M3b Done标准;规约第九节;架构推演延伸 ① 规约 7 个场景单测 100% 全绿,断言精确匹配期望值(如场景 5 必须等于 149);
补充单测(PM 强制):构造一只基金包含 2 个 segment 的 Mock 数据,断言 segment 0 末尾的标签计算绝不使用 segment 1 的数据;
pytest tests/test_label_generator.py -v 纳入 CI 强阻断红线。
2天

可优化与可复用逻辑提示

1. “有效窗口长度 < 2” 的防御性考量

规约伪代码写道:“若有效窗口长度 < 2 → label = 0”。
在实际工程中,如果由于 Step 2 的截断导致某个时间点 t 之后只剩下 1 周的数据(即 t+1 存在,但 t+2 不存在),np.cumsum 依然可以运行(长度为 1 的数组),但这不符合“定投”的语义(至少要投入 2 期才能算成本)。因此,强制拦截 < 2 是极其正确的防御,单测场景 7(建仓期后首周)刚好能覆盖此边界。研发在实现时切勿将判断条件放宽为 < 1

2. np.argmax(mask) 的陷阱防御

NumPy 的 argmaxmask 全为 False 时会返回 0。虽然伪代码中用了 any(mask) 做前置判断,但在向量化实现(如使用 np.where 或底层 NumPy 循环)时,极易引入隐式 Bug。
建议实现:不要过度追求炫技的纯矩阵运算,规约已允许“对每只基金处理极小数组”,直接在 map_elements 或 Python UDF 中使用规约给定的标准 if-else 伪代码逻辑是最安全的,性能瓶颈不在 Python 层,而在 Polars 的 group_by 调度。


新增/修改文件清单

操作 文件路径 简介与企业级约束
新增 src/label_generator.py 核心模块。包含 2 个公开函数:
generate_labels(df: pl.DataFrame) -> pl.DataFrame — 主流程(按 segment 隔离 + DCA 计算);
balance_samples(df: pl.DataFrame) -> pl.DataFrame — 样本平衡(linspace 抽样)。严禁出现 150、0.20 等魔法数字
新增 tests/test_label_generator.py 防线级单测。必须包含规约第九节全部 7 个场景的测试用例,输入数据需精心构造净值序列(如场景 5 需构造首周涨幅超 20% 的极端数据),并附加 segment 边界隔离测试。
修改 docs/data_contract.md 新增《标签矩阵接口契约单》:明确 labels.parquet 的 Schema(重点标注包含 segment_id),以及 label 字段的值域约束 [0, 149]、数据类型 Int32
修改 .gitea/workflows/ci.yml 无需额外修改,已在 Step 3 阶段配置了全局 pytest tests/ 触发逻辑。
产出 data/labels/labels.parquet 运行时产物。长表形态,经过样本平衡后的标签数据集。
产出 data/metadata/labels_hash.json 运行时产物。基于 src/utils/hash.py 计算。

下级目录


FSS-Sprint5

打开

截止日期到 44 天 (2026-05-31)

0005-模型训练与评估

0%

7 问题   (0 已关闭 — 7 打开)

模型训练与评估

周次 Story ID Story 描述 规约硬性映射 验收标准 工时
W6 S5-01 时序切分基座与特征标签对齐
读取按年 Parquet,执行 LEFT JOIN 产出未标准化的 train_dataset.parquet,实现 TimeSeriesSplit 索引划分。
规约 5.1 (训练数据流);规约 5.2 (严禁随机 K-Fold) ① 产出 train_dataset.parquet,Schema 为长表且特征均为原始值
② 实现严格的 TimeSeriesSplit(n_splits=5) 索引生成器,按时间排序切分,严禁打乱;
③ 单测验证各折的 Train/Val 时间范围无重叠且无未来穿越。
2天
W6 S5-02 逐折闭环引擎(标准化+共线性+训练+评估)
实现单折内部的时序隔离计算流,复用 Step 3 纯函数。
规约 5.2 (每折独立执行);CR-002 (复用Step3函数);规约 4.4 (严禁跨时间窗口IC计算) ① 调用 feature_engineering.calculate_stats() 仅基于 Train 折数据计算统计量;
② 调用 apply_standardization() 应用 T-1 参数,Val 折严格借用对应 Train 折参数;
③ 调用 remove_collinear_features() 时,计算 Spearman IC 仅使用 Train 折标签
④ 产出每折的 AUC、NDCG 指标。
2天
W6 S5-03 Optuna 单折搜索与 1 小时硬限拦截
在最后一个折上配置 Optuna 搜索空间与超时机制。
章程第八章 (1小时硬限);规约 5.3 (参数范围/停止条件) ① Optuna 目标函数配置 early_stopping_rounds=50
② 调用 study.optimize(timeout=3600) 实现强制截断,到达 1 小时无论是否收敛必须停止并取当前最优;
③ 搜索范围 8 个超参数严格对齐规约 5.3 表格,严禁自行添加。
2天
W7 S5-04 四桶回测引擎(单点快照+绝对截断)
在最后折的 Val 集最后一个截面,实现绝对分桶与 DCA 组合净值模拟。
CR-004 (单点快照/绝对截断/空桶保留);规约 5.4 (回测指标) ① 仅取最后折 Val 集的最后一个日期截面进行打分;
② 严格按绝对数量分桶(Top20 取 1~20 名),空桶净值曲线输出为 None
③ 实现周定投组合净值追踪:每周投入 20 元均分,计算年化收益率、最大回撤、Sharpe、止盈成功率;
④ 计算全标的池等权基准的相同指标。
2天
W7 S5-05 全量参数重算与 MLflow 归档闭环
产出最终推理级参数,执行 SHAP 归因,完成 MLflow Production 打标。
CR-005 (全量重算参数);规约 5.5 (MLflow 记录);规约 7.3 (config.yaml Hash) ① 全量加载 features.parquet,重新跑一遍标准化,产出全局基准 standardization_params.parquet 存入 MLflow;
② 使用最优参数在所有折上重训最终模型,产出 .txt 文件;
③ 产出 SHAP summary plot 与四桶回测净值曲线图;
④ 在 MLflow Model Registry 将新模型打上 Production 标签,旧模型归档至 Archived
2天

可优化与可复用逻辑提示

1. 【高优提醒】LightGBM 目标函数的对齐缺失

规约 5.4 要求评估 NDCG@20(排序质量指标),但规约 5.3 未明确 LightGBM 的 objective(目标函数)。

  • 如果使用 binary:logloss,模型输出的是概率,无法直接完美对齐 NDCG。
  • 建议实现:在 S5-03 的 Optuna 定义中,建议研发使用 lambdarank 目标函数,或至少在评估阶段将模型概率输出转化为分位数后计算 NDCG。此细节无需 PM 拍板,但请研发在 Review 时重点关注,避免 NDCG 指标失真。

2. LightGBM 特征名与共线性剔除后列表的严格对齐

在 S5-02 逐折循环中,共线性剔除会丢弃部分特征。在将 Polars DataFrame 转换为 LightGBM Dataset 时,必须显式传入 feature_names=final_feature_list。如果不传,LightGBM 会按内部索引(0, 1, 2...)生成特征名,后续导出的 SHAP 归因图将变成无意义的 Feature_0,直接导致 M4 验收失败。

3. 回测 DCA 净值模拟的“快照隔离”

规约要求模拟“等权周定投组合”。在单点快照(假设 T 日为 2025-12-31)下,组合从 T+1 日开始买入,但买入的标的必须是 T 日排名分桶后的基金池。严禁在回测的 150 周内允许新基金加入组合(因为 T 日之后没有新数据来给新基金打分)。这是一个极易踩坑的业务边界。


新增/修改文件清单

操作 文件路径 简介与企业级约束
新增 src/trainer.py 核心模块。包含主入口 run_training_pipeline(),以及内部函数 _train_single_fold()_evaluate_single_fold()_run_optuna()_backtest_single_point()。严禁出现任何硬编码超参数。
新增 src/utils/backtest.py 回测工具模块。包含 _simulate_dca_portfolio()(给定基金池,按周均分资金模拟定投净值)和 _calculate_portfolio_metrics()(计算年化、最大回撤、Sharpe等)。从 trainer.py 中剥离,保持单一职责。
修改 docs/architecture_baseline.md 新增《Step 5 时序隔离防泄露架构图》:明确展示 Train 折计算 Stats -> Val 折使用 Shifted Stats 的虚线依赖关系,以及全量重算参数的旁路流程。
修改 .gitea/workflows/ci.yml 增加 Step 5 所需系统依赖的安装检查(如 lightgbm, optuna, shap, scikit-learn, matplotlib),防止流水线在拉取代码后因缺包报错。
产出 data/models/model_final.txt 运行时产物。使用最优参数在全量 5 折上重训的最终 LightGBM 模型文件。
产出 data/models/standardization_params.parquet 运行时产物(CR-005 定义的全局基准版)。Schema 严格符合规约 4.3 节,供 Step 6 推理服务直读。
产出 data/models/final_feature_list.json 运行时产物。最后一次重训时保留的特征名列表(可能因逐折 IC 剔除而与 Step 3 预览版不同)。
产出 mlruns/.../artifacts/shap_summary.png 运行时产物。SHAP 特征重要性摘要图,用于解释模型决策依据。
产出 mlruns/.../artifacts/backtest_curves.png 运行时产物。包含 Top20、Top21-50、Top51-100、其余 四条净值曲线与等权基准曲线的对比图。

下级目录

CR-004-回测评估执行策略与空桶防御机制修正
CR-005-MLflow_归档标准化参数语义变更

FSS-Sprint6

打开

截止日期到 51 天 (2026-06-07)

0006-推理收口

0%

5 问题   (0 已关闭 — 5 打开)

推理收口

作为项目收官之周,Step 6 的重心不再是算法攻坚,而是工程闭环、接口防穿透与双轨制收尾

周次 Story ID Story 描述 规约硬性映射 验收标准 工时
W8 S6-01 单基金查询推理引擎(含 SHAP 全链路)
实现从 CSV 到决策文本的单基金端到端推理。
规约 6.1/6.2;规约 6.3 (文案拼接);CR-002 (复用Step3原始特征);CR-005 (加载全局参数) ① 输入 ≥52 周的净值 CSV,输出严格包含 4 个字段:score(0-100整数), top5_positive_features(List), top5_negative_features(List), recommendation_text(String);
score 必须由 round(概率 × 100) 生成,严禁出现小数;
③ 文案模板严格对齐规约 6.3 四个得分区间,且必须拼接 Top 贡献特征名;
④ 加载 MLflow 模型时,必须显式指定过滤条件 tags.production = "True"
2天
W8 S6-02 批量推荐引擎(性能隔离与降级)
实现基于 DataFrame 的批量打分与排序。
规约 6.1 (批量输出);CR-006 (严禁SHAP/性能降级) ① 输入包含多只基金最新净值的 DataFrame,输出按 score 降序排列的 DataFrame;
CI 红线拦截:通过 AST 扫描或人工 Review 确保 inference.py 的批量分支中无任何 shap 关键字
③ 返回的 top_features 列所有行必须为空字符串 ""
④ 批量 1000 只基金的端到端耗时(含特征计算+预测)应 < 3 秒。
1天
W8 S6-03 流水线端到端闭环与双轨制文档收尾
验证 Docker 环境下一键运行,更新技术真相源与 Redmine 状态。
章程 M5 Done标准;章程成功标准(流水线闭环);架构基线 3 (双轨制) ① 在 Ubuntu 24 Docker 环境下,通过一条主指令成功执行“全量刷盘重训 → 自动评估 → 自动归档模型”全流程,零人工干预
docs/inference_api.md 沉淀单基金/批量接口的输入输出 Schema 及 CR-006 的调用约束;
③ Gitea /docs 与 Redmine M1-M5 全部任务状态实现 100% 同步闭合。
2天

可优化与可复用逻辑提示

1. 特征计算“精简版”的接口复用(防笨重化)

Step 3 的 calculate_raw_features 是针对按年分区大表设计的,内部包含了 segment_id 的复杂隔离逻辑与按年循环。
但在 Step 6 推理时,输入的永远是单只基金的小 DataFrame(通常 < 500 行),且 segment_id 必然只有 0 这一个值。
建议实现:在 feature_engineering.py 中暴露一个轻量级包装器 calculate_raw_features_inference(df_single_fund: pl.DataFrame),内部直接调用核心滚动逻辑,但跳过按年循环与 segment 隔离的开销。这能将单基金特征计算耗时从秒级压榨到毫秒级。

2. MLflow 模型加载的“防穿透”硬编码

规约 5.5 提到“新模型打 Production 标签后直接替换”。
防坑点:MLflow 的 mlflow.lightgbm.load_model("models:/model_name/latest") 中的 latest 指的是最后运行的一次 Experiment,而不是打了 Production 标签的版本。如果在一次失败的训练后没有打标,latest 会指向垃圾模型。
强制要求inference.py 加载模型必须使用如下严格语法:

model_uri = "models:/fund_dca_model/production"
# 禁止使用 "latest"
model = mlflow.lightgbm.load_model(model_uri)

3. “得分区间文案”的逻辑外置

规约 6.3 的四段文案极易在后续业务迭代中被修改。建议将这四个模板(包含占位符 {top_feature})统一定义在 config/config.yamlinference.recommendation_templates 节点下,inference.py 仅负责读取配置并 format(),严禁在代码中硬编码大段中文文案。


新增/修改文件清单

操作 文件路径 简介与企业级约束
新增 src/inference.py 收官核心模块。包含 predict_single_fund(csv_path: str) -> dictpredict_batch(df: pl.DataFrame) -> pl.DataFrame 两个公开接口。全局严禁显式 import pandas
新增 docs/inference_api.md 技术真相源。定义推理服务的契约,包含:单基金输入 CSV 格式示例、输出 JSON Schema、批量接口 DataFrame Schema,以及醒目标注 CR-006 的调用约束。
修改 src/feature_engineering.py 新增 calculate_raw_features_inference() 轻量级公开函数,专为 Step 6 单基金小数据量优化,剥离大表循环逻辑。
修改 config/config.yaml inference: 节点下新增 recommendation_templates 字典,包含 80-100、60-79、40-59、0-39 四个区间的文案模板。
修改 docs/architecture_baseline.md 更新最终版数据流图,补充 Step 6 推理服务从 MLflow 拉取 Production 模型与全局参数的虚线指向。
修改 src/utils/config.py 对应新增 inference.recommendation_templates 的强类型属性。

下级目录

CR-006-批量推荐接口_SHAP_归因性能降级策略