fix(provider): value ROE 配对改公共报告期交集治财报季 vintage 错位清仓——09-01 09:32 value_selection_ex 月度窗口 L3=0 全清仓实锤:roe_series 构建=income/balance 各自 latest-4 再硬 REPORT_DATE 匹配,财报季一表领先一季(balance 已带 2026 中报而 income 未刷新,ak-quarter 马拉松 09:32 时 94%/0% 撕裂)两窗错开一格只交 3 期→策略 len>=4 全体不过→821 只零候选→卖 600276/603288 共 1.23 万股趴 483k 现金;修=两表各取 2 倍缓冲→REPORT_DATE 公共交集回最近 4 季(同季重述取最新 notice),策略层 len>=4 契约不变,原版+_ex 同受益;+3 回归(领先一季仍满 4 期/领先季绝不跨季错配/对齐两表语义不变);538 绿 [vps]
CI/CD / test (push) Successful in 17s
CI/CD / nas-deploy (push) Successful in 11s
CI/CD / nas-verify (push) Successful in 12s

This commit is contained in:
2026-09-01 18:59:32 +08:00
parent 9f264f8bd0
commit 3b3f7d689c
2 changed files with 103 additions and 6 deletions
@@ -478,24 +478,34 @@ class LocalParquetProvider(DataProvider): # type: ignore[misc]
cur_ratio = ca / cl
# 近 4 季 ROE(PARENT_NETPROFIT / TOTAL_PARENT_EQUITY, 按报告期对齐)
# 2026-09-01 修(策略01 清仓事故):原「两表各自 latest-4 再硬匹配」对
# vintage 错位零容忍——财报季一表领先一季(balance 已带中报而 income
# 未刷新)时两窗错开一格只交 3 期 → 策略 len>=4 全体不过 → L3=0 全清仓。
# 改为两表各取 2 倍缓冲,按 REPORT_DATE 公共交集回最近 4 季。
roe_series: List[float] = []
if not income_df.empty and not balance_df.empty:
inc_rows = self._latest_n_published(income_df, date_str, 4)
bal_rows = self._latest_n_published(balance_df, date_str, 4)
_roe_n = 4
inc_rows = self._latest_n_published(income_df, date_str, _roe_n * 2)
bal_rows = self._latest_n_published(balance_df, date_str, _roe_n * 2)
bal_by_rdate: dict = {}
for b in bal_rows:
rd = b.get("REPORT_DATE")
if rd is not None:
bal_by_rdate.setdefault(rd, b) # 最新notice优先(同季重述取最新)
for inc_row in inc_rows:
rdate = inc_row.get("REPORT_DATE")
if rdate is None:
continue
# 按报告期对齐: 找同 REPORT_DATE 的 balance 行
bal_match = next(
(b for b in bal_rows if b.get("REPORT_DATE") == rdate), None,
)
# 按报告期对齐: 找同 REPORT_DATE 的 balance 行(仅公共季入选)
bal_match = bal_by_rdate.get(rdate)
if bal_match is None:
continue
np_ = _to_float(inc_row.get("PARENT_NETPROFIT"))
eq = _to_float(bal_match.get("TOTAL_PARENT_EQUITY"))
if np_ is not None and eq and eq != 0:
roe_series.append(np_ / eq)
if len(roe_series) >= _roe_n:
break
# 近 4 季营收同比(OPERATE_INCOME_YOY, 百分数) + 净利润同比(PARENT_NETPROFIT_YOY, 百分数)
yoy_series: List[float] = []
@@ -713,6 +713,93 @@ class TestGetValueMetricsBatch:
), code
class TestValueMetricsRoeQuarterAlignment:
"""2026-09-01 value_selection 清仓事故回归(策略侧根因):roe_series 需
income×balance 同 REPORT_DATE 配对满 4 期,原实现两表**各自**取最新 4 期再
硬匹配——财报季一表领先一季(balance 已带中报而 income 未刷新,09-01 实测
vintage 撕裂)时两窗错开一格只交出 3 期 → 策略 len>=4 全体不过 → L3=0 →
月度窗口全清仓。修=两表各取缓冲后按**公共报告期交集**回最近 4 季。"""
@staticmethod
def _write_income(tmp_path, periods):
"""periods: (report_date, notice_date, parent_netprofit) 升序。"""
d = tmp_path / "static" / "income"
d.mkdir(parents=True, exist_ok=True)
pd.DataFrame({
"SECUCODE": ["600519.SH"] * len(periods),
"REPORT_DATE": [p[0] for p in periods],
"NOTICE_DATE": [p[1] for p in periods],
"REPORT_TYPE": ["季报"] * len(periods),
"PARENT_NETPROFIT": [p[2] for p in periods],
"OPERATE_INCOME_YOY": [10.0] * len(periods),
"PARENT_NETPROFIT_YOY": [10.0] * len(periods),
}).to_parquet(d / "600519.SH_income.parquet", index=False)
@staticmethod
def _write_balance(tmp_path, periods):
"""periods: (report_date, notice_date, total_parent_equity) 升序。"""
d = tmp_path / "static" / "balance"
d.mkdir(parents=True, exist_ok=True)
pd.DataFrame({
"SECUCODE": ["600519.SH"] * len(periods),
"REPORT_DATE": [p[0] for p in periods],
"NOTICE_DATE": [p[1] for p in periods],
"REPORT_TYPE": ["季报"] * len(periods),
"TOTAL_PARENT_EQUITY": [p[2] for p in periods],
}).to_parquet(d / "600519.SH_balance.parquet", index=False)
_INCOME_5Q = [ # 停在 2025-12-31(未带 26Q1 = 滞后一季)
("2024-12-31", "2025-03-26", 1e9),
("2025-03-31", "2025-04-25", 2e9),
("2025-06-30", "2025-08-22", 3e9),
("2025-09-30", "2025-10-28", 4e9),
("2025-12-31", "2026-03-26", 5e9),
]
_BALANCE_6Q = [ # 多带 2026-03-31(= 领先一季),权益逐季翻倍使错配可测
("2024-12-31", "2025-03-26", 1.0e11),
("2025-03-31", "2025-04-25", 2.0e11),
("2025-06-30", "2025-08-22", 4.0e11),
("2025-09-30", "2025-10-28", 8.0e11),
("2025-12-31", "2026-03-26", 1.6e12),
("2026-03-31", "2026-04-24", 3.2e12),
]
def test_one_table_leads_a_quarter_still_full_4(self, tmp_path):
"""balance 领先一季:roe_series 仍须满 4 期,且按公共季对齐(25Q4..Q1),
不含领先季。旧实现两窗错位只交 3 期(09-01 L3=0 形态)。"""
self._write_income(tmp_path, self._INCOME_5Q)
self._write_balance(tmp_path, self._BALANCE_6Q)
p = LocalUnifiedProvider({"data_dir": str(tmp_path)})
m = p.get_value_metrics("600519.XSHG", "2026-06-01")
assert m is not None
assert len(m["roe_series"]) == 4
exp = [5e9 / 1.6e12, 4e9 / 8e11, 3e9 / 4e11, 2e9 / 2e11]
assert m["roe_series"] == pytest.approx(exp)
def test_leading_quarter_not_mispaired(self, tmp_path):
"""领先季(26Q1)绝不与公共季错配:任何元素都不等于
25Q4 净利/26Q1 权益 等跨季错配值。"""
self._write_income(tmp_path, self._INCOME_5Q)
self._write_balance(tmp_path, self._BALANCE_6Q)
p = LocalUnifiedProvider({"data_dir": str(tmp_path)})
m = p.get_value_metrics("600519.XSHG", "2026-06-01")
assert m is not None
wrong = {5e9 / 3.2e12, 4e9 / 1.6e12, 3e9 / 8e11}
for v in m["roe_series"]:
assert v not in wrong
def test_aligned_tables_semantics_unchanged(self, tmp_path):
"""两表对齐(同 5 期):仍回最近 4 期公共季,语义与修前一致。"""
self._write_income(tmp_path, self._INCOME_5Q)
self._write_balance(tmp_path, self._BALANCE_6Q[:-1]) # 去掉领先季
p = LocalUnifiedProvider({"data_dir": str(tmp_path)})
m = p.get_value_metrics("600519.XSHG", "2026-06-01")
assert m is not None
assert len(m["roe_series"]) == 4
exp = [5e9 / 1.6e12, 4e9 / 8e11, 3e9 / 4e11, 2e9 / 2e11]
assert m["roe_series"] == pytest.approx(exp)
# ======================== Task 3d: get_limit_status_batch (涨跌停/停牌回测修正) ========================
def _make_limit_fixture(tmp_path):
"""dbbardata 2 日线(T-1=06-19, T=06-20)覆盖涨停/跌停/停牌/创业板20%/正常/缺失。