fix(fundamentals): 换报告期公告当日不再丢失上一期财务因子值

attach_fundamental_factors 用公告日做 asof 回看再按「严格大于」门控:
日期正好等于新一期公告日时, join_asof 已匹配到「当天还不能用」的新记录,
门控随即置 null, 打断上一期的前向填充。矩阵路径 build_fundamental_matrices
按 searchsorted(side="right") 逐期覆盖, 公告当日保留上一期, 两条路径因此不一致
(其 docstring 声明与 attach_fundamental_factors 同口径)。

asof 键改用生效日 (公告日次日), 只命中已生效的报告期; 公告前仍为 null 的门控不变。
补 tests/test_fundamental_factors.py 的两条公告一致性用例。
This commit is contained in:
kevin9327
2026-09-09 07:19:09 +09:00
parent 9a4bdcd07d
commit 7a13713903
2 changed files with 30 additions and 4 deletions
+10 -3
View File
@@ -103,11 +103,18 @@ def attach_fundamental_factors(
columns = sorted(
{FUNDAMENTAL_FACTORS[name]["column"] for name in missing_columns}
)
right = snapshot.select(["symbol", "_announce", *columns]).sort(["symbol", "_announce"])
# asof 键取生效日 (公告日次日) 而非公告日: 直接用公告日回看会在换报告期的
# 公告当日取到「尚未生效」的新一期并被门控置 null, 打断上一期的前向填充,
# 与矩阵路径 (searchsorted side="right") 不一致。
right = (
snapshot.select(["symbol", "_announce", *columns])
.with_columns(pl.col("_announce").dt.offset_by("1d").alias("_effective"))
.sort(["symbol", "_effective"])
)
joined = panel.join_asof(
right,
left_on="date",
right_on="_announce",
right_on="_effective",
by="symbol",
strategy="backward",
check_sortedness=False, # 双侧均已按 (symbol, key) 排序, 免除逐组检查开销
@@ -128,7 +135,7 @@ def attach_fundamental_factors(
expressions.append(
pl.when(announced).then(value).otherwise(None).alias(name)
)
return joined.with_columns(expressions)
return joined.with_columns(expressions).drop("_effective")
def build_fundamental_matrices(
+20 -1
View File
@@ -85,7 +85,7 @@ def test_attach_replaces_with_newer_announcement():
assert roe[4] is None # 4-5 公告日
assert roe[5] == 20.0 # 4-6 起 20.0
assert roe[13] == 20.0 # 4-14
assert roe[14] is None # 4-15 二次公告日, 当天仍不可用 (严格大于)
assert roe[14] == 20.0 # 4-15 二次公告日: 新一期尚未生效, 仍保留上一期
assert roe[15] == 33.0 # 4-16 起新公告生效
@@ -121,6 +121,25 @@ def test_matrix_field_matches_polars_attach():
np.testing.assert_allclose(actual, expected, rtol=1e-6)
def test_matrix_field_matches_polars_attach_across_two_announcements():
"""换报告期时两条路径仍须一致: 新公告当日应保留上一期值(前向填充不断档)。"""
panel = _daily_panel(date(2026, 4, 1), 20, ("600000.SH",))
snapshot = _snapshot_frame([
{"symbol": "600000.SH", "announce": "2026-04-05", "roe": 20.0},
{"symbol": "600000.SH", "announce": "2026-04-15", "roe": 33.0},
])
attached = attach_fundamental_factors(panel, snapshot, ["roe_latest"]).sort("date")
market = build_market_data_matrix(panel)
matrix = build_fundamental_matrices(market, snapshot, ["roe_latest"])["roe_latest"]
column = market.symbols.index("600000.SH")
for row_index, value in enumerate(attached["roe_latest"].to_list()):
actual = matrix[row_index, column]
if value is None:
assert np.isnan(actual), (row_index, actual)
else:
np.testing.assert_allclose(actual, value, rtol=1e-6)
def test_bps_nonpositive_gives_null_pb():
panel = _daily_panel(date(2026, 4, 1), 8, ("000001.SZ",))
snapshot = _snapshot_frame([