mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 14:24:15 +08:00
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:
@@ -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(
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user