fix(artifact): enforce risk data lineage

This commit is contained in:
ao gong
2026-08-25 22:39:45 +08:00
parent 7c7f0c06a7
commit c12c9ba335
3 changed files with 34 additions and 1 deletions
@@ -45,6 +45,8 @@
- `prepare_asset_return_snapshot` 从规范化长表行情生成不前向填充的 simple daily returns;
显式 ingestion snapshot ID、源/字段/复权口径、价格值和缺失掩码共同形成
`asset-returns-v1:<sha256>`,并把同一 ID 传给 covariance 与 run artifact。
- artifact builder fail closed:每个 `CovarianceSnapshot.data_snapshot_id` 必须与 run 级
`data_snapshot_id` 完全一致,禁止把其他行情快照的风险分解静默发布到当前研究运行。
- shrinkage 适配器本轮不实现:scikit-learn 尚非声明依赖,未来只允许薄适配
`LedoitWolf` / `OAS`,不复制公式、不依赖环境偶然安装状态。
+4 -1
View File
@@ -390,6 +390,7 @@ def _risk_trade_date(value: object) -> date:
def _build_risk(
result: FactorBacktestResult,
run_id: str,
data_snapshot_id: str,
risk_snapshots: Mapping[object, CovarianceSnapshot] | None,
) -> pd.DataFrame:
if risk_snapshots is None:
@@ -414,6 +415,8 @@ def _build_risk(
f"covariance as_of_date {snapshot.as_of_date} must not be after trade date "
f"{trade_date}"
)
if snapshot.data_snapshot_id != data_snapshot_id:
raise ValueError("covariance snapshot data lineage differs from research run")
normalized[trade_date] = snapshot
weights_by_date = result.position_weights
@@ -572,6 +575,6 @@ def build_research_run_artifact(
_positions=_build_positions(result, normalized_run_id),
_attribution=attribution,
_attribution_daily=attribution_daily,
_risk=_build_risk(result, normalized_run_id, risk_snapshots),
_risk=_build_risk(result, normalized_run_id, normalized_snapshot, risk_snapshots),
_performance=_build_performance(result, normalized_run_id, benchmark_returns),
)
+28
View File
@@ -164,6 +164,7 @@ def test_research_artifact_projects_annualized_risk_from_actual_positions() -> N
covariance=covariance,
return_frequency="1d",
periods_per_year=252,
data_snapshot_id="qtdb-pro-20260108-v1",
)
artifact = _build(result, risk_snapshots={trade_date: snapshot})
@@ -182,6 +183,31 @@ def test_research_artifact_projects_annualized_risk_from_actual_positions() -> N
assert risk["risk_contribution"].sum() == pytest.approx(1.0)
def test_research_artifact_rejects_risk_from_a_different_data_snapshot() -> None:
result = _backtest_result()
trade_date = result.position_weights.index[-1].date()
covariance = pd.DataFrame(
[[0.0001, 0.0], [0.0, 0.0004]],
index=["A", "B"],
columns=["A", "B"],
)
with pytest.raises(ValueError, match="data lineage differs"):
_build(
result,
risk_snapshots={
trade_date: CovarianceSnapshot(
snapshot_id="foreign-covariance",
as_of_date="2026-01-07",
covariance=covariance,
return_frequency="1d",
periods_per_year=252,
data_snapshot_id="different-market-snapshot",
)
},
)
def test_research_artifact_rejects_future_or_misaligned_risk_snapshots() -> None:
result = _backtest_result()
trade_date = result.position_weights.index[-1].date()
@@ -201,6 +227,7 @@ def test_research_artifact_rejects_future_or_misaligned_risk_snapshots() -> None
covariance=covariance,
return_frequency="1d",
periods_per_year=252,
data_snapshot_id="qtdb-pro-20260108-v1",
)
},
)
@@ -215,6 +242,7 @@ def test_research_artifact_rejects_future_or_misaligned_risk_snapshots() -> None
covariance=covariance.loc[["B"], ["B"]],
return_frequency="1d",
periods_per_year=252,
data_snapshot_id="qtdb-pro-20260108-v1",
)
},
)