0002-基金量化定投选基系统设计规约 » 历史记录 » 版本 6
Huarui Lin, 2026-04-12 14:53
| 1 | 1 | Huarui Lin | --- |
|---|---|---|---|
| 2 | 2 | Huarui Lin | |
| 3 | **document_id:** SPEC-FUND-DCA-001 |
||
| 4 | **version:** 1.0.0 |
||
| 5 | **status:** Frozen (Baseline) |
||
| 6 | **created_date:** 2026-04-11 |
||
| 7 | **approved_by:** Henry Lin |
||
| 8 | **redmine_link:** https://redmine.persys.top/projects/fund-selection-system |
||
| 9 | |||
| 10 | 1 | Huarui Lin | --- |
| 11 | # 基金量化定投选基系统 — 最终设计规约 (v1.0) |
||
| 12 | > **⚠️ 机器与人工强制红线声明** |
||
| 13 | > 本文档为系统的唯一业务与技术真相源。AI 必须严格按此执行,研发人员任何代码实现若与本文档冲突,一律以本文档为准。**严禁擅自修改任何参数、公式或逻辑。** |
||
| 14 | ## 变更日志 |
||
| 15 | | 版本 | 日期 | 变更人 | 变更内容摘要 | 审批状态 | |
||
| 16 | |------|------|--------|--------------|----------| |
||
| 17 | | v1.0.0 | 2026-04-11 | Henry Lin | 初始基线版本确立,锁定所有业务参数、引擎防火墙与工程质量红线。 | ✅ Approved | |
||
| 18 | --- |
||
| 19 | ## 〇、决策确认总表 |
||
| 20 | | 编号 | 决策项 | 最终决策 | |
||
| 21 | |------|--------|---------| |
||
| 22 | | D-1 | 类型表无时间维度 | 使用最新 `fund_type` 全局过滤,工程中预留接口扩展位 | |
||
| 23 | | D-2 | 标签定义 | DCA 简单平均成本法,20% 止盈,150 周窗口,未达则 0 | |
||
| 24 | | D-3 | label=0 样本保留 | 单基金上限 `max_samples_per_fund × 0.3`(默认 50×0.3=15 条) | |
||
| 25 | | D-4 | 无风险利率 | 年化 2.0%,复利折算至周频 | |
||
| 26 | | D-5 | 增量更新 | 不采用增量,每周全量刷盘重训,MLflow 管理模型版本替换 | |
||
| 27 | | D-6 | 止盈贴近度特征 | 不添加,依赖现有 `dca_return` 系列特征覆盖定投浮盈信息 | |
||
| 28 | | D-7 | IC 计算 | 严格限制在 Time-Series Split 训练折内局部计算 | |
||
| 29 | ## 一、数据基础 |
||
| 30 | ### 1.1 数据源 Schema |
||
| 31 | **净值表 `fund_net_info`**(主表): |
||
| 32 | | 字段 | 类型 | 说明 | |
||
| 33 | |------|------|------| |
||
| 34 | | `id` | BigInteger PK | 自增主键,清洗阶段丢弃 | |
||
| 35 | | `fund_id` | String(50) | 基金代码 | |
||
| 36 | | `cumulative_net_value` | Numeric(12,4) | 累计净值,读入后映射为 Float64 | |
||
| 37 | | `net_value_date` | Date | 净值日期 | |
||
| 38 | **基金信息表 `fund_basic_info`**(辅助表): |
||
| 39 | | 字段 | 类型 | 说明 | |
||
| 40 | |------|------|------| |
||
| 41 | | `fund_id` | String PK | 基金代码 | |
||
| 42 | | `fund_name` | String | 基金名称 | |
||
| 43 | | `fund_type` | String | 当前基金类型(仅快照值,无时间维度) | |
||
| 44 | | `create_date` | Date | 基金成立日期(仅作参考,系统以首条净值日期为实际 inception_date) | |
||
| 45 | **数据规模**:约 3000 万行,周频,64GB 内存。 |
||
| 46 | ### 1.2 数据处理规则 |
||
| 47 | | 环节 | 规则 | 说明 | |
||
| 48 | |------|------|------| |
||
| 49 | | 周频聚合 | 同一 `fund_id` + 同一自然周内若存在多条记录,取该周最后一个交易日的 `cumulative_net_value`,其余丢弃 | 防止重复计数 | |
||
| 50 | | 类型过滤 | 一次性 JOIN `fund_basic_info`,保留白名单类型基金,后续不再变更 | 白名单类型见下方 | |
||
| 51 | | 建仓期剔除 | 以每只基金第一条有效净值的 `net_value_date` 为 `inception_date`,硬删前 12 周数据 | 防止建仓期净值污染波动率特征 | |
||
| 52 | | 缺失值填充 | 单基金内部按时间排序,前向填充;**连续缺失 > 4 周的区段直接截断**,截断后各段独立参与后续计算 | 短缺用 ffill,长缺硬切 | |
||
| 53 | | 异常值 | `cumulative_net_value ≤ 0` → 剔除该行;单周涨跌幅绝对值 > 50% → 该值置 NULL(不删行) | 保留时间序列连续性 | |
||
| 54 | | 存活期过滤 | 单基金(含所有截断段的总有效周数)< 52 周则整体剔除 | 避免信息量过少引入噪声 | |
||
| 55 | | 清盘基金 | 数据中断前所有有效数据完整保留,满足 52 周最低要求即纳入 | 控制幸存者偏差 | |
||
| 56 | **类型白名单**: |
||
| 57 | `['股票型', '混合型-偏股', '混合型-灵活', 'QDII-普通股票', 'QDII-混合偏股', 'QDII-混合灵活']` |
||
| 58 | ### 1.3 存储方案 |
||
| 59 | - 按年份分区存储:`data/processed/net_value_YYYY.parquet` |
||
| 60 | - Parquet `row_group_size = 100,000` |
||
| 61 | - 每次全量刷新时覆盖写入,不追加 |
||
| 62 | ## 二、计算引擎架构 |
||
| 63 | 3 | Huarui Lin | |
| 64 | 1 | Huarui Lin | ### 2.1 DuckDB ↔ Polars 职责防火墙 |
| 65 | 6 | Huarui Lin | |
| 66 | 3 | Huarui Lin | {{mermaid |
| 67 | 1 | Huarui Lin | graph TD |
| 68 | 6 | Huarui Lin | subgraph 数据处理流水线 |
| 69 | A[DuckDB] -->|SQL下推过滤| B[Arrow单年级小于300万行] |
||
| 70 | B --> C[Polars计算引擎] |
||
| 71 | C --> D[覆盖写回Parquet加MLflow] |
||
| 72 | 1 | Huarui Lin | end |
| 73 | 6 | Huarui Lin | subgraph DuckDB职责边界 |
| 74 | A1[按年份提取Parquet] |
||
| 75 | A2[JOIN类型表] |
||
| 76 | A3[WHERE条件过滤] |
||
| 77 | A4[COUNT和SUM审计统计] |
||
| 78 | 1 | Huarui Lin | end |
| 79 | 6 | Huarui Lin | subgraph Polars职责边界 |
| 80 | C1[rolling滚动窗口] |
||
| 81 | C2[EWM指数加权] |
||
| 82 | C3[截面ZScore] |
||
| 83 | 1 | Huarui Lin | C4[标签生成] |
| 84 | C5[共线性筛选] |
||
| 85 | end |
||
| 86 | A --- A1 |
||
| 87 | A --- A2 |
||
| 88 | A --- A3 |
||
| 89 | A --- A4 |
||
| 90 | C --- C1 |
||
| 91 | C --- C2 |
||
| 92 | C --- C3 |
||
| 93 | C --- C4 |
||
| 94 | C --- C5 |
||
| 95 | }} |
||
| 96 | 5 | Huarui Lin | |
| 97 | 3 | Huarui Lin | |
| 98 | 1 | Huarui Lin | ### 2.2 防火墙规则(铁律) |
| 99 | | 编号 | 规则 | |
||
| 100 | |------|------| |
||
| 101 | | FW-1 | DuckDB 仅负责 Parquet 读写、跨表 JOIN、行列过滤、聚合统计审计 | |
||
| 102 | | FW-2 | **严禁** DuckDB 执行滚动窗口函数,该类计算全部由 Polars 原生表达式完成 | |
||
| 103 | | FW-3 | DuckDB → Polars 必须按年份拆分提取,每次 Arrow 转换 ≤ 单年数据量 | |
||
| 104 | | FW-4 | Polars 处理完毕后按年份分区覆盖写回 Parquet | |
||
| 105 | | FW-5 | DuckDB 每次启动重新注册 Parquet 创建 VIEW,不维护持久化数据库状态 | |
||
| 106 | ## 三、标签系统 |
||
| 107 | ### 3.1 标签定义 |
||
| 108 | | 参数 | 值 | 说明 | |
||
| 109 | |------|-----|------| |
||
| 110 | | 起始点 | T 日 | T 日特征截止,T+1 日开始模拟 | |
||
| 111 | | 定投方式 | 等额周定投,每周归一化投入 1 元 | — | |
||
| 112 | | 成本计算 | 简单平均成本法 | `avg_cost = Σ(各周买入净值) / 已买入周数` | |
||
| 113 | | 收益率计算 | `(当前净值 - avg_cost) / avg_cost` | — | |
||
| 114 | | 止盈触发 | 定投累计收益率 **首次 ≥ 20%** | 触发即刻停止模拟 | |
||
| 115 | | 标签映射 | `150 - 耗费周数` | 值域 [1, 149] | |
||
| 116 | | 未达标 | 标签 = 0 | 含:150 周内未达、基金清盘中断 | |
||
| 117 | | 手续费 | 暂不扣除 | `config.yaml` 中预留 `deduct_fee: false` 开关 | |
||
| 118 | ### 3.2 标签计算算法 |
||
| 119 | **核心约束**:不使用全局 Python for 循环,采用 Polars `group_by` + NumPy 向量化并行策略。 |
||
| 120 | ```python |
||
| 121 | 对每只基金(单次处理约 300-500 周的极小数组): |
||
| 122 | nav_array = [nav_0, nav_1, ..., nav_T] |
||
| 123 | 对每个时间点 t(作为"开始定投"的候选时刻): |
||
| 124 | ① 有效窗口 = nav_array[t+1 : t+151] (numpy 切片自动截断于数据末尾) |
||
| 125 | ② 若有效窗口长度 < 2 → label = 0(数据不足以定投) |
||
| 126 | ③ 累计成本序列:cum_cost = np.cumsum(有效窗口) / np.arange(1, len+1) |
||
| 127 | ④ 当前净值序列:current = 有效窗口 |
||
| 128 | ⑤ DCA 收益率序列:dca_ret = (current - cum_cost) / cum_cost |
||
| 129 | ⑥ 首次达标检测:mask = dca_ret >= 0.20 |
||
| 130 | - any(mask) 为 True → weeks = np.argmax(mask) + 1 → label = 150 - weeks |
||
| 131 | - any(mask) 为 False → label = 0 |
||
| 132 | ``` |
||
| 133 | **清盘基金处理**:NumPy 切片在数组越界时自动截断。清盘基金的有效窗口短于 150 周,若截断前未触发止盈则 `any(mask)` 为 False,标签自动为 0。**无需额外逻辑。** |
||
| 134 | ### 3.3 样本平衡机制 |
||
| 135 | | 样本类型 | 保留规则 | |
||
| 136 | |----------|---------| |
||
| 137 | | label > 0(正样本) | 单基金最多保留 `max_samples_per_fund`(默认 50)条,等距抽样 | |
||
| 138 | | label = 0(负样本) | 单基金最多保留 `max_samples_per_fund × 0.3`(默认 15)条,等距抽样 | |
||
| 139 | | 等距抽样方法 | `np.linspace(0, len(samples)-1, N, dtype=int)`,按时间均匀取点 | |
||
| 140 | ## 四、特征系统 |
||
| 141 | ### 4.1 特征目录(共约 40+ 维) |
||
| 142 | #### A. 均值回归类 |
||
| 143 | | 特征名 | 计算方式 | 窗口期 | |
||
| 144 | |--------|---------|--------| |
||
| 145 | | `price_vs_ma_ratio` | `nav / rolling_mean(nav, W)` | 12, 26, 52 | |
||
| 146 | | `price_vs_ma_ratio_ewm` | `nav / ewm_mean(nav, span=W)` | 26, 52 | |
||
| 147 | #### B. 波动类 |
||
| 148 | | 特征名 | 计算方式 | 窗口期 | |
||
| 149 | |--------|---------|--------| |
||
| 150 | | `rolling_std` | `rolling_std(weekly_return, W)` | 12, 26, 52 | |
||
| 151 | | `downside_vol` | `rolling_std(min(weekly_return, 0), W)` | 26, 52 | |
||
| 152 | | `volatility_regime` | `rolling_std(12w) / rolling_std(52w)` | (12 vs 52) | |
||
| 153 | #### C. 趋势与动量类 |
||
| 154 | | 特征名 | 计算方式 | 窗口期 | |
||
| 155 | |--------|---------|--------| |
||
| 156 | | `momentum` | `(nav_t / nav_{t-W}) - 1` | 4, 12, 26, 52 | |
||
| 157 | | `max_drawdown` | `1 - nav_t / rolling_max(nav, W)` | 26, 52 | |
||
| 158 | | `trend_slope` | 对 `log(nav)` 做 OLS 线性回归斜率,再年化 | 26, 52 | |
||
| 159 | | `trend_r_squared` | 上述回归的 R² | 26, 52 | |
||
| 160 | | `consecutive_down_weeks` | 从当前向前连续 `weekly_return < 0` 的周数 | — | |
||
| 161 | #### D. 定投特有类 |
||
| 162 | | 特征名 | 计算方式 | 窗口期 | |
||
| 163 | |--------|---------|--------| |
||
| 164 | | `dca_cost_ratio` | `rolling_mean(nav, W) / nav`(值 <1 表示浮盈) | 12, 26, 52 | |
||
| 165 | | `dca_return` | `(nav - rolling_mean(nav, W)) / rolling_mean(nav, W)` | 12, 26, 52 | |
||
| 166 | | `dca_return_vol` | `rolling_std(dca_return 逐周序列, W)` | 26, 52 | |
||
| 167 | | `dca_win_rate` | `rolling_mean(dca_return > 0 转为 0/1, W)` | 26, 52 | |
||
| 168 | #### E. 风险调整类 |
||
| 169 | | 特征名 | 计算方式 | 窗口期 | |
||
| 170 | |--------|---------|--------| |
||
| 171 | | `rolling_sharpe` | `(rolling_mean(weekly_ret) - weekly_rf) / rolling_std(weekly_ret) × √52` | 26, 52 | |
||
| 172 | | `rolling_sortino` | `(rolling_mean(weekly_ret) - weekly_rf) / rolling_std(min(weekly_ret, 0)) × √52` | 26, 52 | |
||
| 173 | | `calmar_ratio` | `annualized_return(W) / max_drawdown(W)` | 52 | |
||
| 174 | **无风险利率折算公式**: |
||
| 175 | `weekly_rf = (1 + 0.02)^(1/52) - 1 ≈ 0.000381 (0.0381%)` |
||
| 176 | ### 4.2 窗口有效值判定 |
||
| 177 | - 对每个滚动窗口特征,计算窗口内非 NULL 值的数量 |
||
| 178 | - 有效值 < `window_size × min_valid_ratio`(默认 0.20) → 特征值输出 NULL |
||
| 179 | - 示例:52 周窗口至少需要 11 个有效值(`52 × 0.2 = 10.4`,向上取整) |
||
| 180 | ### 4.3 截面标准化(T-1 严格模式) |
||
| 181 | 分四步严格执行: |
||
| 182 | 1. **计算统计量**:计算每个日期截面的均值、标准差、中位数、MAD。 |
||
| 183 | 2. **时间对齐**:将统计量整体 `shift(1)`,即 T 日的标准化参数使用 T-1 日截面的统计量。**首日(数据集第一天)无 T-1 可用,该日所有标准化特征输出 NULL。** |
||
| 184 | 3. **Z-Score**:将 shifted 统计量 Join 回主表,执行 `z_score = (raw_feature - T1_mean) / T1_std`。 |
||
| 185 | 4. **MAD 去极值**: |
||
| 186 | `median = T1 截面中位数` |
||
| 187 | `mad = median(|x - median|) × 1.4826` |
||
| 188 | `clip z_score to [median - 3×mad, median + 3×mad]` |
||
| 189 | **持久化**:每个时间截面的标准化参数独立保存为 `standardization_params.parquet`: |
||
| 190 | | 字段 | 类型 | 说明 | |
||
| 191 | |------|------|------| |
||
| 192 | | `date` | Date | 截面日期(即统计量的来源日期) | |
||
| 193 | | `feature_name` | String | 特征名 | |
||
| 194 | | `mean` | Float64 | 截面均值 | |
||
| 195 | | `std` | Float64 | 截面标准差 | |
||
| 196 | | `median` | Float64 | 截面中位数 | |
||
| 197 | | `mad` | Float64 | 截面 MAD | |
||
| 198 | 推理时加载该文件的最新行,对输入特征做同样的 Z-Score + MAD clip。 |
||
| 199 | ### 4.4 特征共线性剔除 |
||
| 200 | | 步骤 | 说明 | |
||
| 201 | |------|------| |
||
| 202 | | 1 | 计算所有特征间的 Pearson 相关系数矩阵 | |
||
| 203 | | 2 | 找出 \|r\| > 0.90 的特征对 | |
||
| 204 | | 3 | 对每个高相关特征对,计算各自与标签的 **Spearman Rank IC**(**仅在 Time-Series Split 的训练折内**计算,严禁跨时间窗口使用未来标签) | |
||
| 205 | | 4 | 保留 \|IC\| 更高的特征,剔除另一个 | |
||
| 206 | | 5 | 输出最终保留的特征列表,持久化为 `final_feature_list.json` | |
||
| 207 | ## 五、模型训练 |
||
| 208 | ### 5.1 训练数据流 |
||
| 209 | ```text |
||
| 210 | features.parquet (按年分区) |
||
| 211 | │ Polars: 读取并合并 |
||
| 212 | ▼ |
||
| 213 | labels.parquet |
||
| 214 | │ Polars: LEFT JOIN (on fund_id + date) |
||
| 215 | ▼ |
||
| 216 | train_dataset.parquet |
||
| 217 | schema: fund_id | date | feature_1 | ... | feature_K | label |
||
| 218 | ``` |
||
| 219 | 缺失值处理:特征 NULL 在 LightGBM 内部自动处理(设为缺失值,树模型原生支持),不做预填充。 |
||
| 220 | ### 5.2 交叉验证 |
||
| 221 | - 使用 `TimeSeriesSplit(n_splits=5)` |
||
| 222 | - **严禁** 随机 K-Fold |
||
| 223 | - 每一折的训练集内部独立执行:截面标准化 → 共线性筛选 |
||
| 224 | - 验证集仅使用对应训练折产出的标准化参数 |
||
| 225 | ### 5.3 Optuna 超参搜索 |
||
| 226 | | 参数 | 搜索范围 | 类型 | |
||
| 227 | |------|---------|------| |
||
| 228 | | `num_leaves` | [31, 511] | int | |
||
| 229 | | `learning_rate` | [0.01, 0.3] | log uniform | |
||
| 230 | | `feature_fraction` | [0.5, 1.0] | uniform | |
||
| 231 | | `bagging_fraction` | [0.5, 1.0] | uniform | |
||
| 232 | | `lambda_l1` | [0, 10] | log uniform | |
||
| 233 | | `lambda_l2` | [0, 10] | log uniform | |
||
| 234 | | `min_child_samples` | [20, 100] | int | |
||
| 235 | | `max_depth` | [5, 12] | int | |
||
| 236 | 停止条件:200 次试验 或 1 小时,先到者停。 |
||
| 237 | ### 5.4 评估指标体系 |
||
| 238 | **模型层**: |
||
| 239 | | 指标 | 说明 | |
||
| 240 | |------|------| |
||
| 241 | | AUC | 区分"能止盈"与"不能止盈"的能力 | |
||
| 242 | | NDCG@20 | Top-20 推荐的排序质量 | |
||
| 243 | **业务层(回测评估)**: |
||
| 244 | 在验证集的每个时间截面 T 上: |
||
| 245 | 1. 用模型对所有候选基金打分 |
||
| 246 | 2. 按得分分四桶:Top 20 / Top 21-50 / Top 51-100 / 其余 |
||
| 247 | 3. 每桶模拟等权周定投组合(每周投入 20 元均分到该桶基金) |
||
| 248 | 4. 追踪组合净值,计算: |
||
| 249 | | 回测指标 | 说明 | |
||
| 250 | |----------|------| |
||
| 251 | | 年化收益率 | 定投组合的实际年化回报 | |
||
| 252 | | 最大回撤 | 组合净值的最大峰谷跌幅 | |
||
| 253 | | Sharpe Ratio | 风险调整后收益 | |
||
| 254 | | Calmar Ratio | 年化收益 / 最大回撤 | |
||
| 255 | | 止盈成功率 | 150 周内达到 20% 定投收益率的比例 | |
||
| 256 | 5. 基准对照:全标的池等权随机组合的相同指标 |
||
| 257 | ### 5.5 MLflow 实验记录 |
||
| 258 | 每次训练自动记录: |
||
| 259 | - Optuna 最优超参数 |
||
| 260 | - 模型层指标(AUC、NDCG)+ 业务层回测指标 |
||
| 261 | - LightGBM 模型文件(`.txt`) |
||
| 262 | - `config.yaml` 快照 |
||
| 263 | - `final_feature_list.json` |
||
| 264 | - `standardization_params.parquet` |
||
| 265 | - SHAP summary plot(图片) |
||
| 266 | - 各分桶回测净值曲线(图片) |
||
| 267 | 模型通过 MLflow Model Registry 管理,新模型打 `Production` 标签后直接替换推理服务加载的模型版本,旧版本归档至 `Archived`。 |
||
| 268 | ## 六、推理服务 |
||
| 269 | ### 6.1 输入输出 |
||
| 270 | | 场景 | 输入 | 输出 | |
||
| 271 | |------|------|------| |
||
| 272 | | 单基金查询 | 单只基金近期周频净值 CSV(≥52 周) | 推荐得分(0-100)、Top5 正/负贡献特征、决策建议文本 | |
||
| 273 | | 批量推荐 | 基金代码列表 + 最新净值数据 | 排序后的推荐 DataFrame(fund_id, score, top_features, recommendation) | |
||
| 274 | ### 6.2 推理流程 |
||
| 275 | ```text |
||
| 276 | 输入净值 → Polars 读入 |
||
| 277 | ▼ |
||
| 278 | 特征计算(复用 Module3 逻辑,单基金精简版) |
||
| 279 | ▼ |
||
| 280 | 加载 standardization_params.parquet(最新一周参数) |
||
| 281 | ▼ |
||
| 282 | T-1 参数做 Z-Score + MAD clip |
||
| 283 | ▼ |
||
| 284 | 加载 LightGBM Production 模型 → 预测概率 |
||
| 285 | ▼ |
||
| 286 | score = round(概率 × 100) → 0-100 整数分 |
||
| 287 | ▼ |
||
| 288 | SHAP TreeExplainer → 单样本特征归因 |
||
| 289 | ▼ |
||
| 290 | 输出:得分 + Top5正向特征 + Top5负向特征 + 决策建议文本 |
||
| 291 | ``` |
||
| 292 | ### 6.3 决策建议文本生成规则 |
||
| 293 | | 得分区间 | 建议等级 | 文案模板 | |
||
| 294 | |----------|---------|---------| |
||
| 295 | | 80-100 | 强烈推荐定投 | "该基金趋势强劲、波动适中,定投体验优异,建议立即开始定投" | |
||
| 296 | | 60-79 | 推荐定投 | "该基金整体特征良好,建议关注并适时开始定投" | |
||
| 297 | | 40-59 | 中性观望 | "该基金部分特征达标,建议持续关注,等待更好的定投入场时机" | |
||
| 298 | | 0-39 | 不推荐 | "该基金当前特征不佳(主要受XX拖累),建议暂缓定投" | |
||
| 299 | 具体文案需拼接该基金的 Top 贡献特征名。 |
||
| 300 | ## 七、工程质量规范 |
||
| 301 | ### 7.1 目录结构 |
||
| 302 | ```text |
||
| 303 | project-root/ |
||
| 304 | ├── pyproject.toml |
||
| 305 | ├── config/ |
||
| 306 | │ └── config.yaml |
||
| 307 | ├── data/ |
||
| 308 | │ ├── raw/ # 原始 CSV / Parquet |
||
| 309 | │ ├── processed/ # 清洗后 Parquet(按年分区) |
||
| 310 | │ ├── features/ # 特征矩阵 Parquet(按年分区) |
||
| 311 | │ ├── models/ # 模型文件 & 标准化参数 |
||
| 312 | │ └── metadata/ # SHA-256 hash 校验文件 |
||
| 313 | ├── src/ |
||
| 314 | │ ├── __init__.py |
||
| 315 | │ ├── utils/ |
||
| 316 | │ │ ├── __init__.py |
||
| 317 | │ │ ├── config.py # YAML 配置加载器 |
||
| 318 | │ │ ├── logging.py # 结构化日志配置 |
||
| 319 | │ │ └── hash.py # SHA-256 数据血缘校验 |
||
| 320 | │ ├── data_loader.py # 数据清洗与入库 |
||
| 321 | │ ├── feature_engineering.py # 特征计算引擎 |
||
| 322 | │ ├── label_generator.py # 标签生成器 |
||
| 323 | │ ├── trainer.py # 模型训练与评估 |
||
| 324 | │ └── inference.py # 推理服务 |
||
| 325 | ├── tests/ |
||
| 326 | │ ├── test_label_generator.py |
||
| 327 | │ └── test_feature_engineering.py |
||
| 328 | ├── mlruns/ # MLflow 实验追踪 |
||
| 329 | └── logs/ # 运行日志 |
||
| 330 | ``` |
||
| 331 | ### 7.2 配置与代码分离 |
||
| 332 | 所有硬参数集中在 `config.yaml`,代码中严禁魔法数字。核心参数分区如下: |
||
| 333 | ```yaml |
||
| 334 | # === 标签参数 === |
||
| 335 | label: |
||
| 336 | target_return: 0.20 |
||
| 337 | max_weeks: 150 |
||
| 338 | deduction_method: "simple_avg" # 预留: "irr" |
||
| 339 | deduct_fee: false |
||
| 340 | purchase_fee_rate: 0.0012 |
||
| 341 | # === 数据过滤 === |
||
| 342 | filter: |
||
| 343 | fund_type_whitelist: |
||
| 344 | - "股票型" |
||
| 345 | - "混合型-偏股" |
||
| 346 | - "混合型-灵活" |
||
| 347 | - "QDII-普通股票" |
||
| 348 | - "QDII-混合偏股" |
||
| 349 | - "QDII-混合灵活" |
||
| 350 | min_net_value_weeks: 52 |
||
| 351 | building_period_weeks: 12 |
||
| 352 | max_gap_weeks: 4 |
||
| 353 | single_week_change_limit: 0.50 |
||
| 354 | # === 特征参数 === |
||
| 355 | feature: |
||
| 356 | windows: |
||
| 357 | mean_reversion: [12, 26, 52] |
||
| 358 | volatility: [12, 26, 52] |
||
| 359 | momentum: [4, 12, 26, 52] |
||
| 360 | trend: [26, 52] |
||
| 361 | dca_specific: [12, 26, 52] |
||
| 362 | risk_adjusted: [26, 52] |
||
| 363 | min_valid_ratio: 0.20 |
||
| 364 | outlier_method: "mad" |
||
| 365 | outlier_bound: 3.0 |
||
| 366 | # === 截面标准化 === |
||
| 367 | standardization: |
||
| 368 | method: "zscore" |
||
| 369 | use_shift_1: true # 使用 T-1 截面统计量 |
||
| 370 | # === 样本平衡 === |
||
| 371 | sample_balance: |
||
| 372 | max_samples_per_fund: 50 |
||
| 373 | zero_label_ratio: 0.3 # label=0 的上限比例 |
||
| 374 | # === 模型训练 === |
||
| 375 | model: |
||
| 376 | time_series_n_splits: 5 |
||
| 377 | optuna_budget: 200 |
||
| 378 | optuna_timeout_hours: 1 |
||
| 379 | # === 风险 === |
||
| 380 | risk_free_rate: |
||
| 381 | annual: 0.02 |
||
| 382 | ``` |
||
| 383 | ### 7.3 数据血缘 |
||
| 384 | | 检查对象 | 算法 | 存储位置 | 触发时机 | |
||
| 385 | |----------|------|---------|---------| |
||
| 386 | | 原始数据文件 | SHA-256 | `data/metadata/raw_hash.json` | 数据入库前 | |
||
| 387 | | 清洗后 Parquet | SHA-256(按文件) | `data/metadata/processed_hash.json` | 清洗完成后 | |
||
| 388 | | 特征矩阵 Parquet | SHA-256(按文件) | `data/metadata/features_hash.json` | 特征计算完成后 | |
||
| 389 | | `config.yaml` | SHA-256 | MLflow artifact | 每次训练开始时 | |
||
| 390 | Pipeline 启动时自动校验上游 hash,不匹配则输出 WARNING 日志但不中断流程。 |
||
| 391 | ### 7.4 工程约束 |
||
| 392 | | 约束 | 说明 | |
||
| 393 | |------|------| |
||
| 394 | | 禁止 Pandas | 仅允许 LightGBM `.fit()` 内部隐式调用 | |
||
| 395 | | 类型提示 | 所有自定义函数必须有 Python Type Hints | |
||
| 396 | | 日志 | 核心数据处理环节必须记录运行状态与耗时 | |
||
| 397 | | 异常处理 | 关键步骤 try-except 防止单点故障导致全流程崩溃 | |
||
| 398 | | 单元测试 | `label_generator` 必须覆盖全部 7 个场景(见第九节) | |
||
| 399 | | 特征存储 | 严禁 Pivot 成宽表,始终保持长表形态 | |
||
| 400 | ## 八、实施路线图 |
||
| 401 | 按以下 6 个 Step 逐步交付,每步确认后再推进: |
||
| 402 | | Step | 模块 | 交付物 | 依赖 | |
||
| 403 | |------|------|--------|------| |
||
| 404 | | **1** | 工程脚手架 | `pyproject.toml`、目录树、`config.yaml`、`config.py`、`logging.py`、`hash.py` | 无 | |
||
| 405 | | **2** | 数据基建与入库 | `data_loader.py` + 清洗后的按年 Parquet + DuckDB VIEW + 数据质量审计日志 | Step 1 | |
||
| 406 | | **3** | 特征工程引擎 | `feature_engineering.py` + `features.parquet` + `standardization_params.parquet` + `final_feature_list.json` | Step 2 | |
||
| 407 | | **4** | 标签生成器 | `label_generator.py` + `labels.parquet` + `test_label_generator.py` | Step 2 | |
||
| 408 | | **5** | 模型训练与评估 | `trainer.py` + 模型文件 + 回测报告 + SHAP 图 + MLflow 记录 | Step 3 + Step 4 | |
||
| 409 | | **6** | 推理服务 | `inference.py`(单基金 + 批量) | Step 3 + Step 5 | |
||
| 410 | > **注**:Step 3 和 Step 4 无相互依赖,可并行开发。但在流水线执行中,Step 4 的标签需要 Join 到 Step 3 的特征表,所以最终训练集的生成依赖两者都完成。 |
||
| 411 | ## 九、单元测试必覆盖场景 |
||
| 412 | `tests/test_label_generator.py` 必须完整覆盖以下 7 个场景: |
||
| 413 | | 编号 | 测试场景 | 输入条件 | 期望输出 | |
||
| 414 | |------|---------|---------|---------| |
||
| 415 | | 1 | 正常止盈 | 净值从 1.0 经 N 周涨到触发 20% DCA 收益率 | `label = 150 - N` | |
||
| 416 | | 2 | 150周未达 | 净值缓慢增长,150 周内 DCA 收益率始终 < 20% | `label = 0` | |
||
| 417 | | 3 | 清盘截断 | 净值开始定投后,60 周时数据中断,期间未达 20% | `label = 0` | |
||
| 418 | | 4 | 清盘前止盈 | 净值在 30 周达标,第 50 周数据中断 | `label = 120` | |
||
| 419 | | 5 | 精确边界 | 第 1 周净值即满足 20% 收益(防御性测试) | `label = 149` | |
||
| 420 | | 6 | 全亏损路径 | 净值从 1.0 持续下跌到 0.5 | `label = 0` | |
||
| 421 | | 7 | 建仓期后首周 | 数据恰好从第 13 周开始(前 12 周已被 Module2 剔除) | 正常生成标签,不报错 | |