← 文档库
📄 文档learn/量化工程优化/截面选基-本地数据库与Bug修复.md
🌱 创建于2026-06-27 01:04
✏️ 修改于2026-06-29 22:14
👁️ 浏览量—
🔗 短链接/~learn/s/cf18b539
🔐 可见性

截面选基 — 本地数据库与 Bug 修复

文档目标

本文记录截面选基引擎中发现的三个 bug 的根因分析与修复方案,以及新增的本地 SQLite 历史快照存储能力。

一句话结论

三个 bug 均源自数据未正确持久化和 top_n 参数传递错误,修复后“连续 n 周在榜”计数准确、“下周操作清单”不再每次显示“首次建仓”。同时新增了本地 SQLite 数据库自动存储每周 Top10/Bottom10 完整快照,方便日后复盘查询。

一、Bug 分析与修复

Bug 1: 连续 n 周在榜数据全部虚高

根因:generate_push_message() 以 top_n=99 调用 run_selection(),而 run_selection() 内部又将同一个 top_n 传给 signal_tracker.record()。由于 ETF 池约 25 只,top_n=99 意味着所有品种都在 top_set 中,所有品种的 streak 每周 +1,无一掉出榜单。

修复:engine.py 第 291 行 — signal_tracker.record() 始终以固定 top_n=10 追踪,与显示 Top10 的口径一致。

文件变更:

影响:修复后首次运行时,不在 Top10 内的品种 streak 会归零,随后正常累积。

Bug 2: _history 未持久化导致轮动检测失效

根因:SignalTracker._is_rotation_return() 依赖 _history 判断回榜品种,但 to_dict() 和 _load() 均未序列化 _history。脚本重启后 _history 为空,_is_rotation_return() 永远返回 False,导致所有第一次上榜的品种都显示“新晋”而非“回榜”。

修复:

文件变更:

Bug 3: 下周操作清单总是首次建仓

根因:_last_portfolio 是 ETFSelectionEngine 的运行时变量,脚本重启后为 None,_generate_action_plan() 中 old_holdings 始终为空,永远输出“首次建仓”。

修复:新增 _save_portfolio() / _load_portfolio() 方法,使用 last_portfolio_db.json 跨脚本持久化上次持仓组合。

文件变更:

持久化文件:adata/aohan/quant/selection/last_portfolio_db.json

二、全局统一数据库 (quant.db)

设计原则

系统使用单一 SQLite 文件 quant.db 管理所有历史统计与快照数据,由 adata/aohan/quant/db/ 模块统一管理。

截面选基周快照表

表名:cross_section_weekly_snapshot

每运行一次截面选基引擎,自动将当周 Top10 和 Bottom10 的完整快照写入此表,支持日后:

数据库表结构

表名:cross_section_weekly_snapshot

字段 类型 说明
------------------
id INTEGER 自增主键
snapshot_date TEXT 截面日期
rank INTEGER 在 Top10 或 Bottom10 中的排名
code TEXT ETF 代码
name TEXT ETF 名称
score REAL 综合评分
category TEXT 类别
sub_category TEXT 子类
is_top INTEGER 1=Top10, 0=Bottom10
streak_weeks INTEGER 连续在榜周数(仅 Top10)
streak_icon TEXT 信号图标(仅 Top10)
streak_desc TEXT 信号描述(仅 Top10)
factor_momentum REAL 动量因子得分
factor_sharpe REAL 夏普因子得分
factor_overbought REAL 反追高因子得分
factor_vol_stability REAL 波动率稳定性因子得分
regime TEXT 市场环境
exposure_cap REAL 仓位上限
created_at TEXT 记录创建时间

唯一约束:UNIQUE(snapshot_date, code) — 同一日期同一品种只存一条。

文件位置

API 参考

from adata.aohan.quant.db.cross_section import (
    save_weekly_snapshot, get_weekly_history, get_top_appearances,
    get_date_summary, get_available_dates
)

# 查询近90天历史
df = get_weekly_history(days=90)

# 统计近26周 Top10 上榜频率
top_df = get_top_appearances(weeks=26)

# 查询某日完整快照
summary = get_date_summary('2026-05-30')
# summary['top'] → Top10 DataFrame
# summary['bottom'] → Bottom10 DataFrame

# 列出所有已记录的日期
dates = get_available_dates()

