fix(backtest): 财务因子矩阵路径不再沿用上一期的过期值

矩阵侧逐列前向填充时跳过空值写入, 新一期财报缺某指标就继续沿用上一期,
同一行会混用两期报告 (pb 取新期净资产、roe 停在上一期); polars 侧
join_asof 只认最新一期整行, 该指标为 null。同一份配置两条路径给出不同
因子值。改为空值同样覆盖为 NaN, 与 attach_fundamental_factors 口径一致。
This commit is contained in:
kevin9327
2026-09-10 07:50:04 +09:00
parent d0a14b5c1b
commit 03f8966856
2 changed files with 35 additions and 3 deletions
+5 -3
View File
@@ -181,9 +181,11 @@ def build_fundamental_matrices(
continue continue
for column, target in raw_columns.items(): for column, target in raw_columns.items():
value = snapshot[column][row_index] value = snapshot[column][row_index]
if value is None or not np.isfinite(float(value)): numeric = float("nan") if value is None else float(value)
continue # 新一期该指标为空时必须覆盖旧值为 NaN: 跳过写入会让同一行混用两期
target[start:, column_index] = float(value) # 报告 (bps 取新期、roe 停在上一期), 与 polars 侧 join_asof 只认
# 最新一期整行的口径不一致。
target[start:, column_index] = numeric if np.isfinite(numeric) else np.nan
for name in requested: for name in requested:
spec = FUNDAMENTAL_FACTORS[name] spec = FUNDAMENTAL_FACTORS[name]
+30
View File
@@ -140,6 +140,36 @@ def test_matrix_field_matches_polars_attach_across_two_announcements():
np.testing.assert_allclose(actual, value, rtol=1e-6) np.testing.assert_allclose(actual, value, rtol=1e-6)
def test_matrix_field_clears_value_when_newer_report_lacks_metric():
"""新一期财报缺该指标时不得继续沿用上一期值, 否则同一行混用两期报告。
矩阵路径逐列前向填充, 若跳过空值写入, 4-16 起 pb 已换到新期 bps=6,
roe 却仍停在上一期的 20 —— polars 侧 join_asof 只认最新一期整行 (roe 为
null), 两条路径给出不同的因子值。
"""
panel = _daily_panel(date(2026, 4, 1), 20, ("600000.SH",))
snapshot = _snapshot_frame([
{"symbol": "600000.SH", "announce": "2026-04-05", "roe": 20.0, "bps": 4.0},
{"symbol": "600000.SH", "announce": "2026-04-15", "roe": None, "bps": 6.0},
])
attached = attach_fundamental_factors(panel, snapshot, ["roe_latest", "pb_latest"]).sort("date")
market = build_market_data_matrix(panel)
matrices = build_fundamental_matrices(market, snapshot, ["roe_latest", "pb_latest"])
column = market.symbols.index("600000.SH")
# polars 口径: 新公告生效 (4-16) 后 roe 无值, pb 走新一期 bps
assert attached["roe_latest"].to_list()[15:] == [None] * 5
assert all(value is not None for value in attached["pb_latest"].to_list()[15:])
for name in ("roe_latest", "pb_latest"):
for row_index, value in enumerate(attached[name].to_list()):
actual = matrices[name][row_index, column]
if value is None:
assert np.isnan(actual), (name, row_index, actual)
else:
np.testing.assert_allclose(actual, value, rtol=1e-6)
def test_bps_nonpositive_gives_null_pb(): def test_bps_nonpositive_gives_null_pb():
panel = _daily_panel(date(2026, 4, 1), 8, ("000001.SZ",)) panel = _daily_panel(date(2026, 4, 1), 8, ("000001.SZ",))
snapshot = _snapshot_frame([ snapshot = _snapshot_frame([