Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7103594481 | ||
|
|
e7a6e20826 | ||
|
|
a724e1e57a | ||
|
|
78d65b4db0 | ||
|
|
598c2b92a2 | ||
|
|
62ed09842d | ||
|
|
e782e223f7 | ||
|
|
015c1a3602 |
+19
-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": 1, "effective_from": "2026-08-20T00:00:00+08:00"},
|
"authority": {"scope": "module_metadata", "subject": "quant_engine", "owner": "quant-engine-owner", "source": "MODULE_SPEC.yaml", "revision": 5, "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"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -18,13 +19,29 @@
|
|||||||
{"id": "factor-and-indicator-calculation", "summary": "Calculate reusable alpha factors and technical indicators from caller-supplied data.", "status": "operational"},
|
{"id": "factor-and-indicator-calculation", "summary": "Calculate reusable alpha factors and technical indicators from caller-supplied data.", "status": "operational"},
|
||||||
{"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 and performance-methodology evidence without recomputation, 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": [
|
||||||
{"asset_id": "quantitative-model-implementations", "kind": "model", "classification": "internal"},
|
{"asset_id": "quantitative-model-implementations", "kind": "model", "classification": "internal"},
|
||||||
{"asset_id": "simulation-and-metric-results", "kind": "artifact", "classification": "confidential"}
|
{"asset_id": "simulation-and-metric-results", "kind": "artifact", "classification": "confidential"}
|
||||||
]},
|
]},
|
||||||
"contracts": {"provides": [], "consumes": []},
|
"contracts": {
|
||||||
|
"provides": [
|
||||||
|
{"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.performance-evidence", "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"},
|
||||||
|
{"contract_id": "researchhub.data-foundation", "version": "1.0.0", "authority": "researchhub.data", "admission": "content_addressed_selected_views"}
|
||||||
|
]
|
||||||
|
},
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"agent_context": {
|
"agent_context": {
|
||||||
"default_entrypoints": [
|
"default_entrypoints": [
|
||||||
|
|||||||
@@ -19,13 +19,16 @@
|
|||||||
## 模块
|
## 模块
|
||||||
|
|
||||||
- `alpha_factors` — 158 alpha 公式 + 24 基础算子(移植自 qlib alpha158)
|
- `alpha_factors` — 158 alpha 公式 + 24 基础算子(移植自 qlib alpha158)
|
||||||
|
- `factor_contracts` — `FactorDefinition` / `FactorSetRef` v1 纯计算合同、严格 PIT/availability 输入准入与显式 legacy 投影
|
||||||
- `execution` — A 股长仓执行仿真(成本/滑点/现金约束)+ 稀疏调仓/完整交易日 Ledger + 可投影成交与 NAV 审计;T+1、涨跌停、成交量与价差提供独立约束函数
|
- `execution` — A 股长仓执行仿真(成本/滑点/现金约束)+ 稀疏调仓/完整交易日 Ledger + 可投影成交与 NAV 审计;T+1、涨跌停、成交量与价差提供独立约束函数
|
||||||
- `indicators` — 50+ 技术指标(MACD / KDJ / 布林 / ATR / ADX / 等)
|
- `indicators` — 50+ 技术指标(MACD / KDJ / 布林 / ATR / ADX / 等)
|
||||||
- `data_adapter` — 桥接 qtdb_pro 长表与新模块(rename / long-wide / 复权 / vwap 代理)
|
- `data_adapter` — 桥接 qtdb_pro 长表与新模块(rename / long-wide / 复权 / vwap 代理)
|
||||||
- `backtest` — weight-based 多日仿真(rebalance_table / compute_nav / compare_to_benchmark)
|
- `backtest` — weight-based 多日仿真(rebalance_table / compute_nav / compare_to_benchmark)
|
||||||
- `portfolio_construction` — 多期因子分数 → Top-K → 等权目标权重表
|
- `portfolio_construction` — 多期因子分数 → Top-K → 等权目标权重表
|
||||||
- `research_pipeline` — 因子日 → 下一真实交易日 → 显式执行价 → 日末估值 → 成本后绩效(防前视编排)
|
- `research_pipeline` — 因子日 → 下一真实交易日 → 显式执行价 → 日末估值 → 成本后绩效(防前视编排)
|
||||||
- `artifact` — 版本化、确定性、存储中立的完整 research run 事实表与 manifest
|
- `governed_pipeline` — 数据快照 → 因子版本 → 策略版本 → 回测运行 → 目标组合 → 风险决策 → Paper 订单意图;同时拥有输入/配置/重放血缘决定的 `BacktestRunRef`
|
||||||
|
- `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)
|
||||||
@@ -55,6 +58,9 @@ pytest # 单元测试
|
|||||||
pytest --cov=src # 覆盖率
|
pytest --cov=src # 覆盖率
|
||||||
mypy --strict src/ # 类型检查
|
mypy --strict src/ # 类型检查
|
||||||
ruff check src/ tests/ # lint
|
ruff check src/ tests/ # lint
|
||||||
|
|
||||||
|
# 无网络、无数据库、无券商的架构烟测
|
||||||
|
uv run python -m quant_engine.governed_pipeline
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
@@ -191,6 +197,220 @@ print(backtest.stats())
|
|||||||
print(backtest.benchmark_report())
|
print(backtest.benchmark_report())
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 因子/特征合同 v1
|
||||||
|
|
||||||
|
`quant_engine.factor_contracts` 提供 `researchhub.factor-definition` 与
|
||||||
|
`researchhub.factor-set-ref` `1.0.0`。合同使用受限 canonical JSON:只接受 ASCII
|
||||||
|
lower-snake-case object key、UTF-8 string、bool/null 和 safe integer;小数参数必须用显式
|
||||||
|
canonical decimal string。定义、输入映射、上游证据、输出 schema/content 和 lineage 的任一
|
||||||
|
语义变化都会产生新 identity。
|
||||||
|
|
||||||
|
创建 `FactorSetRef` 必须提供完整且可重算 identity 的 `DatasetSnapshotEnvelope` 与
|
||||||
|
`DataFoundationEnvelope`,不能用 ID 字符串或布尔值代替资格证明。每个因子输入都要映射到一个
|
||||||
|
实际选中的 `StandardizedViewRef`,schema 必须同时匹配定义和 view;未消费、缺失、重复或跨
|
||||||
|
snapshot/Foundation/PIT 的 view 都会失败关闭。snapshot PIT 可以早于 Foundation/view PIT,
|
||||||
|
但始终满足 knowledge ≤ snapshot PIT ≤ Foundation/view/FactorSet PIT ≤ evaluation。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from quant_engine.factor_contracts import (
|
||||||
|
DataFoundationEnvelope,
|
||||||
|
DatasetSnapshotEnvelope,
|
||||||
|
FactorDefinition,
|
||||||
|
FactorSetRef,
|
||||||
|
)
|
||||||
|
|
||||||
|
snapshot = DatasetSnapshotEnvelope.from_dict(dataset_snapshot_v1)
|
||||||
|
foundation = DataFoundationEnvelope.from_dict(data_foundation_v1)
|
||||||
|
|
||||||
|
# definition 必须是完整的 FactorDefinition;FactorSetRef.create 还要求显式 input bindings、
|
||||||
|
# view availability、output quality/coverage、canonical output bytes 和 immutable artifact ref。
|
||||||
|
factor_set = FactorSetRef.create(
|
||||||
|
definitions=(definition,),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
**explicit_factor_set_evidence,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`availability_mode="as_available"` 声明 source/view 和计算产物在历史 evaluation 前实际可用;
|
||||||
|
`"retrospective_replay"` 保留历史 evaluation,但要求真实 publication/view creation、compute 和
|
||||||
|
artifact 时间位于之后,并固定 `historical_availability="not_established"`。两种模式都不会授予
|
||||||
|
decision、real-data、production、paper 或 live readiness。
|
||||||
|
|
||||||
|
旧 `governed_pipeline.FactorVersion` 的四字段构造器、`factor_id@version`、run/target/risk/order
|
||||||
|
identity 均保持不变。迁移只能通过 content-addressed `LegacyFactorBinding`,再显式调用
|
||||||
|
`bind_legacy_factor()` 或 `project_legacy_factor()`;后者是有损投影,不表示旧 digest 与新定义
|
||||||
|
digest 等价,也不会把旧 run 静默升级为新合同。
|
||||||
|
|
||||||
|
## 回测引用与证据合同 v1
|
||||||
|
|
||||||
|
`quant_engine.governed_pipeline.BacktestRunRef` 是合格回测运行身份的唯一权威。`run_id` 只由
|
||||||
|
已验收的 Dataset Snapshot / Data Foundation / `FactorSetRef` 身份、universe、日历与公司行动
|
||||||
|
祖先、策略、执行/成本模型、严格整数 seed、完整代码提交、环境锁、配置、时间和重放血缘决定;
|
||||||
|
它不包含任何输出摘要。重放必须绑定直接父运行、连续 attempt 和不变的
|
||||||
|
`replay_spec_digest`,输入漂移或血缘环会失败关闭。
|
||||||
|
|
||||||
|
`quant_engine.artifact.BacktestEvidenceManifest` 只摘要 `ResearchRunArtifact` 已有的九张事实表。
|
||||||
|
固定 `offline_research_v1` 映射为 `run`、`signal`、`fill`、`position_nav`、`performance`、
|
||||||
|
`attribution`、`risk_snapshot` 与 `replay`;每张表都保留列模式摘要、行数和内容摘要,空 risk
|
||||||
|
表也必须有稳定 schema。`manifest_id` 由完整 RunRef 与输出证据决定,因此结果变化不会反向改变
|
||||||
|
`run_id`。当前 artifact 不拥有订单或拒绝事实,所以此画像明确不声明 `order` / `rejection`。
|
||||||
|
|
||||||
|
旧 `BacktestRun` 只能通过 `build_legacy_backtest_evidence_manifest()` 显式映射为
|
||||||
|
`LEGACY_EXPLORATORY`;不能隐式提升为 `CONTRACT_QUALIFIED`。所有资格均只描述离线证据闭合,
|
||||||
|
不表示投资有效、组合获批、Paper、生产或实盘就绪。
|
||||||
|
|
||||||
|
## 绩效证据与方法论合同 v1
|
||||||
|
|
||||||
|
`quant_engine.artifact.PerformanceEvidenceV1` 在现有计算和事实表之外增加一层只读、内容寻址的
|
||||||
|
owner 证据。`build_performance_evidence()` 只接受同一运行的完整 `ResearchRunArtifact`、
|
||||||
|
`BacktestRunRef` 与 `CONTRACT_QUALIFIED BacktestEvidenceManifest`;它核对全部 artifact 表、
|
||||||
|
performance 表和唯一行摘要,并绑定 artifact、row 与严格对齐 benchmark series 的独立摘要。
|
||||||
|
生产 builder 不重算、填补、重命名或覆盖任何绩效值。
|
||||||
|
|
||||||
|
方法论固定为日简单收益、252 期年化、绝对指标年化无风险利率 `0.0`、benchmark 日无风险利率
|
||||||
|
`0.0`,以及 benchmark 存在时的 `exact_session_index`。相对指标使用封闭 availability:无基准为
|
||||||
|
`benchmark_absent`;active variance、benchmark variance 或 alpha 几何年化域不足时分别使用
|
||||||
|
对应 `not_estimable_*` 原因。benchmark 存在时 tracking error 始终必须是有限非负值;null 不会
|
||||||
|
被转成零。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from quant_engine.artifact import build_performance_evidence
|
||||||
|
|
||||||
|
performance_evidence = build_performance_evidence(
|
||||||
|
artifact,
|
||||||
|
backtest_run_ref,
|
||||||
|
backtest_evidence_manifest,
|
||||||
|
)
|
||||||
|
canonical_bytes = performance_evidence.canonical_bytes()
|
||||||
|
```
|
||||||
|
|
||||||
|
该合同范围固定为 `offline_research_only`。它不授予排名、推荐、决策、发布、论文、Paper、生产、
|
||||||
|
实盘、交易或投资建议权限,也不包含原始参数、returns、NAV、benchmark series、表字节、存储
|
||||||
|
locator、URI 或凭证。
|
||||||
|
|
||||||
|
## 组合决策与风险评估合同 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` 不复制因子、回测、组合或执行算法,只编排现有能力并补充版本与风险契约。
|
||||||
|
调用方必须显式提供 `DatasetSnapshot`、`FactorVersion`、`StrategyVersion`、代码提交和
|
||||||
|
`RiskPolicy`。模块只会生成 `environment="paper"` 的订单意图,不连接数据库、数据供应商或
|
||||||
|
券商;风险决策为拒绝时,订单意图固定为空,直接调用创建函数也会失败关闭。
|
||||||
|
|
||||||
|
该切片对应 ResearchHub 架构的首个可执行验收链路:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DatasetSnapshot → FactorVersion → StrategyVersion → BacktestRun
|
||||||
|
→ PortfolioTarget → RiskDecision → PaperOrderIntent
|
||||||
|
```
|
||||||
|
|
||||||
|
平台总架构、五仓职责和十二层能力映射仍以 `research_platform/docs/architecture/` 为权威;
|
||||||
|
本仓只拥有纯计算与离线模拟合同。
|
||||||
|
|
||||||
## 与 research_results 的关系
|
## 与 research_results 的关系
|
||||||
|
|
||||||
`research_results` 依赖 `quant_engine`(通过 re-export 保持向后兼容):
|
`research_results` 依赖 `quant_engine`(通过 re-export 保持向后兼容):
|
||||||
|
|||||||
@@ -3539,6 +3539,95 @@ def evaluate_phase5_formula(name: str, **inputs: pd.Series) -> pd.Series:
|
|||||||
return function(*(inputs[field] for field in required_inputs))
|
return function(*(inputs[field] for field in required_inputs))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 6 formula contract: frozen alpha151-alpha158 surface ──────────────
|
||||||
|
|
||||||
|
# Phase 6 completes the versioned formula contract without mutating any
|
||||||
|
# earlier catalogue, digest, dispatch surface, or formula implementation.
|
||||||
|
ALPHA158_PHASE6_FORMULA_CONTRACT_VERSION = "1.0.0"
|
||||||
|
ALPHA158_PHASE6_FORMULA_CATALOG_SHA256 = (
|
||||||
|
"70ffae16ca6cbb59a1e8644f5cdeec1d291fa75ff8b5647fb534af0083408219"
|
||||||
|
)
|
||||||
|
|
||||||
|
_PHASE6_FORMULA_FUNCTIONS: dict[str, Callable[..., pd.Series]] = {
|
||||||
|
"alpha_151": alpha_151,
|
||||||
|
"alpha_152": alpha_152,
|
||||||
|
"alpha_153": alpha_153,
|
||||||
|
"alpha_154": alpha_154,
|
||||||
|
"alpha_155": alpha_155,
|
||||||
|
"alpha_156": alpha_156,
|
||||||
|
"alpha_157": alpha_157,
|
||||||
|
"alpha_158": alpha_158,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_phase6_formula_specs() -> dict[str, dict[str, Any]]:
|
||||||
|
specs: dict[str, dict[str, Any]] = {}
|
||||||
|
for alpha_id, function in _PHASE6_FORMULA_FUNCTIONS.items():
|
||||||
|
meta = ALPHA158_REGISTRY[alpha_id]
|
||||||
|
call_inputs = _phase3_call_inputs(function)
|
||||||
|
formula_inputs = _phase3_string_list(meta, "inputs", alpha_id)
|
||||||
|
input_category = _PHASE4_INPUT_CATEGORIES.get(len(call_inputs))
|
||||||
|
if input_category is None:
|
||||||
|
raise RuntimeError(f"unsupported formula input count for {alpha_id}")
|
||||||
|
specs[alpha_id] = {
|
||||||
|
"name": alpha_id,
|
||||||
|
"contract_version": ALPHA158_PHASE6_FORMULA_CONTRACT_VERSION,
|
||||||
|
"formula": meta["formula"],
|
||||||
|
"category": meta["category"],
|
||||||
|
"complexity": meta["complexity"],
|
||||||
|
"parameters": _phase3_string_list(meta, "params", alpha_id),
|
||||||
|
"description": meta["description"],
|
||||||
|
"references": _phase3_string_list(meta, "references", alpha_id),
|
||||||
|
"call_inputs": list(call_inputs),
|
||||||
|
"formula_inputs": formula_inputs,
|
||||||
|
"input_category": input_category,
|
||||||
|
}
|
||||||
|
return specs
|
||||||
|
|
||||||
|
|
||||||
|
ALPHA158_PHASE6_FORMULA_SPECS: Mapping[str, Mapping[str, Any]] = (
|
||||||
|
_freeze_phase3_formula_specs(_build_phase6_formula_specs())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_phase6_formulas() -> tuple[str, ...]:
|
||||||
|
"""Return the frozen alpha151-alpha158 formula IDs in stable order."""
|
||||||
|
return tuple(ALPHA158_PHASE6_FORMULA_SPECS)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_phase6_formula(name: str, **inputs: pd.Series) -> pd.Series:
|
||||||
|
"""Evaluate a Phase 6 formula with an exact, alignment-safe input contract."""
|
||||||
|
if name not in ALPHA158_PHASE6_FORMULA_SPECS:
|
||||||
|
raise KeyError(f"formula {name!r} not registered")
|
||||||
|
|
||||||
|
spec = ALPHA158_PHASE6_FORMULA_SPECS[name]
|
||||||
|
required_inputs = cast(tuple[str, ...], spec["call_inputs"])
|
||||||
|
missing_inputs = [field for field in required_inputs if field not in inputs]
|
||||||
|
unexpected_inputs = sorted(field for field in inputs if field not in required_inputs)
|
||||||
|
if missing_inputs or unexpected_inputs:
|
||||||
|
details: list[str] = []
|
||||||
|
if missing_inputs:
|
||||||
|
details.append(f"missing inputs {missing_inputs}")
|
||||||
|
if unexpected_inputs:
|
||||||
|
details.append(f"unexpected inputs {unexpected_inputs}")
|
||||||
|
raise ValueError(f"invalid inputs for {name}: {'; '.join(details)}")
|
||||||
|
|
||||||
|
for field in required_inputs:
|
||||||
|
if not isinstance(inputs[field], pd.Series):
|
||||||
|
raise TypeError(f"{field} must be a pandas Series")
|
||||||
|
|
||||||
|
primary_field = required_inputs[0]
|
||||||
|
primary = inputs[primary_field]
|
||||||
|
for field in required_inputs[1:]:
|
||||||
|
if len(inputs[field]) != len(primary):
|
||||||
|
raise ValueError(f"{field} length must match {primary_field}")
|
||||||
|
if not primary.index.equals(inputs[field].index):
|
||||||
|
raise ValueError(f"{field} index must align with {primary_field}")
|
||||||
|
|
||||||
|
function = _PHASE6_FORMULA_FUNCTIONS[name]
|
||||||
|
return function(*(inputs[field] for field in required_inputs))
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"rank",
|
"rank",
|
||||||
"delta",
|
"delta",
|
||||||
@@ -3587,6 +3676,11 @@ __all__ = [
|
|||||||
"ALPHA158_PHASE5_FORMULA_SPECS",
|
"ALPHA158_PHASE5_FORMULA_SPECS",
|
||||||
"list_phase5_formulas",
|
"list_phase5_formulas",
|
||||||
"evaluate_phase5_formula",
|
"evaluate_phase5_formula",
|
||||||
|
"ALPHA158_PHASE6_FORMULA_CONTRACT_VERSION",
|
||||||
|
"ALPHA158_PHASE6_FORMULA_CATALOG_SHA256",
|
||||||
|
"ALPHA158_PHASE6_FORMULA_SPECS",
|
||||||
|
"list_phase6_formulas",
|
||||||
|
"evaluate_phase6_formula",
|
||||||
"alpha_001",
|
"alpha_001",
|
||||||
"alpha_002",
|
"alpha_002",
|
||||||
"alpha_003",
|
"alpha_003",
|
||||||
|
|||||||
+2368
-19
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+17
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"run_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"replay_spec_digest": "sha256:20f07fcb526bc38b4ba3d71d6c3b00a8c63ad96cab0fecd869326503ab42d98b",
|
||||||
|
"manifest_id": "rhbacktestevidencev1:sha256:f94733e849433f62f3da1e1ec8999891b93d49c7d657832d64e64bef9e211117",
|
||||||
|
"evidence_digest": "sha256:f3913894d032c389c64eef59058b3cbc694cb9cc14ce9cee2699f0068200b650",
|
||||||
|
"table_content_digests": {
|
||||||
|
"run": "sha256:7d947ec93f714641669cbb14bd69dd8cf30387aabb7f3a086918fc2e878ab05c",
|
||||||
|
"signals": "sha256:72dc15064cc45d7c51d2dd4b8c3a6d8c7d4155232d5d70d3c9e7697fab70ce50",
|
||||||
|
"trades": "sha256:2b8b9321e7993941ac486cf50cab5b4c6425b2571a4701f0eb02273ec9a96c53",
|
||||||
|
"positions": "sha256:b456a48fab51742b05084ca6dcaf01c03dfe5b39ad71215ada070e9b1f59f7ea",
|
||||||
|
"nav": "sha256:25649efce860b76410f87dbd36c81085887620086dc0b48b07099918f65d9c78",
|
||||||
|
"performance": "sha256:0856439ea7ec84e38887ccfa0067324f2e9cd293543e4ef683b9c2beb9eb34cf",
|
||||||
|
"attribution": "sha256:ad0f12f668d0a2d9ae5b3636d29989ab4bafcfb95f5de77abeb52a6d9e95d366",
|
||||||
|
"attribution_daily": "sha256:d4459ad650f88871d7b1e40392033037b5818ef92fdf1ee021868929fc1455a5",
|
||||||
|
"risk": "sha256:4816dd4812b5ff2e97e74bf34ca2221bfde675b387a96e277683557f0ad7d975"
|
||||||
|
}
|
||||||
|
}
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
{
|
||||||
|
"dataset_snapshot": {
|
||||||
|
"contract_name": "researchhub.dataset-snapshot",
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
|
||||||
|
"descriptor": {
|
||||||
|
"dataset": {
|
||||||
|
"dataset_id": "rhdataset:market:0123456789abcdef0123456789abcdef",
|
||||||
|
"dataset_kind": "market",
|
||||||
|
"record_schema_version": "1.0.0",
|
||||||
|
"dimensions": ["instrument_id", "effective_time"]
|
||||||
|
},
|
||||||
|
"published_at": "2026-01-02T07:05:00Z",
|
||||||
|
"time_semantics": {
|
||||||
|
"effective_time": {
|
||||||
|
"start_inclusive": "2026-01-02T07:00:00Z",
|
||||||
|
"end_inclusive": "2026-01-02T07:00:00Z"
|
||||||
|
},
|
||||||
|
"knowledge_time": {
|
||||||
|
"start_inclusive": "2026-01-02T07:01:00Z",
|
||||||
|
"end_inclusive": "2026-01-02T07:01:00Z"
|
||||||
|
},
|
||||||
|
"pit_cutoff": "2026-01-02T07:01:00Z"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"digest_algorithm": "sha256",
|
||||||
|
"canonicalization": "RFC8785",
|
||||||
|
"record_order": "canonical-record-byte-order",
|
||||||
|
"content_digest": "sha256:44ea11ba64dc2e6fd55c6d8e038c5edc84ee38d6fd46e38b15a1d5409662a020",
|
||||||
|
"logical_manifest": {
|
||||||
|
"record_count": 2,
|
||||||
|
"chunks": [
|
||||||
|
{
|
||||||
|
"chunk_index": 0,
|
||||||
|
"content_digest": "sha256:44ea11ba64dc2e6fd55c6d8e038c5edc84ee38d6fd46e38b15a1d5409662a020",
|
||||||
|
"record_count": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"manifest_digest": "sha256:d991bb2f8f6b80525f93c51e0b371213a3ed4649dffb073ed4605bfbd32349bd",
|
||||||
|
"record_count": 2
|
||||||
|
},
|
||||||
|
"lineage": {
|
||||||
|
"publisher": {"id": "researchhub.data", "version": "1.0.0"},
|
||||||
|
"transformation": {
|
||||||
|
"id": "rhtransform:00112233445566778899aabbccddeeff",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"upstream_snapshot_ids": [],
|
||||||
|
"upstream_content_digests": []
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"status": "passed",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"check_id": "completeness",
|
||||||
|
"status": "passed",
|
||||||
|
"severity": "blocking",
|
||||||
|
"evidence_digest": "sha256:876fc2fcc6414ddc3f824a47f475d34c82a53d2bda5dc72a234a3f3164e8e2ec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check_id": "pit_time_integrity",
|
||||||
|
"status": "passed",
|
||||||
|
"severity": "blocking",
|
||||||
|
"evidence_digest": "sha256:90a6cc46b9f2ab317a1c6d14dc173784e19b5621cd7fe956e8f41338cbdc5944"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"qualification": {
|
||||||
|
"status": "qualified",
|
||||||
|
"policy_id": "researchhub.dataset-snapshot.pit",
|
||||||
|
"policy_version": "1.0.0",
|
||||||
|
"evaluated_at": "2026-01-02T07:04:00Z",
|
||||||
|
"evidence_digest": "sha256:e192462f9022f2b477f73cdbe9e6c9f891ebcfdc2b4ed4f8ddd7b1ff107ee6a6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data_foundation": {
|
||||||
|
"contract_name": "researchhub.data-foundation",
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"foundation_id": "rhdfv1:sha256:d848237ab753ee9432ae78ec1f93b6ac45c8072d6694023b7f288203daf9d838",
|
||||||
|
"dataset_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
|
||||||
|
"pit_cutoff": "2026-01-03T00:00:00Z",
|
||||||
|
"instrument_routes": [
|
||||||
|
{
|
||||||
|
"route_revision_id": "rhroutev1:sha256:ca67013250e28ab4cce16570607379a8792400e62415ee6cb71c75508e2f3d86",
|
||||||
|
"instrument_id": "rhinstrument:0123456789abcdef0123456789abcdef",
|
||||||
|
"revision_number": 1,
|
||||||
|
"symbol": "600000",
|
||||||
|
"mic": "XSHG",
|
||||||
|
"currency": "CNY",
|
||||||
|
"asset_class": "equity",
|
||||||
|
"instrument_type": "stock",
|
||||||
|
"calendar_id": "rhcalendar:11112222333344445555666677778888",
|
||||||
|
"effective_from": "2020-01-01T00:00:00Z",
|
||||||
|
"knowledge_time": "2026-01-01T07:00:00Z",
|
||||||
|
"evidence_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trading_calendar_revisions": [
|
||||||
|
{
|
||||||
|
"calendar_revision_id": "rhcalv1:sha256:1f4ca22557063389badd669cf774bb35234066e847646682dccfe411e252a078",
|
||||||
|
"calendar_id": "rhcalendar:11112222333344445555666677778888",
|
||||||
|
"session_date": "2026-01-02",
|
||||||
|
"revision_number": 1,
|
||||||
|
"status": "open",
|
||||||
|
"sessions": [
|
||||||
|
{"opens_at": "2026-01-02T01:30:00Z", "closes_at": "2026-01-02T07:00:00Z"}
|
||||||
|
],
|
||||||
|
"knowledge_time": "2026-01-01T08:00:00Z",
|
||||||
|
"evidence_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"corporate_action_revisions": [
|
||||||
|
{
|
||||||
|
"action_revision_id": "rhcav1:sha256:0f947df29f152bfa2c6ab0da7a0d464670c7ad2526cd10f2a7939b42fee5c275",
|
||||||
|
"action_id": "rhaction:99998888777766665555444433332222",
|
||||||
|
"instrument_id": "rhinstrument:0123456789abcdef0123456789abcdef",
|
||||||
|
"revision_number": 1,
|
||||||
|
"action_type": "cash_dividend",
|
||||||
|
"status": "confirmed",
|
||||||
|
"effective_time": "2026-01-02T00:00:00Z",
|
||||||
|
"knowledge_time": "2026-01-01T09:00:00Z",
|
||||||
|
"terms_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||||
|
"evidence_digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"standardized_views": [
|
||||||
|
{
|
||||||
|
"view_ref_id": "rhviewrefv1:sha256:bf776bcd26d940fafde1d650776a5505fb3fe8b5b068c351622bf2c42385629c",
|
||||||
|
"view_id": "rhview:abcdef0123456789abcdef0123456789",
|
||||||
|
"view_version": "1.0.0",
|
||||||
|
"dataset_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
|
||||||
|
"pit_cutoff": "2026-01-03T00:00:00Z",
|
||||||
|
"schema_digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||||
|
"content_digest": "sha256:123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0",
|
||||||
|
"transformation_digest": "sha256:23456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01",
|
||||||
|
"instrument_route_revision_ids": [
|
||||||
|
"rhroutev1:sha256:ca67013250e28ab4cce16570607379a8792400e62415ee6cb71c75508e2f3d86"
|
||||||
|
],
|
||||||
|
"trading_calendar_revision_ids": [
|
||||||
|
"rhcalv1:sha256:1f4ca22557063389badd669cf774bb35234066e847646682dccfe411e252a078"
|
||||||
|
],
|
||||||
|
"corporate_action_revision_ids": [
|
||||||
|
"rhcav1:sha256:0f947df29f152bfa2c6ab0da7a0d464670c7ad2526cd10f2a7939b42fee5c275"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"revision_lineage": [
|
||||||
|
{
|
||||||
|
"revision_kind": "instrument_route",
|
||||||
|
"revision_id": "rhroutev1:sha256:ca67013250e28ab4cce16570607379a8792400e62415ee6cb71c75508e2f3d86",
|
||||||
|
"revision_number": 1,
|
||||||
|
"knowledge_time": "2026-01-01T07:00:00Z",
|
||||||
|
"evidence_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"revision_kind": "trading_calendar",
|
||||||
|
"revision_id": "rhcalv1:sha256:1f4ca22557063389badd669cf774bb35234066e847646682dccfe411e252a078",
|
||||||
|
"revision_number": 1,
|
||||||
|
"knowledge_time": "2026-01-01T08:00:00Z",
|
||||||
|
"evidence_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"revision_kind": "corporate_action",
|
||||||
|
"revision_id": "rhcav1:sha256:0f947df29f152bfa2c6ab0da7a0d464670c7ad2526cd10f2a7939b42fee5c275",
|
||||||
|
"revision_number": 1,
|
||||||
|
"knowledge_time": "2026-01-01T09:00:00Z",
|
||||||
|
"evidence_digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"readiness": {
|
||||||
|
"evidence_scope": "synthetic_fixture",
|
||||||
|
"contract_validation": {
|
||||||
|
"status": "validated",
|
||||||
|
"evidence_digests": [
|
||||||
|
"sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"real_data_validation": {"status": "not_validated", "evidence_digests": []},
|
||||||
|
"production_validation": {"status": "not_validated", "evidence_digests": []},
|
||||||
|
"live_validation": {"status": "not_validated", "evidence_digests": []}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"columns": ["evaluation_at", "factor_id", "instrument_id", "value"],
|
||||||
|
"schema_version": "1.0.0"
|
||||||
|
},
|
||||||
|
"output_content": {
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"evaluation_at": "2026-01-03T11:00:00Z",
|
||||||
|
"factor_id": "alpha_005",
|
||||||
|
"instrument_id": "rhinstrument:0123456789abcdef0123456789abcdef",
|
||||||
|
"value": "0.125"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expected": {
|
||||||
|
"definition_id": "rhfactorv1:sha256:978fb8000d318373844a5e044ca14bf377e01ebe8d85964b826ecd2af9085ce9",
|
||||||
|
"input_schema_digest": "sha256:4501aeab99b4bcc25a1b8813ebe197fb498053fd710d73746bf20fc8eeb4bfa7",
|
||||||
|
"factor_set_id": "rhfactorsetv1:sha256:e9339581cf569e92459f672e8081337712e7bf98e58ad42d60d7ed13f9b5a021",
|
||||||
|
"output_artifact_id": "rhfactoroutputv1:sha256:a4803b5ff66d12d3a0e7e8e5b8cca953cbc137e2bf41514b7c4e5f05da5ee68b",
|
||||||
|
"legacy_binding_id": "rhlegacyfactorv1:sha256:541bc5a9469f9c8e4c2d696a9972fc5f2e6e2bef218b86f728823994b915dede"
|
||||||
|
}
|
||||||
|
}
|
||||||
+871
@@ -0,0 +1,871 @@
|
|||||||
|
{
|
||||||
|
"cases": {
|
||||||
|
"absent": {
|
||||||
|
"artifact_available_at": "2026-01-08T02:05:00Z",
|
||||||
|
"authority": "quant_engine",
|
||||||
|
"backtest_evidence_manifest_document_sha256": "sha256:b7e9139e021de3122bee9376a8c8387fca3db75fe2a698adccf33471caf971d2",
|
||||||
|
"backtest_evidence_manifest_evidence_digest": "sha256:fae26a93d754e98f437bf9e4b635fc1cdc4f85d004a4bde396823b52e2115de5",
|
||||||
|
"backtest_evidence_manifest_id": "rhbacktestevidencev1:sha256:3f67fcad23c75684ffeb325d8405b2f81139a732e237d30fbb30d23f91e32726",
|
||||||
|
"backtest_evidence_qualification": "contract_qualified",
|
||||||
|
"backtest_run_ref_document_sha256": "sha256:6a798adb3e0568aca84ed3e3a285b92d181ec569462fc003952a8c0d8382ae3a",
|
||||||
|
"backtest_run_ref_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"benchmark_alignment_policy": "none",
|
||||||
|
"benchmark_id": "",
|
||||||
|
"benchmark_series_digest": null,
|
||||||
|
"calendar": "CN-A",
|
||||||
|
"code_revision": "dddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
"configuration_digest": "sha256:d28b49ea1bebc78d0023667f4fb9990b1fb45807176c2193b458525def056f4d",
|
||||||
|
"cost_model_digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888",
|
||||||
|
"cost_model_version": "1.0.0",
|
||||||
|
"dataset_content_digest": "sha256:44ea11ba64dc2e6fd55c6d8e038c5edc84ee38d6fd46e38b15a1d5409662a020",
|
||||||
|
"dataset_manifest_digest": "sha256:d991bb2f8f6b80525f93c51e0b371213a3ed4649dffb073ed4605bfbd32349bd",
|
||||||
|
"dataset_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
|
||||||
|
"document_sha256": "sha256:729852af307fd4a94ac566ae45df2e57b3d45c4fbdf45820351b5ef19cdf0ad3",
|
||||||
|
"end_date": "2026-01-08",
|
||||||
|
"environment_lock_digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999",
|
||||||
|
"execution_model_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777",
|
||||||
|
"execution_model_version": "1.0.0",
|
||||||
|
"factor_output_content_digest": "sha256:d78751460dc27fc796163c462d946924ce2bcb45dcfceb5e4b77f76093befc9e",
|
||||||
|
"factor_set_digest": "sha256:e9339581cf569e92459f672e8081337712e7bf98e58ad42d60d7ed13f9b5a021",
|
||||||
|
"factor_set_id": "rhfactorsetv1:sha256:e9339581cf569e92459f672e8081337712e7bf98e58ad42d60d7ed13f9b5a021",
|
||||||
|
"foundation_digest": "sha256:d848237ab753ee9432ae78ec1f93b6ac45c8072d6694023b7f288203daf9d838",
|
||||||
|
"foundation_id": "rhdfv1:sha256:d848237ab753ee9432ae78ec1f93b6ac45c8072d6694023b7f288203daf9d838",
|
||||||
|
"frequency": "1d",
|
||||||
|
"methodology": {
|
||||||
|
"alpha": "daily_ols_intercept_geometric_annualization",
|
||||||
|
"annual_risk_free": 0.0,
|
||||||
|
"annualized_return": "geometric_compound",
|
||||||
|
"annualized_volatility": "sample_std_sqrt_periods",
|
||||||
|
"benchmark_alignment": "none",
|
||||||
|
"benchmark_risk_free_daily": 0.0,
|
||||||
|
"beta": "sample_covariance_over_sample_variance",
|
||||||
|
"calmar_ratio": "unadjusted_annualized_return_over_absolute_maximum_drawdown",
|
||||||
|
"code_revision": "dddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
"implementation_module": "quant_engine.metrics",
|
||||||
|
"implementation_version": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"information_ratio": "mean_active_over_sample_std_active_sqrt_periods",
|
||||||
|
"maximum_drawdown": "non_positive_peak_to_trough_ratio_with_initial_nav_one",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"periods_per_year": 252,
|
||||||
|
"return_type": "simple",
|
||||||
|
"sharpe_ratio": "annualized_return_minus_annual_risk_free_over_annualized_volatility",
|
||||||
|
"sortino_ratio": "annualized_return_minus_annual_risk_free_over_root_mean_square_negative_returns_sqrt_periods",
|
||||||
|
"source_frequency": "1d",
|
||||||
|
"total_return": "final_nav_minus_one",
|
||||||
|
"tracking_error": "sample_std_active_return_sqrt_periods",
|
||||||
|
"win_rate": "positive_daily_return_count_over_observation_count"
|
||||||
|
},
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "total_return",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "total_ret",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.575
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "annualized_return",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "ann_ret",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 2683336646708.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "annualized_volatility",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "ann_volatility",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 1.38901943830891
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "sharpe_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "sharpe",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 1931820803008.3313
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "sortino_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "sortino",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "maximum_drawdown",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "max_dd",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "calmar_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "calmar",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "win_rate",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "win_rate",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "benchmark_absent",
|
||||||
|
"key": "tracking_error",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "tracking_error",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "benchmark_absent",
|
||||||
|
"key": "information_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "ir",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "benchmark_absent",
|
||||||
|
"key": "alpha",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "alpha",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "benchmark_absent",
|
||||||
|
"key": "beta",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "beta",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "trade_count",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "n_trades",
|
||||||
|
"unit": "count",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "day_count",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "n_days",
|
||||||
|
"unit": "count",
|
||||||
|
"value": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"performance_evidence_id": "rhperformanceevidencev1:sha256:d735abe76c28db429e107cb5ba6a1929c2c0f2084a6c4ac41b38ffbc02b2d2bd",
|
||||||
|
"performance_row_digest": "sha256:4e6329b0db4731513fe793491a163dd358e3a6f7baa84d3c94a4d4d2355c7a7d",
|
||||||
|
"performance_table_content_digest": "sha256:074b8511ff9a2154235e4dde8bac300658cb69f900e49f332d61560b82be9af5",
|
||||||
|
"performance_table_logical_name": "performance",
|
||||||
|
"performance_table_row_count": 1,
|
||||||
|
"performance_table_schema_digest": "sha256:16cef93a679761103ae405e622b7929abbfe07115bf276be4f164da5a16128d0",
|
||||||
|
"research_artifact_content_digest": "sha256:ad85ee1317bd8ce5fbef8fddc268b91be8678701ba5b8fbf6d61bf670f918bf9",
|
||||||
|
"research_artifact_schema_version": "1.1.0",
|
||||||
|
"run_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"schema_version": "researchhub.performance-evidence.v1",
|
||||||
|
"scope": "offline_research_only",
|
||||||
|
"start_date": "2026-01-05",
|
||||||
|
"strategy_digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666",
|
||||||
|
"strategy_id": "alpha-top1",
|
||||||
|
"strategy_version": "1.0.0",
|
||||||
|
"timezone": "Asia/Shanghai"
|
||||||
|
},
|
||||||
|
"estimable": {
|
||||||
|
"artifact_available_at": "2026-01-08T02:05:00Z",
|
||||||
|
"authority": "quant_engine",
|
||||||
|
"backtest_evidence_manifest_document_sha256": "sha256:e2e0e63fb4c733d2dba9a290511b6c8b0132bfe877dfe89ee7e614b74b87bc51",
|
||||||
|
"backtest_evidence_manifest_evidence_digest": "sha256:f3913894d032c389c64eef59058b3cbc694cb9cc14ce9cee2699f0068200b650",
|
||||||
|
"backtest_evidence_manifest_id": "rhbacktestevidencev1:sha256:f94733e849433f62f3da1e1ec8999891b93d49c7d657832d64e64bef9e211117",
|
||||||
|
"backtest_evidence_qualification": "contract_qualified",
|
||||||
|
"backtest_run_ref_document_sha256": "sha256:6a798adb3e0568aca84ed3e3a285b92d181ec569462fc003952a8c0d8382ae3a",
|
||||||
|
"backtest_run_ref_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"benchmark_alignment_policy": "exact_session_index",
|
||||||
|
"benchmark_id": "000300.SH",
|
||||||
|
"benchmark_series_digest": "sha256:1c564e4d12af0d59b1fd707d7816da2895238969d9207470984db63cfeac423a",
|
||||||
|
"calendar": "CN-A",
|
||||||
|
"code_revision": "dddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
"configuration_digest": "sha256:d28b49ea1bebc78d0023667f4fb9990b1fb45807176c2193b458525def056f4d",
|
||||||
|
"cost_model_digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888",
|
||||||
|
"cost_model_version": "1.0.0",
|
||||||
|
"dataset_content_digest": "sha256:44ea11ba64dc2e6fd55c6d8e038c5edc84ee38d6fd46e38b15a1d5409662a020",
|
||||||
|
"dataset_manifest_digest": "sha256:d991bb2f8f6b80525f93c51e0b371213a3ed4649dffb073ed4605bfbd32349bd",
|
||||||
|
"dataset_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
|
||||||
|
"document_sha256": "sha256:a6a1a3313f511505c0cf57fddeebdf20a3b0f83a2958f31332d5597d172165e7",
|
||||||
|
"end_date": "2026-01-08",
|
||||||
|
"environment_lock_digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999",
|
||||||
|
"execution_model_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777",
|
||||||
|
"execution_model_version": "1.0.0",
|
||||||
|
"factor_output_content_digest": "sha256:d78751460dc27fc796163c462d946924ce2bcb45dcfceb5e4b77f76093befc9e",
|
||||||
|
"factor_set_digest": "sha256:e9339581cf569e92459f672e8081337712e7bf98e58ad42d60d7ed13f9b5a021",
|
||||||
|
"factor_set_id": "rhfactorsetv1:sha256:e9339581cf569e92459f672e8081337712e7bf98e58ad42d60d7ed13f9b5a021",
|
||||||
|
"foundation_digest": "sha256:d848237ab753ee9432ae78ec1f93b6ac45c8072d6694023b7f288203daf9d838",
|
||||||
|
"foundation_id": "rhdfv1:sha256:d848237ab753ee9432ae78ec1f93b6ac45c8072d6694023b7f288203daf9d838",
|
||||||
|
"frequency": "1d",
|
||||||
|
"methodology": {
|
||||||
|
"alpha": "daily_ols_intercept_geometric_annualization",
|
||||||
|
"annual_risk_free": 0.0,
|
||||||
|
"annualized_return": "geometric_compound",
|
||||||
|
"annualized_volatility": "sample_std_sqrt_periods",
|
||||||
|
"benchmark_alignment": "exact_session_index",
|
||||||
|
"benchmark_risk_free_daily": 0.0,
|
||||||
|
"beta": "sample_covariance_over_sample_variance",
|
||||||
|
"calmar_ratio": "unadjusted_annualized_return_over_absolute_maximum_drawdown",
|
||||||
|
"code_revision": "dddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
"implementation_module": "quant_engine.metrics",
|
||||||
|
"implementation_version": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"information_ratio": "mean_active_over_sample_std_active_sqrt_periods",
|
||||||
|
"maximum_drawdown": "non_positive_peak_to_trough_ratio_with_initial_nav_one",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"periods_per_year": 252,
|
||||||
|
"return_type": "simple",
|
||||||
|
"sharpe_ratio": "annualized_return_minus_annual_risk_free_over_annualized_volatility",
|
||||||
|
"sortino_ratio": "annualized_return_minus_annual_risk_free_over_root_mean_square_negative_returns_sqrt_periods",
|
||||||
|
"source_frequency": "1d",
|
||||||
|
"total_return": "final_nav_minus_one",
|
||||||
|
"tracking_error": "sample_std_active_return_sqrt_periods",
|
||||||
|
"win_rate": "positive_daily_return_count_over_observation_count"
|
||||||
|
},
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "total_return",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "total_ret",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.575
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "annualized_return",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "ann_ret",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 2683336646708.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "annualized_volatility",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "ann_volatility",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 1.38901943830891
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "sharpe_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "sharpe",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 1931820803008.3313
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "sortino_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "sortino",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "maximum_drawdown",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "max_dd",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "calmar_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "calmar",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "win_rate",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "win_rate",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "tracking_error",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "tracking_error",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 1.3032171729991897
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "information_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "ir",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 22.801264912443322
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "alpha",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "alpha",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 123663320625.66454
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "beta",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "beta",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 3.2500000000000013
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "trade_count",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "n_trades",
|
||||||
|
"unit": "count",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "day_count",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "n_days",
|
||||||
|
"unit": "count",
|
||||||
|
"value": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"performance_evidence_id": "rhperformanceevidencev1:sha256:f1f72c34d7427bd0d2e273a66b9677d112aa51c2417ea1cda8245e51e334e4f6",
|
||||||
|
"performance_row_digest": "sha256:bad2b506679a6ca13d80ab00859a442ba692f3872d42b0b45bbc3b1d6ffe72a4",
|
||||||
|
"performance_table_content_digest": "sha256:0856439ea7ec84e38887ccfa0067324f2e9cd293543e4ef683b9c2beb9eb34cf",
|
||||||
|
"performance_table_logical_name": "performance",
|
||||||
|
"performance_table_row_count": 1,
|
||||||
|
"performance_table_schema_digest": "sha256:16cef93a679761103ae405e622b7929abbfe07115bf276be4f164da5a16128d0",
|
||||||
|
"research_artifact_content_digest": "sha256:6ec566d73da3966d3d5f946313e2b0181993374c9f4ceb4b56e165cc6793d382",
|
||||||
|
"research_artifact_schema_version": "1.1.0",
|
||||||
|
"run_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"schema_version": "researchhub.performance-evidence.v1",
|
||||||
|
"scope": "offline_research_only",
|
||||||
|
"start_date": "2026-01-05",
|
||||||
|
"strategy_digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666",
|
||||||
|
"strategy_id": "alpha-top1",
|
||||||
|
"strategy_version": "1.0.0",
|
||||||
|
"timezone": "Asia/Shanghai"
|
||||||
|
},
|
||||||
|
"zero_active_variance": {
|
||||||
|
"artifact_available_at": "2026-01-08T02:05:00Z",
|
||||||
|
"authority": "quant_engine",
|
||||||
|
"backtest_evidence_manifest_document_sha256": "sha256:8a13212575154d97f85011ed54ed3bb588f73a691926b3ca03b360d563e9fc29",
|
||||||
|
"backtest_evidence_manifest_evidence_digest": "sha256:8c0c2052ab93aa920254bc7f4d89435d839b7150ecaaaa798703e89ddf876ec1",
|
||||||
|
"backtest_evidence_manifest_id": "rhbacktestevidencev1:sha256:699ad35ee30a294082c607c40617097aa90d5d58343e7d8c8185d8a53f52596c",
|
||||||
|
"backtest_evidence_qualification": "contract_qualified",
|
||||||
|
"backtest_run_ref_document_sha256": "sha256:6a798adb3e0568aca84ed3e3a285b92d181ec569462fc003952a8c0d8382ae3a",
|
||||||
|
"backtest_run_ref_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"benchmark_alignment_policy": "exact_session_index",
|
||||||
|
"benchmark_id": "000300.SH",
|
||||||
|
"benchmark_series_digest": "sha256:7668b77ac689cd542005fae42cb2abd48fdf772d2f982086dca409606653e6ac",
|
||||||
|
"calendar": "CN-A",
|
||||||
|
"code_revision": "dddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
"configuration_digest": "sha256:d28b49ea1bebc78d0023667f4fb9990b1fb45807176c2193b458525def056f4d",
|
||||||
|
"cost_model_digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888",
|
||||||
|
"cost_model_version": "1.0.0",
|
||||||
|
"dataset_content_digest": "sha256:44ea11ba64dc2e6fd55c6d8e038c5edc84ee38d6fd46e38b15a1d5409662a020",
|
||||||
|
"dataset_manifest_digest": "sha256:d991bb2f8f6b80525f93c51e0b371213a3ed4649dffb073ed4605bfbd32349bd",
|
||||||
|
"dataset_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
|
||||||
|
"document_sha256": "sha256:44a04be0105f29e1cbf8d2430b6b0eb084853d428bc2ccd5834dfdbcf9482bd7",
|
||||||
|
"end_date": "2026-01-08",
|
||||||
|
"environment_lock_digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999",
|
||||||
|
"execution_model_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777",
|
||||||
|
"execution_model_version": "1.0.0",
|
||||||
|
"factor_output_content_digest": "sha256:d78751460dc27fc796163c462d946924ce2bcb45dcfceb5e4b77f76093befc9e",
|
||||||
|
"factor_set_digest": "sha256:e9339581cf569e92459f672e8081337712e7bf98e58ad42d60d7ed13f9b5a021",
|
||||||
|
"factor_set_id": "rhfactorsetv1:sha256:e9339581cf569e92459f672e8081337712e7bf98e58ad42d60d7ed13f9b5a021",
|
||||||
|
"foundation_digest": "sha256:d848237ab753ee9432ae78ec1f93b6ac45c8072d6694023b7f288203daf9d838",
|
||||||
|
"foundation_id": "rhdfv1:sha256:d848237ab753ee9432ae78ec1f93b6ac45c8072d6694023b7f288203daf9d838",
|
||||||
|
"frequency": "1d",
|
||||||
|
"methodology": {
|
||||||
|
"alpha": "daily_ols_intercept_geometric_annualization",
|
||||||
|
"annual_risk_free": 0.0,
|
||||||
|
"annualized_return": "geometric_compound",
|
||||||
|
"annualized_volatility": "sample_std_sqrt_periods",
|
||||||
|
"benchmark_alignment": "exact_session_index",
|
||||||
|
"benchmark_risk_free_daily": 0.0,
|
||||||
|
"beta": "sample_covariance_over_sample_variance",
|
||||||
|
"calmar_ratio": "unadjusted_annualized_return_over_absolute_maximum_drawdown",
|
||||||
|
"code_revision": "dddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
"implementation_module": "quant_engine.metrics",
|
||||||
|
"implementation_version": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"information_ratio": "mean_active_over_sample_std_active_sqrt_periods",
|
||||||
|
"maximum_drawdown": "non_positive_peak_to_trough_ratio_with_initial_nav_one",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"periods_per_year": 252,
|
||||||
|
"return_type": "simple",
|
||||||
|
"sharpe_ratio": "annualized_return_minus_annual_risk_free_over_annualized_volatility",
|
||||||
|
"sortino_ratio": "annualized_return_minus_annual_risk_free_over_root_mean_square_negative_returns_sqrt_periods",
|
||||||
|
"source_frequency": "1d",
|
||||||
|
"total_return": "final_nav_minus_one",
|
||||||
|
"tracking_error": "sample_std_active_return_sqrt_periods",
|
||||||
|
"win_rate": "positive_daily_return_count_over_observation_count"
|
||||||
|
},
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "total_return",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "total_ret",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.575
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "annualized_return",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "ann_ret",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 2683336646708.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "annualized_volatility",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "ann_volatility",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 1.38901943830891
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "sharpe_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "sharpe",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 1931820803008.3313
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "sortino_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "sortino",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "maximum_drawdown",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "max_dd",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "calmar_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "calmar",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "win_rate",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "win_rate",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "tracking_error",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "tracking_error",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "not_estimable_active_variance",
|
||||||
|
"key": "information_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "ir",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "alpha",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "alpha",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "beta",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "beta",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 1.0000000000000002
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "trade_count",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "n_trades",
|
||||||
|
"unit": "count",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "day_count",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "n_days",
|
||||||
|
"unit": "count",
|
||||||
|
"value": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"performance_evidence_id": "rhperformanceevidencev1:sha256:e6730d25d90c85a6370adb72bc45c1bf505235f77b72521dd8197898ca369fe5",
|
||||||
|
"performance_row_digest": "sha256:6528c47fa9416e350aa5010477dbd8c83a35cecf87aa5bc413cec95a9765114d",
|
||||||
|
"performance_table_content_digest": "sha256:7d57a966431c68093a9f6193ae62f79a63d33832f11dd68524ed9bcd2cb6257c",
|
||||||
|
"performance_table_logical_name": "performance",
|
||||||
|
"performance_table_row_count": 1,
|
||||||
|
"performance_table_schema_digest": "sha256:16cef93a679761103ae405e622b7929abbfe07115bf276be4f164da5a16128d0",
|
||||||
|
"research_artifact_content_digest": "sha256:b17ab9e158a7ceeb5107f6fe8a61f332009c314186f3436d4750aa44d6ca3856",
|
||||||
|
"research_artifact_schema_version": "1.1.0",
|
||||||
|
"run_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"schema_version": "researchhub.performance-evidence.v1",
|
||||||
|
"scope": "offline_research_only",
|
||||||
|
"start_date": "2026-01-05",
|
||||||
|
"strategy_digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666",
|
||||||
|
"strategy_id": "alpha-top1",
|
||||||
|
"strategy_version": "1.0.0",
|
||||||
|
"timezone": "Asia/Shanghai"
|
||||||
|
},
|
||||||
|
"zero_benchmark_variance": {
|
||||||
|
"artifact_available_at": "2026-01-08T02:05:00Z",
|
||||||
|
"authority": "quant_engine",
|
||||||
|
"backtest_evidence_manifest_document_sha256": "sha256:2389b804f5d8d20a37b31f596b52892484777c5da799dd68865a082ee90e6e5b",
|
||||||
|
"backtest_evidence_manifest_evidence_digest": "sha256:54bb315bc1940ef6df79999cf03ebb8d10defaf526bf7802d63da33f612520ec",
|
||||||
|
"backtest_evidence_manifest_id": "rhbacktestevidencev1:sha256:ad9559e1f8feca28bb310765216a975935f05f51057139c2d02fd2fe7dfe501e",
|
||||||
|
"backtest_evidence_qualification": "contract_qualified",
|
||||||
|
"backtest_run_ref_document_sha256": "sha256:6a798adb3e0568aca84ed3e3a285b92d181ec569462fc003952a8c0d8382ae3a",
|
||||||
|
"backtest_run_ref_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"benchmark_alignment_policy": "exact_session_index",
|
||||||
|
"benchmark_id": "000300.SH",
|
||||||
|
"benchmark_series_digest": "sha256:d0f1e954d796e95254bcccc893d6e3a8f9d541c3028b88b2c0b24c1658bb2408",
|
||||||
|
"calendar": "CN-A",
|
||||||
|
"code_revision": "dddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
"configuration_digest": "sha256:d28b49ea1bebc78d0023667f4fb9990b1fb45807176c2193b458525def056f4d",
|
||||||
|
"cost_model_digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888",
|
||||||
|
"cost_model_version": "1.0.0",
|
||||||
|
"dataset_content_digest": "sha256:44ea11ba64dc2e6fd55c6d8e038c5edc84ee38d6fd46e38b15a1d5409662a020",
|
||||||
|
"dataset_manifest_digest": "sha256:d991bb2f8f6b80525f93c51e0b371213a3ed4649dffb073ed4605bfbd32349bd",
|
||||||
|
"dataset_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
|
||||||
|
"document_sha256": "sha256:077f3ec84802aff2f046365a3a5f61f475b1ad63ec05e96742844bc531bcb551",
|
||||||
|
"end_date": "2026-01-08",
|
||||||
|
"environment_lock_digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999",
|
||||||
|
"execution_model_digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777",
|
||||||
|
"execution_model_version": "1.0.0",
|
||||||
|
"factor_output_content_digest": "sha256:d78751460dc27fc796163c462d946924ce2bcb45dcfceb5e4b77f76093befc9e",
|
||||||
|
"factor_set_digest": "sha256:e9339581cf569e92459f672e8081337712e7bf98e58ad42d60d7ed13f9b5a021",
|
||||||
|
"factor_set_id": "rhfactorsetv1:sha256:e9339581cf569e92459f672e8081337712e7bf98e58ad42d60d7ed13f9b5a021",
|
||||||
|
"foundation_digest": "sha256:d848237ab753ee9432ae78ec1f93b6ac45c8072d6694023b7f288203daf9d838",
|
||||||
|
"foundation_id": "rhdfv1:sha256:d848237ab753ee9432ae78ec1f93b6ac45c8072d6694023b7f288203daf9d838",
|
||||||
|
"frequency": "1d",
|
||||||
|
"methodology": {
|
||||||
|
"alpha": "daily_ols_intercept_geometric_annualization",
|
||||||
|
"annual_risk_free": 0.0,
|
||||||
|
"annualized_return": "geometric_compound",
|
||||||
|
"annualized_volatility": "sample_std_sqrt_periods",
|
||||||
|
"benchmark_alignment": "exact_session_index",
|
||||||
|
"benchmark_risk_free_daily": 0.0,
|
||||||
|
"beta": "sample_covariance_over_sample_variance",
|
||||||
|
"calmar_ratio": "unadjusted_annualized_return_over_absolute_maximum_drawdown",
|
||||||
|
"code_revision": "dddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
"implementation_module": "quant_engine.metrics",
|
||||||
|
"implementation_version": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"information_ratio": "mean_active_over_sample_std_active_sqrt_periods",
|
||||||
|
"maximum_drawdown": "non_positive_peak_to_trough_ratio_with_initial_nav_one",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"periods_per_year": 252,
|
||||||
|
"return_type": "simple",
|
||||||
|
"sharpe_ratio": "annualized_return_minus_annual_risk_free_over_annualized_volatility",
|
||||||
|
"sortino_ratio": "annualized_return_minus_annual_risk_free_over_root_mean_square_negative_returns_sqrt_periods",
|
||||||
|
"source_frequency": "1d",
|
||||||
|
"total_return": "final_nav_minus_one",
|
||||||
|
"tracking_error": "sample_std_active_return_sqrt_periods",
|
||||||
|
"win_rate": "positive_daily_return_count_over_observation_count"
|
||||||
|
},
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "total_return",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "total_ret",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.575
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "annualized_return",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "ann_ret",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 2683336646708.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "annualized_volatility",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "ann_volatility",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 1.38901943830891
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "sharpe_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "sharpe",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 1931820803008.3313
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "sortino_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "sortino",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "maximum_drawdown",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "max_dd",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "calmar_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "calmar",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "win_rate",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "win_rate",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 0.75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "tracking_error",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "tracking_error",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": 1.38901943830891
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "information_ratio",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "ir",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": 22.299903907544408
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "not_estimable_benchmark_variance",
|
||||||
|
"key": "alpha",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "alpha",
|
||||||
|
"unit": "ratio_per_year",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "not_estimable_benchmark_variance",
|
||||||
|
"key": "beta",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": true,
|
||||||
|
"source_column": "beta",
|
||||||
|
"unit": "ratio",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "trade_count",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "n_trades",
|
||||||
|
"unit": "count",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"availability": "available",
|
||||||
|
"key": "day_count",
|
||||||
|
"methodology_id": "researchhub.quant-performance-methodology.v1",
|
||||||
|
"metric_schema_id": "researchhub.quant-performance-metrics.v1",
|
||||||
|
"nullable": false,
|
||||||
|
"source_column": "n_days",
|
||||||
|
"unit": "count",
|
||||||
|
"value": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"performance_evidence_id": "rhperformanceevidencev1:sha256:86133a6b3c3091477cdc4618ebc294e671c9ba0a3ef5015a068f92bb575729b9",
|
||||||
|
"performance_row_digest": "sha256:90c9aa2f4168c15ff9ac5d4da2a35c3e915bb4e04736e5a440ea80c787a99553",
|
||||||
|
"performance_table_content_digest": "sha256:1570b64d83ad2a849607e6f5203f5ec2f0291a8d6cbb00b8edfe0ae030b7967d",
|
||||||
|
"performance_table_logical_name": "performance",
|
||||||
|
"performance_table_row_count": 1,
|
||||||
|
"performance_table_schema_digest": "sha256:16cef93a679761103ae405e622b7929abbfe07115bf276be4f164da5a16128d0",
|
||||||
|
"research_artifact_content_digest": "sha256:fed7a28888a6e98ccce78e7d84f22d4e3636e928485861af7a97f90eddc91f94",
|
||||||
|
"research_artifact_schema_version": "1.1.0",
|
||||||
|
"run_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
|
||||||
|
"schema_version": "researchhub.performance-evidence.v1",
|
||||||
|
"scope": "offline_research_only",
|
||||||
|
"start_date": "2026-01-05",
|
||||||
|
"strategy_digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666",
|
||||||
|
"strategy_id": "alpha-top1",
|
||||||
|
"strategy_version": "1.0.0",
|
||||||
|
"timezone": "Asia/Shanghai"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema_version": 1,
|
||||||
|
"source_commit": "a724e1e57a99d1304a932d01ee836bac56c5c15c",
|
||||||
|
"source_tree": "4774e88442d25bf79a54eab3d7106ff4d0ba9603"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,77 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
class ModuleSpecTests(unittest.TestCase):
|
def test_module_spec_declares_pure_research_engine_boundary() -> None:
|
||||||
def test_module_spec_declares_pure_research_engine_boundary(self) -> None:
|
|
||||||
spec = json.loads((ROOT / "MODULE_SPEC.yaml").read_text(encoding="utf-8"))
|
spec = json.loads((ROOT / "MODULE_SPEC.yaml").read_text(encoding="utf-8"))
|
||||||
self.assertEqual(spec["module_id"], "quant_engine")
|
assert spec["module_id"] == "quant_engine"
|
||||||
self.assertEqual(spec["authority"]["subject"], spec["module_id"])
|
assert spec["authority"]["subject"] == spec["module_id"]
|
||||||
self.assertEqual(spec["repository"]["type"], "research_engine")
|
assert spec["repository"]["type"] == "research_engine"
|
||||||
self.assertEqual(spec["bounded_context"]["domain"], "quantitative-research-engine")
|
assert spec["bounded_context"]["domain"] == "quantitative-research-engine"
|
||||||
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"):
|
||||||
self.assertIn(term, prohibited)
|
assert term in prohibited
|
||||||
self.assertEqual(spec["contracts"], {"provides": [], "consumes": []})
|
assert spec["authority"]["revision"] == 5
|
||||||
self.assertEqual(spec["dependencies"], [])
|
assert {
|
||||||
self.assertTrue(
|
(item["contract_id"], item["version"])
|
||||||
all(
|
for item in spec["contracts"]["provides"]
|
||||||
|
} == {
|
||||||
|
("researchhub.factor-definition", "1.0.0"),
|
||||||
|
("researchhub.factor-set-ref", "1.0.0"),
|
||||||
|
("researchhub.backtest-run-ref", "1.0.0"),
|
||||||
|
("researchhub.backtest-evidence-manifest", "1.0.0"),
|
||||||
|
("researchhub.performance-evidence", "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.performance-evidence": "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 {
|
||||||
|
item["contract_id"]: item["path"] for item in spec["contracts"]["provides"]
|
||||||
|
} == expected_paths
|
||||||
|
assert {
|
||||||
|
(item["contract_id"], item["version"])
|
||||||
|
for item in spec["contracts"]["consumes"]
|
||||||
|
} == {
|
||||||
|
("researchhub.dataset-snapshot", "1.0.0"),
|
||||||
|
("researchhub.data-foundation", "1.0.0"),
|
||||||
|
}
|
||||||
|
assert all(
|
||||||
|
item["authority"] == "researchhub.data"
|
||||||
|
for item in spec["contracts"]["consumes"]
|
||||||
|
)
|
||||||
|
assert spec["dependencies"] == []
|
||||||
|
capabilities = {item["id"]: item for item in spec["capabilities"]}
|
||||||
|
evidence_contract = capabilities["backtest-evidence-contracts"]
|
||||||
|
assert evidence_contract["status"] == "operational"
|
||||||
|
evidence_summary = evidence_contract["summary"].lower()
|
||||||
|
for term in ("performance-methodology", "without recomputation", "decision authority"):
|
||||||
|
assert term in evidence_summary
|
||||||
|
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"]
|
command["required"] and not command["network"]
|
||||||
for command in spec["verification"]["commands"]
|
for command in spec["verification"]["commands"]
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
test_module_spec_declares_pure_research_engine_boundary()
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import pandas as pd
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import quant_engine.alpha_factors as alpha_factors_module
|
import quant_engine.alpha_factors as alpha_factors_module
|
||||||
|
from quant_engine.factor_contracts import (
|
||||||
|
FactorContractError,
|
||||||
|
FactorInput,
|
||||||
|
ProducerIdentity,
|
||||||
|
factor_definition_from_alpha158,
|
||||||
|
factor_input_schema_digest,
|
||||||
|
)
|
||||||
from quant_engine.alpha_factors import (
|
from quant_engine.alpha_factors import (
|
||||||
ALPHA158_REGISTRY,
|
ALPHA158_REGISTRY,
|
||||||
ALPHA158_PHASE1_OPERATOR_SPECS,
|
ALPHA158_PHASE1_OPERATOR_SPECS,
|
||||||
@@ -20,6 +27,9 @@ from quant_engine.alpha_factors import (
|
|||||||
ALPHA158_PHASE5_FORMULA_CATALOG_SHA256,
|
ALPHA158_PHASE5_FORMULA_CATALOG_SHA256,
|
||||||
ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION,
|
ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION,
|
||||||
ALPHA158_PHASE5_FORMULA_SPECS,
|
ALPHA158_PHASE5_FORMULA_SPECS,
|
||||||
|
ALPHA158_PHASE6_FORMULA_CATALOG_SHA256,
|
||||||
|
ALPHA158_PHASE6_FORMULA_CONTRACT_VERSION,
|
||||||
|
ALPHA158_PHASE6_FORMULA_SPECS,
|
||||||
alpha_001,
|
alpha_001,
|
||||||
alpha_002,
|
alpha_002,
|
||||||
alpha_003,
|
alpha_003,
|
||||||
@@ -183,11 +193,13 @@ from quant_engine.alpha_factors import (
|
|||||||
evaluate_phase3_formula,
|
evaluate_phase3_formula,
|
||||||
evaluate_phase4_formula,
|
evaluate_phase4_formula,
|
||||||
evaluate_phase5_formula,
|
evaluate_phase5_formula,
|
||||||
|
evaluate_phase6_formula,
|
||||||
list_phase1_operators,
|
list_phase1_operators,
|
||||||
list_phase2_operators,
|
list_phase2_operators,
|
||||||
list_phase3_formulas,
|
list_phase3_formulas,
|
||||||
list_phase4_formulas,
|
list_phase4_formulas,
|
||||||
list_phase5_formulas,
|
list_phase5_formulas,
|
||||||
|
list_phase6_formulas,
|
||||||
correlation,
|
correlation,
|
||||||
covariance,
|
covariance,
|
||||||
decay_linear,
|
decay_linear,
|
||||||
@@ -429,6 +441,50 @@ def test_alpha_registry_required_fields():
|
|||||||
assert required <= set(meta.keys()), f"{alpha_id} missing fields"
|
assert required <= set(meta.keys()), f"{alpha_id} missing fields"
|
||||||
|
|
||||||
|
|
||||||
|
def test_alpha_registry_adapts_to_definition_without_copying_formula_or_inputs():
|
||||||
|
factor_input = FactorInput(
|
||||||
|
"market",
|
||||||
|
"sha256:" + "1" * 64,
|
||||||
|
tuple(ALPHA158_REGISTRY["alpha_005"]["inputs"]),
|
||||||
|
)
|
||||||
|
definition = factor_definition_from_alpha158(
|
||||||
|
"alpha_005",
|
||||||
|
version="1.0.0",
|
||||||
|
parameters={},
|
||||||
|
inputs=(factor_input,),
|
||||||
|
implementation_digest="sha256:" + "2" * 64,
|
||||||
|
input_schema_digest=factor_input_schema_digest((factor_input,)),
|
||||||
|
valid_from="2026-01-01T00:00:00Z",
|
||||||
|
valid_until="2027-01-01T00:00:00Z",
|
||||||
|
warmup_sessions=10,
|
||||||
|
lag_sessions=1,
|
||||||
|
producer=ProducerIdentity("quant_engine", "1.0.0"),
|
||||||
|
code_revision="c" * 40,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert definition.formula == ALPHA158_REGISTRY["alpha_005"]["formula"]
|
||||||
|
assert definition.inputs[0].required_columns == tuple(
|
||||||
|
ALPHA158_REGISTRY["alpha_005"]["inputs"]
|
||||||
|
)
|
||||||
|
|
||||||
|
incomplete = FactorInput("market", "sha256:" + "1" * 64, ("close",))
|
||||||
|
with pytest.raises(FactorContractError, match="exactly correspond"):
|
||||||
|
factor_definition_from_alpha158(
|
||||||
|
"alpha_005",
|
||||||
|
version="1.0.0",
|
||||||
|
parameters={},
|
||||||
|
inputs=(incomplete,),
|
||||||
|
implementation_digest="sha256:" + "2" * 64,
|
||||||
|
input_schema_digest=factor_input_schema_digest((incomplete,)),
|
||||||
|
valid_from="2026-01-01T00:00:00Z",
|
||||||
|
valid_until="2027-01-01T00:00:00Z",
|
||||||
|
warmup_sessions=10,
|
||||||
|
lag_sessions=1,
|
||||||
|
producer=ProducerIdentity("quant_engine", "1.0.0"),
|
||||||
|
code_revision="c" * 40,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_alpha_meta_success():
|
def test_get_alpha_meta_success():
|
||||||
"""已知 alpha_id 返回完整 meta。"""
|
"""已知 alpha_id 返回完整 meta。"""
|
||||||
meta = get_alpha_meta("alpha_001")
|
meta = get_alpha_meta("alpha_001")
|
||||||
@@ -1962,3 +2018,193 @@ def test_phase5_contract_is_publicly_exported():
|
|||||||
"list_phase5_formulas",
|
"list_phase5_formulas",
|
||||||
"evaluate_phase5_formula",
|
"evaluate_phase5_formula",
|
||||||
} <= set(alpha_factors_module.__all__)
|
} <= set(alpha_factors_module.__all__)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Alpha158 Phase 6: versioned alpha151-alpha158 formula contract ──────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase6_formula_catalog_is_versioned_exact_and_content_addressed():
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
expected_ids = tuple(f"alpha_{number:03d}" for number in range(151, 159))
|
||||||
|
expected_fields = {
|
||||||
|
"name",
|
||||||
|
"contract_version",
|
||||||
|
"formula",
|
||||||
|
"category",
|
||||||
|
"complexity",
|
||||||
|
"parameters",
|
||||||
|
"description",
|
||||||
|
"references",
|
||||||
|
"call_inputs",
|
||||||
|
"formula_inputs",
|
||||||
|
"input_category",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert ALPHA158_PHASE6_FORMULA_CONTRACT_VERSION == "1.0.0"
|
||||||
|
assert list_phase6_formulas() == expected_ids
|
||||||
|
assert tuple(ALPHA158_PHASE6_FORMULA_SPECS) == expected_ids
|
||||||
|
assert Counter(
|
||||||
|
spec["input_category"] for spec in ALPHA158_PHASE6_FORMULA_SPECS.values()
|
||||||
|
) == {"pair": 6, "triple": 2}
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE6_FORMULA_SPECS.items():
|
||||||
|
assert set(spec) == expected_fields
|
||||||
|
assert spec["name"] == alpha_id
|
||||||
|
assert spec["contract_version"] == ALPHA158_PHASE6_FORMULA_CONTRACT_VERSION
|
||||||
|
assert spec["formula"] == ALPHA158_REGISTRY[alpha_id]["formula"]
|
||||||
|
|
||||||
|
serializable_specs = {
|
||||||
|
alpha_id: {
|
||||||
|
field: list(value) if isinstance(value, tuple) else value
|
||||||
|
for field, value in spec.items()
|
||||||
|
}
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE6_FORMULA_SPECS.items()
|
||||||
|
}
|
||||||
|
encoded = json.dumps(
|
||||||
|
serializable_specs,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
ensure_ascii=False,
|
||||||
|
).encode()
|
||||||
|
assert hashlib.sha256(encoded).hexdigest() == ALPHA158_PHASE6_FORMULA_CATALOG_SHA256
|
||||||
|
assert ALPHA158_PHASE6_FORMULA_CATALOG_SHA256 == (
|
||||||
|
"70ffae16ca6cbb59a1e8644f5cdeec1d291fa75ff8b5647fb534af0083408219"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase6_formula_catalog_is_recursively_immutable():
|
||||||
|
import operator
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(ALPHA158_PHASE6_FORMULA_SPECS, "alpha_151", {})
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(
|
||||||
|
ALPHA158_PHASE6_FORMULA_SPECS["alpha_151"],
|
||||||
|
"formula",
|
||||||
|
"changed",
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(
|
||||||
|
ALPHA158_PHASE6_FORMULA_SPECS["alpha_151"]["call_inputs"],
|
||||||
|
0,
|
||||||
|
"volume",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase6_catalog_freezes_callable_and_formula_inputs():
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE6_FORMULA_SPECS.items():
|
||||||
|
function = getattr(alpha_factors_module, alpha_id)
|
||||||
|
signature_inputs = tuple(
|
||||||
|
"open" if name == "open_" else name
|
||||||
|
for name in inspect.signature(function).parameters
|
||||||
|
)
|
||||||
|
assert spec["call_inputs"] == signature_inputs
|
||||||
|
assert spec["formula_inputs"] == tuple(ALPHA158_REGISTRY[alpha_id]["inputs"])
|
||||||
|
|
||||||
|
assert ALPHA158_PHASE6_FORMULA_SPECS["alpha_158"]["call_inputs"] == (
|
||||||
|
"high",
|
||||||
|
"low",
|
||||||
|
"volume",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase6_dispatch_matches_all_existing_alpha151_alpha158_functions():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE6_FORMULA_SPECS.items():
|
||||||
|
call_inputs = spec["call_inputs"]
|
||||||
|
function = getattr(alpha_factors_module, alpha_id)
|
||||||
|
expected = function(*(inputs[name] for name in call_inputs))
|
||||||
|
actual = evaluate_phase6_formula(
|
||||||
|
alpha_id,
|
||||||
|
**{name: inputs[name] for name in reversed(call_inputs)},
|
||||||
|
)
|
||||||
|
pd.testing.assert_series_equal(actual, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase6_dispatch_rejects_unknown_missing_extra_and_non_series_inputs():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
|
||||||
|
with pytest.raises(KeyError, match="not registered"):
|
||||||
|
evaluate_phase6_formula("alpha_150", close=inputs["close"])
|
||||||
|
with pytest.raises(KeyError, match="not registered"):
|
||||||
|
evaluate_phase6_formula("alpha_159", close=inputs["close"])
|
||||||
|
with pytest.raises(ValueError, match=r"missing inputs.*volume"):
|
||||||
|
evaluate_phase6_formula("alpha_151", close=inputs["close"])
|
||||||
|
with pytest.raises(ValueError, match=r"unexpected inputs.*vwap"):
|
||||||
|
evaluate_phase6_formula(
|
||||||
|
"alpha_151",
|
||||||
|
close=inputs["close"],
|
||||||
|
volume=inputs["volume"],
|
||||||
|
vwap=inputs["vwap"],
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError, match="volume must be a pandas Series"):
|
||||||
|
evaluate_phase6_formula( # type: ignore[arg-type]
|
||||||
|
"alpha_151",
|
||||||
|
close=inputs["close"],
|
||||||
|
volume=[1.0, 2.0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase6_dispatch_rejects_length_and_index_alignment_errors():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
shorter_volume = inputs["volume"].iloc[:-1]
|
||||||
|
misaligned_low = inputs["low"].rename(index={79: 80})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="volume length must match close"):
|
||||||
|
evaluate_phase6_formula(
|
||||||
|
"alpha_151",
|
||||||
|
close=inputs["close"],
|
||||||
|
volume=shorter_volume,
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="low index must align with high"):
|
||||||
|
evaluate_phase6_formula(
|
||||||
|
"alpha_158",
|
||||||
|
high=inputs["high"],
|
||||||
|
low=misaligned_low,
|
||||||
|
volume=inputs["volume"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase6_preserves_complete_alpha001_alpha158_formula_body_fingerprint():
|
||||||
|
import ast
|
||||||
|
import hashlib
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
fingerprints = {}
|
||||||
|
for number in range(1, 159):
|
||||||
|
alpha_id = f"alpha_{number:03d}"
|
||||||
|
source = textwrap.dedent(inspect.getsource(getattr(alpha_factors_module, alpha_id)))
|
||||||
|
node = ast.parse(source).body[0]
|
||||||
|
assert isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||||
|
body = ast.dump(
|
||||||
|
ast.Module(body=node.body, type_ignores=[]),
|
||||||
|
include_attributes=False,
|
||||||
|
)
|
||||||
|
fingerprints[alpha_id] = hashlib.sha256(body.encode()).hexdigest()
|
||||||
|
|
||||||
|
encoded = json.dumps(
|
||||||
|
fingerprints,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode()
|
||||||
|
assert hashlib.sha256(encoded).hexdigest() == (
|
||||||
|
"f3ae807983cf8ca083d0b924c3807ffd84a62a5bd354f9efb1a1a16ca40c7da6"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase6_contract_is_publicly_exported():
|
||||||
|
assert {
|
||||||
|
"ALPHA158_PHASE6_FORMULA_CONTRACT_VERSION",
|
||||||
|
"ALPHA158_PHASE6_FORMULA_CATALOG_SHA256",
|
||||||
|
"ALPHA158_PHASE6_FORMULA_SPECS",
|
||||||
|
"list_phase6_formulas",
|
||||||
|
"evaluate_phase6_formula",
|
||||||
|
} <= set(alpha_factors_module.__all__)
|
||||||
|
|||||||
@@ -0,0 +1,773 @@
|
|||||||
|
"""Backtest run-reference and closed-evidence contract conformance."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from dataclasses import replace
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from quant_engine.artifact import (
|
||||||
|
BacktestEvidenceManifest,
|
||||||
|
EvidenceQualification,
|
||||||
|
RESEARCH_ARTIFACT_SCHEMA_VERSION,
|
||||||
|
ResearchRunArtifact,
|
||||||
|
build_backtest_evidence_manifest,
|
||||||
|
build_legacy_backtest_evidence_manifest,
|
||||||
|
build_research_run_artifact,
|
||||||
|
)
|
||||||
|
from quant_engine.execution import ExecutionConfig
|
||||||
|
from quant_engine.factor_contracts import (
|
||||||
|
ActorIdentity,
|
||||||
|
AvailabilityMode,
|
||||||
|
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 (
|
||||||
|
BacktestContractError,
|
||||||
|
BacktestContractErrorCode,
|
||||||
|
BacktestRun,
|
||||||
|
BacktestRunRef,
|
||||||
|
)
|
||||||
|
from quant_engine.research_pipeline import FactorBacktestResult, run_factor_backtest_research
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
FACTOR_FIXTURE = ROOT / "tests" / "fixtures" / "factor-contracts-v1.golden.json"
|
||||||
|
BACKTEST_FIXTURE = ROOT / "tests" / "fixtures" / "backtest-evidence-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}
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(value: bytes) -> str:
|
||||||
|
return f"sha256:{hashlib.sha256(value).hexdigest()}"
|
||||||
|
|
||||||
|
|
||||||
|
def _accepted_authorities(
|
||||||
|
*,
|
||||||
|
factor_evaluation_at: str = "2026-01-03T11:00:00Z",
|
||||||
|
factor_computed_at: str = "2026-01-03T10:15:00Z",
|
||||||
|
factor_artifact_available_at: str = "2026-01-03T10:20:00Z",
|
||||||
|
factor_availability_mode: AvailabilityMode = AvailabilityMode.AS_AVAILABLE,
|
||||||
|
) -> 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=factor_availability_mode,
|
||||||
|
evaluation_at=factor_evaluation_at,
|
||||||
|
computed_at=factor_computed_at,
|
||||||
|
artifact_available_at=factor_artifact_available_at,
|
||||||
|
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(parameters: dict[str, object] | None = None) -> str:
|
||||||
|
encoded = json.dumps(
|
||||||
|
PARAMETERS if parameters is None else parameters,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
return _sha256(encoded)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_ref(**overrides: Any) -> BacktestRunRef:
|
||||||
|
snapshot, foundation, factor_set = _accepted_authorities()
|
||||||
|
arguments: dict[str, Any] = {
|
||||||
|
"dataset_snapshot": snapshot,
|
||||||
|
"foundation": foundation,
|
||||||
|
"factor_set": factor_set,
|
||||||
|
"universe_digest": "sha256:" + "5" * 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",
|
||||||
|
}
|
||||||
|
arguments.update(overrides)
|
||||||
|
return BacktestRunRef.create(**arguments)
|
||||||
|
|
||||||
|
|
||||||
|
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, *, run_id: str | None = None) -> 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 if run_id is None else 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 _assert_error(
|
||||||
|
error: pytest.ExceptionInfo[BacktestContractError],
|
||||||
|
code: BacktestContractErrorCode,
|
||||||
|
path: str,
|
||||||
|
) -> None:
|
||||||
|
assert error.value.code is code
|
||||||
|
assert error.value.path == path
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_run_ref_is_deterministic_and_binds_only_opaque_authorities() -> None:
|
||||||
|
first = _run_ref()
|
||||||
|
second = _run_ref()
|
||||||
|
|
||||||
|
assert first == second
|
||||||
|
assert first.run_id.startswith("rhbacktestrunv1:sha256:")
|
||||||
|
assert first.replay_spec_digest.startswith("sha256:")
|
||||||
|
assert first.dataset_snapshot_id.startswith("rhdsv1:sha256:")
|
||||||
|
assert first.foundation_id.startswith("rhdfv1:sha256:")
|
||||||
|
assert first.factor_set_id.startswith("rhfactorsetv1:sha256:")
|
||||||
|
assert first.trading_calendar_revision_ids == (CALENDAR_REVISION_ID,)
|
||||||
|
assert first.corporate_action_revision_ids == (ACTION_REVISION_ID,)
|
||||||
|
assert first.replay_parent_run_id is None
|
||||||
|
assert first.replay_attempt == 0
|
||||||
|
assert first.replay_ancestor_run_ids == ()
|
||||||
|
snapshot, foundation, factor_set = _accepted_authorities()
|
||||||
|
assert BacktestRunRef.from_dict(
|
||||||
|
first.to_dict(),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
factor_set=factor_set,
|
||||||
|
) == first
|
||||||
|
forbidden = ("latest", "locator", "uri", "credential", "provider", "broker")
|
||||||
|
assert not any(token in first.to_json().lower() for token in forbidden)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("field", "value"),
|
||||||
|
[
|
||||||
|
("universe_digest", "sha256:" + "a" * 64),
|
||||||
|
("strategy_digest", "sha256:" + "b" * 64),
|
||||||
|
("execution_model_digest", "sha256:" + "c" * 64),
|
||||||
|
("cost_model_digest", "sha256:" + "e" * 64),
|
||||||
|
("random_seed", 8),
|
||||||
|
("code_revision", "e" * 40),
|
||||||
|
("environment_lock_digest", "sha256:" + "f" * 64),
|
||||||
|
("configuration_digest", "sha256:" + "0" * 64),
|
||||||
|
("evaluation_at", "2026-01-08T01:00:01Z"),
|
||||||
|
("computed_at", "2026-01-08T02:00:01Z"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_every_governed_run_input_mutation_changes_run_identity(
|
||||||
|
field: str,
|
||||||
|
value: object,
|
||||||
|
) -> None:
|
||||||
|
assert _run_ref(**{field: value}).run_id != _run_ref().run_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_run_ref_rejects_unclosed_upstream_and_unsafe_scalars() -> None:
|
||||||
|
with pytest.raises(BacktestContractError) as wrong_calendar:
|
||||||
|
_run_ref(trading_calendar_revision_ids=())
|
||||||
|
_assert_error(
|
||||||
|
wrong_calendar,
|
||||||
|
BacktestContractErrorCode.INPUT_CLOSURE_VIOLATION,
|
||||||
|
"$.trading_calendar_revision_ids",
|
||||||
|
)
|
||||||
|
with pytest.raises(BacktestContractError) as wrong_action:
|
||||||
|
_run_ref(corporate_action_revision_ids=())
|
||||||
|
_assert_error(
|
||||||
|
wrong_action,
|
||||||
|
BacktestContractErrorCode.INPUT_CLOSURE_VIOLATION,
|
||||||
|
"$.corporate_action_revision_ids",
|
||||||
|
)
|
||||||
|
with pytest.raises(BacktestContractError) as bool_seed:
|
||||||
|
_run_ref(random_seed=True)
|
||||||
|
_assert_error(bool_seed, BacktestContractErrorCode.TYPE_ERROR, "$.random_seed")
|
||||||
|
with pytest.raises(BacktestContractError) as bad_revision:
|
||||||
|
_run_ref(code_revision="abc")
|
||||||
|
_assert_error(bad_revision, BacktestContractErrorCode.INVALID_FORMAT, "$.code_revision")
|
||||||
|
with pytest.raises(BacktestContractError) as bad_digest:
|
||||||
|
_run_ref(universe_digest="5" * 64)
|
||||||
|
_assert_error(bad_digest, BacktestContractErrorCode.INVALID_FORMAT, "$.universe_digest")
|
||||||
|
with pytest.raises(BacktestContractError) as lookahead:
|
||||||
|
_run_ref(computed_at="2026-01-08T00:59:59Z")
|
||||||
|
_assert_error(lookahead, BacktestContractErrorCode.TIME_ORDER_VIOLATION, "$.computed_at")
|
||||||
|
with pytest.raises(BacktestContractError) as factor_type:
|
||||||
|
_run_ref(factor_set="rhfactorsetv1:sha256:" + "0" * 64)
|
||||||
|
_assert_error(factor_type, BacktestContractErrorCode.TYPE_ERROR, "$.factor_set")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("factor_times", "expected_path"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"factor_evaluation_at": "2026-01-08T01:00:01Z",
|
||||||
|
"factor_computed_at": "2026-01-08T00:59:59Z",
|
||||||
|
"factor_artifact_available_at": "2026-01-08T01:00:00Z",
|
||||||
|
},
|
||||||
|
"$.evaluation_at",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"factor_evaluation_at": "2026-01-03T11:00:00Z",
|
||||||
|
"factor_computed_at": "2026-01-08T01:00:00Z",
|
||||||
|
"factor_artifact_available_at": "2026-01-08T01:00:01Z",
|
||||||
|
"factor_availability_mode": AvailabilityMode.RETROSPECTIVE_REPLAY,
|
||||||
|
},
|
||||||
|
"$.evaluation_at",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_run_ref_evaluation_closes_factor_pit(
|
||||||
|
factor_times: dict[str, Any],
|
||||||
|
expected_path: str,
|
||||||
|
) -> None:
|
||||||
|
snapshot, foundation, factor_set = _accepted_authorities(**factor_times)
|
||||||
|
|
||||||
|
with pytest.raises(BacktestContractError) as lookahead:
|
||||||
|
_run_ref(
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
factor_set=factor_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_assert_error(
|
||||||
|
lookahead,
|
||||||
|
BacktestContractErrorCode.TIME_ORDER_VIOLATION,
|
||||||
|
expected_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_ref_rejects_aliases_locators_unsafe_integers_and_invalid_text() -> None:
|
||||||
|
with pytest.raises(BacktestContractError) as mutable_alias:
|
||||||
|
_run_ref(strategy_id="latest")
|
||||||
|
_assert_error(
|
||||||
|
mutable_alias,
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
"$.strategy_id",
|
||||||
|
)
|
||||||
|
with pytest.raises(BacktestContractError) as physical_uri:
|
||||||
|
_run_ref(execution_model_version="s3://model-bucket/current")
|
||||||
|
_assert_error(
|
||||||
|
physical_uri,
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
"$.execution_model_version",
|
||||||
|
)
|
||||||
|
with pytest.raises(BacktestContractError) as unsafe_seed:
|
||||||
|
_run_ref(random_seed=2**53)
|
||||||
|
_assert_error(
|
||||||
|
unsafe_seed,
|
||||||
|
BacktestContractErrorCode.INVALID_VALUE,
|
||||||
|
"$.random_seed",
|
||||||
|
)
|
||||||
|
with pytest.raises(BacktestContractError) as invalid_unicode:
|
||||||
|
_run_ref(strategy_id="\ud800")
|
||||||
|
_assert_error(
|
||||||
|
invalid_unicode,
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
"$.strategy_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
run_ref = _run_ref()
|
||||||
|
mixed_keys = run_ref.to_dict()
|
||||||
|
mixed_keys[1] = "not-a-contract-key" # type: ignore[index]
|
||||||
|
snapshot, foundation, factor_set = _accepted_authorities()
|
||||||
|
with pytest.raises(BacktestContractError) as invalid_key:
|
||||||
|
BacktestRunRef.from_dict(
|
||||||
|
mixed_keys,
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
factor_set=factor_set,
|
||||||
|
)
|
||||||
|
_assert_error(invalid_key, BacktestContractErrorCode.TYPE_ERROR, "$")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"physical_id",
|
||||||
|
["db.table", "source_alpha", "wind.model", "qtdb_view", "bloomberg-signal"],
|
||||||
|
)
|
||||||
|
def test_run_ref_rejects_physical_terms_in_logical_ids(physical_id: str) -> None:
|
||||||
|
with pytest.raises(BacktestContractError) as physical:
|
||||||
|
_run_ref(strategy_id=physical_id)
|
||||||
|
_assert_error(
|
||||||
|
physical,
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
"$.strategy_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("version", ["1.0.0-.", "1.0.0-foo..bar", "1.0.0-01"])
|
||||||
|
def test_run_ref_requires_strict_semver_prerelease_identifiers(version: str) -> None:
|
||||||
|
with pytest.raises(BacktestContractError) as invalid:
|
||||||
|
_run_ref(strategy_version=version)
|
||||||
|
_assert_error(
|
||||||
|
invalid,
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
"$.strategy_version",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert _run_ref(strategy_version="1.0.0-alpha.1").strategy_version == "1.0.0-alpha.1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_replay_lineage_is_acyclic_and_cannot_claim_changed_inputs() -> None:
|
||||||
|
parent = _run_ref()
|
||||||
|
replay = _run_ref(
|
||||||
|
computed_at="2026-01-08T03:00:00Z",
|
||||||
|
parent=parent,
|
||||||
|
replay_reason="deterministic_reproduction",
|
||||||
|
replay_attempt=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert replay.run_id != parent.run_id
|
||||||
|
assert replay.replay_spec_digest == parent.replay_spec_digest
|
||||||
|
assert replay.replay_parent_run_id == parent.run_id
|
||||||
|
assert replay.replay_ancestor_run_ids == (parent.run_id,)
|
||||||
|
|
||||||
|
with pytest.raises(BacktestContractError) as changed_input:
|
||||||
|
_run_ref(
|
||||||
|
universe_digest="sha256:" + "a" * 64,
|
||||||
|
computed_at="2026-01-08T03:00:00Z",
|
||||||
|
parent=parent,
|
||||||
|
replay_reason="changed_universe",
|
||||||
|
replay_attempt=1,
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
changed_input,
|
||||||
|
BacktestContractErrorCode.LINEAGE_VIOLATION,
|
||||||
|
"$.replay_spec_digest",
|
||||||
|
)
|
||||||
|
with pytest.raises(BacktestContractError) as skipped_attempt:
|
||||||
|
_run_ref(
|
||||||
|
computed_at="2026-01-08T03:00:00Z",
|
||||||
|
parent=parent,
|
||||||
|
replay_reason="skipped_attempt",
|
||||||
|
replay_attempt=2,
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
skipped_attempt,
|
||||||
|
BacktestContractErrorCode.LINEAGE_VIOLATION,
|
||||||
|
"$.replay_attempt",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_offline_research_manifest_closes_exact_existing_evidence_mapping() -> None:
|
||||||
|
run_ref = _run_ref()
|
||||||
|
artifact = _artifact(run_ref)
|
||||||
|
first = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
qualification=EvidenceQualification.CONTRACT_QUALIFIED,
|
||||||
|
)
|
||||||
|
second = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
qualification=EvidenceQualification.CONTRACT_QUALIFIED,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert first == second
|
||||||
|
assert first.manifest_id.startswith("rhbacktestevidencev1:sha256:")
|
||||||
|
assert first.run_id == run_ref.run_id
|
||||||
|
assert first.profile == "offline_research_v1"
|
||||||
|
assert first.qualification is EvidenceQualification.CONTRACT_QUALIFIED
|
||||||
|
mapping = {
|
||||||
|
item.category: tuple(table.logical_name for table in item.tables)
|
||||||
|
for item in first.evidence
|
||||||
|
}
|
||||||
|
assert mapping == {
|
||||||
|
"run": ("run",),
|
||||||
|
"signal": ("signals",),
|
||||||
|
"fill": ("trades",),
|
||||||
|
"position_nav": ("positions", "nav"),
|
||||||
|
"performance": ("performance",),
|
||||||
|
"attribution": ("attribution", "attribution_daily"),
|
||||||
|
"risk_snapshot": ("risk",),
|
||||||
|
"replay": (),
|
||||||
|
}
|
||||||
|
assert "order" not in mapping
|
||||||
|
assert "rejection" not in mapping
|
||||||
|
risk = next(item for item in first.evidence if item.category == "risk_snapshot")
|
||||||
|
assert risk.tables[0].row_count == 0
|
||||||
|
assert risk.tables[0].schema_digest.startswith("sha256:")
|
||||||
|
|
||||||
|
changed_performance = artifact.performance
|
||||||
|
changed_performance.loc[0, "n_days"] += 1
|
||||||
|
changed_artifact = replace(artifact, _performance=changed_performance)
|
||||||
|
changed = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
changed_artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
assert changed.manifest_id != first.manifest_id
|
||||||
|
assert run_ref.run_id == first.run_id == changed.run_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_manifest_rejects_missing_mismatched_or_duplicate_evidence() -> None:
|
||||||
|
run_ref = _run_ref()
|
||||||
|
artifact = _artifact(run_ref)
|
||||||
|
with pytest.raises(BacktestContractError) as wrong_run:
|
||||||
|
build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
_artifact(run_ref, run_id="different-run"),
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
wrong_run,
|
||||||
|
BacktestContractErrorCode.IDENTITY_MISMATCH,
|
||||||
|
"$.artifact.tables.run.run_id",
|
||||||
|
)
|
||||||
|
missing_signals = replace(artifact, _signals=None) # type: ignore[arg-type]
|
||||||
|
with pytest.raises(BacktestContractError) as missing_table:
|
||||||
|
build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
missing_signals,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
missing_table,
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
"$.artifact.tables.signals",
|
||||||
|
)
|
||||||
|
with pytest.raises(BacktestContractError) as digest_mismatch:
|
||||||
|
build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
expected_table_digests={"performance": "sha256:" + "0" * 64},
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
digest_mismatch,
|
||||||
|
BacktestContractErrorCode.EVIDENCE_MISMATCH,
|
||||||
|
"$.artifact.tables.performance.content_digest",
|
||||||
|
)
|
||||||
|
manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
duplicate = manifest.to_dict()
|
||||||
|
duplicate["evidence"].append(copy.deepcopy(duplicate["evidence"][0]))
|
||||||
|
with pytest.raises(BacktestContractError) as duplicate_category:
|
||||||
|
BacktestEvidenceManifest.from_dict(
|
||||||
|
duplicate,
|
||||||
|
backtest_run_ref=run_ref,
|
||||||
|
artifact=artifact,
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
duplicate_category,
|
||||||
|
BacktestContractErrorCode.INVALID_VALUE,
|
||||||
|
"$.evidence[8].category",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_manifest_binds_supported_schema_and_has_collision_free_cell_encoding() -> None:
|
||||||
|
run_ref = _run_ref()
|
||||||
|
artifact = _artifact(run_ref)
|
||||||
|
|
||||||
|
with pytest.raises(BacktestContractError) as unsupported_schema:
|
||||||
|
build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
replace(artifact, schema_version="999.0.0"),
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
unsupported_schema,
|
||||||
|
BacktestContractErrorCode.INVALID_VALUE,
|
||||||
|
"$.artifact.schema_version",
|
||||||
|
)
|
||||||
|
|
||||||
|
identities: set[str] = set()
|
||||||
|
for value in (float("nan"), float("inf"), float("-inf")):
|
||||||
|
performance = artifact.performance
|
||||||
|
performance.loc[0, "alpha"] = value
|
||||||
|
manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
replace(artifact, _performance=performance),
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
assert manifest.artifact_schema_version == RESEARCH_ARTIFACT_SCHEMA_VERSION
|
||||||
|
identities.add(manifest.manifest_id)
|
||||||
|
assert len(identities) == 3
|
||||||
|
|
||||||
|
content_digests: set[str] = set()
|
||||||
|
for value in (float("nan"), {"non_finite_float": "nan"}):
|
||||||
|
performance = artifact.performance.astype(object)
|
||||||
|
performance.at[0, "alpha"] = value
|
||||||
|
manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
replace(artifact, _performance=performance),
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
performance_entry = next(
|
||||||
|
entry for entry in manifest.evidence if entry.category == "performance"
|
||||||
|
)
|
||||||
|
content_digests.add(performance_entry.tables[0].content_digest)
|
||||||
|
assert len(content_digests) == 2
|
||||||
|
|
||||||
|
unsupported = artifact.performance.astype(object)
|
||||||
|
unsupported.loc[0, "alpha"] = object()
|
||||||
|
with pytest.raises(BacktestContractError) as unsupported_cell:
|
||||||
|
build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
replace(artifact, _performance=unsupported),
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
unsupported_cell,
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
"$.artifact.tables.performance.rows[0].alpha",
|
||||||
|
)
|
||||||
|
|
||||||
|
invalid_nested_key = artifact.performance.astype(object)
|
||||||
|
invalid_nested_key.at[0, "alpha"] = {"\ud800": "value"}
|
||||||
|
with pytest.raises(BacktestContractError) as invalid_utf8:
|
||||||
|
build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
replace(artifact, _performance=invalid_nested_key),
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
invalid_utf8,
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
"$.artifact.tables.performance.rows[0].alpha.keys",
|
||||||
|
)
|
||||||
|
|
||||||
|
unsafe_integer = artifact.performance.astype(object)
|
||||||
|
unsafe_integer.loc[0, "alpha"] = 10**5000
|
||||||
|
with pytest.raises(BacktestContractError) as unsafe_cell:
|
||||||
|
build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
replace(artifact, _performance=unsafe_integer),
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
unsafe_cell,
|
||||||
|
BacktestContractErrorCode.INVALID_VALUE,
|
||||||
|
"$.artifact.tables.performance.rows[0].alpha",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_artifact_canonical_content_has_typed_collision_free_cell_encoding() -> None:
|
||||||
|
run_ref = _run_ref()
|
||||||
|
artifact = _artifact(run_ref)
|
||||||
|
|
||||||
|
content_hashes: set[str] = set()
|
||||||
|
for value in (
|
||||||
|
float("nan"),
|
||||||
|
float("inf"),
|
||||||
|
float("-inf"),
|
||||||
|
{"non_finite_float": "nan"},
|
||||||
|
):
|
||||||
|
performance = artifact.performance.astype(object)
|
||||||
|
performance.at[0, "alpha"] = value
|
||||||
|
mutated = replace(artifact, _performance=performance)
|
||||||
|
content_hashes.add(mutated.content_sha256)
|
||||||
|
assert "non_finite_float" in mutated.canonical_json()
|
||||||
|
assert len(content_hashes) == 4
|
||||||
|
|
||||||
|
unsupported = artifact.performance.astype(object)
|
||||||
|
unsupported.loc[0, "alpha"] = object()
|
||||||
|
with pytest.raises(BacktestContractError) as unsupported_cell:
|
||||||
|
replace(artifact, _performance=unsupported).canonical_json()
|
||||||
|
_assert_error(
|
||||||
|
unsupported_cell,
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
"$.tables.performance.rows[0].alpha",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_bridge_is_explicit_and_cannot_be_contract_qualified() -> None:
|
||||||
|
run_ref = _run_ref()
|
||||||
|
legacy_run = BacktestRun(
|
||||||
|
run_id="legacy-run-001",
|
||||||
|
dataset_snapshot_id=run_ref.dataset_snapshot_id,
|
||||||
|
factor_version_id="alpha_005@1.0.0",
|
||||||
|
strategy_version_id="alpha-top1@1.0.0",
|
||||||
|
code_revision=run_ref.code_revision,
|
||||||
|
config_hash=_config_digest().removeprefix("sha256:"),
|
||||||
|
created_at=datetime(2026, 1, 8, 2, 0, tzinfo=UTC),
|
||||||
|
)
|
||||||
|
artifact = _artifact(run_ref, run_id=legacy_run.run_id)
|
||||||
|
manifest = build_legacy_backtest_evidence_manifest(
|
||||||
|
legacy_run,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert manifest.qualification is EvidenceQualification.LEGACY_EXPLORATORY
|
||||||
|
assert manifest.run_id == legacy_run.run_id
|
||||||
|
assert manifest.backtest_run_ref is None
|
||||||
|
assert manifest.to_dict()["run_reference"]["kind"] == "legacy_backtest_run"
|
||||||
|
assert BacktestEvidenceManifest.from_dict(
|
||||||
|
manifest.to_dict(),
|
||||||
|
artifact=artifact,
|
||||||
|
) == manifest
|
||||||
|
with pytest.raises(BacktestContractError) as implicit_promotion:
|
||||||
|
build_backtest_evidence_manifest( # type: ignore[arg-type]
|
||||||
|
legacy_run,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
qualification=EvidenceQualification.CONTRACT_QUALIFIED,
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
implicit_promotion,
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
"$.backtest_run_ref",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_contract_and_architecture_boundary() -> None:
|
||||||
|
run_ref = _run_ref()
|
||||||
|
artifact = _artifact(run_ref)
|
||||||
|
manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
golden = json.loads(BACKTEST_FIXTURE.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
table_digests = {
|
||||||
|
table.logical_name: table.content_digest
|
||||||
|
for item in manifest.evidence
|
||||||
|
for table in item.tables
|
||||||
|
}
|
||||||
|
assert golden == {
|
||||||
|
"run_id": run_ref.run_id,
|
||||||
|
"replay_spec_digest": run_ref.replay_spec_digest,
|
||||||
|
"manifest_id": manifest.manifest_id,
|
||||||
|
"evidence_digest": manifest.evidence_digest,
|
||||||
|
"table_content_digests": table_digests,
|
||||||
|
}
|
||||||
|
governed_source = (ROOT / "src" / "quant_engine" / "governed_pipeline.py").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
artifact_source = (ROOT / "src" / "quant_engine" / "artifact.py").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert "from quant_engine.artifact" not in governed_source
|
||||||
|
assert "BacktestRunRef" in governed_source
|
||||||
|
assert "BacktestEvidenceManifest" not in governed_source
|
||||||
|
assert "BacktestEvidenceManifest" in artifact_source
|
||||||
|
assert not (ROOT / "src" / "quant_engine" / "backtest_contracts.py").exists()
|
||||||
@@ -0,0 +1,938 @@
|
|||||||
|
"""Versioned factor-definition and factor-set contract conformance."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from dataclasses import FrozenInstanceError
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from quant_engine.factor_contracts import (
|
||||||
|
ActorIdentity,
|
||||||
|
AvailabilityMode,
|
||||||
|
ContractErrorCode,
|
||||||
|
Causation,
|
||||||
|
DataFoundationEnvelope,
|
||||||
|
DatasetSnapshotEnvelope,
|
||||||
|
FactorContractError,
|
||||||
|
FactorDefinition,
|
||||||
|
FactorInput,
|
||||||
|
FactorSetRef,
|
||||||
|
HistoricalAvailability,
|
||||||
|
InputBinding,
|
||||||
|
LegacyFactorBinding,
|
||||||
|
OutputArtifactRef,
|
||||||
|
OutputCoverage,
|
||||||
|
OutputQuality,
|
||||||
|
OutputQualityCheck,
|
||||||
|
PayloadValidation,
|
||||||
|
ProducerIdentity,
|
||||||
|
TypedParameter,
|
||||||
|
ViewAvailability,
|
||||||
|
canonical_json_bytes,
|
||||||
|
factor_definition_from_alpha158,
|
||||||
|
factor_input_schema_digest,
|
||||||
|
validate_factor_catalog,
|
||||||
|
)
|
||||||
|
from quant_engine.governed_pipeline import (
|
||||||
|
FactorVersion,
|
||||||
|
bind_legacy_factor,
|
||||||
|
project_legacy_factor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FIXTURE_PATH = Path(__file__).parent / "fixtures" / "factor-contracts-v1.golden.json"
|
||||||
|
VIEW_REF_ID = "rhviewrefv1:sha256:bf776bcd26d940fafde1d650776a5505fb3fe8b5b068c351622bf2c42385629c"
|
||||||
|
VIEW_SCHEMA_DIGEST = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
|
||||||
|
|
||||||
|
def _golden() -> dict[str, Any]:
|
||||||
|
loaded = json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))
|
||||||
|
assert isinstance(loaded, dict)
|
||||||
|
return loaded
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(value: bytes) -> str:
|
||||||
|
return f"sha256:{hashlib.sha256(value).hexdigest()}"
|
||||||
|
|
||||||
|
|
||||||
|
def _reidentify(item: dict[str, Any], field: str, prefix: str) -> None:
|
||||||
|
payload = {key: value for key, value in item.items() if key != field}
|
||||||
|
item[field] = f"{prefix}{hashlib.sha256(canonical_json_bytes(payload)).hexdigest()}"
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_and_foundation(
|
||||||
|
fixture: dict[str, Any] | None = None,
|
||||||
|
) -> tuple[DatasetSnapshotEnvelope, DataFoundationEnvelope]:
|
||||||
|
source = _golden() if fixture is None else fixture
|
||||||
|
return (
|
||||||
|
DatasetSnapshotEnvelope.from_dict(source["dataset_snapshot"]),
|
||||||
|
DataFoundationEnvelope.from_dict(source["data_foundation"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _definition(
|
||||||
|
*,
|
||||||
|
inputs: tuple[FactorInput, ...] | None = None,
|
||||||
|
**overrides: Any,
|
||||||
|
) -> FactorDefinition:
|
||||||
|
factor_inputs = inputs or (
|
||||||
|
FactorInput("market", VIEW_SCHEMA_DIGEST, ("close", "volume")),
|
||||||
|
)
|
||||||
|
arguments: dict[str, Any] = {
|
||||||
|
"factor_id": "alpha_005",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"formula": "correlation(close, volume, 10)",
|
||||||
|
"parameters": {},
|
||||||
|
"implementation_digest": "sha256:" + "1" * 64,
|
||||||
|
"input_schema_digest": factor_input_schema_digest(factor_inputs),
|
||||||
|
"inputs": factor_inputs,
|
||||||
|
"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,
|
||||||
|
}
|
||||||
|
arguments.update(overrides)
|
||||||
|
return FactorDefinition.create(**arguments)
|
||||||
|
|
||||||
|
|
||||||
|
def _golden_definition() -> FactorDefinition:
|
||||||
|
factor_input = FactorInput("market", VIEW_SCHEMA_DIGEST, ("close", "volume"))
|
||||||
|
return 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _factor_set_arguments(
|
||||||
|
*,
|
||||||
|
fixture: dict[str, Any] | None = None,
|
||||||
|
snapshot: DatasetSnapshotEnvelope | None = None,
|
||||||
|
foundation: DataFoundationEnvelope | None = None,
|
||||||
|
definition: FactorDefinition | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
source = _golden() if fixture is None else fixture
|
||||||
|
if snapshot is None or foundation is None:
|
||||||
|
parsed_snapshot, parsed_foundation = _snapshot_and_foundation(source)
|
||||||
|
snapshot = snapshot or parsed_snapshot
|
||||||
|
foundation = foundation or parsed_foundation
|
||||||
|
selected_definition = definition or _golden_definition()
|
||||||
|
output_schema_bytes = canonical_json_bytes(source["output_schema"])
|
||||||
|
output_content_bytes = canonical_json_bytes(source["output_content"])
|
||||||
|
artifact = OutputArtifactRef.create(
|
||||||
|
schema_digest=_sha256(output_schema_bytes),
|
||||||
|
content_digest=_sha256(output_content_bytes),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"definitions": (selected_definition,),
|
||||||
|
"dataset_snapshot": snapshot,
|
||||||
|
"foundation": foundation,
|
||||||
|
"selected_view_ref_ids": (VIEW_REF_ID,),
|
||||||
|
"input_bindings": (
|
||||||
|
InputBinding(
|
||||||
|
selected_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,
|
||||||
|
"availability_mode": AvailabilityMode.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _factor_set(**overrides: Any) -> FactorSetRef:
|
||||||
|
arguments = _factor_set_arguments()
|
||||||
|
arguments.update(overrides)
|
||||||
|
return FactorSetRef.create(**arguments)
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_error(
|
||||||
|
error: pytest.ExceptionInfo[FactorContractError],
|
||||||
|
code: ContractErrorCode,
|
||||||
|
path: str,
|
||||||
|
) -> None:
|
||||||
|
assert error.value.code is code
|
||||||
|
assert error.value.path == path
|
||||||
|
|
||||||
|
|
||||||
|
def _mutate_artifact_schema_binding(value: dict[str, Any]) -> None:
|
||||||
|
artifact = value["output_artifact_ref"]
|
||||||
|
artifact["schema_digest"] = "sha256:" + "0" * 64
|
||||||
|
_reidentify(artifact, "artifact_id", "rhfactoroutputv1:sha256:")
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_contracts_are_content_addressed_round_trippable_and_deeply_immutable() -> None:
|
||||||
|
fixture = _golden()
|
||||||
|
original_snapshot = copy.deepcopy(fixture["dataset_snapshot"])
|
||||||
|
original_foundation = copy.deepcopy(fixture["data_foundation"])
|
||||||
|
snapshot, foundation = _snapshot_and_foundation(fixture)
|
||||||
|
definition = _golden_definition()
|
||||||
|
factor_set = FactorSetRef.create(**_factor_set_arguments(fixture=fixture, snapshot=snapshot, foundation=foundation, definition=definition))
|
||||||
|
binding = LegacyFactorBinding.create(
|
||||||
|
definition=definition,
|
||||||
|
legacy_factor_id="factor:demo-momentum",
|
||||||
|
legacy_version="1.0.0",
|
||||||
|
legacy_definition_sha256="b" * 64,
|
||||||
|
legacy_dataset_schema_version="1.0.0",
|
||||||
|
canonical_input_schema_digest=definition.input_schema_digest,
|
||||||
|
correspondence_evidence_digest="sha256:" + "5" * 64,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert snapshot.pit_cutoff == "2026-01-02T07:01:00Z"
|
||||||
|
assert foundation.pit_cutoff == factor_set.pit_cutoff == "2026-01-03T00:00:00Z"
|
||||||
|
assert snapshot.pit_cutoff != foundation.pit_cutoff
|
||||||
|
assert definition.definition_id == fixture["expected"]["definition_id"]
|
||||||
|
assert definition.input_schema_digest == fixture["expected"]["input_schema_digest"]
|
||||||
|
assert factor_set.factor_set_id == fixture["expected"]["factor_set_id"]
|
||||||
|
assert factor_set.output_artifact_ref.artifact_id == fixture["expected"]["output_artifact_id"]
|
||||||
|
assert binding.binding_id == fixture["expected"]["legacy_binding_id"]
|
||||||
|
assert not definition.to_json().endswith("\n")
|
||||||
|
assert not factor_set.to_json().endswith("\n")
|
||||||
|
assert FactorDefinition.from_json(definition.to_json()) == definition
|
||||||
|
|
||||||
|
reparsed = FactorSetRef.from_json(
|
||||||
|
factor_set.to_json(),
|
||||||
|
definitions=(definition,),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
output_schema_bytes=canonical_json_bytes(fixture["output_schema"]),
|
||||||
|
output_content_bytes=canonical_json_bytes(fixture["output_content"]),
|
||||||
|
)
|
||||||
|
reference_only = FactorSetRef.from_json(
|
||||||
|
factor_set.to_json(),
|
||||||
|
definitions=(definition,),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
)
|
||||||
|
assert reparsed.factor_set_id == factor_set.factor_set_id
|
||||||
|
assert reparsed.payload_validation is PayloadValidation.PAYLOAD_REVALIDATED
|
||||||
|
assert reference_only.payload_validation is PayloadValidation.REFERENCE_ONLY
|
||||||
|
|
||||||
|
fixture["dataset_snapshot"]["descriptor"]["dataset"]["dimensions"].append("forbidden")
|
||||||
|
fixture["data_foundation"]["standardized_views"][0]["schema_digest"] = "sha256:" + "0" * 64
|
||||||
|
assert snapshot.to_dict() == original_snapshot
|
||||||
|
assert foundation.to_dict() == original_foundation
|
||||||
|
returned = snapshot.to_dict()
|
||||||
|
returned["descriptor"]["dataset"]["dimensions"].append("also_forbidden")
|
||||||
|
assert snapshot.to_dict() == original_snapshot
|
||||||
|
with pytest.raises(FrozenInstanceError):
|
||||||
|
snapshot.snapshot_id = "rhdsv1:sha256:" + "0" * 64 # type: ignore[misc]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("variant", ["whitespace", "key_order"])
|
||||||
|
def test_contract_decoders_reject_non_canonical_json(variant: str) -> None:
|
||||||
|
fixture = _golden()
|
||||||
|
snapshot, foundation = _snapshot_and_foundation(fixture)
|
||||||
|
definition = _golden_definition()
|
||||||
|
factor_set = FactorSetRef.create(
|
||||||
|
**_factor_set_arguments(
|
||||||
|
fixture=fixture,
|
||||||
|
snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
definition=definition,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
binding = LegacyFactorBinding.create(
|
||||||
|
definition=definition,
|
||||||
|
legacy_factor_id="factor:demo-momentum",
|
||||||
|
legacy_version="1.0.0",
|
||||||
|
legacy_definition_sha256="b" * 64,
|
||||||
|
legacy_dataset_schema_version="1.0.0",
|
||||||
|
canonical_input_schema_digest=definition.input_schema_digest,
|
||||||
|
correspondence_evidence_digest="sha256:" + "5" * 64,
|
||||||
|
)
|
||||||
|
|
||||||
|
def non_canonical(value: str) -> str:
|
||||||
|
if variant == "whitespace":
|
||||||
|
return value + "\n"
|
||||||
|
loaded = json.loads(value)
|
||||||
|
reversed_items = dict(reversed(tuple(loaded.items())))
|
||||||
|
return json.dumps(reversed_items, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
|
||||||
|
decoders = (
|
||||||
|
lambda value: FactorDefinition.from_json(value),
|
||||||
|
lambda value: FactorSetRef.from_json(
|
||||||
|
value,
|
||||||
|
definitions=(definition,),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
),
|
||||||
|
lambda value: LegacyFactorBinding.from_json(value, definition=definition),
|
||||||
|
)
|
||||||
|
for decoder, encoded in zip(
|
||||||
|
decoders,
|
||||||
|
(definition.to_json(), factor_set.to_json(), binding.to_json()),
|
||||||
|
strict=True,
|
||||||
|
):
|
||||||
|
with pytest.raises(FactorContractError) as exc_info:
|
||||||
|
decoder(non_canonical(encoded))
|
||||||
|
assert exc_info.value.code is ContractErrorCode.INVALID_FORMAT
|
||||||
|
assert exc_info.value.path == "$"
|
||||||
|
|
||||||
|
|
||||||
|
def test_factor_definition_identity_is_order_independent_where_semantics_are_unordered() -> None:
|
||||||
|
first_input = FactorInput("prices", "sha256:" + "6" * 64, ("close",))
|
||||||
|
second_input = FactorInput("volumes", "sha256:" + "7" * 64, ("volume",))
|
||||||
|
inputs = (first_input, second_input)
|
||||||
|
parameters_a = {
|
||||||
|
"window": TypedParameter("integer", 10),
|
||||||
|
"weights": TypedParameter("json", {"fast": [1, 2], "slow": [3, 4]}),
|
||||||
|
}
|
||||||
|
parameters_b = {
|
||||||
|
"weights": TypedParameter("json", {"slow": [3, 4], "fast": [1, 2]}),
|
||||||
|
"window": TypedParameter("integer", 10),
|
||||||
|
}
|
||||||
|
first = _definition(
|
||||||
|
inputs=inputs,
|
||||||
|
parameters=parameters_a,
|
||||||
|
input_schema_digest=factor_input_schema_digest(inputs),
|
||||||
|
)
|
||||||
|
second = _definition(
|
||||||
|
inputs=tuple(reversed(inputs)),
|
||||||
|
parameters=parameters_b,
|
||||||
|
input_schema_digest=factor_input_schema_digest(tuple(reversed(inputs))),
|
||||||
|
)
|
||||||
|
assert first.definition_id == second.definition_id
|
||||||
|
assert first.to_json() == second.to_json()
|
||||||
|
|
||||||
|
semantic_changes = (
|
||||||
|
_definition(factor_id="alpha_006"),
|
||||||
|
_definition(version="1.0.1"),
|
||||||
|
_definition(formula="correlation(close, volume, 11)"),
|
||||||
|
_definition(parameters={"window": TypedParameter("integer", 10)}),
|
||||||
|
_definition(implementation_digest="sha256:" + "9" * 64),
|
||||||
|
_definition(valid_until="2027-01-02T00:00:00Z"),
|
||||||
|
_definition(warmup_sessions=11),
|
||||||
|
_definition(lag_sessions=2),
|
||||||
|
_definition(producer=ProducerIdentity("quant_engine", "1.0.1")),
|
||||||
|
_definition(code_revision="d" * 40),
|
||||||
|
)
|
||||||
|
assert all(changed.definition_id != _golden_definition().definition_id for changed in semantic_changes)
|
||||||
|
assert len({changed.definition_id for changed in semantic_changes}) == len(semantic_changes)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parameter_types_decimal_profile_and_detached_nested_values_are_strict() -> None:
|
||||||
|
nested = {"ordered": [1, {"flag": True}]}
|
||||||
|
parameter = TypedParameter("json", nested)
|
||||||
|
nested["ordered"].append(2)
|
||||||
|
definition = _definition(parameters={"payload": parameter})
|
||||||
|
assert definition.to_dict()["parameters"]["payload"]["value"] == {
|
||||||
|
"ordered": [1, {"flag": True}]
|
||||||
|
}
|
||||||
|
integer_definition = _definition(parameters={"value": TypedParameter("integer", 1)})
|
||||||
|
string_definition = _definition(parameters={"value": TypedParameter("string", "1")})
|
||||||
|
assert integer_definition.definition_id != string_definition.definition_id
|
||||||
|
|
||||||
|
for parameter_type, value, code in (
|
||||||
|
("decimal", "1.0", ContractErrorCode.INVALID_FORMAT),
|
||||||
|
("decimal", "1e3", ContractErrorCode.INVALID_FORMAT),
|
||||||
|
("decimal", "-0", ContractErrorCode.INVALID_FORMAT),
|
||||||
|
("integer", True, ContractErrorCode.TYPE_ERROR),
|
||||||
|
("json", 1.5, ContractErrorCode.TYPE_ERROR),
|
||||||
|
("json", {"é": "bad-key"}, ContractErrorCode.INVALID_FORMAT),
|
||||||
|
("json", 9_007_199_254_740_992, ContractErrorCode.INVALID_VALUE),
|
||||||
|
):
|
||||||
|
with pytest.raises(FactorContractError) as error:
|
||||||
|
TypedParameter(parameter_type, value)
|
||||||
|
assert error.value.code is code
|
||||||
|
assert TypedParameter("decimal", "10.25").to_dict()["value"] == "10.25"
|
||||||
|
|
||||||
|
|
||||||
|
def test_catalog_rejects_duplicate_and_overlapping_logical_validity_but_allows_adjacency() -> None:
|
||||||
|
base = _golden_definition()
|
||||||
|
adjacent = _definition(valid_from="2027-01-01T00:00:00Z", valid_until="2028-01-01T00:00:00Z")
|
||||||
|
assert len(validate_factor_catalog((adjacent, base))) == 2
|
||||||
|
with pytest.raises(FactorContractError) as duplicate:
|
||||||
|
validate_factor_catalog((base, base))
|
||||||
|
_assert_error(duplicate, ContractErrorCode.INVALID_VALUE, "$.definitions")
|
||||||
|
overlapping = _definition(valid_from="2026-06-01T00:00:00Z", valid_until="2028-01-01T00:00:00Z")
|
||||||
|
with pytest.raises(FactorContractError) as overlap:
|
||||||
|
validate_factor_catalog((base, overlapping))
|
||||||
|
_assert_error(overlap, ContractErrorCode.TIME_ORDER_VIOLATION, "$.definitions")
|
||||||
|
|
||||||
|
|
||||||
|
def test_upstream_contracts_reject_unknown_fields_identity_forgery_and_unqualified_input() -> None:
|
||||||
|
unknown = _golden()["dataset_snapshot"]
|
||||||
|
unknown["provider"] = "forbidden"
|
||||||
|
with pytest.raises(FactorContractError) as unknown_error:
|
||||||
|
DatasetSnapshotEnvelope.from_dict(unknown)
|
||||||
|
_assert_error(unknown_error, ContractErrorCode.UNKNOWN_FIELD, "$.provider")
|
||||||
|
|
||||||
|
forged = _golden()["data_foundation"]
|
||||||
|
forged["standardized_views"][0]["schema_digest"] = "sha256:" + "0" * 64
|
||||||
|
with pytest.raises(FactorContractError) as forged_error:
|
||||||
|
DataFoundationEnvelope.from_dict(forged)
|
||||||
|
assert forged_error.value.code is ContractErrorCode.IDENTITY_MISMATCH
|
||||||
|
assert forged_error.value.path.endswith("view_ref_id")
|
||||||
|
|
||||||
|
rejected_source = _golden()
|
||||||
|
rejected_source["dataset_snapshot"]["descriptor"]["qualification"]["status"] = "rejected"
|
||||||
|
_reidentify(rejected_source["dataset_snapshot"], "snapshot_id", "rhdsv1:sha256:")
|
||||||
|
rejected_snapshot = DatasetSnapshotEnvelope.from_dict(rejected_source["dataset_snapshot"])
|
||||||
|
_, foundation = _snapshot_and_foundation()
|
||||||
|
with pytest.raises(FactorContractError) as rejected_error:
|
||||||
|
FactorSetRef.create(
|
||||||
|
**_factor_set_arguments(snapshot=rejected_snapshot, foundation=foundation)
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
rejected_error,
|
||||||
|
ContractErrorCode.QUALIFICATION_REJECTED,
|
||||||
|
"$.dataset_snapshot.descriptor.qualification",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_foundation_rejects_future_knowledge_and_per_view_calendar_borrowing() -> None:
|
||||||
|
future = _golden()["data_foundation"]
|
||||||
|
action = future["corporate_action_revisions"][0]
|
||||||
|
old_action_id = action["action_revision_id"]
|
||||||
|
action["knowledge_time"] = "2026-01-03T00:00:01Z"
|
||||||
|
_reidentify(action, "action_revision_id", "rhcav1:sha256:")
|
||||||
|
future["standardized_views"][0]["corporate_action_revision_ids"] = [action["action_revision_id"]]
|
||||||
|
lineage = next(item for item in future["revision_lineage"] if item["revision_id"] == old_action_id)
|
||||||
|
lineage["revision_id"] = action["action_revision_id"]
|
||||||
|
lineage["knowledge_time"] = action["knowledge_time"]
|
||||||
|
_reidentify(future["standardized_views"][0], "view_ref_id", "rhviewrefv1:sha256:")
|
||||||
|
_reidentify(future, "foundation_id", "rhdfv1:sha256:")
|
||||||
|
with pytest.raises(FactorContractError) as future_error:
|
||||||
|
DataFoundationEnvelope.from_dict(future)
|
||||||
|
_assert_error(
|
||||||
|
future_error,
|
||||||
|
ContractErrorCode.TIME_ORDER_VIOLATION,
|
||||||
|
"$.revision_lineage.knowledge_time",
|
||||||
|
)
|
||||||
|
|
||||||
|
uncovered = _golden()["data_foundation"]
|
||||||
|
original_route_id = uncovered["instrument_routes"][0]["route_revision_id"]
|
||||||
|
second_calendar = copy.deepcopy(uncovered["trading_calendar_revisions"][0])
|
||||||
|
second_calendar["calendar_id"] = "rhcalendar:99990000111122223333444455556666"
|
||||||
|
_reidentify(second_calendar, "calendar_revision_id", "rhcalv1:sha256:")
|
||||||
|
uncovered["trading_calendar_revisions"].append(second_calendar)
|
||||||
|
route = uncovered["instrument_routes"][0]
|
||||||
|
route["calendar_id"] = second_calendar["calendar_id"]
|
||||||
|
_reidentify(route, "route_revision_id", "rhroutev1:sha256:")
|
||||||
|
route_lineage = next(item for item in uncovered["revision_lineage"] if item["revision_id"] == original_route_id)
|
||||||
|
route_lineage["revision_id"] = route["route_revision_id"]
|
||||||
|
uncovered["revision_lineage"].append(
|
||||||
|
{
|
||||||
|
"revision_kind": "trading_calendar",
|
||||||
|
"revision_id": second_calendar["calendar_revision_id"],
|
||||||
|
"revision_number": 1,
|
||||||
|
"knowledge_time": second_calendar["knowledge_time"],
|
||||||
|
"evidence_digest": second_calendar["evidence_digest"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
view = uncovered["standardized_views"][0]
|
||||||
|
view["instrument_route_revision_ids"] = [route["route_revision_id"]]
|
||||||
|
_reidentify(view, "view_ref_id", "rhviewrefv1:sha256:")
|
||||||
|
_reidentify(uncovered, "foundation_id", "rhdfv1:sha256:")
|
||||||
|
with pytest.raises(FactorContractError) as calendar_error:
|
||||||
|
DataFoundationEnvelope.from_dict(uncovered)
|
||||||
|
assert calendar_error.value.code is ContractErrorCode.INPUT_CLOSURE_VIOLATION
|
||||||
|
assert "selected route calendar" in calendar_error.value.detail
|
||||||
|
|
||||||
|
|
||||||
|
def _replay_fixture() -> dict[str, Any]:
|
||||||
|
fixture = _golden()
|
||||||
|
snapshot = fixture["dataset_snapshot"]
|
||||||
|
snapshot["descriptor"]["published_at"] = "2026-01-04T00:00:00Z"
|
||||||
|
_reidentify(snapshot, "snapshot_id", "rhdsv1:sha256:")
|
||||||
|
foundation = fixture["data_foundation"]
|
||||||
|
foundation["dataset_snapshot_id"] = snapshot["snapshot_id"]
|
||||||
|
for view in foundation["standardized_views"]:
|
||||||
|
view["dataset_snapshot_id"] = snapshot["snapshot_id"]
|
||||||
|
_reidentify(view, "view_ref_id", "rhviewrefv1:sha256:")
|
||||||
|
_reidentify(foundation, "foundation_id", "rhdfv1:sha256:")
|
||||||
|
return fixture
|
||||||
|
|
||||||
|
|
||||||
|
def test_as_available_and_retrospective_replay_keep_distinct_time_claims() -> None:
|
||||||
|
as_available = _factor_set()
|
||||||
|
assert as_available.historical_availability is HistoricalAvailability.DECLARED_AS_AVAILABLE
|
||||||
|
|
||||||
|
replay_source = _replay_fixture()
|
||||||
|
snapshot, foundation = _snapshot_and_foundation(replay_source)
|
||||||
|
replay_view_id = next(iter(foundation.views))
|
||||||
|
arguments = _factor_set_arguments(
|
||||||
|
fixture=replay_source,
|
||||||
|
snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
)
|
||||||
|
arguments.update(
|
||||||
|
selected_view_ref_ids=(replay_view_id,),
|
||||||
|
input_bindings=(
|
||||||
|
InputBinding(
|
||||||
|
arguments["definitions"][0].definition_id,
|
||||||
|
"market",
|
||||||
|
replay_view_id,
|
||||||
|
VIEW_SCHEMA_DIGEST,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
view_availability=(
|
||||||
|
ViewAvailability(replay_view_id, "2026-01-04T00:10:00Z", "sha256:" + "2" * 64),
|
||||||
|
),
|
||||||
|
availability_mode=AvailabilityMode.RETROSPECTIVE_REPLAY,
|
||||||
|
computed_at="2026-01-04T00:20:00Z",
|
||||||
|
artifact_available_at="2026-01-04T00:25:00Z",
|
||||||
|
causation=Causation("foundation", foundation.foundation_id),
|
||||||
|
)
|
||||||
|
replay = FactorSetRef.create(**arguments)
|
||||||
|
assert replay.evaluation_at == "2026-01-03T11:00:00Z"
|
||||||
|
assert replay.computed_at == "2026-01-04T00:20:00Z"
|
||||||
|
assert replay.historical_availability is HistoricalAvailability.NOT_ESTABLISHED
|
||||||
|
|
||||||
|
replay_source_args = _factor_set_arguments(
|
||||||
|
fixture=replay_source,
|
||||||
|
snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
)
|
||||||
|
replay_source_args.update(
|
||||||
|
selected_view_ref_ids=(replay_view_id,),
|
||||||
|
input_bindings=(
|
||||||
|
InputBinding(
|
||||||
|
replay_source_args["definitions"][0].definition_id,
|
||||||
|
"market",
|
||||||
|
replay_view_id,
|
||||||
|
VIEW_SCHEMA_DIGEST,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
view_availability=(
|
||||||
|
ViewAvailability(replay_view_id, "2026-01-02T23:50:00Z", "sha256:" + "2" * 64),
|
||||||
|
),
|
||||||
|
causation=Causation("foundation", foundation.foundation_id),
|
||||||
|
)
|
||||||
|
with pytest.raises(FactorContractError) as late_publication:
|
||||||
|
FactorSetRef.create(**replay_source_args)
|
||||||
|
_assert_error(
|
||||||
|
late_publication,
|
||||||
|
ContractErrorCode.TIME_ORDER_VIOLATION,
|
||||||
|
"$.dataset_snapshot.descriptor.published_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("overrides", "path"),
|
||||||
|
[
|
||||||
|
({"view_availability": (ViewAvailability(VIEW_REF_ID, "2026-01-03T00:00:01Z", "sha256:" + "2" * 64),)}, "$.view_availability"),
|
||||||
|
({"computed_at": "2026-01-02T23:40:00Z"}, "$.computed_at"),
|
||||||
|
({"artifact_available_at": "2026-01-03T10:14:00Z"}, "$.artifact_available_at"),
|
||||||
|
({"artifact_available_at": "2026-01-03T11:00:01Z"}, "$.artifact_available_at"),
|
||||||
|
({"evaluation_at": "2026-01-03T11:00:00"}, "$.evaluation_at"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_as_available_time_failures_are_typed(overrides: dict[str, Any], path: str) -> None:
|
||||||
|
with pytest.raises(FactorContractError) as error:
|
||||||
|
_factor_set(**overrides)
|
||||||
|
assert error.value.code in {
|
||||||
|
ContractErrorCode.INVALID_FORMAT,
|
||||||
|
ContractErrorCode.TIME_ORDER_VIOLATION,
|
||||||
|
}
|
||||||
|
assert error.value.path == path
|
||||||
|
|
||||||
|
|
||||||
|
def test_replay_rejects_backdating_and_historical_availability_promotion() -> None:
|
||||||
|
source = _replay_fixture()
|
||||||
|
snapshot, foundation = _snapshot_and_foundation(source)
|
||||||
|
view_id = next(iter(foundation.views))
|
||||||
|
arguments = _factor_set_arguments(fixture=source, snapshot=snapshot, foundation=foundation)
|
||||||
|
definition = arguments["definitions"][0]
|
||||||
|
arguments.update(
|
||||||
|
selected_view_ref_ids=(view_id,),
|
||||||
|
input_bindings=(InputBinding(definition.definition_id, "market", view_id, VIEW_SCHEMA_DIGEST),),
|
||||||
|
view_availability=(ViewAvailability(view_id, "2026-01-04T00:10:00Z", "sha256:" + "2" * 64),),
|
||||||
|
availability_mode=AvailabilityMode.RETROSPECTIVE_REPLAY,
|
||||||
|
computed_at="2026-01-04T00:20:00Z",
|
||||||
|
artifact_available_at="2026-01-04T00:25:00Z",
|
||||||
|
causation=Causation("foundation", foundation.foundation_id),
|
||||||
|
)
|
||||||
|
replay = FactorSetRef.create(**arguments)
|
||||||
|
promoted = replay.to_dict()
|
||||||
|
promoted["historical_availability"] = "declared_as_available"
|
||||||
|
_reidentify(promoted, "factor_set_id", "rhfactorsetv1:sha256:")
|
||||||
|
with pytest.raises(FactorContractError) as promotion_error:
|
||||||
|
FactorSetRef.from_dict(
|
||||||
|
promoted,
|
||||||
|
definitions=(definition,),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
promotion_error,
|
||||||
|
ContractErrorCode.READINESS_ESCALATION,
|
||||||
|
"$.historical_availability",
|
||||||
|
)
|
||||||
|
arguments["computed_at"] = "2026-01-03T11:30:00Z"
|
||||||
|
with pytest.raises(FactorContractError) as backdated_error:
|
||||||
|
FactorSetRef.create(**arguments)
|
||||||
|
_assert_error(backdated_error, ContractErrorCode.TIME_ORDER_VIOLATION, "$.computed_at")
|
||||||
|
|
||||||
|
|
||||||
|
def _multi_view_fixture() -> tuple[dict[str, Any], str]:
|
||||||
|
fixture = _golden()
|
||||||
|
foundation = fixture["data_foundation"]
|
||||||
|
second = copy.deepcopy(foundation["standardized_views"][0])
|
||||||
|
second["view_id"] = "rhview:11111111222222223333333344444444"
|
||||||
|
second["schema_digest"] = "sha256:" + "6" * 64
|
||||||
|
second["content_digest"] = "sha256:" + "7" * 64
|
||||||
|
second["transformation_digest"] = "sha256:" + "8" * 64
|
||||||
|
_reidentify(second, "view_ref_id", "rhviewrefv1:sha256:")
|
||||||
|
foundation["standardized_views"].append(second)
|
||||||
|
_reidentify(foundation, "foundation_id", "rhdfv1:sha256:")
|
||||||
|
return fixture, second["view_ref_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_multi_input_mapping_requires_exact_consumption_closure_and_is_order_independent() -> None:
|
||||||
|
fixture, second_view_id = _multi_view_fixture()
|
||||||
|
snapshot, foundation = _snapshot_and_foundation(fixture)
|
||||||
|
inputs = (
|
||||||
|
FactorInput("prices", VIEW_SCHEMA_DIGEST, ("close",)),
|
||||||
|
FactorInput("volumes", "sha256:" + "6" * 64, ("volume",)),
|
||||||
|
)
|
||||||
|
definition = _definition(
|
||||||
|
inputs=inputs,
|
||||||
|
formula="correlation(close, volume, 10)",
|
||||||
|
input_schema_digest=factor_input_schema_digest(inputs),
|
||||||
|
)
|
||||||
|
first_binding = InputBinding(definition.definition_id, "prices", VIEW_REF_ID, VIEW_SCHEMA_DIGEST)
|
||||||
|
second_binding = InputBinding(definition.definition_id, "volumes", second_view_id, "sha256:" + "6" * 64)
|
||||||
|
first_availability = ViewAvailability(VIEW_REF_ID, "2026-01-02T23:40:00Z", "sha256:" + "2" * 64)
|
||||||
|
second_availability = ViewAvailability(second_view_id, "2026-01-02T23:50:00Z", "sha256:" + "6" * 64)
|
||||||
|
base = _factor_set_arguments(fixture=fixture, snapshot=snapshot, foundation=foundation, definition=definition)
|
||||||
|
base.update(
|
||||||
|
selected_view_ref_ids=(VIEW_REF_ID, second_view_id),
|
||||||
|
input_bindings=(first_binding, second_binding),
|
||||||
|
view_availability=(first_availability, second_availability),
|
||||||
|
causation=Causation("foundation", foundation.foundation_id),
|
||||||
|
)
|
||||||
|
first = FactorSetRef.create(**base)
|
||||||
|
reordered = dict(base)
|
||||||
|
reordered.update(
|
||||||
|
selected_view_ref_ids=(second_view_id, VIEW_REF_ID),
|
||||||
|
input_bindings=(second_binding, first_binding),
|
||||||
|
view_availability=(second_availability, first_availability),
|
||||||
|
)
|
||||||
|
assert FactorSetRef.create(**reordered).factor_set_id == first.factor_set_id
|
||||||
|
|
||||||
|
for invalid_bindings, invalid_views in (
|
||||||
|
((first_binding,), (VIEW_REF_ID, second_view_id)),
|
||||||
|
((first_binding, second_binding), (VIEW_REF_ID,)),
|
||||||
|
((first_binding, second_binding), (VIEW_REF_ID, second_view_id, VIEW_REF_ID)),
|
||||||
|
):
|
||||||
|
invalid = dict(base)
|
||||||
|
invalid.update(input_bindings=invalid_bindings, selected_view_ref_ids=invalid_views)
|
||||||
|
with pytest.raises(FactorContractError) as error:
|
||||||
|
FactorSetRef.create(**invalid)
|
||||||
|
assert error.value.code in {
|
||||||
|
ContractErrorCode.INPUT_CLOSURE_VIOLATION,
|
||||||
|
ContractErrorCode.INVALID_VALUE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("mutate", "code", "path"),
|
||||||
|
[
|
||||||
|
(lambda value: value["producer"].pop("id"), ContractErrorCode.MISSING_FIELD, "$.producer.id"),
|
||||||
|
(lambda value: value["producer"].pop("version"), ContractErrorCode.MISSING_FIELD, "$.producer.version"),
|
||||||
|
(lambda value: value["producer"].update(id="other_engine"), ContractErrorCode.LINEAGE_VIOLATION, "$.producer.id"),
|
||||||
|
(lambda value: value["producer"].update(version="latest"), ContractErrorCode.INVALID_FORMAT, "$.producer.version"),
|
||||||
|
(lambda value: value.update(code_revision="bad"), ContractErrorCode.INVALID_FORMAT, "$.code_revision"),
|
||||||
|
(lambda value: value["actor"].pop("kind"), ContractErrorCode.MISSING_FIELD, "$.actor.kind"),
|
||||||
|
(lambda value: value["actor"].pop("id"), ContractErrorCode.MISSING_FIELD, "$.actor.id"),
|
||||||
|
(lambda value: value["actor"].update(kind="robot"), ContractErrorCode.INVALID_VALUE, "$.actor.kind"),
|
||||||
|
(lambda value: value["actor"].update(id="latest"), ContractErrorCode.INVALID_VALUE, "$.actor.id"),
|
||||||
|
(lambda value: value.pop("correlation_id"), ContractErrorCode.MISSING_FIELD, "$.correlation_id"),
|
||||||
|
(lambda value: value.update(correlation_id="latest"), ContractErrorCode.INVALID_VALUE, "$.correlation_id"),
|
||||||
|
(lambda value: value["causation"].pop("kind"), ContractErrorCode.MISSING_FIELD, "$.causation.kind"),
|
||||||
|
(lambda value: value["causation"].update(kind="run"), ContractErrorCode.INVALID_VALUE, "$.causation.kind"),
|
||||||
|
(lambda value: value["causation"].update(id="rhdfv1:sha256:" + "0" * 64), ContractErrorCode.LINEAGE_VIOLATION, "$.causation.id"),
|
||||||
|
(lambda value: value.pop("output_artifact_ref"), ContractErrorCode.MISSING_FIELD, "$.output_artifact_ref"),
|
||||||
|
(lambda value: value["output_artifact_ref"].update(artifact_id="rhfactoroutputv1:sha256:" + "0" * 64), ContractErrorCode.IDENTITY_MISMATCH, "$.output_artifact_ref.artifact_id"),
|
||||||
|
(_mutate_artifact_schema_binding, ContractErrorCode.ARTIFACT_MISMATCH, "$.output_artifact_ref"),
|
||||||
|
(lambda value: value.update(availability_mode="implicit_fallback"), ContractErrorCode.INVALID_VALUE, "$.availability_mode"),
|
||||||
|
(lambda value: value.pop("computed_at"), ContractErrorCode.MISSING_FIELD, "$.computed_at"),
|
||||||
|
(lambda value: value.update(decision_eligible=True), ContractErrorCode.READINESS_ESCALATION, "$.decision_eligible"),
|
||||||
|
(lambda value: value.update(evidence_scope="real_data"), ContractErrorCode.READINESS_ESCALATION, "$.evidence_scope"),
|
||||||
|
(lambda value: value["upstream_evidence"].update(qualification_evidence_digest="sha256:" + "0" * 64), ContractErrorCode.IDENTITY_MISMATCH, "$.upstream_evidence"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_lineage_artifact_and_readiness_fields_have_independent_typed_negatives(
|
||||||
|
mutate: Callable[[dict[str, Any]], Any],
|
||||||
|
code: ContractErrorCode,
|
||||||
|
path: str,
|
||||||
|
) -> None:
|
||||||
|
factor_set = _factor_set()
|
||||||
|
value = factor_set.to_dict()
|
||||||
|
mutate(value)
|
||||||
|
if "factor_set_id" in value:
|
||||||
|
_reidentify(value, "factor_set_id", "rhfactorsetv1:sha256:")
|
||||||
|
snapshot, foundation = _snapshot_and_foundation()
|
||||||
|
with pytest.raises(FactorContractError) as error:
|
||||||
|
FactorSetRef.from_dict(
|
||||||
|
value,
|
||||||
|
definitions=(_golden_definition(),),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
)
|
||||||
|
_assert_error(error, code, path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_schema_content_bytes_cannot_be_swapped_or_forged() -> None:
|
||||||
|
factor_set = _factor_set()
|
||||||
|
fixture = _golden()
|
||||||
|
snapshot, foundation = _snapshot_and_foundation()
|
||||||
|
schema_bytes = canonical_json_bytes(fixture["output_schema"])
|
||||||
|
content_bytes = canonical_json_bytes(fixture["output_content"])
|
||||||
|
with pytest.raises(FactorContractError) as swapped:
|
||||||
|
FactorSetRef.from_dict(
|
||||||
|
factor_set.to_dict(),
|
||||||
|
definitions=(_golden_definition(),),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
output_schema_bytes=content_bytes,
|
||||||
|
output_content_bytes=schema_bytes,
|
||||||
|
)
|
||||||
|
_assert_error(swapped, ContractErrorCode.ARTIFACT_MISMATCH, "$.output_artifact_ref")
|
||||||
|
with pytest.raises(FactorContractError) as noncanonical:
|
||||||
|
FactorSetRef.create(
|
||||||
|
**{
|
||||||
|
**_factor_set_arguments(),
|
||||||
|
"output_schema_bytes": json.dumps(fixture["output_schema"], indent=2).encode(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_assert_error(noncanonical, ContractErrorCode.INVALID_FORMAT, "$.output_schema_bytes")
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsuccessful_output_quality_or_coverage_cannot_form_a_factor_set() -> None:
|
||||||
|
with pytest.raises(FactorContractError) as failed_quality:
|
||||||
|
_factor_set(
|
||||||
|
output_quality=OutputQuality(
|
||||||
|
"failed",
|
||||||
|
(OutputQualityCheck("finite_values", "failed", "sha256:" + "3" * 64),),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_assert_error(failed_quality, ContractErrorCode.INVALID_VALUE, "$.output_quality")
|
||||||
|
|
||||||
|
for coverage in (
|
||||||
|
OutputCoverage("incomplete", 2, 1, "row", "alpha_005.cn_a", "sha256:" + "4" * 64),
|
||||||
|
OutputCoverage("complete", 2, 1, "row", "alpha_005.cn_a", "sha256:" + "4" * 64),
|
||||||
|
):
|
||||||
|
with pytest.raises(FactorContractError) as incomplete:
|
||||||
|
_factor_set(output_coverage=coverage)
|
||||||
|
_assert_error(incomplete, ContractErrorCode.INVALID_VALUE, "$.output_coverage")
|
||||||
|
|
||||||
|
|
||||||
|
def test_external_snapshot_definition_and_view_references_cannot_be_substituted() -> None:
|
||||||
|
factor_set = _factor_set()
|
||||||
|
snapshot, foundation = _snapshot_and_foundation()
|
||||||
|
value = factor_set.to_dict()
|
||||||
|
value["dataset_snapshot_id"] = "rhdsv1:sha256:" + "0" * 64
|
||||||
|
_reidentify(value, "factor_set_id", "rhfactorsetv1:sha256:")
|
||||||
|
with pytest.raises(FactorContractError) as snapshot_error:
|
||||||
|
FactorSetRef.from_dict(
|
||||||
|
value,
|
||||||
|
definitions=(_golden_definition(),),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
snapshot_error,
|
||||||
|
ContractErrorCode.INPUT_CLOSURE_VIOLATION,
|
||||||
|
"$.dataset_snapshot_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
value = factor_set.to_dict()
|
||||||
|
value["definition_ids"] = ["rhfactorv1:sha256:" + "0" * 64]
|
||||||
|
_reidentify(value, "factor_set_id", "rhfactorsetv1:sha256:")
|
||||||
|
with pytest.raises(FactorContractError) as definition_error:
|
||||||
|
FactorSetRef.from_dict(
|
||||||
|
value,
|
||||||
|
definitions=(_golden_definition(),),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
definition_error,
|
||||||
|
ContractErrorCode.INPUT_CLOSURE_VIOLATION,
|
||||||
|
"$.definition_ids",
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = _factor_set_arguments()
|
||||||
|
arguments["selected_view_ref_ids"] = ("rhviewrefv1:sha256:" + "0" * 64,)
|
||||||
|
with pytest.raises(FactorContractError) as view_error:
|
||||||
|
FactorSetRef.create(**arguments)
|
||||||
|
_assert_error(
|
||||||
|
view_error,
|
||||||
|
ContractErrorCode.INPUT_CLOSURE_VIOLATION,
|
||||||
|
"$.selected_view_ref_ids",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"invalid_definition_id",
|
||||||
|
[
|
||||||
|
{"unexpected": "object"},
|
||||||
|
["array"],
|
||||||
|
42,
|
||||||
|
True,
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_factor_set_ref_definition_ids_reject_non_string_types(
|
||||||
|
invalid_definition_id: Any,
|
||||||
|
) -> None:
|
||||||
|
factor_set = _factor_set()
|
||||||
|
definition = _golden_definition()
|
||||||
|
value = factor_set.to_dict()
|
||||||
|
value["definition_ids"] = [definition.definition_id, invalid_definition_id]
|
||||||
|
_reidentify(value, "factor_set_id", "rhfactorsetv1:sha256:")
|
||||||
|
snapshot, foundation = _snapshot_and_foundation()
|
||||||
|
|
||||||
|
with pytest.raises(FactorContractError) as error:
|
||||||
|
FactorSetRef.from_json(
|
||||||
|
canonical_json_bytes(value),
|
||||||
|
definitions=(definition,),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
)
|
||||||
|
|
||||||
|
_assert_error(error, ContractErrorCode.TYPE_ERROR, "$.definition_ids[1]")
|
||||||
|
|
||||||
|
|
||||||
|
def test_factor_set_ref_definition_ids_still_reject_duplicate_strings() -> None:
|
||||||
|
factor_set = _factor_set()
|
||||||
|
definition = _golden_definition()
|
||||||
|
value = factor_set.to_dict()
|
||||||
|
value["definition_ids"] = [definition.definition_id, definition.definition_id]
|
||||||
|
_reidentify(value, "factor_set_id", "rhfactorsetv1:sha256:")
|
||||||
|
snapshot, foundation = _snapshot_and_foundation()
|
||||||
|
|
||||||
|
with pytest.raises(FactorContractError) as error:
|
||||||
|
FactorSetRef.from_json(
|
||||||
|
canonical_json_bytes(value),
|
||||||
|
definitions=(definition,),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
)
|
||||||
|
|
||||||
|
_assert_error(error, ContractErrorCode.INVALID_VALUE, "$.definition_ids")
|
||||||
|
|
||||||
|
|
||||||
|
def test_factor_set_parent_requires_exact_identity_and_correlation() -> None:
|
||||||
|
parent = _factor_set()
|
||||||
|
child_arguments = _factor_set_arguments()
|
||||||
|
child_arguments.update(
|
||||||
|
output_content_bytes=canonical_json_bytes({"rows": [{"value": "0.250"}]}),
|
||||||
|
causation=Causation("factor_set", parent.factor_set_id),
|
||||||
|
parent=parent,
|
||||||
|
)
|
||||||
|
child_arguments["output_artifact_ref"] = OutputArtifactRef.create(
|
||||||
|
schema_digest=_sha256(child_arguments["output_schema_bytes"]),
|
||||||
|
content_digest=_sha256(child_arguments["output_content_bytes"]),
|
||||||
|
)
|
||||||
|
child = FactorSetRef.create(**child_arguments)
|
||||||
|
assert child.causation.id == parent.factor_set_id
|
||||||
|
missing_parent = child.to_dict()
|
||||||
|
snapshot, foundation = _snapshot_and_foundation()
|
||||||
|
with pytest.raises(FactorContractError) as missing_error:
|
||||||
|
FactorSetRef.from_dict(
|
||||||
|
missing_parent,
|
||||||
|
definitions=(_golden_definition(),),
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
)
|
||||||
|
_assert_error(missing_error, ContractErrorCode.LINEAGE_VIOLATION, "$.causation")
|
||||||
|
wrong_correlation = dict(child_arguments)
|
||||||
|
wrong_correlation["correlation_id"] = "different_run"
|
||||||
|
with pytest.raises(FactorContractError) as correlation_error:
|
||||||
|
FactorSetRef.create(**wrong_correlation)
|
||||||
|
_assert_error(correlation_error, ContractErrorCode.LINEAGE_VIOLATION, "$.correlation_id")
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_bridge_is_explicit_lossy_and_preserves_all_four_historical_fields() -> None:
|
||||||
|
definition = _golden_definition()
|
||||||
|
legacy = FactorVersion(
|
||||||
|
factor_id="factor:demo-momentum",
|
||||||
|
version="1.0.0",
|
||||||
|
definition_sha256="b" * 64,
|
||||||
|
dataset_schema_version="1.0.0",
|
||||||
|
)
|
||||||
|
binding = LegacyFactorBinding.create(
|
||||||
|
definition=definition,
|
||||||
|
legacy_factor_id=legacy.factor_id,
|
||||||
|
legacy_version=legacy.version,
|
||||||
|
legacy_definition_sha256=legacy.definition_sha256,
|
||||||
|
legacy_dataset_schema_version=legacy.dataset_schema_version,
|
||||||
|
canonical_input_schema_digest=definition.input_schema_digest,
|
||||||
|
correspondence_evidence_digest="sha256:" + "5" * 64,
|
||||||
|
)
|
||||||
|
assert bind_legacy_factor(legacy, definition, binding) is definition
|
||||||
|
assert project_legacy_factor(definition, binding) == legacy
|
||||||
|
assert legacy.version_id == "factor:demo-momentum@1.0.0"
|
||||||
|
assert legacy.definition_sha256 != definition.definition_id.rsplit(":", maxsplit=1)[-1]
|
||||||
|
assert LegacyFactorBinding.from_json(binding.to_json(), definition=definition) == binding
|
||||||
|
|
||||||
|
mismatched = FactorVersion(
|
||||||
|
factor_id="factor:different",
|
||||||
|
version=legacy.version,
|
||||||
|
definition_sha256=legacy.definition_sha256,
|
||||||
|
dataset_schema_version=legacy.dataset_schema_version,
|
||||||
|
)
|
||||||
|
with pytest.raises(FactorContractError) as mismatch_error:
|
||||||
|
bind_legacy_factor(mismatched, definition, binding)
|
||||||
|
_assert_error(mismatch_error, ContractErrorCode.LEGACY_BINDING_MISMATCH, "$.binding")
|
||||||
|
|
||||||
|
|
||||||
|
def test_bare_legacy_factor_or_id_cannot_enter_factor_set_contract() -> None:
|
||||||
|
legacy = FactorVersion("factor:demo-momentum", "1.0.0", "b" * 64, "1.0.0")
|
||||||
|
arguments = _factor_set_arguments()
|
||||||
|
arguments["definitions"] = (legacy,)
|
||||||
|
with pytest.raises(FactorContractError) as legacy_error:
|
||||||
|
FactorSetRef.create(**arguments)
|
||||||
|
_assert_error(legacy_error, ContractErrorCode.TYPE_ERROR, "$.definitions[0]")
|
||||||
|
arguments["definitions"] = (legacy.version_id,)
|
||||||
|
with pytest.raises(FactorContractError) as id_error:
|
||||||
|
FactorSetRef.create(**arguments)
|
||||||
|
_assert_error(id_error, ContractErrorCode.TYPE_ERROR, "$.definitions[0]")
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
"""Governed Personal Quant OS vertical-slice contracts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from quant_engine.execution import ExecutionConfig
|
||||||
|
from quant_engine.governed_pipeline import (
|
||||||
|
DatasetSnapshot,
|
||||||
|
FactorVersion,
|
||||||
|
PaperOrderIntent,
|
||||||
|
RiskDecisionStatus,
|
||||||
|
RiskPolicy,
|
||||||
|
StrategyStage,
|
||||||
|
StrategyVersion,
|
||||||
|
create_paper_order_intent,
|
||||||
|
run_governed_factor_slice,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _calendar() -> pd.DatetimeIndex:
|
||||||
|
return pd.date_range("2026-01-05", periods=4, freq="B")
|
||||||
|
|
||||||
|
|
||||||
|
def _scores() -> pd.DataFrame:
|
||||||
|
dates = _calendar()
|
||||||
|
return pd.DataFrame(
|
||||||
|
{"A": [3.0, 1.0], "B": [2.0, 3.0], "C": [1.0, 2.0]},
|
||||||
|
index=dates[:2],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _prices() -> tuple[pd.DataFrame, pd.DataFrame]:
|
||||||
|
dates = _calendar()
|
||||||
|
opens = pd.DataFrame(
|
||||||
|
{"A": [10.0, 10.0, 10.2, 10.4], "B": [20.0, 20.0, 20.5, 21.0], "C": [30.0, 30.0, 30.0, 30.0]},
|
||||||
|
index=dates,
|
||||||
|
)
|
||||||
|
closes = opens * 1.01
|
||||||
|
return opens, closes
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot() -> DatasetSnapshot:
|
||||||
|
return DatasetSnapshot(
|
||||||
|
snapshot_id="dataset:cn-a-daily-20260108-v1",
|
||||||
|
schema_version="1.0.0",
|
||||||
|
content_sha256="a" * 64,
|
||||||
|
effective_at=datetime(2026, 1, 8, 7, tzinfo=UTC),
|
||||||
|
available_at=datetime(2026, 1, 8, 8, tzinfo=UTC),
|
||||||
|
ingested_at=datetime(2026, 1, 8, 8, 5, tzinfo=UTC),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _factor() -> FactorVersion:
|
||||||
|
return FactorVersion(
|
||||||
|
factor_id="factor:demo-momentum",
|
||||||
|
version="1.0.0",
|
||||||
|
definition_sha256="b" * 64,
|
||||||
|
dataset_schema_version="1.0.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _strategy() -> StrategyVersion:
|
||||||
|
return StrategyVersion(
|
||||||
|
strategy_id="strategy:demo-top2",
|
||||||
|
version="1.0.0",
|
||||||
|
factor_version_id="factor:demo-momentum@1.0.0",
|
||||||
|
stage=StrategyStage.APPROVED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _execution_config() -> ExecutionConfig:
|
||||||
|
return ExecutionConfig(
|
||||||
|
commission_bps=0,
|
||||||
|
stamp_tax_bps=0,
|
||||||
|
slippage_bps=0,
|
||||||
|
min_trade_amount=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_governed_slice_is_reproducible_and_creates_only_paper_intent() -> None:
|
||||||
|
opens, closes = _prices()
|
||||||
|
created_at = datetime(2026, 1, 9, 1, tzinfo=UTC)
|
||||||
|
policy = RiskPolicy(
|
||||||
|
policy_id="risk:paper-default@1.0.0",
|
||||||
|
max_gross_exposure=1.0,
|
||||||
|
max_single_asset_weight=0.6,
|
||||||
|
max_positions=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = run_governed_factor_slice(
|
||||||
|
factor_scores=_scores(),
|
||||||
|
execution_prices=opens,
|
||||||
|
valuation_prices=closes,
|
||||||
|
dataset_snapshot=_snapshot(),
|
||||||
|
factor_version=_factor(),
|
||||||
|
strategy_version=_strategy(),
|
||||||
|
risk_policy=policy,
|
||||||
|
code_revision="c" * 40,
|
||||||
|
created_at=created_at,
|
||||||
|
top_k=2,
|
||||||
|
execution_price_field="open",
|
||||||
|
valuation_price_field="close",
|
||||||
|
execution_config=_execution_config(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.backtest_run.dataset_snapshot_id == _snapshot().snapshot_id
|
||||||
|
assert result.backtest_run.factor_version_id == _factor().version_id
|
||||||
|
assert result.backtest_run.strategy_version_id == _strategy().version_id
|
||||||
|
assert result.backtest_run.code_revision == "c" * 40
|
||||||
|
assert len(result.backtest_run.config_hash) == 64
|
||||||
|
assert result.portfolio_target.backtest_run_id == result.backtest_run.run_id
|
||||||
|
assert result.risk_decision.status is RiskDecisionStatus.APPROVED
|
||||||
|
assert result.risk_decision.portfolio_target_id == result.portfolio_target.target_id
|
||||||
|
assert result.order_intent is not None
|
||||||
|
assert result.order_intent.environment == "paper"
|
||||||
|
assert result.order_intent.risk_decision_id == result.risk_decision.decision_id
|
||||||
|
assert result.order_intent.portfolio_target_id == result.portfolio_target.target_id
|
||||||
|
assert result.factor_version.version_id == "factor:demo-momentum@1.0.0"
|
||||||
|
assert result.factor_version.definition_sha256 == "b" * 64
|
||||||
|
assert result.strategy_version.version_id == "strategy:demo-top2@1.0.0"
|
||||||
|
assert result.backtest_run.run_id == (
|
||||||
|
"backtest-run:e74403571f6a73c98b380220748957a422518c0bed883fabc4b93ebe13f05a37"
|
||||||
|
)
|
||||||
|
assert result.backtest_run.config_hash == (
|
||||||
|
"40a3c804a2dc940161a626d1a5d25817c13463e685005e37fc48c41d1e20b87b"
|
||||||
|
)
|
||||||
|
assert result.portfolio_target.target_id == (
|
||||||
|
"portfolio-target:ab2d398489aa9a292ee155a1098e9340beac0a924874cdaeb5b7b4d379ac9ce8"
|
||||||
|
)
|
||||||
|
assert result.risk_decision.decision_id == (
|
||||||
|
"risk-decision:95924926bd327e44beeb15a63f47d14c5e77b97533fc3227fba2eda68e9b423d"
|
||||||
|
)
|
||||||
|
assert result.order_intent.intent_id == (
|
||||||
|
"order-intent:73c349086c2c05b68424ace9286896eb21507b9080da5ff9b96b58c88d8f6ac1"
|
||||||
|
)
|
||||||
|
|
||||||
|
repeated = run_governed_factor_slice(
|
||||||
|
factor_scores=_scores(),
|
||||||
|
execution_prices=opens,
|
||||||
|
valuation_prices=closes,
|
||||||
|
dataset_snapshot=_snapshot(),
|
||||||
|
factor_version=_factor(),
|
||||||
|
strategy_version=_strategy(),
|
||||||
|
risk_policy=policy,
|
||||||
|
code_revision="c" * 40,
|
||||||
|
created_at=created_at,
|
||||||
|
top_k=2,
|
||||||
|
execution_price_field="open",
|
||||||
|
valuation_price_field="close",
|
||||||
|
execution_config=_execution_config(),
|
||||||
|
)
|
||||||
|
assert repeated.backtest_run.run_id == result.backtest_run.run_id
|
||||||
|
assert repeated.portfolio_target.target_id == result.portfolio_target.target_id
|
||||||
|
assert repeated.risk_decision.decision_id == result.risk_decision.decision_id
|
||||||
|
assert repeated.order_intent == result.order_intent
|
||||||
|
|
||||||
|
|
||||||
|
def test_risk_rejection_blocks_order_intent() -> None:
|
||||||
|
opens, closes = _prices()
|
||||||
|
result = run_governed_factor_slice(
|
||||||
|
factor_scores=_scores(),
|
||||||
|
execution_prices=opens,
|
||||||
|
valuation_prices=closes,
|
||||||
|
dataset_snapshot=_snapshot(),
|
||||||
|
factor_version=_factor(),
|
||||||
|
strategy_version=_strategy(),
|
||||||
|
risk_policy=RiskPolicy(
|
||||||
|
policy_id="risk:no-concentration@1.0.0",
|
||||||
|
max_gross_exposure=1.0,
|
||||||
|
max_single_asset_weight=0.4,
|
||||||
|
max_positions=10,
|
||||||
|
),
|
||||||
|
code_revision="c" * 40,
|
||||||
|
created_at=datetime(2026, 1, 9, 1, tzinfo=UTC),
|
||||||
|
top_k=2,
|
||||||
|
execution_price_field="open",
|
||||||
|
valuation_price_field="close",
|
||||||
|
execution_config=_execution_config(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.risk_decision.status is RiskDecisionStatus.REJECTED
|
||||||
|
assert any("single-asset weight" in reason for reason in result.risk_decision.reasons)
|
||||||
|
assert result.order_intent is None
|
||||||
|
with pytest.raises(ValueError, match="approved risk decision"):
|
||||||
|
create_paper_order_intent(result.portfolio_target, result.risk_decision)
|
||||||
|
with pytest.raises(ValueError, match="approved risk decision"):
|
||||||
|
PaperOrderIntent(result.portfolio_target, result.risk_decision)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataset_snapshot_requires_point_in_time_ordering_and_aware_times() -> None:
|
||||||
|
with pytest.raises(ValueError, match="timezone-aware"):
|
||||||
|
DatasetSnapshot(
|
||||||
|
snapshot_id="dataset:invalid",
|
||||||
|
schema_version="1.0.0",
|
||||||
|
content_sha256="a" * 64,
|
||||||
|
effective_at=datetime(2026, 1, 8, 7),
|
||||||
|
available_at=datetime(2026, 1, 8, 8, tzinfo=UTC),
|
||||||
|
ingested_at=datetime(2026, 1, 8, 9, tzinfo=UTC),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="effective_at <= available_at <= ingested_at"):
|
||||||
|
DatasetSnapshot(
|
||||||
|
snapshot_id="dataset:invalid",
|
||||||
|
schema_version="1.0.0",
|
||||||
|
content_sha256="a" * 64,
|
||||||
|
effective_at=datetime(2026, 1, 8, 9, tzinfo=UTC),
|
||||||
|
available_at=datetime(2026, 1, 8, 8, tzinfo=UTC),
|
||||||
|
ingested_at=datetime(2026, 1, 8, 10, tzinfo=UTC),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_factor_lineage_must_match() -> None:
|
||||||
|
opens, closes = _prices()
|
||||||
|
mismatched = StrategyVersion(
|
||||||
|
strategy_id="strategy:demo-top2",
|
||||||
|
version="1.0.0",
|
||||||
|
factor_version_id="factor:other@1.0.0",
|
||||||
|
stage=StrategyStage.APPROVED,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="factor lineage"):
|
||||||
|
run_governed_factor_slice(
|
||||||
|
factor_scores=_scores(),
|
||||||
|
execution_prices=opens,
|
||||||
|
valuation_prices=closes,
|
||||||
|
dataset_snapshot=_snapshot(),
|
||||||
|
factor_version=_factor(),
|
||||||
|
strategy_version=mismatched,
|
||||||
|
risk_policy=RiskPolicy(
|
||||||
|
policy_id="risk:paper-default@1.0.0",
|
||||||
|
max_gross_exposure=1.0,
|
||||||
|
max_single_asset_weight=0.6,
|
||||||
|
max_positions=10,
|
||||||
|
),
|
||||||
|
code_revision="c" * 40,
|
||||||
|
created_at=datetime(2026, 1, 9, 1, tzinfo=UTC),
|
||||||
|
top_k=2,
|
||||||
|
execution_price_field="open",
|
||||||
|
valuation_price_field="close",
|
||||||
|
execution_config=_execution_config(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_governed_slice_requires_matching_schema_and_snapshot_available_by_run_time() -> None:
|
||||||
|
opens, closes = _prices()
|
||||||
|
common = {
|
||||||
|
"factor_scores": _scores(),
|
||||||
|
"execution_prices": opens,
|
||||||
|
"valuation_prices": closes,
|
||||||
|
"strategy_version": _strategy(),
|
||||||
|
"risk_policy": RiskPolicy(
|
||||||
|
policy_id="risk:paper-default@1.0.0",
|
||||||
|
max_gross_exposure=1.0,
|
||||||
|
max_single_asset_weight=0.6,
|
||||||
|
max_positions=10,
|
||||||
|
),
|
||||||
|
"code_revision": "c" * 40,
|
||||||
|
"top_k": 2,
|
||||||
|
"execution_price_field": "open",
|
||||||
|
"valuation_price_field": "close",
|
||||||
|
"execution_config": _execution_config(),
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="dataset schema"):
|
||||||
|
run_governed_factor_slice(
|
||||||
|
dataset_snapshot=_snapshot(),
|
||||||
|
factor_version=FactorVersion(
|
||||||
|
factor_id="factor:demo-momentum",
|
||||||
|
version="1.0.0",
|
||||||
|
definition_sha256="b" * 64,
|
||||||
|
dataset_schema_version="2.0.0",
|
||||||
|
),
|
||||||
|
created_at=datetime(2026, 1, 9, 1, tzinfo=UTC),
|
||||||
|
**common,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="available before the research run"):
|
||||||
|
run_governed_factor_slice(
|
||||||
|
dataset_snapshot=_snapshot(),
|
||||||
|
factor_version=_factor(),
|
||||||
|
created_at=datetime(2026, 1, 8, 7, 30, tzinfo=UTC),
|
||||||
|
**common,
|
||||||
|
)
|
||||||
|
|
||||||
|
future_scores = _scores()
|
||||||
|
future_scores.index = pd.date_range("2026-01-12", periods=2, freq="B")
|
||||||
|
with pytest.raises(ValueError, match="future decision dates"):
|
||||||
|
run_governed_factor_slice(
|
||||||
|
dataset_snapshot=_snapshot(),
|
||||||
|
factor_version=_factor(),
|
||||||
|
factor_scores=future_scores,
|
||||||
|
execution_prices=opens,
|
||||||
|
valuation_prices=closes,
|
||||||
|
strategy_version=common["strategy_version"],
|
||||||
|
risk_policy=common["risk_policy"],
|
||||||
|
code_revision="c" * 40,
|
||||||
|
created_at=datetime(2026, 1, 9, 1, tzinfo=UTC),
|
||||||
|
top_k=2,
|
||||||
|
execution_price_field="open",
|
||||||
|
valuation_price_field="close",
|
||||||
|
execution_config=_execution_config(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_paper_intent_requires_approved_strategy_stage() -> None:
|
||||||
|
opens, closes = _prices()
|
||||||
|
validated = StrategyVersion(
|
||||||
|
strategy_id="strategy:demo-top2",
|
||||||
|
version="1.0.0",
|
||||||
|
factor_version_id=_factor().version_id,
|
||||||
|
stage=StrategyStage.VALIDATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Approved or Paper"):
|
||||||
|
run_governed_factor_slice(
|
||||||
|
factor_scores=_scores(),
|
||||||
|
execution_prices=opens,
|
||||||
|
valuation_prices=closes,
|
||||||
|
dataset_snapshot=_snapshot(),
|
||||||
|
factor_version=_factor(),
|
||||||
|
strategy_version=validated,
|
||||||
|
risk_policy=RiskPolicy(
|
||||||
|
policy_id="risk:paper-default@1.0.0",
|
||||||
|
max_gross_exposure=1.0,
|
||||||
|
max_single_asset_weight=0.6,
|
||||||
|
max_positions=10,
|
||||||
|
),
|
||||||
|
code_revision="c" * 40,
|
||||||
|
created_at=datetime(2026, 1, 9, 1, tzinfo=UTC),
|
||||||
|
top_k=2,
|
||||||
|
execution_price_field="open",
|
||||||
|
valuation_price_field="close",
|
||||||
|
execution_config=_execution_config(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,727 @@
|
|||||||
|
"""Closed performance-evidence contract conformance tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from dataclasses import replace
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from quant_engine.artifact import (
|
||||||
|
PERFORMANCE_EVIDENCE_SCHEMA_VERSION,
|
||||||
|
PERFORMANCE_METRIC_SCHEMA_ID,
|
||||||
|
PERFORMANCE_METHODOLOGY_ID,
|
||||||
|
BacktestEvidenceManifest,
|
||||||
|
EvidenceQualification,
|
||||||
|
PerformanceEvidenceError,
|
||||||
|
PerformanceEvidenceErrorCode,
|
||||||
|
PerformanceEvidenceV1,
|
||||||
|
PerformanceMetricAvailability,
|
||||||
|
ResearchRunArtifact,
|
||||||
|
build_backtest_evidence_manifest,
|
||||||
|
build_performance_evidence,
|
||||||
|
build_research_run_artifact,
|
||||||
|
)
|
||||||
|
from quant_engine.execution import ExecutionConfig
|
||||||
|
from quant_engine.factor_contracts import (
|
||||||
|
ActorIdentity,
|
||||||
|
AvailabilityMode,
|
||||||
|
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 BacktestRunRef
|
||||||
|
from quant_engine.metrics import TRADING_DAYS_PER_YEAR, benchmark_summary, summary
|
||||||
|
from quant_engine.research_pipeline import FactorBacktestResult, run_factor_backtest_research
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
FACTOR_FIXTURE = ROOT / "tests" / "fixtures" / "factor-contracts-v1.golden.json"
|
||||||
|
PERFORMANCE_FIXTURE = (
|
||||||
|
ROOT / "tests" / "fixtures" / "performance-evidence-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}
|
||||||
|
|
||||||
|
|
||||||
|
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=AvailabilityMode.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 _configuration_digest() -> str:
|
||||||
|
return _sha256(
|
||||||
|
json.dumps(
|
||||||
|
PARAMETERS,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_ref(**overrides: Any) -> BacktestRunRef:
|
||||||
|
snapshot, foundation, factor_set = _accepted_authorities()
|
||||||
|
arguments: dict[str, Any] = {
|
||||||
|
"dataset_snapshot": snapshot,
|
||||||
|
"foundation": foundation,
|
||||||
|
"factor_set": factor_set,
|
||||||
|
"universe_digest": "sha256:" + "5" * 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": _configuration_digest(),
|
||||||
|
"evaluation_at": "2026-01-08T01:00:00Z",
|
||||||
|
"computed_at": "2026-01-08T02:00:00Z",
|
||||||
|
}
|
||||||
|
arguments.update(overrides)
|
||||||
|
return BacktestRunRef.create(**arguments)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
benchmark_kind: str,
|
||||||
|
) -> tuple[ResearchRunArtifact, FactorBacktestResult]:
|
||||||
|
result = _backtest_result()
|
||||||
|
benchmark_id: str | None
|
||||||
|
benchmark_returns: pd.Series | None
|
||||||
|
if benchmark_kind == "absent":
|
||||||
|
benchmark_id = None
|
||||||
|
benchmark_returns = None
|
||||||
|
elif benchmark_kind == "estimable":
|
||||||
|
benchmark_id = "000300.SH"
|
||||||
|
benchmark_returns = pd.Series(
|
||||||
|
[0.0, 0.01, -0.01, 0.02],
|
||||||
|
index=result.returns.index,
|
||||||
|
name="benchmark_return",
|
||||||
|
)
|
||||||
|
elif benchmark_kind == "zero_active_variance":
|
||||||
|
benchmark_id = "000300.SH"
|
||||||
|
benchmark_returns = result.returns.rename("benchmark_return")
|
||||||
|
elif benchmark_kind == "zero_benchmark_variance":
|
||||||
|
benchmark_id = "000300.SH"
|
||||||
|
benchmark_returns = pd.Series(
|
||||||
|
np.zeros(len(result.returns)),
|
||||||
|
index=result.returns.index,
|
||||||
|
name="benchmark_return",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"unknown benchmark_kind: {benchmark_kind}")
|
||||||
|
artifact = 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=benchmark_id,
|
||||||
|
benchmark_returns=benchmark_returns,
|
||||||
|
)
|
||||||
|
return artifact, result
|
||||||
|
|
||||||
|
|
||||||
|
def _case(
|
||||||
|
benchmark_kind: str,
|
||||||
|
) -> tuple[PerformanceEvidenceV1, ResearchRunArtifact, BacktestRunRef, BacktestEvidenceManifest]:
|
||||||
|
run_ref = _run_ref()
|
||||||
|
artifact, _ = _artifact(run_ref, benchmark_kind)
|
||||||
|
manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
qualification=EvidenceQualification.CONTRACT_QUALIFIED,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
build_performance_evidence(artifact, run_ref, manifest),
|
||||||
|
artifact,
|
||||||
|
run_ref,
|
||||||
|
manifest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _metric_map(evidence: PerformanceEvidenceV1) -> dict[str, Any]:
|
||||||
|
return {metric.key: metric for metric in evidence.metrics}
|
||||||
|
|
||||||
|
|
||||||
|
def _mutate_frozen(value: Any, field: str, replacement: object) -> Any:
|
||||||
|
changed = copy.copy(value)
|
||||||
|
object.__setattr__(changed, field, replacement)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_error(
|
||||||
|
error: pytest.ExceptionInfo[PerformanceEvidenceError],
|
||||||
|
code: PerformanceEvidenceErrorCode,
|
||||||
|
path: str,
|
||||||
|
) -> None:
|
||||||
|
assert error.value.code is code
|
||||||
|
assert error.value.path == path
|
||||||
|
|
||||||
|
|
||||||
|
def test_present_evidence_is_deterministic_content_addressed_and_three_party_closed() -> None:
|
||||||
|
first, artifact, run_ref, manifest = _case("estimable")
|
||||||
|
second = build_performance_evidence(artifact, run_ref, manifest)
|
||||||
|
|
||||||
|
assert first == second
|
||||||
|
assert first.schema_version == PERFORMANCE_EVIDENCE_SCHEMA_VERSION
|
||||||
|
assert first.performance_evidence_id.startswith("rhperformanceevidencev1:sha256:")
|
||||||
|
assert first.document_sha256.startswith("sha256:")
|
||||||
|
assert first.authority == "quant_engine"
|
||||||
|
assert first.scope == "offline_research_only"
|
||||||
|
assert first.run_id == first.backtest_run_ref_id == run_ref.run_id == manifest.run_id
|
||||||
|
assert first.backtest_evidence_manifest_id == manifest.manifest_id
|
||||||
|
assert first.backtest_evidence_manifest_evidence_digest == manifest.evidence_digest
|
||||||
|
assert first.backtest_evidence_qualification == "contract_qualified"
|
||||||
|
assert first.research_artifact_content_digest == f"sha256:{artifact.content_sha256}"
|
||||||
|
assert first.performance_table_logical_name == "performance"
|
||||||
|
assert first.performance_table_row_count == 1
|
||||||
|
assert first.performance_row_digest.startswith("sha256:")
|
||||||
|
assert first.benchmark_series_digest is not None
|
||||||
|
assert first.canonical_bytes() == first.to_json().encode("utf-8")
|
||||||
|
assert not first.canonical_bytes().endswith(b"\n")
|
||||||
|
document_payload = first.to_dict()
|
||||||
|
document_payload.pop("document_sha256")
|
||||||
|
expected_document = json.dumps(
|
||||||
|
document_payload,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
assert _sha256(expected_document) == first.document_sha256
|
||||||
|
assert PerformanceEvidenceV1.from_dict(
|
||||||
|
first.to_dict(),
|
||||||
|
artifact=artifact,
|
||||||
|
run_ref=run_ref,
|
||||||
|
evidence_manifest=manifest,
|
||||||
|
) == first
|
||||||
|
|
||||||
|
|
||||||
|
def test_methodology_and_metrics_bind_the_actual_artifact_builder_path() -> None:
|
||||||
|
evidence, artifact, _, _ = _case("estimable")
|
||||||
|
result = _backtest_result()
|
||||||
|
expected_absolute = summary(result.returns, rf=0.0)
|
||||||
|
benchmark = artifact.nav.set_index("trade_date")["benchmark_return"]
|
||||||
|
benchmark.index = result.returns.index
|
||||||
|
expected_relative = benchmark_summary(
|
||||||
|
result.returns,
|
||||||
|
benchmark,
|
||||||
|
risk_free_daily=0.0,
|
||||||
|
annualization=TRADING_DAYS_PER_YEAR,
|
||||||
|
)
|
||||||
|
metrics = _metric_map(evidence)
|
||||||
|
|
||||||
|
assert evidence.methodology.methodology_id == PERFORMANCE_METHODOLOGY_ID
|
||||||
|
assert evidence.metric_schema_id == PERFORMANCE_METRIC_SCHEMA_ID
|
||||||
|
assert evidence.methodology.return_type == "simple"
|
||||||
|
assert evidence.methodology.source_frequency == "1d"
|
||||||
|
assert evidence.methodology.periods_per_year == TRADING_DAYS_PER_YEAR == 252
|
||||||
|
assert evidence.methodology.annual_risk_free == 0.0
|
||||||
|
assert evidence.methodology.benchmark_risk_free_daily == 0.0
|
||||||
|
assert evidence.methodology.benchmark_alignment == "exact_session_index"
|
||||||
|
assert metrics["annualized_return"].value == pytest.approx(
|
||||||
|
expected_absolute["ann_return"]
|
||||||
|
)
|
||||||
|
assert metrics["sharpe_ratio"].value == pytest.approx(expected_absolute["sharpe"])
|
||||||
|
assert metrics["tracking_error"].value == pytest.approx(
|
||||||
|
expected_relative["tracking_error"]
|
||||||
|
)
|
||||||
|
assert metrics["alpha"].value == pytest.approx(expected_relative["alpha"])
|
||||||
|
assert all(metric.methodology_id == PERFORMANCE_METHODOLOGY_ID for metric in metrics.values())
|
||||||
|
assert all(metric.metric_schema_id == PERFORMANCE_METRIC_SCHEMA_ID for metric in metrics.values())
|
||||||
|
|
||||||
|
|
||||||
|
def test_relative_metric_availability_is_closed_for_present_absent_and_unestimable() -> None:
|
||||||
|
present, *_ = _case("estimable")
|
||||||
|
absent, *_ = _case("absent")
|
||||||
|
zero_active, *_ = _case("zero_active_variance")
|
||||||
|
zero_benchmark, *_ = _case("zero_benchmark_variance")
|
||||||
|
|
||||||
|
present_metrics = _metric_map(present)
|
||||||
|
assert all(
|
||||||
|
present_metrics[key].availability is PerformanceMetricAvailability.AVAILABLE
|
||||||
|
for key in ("tracking_error", "information_ratio", "alpha", "beta")
|
||||||
|
)
|
||||||
|
absent_metrics = _metric_map(absent)
|
||||||
|
assert absent.benchmark_series_digest is None
|
||||||
|
assert absent.benchmark_id == ""
|
||||||
|
assert absent.benchmark_alignment_policy == "none"
|
||||||
|
assert all(
|
||||||
|
absent_metrics[key].value is None
|
||||||
|
and absent_metrics[key].availability
|
||||||
|
is PerformanceMetricAvailability.BENCHMARK_ABSENT
|
||||||
|
for key in ("tracking_error", "information_ratio", "alpha", "beta")
|
||||||
|
)
|
||||||
|
zero_active_metrics = _metric_map(zero_active)
|
||||||
|
assert zero_active_metrics["tracking_error"].value == pytest.approx(0.0)
|
||||||
|
assert (
|
||||||
|
zero_active_metrics["information_ratio"].availability
|
||||||
|
is PerformanceMetricAvailability.NOT_ESTIMABLE_ACTIVE_VARIANCE
|
||||||
|
)
|
||||||
|
assert zero_active_metrics["information_ratio"].value is None
|
||||||
|
zero_benchmark_metrics = _metric_map(zero_benchmark)
|
||||||
|
assert np.isfinite(zero_benchmark_metrics["tracking_error"].value)
|
||||||
|
for key in ("alpha", "beta"):
|
||||||
|
assert zero_benchmark_metrics[key].value is None
|
||||||
|
assert (
|
||||||
|
zero_benchmark_metrics[key].availability
|
||||||
|
is PerformanceMetricAvailability.NOT_ESTIMABLE_BENCHMARK_VARIANCE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_covers_present_absent_and_both_unestimable_states() -> None:
|
||||||
|
expected = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"source_commit": "a724e1e57a99d1304a932d01ee836bac56c5c15c",
|
||||||
|
"source_tree": "4774e88442d25bf79a54eab3d7106ff4d0ba9603",
|
||||||
|
"cases": {
|
||||||
|
name: _case(name)[0].to_dict()
|
||||||
|
for name in (
|
||||||
|
"estimable",
|
||||||
|
"zero_active_variance",
|
||||||
|
"zero_benchmark_variance",
|
||||||
|
"absent",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert json.loads(PERFORMANCE_FIXTURE.read_text(encoding="utf-8")) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("owner", "field", "replacement", "code", "path"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"run_ref",
|
||||||
|
"run_id",
|
||||||
|
"rhbacktestrunv1:sha256:" + "0" * 64,
|
||||||
|
PerformanceEvidenceErrorCode.IDENTITY_MISMATCH,
|
||||||
|
"$.backtest_run_ref.run_id",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"manifest",
|
||||||
|
"manifest_id",
|
||||||
|
"rhbacktestevidencev1:sha256:" + "0" * 64,
|
||||||
|
PerformanceEvidenceErrorCode.EVIDENCE_MISMATCH,
|
||||||
|
"$.backtest_evidence_manifest.manifest_id",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"manifest",
|
||||||
|
"qualification",
|
||||||
|
EvidenceQualification.EXPLORATORY,
|
||||||
|
PerformanceEvidenceErrorCode.AUTHORITY_REJECTED,
|
||||||
|
"$.backtest_evidence_manifest.qualification",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_owner_identity_and_authority_mismatches_fail_closed(
|
||||||
|
owner: str,
|
||||||
|
field: str,
|
||||||
|
replacement: object,
|
||||||
|
code: PerformanceEvidenceErrorCode,
|
||||||
|
path: str,
|
||||||
|
) -> None:
|
||||||
|
_, artifact, run_ref, manifest = _case("estimable")
|
||||||
|
changed_run_ref = _mutate_frozen(run_ref, field, replacement) if owner == "run_ref" else run_ref
|
||||||
|
changed_manifest = (
|
||||||
|
_mutate_frozen(manifest, field, replacement) if owner == "manifest" else manifest
|
||||||
|
)
|
||||||
|
with pytest.raises(PerformanceEvidenceError) as rejected:
|
||||||
|
build_performance_evidence(artifact, changed_run_ref, changed_manifest)
|
||||||
|
_assert_error(rejected, code, path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_performance_table_row_and_benchmark_digest_mismatches_fail_closed() -> None:
|
||||||
|
evidence, artifact, run_ref, manifest = _case("estimable")
|
||||||
|
performance = artifact.performance
|
||||||
|
performance.loc[0, "n_days"] += 1
|
||||||
|
changed_artifact = replace(artifact, _performance=performance)
|
||||||
|
with pytest.raises(PerformanceEvidenceError) as table_mismatch:
|
||||||
|
build_performance_evidence(changed_artifact, run_ref, manifest)
|
||||||
|
_assert_error(
|
||||||
|
table_mismatch,
|
||||||
|
PerformanceEvidenceErrorCode.EVIDENCE_MISMATCH,
|
||||||
|
"$.artifact.tables.performance.content_digest",
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = evidence.to_dict()
|
||||||
|
payload["benchmark_series_digest"] = "sha256:" + "0" * 64
|
||||||
|
with pytest.raises(PerformanceEvidenceError) as benchmark_mismatch:
|
||||||
|
PerformanceEvidenceV1.from_dict(
|
||||||
|
payload,
|
||||||
|
artifact=artifact,
|
||||||
|
run_ref=run_ref,
|
||||||
|
evidence_manifest=manifest,
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
benchmark_mismatch,
|
||||||
|
PerformanceEvidenceErrorCode.BENCHMARK_INVALID,
|
||||||
|
"$.benchmark_series_digest",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_manifest_closure_covers_non_performance_artifact_tables() -> None:
|
||||||
|
_, artifact, run_ref, manifest = _case("estimable")
|
||||||
|
nav = artifact.nav
|
||||||
|
nav.loc[0, "nav"] += 0.01
|
||||||
|
changed_artifact = replace(artifact, _nav=nav)
|
||||||
|
|
||||||
|
with pytest.raises(PerformanceEvidenceError) as rejected:
|
||||||
|
build_performance_evidence(changed_artifact, run_ref, manifest)
|
||||||
|
|
||||||
|
_assert_error(
|
||||||
|
rejected,
|
||||||
|
PerformanceEvidenceErrorCode.EVIDENCE_MISMATCH,
|
||||||
|
"$.artifact.tables.nav.content_digest",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_row_and_benchmark_mutations_change_their_digests_and_document_identity() -> None:
|
||||||
|
original, artifact, run_ref, _ = _case("estimable")
|
||||||
|
performance = artifact.performance
|
||||||
|
performance.loc[0, "sharpe"] += 0.01
|
||||||
|
changed_performance_artifact = replace(artifact, _performance=performance)
|
||||||
|
changed_performance_manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
changed_performance_artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
changed_performance = build_performance_evidence(
|
||||||
|
changed_performance_artifact,
|
||||||
|
run_ref,
|
||||||
|
changed_performance_manifest,
|
||||||
|
)
|
||||||
|
assert changed_performance.performance_row_digest != original.performance_row_digest
|
||||||
|
assert changed_performance.performance_evidence_id != original.performance_evidence_id
|
||||||
|
|
||||||
|
nav = artifact.nav
|
||||||
|
nav.loc[0, "benchmark_nav"] += 0.01
|
||||||
|
changed_benchmark_artifact = replace(artifact, _nav=nav)
|
||||||
|
changed_benchmark_manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
changed_benchmark_artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
changed_benchmark = build_performance_evidence(
|
||||||
|
changed_benchmark_artifact,
|
||||||
|
run_ref,
|
||||||
|
changed_benchmark_manifest,
|
||||||
|
)
|
||||||
|
assert changed_benchmark.benchmark_series_digest != original.benchmark_series_digest
|
||||||
|
assert changed_benchmark.performance_row_digest == original.performance_row_digest
|
||||||
|
assert changed_benchmark.performance_evidence_id != original.performance_evidence_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_relative_metric_null_reasons_cannot_be_invented() -> None:
|
||||||
|
_, artifact, run_ref, _ = _case("estimable")
|
||||||
|
performance = artifact.performance
|
||||||
|
performance.loc[0, "alpha"] = float("nan")
|
||||||
|
changed_artifact = replace(artifact, _performance=performance)
|
||||||
|
changed_manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
changed_artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
with pytest.raises(PerformanceEvidenceError) as false_alpha_domain:
|
||||||
|
build_performance_evidence(changed_artifact, run_ref, changed_manifest)
|
||||||
|
_assert_error(
|
||||||
|
false_alpha_domain,
|
||||||
|
PerformanceEvidenceErrorCode.METRIC_INVALID,
|
||||||
|
"$.metrics.alpha.value",
|
||||||
|
)
|
||||||
|
|
||||||
|
_, absent_artifact, absent_run_ref, _ = _case("absent")
|
||||||
|
absent_performance = absent_artifact.performance
|
||||||
|
absent_performance.loc[0, "tracking_error"] = 0.0
|
||||||
|
changed_absent = replace(absent_artifact, _performance=absent_performance)
|
||||||
|
changed_absent_manifest = build_backtest_evidence_manifest(
|
||||||
|
absent_run_ref,
|
||||||
|
changed_absent,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
with pytest.raises(PerformanceEvidenceError) as false_absence:
|
||||||
|
build_performance_evidence(
|
||||||
|
changed_absent,
|
||||||
|
absent_run_ref,
|
||||||
|
changed_absent_manifest,
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
false_absence,
|
||||||
|
PerformanceEvidenceErrorCode.BENCHMARK_INVALID,
|
||||||
|
"$.metrics.tracking_error.availability",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_artifact_builder_enforces_strict_benchmark_session_alignment() -> None:
|
||||||
|
run_ref = _run_ref()
|
||||||
|
result = _backtest_result()
|
||||||
|
misaligned = pd.Series(
|
||||||
|
[0.0, 0.01, -0.01, 0.02],
|
||||||
|
index=result.returns.index.shift(1, freq="B"),
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="matching indexes"):
|
||||||
|
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=misaligned,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("column", "value", "path"),
|
||||||
|
[
|
||||||
|
("total_ret", -1.01, "$.metrics.total_return.value"),
|
||||||
|
("ann_ret", -1.01, "$.metrics.annualized_return.value"),
|
||||||
|
("ann_volatility", -0.01, "$.metrics.annualized_volatility.value"),
|
||||||
|
("max_dd", 0.01, "$.metrics.maximum_drawdown.value"),
|
||||||
|
("win_rate", 1.01, "$.metrics.win_rate.value"),
|
||||||
|
("tracking_error", -0.01, "$.metrics.tracking_error.value"),
|
||||||
|
("n_trades", True, "$.metrics.trade_count.value"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_metric_domains_reject_invalid_source_values(
|
||||||
|
column: str,
|
||||||
|
value: object,
|
||||||
|
path: str,
|
||||||
|
) -> None:
|
||||||
|
_, artifact, run_ref, _ = _case("estimable")
|
||||||
|
performance = artifact.performance.astype(object)
|
||||||
|
performance.at[0, column] = value
|
||||||
|
changed_artifact = replace(artifact, _performance=performance)
|
||||||
|
changed_manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
changed_artifact,
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
with pytest.raises(PerformanceEvidenceError) as rejected:
|
||||||
|
build_performance_evidence(changed_artifact, run_ref, changed_manifest)
|
||||||
|
_assert_error(rejected, PerformanceEvidenceErrorCode.METRIC_INVALID, path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_closed_parser_rejects_unknown_non_ascii_non_finite_bool_and_unsafe_integer() -> None:
|
||||||
|
evidence, artifact, run_ref, manifest = _case("estimable")
|
||||||
|
|
||||||
|
mutations: list[tuple[dict[str, Any], PerformanceEvidenceErrorCode, str]] = []
|
||||||
|
unknown = evidence.to_dict()
|
||||||
|
unknown["unexpected"] = "value"
|
||||||
|
mutations.append((unknown, PerformanceEvidenceErrorCode.TYPE_ERROR, "$.unexpected"))
|
||||||
|
non_ascii = evidence.to_dict()
|
||||||
|
non_ascii["métric"] = "value"
|
||||||
|
mutations.append((non_ascii, PerformanceEvidenceErrorCode.TYPE_ERROR, "$.métric"))
|
||||||
|
non_finite = evidence.to_dict()
|
||||||
|
non_finite["metrics"][0]["value"] = float("inf")
|
||||||
|
mutations.append(
|
||||||
|
(non_finite, PerformanceEvidenceErrorCode.METRIC_INVALID, "$.metrics[0].value")
|
||||||
|
)
|
||||||
|
bool_number = evidence.to_dict()
|
||||||
|
bool_number["methodology"]["periods_per_year"] = True
|
||||||
|
mutations.append(
|
||||||
|
(
|
||||||
|
bool_number,
|
||||||
|
PerformanceEvidenceErrorCode.METHODOLOGY_MISMATCH,
|
||||||
|
"$.methodology.periods_per_year",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
unsafe = evidence.to_dict()
|
||||||
|
unsafe["performance_table_row_count"] = 2**53
|
||||||
|
mutations.append(
|
||||||
|
(
|
||||||
|
unsafe,
|
||||||
|
PerformanceEvidenceErrorCode.EVIDENCE_MISMATCH,
|
||||||
|
"$.performance_table_row_count",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for payload, code, path in mutations:
|
||||||
|
with pytest.raises(PerformanceEvidenceError) as rejected:
|
||||||
|
PerformanceEvidenceV1.from_dict(
|
||||||
|
payload,
|
||||||
|
artifact=artifact,
|
||||||
|
run_ref=run_ref,
|
||||||
|
evidence_manifest=manifest,
|
||||||
|
)
|
||||||
|
_assert_error(rejected, code, path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_mapping_has_no_raw_inputs_storage_or_runtime_authority() -> None:
|
||||||
|
evidence, *_ = _case("estimable")
|
||||||
|
payload = evidence.to_dict()
|
||||||
|
serialized = evidence.to_json().lower()
|
||||||
|
forbidden_keys = {
|
||||||
|
"parameters",
|
||||||
|
"params_json",
|
||||||
|
"returns",
|
||||||
|
"nav",
|
||||||
|
"benchmark_series",
|
||||||
|
"table_bytes",
|
||||||
|
"locator",
|
||||||
|
"uri",
|
||||||
|
"credential",
|
||||||
|
"decision_eligible",
|
||||||
|
"publication_eligible",
|
||||||
|
"paper_trading",
|
||||||
|
"live_trading",
|
||||||
|
"investment_advice",
|
||||||
|
}
|
||||||
|
|
||||||
|
def keys(value: object) -> set[str]:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return set(value) | {key for item in value.values() for key in keys(item)}
|
||||||
|
if isinstance(value, list):
|
||||||
|
return {key for item in value for key in keys(item)}
|
||||||
|
return set()
|
||||||
|
|
||||||
|
assert not (keys(payload) & forbidden_keys)
|
||||||
|
for token in ("postgres://", "mysql://", "s3://", "credential", "broker"):
|
||||||
|
assert token not in serialized
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user