Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a7de4b1f3 | ||
|
|
36d444924a | ||
|
|
e1e10ce293 | ||
|
|
598c2b92a2 | ||
|
|
62ed09842d | ||
|
|
e782e223f7 | ||
|
|
015c1a3602 | ||
|
|
2bc8aea435 | ||
|
|
03e38d5123 | ||
|
|
90a43adda2 | ||
|
|
e72fe0a8d1 | ||
|
|
fd3014c286 | ||
|
|
38a984b245 |
+14
-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": 3, "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",
|
||||||
@@ -18,13 +18,25 @@
|
|||||||
{"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 evidence without persistence or decision 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"}
|
||||||
|
],
|
||||||
|
"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,15 @@
|
|||||||
## 模块
|
## 模块
|
||||||
|
|
||||||
- `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`
|
||||||
- `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 +57,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 +196,86 @@ 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、生产或实盘就绪。
|
||||||
|
|
||||||
|
## 治理垂直切片
|
||||||
|
|
||||||
|
`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 保持向后兼容):
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ v1.2.0 Phase 0:5 个基础算子 + 5 个 alpha 公式(alpha001–alpha005)
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from collections.abc import Callable, Mapping
|
||||||
|
from types import MappingProxyType
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -209,6 +211,367 @@ def indneutralize(series: pd.Series, groups: pd.Series) -> pd.Series:
|
|||||||
return series - series.groupby(groups).transform("mean")
|
return series - series.groupby(groups).transform("mean")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 1 operator contract ──────────────────────────
|
||||||
|
|
||||||
|
# This is deliberately a small, stable surface for downstream research
|
||||||
|
# orchestration. The full alpha158 formula catalogue can continue to grow,
|
||||||
|
# while callers use one validated dispatch entry point for the first ten
|
||||||
|
# deterministic building blocks.
|
||||||
|
ALPHA158_PHASE1_MAX_WINDOW = 252
|
||||||
|
|
||||||
|
ALPHA158_PHASE1_OPERATOR_SPECS: dict[str, dict[str, Any]] = {
|
||||||
|
"rank": {
|
||||||
|
"name": "rank",
|
||||||
|
"formula": "rank(series)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"windowed": False,
|
||||||
|
},
|
||||||
|
"delta": {
|
||||||
|
"name": "delta",
|
||||||
|
"formula": "delta(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"ts_mean": {
|
||||||
|
"name": "ts_mean",
|
||||||
|
"formula": "ts_mean(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"ts_std": {
|
||||||
|
"name": "ts_std",
|
||||||
|
"formula": "ts_std(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"ts_rank": {
|
||||||
|
"name": "ts_rank",
|
||||||
|
"formula": "ts_rank(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"correlation": {
|
||||||
|
"name": "correlation",
|
||||||
|
"formula": "correlation(series, secondary, window)",
|
||||||
|
"inputs": ["series", "secondary"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"ts_min": {
|
||||||
|
"name": "ts_min",
|
||||||
|
"formula": "ts_min(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"ts_max": {
|
||||||
|
"name": "ts_max",
|
||||||
|
"formula": "ts_max(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"ts_sum": {
|
||||||
|
"name": "ts_sum",
|
||||||
|
"formula": "ts_sum(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"decay_linear": {
|
||||||
|
"name": "decay_linear",
|
||||||
|
"formula": "decay_linear(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_PHASE1_OPERATOR_FUNCTIONS: dict[str, Callable[..., pd.Series]] = {
|
||||||
|
"rank": rank,
|
||||||
|
"delta": delta,
|
||||||
|
"ts_mean": ts_mean,
|
||||||
|
"ts_std": ts_std,
|
||||||
|
"ts_rank": ts_rank,
|
||||||
|
"correlation": correlation,
|
||||||
|
"ts_min": ts_min,
|
||||||
|
"ts_max": ts_max,
|
||||||
|
"ts_sum": ts_sum,
|
||||||
|
"decay_linear": decay_linear,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def list_phase1_operators() -> tuple[str, ...]:
|
||||||
|
"""Return the deterministic Phase 1 operator names in stable order."""
|
||||||
|
return tuple(ALPHA158_PHASE1_OPERATOR_SPECS)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_phase1_operator(
|
||||||
|
name: str,
|
||||||
|
series: pd.Series,
|
||||||
|
secondary: pd.Series | None = None,
|
||||||
|
*,
|
||||||
|
window: int | None = None,
|
||||||
|
) -> pd.Series:
|
||||||
|
"""Evaluate one of the ten Phase 1 operators with a validated contract.
|
||||||
|
|
||||||
|
``window`` is required for time-series operators and forbidden for the
|
||||||
|
cross-sectional ``rank`` operator. Binary ``correlation`` also requires
|
||||||
|
a same-index secondary series so that callers cannot silently introduce
|
||||||
|
alignment-dependent results.
|
||||||
|
"""
|
||||||
|
if name not in ALPHA158_PHASE1_OPERATOR_SPECS:
|
||||||
|
raise KeyError(f"operator {name!r} not registered")
|
||||||
|
|
||||||
|
is_windowed = bool(ALPHA158_PHASE1_OPERATOR_SPECS[name]["windowed"])
|
||||||
|
if is_windowed:
|
||||||
|
if isinstance(window, bool) or not isinstance(window, int) or window <= 0:
|
||||||
|
raise ValueError(f"window must be a positive integer for {name}")
|
||||||
|
if window > ALPHA158_PHASE1_MAX_WINDOW:
|
||||||
|
raise ValueError(
|
||||||
|
f"window exceeds maximum supported value {ALPHA158_PHASE1_MAX_WINDOW} for {name}"
|
||||||
|
)
|
||||||
|
if not is_windowed and window is not None:
|
||||||
|
raise ValueError(f"window is not supported for {name}")
|
||||||
|
|
||||||
|
if name == "correlation":
|
||||||
|
if secondary is None:
|
||||||
|
raise ValueError("secondary is required for correlation")
|
||||||
|
if not series.index.equals(secondary.index):
|
||||||
|
raise ValueError("secondary index must align with series")
|
||||||
|
return correlation(series, secondary, window) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
if secondary is not None:
|
||||||
|
raise ValueError(f"secondary is not supported for {name}")
|
||||||
|
|
||||||
|
operator = _PHASE1_OPERATOR_FUNCTIONS[name]
|
||||||
|
if name == "rank":
|
||||||
|
return operator(series)
|
||||||
|
return operator(series, window)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 2 cumulative operator contract ──────────────────────────────
|
||||||
|
|
||||||
|
# Phase 2 is cumulative: downstream callers can upgrade to one dispatch
|
||||||
|
# surface covering every existing alpha158 building block, while Phase 1
|
||||||
|
# names, metadata, ordering, and evaluation remain unchanged.
|
||||||
|
ALPHA158_PHASE2_MAX_WINDOW = ALPHA158_PHASE1_MAX_WINDOW
|
||||||
|
|
||||||
|
ALPHA158_PHASE2_OPERATOR_SPECS: dict[str, dict[str, Any]] = {
|
||||||
|
name: {
|
||||||
|
**spec,
|
||||||
|
"parameters": ["window"] if bool(spec["windowed"]) else [],
|
||||||
|
}
|
||||||
|
for name, spec in ALPHA158_PHASE1_OPERATOR_SPECS.items()
|
||||||
|
}
|
||||||
|
ALPHA158_PHASE2_OPERATOR_SPECS.update(
|
||||||
|
{
|
||||||
|
"ts_argmin": {
|
||||||
|
"name": "ts_argmin",
|
||||||
|
"formula": "ts_argmin(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"parameters": ["window"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"ts_argmax": {
|
||||||
|
"name": "ts_argmax",
|
||||||
|
"formula": "ts_argmax(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"parameters": ["window"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"product": {
|
||||||
|
"name": "product",
|
||||||
|
"formula": "product(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"parameters": ["window"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"returns": {
|
||||||
|
"name": "returns",
|
||||||
|
"formula": "returns(series)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"parameters": [],
|
||||||
|
"windowed": False,
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"name": "scale",
|
||||||
|
"formula": "scale(series)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"parameters": [],
|
||||||
|
"windowed": False,
|
||||||
|
},
|
||||||
|
"signed_power": {
|
||||||
|
"name": "signed_power",
|
||||||
|
"formula": "signed_power(series, exponent)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"parameters": ["exponent"],
|
||||||
|
"windowed": False,
|
||||||
|
},
|
||||||
|
"stddev": {
|
||||||
|
"name": "stddev",
|
||||||
|
"formula": "stddev(series, window)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"parameters": ["window"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"covariance": {
|
||||||
|
"name": "covariance",
|
||||||
|
"formula": "covariance(series, secondary, window)",
|
||||||
|
"inputs": ["series", "secondary"],
|
||||||
|
"parameters": ["window"],
|
||||||
|
"windowed": True,
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"name": "log",
|
||||||
|
"formula": "log(series)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"parameters": [],
|
||||||
|
"windowed": False,
|
||||||
|
},
|
||||||
|
"abs_series": {
|
||||||
|
"name": "abs_series",
|
||||||
|
"formula": "abs_series(series)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"parameters": [],
|
||||||
|
"windowed": False,
|
||||||
|
},
|
||||||
|
"sign": {
|
||||||
|
"name": "sign",
|
||||||
|
"formula": "sign(series)",
|
||||||
|
"inputs": ["series"],
|
||||||
|
"parameters": [],
|
||||||
|
"windowed": False,
|
||||||
|
},
|
||||||
|
"max_pair": {
|
||||||
|
"name": "max_pair",
|
||||||
|
"formula": "max_pair(series, secondary)",
|
||||||
|
"inputs": ["series", "secondary"],
|
||||||
|
"parameters": [],
|
||||||
|
"windowed": False,
|
||||||
|
},
|
||||||
|
"min_pair": {
|
||||||
|
"name": "min_pair",
|
||||||
|
"formula": "min_pair(series, secondary)",
|
||||||
|
"inputs": ["series", "secondary"],
|
||||||
|
"parameters": [],
|
||||||
|
"windowed": False,
|
||||||
|
},
|
||||||
|
"indneutralize": {
|
||||||
|
"name": "indneutralize",
|
||||||
|
"formula": "indneutralize(series, groups)",
|
||||||
|
"inputs": ["series", "groups"],
|
||||||
|
"parameters": [],
|
||||||
|
"windowed": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
_PHASE2_OPERATOR_FUNCTIONS: dict[str, Callable[..., pd.Series]] = {
|
||||||
|
**_PHASE1_OPERATOR_FUNCTIONS,
|
||||||
|
"ts_argmin": ts_argmin,
|
||||||
|
"ts_argmax": ts_argmax,
|
||||||
|
"product": product,
|
||||||
|
"returns": returns,
|
||||||
|
"scale": scale,
|
||||||
|
"signed_power": signed_power,
|
||||||
|
"stddev": stddev,
|
||||||
|
"covariance": covariance,
|
||||||
|
"log": log,
|
||||||
|
"abs_series": abs_series,
|
||||||
|
"sign": sign,
|
||||||
|
"max_pair": max_pair,
|
||||||
|
"min_pair": min_pair,
|
||||||
|
"indneutralize": indneutralize,
|
||||||
|
}
|
||||||
|
|
||||||
|
_PHASE2_WINDOWED_OPERATORS = frozenset(
|
||||||
|
name for name, spec in ALPHA158_PHASE2_OPERATOR_SPECS.items() if bool(spec["windowed"])
|
||||||
|
)
|
||||||
|
_PHASE2_BINARY_OPERATORS = frozenset({"correlation", "covariance", "max_pair", "min_pair"})
|
||||||
|
|
||||||
|
|
||||||
|
def list_phase2_operators() -> tuple[str, ...]:
|
||||||
|
"""Return all Phase 2 operator names in stable cumulative order."""
|
||||||
|
return tuple(ALPHA158_PHASE2_OPERATOR_SPECS)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_phase2_window(name: str, window: int | None) -> int:
|
||||||
|
if isinstance(window, bool) or not isinstance(window, int) or window <= 0:
|
||||||
|
raise ValueError(f"window must be a positive integer for {name}")
|
||||||
|
if window > ALPHA158_PHASE2_MAX_WINDOW:
|
||||||
|
raise ValueError(
|
||||||
|
f"window exceeds maximum supported value {ALPHA158_PHASE2_MAX_WINDOW} for {name}"
|
||||||
|
)
|
||||||
|
return window
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_phase2_operator(
|
||||||
|
name: str,
|
||||||
|
series: pd.Series,
|
||||||
|
secondary: pd.Series | None = None,
|
||||||
|
*,
|
||||||
|
window: int | None = None,
|
||||||
|
exponent: float | None = None,
|
||||||
|
groups: pd.Series | None = None,
|
||||||
|
) -> pd.Series:
|
||||||
|
"""Evaluate any existing alpha158 building block through a strict contract.
|
||||||
|
|
||||||
|
Phase 2 rejects implicit alignment, missing required arguments, unused
|
||||||
|
arguments, unbounded windows, and non-finite exponents before dispatch.
|
||||||
|
"""
|
||||||
|
if name not in ALPHA158_PHASE2_OPERATOR_SPECS:
|
||||||
|
raise KeyError(f"operator {name!r} not registered")
|
||||||
|
if not isinstance(series, pd.Series):
|
||||||
|
raise TypeError("series must be a pandas Series")
|
||||||
|
|
||||||
|
validated_window: int | None = None
|
||||||
|
if name in _PHASE2_WINDOWED_OPERATORS:
|
||||||
|
validated_window = _validate_phase2_window(name, window)
|
||||||
|
elif window is not None:
|
||||||
|
raise ValueError(f"window is not supported for {name}")
|
||||||
|
|
||||||
|
if name in _PHASE2_BINARY_OPERATORS:
|
||||||
|
if secondary is None:
|
||||||
|
raise ValueError(f"secondary is required for {name}")
|
||||||
|
if not isinstance(secondary, pd.Series):
|
||||||
|
raise TypeError("secondary must be a pandas Series")
|
||||||
|
if not series.index.equals(secondary.index):
|
||||||
|
raise ValueError("secondary index must align with series")
|
||||||
|
elif secondary is not None:
|
||||||
|
raise ValueError(f"secondary is not supported for {name}")
|
||||||
|
|
||||||
|
validated_exponent: float | None = None
|
||||||
|
if name == "signed_power":
|
||||||
|
if (
|
||||||
|
isinstance(exponent, bool)
|
||||||
|
or not isinstance(exponent, (int, float))
|
||||||
|
or not np.isfinite(exponent)
|
||||||
|
):
|
||||||
|
raise ValueError("exponent must be a finite number for signed_power")
|
||||||
|
validated_exponent = float(exponent)
|
||||||
|
elif exponent is not None:
|
||||||
|
raise ValueError(f"exponent is not supported for {name}")
|
||||||
|
|
||||||
|
if name == "indneutralize":
|
||||||
|
if groups is None:
|
||||||
|
raise ValueError("groups is required for indneutralize")
|
||||||
|
if not isinstance(groups, pd.Series):
|
||||||
|
raise TypeError("groups must be a pandas Series")
|
||||||
|
if not series.index.equals(groups.index):
|
||||||
|
raise ValueError("groups index must align with series")
|
||||||
|
elif groups is not None:
|
||||||
|
raise ValueError(f"groups is not supported for {name}")
|
||||||
|
|
||||||
|
operator = _PHASE2_OPERATOR_FUNCTIONS[name]
|
||||||
|
if name == "signed_power":
|
||||||
|
return operator(series, validated_exponent)
|
||||||
|
if name == "indneutralize":
|
||||||
|
return operator(series, groups)
|
||||||
|
if name in {"correlation", "covariance"}:
|
||||||
|
return operator(series, secondary, validated_window)
|
||||||
|
if name in {"max_pair", "min_pair"}:
|
||||||
|
return operator(series, secondary)
|
||||||
|
if validated_window is not None:
|
||||||
|
return operator(series, validated_window)
|
||||||
|
return operator(series)
|
||||||
|
|
||||||
|
|
||||||
# ── 组合算子(alpha158 公式样本) ─────────────────────────
|
# ── 组合算子(alpha158 公式样本) ─────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -2730,6 +3093,541 @@ def parse_alpha_formula(formula_str: str) -> dict[str, Any]:
|
|||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 3 formula contract: frozen alpha001-alpha050 surface ──────────────
|
||||||
|
|
||||||
|
# Formula functions remain the implementation source of truth. This contract
|
||||||
|
# freezes their callable surface separately from formula dependencies so that
|
||||||
|
# historical compatibility-only arguments remain explicit without rewriting
|
||||||
|
# formulas or changing direct-call APIs.
|
||||||
|
ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION = "1.0.0"
|
||||||
|
ALPHA158_PHASE3_FORMULA_CATALOG_SHA256 = (
|
||||||
|
"9a3360e5ee77a85a35d3c2fdab1efaa531fa0c263a2cb2bb5b965a1fb96fe1bd"
|
||||||
|
)
|
||||||
|
|
||||||
|
_PHASE3_FORMULA_FUNCTIONS: dict[str, Callable[..., pd.Series]] = {
|
||||||
|
"alpha_001": alpha_001,
|
||||||
|
"alpha_002": alpha_002,
|
||||||
|
"alpha_003": alpha_003,
|
||||||
|
"alpha_004": alpha_004,
|
||||||
|
"alpha_005": alpha_005,
|
||||||
|
"alpha_006": alpha_006,
|
||||||
|
"alpha_007": alpha_007,
|
||||||
|
"alpha_008": alpha_008,
|
||||||
|
"alpha_009": alpha_009,
|
||||||
|
"alpha_010": alpha_010,
|
||||||
|
"alpha_011": alpha_011,
|
||||||
|
"alpha_012": alpha_012,
|
||||||
|
"alpha_013": alpha_013,
|
||||||
|
"alpha_014": alpha_014,
|
||||||
|
"alpha_015": alpha_015,
|
||||||
|
"alpha_016": alpha_016,
|
||||||
|
"alpha_017": alpha_017,
|
||||||
|
"alpha_018": alpha_018,
|
||||||
|
"alpha_019": alpha_019,
|
||||||
|
"alpha_020": alpha_020,
|
||||||
|
"alpha_021": alpha_021,
|
||||||
|
"alpha_022": alpha_022,
|
||||||
|
"alpha_023": alpha_023,
|
||||||
|
"alpha_024": alpha_024,
|
||||||
|
"alpha_025": alpha_025,
|
||||||
|
"alpha_026": alpha_026,
|
||||||
|
"alpha_027": alpha_027,
|
||||||
|
"alpha_028": alpha_028,
|
||||||
|
"alpha_029": alpha_029,
|
||||||
|
"alpha_030": alpha_030,
|
||||||
|
"alpha_031": alpha_031,
|
||||||
|
"alpha_032": alpha_032,
|
||||||
|
"alpha_033": alpha_033,
|
||||||
|
"alpha_034": alpha_034,
|
||||||
|
"alpha_035": alpha_035,
|
||||||
|
"alpha_036": alpha_036,
|
||||||
|
"alpha_037": alpha_037,
|
||||||
|
"alpha_038": alpha_038,
|
||||||
|
"alpha_039": alpha_039,
|
||||||
|
"alpha_040": alpha_040,
|
||||||
|
"alpha_041": alpha_041,
|
||||||
|
"alpha_042": alpha_042,
|
||||||
|
"alpha_043": alpha_043,
|
||||||
|
"alpha_044": alpha_044,
|
||||||
|
"alpha_045": alpha_045,
|
||||||
|
"alpha_046": alpha_046,
|
||||||
|
"alpha_047": alpha_047,
|
||||||
|
"alpha_048": alpha_048,
|
||||||
|
"alpha_049": alpha_049,
|
||||||
|
"alpha_050": alpha_050,
|
||||||
|
}
|
||||||
|
|
||||||
|
_PHASE3_FORMULA_INPUT_OVERRIDES: dict[str, list[str]] = {
|
||||||
|
"alpha_011": ["close", "high", "low"],
|
||||||
|
"alpha_035": ["volume"],
|
||||||
|
"alpha_036": ["close"],
|
||||||
|
"alpha_040": ["high", "low"],
|
||||||
|
"alpha_042": ["close"],
|
||||||
|
"alpha_043": ["volume"],
|
||||||
|
}
|
||||||
|
|
||||||
|
_PHASE3_INPUT_CATEGORIES = {
|
||||||
|
1: "single",
|
||||||
|
2: "pair",
|
||||||
|
3: "triple",
|
||||||
|
4: "quadruple",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _phase3_call_inputs(function: Callable[..., pd.Series]) -> list[str]:
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
parameters = list(inspect.signature(function).parameters.values())
|
||||||
|
if any(
|
||||||
|
parameter.kind is not inspect.Parameter.POSITIONAL_OR_KEYWORD
|
||||||
|
or parameter.default is not inspect.Parameter.empty
|
||||||
|
for parameter in parameters
|
||||||
|
):
|
||||||
|
raise RuntimeError(f"unsupported formula signature for {function.__name__}")
|
||||||
|
return ["open" if parameter.name == "open_" else parameter.name for parameter in parameters]
|
||||||
|
|
||||||
|
|
||||||
|
def _phase3_string_list(meta: dict[str, Any], field: str, alpha_id: str) -> list[str]:
|
||||||
|
value = meta[field]
|
||||||
|
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
||||||
|
raise RuntimeError(f"{field} must be a list of strings for {alpha_id}")
|
||||||
|
return list(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_phase3_formula_specs() -> dict[str, dict[str, Any]]:
|
||||||
|
specs: dict[str, dict[str, Any]] = {}
|
||||||
|
for alpha_id, function in _PHASE3_FORMULA_FUNCTIONS.items():
|
||||||
|
meta = ALPHA158_REGISTRY[alpha_id]
|
||||||
|
call_inputs = _phase3_call_inputs(function)
|
||||||
|
formula_inputs = _PHASE3_FORMULA_INPUT_OVERRIDES.get(alpha_id, call_inputs)
|
||||||
|
input_category = _PHASE3_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_PHASE3_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": list(formula_inputs),
|
||||||
|
"input_category": input_category,
|
||||||
|
}
|
||||||
|
return specs
|
||||||
|
|
||||||
|
|
||||||
|
def _freeze_phase3_formula_specs(
|
||||||
|
specs: dict[str, dict[str, Any]],
|
||||||
|
) -> Mapping[str, Mapping[str, Any]]:
|
||||||
|
frozen_specs: dict[str, Mapping[str, Any]] = {}
|
||||||
|
for alpha_id, spec in specs.items():
|
||||||
|
frozen_specs[alpha_id] = MappingProxyType(
|
||||||
|
{field: tuple(value) if isinstance(value, list) else value for field, value in spec.items()}
|
||||||
|
)
|
||||||
|
return MappingProxyType(frozen_specs)
|
||||||
|
|
||||||
|
|
||||||
|
ALPHA158_PHASE3_FORMULA_SPECS: Mapping[str, Mapping[str, Any]] = (
|
||||||
|
_freeze_phase3_formula_specs(_build_phase3_formula_specs())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_phase3_formulas() -> tuple[str, ...]:
|
||||||
|
"""Return the frozen alpha001-alpha050 formula IDs in stable order."""
|
||||||
|
return tuple(ALPHA158_PHASE3_FORMULA_SPECS)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_phase3_formula(name: str, **inputs: pd.Series) -> pd.Series:
|
||||||
|
"""Evaluate a Phase 3 formula with an exact, alignment-safe input contract."""
|
||||||
|
if name not in ALPHA158_PHASE3_FORMULA_SPECS:
|
||||||
|
raise KeyError(f"formula {name!r} not registered")
|
||||||
|
|
||||||
|
spec = ALPHA158_PHASE3_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 not primary.index.equals(inputs[field].index):
|
||||||
|
raise ValueError(f"{field} index must align with {primary_field}")
|
||||||
|
|
||||||
|
function = _PHASE3_FORMULA_FUNCTIONS[name]
|
||||||
|
return function(*(inputs[field] for field in required_inputs))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 4 formula contract: frozen alpha051-alpha100 surface ──────────────
|
||||||
|
|
||||||
|
# Phase 4 extends the versioned formula contract without mutating the Phase 3
|
||||||
|
# catalogue, digest, dispatch surface, or the existing formula functions.
|
||||||
|
ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION = "1.0.0"
|
||||||
|
ALPHA158_PHASE4_FORMULA_CATALOG_SHA256 = (
|
||||||
|
"858daf5e2abab5063fc28fcf7c79936096e2c76dde582fc7bab78b3458f17054"
|
||||||
|
)
|
||||||
|
|
||||||
|
_PHASE4_FORMULA_FUNCTIONS: dict[str, Callable[..., pd.Series]] = {
|
||||||
|
"alpha_051": alpha_051,
|
||||||
|
"alpha_052": alpha_052,
|
||||||
|
"alpha_053": alpha_053,
|
||||||
|
"alpha_054": alpha_054,
|
||||||
|
"alpha_055": alpha_055,
|
||||||
|
"alpha_056": alpha_056,
|
||||||
|
"alpha_057": alpha_057,
|
||||||
|
"alpha_058": alpha_058,
|
||||||
|
"alpha_059": alpha_059,
|
||||||
|
"alpha_060": alpha_060,
|
||||||
|
"alpha_061": alpha_061,
|
||||||
|
"alpha_062": alpha_062,
|
||||||
|
"alpha_063": alpha_063,
|
||||||
|
"alpha_064": alpha_064,
|
||||||
|
"alpha_065": alpha_065,
|
||||||
|
"alpha_066": alpha_066,
|
||||||
|
"alpha_067": alpha_067,
|
||||||
|
"alpha_068": alpha_068,
|
||||||
|
"alpha_069": alpha_069,
|
||||||
|
"alpha_070": alpha_070,
|
||||||
|
"alpha_071": alpha_071,
|
||||||
|
"alpha_072": alpha_072,
|
||||||
|
"alpha_073": alpha_073,
|
||||||
|
"alpha_074": alpha_074,
|
||||||
|
"alpha_075": alpha_075,
|
||||||
|
"alpha_076": alpha_076,
|
||||||
|
"alpha_077": alpha_077,
|
||||||
|
"alpha_078": alpha_078,
|
||||||
|
"alpha_079": alpha_079,
|
||||||
|
"alpha_080": alpha_080,
|
||||||
|
"alpha_081": alpha_081,
|
||||||
|
"alpha_082": alpha_082,
|
||||||
|
"alpha_083": alpha_083,
|
||||||
|
"alpha_084": alpha_084,
|
||||||
|
"alpha_085": alpha_085,
|
||||||
|
"alpha_086": alpha_086,
|
||||||
|
"alpha_087": alpha_087,
|
||||||
|
"alpha_088": alpha_088,
|
||||||
|
"alpha_089": alpha_089,
|
||||||
|
"alpha_090": alpha_090,
|
||||||
|
"alpha_091": alpha_091,
|
||||||
|
"alpha_092": alpha_092,
|
||||||
|
"alpha_093": alpha_093,
|
||||||
|
"alpha_094": alpha_094,
|
||||||
|
"alpha_095": alpha_095,
|
||||||
|
"alpha_096": alpha_096,
|
||||||
|
"alpha_097": alpha_097,
|
||||||
|
"alpha_098": alpha_098,
|
||||||
|
"alpha_099": alpha_099,
|
||||||
|
"alpha_100": alpha_100,
|
||||||
|
}
|
||||||
|
|
||||||
|
_PHASE4_INPUT_CATEGORIES = {
|
||||||
|
1: "single",
|
||||||
|
2: "pair",
|
||||||
|
3: "triple",
|
||||||
|
4: "quadruple",
|
||||||
|
5: "quintuple",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_phase4_formula_specs() -> dict[str, dict[str, Any]]:
|
||||||
|
specs: dict[str, dict[str, Any]] = {}
|
||||||
|
for alpha_id, function in _PHASE4_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_PHASE4_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_PHASE4_FORMULA_SPECS: Mapping[str, Mapping[str, Any]] = (
|
||||||
|
_freeze_phase3_formula_specs(_build_phase4_formula_specs())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_phase4_formulas() -> tuple[str, ...]:
|
||||||
|
"""Return the frozen alpha051-alpha100 formula IDs in stable order."""
|
||||||
|
return tuple(ALPHA158_PHASE4_FORMULA_SPECS)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_phase4_formula(name: str, **inputs: pd.Series) -> pd.Series:
|
||||||
|
"""Evaluate a Phase 4 formula with an exact, alignment-safe input contract."""
|
||||||
|
if name not in ALPHA158_PHASE4_FORMULA_SPECS:
|
||||||
|
raise KeyError(f"formula {name!r} not registered")
|
||||||
|
|
||||||
|
spec = ALPHA158_PHASE4_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 not primary.index.equals(inputs[field].index):
|
||||||
|
raise ValueError(f"{field} index must align with {primary_field}")
|
||||||
|
|
||||||
|
function = _PHASE4_FORMULA_FUNCTIONS[name]
|
||||||
|
return function(*(inputs[field] for field in required_inputs))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Phase 5 formula contract: frozen alpha101-alpha150 surface ──────────────
|
||||||
|
|
||||||
|
# Phase 5 extends the versioned formula contract without mutating any earlier
|
||||||
|
# catalogue, digest, dispatch surface, or existing formula implementation.
|
||||||
|
ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION = "1.0.0"
|
||||||
|
ALPHA158_PHASE5_FORMULA_CATALOG_SHA256 = (
|
||||||
|
"3368796169c9fbd39c4a34ea137e569964b15882fbf4de25124790d548db6533"
|
||||||
|
)
|
||||||
|
|
||||||
|
_PHASE5_FORMULA_FUNCTIONS: dict[str, Callable[..., pd.Series]] = {
|
||||||
|
"alpha_101": alpha_101,
|
||||||
|
"alpha_102": alpha_102,
|
||||||
|
"alpha_103": alpha_103,
|
||||||
|
"alpha_104": alpha_104,
|
||||||
|
"alpha_105": alpha_105,
|
||||||
|
"alpha_106": alpha_106,
|
||||||
|
"alpha_107": alpha_107,
|
||||||
|
"alpha_108": alpha_108,
|
||||||
|
"alpha_109": alpha_109,
|
||||||
|
"alpha_110": alpha_110,
|
||||||
|
"alpha_111": alpha_111,
|
||||||
|
"alpha_112": alpha_112,
|
||||||
|
"alpha_113": alpha_113,
|
||||||
|
"alpha_114": alpha_114,
|
||||||
|
"alpha_115": alpha_115,
|
||||||
|
"alpha_116": alpha_116,
|
||||||
|
"alpha_117": alpha_117,
|
||||||
|
"alpha_118": alpha_118,
|
||||||
|
"alpha_119": alpha_119,
|
||||||
|
"alpha_120": alpha_120,
|
||||||
|
"alpha_121": alpha_121,
|
||||||
|
"alpha_122": alpha_122,
|
||||||
|
"alpha_123": alpha_123,
|
||||||
|
"alpha_124": alpha_124,
|
||||||
|
"alpha_125": alpha_125,
|
||||||
|
"alpha_126": alpha_126,
|
||||||
|
"alpha_127": alpha_127,
|
||||||
|
"alpha_128": alpha_128,
|
||||||
|
"alpha_129": alpha_129,
|
||||||
|
"alpha_130": alpha_130,
|
||||||
|
"alpha_131": alpha_131,
|
||||||
|
"alpha_132": alpha_132,
|
||||||
|
"alpha_133": alpha_133,
|
||||||
|
"alpha_134": alpha_134,
|
||||||
|
"alpha_135": alpha_135,
|
||||||
|
"alpha_136": alpha_136,
|
||||||
|
"alpha_137": alpha_137,
|
||||||
|
"alpha_138": alpha_138,
|
||||||
|
"alpha_139": alpha_139,
|
||||||
|
"alpha_140": alpha_140,
|
||||||
|
"alpha_141": alpha_141,
|
||||||
|
"alpha_142": alpha_142,
|
||||||
|
"alpha_143": alpha_143,
|
||||||
|
"alpha_144": alpha_144,
|
||||||
|
"alpha_145": alpha_145,
|
||||||
|
"alpha_146": alpha_146,
|
||||||
|
"alpha_147": alpha_147,
|
||||||
|
"alpha_148": alpha_148,
|
||||||
|
"alpha_149": alpha_149,
|
||||||
|
"alpha_150": alpha_150,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_phase5_formula_specs() -> dict[str, dict[str, Any]]:
|
||||||
|
specs: dict[str, dict[str, Any]] = {}
|
||||||
|
for alpha_id, function in _PHASE5_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_PHASE5_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_PHASE5_FORMULA_SPECS: Mapping[str, Mapping[str, Any]] = (
|
||||||
|
_freeze_phase3_formula_specs(_build_phase5_formula_specs())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_phase5_formulas() -> tuple[str, ...]:
|
||||||
|
"""Return the frozen alpha101-alpha150 formula IDs in stable order."""
|
||||||
|
return tuple(ALPHA158_PHASE5_FORMULA_SPECS)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_phase5_formula(name: str, **inputs: pd.Series) -> pd.Series:
|
||||||
|
"""Evaluate a Phase 5 formula with an exact, alignment-safe input contract."""
|
||||||
|
if name not in ALPHA158_PHASE5_FORMULA_SPECS:
|
||||||
|
raise KeyError(f"formula {name!r} not registered")
|
||||||
|
|
||||||
|
spec = ALPHA158_PHASE5_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 = _PHASE5_FORMULA_FUNCTIONS[name]
|
||||||
|
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",
|
||||||
@@ -2755,6 +3653,34 @@ __all__ = [
|
|||||||
"max_pair",
|
"max_pair",
|
||||||
"min_pair",
|
"min_pair",
|
||||||
"indneutralize",
|
"indneutralize",
|
||||||
|
"ALPHA158_PHASE1_MAX_WINDOW",
|
||||||
|
"ALPHA158_PHASE1_OPERATOR_SPECS",
|
||||||
|
"list_phase1_operators",
|
||||||
|
"evaluate_phase1_operator",
|
||||||
|
"ALPHA158_PHASE2_MAX_WINDOW",
|
||||||
|
"ALPHA158_PHASE2_OPERATOR_SPECS",
|
||||||
|
"list_phase2_operators",
|
||||||
|
"evaluate_phase2_operator",
|
||||||
|
"ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION",
|
||||||
|
"ALPHA158_PHASE3_FORMULA_CATALOG_SHA256",
|
||||||
|
"ALPHA158_PHASE3_FORMULA_SPECS",
|
||||||
|
"list_phase3_formulas",
|
||||||
|
"evaluate_phase3_formula",
|
||||||
|
"ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION",
|
||||||
|
"ALPHA158_PHASE4_FORMULA_CATALOG_SHA256",
|
||||||
|
"ALPHA158_PHASE4_FORMULA_SPECS",
|
||||||
|
"list_phase4_formulas",
|
||||||
|
"evaluate_phase4_formula",
|
||||||
|
"ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION",
|
||||||
|
"ALPHA158_PHASE5_FORMULA_CATALOG_SHA256",
|
||||||
|
"ALPHA158_PHASE5_FORMULA_SPECS",
|
||||||
|
"list_phase5_formulas",
|
||||||
|
"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",
|
||||||
|
|||||||
+1012
-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
+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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,58 @@
|
|||||||
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"] == 3
|
||||||
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"),
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
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"] == []
|
||||||
|
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()
|
||||||
|
|||||||
@@ -6,8 +6,30 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
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_PHASE2_OPERATOR_SPECS,
|
||||||
|
ALPHA158_PHASE3_FORMULA_CATALOG_SHA256,
|
||||||
|
ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION,
|
||||||
|
ALPHA158_PHASE3_FORMULA_SPECS,
|
||||||
|
ALPHA158_PHASE4_FORMULA_CATALOG_SHA256,
|
||||||
|
ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION,
|
||||||
|
ALPHA158_PHASE4_FORMULA_SPECS,
|
||||||
|
ALPHA158_PHASE5_FORMULA_CATALOG_SHA256,
|
||||||
|
ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION,
|
||||||
|
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,
|
||||||
@@ -166,6 +188,18 @@ from quant_engine.alpha_factors import (
|
|||||||
alpha_156,
|
alpha_156,
|
||||||
alpha_157,
|
alpha_157,
|
||||||
alpha_158,
|
alpha_158,
|
||||||
|
evaluate_phase1_operator,
|
||||||
|
evaluate_phase2_operator,
|
||||||
|
evaluate_phase3_formula,
|
||||||
|
evaluate_phase4_formula,
|
||||||
|
evaluate_phase5_formula,
|
||||||
|
evaluate_phase6_formula,
|
||||||
|
list_phase1_operators,
|
||||||
|
list_phase2_operators,
|
||||||
|
list_phase3_formulas,
|
||||||
|
list_phase4_formulas,
|
||||||
|
list_phase5_formulas,
|
||||||
|
list_phase6_formulas,
|
||||||
correlation,
|
correlation,
|
||||||
covariance,
|
covariance,
|
||||||
decay_linear,
|
decay_linear,
|
||||||
@@ -407,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")
|
||||||
@@ -1232,3 +1310,901 @@ def test_parse_alpha_formula_round_trip_jsonb():
|
|||||||
serialized = json.dumps(parsed)
|
serialized = json.dumps(parsed)
|
||||||
assert isinstance(serialized, str)
|
assert isinstance(serialized, str)
|
||||||
assert "ts_rank" in serialized
|
assert "ts_rank" in serialized
|
||||||
|
|
||||||
|
|
||||||
|
# ── v1.2.0 Phase 1: deterministic operator dispatch contract ──────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase1_operator_catalog_is_explicit_and_serializable():
|
||||||
|
"""Phase 1 exposes a stable, JSON-friendly catalog for downstream callers."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"rank",
|
||||||
|
"delta",
|
||||||
|
"ts_mean",
|
||||||
|
"ts_std",
|
||||||
|
"ts_rank",
|
||||||
|
"correlation",
|
||||||
|
"ts_min",
|
||||||
|
"ts_max",
|
||||||
|
"ts_sum",
|
||||||
|
"decay_linear",
|
||||||
|
}
|
||||||
|
assert set(list_phase1_operators()) == expected
|
||||||
|
assert set(ALPHA158_PHASE1_OPERATOR_SPECS) == expected
|
||||||
|
json.dumps(ALPHA158_PHASE1_OPERATOR_SPECS)
|
||||||
|
for name, spec in ALPHA158_PHASE1_OPERATOR_SPECS.items():
|
||||||
|
assert spec["name"] == name
|
||||||
|
assert isinstance(spec["inputs"], list)
|
||||||
|
assert isinstance(spec["formula"], str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase1_unary_operators_preserve_index_and_are_deterministic():
|
||||||
|
values = pd.Series([1.0, 2.0, 3.0, 4.0], index=["a", "b", "c", "d"])
|
||||||
|
|
||||||
|
first = evaluate_phase1_operator("rank", values)
|
||||||
|
second = evaluate_phase1_operator("rank", values)
|
||||||
|
|
||||||
|
pd.testing.assert_series_equal(first, second)
|
||||||
|
assert first.index.equals(values.index)
|
||||||
|
assert first.iloc[-1] == pytest.approx(1.0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("name", "window"),
|
||||||
|
[
|
||||||
|
("delta", 2),
|
||||||
|
("ts_mean", 2),
|
||||||
|
("ts_std", 2),
|
||||||
|
("ts_rank", 2),
|
||||||
|
("ts_min", 2),
|
||||||
|
("ts_max", 2),
|
||||||
|
("ts_sum", 2),
|
||||||
|
("decay_linear", 2),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_phase1_windowed_operators_require_explicit_window(name: str, window: int):
|
||||||
|
values = pd.Series([1.0, 2.0, 3.0, 4.0])
|
||||||
|
|
||||||
|
result = evaluate_phase1_operator(name, values, window=window)
|
||||||
|
|
||||||
|
assert result.index.equals(values.index)
|
||||||
|
with pytest.raises(ValueError, match="window"):
|
||||||
|
evaluate_phase1_operator(name, values)
|
||||||
|
with pytest.raises(ValueError, match="positive integer"):
|
||||||
|
evaluate_phase1_operator(name, values, window=1.5) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase1_binary_correlation_requires_aligned_secondary_input():
|
||||||
|
values = pd.Series([1.0, 2.0, 3.0, 4.0])
|
||||||
|
other = pd.Series([4.0, 3.0, 2.0, 1.0])
|
||||||
|
|
||||||
|
result = evaluate_phase1_operator("correlation", values, other, window=2)
|
||||||
|
|
||||||
|
assert result.iloc[-1] == pytest.approx(-1.0)
|
||||||
|
with pytest.raises(ValueError, match="secondary"):
|
||||||
|
evaluate_phase1_operator("correlation", values, window=2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase1_dispatch_rejects_unknown_or_unused_arguments():
|
||||||
|
values = pd.Series([1.0, 2.0, 3.0])
|
||||||
|
|
||||||
|
with pytest.raises(KeyError, match="not registered"):
|
||||||
|
evaluate_phase1_operator("unknown", values)
|
||||||
|
with pytest.raises(ValueError, match="window"):
|
||||||
|
evaluate_phase1_operator("rank", values, window=2)
|
||||||
|
with pytest.raises(ValueError, match="secondary"):
|
||||||
|
evaluate_phase1_operator("rank", values, values)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase1_dispatch_rejects_window_above_supported_limit():
|
||||||
|
values = pd.Series([1.0, 2.0, 3.0])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="maximum"):
|
||||||
|
evaluate_phase1_operator("ts_mean", values, window=2**63)
|
||||||
|
|
||||||
|
|
||||||
|
# ── v1.2.0 Phase 2: cumulative deterministic operator contract ─────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase2_operator_catalog_is_cumulative_stable_and_serializable():
|
||||||
|
"""Phase 2 exposes all existing building blocks without changing Phase 1."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
phase1 = list_phase1_operators()
|
||||||
|
expected_phase2 = (
|
||||||
|
*phase1,
|
||||||
|
"ts_argmin",
|
||||||
|
"ts_argmax",
|
||||||
|
"product",
|
||||||
|
"returns",
|
||||||
|
"scale",
|
||||||
|
"signed_power",
|
||||||
|
"stddev",
|
||||||
|
"covariance",
|
||||||
|
"log",
|
||||||
|
"abs_series",
|
||||||
|
"sign",
|
||||||
|
"max_pair",
|
||||||
|
"min_pair",
|
||||||
|
"indneutralize",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert list_phase2_operators() == expected_phase2
|
||||||
|
assert tuple(ALPHA158_PHASE2_OPERATOR_SPECS) == expected_phase2
|
||||||
|
assert tuple(ALPHA158_PHASE1_OPERATOR_SPECS) == phase1
|
||||||
|
json.dumps(ALPHA158_PHASE2_OPERATOR_SPECS)
|
||||||
|
for name, spec in ALPHA158_PHASE2_OPERATOR_SPECS.items():
|
||||||
|
assert spec["name"] == name
|
||||||
|
assert isinstance(spec["inputs"], list)
|
||||||
|
assert isinstance(spec["parameters"], list)
|
||||||
|
assert isinstance(spec["formula"], str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", ["ts_argmin", "ts_argmax", "product", "stddev"])
|
||||||
|
def test_phase2_windowed_unary_dispatch_is_deterministic(name: str):
|
||||||
|
values = pd.Series([3.0, 1.0, 4.0, 2.0], index=["a", "b", "c", "d"])
|
||||||
|
|
||||||
|
first = evaluate_phase2_operator(name, values, window=3)
|
||||||
|
second = evaluate_phase2_operator(name, values, window=3)
|
||||||
|
|
||||||
|
pd.testing.assert_series_equal(first, second)
|
||||||
|
assert first.index.equals(values.index)
|
||||||
|
with pytest.raises(ValueError, match="window"):
|
||||||
|
evaluate_phase2_operator(name, values)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", ["returns", "scale", "log", "abs_series", "sign"])
|
||||||
|
def test_phase2_unary_dispatch_rejects_unused_arguments(name: str):
|
||||||
|
values = pd.Series([1.0, 2.0, 4.0], index=["a", "b", "c"])
|
||||||
|
|
||||||
|
result = evaluate_phase2_operator(name, values)
|
||||||
|
|
||||||
|
assert result.index.equals(values.index)
|
||||||
|
with pytest.raises(ValueError, match="window"):
|
||||||
|
evaluate_phase2_operator(name, values, window=2)
|
||||||
|
with pytest.raises(ValueError, match="secondary"):
|
||||||
|
evaluate_phase2_operator(name, values, secondary=values)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("name", "window"),
|
||||||
|
[("correlation", 2), ("covariance", 2), ("max_pair", None), ("min_pair", None)],
|
||||||
|
)
|
||||||
|
def test_phase2_binary_dispatch_requires_aligned_secondary(name: str, window: int | None):
|
||||||
|
values = pd.Series([1.0, 2.0, 3.0], index=["a", "b", "c"])
|
||||||
|
secondary = pd.Series([3.0, 2.0, 1.0], index=values.index)
|
||||||
|
|
||||||
|
result = evaluate_phase2_operator(name, values, secondary=secondary, window=window)
|
||||||
|
|
||||||
|
assert result.index.equals(values.index)
|
||||||
|
with pytest.raises(ValueError, match="secondary is required"):
|
||||||
|
evaluate_phase2_operator(name, values, window=window)
|
||||||
|
with pytest.raises(ValueError, match="secondary index"):
|
||||||
|
evaluate_phase2_operator(
|
||||||
|
name,
|
||||||
|
values,
|
||||||
|
secondary=secondary.rename(index={"c": "z"}),
|
||||||
|
window=window,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase2_signed_power_requires_finite_numeric_exponent():
|
||||||
|
values = pd.Series([-4.0, 0.0, 9.0])
|
||||||
|
|
||||||
|
result = evaluate_phase2_operator("signed_power", values, exponent=0.5)
|
||||||
|
|
||||||
|
pd.testing.assert_series_equal(result, pd.Series([-2.0, 0.0, 3.0]))
|
||||||
|
for exponent in (None, True, float("inf"), float("nan"), "2"):
|
||||||
|
with pytest.raises(ValueError, match="exponent"):
|
||||||
|
evaluate_phase2_operator( # type: ignore[arg-type]
|
||||||
|
"signed_power",
|
||||||
|
values,
|
||||||
|
exponent=exponent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase2_indneutralize_requires_aligned_groups():
|
||||||
|
values = pd.Series([1.0, 3.0, 10.0, 14.0], index=["a", "b", "c", "d"])
|
||||||
|
groups = pd.Series(["x", "x", "y", "y"], index=values.index)
|
||||||
|
|
||||||
|
result = evaluate_phase2_operator("indneutralize", values, groups=groups)
|
||||||
|
|
||||||
|
pd.testing.assert_series_equal(result, pd.Series([-1.0, 1.0, -2.0, 2.0], index=values.index))
|
||||||
|
with pytest.raises(ValueError, match="groups is required"):
|
||||||
|
evaluate_phase2_operator("indneutralize", values)
|
||||||
|
with pytest.raises(ValueError, match="groups index"):
|
||||||
|
evaluate_phase2_operator(
|
||||||
|
"indneutralize",
|
||||||
|
values,
|
||||||
|
groups=groups.rename(index={"d": "z"}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase2_dispatch_validates_primary_series_and_unused_parameters():
|
||||||
|
values = pd.Series([1.0, 2.0, 3.0])
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="series must be a pandas Series"):
|
||||||
|
evaluate_phase2_operator("rank", [1.0, 2.0, 3.0]) # type: ignore[arg-type]
|
||||||
|
with pytest.raises(KeyError, match="not registered"):
|
||||||
|
evaluate_phase2_operator("unknown", values)
|
||||||
|
with pytest.raises(ValueError, match="exponent"):
|
||||||
|
evaluate_phase2_operator("rank", values, exponent=2.0)
|
||||||
|
with pytest.raises(ValueError, match="groups"):
|
||||||
|
evaluate_phase2_operator("rank", values, groups=pd.Series(["x", "x", "x"]))
|
||||||
|
with pytest.raises(ValueError, match="maximum"):
|
||||||
|
evaluate_phase2_operator("product", values, window=253)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Alpha158 Phase 3: versioned alpha001-alpha050 formula contract ──────────
|
||||||
|
|
||||||
|
|
||||||
|
def _phase3_market_inputs() -> dict[str, pd.Series]:
|
||||||
|
positions = np.arange(80, dtype=float)
|
||||||
|
index = pd.RangeIndex(len(positions), name="row")
|
||||||
|
open_ = pd.Series(100.0 + positions * 0.2 + np.sin(positions / 4.0), index=index)
|
||||||
|
close = pd.Series(100.5 + positions * 0.18 + np.cos(positions / 5.0), index=index)
|
||||||
|
high = pd.Series(np.maximum(open_, close) + 1.0, index=index)
|
||||||
|
low = pd.Series(np.minimum(open_, close) - 1.0, index=index)
|
||||||
|
volume = pd.Series(1_000.0 + positions**1.3 + 20.0 * np.sin(positions / 3.0), index=index)
|
||||||
|
vwap = (open_ + close + high + low) / 4.0
|
||||||
|
return {
|
||||||
|
"open": open_,
|
||||||
|
"close": close,
|
||||||
|
"high": high,
|
||||||
|
"low": low,
|
||||||
|
"volume": volume,
|
||||||
|
"vwap": vwap,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase3_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(1, 51))
|
||||||
|
expected_fields = {
|
||||||
|
"name",
|
||||||
|
"contract_version",
|
||||||
|
"formula",
|
||||||
|
"category",
|
||||||
|
"complexity",
|
||||||
|
"parameters",
|
||||||
|
"description",
|
||||||
|
"references",
|
||||||
|
"call_inputs",
|
||||||
|
"formula_inputs",
|
||||||
|
"input_category",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert ALPHA158_PHASE3_FORMULA_CONTRACT_VERSION == "1.0.0"
|
||||||
|
assert list_phase3_formulas() == expected_ids
|
||||||
|
assert tuple(ALPHA158_PHASE3_FORMULA_SPECS) == expected_ids
|
||||||
|
assert Counter(
|
||||||
|
spec["input_category"] for spec in ALPHA158_PHASE3_FORMULA_SPECS.values()
|
||||||
|
) == {"single": 19, "pair": 26, "triple": 4, "quadruple": 1}
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE3_FORMULA_SPECS.items():
|
||||||
|
assert set(spec) == expected_fields
|
||||||
|
assert spec["name"] == alpha_id
|
||||||
|
assert spec["contract_version"] == ALPHA158_PHASE3_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_PHASE3_FORMULA_SPECS.items()
|
||||||
|
}
|
||||||
|
encoded = json.dumps(
|
||||||
|
serializable_specs,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
ensure_ascii=False,
|
||||||
|
).encode()
|
||||||
|
assert hashlib.sha256(encoded).hexdigest() == ALPHA158_PHASE3_FORMULA_CATALOG_SHA256
|
||||||
|
assert ALPHA158_PHASE3_FORMULA_CATALOG_SHA256 == (
|
||||||
|
"9a3360e5ee77a85a35d3c2fdab1efaa531fa0c263a2cb2bb5b965a1fb96fe1bd"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase3_formula_catalog_is_recursively_immutable():
|
||||||
|
import operator
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(ALPHA158_PHASE3_FORMULA_SPECS, "alpha_001", {})
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(
|
||||||
|
ALPHA158_PHASE3_FORMULA_SPECS["alpha_001"],
|
||||||
|
"formula",
|
||||||
|
"changed",
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(
|
||||||
|
ALPHA158_PHASE3_FORMULA_SPECS["alpha_001"]["call_inputs"],
|
||||||
|
0,
|
||||||
|
"volume",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase3_catalog_freezes_callable_signatures_without_rewriting_formulas():
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
legacy_formula_input_differences = {
|
||||||
|
"alpha_011": ("close", "high", "low"),
|
||||||
|
"alpha_035": ("volume",),
|
||||||
|
"alpha_036": ("close",),
|
||||||
|
"alpha_040": ("high", "low"),
|
||||||
|
"alpha_042": ("close",),
|
||||||
|
"alpha_043": ("volume",),
|
||||||
|
}
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE3_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"] == legacy_formula_input_differences.get(
|
||||||
|
alpha_id,
|
||||||
|
signature_inputs,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ALPHA158_PHASE3_FORMULA_SPECS["alpha_035"]["call_inputs"] == (
|
||||||
|
"close",
|
||||||
|
"volume",
|
||||||
|
)
|
||||||
|
assert ALPHA158_REGISTRY["alpha_035"]["inputs"] == ["volume"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase3_dispatch_matches_all_existing_alpha001_alpha050_functions():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE3_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_phase3_formula(
|
||||||
|
alpha_id,
|
||||||
|
**{name: inputs[name] for name in reversed(call_inputs)},
|
||||||
|
)
|
||||||
|
pd.testing.assert_series_equal(actual, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase3_dispatch_rejects_unknown_missing_extra_and_non_series_inputs():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
|
||||||
|
with pytest.raises(KeyError, match="not registered"):
|
||||||
|
evaluate_phase3_formula("alpha_051", close=inputs["close"])
|
||||||
|
with pytest.raises(ValueError, match=r"missing inputs.*volume"):
|
||||||
|
evaluate_phase3_formula("alpha_005", close=inputs["close"])
|
||||||
|
with pytest.raises(ValueError, match=r"unexpected inputs.*vwap"):
|
||||||
|
evaluate_phase3_formula(
|
||||||
|
"alpha_005",
|
||||||
|
close=inputs["close"],
|
||||||
|
volume=inputs["volume"],
|
||||||
|
vwap=inputs["vwap"],
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError, match="close must be a pandas Series"):
|
||||||
|
evaluate_phase3_formula( # type: ignore[arg-type]
|
||||||
|
"alpha_005",
|
||||||
|
close=[1.0, 2.0],
|
||||||
|
volume=inputs["volume"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase3_dispatch_rejects_implicit_series_alignment():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
misaligned_volume = inputs["volume"].rename(index={79: 80})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="volume index must align with close"):
|
||||||
|
evaluate_phase3_formula(
|
||||||
|
"alpha_005",
|
||||||
|
close=inputs["close"],
|
||||||
|
volume=misaligned_volume,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Alpha158 Phase 4: versioned alpha051-alpha100 formula contract ──────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase4_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(51, 101))
|
||||||
|
expected_fields = {
|
||||||
|
"name",
|
||||||
|
"contract_version",
|
||||||
|
"formula",
|
||||||
|
"category",
|
||||||
|
"complexity",
|
||||||
|
"parameters",
|
||||||
|
"description",
|
||||||
|
"references",
|
||||||
|
"call_inputs",
|
||||||
|
"formula_inputs",
|
||||||
|
"input_category",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert ALPHA158_PHASE4_FORMULA_CONTRACT_VERSION == "1.0.0"
|
||||||
|
assert list_phase4_formulas() == expected_ids
|
||||||
|
assert tuple(ALPHA158_PHASE4_FORMULA_SPECS) == expected_ids
|
||||||
|
assert Counter(
|
||||||
|
spec["input_category"] for spec in ALPHA158_PHASE4_FORMULA_SPECS.values()
|
||||||
|
) == {"single": 1, "pair": 27, "triple": 11, "quadruple": 10, "quintuple": 1}
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE4_FORMULA_SPECS.items():
|
||||||
|
assert set(spec) == expected_fields
|
||||||
|
assert spec["name"] == alpha_id
|
||||||
|
assert spec["contract_version"] == ALPHA158_PHASE4_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_PHASE4_FORMULA_SPECS.items()
|
||||||
|
}
|
||||||
|
encoded = json.dumps(
|
||||||
|
serializable_specs,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
ensure_ascii=False,
|
||||||
|
).encode()
|
||||||
|
assert hashlib.sha256(encoded).hexdigest() == ALPHA158_PHASE4_FORMULA_CATALOG_SHA256
|
||||||
|
assert ALPHA158_PHASE4_FORMULA_CATALOG_SHA256 == (
|
||||||
|
"858daf5e2abab5063fc28fcf7c79936096e2c76dde582fc7bab78b3458f17054"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase4_formula_catalog_is_recursively_immutable():
|
||||||
|
import operator
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(ALPHA158_PHASE4_FORMULA_SPECS, "alpha_051", {})
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(
|
||||||
|
ALPHA158_PHASE4_FORMULA_SPECS["alpha_051"],
|
||||||
|
"formula",
|
||||||
|
"changed",
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(
|
||||||
|
ALPHA158_PHASE4_FORMULA_SPECS["alpha_051"]["call_inputs"],
|
||||||
|
0,
|
||||||
|
"volume",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase4_catalog_freezes_callable_and_formula_inputs():
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE4_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_PHASE4_FORMULA_SPECS["alpha_055"]["call_inputs"] == (
|
||||||
|
"open",
|
||||||
|
"high",
|
||||||
|
"low",
|
||||||
|
"volume",
|
||||||
|
"close",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase4_dispatch_matches_all_existing_alpha051_alpha100_functions():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE4_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_phase4_formula(
|
||||||
|
alpha_id,
|
||||||
|
**{name: inputs[name] for name in reversed(call_inputs)},
|
||||||
|
)
|
||||||
|
pd.testing.assert_series_equal(actual, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase4_dispatch_rejects_unknown_missing_extra_and_non_series_inputs():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
|
||||||
|
with pytest.raises(KeyError, match="not registered"):
|
||||||
|
evaluate_phase4_formula("alpha_050", close=inputs["close"])
|
||||||
|
with pytest.raises(ValueError, match=r"missing inputs.*low"):
|
||||||
|
evaluate_phase4_formula("alpha_051", high=inputs["high"])
|
||||||
|
with pytest.raises(ValueError, match=r"unexpected inputs.*vwap"):
|
||||||
|
evaluate_phase4_formula(
|
||||||
|
"alpha_051",
|
||||||
|
high=inputs["high"],
|
||||||
|
low=inputs["low"],
|
||||||
|
vwap=inputs["vwap"],
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError, match="high must be a pandas Series"):
|
||||||
|
evaluate_phase4_formula( # type: ignore[arg-type]
|
||||||
|
"alpha_051",
|
||||||
|
high=[1.0, 2.0],
|
||||||
|
low=inputs["low"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase4_dispatch_rejects_implicit_series_alignment():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
misaligned_low = inputs["low"].rename(index={79: 80})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="low index must align with high"):
|
||||||
|
evaluate_phase4_formula(
|
||||||
|
"alpha_051",
|
||||||
|
high=inputs["high"],
|
||||||
|
low=misaligned_low,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Alpha158 Phase 5: versioned alpha101-alpha150 formula contract ──────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase5_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(101, 151))
|
||||||
|
expected_fields = {
|
||||||
|
"name",
|
||||||
|
"contract_version",
|
||||||
|
"formula",
|
||||||
|
"category",
|
||||||
|
"complexity",
|
||||||
|
"parameters",
|
||||||
|
"description",
|
||||||
|
"references",
|
||||||
|
"call_inputs",
|
||||||
|
"formula_inputs",
|
||||||
|
"input_category",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION == "1.0.0"
|
||||||
|
assert list_phase5_formulas() == expected_ids
|
||||||
|
assert tuple(ALPHA158_PHASE5_FORMULA_SPECS) == expected_ids
|
||||||
|
assert Counter(
|
||||||
|
spec["input_category"] for spec in ALPHA158_PHASE5_FORMULA_SPECS.values()
|
||||||
|
) == {"pair": 33, "triple": 14, "quadruple": 3}
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE5_FORMULA_SPECS.items():
|
||||||
|
assert set(spec) == expected_fields
|
||||||
|
assert spec["name"] == alpha_id
|
||||||
|
assert spec["contract_version"] == ALPHA158_PHASE5_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_PHASE5_FORMULA_SPECS.items()
|
||||||
|
}
|
||||||
|
encoded = json.dumps(
|
||||||
|
serializable_specs,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
ensure_ascii=False,
|
||||||
|
).encode()
|
||||||
|
assert hashlib.sha256(encoded).hexdigest() == ALPHA158_PHASE5_FORMULA_CATALOG_SHA256
|
||||||
|
assert ALPHA158_PHASE5_FORMULA_CATALOG_SHA256 == (
|
||||||
|
"3368796169c9fbd39c4a34ea137e569964b15882fbf4de25124790d548db6533"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase5_formula_catalog_is_recursively_immutable():
|
||||||
|
import operator
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(ALPHA158_PHASE5_FORMULA_SPECS, "alpha_101", {})
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(
|
||||||
|
ALPHA158_PHASE5_FORMULA_SPECS["alpha_101"],
|
||||||
|
"formula",
|
||||||
|
"changed",
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
operator.setitem(
|
||||||
|
ALPHA158_PHASE5_FORMULA_SPECS["alpha_101"]["call_inputs"],
|
||||||
|
0,
|
||||||
|
"volume",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase5_catalog_freezes_callable_and_formula_inputs():
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE5_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_PHASE5_FORMULA_SPECS["alpha_101"]["call_inputs"] == (
|
||||||
|
"close",
|
||||||
|
"high",
|
||||||
|
"low",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase5_dispatch_matches_all_existing_alpha101_alpha150_functions():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
|
||||||
|
for alpha_id, spec in ALPHA158_PHASE5_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_phase5_formula(
|
||||||
|
alpha_id,
|
||||||
|
**{name: inputs[name] for name in reversed(call_inputs)},
|
||||||
|
)
|
||||||
|
pd.testing.assert_series_equal(actual, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase5_dispatch_rejects_unknown_missing_extra_and_non_series_inputs():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
|
||||||
|
with pytest.raises(KeyError, match="not registered"):
|
||||||
|
evaluate_phase5_formula("alpha_100", close=inputs["close"])
|
||||||
|
with pytest.raises(KeyError, match="not registered"):
|
||||||
|
evaluate_phase5_formula("alpha_151", close=inputs["close"])
|
||||||
|
with pytest.raises(ValueError, match=r"missing inputs.*low"):
|
||||||
|
evaluate_phase5_formula(
|
||||||
|
"alpha_101",
|
||||||
|
close=inputs["close"],
|
||||||
|
high=inputs["high"],
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match=r"unexpected inputs.*vwap"):
|
||||||
|
evaluate_phase5_formula(
|
||||||
|
"alpha_101",
|
||||||
|
close=inputs["close"],
|
||||||
|
high=inputs["high"],
|
||||||
|
low=inputs["low"],
|
||||||
|
vwap=inputs["vwap"],
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError, match="high must be a pandas Series"):
|
||||||
|
evaluate_phase5_formula( # type: ignore[arg-type]
|
||||||
|
"alpha_101",
|
||||||
|
close=inputs["close"],
|
||||||
|
high=[1.0, 2.0],
|
||||||
|
low=inputs["low"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase5_dispatch_rejects_length_and_index_alignment_errors():
|
||||||
|
inputs = _phase3_market_inputs()
|
||||||
|
shorter_low = inputs["low"].iloc[:-1]
|
||||||
|
misaligned_high = inputs["high"].rename(index={79: 80})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="low length must match close"):
|
||||||
|
evaluate_phase5_formula(
|
||||||
|
"alpha_101",
|
||||||
|
close=inputs["close"],
|
||||||
|
high=inputs["high"],
|
||||||
|
low=shorter_low,
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="high index must align with close"):
|
||||||
|
evaluate_phase5_formula(
|
||||||
|
"alpha_101",
|
||||||
|
close=inputs["close"],
|
||||||
|
high=misaligned_high,
|
||||||
|
low=inputs["low"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_phase5_contract_is_publicly_exported():
|
||||||
|
assert {
|
||||||
|
"ALPHA158_PHASE5_FORMULA_CONTRACT_VERSION",
|
||||||
|
"ALPHA158_PHASE5_FORMULA_CATALOG_SHA256",
|
||||||
|
"ALPHA158_PHASE5_FORMULA_SPECS",
|
||||||
|
"list_phase5_formulas",
|
||||||
|
"evaluate_phase5_formula",
|
||||||
|
} <= 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(),
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user