Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c7f0c06a7 | ||
|
|
741d5f1ad3 | ||
|
|
cbb56d2f52 | ||
|
|
524fc73852 | ||
|
|
8ef8e3208e | ||
|
|
dc67f0e982 | ||
|
|
1a60fef6e1 | ||
|
|
a5dc04bf7e | ||
|
|
a3cefe364d | ||
|
|
d6c3614301 | ||
|
|
8addce4a68 | ||
|
|
2071d508d0 | ||
|
|
0568cabda1 | ||
|
|
b2f3da9d66 | ||
|
|
cfa5bed188 | ||
|
|
a9465e6479 | ||
|
|
55eeff3951 | ||
|
|
3b1ad07c69 | ||
|
|
1b5b353098 | ||
|
|
2bb9f52080 | ||
|
|
76bb5494a2 | ||
|
|
f7ad82534a | ||
|
|
35a52d781e | ||
|
|
f14ab464f7 | ||
|
|
de2f9494fc | ||
|
|
19fe22b01a | ||
|
|
212351e984 | ||
|
|
5fb4b85cc3 | ||
|
|
a421278527 | ||
|
|
c774a4546a | ||
|
|
022b87fdac | ||
|
|
1b39c53f18 | ||
|
|
b794ab2e8f | ||
|
|
a44e2d306a | ||
|
|
15b283bdf9 | ||
|
|
f9b7f2ab1a | ||
|
|
213aa88deb | ||
|
|
b7f77e2a6c | ||
|
|
c9fb5978f0 | ||
|
|
b2af2a10a6 | ||
|
|
da2ca51ff7 | ||
|
|
ee22d1eb9d | ||
|
|
17b680604d | ||
|
|
c51d803daf | ||
|
|
5578851d85 | ||
|
|
b4f7b74c04 | ||
|
|
0a236622f3 | ||
|
|
88af157ee7 | ||
|
|
a1130ea43b | ||
|
|
96e8f1ad62 | ||
|
|
ecf6b4e4bf | ||
|
|
979166ad16 | ||
|
|
cdf41edf1f | ||
|
|
585a635797 | ||
|
|
2ff0630973 | ||
|
|
bae4dedf70 | ||
|
|
e359792ef5 | ||
|
|
0c3a375b1e |
+2
-15
@@ -16,8 +16,6 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
lite:
|
lite:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
UV_PYTHON_DOWNLOADS: never
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@524e936cd9e579adf00e308bfdf971aebc7de09e
|
- uses: actions/checkout@524e936cd9e579adf00e308bfdf971aebc7de09e
|
||||||
with:
|
with:
|
||||||
@@ -28,18 +26,7 @@ jobs:
|
|||||||
if git ls-files .DS_Store | grep -q .; then echo "跟踪 .DS_Store"; exit 1; fi
|
if git ls-files .DS_Store | grep -q .; then echo "跟踪 .DS_Store"; exit 1; fi
|
||||||
if git grep -n -I -E 'sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|xox[baprs]-[A-Za-z0-9-]{10,}' HEAD | grep -q .; then echo "检出疑似凭证"; exit 1; fi
|
if git grep -n -I -E 'sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|xox[baprs]-[A-Za-z0-9-]{10,}' HEAD | grep -q .; then echo "检出疑似凭证"; exit 1; fi
|
||||||
echo "Gitea 合规校验通过"
|
echo "Gitea 合规校验通过"
|
||||||
|
|
||||||
- name: 验证并同步共享运行时
|
|
||||||
run: |
|
|
||||||
test "$(python3 --version)" = "Python 3.13.15"
|
|
||||||
test "$(uv --version | cut -d' ' -f1-2)" = "uv 0.12.3"
|
|
||||||
uv sync --locked --extra dev
|
|
||||||
uv run --locked --no-sync python -c 'import sys; assert sys.version_info[:2] == (3, 13)'
|
|
||||||
|
|
||||||
- name: 架构模块契约测试
|
- name: 架构模块契约测试
|
||||||
run: |
|
run: python3 tests/governance/test_module_spec.py
|
||||||
uv run --locked --no-sync python tests/governance/test_module_spec.py
|
|
||||||
uv run --locked --no-sync python tests/governance/test_ci_contract.py
|
|
||||||
|
|
||||||
- name: Syntax check
|
- name: Syntax check
|
||||||
run: git ls-files -z '*.py' | xargs -0 uv run --locked --no-sync python -m py_compile
|
run: git ls-files -z '*.py' | xargs -0 python3 -m py_compile
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
3.13
|
|
||||||
+2
-11
@@ -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": 2, "effective_from": "2026-09-01T00:00:00+08:00"},
|
"authority": {"scope": "module_metadata", "subject": "quant_engine", "owner": "quant-engine-owner", "source": "MODULE_SPEC.yaml", "revision": 1, "effective_from": "2026-08-20T00:00:00+08:00"},
|
||||||
"repository": {"name": "quant_engine", "workspace_id": "researchhub", "type": "research_engine", "maturity": "operational"},
|
"repository": {"name": "quant_engine", "workspace_id": "researchhub", "type": "research_engine", "maturity": "operational"},
|
||||||
"bounded_context": {
|
"bounded_context": {
|
||||||
"domain": "quantitative-research-engine",
|
"domain": "quantitative-research-engine",
|
||||||
@@ -24,16 +24,7 @@
|
|||||||
{"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": {
|
"contracts": {"provides": [], "consumes": []},
|
||||||
"provides": [
|
|
||||||
{"contract_id": "researchhub.factor-definition", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/factor_contracts.py"},
|
|
||||||
{"contract_id": "researchhub.factor-set-ref", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/factor_contracts.py"}
|
|
||||||
],
|
|
||||||
"consumes": [
|
|
||||||
{"contract_id": "researchhub.dataset-snapshot", "version": "1.0.0", "authority": "researchhub.data", "admission": "qualified_immutable_envelope"},
|
|
||||||
{"contract_id": "researchhub.data-foundation", "version": "1.0.0", "authority": "researchhub.data", "admission": "content_addressed_selected_views"}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"agent_context": {
|
"agent_context": {
|
||||||
"default_entrypoints": [
|
"default_entrypoints": [
|
||||||
|
|||||||
@@ -19,14 +19,12 @@
|
|||||||
## 模块
|
## 模块
|
||||||
|
|
||||||
- `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` — 因子日 → 下一真实交易日 → 显式执行价 → 日末估值 → 成本后绩效(防前视编排)
|
||||||
- `governed_pipeline` — 数据快照 → 因子版本 → 策略版本 → 回测运行 → 目标组合 → 风险决策 → Paper 订单意图;全链路带确定性 ID,风险拒绝时禁止生成订单意图
|
|
||||||
- `artifact` — 版本化、确定性、存储中立的完整 research run 事实表与 manifest
|
- `artifact` — 版本化、确定性、存储中立的完整 research run 事实表与 manifest
|
||||||
- `attribution` — 基于实际成交后持仓的隔夜 / 日内 / 交易成本逐日收益归因与闭合审计
|
- `attribution` — 基于实际成交后持仓的隔夜 / 日内 / 交易成本逐日收益归因与闭合审计
|
||||||
- `metrics` — 绝对绩效 + 严格日期对齐的 TE / IR / alpha / beta 基准相对绩效
|
- `metrics` — 绝对绩效 + 严格日期对齐的 TE / IR / alpha / beta 基准相对绩效
|
||||||
@@ -57,9 +55,6 @@ 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
@@ -196,68 +191,6 @@ print(backtest.stats())
|
|||||||
print(backtest.benchmark_report())
|
print(backtest.benchmark_report())
|
||||||
```
|
```
|
||||||
|
|
||||||
## 因子/特征合同 v1
|
|
||||||
|
|
||||||
`quant_engine.factor_contracts` 提供 `researchhub.factor-definition` 与
|
|
||||||
`researchhub.factor-set-ref` `1.0.0`。合同使用受限 canonical JSON:只接受 ASCII
|
|
||||||
lower-snake-case object key、UTF-8 string、bool/null 和 safe integer;小数参数必须用显式
|
|
||||||
canonical decimal string。定义、输入映射、上游证据、输出 schema/content 和 lineage 的任一
|
|
||||||
语义变化都会产生新 identity。
|
|
||||||
|
|
||||||
创建 `FactorSetRef` 必须提供完整且可重算 identity 的 `DatasetSnapshotEnvelope` 与
|
|
||||||
`DataFoundationEnvelope`,不能用 ID 字符串或布尔值代替资格证明。每个因子输入都要映射到一个
|
|
||||||
实际选中的 `StandardizedViewRef`,schema 必须同时匹配定义和 view;未消费、缺失、重复或跨
|
|
||||||
snapshot/Foundation/PIT 的 view 都会失败关闭。snapshot PIT 可以早于 Foundation/view PIT,
|
|
||||||
但始终满足 knowledge ≤ snapshot PIT ≤ Foundation/view/FactorSet PIT ≤ evaluation。
|
|
||||||
|
|
||||||
```python
|
|
||||||
from quant_engine.factor_contracts import (
|
|
||||||
DataFoundationEnvelope,
|
|
||||||
DatasetSnapshotEnvelope,
|
|
||||||
FactorDefinition,
|
|
||||||
FactorSetRef,
|
|
||||||
)
|
|
||||||
|
|
||||||
snapshot = DatasetSnapshotEnvelope.from_dict(dataset_snapshot_v1)
|
|
||||||
foundation = DataFoundationEnvelope.from_dict(data_foundation_v1)
|
|
||||||
|
|
||||||
# definition 必须是完整的 FactorDefinition;FactorSetRef.create 还要求显式 input bindings、
|
|
||||||
# view availability、output quality/coverage、canonical output bytes 和 immutable artifact ref。
|
|
||||||
factor_set = FactorSetRef.create(
|
|
||||||
definitions=(definition,),
|
|
||||||
dataset_snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
**explicit_factor_set_evidence,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
`availability_mode="as_available"` 声明 source/view 和计算产物在历史 evaluation 前实际可用;
|
|
||||||
`"retrospective_replay"` 保留历史 evaluation,但要求真实 publication/view creation、compute 和
|
|
||||||
artifact 时间位于之后,并固定 `historical_availability="not_established"`。两种模式都不会授予
|
|
||||||
decision、real-data、production、paper 或 live readiness。
|
|
||||||
|
|
||||||
旧 `governed_pipeline.FactorVersion` 的四字段构造器、`factor_id@version`、run/target/risk/order
|
|
||||||
identity 均保持不变。迁移只能通过 content-addressed `LegacyFactorBinding`,再显式调用
|
|
||||||
`bind_legacy_factor()` 或 `project_legacy_factor()`;后者是有损投影,不表示旧 digest 与新定义
|
|
||||||
digest 等价,也不会把旧 run 静默升级为新合同。
|
|
||||||
|
|
||||||
## 治理垂直切片
|
|
||||||
|
|
||||||
`governed_pipeline` 不复制因子、回测、组合或执行算法,只编排现有能力并补充版本与风险契约。
|
|
||||||
调用方必须显式提供 `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 保持向后兼容):
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
profile: lite
|
|
||||||
runtime_contract: v1
|
|
||||||
language: python
|
|
||||||
python_version: "3.13"
|
|
||||||
python_manager: uv
|
|
||||||
python_root: "."
|
|
||||||
local_test_command: "python3 tests/governance/test_module_spec.py"
|
|
||||||
requires_database: false
|
|
||||||
integration_profile: none
|
|
||||||
@@ -45,8 +45,6 @@
|
|||||||
- `prepare_asset_return_snapshot` 从规范化长表行情生成不前向填充的 simple daily returns;
|
- `prepare_asset_return_snapshot` 从规范化长表行情生成不前向填充的 simple daily returns;
|
||||||
显式 ingestion snapshot ID、源/字段/复权口径、价格值和缺失掩码共同形成
|
显式 ingestion snapshot ID、源/字段/复权口径、价格值和缺失掩码共同形成
|
||||||
`asset-returns-v1:<sha256>`,并把同一 ID 传给 covariance 与 run artifact。
|
`asset-returns-v1:<sha256>`,并把同一 ID 传给 covariance 与 run artifact。
|
||||||
- artifact builder fail closed:每个 `CovarianceSnapshot.data_snapshot_id` 必须与 run 级
|
|
||||||
`data_snapshot_id` 完全一致,禁止把其他行情快照的风险分解静默发布到当前研究运行。
|
|
||||||
- shrinkage 适配器本轮不实现:scikit-learn 尚非声明依赖,未来只允许薄适配
|
- shrinkage 适配器本轮不实现:scikit-learn 尚非声明依赖,未来只允许薄适配
|
||||||
`LedoitWolf` / `OAS`,不复制公式、不依赖环境偶然安装状态。
|
`LedoitWolf` / `OAS`,不复制公式、不依赖环境偶然安装状态。
|
||||||
|
|
||||||
|
|||||||
+3
-6
@@ -7,7 +7,7 @@ name = "quant_engine"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "量化研究引擎 —— alpha 因子库 + 执行仿真 + 技术指标 + 数据适配 + 回测工具(v1.2.0 从 research_results 抽出)"
|
description = "量化研究引擎 —— alpha 因子库 + 执行仿真 + 技术指标 + 数据适配 + 回测工具(v1.2.0 从 research_results 抽出)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13,<3.14"
|
requires-python = ">=3.11"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "researchhub team" },
|
{ name = "researchhub team" },
|
||||||
@@ -39,7 +39,7 @@ where = ["src"]
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = "py313"
|
target-version = "py311"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "PT", "RUF"]
|
select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "PT", "RUF"]
|
||||||
@@ -54,13 +54,10 @@ ignore = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.13"
|
python_version = "3.11"
|
||||||
strict = true
|
strict = true
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
[tool.uv]
|
|
||||||
index-url = "https://mirrors.cloud.tencent.com/pypi/simple"
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
addopts = "-v --tb=short"
|
addopts = "-v --tb=short"
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ v1.2.0 Phase 0:5 个基础算子 + 5 个 alpha 公式(alpha001–alpha005)
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable, Mapping
|
from typing import Any
|
||||||
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
|
||||||
@@ -211,367 +209,6 @@ 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 公式样本) ─────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -3093,541 +2730,6 @@ 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",
|
||||||
@@ -3653,34 +2755,6 @@ __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",
|
||||||
|
|||||||
@@ -390,7 +390,6 @@ def _risk_trade_date(value: object) -> date:
|
|||||||
def _build_risk(
|
def _build_risk(
|
||||||
result: FactorBacktestResult,
|
result: FactorBacktestResult,
|
||||||
run_id: str,
|
run_id: str,
|
||||||
data_snapshot_id: str,
|
|
||||||
risk_snapshots: Mapping[object, CovarianceSnapshot] | None,
|
risk_snapshots: Mapping[object, CovarianceSnapshot] | None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
if risk_snapshots is None:
|
if risk_snapshots is None:
|
||||||
@@ -415,8 +414,6 @@ def _build_risk(
|
|||||||
f"covariance as_of_date {snapshot.as_of_date} must not be after trade date "
|
f"covariance as_of_date {snapshot.as_of_date} must not be after trade date "
|
||||||
f"{trade_date}"
|
f"{trade_date}"
|
||||||
)
|
)
|
||||||
if snapshot.data_snapshot_id != data_snapshot_id:
|
|
||||||
raise ValueError("covariance snapshot data lineage differs from research run")
|
|
||||||
normalized[trade_date] = snapshot
|
normalized[trade_date] = snapshot
|
||||||
|
|
||||||
weights_by_date = result.position_weights
|
weights_by_date = result.position_weights
|
||||||
@@ -575,6 +572,6 @@ def build_research_run_artifact(
|
|||||||
_positions=_build_positions(result, normalized_run_id),
|
_positions=_build_positions(result, normalized_run_id),
|
||||||
_attribution=attribution,
|
_attribution=attribution,
|
||||||
_attribution_daily=attribution_daily,
|
_attribution_daily=attribution_daily,
|
||||||
_risk=_build_risk(result, normalized_run_id, normalized_snapshot, risk_snapshots),
|
_risk=_build_risk(result, normalized_run_id, risk_snapshots),
|
||||||
_performance=_build_performance(result, normalized_run_id, benchmark_returns),
|
_performance=_build_performance(result, normalized_run_id, benchmark_returns),
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,622 +0,0 @@
|
|||||||
"""Versioned, risk-gated, paper-only quantitative research vertical slice.
|
|
||||||
|
|
||||||
The module composes existing ``quant_engine`` calculations into a small set of
|
|
||||||
storage-neutral governance contracts. It never reads a database, calls a data
|
|
||||||
vendor, loads credentials, or routes an order to a broker.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import re
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from dataclasses import asdict, dataclass
|
|
||||||
from datetime import UTC, datetime
|
|
||||||
from enum import StrEnum
|
|
||||||
from types import MappingProxyType
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from quant_engine.execution import ExecutionConfig
|
|
||||||
from quant_engine.factor_contracts import (
|
|
||||||
ContractErrorCode,
|
|
||||||
FactorContractError,
|
|
||||||
FactorDefinition,
|
|
||||||
LegacyFactorBinding,
|
|
||||||
)
|
|
||||||
from quant_engine.research_pipeline import FactorBacktestResult, run_factor_backtest_research
|
|
||||||
|
|
||||||
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
||||||
_GIT_SHA = re.compile(r"^[0-9a-f]{40}$")
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"DatasetSnapshot",
|
|
||||||
"FactorVersion",
|
|
||||||
"bind_legacy_factor",
|
|
||||||
"project_legacy_factor",
|
|
||||||
"StrategyStage",
|
|
||||||
"StrategyVersion",
|
|
||||||
"BacktestRun",
|
|
||||||
"PortfolioTarget",
|
|
||||||
"RiskPolicy",
|
|
||||||
"RiskDecisionStatus",
|
|
||||||
"RiskDecision",
|
|
||||||
"PaperOrderIntent",
|
|
||||||
"GovernedFactorSliceResult",
|
|
||||||
"evaluate_portfolio_risk",
|
|
||||||
"create_paper_order_intent",
|
|
||||||
"run_governed_factor_slice",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _required_text(value: str, name: str) -> str:
|
|
||||||
normalized = value.strip()
|
|
||||||
if not normalized:
|
|
||||||
raise ValueError(f"{name} must be non-empty")
|
|
||||||
return normalized
|
|
||||||
|
|
||||||
|
|
||||||
def _aware_utc(value: datetime, name: str) -> datetime:
|
|
||||||
if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None:
|
|
||||||
raise ValueError(f"{name} must be timezone-aware")
|
|
||||||
return value.astimezone(UTC)
|
|
||||||
|
|
||||||
|
|
||||||
def _canonical_value(value: object) -> object:
|
|
||||||
if value is None or isinstance(value, str | bool | int):
|
|
||||||
return value
|
|
||||||
if isinstance(value, float):
|
|
||||||
if math.isnan(value):
|
|
||||||
return "NaN"
|
|
||||||
if math.isinf(value):
|
|
||||||
return "Infinity" if value > 0 else "-Infinity"
|
|
||||||
return value
|
|
||||||
if isinstance(value, datetime):
|
|
||||||
return _aware_utc(value, "datetime").isoformat()
|
|
||||||
if isinstance(value, StrEnum):
|
|
||||||
return value.value
|
|
||||||
if isinstance(value, Mapping):
|
|
||||||
return {
|
|
||||||
str(key): _canonical_value(item)
|
|
||||||
for key, item in sorted(value.items(), key=lambda pair: str(pair[0]))
|
|
||||||
}
|
|
||||||
if isinstance(value, list | tuple):
|
|
||||||
return [_canonical_value(item) for item in value]
|
|
||||||
raise TypeError(f"unsupported canonical value: {type(value).__name__}")
|
|
||||||
|
|
||||||
|
|
||||||
def _stable_id(prefix: str, payload: Mapping[str, object]) -> str:
|
|
||||||
encoded = json.dumps(
|
|
||||||
_canonical_value(payload),
|
|
||||||
ensure_ascii=False,
|
|
||||||
sort_keys=True,
|
|
||||||
separators=(",", ":"),
|
|
||||||
allow_nan=False,
|
|
||||||
).encode("utf-8")
|
|
||||||
return f"{prefix}:{hashlib.sha256(encoded).hexdigest()}"
|
|
||||||
|
|
||||||
|
|
||||||
def _immutable_weights(values: Mapping[str, float]) -> Mapping[str, float]:
|
|
||||||
normalized: dict[str, float] = {}
|
|
||||||
for raw_asset, raw_weight in values.items():
|
|
||||||
asset = _required_text(str(raw_asset), "asset_id")
|
|
||||||
weight = float(raw_weight)
|
|
||||||
if not math.isfinite(weight) or weight < 0:
|
|
||||||
raise ValueError("portfolio weights must be finite and non-negative")
|
|
||||||
if asset in normalized:
|
|
||||||
raise ValueError(f"duplicate asset_id: {asset}")
|
|
||||||
normalized[asset] = weight
|
|
||||||
return MappingProxyType(dict(sorted(normalized.items())))
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class DatasetSnapshot:
|
|
||||||
"""Point-in-time identity for caller-supplied research data."""
|
|
||||||
|
|
||||||
snapshot_id: str
|
|
||||||
schema_version: str
|
|
||||||
content_sha256: str
|
|
||||||
effective_at: datetime
|
|
||||||
available_at: datetime
|
|
||||||
ingested_at: datetime
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
object.__setattr__(self, "snapshot_id", _required_text(self.snapshot_id, "snapshot_id"))
|
|
||||||
object.__setattr__(
|
|
||||||
self, "schema_version", _required_text(self.schema_version, "schema_version")
|
|
||||||
)
|
|
||||||
digest = self.content_sha256.strip().lower()
|
|
||||||
if not _SHA256.fullmatch(digest):
|
|
||||||
raise ValueError("content_sha256 must be a lowercase SHA-256 digest")
|
|
||||||
object.__setattr__(self, "content_sha256", digest)
|
|
||||||
effective_at = _aware_utc(self.effective_at, "effective_at")
|
|
||||||
available_at = _aware_utc(self.available_at, "available_at")
|
|
||||||
ingested_at = _aware_utc(self.ingested_at, "ingested_at")
|
|
||||||
if not effective_at <= available_at <= ingested_at:
|
|
||||||
raise ValueError("timestamps must satisfy effective_at <= available_at <= ingested_at")
|
|
||||||
object.__setattr__(self, "effective_at", effective_at)
|
|
||||||
object.__setattr__(self, "available_at", available_at)
|
|
||||||
object.__setattr__(self, "ingested_at", ingested_at)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class FactorVersion:
|
|
||||||
"""Versioned factor definition identity without factor implementation duplication."""
|
|
||||||
|
|
||||||
factor_id: str
|
|
||||||
version: str
|
|
||||||
definition_sha256: str
|
|
||||||
dataset_schema_version: str
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
object.__setattr__(self, "factor_id", _required_text(self.factor_id, "factor_id"))
|
|
||||||
object.__setattr__(self, "version", _required_text(self.version, "version"))
|
|
||||||
object.__setattr__(
|
|
||||||
self,
|
|
||||||
"dataset_schema_version",
|
|
||||||
_required_text(self.dataset_schema_version, "dataset_schema_version"),
|
|
||||||
)
|
|
||||||
digest = self.definition_sha256.strip().lower()
|
|
||||||
if not _SHA256.fullmatch(digest):
|
|
||||||
raise ValueError("definition_sha256 must be a lowercase SHA-256 digest")
|
|
||||||
object.__setattr__(self, "definition_sha256", digest)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version_id(self) -> str:
|
|
||||||
return f"{self.factor_id}@{self.version}"
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_legacy_factor_binding(
|
|
||||||
legacy: FactorVersion,
|
|
||||||
definition: FactorDefinition,
|
|
||||||
binding: LegacyFactorBinding,
|
|
||||||
) -> None:
|
|
||||||
if not isinstance(legacy, FactorVersion):
|
|
||||||
raise FactorContractError(
|
|
||||||
ContractErrorCode.TYPE_ERROR,
|
|
||||||
"$.legacy",
|
|
||||||
"FactorVersion is required",
|
|
||||||
)
|
|
||||||
if not isinstance(definition, FactorDefinition):
|
|
||||||
raise FactorContractError(
|
|
||||||
ContractErrorCode.TYPE_ERROR,
|
|
||||||
"$.definition",
|
|
||||||
"complete FactorDefinition is required",
|
|
||||||
)
|
|
||||||
if not isinstance(binding, LegacyFactorBinding):
|
|
||||||
raise FactorContractError(
|
|
||||||
ContractErrorCode.TYPE_ERROR,
|
|
||||||
"$.binding",
|
|
||||||
"LegacyFactorBinding is required",
|
|
||||||
)
|
|
||||||
expected = (
|
|
||||||
definition.definition_id,
|
|
||||||
legacy.factor_id,
|
|
||||||
legacy.version,
|
|
||||||
legacy.definition_sha256,
|
|
||||||
legacy.dataset_schema_version,
|
|
||||||
definition.input_schema_digest,
|
|
||||||
)
|
|
||||||
actual = (
|
|
||||||
binding.definition_id,
|
|
||||||
binding.legacy_factor_id,
|
|
||||||
binding.legacy_version,
|
|
||||||
binding.legacy_definition_sha256,
|
|
||||||
binding.legacy_dataset_schema_version,
|
|
||||||
binding.canonical_input_schema_digest,
|
|
||||||
)
|
|
||||||
if actual != expected:
|
|
||||||
raise FactorContractError(
|
|
||||||
ContractErrorCode.LEGACY_BINDING_MISMATCH,
|
|
||||||
"$.binding",
|
|
||||||
"binding does not exactly associate the supplied legacy and canonical identities",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def bind_legacy_factor(
|
|
||||||
legacy: FactorVersion,
|
|
||||||
complete_definition: FactorDefinition,
|
|
||||||
binding: LegacyFactorBinding,
|
|
||||||
) -> FactorDefinition:
|
|
||||||
"""Validate an explicit migration binding without manufacturing missing semantics."""
|
|
||||||
|
|
||||||
_validate_legacy_factor_binding(legacy, complete_definition, binding)
|
|
||||||
return complete_definition
|
|
||||||
|
|
||||||
|
|
||||||
def project_legacy_factor(
|
|
||||||
complete_definition: FactorDefinition,
|
|
||||||
binding: LegacyFactorBinding,
|
|
||||||
) -> FactorVersion:
|
|
||||||
"""Project a canonical definition into its recorded, explicitly lossy legacy identity."""
|
|
||||||
|
|
||||||
legacy = FactorVersion(
|
|
||||||
factor_id=binding.legacy_factor_id,
|
|
||||||
version=binding.legacy_version,
|
|
||||||
definition_sha256=binding.legacy_definition_sha256,
|
|
||||||
dataset_schema_version=binding.legacy_dataset_schema_version,
|
|
||||||
)
|
|
||||||
_validate_legacy_factor_binding(legacy, complete_definition, binding)
|
|
||||||
return legacy
|
|
||||||
|
|
||||||
|
|
||||||
class StrategyStage(StrEnum):
|
|
||||||
DRAFT = "Draft"
|
|
||||||
RESEARCH = "Research"
|
|
||||||
VALIDATED = "Validated"
|
|
||||||
APPROVED = "Approved"
|
|
||||||
PAPER = "Paper"
|
|
||||||
LIVE = "Live"
|
|
||||||
PAUSED = "Paused"
|
|
||||||
RETIRED = "Retired"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class StrategyVersion:
|
|
||||||
"""Strategy lineage and lifecycle state used by the governed slice."""
|
|
||||||
|
|
||||||
strategy_id: str
|
|
||||||
version: str
|
|
||||||
factor_version_id: str
|
|
||||||
stage: StrategyStage
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
object.__setattr__(self, "strategy_id", _required_text(self.strategy_id, "strategy_id"))
|
|
||||||
object.__setattr__(self, "version", _required_text(self.version, "version"))
|
|
||||||
object.__setattr__(
|
|
||||||
self,
|
|
||||||
"factor_version_id",
|
|
||||||
_required_text(self.factor_version_id, "factor_version_id"),
|
|
||||||
)
|
|
||||||
if not isinstance(self.stage, StrategyStage):
|
|
||||||
raise TypeError("stage must be a StrategyStage")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version_id(self) -> str:
|
|
||||||
return f"{self.strategy_id}@{self.version}"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class BacktestRun:
|
|
||||||
run_id: str
|
|
||||||
dataset_snapshot_id: str
|
|
||||||
factor_version_id: str
|
|
||||||
strategy_version_id: str
|
|
||||||
code_revision: str
|
|
||||||
config_hash: str
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class PortfolioTarget:
|
|
||||||
target_id: str
|
|
||||||
backtest_run_id: str
|
|
||||||
dataset_snapshot_id: str
|
|
||||||
weights: Mapping[str, float]
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
object.__setattr__(self, "weights", _immutable_weights(self.weights))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gross_exposure(self) -> float:
|
|
||||||
return float(sum(abs(weight) for weight in self.weights.values()))
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class RiskPolicy:
|
|
||||||
policy_id: str
|
|
||||||
max_gross_exposure: float
|
|
||||||
max_single_asset_weight: float
|
|
||||||
max_positions: int
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
object.__setattr__(self, "policy_id", _required_text(self.policy_id, "policy_id"))
|
|
||||||
if not math.isfinite(self.max_gross_exposure) or self.max_gross_exposure <= 0:
|
|
||||||
raise ValueError("max_gross_exposure must be positive and finite")
|
|
||||||
if not math.isfinite(self.max_single_asset_weight) or not (
|
|
||||||
0 < self.max_single_asset_weight <= 1
|
|
||||||
):
|
|
||||||
raise ValueError("max_single_asset_weight must be in (0, 1]")
|
|
||||||
if (
|
|
||||||
isinstance(self.max_positions, bool)
|
|
||||||
or not isinstance(self.max_positions, int)
|
|
||||||
or self.max_positions <= 0
|
|
||||||
):
|
|
||||||
raise ValueError("max_positions must be a positive integer")
|
|
||||||
|
|
||||||
|
|
||||||
class RiskDecisionStatus(StrEnum):
|
|
||||||
APPROVED = "approved"
|
|
||||||
REJECTED = "rejected"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class RiskDecision:
|
|
||||||
decision_id: str
|
|
||||||
portfolio_target_id: str
|
|
||||||
policy_id: str
|
|
||||||
status: RiskDecisionStatus
|
|
||||||
reasons: tuple[str, ...]
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True, init=False)
|
|
||||||
class PaperOrderIntent:
|
|
||||||
intent_id: str
|
|
||||||
portfolio_target_id: str
|
|
||||||
risk_decision_id: str
|
|
||||||
environment: str
|
|
||||||
target_weights: Mapping[str, float]
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
def __init__(self, target: PortfolioTarget, decision: RiskDecision) -> None:
|
|
||||||
if decision.status is not RiskDecisionStatus.APPROVED:
|
|
||||||
raise ValueError("an approved risk decision is required")
|
|
||||||
if decision.portfolio_target_id != target.target_id:
|
|
||||||
raise ValueError("risk decision does not bind the supplied portfolio target")
|
|
||||||
target_weights = _immutable_weights(target.weights)
|
|
||||||
payload = {
|
|
||||||
"portfolio_target_id": target.target_id,
|
|
||||||
"risk_decision_id": decision.decision_id,
|
|
||||||
"environment": "paper",
|
|
||||||
"target_weights": target_weights,
|
|
||||||
"created_at": decision.created_at,
|
|
||||||
}
|
|
||||||
object.__setattr__(self, "intent_id", _stable_id("order-intent", payload))
|
|
||||||
object.__setattr__(self, "portfolio_target_id", target.target_id)
|
|
||||||
object.__setattr__(self, "risk_decision_id", decision.decision_id)
|
|
||||||
object.__setattr__(self, "environment", "paper")
|
|
||||||
object.__setattr__(self, "target_weights", target_weights)
|
|
||||||
object.__setattr__(self, "created_at", decision.created_at)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class GovernedFactorSliceResult:
|
|
||||||
dataset_snapshot: DatasetSnapshot
|
|
||||||
factor_version: FactorVersion
|
|
||||||
strategy_version: StrategyVersion
|
|
||||||
research_result: FactorBacktestResult
|
|
||||||
backtest_run: BacktestRun
|
|
||||||
portfolio_target: PortfolioTarget
|
|
||||||
risk_decision: RiskDecision
|
|
||||||
order_intent: PaperOrderIntent | None
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_portfolio_risk(
|
|
||||||
target: PortfolioTarget,
|
|
||||||
policy: RiskPolicy,
|
|
||||||
*,
|
|
||||||
created_at: datetime | None = None,
|
|
||||||
) -> RiskDecision:
|
|
||||||
"""Apply fail-closed paper risk limits to a versioned target portfolio."""
|
|
||||||
decision_time = target.created_at if created_at is None else _aware_utc(created_at, "created_at")
|
|
||||||
reasons: list[str] = []
|
|
||||||
if target.gross_exposure > policy.max_gross_exposure + 1e-12:
|
|
||||||
reasons.append(
|
|
||||||
f"gross exposure {target.gross_exposure:.12g} exceeds "
|
|
||||||
f"limit {policy.max_gross_exposure:.12g}"
|
|
||||||
)
|
|
||||||
positive_weights = [weight for weight in target.weights.values() if weight > 0]
|
|
||||||
largest_weight = max(positive_weights, default=0.0)
|
|
||||||
if largest_weight > policy.max_single_asset_weight + 1e-12:
|
|
||||||
reasons.append(
|
|
||||||
f"single-asset weight {largest_weight:.12g} exceeds "
|
|
||||||
f"limit {policy.max_single_asset_weight:.12g}"
|
|
||||||
)
|
|
||||||
if len(positive_weights) > policy.max_positions:
|
|
||||||
reasons.append(
|
|
||||||
f"position count {len(positive_weights)} exceeds limit {policy.max_positions}"
|
|
||||||
)
|
|
||||||
status = RiskDecisionStatus.REJECTED if reasons else RiskDecisionStatus.APPROVED
|
|
||||||
payload = {
|
|
||||||
"portfolio_target_id": target.target_id,
|
|
||||||
"policy_id": policy.policy_id,
|
|
||||||
"status": status,
|
|
||||||
"reasons": reasons,
|
|
||||||
"created_at": decision_time,
|
|
||||||
}
|
|
||||||
return RiskDecision(
|
|
||||||
decision_id=_stable_id("risk-decision", payload),
|
|
||||||
portfolio_target_id=target.target_id,
|
|
||||||
policy_id=policy.policy_id,
|
|
||||||
status=status,
|
|
||||||
reasons=tuple(reasons),
|
|
||||||
created_at=decision_time,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_paper_order_intent(
|
|
||||||
target: PortfolioTarget,
|
|
||||||
decision: RiskDecision,
|
|
||||||
) -> PaperOrderIntent:
|
|
||||||
"""Create a paper-only intent after an exact, approved risk decision."""
|
|
||||||
return PaperOrderIntent(target, decision)
|
|
||||||
|
|
||||||
|
|
||||||
def run_governed_factor_slice(
|
|
||||||
*,
|
|
||||||
factor_scores: pd.DataFrame,
|
|
||||||
execution_prices: pd.DataFrame,
|
|
||||||
valuation_prices: pd.DataFrame,
|
|
||||||
dataset_snapshot: DatasetSnapshot,
|
|
||||||
factor_version: FactorVersion,
|
|
||||||
strategy_version: StrategyVersion,
|
|
||||||
risk_policy: RiskPolicy,
|
|
||||||
code_revision: str,
|
|
||||||
created_at: datetime,
|
|
||||||
top_k: int,
|
|
||||||
execution_price_field: str,
|
|
||||||
valuation_price_field: str,
|
|
||||||
lag_sessions: int = 1,
|
|
||||||
gross_exposure: float = 1.0,
|
|
||||||
largest: bool = True,
|
|
||||||
initial_cash: float = 1_000_000.0,
|
|
||||||
execution_config: ExecutionConfig | None = None,
|
|
||||||
) -> GovernedFactorSliceResult:
|
|
||||||
"""Run snapshot → factor → strategy → backtest → target → risk → paper intent."""
|
|
||||||
if strategy_version.factor_version_id != factor_version.version_id:
|
|
||||||
raise ValueError("strategy factor lineage does not match factor_version")
|
|
||||||
if dataset_snapshot.schema_version != factor_version.dataset_schema_version:
|
|
||||||
raise ValueError("factor dataset schema does not match dataset snapshot")
|
|
||||||
if strategy_version.stage not in {StrategyStage.APPROVED, StrategyStage.PAPER}:
|
|
||||||
raise ValueError("strategy must be in Approved or Paper stage")
|
|
||||||
normalized_revision = code_revision.strip().lower()
|
|
||||||
if not _GIT_SHA.fullmatch(normalized_revision):
|
|
||||||
raise ValueError("code_revision must be a lowercase 40-character Git SHA")
|
|
||||||
run_time = _aware_utc(created_at, "created_at")
|
|
||||||
if dataset_snapshot.available_at > run_time or dataset_snapshot.ingested_at > run_time:
|
|
||||||
raise ValueError("dataset snapshot must be available before the research run")
|
|
||||||
if not factor_scores.empty:
|
|
||||||
latest_decision = pd.Timestamp(factor_scores.index.max())
|
|
||||||
latest_decision_date = latest_decision.date()
|
|
||||||
if latest_decision_date > run_time.date():
|
|
||||||
raise ValueError("factor_scores contain future decision dates")
|
|
||||||
config = ExecutionConfig() if execution_config is None else execution_config
|
|
||||||
if not isinstance(config, ExecutionConfig):
|
|
||||||
raise TypeError("execution_config must be an ExecutionConfig")
|
|
||||||
|
|
||||||
parameters: dict[str, object] = {
|
|
||||||
"top_k": top_k,
|
|
||||||
"execution_price_field": execution_price_field,
|
|
||||||
"valuation_price_field": valuation_price_field,
|
|
||||||
"lag_sessions": lag_sessions,
|
|
||||||
"gross_exposure": gross_exposure,
|
|
||||||
"largest": largest,
|
|
||||||
"initial_cash": initial_cash,
|
|
||||||
"execution_config": asdict(config),
|
|
||||||
}
|
|
||||||
config_hash = _stable_id("config", parameters).split(":", maxsplit=1)[1]
|
|
||||||
run_payload = {
|
|
||||||
"dataset_snapshot_id": dataset_snapshot.snapshot_id,
|
|
||||||
"dataset_content_sha256": dataset_snapshot.content_sha256,
|
|
||||||
"factor_version_id": factor_version.version_id,
|
|
||||||
"factor_definition_sha256": factor_version.definition_sha256,
|
|
||||||
"strategy_version_id": strategy_version.version_id,
|
|
||||||
"code_revision": normalized_revision,
|
|
||||||
"config_hash": config_hash,
|
|
||||||
"created_at": run_time,
|
|
||||||
}
|
|
||||||
backtest_run = BacktestRun(
|
|
||||||
run_id=_stable_id("backtest-run", run_payload),
|
|
||||||
dataset_snapshot_id=dataset_snapshot.snapshot_id,
|
|
||||||
factor_version_id=factor_version.version_id,
|
|
||||||
strategy_version_id=strategy_version.version_id,
|
|
||||||
code_revision=normalized_revision,
|
|
||||||
config_hash=config_hash,
|
|
||||||
created_at=run_time,
|
|
||||||
)
|
|
||||||
|
|
||||||
research_result = run_factor_backtest_research(
|
|
||||||
factor_scores,
|
|
||||||
execution_prices,
|
|
||||||
valuation_prices,
|
|
||||||
top_k=top_k,
|
|
||||||
execution_price_field=execution_price_field,
|
|
||||||
valuation_price_field=valuation_price_field,
|
|
||||||
lag_sessions=lag_sessions,
|
|
||||||
gross_exposure=gross_exposure,
|
|
||||||
largest=largest,
|
|
||||||
initial_cash=initial_cash,
|
|
||||||
config=config,
|
|
||||||
)
|
|
||||||
if research_result.schedule.decision_weights.empty:
|
|
||||||
raise ValueError("governed slice requires at least one target portfolio")
|
|
||||||
final_weights = {
|
|
||||||
str(asset): float(weight)
|
|
||||||
for asset, weight in research_result.schedule.decision_weights.iloc[-1].items()
|
|
||||||
}
|
|
||||||
target_payload = {
|
|
||||||
"backtest_run_id": backtest_run.run_id,
|
|
||||||
"dataset_snapshot_id": dataset_snapshot.snapshot_id,
|
|
||||||
"weights": final_weights,
|
|
||||||
"created_at": run_time,
|
|
||||||
}
|
|
||||||
portfolio_target = PortfolioTarget(
|
|
||||||
target_id=_stable_id("portfolio-target", target_payload),
|
|
||||||
backtest_run_id=backtest_run.run_id,
|
|
||||||
dataset_snapshot_id=dataset_snapshot.snapshot_id,
|
|
||||||
weights=final_weights,
|
|
||||||
created_at=run_time,
|
|
||||||
)
|
|
||||||
risk_decision = evaluate_portfolio_risk(
|
|
||||||
portfolio_target,
|
|
||||||
risk_policy,
|
|
||||||
created_at=run_time,
|
|
||||||
)
|
|
||||||
order_intent = (
|
|
||||||
create_paper_order_intent(portfolio_target, risk_decision)
|
|
||||||
if risk_decision.status is RiskDecisionStatus.APPROVED
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
return GovernedFactorSliceResult(
|
|
||||||
dataset_snapshot=dataset_snapshot,
|
|
||||||
factor_version=factor_version,
|
|
||||||
strategy_version=strategy_version,
|
|
||||||
research_result=research_result,
|
|
||||||
backtest_run=backtest_run,
|
|
||||||
portfolio_target=portfolio_target,
|
|
||||||
risk_decision=risk_decision,
|
|
||||||
order_intent=order_intent,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _demo() -> Mapping[str, object]:
|
|
||||||
dates = pd.date_range("2026-01-05", periods=4, freq="B")
|
|
||||||
scores = pd.DataFrame({"A": [2.0, 1.0], "B": [1.0, 2.0]}, index=dates[:2])
|
|
||||||
opens = pd.DataFrame({"A": [10.0, 10.0, 10.2, 10.3], "B": [20.0, 20.0, 20.5, 21.0]}, index=dates)
|
|
||||||
result = run_governed_factor_slice(
|
|
||||||
factor_scores=scores,
|
|
||||||
execution_prices=opens,
|
|
||||||
valuation_prices=opens * 1.01,
|
|
||||||
dataset_snapshot=DatasetSnapshot(
|
|
||||||
snapshot_id="dataset:architecture-smoke-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),
|
|
||||||
),
|
|
||||||
factor_version=FactorVersion(
|
|
||||||
factor_id="factor:architecture-smoke",
|
|
||||||
version="1.0.0",
|
|
||||||
definition_sha256="b" * 64,
|
|
||||||
dataset_schema_version="1.0.0",
|
|
||||||
),
|
|
||||||
strategy_version=StrategyVersion(
|
|
||||||
strategy_id="strategy:architecture-smoke",
|
|
||||||
version="1.0.0",
|
|
||||||
factor_version_id="factor:architecture-smoke@1.0.0",
|
|
||||||
stage=StrategyStage.APPROVED,
|
|
||||||
),
|
|
||||||
risk_policy=RiskPolicy(
|
|
||||||
policy_id="risk:architecture-smoke@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=ExecutionConfig(
|
|
||||||
commission_bps=0,
|
|
||||||
stamp_tax_bps=0,
|
|
||||||
slippage_bps=0,
|
|
||||||
min_trade_amount=0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"backtest_run_id": result.backtest_run.run_id,
|
|
||||||
"portfolio_target_id": result.portfolio_target.target_id,
|
|
||||||
"risk_decision": result.risk_decision.status.value,
|
|
||||||
"order_intent_id": result.order_intent.intent_id if result.order_intent else None,
|
|
||||||
"environment": result.order_intent.environment if result.order_intent else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print(json.dumps(_demo(), ensure_ascii=False, sort_keys=True))
|
|
||||||
-206
@@ -1,206 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,22 +9,14 @@ ROOT = Path(__file__).resolve().parents[2]
|
|||||||
|
|
||||||
|
|
||||||
class CiContractTests(unittest.TestCase):
|
class CiContractTests(unittest.TestCase):
|
||||||
def test_ci_is_one_locked_shared_runtime_lite_gate(self) -> None:
|
def test_ci_is_one_dependency_free_lite_gate(self) -> None:
|
||||||
workflow = (ROOT / ".gitea/workflows/ci.yml").read_text(encoding="utf-8")
|
workflow = (ROOT / ".gitea/workflows/ci.yml").read_text(encoding="utf-8")
|
||||||
jobs = workflow.split("jobs:", 1)[1]
|
jobs = workflow.split("jobs:", 1)[1]
|
||||||
self.assertEqual(re.findall(r"(?m)^ ([a-z][a-z0-9_-]*):\s*$", jobs), ["lite"])
|
self.assertEqual(re.findall(r"(?m)^ ([a-z][a-z0-9_-]*):\s*$", jobs), ["lite"])
|
||||||
self.assertIn("actions/checkout@524e936cd9e579adf00e308bfdf971aebc7de09e", workflow)
|
self.assertIn("actions/checkout@524e936cd9e579adf00e308bfdf971aebc7de09e", workflow)
|
||||||
self.assertIn("persist-credentials: false", workflow)
|
self.assertIn("persist-credentials: false", workflow)
|
||||||
self.assertIn("UV_PYTHON_DOWNLOADS: never", workflow)
|
self.assertIn("python3 tests/governance/test_module_spec.py", workflow)
|
||||||
self.assertIn('test "$(python3 --version)" = "Python 3.13.15"', workflow)
|
for forbidden in ("setup-python", "pip ", "curl ", "wget ", "docker pull"):
|
||||||
self.assertIn(
|
|
||||||
'test "$(uv --version | cut -d\' \' -f1-2)" = "uv 0.12.3"',
|
|
||||||
workflow,
|
|
||||||
)
|
|
||||||
self.assertIn("uv sync --locked --extra dev", workflow)
|
|
||||||
self.assertIn("uv run --locked --no-sync python", workflow)
|
|
||||||
self.assertIn("tests/governance/test_ci_contract.py", workflow)
|
|
||||||
for forbidden in ("setup-python", "setup-uv", "pip ", "curl ", "wget ", "docker pull"):
|
|
||||||
self.assertNotIn(forbidden, workflow)
|
self.assertNotIn(forbidden, workflow)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,32 @@
|
|||||||
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]
|
||||||
|
|
||||||
|
|
||||||
def test_module_spec_declares_pure_research_engine_boundary() -> None:
|
class ModuleSpecTests(unittest.TestCase):
|
||||||
spec = json.loads((ROOT / "MODULE_SPEC.yaml").read_text(encoding="utf-8"))
|
def test_module_spec_declares_pure_research_engine_boundary(self) -> None:
|
||||||
assert spec["module_id"] == "quant_engine"
|
spec = json.loads((ROOT / "MODULE_SPEC.yaml").read_text(encoding="utf-8"))
|
||||||
assert spec["authority"]["subject"] == spec["module_id"]
|
self.assertEqual(spec["module_id"], "quant_engine")
|
||||||
assert spec["repository"]["type"] == "research_engine"
|
self.assertEqual(spec["authority"]["subject"], spec["module_id"])
|
||||||
assert spec["bounded_context"]["domain"] == "quantitative-research-engine"
|
self.assertEqual(spec["repository"]["type"], "research_engine")
|
||||||
prohibited = " ".join(spec["bounded_context"]["prohibited_responsibilities"]).lower()
|
self.assertEqual(spec["bounded_context"]["domain"], "quantitative-research-engine")
|
||||||
for term in ("investment advice", "live order", "credentials", "source facts"):
|
prohibited = " ".join(spec["bounded_context"]["prohibited_responsibilities"]).lower()
|
||||||
assert term in prohibited
|
for term in ("investment advice", "live order", "credentials", "source facts"):
|
||||||
assert spec["authority"]["revision"] == 2
|
self.assertIn(term, prohibited)
|
||||||
assert {
|
self.assertEqual(spec["contracts"], {"provides": [], "consumes": []})
|
||||||
(item["contract_id"], item["version"])
|
self.assertEqual(spec["dependencies"], [])
|
||||||
for item in spec["contracts"]["provides"]
|
self.assertTrue(
|
||||||
} == {
|
all(
|
||||||
("researchhub.factor-definition", "1.0.0"),
|
command["required"] and not command["network"]
|
||||||
("researchhub.factor-set-ref", "1.0.0"),
|
for command in spec["verification"]["commands"]
|
||||||
}
|
)
|
||||||
assert all(
|
)
|
||||||
item["authority"] == "quant_engine"
|
|
||||||
and item["path"] == "src/quant_engine/factor_contracts.py"
|
|
||||||
for item in spec["contracts"]["provides"]
|
|
||||||
)
|
|
||||||
assert {
|
|
||||||
(item["contract_id"], item["version"])
|
|
||||||
for item in spec["contracts"]["consumes"]
|
|
||||||
} == {
|
|
||||||
("researchhub.dataset-snapshot", "1.0.0"),
|
|
||||||
("researchhub.data-foundation", "1.0.0"),
|
|
||||||
}
|
|
||||||
assert all(
|
|
||||||
item["authority"] == "researchhub.data"
|
|
||||||
for item in spec["contracts"]["consumes"]
|
|
||||||
)
|
|
||||||
assert spec["dependencies"] == []
|
|
||||||
assert all(
|
|
||||||
command["required"] and not command["network"]
|
|
||||||
for command in spec["verification"]["commands"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_module_spec_declares_pure_research_engine_boundary()
|
unittest.main()
|
||||||
|
|||||||
@@ -6,30 +6,8 @@ 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,
|
||||||
@@ -188,18 +166,6 @@ 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,
|
||||||
@@ -441,50 +407,6 @@ 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")
|
||||||
@@ -1310,901 +1232,3 @@ 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__)
|
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ def test_research_artifact_projects_annualized_risk_from_actual_positions() -> N
|
|||||||
covariance=covariance,
|
covariance=covariance,
|
||||||
return_frequency="1d",
|
return_frequency="1d",
|
||||||
periods_per_year=252,
|
periods_per_year=252,
|
||||||
data_snapshot_id="qtdb-pro-20260108-v1",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
artifact = _build(result, risk_snapshots={trade_date: snapshot})
|
artifact = _build(result, risk_snapshots={trade_date: snapshot})
|
||||||
@@ -183,31 +182,6 @@ def test_research_artifact_projects_annualized_risk_from_actual_positions() -> N
|
|||||||
assert risk["risk_contribution"].sum() == pytest.approx(1.0)
|
assert risk["risk_contribution"].sum() == pytest.approx(1.0)
|
||||||
|
|
||||||
|
|
||||||
def test_research_artifact_rejects_risk_from_a_different_data_snapshot() -> None:
|
|
||||||
result = _backtest_result()
|
|
||||||
trade_date = result.position_weights.index[-1].date()
|
|
||||||
covariance = pd.DataFrame(
|
|
||||||
[[0.0001, 0.0], [0.0, 0.0004]],
|
|
||||||
index=["A", "B"],
|
|
||||||
columns=["A", "B"],
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="data lineage differs"):
|
|
||||||
_build(
|
|
||||||
result,
|
|
||||||
risk_snapshots={
|
|
||||||
trade_date: CovarianceSnapshot(
|
|
||||||
snapshot_id="foreign-covariance",
|
|
||||||
as_of_date="2026-01-07",
|
|
||||||
covariance=covariance,
|
|
||||||
return_frequency="1d",
|
|
||||||
periods_per_year=252,
|
|
||||||
data_snapshot_id="different-market-snapshot",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_research_artifact_rejects_future_or_misaligned_risk_snapshots() -> None:
|
def test_research_artifact_rejects_future_or_misaligned_risk_snapshots() -> None:
|
||||||
result = _backtest_result()
|
result = _backtest_result()
|
||||||
trade_date = result.position_weights.index[-1].date()
|
trade_date = result.position_weights.index[-1].date()
|
||||||
@@ -227,7 +201,6 @@ def test_research_artifact_rejects_future_or_misaligned_risk_snapshots() -> None
|
|||||||
covariance=covariance,
|
covariance=covariance,
|
||||||
return_frequency="1d",
|
return_frequency="1d",
|
||||||
periods_per_year=252,
|
periods_per_year=252,
|
||||||
data_snapshot_id="qtdb-pro-20260108-v1",
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -242,7 +215,6 @@ def test_research_artifact_rejects_future_or_misaligned_risk_snapshots() -> None
|
|||||||
covariance=covariance.loc[["B"], ["B"]],
|
covariance=covariance.loc[["B"], ["B"]],
|
||||||
return_frequency="1d",
|
return_frequency="1d",
|
||||||
periods_per_year=252,
|
periods_per_year=252,
|
||||||
data_snapshot_id="qtdb-pro-20260108-v1",
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,888 +0,0 @@
|
|||||||
"""Versioned factor-definition and factor-set contract conformance."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
from dataclasses import FrozenInstanceError
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Callable
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from quant_engine.factor_contracts import (
|
|
||||||
ActorIdentity,
|
|
||||||
AvailabilityMode,
|
|
||||||
ContractErrorCode,
|
|
||||||
Causation,
|
|
||||||
DataFoundationEnvelope,
|
|
||||||
DatasetSnapshotEnvelope,
|
|
||||||
FactorContractError,
|
|
||||||
FactorDefinition,
|
|
||||||
FactorInput,
|
|
||||||
FactorSetRef,
|
|
||||||
HistoricalAvailability,
|
|
||||||
InputBinding,
|
|
||||||
LegacyFactorBinding,
|
|
||||||
OutputArtifactRef,
|
|
||||||
OutputCoverage,
|
|
||||||
OutputQuality,
|
|
||||||
OutputQualityCheck,
|
|
||||||
PayloadValidation,
|
|
||||||
ProducerIdentity,
|
|
||||||
TypedParameter,
|
|
||||||
ViewAvailability,
|
|
||||||
canonical_json_bytes,
|
|
||||||
factor_definition_from_alpha158,
|
|
||||||
factor_input_schema_digest,
|
|
||||||
validate_factor_catalog,
|
|
||||||
)
|
|
||||||
from quant_engine.governed_pipeline import (
|
|
||||||
FactorVersion,
|
|
||||||
bind_legacy_factor,
|
|
||||||
project_legacy_factor,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
FIXTURE_PATH = Path(__file__).parent / "fixtures" / "factor-contracts-v1.golden.json"
|
|
||||||
VIEW_REF_ID = "rhviewrefv1:sha256:bf776bcd26d940fafde1d650776a5505fb3fe8b5b068c351622bf2c42385629c"
|
|
||||||
VIEW_SCHEMA_DIGEST = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
||||||
|
|
||||||
|
|
||||||
def _golden() -> dict[str, Any]:
|
|
||||||
loaded = json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))
|
|
||||||
assert isinstance(loaded, dict)
|
|
||||||
return loaded
|
|
||||||
|
|
||||||
|
|
||||||
def _sha256(value: bytes) -> str:
|
|
||||||
return f"sha256:{hashlib.sha256(value).hexdigest()}"
|
|
||||||
|
|
||||||
|
|
||||||
def _reidentify(item: dict[str, Any], field: str, prefix: str) -> None:
|
|
||||||
payload = {key: value for key, value in item.items() if key != field}
|
|
||||||
item[field] = f"{prefix}{hashlib.sha256(canonical_json_bytes(payload)).hexdigest()}"
|
|
||||||
|
|
||||||
|
|
||||||
def _snapshot_and_foundation(
|
|
||||||
fixture: dict[str, Any] | None = None,
|
|
||||||
) -> tuple[DatasetSnapshotEnvelope, DataFoundationEnvelope]:
|
|
||||||
source = _golden() if fixture is None else fixture
|
|
||||||
return (
|
|
||||||
DatasetSnapshotEnvelope.from_dict(source["dataset_snapshot"]),
|
|
||||||
DataFoundationEnvelope.from_dict(source["data_foundation"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _definition(
|
|
||||||
*,
|
|
||||||
inputs: tuple[FactorInput, ...] | None = None,
|
|
||||||
**overrides: Any,
|
|
||||||
) -> FactorDefinition:
|
|
||||||
factor_inputs = inputs or (
|
|
||||||
FactorInput("market", VIEW_SCHEMA_DIGEST, ("close", "volume")),
|
|
||||||
)
|
|
||||||
arguments: dict[str, Any] = {
|
|
||||||
"factor_id": "alpha_005",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"formula": "correlation(close, volume, 10)",
|
|
||||||
"parameters": {},
|
|
||||||
"implementation_digest": "sha256:" + "1" * 64,
|
|
||||||
"input_schema_digest": factor_input_schema_digest(factor_inputs),
|
|
||||||
"inputs": factor_inputs,
|
|
||||||
"valid_from": "2026-01-01T00:00:00.000000Z",
|
|
||||||
"valid_until": "2027-01-01T00:00:00Z",
|
|
||||||
"warmup_sessions": 10,
|
|
||||||
"lag_sessions": 1,
|
|
||||||
"producer": ProducerIdentity("quant_engine", "1.0.0"),
|
|
||||||
"code_revision": "c" * 40,
|
|
||||||
}
|
|
||||||
arguments.update(overrides)
|
|
||||||
return FactorDefinition.create(**arguments)
|
|
||||||
|
|
||||||
|
|
||||||
def _golden_definition() -> FactorDefinition:
|
|
||||||
factor_input = FactorInput("market", VIEW_SCHEMA_DIGEST, ("close", "volume"))
|
|
||||||
return factor_definition_from_alpha158(
|
|
||||||
"alpha_005",
|
|
||||||
version="1.0.0",
|
|
||||||
parameters={},
|
|
||||||
inputs=(factor_input,),
|
|
||||||
implementation_digest="sha256:" + "1" * 64,
|
|
||||||
input_schema_digest=factor_input_schema_digest((factor_input,)),
|
|
||||||
valid_from="2026-01-01T00:00:00.000000Z",
|
|
||||||
valid_until="2027-01-01T00:00:00Z",
|
|
||||||
warmup_sessions=10,
|
|
||||||
lag_sessions=1,
|
|
||||||
producer=ProducerIdentity("quant_engine", "1.0.0"),
|
|
||||||
code_revision="c" * 40,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _factor_set_arguments(
|
|
||||||
*,
|
|
||||||
fixture: dict[str, Any] | None = None,
|
|
||||||
snapshot: DatasetSnapshotEnvelope | None = None,
|
|
||||||
foundation: DataFoundationEnvelope | None = None,
|
|
||||||
definition: FactorDefinition | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
source = _golden() if fixture is None else fixture
|
|
||||||
if snapshot is None or foundation is None:
|
|
||||||
parsed_snapshot, parsed_foundation = _snapshot_and_foundation(source)
|
|
||||||
snapshot = snapshot or parsed_snapshot
|
|
||||||
foundation = foundation or parsed_foundation
|
|
||||||
selected_definition = definition or _golden_definition()
|
|
||||||
output_schema_bytes = canonical_json_bytes(source["output_schema"])
|
|
||||||
output_content_bytes = canonical_json_bytes(source["output_content"])
|
|
||||||
artifact = OutputArtifactRef.create(
|
|
||||||
schema_digest=_sha256(output_schema_bytes),
|
|
||||||
content_digest=_sha256(output_content_bytes),
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"definitions": (selected_definition,),
|
|
||||||
"dataset_snapshot": snapshot,
|
|
||||||
"foundation": foundation,
|
|
||||||
"selected_view_ref_ids": (VIEW_REF_ID,),
|
|
||||||
"input_bindings": (
|
|
||||||
InputBinding(
|
|
||||||
selected_definition.definition_id,
|
|
||||||
"market",
|
|
||||||
VIEW_REF_ID,
|
|
||||||
VIEW_SCHEMA_DIGEST,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
"view_availability": (
|
|
||||||
ViewAvailability(VIEW_REF_ID, "2026-01-02T23:50:00Z", "sha256:" + "2" * 64),
|
|
||||||
),
|
|
||||||
"output_quality": OutputQuality(
|
|
||||||
"passed",
|
|
||||||
(OutputQualityCheck("finite_values", "passed", "sha256:" + "3" * 64),),
|
|
||||||
),
|
|
||||||
"output_coverage": OutputCoverage(
|
|
||||||
"complete",
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
"row",
|
|
||||||
"alpha_005.cn_a",
|
|
||||||
"sha256:" + "4" * 64,
|
|
||||||
),
|
|
||||||
"output_schema_bytes": output_schema_bytes,
|
|
||||||
"output_content_bytes": output_content_bytes,
|
|
||||||
"output_artifact_ref": artifact,
|
|
||||||
"availability_mode": AvailabilityMode.AS_AVAILABLE,
|
|
||||||
"evaluation_at": "2026-01-03T11:00:00Z",
|
|
||||||
"computed_at": "2026-01-03T10:15:00Z",
|
|
||||||
"artifact_available_at": "2026-01-03T10:20:00Z",
|
|
||||||
"producer": ProducerIdentity("quant_engine", "1.0.0"),
|
|
||||||
"code_revision": "c" * 40,
|
|
||||||
"actor": ActorIdentity("service", "factor_worker_v1"),
|
|
||||||
"correlation_id": "research_run_001",
|
|
||||||
"causation": Causation("foundation", foundation.foundation_id),
|
|
||||||
"evidence_scope": "synthetic_fixture",
|
|
||||||
"decision_eligible": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _factor_set(**overrides: Any) -> FactorSetRef:
|
|
||||||
arguments = _factor_set_arguments()
|
|
||||||
arguments.update(overrides)
|
|
||||||
return FactorSetRef.create(**arguments)
|
|
||||||
|
|
||||||
|
|
||||||
def _assert_error(
|
|
||||||
error: pytest.ExceptionInfo[FactorContractError],
|
|
||||||
code: ContractErrorCode,
|
|
||||||
path: str,
|
|
||||||
) -> None:
|
|
||||||
assert error.value.code is code
|
|
||||||
assert error.value.path == path
|
|
||||||
|
|
||||||
|
|
||||||
def _mutate_artifact_schema_binding(value: dict[str, Any]) -> None:
|
|
||||||
artifact = value["output_artifact_ref"]
|
|
||||||
artifact["schema_digest"] = "sha256:" + "0" * 64
|
|
||||||
_reidentify(artifact, "artifact_id", "rhfactoroutputv1:sha256:")
|
|
||||||
|
|
||||||
|
|
||||||
def test_golden_contracts_are_content_addressed_round_trippable_and_deeply_immutable() -> None:
|
|
||||||
fixture = _golden()
|
|
||||||
original_snapshot = copy.deepcopy(fixture["dataset_snapshot"])
|
|
||||||
original_foundation = copy.deepcopy(fixture["data_foundation"])
|
|
||||||
snapshot, foundation = _snapshot_and_foundation(fixture)
|
|
||||||
definition = _golden_definition()
|
|
||||||
factor_set = FactorSetRef.create(**_factor_set_arguments(fixture=fixture, snapshot=snapshot, foundation=foundation, definition=definition))
|
|
||||||
binding = LegacyFactorBinding.create(
|
|
||||||
definition=definition,
|
|
||||||
legacy_factor_id="factor:demo-momentum",
|
|
||||||
legacy_version="1.0.0",
|
|
||||||
legacy_definition_sha256="b" * 64,
|
|
||||||
legacy_dataset_schema_version="1.0.0",
|
|
||||||
canonical_input_schema_digest=definition.input_schema_digest,
|
|
||||||
correspondence_evidence_digest="sha256:" + "5" * 64,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert snapshot.pit_cutoff == "2026-01-02T07:01:00Z"
|
|
||||||
assert foundation.pit_cutoff == factor_set.pit_cutoff == "2026-01-03T00:00:00Z"
|
|
||||||
assert snapshot.pit_cutoff != foundation.pit_cutoff
|
|
||||||
assert definition.definition_id == fixture["expected"]["definition_id"]
|
|
||||||
assert definition.input_schema_digest == fixture["expected"]["input_schema_digest"]
|
|
||||||
assert factor_set.factor_set_id == fixture["expected"]["factor_set_id"]
|
|
||||||
assert factor_set.output_artifact_ref.artifact_id == fixture["expected"]["output_artifact_id"]
|
|
||||||
assert binding.binding_id == fixture["expected"]["legacy_binding_id"]
|
|
||||||
assert not definition.to_json().endswith("\n")
|
|
||||||
assert not factor_set.to_json().endswith("\n")
|
|
||||||
assert FactorDefinition.from_json(definition.to_json()) == definition
|
|
||||||
|
|
||||||
reparsed = FactorSetRef.from_json(
|
|
||||||
factor_set.to_json(),
|
|
||||||
definitions=(definition,),
|
|
||||||
dataset_snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
output_schema_bytes=canonical_json_bytes(fixture["output_schema"]),
|
|
||||||
output_content_bytes=canonical_json_bytes(fixture["output_content"]),
|
|
||||||
)
|
|
||||||
reference_only = FactorSetRef.from_json(
|
|
||||||
factor_set.to_json(),
|
|
||||||
definitions=(definition,),
|
|
||||||
dataset_snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
)
|
|
||||||
assert reparsed.factor_set_id == factor_set.factor_set_id
|
|
||||||
assert reparsed.payload_validation is PayloadValidation.PAYLOAD_REVALIDATED
|
|
||||||
assert reference_only.payload_validation is PayloadValidation.REFERENCE_ONLY
|
|
||||||
|
|
||||||
fixture["dataset_snapshot"]["descriptor"]["dataset"]["dimensions"].append("forbidden")
|
|
||||||
fixture["data_foundation"]["standardized_views"][0]["schema_digest"] = "sha256:" + "0" * 64
|
|
||||||
assert snapshot.to_dict() == original_snapshot
|
|
||||||
assert foundation.to_dict() == original_foundation
|
|
||||||
returned = snapshot.to_dict()
|
|
||||||
returned["descriptor"]["dataset"]["dimensions"].append("also_forbidden")
|
|
||||||
assert snapshot.to_dict() == original_snapshot
|
|
||||||
with pytest.raises(FrozenInstanceError):
|
|
||||||
snapshot.snapshot_id = "rhdsv1:sha256:" + "0" * 64 # type: ignore[misc]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("variant", ["whitespace", "key_order"])
|
|
||||||
def test_contract_decoders_reject_non_canonical_json(variant: str) -> None:
|
|
||||||
fixture = _golden()
|
|
||||||
snapshot, foundation = _snapshot_and_foundation(fixture)
|
|
||||||
definition = _golden_definition()
|
|
||||||
factor_set = FactorSetRef.create(
|
|
||||||
**_factor_set_arguments(
|
|
||||||
fixture=fixture,
|
|
||||||
snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
definition=definition,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
binding = LegacyFactorBinding.create(
|
|
||||||
definition=definition,
|
|
||||||
legacy_factor_id="factor:demo-momentum",
|
|
||||||
legacy_version="1.0.0",
|
|
||||||
legacy_definition_sha256="b" * 64,
|
|
||||||
legacy_dataset_schema_version="1.0.0",
|
|
||||||
canonical_input_schema_digest=definition.input_schema_digest,
|
|
||||||
correspondence_evidence_digest="sha256:" + "5" * 64,
|
|
||||||
)
|
|
||||||
|
|
||||||
def non_canonical(value: str) -> str:
|
|
||||||
if variant == "whitespace":
|
|
||||||
return value + "\n"
|
|
||||||
loaded = json.loads(value)
|
|
||||||
reversed_items = dict(reversed(tuple(loaded.items())))
|
|
||||||
return json.dumps(reversed_items, ensure_ascii=False, separators=(",", ":"))
|
|
||||||
|
|
||||||
decoders = (
|
|
||||||
lambda value: FactorDefinition.from_json(value),
|
|
||||||
lambda value: FactorSetRef.from_json(
|
|
||||||
value,
|
|
||||||
definitions=(definition,),
|
|
||||||
dataset_snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
),
|
|
||||||
lambda value: LegacyFactorBinding.from_json(value, definition=definition),
|
|
||||||
)
|
|
||||||
for decoder, encoded in zip(
|
|
||||||
decoders,
|
|
||||||
(definition.to_json(), factor_set.to_json(), binding.to_json()),
|
|
||||||
strict=True,
|
|
||||||
):
|
|
||||||
with pytest.raises(FactorContractError) as exc_info:
|
|
||||||
decoder(non_canonical(encoded))
|
|
||||||
assert exc_info.value.code is ContractErrorCode.INVALID_FORMAT
|
|
||||||
assert exc_info.value.path == "$"
|
|
||||||
|
|
||||||
|
|
||||||
def test_factor_definition_identity_is_order_independent_where_semantics_are_unordered() -> None:
|
|
||||||
first_input = FactorInput("prices", "sha256:" + "6" * 64, ("close",))
|
|
||||||
second_input = FactorInput("volumes", "sha256:" + "7" * 64, ("volume",))
|
|
||||||
inputs = (first_input, second_input)
|
|
||||||
parameters_a = {
|
|
||||||
"window": TypedParameter("integer", 10),
|
|
||||||
"weights": TypedParameter("json", {"fast": [1, 2], "slow": [3, 4]}),
|
|
||||||
}
|
|
||||||
parameters_b = {
|
|
||||||
"weights": TypedParameter("json", {"slow": [3, 4], "fast": [1, 2]}),
|
|
||||||
"window": TypedParameter("integer", 10),
|
|
||||||
}
|
|
||||||
first = _definition(
|
|
||||||
inputs=inputs,
|
|
||||||
parameters=parameters_a,
|
|
||||||
input_schema_digest=factor_input_schema_digest(inputs),
|
|
||||||
)
|
|
||||||
second = _definition(
|
|
||||||
inputs=tuple(reversed(inputs)),
|
|
||||||
parameters=parameters_b,
|
|
||||||
input_schema_digest=factor_input_schema_digest(tuple(reversed(inputs))),
|
|
||||||
)
|
|
||||||
assert first.definition_id == second.definition_id
|
|
||||||
assert first.to_json() == second.to_json()
|
|
||||||
|
|
||||||
semantic_changes = (
|
|
||||||
_definition(factor_id="alpha_006"),
|
|
||||||
_definition(version="1.0.1"),
|
|
||||||
_definition(formula="correlation(close, volume, 11)"),
|
|
||||||
_definition(parameters={"window": TypedParameter("integer", 10)}),
|
|
||||||
_definition(implementation_digest="sha256:" + "9" * 64),
|
|
||||||
_definition(valid_until="2027-01-02T00:00:00Z"),
|
|
||||||
_definition(warmup_sessions=11),
|
|
||||||
_definition(lag_sessions=2),
|
|
||||||
_definition(producer=ProducerIdentity("quant_engine", "1.0.1")),
|
|
||||||
_definition(code_revision="d" * 40),
|
|
||||||
)
|
|
||||||
assert all(changed.definition_id != _golden_definition().definition_id for changed in semantic_changes)
|
|
||||||
assert len({changed.definition_id for changed in semantic_changes}) == len(semantic_changes)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parameter_types_decimal_profile_and_detached_nested_values_are_strict() -> None:
|
|
||||||
nested = {"ordered": [1, {"flag": True}]}
|
|
||||||
parameter = TypedParameter("json", nested)
|
|
||||||
nested["ordered"].append(2)
|
|
||||||
definition = _definition(parameters={"payload": parameter})
|
|
||||||
assert definition.to_dict()["parameters"]["payload"]["value"] == {
|
|
||||||
"ordered": [1, {"flag": True}]
|
|
||||||
}
|
|
||||||
integer_definition = _definition(parameters={"value": TypedParameter("integer", 1)})
|
|
||||||
string_definition = _definition(parameters={"value": TypedParameter("string", "1")})
|
|
||||||
assert integer_definition.definition_id != string_definition.definition_id
|
|
||||||
|
|
||||||
for parameter_type, value, code in (
|
|
||||||
("decimal", "1.0", ContractErrorCode.INVALID_FORMAT),
|
|
||||||
("decimal", "1e3", ContractErrorCode.INVALID_FORMAT),
|
|
||||||
("decimal", "-0", ContractErrorCode.INVALID_FORMAT),
|
|
||||||
("integer", True, ContractErrorCode.TYPE_ERROR),
|
|
||||||
("json", 1.5, ContractErrorCode.TYPE_ERROR),
|
|
||||||
("json", {"é": "bad-key"}, ContractErrorCode.INVALID_FORMAT),
|
|
||||||
("json", 9_007_199_254_740_992, ContractErrorCode.INVALID_VALUE),
|
|
||||||
):
|
|
||||||
with pytest.raises(FactorContractError) as error:
|
|
||||||
TypedParameter(parameter_type, value)
|
|
||||||
assert error.value.code is code
|
|
||||||
assert TypedParameter("decimal", "10.25").to_dict()["value"] == "10.25"
|
|
||||||
|
|
||||||
|
|
||||||
def test_catalog_rejects_duplicate_and_overlapping_logical_validity_but_allows_adjacency() -> None:
|
|
||||||
base = _golden_definition()
|
|
||||||
adjacent = _definition(valid_from="2027-01-01T00:00:00Z", valid_until="2028-01-01T00:00:00Z")
|
|
||||||
assert len(validate_factor_catalog((adjacent, base))) == 2
|
|
||||||
with pytest.raises(FactorContractError) as duplicate:
|
|
||||||
validate_factor_catalog((base, base))
|
|
||||||
_assert_error(duplicate, ContractErrorCode.INVALID_VALUE, "$.definitions")
|
|
||||||
overlapping = _definition(valid_from="2026-06-01T00:00:00Z", valid_until="2028-01-01T00:00:00Z")
|
|
||||||
with pytest.raises(FactorContractError) as overlap:
|
|
||||||
validate_factor_catalog((base, overlapping))
|
|
||||||
_assert_error(overlap, ContractErrorCode.TIME_ORDER_VIOLATION, "$.definitions")
|
|
||||||
|
|
||||||
|
|
||||||
def test_upstream_contracts_reject_unknown_fields_identity_forgery_and_unqualified_input() -> None:
|
|
||||||
unknown = _golden()["dataset_snapshot"]
|
|
||||||
unknown["provider"] = "forbidden"
|
|
||||||
with pytest.raises(FactorContractError) as unknown_error:
|
|
||||||
DatasetSnapshotEnvelope.from_dict(unknown)
|
|
||||||
_assert_error(unknown_error, ContractErrorCode.UNKNOWN_FIELD, "$.provider")
|
|
||||||
|
|
||||||
forged = _golden()["data_foundation"]
|
|
||||||
forged["standardized_views"][0]["schema_digest"] = "sha256:" + "0" * 64
|
|
||||||
with pytest.raises(FactorContractError) as forged_error:
|
|
||||||
DataFoundationEnvelope.from_dict(forged)
|
|
||||||
assert forged_error.value.code is ContractErrorCode.IDENTITY_MISMATCH
|
|
||||||
assert forged_error.value.path.endswith("view_ref_id")
|
|
||||||
|
|
||||||
rejected_source = _golden()
|
|
||||||
rejected_source["dataset_snapshot"]["descriptor"]["qualification"]["status"] = "rejected"
|
|
||||||
_reidentify(rejected_source["dataset_snapshot"], "snapshot_id", "rhdsv1:sha256:")
|
|
||||||
rejected_snapshot = DatasetSnapshotEnvelope.from_dict(rejected_source["dataset_snapshot"])
|
|
||||||
_, foundation = _snapshot_and_foundation()
|
|
||||||
with pytest.raises(FactorContractError) as rejected_error:
|
|
||||||
FactorSetRef.create(
|
|
||||||
**_factor_set_arguments(snapshot=rejected_snapshot, foundation=foundation)
|
|
||||||
)
|
|
||||||
_assert_error(
|
|
||||||
rejected_error,
|
|
||||||
ContractErrorCode.QUALIFICATION_REJECTED,
|
|
||||||
"$.dataset_snapshot.descriptor.qualification",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_foundation_rejects_future_knowledge_and_per_view_calendar_borrowing() -> None:
|
|
||||||
future = _golden()["data_foundation"]
|
|
||||||
action = future["corporate_action_revisions"][0]
|
|
||||||
old_action_id = action["action_revision_id"]
|
|
||||||
action["knowledge_time"] = "2026-01-03T00:00:01Z"
|
|
||||||
_reidentify(action, "action_revision_id", "rhcav1:sha256:")
|
|
||||||
future["standardized_views"][0]["corporate_action_revision_ids"] = [action["action_revision_id"]]
|
|
||||||
lineage = next(item for item in future["revision_lineage"] if item["revision_id"] == old_action_id)
|
|
||||||
lineage["revision_id"] = action["action_revision_id"]
|
|
||||||
lineage["knowledge_time"] = action["knowledge_time"]
|
|
||||||
_reidentify(future["standardized_views"][0], "view_ref_id", "rhviewrefv1:sha256:")
|
|
||||||
_reidentify(future, "foundation_id", "rhdfv1:sha256:")
|
|
||||||
with pytest.raises(FactorContractError) as future_error:
|
|
||||||
DataFoundationEnvelope.from_dict(future)
|
|
||||||
_assert_error(
|
|
||||||
future_error,
|
|
||||||
ContractErrorCode.TIME_ORDER_VIOLATION,
|
|
||||||
"$.revision_lineage.knowledge_time",
|
|
||||||
)
|
|
||||||
|
|
||||||
uncovered = _golden()["data_foundation"]
|
|
||||||
original_route_id = uncovered["instrument_routes"][0]["route_revision_id"]
|
|
||||||
second_calendar = copy.deepcopy(uncovered["trading_calendar_revisions"][0])
|
|
||||||
second_calendar["calendar_id"] = "rhcalendar:99990000111122223333444455556666"
|
|
||||||
_reidentify(second_calendar, "calendar_revision_id", "rhcalv1:sha256:")
|
|
||||||
uncovered["trading_calendar_revisions"].append(second_calendar)
|
|
||||||
route = uncovered["instrument_routes"][0]
|
|
||||||
route["calendar_id"] = second_calendar["calendar_id"]
|
|
||||||
_reidentify(route, "route_revision_id", "rhroutev1:sha256:")
|
|
||||||
route_lineage = next(item for item in uncovered["revision_lineage"] if item["revision_id"] == original_route_id)
|
|
||||||
route_lineage["revision_id"] = route["route_revision_id"]
|
|
||||||
uncovered["revision_lineage"].append(
|
|
||||||
{
|
|
||||||
"revision_kind": "trading_calendar",
|
|
||||||
"revision_id": second_calendar["calendar_revision_id"],
|
|
||||||
"revision_number": 1,
|
|
||||||
"knowledge_time": second_calendar["knowledge_time"],
|
|
||||||
"evidence_digest": second_calendar["evidence_digest"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
view = uncovered["standardized_views"][0]
|
|
||||||
view["instrument_route_revision_ids"] = [route["route_revision_id"]]
|
|
||||||
_reidentify(view, "view_ref_id", "rhviewrefv1:sha256:")
|
|
||||||
_reidentify(uncovered, "foundation_id", "rhdfv1:sha256:")
|
|
||||||
with pytest.raises(FactorContractError) as calendar_error:
|
|
||||||
DataFoundationEnvelope.from_dict(uncovered)
|
|
||||||
assert calendar_error.value.code is ContractErrorCode.INPUT_CLOSURE_VIOLATION
|
|
||||||
assert "selected route calendar" in calendar_error.value.detail
|
|
||||||
|
|
||||||
|
|
||||||
def _replay_fixture() -> dict[str, Any]:
|
|
||||||
fixture = _golden()
|
|
||||||
snapshot = fixture["dataset_snapshot"]
|
|
||||||
snapshot["descriptor"]["published_at"] = "2026-01-04T00:00:00Z"
|
|
||||||
_reidentify(snapshot, "snapshot_id", "rhdsv1:sha256:")
|
|
||||||
foundation = fixture["data_foundation"]
|
|
||||||
foundation["dataset_snapshot_id"] = snapshot["snapshot_id"]
|
|
||||||
for view in foundation["standardized_views"]:
|
|
||||||
view["dataset_snapshot_id"] = snapshot["snapshot_id"]
|
|
||||||
_reidentify(view, "view_ref_id", "rhviewrefv1:sha256:")
|
|
||||||
_reidentify(foundation, "foundation_id", "rhdfv1:sha256:")
|
|
||||||
return fixture
|
|
||||||
|
|
||||||
|
|
||||||
def test_as_available_and_retrospective_replay_keep_distinct_time_claims() -> None:
|
|
||||||
as_available = _factor_set()
|
|
||||||
assert as_available.historical_availability is HistoricalAvailability.DECLARED_AS_AVAILABLE
|
|
||||||
|
|
||||||
replay_source = _replay_fixture()
|
|
||||||
snapshot, foundation = _snapshot_and_foundation(replay_source)
|
|
||||||
replay_view_id = next(iter(foundation.views))
|
|
||||||
arguments = _factor_set_arguments(
|
|
||||||
fixture=replay_source,
|
|
||||||
snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
)
|
|
||||||
arguments.update(
|
|
||||||
selected_view_ref_ids=(replay_view_id,),
|
|
||||||
input_bindings=(
|
|
||||||
InputBinding(
|
|
||||||
arguments["definitions"][0].definition_id,
|
|
||||||
"market",
|
|
||||||
replay_view_id,
|
|
||||||
VIEW_SCHEMA_DIGEST,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
view_availability=(
|
|
||||||
ViewAvailability(replay_view_id, "2026-01-04T00:10:00Z", "sha256:" + "2" * 64),
|
|
||||||
),
|
|
||||||
availability_mode=AvailabilityMode.RETROSPECTIVE_REPLAY,
|
|
||||||
computed_at="2026-01-04T00:20:00Z",
|
|
||||||
artifact_available_at="2026-01-04T00:25:00Z",
|
|
||||||
causation=Causation("foundation", foundation.foundation_id),
|
|
||||||
)
|
|
||||||
replay = FactorSetRef.create(**arguments)
|
|
||||||
assert replay.evaluation_at == "2026-01-03T11:00:00Z"
|
|
||||||
assert replay.computed_at == "2026-01-04T00:20:00Z"
|
|
||||||
assert replay.historical_availability is HistoricalAvailability.NOT_ESTABLISHED
|
|
||||||
|
|
||||||
replay_source_args = _factor_set_arguments(
|
|
||||||
fixture=replay_source,
|
|
||||||
snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
)
|
|
||||||
replay_source_args.update(
|
|
||||||
selected_view_ref_ids=(replay_view_id,),
|
|
||||||
input_bindings=(
|
|
||||||
InputBinding(
|
|
||||||
replay_source_args["definitions"][0].definition_id,
|
|
||||||
"market",
|
|
||||||
replay_view_id,
|
|
||||||
VIEW_SCHEMA_DIGEST,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
view_availability=(
|
|
||||||
ViewAvailability(replay_view_id, "2026-01-02T23:50:00Z", "sha256:" + "2" * 64),
|
|
||||||
),
|
|
||||||
causation=Causation("foundation", foundation.foundation_id),
|
|
||||||
)
|
|
||||||
with pytest.raises(FactorContractError) as late_publication:
|
|
||||||
FactorSetRef.create(**replay_source_args)
|
|
||||||
_assert_error(
|
|
||||||
late_publication,
|
|
||||||
ContractErrorCode.TIME_ORDER_VIOLATION,
|
|
||||||
"$.dataset_snapshot.descriptor.published_at",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("overrides", "path"),
|
|
||||||
[
|
|
||||||
({"view_availability": (ViewAvailability(VIEW_REF_ID, "2026-01-03T00:00:01Z", "sha256:" + "2" * 64),)}, "$.view_availability"),
|
|
||||||
({"computed_at": "2026-01-02T23:40:00Z"}, "$.computed_at"),
|
|
||||||
({"artifact_available_at": "2026-01-03T10:14:00Z"}, "$.artifact_available_at"),
|
|
||||||
({"artifact_available_at": "2026-01-03T11:00:01Z"}, "$.artifact_available_at"),
|
|
||||||
({"evaluation_at": "2026-01-03T11:00:00"}, "$.evaluation_at"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_as_available_time_failures_are_typed(overrides: dict[str, Any], path: str) -> None:
|
|
||||||
with pytest.raises(FactorContractError) as error:
|
|
||||||
_factor_set(**overrides)
|
|
||||||
assert error.value.code in {
|
|
||||||
ContractErrorCode.INVALID_FORMAT,
|
|
||||||
ContractErrorCode.TIME_ORDER_VIOLATION,
|
|
||||||
}
|
|
||||||
assert error.value.path == path
|
|
||||||
|
|
||||||
|
|
||||||
def test_replay_rejects_backdating_and_historical_availability_promotion() -> None:
|
|
||||||
source = _replay_fixture()
|
|
||||||
snapshot, foundation = _snapshot_and_foundation(source)
|
|
||||||
view_id = next(iter(foundation.views))
|
|
||||||
arguments = _factor_set_arguments(fixture=source, snapshot=snapshot, foundation=foundation)
|
|
||||||
definition = arguments["definitions"][0]
|
|
||||||
arguments.update(
|
|
||||||
selected_view_ref_ids=(view_id,),
|
|
||||||
input_bindings=(InputBinding(definition.definition_id, "market", view_id, VIEW_SCHEMA_DIGEST),),
|
|
||||||
view_availability=(ViewAvailability(view_id, "2026-01-04T00:10:00Z", "sha256:" + "2" * 64),),
|
|
||||||
availability_mode=AvailabilityMode.RETROSPECTIVE_REPLAY,
|
|
||||||
computed_at="2026-01-04T00:20:00Z",
|
|
||||||
artifact_available_at="2026-01-04T00:25:00Z",
|
|
||||||
causation=Causation("foundation", foundation.foundation_id),
|
|
||||||
)
|
|
||||||
replay = FactorSetRef.create(**arguments)
|
|
||||||
promoted = replay.to_dict()
|
|
||||||
promoted["historical_availability"] = "declared_as_available"
|
|
||||||
_reidentify(promoted, "factor_set_id", "rhfactorsetv1:sha256:")
|
|
||||||
with pytest.raises(FactorContractError) as promotion_error:
|
|
||||||
FactorSetRef.from_dict(
|
|
||||||
promoted,
|
|
||||||
definitions=(definition,),
|
|
||||||
dataset_snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
)
|
|
||||||
_assert_error(
|
|
||||||
promotion_error,
|
|
||||||
ContractErrorCode.READINESS_ESCALATION,
|
|
||||||
"$.historical_availability",
|
|
||||||
)
|
|
||||||
arguments["computed_at"] = "2026-01-03T11:30:00Z"
|
|
||||||
with pytest.raises(FactorContractError) as backdated_error:
|
|
||||||
FactorSetRef.create(**arguments)
|
|
||||||
_assert_error(backdated_error, ContractErrorCode.TIME_ORDER_VIOLATION, "$.computed_at")
|
|
||||||
|
|
||||||
|
|
||||||
def _multi_view_fixture() -> tuple[dict[str, Any], str]:
|
|
||||||
fixture = _golden()
|
|
||||||
foundation = fixture["data_foundation"]
|
|
||||||
second = copy.deepcopy(foundation["standardized_views"][0])
|
|
||||||
second["view_id"] = "rhview:11111111222222223333333344444444"
|
|
||||||
second["schema_digest"] = "sha256:" + "6" * 64
|
|
||||||
second["content_digest"] = "sha256:" + "7" * 64
|
|
||||||
second["transformation_digest"] = "sha256:" + "8" * 64
|
|
||||||
_reidentify(second, "view_ref_id", "rhviewrefv1:sha256:")
|
|
||||||
foundation["standardized_views"].append(second)
|
|
||||||
_reidentify(foundation, "foundation_id", "rhdfv1:sha256:")
|
|
||||||
return fixture, second["view_ref_id"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_multi_input_mapping_requires_exact_consumption_closure_and_is_order_independent() -> None:
|
|
||||||
fixture, second_view_id = _multi_view_fixture()
|
|
||||||
snapshot, foundation = _snapshot_and_foundation(fixture)
|
|
||||||
inputs = (
|
|
||||||
FactorInput("prices", VIEW_SCHEMA_DIGEST, ("close",)),
|
|
||||||
FactorInput("volumes", "sha256:" + "6" * 64, ("volume",)),
|
|
||||||
)
|
|
||||||
definition = _definition(
|
|
||||||
inputs=inputs,
|
|
||||||
formula="correlation(close, volume, 10)",
|
|
||||||
input_schema_digest=factor_input_schema_digest(inputs),
|
|
||||||
)
|
|
||||||
first_binding = InputBinding(definition.definition_id, "prices", VIEW_REF_ID, VIEW_SCHEMA_DIGEST)
|
|
||||||
second_binding = InputBinding(definition.definition_id, "volumes", second_view_id, "sha256:" + "6" * 64)
|
|
||||||
first_availability = ViewAvailability(VIEW_REF_ID, "2026-01-02T23:40:00Z", "sha256:" + "2" * 64)
|
|
||||||
second_availability = ViewAvailability(second_view_id, "2026-01-02T23:50:00Z", "sha256:" + "6" * 64)
|
|
||||||
base = _factor_set_arguments(fixture=fixture, snapshot=snapshot, foundation=foundation, definition=definition)
|
|
||||||
base.update(
|
|
||||||
selected_view_ref_ids=(VIEW_REF_ID, second_view_id),
|
|
||||||
input_bindings=(first_binding, second_binding),
|
|
||||||
view_availability=(first_availability, second_availability),
|
|
||||||
causation=Causation("foundation", foundation.foundation_id),
|
|
||||||
)
|
|
||||||
first = FactorSetRef.create(**base)
|
|
||||||
reordered = dict(base)
|
|
||||||
reordered.update(
|
|
||||||
selected_view_ref_ids=(second_view_id, VIEW_REF_ID),
|
|
||||||
input_bindings=(second_binding, first_binding),
|
|
||||||
view_availability=(second_availability, first_availability),
|
|
||||||
)
|
|
||||||
assert FactorSetRef.create(**reordered).factor_set_id == first.factor_set_id
|
|
||||||
|
|
||||||
for invalid_bindings, invalid_views in (
|
|
||||||
((first_binding,), (VIEW_REF_ID, second_view_id)),
|
|
||||||
((first_binding, second_binding), (VIEW_REF_ID,)),
|
|
||||||
((first_binding, second_binding), (VIEW_REF_ID, second_view_id, VIEW_REF_ID)),
|
|
||||||
):
|
|
||||||
invalid = dict(base)
|
|
||||||
invalid.update(input_bindings=invalid_bindings, selected_view_ref_ids=invalid_views)
|
|
||||||
with pytest.raises(FactorContractError) as error:
|
|
||||||
FactorSetRef.create(**invalid)
|
|
||||||
assert error.value.code in {
|
|
||||||
ContractErrorCode.INPUT_CLOSURE_VIOLATION,
|
|
||||||
ContractErrorCode.INVALID_VALUE,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("mutate", "code", "path"),
|
|
||||||
[
|
|
||||||
(lambda value: value["producer"].pop("id"), ContractErrorCode.MISSING_FIELD, "$.producer.id"),
|
|
||||||
(lambda value: value["producer"].pop("version"), ContractErrorCode.MISSING_FIELD, "$.producer.version"),
|
|
||||||
(lambda value: value["producer"].update(id="other_engine"), ContractErrorCode.LINEAGE_VIOLATION, "$.producer.id"),
|
|
||||||
(lambda value: value["producer"].update(version="latest"), ContractErrorCode.INVALID_FORMAT, "$.producer.version"),
|
|
||||||
(lambda value: value.update(code_revision="bad"), ContractErrorCode.INVALID_FORMAT, "$.code_revision"),
|
|
||||||
(lambda value: value["actor"].pop("kind"), ContractErrorCode.MISSING_FIELD, "$.actor.kind"),
|
|
||||||
(lambda value: value["actor"].pop("id"), ContractErrorCode.MISSING_FIELD, "$.actor.id"),
|
|
||||||
(lambda value: value["actor"].update(kind="robot"), ContractErrorCode.INVALID_VALUE, "$.actor.kind"),
|
|
||||||
(lambda value: value["actor"].update(id="latest"), ContractErrorCode.INVALID_VALUE, "$.actor.id"),
|
|
||||||
(lambda value: value.pop("correlation_id"), ContractErrorCode.MISSING_FIELD, "$.correlation_id"),
|
|
||||||
(lambda value: value.update(correlation_id="latest"), ContractErrorCode.INVALID_VALUE, "$.correlation_id"),
|
|
||||||
(lambda value: value["causation"].pop("kind"), ContractErrorCode.MISSING_FIELD, "$.causation.kind"),
|
|
||||||
(lambda value: value["causation"].update(kind="run"), ContractErrorCode.INVALID_VALUE, "$.causation.kind"),
|
|
||||||
(lambda value: value["causation"].update(id="rhdfv1:sha256:" + "0" * 64), ContractErrorCode.LINEAGE_VIOLATION, "$.causation.id"),
|
|
||||||
(lambda value: value.pop("output_artifact_ref"), ContractErrorCode.MISSING_FIELD, "$.output_artifact_ref"),
|
|
||||||
(lambda value: value["output_artifact_ref"].update(artifact_id="rhfactoroutputv1:sha256:" + "0" * 64), ContractErrorCode.IDENTITY_MISMATCH, "$.output_artifact_ref.artifact_id"),
|
|
||||||
(_mutate_artifact_schema_binding, ContractErrorCode.ARTIFACT_MISMATCH, "$.output_artifact_ref"),
|
|
||||||
(lambda value: value.update(availability_mode="implicit_fallback"), ContractErrorCode.INVALID_VALUE, "$.availability_mode"),
|
|
||||||
(lambda value: value.pop("computed_at"), ContractErrorCode.MISSING_FIELD, "$.computed_at"),
|
|
||||||
(lambda value: value.update(decision_eligible=True), ContractErrorCode.READINESS_ESCALATION, "$.decision_eligible"),
|
|
||||||
(lambda value: value.update(evidence_scope="real_data"), ContractErrorCode.READINESS_ESCALATION, "$.evidence_scope"),
|
|
||||||
(lambda value: value["upstream_evidence"].update(qualification_evidence_digest="sha256:" + "0" * 64), ContractErrorCode.IDENTITY_MISMATCH, "$.upstream_evidence"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_lineage_artifact_and_readiness_fields_have_independent_typed_negatives(
|
|
||||||
mutate: Callable[[dict[str, Any]], Any],
|
|
||||||
code: ContractErrorCode,
|
|
||||||
path: str,
|
|
||||||
) -> None:
|
|
||||||
factor_set = _factor_set()
|
|
||||||
value = factor_set.to_dict()
|
|
||||||
mutate(value)
|
|
||||||
if "factor_set_id" in value:
|
|
||||||
_reidentify(value, "factor_set_id", "rhfactorsetv1:sha256:")
|
|
||||||
snapshot, foundation = _snapshot_and_foundation()
|
|
||||||
with pytest.raises(FactorContractError) as error:
|
|
||||||
FactorSetRef.from_dict(
|
|
||||||
value,
|
|
||||||
definitions=(_golden_definition(),),
|
|
||||||
dataset_snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
)
|
|
||||||
_assert_error(error, code, path)
|
|
||||||
|
|
||||||
|
|
||||||
def test_output_schema_content_bytes_cannot_be_swapped_or_forged() -> None:
|
|
||||||
factor_set = _factor_set()
|
|
||||||
fixture = _golden()
|
|
||||||
snapshot, foundation = _snapshot_and_foundation()
|
|
||||||
schema_bytes = canonical_json_bytes(fixture["output_schema"])
|
|
||||||
content_bytes = canonical_json_bytes(fixture["output_content"])
|
|
||||||
with pytest.raises(FactorContractError) as swapped:
|
|
||||||
FactorSetRef.from_dict(
|
|
||||||
factor_set.to_dict(),
|
|
||||||
definitions=(_golden_definition(),),
|
|
||||||
dataset_snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
output_schema_bytes=content_bytes,
|
|
||||||
output_content_bytes=schema_bytes,
|
|
||||||
)
|
|
||||||
_assert_error(swapped, ContractErrorCode.ARTIFACT_MISMATCH, "$.output_artifact_ref")
|
|
||||||
with pytest.raises(FactorContractError) as noncanonical:
|
|
||||||
FactorSetRef.create(
|
|
||||||
**{
|
|
||||||
**_factor_set_arguments(),
|
|
||||||
"output_schema_bytes": json.dumps(fixture["output_schema"], indent=2).encode(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
_assert_error(noncanonical, ContractErrorCode.INVALID_FORMAT, "$.output_schema_bytes")
|
|
||||||
|
|
||||||
|
|
||||||
def test_unsuccessful_output_quality_or_coverage_cannot_form_a_factor_set() -> None:
|
|
||||||
with pytest.raises(FactorContractError) as failed_quality:
|
|
||||||
_factor_set(
|
|
||||||
output_quality=OutputQuality(
|
|
||||||
"failed",
|
|
||||||
(OutputQualityCheck("finite_values", "failed", "sha256:" + "3" * 64),),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
_assert_error(failed_quality, ContractErrorCode.INVALID_VALUE, "$.output_quality")
|
|
||||||
|
|
||||||
for coverage in (
|
|
||||||
OutputCoverage("incomplete", 2, 1, "row", "alpha_005.cn_a", "sha256:" + "4" * 64),
|
|
||||||
OutputCoverage("complete", 2, 1, "row", "alpha_005.cn_a", "sha256:" + "4" * 64),
|
|
||||||
):
|
|
||||||
with pytest.raises(FactorContractError) as incomplete:
|
|
||||||
_factor_set(output_coverage=coverage)
|
|
||||||
_assert_error(incomplete, ContractErrorCode.INVALID_VALUE, "$.output_coverage")
|
|
||||||
|
|
||||||
|
|
||||||
def test_external_snapshot_definition_and_view_references_cannot_be_substituted() -> None:
|
|
||||||
factor_set = _factor_set()
|
|
||||||
snapshot, foundation = _snapshot_and_foundation()
|
|
||||||
value = factor_set.to_dict()
|
|
||||||
value["dataset_snapshot_id"] = "rhdsv1:sha256:" + "0" * 64
|
|
||||||
_reidentify(value, "factor_set_id", "rhfactorsetv1:sha256:")
|
|
||||||
with pytest.raises(FactorContractError) as snapshot_error:
|
|
||||||
FactorSetRef.from_dict(
|
|
||||||
value,
|
|
||||||
definitions=(_golden_definition(),),
|
|
||||||
dataset_snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
)
|
|
||||||
_assert_error(
|
|
||||||
snapshot_error,
|
|
||||||
ContractErrorCode.INPUT_CLOSURE_VIOLATION,
|
|
||||||
"$.dataset_snapshot_id",
|
|
||||||
)
|
|
||||||
|
|
||||||
value = factor_set.to_dict()
|
|
||||||
value["definition_ids"] = ["rhfactorv1:sha256:" + "0" * 64]
|
|
||||||
_reidentify(value, "factor_set_id", "rhfactorsetv1:sha256:")
|
|
||||||
with pytest.raises(FactorContractError) as definition_error:
|
|
||||||
FactorSetRef.from_dict(
|
|
||||||
value,
|
|
||||||
definitions=(_golden_definition(),),
|
|
||||||
dataset_snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
)
|
|
||||||
_assert_error(
|
|
||||||
definition_error,
|
|
||||||
ContractErrorCode.INPUT_CLOSURE_VIOLATION,
|
|
||||||
"$.definition_ids",
|
|
||||||
)
|
|
||||||
|
|
||||||
arguments = _factor_set_arguments()
|
|
||||||
arguments["selected_view_ref_ids"] = ("rhviewrefv1:sha256:" + "0" * 64,)
|
|
||||||
with pytest.raises(FactorContractError) as view_error:
|
|
||||||
FactorSetRef.create(**arguments)
|
|
||||||
_assert_error(
|
|
||||||
view_error,
|
|
||||||
ContractErrorCode.INPUT_CLOSURE_VIOLATION,
|
|
||||||
"$.selected_view_ref_ids",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_factor_set_parent_requires_exact_identity_and_correlation() -> None:
|
|
||||||
parent = _factor_set()
|
|
||||||
child_arguments = _factor_set_arguments()
|
|
||||||
child_arguments.update(
|
|
||||||
output_content_bytes=canonical_json_bytes({"rows": [{"value": "0.250"}]}),
|
|
||||||
causation=Causation("factor_set", parent.factor_set_id),
|
|
||||||
parent=parent,
|
|
||||||
)
|
|
||||||
child_arguments["output_artifact_ref"] = OutputArtifactRef.create(
|
|
||||||
schema_digest=_sha256(child_arguments["output_schema_bytes"]),
|
|
||||||
content_digest=_sha256(child_arguments["output_content_bytes"]),
|
|
||||||
)
|
|
||||||
child = FactorSetRef.create(**child_arguments)
|
|
||||||
assert child.causation.id == parent.factor_set_id
|
|
||||||
missing_parent = child.to_dict()
|
|
||||||
snapshot, foundation = _snapshot_and_foundation()
|
|
||||||
with pytest.raises(FactorContractError) as missing_error:
|
|
||||||
FactorSetRef.from_dict(
|
|
||||||
missing_parent,
|
|
||||||
definitions=(_golden_definition(),),
|
|
||||||
dataset_snapshot=snapshot,
|
|
||||||
foundation=foundation,
|
|
||||||
)
|
|
||||||
_assert_error(missing_error, ContractErrorCode.LINEAGE_VIOLATION, "$.causation")
|
|
||||||
wrong_correlation = dict(child_arguments)
|
|
||||||
wrong_correlation["correlation_id"] = "different_run"
|
|
||||||
with pytest.raises(FactorContractError) as correlation_error:
|
|
||||||
FactorSetRef.create(**wrong_correlation)
|
|
||||||
_assert_error(correlation_error, ContractErrorCode.LINEAGE_VIOLATION, "$.correlation_id")
|
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_bridge_is_explicit_lossy_and_preserves_all_four_historical_fields() -> None:
|
|
||||||
definition = _golden_definition()
|
|
||||||
legacy = FactorVersion(
|
|
||||||
factor_id="factor:demo-momentum",
|
|
||||||
version="1.0.0",
|
|
||||||
definition_sha256="b" * 64,
|
|
||||||
dataset_schema_version="1.0.0",
|
|
||||||
)
|
|
||||||
binding = LegacyFactorBinding.create(
|
|
||||||
definition=definition,
|
|
||||||
legacy_factor_id=legacy.factor_id,
|
|
||||||
legacy_version=legacy.version,
|
|
||||||
legacy_definition_sha256=legacy.definition_sha256,
|
|
||||||
legacy_dataset_schema_version=legacy.dataset_schema_version,
|
|
||||||
canonical_input_schema_digest=definition.input_schema_digest,
|
|
||||||
correspondence_evidence_digest="sha256:" + "5" * 64,
|
|
||||||
)
|
|
||||||
assert bind_legacy_factor(legacy, definition, binding) is definition
|
|
||||||
assert project_legacy_factor(definition, binding) == legacy
|
|
||||||
assert legacy.version_id == "factor:demo-momentum@1.0.0"
|
|
||||||
assert legacy.definition_sha256 != definition.definition_id.rsplit(":", maxsplit=1)[-1]
|
|
||||||
assert LegacyFactorBinding.from_json(binding.to_json(), definition=definition) == binding
|
|
||||||
|
|
||||||
mismatched = FactorVersion(
|
|
||||||
factor_id="factor:different",
|
|
||||||
version=legacy.version,
|
|
||||||
definition_sha256=legacy.definition_sha256,
|
|
||||||
dataset_schema_version=legacy.dataset_schema_version,
|
|
||||||
)
|
|
||||||
with pytest.raises(FactorContractError) as mismatch_error:
|
|
||||||
bind_legacy_factor(mismatched, definition, binding)
|
|
||||||
_assert_error(mismatch_error, ContractErrorCode.LEGACY_BINDING_MISMATCH, "$.binding")
|
|
||||||
|
|
||||||
|
|
||||||
def test_bare_legacy_factor_or_id_cannot_enter_factor_set_contract() -> None:
|
|
||||||
legacy = FactorVersion("factor:demo-momentum", "1.0.0", "b" * 64, "1.0.0")
|
|
||||||
arguments = _factor_set_arguments()
|
|
||||||
arguments["definitions"] = (legacy,)
|
|
||||||
with pytest.raises(FactorContractError) as legacy_error:
|
|
||||||
FactorSetRef.create(**arguments)
|
|
||||||
_assert_error(legacy_error, ContractErrorCode.TYPE_ERROR, "$.definitions[0]")
|
|
||||||
arguments["definitions"] = (legacy.version_id,)
|
|
||||||
with pytest.raises(FactorContractError) as id_error:
|
|
||||||
FactorSetRef.create(**arguments)
|
|
||||||
_assert_error(id_error, ContractErrorCode.TYPE_ERROR, "$.definitions[0]")
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
"""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(),
|
|
||||||
)
|
|
||||||
@@ -1,408 +0,0 @@
|
|||||||
version = 1
|
|
||||||
revision = 3
|
|
||||||
requires-python = "==3.13.*"
|
|
||||||
resolution-markers = [
|
|
||||||
"sys_platform == 'win32'",
|
|
||||||
"sys_platform == 'emscripten'",
|
|
||||||
"sys_platform != 'emscripten' and sys_platform != 'win32'",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ast-serialize"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorama"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "coverage"
|
|
||||||
version = "7.15.4"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iniconfig"
|
|
||||||
version = "2.3.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "librt"
|
|
||||||
version = "0.15.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "loguru"
|
|
||||||
version = "0.7.3"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
||||||
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy"
|
|
||||||
version = "2.3.1"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "ast-serialize" },
|
|
||||||
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
|
|
||||||
{ name = "mypy-extensions" },
|
|
||||||
{ name = "pathspec" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy-extensions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "numpy"
|
|
||||||
version = "2.5.2"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "packaging"
|
|
||||||
version = "26.3"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pandas"
|
|
||||||
version = "3.0.5"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "numpy" },
|
|
||||||
{ name = "python-dateutil" },
|
|
||||||
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pathspec"
|
|
||||||
version = "1.1.1"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pluggy"
|
|
||||||
version = "1.6.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pygments"
|
|
||||||
version = "2.21.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytest"
|
|
||||||
version = "9.1.1"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
||||||
{ name = "iniconfig" },
|
|
||||||
{ name = "packaging" },
|
|
||||||
{ name = "pluggy" },
|
|
||||||
{ name = "pygments" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytest-asyncio"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "pytest" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytest-cov"
|
|
||||||
version = "7.1.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "coverage" },
|
|
||||||
{ name = "pluggy" },
|
|
||||||
{ name = "pytest" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-dateutil"
|
|
||||||
version = "2.9.0.post0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "six" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quant-engine"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { editable = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "loguru" },
|
|
||||||
{ name = "numpy" },
|
|
||||||
{ name = "pandas" },
|
|
||||||
{ name = "scipy" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.optional-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "mypy" },
|
|
||||||
{ name = "pytest" },
|
|
||||||
{ name = "pytest-asyncio" },
|
|
||||||
{ name = "pytest-cov" },
|
|
||||||
{ name = "ruff" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "loguru", specifier = ">=0.7" },
|
|
||||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" },
|
|
||||||
{ name = "numpy", specifier = ">=1.24" },
|
|
||||||
{ name = "pandas", specifier = ">=2.0" },
|
|
||||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
|
||||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" },
|
|
||||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1" },
|
|
||||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" },
|
|
||||||
{ name = "scipy", specifier = ">=1.10" },
|
|
||||||
]
|
|
||||||
provides-extras = ["dev"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ruff"
|
|
||||||
version = "0.16.4"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scipy"
|
|
||||||
version = "1.18.1"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "numpy" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28" },
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "six"
|
|
||||||
version = "1.17.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typing-extensions"
|
|
||||||
version = "4.16.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tzdata"
|
|
||||||
version = "2026.3"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "win32-setctime"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390" },
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user