日常使用中无需手动调用 — generate_push_message() 会自动写入。

三、数据自愈说明

修复 Bug1 后,现有 signal_streaks_db.json 中的 streak 数据包含历史错误累积值。首次运行修复后的代码时:

无需手动清理 JSON 文件。

四、文件变更汇总

文件 变更类型 说明
---------------------
engine.py 修改 Bug1/3 修复 + SQLite 集成
signal_tracker.py 修改 Bug2 修复 (_history 持久化)
core.py 新增 全局 DB 连接管理、表初始化注册
cross_section.py 新增 截面周快照表 schema + CRUD
db/__init__.py 新增 包导出
CLAUDE.md 修改 新增全局 DB 规范章节
selection/db/ (旧) 删除 迁移到 quant/db/ 统一管理

五、最新修复记录(2026-06-28)

Bug 4:快照口径不一致导致网站排名对比全乱(城投债冲顶 + Bottom 因子为空)

现象:

根因 4a:run_selection() 经过分散化过滤器(同指数去重+相关性剔除+类目限数)后,107 只候选只剩 ~18 只进入 result_df。保存快照时对 result_df.head(20) 和 result_df.tail(10) 严重重叠(总共才 18 行),且 Bottom 的 _format_snapshot() 只保留了 6 个基础字段,丢掉了所有因子/streak/排名变动字段。

根因 4b:回补脚本 backfill_cross_section.py 创建引擎时没传 etf_pool,导致 selector.etf_pool = None。防御类软惩罚代码检查 self.etf_pool is None 时直接跳过,所有债券×0.82/REITs×0.88/商品×0.95 都没触发。城投债未经惩罚自然分数畸高冲到第一。

修复:

文件 变更
------------
engine.py run_selection() 新增 _last_full_ranked 保存全量排名(过滤前);__init__ 自动注入 etf_pool 到 selector 及其所有因子
cross_section.py save_weekly_snapshot() 开头先 DELETE 同日旧记录
unified_server.py _format_snapshot() 重写:合并 top/bottom 后全局排序取前20/后10;Bottom 表格复用 Top 渲染全套因子/排名变化/因子Δ
backfill_cross_section.py 显式创建 ETFPool 并传入 selector,确保防御惩罚生效
DB 清空全部历史快照,用修复后脚本重新回补过去1年所有周五(44期)

影响:网站排名对比口径一致(全为全市场107只完整排序),城投债从第1掉到第34,Top5 全是权益资产。

Bug 5:网站 batch K 线接口太慢(18秒)+ 返回未来假数据

现象:截面板块页面加载后,"最近交易日"和"7日趋势"两列一直显示 ···,加载不出来。

根因 5a:batch kline 接口走的是 _fetch_data_batch()(实时拉 tushare/adata),43 只 ETF × 6 并发耗时 18 秒,浏览器等不到返回。

根因 5b:tushare 在周六访问时返回下周一(2026-06-29)的占位假行(OHLC 全一样,change_pct=0),被当作最新日展示。

修复:

文件 变更
------------
unified_server.py 新增 _load_kline_df_from_cache():优先读本地 CSV 缓存(DataCacheManager.load_best,future_days=0),缓存 miss 才降级到实时拉取
unified_server.py 新增 _df_to_kline_payload():过滤 trade_date > today 的未来行 + 自动补算 change_pct + NaN/Inf 安全

影响:43 只 ETF 7日 K 线接口从 18 秒降到 0.8 秒,未来日期 0 条,最新交易日为真实最近日(2026-06-25)。

Bug 6:横截面数据污染

现象:_format_snapshot() 在合并 top_df 和 bottom_df 时,pandas concat 因两表列不完全一致产生 FutureWarning,streak_weeks 等字段的 NaN 在 int() 转换时抛出 ValueError。

修复:_format_snapshot() 新增 _safe_int() / _safe_float() 安全转换,过滤 NaN/Inf。

▶ 📜 变更历史(2 次提交)

5e412fa2026-06-29 22:14fix: 缓存选择逻辑修复 + 数据跨度不足告警
4616ca62026-06-27 01:04docs: 同步 learn/ 文档 & 工作区改动
🍃quant 知识库
by 伊甸园和枫

🔊 播放设置

勾选音源后调节配比,总和自动归一化
已选 0 个 · 合计:0%
合成中...
00:00/00:00
1.0x

分享图片

生成中...