feat: lock trading delivery checklist and liquidity repair flow

This commit is contained in:
2026-05-04 05:20:56 -04:00
parent d17b0c8dc2
commit 36993edf5b
14 changed files with 1225 additions and 50 deletions
+62 -2
View File
@@ -141,6 +141,65 @@ test "$(docker inspect -f '{{.State.Health.Status}}' ai-ict)" = "healthy"
.venv/bin/python scripts/check_local_demo_readiness.py
```
## Trading Delivery Checklist
Use this locked trading delivery checklist before every market validation or deploy, so the trading module can be promoted without relying on chat memory.
must-run before every market validation or deploy:
```bash
.venv/bin/python -m unittest discover -s tests/unit -p "test_*.py"
.venv/bin/python scripts/run_offline_checks.py
.venv/bin/python scripts/check_local_demo_readiness.py
.venv/bin/python scripts/check_trading_delivery_readiness.py
```
Database gate:
```bash
READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py
HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py
.venv/bin/python scripts/run_historical_compatibility_repair.py --apply-safe-repairs
HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py
PIPELINE_OUTPUT_JSON=1 BACKTEST_REQUIRE_QUALITY_GATE=1 .venv/bin/python scripts/run_pipeline.py
```
Use `--apply-safe-repairs` only when the dry-run output shows safe candidates. After the second dry-run, manual review is still required for `needs_sweep_reference_migration` and `keep_version_history`, and `backtest_quality_gate.passed` must be `true` before promotion.
Deploy and health check:
```bash
docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml down --remove-orphans
docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml up -d --build
docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml ps
test "$(docker ps --filter label=com.docker.compose.project=ai-ict --format '{{.Names}}' | wc -l | tr -d ' ')" = "1"
test "$(docker ps --filter name=^/ai-ict$ --format '{{.Names}}' | wc -l | tr -d ' ')" = "1"
test "$(docker inspect -f '{{.State.Health.Status}}' ai-ict)" = "healthy"
curl -fsS http://127.0.0.1:8000/health
curl -fsS "http://127.0.0.1:8000/signals?instrument_id=1&limit=1"
```
Post-deploy observe:
```bash
READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py
REPORTS_OUTPUT_JSON=1 .venv/bin/python scripts/run_reports.py
```
The paper execution boundary remains in force here: `run_pipeline.py` and the execution path are still paper-only, and any API `5xx`, Docker unhealthy state, or non-empty readiness reasons should stop promotion immediately.
Rollback trigger:
```bash
git rev-parse HEAD
docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml logs --tail=100 ai-ict
git worktree add /tmp/ai-ict-rollback <last-known-good-sha>
cd /tmp/ai-ict-rollback && ./scripts/deploy_server.exp
rm -rf /tmp/ai-ict-rollback
```
Record the current commit as the last-known-good SHA before deploy. If any gate above fails, capture logs first, then redeploy from the clean rollback worktree rather than mutating the active development checkout.
## Runtime utility scripts
- `python3 scripts/run_dev.py` - start the local development API server and minimal dashboard
@@ -161,13 +220,14 @@ test "$(docker inspect -f '{{.State.Health.Status}}' ai-ict)" = "healthy"
- `python3 scripts/run_backtest.py` - run backtest and write results
- `python3 scripts/run_autonomous_cycle.py` - run the unattended cycle: offline checks, database probe, readiness checks, then guarded pipeline when the database is available; prints `Next actions` for the next unattended step
- `python3 scripts/run_database_readiness.py` - run read-only database readiness checks for connection, schema, seed data, and candle availability; prints `next_actions` and `action_plan` in JSON mode
- `.venv/bin/python scripts/run_offline_checks.py` - run the broader offline check suite, including the M35 acceptance posture through the Training Case acceptance manifest for focused milestone test suites, the Training Case package smoke registered as `training_case_package`, dashboard syntax validation as `dashboard_js_syntax`, and dashboard and public docs privacy smoke coverage; this public validation runs as offline checks without external services
- `.venv/bin/python scripts/run_offline_checks.py` - run the broader offline check suite, including the M35 acceptance posture through the Training Case acceptance manifest for focused milestone test suites, the Training Case package smoke registered as `training_case_package`, trading delivery checklist drift guard registered as `trading_delivery_readiness`, dashboard syntax validation as `dashboard_js_syntax`, and dashboard and public docs privacy smoke coverage; this public validation runs as offline checks without external services
- `.venv/bin/python scripts/check_trading_delivery_readiness.py` - verify `README.md` and `RUNBOOK.md` still expose the locked trading delivery checklist for preflight, deploy, observe, and rollback without relying on chat memory
- `.venv/bin/python scripts/run_training_case_package.py --help` - show the offline package smoke help for public package import/export behavior, public export/public output semantics, and stable public error codes for invalid input paths and invalid output targets
- `.venv/bin/python scripts/run_training_case_package.py --input tests/fixtures/training_case_package_m6.json --output /tmp/training_case_public_export.json` - run the Training Case offline package smoke, an import/export smoke with local files, and write public output as the public export in the public package shape; exported public JSON omits internal solution details
- `.venv/bin/python scripts/run_training_case_generator.py --help` - show the offline prototype generator help for read-only Training case draft generation from trading-derived seed events
- `.venv/bin/python scripts/run_training_case_generator.py --input tests/fixtures/training_case_generator_seed_v1.json --output /tmp/generated_training_case_drafts.json` - run the offline Training case generator prototype and write a public draft package without touching Training database tables
- `.venv/bin/python scripts/run_historical_compatibility_repair.py` - scan for older stored signal/FVG/MSS/liquidity compatibility issues in the connected database without applying changes
- `.venv/bin/python scripts/run_historical_compatibility_repair.py --apply-safe-repairs` - apply only the safe historical compatibility repairs for signal invalidations plus MSS/FVG meta backfills; liquidity duplicate groups remain manual-review only
- `.venv/bin/python scripts/run_historical_compatibility_repair.py --apply-safe-repairs` - apply the safe historical compatibility repairs for signal invalidations, MSS/FVG meta backfills, and liquidity safe merges limited to unreferenced supersets plus exact duplicate rows with identical sweep events; `needs_sweep_reference_migration` and `keep_version_history` groups still require manual review
- `python3 scripts/run_migration_static_checks.py` - check migration order and required core schema without connecting to the database
- `python3 scripts/run_script_static_checks.py` - check automation scripts for unsafe ambient environment fallback patterns
- `python3 scripts/run_replay_file.py <candles.json|candles.jsonl|candles.csv>` - run offline no-lookahead candle replay without database access
+62 -2
View File
@@ -38,6 +38,7 @@ cd /home/yuqei/project/ai-exchange
.venv/bin/python -m unittest discover -s tests/unit -p "test_*.py"
.venv/bin/python scripts/run_offline_checks.py
.venv/bin/python scripts/check_local_demo_readiness.py
.venv/bin/python scripts/check_trading_delivery_readiness.py
python3 scripts/run_migration_static_checks.py
python3 scripts/run_script_static_checks.py
```
@@ -74,8 +75,8 @@ Prototype intent:
The current historical compatibility repair path is intentionally safe by default:
- default mode is read-only scan
- safe apply mode repairs only signal invalidations plus MSS/FVG meta compatibility fields
- liquidity duplicate groups are reported for manual review and are not auto-merged
- safe apply mode repairs signal invalidations, MSS/FVG meta compatibility fields, and liquidity safe merges limited to unreferenced supersets plus exact duplicate rows with identical sweep events
- manual review still owns `needs_sweep_reference_migration` and `keep_version_history` groups
Dry-run scan:
@@ -98,6 +99,65 @@ cd /home/yuqei/project/ai-exchange
.venv/bin/python scripts/run_historical_compatibility_repair.py --apply-safe-repairs
```
## Trading Delivery Checklist
Use this locked trading delivery checklist before every market validation or deploy, so the trading module can be promoted without relying on chat memory.
must-run before every market validation or deploy:
```bash
.venv/bin/python -m unittest discover -s tests/unit -p "test_*.py"
.venv/bin/python scripts/run_offline_checks.py
.venv/bin/python scripts/check_local_demo_readiness.py
.venv/bin/python scripts/check_trading_delivery_readiness.py
```
Database gate:
```bash
READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py
HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py
.venv/bin/python scripts/run_historical_compatibility_repair.py --apply-safe-repairs
HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py
PIPELINE_OUTPUT_JSON=1 BACKTEST_REQUIRE_QUALITY_GATE=1 .venv/bin/python scripts/run_pipeline.py
```
Use `--apply-safe-repairs` only when the dry-run output shows safe candidates. After the second dry-run, manual review is still required for `needs_sweep_reference_migration` and `keep_version_history`, and `backtest_quality_gate.passed` must be `true` before promotion.
Deploy and health check:
```bash
docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml down --remove-orphans
docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml up -d --build
docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml ps
test "$(docker ps --filter label=com.docker.compose.project=ai-ict --format '{{.Names}}' | wc -l | tr -d ' ')" = "1"
test "$(docker ps --filter name=^/ai-ict$ --format '{{.Names}}' | wc -l | tr -d ' ')" = "1"
test "$(docker inspect -f '{{.State.Health.Status}}' ai-ict)" = "healthy"
curl -fsS http://127.0.0.1:8000/health
curl -fsS "http://127.0.0.1:8000/signals?instrument_id=1&limit=1"
```
Post-deploy observe:
```bash
READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py
REPORTS_OUTPUT_JSON=1 .venv/bin/python scripts/run_reports.py
```
The paper execution boundary remains in force here: `run_pipeline.py` and the execution path are still paper-only, and any API `5xx`, Docker unhealthy state, or non-empty readiness reasons should stop promotion immediately.
Rollback trigger:
```bash
git rev-parse HEAD
docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml logs --tail=100 ai-ict
git worktree add /tmp/ai-ict-rollback <last-known-good-sha>
cd /tmp/ai-ict-rollback && ./scripts/deploy_server.exp
rm -rf /tmp/ai-ict-rollback
```
Record the current commit as the last-known-good SHA before deploy. If any gate above fails, capture logs first, then redeploy from the clean rollback worktree rather than mutating the active development checkout.
## Git Remote
This repo is currently configured to use SSH for `origin`:
+21 -21
View File
@@ -2,7 +2,7 @@
## Current Goal
Current roadmap items are implemented. The next practical step is to run the new historical compatibility repair path in dry-run mode against a real database and decide whether to apply the safe repairs, or define the next roadmap increment.
从现在开始只推进交易模块交付,优先把运行清单、部署纪律和市场验证前准备补齐,不再扩展 Training 新能力。
## Current Branch
@@ -14,12 +14,14 @@ Current roadmap items are implemented. The next practical step is to run the new
## Completed In Last Session
- Added a historical compatibility repair core module in [src/services/maintenance/historical_compatibility.py](/home/yuqei/project/ai-exchange/src/services/maintenance/historical_compatibility.py) covering three safe auto-repair classes: signal invalidation normalization, MSS meta backfill, and FVG meta compatibility backfill.
- Added [scripts/run_historical_compatibility_repair.py](/home/yuqei/project/ai-exchange/scripts/run_historical_compatibility_repair.py) with default dry-run behavior, explicit safe-apply flags, JSON output support, and manual-review reporting for liquidity duplicate groups.
- Kept liquidity duplicate handling scan-only by default; the script reports duplicate groups for review instead of auto-merging or mutating them.
- Added focused unit coverage for repair planning logic, script dry-run/apply behavior, database-unavailable JSON behavior, and runtime script registry compatibility.
- Documented the new historical compatibility repair script in `README.md` and `RUNBOOK.md`.
- Revalidated the repository with `.venv/bin/python3 -m unittest discover -s tests/unit -p "test_*.py"`, `.venv/bin/python3 scripts/run_offline_checks.py`, and `.venv/bin/python3 scripts/check_local_demo_readiness.py`.
- 已经把交易模块交付前 checklist 锁成可执行单一来源:新增 `scripts/check_trading_delivery_readiness.py`,并把 preflight / database gate / deploy / observe / rollback 的固定命令顺序同步到 `README.md` 与 `RUNBOOK.md`。
- 已经用 focused tests 把这套 checklist 固化:新增 `tests/unit/test_trading_delivery_readiness.py`,扩展 `tests/unit/test_documentation_consistency.py` 和 `scripts/run_offline_checks.py`,避免运行清单再次依赖聊天记忆漂移。
- 已把历史兼容 safe repair 的文档语义纠正到当前实现:`--apply-safe-repairs` 现在包含 signal invalidations、MSS/FVG meta,以及 liquidity safe merges;`needs_sweep_reference_migration` / `keep_version_history` 仍保留人工决策边界。
- 已经完成交易主线的主要规则与验证增强,包括:MSS/FVG richer state、execution/backtest/report lifecycle contract、backtest `by_symbol`、FVG candidate priority convergence,以及历史兼容安全修复脚本。
- 已经在真实数据库上执行了历史兼容安全修复:`signal_invalidations=11`、`mss_meta=4`、`fvg_meta=1189`,再次 dry-run 后这三类候选修复已全部清零。
- 已把 `liquidity` duplicate groups 程序化分类成四类:`safe_auto_merge_unreferenced_superset`、`safe_exact_duplicate_with_duplicate_sweeps`、`needs_sweep_reference_migration`、`keep_version_history`。
- 已在真实数据库上执行 liquidity safe merges:共处理 `8` 组,其中 `1` 组未引用超集重复、`7` 组 exact duplicate 且 duplicate sweep 完全同构;再次 dry-run 后仅剩 `5` 组 `keep_version_history`。
- 训练侧的 quality / selector / generator 原型已完成,但从本轮开始这些不再是当前开发主线。
## Current Working State
@@ -43,9 +45,10 @@ Current roadmap items are implemented. The next practical step is to run the new
- Training case list/export service/API surfaces now also support stable selector inputs for `quality_rank`, `min_quality_score`, and `sort_by`.
- The generator prototype now exists as an offline-only flow: trading-derived seed JSON -> public Training draft package JSON, with empty `answer` content and no database writes.
- Signal-side FVG selection is now explicit and deterministic: exact MSS displacement match wins, otherwise newest same-direction wins, otherwise newest overall wins.
- Historical compatibility repair now exists as a script-backed path: default dry-run scan, optional safe apply for signal invalidations plus MSS/FVG meta, and manual-review-only reporting for liquidity duplicate groups.
- `TASKS.md` now reflects the intended development sequence more explicitly:
there are no active roadmap items left after the current sequence.
- Historical compatibility repair now exists as a script-backed path: default dry-run scan, optional safe apply for signal invalidations plus MSS/FVG meta plus liquidity safe merges, and explicit manual-review boundaries for `needs_sweep_reference_migration` / `keep_version_history`.
- 交易模块交付前 checklist 现在已经被 `README.md` / `RUNBOOK.md` / `scripts/check_trading_delivery_readiness.py` / offline checks 四处对齐,真实部署步骤不再只存在于聊天上下文里。
- 当前真正阻碍交易模块进入市场验证的,已经不再是历史兼容 safe repair 或运行清单缺失,而是是否要继续进入高风险的 `sweep_event pool_id` 引用迁移执行器,以及何时在目标环境执行首轮 checklist rehearsal。
- `TASKS.md` 已重新切回交易模块交付优先,不再把 Training 继续当作当前 in-progress 主线。
- FVG state metadata in the dirty worktree still includes `last_touch_ts`, `touch_count`, `status_reason`, and gap warnings.
- Replay-backed no-lookahead backtest mode remains present and validation is green.
- `scripts/run_dev.py` still starts the persistent local API/dashboard dev server using `AI_ICT_API_HOST` / `AI_ICT_API_PORT`.
@@ -54,10 +57,10 @@ Current roadmap items are implemented. The next practical step is to run the new
## Exact Next Step
If continuing from the current implementation, do this next:
1. run `.venv/bin/python scripts/run_historical_compatibility_repair.py` against the target database in dry-run mode and inspect candidate repair counts plus liquidity duplicate groups;
2. if the dry-run output is acceptable, execute `.venv/bin/python scripts/run_historical_compatibility_repair.py --apply-safe-repairs` in a controlled environment;
3. after the real repair run, decide whether the roadmap should branch into a new migration-backed cleanup phase or shift to a new feature roadmap.
直接在目标环境做首轮 checklist rehearsal:
1. 先跑 `.venv/bin/python scripts/check_trading_delivery_readiness.py`,确认本地文档/脚本未漂移;
2. 按 README/RUNBOOK 中的 Trading Delivery Checklist 顺序执行 database gate、deploy、health-check、post-deploy observe;
3. 根据 rehearsal 结果决定是进入真实市场验证,还是开启高风险的 `sweep_event pool_id` 引用迁移执行器。
## Validation Commands
@@ -65,14 +68,11 @@ If continuing from the current implementation, do this next:
.venv/bin/python3 -m unittest discover -s tests/unit -p "test_*.py"
.venv/bin/python3 scripts/run_offline_checks.py
.venv/bin/python3 scripts/check_local_demo_readiness.py
.venv/bin/python3 scripts/check_trading_delivery_readiness.py
```
## Risks Or Open Questions
- The dashboard now surfaces the new structure-state fields directly, but signal detail is still rendered as structured JSON rather than a dedicated custom detail card.
- Legacy `trade_signal.invalidations` strings are now normalized for API/report/dashboard consumers, but historical rows still physically store mixed old/new string forms in the database until a later migration or cleanup pass.
- The new FVG selector rule is explicit, but it still reflects one specific interpretation of “recent plus context”; if trading review later wants recency to dominate exact context in some setups, that will require another deliberate rule change rather than a repository ordering tweak.
- The generator prototype is intentionally offline and read-only; it is not yet wired into repository-backed Training flows or any public generation route.
- The historical repair path is script-based rather than migration-based. That keeps it safer and reversible, but it also means real data repair still depends on an explicit operator run rather than automatic deploy-time migration.
- The current worktree is no longer fully clean because this session added both code and documentation changes, and there is unrelated untracked legal/planning content in the repo; decide later whether to commit or separate that unrelated content.
- Training remains a maintenance-first module for now; upstream trading semantics should stabilize before adding larger Training features.
- 剩余 `5` 组 `keep_version_history` 类型 liquidity 记录目前被有意保留;如果后面仍要继续清理,就不再是 safe repair,而是更高风险的 sweep 引用迁移工程。
- 交易模块虽然已经有了稳定的 checklist 与回滚边界,但真正的目标环境 rehearsal 还没执行,部署纪律是否足够仍需要一次实跑验证。
- 当前本地工作区还有未跟踪的法务内容草稿,不属于交易交付主线,后续应避免把它们混入交易模块发布。
+11 -3
View File
@@ -2,11 +2,17 @@
## In Progress
- [ ] No active in-progress items beyond the current roadmap.
- [ ] 交易模块交付优先:目标环境首轮 checklist rehearsal
Files: `README.md`, `RUNBOOK.md`, `SESSION.md`, `scripts/check_trading_delivery_readiness.py`
Acceptance: 按锁定后的 Trading Delivery Checklist 在目标环境跑完一次 database gate、deploy、health-check、post-deploy observe,并记录人工复核结论。
Risks: 如果 rehearsal 没有在真实环境执行,当前 checklist 仍然只是在本地文档和测试层面成立。
## Next Up
- [ ] No additional next-up items beyond the current roadmap.
- [ ] 交易模块交付优先:sweep_event 引用迁移与 liquidity 合并执行器
Files: `src/services/maintenance/`, `scripts/`, `tests/unit/`, `migrations/`
Acceptance: 在 exact duplicate 且引用可安全迁移的前提下,提供 `sweep_events.pool_id` 重绑与冗余 pool 清理执行器;默认可 dry-run,显式 apply,且有冲突保护。
Risks: 这项工作会直接修改历史引用链,如果唯一约束冲突处理不完整,会破坏 sweep 历史语义。
## Backlog
@@ -18,7 +24,9 @@
## Done
- [x] 2026-05-04 完成历史数据修复与兼容迁移:新增历史兼容修复核心模块与 `scripts/run_historical_compatibility_repair.py`,默认 dry-run 扫描旧 `invalidations` 文案、MSS/FVG 粗元数据与 liquidity duplicate group,并支持仅对 signal invalidations 与 MSS/FVG meta 执行 safe repairs;补充 focused tests、README/RUNBOOK 入口说明,并通过单测、offline checks 和 local demo readiness。
- [x] 2026-05-04 完成交易模块交付前运行/观测/回滚清单固化:新增 `scripts/check_trading_delivery_readiness.py` 作为 checklist 单一来源,把 preflight / database gate / deploy / observe / rollback 命令顺序同步到 `README.md` 与 `RUNBOOK.md`,扩展 `tests/unit/test_trading_delivery_readiness.py`、`tests/unit/test_documentation_consistency.py` 与 offline checks;同时把 `run_historical_compatibility_repair.py --apply-safe-repairs` 文档语义更新为当前真实实现(signal invalidations、MSS/FVG meta、liquidity safe merges),避免交易交付继续依赖聊天记忆。
- [x] 2026-05-04 完成交易模块交付优先的 liquidity 历史重复组分类与安全收敛路径:脚本已能把历史 pool 分成 `safe_auto_merge_unreferenced_superset`、`safe_exact_duplicate_with_duplicate_sweeps`、`needs_sweep_reference_migration`、`keep_version_history` 四类;并已在真实数据库上自动处理 `8` 组安全合并(`1` 组未引用超集 + `7` 组 exact duplicate 且 duplicate sweep 完全同构),剩余 `5` 组保留为结构版本历史;已通过 focused tests、全量单测、offline checks 和 local demo readiness。
- [x] 2026-05-04 完成历史数据修复与兼容迁移:新增历史兼容修复核心模块与 `scripts/run_historical_compatibility_repair.py`,默认 dry-run 扫描旧 `invalidations` 文案、MSS/FVG 粗元数据与 liquidity duplicate group,并支持对 signal invalidations、MSS/FVG meta 与 liquidity safe merges 执行受控 safe repairs;补充 focused tests、README/RUNBOOK 入口说明,并通过单测、offline checks 和 local demo readiness。
- [x] 2026-05-04 完成 FVG 候选优先级规则再收敛:signal 层不再被 repository 的隐式 SQL 偏好控制,而是显式使用“先取最近候选,再优先与当前 MSS displacement 精确匹配,其次同方向,最后回退到最新 FVG”的规则;已通过 focused FVG/signal tests、全量单测、offline checks 和 local demo readiness。
- [x] 2026-05-04 完成自动 Training case generator 预研:新增只读离线原型 `src/services/training/case_generator.py` 与 `scripts/run_training_case_generator.py`,输入 trading-derived seed package,输出公开可消费的 Training case 草案 package;新增 fixture、focused tests、README/RUNBOOK 入口说明;已通过单测、offline checks 和 local demo readiness。
- [x] 2026-05-04 完成 Training case quality scorer / selector:在现有公开只读 `quality` 对象上新增稳定的 quality filter/sort 能力,支持 `quality_rank`、`min_quality_score`、`sort_by` 的服务/API 选择器入口,且默认行为不变;已通过单测、offline checks 和 local demo readiness。
+197
View File
@@ -0,0 +1,197 @@
from dataclasses import dataclass
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parents[1]
TRADING_DELIVERY_DOCS = (
ROOT / "README.md",
ROOT / "RUNBOOK.md",
)
TRADING_DELIVERY_SECTION_HEADING = "Trading Delivery Checklist"
TRADING_DELIVERY_COMPOSE_PREFIX = (
"docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml"
)
TRADING_DELIVERY_COMMAND_GROUPS = (
(
"pre_deploy_offline",
(
'.venv/bin/python -m unittest discover -s tests/unit -p "test_*.py"',
".venv/bin/python scripts/run_offline_checks.py",
".venv/bin/python scripts/check_local_demo_readiness.py",
".venv/bin/python scripts/check_trading_delivery_readiness.py",
),
),
(
"database_gate",
(
"READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py",
"HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py",
".venv/bin/python scripts/run_historical_compatibility_repair.py --apply-safe-repairs",
"HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py",
"PIPELINE_OUTPUT_JSON=1 BACKTEST_REQUIRE_QUALITY_GATE=1 .venv/bin/python scripts/run_pipeline.py",
),
),
(
"deploy_and_health",
(
f"{TRADING_DELIVERY_COMPOSE_PREFIX} down --remove-orphans",
f"{TRADING_DELIVERY_COMPOSE_PREFIX} up -d --build",
f"{TRADING_DELIVERY_COMPOSE_PREFIX} ps",
'test "$(docker ps --filter label=com.docker.compose.project=ai-ict --format \'{{.Names}}\' | wc -l | tr -d \' \')" = "1"',
'test "$(docker ps --filter name=^/ai-ict$ --format \'{{.Names}}\' | wc -l | tr -d \' \')" = "1"',
'test "$(docker inspect -f \'{{.State.Health.Status}}\' ai-ict)" = "healthy"',
"curl -fsS http://127.0.0.1:8000/health",
'curl -fsS "http://127.0.0.1:8000/signals?instrument_id=1&limit=1"',
),
),
(
"post_deploy_observe",
(
"READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py",
"REPORTS_OUTPUT_JSON=1 .venv/bin/python scripts/run_reports.py",
),
),
(
"rollback_boundary",
(
"git rev-parse HEAD",
f"{TRADING_DELIVERY_COMPOSE_PREFIX} logs --tail=100 ai-ict",
"git worktree add /tmp/ai-ict-rollback <last-known-good-sha>",
"cd /tmp/ai-ict-rollback && ./scripts/deploy_server.exp",
"rm -rf /tmp/ai-ict-rollback",
),
),
)
TRADING_DELIVERY_COMMANDS = tuple(
command
for _group_name, commands in TRADING_DELIVERY_COMMAND_GROUPS
for command in commands
)
TRADING_DELIVERY_REQUIRED_SNIPPETS = (
"must-run before every market validation or deploy",
"only when the dry-run output shows safe candidates",
"manual review is still required for `needs_sweep_reference_migration` and `keep_version_history`",
"paper execution boundary",
"Rollback trigger",
"last-known-good SHA",
"without relying on chat memory",
)
TRADING_DELIVERY_REQUIRED_LOCAL_PATHS = (
ROOT / "scripts" / "check_local_demo_readiness.py",
ROOT / "scripts" / "deploy_server.exp",
ROOT / "scripts" / "run_database_readiness.py",
ROOT / "scripts" / "run_historical_compatibility_repair.py",
ROOT / "scripts" / "run_offline_checks.py",
ROOT / "scripts" / "run_pipeline.py",
ROOT / "scripts" / "run_reports.py",
)
TRADING_DELIVERY_COMMAND_PREFIXES = (
".venv/bin/python",
"READINESS_OUTPUT_JSON=1",
"HISTORICAL_REPAIR_OUTPUT_JSON=1",
"PIPELINE_OUTPUT_JSON=1",
"REPORTS_OUTPUT_JSON=1",
"docker compose",
"test ",
"curl ",
"git ",
"cd /tmp/ai-ict-rollback &&",
"rm -rf /tmp/ai-ict-rollback",
)
@dataclass(frozen=True)
class TradingDeliveryReadinessReport:
checked_docs: tuple[Path, ...]
command_lines_by_doc: tuple[tuple[Path, tuple[str, ...]], ...]
errors: tuple[str, ...]
def check_trading_delivery_readiness(
docs: tuple[Path, ...] = TRADING_DELIVERY_DOCS,
) -> TradingDeliveryReadinessReport:
command_lines_by_doc = []
errors = []
for path in TRADING_DELIVERY_REQUIRED_LOCAL_PATHS:
if not path.is_file():
errors.append(f"{_display_path(path)} is missing")
for path in docs:
if not path.is_file():
command_lines_by_doc.append((path, ()))
errors.append(f"{_display_path(path)} is missing")
continue
text = path.read_text(encoding="utf-8")
try:
section = _trading_delivery_section(text)
except ValueError as exc:
command_lines_by_doc.append((path, ()))
errors.append(f"{_display_path(path)} {exc}")
continue
command_lines = _trading_delivery_command_lines(section)
command_lines_by_doc.append((path, command_lines))
for snippet in TRADING_DELIVERY_REQUIRED_SNIPPETS:
if snippet not in section:
errors.append(f"{_display_path(path)} missing trading delivery snippet: {snippet}")
if command_lines != TRADING_DELIVERY_COMMANDS:
errors.append(
f"{_display_path(path)} trading delivery commands drifted from the locked checklist shape"
)
return TradingDeliveryReadinessReport(
checked_docs=tuple(docs),
command_lines_by_doc=tuple(command_lines_by_doc),
errors=tuple(errors),
)
def main() -> int:
report = check_trading_delivery_readiness()
if report.errors:
print("Trading delivery readiness check failed")
for error in report.errors:
print(f"- {error}")
return 1
print(
"Trading delivery readiness check passed: "
f"{len(report.checked_docs)} docs, "
f"{len(TRADING_DELIVERY_COMMAND_GROUPS)} phases, "
f"{len(TRADING_DELIVERY_COMMANDS)} locked commands."
)
return 0
def _trading_delivery_section(text: str) -> str:
match = re.search(
rf"^## {re.escape(TRADING_DELIVERY_SECTION_HEADING)}\n(?P<section>.*?)(?:\n## |\Z)",
text,
re.DOTALL | re.MULTILINE,
)
if match is None:
raise ValueError(f"is missing the {TRADING_DELIVERY_SECTION_HEADING} section")
return match.group("section")
def _trading_delivery_command_lines(text: str) -> tuple[str, ...]:
return tuple(
line.strip()
for line in text.splitlines()
if line.strip().startswith(TRADING_DELIVERY_COMMAND_PREFIXES)
)
def _display_path(path: Path) -> str:
try:
return str(path.relative_to(ROOT))
except ValueError:
return str(path)
if __name__ == "__main__":
raise SystemExit(main())
+116 -17
View File
@@ -14,10 +14,10 @@ if str(ROOT) not in sys.path:
from src.db.engine import engine
from src.services.maintenance.historical_compatibility import (
build_historical_compatibility_summary,
classify_liquidity_duplicate_groups,
plan_fvg_meta_repairs,
plan_mss_meta_repairs,
plan_trade_signal_invalidation_repairs,
summarize_liquidity_duplicate_groups,
)
@@ -26,23 +26,25 @@ def run_historical_compatibility_repair(
apply_signal_invalidations: bool = False,
apply_mss_meta: bool = False,
apply_fvg_meta: bool = False,
apply_liquidity_safe_merges: bool = False,
) -> dict:
dry_run = not any((apply_signal_invalidations, apply_mss_meta, apply_fvg_meta))
dry_run = not any((apply_signal_invalidations, apply_mss_meta, apply_fvg_meta, apply_liquidity_safe_merges))
with engine.begin() as connection:
signal_rows = _fetch_trade_signal_rows(connection)
mss_rows = _fetch_mss_rows(connection)
fvg_rows = _fetch_fvg_rows(connection)
liquidity_duplicate_rows = _fetch_liquidity_duplicate_groups(connection)
liquidity_duplicate_rows = _fetch_liquidity_duplicate_rows(connection)
signal_repairs = plan_trade_signal_invalidation_repairs(signal_rows)
mss_repairs = plan_mss_meta_repairs(mss_rows)
fvg_repairs = plan_fvg_meta_repairs(fvg_rows)
liquidity_duplicates = summarize_liquidity_duplicate_groups(liquidity_duplicate_rows)
liquidity_duplicates = classify_liquidity_duplicate_groups(liquidity_duplicate_rows)
applied_repairs = {
"signal_invalidations": 0,
"mss_meta": 0,
"fvg_meta": 0,
"liquidity_safe_merges": 0,
}
if apply_signal_invalidations:
@@ -57,6 +59,8 @@ def run_historical_compatibility_repair(
for repair in fvg_repairs:
_update_fvg_meta(connection, repair)
applied_repairs["fvg_meta"] = len(fvg_repairs)
if apply_liquidity_safe_merges:
applied_repairs["liquidity_safe_merges"] = _apply_liquidity_safe_merges(connection, liquidity_duplicates)
return build_historical_compatibility_summary(
signal_repairs=signal_repairs,
@@ -102,27 +106,96 @@ def _fetch_fvg_rows(connection) -> list[dict]:
return [dict(row) for row in connection.execute(sql).mappings().all()]
def _fetch_liquidity_duplicate_groups(connection) -> list[dict]:
def _fetch_liquidity_duplicate_rows(connection) -> list[dict]:
sql = text(
"""
with dupes as (
select instrument_id, timeframe, side, price_low, price_high
from liquidity_pools
where status = 'untouched'
group by instrument_id, timeframe, side, price_low, price_high
having count(*) > 1
)
select
instrument_id,
timeframe,
side,
price_low,
price_high,
count(*) as duplicate_count,
array_agg(id order by created_ts desc, id desc) as ids
from liquidity_pools
where status = 'untouched'
group by instrument_id, timeframe, side, price_low, price_high
having count(*) > 1
order by count(*) desc, instrument_id asc, timeframe asc, side asc
lp.instrument_id,
lp.timeframe,
lp.side,
lp.price_low,
lp.price_high,
lp.id,
lp.pool_type,
lp.reference_ids,
lp.created_ts,
lp.meta,
coalesce(
json_agg(
json_build_object(
'id', se.id,
'sweep_ts', se.sweep_ts,
'side_swept', se.side_swept,
'sweep_high', se.sweep_high,
'sweep_low', se.sweep_low,
'close_back_inside', se.close_back_inside,
'validation_level', se.validation_level,
'status', se.status,
'meta', se.meta
)
order by se.sweep_ts, se.id
) filter (where se.id is not null),
'[]'::json
) as sweep_events,
coalesce(array_agg(se.sweep_ts order by se.sweep_ts) filter (where se.id is not null), '{}') as sweep_ts_list,
count(se.id) as sweep_count
from liquidity_pools lp
left join sweep_events se on se.pool_id = lp.id
join dupes d
on d.instrument_id = lp.instrument_id
and d.timeframe = lp.timeframe
and d.side = lp.side
and d.price_low = lp.price_low
and d.price_high = lp.price_high
where lp.status = 'untouched'
group by
lp.instrument_id,
lp.timeframe,
lp.side,
lp.price_low,
lp.price_high,
lp.id,
lp.pool_type,
lp.reference_ids,
lp.created_ts,
lp.meta
order by
lp.instrument_id asc,
lp.timeframe asc,
lp.side asc,
lp.price_low asc,
lp.created_ts desc,
lp.id desc
"""
)
return [dict(row) for row in connection.execute(sql).mappings().all()]
def _apply_liquidity_safe_merges(connection, liquidity_duplicates: dict) -> int:
applied = 0
for group in liquidity_duplicates.get("groups") or []:
classification = group.get("classification")
if classification not in {"safe_auto_merge_unreferenced_superset", "safe_exact_duplicate_with_duplicate_sweeps"}:
continue
remove_pool_ids = list(group.get("remove_pool_ids") or [])
if not remove_pool_ids:
continue
if classification == "safe_exact_duplicate_with_duplicate_sweeps":
for pool_id in remove_pool_ids:
_delete_sweep_events_for_pool(connection, pool_id)
for pool_id in remove_pool_ids:
_delete_liquidity_pool(connection, pool_id)
applied += 1
return applied
def _update_trade_signal_invalidations(connection, repair: dict) -> None:
sql = text(
"""
@@ -174,6 +247,27 @@ def _update_fvg_meta(connection, repair: dict) -> None:
)
def _delete_sweep_events_for_pool(connection, pool_id: int) -> None:
sql = text(
"""
delete from sweep_events
where pool_id = :pool_id
"""
)
connection.execute(sql, {"pool_id": int(pool_id)})
def _delete_liquidity_pool(connection, pool_id: int) -> None:
sql = text(
"""
delete from liquidity_pools
where id = :pool_id
and status = 'untouched'
"""
)
connection.execute(sql, {"pool_id": int(pool_id)})
def main(argv=None, env=None) -> int:
env = os.environ if env is None else env
parser = _build_parser()
@@ -182,12 +276,14 @@ def main(argv=None, env=None) -> int:
apply_signal_invalidations = bool(args.apply_signal_invalidations or apply_safe_repairs)
apply_mss_meta = bool(args.apply_mss_meta or apply_safe_repairs)
apply_fvg_meta = bool(args.apply_fvg_meta or apply_safe_repairs)
apply_liquidity_safe_merges = bool(args.apply_liquidity_safe_merges or apply_safe_repairs)
try:
summary = run_historical_compatibility_repair(
apply_signal_invalidations=apply_signal_invalidations,
apply_mss_meta=apply_mss_meta,
apply_fvg_meta=apply_fvg_meta,
apply_liquidity_safe_merges=apply_liquidity_safe_merges,
)
except SQLAlchemyError as exc:
summary = {
@@ -212,6 +308,7 @@ def _build_parser() -> argparse.ArgumentParser:
parser.add_argument("--apply-signal-invalidations", action="store_true", help="Apply only trade_signals invalidation normalization.")
parser.add_argument("--apply-mss-meta", action="store_true", help="Apply only MSS meta backfill for state/status_reason.")
parser.add_argument("--apply-fvg-meta", action="store_true", help="Apply only FVG meta backfill for status_reason/default compatibility fields.")
parser.add_argument("--apply-liquidity-safe-merges", action="store_true", help="Apply only liquidity safe merges for unreferenced superset rows and exact duplicate rows with identical sweep events.")
return parser
@@ -229,6 +326,8 @@ def _print_summary(summary: dict, json_output: bool) -> None:
print(f"MSS meta repairs: {candidate_repairs.get('mss_meta', 0)}")
print(f"FVG meta repairs: {candidate_repairs.get('fvg_meta', 0)}")
print(f"Liquidity duplicate groups: {candidate_repairs.get('liquidity_duplicate_groups', 0)}")
print(f"Liquidity safe auto-merge groups: {candidate_repairs.get('liquidity_safe_auto_merge_groups', 0)}")
print(f"Liquidity safe duplicate-sweep merge groups: {candidate_repairs.get('liquidity_safe_duplicate_sweep_merge_groups', 0)}")
def _get_bool(env, key: str, default: bool) -> bool:
+1
View File
@@ -13,6 +13,7 @@ DEFAULT_CHECKS = [
("training_case_package", [sys.executable, "scripts/run_training_case_package.py"]),
("migration_static_checks", [sys.executable, "scripts/run_migration_static_checks.py"]),
("script_static_checks", [sys.executable, "scripts/run_script_static_checks.py"]),
("trading_delivery_readiness", [sys.executable, "scripts/check_trading_delivery_readiness.py"]),
("dashboard_js_syntax", ["node", "--check", "src/web/app.js"]),
]
+2
View File
@@ -1,5 +1,6 @@
from src.services.maintenance.historical_compatibility import (
build_historical_compatibility_summary,
classify_liquidity_duplicate_groups,
derive_historical_fvg_meta,
derive_historical_mss_meta,
plan_fvg_meta_repairs,
@@ -10,6 +11,7 @@ from src.services.maintenance.historical_compatibility import (
__all__ = [
"build_historical_compatibility_summary",
"classify_liquidity_duplicate_groups",
"derive_historical_fvg_meta",
"derive_historical_mss_meta",
"plan_fvg_meta_repairs",
@@ -1,5 +1,6 @@
from __future__ import annotations
import json
from typing import Optional
from src.services.signals import normalize_signal_invalidations
@@ -107,6 +108,174 @@ def summarize_liquidity_duplicate_groups(rows: list[dict]) -> dict:
}
def classify_liquidity_duplicate_groups(rows: list[dict]) -> dict:
grouped_rows: dict[tuple, list[dict]] = {}
for row in rows:
key = (
row.get("instrument_id"),
row.get("timeframe"),
row.get("side"),
str(row.get("price_low")),
str(row.get("price_high")),
)
grouped_rows.setdefault(key, []).append(_normalize_liquidity_duplicate_row(row))
groups = []
class_counts = {
"safe_auto_merge_unreferenced_superset": 0,
"safe_exact_duplicate_with_duplicate_sweeps": 0,
"needs_sweep_reference_migration": 0,
"keep_version_history": 0,
}
row_count = 0
for key, items in grouped_rows.items():
items.sort(key=lambda row: (row["created_ts"], row["id"]))
classification = _classify_liquidity_group(items)
groups.append(
{
"instrument_id": key[0],
"timeframe": key[1],
"side": key[2],
"price_low": key[3],
"price_high": key[4],
"duplicate_count": len(items),
**classification,
}
)
class_counts[classification["classification"]] += 1
row_count += len(items)
groups.sort(key=lambda row: (-int(row["duplicate_count"]), row["instrument_id"], row["timeframe"], row["side"], row["price_low"]))
return {
"group_count": len(groups),
"row_count": row_count,
"classification_counts": class_counts,
"groups": groups,
}
def _normalize_liquidity_duplicate_row(row: dict) -> dict:
return {
"id": int(row["id"]),
"pool_type": row.get("pool_type"),
"reference_ids": list(row.get("reference_ids") or []),
"created_ts": str(row.get("created_ts")),
"meta": dict(row.get("meta") or {}),
"sweep_ts_list": [str(value) for value in (row.get("sweep_ts_list") or [])],
"sweep_events": [_normalize_sweep_event_payload(item) for item in (row.get("sweep_events") or [])],
"sweep_count": int(row.get("sweep_count") or 0),
}
def _classify_liquidity_group(items: list[dict]) -> dict:
if _is_unreferenced_superset_pair(items):
canonical = max(items, key=lambda row: (len(row["reference_ids"]), row["id"]))
remove = min(items, key=lambda row: (len(row["reference_ids"]), row["id"]))
return {
"classification": "safe_auto_merge_unreferenced_superset",
"reason": "unreferenced_same_type_superset_chain",
"canonical_pool_id": canonical["id"],
"remove_pool_ids": [remove["id"]],
"rows": items,
}
exact_duplicate_buckets = _exact_duplicate_buckets(items)
if exact_duplicate_buckets and len(exact_duplicate_buckets) == 1 and len(exact_duplicate_buckets[0]) == len(items):
bucket = exact_duplicate_buckets[0]
canonical = max(bucket, key=lambda row: row["id"])
remove_ids = sorted(row["id"] for row in bucket if row["id"] != canonical["id"])
if _rows_have_identical_sweep_event_signatures(bucket):
return {
"classification": "safe_exact_duplicate_with_duplicate_sweeps",
"reason": "exact_duplicate_pool_rows_with_identical_sweep_events",
"canonical_pool_id": canonical["id"],
"remove_pool_ids": remove_ids,
"rows": items,
}
return {
"classification": "needs_sweep_reference_migration",
"reason": "exact_duplicate_pool_rows_with_nonidentical_sweep_events",
"canonical_pool_id": canonical["id"],
"remove_pool_ids": remove_ids,
"rows": items,
}
return {
"classification": "keep_version_history",
"reason": "mixed_structure_versions_or_reference_collisions",
"canonical_pool_id": None,
"remove_pool_ids": [],
"rows": items,
}
def _is_unreferenced_superset_pair(items: list[dict]) -> bool:
if len(items) != 2:
return False
left, right = items
if left["pool_type"] != right["pool_type"]:
return False
if left["sweep_count"] != 0 or right["sweep_count"] != 0:
return False
left_refs = set(left["reference_ids"])
right_refs = set(right["reference_ids"])
return left_refs < right_refs or right_refs < left_refs
def _exact_duplicate_buckets(items: list[dict]) -> list[list[dict]]:
buckets: dict[tuple, list[dict]] = {}
for row in items:
key = (
row["pool_type"],
tuple(row["reference_ids"]),
row["created_ts"],
json.dumps(row["meta"], sort_keys=True, default=str),
)
buckets.setdefault(key, []).append(row)
return [bucket for bucket in buckets.values() if len(bucket) > 1]
def _normalize_sweep_event_payload(item: dict) -> dict:
return {
"id": int(item.get("id") or 0),
"sweep_ts": str(item.get("sweep_ts")),
"side_swept": item.get("side_swept"),
"sweep_high": str(item.get("sweep_high")),
"sweep_low": str(item.get("sweep_low")),
"close_back_inside": bool(item.get("close_back_inside")),
"validation_level": item.get("validation_level"),
"status": item.get("status"),
"meta": dict(item.get("meta") or {}),
}
def _rows_have_identical_sweep_event_signatures(rows: list[dict]) -> bool:
if not rows:
return False
signatures = [_sweep_event_signatures(row) for row in rows]
return all(signature == signatures[0] for signature in signatures[1:])
def _sweep_event_signatures(row: dict) -> tuple:
events = list(row.get("sweep_events") or [])
normalized = []
for event in events:
normalized.append(
(
event.get("sweep_ts"),
event.get("side_swept"),
event.get("sweep_high"),
event.get("sweep_low"),
event.get("close_back_inside"),
event.get("validation_level"),
event.get("status"),
json.dumps(event.get("meta") or {}, sort_keys=True, default=str),
)
)
return tuple(normalized)
def build_historical_compatibility_summary(
*,
signal_repairs: list[dict],
@@ -126,11 +295,16 @@ def build_historical_compatibility_summary(
"fvg_meta": len(fvg_repairs),
"liquidity_duplicate_groups": int(liquidity_duplicates.get("group_count") or 0),
"liquidity_duplicate_rows": int(liquidity_duplicates.get("row_count") or 0),
"liquidity_safe_auto_merge_groups": int((liquidity_duplicates.get("classification_counts") or {}).get("safe_auto_merge_unreferenced_superset", 0)),
"liquidity_safe_duplicate_sweep_merge_groups": int((liquidity_duplicates.get("classification_counts") or {}).get("safe_exact_duplicate_with_duplicate_sweeps", 0)),
"liquidity_reference_migration_groups": int((liquidity_duplicates.get("classification_counts") or {}).get("needs_sweep_reference_migration", 0)),
"liquidity_keep_history_groups": int((liquidity_duplicates.get("classification_counts") or {}).get("keep_version_history", 0)),
},
"applied_repairs": {
"signal_invalidations": int(applied_repairs.get("signal_invalidations") or 0),
"mss_meta": int(applied_repairs.get("mss_meta") or 0),
"fvg_meta": int(applied_repairs.get("fvg_meta") or 0),
"liquidity_safe_merges": int(applied_repairs.get("liquidity_safe_merges") or 0),
},
"manual_review": {
"liquidity_duplicates": liquidity_duplicates.get("groups") or [],
@@ -139,5 +313,6 @@ def build_historical_compatibility_summary(
"signal_invalidations",
"mss_meta",
"fvg_meta",
"liquidity_safe_merges",
],
}
@@ -7,6 +7,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PROJECT_README = ROOT / "README.md"
PROJECT_RUNBOOK = ROOT / "RUNBOOK.md"
TRAINING_PRODUCT_DOC = ROOT / "docs" / "training_case_engine_m1_m6_product_readme.md"
TRAINING_PACKAGE_SMOKE_DOC = ROOT / "docs" / "examples" / "training_case_package_smoke_m6.md"
TRAINING_DOCS_CLI_TEST = ROOT / "tests" / "unit" / "test_training_case_docs_cli_m10.py"
@@ -456,6 +457,98 @@ LOCAL_DOCKER_POLICY_FORBIDDEN_ENCOURAGEMENT = (
"start an extra ai-ict container",
"duplicate ai-ict containers",
)
TRADING_DELIVERY_DOCS = (PROJECT_README, PROJECT_RUNBOOK)
TRADING_DELIVERY_SECTION_HEADING = "Trading Delivery Checklist"
TRADING_DELIVERY_DOCUMENTATION_GUARD_TEST_INVENTORY = (
"test_docs_describe_trading_delivery_checklist",
"test_docs_lock_trading_delivery_command_shape",
)
TRADING_DELIVERY_DOCUMENTATION_GUARD_SOURCE_CONSTANTS = (
"TRADING_DELIVERY_DOCS",
"TRADING_DELIVERY_COMMAND_GROUPS",
"TRADING_DELIVERY_COMMANDS",
)
TRADING_DELIVERY_COMPOSE_PREFIX = (
"docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml"
)
TRADING_DELIVERY_COMMAND_GROUPS = (
(
"pre_deploy_offline",
(
'.venv/bin/python -m unittest discover -s tests/unit -p "test_*.py"',
".venv/bin/python scripts/run_offline_checks.py",
".venv/bin/python scripts/check_local_demo_readiness.py",
".venv/bin/python scripts/check_trading_delivery_readiness.py",
),
),
(
"database_gate",
(
"READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py",
"HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py",
".venv/bin/python scripts/run_historical_compatibility_repair.py --apply-safe-repairs",
"HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py",
"PIPELINE_OUTPUT_JSON=1 BACKTEST_REQUIRE_QUALITY_GATE=1 .venv/bin/python scripts/run_pipeline.py",
),
),
(
"deploy_and_health",
(
f"{TRADING_DELIVERY_COMPOSE_PREFIX} down --remove-orphans",
f"{TRADING_DELIVERY_COMPOSE_PREFIX} up -d --build",
f"{TRADING_DELIVERY_COMPOSE_PREFIX} ps",
'test "$(docker ps --filter label=com.docker.compose.project=ai-ict --format \'{{.Names}}\' | wc -l | tr -d \' \')" = "1"',
'test "$(docker ps --filter name=^/ai-ict$ --format \'{{.Names}}\' | wc -l | tr -d \' \')" = "1"',
'test "$(docker inspect -f \'{{.State.Health.Status}}\' ai-ict)" = "healthy"',
"curl -fsS http://127.0.0.1:8000/health",
'curl -fsS "http://127.0.0.1:8000/signals?instrument_id=1&limit=1"',
),
),
(
"post_deploy_observe",
(
"READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py",
"REPORTS_OUTPUT_JSON=1 .venv/bin/python scripts/run_reports.py",
),
),
(
"rollback_boundary",
(
"git rev-parse HEAD",
f"{TRADING_DELIVERY_COMPOSE_PREFIX} logs --tail=100 ai-ict",
"git worktree add /tmp/ai-ict-rollback <last-known-good-sha>",
"cd /tmp/ai-ict-rollback && ./scripts/deploy_server.exp",
"rm -rf /tmp/ai-ict-rollback",
),
),
)
TRADING_DELIVERY_COMMANDS = tuple(
command
for _group_name, commands in TRADING_DELIVERY_COMMAND_GROUPS
for command in commands
)
TRADING_DELIVERY_REQUIRED_SNIPPETS = (
"must-run before every market validation or deploy",
"only when the dry-run output shows safe candidates",
"manual review is still required for `needs_sweep_reference_migration` and `keep_version_history`",
"paper execution boundary",
"Rollback trigger",
"last-known-good SHA",
"without relying on chat memory",
)
TRADING_DELIVERY_COMMAND_PREFIXES = (
".venv/bin/python",
"READINESS_OUTPUT_JSON=1",
"HISTORICAL_REPAIR_OUTPUT_JSON=1",
"PIPELINE_OUTPUT_JSON=1",
"REPORTS_OUTPUT_JSON=1",
"docker compose",
"test ",
"curl ",
"git ",
"cd /tmp/ai-ict-rollback &&",
"rm -rf /tmp/ai-ict-rollback",
)
TRAINING_PUBLIC_PRIVACY_EXCLUSION_WORDS = (
"scrub",
"omit",
@@ -608,6 +701,26 @@ def _local_docker_command_lines(text: str) -> tuple[str, ...]:
)
def _trading_delivery_section(path: Path) -> str:
text = path.read_text(encoding="utf-8")
match = re.search(
rf"^## {re.escape(TRADING_DELIVERY_SECTION_HEADING)}\n(?P<section>.*?)(?:\n## |\Z)",
text,
re.DOTALL | re.MULTILINE,
)
if match is None:
raise AssertionError(f"{path} is missing the {TRADING_DELIVERY_SECTION_HEADING} section")
return match.group("section")
def _trading_delivery_command_lines(text: str) -> tuple[str, ...]:
return tuple(
line.strip()
for line in text.splitlines()
if line.strip().startswith(TRADING_DELIVERY_COMMAND_PREFIXES)
)
def _training_public_privacy_lines(text: str) -> tuple[str, ...]:
lines = []
for line in text.splitlines():
@@ -4395,6 +4508,53 @@ TRAINING_M41_RELEASE_INDEX_REQUIRED_SNIPPET_INVENTORY = (
self.assertIn(constant_name, top_level_names)
self.assertIn(f"{constant_name} =", module_source)
def test_trading_delivery_documentation_guard_inventory_is_current(self) -> None:
module_path = Path(__file__)
module_source = module_path.read_text(encoding="utf-8")
module_tree = ast.parse(module_source)
class_nodes = [
node
for node in module_tree.body
if isinstance(node, ast.ClassDef) and node.name == "DocumentationConsistencyTests"
]
top_level_names = {
target.id
for node in module_tree.body
if isinstance(node, ast.Assign)
for target in node.targets
if isinstance(target, ast.Name)
}
required_guard_tests = {
"test_docs_describe_trading_delivery_checklist",
"test_docs_lock_trading_delivery_command_shape",
}
inventory_names = tuple(TRADING_DELIVERY_DOCUMENTATION_GUARD_TEST_INVENTORY)
self.assertEqual(len(class_nodes), 1)
self.assertEqual(len(inventory_names), len(set(inventory_names)))
self.assertLessEqual(required_guard_tests, set(inventory_names))
test_names = {
item.name
for item in class_nodes[0].body
if isinstance(item, ast.FunctionDef) and item.name.startswith("test_")
}
for name in inventory_names:
with self.subTest(name=name):
loader = unittest.TestLoader()
suite = loader.loadTestsFromName(
f"{type(self).__module__}.DocumentationConsistencyTests.{name}"
)
self.assertIn(name, test_names)
self.assertEqual(loader.errors, [])
self.assertGreater(suite.countTestCases(), 0)
for constant_name in TRADING_DELIVERY_DOCUMENTATION_GUARD_SOURCE_CONSTANTS:
with self.subTest(constant_name=constant_name):
self.assertIn(constant_name, top_level_names)
self.assertIn(f"{constant_name} =", module_source)
def test_training_case_acceptance_manifest_modules_are_discoverable(self) -> None:
manifest_modules = tuple(TRAINING_CASE_ACCEPTANCE_TEST_MANIFEST)
missing_modules = [
@@ -4501,6 +4661,33 @@ TRAINING_M41_RELEASE_INDEX_REQUIRED_SNIPPET_INVENTORY = (
self.assertNotIn("docker compose -p ai-ict down --remove-orphans", command_lines)
self.assertNotIn("docker compose -p ai-ict up -d --build", command_lines)
def test_docs_describe_trading_delivery_checklist(self) -> None:
for path in TRADING_DELIVERY_DOCS:
section = _trading_delivery_section(path)
with self.subTest(path=path.name):
for snippet in TRADING_DELIVERY_REQUIRED_SNIPPETS:
self.assertIn(snippet, section)
self.assertLess(
section.index("must-run before every market validation or deploy"),
section.index("Rollback trigger"),
)
self.assertIn("backtest_quality_gate.passed", section)
def test_docs_lock_trading_delivery_command_shape(self) -> None:
command_lines_by_path = {
path: _trading_delivery_command_lines(_trading_delivery_section(path))
for path in TRADING_DELIVERY_DOCS
}
self.assertEqual(
command_lines_by_path[PROJECT_README],
command_lines_by_path[PROJECT_RUNBOOK],
)
for path, command_lines in command_lines_by_path.items():
with self.subTest(path=path.name):
self.assertEqual(command_lines, TRADING_DELIVERY_COMMANDS)
def test_training_case_product_doc_names_public_response_privacy_contracts(self) -> None:
text = TRAINING_PRODUCT_DOC.read_text(encoding="utf-8")
@@ -35,7 +35,7 @@ class _ConnectionStub:
return _MappingsResult([{"id": 2, "accepted": True, "meta": {}}])
if "from fvg_zones" in sql_text and "select" in sql_text:
return _MappingsResult([{"id": 3, "status": "open", "meta": {}}])
if "from liquidity_pools" in sql_text:
if "select" in sql_text and "from liquidity_pools" in sql_text:
return _MappingsResult(
[
{
@@ -44,8 +44,29 @@ class _ConnectionStub:
"side": "buy_side",
"price_low": 100.0,
"price_high": 100.0,
"duplicate_count": 2,
"ids": [10, 9],
"id": 9,
"pool_type": "equal_highs",
"reference_ids": [1, 2],
"created_ts": "2026-04-29T10:36:00+00:00",
"meta": {"merged_reference_count": 2},
"sweep_events": [],
"sweep_ts_list": [],
"sweep_count": 0,
},
{
"instrument_id": 1,
"timeframe": "1m",
"side": "buy_side",
"price_low": 100.0,
"price_high": 100.0,
"id": 10,
"pool_type": "equal_highs",
"reference_ids": [1, 2, 3],
"created_ts": "2026-04-29T10:36:00+00:00",
"meta": {"merged_reference_count": 3},
"sweep_events": [],
"sweep_ts_list": [],
"sweep_count": 0,
}
]
)
@@ -77,6 +98,8 @@ class HistoricalCompatibilityRepairScriptTests(unittest.TestCase):
self.assertEqual(summary["candidate_repairs"]["mss_meta"], 1)
self.assertEqual(summary["candidate_repairs"]["fvg_meta"], 1)
self.assertEqual(summary["candidate_repairs"]["liquidity_duplicate_groups"], 1)
self.assertEqual(summary["candidate_repairs"]["liquidity_safe_auto_merge_groups"], 1)
self.assertEqual(summary["candidate_repairs"]["liquidity_safe_duplicate_sweep_merge_groups"], 0)
self.assertEqual(connection.updates, [])
def test_apply_safe_repairs_updates_supported_categories(self) -> None:
@@ -87,13 +110,28 @@ class HistoricalCompatibilityRepairScriptTests(unittest.TestCase):
apply_signal_invalidations=True,
apply_mss_meta=True,
apply_fvg_meta=True,
apply_liquidity_safe_merges=True,
)
self.assertFalse(summary["dry_run"])
self.assertEqual(summary["applied_repairs"]["signal_invalidations"], 1)
self.assertEqual(summary["applied_repairs"]["mss_meta"], 1)
self.assertEqual(summary["applied_repairs"]["fvg_meta"], 1)
self.assertEqual(len(connection.updates), 3)
self.assertEqual(summary["applied_repairs"]["liquidity_safe_merges"], 1)
self.assertEqual(len(connection.updates), 4)
def test_apply_liquidity_safe_merges_can_run_independently(self) -> None:
connection = _ConnectionStub()
with patch("scripts.run_historical_compatibility_repair.engine.begin", return_value=_BeginStub(connection)):
summary = run_historical_compatibility_repair.run_historical_compatibility_repair(
apply_liquidity_safe_merges=True,
)
self.assertFalse(summary["dry_run"])
self.assertEqual(summary["applied_repairs"]["signal_invalidations"], 0)
self.assertEqual(summary["applied_repairs"]["liquidity_safe_merges"], 1)
self.assertEqual(len(connection.updates), 1)
def test_main_reports_database_unavailable_json(self) -> None:
output = io.StringIO()
@@ -5,6 +5,7 @@ os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:
from src.services.maintenance import (
build_historical_compatibility_summary,
classify_liquidity_duplicate_groups,
derive_historical_fvg_meta,
derive_historical_mss_meta,
plan_fvg_meta_repairs,
@@ -63,12 +64,223 @@ class HistoricalCompatibilityServiceTests(unittest.TestCase):
self.assertEqual(summary["row_count"], 2)
self.assertEqual(summary["groups"][0]["ids"], [10, 9])
def test_classifies_unreferenced_superset_pair_as_safe_auto_merge(self) -> None:
classified = classify_liquidity_duplicate_groups(
[
{
"instrument_id": 1,
"timeframe": "1m",
"side": "buy_side",
"price_low": 100.0,
"price_high": 100.0,
"id": 9,
"pool_type": "equal_highs",
"reference_ids": [1, 2],
"created_ts": "2026-04-29T10:36:00+00:00",
"meta": {"merged_reference_count": 2},
"sweep_ts_list": [],
"sweep_count": 0,
},
{
"instrument_id": 1,
"timeframe": "1m",
"side": "buy_side",
"price_low": 100.0,
"price_high": 100.0,
"id": 10,
"pool_type": "equal_highs",
"reference_ids": [1, 2, 3],
"created_ts": "2026-04-29T10:36:00+00:00",
"meta": {"merged_reference_count": 3},
"sweep_ts_list": [],
"sweep_count": 0,
},
]
)
self.assertEqual(classified["classification_counts"]["safe_auto_merge_unreferenced_superset"], 1)
self.assertEqual(classified["groups"][0]["classification"], "safe_auto_merge_unreferenced_superset")
self.assertEqual(classified["groups"][0]["canonical_pool_id"], 10)
self.assertEqual(classified["groups"][0]["remove_pool_ids"], [9])
def test_classifies_exact_duplicate_referenced_pair_as_reference_migration(self) -> None:
classified = classify_liquidity_duplicate_groups(
[
{
"instrument_id": 1,
"timeframe": "1m",
"side": "sell_side",
"price_low": 100.0,
"price_high": 100.0,
"id": 11,
"pool_type": "prior_low",
"reference_ids": [7],
"created_ts": "2026-04-23T05:24:00+00:00",
"meta": {},
"sweep_events": [
{
"id": 100,
"sweep_ts": "2026-04-23T05:24:00+00:00",
"side_swept": "sell_side",
"sweep_high": "101.0",
"sweep_low": "99.0",
"close_back_inside": False,
"validation_level": "weak",
"status": "active",
"meta": {"pool_type": "prior_low"},
}
],
"sweep_ts_list": ["2026-04-23T05:24:00+00:00"],
"sweep_count": 1,
},
{
"instrument_id": 1,
"timeframe": "1m",
"side": "sell_side",
"price_low": 100.0,
"price_high": 100.0,
"id": 12,
"pool_type": "prior_low",
"reference_ids": [7],
"created_ts": "2026-04-23T05:24:00+00:00",
"meta": {},
"sweep_events": [
{
"id": 101,
"sweep_ts": "2026-04-23T05:24:00+00:00",
"side_swept": "sell_side",
"sweep_high": "101.0",
"sweep_low": "99.0",
"close_back_inside": False,
"validation_level": "weak",
"status": "active",
"meta": {"pool_type": "prior_low"},
}
],
"sweep_ts_list": ["2026-04-23T05:24:00+00:00"],
"sweep_count": 1,
},
]
)
self.assertEqual(classified["classification_counts"]["safe_exact_duplicate_with_duplicate_sweeps"], 1)
self.assertEqual(classified["groups"][0]["classification"], "safe_exact_duplicate_with_duplicate_sweeps")
def test_classifies_nonidentical_referenced_pair_as_reference_migration(self) -> None:
classified = classify_liquidity_duplicate_groups(
[
{
"instrument_id": 1,
"timeframe": "1m",
"side": "sell_side",
"price_low": 100.0,
"price_high": 100.0,
"id": 21,
"pool_type": "prior_low",
"reference_ids": [9],
"created_ts": "2026-04-23T05:24:00+00:00",
"meta": {},
"sweep_events": [
{
"id": 200,
"sweep_ts": "2026-04-23T05:24:00+00:00",
"side_swept": "sell_side",
"sweep_high": "101.0",
"sweep_low": "99.0",
"close_back_inside": False,
"validation_level": "weak",
"status": "active",
"meta": {"pool_type": "prior_low"},
}
],
"sweep_ts_list": ["2026-04-23T05:24:00+00:00"],
"sweep_count": 1,
},
{
"instrument_id": 1,
"timeframe": "1m",
"side": "sell_side",
"price_low": 100.0,
"price_high": 100.0,
"id": 22,
"pool_type": "prior_low",
"reference_ids": [9],
"created_ts": "2026-04-23T05:24:00+00:00",
"meta": {},
"sweep_events": [
{
"id": 201,
"sweep_ts": "2026-04-23T05:24:00+00:00",
"side_swept": "sell_side",
"sweep_high": "102.0",
"sweep_low": "98.0",
"close_back_inside": False,
"validation_level": "weak",
"status": "active",
"meta": {"pool_type": "prior_low"},
}
],
"sweep_ts_list": ["2026-04-23T05:24:00+00:00"],
"sweep_count": 1,
},
]
)
self.assertEqual(classified["classification_counts"]["needs_sweep_reference_migration"], 1)
self.assertEqual(classified["groups"][0]["classification"], "needs_sweep_reference_migration")
def test_classifies_mixed_version_group_as_keep_history(self) -> None:
classified = classify_liquidity_duplicate_groups(
[
{
"instrument_id": 1,
"timeframe": "1m",
"side": "buy_side",
"price_low": 100.0,
"price_high": 100.0,
"id": 13,
"pool_type": "prior_high",
"reference_ids": [1],
"created_ts": "2026-04-23T05:24:00+00:00",
"meta": {},
"sweep_ts_list": ["2026-04-23T05:24:00+00:00"],
"sweep_count": 1,
},
{
"instrument_id": 1,
"timeframe": "1m",
"side": "buy_side",
"price_low": 100.0,
"price_high": 100.0,
"id": 14,
"pool_type": "equal_highs",
"reference_ids": [1, 2],
"created_ts": "2026-04-23T05:24:00+00:00",
"meta": {"merged_reference_count": 2},
"sweep_ts_list": ["2026-04-23T05:24:00+00:00"],
"sweep_count": 1,
},
]
)
self.assertEqual(classified["classification_counts"]["keep_version_history"], 1)
self.assertEqual(classified["groups"][0]["classification"], "keep_version_history")
def test_builds_historical_compatibility_summary(self) -> None:
summary = build_historical_compatibility_summary(
signal_repairs=[{"id": 1}],
mss_repairs=[{"id": 2}],
fvg_repairs=[],
liquidity_duplicates={"group_count": 1, "row_count": 3, "groups": [{"ids": [9, 8, 7]}]},
liquidity_duplicates={
"group_count": 1,
"row_count": 3,
"classification_counts": {
"safe_auto_merge_unreferenced_superset": 1,
"needs_sweep_reference_migration": 0,
"keep_version_history": 0,
},
"groups": [{"ids": [9, 8, 7]}],
},
applied_repairs={"signal_invalidations": 1, "mss_meta": 1, "fvg_meta": 0},
dry_run=False,
)
@@ -76,6 +288,7 @@ class HistoricalCompatibilityServiceTests(unittest.TestCase):
self.assertTrue(summary["passed"])
self.assertFalse(summary["dry_run"])
self.assertEqual(summary["candidate_repairs"]["liquidity_duplicate_rows"], 3)
self.assertEqual(summary["candidate_repairs"]["liquidity_safe_auto_merge_groups"], 1)
self.assertEqual(summary["applied_repairs"]["signal_invalidations"], 1)
self.assertEqual(summary["manual_review"]["liquidity_duplicates"][0]["ids"], [9, 8, 7])
@@ -0,0 +1,134 @@
import ast
import subprocess
import sys
import unittest
from pathlib import Path
from scripts import check_trading_delivery_readiness as readiness
from scripts import run_offline_checks
ROOT = Path(__file__).resolve().parents[2]
CHECK_COMMAND = ".venv/bin/python scripts/check_trading_delivery_readiness.py"
EXPECTED_DOC_PATHS = (
ROOT / "README.md",
ROOT / "RUNBOOK.md",
)
EXPECTED_COMMAND_GROUPS = (
(
"pre_deploy_offline",
(
'.venv/bin/python -m unittest discover -s tests/unit -p "test_*.py"',
".venv/bin/python scripts/run_offline_checks.py",
".venv/bin/python scripts/check_local_demo_readiness.py",
".venv/bin/python scripts/check_trading_delivery_readiness.py",
),
),
(
"database_gate",
(
"READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py",
"HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py",
".venv/bin/python scripts/run_historical_compatibility_repair.py --apply-safe-repairs",
"HISTORICAL_REPAIR_OUTPUT_JSON=1 .venv/bin/python scripts/run_historical_compatibility_repair.py",
"PIPELINE_OUTPUT_JSON=1 BACKTEST_REQUIRE_QUALITY_GATE=1 .venv/bin/python scripts/run_pipeline.py",
),
),
(
"deploy_and_health",
(
"docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml down --remove-orphans",
"docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml up -d --build",
"docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml ps",
'test "$(docker ps --filter label=com.docker.compose.project=ai-ict --format \'{{.Names}}\' | wc -l | tr -d \' \')" = "1"',
'test "$(docker ps --filter name=^/ai-ict$ --format \'{{.Names}}\' | wc -l | tr -d \' \')" = "1"',
'test "$(docker inspect -f \'{{.State.Health.Status}}\' ai-ict)" = "healthy"',
"curl -fsS http://127.0.0.1:8000/health",
'curl -fsS "http://127.0.0.1:8000/signals?instrument_id=1&limit=1"',
),
),
(
"post_deploy_observe",
(
"READINESS_OUTPUT_JSON=1 .venv/bin/python scripts/run_database_readiness.py",
"REPORTS_OUTPUT_JSON=1 .venv/bin/python scripts/run_reports.py",
),
),
(
"rollback_boundary",
(
"git rev-parse HEAD",
"docker compose -p ai-ict -f /root/deploy/ai-ict/docker-compose.yml logs --tail=100 ai-ict",
"git worktree add /tmp/ai-ict-rollback <last-known-good-sha>",
"cd /tmp/ai-ict-rollback && ./scripts/deploy_server.exp",
"rm -rf /tmp/ai-ict-rollback",
),
),
)
EXPECTED_COMMANDS = tuple(
command
for _group_name, commands in EXPECTED_COMMAND_GROUPS
for command in commands
)
class TradingDeliveryReadinessTests(unittest.TestCase):
def test_checker_locks_trading_delivery_constants(self) -> None:
self.assertEqual(readiness.TRADING_DELIVERY_DOCS, EXPECTED_DOC_PATHS)
self.assertEqual(readiness.TRADING_DELIVERY_COMMAND_GROUPS, EXPECTED_COMMAND_GROUPS)
self.assertEqual(readiness.TRADING_DELIVERY_COMMANDS, EXPECTED_COMMANDS)
def test_checker_verifies_documented_trading_delivery_commands(self) -> None:
report = readiness.check_trading_delivery_readiness()
self.assertEqual(report.errors, ())
self.assertEqual(report.checked_docs, EXPECTED_DOC_PATHS)
for path, command_lines in report.command_lines_by_doc:
with self.subTest(path=path.name):
self.assertEqual(command_lines, EXPECTED_COMMANDS)
def test_checker_command_is_documented_and_offline(self) -> None:
for path in EXPECTED_DOC_PATHS:
text = path.read_text(encoding="utf-8")
with self.subTest(path=path.name):
self.assertIn(CHECK_COMMAND, text)
self.assertIn("without relying on chat memory", text)
process = subprocess.run(
[sys.executable, "scripts/check_trading_delivery_readiness.py"],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(process.returncode, 0, process.stderr)
self.assertIn("Trading delivery readiness check passed", process.stdout)
def test_checker_source_has_no_process_launches(self) -> None:
source = Path(readiness.__file__).read_text(encoding="utf-8")
tree = ast.parse(source)
imported_modules = {
alias.name
for node in ast.walk(tree)
if isinstance(node, ast.Import)
for alias in node.names
}
called_attributes = {
node.attr for node in ast.walk(tree) if isinstance(node, ast.Attribute)
}
self.assertNotIn("subprocess", imported_modules)
self.assertNotIn("system", called_attributes)
def test_offline_checks_include_trading_delivery_readiness(self) -> None:
commands = dict(run_offline_checks.build_check_commands(env={}))
self.assertEqual(
commands["trading_delivery_readiness"],
[sys.executable, "scripts/check_trading_delivery_readiness.py"],
)
if __name__ == "__main__":
unittest.main()
@@ -36,6 +36,7 @@ OFFLINE_CHECK_REGISTRY_NAMES = (
OFFLINE_CHECK_NAME,
"migration_static_checks",
"script_static_checks",
"trading_delivery_readiness",
DASHBOARD_SYNTAX_CHECK_NAME,
)
CURRENT_TRAINING_DOC_MATURITY_END = 28