Skip to content

Commit b1d0670

Browse files
committed
feat: implement automatic valuation history recording in batch processing
1 parent 389ce87 commit b1d0670

4 files changed

Lines changed: 67 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,14 @@ cb-sync-events --apply
185185
cb-screen-pool
186186

187187
# ⑥ 打开 GUI 做批量复核、单债钻取和敏感性分析
188+
# (批量重算成功后会自动把当期估值快照记入历史基线)
188189
cb-gui
190+
191+
# ⑦ 查看转债大类估值/择时信号 (全市场中位偏差 + 历史分位; --record 手动入基线)
192+
cb-valuation
193+
194+
# ⑧ 策略回测 CLI (与 GUI 策略页等价; --cache-dir 复跑提速, 详见 --help)
195+
cb-strategy-backtest --start 2025-01-01 --end 2026-01-01 --freq M --cash-yield 0.022
189196
```
190197

191198
---

convertible_bond/gui/tabs/batch.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66
from __future__ import annotations
77

8+
import logging
89
import threading
910
import tkinter as tk
1011
from datetime import date
@@ -32,7 +33,12 @@
3233
write_batch_results_csv,
3334
)
3435
from ...pricing_api import batch_price_from_provider_threaded
35-
from ...market_valuation import load_history, valuation_banner
36+
from ...market_valuation import (
37+
append_history,
38+
compute_snapshot,
39+
load_history,
40+
valuation_banner,
41+
)
3642
from ...paths import data_path
3743
from ...watchlist import load_watchlist
3844
from ..widgets import Tooltip
@@ -60,6 +66,8 @@
6066
if TYPE_CHECKING:
6167
from ..app import CBPricerApp
6268

69+
logger = logging.getLogger(__name__)
70+
6371
# 列预设: 简洁视图只保留投资决策最常看的字段, 完整视图沿用所有字段
6472
# 状态列: 成功 → ✓ (单字符即可), 失败行保留错误文本, 故宽度大幅收窄
6573
_BATCH_COLS_FULL = (
@@ -413,6 +421,9 @@ def on_progress(done, total):
413421
params=params,
414422
upcoming_results=watchlist_pricing,
415423
)
424+
# 样本外纪律: 全市场重算成功即自动记录当期估值快照 (同估值日幂等覆盖)。
425+
# 仅主池重算触发; 缓存加载/关注池局部重算不记, 避免污染基线。
426+
_record_valuation_history(results)
416427
app._batch_results = results
417428
app._batch_upcoming_results = watchlist_pricing
418429
app._last_batch_source = provider.name
@@ -528,6 +539,21 @@ def _render_batch_views(
528539
_render_watchlist_table(app)
529540

530541

542+
def _record_valuation_history(results, history_path=None) -> bool:
543+
"""全市场重算成功后把当期估值快照并入历史基线 (样本外纪律的自动化形态)。
544+
545+
同估值日幂等覆盖; 失败静默 (记录是副产品, 不能影响主流程)。返回是否成功记录。
546+
"""
547+
try:
548+
snapshot = compute_snapshot(results or [])
549+
path = history_path or data_path("cb_valuation_history.json", seed=True)
550+
append_history(path, snapshot)
551+
return True
552+
except Exception:
553+
logger.debug("估值历史自动记录失败 (忽略)", exc_info=True)
554+
return False
555+
556+
531557
def _update_valuation_banner(app, base_results) -> None:
532558
"""用全量定价池更新标题栏的转债大类估值/择时信号 (静默失败不影响主流程)。"""
533559
if not hasattr(app, "v_batch_valuation"):

docs/research/2026-06-score-ic-and-valuation-timing.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,22 @@ pool+max_holdings 截断改按余额降序 (分数不再从截断后门回流)
126126
17 个季度的 Sharpe 差异永远够不到该门槛。
127127
(本轮曾基于单一牛市证据把 GUI 默认改为 pool, 4 年复测后回退 — 引以为戒。)
128128

129+
## 样本外预注册 (2026-06-11 固定, 不得事后修改)
130+
131+
记录机制: GUI 批量重算成功后自动把当期估值快照记入 `data/cb_valuation_history.json`
132+
(同估值日幂等覆盖); 亦可 `cb-valuation --record` 手动入基线。
133+
134+
**复评时点**: 2026-09-30、2026-12-31 (季度末后一周内执行)。
135+
136+
**复评内容与成功标准 (先于数据写死)**:
137+
1. 择时信号: 把新季度追加进序列重算 corr(中位偏差, 指数下一季收益) —
138+
**维持负号**视为信号存活; 翻正两个季度则降级信号并撤估值横幅的择时表述。
139+
2. 择时叠加: 按预承诺映射 (k=2.5, floor=0.5, 锚 0%/+20%) 计算本季应有 gross,
140+
对照实际市场路径核算叠加的纸面表现 — **不允许修改映射参数**;
141+
连续四个季度纸面落后同组合满仓基线的风险调整后指标则放弃该叠加。
142+
3. 横截面: 新季度 deviation Rank-IC 追加 — 若稳健转正 (连续 ≥3 季 >+0.05)
143+
才重新讨论机会分参与排序的可能性。
144+
129145
## 局限
130146

131147
- 样本: 17 个季度、单一市场、约一个完整估值周期; 所有 t 值仅作方向参考。

tests/test_market_valuation.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,23 @@ def test_load_history_missing_returns_empty(tmp_path):
115115
assert load_history(tmp_path / "nope.json") == []
116116

117117

118+
def test_gui_auto_record_helper_idempotent(tmp_path):
119+
"""批量页自动记录: 成功落盘、同估值日幂等覆盖、空结果静默失败。"""
120+
from convertible_bond.gui.tabs.batch import _record_valuation_history
121+
path = tmp_path / "hist.json"
122+
assert _record_valuation_history(_rows([0.10, 0.15, 0.20]), history_path=path) is True
123+
loaded = load_history(path)
124+
assert len(loaded) == 1
125+
assert loaded[0].median_deviation == pytest.approx(0.15)
126+
# 同日重算 → 覆盖而非追加
127+
assert _record_valuation_history(_rows([0.30, 0.30, 0.30]), history_path=path) is True
128+
loaded = load_history(path)
129+
assert len(loaded) == 1
130+
assert loaded[0].median_deviation == pytest.approx(0.30)
131+
# 空结果 → 静默失败不写盘
132+
assert _record_valuation_history([], history_path=path) is False
133+
134+
118135
# ---------------- valuation_banner (GUI 横幅) ----------------
119136

120137
def test_valuation_banner_rich():

0 commit comments

Comments
 (0)