Files
shy3130 e60c438328 fix(mining): 修复 Windows 挖掘锁状态误报
锁诊断优先使用当前后端实际持锁对象确认自身归属,避免 Windows 排他锁阻止二次读取文件时误报未知持有者。真实外部占用仍按陈旧判定处理,并拒绝终止当前持锁后端。

验证:83 项相关 pytest 通过,Ruff 和 git diff --check 通过。原生 Windows 安装包仍待实机复验。

Signed-off-by: shy3130 <shy3130@qq.com>
2026-11-01 20:02:10 +08:00

149 lines
14 KiB
Markdown
Raw Permalink 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.
# 因子与策略挖掘
本文说明 V1 因子与策略挖掘的金融口径、执行边界、结果解释和运行要求。
## 功能边界
V1 仅研究仓库已经提供的日频因子和用户明确选择的 matrix-native 日线策略。它完成以下闭环:
1. 在训练区间重新计算因子方向与统计。
2. 按日截面 Rank IC 计算因子相关性并去重。
3. 搜索最多四个因子的受控排名组合;进入 beam 搜索的因子按训练折综合分排序并截断到 `beam_width` 个,保证单因子打分后组合搜索仍有代理预算可用。
4. 用嵌套样本外验证比较新组合;用户选择的已有策略作为对照轨在每个 outer 测试窗独立评估,不参与因子竞争,也不会占用候选名额。
5. 将运行、事件和结果持久化,允许刷新后重连。
6. 将候选保存到研究候选库;只有用户显式确认且通过晋级门槛后才发布独立策略。
V1 不生成任意公式,不接受 AI 自由代码,不使用扩展数据生成新因子,也不使用分钟数据优化入场。分钟级成交和分钟因子需要独立的数据完整性、防未来函数和性能边界,属于后续版本范围。
因子数量以运行时 `FACTOR_COLUMNS` 目录为准。当前目录与后续版本可能不同;单次请求硬上限为 48,不应把历史设计稿中的固定数量当作稳定 API 契约。
当前目录除价量技术类(动量、均线偏离、趋势、波动率、量价、价格位置、超买超卖)外,还包含三个 A 股实证维度:收益形态(`max_ret_20d` 彩票效应、`ret_skew_20d`、`up_days_20d`)、流动性(`amihud_20d` 非流动性、`turnover_z_60d` 换手异动)、涨停基因(`limit_up_count_20d/60d`,基于存储列 `consecutive_limit_ups` 计数,涨停判定沿用环境信号口径)。这些因子仍只用本地日频存储数据计算,不依赖扩展数据源。
财务因子(`pb_latest`、`roe_latest`、`gross_margin_latest`、`net_margin_latest`、`revenue_yoy_latest`、`net_income_yoy_latest`、`debt_ratio_latest`)来自本地财务快照(数据页同步),采用严格点时口径:**因子值只在晚于公告日的交易日生效**(公告多在盘后, 保守取公告次一交易日起),同一报告期以最新公告为准。无财务数据的标的、公告前的日期一律为空值并从当日截面剔除,绝不填 0;本地完全没有财务数据时因子回测直接返回明确错误而不是全空结果。回测区间早于首次公告时同样报告"无有效数据"。挖掘中财务因子在训练折覆盖不足时综合分为 0 并被自然淘汰,不会阻塞其他因子。财务报表同步按 `(symbol, period_end)` 累积历史(同 shares 表模式),每次同步只拉最新期并为新标的补全量历史;随着季度累积,财务因子的可回测区间会逐步变长。
## 时间与收益口径
### 全局交易日标签
1、3、5 日 forward return 都按全局交易日轴精确连接。停牌或缺行不会把“下一条标的数据”误当成下一交易日。
周频使用每个 ISO 周的第一个实际交易日,月频使用每月第一个实际交易日。节假日不会导致整周漏掉调仓。
### T-1 市场环境
交易日 T 只能使用上一交易日已经得到的环境标签 T-1。**统计与挖掘口径**聚合为三档:
```text
strong = strong + lean_strong
range = range
weak = lean_weak + weak
```
**策略回测的环境过滤**按原始五档匹配:勾选“强势”只允许 T-1 为 strong 的交易日入场,需要三档口径时必须同时勾选“强势+偏强”。挖掘内部的 strong/range/weak 环境拆分仍按上方三档聚合实现(显式枚举原始状态),两者互不影响。
正式区间缺少前驱交易日或前驱环境时会 fail-closed,不会用当日环境、最近自然日或默认环境补齐。
### 成交和成本
候选策略沿用现有回测撮合规则,包括 T+1、佣金、卖侧印花税、滑点以及涨跌停不可成交约束。因子统计中的 long-short spread 仅用于衡量因子区分能力,是理论价差,不表示 A 股可执行卖空。
## 防止未来数据
挖掘使用嵌套 walk-forward,而不是在完整样本上选择后再报告同一段表现:
- 每个 inner 训练折独立学习方向、计算统计、相关去重和组合搜索。
- inner test 只用于选择候选,不参与该折的训练计算。
- 选择完成后在完整 outer train 上重新训练。
- outer test 只进行一次最终样本外评估。
- 训练和测试之间保留 purge 与 embargo;默认 purge 为 30 个交易日。
任何 fold 缺数据、缺 T-1 环境或不能可靠撮合时都记录为 skipped 并给出原因,缺失指标返回 `null`,不会显示为 0。
## 候选证据口径
run 级的“有效折”统计只描述因子赛道:它按每折被选中的候选聚合,不是任何单个候选的样本量。单个候选的逐折证据由三类行组成,`folds.parquet` 的 `evaluation_kind` 区分:
- `selected`:该候选在该 outer 折经 inner 验证胜出后的 outer 测试结果。
- `cross`:该候选的定义在其他折胜出后,在本折补评的跨折结果。胜出定义会在所有 outer 折上评估,使单个候选的证据不再依赖“它恰好在哪里获胜”。
- `benchmark`:对照策略在该 outer 测试窗的独立评估,与因子赛道成败无关;即使因子赛道在某折没有任何候选完成内部验证,对照行仍会写入。
对照策略是固定定义,不需要逐折重拟合,因此每个 outer 折只评估一次;预算耗尽时写入带原因的 skipped 行,不会静默缺失。探索档(exploratory)结果固定为低置信度,只能保存为 pending 候选。
## 晋级与发布门槛
保存(promote)始终允许,结果进入研究候选库 pending 状态。发布(publish)在服务端强制校验以下证据门槛,不满足即拒绝并返回原因列表:
- 非探索档置信度(exploratory 运行产生的候选不能发布);
- 至少 2 个有效 outer 折;
- 正收益折比例不低于 2/3;
- 样本外 Sharpe 不低于 0.5;
- 最大回撤不劣于 -25%;
- 样本外交易数不低于 60。
门槛按 artifact 行指标在读取时计算,不改变历史 artifact 的 schema,因此旧运行的候选会得到相同的门槛判断。工作台会在候选上显示“达标/未达标”标记,未达标的候选“显式发布”按钮被禁用并展示原因。
## 冠军排序与权重精调(mining-v3)
选冠语义自 v3 起从"外层折 Sharpe 均值"改为**稳健分** `0.6×中位 + 0.4×最差`(复用 `robust_optimizer.robust_score`):artifact 的 `score` 列记录稳健分,`oos_sharpe` 仍为均值且**发布门槛阈值不变**;新增可选列 `oos_worst_sharpe`(最差外层折 Sharpe),v2 旧 artifact 缺该列仍可正常读取。strict 档(confidence=high)在列存在时额外要求最差折 Sharpe>0 才能发布。算法版本号升级进数据指纹,防止升级后周度 claim 幂等复用旧口径结果。
冠军组合再做**权重精调**:固定首个因子权重 1.0(截面分位打分天然尺度不变),其余因子在 `{0.5, 1.0, 1.5, 2.0}` 网格上枚举(≤64 变体);stage1 复用本 run 已备矩阵在外层测试窗评估,stage2 全窗口复验,较 stage1 退化 >20% 即放弃。精调消耗 run 剩余预算(上限 96 trial),预算不足自动跳过;精调产物作为**独立候选行**追加(自有签名,不覆盖原冠军),run summary 带 `refinement` 摘要。
## 置信度
三个预算档使用不同的训练窗口:
| 档位 | 外层训练 | 外层测试 | 步长 | 用途 |
| --- | ---: | ---: | ---: | --- |
| exploratory | 126 | 63 | 63 | 数据较短时探索,只能作为低置信度 pending 候选 |
| balanced | 504 | 126 | 63 | 默认自动研究,至少需要 3 个 outer folds |
| strict | 756 | 126 | 126 | 更长训练窗,至少需要 3 个 outer folds |
探索性结果不是已验证策略。候选晋级仍需同时检查正收益折比例、样本外 Sharpe、最大回撤、交易数和环境样本覆盖;发布动作会按上文“晋级与发布门槛”在服务端强制执行同一组阈值。
## 任务与资源隔离
挖掘通过 `spawn` 子进程执行,不在 FastAPI 请求线程、实时行情回调线程或浏览器连接生命周期内运行。浏览器断开不会取消任务;刷新后可通过持久 run ID 重连。取消请求会进入 `cancelling`,界面应等待后端进入终态。
共享重任务限流容量为 2:普通回测、优化、walk-forward 和矩阵预热占 1,挖掘独占 2。因此一个挖掘任务不会与另一项大矩阵计算并发。
当前 run store 和重任务 limiter 使用进程内锁。生产环境必须只启动一个应用进程负责挖掘;多 worker Uvicorn 部署不能保证跨进程单飞、事件追加和容量限制。要支持多应用进程,必须先增加操作系统级文件锁或外部任务协调器。
**挖掘锁冲突的降级与强制释放**:数据目录的 `.mining_process.lock`(flock)保证同一时刻只有一个应用进程负责挖掘;持锁者会把 `{pid, started_at, role}` 写进锁文件(flock 仍是唯一事实,内容只是诊断线索,旧格式 `"0"` 视为持有者未知)。撞锁不再导致后端启动失败——面板以降级模式运行:挖掘启动入口(手动/自动/周度调度)返回 409,守护复验等其他功能不受影响;挖掘页顶部显示持有者横幅(pid、角色、挖掘存储静默时长)。当且仅当满足陈旧判定(持有者信息可读、进程存活且 pid 未被复用、无进行中的挖掘运行、挖掘存储 10 分钟内无写入——全部 fail-closed,任一不满足即拒绝并给出原因)时,可一键「强制停止并接管」:终止持有者进程并由当前后端获取锁,挖掘入口立即恢复。不做启动时自动清理——终止进程是破坏性操作,必须人工确认。
锁状态接口优先使用当前后端实际持有的锁句柄确认归属。Windows 的字节范围排他锁会阻止另一个句柄读取锁文件,即使两个句柄属于同一进程;读取诊断信息失败时,不能把已持锁的当前后端误报成未知残留进程。真正的外部占用仍按上述陈旧判定处理,启动标志本身不能代替实际持锁状态。
每个运行目录包含 manifest、compact summary、最近 256 条事件和四个 Parquet artifact。大面板、完整矩阵对象和逐折宽结果不会通过 worker IPC 返回。
## 自动运行
周度自动挖掘默认关闭,只允许 balanced 或 strict。启用后,它会在北京时间配置工作日及其后的同周工作日、日线 enriched 刷新和环境更新成功后尝试入队;如果配置日的数据流水线失败,后续工作日可以补跑。
同一 ISO 周使用确定性的 claim,只触发一次。已经生成运行记录的失败或 `skipped_prerequisite` 仍会占用该周 claim,不重复消耗资源。数据或 T-1 环境覆盖不足时会生成可见的 `skipped_prerequisite` 运行,不会静默降级到 exploratory。挖掘失败不改变已经成功的日度数据流水线状态。
自动任务只生成 pending 结果,永远不会自动发布策略。
## 保存与发布
“保存候选”只从服务端已注册的 `candidates.parquet` 读取定义,并重新校验 artifact schema、canonical signature、原始请求中的因子/策略范围和当前策略兼容性,再写入研究候选库。客户端只能提交 run ID 与 candidate signature,不能在保存时重交权重、方向、公式或代码。保存按 `(origin_run_id, candidate_signature)` 幂等;如果候选库已写入但 artifact backlink 回写失败,重试只修复 backlink,不会创建重复候选。
“发布”是独立的显式动作:
- 已有策略候选返回原策略 ID,不复制或覆盖源文件。
- 因子组合候选由服务端根据 run ID 与 candidate signature 派生独立策略 ID;客户端不能提交策略 ID、权重、方向、公式或代码。
- 不同 run 即使得到相同组合,也会生成互相独立的策略文件;同一 run 与 signature 重试时只验证已有源码并修复 backlink。
- 发布过程执行 AST/META 校验、create-only 原子写入、引擎 reload、策略缓存失效和监控状态失效。
- reload 或运行时失效失败会回滚首次创建的新文件;backlink 回写失败不会删除已成功加载的策略,重试可修复 backlink。
- 发布不会修改共享 `factor_rank_research` 源码或 override。
发布仅表示把固定候选变成可使用策略,不代表样本外表现会在未来持续。
## 性能口径
相关矩阵按交易日计算 pairwise-finite Spearman,只聚合因子平方级的逐日结果,不物化百万行永久 rank 宽表。缺失集合不同的因子会先取共同 finite 样本再排名;无法估计的 pair 保持空值,不按零处理。
生产 runtime 只生成本次搜索 horizon 的一列 forward label,并在阶段间释放中间列。内存表示优化对照 run `46ed81d537e9404baa06324a3ac3a45f` 在 132.0 万行、243 个交易日、44 个因子上峰值 RSS 为 1.291 GiB,总耗时 25.140 秒;相对 2.395 GiB、37.358 秒的原始对照基线,RSS 和耗时分别下降 46.09% 和 32.70%,四个 Parquet artifact 逐项完全一致。
最终 `mining-v2` 额外修复了缺失掩码下的 pairwise Spearman 口径,因此 correlation artifact 与 v1 不再要求数值相等。最终 run `10574abb5d8e4b32ade7cbdd23975c45` 峰值 RSS 为 1.350 GiB、总耗时 40.699 秒,相对原始对照基线内存下降 43.63%、耗时增加 8.94%;仍满足 1.5 GiB 内存门槛和统计增强耗时不超过 30% 的目标。v2 的 Float32 聚合与 Float64 慢参考具有相同 pair counts、空值位置和 0.75 剪枝判断,相关系数最大绝对误差为 `1.70e-7`。
这些结果只证明上述基准工作负载达到目标,不表示所有数据规模和搜索预算都具有固定内存上界。当前验证结果:后端全量测试 `876 passed, 24 warnings`,挖掘相关回归含基准轨、跨折证据与晋级门槛共 145 项通过,前端 TypeScript 检查和生产构建均通过;另以真实数据完成 exploratory 运行核验 `selected`/`cross`/`benchmark` 三类行与发布禁用。