feat: preserve Quant OS target-package vertical slice
This commit is contained in:
+23
-9
@@ -5,9 +5,8 @@ Quant OS 是项目名;`quant60` 是第一版 A 股 Baseline 内核、CLI names
|
|||||||
可无人值守重放、聚宽登录后可直接回测、QMT 授权环境就绪后无需改源码即可完成
|
可无人值守重放、聚宽登录后可直接回测、QMT 授权环境就绪后无需改源码即可完成
|
||||||
回测/影子验证,并且所有 G1—G10 都由版本绑定的真实证据支持。
|
回测/影子验证,并且所有 G1—G10 都由版本绑定的真实证据支持。
|
||||||
|
|
||||||
当前结论仍为 `NOT_BASELINE_60`。最近一次标准库全量测试记录为
|
当前结论仍为 `NOT_BASELINE_60`。标准库全量测试证明代码合同,不证明真实
|
||||||
`Ran 224 tests`、`OK (skipped=6)`;六个 skip 是可选依赖/运行时路径。
|
平台、券商或合规 Gate;测试总数随合同补充会变化,不作为 Gate 分数。
|
||||||
该结果证明代码合同,不证明真实平台、券商或合规 Gate。
|
|
||||||
|
|
||||||
## Stage 1: 独立仓库与确定性执行
|
## Stage 1: 独立仓库与确定性执行
|
||||||
|
|
||||||
@@ -36,7 +35,14 @@ Quant OS 是项目名;`quant60` 是第一版 A 股 Baseline 内核、CLI names
|
|||||||
- openable / hold-only / sell-only / frozen / excluded 状态与原因码;
|
- openable / hold-only / sell-only / frozen / excluded 状态与原因码;
|
||||||
- 透明价量特征、可执行时钟标签、train-only 预处理;
|
- 透明价量特征、可执行时钟标签、train-only 预处理;
|
||||||
- purged walk-forward、deterministic Ridge challenger、IC/RankIC 报告;
|
- purged walk-forward、deterministic Ridge challenger、IC/RankIC 报告;
|
||||||
- 日期化费用、low/base/high 冲击、透明因子风险、约束优化与失败诊断;
|
- Ridge train-only 预处理、系数、标签、训练截止日与 lineage 的冻结 model
|
||||||
|
bundle;
|
||||||
|
- 一条真实可达的 synthetic vertical slice:Universe → Ridge Alpha →
|
||||||
|
60 日对角风险与日期化成本参与的 deterministic Portfolio → post-risk →
|
||||||
|
reference Execution,并输出五层 hash trace;
|
||||||
|
- 无账户股数的 `TargetPackageV1`,绑定 model/data/feature/risk/cost/
|
||||||
|
optimizer/config/source hash 与 T-close/T+1 双时钟;
|
||||||
|
- 独立组件仍包括透明因子风险、CVXPY optimizer 与 guarded TWAP/POV;
|
||||||
- JQData adapter 通过 `get_bars` 获取 raw OHLCV/money、factor、`pre_close`、
|
- JQData adapter 通过 `get_bars` 获取 raw OHLCV/money、factor、`pre_close`、
|
||||||
每日涨跌停/停牌字段,通过 `get_extras` 获取 PIT `is_st`,并逐交易日
|
每日涨跌停/停牌字段,通过 `get_extras` 获取 PIT `is_st`,并逐交易日
|
||||||
获取 PIT 指数成员;区间 `get_trade_days` 必须与 `get_all_trade_days`
|
获取 PIT 指数成员;区间 `get_trade_days` 必须与 `get_all_trade_days`
|
||||||
@@ -48,16 +54,21 @@ Quant OS 是项目名;`quant60` 是第一版 A 股 Baseline 内核、CLI names
|
|||||||
fail-closed 账户状态输入;
|
fail-closed 账户状态输入;
|
||||||
- synthetic research artifacts 和 snapshot decision artifacts 都有精确
|
- synthetic research artifacts 和 snapshot decision artifacts 都有精确
|
||||||
manifest verifier。
|
manifest verifier。
|
||||||
|
- 每层部署状态由 reachability contract 分开记录 implemented、unit tested、
|
||||||
|
local/JQ/QMT entrypoint reachable 与 real-platform observed。
|
||||||
|
|
||||||
**仍缺**:
|
**仍缺**:
|
||||||
|
|
||||||
- 授权 JQData 账户上的真实快照、长样本质量报告和许可记录;
|
- 授权 JQData 账户上的真实快照、长样本质量报告和许可记录;
|
||||||
- 完整 Security Master、基本面公告时点、公司行动与退市链;
|
- 完整 Security Master、基本面公告时点、公司行动与退市链;
|
||||||
- 真实样本风险/冲击/容量校准;
|
- 真实样本风险/冲击/容量校准;
|
||||||
- Ridge 的冻结 model bundle、真实 OOS 报告与跨引擎推理验证。
|
- 授权真实数据训练的 model bundle、真实 OOS 报告与跨引擎 TargetPackage
|
||||||
|
执行验证;
|
||||||
|
- 把 factor risk/CVXPY/TWAP-POV 从组件级测试提升到经过取舍的候选主链。
|
||||||
|
|
||||||
**状态**:功能实现和合成/fake-provider 合同验证完成,生产证据未完成。
|
**状态**:synthetic 五层 vertical slice 与交换合同完成;provider-data
|
||||||
Ridge 仍是 research challenger,不是已部署模型。
|
Baseline candidate、真实校准和生产证据未完成。Ridge 仍是 research
|
||||||
|
challenger,不是已部署模型。
|
||||||
|
|
||||||
## Stage 3: 平台回测与跨引擎证据
|
## Stage 3: 平台回测与跨引擎证据
|
||||||
|
|
||||||
@@ -68,6 +79,9 @@ Ridge 仍是 research challenger,不是已部署模型。
|
|||||||
|
|
||||||
- 本地 deterministic execution/research 与 manifest 重放;
|
- 本地 deterministic execution/research 与 manifest 重放;
|
||||||
- JoinQuant/QMT fake wrapper 合同和 mock parity exporter;
|
- JoinQuant/QMT fake wrapper 合同和 mock parity exporter;
|
||||||
|
- JoinQuant/QMT 的 TargetPackage 消费模式:本地上游四层保持唯一权威,
|
||||||
|
平台只做严格包验证、双时钟命中、账户/价格绑定与 execution;不存在
|
||||||
|
momentum fallback;
|
||||||
- 2026-07-26 使用生成 bundle 完成 2024 全年真实 JoinQuant hosted
|
- 2026-07-26 使用生成 bundle 完成 2024 全年真实 JoinQuant hosted
|
||||||
backtest,可见完成日志无 ERROR;该运行还促成 hosted namespace、
|
backtest,可见完成日志无 ERROR;该运行还促成 hosted namespace、
|
||||||
科创板保护价与 200 股最小申报的真实修复;
|
科创板保护价与 200 股最小申报的真实修复;
|
||||||
@@ -86,8 +100,8 @@ Qlib 两次 smoke 使用的是确定性两股票 synthetic fixture,性能没
|
|||||||
|
|
||||||
**仍缺**:
|
**仍缺**:
|
||||||
|
|
||||||
- 聚宽完整输入/plan/orders/fills 导出和相同输入本地 peer;
|
- 同一个 TargetPackage 的聚宽完整 package/plan/orders/fills 导出和本地 peer;
|
||||||
- 授权 QMT built-in 与 qmttools 回测导出;
|
- 同一个 TargetPackage 的授权 QMT built-in 与 qmttools 回测导出;
|
||||||
- 至少 20 个交易日的真实 QMT/broker shadow;
|
- 至少 20 个交易日的真实 QMT/broker shadow;
|
||||||
- 同一 canonical 输入的 L1—L5 差异报告。
|
- 同一 canonical 输入的 L1—L5 差异报告。
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ Quant OS 是项目名;`quant60` 是第一版 A 股 Baseline 的 Python
|
|||||||
本地与 Colab 可零账号运行工程 smoke;JQData 登录后可生成不可变快照并直接
|
本地与 Colab 可零账号运行工程 smoke;JQData 登录后可生成不可变快照并直接
|
||||||
运行 PIT 本地回测/目标决策;聚宽、QMT 与 Qlib 有各自明确的运行入口。
|
运行 PIT 本地回测/目标决策;聚宽、QMT 与 Qlib 有各自明确的运行入口。
|
||||||
|
|
||||||
> 当前状态仍是 `NOT_BASELINE_60`。代码存在、单测、synthetic、fake harness、
|
> 当前状态仍是 `NOT_BASELINE_60`。本地五层 vertical slice、单测、
|
||||||
> Qlib fixture 和单次真实聚宽 hosted smoke,都不能代替同输入跨引擎对比、
|
> synthetic、fake harness、Qlib fixture、真实聚宽 hosted 动量 smoke,以及
|
||||||
> 真实 QMT、券商影子盘、连续对账与合规证据。
|
> 单个 synthetic-research TargetPackage 的真实聚宽 **execution smoke**,
|
||||||
|
> 都不能代替授权真实数据 OOS、逐层同输入 parity、真实 QMT、券商影子盘、
|
||||||
|
> 连续对账与合规证据。
|
||||||
> 原文章复评为设计覆盖约 70/100、可实施规格 49/100、原实现证据 6/100、
|
> 原文章复评为设计覆盖约 70/100、可实施规格 49/100、原实现证据 6/100、
|
||||||
> G1—G10 为 0/10。详见
|
> G1—G10 为 0/10。详见
|
||||||
> [`docs/AUDIT_2026-07-25.md`](docs/AUDIT_2026-07-25.md) 与
|
> [`docs/AUDIT_2026-07-25.md`](docs/AUDIT_2026-07-25.md) 与
|
||||||
@@ -40,18 +42,21 @@ python3 tools/build_public_status.py --check
|
|||||||
| 路径 | 当前能做什么 | 已有证据 | 仍缺什么 |
|
| 路径 | 当前能做什么 | 已有证据 | 仍缺什么 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 本地 synthetic execution | 周度信号、T+1 开盘撮合、费用、涨跌停、部分成交、ledger、重放 | 标准库全量测试、确定性 smoke、manifest verifier | 不代表真实市场或投资价值 |
|
| 本地 synthetic execution | 周度信号、T+1 开盘撮合、费用、涨跌停、部分成交、ledger、重放 | 标准库全量测试、确定性 smoke、manifest verifier | 不代表真实市场或投资价值 |
|
||||||
| 本地 synthetic research | PIT/Universe、透明特征、purged walk-forward、Ridge、成本/风险、约束 target | research artifact 与 verifier | 仍是 synthetic;Ridge 尚未冻结为跨平台生产模型 |
|
| 本地 synthetic research | Universe → Ridge Alpha → 风险/成本参与的 Portfolio → post-risk → reference execution 五层链 | 冻结 model bundle、无股数 TargetPackage、逐层 hash trace 与 manifest verifier | 仍是 synthetic;当前风险是 60 日对角方差,尚无真实样本校准或投资价值 |
|
||||||
| JQData → 本地 | 原始价、复权因子、前收、涨跌停、停牌、PIT ST 状态与历史指数成员快照;PIT 本地回测与单日目标 | fake-provider 端到端回归测试、完整收盘日 fail-closed 合同 | 需要用户授权登录后保存真实数据运行;基本面/公司行动尚未全接 |
|
| JQData → 本地 | 原始价、复权因子、前收、涨跌停、停牌、PIT ST 状态与历史指数成员快照;PIT 本地回测与单日目标 | fake-provider 端到端回归测试、完整收盘日 fail-closed 合同 | 需要用户授权登录后保存真实数据运行;基本面/公司行动尚未全接 |
|
||||||
| Colab | 自动 clone、测试、bundle、两套 synthetic 流程;可选 JQData/Qlib | notebook 本地逐 cell 执行 | JQData/Qlib 可选 cell 需对应账号/运行时 |
|
| Colab | 自动 clone、测试、bundle、两套 synthetic 流程;可选 JQData/Qlib | notebook 本地逐 cell 执行 | JQData/Qlib 可选 cell 需对应账号/运行时 |
|
||||||
| 聚宽 hosted | 可上传的 Python 单文件,直接回测 portable baseline | 2024 全年、100 万资金的真实 hosted run 已完成且可见日志无 ERROR;另有 bundle/fake/mock 合约 | 仍需完整导出、相同输入本地 peer 与逐层差异报告 |
|
| 聚宽 hosted | 默认动量 smoke;显式注入 TargetPackage 后只消费本地 post-risk 权重并绑定平台账户/价格 | 动量模式已有一次 2024 全年 run;一个 synthetic-research TargetPackage 已真实命中、绑定 `day_open` 并产生 3 笔委托/成交 | 只观察 execution consumer;无授权真实长样本/OOS、上游四层 hosted 执行、QMT peer 或逐层真实数据 parity |
|
||||||
| QMT built-in/qmttools | 可上传/驱动的硬 backtest-only portable baseline;固定中证 500 benchmark、10% 参与率、PIT 成分/ST | Python 3.6 语法、fake/qmttools 合约 | 需要已授权 QMT 客户端、历史 ST 数据权限/正例探针与实跑导出 |
|
| QMT built-in/qmttools | 默认动量 smoke;TargetPackage 模式为 Python 3.6 backtest-only 消费器 | Python 3.6 语法、TargetPackage/fake/qmttools 合约 | 需要已授权 QMT 客户端、历史数据权限与 TargetPackage 实跑导出 |
|
||||||
| Qlib 0.9.7 | native momentum backtest;Alpha158/LightGBM CLI/Recorder 工作流 | Python 3.12 两条 fixture 已真实跑通,并保存表级 hash、行列/时间边界和重算指标 | synthetic 两股票没有投资价值;无真实 OOS、L3/L4 parity |
|
| Qlib 0.9.7 | native momentum backtest;Alpha158/LightGBM CLI/Recorder 工作流 | Python 3.12 两条 fixture 已真实跑通,并保存表级 hash、行列/时间边界和重算指标 | synthetic 两股票没有投资价值;无真实 OOS、L3/L4 parity |
|
||||||
| Tushare → Qlib | 只读盘点 completed Parquet、校验 checksum、构建/验证不可变 provider、运行本地回测 | 93 个 completed 文件已验证;早期未复权 provider 与两次 byte-identical momentum run | 下载仅到 1993-10;缺复权、真实 benchmark、历史成分/ST/停牌/涨跌停,不能进入生产 decision |
|
| Tushare → Qlib | 只读盘点 completed Parquet、校验 checksum、构建/验证不可变 provider、运行本地回测 | 93 个 completed 文件已验证;早期未复权 provider 与两次 byte-identical momentum run | 下载仅到 1993-10;缺复权、真实 benchmark、历史成分/ST/停牌/涨跌停,不能进入生产 decision |
|
||||||
| XtTrader shadow | 只读账户查询、HMAC 账户绑定、broker observation/snapshot、零下单 target-diff 计划与完整 verifier | fake broker/QMT 合约;所有 broker mutation 均不可达 | 需要授权 QMT 环境的账户合同探针、20 日影子、恢复/对账和券商确认 |
|
| XtTrader shadow | 只读账户查询、HMAC 账户绑定、broker observation/snapshot、零下单 target-diff 计划与完整 verifier | fake broker/QMT 合约;所有 broker mutation 均不可达 | 需要授权 QMT 环境的账户合同探针、20 日影子、恢复/对账和券商确认 |
|
||||||
|
|
||||||
跨四个引擎当前共同的、可冻结生产基线是
|
`portable-momentum-v1` 只是当前真实跑过聚宽的跨引擎连通性 smoke,不是
|
||||||
`portable-momentum-v1`。本地 Ridge 与 Qlib Alpha158/LightGBM 是研究候选,
|
60 分 Baseline。Baseline 的权威决策留在本地:冻结 Ridge model bundle,
|
||||||
在出现版本化 model bundle、真实 OOS 证据和跨引擎推理验证前,不会冒充已经部署。
|
完成五层链并发布无股数的 `TargetPackageV1`;聚宽/QMT 只做精确双时钟查找、
|
||||||
|
账户状态绑定和执行。当前 synthetic 包已经真实通过聚宽 execution consumer,
|
||||||
|
但这只证明平台边界可运行;尚未证明上游四层的真实数据有效性、QMT peer 或
|
||||||
|
跨引擎一致性。
|
||||||
|
|
||||||
## 一条命令验证本地工程
|
## 一条命令验证本地工程
|
||||||
|
|
||||||
@@ -90,6 +95,9 @@ artifacts/
|
|||||||
│ └── report.json
|
│ └── report.json
|
||||||
├── research-smoke/
|
├── research-smoke/
|
||||||
│ ├── manifest.json
|
│ ├── manifest.json
|
||||||
|
│ ├── model_bundle.json
|
||||||
|
│ ├── target_package.json
|
||||||
|
│ ├── pipeline.json
|
||||||
│ ├── signals.json
|
│ ├── signals.json
|
||||||
│ ├── targets.json
|
│ ├── targets.json
|
||||||
│ ├── forecasts.json
|
│ ├── forecasts.json
|
||||||
@@ -175,6 +183,11 @@ provider calendar 的唯一下一交易日 Asia/Shanghai `[09:00,09:30)`;它
|
|||||||
|
|
||||||
它仍是本地 fill model,不是聚宽/QMT 的真实成交证据。
|
它仍是本地 fill model,不是聚宽/QMT 的真实成交证据。
|
||||||
|
|
||||||
|
注意:现有 `snapshot-decision` 仍是 `portable-momentum-v1` 的 provider-data
|
||||||
|
路径,不能因为输入来自 JQData 就冒充 Ridge Baseline。完整候选链的权威交换物
|
||||||
|
是 `TargetPackageV1`;在真实数据训练、风险/成本校准与 OOS 冻结完成前,
|
||||||
|
`research-smoke/target_package.json` 只用于验证部署结构。
|
||||||
|
|
||||||
## Colab
|
## Colab
|
||||||
|
|
||||||
打开 [`notebooks/quant_os_colab.ipynb`](notebooks/quant_os_colab.ipynb)
|
打开 [`notebooks/quant_os_colab.ipynb`](notebooks/quant_os_colab.ipynb)
|
||||||
@@ -197,7 +210,7 @@ python3 tools/run_colab_notebook.py notebooks/quant_os_colab.ipynb --execute
|
|||||||
|
|
||||||
## 聚宽:直接上传回测
|
## 聚宽:直接上传回测
|
||||||
|
|
||||||
先构建:
|
不传目标包时,构建的是历史兼容的动量 smoke:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 tools/bundle_platforms.py
|
python3 tools/bundle_platforms.py
|
||||||
@@ -221,7 +234,7 @@ canonical 周时钟不是固定星期几:某 ISO 周第一实际交易日收
|
|||||||
作为限价保护;共享核心拒绝不足 200 股的普通科创板申报,只保留一次性卖清
|
作为限价保护;共享核心拒绝不足 200 股的普通科创板申报,只保留一次性卖清
|
||||||
不足 200 股余额的交易所例外。
|
不足 200 股余额的交易所例外。
|
||||||
|
|
||||||
2026-07-26 已用生成 bundle 完成一次真实聚宽全年 hosted backtest。运行记录、
|
2026-07-26 已用该动量 smoke bundle 完成一次真实聚宽全年 hosted backtest。运行记录、
|
||||||
bundle hash、指标、真实平台发现的问题和证据边界见
|
bundle hash、指标、真实平台发现的问题和证据边界见
|
||||||
[`docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md`](docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md)。
|
[`docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md`](docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md)。
|
||||||
该次运行的可见完成日志没有 `ERROR`,但尚未导出相同输入、plan、orders/fills,
|
该次运行的可见完成日志没有 `ERROR`,但尚未导出相同输入、plan、orders/fills,
|
||||||
@@ -238,13 +251,52 @@ bundle hash、指标、真实平台发现的问题和证据边界见
|
|||||||
单次 hosted smoke 是外部运行证据,但没有同输入跨引擎 manifest 时仍不算
|
单次 hosted smoke 是外部运行证据,但没有同输入跨引擎 manifest 时仍不算
|
||||||
真实聚宽 Gate 通过。
|
真实聚宽 Gate 通过。
|
||||||
|
|
||||||
|
要验证五层候选链,必须显式传入已验真的 TargetPackage;打包器不会自动从
|
||||||
|
动量回退或把 smoke 提升成 Baseline:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/bundle_platforms.py \
|
||||||
|
--target-package artifacts/research-smoke/target_package.json \
|
||||||
|
--output-dir dist/target-package
|
||||||
|
```
|
||||||
|
|
||||||
|
这个示例包来自 synthetic 数据,只能验证线路。目标包保存 post-risk **权重**
|
||||||
|
及 model/data/feature/risk/cost/optimizer/config/source hash,不保存账户股数。
|
||||||
|
聚宽在包声明的 `next_session` 开盘读取真实 equity、持仓、可卖数量和价格,
|
||||||
|
形成 execution plan;包缺失、双时钟不匹配、hash 篡改或重复决策都会停止,
|
||||||
|
不会现场改算 momentum。
|
||||||
|
|
||||||
|
此时应上传
|
||||||
|
[`dist/target-package/joinquant_strategy.py`](dist/target-package/joinquant_strategy.py),
|
||||||
|
并以同目录
|
||||||
|
[`bundle_manifest.json`](dist/target-package/bundle_manifest.json)
|
||||||
|
核对 package ID、package SHA 与 tape SHA;不要误传默认 smoke 文件。
|
||||||
|
|
||||||
|
2026-07-26,`TP-20240311-795dcfba` 已用 FINAL4 在真实 JoinQuant hosted
|
||||||
|
环境完成 2024-03-11 至 2024-03-13 的 execution smoke:3 月 11/13 日为
|
||||||
|
精确时钟 no-op,3 月 12 日按 `CURRENT_DATA_DAY_OPEN` 命中包,生成 3 笔
|
||||||
|
委托并全部成交。`600519.XSHG` 虽有 10% 权重,但 1693.94 元开盘价使一手
|
||||||
|
价值 169,394 元,高于 100,000 元目标名义金额,因而合法取整为 0 股。
|
||||||
|
|
||||||
|
- 脱敏机器记录:
|
||||||
|
[`evidence/joinquant/TP-20240311-795dcfba/2457a7c39a276e09e0fabf99e28978e1/run.json`](evidence/joinquant/TP-20240311-795dcfba/2457a7c39a276e09e0fabf99e28978e1/run.json)
|
||||||
|
- 证据边界:
|
||||||
|
[`docs/JOINQUANT_TARGET_PACKAGE_EVIDENCE_2026-07-26.md`](docs/JOINQUANT_TARGET_PACKAGE_EVIDENCE_2026-07-26.md)
|
||||||
|
|
||||||
|
该 run 的 `platform_evidence_class` 是 `real_platform_runtime`,但输入
|
||||||
|
`input_evidence_class` 仍是 `synthetic-research`;它只支持
|
||||||
|
`execution.real_platform_observed=true`。JoinQuant 没有重跑本地冻结的
|
||||||
|
Universe、Alpha、Portfolio、Risk,三日收益字段仅用于识别 smoke,
|
||||||
|
`performance_claim=false`,不计 G9 或 Baseline 60。
|
||||||
|
|
||||||
## QMT built-in 与 qmttools
|
## QMT built-in 与 qmttools
|
||||||
|
|
||||||
同一 bundler 生成:
|
同一 bundler 可生成动量 smoke 或显式 TargetPackage 消费器:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
dist/qmt_builtin_strategy.py
|
dist/qmt_builtin_strategy.py # 默认 momentum smoke
|
||||||
dist/bundle_manifest.json
|
dist/target-package/qmt_builtin_strategy.py # synthetic 候选线路调试
|
||||||
|
dist/target-package/bundle_manifest.json # 目标包与 bundle 身份
|
||||||
```
|
```
|
||||||
|
|
||||||
在 QMT Python 策略编辑器导入,选择 `1d` 与 **backtest**,主图/基准设为
|
在 QMT Python 策略编辑器导入,选择 `1d` 与 **backtest**,主图/基准设为
|
||||||
@@ -253,18 +305,20 @@ dist/bundle_manifest.json
|
|||||||
QMT 文档所述的 `ContextInfo` 用户属性回滚。它硬编码 backtest-only;参数和
|
QMT 文档所述的 `ContextInfo` 用户属性回滚。它硬编码 backtest-only;参数和
|
||||||
环境变量均不能把它提升成实盘 `passorder`。
|
环境变量均不能把它提升成实盘 `passorder`。
|
||||||
|
|
||||||
策略按原始毫秒 timetag 查询历史中证 500 成分,并通过
|
在动量 smoke 模式,策略按原始毫秒 timetag 查询历史中证 500 成分,并通过
|
||||||
`ContextInfo.get_his_st_data` 读取决策日 ST/*ST/PT 区间;接口缺失、格式异常
|
`ContextInfo.get_his_st_data` 读取决策日 ST/*ST/PT 区间;接口缺失、格式异常
|
||||||
或覆盖不足会阻止计划。首次真实验收还必须用一只已知历史 ST 股票做正例探针,
|
或覆盖不足会阻止计划。首次真实验收还必须用一只已知历史 ST 股票做正例探针,
|
||||||
证明客户端权限和本地历史 ST 数据确实可用,不能把空结果直接解释成“当天无
|
证明客户端权限和本地历史 ST 数据确实可用,不能把空结果直接解释成“当天无
|
||||||
ST”。
|
ST”。在 TargetPackage 模式,Universe/Alpha/Portfolio/Risk 已由本地包冻结;
|
||||||
|
QMT 不重算这些层,只在包的 signal close 命中后绑定账户和完成日线价格,
|
||||||
|
以 `quickTrade=0` 交给下一执行步。两种模式的证据不得混记。
|
||||||
|
|
||||||
Windows 上、已登录且安装券商分发 `xtquant` 的 qmttools 入口:
|
Windows 上、已登录且安装券商分发 `xtquant` 的 qmttools 入口:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:PYTHONPATH = "src;."
|
$env:PYTHONPATH = "src;."
|
||||||
python -m platforms.qmt_research_runner `
|
python -m platforms.qmt_research_runner `
|
||||||
dist/qmt_builtin_strategy.py `
|
dist/target-package/qmt_builtin_strategy.py `
|
||||||
--stock-code 000905.SH `
|
--stock-code 000905.SH `
|
||||||
--start 20240102 `
|
--start 20240102 `
|
||||||
--end 20241231 `
|
--end 20241231 `
|
||||||
@@ -509,6 +563,11 @@ gate_credit = []
|
|||||||
- `universe.py`:openable / hold-only / sell-only / frozen / excluded;
|
- `universe.py`:openable / hold-only / sell-only / frozen / excluded;
|
||||||
- `features.py`:透明价量特征、可执行时钟标签、train-only 预处理;
|
- `features.py`:透明价量特征、可执行时钟标签、train-only 预处理;
|
||||||
- `research.py` / `experiment.py`:purge/embargo、Ridge、OOS IC/RankIC;
|
- `research.py` / `experiment.py`:purge/embargo、Ridge、OOS IC/RankIC;
|
||||||
|
- `model_bundle.py`:冻结 train-only 预处理、Ridge 参数、标签与训练 lineage;
|
||||||
|
- `baseline_pipeline.py`:唯一五层本地主链和因果 hash trace;
|
||||||
|
- `target_package.py`:无股数、双时钟、可防篡改的 post-risk 权重合同;
|
||||||
|
- `release_reachability.py`:逐层区分 implemented、tested、entrypoint
|
||||||
|
reachable 与 real-platform observed;
|
||||||
- `factor_risk.py`:行业/风格暴露、EWMA 收缩因子协方差、特异风险、stress;
|
- `factor_risk.py`:行业/风格暴露、EWMA 收缩因子协方差、特异风险、stress;
|
||||||
- `costs.py`:日期化费率、最低佣金、low/base/high impact 与 capacity;
|
- `costs.py`:日期化费率、最低佣金、low/base/high impact 与 capacity;
|
||||||
- `optimizer.py`:deterministic fallback 与 lazy CVXPY 约束优化;
|
- `optimizer.py`:deterministic fallback 与 lazy CVXPY 约束优化;
|
||||||
@@ -519,22 +578,27 @@ gate_credit = []
|
|||||||
provider snapshot 到 PIT target 与本地历史回测;
|
provider snapshot 到 PIT target 与本地历史回测;
|
||||||
- `platforms/` / `adapters/`:聚宽、QMT、Qlib 与 XtTrader 边界。
|
- `platforms/` / `adapters/`:聚宽、QMT、Qlib 与 XtTrader 边界。
|
||||||
|
|
||||||
这些模块已有自动测试,聚宽 hosted 路径也已有一次真实运行;真实数据校准、
|
组件存在、单测通过、进入本地入口、进入平台入口和真实平台观察是五种不同
|
||||||
QMT/券商运行和运营证据仍分别计分。
|
事实。聚宽现有 TargetPackage run 只观察 execution consumer;它不证明
|
||||||
|
factor risk、CVXPY、TWAP/POV 或 Ridge 上游四层在 hosted 环境被调用。
|
||||||
|
|
||||||
## 仍未达到 60 的核心原因
|
## 仍未达到 60 的核心原因
|
||||||
|
|
||||||
- 基本面公告时点、完整公司行动、Security Master 与退市链尚未形成生产数据湖;
|
- 基本面公告时点、完整公司行动、Security Master 与退市链尚未形成生产数据湖;
|
||||||
- Ridge/LightGBM 候选尚未冻结成四引擎共同加载的 model bundle;
|
- Ridge 已能冻结成可重放 model bundle,但目前只有 synthetic 候选;
|
||||||
|
尚无授权真实长样本 OOS,也没有必要让 hosted 引擎重新加载训练栈;
|
||||||
- 风险协方差、冲击参数和容量尚未用真实长样本校准;
|
- 风险协方差、冲击参数和容量尚未用真实长样本校准;
|
||||||
- 聚宽已有真实 hosted run 但尚无完整同输入导出;QMT 尚无真实导出,QMT
|
- factor risk、CVXPY 和 guarded TWAP/POV 仍是组件级能力,尚未进入当前
|
||||||
|
Baseline vertical slice;
|
||||||
|
- 聚宽已有单包 TargetPackage execution 导出,但仍缺授权真实数据的多期
|
||||||
|
TargetPackage、本地/聚宽逐层同输入对账;QMT 尚无真实 peer 导出,QMT
|
||||||
历史 ST 权限正例、账户 query 合同、
|
历史 ST 权限正例、账户 query 合同、
|
||||||
20 日只读影子、回调/恢复/对账尚未形成真实证据;
|
20 日只读影子、回调/恢复/对账尚未形成真实证据;
|
||||||
- 自动 scheduler、日终对账服务、监控告警与 kill-switch 演练未形成连续证据;
|
- 自动 scheduler、日终对账服务、监控告警与 kill-switch 演练未形成连续证据;
|
||||||
- 实际券商程序化交易报告、权限、频率与软件要求尚未书面确认。
|
- 实际券商程序化交易报告、权限、频率与软件要求尚未书面确认。
|
||||||
|
|
||||||
因此当前是可以继续填真实证据的 Quant OS 生产候选框架,不是可直接投入资金的
|
因此当前是“已接通本地五层与平台 TargetPackage 边界的候选骨架”,不是已经
|
||||||
成品,也不构成投资建议。
|
达到 60 分或可直接投入资金的成品,也不构成投资建议。
|
||||||
|
|
||||||
## 安全与进一步文档
|
## 安全与进一步文档
|
||||||
|
|
||||||
@@ -542,7 +606,11 @@ QMT/券商运行和运营证据仍分别计分。
|
|||||||
行情数据一律不得进入 Git。
|
行情数据一律不得进入 Git。
|
||||||
|
|
||||||
- 架构:[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
- 架构:[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
||||||
|
- 五层真实结构与缺口:
|
||||||
|
[`docs/BASELINE_VERTICAL_SLICE.md`](docs/BASELINE_VERTICAL_SLICE.md)
|
||||||
- 平台矩阵:[`docs/PLATFORM_MATRIX.md`](docs/PLATFORM_MATRIX.md)
|
- 平台矩阵:[`docs/PLATFORM_MATRIX.md`](docs/PLATFORM_MATRIX.md)
|
||||||
|
- 聚宽 TargetPackage 证据:
|
||||||
|
[`docs/JOINQUANT_TARGET_PACKAGE_EVIDENCE_2026-07-26.md`](docs/JOINQUANT_TARGET_PACKAGE_EVIDENCE_2026-07-26.md)
|
||||||
- 部署:[`runbooks/PLATFORM_DEPLOYMENT.md`](runbooks/PLATFORM_DEPLOYMENT.md)
|
- 部署:[`runbooks/PLATFORM_DEPLOYMENT.md`](runbooks/PLATFORM_DEPLOYMENT.md)
|
||||||
- 安全:[`docs/SECURITY.md`](docs/SECURITY.md)
|
- 安全:[`docs/SECURITY.md`](docs/SECURITY.md)
|
||||||
- 故障:[`runbooks/INCIDENTS.md`](runbooks/INCIDENTS.md)
|
- 故障:[`runbooks/INCIDENTS.md`](runbooks/INCIDENTS.md)
|
||||||
|
|||||||
Vendored
+35
-10
@@ -2,25 +2,50 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"joinquant": {
|
"joinquant": {
|
||||||
"core": "src/quant60/portable_core.py",
|
"core": "src/quant60/portable_core.py",
|
||||||
"core_sha256": "af29e4e5e10705cf6aff89874394c1e80d3780f093c19d90ed53ef9d0188a934",
|
"core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||||
"effective_config_sha256": "3ef9b9efda9ec6cc5f57a5daca57cab3b68242e0368f18fdaaf9e249bdaa7873",
|
"effective_config_sha256": "d4ae2882e9b5d8d673e7c62c0dc54def7c20221c358ac091ee6529bf0413df0b",
|
||||||
"output": "dist/joinquant_strategy.py",
|
"output": "dist/joinquant_strategy.py",
|
||||||
"output_sha256": "22ddb0546beb3bd4e4bbedecb271452111324c3903e45be1f4ac3d39f38cce4f",
|
"output_sha256": "be4ca1d5764416b7c8e6c206121c0f0bd8219e7317679d79bc72a95492286b1a",
|
||||||
"wrapper": "platforms/joinquant_strategy.py",
|
"wrapper": "platforms/joinquant_strategy.py",
|
||||||
"wrapper_sha256": "b5a8e3b0a774a0f45f52f6bcccbe786e234913a1a4cf5b68b85e2bd989ee9294"
|
"wrapper_sha256": "0c672b64665b7c1d4409ba288b00d1608e976800af3a4f9a6467767ce4def592"
|
||||||
},
|
},
|
||||||
"qmt_builtin": {
|
"qmt_builtin": {
|
||||||
"core": "src/quant60/portable_core.py",
|
"core": "src/quant60/portable_core.py",
|
||||||
"core_sha256": "af29e4e5e10705cf6aff89874394c1e80d3780f093c19d90ed53ef9d0188a934",
|
"core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||||
"effective_config_sha256": "11f189ea5c9d4eaddeb6394dc342abc871941cb3395d7ff79aae1275ce2ad98f",
|
"effective_config_sha256": "7e4739937e4e99e5b58154f53271d8d3bfaed47d8d1d5adc747101babde827f5",
|
||||||
"output": "dist/qmt_builtin_strategy.py",
|
"output": "dist/qmt_builtin_strategy.py",
|
||||||
"output_sha256": "ad503e5ebde60a5deb0ec86334c04d4be2d77ab91d9607d4fe88e2119f22d5dc",
|
"output_sha256": "483877b7ea55c8e0f5e4e47b48a156b3add6cf0a8085011414c5c600913939b5",
|
||||||
"wrapper": "platforms/qmt_builtin_strategy.py",
|
"wrapper": "platforms/qmt_builtin_strategy.py",
|
||||||
"wrapper_sha256": "efb931bbd04d0abf0a576f2422a13403fd4d659dccf59f8170e8b13b1bafa640"
|
"wrapper_sha256": "a1e53dc22ebd9bb01be43675786151606bbed273d8ab883fb933461e379d2727"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"baseline_config": "configs/baseline.json",
|
"baseline_config": "configs/baseline.json",
|
||||||
"baseline_config_sha256": "939a7df386b06bc82869940572c35544e9361913b94d9e03027411e51b1006e1",
|
"baseline_config_sha256": "939a7df386b06bc82869940572c35544e9361913b94d9e03027411e51b1006e1",
|
||||||
"portable_core_sha256": "af29e4e5e10705cf6aff89874394c1e80d3780f093c19d90ed53ef9d0188a934",
|
"decision_mode": "portable_momentum_smoke",
|
||||||
"schema_version": 1
|
"joinquant_observation": {
|
||||||
|
"eod_api_policy": "best_effort_get_orders_get_trades",
|
||||||
|
"eod_chunk_characters": 1800,
|
||||||
|
"eod_chunk_encoding": "base64-canonical-json",
|
||||||
|
"events": [
|
||||||
|
"INIT",
|
||||||
|
"TARGET_PACKAGE_NOOP",
|
||||||
|
"TARGET_PACKAGE_HIT",
|
||||||
|
"PLATFORM_BINDING_INPUT",
|
||||||
|
"TARGET_PACKAGE_PLAN",
|
||||||
|
"SKIPPED_ORDER",
|
||||||
|
"ORDER_REQUEST",
|
||||||
|
"ORDER_RETURN",
|
||||||
|
"EOD_STATUS"
|
||||||
|
],
|
||||||
|
"fail_soft": true,
|
||||||
|
"prefix": "QUANT60_JOINQUANT_EVIDENCE_V1 ",
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"target_price_source": "CURRENT_DATA_DAY_OPEN"
|
||||||
|
},
|
||||||
|
"portable_core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||||
|
"schema_version": 1,
|
||||||
|
"target_package_count": 0,
|
||||||
|
"target_package_ids": [],
|
||||||
|
"target_package_sha256": [],
|
||||||
|
"target_package_tape_sha256": null
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1211
-10
File diff suppressed because it is too large
Load Diff
Vendored
+835
-1
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
# __PORTABLE_CORE_BUNDLE_START__
|
# __PORTABLE_CORE_BUNDLE_START__
|
||||||
# Inlined by tools/bundle_platforms.py; do not edit this region.
|
# Inlined by tools/bundle_platforms.py; do not edit this region.
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -26,6 +28,45 @@ _SHORT_EXCHANGES = {
|
|||||||
"XBSE": "BJ",
|
"XBSE": "BJ",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_TARGET_PACKAGE_FIELDS = {
|
||||||
|
"schema_version",
|
||||||
|
"package_type",
|
||||||
|
"package_id",
|
||||||
|
"decision_id",
|
||||||
|
"release",
|
||||||
|
"evidence_class",
|
||||||
|
"model_id",
|
||||||
|
"signal_as_of",
|
||||||
|
"next_session",
|
||||||
|
"model_sha256",
|
||||||
|
"data_sha256",
|
||||||
|
"feature_sha256",
|
||||||
|
"risk_sha256",
|
||||||
|
"cost_sha256",
|
||||||
|
"optimizer_sha256",
|
||||||
|
"config_sha256",
|
||||||
|
"source_sha256",
|
||||||
|
"target_weights",
|
||||||
|
"universe",
|
||||||
|
"constraints",
|
||||||
|
"package_sha256",
|
||||||
|
}
|
||||||
|
|
||||||
|
_TARGET_PACKAGE_HASH_FIELDS = (
|
||||||
|
"model_sha256",
|
||||||
|
"data_sha256",
|
||||||
|
"feature_sha256",
|
||||||
|
"risk_sha256",
|
||||||
|
"cost_sha256",
|
||||||
|
"optimizer_sha256",
|
||||||
|
"config_sha256",
|
||||||
|
"source_sha256",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TargetPackageLookupMiss(ValueError, LookupError):
|
||||||
|
"""An already verified tape has no exact requested decision."""
|
||||||
|
|
||||||
|
|
||||||
def _sum_numeric(values):
|
def _sum_numeric(values):
|
||||||
"""Sum numerics without trusting a hosted runtime's global ``sum`` name."""
|
"""Sum numerics without trusting a hosted runtime's global ``sum`` name."""
|
||||||
@@ -36,6 +77,15 @@ def _sum_numeric(values):
|
|||||||
return total
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def _any_true(values):
|
||||||
|
"""Return built-in ``any`` semantics without trusting hosted globals."""
|
||||||
|
|
||||||
|
for value in values:
|
||||||
|
if value:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _infer_exchange(code):
|
def _infer_exchange(code):
|
||||||
if code.startswith(("4", "8")):
|
if code.startswith(("4", "8")):
|
||||||
return "XBSE"
|
return "XBSE"
|
||||||
@@ -88,6 +138,396 @@ def convert_symbol(symbol, target="canonical"):
|
|||||||
return normalize_symbol(symbol, target)
|
return normalize_symbol(symbol, target)
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(value):
|
||||||
|
"""Match ``quant60.ledger.canonical_json`` for plain JSON values."""
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
value,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _json_copy(value, name):
|
||||||
|
try:
|
||||||
|
return json.loads(_canonical_json(value))
|
||||||
|
except (TypeError, ValueError) as error:
|
||||||
|
raise ValueError("%s must contain finite JSON values only" % name) from error
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_mapping(value, name, value_validator):
|
||||||
|
if not hasattr(value, "items"):
|
||||||
|
raise TypeError("%s must be a mapping" % name)
|
||||||
|
output = {}
|
||||||
|
for raw_symbol, raw_value in value.items():
|
||||||
|
symbol = normalize_symbol(raw_symbol)
|
||||||
|
if symbol in output:
|
||||||
|
raise ValueError(
|
||||||
|
"%s contains duplicate normalized symbol %s" % (name, symbol)
|
||||||
|
)
|
||||||
|
output[symbol] = value_validator(raw_value, "%s.%s" % (name, symbol))
|
||||||
|
return dict((symbol, output[symbol]) for symbol in sorted(output))
|
||||||
|
|
||||||
|
|
||||||
|
def _finite_non_negative(value, name):
|
||||||
|
if isinstance(value, bool):
|
||||||
|
raise ValueError("%s must be a finite non-negative number" % name)
|
||||||
|
try:
|
||||||
|
numeric = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValueError("%s must be a finite non-negative number" % name)
|
||||||
|
if not math.isfinite(numeric) or numeric < 0.0:
|
||||||
|
raise ValueError("%s must be a finite non-negative number" % name)
|
||||||
|
return numeric
|
||||||
|
|
||||||
|
|
||||||
|
def _non_negative_quantity(value, name):
|
||||||
|
numeric = _finite_non_negative(value, name)
|
||||||
|
if numeric != math.floor(numeric):
|
||||||
|
raise ValueError("%s must be a whole-share quantity" % name)
|
||||||
|
return int(numeric)
|
||||||
|
|
||||||
|
|
||||||
|
def _positive_price(value, name):
|
||||||
|
numeric = _finite_non_negative(value, name)
|
||||||
|
if numeric <= 0.0:
|
||||||
|
raise ValueError("%s must be positive" % name)
|
||||||
|
return numeric
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_non_empty_string(value, name):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(value, str) or not value.strip():
|
||||||
|
raise ValueError("%s must be a non-empty string when provided" % name)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256_hex(value, name, allow_none=False):
|
||||||
|
if value is None and allow_none:
|
||||||
|
return None
|
||||||
|
if (
|
||||||
|
not isinstance(value, str)
|
||||||
|
or len(value) != 64
|
||||||
|
or _any_true(
|
||||||
|
character not in "0123456789abcdef"
|
||||||
|
for character in value
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"%s must be a 64-character lowercase SHA-256 digest" % name
|
||||||
|
)
|
||||||
|
if value == "0" * 64:
|
||||||
|
raise ValueError("%s cannot be an all-zero placeholder" % name)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _reject_quantity_fields(value, path="$"):
|
||||||
|
if hasattr(value, "items"):
|
||||||
|
for raw_key, child in value.items():
|
||||||
|
key = str(raw_key)
|
||||||
|
normalized = key.lower().replace("-", "_")
|
||||||
|
if "quantity" in normalized or "quantities" in normalized:
|
||||||
|
raise ValueError(
|
||||||
|
"%s.%s: quantities do not belong in TargetPackageV1"
|
||||||
|
% (path, key)
|
||||||
|
)
|
||||||
|
_reject_quantity_fields(child, "%s.%s" % (path, key))
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for index, child in enumerate(value):
|
||||||
|
_reject_quantity_fields(child, "%s[%d]" % (path, index))
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_identifier(value, name):
|
||||||
|
if (
|
||||||
|
not isinstance(value, str)
|
||||||
|
or re.match(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", value) is None
|
||||||
|
):
|
||||||
|
raise ValueError("%s is not a canonical identifier" % name)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_target_package_clock(signal_as_of, next_session):
|
||||||
|
if (
|
||||||
|
not isinstance(signal_as_of, str)
|
||||||
|
or re.match(
|
||||||
|
r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T15:00:00\+08:00$",
|
||||||
|
signal_as_of,
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
):
|
||||||
|
raise ValueError("signal_as_of must be canonical China-market close")
|
||||||
|
if (
|
||||||
|
not isinstance(next_session, str)
|
||||||
|
or re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$", next_session) is None
|
||||||
|
):
|
||||||
|
raise ValueError("next_session must use canonical YYYY-MM-DD")
|
||||||
|
if next_session <= signal_as_of[:10]:
|
||||||
|
raise ValueError("next_session must follow signal_as_of")
|
||||||
|
|
||||||
|
|
||||||
|
def _target_package_content_sha256(package):
|
||||||
|
body = dict(package)
|
||||||
|
body.pop("package_sha256", None)
|
||||||
|
_reject_quantity_fields(body)
|
||||||
|
try:
|
||||||
|
payload = _canonical_json(body).encode("utf-8")
|
||||||
|
except (TypeError, ValueError) as error:
|
||||||
|
raise ValueError(
|
||||||
|
"target package must contain finite JSON values only"
|
||||||
|
) from error
|
||||||
|
return hashlib.sha256(payload).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_target_package(
|
||||||
|
package,
|
||||||
|
expected_signal_as_of=None,
|
||||||
|
expected_next_session=None,
|
||||||
|
expected_decision_id=None,
|
||||||
|
expected_package_sha256=None,
|
||||||
|
):
|
||||||
|
"""Verify the platform-critical subset of a sealed ``TargetPackageV1``.
|
||||||
|
|
||||||
|
This intentionally duplicates a small, Python-3.6-compatible safety
|
||||||
|
boundary from the richer local validator. Hosted runtimes must verify the
|
||||||
|
embedded package again instead of trusting the bundling process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not hasattr(package, "items"):
|
||||||
|
raise TypeError("target package must be a mapping")
|
||||||
|
candidate = _json_copy(package, "target package")
|
||||||
|
_reject_quantity_fields(candidate)
|
||||||
|
fields = set(candidate)
|
||||||
|
if fields != _TARGET_PACKAGE_FIELDS:
|
||||||
|
missing = sorted(_TARGET_PACKAGE_FIELDS - fields)
|
||||||
|
unknown = sorted(fields - _TARGET_PACKAGE_FIELDS)
|
||||||
|
raise ValueError(
|
||||||
|
"target package fields mismatch; missing=%s, unknown=%s"
|
||||||
|
% (missing, unknown)
|
||||||
|
)
|
||||||
|
if candidate["schema_version"] != "1.0":
|
||||||
|
raise ValueError("unsupported target package schema_version")
|
||||||
|
if candidate["package_type"] != "TargetPackageV1":
|
||||||
|
raise ValueError("unsupported target package type")
|
||||||
|
for name in (
|
||||||
|
"package_id",
|
||||||
|
"decision_id",
|
||||||
|
"release",
|
||||||
|
"evidence_class",
|
||||||
|
"model_id",
|
||||||
|
):
|
||||||
|
_verify_identifier(candidate[name], name)
|
||||||
|
_verify_target_package_clock(
|
||||||
|
candidate["signal_as_of"], candidate["next_session"]
|
||||||
|
)
|
||||||
|
for name in _TARGET_PACKAGE_HASH_FIELDS:
|
||||||
|
_sha256_hex(candidate[name], name)
|
||||||
|
_sha256_hex(candidate["package_sha256"], "package_sha256")
|
||||||
|
|
||||||
|
raw_weights = candidate["target_weights"]
|
||||||
|
if not hasattr(raw_weights, "items") or not raw_weights:
|
||||||
|
raise ValueError("target_weights must be a non-empty mapping")
|
||||||
|
weights = {}
|
||||||
|
for raw_symbol, raw_weight in raw_weights.items():
|
||||||
|
symbol = normalize_symbol(raw_symbol)
|
||||||
|
if symbol != raw_symbol:
|
||||||
|
raise ValueError(
|
||||||
|
"target_weights symbol must be canonical: %s" % raw_symbol
|
||||||
|
)
|
||||||
|
if symbol in weights:
|
||||||
|
raise ValueError("duplicate target weight for %s" % symbol)
|
||||||
|
weight = _finite_non_negative(
|
||||||
|
raw_weight, "target_weights.%s" % symbol
|
||||||
|
)
|
||||||
|
if weight > 1.0:
|
||||||
|
raise ValueError("target weight exceeds one for %s" % symbol)
|
||||||
|
weights[symbol] = weight
|
||||||
|
gross = math.fsum(weights.values())
|
||||||
|
if gross > 1.0 + 1e-12:
|
||||||
|
raise ValueError("target_weights gross exceeds one")
|
||||||
|
|
||||||
|
universe = candidate["universe"]
|
||||||
|
if not hasattr(universe, "items") or not universe:
|
||||||
|
raise ValueError("universe must be a non-empty mapping")
|
||||||
|
canonical_universe = {}
|
||||||
|
for raw_symbol, raw_state in universe.items():
|
||||||
|
symbol = normalize_symbol(raw_symbol)
|
||||||
|
if symbol != raw_symbol:
|
||||||
|
raise ValueError("universe symbol must be canonical: %s" % raw_symbol)
|
||||||
|
if symbol in canonical_universe:
|
||||||
|
raise ValueError("duplicate universe symbol %s" % symbol)
|
||||||
|
if not hasattr(raw_state, "items"):
|
||||||
|
raise ValueError("universe state must be a mapping for %s" % symbol)
|
||||||
|
state = raw_state.get("state")
|
||||||
|
if state not in (
|
||||||
|
"openable",
|
||||||
|
"hold_only",
|
||||||
|
"sell_only",
|
||||||
|
"frozen",
|
||||||
|
"excluded",
|
||||||
|
):
|
||||||
|
raise ValueError("unsupported universe state for %s" % symbol)
|
||||||
|
canonical_universe[symbol] = state
|
||||||
|
missing_universe = sorted(set(weights) - set(canonical_universe))
|
||||||
|
if missing_universe:
|
||||||
|
raise ValueError(
|
||||||
|
"target symbols are absent from universe: %s" % missing_universe
|
||||||
|
)
|
||||||
|
for symbol, weight in weights.items():
|
||||||
|
if canonical_universe[symbol] == "excluded" and weight > 1e-12:
|
||||||
|
raise ValueError("excluded symbol has positive weight: %s" % symbol)
|
||||||
|
|
||||||
|
constraints = candidate["constraints"]
|
||||||
|
if not hasattr(constraints, "items"):
|
||||||
|
raise ValueError("constraints must be a mapping")
|
||||||
|
if constraints.get("long_only") is not True:
|
||||||
|
raise ValueError("TargetPackageV1 must be long-only")
|
||||||
|
declared_gross = _finite_non_negative(
|
||||||
|
constraints.get("actual_gross_weight"),
|
||||||
|
"constraints.actual_gross_weight",
|
||||||
|
)
|
||||||
|
max_gross = _finite_non_negative(
|
||||||
|
constraints.get("max_gross_weight"),
|
||||||
|
"constraints.max_gross_weight",
|
||||||
|
)
|
||||||
|
max_single = _finite_non_negative(
|
||||||
|
constraints.get("max_single_weight"),
|
||||||
|
"constraints.max_single_weight",
|
||||||
|
)
|
||||||
|
if abs(declared_gross - gross) > 1e-12:
|
||||||
|
raise ValueError("declared gross does not equal target_weights gross")
|
||||||
|
if gross > max_gross + 1e-12 or max_gross > 1.0:
|
||||||
|
raise ValueError("target_weights exceed max_gross_weight")
|
||||||
|
if max_single <= 0.0 or max_single > 1.0:
|
||||||
|
raise ValueError("invalid max_single_weight")
|
||||||
|
if _any_true(
|
||||||
|
weight > max_single + 1e-12 for weight in weights.values()
|
||||||
|
):
|
||||||
|
raise ValueError("target_weights exceed max_single_weight")
|
||||||
|
model_claim = " ".join(
|
||||||
|
(
|
||||||
|
candidate["release"],
|
||||||
|
candidate["evidence_class"],
|
||||||
|
)
|
||||||
|
).lower()
|
||||||
|
if (
|
||||||
|
"baseline" in model_claim
|
||||||
|
and "portable-momentum" in candidate["model_id"].lower()
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"portable-momentum cannot claim a baseline target package"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_hash = _target_package_content_sha256(candidate)
|
||||||
|
if candidate["package_sha256"] != expected_hash:
|
||||||
|
raise ValueError("target package content hash mismatch")
|
||||||
|
if (
|
||||||
|
expected_signal_as_of is not None
|
||||||
|
and candidate["signal_as_of"] != expected_signal_as_of
|
||||||
|
):
|
||||||
|
raise ValueError("target package signal_as_of does not match request")
|
||||||
|
if (
|
||||||
|
expected_next_session is not None
|
||||||
|
and candidate["next_session"] != expected_next_session
|
||||||
|
):
|
||||||
|
raise ValueError("target package next_session does not match request")
|
||||||
|
if (
|
||||||
|
expected_decision_id is not None
|
||||||
|
and candidate["decision_id"] != expected_decision_id
|
||||||
|
):
|
||||||
|
raise ValueError("target package decision_id does not match request")
|
||||||
|
if expected_package_sha256 is not None:
|
||||||
|
_sha256_hex(
|
||||||
|
expected_package_sha256,
|
||||||
|
"expected_package_sha256",
|
||||||
|
)
|
||||||
|
if candidate["package_sha256"] != expected_package_sha256:
|
||||||
|
raise ValueError("target package digest does not match request")
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
def target_package_tape_sha256(packages):
|
||||||
|
"""Hash the exact ordered canonical JSON package array."""
|
||||||
|
|
||||||
|
if not isinstance(packages, (list, tuple)):
|
||||||
|
raise TypeError("target package tape must be a list or tuple")
|
||||||
|
try:
|
||||||
|
payload = _canonical_json(list(packages)).encode("utf-8")
|
||||||
|
except (TypeError, ValueError) as error:
|
||||||
|
raise ValueError(
|
||||||
|
"target package tape must contain finite JSON values only"
|
||||||
|
) from error
|
||||||
|
return hashlib.sha256(payload).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_target_package_exact(
|
||||||
|
packages,
|
||||||
|
signal_as_of,
|
||||||
|
next_session,
|
||||||
|
decision_id=None,
|
||||||
|
expected_tape_sha256=None,
|
||||||
|
expected_count=None,
|
||||||
|
):
|
||||||
|
"""Verify a complete tape, then return one exact dual-clock decision."""
|
||||||
|
|
||||||
|
if not isinstance(packages, (list, tuple)) or not packages:
|
||||||
|
raise ValueError("target package tape must be a non-empty list or tuple")
|
||||||
|
_verify_target_package_clock(signal_as_of, next_session)
|
||||||
|
decision_id = _optional_non_empty_string(decision_id, "decision_id")
|
||||||
|
if expected_count is not None:
|
||||||
|
if (
|
||||||
|
isinstance(expected_count, bool)
|
||||||
|
or not isinstance(expected_count, int)
|
||||||
|
or expected_count < 0
|
||||||
|
):
|
||||||
|
raise ValueError("expected_count must be a non-negative integer")
|
||||||
|
if len(packages) != expected_count:
|
||||||
|
raise ValueError("target package tape count mismatch")
|
||||||
|
if expected_tape_sha256 is not None:
|
||||||
|
_sha256_hex(expected_tape_sha256, "expected_tape_sha256")
|
||||||
|
if target_package_tape_sha256(packages) != expected_tape_sha256:
|
||||||
|
raise ValueError("target package tape digest mismatch")
|
||||||
|
|
||||||
|
verified = []
|
||||||
|
package_ids = set()
|
||||||
|
decision_ids = set()
|
||||||
|
clock_keys = set()
|
||||||
|
for raw_package in packages:
|
||||||
|
package = verify_target_package(raw_package)
|
||||||
|
clock_key = (package["signal_as_of"], package["next_session"])
|
||||||
|
if package["package_id"] in package_ids:
|
||||||
|
raise ValueError("duplicate target package_id")
|
||||||
|
if package["decision_id"] in decision_ids:
|
||||||
|
raise ValueError("duplicate target decision_id")
|
||||||
|
if clock_key in clock_keys:
|
||||||
|
raise ValueError("duplicate target package clock")
|
||||||
|
package_ids.add(package["package_id"])
|
||||||
|
decision_ids.add(package["decision_id"])
|
||||||
|
clock_keys.add(clock_key)
|
||||||
|
verified.append(package)
|
||||||
|
|
||||||
|
matches = [
|
||||||
|
package
|
||||||
|
for package in verified
|
||||||
|
if package["signal_as_of"] == signal_as_of
|
||||||
|
and package["next_session"] == next_session
|
||||||
|
]
|
||||||
|
if not matches:
|
||||||
|
raise TargetPackageLookupMiss(
|
||||||
|
"no target package for exact signal_as_of/next_session clock"
|
||||||
|
)
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise ValueError("ambiguous exact target package decision")
|
||||||
|
if (
|
||||||
|
decision_id is not None
|
||||||
|
and matches[0]["decision_id"] != decision_id
|
||||||
|
):
|
||||||
|
raise ValueError("exact target package decision_id mismatch")
|
||||||
|
return _json_copy(matches[0], "target package")
|
||||||
|
|
||||||
|
|
||||||
def momentum_score(prices, lookback=20, skip=0):
|
def momentum_score(prices, lookback=20, skip=0):
|
||||||
"""Return trailing simple momentum, or ``None`` if history is insufficient.
|
"""Return trailing simple momentum, or ``None`` if history is insufficient.
|
||||||
|
|
||||||
@@ -314,6 +754,108 @@ def order_deltas(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def build_target_weight_plan(
|
||||||
|
target_weights,
|
||||||
|
prices,
|
||||||
|
current,
|
||||||
|
sellable,
|
||||||
|
equity,
|
||||||
|
cash_buffer=0.02,
|
||||||
|
lot_size=100,
|
||||||
|
decision_id=None,
|
||||||
|
package_sha256=None,
|
||||||
|
):
|
||||||
|
"""Bind canonical post-risk weights to one account without recomputing Alpha.
|
||||||
|
|
||||||
|
``prices`` must cover exactly every target or currently managed symbol.
|
||||||
|
Missing prices fail closed even for a held symbol whose desired weight is
|
||||||
|
zero. A missing ``sellable`` entry is safe and means zero sellable shares.
|
||||||
|
"""
|
||||||
|
|
||||||
|
weights = _canonical_mapping(
|
||||||
|
target_weights,
|
||||||
|
"target_weights",
|
||||||
|
_finite_non_negative,
|
||||||
|
)
|
||||||
|
gross = math.fsum(weights.values())
|
||||||
|
if _any_true(weight > 1.0 for weight in weights.values()):
|
||||||
|
raise ValueError("target weight must not exceed one")
|
||||||
|
if gross > 1.0 + 1e-12:
|
||||||
|
raise ValueError("target weight gross must not exceed one")
|
||||||
|
|
||||||
|
canonical_current = _canonical_mapping(
|
||||||
|
current,
|
||||||
|
"current",
|
||||||
|
_non_negative_quantity,
|
||||||
|
)
|
||||||
|
canonical_sellable = _canonical_mapping(
|
||||||
|
sellable,
|
||||||
|
"sellable",
|
||||||
|
_non_negative_quantity,
|
||||||
|
)
|
||||||
|
unknown_sellable = sorted(set(canonical_sellable) - set(canonical_current))
|
||||||
|
if unknown_sellable:
|
||||||
|
raise ValueError(
|
||||||
|
"sellable contains symbols absent from current: %s"
|
||||||
|
% unknown_sellable
|
||||||
|
)
|
||||||
|
for symbol, quantity in canonical_sellable.items():
|
||||||
|
if quantity > canonical_current[symbol]:
|
||||||
|
raise ValueError(
|
||||||
|
"sellable exceeds current quantity for %s" % symbol
|
||||||
|
)
|
||||||
|
|
||||||
|
canonical_prices = _canonical_mapping(
|
||||||
|
prices,
|
||||||
|
"prices",
|
||||||
|
_positive_price,
|
||||||
|
)
|
||||||
|
managed_symbols = set(weights) | set(canonical_current)
|
||||||
|
price_symbols = set(canonical_prices)
|
||||||
|
missing_prices = sorted(managed_symbols - price_symbols)
|
||||||
|
extra_prices = sorted(price_symbols - managed_symbols)
|
||||||
|
if missing_prices or extra_prices:
|
||||||
|
if missing_prices:
|
||||||
|
raise KeyError(
|
||||||
|
"prices do not cover managed symbols: missing=%s"
|
||||||
|
% missing_prices
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
"prices contain unmanaged symbols: extra=%s" % extra_prices
|
||||||
|
)
|
||||||
|
|
||||||
|
complete_weights = dict(
|
||||||
|
(symbol, weights.get(symbol, 0.0))
|
||||||
|
for symbol in sorted(managed_symbols)
|
||||||
|
)
|
||||||
|
targets = target_quantities(
|
||||||
|
complete_weights,
|
||||||
|
canonical_prices,
|
||||||
|
equity,
|
||||||
|
lot_size,
|
||||||
|
cash_buffer,
|
||||||
|
)
|
||||||
|
orders = order_deltas(
|
||||||
|
targets,
|
||||||
|
canonical_current,
|
||||||
|
canonical_sellable,
|
||||||
|
lot_size,
|
||||||
|
)
|
||||||
|
decision_id = _optional_non_empty_string(decision_id, "decision_id")
|
||||||
|
package_sha256 = _sha256_hex(
|
||||||
|
package_sha256,
|
||||||
|
"package_sha256",
|
||||||
|
allow_none=True,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"decision_id": decision_id,
|
||||||
|
"package_sha256": package_sha256,
|
||||||
|
"weights": complete_weights,
|
||||||
|
"targets": targets,
|
||||||
|
"orders": orders,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_rebalance_plan(
|
def build_rebalance_plan(
|
||||||
price_history,
|
price_history,
|
||||||
current,
|
current,
|
||||||
@@ -359,14 +901,19 @@ def build_rebalance_plan(
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"build_rebalance_plan",
|
"build_rebalance_plan",
|
||||||
|
"build_target_weight_plan",
|
||||||
"capped_target_weights",
|
"capped_target_weights",
|
||||||
"convert_symbol",
|
"convert_symbol",
|
||||||
|
"lookup_target_package_exact",
|
||||||
"momentum_score",
|
"momentum_score",
|
||||||
"normalize_symbol",
|
"normalize_symbol",
|
||||||
"order_deltas",
|
"order_deltas",
|
||||||
"rank_momentum",
|
"rank_momentum",
|
||||||
"round_board_lot",
|
"round_board_lot",
|
||||||
|
"TargetPackageLookupMiss",
|
||||||
|
"target_package_tape_sha256",
|
||||||
"target_quantities",
|
"target_quantities",
|
||||||
|
"verify_target_package",
|
||||||
]
|
]
|
||||||
# __PORTABLE_CORE_BUNDLE_END__
|
# __PORTABLE_CORE_BUNDLE_END__
|
||||||
|
|
||||||
@@ -399,6 +946,9 @@ g = G()
|
|||||||
# __BASELINE_CONFIG_START__
|
# __BASELINE_CONFIG_START__
|
||||||
# Generated from configs/baseline.json; do not edit this region.
|
# Generated from configs/baseline.json; do not edit this region.
|
||||||
CONFIG = {'account_id': 'test',
|
CONFIG = {'account_id': 'test',
|
||||||
|
'decision_mode': 'portable_momentum_smoke',
|
||||||
|
'target_package_count': 0,
|
||||||
|
'target_package_tape_sha256': None,
|
||||||
'universe_mode': 'pit_index',
|
'universe_mode': 'pit_index',
|
||||||
'index_symbol': '000905.XSHG',
|
'index_symbol': '000905.XSHG',
|
||||||
'qmt_sector_name': '\u4e2d\u8bc1500',
|
'qmt_sector_name': '\u4e2d\u8bc1500',
|
||||||
@@ -421,6 +971,77 @@ CONFIG = {'account_id': 'test',
|
|||||||
'rebalance_schedule': 'weekly_first_close'}
|
'rebalance_schedule': 'weekly_first_close'}
|
||||||
# __BASELINE_CONFIG_END__
|
# __BASELINE_CONFIG_END__
|
||||||
|
|
||||||
|
# __TARGET_PACKAGE_TAPE_START__
|
||||||
|
# Generated by tools/bundle_platforms.py; do not edit this region.
|
||||||
|
TARGET_PACKAGES = []
|
||||||
|
# __TARGET_PACKAGE_TAPE_END__
|
||||||
|
|
||||||
|
|
||||||
|
MOMENTUM_SMOKE_MODE = "portable_momentum_smoke"
|
||||||
|
TARGET_PACKAGE_MODE = "target_package"
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_target_packages(config):
|
||||||
|
mode = config.get("decision_mode")
|
||||||
|
if mode not in (MOMENTUM_SMOKE_MODE, TARGET_PACKAGE_MODE):
|
||||||
|
raise UnsupportedStrategyPeriod("unsupported decision_mode")
|
||||||
|
if not isinstance(TARGET_PACKAGES, list):
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"TARGET_PACKAGES must be a list"
|
||||||
|
)
|
||||||
|
expected_count = config.get("target_package_count")
|
||||||
|
expected_sha256 = config.get("target_package_tape_sha256")
|
||||||
|
if mode == MOMENTUM_SMOKE_MODE:
|
||||||
|
if (
|
||||||
|
type(expected_count) is not int
|
||||||
|
or expected_count != 0
|
||||||
|
or expected_sha256 is not None
|
||||||
|
or TARGET_PACKAGES
|
||||||
|
):
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"portable_momentum_smoke cannot carry target packages"
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
if type(expected_count) is not int or expected_count <= 0:
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"target_package mode requires a positive target_package_count"
|
||||||
|
)
|
||||||
|
if expected_count != len(TARGET_PACKAGES):
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"target package count mismatch"
|
||||||
|
)
|
||||||
|
actual_sha256 = target_package_tape_sha256(TARGET_PACKAGES)
|
||||||
|
if expected_sha256 != actual_sha256:
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"target package tape sha256 mismatch"
|
||||||
|
)
|
||||||
|
verified = []
|
||||||
|
package_ids = set()
|
||||||
|
decision_ids = set()
|
||||||
|
clocks = set()
|
||||||
|
signal_clocks = set()
|
||||||
|
for raw_package in TARGET_PACKAGES:
|
||||||
|
package = verify_target_package(raw_package)
|
||||||
|
package_id = package["package_id"]
|
||||||
|
decision_id = package["decision_id"]
|
||||||
|
clock = (package["signal_as_of"], package["next_session"])
|
||||||
|
signal_as_of = package["signal_as_of"]
|
||||||
|
if (
|
||||||
|
package_id in package_ids
|
||||||
|
or decision_id in decision_ids
|
||||||
|
or clock in clocks
|
||||||
|
or signal_as_of in signal_clocks
|
||||||
|
):
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"duplicate or ambiguous target package decision identity"
|
||||||
|
)
|
||||||
|
package_ids.add(package_id)
|
||||||
|
decision_ids.add(decision_id)
|
||||||
|
clocks.add(clock)
|
||||||
|
signal_clocks.add(signal_as_of)
|
||||||
|
verified.append(package)
|
||||||
|
return verified
|
||||||
|
|
||||||
|
|
||||||
def _param(ContextInfo, name, default=None):
|
def _param(ContextInfo, name, default=None):
|
||||||
params = getattr(ContextInfo, "_param", {})
|
params = getattr(ContextInfo, "_param", {})
|
||||||
@@ -442,6 +1063,9 @@ def init(ContextInfo):
|
|||||||
override = _param(ContextInfo, "q60_" + key, None)
|
override = _param(ContextInfo, "q60_" + key, None)
|
||||||
if override is not None:
|
if override is not None:
|
||||||
g.config[key] = override
|
g.config[key] = override
|
||||||
|
g.target_packages = _validated_target_packages(g.config)
|
||||||
|
g.decision_mode = g.config["decision_mode"]
|
||||||
|
if g.config["decision_mode"] == MOMENTUM_SMOKE_MODE:
|
||||||
if (
|
if (
|
||||||
g.config.get("rebalance_schedule")
|
g.config.get("rebalance_schedule")
|
||||||
!= "weekly_first_close"
|
!= "weekly_first_close"
|
||||||
@@ -487,6 +1111,7 @@ def init(ContextInfo):
|
|||||||
g.last_plan = None
|
g.last_plan = None
|
||||||
g.last_universe = None
|
g.last_universe = None
|
||||||
g.last_universe_as_of = None
|
g.last_universe_as_of = None
|
||||||
|
g.last_target_signal_as_of = None
|
||||||
if (
|
if (
|
||||||
g.config.get("universe_mode") == "fixed"
|
g.config.get("universe_mode") == "fixed"
|
||||||
and hasattr(ContextInfo, "set_universe")
|
and hasattr(ContextInfo, "set_universe")
|
||||||
@@ -494,6 +1119,16 @@ def init(ContextInfo):
|
|||||||
ContextInfo.set_universe(g.config["universe"])
|
ContextInfo.set_universe(g.config["universe"])
|
||||||
|
|
||||||
|
|
||||||
|
def _decision_mode():
|
||||||
|
configured = g.config.get("decision_mode")
|
||||||
|
frozen = getattr(g, "decision_mode", configured)
|
||||||
|
if configured != frozen:
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"decision_mode cannot change after init"
|
||||||
|
)
|
||||||
|
return frozen
|
||||||
|
|
||||||
|
|
||||||
def _bar_datetime(ContextInfo):
|
def _bar_datetime(ContextInfo):
|
||||||
timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
|
timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
|
||||||
return datetime.datetime.fromtimestamp(float(timetag) / 1000.0)
|
return datetime.datetime.fromtimestamp(float(timetag) / 1000.0)
|
||||||
@@ -670,7 +1305,10 @@ def _portfolio(ContextInfo, managed):
|
|||||||
convert_symbol(symbol, "canonical")
|
convert_symbol(symbol, "canonical")
|
||||||
for symbol in managed
|
for symbol in managed
|
||||||
)
|
)
|
||||||
dynamic = g.config.get("universe_mode") == "pit_index"
|
dynamic = (
|
||||||
|
_decision_mode() == MOMENTUM_SMOKE_MODE
|
||||||
|
and g.config.get("universe_mode") == "pit_index"
|
||||||
|
)
|
||||||
for position in _trade_details(ContextInfo, "position") or []:
|
for position in _trade_details(ContextInfo, "position") or []:
|
||||||
code = str(
|
code = str(
|
||||||
_get_attr(position, ["stock_code", "m_strInstrumentID"], "")
|
_get_attr(position, ["stock_code", "m_strInstrumentID"], "")
|
||||||
@@ -716,6 +1354,8 @@ def _portfolio(ContextInfo, managed):
|
|||||||
|
|
||||||
|
|
||||||
def compute_plan(ContextInfo, bar_time=None):
|
def compute_plan(ContextInfo, bar_time=None):
|
||||||
|
if _decision_mode() == TARGET_PACKAGE_MODE:
|
||||||
|
return _compute_target_package_plan(ContextInfo, bar_time)
|
||||||
if bar_time is None:
|
if bar_time is None:
|
||||||
bar_time = _bar_datetime(ContextInfo)
|
bar_time = _bar_datetime(ContextInfo)
|
||||||
membership_timetag = int(
|
membership_timetag = int(
|
||||||
@@ -748,6 +1388,136 @@ def compute_plan(ContextInfo, bar_time=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _signal_as_of(bar_time):
|
||||||
|
if (
|
||||||
|
int(bar_time.hour) != 15
|
||||||
|
or int(bar_time.minute) != 0
|
||||||
|
or int(bar_time.second) != 0
|
||||||
|
or int(bar_time.microsecond) != 0
|
||||||
|
):
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"target_package mode requires the completed 15:00 daily close"
|
||||||
|
)
|
||||||
|
return bar_time.strftime("%Y-%m-%dT15:00:00+08:00")
|
||||||
|
|
||||||
|
|
||||||
|
def _target_package_for_close(bar_time):
|
||||||
|
signal_as_of = _signal_as_of(bar_time)
|
||||||
|
config = g.config
|
||||||
|
packages = _validated_target_packages(config)
|
||||||
|
matches = [
|
||||||
|
package
|
||||||
|
for package in packages
|
||||||
|
if package["signal_as_of"] == signal_as_of
|
||||||
|
]
|
||||||
|
if not matches:
|
||||||
|
return None
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"ambiguous target package signal clock"
|
||||||
|
)
|
||||||
|
package = matches[0]
|
||||||
|
return lookup_target_package_exact(
|
||||||
|
packages,
|
||||||
|
signal_as_of,
|
||||||
|
package["next_session"],
|
||||||
|
decision_id=package["decision_id"],
|
||||||
|
expected_tape_sha256=config["target_package_tape_sha256"],
|
||||||
|
expected_count=config["target_package_count"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _target_close_prices(ContextInfo, bar_time, symbols):
|
||||||
|
platform_symbols = [
|
||||||
|
convert_symbol(symbol, "qmt")
|
||||||
|
for symbol in sorted(symbols)
|
||||||
|
]
|
||||||
|
raw = ContextInfo.get_market_data_ex(
|
||||||
|
fields=["close"],
|
||||||
|
stock_code=platform_symbols,
|
||||||
|
period="1d",
|
||||||
|
start_time="",
|
||||||
|
end_time=bar_time.strftime("%Y%m%d"),
|
||||||
|
count=1,
|
||||||
|
dividend_type="front_ratio",
|
||||||
|
fill_data=False,
|
||||||
|
subscribe=False,
|
||||||
|
)
|
||||||
|
if not hasattr(raw, "get"):
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"target close query returned an invalid payload"
|
||||||
|
)
|
||||||
|
prices = {}
|
||||||
|
for symbol in platform_symbols:
|
||||||
|
values = _series_close(raw.get(symbol))
|
||||||
|
if len(values) != 1:
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"%s needs one completed daily close" % symbol
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
price = float(values[0])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"%s contains a missing/non-numeric close" % symbol
|
||||||
|
)
|
||||||
|
if not math.isfinite(price) or price <= 0.0:
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"%s contains a missing/non-positive close" % symbol
|
||||||
|
)
|
||||||
|
prices[convert_symbol(symbol, "canonical")] = price
|
||||||
|
return prices
|
||||||
|
|
||||||
|
|
||||||
|
def _target_lineage(package):
|
||||||
|
return {
|
||||||
|
"package_id": package["package_id"],
|
||||||
|
"package_sha256": package["package_sha256"],
|
||||||
|
"decision_id": package["decision_id"],
|
||||||
|
"signal_as_of": package["signal_as_of"],
|
||||||
|
"next_session": package["next_session"],
|
||||||
|
"release": package["release"],
|
||||||
|
"model_id": package["model_id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_target_package_plan(ContextInfo, bar_time=None):
|
||||||
|
if bar_time is None:
|
||||||
|
bar_time = _bar_datetime(ContextInfo)
|
||||||
|
package = _target_package_for_close(bar_time)
|
||||||
|
if package is None:
|
||||||
|
return None
|
||||||
|
managed = sorted(package["universe"])
|
||||||
|
current, sellable, equity = _portfolio(ContextInfo, managed)
|
||||||
|
if equity <= 0:
|
||||||
|
raise ValueError("QMT account equity must be positive")
|
||||||
|
plan = build_target_weight_plan(
|
||||||
|
target_weights=package["target_weights"],
|
||||||
|
prices=_target_close_prices(
|
||||||
|
ContextInfo,
|
||||||
|
bar_time,
|
||||||
|
set(package["target_weights"]) | set(current),
|
||||||
|
),
|
||||||
|
current=current,
|
||||||
|
sellable=sellable,
|
||||||
|
equity=equity,
|
||||||
|
cash_buffer=g.config["cash_buffer"],
|
||||||
|
lot_size=g.config["lot_size"],
|
||||||
|
decision_id=package["decision_id"],
|
||||||
|
package_sha256=package["package_sha256"],
|
||||||
|
)
|
||||||
|
plan["decision_mode"] = TARGET_PACKAGE_MODE
|
||||||
|
plan["lineage"] = _target_lineage(package)
|
||||||
|
plan["universe"] = package["universe"]
|
||||||
|
plan["execution_contract"] = {
|
||||||
|
"signal_price_source": "COMPLETED_DAILY_CLOSE",
|
||||||
|
"submit_trigger": "NEXT_BAR_FIRST_TICK",
|
||||||
|
"quick_trade": 0,
|
||||||
|
"declared_next_session": package["next_session"],
|
||||||
|
"real_platform_observed": False,
|
||||||
|
}
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
def _orders_allowed(ContextInfo):
|
def _orders_allowed(ContextInfo):
|
||||||
# This hosted wrapper is deliberately backtest-only. Its cached account
|
# This hosted wrapper is deliberately backtest-only. Its cached account
|
||||||
# query and in-memory weekly marker cannot provide crash-safe live
|
# query and in-memory weekly marker cannot provide crash-safe live
|
||||||
@@ -810,12 +1580,76 @@ def _submit_delta(ContextInfo, canonical, delta, week_key):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _submit_target_delta(ContextInfo, canonical, delta, package_sha256):
|
||||||
|
config = g.config
|
||||||
|
order_code = convert_symbol(canonical, "qmt")
|
||||||
|
operation = 23 if delta > 0 else 24
|
||||||
|
volume = abs(int(delta))
|
||||||
|
user_order_id = "q60t-%s-%s" % (
|
||||||
|
str(package_sha256)[:8],
|
||||||
|
order_code.split(".")[0],
|
||||||
|
)
|
||||||
|
function = globals()["passorder"]
|
||||||
|
arg_count = getattr(getattr(function, "__code__", None), "co_argcount", 11)
|
||||||
|
if arg_count <= 8:
|
||||||
|
function(
|
||||||
|
operation,
|
||||||
|
1101,
|
||||||
|
config["account_id"],
|
||||||
|
order_code,
|
||||||
|
5,
|
||||||
|
-1,
|
||||||
|
volume,
|
||||||
|
ContextInfo,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
function(
|
||||||
|
operation,
|
||||||
|
1101,
|
||||||
|
config["account_id"],
|
||||||
|
order_code,
|
||||||
|
5,
|
||||||
|
-1,
|
||||||
|
volume,
|
||||||
|
"quant60",
|
||||||
|
0,
|
||||||
|
user_order_id,
|
||||||
|
ContextInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handlebar(ContextInfo):
|
def handlebar(ContextInfo):
|
||||||
if not _is_backtest(ContextInfo) and hasattr(ContextInfo, "is_last_bar"):
|
if not _is_backtest(ContextInfo) and hasattr(ContextInfo, "is_last_bar"):
|
||||||
if not ContextInfo.is_last_bar():
|
if not ContextInfo.is_last_bar():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
bar_time = _bar_datetime(ContextInfo)
|
bar_time = _bar_datetime(ContextInfo)
|
||||||
|
if _decision_mode() == TARGET_PACKAGE_MODE:
|
||||||
|
signal_as_of = _signal_as_of(bar_time)
|
||||||
|
if g.last_target_signal_as_of == signal_as_of:
|
||||||
|
return None
|
||||||
|
plan = compute_plan(ContextInfo, bar_time)
|
||||||
|
# Consume the clock before crossing passorder so a callback retry
|
||||||
|
# cannot duplicate an already accepted order prefix.
|
||||||
|
g.last_target_signal_as_of = signal_as_of
|
||||||
|
if plan is None:
|
||||||
|
return None
|
||||||
|
g.last_plan = plan
|
||||||
|
if not _orders_allowed(ContextInfo):
|
||||||
|
return plan
|
||||||
|
ordered = sorted(
|
||||||
|
plan["orders"].items(),
|
||||||
|
key=lambda item: item[1],
|
||||||
|
)
|
||||||
|
for canonical, delta in ordered:
|
||||||
|
if int(delta) != 0:
|
||||||
|
_submit_target_delta(
|
||||||
|
ContextInfo,
|
||||||
|
canonical,
|
||||||
|
delta,
|
||||||
|
plan["lineage"]["package_sha256"],
|
||||||
|
)
|
||||||
|
return plan
|
||||||
year, week, unused = bar_time.isocalendar()
|
year, week, unused = bar_time.isocalendar()
|
||||||
del unused
|
del unused
|
||||||
week_key = (year, week)
|
week_key = (year, week)
|
||||||
|
|||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"artifacts": {
|
||||||
|
"joinquant": {
|
||||||
|
"core": "src/quant60/portable_core.py",
|
||||||
|
"core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||||
|
"effective_config_sha256": "640980026f397df2e6d0f9e3884b58c7e2343e1be70ca1b629f6111b889ec7cb",
|
||||||
|
"output": "dist/target-package/joinquant_strategy.py",
|
||||||
|
"output_sha256": "0586930ddc483f4489f09b562f4d4232b458b703e6a1194af809b707211be8c4",
|
||||||
|
"wrapper": "platforms/joinquant_strategy.py",
|
||||||
|
"wrapper_sha256": "0c672b64665b7c1d4409ba288b00d1608e976800af3a4f9a6467767ce4def592"
|
||||||
|
},
|
||||||
|
"qmt_builtin": {
|
||||||
|
"core": "src/quant60/portable_core.py",
|
||||||
|
"core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||||
|
"effective_config_sha256": "ace13da661a2921e6bf75e48a574fd605fb6eef87c0025b89460082326a39d36",
|
||||||
|
"output": "dist/target-package/qmt_builtin_strategy.py",
|
||||||
|
"output_sha256": "21f634a9831341fef2d86a6df612d01c79121794a48e525da76df226dc0729ed",
|
||||||
|
"wrapper": "platforms/qmt_builtin_strategy.py",
|
||||||
|
"wrapper_sha256": "a1e53dc22ebd9bb01be43675786151606bbed273d8ab883fb933461e379d2727"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"baseline_config": "configs/baseline.json",
|
||||||
|
"baseline_config_sha256": "939a7df386b06bc82869940572c35544e9361913b94d9e03027411e51b1006e1",
|
||||||
|
"decision_mode": "target_package",
|
||||||
|
"joinquant_observation": {
|
||||||
|
"eod_api_policy": "best_effort_get_orders_get_trades",
|
||||||
|
"eod_chunk_characters": 1800,
|
||||||
|
"eod_chunk_encoding": "base64-canonical-json",
|
||||||
|
"events": [
|
||||||
|
"INIT",
|
||||||
|
"TARGET_PACKAGE_NOOP",
|
||||||
|
"TARGET_PACKAGE_HIT",
|
||||||
|
"PLATFORM_BINDING_INPUT",
|
||||||
|
"TARGET_PACKAGE_PLAN",
|
||||||
|
"SKIPPED_ORDER",
|
||||||
|
"ORDER_REQUEST",
|
||||||
|
"ORDER_RETURN",
|
||||||
|
"EOD_STATUS"
|
||||||
|
],
|
||||||
|
"fail_soft": true,
|
||||||
|
"prefix": "QUANT60_JOINQUANT_EVIDENCE_V1 ",
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"target_price_source": "CURRENT_DATA_DAY_OPEN"
|
||||||
|
},
|
||||||
|
"portable_core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||||
|
"schema_version": 1,
|
||||||
|
"target_package_count": 1,
|
||||||
|
"target_package_ids": [
|
||||||
|
"TP-20240311-795dcfba"
|
||||||
|
],
|
||||||
|
"target_package_sha256": [
|
||||||
|
"baa15c8156aa6cb54d3775d0cd562613a9d0a4f15b0c39f2a60e0f8b8c6f1f3a"
|
||||||
|
],
|
||||||
|
"target_package_tape_sha256": "5383c376951637b8d4b1ca42fc6db80347abda74c0181c0f70d6c411fb97ea64"
|
||||||
|
}
|
||||||
+2081
File diff suppressed because it is too large
Load Diff
+1735
File diff suppressed because it is too large
Load Diff
+53
-30
@@ -13,23 +13,26 @@ JQData (authorized account)
|
|||||||
canonical immutable snapshot
|
canonical immutable snapshot
|
||||||
exact-set/hash + semantic reconstruction
|
exact-set/hash + semantic reconstruction
|
||||||
|
|
|
|
||||||
+---------+----------+
|
+----------------------+
|
||||||
| |
|
|
||||||
v v
|
|
||||||
snapshot-backtest snapshot-decision
|
|
||||||
local event engine Signal -> Target -> Order Delta
|
|
||||||
| |
|
|
||||||
+---------+----------+
|
|
||||||
|
|
|
|
||||||
portable-momentum-v1
|
v
|
||||||
|
snapshot-backtest / snapshot-decision
|
||||||
|
portable-momentum-v1 compatibility path (smoke only)
|
||||||
|
|
||||||
|
local Baseline authority:
|
||||||
|
Universe
|
||||||
|
-> frozen Ridge model bundle / Alpha
|
||||||
|
-> risk + cost-aware Portfolio
|
||||||
|
-> post-risk TargetPackageV1 (weights, no account quantities)
|
||||||
|
-> five-layer hash trace
|
||||||
|
|
|
|
||||||
+------------+-------------+
|
+------------+-------------+
|
||||||
| | |
|
| |
|
||||||
JoinQuant bundle QMT bundle local contract oracle
|
JoinQuant consumer QMT consumer
|
||||||
hosted backtest built-in/qmttools backtest-only
|
T+1-open account bind completed-close bind + quickTrade=0
|
||||||
|
order_target passorder (backtest-only)
|
||||||
|
|
||||||
synthetic PIT/features -> purged walk-forward -> Ridge challenger
|
Current TargetPackage fixture is synthetic research evidence, not market proof.
|
||||||
(not deployment-frozen)
|
|
||||||
|
|
||||||
Qlib 0.9.7:
|
Qlib 0.9.7:
|
||||||
Tushare completed/checksummed Parquet -> immutable managed provider
|
Tushare completed/checksummed Parquet -> immutable managed provider
|
||||||
@@ -159,19 +162,24 @@ terminal/xtquant build 探针时不假设它与 `price × quantity` 的精确误
|
|||||||
|
|
||||||
## Research and model boundary
|
## Research and model boundary
|
||||||
|
|
||||||
synthetic research pipeline 已实现:
|
synthetic research vertical slice 当前实际接通:
|
||||||
|
|
||||||
- PIT table 和未来值拒绝;
|
- PIT table 和未来值拒绝;
|
||||||
- Universe 原因码;
|
- Universe 原因码;
|
||||||
- 透明价量特征与 T+1-open 标签时钟;
|
- 透明价量特征与 T+1-open 标签时钟;
|
||||||
- train-only winsor/impute/standardize;
|
- train-only winsor/impute/standardize;
|
||||||
- purge/embargo walk-forward;
|
- purge/embargo walk-forward;
|
||||||
- deterministic Ridge、OOS IC/RankIC;
|
- deterministic Ridge、OOS IC/RankIC 与可重放 model bundle;
|
||||||
- factor risk、dated cost/capacity 和 constrained target。
|
- 60 日对角 realized variance;
|
||||||
|
- dated explicit fee + spread + square-root impact/capacity;
|
||||||
|
- deterministic constrained portfolio、独立 post-risk gate;
|
||||||
|
- reference execution binding、完整五层 hash trace 与 TargetPackageV1。
|
||||||
|
|
||||||
这些功能证明研究管线可执行。Ridge 尚未冻结成带版本、输入 schema 和推理
|
`factor_risk.py`、CVXPY 路径和 guarded TWAP/POV 仍是有单测的独立组件,
|
||||||
hash 的 deployment bundle,也没有授权真实长样本 OOS 与四引擎推理验证,因此
|
不在上述 vertical slice 中。Ridge 已能冻结预处理、系数、标签、训练截止日
|
||||||
不是生产 champion。
|
和 lineage,但目前只有 synthetic 候选;没有授权真实长样本 OOS、真实风险/
|
||||||
|
成本校准、多期逐层 parity 或 QMT peer,因此不是生产 champion。已有的单包
|
||||||
|
JoinQuant run 只观察 execution consumer。
|
||||||
|
|
||||||
Qlib 路径固定为 `0.9.7`。CPython 3.12 实际 fixture smoke 已得到:
|
Qlib 路径固定为 `0.9.7`。CPython 3.12 实际 fixture smoke 已得到:
|
||||||
|
|
||||||
@@ -191,9 +199,15 @@ L6,不声称 L3 target 或 L4 order-intent parity。
|
|||||||
|
|
||||||
## Portable strategy and platform boundary
|
## Portable strategy and platform boundary
|
||||||
|
|
||||||
当前可跨 hosted wrapper 冻结的共同基线是 `portable-momentum-v1`。平台 API
|
hosted artifact 有两个互斥模式:
|
||||||
不得进入 symbol normalization、momentum score、target weight、target
|
|
||||||
quantity 和 order delta 计算。
|
- `portable_momentum_smoke`:保留历史连通性回归,平台端计算动量;
|
||||||
|
- `target_package`:本地唯一权威已完成 Universe/Alpha/Portfolio/Risk,
|
||||||
|
hosted wrapper 只验证包、精确匹配 `signal_as_of + next_session`、读取账户
|
||||||
|
事实并构造 execution plan,绝不回退到 momentum。
|
||||||
|
|
||||||
|
`portable-momentum-v1` 不是生产 Baseline。TargetPackage 只有权重,没有股数、
|
||||||
|
现金、订单或成交;这些属于下一交易日平台账户绑定后的 execution layer。
|
||||||
|
|
||||||
canonical 股票代码采用 JoinQuant 形式:
|
canonical 股票代码采用 JoinQuant 形式:
|
||||||
|
|
||||||
@@ -208,8 +222,9 @@ canonical 股票代码采用 JoinQuant 形式:
|
|||||||
|
|
||||||
QMT built-in bundle 和 qmttools runner 都硬限制为 backtest/history。built-in
|
QMT built-in bundle 和 qmttools runner 都硬限制为 backtest/history。built-in
|
||||||
状态存放在模块全局 `g`,不写入可能在下个 `handlebar` 回滚的
|
状态存放在模块全局 `g`,不写入可能在下个 `handlebar` 回滚的
|
||||||
`ContextInfo` 用户属性。两条路径固定中证 500 benchmark、PIT 历史成分/ST
|
`ContextInfo` 用户属性。动量 smoke 路径固定中证 500 benchmark、PIT 历史
|
||||||
与 baseline 费率;built-in 的 10% 参与率由 QMT GUI 设置,qmttools
|
成分/ST 与费率;TargetPackage 路径消费本地冻结 Universe/权重,不重新计算
|
||||||
|
Alpha/Portfolio/Risk。built-in 的 10% 参与率由 QMT GUI 设置,qmttools
|
||||||
程序化传入。live mutation 只存在于外部 XtTrader adapter,并且默认关闭、
|
程序化传入。live mutation 只存在于外部 XtTrader adapter,并且默认关闭、
|
||||||
未认证、没有 live 启动命令;operator 可运行的 shadow CLI 只接受查询边界。
|
未认证、没有 live 启动命令;operator 可运行的 shadow CLI 只接受查询边界。
|
||||||
|
|
||||||
@@ -217,6 +232,9 @@ QMT built-in bundle 和 qmttools runner 都硬限制为 backtest/history。built
|
|||||||
|
|
||||||
最低交换合同是:
|
最低交换合同是:
|
||||||
|
|
||||||
|
- [`model_bundle.schema.json`](../schemas/model_bundle.schema.json);
|
||||||
|
- [`target_package.schema.json`](../schemas/target_package.schema.json);
|
||||||
|
- [`release_reachability.schema.json`](../schemas/release_reachability.schema.json);
|
||||||
- [`signal.schema.json`](../schemas/signal.schema.json);
|
- [`signal.schema.json`](../schemas/signal.schema.json);
|
||||||
- [`target.schema.json`](../schemas/target.schema.json);
|
- [`target.schema.json`](../schemas/target.schema.json);
|
||||||
- [`order_event.schema.json`](../schemas/order_event.schema.json);
|
- [`order_event.schema.json`](../schemas/order_event.schema.json);
|
||||||
@@ -228,8 +246,13 @@ saved/current source aggregate、saved/current schema aggregate 和 payload
|
|||||||
schema。`verify-snapshot-decision` 与 data snapshot semantic verifier 承担
|
schema。`verify-snapshot-decision` 与 data snapshot semantic verifier 承担
|
||||||
各自边界的同类职责。
|
各自边界的同类职责。
|
||||||
|
|
||||||
这些合同已有自动测试;最近一次标准库套件为 224 tests、OK、6 个可选路径
|
`release_reachability` 对每层分别记录 `implemented`、`unit_tested`、
|
||||||
skip。测试结果不能替代 schema 迁移演练、真实平台导出或券商回调认证。
|
`local_entrypoint_reachable`、`jq_entrypoint_reachable`、
|
||||||
|
`qmt_entrypoint_reachable` 和 `real_platform_observed`,并逐文件验证路径与
|
||||||
|
SHA。源文件总 hash 不能冒充真实运行。测试结果仍不能替代 schema 迁移演练、
|
||||||
|
真实平台导出或券商回调认证。当前只有
|
||||||
|
`execution.real_platform_observed=true`,由一个真实 JoinQuant
|
||||||
|
TargetPackage smoke 支持;其他四层及 all-layer summary 仍为 false。
|
||||||
|
|
||||||
## Clocks and reconciliation
|
## Clocks and reconciliation
|
||||||
|
|
||||||
@@ -274,12 +297,12 @@ synthetic/fake 数据是合同测试,不是市场证据;Qlib synthetic fixtu
|
|||||||
当前还缺:
|
当前还缺:
|
||||||
|
|
||||||
- 授权 JQData 真实快照、基本面公告 PIT、完整公司行动/Security Master;
|
- 授权 JQData 真实快照、基本面公告 PIT、完整公司行动/Security Master;
|
||||||
- Ridge/LightGBM 冻结模型包和真实 OOS 证据;
|
- 授权真实数据训练的冻结模型包和真实 OOS 证据;
|
||||||
- 真实数据校准的风险、冲击、容量和 TCA;
|
- 真实数据校准的风险、冲击、容量和 TCA;
|
||||||
- 真实 JoinQuant/QMT 导出;
|
- 授权真实数据的多期 JoinQuant 导出、逐层本地对账与真实 QMT peer;
|
||||||
- 券商状态映射、restart recovery daemon、20 日 shadow 与连续对账;
|
- 券商状态映射、restart recovery daemon、20 日 shadow 与连续对账;
|
||||||
- 运维监控、kill-switch 演练和程序化交易合规确认。
|
- 运维监控、kill-switch 演练和程序化交易合规确认。
|
||||||
|
|
||||||
所以 Quant OS 是可执行、可继续填证据的生产候选框架,不是已经达到整体
|
所以 Quant OS 已具备本地五层 vertical slice 与 TargetPackage 平台边界,
|
||||||
60 分的实盘系统。证据政策见
|
但仍只是可继续填证据的候选骨架,不是已经达到整体 60 分的实盘系统。证据政策见
|
||||||
[`AUDIT_2026-07-25.md`](AUDIT_2026-07-25.md)。
|
[`AUDIT_2026-07-25.md`](AUDIT_2026-07-25.md)。
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ A 股 Baseline 内核。它的新增得分必须在授权数据、真实平台
|
|||||||
|
|
||||||
截至 2026-07-26 的实现事实快照:
|
截至 2026-07-26 的实现事实快照:
|
||||||
|
|
||||||
- 截至 2026-07-26 的最新标准库全量测试为 `Ran 224 tests`、`OK (skipped=6)`;
|
> 这是复评当日的历史快照,不是滚动状态页。当前五层主链、测试数和平台证据
|
||||||
|
> 边界以 [`BASELINE_VERTICAL_SLICE.md`](BASELINE_VERTICAL_SLICE.md)、
|
||||||
|
> [`gate_scorecard.json`](../gate_scorecard.json) 和生成的公开状态为准。
|
||||||
|
|
||||||
|
- 本次复评当时的标准库全量测试为 `Ran 224 tests`、`OK (skipped=6)`;
|
||||||
- JQData raw/factor/pre-close/limits/paused + 每日 PIT `is_st`/指数成员
|
- JQData raw/factor/pre-close/limits/paused + 每日 PIT `is_st`/指数成员
|
||||||
已连接到 semantic snapshot verifier、`snapshot-backtest` 和
|
已连接到 semantic snapshot verifier、`snapshot-backtest` 和
|
||||||
`snapshot-decision`,并拒绝把抓取当日未到 24:00 的日线标成完整收盘,
|
`snapshot-decision`,并拒绝把抓取当日未到 24:00 的日线标成完整收盘,
|
||||||
@@ -39,8 +43,12 @@ A 股 Baseline 内核。它的新增得分必须在授权数据、真实平台
|
|||||||
- fixture 只有两只 synthetic 股票,任何性能数字都没有投资意义;
|
- fixture 只有两只 synthetic 股票,任何性能数字都没有投资意义;
|
||||||
- JoinQuant/QMT mock parity exporter 已实现,但
|
- JoinQuant/QMT mock parity exporter 已实现,但
|
||||||
`real_platform_pass=false` 且不计 G9;
|
`real_platform_pass=false` 且不计 G9;
|
||||||
- 2026-07-26 已完成一次 2024 全年真实 JoinQuant hosted smoke,可见日志
|
- 2026-07-26 已完成一次 2024 全年真实 JoinQuant hosted momentum smoke,可见
|
||||||
无 ERROR;它证明 runtime/path,但尚无完整同输入导出和本地 strict peer;
|
日志无 ERROR;它证明 `portable_momentum_smoke` runtime/path,不是
|
||||||
|
Ridge/TargetPackage Baseline 证据,且尚无完整同输入导出和本地 strict peer;
|
||||||
|
- 同日一个 `synthetic-research` TargetPackage 已在真实 JoinQuant hosted
|
||||||
|
环境完成 exact-clock/`day_open` 绑定、3 笔委托和 3 笔成交;该证据只支持
|
||||||
|
execution 层真实平台观察,不支持上游四层、G9 或投资表现;
|
||||||
- XtTrader 已有严格只读、HMAC 账户绑定的 observation/snapshot/shadow
|
- XtTrader 已有严格只读、HMAC 账户绑定的 observation/snapshot/shadow
|
||||||
plan 与 verifier,但真实 QMT、broker callback/shadow 均未运行,
|
plan 与 verifier,但真实 QMT、broker callback/shadow 均未运行,
|
||||||
G1—G10 仍全部 `not_passed`。
|
G1—G10 仍全部 `not_passed`。
|
||||||
@@ -233,8 +241,8 @@ PYTHONPATH=src:. python -m quant60 snapshot-backtest \
|
|||||||
|
|
||||||
1. 用户授权登录 JQData,生成不可变真实 snapshot,保存许可/lineage,并通过
|
1. 用户授权登录 JQData,生成不可变真实 snapshot,保存许可/lineage,并通过
|
||||||
semantic verifier、`snapshot-backtest` 和 `snapshot-decision`;
|
semantic verifier、`snapshot-backtest` 和 `snapshot-decision`;
|
||||||
2. 基于已完成的聚宽 hosted smoke,冻结一个小型 canonical 平台输入并导出
|
2. 把已完成的单包 JoinQuant execution smoke 扩展为授权真实数据的多期
|
||||||
close arrays/hash、plan、orders/fills;
|
TargetPackage,并保存完整 identity/plan/orders/fills;
|
||||||
3. 用本地与聚宽对相同输入逐层比较 L1—L4,而不是先比较净值;mock parity
|
3. 用本地与聚宽对相同输入逐层比较 L1—L4,而不是先比较净值;mock parity
|
||||||
只作为回归辅助;
|
只作为回归辅助;
|
||||||
4. 把基本面公告 available-time、完整公司行动/Security Master 加入真实
|
4. 把基本面公告 available-time、完整公司行动/Security Master 加入真实
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Quant OS Baseline vertical slice:当前真实结构与缺口
|
||||||
|
|
||||||
|
更新时间:2026-07-26。
|
||||||
|
|
||||||
|
## 一句话结论
|
||||||
|
|
||||||
|
Quant OS 现在已经不是“只有一堆互不相连的模块”:本地 synthetic 路径已经把
|
||||||
|
Universe、Ridge Alpha、风险/成本参与的 Portfolio、post-risk gate 和
|
||||||
|
reference Execution 串成唯一五层链,并发布冻结模型包与无股数
|
||||||
|
`TargetPackageV1`。
|
||||||
|
|
||||||
|
但它仍然是 `NOT_BASELINE_60`。原因不是代码行数,而是当前候选只在 synthetic
|
||||||
|
数据上形成上游四层;虽然一个 synthetic-research TargetPackage 已真实通过
|
||||||
|
JoinQuant execution consumer 并成交,真实 JQData 长样本 OOS、上游逐层
|
||||||
|
真实数据 parity、真实 QMT peer 与 20 日 shadow 都还没有证据。
|
||||||
|
|
||||||
|
## 两条不能混记的运行路径
|
||||||
|
|
||||||
|
| 模式 | 上游决策在哪里 | 平台上运行什么 | 当前真实平台事实 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `portable_momentum_smoke` | JoinQuant/QMT wrapper 现场用完成日线算动量 | 动量、封顶权重、股数与 delta | JoinQuant 2024 全年跑过;不是 Baseline |
|
||||||
|
| `target_package` | 本地 Quant OS 完成 Universe → Alpha → Portfolio → Risk | 验包、精确双时钟命中、读取 equity/position/sellable/price、Execution | JoinQuant 已有一个真实 execution smoke(3 单/3 成交);上游仍是 synthetic,QMT 未实跑 |
|
||||||
|
|
||||||
|
默认 bundler 仍生成 smoke,保持历史回归兼容。只有显式传
|
||||||
|
`--target-package` 才会生成候选消费者;目标包缺失、重复、篡改或时钟不匹配
|
||||||
|
都会停止,不会回退到 momentum。
|
||||||
|
|
||||||
|
## 五层实际用了什么
|
||||||
|
|
||||||
|
| 层 | 当前 vertical slice 的实际实现 | 已接通的策略 | 明确未接通/未证明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Universe | `SecurityEligibility` → 五态 state machine,原因码进入 trace/package | openable、hold-only、sell-only、frozen、excluded | synthetic 成员;真实基本面 PIT、Security Master、退市链未完成 |
|
||||||
|
| Alpha | train-only winsor/impute/standardize + `DeterministicRidge(alpha=5)` | 冻结 feature order、transform、系数、截距、训练截止与标签 | 无授权真实 OOS;Qlib Alpha158/LightGBM 是另一研究路径 |
|
||||||
|
| Portfolio | deterministic long-only constrained optimizer | Alpha 减显式成本,按预测方差调整;单名、行业、gross、capacity、turnover 约束 | CVXPY 有组件测试但未进入当前主链 |
|
||||||
|
| Risk | 60 日 realized diagonal variance 进入优化,随后独立 concentration/gross/turnover post-check | 风险输入变化必须改变 target;风险 gate 不可旁路 | `factor_risk.py` 的因子协方差/stress 尚未进入主链,也未真实校准 |
|
||||||
|
| Execution | 本地 reference account 生成 quantity/delta;平台从 TargetPackage 权重重新绑定真实账户 | 百股、科创板最小申报、T+1 sellable、卖单优先、平台保护价;JoinQuant 单包已有 3 单/3 fill | guarded TWAP/POV 尚未进入这条 slice;无 QMT peer、多期真实数据回放或长期 callback 证据 |
|
||||||
|
|
||||||
|
权威实现和合同:
|
||||||
|
|
||||||
|
- `src/quant60/baseline_pipeline.py`
|
||||||
|
- `src/quant60/model_bundle.py`
|
||||||
|
- `schemas/model_bundle.schema.json`
|
||||||
|
- `src/quant60/target_package.py`
|
||||||
|
- `schemas/target_package.schema.json`
|
||||||
|
- `src/quant60/release_reachability.py`
|
||||||
|
- `schemas/release_reachability.schema.json`
|
||||||
|
- `releases/quant60-research-candidate-v1/reachability.json`
|
||||||
|
|
||||||
|
可直接做平台线路调试的 synthetic 单文件在:
|
||||||
|
|
||||||
|
- `dist/target-package/joinquant_strategy.py`
|
||||||
|
- `dist/target-package/qmt_builtin_strategy.py`
|
||||||
|
- `dist/target-package/bundle_manifest.json`
|
||||||
|
|
||||||
|
它们只用于验证 TargetPackage 消费与执行边界,不能当作真实数据 Baseline
|
||||||
|
或投资结果。
|
||||||
|
|
||||||
|
## 为什么 hosted 文件仍然可以很短
|
||||||
|
|
||||||
|
“完整 Baseline”不等于把训练、特征工程、协方差估计和优化器全部复制进聚宽或
|
||||||
|
QMT。那样会制造多个决策权威,也会被平台 Python 版本、依赖和数据语义拖成
|
||||||
|
不同策略。
|
||||||
|
|
||||||
|
目标架构是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
本地唯一权威
|
||||||
|
冻结数据/模型/配置
|
||||||
|
-> 五层决策与逐层 hash
|
||||||
|
-> post-risk TargetPackage(权重)
|
||||||
|
|
||||||
|
平台薄消费者
|
||||||
|
验证同一个 TargetPackage
|
||||||
|
-> 绑定平台账户事实和执行价格
|
||||||
|
-> quantity / order delta
|
||||||
|
-> order API 与导出
|
||||||
|
```
|
||||||
|
|
||||||
|
所以平台代码短本身是合理的;不合理的是薄 wrapper 自己改算 momentum,却被
|
||||||
|
文档称为完整 Baseline。现在这两种模式已经在配置、manifest、测试和文档中
|
||||||
|
分开。
|
||||||
|
|
||||||
|
## 当前机器可证明到哪里
|
||||||
|
|
||||||
|
每一层不再只写一个“已实现”,而是分别记录:
|
||||||
|
|
||||||
|
1. `implemented`
|
||||||
|
2. `unit_tested`
|
||||||
|
3. `local_entrypoint_reachable`
|
||||||
|
4. `jq_entrypoint_reachable`
|
||||||
|
5. `qmt_entrypoint_reachable`
|
||||||
|
6. `real_platform_observed`
|
||||||
|
|
||||||
|
`release_reachability` 要求每个 true 状态有类型匹配、仓库相对路径和实际
|
||||||
|
SHA-256 证据。源文件 aggregate hash 不能冒充真实平台运行。2026-07-26 的
|
||||||
|
TargetPackage hosted run 只允许把 `execution.real_platform_observed` 设为
|
||||||
|
true;Universe、Alpha、Portfolio、Risk 仍为 false,因此
|
||||||
|
`all_layers_real_platform_observed` 也必须保持 false。
|
||||||
|
|
||||||
|
该真实执行记录使用 `synthetic-research` 输入,只证明验包、时钟、
|
||||||
|
`day_open`、账户绑定、下单和成交:
|
||||||
|
|
||||||
|
- [`JOINQUANT_TARGET_PACKAGE_EVIDENCE_2026-07-26.md`](JOINQUANT_TARGET_PACKAGE_EVIDENCE_2026-07-26.md)
|
||||||
|
- [`../evidence/joinquant/TP-20240311-795dcfba/2457a7c39a276e09e0fabf99e28978e1/run.json`](../evidence/joinquant/TP-20240311-795dcfba/2457a7c39a276e09e0fabf99e28978e1/run.json)
|
||||||
|
|
||||||
|
## 还差什么
|
||||||
|
|
||||||
|
按先后顺序:
|
||||||
|
|
||||||
|
1. 用授权 JQData/其他合法数据形成真实 PIT 长样本,训练并冻结候选 model
|
||||||
|
bundle;封住测试集,保存 OOS、容量和多状态报告。
|
||||||
|
2. 用真实数据校准 risk/cost;决定并接通 factor risk、CVXPY 是否进入
|
||||||
|
Baseline 60,而不是因为模块存在就自动算完成。
|
||||||
|
3. 从同一个真实候选生成多期 TargetPackage tape;不能用当前单个 synthetic
|
||||||
|
package 代替。
|
||||||
|
4. 把同一真实 tape 分别打入 JoinQuant/QMT;现有 JoinQuant 单包 execution
|
||||||
|
smoke 作为线路先验,但仍要导出完整多期 identity/plan/orders/fills。
|
||||||
|
5. 做 local/JoinQuant/QMT 逐层差异;任何 `UNEXPLAINED` 必须为零。当前无
|
||||||
|
QMT peer,所以 G9 仍未通过。
|
||||||
|
6. 完成至少 20 个交易日 QMT 只读 shadow、重启/回调/对账和合规确认。
|
||||||
|
|
||||||
|
## 用户什么时候需要参与
|
||||||
|
|
||||||
|
当前代码、测试、合同和文档整理不需要用户提供任何凭据,可以直接完成。
|
||||||
|
后续只有三处需要用户环境:
|
||||||
|
|
||||||
|
- JQData:在本机授权环境抓取可保存的脱敏快照;
|
||||||
|
- JoinQuant:已有一个 synthetic 单包 execution smoke;真实候选形成后仍需
|
||||||
|
上传多期 tape 并导出完整结果;
|
||||||
|
- Windows QMT/MiniQMT:使用已登录且有历史数据权限的客户端做 backtest 和
|
||||||
|
read-only shadow。
|
||||||
|
|
||||||
|
不要把密码、cookie、token、QMT userdata、account id 或持牌二进制发到聊天、
|
||||||
|
Git 或文档中。
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# JoinQuant TargetPackage execution evidence — 2026-07-26
|
||||||
|
|
||||||
|
This is a conservative record of one real JoinQuant hosted execution smoke.
|
||||||
|
It proves that the frozen TargetPackage consumer reached the real hosted
|
||||||
|
account/price/order/fill boundary. It does not prove that JoinQuant recomputed
|
||||||
|
the upstream Universe, Alpha, Portfolio, or Risk layers, and it is not an
|
||||||
|
investment-performance claim.
|
||||||
|
|
||||||
|
## Frozen identity
|
||||||
|
|
||||||
|
- Run:
|
||||||
|
[JoinQuant backtest detail](https://www.joinquant.com/algorithm/backtest/detail?backtestId=2457a7c39a276e09e0fabf99e28978e1)
|
||||||
|
- Run ID: `2457a7c39a276e09e0fabf99e28978e1`
|
||||||
|
- Observed at: `2026-07-26T08:50:15Z`
|
||||||
|
- Bundle SHA-256:
|
||||||
|
`0586930ddc483f4489f09b562f4d4232b458b703e6a1194af809b707211be8c4`
|
||||||
|
- Bundle manifest SHA-256:
|
||||||
|
`91f4b4dc0bdd0d9f99b3466c830600e2678be78dd991d4499a4154bffec8597a`
|
||||||
|
- Portable core SHA-256:
|
||||||
|
`f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19`
|
||||||
|
- Wrapper SHA-256:
|
||||||
|
`0c672b64665b7c1d4409ba288b00d1608e976800af3a4f9a6467767ce4def592`
|
||||||
|
- TargetPackage ID: `TP-20240311-795dcfba`
|
||||||
|
- Package SHA-256:
|
||||||
|
`baa15c8156aa6cb54d3775d0cd562613a9d0a4f15b0c39f2a60e0f8b8c6f1f3a`
|
||||||
|
- Tape SHA-256:
|
||||||
|
`5383c376951637b8d4b1ca42fc6db80347abda74c0181c0f70d6c411fb97ea64`
|
||||||
|
- Evidence class: `synthetic-research`
|
||||||
|
|
||||||
|
The uploaded browser snapshot and the frozen bundle had the same SHA-256.
|
||||||
|
The machine record distinguishes
|
||||||
|
`platform_evidence_class=real_platform_runtime` from
|
||||||
|
`input_evidence_class=synthetic-research` and fixes
|
||||||
|
`performance_claim=false`.
|
||||||
|
The tracked, redacted machine record is
|
||||||
|
[`run.json`](../evidence/joinquant/TP-20240311-795dcfba/2457a7c39a276e09e0fabf99e28978e1/run.json).
|
||||||
|
It contains no account identifier, cookie, token, credentials, screenshots, or
|
||||||
|
personal information.
|
||||||
|
|
||||||
|
## Runtime and observed sessions
|
||||||
|
|
||||||
|
- Period: 2024-03-11 through 2024-03-13
|
||||||
|
- Initial capital: CNY 1,000,000
|
||||||
|
- Frequency/runtime: daily, Python 3
|
||||||
|
- Benchmark: CSI 500 (`000905.XSHG`)
|
||||||
|
- Decision mode: `target_package`
|
||||||
|
|
||||||
|
| Session | Hosted observation | Orders | Fills |
|
||||||
|
| --- | --- | ---: | ---: |
|
||||||
|
| 2024-03-11 | Exact-clock miss; `TARGET_PACKAGE_NOOP`; no fallback | 0 | 0 |
|
||||||
|
| 2024-03-12 | Package hit; prices bound from `CURRENT_DATA_DAY_OPEN` | 3 | 3 |
|
||||||
|
| 2024-03-13 | Exact-clock miss; `TARGET_PACKAGE_NOOP`; no fallback | 0 | 0 |
|
||||||
|
|
||||||
|
The hosted plan preserved the package weights exactly: 10% each for
|
||||||
|
`600519.XSHG`, `601012.XSHG`, `601318.XSHG`, and `688981.XSHG`; all other
|
||||||
|
weights were zero.
|
||||||
|
|
||||||
|
The resulting targets and fills were:
|
||||||
|
|
||||||
|
| Symbol | Weight | Target quantity | Filled quantity | Fill price |
|
||||||
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
|
| `600519.XSHG` | 0.10 | 0 | 0 | — |
|
||||||
|
| `601012.XSHG` | 0.10 | 4,500 | 4,500 | 22.22 |
|
||||||
|
| `601318.XSHG` | 0.10 | 2,300 | 2,300 | 43.18 |
|
||||||
|
| `688981.XSHG` | 0.10 | 2,000 | 2,000 | 47.87 |
|
||||||
|
|
||||||
|
`600519.XSHG` opened at CNY 1,693.94. One 100-share board lot therefore
|
||||||
|
required CNY 169,394, above the CNY 100,000 target notional. The deterministic
|
||||||
|
lot rule correctly produced a zero-share target; this was not a rejected or
|
||||||
|
skipped order. The STAR Market request for `688981.XSHG` used the configured
|
||||||
|
hosted limit-price protection.
|
||||||
|
|
||||||
|
## What this evidence proves
|
||||||
|
|
||||||
|
For the `execution` layer only, this run observes:
|
||||||
|
|
||||||
|
- exact bundle/package identity verification;
|
||||||
|
- exact signal-close/next-session clock matching with no momentum fallback;
|
||||||
|
- real hosted equity and `day_open` price binding;
|
||||||
|
- deterministic target-quantity and order construction;
|
||||||
|
- three accepted orders and three real hosted fills;
|
||||||
|
- clean no-op behavior on the surrounding sessions.
|
||||||
|
|
||||||
|
The release reachability manifest may therefore mark only
|
||||||
|
`execution.real_platform_observed=true`, backed by the tracked machine record.
|
||||||
|
Universe, Alpha, Portfolio, and Risk remain
|
||||||
|
`real_platform_observed=false`: those layers were computed locally and carried
|
||||||
|
to JoinQuant as a frozen TargetPackage rather than recomputed inside JoinQuant.
|
||||||
|
|
||||||
|
## What this evidence does not prove
|
||||||
|
|
||||||
|
The package still has evidence class `synthetic-research`. This three-session,
|
||||||
|
single-package run provides no:
|
||||||
|
|
||||||
|
- authorized real-data long-sample or out-of-sample validation;
|
||||||
|
- real-data calibration of the upstream Alpha, Portfolio, or Risk layers;
|
||||||
|
- multi-period same-input layer-by-layer local/JoinQuant parity;
|
||||||
|
- real QMT peer run or 20-trading-day broker shadow;
|
||||||
|
- G9 cross-engine acceptance, `BASELINE_60`, release verification, live
|
||||||
|
readiness, or investment-value evidence.
|
||||||
|
|
||||||
|
The hosted performance fields are retained in the machine record solely to
|
||||||
|
identify the completed smoke. They are economically meaningless over this
|
||||||
|
three-session window and must not be presented as strategy performance.
|
||||||
+22
-12
@@ -5,16 +5,16 @@
|
|||||||
|
|
||||||
| Path | Intended use | Runtime / prerequisite | Current verified fact | Missing release evidence |
|
| Path | Intended use | Runtime / prerequisite | Current verified fact | Missing release evidence |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| Local synthetic execution | 确定性事件回测、ledger/replay | Python ≥3.10,无账号 | 标准库套件最近记录为 224 tests、OK、6 skip;smoke + exact manifest verifier 可运行 | synthetic 不代表真实市场/收益 |
|
| Local synthetic execution | 确定性事件回测、ledger/replay | Python ≥3.10,无账号 | 标准库套件、smoke + exact manifest verifier 可运行 | synthetic 不代表真实市场/收益 |
|
||||||
| Local synthetic research | PIT/features/walk-forward/Ridge/risk/cost/target | Python ≥3.10,无账号 | research smoke + manifest verifier 可运行 | Ridge 未冻结部署,未做授权长样本 OOS |
|
| Local synthetic research | 五层 vertical slice:Universe/Ridge Alpha/risk+cost Portfolio/post-risk/reference Execution | Python ≥3.10,无账号 | 冻结 model bundle、TargetPackageV1、layer trace + manifest verifier 可运行;风险/成本扰动必须改变 target | 仍是 synthetic;当前只接对角风险与 deterministic optimizer,未做授权长样本 OOS |
|
||||||
| JQData ingestion | 授权数据到 canonical immutable snapshot | `jqdatasdk==1.9.8` + 授权账号 | fake-provider 测试覆盖 raw/factor/pre-close/limits/paused/PIT `is_st`/membership、24:00 完整收盘约束和语义 verifier | 尚未登录真实账号保存快照、许可与 lineage |
|
| JQData ingestion | 授权数据到 canonical immutable snapshot | `jqdatasdk==1.9.8` + 授权账号 | fake-provider 测试覆盖 raw/factor/pre-close/limits/paused/PIT `is_st`/membership、24:00 完整收盘约束和语义 verifier | 尚未登录真实账号保存快照、许可与 lineage |
|
||||||
| Snapshot local backtest | PIT membership 的 provider-data 本地回测 | semantic verifier 通过的 snapshot | `snapshot-backtest` 复用 production decision path,输出普通 run manifest | 尚无授权真实 snapshot;仍是本地 fill model |
|
| Snapshot local backtest | PIT membership 的 provider-data 本地回测 | semantic verifier 通过的 snapshot | `snapshot-backtest` 复用 production decision path,输出普通 run manifest | 尚无授权真实 snapshot;仍是本地 fill model |
|
||||||
| Snapshot decision | 某决策日 Signal/Target/Order Delta | snapshot + equity 或完整 broker snapshot | fail-closed broker state 与 exact decision manifest verifier 已实现 | 未接真实账户 snapshot,未冻结生产模型 |
|
| Snapshot decision | 某决策日 Signal/Target/Order Delta | snapshot + equity 或完整 broker snapshot | fail-closed broker state 与 exact decision manifest verifier 已实现 | 未接真实账户 snapshot,未冻结生产模型 |
|
||||||
| Colab | clone、测试、bundle、本地两条 synthetic 链;可选 JQData/Qlib | Colab runtime | notebook 已可本地逐 cell 执行 | 可选账号/依赖 cell 仍需运行时授权;不是平台证据 |
|
| Colab | clone、测试、bundle、本地两条 synthetic 链;可选 JQData/Qlib | Colab runtime | notebook 已可本地逐 cell 执行 | 可选账号/依赖 cell 仍需运行时授权;不是平台证据 |
|
||||||
| JoinQuant local harness | wrapper API 合同 | Python ≥3.10,无账号 | fake lifecycle 与 mock parity 可运行 | 不证明 JoinQuant |
|
| JoinQuant local harness | wrapper API 合同 | Python ≥3.10,无账号 | 动量 smoke 与 TargetPackage 消费模式均有 fake lifecycle;目标包缺失/篡改不回退 | 不证明 JoinQuant |
|
||||||
| JoinQuant hosted bundle | hosted backtest | JoinQuant 登录/数据权限 | 真实 2024 全年 run 已完成且可见日志 0 ERROR;单文件、PIT index/ST、批量 history、费率/滑点/10% 参与率、科创板保护价/200 股约束已运行 | 尚无完整输入/plan/orders/fills 导出和同输入本地比较 |
|
| JoinQuant hosted bundle | hosted backtest | JoinQuant 登录/数据权限 | 动量 smoke 的真实 2024 全年 run 已完成;FINAL4 的单个 synthetic-research TargetPackage 已在真实 hosted 环境按 `day_open` 命中并产生 3 单/3 fill | 只观察 execution consumer;无授权真实长样本/OOS、上游四层 hosted 执行、QMT peer 或逐层真实数据 parity |
|
||||||
| QMT local harness | built-in wrapper 合同 | Python ≥3.10,无账号 | Python 3.6 source、`ContextInfo` 回滚模拟和 mock parity | 不证明 QMT |
|
| QMT local harness | built-in wrapper 合同 | Python ≥3.10,无账号 | Python 3.6 source、`ContextInfo` 回滚模拟、动量 smoke 与 TargetPackage 合同 | 不证明 QMT |
|
||||||
| QMT built-in bundle | GUI backtest-only | 授权 QMT 客户端/板块与历史 ST 数据 | bundle 硬限制 backtest;毫秒 timetag PIT 成分/ST;固定中证 500、费率/滑点;模块全局 `g` | 尚无真实 QMT run/export;10% GUI 配置与历史 ST 正例探针待取证 |
|
| QMT built-in bundle | GUI backtest-only | 授权 QMT 客户端/历史数据 | 两种模式都硬限制 backtest;TargetPackage 模式不重算上游四层;模块全局 `g` | 尚无 TargetPackage 真实 QMT run/export;10% GUI 配置和执行时钟待取证 |
|
||||||
| qmttools runner | native-Python QMT backtest/history | 券商分发 `xtquant` + 已登录 terminal | 固定中证 500/10% 参与率、参数、只读 preflight 和 hard backtest/history 合同已测 | 尚无专有运行时实跑 |
|
| qmttools runner | native-Python QMT backtest/history | 券商分发 `xtquant` + 已登录 terminal | 固定中证 500/10% 参与率、参数、只读 preflight 和 hard backtest/history 合同已测 | 尚无专有运行时实跑 |
|
||||||
| XtTrader read-only shadow | 账户绑定 target-diff 与 broker observation | MiniQMT/QMT、账户查询权限、既有本地 HMAC key | HMAC 账户绑定 + authenticated evidence envelope、decision/observation semantic replay、资产/持仓/委托/成交恒等式、fail-closed exact verifier 与 fake broker 合同 | HMAC 不是 broker attestation;仍缺官方 query 失败/空结果合同、callback/restart、20 日真实 shadow;not live-ready |
|
| XtTrader read-only shadow | 账户绑定 target-diff 与 broker observation | MiniQMT/QMT、账户查询权限、既有本地 HMAC key | HMAC 账户绑定 + authenticated evidence envelope、decision/observation semantic replay、资产/持仓/委托/成交恒等式、fail-closed exact verifier 与 fake broker 合同 | HMAC 不是 broker attestation;仍缺官方 query 失败/空结果合同、callback/restart、20 日真实 shadow;not live-ready |
|
||||||
| Qlib native momentum | signal/model research | CPython 3.12 + exactly `pyqlib==0.9.7` | 真实本地 fixture 成功,24 signal;保存 signal/report hash、表边界和重算 portfolio 指标 | synthetic 两股票;无真实数据/OOS,无 L3/L4 parity |
|
| Qlib native momentum | signal/model research | CPython 3.12 + exactly `pyqlib==0.9.7` | 真实本地 fixture 成功,24 signal;保存 signal/report hash、表边界和重算 portfolio 指标 | synthetic 两股票;无真实数据/OOS,无 L3/L4 parity |
|
||||||
@@ -22,12 +22,14 @@
|
|||||||
| Tushare local → Qlib | completed Parquet 的只读盘点、不可变 provider 与本地研究 smoke | 同上 + `pyarrow==24.0.0`;本地镜像 | 93 个 completed 文件全部通过 size/SHA/row-count;v4 provider verifier 成功;同一动量命令连续两次 evidence JSON 逐 byte 相同 | `daily` 仅完成约 8.2%,缺复权、真实 benchmark、PIT 成分/ST/停牌/涨跌停;`production_ready=false` |
|
| Tushare local → Qlib | completed Parquet 的只读盘点、不可变 provider 与本地研究 smoke | 同上 + `pyarrow==24.0.0`;本地镜像 | 93 个 completed 文件全部通过 size/SHA/row-count;v4 provider verifier 成功;同一动量命令连续两次 evidence JSON 逐 byte 相同 | `daily` 仅完成约 8.2%,缺复权、真实 benchmark、PIT 成分/ST/停牌/涨跌停;`production_ready=false` |
|
||||||
| Mock parity exporter | JoinQuant/QMT wrapper 的 L2—L4 合同回归 | Python ≥3.10 | `mock_contract_only`、`real_platform_pass=false`、`gate_credit=[]` | 不计 G9;必须换成真实平台导出 |
|
| Mock parity exporter | JoinQuant/QMT wrapper 的 L2—L4 合同回归 | Python ≥3.10 | `mock_contract_only`、`real_platform_pass=false`、`gate_credit=[]` | 不计 G9;必须换成真实平台导出 |
|
||||||
|
|
||||||
G1—G10 当前都未通过。JoinQuant hosted smoke 与 Qlib fixture smoke 是真实
|
G1—G10 当前都未通过。JoinQuant TargetPackage run 支持且只支持
|
||||||
runtime 证据,但不是市场有效性或跨平台 Gate 通过证据。
|
`execution.real_platform_observed=true`;输入仍是 `synthetic-research`,
|
||||||
|
Universe/Alpha/Portfolio/Risk 的真实平台观察均为 false。它与旧 momentum
|
||||||
|
smoke、Qlib fixture 一样,不是市场有效性或跨平台 Gate 通过证据。
|
||||||
|
|
||||||
## Stable strategy boundary
|
## Stable strategy boundary
|
||||||
|
|
||||||
当前四引擎共同可冻结的是 `portable-momentum-v1`:
|
`portable-momentum-v1` 当前只作为 smoke 冻结:
|
||||||
|
|
||||||
- canonical symbol normalization;
|
- canonical symbol normalization;
|
||||||
- momentum score/rank;
|
- momentum score/rank;
|
||||||
@@ -36,8 +38,11 @@ runtime 证据,但不是市场有效性或跨平台 Gate 通过证据。
|
|||||||
- T+1/sellable-aware order delta。
|
- T+1/sellable-aware order delta。
|
||||||
- 科创板 200 股最小申报、余额一次性卖清与 hosted 最坏价保护。
|
- 科创板 200 股最小申报、余额一次性卖清与 hosted 最坏价保护。
|
||||||
|
|
||||||
本地 Ridge 与 Qlib Alpha158/LightGBM 是研究候选。没有 versioned model
|
本地 Ridge 已能冻结为 versioned model bundle,并通过
|
||||||
bundle、授权真实 OOS 和跨引擎推理结果前,不能称为已部署 champion。
|
`TargetPackageV1` 把 post-risk 权重移交给 JoinQuant/QMT;平台不需要、也
|
||||||
|
不应重新运行训练栈。一个 synthetic 包已通过真实 JoinQuant execution
|
||||||
|
consumer,但没有授权真实 OOS、真实校准、多期逐层 parity 和 QMT peer,
|
||||||
|
仍不能称为已部署 champion。
|
||||||
|
|
||||||
Qlib 的单一 `limit_threshold` 是市场级近似,不能表达逐日板块/ST 限制;
|
Qlib 的单一 `limit_threshold` 是市场级近似,不能表达逐日板块/ST 限制;
|
||||||
因此它只参加 L1/L2 和诊断 L6。XtTrader mutation 边界目前只接受 `.SH`
|
因此它只参加 L1/L2 和诊断 L6。XtTrader mutation 边界目前只接受 `.SH`
|
||||||
@@ -81,8 +86,13 @@ PYTHONPATH=src:. python -m quant60 snapshot-decision \
|
|||||||
--config configs/baseline.json --as-of 2024-12-31 \
|
--config configs/baseline.json --as-of 2024-12-31 \
|
||||||
--equity 1000000 --output artifacts/csi500-2024-decision
|
--equity 1000000 --output artifacts/csi500-2024-decision
|
||||||
|
|
||||||
# hosted bundle 和 mock-only parity
|
# 默认只构建 portable momentum smoke
|
||||||
python tools/bundle_platforms.py
|
python tools/bundle_platforms.py
|
||||||
|
|
||||||
|
# 显式构建无 fallback 的 TargetPackage 消费器
|
||||||
|
python tools/bundle_platforms.py \
|
||||||
|
--target-package artifacts/research-smoke/target_package.json \
|
||||||
|
--output-dir dist/target-package
|
||||||
PYTHONPATH=src:. python tools/export_mock_parity.py \
|
PYTHONPATH=src:. python tools/export_mock_parity.py \
|
||||||
export --output artifacts/mock-parity/report.json
|
export --output artifacts/mock-parity/report.json
|
||||||
PYTHONPATH=src:. python tools/export_mock_parity.py \
|
PYTHONPATH=src:. python tools/export_mock_parity.py \
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"record_type": "joinquant_target_package_execution_smoke",
|
||||||
|
"platform": "joinquant",
|
||||||
|
"status": "completed",
|
||||||
|
"platform_evidence_class": "real_platform_runtime",
|
||||||
|
"input_evidence_class": "synthetic-research",
|
||||||
|
"performance_claim": false,
|
||||||
|
"observed_at": "2026-07-26T08:50:15Z",
|
||||||
|
"run_id": "2457a7c39a276e09e0fabf99e28978e1",
|
||||||
|
"backtest": {
|
||||||
|
"id": "2457a7c39a276e09e0fabf99e28978e1",
|
||||||
|
"url": "https://www.joinquant.com/algorithm/backtest/detail?backtestId=2457a7c39a276e09e0fabf99e28978e1",
|
||||||
|
"start_date": "2024-03-11",
|
||||||
|
"end_date": "2024-03-13",
|
||||||
|
"initial_capital_cny": 1000000,
|
||||||
|
"frequency": "day",
|
||||||
|
"python": "Python3",
|
||||||
|
"benchmark": "000905.XSHG",
|
||||||
|
"timezone": "Asia/Shanghai"
|
||||||
|
},
|
||||||
|
"artifact_identity": {
|
||||||
|
"bundle_sha256": "0586930ddc483f4489f09b562f4d4232b458b703e6a1194af809b707211be8c4",
|
||||||
|
"browser_snapshot_sha256": "0586930ddc483f4489f09b562f4d4232b458b703e6a1194af809b707211be8c4",
|
||||||
|
"exact_browser_match": true,
|
||||||
|
"bundle_manifest_sha256": "91f4b4dc0bdd0d9f99b3466c830600e2678be78dd991d4499a4154bffec8597a",
|
||||||
|
"portable_core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||||
|
"wrapper_sha256": "0c672b64665b7c1d4409ba288b00d1608e976800af3a4f9a6467767ce4def592"
|
||||||
|
},
|
||||||
|
"target_package": {
|
||||||
|
"package_id": "TP-20240311-795dcfba",
|
||||||
|
"decision_id": "RD-20240311",
|
||||||
|
"release": "quant60-research-candidate-v1",
|
||||||
|
"evidence_class": "synthetic-research",
|
||||||
|
"package_sha256": "baa15c8156aa6cb54d3775d0cd562613a9d0a4f15b0c39f2a60e0f8b8c6f1f3a",
|
||||||
|
"tape_sha256": "5383c376951637b8d4b1ca42fc6db80347abda74c0181c0f70d6c411fb97ea64",
|
||||||
|
"signal_as_of": "2024-03-11T15:00:00+08:00",
|
||||||
|
"next_session": "2024-03-12"
|
||||||
|
},
|
||||||
|
"session_observations": [
|
||||||
|
{
|
||||||
|
"date": "2024-03-11",
|
||||||
|
"event": "TARGET_PACKAGE_NOOP",
|
||||||
|
"exact_clock_match": false,
|
||||||
|
"fallback_invoked": false,
|
||||||
|
"orders": 0,
|
||||||
|
"fills": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2024-03-12",
|
||||||
|
"event": "TARGET_PACKAGE_HIT",
|
||||||
|
"exact_clock_match": true,
|
||||||
|
"price_source": "CURRENT_DATA_DAY_OPEN",
|
||||||
|
"package_weights_exact": true,
|
||||||
|
"orders": 3,
|
||||||
|
"fills": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2024-03-13",
|
||||||
|
"event": "TARGET_PACKAGE_NOOP",
|
||||||
|
"exact_clock_match": false,
|
||||||
|
"fallback_invoked": false,
|
||||||
|
"orders": 0,
|
||||||
|
"fills": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"day_open_prices": {
|
||||||
|
"000001.XSHE": 10.48,
|
||||||
|
"000333.XSHE": 65.35,
|
||||||
|
"000651.XSHE": 40.46,
|
||||||
|
"002415.XSHE": 34.21,
|
||||||
|
"002594.XSHE": 201.0,
|
||||||
|
"300059.XSHE": 13.85,
|
||||||
|
"300750.XSHE": 183.34,
|
||||||
|
"600000.XSHG": 7.11,
|
||||||
|
"600519.XSHG": 1693.94,
|
||||||
|
"601012.XSHG": 22.22,
|
||||||
|
"601318.XSHG": 43.17,
|
||||||
|
"688981.XSHG": 47.86
|
||||||
|
},
|
||||||
|
"target_weights": {
|
||||||
|
"000001.XSHE": 0.0,
|
||||||
|
"000333.XSHE": 0.0,
|
||||||
|
"000651.XSHE": 0.0,
|
||||||
|
"002415.XSHE": 0.0,
|
||||||
|
"002594.XSHE": 0.0,
|
||||||
|
"300059.XSHE": 0.0,
|
||||||
|
"300750.XSHE": 0.0,
|
||||||
|
"600000.XSHG": 0.0,
|
||||||
|
"600519.XSHG": 0.1,
|
||||||
|
"601012.XSHG": 0.1,
|
||||||
|
"601318.XSHG": 0.1,
|
||||||
|
"688981.XSHG": 0.1
|
||||||
|
},
|
||||||
|
"target_quantities": {
|
||||||
|
"000001.XSHE": 0,
|
||||||
|
"000333.XSHE": 0,
|
||||||
|
"000651.XSHE": 0,
|
||||||
|
"002415.XSHE": 0,
|
||||||
|
"002594.XSHE": 0,
|
||||||
|
"300059.XSHE": 0,
|
||||||
|
"300750.XSHE": 0,
|
||||||
|
"600000.XSHG": 0,
|
||||||
|
"600519.XSHG": 0,
|
||||||
|
"601012.XSHG": 4500,
|
||||||
|
"601318.XSHG": 2300,
|
||||||
|
"688981.XSHG": 2000
|
||||||
|
},
|
||||||
|
"lot_rounding": {
|
||||||
|
"symbol": "600519.XSHG",
|
||||||
|
"target_weight": 0.1,
|
||||||
|
"day_open": 1693.94,
|
||||||
|
"board_lot": 100,
|
||||||
|
"target_notional_cny": 100000,
|
||||||
|
"one_board_lot_notional_cny": 169394,
|
||||||
|
"target_quantity": 0,
|
||||||
|
"reason": "one board lot costs more than target notional"
|
||||||
|
},
|
||||||
|
"orders": [
|
||||||
|
{
|
||||||
|
"security": "601012.XSHG",
|
||||||
|
"target_quantity": 4500,
|
||||||
|
"submitted_quantity": 4500,
|
||||||
|
"style": "market",
|
||||||
|
"status": "held"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"security": "601318.XSHG",
|
||||||
|
"target_quantity": 2300,
|
||||||
|
"submitted_quantity": 2300,
|
||||||
|
"style": "market",
|
||||||
|
"status": "held"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"security": "688981.XSHG",
|
||||||
|
"target_quantity": 2000,
|
||||||
|
"submitted_quantity": 2000,
|
||||||
|
"style": "limit",
|
||||||
|
"protection_price": 57.59,
|
||||||
|
"status": "held"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fills": [
|
||||||
|
{
|
||||||
|
"security": "601012.XSHG",
|
||||||
|
"quantity": 4500,
|
||||||
|
"price": 22.22,
|
||||||
|
"time": "2024-03-12T09:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"security": "601318.XSHG",
|
||||||
|
"quantity": 2300,
|
||||||
|
"price": 43.18,
|
||||||
|
"time": "2024-03-12T09:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"security": "688981.XSHG",
|
||||||
|
"quantity": 2000,
|
||||||
|
"price": 47.87,
|
||||||
|
"time": "2024-03-12T09:30:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"counts": {
|
||||||
|
"orders": 3,
|
||||||
|
"fills": 3,
|
||||||
|
"skipped_orders": 0
|
||||||
|
},
|
||||||
|
"performance": {
|
||||||
|
"strategy_return": -0.0029,
|
||||||
|
"annualized_return": -0.2118,
|
||||||
|
"excess_return": -0.0149,
|
||||||
|
"benchmark_return": 0.0122,
|
||||||
|
"alpha": -0.341,
|
||||||
|
"beta": 0.052,
|
||||||
|
"sharpe": -4.712,
|
||||||
|
"max_drawdown": 0.0047,
|
||||||
|
"final_total_value_cny": 997148.04076,
|
||||||
|
"scope": "three-session execution wiring smoke only",
|
||||||
|
"investment_value_claim": false
|
||||||
|
},
|
||||||
|
"claim_boundary": {
|
||||||
|
"real_platform_observed_layers": [
|
||||||
|
"execution"
|
||||||
|
],
|
||||||
|
"upstream_layers_computed_in_joinquant": [],
|
||||||
|
"upstream_layers_real_platform_observed": false,
|
||||||
|
"authorized_real_data_oos_observed": false,
|
||||||
|
"qmt_peer_observed": false,
|
||||||
|
"same_input_cross_engine_parity_observed": false,
|
||||||
|
"all_five_layers_real_platform_observed": false,
|
||||||
|
"g9_passed": false,
|
||||||
|
"baseline_60_passed": false,
|
||||||
|
"release_verified": false,
|
||||||
|
"investment_value_claim": false,
|
||||||
|
"note": "This run proves only the hosted TargetPackage consumer, account/price binding, order submission, and fills for one synthetic-research package."
|
||||||
|
},
|
||||||
|
"redaction": {
|
||||||
|
"contains_account_identifier": false,
|
||||||
|
"contains_cookie": false,
|
||||||
|
"contains_token": false,
|
||||||
|
"contains_credentials": false,
|
||||||
|
"contains_personal_information": false,
|
||||||
|
"raw_screenshots_or_logs_included": false
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-6
@@ -33,22 +33,31 @@
|
|||||||
"current_delivery": {
|
"current_delivery": {
|
||||||
"score": null,
|
"score": null,
|
||||||
"status": "not_scored",
|
"status": "not_scored",
|
||||||
"reason": "Quant OS 已有可执行本地流水线、真实 Qlib fixture smoke、带校验但不完整的 Tushare→Qlib 技术 smoke,以及一次完成的真实聚宽 hosted smoke;但仍缺完整复权 PIT 数据、同输入聚宽比较、真实 QMT、券商影子盘、连续对账与合规证据,因此不能计入 Baseline 60。",
|
"reason": "Quant OS 已接通本地 synthetic 五层候选链,能冻结 Ridge ModelBundle、逐层 trace 和无股数 TargetPackageV1;一个 synthetic-research TargetPackage 已在真实 JoinQuant hosted 环境完成消费、账户/开盘价绑定、3 笔委托和 3 笔成交,因此 execution 层已有真实平台观察。但真实 provider-data 长样本/OOS 尚未接入,JoinQuant 没有重跑上游 Universe/Alpha/Portfolio/Risk,QMT peer 与逐层真实数据 parity 仍缺失,factor risk、CVXPY 和 guarded TWAP/POV 也尚未进入当前主链。因此它仍是 NOT_BASELINE_60,G9 未通过。",
|
||||||
"verified_supporting_facts": [
|
"verified_supporting_facts": [
|
||||||
"The latest standard-library suite completed 224 tests with OK and 6 optional-runtime skips; the CPython 3.12 pyqlib environment completed the same 224 tests with the native Qlib smoke enabled and no skips.",
|
"The current standard-library suite completed 285 tests with OK and 6 optional-runtime skips; successful local tests do not prove real-platform, broker or compliance gates.",
|
||||||
|
"The local synthetic research entrypoint now executes one causally linked Universe → Ridge Alpha → cost/risk-aware Portfolio → post-risk gate → reference Execution path, freezes a replayable ModelBundle, and publishes a hash-linked TargetPackageV1 and five-layer trace. This is structural synthetic evidence only.",
|
||||||
|
"The connected candidate currently uses 60-session diagonal realized variance, explicit spread/fee/square-root-impact cost forecasts, the deterministic constrained optimizer, a post-target risk gate, and reference lot/T+1 order deltas. factor_risk.py, the optional CVXPY solver mode, and guarded TWAP/POV are not selected by the current main slice.",
|
||||||
|
"JoinQuant and QMT target-package modes verify an exact embedded package tape, match the declared dual clock, bind platform equity/positions/sellable quantities/prices, and reject missing, duplicate or tampered packages without falling back to momentum. Both modes have local harness/contract evidence; JoinQuant execution alone now also has one narrow real hosted smoke.",
|
||||||
|
"JoinQuant backtest 2457a7c39a276e09e0fabf99e28978e1 ran the exact FINAL4 bundle for 2024-03-11 through 2024-03-13 with CNY 1,000,000, daily Python3 and CSI 500 benchmark. It observed 3/11 and 3/13 no-op, an exact package hit on 3/12 using CURRENT_DATA_DAY_OPEN, exact package weights, three accepted orders and three fills. The input evidence class remains synthetic-research; only execution.real_platform_observed is true, performance_claim is false, and this run earns no G9 or Baseline score.",
|
||||||
"The JQData adapter maps raw bars, adjustment factor, previous close, daily limits, pause state, PIT is_st and daily PIT index membership into a semantically revalidated snapshot; it rejects an end date that has not reached the provider's complete daily-bar boundary, and only fake-provider tests have been run so far.",
|
"The JQData adapter maps raw bars, adjustment factor, previous close, daily limits, pause state, PIT is_st and daily PIT index membership into a semantically revalidated snapshot; it rejects an end date that has not reached the provider's complete daily-bar boundary, and only fake-provider tests have been run so far.",
|
||||||
"The local snapshot can drive snapshot-backtest and snapshot-decision with manifest verification; it freezes a cross-checked provider trading calendar, and a T-close signal can bind only to a broker observation in the unique next session's Asia/Shanghai [09:00,09:30) window without collapsing the two clocks.",
|
"The local snapshot can drive snapshot-backtest and snapshot-decision with manifest verification; it freezes a cross-checked provider trading calendar, and a T-close signal can bind only to a broker observation in the unique next session's Asia/Shanghai [09:00,09:30) window without collapsing the two clocks.",
|
||||||
"CPython 3.12 with pyqlib 0.9.7 completed a native momentum fixture smoke with 24 signals and saved audited portfolio-table hashes, bounds and recomputed metrics.",
|
"CPython 3.12 with pyqlib 0.9.7 completed a native momentum fixture smoke with 24 signals and saved audited portfolio-table hashes, bounds and recomputed metrics.",
|
||||||
"Qlib Alpha158, LightGBM and Recorder completed a real local fixture smoke with audited portfolio output; the deterministic two-stock synthetic fixture has no investment-value meaning.",
|
"Qlib Alpha158, LightGBM and Recorder completed a real local fixture smoke with audited portfolio output; the deterministic two-stock synthetic fixture has no investment-value meaning.",
|
||||||
"The local Tushare inventory verified all 93 completed Parquet files against recorded size, SHA-256 and row count. An immutable managed Qlib provider and the same early-history momentum command produced byte-identical evidence JSON twice with PYTHONHASHSEED=0. The mirror is only about 8.2% complete for daily partitions and lacks adjustment, real benchmark, point-in-time membership, ST, suspension and limit data, so production_ready and investment_value_claim remain false and no gate credit is earned.",
|
"The local Tushare inventory verified all 93 completed Parquet files against recorded size, SHA-256 and row count. An immutable managed Qlib provider and the same early-history momentum command produced byte-identical evidence JSON twice with PYTHONHASHSEED=0. The mirror is only about 8.2% complete for daily partitions and lacks adjustment, real benchmark, point-in-time membership, ST, suspension and limit data, so production_ready and investment_value_claim remain false and no gate credit is earned.",
|
||||||
"A real JoinQuant daily hosted backtest completed for 2024-01-02 through 2024-12-31 with CNY 1,000,000; the visible completed-run log had zero ERROR/Traceback/order-rejection entries. This proves hosted runtime execution only: exact input/plan/order/fill export and a local same-input peer are still missing.",
|
"A real JoinQuant daily hosted backtest completed for 2024-01-02 through 2024-12-31 with CNY 1,000,000; the visible completed-run log had zero ERROR/Traceback/order-rejection entries. That run used portable_momentum_smoke, not the Ridge/TargetPackage Baseline candidate, and earns no candidate or cross-engine gate credit.",
|
||||||
"The QMT shadow CLI is strictly read-only and produces HMAC account-bound broker observation, broker snapshot and target-diff artifacts. QMT_SHADOW_EVIDENCE_V1 authenticates semantic content, deterministic published bytes, the original decision, planner/engine/operator source and fixed policy; semantic replay verifies exact derived state. Independent local QA completed 58 deterministic scenarios, 500 malformed fuzz cases and 300 legal-observation fuzz cases with no P0/P1. This is Quant OS publisher integrity, not broker attestation, and it still has only fake-broker evidence.",
|
"The QMT shadow CLI is strictly read-only and produces HMAC account-bound broker observation, broker snapshot and target-diff artifacts. QMT_SHADOW_EVIDENCE_V1 authenticates semantic content, deterministic published bytes, the original decision, planner/engine/operator source and fixed policy; semantic replay verifies exact derived state. Independent local QA completed 58 deterministic scenarios, 500 malformed fuzz cases and 300 legal-observation fuzz cases with no P0/P1. This is Quant OS publisher integrity, not broker attestation, and it still has only fake-broker evidence.",
|
||||||
"JoinQuant/QMT mock parity is mock_contract_only, real_platform_pass is false and it earns no G9 gate credit."
|
"JoinQuant/QMT mock parity is mock_contract_only, real_platform_pass is false and it earns no G9 gate credit."
|
||||||
],
|
],
|
||||||
|
"internal_delivery_gaps": [
|
||||||
|
"The authorized provider-data path has not yet been connected end to end to the frozen Ridge ModelBundle, calibrated risk/cost inputs, five-layer Baseline candidate and TargetPackage tape; the current connected candidate remains synthetic.",
|
||||||
|
"factor_risk.py, the optional CVXPY optimizer mode and guarded TWAP/POV exist as components or optional paths but are not selected by the current Baseline main slice; the connected risk model is 60-session diagonal realized variance and execution is the reference one-shot planner.",
|
||||||
|
"One synthetic-research TargetPackageV1 has been observed at the real JoinQuant execution boundary, but JoinQuant did not recompute the upstream four layers; there is no authorized real-data multi-period tape, same-input layer-by-layer parity, or real QMT peer."
|
||||||
|
],
|
||||||
"external_evidence_missing": [
|
"external_evidence_missing": [
|
||||||
"authorized JQData market snapshot and licence/lineage record",
|
"authorized JQData market snapshot, licence/lineage record and a frozen provider-data candidate tape",
|
||||||
"complete JoinQuant hosted input/plan/order/fill export and same-input local comparison",
|
"authorized real-data multi-period JoinQuant TargetPackage run plus same-input local/JoinQuant layer-by-layer reconciliation",
|
||||||
"real QMT built-in and qmttools backtest exports",
|
"real QMT TargetPackage built-in and qmttools backtest exports",
|
||||||
"real QMT historical-ST positive control, terminal/xtquant build record, broker query contract and trade-amount semantic probes, callback/restart and at least 20 trading days of shadow evidence",
|
"real QMT historical-ST positive control, terminal/xtquant build record, broker query contract and trade-amount semantic probes, callback/restart and at least 20 trading days of shadow evidence",
|
||||||
"continuous reconciliation, recovery drills and broker compliance confirmation"
|
"continuous reconciliation, recovery drills and broker compliance confirmation"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ class FakeJoinQuantHarness(object):
|
|||||||
is_st=False,
|
is_st=False,
|
||||||
high_limit=9999.0,
|
high_limit=9999.0,
|
||||||
low_limit=0.01,
|
low_limit=0.01,
|
||||||
|
day_open=float(values[-1]),
|
||||||
|
last_price=float(values[-1]),
|
||||||
)
|
)
|
||||||
for symbol in histories
|
for symbol, values in self.histories.items()
|
||||||
}
|
}
|
||||||
self.context = SimpleNamespace(
|
self.context = SimpleNamespace(
|
||||||
current_dt=datetime.datetime(2026, 7, 20, 9, 30),
|
current_dt=datetime.datetime(2026, 7, 20, 9, 30),
|
||||||
|
|||||||
+670
-10
@@ -6,9 +6,18 @@ tool replaces the marked import block with the exact portable core source.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# __PORTABLE_CORE_BUNDLE_START__
|
# __PORTABLE_CORE_BUNDLE_START__
|
||||||
from quant60.portable_core import build_rebalance_plan, convert_symbol
|
from quant60.portable_core import (
|
||||||
|
build_rebalance_plan,
|
||||||
|
build_target_weight_plan,
|
||||||
|
convert_symbol,
|
||||||
|
lookup_target_package_exact,
|
||||||
|
target_package_tape_sha256,
|
||||||
|
verify_target_package,
|
||||||
|
)
|
||||||
# __PORTABLE_CORE_BUNDLE_END__
|
# __PORTABLE_CORE_BUNDLE_END__
|
||||||
|
|
||||||
|
import base64 as _q60_base64
|
||||||
|
import json as _q60_json
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +35,9 @@ class ClockStateError(RuntimeError):
|
|||||||
|
|
||||||
# __BASELINE_CONFIG_START__
|
# __BASELINE_CONFIG_START__
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
|
"decision_mode": "portable_momentum_smoke",
|
||||||
|
"target_package_count": 0,
|
||||||
|
"target_package_tape_sha256": None,
|
||||||
"universe_mode": "pit_index",
|
"universe_mode": "pit_index",
|
||||||
"index_symbol": "000905.XSHG",
|
"index_symbol": "000905.XSHG",
|
||||||
"qmt_sector_name": "中证500",
|
"qmt_sector_name": "中证500",
|
||||||
@@ -56,12 +68,353 @@ CONFIG = {
|
|||||||
}
|
}
|
||||||
# __BASELINE_CONFIG_END__
|
# __BASELINE_CONFIG_END__
|
||||||
|
|
||||||
|
# __TARGET_PACKAGE_TAPE_START__
|
||||||
|
TARGET_PACKAGES = []
|
||||||
|
# __TARGET_PACKAGE_TAPE_END__
|
||||||
|
|
||||||
|
|
||||||
|
MOMENTUM_SMOKE_MODE = "portable_momentum_smoke"
|
||||||
|
TARGET_PACKAGE_MODE = "target_package"
|
||||||
|
EVIDENCE_PREFIX = "QUANT60_JOINQUANT_EVIDENCE_V1 "
|
||||||
|
EVIDENCE_CHUNK_CHARACTERS = 1800
|
||||||
|
TARGET_PRICE_SOURCE = "CURRENT_DATA_DAY_OPEN"
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence_value(value, depth=0):
|
||||||
|
"""Return stable JSON data without allowing observation to stop trading."""
|
||||||
|
if depth > 8:
|
||||||
|
return "<max-depth>"
|
||||||
|
if value is None or isinstance(value, (bool, int, str)):
|
||||||
|
return value
|
||||||
|
if isinstance(value, float):
|
||||||
|
return value if math.isfinite(value) else None
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {
|
||||||
|
str(key): _evidence_value(item, depth + 1)
|
||||||
|
for key, item in sorted(
|
||||||
|
value.items(),
|
||||||
|
key=lambda pair: str(pair[0]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if isinstance(value, (list, tuple)):
|
||||||
|
return [_evidence_value(item, depth + 1) for item in value]
|
||||||
|
isoformat = getattr(value, "isoformat", None)
|
||||||
|
if callable(isoformat):
|
||||||
|
try:
|
||||||
|
return str(isoformat())
|
||||||
|
except Exception:
|
||||||
|
return "<isoformat-error>"
|
||||||
|
try:
|
||||||
|
return str(value)
|
||||||
|
except Exception:
|
||||||
|
return "<string-error>"
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence_context_time(context):
|
||||||
|
try:
|
||||||
|
return _evidence_value(getattr(context, "current_dt", None))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_evidence(
|
||||||
|
event,
|
||||||
|
payload=None,
|
||||||
|
context=None,
|
||||||
|
chunk_index=None,
|
||||||
|
chunk_count=None,
|
||||||
|
):
|
||||||
|
"""Print one prefixed canonical JSON record and always fail soft."""
|
||||||
|
try:
|
||||||
|
sequence = int(
|
||||||
|
getattr(g, "quant60_evidence_sequence", 0) or 0
|
||||||
|
) + 1
|
||||||
|
g.quant60_evidence_sequence = sequence
|
||||||
|
record = {
|
||||||
|
"context_time": _evidence_context_time(context),
|
||||||
|
"decision_mode": getattr(
|
||||||
|
g,
|
||||||
|
"quant60_decision_mode",
|
||||||
|
CONFIG.get("decision_mode"),
|
||||||
|
),
|
||||||
|
"event": str(event),
|
||||||
|
"payload": _evidence_value(payload or {}),
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"sequence": sequence,
|
||||||
|
"source": "joinquant_hosted",
|
||||||
|
}
|
||||||
|
if chunk_index is not None or chunk_count is not None:
|
||||||
|
if (
|
||||||
|
type(chunk_index) is not int
|
||||||
|
or type(chunk_count) is not int
|
||||||
|
or chunk_index <= 0
|
||||||
|
or chunk_count <= 0
|
||||||
|
or chunk_index > chunk_count
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
record["chunk_index"] = chunk_index
|
||||||
|
record["chunk_count"] = chunk_count
|
||||||
|
encoded = _q60_json.dumps(
|
||||||
|
record,
|
||||||
|
ensure_ascii=True,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
)
|
||||||
|
print(EVIDENCE_PREFIX + encoded)
|
||||||
|
except Exception:
|
||||||
|
# Evidence is observational. A logging/serialization failure must
|
||||||
|
# never alter an already verified decision or hosted order path.
|
||||||
|
return None
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_chunked_evidence(event, payload, context=None):
|
||||||
|
"""Emit reconstructable base64 chunks of one canonical JSON payload."""
|
||||||
|
try:
|
||||||
|
canonical = _q60_json.dumps(
|
||||||
|
_evidence_value(payload),
|
||||||
|
ensure_ascii=True,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("ascii")
|
||||||
|
encoded = _q60_base64.b64encode(canonical).decode("ascii")
|
||||||
|
chunks = [
|
||||||
|
encoded[index : index + EVIDENCE_CHUNK_CHARACTERS]
|
||||||
|
for index in range(0, len(encoded), EVIDENCE_CHUNK_CHARACTERS)
|
||||||
|
] or [""]
|
||||||
|
count = len(chunks)
|
||||||
|
for index, content in enumerate(chunks, 1):
|
||||||
|
_emit_evidence(
|
||||||
|
event,
|
||||||
|
{
|
||||||
|
"chunk_encoding": "base64-canonical-json",
|
||||||
|
"content": content,
|
||||||
|
"encoded_length": len(encoded),
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
chunk_index=index,
|
||||||
|
chunk_count=count,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def _observe_object(value, fields):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
output = {"object_type": type(value).__name__}
|
||||||
|
for name in fields:
|
||||||
|
try:
|
||||||
|
item = getattr(value, name)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if callable(item):
|
||||||
|
continue
|
||||||
|
output[name] = _evidence_value(item)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def _observe_order(value):
|
||||||
|
return _observe_object(
|
||||||
|
value,
|
||||||
|
(
|
||||||
|
"order_id",
|
||||||
|
"add_time",
|
||||||
|
"security",
|
||||||
|
"amount",
|
||||||
|
"filled",
|
||||||
|
"price",
|
||||||
|
"avg_cost",
|
||||||
|
"side",
|
||||||
|
"action",
|
||||||
|
"status",
|
||||||
|
"target_quantity",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _observe_trade(value):
|
||||||
|
return _observe_object(
|
||||||
|
value,
|
||||||
|
(
|
||||||
|
"trade_id",
|
||||||
|
"order_id",
|
||||||
|
"time",
|
||||||
|
"security",
|
||||||
|
"amount",
|
||||||
|
"price",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _observe_platform_collection(api_name, observer):
|
||||||
|
"""Read an optional hosted collection without assuming API availability."""
|
||||||
|
function = globals().get(api_name)
|
||||||
|
if not callable(function):
|
||||||
|
return {
|
||||||
|
"available": False,
|
||||||
|
"reason": "API_UNAVAILABLE",
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
raw = function()
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"available": False,
|
||||||
|
"error_type": type(exc).__name__,
|
||||||
|
"reason": "API_CALL_FAILED",
|
||||||
|
}
|
||||||
|
if hasattr(raw, "items"):
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
"key": str(key),
|
||||||
|
"value": observer(value),
|
||||||
|
}
|
||||||
|
for key, value in sorted(
|
||||||
|
raw.items(),
|
||||||
|
key=lambda pair: str(pair[0]),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
elif isinstance(raw, (list, tuple)):
|
||||||
|
items = [observer(value) for value in raw]
|
||||||
|
elif raw is None:
|
||||||
|
items = []
|
||||||
|
else:
|
||||||
|
items = [observer(raw)]
|
||||||
|
return {
|
||||||
|
"available": True,
|
||||||
|
"count": len(items),
|
||||||
|
"items": items,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _observe_portfolio(context):
|
||||||
|
try:
|
||||||
|
portfolio = context.portfolio
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"available": False,
|
||||||
|
"error_type": type(exc).__name__,
|
||||||
|
}
|
||||||
|
output = {
|
||||||
|
"available": True,
|
||||||
|
"fields": {},
|
||||||
|
"positions": [],
|
||||||
|
}
|
||||||
|
for name in (
|
||||||
|
"total_value",
|
||||||
|
"cash",
|
||||||
|
"available_cash",
|
||||||
|
"positions_value",
|
||||||
|
"returns",
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
value = getattr(portfolio, name)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
output["fields"][name] = _evidence_value(value)
|
||||||
|
try:
|
||||||
|
positions = getattr(portfolio, "positions", {})
|
||||||
|
if hasattr(positions, "items"):
|
||||||
|
for symbol, position in sorted(
|
||||||
|
positions.items(),
|
||||||
|
key=lambda pair: str(pair[0]),
|
||||||
|
):
|
||||||
|
item = _observe_object(
|
||||||
|
position,
|
||||||
|
(
|
||||||
|
"total_amount",
|
||||||
|
"closeable_amount",
|
||||||
|
"price",
|
||||||
|
"avg_cost",
|
||||||
|
"value",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
item["symbol"] = str(symbol)
|
||||||
|
output["positions"].append(item)
|
||||||
|
except Exception as exc:
|
||||||
|
output["positions_error_type"] = type(exc).__name__
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_evidence(plan):
|
||||||
|
if not isinstance(plan, dict):
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"decision_mode": plan.get("decision_mode"),
|
||||||
|
"execution_contract": plan.get("execution_contract"),
|
||||||
|
"lineage": plan.get("lineage"),
|
||||||
|
"orders": plan.get("orders"),
|
||||||
|
"targets": plan.get("targets"),
|
||||||
|
"universe": plan.get("universe"),
|
||||||
|
"weights": plan.get("weights"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_target_packages(config):
|
||||||
|
mode = config.get("decision_mode")
|
||||||
|
if mode not in (MOMENTUM_SMOKE_MODE, TARGET_PACKAGE_MODE):
|
||||||
|
raise ValueError("unsupported decision_mode")
|
||||||
|
if not isinstance(TARGET_PACKAGES, list):
|
||||||
|
raise ValueError("TARGET_PACKAGES must be a list")
|
||||||
|
expected_count = config.get("target_package_count")
|
||||||
|
expected_sha256 = config.get("target_package_tape_sha256")
|
||||||
|
if mode == MOMENTUM_SMOKE_MODE:
|
||||||
|
if (
|
||||||
|
type(expected_count) is not int
|
||||||
|
or expected_count != 0
|
||||||
|
or expected_sha256 is not None
|
||||||
|
or TARGET_PACKAGES
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"portable_momentum_smoke cannot carry target packages"
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
if type(expected_count) is not int or expected_count <= 0:
|
||||||
|
raise ValueError(
|
||||||
|
"target_package mode requires a positive target_package_count"
|
||||||
|
)
|
||||||
|
if expected_count != len(TARGET_PACKAGES):
|
||||||
|
raise ValueError("target package count mismatch")
|
||||||
|
actual_sha256 = target_package_tape_sha256(TARGET_PACKAGES)
|
||||||
|
if expected_sha256 != actual_sha256:
|
||||||
|
raise ValueError("target package tape sha256 mismatch")
|
||||||
|
verified = []
|
||||||
|
package_ids = set()
|
||||||
|
decision_ids = set()
|
||||||
|
clocks = set()
|
||||||
|
for raw_package in TARGET_PACKAGES:
|
||||||
|
package = verify_target_package(raw_package)
|
||||||
|
package_id = package["package_id"]
|
||||||
|
decision_id = package["decision_id"]
|
||||||
|
clock = (package["signal_as_of"], package["next_session"])
|
||||||
|
if (
|
||||||
|
package_id in package_ids
|
||||||
|
or decision_id in decision_ids
|
||||||
|
or clock in clocks
|
||||||
|
):
|
||||||
|
raise ValueError("duplicate target package decision identity")
|
||||||
|
package_ids.add(package_id)
|
||||||
|
decision_ids.add(decision_id)
|
||||||
|
clocks.add(clock)
|
||||||
|
verified.append(package)
|
||||||
|
return verified
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
"""Configure a daily-open clock that executes a first-week-close signal."""
|
"""Configure the selected daily-open decision mode."""
|
||||||
set_option("avoid_future_data", True)
|
set_option("avoid_future_data", True)
|
||||||
set_option("use_real_price", True)
|
set_option("use_real_price", True)
|
||||||
|
g.quant60_evidence_sequence = 0
|
||||||
|
g.quant60_order_observations = []
|
||||||
g.quant60_config = dict(CONFIG)
|
g.quant60_config = dict(CONFIG)
|
||||||
|
g.quant60_target_packages = _validated_target_packages(
|
||||||
|
g.quant60_config
|
||||||
|
)
|
||||||
|
g.quant60_decision_mode = g.quant60_config["decision_mode"]
|
||||||
|
if g.quant60_config["decision_mode"] == MOMENTUM_SMOKE_MODE:
|
||||||
if g.quant60_config.get("rebalance_schedule") != "weekly_first_close":
|
if g.quant60_config.get("rebalance_schedule") != "weekly_first_close":
|
||||||
raise ValueError("unsupported rebalance_schedule")
|
raise ValueError("unsupported rebalance_schedule")
|
||||||
if g.quant60_config.get("universe_mode") not in ("pit_index", "fixed"):
|
if g.quant60_config.get("universe_mode") not in ("pit_index", "fixed"):
|
||||||
@@ -108,6 +461,30 @@ def initialize(context):
|
|||||||
g.quant60_pending_first_session = None
|
g.quant60_pending_first_session = None
|
||||||
g.quant60_last_callback_date = None
|
g.quant60_last_callback_date = None
|
||||||
run_daily(rebalance, time="open")
|
run_daily(rebalance, time="open")
|
||||||
|
_emit_evidence(
|
||||||
|
"INIT",
|
||||||
|
{
|
||||||
|
"benchmark": g.quant60_config["index_symbol"],
|
||||||
|
"package_count": len(g.quant60_target_packages),
|
||||||
|
"package_ids": [
|
||||||
|
package["package_id"]
|
||||||
|
for package in g.quant60_target_packages
|
||||||
|
],
|
||||||
|
"rebalance_callback": "run_daily_open",
|
||||||
|
"target_package_tape_sha256": g.quant60_config.get(
|
||||||
|
"target_package_tape_sha256"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _decision_mode():
|
||||||
|
configured = g.quant60_config.get("decision_mode")
|
||||||
|
frozen = getattr(g, "quant60_decision_mode", configured)
|
||||||
|
if configured != frozen:
|
||||||
|
raise ValueError("decision_mode cannot change after initialize")
|
||||||
|
return frozen
|
||||||
|
|
||||||
|
|
||||||
def _close_history(symbol, count):
|
def _close_history(symbol, count):
|
||||||
@@ -262,7 +639,10 @@ def _snapshot_portfolio(context, managed):
|
|||||||
current = {}
|
current = {}
|
||||||
sellable = {}
|
sellable = {}
|
||||||
managed = set(convert_symbol(symbol, "canonical") for symbol in managed)
|
managed = set(convert_symbol(symbol, "canonical") for symbol in managed)
|
||||||
dynamic = g.quant60_config.get("universe_mode") == "pit_index"
|
dynamic = (
|
||||||
|
_decision_mode() == MOMENTUM_SMOKE_MODE
|
||||||
|
and g.quant60_config.get("universe_mode") == "pit_index"
|
||||||
|
)
|
||||||
positions = getattr(context.portfolio, "positions", {})
|
positions = getattr(context.portfolio, "positions", {})
|
||||||
for symbol, position in positions.items():
|
for symbol, position in positions.items():
|
||||||
canonical = convert_symbol(str(symbol), "canonical")
|
canonical = convert_symbol(str(symbol), "canonical")
|
||||||
@@ -342,8 +722,156 @@ def _order_request(symbol, delta):
|
|||||||
return {"style": style}
|
return {"style": style}
|
||||||
|
|
||||||
|
|
||||||
|
def _target_package_for_open(context):
|
||||||
|
current_date = _as_date(getattr(context, "current_dt", None), "current_dt")
|
||||||
|
previous_date = _as_date(
|
||||||
|
getattr(context, "previous_date", None),
|
||||||
|
"previous_date",
|
||||||
|
)
|
||||||
|
signal_as_of = (
|
||||||
|
previous_date.isoformat() + "T15:00:00+08:00"
|
||||||
|
)
|
||||||
|
next_session = current_date.isoformat()
|
||||||
|
config = g.quant60_config
|
||||||
|
packages = _validated_target_packages(config)
|
||||||
|
matches = [
|
||||||
|
package
|
||||||
|
for package in packages
|
||||||
|
if package["signal_as_of"] == signal_as_of
|
||||||
|
and package["next_session"] == next_session
|
||||||
|
]
|
||||||
|
if not matches:
|
||||||
|
_emit_evidence(
|
||||||
|
"TARGET_PACKAGE_NOOP",
|
||||||
|
{
|
||||||
|
"fallback_invoked": False,
|
||||||
|
"next_session": next_session,
|
||||||
|
"orders_submitted": 0,
|
||||||
|
"package_count": len(packages),
|
||||||
|
"reason": "NO_EXACT_CLOCK_MATCH",
|
||||||
|
"signal_as_of": signal_as_of,
|
||||||
|
"target_package_tape_sha256": config[
|
||||||
|
"target_package_tape_sha256"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise ValueError("ambiguous target package clock")
|
||||||
|
package = lookup_target_package_exact(
|
||||||
|
packages,
|
||||||
|
signal_as_of,
|
||||||
|
next_session,
|
||||||
|
decision_id=matches[0]["decision_id"],
|
||||||
|
expected_tape_sha256=config["target_package_tape_sha256"],
|
||||||
|
expected_count=config["target_package_count"],
|
||||||
|
)
|
||||||
|
_emit_evidence(
|
||||||
|
"TARGET_PACKAGE_HIT",
|
||||||
|
{
|
||||||
|
"lineage": _target_lineage(package),
|
||||||
|
"target_package_tape_sha256": config[
|
||||||
|
"target_package_tape_sha256"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
return package
|
||||||
|
|
||||||
|
|
||||||
|
def _target_prices(symbols):
|
||||||
|
try:
|
||||||
|
current_data = get_current_data()
|
||||||
|
except Exception as exc:
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"current_data is unavailable: %s" % exc
|
||||||
|
)
|
||||||
|
prices = {}
|
||||||
|
for canonical in sorted(symbols):
|
||||||
|
symbol = convert_symbol(canonical, "joinquant")
|
||||||
|
try:
|
||||||
|
raw_price = getattr(current_data[symbol], "day_open")
|
||||||
|
price = float(raw_price)
|
||||||
|
except (KeyError, AttributeError, TypeError, ValueError):
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"%s has no usable current day_open" % symbol
|
||||||
|
)
|
||||||
|
if not math.isfinite(price) or price <= 0.0:
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"%s has no usable current day_open" % symbol
|
||||||
|
)
|
||||||
|
prices[canonical] = price
|
||||||
|
return prices
|
||||||
|
|
||||||
|
|
||||||
|
def _target_lineage(package):
|
||||||
|
return {
|
||||||
|
"package_id": package["package_id"],
|
||||||
|
"package_sha256": package["package_sha256"],
|
||||||
|
"decision_id": package["decision_id"],
|
||||||
|
"signal_as_of": package["signal_as_of"],
|
||||||
|
"next_session": package["next_session"],
|
||||||
|
"release": package["release"],
|
||||||
|
"model_id": package["model_id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_target_package_plan(context):
|
||||||
|
package = _target_package_for_open(context)
|
||||||
|
if package is None:
|
||||||
|
return None
|
||||||
|
managed = sorted(package["universe"])
|
||||||
|
current, sellable, equity = _snapshot_portfolio(context, managed)
|
||||||
|
if equity <= 0:
|
||||||
|
raise ValueError("portfolio.total_value must be positive")
|
||||||
|
prices = _target_prices(
|
||||||
|
set(package["target_weights"]) | set(current)
|
||||||
|
)
|
||||||
|
_emit_evidence(
|
||||||
|
"PLATFORM_BINDING_INPUT",
|
||||||
|
{
|
||||||
|
"current": current,
|
||||||
|
"equity": equity,
|
||||||
|
"lineage": _target_lineage(package),
|
||||||
|
"price_source": TARGET_PRICE_SOURCE,
|
||||||
|
"prices": prices,
|
||||||
|
"sellable": sellable,
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
plan = build_target_weight_plan(
|
||||||
|
target_weights=package["target_weights"],
|
||||||
|
prices=prices,
|
||||||
|
current=current,
|
||||||
|
sellable=sellable,
|
||||||
|
equity=equity,
|
||||||
|
cash_buffer=g.quant60_config["cash_buffer"],
|
||||||
|
lot_size=g.quant60_config["lot_size"],
|
||||||
|
decision_id=package["decision_id"],
|
||||||
|
package_sha256=package["package_sha256"],
|
||||||
|
)
|
||||||
|
plan["decision_mode"] = TARGET_PACKAGE_MODE
|
||||||
|
plan["lineage"] = _target_lineage(package)
|
||||||
|
plan["universe"] = package["universe"]
|
||||||
|
plan["execution_contract"] = {
|
||||||
|
"signal_price_source": TARGET_PRICE_SOURCE,
|
||||||
|
"submit_trigger": "DECLARED_NEXT_SESSION_OPEN_CALLBACK",
|
||||||
|
"declared_next_session": package["next_session"],
|
||||||
|
"real_platform_observed": False,
|
||||||
|
}
|
||||||
|
_emit_evidence(
|
||||||
|
"TARGET_PACKAGE_PLAN",
|
||||||
|
_plan_evidence(plan),
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
def compute_plan(context):
|
def compute_plan(context):
|
||||||
"""Return the portable plan without submitting orders."""
|
"""Return the selected portable plan without submitting orders."""
|
||||||
|
if _decision_mode() == TARGET_PACKAGE_MODE:
|
||||||
|
return _compute_target_package_plan(context)
|
||||||
config = g.quant60_config
|
config = g.quant60_config
|
||||||
required = int(config["lookback"]) + int(config["skip"]) + 1
|
required = int(config["lookback"]) + int(config["skip"]) + 1
|
||||||
platform_universe = _decision_universe(context)
|
platform_universe = _decision_universe(context)
|
||||||
@@ -375,13 +903,17 @@ def compute_plan(context):
|
|||||||
return plan
|
return plan
|
||||||
|
|
||||||
|
|
||||||
def _execute_rebalance(context):
|
def _submit_plan(context, plan, managed):
|
||||||
"""Submit the portable plan's exact executable share deltas."""
|
"""Submit a portable plan's exact executable share deltas."""
|
||||||
plan = compute_plan(context)
|
if plan is None:
|
||||||
|
return None
|
||||||
|
observe_target_package = (
|
||||||
|
plan.get("decision_mode") == TARGET_PACKAGE_MODE
|
||||||
|
)
|
||||||
order_deltas = plan["orders"]
|
order_deltas = plan["orders"]
|
||||||
current, _, unused_equity = _snapshot_portfolio(
|
current, _, unused_equity = _snapshot_portfolio(
|
||||||
context,
|
context,
|
||||||
g.quant60_last_universe or g.quant60_config["universe"],
|
managed,
|
||||||
)
|
)
|
||||||
del unused_equity
|
del unused_equity
|
||||||
all_symbols = set(current)
|
all_symbols = set(current)
|
||||||
@@ -401,23 +933,107 @@ def _execute_rebalance(context):
|
|||||||
if delta == 0:
|
if delta == 0:
|
||||||
continue
|
continue
|
||||||
platform_symbol = convert_symbol(canonical, "joinquant")
|
platform_symbol = convert_symbol(canonical, "joinquant")
|
||||||
|
skipped_before = len(g.quant60_skipped_orders)
|
||||||
request = _order_request(platform_symbol, delta)
|
request = _order_request(platform_symbol, delta)
|
||||||
if request is None:
|
if request is None:
|
||||||
|
skipped = g.quant60_skipped_orders[skipped_before:]
|
||||||
|
if not skipped:
|
||||||
|
skipped = [
|
||||||
|
{
|
||||||
|
"symbol": platform_symbol,
|
||||||
|
"reason": "ORDER_REQUEST_NOT_BUILT",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if observe_target_package:
|
||||||
|
for item in skipped:
|
||||||
|
_emit_evidence(
|
||||||
|
"SKIPPED_ORDER",
|
||||||
|
{
|
||||||
|
"canonical_symbol": canonical,
|
||||||
|
"delta": delta,
|
||||||
|
"lineage": plan.get("lineage"),
|
||||||
|
"skip": item,
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
target = int(current.get(canonical, 0)) + delta
|
target = int(current.get(canonical, 0)) + delta
|
||||||
|
request_evidence = {
|
||||||
|
"canonical_symbol": canonical,
|
||||||
|
"current_quantity": int(current.get(canonical, 0)),
|
||||||
|
"delta": delta,
|
||||||
|
"lineage": plan.get("lineage"),
|
||||||
|
"platform_symbol": platform_symbol,
|
||||||
|
"style": _observe_object(
|
||||||
|
request["style"],
|
||||||
|
("kind", "limit_price"),
|
||||||
|
),
|
||||||
|
"target_quantity": target,
|
||||||
|
}
|
||||||
|
if observe_target_package:
|
||||||
|
_emit_evidence(
|
||||||
|
"ORDER_REQUEST",
|
||||||
|
request_evidence,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
try:
|
||||||
if request["style"] is None:
|
if request["style"] is None:
|
||||||
order_target(platform_symbol, target)
|
order_result = order_target(platform_symbol, target)
|
||||||
else:
|
else:
|
||||||
order_target(
|
order_result = order_target(
|
||||||
platform_symbol,
|
platform_symbol,
|
||||||
target,
|
target,
|
||||||
style=request["style"],
|
style=request["style"],
|
||||||
)
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
if observe_target_package:
|
||||||
|
_emit_evidence(
|
||||||
|
"ORDER_RETURN",
|
||||||
|
{
|
||||||
|
"error_type": type(exc).__name__,
|
||||||
|
"lineage": plan.get("lineage"),
|
||||||
|
"outcome": "EXCEPTION",
|
||||||
|
"platform_symbol": platform_symbol,
|
||||||
|
"target_quantity": target,
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
observation = {
|
||||||
|
"lineage": plan.get("lineage"),
|
||||||
|
"order": _observe_order(order_result),
|
||||||
|
"outcome": "RETURNED",
|
||||||
|
"platform_symbol": platform_symbol,
|
||||||
|
"target_quantity": target,
|
||||||
|
}
|
||||||
|
if observe_target_package:
|
||||||
|
g.quant60_order_observations.append(observation)
|
||||||
|
_emit_evidence(
|
||||||
|
"ORDER_RETURN",
|
||||||
|
observation,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
g.quant60_last_plan = plan
|
g.quant60_last_plan = plan
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_rebalance(context):
|
||||||
|
"""Submit the selected portable plan's exact executable share deltas."""
|
||||||
|
plan = compute_plan(context)
|
||||||
|
if plan is None:
|
||||||
|
return None
|
||||||
|
managed = (
|
||||||
|
sorted(plan["universe"])
|
||||||
|
if "universe" in plan
|
||||||
|
else (
|
||||||
|
g.quant60_last_universe
|
||||||
|
or g.quant60_config["universe"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return _submit_plan(context, plan, managed)
|
||||||
|
|
||||||
|
|
||||||
def _as_date(value, name):
|
def _as_date(value, name):
|
||||||
if hasattr(value, "date"):
|
if hasattr(value, "date"):
|
||||||
value = value.date()
|
value = value.date()
|
||||||
@@ -439,6 +1055,16 @@ def rebalance(context):
|
|||||||
)
|
)
|
||||||
if g.quant60_last_callback_date == current_date:
|
if g.quant60_last_callback_date == current_date:
|
||||||
return None
|
return None
|
||||||
|
if _decision_mode() == TARGET_PACKAGE_MODE:
|
||||||
|
# EOD evidence is session-scoped. Clear the prior session only after
|
||||||
|
# the duplicate-date guard so a same-day retry cannot erase evidence.
|
||||||
|
g.quant60_last_plan = None
|
||||||
|
g.quant60_order_observations = []
|
||||||
|
g.quant60_skipped_orders = []
|
||||||
|
# Consume the daily clock token before crossing the hosted order
|
||||||
|
# boundary so callback retries cannot duplicate an accepted prefix.
|
||||||
|
g.quant60_last_callback_date = current_date
|
||||||
|
return _execute_rebalance(context)
|
||||||
pending = g.quant60_pending_first_session
|
pending = g.quant60_pending_first_session
|
||||||
is_first_session = (
|
is_first_session = (
|
||||||
previous_date.isocalendar()[:2]
|
previous_date.isocalendar()[:2]
|
||||||
@@ -463,3 +1089,37 @@ def rebalance(context):
|
|||||||
g.quant60_pending_first_session = current_date
|
g.quant60_pending_first_session = current_date
|
||||||
g.quant60_last_callback_date = current_date
|
g.quant60_last_callback_date = current_date
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def after_trading_end(context):
|
||||||
|
"""Emit a best-effort end-of-day observation without changing state."""
|
||||||
|
if _decision_mode() != TARGET_PACKAGE_MODE:
|
||||||
|
return None
|
||||||
|
return _emit_chunked_evidence(
|
||||||
|
"EOD_STATUS",
|
||||||
|
{
|
||||||
|
"last_plan": _plan_evidence(
|
||||||
|
getattr(g, "quant60_last_plan", None)
|
||||||
|
),
|
||||||
|
"order_returns": getattr(
|
||||||
|
g,
|
||||||
|
"quant60_order_observations",
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
"orders_api": _observe_platform_collection(
|
||||||
|
"get_orders",
|
||||||
|
_observe_order,
|
||||||
|
),
|
||||||
|
"portfolio": _observe_portfolio(context),
|
||||||
|
"skipped_orders": getattr(
|
||||||
|
g,
|
||||||
|
"quant60_skipped_orders",
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
"trades_api": _observe_platform_collection(
|
||||||
|
"get_trades",
|
||||||
|
_observe_trade,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,7 +2,14 @@
|
|||||||
"""QMT built-in Python 3.6 strategy wrapper. Keep this source ASCII-only."""
|
"""QMT built-in Python 3.6 strategy wrapper. Keep this source ASCII-only."""
|
||||||
|
|
||||||
# __PORTABLE_CORE_BUNDLE_START__
|
# __PORTABLE_CORE_BUNDLE_START__
|
||||||
from quant60.portable_core import build_rebalance_plan, convert_symbol
|
from quant60.portable_core import (
|
||||||
|
build_rebalance_plan,
|
||||||
|
build_target_weight_plan,
|
||||||
|
convert_symbol,
|
||||||
|
lookup_target_package_exact,
|
||||||
|
target_package_tape_sha256,
|
||||||
|
verify_target_package,
|
||||||
|
)
|
||||||
# __PORTABLE_CORE_BUNDLE_END__
|
# __PORTABLE_CORE_BUNDLE_END__
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
@@ -34,6 +41,9 @@ g = G()
|
|||||||
# __BASELINE_CONFIG_START__
|
# __BASELINE_CONFIG_START__
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"account_id": "test",
|
"account_id": "test",
|
||||||
|
"decision_mode": "portable_momentum_smoke",
|
||||||
|
"target_package_count": 0,
|
||||||
|
"target_package_tape_sha256": None,
|
||||||
"universe_mode": "pit_index",
|
"universe_mode": "pit_index",
|
||||||
"index_symbol": "000905.XSHG",
|
"index_symbol": "000905.XSHG",
|
||||||
"qmt_sector_name": "\u4e2d\u8bc1500",
|
"qmt_sector_name": "\u4e2d\u8bc1500",
|
||||||
@@ -63,6 +73,76 @@ CONFIG = {
|
|||||||
}
|
}
|
||||||
# __BASELINE_CONFIG_END__
|
# __BASELINE_CONFIG_END__
|
||||||
|
|
||||||
|
# __TARGET_PACKAGE_TAPE_START__
|
||||||
|
TARGET_PACKAGES = []
|
||||||
|
# __TARGET_PACKAGE_TAPE_END__
|
||||||
|
|
||||||
|
|
||||||
|
MOMENTUM_SMOKE_MODE = "portable_momentum_smoke"
|
||||||
|
TARGET_PACKAGE_MODE = "target_package"
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_target_packages(config):
|
||||||
|
mode = config.get("decision_mode")
|
||||||
|
if mode not in (MOMENTUM_SMOKE_MODE, TARGET_PACKAGE_MODE):
|
||||||
|
raise UnsupportedStrategyPeriod("unsupported decision_mode")
|
||||||
|
if not isinstance(TARGET_PACKAGES, list):
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"TARGET_PACKAGES must be a list"
|
||||||
|
)
|
||||||
|
expected_count = config.get("target_package_count")
|
||||||
|
expected_sha256 = config.get("target_package_tape_sha256")
|
||||||
|
if mode == MOMENTUM_SMOKE_MODE:
|
||||||
|
if (
|
||||||
|
type(expected_count) is not int
|
||||||
|
or expected_count != 0
|
||||||
|
or expected_sha256 is not None
|
||||||
|
or TARGET_PACKAGES
|
||||||
|
):
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"portable_momentum_smoke cannot carry target packages"
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
if type(expected_count) is not int or expected_count <= 0:
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"target_package mode requires a positive target_package_count"
|
||||||
|
)
|
||||||
|
if expected_count != len(TARGET_PACKAGES):
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"target package count mismatch"
|
||||||
|
)
|
||||||
|
actual_sha256 = target_package_tape_sha256(TARGET_PACKAGES)
|
||||||
|
if expected_sha256 != actual_sha256:
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"target package tape sha256 mismatch"
|
||||||
|
)
|
||||||
|
verified = []
|
||||||
|
package_ids = set()
|
||||||
|
decision_ids = set()
|
||||||
|
clocks = set()
|
||||||
|
signal_clocks = set()
|
||||||
|
for raw_package in TARGET_PACKAGES:
|
||||||
|
package = verify_target_package(raw_package)
|
||||||
|
package_id = package["package_id"]
|
||||||
|
decision_id = package["decision_id"]
|
||||||
|
clock = (package["signal_as_of"], package["next_session"])
|
||||||
|
signal_as_of = package["signal_as_of"]
|
||||||
|
if (
|
||||||
|
package_id in package_ids
|
||||||
|
or decision_id in decision_ids
|
||||||
|
or clock in clocks
|
||||||
|
or signal_as_of in signal_clocks
|
||||||
|
):
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"duplicate or ambiguous target package decision identity"
|
||||||
|
)
|
||||||
|
package_ids.add(package_id)
|
||||||
|
decision_ids.add(decision_id)
|
||||||
|
clocks.add(clock)
|
||||||
|
signal_clocks.add(signal_as_of)
|
||||||
|
verified.append(package)
|
||||||
|
return verified
|
||||||
|
|
||||||
|
|
||||||
def _param(ContextInfo, name, default=None):
|
def _param(ContextInfo, name, default=None):
|
||||||
params = getattr(ContextInfo, "_param", {})
|
params = getattr(ContextInfo, "_param", {})
|
||||||
@@ -84,6 +164,9 @@ def init(ContextInfo):
|
|||||||
override = _param(ContextInfo, "q60_" + key, None)
|
override = _param(ContextInfo, "q60_" + key, None)
|
||||||
if override is not None:
|
if override is not None:
|
||||||
g.config[key] = override
|
g.config[key] = override
|
||||||
|
g.target_packages = _validated_target_packages(g.config)
|
||||||
|
g.decision_mode = g.config["decision_mode"]
|
||||||
|
if g.config["decision_mode"] == MOMENTUM_SMOKE_MODE:
|
||||||
if (
|
if (
|
||||||
g.config.get("rebalance_schedule")
|
g.config.get("rebalance_schedule")
|
||||||
!= "weekly_first_close"
|
!= "weekly_first_close"
|
||||||
@@ -129,6 +212,7 @@ def init(ContextInfo):
|
|||||||
g.last_plan = None
|
g.last_plan = None
|
||||||
g.last_universe = None
|
g.last_universe = None
|
||||||
g.last_universe_as_of = None
|
g.last_universe_as_of = None
|
||||||
|
g.last_target_signal_as_of = None
|
||||||
if (
|
if (
|
||||||
g.config.get("universe_mode") == "fixed"
|
g.config.get("universe_mode") == "fixed"
|
||||||
and hasattr(ContextInfo, "set_universe")
|
and hasattr(ContextInfo, "set_universe")
|
||||||
@@ -136,6 +220,16 @@ def init(ContextInfo):
|
|||||||
ContextInfo.set_universe(g.config["universe"])
|
ContextInfo.set_universe(g.config["universe"])
|
||||||
|
|
||||||
|
|
||||||
|
def _decision_mode():
|
||||||
|
configured = g.config.get("decision_mode")
|
||||||
|
frozen = getattr(g, "decision_mode", configured)
|
||||||
|
if configured != frozen:
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"decision_mode cannot change after init"
|
||||||
|
)
|
||||||
|
return frozen
|
||||||
|
|
||||||
|
|
||||||
def _bar_datetime(ContextInfo):
|
def _bar_datetime(ContextInfo):
|
||||||
timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
|
timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
|
||||||
return datetime.datetime.fromtimestamp(float(timetag) / 1000.0)
|
return datetime.datetime.fromtimestamp(float(timetag) / 1000.0)
|
||||||
@@ -312,7 +406,10 @@ def _portfolio(ContextInfo, managed):
|
|||||||
convert_symbol(symbol, "canonical")
|
convert_symbol(symbol, "canonical")
|
||||||
for symbol in managed
|
for symbol in managed
|
||||||
)
|
)
|
||||||
dynamic = g.config.get("universe_mode") == "pit_index"
|
dynamic = (
|
||||||
|
_decision_mode() == MOMENTUM_SMOKE_MODE
|
||||||
|
and g.config.get("universe_mode") == "pit_index"
|
||||||
|
)
|
||||||
for position in _trade_details(ContextInfo, "position") or []:
|
for position in _trade_details(ContextInfo, "position") or []:
|
||||||
code = str(
|
code = str(
|
||||||
_get_attr(position, ["stock_code", "m_strInstrumentID"], "")
|
_get_attr(position, ["stock_code", "m_strInstrumentID"], "")
|
||||||
@@ -358,6 +455,8 @@ def _portfolio(ContextInfo, managed):
|
|||||||
|
|
||||||
|
|
||||||
def compute_plan(ContextInfo, bar_time=None):
|
def compute_plan(ContextInfo, bar_time=None):
|
||||||
|
if _decision_mode() == TARGET_PACKAGE_MODE:
|
||||||
|
return _compute_target_package_plan(ContextInfo, bar_time)
|
||||||
if bar_time is None:
|
if bar_time is None:
|
||||||
bar_time = _bar_datetime(ContextInfo)
|
bar_time = _bar_datetime(ContextInfo)
|
||||||
membership_timetag = int(
|
membership_timetag = int(
|
||||||
@@ -390,6 +489,136 @@ def compute_plan(ContextInfo, bar_time=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _signal_as_of(bar_time):
|
||||||
|
if (
|
||||||
|
int(bar_time.hour) != 15
|
||||||
|
or int(bar_time.minute) != 0
|
||||||
|
or int(bar_time.second) != 0
|
||||||
|
or int(bar_time.microsecond) != 0
|
||||||
|
):
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"target_package mode requires the completed 15:00 daily close"
|
||||||
|
)
|
||||||
|
return bar_time.strftime("%Y-%m-%dT15:00:00+08:00")
|
||||||
|
|
||||||
|
|
||||||
|
def _target_package_for_close(bar_time):
|
||||||
|
signal_as_of = _signal_as_of(bar_time)
|
||||||
|
config = g.config
|
||||||
|
packages = _validated_target_packages(config)
|
||||||
|
matches = [
|
||||||
|
package
|
||||||
|
for package in packages
|
||||||
|
if package["signal_as_of"] == signal_as_of
|
||||||
|
]
|
||||||
|
if not matches:
|
||||||
|
return None
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise UnsupportedStrategyPeriod(
|
||||||
|
"ambiguous target package signal clock"
|
||||||
|
)
|
||||||
|
package = matches[0]
|
||||||
|
return lookup_target_package_exact(
|
||||||
|
packages,
|
||||||
|
signal_as_of,
|
||||||
|
package["next_session"],
|
||||||
|
decision_id=package["decision_id"],
|
||||||
|
expected_tape_sha256=config["target_package_tape_sha256"],
|
||||||
|
expected_count=config["target_package_count"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _target_close_prices(ContextInfo, bar_time, symbols):
|
||||||
|
platform_symbols = [
|
||||||
|
convert_symbol(symbol, "qmt")
|
||||||
|
for symbol in sorted(symbols)
|
||||||
|
]
|
||||||
|
raw = ContextInfo.get_market_data_ex(
|
||||||
|
fields=["close"],
|
||||||
|
stock_code=platform_symbols,
|
||||||
|
period="1d",
|
||||||
|
start_time="",
|
||||||
|
end_time=bar_time.strftime("%Y%m%d"),
|
||||||
|
count=1,
|
||||||
|
dividend_type="front_ratio",
|
||||||
|
fill_data=False,
|
||||||
|
subscribe=False,
|
||||||
|
)
|
||||||
|
if not hasattr(raw, "get"):
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"target close query returned an invalid payload"
|
||||||
|
)
|
||||||
|
prices = {}
|
||||||
|
for symbol in platform_symbols:
|
||||||
|
values = _series_close(raw.get(symbol))
|
||||||
|
if len(values) != 1:
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"%s needs one completed daily close" % symbol
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
price = float(values[0])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"%s contains a missing/non-numeric close" % symbol
|
||||||
|
)
|
||||||
|
if not math.isfinite(price) or price <= 0.0:
|
||||||
|
raise PriceHistoryUnavailable(
|
||||||
|
"%s contains a missing/non-positive close" % symbol
|
||||||
|
)
|
||||||
|
prices[convert_symbol(symbol, "canonical")] = price
|
||||||
|
return prices
|
||||||
|
|
||||||
|
|
||||||
|
def _target_lineage(package):
|
||||||
|
return {
|
||||||
|
"package_id": package["package_id"],
|
||||||
|
"package_sha256": package["package_sha256"],
|
||||||
|
"decision_id": package["decision_id"],
|
||||||
|
"signal_as_of": package["signal_as_of"],
|
||||||
|
"next_session": package["next_session"],
|
||||||
|
"release": package["release"],
|
||||||
|
"model_id": package["model_id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_target_package_plan(ContextInfo, bar_time=None):
|
||||||
|
if bar_time is None:
|
||||||
|
bar_time = _bar_datetime(ContextInfo)
|
||||||
|
package = _target_package_for_close(bar_time)
|
||||||
|
if package is None:
|
||||||
|
return None
|
||||||
|
managed = sorted(package["universe"])
|
||||||
|
current, sellable, equity = _portfolio(ContextInfo, managed)
|
||||||
|
if equity <= 0:
|
||||||
|
raise ValueError("QMT account equity must be positive")
|
||||||
|
plan = build_target_weight_plan(
|
||||||
|
target_weights=package["target_weights"],
|
||||||
|
prices=_target_close_prices(
|
||||||
|
ContextInfo,
|
||||||
|
bar_time,
|
||||||
|
set(package["target_weights"]) | set(current),
|
||||||
|
),
|
||||||
|
current=current,
|
||||||
|
sellable=sellable,
|
||||||
|
equity=equity,
|
||||||
|
cash_buffer=g.config["cash_buffer"],
|
||||||
|
lot_size=g.config["lot_size"],
|
||||||
|
decision_id=package["decision_id"],
|
||||||
|
package_sha256=package["package_sha256"],
|
||||||
|
)
|
||||||
|
plan["decision_mode"] = TARGET_PACKAGE_MODE
|
||||||
|
plan["lineage"] = _target_lineage(package)
|
||||||
|
plan["universe"] = package["universe"]
|
||||||
|
plan["execution_contract"] = {
|
||||||
|
"signal_price_source": "COMPLETED_DAILY_CLOSE",
|
||||||
|
"submit_trigger": "NEXT_BAR_FIRST_TICK",
|
||||||
|
"quick_trade": 0,
|
||||||
|
"declared_next_session": package["next_session"],
|
||||||
|
"real_platform_observed": False,
|
||||||
|
}
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
def _orders_allowed(ContextInfo):
|
def _orders_allowed(ContextInfo):
|
||||||
# This hosted wrapper is deliberately backtest-only. Its cached account
|
# This hosted wrapper is deliberately backtest-only. Its cached account
|
||||||
# query and in-memory weekly marker cannot provide crash-safe live
|
# query and in-memory weekly marker cannot provide crash-safe live
|
||||||
@@ -452,12 +681,76 @@ def _submit_delta(ContextInfo, canonical, delta, week_key):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _submit_target_delta(ContextInfo, canonical, delta, package_sha256):
|
||||||
|
config = g.config
|
||||||
|
order_code = convert_symbol(canonical, "qmt")
|
||||||
|
operation = 23 if delta > 0 else 24
|
||||||
|
volume = abs(int(delta))
|
||||||
|
user_order_id = "q60t-%s-%s" % (
|
||||||
|
str(package_sha256)[:8],
|
||||||
|
order_code.split(".")[0],
|
||||||
|
)
|
||||||
|
function = globals()["passorder"]
|
||||||
|
arg_count = getattr(getattr(function, "__code__", None), "co_argcount", 11)
|
||||||
|
if arg_count <= 8:
|
||||||
|
function(
|
||||||
|
operation,
|
||||||
|
1101,
|
||||||
|
config["account_id"],
|
||||||
|
order_code,
|
||||||
|
5,
|
||||||
|
-1,
|
||||||
|
volume,
|
||||||
|
ContextInfo,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
function(
|
||||||
|
operation,
|
||||||
|
1101,
|
||||||
|
config["account_id"],
|
||||||
|
order_code,
|
||||||
|
5,
|
||||||
|
-1,
|
||||||
|
volume,
|
||||||
|
"quant60",
|
||||||
|
0,
|
||||||
|
user_order_id,
|
||||||
|
ContextInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handlebar(ContextInfo):
|
def handlebar(ContextInfo):
|
||||||
if not _is_backtest(ContextInfo) and hasattr(ContextInfo, "is_last_bar"):
|
if not _is_backtest(ContextInfo) and hasattr(ContextInfo, "is_last_bar"):
|
||||||
if not ContextInfo.is_last_bar():
|
if not ContextInfo.is_last_bar():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
bar_time = _bar_datetime(ContextInfo)
|
bar_time = _bar_datetime(ContextInfo)
|
||||||
|
if _decision_mode() == TARGET_PACKAGE_MODE:
|
||||||
|
signal_as_of = _signal_as_of(bar_time)
|
||||||
|
if g.last_target_signal_as_of == signal_as_of:
|
||||||
|
return None
|
||||||
|
plan = compute_plan(ContextInfo, bar_time)
|
||||||
|
# Consume the clock before crossing passorder so a callback retry
|
||||||
|
# cannot duplicate an already accepted order prefix.
|
||||||
|
g.last_target_signal_as_of = signal_as_of
|
||||||
|
if plan is None:
|
||||||
|
return None
|
||||||
|
g.last_plan = plan
|
||||||
|
if not _orders_allowed(ContextInfo):
|
||||||
|
return plan
|
||||||
|
ordered = sorted(
|
||||||
|
plan["orders"].items(),
|
||||||
|
key=lambda item: item[1],
|
||||||
|
)
|
||||||
|
for canonical, delta in ordered:
|
||||||
|
if int(delta) != 0:
|
||||||
|
_submit_target_delta(
|
||||||
|
ContextInfo,
|
||||||
|
canonical,
|
||||||
|
delta,
|
||||||
|
plan["lineage"]["package_sha256"],
|
||||||
|
)
|
||||||
|
return plan
|
||||||
year, week, unused = bar_time.isocalendar()
|
year, week, unused = bar_time.isocalendar()
|
||||||
del unused
|
del unused
|
||||||
week_key = (year, week)
|
week_key = (year, week)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,190 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://boat.local/quant60/schemas/model_bundle.schema.json",
|
||||||
|
"title": "Quant60 deterministic Ridge model bundle",
|
||||||
|
"description": "Frozen train-only feature transforms, Ridge parameters, training cutoff, label semantics, and lineage hashes for deterministic replay.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"artifact_type",
|
||||||
|
"bundle_sha256",
|
||||||
|
"trained_through",
|
||||||
|
"label",
|
||||||
|
"lineage",
|
||||||
|
"preprocessor",
|
||||||
|
"model"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {
|
||||||
|
"const": "1.0"
|
||||||
|
},
|
||||||
|
"artifact_type": {
|
||||||
|
"const": "quant60-deterministic-ridge-bundle"
|
||||||
|
},
|
||||||
|
"bundle_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"trained_through": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"horizon_trading_days"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"horizon_trading_days": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lineage": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"data_sha256",
|
||||||
|
"source_sha256",
|
||||||
|
"config_sha256"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"data_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"source_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"config_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preprocessor": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"winsor_fraction",
|
||||||
|
"feature_order",
|
||||||
|
"output_feature_order",
|
||||||
|
"transforms"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "train-only-winsor-impute-standardize"
|
||||||
|
},
|
||||||
|
"winsor_fraction": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 0.5
|
||||||
|
},
|
||||||
|
"feature_order": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"output_feature_order": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 2,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"transforms": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/feature_transform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"alpha",
|
||||||
|
"fit_intercept",
|
||||||
|
"coefficients",
|
||||||
|
"intercept"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "deterministic-ridge"
|
||||||
|
},
|
||||||
|
"alpha": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"fit_intercept": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"coefficients": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"intercept": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"sha256": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"feature_transform": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"lower",
|
||||||
|
"upper",
|
||||||
|
"median",
|
||||||
|
"mean",
|
||||||
|
"scale"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"lower": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"upper": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"median": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"mean": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"type": "number",
|
||||||
|
"exclusiveMinimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://boat.local/quant60/schemas/release_reachability.schema.json",
|
||||||
|
"title": "Quant60 Release Reachability",
|
||||||
|
"description": "Per-layer evidence for implementation, tests, entrypoint reachability and real-platform observation. A repository source hash is not runtime evidence.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"artifact_type",
|
||||||
|
"release_id",
|
||||||
|
"generated_at",
|
||||||
|
"layers",
|
||||||
|
"summary",
|
||||||
|
"manifest_sha256"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {
|
||||||
|
"const": "1.0"
|
||||||
|
},
|
||||||
|
"artifact_type": {
|
||||||
|
"const": "quant60-release-reachability"
|
||||||
|
},
|
||||||
|
"release_id": {
|
||||||
|
"$ref": "#/$defs/identifier"
|
||||||
|
},
|
||||||
|
"generated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"
|
||||||
|
},
|
||||||
|
"layers": {
|
||||||
|
"$ref": "#/$defs/layers"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"$ref": "#/$defs/summary"
|
||||||
|
},
|
||||||
|
"manifest_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"identifier": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
|
||||||
|
},
|
||||||
|
"sha256": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"artifact": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"artifact_id",
|
||||||
|
"kind",
|
||||||
|
"path",
|
||||||
|
"sha256",
|
||||||
|
"platform"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"artifact_id": {
|
||||||
|
"$ref": "#/$defs/identifier"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"source",
|
||||||
|
"test",
|
||||||
|
"entrypoint",
|
||||||
|
"platform_run"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"repository",
|
||||||
|
"local",
|
||||||
|
"joinquant",
|
||||||
|
"qmt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observed_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"
|
||||||
|
},
|
||||||
|
"run_id": {
|
||||||
|
"$ref": "#/$defs/identifier"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependentRequired": {
|
||||||
|
"observed_at": [
|
||||||
|
"run_id"
|
||||||
|
],
|
||||||
|
"run_id": [
|
||||||
|
"observed_at"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"claim": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"artifacts"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"artifacts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/artifact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"layer": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"implemented",
|
||||||
|
"unit_tested",
|
||||||
|
"local_entrypoint_reachable",
|
||||||
|
"jq_entrypoint_reachable",
|
||||||
|
"qmt_entrypoint_reachable",
|
||||||
|
"real_platform_observed"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"implemented": {
|
||||||
|
"$ref": "#/$defs/claim"
|
||||||
|
},
|
||||||
|
"unit_tested": {
|
||||||
|
"$ref": "#/$defs/claim"
|
||||||
|
},
|
||||||
|
"local_entrypoint_reachable": {
|
||||||
|
"$ref": "#/$defs/claim"
|
||||||
|
},
|
||||||
|
"jq_entrypoint_reachable": {
|
||||||
|
"$ref": "#/$defs/claim"
|
||||||
|
},
|
||||||
|
"qmt_entrypoint_reachable": {
|
||||||
|
"$ref": "#/$defs/claim"
|
||||||
|
},
|
||||||
|
"real_platform_observed": {
|
||||||
|
"$ref": "#/$defs/claim"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"layers": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"universe",
|
||||||
|
"alpha",
|
||||||
|
"portfolio",
|
||||||
|
"risk",
|
||||||
|
"execution"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"universe": {
|
||||||
|
"$ref": "#/$defs/layer"
|
||||||
|
},
|
||||||
|
"alpha": {
|
||||||
|
"$ref": "#/$defs/layer"
|
||||||
|
},
|
||||||
|
"portfolio": {
|
||||||
|
"$ref": "#/$defs/layer"
|
||||||
|
},
|
||||||
|
"risk": {
|
||||||
|
"$ref": "#/$defs/layer"
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"$ref": "#/$defs/layer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"all_layers_implemented",
|
||||||
|
"all_layers_unit_tested",
|
||||||
|
"all_layers_local_entrypoint_reachable",
|
||||||
|
"all_layers_jq_entrypoint_reachable",
|
||||||
|
"all_layers_qmt_entrypoint_reachable",
|
||||||
|
"all_layers_real_platform_observed"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"all_layers_implemented": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"all_layers_unit_tested": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"all_layers_local_entrypoint_reachable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"all_layers_jq_entrypoint_reachable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"all_layers_qmt_entrypoint_reachable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"all_layers_real_platform_observed": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://boat.local/quant60/schemas/target_package.schema.json",
|
||||||
|
"title": "Quant60 TargetPackageV1",
|
||||||
|
"description": "Canonical post-risk portfolio weights. Share quantities, orders, fills and account facts are forbidden and belong to the downstream execution plan.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"package_type",
|
||||||
|
"package_id",
|
||||||
|
"decision_id",
|
||||||
|
"release",
|
||||||
|
"evidence_class",
|
||||||
|
"model_id",
|
||||||
|
"signal_as_of",
|
||||||
|
"next_session",
|
||||||
|
"model_sha256",
|
||||||
|
"data_sha256",
|
||||||
|
"feature_sha256",
|
||||||
|
"risk_sha256",
|
||||||
|
"cost_sha256",
|
||||||
|
"optimizer_sha256",
|
||||||
|
"config_sha256",
|
||||||
|
"source_sha256",
|
||||||
|
"target_weights",
|
||||||
|
"universe",
|
||||||
|
"constraints",
|
||||||
|
"package_sha256"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {
|
||||||
|
"const": "1.0"
|
||||||
|
},
|
||||||
|
"package_type": {
|
||||||
|
"const": "TargetPackageV1"
|
||||||
|
},
|
||||||
|
"package_id": {
|
||||||
|
"$ref": "#/$defs/identifier"
|
||||||
|
},
|
||||||
|
"decision_id": {
|
||||||
|
"$ref": "#/$defs/identifier"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"$ref": "#/$defs/identifier"
|
||||||
|
},
|
||||||
|
"evidence_class": {
|
||||||
|
"$ref": "#/$defs/identifier"
|
||||||
|
},
|
||||||
|
"model_id": {
|
||||||
|
"$ref": "#/$defs/identifier"
|
||||||
|
},
|
||||||
|
"signal_as_of": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T15:00:00\\+08:00$"
|
||||||
|
},
|
||||||
|
"next_session": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date"
|
||||||
|
},
|
||||||
|
"model_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"data_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"feature_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"risk_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"cost_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"optimizer_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"config_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"source_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
},
|
||||||
|
"target_weights": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": 1,
|
||||||
|
"propertyNames": {
|
||||||
|
"$ref": "#/$defs/canonical_symbol"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"universe": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": 1,
|
||||||
|
"propertyNames": {
|
||||||
|
"$ref": "#/$defs/canonical_symbol"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/universe_member"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"$ref": "#/$defs/constraints"
|
||||||
|
},
|
||||||
|
"package_sha256": {
|
||||||
|
"$ref": "#/$defs/sha256"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"identifier": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
|
||||||
|
},
|
||||||
|
"sha256": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"canonical_symbol": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]{6}\\.(XSHG|XSHE|XBSE)$"
|
||||||
|
},
|
||||||
|
"universe_member": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"state",
|
||||||
|
"reasons"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"openable",
|
||||||
|
"hold_only",
|
||||||
|
"sell_only",
|
||||||
|
"frozen",
|
||||||
|
"excluded"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reasons": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[A-Z][A-Z0-9_]*$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"constraint_state",
|
||||||
|
"long_only",
|
||||||
|
"max_single_weight",
|
||||||
|
"max_gross_weight",
|
||||||
|
"max_one_way_turnover",
|
||||||
|
"actual_gross_weight"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"constraint_state": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"FEASIBLE",
|
||||||
|
"REPAIRED",
|
||||||
|
"RISK_REDUCTION_ONLY",
|
||||||
|
"NO_TRADE"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"long_only": {
|
||||||
|
"const": true
|
||||||
|
},
|
||||||
|
"max_single_weight": {
|
||||||
|
"type": "number",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 1
|
||||||
|
},
|
||||||
|
"max_gross_weight": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1
|
||||||
|
},
|
||||||
|
"max_one_way_turnover": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1
|
||||||
|
},
|
||||||
|
"actual_gross_weight": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
"""Canonical five-layer decision pipeline for a Baseline candidate.
|
||||||
|
|
||||||
|
This module is deliberately platform-neutral. Hosted adapters may consume its
|
||||||
|
post-risk target weights, but they must not silently replace any layer with the
|
||||||
|
portable momentum smoke strategy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Iterable, Mapping
|
||||||
|
|
||||||
|
from .ledger import canonical_json
|
||||||
|
from .optimizer import (
|
||||||
|
PortfolioCandidate,
|
||||||
|
PortfolioConstraints,
|
||||||
|
PortfolioSolution,
|
||||||
|
optimize_cvxpy,
|
||||||
|
optimize_deterministic,
|
||||||
|
)
|
||||||
|
from .portable_core import normalize_symbol, order_deltas, target_quantities
|
||||||
|
from .risk import RiskDecision, check_target_weights
|
||||||
|
from .universe import UniverseState
|
||||||
|
|
||||||
|
|
||||||
|
FIVE_LAYER_ORDER = (
|
||||||
|
"universe",
|
||||||
|
"alpha",
|
||||||
|
"portfolio",
|
||||||
|
"risk",
|
||||||
|
"execution",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaselinePipelineError(ValueError):
|
||||||
|
"""The canonical Baseline decision cannot be built safely."""
|
||||||
|
|
||||||
|
|
||||||
|
def _finite(value: float, name: str) -> float:
|
||||||
|
numeric = float(value)
|
||||||
|
if not math.isfinite(numeric):
|
||||||
|
raise BaselinePipelineError(f"{name} must be finite")
|
||||||
|
return numeric
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(payload: Any) -> str:
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
return hashlib.sha256(canonical_json(payload).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AlphaForecastInput:
|
||||||
|
symbol: str
|
||||||
|
expected_excess_return: float
|
||||||
|
model_version: str
|
||||||
|
feature_version: str
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "symbol", normalize_symbol(self.symbol))
|
||||||
|
_finite(self.expected_excess_return, "expected_excess_return")
|
||||||
|
if not str(self.model_version).strip():
|
||||||
|
raise BaselinePipelineError("model_version is required")
|
||||||
|
if not str(self.feature_version).strip():
|
||||||
|
raise BaselinePipelineError("feature_version is required")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RiskForecastInput:
|
||||||
|
symbol: str
|
||||||
|
predicted_variance: float
|
||||||
|
risk_model_version: str
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "symbol", normalize_symbol(self.symbol))
|
||||||
|
variance = _finite(self.predicted_variance, "predicted_variance")
|
||||||
|
if variance <= 0:
|
||||||
|
raise BaselinePipelineError("predicted_variance must be positive")
|
||||||
|
if not str(self.risk_model_version).strip():
|
||||||
|
raise BaselinePipelineError("risk_model_version is required")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CostForecastInput:
|
||||||
|
symbol: str
|
||||||
|
base_bps: float
|
||||||
|
max_adv_weight: float
|
||||||
|
cost_model_version: str
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "symbol", normalize_symbol(self.symbol))
|
||||||
|
base_bps = _finite(self.base_bps, "base_bps")
|
||||||
|
max_adv_weight = _finite(self.max_adv_weight, "max_adv_weight")
|
||||||
|
if base_bps < 0:
|
||||||
|
raise BaselinePipelineError("base_bps must be non-negative")
|
||||||
|
if not 0 <= max_adv_weight <= 1:
|
||||||
|
raise BaselinePipelineError("max_adv_weight must lie in [0, 1]")
|
||||||
|
if not str(self.cost_model_version).strip():
|
||||||
|
raise BaselinePipelineError("cost_model_version is required")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SecurityStateInput:
|
||||||
|
symbol: str
|
||||||
|
state: UniverseState
|
||||||
|
industry: str
|
||||||
|
price: float
|
||||||
|
current_quantity: int = 0
|
||||||
|
sellable_quantity: int = 0
|
||||||
|
reasons: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
object.__setattr__(self, "symbol", normalize_symbol(self.symbol))
|
||||||
|
if not isinstance(self.state, UniverseState):
|
||||||
|
raise BaselinePipelineError("state must be a UniverseState")
|
||||||
|
if not str(self.industry).strip():
|
||||||
|
raise BaselinePipelineError("industry is required")
|
||||||
|
price = _finite(self.price, "price")
|
||||||
|
if price <= 0:
|
||||||
|
raise BaselinePipelineError("price must be positive")
|
||||||
|
if type(self.current_quantity) is not int or self.current_quantity < 0:
|
||||||
|
raise BaselinePipelineError(
|
||||||
|
"current_quantity must be a non-negative integer"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
type(self.sellable_quantity) is not int
|
||||||
|
or self.sellable_quantity < 0
|
||||||
|
or self.sellable_quantity > self.current_quantity
|
||||||
|
):
|
||||||
|
raise BaselinePipelineError(
|
||||||
|
"sellable_quantity must lie within current_quantity"
|
||||||
|
)
|
||||||
|
if any(not str(reason).strip() for reason in self.reasons):
|
||||||
|
raise BaselinePipelineError("universe reasons must be non-empty strings")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class BaselinePipelinePolicy:
|
||||||
|
constraints: PortfolioConstraints = PortfolioConstraints()
|
||||||
|
solver: str = "deterministic"
|
||||||
|
lot_size: int = 100
|
||||||
|
cash_buffer: float = 0.05
|
||||||
|
release_role: str = "baseline_candidate"
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
self.constraints.validate()
|
||||||
|
if self.solver not in {"deterministic", "cvxpy"}:
|
||||||
|
raise BaselinePipelineError("solver must be deterministic or cvxpy")
|
||||||
|
if type(self.lot_size) is not int or self.lot_size <= 0:
|
||||||
|
raise BaselinePipelineError("lot_size must be a positive integer")
|
||||||
|
cash_buffer = _finite(self.cash_buffer, "cash_buffer")
|
||||||
|
if not 0 <= cash_buffer < 1:
|
||||||
|
raise BaselinePipelineError("cash_buffer must lie in [0, 1)")
|
||||||
|
if self.release_role not in {"baseline_candidate", "research_candidate"}:
|
||||||
|
raise BaselinePipelineError("unsupported release_role")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class BaselinePipelineResult:
|
||||||
|
pipeline_id: str
|
||||||
|
release_role: str
|
||||||
|
solution: PortfolioSolution
|
||||||
|
risk_decision: RiskDecision
|
||||||
|
target_quantities: dict[str, int]
|
||||||
|
order_deltas: dict[str, int]
|
||||||
|
layer_payloads: dict[str, dict[str, Any]]
|
||||||
|
layer_trace: tuple[dict[str, Any], ...]
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"pipeline_id": self.pipeline_id,
|
||||||
|
"release_role": self.release_role,
|
||||||
|
"weights": self.solution.weights,
|
||||||
|
"target_quantities": self.target_quantities,
|
||||||
|
"order_deltas": self.order_deltas,
|
||||||
|
"portfolio": {
|
||||||
|
"solver": self.solution.solver,
|
||||||
|
"status": self.solution.status,
|
||||||
|
"gross_weight": self.solution.gross_weight,
|
||||||
|
"cash_weight": self.solution.cash_weight,
|
||||||
|
"one_way_turnover": self.solution.one_way_turnover,
|
||||||
|
"predicted_variance": self.solution.predicted_variance,
|
||||||
|
"expected_score": self.solution.expected_score,
|
||||||
|
"estimated_cost_bps_weighted": (
|
||||||
|
self.solution.estimated_cost_bps_weighted
|
||||||
|
),
|
||||||
|
"diagnostics": self.solution.diagnostics,
|
||||||
|
},
|
||||||
|
"risk": {
|
||||||
|
"approved": self.risk_decision.approved,
|
||||||
|
"gross_weight": self.risk_decision.gross_weight,
|
||||||
|
"one_way_turnover": self.risk_decision.one_way_turnover,
|
||||||
|
"violations": [
|
||||||
|
{
|
||||||
|
"code": item.code,
|
||||||
|
"message": item.message,
|
||||||
|
"symbol": item.symbol,
|
||||||
|
}
|
||||||
|
for item in self.risk_decision.violations
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"layer_payloads": self.layer_payloads,
|
||||||
|
"layer_trace": list(self.layer_trace),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _unique_by_symbol(
|
||||||
|
values: Iterable[Any],
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
result: dict[str, Any] = {}
|
||||||
|
for item in values:
|
||||||
|
symbol = normalize_symbol(item.symbol)
|
||||||
|
if symbol in result:
|
||||||
|
raise BaselinePipelineError(f"duplicate {name} for {symbol}")
|
||||||
|
result[symbol] = item
|
||||||
|
if not result:
|
||||||
|
raise BaselinePipelineError(f"{name} inputs are empty")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _trace_event(
|
||||||
|
*,
|
||||||
|
layer: str,
|
||||||
|
implementation: str,
|
||||||
|
input_sha256: str,
|
||||||
|
output_payload: Mapping[str, Any],
|
||||||
|
previous_event_sha256: str | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
output_sha256 = _sha256(output_payload)
|
||||||
|
body = {
|
||||||
|
"sequence": FIVE_LAYER_ORDER.index(layer) + 1,
|
||||||
|
"layer": layer,
|
||||||
|
"implementation": implementation,
|
||||||
|
"input_sha256": input_sha256,
|
||||||
|
"output_sha256": output_sha256,
|
||||||
|
"previous_event_sha256": previous_event_sha256,
|
||||||
|
}
|
||||||
|
body["event_sha256"] = _sha256(body)
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def verify_layer_trace(
|
||||||
|
trace: Iterable[Mapping[str, Any]],
|
||||||
|
layer_payloads: Mapping[str, Mapping[str, Any]],
|
||||||
|
) -> None:
|
||||||
|
events = tuple(dict(item) for item in trace)
|
||||||
|
if tuple(item.get("layer") for item in events) != FIVE_LAYER_ORDER:
|
||||||
|
raise BaselinePipelineError("layer trace is not the canonical five-layer path")
|
||||||
|
previous = None
|
||||||
|
for sequence, event in enumerate(events, 1):
|
||||||
|
if event.get("sequence") != sequence:
|
||||||
|
raise BaselinePipelineError("layer trace sequence is invalid")
|
||||||
|
if event.get("previous_event_sha256") != previous:
|
||||||
|
raise BaselinePipelineError("layer trace hash chain is broken")
|
||||||
|
layer = str(event["layer"])
|
||||||
|
if layer not in layer_payloads:
|
||||||
|
raise BaselinePipelineError(f"missing layer payload for {layer}")
|
||||||
|
if event.get("output_sha256") != _sha256(layer_payloads[layer]):
|
||||||
|
raise BaselinePipelineError(f"layer output hash mismatch for {layer}")
|
||||||
|
body = dict(event)
|
||||||
|
declared = body.pop("event_sha256", None)
|
||||||
|
if declared != _sha256(body):
|
||||||
|
raise BaselinePipelineError(f"layer event hash mismatch for {layer}")
|
||||||
|
previous = str(declared)
|
||||||
|
|
||||||
|
|
||||||
|
def build_baseline_pipeline(
|
||||||
|
*,
|
||||||
|
alpha: Iterable[AlphaForecastInput],
|
||||||
|
risk: Iterable[RiskForecastInput],
|
||||||
|
cost: Iterable[CostForecastInput],
|
||||||
|
securities: Iterable[SecurityStateInput],
|
||||||
|
equity: float,
|
||||||
|
policy: BaselinePipelinePolicy = BaselinePipelinePolicy(),
|
||||||
|
) -> BaselinePipelineResult:
|
||||||
|
"""Compose all five layers and return their causally linked decision trace."""
|
||||||
|
|
||||||
|
policy.validate()
|
||||||
|
account_equity = _finite(equity, "equity")
|
||||||
|
if account_equity <= 0:
|
||||||
|
raise BaselinePipelineError("equity must be positive")
|
||||||
|
alpha_by_symbol = _unique_by_symbol(alpha, name="alpha")
|
||||||
|
risk_by_symbol = _unique_by_symbol(risk, name="risk")
|
||||||
|
cost_by_symbol = _unique_by_symbol(cost, name="cost")
|
||||||
|
state_by_symbol = _unique_by_symbol(securities, name="universe")
|
||||||
|
symbol_sets = {
|
||||||
|
"alpha": set(alpha_by_symbol),
|
||||||
|
"risk": set(risk_by_symbol),
|
||||||
|
"cost": set(cost_by_symbol),
|
||||||
|
"universe": set(state_by_symbol),
|
||||||
|
}
|
||||||
|
if len({frozenset(values) for values in symbol_sets.values()}) != 1:
|
||||||
|
raise BaselinePipelineError(
|
||||||
|
"alpha/risk/cost/universe symbol sets must match exactly: "
|
||||||
|
+ ", ".join(
|
||||||
|
f"{name}={sorted(values)}"
|
||||||
|
for name, values in sorted(symbol_sets.items())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
symbols = sorted(state_by_symbol)
|
||||||
|
model_versions = {alpha_by_symbol[symbol].model_version for symbol in symbols}
|
||||||
|
feature_versions = {
|
||||||
|
alpha_by_symbol[symbol].feature_version for symbol in symbols
|
||||||
|
}
|
||||||
|
risk_versions = {
|
||||||
|
risk_by_symbol[symbol].risk_model_version for symbol in symbols
|
||||||
|
}
|
||||||
|
cost_versions = {
|
||||||
|
cost_by_symbol[symbol].cost_model_version for symbol in symbols
|
||||||
|
}
|
||||||
|
for name, versions in (
|
||||||
|
("model", model_versions),
|
||||||
|
("feature", feature_versions),
|
||||||
|
("risk model", risk_versions),
|
||||||
|
("cost model", cost_versions),
|
||||||
|
):
|
||||||
|
if len(versions) != 1:
|
||||||
|
raise BaselinePipelineError(f"{name} versions are inconsistent")
|
||||||
|
model_version = next(iter(model_versions))
|
||||||
|
if (
|
||||||
|
policy.release_role == "baseline_candidate"
|
||||||
|
and model_version == "portable-momentum-v1"
|
||||||
|
):
|
||||||
|
raise BaselinePipelineError(
|
||||||
|
"portable-momentum-v1 is a platform smoke strategy, not a "
|
||||||
|
"Baseline candidate model"
|
||||||
|
)
|
||||||
|
|
||||||
|
current_quantities = {
|
||||||
|
symbol: state_by_symbol[symbol].current_quantity for symbol in symbols
|
||||||
|
}
|
||||||
|
sellable = {
|
||||||
|
symbol: state_by_symbol[symbol].sellable_quantity for symbol in symbols
|
||||||
|
}
|
||||||
|
prices = {symbol: state_by_symbol[symbol].price for symbol in symbols}
|
||||||
|
current_weights = {
|
||||||
|
symbol: (
|
||||||
|
current_quantities[symbol] * prices[symbol] / account_equity
|
||||||
|
)
|
||||||
|
for symbol in symbols
|
||||||
|
}
|
||||||
|
candidates = [
|
||||||
|
PortfolioCandidate(
|
||||||
|
symbol=symbol,
|
||||||
|
score=alpha_by_symbol[symbol].expected_excess_return,
|
||||||
|
variance=risk_by_symbol[symbol].predicted_variance,
|
||||||
|
current_weight=current_weights[symbol],
|
||||||
|
cost_bps=cost_by_symbol[symbol].base_bps,
|
||||||
|
max_adv_weight=cost_by_symbol[symbol].max_adv_weight,
|
||||||
|
industry=state_by_symbol[symbol].industry,
|
||||||
|
state=state_by_symbol[symbol].state,
|
||||||
|
)
|
||||||
|
for symbol in symbols
|
||||||
|
]
|
||||||
|
|
||||||
|
universe_payload = {
|
||||||
|
"symbols": [
|
||||||
|
{
|
||||||
|
"symbol": symbol,
|
||||||
|
"state": state_by_symbol[symbol].state.value,
|
||||||
|
"industry": state_by_symbol[symbol].industry,
|
||||||
|
"reasons": list(state_by_symbol[symbol].reasons),
|
||||||
|
"current_quantity": current_quantities[symbol],
|
||||||
|
"sellable_quantity": sellable[symbol],
|
||||||
|
}
|
||||||
|
for symbol in symbols
|
||||||
|
]
|
||||||
|
}
|
||||||
|
alpha_payload = {
|
||||||
|
"model_version": model_version,
|
||||||
|
"feature_version": next(iter(feature_versions)),
|
||||||
|
"forecasts": {
|
||||||
|
symbol: alpha_by_symbol[symbol].expected_excess_return
|
||||||
|
for symbol in symbols
|
||||||
|
},
|
||||||
|
}
|
||||||
|
portfolio_input = {
|
||||||
|
"alpha_sha256": _sha256(alpha_payload),
|
||||||
|
"risk_model_version": next(iter(risk_versions)),
|
||||||
|
"risk": {
|
||||||
|
symbol: risk_by_symbol[symbol].predicted_variance
|
||||||
|
for symbol in symbols
|
||||||
|
},
|
||||||
|
"cost_model_version": next(iter(cost_versions)),
|
||||||
|
"cost_bps": {
|
||||||
|
symbol: cost_by_symbol[symbol].base_bps for symbol in symbols
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
symbol: cost_by_symbol[symbol].max_adv_weight for symbol in symbols
|
||||||
|
},
|
||||||
|
"current_weights": current_weights,
|
||||||
|
}
|
||||||
|
solution = (
|
||||||
|
optimize_cvxpy(candidates, policy.constraints)
|
||||||
|
if policy.solver == "cvxpy"
|
||||||
|
else optimize_deterministic(candidates, policy.constraints)
|
||||||
|
)
|
||||||
|
portfolio_payload = {
|
||||||
|
"solver": solution.solver,
|
||||||
|
"status": solution.status,
|
||||||
|
"weights": solution.weights,
|
||||||
|
"gross_weight": solution.gross_weight,
|
||||||
|
"cash_weight": solution.cash_weight,
|
||||||
|
"one_way_turnover": solution.one_way_turnover,
|
||||||
|
"predicted_variance": solution.predicted_variance,
|
||||||
|
"estimated_cost_bps_weighted": solution.estimated_cost_bps_weighted,
|
||||||
|
"input_sha256": _sha256(portfolio_input),
|
||||||
|
}
|
||||||
|
risk_decision = check_target_weights(
|
||||||
|
solution.weights,
|
||||||
|
current_weights,
|
||||||
|
max_single_weight=policy.constraints.single_name_cap,
|
||||||
|
max_gross_weight=policy.constraints.gross_target,
|
||||||
|
max_one_way_turnover=policy.constraints.one_way_turnover_cap,
|
||||||
|
)
|
||||||
|
risk_decision.require_approved()
|
||||||
|
risk_payload = {
|
||||||
|
"approved": risk_decision.approved,
|
||||||
|
"gross_weight": risk_decision.gross_weight,
|
||||||
|
"one_way_turnover": risk_decision.one_way_turnover,
|
||||||
|
"violations": [],
|
||||||
|
"post_risk_weights": solution.weights,
|
||||||
|
}
|
||||||
|
quantities = target_quantities(
|
||||||
|
solution.weights,
|
||||||
|
prices,
|
||||||
|
account_equity,
|
||||||
|
lot_size=policy.lot_size,
|
||||||
|
cash_buffer=policy.cash_buffer,
|
||||||
|
)
|
||||||
|
deltas = order_deltas(
|
||||||
|
quantities,
|
||||||
|
current_quantities,
|
||||||
|
sellable,
|
||||||
|
lot_size=policy.lot_size,
|
||||||
|
)
|
||||||
|
execution_payload = {
|
||||||
|
"equity": account_equity,
|
||||||
|
"prices": prices,
|
||||||
|
"lot_size": policy.lot_size,
|
||||||
|
"cash_buffer": policy.cash_buffer,
|
||||||
|
"target_quantities": quantities,
|
||||||
|
"order_deltas": deltas,
|
||||||
|
}
|
||||||
|
layer_payloads = {
|
||||||
|
"universe": universe_payload,
|
||||||
|
"alpha": alpha_payload,
|
||||||
|
"portfolio": portfolio_payload,
|
||||||
|
"risk": risk_payload,
|
||||||
|
"execution": execution_payload,
|
||||||
|
}
|
||||||
|
implementation = {
|
||||||
|
"universe": "quant60.baseline_pipeline:SecurityStateInput",
|
||||||
|
"alpha": "quant60.baseline_pipeline:AlphaForecastInput",
|
||||||
|
"portfolio": (
|
||||||
|
"quant60.optimizer:optimize_cvxpy"
|
||||||
|
if policy.solver == "cvxpy"
|
||||||
|
else "quant60.optimizer:optimize_deterministic"
|
||||||
|
),
|
||||||
|
"risk": "quant60.risk:check_target_weights",
|
||||||
|
"execution": "quant60.portable_core:target_quantities+order_deltas",
|
||||||
|
}
|
||||||
|
trace: list[dict[str, Any]] = []
|
||||||
|
previous_event_sha256 = None
|
||||||
|
previous_output_sha256 = _sha256(
|
||||||
|
{
|
||||||
|
"equity": account_equity,
|
||||||
|
"release_role": policy.release_role,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for layer in FIVE_LAYER_ORDER:
|
||||||
|
event = _trace_event(
|
||||||
|
layer=layer,
|
||||||
|
implementation=implementation[layer],
|
||||||
|
input_sha256=previous_output_sha256,
|
||||||
|
output_payload=layer_payloads[layer],
|
||||||
|
previous_event_sha256=previous_event_sha256,
|
||||||
|
)
|
||||||
|
trace.append(event)
|
||||||
|
previous_event_sha256 = event["event_sha256"]
|
||||||
|
previous_output_sha256 = event["output_sha256"]
|
||||||
|
verify_layer_trace(trace, layer_payloads)
|
||||||
|
pipeline_id = "q60-pipeline-" + _sha256(
|
||||||
|
{
|
||||||
|
"release_role": policy.release_role,
|
||||||
|
"model_version": model_version,
|
||||||
|
"layers": [event["event_sha256"] for event in trace],
|
||||||
|
}
|
||||||
|
)[:16]
|
||||||
|
return BaselinePipelineResult(
|
||||||
|
pipeline_id=pipeline_id,
|
||||||
|
release_role=policy.release_role,
|
||||||
|
solution=solution,
|
||||||
|
risk_decision=risk_decision,
|
||||||
|
target_quantities=quantities,
|
||||||
|
order_deltas=deltas,
|
||||||
|
layer_payloads=layer_payloads,
|
||||||
|
layer_trace=tuple(trace),
|
||||||
|
)
|
||||||
+381
-89
@@ -13,20 +13,33 @@ from datetime import date
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from .baseline_pipeline import (
|
||||||
|
AlphaForecastInput,
|
||||||
|
BaselinePipelinePolicy,
|
||||||
|
CostForecastInput,
|
||||||
|
RiskForecastInput,
|
||||||
|
SecurityStateInput,
|
||||||
|
build_baseline_pipeline,
|
||||||
|
verify_layer_trace,
|
||||||
|
)
|
||||||
|
from .config import FeeConfig
|
||||||
from .contracts import validate_records
|
from .contracts import validate_records
|
||||||
from .domain import Bar
|
from .costs import (
|
||||||
|
CostModelParameters,
|
||||||
|
DatedFeeRule,
|
||||||
|
forecast_trade_cost,
|
||||||
|
)
|
||||||
|
from .domain import Bar, Side
|
||||||
from .features import (
|
from .features import (
|
||||||
TrainOnlyFeaturePreprocessor,
|
TrainOnlyFeaturePreprocessor,
|
||||||
build_executable_label,
|
build_executable_label,
|
||||||
compute_price_features,
|
compute_price_features,
|
||||||
)
|
)
|
||||||
from .ledger import canonical_json, jsonable
|
from .ledger import canonical_json, jsonable
|
||||||
|
from .model_bundle import create_model_bundle, verify_model_bundle
|
||||||
from .optimizer import (
|
from .optimizer import (
|
||||||
PortfolioCandidate,
|
|
||||||
PortfolioConstraints,
|
PortfolioConstraints,
|
||||||
optimize_deterministic,
|
|
||||||
)
|
)
|
||||||
from .portable_core import target_quantities
|
|
||||||
from .research import (
|
from .research import (
|
||||||
DeterministicRidge,
|
DeterministicRidge,
|
||||||
pearson_correlation,
|
pearson_correlation,
|
||||||
@@ -34,6 +47,7 @@ from .research import (
|
|||||||
spearman_correlation,
|
spearman_correlation,
|
||||||
)
|
)
|
||||||
from .synthetic import generate_synthetic_bars
|
from .synthetic import generate_synthetic_bars
|
||||||
|
from .target_package import seal_target_package, validate_target_package
|
||||||
from .universe import SecurityEligibility, classify_security
|
from .universe import SecurityEligibility, classify_security
|
||||||
|
|
||||||
|
|
||||||
@@ -119,6 +133,32 @@ def _bar_data_version(bars: list[Bar]) -> str:
|
|||||||
return _sha256(payload)
|
return _sha256(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _training_rows_version(rows: list[dict[str, Any]]) -> str:
|
||||||
|
return _sha256(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"sample_id": row["sample_id"],
|
||||||
|
"decision_date": row["decision_date"],
|
||||||
|
"entry_date": row["entry_date"],
|
||||||
|
"exit_date": row["exit_date"],
|
||||||
|
"symbol": row["symbol"],
|
||||||
|
"features": row["features"],
|
||||||
|
"label": row["label"],
|
||||||
|
}
|
||||||
|
for row in sorted(
|
||||||
|
rows,
|
||||||
|
key=lambda item: (item["decision_date"], item["symbol"]),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _label_is_available(row: dict[str, Any], target_date: date) -> bool:
|
||||||
|
"""Return whether a completed label is known strictly before a decision."""
|
||||||
|
|
||||||
|
return date.fromisoformat(row["exit_date"]) < target_date
|
||||||
|
|
||||||
|
|
||||||
def _first_sessions_by_week(dates: list[date]) -> list[date]:
|
def _first_sessions_by_week(dates: list[date]) -> list[date]:
|
||||||
result: list[date] = []
|
result: list[date] = []
|
||||||
prior_week = None
|
prior_week = None
|
||||||
@@ -320,94 +360,25 @@ def run_research_smoke(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Train strictly before the final decision and emit a point-in-time target.
|
# Train only on labels whose exit observation was known before the final
|
||||||
|
# decision. Filtering merely on decision_date would leak the final
|
||||||
|
# horizon's future opens into the fitted model.
|
||||||
target_date = decision_dates[-1]
|
target_date = decision_dates[-1]
|
||||||
final_train = [
|
final_train = [
|
||||||
row
|
row
|
||||||
for row in rows
|
for row in rows
|
||||||
if date.fromisoformat(row["decision_date"]) < target_date
|
if _label_is_available(row, target_date)
|
||||||
]
|
]
|
||||||
final_rows = rows_by_date[target_date]
|
final_rows = rows_by_date[target_date]
|
||||||
final_processor = TrainOnlyFeaturePreprocessor().fit(
|
if not final_train:
|
||||||
[row["features"] for row in final_train]
|
raise ValueError("no labels are available before the final decision")
|
||||||
|
sessions = sorted({bar.trading_date for bar in bars})
|
||||||
|
next_session = next(session for session in sessions if session > target_date)
|
||||||
|
full_data_version = _bar_data_version(bars)
|
||||||
|
decision_data_version = _bar_data_version(
|
||||||
|
[bar for bar in bars if bar.trading_date <= target_date]
|
||||||
)
|
)
|
||||||
final_model = DeterministicRidge(alpha=5.0).fit(
|
training_data_version = _training_rows_version(final_train)
|
||||||
_matrix(final_train, final_processor),
|
|
||||||
[row["label"] for row in final_train],
|
|
||||||
)
|
|
||||||
final_predictions = final_model.predict(_matrix(final_rows, final_processor))
|
|
||||||
centered = statistics.fmean(final_predictions)
|
|
||||||
scores = {
|
|
||||||
row["symbol"]: prediction - centered
|
|
||||||
for row, prediction in zip(final_rows, final_predictions)
|
|
||||||
}
|
|
||||||
candidates = []
|
|
||||||
current_weights = {symbol: 0.0 for symbol in RESEARCH_SYMBOLS}
|
|
||||||
latest_prices = {}
|
|
||||||
for symbol in RESEARCH_SYMBOLS:
|
|
||||||
completed = [
|
|
||||||
bar for bar in by_symbol[symbol] if bar.trading_date <= target_date
|
|
||||||
]
|
|
||||||
latest = completed[-1]
|
|
||||||
latest_prices[symbol] = float(latest.close)
|
|
||||||
returns = [
|
|
||||||
float(completed[index].close)
|
|
||||||
/ float(completed[index - 1].close)
|
|
||||||
- 1.0
|
|
||||||
for index in range(max(1, len(completed) - 60), len(completed))
|
|
||||||
]
|
|
||||||
variance = (
|
|
||||||
statistics.variance(returns) if len(returns) > 1 else 1e-4
|
|
||||||
)
|
|
||||||
feature_row = next(
|
|
||||||
row for row in final_rows if row["symbol"] == symbol
|
|
||||||
)
|
|
||||||
turnover = feature_row["features"]["median_turnover_20"] or 0.0
|
|
||||||
eligibility = SecurityEligibility(
|
|
||||||
symbol=symbol,
|
|
||||||
index_member=True,
|
|
||||||
listing_sessions=500,
|
|
||||||
median_turnover=turnover,
|
|
||||||
minimum_turnover=0.0,
|
|
||||||
held_quantity=0,
|
|
||||||
sellable_quantity=0,
|
|
||||||
)
|
|
||||||
state = classify_security(eligibility).state
|
|
||||||
candidates.append(
|
|
||||||
PortfolioCandidate(
|
|
||||||
symbol=symbol,
|
|
||||||
score=scores[symbol],
|
|
||||||
variance=max(1e-6, variance),
|
|
||||||
current_weight=current_weights[symbol],
|
|
||||||
cost_bps=15.0,
|
|
||||||
max_adv_weight=max(
|
|
||||||
0.0,
|
|
||||||
min(0.10, turnover * 0.03 / 1_000_000.0),
|
|
||||||
),
|
|
||||||
industry=INDUSTRY_BY_SYMBOL[symbol],
|
|
||||||
state=state,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
solution = optimize_deterministic(
|
|
||||||
candidates,
|
|
||||||
PortfolioConstraints(
|
|
||||||
gross_target=0.90,
|
|
||||||
single_name_cap=0.10,
|
|
||||||
industry_cap=0.30,
|
|
||||||
one_way_turnover_cap=0.45,
|
|
||||||
risk_aversion=1.0,
|
|
||||||
cost_aversion=1.0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
quantities = target_quantities(
|
|
||||||
solution.weights,
|
|
||||||
latest_prices,
|
|
||||||
1_000_000.0,
|
|
||||||
lot_size=100,
|
|
||||||
cash_buffer=0.05,
|
|
||||||
)
|
|
||||||
|
|
||||||
data_version = _bar_data_version(bars)
|
|
||||||
source_sha256, sources = _source_identity()
|
source_sha256, sources = _source_identity()
|
||||||
config = {
|
config = {
|
||||||
"trading_days": trading_days,
|
"trading_days": trading_days,
|
||||||
@@ -423,16 +394,284 @@ def run_research_smoke(
|
|||||||
"test_dates": test_size,
|
"test_dates": test_size,
|
||||||
"purge_dates": 1,
|
"purge_dates": 1,
|
||||||
"embargo_dates": 1,
|
"embargo_dates": 1,
|
||||||
|
"final_label_availability": "exit_date_strictly_before_target",
|
||||||
|
},
|
||||||
|
"portfolio": {
|
||||||
|
"solver": "deterministic",
|
||||||
|
"gross_target": 0.90,
|
||||||
|
"single_name_cap": 0.10,
|
||||||
|
"industry_cap": 0.30,
|
||||||
|
"one_way_turnover_cap": 0.45,
|
||||||
|
"risk_aversion": 1.0,
|
||||||
|
"cost_aversion": 1.0,
|
||||||
|
},
|
||||||
|
"cost_model": {
|
||||||
|
"fee_version": "cn-a-share-synthetic-v1",
|
||||||
|
"half_spread_bps": 2.0,
|
||||||
|
"impact_coefficient": 0.10,
|
||||||
|
"max_participation": 0.10,
|
||||||
|
"reference_notional_weight": 0.10,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
config_hash = _sha256(config)
|
config_hash = _sha256(config)
|
||||||
|
final_processor = TrainOnlyFeaturePreprocessor().fit(
|
||||||
|
[row["features"] for row in final_train]
|
||||||
|
)
|
||||||
|
final_model = DeterministicRidge(alpha=5.0).fit(
|
||||||
|
_matrix(final_train, final_processor),
|
||||||
|
[row["label"] for row in final_train],
|
||||||
|
)
|
||||||
|
trained_through = max(
|
||||||
|
date.fromisoformat(row["exit_date"]) for row in final_train
|
||||||
|
)
|
||||||
|
model_bundle = create_model_bundle(
|
||||||
|
final_processor,
|
||||||
|
final_model,
|
||||||
|
trained_through=trained_through,
|
||||||
|
label_name=config["label"],
|
||||||
|
horizon_trading_days=horizon_sessions,
|
||||||
|
data_sha256=training_data_version,
|
||||||
|
source_sha256=source_sha256,
|
||||||
|
config_sha256=config_hash,
|
||||||
|
)
|
||||||
|
# Drive the published decision from the frozen artifact replay. The
|
||||||
|
# model digest carried by TargetPackageV1 is therefore a causal dependency
|
||||||
|
# of the target, not just a sidecar identity for an in-memory model.
|
||||||
|
frozen_model = verify_model_bundle(
|
||||||
|
model_bundle,
|
||||||
|
expected_data_sha256=training_data_version,
|
||||||
|
expected_source_sha256=source_sha256,
|
||||||
|
expected_config_sha256=config_hash,
|
||||||
|
)
|
||||||
|
final_predictions = frozen_model.predict(
|
||||||
|
[row["features"] for row in final_rows]
|
||||||
|
)
|
||||||
|
centered = statistics.fmean(final_predictions)
|
||||||
|
scores = {
|
||||||
|
row["symbol"]: prediction - centered
|
||||||
|
for row, prediction in zip(final_rows, final_predictions)
|
||||||
|
}
|
||||||
|
alpha_inputs = []
|
||||||
|
risk_inputs = []
|
||||||
|
cost_inputs = []
|
||||||
|
security_inputs = []
|
||||||
|
cost_forecasts = {}
|
||||||
|
risk_forecasts = {}
|
||||||
|
fee_rule = DatedFeeRule(
|
||||||
|
effective_from=date(2000, 1, 1),
|
||||||
|
effective_to=None,
|
||||||
|
fees=FeeConfig(),
|
||||||
|
version=config["cost_model"]["fee_version"],
|
||||||
|
)
|
||||||
|
cost_parameters = CostModelParameters(
|
||||||
|
half_spread_bps=config["cost_model"]["half_spread_bps"],
|
||||||
|
impact_coefficient=config["cost_model"]["impact_coefficient"],
|
||||||
|
max_participation=config["cost_model"]["max_participation"],
|
||||||
|
)
|
||||||
|
for symbol in RESEARCH_SYMBOLS:
|
||||||
|
completed = [
|
||||||
|
bar for bar in by_symbol[symbol] if bar.trading_date <= target_date
|
||||||
|
]
|
||||||
|
latest = completed[-1]
|
||||||
|
price = float(latest.close)
|
||||||
|
returns = [
|
||||||
|
float(completed[index].close)
|
||||||
|
/ float(completed[index - 1].close)
|
||||||
|
- 1.0
|
||||||
|
for index in range(max(1, len(completed) - 60), len(completed))
|
||||||
|
]
|
||||||
|
variance = (
|
||||||
|
statistics.variance(returns) if len(returns) > 1 else 1e-4
|
||||||
|
)
|
||||||
|
daily_volatility = (
|
||||||
|
statistics.stdev(returns) if len(returns) > 1 else 0.01
|
||||||
|
)
|
||||||
|
adv_quantity = statistics.median(
|
||||||
|
max(1, int(bar.volume)) for bar in completed[-20:]
|
||||||
|
)
|
||||||
|
reference_quantity = max(
|
||||||
|
100,
|
||||||
|
int(
|
||||||
|
1_000_000.0
|
||||||
|
* config["cost_model"]["reference_notional_weight"]
|
||||||
|
/ price
|
||||||
|
)
|
||||||
|
// 100
|
||||||
|
* 100,
|
||||||
|
)
|
||||||
|
cost_forecast = forecast_trade_cost(
|
||||||
|
symbol=symbol,
|
||||||
|
side=Side.BUY,
|
||||||
|
quantity=reference_quantity,
|
||||||
|
price=price,
|
||||||
|
adv_quantity=adv_quantity,
|
||||||
|
daily_volatility=max(1e-6, daily_volatility),
|
||||||
|
fee_rule=fee_rule,
|
||||||
|
parameters=cost_parameters,
|
||||||
|
)
|
||||||
|
max_adv_weight = min(
|
||||||
|
config["portfolio"]["single_name_cap"],
|
||||||
|
adv_quantity
|
||||||
|
* cost_parameters.max_participation
|
||||||
|
* price
|
||||||
|
/ 1_000_000.0,
|
||||||
|
)
|
||||||
|
feature_row = next(
|
||||||
|
row for row in final_rows if row["symbol"] == symbol
|
||||||
|
)
|
||||||
|
turnover = feature_row["features"]["median_turnover_20"] or 0.0
|
||||||
|
eligibility = SecurityEligibility(
|
||||||
|
symbol=symbol,
|
||||||
|
index_member=True,
|
||||||
|
listing_sessions=500,
|
||||||
|
median_turnover=turnover,
|
||||||
|
minimum_turnover=0.0,
|
||||||
|
held_quantity=0,
|
||||||
|
sellable_quantity=0,
|
||||||
|
)
|
||||||
|
universe_decision = classify_security(eligibility)
|
||||||
|
alpha_inputs.append(
|
||||||
|
AlphaForecastInput(
|
||||||
|
symbol=symbol,
|
||||||
|
expected_excess_return=scores[symbol],
|
||||||
|
model_version="deterministic-ridge-alpha-5",
|
||||||
|
feature_version=config["feature_version"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
risk_inputs.append(
|
||||||
|
RiskForecastInput(
|
||||||
|
symbol=symbol,
|
||||||
|
predicted_variance=max(1e-6, variance),
|
||||||
|
risk_model_version="diagonal-realized-variance-60d-v1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cost_inputs.append(
|
||||||
|
CostForecastInput(
|
||||||
|
symbol=symbol,
|
||||||
|
base_bps=cost_forecast.base_bps,
|
||||||
|
max_adv_weight=max(0.0, max_adv_weight),
|
||||||
|
cost_model_version="explicit-spread-sqrt-impact-v1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
security_inputs.append(
|
||||||
|
SecurityStateInput(
|
||||||
|
symbol=symbol,
|
||||||
|
state=universe_decision.state,
|
||||||
|
industry=INDUSTRY_BY_SYMBOL[symbol],
|
||||||
|
price=price,
|
||||||
|
current_quantity=0,
|
||||||
|
sellable_quantity=0,
|
||||||
|
reasons=universe_decision.reasons,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
risk_forecasts[symbol] = max(1e-6, variance)
|
||||||
|
cost_forecasts[symbol] = {
|
||||||
|
"base_bps": cost_forecast.base_bps,
|
||||||
|
"participation": cost_forecast.participation,
|
||||||
|
"capacity_breached": cost_forecast.capacity_breached,
|
||||||
|
"max_adv_weight": max(0.0, max_adv_weight),
|
||||||
|
"fee_version": cost_forecast.fee_version,
|
||||||
|
}
|
||||||
|
constraints = PortfolioConstraints(
|
||||||
|
gross_target=config["portfolio"]["gross_target"],
|
||||||
|
single_name_cap=config["portfolio"]["single_name_cap"],
|
||||||
|
industry_cap=config["portfolio"]["industry_cap"],
|
||||||
|
one_way_turnover_cap=config["portfolio"]["one_way_turnover_cap"],
|
||||||
|
risk_aversion=config["portfolio"]["risk_aversion"],
|
||||||
|
cost_aversion=config["portfolio"]["cost_aversion"],
|
||||||
|
)
|
||||||
|
pipeline = build_baseline_pipeline(
|
||||||
|
alpha=alpha_inputs,
|
||||||
|
risk=risk_inputs,
|
||||||
|
cost=cost_inputs,
|
||||||
|
securities=security_inputs,
|
||||||
|
equity=1_000_000.0,
|
||||||
|
policy=BaselinePipelinePolicy(
|
||||||
|
constraints=constraints,
|
||||||
|
solver=config["portfolio"]["solver"],
|
||||||
|
lot_size=100,
|
||||||
|
cash_buffer=0.05,
|
||||||
|
release_role="research_candidate",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
solution = pipeline.solution
|
||||||
|
quantities = pipeline.target_quantities
|
||||||
|
risk_sha256 = _sha256(
|
||||||
|
{
|
||||||
|
"model_version": "diagonal-realized-variance-60d-v1",
|
||||||
|
"forecasts": risk_forecasts,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cost_sha256 = _sha256(
|
||||||
|
{
|
||||||
|
"model_version": "explicit-spread-sqrt-impact-v1",
|
||||||
|
"forecasts": cost_forecasts,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
feature_sha256 = _sha256(
|
||||||
|
{
|
||||||
|
"feature_version": config["feature_version"],
|
||||||
|
"as_of": target_date.isoformat(),
|
||||||
|
"rows": {
|
||||||
|
row["symbol"]: row["features"]
|
||||||
|
for row in sorted(final_rows, key=lambda item: item["symbol"])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
optimizer_sha256 = _sha256(
|
||||||
|
{
|
||||||
|
"implementation": pipeline.layer_trace[2]["implementation"],
|
||||||
|
"constraints": config["portfolio"],
|
||||||
|
"portfolio_output_sha256": pipeline.layer_trace[2]["output_sha256"],
|
||||||
|
}
|
||||||
|
)
|
||||||
run_id = "q60-research-" + _sha256(
|
run_id = "q60-research-" + _sha256(
|
||||||
{
|
{
|
||||||
"data": data_version,
|
"data": full_data_version,
|
||||||
"source": source_sha256,
|
"source": source_sha256,
|
||||||
"config": config_hash,
|
"config": config_hash,
|
||||||
}
|
}
|
||||||
)[:16]
|
)[:16]
|
||||||
|
target_package = seal_target_package(
|
||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"package_type": "TargetPackageV1",
|
||||||
|
"package_id": f"TP-{target_date.strftime('%Y%m%d')}-{run_id[-8:]}",
|
||||||
|
"decision_id": f"RD-{target_date.strftime('%Y%m%d')}",
|
||||||
|
"release": "quant60-research-candidate-v1",
|
||||||
|
"evidence_class": "synthetic-research",
|
||||||
|
"model_id": "deterministic-ridge-alpha-5",
|
||||||
|
"signal_as_of": f"{target_date.isoformat()}T15:00:00+08:00",
|
||||||
|
"next_session": next_session.isoformat(),
|
||||||
|
"model_sha256": model_bundle["bundle_sha256"],
|
||||||
|
"data_sha256": decision_data_version,
|
||||||
|
"feature_sha256": feature_sha256,
|
||||||
|
"risk_sha256": risk_sha256,
|
||||||
|
"cost_sha256": cost_sha256,
|
||||||
|
"optimizer_sha256": optimizer_sha256,
|
||||||
|
"config_sha256": config_hash,
|
||||||
|
"source_sha256": source_sha256,
|
||||||
|
"target_weights": dict(sorted(solution.weights.items())),
|
||||||
|
"universe": {
|
||||||
|
item.symbol: {
|
||||||
|
"state": item.state.value,
|
||||||
|
"reasons": sorted(item.reasons),
|
||||||
|
}
|
||||||
|
for item in sorted(
|
||||||
|
security_inputs,
|
||||||
|
key=lambda security: security.symbol,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"constraint_state": "FEASIBLE",
|
||||||
|
"long_only": True,
|
||||||
|
"max_single_weight": constraints.single_name_cap,
|
||||||
|
"max_gross_weight": constraints.gross_target,
|
||||||
|
"max_one_way_turnover": constraints.one_way_turnover_cap,
|
||||||
|
"actual_gross_weight": solution.gross_weight,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
signal_records = [
|
signal_records = [
|
||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
@@ -445,7 +684,7 @@ def run_research_smoke(
|
|||||||
"expected_excess_return": scores[symbol],
|
"expected_excess_return": scores[symbol],
|
||||||
"confidence": None,
|
"confidence": None,
|
||||||
"model_version": "deterministic-ridge-alpha-5",
|
"model_version": "deterministic-ridge-alpha-5",
|
||||||
"data_version": data_version,
|
"data_version": decision_data_version,
|
||||||
"feature_version": "transparent-price-v1",
|
"feature_version": "transparent-price-v1",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"evidence_class": "synthetic-research",
|
"evidence_class": "synthetic-research",
|
||||||
@@ -483,7 +722,9 @@ def run_research_smoke(
|
|||||||
"evidence_class": "synthetic-research",
|
"evidence_class": "synthetic-research",
|
||||||
"investment_value_claim": False,
|
"investment_value_claim": False,
|
||||||
"config_hash": config_hash,
|
"config_hash": config_hash,
|
||||||
"data_version": data_version,
|
"data_version": decision_data_version,
|
||||||
|
"full_synthetic_data_version": full_data_version,
|
||||||
|
"training_data_version": training_data_version,
|
||||||
"engine_source_sha256": source_sha256,
|
"engine_source_sha256": source_sha256,
|
||||||
"sample_count": len(rows),
|
"sample_count": len(rows),
|
||||||
"decision_date_count": len(decision_dates),
|
"decision_date_count": len(decision_dates),
|
||||||
@@ -500,6 +741,19 @@ def run_research_smoke(
|
|||||||
else 0.0
|
else 0.0
|
||||||
),
|
),
|
||||||
"target_date": target_date.isoformat(),
|
"target_date": target_date.isoformat(),
|
||||||
|
"next_session": next_session.isoformat(),
|
||||||
|
"final_training_max_label_exit": trained_through.isoformat(),
|
||||||
|
"model_bundle_sha256": model_bundle["bundle_sha256"],
|
||||||
|
"target_package_sha256": target_package["package_sha256"],
|
||||||
|
"pipeline_id": pipeline.pipeline_id,
|
||||||
|
"five_layer_trace": [
|
||||||
|
{
|
||||||
|
"layer": item["layer"],
|
||||||
|
"implementation": item["implementation"],
|
||||||
|
"event_sha256": item["event_sha256"],
|
||||||
|
}
|
||||||
|
for item in pipeline.layer_trace
|
||||||
|
],
|
||||||
"target_solver": solution.solver,
|
"target_solver": solution.solver,
|
||||||
"target_gross_weight": solution.gross_weight,
|
"target_gross_weight": solution.gross_weight,
|
||||||
"target_cash_weight": solution.cash_weight,
|
"target_cash_weight": solution.cash_weight,
|
||||||
@@ -529,7 +783,9 @@ def run_research_smoke(
|
|||||||
"evidence_class": "synthetic-research",
|
"evidence_class": "synthetic-research",
|
||||||
"config": config,
|
"config": config,
|
||||||
"config_hash": config_hash,
|
"config_hash": config_hash,
|
||||||
"data_version": data_version,
|
"data_version": decision_data_version,
|
||||||
|
"full_synthetic_data_version": full_data_version,
|
||||||
|
"training_data_version": training_data_version,
|
||||||
"engine_source_sha256": source_sha256,
|
"engine_source_sha256": source_sha256,
|
||||||
"engine_sources": sources,
|
"engine_sources": sources,
|
||||||
"deterministic": True,
|
"deterministic": True,
|
||||||
@@ -542,6 +798,9 @@ def run_research_smoke(
|
|||||||
"targets": target_records,
|
"targets": target_records,
|
||||||
"forecasts": forecasts,
|
"forecasts": forecasts,
|
||||||
"samples": sample_index,
|
"samples": sample_index,
|
||||||
|
"model_bundle": model_bundle,
|
||||||
|
"target_package": target_package,
|
||||||
|
"pipeline": pipeline.as_dict(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -551,6 +810,9 @@ RESEARCH_ARTIFACTS = {
|
|||||||
"targets": "targets.json",
|
"targets": "targets.json",
|
||||||
"forecasts": "forecasts.json",
|
"forecasts": "forecasts.json",
|
||||||
"samples": "samples.json",
|
"samples": "samples.json",
|
||||||
|
"model_bundle": "model_bundle.json",
|
||||||
|
"target_package": "target_package.json",
|
||||||
|
"pipeline": "pipeline.json",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -612,9 +874,39 @@ def verify_research_manifest(path: str | Path) -> dict[str, Any]:
|
|||||||
targets = json.loads((root / RESEARCH_ARTIFACTS["targets"]).read_text())
|
targets = json.loads((root / RESEARCH_ARTIFACTS["targets"]).read_text())
|
||||||
validate_records(signals, "signal.schema.json")
|
validate_records(signals, "signal.schema.json")
|
||||||
validate_records(targets, "target.schema.json")
|
validate_records(targets, "target.schema.json")
|
||||||
|
model_bundle = json.loads(
|
||||||
|
(root / RESEARCH_ARTIFACTS["model_bundle"]).read_text()
|
||||||
|
)
|
||||||
|
verified_model = verify_model_bundle(
|
||||||
|
model_bundle,
|
||||||
|
expected_source_sha256=manifest["engine_source_sha256"],
|
||||||
|
expected_config_sha256=manifest["config_hash"],
|
||||||
|
)
|
||||||
|
target_package = json.loads(
|
||||||
|
(root / RESEARCH_ARTIFACTS["target_package"]).read_text()
|
||||||
|
)
|
||||||
|
validated_package = validate_target_package(target_package)
|
||||||
|
if validated_package["model_sha256"] != verified_model.bundle_sha256:
|
||||||
|
raise ValueError("target package/model bundle identity mismatch")
|
||||||
|
pipeline = json.loads((root / RESEARCH_ARTIFACTS["pipeline"]).read_text())
|
||||||
|
verify_layer_trace(pipeline["layer_trace"], pipeline["layer_payloads"])
|
||||||
|
if validated_package["target_weights"] != pipeline["weights"]:
|
||||||
|
raise ValueError("target package/pipeline weight mismatch")
|
||||||
report = json.loads((root / RESEARCH_ARTIFACTS["report"]).read_text())
|
report = json.loads((root / RESEARCH_ARTIFACTS["report"]).read_text())
|
||||||
if report["run_id"] != manifest["run_id"]:
|
if report["run_id"] != manifest["run_id"]:
|
||||||
raise ValueError("research report/manifest run_id mismatch")
|
raise ValueError("research report/manifest run_id mismatch")
|
||||||
|
if report["model_bundle_sha256"] != verified_model.bundle_sha256:
|
||||||
|
raise ValueError("research report/model bundle identity mismatch")
|
||||||
|
if (
|
||||||
|
report["target_package_sha256"]
|
||||||
|
!= validated_package["package_sha256"]
|
||||||
|
):
|
||||||
|
raise ValueError("research report/target package identity mismatch")
|
||||||
|
if not (
|
||||||
|
date.fromisoformat(report["final_training_max_label_exit"])
|
||||||
|
< date.fromisoformat(report["target_date"])
|
||||||
|
):
|
||||||
|
raise ValueError("final model contains a label unavailable at decision")
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"run_id": manifest["run_id"],
|
"run_id": manifest["run_id"],
|
||||||
|
|||||||
@@ -0,0 +1,602 @@
|
|||||||
|
"""Frozen, dependency-free Ridge model artifacts and deterministic replay.
|
||||||
|
|
||||||
|
The bundle is deliberately limited to the model path that Quant60 can replay
|
||||||
|
without a research environment: ``TrainOnlyFeaturePreprocessor`` followed by
|
||||||
|
``DeterministicRidge``. A self-hash detects byte/content tampering, while the
|
||||||
|
semantic verifier checks the relationships that JSON Schema cannot express
|
||||||
|
(feature order, transform order, and coefficient width).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Mapping, Sequence
|
||||||
|
|
||||||
|
from .contracts import load_schema, validate_contract
|
||||||
|
from .features import FeatureTransform, TrainOnlyFeaturePreprocessor
|
||||||
|
from .ledger import canonical_json
|
||||||
|
from .research import DeterministicRidge
|
||||||
|
|
||||||
|
|
||||||
|
MODEL_BUNDLE_SCHEMA_VERSION = "1.0"
|
||||||
|
MODEL_BUNDLE_ARTIFACT_TYPE = "quant60-deterministic-ridge-bundle"
|
||||||
|
PREPROCESSOR_TYPE = "train-only-winsor-impute-standardize"
|
||||||
|
MODEL_TYPE = "deterministic-ridge"
|
||||||
|
_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
|
||||||
|
|
||||||
|
|
||||||
|
class ModelBundleIntegrityError(ValueError):
|
||||||
|
"""A model bundle is malformed, inconsistent, or fails its identity check."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class FrozenFeatureTransform:
|
||||||
|
name: str
|
||||||
|
lower: float
|
||||||
|
upper: float
|
||||||
|
median: float
|
||||||
|
mean: float
|
||||||
|
scale: float
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"lower": self.lower,
|
||||||
|
"upper": self.upper,
|
||||||
|
"median": self.median,
|
||||||
|
"mean": self.mean,
|
||||||
|
"scale": self.scale,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class FrozenRidgeModelBundle:
|
||||||
|
"""An immutable, semantically verified model ready for replay."""
|
||||||
|
|
||||||
|
bundle_sha256: str
|
||||||
|
trained_through: date
|
||||||
|
label_name: str
|
||||||
|
horizon_trading_days: int
|
||||||
|
data_sha256: str
|
||||||
|
source_sha256: str
|
||||||
|
config_sha256: str
|
||||||
|
winsor_fraction: float
|
||||||
|
feature_order: tuple[str, ...]
|
||||||
|
output_feature_order: tuple[str, ...]
|
||||||
|
transforms: tuple[FrozenFeatureTransform, ...]
|
||||||
|
alpha: float
|
||||||
|
fit_intercept: bool
|
||||||
|
coefficients: tuple[float, ...]
|
||||||
|
intercept: float
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"schema_version": MODEL_BUNDLE_SCHEMA_VERSION,
|
||||||
|
"artifact_type": MODEL_BUNDLE_ARTIFACT_TYPE,
|
||||||
|
"bundle_sha256": self.bundle_sha256,
|
||||||
|
"trained_through": self.trained_through.isoformat(),
|
||||||
|
"label": {
|
||||||
|
"name": self.label_name,
|
||||||
|
"horizon_trading_days": self.horizon_trading_days,
|
||||||
|
},
|
||||||
|
"lineage": {
|
||||||
|
"data_sha256": self.data_sha256,
|
||||||
|
"source_sha256": self.source_sha256,
|
||||||
|
"config_sha256": self.config_sha256,
|
||||||
|
},
|
||||||
|
"preprocessor": {
|
||||||
|
"type": PREPROCESSOR_TYPE,
|
||||||
|
"winsor_fraction": self.winsor_fraction,
|
||||||
|
"feature_order": list(self.feature_order),
|
||||||
|
"output_feature_order": list(self.output_feature_order),
|
||||||
|
"transforms": [
|
||||||
|
transform.as_dict() for transform in self.transforms
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": MODEL_TYPE,
|
||||||
|
"alpha": self.alpha,
|
||||||
|
"fit_intercept": self.fit_intercept,
|
||||||
|
"coefficients": list(self.coefficients),
|
||||||
|
"intercept": self.intercept,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def canonical_bytes(self) -> bytes:
|
||||||
|
"""Return the exact deterministic bytes used by ``write_model_bundle``."""
|
||||||
|
|
||||||
|
return _published_bytes(self.as_dict())
|
||||||
|
|
||||||
|
def _preprocessor(self) -> TrainOnlyFeaturePreprocessor:
|
||||||
|
processor = TrainOnlyFeaturePreprocessor(self.winsor_fraction)
|
||||||
|
processor.transforms = {
|
||||||
|
transform.name: FeatureTransform(
|
||||||
|
lower=transform.lower,
|
||||||
|
upper=transform.upper,
|
||||||
|
median=transform.median,
|
||||||
|
mean=transform.mean,
|
||||||
|
scale=transform.scale,
|
||||||
|
)
|
||||||
|
for transform in self.transforms
|
||||||
|
}
|
||||||
|
return processor
|
||||||
|
|
||||||
|
def _model(self) -> DeterministicRidge:
|
||||||
|
model = DeterministicRidge(
|
||||||
|
alpha=self.alpha,
|
||||||
|
fit_intercept=self.fit_intercept,
|
||||||
|
)
|
||||||
|
model.coef_ = self.coefficients
|
||||||
|
model.intercept_ = self.intercept
|
||||||
|
return model
|
||||||
|
|
||||||
|
def predict(
|
||||||
|
self,
|
||||||
|
rows: Sequence[Mapping[str, float | None]],
|
||||||
|
) -> list[float]:
|
||||||
|
"""Replay predictions using only the state frozen in this bundle."""
|
||||||
|
|
||||||
|
processor = self._preprocessor()
|
||||||
|
transformed = processor.transform(rows)
|
||||||
|
matrix = [
|
||||||
|
[row[name] for name in self.output_feature_order]
|
||||||
|
for row in transformed
|
||||||
|
]
|
||||||
|
return self._model().predict(matrix)
|
||||||
|
|
||||||
|
def prediction_bytes(
|
||||||
|
self,
|
||||||
|
rows: Sequence[Mapping[str, float | None]],
|
||||||
|
) -> bytes:
|
||||||
|
"""Return a deterministic prediction envelope suitable for hashing."""
|
||||||
|
|
||||||
|
return _published_bytes(
|
||||||
|
{
|
||||||
|
"bundle_sha256": self.bundle_sha256,
|
||||||
|
"predictions": self.predict(rows),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _published_bytes(value: Any) -> bytes:
|
||||||
|
return (canonical_json(value) + "\n").encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _bundle_body(payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
key: value
|
||||||
|
for key, value in payload.items()
|
||||||
|
if key != "bundle_sha256"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def model_bundle_sha256(payload: Mapping[str, Any]) -> str:
|
||||||
|
"""Hash the canonical bundle body, excluding its self-hash field."""
|
||||||
|
|
||||||
|
return hashlib.sha256(
|
||||||
|
canonical_json(_bundle_body(payload)).encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _strict_json(path: Path) -> Any:
|
||||||
|
def reject_constant(value: str) -> None:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"non-finite JSON constant in model bundle: {value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def reject_duplicate_keys(
|
||||||
|
pairs: list[tuple[str, Any]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
output: dict[str, Any] = {}
|
||||||
|
for key, value in pairs:
|
||||||
|
if key in output:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"duplicate JSON key in model bundle: {key}"
|
||||||
|
)
|
||||||
|
output[key] = value
|
||||||
|
return output
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(
|
||||||
|
path.read_text(encoding="utf-8"),
|
||||||
|
parse_constant=reject_constant,
|
||||||
|
object_pairs_hook=reject_duplicate_keys,
|
||||||
|
)
|
||||||
|
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"model bundle is not valid UTF-8 JSON: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _json_copy(payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
copied = json.loads(canonical_json(dict(payload)))
|
||||||
|
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"model bundle is not canonical JSON data: {exc}"
|
||||||
|
) from exc
|
||||||
|
if not isinstance(copied, dict):
|
||||||
|
raise ModelBundleIntegrityError("model bundle root must be an object")
|
||||||
|
return copied
|
||||||
|
|
||||||
|
|
||||||
|
def _finite_float(value: Any, field: str) -> float:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
raise ModelBundleIntegrityError(f"{field} must be a finite number")
|
||||||
|
try:
|
||||||
|
normalized = float(value)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"{field} must be a finite number"
|
||||||
|
) from exc
|
||||||
|
if not math.isfinite(normalized):
|
||||||
|
raise ModelBundleIntegrityError(f"{field} must be a finite number")
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _require_sha256(value: Any, field: str) -> str:
|
||||||
|
normalized = str(value)
|
||||||
|
if _SHA256_PATTERN.fullmatch(normalized) is None:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"{field} must be a lowercase SHA-256 digest"
|
||||||
|
)
|
||||||
|
if normalized == "0" * 64:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"{field} cannot be an all-zero placeholder"
|
||||||
|
)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _semantic_model_bundle(payload: Mapping[str, Any]) -> FrozenRidgeModelBundle:
|
||||||
|
try:
|
||||||
|
trained_through = date.fromisoformat(str(payload["trained_through"]))
|
||||||
|
label = payload["label"]
|
||||||
|
lineage = payload["lineage"]
|
||||||
|
preprocessor = payload["preprocessor"]
|
||||||
|
model = payload["model"]
|
||||||
|
except (KeyError, TypeError, ValueError) as exc:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"model bundle metadata is invalid: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
label_name = str(label["name"])
|
||||||
|
if not label_name.strip() or label_name != label_name.strip():
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"label.name must be non-empty without surrounding whitespace"
|
||||||
|
)
|
||||||
|
horizon = label["horizon_trading_days"]
|
||||||
|
if type(horizon) is not int or horizon <= 0:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"label.horizon_trading_days must be a positive integer"
|
||||||
|
)
|
||||||
|
|
||||||
|
winsor_fraction = _finite_float(
|
||||||
|
preprocessor["winsor_fraction"],
|
||||||
|
"preprocessor.winsor_fraction",
|
||||||
|
)
|
||||||
|
if not 0.0 <= winsor_fraction < 0.5:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"preprocessor.winsor_fraction must lie in [0, 0.5)"
|
||||||
|
)
|
||||||
|
|
||||||
|
feature_order = tuple(preprocessor["feature_order"])
|
||||||
|
output_order = tuple(preprocessor["output_feature_order"])
|
||||||
|
if (
|
||||||
|
not feature_order
|
||||||
|
or any(
|
||||||
|
not isinstance(name, str)
|
||||||
|
or not name
|
||||||
|
or name != name.strip()
|
||||||
|
for name in feature_order
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"preprocessor.feature_order must contain non-empty names"
|
||||||
|
)
|
||||||
|
if len(set(feature_order)) != len(feature_order):
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"preprocessor.feature_order contains duplicates"
|
||||||
|
)
|
||||||
|
if feature_order != tuple(sorted(feature_order)):
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"preprocessor.feature_order is not canonical"
|
||||||
|
)
|
||||||
|
expected_output_order = tuple(
|
||||||
|
item
|
||||||
|
for name in feature_order
|
||||||
|
for item in (name, f"{name}__missing")
|
||||||
|
)
|
||||||
|
if output_order != expected_output_order:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"preprocessor.output_feature_order is inconsistent"
|
||||||
|
)
|
||||||
|
if len(set(output_order)) != len(output_order):
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"preprocessor.output_feature_order contains collisions"
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_transforms = preprocessor["transforms"]
|
||||||
|
if not isinstance(raw_transforms, list):
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"preprocessor.transforms must be an array"
|
||||||
|
)
|
||||||
|
transforms: list[FrozenFeatureTransform] = []
|
||||||
|
for index, raw in enumerate(raw_transforms):
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"preprocessor.transforms[{index}] must be an object"
|
||||||
|
)
|
||||||
|
name = raw.get("name")
|
||||||
|
lower = _finite_float(raw.get("lower"), f"transforms[{index}].lower")
|
||||||
|
upper = _finite_float(raw.get("upper"), f"transforms[{index}].upper")
|
||||||
|
median = _finite_float(raw.get("median"), f"transforms[{index}].median")
|
||||||
|
mean = _finite_float(raw.get("mean"), f"transforms[{index}].mean")
|
||||||
|
scale = _finite_float(raw.get("scale"), f"transforms[{index}].scale")
|
||||||
|
if lower > upper:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"preprocessor transform {name!r} has lower above upper"
|
||||||
|
)
|
||||||
|
if not lower <= median <= upper:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"preprocessor transform {name!r} has median outside bounds"
|
||||||
|
)
|
||||||
|
if not lower <= mean <= upper:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"preprocessor transform {name!r} has mean outside bounds"
|
||||||
|
)
|
||||||
|
if scale <= 0:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"preprocessor transform {name!r} has non-positive scale"
|
||||||
|
)
|
||||||
|
transforms.append(
|
||||||
|
FrozenFeatureTransform(
|
||||||
|
name=str(name),
|
||||||
|
lower=lower,
|
||||||
|
upper=upper,
|
||||||
|
median=median,
|
||||||
|
mean=mean,
|
||||||
|
scale=scale,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
transform_names = tuple(item.name for item in transforms)
|
||||||
|
if transform_names != feature_order:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"preprocessor transform order does not match feature_order"
|
||||||
|
)
|
||||||
|
|
||||||
|
alpha = _finite_float(model["alpha"], "model.alpha")
|
||||||
|
if alpha < 0:
|
||||||
|
raise ModelBundleIntegrityError("model.alpha must be non-negative")
|
||||||
|
fit_intercept = model["fit_intercept"]
|
||||||
|
if type(fit_intercept) is not bool:
|
||||||
|
raise ModelBundleIntegrityError("model.fit_intercept must be a bool")
|
||||||
|
coefficients = tuple(
|
||||||
|
_finite_float(value, f"model.coefficients[{index}]")
|
||||||
|
for index, value in enumerate(model["coefficients"])
|
||||||
|
)
|
||||||
|
if len(coefficients) != len(output_order):
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"model coefficient width does not match output_feature_order"
|
||||||
|
)
|
||||||
|
intercept = _finite_float(model["intercept"], "model.intercept")
|
||||||
|
|
||||||
|
bundle = FrozenRidgeModelBundle(
|
||||||
|
bundle_sha256=_require_sha256(
|
||||||
|
payload["bundle_sha256"],
|
||||||
|
"bundle_sha256",
|
||||||
|
),
|
||||||
|
trained_through=trained_through,
|
||||||
|
label_name=label_name,
|
||||||
|
horizon_trading_days=horizon,
|
||||||
|
data_sha256=_require_sha256(
|
||||||
|
lineage["data_sha256"],
|
||||||
|
"lineage.data_sha256",
|
||||||
|
),
|
||||||
|
source_sha256=_require_sha256(
|
||||||
|
lineage["source_sha256"],
|
||||||
|
"lineage.source_sha256",
|
||||||
|
),
|
||||||
|
config_sha256=_require_sha256(
|
||||||
|
lineage["config_sha256"],
|
||||||
|
"lineage.config_sha256",
|
||||||
|
),
|
||||||
|
winsor_fraction=winsor_fraction,
|
||||||
|
feature_order=feature_order,
|
||||||
|
output_feature_order=output_order,
|
||||||
|
transforms=tuple(transforms),
|
||||||
|
alpha=alpha,
|
||||||
|
fit_intercept=fit_intercept,
|
||||||
|
coefficients=coefficients,
|
||||||
|
intercept=intercept,
|
||||||
|
)
|
||||||
|
if bundle.as_dict() != payload:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"model bundle is not in canonical semantic form"
|
||||||
|
)
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
def create_model_bundle(
|
||||||
|
preprocessor: TrainOnlyFeaturePreprocessor,
|
||||||
|
model: DeterministicRidge,
|
||||||
|
*,
|
||||||
|
trained_through: date | str,
|
||||||
|
label_name: str,
|
||||||
|
horizon_trading_days: int,
|
||||||
|
data_sha256: str,
|
||||||
|
source_sha256: str,
|
||||||
|
config_sha256: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Freeze a fitted preprocessor and Ridge model into a verified payload."""
|
||||||
|
|
||||||
|
if not isinstance(preprocessor, TrainOnlyFeaturePreprocessor):
|
||||||
|
raise TypeError(
|
||||||
|
"preprocessor must be a TrainOnlyFeaturePreprocessor"
|
||||||
|
)
|
||||||
|
if preprocessor.transforms is None:
|
||||||
|
raise ValueError("preprocessor is not fitted")
|
||||||
|
if not isinstance(model, DeterministicRidge):
|
||||||
|
raise TypeError("model must be a DeterministicRidge")
|
||||||
|
if model.coef_ is None:
|
||||||
|
raise ValueError("model is not fitted")
|
||||||
|
try:
|
||||||
|
training_date = (
|
||||||
|
trained_through
|
||||||
|
if isinstance(trained_through, date)
|
||||||
|
else date.fromisoformat(str(trained_through))
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError("trained_through must be an ISO date") from exc
|
||||||
|
|
||||||
|
feature_order = tuple(sorted(preprocessor.transforms))
|
||||||
|
transforms = [
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"lower": preprocessor.transforms[name].lower,
|
||||||
|
"upper": preprocessor.transforms[name].upper,
|
||||||
|
"median": preprocessor.transforms[name].median,
|
||||||
|
"mean": preprocessor.transforms[name].mean,
|
||||||
|
"scale": preprocessor.transforms[name].scale,
|
||||||
|
}
|
||||||
|
for name in feature_order
|
||||||
|
]
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
"schema_version": MODEL_BUNDLE_SCHEMA_VERSION,
|
||||||
|
"artifact_type": MODEL_BUNDLE_ARTIFACT_TYPE,
|
||||||
|
"bundle_sha256": "0" * 64,
|
||||||
|
"trained_through": training_date.isoformat(),
|
||||||
|
"label": {
|
||||||
|
"name": label_name,
|
||||||
|
"horizon_trading_days": horizon_trading_days,
|
||||||
|
},
|
||||||
|
"lineage": {
|
||||||
|
"data_sha256": data_sha256,
|
||||||
|
"source_sha256": source_sha256,
|
||||||
|
"config_sha256": config_sha256,
|
||||||
|
},
|
||||||
|
"preprocessor": {
|
||||||
|
"type": PREPROCESSOR_TYPE,
|
||||||
|
"winsor_fraction": preprocessor.winsor_fraction,
|
||||||
|
"feature_order": list(feature_order),
|
||||||
|
"output_feature_order": list(preprocessor.output_names),
|
||||||
|
"transforms": transforms,
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": MODEL_TYPE,
|
||||||
|
"alpha": model.alpha,
|
||||||
|
"fit_intercept": model.fit_intercept,
|
||||||
|
"coefficients": list(model.coef_),
|
||||||
|
"intercept": model.intercept_,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payload["bundle_sha256"] = model_bundle_sha256(payload)
|
||||||
|
return verify_model_bundle(payload).as_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_model_bundle(
|
||||||
|
value: Mapping[str, Any] | str | Path,
|
||||||
|
*,
|
||||||
|
expected_bundle_sha256: str | None = None,
|
||||||
|
expected_data_sha256: str | None = None,
|
||||||
|
expected_source_sha256: str | None = None,
|
||||||
|
expected_config_sha256: str | None = None,
|
||||||
|
) -> FrozenRidgeModelBundle:
|
||||||
|
"""Validate schema, self-hash, semantic relationships, and lineage pins."""
|
||||||
|
|
||||||
|
if isinstance(value, Mapping):
|
||||||
|
payload = _json_copy(value)
|
||||||
|
else:
|
||||||
|
path = Path(value).expanduser().resolve()
|
||||||
|
raw = _strict_json(path)
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
"model bundle root must be an object"
|
||||||
|
)
|
||||||
|
payload = raw
|
||||||
|
try:
|
||||||
|
validate_contract(
|
||||||
|
payload,
|
||||||
|
load_schema("model_bundle.schema.json"),
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, KeyError, TypeError, ValueError) as exc:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"model bundle schema validation failed: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
actual_hash = model_bundle_sha256(payload)
|
||||||
|
if payload.get("bundle_sha256") != actual_hash:
|
||||||
|
raise ModelBundleIntegrityError("model bundle SHA-256 mismatch")
|
||||||
|
bundle = _semantic_model_bundle(payload)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"bundle_sha256": expected_bundle_sha256,
|
||||||
|
"data_sha256": expected_data_sha256,
|
||||||
|
"source_sha256": expected_source_sha256,
|
||||||
|
"config_sha256": expected_config_sha256,
|
||||||
|
}
|
||||||
|
actual = {
|
||||||
|
"bundle_sha256": bundle.bundle_sha256,
|
||||||
|
"data_sha256": bundle.data_sha256,
|
||||||
|
"source_sha256": bundle.source_sha256,
|
||||||
|
"config_sha256": bundle.config_sha256,
|
||||||
|
}
|
||||||
|
for field, expected_value in expected.items():
|
||||||
|
if expected_value is None:
|
||||||
|
continue
|
||||||
|
normalized = _require_sha256(expected_value, f"expected_{field}")
|
||||||
|
if actual[field] != normalized:
|
||||||
|
raise ModelBundleIntegrityError(
|
||||||
|
f"model bundle {field} does not match expected identity"
|
||||||
|
)
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
def write_model_bundle(
|
||||||
|
bundle: FrozenRidgeModelBundle | Mapping[str, Any],
|
||||||
|
path: str | Path,
|
||||||
|
) -> Path:
|
||||||
|
"""Atomically write a verified bundle in deterministic canonical JSON."""
|
||||||
|
|
||||||
|
verified = verify_model_bundle(
|
||||||
|
bundle.as_dict()
|
||||||
|
if isinstance(bundle, FrozenRidgeModelBundle)
|
||||||
|
else bundle
|
||||||
|
)
|
||||||
|
destination = Path(path)
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
descriptor, temporary_name = tempfile.mkstemp(
|
||||||
|
prefix=f".{destination.name}.",
|
||||||
|
suffix=".tmp",
|
||||||
|
dir=str(destination.parent),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with os.fdopen(descriptor, "wb") as handle:
|
||||||
|
handle.write(verified.canonical_bytes)
|
||||||
|
handle.flush()
|
||||||
|
os.fsync(handle.fileno())
|
||||||
|
os.replace(temporary_name, destination)
|
||||||
|
except BaseException:
|
||||||
|
try:
|
||||||
|
os.unlink(temporary_name)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
return destination
|
||||||
|
|
||||||
|
|
||||||
|
def load_verified_model_bundle(
|
||||||
|
path: str | Path,
|
||||||
|
**expected_identity: str | None,
|
||||||
|
) -> FrozenRidgeModelBundle:
|
||||||
|
"""Read a bundle from disk and fail closed unless all checks pass."""
|
||||||
|
|
||||||
|
return verify_model_bundle(path, **expected_identity)
|
||||||
@@ -7,6 +7,8 @@ single file when importing the complete ``quant60`` package is not possible.
|
|||||||
|
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -30,6 +32,45 @@ _SHORT_EXCHANGES = {
|
|||||||
"XBSE": "BJ",
|
"XBSE": "BJ",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_TARGET_PACKAGE_FIELDS = {
|
||||||
|
"schema_version",
|
||||||
|
"package_type",
|
||||||
|
"package_id",
|
||||||
|
"decision_id",
|
||||||
|
"release",
|
||||||
|
"evidence_class",
|
||||||
|
"model_id",
|
||||||
|
"signal_as_of",
|
||||||
|
"next_session",
|
||||||
|
"model_sha256",
|
||||||
|
"data_sha256",
|
||||||
|
"feature_sha256",
|
||||||
|
"risk_sha256",
|
||||||
|
"cost_sha256",
|
||||||
|
"optimizer_sha256",
|
||||||
|
"config_sha256",
|
||||||
|
"source_sha256",
|
||||||
|
"target_weights",
|
||||||
|
"universe",
|
||||||
|
"constraints",
|
||||||
|
"package_sha256",
|
||||||
|
}
|
||||||
|
|
||||||
|
_TARGET_PACKAGE_HASH_FIELDS = (
|
||||||
|
"model_sha256",
|
||||||
|
"data_sha256",
|
||||||
|
"feature_sha256",
|
||||||
|
"risk_sha256",
|
||||||
|
"cost_sha256",
|
||||||
|
"optimizer_sha256",
|
||||||
|
"config_sha256",
|
||||||
|
"source_sha256",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TargetPackageLookupMiss(ValueError, LookupError):
|
||||||
|
"""An already verified tape has no exact requested decision."""
|
||||||
|
|
||||||
|
|
||||||
def _sum_numeric(values):
|
def _sum_numeric(values):
|
||||||
"""Sum numerics without trusting a hosted runtime's global ``sum`` name."""
|
"""Sum numerics without trusting a hosted runtime's global ``sum`` name."""
|
||||||
@@ -40,6 +81,15 @@ def _sum_numeric(values):
|
|||||||
return total
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def _any_true(values):
|
||||||
|
"""Return built-in ``any`` semantics without trusting hosted globals."""
|
||||||
|
|
||||||
|
for value in values:
|
||||||
|
if value:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _infer_exchange(code):
|
def _infer_exchange(code):
|
||||||
if code.startswith(("4", "8")):
|
if code.startswith(("4", "8")):
|
||||||
return "XBSE"
|
return "XBSE"
|
||||||
@@ -92,6 +142,396 @@ def convert_symbol(symbol, target="canonical"):
|
|||||||
return normalize_symbol(symbol, target)
|
return normalize_symbol(symbol, target)
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(value):
|
||||||
|
"""Match ``quant60.ledger.canonical_json`` for plain JSON values."""
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
value,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _json_copy(value, name):
|
||||||
|
try:
|
||||||
|
return json.loads(_canonical_json(value))
|
||||||
|
except (TypeError, ValueError) as error:
|
||||||
|
raise ValueError("%s must contain finite JSON values only" % name) from error
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_mapping(value, name, value_validator):
|
||||||
|
if not hasattr(value, "items"):
|
||||||
|
raise TypeError("%s must be a mapping" % name)
|
||||||
|
output = {}
|
||||||
|
for raw_symbol, raw_value in value.items():
|
||||||
|
symbol = normalize_symbol(raw_symbol)
|
||||||
|
if symbol in output:
|
||||||
|
raise ValueError(
|
||||||
|
"%s contains duplicate normalized symbol %s" % (name, symbol)
|
||||||
|
)
|
||||||
|
output[symbol] = value_validator(raw_value, "%s.%s" % (name, symbol))
|
||||||
|
return dict((symbol, output[symbol]) for symbol in sorted(output))
|
||||||
|
|
||||||
|
|
||||||
|
def _finite_non_negative(value, name):
|
||||||
|
if isinstance(value, bool):
|
||||||
|
raise ValueError("%s must be a finite non-negative number" % name)
|
||||||
|
try:
|
||||||
|
numeric = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValueError("%s must be a finite non-negative number" % name)
|
||||||
|
if not math.isfinite(numeric) or numeric < 0.0:
|
||||||
|
raise ValueError("%s must be a finite non-negative number" % name)
|
||||||
|
return numeric
|
||||||
|
|
||||||
|
|
||||||
|
def _non_negative_quantity(value, name):
|
||||||
|
numeric = _finite_non_negative(value, name)
|
||||||
|
if numeric != math.floor(numeric):
|
||||||
|
raise ValueError("%s must be a whole-share quantity" % name)
|
||||||
|
return int(numeric)
|
||||||
|
|
||||||
|
|
||||||
|
def _positive_price(value, name):
|
||||||
|
numeric = _finite_non_negative(value, name)
|
||||||
|
if numeric <= 0.0:
|
||||||
|
raise ValueError("%s must be positive" % name)
|
||||||
|
return numeric
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_non_empty_string(value, name):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(value, str) or not value.strip():
|
||||||
|
raise ValueError("%s must be a non-empty string when provided" % name)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256_hex(value, name, allow_none=False):
|
||||||
|
if value is None and allow_none:
|
||||||
|
return None
|
||||||
|
if (
|
||||||
|
not isinstance(value, str)
|
||||||
|
or len(value) != 64
|
||||||
|
or _any_true(
|
||||||
|
character not in "0123456789abcdef"
|
||||||
|
for character in value
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"%s must be a 64-character lowercase SHA-256 digest" % name
|
||||||
|
)
|
||||||
|
if value == "0" * 64:
|
||||||
|
raise ValueError("%s cannot be an all-zero placeholder" % name)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _reject_quantity_fields(value, path="$"):
|
||||||
|
if hasattr(value, "items"):
|
||||||
|
for raw_key, child in value.items():
|
||||||
|
key = str(raw_key)
|
||||||
|
normalized = key.lower().replace("-", "_")
|
||||||
|
if "quantity" in normalized or "quantities" in normalized:
|
||||||
|
raise ValueError(
|
||||||
|
"%s.%s: quantities do not belong in TargetPackageV1"
|
||||||
|
% (path, key)
|
||||||
|
)
|
||||||
|
_reject_quantity_fields(child, "%s.%s" % (path, key))
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for index, child in enumerate(value):
|
||||||
|
_reject_quantity_fields(child, "%s[%d]" % (path, index))
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_identifier(value, name):
|
||||||
|
if (
|
||||||
|
not isinstance(value, str)
|
||||||
|
or re.match(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", value) is None
|
||||||
|
):
|
||||||
|
raise ValueError("%s is not a canonical identifier" % name)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_target_package_clock(signal_as_of, next_session):
|
||||||
|
if (
|
||||||
|
not isinstance(signal_as_of, str)
|
||||||
|
or re.match(
|
||||||
|
r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T15:00:00\+08:00$",
|
||||||
|
signal_as_of,
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
):
|
||||||
|
raise ValueError("signal_as_of must be canonical China-market close")
|
||||||
|
if (
|
||||||
|
not isinstance(next_session, str)
|
||||||
|
or re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$", next_session) is None
|
||||||
|
):
|
||||||
|
raise ValueError("next_session must use canonical YYYY-MM-DD")
|
||||||
|
if next_session <= signal_as_of[:10]:
|
||||||
|
raise ValueError("next_session must follow signal_as_of")
|
||||||
|
|
||||||
|
|
||||||
|
def _target_package_content_sha256(package):
|
||||||
|
body = dict(package)
|
||||||
|
body.pop("package_sha256", None)
|
||||||
|
_reject_quantity_fields(body)
|
||||||
|
try:
|
||||||
|
payload = _canonical_json(body).encode("utf-8")
|
||||||
|
except (TypeError, ValueError) as error:
|
||||||
|
raise ValueError(
|
||||||
|
"target package must contain finite JSON values only"
|
||||||
|
) from error
|
||||||
|
return hashlib.sha256(payload).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_target_package(
|
||||||
|
package,
|
||||||
|
expected_signal_as_of=None,
|
||||||
|
expected_next_session=None,
|
||||||
|
expected_decision_id=None,
|
||||||
|
expected_package_sha256=None,
|
||||||
|
):
|
||||||
|
"""Verify the platform-critical subset of a sealed ``TargetPackageV1``.
|
||||||
|
|
||||||
|
This intentionally duplicates a small, Python-3.6-compatible safety
|
||||||
|
boundary from the richer local validator. Hosted runtimes must verify the
|
||||||
|
embedded package again instead of trusting the bundling process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not hasattr(package, "items"):
|
||||||
|
raise TypeError("target package must be a mapping")
|
||||||
|
candidate = _json_copy(package, "target package")
|
||||||
|
_reject_quantity_fields(candidate)
|
||||||
|
fields = set(candidate)
|
||||||
|
if fields != _TARGET_PACKAGE_FIELDS:
|
||||||
|
missing = sorted(_TARGET_PACKAGE_FIELDS - fields)
|
||||||
|
unknown = sorted(fields - _TARGET_PACKAGE_FIELDS)
|
||||||
|
raise ValueError(
|
||||||
|
"target package fields mismatch; missing=%s, unknown=%s"
|
||||||
|
% (missing, unknown)
|
||||||
|
)
|
||||||
|
if candidate["schema_version"] != "1.0":
|
||||||
|
raise ValueError("unsupported target package schema_version")
|
||||||
|
if candidate["package_type"] != "TargetPackageV1":
|
||||||
|
raise ValueError("unsupported target package type")
|
||||||
|
for name in (
|
||||||
|
"package_id",
|
||||||
|
"decision_id",
|
||||||
|
"release",
|
||||||
|
"evidence_class",
|
||||||
|
"model_id",
|
||||||
|
):
|
||||||
|
_verify_identifier(candidate[name], name)
|
||||||
|
_verify_target_package_clock(
|
||||||
|
candidate["signal_as_of"], candidate["next_session"]
|
||||||
|
)
|
||||||
|
for name in _TARGET_PACKAGE_HASH_FIELDS:
|
||||||
|
_sha256_hex(candidate[name], name)
|
||||||
|
_sha256_hex(candidate["package_sha256"], "package_sha256")
|
||||||
|
|
||||||
|
raw_weights = candidate["target_weights"]
|
||||||
|
if not hasattr(raw_weights, "items") or not raw_weights:
|
||||||
|
raise ValueError("target_weights must be a non-empty mapping")
|
||||||
|
weights = {}
|
||||||
|
for raw_symbol, raw_weight in raw_weights.items():
|
||||||
|
symbol = normalize_symbol(raw_symbol)
|
||||||
|
if symbol != raw_symbol:
|
||||||
|
raise ValueError(
|
||||||
|
"target_weights symbol must be canonical: %s" % raw_symbol
|
||||||
|
)
|
||||||
|
if symbol in weights:
|
||||||
|
raise ValueError("duplicate target weight for %s" % symbol)
|
||||||
|
weight = _finite_non_negative(
|
||||||
|
raw_weight, "target_weights.%s" % symbol
|
||||||
|
)
|
||||||
|
if weight > 1.0:
|
||||||
|
raise ValueError("target weight exceeds one for %s" % symbol)
|
||||||
|
weights[symbol] = weight
|
||||||
|
gross = math.fsum(weights.values())
|
||||||
|
if gross > 1.0 + 1e-12:
|
||||||
|
raise ValueError("target_weights gross exceeds one")
|
||||||
|
|
||||||
|
universe = candidate["universe"]
|
||||||
|
if not hasattr(universe, "items") or not universe:
|
||||||
|
raise ValueError("universe must be a non-empty mapping")
|
||||||
|
canonical_universe = {}
|
||||||
|
for raw_symbol, raw_state in universe.items():
|
||||||
|
symbol = normalize_symbol(raw_symbol)
|
||||||
|
if symbol != raw_symbol:
|
||||||
|
raise ValueError("universe symbol must be canonical: %s" % raw_symbol)
|
||||||
|
if symbol in canonical_universe:
|
||||||
|
raise ValueError("duplicate universe symbol %s" % symbol)
|
||||||
|
if not hasattr(raw_state, "items"):
|
||||||
|
raise ValueError("universe state must be a mapping for %s" % symbol)
|
||||||
|
state = raw_state.get("state")
|
||||||
|
if state not in (
|
||||||
|
"openable",
|
||||||
|
"hold_only",
|
||||||
|
"sell_only",
|
||||||
|
"frozen",
|
||||||
|
"excluded",
|
||||||
|
):
|
||||||
|
raise ValueError("unsupported universe state for %s" % symbol)
|
||||||
|
canonical_universe[symbol] = state
|
||||||
|
missing_universe = sorted(set(weights) - set(canonical_universe))
|
||||||
|
if missing_universe:
|
||||||
|
raise ValueError(
|
||||||
|
"target symbols are absent from universe: %s" % missing_universe
|
||||||
|
)
|
||||||
|
for symbol, weight in weights.items():
|
||||||
|
if canonical_universe[symbol] == "excluded" and weight > 1e-12:
|
||||||
|
raise ValueError("excluded symbol has positive weight: %s" % symbol)
|
||||||
|
|
||||||
|
constraints = candidate["constraints"]
|
||||||
|
if not hasattr(constraints, "items"):
|
||||||
|
raise ValueError("constraints must be a mapping")
|
||||||
|
if constraints.get("long_only") is not True:
|
||||||
|
raise ValueError("TargetPackageV1 must be long-only")
|
||||||
|
declared_gross = _finite_non_negative(
|
||||||
|
constraints.get("actual_gross_weight"),
|
||||||
|
"constraints.actual_gross_weight",
|
||||||
|
)
|
||||||
|
max_gross = _finite_non_negative(
|
||||||
|
constraints.get("max_gross_weight"),
|
||||||
|
"constraints.max_gross_weight",
|
||||||
|
)
|
||||||
|
max_single = _finite_non_negative(
|
||||||
|
constraints.get("max_single_weight"),
|
||||||
|
"constraints.max_single_weight",
|
||||||
|
)
|
||||||
|
if abs(declared_gross - gross) > 1e-12:
|
||||||
|
raise ValueError("declared gross does not equal target_weights gross")
|
||||||
|
if gross > max_gross + 1e-12 or max_gross > 1.0:
|
||||||
|
raise ValueError("target_weights exceed max_gross_weight")
|
||||||
|
if max_single <= 0.0 or max_single > 1.0:
|
||||||
|
raise ValueError("invalid max_single_weight")
|
||||||
|
if _any_true(
|
||||||
|
weight > max_single + 1e-12 for weight in weights.values()
|
||||||
|
):
|
||||||
|
raise ValueError("target_weights exceed max_single_weight")
|
||||||
|
model_claim = " ".join(
|
||||||
|
(
|
||||||
|
candidate["release"],
|
||||||
|
candidate["evidence_class"],
|
||||||
|
)
|
||||||
|
).lower()
|
||||||
|
if (
|
||||||
|
"baseline" in model_claim
|
||||||
|
and "portable-momentum" in candidate["model_id"].lower()
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"portable-momentum cannot claim a baseline target package"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_hash = _target_package_content_sha256(candidate)
|
||||||
|
if candidate["package_sha256"] != expected_hash:
|
||||||
|
raise ValueError("target package content hash mismatch")
|
||||||
|
if (
|
||||||
|
expected_signal_as_of is not None
|
||||||
|
and candidate["signal_as_of"] != expected_signal_as_of
|
||||||
|
):
|
||||||
|
raise ValueError("target package signal_as_of does not match request")
|
||||||
|
if (
|
||||||
|
expected_next_session is not None
|
||||||
|
and candidate["next_session"] != expected_next_session
|
||||||
|
):
|
||||||
|
raise ValueError("target package next_session does not match request")
|
||||||
|
if (
|
||||||
|
expected_decision_id is not None
|
||||||
|
and candidate["decision_id"] != expected_decision_id
|
||||||
|
):
|
||||||
|
raise ValueError("target package decision_id does not match request")
|
||||||
|
if expected_package_sha256 is not None:
|
||||||
|
_sha256_hex(
|
||||||
|
expected_package_sha256,
|
||||||
|
"expected_package_sha256",
|
||||||
|
)
|
||||||
|
if candidate["package_sha256"] != expected_package_sha256:
|
||||||
|
raise ValueError("target package digest does not match request")
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
def target_package_tape_sha256(packages):
|
||||||
|
"""Hash the exact ordered canonical JSON package array."""
|
||||||
|
|
||||||
|
if not isinstance(packages, (list, tuple)):
|
||||||
|
raise TypeError("target package tape must be a list or tuple")
|
||||||
|
try:
|
||||||
|
payload = _canonical_json(list(packages)).encode("utf-8")
|
||||||
|
except (TypeError, ValueError) as error:
|
||||||
|
raise ValueError(
|
||||||
|
"target package tape must contain finite JSON values only"
|
||||||
|
) from error
|
||||||
|
return hashlib.sha256(payload).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_target_package_exact(
|
||||||
|
packages,
|
||||||
|
signal_as_of,
|
||||||
|
next_session,
|
||||||
|
decision_id=None,
|
||||||
|
expected_tape_sha256=None,
|
||||||
|
expected_count=None,
|
||||||
|
):
|
||||||
|
"""Verify a complete tape, then return one exact dual-clock decision."""
|
||||||
|
|
||||||
|
if not isinstance(packages, (list, tuple)) or not packages:
|
||||||
|
raise ValueError("target package tape must be a non-empty list or tuple")
|
||||||
|
_verify_target_package_clock(signal_as_of, next_session)
|
||||||
|
decision_id = _optional_non_empty_string(decision_id, "decision_id")
|
||||||
|
if expected_count is not None:
|
||||||
|
if (
|
||||||
|
isinstance(expected_count, bool)
|
||||||
|
or not isinstance(expected_count, int)
|
||||||
|
or expected_count < 0
|
||||||
|
):
|
||||||
|
raise ValueError("expected_count must be a non-negative integer")
|
||||||
|
if len(packages) != expected_count:
|
||||||
|
raise ValueError("target package tape count mismatch")
|
||||||
|
if expected_tape_sha256 is not None:
|
||||||
|
_sha256_hex(expected_tape_sha256, "expected_tape_sha256")
|
||||||
|
if target_package_tape_sha256(packages) != expected_tape_sha256:
|
||||||
|
raise ValueError("target package tape digest mismatch")
|
||||||
|
|
||||||
|
verified = []
|
||||||
|
package_ids = set()
|
||||||
|
decision_ids = set()
|
||||||
|
clock_keys = set()
|
||||||
|
for raw_package in packages:
|
||||||
|
package = verify_target_package(raw_package)
|
||||||
|
clock_key = (package["signal_as_of"], package["next_session"])
|
||||||
|
if package["package_id"] in package_ids:
|
||||||
|
raise ValueError("duplicate target package_id")
|
||||||
|
if package["decision_id"] in decision_ids:
|
||||||
|
raise ValueError("duplicate target decision_id")
|
||||||
|
if clock_key in clock_keys:
|
||||||
|
raise ValueError("duplicate target package clock")
|
||||||
|
package_ids.add(package["package_id"])
|
||||||
|
decision_ids.add(package["decision_id"])
|
||||||
|
clock_keys.add(clock_key)
|
||||||
|
verified.append(package)
|
||||||
|
|
||||||
|
matches = [
|
||||||
|
package
|
||||||
|
for package in verified
|
||||||
|
if package["signal_as_of"] == signal_as_of
|
||||||
|
and package["next_session"] == next_session
|
||||||
|
]
|
||||||
|
if not matches:
|
||||||
|
raise TargetPackageLookupMiss(
|
||||||
|
"no target package for exact signal_as_of/next_session clock"
|
||||||
|
)
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise ValueError("ambiguous exact target package decision")
|
||||||
|
if (
|
||||||
|
decision_id is not None
|
||||||
|
and matches[0]["decision_id"] != decision_id
|
||||||
|
):
|
||||||
|
raise ValueError("exact target package decision_id mismatch")
|
||||||
|
return _json_copy(matches[0], "target package")
|
||||||
|
|
||||||
|
|
||||||
def momentum_score(prices, lookback=20, skip=0):
|
def momentum_score(prices, lookback=20, skip=0):
|
||||||
"""Return trailing simple momentum, or ``None`` if history is insufficient.
|
"""Return trailing simple momentum, or ``None`` if history is insufficient.
|
||||||
|
|
||||||
@@ -318,6 +758,108 @@ def order_deltas(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def build_target_weight_plan(
|
||||||
|
target_weights,
|
||||||
|
prices,
|
||||||
|
current,
|
||||||
|
sellable,
|
||||||
|
equity,
|
||||||
|
cash_buffer=0.02,
|
||||||
|
lot_size=100,
|
||||||
|
decision_id=None,
|
||||||
|
package_sha256=None,
|
||||||
|
):
|
||||||
|
"""Bind canonical post-risk weights to one account without recomputing Alpha.
|
||||||
|
|
||||||
|
``prices`` must cover exactly every target or currently managed symbol.
|
||||||
|
Missing prices fail closed even for a held symbol whose desired weight is
|
||||||
|
zero. A missing ``sellable`` entry is safe and means zero sellable shares.
|
||||||
|
"""
|
||||||
|
|
||||||
|
weights = _canonical_mapping(
|
||||||
|
target_weights,
|
||||||
|
"target_weights",
|
||||||
|
_finite_non_negative,
|
||||||
|
)
|
||||||
|
gross = math.fsum(weights.values())
|
||||||
|
if _any_true(weight > 1.0 for weight in weights.values()):
|
||||||
|
raise ValueError("target weight must not exceed one")
|
||||||
|
if gross > 1.0 + 1e-12:
|
||||||
|
raise ValueError("target weight gross must not exceed one")
|
||||||
|
|
||||||
|
canonical_current = _canonical_mapping(
|
||||||
|
current,
|
||||||
|
"current",
|
||||||
|
_non_negative_quantity,
|
||||||
|
)
|
||||||
|
canonical_sellable = _canonical_mapping(
|
||||||
|
sellable,
|
||||||
|
"sellable",
|
||||||
|
_non_negative_quantity,
|
||||||
|
)
|
||||||
|
unknown_sellable = sorted(set(canonical_sellable) - set(canonical_current))
|
||||||
|
if unknown_sellable:
|
||||||
|
raise ValueError(
|
||||||
|
"sellable contains symbols absent from current: %s"
|
||||||
|
% unknown_sellable
|
||||||
|
)
|
||||||
|
for symbol, quantity in canonical_sellable.items():
|
||||||
|
if quantity > canonical_current[symbol]:
|
||||||
|
raise ValueError(
|
||||||
|
"sellable exceeds current quantity for %s" % symbol
|
||||||
|
)
|
||||||
|
|
||||||
|
canonical_prices = _canonical_mapping(
|
||||||
|
prices,
|
||||||
|
"prices",
|
||||||
|
_positive_price,
|
||||||
|
)
|
||||||
|
managed_symbols = set(weights) | set(canonical_current)
|
||||||
|
price_symbols = set(canonical_prices)
|
||||||
|
missing_prices = sorted(managed_symbols - price_symbols)
|
||||||
|
extra_prices = sorted(price_symbols - managed_symbols)
|
||||||
|
if missing_prices or extra_prices:
|
||||||
|
if missing_prices:
|
||||||
|
raise KeyError(
|
||||||
|
"prices do not cover managed symbols: missing=%s"
|
||||||
|
% missing_prices
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
"prices contain unmanaged symbols: extra=%s" % extra_prices
|
||||||
|
)
|
||||||
|
|
||||||
|
complete_weights = dict(
|
||||||
|
(symbol, weights.get(symbol, 0.0))
|
||||||
|
for symbol in sorted(managed_symbols)
|
||||||
|
)
|
||||||
|
targets = target_quantities(
|
||||||
|
complete_weights,
|
||||||
|
canonical_prices,
|
||||||
|
equity,
|
||||||
|
lot_size,
|
||||||
|
cash_buffer,
|
||||||
|
)
|
||||||
|
orders = order_deltas(
|
||||||
|
targets,
|
||||||
|
canonical_current,
|
||||||
|
canonical_sellable,
|
||||||
|
lot_size,
|
||||||
|
)
|
||||||
|
decision_id = _optional_non_empty_string(decision_id, "decision_id")
|
||||||
|
package_sha256 = _sha256_hex(
|
||||||
|
package_sha256,
|
||||||
|
"package_sha256",
|
||||||
|
allow_none=True,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"decision_id": decision_id,
|
||||||
|
"package_sha256": package_sha256,
|
||||||
|
"weights": complete_weights,
|
||||||
|
"targets": targets,
|
||||||
|
"orders": orders,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_rebalance_plan(
|
def build_rebalance_plan(
|
||||||
price_history,
|
price_history,
|
||||||
current,
|
current,
|
||||||
@@ -363,12 +905,17 @@ def build_rebalance_plan(
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"build_rebalance_plan",
|
"build_rebalance_plan",
|
||||||
|
"build_target_weight_plan",
|
||||||
"capped_target_weights",
|
"capped_target_weights",
|
||||||
"convert_symbol",
|
"convert_symbol",
|
||||||
|
"lookup_target_package_exact",
|
||||||
"momentum_score",
|
"momentum_score",
|
||||||
"normalize_symbol",
|
"normalize_symbol",
|
||||||
"order_deltas",
|
"order_deltas",
|
||||||
"rank_momentum",
|
"rank_momentum",
|
||||||
"round_board_lot",
|
"round_board_lot",
|
||||||
|
"TargetPackageLookupMiss",
|
||||||
|
"target_package_tape_sha256",
|
||||||
"target_quantities",
|
"target_quantities",
|
||||||
|
"verify_target_package",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,619 @@
|
|||||||
|
"""Tamper-evident, per-layer deployment reachability evidence.
|
||||||
|
|
||||||
|
This contract deliberately separates six different claims for every Quant60
|
||||||
|
layer. A source file can prove that code is implemented; it cannot, by
|
||||||
|
itself, prove that the code is tested, reachable from an entrypoint, or
|
||||||
|
observed on a real trading platform.
|
||||||
|
|
||||||
|
All evidence is repository-relative, content-addressed, and checked against
|
||||||
|
the filesystem. The manifest also carries a self-hash so edits to statuses or
|
||||||
|
evidence fail closed even when every referenced artifact is unchanged.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from .contracts import ContractValidationError, load_schema, validate_contract
|
||||||
|
from .ledger import canonical_json
|
||||||
|
|
||||||
|
|
||||||
|
SCHEMA_VERSION = "1.0"
|
||||||
|
ARTIFACT_TYPE = "quant60-release-reachability"
|
||||||
|
LAYERS = ("universe", "alpha", "portfolio", "risk", "execution")
|
||||||
|
CAPABILITIES = (
|
||||||
|
"implemented",
|
||||||
|
"unit_tested",
|
||||||
|
"local_entrypoint_reachable",
|
||||||
|
"jq_entrypoint_reachable",
|
||||||
|
"qmt_entrypoint_reachable",
|
||||||
|
"real_platform_observed",
|
||||||
|
)
|
||||||
|
CAPABILITY_EVIDENCE = {
|
||||||
|
"implemented": ("source", "repository"),
|
||||||
|
"unit_tested": ("test", "repository"),
|
||||||
|
"local_entrypoint_reachable": ("entrypoint", "local"),
|
||||||
|
"jq_entrypoint_reachable": ("entrypoint", "joinquant"),
|
||||||
|
"qmt_entrypoint_reachable": ("entrypoint", "qmt"),
|
||||||
|
}
|
||||||
|
RUN_EVIDENCE_ROOTS = frozenset({"artifacts", "evidence", "runs"})
|
||||||
|
ENTRYPOINT_ROOTS = frozenset({"platforms", "src", "tools"})
|
||||||
|
IDENTIFIER_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
||||||
|
SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseReachabilityError(ValueError):
|
||||||
|
"""A reachability manifest is malformed or its evidence cannot be trusted."""
|
||||||
|
|
||||||
|
|
||||||
|
def _project_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def _json_copy(value: Any) -> Any:
|
||||||
|
try:
|
||||||
|
return json.loads(canonical_json(value))
|
||||||
|
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"reachability manifest must contain finite JSON values only"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _strict_json(path: Path) -> Any:
|
||||||
|
def reject_constant(value: str) -> None:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"non-finite JSON constant in reachability manifest: {value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def reject_duplicate_keys(
|
||||||
|
pairs: list[tuple[str, Any]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
output: dict[str, Any] = {}
|
||||||
|
for key, value in pairs:
|
||||||
|
if key in output:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"duplicate JSON key in reachability manifest: {key}"
|
||||||
|
)
|
||||||
|
output[key] = value
|
||||||
|
return output
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(
|
||||||
|
path.read_text(encoding="utf-8"),
|
||||||
|
parse_constant=reject_constant,
|
||||||
|
object_pairs_hook=reject_duplicate_keys,
|
||||||
|
)
|
||||||
|
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"reachability manifest is not valid UTF-8 JSON: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _manifest_body(payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
key: value
|
||||||
|
for key, value in payload.items()
|
||||||
|
if key != "manifest_sha256"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def release_reachability_sha256(payload: Mapping[str, Any]) -> str:
|
||||||
|
"""Return the canonical self-hash, excluding ``manifest_sha256``."""
|
||||||
|
|
||||||
|
return hashlib.sha256(
|
||||||
|
canonical_json(_manifest_body(payload)).encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_utc(value: Any, field: str) -> datetime:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise ReleaseReachabilityError(f"{field} must be an ISO-8601 string")
|
||||||
|
try:
|
||||||
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field} must be a valid ISO-8601 timestamp"
|
||||||
|
) from exc
|
||||||
|
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
||||||
|
raise ReleaseReachabilityError(f"{field} must include a timezone")
|
||||||
|
normalized = (
|
||||||
|
parsed.astimezone(timezone.utc)
|
||||||
|
.replace(microsecond=0)
|
||||||
|
.isoformat()
|
||||||
|
.replace("+00:00", "Z")
|
||||||
|
)
|
||||||
|
if value != normalized:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field} must use canonical UTC YYYY-MM-DDTHH:MM:SSZ"
|
||||||
|
)
|
||||||
|
return parsed.astimezone(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def _identifier(value: Any, field: str) -> str:
|
||||||
|
if not isinstance(value, str) or IDENTIFIER_PATTERN.fullmatch(value) is None:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field} must be a non-empty canonical identifier"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(value: Any, field: str) -> str:
|
||||||
|
if not isinstance(value, str) or SHA256_PATTERN.fullmatch(value) is None:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field} must be a lowercase SHA-256 digest"
|
||||||
|
)
|
||||||
|
if value == "0" * 64:
|
||||||
|
raise ReleaseReachabilityError(f"{field} cannot be an all-zero placeholder")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _repository_relative_path(value: Any, field: str) -> str:
|
||||||
|
if not isinstance(value, str) or not value:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field} must be a non-empty repository-relative path"
|
||||||
|
)
|
||||||
|
if "\\" in value:
|
||||||
|
raise ReleaseReachabilityError(f"{field} must use POSIX separators")
|
||||||
|
candidate = PurePosixPath(value)
|
||||||
|
if (
|
||||||
|
candidate.is_absolute()
|
||||||
|
or value != candidate.as_posix()
|
||||||
|
or any(part in {"", ".", ".."} for part in candidate.parts)
|
||||||
|
):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field} must be a normalized repository-relative path"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact_file(
|
||||||
|
repository_root: Path,
|
||||||
|
relative_path: str,
|
||||||
|
*,
|
||||||
|
field: str,
|
||||||
|
) -> Path:
|
||||||
|
try:
|
||||||
|
root = repository_root.resolve(strict=True)
|
||||||
|
except OSError as exc:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"repository_root must be an existing directory"
|
||||||
|
) from exc
|
||||||
|
if not root.is_dir():
|
||||||
|
raise ReleaseReachabilityError("repository_root must be a directory")
|
||||||
|
lexical = root.joinpath(*PurePosixPath(relative_path).parts)
|
||||||
|
if lexical.is_symlink():
|
||||||
|
raise ReleaseReachabilityError(f"{field} cannot reference a symlink")
|
||||||
|
try:
|
||||||
|
resolved = lexical.resolve(strict=True)
|
||||||
|
except OSError as exc:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field} does not reference an existing artifact: {relative_path}"
|
||||||
|
) from exc
|
||||||
|
if not resolved.is_relative_to(root) or not resolved.is_file():
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field} must reference a regular file inside repository_root"
|
||||||
|
)
|
||||||
|
if resolved.stat().st_size <= 0:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field} cannot reference an empty artifact"
|
||||||
|
)
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_artifact_path_class(kind: str, path: str, field: str) -> None:
|
||||||
|
root = PurePosixPath(path).parts[0]
|
||||||
|
if kind == "source" and root != "src":
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field}: source evidence must live under src/"
|
||||||
|
)
|
||||||
|
if kind == "test" and root != "tests":
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field}: test evidence must live under tests/"
|
||||||
|
)
|
||||||
|
if kind == "entrypoint" and root not in ENTRYPOINT_ROOTS:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field}: entrypoint evidence must live under "
|
||||||
|
"platforms/, src/, or tools/"
|
||||||
|
)
|
||||||
|
if kind == "platform_run" and root not in RUN_EVIDENCE_ROOTS:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field}: real-platform evidence must live under "
|
||||||
|
"artifacts/, evidence/, or runs/"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_evidence_artifact(
|
||||||
|
*,
|
||||||
|
artifact_id: str,
|
||||||
|
kind: str,
|
||||||
|
path: str,
|
||||||
|
platform: str,
|
||||||
|
repository_root: str | Path | None = None,
|
||||||
|
observed_at: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Content-address one evidence file for use in a reachability claim."""
|
||||||
|
|
||||||
|
root = _project_root() if repository_root is None else Path(repository_root)
|
||||||
|
normalized_path = _repository_relative_path(path, "artifact.path")
|
||||||
|
if kind not in {"source", "test", "entrypoint", "platform_run"}:
|
||||||
|
raise ReleaseReachabilityError("artifact.kind is not supported")
|
||||||
|
if platform not in {"repository", "local", "joinquant", "qmt"}:
|
||||||
|
raise ReleaseReachabilityError("artifact.platform is not supported")
|
||||||
|
_validate_artifact_path_class(kind, normalized_path, "artifact.path")
|
||||||
|
if kind == "platform_run":
|
||||||
|
if platform not in {"joinquant", "qmt"}:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"platform_run evidence requires joinquant or qmt"
|
||||||
|
)
|
||||||
|
if observed_at is None or run_id is None:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"platform_run evidence requires observed_at and run_id"
|
||||||
|
)
|
||||||
|
_canonical_utc(observed_at, "artifact.observed_at")
|
||||||
|
_identifier(run_id, "artifact.run_id")
|
||||||
|
elif observed_at is not None or run_id is not None:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"only platform_run evidence may carry observed_at and run_id"
|
||||||
|
)
|
||||||
|
resolved = _artifact_file(root, normalized_path, field="artifact.path")
|
||||||
|
artifact: dict[str, Any] = {
|
||||||
|
"artifact_id": _identifier(artifact_id, "artifact.artifact_id"),
|
||||||
|
"kind": kind,
|
||||||
|
"path": normalized_path,
|
||||||
|
"sha256": hashlib.sha256(resolved.read_bytes()).hexdigest(),
|
||||||
|
"platform": platform,
|
||||||
|
}
|
||||||
|
if observed_at is not None:
|
||||||
|
artifact["observed_at"] = observed_at
|
||||||
|
if run_id is not None:
|
||||||
|
artifact["run_id"] = run_id
|
||||||
|
return artifact
|
||||||
|
|
||||||
|
|
||||||
|
def _expected_summary(layers: Mapping[str, Any]) -> dict[str, bool]:
|
||||||
|
if set(layers) != set(LAYERS):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers must be exactly {list(LAYERS)}"
|
||||||
|
)
|
||||||
|
for layer in LAYERS:
|
||||||
|
claims = layers[layer]
|
||||||
|
if not isinstance(claims, Mapping) or set(claims) != set(CAPABILITIES):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers.{layer} must contain exactly {list(CAPABILITIES)}"
|
||||||
|
)
|
||||||
|
for capability in CAPABILITIES:
|
||||||
|
claim = claims[capability]
|
||||||
|
if (
|
||||||
|
not isinstance(claim, Mapping)
|
||||||
|
or set(claim) != {"status", "artifacts"}
|
||||||
|
or type(claim.get("status")) is not bool
|
||||||
|
or not isinstance(claim.get("artifacts"), list)
|
||||||
|
):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers.{layer}.{capability} must contain a boolean "
|
||||||
|
"status and an artifacts array"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
f"all_layers_{capability}": all(
|
||||||
|
bool(layers[layer][capability]["status"])
|
||||||
|
for layer in LAYERS
|
||||||
|
)
|
||||||
|
for capability in CAPABILITIES
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_artifact(
|
||||||
|
artifact: Any,
|
||||||
|
*,
|
||||||
|
layer: str,
|
||||||
|
capability: str,
|
||||||
|
repository_root: Path,
|
||||||
|
generated_at: datetime,
|
||||||
|
seen_artifact_ids: set[str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
field = f"layers.{layer}.{capability}.artifacts"
|
||||||
|
if not isinstance(artifact, Mapping):
|
||||||
|
raise ReleaseReachabilityError(f"{field} entries must be objects")
|
||||||
|
normalized = _json_copy(dict(artifact))
|
||||||
|
base_fields = {"artifact_id", "kind", "path", "sha256", "platform"}
|
||||||
|
run_fields = base_fields | {"observed_at", "run_id"}
|
||||||
|
expected_fields = (
|
||||||
|
run_fields
|
||||||
|
if normalized.get("kind") == "platform_run"
|
||||||
|
else base_fields
|
||||||
|
)
|
||||||
|
if set(normalized) != expected_fields:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field} entry fields must be exactly {sorted(expected_fields)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
artifact_id = _identifier(
|
||||||
|
normalized["artifact_id"],
|
||||||
|
f"{field}.artifact_id",
|
||||||
|
)
|
||||||
|
if artifact_id in seen_artifact_ids:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"artifact_id {artifact_id!r} is reused across claims"
|
||||||
|
)
|
||||||
|
seen_artifact_ids.add(artifact_id)
|
||||||
|
|
||||||
|
kind = normalized["kind"]
|
||||||
|
platform = normalized["platform"]
|
||||||
|
if capability == "real_platform_observed":
|
||||||
|
if kind != "platform_run" or platform not in {"joinquant", "qmt"}:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field}: real_platform_observed requires a "
|
||||||
|
"joinquant/qmt platform_run artifact"
|
||||||
|
)
|
||||||
|
observed_at = _canonical_utc(
|
||||||
|
normalized["observed_at"],
|
||||||
|
f"{field}.observed_at",
|
||||||
|
)
|
||||||
|
if observed_at > generated_at:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field}.observed_at cannot be after generated_at"
|
||||||
|
)
|
||||||
|
_identifier(normalized["run_id"], f"{field}.run_id")
|
||||||
|
else:
|
||||||
|
expected_kind, expected_platform = CAPABILITY_EVIDENCE[capability]
|
||||||
|
if (kind, platform) != (expected_kind, expected_platform):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field}: {capability} requires kind={expected_kind!r} "
|
||||||
|
f"and platform={expected_platform!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
path = _repository_relative_path(normalized["path"], f"{field}.path")
|
||||||
|
_validate_artifact_path_class(kind, path, field)
|
||||||
|
expected_hash = _sha256(normalized["sha256"], f"{field}.sha256")
|
||||||
|
actual_hash = hashlib.sha256(
|
||||||
|
_artifact_file(
|
||||||
|
repository_root,
|
||||||
|
path,
|
||||||
|
field=f"{field}.path",
|
||||||
|
).read_bytes()
|
||||||
|
).hexdigest()
|
||||||
|
if actual_hash != expected_hash:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"{field}: artifact hash mismatch for {path}; "
|
||||||
|
f"expected {expected_hash}, got {actual_hash}"
|
||||||
|
)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def validate_release_reachability(
|
||||||
|
payload: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
repository_root: str | Path | None = None,
|
||||||
|
expected_release_id: str | None = None,
|
||||||
|
expected_manifest_sha256: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Validate shape, status semantics, hashes, and every evidence file."""
|
||||||
|
|
||||||
|
if not isinstance(payload, Mapping):
|
||||||
|
raise ReleaseReachabilityError("reachability manifest must be an object")
|
||||||
|
normalized = _json_copy(dict(payload))
|
||||||
|
try:
|
||||||
|
validate_contract(
|
||||||
|
normalized,
|
||||||
|
load_schema("release_reachability.schema.json"),
|
||||||
|
)
|
||||||
|
except (ContractValidationError, FileNotFoundError, ValueError) as exc:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"reachability schema validation failed: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
release_id = _identifier(normalized["release_id"], "release_id")
|
||||||
|
if expected_release_id is not None and release_id != expected_release_id:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"release_id does not match expected identity"
|
||||||
|
)
|
||||||
|
generated_at = _canonical_utc(normalized["generated_at"], "generated_at")
|
||||||
|
supplied_hash = _sha256(
|
||||||
|
normalized["manifest_sha256"],
|
||||||
|
"manifest_sha256",
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
expected_manifest_sha256 is not None
|
||||||
|
and supplied_hash != _sha256(
|
||||||
|
expected_manifest_sha256,
|
||||||
|
"expected_manifest_sha256",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"manifest_sha256 does not match expected identity"
|
||||||
|
)
|
||||||
|
actual_manifest_hash = release_reachability_sha256(normalized)
|
||||||
|
if supplied_hash != actual_manifest_hash:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"reachability manifest SHA-256 mismatch"
|
||||||
|
)
|
||||||
|
|
||||||
|
layers = normalized["layers"]
|
||||||
|
if set(layers) != set(LAYERS):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers must be exactly {list(LAYERS)}"
|
||||||
|
)
|
||||||
|
root = _project_root() if repository_root is None else Path(repository_root)
|
||||||
|
seen_artifact_ids: set[str] = set()
|
||||||
|
for layer in LAYERS:
|
||||||
|
claims = layers[layer]
|
||||||
|
if set(claims) != set(CAPABILITIES):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers.{layer} must contain exactly {list(CAPABILITIES)}"
|
||||||
|
)
|
||||||
|
for capability in CAPABILITIES:
|
||||||
|
claim = claims[capability]
|
||||||
|
if not isinstance(claim, Mapping) or set(claim) != {
|
||||||
|
"status",
|
||||||
|
"artifacts",
|
||||||
|
}:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers.{layer}.{capability} must contain only "
|
||||||
|
"status and artifacts"
|
||||||
|
)
|
||||||
|
status = claim["status"]
|
||||||
|
artifacts = claim["artifacts"]
|
||||||
|
if type(status) is not bool or not isinstance(artifacts, list):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers.{layer}.{capability} has invalid status/artifacts"
|
||||||
|
)
|
||||||
|
if status != bool(artifacts):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers.{layer}.{capability}: status must be true "
|
||||||
|
"iff at least one evidence artifact exists"
|
||||||
|
)
|
||||||
|
for artifact in artifacts:
|
||||||
|
_validate_artifact(
|
||||||
|
artifact,
|
||||||
|
layer=layer,
|
||||||
|
capability=capability,
|
||||||
|
repository_root=root,
|
||||||
|
generated_at=generated_at,
|
||||||
|
seen_artifact_ids=seen_artifact_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
if claims["unit_tested"]["status"] and not claims["implemented"]["status"]:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers.{layer}: unit_tested requires implemented"
|
||||||
|
)
|
||||||
|
for capability in (
|
||||||
|
"local_entrypoint_reachable",
|
||||||
|
"jq_entrypoint_reachable",
|
||||||
|
"qmt_entrypoint_reachable",
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
claims[capability]["status"]
|
||||||
|
and not claims["implemented"]["status"]
|
||||||
|
):
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers.{layer}: {capability} requires implemented"
|
||||||
|
)
|
||||||
|
for artifact in claims["real_platform_observed"]["artifacts"]:
|
||||||
|
platform_capability = {
|
||||||
|
"joinquant": "jq_entrypoint_reachable",
|
||||||
|
"qmt": "qmt_entrypoint_reachable",
|
||||||
|
}[artifact["platform"]]
|
||||||
|
if not claims[platform_capability]["status"]:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
f"layers.{layer}: real {artifact['platform']} observation "
|
||||||
|
f"requires {platform_capability}"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_summary = _expected_summary(layers)
|
||||||
|
if normalized["summary"] != expected_summary:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"summary is not the exact derived all-layer status"
|
||||||
|
)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def seal_release_reachability(
|
||||||
|
body: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
repository_root: str | Path | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Add derived summary/self-hash and perform full fail-closed validation."""
|
||||||
|
|
||||||
|
if not isinstance(body, Mapping):
|
||||||
|
raise ReleaseReachabilityError("reachability body must be an object")
|
||||||
|
normalized = _json_copy(dict(body))
|
||||||
|
if "manifest_sha256" in normalized:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"unsealed reachability body cannot contain manifest_sha256"
|
||||||
|
)
|
||||||
|
if "summary" in normalized:
|
||||||
|
raise ReleaseReachabilityError(
|
||||||
|
"unsealed reachability body cannot contain derived summary"
|
||||||
|
)
|
||||||
|
layers = normalized.get("layers")
|
||||||
|
if not isinstance(layers, Mapping):
|
||||||
|
raise ReleaseReachabilityError("layers must be an object")
|
||||||
|
normalized["summary"] = _expected_summary(layers)
|
||||||
|
normalized["manifest_sha256"] = release_reachability_sha256(normalized)
|
||||||
|
return validate_release_reachability(
|
||||||
|
normalized,
|
||||||
|
repository_root=repository_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_release_reachability(
|
||||||
|
*,
|
||||||
|
release_id: str,
|
||||||
|
generated_at: str,
|
||||||
|
layers: Mapping[str, Any],
|
||||||
|
repository_root: str | Path | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Construct and seal one complete five-layer reachability manifest."""
|
||||||
|
|
||||||
|
return seal_release_reachability(
|
||||||
|
{
|
||||||
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"artifact_type": ARTIFACT_TYPE,
|
||||||
|
"release_id": release_id,
|
||||||
|
"generated_at": generated_at,
|
||||||
|
"layers": copy.deepcopy(dict(layers)),
|
||||||
|
},
|
||||||
|
repository_root=repository_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_release_reachability(
|
||||||
|
payload: Mapping[str, Any],
|
||||||
|
path: str | Path,
|
||||||
|
*,
|
||||||
|
repository_root: str | Path | None = None,
|
||||||
|
) -> Path:
|
||||||
|
"""Atomically write a fully verified canonical manifest."""
|
||||||
|
|
||||||
|
verified = validate_release_reachability(
|
||||||
|
payload,
|
||||||
|
repository_root=repository_root,
|
||||||
|
)
|
||||||
|
destination = Path(path)
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
descriptor, temporary_name = tempfile.mkstemp(
|
||||||
|
prefix=f".{destination.name}.",
|
||||||
|
suffix=".tmp",
|
||||||
|
dir=str(destination.parent),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write(canonical_json(verified))
|
||||||
|
handle.write("\n")
|
||||||
|
handle.flush()
|
||||||
|
os.fsync(handle.fileno())
|
||||||
|
os.replace(temporary_name, destination)
|
||||||
|
except BaseException:
|
||||||
|
try:
|
||||||
|
os.unlink(temporary_name)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
return destination
|
||||||
|
|
||||||
|
|
||||||
|
def load_verified_release_reachability(
|
||||||
|
path: str | Path,
|
||||||
|
*,
|
||||||
|
repository_root: str | Path | None = None,
|
||||||
|
expected_release_id: str | None = None,
|
||||||
|
expected_manifest_sha256: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Strictly load a manifest and re-check both it and all artifacts."""
|
||||||
|
|
||||||
|
raw = _strict_json(Path(path))
|
||||||
|
return validate_release_reachability(
|
||||||
|
raw,
|
||||||
|
repository_root=repository_root,
|
||||||
|
expected_release_id=expected_release_id,
|
||||||
|
expected_manifest_sha256=expected_manifest_sha256,
|
||||||
|
)
|
||||||
@@ -0,0 +1,667 @@
|
|||||||
|
"""Canonical, quantity-free target-package contract.
|
||||||
|
|
||||||
|
``TargetPackageV1`` is the boundary between research/portfolio construction
|
||||||
|
and a platform execution adapter. It deliberately contains post-risk target
|
||||||
|
*weights*, but never share quantities, orders, fills, cash or account state.
|
||||||
|
Those values belong to a later, account-bound execution plan.
|
||||||
|
|
||||||
|
The validator is intentionally fail closed. JSON Schema provides the wire
|
||||||
|
shape while this module enforces cross-field invariants that JSON Schema
|
||||||
|
cannot express compactly: the China-close decision clock, canonical symbols,
|
||||||
|
gross exposure, content hashing and unambiguous tape lookup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import date, datetime, time, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable, Iterator, Mapping
|
||||||
|
|
||||||
|
from .contracts import ContractValidationError, load_schema, validate_contract
|
||||||
|
from .ledger import canonical_json
|
||||||
|
from .portable_core import normalize_symbol
|
||||||
|
|
||||||
|
|
||||||
|
PACKAGE_TYPE = "TargetPackageV1"
|
||||||
|
SCHEMA_VERSION = "1.0"
|
||||||
|
SHANGHAI_OFFSET = timedelta(hours=8)
|
||||||
|
SHA256_FIELDS = (
|
||||||
|
"model_sha256",
|
||||||
|
"data_sha256",
|
||||||
|
"feature_sha256",
|
||||||
|
"risk_sha256",
|
||||||
|
"cost_sha256",
|
||||||
|
"optimizer_sha256",
|
||||||
|
"config_sha256",
|
||||||
|
"source_sha256",
|
||||||
|
)
|
||||||
|
UNIVERSE_STATES = {
|
||||||
|
"openable",
|
||||||
|
"hold_only",
|
||||||
|
"sell_only",
|
||||||
|
"frozen",
|
||||||
|
"excluded",
|
||||||
|
}
|
||||||
|
WEIGHT_TOLERANCE = 1e-12
|
||||||
|
|
||||||
|
|
||||||
|
class TargetPackageError(ValueError):
|
||||||
|
"""A target package is incomplete, ambiguous or internally inconsistent."""
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateTargetDecisionError(TargetPackageError):
|
||||||
|
"""A tape contains more than one representation of a decision identity."""
|
||||||
|
|
||||||
|
|
||||||
|
class TargetPackageNotFoundError(TargetPackageError, LookupError):
|
||||||
|
"""An exact signal/session pair is absent from a target-package tape."""
|
||||||
|
|
||||||
|
|
||||||
|
def _json_copy(value: Any) -> Any:
|
||||||
|
"""Return a detached JSON value and reject non-JSON/non-finite input."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(canonical_json(value))
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise TargetPackageError(
|
||||||
|
"target package must contain finite JSON values only"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _strict_json_text(value: str, *, source: str) -> Any:
|
||||||
|
def reject_constant(constant: str) -> None:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"{source} contains a non-finite JSON constant: {constant}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def reject_duplicate_keys(
|
||||||
|
pairs: list[tuple[str, Any]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
output: dict[str, Any] = {}
|
||||||
|
for key, child in pairs:
|
||||||
|
if key in output:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"{source} contains a duplicate JSON key: {key}"
|
||||||
|
)
|
||||||
|
output[key] = child
|
||||||
|
return output
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(
|
||||||
|
value,
|
||||||
|
parse_constant=reject_constant,
|
||||||
|
object_pairs_hook=reject_duplicate_keys,
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"{source} is not valid JSON: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _reject_quantity_fields(value: Any, path: str = "$") -> None:
|
||||||
|
if isinstance(value, Mapping):
|
||||||
|
for raw_key, child in value.items():
|
||||||
|
key = str(raw_key)
|
||||||
|
normalized = key.lower().replace("-", "_")
|
||||||
|
if "quantity" in normalized or "quantities" in normalized:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"{path}.{key}: quantities do not belong in TargetPackageV1"
|
||||||
|
)
|
||||||
|
_reject_quantity_fields(child, f"{path}.{key}")
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for index, child in enumerate(value):
|
||||||
|
_reject_quantity_fields(child, f"{path}[{index}]")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_signal_as_of(value: Any) -> datetime:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TargetPackageError("signal_as_of must be an ISO-8601 string")
|
||||||
|
try:
|
||||||
|
parsed = datetime.fromisoformat(value)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise TargetPackageError(
|
||||||
|
"signal_as_of must be a valid ISO-8601 timestamp"
|
||||||
|
) from exc
|
||||||
|
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
||||||
|
raise TargetPackageError("signal_as_of must include a timezone offset")
|
||||||
|
if parsed.utcoffset() != SHANGHAI_OFFSET:
|
||||||
|
raise TargetPackageError(
|
||||||
|
"signal_as_of must use the Asia/Shanghai +08:00 offset"
|
||||||
|
)
|
||||||
|
if parsed.timetz().replace(tzinfo=None) != time(15, 0):
|
||||||
|
raise TargetPackageError(
|
||||||
|
"signal_as_of must be the completed 15:00 China-market close"
|
||||||
|
)
|
||||||
|
canonical = (
|
||||||
|
f"{parsed.date().isoformat()}T15:00:00+08:00"
|
||||||
|
)
|
||||||
|
if value != canonical:
|
||||||
|
raise TargetPackageError(
|
||||||
|
"signal_as_of must use canonical "
|
||||||
|
"YYYY-MM-DDT15:00:00+08:00 serialization"
|
||||||
|
)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_next_session(value: Any, signal_as_of: datetime) -> date:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TargetPackageError("next_session must be an ISO date string")
|
||||||
|
try:
|
||||||
|
parsed = date.fromisoformat(value)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise TargetPackageError("next_session must be a valid ISO date") from exc
|
||||||
|
if value != parsed.isoformat():
|
||||||
|
raise TargetPackageError("next_session must use canonical YYYY-MM-DD")
|
||||||
|
if parsed <= signal_as_of.date():
|
||||||
|
raise TargetPackageError("next_session must be after the signal session")
|
||||||
|
if parsed.weekday() >= 5:
|
||||||
|
raise TargetPackageError("next_session cannot be a weekend")
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_sha256(value: Any, name: str) -> str:
|
||||||
|
if (
|
||||||
|
not isinstance(value, str)
|
||||||
|
or len(value) != 64
|
||||||
|
or any(character not in "0123456789abcdef" for character in value)
|
||||||
|
):
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"{name} must be a 64-character lowercase SHA-256 hex digest"
|
||||||
|
)
|
||||||
|
if value == "0" * 64:
|
||||||
|
raise TargetPackageError(f"{name} cannot be an all-zero placeholder")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_symbol(value: Any, name: str) -> str:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TargetPackageError(f"{name} must be a canonical symbol string")
|
||||||
|
try:
|
||||||
|
canonical = normalize_symbol(value)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise TargetPackageError(f"{name} is not a supported A-share symbol") from exc
|
||||||
|
if value != canonical:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"{name} must be canonical; expected {canonical!r}"
|
||||||
|
)
|
||||||
|
return canonical
|
||||||
|
|
||||||
|
|
||||||
|
def _finite_weight(value: Any, name: str) -> float:
|
||||||
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||||
|
raise TargetPackageError(f"{name} must be a finite JSON number")
|
||||||
|
numeric = float(value)
|
||||||
|
if not math.isfinite(numeric):
|
||||||
|
raise TargetPackageError(f"{name} must be finite")
|
||||||
|
if numeric < 0 or numeric > 1:
|
||||||
|
raise TargetPackageError(f"{name} must lie in [0, 1]")
|
||||||
|
return numeric
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_weights(value: Any) -> dict[str, float]:
|
||||||
|
if not isinstance(value, Mapping) or not value:
|
||||||
|
raise TargetPackageError("target_weights must be a non-empty object")
|
||||||
|
output: dict[str, float] = {}
|
||||||
|
for raw_symbol, raw_weight in value.items():
|
||||||
|
symbol = _canonical_symbol(raw_symbol, "target_weights symbol")
|
||||||
|
if symbol in output:
|
||||||
|
raise TargetPackageError(f"duplicate target weight for {symbol}")
|
||||||
|
output[symbol] = _finite_weight(
|
||||||
|
raw_weight,
|
||||||
|
f"target_weights.{symbol}",
|
||||||
|
)
|
||||||
|
if list(value) != sorted(value):
|
||||||
|
raise TargetPackageError(
|
||||||
|
"target_weights keys must be in canonical symbol order"
|
||||||
|
)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_reasons(value: Any, symbol: str) -> tuple[str, ...]:
|
||||||
|
if not isinstance(value, list) or not value:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"universe.{symbol}.reasons must be a non-empty array"
|
||||||
|
)
|
||||||
|
reasons: list[str] = []
|
||||||
|
for reason in value:
|
||||||
|
if (
|
||||||
|
not isinstance(reason, str)
|
||||||
|
or not reason
|
||||||
|
or any(
|
||||||
|
character not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
|
||||||
|
for character in reason
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"universe.{symbol}.reasons must contain uppercase reason codes"
|
||||||
|
)
|
||||||
|
reasons.append(reason)
|
||||||
|
if reasons != sorted(set(reasons)):
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"universe.{symbol}.reasons must be unique and sorted"
|
||||||
|
)
|
||||||
|
return tuple(reasons)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_universe(value: Any) -> dict[str, dict[str, Any]]:
|
||||||
|
if not isinstance(value, Mapping) or not value:
|
||||||
|
raise TargetPackageError("universe must be a non-empty object")
|
||||||
|
output: dict[str, dict[str, Any]] = {}
|
||||||
|
for raw_symbol, raw_state in value.items():
|
||||||
|
symbol = _canonical_symbol(raw_symbol, "universe symbol")
|
||||||
|
if symbol in output:
|
||||||
|
raise TargetPackageError(f"duplicate universe symbol {symbol}")
|
||||||
|
if not isinstance(raw_state, Mapping):
|
||||||
|
raise TargetPackageError(f"universe.{symbol} must be an object")
|
||||||
|
if set(raw_state) != {"state", "reasons"}:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"universe.{symbol} must contain only state and reasons"
|
||||||
|
)
|
||||||
|
state = raw_state["state"]
|
||||||
|
if state not in UNIVERSE_STATES:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"universe.{symbol}.state is not a canonical universe state"
|
||||||
|
)
|
||||||
|
reasons = _validate_reasons(raw_state["reasons"], symbol)
|
||||||
|
if state == "openable" and reasons != ("ELIGIBLE",):
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"openable universe member {symbol} must have reason ELIGIBLE"
|
||||||
|
)
|
||||||
|
if state != "openable" and "ELIGIBLE" in reasons:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"non-openable universe member {symbol} cannot be ELIGIBLE"
|
||||||
|
)
|
||||||
|
output[symbol] = {"state": state, "reasons": list(reasons)}
|
||||||
|
if list(value) != sorted(value):
|
||||||
|
raise TargetPackageError("universe keys must be in canonical symbol order")
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_constraints(
|
||||||
|
value: Any,
|
||||||
|
weights: Mapping[str, float],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
required = {
|
||||||
|
"constraint_state",
|
||||||
|
"long_only",
|
||||||
|
"max_single_weight",
|
||||||
|
"max_gross_weight",
|
||||||
|
"max_one_way_turnover",
|
||||||
|
"actual_gross_weight",
|
||||||
|
}
|
||||||
|
if not isinstance(value, Mapping) or set(value) != required:
|
||||||
|
missing = sorted(required - set(value or {})) if isinstance(value, Mapping) else sorted(required)
|
||||||
|
unknown = sorted(set(value or {}) - required) if isinstance(value, Mapping) else []
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"constraints fields mismatch; missing={missing}, unknown={unknown}"
|
||||||
|
)
|
||||||
|
if value["constraint_state"] not in {
|
||||||
|
"FEASIBLE",
|
||||||
|
"REPAIRED",
|
||||||
|
"RISK_REDUCTION_ONLY",
|
||||||
|
"NO_TRADE",
|
||||||
|
}:
|
||||||
|
raise TargetPackageError("constraints.constraint_state is unsupported")
|
||||||
|
if value["long_only"] is not True:
|
||||||
|
raise TargetPackageError("TargetPackageV1 is strictly long-only")
|
||||||
|
max_single = _finite_weight(
|
||||||
|
value["max_single_weight"],
|
||||||
|
"constraints.max_single_weight",
|
||||||
|
)
|
||||||
|
if max_single <= 0:
|
||||||
|
raise TargetPackageError(
|
||||||
|
"constraints.max_single_weight must be positive"
|
||||||
|
)
|
||||||
|
max_gross = _finite_weight(
|
||||||
|
value["max_gross_weight"],
|
||||||
|
"constraints.max_gross_weight",
|
||||||
|
)
|
||||||
|
max_turnover = _finite_weight(
|
||||||
|
value["max_one_way_turnover"],
|
||||||
|
"constraints.max_one_way_turnover",
|
||||||
|
)
|
||||||
|
declared_gross = _finite_weight(
|
||||||
|
value["actual_gross_weight"],
|
||||||
|
"constraints.actual_gross_weight",
|
||||||
|
)
|
||||||
|
actual_gross = math.fsum(weights.values())
|
||||||
|
if abs(declared_gross - actual_gross) > WEIGHT_TOLERANCE:
|
||||||
|
raise TargetPackageError(
|
||||||
|
"constraints.actual_gross_weight does not equal target_weights gross"
|
||||||
|
)
|
||||||
|
if actual_gross > max_gross + WEIGHT_TOLERANCE:
|
||||||
|
raise TargetPackageError("target_weights exceed max_gross_weight")
|
||||||
|
breaches = [
|
||||||
|
symbol
|
||||||
|
for symbol, weight in weights.items()
|
||||||
|
if weight > max_single + WEIGHT_TOLERANCE
|
||||||
|
]
|
||||||
|
if breaches:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"target_weights exceed max_single_weight for {breaches}"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"constraint_state": value["constraint_state"],
|
||||||
|
"long_only": True,
|
||||||
|
"max_single_weight": max_single,
|
||||||
|
"max_gross_weight": max_gross,
|
||||||
|
"max_one_way_turnover": max_turnover,
|
||||||
|
"actual_gross_weight": declared_gross,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _claims_baseline(package: Mapping[str, Any]) -> bool:
|
||||||
|
claim = " ".join(
|
||||||
|
str(package.get(name, ""))
|
||||||
|
for name in ("release", "evidence_class")
|
||||||
|
).lower()
|
||||||
|
return "baseline" in claim
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_release_claim(package: Mapping[str, Any]) -> None:
|
||||||
|
model_id = str(package.get("model_id", "")).strip().lower()
|
||||||
|
if _claims_baseline(package) and "portable-momentum" in model_id:
|
||||||
|
raise TargetPackageError(
|
||||||
|
"portable-momentum is a platform smoke strategy and cannot claim "
|
||||||
|
"a baseline release"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def target_package_sha256(package: Mapping[str, Any]) -> str:
|
||||||
|
"""Compute the canonical content digest, excluding only its digest field."""
|
||||||
|
|
||||||
|
if not isinstance(package, Mapping):
|
||||||
|
raise TargetPackageError("target package must be an object")
|
||||||
|
body = dict(package)
|
||||||
|
body.pop("package_sha256", None)
|
||||||
|
_reject_quantity_fields(body)
|
||||||
|
return hashlib.sha256(canonical_json(body).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def seal_target_package(package: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Attach ``package_sha256`` to an unsealed package and validate it."""
|
||||||
|
|
||||||
|
if not isinstance(package, Mapping):
|
||||||
|
raise TargetPackageError("target package must be an object")
|
||||||
|
if "package_sha256" in package:
|
||||||
|
raise TargetPackageError("refusing to reseal an already sealed package")
|
||||||
|
sealed = _json_copy(package)
|
||||||
|
sealed["package_sha256"] = target_package_sha256(sealed)
|
||||||
|
return validate_target_package(sealed)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_target_package(
|
||||||
|
package: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
expected_release: str | None = None,
|
||||||
|
expected_signal_as_of: str | None = None,
|
||||||
|
expected_next_session: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Validate and return a detached canonical ``TargetPackageV1`` mapping."""
|
||||||
|
|
||||||
|
if not isinstance(package, Mapping):
|
||||||
|
raise TargetPackageError("target package must be an object")
|
||||||
|
candidate = _json_copy(package)
|
||||||
|
_reject_quantity_fields(candidate)
|
||||||
|
try:
|
||||||
|
validate_contract(
|
||||||
|
candidate,
|
||||||
|
load_schema("target_package.schema.json"),
|
||||||
|
)
|
||||||
|
except (ContractValidationError, FileNotFoundError) as exc:
|
||||||
|
raise TargetPackageError(f"target package schema rejected: {exc}") from exc
|
||||||
|
|
||||||
|
signal = _parse_signal_as_of(candidate["signal_as_of"])
|
||||||
|
_parse_next_session(candidate["next_session"], signal)
|
||||||
|
for name in SHA256_FIELDS:
|
||||||
|
_validate_sha256(candidate[name], name)
|
||||||
|
_validate_sha256(candidate["package_sha256"], "package_sha256")
|
||||||
|
|
||||||
|
if not str(candidate["package_id"]).strip():
|
||||||
|
raise TargetPackageError("package_id must be non-empty")
|
||||||
|
if not str(candidate["decision_id"]).strip():
|
||||||
|
raise TargetPackageError("decision_id must be non-empty")
|
||||||
|
if not str(candidate["release"]).strip():
|
||||||
|
raise TargetPackageError("release must be non-empty")
|
||||||
|
if not str(candidate["evidence_class"]).strip():
|
||||||
|
raise TargetPackageError("evidence_class must be non-empty")
|
||||||
|
if not str(candidate["model_id"]).strip():
|
||||||
|
raise TargetPackageError("model_id must be non-empty")
|
||||||
|
|
||||||
|
weights = _validate_weights(candidate["target_weights"])
|
||||||
|
universe = _validate_universe(candidate["universe"])
|
||||||
|
missing_universe = sorted(set(weights) - set(universe))
|
||||||
|
if missing_universe:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"target symbols are absent from universe: {missing_universe}"
|
||||||
|
)
|
||||||
|
invalid_excluded = sorted(
|
||||||
|
symbol
|
||||||
|
for symbol, weight in weights.items()
|
||||||
|
if universe[symbol]["state"] == "excluded"
|
||||||
|
and weight > WEIGHT_TOLERANCE
|
||||||
|
)
|
||||||
|
if invalid_excluded:
|
||||||
|
raise TargetPackageError(
|
||||||
|
f"excluded symbols have positive target weights: {invalid_excluded}"
|
||||||
|
)
|
||||||
|
_validate_constraints(candidate["constraints"], weights)
|
||||||
|
_validate_release_claim(candidate)
|
||||||
|
|
||||||
|
expected_hash = target_package_sha256(candidate)
|
||||||
|
if candidate["package_sha256"] != expected_hash:
|
||||||
|
raise TargetPackageError("package_sha256 content hash mismatch")
|
||||||
|
if expected_release is not None and candidate["release"] != expected_release:
|
||||||
|
raise TargetPackageError("target package release does not match request")
|
||||||
|
if (
|
||||||
|
expected_signal_as_of is not None
|
||||||
|
and candidate["signal_as_of"] != expected_signal_as_of
|
||||||
|
):
|
||||||
|
raise TargetPackageError(
|
||||||
|
"target package signal_as_of does not match request"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
expected_next_session is not None
|
||||||
|
and candidate["next_session"] != expected_next_session
|
||||||
|
):
|
||||||
|
raise TargetPackageError(
|
||||||
|
"target package next_session does not match request"
|
||||||
|
)
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TargetPackageV1:
|
||||||
|
"""Immutable value wrapper around a validated package payload."""
|
||||||
|
|
||||||
|
_canonical_payload: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_mapping(
|
||||||
|
cls,
|
||||||
|
package: Mapping[str, Any],
|
||||||
|
**expected: str,
|
||||||
|
) -> "TargetPackageV1":
|
||||||
|
validated = validate_target_package(package, **expected)
|
||||||
|
return cls(canonical_json(validated))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(
|
||||||
|
cls,
|
||||||
|
package: Mapping[str, Any],
|
||||||
|
**expected: str,
|
||||||
|
) -> "TargetPackageV1":
|
||||||
|
return cls.from_mapping(package, **expected)
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
return json.loads(self._canonical_payload)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def package_id(self) -> str:
|
||||||
|
return self.as_dict()["package_id"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decision_id(self) -> str:
|
||||||
|
return self.as_dict()["decision_id"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def signal_as_of(self) -> str:
|
||||||
|
return self.as_dict()["signal_as_of"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def next_session(self) -> str:
|
||||||
|
return self.as_dict()["next_session"]
|
||||||
|
|
||||||
|
|
||||||
|
class TargetPackageTape:
|
||||||
|
"""Validated multi-decision tape with fail-closed exact lookup."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
packages: Iterable[Mapping[str, Any] | TargetPackageV1],
|
||||||
|
) -> None:
|
||||||
|
items: list[TargetPackageV1] = []
|
||||||
|
package_ids: set[str] = set()
|
||||||
|
decision_ids: set[str] = set()
|
||||||
|
clock_keys: set[tuple[str, str]] = set()
|
||||||
|
for raw in packages:
|
||||||
|
item = (
|
||||||
|
raw
|
||||||
|
if isinstance(raw, TargetPackageV1)
|
||||||
|
else TargetPackageV1.from_mapping(raw)
|
||||||
|
)
|
||||||
|
key = (item.signal_as_of, item.next_session)
|
||||||
|
if item.package_id in package_ids:
|
||||||
|
raise DuplicateTargetDecisionError(
|
||||||
|
f"duplicate package_id {item.package_id!r}"
|
||||||
|
)
|
||||||
|
if item.decision_id in decision_ids:
|
||||||
|
raise DuplicateTargetDecisionError(
|
||||||
|
f"duplicate decision_id {item.decision_id!r}"
|
||||||
|
)
|
||||||
|
if key in clock_keys:
|
||||||
|
raise DuplicateTargetDecisionError(
|
||||||
|
"duplicate decision for signal_as_of/next_session "
|
||||||
|
f"{key!r}"
|
||||||
|
)
|
||||||
|
package_ids.add(item.package_id)
|
||||||
|
decision_ids.add(item.decision_id)
|
||||||
|
clock_keys.add(key)
|
||||||
|
items.append(item)
|
||||||
|
if not items:
|
||||||
|
raise TargetPackageError("target-package tape is empty")
|
||||||
|
self._items = tuple(
|
||||||
|
sorted(
|
||||||
|
items,
|
||||||
|
key=lambda item: (
|
||||||
|
item.signal_as_of,
|
||||||
|
item.next_session,
|
||||||
|
item.decision_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._by_clock = {
|
||||||
|
(item.signal_as_of, item.next_session): item
|
||||||
|
for item in self._items
|
||||||
|
}
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._items)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[dict[str, Any]]:
|
||||||
|
for item in self._items:
|
||||||
|
yield item.as_dict()
|
||||||
|
|
||||||
|
def lookup_exact(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
signal_as_of: str,
|
||||||
|
next_session: str,
|
||||||
|
decision_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
signal = _parse_signal_as_of(signal_as_of)
|
||||||
|
_parse_next_session(next_session, signal)
|
||||||
|
item = self._by_clock.get((signal_as_of, next_session))
|
||||||
|
if item is None:
|
||||||
|
raise TargetPackageNotFoundError(
|
||||||
|
"no target package for exact signal_as_of/next_session pair"
|
||||||
|
)
|
||||||
|
if decision_id is not None and item.decision_id != decision_id:
|
||||||
|
raise TargetPackageNotFoundError(
|
||||||
|
"exact clock pair exists but decision_id does not match"
|
||||||
|
)
|
||||||
|
return item.as_dict()
|
||||||
|
|
||||||
|
def lookup(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
signal_as_of: str,
|
||||||
|
next_session: str,
|
||||||
|
decision_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return self.lookup_exact(
|
||||||
|
signal_as_of=signal_as_of,
|
||||||
|
next_session=next_session,
|
||||||
|
decision_id=decision_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_target_package_tape(
|
||||||
|
packages: Iterable[Mapping[str, Any] | TargetPackageV1],
|
||||||
|
) -> TargetPackageTape:
|
||||||
|
return TargetPackageTape(packages)
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_target_package(
|
||||||
|
packages: Iterable[Mapping[str, Any] | TargetPackageV1],
|
||||||
|
*,
|
||||||
|
signal_as_of: str,
|
||||||
|
next_session: str,
|
||||||
|
decision_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return TargetPackageTape(packages).lookup_exact(
|
||||||
|
signal_as_of=signal_as_of,
|
||||||
|
next_session=next_session,
|
||||||
|
decision_id=decision_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_target_package_tape(path: str | Path) -> TargetPackageTape:
|
||||||
|
"""Strictly load one package, an array/``packages`` object, or JSONL."""
|
||||||
|
|
||||||
|
source = Path(path)
|
||||||
|
try:
|
||||||
|
if source.suffix.lower() == ".jsonl":
|
||||||
|
packages = [
|
||||||
|
_strict_json_text(
|
||||||
|
line,
|
||||||
|
source=f"target-package tape line {line_number}",
|
||||||
|
)
|
||||||
|
for line_number, line in enumerate(
|
||||||
|
source.read_text(encoding="utf-8").splitlines(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
if line.strip()
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
raw = _strict_json_text(
|
||||||
|
source.read_text(encoding="utf-8"),
|
||||||
|
source="target-package tape",
|
||||||
|
)
|
||||||
|
if isinstance(raw, dict) and raw.get("package_type") == PACKAGE_TYPE:
|
||||||
|
packages = [raw]
|
||||||
|
else:
|
||||||
|
packages = raw.get("packages") if isinstance(raw, dict) else raw
|
||||||
|
except (OSError, UnicodeError) as exc:
|
||||||
|
raise TargetPackageError(f"cannot load target-package tape: {exc}") from exc
|
||||||
|
if not isinstance(packages, list):
|
||||||
|
raise TargetPackageError(
|
||||||
|
"target-package tape must be one TargetPackageV1, a JSON array, "
|
||||||
|
"a packages object, or JSONL"
|
||||||
|
)
|
||||||
|
return TargetPackageTape(packages)
|
||||||
+228
-72
@@ -5,25 +5,25 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"artifacts": [
|
"artifacts": [
|
||||||
{
|
{
|
||||||
"core": "src/quant60/portable_core.py",
|
"core": "src/quant60/portable_core.py",
|
||||||
"coreSha256": "af29e4e5e10705cf6aff89874394c1e80d3780f093c19d90ed53ef9d0188a934",
|
"coreSha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||||
"effectiveConfigSha256": "3ef9b9efda9ec6cc5f57a5daca57cab3b68242e0368f18fdaaf9e249bdaa7873",
|
"effectiveConfigSha256": "d4ae2882e9b5d8d673e7c62c0dc54def7c20221c358ac091ee6529bf0413df0b",
|
||||||
"id": "joinquant",
|
"id": "joinquant",
|
||||||
"integrity": "manifest_hashes_match_repository",
|
"integrity": "manifest_hashes_match_repository",
|
||||||
"output": "dist/joinquant_strategy.py",
|
"output": "dist/joinquant_strategy.py",
|
||||||
"outputSha256": "22ddb0546beb3bd4e4bbedecb271452111324c3903e45be1f4ac3d39f38cce4f",
|
"outputSha256": "be4ca1d5764416b7c8e6c206121c0f0bd8219e7317679d79bc72a95492286b1a",
|
||||||
"wrapper": "platforms/joinquant_strategy.py",
|
"wrapper": "platforms/joinquant_strategy.py",
|
||||||
"wrapperSha256": "b5a8e3b0a774a0f45f52f6bcccbe786e234913a1a4cf5b68b85e2bd989ee9294"
|
"wrapperSha256": "0c672b64665b7c1d4409ba288b00d1608e976800af3a4f9a6467767ce4def592"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"core": "src/quant60/portable_core.py",
|
"core": "src/quant60/portable_core.py",
|
||||||
"coreSha256": "af29e4e5e10705cf6aff89874394c1e80d3780f093c19d90ed53ef9d0188a934",
|
"coreSha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||||
"effectiveConfigSha256": "11f189ea5c9d4eaddeb6394dc342abc871941cb3395d7ff79aae1275ce2ad98f",
|
"effectiveConfigSha256": "7e4739937e4e99e5b58154f53271d8d3bfaed47d8d1d5adc747101babde827f5",
|
||||||
"id": "qmt_builtin",
|
"id": "qmt_builtin",
|
||||||
"integrity": "manifest_hashes_match_repository",
|
"integrity": "manifest_hashes_match_repository",
|
||||||
"output": "dist/qmt_builtin_strategy.py",
|
"output": "dist/qmt_builtin_strategy.py",
|
||||||
"outputSha256": "ad503e5ebde60a5deb0ec86334c04d4be2d77ab91d9607d4fe88e2119f22d5dc",
|
"outputSha256": "483877b7ea55c8e0f5e4e47b48a156b3add6cf0a8085011414c5c600913939b5",
|
||||||
"wrapper": "platforms/qmt_builtin_strategy.py",
|
"wrapper": "platforms/qmt_builtin_strategy.py",
|
||||||
"wrapperSha256": "efb931bbd04d0abf0a576f2422a13403fd4d659dccf59f8170e8b13b1bafa640"
|
"wrapperSha256": "a1e53dc22ebd9bb01be43675786151606bbed273d8ab883fb933461e379d2727"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"baselineConfig": "configs/baseline.json",
|
"baselineConfig": "configs/baseline.json",
|
||||||
@@ -40,21 +40,91 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"topN": 20,
|
"topN": 20,
|
||||||
"universeMode": "pit_index"
|
"universeMode": "pit_index"
|
||||||
},
|
},
|
||||||
"portableCoreSha256": "af29e4e5e10705cf6aff89874394c1e80d3780f093c19d90ed53ef9d0188a934",
|
"portableCoreSha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||||
"schemaVersion": 1
|
"schemaVersion": 1
|
||||||
},
|
},
|
||||||
|
"candidateBoundary": {
|
||||||
|
"allFiveLayersRealPlatformObserved": false,
|
||||||
|
"connectedExecution": "reference one-shot quantity/order-delta planner",
|
||||||
|
"connectedOptimizer": "deterministic constrained optimizer",
|
||||||
|
"connectedRiskModel": "60-session diagonal realized variance",
|
||||||
|
"decisionMode": "target_package",
|
||||||
|
"historicalJoinQuantCountsAsBaseline": false,
|
||||||
|
"historicalJoinQuantMode": "portable_momentum_smoke",
|
||||||
|
"joinQuantTargetPackageCountsAsBaseline": false,
|
||||||
|
"joinQuantTargetPackageCountsAsG9": false,
|
||||||
|
"joinQuantTargetPackageRun": {
|
||||||
|
"allFiveLayersObserved": false,
|
||||||
|
"baseline60Passed": false,
|
||||||
|
"bundleSha256": "0586930ddc483f4489f09b562f4d4232b458b703e6a1194af809b707211be8c4",
|
||||||
|
"evidenceClass": "synthetic-research",
|
||||||
|
"fills": 3,
|
||||||
|
"g9Passed": false,
|
||||||
|
"investmentValueClaim": false,
|
||||||
|
"marketDataFrom": "2024-03-11",
|
||||||
|
"marketDataThrough": "2024-03-13",
|
||||||
|
"observedAt": "2026-07-26T08:50:15Z",
|
||||||
|
"observedLayers": [
|
||||||
|
"execution"
|
||||||
|
],
|
||||||
|
"orders": 3,
|
||||||
|
"packageId": "TP-20240311-795dcfba",
|
||||||
|
"qmtPeerObserved": false,
|
||||||
|
"runId": "2457a7c39a276e09e0fabf99e28978e1"
|
||||||
|
},
|
||||||
|
"localEvidenceClass": "synthetic_research",
|
||||||
|
"reachability": {
|
||||||
|
"manifest": "releases/quant60-research-candidate-v1/reachability.json",
|
||||||
|
"manifestSha256": "bf3379b1d8d73ea64d73db8317cfa27307b5dc218884ea2710ffe71dd116c017",
|
||||||
|
"realPlatformObservedLayers": [
|
||||||
|
"execution"
|
||||||
|
],
|
||||||
|
"realPlatformsObserved": [
|
||||||
|
"joinquant"
|
||||||
|
],
|
||||||
|
"releaseId": "quant60-research-candidate-v1",
|
||||||
|
"summary": {
|
||||||
|
"all_layers_implemented": true,
|
||||||
|
"all_layers_jq_entrypoint_reachable": true,
|
||||||
|
"all_layers_local_entrypoint_reachable": true,
|
||||||
|
"all_layers_qmt_entrypoint_reachable": true,
|
||||||
|
"all_layers_real_platform_observed": false,
|
||||||
|
"all_layers_unit_tested": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"realJoinQuantObserved": true,
|
||||||
|
"realPlatformObservedLayers": [
|
||||||
|
"execution"
|
||||||
|
],
|
||||||
|
"realQmtObserved": false,
|
||||||
|
"releaseVerified": false
|
||||||
|
},
|
||||||
"claim": {
|
"claim": {
|
||||||
"allGatesRequired": true,
|
"allGatesRequired": true,
|
||||||
"baselineThreshold": 60,
|
"baselineThreshold": 60,
|
||||||
"gatesPassed": 0,
|
"gatesPassed": 0,
|
||||||
"gatesTotal": 10,
|
"gatesTotal": 10,
|
||||||
"liveReady": false,
|
"liveReady": false,
|
||||||
"reason": "Quant OS 已有可执行本地流水线、真实 Qlib fixture smoke、带校验但不完整的 Tushare→Qlib 技术 smoke,以及一次完成的真实聚宽 hosted smoke;但仍缺完整复权 PIT 数据、同输入聚宽比较、真实 QMT、券商影子盘、连续对账与合规证据,因此不能计入 Baseline 60。",
|
"reason": "Quant OS 已接通本地 synthetic 五层候选链,能冻结 Ridge ModelBundle、逐层 trace 和无股数 TargetPackageV1;一个 synthetic-research TargetPackage 已在真实 JoinQuant hosted 环境完成消费、账户/开盘价绑定、3 笔委托和 3 笔成交,因此 execution 层已有真实平台观察。但真实 provider-data 长样本/OOS 尚未接入,JoinQuant 没有重跑上游 Universe/Alpha/Portfolio/Risk,QMT peer 与逐层真实数据 parity 仍缺失,factor risk、CVXPY 和 guarded TWAP/POV 也尚未进入当前主链。因此它仍是 NOT_BASELINE_60,G9 未通过。",
|
||||||
"releaseVerified": false,
|
"releaseVerified": false,
|
||||||
"score": null,
|
"score": null,
|
||||||
"scoreStatus": "not_scored",
|
"scoreStatus": "not_scored",
|
||||||
"status": "NOT_BASELINE_60"
|
"status": "NOT_BASELINE_60"
|
||||||
},
|
},
|
||||||
|
"deliveryGaps": {
|
||||||
|
"externalEvidence": [
|
||||||
|
"authorized JQData market snapshot, licence/lineage record and a frozen provider-data candidate tape",
|
||||||
|
"authorized real-data multi-period JoinQuant TargetPackage run plus same-input local/JoinQuant layer-by-layer reconciliation",
|
||||||
|
"real QMT TargetPackage built-in and qmttools backtest exports",
|
||||||
|
"real QMT historical-ST positive control, terminal/xtquant build record, broker query contract and trade-amount semantic probes, callback/restart and at least 20 trading days of shadow evidence",
|
||||||
|
"continuous reconciliation, recovery drills and broker compliance confirmation"
|
||||||
|
],
|
||||||
|
"internal": [
|
||||||
|
"The authorized provider-data path has not yet been connected end to end to the frozen Ridge ModelBundle, calibrated risk/cost inputs, five-layer Baseline candidate and TargetPackage tape; the current connected candidate remains synthetic.",
|
||||||
|
"factor_risk.py, the optional CVXPY optimizer mode and guarded TWAP/POV exist as components or optional paths but are not selected by the current Baseline main slice; the connected risk model is 60-session diagonal realized variance and execution is the reference one-shot planner.",
|
||||||
|
"One synthetic-research TargetPackageV1 has been observed at the real JoinQuant execution boundary, but JoinQuant did not recompute the upstream four layers; there is no authorized real-data multi-period tape, same-input layer-by-layer parity, or real QMT peer."
|
||||||
|
]
|
||||||
|
},
|
||||||
"evidence": [
|
"evidence": [
|
||||||
{
|
{
|
||||||
"id": "scorecard",
|
"id": "scorecard",
|
||||||
@@ -68,6 +138,12 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"label": "Portable bundle manifest",
|
"label": "Portable bundle manifest",
|
||||||
"path": "dist/bundle_manifest.json"
|
"path": "dist/bundle_manifest.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "release-reachability",
|
||||||
|
"kind": "machine_record",
|
||||||
|
"label": "Five-layer release reachability manifest",
|
||||||
|
"path": "releases/quant60-research-candidate-v1/reachability.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "readme",
|
"id": "readme",
|
||||||
"kind": "documentation",
|
"kind": "documentation",
|
||||||
@@ -101,9 +177,21 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
{
|
{
|
||||||
"id": "joinquant-hosted-evidence",
|
"id": "joinquant-hosted-evidence",
|
||||||
"kind": "runtime_evidence",
|
"kind": "runtime_evidence",
|
||||||
"label": "JoinQuant hosted smoke boundary",
|
"label": "JoinQuant hosted momentum smoke boundary",
|
||||||
"path": "docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md"
|
"path": "docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "joinquant-target-package-evidence",
|
||||||
|
"kind": "runtime_evidence",
|
||||||
|
"label": "JoinQuant TargetPackage execution evidence boundary",
|
||||||
|
"path": "docs/JOINQUANT_TARGET_PACKAGE_EVIDENCE_2026-07-26.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "joinquant-target-package-run",
|
||||||
|
"kind": "machine_record",
|
||||||
|
"label": "Redacted JoinQuant TargetPackage hosted run",
|
||||||
|
"path": "evidence/joinquant/TP-20240311-795dcfba/2457a7c39a276e09e0fabf99e28978e1/run.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "tushare-data",
|
"id": "tushare-data",
|
||||||
"kind": "runtime_evidence",
|
"kind": "runtime_evidence",
|
||||||
@@ -136,14 +224,32 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"scorecard"
|
"scorecard"
|
||||||
],
|
],
|
||||||
"id": "joinquant-hosted-market-window",
|
"id": "joinquant-hosted-market-window",
|
||||||
"label": "JoinQuant hosted smoke 行情区间",
|
"label": "JoinQuant hosted momentum smoke 行情区间",
|
||||||
"marketDataFrom": "2024-01-02",
|
"marketDataFrom": "2024-01-02",
|
||||||
"marketDataThrough": "2024-12-31",
|
"marketDataThrough": "2024-12-31",
|
||||||
"productionReady": false,
|
"productionReady": false,
|
||||||
"semantics": "单次真实 hosted runtime smoke 的回测区间;不代表同输入跨引擎通过。",
|
"semantics": "旧 momentum hosted runtime smoke 的回测区间;不是 TargetPackage 候选或同输入跨引擎证据。",
|
||||||
"status": "runtime_smoke",
|
"status": "runtime_smoke",
|
||||||
"value": "2024-01-02 — 2024-12-31"
|
"value": "2024-01-02 — 2024-12-31"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"evidenceLinks": [
|
||||||
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run",
|
||||||
|
"release-reachability",
|
||||||
|
"scorecard"
|
||||||
|
],
|
||||||
|
"id": "joinquant-target-package-execution-smoke",
|
||||||
|
"investmentValueClaim": false,
|
||||||
|
"label": "JoinQuant TargetPackage execution smoke",
|
||||||
|
"marketDataFrom": "2024-03-11",
|
||||||
|
"marketDataThrough": "2024-03-13",
|
||||||
|
"observedAt": "2026-07-26T08:50:15Z",
|
||||||
|
"productionReady": false,
|
||||||
|
"semantics": "真实平台只观察 synthetic-research TargetPackage 的 execution consumer;不代表上游四层真实平台执行、长样本 OOS 或 G9。",
|
||||||
|
"status": "runtime_smoke",
|
||||||
|
"value": "2024-03-11 — 2024-03-13; 3 orders / 3 fills"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"completedPartitions": 35,
|
"completedPartitions": 35,
|
||||||
"completedRows": 28731,
|
"completedRows": 28731,
|
||||||
@@ -422,7 +528,7 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"Tushare completed-Parquet inventory and immutable Qlib provider",
|
"Tushare completed-Parquet inventory and immutable Qlib provider",
|
||||||
"semantic manifest and data-version verification"
|
"semantic manifest and data-version verification"
|
||||||
],
|
],
|
||||||
"claim": "不可变快照、PIT 字段合同和 Tushare→Qlib 技术链路已实现并有本地合同/运行证据。",
|
"claim": "不可变快照、PIT 字段合同和 Tushare→Qlib 技术链路已有本地合同/运行证据;这些 provider-data 路径尚未接成当前 Ridge 五层候选。",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"architecture",
|
"architecture",
|
||||||
"tushare-data",
|
"tushare-data",
|
||||||
@@ -430,7 +536,7 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
],
|
],
|
||||||
"id": "data",
|
"id": "data",
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"尚无授权 JQData 真实快照与许可 lineage;Tushare 镜像仅部分完成且缺生产关键字段。"
|
"尚无授权 JQData 真实快照与许可 lineage;Tushare 镜像仅部分完成且缺生产关键字段;当前五层候选仍使用 synthetic 数据。"
|
||||||
],
|
],
|
||||||
"name": "数据与时点",
|
"name": "数据与时点",
|
||||||
"releaseVerified": false,
|
"releaseVerified": false,
|
||||||
@@ -438,19 +544,21 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"PIT features, purged walk-forward and deterministic Ridge",
|
"train-only preprocessing, purged walk-forward and deterministic Ridge",
|
||||||
|
"replayable frozen ModelBundle and synthetic TargetPackageV1",
|
||||||
"Qlib native momentum",
|
"Qlib native momentum",
|
||||||
"Qlib Alpha158, LightGBM and Recorder workflow"
|
"Qlib Alpha158, LightGBM and Recorder workflow"
|
||||||
],
|
],
|
||||||
"claim": "synthetic research pipeline 已通过测试;Qlib 0.9.7 的 momentum 与 Alpha158/LightGBM fixture 已在真实本地运行时完成。",
|
"claim": "synthetic 五层研究入口能冻结 train-only Ridge ModelBundle、逐层 trace 与 TargetPackageV1;Qlib momentum 和 Alpha158/LightGBM fixture 是另外的受限研究 smoke。",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"architecture",
|
"architecture",
|
||||||
"platform-matrix",
|
"platform-matrix",
|
||||||
|
"release-reachability",
|
||||||
"scorecard"
|
"scorecard"
|
||||||
],
|
],
|
||||||
"id": "research",
|
"id": "research",
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"fixture 仅两只 synthetic 股票;没有授权长样本 OOS、冻结模型包或投资价值证据。"
|
"候选和 Qlib fixture 均是 synthetic;尚无授权长样本 OOS、真实数据校准、平台候选观察或投资价值证据。"
|
||||||
],
|
],
|
||||||
"name": "研究与模型",
|
"name": "研究与模型",
|
||||||
"releaseVerified": false,
|
"releaseVerified": false,
|
||||||
@@ -458,18 +566,20 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"portable momentum scoring and capped target weights",
|
"explicit fee/spread/square-root-impact cost forecast",
|
||||||
"lot-rounded target quantities and T+1 sellable deltas",
|
"diagonal realized-variance risk input and deterministic constrained target",
|
||||||
"factor risk, dated cost and constrained target research"
|
"single-name, industry, gross, capacity and turnover constraints",
|
||||||
|
"post-risk weights with hash-linked lineage"
|
||||||
],
|
],
|
||||||
"claim": "Signal→Target→Order Delta 合同、成本、风险与约束目标在本地测试路径可重放。",
|
"claim": "当前 synthetic 主链把 Ridge Alpha、显式成本、60 日对角 realized variance、deterministic optimizer 和 post-risk gate 接成可重放 TargetPackage。",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"architecture",
|
"architecture",
|
||||||
|
"release-reachability",
|
||||||
"scorecard"
|
"scorecard"
|
||||||
],
|
],
|
||||||
"id": "signal-portfolio",
|
"id": "signal-portfolio",
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"风险、冲击和容量尚未用授权真实数据校准;G4/G5 均未通过。"
|
"factor_risk.py 与 optional CVXPY solver 未被当前主链选择;风险、冲击和容量尚未用授权真实数据校准,G4/G5 均未通过。"
|
||||||
],
|
],
|
||||||
"name": "信号、组合与事前风险",
|
"name": "信号、组合与事前风险",
|
||||||
"releaseVerified": false,
|
"releaseVerified": false,
|
||||||
@@ -478,20 +588,22 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
{
|
{
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"local event and snapshot backtests",
|
"local event and snapshot backtests",
|
||||||
"JoinQuant hosted single-file bundle",
|
"fail-closed JoinQuant TargetPackage single-file consumer",
|
||||||
"QMT built-in/qmttools backtest-only bundle",
|
"fail-closed QMT built-in TargetPackage backtest consumer",
|
||||||
"Qlib research runners"
|
"Qlib research runners"
|
||||||
],
|
],
|
||||||
"claim": "本地 synthetic、Qlib fixture 与一次真实 JoinQuant hosted smoke 已运行;QMT 仍只有 bundle/harness。",
|
"claim": "本地 synthetic 五层候选、平台 harness 和一次真实 JoinQuant TargetPackage execution smoke 已运行;QMT 尚无真实 peer run。",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"platform-matrix",
|
"platform-matrix",
|
||||||
"joinquant-hosted-evidence",
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run",
|
||||||
"cross-engine",
|
"cross-engine",
|
||||||
|
"release-reachability",
|
||||||
"scorecard"
|
"scorecard"
|
||||||
],
|
],
|
||||||
"id": "backtest",
|
"id": "backtest",
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"没有冻结同输入的本地/聚宽/QMT 逐层对比;真实 QMT 尚未运行,G9 未通过。"
|
"聚宽只观察到一个 synthetic-research TargetPackage 的消费、账户/开盘价绑定和三笔成交;无真实长样本 OOS、QMT peer 或逐层真实数据 parity,G9 未通过。"
|
||||||
],
|
],
|
||||||
"name": "回测与跨引擎",
|
"name": "回测与跨引擎",
|
||||||
"releaseVerified": false,
|
"releaseVerified": false,
|
||||||
@@ -501,17 +613,20 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"capabilities": [
|
"capabilities": [
|
||||||
"local and Colab zero-account smoke",
|
"local and Colab zero-account smoke",
|
||||||
"JQData snapshot entrypoint",
|
"JQData snapshot entrypoint",
|
||||||
"JoinQuant upload bundle",
|
"JoinQuant momentum or explicit TargetPackage upload bundle",
|
||||||
"QMT built-in and qmttools entrypoints"
|
"QMT built-in TargetPackage and qmttools entrypoints"
|
||||||
],
|
],
|
||||||
"claim": "Local、Colab、JoinQuant、Qlib 有可执行入口;JoinQuant/Qlib 有受限真实运行时 smoke。",
|
"claim": "Local、Colab、JoinQuant、QMT、Qlib 有入口;JoinQuant 已真实观察 synthetic-research TargetPackage 的 execution consumer,QMT 候选仍未实跑。",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"platform-matrix",
|
"platform-matrix",
|
||||||
"platform-deployment"
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run",
|
||||||
|
"platform-deployment",
|
||||||
|
"release-reachability"
|
||||||
],
|
],
|
||||||
"id": "platforms",
|
"id": "platforms",
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"JQData 真实账号、QMT 客户端/数据权限及平台完整导出仍是外部前置条件。"
|
"JoinQuant 没有重跑上游四层;JQData 授权真实候选、QMT 客户端/数据权限、QMT peer 导出与逐层真实数据 parity 仍是外部前置条件。"
|
||||||
],
|
],
|
||||||
"name": "平台适配",
|
"name": "平台适配",
|
||||||
"releaseVerified": false,
|
"releaseVerified": false,
|
||||||
@@ -560,7 +675,7 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
],
|
],
|
||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"claim": "共享 symbol normalization、momentum、target weight/quantity 与 order-delta 计算;已进入 JoinQuant hosted smoke 和本地 Qlib momentum 路径。",
|
"claim": "共享 symbol normalization、momentum、target weight/quantity 与 order-delta 计算;2024 JoinQuant hosted run 只证明这一 smoke 路径。",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"architecture",
|
"architecture",
|
||||||
"bundle-manifest",
|
"bundle-manifest",
|
||||||
@@ -569,27 +684,29 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"id": "portable-momentum-v1",
|
"id": "portable-momentum-v1",
|
||||||
"investmentValueClaim": false,
|
"investmentValueClaim": false,
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"尚无同输入 L1–L4 真实跨引擎通过证据,不是已验证的生产 champion。"
|
"不是 Ridge/TargetPackage Baseline 候选;旧 hosted run 不得用于候选或 G9 计分。"
|
||||||
],
|
],
|
||||||
"name": "portable-momentum-v1",
|
"name": "portable-momentum-v1",
|
||||||
"releaseVerified": false,
|
"releaseVerified": false,
|
||||||
"role": "跨引擎冻结计算基线",
|
"role": "平台连通性 smoke 策略",
|
||||||
"status": "real_runtime"
|
"status": "real_runtime"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"claim": "purged walk-forward research pipeline 中可确定性训练和评估。",
|
"claim": "train-only 预处理、系数、截距、训练截止和 lineage 可冻结为可重放 ModelBundle;其 synthetic TargetPackageV1 已被真实 JoinQuant execution consumer 消费并成交。",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"architecture",
|
"architecture",
|
||||||
"platform-matrix"
|
"platform-matrix",
|
||||||
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run"
|
||||||
],
|
],
|
||||||
"id": "ridge-challenger",
|
"id": "ridge-challenger",
|
||||||
"investmentValueClaim": false,
|
"investmentValueClaim": false,
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"只在 synthetic 研究路径验证;无真实 OOS、versioned model bundle 或跨平台推理验证。"
|
"JoinQuant 只观察执行边界,未重跑上游四层;无授权真实 OOS、真实校准、QMT peer 或投资价值证据。"
|
||||||
],
|
],
|
||||||
"name": "Deterministic Ridge",
|
"name": "Deterministic Ridge",
|
||||||
"releaseVerified": false,
|
"releaseVerified": false,
|
||||||
"role": "synthetic 研究候选",
|
"role": "synthetic 五层 Baseline 研究候选",
|
||||||
"status": "tested"
|
"status": "tested"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -613,31 +730,35 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
{
|
{
|
||||||
"gateIds": [
|
"gateIds": [
|
||||||
"G1",
|
"G1",
|
||||||
|
"G2",
|
||||||
"G3",
|
"G3",
|
||||||
|
"G5",
|
||||||
"G7"
|
"G7"
|
||||||
],
|
],
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"title": "生成授权 JQData 不可变真实快照",
|
"title": "接通真实数据五层候选",
|
||||||
"why": "authorized JQData market snapshot and licence/lineage record"
|
"why": "The authorized provider-data path has not yet been connected end to end to the frozen Ridge ModelBundle, calibrated risk/cost inputs, five-layer Baseline candidate and TargetPackage tape; the current connected candidate remains synthetic. External prerequisite: authorized JQData market snapshot, licence/lineage record and a frozen provider-data candidate tape"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"gateIds": [
|
"gateIds": [
|
||||||
"G3",
|
"G3",
|
||||||
|
"G5",
|
||||||
|
"G7",
|
||||||
"G9"
|
"G9"
|
||||||
],
|
],
|
||||||
"priority": 2,
|
"priority": 2,
|
||||||
"title": "冻结聚宽同输入并完成逐层本地比较",
|
"title": "冻结主链风险、优化与执行选择",
|
||||||
"why": "complete JoinQuant hosted input/plan/order/fill export and same-input local comparison"
|
"why": "factor_risk.py, the optional CVXPY optimizer mode and guarded TWAP/POV exist as components or optional paths but are not selected by the current Baseline main slice; the connected risk model is 60-session diagonal realized variance and execution is the reference one-shot planner."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"gateIds": [
|
"gateIds": [
|
||||||
"G3",
|
"G3",
|
||||||
"G6",
|
"G7",
|
||||||
"G9"
|
"G9"
|
||||||
],
|
],
|
||||||
"priority": 3,
|
"priority": 3,
|
||||||
"title": "取得真实 QMT built-in 与 qmttools 导出",
|
"title": "扩展 JoinQuant 多期对账与逐层 parity",
|
||||||
"why": "real QMT built-in and qmttools backtest exports"
|
"why": "One synthetic-research TargetPackageV1 has been observed at the real JoinQuant execution boundary, but JoinQuant did not recompute the upstream four layers; there is no authorized real-data multi-period tape, same-input layer-by-layer parity, or real QMT peer. Required evidence: authorized real-data multi-period JoinQuant TargetPackage run plus same-input local/JoinQuant layer-by-layer reconciliation"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"gateIds": [
|
"gateIds": [
|
||||||
@@ -646,6 +767,16 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"G9"
|
"G9"
|
||||||
],
|
],
|
||||||
"priority": 4,
|
"priority": 4,
|
||||||
|
"title": "运行真实 QMT TargetPackage 候选",
|
||||||
|
"why": "real QMT TargetPackage built-in and qmttools backtest exports"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gateIds": [
|
||||||
|
"G6",
|
||||||
|
"G8",
|
||||||
|
"G9"
|
||||||
|
],
|
||||||
|
"priority": 5,
|
||||||
"title": "完成 QMT 券商合同探针与至少 20 日影子盘",
|
"title": "完成 QMT 券商合同探针与至少 20 日影子盘",
|
||||||
"why": "real QMT historical-ST positive control, terminal/xtquant build record, broker query contract and trade-amount semantic probes, callback/restart and at least 20 trading days of shadow evidence"
|
"why": "real QMT historical-ST positive control, terminal/xtquant build record, broker query contract and trade-amount semantic probes, callback/restart and at least 20 trading days of shadow evidence"
|
||||||
},
|
},
|
||||||
@@ -654,14 +785,14 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"G8",
|
"G8",
|
||||||
"G10"
|
"G10"
|
||||||
],
|
],
|
||||||
"priority": 5,
|
"priority": 6,
|
||||||
"title": "完成连续对账、恢复演练与券商合规确认",
|
"title": "完成连续对账、恢复演练与券商合规确认",
|
||||||
"why": "continuous reconciliation, recovery drills and broker compliance confirmation"
|
"why": "continuous reconciliation, recovery drills and broker compliance confirmation"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"platforms": [
|
"platforms": [
|
||||||
{
|
{
|
||||||
"claim": "标准库套件最近记录为 224 tests、OK、6 skip;smoke + exact manifest verifier 可运行",
|
"claim": "标准库套件、smoke + exact manifest verifier 可运行",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"platform-matrix"
|
"platform-matrix"
|
||||||
],
|
],
|
||||||
@@ -677,14 +808,14 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"status": "tested"
|
"status": "tested"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"claim": "research smoke + manifest verifier 可运行",
|
"claim": "冻结 model bundle、TargetPackageV1、layer trace + manifest verifier 可运行;风险/成本扰动必须改变 target",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"platform-matrix"
|
"platform-matrix"
|
||||||
],
|
],
|
||||||
"id": "local-synthetic-research",
|
"id": "local-synthetic-research",
|
||||||
"intendedUse": "PIT/features/walk-forward/Ridge/risk/cost/target",
|
"intendedUse": "五层 vertical slice:Universe/Ridge Alpha/risk+cost Portfolio/post-risk/reference Execution",
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"Ridge 未冻结部署,未做授权长样本 OOS"
|
"仍是 synthetic;当前只接对角风险与 deterministic optimizer,未做授权长样本 OOS"
|
||||||
],
|
],
|
||||||
"liveReady": false,
|
"liveReady": false,
|
||||||
"name": "Local synthetic research",
|
"name": "Local synthetic research",
|
||||||
@@ -757,10 +888,12 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"status": "tested"
|
"status": "tested"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"claim": "fake lifecycle 与 mock parity 可运行",
|
"claim": "动量 smoke 与 TargetPackage 消费模式均有 fake lifecycle;目标包缺失/篡改不回退",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"platform-matrix",
|
"platform-matrix",
|
||||||
"joinquant-hosted-evidence"
|
"joinquant-hosted-evidence",
|
||||||
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run"
|
||||||
],
|
],
|
||||||
"id": "joinquant-local-harness",
|
"id": "joinquant-local-harness",
|
||||||
"intendedUse": "wrapper API 合同",
|
"intendedUse": "wrapper API 合同",
|
||||||
@@ -774,15 +907,17 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"status": "tested"
|
"status": "tested"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"claim": "真实 2024 全年 run 已完成且可见日志 0 ERROR;单文件、PIT index/ST、批量 history、费率/滑点/10% 参与率、科创板保护价/200 股约束已运行",
|
"claim": "动量 smoke 的真实 2024 全年 run 已完成;FINAL4 的单个 synthetic-research TargetPackage 已在真实 hosted 环境按 day_open 命中并产生 3 单/3 fill",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"platform-matrix",
|
"platform-matrix",
|
||||||
"joinquant-hosted-evidence"
|
"joinquant-hosted-evidence",
|
||||||
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run"
|
||||||
],
|
],
|
||||||
"id": "joinquant-hosted-bundle",
|
"id": "joinquant-hosted-bundle",
|
||||||
"intendedUse": "hosted backtest",
|
"intendedUse": "hosted backtest",
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"尚无完整输入/plan/orders/fills 导出和同输入本地比较"
|
"只观察 execution consumer;无授权真实长样本/OOS、上游四层 hosted 执行、QMT peer 或逐层真实数据 parity"
|
||||||
],
|
],
|
||||||
"liveReady": false,
|
"liveReady": false,
|
||||||
"name": "JoinQuant hosted bundle",
|
"name": "JoinQuant hosted bundle",
|
||||||
@@ -791,7 +926,7 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"status": "real_runtime"
|
"status": "real_runtime"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"claim": "Python 3.6 source、ContextInfo 回滚模拟和 mock parity",
|
"claim": "Python 3.6 source、ContextInfo 回滚模拟、动量 smoke 与 TargetPackage 合同",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"platform-matrix",
|
"platform-matrix",
|
||||||
"platform-deployment"
|
"platform-deployment"
|
||||||
@@ -808,7 +943,7 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"status": "tested"
|
"status": "tested"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"claim": "bundle 硬限制 backtest;毫秒 timetag PIT 成分/ST;固定中证 500、费率/滑点;模块全局 g",
|
"claim": "两种模式都硬限制 backtest;TargetPackage 模式不重算上游四层;模块全局 g",
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"platform-matrix",
|
"platform-matrix",
|
||||||
"platform-deployment"
|
"platform-deployment"
|
||||||
@@ -816,11 +951,11 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"id": "qmt-built-in-bundle",
|
"id": "qmt-built-in-bundle",
|
||||||
"intendedUse": "GUI backtest-only",
|
"intendedUse": "GUI backtest-only",
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"尚无真实 QMT run/export;10% GUI 配置与历史 ST 正例探针待取证"
|
"尚无 TargetPackage 真实 QMT run/export;10% GUI 配置和执行时钟待取证"
|
||||||
],
|
],
|
||||||
"liveReady": false,
|
"liveReady": false,
|
||||||
"name": "QMT built-in bundle",
|
"name": "QMT built-in bundle",
|
||||||
"prerequisite": "授权 QMT 客户端/板块与历史 ST 数据",
|
"prerequisite": "授权 QMT 客户端/历史数据",
|
||||||
"releaseVerified": false,
|
"releaseVerified": false,
|
||||||
"status": "implemented"
|
"status": "implemented"
|
||||||
},
|
},
|
||||||
@@ -928,19 +1063,28 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"provenance": {
|
"provenance": {
|
||||||
"baselineConfig": "configs/baseline.json",
|
"baselineConfig": "configs/baseline.json",
|
||||||
"bundleManifest": "dist/bundle_manifest.json",
|
"bundleManifest": "dist/bundle_manifest.json",
|
||||||
"payloadSha256": "1243b3d6821fd7cc24d8473b094dbeb1f1791cc1931f25e5fb683489bbd0bd98",
|
"payloadSha256": "76aad7a50088b9949b9c2549a4da568d14c6bc2ff4acc7df6419989637386bc3",
|
||||||
"payloadSha256Semantics": "SHA-256 of canonical payload before payloadSha256 is added.",
|
"payloadSha256Semantics": "SHA-256 of canonical payload before payloadSha256 is added.",
|
||||||
"platformMatrix": "docs/PLATFORM_MATRIX.md",
|
"platformMatrix": "docs/PLATFORM_MATRIX.md",
|
||||||
|
"releaseReachability": "releases/quant60-research-candidate-v1/reachability.json",
|
||||||
"scorecard": "gate_scorecard.json",
|
"scorecard": "gate_scorecard.json",
|
||||||
"sourceSetSha256": "e49692c14e142b3bab524763243c16ae4964eb5933d35d867cf89ed52dbcc90c",
|
"sourceSetSha256": "e1b8bb8827c6c051a73872f2b0ac46f18dcf705fb50768eb98090bbbe08f863c",
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"path": "gate_scorecard.json",
|
"path": "gate_scorecard.json",
|
||||||
"sha256": "59e5db5c57d1665ee4a2a2f4cf0ef93a1e7a17e1ef7aa62d0fb64da4ad637c6e"
|
"sha256": "db9ed17d1b56452f973db23461e8298b4f43f319b04be373adb383133500c2b6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dist/bundle_manifest.json",
|
"path": "dist/bundle_manifest.json",
|
||||||
"sha256": "7290480dbe9c5ec7453896f0ffa6d5cec2f72284cb0f723902d29b37d912bdee"
|
"sha256": "3323a07d0c93c6a9dcf8ead2760025ee8f78136bf9334ca5bd4fd836fbbedb54"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "releases/quant60-research-candidate-v1/reachability.json",
|
||||||
|
"sha256": "2a506c2a4a9f4d813c34be4b3d02acdef6a2aa4c6eeff304be5065f1d3d34cbb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "evidence/joinquant/TP-20240311-795dcfba/2457a7c39a276e09e0fabf99e28978e1/run.json",
|
||||||
|
"sha256": "9956c5e61bdc1c82bc24f20f24c1d65a201bab2f71c2c9e4e23eb5bd40eca6ce"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "configs/baseline.json",
|
"path": "configs/baseline.json",
|
||||||
@@ -948,19 +1092,19 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "README.md",
|
"path": "README.md",
|
||||||
"sha256": "3bbd5821ad0641ad9bd6b6dd72a8dcebcddf8655fb0d5fa10ca16a46eaf35a62"
|
"sha256": "d5593bdc4e535502ba817636053dc8ac38f8594be6f9e61f0fe2d11cf883a985"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "docs/ARCHITECTURE.md",
|
"path": "docs/ARCHITECTURE.md",
|
||||||
"sha256": "b22fc952a260d0201d3b1a6bf221628c5dd341391b4e814261413e27ab181fd1"
|
"sha256": "93b1981168084f40c6d127dc534ff2e60eb4cd0703c8ef446c6df58b351dfbaa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "docs/AUDIT_2026-07-25.md",
|
"path": "docs/AUDIT_2026-07-25.md",
|
||||||
"sha256": "39731b2c828c01f83c5b8626f2ca02762c376da2cfcd62f2e2c4d8a2f32f780e"
|
"sha256": "c070b49dfc2805c338400697369d886a6dc47b53af4a1834d0dfd383431e74d0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "docs/PLATFORM_MATRIX.md",
|
"path": "docs/PLATFORM_MATRIX.md",
|
||||||
"sha256": "5a084e99df1cd31f89859b390c6cb0e79a68a0f34514bf8a30b3cc1a0c64f1eb"
|
"sha256": "4880b17e54257b794398bf734d80ea40d1180e52845762987443a9c61d562a23"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "docs/CROSS_ENGINE_CONSISTENCY.md",
|
"path": "docs/CROSS_ENGINE_CONSISTENCY.md",
|
||||||
@@ -970,6 +1114,10 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"path": "docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md",
|
"path": "docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md",
|
||||||
"sha256": "03df249829b3ad9b4fe33c5d93afbd842561458f4ff2fe10e80e4432934eb2d2"
|
"sha256": "03df249829b3ad9b4fe33c5d93afbd842561458f4ff2fe10e80e4432934eb2d2"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/JOINQUANT_TARGET_PACKAGE_EVIDENCE_2026-07-26.md",
|
||||||
|
"sha256": "99b62e06b843e9866dc88b0479ce474232a5cae84c8595ad5adb2bf707b3d205"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "docs/TUSHARE_LOCAL_DATA.md",
|
"path": "docs/TUSHARE_LOCAL_DATA.md",
|
||||||
"sha256": "fd6cfe6cf45dbdd82e6aa88e80b119e05a888c560b79907370c6e023b5e00e82"
|
"sha256": "fd6cfe6cf45dbdd82e6aa88e80b119e05a888c560b79907370c6e023b5e00e82"
|
||||||
@@ -978,26 +1126,34 @@ window.QUANT_OS_PUBLIC_STATUS = {
|
|||||||
"path": "runbooks/PLATFORM_DEPLOYMENT.md",
|
"path": "runbooks/PLATFORM_DEPLOYMENT.md",
|
||||||
"sha256": "f6bb1eda335c0d05f96d39b4d4167ee623fe6cffeef75015ba7985638fa8f635"
|
"sha256": "f6bb1eda335c0d05f96d39b4d4167ee623fe6cffeef75015ba7985638fa8f635"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/build_release_reachability.py",
|
||||||
|
"sha256": "fecee5980917f86440e96ccfe488e52111db5715b6f3847b87cb98f9a895b4be"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/build_public_status.py",
|
"path": "tools/build_public_status.py",
|
||||||
"sha256": "329332a09238499cf63530d011d4181d26edf506a23d665165e4b3ff49f796ca"
|
"sha256": "38347c2238a38fc603703c7265fd1ca54480b3f3f8c453cde9f21056f207a4ec"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"summary": {
|
"summary": {
|
||||||
|
"allFiveLayersRealPlatformObserved": false,
|
||||||
|
"candidateRealPlatformObservationScope": "target_package_execution_consumer_only",
|
||||||
|
"candidateRealPlatformObserved": true,
|
||||||
"gateCoverageLabel": "0/10 passed",
|
"gateCoverageLabel": "0/10 passed",
|
||||||
"investmentValueClaim": false,
|
"investmentValueClaim": false,
|
||||||
"optionalSkipsRecorded": 6,
|
"optionalSkipsRecorded": 6,
|
||||||
"productionReady": false,
|
"productionReady": false,
|
||||||
"realRuntimeSmokes": [
|
"realRuntimeSmokes": [
|
||||||
"JoinQuant hosted daily backtest (runtime/path only)",
|
"JoinQuant TargetPackage execution consumer: one synthetic-research package, 3 orders / 3 fills (not Baseline/G9 evidence)",
|
||||||
|
"JoinQuant portable_momentum_smoke hosted daily backtest (not Baseline/TargetPackage evidence)",
|
||||||
"Qlib native momentum fixture",
|
"Qlib native momentum fixture",
|
||||||
"Qlib Alpha158 + LightGBM fixture",
|
"Qlib Alpha158 + LightGBM fixture",
|
||||||
"Tushare completed-partition Qlib momentum smoke"
|
"Tushare completed-partition Qlib momentum smoke"
|
||||||
],
|
],
|
||||||
"testEvidenceQualifier": "Scorecard-recorded run; tests do not prove real-platform or broker gates.",
|
"testEvidenceQualifier": "Scorecard-recorded run; tests do not prove real-platform or broker gates.",
|
||||||
"testsRecorded": 224
|
"testsRecorded": 285
|
||||||
},
|
},
|
||||||
"system": "Quant OS / quant60 baseline kernel"
|
"system": "Quant OS / quant60 baseline kernel"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,10 +11,57 @@ sys.path.insert(0, str(ROOT))
|
|||||||
sys.path.insert(0, str(ROOT / "src"))
|
sys.path.insert(0, str(ROOT / "src"))
|
||||||
|
|
||||||
from platforms import joinquant_strategy, qmt_builtin_strategy
|
from platforms import joinquant_strategy, qmt_builtin_strategy
|
||||||
|
from quant60.target_package import seal_target_package
|
||||||
from tools.bundle_platforms import bundle_all, effective_platform_config
|
from tools.bundle_platforms import bundle_all, effective_platform_config
|
||||||
from tools.capability_probe import probe_capabilities
|
from tools.capability_probe import probe_capabilities
|
||||||
|
|
||||||
|
|
||||||
|
def target_package():
|
||||||
|
return seal_target_package(
|
||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"package_type": "TargetPackageV1",
|
||||||
|
"package_id": "TP-20260724-bundle",
|
||||||
|
"decision_id": "D-20260724-bundle",
|
||||||
|
"release": "quant60-research-candidate-v1",
|
||||||
|
"evidence_class": "synthetic-research",
|
||||||
|
"model_id": "deterministic-ridge-alpha-5",
|
||||||
|
"signal_as_of": "2026-07-24T15:00:00+08:00",
|
||||||
|
"next_session": "2026-07-27",
|
||||||
|
"model_sha256": "1" * 64,
|
||||||
|
"data_sha256": "2" * 64,
|
||||||
|
"feature_sha256": "3" * 64,
|
||||||
|
"risk_sha256": "4" * 64,
|
||||||
|
"cost_sha256": "5" * 64,
|
||||||
|
"optimizer_sha256": "6" * 64,
|
||||||
|
"config_sha256": "7" * 64,
|
||||||
|
"source_sha256": "8" * 64,
|
||||||
|
"target_weights": {
|
||||||
|
"000001.XSHE": 0.30,
|
||||||
|
"600000.XSHG": 0.20,
|
||||||
|
},
|
||||||
|
"universe": {
|
||||||
|
"000001.XSHE": {
|
||||||
|
"state": "openable",
|
||||||
|
"reasons": ["ELIGIBLE"],
|
||||||
|
},
|
||||||
|
"600000.XSHG": {
|
||||||
|
"state": "openable",
|
||||||
|
"reasons": ["ELIGIBLE"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"constraint_state": "FEASIBLE",
|
||||||
|
"long_only": True,
|
||||||
|
"max_single_weight": 0.40,
|
||||||
|
"max_gross_weight": 0.80,
|
||||||
|
"max_one_way_turnover": 0.50,
|
||||||
|
"actual_gross_weight": 0.50,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BundleTest(unittest.TestCase):
|
class BundleTest(unittest.TestCase):
|
||||||
def test_source_wrappers_match_canonical_baseline_config(self):
|
def test_source_wrappers_match_canonical_baseline_config(self):
|
||||||
baseline = json.loads(
|
baseline = json.loads(
|
||||||
@@ -66,6 +113,35 @@ class BundleTest(unittest.TestCase):
|
|||||||
(ROOT / "configs/baseline.json").read_bytes()
|
(ROOT / "configs/baseline.json").read_bytes()
|
||||||
).hexdigest(),
|
).hexdigest(),
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
stored["decision_mode"],
|
||||||
|
"portable_momentum_smoke",
|
||||||
|
)
|
||||||
|
self.assertEqual(stored["target_package_count"], 0)
|
||||||
|
self.assertIsNone(stored["target_package_tape_sha256"])
|
||||||
|
self.assertEqual(
|
||||||
|
stored["joinquant_observation"]["prefix"],
|
||||||
|
"QUANT60_JOINQUANT_EVIDENCE_V1 ",
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
stored["joinquant_observation"]["fail_soft"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
stored["joinquant_observation"]["target_price_source"],
|
||||||
|
"CURRENT_DATA_DAY_OPEN",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
stored["joinquant_observation"]["eod_chunk_encoding"],
|
||||||
|
"base64-canonical-json",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
stored["joinquant_observation"]["eod_chunk_characters"],
|
||||||
|
1800,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"EOD_STATUS",
|
||||||
|
stored["joinquant_observation"]["events"],
|
||||||
|
)
|
||||||
|
|
||||||
def test_committed_dist_is_byte_for_byte_fresh(self):
|
def test_committed_dist_is_byte_for_byte_fresh(self):
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
@@ -112,6 +188,65 @@ class BundleTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(all(value >= 0 for value in quantities.values()))
|
self.assertTrue(all(value >= 0 for value in quantities.values()))
|
||||||
|
|
||||||
|
def test_target_package_bundle_is_explicit_pinned_and_no_fallback(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
root = Path(temp)
|
||||||
|
package_path = root / "target_package.json"
|
||||||
|
package = target_package()
|
||||||
|
package_path.write_text(
|
||||||
|
json.dumps(package, ensure_ascii=False),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
output = root / "bundle"
|
||||||
|
manifest = bundle_all(ROOT, output, package_path)
|
||||||
|
self.assertEqual(manifest["decision_mode"], "target_package")
|
||||||
|
self.assertEqual(manifest["target_package_count"], 1)
|
||||||
|
self.assertEqual(
|
||||||
|
manifest["target_package_ids"],
|
||||||
|
[package["package_id"]],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
manifest["target_package_sha256"],
|
||||||
|
[package["package_sha256"]],
|
||||||
|
)
|
||||||
|
self.assertNotIn(
|
||||||
|
str(root),
|
||||||
|
json.dumps(manifest, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
for filename in (
|
||||||
|
"joinquant_strategy.py",
|
||||||
|
"qmt_builtin_strategy.py",
|
||||||
|
):
|
||||||
|
source = (output / filename).read_text(encoding="utf-8")
|
||||||
|
self.assertIn(
|
||||||
|
"'decision_mode': 'target_package'",
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
self.assertNotIn("TARGET_PACKAGES = []", source)
|
||||||
|
self.assertIn(package["package_sha256"], source)
|
||||||
|
self.assertIn("build_target_weight_plan", source)
|
||||||
|
joinquant_source = (
|
||||||
|
output / "joinquant_strategy.py"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
self.assertIn(
|
||||||
|
"QUANT60_JOINQUANT_EVIDENCE_V1 ",
|
||||||
|
joinquant_source,
|
||||||
|
)
|
||||||
|
self.assertIn("CURRENT_DATA_DAY_OPEN", joinquant_source)
|
||||||
|
self.assertIn("day_open", joinquant_source)
|
||||||
|
self.assertNotIn("CURRENT_DATA_LAST_PRICE", joinquant_source)
|
||||||
|
for event in manifest["joinquant_observation"]["events"]:
|
||||||
|
self.assertIn(event, joinquant_source)
|
||||||
|
|
||||||
|
tampered = dict(package)
|
||||||
|
tampered["release"] = "tampered"
|
||||||
|
package_path.write_text(
|
||||||
|
json.dumps(tampered),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(ValueError, "hash mismatch"):
|
||||||
|
bundle_all(ROOT, root / "tampered", package_path)
|
||||||
|
|
||||||
|
|
||||||
class CapabilityProbeTest(unittest.TestCase):
|
class CapabilityProbeTest(unittest.TestCase):
|
||||||
def test_shallow_probe_does_not_claim_external_verification(self):
|
def test_shallow_probe_does_not_claim_external_verification(self):
|
||||||
|
|||||||
@@ -0,0 +1,687 @@
|
|||||||
|
import base64
|
||||||
|
import copy
|
||||||
|
import contextlib
|
||||||
|
import datetime
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "src"))
|
||||||
|
|
||||||
|
from platforms import joinquant_strategy, qmt_builtin_strategy
|
||||||
|
from platforms.fake_joinquant_harness import (
|
||||||
|
FakeJoinQuantHarness,
|
||||||
|
FakePosition,
|
||||||
|
)
|
||||||
|
from platforms.fake_qmt_harness import FakeQmtContext, FakeQmtHarness
|
||||||
|
from quant60 import portable_core
|
||||||
|
from quant60.portable_core import target_package_tape_sha256
|
||||||
|
from quant60.target_package import seal_target_package
|
||||||
|
|
||||||
|
|
||||||
|
def _package(
|
||||||
|
signal_as_of="2026-07-17T15:00:00+08:00",
|
||||||
|
next_session="2026-07-20",
|
||||||
|
package_id="TP-20260717-A",
|
||||||
|
decision_id="D-20260717-A",
|
||||||
|
):
|
||||||
|
return seal_target_package(
|
||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"package_type": "TargetPackageV1",
|
||||||
|
"package_id": package_id,
|
||||||
|
"decision_id": decision_id,
|
||||||
|
"release": "quant60-baseline-60-v1",
|
||||||
|
"evidence_class": "provider-snapshot-local",
|
||||||
|
"model_id": "ridge-alpha-v1",
|
||||||
|
"signal_as_of": signal_as_of,
|
||||||
|
"next_session": next_session,
|
||||||
|
"model_sha256": "1" * 64,
|
||||||
|
"data_sha256": "2" * 64,
|
||||||
|
"feature_sha256": "3" * 64,
|
||||||
|
"risk_sha256": "4" * 64,
|
||||||
|
"cost_sha256": "5" * 64,
|
||||||
|
"optimizer_sha256": "6" * 64,
|
||||||
|
"config_sha256": "7" * 64,
|
||||||
|
"source_sha256": "8" * 64,
|
||||||
|
"target_weights": {
|
||||||
|
"000001.XSHE": 0.30,
|
||||||
|
"600000.XSHG": 0.0,
|
||||||
|
},
|
||||||
|
"universe": {
|
||||||
|
"000001.XSHE": {
|
||||||
|
"state": "openable",
|
||||||
|
"reasons": ["ELIGIBLE"],
|
||||||
|
},
|
||||||
|
"600000.XSHG": {
|
||||||
|
"state": "sell_only",
|
||||||
|
"reasons": ["RISK_REDUCTION"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"constraint_state": "FEASIBLE",
|
||||||
|
"long_only": True,
|
||||||
|
"max_single_weight": 0.40,
|
||||||
|
"max_gross_weight": 0.80,
|
||||||
|
"max_one_way_turnover": 0.80,
|
||||||
|
"actual_gross_weight": 0.30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _jq_histories():
|
||||||
|
return {
|
||||||
|
"000001.XSHE": [
|
||||||
|
10.0 + 0.1 * index for index in range(21)
|
||||||
|
],
|
||||||
|
"600000.XSHG": [
|
||||||
|
15.0 - 0.02 * index for index in range(21)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _qmt_histories():
|
||||||
|
return {
|
||||||
|
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
|
||||||
|
for symbol, values in _jq_histories().items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _target_config(base, packages):
|
||||||
|
config = dict(base)
|
||||||
|
config.update(
|
||||||
|
{
|
||||||
|
"decision_mode": "target_package",
|
||||||
|
"target_package_count": len(packages),
|
||||||
|
"target_package_tape_sha256": target_package_tape_sha256(
|
||||||
|
packages
|
||||||
|
),
|
||||||
|
"universe_mode": "fixed",
|
||||||
|
"universe": sorted(
|
||||||
|
{
|
||||||
|
symbol
|
||||||
|
for package in packages
|
||||||
|
for symbol in package["universe"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence_records(output):
|
||||||
|
prefix = joinquant_strategy.EVIDENCE_PREFIX
|
||||||
|
records = []
|
||||||
|
for line in output.getvalue().splitlines():
|
||||||
|
if not line.startswith(prefix):
|
||||||
|
continue
|
||||||
|
encoded = line[len(prefix) :]
|
||||||
|
record = json.loads(encoded)
|
||||||
|
canonical = json.dumps(
|
||||||
|
record,
|
||||||
|
ensure_ascii=True,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
)
|
||||||
|
if canonical != encoded:
|
||||||
|
raise AssertionError("JoinQuant evidence is not canonical JSON")
|
||||||
|
records.append(record)
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformTargetPackageExecutionTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.jq_config = joinquant_strategy.CONFIG
|
||||||
|
self.jq_packages = joinquant_strategy.TARGET_PACKAGES
|
||||||
|
self.qmt_config = qmt_builtin_strategy.CONFIG
|
||||||
|
self.qmt_packages = qmt_builtin_strategy.TARGET_PACKAGES
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
joinquant_strategy.CONFIG = self.jq_config
|
||||||
|
joinquant_strategy.TARGET_PACKAGES = self.jq_packages
|
||||||
|
qmt_builtin_strategy.CONFIG = self.qmt_config
|
||||||
|
qmt_builtin_strategy.TARGET_PACKAGES = self.qmt_packages
|
||||||
|
|
||||||
|
def _install_package(self, package):
|
||||||
|
self._install_packages([package])
|
||||||
|
|
||||||
|
def _install_packages(self, packages):
|
||||||
|
packages = copy.deepcopy(packages)
|
||||||
|
joinquant_strategy.TARGET_PACKAGES = copy.deepcopy(packages)
|
||||||
|
joinquant_strategy.CONFIG = _target_config(
|
||||||
|
self.jq_config,
|
||||||
|
packages,
|
||||||
|
)
|
||||||
|
qmt_builtin_strategy.TARGET_PACKAGES = copy.deepcopy(packages)
|
||||||
|
qmt_config = _target_config(self.qmt_config, packages)
|
||||||
|
qmt_config["universe"] = [
|
||||||
|
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH")
|
||||||
|
for symbol in qmt_config["universe"]
|
||||||
|
]
|
||||||
|
qmt_builtin_strategy.CONFIG = qmt_config
|
||||||
|
|
||||||
|
def test_same_package_binds_same_weights_orders_and_lineage(self):
|
||||||
|
package = _package()
|
||||||
|
self._install_package(package)
|
||||||
|
jq = FakeJoinQuantHarness(
|
||||||
|
_jq_histories(),
|
||||||
|
positions={
|
||||||
|
"600000.XSHG": FakePosition(
|
||||||
|
total_amount=1050,
|
||||||
|
closeable_amount=200,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
jq_plan = jq.run_scheduled()
|
||||||
|
|
||||||
|
qmt_position = SimpleNamespace(
|
||||||
|
stock_code="600000.SH",
|
||||||
|
volume=1050,
|
||||||
|
can_use_volume=200,
|
||||||
|
)
|
||||||
|
context = FakeQmtContext(
|
||||||
|
_qmt_histories(),
|
||||||
|
positions=[qmt_position],
|
||||||
|
bar_time=datetime.datetime(2026, 7, 17, 15, 0),
|
||||||
|
)
|
||||||
|
qmt = FakeQmtHarness(context)
|
||||||
|
qmt_plan = qmt.run(qmt_builtin_strategy)
|
||||||
|
|
||||||
|
self.assertIsNotNone(jq_plan)
|
||||||
|
self.assertIsNotNone(qmt_plan)
|
||||||
|
for field in (
|
||||||
|
"weights",
|
||||||
|
"targets",
|
||||||
|
"orders",
|
||||||
|
"lineage",
|
||||||
|
"universe",
|
||||||
|
):
|
||||||
|
self.assertEqual(jq_plan[field], qmt_plan[field])
|
||||||
|
self.assertEqual(
|
||||||
|
jq_plan["lineage"],
|
||||||
|
{
|
||||||
|
"package_id": package["package_id"],
|
||||||
|
"package_sha256": package["package_sha256"],
|
||||||
|
"decision_id": package["decision_id"],
|
||||||
|
"signal_as_of": package["signal_as_of"],
|
||||||
|
"next_session": package["next_session"],
|
||||||
|
"release": package["release"],
|
||||||
|
"model_id": package["model_id"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
jq_plan["execution_contract"],
|
||||||
|
{
|
||||||
|
"signal_price_source": "CURRENT_DATA_DAY_OPEN",
|
||||||
|
"submit_trigger": "DECLARED_NEXT_SESSION_OPEN_CALLBACK",
|
||||||
|
"declared_next_session": package["next_session"],
|
||||||
|
"real_platform_observed": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
qmt_plan["execution_contract"],
|
||||||
|
{
|
||||||
|
"signal_price_source": "COMPLETED_DAILY_CLOSE",
|
||||||
|
"submit_trigger": "NEXT_BAR_FIRST_TICK",
|
||||||
|
"quick_trade": 0,
|
||||||
|
"declared_next_session": package["next_session"],
|
||||||
|
"real_platform_observed": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(jq_plan["orders"]["600000.XSHG"], -200)
|
||||||
|
self.assertEqual(dict(jq.orders)["600000.XSHG"], 850)
|
||||||
|
self.assertEqual(qmt_plan["orders"]["600000.XSHG"], -200)
|
||||||
|
sell_order = [
|
||||||
|
order
|
||||||
|
for order in qmt.orders
|
||||||
|
if order["order_code"] == "600000.SH"
|
||||||
|
][0]
|
||||||
|
self.assertEqual(sell_order["volume"], 200)
|
||||||
|
self.assertEqual(sell_order["quick_trade"], 0)
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
int(quantity) % 100 == 0
|
||||||
|
for quantity in jq_plan["targets"].values()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_missing_exact_clock_is_noop_without_momentum_fallback(self):
|
||||||
|
package = _package(
|
||||||
|
signal_as_of="2026-07-10T15:00:00+08:00",
|
||||||
|
next_session="2026-07-13",
|
||||||
|
)
|
||||||
|
self._install_package(package)
|
||||||
|
jq = FakeJoinQuantHarness(_jq_histories())
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
self.assertIsNone(jq.run_scheduled())
|
||||||
|
self.assertEqual(jq.orders, [])
|
||||||
|
self.assertIsNone(jq.last_index_request)
|
||||||
|
self.assertIsNone(jq.last_extras_request)
|
||||||
|
self.assertIsNone(jq.last_history_request)
|
||||||
|
|
||||||
|
context = FakeQmtContext(
|
||||||
|
_qmt_histories(),
|
||||||
|
bar_time=datetime.datetime(2026, 7, 17, 15, 0),
|
||||||
|
)
|
||||||
|
qmt = FakeQmtHarness(context)
|
||||||
|
self.assertIsNone(qmt.run(qmt_builtin_strategy))
|
||||||
|
self.assertEqual(qmt.orders, [])
|
||||||
|
self.assertIsNone(context.last_sector_request)
|
||||||
|
self.assertIsNone(context.last_market_request)
|
||||||
|
|
||||||
|
def test_tampered_package_fails_closed_during_initialization(self):
|
||||||
|
package = _package()
|
||||||
|
tampered = copy.deepcopy(package)
|
||||||
|
tampered["target_weights"]["000001.XSHE"] = 0.25
|
||||||
|
tampered["constraints"]["actual_gross_weight"] = 0.25
|
||||||
|
self._install_package(tampered)
|
||||||
|
|
||||||
|
jq = FakeJoinQuantHarness(_jq_histories())
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
self.assertEqual(jq.orders, [])
|
||||||
|
|
||||||
|
context = FakeQmtContext(_qmt_histories())
|
||||||
|
qmt = FakeQmtHarness(context)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
qmt.run(qmt_builtin_strategy)
|
||||||
|
self.assertEqual(qmt.orders, [])
|
||||||
|
|
||||||
|
def test_duplicate_package_id_fails_closed_during_initialization(self):
|
||||||
|
first = _package()
|
||||||
|
second = _package(
|
||||||
|
signal_as_of="2026-07-24T15:00:00+08:00",
|
||||||
|
next_session="2026-07-27",
|
||||||
|
package_id=first["package_id"],
|
||||||
|
decision_id="D-20260724-B",
|
||||||
|
)
|
||||||
|
self._install_packages([first, second])
|
||||||
|
|
||||||
|
jq = FakeJoinQuantHarness(_jq_histories())
|
||||||
|
with self.assertRaisesRegex(ValueError, "duplicate"):
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
|
||||||
|
context = FakeQmtContext(_qmt_histories())
|
||||||
|
qmt = FakeQmtHarness(context)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
qmt_builtin_strategy.UnsupportedStrategyPeriod,
|
||||||
|
"duplicate",
|
||||||
|
):
|
||||||
|
qmt.run(qmt_builtin_strategy)
|
||||||
|
|
||||||
|
def test_joinquant_emits_canonical_observation_stream_and_fail_soft_eod(self):
|
||||||
|
package = _package()
|
||||||
|
self._install_package(package)
|
||||||
|
jq = FakeJoinQuantHarness(
|
||||||
|
_jq_histories(),
|
||||||
|
positions={
|
||||||
|
"600000.XSHG": FakePosition(
|
||||||
|
total_amount=1050,
|
||||||
|
closeable_amount=200,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
plan = jq.run_scheduled()
|
||||||
|
joinquant_strategy.after_trading_end(jq.context)
|
||||||
|
self.assertIsNotNone(plan)
|
||||||
|
|
||||||
|
records = _evidence_records(output)
|
||||||
|
events = [record["event"] for record in records]
|
||||||
|
for event in (
|
||||||
|
"INIT",
|
||||||
|
"TARGET_PACKAGE_HIT",
|
||||||
|
"PLATFORM_BINDING_INPUT",
|
||||||
|
"TARGET_PACKAGE_PLAN",
|
||||||
|
"ORDER_REQUEST",
|
||||||
|
"ORDER_RETURN",
|
||||||
|
"EOD_STATUS",
|
||||||
|
):
|
||||||
|
self.assertIn(event, events)
|
||||||
|
self.assertEqual(
|
||||||
|
[record["sequence"] for record in records],
|
||||||
|
list(range(1, len(records) + 1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
binding = [
|
||||||
|
record["payload"]
|
||||||
|
for record in records
|
||||||
|
if record["event"] == "PLATFORM_BINDING_INPUT"
|
||||||
|
][0]
|
||||||
|
self.assertEqual(binding["equity"], 1_000_000.0)
|
||||||
|
self.assertEqual(binding["current"], {"600000.XSHG": 1050})
|
||||||
|
self.assertEqual(binding["sellable"], {"600000.XSHG": 200})
|
||||||
|
self.assertEqual(
|
||||||
|
binding["price_source"],
|
||||||
|
"CURRENT_DATA_DAY_OPEN",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
binding["lineage"]["package_sha256"],
|
||||||
|
package["package_sha256"],
|
||||||
|
)
|
||||||
|
|
||||||
|
plan_record = [
|
||||||
|
record["payload"]
|
||||||
|
for record in records
|
||||||
|
if record["event"] == "TARGET_PACKAGE_PLAN"
|
||||||
|
][0]
|
||||||
|
self.assertEqual(plan_record["weights"], package["target_weights"])
|
||||||
|
self.assertEqual(plan_record["orders"], plan["orders"])
|
||||||
|
|
||||||
|
eod_records = [
|
||||||
|
record
|
||||||
|
for record in records
|
||||||
|
if record["event"] == "EOD_STATUS"
|
||||||
|
]
|
||||||
|
self.assertTrue(eod_records)
|
||||||
|
chunk_count = eod_records[0]["chunk_count"]
|
||||||
|
self.assertEqual(len(eod_records), chunk_count)
|
||||||
|
self.assertEqual(
|
||||||
|
[record["chunk_index"] for record in eod_records],
|
||||||
|
list(range(1, chunk_count + 1)),
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
record["chunk_count"] == chunk_count
|
||||||
|
for record in eod_records
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
record["payload"]["chunk_encoding"]
|
||||||
|
== "base64-canonical-json"
|
||||||
|
for record in eod_records
|
||||||
|
)
|
||||||
|
)
|
||||||
|
encoded = "".join(
|
||||||
|
record["payload"]["content"]
|
||||||
|
for record in eod_records
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
record["payload"]["encoded_length"] == len(encoded)
|
||||||
|
for record in eod_records
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for record in eod_records:
|
||||||
|
line = joinquant_strategy.EVIDENCE_PREFIX + json.dumps(
|
||||||
|
record,
|
||||||
|
ensure_ascii=True,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
)
|
||||||
|
self.assertLess(len(line.encode("utf-8")), 3 * 1024)
|
||||||
|
canonical = base64.b64decode(
|
||||||
|
encoded.encode("ascii"),
|
||||||
|
validate=True,
|
||||||
|
)
|
||||||
|
eod = json.loads(canonical.decode("ascii"))
|
||||||
|
self.assertEqual(
|
||||||
|
base64.b64encode(
|
||||||
|
json.dumps(
|
||||||
|
eod,
|
||||||
|
ensure_ascii=True,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("ascii")
|
||||||
|
).decode("ascii"),
|
||||||
|
encoded,
|
||||||
|
)
|
||||||
|
self.assertFalse(eod["orders_api"]["available"])
|
||||||
|
self.assertFalse(eod["trades_api"]["available"])
|
||||||
|
self.assertTrue(eod["portfolio"]["available"])
|
||||||
|
self.assertEqual(
|
||||||
|
eod["last_plan"]["lineage"]["package_id"],
|
||||||
|
package["package_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_joinquant_target_package_binds_day_open_not_last_price(self):
|
||||||
|
package = _package()
|
||||||
|
self._install_package(package)
|
||||||
|
jq = FakeJoinQuantHarness(_jq_histories())
|
||||||
|
jq.current_data["000001.XSHE"].day_open = 20.0
|
||||||
|
jq.current_data["000001.XSHE"].last_price = 999.0
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
plan = jq.run_scheduled()
|
||||||
|
|
||||||
|
self.assertIsNotNone(plan)
|
||||||
|
self.assertEqual(plan["targets"]["000001.XSHE"], 15_000)
|
||||||
|
self.assertEqual(jq.orders, [("000001.XSHE", 15_000)])
|
||||||
|
binding = [
|
||||||
|
record["payload"]
|
||||||
|
for record in _evidence_records(output)
|
||||||
|
if record["event"] == "PLATFORM_BINDING_INPUT"
|
||||||
|
][0]
|
||||||
|
self.assertEqual(binding["price_source"], "CURRENT_DATA_DAY_OPEN")
|
||||||
|
self.assertEqual(binding["prices"]["000001.XSHE"], 20.0)
|
||||||
|
self.assertIsNone(jq.last_history_request)
|
||||||
|
|
||||||
|
def test_joinquant_missing_day_open_fails_closed_without_fallback(self):
|
||||||
|
package = _package()
|
||||||
|
self._install_package(package)
|
||||||
|
jq = FakeJoinQuantHarness(_jq_histories())
|
||||||
|
del jq.current_data["000001.XSHE"].day_open
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
joinquant_strategy.PriceHistoryUnavailable,
|
||||||
|
"day_open",
|
||||||
|
):
|
||||||
|
jq.run_scheduled()
|
||||||
|
|
||||||
|
self.assertEqual(jq.orders, [])
|
||||||
|
self.assertIsNone(jq.last_index_request)
|
||||||
|
self.assertIsNone(jq.last_extras_request)
|
||||||
|
self.assertIsNone(jq.last_history_request)
|
||||||
|
events = [
|
||||||
|
record["event"] for record in _evidence_records(output)
|
||||||
|
]
|
||||||
|
self.assertIn("TARGET_PACKAGE_HIT", events)
|
||||||
|
self.assertNotIn("PLATFORM_BINDING_INPUT", events)
|
||||||
|
self.assertNotIn("TARGET_PACKAGE_PLAN", events)
|
||||||
|
self.assertNotIn("ORDER_REQUEST", events)
|
||||||
|
|
||||||
|
def test_joinquant_noop_eod_clears_prior_session_evidence(self):
|
||||||
|
package = _package()
|
||||||
|
self._install_package(package)
|
||||||
|
jq = FakeJoinQuantHarness(
|
||||||
|
_jq_histories(),
|
||||||
|
positions={
|
||||||
|
"600000.XSHG": FakePosition(
|
||||||
|
total_amount=1050,
|
||||||
|
closeable_amount=200,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
jq.current_data["000001.XSHE"].paused = True
|
||||||
|
|
||||||
|
def eod_payload(output):
|
||||||
|
chunks = [
|
||||||
|
record
|
||||||
|
for record in _evidence_records(output)
|
||||||
|
if record["event"] == "EOD_STATUS"
|
||||||
|
]
|
||||||
|
self.assertTrue(chunks)
|
||||||
|
self.assertEqual(
|
||||||
|
[record["chunk_index"] for record in chunks],
|
||||||
|
list(range(1, chunks[0]["chunk_count"] + 1)),
|
||||||
|
)
|
||||||
|
encoded = "".join(
|
||||||
|
record["payload"]["content"] for record in chunks
|
||||||
|
)
|
||||||
|
return json.loads(
|
||||||
|
base64.b64decode(
|
||||||
|
encoded.encode("ascii"),
|
||||||
|
validate=True,
|
||||||
|
).decode("ascii")
|
||||||
|
)
|
||||||
|
|
||||||
|
hit_output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(hit_output):
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
self.assertIsNotNone(jq.run_scheduled())
|
||||||
|
joinquant_strategy.after_trading_end(jq.context)
|
||||||
|
hit_eod = eod_payload(hit_output)
|
||||||
|
self.assertIsNotNone(hit_eod["last_plan"])
|
||||||
|
self.assertTrue(hit_eod["order_returns"])
|
||||||
|
self.assertTrue(hit_eod["skipped_orders"])
|
||||||
|
orders_after_hit = list(jq.orders)
|
||||||
|
self.assertTrue(orders_after_hit)
|
||||||
|
|
||||||
|
for unused_day in range(2):
|
||||||
|
del unused_day
|
||||||
|
jq.advance_session()
|
||||||
|
noop_output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(noop_output):
|
||||||
|
self.assertIsNone(jq.run_scheduled())
|
||||||
|
joinquant_strategy.after_trading_end(jq.context)
|
||||||
|
noop_records = _evidence_records(noop_output)
|
||||||
|
self.assertIn(
|
||||||
|
"TARGET_PACKAGE_NOOP",
|
||||||
|
[record["event"] for record in noop_records],
|
||||||
|
)
|
||||||
|
noop_eod = eod_payload(noop_output)
|
||||||
|
self.assertIsNone(noop_eod["last_plan"])
|
||||||
|
self.assertEqual(noop_eod["order_returns"], [])
|
||||||
|
self.assertEqual(noop_eod["skipped_orders"], [])
|
||||||
|
self.assertEqual(jq.orders, orders_after_hit)
|
||||||
|
|
||||||
|
def test_joinquant_hostile_any_still_runs_final_three_day_tape(self):
|
||||||
|
bundled = types.ModuleType("hostile_joinquant_bundle")
|
||||||
|
source = (
|
||||||
|
ROOT / "dist/target-package/joinquant_strategy.py"
|
||||||
|
).read_bytes()
|
||||||
|
exec(
|
||||||
|
compile(
|
||||||
|
source,
|
||||||
|
"dist/target-package/joinquant_strategy.py",
|
||||||
|
"exec",
|
||||||
|
),
|
||||||
|
bundled.__dict__,
|
||||||
|
)
|
||||||
|
package = copy.deepcopy(bundled.TARGET_PACKAGES[0])
|
||||||
|
histories = {
|
||||||
|
symbol: [10.0] * 21 for symbol in package["universe"]
|
||||||
|
}
|
||||||
|
hostile_any = lambda values: True
|
||||||
|
|
||||||
|
def run_three_days(module):
|
||||||
|
jq = FakeJoinQuantHarness(histories)
|
||||||
|
jq.context.current_dt = datetime.datetime(
|
||||||
|
2024,
|
||||||
|
3,
|
||||||
|
11,
|
||||||
|
9,
|
||||||
|
30,
|
||||||
|
)
|
||||||
|
jq.context.previous_date = datetime.date(2024, 3, 8)
|
||||||
|
daily_orders = []
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
jq.initialize(module)
|
||||||
|
for day in range(3):
|
||||||
|
before = len(jq.orders)
|
||||||
|
jq.run_scheduled()
|
||||||
|
daily_orders.append(len(jq.orders) - before)
|
||||||
|
if day < 2:
|
||||||
|
jq.advance_session()
|
||||||
|
self.assertEqual(daily_orders, [0, 4, 0])
|
||||||
|
|
||||||
|
with self.subTest(artifact="source-wrapper"):
|
||||||
|
self._install_package(package)
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
portable_core,
|
||||||
|
"any",
|
||||||
|
hostile_any,
|
||||||
|
create=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
joinquant_strategy,
|
||||||
|
"any",
|
||||||
|
hostile_any,
|
||||||
|
create=True,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
run_three_days(joinquant_strategy)
|
||||||
|
|
||||||
|
with self.subTest(artifact="final-target-bundle"):
|
||||||
|
bundled.any = hostile_any
|
||||||
|
self.assertEqual(
|
||||||
|
bundled.TARGET_PACKAGES[0]["package_sha256"],
|
||||||
|
package["package_sha256"],
|
||||||
|
)
|
||||||
|
run_three_days(bundled)
|
||||||
|
|
||||||
|
def test_joinquant_emits_noop_and_skipped_order(self):
|
||||||
|
missing = _package(
|
||||||
|
signal_as_of="2026-07-10T15:00:00+08:00",
|
||||||
|
next_session="2026-07-13",
|
||||||
|
)
|
||||||
|
self._install_package(missing)
|
||||||
|
jq = FakeJoinQuantHarness(_jq_histories())
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
self.assertIsNone(jq.run_scheduled())
|
||||||
|
events = [
|
||||||
|
record["event"] for record in _evidence_records(output)
|
||||||
|
]
|
||||||
|
self.assertIn("TARGET_PACKAGE_NOOP", events)
|
||||||
|
self.assertNotIn("TARGET_PACKAGE_PLAN", events)
|
||||||
|
self.assertNotIn("ORDER_REQUEST", events)
|
||||||
|
|
||||||
|
package = _package()
|
||||||
|
self._install_package(package)
|
||||||
|
jq = FakeJoinQuantHarness(_jq_histories())
|
||||||
|
jq.current_data["000001.XSHE"].paused = True
|
||||||
|
output = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(output):
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
plan = jq.run_scheduled()
|
||||||
|
self.assertIsNotNone(plan)
|
||||||
|
records = _evidence_records(output)
|
||||||
|
skipped = [
|
||||||
|
record["payload"]
|
||||||
|
for record in records
|
||||||
|
if record["event"] == "SKIPPED_ORDER"
|
||||||
|
]
|
||||||
|
self.assertEqual(len(skipped), 1)
|
||||||
|
self.assertEqual(skipped[0]["skip"]["reason"], "paused")
|
||||||
|
self.assertEqual(jq.orders, [])
|
||||||
|
|
||||||
|
def test_joinquant_observation_failure_does_not_change_order_path(self):
|
||||||
|
package = _package()
|
||||||
|
self._install_package(package)
|
||||||
|
jq = FakeJoinQuantHarness(_jq_histories())
|
||||||
|
with mock.patch("builtins.print", side_effect=RuntimeError("log down")):
|
||||||
|
jq.initialize(joinquant_strategy)
|
||||||
|
plan = jq.run_scheduled()
|
||||||
|
joinquant_strategy.after_trading_end(jq.context)
|
||||||
|
self.assertIsNotNone(plan)
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(jq.orders),
|
||||||
|
[("000001.XSHE", 25000)],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import copy
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from quant60.baseline_pipeline import (
|
||||||
|
AlphaForecastInput,
|
||||||
|
BaselinePipelineError,
|
||||||
|
BaselinePipelinePolicy,
|
||||||
|
CostForecastInput,
|
||||||
|
FIVE_LAYER_ORDER,
|
||||||
|
RiskForecastInput,
|
||||||
|
SecurityStateInput,
|
||||||
|
build_baseline_pipeline,
|
||||||
|
verify_layer_trace,
|
||||||
|
)
|
||||||
|
from quant60.optimizer import PortfolioConstraints
|
||||||
|
from quant60.universe import UniverseState
|
||||||
|
|
||||||
|
|
||||||
|
SYMBOLS = (
|
||||||
|
"600000.XSHG",
|
||||||
|
"000001.XSHE",
|
||||||
|
"000333.XSHE",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def inputs(*, first_variance=0.03, first_cost_bps=8.0):
|
||||||
|
alpha = [
|
||||||
|
AlphaForecastInput(
|
||||||
|
SYMBOLS[0],
|
||||||
|
expected_excess_return=0.030,
|
||||||
|
model_version="ridge-frozen-fixture-v1",
|
||||||
|
feature_version="transparent-price-v1",
|
||||||
|
),
|
||||||
|
AlphaForecastInput(
|
||||||
|
SYMBOLS[1],
|
||||||
|
expected_excess_return=0.020,
|
||||||
|
model_version="ridge-frozen-fixture-v1",
|
||||||
|
feature_version="transparent-price-v1",
|
||||||
|
),
|
||||||
|
AlphaForecastInput(
|
||||||
|
SYMBOLS[2],
|
||||||
|
expected_excess_return=0.010,
|
||||||
|
model_version="ridge-frozen-fixture-v1",
|
||||||
|
feature_version="transparent-price-v1",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
risk = [
|
||||||
|
RiskForecastInput(
|
||||||
|
SYMBOLS[0],
|
||||||
|
first_variance,
|
||||||
|
"diagonal-risk-fixture-v1",
|
||||||
|
),
|
||||||
|
RiskForecastInput(SYMBOLS[1], 0.03, "diagonal-risk-fixture-v1"),
|
||||||
|
RiskForecastInput(SYMBOLS[2], 0.03, "diagonal-risk-fixture-v1"),
|
||||||
|
]
|
||||||
|
cost = [
|
||||||
|
CostForecastInput(
|
||||||
|
SYMBOLS[0],
|
||||||
|
first_cost_bps,
|
||||||
|
1.0,
|
||||||
|
"dated-cost-fixture-v1",
|
||||||
|
),
|
||||||
|
CostForecastInput(SYMBOLS[1], 8.0, 1.0, "dated-cost-fixture-v1"),
|
||||||
|
CostForecastInput(SYMBOLS[2], 8.0, 1.0, "dated-cost-fixture-v1"),
|
||||||
|
]
|
||||||
|
securities = [
|
||||||
|
SecurityStateInput(
|
||||||
|
SYMBOLS[0],
|
||||||
|
UniverseState.OPENABLE,
|
||||||
|
"bank",
|
||||||
|
10.0,
|
||||||
|
),
|
||||||
|
SecurityStateInput(
|
||||||
|
SYMBOLS[1],
|
||||||
|
UniverseState.OPENABLE,
|
||||||
|
"bank",
|
||||||
|
20.0,
|
||||||
|
),
|
||||||
|
SecurityStateInput(
|
||||||
|
SYMBOLS[2],
|
||||||
|
UniverseState.OPENABLE,
|
||||||
|
"consumer",
|
||||||
|
50.0,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
return alpha, risk, cost, securities
|
||||||
|
|
||||||
|
|
||||||
|
def policy():
|
||||||
|
return BaselinePipelinePolicy(
|
||||||
|
constraints=PortfolioConstraints(
|
||||||
|
gross_target=0.80,
|
||||||
|
single_name_cap=0.80,
|
||||||
|
industry_cap=0.80,
|
||||||
|
one_way_turnover_cap=0.80,
|
||||||
|
risk_aversion=1.0,
|
||||||
|
cost_aversion=1.0,
|
||||||
|
turnover_aversion=0.0,
|
||||||
|
),
|
||||||
|
cash_buffer=0.10,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaselinePipelineTests(unittest.TestCase):
|
||||||
|
def test_canonical_path_emits_exact_five_layer_hash_chain(self):
|
||||||
|
alpha, risk, cost, securities = inputs()
|
||||||
|
result = build_baseline_pipeline(
|
||||||
|
alpha=alpha,
|
||||||
|
risk=risk,
|
||||||
|
cost=cost,
|
||||||
|
securities=securities,
|
||||||
|
equity=1_000_000,
|
||||||
|
policy=policy(),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
tuple(event["layer"] for event in result.layer_trace),
|
||||||
|
FIVE_LAYER_ORDER,
|
||||||
|
)
|
||||||
|
self.assertTrue(result.risk_decision.approved)
|
||||||
|
self.assertTrue(result.order_deltas)
|
||||||
|
verify_layer_trace(result.layer_trace, result.layer_payloads)
|
||||||
|
|
||||||
|
def test_risk_and_cost_are_causal_inputs_to_portfolio(self):
|
||||||
|
alpha, risk, cost, securities = inputs()
|
||||||
|
base = build_baseline_pipeline(
|
||||||
|
alpha=alpha,
|
||||||
|
risk=risk,
|
||||||
|
cost=cost,
|
||||||
|
securities=securities,
|
||||||
|
equity=1_000_000,
|
||||||
|
policy=policy(),
|
||||||
|
)
|
||||||
|
alpha, risk, cost, securities = inputs(first_variance=0.30)
|
||||||
|
high_risk = build_baseline_pipeline(
|
||||||
|
alpha=alpha,
|
||||||
|
risk=risk,
|
||||||
|
cost=cost,
|
||||||
|
securities=securities,
|
||||||
|
equity=1_000_000,
|
||||||
|
policy=policy(),
|
||||||
|
)
|
||||||
|
alpha, risk, cost, securities = inputs(first_cost_bps=250.0)
|
||||||
|
high_cost = build_baseline_pipeline(
|
||||||
|
alpha=alpha,
|
||||||
|
risk=risk,
|
||||||
|
cost=cost,
|
||||||
|
securities=securities,
|
||||||
|
equity=1_000_000,
|
||||||
|
policy=policy(),
|
||||||
|
)
|
||||||
|
symbol = SYMBOLS[0]
|
||||||
|
self.assertLess(
|
||||||
|
high_risk.solution.weights[symbol],
|
||||||
|
base.solution.weights[symbol],
|
||||||
|
)
|
||||||
|
self.assertLess(
|
||||||
|
high_cost.solution.weights[symbol],
|
||||||
|
base.solution.weights[symbol],
|
||||||
|
)
|
||||||
|
self.assertNotEqual(
|
||||||
|
base.layer_trace[2]["event_sha256"],
|
||||||
|
high_risk.layer_trace[2]["event_sha256"],
|
||||||
|
)
|
||||||
|
self.assertNotEqual(
|
||||||
|
base.layer_trace[2]["event_sha256"],
|
||||||
|
high_cost.layer_trace[2]["event_sha256"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_missing_layer_input_fails_closed(self):
|
||||||
|
alpha, risk, cost, securities = inputs()
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
BaselinePipelineError,
|
||||||
|
"symbol sets must match exactly",
|
||||||
|
):
|
||||||
|
build_baseline_pipeline(
|
||||||
|
alpha=alpha,
|
||||||
|
risk=risk[:-1],
|
||||||
|
cost=cost,
|
||||||
|
securities=securities,
|
||||||
|
equity=1_000_000,
|
||||||
|
policy=policy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_smoke_model_cannot_claim_baseline_candidate(self):
|
||||||
|
alpha, risk, cost, securities = inputs()
|
||||||
|
alpha = [
|
||||||
|
AlphaForecastInput(
|
||||||
|
item.symbol,
|
||||||
|
item.expected_excess_return,
|
||||||
|
"portable-momentum-v1",
|
||||||
|
item.feature_version,
|
||||||
|
)
|
||||||
|
for item in alpha
|
||||||
|
]
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
BaselinePipelineError,
|
||||||
|
"platform smoke strategy",
|
||||||
|
):
|
||||||
|
build_baseline_pipeline(
|
||||||
|
alpha=alpha,
|
||||||
|
risk=risk,
|
||||||
|
cost=cost,
|
||||||
|
securities=securities,
|
||||||
|
equity=1_000_000,
|
||||||
|
policy=policy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_layer_trace_tampering_is_rejected(self):
|
||||||
|
alpha, risk, cost, securities = inputs()
|
||||||
|
result = build_baseline_pipeline(
|
||||||
|
alpha=alpha,
|
||||||
|
risk=risk,
|
||||||
|
cost=cost,
|
||||||
|
securities=securities,
|
||||||
|
equity=1_000_000,
|
||||||
|
policy=policy(),
|
||||||
|
)
|
||||||
|
trace = copy.deepcopy(list(result.layer_trace))
|
||||||
|
trace[2]["output_sha256"] = "0" * 64
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
BaselinePipelineError,
|
||||||
|
"output hash mismatch",
|
||||||
|
):
|
||||||
|
verify_layer_trace(trace, result.layer_payloads)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from quant60.ledger import canonical_json
|
||||||
|
from quant60.release_reachability import LAYERS
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(PROJECT))
|
||||||
|
|
||||||
|
from tools.build_release_reachability import ( # noqa: E402
|
||||||
|
DEFAULT_OUTPUT,
|
||||||
|
JOINQUANT_TARGET_PACKAGE_OBSERVED_AT,
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_ID,
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_PATH,
|
||||||
|
PLATFORM_ENTRYPOINTS,
|
||||||
|
RELEASE_ID,
|
||||||
|
build_manifest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildReleaseReachabilityTests(unittest.TestCase):
|
||||||
|
def test_checked_in_manifest_exactly_matches_generator(self):
|
||||||
|
expected = build_manifest(repository_root=PROJECT)
|
||||||
|
observed = json.loads(DEFAULT_OUTPUT.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(observed, expected)
|
||||||
|
self.assertEqual(
|
||||||
|
DEFAULT_OUTPUT.read_bytes(),
|
||||||
|
(canonical_json(expected) + "\n").encode("utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_only_execution_has_real_joinquant_observation(self):
|
||||||
|
manifest = build_manifest(repository_root=PROJECT)
|
||||||
|
self.assertEqual(manifest["release_id"], RELEASE_ID)
|
||||||
|
for layer in LAYERS:
|
||||||
|
claims = manifest["layers"][layer]
|
||||||
|
for declaration in PLATFORM_ENTRYPOINTS.values():
|
||||||
|
claim = claims[declaration["capability"]]
|
||||||
|
self.assertTrue(claim["status"])
|
||||||
|
self.assertEqual(len(claim["artifacts"]), 1)
|
||||||
|
artifact = claim["artifacts"][0]
|
||||||
|
self.assertIn("targetpackagev1", artifact["artifact_id"])
|
||||||
|
self.assertEqual(artifact["path"], declaration["path"])
|
||||||
|
self.assertEqual(artifact["platform"], declaration["platform"])
|
||||||
|
observed = claims["real_platform_observed"]
|
||||||
|
if layer != "execution":
|
||||||
|
self.assertEqual(
|
||||||
|
observed,
|
||||||
|
{"status": False, "artifacts": []},
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
self.assertTrue(observed["status"])
|
||||||
|
self.assertEqual(len(observed["artifacts"]), 1)
|
||||||
|
artifact = observed["artifacts"][0]
|
||||||
|
self.assertEqual(artifact["kind"], "platform_run")
|
||||||
|
self.assertEqual(artifact["platform"], "joinquant")
|
||||||
|
self.assertEqual(
|
||||||
|
artifact["path"],
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_PATH,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
artifact["observed_at"],
|
||||||
|
JOINQUANT_TARGET_PACKAGE_OBSERVED_AT,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
artifact["run_id"],
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_ID,
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
manifest["summary"]["all_layers_real_platform_observed"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_tracked_joinquant_run_preserves_conservative_boundary(self):
|
||||||
|
run = json.loads(
|
||||||
|
(PROJECT / JOINQUANT_TARGET_PACKAGE_RUN_PATH).read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(run["run_id"], JOINQUANT_TARGET_PACKAGE_RUN_ID)
|
||||||
|
self.assertEqual(run["observed_at"], JOINQUANT_TARGET_PACKAGE_OBSERVED_AT)
|
||||||
|
self.assertEqual(
|
||||||
|
run["platform_evidence_class"],
|
||||||
|
"real_platform_runtime",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
run["input_evidence_class"],
|
||||||
|
"synthetic-research",
|
||||||
|
)
|
||||||
|
self.assertFalse(run["performance_claim"])
|
||||||
|
self.assertEqual(
|
||||||
|
run["target_package"]["evidence_class"],
|
||||||
|
"synthetic-research",
|
||||||
|
)
|
||||||
|
self.assertEqual(run["counts"], {
|
||||||
|
"orders": 3,
|
||||||
|
"fills": 3,
|
||||||
|
"skipped_orders": 0,
|
||||||
|
})
|
||||||
|
boundary = run["claim_boundary"]
|
||||||
|
self.assertEqual(
|
||||||
|
boundary["real_platform_observed_layers"],
|
||||||
|
["execution"],
|
||||||
|
)
|
||||||
|
self.assertFalse(boundary["upstream_layers_real_platform_observed"])
|
||||||
|
self.assertFalse(boundary["qmt_peer_observed"])
|
||||||
|
self.assertFalse(boundary["same_input_cross_engine_parity_observed"])
|
||||||
|
self.assertFalse(boundary["all_five_layers_real_platform_observed"])
|
||||||
|
self.assertFalse(boundary["g9_passed"])
|
||||||
|
self.assertFalse(boundary["baseline_60_passed"])
|
||||||
|
self.assertFalse(boundary["investment_value_claim"])
|
||||||
|
|
||||||
|
def test_check_mode_verifies_current_file(self):
|
||||||
|
completed = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(PROJECT / "tools/build_release_reachability.py"),
|
||||||
|
"--check",
|
||||||
|
],
|
||||||
|
cwd=PROJECT,
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(completed.returncode, 0, completed.stderr)
|
||||||
|
result = json.loads(completed.stdout)
|
||||||
|
self.assertTrue(result["ok"])
|
||||||
|
self.assertTrue(result["checked"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -19,6 +19,22 @@ class ResearchExperimentTests(unittest.TestCase):
|
|||||||
self.assertGreater(first["report"]["fold_count"], 0)
|
self.assertGreater(first["report"]["fold_count"], 0)
|
||||||
self.assertFalse(first["report"]["investment_value_claim"])
|
self.assertFalse(first["report"]["investment_value_claim"])
|
||||||
self.assertTrue(first["targets"])
|
self.assertTrue(first["targets"])
|
||||||
|
self.assertLess(
|
||||||
|
first["report"]["final_training_max_label_exit"],
|
||||||
|
first["report"]["target_date"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
first["target_package"]["model_sha256"],
|
||||||
|
first["model_bundle"]["bundle_sha256"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
[item["layer"] for item in first["pipeline"]["layer_trace"]],
|
||||||
|
["universe", "alpha", "portfolio", "risk", "execution"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
first["target_package"]["target_weights"],
|
||||||
|
first["pipeline"]["weights"],
|
||||||
|
)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as left_dir, tempfile.TemporaryDirectory() as right_dir:
|
with tempfile.TemporaryDirectory() as left_dir, tempfile.TemporaryDirectory() as right_dir:
|
||||||
left = write_research_artifacts(first, left_dir)
|
left = write_research_artifacts(first, left_dir)
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from quant60.features import TrainOnlyFeaturePreprocessor
|
||||||
|
from quant60.ledger import canonical_json
|
||||||
|
from quant60.model_bundle import (
|
||||||
|
ModelBundleIntegrityError,
|
||||||
|
create_model_bundle,
|
||||||
|
load_verified_model_bundle,
|
||||||
|
model_bundle_sha256,
|
||||||
|
verify_model_bundle,
|
||||||
|
write_model_bundle,
|
||||||
|
)
|
||||||
|
from quant60.research import DeterministicRidge
|
||||||
|
|
||||||
|
|
||||||
|
HASHES = {
|
||||||
|
"data_sha256": hashlib.sha256(b"training-data").hexdigest(),
|
||||||
|
"source_sha256": hashlib.sha256(b"source-tree").hexdigest(),
|
||||||
|
"config_sha256": hashlib.sha256(b"training-config").hexdigest(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ModelBundleTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.training_rows = [
|
||||||
|
{"momentum": -0.2, "volatility": 0.10},
|
||||||
|
{"momentum": -0.1, "volatility": None},
|
||||||
|
{"momentum": 0.0, "volatility": 0.20},
|
||||||
|
{"momentum": 0.1, "volatility": 0.25},
|
||||||
|
{"momentum": 0.2, "volatility": 0.30},
|
||||||
|
]
|
||||||
|
self.processor = TrainOnlyFeaturePreprocessor(
|
||||||
|
winsor_fraction=0.1
|
||||||
|
).fit(self.training_rows)
|
||||||
|
transformed = self.processor.transform(self.training_rows)
|
||||||
|
matrix = [
|
||||||
|
[row[name] for name in self.processor.output_names]
|
||||||
|
for row in transformed
|
||||||
|
]
|
||||||
|
self.model = DeterministicRidge(alpha=0.5).fit(
|
||||||
|
matrix,
|
||||||
|
[-0.03, -0.01, 0.0, 0.02, 0.04],
|
||||||
|
)
|
||||||
|
self.bundle = create_model_bundle(
|
||||||
|
self.processor,
|
||||||
|
self.model,
|
||||||
|
trained_through="2024-12-31",
|
||||||
|
label_name="t_plus_1_open_to_t_plus_6_open_excess",
|
||||||
|
horizon_trading_days=5,
|
||||||
|
**HASHES,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_freezes_preprocessor_model_label_and_lineage(self):
|
||||||
|
verified = verify_model_bundle(self.bundle)
|
||||||
|
self.assertEqual(
|
||||||
|
verified.feature_order,
|
||||||
|
tuple(sorted(self.training_rows[0])),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
verified.output_feature_order,
|
||||||
|
self.processor.output_names,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
verified.coefficients,
|
||||||
|
self.model.coef_,
|
||||||
|
)
|
||||||
|
self.assertEqual(verified.intercept, self.model.intercept_)
|
||||||
|
self.assertEqual(verified.trained_through.isoformat(), "2024-12-31")
|
||||||
|
self.assertEqual(verified.horizon_trading_days, 5)
|
||||||
|
self.assertEqual(verified.data_sha256, HASHES["data_sha256"])
|
||||||
|
|
||||||
|
def test_write_read_and_prediction_replay_are_deterministic(self):
|
||||||
|
replay_rows = [
|
||||||
|
{"volatility": None, "momentum": 0.07},
|
||||||
|
{"momentum": -100.0, "volatility": 100.0},
|
||||||
|
]
|
||||||
|
transformed = self.processor.transform(replay_rows)
|
||||||
|
matrix = [
|
||||||
|
[row[name] for name in self.processor.output_names]
|
||||||
|
for row in transformed
|
||||||
|
]
|
||||||
|
expected = self.model.predict(matrix)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
left = write_model_bundle(
|
||||||
|
self.bundle,
|
||||||
|
Path(directory) / "left.json",
|
||||||
|
)
|
||||||
|
right = write_model_bundle(
|
||||||
|
copy.deepcopy(self.bundle),
|
||||||
|
Path(directory) / "right.json",
|
||||||
|
)
|
||||||
|
self.assertEqual(left.read_bytes(), right.read_bytes())
|
||||||
|
loaded = load_verified_model_bundle(
|
||||||
|
left,
|
||||||
|
expected_bundle_sha256=self.bundle["bundle_sha256"],
|
||||||
|
**{
|
||||||
|
f"expected_{key}": value
|
||||||
|
for key, value in HASHES.items()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(loaded.predict(replay_rows), expected)
|
||||||
|
independently_loaded = load_verified_model_bundle(right)
|
||||||
|
self.assertEqual(
|
||||||
|
loaded.prediction_bytes(replay_rows),
|
||||||
|
independently_loaded.prediction_bytes(replay_rows),
|
||||||
|
)
|
||||||
|
self.assertEqual(loaded.canonical_bytes, left.read_bytes())
|
||||||
|
|
||||||
|
def test_content_tampering_fails_closed(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = write_model_bundle(
|
||||||
|
self.bundle,
|
||||||
|
Path(directory) / "bundle.json",
|
||||||
|
)
|
||||||
|
tampered = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
tampered["model"]["coefficients"][0] += 0.01
|
||||||
|
path.write_text(
|
||||||
|
canonical_json(tampered) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ModelBundleIntegrityError,
|
||||||
|
"SHA-256 mismatch",
|
||||||
|
):
|
||||||
|
load_verified_model_bundle(path)
|
||||||
|
|
||||||
|
def test_resealed_semantic_tampering_fails_closed(self):
|
||||||
|
tampered = copy.deepcopy(self.bundle)
|
||||||
|
tampered["preprocessor"]["output_feature_order"] = list(
|
||||||
|
reversed(tampered["preprocessor"]["output_feature_order"])
|
||||||
|
)
|
||||||
|
tampered["bundle_sha256"] = model_bundle_sha256(tampered)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ModelBundleIntegrityError,
|
||||||
|
"output_feature_order is inconsistent",
|
||||||
|
):
|
||||||
|
verify_model_bundle(tampered)
|
||||||
|
|
||||||
|
def test_expected_identity_rejects_a_valid_but_wrong_bundle(self):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ModelBundleIntegrityError,
|
||||||
|
"data_sha256 does not match expected identity",
|
||||||
|
):
|
||||||
|
verify_model_bundle(
|
||||||
|
self.bundle,
|
||||||
|
expected_data_sha256="f" * 64,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_placeholder_lineage_hash_is_rejected(self):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ModelBundleIntegrityError,
|
||||||
|
"all-zero placeholder",
|
||||||
|
):
|
||||||
|
create_model_bundle(
|
||||||
|
self.processor,
|
||||||
|
self.model,
|
||||||
|
trained_through="2024-12-31",
|
||||||
|
label_name="label",
|
||||||
|
horizon_trading_days=5,
|
||||||
|
data_sha256="0" * 64,
|
||||||
|
source_sha256=HASHES["source_sha256"],
|
||||||
|
config_sha256=HASHES["config_sha256"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unfitted_components_and_non_finite_state_are_rejected(self):
|
||||||
|
with self.assertRaisesRegex(ValueError, "preprocessor is not fitted"):
|
||||||
|
create_model_bundle(
|
||||||
|
TrainOnlyFeaturePreprocessor(),
|
||||||
|
self.model,
|
||||||
|
trained_through="2024-12-31",
|
||||||
|
label_name="label",
|
||||||
|
horizon_trading_days=5,
|
||||||
|
**HASHES,
|
||||||
|
)
|
||||||
|
tampered = copy.deepcopy(self.bundle)
|
||||||
|
tampered["model"]["intercept"] = float("nan")
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ModelBundleIntegrityError,
|
||||||
|
"canonical JSON",
|
||||||
|
):
|
||||||
|
verify_model_bundle(tampered)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,16 +1,99 @@
|
|||||||
|
import ast
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
import unittest
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from quant60.ledger import canonical_json
|
||||||
from quant60.portable_core import (
|
from quant60.portable_core import (
|
||||||
|
TargetPackageLookupMiss,
|
||||||
|
_any_true,
|
||||||
build_rebalance_plan,
|
build_rebalance_plan,
|
||||||
|
build_target_weight_plan,
|
||||||
capped_target_weights,
|
capped_target_weights,
|
||||||
|
lookup_target_package_exact,
|
||||||
momentum_score,
|
momentum_score,
|
||||||
normalize_symbol,
|
normalize_symbol,
|
||||||
order_deltas,
|
order_deltas,
|
||||||
|
target_package_tape_sha256,
|
||||||
target_quantities,
|
target_quantities,
|
||||||
|
verify_target_package,
|
||||||
)
|
)
|
||||||
|
from quant60.target_package import seal_target_package
|
||||||
|
|
||||||
|
|
||||||
|
def sealed_target_package(
|
||||||
|
package_id="TP-20260724-A",
|
||||||
|
decision_id="D-20260724-A",
|
||||||
|
signal_as_of="2026-07-24T15:00:00+08:00",
|
||||||
|
next_session="2026-07-27",
|
||||||
|
):
|
||||||
|
return seal_target_package(
|
||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"package_type": "TargetPackageV1",
|
||||||
|
"package_id": package_id,
|
||||||
|
"decision_id": decision_id,
|
||||||
|
"release": "quant60-baseline-60-v1",
|
||||||
|
"evidence_class": "provider-snapshot-local",
|
||||||
|
"model_id": "alpha158-lightgbm-v1",
|
||||||
|
"signal_as_of": signal_as_of,
|
||||||
|
"next_session": next_session,
|
||||||
|
"model_sha256": "1" * 64,
|
||||||
|
"data_sha256": "2" * 64,
|
||||||
|
"feature_sha256": "3" * 64,
|
||||||
|
"risk_sha256": "4" * 64,
|
||||||
|
"cost_sha256": "5" * 64,
|
||||||
|
"optimizer_sha256": "6" * 64,
|
||||||
|
"config_sha256": "7" * 64,
|
||||||
|
"source_sha256": "8" * 64,
|
||||||
|
"target_weights": {
|
||||||
|
"000001.XSHE": 0.30,
|
||||||
|
"600000.XSHG": 0.20,
|
||||||
|
},
|
||||||
|
"universe": {
|
||||||
|
"000001.XSHE": {
|
||||||
|
"state": "openable",
|
||||||
|
"reasons": ["ELIGIBLE"],
|
||||||
|
},
|
||||||
|
"600000.XSHG": {
|
||||||
|
"state": "hold_only",
|
||||||
|
"reasons": ["LIQUIDITY_BELOW_FLOOR"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"constraint_state": "FEASIBLE",
|
||||||
|
"long_only": True,
|
||||||
|
"max_single_weight": 0.40,
|
||||||
|
"max_gross_weight": 0.80,
|
||||||
|
"max_one_way_turnover": 0.20,
|
||||||
|
"actual_gross_weight": 0.50,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PortableCoreTests(unittest.TestCase):
|
class PortableCoreTests(unittest.TestCase):
|
||||||
|
def test_private_any_true_preserves_builtin_semantics(self):
|
||||||
|
for values in (
|
||||||
|
[],
|
||||||
|
[False, 0, None, ""],
|
||||||
|
[False, 0, "truthy"],
|
||||||
|
[1],
|
||||||
|
):
|
||||||
|
with self.subTest(values=values):
|
||||||
|
self.assertEqual(_any_true(iter(values)), any(values))
|
||||||
|
|
||||||
|
visited = []
|
||||||
|
|
||||||
|
def values():
|
||||||
|
for value in (False, True, "must-not-be-read"):
|
||||||
|
visited.append(value)
|
||||||
|
yield value
|
||||||
|
|
||||||
|
self.assertTrue(_any_true(values()))
|
||||||
|
self.assertEqual(visited, [False, True])
|
||||||
|
|
||||||
def test_symbol_round_trip(self):
|
def test_symbol_round_trip(self):
|
||||||
inputs = [
|
inputs = [
|
||||||
("600000.XSHG", "600000.SH", "SH600000"),
|
("600000.XSHG", "600000.SH", "SH600000"),
|
||||||
@@ -166,6 +249,255 @@ class PortableCoreTests(unittest.TestCase):
|
|||||||
build_rebalance_plan(**arguments),
|
build_rebalance_plan(**arguments),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_target_weight_plan_is_causal_and_preserves_lineage(self):
|
||||||
|
common = {
|
||||||
|
"prices": {
|
||||||
|
"600000.SH": 10.0,
|
||||||
|
"000001.SZ": 10.0,
|
||||||
|
},
|
||||||
|
"current": {},
|
||||||
|
"sellable": {},
|
||||||
|
"equity": 100_000,
|
||||||
|
"cash_buffer": 0.02,
|
||||||
|
"lot_size": 100,
|
||||||
|
"decision_id": "D-20260724-A",
|
||||||
|
"package_sha256": "a" * 64,
|
||||||
|
}
|
||||||
|
first = build_target_weight_plan(
|
||||||
|
{
|
||||||
|
"600000.XSHG": 0.60,
|
||||||
|
"000001.XSHE": 0.20,
|
||||||
|
},
|
||||||
|
**common
|
||||||
|
)
|
||||||
|
second = build_target_weight_plan(
|
||||||
|
{
|
||||||
|
"600000.XSHG": 0.20,
|
||||||
|
"000001.XSHE": 0.60,
|
||||||
|
},
|
||||||
|
**common
|
||||||
|
)
|
||||||
|
self.assertNotEqual(first["targets"], second["targets"])
|
||||||
|
self.assertNotEqual(first["orders"], second["orders"])
|
||||||
|
self.assertEqual(first["decision_id"], "D-20260724-A")
|
||||||
|
self.assertEqual(first["package_sha256"], "a" * 64)
|
||||||
|
self.assertNotIn("scores", first)
|
||||||
|
|
||||||
|
def test_target_weight_plan_rejects_alias_duplicates_and_bad_weights(self):
|
||||||
|
common = {
|
||||||
|
"prices": {"600000.XSHG": 10.0},
|
||||||
|
"current": {},
|
||||||
|
"sellable": {},
|
||||||
|
"equity": 100_000,
|
||||||
|
}
|
||||||
|
with self.assertRaisesRegex(ValueError, "duplicate normalized"):
|
||||||
|
build_target_weight_plan(
|
||||||
|
{
|
||||||
|
"600000.SH": 0.20,
|
||||||
|
"600000.XSHG": 0.20,
|
||||||
|
},
|
||||||
|
**common
|
||||||
|
)
|
||||||
|
for weights in (
|
||||||
|
{"600000.XSHG": -0.1},
|
||||||
|
{"600000.XSHG": float("nan")},
|
||||||
|
{"600000.XSHG": 1.01},
|
||||||
|
):
|
||||||
|
with self.subTest(weights=weights):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
build_target_weight_plan(weights, **common)
|
||||||
|
with self.assertRaisesRegex(ValueError, "gross"):
|
||||||
|
build_target_weight_plan(
|
||||||
|
{
|
||||||
|
"600000.XSHG": 0.60,
|
||||||
|
"000001.XSHE": 0.50,
|
||||||
|
},
|
||||||
|
prices={
|
||||||
|
"600000.XSHG": 10.0,
|
||||||
|
"000001.XSHE": 10.0,
|
||||||
|
},
|
||||||
|
current={},
|
||||||
|
sellable={},
|
||||||
|
equity=100_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_target_weight_plan_respects_t1_and_odd_lot_liquidation(self):
|
||||||
|
t1_limited = build_target_weight_plan(
|
||||||
|
{},
|
||||||
|
{"600000.XSHG": 10.0},
|
||||||
|
{"600000.XSHG": 1050},
|
||||||
|
{"600000.XSHG": 250},
|
||||||
|
equity=100_000,
|
||||||
|
)
|
||||||
|
self.assertEqual(t1_limited["weights"], {"600000.XSHG": 0.0})
|
||||||
|
self.assertEqual(t1_limited["targets"], {"600000.XSHG": 0})
|
||||||
|
self.assertEqual(t1_limited["orders"], {"600000.XSHG": -200})
|
||||||
|
|
||||||
|
complete = build_target_weight_plan(
|
||||||
|
{},
|
||||||
|
{"600000.XSHG": 10.0},
|
||||||
|
{"600000.XSHG": 1050},
|
||||||
|
{"600000.XSHG": 1050},
|
||||||
|
equity=100_000,
|
||||||
|
)
|
||||||
|
self.assertEqual(complete["orders"], {"600000.XSHG": -1050})
|
||||||
|
|
||||||
|
unsellable = build_target_weight_plan(
|
||||||
|
{},
|
||||||
|
{"600000.XSHG": 10.0},
|
||||||
|
{"600000.XSHG": 1050},
|
||||||
|
{},
|
||||||
|
equity=100_000,
|
||||||
|
)
|
||||||
|
self.assertEqual(unsellable["orders"], {})
|
||||||
|
|
||||||
|
def test_target_weight_plan_requires_exact_managed_price_coverage(self):
|
||||||
|
with self.assertRaisesRegex(KeyError, "missing"):
|
||||||
|
build_target_weight_plan(
|
||||||
|
{"600000.XSHG": 0.20},
|
||||||
|
{"600000.XSHG": 10.0},
|
||||||
|
{"000001.XSHE": 100},
|
||||||
|
{"000001.XSHE": 100},
|
||||||
|
equity=100_000,
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(ValueError, "unmanaged"):
|
||||||
|
build_target_weight_plan(
|
||||||
|
{"600000.XSHG": 0.20},
|
||||||
|
{
|
||||||
|
"600000.XSHG": 10.0,
|
||||||
|
"000001.XSHE": 10.0,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
equity=100_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_portable_target_package_verifier_matches_local_seal(self):
|
||||||
|
package = sealed_target_package()
|
||||||
|
self.assertEqual(verify_target_package(package), package)
|
||||||
|
self.assertEqual(
|
||||||
|
verify_target_package(
|
||||||
|
package,
|
||||||
|
expected_signal_as_of=package["signal_as_of"],
|
||||||
|
expected_next_session=package["next_session"],
|
||||||
|
expected_decision_id=package["decision_id"],
|
||||||
|
expected_package_sha256=package["package_sha256"],
|
||||||
|
),
|
||||||
|
package,
|
||||||
|
)
|
||||||
|
|
||||||
|
tampered = copy.deepcopy(package)
|
||||||
|
tampered["target_weights"]["000001.XSHE"] = 0.29
|
||||||
|
tampered["constraints"]["actual_gross_weight"] = 0.49
|
||||||
|
with self.assertRaisesRegex(ValueError, "hash mismatch"):
|
||||||
|
verify_target_package(tampered)
|
||||||
|
|
||||||
|
quantity_injected = copy.deepcopy(package)
|
||||||
|
quantity_injected["target_quantity"] = 100
|
||||||
|
with self.assertRaisesRegex(ValueError, "quantities do not belong"):
|
||||||
|
verify_target_package(quantity_injected)
|
||||||
|
|
||||||
|
noncanonical = copy.deepcopy(package)
|
||||||
|
noncanonical["target_weights"]["000001.SZ"] = noncanonical[
|
||||||
|
"target_weights"
|
||||||
|
].pop("000001.XSHE")
|
||||||
|
with self.assertRaisesRegex(ValueError, "must be canonical"):
|
||||||
|
verify_target_package(noncanonical)
|
||||||
|
|
||||||
|
def test_target_package_tape_is_pinned_and_exact_only(self):
|
||||||
|
first = sealed_target_package()
|
||||||
|
second = sealed_target_package(
|
||||||
|
package_id="TP-20260731-B",
|
||||||
|
decision_id="D-20260731-B",
|
||||||
|
signal_as_of="2026-07-31T15:00:00+08:00",
|
||||||
|
next_session="2026-08-03",
|
||||||
|
)
|
||||||
|
packages = [first, second]
|
||||||
|
tape_hash = target_package_tape_sha256(packages)
|
||||||
|
self.assertEqual(
|
||||||
|
tape_hash,
|
||||||
|
hashlib.sha256(
|
||||||
|
canonical_json(packages).encode("utf-8")
|
||||||
|
).hexdigest(),
|
||||||
|
)
|
||||||
|
found = lookup_target_package_exact(
|
||||||
|
packages,
|
||||||
|
signal_as_of=second["signal_as_of"],
|
||||||
|
next_session=second["next_session"],
|
||||||
|
decision_id=second["decision_id"],
|
||||||
|
expected_tape_sha256=tape_hash,
|
||||||
|
expected_count=2,
|
||||||
|
)
|
||||||
|
self.assertEqual(found, second)
|
||||||
|
found["release"] = "mutated"
|
||||||
|
self.assertEqual(packages[1]["release"], second["release"])
|
||||||
|
|
||||||
|
with self.assertRaises(TargetPackageLookupMiss):
|
||||||
|
lookup_target_package_exact(
|
||||||
|
packages,
|
||||||
|
signal_as_of="2026-07-30T15:00:00+08:00",
|
||||||
|
next_session="2026-07-31",
|
||||||
|
expected_tape_sha256=tape_hash,
|
||||||
|
expected_count=2,
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(ValueError, "decision_id mismatch"):
|
||||||
|
lookup_target_package_exact(
|
||||||
|
packages,
|
||||||
|
signal_as_of=first["signal_as_of"],
|
||||||
|
next_session=first["next_session"],
|
||||||
|
decision_id="D-WRONG",
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(ValueError, "count mismatch"):
|
||||||
|
lookup_target_package_exact(
|
||||||
|
packages[:1],
|
||||||
|
signal_as_of=first["signal_as_of"],
|
||||||
|
next_session=first["next_session"],
|
||||||
|
expected_count=2,
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(ValueError, "digest mismatch"):
|
||||||
|
lookup_target_package_exact(
|
||||||
|
packages[:1],
|
||||||
|
signal_as_of=first["signal_as_of"],
|
||||||
|
next_session=first["next_session"],
|
||||||
|
expected_tape_sha256=tape_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_target_package_tape_rejects_duplicates_and_unmatched_tamper(self):
|
||||||
|
first = sealed_target_package()
|
||||||
|
duplicate_clock = sealed_target_package(
|
||||||
|
package_id="TP-20260724-B",
|
||||||
|
decision_id="D-20260724-B",
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(ValueError, "duplicate target package clock"):
|
||||||
|
lookup_target_package_exact(
|
||||||
|
[first, duplicate_clock],
|
||||||
|
signal_as_of=first["signal_as_of"],
|
||||||
|
next_session=first["next_session"],
|
||||||
|
)
|
||||||
|
|
||||||
|
later = sealed_target_package(
|
||||||
|
package_id="TP-20260731-B",
|
||||||
|
decision_id="D-20260731-B",
|
||||||
|
signal_as_of="2026-07-31T15:00:00+08:00",
|
||||||
|
next_session="2026-08-03",
|
||||||
|
)
|
||||||
|
later["release"] = "tampered"
|
||||||
|
with self.assertRaisesRegex(ValueError, "hash mismatch"):
|
||||||
|
lookup_target_package_exact(
|
||||||
|
[first, later],
|
||||||
|
signal_as_of=first["signal_as_of"],
|
||||||
|
next_session=first["next_session"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_portable_core_parses_as_python_36(self):
|
||||||
|
source = (
|
||||||
|
Path(__file__).parents[1]
|
||||||
|
/ "src"
|
||||||
|
/ "quant60"
|
||||||
|
/ "portable_core.py"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
ast.parse(source, feature_version=(3, 6))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -112,10 +113,191 @@ class PublicStatusTests(unittest.TestCase):
|
|||||||
|
|
||||||
platforms = {platform["name"]: platform for platform in payload["platforms"]}
|
platforms = {platform["name"]: platform for platform in payload["platforms"]}
|
||||||
self.assertEqual(platforms["JoinQuant hosted bundle"]["status"], "real_runtime")
|
self.assertEqual(platforms["JoinQuant hosted bundle"]["status"], "real_runtime")
|
||||||
|
self.assertIn(
|
||||||
|
"3",
|
||||||
|
platforms["JoinQuant hosted bundle"]["claim"],
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"execution",
|
||||||
|
platforms["JoinQuant hosted bundle"]["limitations"][0],
|
||||||
|
)
|
||||||
self.assertEqual(platforms["QMT built-in bundle"]["status"], "implemented")
|
self.assertEqual(platforms["QMT built-in bundle"]["status"], "implemented")
|
||||||
self.assertFalse(platforms["XtTrader read-only shadow"]["liveReady"])
|
self.assertFalse(platforms["XtTrader read-only shadow"]["liveReady"])
|
||||||
self.assertIn("fake", platforms["XtTrader read-only shadow"]["claim"].lower())
|
self.assertIn("fake", platforms["XtTrader read-only shadow"]["claim"].lower())
|
||||||
|
|
||||||
|
def test_candidate_records_only_real_joinquant_execution_observation(self):
|
||||||
|
payload = _read_payload()
|
||||||
|
candidate = payload["candidateBoundary"]
|
||||||
|
self.assertEqual(candidate["decisionMode"], "target_package")
|
||||||
|
self.assertEqual(candidate["localEvidenceClass"], "synthetic_research")
|
||||||
|
self.assertTrue(candidate["realJoinQuantObserved"])
|
||||||
|
self.assertFalse(candidate["realQmtObserved"])
|
||||||
|
self.assertEqual(
|
||||||
|
candidate["realPlatformObservedLayers"],
|
||||||
|
["execution"],
|
||||||
|
)
|
||||||
|
self.assertFalse(candidate["allFiveLayersRealPlatformObserved"])
|
||||||
|
run = candidate["joinQuantTargetPackageRun"]
|
||||||
|
self.assertEqual(
|
||||||
|
run["runId"],
|
||||||
|
"2457a7c39a276e09e0fabf99e28978e1",
|
||||||
|
)
|
||||||
|
self.assertEqual(run["evidenceClass"], "synthetic-research")
|
||||||
|
self.assertEqual(run["observedLayers"], ["execution"])
|
||||||
|
self.assertEqual((run["orders"], run["fills"]), (3, 3))
|
||||||
|
self.assertFalse(run["allFiveLayersObserved"])
|
||||||
|
self.assertFalse(run["qmtPeerObserved"])
|
||||||
|
self.assertFalse(run["g9Passed"])
|
||||||
|
self.assertFalse(run["baseline60Passed"])
|
||||||
|
self.assertFalse(run["investmentValueClaim"])
|
||||||
|
self.assertFalse(candidate["joinQuantTargetPackageCountsAsBaseline"])
|
||||||
|
self.assertFalse(candidate["joinQuantTargetPackageCountsAsG9"])
|
||||||
|
self.assertEqual(
|
||||||
|
candidate["historicalJoinQuantMode"], "portable_momentum_smoke"
|
||||||
|
)
|
||||||
|
self.assertFalse(candidate["historicalJoinQuantCountsAsBaseline"])
|
||||||
|
self.assertTrue(payload["summary"]["candidateRealPlatformObserved"])
|
||||||
|
self.assertEqual(
|
||||||
|
payload["summary"]["candidateRealPlatformObservationScope"],
|
||||||
|
"target_package_execution_consumer_only",
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
payload["summary"]["allFiveLayersRealPlatformObserved"]
|
||||||
|
)
|
||||||
|
reachability = candidate["reachability"]
|
||||||
|
self.assertEqual(
|
||||||
|
reachability["releaseId"], "quant60-research-candidate-v1"
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
reachability["summary"]["all_layers_implemented"]
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
reachability["summary"]["all_layers_local_entrypoint_reachable"]
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
reachability["summary"]["all_layers_real_platform_observed"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
reachability["realPlatformsObserved"],
|
||||||
|
["joinquant"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
reachability["realPlatformObservedLayers"],
|
||||||
|
["execution"],
|
||||||
|
)
|
||||||
|
evidence = {item["id"]: item for item in payload["evidence"]}
|
||||||
|
self.assertEqual(
|
||||||
|
evidence["release-reachability"]["path"],
|
||||||
|
"releases/quant60-research-candidate-v1/reachability.json",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
evidence["joinquant-target-package-run"]["path"],
|
||||||
|
"evidence/joinquant/TP-20240311-795dcfba/"
|
||||||
|
"2457a7c39a276e09e0fabf99e28978e1/run.json",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
evidence["joinquant-target-package-evidence"]["path"],
|
||||||
|
"docs/JOINQUANT_TARGET_PACKAGE_EVIDENCE_2026-07-26.md",
|
||||||
|
)
|
||||||
|
|
||||||
|
gaps = payload["deliveryGaps"]
|
||||||
|
self.assertGreaterEqual(len(gaps["internal"]), 3)
|
||||||
|
self.assertGreaterEqual(len(gaps["externalEvidence"]), 5)
|
||||||
|
internal = " ".join(gaps["internal"])
|
||||||
|
self.assertIn("provider-data", internal)
|
||||||
|
self.assertIn("factor_risk.py", internal)
|
||||||
|
self.assertIn("CVXPY", internal)
|
||||||
|
self.assertIn("TWAP/POV", internal)
|
||||||
|
self.assertIn("synthetic-research", internal)
|
||||||
|
self.assertIn("TargetPackageV1", internal)
|
||||||
|
self.assertIn("real JoinQuant", internal)
|
||||||
|
self.assertIn("real QMT", internal)
|
||||||
|
|
||||||
|
def test_public_builder_rejects_wrong_reachability_evidence_class(self):
|
||||||
|
module = _load_builder()
|
||||||
|
path = (
|
||||||
|
PROJECT
|
||||||
|
/ "releases"
|
||||||
|
/ "quant60-research-candidate-v1"
|
||||||
|
/ "reachability.json"
|
||||||
|
)
|
||||||
|
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
artifact = manifest["layers"]["alpha"]["jq_entrypoint_reachable"][
|
||||||
|
"artifacts"
|
||||||
|
][0]
|
||||||
|
artifact["kind"] = "source"
|
||||||
|
body = {
|
||||||
|
key: value
|
||||||
|
for key, value in manifest.items()
|
||||||
|
if key != "manifest_sha256"
|
||||||
|
}
|
||||||
|
manifest["manifest_sha256"] = hashlib.sha256(
|
||||||
|
json.dumps(
|
||||||
|
body,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
with self.assertRaises(module.PublicStatusError):
|
||||||
|
module._validate_reachability_manifest(PROJECT, manifest)
|
||||||
|
|
||||||
|
def test_public_builder_rejects_upstream_real_platform_overclaim(self):
|
||||||
|
module = _load_builder()
|
||||||
|
path = (
|
||||||
|
PROJECT
|
||||||
|
/ "releases"
|
||||||
|
/ "quant60-research-candidate-v1"
|
||||||
|
/ "reachability.json"
|
||||||
|
)
|
||||||
|
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
artifact = dict(
|
||||||
|
manifest["layers"]["execution"]["real_platform_observed"][
|
||||||
|
"artifacts"
|
||||||
|
][0]
|
||||||
|
)
|
||||||
|
artifact["artifact_id"] = "alpha-real-platform-overclaim"
|
||||||
|
manifest["layers"]["alpha"]["real_platform_observed"] = {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": [artifact],
|
||||||
|
}
|
||||||
|
body = {
|
||||||
|
key: value
|
||||||
|
for key, value in manifest.items()
|
||||||
|
if key != "manifest_sha256"
|
||||||
|
}
|
||||||
|
manifest["manifest_sha256"] = hashlib.sha256(
|
||||||
|
json.dumps(
|
||||||
|
body,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
module.PublicStatusError,
|
||||||
|
"only JoinQuant execution",
|
||||||
|
):
|
||||||
|
module._validate_reachability_manifest(PROJECT, manifest)
|
||||||
|
|
||||||
|
def test_public_builder_rejects_target_package_performance_claim(self):
|
||||||
|
module = _load_builder()
|
||||||
|
path = (
|
||||||
|
PROJECT
|
||||||
|
/ "evidence"
|
||||||
|
/ "joinquant"
|
||||||
|
/ "TP-20240311-795dcfba"
|
||||||
|
/ "2457a7c39a276e09e0fabf99e28978e1"
|
||||||
|
/ "run.json"
|
||||||
|
)
|
||||||
|
run = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
run["performance_claim"] = True
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
module.PublicStatusError,
|
||||||
|
"evidence classes are unsafe",
|
||||||
|
):
|
||||||
|
module._validate_joinquant_target_package_run(PROJECT, run)
|
||||||
|
|
||||||
def test_freshness_uses_daily_bars_not_the_later_trade_calendar(self):
|
def test_freshness_uses_daily_bars_not_the_later_trade_calendar(self):
|
||||||
payload = _read_payload()
|
payload = _read_payload()
|
||||||
freshness = {item["id"]: item for item in payload["freshness"]}
|
freshness = {item["id"]: item for item in payload["freshness"]}
|
||||||
@@ -124,6 +306,19 @@ class PublicStatusTests(unittest.TestCase):
|
|||||||
freshness["joinquant-hosted-market-window"]["marketDataThrough"],
|
freshness["joinquant-hosted-market-window"]["marketDataThrough"],
|
||||||
"2024-12-31",
|
"2024-12-31",
|
||||||
)
|
)
|
||||||
|
target_package = freshness[
|
||||||
|
"joinquant-target-package-execution-smoke"
|
||||||
|
]
|
||||||
|
self.assertEqual(target_package["marketDataFrom"], "2024-03-11")
|
||||||
|
self.assertEqual(target_package["marketDataThrough"], "2024-03-13")
|
||||||
|
self.assertEqual(
|
||||||
|
target_package["observedAt"],
|
||||||
|
"2026-07-26T08:50:15Z",
|
||||||
|
)
|
||||||
|
self.assertFalse(target_package["productionReady"])
|
||||||
|
self.assertFalse(target_package["investmentValueClaim"])
|
||||||
|
self.assertIn("execution", target_package["semantics"])
|
||||||
|
self.assertIn("G9", target_package["semantics"])
|
||||||
|
|
||||||
tushare = freshness["tushare-daily-market-data"]
|
tushare = freshness["tushare-daily-market-data"]
|
||||||
self.assertEqual(tushare["completedPartitions"], 35)
|
self.assertEqual(tushare["completedPartitions"], 35)
|
||||||
|
|||||||
@@ -0,0 +1,405 @@
|
|||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from quant60.ledger import canonical_json
|
||||||
|
from quant60.release_reachability import (
|
||||||
|
CAPABILITIES,
|
||||||
|
LAYERS,
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
build_evidence_artifact,
|
||||||
|
build_release_reachability,
|
||||||
|
load_verified_release_reachability,
|
||||||
|
release_reachability_sha256,
|
||||||
|
seal_release_reachability,
|
||||||
|
validate_release_reachability,
|
||||||
|
write_release_reachability,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
GENERATED_AT = "2026-07-26T12:00:00Z"
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseReachabilityTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
temporary = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(temporary.cleanup)
|
||||||
|
self.root = Path(temporary.name)
|
||||||
|
for directory in ("src", "tests", "tools", "platforms", "evidence"):
|
||||||
|
(self.root / directory).mkdir()
|
||||||
|
for layer in LAYERS:
|
||||||
|
(self.root / "src" / f"{layer}.py").write_text(
|
||||||
|
f"LAYER = {layer!r}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(self.root / "tests" / f"test_{layer}.py").write_text(
|
||||||
|
f"def test_{layer}():\n assert True\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(self.root / "tools" / f"{layer}_entry.py").write_text(
|
||||||
|
f"from src import {layer}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(self.root / "platforms" / "joinquant_entry.py").write_text(
|
||||||
|
"def execute():\n return 'submitted'\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(self.root / "platforms" / "qmt_entry.py").write_text(
|
||||||
|
"def execute():\n return 'submitted'\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(self.root / "evidence" / "jq-execution.json").write_text(
|
||||||
|
canonical_json(
|
||||||
|
{
|
||||||
|
"platform": "joinquant",
|
||||||
|
"layer": "execution",
|
||||||
|
"release_id": "quant60-baseline-v1",
|
||||||
|
"result": "passed",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _false_claim():
|
||||||
|
return {"status": False, "artifacts": []}
|
||||||
|
|
||||||
|
def _artifact(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
artifact_id,
|
||||||
|
kind,
|
||||||
|
path,
|
||||||
|
platform,
|
||||||
|
observed_at=None,
|
||||||
|
run_id=None,
|
||||||
|
):
|
||||||
|
return build_evidence_artifact(
|
||||||
|
artifact_id=artifact_id,
|
||||||
|
kind=kind,
|
||||||
|
path=path,
|
||||||
|
platform=platform,
|
||||||
|
repository_root=self.root,
|
||||||
|
observed_at=observed_at,
|
||||||
|
run_id=run_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _layers(self):
|
||||||
|
layers = {}
|
||||||
|
for layer in LAYERS:
|
||||||
|
layers[layer] = {
|
||||||
|
"implemented": {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": [
|
||||||
|
self._artifact(
|
||||||
|
artifact_id=f"{layer}-source",
|
||||||
|
kind="source",
|
||||||
|
path=f"src/{layer}.py",
|
||||||
|
platform="repository",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"unit_tested": {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": [
|
||||||
|
self._artifact(
|
||||||
|
artifact_id=f"{layer}-test",
|
||||||
|
kind="test",
|
||||||
|
path=f"tests/test_{layer}.py",
|
||||||
|
platform="repository",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"local_entrypoint_reachable": {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": [
|
||||||
|
self._artifact(
|
||||||
|
artifact_id=f"{layer}-local-entry",
|
||||||
|
kind="entrypoint",
|
||||||
|
path=f"tools/{layer}_entry.py",
|
||||||
|
platform="local",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"jq_entrypoint_reachable": self._false_claim(),
|
||||||
|
"qmt_entrypoint_reachable": self._false_claim(),
|
||||||
|
"real_platform_observed": self._false_claim(),
|
||||||
|
}
|
||||||
|
layers["execution"]["jq_entrypoint_reachable"] = {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": [
|
||||||
|
self._artifact(
|
||||||
|
artifact_id="execution-jq-entry",
|
||||||
|
kind="entrypoint",
|
||||||
|
path="platforms/joinquant_entry.py",
|
||||||
|
platform="joinquant",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
layers["execution"]["real_platform_observed"] = {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": [
|
||||||
|
self._artifact(
|
||||||
|
artifact_id="execution-jq-run",
|
||||||
|
kind="platform_run",
|
||||||
|
path="evidence/jq-execution.json",
|
||||||
|
platform="joinquant",
|
||||||
|
observed_at="2026-07-26T11:30:00Z",
|
||||||
|
run_id="jq-backtest-20260726-01",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return layers
|
||||||
|
|
||||||
|
def _manifest(self):
|
||||||
|
return build_release_reachability(
|
||||||
|
release_id="quant60-baseline-v1",
|
||||||
|
generated_at=GENERATED_AT,
|
||||||
|
layers=self._layers(),
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_build_write_load_preserves_exact_six_claims_per_layer(self):
|
||||||
|
manifest = self._manifest()
|
||||||
|
self.assertEqual(
|
||||||
|
manifest["manifest_sha256"],
|
||||||
|
release_reachability_sha256(manifest),
|
||||||
|
)
|
||||||
|
for layer in LAYERS:
|
||||||
|
self.assertEqual(
|
||||||
|
set(manifest["layers"][layer]),
|
||||||
|
set(CAPABILITIES),
|
||||||
|
)
|
||||||
|
self.assertTrue(manifest["summary"]["all_layers_implemented"])
|
||||||
|
self.assertTrue(manifest["summary"]["all_layers_unit_tested"])
|
||||||
|
self.assertTrue(
|
||||||
|
manifest["summary"]["all_layers_local_entrypoint_reachable"]
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
manifest["summary"]["all_layers_jq_entrypoint_reachable"]
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
manifest["summary"]["all_layers_qmt_entrypoint_reachable"]
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
manifest["summary"]["all_layers_real_platform_observed"]
|
||||||
|
)
|
||||||
|
|
||||||
|
path = write_release_reachability(
|
||||||
|
manifest,
|
||||||
|
self.root / "reachability.json",
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
loaded = load_verified_release_reachability(
|
||||||
|
path,
|
||||||
|
repository_root=self.root,
|
||||||
|
expected_release_id="quant60-baseline-v1",
|
||||||
|
expected_manifest_sha256=manifest["manifest_sha256"],
|
||||||
|
)
|
||||||
|
self.assertEqual(loaded, manifest)
|
||||||
|
self.assertEqual(
|
||||||
|
path.read_bytes(),
|
||||||
|
(canonical_json(manifest) + "\n").encode("utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manifest_and_artifact_tampering_both_fail_closed(self):
|
||||||
|
manifest = self._manifest()
|
||||||
|
status_tamper = copy.deepcopy(manifest)
|
||||||
|
status_tamper["layers"]["alpha"]["implemented"]["status"] = False
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
"SHA-256 mismatch",
|
||||||
|
):
|
||||||
|
validate_release_reachability(
|
||||||
|
status_tamper,
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
|
||||||
|
path = write_release_reachability(
|
||||||
|
manifest,
|
||||||
|
self.root / "reachability.json",
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
(self.root / "src" / "alpha.py").write_text(
|
||||||
|
"LAYER = 'tampered'\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
"artifact hash mismatch",
|
||||||
|
):
|
||||||
|
load_verified_release_reachability(
|
||||||
|
path,
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_source_hash_cannot_masquerade_as_real_platform_observation(self):
|
||||||
|
layers = self._layers()
|
||||||
|
source = self.root / "src" / "execution.py"
|
||||||
|
layers["execution"]["real_platform_observed"] = {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"artifact_id": "fake-platform-run",
|
||||||
|
"kind": "platform_run",
|
||||||
|
"path": "src/execution.py",
|
||||||
|
"sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||||
|
"platform": "joinquant",
|
||||||
|
"observed_at": "2026-07-26T11:30:00Z",
|
||||||
|
"run_id": "fake-run",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
"real-platform evidence must live under",
|
||||||
|
):
|
||||||
|
build_release_reachability(
|
||||||
|
release_id="quant60-baseline-v1",
|
||||||
|
generated_at=GENERATED_AT,
|
||||||
|
layers=layers,
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_true_status_requires_typed_evidence_and_false_forbids_it(self):
|
||||||
|
true_without_evidence = self._layers()
|
||||||
|
true_without_evidence["risk"]["unit_tested"] = {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": [],
|
||||||
|
}
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
"iff at least one evidence",
|
||||||
|
):
|
||||||
|
build_release_reachability(
|
||||||
|
release_id="quant60-baseline-v1",
|
||||||
|
generated_at=GENERATED_AT,
|
||||||
|
layers=true_without_evidence,
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
|
||||||
|
wrong_evidence_type = self._layers()
|
||||||
|
wrong_evidence_type["risk"]["unit_tested"]["artifacts"][0].update(
|
||||||
|
{"kind": "source", "platform": "repository"}
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
"requires kind='test'",
|
||||||
|
):
|
||||||
|
build_release_reachability(
|
||||||
|
release_id="quant60-baseline-v1",
|
||||||
|
generated_at=GENERATED_AT,
|
||||||
|
layers=wrong_evidence_type,
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_real_observation_requires_corresponding_platform_entrypoint(self):
|
||||||
|
layers = self._layers()
|
||||||
|
layers["execution"]["jq_entrypoint_reachable"] = self._false_claim()
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
"requires jq_entrypoint_reachable",
|
||||||
|
):
|
||||||
|
build_release_reachability(
|
||||||
|
release_id="quant60-baseline-v1",
|
||||||
|
generated_at=GENERATED_AT,
|
||||||
|
layers=layers,
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_path_hash_id_and_clock_validation_are_strict(self):
|
||||||
|
cases = []
|
||||||
|
|
||||||
|
absolute = self._layers()
|
||||||
|
absolute["alpha"]["implemented"]["artifacts"][0]["path"] = str(
|
||||||
|
self.root / "src" / "alpha.py"
|
||||||
|
)
|
||||||
|
cases.append((absolute, "repository-relative path"))
|
||||||
|
|
||||||
|
traversal = self._layers()
|
||||||
|
traversal["alpha"]["implemented"]["artifacts"][0]["path"] = (
|
||||||
|
"src/../tests/test_alpha.py"
|
||||||
|
)
|
||||||
|
cases.append((traversal, "normalized repository-relative path"))
|
||||||
|
|
||||||
|
placeholder = self._layers()
|
||||||
|
placeholder["alpha"]["implemented"]["artifacts"][0]["sha256"] = (
|
||||||
|
"0" * 64
|
||||||
|
)
|
||||||
|
cases.append((placeholder, "all-zero placeholder"))
|
||||||
|
|
||||||
|
duplicate = self._layers()
|
||||||
|
duplicate["alpha"]["implemented"]["artifacts"][0]["artifact_id"] = (
|
||||||
|
duplicate["universe"]["implemented"]["artifacts"][0]["artifact_id"]
|
||||||
|
)
|
||||||
|
cases.append((duplicate, "reused across claims"))
|
||||||
|
|
||||||
|
future_run = self._layers()
|
||||||
|
future_run["execution"]["real_platform_observed"]["artifacts"][0][
|
||||||
|
"observed_at"
|
||||||
|
] = "2026-07-26T12:00:01Z"
|
||||||
|
cases.append((future_run, "cannot be after generated_at"))
|
||||||
|
|
||||||
|
for layers, message in cases:
|
||||||
|
with self.subTest(message=message):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
message,
|
||||||
|
):
|
||||||
|
build_release_reachability(
|
||||||
|
release_id="quant60-baseline-v1",
|
||||||
|
generated_at=GENERATED_AT,
|
||||||
|
layers=layers,
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_strict_loader_rejects_duplicate_json_keys(self):
|
||||||
|
path = self.root / "duplicate.json"
|
||||||
|
path.write_text(
|
||||||
|
'{"schema_version":"1.0","schema_version":"1.0"}',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
"duplicate JSON key",
|
||||||
|
):
|
||||||
|
load_verified_release_reachability(
|
||||||
|
path,
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_resealed_derived_summary_tamper_is_rejected(self):
|
||||||
|
manifest = self._manifest()
|
||||||
|
manifest["summary"]["all_layers_qmt_entrypoint_reachable"] = True
|
||||||
|
manifest["manifest_sha256"] = release_reachability_sha256(manifest)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
"summary is not the exact derived",
|
||||||
|
):
|
||||||
|
validate_release_reachability(
|
||||||
|
manifest,
|
||||||
|
repository_root=self.root,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_seal_rejects_prepopulated_derived_fields(self):
|
||||||
|
body = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"artifact_type": "quant60-release-reachability",
|
||||||
|
"release_id": "quant60-baseline-v1",
|
||||||
|
"generated_at": GENERATED_AT,
|
||||||
|
"layers": self._layers(),
|
||||||
|
"summary": {},
|
||||||
|
}
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
"derived summary",
|
||||||
|
):
|
||||||
|
seal_release_reachability(body, repository_root=self.root)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from quant60.target_package import (
|
||||||
|
DuplicateTargetDecisionError,
|
||||||
|
SHA256_FIELDS,
|
||||||
|
TargetPackageError,
|
||||||
|
TargetPackageNotFoundError,
|
||||||
|
TargetPackageTape,
|
||||||
|
TargetPackageV1,
|
||||||
|
load_target_package_tape,
|
||||||
|
seal_target_package,
|
||||||
|
target_package_sha256,
|
||||||
|
validate_target_package,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def package_body(
|
||||||
|
*,
|
||||||
|
package_id: str = "TP-20260724-A",
|
||||||
|
decision_id: str = "D-20260724-A",
|
||||||
|
signal_as_of: str = "2026-07-24T15:00:00+08:00",
|
||||||
|
next_session: str = "2026-07-27",
|
||||||
|
release: str = "quant60-baseline-60-v1",
|
||||||
|
model_id: str = "alpha158-lightgbm-v1",
|
||||||
|
) -> dict:
|
||||||
|
return {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"package_type": "TargetPackageV1",
|
||||||
|
"package_id": package_id,
|
||||||
|
"decision_id": decision_id,
|
||||||
|
"release": release,
|
||||||
|
"evidence_class": "provider-snapshot-local",
|
||||||
|
"model_id": model_id,
|
||||||
|
"signal_as_of": signal_as_of,
|
||||||
|
"next_session": next_session,
|
||||||
|
"model_sha256": "1" * 64,
|
||||||
|
"data_sha256": "2" * 64,
|
||||||
|
"feature_sha256": "3" * 64,
|
||||||
|
"risk_sha256": "4" * 64,
|
||||||
|
"cost_sha256": "5" * 64,
|
||||||
|
"optimizer_sha256": "6" * 64,
|
||||||
|
"config_sha256": "7" * 64,
|
||||||
|
"source_sha256": "8" * 64,
|
||||||
|
"target_weights": {
|
||||||
|
"000001.XSHE": 0.30,
|
||||||
|
"600000.XSHG": 0.20,
|
||||||
|
},
|
||||||
|
"universe": {
|
||||||
|
"000001.XSHE": {
|
||||||
|
"state": "openable",
|
||||||
|
"reasons": ["ELIGIBLE"],
|
||||||
|
},
|
||||||
|
"600000.XSHG": {
|
||||||
|
"state": "hold_only",
|
||||||
|
"reasons": ["LIQUIDITY_BELOW_FLOOR"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"constraint_state": "FEASIBLE",
|
||||||
|
"long_only": True,
|
||||||
|
"max_single_weight": 0.40,
|
||||||
|
"max_gross_weight": 0.80,
|
||||||
|
"max_one_way_turnover": 0.20,
|
||||||
|
"actual_gross_weight": 0.50,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sealed_package(**overrides) -> dict:
|
||||||
|
return seal_target_package(package_body(**overrides))
|
||||||
|
|
||||||
|
|
||||||
|
class TargetPackageTests(unittest.TestCase):
|
||||||
|
def test_valid_package_is_deterministic_and_quantity_free(self):
|
||||||
|
package = sealed_package()
|
||||||
|
self.assertEqual(
|
||||||
|
package["package_sha256"],
|
||||||
|
target_package_sha256(package),
|
||||||
|
)
|
||||||
|
self.assertEqual(len(package["package_sha256"]), 64)
|
||||||
|
self.assertNotIn("quantity", json.dumps(package))
|
||||||
|
self.assertEqual(validate_target_package(package), package)
|
||||||
|
wrapped = TargetPackageV1.from_dict(package)
|
||||||
|
self.assertEqual(wrapped.package_id, package["package_id"])
|
||||||
|
self.assertEqual(wrapped.as_dict(), package)
|
||||||
|
|
||||||
|
def test_tamper_is_rejected(self):
|
||||||
|
package = sealed_package()
|
||||||
|
package["target_weights"]["000001.XSHE"] = 0.25
|
||||||
|
package["constraints"]["actual_gross_weight"] = 0.45
|
||||||
|
with self.assertRaisesRegex(TargetPackageError, "hash mismatch"):
|
||||||
|
validate_target_package(package)
|
||||||
|
|
||||||
|
def test_every_layer_hash_is_required_and_strict(self):
|
||||||
|
package = sealed_package()
|
||||||
|
for field in SHA256_FIELDS:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
missing = copy.deepcopy(package)
|
||||||
|
missing.pop(field)
|
||||||
|
with self.assertRaises(TargetPackageError):
|
||||||
|
validate_target_package(missing)
|
||||||
|
malformed = copy.deepcopy(package)
|
||||||
|
malformed[field] = "A" * 64
|
||||||
|
with self.assertRaises(TargetPackageError):
|
||||||
|
validate_target_package(malformed)
|
||||||
|
placeholder = copy.deepcopy(package)
|
||||||
|
placeholder[field] = "0" * 64
|
||||||
|
placeholder["package_sha256"] = target_package_sha256(
|
||||||
|
placeholder
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
TargetPackageError,
|
||||||
|
"all-zero placeholder",
|
||||||
|
):
|
||||||
|
validate_target_package(placeholder)
|
||||||
|
|
||||||
|
def test_portable_momentum_cannot_masquerade_as_baseline(self):
|
||||||
|
body = package_body(model_id="portable-momentum-v1")
|
||||||
|
with self.assertRaisesRegex(TargetPackageError, "cannot claim"):
|
||||||
|
seal_target_package(body)
|
||||||
|
|
||||||
|
smoke = package_body(
|
||||||
|
release="platform-smoke-v1",
|
||||||
|
model_id="portable-momentum-v1",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
seal_target_package(smoke)["model_id"],
|
||||||
|
"portable-momentum-v1",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_quantities_are_forbidden_at_any_depth(self):
|
||||||
|
for mutation in (
|
||||||
|
lambda body: body.update({"target_quantity": 100}),
|
||||||
|
lambda body: body["constraints"].update({"quantity": 100}),
|
||||||
|
lambda body: body["universe"]["000001.XSHE"].update(
|
||||||
|
{"sellable_quantity": 100}
|
||||||
|
),
|
||||||
|
):
|
||||||
|
with self.subTest(mutation=mutation):
|
||||||
|
body = package_body()
|
||||||
|
mutation(body)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
TargetPackageError,
|
||||||
|
"do not belong",
|
||||||
|
):
|
||||||
|
seal_target_package(body)
|
||||||
|
|
||||||
|
def test_clock_must_be_canonical_close_and_future_weekday(self):
|
||||||
|
cases = (
|
||||||
|
("2026-07-24T14:59:59+08:00", "2026-07-27"),
|
||||||
|
("2026-07-24T07:00:00+00:00", "2026-07-27"),
|
||||||
|
("2026-07-24T15:00:00.000000+08:00", "2026-07-27"),
|
||||||
|
("2026-07-24T15:00:00+08:00", "2026-07-24"),
|
||||||
|
("2026-07-24T15:00:00+08:00", "2026-07-25"),
|
||||||
|
)
|
||||||
|
for signal_as_of, next_session in cases:
|
||||||
|
with self.subTest(
|
||||||
|
signal_as_of=signal_as_of,
|
||||||
|
next_session=next_session,
|
||||||
|
):
|
||||||
|
with self.assertRaises(TargetPackageError):
|
||||||
|
seal_target_package(
|
||||||
|
package_body(
|
||||||
|
signal_as_of=signal_as_of,
|
||||||
|
next_session=next_session,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_symbols_weights_and_gross_fail_closed(self):
|
||||||
|
noncanonical = package_body()
|
||||||
|
noncanonical["target_weights"] = {
|
||||||
|
"000001.SZ": 0.30,
|
||||||
|
"600000.XSHG": 0.20,
|
||||||
|
}
|
||||||
|
with self.assertRaisesRegex(TargetPackageError, "canonical"):
|
||||||
|
seal_target_package(noncanonical)
|
||||||
|
|
||||||
|
missing_universe = package_body()
|
||||||
|
missing_universe["universe"].pop("000001.XSHE")
|
||||||
|
with self.assertRaisesRegex(TargetPackageError, "absent from universe"):
|
||||||
|
seal_target_package(missing_universe)
|
||||||
|
|
||||||
|
bad_gross = package_body()
|
||||||
|
bad_gross["constraints"]["actual_gross_weight"] = 0.49
|
||||||
|
with self.assertRaisesRegex(TargetPackageError, "does not equal"):
|
||||||
|
seal_target_package(bad_gross)
|
||||||
|
|
||||||
|
gross_breach = package_body()
|
||||||
|
gross_breach["constraints"]["max_gross_weight"] = 0.49
|
||||||
|
with self.assertRaisesRegex(TargetPackageError, "max_gross"):
|
||||||
|
seal_target_package(gross_breach)
|
||||||
|
|
||||||
|
single_breach = package_body()
|
||||||
|
single_breach["constraints"]["max_single_weight"] = 0.25
|
||||||
|
with self.assertRaisesRegex(TargetPackageError, "max_single"):
|
||||||
|
seal_target_package(single_breach)
|
||||||
|
|
||||||
|
excluded = package_body()
|
||||||
|
excluded["universe"]["000001.XSHE"] = {
|
||||||
|
"state": "excluded",
|
||||||
|
"reasons": ["NOT_PIT_INDEX_MEMBER"],
|
||||||
|
}
|
||||||
|
with self.assertRaisesRegex(TargetPackageError, "excluded"):
|
||||||
|
seal_target_package(excluded)
|
||||||
|
|
||||||
|
def test_tape_supports_multiple_decisions_with_exact_lookup(self):
|
||||||
|
first = sealed_package()
|
||||||
|
second = sealed_package(
|
||||||
|
package_id="TP-20260731-B",
|
||||||
|
decision_id="D-20260731-B",
|
||||||
|
signal_as_of="2026-07-31T15:00:00+08:00",
|
||||||
|
next_session="2026-08-03",
|
||||||
|
)
|
||||||
|
tape = TargetPackageTape([second, first])
|
||||||
|
self.assertEqual(len(tape), 2)
|
||||||
|
found = tape.lookup_exact(
|
||||||
|
signal_as_of=first["signal_as_of"],
|
||||||
|
next_session=first["next_session"],
|
||||||
|
)
|
||||||
|
self.assertEqual(found["decision_id"], first["decision_id"])
|
||||||
|
found["release"] = "mutated"
|
||||||
|
self.assertEqual(
|
||||||
|
tape.lookup_exact(
|
||||||
|
signal_as_of=first["signal_as_of"],
|
||||||
|
next_session=first["next_session"],
|
||||||
|
)["release"],
|
||||||
|
first["release"],
|
||||||
|
)
|
||||||
|
with self.assertRaises(TargetPackageNotFoundError):
|
||||||
|
tape.lookup_exact(
|
||||||
|
signal_as_of="2026-07-30T15:00:00+08:00",
|
||||||
|
next_session="2026-07-31",
|
||||||
|
)
|
||||||
|
with self.assertRaises(TargetPackageNotFoundError):
|
||||||
|
tape.lookup_exact(
|
||||||
|
signal_as_of=first["signal_as_of"],
|
||||||
|
next_session=first["next_session"],
|
||||||
|
decision_id="D-wrong",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_duplicate_decision_identity_is_rejected(self):
|
||||||
|
first = sealed_package()
|
||||||
|
duplicate_id = sealed_package(
|
||||||
|
package_id="TP-20260731-B",
|
||||||
|
decision_id=first["decision_id"],
|
||||||
|
signal_as_of="2026-07-31T15:00:00+08:00",
|
||||||
|
next_session="2026-08-03",
|
||||||
|
)
|
||||||
|
with self.assertRaises(DuplicateTargetDecisionError):
|
||||||
|
TargetPackageTape([first, duplicate_id])
|
||||||
|
|
||||||
|
duplicate_clock = sealed_package(
|
||||||
|
package_id="TP-20260724-B",
|
||||||
|
decision_id="D-20260724-B",
|
||||||
|
)
|
||||||
|
with self.assertRaises(DuplicateTargetDecisionError):
|
||||||
|
TargetPackageTape([first, duplicate_clock])
|
||||||
|
|
||||||
|
def test_json_and_jsonl_tapes_load_multiple_decisions(self):
|
||||||
|
packages = [
|
||||||
|
sealed_package(),
|
||||||
|
sealed_package(
|
||||||
|
package_id="TP-20260731-B",
|
||||||
|
decision_id="D-20260731-B",
|
||||||
|
signal_as_of="2026-07-31T15:00:00+08:00",
|
||||||
|
next_session="2026-08-03",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
json_path = root / "tape.json"
|
||||||
|
json_path.write_text(
|
||||||
|
json.dumps({"packages": packages}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
jsonl_path = root / "tape.jsonl"
|
||||||
|
jsonl_path.write_text(
|
||||||
|
"".join(json.dumps(item) + "\n" for item in packages),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(load_target_package_tape(json_path)), 2)
|
||||||
|
self.assertEqual(len(load_target_package_tape(jsonl_path)), 2)
|
||||||
|
|
||||||
|
def test_single_package_loads_and_strict_json_rejects_ambiguity(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
single = root / "single.json"
|
||||||
|
single.write_text(
|
||||||
|
json.dumps(sealed_package()),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(load_target_package_tape(single)), 1)
|
||||||
|
|
||||||
|
duplicate = root / "duplicate.json"
|
||||||
|
duplicate.write_text(
|
||||||
|
'{"package_type":"TargetPackageV1",'
|
||||||
|
'"package_type":"TargetPackageV1"}',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(TargetPackageError, "duplicate JSON key"):
|
||||||
|
load_target_package_tape(duplicate)
|
||||||
|
|
||||||
|
non_finite = root / "nan.json"
|
||||||
|
non_finite.write_text("[NaN]", encoding="utf-8")
|
||||||
|
with self.assertRaisesRegex(TargetPackageError, "non-finite"):
|
||||||
|
load_target_package_tape(non_finite)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+506
-46
@@ -18,10 +18,27 @@ PROJECT = Path(__file__).resolve().parents[1]
|
|||||||
DEFAULT_OUTPUT = PROJECT / "status" / "status-data.js"
|
DEFAULT_OUTPUT = PROJECT / "status" / "status-data.js"
|
||||||
GLOBAL_NAME = "QUANT_OS_PUBLIC_STATUS"
|
GLOBAL_NAME = "QUANT_OS_PUBLIC_STATUS"
|
||||||
SCHEMA_VERSION = 1
|
SCHEMA_VERSION = 1
|
||||||
|
REACHABILITY_PATH = (
|
||||||
|
"releases/quant60-research-candidate-v1/reachability.json"
|
||||||
|
)
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_PATH = (
|
||||||
|
"evidence/joinquant/TP-20240311-795dcfba/"
|
||||||
|
"2457a7c39a276e09e0fabf99e28978e1/run.json"
|
||||||
|
)
|
||||||
|
JOINQUANT_TARGET_PACKAGE_DOC_PATH = (
|
||||||
|
"docs/JOINQUANT_TARGET_PACKAGE_EVIDENCE_2026-07-26.md"
|
||||||
|
)
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_ID = "2457a7c39a276e09e0fabf99e28978e1"
|
||||||
|
JOINQUANT_TARGET_PACKAGE_OBSERVED_AT = "2026-07-26T08:50:15Z"
|
||||||
|
JOINQUANT_TARGET_PACKAGE_BUNDLE_SHA256 = (
|
||||||
|
"0586930ddc483f4489f09b562f4d4232b458b703e6a1194af809b707211be8c4"
|
||||||
|
)
|
||||||
|
|
||||||
SOURCE_PATHS: Tuple[str, ...] = (
|
SOURCE_PATHS: Tuple[str, ...] = (
|
||||||
"gate_scorecard.json",
|
"gate_scorecard.json",
|
||||||
"dist/bundle_manifest.json",
|
"dist/bundle_manifest.json",
|
||||||
|
REACHABILITY_PATH,
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_PATH,
|
||||||
"configs/baseline.json",
|
"configs/baseline.json",
|
||||||
"README.md",
|
"README.md",
|
||||||
"docs/ARCHITECTURE.md",
|
"docs/ARCHITECTURE.md",
|
||||||
@@ -29,8 +46,10 @@ SOURCE_PATHS: Tuple[str, ...] = (
|
|||||||
"docs/PLATFORM_MATRIX.md",
|
"docs/PLATFORM_MATRIX.md",
|
||||||
"docs/CROSS_ENGINE_CONSISTENCY.md",
|
"docs/CROSS_ENGINE_CONSISTENCY.md",
|
||||||
"docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md",
|
"docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md",
|
||||||
|
JOINQUANT_TARGET_PACKAGE_DOC_PATH,
|
||||||
"docs/TUSHARE_LOCAL_DATA.md",
|
"docs/TUSHARE_LOCAL_DATA.md",
|
||||||
"runbooks/PLATFORM_DEPLOYMENT.md",
|
"runbooks/PLATFORM_DEPLOYMENT.md",
|
||||||
|
"tools/build_release_reachability.py",
|
||||||
"tools/build_public_status.py",
|
"tools/build_public_status.py",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -245,6 +264,287 @@ def _validate_bundle_manifest(
|
|||||||
return public_artifacts
|
return public_artifacts
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_reachability_manifest(
|
||||||
|
project: Path, manifest: Mapping[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
source_root = project / "src"
|
||||||
|
inserted = str(source_root) not in sys.path
|
||||||
|
if inserted:
|
||||||
|
sys.path.insert(0, str(source_root))
|
||||||
|
try:
|
||||||
|
from quant60.release_reachability import ( # pylint: disable=import-outside-toplevel
|
||||||
|
ReleaseReachabilityError,
|
||||||
|
validate_release_reachability,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
manifest = validate_release_reachability(
|
||||||
|
manifest,
|
||||||
|
repository_root=project,
|
||||||
|
expected_release_id="quant60-research-candidate-v1",
|
||||||
|
)
|
||||||
|
except ReleaseReachabilityError as exc:
|
||||||
|
raise PublicStatusError(
|
||||||
|
f"release reachability manifest failed canonical verification: {exc}"
|
||||||
|
) from exc
|
||||||
|
finally:
|
||||||
|
if inserted:
|
||||||
|
sys.path.remove(str(source_root))
|
||||||
|
|
||||||
|
expected_layers = {"universe", "alpha", "portfolio", "risk", "execution"}
|
||||||
|
expected_capabilities = {
|
||||||
|
"implemented",
|
||||||
|
"unit_tested",
|
||||||
|
"local_entrypoint_reachable",
|
||||||
|
"jq_entrypoint_reachable",
|
||||||
|
"qmt_entrypoint_reachable",
|
||||||
|
"real_platform_observed",
|
||||||
|
}
|
||||||
|
declared_sha = str(manifest.get("manifest_sha256", ""))
|
||||||
|
body = {
|
||||||
|
key: value
|
||||||
|
for key, value in manifest.items()
|
||||||
|
if key != "manifest_sha256"
|
||||||
|
}
|
||||||
|
body_bytes = json.dumps(
|
||||||
|
body,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
actual_sha = _sha256_bytes(body_bytes)
|
||||||
|
if not re.fullmatch(r"[0-9a-f]{64}", declared_sha) or declared_sha != actual_sha:
|
||||||
|
raise PublicStatusError("release reachability manifest self-hash is invalid")
|
||||||
|
|
||||||
|
layers = manifest.get("layers")
|
||||||
|
if not isinstance(layers, dict) or set(layers) != expected_layers:
|
||||||
|
raise PublicStatusError(
|
||||||
|
"release reachability manifest must contain the canonical five layers"
|
||||||
|
)
|
||||||
|
calculated_summary: Dict[str, bool] = {}
|
||||||
|
real_platforms: set[str] = set()
|
||||||
|
real_platform_observed_layers: set[str] = set()
|
||||||
|
for capability in sorted(expected_capabilities):
|
||||||
|
statuses: List[bool] = []
|
||||||
|
for layer in sorted(expected_layers):
|
||||||
|
claims = layers[layer]
|
||||||
|
if not isinstance(claims, dict) or set(claims) != expected_capabilities:
|
||||||
|
raise PublicStatusError(
|
||||||
|
f"release reachability claims are incomplete for {layer}"
|
||||||
|
)
|
||||||
|
claim = claims[capability]
|
||||||
|
if not isinstance(claim, dict) or not isinstance(
|
||||||
|
claim.get("status"), bool
|
||||||
|
):
|
||||||
|
raise PublicStatusError(
|
||||||
|
f"release reachability status is invalid for {layer}.{capability}"
|
||||||
|
)
|
||||||
|
artifacts = claim.get("artifacts")
|
||||||
|
if not isinstance(artifacts, list):
|
||||||
|
raise PublicStatusError(
|
||||||
|
f"release reachability artifacts are invalid for {layer}.{capability}"
|
||||||
|
)
|
||||||
|
status = claim["status"]
|
||||||
|
if status != bool(artifacts):
|
||||||
|
raise PublicStatusError(
|
||||||
|
f"release reachability evidence/status mismatch for "
|
||||||
|
f"{layer}.{capability}"
|
||||||
|
)
|
||||||
|
for index, artifact in enumerate(artifacts):
|
||||||
|
if not isinstance(artifact, dict):
|
||||||
|
raise PublicStatusError(
|
||||||
|
f"release reachability artifact is invalid for "
|
||||||
|
f"{layer}.{capability}[{index}]"
|
||||||
|
)
|
||||||
|
path = _safe_repo_path(
|
||||||
|
artifact.get("path"),
|
||||||
|
label=f"{layer}.{capability}[{index}].path",
|
||||||
|
)
|
||||||
|
_verify_file_hash(project, path, str(artifact.get("sha256", "")))
|
||||||
|
if capability == "real_platform_observed":
|
||||||
|
real_platforms.add(str(artifact.get("platform", "")))
|
||||||
|
real_platform_observed_layers.add(layer)
|
||||||
|
statuses.append(status)
|
||||||
|
calculated_summary[f"all_layers_{capability}"] = all(statuses)
|
||||||
|
|
||||||
|
summary = manifest.get("summary")
|
||||||
|
if not isinstance(summary, dict) or summary != calculated_summary:
|
||||||
|
raise PublicStatusError(
|
||||||
|
"release reachability summary does not match per-layer claims"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
real_platforms != {"joinquant"}
|
||||||
|
or real_platform_observed_layers != {"execution"}
|
||||||
|
or calculated_summary["all_layers_real_platform_observed"]
|
||||||
|
):
|
||||||
|
raise PublicStatusError(
|
||||||
|
"current release may claim only JoinQuant execution observation"
|
||||||
|
)
|
||||||
|
release_id = str(manifest.get("release_id", ""))
|
||||||
|
if release_id != "quant60-research-candidate-v1":
|
||||||
|
raise PublicStatusError("unexpected release reachability release_id")
|
||||||
|
return {
|
||||||
|
"releaseId": release_id,
|
||||||
|
"manifest": REACHABILITY_PATH,
|
||||||
|
"manifestSha256": declared_sha,
|
||||||
|
"summary": calculated_summary,
|
||||||
|
"realPlatformsObserved": sorted(
|
||||||
|
platform for platform in real_platforms if platform
|
||||||
|
),
|
||||||
|
"realPlatformObservedLayers": sorted(real_platform_observed_layers),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_joinquant_target_package_run(
|
||||||
|
project: Path,
|
||||||
|
run: Mapping[str, Any],
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Validate the narrow, redacted hosted execution observation."""
|
||||||
|
|
||||||
|
if run.get("record_type") != "joinquant_target_package_execution_smoke":
|
||||||
|
raise PublicStatusError("unexpected JoinQuant TargetPackage run type")
|
||||||
|
if run.get("platform") != "joinquant" or run.get("status") != "completed":
|
||||||
|
raise PublicStatusError("JoinQuant TargetPackage run is not completed")
|
||||||
|
if (
|
||||||
|
run.get("platform_evidence_class") != "real_platform_runtime"
|
||||||
|
or run.get("input_evidence_class") != "synthetic-research"
|
||||||
|
or run.get("performance_claim") is not False
|
||||||
|
):
|
||||||
|
raise PublicStatusError(
|
||||||
|
"JoinQuant TargetPackage evidence classes are unsafe"
|
||||||
|
)
|
||||||
|
if run.get("run_id") != JOINQUANT_TARGET_PACKAGE_RUN_ID:
|
||||||
|
raise PublicStatusError("unexpected JoinQuant TargetPackage run identity")
|
||||||
|
if run.get("observed_at") != JOINQUANT_TARGET_PACKAGE_OBSERVED_AT:
|
||||||
|
raise PublicStatusError("unexpected JoinQuant TargetPackage observation time")
|
||||||
|
|
||||||
|
backtest = run.get("backtest")
|
||||||
|
if not isinstance(backtest, dict):
|
||||||
|
raise PublicStatusError("JoinQuant TargetPackage backtest must be an object")
|
||||||
|
expected_url = (
|
||||||
|
"https://www.joinquant.com/algorithm/backtest/detail?backtestId="
|
||||||
|
+ JOINQUANT_TARGET_PACKAGE_RUN_ID
|
||||||
|
)
|
||||||
|
expected_backtest = {
|
||||||
|
"id": JOINQUANT_TARGET_PACKAGE_RUN_ID,
|
||||||
|
"url": expected_url,
|
||||||
|
"start_date": "2024-03-11",
|
||||||
|
"end_date": "2024-03-13",
|
||||||
|
"initial_capital_cny": 1_000_000,
|
||||||
|
"frequency": "day",
|
||||||
|
"python": "Python3",
|
||||||
|
"benchmark": "000905.XSHG",
|
||||||
|
"timezone": "Asia/Shanghai",
|
||||||
|
}
|
||||||
|
if backtest != expected_backtest:
|
||||||
|
raise PublicStatusError("JoinQuant TargetPackage settings drifted")
|
||||||
|
|
||||||
|
identity = run.get("artifact_identity")
|
||||||
|
if not isinstance(identity, dict):
|
||||||
|
raise PublicStatusError("JoinQuant TargetPackage identity must be an object")
|
||||||
|
expected_identity = {
|
||||||
|
"bundle_sha256": JOINQUANT_TARGET_PACKAGE_BUNDLE_SHA256,
|
||||||
|
"browser_snapshot_sha256": JOINQUANT_TARGET_PACKAGE_BUNDLE_SHA256,
|
||||||
|
"exact_browser_match": True,
|
||||||
|
"bundle_manifest_sha256": (
|
||||||
|
"91f4b4dc0bdd0d9f99b3466c830600e2678be78dd991d4499a4154bffec8597a"
|
||||||
|
),
|
||||||
|
"portable_core_sha256": (
|
||||||
|
"f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19"
|
||||||
|
),
|
||||||
|
"wrapper_sha256": (
|
||||||
|
"0c672b64665b7c1d4409ba288b00d1608e976800af3a4f9a6467767ce4def592"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if identity != expected_identity:
|
||||||
|
raise PublicStatusError("JoinQuant TargetPackage artifact identity drifted")
|
||||||
|
|
||||||
|
target_package = run.get("target_package")
|
||||||
|
if not isinstance(target_package, dict):
|
||||||
|
raise PublicStatusError("JoinQuant TargetPackage identity is missing")
|
||||||
|
if (
|
||||||
|
target_package.get("package_id") != "TP-20240311-795dcfba"
|
||||||
|
or target_package.get("evidence_class") != "synthetic-research"
|
||||||
|
or target_package.get("package_sha256")
|
||||||
|
!= "baa15c8156aa6cb54d3775d0cd562613a9d0a4f15b0c39f2a60e0f8b8c6f1f3a"
|
||||||
|
or target_package.get("tape_sha256")
|
||||||
|
!= "5383c376951637b8d4b1ca42fc6db80347abda74c0181c0f70d6c411fb97ea64"
|
||||||
|
):
|
||||||
|
raise PublicStatusError("JoinQuant TargetPackage lineage drifted")
|
||||||
|
|
||||||
|
sessions = run.get("session_observations")
|
||||||
|
if not isinstance(sessions, list) or [
|
||||||
|
(item.get("date"), item.get("event"), item.get("orders"), item.get("fills"))
|
||||||
|
for item in sessions
|
||||||
|
if isinstance(item, dict)
|
||||||
|
] != [
|
||||||
|
("2024-03-11", "TARGET_PACKAGE_NOOP", 0, 0),
|
||||||
|
("2024-03-12", "TARGET_PACKAGE_HIT", 3, 3),
|
||||||
|
("2024-03-13", "TARGET_PACKAGE_NOOP", 0, 0),
|
||||||
|
]:
|
||||||
|
raise PublicStatusError("JoinQuant TargetPackage session evidence drifted")
|
||||||
|
if run.get("counts") != {
|
||||||
|
"orders": 3,
|
||||||
|
"fills": 3,
|
||||||
|
"skipped_orders": 0,
|
||||||
|
}:
|
||||||
|
raise PublicStatusError("JoinQuant TargetPackage order/fill counts drifted")
|
||||||
|
|
||||||
|
boundary = run.get("claim_boundary")
|
||||||
|
if not isinstance(boundary, dict):
|
||||||
|
raise PublicStatusError("JoinQuant TargetPackage claim boundary is missing")
|
||||||
|
required_false = (
|
||||||
|
"upstream_layers_real_platform_observed",
|
||||||
|
"authorized_real_data_oos_observed",
|
||||||
|
"qmt_peer_observed",
|
||||||
|
"same_input_cross_engine_parity_observed",
|
||||||
|
"all_five_layers_real_platform_observed",
|
||||||
|
"g9_passed",
|
||||||
|
"baseline_60_passed",
|
||||||
|
"release_verified",
|
||||||
|
"investment_value_claim",
|
||||||
|
)
|
||||||
|
if boundary.get("real_platform_observed_layers") != ["execution"] or any(
|
||||||
|
boundary.get(field) is not False for field in required_false
|
||||||
|
):
|
||||||
|
raise PublicStatusError(
|
||||||
|
"JoinQuant TargetPackage run overclaims its evidence boundary"
|
||||||
|
)
|
||||||
|
performance = run.get("performance")
|
||||||
|
if (
|
||||||
|
not isinstance(performance, dict)
|
||||||
|
or performance.get("investment_value_claim") is not False
|
||||||
|
or performance.get("scope") != "three-session execution wiring smoke only"
|
||||||
|
):
|
||||||
|
raise PublicStatusError(
|
||||||
|
"JoinQuant TargetPackage performance boundary is unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
path = _safe_repo_path(
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_PATH,
|
||||||
|
label="JoinQuant TargetPackage run",
|
||||||
|
)
|
||||||
|
if not (project / path).is_file():
|
||||||
|
raise PublicStatusError("tracked JoinQuant TargetPackage run is missing")
|
||||||
|
return {
|
||||||
|
"runId": JOINQUANT_TARGET_PACKAGE_RUN_ID,
|
||||||
|
"observedAt": JOINQUANT_TARGET_PACKAGE_OBSERVED_AT,
|
||||||
|
"marketDataFrom": backtest["start_date"],
|
||||||
|
"marketDataThrough": backtest["end_date"],
|
||||||
|
"bundleSha256": identity["bundle_sha256"],
|
||||||
|
"packageId": target_package["package_id"],
|
||||||
|
"evidenceClass": target_package["evidence_class"],
|
||||||
|
"orders": 3,
|
||||||
|
"fills": 3,
|
||||||
|
"observedLayers": ["execution"],
|
||||||
|
"allFiveLayersObserved": False,
|
||||||
|
"qmtPeerObserved": False,
|
||||||
|
"g9Passed": False,
|
||||||
|
"baseline60Passed": False,
|
||||||
|
"investmentValueClaim": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _extract_test_summary(facts: Sequence[Any]) -> Tuple[int | None, int | None]:
|
def _extract_test_summary(facts: Sequence[Any]) -> Tuple[int | None, int | None]:
|
||||||
for fact in facts:
|
for fact in facts:
|
||||||
text = str(fact)
|
text = str(fact)
|
||||||
@@ -288,7 +588,13 @@ def _platform_status(name: str) -> str:
|
|||||||
def _platform_evidence_links(name: str) -> List[str]:
|
def _platform_evidence_links(name: str) -> List[str]:
|
||||||
links = ["platform-matrix"]
|
links = ["platform-matrix"]
|
||||||
if "JoinQuant" in name:
|
if "JoinQuant" in name:
|
||||||
links.append("joinquant-hosted-evidence")
|
links.extend(
|
||||||
|
(
|
||||||
|
"joinquant-hosted-evidence",
|
||||||
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run",
|
||||||
|
)
|
||||||
|
)
|
||||||
if "Qlib" in name or "Tushare" in name:
|
if "Qlib" in name or "Tushare" in name:
|
||||||
links.append("tushare-data")
|
links.append("tushare-data")
|
||||||
if "QMT" in name or "XtTrader" in name:
|
if "QMT" in name or "XtTrader" in name:
|
||||||
@@ -337,6 +643,12 @@ def _build_evidence(project: Path) -> List[Dict[str, Any]]:
|
|||||||
"dist/bundle_manifest.json",
|
"dist/bundle_manifest.json",
|
||||||
"machine_record",
|
"machine_record",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"release-reachability",
|
||||||
|
"Five-layer release reachability manifest",
|
||||||
|
REACHABILITY_PATH,
|
||||||
|
"machine_record",
|
||||||
|
),
|
||||||
("readme", "Quant OS verification guide", "README.md", "documentation"),
|
("readme", "Quant OS verification guide", "README.md", "documentation"),
|
||||||
(
|
(
|
||||||
"architecture",
|
"architecture",
|
||||||
@@ -364,10 +676,22 @@ def _build_evidence(project: Path) -> List[Dict[str, Any]]:
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"joinquant-hosted-evidence",
|
"joinquant-hosted-evidence",
|
||||||
"JoinQuant hosted smoke boundary",
|
"JoinQuant hosted momentum smoke boundary",
|
||||||
"docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md",
|
"docs/JOINQUANT_HOSTED_EVIDENCE_2026-07-26.md",
|
||||||
"runtime_evidence",
|
"runtime_evidence",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"joinquant-target-package-evidence",
|
||||||
|
"JoinQuant TargetPackage execution evidence boundary",
|
||||||
|
JOINQUANT_TARGET_PACKAGE_DOC_PATH,
|
||||||
|
"runtime_evidence",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"joinquant-target-package-run",
|
||||||
|
"Redacted JoinQuant TargetPackage hosted run",
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_PATH,
|
||||||
|
"machine_record",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"tushare-data",
|
"tushare-data",
|
||||||
"Tushare inventory and Qlib bridge record",
|
"Tushare inventory and Qlib bridge record",
|
||||||
@@ -403,14 +727,14 @@ def _build_layers() -> List[Dict[str, Any]]:
|
|||||||
"id": "data",
|
"id": "data",
|
||||||
"name": "数据与时点",
|
"name": "数据与时点",
|
||||||
"status": "tested",
|
"status": "tested",
|
||||||
"claim": "不可变快照、PIT 字段合同和 Tushare→Qlib 技术链路已实现并有本地合同/运行证据。",
|
"claim": "不可变快照、PIT 字段合同和 Tushare→Qlib 技术链路已有本地合同/运行证据;这些 provider-data 路径尚未接成当前 Ridge 五层候选。",
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"JQData raw/factor/limits/paused/PIT membership/ST snapshot",
|
"JQData raw/factor/limits/paused/PIT membership/ST snapshot",
|
||||||
"Tushare completed-Parquet inventory and immutable Qlib provider",
|
"Tushare completed-Parquet inventory and immutable Qlib provider",
|
||||||
"semantic manifest and data-version verification",
|
"semantic manifest and data-version verification",
|
||||||
],
|
],
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"尚无授权 JQData 真实快照与许可 lineage;Tushare 镜像仅部分完成且缺生产关键字段。"
|
"尚无授权 JQData 真实快照与许可 lineage;Tushare 镜像仅部分完成且缺生产关键字段;当前五层候选仍使用 synthetic 数据。"
|
||||||
],
|
],
|
||||||
"evidenceLinks": ["architecture", "tushare-data", "scorecard"],
|
"evidenceLinks": ["architecture", "tushare-data", "scorecard"],
|
||||||
"releaseVerified": False,
|
"releaseVerified": False,
|
||||||
@@ -419,52 +743,65 @@ def _build_layers() -> List[Dict[str, Any]]:
|
|||||||
"id": "research",
|
"id": "research",
|
||||||
"name": "研究与模型",
|
"name": "研究与模型",
|
||||||
"status": "real_runtime",
|
"status": "real_runtime",
|
||||||
"claim": "synthetic research pipeline 已通过测试;Qlib 0.9.7 的 momentum 与 Alpha158/LightGBM fixture 已在真实本地运行时完成。",
|
"claim": "synthetic 五层研究入口能冻结 train-only Ridge ModelBundle、逐层 trace 与 TargetPackageV1;Qlib momentum 和 Alpha158/LightGBM fixture 是另外的受限研究 smoke。",
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"PIT features, purged walk-forward and deterministic Ridge",
|
"train-only preprocessing, purged walk-forward and deterministic Ridge",
|
||||||
|
"replayable frozen ModelBundle and synthetic TargetPackageV1",
|
||||||
"Qlib native momentum",
|
"Qlib native momentum",
|
||||||
"Qlib Alpha158, LightGBM and Recorder workflow",
|
"Qlib Alpha158, LightGBM and Recorder workflow",
|
||||||
],
|
],
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"fixture 仅两只 synthetic 股票;没有授权长样本 OOS、冻结模型包或投资价值证据。"
|
"候选和 Qlib fixture 均是 synthetic;尚无授权长样本 OOS、真实数据校准、平台候选观察或投资价值证据。"
|
||||||
|
],
|
||||||
|
"evidenceLinks": [
|
||||||
|
"architecture",
|
||||||
|
"platform-matrix",
|
||||||
|
"release-reachability",
|
||||||
|
"scorecard",
|
||||||
],
|
],
|
||||||
"evidenceLinks": ["architecture", "platform-matrix", "scorecard"],
|
|
||||||
"releaseVerified": False,
|
"releaseVerified": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "signal-portfolio",
|
"id": "signal-portfolio",
|
||||||
"name": "信号、组合与事前风险",
|
"name": "信号、组合与事前风险",
|
||||||
"status": "tested",
|
"status": "tested",
|
||||||
"claim": "Signal→Target→Order Delta 合同、成本、风险与约束目标在本地测试路径可重放。",
|
"claim": "当前 synthetic 主链把 Ridge Alpha、显式成本、60 日对角 realized variance、deterministic optimizer 和 post-risk gate 接成可重放 TargetPackage。",
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"portable momentum scoring and capped target weights",
|
"explicit fee/spread/square-root-impact cost forecast",
|
||||||
"lot-rounded target quantities and T+1 sellable deltas",
|
"diagonal realized-variance risk input and deterministic constrained target",
|
||||||
"factor risk, dated cost and constrained target research",
|
"single-name, industry, gross, capacity and turnover constraints",
|
||||||
|
"post-risk weights with hash-linked lineage",
|
||||||
],
|
],
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"风险、冲击和容量尚未用授权真实数据校准;G4/G5 均未通过。"
|
"factor_risk.py 与 optional CVXPY solver 未被当前主链选择;风险、冲击和容量尚未用授权真实数据校准,G4/G5 均未通过。"
|
||||||
|
],
|
||||||
|
"evidenceLinks": [
|
||||||
|
"architecture",
|
||||||
|
"release-reachability",
|
||||||
|
"scorecard",
|
||||||
],
|
],
|
||||||
"evidenceLinks": ["architecture", "scorecard"],
|
|
||||||
"releaseVerified": False,
|
"releaseVerified": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "backtest",
|
"id": "backtest",
|
||||||
"name": "回测与跨引擎",
|
"name": "回测与跨引擎",
|
||||||
"status": "real_runtime",
|
"status": "real_runtime",
|
||||||
"claim": "本地 synthetic、Qlib fixture 与一次真实 JoinQuant hosted smoke 已运行;QMT 仍只有 bundle/harness。",
|
"claim": "本地 synthetic 五层候选、平台 harness 和一次真实 JoinQuant TargetPackage execution smoke 已运行;QMT 尚无真实 peer run。",
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"local event and snapshot backtests",
|
"local event and snapshot backtests",
|
||||||
"JoinQuant hosted single-file bundle",
|
"fail-closed JoinQuant TargetPackage single-file consumer",
|
||||||
"QMT built-in/qmttools backtest-only bundle",
|
"fail-closed QMT built-in TargetPackage backtest consumer",
|
||||||
"Qlib research runners",
|
"Qlib research runners",
|
||||||
],
|
],
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"没有冻结同输入的本地/聚宽/QMT 逐层对比;真实 QMT 尚未运行,G9 未通过。"
|
"聚宽只观察到一个 synthetic-research TargetPackage 的消费、账户/开盘价绑定和三笔成交;无真实长样本 OOS、QMT peer 或逐层真实数据 parity,G9 未通过。"
|
||||||
],
|
],
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"platform-matrix",
|
"platform-matrix",
|
||||||
"joinquant-hosted-evidence",
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run",
|
||||||
"cross-engine",
|
"cross-engine",
|
||||||
|
"release-reachability",
|
||||||
"scorecard",
|
"scorecard",
|
||||||
],
|
],
|
||||||
"releaseVerified": False,
|
"releaseVerified": False,
|
||||||
@@ -473,17 +810,23 @@ def _build_layers() -> List[Dict[str, Any]]:
|
|||||||
"id": "platforms",
|
"id": "platforms",
|
||||||
"name": "平台适配",
|
"name": "平台适配",
|
||||||
"status": "real_runtime",
|
"status": "real_runtime",
|
||||||
"claim": "Local、Colab、JoinQuant、Qlib 有可执行入口;JoinQuant/Qlib 有受限真实运行时 smoke。",
|
"claim": "Local、Colab、JoinQuant、QMT、Qlib 有入口;JoinQuant 已真实观察 synthetic-research TargetPackage 的 execution consumer,QMT 候选仍未实跑。",
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"local and Colab zero-account smoke",
|
"local and Colab zero-account smoke",
|
||||||
"JQData snapshot entrypoint",
|
"JQData snapshot entrypoint",
|
||||||
"JoinQuant upload bundle",
|
"JoinQuant momentum or explicit TargetPackage upload bundle",
|
||||||
"QMT built-in and qmttools entrypoints",
|
"QMT built-in TargetPackage and qmttools entrypoints",
|
||||||
],
|
],
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"JQData 真实账号、QMT 客户端/数据权限及平台完整导出仍是外部前置条件。"
|
"JoinQuant 没有重跑上游四层;JQData 授权真实候选、QMT 客户端/数据权限、QMT peer 导出与逐层真实数据 parity 仍是外部前置条件。"
|
||||||
|
],
|
||||||
|
"evidenceLinks": [
|
||||||
|
"platform-matrix",
|
||||||
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run",
|
||||||
|
"platform-deployment",
|
||||||
|
"release-reachability",
|
||||||
],
|
],
|
||||||
"evidenceLinks": ["platform-matrix", "platform-deployment"],
|
|
||||||
"releaseVerified": False,
|
"releaseVerified": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -526,11 +869,11 @@ def _build_models() -> List[Dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"id": "portable-momentum-v1",
|
"id": "portable-momentum-v1",
|
||||||
"name": "portable-momentum-v1",
|
"name": "portable-momentum-v1",
|
||||||
"role": "跨引擎冻结计算基线",
|
"role": "平台连通性 smoke 策略",
|
||||||
"status": "real_runtime",
|
"status": "real_runtime",
|
||||||
"claim": "共享 symbol normalization、momentum、target weight/quantity 与 order-delta 计算;已进入 JoinQuant hosted smoke 和本地 Qlib momentum 路径。",
|
"claim": "共享 symbol normalization、momentum、target weight/quantity 与 order-delta 计算;2024 JoinQuant hosted run 只证明这一 smoke 路径。",
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"尚无同输入 L1–L4 真实跨引擎通过证据,不是已验证的生产 champion。"
|
"不是 Ridge/TargetPackage Baseline 候选;旧 hosted run 不得用于候选或 G9 计分。"
|
||||||
],
|
],
|
||||||
"evidenceLinks": [
|
"evidenceLinks": [
|
||||||
"architecture",
|
"architecture",
|
||||||
@@ -543,13 +886,18 @@ def _build_models() -> List[Dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"id": "ridge-challenger",
|
"id": "ridge-challenger",
|
||||||
"name": "Deterministic Ridge",
|
"name": "Deterministic Ridge",
|
||||||
"role": "synthetic 研究候选",
|
"role": "synthetic 五层 Baseline 研究候选",
|
||||||
"status": "tested",
|
"status": "tested",
|
||||||
"claim": "purged walk-forward research pipeline 中可确定性训练和评估。",
|
"claim": "train-only 预处理、系数、截距、训练截止和 lineage 可冻结为可重放 ModelBundle;其 synthetic TargetPackageV1 已被真实 JoinQuant execution consumer 消费并成交。",
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"只在 synthetic 研究路径验证;无真实 OOS、versioned model bundle 或跨平台推理验证。"
|
"JoinQuant 只观察执行边界,未重跑上游四层;无授权真实 OOS、真实校准、QMT peer 或投资价值证据。"
|
||||||
|
],
|
||||||
|
"evidenceLinks": [
|
||||||
|
"architecture",
|
||||||
|
"platform-matrix",
|
||||||
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run",
|
||||||
],
|
],
|
||||||
"evidenceLinks": ["architecture", "platform-matrix"],
|
|
||||||
"investmentValueClaim": False,
|
"investmentValueClaim": False,
|
||||||
"releaseVerified": False,
|
"releaseVerified": False,
|
||||||
},
|
},
|
||||||
@@ -570,8 +918,14 @@ def _build_models() -> List[Dict[str, Any]]:
|
|||||||
|
|
||||||
|
|
||||||
def _build_next_actions(scorecard: Mapping[str, Any]) -> List[Dict[str, Any]]:
|
def _build_next_actions(scorecard: Mapping[str, Any]) -> List[Dict[str, Any]]:
|
||||||
missing = scorecard.get("current_delivery", {}).get(
|
delivery = scorecard.get("current_delivery", {})
|
||||||
"external_evidence_missing", []
|
if not isinstance(delivery, dict):
|
||||||
|
raise PublicStatusError("scorecard current_delivery must be an object")
|
||||||
|
internal = delivery.get("internal_delivery_gaps", [])
|
||||||
|
missing = delivery.get("external_evidence_missing", [])
|
||||||
|
if not isinstance(internal, list) or len(internal) < 3:
|
||||||
|
raise PublicStatusError(
|
||||||
|
"scorecard must list the internal delivery gaps blocking Baseline"
|
||||||
)
|
)
|
||||||
if not isinstance(missing, list) or len(missing) < 5:
|
if not isinstance(missing, list) or len(missing) < 5:
|
||||||
raise PublicStatusError(
|
raise PublicStatusError(
|
||||||
@@ -580,30 +934,36 @@ def _build_next_actions(scorecard: Mapping[str, Any]) -> List[Dict[str, Any]]:
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"priority": 1,
|
"priority": 1,
|
||||||
"title": "生成授权 JQData 不可变真实快照",
|
"title": "接通真实数据五层候选",
|
||||||
"why": str(missing[0]),
|
"why": f"{internal[0]} External prerequisite: {missing[0]}",
|
||||||
"gateIds": ["G1", "G3", "G7"],
|
"gateIds": ["G1", "G2", "G3", "G5", "G7"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"priority": 2,
|
"priority": 2,
|
||||||
"title": "冻结聚宽同输入并完成逐层本地比较",
|
"title": "冻结主链风险、优化与执行选择",
|
||||||
"why": str(missing[1]),
|
"why": str(internal[1]),
|
||||||
"gateIds": ["G3", "G9"],
|
"gateIds": ["G3", "G5", "G7", "G9"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"priority": 3,
|
"priority": 3,
|
||||||
"title": "取得真实 QMT built-in 与 qmttools 导出",
|
"title": "扩展 JoinQuant 多期对账与逐层 parity",
|
||||||
"why": str(missing[2]),
|
"why": f"{internal[2]} Required evidence: {missing[1]}",
|
||||||
"gateIds": ["G3", "G6", "G9"],
|
"gateIds": ["G3", "G7", "G9"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"priority": 4,
|
"priority": 4,
|
||||||
|
"title": "运行真实 QMT TargetPackage 候选",
|
||||||
|
"why": str(missing[2]),
|
||||||
|
"gateIds": ["G6", "G8", "G9"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": 5,
|
||||||
"title": "完成 QMT 券商合同探针与至少 20 日影子盘",
|
"title": "完成 QMT 券商合同探针与至少 20 日影子盘",
|
||||||
"why": str(missing[3]),
|
"why": str(missing[3]),
|
||||||
"gateIds": ["G6", "G8", "G9"],
|
"gateIds": ["G6", "G8", "G9"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"priority": 5,
|
"priority": 6,
|
||||||
"title": "完成连续对账、恢复演练与券商合规确认",
|
"title": "完成连续对账、恢复演练与券商合规确认",
|
||||||
"why": str(missing[4]),
|
"why": str(missing[4]),
|
||||||
"gateIds": ["G8", "G10"],
|
"gateIds": ["G8", "G10"],
|
||||||
@@ -615,6 +975,7 @@ def _build_freshness(
|
|||||||
*,
|
*,
|
||||||
scorecard: Mapping[str, Any],
|
scorecard: Mapping[str, Any],
|
||||||
joinquant_markdown: str,
|
joinquant_markdown: str,
|
||||||
|
joinquant_target_package_run: Mapping[str, Any],
|
||||||
tushare_markdown: str,
|
tushare_markdown: str,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
period_match = re.search(
|
period_match = re.search(
|
||||||
@@ -686,15 +1047,44 @@ def _build_freshness(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "joinquant-hosted-market-window",
|
"id": "joinquant-hosted-market-window",
|
||||||
"label": "JoinQuant hosted smoke 行情区间",
|
"label": "JoinQuant hosted momentum smoke 行情区间",
|
||||||
"status": "runtime_smoke",
|
"status": "runtime_smoke",
|
||||||
"marketDataFrom": joinquant_start,
|
"marketDataFrom": joinquant_start,
|
||||||
"marketDataThrough": joinquant_end,
|
"marketDataThrough": joinquant_end,
|
||||||
"value": f"{joinquant_start} — {joinquant_end}",
|
"value": f"{joinquant_start} — {joinquant_end}",
|
||||||
"productionReady": False,
|
"productionReady": False,
|
||||||
"semantics": "单次真实 hosted runtime smoke 的回测区间;不代表同输入跨引擎通过。",
|
"semantics": "旧 momentum hosted runtime smoke 的回测区间;不是 TargetPackage 候选或同输入跨引擎证据。",
|
||||||
"evidenceLinks": ["joinquant-hosted-evidence", "scorecard"],
|
"evidenceLinks": ["joinquant-hosted-evidence", "scorecard"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "joinquant-target-package-execution-smoke",
|
||||||
|
"label": "JoinQuant TargetPackage execution smoke",
|
||||||
|
"status": "runtime_smoke",
|
||||||
|
"observedAt": str(joinquant_target_package_run["observedAt"]),
|
||||||
|
"marketDataFrom": str(
|
||||||
|
joinquant_target_package_run["marketDataFrom"]
|
||||||
|
),
|
||||||
|
"marketDataThrough": str(
|
||||||
|
joinquant_target_package_run["marketDataThrough"]
|
||||||
|
),
|
||||||
|
"value": (
|
||||||
|
f"{joinquant_target_package_run['marketDataFrom']} — "
|
||||||
|
f"{joinquant_target_package_run['marketDataThrough']}; "
|
||||||
|
"3 orders / 3 fills"
|
||||||
|
),
|
||||||
|
"productionReady": False,
|
||||||
|
"investmentValueClaim": False,
|
||||||
|
"semantics": (
|
||||||
|
"真实平台只观察 synthetic-research TargetPackage 的 execution "
|
||||||
|
"consumer;不代表上游四层真实平台执行、长样本 OOS 或 G9。"
|
||||||
|
),
|
||||||
|
"evidenceLinks": [
|
||||||
|
"joinquant-target-package-evidence",
|
||||||
|
"joinquant-target-package-run",
|
||||||
|
"release-reachability",
|
||||||
|
"scorecard",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "tushare-daily-market-data",
|
"id": "tushare-daily-market-data",
|
||||||
"label": "Tushare daily 行情覆盖",
|
"label": "Tushare daily 行情覆盖",
|
||||||
@@ -806,6 +1196,10 @@ def build_public_status(
|
|||||||
project = project.resolve()
|
project = project.resolve()
|
||||||
scorecard = _load_json(project / "gate_scorecard.json")
|
scorecard = _load_json(project / "gate_scorecard.json")
|
||||||
manifest = _load_json(project / "dist" / "bundle_manifest.json")
|
manifest = _load_json(project / "dist" / "bundle_manifest.json")
|
||||||
|
reachability_manifest = _load_json(project / REACHABILITY_PATH)
|
||||||
|
joinquant_target_package_run_raw = _load_json(
|
||||||
|
project / JOINQUANT_TARGET_PACKAGE_RUN_PATH
|
||||||
|
)
|
||||||
baseline = _load_json(project / "configs" / "baseline.json")
|
baseline = _load_json(project / "configs" / "baseline.json")
|
||||||
platform_markdown = (project / "docs" / "PLATFORM_MATRIX.md").read_text(
|
platform_markdown = (project / "docs" / "PLATFORM_MATRIX.md").read_text(
|
||||||
encoding="utf-8"
|
encoding="utf-8"
|
||||||
@@ -870,6 +1264,13 @@ def build_public_status(
|
|||||||
test_count, optional_skips = _extract_test_summary(facts)
|
test_count, optional_skips = _extract_test_summary(facts)
|
||||||
|
|
||||||
artifacts = _validate_bundle_manifest(project, manifest)
|
artifacts = _validate_bundle_manifest(project, manifest)
|
||||||
|
reachability = _validate_reachability_manifest(
|
||||||
|
project, reachability_manifest
|
||||||
|
)
|
||||||
|
joinquant_target_package_run = _validate_joinquant_target_package_run(
|
||||||
|
project,
|
||||||
|
joinquant_target_package_run_raw,
|
||||||
|
)
|
||||||
source_records: List[Dict[str, str]] = []
|
source_records: List[Dict[str, str]] = []
|
||||||
for relative in SOURCE_PATHS:
|
for relative in SOURCE_PATHS:
|
||||||
safe_path = _safe_repo_path(relative, label="provenance source")
|
safe_path = _safe_repo_path(relative, label="provenance source")
|
||||||
@@ -883,6 +1284,19 @@ def build_public_status(
|
|||||||
|
|
||||||
claim_policy = scorecard["claim_policy"]
|
claim_policy = scorecard["claim_policy"]
|
||||||
delivery = scorecard["current_delivery"]
|
delivery = scorecard["current_delivery"]
|
||||||
|
internal_delivery_gaps = delivery.get("internal_delivery_gaps", [])
|
||||||
|
external_evidence_missing = delivery.get("external_evidence_missing", [])
|
||||||
|
if not isinstance(internal_delivery_gaps, list) or len(internal_delivery_gaps) < 3:
|
||||||
|
raise PublicStatusError(
|
||||||
|
"scorecard must list the internal delivery gaps blocking Baseline"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not isinstance(external_evidence_missing, list)
|
||||||
|
or len(external_evidence_missing) < 5
|
||||||
|
):
|
||||||
|
raise PublicStatusError(
|
||||||
|
"scorecard must list the external evidence blocking release"
|
||||||
|
)
|
||||||
payload: Dict[str, Any] = {
|
payload: Dict[str, Any] = {
|
||||||
"schemaVersion": SCHEMA_VERSION,
|
"schemaVersion": SCHEMA_VERSION,
|
||||||
"generatedAt": generated_at_value,
|
"generatedAt": generated_at_value,
|
||||||
@@ -905,8 +1319,20 @@ def build_public_status(
|
|||||||
"testsRecorded": test_count,
|
"testsRecorded": test_count,
|
||||||
"optionalSkipsRecorded": optional_skips,
|
"optionalSkipsRecorded": optional_skips,
|
||||||
"testEvidenceQualifier": "Scorecard-recorded run; tests do not prove real-platform or broker gates.",
|
"testEvidenceQualifier": "Scorecard-recorded run; tests do not prove real-platform or broker gates.",
|
||||||
|
"candidateRealPlatformObserved": bool(
|
||||||
|
reachability["realPlatformsObserved"]
|
||||||
|
),
|
||||||
|
"candidateRealPlatformObservationScope": (
|
||||||
|
"target_package_execution_consumer_only"
|
||||||
|
),
|
||||||
|
"allFiveLayersRealPlatformObserved": bool(
|
||||||
|
reachability["summary"][
|
||||||
|
"all_layers_real_platform_observed"
|
||||||
|
]
|
||||||
|
),
|
||||||
"realRuntimeSmokes": [
|
"realRuntimeSmokes": [
|
||||||
"JoinQuant hosted daily backtest (runtime/path only)",
|
"JoinQuant TargetPackage execution consumer: one synthetic-research package, 3 orders / 3 fills (not Baseline/G9 evidence)",
|
||||||
|
"JoinQuant portable_momentum_smoke hosted daily backtest (not Baseline/TargetPackage evidence)",
|
||||||
"Qlib native momentum fixture",
|
"Qlib native momentum fixture",
|
||||||
"Qlib Alpha158 + LightGBM fixture",
|
"Qlib Alpha158 + LightGBM fixture",
|
||||||
"Tushare completed-partition Qlib momentum smoke",
|
"Tushare completed-partition Qlib momentum smoke",
|
||||||
@@ -915,12 +1341,45 @@ def build_public_status(
|
|||||||
"productionReady": False,
|
"productionReady": False,
|
||||||
"investmentValueClaim": False,
|
"investmentValueClaim": False,
|
||||||
},
|
},
|
||||||
|
"candidateBoundary": {
|
||||||
|
"decisionMode": "target_package",
|
||||||
|
"localEvidenceClass": "synthetic_research",
|
||||||
|
"connectedRiskModel": "60-session diagonal realized variance",
|
||||||
|
"connectedOptimizer": "deterministic constrained optimizer",
|
||||||
|
"connectedExecution": "reference one-shot quantity/order-delta planner",
|
||||||
|
"realJoinQuantObserved": (
|
||||||
|
"joinquant" in reachability["realPlatformsObserved"]
|
||||||
|
),
|
||||||
|
"realQmtObserved": (
|
||||||
|
"qmt" in reachability["realPlatformsObserved"]
|
||||||
|
),
|
||||||
|
"realPlatformObservedLayers": reachability[
|
||||||
|
"realPlatformObservedLayers"
|
||||||
|
],
|
||||||
|
"allFiveLayersRealPlatformObserved": bool(
|
||||||
|
reachability["summary"][
|
||||||
|
"all_layers_real_platform_observed"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"joinQuantTargetPackageRun": joinquant_target_package_run,
|
||||||
|
"joinQuantTargetPackageCountsAsBaseline": False,
|
||||||
|
"joinQuantTargetPackageCountsAsG9": False,
|
||||||
|
"historicalJoinQuantMode": "portable_momentum_smoke",
|
||||||
|
"historicalJoinQuantCountsAsBaseline": False,
|
||||||
|
"reachability": reachability,
|
||||||
|
"releaseVerified": False,
|
||||||
|
},
|
||||||
|
"deliveryGaps": {
|
||||||
|
"internal": [str(item) for item in internal_delivery_gaps],
|
||||||
|
"externalEvidence": [str(item) for item in external_evidence_missing],
|
||||||
|
},
|
||||||
"layers": _build_layers(),
|
"layers": _build_layers(),
|
||||||
"models": _build_models(),
|
"models": _build_models(),
|
||||||
"platforms": _build_platforms(platform_markdown),
|
"platforms": _build_platforms(platform_markdown),
|
||||||
"freshness": _build_freshness(
|
"freshness": _build_freshness(
|
||||||
scorecard=scorecard,
|
scorecard=scorecard,
|
||||||
joinquant_markdown=joinquant_markdown,
|
joinquant_markdown=joinquant_markdown,
|
||||||
|
joinquant_target_package_run=joinquant_target_package_run,
|
||||||
tushare_markdown=tushare_markdown,
|
tushare_markdown=tushare_markdown,
|
||||||
),
|
),
|
||||||
"gates": gates,
|
"gates": gates,
|
||||||
@@ -954,6 +1413,7 @@ def build_public_status(
|
|||||||
"provenance": {
|
"provenance": {
|
||||||
"scorecard": "gate_scorecard.json",
|
"scorecard": "gate_scorecard.json",
|
||||||
"bundleManifest": "dist/bundle_manifest.json",
|
"bundleManifest": "dist/bundle_manifest.json",
|
||||||
|
"releaseReachability": REACHABILITY_PATH,
|
||||||
"platformMatrix": "docs/PLATFORM_MATRIX.md",
|
"platformMatrix": "docs/PLATFORM_MATRIX.md",
|
||||||
"baselineConfig": "configs/baseline.json",
|
"baselineConfig": "configs/baseline.json",
|
||||||
"sourceSetSha256": source_set_sha,
|
"sourceSetSha256": source_set_sha,
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
"""Build the conservative Quant60 research-candidate reachability manifest.
|
||||||
|
|
||||||
|
The hosted-platform claims in this manifest have deliberately narrow
|
||||||
|
semantics. For Universe, Alpha, Portfolio, and Risk, ``*_entrypoint_reachable``
|
||||||
|
means that the locally computed output and its lineage reach the hosted
|
||||||
|
consumer through a verified ``TargetPackageV1``. It does *not* mean that
|
||||||
|
JoinQuant or QMT execute those upstream Python implementations. Execution is
|
||||||
|
the only layer implemented inside the hosted wrappers.
|
||||||
|
|
||||||
|
One real JoinQuant TargetPackage smoke observes the execution consumer,
|
||||||
|
account/price binding, orders, and fills. It therefore supports only
|
||||||
|
``execution.real_platform_observed``. The other four layer observations and
|
||||||
|
the derived all-layer observation remain false.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT = Path(__file__).resolve().parents[1]
|
||||||
|
SOURCE_ROOT = PROJECT / "src"
|
||||||
|
if str(SOURCE_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SOURCE_ROOT))
|
||||||
|
|
||||||
|
from quant60.ledger import canonical_json # noqa: E402
|
||||||
|
from quant60.release_reachability import ( # noqa: E402
|
||||||
|
LAYERS,
|
||||||
|
build_evidence_artifact,
|
||||||
|
build_release_reachability,
|
||||||
|
load_verified_release_reachability,
|
||||||
|
write_release_reachability,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
RELEASE_ID = "quant60-research-candidate-v1"
|
||||||
|
RELEASE_DECLARED_AT = "2026-07-26T12:00:00Z"
|
||||||
|
DEFAULT_OUTPUT = (
|
||||||
|
PROJECT / "releases" / RELEASE_ID / "reachability.json"
|
||||||
|
)
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_PATH = (
|
||||||
|
"evidence/joinquant/TP-20240311-795dcfba/"
|
||||||
|
"2457a7c39a276e09e0fabf99e28978e1/run.json"
|
||||||
|
)
|
||||||
|
JOINQUANT_TARGET_PACKAGE_RUN_ID = "2457a7c39a276e09e0fabf99e28978e1"
|
||||||
|
JOINQUANT_TARGET_PACKAGE_OBSERVED_AT = "2026-07-26T08:50:15Z"
|
||||||
|
PLATFORM_CLAIM_NOTE = (
|
||||||
|
"Upstream layer status means its locally computed output/lineage is "
|
||||||
|
"carried through TargetPackageV1 to the hosted consumer; it does not "
|
||||||
|
"claim JoinQuant/QMT executes the upstream Python. Execution alone runs "
|
||||||
|
"inside the hosted wrapper. One JoinQuant smoke observes only that "
|
||||||
|
"execution boundary; it does not observe all five layers."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Each path names code used by the connected local five-layer decision path.
|
||||||
|
# A shared composition module appears in several claims by design; artifact IDs
|
||||||
|
# remain unique, as required by the reachability contract.
|
||||||
|
IMPLEMENTED_EVIDENCE: dict[str, tuple[tuple[str, str], ...]] = {
|
||||||
|
"universe": (
|
||||||
|
("baseline-pipeline", "src/quant60/baseline_pipeline.py"),
|
||||||
|
),
|
||||||
|
"alpha": (
|
||||||
|
("frozen-model-bundle", "src/quant60/model_bundle.py"),
|
||||||
|
("baseline-pipeline", "src/quant60/baseline_pipeline.py"),
|
||||||
|
),
|
||||||
|
"portfolio": (
|
||||||
|
("optimizer", "src/quant60/optimizer.py"),
|
||||||
|
("baseline-pipeline", "src/quant60/baseline_pipeline.py"),
|
||||||
|
),
|
||||||
|
"risk": (
|
||||||
|
("risk-gate", "src/quant60/risk.py"),
|
||||||
|
("baseline-pipeline", "src/quant60/baseline_pipeline.py"),
|
||||||
|
),
|
||||||
|
"execution": (
|
||||||
|
("portable-execution-core", "src/quant60/portable_core.py"),
|
||||||
|
("baseline-pipeline", "src/quant60/baseline_pipeline.py"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
UNIT_TEST_EVIDENCE: dict[str, tuple[tuple[str, str], ...]] = {
|
||||||
|
"universe": (
|
||||||
|
("five-layer-causality", "tests/test_baseline_pipeline.py"),
|
||||||
|
),
|
||||||
|
"alpha": (
|
||||||
|
("five-layer-causality", "tests/test_baseline_pipeline.py"),
|
||||||
|
("frozen-model-replay", "tests/test_model_bundle.py"),
|
||||||
|
),
|
||||||
|
"portfolio": (
|
||||||
|
("five-layer-causality", "tests/test_baseline_pipeline.py"),
|
||||||
|
),
|
||||||
|
"risk": (
|
||||||
|
("five-layer-causality", "tests/test_baseline_pipeline.py"),
|
||||||
|
),
|
||||||
|
"execution": (
|
||||||
|
("five-layer-causality", "tests/test_baseline_pipeline.py"),
|
||||||
|
("portable-order-plan", "tests/test_portable_core.py"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
LOCAL_ENTRYPOINT_EVIDENCE: tuple[tuple[str, str], ...] = (
|
||||||
|
("research-smoke-cli", "src/quant60/cli.py"),
|
||||||
|
("research-experiment", "src/quant60/experiment.py"),
|
||||||
|
)
|
||||||
|
|
||||||
|
PLATFORM_ENTRYPOINTS = {
|
||||||
|
"jq": {
|
||||||
|
"capability": "jq_entrypoint_reachable",
|
||||||
|
"platform": "joinquant",
|
||||||
|
"path": "platforms/joinquant_strategy.py",
|
||||||
|
},
|
||||||
|
"qmt": {
|
||||||
|
"capability": "qmt_entrypoint_reachable",
|
||||||
|
"platform": "qmt",
|
||||||
|
"path": "platforms/qmt_builtin_strategy.py",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# These labels are part of the signed artifact IDs. They keep the narrow
|
||||||
|
# meaning visible even though the v1 schema intentionally has no free-text
|
||||||
|
# claim field.
|
||||||
|
PLATFORM_OUTPUT_LABELS = {
|
||||||
|
"universe": "universe-state-via-targetpackagev1-consumer",
|
||||||
|
"alpha": "alpha-lineage-derived-target-via-targetpackagev1-consumer",
|
||||||
|
"portfolio": "portfolio-weight-via-targetpackagev1-consumer",
|
||||||
|
"risk": "post-risk-weight-constraints-via-targetpackagev1-consumer",
|
||||||
|
"execution": "targetpackagev1-hosted-execution-entrypoint",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _false_claim() -> dict[str, Any]:
|
||||||
|
return {"status": False, "artifacts": []}
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence(
|
||||||
|
*,
|
||||||
|
repository_root: Path,
|
||||||
|
artifact_id: str,
|
||||||
|
kind: str,
|
||||||
|
path: str,
|
||||||
|
platform: str,
|
||||||
|
observed_at: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return build_evidence_artifact(
|
||||||
|
artifact_id=artifact_id,
|
||||||
|
kind=kind,
|
||||||
|
path=path,
|
||||||
|
platform=platform,
|
||||||
|
repository_root=repository_root,
|
||||||
|
observed_at=observed_at,
|
||||||
|
run_id=run_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_manifest(
|
||||||
|
*,
|
||||||
|
repository_root: str | Path = PROJECT,
|
||||||
|
generated_at: str = RELEASE_DECLARED_AT,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Build a fully content-addressed manifest from the current repository."""
|
||||||
|
|
||||||
|
root = Path(repository_root)
|
||||||
|
layers: dict[str, dict[str, Any]] = {}
|
||||||
|
for layer in LAYERS:
|
||||||
|
implemented = [
|
||||||
|
_evidence(
|
||||||
|
repository_root=root,
|
||||||
|
artifact_id=f"{layer}-implemented-{label}",
|
||||||
|
kind="source",
|
||||||
|
path=path,
|
||||||
|
platform="repository",
|
||||||
|
)
|
||||||
|
for label, path in IMPLEMENTED_EVIDENCE[layer]
|
||||||
|
]
|
||||||
|
unit_tested = [
|
||||||
|
_evidence(
|
||||||
|
repository_root=root,
|
||||||
|
artifact_id=f"{layer}-unit-tested-{label}",
|
||||||
|
kind="test",
|
||||||
|
path=path,
|
||||||
|
platform="repository",
|
||||||
|
)
|
||||||
|
for label, path in UNIT_TEST_EVIDENCE[layer]
|
||||||
|
]
|
||||||
|
local_entrypoint = [
|
||||||
|
_evidence(
|
||||||
|
repository_root=root,
|
||||||
|
artifact_id=f"{layer}-local-{label}",
|
||||||
|
kind="entrypoint",
|
||||||
|
path=path,
|
||||||
|
platform="local",
|
||||||
|
)
|
||||||
|
for label, path in LOCAL_ENTRYPOINT_EVIDENCE
|
||||||
|
]
|
||||||
|
claims: dict[str, Any] = {
|
||||||
|
"implemented": {"status": True, "artifacts": implemented},
|
||||||
|
"unit_tested": {"status": True, "artifacts": unit_tested},
|
||||||
|
"local_entrypoint_reachable": {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": local_entrypoint,
|
||||||
|
},
|
||||||
|
"real_platform_observed": _false_claim(),
|
||||||
|
}
|
||||||
|
if layer == "execution":
|
||||||
|
claims["real_platform_observed"] = {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": [
|
||||||
|
_evidence(
|
||||||
|
repository_root=root,
|
||||||
|
artifact_id=(
|
||||||
|
"execution-real-platform-targetpackagev1-smoke-jq"
|
||||||
|
),
|
||||||
|
kind="platform_run",
|
||||||
|
path=JOINQUANT_TARGET_PACKAGE_RUN_PATH,
|
||||||
|
platform="joinquant",
|
||||||
|
observed_at=JOINQUANT_TARGET_PACKAGE_OBSERVED_AT,
|
||||||
|
run_id=JOINQUANT_TARGET_PACKAGE_RUN_ID,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
for name, declaration in PLATFORM_ENTRYPOINTS.items():
|
||||||
|
capability = declaration["capability"]
|
||||||
|
claims[capability] = {
|
||||||
|
"status": True,
|
||||||
|
"artifacts": [
|
||||||
|
_evidence(
|
||||||
|
repository_root=root,
|
||||||
|
artifact_id=(
|
||||||
|
f"{layer}-{PLATFORM_OUTPUT_LABELS[layer]}-{name}"
|
||||||
|
),
|
||||||
|
kind="entrypoint",
|
||||||
|
path=declaration["path"],
|
||||||
|
platform=declaration["platform"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
layers[layer] = claims
|
||||||
|
|
||||||
|
return build_release_reachability(
|
||||||
|
release_id=RELEASE_ID,
|
||||||
|
generated_at=generated_at,
|
||||||
|
layers=layers,
|
||||||
|
repository_root=root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
type=Path,
|
||||||
|
default=DEFAULT_OUTPUT,
|
||||||
|
help="manifest path (default: releases/<release>/reachability.json)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--generated-at",
|
||||||
|
default=RELEASE_DECLARED_AT,
|
||||||
|
help="canonical UTC release declaration time",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--check",
|
||||||
|
action="store_true",
|
||||||
|
help="verify that the existing manifest exactly matches regeneration",
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = _build_parser().parse_args(argv)
|
||||||
|
expected = build_manifest(generated_at=args.generated_at)
|
||||||
|
output = args.output.expanduser().resolve()
|
||||||
|
if args.check:
|
||||||
|
observed = load_verified_release_reachability(
|
||||||
|
output,
|
||||||
|
repository_root=PROJECT,
|
||||||
|
expected_release_id=RELEASE_ID,
|
||||||
|
expected_manifest_sha256=expected["manifest_sha256"],
|
||||||
|
)
|
||||||
|
expected_bytes = (canonical_json(expected) + "\n").encode("utf-8")
|
||||||
|
if observed != expected or output.read_bytes() != expected_bytes:
|
||||||
|
raise ValueError(
|
||||||
|
"reachability manifest does not exactly match regeneration"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
write_release_reachability(
|
||||||
|
expected,
|
||||||
|
output,
|
||||||
|
repository_root=PROJECT,
|
||||||
|
)
|
||||||
|
summary = {
|
||||||
|
"ok": True,
|
||||||
|
"checked": bool(args.check),
|
||||||
|
"output": str(output),
|
||||||
|
"release_id": RELEASE_ID,
|
||||||
|
"manifest_sha256": expected["manifest_sha256"],
|
||||||
|
"platform_claim_semantics": PLATFORM_CLAIM_NOTE,
|
||||||
|
}
|
||||||
|
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+196
-14
@@ -7,14 +7,33 @@ import ast
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import pprint
|
import pprint
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
START = "# __PORTABLE_CORE_BUNDLE_START__"
|
START = "# __PORTABLE_CORE_BUNDLE_START__"
|
||||||
END = "# __PORTABLE_CORE_BUNDLE_END__"
|
END = "# __PORTABLE_CORE_BUNDLE_END__"
|
||||||
CONFIG_START = "# __BASELINE_CONFIG_START__"
|
CONFIG_START = "# __BASELINE_CONFIG_START__"
|
||||||
CONFIG_END = "# __BASELINE_CONFIG_END__"
|
CONFIG_END = "# __BASELINE_CONFIG_END__"
|
||||||
|
TARGET_START = "# __TARGET_PACKAGE_TAPE_START__"
|
||||||
|
TARGET_END = "# __TARGET_PACKAGE_TAPE_END__"
|
||||||
|
MOMENTUM_SMOKE_MODE = "portable_momentum_smoke"
|
||||||
|
TARGET_PACKAGE_MODE = "target_package"
|
||||||
|
JOINQUANT_EVIDENCE_PREFIX = "QUANT60_JOINQUANT_EVIDENCE_V1 "
|
||||||
|
JOINQUANT_EVIDENCE_CHUNK_CHARACTERS = 1800
|
||||||
|
JOINQUANT_TARGET_PRICE_SOURCE = "CURRENT_DATA_DAY_OPEN"
|
||||||
|
JOINQUANT_EVIDENCE_EVENTS = [
|
||||||
|
"INIT",
|
||||||
|
"TARGET_PACKAGE_NOOP",
|
||||||
|
"TARGET_PACKAGE_HIT",
|
||||||
|
"PLATFORM_BINDING_INPUT",
|
||||||
|
"TARGET_PACKAGE_PLAN",
|
||||||
|
"SKIPPED_ORDER",
|
||||||
|
"ORDER_REQUEST",
|
||||||
|
"ORDER_RETURN",
|
||||||
|
"EOD_STATUS",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def sha256_bytes(value: bytes) -> str:
|
def sha256_bytes(value: bytes) -> str:
|
||||||
@@ -56,6 +75,73 @@ def inline_core(wrapper_source: str, core_source: str) -> str:
|
|||||||
return wrapper_source[:start] + replacement + wrapper_source[end:]
|
return wrapper_source[:start] + replacement + wrapper_source[end:]
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_target_tape_bytes(
|
||||||
|
target_packages: List[Dict[str, Any]],
|
||||||
|
) -> bytes:
|
||||||
|
return json.dumps(
|
||||||
|
target_packages,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def inline_target_packages(
|
||||||
|
wrapper_source: str,
|
||||||
|
target_packages: List[Dict[str, Any]],
|
||||||
|
) -> str:
|
||||||
|
"""Inline a pre-validated, quantity-free decision tape."""
|
||||||
|
|
||||||
|
if (
|
||||||
|
wrapper_source.count(TARGET_START) != 1
|
||||||
|
or wrapper_source.count(TARGET_END) != 1
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"wrapper must contain exactly one target-package marker pair"
|
||||||
|
)
|
||||||
|
start = wrapper_source.index(TARGET_START)
|
||||||
|
end = wrapper_source.index(TARGET_END, start) + len(TARGET_END)
|
||||||
|
body = pprint.pformat(
|
||||||
|
target_packages,
|
||||||
|
width=88,
|
||||||
|
sort_dicts=False,
|
||||||
|
)
|
||||||
|
body = body.encode("ascii", "backslashreplace").decode("ascii")
|
||||||
|
replacement = (
|
||||||
|
TARGET_START
|
||||||
|
+ "\n# Generated by tools/bundle_platforms.py; do not edit this region.\n"
|
||||||
|
+ "TARGET_PACKAGES = "
|
||||||
|
+ body
|
||||||
|
+ "\n"
|
||||||
|
+ TARGET_END
|
||||||
|
)
|
||||||
|
return wrapper_source[:start] + replacement + wrapper_source[end:]
|
||||||
|
|
||||||
|
|
||||||
|
def load_verified_target_packages(
|
||||||
|
project_root: Path,
|
||||||
|
path: Path,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Load one package or a multi-decision tape through the canonical verifier."""
|
||||||
|
|
||||||
|
root = project_root.expanduser().resolve()
|
||||||
|
source = path.expanduser().resolve()
|
||||||
|
src_path = str(root / "src")
|
||||||
|
inserted = src_path not in sys.path
|
||||||
|
if inserted:
|
||||||
|
sys.path.insert(0, src_path)
|
||||||
|
try:
|
||||||
|
from quant60.target_package import ( # pylint: disable=import-outside-toplevel
|
||||||
|
load_target_package_tape,
|
||||||
|
)
|
||||||
|
|
||||||
|
return list(load_target_package_tape(source))
|
||||||
|
finally:
|
||||||
|
if inserted:
|
||||||
|
sys.path.remove(src_path)
|
||||||
|
|
||||||
|
|
||||||
def _qmt_symbol(symbol: str) -> str:
|
def _qmt_symbol(symbol: str) -> str:
|
||||||
suffixes = {
|
suffixes = {
|
||||||
".XSHG": ".SH",
|
".XSHG": ".SH",
|
||||||
@@ -71,20 +157,52 @@ def _qmt_symbol(symbol: str) -> str:
|
|||||||
def effective_platform_config(
|
def effective_platform_config(
|
||||||
baseline: Dict[str, Any],
|
baseline: Dict[str, Any],
|
||||||
platform: str,
|
platform: str,
|
||||||
|
*,
|
||||||
|
decision_mode: str = MOMENTUM_SMOKE_MODE,
|
||||||
|
target_packages: Optional[List[Dict[str, Any]]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
packages = list(target_packages or [])
|
||||||
|
if decision_mode not in {MOMENTUM_SMOKE_MODE, TARGET_PACKAGE_MODE}:
|
||||||
|
raise ValueError(f"unsupported decision_mode: {decision_mode}")
|
||||||
|
if decision_mode == TARGET_PACKAGE_MODE and not packages:
|
||||||
|
raise ValueError("target_package mode requires a non-empty package tape")
|
||||||
|
if decision_mode == MOMENTUM_SMOKE_MODE and packages:
|
||||||
|
raise ValueError("momentum smoke mode cannot embed target packages")
|
||||||
strategy = baseline["strategy"]
|
strategy = baseline["strategy"]
|
||||||
execution = baseline["execution"]
|
execution = baseline["execution"]
|
||||||
fees = baseline["fees"]
|
fees = baseline["fees"]
|
||||||
universe_config = baseline["universe"]
|
universe_config = baseline["universe"]
|
||||||
symbols = [str(symbol) for symbol in baseline["symbols"]]
|
symbols = (
|
||||||
|
sorted(
|
||||||
|
{
|
||||||
|
str(symbol)
|
||||||
|
for package in packages
|
||||||
|
for symbol in package["universe"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if packages
|
||||||
|
else [str(symbol) for symbol in baseline["symbols"]]
|
||||||
|
)
|
||||||
if platform == "qmt_builtin":
|
if platform == "qmt_builtin":
|
||||||
universe = [_qmt_symbol(symbol) for symbol in symbols]
|
universe = [_qmt_symbol(symbol) for symbol in symbols]
|
||||||
elif platform == "joinquant":
|
elif platform == "joinquant":
|
||||||
universe = symbols
|
universe = symbols
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"unsupported platform config: {platform}")
|
raise ValueError(f"unsupported platform config: {platform}")
|
||||||
|
tape_sha256 = (
|
||||||
|
sha256_bytes(_canonical_target_tape_bytes(packages))
|
||||||
|
if packages
|
||||||
|
else None
|
||||||
|
)
|
||||||
common: Dict[str, Any] = {
|
common: Dict[str, Any] = {
|
||||||
"universe_mode": str(universe_config["mode"]),
|
"decision_mode": decision_mode,
|
||||||
|
"target_package_count": len(packages),
|
||||||
|
"target_package_tape_sha256": tape_sha256,
|
||||||
|
"universe_mode": (
|
||||||
|
"fixed"
|
||||||
|
if decision_mode == TARGET_PACKAGE_MODE
|
||||||
|
else str(universe_config["mode"])
|
||||||
|
),
|
||||||
"index_symbol": str(universe_config["index_symbol"]),
|
"index_symbol": str(universe_config["index_symbol"]),
|
||||||
"qmt_sector_name": str(universe_config["qmt_sector_name"]),
|
"qmt_sector_name": str(universe_config["qmt_sector_name"]),
|
||||||
"dedicated_account_required": bool(
|
"dedicated_account_required": bool(
|
||||||
@@ -145,15 +263,19 @@ def bundle_one(
|
|||||||
wrapper_path: Path,
|
wrapper_path: Path,
|
||||||
output_path: Path,
|
output_path: Path,
|
||||||
effective_config: Dict[str, Any],
|
effective_config: Dict[str, Any],
|
||||||
|
target_packages: Optional[List[Dict[str, Any]]] = None,
|
||||||
require_ascii: bool = False,
|
require_ascii: bool = False,
|
||||||
) -> Dict[str, str]:
|
) -> Dict[str, str]:
|
||||||
core_bytes = core_path.read_bytes()
|
core_bytes = core_path.read_bytes()
|
||||||
wrapper_bytes = wrapper_path.read_bytes()
|
wrapper_bytes = wrapper_path.read_bytes()
|
||||||
core_source = core_bytes.decode("utf-8")
|
core_source = core_bytes.decode("utf-8")
|
||||||
wrapper_source = wrapper_bytes.decode("utf-8")
|
wrapper_source = wrapper_bytes.decode("utf-8")
|
||||||
bundled = inline_config(
|
bundled = inline_target_packages(
|
||||||
|
inline_config(
|
||||||
inline_core(wrapper_source, core_source),
|
inline_core(wrapper_source, core_source),
|
||||||
effective_config,
|
effective_config,
|
||||||
|
),
|
||||||
|
list(target_packages or []),
|
||||||
)
|
)
|
||||||
output_bytes = bundled.encode("utf-8")
|
output_bytes = bundled.encode("utf-8")
|
||||||
if require_ascii:
|
if require_ascii:
|
||||||
@@ -194,6 +316,7 @@ def bundle_one(
|
|||||||
def bundle_all(
|
def bundle_all(
|
||||||
project_root: Path,
|
project_root: Path,
|
||||||
output_dir: Optional[Path] = None,
|
output_dir: Optional[Path] = None,
|
||||||
|
target_package_path: Optional[Path] = None,
|
||||||
) -> Dict[str, object]:
|
) -> Dict[str, object]:
|
||||||
root = project_root.expanduser().resolve()
|
root = project_root.expanduser().resolve()
|
||||||
output = (
|
output = (
|
||||||
@@ -209,43 +332,90 @@ def bundle_all(
|
|||||||
baseline = json.loads(baseline_bytes.decode("utf-8"))
|
baseline = json.loads(baseline_bytes.decode("utf-8"))
|
||||||
if not isinstance(baseline, dict):
|
if not isinstance(baseline, dict):
|
||||||
raise ValueError("configs/baseline.json must contain an object")
|
raise ValueError("configs/baseline.json must contain an object")
|
||||||
|
target_packages = (
|
||||||
|
load_verified_target_packages(root, target_package_path)
|
||||||
|
if target_package_path is not None
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
decision_mode = (
|
||||||
|
TARGET_PACKAGE_MODE if target_packages else MOMENTUM_SMOKE_MODE
|
||||||
|
)
|
||||||
entries = {
|
entries = {
|
||||||
"joinquant": bundle_one(
|
"joinquant": bundle_one(
|
||||||
core_path=core,
|
core_path=core,
|
||||||
wrapper_path=root / "platforms" / "joinquant_strategy.py",
|
wrapper_path=root / "platforms" / "joinquant_strategy.py",
|
||||||
output_path=output / "joinquant_strategy.py",
|
output_path=output / "joinquant_strategy.py",
|
||||||
effective_config=effective_platform_config(baseline, "joinquant"),
|
effective_config=effective_platform_config(
|
||||||
|
baseline,
|
||||||
|
"joinquant",
|
||||||
|
decision_mode=decision_mode,
|
||||||
|
target_packages=target_packages,
|
||||||
|
),
|
||||||
|
target_packages=target_packages,
|
||||||
),
|
),
|
||||||
"qmt_builtin": bundle_one(
|
"qmt_builtin": bundle_one(
|
||||||
core_path=core,
|
core_path=core,
|
||||||
wrapper_path=root / "platforms" / "qmt_builtin_strategy.py",
|
wrapper_path=root / "platforms" / "qmt_builtin_strategy.py",
|
||||||
output_path=output / "qmt_builtin_strategy.py",
|
output_path=output / "qmt_builtin_strategy.py",
|
||||||
effective_config=effective_platform_config(baseline, "qmt_builtin"),
|
effective_config=effective_platform_config(
|
||||||
|
baseline,
|
||||||
|
"qmt_builtin",
|
||||||
|
decision_mode=decision_mode,
|
||||||
|
target_packages=target_packages,
|
||||||
|
),
|
||||||
|
target_packages=target_packages,
|
||||||
require_ascii=True,
|
require_ascii=True,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
# Paths in a committed manifest must be portable and must not expose the
|
# Paths in a manifest must be portable and must not expose the local
|
||||||
# local account/home directory. Hashes remain byte-for-byte identities.
|
# account/home directory. Preserve a real project-relative location for
|
||||||
logical_outputs = {
|
# outputs under the project (for example dist/target-package/...). A
|
||||||
|
# clean-room output outside the project keeps the committed logical names
|
||||||
|
# so its manifest remains byte-for-byte comparable with committed dist.
|
||||||
|
default_logical_outputs = {
|
||||||
"joinquant": "dist/joinquant_strategy.py",
|
"joinquant": "dist/joinquant_strategy.py",
|
||||||
"qmt_builtin": "dist/qmt_builtin_strategy.py",
|
"qmt_builtin": "dist/qmt_builtin_strategy.py",
|
||||||
}
|
}
|
||||||
for platform_name, entry in entries.items():
|
for platform_name, entry in entries.items():
|
||||||
|
physical_output = Path(entry["output"])
|
||||||
entry["core"] = "src/quant60/portable_core.py"
|
entry["core"] = "src/quant60/portable_core.py"
|
||||||
entry["wrapper"] = (
|
entry["wrapper"] = (
|
||||||
"platforms/joinquant_strategy.py"
|
"platforms/joinquant_strategy.py"
|
||||||
if "joinquant" in entry["wrapper"]
|
if "joinquant" in entry["wrapper"]
|
||||||
else "platforms/qmt_builtin_strategy.py"
|
else "platforms/qmt_builtin_strategy.py"
|
||||||
)
|
)
|
||||||
# The manifest describes the committed logical artifact, not the
|
try:
|
||||||
# caller's temporary build directory. This keeps clean-room builds
|
entry["output"] = physical_output.relative_to(root).as_posix()
|
||||||
# byte-for-byte comparable with committed ``dist``.
|
except ValueError:
|
||||||
entry["output"] = logical_outputs[platform_name]
|
entry["output"] = default_logical_outputs[platform_name]
|
||||||
manifest = {
|
manifest = {
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"baseline_config": "configs/baseline.json",
|
"baseline_config": "configs/baseline.json",
|
||||||
"baseline_config_sha256": sha256_bytes(baseline_bytes),
|
"baseline_config_sha256": sha256_bytes(baseline_bytes),
|
||||||
"portable_core_sha256": entries["joinquant"]["core_sha256"],
|
"portable_core_sha256": entries["joinquant"]["core_sha256"],
|
||||||
|
"decision_mode": decision_mode,
|
||||||
|
"target_package_count": len(target_packages),
|
||||||
|
"target_package_tape_sha256": (
|
||||||
|
sha256_bytes(_canonical_target_tape_bytes(target_packages))
|
||||||
|
if target_packages
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"target_package_ids": [
|
||||||
|
package["package_id"] for package in target_packages
|
||||||
|
],
|
||||||
|
"target_package_sha256": [
|
||||||
|
package["package_sha256"] for package in target_packages
|
||||||
|
],
|
||||||
|
"joinquant_observation": {
|
||||||
|
"eod_chunk_characters": JOINQUANT_EVIDENCE_CHUNK_CHARACTERS,
|
||||||
|
"eod_chunk_encoding": "base64-canonical-json",
|
||||||
|
"eod_api_policy": "best_effort_get_orders_get_trades",
|
||||||
|
"events": JOINQUANT_EVIDENCE_EVENTS,
|
||||||
|
"fail_soft": True,
|
||||||
|
"prefix": JOINQUANT_EVIDENCE_PREFIX,
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"target_price_source": JOINQUANT_TARGET_PRICE_SOURCE,
|
||||||
|
},
|
||||||
"artifacts": entries,
|
"artifacts": entries,
|
||||||
}
|
}
|
||||||
manifest_path = output / "bundle_manifest.json"
|
manifest_path = output / "bundle_manifest.json"
|
||||||
@@ -266,8 +436,20 @@ def _main(argv: Optional[list[str]] = None) -> int:
|
|||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--project-root", type=Path, default=default_root)
|
parser.add_argument("--project-root", type=Path, default=default_root)
|
||||||
parser.add_argument("--output-dir", type=Path)
|
parser.add_argument("--output-dir", type=Path)
|
||||||
|
parser.add_argument(
|
||||||
|
"--target-package",
|
||||||
|
type=Path,
|
||||||
|
help=(
|
||||||
|
"verified TargetPackageV1 JSON/JSONL tape; switches both hosted "
|
||||||
|
"artifacts from momentum smoke to target-package execution"
|
||||||
|
),
|
||||||
|
)
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
manifest = bundle_all(args.project_root, args.output_dir)
|
manifest = bundle_all(
|
||||||
|
args.project_root,
|
||||||
|
args.output_dir,
|
||||||
|
args.target_package,
|
||||||
|
)
|
||||||
print(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True))
|
print(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user