This commit is contained in:
+6
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"module_id": "quant_engine",
|
"module_id": "quant_engine",
|
||||||
"authority": {"scope": "module_metadata", "subject": "quant_engine", "owner": "quant-engine-owner", "source": "MODULE_SPEC.yaml", "revision": 3, "effective_from": "2026-09-01T00:00:00+08:00"},
|
"authority": {"scope": "module_metadata", "subject": "quant_engine", "owner": "quant-engine-owner", "source": "MODULE_SPEC.yaml", "revision": 4, "effective_from": "2026-09-01T00:00:00+08:00"},
|
||||||
"repository": {"name": "quant_engine", "workspace_id": "researchhub", "type": "research_engine", "maturity": "operational"},
|
"repository": {"name": "quant_engine", "workspace_id": "researchhub", "type": "research_engine", "maturity": "operational"},
|
||||||
"bounded_context": {
|
"bounded_context": {
|
||||||
"domain": "quantitative-research-engine",
|
"domain": "quantitative-research-engine",
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"Submitting live orders, routing trades, managing brokerage accounts, or claiming transaction execution",
|
"Submitting live orders, routing trades, managing brokerage accounts, or claiming transaction execution",
|
||||||
"Owning market-data source facts, research-result publication, or platform presentation state",
|
"Owning market-data source facts, research-result publication, or platform presentation state",
|
||||||
"Loading provider credentials, brokerage credentials, or production secrets",
|
"Loading provider credentials, brokerage credentials, or production secrets",
|
||||||
|
"Granting portfolio approval, maker-checker decisions, publication eligibility, paper execution, or live execution authority",
|
||||||
"Changing financial model semantics through module metadata"
|
"Changing financial model semantics through module metadata"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
{"id": "execution-simulation", "summary": "Simulate costs, slippage, market constraints, fills, NAV, and PnL without live order routing.", "status": "operational"},
|
{"id": "execution-simulation", "summary": "Simulate costs, slippage, market constraints, fills, NAV, and PnL without live order routing.", "status": "operational"},
|
||||||
{"id": "portfolio-backtesting", "summary": "Run weight-based backtests and benchmark comparisons.", "status": "operational"},
|
{"id": "portfolio-backtesting", "summary": "Run weight-based backtests and benchmark comparisons.", "status": "operational"},
|
||||||
{"id": "backtest-evidence-contracts", "summary": "Identify governed offline backtest inputs and close existing research artifact evidence without persistence or decision authority.", "status": "operational"},
|
{"id": "backtest-evidence-contracts", "summary": "Identify governed offline backtest inputs and close existing research artifact evidence without persistence or decision authority.", "status": "operational"},
|
||||||
|
{"id": "portfolio-risk-computation-contracts", "summary": "Verify deterministic portfolio-computation receipts and expose S3-bound portfolio decisions and risk assessments without adding algorithms or execution authority.", "status": "operational"},
|
||||||
{"id": "risk-and-performance-analysis", "summary": "Calculate portfolio decomposition, risk contribution, and performance statistics.", "status": "operational"}
|
{"id": "risk-and-performance-analysis", "summary": "Calculate portfolio decomposition, risk contribution, and performance statistics.", "status": "operational"}
|
||||||
],
|
],
|
||||||
"data": {"owns": [
|
"data": {"owns": [
|
||||||
@@ -30,7 +32,9 @@
|
|||||||
{"contract_id": "researchhub.factor-definition", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/factor_contracts.py"},
|
{"contract_id": "researchhub.factor-definition", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/factor_contracts.py"},
|
||||||
{"contract_id": "researchhub.factor-set-ref", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/factor_contracts.py"},
|
{"contract_id": "researchhub.factor-set-ref", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/factor_contracts.py"},
|
||||||
{"contract_id": "researchhub.backtest-run-ref", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/governed_pipeline.py"},
|
{"contract_id": "researchhub.backtest-run-ref", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/governed_pipeline.py"},
|
||||||
{"contract_id": "researchhub.backtest-evidence-manifest", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/artifact.py"}
|
{"contract_id": "researchhub.backtest-evidence-manifest", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/artifact.py"},
|
||||||
|
{"contract_id": "researchhub.portfolio-decision", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/portfolio_risk_contracts.py"},
|
||||||
|
{"contract_id": "researchhub.risk-assessment", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/portfolio_risk_contracts.py"}
|
||||||
],
|
],
|
||||||
"consumes": [
|
"consumes": [
|
||||||
{"contract_id": "researchhub.dataset-snapshot", "version": "1.0.0", "authority": "researchhub.data", "admission": "qualified_immutable_envelope"},
|
{"contract_id": "researchhub.dataset-snapshot", "version": "1.0.0", "authority": "researchhub.data", "admission": "qualified_immutable_envelope"},
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
- `research_pipeline` — 因子日 → 下一真实交易日 → 显式执行价 → 日末估值 → 成本后绩效(防前视编排)
|
- `research_pipeline` — 因子日 → 下一真实交易日 → 显式执行价 → 日末估值 → 成本后绩效(防前视编排)
|
||||||
- `governed_pipeline` — 数据快照 → 因子版本 → 策略版本 → 回测运行 → 目标组合 → 风险决策 → Paper 订单意图;同时拥有输入/配置/重放血缘决定的 `BacktestRunRef`
|
- `governed_pipeline` — 数据快照 → 因子版本 → 策略版本 → 回测运行 → 目标组合 → 风险决策 → Paper 订单意图;同时拥有输入/配置/重放血缘决定的 `BacktestRunRef`
|
||||||
- `artifact` — 版本化、确定性、存储中立的完整 research run 事实表,以及只映射现有表的 `BacktestEvidenceManifest`
|
- `artifact` — 版本化、确定性、存储中立的完整 research run 事实表,以及只映射现有表的 `BacktestEvidenceManifest`
|
||||||
|
- `portfolio_risk_contracts` — S3 证据闭合的 `PortfolioDecision` / `RiskAssessment` v1;独立复核 freshness、约束与 computation receipt,并复用既有标签安全风险分解
|
||||||
- `attribution` — 基于实际成交后持仓的隔夜 / 日内 / 交易成本逐日收益归因与闭合审计
|
- `attribution` — 基于实际成交后持仓的隔夜 / 日内 / 交易成本逐日收益归因与闭合审计
|
||||||
- `metrics` — 绝对绩效 + 严格日期对齐的 TE / IR / alpha / beta 基准相对绩效
|
- `metrics` — 绝对绩效 + 严格日期对齐的 TE / IR / alpha / beta 基准相对绩效
|
||||||
- `factor_library` — 通用方法(turnover / winsorize / IC / OLS / jb_test)
|
- `factor_library` — 通用方法(turnover / winsorize / IC / OLS / jb_test)
|
||||||
@@ -259,6 +260,111 @@ digest 等价,也不会把旧 run 静默升级为新合同。
|
|||||||
`LEGACY_EXPLORATORY`;不能隐式提升为 `CONTRACT_QUALIFIED`。所有资格均只描述离线证据闭合,
|
`LEGACY_EXPLORATORY`;不能隐式提升为 `CONTRACT_QUALIFIED`。所有资格均只描述离线证据闭合,
|
||||||
不表示投资有效、组合获批、Paper、生产或实盘就绪。
|
不表示投资有效、组合获批、Paper、生产或实盘就绪。
|
||||||
|
|
||||||
|
## 组合决策与风险评估合同 v1
|
||||||
|
|
||||||
|
`quant_engine.portfolio_risk_contracts` 是现有计算 owner 外围的薄合同层。创建
|
||||||
|
`PortfolioDecision` 必须同时提供完整 `BacktestRunRef`、嵌入同一 RunRef 的
|
||||||
|
`CONTRACT_QUALIFIED` 非 legacy `BacktestEvidenceManifest`、现有 `PortfolioTarget`、
|
||||||
|
`FreshnessPolicy`、`ConstraintSetV1` 与 `ComputationReceipt`。适配器会从权威输入独立重算
|
||||||
|
receipt 的 input/constraint/output digest、敞口、持仓数和 L1 turnover 残差;receipt 自报
|
||||||
|
成功、fallback 或放宽 tolerance 均不能替代复核。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from quant_engine.portfolio_risk_contracts import (
|
||||||
|
ComputationReceipt,
|
||||||
|
ConstraintSetV1,
|
||||||
|
FreshnessPolicy,
|
||||||
|
assess_portfolio_risk,
|
||||||
|
build_portfolio_decision,
|
||||||
|
compute_portfolio_receipt_digests,
|
||||||
|
)
|
||||||
|
|
||||||
|
freshness = FreshnessPolicy(
|
||||||
|
max_manifest_age_seconds=3600,
|
||||||
|
max_covariance_age_days=5,
|
||||||
|
)
|
||||||
|
constraints = ConstraintSetV1(
|
||||||
|
gross_exposure_max=1.0,
|
||||||
|
single_asset_max=0.10,
|
||||||
|
position_count_max=20,
|
||||||
|
turnover_max=0.30,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 生产者先形成公开 canonical digest;decision 构建时仍会独立重算。
|
||||||
|
expected = compute_portfolio_receipt_digests(
|
||||||
|
backtest_run_ref=run_ref,
|
||||||
|
manifest=evidence_manifest,
|
||||||
|
target=portfolio_target,
|
||||||
|
objective_name="long_only_allocation",
|
||||||
|
objective_version="1.0.0",
|
||||||
|
objective_digest=objective_digest,
|
||||||
|
model_name="factor_weighting",
|
||||||
|
model_version="1.0.0",
|
||||||
|
model_digest=model_digest,
|
||||||
|
expected_return_digest=expected_return_digest,
|
||||||
|
covariance_digest=covariance_digest,
|
||||||
|
scenario_digest=scenario_digest,
|
||||||
|
constraints=constraints,
|
||||||
|
freshness_policy=freshness,
|
||||||
|
prior_weights=prior_weights,
|
||||||
|
)
|
||||||
|
|
||||||
|
receipt = ComputationReceipt(
|
||||||
|
algorithm="factor_weighting",
|
||||||
|
algorithm_version="1.0.0",
|
||||||
|
implementation_digest=implementation_digest,
|
||||||
|
parameter_digest=parameter_digest,
|
||||||
|
input_digest=expected["input_digest"],
|
||||||
|
constraint_digest=expected["constraint_digest"],
|
||||||
|
output_digest=expected["output_digest"],
|
||||||
|
status="completed",
|
||||||
|
solver_required=False,
|
||||||
|
solver_name=None,
|
||||||
|
solver_version=None,
|
||||||
|
solver_config_digest=None,
|
||||||
|
iterations=None,
|
||||||
|
objective_value=None,
|
||||||
|
max_constraint_residual=expected["max_constraint_residual"],
|
||||||
|
tolerance=1e-12,
|
||||||
|
computed_at=computed_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
decision = build_portfolio_decision(
|
||||||
|
backtest_run_ref=run_ref,
|
||||||
|
manifest=evidence_manifest,
|
||||||
|
target=portfolio_target,
|
||||||
|
objective_name="long_only_allocation",
|
||||||
|
objective_version="1.0.0",
|
||||||
|
objective_digest=objective_digest,
|
||||||
|
model_name="factor_weighting",
|
||||||
|
model_version="1.0.0",
|
||||||
|
model_digest=model_digest,
|
||||||
|
expected_return_digest=expected_return_digest,
|
||||||
|
covariance_digest=covariance_digest,
|
||||||
|
scenario_digest=scenario_digest,
|
||||||
|
constraints=constraints,
|
||||||
|
freshness_policy=freshness,
|
||||||
|
receipt=receipt,
|
||||||
|
computed_at=computed_at,
|
||||||
|
prior_weights=prior_weights,
|
||||||
|
)
|
||||||
|
|
||||||
|
assessment = assess_portfolio_risk(
|
||||||
|
portfolio_decision=decision,
|
||||||
|
backtest_run_ref=run_ref,
|
||||||
|
manifest=evidence_manifest,
|
||||||
|
covariance=covariance_snapshot,
|
||||||
|
risk_model_name="euler_volatility",
|
||||||
|
risk_model_version="1.0.0",
|
||||||
|
risk_model_digest=risk_model_digest,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`source_universe_digest` 保留 S3 研究 universe 身份,`portfolio_asset_set_digest` 只描述实际
|
||||||
|
目标资产标签;二者不会互相冒充成员证明。风险评估在任何数值计算前要求 covariance、target、
|
||||||
|
RunRef 的 dataset identity 三方一致,并且只调用一次现有 `labeled_component_risk()`。合同中的
|
||||||
|
`qualified` 仅表示 S4.1 计算证据闭合,不授予 maker-checker、发布、订单、Paper、生产或实盘权限。
|
||||||
|
|
||||||
## 治理垂直切片
|
## 治理垂直切片
|
||||||
|
|
||||||
`governed_pipeline` 不复制因子、回测、组合或执行算法,只编排现有能力并补充版本与风险契约。
|
`governed_pipeline` 不复制因子、回测、组合或执行算法,只编排现有能力并补充版本与风险契约。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"portfolio_decision": {
|
||||||
|
"computed_at": "2026-01-08T03:01:00Z",
|
||||||
|
"constraint_residuals": {
|
||||||
|
"gross_exposure_max": 0.0,
|
||||||
|
"net_exposure_max": 0.0,
|
||||||
|
"net_exposure_min": 0.0,
|
||||||
|
"position_count_max": 0.0,
|
||||||
|
"single_asset_max": 0.0,
|
||||||
|
"single_asset_min": 0.0,
|
||||||
|
"turnover_max": 0.0
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"gross_exposure_max": 1.0,
|
||||||
|
"net_exposure_max": 1.0,
|
||||||
|
"net_exposure_min": 1.0,
|
||||||
|
"position_count_max": 2,
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"single_asset_max": 0.7,
|
||||||
|
"single_asset_min": 0.2,
|
||||||
|
"turnover_max": 0.2
|
||||||
|
},
|
||||||
|
"contract_name": "researchhub.portfolio-decision",
|
||||||
|
"covariance_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"dataset_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
|
||||||
|
"decision_id": "rhportfoliodecisionv1:sha256:0e6e5ce2fa08de8006cc392610695a013327fc80645bfa4d7a908ad3f615bebd",
|
||||||
|
"effective_at": "2026-01-08T03:00:00Z",
|
||||||
|
"evidence_digest": "sha256:f3913894d032c389c64eef59058b3cbc694cb9cc14ce9cee2699f0068200b650",
|
||||||
|
"expected_return_digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
"freshness_policy": {
|
||||||
|
"max_covariance_age_days": 0,
|
||||||
|
"max_manifest_age_seconds": 3600,
|
||||||
|
"schema_version": "1.0.0"
|
||||||
|
},
|
||||||
|
"gross_exposure": 1.0,
|
||||||
|
"manifest_document_sha256": "fbf54218f770528978f9ccd35577e1ab00877143ea397a576e071e75f0afbab0",
|
||||||
|
"manifest_id": "rhbacktestevidencev1:sha256:681c49cbdfb3e221b273e7bc616602ad80a7ab01206970807ad4294b176ebc75",
|
||||||
|
"model_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||||
|
"model_name": "deterministic_weights",
|
||||||
|
"model_version": "1.0.0",
|
||||||
|
"net_exposure": 1.0,
|
||||||
|
"objective_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
"objective_name": "long_only_allocation",
|
||||||
|
"objective_version": "1.0.0",
|
||||||
|
"output_digest": "sha256:bd3b964c628c8648322d036e57dd6f444ca287017d1578bab3689d07d32b28ce",
|
||||||
|
"portfolio_asset_set_digest": "sha256:b64e3448a83a5b86466465080361c1a7e1157a27ddccd4b68069cb18caffb74a",
|
||||||
|
"position_count": 2,
|
||||||
|
"prior_weights": {
|
||||||
|
"A": 0.5,
|
||||||
|
"B": 0.5
|
||||||
|
},
|
||||||
|
"receipt": {
|
||||||
|
"algorithm": "bounded_allocation",
|
||||||
|
"algorithm_version": "1.0.0",
|
||||||
|
"computed_at": "2026-01-08T03:01:00Z",
|
||||||
|
"constraint_digest": "sha256:34df0e5c00f503748ff936f9cd415a8947169181f8ca0917bce97dd606e08e94",
|
||||||
|
"implementation_digest": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
"input_digest": "sha256:ebd8ff957115e1adfd84eafa5cea49470356194d747b64ee5897858f9dd067b7",
|
||||||
|
"iterations": null,
|
||||||
|
"max_constraint_residual": 0.0,
|
||||||
|
"objective_value": null,
|
||||||
|
"output_digest": "sha256:bd3b964c628c8648322d036e57dd6f444ca287017d1578bab3689d07d32b28ce",
|
||||||
|
"parameter_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"solver_config_digest": null,
|
||||||
|
"solver_name": null,
|
||||||
|
"solver_required": false,
|
||||||
|
"solver_version": null,
|
||||||
|
"status": "completed",
|
||||||
|
"tolerance": 1e-12
|
||||||
|
},
|
||||||
|
"run_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"run_ref_document_sha256": "6a798adb3e0568aca84ed3e3a285b92d181ec569462fc003952a8c0d8382ae3a",
|
||||||
|
"scenario_digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"source_universe_digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555",
|
||||||
|
"target_id": "portfolio-target:synthetic-v1",
|
||||||
|
"target_weights": {
|
||||||
|
"A": 0.6,
|
||||||
|
"B": 0.4
|
||||||
|
},
|
||||||
|
"turnover_l1": 0.19999999999999996
|
||||||
|
},
|
||||||
|
"risk_assessment": {
|
||||||
|
"assessment_id": "rhriskassessmentv1:sha256:dbc38825cffcf6d95bd0216d22dbba4e0d4a1359ca5924a3ee529b99a7d78b6d",
|
||||||
|
"component_risk": {
|
||||||
|
"A": 1.4549226783578566,
|
||||||
|
"B": 1.4549226783578568
|
||||||
|
},
|
||||||
|
"contract_name": "researchhub.risk-assessment",
|
||||||
|
"covariance_as_of_date": "2026-01-08",
|
||||||
|
"covariance_data_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
|
||||||
|
"covariance_input_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"covariance_snapshot_id": "covariance:synthetic-v1",
|
||||||
|
"dataset_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
|
||||||
|
"decision_id": "rhportfoliodecisionv1:sha256:0e6e5ce2fa08de8006cc392610695a013327fc80645bfa4d7a908ad3f615bebd",
|
||||||
|
"findings": [],
|
||||||
|
"freshness_policy_digest": "sha256:833f58f4d1056f0450f7369fd4edd70534cc74e9e55cd60f05b2b3d7a2979763",
|
||||||
|
"group_exposure": {
|
||||||
|
"equity": 1.4549226783578566,
|
||||||
|
"fixed_income": 1.4549226783578568
|
||||||
|
},
|
||||||
|
"manifest_id": "rhbacktestevidencev1:sha256:681c49cbdfb3e221b273e7bc616602ad80a7ab01206970807ad4294b176ebc75",
|
||||||
|
"marginal_risk": {
|
||||||
|
"A": 2.424871130596428,
|
||||||
|
"B": 3.637306695894642
|
||||||
|
},
|
||||||
|
"percentage_risk": {
|
||||||
|
"A": 0.49999999999999983,
|
||||||
|
"B": 0.49999999999999994
|
||||||
|
},
|
||||||
|
"periods_per_year": 252,
|
||||||
|
"portfolio_volatility": 2.909845356715714,
|
||||||
|
"portfolio_volatility_limit": 10.0,
|
||||||
|
"qualified": true,
|
||||||
|
"return_frequency": "1d",
|
||||||
|
"risk_budget": {
|
||||||
|
"A": 0.8,
|
||||||
|
"B": 0.8
|
||||||
|
},
|
||||||
|
"risk_model_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
|
||||||
|
"risk_model_name": "euler_volatility",
|
||||||
|
"risk_model_version": "1.0.0",
|
||||||
|
"run_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"scenario_digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"status": "ready"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ def test_module_spec_declares_pure_research_engine_boundary() -> None:
|
|||||||
prohibited = " ".join(spec["bounded_context"]["prohibited_responsibilities"]).lower()
|
prohibited = " ".join(spec["bounded_context"]["prohibited_responsibilities"]).lower()
|
||||||
for term in ("investment advice", "live order", "credentials", "source facts"):
|
for term in ("investment advice", "live order", "credentials", "source facts"):
|
||||||
assert term in prohibited
|
assert term in prohibited
|
||||||
assert spec["authority"]["revision"] == 3
|
assert spec["authority"]["revision"] == 4
|
||||||
assert {
|
assert {
|
||||||
(item["contract_id"], item["version"])
|
(item["contract_id"], item["version"])
|
||||||
for item in spec["contracts"]["provides"]
|
for item in spec["contracts"]["provides"]
|
||||||
@@ -25,12 +25,16 @@ def test_module_spec_declares_pure_research_engine_boundary() -> None:
|
|||||||
("researchhub.factor-set-ref", "1.0.0"),
|
("researchhub.factor-set-ref", "1.0.0"),
|
||||||
("researchhub.backtest-run-ref", "1.0.0"),
|
("researchhub.backtest-run-ref", "1.0.0"),
|
||||||
("researchhub.backtest-evidence-manifest", "1.0.0"),
|
("researchhub.backtest-evidence-manifest", "1.0.0"),
|
||||||
|
("researchhub.portfolio-decision", "1.0.0"),
|
||||||
|
("researchhub.risk-assessment", "1.0.0"),
|
||||||
}
|
}
|
||||||
expected_paths = {
|
expected_paths = {
|
||||||
"researchhub.factor-definition": "src/quant_engine/factor_contracts.py",
|
"researchhub.factor-definition": "src/quant_engine/factor_contracts.py",
|
||||||
"researchhub.factor-set-ref": "src/quant_engine/factor_contracts.py",
|
"researchhub.factor-set-ref": "src/quant_engine/factor_contracts.py",
|
||||||
"researchhub.backtest-run-ref": "src/quant_engine/governed_pipeline.py",
|
"researchhub.backtest-run-ref": "src/quant_engine/governed_pipeline.py",
|
||||||
"researchhub.backtest-evidence-manifest": "src/quant_engine/artifact.py",
|
"researchhub.backtest-evidence-manifest": "src/quant_engine/artifact.py",
|
||||||
|
"researchhub.portfolio-decision": "src/quant_engine/portfolio_risk_contracts.py",
|
||||||
|
"researchhub.risk-assessment": "src/quant_engine/portfolio_risk_contracts.py",
|
||||||
}
|
}
|
||||||
assert all(item["authority"] == "quant_engine" for item in spec["contracts"]["provides"])
|
assert all(item["authority"] == "quant_engine" for item in spec["contracts"]["provides"])
|
||||||
assert {
|
assert {
|
||||||
@@ -48,6 +52,14 @@ def test_module_spec_declares_pure_research_engine_boundary() -> None:
|
|||||||
for item in spec["contracts"]["consumes"]
|
for item in spec["contracts"]["consumes"]
|
||||||
)
|
)
|
||||||
assert spec["dependencies"] == []
|
assert spec["dependencies"] == []
|
||||||
|
capabilities = {item["id"]: item for item in spec["capabilities"]}
|
||||||
|
portfolio_contract = capabilities["portfolio-risk-computation-contracts"]
|
||||||
|
assert portfolio_contract["status"] == "operational"
|
||||||
|
summary = portfolio_contract["summary"].lower()
|
||||||
|
for term in ("receipt", "portfolio decisions", "risk assessments", "without"):
|
||||||
|
assert term in summary
|
||||||
|
for term in ("approval", "maker-checker", "publication", "paper", "live"):
|
||||||
|
assert term in prohibited
|
||||||
assert all(
|
assert all(
|
||||||
command["required"] and not command["network"]
|
command["required"] and not command["network"]
|
||||||
for command in spec["verification"]["commands"]
|
for command in spec["verification"]["commands"]
|
||||||
|
|||||||
@@ -1,17 +1,428 @@
|
|||||||
|
"""S4.1 portfolio-decision and risk-assessment contract conformance."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from datetime import UTC, date, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
def test_s41_public_contract_surface_exists() -> None:
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import quant_engine.portfolio_risk_contracts as contracts_module
|
||||||
|
from quant_engine.artifact import (
|
||||||
|
BacktestEvidenceManifest,
|
||||||
|
EvidenceQualification,
|
||||||
|
ResearchRunArtifact,
|
||||||
|
build_backtest_evidence_manifest,
|
||||||
|
build_research_run_artifact,
|
||||||
|
)
|
||||||
|
from quant_engine.execution import ExecutionConfig
|
||||||
|
from quant_engine.factor_contracts import (
|
||||||
|
ActorIdentity,
|
||||||
|
Causation,
|
||||||
|
DataFoundationEnvelope,
|
||||||
|
DatasetSnapshotEnvelope,
|
||||||
|
FactorInput,
|
||||||
|
FactorSetRef,
|
||||||
|
InputBinding,
|
||||||
|
OutputArtifactRef,
|
||||||
|
OutputCoverage,
|
||||||
|
OutputQuality,
|
||||||
|
OutputQualityCheck,
|
||||||
|
ProducerIdentity,
|
||||||
|
ViewAvailability,
|
||||||
|
canonical_json_bytes,
|
||||||
|
factor_definition_from_alpha158,
|
||||||
|
factor_input_schema_digest,
|
||||||
|
)
|
||||||
|
from quant_engine.governed_pipeline import (
|
||||||
|
BacktestRun,
|
||||||
|
BacktestRunRef,
|
||||||
|
PortfolioTarget,
|
||||||
|
RiskDecision,
|
||||||
|
RiskDecisionStatus,
|
||||||
|
RiskPolicy,
|
||||||
|
create_paper_order_intent,
|
||||||
|
evaluate_portfolio_risk,
|
||||||
|
)
|
||||||
from quant_engine.portfolio_risk_contracts import (
|
from quant_engine.portfolio_risk_contracts import (
|
||||||
ComputationReceipt,
|
ComputationReceipt,
|
||||||
ConstraintSetV1,
|
ConstraintSetV1,
|
||||||
FreshnessPolicy,
|
FreshnessPolicy,
|
||||||
PortfolioDecision,
|
PortfolioDecision,
|
||||||
|
PortfolioRiskContractError,
|
||||||
|
PortfolioRiskContractErrorCode,
|
||||||
|
ReceiptStatus,
|
||||||
RiskAssessment,
|
RiskAssessment,
|
||||||
|
RiskAssessmentStatus,
|
||||||
|
RiskFindingCode,
|
||||||
assess_portfolio_risk,
|
assess_portfolio_risk,
|
||||||
build_portfolio_decision,
|
build_portfolio_decision,
|
||||||
|
compute_portfolio_receipt_digests,
|
||||||
|
)
|
||||||
|
from quant_engine.research_pipeline import FactorBacktestResult, run_factor_backtest_research
|
||||||
|
from quant_engine.risk import ComponentRiskResult, CovarianceSnapshot, labeled_component_risk
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
FACTOR_FIXTURE = ROOT / "tests" / "fixtures" / "factor-contracts-v1.golden.json"
|
||||||
|
GOLDEN_FIXTURE = ROOT / "tests" / "fixtures" / "portfolio-risk-computation-v1.golden.json"
|
||||||
|
VIEW_REF_ID = "rhviewrefv1:sha256:bf776bcd26d940fafde1d650776a5505fb3fe8b5b068c351622bf2c42385629c"
|
||||||
|
VIEW_SCHEMA_DIGEST = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
CALENDAR_REVISION_ID = "rhcalv1:sha256:1f4ca22557063389badd669cf774bb35234066e847646682dccfe411e252a078"
|
||||||
|
ACTION_REVISION_ID = "rhcav1:sha256:0f947df29f152bfa2c6ab0da7a0d464670c7ad2526cd10f2a7939b42fee5c275"
|
||||||
|
PARAMETERS = {"lag_sessions": 1, "top_k": 1}
|
||||||
|
DECISION_FIELDS: dict[str, str] = {
|
||||||
|
"objective_name": "long_only_allocation",
|
||||||
|
"objective_version": "1.0.0",
|
||||||
|
"objective_digest": "sha256:" + "b" * 64,
|
||||||
|
"model_name": "deterministic_weights",
|
||||||
|
"model_version": "1.0.0",
|
||||||
|
"model_digest": "sha256:" + "c" * 64,
|
||||||
|
"expected_return_digest": "sha256:" + "d" * 64,
|
||||||
|
"covariance_digest": "sha256:" + "a" * 64,
|
||||||
|
"scenario_digest": "sha256:" + "e" * 64,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(value: bytes) -> str:
|
||||||
|
return f"sha256:{hashlib.sha256(value).hexdigest()}"
|
||||||
|
|
||||||
|
|
||||||
|
def _accepted_authorities() -> tuple[
|
||||||
|
DatasetSnapshotEnvelope,
|
||||||
|
DataFoundationEnvelope,
|
||||||
|
FactorSetRef,
|
||||||
|
]:
|
||||||
|
fixture = json.loads(FACTOR_FIXTURE.read_text(encoding="utf-8"))
|
||||||
|
snapshot = DatasetSnapshotEnvelope.from_dict(fixture["dataset_snapshot"])
|
||||||
|
foundation = DataFoundationEnvelope.from_dict(fixture["data_foundation"])
|
||||||
|
factor_input = FactorInput("market", VIEW_SCHEMA_DIGEST, ("close", "volume"))
|
||||||
|
definition = factor_definition_from_alpha158(
|
||||||
|
"alpha_005",
|
||||||
|
version="1.0.0",
|
||||||
|
parameters={},
|
||||||
|
inputs=(factor_input,),
|
||||||
|
implementation_digest="sha256:" + "1" * 64,
|
||||||
|
input_schema_digest=factor_input_schema_digest((factor_input,)),
|
||||||
|
valid_from="2026-01-01T00:00:00.000000Z",
|
||||||
|
valid_until="2027-01-01T00:00:00Z",
|
||||||
|
warmup_sessions=10,
|
||||||
|
lag_sessions=1,
|
||||||
|
producer=ProducerIdentity("quant_engine", "1.0.0"),
|
||||||
|
code_revision="c" * 40,
|
||||||
|
)
|
||||||
|
output_schema_bytes = canonical_json_bytes(fixture["output_schema"])
|
||||||
|
output_content_bytes = canonical_json_bytes(fixture["output_content"])
|
||||||
|
artifact_ref = OutputArtifactRef.create(
|
||||||
|
schema_digest=_sha256(output_schema_bytes),
|
||||||
|
content_digest=_sha256(output_content_bytes),
|
||||||
|
)
|
||||||
|
factor_set = FactorSetRef.create(
|
||||||
|
definitions=(definition,),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
selected_view_ref_ids=(VIEW_REF_ID,),
|
||||||
|
input_bindings=(
|
||||||
|
InputBinding(definition.definition_id, "market", VIEW_REF_ID, VIEW_SCHEMA_DIGEST),
|
||||||
|
),
|
||||||
|
view_availability=(
|
||||||
|
ViewAvailability(VIEW_REF_ID, "2026-01-02T23:50:00Z", "sha256:" + "2" * 64),
|
||||||
|
),
|
||||||
|
output_quality=OutputQuality(
|
||||||
|
"passed",
|
||||||
|
(OutputQualityCheck("finite_values", "passed", "sha256:" + "3" * 64),),
|
||||||
|
),
|
||||||
|
output_coverage=OutputCoverage(
|
||||||
|
"complete", 1, 1, "row", "alpha_005.cn_a", "sha256:" + "4" * 64
|
||||||
|
),
|
||||||
|
output_schema_bytes=output_schema_bytes,
|
||||||
|
output_content_bytes=output_content_bytes,
|
||||||
|
output_artifact_ref=artifact_ref,
|
||||||
|
availability_mode="as_available",
|
||||||
|
evaluation_at="2026-01-03T11:00:00Z",
|
||||||
|
computed_at="2026-01-03T10:15:00Z",
|
||||||
|
artifact_available_at="2026-01-03T10:20:00Z",
|
||||||
|
producer=ProducerIdentity("quant_engine", "1.0.0"),
|
||||||
|
code_revision="c" * 40,
|
||||||
|
actor=ActorIdentity("service", "factor_worker_v1"),
|
||||||
|
correlation_id="research_run_001",
|
||||||
|
causation=Causation("foundation", foundation.foundation_id),
|
||||||
|
evidence_scope="synthetic_fixture",
|
||||||
|
decision_eligible=False,
|
||||||
|
)
|
||||||
|
return snapshot, foundation, factor_set
|
||||||
|
|
||||||
|
|
||||||
|
def _config_digest() -> str:
|
||||||
|
encoded = json.dumps(
|
||||||
|
PARAMETERS,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
return _sha256(encoded)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_ref(*, universe_character: str = "5") -> BacktestRunRef:
|
||||||
|
snapshot, foundation, factor_set = _accepted_authorities()
|
||||||
|
return BacktestRunRef.create(
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
factor_set=factor_set,
|
||||||
|
universe_digest="sha256:" + universe_character * 64,
|
||||||
|
trading_calendar_revision_ids=(CALENDAR_REVISION_ID,),
|
||||||
|
corporate_action_revision_ids=(ACTION_REVISION_ID,),
|
||||||
|
strategy_id="alpha-top1",
|
||||||
|
strategy_version="1.0.0",
|
||||||
|
strategy_digest="sha256:" + "6" * 64,
|
||||||
|
execution_model_version="1.0.0",
|
||||||
|
execution_model_digest="sha256:" + "7" * 64,
|
||||||
|
cost_model_version="1.0.0",
|
||||||
|
cost_model_digest="sha256:" + "8" * 64,
|
||||||
|
random_seed=7,
|
||||||
|
code_revision="d" * 40,
|
||||||
|
environment_lock_digest="sha256:" + "9" * 64,
|
||||||
|
configuration_digest=_config_digest(),
|
||||||
|
evaluation_at="2026-01-08T01:00:00Z",
|
||||||
|
computed_at="2026-01-08T02:00:00Z",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _backtest_result() -> FactorBacktestResult:
|
||||||
|
dates = pd.date_range("2026-01-05", periods=4, freq="B")
|
||||||
|
scores = pd.DataFrame({"A": [2.0, 0.0], "B": [1.0, 3.0]}, index=dates[:2])
|
||||||
|
opens = pd.DataFrame(
|
||||||
|
{"A": [10.0, 10.0, 15.0, 15.0], "B": [20.0, 20.0, 20.0, 21.0]},
|
||||||
|
index=dates,
|
||||||
|
)
|
||||||
|
closes = pd.DataFrame(
|
||||||
|
{"A": [10.0, 12.0, 15.0, 15.0], "B": [20.0, 20.0, 18.0, 21.0]},
|
||||||
|
index=dates,
|
||||||
|
)
|
||||||
|
return run_factor_backtest_research(
|
||||||
|
scores,
|
||||||
|
opens,
|
||||||
|
closes,
|
||||||
|
top_k=1,
|
||||||
|
execution_price_field="open",
|
||||||
|
valuation_price_field="close",
|
||||||
|
initial_cash=1_000.0,
|
||||||
|
config=ExecutionConfig(
|
||||||
|
commission_bps=0,
|
||||||
|
stamp_tax_bps=0,
|
||||||
|
slippage_bps=0,
|
||||||
|
min_trade_amount=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact(run_ref: BacktestRunRef) -> ResearchRunArtifact:
|
||||||
|
result = _backtest_result()
|
||||||
|
benchmark = pd.Series(
|
||||||
|
[0.0, 0.01, -0.01, 0.02],
|
||||||
|
index=result.returns.index,
|
||||||
|
name="benchmark_return",
|
||||||
|
)
|
||||||
|
return build_research_run_artifact(
|
||||||
|
result,
|
||||||
|
run_id=run_ref.run_id,
|
||||||
|
strategy_id=run_ref.strategy_id,
|
||||||
|
strategy_name="Alpha Top 1",
|
||||||
|
strategy_version=run_ref.strategy_version,
|
||||||
|
engine_version="1.2.0",
|
||||||
|
code_revision=run_ref.code_revision,
|
||||||
|
data_snapshot_id=run_ref.dataset_snapshot_id,
|
||||||
|
calendar="CN-A",
|
||||||
|
timezone="Asia/Shanghai",
|
||||||
|
started_at="2026-01-08T10:00:00+08:00",
|
||||||
|
finished_at="2026-01-08T10:01:00+08:00",
|
||||||
|
parameters=PARAMETERS,
|
||||||
|
benchmark_id="000300.SH",
|
||||||
|
benchmark_returns=benchmark,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _covariance(
|
||||||
|
context: dict[str, Any],
|
||||||
|
*,
|
||||||
|
matrix: pd.DataFrame | None = None,
|
||||||
|
data_snapshot_id: str | None = None,
|
||||||
|
as_of_date: date = date(2026, 1, 8),
|
||||||
|
input_sha256: str = "a" * 64,
|
||||||
|
) -> CovarianceSnapshot:
|
||||||
|
return CovarianceSnapshot(
|
||||||
|
snapshot_id="covariance:synthetic-v1",
|
||||||
|
as_of_date=as_of_date,
|
||||||
|
covariance=(
|
||||||
|
pd.DataFrame([[0.04, 0.01], [0.01, 0.09]], index=["A", "B"], columns=["A", "B"])
|
||||||
|
if matrix is None
|
||||||
|
else matrix
|
||||||
|
),
|
||||||
|
return_frequency="1d",
|
||||||
|
periods_per_year=252,
|
||||||
|
method="provided",
|
||||||
|
window_start_date=date(2026, 1, 1),
|
||||||
|
window_end_date=as_of_date,
|
||||||
|
observations=6,
|
||||||
|
lookback_sessions=6,
|
||||||
|
missing_policy="complete_case",
|
||||||
|
data_snapshot_id=(
|
||||||
|
context["run_ref"].dataset_snapshot_id
|
||||||
|
if data_snapshot_id is None
|
||||||
|
else data_snapshot_id
|
||||||
|
),
|
||||||
|
input_sha256=input_sha256,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _context(
|
||||||
|
*,
|
||||||
|
universe_character: str = "5",
|
||||||
|
artifact_available_at: str = "2026-01-08T02:01:00Z",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
run_ref = _run_ref(universe_character=universe_character)
|
||||||
|
artifact = _artifact(run_ref)
|
||||||
|
manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at=artifact_available_at,
|
||||||
|
)
|
||||||
|
target = PortfolioTarget(
|
||||||
|
target_id="portfolio-target:synthetic-v1",
|
||||||
|
backtest_run_id=run_ref.run_id,
|
||||||
|
dataset_snapshot_id=run_ref.dataset_snapshot_id,
|
||||||
|
weights={"B": 0.4, "A": 0.6},
|
||||||
|
created_at=datetime(2026, 1, 8, 3, 0, tzinfo=UTC),
|
||||||
|
)
|
||||||
|
result: dict[str, Any] = {
|
||||||
|
"run_ref": run_ref,
|
||||||
|
"artifact": artifact,
|
||||||
|
"manifest": manifest,
|
||||||
|
"target": target,
|
||||||
|
"constraints": ConstraintSetV1(
|
||||||
|
gross_exposure_max=1.0,
|
||||||
|
net_exposure_min=1.0,
|
||||||
|
net_exposure_max=1.0,
|
||||||
|
single_asset_min=0.2,
|
||||||
|
single_asset_max=0.7,
|
||||||
|
position_count_max=2,
|
||||||
|
turnover_max=0.2,
|
||||||
|
),
|
||||||
|
"freshness_policy": FreshnessPolicy(
|
||||||
|
max_manifest_age_seconds=3_600,
|
||||||
|
max_covariance_age_days=0,
|
||||||
|
),
|
||||||
|
"prior_weights": {"A": 0.5, "B": 0.5},
|
||||||
|
"computed_at": datetime(2026, 1, 8, 3, 1, tzinfo=UTC),
|
||||||
|
}
|
||||||
|
result["covariance"] = _covariance(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def context() -> dict[str, Any]:
|
||||||
|
return _context()
|
||||||
|
|
||||||
|
|
||||||
|
def _decision_values(context: dict[str, Any], **overrides: Any) -> dict[str, Any]:
|
||||||
|
values: dict[str, Any] = {
|
||||||
|
"backtest_run_ref": context["run_ref"],
|
||||||
|
"manifest": context["manifest"],
|
||||||
|
"target": context["target"],
|
||||||
|
**DECISION_FIELDS,
|
||||||
|
"constraints": context["constraints"],
|
||||||
|
"freshness_policy": context["freshness_policy"],
|
||||||
|
"computed_at": context["computed_at"],
|
||||||
|
"prior_weights": context["prior_weights"],
|
||||||
|
}
|
||||||
|
values.update(overrides)
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _receipt(
|
||||||
|
values: dict[str, Any],
|
||||||
|
*,
|
||||||
|
solver_required: bool = False,
|
||||||
|
status: ReceiptStatus = ReceiptStatus.COMPLETED,
|
||||||
|
overrides: dict[str, Any] | None = None,
|
||||||
|
) -> ComputationReceipt:
|
||||||
|
digest_arguments = {
|
||||||
|
key: value
|
||||||
|
for key, value in values.items()
|
||||||
|
if key not in {"computed_at", "receipt"}
|
||||||
|
}
|
||||||
|
digests = compute_portfolio_receipt_digests(**digest_arguments)
|
||||||
|
receipt_values: dict[str, Any] = {
|
||||||
|
"algorithm": "bounded_allocation",
|
||||||
|
"algorithm_version": "1.0.0",
|
||||||
|
"implementation_digest": "sha256:" + "f" * 64,
|
||||||
|
"parameter_digest": "sha256:" + "0" * 64,
|
||||||
|
"input_digest": digests["input_digest"],
|
||||||
|
"constraint_digest": digests["constraint_digest"],
|
||||||
|
"output_digest": digests["output_digest"],
|
||||||
|
"status": status,
|
||||||
|
"solver_required": solver_required,
|
||||||
|
"solver_name": "slsqp" if solver_required else None,
|
||||||
|
"solver_version": "1.0.0" if solver_required else None,
|
||||||
|
"solver_config_digest": "sha256:" + "1" * 64 if solver_required else None,
|
||||||
|
"iterations": 4 if solver_required else None,
|
||||||
|
"objective_value": 0.25 if solver_required else None,
|
||||||
|
"max_constraint_residual": digests["max_constraint_residual"],
|
||||||
|
"tolerance": 1.0 if solver_required else 1e-12,
|
||||||
|
"computed_at": values["computed_at"],
|
||||||
|
}
|
||||||
|
if overrides:
|
||||||
|
receipt_values.update(overrides)
|
||||||
|
return ComputationReceipt(**receipt_values)
|
||||||
|
|
||||||
|
|
||||||
|
def _decision(context: dict[str, Any], **overrides: Any) -> PortfolioDecision:
|
||||||
|
values = _decision_values(context, **overrides)
|
||||||
|
receipt = values.pop("receipt", None)
|
||||||
|
if receipt is None:
|
||||||
|
receipt = _receipt(values)
|
||||||
|
return build_portfolio_decision(**values, receipt=receipt)
|
||||||
|
|
||||||
|
|
||||||
|
def _assessment(
|
||||||
|
context: dict[str, Any],
|
||||||
|
*,
|
||||||
|
decision: PortfolioDecision | None = None,
|
||||||
|
covariance: CovarianceSnapshot | None = None,
|
||||||
|
**overrides: Any,
|
||||||
|
) -> RiskAssessment:
|
||||||
|
values: dict[str, Any] = {
|
||||||
|
"portfolio_decision": _decision(context) if decision is None else decision,
|
||||||
|
"backtest_run_ref": context["run_ref"],
|
||||||
|
"manifest": context["manifest"],
|
||||||
|
"covariance": context["covariance"] if covariance is None else covariance,
|
||||||
|
"risk_model_name": "euler_volatility",
|
||||||
|
"risk_model_version": "1.0.0",
|
||||||
|
"risk_model_digest": "sha256:" + "2" * 64,
|
||||||
|
"risk_budget": {"A": 0.8, "B": 0.8},
|
||||||
|
"portfolio_volatility_limit": 10.0,
|
||||||
|
"groups": {"A": "equity", "B": "fixed_income"},
|
||||||
|
}
|
||||||
|
values.update(overrides)
|
||||||
|
return assess_portfolio_risk(**values)
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_error(
|
||||||
|
error: pytest.ExceptionInfo[PortfolioRiskContractError],
|
||||||
|
code: PortfolioRiskContractErrorCode,
|
||||||
|
path: str,
|
||||||
|
) -> None:
|
||||||
|
assert error.value.code is code
|
||||||
|
assert error.value.path == path
|
||||||
|
|
||||||
|
|
||||||
|
def test_s41_public_contract_surface_exists() -> None:
|
||||||
assert all(
|
assert all(
|
||||||
symbol is not None
|
symbol is not None
|
||||||
for symbol in (
|
for symbol in (
|
||||||
@@ -24,3 +435,570 @@ def test_s41_public_contract_surface_exists() -> None:
|
|||||||
build_portfolio_decision,
|
build_portfolio_decision,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_positive_decision_and_assessment_match_canonical_golden(
|
||||||
|
context: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
decision = _decision(context)
|
||||||
|
assessment = _assessment(context, decision=decision)
|
||||||
|
|
||||||
|
assert decision.run_ref_document_sha256 == hashlib.sha256(
|
||||||
|
context["run_ref"].to_json().encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
assert decision.manifest_document_sha256 == hashlib.sha256(
|
||||||
|
context["manifest"].to_json().encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
assert decision.source_universe_digest == context["run_ref"].universe_digest
|
||||||
|
assert decision.source_universe_digest != decision.portfolio_asset_set_digest
|
||||||
|
assert decision.gross_exposure == decision.net_exposure == 1.0
|
||||||
|
assert decision.turnover_l1 == pytest.approx(0.2)
|
||||||
|
assert assessment.status is RiskAssessmentStatus.READY
|
||||||
|
assert assessment.qualified is True
|
||||||
|
assert assessment.findings == ()
|
||||||
|
assert sum(assessment.component_risk.values()) == pytest.approx(
|
||||||
|
assessment.portfolio_volatility
|
||||||
|
)
|
||||||
|
assert sum(assessment.percentage_risk.values()) == pytest.approx(1.0)
|
||||||
|
actual = {
|
||||||
|
"portfolio_decision": decision.to_dict(),
|
||||||
|
"risk_assessment": assessment.to_dict(),
|
||||||
|
}
|
||||||
|
expected = json.loads(GOLDEN_FIXTURE.read_text(encoding="utf-8"))
|
||||||
|
assert actual == expected
|
||||||
|
assert decision.to_json() == json.dumps(
|
||||||
|
decision.to_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_container_order_is_identity_neutral(context: dict[str, Any]) -> None:
|
||||||
|
target = PortfolioTarget(
|
||||||
|
target_id=context["target"].target_id,
|
||||||
|
backtest_run_id=context["target"].backtest_run_id,
|
||||||
|
dataset_snapshot_id=context["target"].dataset_snapshot_id,
|
||||||
|
weights={"A": 0.6, "B": 0.4},
|
||||||
|
created_at=context["target"].created_at,
|
||||||
|
)
|
||||||
|
reordered = _decision(context, target=target, prior_weights={"B": 0.5, "A": 0.5})
|
||||||
|
assert reordered.decision_id == _decision(context).decision_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_manifest_target_and_model_mutations_change_identity(
|
||||||
|
context: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
baseline = _decision(context)
|
||||||
|
other_run_context = _context(universe_character="4")
|
||||||
|
other_run = _decision(other_run_context)
|
||||||
|
later_manifest_context = _context(artifact_available_at="2026-01-08T02:02:00Z")
|
||||||
|
later_manifest = _decision(later_manifest_context)
|
||||||
|
changed_target = PortfolioTarget(
|
||||||
|
target_id="portfolio-target:synthetic-v2",
|
||||||
|
backtest_run_id=context["run_ref"].run_id,
|
||||||
|
dataset_snapshot_id=context["run_ref"].dataset_snapshot_id,
|
||||||
|
weights={"A": 0.5, "B": 0.5},
|
||||||
|
created_at=context["target"].created_at,
|
||||||
|
)
|
||||||
|
changed_weights = _decision(context, target=changed_target)
|
||||||
|
changed_assets_target = PortfolioTarget(
|
||||||
|
target_id="portfolio-target:synthetic-v3",
|
||||||
|
backtest_run_id=context["run_ref"].run_id,
|
||||||
|
dataset_snapshot_id=context["run_ref"].dataset_snapshot_id,
|
||||||
|
weights={"A": 0.6, "C": 0.4},
|
||||||
|
created_at=context["target"].created_at,
|
||||||
|
)
|
||||||
|
changed_assets = _decision(
|
||||||
|
context,
|
||||||
|
target=changed_assets_target,
|
||||||
|
prior_weights={"A": 0.5, "C": 0.5},
|
||||||
|
)
|
||||||
|
changed_model = _decision(
|
||||||
|
context,
|
||||||
|
model_version="1.0.1",
|
||||||
|
model_digest="sha256:" + "3" * 64,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert other_run.run_ref_document_sha256 != baseline.run_ref_document_sha256
|
||||||
|
assert other_run.source_universe_digest != baseline.source_universe_digest
|
||||||
|
assert later_manifest.manifest_document_sha256 != baseline.manifest_document_sha256
|
||||||
|
assert changed_weights.portfolio_asset_set_digest == baseline.portfolio_asset_set_digest
|
||||||
|
assert changed_weights.output_digest != baseline.output_digest
|
||||||
|
assert changed_assets.portfolio_asset_set_digest != baseline.portfolio_asset_set_digest
|
||||||
|
assert len(
|
||||||
|
{
|
||||||
|
baseline.decision_id,
|
||||||
|
other_run.decision_id,
|
||||||
|
later_manifest.decision_id,
|
||||||
|
changed_weights.decision_id,
|
||||||
|
changed_assets.decision_id,
|
||||||
|
changed_model.decision_id,
|
||||||
|
}
|
||||||
|
) == 6
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("payload", "path"),
|
||||||
|
[
|
||||||
|
({"schema_version": "1.0.0", "max_covariance_age_days": 1}, "$.max_manifest_age_seconds"),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"max_manifest_age_seconds": 1,
|
||||||
|
"max_covariance_age_days": 1,
|
||||||
|
"extra": 1,
|
||||||
|
},
|
||||||
|
"$.extra",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"max_manifest_age_seconds": -1,
|
||||||
|
"max_covariance_age_days": 1,
|
||||||
|
},
|
||||||
|
"$.max_manifest_age_seconds",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"max_manifest_age_seconds": True,
|
||||||
|
"max_covariance_age_days": 1,
|
||||||
|
},
|
||||||
|
"$.max_manifest_age_seconds",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"max_manifest_age_seconds": 1.0,
|
||||||
|
"max_covariance_age_days": 1,
|
||||||
|
},
|
||||||
|
"$.max_manifest_age_seconds",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"max_manifest_age_seconds": 2**53,
|
||||||
|
"max_covariance_age_days": 1,
|
||||||
|
},
|
||||||
|
"$.max_manifest_age_seconds",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_freshness_policy_has_strict_schema_and_safe_thresholds(
|
||||||
|
payload: dict[str, object], path: str
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
FreshnessPolicy.from_dict(payload)
|
||||||
|
_assert_error(error, PortfolioRiskContractErrorCode.INVALID_FRESHNESS_POLICY, path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_manifest_freshness_boundary_and_timezone_normalization(
|
||||||
|
context: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
equal_boundary = _decision(
|
||||||
|
context,
|
||||||
|
computed_at="2026-01-08T11:01:00+08:00",
|
||||||
|
)
|
||||||
|
assert equal_boundary.computed_at == "2026-01-08T03:01:00Z"
|
||||||
|
stale_values = _decision_values(
|
||||||
|
context,
|
||||||
|
computed_at="2026-01-08T03:01:01Z",
|
||||||
|
)
|
||||||
|
stale_receipt = _receipt(stale_values)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
build_portfolio_decision(**stale_values, receipt=stale_receipt)
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.MANIFEST_STALE,
|
||||||
|
"$.manifest.artifact_available_at",
|
||||||
|
)
|
||||||
|
naive_values = _decision_values(
|
||||||
|
context,
|
||||||
|
computed_at=datetime(2026, 1, 8, 3, 1),
|
||||||
|
)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
_receipt(naive_values)
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.TIME_ORDER_VIOLATION,
|
||||||
|
"$.computed_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_constraint_set_rejects_unknown_nonfinite_and_unsafe_fields() -> None:
|
||||||
|
payload = ConstraintSetV1().to_dict()
|
||||||
|
payload["custom_constraint"] = 1.0
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
ConstraintSetV1.from_dict(payload)
|
||||||
|
_assert_error(error, PortfolioRiskContractErrorCode.UNKNOWN_FIELD, "$.custom_constraint")
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
ConstraintSetV1(gross_exposure_max=float("nan"))
|
||||||
|
_assert_error(error, PortfolioRiskContractErrorCode.INVALID_VALUE, "$.gross_exposure_max")
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
ConstraintSetV1(position_count_max=True)
|
||||||
|
_assert_error(error, PortfolioRiskContractErrorCode.TYPE_ERROR, "$.position_count_max")
|
||||||
|
|
||||||
|
|
||||||
|
def test_s3_qualification_embedded_run_and_dataset_mismatch_fail_closed(
|
||||||
|
context: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
exploratory = build_backtest_evidence_manifest(
|
||||||
|
context["run_ref"],
|
||||||
|
context["artifact"],
|
||||||
|
artifact_available_at="2026-01-08T02:01:00Z",
|
||||||
|
qualification=EvidenceQualification.EXPLORATORY,
|
||||||
|
)
|
||||||
|
values = _decision_values(context, manifest=exploratory)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
build_portfolio_decision(**values, receipt=_receipt(values))
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.QUALIFICATION_REJECTED,
|
||||||
|
"$.manifest.qualification",
|
||||||
|
)
|
||||||
|
|
||||||
|
other_run = _run_ref(universe_character="4")
|
||||||
|
manifest = context["manifest"]
|
||||||
|
mismatched = object.__new__(BacktestEvidenceManifest)
|
||||||
|
for field_name in manifest.__dataclass_fields__:
|
||||||
|
object.__setattr__(
|
||||||
|
mismatched,
|
||||||
|
field_name,
|
||||||
|
other_run if field_name == "backtest_run_ref" else getattr(manifest, field_name),
|
||||||
|
)
|
||||||
|
mismatch_values = _decision_values(context, manifest=mismatched)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
build_portfolio_decision(**mismatch_values, receipt=_receipt(mismatch_values))
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.IDENTITY_MISMATCH,
|
||||||
|
"$.manifest.run_reference.value",
|
||||||
|
)
|
||||||
|
|
||||||
|
target = PortfolioTarget(
|
||||||
|
target_id="portfolio-target:dataset-drift",
|
||||||
|
backtest_run_id=context["run_ref"].run_id,
|
||||||
|
dataset_snapshot_id="dataset:other",
|
||||||
|
weights={"A": 0.6, "B": 0.4},
|
||||||
|
created_at=context["target"].created_at,
|
||||||
|
)
|
||||||
|
drift_values = _decision_values(context, target=target)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
build_portfolio_decision(**drift_values, receipt=_receipt(drift_values))
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.DATASET_IDENTITY_MISMATCH,
|
||||||
|
"$.portfolio_target.dataset_snapshot_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_legacy_manifest_cannot_form_a_decision(context: dict[str, Any]) -> None:
|
||||||
|
manifest = context["manifest"]
|
||||||
|
legacy_run = BacktestRun(
|
||||||
|
run_id=context["run_ref"].run_id,
|
||||||
|
dataset_snapshot_id=context["run_ref"].dataset_snapshot_id,
|
||||||
|
factor_version_id="legacy-factor@1.0.0",
|
||||||
|
strategy_version_id="alpha-top1@1.0.0",
|
||||||
|
code_revision=context["run_ref"].code_revision,
|
||||||
|
config_hash=context["run_ref"].configuration_digest.removeprefix("sha256:"),
|
||||||
|
created_at=datetime(2026, 1, 8, 2, tzinfo=UTC),
|
||||||
|
)
|
||||||
|
legacy = object.__new__(BacktestEvidenceManifest)
|
||||||
|
overrides = {
|
||||||
|
"qualification": EvidenceQualification.LEGACY_EXPLORATORY,
|
||||||
|
"backtest_run_ref": None,
|
||||||
|
"legacy_backtest_run": legacy_run,
|
||||||
|
}
|
||||||
|
for field_name in manifest.__dataclass_fields__:
|
||||||
|
object.__setattr__(
|
||||||
|
legacy,
|
||||||
|
field_name,
|
||||||
|
overrides.get(field_name, getattr(manifest, field_name)),
|
||||||
|
)
|
||||||
|
values = _decision_values(context, manifest=legacy)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
build_portfolio_decision(**values, receipt=_receipt(values))
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.QUALIFICATION_REJECTED,
|
||||||
|
"$.manifest.qualification",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_receipt_field_matrix_and_digest_recomputation(context: dict[str, Any]) -> None:
|
||||||
|
values = _decision_values(context)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
_receipt(values, overrides={"algorithm_version": "v1"})
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.INVALID_FORMAT,
|
||||||
|
"$.algorithm_version",
|
||||||
|
)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
_receipt(values, overrides={"solver_name": "unexpected"})
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.INVALID_VALUE,
|
||||||
|
"$.solver_name",
|
||||||
|
)
|
||||||
|
bad_receipt = _receipt(values, overrides={"input_digest": "sha256:" + "9" * 64})
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
build_portfolio_decision(**values, receipt=bad_receipt)
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.RECEIPT_MISMATCH,
|
||||||
|
"$.receipt.input_digest",
|
||||||
|
)
|
||||||
|
for status in (ReceiptStatus.FAILED, ReceiptStatus.FALLBACK):
|
||||||
|
rejected = _receipt(
|
||||||
|
values,
|
||||||
|
solver_required=True,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
build_portfolio_decision(**values, receipt=rejected)
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.QUALIFICATION_REJECTED,
|
||||||
|
"$.receipt.status",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_constraints_are_independently_recomputed(context: dict[str, Any]) -> None:
|
||||||
|
target = PortfolioTarget(
|
||||||
|
target_id="portfolio-target:violating",
|
||||||
|
backtest_run_id=context["run_ref"].run_id,
|
||||||
|
dataset_snapshot_id=context["run_ref"].dataset_snapshot_id,
|
||||||
|
weights={"A": 0.8, "B": 0.2},
|
||||||
|
created_at=context["target"].created_at,
|
||||||
|
)
|
||||||
|
values = _decision_values(context, target=target)
|
||||||
|
receipt = _receipt(values, solver_required=True, status=ReceiptStatus.CONVERGED)
|
||||||
|
assert receipt.max_constraint_residual == pytest.approx(0.4)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
build_portfolio_decision(**values, receipt=receipt)
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.CONSTRAINT_VIOLATION,
|
||||||
|
"$.constraints",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_covariance_target_and_run_dataset_identity_is_three_way_closed(
|
||||||
|
context: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
covariance = _covariance(context, data_snapshot_id="dataset:other")
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
_assessment(context, covariance=covariance)
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.DATASET_IDENTITY_MISMATCH,
|
||||||
|
"$.covariance.data_snapshot_id",
|
||||||
|
)
|
||||||
|
wrong_digest = _covariance(context, input_sha256="b" * 64)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
_assessment(context, covariance=wrong_digest)
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.IDENTITY_MISMATCH,
|
||||||
|
"$.covariance.input_sha256",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_covariance_pit_and_freshness_are_enforced(context: dict[str, Any]) -> None:
|
||||||
|
future = _covariance(context, as_of_date=date(2026, 1, 9))
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
_assessment(context, covariance=future)
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.TIME_ORDER_VIOLATION,
|
||||||
|
"$.covariance.as_of_date",
|
||||||
|
)
|
||||||
|
stale = _covariance(context, as_of_date=date(2026, 1, 7))
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
_assessment(context, covariance=stale)
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.COVARIANCE_STALE,
|
||||||
|
"$.covariance.as_of_date",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_risk_decomposition_is_delegated_exactly_once(
|
||||||
|
context: dict[str, Any], monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
calls = 0
|
||||||
|
|
||||||
|
def wrapped(weights: pd.Series[Any], covariance: pd.DataFrame) -> ComponentRiskResult:
|
||||||
|
nonlocal calls
|
||||||
|
calls += 1
|
||||||
|
return labeled_component_risk(weights, covariance)
|
||||||
|
|
||||||
|
monkeypatch.setattr(contracts_module, "labeled_component_risk", wrapped)
|
||||||
|
assessment = _assessment(context)
|
||||||
|
assert calls == 1
|
||||||
|
assert assessment.qualified is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("matrix", "finding"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
pd.DataFrame([[1.0, 2.0], [2.0, 1.0]], index=["A", "B"], columns=["A", "B"]),
|
||||||
|
RiskFindingCode.COVARIANCE_NOT_PSD,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
pd.DataFrame([[0.0, 0.0], [0.0, 0.0]], index=["A", "B"], columns=["A", "B"]),
|
||||||
|
RiskFindingCode.PORTFOLIO_VARIANCE_NON_POSITIVE,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_named_numerical_failures_return_stable_unavailable_findings(
|
||||||
|
context: dict[str, Any], matrix: pd.DataFrame, finding: RiskFindingCode
|
||||||
|
) -> None:
|
||||||
|
result = _assessment(context, covariance=_covariance(context, matrix=matrix))
|
||||||
|
assert result.status is RiskAssessmentStatus.UNAVAILABLE
|
||||||
|
assert result.qualified is False
|
||||||
|
assert result.findings == (finding,)
|
||||||
|
assert result.portfolio_volatility is None
|
||||||
|
assert result.marginal_risk == result.component_risk == result.percentage_risk == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_numerical_error_is_sanitized(
|
||||||
|
context: dict[str, Any], monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
def fail_unknown(weights: pd.Series[Any], covariance: pd.DataFrame) -> ComponentRiskResult:
|
||||||
|
raise ValueError("sensitive lower-level details")
|
||||||
|
|
||||||
|
monkeypatch.setattr(contracts_module, "labeled_component_risk", fail_unknown)
|
||||||
|
with pytest.raises(PortfolioRiskContractError) as error:
|
||||||
|
_assessment(context)
|
||||||
|
_assert_error(
|
||||||
|
error,
|
||||||
|
PortfolioRiskContractErrorCode.COMPUTATION_FAILURE,
|
||||||
|
"$.covariance",
|
||||||
|
)
|
||||||
|
assert "sensitive" not in str(error.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_closed_risk_and_budget_breach_have_distinct_semantics(
|
||||||
|
context: dict[str, Any], monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
def non_closed(weights: pd.Series[Any], covariance: pd.DataFrame) -> ComponentRiskResult:
|
||||||
|
return ComponentRiskResult(
|
||||||
|
portfolio_volatility=1.0,
|
||||||
|
marginal=pd.Series({"A": 0.1, "B": 0.1}),
|
||||||
|
component=pd.Series({"A": 0.1, "B": 0.1}),
|
||||||
|
percentage=pd.Series({"A": 0.5, "B": 0.4}),
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(contracts_module, "labeled_component_risk", non_closed)
|
||||||
|
unavailable = _assessment(context)
|
||||||
|
assert unavailable.status is RiskAssessmentStatus.UNAVAILABLE
|
||||||
|
assert unavailable.findings == (RiskFindingCode.RISK_CONTRIBUTION_NOT_CLOSED,)
|
||||||
|
monkeypatch.setattr(contracts_module, "labeled_component_risk", labeled_component_risk)
|
||||||
|
breached = _assessment(context, risk_budget={"A": 0.0})
|
||||||
|
assert breached.status is RiskAssessmentStatus.READY
|
||||||
|
assert breached.qualified is False
|
||||||
|
assert breached.findings == (RiskFindingCode.RISK_BUDGET_BREACH,)
|
||||||
|
assert breached.component_risk
|
||||||
|
|
||||||
|
|
||||||
|
def test_existing_target_risk_decision_artifact_and_paper_slice_remain_legacy_only(
|
||||||
|
context: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
target = context["target"]
|
||||||
|
legacy_decision = evaluate_portfolio_risk(
|
||||||
|
target,
|
||||||
|
RiskPolicy(
|
||||||
|
policy_id="legacy-paper-policy",
|
||||||
|
max_gross_exposure=1.0,
|
||||||
|
max_single_asset_weight=0.7,
|
||||||
|
max_positions=2,
|
||||||
|
),
|
||||||
|
created_at=target.created_at,
|
||||||
|
)
|
||||||
|
intent = create_paper_order_intent(target, legacy_decision)
|
||||||
|
assert isinstance(legacy_decision, RiskDecision)
|
||||||
|
assert legacy_decision.status is RiskDecisionStatus.APPROVED
|
||||||
|
assert intent.environment == "paper"
|
||||||
|
assert isinstance(context["artifact"], ResearchRunArtifact)
|
||||||
|
assert not isinstance(target, PortfolioDecision)
|
||||||
|
assert not isinstance(legacy_decision, RiskAssessment)
|
||||||
|
|
||||||
|
|
||||||
|
def test_architecture_dependency_no_copy_and_authority_boundaries() -> None:
|
||||||
|
new_path = ROOT / "src" / "quant_engine" / "portfolio_risk_contracts.py"
|
||||||
|
source = new_path.read_text(encoding="utf-8")
|
||||||
|
tree = ast.parse(source)
|
||||||
|
called = {
|
||||||
|
node.func.id
|
||||||
|
for node in ast.walk(tree)
|
||||||
|
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
|
||||||
|
}
|
||||||
|
assert not called & {
|
||||||
|
"evaluate_portfolio_risk",
|
||||||
|
"create_paper_order_intent",
|
||||||
|
"run_governed_factor_slice",
|
||||||
|
}
|
||||||
|
imports = {
|
||||||
|
alias.name
|
||||||
|
for node in ast.walk(tree)
|
||||||
|
if isinstance(node, ast.Import)
|
||||||
|
for alias in node.names
|
||||||
|
} | {
|
||||||
|
node.module or ""
|
||||||
|
for node in ast.walk(tree)
|
||||||
|
if isinstance(node, ast.ImportFrom)
|
||||||
|
}
|
||||||
|
assert not any(name.startswith(("research_results", "research_platform")) for name in imports)
|
||||||
|
for candidate in ("riskfolio", "pyp", "skfolio", "cvxportfolio"):
|
||||||
|
assert candidate not in source.lower()
|
||||||
|
for owner_path in (
|
||||||
|
ROOT / "src" / "quant_engine" / "governed_pipeline.py",
|
||||||
|
ROOT / "src" / "quant_engine" / "artifact.py",
|
||||||
|
ROOT / "src" / "quant_engine" / "portfolio_construction.py",
|
||||||
|
ROOT / "src" / "quant_engine" / "portfolio_decomp.py",
|
||||||
|
ROOT / "src" / "quant_engine" / "risk.py",
|
||||||
|
):
|
||||||
|
assert "portfolio_risk_contracts" not in owner_path.read_text(encoding="utf-8")
|
||||||
|
forbidden_fields = {
|
||||||
|
"approved",
|
||||||
|
"approval",
|
||||||
|
"maker",
|
||||||
|
"checker",
|
||||||
|
"owner",
|
||||||
|
"publication",
|
||||||
|
"order",
|
||||||
|
"broker",
|
||||||
|
"environment",
|
||||||
|
"paper",
|
||||||
|
"live",
|
||||||
|
"uri",
|
||||||
|
"path",
|
||||||
|
"locator",
|
||||||
|
}
|
||||||
|
for contract_type in (
|
||||||
|
FreshnessPolicy,
|
||||||
|
ConstraintSetV1,
|
||||||
|
ComputationReceipt,
|
||||||
|
PortfolioDecision,
|
||||||
|
RiskAssessment,
|
||||||
|
):
|
||||||
|
assert not forbidden_fields & set(contract_type.__dataclass_fields__)
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_only_owner_dependency_lock_and_ci_hashes_match_baseline() -> None:
|
||||||
|
expected = {
|
||||||
|
"src/quant_engine/governed_pipeline.py": "3b334f340898db78ed869375ab532f156e8c1fee595a8318f544bebdd391049d",
|
||||||
|
"src/quant_engine/artifact.py": "e15feec412d3bfff10d8f21ca20813fc65cabc0703940371ed661896147bc379",
|
||||||
|
"src/quant_engine/portfolio_construction.py": "e93d71da8d61b2047c19d4b99dace934a8cbc96d8d2b150ad62a9ceebd9163d4",
|
||||||
|
"src/quant_engine/portfolio_decomp.py": "1a4f9f9aac2c46bf6ed2826d1b0d1723f06e3ce7c4b3f6479e098cbbd135bea6",
|
||||||
|
"src/quant_engine/risk.py": "4a66c312d517d40f6f67bb71f438523e135645624ae78d49fa9c0fda2c02074e",
|
||||||
|
"pyproject.toml": "9340a25f3710778945765f14a02c91844aa66bd47b77c55d8fa4bd36849b4bd5",
|
||||||
|
"uv.lock": "076a16a01bc363109174909999652e81055954200a3ab93eab5f76df2249ee1a",
|
||||||
|
"ci-profile.yml": "dfe1b7c2820747fa28eaad409153623c468ee3e10f38f4fd94241731c5a55803",
|
||||||
|
}
|
||||||
|
actual = {
|
||||||
|
path: hashlib.sha256((ROOT / path).read_bytes()).hexdigest()
|
||||||
|
for path in expected
|
||||||
|
}
|
||||||
|
assert actual == expected
|
||||||
|
|||||||
Reference in New Issue
Block a user