fix(artifact): enforce risk data lineage
This commit is contained in:
@@ -45,6 +45,8 @@
|
|||||||
- `prepare_asset_return_snapshot` 从规范化长表行情生成不前向填充的 simple daily returns;
|
- `prepare_asset_return_snapshot` 从规范化长表行情生成不前向填充的 simple daily returns;
|
||||||
显式 ingestion snapshot ID、源/字段/复权口径、价格值和缺失掩码共同形成
|
显式 ingestion snapshot ID、源/字段/复权口径、价格值和缺失掩码共同形成
|
||||||
`asset-returns-v1:<sha256>`,并把同一 ID 传给 covariance 与 run artifact。
|
`asset-returns-v1:<sha256>`,并把同一 ID 传给 covariance 与 run artifact。
|
||||||
|
- artifact builder fail closed:每个 `CovarianceSnapshot.data_snapshot_id` 必须与 run 级
|
||||||
|
`data_snapshot_id` 完全一致,禁止把其他行情快照的风险分解静默发布到当前研究运行。
|
||||||
- shrinkage 适配器本轮不实现:scikit-learn 尚非声明依赖,未来只允许薄适配
|
- shrinkage 适配器本轮不实现:scikit-learn 尚非声明依赖,未来只允许薄适配
|
||||||
`LedoitWolf` / `OAS`,不复制公式、不依赖环境偶然安装状态。
|
`LedoitWolf` / `OAS`,不复制公式、不依赖环境偶然安装状态。
|
||||||
|
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ def _risk_trade_date(value: object) -> date:
|
|||||||
def _build_risk(
|
def _build_risk(
|
||||||
result: FactorBacktestResult,
|
result: FactorBacktestResult,
|
||||||
run_id: str,
|
run_id: str,
|
||||||
|
data_snapshot_id: str,
|
||||||
risk_snapshots: Mapping[object, CovarianceSnapshot] | None,
|
risk_snapshots: Mapping[object, CovarianceSnapshot] | None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
if risk_snapshots is None:
|
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"covariance as_of_date {snapshot.as_of_date} must not be after trade date "
|
||||||
f"{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
|
normalized[trade_date] = snapshot
|
||||||
|
|
||||||
weights_by_date = result.position_weights
|
weights_by_date = result.position_weights
|
||||||
@@ -572,6 +575,6 @@ def build_research_run_artifact(
|
|||||||
_positions=_build_positions(result, normalized_run_id),
|
_positions=_build_positions(result, normalized_run_id),
|
||||||
_attribution=attribution,
|
_attribution=attribution,
|
||||||
_attribution_daily=attribution_daily,
|
_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),
|
_performance=_build_performance(result, normalized_run_id, benchmark_returns),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ def test_research_artifact_projects_annualized_risk_from_actual_positions() -> N
|
|||||||
covariance=covariance,
|
covariance=covariance,
|
||||||
return_frequency="1d",
|
return_frequency="1d",
|
||||||
periods_per_year=252,
|
periods_per_year=252,
|
||||||
|
data_snapshot_id="qtdb-pro-20260108-v1",
|
||||||
)
|
)
|
||||||
|
|
||||||
artifact = _build(result, risk_snapshots={trade_date: snapshot})
|
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)
|
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:
|
def test_research_artifact_rejects_future_or_misaligned_risk_snapshots() -> None:
|
||||||
result = _backtest_result()
|
result = _backtest_result()
|
||||||
trade_date = result.position_weights.index[-1].date()
|
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,
|
covariance=covariance,
|
||||||
return_frequency="1d",
|
return_frequency="1d",
|
||||||
periods_per_year=252,
|
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"]],
|
covariance=covariance.loc[["B"], ["B"]],
|
||||||
return_frequency="1d",
|
return_frequency="1d",
|
||||||
periods_per_year=252,
|
periods_per_year=252,
|
||||||
|
data_snapshot_id="qtdb-pro-20260108-v1",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user