This commit was merged in pull request #15.
This commit is contained in:
+11
-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": 2, "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",
|
||||||
@@ -24,7 +24,16 @@
|
|||||||
{"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"}
|
||||||
|
],
|
||||||
|
"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,6 +19,7 @@
|
|||||||
## 模块
|
## 模块
|
||||||
|
|
||||||
- `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 代理)
|
||||||
@@ -195,6 +196,51 @@ 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 静默升级为新合同。
|
||||||
|
|
||||||
## 治理垂直切片
|
## 治理垂直切片
|
||||||
|
|
||||||
`governed_pipeline` 不复制因子、回测、组合或执行算法,只编排现有能力并补充版本与风险契约。
|
`governed_pipeline` 不复制因子、回测、组合或执行算法,只编排现有能力并补充版本与风险契约。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,12 @@ from types import MappingProxyType
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from quant_engine.execution import ExecutionConfig
|
from quant_engine.execution import ExecutionConfig
|
||||||
|
from quant_engine.factor_contracts import (
|
||||||
|
ContractErrorCode,
|
||||||
|
FactorContractError,
|
||||||
|
FactorDefinition,
|
||||||
|
LegacyFactorBinding,
|
||||||
|
)
|
||||||
from quant_engine.research_pipeline import FactorBacktestResult, run_factor_backtest_research
|
from quant_engine.research_pipeline import FactorBacktestResult, run_factor_backtest_research
|
||||||
|
|
||||||
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||||
@@ -28,6 +34,8 @@ _GIT_SHA = re.compile(r"^[0-9a-f]{40}$")
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"DatasetSnapshot",
|
"DatasetSnapshot",
|
||||||
"FactorVersion",
|
"FactorVersion",
|
||||||
|
"bind_legacy_factor",
|
||||||
|
"project_legacy_factor",
|
||||||
"StrategyStage",
|
"StrategyStage",
|
||||||
"StrategyVersion",
|
"StrategyVersion",
|
||||||
"BacktestRun",
|
"BacktestRun",
|
||||||
@@ -160,6 +168,80 @@ class FactorVersion:
|
|||||||
return f"{self.factor_id}@{self.version}"
|
return f"{self.factor_id}@{self.version}"
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_legacy_factor_binding(
|
||||||
|
legacy: FactorVersion,
|
||||||
|
definition: FactorDefinition,
|
||||||
|
binding: LegacyFactorBinding,
|
||||||
|
) -> None:
|
||||||
|
if not isinstance(legacy, FactorVersion):
|
||||||
|
raise FactorContractError(
|
||||||
|
ContractErrorCode.TYPE_ERROR,
|
||||||
|
"$.legacy",
|
||||||
|
"FactorVersion is required",
|
||||||
|
)
|
||||||
|
if not isinstance(definition, FactorDefinition):
|
||||||
|
raise FactorContractError(
|
||||||
|
ContractErrorCode.TYPE_ERROR,
|
||||||
|
"$.definition",
|
||||||
|
"complete FactorDefinition is required",
|
||||||
|
)
|
||||||
|
if not isinstance(binding, LegacyFactorBinding):
|
||||||
|
raise FactorContractError(
|
||||||
|
ContractErrorCode.TYPE_ERROR,
|
||||||
|
"$.binding",
|
||||||
|
"LegacyFactorBinding is required",
|
||||||
|
)
|
||||||
|
expected = (
|
||||||
|
definition.definition_id,
|
||||||
|
legacy.factor_id,
|
||||||
|
legacy.version,
|
||||||
|
legacy.definition_sha256,
|
||||||
|
legacy.dataset_schema_version,
|
||||||
|
definition.input_schema_digest,
|
||||||
|
)
|
||||||
|
actual = (
|
||||||
|
binding.definition_id,
|
||||||
|
binding.legacy_factor_id,
|
||||||
|
binding.legacy_version,
|
||||||
|
binding.legacy_definition_sha256,
|
||||||
|
binding.legacy_dataset_schema_version,
|
||||||
|
binding.canonical_input_schema_digest,
|
||||||
|
)
|
||||||
|
if actual != expected:
|
||||||
|
raise FactorContractError(
|
||||||
|
ContractErrorCode.LEGACY_BINDING_MISMATCH,
|
||||||
|
"$.binding",
|
||||||
|
"binding does not exactly associate the supplied legacy and canonical identities",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bind_legacy_factor(
|
||||||
|
legacy: FactorVersion,
|
||||||
|
complete_definition: FactorDefinition,
|
||||||
|
binding: LegacyFactorBinding,
|
||||||
|
) -> FactorDefinition:
|
||||||
|
"""Validate an explicit migration binding without manufacturing missing semantics."""
|
||||||
|
|
||||||
|
_validate_legacy_factor_binding(legacy, complete_definition, binding)
|
||||||
|
return complete_definition
|
||||||
|
|
||||||
|
|
||||||
|
def project_legacy_factor(
|
||||||
|
complete_definition: FactorDefinition,
|
||||||
|
binding: LegacyFactorBinding,
|
||||||
|
) -> FactorVersion:
|
||||||
|
"""Project a canonical definition into its recorded, explicitly lossy legacy identity."""
|
||||||
|
|
||||||
|
legacy = FactorVersion(
|
||||||
|
factor_id=binding.legacy_factor_id,
|
||||||
|
version=binding.legacy_version,
|
||||||
|
definition_sha256=binding.legacy_definition_sha256,
|
||||||
|
dataset_schema_version=binding.legacy_dataset_schema_version,
|
||||||
|
)
|
||||||
|
_validate_legacy_factor_binding(legacy, complete_definition, binding)
|
||||||
|
return legacy
|
||||||
|
|
||||||
|
|
||||||
class StrategyStage(StrEnum):
|
class StrategyStage(StrEnum):
|
||||||
DRAFT = "Draft"
|
DRAFT = "Draft"
|
||||||
RESEARCH = "Research"
|
RESEARCH = "Research"
|
||||||
|
|||||||
+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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,51 @@
|
|||||||
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"))
|
assert spec["module_id"] == "quant_engine"
|
||||||
self.assertEqual(spec["module_id"], "quant_engine")
|
assert spec["authority"]["subject"] == spec["module_id"]
|
||||||
self.assertEqual(spec["authority"]["subject"], spec["module_id"])
|
assert spec["repository"]["type"] == "research_engine"
|
||||||
self.assertEqual(spec["repository"]["type"], "research_engine")
|
assert spec["bounded_context"]["domain"] == "quantitative-research-engine"
|
||||||
self.assertEqual(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"):
|
assert term in prohibited
|
||||||
self.assertIn(term, prohibited)
|
assert spec["authority"]["revision"] == 2
|
||||||
self.assertEqual(spec["contracts"], {"provides": [], "consumes": []})
|
assert {
|
||||||
self.assertEqual(spec["dependencies"], [])
|
(item["contract_id"], item["version"])
|
||||||
self.assertTrue(
|
for item in spec["contracts"]["provides"]
|
||||||
all(
|
} == {
|
||||||
command["required"] and not command["network"]
|
("researchhub.factor-definition", "1.0.0"),
|
||||||
for command in spec["verification"]["commands"]
|
("researchhub.factor-set-ref", "1.0.0"),
|
||||||
)
|
}
|
||||||
)
|
assert all(
|
||||||
|
item["authority"] == "quant_engine"
|
||||||
|
and item["path"] == "src/quant_engine/factor_contracts.py"
|
||||||
|
for item in spec["contracts"]["provides"]
|
||||||
|
)
|
||||||
|
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"] == []
|
||||||
|
assert all(
|
||||||
|
command["required"] and not command["network"]
|
||||||
|
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,
|
||||||
@@ -434,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")
|
||||||
|
|||||||
@@ -0,0 +1,888 @@
|
|||||||
|
"""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",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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]")
|
||||||
@@ -119,6 +119,24 @@ def test_governed_slice_is_reproducible_and_creates_only_paper_intent() -> None:
|
|||||||
assert result.order_intent.environment == "paper"
|
assert result.order_intent.environment == "paper"
|
||||||
assert result.order_intent.risk_decision_id == result.risk_decision.decision_id
|
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.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(
|
repeated = run_governed_factor_slice(
|
||||||
factor_scores=_scores(),
|
factor_scores=_scores(),
|
||||||
|
|||||||
Reference in New Issue
Block a user