From 61f1861b4dc9ad802f3968ee3b52eccc0075f82b Mon Sep 17 00:00:00 2001 From: liuli195 Date: Thu, 16 Jul 2026 21:12:58 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A0=94=E7=A9=B6=EF=BC=9A=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E6=89=A9=E5=B1=95=E7=AD=9B=E9=80=89=E5=B9=B6?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=A8=A1=E6=8B=9F=E4=BA=A4=E6=98=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .comet.yaml | 8 + ...7-16-turtle-independent-asset-expansion.md | 1523 +++++++++++++++++ ...rtle-independent-asset-expansion-design.md | 265 +++ .../strategies/strategy-001/manifest.json | 7 +- ...e-history-93a631dd3a95632d3b3a502f.json.gz | 3 + ...ution_log-0dcd6b38e3665bab5046f63f.parquet | 3 + .../data/balances.parquet | 3 + .../data/orders.parquet | 3 + .../data/positions.parquet | 3 + .../data/results.parquet | 3 + .../data/risk.parquet | 3 + ...tion-log-0dcd6b38e3665bab5046f63f.jsonl.gz | 3 + .../raw/balances.json.gz | 3 + .../raw/normal-log-pages.json.gz | 3 + .../raw/normal-log.jsonl.gz | 3 + .../raw/orders.json.gz | 3 + .../raw/period_risks.json.gz | 3 + .../raw/positions.json.gz | 3 + .../raw/records.json.gz | 3 + ...-response-724bc05499f352dc6bdfdf78.json.gz | 3 + .../raw/results.json.gz | 3 + .../e85f5fe70206488491dd5068/raw/risk.json.gz | 3 + .../reports/live-summary.json.gz | 3 + ...aaac9a9f92dfd5552f1a12488029d805a8323.json | 3 + .../strategies/strategy-002/manifest.json | 7 +- ...e-history-220beb0c54bf08fe184a740a.json.gz | 3 + .../data/balances.parquet | 3 + .../data/orders.parquet | 3 + .../data/positions.parquet | 3 + .../data/results.parquet | 3 + .../data/risk.parquet | 3 + .../raw/balances.json.gz | 3 + .../raw/normal-log-pages.json.gz | 3 + .../raw/normal-log.jsonl.gz | 3 + .../raw/orders.json.gz | 3 + .../raw/period_risks.json.gz | 3 + .../raw/positions.json.gz | 3 + .../raw/records.json.gz | 3 + ...-response-537e1e2e2ace4daeb56a969f.json.gz | 3 + .../raw/results.json.gz | 3 + .../58318b3133b566a639b76b59/raw/risk.json.gz | 3 + .../reports/live-summary.json.gz | 3 + ...eaceef8fb9d48452566a0365d64aba4d2d9fc.json | 3 + joinquant/strategies/strategy_index.csv | 2 +- .../local-quant-research-workflow/spec.md | 10 + .../research/market_data/candidate_screen.py | 321 ++++ .../research/market_data/joinquant_export.py | 17 +- .../test_joinquant_export.py | 12 +- .../test_market_data_candidate_screen.py | 149 ++ 49 files changed, 2427 insertions(+), 8 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-16-turtle-independent-asset-expansion.md create mode 100644 docs/superpowers/specs/2026-07-16-turtle-independent-asset-expansion-design.md create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/raw/code-history-93a631dd3a95632d3b3a502f.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/attribution_log-0dcd6b38e3665bab5046f63f.parquet create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/balances.parquet create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/orders.parquet create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/positions.parquet create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/results.parquet create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/risk.parquet create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/attribution-log-0dcd6b38e3665bab5046f63f.jsonl.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/balances.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/normal-log-pages.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/normal-log.jsonl.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/orders.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/period_risks.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/positions.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/records.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/research-response-724bc05499f352dc6bdfdf78.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/results.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/risk.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/reports/live-summary.json.gz create mode 100644 joinquant/strategies/strategy-001/simulations/simulation-001/source_versions/348e35c4d11426ab63444d1e504aaac9a9f92dfd5552f1a12488029d805a8323.json create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/raw/code-history-220beb0c54bf08fe184a740a.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/balances.parquet create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/orders.parquet create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/positions.parquet create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/results.parquet create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/risk.parquet create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/balances.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/normal-log-pages.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/normal-log.jsonl.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/orders.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/period_risks.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/positions.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/records.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/research-response-537e1e2e2ace4daeb56a969f.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/results.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/risk.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/reports/live-summary.json.gz create mode 100644 joinquant/strategies/strategy-002/simulations/simulation-001/source_versions/016ce20a8342a3337b75d3f20e9eaceef8fb9d48452566a0365d64aba4d2d9fc.json create mode 100644 scripts/research/market_data/candidate_screen.py create mode 100644 tests/local_quant_research/test_market_data_candidate_screen.py diff --git a/.comet.yaml b/.comet.yaml index c00936e..c78cecf 100644 --- a/.comet.yaml +++ b/.comet.yaml @@ -2,3 +2,11 @@ build_command: .venv/Scripts/python.exe .build-and-verify/runtime/build_and_verify.py build --project . verify_command: .venv/Scripts/python.exe .build-and-verify/runtime/build_and_verify.py verify --project . + +phase: build +build_mode: executing-plans +tdd_mode: tdd +review_mode: thorough +verify_mode: full +design_doc: docs/superpowers/specs/2026-07-16-turtle-independent-asset-expansion-design.md +plan: docs/superpowers/plans/2026-07-16-turtle-independent-asset-expansion.md diff --git a/docs/superpowers/plans/2026-07-16-turtle-independent-asset-expansion.md b/docs/superpowers/plans/2026-07-16-turtle-independent-asset-expansion.md new file mode 100644 index 0000000..241fe95 --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-turtle-independent-asset-expansion.md @@ -0,0 +1,1523 @@ +# 海龟 ETF 独立资产扩展实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 用六只预先冻结的候选 ETF(交易型开放式指数基金)验证“增加独立风险来源”能否解决现有海龟系统趋势利用不足,并输出真实行情、最多 16 个本地回测结果包、完整分析报告和人工可确认的推荐结论。 + +**Architecture:** 保留现有单场景 `run-local-quant-research` Skill(本地量化研究技能)和标准结果包。共享行情层新增连续经济收益、候选筛选和跨快照重叠校验;海龟项目层新增候选场景矩阵和冻结订单延迟执行;通用分析层按每场景资产池消费真实删除、真实成本结果,并增加有效秩和趋势利用分析。所有场景使用相同不可变行情批次、截止日和口径,但使用与自身证券集合完全匹配的内容寻址快照。 + +**Tech Stack:** Python 3.12(编程语言)、vectorbt 1.1.0(向量化回测框架)、Numba(即时编译)、NumPy(数组计算)、Pandas(数据处理)、PyArrow/Parquet(列式存储)、DuckDB `:memory:`(内存数据库)、JSON Schema(结构契约)、Pytest(测试框架)、JoinQuant(聚宽)研究环境、Vibe-Trading 0.1.10(氛围量化单体审计)。 + +## Global Constraints + +- 所有 Python 命令必须使用 `.\.venv\Scripts\python.exe`;不得使用系统 Python。 +- 正式回测和模拟交易只在 JoinQuant(聚宽)云端运行;本计划只做本地探索性研究,不修改正式策略资产池。 +- 候选固定为 `159980.XSHE`、`159981.XSHE`、`159985.XSHE`、`511260.XSHG`、`513030.XSHG`、`513800.XSHG`,按代码排序;读取回测收益后不得替换、增删或放宽筛选门槛。 +- 行情截止日固定为 `2026-07-13`,`fq=None`、`skip_paused=False`,字段固定为 `MARKET_DATA_FIELDS`;必须同步公司行动证据。 +- 成交额只用于资产池前置筛选;海龟配置、输入、回调、原因码和订单不得出现运行时流动性门槛或“单笔订单不超过成交额 1%”。 +- 主场景显式使用 `commission_multiplier=1.0`、`one_way_slippage=0.0005`;不得依赖引擎当前的零滑点默认值。 +- 本地研究 Skill 每次只运行一个场景并返回 `next_action=return_to_caller`;最多 16 个场景由主 Agent(主代理)逐次调用,Skill 内不得循环、聚合或分析。 +- 单个场景冷启动和预热都必须不超过 180 秒;超时、摘要不一致或标准结果包不完整均为失败。 +- 延迟压力采用“两段式冻结原即时订单”:原执行日完成 A1(统一完成比例)和风险门禁,冻结方向、目标数量、原因码与 N;延迟日只按开盘和成本机械成交,不读取延迟日新信号或新风险估计。 +- 标准结果包物理结构不变;延迟证据写入现有订单时间字段和 attribution(归因)`details_json`。 +- Vibe-Trading 只允许公开单体 `run` 入口做只读定性复核;禁止 `--swarm-run`、Vibe 回测和存在前视偏差的组合优化器;确定性数值和门槛始终是唯一裁判。 +- 只保留 Parquet、清单、摘要、快照、标准结果包和分析交付;聚宽/本地传输 CSV、Vibe 临时提示文件、预热副本和失败临时目录必须在完成前清理。 +- 当前工作树已有用户改动。每个提交步骤都是授权门禁:执行时没有用户新的明确提交授权,就停在未暂存状态,不得提交、切分支或覆盖用户改动。 + +--- + +### Task 1: 同步 Comet 与 OpenSpec 规划契约 + +**Files:** +- Modify: `.comet.yaml` +- Modify: `openspec/changes/build-turtle-etf-local-research-workflow/proposal.md` +- Modify: `openspec/changes/build-turtle-etf-local-research-workflow/design.md` +- Modify: `openspec/changes/build-turtle-etf-local-research-workflow/tasks.md` +- Modify: `openspec/changes/build-turtle-etf-local-research-workflow/specs/local-quant-research-workflow/spec.md` +- Modify: `openspec/changes/build-turtle-etf-local-research-workflow/specs/standard-strategy-analysis-data/spec.md` +- Modify: `openspec/changes/build-turtle-etf-local-research-workflow/specs/turtle-etf-local-research/spec.md` +- Reference: `docs/superpowers/specs/2026-07-16-turtle-independent-asset-expansion-design.md` +- Reference: `docs/superpowers/plans/2026-07-16-turtle-independent-asset-expansion.md` + +**Interfaces:** +- Consumes: 已确认设计文档中的六只候选、最多 16 次真实场景、精确快照、有效秩和完整验收门槛。 +- Produces: OpenSpec(开放规格)可严格校验的增量需求,以及指向本设计和本计划的 Comet(工作流)运行态。 + +- [ ] **Step 1: 记录更新前缺口** + +Run: + +```powershell +rg -n "159985|expanded-universe|最多 16|冻结订单|有效秩" openspec/changes/build-turtle-etf-local-research-workflow .comet.yaml +``` + +Expected: 当前 OpenSpec 不完整覆盖上述五项,命令不能同时命中全部契约。 + +- [ ] **Step 2: 更新提案、设计、规格与任务** + +在三个 delta spec(增量规格)中分别增加: + +```markdown +#### Scenario: 独立资产扩展使用真实单场景矩阵 +- **WHEN** 主 Agent 已冻结候选前置筛选结果 +- **THEN** 系统生成一个原 11 只基线、一个完整扩展、逐只删除、逐扩展切片删除和五个成本执行压力场景 +- **AND** 每个场景由本地研究 Skill 独立运行一次并生成一个标准结果包 +- **AND** 全部通过候选时场景总数为 16 + +#### Scenario: 不同资产池使用可比较的精确快照 +- **WHEN** 两个场景的证券集合不同 +- **THEN** 每个场景使用与自身证券集合完全匹配的快照 +- **AND** 快照共享相同不可变批次、截止日、字段和价格口径 +- **AND** 重叠证券行情及公司行动摘要必须完全一致 +``` + +在 `tasks.md` 追加本计划 Task 2—12 的逐项检查框;不改写已完成任务的历史证据。 + +- [ ] **Step 3: 更新 Comet 运行态引用** + +将 `.comet.yaml` 中两项改为: + +```yaml +design_doc: docs/superpowers/specs/2026-07-16-turtle-independent-asset-expansion-design.md +plan: docs/superpowers/plans/2026-07-16-turtle-independent-asset-expansion.md +``` + +保留 `phase: build`、`build_mode: executing-plans`、`tdd_mode: tdd`、`review_mode: thorough` 和 `verify_mode: full`。 + +- [ ] **Step 4: 严格验证规划产物** + +Run: + +```powershell +openspec validate build-turtle-etf-local-research-workflow --strict +git diff --check +``` + +Expected: OpenSpec 输出有效且无错误;`git diff --check` 无输出并以 0 退出。 + +- [ ] **Step 5: 授权后提交规划同步** + +```powershell +git add .comet.yaml ` + openspec/changes/build-turtle-etf-local-research-workflow/proposal.md ` + openspec/changes/build-turtle-etf-local-research-workflow/design.md ` + openspec/changes/build-turtle-etf-local-research-workflow/tasks.md ` + openspec/changes/build-turtle-etf-local-research-workflow/specs/local-quant-research-workflow/spec.md ` + openspec/changes/build-turtle-etf-local-research-workflow/specs/standard-strategy-analysis-data/spec.md ` + openspec/changes/build-turtle-etf-local-research-workflow/specs/turtle-etf-local-research/spec.md ` + docs/superpowers/specs/2026-07-16-turtle-independent-asset-expansion-design.md ` + docs/superpowers/plans/2026-07-16-turtle-independent-asset-expansion.md +git commit -m "规划:固化独立资产扩展研究契约" +``` + +Expected: 只有获得用户明确提交授权后执行;否则记录“未授权提交”并继续保留未暂存改动。 + +### Task 2: 提取共享连续经济收益能力 + +**Files:** +- Create: `scripts/research/market_data/economic_returns.py` +- Modify: `joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_inputs.py:125-337,459-573` +- Modify: `joinquant/strategies/strategy-003/research/code-identity.json` +- Create: `tests/local_quant_research/test_market_data_economic_returns.py` +- Modify: `tests/local_quant_research/test_turtle_vectorbt_inputs.py:270-480` + +**Interfaces:** +- Consumes: `SnapshotView`、未复权行情、公司行动清单。 +- Produces: `derive_continuous_prices(...) -> ContinuousPriceResult` 和 `snapshot_return_panel(...) -> pd.DataFrame`;海龟输入和通用分析共用同一实现。 + +- [ ] **Step 1: 写共享收益失败测试** + +```python +def test_shared_continuous_returns_match_turtle_covariance_returns() -> None: + result = derive_continuous_prices( + _corporate_action_frame(), + security="ETF-A", + corporate_actions=[_corporate_action(event_type="cash_dividend")], + ) + expected = result.frame["close"] / result.frame["pre_close"] - 1.0 + assert result.returns.to_numpy() == pytest.approx(expected.to_numpy()) + assert result.applications[0].security == "ETF-A" + + +def test_continuous_returns_reject_unreconciled_corporate_action() -> None: + with pytest.raises(EconomicReturnError, match="cannot be reconciled"): + derive_continuous_prices( + _unexplained_basis_change_frame(), + security="ETF-A", + corporate_actions=[], + ) +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_market_data_economic_returns.py -q +``` + +Expected: FAIL,提示 `scripts.research.market_data.economic_returns` 不存在。 + +- [ ] **Step 3: 实现共享接口并迁移原算法** + +`economic_returns.py` 固定公开接口: + +```python +@dataclass(frozen=True) +class ContinuousPriceResult: + frame: pd.DataFrame + returns: pd.Series + applications: tuple[CorporateActionApplication, ...] + + +def derive_continuous_prices( + frame: pd.DataFrame, + *, + security: str, + corporate_actions: Sequence[Mapping[str, object]], +) -> ContinuousPriceResult: + normalized, applications = _derive_forward_only_continuity( + frame, security=security, corporate_actions=corporate_actions + ) + returns = normalized["close"] / normalized["pre_close"] - 1.0 + return ContinuousPriceResult( + frame=normalized, + returns=returns.astype("float64"), + applications=tuple(applications), + ) + + +def snapshot_return_panel( + snapshot: SnapshotView, + securities: Sequence[str] | None = None, +) -> pd.DataFrame: + selected = tuple(sorted(securities or {str(row["security"]) for row in snapshot.rows})) + columns = {} + for security in selected: + frame = pd.DataFrame(row for row in snapshot.rows if row["security"] == security) + actions = [row for row in snapshot.corporate_actions if row["security"] == security] + result = derive_continuous_prices( + frame, security=security, corporate_actions=actions + ) + columns[security] = pd.Series( + result.returns.to_numpy(), + index=pd.to_datetime(result.frame["date"]).dt.normalize(), + ) + return pd.DataFrame(columns).sort_index() +``` + +把 `CorporateActionApplication` 数据类和私有连续价格算法一并迁入 `economic_returns.py`;`vectorbt_inputs.py` 只导入共享类型和函数,既有结果适配器继续通过同名属性读取。其协方差仍使用共享结果的 `continuous_close / continuous_pre_close - 1`。 + +- [ ] **Step 4: 验证海龟输入行为没有漂移** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_market_data_economic_returns.py tests\local_quant_research\test_turtle_vectorbt_inputs.py -q +``` + +Expected: PASS;拆分、现金分红、停牌延后、未来信息和协方差收益既有测试全部通过。 + +- [ ] **Step 5: 更新代码身份并授权后提交** + +重新计算 `economic_returns.py`、`vectorbt_inputs.py` 及依赖摘要,写入 `code-identity.json` 后运行: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_strategy_identity.py -q +git add scripts/research/market_data/economic_returns.py ` + joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_inputs.py ` + joinquant/strategies/strategy-003/research/code-identity.json ` + tests/local_quant_research/test_market_data_economic_returns.py ` + tests/local_quant_research/test_turtle_vectorbt_inputs.py +git commit -m "实现:共享公司行动连续收益口径" +``` + +Expected: 测试通过;提交仍受用户明确授权门禁控制。 + +### Task 3: 增加候选筛选和跨快照重叠校验 + +**Files:** +- Create: `scripts/research/market_data/candidate_screen.py` +- Modify: `scripts/research/market_data/query.py:19-170` +- Modify: `scripts/research/market_data/joinquant_export.py:18-260` +- Create: `tests/local_quant_research/test_market_data_candidate_screen.py` +- Modify: `tests/local_quant_research/test_market_data_query.py` +- Modify: `tests/local_quant_research/test_joinquant_export.py` + +**Interfaces:** +- Consumes: 候选批次行情、公司行动、聚宽导出证券元数据、共享连续经济收益。 +- Produces: `screen_candidates(...) -> CandidateScreenResult`、`write_candidate_screen(...) -> Path`、`validate_snapshot_overlap(...) -> SnapshotOverlapEvidence`,以及带上市/覆盖证据的 `export_result`。 + +- [ ] **Step 1: 写候选筛选失败测试** + +```python +RULE = CandidateScreenRule( + min_valid_days=750, + money_lookback_days=20, + min_median_money=100_000_000.0, +) + + +def test_screen_counts_only_valid_non_paused_days_and_freezes_all_results() -> None: + result = screen_candidates( + rows=_candidate_rows(valid_days=750, median_money=100_000_000.0), + corporate_actions=(), + official_security_metadata=_official_metadata(), + requested_securities=("159985.XSHE",), + as_of_date="2026-07-13", + rule=RULE, + ) + assert result.results[0].status == "pass" + assert result.passed_securities == ("159985.XSHE",) + + +@pytest.mark.parametrize( + ("rows", "reason"), + [ + (_candidate_rows(valid_days=749), "valid_days_below_750"), + (_candidate_rows(median_money=99_999_999.0), "median_money_below_100000000"), + (_candidate_rows(illegal_ohlc=True), "illegal_ohlc"), + ], +) +def test_screen_rejects_each_fixed_gate(rows, reason: str) -> None: + result = screen_candidates( + rows=rows, + corporate_actions=(), + official_security_metadata=_official_metadata(), + requested_securities=("159985.XSHE",), + as_of_date="2026-07-13", + rule=RULE, + ) + assert result.results[0].reason_codes == (reason,) +``` + +- [ ] **Step 2: 写跨快照摘要失败测试** + +```python +def test_snapshot_overlap_accepts_subset_and_rejects_action_drift(store_root: Path) -> None: + evidence = validate_snapshot_overlap(BASELINE_ID, EXPANDED_ID, root=store_root) + assert evidence.securities == ("510300.XSHG",) + assert evidence.market_digest + assert evidence.corporate_actions_digest + + _mutate_action_in_fixture(store_root, EXPANDED_ID) + with pytest.raises(MarketDataIntegrityError, match="overlap.*corporate"): + validate_snapshot_overlap(BASELINE_ID, EXPANDED_ID, root=store_root) +``` + +- [ ] **Step 3: 运行测试确认失败** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_market_data_candidate_screen.py tests\local_quant_research\test_market_data_query.py -q +``` + +Expected: FAIL,缺少 `candidate_screen` 和 `validate_snapshot_overlap`。 + +- [ ] **Step 4: 实现筛选和重叠证据** + +```python +@dataclass(frozen=True) +class CandidateScreenRule: + min_valid_days: int = 750 + money_lookback_days: int = 20 + min_median_money: float = 100_000_000.0 + + +@dataclass(frozen=True) +class CandidateScreenResult: + screen_id: str + as_of_date: str + requested_securities: tuple[str, ...] + passed_securities: tuple[str, ...] + results: tuple[SecurityScreenResult, ...] + + +def write_candidate_screen(result: CandidateScreenResult, *, root: Path) -> Path: + target = root / "screens" / f"{result.screen_id}.json" + return write_json_exclusive(target, candidate_screen_document(result)) + + +def validate_snapshot_overlap( + left_snapshot_id: str, + right_snapshot_id: str, + *, + root: Path, +) -> SnapshotOverlapEvidence: + left = open_snapshot(left_snapshot_id, root=root) + right = open_snapshot(right_snapshot_id, root=root) + _require_same_snapshot_semantics(left_snapshot_id, right_snapshot_id, root=root) + overlap = tuple(sorted(_securities(left) & _securities(right))) + left_market = normalized_digest(row for row in left.rows if row["security"] in overlap) + right_market = normalized_digest(row for row in right.rows if row["security"] in overlap) + left_actions = corporate_actions_digest(row for row in left.corporate_actions if row["security"] in overlap) + right_actions = corporate_actions_digest(row for row in right.corporate_actions if row["security"] in overlap) + if (left_market, left_actions) != (right_market, right_actions): + raise MarketDataIntegrityError("snapshot overlap evidence differs") + return SnapshotOverlapEvidence(overlap, left_market, left_actions) +``` + +`SecurityScreenResult` 除硬门槛证据外还必须输出 `instrument_risk_notes`:`513030.XSHG`、`513800.XSHG` 固定说明境内外休市错位、汇率和折溢价风险;三个商品期货 ETF 固定说明移仓、期限结构和保证金外资产收益;`511260.XSHG` 固定说明分红现金和不复权跳变。上述字段只作风险披露,不改变通过/失败判定。测试逐类断言披露齐全。 + +扩展 `render_export_program()` 的 `export_result`,逐证券返回 `official_start_date`、`first_market_date`、`last_market_date` 和 `market_rows`;不改变两个 CSV 的字段或新增第三个传输文件。 + +- [ ] **Step 5: 跑行情层回归并授权后提交** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_market_data_candidate_screen.py tests\local_quant_research\test_market_data_query.py tests\local_quant_research\test_joinquant_export.py tests\local_quant_research\test_market_data_storage.py -q +git add scripts/research/market_data/candidate_screen.py ` + scripts/research/market_data/query.py ` + scripts/research/market_data/joinquant_export.py ` + tests/local_quant_research/test_market_data_candidate_screen.py ` + tests/local_quant_research/test_market_data_query.py ` + tests/local_quant_research/test_joinquant_export.py +git commit -m "实现:增加资产候选筛选与快照重叠校验" +``` + +Expected: 全部 PASS;存储格式、不可变批次和 DuckDB 内存查询既有测试无回归;提交受授权门禁控制。 + +### Task 4: 实现冻结原订单的真实延迟执行 + +**Files:** +- Create: `joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_delayed.py` +- Modify: `joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_inputs.py:529-572` +- Modify: `joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_engine.py:22-228` +- Modify: `joinquant/strategies/strategy-003/research/turtle_etf/result_adapter.py:308-590` +- Modify: `joinquant/strategies/strategy-003/research/code-identity.json` +- Create: `tests/local_quant_research/test_turtle_vectorbt_delayed.py` +- Modify: `tests/local_quant_research/test_turtle_vectorbt_inputs.py` +- Modify: `tests/local_quant_research/test_turtle_vectorbt_engine.py` +- Modify: `tests/local_quant_research/test_turtle_result_adapter.py` +- Modify: `tests/local_quant_research/test_turtle_vectorbt_performance.py` + +**Interfaces:** +- Consumes: 即时路径生成的原始有效订单计划和 `execution.additional_delay_days`。 +- Produces: `freeze_order_plan(...) -> FrozenOrderPlan`、`run_delayed_execution(...) -> VectorbtSimulationResult`;`delay=0` 保持即时路径逐事实不变。 + +- [ ] **Step 1: 先锁定 delay=0 现有行为** + +```python +def test_zero_delay_keeps_existing_execution_facts_digest(real_baseline_fixture) -> None: + result = run_vectorbt_simulation( + real_baseline_fixture.inputs, + real_baseline_fixture.config, + ) + facts = to_joinquant_facts(simulation=result, inputs=real_baseline_fixture.inputs, scenario_id="baseline") + assert execution_facts_digest(facts) == ( + "7e77fbdc318569828c90cdec61911c929308ddc7ae256f1ad7ac72420ac4ba1e" + ) +``` + +此测试使用当前零滑点配置先锁定引擎兼容;Task 5 再按已确认契约显式加入 0.05% 基准滑点并产生新研究身份。 + +- [ ] **Step 2: 写延迟语义失败测试** + +```python +def test_delayed_execution_freezes_original_action_target_reason_and_signal_n() -> None: + immediate = _run_fixture(delay_days=0) + delayed = _run_fixture(delay_days=1, mutate_delay_day_signal=True) + assert delayed.action_codes[2, 0] == immediate.action_codes[1, 0] + assert delayed.reason_codes[2, 0] == immediate.reason_codes[1, 0] + assert delayed.planned_quantities[2, 0] == immediate.planned_quantities[1, 0] + assert delayed.planned_row_indices[2, 0] == 1 + + +def test_delayed_buy_only_uses_lot_cash_truncation() -> None: + delayed = _run_fixture(delay_days=1, delayed_open=200.0) + assert delayed.execution_adjustment_codes[2, 0] == ADJUST_CASH_TRUNCATED + assert delayed.filled_quantities[2, 0] % 100 == 0 + assert delayed.filled_quantities[2, 0] < delayed.planned_quantities[2, 0] + + +def test_delayed_queue_executes_before_today_orders_enter_next_day_queue() -> None: + delayed = _run_collision_fixture(delay_days=1) + assert delayed.execution_sequence[2].tolist() == ["queued-from-row-1"] + assert delayed.planned_row_indices[3, 1] == 2 + assert delayed.execution_sequence[3].tolist() == ["queued-from-row-2"] +``` + +- [ ] **Step 3: 运行测试确认失败** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_turtle_vectorbt_delayed.py tests\local_quant_research\test_turtle_vectorbt_inputs.py -q +``` + +Expected: FAIL,延迟仍会移动信号/协方差且缺少冻结执行模块。 + +- [ ] **Step 4: 固定输入只做 T 到 T+1 错位** + +删除 `vectorbt_inputs.py` 中 `additional_delay_days` 对 `signal_*` 和协方差 source row 的影响,固定: + +```python +shift = 1 +for execution_row in range(shift, row_count): + source_row = execution_row - shift + signal_source_index[execution_row] = source_row + signal_close[execution_row] = continuous_close[source_row] +``` + +延迟只在交易执行层处理。 + +- [ ] **Step 5: 实现两段式冻结计划与真实回放** + +```python +@dataclass(frozen=True) +class FrozenOrderPlan: + planned_row_indices: np.ndarray + action_codes: np.ndarray + reason_codes: np.ndarray + requested_quantities: np.ndarray + target_quantities: np.ndarray + signal_n: np.ndarray + + +def freeze_order_plan( + inputs: SimulationInputs, + immediate: VectorbtSimulationResult, +) -> FrozenOrderPlan: + valid = immediate.filled_quantities > 0 + planned_rows = np.where(valid, np.arange(valid.shape[0])[:, None], -1) + return FrozenOrderPlan( + planned_row_indices=planned_rows.astype(np.int64), + action_codes=np.where(valid, immediate.action_codes, ACTION_NONE).astype(np.int16), + reason_codes=np.where(valid, immediate.reason_codes, REASON_NONE).astype(np.int16), + requested_quantities=np.where(valid, immediate.requested_quantities, 0).astype(np.int64), + target_quantities=np.where(valid, immediate.planned_quantities, 0).astype(np.int64), + signal_n=np.where(valid, inputs.signal_n, np.nan).astype(np.float64), + ) +``` + +`run_vectorbt_simulation()` 保持签名不变:`delay_days=0` 调 `_run_immediate()`;`delay_days=1` 先生成即时计划,再由 `vectorbt_delayed.py` 使用下一交易日开盘真实回放。每个执行日先按原策略优先级执行前一日延迟队列,再把当日新生成订单冻结到下一交易日队列;同一队列内按原策略优先级和证券代码稳定排序,当日新订单不得提前成交。不可交易、现金不足、持仓不足和样本末尾分别记录独立 `execution_adjustment_code`,不覆盖冻结原因码,不做二次 A1 分配。 + +- [ ] **Step 6: 保持标准表结构并写入延迟证据** + +`VectorbtSimulationResult` 墯加: + +```python +planned_row_indices: np.ndarray +execution_adjustment_codes: np.ndarray +execution_delay_days: int +``` + +`result_adapter.py` 映射固定为: + +```python +order["entrust_time"] = planned_date + " 09:30:00" +order["match_time"] = execution_date + " 09:30:00" if filled else None +order["finish_time"] = order["match_time"] +order["time"] = execution_date + " 09:30:00" +order["amount"] = frozen_target +order["filled"] = actual_fill +details.update( + planned_date=planned_date, + execution_date=execution_date, + delay_days=execution_delay_days, + frozen_reason=frozen_reason, + frozen_target_amount=frozen_target, + execution_adjustment=adjustment_name, +) +``` + +样本末尾无法执行的订单必须输出 attribution-only(仅归因)`horizon_expired` 证据。 + +- [ ] **Step 7: 跑延迟、适配和性能测试** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_turtle_vectorbt_delayed.py tests\local_quant_research\test_turtle_vectorbt_engine.py tests\local_quant_research\test_turtle_result_adapter.py tests\local_quant_research\test_turtle_vectorbt_performance.py -q +``` + +Expected: PASS;`delay=0` 摘要不变;延迟冷启动和预热均小于等于 180 秒。 + +- [ ] **Step 8: 更新代码身份并授权后提交** + +```powershell +git add joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_delayed.py ` + joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_inputs.py ` + joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_engine.py ` + joinquant/strategies/strategy-003/research/turtle_etf/result_adapter.py ` + joinquant/strategies/strategy-003/research/code-identity.json ` + tests/local_quant_research/test_turtle_vectorbt_delayed.py ` + tests/local_quant_research/test_turtle_vectorbt_inputs.py ` + tests/local_quant_research/test_turtle_vectorbt_engine.py ` + tests/local_quant_research/test_turtle_result_adapter.py ` + tests/local_quant_research/test_turtle_vectorbt_performance.py +git commit -m "实现:增加冻结订单延迟执行压力" +``` + +Expected: 提交受用户明确授权门禁控制。 + +### Task 5: 生成资产扩展真实场景矩阵和精确快照 + +**Files:** +- Create: `joinquant/strategies/strategy-003/research/asset-expansion-plan.json` +- Create: `joinquant/strategies/strategy-003/research/turtle_etf/asset_expansion.py` +- Modify: `joinquant/strategies/strategy-003/research/baseline.json` +- Modify: `scripts/research/quant_analysis/schemas/analysis-plan.schema.json` +- Modify: `scripts/research/quant_analysis/analysis_plan.py:89-150` +- Modify: `scripts/research/quant_analysis/orchestration.py:64-190` +- Create: `tests/local_quant_research/test_turtle_asset_expansion.py` +- Modify: `tests/quant_analysis/test_analysis_plan.py` +- Create: `tests/quant_analysis/test_orchestration.py` + +**Interfaces:** +- Consumes: 冻结候选筛选结果、原基线配置、相同批次集合和快照选择模板。 +- Produces: `build_asset_expansion_scenarios(...)`、`bind_exact_snapshots(...)`、`materialize_asset_expansion_plan(...)` 和 `strategy-analysis-plan/2` 每场景完整身份。 + +- [ ] **Step 1: 写 16 场景失败测试** + +```python +def test_builds_sixteen_scenarios_when_all_candidates_pass() -> None: + scenarios = build_asset_expansion_scenarios( + _baseline_config(), + _all_six_passed_candidates(), + ) + assert len(scenarios) == 16 + assert [item.role for item in scenarios].count("baseline") == 1 + assert [item.role for item in scenarios].count("expanded") == 1 + assert [item.role for item in scenarios].count("security_deletion") == 6 + assert [item.role for item in scenarios].count("slice_deletion") == 3 + assert [item.role for item in scenarios].count("cost_execution") == 5 + + +def test_cost_matrix_matches_five_confirmed_variants() -> None: + costs = _cost_scenarios(build_asset_expansion_scenarios(_baseline_config(), _all_six_passed_candidates())) + assert [(x.commission, x.slippage, x.delay_days) for x in costs] == [ + (2.0, 0.0005, 0), + (1.0, 0.0010, 0), + (2.0, 0.0010, 0), + (1.0, 0.0005, 1), + (2.0, 0.0010, 1), + ] +``` + +- [ ] **Step 2: 写精确快照和不同资产池计划失败测试** + +```python +def test_every_scenario_binds_an_exact_snapshot(tmp_path: Path) -> None: + scenarios = build_asset_expansion_scenarios(_baseline_config(), _all_six_passed_candidates()) + mapping = bind_exact_snapshots( + scenarios, + batch_ids=(OLD_BATCH_ID, NEW_BATCH_ID), + selection_template=_selection(), + market_data_root=tmp_path, + ) + for scenario in scenarios: + view = open_snapshot(mapping[scenario.scenario_id], root=tmp_path) + assert {row["security"] for row in view.rows} == set(scenario.universe) +``` + +- [ ] **Step 3: 运行测试确认失败** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_turtle_asset_expansion.py tests\quant_analysis\test_analysis_plan.py -q +``` + +Expected: FAIL,缺少资产扩展计划器,Schema 仍要求所有场景使用同一基线资产池。 + +- [ ] **Step 4: 显式修正主场景成本口径** + +在 `baseline.json` 增加: + +```json +"costs": { + "commission_multiplier": 1.0, + "one_way_slippage": 0.0005 +} +``` + +添加测试断言主场景必须显式包含两项,禁止依赖默认值。 + +此修改会形成新的主场景研究身份。后续所有验收比较必须使用重新运行的 `baseline-11`;旧报告中的 5.87% 年化收益只保留为问题诊断历史,不得用作扩展方案对照基准。 + +- [ ] **Step 5: 实现场景构建器** + +```python +@dataclass(frozen=True) +class ExpansionScenario: + scenario_id: str + dimension: str + role: str + parent_scenario_id: str | None + target: str | None + universe: Mapping[str, str] + config: Mapping[str, object] + + +def build_asset_expansion_scenarios( + baseline_config: Mapping[str, object], + passed_candidates: Sequence[CandidateSpec], +) -> tuple[ExpansionScenario, ...]: + accepted = tuple(sorted(passed_candidates, key=lambda item: item.security)) + baseline = _scenario("baseline-11", "baseline", baseline_config) + expanded = _expanded_scenario(baseline_config, accepted) + security_deletions = tuple(_delete_security(expanded, item.security) for item in accepted) + slice_deletions = tuple( + _delete_slice(expanded, slice_name) + for slice_name in ("developed_non_us_equity", "commodity_futures", "treasury_duration") + if _slice_has_accepted_candidate(accepted, slice_name) + ) + costs = _five_cost_execution_scenarios(expanded) + return (baseline, expanded, *security_deletions, *slice_deletions, *costs) +``` + +`asset-expansion-plan.json` 固定六只候选、三个扩展切片、筛选阈值、五个成本执行场景和设计文档全部结论门槛;不保存回测结果或通过者。 + +项目定义同时固定压力映射,不允许未知资产组默认为零:`developed_non_us_equity` 在每个既有冲击中复制 `cross_border_tech_equity` 的冲击值,`commodity_futures` 复制 `gold` 的冲击值,`511260.XSHG` 继续属于已有 `treasury_bond` 并使用其冲击值。若来源键或新资产组键缺失,计划校验失败。新增测试逐个压力场景断言所有实际持仓组都有显式键。 + +- [ ] **Step 6: 扩展分析计划为每场景完整身份** + +项目专属构建器先在 `.local/asset-expansion-plans/{matrix_id}/` 写入场景配置和带真实快照的具体计划。具体 `strategy-analysis-plan/2` 场景项固定由以下代码生成: + +```python +{ + "scenario_id": "expanded-universe", + "dimension": "primary_comparison", + "role": "expanded", + "parent_scenario_id": "baseline-11", + "target": None, + "project_config": ( + f".local/asset-expansion-plans/{matrix_id}/" + "scenario-configs/expanded-universe/params.json" + ), + "snapshot_id": expanded_snapshot_id, +} +``` + +`matrix_id`、配置摘要和快照身份都由实际内容生成,不接受调用者自定义随机名称。`strategy-analysis-plan/1` 保持既有参数挑战行为。 + +对场景配置做规范化深比较:仅允许 `scenario_id`、`universe`、新增固定资产组、对应 `snapshot_id` 以及五个压力场景声明的成本/延迟字段不同;初始资金、55/20 日信号、0.5N 加仓、2N 止损、A1、单标的/资产组/组合上限、10% 目标波动率、无杠杆和执行时点必须逐字段相等。测试分别篡改每类不变量并断言计划拒绝。 + +- [ ] **Step 7: 内容寻址创建并绑定精确快照** + +```python +def bind_exact_snapshots( + scenarios: Sequence[ExpansionScenario], + *, + batch_ids: Sequence[str], + selection_template: SnapshotSelection, + market_data_root: Path, +) -> Mapping[str, str]: + by_universe: dict[tuple[str, ...], str] = {} + result: dict[str, str] = {} + for scenario in scenarios: + securities = tuple(sorted(scenario.universe)) + if securities not in by_universe: + snapshot = create_snapshot( + batch_ids=batch_ids, + selection=replace(selection_template, securities=securities), + root=market_data_root, + ) + by_universe[securities] = snapshot.snapshot_id + result[scenario.scenario_id] = by_universe[securities] + return result +``` + +五个成本执行场景复用扩展资产池的同一个快照 ID;不人为制造重复快照。 + +再实现: + +```python +def materialize_asset_expansion_plan( + *, + baseline_config_path: Path, + definition_path: Path, + screen_report_path: Path, + batch_ids: Sequence[str], + selection_template: SnapshotSelection, + market_data_root: Path, + output_root: Path, +) -> Path: + scenarios = build_asset_expansion_scenarios( + _load_baseline(baseline_config_path), + _load_passed_candidates(screen_report_path, definition_path), + ) + snapshot_ids = bind_exact_snapshots( + scenarios, + batch_ids=batch_ids, + selection_template=selection_template, + market_data_root=market_data_root, + ) + return _write_content_addressed_v2_plan( + scenarios, snapshot_ids=snapshot_ids, output_root=output_root + ) +``` + +`asset_expansion.py` 同时提供 `materialize` 命令行入口,参数固定为 `--repo-root`、`--definition`、`--baseline`、`--screen-report`、可重复的 `--batch-id`、`--start-date`、`--end-date`、`--market-data-root` 和 `--output-root`;标准输出只返回包含 `matrix_id`、`plan_path`、`scenario_count` 和 `snapshot_ids` 的 JSON。测试必须从该公开入口真实生成一个最小计划,不能只调用内部函数。 + +- [ ] **Step 8: 验证矩阵和授权后提交** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_turtle_asset_expansion.py tests\quant_analysis\test_analysis_plan.py tests\quant_analysis\test_orchestration.py -q +git add joinquant/strategies/strategy-003/research/asset-expansion-plan.json ` + joinquant/strategies/strategy-003/research/baseline.json ` + joinquant/strategies/strategy-003/research/turtle_etf/asset_expansion.py ` + scripts/research/quant_analysis/schemas/analysis-plan.schema.json ` + scripts/research/quant_analysis/analysis_plan.py ` + scripts/research/quant_analysis/orchestration.py ` + tests/local_quant_research/test_turtle_asset_expansion.py ` + tests/quant_analysis/test_analysis_plan.py ` + tests/quant_analysis/test_orchestration.py +git commit -m "实现:生成独立资产扩展真实场景矩阵" +``` + +Expected: 全部 PASS;提交受用户明确授权门禁控制。 + +### Task 6: 让统一分析按每场景资产池消费真实结果 + +**Files:** +- Create: `scripts/research/quant_analysis/scenario_matrix.py` +- Modify: `scripts/research/quant_analysis/unified_analysis.py:70-82,307-530,598-680,914-1050,1235-1415` +- Create: `tests/quant_analysis/test_asset_expansion_analysis.py` +- Modify: `tests/quant_analysis/test_unified_analysis.py` + +**Interfaces:** +- Consumes: `strategy-analysis-plan/2`、显式来源登记 JSON 中的全部 `scenario_id -> run_id` 和逐场景精确快照证据。 +- Produces: `evaluate_real_scenario_matrix(...)`、每场景绩效/归因、真实删除层和真实成本层门槛结果。 + +- [ ] **Step 1: 写不同资产池与真实矩阵失败测试** + +```python +def test_analysis_accepts_distinct_universes_with_compatible_snapshots(tmp_path: Path) -> None: + result = run_deterministic_analysis( + tmp_path, + _v2_preparation(tmp_path), + _all_real_source_runs(), + ) + assert result["primary_comparison"]["baseline_scenario_id"] == "baseline-11" + assert result["primary_comparison"]["expanded_scenario_id"] == "expanded-universe" + + +def test_v2_uses_real_deletion_and_cost_runs_not_approximations(monkeypatch) -> None: + monkeypatch.setattr(unified_analysis, "_deletion_sensitivity", _raise_if_called) + monkeypatch.setattr(unified_analysis, "_cost_sensitivity", _raise_if_called) + result = run_deterministic_analysis(ROOT, PREPARATION, SOURCES) + assert result["robustness"]["asset_deletions"][0]["method"] == "real_backtest" + assert result["robustness"]["cost_execution"][0]["method"] == "real_backtest" + + +def test_cli_requires_registry_to_cover_every_planned_scenario(tmp_path: Path) -> None: + registry = _write_source_registry(tmp_path, omit="cost-delay-double") + with pytest.raises(AnalysisInputError, match="missing scenario source"): + load_source_registry(registry, preparation=_v2_preparation(tmp_path)) +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\quant_analysis\test_asset_expansion_analysis.py tests\quant_analysis\test_unified_analysis.py -q +``` + +Expected: FAIL;当前分析强制共享快照和全局资产池,并调用近似删除/成本方法。 + +- [ ] **Step 3: 给场景输入绑定自己的身份** + +```python +@dataclass(frozen=True) +class ScenarioInput: + scenario_id: str + role: str + parent_scenario_id: str | None + target: str | None + universe: Mapping[str, str] + snapshot_id: str + run_id: str + result_dir: Path + manifest: Mapping[str, object] + performance: Mapping[str, object] + returns: pd.Series + balances: pd.DataFrame + positions: pd.DataFrame + orders: pd.DataFrame + attribution: pd.DataFrame +``` + +`_position_facts()` 和 `_security_pnl_facts()` 改为只用 `scenario.universe`;未知证券继续硬失败。`_register_source_results()` 要求代码摘要、执行后端、批次集合、截止日、字段和价格口径相同,但允许 `snapshot_id` 不同,并调用 `validate_snapshot_overlap()` 校验重叠证券。 + +`unified_analysis` 命令行增加 `--source-registry PATH`;文件固定包含 `preparation_id`、`plan_digest` 和 `sources` 映射。v2 禁止与零散 `--source` 混用,缺少、多出或重复任一计划场景都硬失败;v1 的既有 `--source` 行为不变。 + +- [ ] **Step 4: 用真实场景评估层级门槛** + +```python +def evaluate_real_scenario_matrix( + metrics_by_scenario: Mapping[str, Mapping[str, object]], + scenario_specs: Sequence[Mapping[str, object]], + thresholds: Mapping[str, object], +) -> dict[str, object]: + deletion_security = _layer("security_deletion", metrics_by_scenario, scenario_specs) + deletion_slice = _layer("slice_deletion", metrics_by_scenario, scenario_specs) + cost_execution = _layer("cost_execution", metrics_by_scenario, scenario_specs) + return { + "security_deletion": _evaluate_layer( + deletion_security, cagr_min=0.0, drawdown_max=0.20, + worst_calmar_ratio=0.50, average_calmar_ratio=0.75, + ), + "slice_deletion": _evaluate_layer( + deletion_slice, cagr_min=0.0, drawdown_max=0.20, + worst_calmar_ratio=0.50, average_calmar_ratio=0.75, + ), + "cost_execution": _evaluate_layer( + cost_execution, cagr_min=0.0, drawdown_max=0.20, + worst_calmar_ratio=0.50, average_calmar_ratio=0.75, + ), + } +``` + +三层 Calmar 比例都相对 `expanded-universe`。三个切片删除变体中至少两个还必须同时满足“CAGR 高于 `baseline-11`、Calmar 不低于 `baseline-11`”。 + +- [ ] **Step 5: 保留 v1,v2 禁止近似验收** + +`strategy-analysis-plan/1` 保持现有参数研究报告行为;`strategy-analysis-plan/2` 只从真实场景构建 `asset_deletions` 和 `cost_execution`,近似 `_deletion_sensitivity()`、`_cost_sensitivity()` 不进入 v2 证据矩阵或推荐。 + +- [ ] **Step 6: 跑统一分析回归并授权后提交** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\quant_analysis\test_asset_expansion_analysis.py tests\quant_analysis\test_unified_analysis.py tests\quant_analysis\test_statistics.py -q +git add scripts/research/quant_analysis/scenario_matrix.py ` + scripts/research/quant_analysis/unified_analysis.py ` + tests/quant_analysis/test_asset_expansion_analysis.py ` + tests/quant_analysis/test_unified_analysis.py +git commit -m "实现:统一分析消费真实跨资产池场景" +``` + +Expected: v1、v2 测试全部 PASS;提交受授权门禁控制。 + +### Task 7: 增加有效秩和趋势利用分析 + +**Files:** +- Create: `scripts/research/quant_analysis/independence.py` +- Create: `scripts/research/quant_analysis/trend_utilization.py` +- Modify: `scripts/research/quant_analysis/unified_analysis.py` +- Create: `tests/quant_analysis/test_independence.py` +- Create: `tests/quant_analysis/test_trend_utilization.py` + +**Interfaces:** +- Consumes: 共享 `snapshot_return_panel()`、基线/扩展标准结果包、计划定义的原因码集合。 +- Produces: `rolling_effective_rank(...)` 和 `analyze_trend_utilization(...)`,不导入海龟回调模块。 + +- [ ] **Step 1: 写有效秩失败测试** + +```python +def test_effective_rank_uses_last_sixty_common_observations() -> None: + returns = _return_panel(rows=61, include_missing_first_row=True) + result = rolling_effective_rank(returns, window_days=60, variance_epsilon=1e-12) + assert len(result) == 1 + assert result.iloc[0]["security_count"] == 3 + + +def test_effective_rank_excludes_zero_variance_and_clips_eigenvalues(monkeypatch) -> None: + # 夹具含四只证券,其中一只为常数列;剔除后相关矩阵恰有三个特征值。 + returns = _panel_with_constant_security() + monkeypatch.setattr(np.linalg, "eigvalsh", lambda _: np.array([-1e-15, 0.5, 1.5])) + result = rolling_effective_rank(returns) + assert result.iloc[-1]["excluded_zero_variance"] == 1 + assert result.iloc[-1]["effective_rank"] == pytest.approx(1.6) + + +@pytest.mark.parametrize( + "fixture_name", + ("paused_day", "missing_listing_row", "fewer_than_60_common", "duplicate_action", "unreconciled_action"), +) +def test_effective_rank_obeys_shared_economic_return_evidence(fixture_name: str) -> None: + _assert_expected_effective_rank_behavior(fixture_name) +``` + +- [ ] **Step 2: 写趋势利用失败测试** + +```python +def test_low_exposure_opportunities_count_signal_fill_and_budget_block() -> None: + result = analyze_trend_utilization( + baseline=_baseline_scenario(invested_ratio=0.40), + expanded=_expanded_scenario_with_new_events(), + new_securities=("159985.XSHE",), + entry_reason_codes=("entry_breakout",), + budget_block_reason_codes=("allocation_constraint",), + ) + assert result.low_exposure_days == 1 + assert result.new_breakout_events == 2 + assert result.new_filled_entries == 1 + assert result.new_budget_blocks == 1 +``` + +- [ ] **Step 3: 运行测试确认失败** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\quant_analysis\test_independence.py tests\quant_analysis\test_trend_utilization.py -q +``` + +Expected: FAIL,两个通用分析模块不存在。 + +- [ ] **Step 4: 实现固定有效秩算法** + +```python +def rolling_effective_rank( + returns: pd.DataFrame, + *, + window_days: int = 60, + variance_epsilon: float = 1e-12, +) -> pd.DataFrame: + rows: list[dict[str, object]] = [] + for date in returns.index: + common = returns.loc[:date].dropna(how="any").tail(window_days) + if len(common) < window_days: + continue + std = common.std(ddof=1) + active = std > variance_epsilon + selected = common.loc[:, active] + if selected.shape[1] < 2: + continue + eigenvalues = np.clip(np.linalg.eigvalsh(selected.corr().to_numpy()), 0.0, None) + effective = float(eigenvalues.sum() ** 2 / np.square(eigenvalues).sum()) + rows.append({ + "date": pd.Timestamp(date).normalize(), + "effective_rank": effective, + "normalized_effective_rank": effective / selected.shape[1], + "security_count": int(selected.shape[1]), + "excluded_zero_variance": int((~active).sum()), + }) + return pd.DataFrame.from_records(rows) +``` + +- [ ] **Step 5: 实现趋势供给、成交和贡献分析** + +分析器通过计划传入原因码,不硬编码海龟常量;固定输出:低于半仓日期、新资产突破/建仓/风险阻止次数、信号成交率、平均持仓、平均风险预算、盈利趋势占比、证券/切片/时期贡献,以及至少两个切片跨至少两个固定时期出现真实成交趋势的布尔证据。 + +“可执行突破”只统计结果包中 `entry_breakout` 决策已通过非价格门禁且下一执行日可交易的事件;“真实成交趋势”必须存在已成交建仓订单,并在随后至少一个交易日持仓大于零。比较有效秩时先从扩展资产池找到“最后一只通过候选累计满 60 个有效交易日”的首日,再把基线和扩展结果严格对齐到从该日到 `2026-07-13` 的同一日期集合;任何一侧缺日都失败,不做填充。 + +- [ ] **Step 6: 接入统一分析并验证门槛** + +在 `deterministic-analysis.json` 增加: + +```json +"independence": { + "common_window": {"start": "YYYY-MM-DD", "end": "2026-07-13"}, + "baseline": {}, + "expanded": {}, + "delta": {}, + "gate": {"status": "pass|fail", "reasons": []} +}, +"trend_utilization": { + "low_exposure": {}, + "new_assets": [], + "new_slices": [], + "gate": {"status": "pass|fail", "reasons": []} +} +``` + +门槛严格采用设计文档:原始有效秩中位数增量至少 0.5、归一化中位数不下降、原始有效秩提高日占比超过 60%。 + +趋势利用门槛也必须逐项输出:基线低于半仓日存在新增资产可执行突破并获得仓位;平均投入仓位或风险预算利用率至少一项严格提高;所有既有硬上限违规次数为零;新增资产合计净贡献为正;至少两个新增资产组净贡献为正;至少两个不同扩展切片各自在至少两个固定时期出现真实成交趋势。任何一项缺证据都不能默认为通过。 + +- [ ] **Step 7: 跑分析测试并授权后提交** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\quant_analysis\test_independence.py tests\quant_analysis\test_trend_utilization.py tests\quant_analysis\test_asset_expansion_analysis.py -q +git add scripts/research/quant_analysis/independence.py ` + scripts/research/quant_analysis/trend_utilization.py ` + scripts/research/quant_analysis/unified_analysis.py ` + tests/quant_analysis/test_independence.py ` + tests/quant_analysis/test_trend_utilization.py +git commit -m "实现:增加独立风险来源与趋势利用分析" +``` + +Expected: 全部 PASS;提交受授权门禁控制。 + +### Task 8: 扩展完整报告和推荐结论 + +**Files:** +- Modify: `scripts/research/quant_analysis/reporting.py:135-760` +- Modify: `tests/quant_analysis/test_reporting.py` + +**Interfaces:** +- Consumes: v2 确定性分析中的主场景对照、独立性、趋势利用、真实删除/成本、双基准、归因和压力证据。 +- Produces: 完整 `local-strategy-analysis-report.md` 和只等待人工确认的 `recommendation.json`。 + +- [ ] **Step 1: 写报告失败测试** + +```python +def test_asset_expansion_report_contains_every_required_conclusion() -> None: + report = render_analysis_report(_asset_expansion_analysis(), _recommendation(), _vibe()) + for heading in ( + "原 11 只与扩展资产池对照", + "独立风险来源与有效秩", + "趋势供给与利用", + "逐只与逐切片真实删除", + "五个真实成本与执行压力", + "双基准与 Alpha/Beta", + "反对证据与不确定性", + ): + assert heading in report + assert "JoinQuant(聚宽)正式回测" in report + assert "人工确认" in report + + +def test_single_asset_or_period_concentration_fails_recommendation() -> None: + recommendation = build_recommendation(_analysis(single_asset_share=0.71)) + assert recommendation["decision"] == "do_not_expand" + assert "single_asset_concentration" in recommendation["reasons"] +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\quant_analysis\test_reporting.py -q +``` + +Expected: FAIL,报告缺少资产扩展专用章节和结论门槛。 + +- [ ] **Step 3: 实现明确的三类结论** + +```python +def _asset_expansion_decision(analysis: Mapping[str, object]) -> tuple[str, list[str]]: + failed = _failed_required_gates(analysis) + if failed: + return "do_not_expand", failed + if _independence_passed(analysis) and not _return_risk_passed(analysis): + return "independence_improved_allocation_insufficient", [] + return "recommend_asset_expansion_for_human_confirmation", [] +``` + +`_failed_required_gates()` 必须覆盖独立性、趋势利用、主场景 CAGR/回撤/Calmar、五个历史窗口、全部持仓冲击、五个真实成本执行场景、逐只删除、逐切片删除、单资产 70% 和单时期 70% 门槛。报告完整展示所有失败门槛,不允许用高收益覆盖回撤、稳健性、独立性或单点依赖失败。Vibe 单体只放在审计章节,不改变上述函数结果。 + +分析状态只允许 `complete`、`evidence_insufficient`、`failed`。只有数据、全部实际场景、守恒校验和报告均完整时才为 `complete`;验收门槛不通过仍是 `complete` 并给出明确不推荐结论。证据缺失与运行失败分别按设计文档停止,不用猜测补齐。 + +- [ ] **Step 4: 跑报告回归并授权后提交** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\quant_analysis\test_reporting.py tests\quant_analysis\test_unified_analysis.py -q +git add scripts/research/quant_analysis/reporting.py tests/quant_analysis/test_reporting.py +git commit -m "实现:输出资产扩展完整研究报告" +``` + +Expected: PASS;提交受授权门禁控制。 + +### Task 9: 从聚宽真实导出六只候选并完成前置筛选 + +**Files:** +- Runtime only: `.local/market-data/transfers/` +- Runtime only: `.local/market-data/batches/$NewBatchId/` +- Runtime only: `.local/market-data/snapshots/$CandidateScreenSnapshotId.json` +- Runtime only: `.local/market-data/screens/$ScreenId.json` +- No repository source edit. + +**Interfaces:** +- Consumes: `ExportRequest`、已登录聚宽研究环境、现有 `import_verified_transfer()`。 +- Produces: 一个只含六只候选的新不可变批次和冻结筛选报告;不复制原 11 只批次。 + +- [ ] **Step 1: 审计现有行情中心** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m scripts.research.market_data.cli audit --root .local\market-data +``` + +Expected: `status=complete`;当前原批次仍为 `1923c902f5692d35bd84e2745620a06cb6c18666c4a4add724ce80d261d5f4e1`,传输目录为空。若身份已变化,记录实际审计结果并停止使用旧常量。 + +- [ ] **Step 2: 生成固定聚宽研究程序** + +使用 `.\.venv\Scripts\python.exe` 调用: + +```python +from scripts.research.market_data.contracts import MARKET_DATA_FIELDS +from scripts.research.market_data.joinquant_export import ExportRequest, render_export_program + +request = ExportRequest( + securities=( + "159980.XSHE", "159981.XSHE", "159985.XSHE", + "511260.XSHG", "513030.XSHG", "513800.XSHG", + ), + fields=MARKET_DATA_FIELDS, + snapshot_end_date="2026-07-13", +) +print(render_export_program(request)) +``` + +保存实际程序字节 SHA256;输出程序必须声明 `fq=None`、`skip_paused=False`,并生成两个 CSV 和逐证券覆盖元数据。 + +- [ ] **Step 3: 使用已登录浏览器真实执行和下载** + +执行时先读取并使用 `chrome:control-chrome` Skill(浏览器控制技能): + +1. 打开聚宽研究页并确认登录身份。 +2. 粘贴完全相同的生成程序并运行。 +3. 保存 `export_result` JSON,核对两个远端文件路径、字节数、行数、SHA256 和六只证券覆盖。 +4. 下载 `market-data.csv`、`corporate-actions.csv` 到 `.local/market-data/transfers/$TransferId/`。 +5. 不在此步修改任何正式策略或启动正式回测。 + +Expected: 六只证券都在导出结果中;文件摘要与远端回读一致。 + +- [ ] **Step 4: 使用现有受验证导入器发布候选批次** + +通过 `apply_patch` 在 `.local/market-data/transfers/$TransferId/` 临时创建 `import_candidate_batch.py`。脚本调用 `import_verified_transfer()`;其 `cleanup_remote` 回调先原子写入 `awaiting-remote-cleanup.json`,然后最多等待 120 秒,只有读到包含两个远端路径及浏览器核验时间的 `remote-cleanup-confirmed.json` 才返回 `True`。manifest 固定为 JoinQuant 研究环境、ETF、日线、完整字段、不复权和截止日。 + +Run: + +```powershell +$ImportProcess = Start-Process -FilePath ".\.venv\Scripts\python.exe" ` + -ArgumentList @(".local\market-data\transfers\$TransferId\import_candidate_batch.py") ` + -PassThru -WindowStyle Hidden +``` + +等待 `awaiting-remote-cleanup.json` 出现后,由同一个已登录浏览器删除两个远端文件并重新列目录确认均不存在;再用 `apply_patch` 写入 `remote-cleanup-confirmed.json`。随后等待 `$ImportProcess` 正常退出并读取其结果 JSON。成功后删除临时脚本和两个握手文件;若超时、浏览器未确认或子进程非零退出,保持失败并保留 CSV 供诊断,不得绕过回调直接调用 `import_batch()`。 + +Expected: 新 `batch_id` 为 64 位小写十六进制;Parquet 和 DuckDB 回读一致;远端删除确认后本地两个 CSV 自动删除。 + +- [ ] **Step 5: 建候选筛选快照并冻结结果** + +候选筛选快照只引用新候选批次并包含六只代码;逐只运行 `screen_candidates()`,再用 `write_candidate_screen()` 输出 `.local/market-data/screens/$ScreenId.json`。 + +Expected: + +```text +requested_securities = 固定六只 +passed_securities = 只由前置证据决定 +每只均有 valid_days、20日成交额中位数、OHLC、覆盖和公司行动结论 +没有收益、回撤、Calmar 或回测字段 +``` + +- [ ] **Step 6: 复核清理和审计** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m scripts.research.market_data.cli audit --root .local\market-data +Get-ChildItem .local\market-data\transfers -File -Recurse +Get-ChildItem .local -Filter *.csv -File -Recurse +``` + +Expected: 审计 complete;两次文件扫描均无输出。若清理失败,停止状态为 `failed` 并报告准确残留路径。 + +### Task 10: 准备并逐次运行全部真实场景 + +**Files:** +- Runtime only: `.local/asset-expansion-plans/$MatrixId/` +- Runtime only: `.local/strategy-analysis-preparations/$PreparationId/` +- Runtime only: `.local/quant-research/strategy-003/$RunId/` +- Runtime only: `.local/strategy-analysis-preparations/$PreparationId/source-registry.json` +- No repository source edit. + +**Interfaces:** +- Consumes: 冻结筛选报告、原/新批次、双基准集、场景构建器和项目运行模板。 +- Produces: 最多 16 个独立标准结果包和完整、内容可核验的 `scenario_id -> run_id` 来源登记。 + +- [ ] **Step 1: 冻结场景和精确快照** + +先用项目专属命令将已冻结筛选结果物化为具体 v2 计划: + +```powershell +$Matrix = & .\.venv\Scripts\python.exe ` + joinquant\strategies\strategy-003\research\turtle_etf\asset_expansion.py materialize ` + --repo-root . ` + --definition joinquant\strategies\strategy-003\research\asset-expansion-plan.json ` + --baseline joinquant\strategies\strategy-003\research\baseline.json ` + --screen-report $env:SCREEN_REPORT ` + --batch-id $env:BASELINE_BATCH_ID ` + --batch-id $env:CANDIDATE_BATCH_ID ` + --start-date 2012-05-28 ` + --end-date 2026-07-13 ` + --market-data-root .local\market-data ` + --output-root .local\asset-expansion-plans | ConvertFrom-Json +$env:CONCRETE_PLAN = $Matrix.plan_path +``` + +该入口内部调用 `build_asset_expansion_scenarios()` 和 `bind_exact_snapshots()`。所有快照固定: + +```text +batch_ids = [原11只批次, 新候选批次] +start_date = 2012-05-28 +end_date = 2026-07-13 +fields = MARKET_DATA_FIELDS +price_semantics = {fq: null, skip_paused: false} +``` + +对所有不同证券集合调用 `validate_snapshot_overlap()`;五个成本执行场景复用扩展快照。 + +Expected: `$Matrix.scenario_count` 等于实际矩阵数量,全部通过时为 16;`$env:CONCRETE_PLAN` 指向内容寻址的 `strategy-analysis-plan/2`,而不是仓库中的定义模板。 + +- [ ] **Step 2: 生成不可变 preparation** + +Run: + +```powershell +$Preparation = & .\.venv\Scripts\python.exe -m scripts.research.quant_analysis.orchestration ` + --repo-root . ` + --plan $env:CONCRETE_PLAN ` + --run-template joinquant\strategies\strategy-003\research\project-run.json ` + --benchmark-set-id $env:BENCHMARK_SET_ID | ConvertFrom-Json +$env:PREPARATION_WORKSPACE = $Preparation.workspace +``` + +Expected: `expected_scenario_runs` 等于实际矩阵数量,全部通过候选时为 16;每个 run.json 只有一个场景、一个精确 snapshot_id 和一个 required output。 + +- [ ] **Step 3: 主 Agent 逐次调用单场景 Skill** + +对 preparation 的 `scenario_run_configs` 按列表顺序执行: + +1. 主 Agent 每次只把一份 `run.json` 交给 `run-local-quant-research` Skill。 +2. 等待该次返回 `complete` 和 `next_action=return_to_caller` 后再进入下一份。 +3. 记录 `scenario_id -> run_id`,不扫描历史目录猜测来源。 +4. 每次核对 `cold_seconds <= 180`、`warm_seconds <= 180`、规范化结果摘要一致和标准结果包门禁通过。 +5. 每次成功后原子更新 `$env:PREPARATION_WORKSPACE\source-registry.json`,固定写入 `schema_version`、`preparation_id`、`plan_digest` 和完整 `sources` 映射;只有来源数与计划场景数完全一致时才封存。 + +Expected: 每次 Skill 只生成一份本地回测;任何一次失败都停止矩阵,不跳过、不用旧结果代替。 + +- [ ] **Step 4: 验证最多 16 个来源身份** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_turtle_single_scenario.py tests\local_quant_research\test_turtle_vectorbt_performance.py -q +``` + +Expected: PASS;来源登记数量与 preparation 完全一致;场景间共享代码身份和执行后端,精确快照重叠证据一致。 + +### Task 11: 生成确定性分析、Vibe 单体复核和完整报告 + +**Files:** +- Runtime only: `.local/strategy-analysis/$AnalysisId/deterministic-analysis.json` +- Runtime only: `.local/strategy-analysis/$AnalysisId/evidence-matrix.parquet` +- Runtime only: `.local/strategy-analysis/$AnalysisId/vibe-evidence.json` +- Runtime only: `.local/strategy-analysis/$AnalysisId/local-strategy-analysis-report.md` +- Runtime only: `.local/strategy-analysis/$AnalysisId/recommendation.json` + +**Interfaces:** +- Consumes: 全部显式真实结果、双基准和共享行情快照。 +- Produces: 完整本地研究报告、反对证据和人工可确认推荐;Vibe 只作定性审计。 + +- [ ] **Step 1: 显式登记全部来源并运行确定性分析** + +Run: + +```powershell +$env:SOURCE_REGISTRY = Join-Path $env:PREPARATION_WORKSPACE "source-registry.json" +$Analysis = & .\.venv\Scripts\python.exe -m scripts.research.quant_analysis.unified_analysis ` + --repo-root . ` + --preparation-workspace $env:PREPARATION_WORKSPACE ` + --source-registry $env:SOURCE_REGISTRY | ConvertFrom-Json +$env:ANALYSIS_WORKSPACE = $Analysis.workspace +``` + +分析入口必须校验登记文件恰好覆盖 preparation 的所有场景;不得手工省略删除/成本来源,也不得扫描历史目录补齐。 + +Expected: 输出 `analysis_id`、`status=complete`;证据矩阵包含全部真实场景、时期、滚动、区块抽样、压力、冲击、CVaR(条件风险价值)、有效秩和趋势利用结果。 + +- [ ] **Step 2: 真实运行 Vibe 单体只读复核** + +通过 `apply_patch` 在分析目录建立临时提示文件,内容固定要求: + +```text +只读复核指定 deterministic-analysis.json。 +只使用 performance-attribution、risk-analysis、report-generate 三项单体方法。 +确定性文件的数值、状态、原因和门槛是唯一裁判。 +禁止回测、获取新行情、优化、群体分析和修改文件。 +返回 status、capabilities_loaded、key_findings、objections、limitations、recommendation_alignment、constraints。 +``` + +Run: + +```powershell +.\.venv\Scripts\vibe-trading.exe run --json --no-rich --max-iter 12 --prompt-file $env:VIBE_PROMPT_FILE +``` + +Expected: 公共单体运行完成,`constraints` 明确 `read_only=true`、`backtest_called=false`、`new_market_data_called=false`、`optimizer_called=false`、`swarm_called=false`、`deterministic_metrics_are_authoritative=true`。将运行身份和 trace 摘要写入 `vibe-evidence.json`;删除临时提示文件。禁止调用任何 swarm 命令。 + +- [ ] **Step 3: 生成报告和推荐** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m scripts.research.quant_analysis.reporting --workspace $env:ANALYSIS_WORKSPACE +``` + +Expected: 报告完整给出收益、最大回撤、Calmar、Sharpe、仓位、风险预算、有效秩、趋势供给/利用、Alpha/Beta、归因、全部真实稳健性、压力、反对证据、限制和推荐;`next_action=human_confirmation_required`。 + +- [ ] **Step 4: 核对推荐门槛** + +只允许三个结论: + +```text +recommend_asset_expansion_for_human_confirmation +independence_improved_allocation_insufficient +do_not_expand +``` + +无论结论为何,都必须完整报告失败证据;不得自动修改正式资产池、进入聚宽正式复核或启动新的资金分配优化。 + +### Task 12: 完成端到端回归、全量验证和临时产物清理 + +**Files:** +- Modify only when affected mapping requires: `.build-and-verify/config.json` +- Modify: `openspec/changes/build-turtle-etf-local-research-workflow/tasks.md` +- Runtime evidence: `docs/superpowers/reports/2026-07-16-turtle-independent-asset-expansion-verify.md` + +**Interfaces:** +- Consumes: Task 1—11 的实现、真实行情、最多 16 个结果包和完整分析交付。 +- Produces: 可复核的全量验证报告;停止在人工确认,不提交或归档除非用户另行授权。 + +- [ ] **Step 1: 运行定向回归** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research\test_market_data_economic_returns.py tests\local_quant_research\test_market_data_candidate_screen.py tests\local_quant_research\test_turtle_vectorbt_delayed.py tests\local_quant_research\test_turtle_asset_expansion.py tests\quant_analysis\test_asset_expansion_analysis.py tests\quant_analysis\test_independence.py tests\quant_analysis\test_trend_utilization.py tests\quant_analysis\test_reporting.py -q +``` + +Expected: 全部 PASS。 + +- [ ] **Step 2: 运行本地研究和分析完整回归** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest tests\local_quant_research tests\quant_analysis -q +``` + +Expected: 全部 PASS;无跳过关键端到端测试。 + +- [ ] **Step 3: 运行公开入口端到端** + +端到端必须从 `run-local-quant-research` Skill 用户入口贯通一个普通场景和一个真实延迟场景,再由主 Agent 使用冻结 preparation 复数调用全部实际场景,最后贯通统一分析、Vibe 单体和报告。 + +Expected: + +```text +每次 Skill 一场景 +每次 <= 180 秒 +标准结果包完整 +显式来源数量等于计划数量 +完整分析和报告生成 +next_action = human_confirmation_required +``` + +- [ ] **Step 4: 运行全仓与 Build and Verify 完整门禁** + +Run: + +```powershell +.\.venv\Scripts\python.exe -m pytest -q +openspec validate build-turtle-etf-local-research-workflow --strict +.\.venv\Scripts\python.exe .build-and-verify\runtime\build_and_verify.py verify --project . --full +git diff --check +``` + +Expected: 全仓 PASS;OpenSpec 严格通过;Build and Verify 输出 `status=passed` 且 `full-not-run=false`;`git diff --check` 无输出。 + +- [ ] **Step 5: 扫描禁止能力和临时产物** + +Run: + +```powershell +rg -n "minimum_turnover|participation_rate|order.*1%|run_swarm|swarm-run" joinquant/strategies/strategy-003/research scripts/research +Get-ChildItem .local\market-data\transfers -File -Recurse +Get-ChildItem .local -Filter *.csv -File -Recurse +Get-ChildItem .local -Directory -Recurse | Where-Object { $_.Name -in @('.attempts', '.benchmark-work') } +``` + +Expected: 生产代码中没有海龟流动性规则或群体分析调用;三个临时产物扫描无输出。测试夹具或文档中的禁止词必须逐项解释,不能误报为生产行为。 + +- [ ] **Step 6: 写验证报告并更新 OpenSpec 任务证据** + +验证报告必须列出:真实候选筛选、批次/快照身份、实际场景数、逐场景耗时、全部测试命令与计数、分析 ID、Vibe 单体运行 ID、报告路径、清理证明、已验证与无法验证项,以及最终推荐结论。不得把本地结论写成正式回测结论。 + +- [ ] **Step 7: 授权后提交最终实现** + +```powershell +$Allowed = @( + ".comet.yaml", + "openspec/changes/build-turtle-etf-local-research-workflow/proposal.md", + "openspec/changes/build-turtle-etf-local-research-workflow/design.md", + "openspec/changes/build-turtle-etf-local-research-workflow/tasks.md", + "openspec/changes/build-turtle-etf-local-research-workflow/specs/local-quant-research-workflow/spec.md", + "openspec/changes/build-turtle-etf-local-research-workflow/specs/standard-strategy-analysis-data/spec.md", + "openspec/changes/build-turtle-etf-local-research-workflow/specs/turtle-etf-local-research/spec.md", + "docs/superpowers/specs/2026-07-16-turtle-independent-asset-expansion-design.md", + "docs/superpowers/plans/2026-07-16-turtle-independent-asset-expansion.md", + "docs/superpowers/reports/2026-07-16-turtle-independent-asset-expansion-verify.md", + "scripts/research/market_data/economic_returns.py", + "scripts/research/market_data/candidate_screen.py", + "scripts/research/market_data/query.py", + "scripts/research/market_data/joinquant_export.py", + "joinquant/strategies/strategy-003/research/asset-expansion-plan.json", + "joinquant/strategies/strategy-003/research/baseline.json", + "joinquant/strategies/strategy-003/research/code-identity.json", + "joinquant/strategies/strategy-003/research/turtle_etf/asset_expansion.py", + "joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_delayed.py", + "joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_inputs.py", + "joinquant/strategies/strategy-003/research/turtle_etf/vectorbt_engine.py", + "joinquant/strategies/strategy-003/research/turtle_etf/result_adapter.py", + "scripts/research/quant_analysis/schemas/analysis-plan.schema.json", + "scripts/research/quant_analysis/analysis_plan.py", + "scripts/research/quant_analysis/orchestration.py", + "scripts/research/quant_analysis/scenario_matrix.py", + "scripts/research/quant_analysis/unified_analysis.py", + "scripts/research/quant_analysis/independence.py", + "scripts/research/quant_analysis/trend_utilization.py", + "scripts/research/quant_analysis/reporting.py", + "tests/local_quant_research/test_market_data_economic_returns.py", + "tests/local_quant_research/test_market_data_candidate_screen.py", + "tests/local_quant_research/test_market_data_query.py", + "tests/local_quant_research/test_joinquant_export.py", + "tests/local_quant_research/test_turtle_asset_expansion.py", + "tests/local_quant_research/test_turtle_vectorbt_delayed.py", + "tests/local_quant_research/test_turtle_vectorbt_inputs.py", + "tests/local_quant_research/test_turtle_vectorbt_engine.py", + "tests/local_quant_research/test_turtle_result_adapter.py", + "tests/local_quant_research/test_turtle_vectorbt_performance.py", + "tests/quant_analysis/test_analysis_plan.py", + "tests/quant_analysis/test_orchestration.py", + "tests/quant_analysis/test_asset_expansion_analysis.py", + "tests/quant_analysis/test_unified_analysis.py", + "tests/quant_analysis/test_independence.py", + "tests/quant_analysis/test_trend_utilization.py", + "tests/quant_analysis/test_reporting.py" +) +$Allowed | ForEach-Object { git diff -- $_ } +git add -- $Allowed +# 仅当 Step 4 证明 affected mapping 确实需要且已逐行复核时,再单独执行: +# git add -- .build-and-verify/config.json +git diff --cached --check +git diff --cached --name-only +git status --short +git commit -m "实现:完成独立资产扩展本地研究" +``` + +Expected: 提交前逐文件检查 diff(差异)和 staged(暂存)清单;若任一文件含无关用户改动,先按 hunk(差异块)拆分且不得覆盖。清单不得包含 `.local`、密钥、Cookie(浏览器凭证)或清单外路径;只有用户明确授权后提交。若前面已按任务提交,Git(版本管理)会自动忽略未变化文件。 + +--- + +## 实施顺序与审查门禁 + +1. Task 1 锁定文档契约。 +2. Task 2—3 完成共享行情能力。 +3. Task 4 完成真实延迟执行并验证 `delay=0` 不漂移。 +4. Task 5 生成场景矩阵和精确快照。 +5. Task 6—8 完成通用分析与报告。 +6. Task 9 获取真实聚宽候选数据并冻结筛选。 +7. Task 10 逐次运行全部实际场景。 +8. Task 11 生成确定性分析、Vibe 单体审计和报告。 +9. Task 12 完成全量验证、清理和人工确认前停止。 + +每个 Task 完成后进行一次规格一致性审查和一次代码质量审查;有阻断项时先修复并复审,不带阻断进入下一 Task。 diff --git a/docs/superpowers/specs/2026-07-16-turtle-independent-asset-expansion-design.md b/docs/superpowers/specs/2026-07-16-turtle-independent-asset-expansion-design.md new file mode 100644 index 0000000..a031883 --- /dev/null +++ b/docs/superpowers/specs/2026-07-16-turtle-independent-asset-expansion-design.md @@ -0,0 +1,265 @@ +# 海龟 ETF 独立资产扩展挑战设计 + +**状态:** 待用户书面复核 +**范围:** 本地探索性研究,不修改正式聚宽策略,不改变现有海龟入场、加仓、退出、止损、A1(统一完成比例)分配或风险控制规则 + +## 1. 背景与研究问题 + +现有 11 只 ETF(交易型开放式指数基金)基线能够发现并进入大量趋势,但组合长期平均投入仓位约为 44.07%,净年化收益约为 5.87%。已有挑战表明,简单放宽持仓上限、改协方差窗口、改目标波动率估计、恢复经典四单位或使用短中期动量排序,都不能稳定提高收益风险比。 + +当前资产池的主要问题不是缺少入场信号,而是很多信号来自高度相关的权益资产。最近 500 个共同交易日内,11 只 ETF 的有效秩约为 3.48,其中 9 只权益类 ETF 的有效秩约为 2.59、平均两两相关系数约为 0.515。大量同时出现的突破并不等于大量独立趋势,风险预算因此被重复的权益风险来源占用。 + +本挑战验证一个更上游的问题:在完全不改变海龟交易和风险规则的前提下,增加真正不同的地区、商品和债券风险来源,能否提高有效趋势供给、趋势利用率和最终收益风险比。 + +本挑战不验证价格重新武装、止损调整、动量加权或新的组合优化器。只有资产扩展仍不能解决问题时,才另行设计边际分散贡献分配挑战。 + +## 2. 设计决策 + +### 2.1 固定候选集合 + +候选集合在读取回测收益前一次性锁定: + +| 代码 | 角色 | 扩展资产组 | 主要验证价值 | +|---|---|---|---| +| `513030.XSHG` | 德国 DAX ETF | `developed_non_us_equity` | 增加欧洲宽基权益暴露 | +| `513800.XSHG` | 日本 TOPIX ETF | `developed_non_us_equity` | 增加日本宽基权益暴露 | +| `511260.XSHG` | 10 年国债 ETF | `treasury_bond` | 在现有 5 年国债之外增加久期差异 | +| `159980.XSHE` | 有色期货 ETF | `commodity_futures` | 增加工业金属期货风险来源 | +| `159981.XSHE` | 能源化工期货 ETF | `commodity_futures` | 增加能源化工期货风险来源 | +| `159985.XSHE` | 豆粕期货 ETF | `commodity_futures` | 增加农产品期货风险来源 | + +用户已明确撤销原方案中“不加入 `159985.XSHE`”的限制。本次只重新开放该候选,不代表它已经通过前置筛选或成为正式资产池成员。 + +`513030.XSHG` 与 `513800.XSHG` 共用非美国发达市场权益组;三个商品 ETF 共用商品期货组;`511260.XSHG` 加入现有国债组。资产组仍是固定配置,不根据回测后相关性动态拆分或合并。 + +本次不是对中国内地全部 ETF 做穷举选优,而是验证三个预先提出、当前资产池明显缺少或覆盖不足的方向:非美国发达市场权益、非黄金商品期货和国债久期。六只候选来自这三个方向的固定研究清单,清单冻结于 `2026-07-16`,在读取候选回测结果后不得新增、替换或删除。若将来要做全市场候选发现,必须另开研究变更并在读取收益前冻结发现规则。 + +### 2.2 不变边界 + +- 原 11 只 ETF 基线保持不变,作为对照组。 +- 信号仍为 55 日突破入场、上涨 0.5N 加仓、2N 保护性止损和 20 日反向突破退出。 +- A1 资金分配、单标的上限、资产组上限、组合风险上限和 10% 目标波动率保持不变。 +- 两个主场景都显式使用实际佣金和单边 0.05% 滑点;配置不得依赖引擎默认值。五个成本执行场景只在此基准上覆盖已确认的成本或延迟字段。 +- 只做多、日线收盘确认、下一交易日开盘执行、不使用杠杆。 +- ETF 池流动性只在回测前做一次候选筛选;策略运行时不读取成交额,不增加任何流动性入场、加仓、退出或减仓规则。 +- 完全删除“单笔订单不超过成交额 1%”规则,不在配置、代码、报告或候选验收中保留。 +- 本地结果只提供探索性证据;是否改变正式策略资产池由人工确认,正式裁决仍在 JoinQuant(聚宽)完成。 + +## 3. 行情数据与候选前置筛选 + +### 3.1 数据来源和口径 + +六只候选必须从 JoinQuant(聚宽)研究环境真实导出,不能使用 AkShare(金融数据聚合)、网络下载文件或人工拼接数据替代。导出区间为各 ETF 首个完整交易日至 `2026-07-13`,固定: + +- 日线; +- `fq=None`,即不复权; +- `skip_paused=False`,保留停牌日; +- 行情字段为 `date`、`security`、`open`、`high`、`low`、`close`、`pre_close`、`volume`、`money`、`factor`、`paused`、`high_limit`、`low_limit`; +- 同步导出基金分红等公司行动证据。 + +公司行动只用于正确核算现金权益和解释不复权价格跳变,不用未来复权因子改写历史信号。 + +### 3.2 不可变存储与清理 + +沿用仓库行情数据中心: + +1. 聚宽导出 `market-data.csv` 和 `corporate-actions.csv`,远端回读并计算 SHA256(摘要校验值)。 +2. 本地校验文件摘要和行数。 +3. 转换为新的不可变 Parquet(列式存储)批次,并用 DuckDB(内存分析数据库)回读验证。 +4. 所有研究快照引用相同的原 11 只批次和新增候选批次,使用相同截止日和价格口径;每个回测场景建立一份与自身证券集合完全一致的不可变快照,不修改旧批次、旧快照。 +5. 远端清理确认成功后删除本地传输 CSV(逗号分隔文件);最终只保留 Parquet、清单、摘要和快照,不保留临时文件。 + +导入、远端清理或本地清理任一步失败,均报告明确停止状态,不创建研究快照。失败时可暂时保留本地传输 CSV 供校验和重试;只有远端清理确认成功后才能删除本地 CSV,并且在最终交付前必须确认所有传输文件均已删除。 + +### 3.3 前置筛选 + +筛选必须在读取任何候选回测收益前完成。每只候选独立检查: + +- 至少 750 个非停牌且开高低收合法的有效交易日; +- 截止 `2026-07-13` 的最近 20 个交易日成交额中位数不少于 1 亿元; +- 上市日期、首个完整交易日和聚宽证券代码一致; +- 非停牌交易日的开高低收、前收盘和成交量字段完整且价格关系合法; +- 停牌、涨跌停、复权因子变化和公司行动可解释,不存在无法核算的数据断层; +- QDII(合格境内机构投资者)ETF 额外报告境内外休市错位、汇率和折溢价风险; +- 商品期货 ETF 额外报告移仓、期限结构和保证金外资产收益影响,明确其收益不等于现货涨跌; +- 债券 ETF 额外核对分红现金和不复权价格跳变。 + +通过者全部进入扩展资产池;未通过者按前置证据排除并记录原因。禁止根据后续收益、回撤或归因结果选择性保留候选,也禁止事后放宽筛选门槛。 + +## 4. 对照运行设计 + +### 4.1 两个固定场景 + +使用相同不可变行情批次、同一个代码身份、同一个截止日和同一价格口径运行: + +1. `baseline-11`:只使用原 11 只 ETF。 +2. `expanded-universe`:原 11 只加全部通过前置筛选的候选。 + +两个场景分别使用与自身证券集合完全一致的快照,避免放宽现有“快照证券必须与场景资产池完全一致”的入口校验;各快照引用相同批次集合。除 `universe`、对应快照身份和新增固定资产组外,所有参数必须完全一致。新 ETF 在真实上市前不可交易;对照期不裁剪到最晚上市日,避免丢失原基线早期历史。报告同时给出全区间和固定分段结果。 + +每个场景通过本地研究 Skill(技能)各运行一次。Skill 只负责单次流程编排,不把场景数量或本次六只候选写死在通用能力中。 + +### 4.2 跨资产池分析兼容 + +现有分析计划要求所有场景与一个基线资产池完全相同,现有统一分析也使用一份全局证券到资产组映射校验持仓和估值;该约束不适用于本挑战,不能通过关闭校验或把两个资产池粗暴合并来绕过。 + +本挑战需要增加通用的“每场景配置”能力: + +- `baseline-11` 和 `expanded-universe` 分别引用自己的场景配置文件; +- 每个场景使用证券集合完全匹配的快照,快照只筛选自身证券及其公司行动;快照内出现缺失证券或额外证券都必须拒绝; +- 分析计划逐场景读取并校验该配置声明的证券和资产组; +- 结果包中的持仓、订单和估值只能出现所属场景配置内的证券; +- 两个场景仍须共享相同代码身份、行情批次集合、截止日、价格口径、初始资金、信号、执行和风险参数;只有资产池和与其精确匹配的快照身份可以不同,分析器显式逐字段验证这些不变量; +- 不同场景快照中的重叠证券必须具有完全一致的行情行摘要和公司行动摘要;不一致时停止比较; +- 标准分析数据包结构保持不变,不要求现有聚宽结果增加表或改目录;行情快照作为策略分析的独立声明输入,由编排层传入; +- 有效秩和趋势供给属于通用策略分析的派生结果,不写入海龟内核,也不耦合到本地研究 Skill。 + +若场景配置与结果包证券不一致,分析必须失败,不能把未知证券归入临时或默认资产组。 + +### 4.3 趋势供给与利用分析 + +除收益指标外,必须回答: + +1. 新资产是否增加了独立风险来源? +2. 低仓位阶段是否出现了可交易的新趋势? +3. 新趋势是否真正获得资金并形成正贡献? +4. 改善是否来自多个资产和多个时期,而不是一个偶然赢家? + +固定分析指标: + +- 60 日滚动收益相关矩阵的有效秩,定义为 `(特征值之和²) / 特征值平方和`; +- 归一化有效秩,定义为 `有效秩 / 当日参与计算的证券数量`; +- 原始和归一化有效秩高于基线的交易日占比; +- 基线仓位低于 50% 时,新资产有效突破、实际建仓、被风险预算阻止的次数和覆盖天数; +- 新资产信号到成交的转化率、平均持仓、平均风险预算、盈利趋势占比和收益贡献; +- 总投入仓位、风险预算利用率和目标波动率约束触发频率; +- 按 ETF、资产组和固定时期拆分的收益、回撤与贡献。 + +有效秩的输入口径必须与组合协方差使用的经济收益一致,并沉淀为共享行情分析能力,不能复制一份海龟专用算法: + +- 从不复权 `close`、`pre_close` 和公司行动证据生成只向未来生效的连续价格; +- 单日收益固定为 `continuous_close / continuous_pre_close - 1`; +- 已上市证券的停牌日如价格字段合法,保留该日收益(通常为零);上市前、缺失行或无法核算的公司行动记为缺失,不填零; +- 每个滚动点只使用截至当日已经可见的数据,取最近 60 个所有参与证券均非缺失的共同观察;不足 60 个共同观察不输出指标; +- 60 日内收益标准差不大于 `1e-12` 的证券从当日矩阵和当日证券数量中同时剔除并记录;少于两只证券时不输出指标; +- 相关矩阵使用样本相关系数,特征值因浮点误差小于零时截为零,再计算原始和归一化有效秩; +- 公司行动不完整、同一事件重复或无法与 `pre_close` 对账时停止分析,不用复权行情或未来信息替代。 + +## 5. 收益、归因和稳健性 + +两个场景都输出标准分析数据包,并由现有统一策略分析能力生成完整报告。至少包括: + +- 净年化收益、累计收益、最大回撤、Calmar(卡玛比率)、Sharpe(夏普比率)、波动率和平均投入仓位; +- 相对沪深 300 人民币总回报与纳斯达克 100 人民币总回报的 Beta(市场暴露)、Alpha(超额收益)、主动收益和相关性; +- 收益按标的、资产组、交易原因和时间区间的归因; +- 逐只删除新增 ETF、逐扩展切片删除新增资产;每个删除变体都必须使用相同批次集合、截止日、价格口径和交易参数,并以精确匹配自身资产池的快照真实重新回测,生成独立标准结果包,不能用现有“扣除历史收益贡献且不重新分配资金”的近似结果替代; +- 固定分析时期 `2015-01-01—2018-12-31`、`2019-01-01—2022-12-31`、`2023-01-01—2026-07-13`,以及现有固定历史窗口、滚动窗口、成本扰动和压力测试; +- 数据完整性、公司行动核算、订单守恒、现金守恒和结果包结构校验。 + +逐只、逐组删除仅用于判断依赖性,不能据此反向挑选资产池。现有贡献扣除法可以保留为诊断附表,但不得用于本挑战的验收结论。 + +### 5.1 完整运行矩阵 + +本地研究 Skill 每次仍只运行一个场景,由主 Agent(主代理)逐次调用;分析器不得在内部隐式执行交易。运行矩阵在前置筛选结束后一次性生成并冻结: + +| 类型 | 场景数 | 运行方式 | +|---|---:|---| +| 原 11 只基线 | 1 | 独立配置、精确资产池快照、一次 Skill 调用、一个标准结果包 | +| 全部通过候选的扩展场景 | 1 | 独立配置、精确资产池快照、一次 Skill 调用、一个标准结果包 | +| 逐只删除新增 ETF | `N` | `N` 为通过前置筛选的候选数;每只各真实重跑一次 | +| 逐扩展切片删除 | 最多 3 | 只为至少有一只通过候选的切片各真实重跑一次 | +| 扩展场景成本与执行扰动 | 5 | 完整五个已确认压力场景各真实重跑一次 | + +因此最多运行 `2 + 6 + 3 + 5 = 16` 次。每个场景都有独立的 `scenario_id`、配置摘要、精确资产池快照身份、运行身份和标准结果包。固定时期、滚动窗口和持仓压力测试由策略分析读取结果包后确定性计算,不额外调用交易引擎;报告必须明确这些属于分段统计或假设分析。成本和执行扰动必须进入交易引擎真实重跑,不能用原订单的一阶损益调整替代。 + +五个成本与执行压力场景固定为: + +1. 双倍佣金、单边 0.05% 滑点、原定次日开盘执行; +2. 实际佣金、单边 0.10% 滑点、原定次日开盘执行; +3. 双倍佣金、单边 0.10% 滑点、原定次日开盘执行; +4. 实际佣金、单边 0.05% 滑点、全部原订单再延迟一个交易日执行; +5. 双倍佣金、单边 0.10% 滑点、全部原订单再延迟一个交易日执行。 + +延迟场景的语义不是把信号窗口整体后移,也不是在延迟日重新运行策略判断: + +- 在原定执行日按原信号和当时可见信息完成 A1 分配及全部风险门禁,冻结订单方向、目标数量和原因码,然后进入延迟队列; +- 建仓、加仓、保护性止损、20 日退出和强制风险减仓全部使用同一延迟; +- 延迟执行日只使用当日开盘价和成交成本执行已冻结订单,不得使用延迟日的新突破、新 N 值、新协方差或新目标波动率重新决定是否交易; +- 若延迟日实际现金或持仓不足,只能做机械性的可买数量下取整或可卖持仓截断,并记录部分成交或拒绝原因,不能反向改写原目标订单; +- 延迟队列和当日新生成订单必须按原有执行优先级确定性处理,并在标准结果包中分别保留原计划日期、实际成交日期和延迟天数。 + +新增资产组必须进入全部适用压力测试:非美国发达市场权益按跨境权益冲击,商品期货按商品风险冲击,`511260.XSHG` 按国债冲击;任何新增组不得因为旧压力配置没有对应键而被默认为零冲击。 + +## 6. 结论门槛 + +只有同时满足以下条件,报告才可推荐继续推进资产池扩展: + +### 6.1 独立性成立 + +- 共同可比较窗口从“最后一只通过前置筛选的 ETF 累积满 60 个有效交易日”开始,截止 `2026-07-13`;基线和扩展场景在同一日期集合上计算; +- 扩展场景的 60 日滚动原始有效秩中位数至少比基线高 `0.5`; +- 扩展场景的归一化有效秩中位数不低于基线; +- 在共同窗口中,扩展场景原始有效秩高于基线的天数占比超过 60%; +- “真实成交趋势”固定定义为存在已成交的 `entry_breakout` 建仓订单且随后至少一个交易日持仓大于零;至少两个不同扩展切片各自在三个固定分析时期中的至少两个时期出现真实成交趋势,而不是只有同一切片在单一时期重复突破。 + +### 6.2 趋势利用改善 + +- 基线投入仓位低于 50% 的日期中,新增资产产生可执行突破并实际获得仓位; +- 新增资产提高平均投入仓位或风险预算利用率,但不能靠突破既有硬上限实现; +- 新增资产整体净收益贡献为正,且至少两个新增资产组的净贡献为正。 + +### 6.3 收益风险改善 + +- 扩展场景净年化收益严格高于 `baseline-11`; +- 最大回撤不超过 15%,且相对基线恶化不超过 3 个百分点; +- Calmar(卡玛比率)不低于基线; +- 五个成本与执行压力场景的净年化收益均大于零、最大回撤均不超过 20%;最差场景 Calmar 不低于扩展主场景的 50%,五个场景平均 Calmar 不低于扩展主场景的 75%; +- 五个固定历史压力窗口的最大回撤分别不超过 15%,全部持仓冲击的单日假设损失不超过 15%。 + +### 6.4 不依赖单点 + +- 固定三个扩展切片:非美国发达市场权益切片删除 `513030.XSHG`、`513800.XSHG`;商品期货切片删除 `159980.XSHE`、`159981.XSHE`、`159985.XSHE`;国债久期切片只删除新增的 `511260.XSHG`、保留原 `511010.XSHG`。三个删除变体中,至少两个仍须同时满足“净年化收益高于基线、Calmar 不低于基线”; +- 所有逐只删除和逐切片删除变体的净年化收益必须大于零、最大回撤不超过 20%;逐只删除层和逐切片删除层分别要求最差 Calmar 不低于扩展主场景的 50%、该层平均 Calmar 不低于扩展主场景的 75%; +- 单一新增 ETF 占全部新增 ETF 正贡献之和的比例不得超过 70%; +- 单一固定分析时期占全部正增量收益之和的比例不得超过 70%; +- 若改善只来自一个短样本商品周期、一个 QDII 溢价阶段或一次债券行情,研究仍以 `complete` 完成,但验收结论为“不通过,不推荐升级资产池”。 + +如果独立性提高但收益风险未改善,结论为“资产扩展方向有效,但现有固定 A1 分配未能利用新增机会”,下一步才进入边际分散贡献分配挑战。如果独立性本身没有提高,则停止资金分配优化,重新审视候选资产类别。 + +## 7. 测试与停止状态 + +### 7.1 实施测试 + +- 为六只候选、三个资产组和证券代码排序先写失败测试; +- 验证原 11 只基线配置和行为不变; +- 验证所有前置筛选通过者整包进入、失败者只因前置证据退出; +- 验证新旧场景除资产池外参数完全一致; +- 验证每个场景快照与自身资产池完全一致,并拒绝快照中的缺失或额外证券及公司行动; +- 验证不同场景快照的重叠证券行情摘要和公司行动摘要完全一致; +- 验证运行矩阵中每个主场景、逐只删除场景、逐切片删除场景和成本执行场景都有独立 Skill 调用、配置摘要、快照身份和标准结果包; +- 验证五个成本执行场景完整,且延迟场景冻结原订单方向、目标数量和原因码;延迟日新信号与新风险估计不改变冻结订单,现金或持仓不足只产生有证据的机械截断; +- 验证上市前无价格、无信号、无持仓; +- 验证公司行动现金核算和不复权信号口径; +- 验证有效秩使用连续收盘/连续前收盘收益,覆盖停牌日、缺失行、少于 60 个共同观察、零方差证券、负数特征值截断和公司行动失败; +- 验证 Parquet 批次、DuckDB 回读、新快照和传输文件清理; +- 运行本地研究入口到标准结果包、统一分析和完整报告的端到端回归; +- 单次本地回测必须在 3 分钟内完成。 + +### 7.2 停止状态 + +- `complete`:数据、所需场景、分析和报告均完整;无论验收通过或不通过,都给出明确推荐或不推荐结论。 +- `evidence_insufficient`:缺少代码身份、行情快照、研究范围、必要输入,或候选数据与公司行动证据不足以可靠核算,因而无法完成分析。 +- `failed`:数据摘要、不可变存储、守恒校验、结果包或端到端回归失败。 + +任何停止状态都必须保留可复核证据,不得用猜测补齐缺失数据。只有 `complete` 才能声称临时产物已经清理完毕;`evidence_insufficient` 或 `failed` 如因传输清理失败而保留 CSV,必须列明准确路径和恢复动作,恢复后再删除。 + +## 8. 交付物 + +- 候选前置筛选报告; +- 新的不可变行情批次,以及逐场景与资产池精确匹配的不可变快照; +- `baseline-11`、`expanded-universe` 及全部真实删除变体的独立标准分析数据包; +- 完整策略分析报告,覆盖收益、回撤、趋势供给、趋势利用、Beta、Alpha、归因和稳健性; +- 人工可直接确认的推荐结论; +- 全部实现测试和端到端回归证据。 + +本设计不修改正式聚宽回测流程,不执行正式聚宽回测,也不改变正式策略资产池。 diff --git a/joinquant/strategies/strategy-001/manifest.json b/joinquant/strategies/strategy-001/manifest.json index be4a782..51d705f 100644 --- a/joinquant/strategies/strategy-001/manifest.json +++ b/joinquant/strategies/strategy-001/manifest.json @@ -7,8 +7,11 @@ "name": "etf_factor_rotation" }, "source": { - "url": "https://www.joinquant.com/algorithm/index/edit?algorithmId=2632adf656bc9797f82e2748c2ce928e&backtest=115", + "url": "https://www.joinquant.com/algorithm/index/edit?algorithmId=9569e5f3a071602af031c8cff2ed9c6e&backtest=115", "aliases": [ + { + "url": "https://www.joinquant.com/algorithm/index/edit?algorithmId=9569e5f3a071602af031c8cff2ed9c6e&backtest=115" + }, { "url": "https://www.joinquant.com/algorithm/index/edit?algorithmId=2632adf656bc9797f82e2748c2ce928e&backtest=115" }, @@ -568,7 +571,7 @@ "url": "https://www.joinquant.com/algorithm/index/edit?algorithmId=05d8070463bd0c212db54eed317288a1&backtest=115" } ], - "observed_at": "2026-07-15T04:00:06+08:00" + "observed_at": "2026-07-16T04:00:09+08:00" }, "fence": { "before_sha256": "79b2bdb2c82de35a722741b34292df7ed012f8348a19f1b0acfb209020322857", diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/raw/code-history-93a631dd3a95632d3b3a502f.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/raw/code-history-93a631dd3a95632d3b3a502f.json.gz new file mode 100644 index 0000000..1e180a7 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/raw/code-history-93a631dd3a95632d3b3a502f.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:780277182f32b80c32b1cf7fea1a6fe199f7e47fabee90e3e8131252cda1d887 +size 829 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/attribution_log-0dcd6b38e3665bab5046f63f.parquet b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/attribution_log-0dcd6b38e3665bab5046f63f.parquet new file mode 100644 index 0000000..a5af3dc --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/attribution_log-0dcd6b38e3665bab5046f63f.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43c3663350069f1d4f859d4e792dc77cb0a1d38b5c9f36f8a86be38caccbb788 +size 29118 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/balances.parquet b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/balances.parquet new file mode 100644 index 0000000..bf45869 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/balances.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da9222def1c2e56bb57c502cf80e86081f5989e0553a63d3d408693733154e1a +size 2615 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/orders.parquet b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/orders.parquet new file mode 100644 index 0000000..104de73 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/orders.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d34dbe36c990af3f9f7300077bea1c5f8cbe54741dec148558306c74990ae39f +size 6339 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/positions.parquet b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/positions.parquet new file mode 100644 index 0000000..4aa7c09 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/positions.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:660e9cfc154f3461b039be7b39bc740984b5a84cf28c6ba9cace4d1166eee71c +size 6992 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/results.parquet b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/results.parquet new file mode 100644 index 0000000..851159d --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/results.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ecbb53c9e1fd9f0c7a3d48642cce97cb0aae931ed9d0f6acc71f66c9ffd295c +size 2024 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/risk.parquet b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/risk.parquet new file mode 100644 index 0000000..3b18931 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/data/risk.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:195920c6fb543dfe2c2557178715d5b5f5c4641acd603b27fc50c5b513f508be +size 11160 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/attribution-log-0dcd6b38e3665bab5046f63f.jsonl.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/attribution-log-0dcd6b38e3665bab5046f63f.jsonl.gz new file mode 100644 index 0000000..6db08ac --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/attribution-log-0dcd6b38e3665bab5046f63f.jsonl.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1db439cfdd969da596b71692647ef60647a2767e7a0a4d1a39c7f9b8c5fae016 +size 3855 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/balances.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/balances.json.gz new file mode 100644 index 0000000..eb237d7 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/balances.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a7ab90a226326c038c59cbb0343d37ef90bf80ef67e585fb36d3f2f33f56b3a +size 682 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/normal-log-pages.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/normal-log-pages.json.gz new file mode 100644 index 0000000..c05a677 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/normal-log-pages.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b37b73f0be0b950d50b6a028e468bb2732f03868ebe0cdb2a7f9d5f1f52f108d +size 7305 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/normal-log.jsonl.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/normal-log.jsonl.gz new file mode 100644 index 0000000..7ee0626 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/normal-log.jsonl.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fb747631855f163d851802f8ac71b1362fd26816ed87b0cb0ba82f21c762aac +size 9346 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/orders.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/orders.json.gz new file mode 100644 index 0000000..12e65d6 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/orders.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5789390bee80dd5ca2d0466fe0f09444257a914d863bb79febda78edfd309f6c +size 678 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/period_risks.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/period_risks.json.gz new file mode 100644 index 0000000..f851046 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/period_risks.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c14bfcf1832a180081ce0abd2061eeb1dc780074f0c2cdfd923be805f2407368 +size 22 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/positions.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/positions.json.gz new file mode 100644 index 0000000..be3f9ce --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/positions.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e6f7eb9de29ca9cb65870dd7a12f40c048c9a9bdb12be27aa8b439c0c1ccc3e +size 1970 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/records.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/records.json.gz new file mode 100644 index 0000000..c7f68c1 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/records.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64406e753d82e11c6bd63d1d04bb6aa083eefa0a46949bbfce994d556b989f0f +size 22 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/research-response-724bc05499f352dc6bdfdf78.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/research-response-724bc05499f352dc6bdfdf78.json.gz new file mode 100644 index 0000000..885af20 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/research-response-724bc05499f352dc6bdfdf78.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ace0e516bddd780db6d9cf7f54ad1978dd23ff5001d63e5b99b8ce40c7ca85a7 +size 1761 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/results.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/results.json.gz new file mode 100644 index 0000000..76c47cc --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/results.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:688adc505b97fa2b9bcd98e8259d03b27fb1d20e60383e089ba949597a5454e6 +size 1040 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/risk.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/risk.json.gz new file mode 100644 index 0000000..9b72653 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/raw/risk.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f3e697dae8e3ad92fcffccac276f9e4590a0957b1aa576e3ba88e4c48a932fa +size 571 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/reports/live-summary.json.gz b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/reports/live-summary.json.gz new file mode 100644 index 0000000..f2702e2 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/snapshots/e85f5fe70206488491dd5068/reports/live-summary.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64e25737ebf91d348a42715957ddf83e26ad7f4790927f29bc60f96d9fa5d3a5 +size 669 diff --git a/joinquant/strategies/strategy-001/simulations/simulation-001/source_versions/348e35c4d11426ab63444d1e504aaac9a9f92dfd5552f1a12488029d805a8323.json b/joinquant/strategies/strategy-001/simulations/simulation-001/source_versions/348e35c4d11426ab63444d1e504aaac9a9f92dfd5552f1a12488029d805a8323.json new file mode 100644 index 0000000..4dfecb7 --- /dev/null +++ b/joinquant/strategies/strategy-001/simulations/simulation-001/source_versions/348e35c4d11426ab63444d1e504aaac9a9f92dfd5552f1a12488029d805a8323.json @@ -0,0 +1,3 @@ +{ + "backtest_id": "d6507bc7886cdd32aad6c1c173af57fb" +} diff --git a/joinquant/strategies/strategy-002/manifest.json b/joinquant/strategies/strategy-002/manifest.json index 9bba774..fe47a69 100644 --- a/joinquant/strategies/strategy-002/manifest.json +++ b/joinquant/strategies/strategy-002/manifest.json @@ -7,8 +7,11 @@ "name": "ETF动态调仓" }, "source": { - "url": "https://www.joinquant.com/algorithm/index/edit?algorithmId=89bd88cca11d4ed59fd77462ee6addcb&backtest=12", + "url": "https://www.joinquant.com/algorithm/index/edit?algorithmId=9b71913a43c1ec34c56bbe905b22b6fb&backtest=12", "aliases": [ + { + "url": "https://www.joinquant.com/algorithm/index/edit?algorithmId=9b71913a43c1ec34c56bbe905b22b6fb&backtest=12" + }, { "url": "https://www.joinquant.com/algorithm/index/edit?algorithmId=89bd88cca11d4ed59fd77462ee6addcb&backtest=12" }, @@ -136,7 +139,7 @@ "url": "https://www.joinquant.com/algorithm/index/edit?algorithmId=c57fcc9eeb34b39428c46f5c87bfe77f&backtest=12" } ], - "observed_at": "2026-07-15T04:00:33+08:00" + "observed_at": "2026-07-16T04:00:35+08:00" }, "fence": { "before_sha256": "9f25381bdf78a8b0a42a71d09fa8f359bfa1e53855c829526607f6e4b254b731", diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/raw/code-history-220beb0c54bf08fe184a740a.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/raw/code-history-220beb0c54bf08fe184a740a.json.gz new file mode 100644 index 0000000..8f45e86 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/raw/code-history-220beb0c54bf08fe184a740a.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6e6b1f914fcaa3632d9b3eb34061b2a09974bcc2e37195a60312159f86686e1 +size 319 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/balances.parquet b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/balances.parquet new file mode 100644 index 0000000..94d1f4c --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/balances.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb53d14f9bb6dcd36373974f115e249e910d6d6a372548732e865e0dde5ed11a +size 2706 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/orders.parquet b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/orders.parquet new file mode 100644 index 0000000..2a7c14b --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/orders.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1acbe363de1ac0f741e1c8bb4149bb95ec4ea70106c8ca5eb343391c6a9686c9 +size 6771 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/positions.parquet b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/positions.parquet new file mode 100644 index 0000000..2188ca0 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/positions.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37c98d6839e0292f6e62fde406613f8d885f6af0d52e1a6f42d3d3699cc10965 +size 8548 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/results.parquet b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/results.parquet new file mode 100644 index 0000000..59a50c8 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/results.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:785c6b3b236fc9c91639a0c3ea05cd9809e82144dc7726f7c8005b4b1b3ad804 +size 2024 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/risk.parquet b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/risk.parquet new file mode 100644 index 0000000..09a114e --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/data/risk.parquet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:becbc5584940114909969cb68641d1dad728071214056c5a0edc769ece206774 +size 11160 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/balances.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/balances.json.gz new file mode 100644 index 0000000..eca8e3a --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/balances.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4cac312d4d8f4f218852447c21c188f0edf5cbdbf12ee193ca556f34bd4cc203 +size 673 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/normal-log-pages.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/normal-log-pages.json.gz new file mode 100644 index 0000000..683032a --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/normal-log-pages.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8c37eefdd8506ce99a2693918b4f05364875da8e8b0b2ddc7c29a8de7ac11a3 +size 9432 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/normal-log.jsonl.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/normal-log.jsonl.gz new file mode 100644 index 0000000..ad98f85 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/normal-log.jsonl.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:318ebf0566f35817bbc07a22a16b433153c15d9cfe5d6f29fb6a22ac4c270475 +size 12585 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/orders.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/orders.json.gz new file mode 100644 index 0000000..2b7f650 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/orders.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64e36a194f0a69c76dc2e24339795179ceaa3b2ae30f172897d6349f2468485e +size 982 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/period_risks.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/period_risks.json.gz new file mode 100644 index 0000000..f851046 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/period_risks.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c14bfcf1832a180081ce0abd2061eeb1dc780074f0c2cdfd923be805f2407368 +size 22 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/positions.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/positions.json.gz new file mode 100644 index 0000000..2ddac82 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/positions.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3759083cf4a5026b991187bb08d15c720a0671effa3af071c7d0af42a034cce9 +size 3406 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/records.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/records.json.gz new file mode 100644 index 0000000..c7f68c1 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/records.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64406e753d82e11c6bd63d1d04bb6aa083eefa0a46949bbfce994d556b989f0f +size 22 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/research-response-537e1e2e2ace4daeb56a969f.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/research-response-537e1e2e2ace4daeb56a969f.json.gz new file mode 100644 index 0000000..09b4fe5 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/research-response-537e1e2e2ace4daeb56a969f.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d992a3d340f638fa0e328468149fe2189f06fc81cdc211c73b8f2b7c4b0f43c4 +size 1972 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/results.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/results.json.gz new file mode 100644 index 0000000..85dd926 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/results.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e29d5335698c448764f0b809cb741d2b86f5838c2a5d01b75e62648dfddffe1a +size 1024 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/risk.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/risk.json.gz new file mode 100644 index 0000000..4cb814b --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/raw/risk.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:297d63e4a122f1d182e8cf86e330102b0dd9a7fc9544299190ec01905587164d +size 572 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/reports/live-summary.json.gz b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/reports/live-summary.json.gz new file mode 100644 index 0000000..ddd6b06 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/snapshots/58318b3133b566a639b76b59/reports/live-summary.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:071d34c536e95d296ec867aa743b626dc933963583989be3e1ac648046735890 +size 671 diff --git a/joinquant/strategies/strategy-002/simulations/simulation-001/source_versions/016ce20a8342a3337b75d3f20e9eaceef8fb9d48452566a0365d64aba4d2d9fc.json b/joinquant/strategies/strategy-002/simulations/simulation-001/source_versions/016ce20a8342a3337b75d3f20e9eaceef8fb9d48452566a0365d64aba4d2d9fc.json new file mode 100644 index 0000000..3ca2748 --- /dev/null +++ b/joinquant/strategies/strategy-002/simulations/simulation-001/source_versions/016ce20a8342a3337b75d3f20e9eaceef8fb9d48452566a0365d64aba4d2d9fc.json @@ -0,0 +1,3 @@ +{ + "backtest_id": "378bf9974b75f1877c17c6b658e074b9" +} diff --git a/joinquant/strategies/strategy_index.csv b/joinquant/strategies/strategy_index.csv index 6d72f0c..141d4a4 100644 --- a/joinquant/strategies/strategy_index.csv +++ b/joinquant/strategies/strategy_index.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:021b707f42792c604d97f4160ed7ecca03a8eba03c8dfd84604d328f5f4047e1 +oid sha256:9345a6e6c2dfb91d7cf899adb3149f0f968bba8c497a7726da167f95422b0422 size 820 diff --git a/openspec/changes/build-turtle-etf-local-research-workflow/specs/local-quant-research-workflow/spec.md b/openspec/changes/build-turtle-etf-local-research-workflow/specs/local-quant-research-workflow/spec.md index d912fc2..3607eda 100644 --- a/openspec/changes/build-turtle-etf-local-research-workflow/specs/local-quant-research-workflow/spec.md +++ b/openspec/changes/build-turtle-etf-local-research-workflow/specs/local-quant-research-workflow/spec.md @@ -168,6 +168,16 @@ - **WHEN** 扫描本地研究运行器和策略项目的生产导入 - **THEN** 两者只依赖以聚宽现有结果数据为基准的读取契约,不导入 `quant_analysis` 的指标、归因、稳健性、压力、证据矩阵或报告实现 +#### Scenario: 独立资产扩展使用真实单场景矩阵 +- **WHEN** 主 agent 已冻结候选前置筛选结果并生成原 11 只基线、完整扩展、逐只删除、逐扩展切片删除和五个成本执行压力场景 +- **THEN** 每个场景分别调用本地研究 Skill 一次并生成一个标准结果包,全部六只候选通过时总数为 16 +- **AND** Skill 的输入输出和停止状态仍只表达一个场景,不包含本次候选、场景数量或分析逻辑 + +#### Scenario: 不同资产池使用可比较的精确快照 +- **WHEN** 两个调用场景的证券集合不同 +- **THEN** 每个调用只接收与自身证券集合完全一致的不可变快照 +- **AND** 快照共享相同不可变批次、截止日、字段和价格口径,重叠证券行情及公司行动摘要必须完全一致 + ### Requirement: 仓库运行与能力复用边界 系统 MUST(必须)使用项目 `.venv`(虚拟环境)运行本地 Python(编程语言)入口,并复用既有聚宽认证和归档能力,不得保存或打印账号、密码、Token(访问令牌)或 Cookie(浏览器凭证)。 diff --git a/scripts/research/market_data/candidate_screen.py b/scripts/research/market_data/candidate_screen.py new file mode 100644 index 0000000..e12176a --- /dev/null +++ b/scripts/research/market_data/candidate_screen.py @@ -0,0 +1,321 @@ +from __future__ import annotations + +import hashlib +import json +import math +from dataclasses import asdict, dataclass, replace +from pathlib import Path +from statistics import median +from typing import Literal, Mapping, Sequence + +import pandas as pd + +from .contracts import ( + MarketDataContractError, + corporate_actions_digest, + normalize_market_rows, +) +from .economic_returns import EconomicReturnError, derive_continuous_prices +from .storage import MarketDataIntegrityError + + +@dataclass(frozen=True) +class CandidateScreenRule: + min_valid_days: int = 750 + money_lookback_days: int = 20 + min_median_money: float = 100_000_000.0 + + def __post_init__(self) -> None: + if self.min_valid_days <= 0 or self.money_lookback_days <= 0: + raise ValueError("candidate screen day thresholds must be positive") + if not math.isfinite(self.min_median_money) or self.min_median_money < 0: + raise ValueError("candidate screen money threshold must be non-negative") + + +@dataclass(frozen=True) +class SecurityScreenResult: + security: str + status: Literal["pass", "fail"] + valid_days: int + median_money_20d: float | None + official_start_date: str | None + first_market_date: str | None + last_market_date: str | None + market_rows: int + corporate_actions_digest: str + reason_codes: tuple[str, ...] + instrument_risk_notes: tuple[str, ...] + + +@dataclass(frozen=True) +class CandidateScreenResult: + screen_id: str + as_of_date: str + requested_securities: tuple[str, ...] + passed_securities: tuple[str, ...] + rule: CandidateScreenRule + results: tuple[SecurityScreenResult, ...] + + +def _finite_number(value: object) -> float | None: + try: + result = float(value) + except (TypeError, ValueError): + return None + return result if math.isfinite(result) else None + + +def _integer(value: object) -> int | None: + if isinstance(value, bool): + return None + try: + result = int(value) + except (TypeError, ValueError): + return None + return result if result == value else None + + +def _valid_non_paused_row(row: Mapping[str, object]) -> tuple[bool, str | None]: + if row.get("paused") is True: + return False, None + values = { + field: _finite_number(row.get(field)) + for field in ( + "open", + "high", + "low", + "close", + "pre_close", + "volume", + "money", + "factor", + "high_limit", + "low_limit", + ) + } + if any(value is None for value in values.values()): + return False, "incomplete_non_paused_fields" + open_price = float(values["open"]) + high = float(values["high"]) + low = float(values["low"]) + close = float(values["close"]) + pre_close = float(values["pre_close"]) + volume = float(values["volume"]) + money = float(values["money"]) + factor = float(values["factor"]) + high_limit = float(values["high_limit"]) + low_limit = float(values["low_limit"]) + if ( + min(open_price, high, low, close, pre_close, factor) <= 0.0 + or volume < 0.0 + or money < 0.0 + or high < max(open_price, low, close) + or low > min(open_price, high, close) + or high_limit < low_limit + ): + return False, "illegal_ohlc" + return True, None + + +def _canonical_document(result: CandidateScreenResult) -> dict[str, object]: + document = asdict(result) + document["requested_securities"] = list(result.requested_securities) + document["passed_securities"] = list(result.passed_securities) + document["results"] = [ + { + **asdict(item), + "reason_codes": list(item.reason_codes), + "instrument_risk_notes": list(item.instrument_risk_notes), + } + for item in result.results + ] + return document + + +def _screen_id(result: CandidateScreenResult) -> str: + document = _canonical_document(replace(result, screen_id="")) + document.pop("screen_id", None) + payload = json.dumps( + document, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode("utf-8") + return hashlib.sha256(payload).hexdigest() + + +def _screen_security( + *, + security: str, + raw_rows: Sequence[Mapping[str, object]], + corporate_actions: Sequence[Mapping[str, object]], + metadata: Mapping[str, object] | None, + as_of_date: str, + rule: CandidateScreenRule, + risk_notes: Sequence[str], +) -> SecurityScreenResult: + reasons: set[str] = set() + try: + rows = normalize_market_rows(raw_rows) + except MarketDataContractError: + rows = [] + reasons.add("invalid_market_contract") + if len(rows) != len(raw_rows): + reasons.add("duplicate_market_rows") + if any(str(row["date"]) > as_of_date for row in rows): + reasons.add("market_rows_after_cutoff") + rows = [row for row in rows if str(row["date"]) <= as_of_date] + + valid_rows: list[dict[str, object]] = [] + for row in rows: + is_valid, reason = _valid_non_paused_row(row) + if reason: + reasons.add(reason) + if is_valid: + valid_rows.append(row) + if len(valid_rows) < rule.min_valid_days: + reasons.add(f"valid_days_below_{rule.min_valid_days}") + + median_money: float | None = None + if len(valid_rows) >= rule.money_lookback_days: + money_values = [ + float(row["money"]) + for row in valid_rows[-rule.money_lookback_days :] + ] + median_money = float(median(money_values)) + if median_money < rule.min_median_money: + reasons.add( + "median_money_below_" + str(int(rule.min_median_money)) + ) + else: + reasons.add("insufficient_money_lookback") + + first_date = str(rows[0]["date"]) if rows else None + last_date = str(rows[-1]["date"]) if rows else None + official_start: str | None = None + if metadata is None: + reasons.add("official_metadata_missing") + else: + if str(metadata.get("security", "")) != security: + reasons.add("official_security_mismatch") + official_start = str(metadata.get("official_start_date") or "") or None + if ( + official_start is None + or first_date is None + or official_start > first_date + or str(metadata.get("first_market_date") or "") != first_date + or str(metadata.get("last_market_date") or "") != last_date + or _integer(metadata.get("market_rows")) != len(rows) + ): + reasons.add("official_coverage_mismatch") + + selected_actions = tuple( + action + for action in corporate_actions + if str(action.get("security", "")) == security + ) + action_digest = corporate_actions_digest(selected_actions) + if rows: + try: + derive_continuous_prices( + pd.DataFrame(rows), + security=security, + corporate_actions=selected_actions, + ) + except (EconomicReturnError, ValueError): + reasons.add("corporate_action_evidence_unreconciled") + + ordered_reasons = tuple(sorted(reasons)) + return SecurityScreenResult( + security=security, + status="fail" if ordered_reasons else "pass", + valid_days=len(valid_rows), + median_money_20d=median_money, + official_start_date=official_start, + first_market_date=first_date, + last_market_date=last_date, + market_rows=len(rows), + corporate_actions_digest=action_digest, + reason_codes=ordered_reasons, + instrument_risk_notes=tuple(str(note) for note in risk_notes), + ) + + +def screen_candidates( + *, + rows: Sequence[Mapping[str, object]], + corporate_actions: Sequence[Mapping[str, object]], + official_security_metadata: Mapping[str, Mapping[str, object]], + requested_securities: Sequence[str], + as_of_date: str, + rule: CandidateScreenRule, + instrument_risk_notes: Mapping[str, Sequence[str]] | None = None, +) -> CandidateScreenResult: + try: + pd.Timestamp(as_of_date) + except (TypeError, ValueError) as exc: + raise ValueError("as_of_date must be a valid date") from exc + requested = tuple(sorted(str(value) for value in requested_securities)) + if not requested or len(set(requested)) != len(requested): + raise ValueError("requested securities must be non-empty and unique") + notes = instrument_risk_notes or {} + results = tuple( + _screen_security( + security=security, + raw_rows=tuple( + row for row in rows if str(row.get("security", "")) == security + ), + corporate_actions=corporate_actions, + metadata=official_security_metadata.get(security), + as_of_date=as_of_date, + rule=rule, + risk_notes=notes.get(security, ()), + ) + for security in requested + ) + unknown_rows = sorted( + {str(row.get("security", "")) for row in rows} - set(requested) + ) + if unknown_rows: + raise ValueError( + "candidate rows include unrequested securities: " + ", ".join(unknown_rows) + ) + provisional = CandidateScreenResult( + screen_id="", + as_of_date=as_of_date, + requested_securities=requested, + passed_securities=tuple( + item.security for item in results if item.status == "pass" + ), + rule=rule, + results=results, + ) + return replace(provisional, screen_id=_screen_id(provisional)) + + +def write_candidate_screen(result: CandidateScreenResult, *, root: Path) -> Path: + document = _canonical_document(result) + payload = ( + json.dumps( + document, + ensure_ascii=False, + sort_keys=True, + indent=2, + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + target = Path(root) / "screens" / f"{result.screen_id}.json" + target.parent.mkdir(parents=True, exist_ok=True) + if target.exists(): + if target.read_bytes() != payload: + raise MarketDataIntegrityError("candidate screen content conflict") + return target + try: + with target.open("xb") as handle: + handle.write(payload) + except FileExistsError: + if target.read_bytes() != payload: + raise MarketDataIntegrityError("candidate screen content conflict") + return target diff --git a/scripts/research/market_data/joinquant_export.py b/scripts/research/market_data/joinquant_export.py index f672c85..bc99a30 100644 --- a/scripts/research/market_data/joinquant_export.py +++ b/scripts/research/market_data/joinquant_export.py @@ -135,8 +135,10 @@ def _write_verified(path, output): def export_market_data(): frames = [] + coverage = [] for security in SECURITIES: - start_date = get_security_info(security).start_date + security_info = get_security_info(security) + start_date = security_info.start_date frame = get_price( security, start_date=start_date, @@ -152,7 +154,16 @@ def export_market_data(): frame['date'] = frame['date'].map( lambda value: value.strftime('%Y-%m-%d') ) + if frame.empty: + raise ValueError('security has no market rows: ' + security) frame.insert(1, 'security', security) + coverage.append({{ + 'security': security, + 'official_start_date': _text(security_info.start_date), + 'first_market_date': frame['date'].iloc[0], + 'last_market_date': frame['date'].iloc[-1], + 'market_rows': len(frame), + }}) frames.append(frame[OUTPUT_FIELDS]) if not frames: @@ -161,7 +172,9 @@ def export_market_data(): output = output[OUTPUT_FIELDS].sort_values( ['date', 'security'], kind='mergesort' ) - return _write_verified(REMOTE_PATH, output) + result = _write_verified(REMOTE_PATH, output) + result['securities'] = coverage + return result def export_corporate_actions(): diff --git a/tests/local_quant_research/test_joinquant_export.py b/tests/local_quant_research/test_joinquant_export.py index 4d59ac0..b63f388 100644 --- a/tests/local_quant_research/test_joinquant_export.py +++ b/tests/local_quant_research/test_joinquant_export.py @@ -105,7 +105,8 @@ def test_rendered_program_uses_verified_joinquant_research_contract() -> None: ) compile(program, "", "exec") - assert "get_security_info(security).start_date" in program + assert "security_info = get_security_info(security)" in program + assert "start_date = security_info.start_date" in program assert "get_price(" in program assert "fq=None" in program assert "skip_paused=False" in program @@ -230,6 +231,15 @@ def run_query(_query): payload ).hexdigest() assert namespace["export_result"]["market_data"]["rows"] == 2 + assert namespace["export_result"]["market_data"]["securities"] == [ + { + "security": "510300.XSHG", + "official_start_date": "2026-01-05", + "first_market_date": "2026-01-05", + "last_market_date": "2026-01-06", + "market_rows": 2, + } + ] assert namespace["export_result"]["corporate_actions"]["sha256"] == ( hashlib.sha256(action_payload).hexdigest() ) diff --git a/tests/local_quant_research/test_market_data_candidate_screen.py b/tests/local_quant_research/test_market_data_candidate_screen.py new file mode 100644 index 0000000..d978534 --- /dev/null +++ b/tests/local_quant_research/test_market_data_candidate_screen.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pandas as pd +import pytest + +from scripts.research.market_data.candidate_screen import ( + CandidateScreenRule, + screen_candidates, + write_candidate_screen, +) + + +RULE = CandidateScreenRule( + min_valid_days=750, + money_lookback_days=20, + min_median_money=100_000_000.0, +) + + +def _candidate_rows( + *, + valid_days: int = 750, + median_money: float = 100_000_000.0, + illegal_ohlc: bool = False, +) -> list[dict[str, object]]: + dates = pd.bdate_range(end="2026-07-13", periods=valid_days) + rows = [] + previous_close = 10.0 + for index, current_date in enumerate(dates): + close = 10.0 + index / 10_000 + row = { + "date": current_date.strftime("%Y-%m-%d"), + "security": "159985.XSHE", + "open": close, + "high": close + 0.1, + "low": close - 0.1, + "close": close, + "pre_close": previous_close, + "volume": 10_000_000.0, + "money": median_money, + "factor": 1.0, + "paused": False, + "high_limit": close * 1.1, + "low_limit": close * 0.9, + } + rows.append(row) + previous_close = close + if illegal_ohlc: + rows[-1]["high"] = float(rows[-1]["low"]) - 1.0 + return rows + + +def _official_metadata(rows: list[dict[str, object]]) -> dict[str, dict[str, object]]: + return { + "159985.XSHE": { + "security": "159985.XSHE", + "official_start_date": rows[0]["date"], + "first_market_date": rows[0]["date"], + "last_market_date": rows[-1]["date"], + "market_rows": len(rows), + } + } + + +def _screen(rows: list[dict[str, object]]): + return screen_candidates( + rows=rows, + corporate_actions=(), + official_security_metadata=_official_metadata(rows), + requested_securities=("159985.XSHE",), + as_of_date="2026-07-13", + rule=RULE, + instrument_risk_notes={ + "159985.XSHE": ( + "移仓与期限结构风险", + "保证金外资产收益不等于现货涨跌", + ) + }, + ) + + +def test_screen_counts_only_valid_days_and_freezes_risk_disclosure() -> None: + rows = _candidate_rows() + + result = _screen(rows) + + security = result.results[0] + assert security.status == "pass" + assert security.valid_days == 750 + assert security.median_money_20d == pytest.approx(100_000_000.0) + assert security.instrument_risk_notes == ( + "移仓与期限结构风险", + "保证金外资产收益不等于现货涨跌", + ) + assert result.passed_securities == ("159985.XSHE",) + + +@pytest.mark.parametrize( + ("rows", "reason"), + [ + (_candidate_rows(valid_days=749), "valid_days_below_750"), + (_candidate_rows(median_money=99_999_999.0), "median_money_below_100000000"), + (_candidate_rows(illegal_ohlc=True), "illegal_ohlc"), + ], +) +def test_screen_rejects_each_fixed_gate( + rows: list[dict[str, object]], reason: str +) -> None: + result = _screen(rows) + + assert reason in result.results[0].reason_codes + assert result.passed_securities == () + + +def test_screen_report_is_content_addressed_and_contains_no_backtest_metrics( + tmp_path: Path, +) -> None: + result = _screen(_candidate_rows()) + + path = write_candidate_screen(result, root=tmp_path) + document = json.loads(path.read_text(encoding="utf-8")) + + assert path == tmp_path / "screens" / f"{result.screen_id}.json" + assert document["screen_id"] == result.screen_id + forbidden = {"returns", "cagr", "calmar", "max_drawdown"} + serialized = json.dumps(document, ensure_ascii=False).lower() + assert all(f'"{field}"' not in serialized for field in forbidden) + assert write_candidate_screen(result, root=tmp_path) == path + + +def test_invalid_official_coverage_is_a_screen_failure_not_an_exception() -> None: + rows = _candidate_rows() + metadata = _official_metadata(rows) + metadata["159985.XSHE"]["market_rows"] = None + + result = screen_candidates( + rows=rows, + corporate_actions=(), + official_security_metadata=metadata, + requested_securities=("159985.XSHE",), + as_of_date="2026-07-13", + rule=RULE, + ) + + assert result.results[0].status == "fail" + assert "official_coverage_mismatch" in result.results[0].reason_codes