Compare commits

..
Author SHA1 Message Date
ao gong 55eeff3951 docs: add realized ledger weights to handoff 2026-08-21 22:22:00 +08:00
ao gong 3b1ad07c69 feat: expose realized ledger position weights 2026-08-21 22:21:32 +08:00
ao gong 1b5b353098 test: define realized ledger weight projection contract 2026-08-21 22:21:03 +08:00
ao gong 2bb9f52080 wip: hand off ledger-backed attribution 2026-08-21 22:19:20 +08:00
ao gong 76bb5494a2 docs: record lightweight attribution design references 2026-08-21 22:18:24 +08:00
ao gong f7ad82534a feat: add label-safe component risk decomposition 2026-08-21 22:17:05 +08:00
ao gong 35a52d781e test: define labeled component risk contract 2026-08-21 22:16:24 +08:00
ao gong f14ab464f7 feat: add strict benchmark-relative performance metrics 2026-08-21 22:15:47 +08:00
ao gong de2f9494fc test: define benchmark-relative performance contract 2026-08-21 22:15:05 +08:00
ao gong 19fe22b01a feat: add ledger-backed daily return attribution 2026-08-21 22:14:18 +08:00
ao gong 212351e984 test: define post-execution return attribution contract 2026-08-21 22:13:04 +08:00
ao gong 5fb4b85cc3 wip: hand off stacked daily ledger 2026-08-21 22:06:51 +08:00
ao gong a421278527 fix: align ledger performance with signal window 2026-08-21 22:05:28 +08:00
ao gong c774a4546a test: exclude factor warmup from performance window 2026-08-21 22:04:52 +08:00
ao gong 022b87fdac feat: expose platform-neutral ledger projection 2026-08-21 22:03:21 +08:00
ao gong 1b39c53f18 test: define daily ledger projection contract 2026-08-21 22:02:58 +08:00
ao gong b794ab2e8f feat: add post-execution daily ledger 2026-08-21 22:01:35 +08:00
ao gong a44e2d306a test: define post-execution daily ledger contract 2026-08-21 21:58:01 +08:00
ao gong 15b283bdf9 docs: distinguish signal execution and holding times
CI / lite (pull_request) Successful in 4s
2026-08-21 21:49:07 +08:00
ao gong f9b7f2ab1a feat: schedule factor weights for next-session execution 2026-08-21 21:47:53 +08:00
ao gong 213aa88deb test: require execution-price input snapshot 2026-08-21 21:46:47 +08:00
ao gong b7f77e2a6c test: define lagged factor-to-execution contract 2026-08-21 21:46:00 +08:00
ao gong c9fb5978f0 docs: clarify execution audit result contract
CI / lite (pull_request) Successful in 3s
2026-08-21 21:39:12 +08:00
ao gong b2af2a10a6 docs: document auditable execution workflow 2026-08-21 21:38:08 +08:00
ao gong da2ca51ff7 fix: enforce cash-backed long-only rebalancing 2026-08-21 21:37:02 +08:00
ao gong ee22d1eb9d test: reproduce execution cash and long-only violations 2026-08-21 21:35:47 +08:00
ao gong 17b680604d fix: derive multi-day trades from target-weight deltas 2026-08-21 21:35:15 +08:00
ao gong c51d803daf test: reproduce multi-day execution audit gaps 2026-08-21 21:31:52 +08:00
ao gong 5578851d85 fix: enforce chronological rebalance scores
CI / lite (pull_request) Canceled after 0s
2026-08-21 21:25:27 +08:00
ao gong b4f7b74c04 test: reject unsorted rebalance dates 2026-08-21 21:25:11 +08:00
ao gong 0a236622f3 docs: add factor-to-backtest portfolio workflow 2026-08-21 21:24:41 +08:00
ao gong 88af157ee7 fix: validate unique rebalance dates 2026-08-21 21:24:17 +08:00
ao gong a1130ea43b test: reject ambiguous duplicate rebalance dates 2026-08-21 21:24:03 +08:00
ao gong 96e8f1ad62 feat: build target weights from factor scores 2026-08-21 21:23:25 +08:00
ao gong ecf6b4e4bf test: add red factor-to-weights portfolio contract 2026-08-21 21:22:36 +08:00
ao gong 979166ad16 docs: document unified backtest result workflow
CI / lite (pull_request) Successful in 3s
2026-08-21 21:11:52 +08:00
ao gong cdf41edf1f feat: add unified weight backtest result facade 2026-08-21 21:11:06 +08:00
ao gong 585a635797 test: add red contract for unified backtest result 2026-08-21 21:10:33 +08:00
ao gong 2ff0630973 refactor: clarify quant core validation internals
CI / lite (pull_request) Successful in 3s
2026-08-21 21:00:19 +08:00
ao gong bae4dedf70 test: satisfy metric contract lint 2026-08-21 20:59:27 +08:00
ao gong e359792ef5 fix: harden quant core calculation boundaries 2026-08-21 20:58:30 +08:00
ao gong 0c3a375b1e test: add red contracts for quant core boundaries 2026-08-21 20:57:49 +08:00
31 changed files with 38 additions and 14512 deletions
+2 -15
View File
@@ -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
View File
@@ -1 +0,0 @@
3.13
+2 -18
View File
@@ -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": 4, "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",
@@ -11,7 +11,6 @@
"Submitting live orders, routing trades, managing brokerage accounts, or claiming transaction execution", "Submitting live orders, routing trades, managing brokerage accounts, or claiming transaction execution",
"Owning market-data source facts, research-result publication, or platform presentation state", "Owning market-data source facts, research-result publication, or platform presentation state",
"Loading provider credentials, brokerage credentials, or production secrets", "Loading provider credentials, brokerage credentials, or production secrets",
"Granting portfolio approval, maker-checker decisions, publication eligibility, paper execution, or live execution authority",
"Changing financial model semantics through module metadata" "Changing financial model semantics through module metadata"
] ]
}, },
@@ -19,28 +18,13 @@
{"id": "factor-and-indicator-calculation", "summary": "Calculate reusable alpha factors and technical indicators from caller-supplied data.", "status": "operational"}, {"id": "factor-and-indicator-calculation", "summary": "Calculate reusable alpha factors and technical indicators from caller-supplied data.", "status": "operational"},
{"id": "execution-simulation", "summary": "Simulate costs, slippage, market constraints, fills, NAV, and PnL without live order routing.", "status": "operational"}, {"id": "execution-simulation", "summary": "Simulate costs, slippage, market constraints, fills, NAV, and PnL without live order routing.", "status": "operational"},
{"id": "portfolio-backtesting", "summary": "Run weight-based backtests and benchmark comparisons.", "status": "operational"}, {"id": "portfolio-backtesting", "summary": "Run weight-based backtests and benchmark comparisons.", "status": "operational"},
{"id": "backtest-evidence-contracts", "summary": "Identify governed offline backtest inputs and close existing research artifact evidence without persistence or decision authority.", "status": "operational"},
{"id": "portfolio-risk-computation-contracts", "summary": "Verify deterministic portfolio-computation receipts and expose S3-bound portfolio decisions and risk assessments without adding algorithms or execution authority.", "status": "operational"},
{"id": "risk-and-performance-analysis", "summary": "Calculate portfolio decomposition, risk contribution, and performance statistics.", "status": "operational"} {"id": "risk-and-performance-analysis", "summary": "Calculate portfolio decomposition, risk contribution, and performance statistics.", "status": "operational"}
], ],
"data": {"owns": [ "data": {"owns": [
{"asset_id": "quantitative-model-implementations", "kind": "model", "classification": "internal"}, {"asset_id": "quantitative-model-implementations", "kind": "model", "classification": "internal"},
{"asset_id": "simulation-and-metric-results", "kind": "artifact", "classification": "confidential"} {"asset_id": "simulation-and-metric-results", "kind": "artifact", "classification": "confidential"}
]}, ]},
"contracts": { "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"},
{"contract_id": "researchhub.backtest-run-ref", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/governed_pipeline.py"},
{"contract_id": "researchhub.backtest-evidence-manifest", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/artifact.py"},
{"contract_id": "researchhub.portfolio-decision", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/portfolio_risk_contracts.py"},
{"contract_id": "researchhub.risk-assessment", "version": "1.0.0", "authority": "quant_engine", "path": "src/quant_engine/portfolio_risk_contracts.py"}
],
"consumes": [
{"contract_id": "researchhub.dataset-snapshot", "version": "1.0.0", "authority": "researchhub.data", "admission": "qualified_immutable_envelope"},
{"contract_id": "researchhub.data-foundation", "version": "1.0.0", "authority": "researchhub.data", "admission": "content_addressed_selected_views"}
]
},
"dependencies": [], "dependencies": [],
"agent_context": { "agent_context": {
"default_entrypoints": [ "default_entrypoints": [
-232
View File
@@ -19,16 +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 订单意图;同时拥有输入/配置/重放血缘决定的 `BacktestRunRef`
- `artifact` — 版本化、确定性、存储中立的完整 research run 事实表,以及只映射现有表的 `BacktestEvidenceManifest`
- `portfolio_risk_contracts` — S3 证据闭合的 `PortfolioDecision` / `RiskAssessment` v1;独立复核 freshness、约束与 computation receipt,并复用既有标签安全风险分解
- `attribution` — 基于实际成交后持仓的隔夜 / 日内 / 交易成本逐日收益归因与闭合审计 - `attribution` — 基于实际成交后持仓的隔夜 / 日内 / 交易成本逐日收益归因与闭合审计
- `metrics` — 绝对绩效 + 严格日期对齐的 TE / IR / alpha / beta 基准相对绩效 - `metrics` — 绝对绩效 + 严格日期对齐的 TE / IR / alpha / beta 基准相对绩效
- `factor_library` — 通用方法(turnover / winsorize / IC / OLS / jb_test) - `factor_library` — 通用方法(turnover / winsorize / IC / OLS / jb_test)
@@ -58,9 +54,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
``` ```
## 使用 ## 使用
@@ -143,46 +136,6 @@ print(attribution.residual) # 应接近 0;否则说明贡献未闭合到账
# benchmark_returns 必须与成本后 factor_backtest.returns 使用完全相同的日期索引。 # benchmark_returns 必须与成本后 factor_backtest.returns 使用完全相同的日期索引。
print(factor_backtest.benchmark_stats(benchmark_returns)) print(factor_backtest.benchmark_stats(benchmark_returns))
# 下游稳定交付:显式提供代码版本、数据快照和时区,不在核心层写数据库。
from quant_engine.artifact import build_research_run_artifact
from quant_engine.data_adapter import prepare_asset_return_snapshot
from quant_engine.risk import estimate_covariance_snapshot
risk_date = factor_backtest.position_weights.index[-1].date()
market_snapshot = prepare_asset_return_snapshot(
qtdb_daily_long,
source="qtdb_pro.hq_daily",
source_snapshot_id="<upstream-ingestion-snapshot-id>",
adjustment="qfq",
)
risk_snapshot = estimate_covariance_snapshot(
market_snapshot.returns,
as_of_date=risk_date,
lookback_sessions=252,
min_observations=120,
data_snapshot_id=market_snapshot.data_snapshot_id,
)
artifact = build_research_run_artifact(
factor_backtest,
run_id="research-run-001",
strategy_id="alpha-top20",
strategy_name="Alpha Top 20",
strategy_version="1.0.0",
engine_version="1.2.0",
code_revision="<git-sha>",
data_snapshot_id=market_snapshot.data_snapshot_id,
calendar="CN-A",
timezone="Asia/Shanghai",
started_at="2026-08-21T10:00:00+08:00",
finished_at="2026-08-21T10:01:00+08:00",
parameters={"top_k": 20, "lag_sessions": 1},
benchmark_id="000300.SH",
benchmark_returns=benchmark_returns,
risk_snapshots={risk_date: risk_snapshot},
)
print(artifact.manifest())
# run_weight_backtest 是低层算子:只接受收益区间开始前已经生效的持仓权重。 # run_weight_backtest 是低层算子:只接受收益区间开始前已经生效的持仓权重。
# 不要把 signal-date 的 factor_scores/decision_weights 直接传给它。 # 不要把 signal-date 的 factor_scores/decision_weights 直接传给它。
backtest = run_weight_backtest( backtest = run_weight_backtest(
@@ -197,191 +150,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 静默升级为新合同。
## 回测引用与证据合同 v1
`quant_engine.governed_pipeline.BacktestRunRef` 是合格回测运行身份的唯一权威。`run_id` 只由
已验收的 Dataset Snapshot / Data Foundation / `FactorSetRef` 身份、universe、日历与公司行动
祖先、策略、执行/成本模型、严格整数 seed、完整代码提交、环境锁、配置、时间和重放血缘决定;
它不包含任何输出摘要。重放必须绑定直接父运行、连续 attempt 和不变的
`replay_spec_digest`,输入漂移或血缘环会失败关闭。
`quant_engine.artifact.BacktestEvidenceManifest` 只摘要 `ResearchRunArtifact` 已有的九张事实表。
固定 `offline_research_v1` 映射为 `run`、`signal`、`fill`、`position_nav`、`performance`、
`attribution`、`risk_snapshot` 与 `replay`;每张表都保留列模式摘要、行数和内容摘要,空 risk
表也必须有稳定 schema。`manifest_id` 由完整 RunRef 与输出证据决定,因此结果变化不会反向改变
`run_id`。当前 artifact 不拥有订单或拒绝事实,所以此画像明确不声明 `order` / `rejection`。
旧 `BacktestRun` 只能通过 `build_legacy_backtest_evidence_manifest()` 显式映射为
`LEGACY_EXPLORATORY`;不能隐式提升为 `CONTRACT_QUALIFIED`。所有资格均只描述离线证据闭合,
不表示投资有效、组合获批、Paper、生产或实盘就绪。
## 组合决策与风险评估合同 v1
`quant_engine.portfolio_risk_contracts` 是现有计算 owner 外围的薄合同层。创建
`PortfolioDecision` 必须同时提供完整 `BacktestRunRef`、嵌入同一 RunRef 的
`CONTRACT_QUALIFIED` 非 legacy `BacktestEvidenceManifest`、现有 `PortfolioTarget`、
`FreshnessPolicy`、`ConstraintSetV1` 与 `ComputationReceipt`。适配器会从权威输入独立重算
receipt 的 input/constraint/output digest、敞口、持仓数和 L1 turnover 残差;receipt 自报
成功、fallback 或放宽 tolerance 均不能替代复核。
```python
from quant_engine.portfolio_risk_contracts import (
ComputationReceipt,
ConstraintSetV1,
FreshnessPolicy,
assess_portfolio_risk,
build_portfolio_decision,
compute_portfolio_receipt_digests,
)
freshness = FreshnessPolicy(
max_manifest_age_seconds=3600,
max_covariance_age_days=5,
)
constraints = ConstraintSetV1(
gross_exposure_max=1.0,
single_asset_max=0.10,
position_count_max=20,
turnover_max=0.30,
)
# 生产者先形成公开 canonical digest;decision 构建时仍会独立重算。
expected = compute_portfolio_receipt_digests(
backtest_run_ref=run_ref,
manifest=evidence_manifest,
target=portfolio_target,
objective_name="long_only_allocation",
objective_version="1.0.0",
objective_digest=objective_digest,
model_name="factor_weighting",
model_version="1.0.0",
model_digest=model_digest,
expected_return_digest=expected_return_digest,
covariance_digest=covariance_digest,
scenario_digest=scenario_digest,
constraints=constraints,
freshness_policy=freshness,
prior_weights=prior_weights,
)
receipt = ComputationReceipt(
algorithm="factor_weighting",
algorithm_version="1.0.0",
implementation_digest=implementation_digest,
parameter_digest=parameter_digest,
input_digest=expected["input_digest"],
constraint_digest=expected["constraint_digest"],
output_digest=expected["output_digest"],
status="completed",
solver_required=False,
solver_name=None,
solver_version=None,
solver_config_digest=None,
iterations=None,
objective_value=None,
max_constraint_residual=expected["max_constraint_residual"],
tolerance=1e-12,
computed_at=computed_at,
)
decision = build_portfolio_decision(
backtest_run_ref=run_ref,
manifest=evidence_manifest,
target=portfolio_target,
objective_name="long_only_allocation",
objective_version="1.0.0",
objective_digest=objective_digest,
model_name="factor_weighting",
model_version="1.0.0",
model_digest=model_digest,
expected_return_digest=expected_return_digest,
covariance_digest=covariance_digest,
scenario_digest=scenario_digest,
constraints=constraints,
freshness_policy=freshness,
receipt=receipt,
computed_at=computed_at,
prior_weights=prior_weights,
)
assessment = assess_portfolio_risk(
portfolio_decision=decision,
backtest_run_ref=run_ref,
manifest=evidence_manifest,
covariance=covariance_snapshot,
risk_model_name="euler_volatility",
risk_model_version="1.0.0",
risk_model_digest=risk_model_digest,
)
```
`source_universe_digest` 保留 S3 研究 universe 身份,`portfolio_asset_set_digest` 只描述实际
目标资产标签;二者不会互相冒充成员证明。风险评估在任何数值计算前要求 covariance、target、
RunRef 的 dataset identity 三方一致,并且只调用一次现有 `labeled_component_risk()`。合同中的
`qualified` 仅表示 S4.1 计算证据闭合,不授予 maker-checker、发布、订单、Paper、生产或实盘权限。
## 治理垂直切片
`governed_pipeline` 不复制因子、回测、组合或执行算法,只编排现有能力并补充版本与风险契约。
调用方必须显式提供 `DatasetSnapshot`、`FactorVersion`、`StrategyVersion`、代码提交和
`RiskPolicy`。模块只会生成 `environment="paper"` 的订单意图,不连接数据库、数据供应商或
券商;风险决策为拒绝时,订单意图固定为空,直接调用创建函数也会失败关闭。
该切片对应 ResearchHub 架构的首个可执行验收链路:
```text
DatasetSnapshot → FactorVersion → StrategyVersion → BacktestRun
→ PortfolioTarget → RiskDecision → PaperOrderIntent
```
平台总架构、五仓职责和十二层能力映射仍以 `research_platform/docs/architecture/` 为权威;
本仓只拥有纯计算与离线模拟合同。
## 与 research_results 的关系 ## 与 research_results 的关系
`research_results` 依赖 `quant_engine`(通过 re-export 保持向后兼容): `research_results` 依赖 `quant_engine`(通过 re-export 保持向后兼容):
-9
View File
@@ -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
-40
View File
@@ -16,46 +16,6 @@
当前核心不新增依赖。逐日收益归因必须从实际换仓前后持仓、成交记录、执行价和 当前核心不新增依赖。逐日收益归因必须从实际换仓前后持仓、成交记录、执行价和
收盘估值推导;因子分数与目标权重只是意图,不能作为成交后归因事实源。 收盘估值推导;因子分数与目标权重只是意图,不能作为成交后归因事实源。
## 2026-08-21:研究运行工件
- 借鉴 [Qlib Recorder / RecordTemplate](https://github.com/microsoft/qlib/blob/main/qlib/workflow/record_temp.py)
将 signal、portfolio analysis 和 risk analysis 分成稳定事实,但不引入 Qlib 运行时;
- 借鉴 [MLflow Tracking](https://mlflow.org/docs/latest/tracking/) 的 run / params /
metrics / artifacts 分层,但 MLflow 只保留为未来可选 exporter;
- HTML、PNG 和 tearsheet 是可再生展示物,不能替代 NAV、成交、持仓、归因和绩效事实。
因此 `ResearchRunArtifact` 使用显式 `schema_version`、`config_hash`、代码版本和数据
快照身份,并提供确定性 JSON / SHA-256 manifest;核心层仍不写数据库或 artifact store。
schema `1.1.0` 将 Qlib 的独立 risk-analysis artifact 思路与 Riskfolio-Lib 的 Euler
component-risk 语义结合,但只保留本项目需要的轻量合同:协方差快照必须声明
`snapshot_id`、`as_of_date`、收益频率和年化期数;风险从成交后的实际日末持仓计算,
component risk 闭合到年化组合波动,percentage contribution 闭合到 1。未来日期、资产
标签不完整和零方差组合都直接失败,不以默认值伪造结果。
## 2026-08-21:协方差快照估计
| 项目 | 借鉴内容 | 当前决策 |
|---|---|---|
| [PyPortfolioOpt risk models](https://github.com/PyPortfolio/PyPortfolioOpt/blob/main/pypfopt/risk_models.py) | 将收益输入、协方差估计器和组合优化解耦;sample / EWM / shrinkage 使用统一标签输出 | 借鉴可替换估计器边界,不引入完整包 |
| [scikit-learn covariance](https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/covariance/_shrunk_covariance.py) | 维护成熟的 Ledoit–Wolf / OAS shrinkage 实现 | 未来作为可选 adapter;不复制统计公式 |
| [Qlib structured risk model](https://github.com/microsoft/qlib/blob/main/qlib/model/riskmodel/structured.py) | PCA/FA 结构化协方差和固定随机状态 | 留作因子风险模型阶段,不进入当前 baseline |
当前 `estimate_covariance_snapshot` 只编排 pandas 的 sample covariance:先按 `as_of_date`
截断,再取固定 session 窗口,使用 complete-case 行并拒绝历史不足;禁止 pandas 默认的
pairwise 样本集合产生含义不一致的矩阵。snapshot ID 对窗口数据、缺失掩码、上游数据
快照身份和估计参数做 SHA-256,追加未来数据不会改变历史快照。
市场适配层现以 `AssetReturnSnapshot` 固化 simple-return 输入:上游 ingestion snapshot ID、
数据源、价格字段、复权口径、规范化价格值和缺失掩码共同形成内容寻址 ID;不前向填充
停牌/缺失价格。该 ID 同时传入协方差快照和研究运行工件,避免同一研究链出现两套数据
身份。
可选 shrinkage adapter 的评估结论是“保留边界,暂不实现”:当前运行依赖没有声明
scikit-learn,本切片也不修改版本或锁文件。未来只有在依赖治理接受后,才以延迟导入
直接调用 scikit-learn 的 `LedoitWolf` / `OAS`,并让估计器名称、库版本与参数进入
snapshot identity;不复制成熟统计公式,也不让环境中偶然存在的包改变 baseline 行为。
## hikyuu 的定位 ## hikyuu 的定位
[hikyuu](https://github.com/fasiondog/hikyuu) 的 SG / MM / CN / PG 部件化思想、 [hikyuu](https://github.com/fasiondog/hikyuu) 的 SG / MM / CN / PG 部件化思想、
@@ -1,57 +0,0 @@
# Research artifact contract handoff
## Goal
把完整可信研究链固化成存储中立、版本化、确定性的 `ResearchRunArtifact`,供
`research_results` 持久化和 `research_platform` 查询:
- run identity / schema version / config hash / code revision / data snapshot;
- signal scores / decision weights / signal-to-execution mapping;
- NAV / returns / benchmark / costs;
- trades / realized positions / cash;
- asset and daily return attribution;
- performance including Sortino / TE / IR / alpha / beta;
- reproducible covariance snapshots and annualized Euler component-risk facts;
- canonical JSON / SHA-256 manifest。
## Branch stack
- 当前:`codex/research-artifact-contract-20260821`
- 基线:`codex/ledger-attribution-20260821`(Draft PR #4)
- 下层:Draft PR #3 → Ready PR #2 → `main`
不得绕过堆叠顺序直接合并到 `main`。
## Verification
- `pytest -q`: 540 passed,9 个既有 SciPy warning;
- data-adapter focused coverage 77%(包含未连接真实 ClickHouse 的 I/O 便捷函数);
- `mypy --strict src/`: 16 source files passed;
- changed-scope Ruff: passed;
- no runtime dependency added;
- no database, network, broker or filesystem write side effect in artifact builder。
- 三仓隔离 ClickHouse 黄金链路通过:市场价格 → return snapshot → covariance → artifact →
publisher → reader;使用随机 localhost 端口、tmpfs 和自动容器清理。
## Current risk contract
- artifact schema:`1.1.0`;
- `CovarianceSnapshot` 对输入矩阵深拷贝并显式记录截至日、频率和年化期数;
- `risk_snapshots` 按研究交易日映射,可只生成需要的风险观察日;
- 使用成交后实际持仓,不包含现金风险资产;协方差资产标签必须与研究资产全集一致;
- `covariance_as_of_date` 不得晚于 `trade_date`;无正组合方差时拒绝产物。
- `estimate_covariance_snapshot` 从显式数据快照的日收益生成无前视、complete-case、
SHA-256 可复现的 per-period sample covariance;不包含 I/O 或未来行。
- `prepare_asset_return_snapshot` 从规范化长表行情生成不前向填充的 simple daily returns;
显式 ingestion snapshot ID、源/字段/复权口径、价格值和缺失掩码共同形成
`asset-returns-v1:<sha256>`,并把同一 ID 传给 covariance 与 run artifact。
- artifact builder fail closed:每个 `CovarianceSnapshot.data_snapshot_id` 必须与 run 级
`data_snapshot_id` 完全一致,禁止把其他行情快照的风险分解静默发布到当前研究运行。
- shrinkage 适配器本轮不实现:scikit-learn 尚非声明依赖,未来只允许薄适配
`LedoitWolf` / `OAS`,不复制公式、不依赖环境偶然安装状态。
## Next action
保持 Draft PR #5,不绕过堆叠顺序合并;下游 `research_results` / `research_platform`
继续在现有 Draft 分支消费同一数据 lineage。下一阶段优先把 ingestion snapshot ID 从
真实 ELT 元数据接入调用方,再在依赖治理通过后单独交付可选 shrinkage adapter。
+3 -6
View File
@@ -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"
+1 -927
View File
@@ -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",
File diff suppressed because it is too large Load Diff
+3 -203
View File
@@ -6,27 +6,22 @@
- execution.py 需要**宽表**(date × stock_code)prices / volumes - execution.py 需要**宽表**(date × stock_code)prices / volumes
- Tushare 字段命名:`ts_code / vol(手) / amount(千元) / pct_chg`,且**无 vwap 字段** - Tushare 字段命名:`ts_code / vol(手) / amount(千元) / pct_chg`,且**无 vwap 字段**
本模块提供可组合的数据适配函数,让新模块直接吃 qtdb_pro 真实数据: 本模块提供 6 个纯函数,让新模块直接吃 qtdb_pro 真实数据:
1. `long_to_wide()` — 长表 → 宽表(date × stock_code) 1. `long_to_wide()` — 长表 → 宽表(date × stock_code)
2. `wide_to_long()` — 宽表 → 长表 2. `wide_to_long()` — 宽表 → 长表
3. `rename_tushare_columns()` — 列名映射(ts_code→stock_code, vol→volume 等) 3. `rename_tushare_columns()` — 列名映射(ts_code→stock_code, vol→volume 等)
4. `add_vwap_proxy()` — vwap 代理(Tushare 无 vwap 字段) 4. `add_vwap_proxy()` — vwap 代理(Tushare 无 vwap 字段)
5. `apply_adj_factor()` — 复权(hq_daily × hq_adj_factor 前复权) 5. `apply_adj_factor()` — 复权(hq_daily × hq_adj_factor 前复权)
6. `prepare_stock_series()` — 单股提取(alpha_factors 输入) 6. `prepare_stock_series()` — 单股提取(alpha_factors 输入)
7. `prepare_asset_return_snapshot()` — 带稳定 lineage 的资产日收益 7. `prepare_execution_inputs()` — execution 输入(prices + volumes 宽表)
8. `prepare_execution_inputs()` — execution 输入(prices + volumes 宽表) 8. `load_qtdb_daily()` — 便捷加载(qtdb_pro.hq_daily + 可选复权)
9. `load_qtdb_daily()` — 便捷加载(qtdb_pro.hq_daily + 可选复权)
全部纯 pandas/numpy,零新依赖,mypy strict 兼容。 全部纯 pandas/numpy,零新依赖,mypy strict 兼容。
""" """
from __future__ import annotations from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import date
from typing import Any from typing import Any
import numpy as np import numpy as np
@@ -37,14 +32,12 @@ from quant_engine.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = [ __all__ = [
"AssetReturnSnapshot",
"long_to_wide", "long_to_wide",
"wide_to_long", "wide_to_long",
"rename_tushare_columns", "rename_tushare_columns",
"add_vwap_proxy", "add_vwap_proxy",
"apply_adj_factor", "apply_adj_factor",
"prepare_stock_series", "prepare_stock_series",
"prepare_asset_return_snapshot",
"prepare_execution_inputs", "prepare_execution_inputs",
"load_qtdb_daily", "load_qtdb_daily",
] ]
@@ -64,107 +57,6 @@ TUSHARE_RENAME: dict[str, str] = {
} }
@dataclass(frozen=True, slots=True, init=False, eq=False)
class AssetReturnSnapshot:
"""Immutable-by-interface daily return matrix with reproducible lineage."""
data_snapshot_id: str
source: str
source_snapshot_id: str
price_field: str
adjustment: str
return_method: str
start_date: date
end_date: date
sessions: int
assets: tuple[str, ...]
_returns: pd.DataFrame
def __init__(
self,
*,
data_snapshot_id: str,
source: str,
source_snapshot_id: str,
price_field: str,
adjustment: str,
return_method: str,
start_date: date,
end_date: date,
assets: tuple[str, ...],
returns: pd.DataFrame,
) -> None:
for value, name in (
(data_snapshot_id, "data_snapshot_id"),
(source, "source"),
(source_snapshot_id, "source_snapshot_id"),
(price_field, "price_field"),
(adjustment, "adjustment"),
(return_method, "return_method"),
):
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{name} must be non-empty")
if returns.empty or not isinstance(returns.index, pd.DatetimeIndex):
raise ValueError("returns must contain a DatetimeIndex and at least one session")
if tuple(returns.columns) != assets:
raise ValueError("assets must match returns columns")
if start_date > end_date:
raise ValueError("start_date must not be after end_date")
object.__setattr__(self, "data_snapshot_id", data_snapshot_id.strip())
object.__setattr__(self, "source", source.strip())
object.__setattr__(self, "source_snapshot_id", source_snapshot_id.strip())
object.__setattr__(self, "price_field", price_field.strip())
object.__setattr__(self, "adjustment", adjustment.strip())
object.__setattr__(self, "return_method", return_method.strip())
object.__setattr__(self, "start_date", start_date)
object.__setattr__(self, "end_date", end_date)
object.__setattr__(self, "sessions", len(returns))
object.__setattr__(self, "assets", assets)
object.__setattr__(self, "_returns", returns.copy(deep=True))
@property
def returns(self) -> pd.DataFrame:
"""Return an isolated copy so callers cannot mutate the snapshot."""
return self._returns.copy(deep=True)
def _non_empty(value: str, name: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{name} must be non-empty")
return value.strip()
def _asset_return_snapshot_id(
prices: pd.DataFrame,
*,
source: str,
source_snapshot_id: str,
price_field: str,
adjustment: str,
) -> str:
values = prices.to_numpy(dtype=float, copy=True)
missing = np.isnan(values)
normalized = np.where(missing, 0.0, values).astype("<f8", copy=False)
metadata = {
"adjustment": adjustment,
"assets": [str(asset) for asset in prices.columns],
"price_field": price_field,
"return_method": "simple",
"schema": "asset-returns-v1",
"sessions": [timestamp.date().isoformat() for timestamp in prices.index],
"shape": list(values.shape),
"source": source,
"source_snapshot_id": source_snapshot_id,
}
digest = hashlib.sha256(
json.dumps(metadata, sort_keys=True, separators=(",", ":")).encode("utf-8")
)
digest.update(missing.astype(np.uint8, copy=False).tobytes(order="C"))
digest.update(normalized.tobytes(order="C"))
return f"asset-returns-v1:{digest.hexdigest()}"
def long_to_wide( def long_to_wide(
df: pd.DataFrame, df: pd.DataFrame,
value_col: str = "close", value_col: str = "close",
@@ -391,98 +283,6 @@ def prepare_stock_series(
return series_map return series_map
def prepare_asset_return_snapshot(
df: pd.DataFrame,
*,
source: str,
source_snapshot_id: str,
price_col: str = "close",
adjustment: str = "none",
stock_col: str = "stock_code",
date_col: str = "trade_date",
) -> AssetReturnSnapshot:
"""Build deterministic simple daily returns from a long market-price table.
``source_snapshot_id`` must identify the upstream ingestion snapshot. The
resulting ID additionally fingerprints canonical price values and their
missing mask, so changed contents cannot retain the same downstream identity.
Missing prices are never forward-filled.
"""
normalized_source = _non_empty(source, "source")
normalized_source_snapshot_id = _non_empty(
source_snapshot_id,
"source_snapshot_id",
)
normalized_price_col = _non_empty(price_col, "price_col")
normalized_adjustment = _non_empty(adjustment, "adjustment")
if not isinstance(df, pd.DataFrame):
raise TypeError("df must be a pandas DataFrame")
if df.empty:
raise ValueError("df must contain market prices")
required = {date_col, stock_col, normalized_price_col}
missing_columns = sorted(required.difference(df.columns))
if missing_columns:
raise ValueError(f"prepare_asset_return_snapshot: missing columns={missing_columns}")
market = df[[date_col, stock_col, normalized_price_col]].copy()
if any(not isinstance(asset, str) or not asset.strip() for asset in market[stock_col]):
raise ValueError("asset labels must be non-empty strings")
market[stock_col] = market[stock_col].str.strip()
try:
normalized_dates = pd.to_datetime(market[date_col], errors="raise")
except (TypeError, ValueError) as error:
raise ValueError("trade dates must be valid dates") from error
if normalized_dates.isna().any():
raise ValueError("trade dates must be valid dates")
market[date_col] = normalized_dates.dt.normalize()
if market.duplicated(subset=[date_col, stock_col]).any():
raise ValueError("duplicate asset/session prices are not allowed")
try:
market[normalized_price_col] = pd.to_numeric(
market[normalized_price_col],
errors="raise",
)
except (TypeError, ValueError) as error:
raise ValueError("prices must be numeric") from error
observed_prices = market[normalized_price_col].dropna().to_numpy(dtype=float)
if observed_prices.size == 0 or not np.isfinite(observed_prices).all():
raise ValueError("prices must contain positive finite observations")
if (observed_prices <= 0.0).any():
raise ValueError("prices must contain positive finite observations")
prices = market.pivot(
index=date_col,
columns=stock_col,
values=normalized_price_col,
).sort_index()
prices = prices.reindex(sorted(str(asset) for asset in prices.columns), axis="columns")
prices = prices.astype(float)
if len(prices) < 2:
raise ValueError("market prices must contain at least two sessions")
returns = prices.pct_change(fill_method=None)
assets = tuple(str(asset) for asset in prices.columns)
snapshot_id = _asset_return_snapshot_id(
prices,
source=normalized_source,
source_snapshot_id=normalized_source_snapshot_id,
price_field=normalized_price_col,
adjustment=normalized_adjustment,
)
return AssetReturnSnapshot(
data_snapshot_id=snapshot_id,
source=normalized_source,
source_snapshot_id=normalized_source_snapshot_id,
price_field=normalized_price_col,
adjustment=normalized_adjustment,
return_method="simple",
start_date=prices.index[0].date(),
end_date=prices.index[-1].date(),
assets=assets,
returns=returns,
)
def prepare_execution_inputs( def prepare_execution_inputs(
df: pd.DataFrame, df: pd.DataFrame,
stock_col: str = "stock_code", stock_col: str = "stock_code",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-15
View File
@@ -65,20 +65,6 @@ def sharpe_ratio(r: pd.Series, rf: float = 0.0) -> float:
return (annualized_return(r) - rf) / vol return (annualized_return(r) - rf) / vol
def sortino_ratio(r: pd.Series, rf: float = 0.0) -> float:
"""Sortino = (年化收益 - rf) / 年化下行偏差。"""
r = _clean(r)
if len(r) < 2:
return 0.0
downside = np.minimum(r.to_numpy(dtype=float), 0.0)
downside_deviation = float(
np.sqrt(np.mean(np.square(downside))) * np.sqrt(TRADING_DAYS_PER_YEAR)
)
if downside_deviation == 0:
return 0.0
return (annualized_return(r) - rf) / downside_deviation
def max_drawdown(r: pd.Series) -> float: def max_drawdown(r: pd.Series) -> float:
"""最大回撤(负数)。例如 -0.2 表示最大亏 20%。""" """最大回撤(负数)。例如 -0.2 表示最大亏 20%。"""
r = _clean(r) r = _clean(r)
@@ -134,7 +120,6 @@ def summary(r: pd.Series, rf: float = 0.0) -> Mapping[str, float]:
"ann_return": ann_ret, "ann_return": ann_ret,
"ann_volatility": ann_vol, "ann_volatility": ann_vol,
"sharpe": sharpe_ratio(r, rf), "sharpe": sharpe_ratio(r, rf),
"sortino": sortino_ratio(r, rf),
"max_drawdown": mdd, "max_drawdown": mdd,
"calmar": calmar_ratio(r), "calmar": calmar_ratio(r),
"win_rate": win_rate(r), "win_rate": win_rate(r),
File diff suppressed because it is too large Load Diff
-250
View File
@@ -5,10 +5,7 @@
from __future__ import annotations from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date
from typing import Any from typing import Any
import numpy as np import numpy as np
@@ -17,260 +14,13 @@ from numpy.typing import NDArray
__all__ = [ __all__ = [
"ComponentRiskResult", "ComponentRiskResult",
"CovarianceSnapshot",
"component_var", "component_var",
"estimate_covariance_snapshot",
"labeled_component_risk", "labeled_component_risk",
"marginal_risk_contribution", "marginal_risk_contribution",
"risk_contribution", "risk_contribution",
] ]
@dataclass(frozen=True, slots=True, init=False, eq=False)
class CovarianceSnapshot:
"""Immutable-by-interface covariance input with explicit time semantics."""
snapshot_id: str
as_of_date: date
_covariance: pd.DataFrame
return_frequency: str
periods_per_year: int
method: str
window_start_date: date | None
window_end_date: date | None
observations: int | None
lookback_sessions: int | None
missing_policy: str
data_snapshot_id: str
input_sha256: str
def __init__(
self,
*,
snapshot_id: str,
as_of_date: str | date | pd.Timestamp,
covariance: pd.DataFrame,
return_frequency: str,
periods_per_year: int,
method: str = "provided",
window_start_date: str | date | pd.Timestamp | None = None,
window_end_date: str | date | pd.Timestamp | None = None,
observations: int | None = None,
lookback_sessions: int | None = None,
missing_policy: str = "provided",
data_snapshot_id: str = "",
input_sha256: str = "",
) -> None:
if not isinstance(snapshot_id, str) or not snapshot_id.strip():
raise ValueError("snapshot_id must be non-empty")
if not isinstance(return_frequency, str) or not return_frequency.strip():
raise ValueError("return_frequency must be non-empty")
if isinstance(periods_per_year, bool) or not isinstance(periods_per_year, int):
raise TypeError("periods_per_year must be an integer")
if periods_per_year <= 0:
raise ValueError("periods_per_year must be positive")
if not isinstance(covariance, pd.DataFrame):
raise TypeError("covariance must be a pandas DataFrame")
if covariance.empty:
raise ValueError("covariance must contain at least one asset")
if not isinstance(method, str) or not method.strip():
raise ValueError("method must be non-empty")
if not isinstance(missing_policy, str) or not missing_policy.strip():
raise ValueError("missing_policy must be non-empty")
for value, name in (
(observations, "observations"),
(lookback_sessions, "lookback_sessions"),
):
if value is not None and (
isinstance(value, bool) or not isinstance(value, int) or value <= 0
):
raise ValueError(f"{name} must be a positive integer when provided")
if input_sha256 and (
len(input_sha256) != 64
or any(character not in "0123456789abcdef" for character in input_sha256)
):
raise ValueError("input_sha256 must be a lowercase SHA-256 digest")
normalized_as_of = _normalized_date(as_of_date, "as_of_date")
normalized_window_start = (
None
if window_start_date is None
else _normalized_date(window_start_date, "window_start_date")
)
normalized_window_end = (
None
if window_end_date is None
else _normalized_date(window_end_date, "window_end_date")
)
if (normalized_window_start is None) != (normalized_window_end is None):
raise ValueError("window_start_date and window_end_date must be provided together")
if (
normalized_window_start is not None
and normalized_window_end is not None
and normalized_window_start > normalized_window_end
):
raise ValueError("window_start_date must not be after window_end_date")
if normalized_window_end is not None and normalized_window_end > normalized_as_of:
raise ValueError("window_end_date must not be after as_of_date")
object.__setattr__(self, "snapshot_id", snapshot_id.strip())
object.__setattr__(self, "as_of_date", normalized_as_of)
object.__setattr__(self, "_covariance", covariance.copy(deep=True))
object.__setattr__(self, "return_frequency", return_frequency.strip())
object.__setattr__(self, "periods_per_year", periods_per_year)
object.__setattr__(self, "method", method.strip())
object.__setattr__(self, "window_start_date", normalized_window_start)
object.__setattr__(self, "window_end_date", normalized_window_end)
object.__setattr__(self, "observations", observations)
object.__setattr__(self, "lookback_sessions", lookback_sessions)
object.__setattr__(self, "missing_policy", missing_policy.strip())
object.__setattr__(self, "data_snapshot_id", data_snapshot_id.strip())
object.__setattr__(self, "input_sha256", input_sha256)
@property
def covariance(self) -> pd.DataFrame:
"""Return an isolated copy so callers cannot mutate the snapshot."""
return self._covariance.copy(deep=True)
def _normalized_date(value: object, name: str) -> date:
try:
timestamp = pd.Timestamp(value)
except (TypeError, ValueError) as error:
raise ValueError(f"{name} must be a valid date") from error
if pd.isna(timestamp):
raise ValueError(f"{name} must be a valid date")
return date(int(timestamp.year), int(timestamp.month), int(timestamp.day))
def _positive_integer(value: int, name: str, *, minimum: int = 1) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
raise ValueError(f"{name} must be an integer of at least {minimum}")
return value
def _input_fingerprint(window: pd.DataFrame, session_dates: list[date]) -> str:
values = window.to_numpy(dtype=float, copy=True)
missing = np.isnan(values)
normalized = np.where(missing, 0.0, values).astype("<f8", copy=False)
metadata = {
"assets": [str(asset) for asset in window.columns],
"sessions": [session.isoformat() for session in session_dates],
"shape": list(values.shape),
}
digest = hashlib.sha256(
json.dumps(metadata, sort_keys=True, separators=(",", ":")).encode("utf-8")
)
digest.update(missing.astype(np.uint8, copy=False).tobytes(order="C"))
digest.update(normalized.tobytes(order="C"))
return digest.hexdigest()
def estimate_covariance_snapshot(
asset_returns: pd.DataFrame,
*,
as_of_date: str | date | pd.Timestamp,
lookback_sessions: int,
min_observations: int,
data_snapshot_id: str,
return_frequency: str = "1d",
periods_per_year: int = 252,
) -> CovarianceSnapshot:
"""Estimate a deterministic per-period sample covariance without look-ahead.
The selected lookback window is truncated at ``as_of_date`` before any
calculation. Rows containing a missing asset return are removed as complete
cases, preventing pairwise sample sets from producing an ambiguous matrix.
"""
if not isinstance(asset_returns, pd.DataFrame):
raise TypeError("asset_returns must be a pandas DataFrame")
if asset_returns.empty or asset_returns.shape[1] == 0:
raise ValueError("asset_returns must contain observations and assets")
if not isinstance(asset_returns.index, pd.DatetimeIndex):
raise TypeError("asset_returns index must be a DatetimeIndex")
if not asset_returns.index.is_unique or not asset_returns.index.is_monotonic_increasing:
raise ValueError("asset_returns index must be unique and strictly increasing")
if not asset_returns.columns.is_unique:
raise ValueError("asset_returns must contain unique asset labels")
if any(not isinstance(asset, str) or not asset.strip() for asset in asset_returns.columns):
raise ValueError("asset_returns asset labels must be non-empty strings")
lookback = _positive_integer(lookback_sessions, "lookback_sessions")
minimum = _positive_integer(min_observations, "min_observations", minimum=2)
if minimum > lookback:
raise ValueError("min_observations must not exceed lookback_sessions")
normalized_data_snapshot_id = data_snapshot_id.strip()
if not normalized_data_snapshot_id:
raise ValueError("data_snapshot_id must be non-empty")
normalized_as_of = _normalized_date(as_of_date, "as_of_date")
returns = asset_returns.astype(float, copy=True)
values = returns.to_numpy()
if np.isinf(values).any():
raise ValueError("asset_returns must not contain infinite values")
session_dates = [
_normalized_date(index_value, "asset_returns index") for index_value in returns.index
]
if len(set(session_dates)) != len(session_dates):
raise ValueError("asset_returns must contain at most one observation per session date")
historical_mask = [session <= normalized_as_of for session in session_dates]
window = returns.loc[historical_mask].tail(lookback)
if window.empty:
raise ValueError("asset_returns contain no observations on or before as_of_date")
window_dates = [
_normalized_date(index_value, "asset_returns index") for index_value in window.index
]
complete = window.dropna(axis=0, how="any")
if len(complete) < minimum:
raise ValueError(
f"complete observations must be at least {minimum}; received {len(complete)}"
)
covariance = complete.cov(ddof=1)
covariance_values = covariance.to_numpy()
if not np.isfinite(covariance_values).all():
raise ValueError("sample covariance must be finite")
input_sha256 = _input_fingerprint(window, window_dates)
identity = {
"as_of_date": normalized_as_of.isoformat(),
"assets": list(returns.columns),
"data_snapshot_id": normalized_data_snapshot_id,
"estimator": "sample-cov-v1",
"input_sha256": input_sha256,
"lookback_sessions": lookback,
"min_observations": minimum,
"missing_policy": "complete_case",
"observations": len(complete),
"periods_per_year": periods_per_year,
"return_frequency": return_frequency,
"window_end_date": window_dates[-1].isoformat(),
"window_start_date": window_dates[0].isoformat(),
}
identity_bytes = json.dumps(
identity,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
digest = hashlib.sha256(identity_bytes)
digest.update(covariance_values.astype("<f8", copy=False).tobytes(order="C"))
snapshot_id = f"sample-cov-v1:{digest.hexdigest()}"
return CovarianceSnapshot(
snapshot_id=snapshot_id,
as_of_date=normalized_as_of,
covariance=covariance,
return_frequency=return_frequency,
periods_per_year=periods_per_year,
method="sample",
window_start_date=window_dates[0],
window_end_date=window_dates[-1],
observations=len(complete),
lookback_sessions=lookback,
missing_policy="complete_case",
data_snapshot_id=normalized_data_snapshot_id,
input_sha256=input_sha256,
)
@dataclass(frozen=True, slots=True, eq=False) @dataclass(frozen=True, slots=True, eq=False)
class ComponentRiskResult: class ComponentRiskResult:
"""Label-preserving Euler decomposition of portfolio volatility.""" """Label-preserving Euler decomposition of portfolio volatility."""
-17
View File
@@ -1,17 +0,0 @@
{
"run_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
"replay_spec_digest": "sha256:20f07fcb526bc38b4ba3d71d6c3b00a8c63ad96cab0fecd869326503ab42d98b",
"manifest_id": "rhbacktestevidencev1:sha256:f94733e849433f62f3da1e1ec8999891b93d49c7d657832d64e64bef9e211117",
"evidence_digest": "sha256:f3913894d032c389c64eef59058b3cbc694cb9cc14ce9cee2699f0068200b650",
"table_content_digests": {
"run": "sha256:7d947ec93f714641669cbb14bd69dd8cf30387aabb7f3a086918fc2e878ab05c",
"signals": "sha256:72dc15064cc45d7c51d2dd4b8c3a6d8c7d4155232d5d70d3c9e7697fab70ce50",
"trades": "sha256:2b8b9321e7993941ac486cf50cab5b4c6425b2571a4701f0eb02273ec9a96c53",
"positions": "sha256:b456a48fab51742b05084ca6dcaf01c03dfe5b39ad71215ada070e9b1f59f7ea",
"nav": "sha256:25649efce860b76410f87dbd36c81085887620086dc0b48b07099918f65d9c78",
"performance": "sha256:0856439ea7ec84e38887ccfa0067324f2e9cd293543e4ef683b9c2beb9eb34cf",
"attribution": "sha256:ad0f12f668d0a2d9ae5b3636d29989ab4bafcfb95f5de77abeb52a6d9e95d366",
"attribution_daily": "sha256:d4459ad650f88871d7b1e40392033037b5818ef92fdf1ee021868929fc1455a5",
"risk": "sha256:4816dd4812b5ff2e97e74bf34ca2221bfde675b387a96e277683557f0ad7d975"
}
}
-206
View File
@@ -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"
}
}
-129
View File
@@ -1,129 +0,0 @@
{
"portfolio_decision": {
"computed_at": "2026-01-08T03:01:00Z",
"constraint_residuals": {
"gross_exposure_max": 0.0,
"net_exposure_max": 0.0,
"net_exposure_min": 0.0,
"position_count_max": 0.0,
"single_asset_max": 0.0,
"single_asset_min": 0.0,
"turnover_max": 0.0
},
"constraints": {
"gross_exposure_max": 1.0,
"net_exposure_max": 1.0,
"net_exposure_min": 1.0,
"position_count_max": 2,
"schema_version": "1.0.0",
"single_asset_max": 0.7,
"single_asset_min": 0.2,
"turnover_max": 0.2
},
"contract_name": "researchhub.portfolio-decision",
"covariance_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"dataset_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
"decision_id": "rhportfoliodecisionv1:sha256:0e6e5ce2fa08de8006cc392610695a013327fc80645bfa4d7a908ad3f615bebd",
"effective_at": "2026-01-08T03:00:00Z",
"evidence_digest": "sha256:f3913894d032c389c64eef59058b3cbc694cb9cc14ce9cee2699f0068200b650",
"expected_return_digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"freshness_policy": {
"max_covariance_age_days": 0,
"max_manifest_age_seconds": 3600,
"schema_version": "1.0.0"
},
"gross_exposure": 1.0,
"manifest_document_sha256": "fbf54218f770528978f9ccd35577e1ab00877143ea397a576e071e75f0afbab0",
"manifest_id": "rhbacktestevidencev1:sha256:681c49cbdfb3e221b273e7bc616602ad80a7ab01206970807ad4294b176ebc75",
"model_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"model_name": "deterministic_weights",
"model_version": "1.0.0",
"net_exposure": 1.0,
"objective_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"objective_name": "long_only_allocation",
"objective_version": "1.0.0",
"output_digest": "sha256:bd3b964c628c8648322d036e57dd6f444ca287017d1578bab3689d07d32b28ce",
"portfolio_asset_set_digest": "sha256:b64e3448a83a5b86466465080361c1a7e1157a27ddccd4b68069cb18caffb74a",
"position_count": 2,
"prior_weights": {
"A": 0.5,
"B": 0.5
},
"receipt": {
"algorithm": "bounded_allocation",
"algorithm_version": "1.0.0",
"computed_at": "2026-01-08T03:01:00Z",
"constraint_digest": "sha256:34df0e5c00f503748ff936f9cd415a8947169181f8ca0917bce97dd606e08e94",
"implementation_digest": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"input_digest": "sha256:ebd8ff957115e1adfd84eafa5cea49470356194d747b64ee5897858f9dd067b7",
"iterations": null,
"max_constraint_residual": 0.0,
"objective_value": null,
"output_digest": "sha256:bd3b964c628c8648322d036e57dd6f444ca287017d1578bab3689d07d32b28ce",
"parameter_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
"schema_version": "1.0.0",
"solver_config_digest": null,
"solver_name": null,
"solver_required": false,
"solver_version": null,
"status": "completed",
"tolerance": 1e-12
},
"run_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
"run_ref_document_sha256": "6a798adb3e0568aca84ed3e3a285b92d181ec569462fc003952a8c0d8382ae3a",
"scenario_digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"schema_version": "1.0.0",
"source_universe_digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555",
"target_id": "portfolio-target:synthetic-v1",
"target_weights": {
"A": 0.6,
"B": 0.4
},
"turnover_l1": 0.19999999999999996
},
"risk_assessment": {
"assessment_id": "rhriskassessmentv1:sha256:dbc38825cffcf6d95bd0216d22dbba4e0d4a1359ca5924a3ee529b99a7d78b6d",
"component_risk": {
"A": 1.4549226783578566,
"B": 1.4549226783578568
},
"contract_name": "researchhub.risk-assessment",
"covariance_as_of_date": "2026-01-08",
"covariance_data_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
"covariance_input_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"covariance_snapshot_id": "covariance:synthetic-v1",
"dataset_snapshot_id": "rhdsv1:sha256:f63a29b4795c63fb7d6b2d3b5544cee9274b633db77c75c63d50a340c0827d57",
"decision_id": "rhportfoliodecisionv1:sha256:0e6e5ce2fa08de8006cc392610695a013327fc80645bfa4d7a908ad3f615bebd",
"findings": [],
"freshness_policy_digest": "sha256:833f58f4d1056f0450f7369fd4edd70534cc74e9e55cd60f05b2b3d7a2979763",
"group_exposure": {
"equity": 1.4549226783578566,
"fixed_income": 1.4549226783578568
},
"manifest_id": "rhbacktestevidencev1:sha256:681c49cbdfb3e221b273e7bc616602ad80a7ab01206970807ad4294b176ebc75",
"marginal_risk": {
"A": 2.424871130596428,
"B": 3.637306695894642
},
"percentage_risk": {
"A": 0.49999999999999983,
"B": 0.49999999999999994
},
"periods_per_year": 252,
"portfolio_volatility": 2.909845356715714,
"portfolio_volatility_limit": 10.0,
"qualified": true,
"return_frequency": "1d",
"risk_budget": {
"A": 0.8,
"B": 0.8
},
"risk_model_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
"risk_model_name": "euler_volatility",
"risk_model_version": "1.0.0",
"run_id": "rhbacktestrunv1:sha256:5036c771c44a2adade9ea590eee0d8cf824ff8f519fadd8ba424e5d914856386",
"scenario_digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"schema_version": "1.0.0",
"status": "ready"
}
}
+3 -11
View File
@@ -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)
+20 -58
View File
@@ -1,70 +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"] == 4 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"]
("researchhub.backtest-run-ref", "1.0.0"), )
("researchhub.backtest-evidence-manifest", "1.0.0"), )
("researchhub.portfolio-decision", "1.0.0"),
("researchhub.risk-assessment", "1.0.0"),
}
expected_paths = {
"researchhub.factor-definition": "src/quant_engine/factor_contracts.py",
"researchhub.factor-set-ref": "src/quant_engine/factor_contracts.py",
"researchhub.backtest-run-ref": "src/quant_engine/governed_pipeline.py",
"researchhub.backtest-evidence-manifest": "src/quant_engine/artifact.py",
"researchhub.portfolio-decision": "src/quant_engine/portfolio_risk_contracts.py",
"researchhub.risk-assessment": "src/quant_engine/portfolio_risk_contracts.py",
}
assert all(item["authority"] == "quant_engine" for item in spec["contracts"]["provides"])
assert {
item["contract_id"]: item["path"] for item in spec["contracts"]["provides"]
} == expected_paths
assert {
(item["contract_id"], item["version"])
for item in spec["contracts"]["consumes"]
} == {
("researchhub.dataset-snapshot", "1.0.0"),
("researchhub.data-foundation", "1.0.0"),
}
assert all(
item["authority"] == "researchhub.data"
for item in spec["contracts"]["consumes"]
)
assert spec["dependencies"] == []
capabilities = {item["id"]: item for item in spec["capabilities"]}
portfolio_contract = capabilities["portfolio-risk-computation-contracts"]
assert portfolio_contract["status"] == "operational"
summary = portfolio_contract["summary"].lower()
for term in ("receipt", "portfolio decisions", "risk assessments", "without"):
assert term in summary
for term in ("approval", "maker-checker", "publication", "paper", "live"):
assert term in prohibited
assert all(
command["required"] and not command["network"]
for command in spec["verification"]["commands"]
)
if __name__ == "__main__": if __name__ == "__main__":
test_module_spec_declares_pure_research_engine_boundary() unittest.main()
-976
View File
@@ -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__)
-309
View File
@@ -1,309 +0,0 @@
"""Stable research-run artifact contracts for downstream persistence."""
from __future__ import annotations
import json
from datetime import date
import pandas as pd
import pytest
from quant_engine.artifact import (
RESEARCH_ARTIFACT_SCHEMA_VERSION,
ResearchRunArtifact,
build_research_run_artifact,
)
from quant_engine.execution import ExecutionConfig
from quant_engine.research_pipeline import FactorBacktestResult, run_factor_backtest_research
from quant_engine.risk import CovarianceSnapshot
def _backtest_result() -> FactorBacktestResult:
dates = pd.date_range("2026-01-05", periods=4, freq="B")
scores = pd.DataFrame(
{"A": [2.0, 0.0], "B": [1.0, 3.0]},
index=dates[:2],
)
opens = pd.DataFrame(
{"A": [10.0, 10.0, 15.0, 15.0], "B": [20.0, 20.0, 20.0, 21.0]},
index=dates,
)
closes = pd.DataFrame(
{"A": [10.0, 12.0, 15.0, 15.0], "B": [20.0, 20.0, 18.0, 21.0]},
index=dates,
)
return run_factor_backtest_research(
scores,
opens,
closes,
top_k=1,
execution_price_field="open",
valuation_price_field="close",
initial_cash=1_000.0,
config=ExecutionConfig(
commission_bps=0,
stamp_tax_bps=0,
slippage_bps=0,
min_trade_amount=0,
),
)
def _build(
result: FactorBacktestResult,
*,
parameters: dict[str, object] | None = None,
risk_snapshots: dict[date, CovarianceSnapshot] | None = None,
) -> ResearchRunArtifact:
benchmark = pd.Series(
[0.0, 0.01, -0.01, 0.02],
index=result.returns.index,
name="benchmark_return",
)
return build_research_run_artifact(
result,
run_id="run-20260105-a",
strategy_id="alpha-top1",
strategy_name="Alpha Top 1",
strategy_version="1.0.0",
engine_version="1.2.0",
code_revision="3b1ad07",
data_snapshot_id="qtdb-pro-20260108-v1",
calendar="CN-A",
timezone="Asia/Shanghai",
started_at="2026-01-08T10:00:00+08:00",
finished_at="2026-01-08T10:01:00+08:00",
parameters=parameters or {"top_k": 1, "lag_sessions": 1},
benchmark_id="000300.SH",
benchmark_returns=benchmark,
risk_snapshots=risk_snapshots,
)
def test_research_artifact_projects_versioned_queryable_fact_tables() -> None:
result = _backtest_result()
artifact = _build(result)
assert artifact.schema_version == RESEARCH_ARTIFACT_SCHEMA_VERSION
assert artifact.run.loc[0, "run_id"] == "run-20260105-a"
assert artifact.run.loc[0, "benchmark_alignment_policy"] == "exact_session_index"
assert artifact.nav["run_id"].unique().tolist() == ["run-20260105-a"]
assert artifact.nav["pnl_pct"].tolist() == pytest.approx(result.returns.tolist())
assert artifact.nav["benchmark_return"].tolist() == pytest.approx(
[0.0, 0.01, -0.01, 0.02]
)
assert artifact.signals.columns.tolist() == [
"run_id",
"signal_date",
"execution_date",
"asset_id",
"factor_score",
"target_weight",
]
first_signal = artifact.signals[
artifact.signals["signal_date"] == result.factor_scores.index[0].date()
]
assert first_signal.set_index("asset_id").loc["A", "factor_score"] == 2.0
assert first_signal.set_index("asset_id").loc["A", "target_weight"] == 1.0
assert first_signal["execution_date"].unique().tolist() == [
result.schedule.signal_to_execution.iloc[0].date()
]
assert set(artifact.trades["side"]) == {"buy", "sell"}
assert artifact.trades["trade_id"].is_unique
assert artifact.trades["trade_id"].str.startswith("run-20260105-a:").all()
assert artifact.trades["signal_id"].str.startswith("run-20260105-a:signal:").all()
assert {"security", "cash"}.issubset(set(artifact.positions["asset_type"]))
assert artifact.positions.groupby("trade_date")["weight"].sum().tolist() == pytest.approx(
[1.0, 1.0, 1.0, 1.0]
)
assert set(artifact.attribution.columns) == {
"run_id",
"trade_date",
"asset_id",
"overnight",
"intraday",
"asset_total",
}
assert artifact.attribution_daily["residual"].abs().max() < 1e-12
assert artifact.risk.empty
assert artifact.risk.columns.tolist() == [
"run_id",
"trade_date",
"asset_id",
"weight",
"marginal_risk",
"component_risk",
"risk_contribution",
"covariance_snapshot_id",
"covariance_as_of_date",
"risk_measure",
"return_frequency",
"periods_per_year",
]
assert artifact.performance.loc[0, "n_trades"] == len(artifact.trades)
assert artifact.performance.loc[0, "ir"] == pytest.approx(
result.benchmark_stats(pd.Series([0.0, 0.01, -0.01, 0.02], index=result.returns.index))[
"information_ratio"
]
)
assert "sortino" in artifact.performance.columns
def test_research_artifact_projects_annualized_risk_from_actual_positions() -> None:
result = _backtest_result()
trade_date = result.position_weights.index[-1].date()
covariance = pd.DataFrame(
[[0.0001, 0.00002], [0.00002, 0.0004]],
index=["A", "B"],
columns=["A", "B"],
)
snapshot = CovarianceSnapshot(
snapshot_id="cov-20260107-v1",
as_of_date="2026-01-07",
covariance=covariance,
return_frequency="1d",
periods_per_year=252,
data_snapshot_id="qtdb-pro-20260108-v1",
)
artifact = _build(result, risk_snapshots={trade_date: snapshot})
risk = artifact.risk.set_index("asset_id")
expected_weights = result.position_weights.loc[pd.Timestamp(trade_date)]
assert artifact.schema_version == "1.1.0"
assert risk.index.tolist() == ["A", "B"]
assert risk["weight"].tolist() == pytest.approx(expected_weights.tolist())
assert risk["covariance_snapshot_id"].unique().tolist() == ["cov-20260107-v1"]
assert risk["covariance_as_of_date"].unique().tolist() == [date(2026, 1, 7)]
assert risk["risk_measure"].unique().tolist() == ["annualized_volatility"]
assert risk["return_frequency"].unique().tolist() == ["1d"]
assert risk["periods_per_year"].unique().tolist() == [252]
assert risk["component_risk"].sum() == pytest.approx((0.0004 * 252) ** 0.5)
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:
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="must not be after trade date"):
_build(
result,
risk_snapshots={
trade_date: CovarianceSnapshot(
snapshot_id="future-covariance",
as_of_date="2026-01-09",
covariance=covariance,
return_frequency="1d",
periods_per_year=252,
data_snapshot_id="qtdb-pro-20260108-v1",
)
},
)
with pytest.raises(ValueError, match="same asset labels"):
_build(
result,
risk_snapshots={
trade_date: CovarianceSnapshot(
snapshot_id="incomplete-universe",
as_of_date="2026-01-07",
covariance=covariance.loc[["B"], ["B"]],
return_frequency="1d",
periods_per_year=252,
data_snapshot_id="qtdb-pro-20260108-v1",
)
},
)
def test_research_artifact_serialization_and_hashes_are_deterministic() -> None:
result = _backtest_result()
first = _build(result, parameters={"top_k": 1, "lag_sessions": 1})
second = _build(result, parameters={"lag_sessions": 1, "top_k": 1})
assert first.run.loc[0, "config_hash"] == second.run.loc[0, "config_hash"]
assert first.content_sha256 == second.content_sha256
assert first.manifest() == second.manifest()
decoded = json.loads(first.canonical_json())
assert decoded["schema_version"] == RESEARCH_ARTIFACT_SCHEMA_VERSION
assert decoded["tables"]["nav"][0]["trade_date"] == "2026-01-05"
leaked_copy = first.nav
leaked_copy.loc[0, "nav"] = -999.0
assert first.nav.loc[0, "nav"] != -999.0
assert first.content_sha256 == second.content_sha256
def test_research_artifact_requires_complete_reproducibility_identity() -> None:
result = _backtest_result()
with pytest.raises(ValueError, match="code_revision"):
build_research_run_artifact(
result,
run_id="run-1",
strategy_id="alpha-top1",
strategy_name="Alpha Top 1",
strategy_version="1.0.0",
engine_version="1.2.0",
code_revision="",
data_snapshot_id="snapshot-1",
calendar="CN-A",
timezone="Asia/Shanghai",
started_at="2026-01-08T10:00:00+08:00",
finished_at="2026-01-08T10:01:00+08:00",
parameters={},
)
def test_research_artifact_requires_benchmark_identity_and_returns_together() -> None:
result = _backtest_result()
with pytest.raises(ValueError, match="benchmark_id and benchmark_returns"):
build_research_run_artifact(
result,
run_id="run-1",
strategy_id="alpha-top1",
strategy_name="Alpha Top 1",
strategy_version="1.0.0",
engine_version="1.2.0",
code_revision="3b1ad07",
data_snapshot_id="snapshot-1",
calendar="CN-A",
timezone="Asia/Shanghai",
started_at="2026-01-08T10:00:00+08:00",
finished_at="2026-01-08T10:01:00+08:00",
parameters={},
benchmark_id="000300.SH",
)
-773
View File
@@ -1,773 +0,0 @@
"""Backtest run-reference and closed-evidence contract conformance."""
from __future__ import annotations
import copy
import hashlib
import json
from dataclasses import replace
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import pandas as pd
import pytest
from quant_engine.artifact import (
BacktestEvidenceManifest,
EvidenceQualification,
RESEARCH_ARTIFACT_SCHEMA_VERSION,
ResearchRunArtifact,
build_backtest_evidence_manifest,
build_legacy_backtest_evidence_manifest,
build_research_run_artifact,
)
from quant_engine.execution import ExecutionConfig
from quant_engine.factor_contracts import (
ActorIdentity,
AvailabilityMode,
Causation,
DataFoundationEnvelope,
DatasetSnapshotEnvelope,
FactorInput,
FactorSetRef,
InputBinding,
OutputArtifactRef,
OutputCoverage,
OutputQuality,
OutputQualityCheck,
ProducerIdentity,
ViewAvailability,
canonical_json_bytes,
factor_definition_from_alpha158,
factor_input_schema_digest,
)
from quant_engine.governed_pipeline import (
BacktestContractError,
BacktestContractErrorCode,
BacktestRun,
BacktestRunRef,
)
from quant_engine.research_pipeline import FactorBacktestResult, run_factor_backtest_research
ROOT = Path(__file__).resolve().parents[1]
FACTOR_FIXTURE = ROOT / "tests" / "fixtures" / "factor-contracts-v1.golden.json"
BACKTEST_FIXTURE = ROOT / "tests" / "fixtures" / "backtest-evidence-v1.golden.json"
VIEW_REF_ID = "rhviewrefv1:sha256:bf776bcd26d940fafde1d650776a5505fb3fe8b5b068c351622bf2c42385629c"
VIEW_SCHEMA_DIGEST = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
CALENDAR_REVISION_ID = "rhcalv1:sha256:1f4ca22557063389badd669cf774bb35234066e847646682dccfe411e252a078"
ACTION_REVISION_ID = "rhcav1:sha256:0f947df29f152bfa2c6ab0da7a0d464670c7ad2526cd10f2a7939b42fee5c275"
PARAMETERS = {"lag_sessions": 1, "top_k": 1}
def _sha256(value: bytes) -> str:
return f"sha256:{hashlib.sha256(value).hexdigest()}"
def _accepted_authorities(
*,
factor_evaluation_at: str = "2026-01-03T11:00:00Z",
factor_computed_at: str = "2026-01-03T10:15:00Z",
factor_artifact_available_at: str = "2026-01-03T10:20:00Z",
factor_availability_mode: AvailabilityMode = AvailabilityMode.AS_AVAILABLE,
) -> tuple[
DatasetSnapshotEnvelope,
DataFoundationEnvelope,
FactorSetRef,
]:
fixture = json.loads(FACTOR_FIXTURE.read_text(encoding="utf-8"))
snapshot = DatasetSnapshotEnvelope.from_dict(fixture["dataset_snapshot"])
foundation = DataFoundationEnvelope.from_dict(fixture["data_foundation"])
factor_input = FactorInput("market", VIEW_SCHEMA_DIGEST, ("close", "volume"))
definition = factor_definition_from_alpha158(
"alpha_005",
version="1.0.0",
parameters={},
inputs=(factor_input,),
implementation_digest="sha256:" + "1" * 64,
input_schema_digest=factor_input_schema_digest((factor_input,)),
valid_from="2026-01-01T00:00:00.000000Z",
valid_until="2027-01-01T00:00:00Z",
warmup_sessions=10,
lag_sessions=1,
producer=ProducerIdentity("quant_engine", "1.0.0"),
code_revision="c" * 40,
)
output_schema_bytes = canonical_json_bytes(fixture["output_schema"])
output_content_bytes = canonical_json_bytes(fixture["output_content"])
artifact_ref = OutputArtifactRef.create(
schema_digest=_sha256(output_schema_bytes),
content_digest=_sha256(output_content_bytes),
)
factor_set = FactorSetRef.create(
definitions=(definition,),
dataset_snapshot=snapshot,
foundation=foundation,
selected_view_ref_ids=(VIEW_REF_ID,),
input_bindings=(
InputBinding(definition.definition_id, "market", VIEW_REF_ID, VIEW_SCHEMA_DIGEST),
),
view_availability=(
ViewAvailability(VIEW_REF_ID, "2026-01-02T23:50:00Z", "sha256:" + "2" * 64),
),
output_quality=OutputQuality(
"passed",
(OutputQualityCheck("finite_values", "passed", "sha256:" + "3" * 64),),
),
output_coverage=OutputCoverage(
"complete", 1, 1, "row", "alpha_005.cn_a", "sha256:" + "4" * 64
),
output_schema_bytes=output_schema_bytes,
output_content_bytes=output_content_bytes,
output_artifact_ref=artifact_ref,
availability_mode=factor_availability_mode,
evaluation_at=factor_evaluation_at,
computed_at=factor_computed_at,
artifact_available_at=factor_artifact_available_at,
producer=ProducerIdentity("quant_engine", "1.0.0"),
code_revision="c" * 40,
actor=ActorIdentity("service", "factor_worker_v1"),
correlation_id="research_run_001",
causation=Causation("foundation", foundation.foundation_id),
evidence_scope="synthetic_fixture",
decision_eligible=False,
)
return snapshot, foundation, factor_set
def _config_digest(parameters: dict[str, object] | None = None) -> str:
encoded = json.dumps(
PARAMETERS if parameters is None else parameters,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
return _sha256(encoded)
def _run_ref(**overrides: Any) -> BacktestRunRef:
snapshot, foundation, factor_set = _accepted_authorities()
arguments: dict[str, Any] = {
"dataset_snapshot": snapshot,
"foundation": foundation,
"factor_set": factor_set,
"universe_digest": "sha256:" + "5" * 64,
"trading_calendar_revision_ids": (CALENDAR_REVISION_ID,),
"corporate_action_revision_ids": (ACTION_REVISION_ID,),
"strategy_id": "alpha-top1",
"strategy_version": "1.0.0",
"strategy_digest": "sha256:" + "6" * 64,
"execution_model_version": "1.0.0",
"execution_model_digest": "sha256:" + "7" * 64,
"cost_model_version": "1.0.0",
"cost_model_digest": "sha256:" + "8" * 64,
"random_seed": 7,
"code_revision": "d" * 40,
"environment_lock_digest": "sha256:" + "9" * 64,
"configuration_digest": _config_digest(),
"evaluation_at": "2026-01-08T01:00:00Z",
"computed_at": "2026-01-08T02:00:00Z",
}
arguments.update(overrides)
return BacktestRunRef.create(**arguments)
def _backtest_result() -> FactorBacktestResult:
dates = pd.date_range("2026-01-05", periods=4, freq="B")
scores = pd.DataFrame({"A": [2.0, 0.0], "B": [1.0, 3.0]}, index=dates[:2])
opens = pd.DataFrame(
{"A": [10.0, 10.0, 15.0, 15.0], "B": [20.0, 20.0, 20.0, 21.0]},
index=dates,
)
closes = pd.DataFrame(
{"A": [10.0, 12.0, 15.0, 15.0], "B": [20.0, 20.0, 18.0, 21.0]},
index=dates,
)
return run_factor_backtest_research(
scores,
opens,
closes,
top_k=1,
execution_price_field="open",
valuation_price_field="close",
initial_cash=1_000.0,
config=ExecutionConfig(
commission_bps=0,
stamp_tax_bps=0,
slippage_bps=0,
min_trade_amount=0,
),
)
def _artifact(run_ref: BacktestRunRef, *, run_id: str | None = None) -> ResearchRunArtifact:
result = _backtest_result()
benchmark = pd.Series(
[0.0, 0.01, -0.01, 0.02],
index=result.returns.index,
name="benchmark_return",
)
return build_research_run_artifact(
result,
run_id=run_ref.run_id if run_id is None else run_id,
strategy_id=run_ref.strategy_id,
strategy_name="Alpha Top 1",
strategy_version=run_ref.strategy_version,
engine_version="1.2.0",
code_revision=run_ref.code_revision,
data_snapshot_id=run_ref.dataset_snapshot_id,
calendar="CN-A",
timezone="Asia/Shanghai",
started_at="2026-01-08T10:00:00+08:00",
finished_at="2026-01-08T10:01:00+08:00",
parameters=PARAMETERS,
benchmark_id="000300.SH",
benchmark_returns=benchmark,
)
def _assert_error(
error: pytest.ExceptionInfo[BacktestContractError],
code: BacktestContractErrorCode,
path: str,
) -> None:
assert error.value.code is code
assert error.value.path == path
def test_backtest_run_ref_is_deterministic_and_binds_only_opaque_authorities() -> None:
first = _run_ref()
second = _run_ref()
assert first == second
assert first.run_id.startswith("rhbacktestrunv1:sha256:")
assert first.replay_spec_digest.startswith("sha256:")
assert first.dataset_snapshot_id.startswith("rhdsv1:sha256:")
assert first.foundation_id.startswith("rhdfv1:sha256:")
assert first.factor_set_id.startswith("rhfactorsetv1:sha256:")
assert first.trading_calendar_revision_ids == (CALENDAR_REVISION_ID,)
assert first.corporate_action_revision_ids == (ACTION_REVISION_ID,)
assert first.replay_parent_run_id is None
assert first.replay_attempt == 0
assert first.replay_ancestor_run_ids == ()
snapshot, foundation, factor_set = _accepted_authorities()
assert BacktestRunRef.from_dict(
first.to_dict(),
dataset_snapshot=snapshot,
foundation=foundation,
factor_set=factor_set,
) == first
forbidden = ("latest", "locator", "uri", "credential", "provider", "broker")
assert not any(token in first.to_json().lower() for token in forbidden)
@pytest.mark.parametrize(
("field", "value"),
[
("universe_digest", "sha256:" + "a" * 64),
("strategy_digest", "sha256:" + "b" * 64),
("execution_model_digest", "sha256:" + "c" * 64),
("cost_model_digest", "sha256:" + "e" * 64),
("random_seed", 8),
("code_revision", "e" * 40),
("environment_lock_digest", "sha256:" + "f" * 64),
("configuration_digest", "sha256:" + "0" * 64),
("evaluation_at", "2026-01-08T01:00:01Z"),
("computed_at", "2026-01-08T02:00:01Z"),
],
)
def test_every_governed_run_input_mutation_changes_run_identity(
field: str,
value: object,
) -> None:
assert _run_ref(**{field: value}).run_id != _run_ref().run_id
def test_backtest_run_ref_rejects_unclosed_upstream_and_unsafe_scalars() -> None:
with pytest.raises(BacktestContractError) as wrong_calendar:
_run_ref(trading_calendar_revision_ids=())
_assert_error(
wrong_calendar,
BacktestContractErrorCode.INPUT_CLOSURE_VIOLATION,
"$.trading_calendar_revision_ids",
)
with pytest.raises(BacktestContractError) as wrong_action:
_run_ref(corporate_action_revision_ids=())
_assert_error(
wrong_action,
BacktestContractErrorCode.INPUT_CLOSURE_VIOLATION,
"$.corporate_action_revision_ids",
)
with pytest.raises(BacktestContractError) as bool_seed:
_run_ref(random_seed=True)
_assert_error(bool_seed, BacktestContractErrorCode.TYPE_ERROR, "$.random_seed")
with pytest.raises(BacktestContractError) as bad_revision:
_run_ref(code_revision="abc")
_assert_error(bad_revision, BacktestContractErrorCode.INVALID_FORMAT, "$.code_revision")
with pytest.raises(BacktestContractError) as bad_digest:
_run_ref(universe_digest="5" * 64)
_assert_error(bad_digest, BacktestContractErrorCode.INVALID_FORMAT, "$.universe_digest")
with pytest.raises(BacktestContractError) as lookahead:
_run_ref(computed_at="2026-01-08T00:59:59Z")
_assert_error(lookahead, BacktestContractErrorCode.TIME_ORDER_VIOLATION, "$.computed_at")
with pytest.raises(BacktestContractError) as factor_type:
_run_ref(factor_set="rhfactorsetv1:sha256:" + "0" * 64)
_assert_error(factor_type, BacktestContractErrorCode.TYPE_ERROR, "$.factor_set")
@pytest.mark.parametrize(
("factor_times", "expected_path"),
[
(
{
"factor_evaluation_at": "2026-01-08T01:00:01Z",
"factor_computed_at": "2026-01-08T00:59:59Z",
"factor_artifact_available_at": "2026-01-08T01:00:00Z",
},
"$.evaluation_at",
),
(
{
"factor_evaluation_at": "2026-01-03T11:00:00Z",
"factor_computed_at": "2026-01-08T01:00:00Z",
"factor_artifact_available_at": "2026-01-08T01:00:01Z",
"factor_availability_mode": AvailabilityMode.RETROSPECTIVE_REPLAY,
},
"$.evaluation_at",
),
],
)
def test_run_ref_evaluation_closes_factor_pit(
factor_times: dict[str, Any],
expected_path: str,
) -> None:
snapshot, foundation, factor_set = _accepted_authorities(**factor_times)
with pytest.raises(BacktestContractError) as lookahead:
_run_ref(
dataset_snapshot=snapshot,
foundation=foundation,
factor_set=factor_set,
)
_assert_error(
lookahead,
BacktestContractErrorCode.TIME_ORDER_VIOLATION,
expected_path,
)
def test_run_ref_rejects_aliases_locators_unsafe_integers_and_invalid_text() -> None:
with pytest.raises(BacktestContractError) as mutable_alias:
_run_ref(strategy_id="latest")
_assert_error(
mutable_alias,
BacktestContractErrorCode.INVALID_FORMAT,
"$.strategy_id",
)
with pytest.raises(BacktestContractError) as physical_uri:
_run_ref(execution_model_version="s3://model-bucket/current")
_assert_error(
physical_uri,
BacktestContractErrorCode.INVALID_FORMAT,
"$.execution_model_version",
)
with pytest.raises(BacktestContractError) as unsafe_seed:
_run_ref(random_seed=2**53)
_assert_error(
unsafe_seed,
BacktestContractErrorCode.INVALID_VALUE,
"$.random_seed",
)
with pytest.raises(BacktestContractError) as invalid_unicode:
_run_ref(strategy_id="\ud800")
_assert_error(
invalid_unicode,
BacktestContractErrorCode.INVALID_FORMAT,
"$.strategy_id",
)
run_ref = _run_ref()
mixed_keys = run_ref.to_dict()
mixed_keys[1] = "not-a-contract-key" # type: ignore[index]
snapshot, foundation, factor_set = _accepted_authorities()
with pytest.raises(BacktestContractError) as invalid_key:
BacktestRunRef.from_dict(
mixed_keys,
dataset_snapshot=snapshot,
foundation=foundation,
factor_set=factor_set,
)
_assert_error(invalid_key, BacktestContractErrorCode.TYPE_ERROR, "$")
@pytest.mark.parametrize(
"physical_id",
["db.table", "source_alpha", "wind.model", "qtdb_view", "bloomberg-signal"],
)
def test_run_ref_rejects_physical_terms_in_logical_ids(physical_id: str) -> None:
with pytest.raises(BacktestContractError) as physical:
_run_ref(strategy_id=physical_id)
_assert_error(
physical,
BacktestContractErrorCode.INVALID_FORMAT,
"$.strategy_id",
)
@pytest.mark.parametrize("version", ["1.0.0-.", "1.0.0-foo..bar", "1.0.0-01"])
def test_run_ref_requires_strict_semver_prerelease_identifiers(version: str) -> None:
with pytest.raises(BacktestContractError) as invalid:
_run_ref(strategy_version=version)
_assert_error(
invalid,
BacktestContractErrorCode.INVALID_FORMAT,
"$.strategy_version",
)
assert _run_ref(strategy_version="1.0.0-alpha.1").strategy_version == "1.0.0-alpha.1"
def test_replay_lineage_is_acyclic_and_cannot_claim_changed_inputs() -> None:
parent = _run_ref()
replay = _run_ref(
computed_at="2026-01-08T03:00:00Z",
parent=parent,
replay_reason="deterministic_reproduction",
replay_attempt=1,
)
assert replay.run_id != parent.run_id
assert replay.replay_spec_digest == parent.replay_spec_digest
assert replay.replay_parent_run_id == parent.run_id
assert replay.replay_ancestor_run_ids == (parent.run_id,)
with pytest.raises(BacktestContractError) as changed_input:
_run_ref(
universe_digest="sha256:" + "a" * 64,
computed_at="2026-01-08T03:00:00Z",
parent=parent,
replay_reason="changed_universe",
replay_attempt=1,
)
_assert_error(
changed_input,
BacktestContractErrorCode.LINEAGE_VIOLATION,
"$.replay_spec_digest",
)
with pytest.raises(BacktestContractError) as skipped_attempt:
_run_ref(
computed_at="2026-01-08T03:00:00Z",
parent=parent,
replay_reason="skipped_attempt",
replay_attempt=2,
)
_assert_error(
skipped_attempt,
BacktestContractErrorCode.LINEAGE_VIOLATION,
"$.replay_attempt",
)
def test_offline_research_manifest_closes_exact_existing_evidence_mapping() -> None:
run_ref = _run_ref()
artifact = _artifact(run_ref)
first = build_backtest_evidence_manifest(
run_ref,
artifact,
artifact_available_at="2026-01-08T02:05:00Z",
qualification=EvidenceQualification.CONTRACT_QUALIFIED,
)
second = build_backtest_evidence_manifest(
run_ref,
artifact,
artifact_available_at="2026-01-08T02:05:00Z",
qualification=EvidenceQualification.CONTRACT_QUALIFIED,
)
assert first == second
assert first.manifest_id.startswith("rhbacktestevidencev1:sha256:")
assert first.run_id == run_ref.run_id
assert first.profile == "offline_research_v1"
assert first.qualification is EvidenceQualification.CONTRACT_QUALIFIED
mapping = {
item.category: tuple(table.logical_name for table in item.tables)
for item in first.evidence
}
assert mapping == {
"run": ("run",),
"signal": ("signals",),
"fill": ("trades",),
"position_nav": ("positions", "nav"),
"performance": ("performance",),
"attribution": ("attribution", "attribution_daily"),
"risk_snapshot": ("risk",),
"replay": (),
}
assert "order" not in mapping
assert "rejection" not in mapping
risk = next(item for item in first.evidence if item.category == "risk_snapshot")
assert risk.tables[0].row_count == 0
assert risk.tables[0].schema_digest.startswith("sha256:")
changed_performance = artifact.performance
changed_performance.loc[0, "n_days"] += 1
changed_artifact = replace(artifact, _performance=changed_performance)
changed = build_backtest_evidence_manifest(
run_ref,
changed_artifact,
artifact_available_at="2026-01-08T02:05:00Z",
)
assert changed.manifest_id != first.manifest_id
assert run_ref.run_id == first.run_id == changed.run_id
def test_manifest_rejects_missing_mismatched_or_duplicate_evidence() -> None:
run_ref = _run_ref()
artifact = _artifact(run_ref)
with pytest.raises(BacktestContractError) as wrong_run:
build_backtest_evidence_manifest(
run_ref,
_artifact(run_ref, run_id="different-run"),
artifact_available_at="2026-01-08T02:05:00Z",
)
_assert_error(
wrong_run,
BacktestContractErrorCode.IDENTITY_MISMATCH,
"$.artifact.tables.run.run_id",
)
missing_signals = replace(artifact, _signals=None) # type: ignore[arg-type]
with pytest.raises(BacktestContractError) as missing_table:
build_backtest_evidence_manifest(
run_ref,
missing_signals,
artifact_available_at="2026-01-08T02:05:00Z",
)
_assert_error(
missing_table,
BacktestContractErrorCode.TYPE_ERROR,
"$.artifact.tables.signals",
)
with pytest.raises(BacktestContractError) as digest_mismatch:
build_backtest_evidence_manifest(
run_ref,
artifact,
artifact_available_at="2026-01-08T02:05:00Z",
expected_table_digests={"performance": "sha256:" + "0" * 64},
)
_assert_error(
digest_mismatch,
BacktestContractErrorCode.EVIDENCE_MISMATCH,
"$.artifact.tables.performance.content_digest",
)
manifest = build_backtest_evidence_manifest(
run_ref,
artifact,
artifact_available_at="2026-01-08T02:05:00Z",
)
duplicate = manifest.to_dict()
duplicate["evidence"].append(copy.deepcopy(duplicate["evidence"][0]))
with pytest.raises(BacktestContractError) as duplicate_category:
BacktestEvidenceManifest.from_dict(
duplicate,
backtest_run_ref=run_ref,
artifact=artifact,
)
_assert_error(
duplicate_category,
BacktestContractErrorCode.INVALID_VALUE,
"$.evidence[8].category",
)
def test_manifest_binds_supported_schema_and_has_collision_free_cell_encoding() -> None:
run_ref = _run_ref()
artifact = _artifact(run_ref)
with pytest.raises(BacktestContractError) as unsupported_schema:
build_backtest_evidence_manifest(
run_ref,
replace(artifact, schema_version="999.0.0"),
artifact_available_at="2026-01-08T02:05:00Z",
)
_assert_error(
unsupported_schema,
BacktestContractErrorCode.INVALID_VALUE,
"$.artifact.schema_version",
)
identities: set[str] = set()
for value in (float("nan"), float("inf"), float("-inf")):
performance = artifact.performance
performance.loc[0, "alpha"] = value
manifest = build_backtest_evidence_manifest(
run_ref,
replace(artifact, _performance=performance),
artifact_available_at="2026-01-08T02:05:00Z",
)
assert manifest.artifact_schema_version == RESEARCH_ARTIFACT_SCHEMA_VERSION
identities.add(manifest.manifest_id)
assert len(identities) == 3
content_digests: set[str] = set()
for value in (float("nan"), {"non_finite_float": "nan"}):
performance = artifact.performance.astype(object)
performance.at[0, "alpha"] = value
manifest = build_backtest_evidence_manifest(
run_ref,
replace(artifact, _performance=performance),
artifact_available_at="2026-01-08T02:05:00Z",
)
performance_entry = next(
entry for entry in manifest.evidence if entry.category == "performance"
)
content_digests.add(performance_entry.tables[0].content_digest)
assert len(content_digests) == 2
unsupported = artifact.performance.astype(object)
unsupported.loc[0, "alpha"] = object()
with pytest.raises(BacktestContractError) as unsupported_cell:
build_backtest_evidence_manifest(
run_ref,
replace(artifact, _performance=unsupported),
artifact_available_at="2026-01-08T02:05:00Z",
)
_assert_error(
unsupported_cell,
BacktestContractErrorCode.TYPE_ERROR,
"$.artifact.tables.performance.rows[0].alpha",
)
invalid_nested_key = artifact.performance.astype(object)
invalid_nested_key.at[0, "alpha"] = {"\ud800": "value"}
with pytest.raises(BacktestContractError) as invalid_utf8:
build_backtest_evidence_manifest(
run_ref,
replace(artifact, _performance=invalid_nested_key),
artifact_available_at="2026-01-08T02:05:00Z",
)
_assert_error(
invalid_utf8,
BacktestContractErrorCode.INVALID_FORMAT,
"$.artifact.tables.performance.rows[0].alpha.keys",
)
unsafe_integer = artifact.performance.astype(object)
unsafe_integer.loc[0, "alpha"] = 10**5000
with pytest.raises(BacktestContractError) as unsafe_cell:
build_backtest_evidence_manifest(
run_ref,
replace(artifact, _performance=unsafe_integer),
artifact_available_at="2026-01-08T02:05:00Z",
)
_assert_error(
unsafe_cell,
BacktestContractErrorCode.INVALID_VALUE,
"$.artifact.tables.performance.rows[0].alpha",
)
def test_artifact_canonical_content_has_typed_collision_free_cell_encoding() -> None:
run_ref = _run_ref()
artifact = _artifact(run_ref)
content_hashes: set[str] = set()
for value in (
float("nan"),
float("inf"),
float("-inf"),
{"non_finite_float": "nan"},
):
performance = artifact.performance.astype(object)
performance.at[0, "alpha"] = value
mutated = replace(artifact, _performance=performance)
content_hashes.add(mutated.content_sha256)
assert "non_finite_float" in mutated.canonical_json()
assert len(content_hashes) == 4
unsupported = artifact.performance.astype(object)
unsupported.loc[0, "alpha"] = object()
with pytest.raises(BacktestContractError) as unsupported_cell:
replace(artifact, _performance=unsupported).canonical_json()
_assert_error(
unsupported_cell,
BacktestContractErrorCode.TYPE_ERROR,
"$.tables.performance.rows[0].alpha",
)
def test_legacy_bridge_is_explicit_and_cannot_be_contract_qualified() -> None:
run_ref = _run_ref()
legacy_run = BacktestRun(
run_id="legacy-run-001",
dataset_snapshot_id=run_ref.dataset_snapshot_id,
factor_version_id="alpha_005@1.0.0",
strategy_version_id="alpha-top1@1.0.0",
code_revision=run_ref.code_revision,
config_hash=_config_digest().removeprefix("sha256:"),
created_at=datetime(2026, 1, 8, 2, 0, tzinfo=UTC),
)
artifact = _artifact(run_ref, run_id=legacy_run.run_id)
manifest = build_legacy_backtest_evidence_manifest(
legacy_run,
artifact,
artifact_available_at="2026-01-08T02:05:00Z",
)
assert manifest.qualification is EvidenceQualification.LEGACY_EXPLORATORY
assert manifest.run_id == legacy_run.run_id
assert manifest.backtest_run_ref is None
assert manifest.to_dict()["run_reference"]["kind"] == "legacy_backtest_run"
assert BacktestEvidenceManifest.from_dict(
manifest.to_dict(),
artifact=artifact,
) == manifest
with pytest.raises(BacktestContractError) as implicit_promotion:
build_backtest_evidence_manifest( # type: ignore[arg-type]
legacy_run,
artifact,
artifact_available_at="2026-01-08T02:05:00Z",
qualification=EvidenceQualification.CONTRACT_QUALIFIED,
)
_assert_error(
implicit_promotion,
BacktestContractErrorCode.TYPE_ERROR,
"$.backtest_run_ref",
)
def test_golden_contract_and_architecture_boundary() -> None:
run_ref = _run_ref()
artifact = _artifact(run_ref)
manifest = build_backtest_evidence_manifest(
run_ref,
artifact,
artifact_available_at="2026-01-08T02:05:00Z",
)
golden = json.loads(BACKTEST_FIXTURE.read_text(encoding="utf-8"))
table_digests = {
table.logical_name: table.content_digest
for item in manifest.evidence
for table in item.tables
}
assert golden == {
"run_id": run_ref.run_id,
"replay_spec_digest": run_ref.replay_spec_digest,
"manifest_id": manifest.manifest_id,
"evidence_digest": manifest.evidence_digest,
"table_content_digests": table_digests,
}
governed_source = (ROOT / "src" / "quant_engine" / "governed_pipeline.py").read_text(
encoding="utf-8"
)
artifact_source = (ROOT / "src" / "quant_engine" / "artifact.py").read_text(
encoding="utf-8"
)
assert "from quant_engine.artifact" not in governed_source
assert "BacktestRunRef" in governed_source
assert "BacktestEvidenceManifest" not in governed_source
assert "BacktestEvidenceManifest" in artifact_source
assert not (ROOT / "src" / "quant_engine" / "backtest_contracts.py").exists()
+3 -168
View File
@@ -7,12 +7,10 @@ import pandas as pd
import pytest import pytest
from quant_engine.data_adapter import ( from quant_engine.data_adapter import (
AssetReturnSnapshot,
add_vwap_proxy, add_vwap_proxy,
apply_adj_factor, apply_adj_factor,
load_qtdb_daily, load_qtdb_daily,
long_to_wide, long_to_wide,
prepare_asset_return_snapshot,
prepare_execution_inputs, prepare_execution_inputs,
prepare_stock_series, prepare_stock_series,
rename_tushare_columns, rename_tushare_columns,
@@ -300,176 +298,13 @@ def test_prepare_execution_inputs_missing_close_raises() -> None:
def test_prepare_execution_inputs_missing_selected_price_raises() -> None: def test_prepare_execution_inputs_missing_selected_price_raises() -> None:
df = pd.DataFrame({"stock_code": ["A"], "trade_date": ["2024-01-01"], "close": [10.0]}) df = pd.DataFrame(
{"stock_code": ["A"], "trade_date": ["2024-01-01"], "close": [10.0]}
)
with pytest.raises(ValueError, match="缺 open"): with pytest.raises(ValueError, match="缺 open"):
prepare_execution_inputs(df, price_col="open") prepare_execution_inputs(df, price_col="open")
# ── prepare_asset_return_snapshot ────────────────────────────
def _daily_prices() -> pd.DataFrame:
return pd.DataFrame(
{
"stock_code": ["B", "A", "B", "A", "B", "A"],
"trade_date": [
"2024-01-02",
"2024-01-01",
"2024-01-01",
"2024-01-03",
"2024-01-03",
"2024-01-02",
],
"close": [18.0, 10.0, 20.0, 12.1, 19.8, 11.0],
}
)
def test_prepare_asset_return_snapshot_is_stable_and_immutable_by_interface() -> None:
snapshot = prepare_asset_return_snapshot(
_daily_prices(),
source="qtdb_pro.hq_daily",
source_snapshot_id="hq-daily:2024-01-03:v1",
adjustment="qfq",
)
assert isinstance(snapshot, AssetReturnSnapshot)
assert snapshot.data_snapshot_id.startswith("asset-returns-v1:")
assert snapshot.source == "qtdb_pro.hq_daily"
assert snapshot.source_snapshot_id == "hq-daily:2024-01-03:v1"
assert snapshot.price_field == "close"
assert snapshot.adjustment == "qfq"
assert snapshot.return_method == "simple"
assert snapshot.start_date.isoformat() == "2024-01-01"
assert snapshot.end_date.isoformat() == "2024-01-03"
assert snapshot.sessions == 3
assert snapshot.assets == ("A", "B")
expected = pd.DataFrame(
{
"A": [np.nan, 0.1, 0.1],
"B": [np.nan, -0.1, 0.1],
},
index=pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]),
)
expected.index.name = "trade_date"
expected.columns.name = "stock_code"
pd.testing.assert_frame_equal(snapshot.returns, expected)
exposed = snapshot.returns
exposed.iloc[1, 0] = 999.0
assert snapshot.returns.iloc[1, 0] == pytest.approx(0.1)
def test_asset_return_snapshot_identity_is_order_independent_and_content_addressed() -> None:
kwargs = {
"source": "qtdb_pro.hq_daily",
"source_snapshot_id": "hq-daily:2024-01-03:v1",
"adjustment": "none",
}
baseline = prepare_asset_return_snapshot(_daily_prices(), **kwargs)
shuffled = prepare_asset_return_snapshot(
_daily_prices().sample(frac=1.0, random_state=7),
**kwargs,
)
changed_prices = _daily_prices().copy()
changed_prices.loc[changed_prices["close"] == 12.1, "close"] = 12.2
changed_content = prepare_asset_return_snapshot(changed_prices, **kwargs)
changed_source = prepare_asset_return_snapshot(
_daily_prices(),
source="qtdb_pro.hq_daily",
source_snapshot_id="hq-daily:2024-01-03:v2",
adjustment="none",
)
assert shuffled.data_snapshot_id == baseline.data_snapshot_id
assert changed_content.data_snapshot_id != baseline.data_snapshot_id
assert changed_source.data_snapshot_id != baseline.data_snapshot_id
def test_prepare_asset_return_snapshot_does_not_fill_missing_prices() -> None:
prices = _daily_prices()
prices.loc[
(prices["stock_code"] == "A") & (prices["trade_date"] == "2024-01-02"),
"close",
] = np.nan
snapshot = prepare_asset_return_snapshot(
prices,
source="qtdb_pro.hq_daily",
source_snapshot_id="hq-daily:missing-middle",
)
assert pd.isna(snapshot.returns.loc[pd.Timestamp("2024-01-02"), "A"])
assert pd.isna(snapshot.returns.loc[pd.Timestamp("2024-01-03"), "A"])
def test_prepare_asset_return_snapshot_rejects_duplicate_sessions() -> None:
duplicate = pd.concat([_daily_prices(), _daily_prices().iloc[[0]]], ignore_index=True)
with pytest.raises(ValueError, match="duplicate"):
prepare_asset_return_snapshot(
duplicate,
source="qtdb_pro.hq_daily",
source_snapshot_id="hq-daily:duplicate",
)
@pytest.mark.parametrize("invalid_price", [0.0, -1.0, np.inf])
def test_prepare_asset_return_snapshot_rejects_invalid_prices(invalid_price: float) -> None:
prices = _daily_prices()
prices.loc[0, "close"] = invalid_price
with pytest.raises(ValueError, match="positive finite"):
prepare_asset_return_snapshot(
prices,
source="qtdb_pro.hq_daily",
source_snapshot_id="hq-daily:invalid-price",
)
@pytest.mark.parametrize(
("source", "source_snapshot_id", "adjustment"),
[
("", "source-1", "none"),
("qtdb_pro.hq_daily", "", "none"),
("qtdb_pro.hq_daily", "source-1", ""),
],
)
def test_prepare_asset_return_snapshot_requires_explicit_identity_semantics(
source: str,
source_snapshot_id: str,
adjustment: str,
) -> None:
with pytest.raises(ValueError, match="must be non-empty"):
prepare_asset_return_snapshot(
_daily_prices(),
source=source,
source_snapshot_id=source_snapshot_id,
adjustment=adjustment,
)
def test_asset_return_snapshot_feeds_reproducible_covariance_lineage() -> None:
from quant_engine.risk import estimate_covariance_snapshot
market_snapshot = prepare_asset_return_snapshot(
_daily_prices(),
source="qtdb_pro.hq_daily",
source_snapshot_id="hq-daily:2024-01-03:v1",
)
covariance_snapshot = estimate_covariance_snapshot(
market_snapshot.returns,
as_of_date=market_snapshot.end_date,
lookback_sessions=3,
min_observations=2,
data_snapshot_id=market_snapshot.data_snapshot_id,
)
assert covariance_snapshot.data_snapshot_id == market_snapshot.data_snapshot_id
assert covariance_snapshot.snapshot_id.startswith("sample-cov-v1:")
# ── 端到端:长表 → 适配 → alpha158 + execution ────────────── # ── 端到端:长表 → 适配 → alpha158 + execution ──────────────
-938
View File
@@ -1,938 +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",
)
@pytest.mark.parametrize(
"invalid_definition_id",
[
{"unexpected": "object"},
["array"],
42,
True,
None,
],
)
def test_factor_set_ref_definition_ids_reject_non_string_types(
invalid_definition_id: Any,
) -> None:
factor_set = _factor_set()
definition = _golden_definition()
value = factor_set.to_dict()
value["definition_ids"] = [definition.definition_id, invalid_definition_id]
_reidentify(value, "factor_set_id", "rhfactorsetv1:sha256:")
snapshot, foundation = _snapshot_and_foundation()
with pytest.raises(FactorContractError) as error:
FactorSetRef.from_json(
canonical_json_bytes(value),
definitions=(definition,),
dataset_snapshot=snapshot,
foundation=foundation,
)
_assert_error(error, ContractErrorCode.TYPE_ERROR, "$.definition_ids[1]")
def test_factor_set_ref_definition_ids_still_reject_duplicate_strings() -> None:
factor_set = _factor_set()
definition = _golden_definition()
value = factor_set.to_dict()
value["definition_ids"] = [definition.definition_id, definition.definition_id]
_reidentify(value, "factor_set_id", "rhfactorsetv1:sha256:")
snapshot, foundation = _snapshot_and_foundation()
with pytest.raises(FactorContractError) as error:
FactorSetRef.from_json(
canonical_json_bytes(value),
definitions=(definition,),
dataset_snapshot=snapshot,
foundation=foundation,
)
_assert_error(error, ContractErrorCode.INVALID_VALUE, "$.definition_ids")
def test_factor_set_parent_requires_exact_identity_and_correlation() -> None:
parent = _factor_set()
child_arguments = _factor_set_arguments()
child_arguments.update(
output_content_bytes=canonical_json_bytes({"rows": [{"value": "0.250"}]}),
causation=Causation("factor_set", parent.factor_set_id),
parent=parent,
)
child_arguments["output_artifact_ref"] = OutputArtifactRef.create(
schema_digest=_sha256(child_arguments["output_schema_bytes"]),
content_digest=_sha256(child_arguments["output_content_bytes"]),
)
child = FactorSetRef.create(**child_arguments)
assert child.causation.id == parent.factor_set_id
missing_parent = child.to_dict()
snapshot, foundation = _snapshot_and_foundation()
with pytest.raises(FactorContractError) as missing_error:
FactorSetRef.from_dict(
missing_parent,
definitions=(_golden_definition(),),
dataset_snapshot=snapshot,
foundation=foundation,
)
_assert_error(missing_error, ContractErrorCode.LINEAGE_VIOLATION, "$.causation")
wrong_correlation = dict(child_arguments)
wrong_correlation["correlation_id"] = "different_run"
with pytest.raises(FactorContractError) as correlation_error:
FactorSetRef.create(**wrong_correlation)
_assert_error(correlation_error, ContractErrorCode.LINEAGE_VIOLATION, "$.correlation_id")
def test_legacy_bridge_is_explicit_lossy_and_preserves_all_four_historical_fields() -> None:
definition = _golden_definition()
legacy = FactorVersion(
factor_id="factor:demo-momentum",
version="1.0.0",
definition_sha256="b" * 64,
dataset_schema_version="1.0.0",
)
binding = LegacyFactorBinding.create(
definition=definition,
legacy_factor_id=legacy.factor_id,
legacy_version=legacy.version,
legacy_definition_sha256=legacy.definition_sha256,
legacy_dataset_schema_version=legacy.dataset_schema_version,
canonical_input_schema_digest=definition.input_schema_digest,
correspondence_evidence_digest="sha256:" + "5" * 64,
)
assert bind_legacy_factor(legacy, definition, binding) is definition
assert project_legacy_factor(definition, binding) == legacy
assert legacy.version_id == "factor:demo-momentum@1.0.0"
assert legacy.definition_sha256 != definition.definition_id.rsplit(":", maxsplit=1)[-1]
assert LegacyFactorBinding.from_json(binding.to_json(), definition=definition) == binding
mismatched = FactorVersion(
factor_id="factor:different",
version=legacy.version,
definition_sha256=legacy.definition_sha256,
dataset_schema_version=legacy.dataset_schema_version,
)
with pytest.raises(FactorContractError) as mismatch_error:
bind_legacy_factor(mismatched, definition, binding)
_assert_error(mismatch_error, ContractErrorCode.LEGACY_BINDING_MISMATCH, "$.binding")
def test_bare_legacy_factor_or_id_cannot_enter_factor_set_contract() -> None:
legacy = FactorVersion("factor:demo-momentum", "1.0.0", "b" * 64, "1.0.0")
arguments = _factor_set_arguments()
arguments["definitions"] = (legacy,)
with pytest.raises(FactorContractError) as legacy_error:
FactorSetRef.create(**arguments)
_assert_error(legacy_error, ContractErrorCode.TYPE_ERROR, "$.definitions[0]")
arguments["definitions"] = (legacy.version_id,)
with pytest.raises(FactorContractError) as id_error:
FactorSetRef.create(**arguments)
_assert_error(id_error, ContractErrorCode.TYPE_ERROR, "$.definitions[0]")
-338
View File
@@ -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 -23
View File
@@ -14,7 +14,6 @@ from quant_engine.metrics import (
calmar_ratio, calmar_ratio,
max_drawdown, max_drawdown,
sharpe_ratio, sharpe_ratio,
sortino_ratio,
summary, summary,
win_rate, win_rate,
) )
@@ -49,22 +48,9 @@ def test_zero_volatility_metrics_return_zero() -> None:
returns = pd.Series([0.0, 0.0, 0.0]) returns = pd.Series([0.0, 0.0, 0.0])
assert sharpe_ratio(returns) == 0.0 assert sharpe_ratio(returns) == 0.0
assert sortino_ratio(returns) == 0.0
assert calmar_ratio(returns) == 0.0 assert calmar_ratio(returns) == 0.0
def test_sortino_ratio_uses_all_sessions_for_downside_deviation() -> None:
returns = pd.Series([0.02, -0.01, 0.0, -0.03])
downside = np.minimum(returns.to_numpy(), 0.0)
downside_deviation = np.sqrt(np.mean(np.square(downside))) * np.sqrt(
TRADING_DAYS_PER_YEAR
)
assert sortino_ratio(returns) == pytest.approx(
annualized_return(returns) / downside_deviation
)
def test_max_drawdown_includes_loss_from_initial_capital() -> None: def test_max_drawdown_includes_loss_from_initial_capital() -> None:
returns = pd.Series([-0.20, 0.0]) returns = pd.Series([-0.20, 0.0])
@@ -94,15 +80,7 @@ def test_summary_aliases_match_canonical_fields() -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"metric", "metric",
[ [annualized_return, annualized_volatility, sharpe_ratio, max_drawdown, calmar_ratio, win_rate],
annualized_return,
annualized_volatility,
sharpe_ratio,
sortino_ratio,
max_drawdown,
calmar_ratio,
win_rate,
],
) )
def test_metrics_reject_non_series_input(metric) -> None: def test_metrics_reject_non_series_input(metric) -> None:
with pytest.raises(TypeError, match=r"expected pd\.Series"): with pytest.raises(TypeError, match=r"expected pd\.Series"):
File diff suppressed because it is too large Load Diff
-151
View File
@@ -8,164 +8,13 @@ import pytest
from quant_engine.risk import ( from quant_engine.risk import (
ComponentRiskResult, ComponentRiskResult,
CovarianceSnapshot,
component_var, component_var,
estimate_covariance_snapshot,
labeled_component_risk, labeled_component_risk,
marginal_risk_contribution, marginal_risk_contribution,
risk_contribution, risk_contribution,
) )
def test_estimate_covariance_snapshot_is_complete_case_and_reproducible() -> None:
dates = pd.date_range("2026-01-05", periods=6, freq="B")
returns = pd.DataFrame(
{
"A": [0.01, 0.02, 0.03, 0.04, 0.05, 99.0],
"B": [0.02, 0.01, np.nan, 0.03, 0.04, -99.0],
},
index=dates,
)
as_of = dates[4]
snapshot = estimate_covariance_snapshot(
returns,
as_of_date=as_of,
lookback_sessions=4,
min_observations=3,
data_snapshot_id="market-returns-20260109-v1",
return_frequency="1d",
periods_per_year=252,
)
expected_window = returns.loc[:as_of].tail(4)
expected = expected_window.dropna(how="any").cov()
pd.testing.assert_frame_equal(snapshot.covariance, expected)
assert snapshot.snapshot_id.startswith("sample-cov-v1:")
assert snapshot.as_of_date == as_of.date()
assert snapshot.method == "sample"
assert snapshot.window_start_date == expected_window.index[0].date()
assert snapshot.window_end_date == as_of.date()
assert snapshot.observations == 3
assert snapshot.lookback_sessions == 4
assert snapshot.missing_policy == "complete_case"
assert snapshot.data_snapshot_id == "market-returns-20260109-v1"
assert len(snapshot.input_sha256) == 64
future_changed = returns.copy()
future_changed.loc[dates[-1], :] = [1_000_000.0, -1_000_000.0]
repeated = estimate_covariance_snapshot(
future_changed,
as_of_date=as_of,
lookback_sessions=4,
min_observations=3,
data_snapshot_id="market-returns-20260109-v1",
return_frequency="1d",
periods_per_year=252,
)
assert repeated.snapshot_id == snapshot.snapshot_id
pd.testing.assert_frame_equal(repeated.covariance, snapshot.covariance)
def test_covariance_snapshot_identity_captures_data_and_estimator_contract() -> None:
dates = pd.date_range("2026-01-05", periods=4, freq="B")
returns = pd.DataFrame(
{"A": [0.01, 0.02, -0.01, 0.03], "B": [0.02, -0.01, 0.01, 0.04]},
index=dates,
)
base = estimate_covariance_snapshot(
returns,
as_of_date=dates[-1],
lookback_sessions=4,
min_observations=3,
data_snapshot_id="snapshot-a",
)
different_source = estimate_covariance_snapshot(
returns,
as_of_date=dates[-1],
lookback_sessions=4,
min_observations=3,
data_snapshot_id="snapshot-b",
)
assert base.snapshot_id != different_source.snapshot_id
assert base.covariance.equals(different_source.covariance)
def test_estimate_covariance_snapshot_rejects_ambiguous_or_insufficient_history() -> None:
dates = pd.date_range("2026-01-05", periods=4, freq="B")
returns = pd.DataFrame(
{"A": [0.01, np.nan, 0.03, 0.04], "B": [0.02, 0.01, np.nan, 0.03]},
index=dates,
)
with pytest.raises(ValueError, match="complete observations"):
estimate_covariance_snapshot(
returns,
as_of_date=dates[-1],
lookback_sessions=4,
min_observations=3,
data_snapshot_id="snapshot-a",
)
with pytest.raises(ValueError, match="strictly increasing"):
estimate_covariance_snapshot(
returns.iloc[::-1],
as_of_date=dates[-1],
lookback_sessions=4,
min_observations=2,
data_snapshot_id="snapshot-a",
)
def test_covariance_snapshot_is_validated_and_immutable_by_interface() -> None:
covariance = pd.DataFrame(
[[0.04, 0.01], [0.01, 0.09]],
index=["A", "B"],
columns=["A", "B"],
)
snapshot = CovarianceSnapshot(
snapshot_id="cov-20260107-v1",
as_of_date="2026-01-07",
covariance=covariance,
return_frequency="1d",
periods_per_year=252,
)
covariance.loc["A", "A"] = 999.0
leaked_copy = snapshot.covariance
leaked_copy.loc["B", "B"] = 999.0
assert snapshot.as_of_date == pd.Timestamp("2026-01-07").date()
assert snapshot.covariance.loc["A", "A"] == pytest.approx(0.04)
assert snapshot.covariance.loc["B", "B"] == pytest.approx(0.09)
@pytest.mark.parametrize(
("kwargs", "message"),
[
({"snapshot_id": ""}, "snapshot_id"),
({"return_frequency": ""}, "return_frequency"),
({"periods_per_year": 0}, "periods_per_year"),
],
)
def test_covariance_snapshot_rejects_incomplete_identity(
kwargs: dict[str, object],
message: str,
) -> None:
values: dict[str, object] = {
"snapshot_id": "cov-20260107-v1",
"as_of_date": "2026-01-07",
"covariance": pd.DataFrame([[0.04]], index=["A"], columns=["A"]),
"return_frequency": "1d",
"periods_per_year": 252,
}
values.update(kwargs)
with pytest.raises((TypeError, ValueError), match=message):
CovarianceSnapshot(**values)
def test_risk_contribution_sums_to_one_for_positive_portfolio_variance() -> None: def test_risk_contribution_sums_to_one_for_positive_portfolio_variance() -> None:
weights = np.array([0.5, 0.5]) weights = np.array([0.5, 0.5])
covariance = np.diag([1.0, 4.0]) covariance = np.diag([1.0, 4.0])
Generated
-408
View File
@@ -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" },
]