/~learn/s/cf18b539
top_n 参数传递错误,修复后“连续 n 周在榜”计数准确、“下周操作清单”不再每次显示“首次建仓”。同时新增了本地 SQLite 数据库自动存储每周 Top10/Bottom10 完整快照,方便日后复盘查询。
根因: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 的口径一致。
文件变更:
top_n=top_n 改为 top_n=10影响:修复后首次运行时,不在 Top10 内的品种 streak 会归零,随后正常累积。
根因:SignalTracker._is_rotation_return() 依赖 _history 判断回榜品种,但 to_dict() 和 _load() 均未序列化 _history。脚本重启后 _history 为空,_is_rotation_return() 永远返回 False,导致所有第一次上榜的品种都显示“新晋”而非“回榜”。
修复:
文件变更:
to_dict() 新增 'history' 键_load() 新增 _history 还原
根因:_last_portfolio 是 ETFSelectionEngine 的运行时变量,脚本重启后为 None,_generate_action_plan() 中 old_holdings 始终为空,永远输出“首次建仓”。
修复:新增 _save_portfolio() / _load_portfolio() 方法,使用 last_portfolio_db.json 跨脚本持久化上次持仓组合。
文件变更:
PORTFOLIO_DB_FILE 常量__init__ 中调用 _load_portfolio()_save_portfolio() / _load_portfolio()generate_push_message() 末尾调用 _save_portfolio()
持久化文件:adata/aohan/quant/selection/last_portfolio_db.json
系统使用单一 SQLite 文件 quant.db 管理所有历史统计与快照数据,由 adata/aohan/quant/db/ 模块统一管理。
adata/aohan/quant/db/quant.dbcore.py — 连接管理、表初始化注册{模块}_{实体}_{粒度} 格式(如 cross_section_weekly_snapshot)quant/db/ 下新建模块文件,注册到 init_all_tables()
表名: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) — 同一日期同一品种只存一条。
adata/aohan/quant/db/core.py — 全局 DB 连接与表初始化adata/aohan/quant/db/cross_section.py — cross_section_weekly_snapshot 表 schema + CRUDadata/aohan/quant/db/quant.dbgenerate_push_message() 末尾自动调用 save_weekly_snapshot()
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 数据包含历史错误累积值。首次运行修复后的代码时:
_prev_top_n 中,streak +1 继续累积无需手动清理 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/ 统一管理 |
现象:
-
根因 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 全是权益资产。
现象:截面板块页面加载后,"最近交易日"和"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)。
现象:_format_snapshot() 在合并 top_df 和 bottom_df 时,pandas concat 因两表列不完全一致产生 FutureWarning,streak_weeks 等字段的 NaN 在 int() 转换时抛出 ValueError。
修复:_format_snapshot() 新增 _safe_int() / _safe_float() 安全转换,过滤 NaN/Inf。