This commit is contained in:
+6
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"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"},
|
||||
"bounded_context": {
|
||||
"domain": "quantitative-research-engine",
|
||||
@@ -11,6 +11,7 @@
|
||||
"Submitting live orders, routing trades, managing brokerage accounts, or claiming transaction execution",
|
||||
"Owning market-data source facts, research-result publication, or platform presentation state",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
@@ -19,6 +20,7 @@
|
||||
{"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": "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"}
|
||||
],
|
||||
"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-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-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": [
|
||||
{"contract_id": "researchhub.dataset-snapshot", "version": "1.0.0", "authority": "researchhub.data", "admission": "qualified_immutable_envelope"},
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- `research_pipeline` — 因子日 → 下一真实交易日 → 显式执行价 → 日末估值 → 成本后绩效(防前视编排)
|
||||
- `governed_pipeline` — 数据快照 → 因子版本 → 策略版本 → 回测运行 → 目标组合 → 风险决策 → Paper 订单意图;同时拥有输入/配置/重放血缘决定的 `BacktestRunRef`
|
||||
- `artifact` — 版本化、确定性、存储中立的完整 research run 事实表,以及只映射现有表的 `BacktestEvidenceManifest`
|
||||
- `portfolio_risk_contracts` — S3 证据闭合的 `PortfolioDecision` / `RiskAssessment` v1;独立复核 freshness、约束与 computation receipt,并复用既有标签安全风险分解
|
||||
- `attribution` — 基于实际成交后持仓的隔夜 / 日内 / 交易成本逐日收益归因与闭合审计
|
||||
- `metrics` — 绝对绩效 + 严格日期对齐的 TE / IR / alpha / beta 基准相对绩效
|
||||
- `factor_library` — 通用方法(turnover / winsorize / IC / OLS / jb_test)
|
||||
@@ -259,6 +260,111 @@ digest 等价,也不会把旧 run 静默升级为新合同。
|
||||
`LEGACY_EXPLORATORY`;不能隐式提升为 `CONTRACT_QUALIFIED`。所有资格均只描述离线证据闭合,
|
||||
不表示投资有效、组合获批、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` 不复制因子、回测、组合或执行算法,只编排现有能力并补充版本与风险契约。
|
||||
|
||||
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()
|
||||
for term in ("investment advice", "live order", "credentials", "source facts"):
|
||||
assert term in prohibited
|
||||
assert spec["authority"]["revision"] == 3
|
||||
assert spec["authority"]["revision"] == 4
|
||||
assert {
|
||||
(item["contract_id"], item["version"])
|
||||
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.backtest-run-ref", "1.0.0"),
|
||||
("researchhub.backtest-evidence-manifest", "1.0.0"),
|
||||
("researchhub.portfolio-decision", "1.0.0"),
|
||||
("researchhub.risk-assessment", "1.0.0"),
|
||||
}
|
||||
expected_paths = {
|
||||
"researchhub.factor-definition": "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-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 {
|
||||
@@ -48,6 +52,14 @@ def test_module_spec_declares_pure_research_engine_boundary() -> None:
|
||||
for item in spec["contracts"]["consumes"]
|
||||
)
|
||||
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(
|
||||
command["required"] and not command["network"]
|
||||
for command in spec["verification"]["commands"]
|
||||
|
||||
@@ -1,17 +1,428 @@
|
||||
"""S4.1 portfolio-decision and risk-assessment contract conformance."""
|
||||
|
||||
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 (
|
||||
ComputationReceipt,
|
||||
ConstraintSetV1,
|
||||
FreshnessPolicy,
|
||||
PortfolioDecision,
|
||||
PortfolioRiskContractError,
|
||||
PortfolioRiskContractErrorCode,
|
||||
ReceiptStatus,
|
||||
RiskAssessment,
|
||||
RiskAssessmentStatus,
|
||||
RiskFindingCode,
|
||||
assess_portfolio_risk,
|
||||
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(
|
||||
symbol is not None
|
||||
for symbol in (
|
||||
@@ -24,3 +435,570 @@ def test_s41_public_contract_surface_exists() -> None:
|
||||
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