Consolidate docs and harden trading state pipeline
This commit is contained in:
@@ -20,5 +20,9 @@ venv/
|
||||
.claude/
|
||||
memory/
|
||||
|
||||
# Local tool state and transient logs
|
||||
.hermes/
|
||||
geckodriver.log
|
||||
|
||||
# Large artifacts and local docs copies
|
||||
*.pdf
|
||||
|
||||
@@ -143,7 +143,7 @@ test "$(docker inspect -f '{{.State.Health.Status}}' ai-ict)" = "healthy"
|
||||
|
||||
## Runtime utility scripts
|
||||
|
||||
- `python3 scripts/run_dev.py` - run the scaffold entrypoint
|
||||
- `python3 scripts/run_dev.py` - start the local development API server and minimal dashboard
|
||||
- `python3 scripts/show_models.py` - list exported ORM model names
|
||||
- `python3 scripts/check_db.py` - run a simple database connectivity check
|
||||
- `python3 scripts/run_migrations.py` - apply SQL files in `migrations/` order
|
||||
@@ -200,6 +200,12 @@ Optional backtest cost environment variables used by `run_backtest.py` and `run_
|
||||
- `BACKTEST_SLIPPAGE_RATE`
|
||||
- `BACKTEST_FIXED_COST_R`
|
||||
|
||||
Optional replay-backed backtest mode variables used by `run_backtest.py` and `run_pipeline.py`:
|
||||
|
||||
- `BACKTEST_MODE` - supported values: `entry_then_outcome` or `replay_no_lookahead`
|
||||
- `BACKTEST_REPLAY_LEFT_BARS`
|
||||
- `BACKTEST_REPLAY_RIGHT_BARS`
|
||||
|
||||
Optional backtest automation gate used by `run_backtest.py` and `run_pipeline.py`:
|
||||
|
||||
- `BACKTEST_OUTPUT_JSON` - set to `1`, `true`, `yes`, or `on` for machine-readable `run_backtest.py` output
|
||||
|
||||
@@ -29,6 +29,8 @@ python3 scripts/run_dev.py
|
||||
python3 scripts/run_pipeline.py
|
||||
```
|
||||
|
||||
`scripts/run_dev.py` is the persistent local dev API/dashboard entrypoint; `scripts/run_pipeline.py` runs the closed loop once and exits.
|
||||
|
||||
## Test And Validation
|
||||
|
||||
```bash
|
||||
|
||||
+33
-14
@@ -2,7 +2,7 @@
|
||||
|
||||
## Current Goal
|
||||
|
||||
Stabilize the working state of the repository and make the next development step explicit before continuing feature work.
|
||||
Continue downstream consumer hardening after report/API structure-state propagation, with the next focus on dashboard visibility and invalidation-code alignment.
|
||||
|
||||
## Current Branch
|
||||
|
||||
@@ -10,33 +10,52 @@ Stabilize the working state of the repository and make the next development step
|
||||
|
||||
## Last Updated
|
||||
|
||||
2026-05-02 America/New_York
|
||||
2026-05-03 UTC
|
||||
|
||||
## Completed In Last Session
|
||||
|
||||
- Added project-local Codex workflow files for multi-project continuation.
|
||||
- Recorded the current repository state and standard continuation flow.
|
||||
- Propagated normalized signal structure-state fields through the public `/signals` API surface, keeping raw `meta` while exposing stable top-level fields such as `structure_state`, `mss_status_reason`, and `fvg_status_reason`.
|
||||
- Enriched `IntradayReportService` so reports now surface MSS/FVG `state` and `status_reason` instead of relying only on `accepted` and coarse FVG status checks.
|
||||
- Updated intraday invalidation logic to emit richer downstream reasons like `mss_invalidated`, `mss_rejected`, `fvg_mitigated`, and `fvg_invalidated`, while carrying FVG data-quality warnings through the report output.
|
||||
- Enriched `PostTradeReviewService` with a `signal_context` section that exposes `structure_state`, MSS/FVG reasons, and FVG data-quality warnings for downstream review consumers.
|
||||
- Added focused report/API unit coverage for signal-row normalization and richer report-state propagation.
|
||||
- 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`.
|
||||
|
||||
## Current Working State
|
||||
|
||||
- `git status --short` showed: modified `.gitignore`
|
||||
- `git status --short` showed: untracked `.hermes/`
|
||||
- `git status --short` showed: untracked `geckodriver.log`
|
||||
- `git status --short` showed: untracked `uv.lock`
|
||||
- `git status --short --branch` shows `## main...origin/main`; the worktree is still dirty from earlier doc/backtest/liquidity/dev-entrypoint sessions, and `uv.lock` remains untracked.
|
||||
- MSS candidates now carry `meta.state` and `meta.status_reason`; accepted structures use `retest_held`, rejected structures use `retest_not_observed`, and invalidated structures use `old_control_reclaimed`.
|
||||
- MSS persistence now records the latest structure state even when `accepted = false`, so downstream consumers no longer read stale accepted-only MSS rows.
|
||||
- MSS detection still collapses duplicate same-direction displacement context at the same `end_ts` to one deterministic candidate, preferring the highest event id.
|
||||
- Signal-side MSS/FVG fetches now include the latest downstream state regardless of actionability, with deterministic same-timestamp tie-breaks.
|
||||
- `validate_signal_context` now returns explicit downstream structure outcomes plus richer MSS/FVG state reasons instead of a single coarse non-actionable result.
|
||||
- Signal metadata now exposes `structure_state`, `mss_state`, `mss_status_reason`, `fvg_status_reason`, `fvg_touch_count`, and FVG `data_quality_warnings`.
|
||||
- `/signals` API rows now normalize key signal context fields to top-level output keys, so API consumers no longer need to inspect nested `meta` for core structure-state reasons.
|
||||
- Intraday reports now expose structure `state` / `status_reason`, FVG `status_reason`, and signal-side richer structure context directly in the report payload.
|
||||
- Post-trade review reports now expose a `signal_context` block with richer structure-state and data-quality fields.
|
||||
- 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`.
|
||||
- Earlier doc cleanup remains in the worktree: `docs/` root contains current requirement/engineering docs, historical material is under `docs/archive/`, and generic workflow helpers are under `docs/workflow/`.
|
||||
- `.hermes/` and `geckodriver.log` are ignored local artifacts.
|
||||
|
||||
## Exact Next Step
|
||||
|
||||
Review the dirty worktree, decide what belongs in version control, then update `TASKS.md` with the single next implementation task.
|
||||
Surface the normalized signal structure-state fields in dashboard table/filter UX and decide whether legacy `trade_signal.invalidations` strings should be normalized to the same machine-readable structure-state reason codes now used in signal/report/API metadata.
|
||||
|
||||
## Validation Commands
|
||||
|
||||
```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/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
|
||||
```
|
||||
|
||||
## Risks Or Open Questions
|
||||
|
||||
- The current product priority is not yet recorded in this repo-local workflow.
|
||||
- The dirty worktree may contain intentional local artifacts, but that is not documented yet.
|
||||
- `uv.lock` is intentionally left visible for version control, but it is still untracked until included in a future commit.
|
||||
- Liquidity merge-dedupe prevents new duplicate inserts at the same collapsed level, but it does not retro-clean older duplicate rows already stored in a database.
|
||||
- The dashboard still shows the new structure-state fields mainly through raw JSON detail rather than first-class table columns or filters.
|
||||
- `trade_signal.invalidations` still uses legacy human-readable strings like `mss invalidated` / `fvg fully mitigated`, while newer signal/report/API fields now use machine-readable state codes; that contract mismatch is still unresolved.
|
||||
- Signal-side FVG selection still prefers displacement context first; if newer non-contextual FVGs should override that preference, the selection rule may need further refinement against expected trading behavior.
|
||||
- Training remains a maintenance-first module for now; upstream trading semantics should stabilize before adding larger Training features.
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
## In Progress
|
||||
|
||||
- [ ] Reconcile the current dirty worktree and confirm the next product task before new feature work starts.
|
||||
- [ ] Surface normalized signal structure-state fields in the dashboard UX and align legacy `trade_signal.invalidations` strings with the newer machine-readable structure-state reason contract.
|
||||
|
||||
## Next Up
|
||||
|
||||
- [ ] Decide whether `.gitignore`, `.hermes/`, `geckodriver.log`, and `uv.lock` should stay, be ignored, or be committed.
|
||||
- [ ] After repo state is clarified, promote one concrete feature or bugfix into `In Progress`.
|
||||
- [ ] Keep the Training M40 public contract green while upstream trading semantics are refined.
|
||||
|
||||
## Backlog
|
||||
|
||||
@@ -15,8 +14,15 @@
|
||||
|
||||
## Blocked
|
||||
|
||||
- [ ] Waiting for product priority clarification if no single next feature is agreed.
|
||||
- [ ] No active blockers.
|
||||
|
||||
## Done
|
||||
|
||||
- [x] 2026-05-03 Propagated richer `structure_state` / MSS / FVG reason fields into report and API consumers, normalized `/signals` output, enriched report payloads, and revalidated with unit tests, offline checks, and local demo readiness.
|
||||
- [x] 2026-05-03 Converged MSS `accepted`/`rejected`/`invalidated` semantics into explicit state metadata, persisted the latest MSS state for downstream consumers, exposed richer MSS/FVG reasons in signal metadata, and revalidated with unit tests, offline checks, and local demo readiness.
|
||||
- [x] 2026-05-03 Fixed the `run_dev.py` exit bug by turning it into the persistent local API/dashboard dev entrypoint, updated docs, and revalidated with unit tests, offline checks, and local demo readiness.
|
||||
- [x] 2026-05-03 Implemented liquidity equal highs/lows merge-dedupe idempotency in the persistence path, stabilized exact dedupe keys, and normalized duplicate downstream displacement context selection in MSS/FVG with focused tests.
|
||||
- [x] 2026-05-03 Hardened the replay-backed no-lookahead backtest mode, aligned replay swing-count expectations with existing outside-bar swing semantics, and revalidated with unit discovery, offline checks, and local demo readiness.
|
||||
- [x] 2026-05-03 Cleaned the docs root, deleted duplicate docs, archived obsolete/history docs, moved workflow helpers into `docs/workflow`, and wrote `docs/AI_ICT_当前开发方案_v2.md`.
|
||||
- [x] 2026-05-03 Ignored local `.hermes/` state and `geckodriver.log`; kept `uv.lock` visible for version control.
|
||||
- [x] 2026-05-02 Added Codex multi-project workflow files for independent continuation.
|
||||
|
||||
@@ -1,872 +0,0 @@
|
||||
# AI ICT Codebase Skeleton Spec v1
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档用于把 AI ICT 项目第一阶段的总体设计进一步落到“代码骨架”粒度,明确:
|
||||
|
||||
1. 目录结构
|
||||
2. 模块职责
|
||||
3. 配置文件分工
|
||||
4. 服务边界
|
||||
5. 运行入口
|
||||
6. 第一轮实现顺序
|
||||
|
||||
目标不是一次把全部业务写完,而是先把后续开发要长期承载的工程骨架固定下来,确保数据库、规则引擎、信号、执行、回测和 AI 报告都能在同一套结构下持续推进。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计原则
|
||||
|
||||
代码骨架应遵循以下原则:
|
||||
|
||||
1. 先支撑基础数据与基础规则,再支撑信号与执行
|
||||
2. 规则判定与 AI 解释分层,不混在一个模块里
|
||||
3. 数据入库、规则计算、信号生成、执行管理职责分离
|
||||
4. 所有核心输出都应能回写 PostgreSQL
|
||||
5. 第一阶段优先单体代码仓库,模块清晰即可,不急于微服务拆分
|
||||
|
||||
因此,第一版更适合采用:
|
||||
|
||||
- 单仓库
|
||||
- 单进程多入口
|
||||
- 共享 `config` / `db` / `models` / `services` / `rules`
|
||||
- 后续再按实际负载拆服务
|
||||
|
||||
---
|
||||
|
||||
## 3. 推荐目录结构
|
||||
|
||||
建议第一版代码仓库采用以下结构:
|
||||
|
||||
```text
|
||||
ai-ict/
|
||||
src/
|
||||
app/
|
||||
config/
|
||||
db/
|
||||
migrations/
|
||||
seeds/
|
||||
repositories/
|
||||
domain/
|
||||
market/
|
||||
session/
|
||||
structure/
|
||||
signal/
|
||||
execution/
|
||||
backtest/
|
||||
report/
|
||||
services/
|
||||
market-data/
|
||||
rule-engine/
|
||||
signal/
|
||||
execution/
|
||||
report/
|
||||
backtest/
|
||||
rules/
|
||||
swings/
|
||||
ranges/
|
||||
liquidity/
|
||||
sweep/
|
||||
displacement/
|
||||
structure/
|
||||
fvg/
|
||||
bias/
|
||||
jobs/
|
||||
api/
|
||||
runtime/
|
||||
utils/
|
||||
types/
|
||||
scripts/
|
||||
tests/
|
||||
unit/
|
||||
integration/
|
||||
fixtures/
|
||||
docs/
|
||||
.env.example
|
||||
package.json
|
||||
tsconfig.json
|
||||
README.md
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `src/domain/` 放领域对象与核心数据结构,不放外部 API 细节
|
||||
- `src/services/` 放跨模块编排逻辑
|
||||
- `src/rules/` 放纯规则判定逻辑
|
||||
- `src/db/` 放 migration、seed、repository 和数据库接入
|
||||
- `src/runtime/` 放各类启动入口
|
||||
- `src/jobs/` 放定时任务、重算任务、数据同步任务
|
||||
- `src/api/` 放 HTTP 接口或内部管理接口
|
||||
|
||||
---
|
||||
|
||||
## 4. 目录职责拆分
|
||||
|
||||
## 4.1 `src/app/`
|
||||
|
||||
用于放应用装配层。
|
||||
|
||||
建议内容:
|
||||
|
||||
- 容器初始化
|
||||
- 配置加载
|
||||
- 日志初始化
|
||||
- 数据库初始化
|
||||
- 服务注册
|
||||
- runtime 共享启动辅助
|
||||
|
||||
这个目录不负责业务规则本身,只负责把系统拼起来。
|
||||
|
||||
---
|
||||
|
||||
## 4.2 `src/config/`
|
||||
|
||||
用于集中管理配置。
|
||||
|
||||
建议拆分:
|
||||
|
||||
```text
|
||||
src/config/
|
||||
env.ts
|
||||
database.ts
|
||||
market.ts
|
||||
sessions.ts
|
||||
timeframes.ts
|
||||
risk.ts
|
||||
models.ts
|
||||
```
|
||||
|
||||
职责建议:
|
||||
|
||||
- `env.ts`
|
||||
- 读取环境变量
|
||||
- 做最小边界校验
|
||||
- `database.ts`
|
||||
- PostgreSQL 连接参数
|
||||
- pool 配置
|
||||
- `market.ts`
|
||||
- 交易所、品种、symbol 映射
|
||||
- `sessions.ts`
|
||||
- ASIA / LONDON / NY_AM / NY_LUNCH / NY_PM 定义
|
||||
- `timeframes.ts`
|
||||
- bias/setup/trigger 周期配置
|
||||
- `risk.ts`
|
||||
- 单笔风险、日亏损、新闻窗口、会话限制
|
||||
- `models.ts`
|
||||
- 各策略模型编码,例如 `SWEEP_MSS_FVG`
|
||||
|
||||
要求:
|
||||
|
||||
- 配置模块只负责提供配置值
|
||||
- 不在配置模块里写业务查询或数据库逻辑
|
||||
|
||||
---
|
||||
|
||||
## 4.3 `src/db/`
|
||||
|
||||
用于统一管理数据库接入。
|
||||
|
||||
建议结构:
|
||||
|
||||
```text
|
||||
src/db/
|
||||
client.ts
|
||||
migrations/
|
||||
seeds/
|
||||
repositories/
|
||||
venues-repository.ts
|
||||
instruments-repository.ts
|
||||
candles-repository.ts
|
||||
market-sessions-repository.ts
|
||||
swings-repository.ts
|
||||
dealing-ranges-repository.ts
|
||||
liquidity-pools-repository.ts
|
||||
sweep-events-repository.ts
|
||||
displacement-events-repository.ts
|
||||
structure-events-repository.ts
|
||||
fvg-zones-repository.ts
|
||||
bias-snapshots-repository.ts
|
||||
trade-signals-repository.ts
|
||||
trade-executions-repository.ts
|
||||
backtest-runs-repository.ts
|
||||
backtest-results-repository.ts
|
||||
```
|
||||
|
||||
职责:
|
||||
|
||||
- `client.ts` 负责 PostgreSQL 连接与事务入口
|
||||
- `migrations/` 放正式 migration 文件
|
||||
- `seeds/` 放初始种子数据
|
||||
- `repositories/` 只负责表读写,不负责业务判断
|
||||
|
||||
建议约束:
|
||||
|
||||
- repository 不直接做规则识别
|
||||
- 复杂业务编排交给 `services/`
|
||||
- 规则计算交给 `rules/`
|
||||
|
||||
---
|
||||
|
||||
## 4.4 `src/domain/`
|
||||
|
||||
用于放领域模型与跨模块共享结构。
|
||||
|
||||
建议子目录:
|
||||
|
||||
```text
|
||||
src/domain/
|
||||
market/
|
||||
session/
|
||||
structure/
|
||||
signal/
|
||||
execution/
|
||||
backtest/
|
||||
report/
|
||||
```
|
||||
|
||||
可承载内容:
|
||||
|
||||
- Candle
|
||||
- SessionWindow
|
||||
- Swing
|
||||
- DealingRange
|
||||
- LiquidityPool
|
||||
- SweepEvent
|
||||
- DisplacementEvent
|
||||
- StructureEvent
|
||||
- FvgZone
|
||||
- BiasSnapshot
|
||||
- TradeSignal
|
||||
- TradeExecution
|
||||
- BacktestRun
|
||||
- BacktestResult
|
||||
|
||||
要求:
|
||||
|
||||
- 领域层表达“数据是什么”
|
||||
- 不表达“从哪个 API 拉来的”
|
||||
- 不耦合具体数据库客户端
|
||||
|
||||
---
|
||||
|
||||
## 4.5 `src/rules/`
|
||||
|
||||
这是第一阶段最关键的纯规则层。
|
||||
|
||||
建议结构:
|
||||
|
||||
```text
|
||||
src/rules/
|
||||
swings/
|
||||
detect-swings.ts
|
||||
ranges/
|
||||
detect-dealing-range.ts
|
||||
classify-price-location.ts
|
||||
liquidity/
|
||||
detect-liquidity-pools.ts
|
||||
sweep/
|
||||
detect-sweeps.ts
|
||||
displacement/
|
||||
detect-displacement.ts
|
||||
structure/
|
||||
detect-msb.ts
|
||||
detect-mss.ts
|
||||
fvg/
|
||||
detect-fvg.ts
|
||||
update-fvg-status.ts
|
||||
bias/
|
||||
build-bias-snapshot.ts
|
||||
```
|
||||
|
||||
职责:
|
||||
|
||||
- 输入标准化市场数据或上游结构对象
|
||||
- 输出结构化判定结果
|
||||
- 尽量保持纯函数、可回测、可重复执行
|
||||
|
||||
设计要求:
|
||||
|
||||
- `rules/` 不直接调用交易所 API
|
||||
- `rules/` 不直接控制下单
|
||||
- `rules/` 可以被实时运行和回测共用
|
||||
|
||||
这层是后续“规则口径与回测口径一致”的核心。
|
||||
|
||||
---
|
||||
|
||||
## 4.6 `src/services/`
|
||||
|
||||
服务层负责把 repository、rules、外部数据源串起来。
|
||||
|
||||
建议结构:
|
||||
|
||||
```text
|
||||
src/services/
|
||||
market-data/
|
||||
ingest-okx-candles.ts
|
||||
sync-instruments.ts
|
||||
generate-market-sessions.ts
|
||||
rule-engine/
|
||||
rebuild-market-structure.ts
|
||||
run-swing-pipeline.ts
|
||||
run-liquidity-pipeline.ts
|
||||
run-structure-pipeline.ts
|
||||
signal/
|
||||
build-bias.ts
|
||||
generate-trade-signals.ts
|
||||
update-signal-state.ts
|
||||
execution/
|
||||
run-risk-check.ts
|
||||
create-paper-order.ts
|
||||
create-semi-auto-order.ts
|
||||
manage-open-position.ts
|
||||
report/
|
||||
build-pre-market-report.ts
|
||||
build-intraday-report.ts
|
||||
build-post-trade-review.ts
|
||||
backtest/
|
||||
run-backtest.ts
|
||||
summarize-backtest.ts
|
||||
```
|
||||
|
||||
服务层职责是:
|
||||
|
||||
- 组织调用顺序
|
||||
- 控制事务边界
|
||||
- 调用 repository 落库
|
||||
- 调用 rules 计算
|
||||
- 处理任务入口与失败恢复
|
||||
|
||||
要求:
|
||||
|
||||
- service 可以依赖多个 repository 和 rule 模块
|
||||
- service 不应把 SQL 写得到处都是
|
||||
- service 不应承载底层公共工具函数
|
||||
|
||||
---
|
||||
|
||||
## 4.7 `src/jobs/`
|
||||
|
||||
用于放异步任务与定时任务入口。
|
||||
|
||||
建议首批任务:
|
||||
|
||||
- `sync-okx-candles-job.ts`
|
||||
- `generate-sessions-job.ts`
|
||||
- `rebuild-swings-job.ts`
|
||||
- `rebuild-liquidity-job.ts`
|
||||
- `generate-signals-job.ts`
|
||||
- `run-backtest-job.ts`
|
||||
|
||||
职责:
|
||||
|
||||
- 作为 cron 或手动触发入口
|
||||
- 调用 `services/` 完成任务
|
||||
- 记录任务执行日志
|
||||
|
||||
---
|
||||
|
||||
## 4.8 `src/api/`
|
||||
|
||||
第一阶段即使不做完整前端,也建议预留轻量 API 层。
|
||||
|
||||
建议提供:
|
||||
|
||||
- health check
|
||||
- recent candles query
|
||||
- current sessions query
|
||||
- current structure query
|
||||
- candidate signals query
|
||||
- backtest summary query
|
||||
|
||||
作用:
|
||||
|
||||
- 供后续 Web 控制台使用
|
||||
- 供人工验证与调试使用
|
||||
- 避免所有数据检查都依赖直接查库
|
||||
|
||||
---
|
||||
|
||||
## 4.9 `src/runtime/`
|
||||
|
||||
用于放程序入口。
|
||||
|
||||
建议结构:
|
||||
|
||||
```text
|
||||
src/runtime/
|
||||
worker.ts
|
||||
api-server.ts
|
||||
market-data-runner.ts
|
||||
rule-engine-runner.ts
|
||||
signal-runner.ts
|
||||
backtest-runner.ts
|
||||
```
|
||||
|
||||
每个入口只负责:
|
||||
|
||||
- 启动应用上下文
|
||||
- 选择要跑的 service 或 job
|
||||
- 处理进程级日志与退出码
|
||||
|
||||
不建议把核心业务直接堆在入口文件里。
|
||||
|
||||
---
|
||||
|
||||
## 4.10 `src/utils/`
|
||||
|
||||
只放通用工具。
|
||||
|
||||
例如:
|
||||
|
||||
- 时间处理
|
||||
- 数值精度处理
|
||||
- 时区转换
|
||||
- 分页工具
|
||||
- 幂等键生成
|
||||
|
||||
要求:
|
||||
|
||||
- 不把业务规则偷偷塞进 `utils`
|
||||
- 与 ICT 规则直接相关的逻辑应留在 `rules/`
|
||||
|
||||
---
|
||||
|
||||
## 4.11 `src/types/`
|
||||
|
||||
用于放跨层共享类型。
|
||||
|
||||
例如:
|
||||
|
||||
- DTO
|
||||
- job payload
|
||||
- service input/output
|
||||
- API response shape
|
||||
|
||||
如果某类型已经明显属于领域对象,应优先放回 `domain/`,不要全堆在 `types/`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 服务边界
|
||||
|
||||
结合总体设计,第一阶段建议保留 5 条主要服务边界。
|
||||
|
||||
## 5.1 market-data-service
|
||||
|
||||
职责:
|
||||
|
||||
- 拉取 OKX / LMAX 行情
|
||||
- 规范化 candles
|
||||
- 维护 instruments 元数据
|
||||
- 生成 market sessions
|
||||
- 持续写入 PostgreSQL
|
||||
|
||||
核心输入:
|
||||
|
||||
- 交易所 API
|
||||
- 品种配置
|
||||
- session 配置
|
||||
|
||||
核心输出:
|
||||
|
||||
- `venues`
|
||||
- `instruments`
|
||||
- `candles`
|
||||
- `market_sessions`
|
||||
|
||||
---
|
||||
|
||||
## 5.2 rule-engine-service
|
||||
|
||||
职责:
|
||||
|
||||
- 基于 candles 和 sessions 识别结构
|
||||
- 生成 swings、ranges、liquidity、sweep、displacement、MSS、FVG
|
||||
- 维护结构对象的更新与重算
|
||||
|
||||
核心输入:
|
||||
|
||||
- `candles`
|
||||
- `market_sessions`
|
||||
- 参数配置
|
||||
|
||||
核心输出:
|
||||
|
||||
- `swings`
|
||||
- `dealing_ranges`
|
||||
- `liquidity_pools`
|
||||
- `sweep_events`
|
||||
- `displacement_events`
|
||||
- `structure_events`
|
||||
- `fvg_zones`
|
||||
|
||||
---
|
||||
|
||||
## 5.3 signal-service
|
||||
|
||||
职责:
|
||||
|
||||
- 汇总 higher timeframe context
|
||||
- 生成 bias snapshot
|
||||
- 把结构链路组合成 trade signal
|
||||
- 维护 signal 状态流转
|
||||
|
||||
核心输出:
|
||||
|
||||
- `bias_snapshots`
|
||||
- `trade_signals`
|
||||
|
||||
---
|
||||
|
||||
## 5.4 execution-service
|
||||
|
||||
职责:
|
||||
|
||||
- 风控检查
|
||||
- 模拟盘执行
|
||||
- 半自动下单编排
|
||||
- 持仓与订单状态更新
|
||||
|
||||
核心输出:
|
||||
|
||||
- `trade_executions`
|
||||
- 风控结论
|
||||
- 审计记录
|
||||
|
||||
---
|
||||
|
||||
## 5.5 report-service
|
||||
|
||||
职责:
|
||||
|
||||
- 盘前计划
|
||||
- 盘中解释
|
||||
- 盘后复盘
|
||||
- 回测摘要
|
||||
- 为 LLM 准备结构化输入
|
||||
|
||||
核心要求:
|
||||
|
||||
- 只解释规则层和执行层的结构化结果
|
||||
- 不绕过规则直接生成裸建议
|
||||
|
||||
---
|
||||
|
||||
## 6. 配置文件与环境变量建议
|
||||
|
||||
第一阶段建议至少具备以下配置文件:
|
||||
|
||||
### 6.1 `.env.example`
|
||||
|
||||
建议包含:
|
||||
|
||||
```text
|
||||
DATABASE_URL=
|
||||
OKX_API_KEY=
|
||||
OKX_API_SECRET=
|
||||
OKX_PASSPHRASE=
|
||||
LMAX_BASE_URL=
|
||||
APP_TIMEZONE=UTC
|
||||
DEFAULT_SESSION_TIMEZONE=America/New_York
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 第一阶段至少把数据库和 OKX 接入所需变量明确下来
|
||||
- 未实际接入的供应商变量也可以预留,但不要提前绑定太多无用项
|
||||
|
||||
### 6.2 `package.json`
|
||||
|
||||
建议承载:
|
||||
|
||||
- dev 启动命令
|
||||
- migration 命令
|
||||
- seed 命令
|
||||
- ingest 命令
|
||||
- rule rebuild 命令
|
||||
- backtest 命令
|
||||
|
||||
### 6.3 `tsconfig.json`
|
||||
|
||||
建议固定:
|
||||
|
||||
- 路径别名
|
||||
- strict 模式
|
||||
- source map
|
||||
- 测试目录纳入方式
|
||||
|
||||
### 6.4 日志与格式配置
|
||||
|
||||
若使用:
|
||||
|
||||
- ESLint / Biome
|
||||
- Prettier
|
||||
- Vitest / Jest
|
||||
|
||||
应在项目启动初期固定,避免中期频繁改工程规范。
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据库层与代码层映射建议
|
||||
|
||||
第一阶段建议先把数据库对象与代码模块稳定映射:
|
||||
|
||||
| 数据表 | repository | 主要服务 | 主要规则 |
|
||||
|---|---|---|---|
|
||||
| `venues` | `venues-repository.ts` | `sync-instruments.ts` | - |
|
||||
| `instruments` | `instruments-repository.ts` | `sync-instruments.ts` | - |
|
||||
| `candles` | `candles-repository.ts` | `ingest-okx-candles.ts` | - |
|
||||
| `market_sessions` | `market-sessions-repository.ts` | `generate-market-sessions.ts` | - |
|
||||
| `swings` | `swings-repository.ts` | `run-swing-pipeline.ts` | `detect-swings.ts` |
|
||||
| `dealing_ranges` | `dealing-ranges-repository.ts` | `run-structure-pipeline.ts` | `detect-dealing-range.ts` |
|
||||
| `liquidity_pools` | `liquidity-pools-repository.ts` | `run-liquidity-pipeline.ts` | `detect-liquidity-pools.ts` |
|
||||
| `displacement_events` | `displacement-events-repository.ts` | `run-structure-pipeline.ts` | `detect-displacement.ts` |
|
||||
| `structure_events` | `structure-events-repository.ts` | `run-structure-pipeline.ts` | `detect-msb.ts` / `detect-mss.ts` |
|
||||
| `fvg_zones` | `fvg-zones-repository.ts` | `run-structure-pipeline.ts` | `detect-fvg.ts` |
|
||||
| `bias_snapshots` | `bias-snapshots-repository.ts` | `build-bias.ts` | `build-bias-snapshot.ts` |
|
||||
| `trade_signals` | `trade-signals-repository.ts` | `generate-trade-signals.ts` | 组合规则 |
|
||||
| `trade_executions` | `trade-executions-repository.ts` | `create-paper-order.ts` | 风控规则 |
|
||||
| `backtest_runs` | `backtest-runs-repository.ts` | `run-backtest.ts` | - |
|
||||
| `backtest_results` | `backtest-results-repository.ts` | `run-backtest.ts` | 回放逻辑 |
|
||||
|
||||
这样可以避免后续代码增长后职责混乱。
|
||||
|
||||
---
|
||||
|
||||
## 8. 运行入口建议
|
||||
|
||||
第一阶段建议至少提供以下运行入口。
|
||||
|
||||
## 8.1 API server
|
||||
|
||||
用途:
|
||||
|
||||
- 健康检查
|
||||
- 查询当前市场结构
|
||||
- 查询候选信号
|
||||
- 查询回测摘要
|
||||
|
||||
建议入口:
|
||||
|
||||
- `src/runtime/api-server.ts`
|
||||
|
||||
---
|
||||
|
||||
## 8.2 market data runner
|
||||
|
||||
用途:
|
||||
|
||||
- 定时拉取 OKX candles
|
||||
- 同步 instruments
|
||||
- 驱动 session 生成
|
||||
|
||||
建议入口:
|
||||
|
||||
- `src/runtime/market-data-runner.ts`
|
||||
|
||||
---
|
||||
|
||||
## 8.3 rule engine runner
|
||||
|
||||
用途:
|
||||
|
||||
- 重算 swings
|
||||
- 重算 dealing range
|
||||
- 重算 liquidity
|
||||
- 重算 displacement / MSS / FVG
|
||||
|
||||
建议入口:
|
||||
|
||||
- `src/runtime/rule-engine-runner.ts`
|
||||
|
||||
---
|
||||
|
||||
## 8.4 signal runner
|
||||
|
||||
用途:
|
||||
|
||||
- 生成 bias snapshot
|
||||
- 生成 trade signal
|
||||
- 更新 signal 状态
|
||||
|
||||
建议入口:
|
||||
|
||||
- `src/runtime/signal-runner.ts`
|
||||
|
||||
---
|
||||
|
||||
## 8.5 backtest runner
|
||||
|
||||
用途:
|
||||
|
||||
- 执行指定模型、品种、周期范围的回测
|
||||
- 输出 backtest summary
|
||||
|
||||
建议入口:
|
||||
|
||||
- `src/runtime/backtest-runner.ts`
|
||||
|
||||
---
|
||||
|
||||
## 9. 第一轮实现顺序
|
||||
|
||||
结合路线图与总体设计,建议第一轮按以下顺序实现。
|
||||
|
||||
## 9.1 Step 1:项目骨架初始化
|
||||
|
||||
先完成:
|
||||
|
||||
- `src/config/`
|
||||
- `src/db/client.ts`
|
||||
- `src/runtime/`
|
||||
- `src/services/market-data/` 基础空壳
|
||||
- `.env.example`
|
||||
- 日志模块
|
||||
|
||||
完成标准:
|
||||
|
||||
- 项目可启动
|
||||
- 配置可加载
|
||||
- PostgreSQL 可连接
|
||||
|
||||
---
|
||||
|
||||
## 9.2 Step 2:基础 migration 落地
|
||||
|
||||
先建:
|
||||
|
||||
- `venues`
|
||||
- `instruments`
|
||||
- `candles`
|
||||
- `market_sessions`
|
||||
- `swings`
|
||||
- `dealing_ranges`
|
||||
- `liquidity_pools`
|
||||
|
||||
说明:
|
||||
|
||||
- 这是第一阶段共享底座
|
||||
- 后续所有规则与信号都依赖这些表
|
||||
|
||||
---
|
||||
|
||||
## 9.3 Step 3:OKX 数据接入
|
||||
|
||||
优先接通:
|
||||
|
||||
- BTC
|
||||
- ETH
|
||||
|
||||
完成内容:
|
||||
|
||||
- candles 拉取
|
||||
- 标准化
|
||||
- 入库
|
||||
- 去重
|
||||
|
||||
---
|
||||
|
||||
## 9.4 Step 4:session generator
|
||||
|
||||
完成内容:
|
||||
|
||||
- ASIA / LONDON / NY_AM / NY_LUNCH / NY_PM 切分
|
||||
- session high / low / midpoint 生成
|
||||
- 状态更新
|
||||
|
||||
---
|
||||
|
||||
## 9.5 Step 5:基础规则层
|
||||
|
||||
优先实现:
|
||||
|
||||
1. swings
|
||||
2. dealing_ranges
|
||||
3. liquidity_pools
|
||||
|
||||
原因:
|
||||
|
||||
- 它们是 sweep / MSS / FVG 的共同前提
|
||||
|
||||
---
|
||||
|
||||
## 9.6 Step 6:核心信号链
|
||||
|
||||
按链路推进:
|
||||
|
||||
- sweep
|
||||
- displacement
|
||||
- MSB / MSS
|
||||
- FVG
|
||||
- bias snapshot
|
||||
- trade signal
|
||||
|
||||
主链路保持为:
|
||||
|
||||
`Sweep -> MSS -> FVG Retrace -> IRL/ERL`
|
||||
|
||||
---
|
||||
|
||||
## 9.7 Step 7:回测与执行
|
||||
|
||||
在信号链稳定后,再进入:
|
||||
|
||||
- backtest
|
||||
- paper trading
|
||||
- semi-auto execution
|
||||
- report generation
|
||||
|
||||
不建议跳过回测直接做执行自动化。
|
||||
|
||||
---
|
||||
|
||||
## 10. 第一阶段最小启动文件清单
|
||||
|
||||
若现在要直接开始建项目,建议先落以下最小文件:
|
||||
|
||||
```text
|
||||
src/config/env.ts
|
||||
src/config/database.ts
|
||||
src/config/sessions.ts
|
||||
src/db/client.ts
|
||||
src/db/migrations/001_create_venues.sql
|
||||
src/db/migrations/002_create_instruments.sql
|
||||
src/db/migrations/003_create_candles.sql
|
||||
src/db/migrations/004_create_candles_partitions_and_indexes.sql
|
||||
src/db/migrations/005_create_market_sessions.sql
|
||||
src/services/market-data/ingest-okx-candles.ts
|
||||
src/services/market-data/generate-market-sessions.ts
|
||||
src/runtime/market-data-runner.ts
|
||||
src/runtime/api-server.ts
|
||||
.env.example
|
||||
```
|
||||
|
||||
若以上文件稳定,再继续进入 swing / dealing range 实现。
|
||||
|
||||
---
|
||||
|
||||
## 11. 验证与关闭标准
|
||||
|
||||
根据执行检查清单,代码骨架文档完成至少应满足:
|
||||
|
||||
- 文档已写入本地 `.md`
|
||||
- 路径存在
|
||||
- 已读取前部内容检查格式正常
|
||||
- 标题、章节结构、核心内容完整
|
||||
|
||||
后续代码真正开始落地时,每个骨架模块也应满足:
|
||||
|
||||
- 有目标文件
|
||||
- 有最小启动或导入验证
|
||||
- 与当前阶段目标直接相关
|
||||
|
||||
---
|
||||
|
||||
## 12. 当前结论
|
||||
|
||||
AI ICT 项目第一阶段最合适的代码组织方式,不是上来拆成很多独立服务,而是:
|
||||
|
||||
> 先建立单仓库、清晰分层、可多入口运行的工程骨架,再按 `数据 -> 基础规则 -> 核心信号 -> 回测 -> 执行 -> 报告` 顺序推进。
|
||||
|
||||
只要骨架按本文档固定下来,后续 migration、OKX 接入、session generator、swings、dealing range、liquidity、MSS、FVG 和 signal 都可以在同一套结构下稳定扩展。
|
||||
@@ -1,584 +0,0 @@
|
||||
# AI ICT Dealing Range 识别规格书 v1
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档定义 AI ICT 项目第一阶段 MVP 的 dealing range identification 规则,用于指导:
|
||||
|
||||
- `dealing_ranges` 表设计与落库
|
||||
- premium / discount / equilibrium 位置判定
|
||||
- bias、liquidity、signal 等下游模块引用
|
||||
- 本地验证、抽样复盘与后续回测
|
||||
|
||||
本文档只覆盖 dealing range 的识别与维护,不覆盖 MSS、FVG、signal 组装等后续规则。
|
||||
|
||||
关联文档:
|
||||
|
||||
1. `AI_ICT_MVP_规则规格书_v1.md`
|
||||
2. `AI_ICT_PostgreSQL_Schema_v1.md`
|
||||
3. `AI_ICT_开发路线图_v1.md`
|
||||
4. `AI_ICT_执行检查清单_v1.md`
|
||||
5. `AI_ICT_Swing_Identification_Spec_v1.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
dealing range 模块需要满足以下目标:
|
||||
|
||||
1. 输出稳定,可重复生成
|
||||
2. range 锚点来源清晰,可审计
|
||||
3. 能稳定计算 equilibrium
|
||||
4. 能对当前价格给出 premium / discount / equilibrium 标注
|
||||
5. 能在同一 timeframe 历史上保留多个 range 记录
|
||||
6. 能明确当前 active range 的选择逻辑
|
||||
7. 具备可控的重算与失效规则
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心定义
|
||||
|
||||
## 3.1 Dealing Range
|
||||
|
||||
Dealing range 是由一组有效结构高低点定义出的当前工作区间,用于建立价格所处位置坐标。
|
||||
|
||||
第一版中,一个 dealing range 由:
|
||||
|
||||
- 一个有效高点锚 `high_swing`
|
||||
- 一个有效低点锚 `low_swing`
|
||||
- 基于两者计算的 `equilibrium`
|
||||
|
||||
共同组成。
|
||||
|
||||
## 3.2 Range Anchor
|
||||
|
||||
range anchor 指用于定义当前 range 边界的 swing 锚点:
|
||||
|
||||
- `high_swing_id`
|
||||
- `low_swing_id`
|
||||
|
||||
锚点必须来自已确认的 swing,且默认优先使用与当前 timeframe 对齐的 confirmed swing。
|
||||
|
||||
## 3.3 Equilibrium
|
||||
|
||||
`equilibrium` 是 dealing range 中点,计算方式为:
|
||||
|
||||
```text
|
||||
equilibrium = (high + low) / 2
|
||||
```
|
||||
|
||||
第一版使用简单中点,不引入成交量、时间权重或 VWAP 修正。
|
||||
|
||||
## 3.4 Premium / Discount / Equilibrium Zone
|
||||
|
||||
给定当前价格 `p`:
|
||||
|
||||
- 若 `p > equilibrium`,则价格位于 `premium`
|
||||
- 若 `p < equilibrium`,则价格位于 `discount`
|
||||
- 若 `p == equilibrium` 或落在定义的中点容忍带内,则位于 `equilibrium`
|
||||
|
||||
---
|
||||
|
||||
## 4. range 锚点规则
|
||||
|
||||
## 4.1 第一版锚点来源
|
||||
|
||||
第一版 dealing range 的锚点只来源于 confirmed swing,不直接来源于 candle、session high/low 或 liquidity pool。
|
||||
|
||||
原因:
|
||||
|
||||
- 与结构识别链路一致
|
||||
- 易于审计与复现
|
||||
- 可直接复用 `swings.id`
|
||||
|
||||
## 4.2 锚点类型
|
||||
|
||||
一个有效 range 必须由:
|
||||
|
||||
- 1 个 `swing_high`
|
||||
- 1 个 `swing_low`
|
||||
|
||||
组成。
|
||||
|
||||
不允许:
|
||||
|
||||
- high/high 组成 range
|
||||
- low/low 组成 range
|
||||
- 未确认 swing 组成正式 range
|
||||
|
||||
## 4.3 锚点确认要求
|
||||
|
||||
用于正式 range 的锚点必须满足:
|
||||
|
||||
- `confirmed = true`
|
||||
- 与 range 相同 `instrument_id`
|
||||
- 与 range 相同 `timeframe`
|
||||
- `strength` 符合当前 timeframe 的默认使用层级
|
||||
|
||||
## 4.4 默认层级选择
|
||||
|
||||
第一版建议:
|
||||
|
||||
- `trigger_tf` / `setup_tf` 上优先使用 ST swing 生成工作 range
|
||||
- `range_tf` 上可优先使用 IT swing 生成更稳定 range
|
||||
- `bias_tf` 若需要生成背景 range,可优先使用 IT 或 LT swing
|
||||
|
||||
但在 MVP 第一阶段,为了降低复杂度:
|
||||
|
||||
- dealing range 模块默认先支持“单一 timeframe 内的 confirmed ST swing 组合”
|
||||
- IT / LT range 作为增强项,在基础 ST range 稳定后再扩展
|
||||
|
||||
## 4.5 时间顺序要求
|
||||
|
||||
锚点必须满足以下条件:
|
||||
|
||||
- `high_swing_id != low_swing_id`
|
||||
- 两个锚点都已有明确 `ts`
|
||||
- 两个锚点共同界定的区间必须覆盖当前工作段
|
||||
|
||||
第一版允许 high swing 早于 low swing,也允许 low swing 早于 high swing,不强制某个方向先出现。
|
||||
|
||||
原因:
|
||||
|
||||
- dealing range 本质是区间坐标,不是方向判断本身
|
||||
- 方向与叙事由 bias / structure 模块补充
|
||||
|
||||
---
|
||||
|
||||
## 5. range 构建规则
|
||||
|
||||
## 5.1 基础构建公式
|
||||
|
||||
给定一组有效锚点:
|
||||
|
||||
- `high = high_swing.price`
|
||||
- `low = low_swing.price`
|
||||
- `equilibrium = (high + low) / 2`
|
||||
|
||||
则生成一条 dealing range。
|
||||
|
||||
## 5.2 合法性要求
|
||||
|
||||
有效 range 必须满足:
|
||||
|
||||
- `high > low`
|
||||
- `equilibrium > low`
|
||||
- `equilibrium < high`
|
||||
|
||||
若不满足,说明锚点或输入数据异常,不得落为正式 range。
|
||||
|
||||
## 5.3 ts_start 与 ts_end
|
||||
|
||||
第一版建议:
|
||||
|
||||
- `ts_start = min(high_swing.ts, low_swing.ts)`
|
||||
- `ts_end` 在 range 仍 active 时为空
|
||||
- 当 range 失效或被新 range 替代时,写入 `ts_end`
|
||||
|
||||
## 5.4 价格突破处理
|
||||
|
||||
价格突破 range 的高低边界,不立即删除历史 range。
|
||||
|
||||
处理方式:
|
||||
|
||||
- 原 range 保留历史记录
|
||||
- 根据 active-range 规则决定是否失效
|
||||
- 若产生新锚点组合,则新建一条 range 记录
|
||||
|
||||
---
|
||||
|
||||
## 6. equilibrium 与位置标注规则
|
||||
|
||||
## 6.1 equilibrium 计算
|
||||
|
||||
第一版固定为:
|
||||
|
||||
```text
|
||||
equilibrium = (high + low) / 2
|
||||
```
|
||||
|
||||
不做:
|
||||
|
||||
- Fibonacci 0.5 以外的扩展坐标
|
||||
- 基于成交量的权重均值
|
||||
- 动态 session 调整
|
||||
|
||||
## 6.2 位置标注
|
||||
|
||||
给定价格 `p`:
|
||||
|
||||
- `p > equilibrium` => `premium`
|
||||
- `p < equilibrium` => `discount`
|
||||
- `p == equilibrium` => `equilibrium`
|
||||
|
||||
## 6.3 equilibrium 容忍带
|
||||
|
||||
由于实际价格精度与不同品种 tick size 不同,第一版建议加入容忍带:
|
||||
|
||||
```text
|
||||
equilibrium_band = max(tick_size, (high - low) * 0.001)
|
||||
```
|
||||
|
||||
位置判定:
|
||||
|
||||
- 若 `abs(p - equilibrium) <= equilibrium_band`,标记为 `equilibrium`
|
||||
- 否则按 premium / discount 判定
|
||||
|
||||
说明:
|
||||
|
||||
- 这是工程容忍带,不是交易策略本身
|
||||
- 主要用于减少价格刚好贴近中点时的标签抖动
|
||||
|
||||
## 6.4 输出内容
|
||||
|
||||
dealing range 模块至少应输出:
|
||||
|
||||
- `high`
|
||||
- `low`
|
||||
- `equilibrium`
|
||||
- `location_label`:premium / discount / equilibrium
|
||||
- `distance_to_eq`
|
||||
- `distance_to_range_high`
|
||||
- `distance_to_range_low`
|
||||
|
||||
其中距离字段可先作为运行时输出,不强制第一版全部落库。
|
||||
|
||||
---
|
||||
|
||||
## 7. active range 选择规则
|
||||
|
||||
## 7.1 为什么需要 active range
|
||||
|
||||
同一 instrument + timeframe 历史上可能存在多条 range 记录,因此需要明确当前哪一条才是正式工作区间。
|
||||
|
||||
## 7.2 第一版 active range 原则
|
||||
|
||||
第一版在同一 `instrument_id + timeframe` 下,最多只认一条 `state = 'valid'` 的当前主 range。
|
||||
|
||||
其余历史 range:
|
||||
|
||||
- 保留为历史记录
|
||||
- `state` 改为非当前状态
|
||||
|
||||
## 7.3 默认选取规则
|
||||
|
||||
若存在多个候选 range,按以下优先级选择 active range:
|
||||
|
||||
1. 锚点均为 confirmed swing
|
||||
2. 锚点时间较新
|
||||
3. 覆盖当前价格结构更完整
|
||||
4. 最近一次被结构事件引用的 range 优先
|
||||
5. 若仍冲突,则取 `ts_start` 更晚者
|
||||
|
||||
第一版最小实现建议:
|
||||
|
||||
- 直接选择最近生成且状态为 `valid` 的一条
|
||||
- 同时保证旧 range 被置为非 active
|
||||
|
||||
## 7.4 状态建议
|
||||
|
||||
第一版建议 `state` 取值:
|
||||
|
||||
- `valid`:当前 active range
|
||||
- `superseded`:已被新 range 替代
|
||||
- `invalidated`:因结构失效而不再有效
|
||||
- `archived`:历史保留,不参与当前选择
|
||||
|
||||
若暂时不做完整状态机,至少要支持:
|
||||
|
||||
- `valid`
|
||||
- `superseded`
|
||||
|
||||
## 7.5 多 timeframe 并存
|
||||
|
||||
允许多个 timeframe 同时各自拥有 active range。
|
||||
|
||||
例如:
|
||||
|
||||
- H1 有一条 active range
|
||||
- M15 也有一条 active range
|
||||
|
||||
这不冲突,因为它们服务于不同层级判断。
|
||||
|
||||
---
|
||||
|
||||
## 8. 重算与更新规则
|
||||
|
||||
## 8.1 重算触发条件
|
||||
|
||||
当出现以下情况时,应触发 dealing range 重算:
|
||||
|
||||
1. 新 confirmed swing 写入
|
||||
2. 既有 provisional swing 转为 confirmed
|
||||
3. 历史数据补齐导致 swing 结果变化
|
||||
4. 参数变更导致 swing 识别结果变化
|
||||
5. 手动触发全量或分段重算
|
||||
|
||||
## 8.2 重算范围
|
||||
|
||||
第一版建议按 `instrument_id + timeframe` 做局部重算,而不是全市场全量重算。
|
||||
|
||||
如需更保守,可按时间窗口重算:
|
||||
|
||||
- 受影响 swing 附近 N 根 candle 对应区间
|
||||
- 再向后重建 active range
|
||||
|
||||
## 8.3 更新策略
|
||||
|
||||
建议:
|
||||
|
||||
- 不直接覆盖历史 dealing range 记录
|
||||
- 旧 range 改 `state`
|
||||
- 新 range 新增一条记录
|
||||
- `updated_at` 仅用于状态变更或元数据更新
|
||||
|
||||
这样做的好处:
|
||||
|
||||
- 历史轨迹清晰
|
||||
- 回测更易复现
|
||||
- 审计更容易
|
||||
|
||||
## 8.4 何时 superseded
|
||||
|
||||
若出现更新、更合理的锚点组合,且新 range 被判定为当前主 range,则旧 range 设为 `superseded`。
|
||||
|
||||
## 8.5 何时 invalidated
|
||||
|
||||
以下情况可将 range 标记为 `invalidated`:
|
||||
|
||||
- 锚点 swing 后续被证明无效
|
||||
- 数据缺口导致锚点可靠性丧失
|
||||
- 结构上下文已切换,旧 range 不再适合作为坐标参考
|
||||
|
||||
第一版如不实现 `invalidated`,可先统一用 `superseded` 处理。
|
||||
|
||||
---
|
||||
|
||||
## 9. 边界与异常情况
|
||||
|
||||
## 9.1 锚点价格相等
|
||||
|
||||
若 `high_swing.price == low_swing.price`,不得生成 range。
|
||||
|
||||
处理:
|
||||
|
||||
- 标记异常
|
||||
- 跳过写入
|
||||
- `meta.error = 'flat_range_anchor'`
|
||||
|
||||
## 9.2 锚点顺序反转
|
||||
|
||||
若 low swing 的时间晚于 high swing,或 high swing 的时间晚于 low swing,不构成错误。
|
||||
|
||||
第一版只要价格边界合法即可。
|
||||
|
||||
## 9.3 锚点过旧
|
||||
|
||||
若锚点距离当前时间过远,可能导致 range 与当前结构脱节。
|
||||
|
||||
第一版不强制设置最大年龄限制,但建议在 `meta` 中记录:
|
||||
|
||||
- `anchor_age_bars`
|
||||
- `anchor_age_minutes`
|
||||
|
||||
后续可用于优化 active-range 选择。
|
||||
|
||||
## 9.4 多组锚点同时成立
|
||||
|
||||
若同一时刻存在多组合法锚点:
|
||||
|
||||
- 第一版按 active-range 默认选取规则择一
|
||||
- 其余记录可写为历史候选或不落库
|
||||
|
||||
为了降低复杂度,MVP 第一版建议只落库最终被选中的主 range。
|
||||
|
||||
## 9.5 数据缺口
|
||||
|
||||
若两个锚点之间存在明显缺口:
|
||||
|
||||
- 可暂不生成正式 range
|
||||
- 或写入但 `meta.data_gap = true`
|
||||
- 下游默认不消费含 `data_gap` 的 range
|
||||
|
||||
第一版更建议:缺口存在时不生成 active range。
|
||||
|
||||
## 9.6 极端宽 range
|
||||
|
||||
若 `high - low` 远大于当前 timeframe 的正常波动,可能说明锚点选取过粗。
|
||||
|
||||
第一版不直接拒绝,但建议记录:
|
||||
|
||||
- `meta.range_width`
|
||||
- `meta.range_width_pct`
|
||||
|
||||
供后续过滤与回测分析使用。
|
||||
|
||||
---
|
||||
|
||||
## 10. 存储模型
|
||||
|
||||
## 10.1 对应表
|
||||
|
||||
`dealing range` 结果落到 `dealing_ranges` 表。
|
||||
|
||||
建议字段:
|
||||
|
||||
- `instrument_id`
|
||||
- `timeframe`
|
||||
- `high`
|
||||
- `low`
|
||||
- `equilibrium`
|
||||
- `high_swing_id`
|
||||
- `low_swing_id`
|
||||
- `state`
|
||||
- `ts_start`
|
||||
- `ts_end`
|
||||
- `meta`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
## 10.2 字段语义
|
||||
|
||||
- `high`:range 上边界
|
||||
- `low`:range 下边界
|
||||
- `equilibrium`:中点
|
||||
- `high_swing_id`:上边界来源 swing
|
||||
- `low_swing_id`:下边界来源 swing
|
||||
- `state`:当前有效性状态
|
||||
- `ts_start`:range 起始时间
|
||||
- `ts_end`:range 结束时间
|
||||
|
||||
## 10.3 meta 建议内容
|
||||
|
||||
适合写入 `meta` 的字段:
|
||||
|
||||
- `anchor_strength_high`
|
||||
- `anchor_strength_low`
|
||||
- `anchor_high_ts`
|
||||
- `anchor_low_ts`
|
||||
- `location_label`
|
||||
- `equilibrium_band`
|
||||
- `data_gap`
|
||||
- `selection_reason`
|
||||
- `recalc_batch_id`
|
||||
- `range_width`
|
||||
- `range_width_pct`
|
||||
|
||||
不应只放在 `meta` 的字段:
|
||||
|
||||
- `high`
|
||||
- `low`
|
||||
- `equilibrium`
|
||||
- `high_swing_id`
|
||||
- `low_swing_id`
|
||||
- `state`
|
||||
- `ts_start`
|
||||
|
||||
## 10.4 更新策略
|
||||
|
||||
建议:
|
||||
|
||||
- 新主 range 产生时新增记录
|
||||
- 旧记录更新 `state` 和 `ts_end`
|
||||
- 不静默覆盖历史 high / low / equilibrium
|
||||
|
||||
---
|
||||
|
||||
## 11. 处理流程建议
|
||||
|
||||
## 11.1 基础计算流程
|
||||
|
||||
1. 读取指定 instrument + timeframe 的 confirmed swings
|
||||
2. 筛选当前可用的 swing_high 与 swing_low
|
||||
3. 按规则组合候选锚点
|
||||
4. 计算 `high / low / equilibrium`
|
||||
5. 校验合法性
|
||||
6. 选出当前 active range
|
||||
7. 落库并更新旧 range 状态
|
||||
|
||||
## 11.2 最小 MVP 实现
|
||||
|
||||
建议先做:
|
||||
|
||||
1. confirmed ST swings 驱动的单 timeframe range
|
||||
2. equilibrium 计算
|
||||
3. premium / discount / equilibrium 标注
|
||||
4. 单一 active range 管理
|
||||
5. superseded 历史保留
|
||||
|
||||
之后再增强:
|
||||
|
||||
- IT / LT range
|
||||
- 更复杂的 active-range 选择
|
||||
- 数据缺口与异常过滤
|
||||
- 更精细的状态机
|
||||
|
||||
---
|
||||
|
||||
## 12. 验证检查点
|
||||
|
||||
## 12.1 功能验证
|
||||
|
||||
- [ ] 能按 timeframe 产出当前主 range
|
||||
- [ ] 同一批输入重复运行结果一致
|
||||
- [ ] active range 可稳定查询
|
||||
- [ ] premium / discount / equilibrium 输出稳定
|
||||
|
||||
## 12.2 规则验证
|
||||
|
||||
- [ ] range 必须由 1 个 confirmed high swing + 1 个 confirmed low swing 组成
|
||||
- [ ] `high > low` 才允许落库
|
||||
- [ ] `equilibrium = (high + low) / 2` 计算正确
|
||||
- [ ] 旧 range 被替代时会正确转为非 active
|
||||
- [ ] 当前价格位置标签与手工计算一致
|
||||
|
||||
## 12.3 存储验证
|
||||
|
||||
- [ ] `high_swing_id` / `low_swing_id` 可正确关联 `swings`
|
||||
- [ ] `state` 可区分当前与历史 range
|
||||
- [ ] `ts_start / ts_end` 逻辑正确
|
||||
- [ ] `meta` 可记录 selection reason 和异常信息
|
||||
|
||||
## 12.4 人工抽样验证
|
||||
|
||||
每个核心 timeframe 至少抽查:
|
||||
|
||||
- 1 个标准 bullish range 样例
|
||||
- 1 个标准 bearish range 样例
|
||||
- 1 个当前价格位于 premium 的样例
|
||||
- 1 个当前价格位于 discount 的样例
|
||||
- 1 个旧 range 被 superseded 的样例
|
||||
- 1 个锚点异常未落库的反例
|
||||
|
||||
## 12.5 与路线图对齐的完成标准
|
||||
|
||||
满足以下条件才算完成:
|
||||
|
||||
- 当前主 range 可查询
|
||||
- 可输出当前价格所处分区
|
||||
- 结果可重复生成
|
||||
- 可作为 bias / liquidity / signal 的上游输入
|
||||
|
||||
---
|
||||
|
||||
## 13. MVP 范围内不做的事
|
||||
|
||||
第一版暂不包含:
|
||||
|
||||
- 多 session 拼接式 range
|
||||
- AI 自由解释生成 range 锚点
|
||||
- 基于外部订单流的 range 加权
|
||||
- 自动合并多个候选 range
|
||||
- Fibonacci 多层分区扩展
|
||||
- 不同品种联动生成共享 range
|
||||
|
||||
---
|
||||
|
||||
## 14. 推荐的最小实现顺序
|
||||
|
||||
1. 先实现 confirmed ST swing 驱动的 range 构建
|
||||
2. 再实现 equilibrium 与位置标签
|
||||
3. 再实现 active range 的单一选择
|
||||
4. 再实现 superseded 历史保留
|
||||
5. 最后补充异常标记、重算批次和增强状态机
|
||||
|
||||
这样可以先满足阶段 2 的最小可用目标,再逐步增强。
|
||||
@@ -1,549 +0,0 @@
|
||||
# AI ICT PostgreSQL Migration SQL Drafts v1
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档提供 AI ICT 项目第一波 PostgreSQL migration 的 SQL 草案,用于直接指导:
|
||||
|
||||
- 基础表初版建表
|
||||
- 主键 / 外键 / 唯一约束落地
|
||||
- 核心查询索引落地
|
||||
- `candles` 月分区初始化
|
||||
- 后续正式 migration 文件编写
|
||||
|
||||
本文档聚焦第一实现波次,不覆盖全部事件表,只覆盖:
|
||||
|
||||
1. `venues`
|
||||
2. `instruments`
|
||||
3. `candles`
|
||||
4. `market_sessions`
|
||||
5. `swings`
|
||||
|
||||
关联文档:
|
||||
|
||||
1. `AI_ICT_PostgreSQL_Schema_v1.md`
|
||||
2. `AI_ICT_PostgreSQL_Migration_Split_Plan_v1.md`
|
||||
3. `AI_ICT_开发路线图_v1.md`
|
||||
4. `AI_ICT_执行检查清单_v1.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计边界与约定
|
||||
|
||||
第一波 migration 草案遵循以下原则:
|
||||
|
||||
1. 优先让基础数据层可落地
|
||||
2. 使用 `text + check constraint`,不先引入数据库 enum
|
||||
3. 高增长表优先做好唯一键与索引
|
||||
4. 只有 `candles` 先做分区,其他表先普通表
|
||||
5. SQL 草案以 PostgreSQL 15+ 常见写法为基准
|
||||
|
||||
统一约定:
|
||||
|
||||
- 主键使用 `bigserial`
|
||||
- 时间字段统一使用 `timestamptz`
|
||||
- 默认 schema 先按 `public` 处理
|
||||
- 金额/价格统一使用 `numeric(20,10)` 或更高精度
|
||||
- JSON 扩展字段使用 `jsonb not null default '{}'::jsonb`
|
||||
|
||||
---
|
||||
|
||||
## 3. 建议 migration 文件顺序
|
||||
|
||||
```text
|
||||
001_base_conventions.sql
|
||||
002_create_venues.sql
|
||||
003_create_instruments.sql
|
||||
004_create_candles.sql
|
||||
005_create_candles_partitions.sql
|
||||
006_create_market_sessions.sql
|
||||
007_create_swings.sql
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `candles` 主表和分区单独拆文件,便于验证与回滚
|
||||
- 若团队偏向更细粒度,也可把索引拆到各自 `*_indexes.sql`
|
||||
- 第一波先不把 seed 数据硬编码进 DDL migration,可单独写 `seed_base_data.sql`
|
||||
|
||||
---
|
||||
|
||||
## 4. 基础公共 SQL 草案
|
||||
|
||||
## 4.1 `001_base_conventions.sql`
|
||||
|
||||
建议先放公共 trigger function:
|
||||
|
||||
```sql
|
||||
create or replace function set_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
- 给存在 `updated_at` 的基础表复用
|
||||
- 减少每张表单独手写更新时间逻辑的重复
|
||||
|
||||
说明:
|
||||
|
||||
- `candles` 第一版没有 `updated_at`,通常不需要该 trigger
|
||||
- 若团队想保持 migration 最小化,也可先不加 trigger,改由应用层更新
|
||||
|
||||
---
|
||||
|
||||
## 5. `venues` SQL 草案
|
||||
|
||||
## 5.1 建表 SQL
|
||||
|
||||
```sql
|
||||
create table if not exists venues (
|
||||
id bigserial primary key,
|
||||
code text not null,
|
||||
name text not null,
|
||||
timezone text not null,
|
||||
status text not null default 'active',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint venues_code_key unique (code),
|
||||
constraint venues_status_check check (status in ('active', 'inactive'))
|
||||
);
|
||||
```
|
||||
|
||||
## 5.2 索引与 trigger
|
||||
|
||||
```sql
|
||||
create index if not exists idx_venues_status
|
||||
on venues (status);
|
||||
|
||||
create trigger trg_venues_set_updated_at
|
||||
before update on venues
|
||||
for each row
|
||||
execute function set_updated_at();
|
||||
```
|
||||
|
||||
## 5.3 说明
|
||||
|
||||
- `code` 用于业务稳定引用,如 `OKX`
|
||||
- `timezone` 保留交易所默认时区配置
|
||||
- `status` 先保留最小集合:`active | inactive`
|
||||
|
||||
---
|
||||
|
||||
## 6. `instruments` SQL 草案
|
||||
|
||||
## 6.1 建表 SQL
|
||||
|
||||
```sql
|
||||
create table if not exists instruments (
|
||||
id bigserial primary key,
|
||||
venue_id bigint not null references venues(id),
|
||||
symbol text not null,
|
||||
asset_class text not null,
|
||||
base_asset text,
|
||||
quote_asset text,
|
||||
price_scale integer not null,
|
||||
qty_scale integer not null,
|
||||
tick_size numeric(20,10) not null,
|
||||
lot_size numeric(20,10),
|
||||
active boolean not null default true,
|
||||
meta jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint instruments_venue_symbol_key unique (venue_id, symbol),
|
||||
constraint instruments_price_scale_check check (price_scale >= 0),
|
||||
constraint instruments_qty_scale_check check (qty_scale >= 0),
|
||||
constraint instruments_tick_size_check check (tick_size > 0),
|
||||
constraint instruments_lot_size_check check (lot_size is null or lot_size > 0)
|
||||
);
|
||||
```
|
||||
|
||||
## 6.2 索引与 trigger
|
||||
|
||||
```sql
|
||||
create index if not exists idx_instruments_venue_id
|
||||
on instruments (venue_id);
|
||||
|
||||
create index if not exists idx_instruments_active
|
||||
on instruments (active);
|
||||
|
||||
create index if not exists idx_instruments_asset_class
|
||||
on instruments (asset_class);
|
||||
|
||||
create trigger trg_instruments_set_updated_at
|
||||
before update on instruments
|
||||
for each row
|
||||
execute function set_updated_at();
|
||||
```
|
||||
|
||||
## 6.3 说明
|
||||
|
||||
- 用 `unique (venue_id, symbol)` 保证交易所内品种唯一
|
||||
- `meta` 用于附加合约规则、别名、外部映射等
|
||||
- `asset_class` 暂时不强加 check,给现货、永续、CFD、贵金属留空间
|
||||
|
||||
---
|
||||
|
||||
## 7. `candles` SQL 草案
|
||||
|
||||
## 7.1 主表建表 SQL
|
||||
|
||||
第一版建议使用按 `ts_open` 的 range partition:
|
||||
|
||||
```sql
|
||||
create table if not exists candles (
|
||||
id bigserial,
|
||||
instrument_id bigint not null references instruments(id),
|
||||
timeframe text not null,
|
||||
ts_open timestamptz not null,
|
||||
ts_close timestamptz not null,
|
||||
open numeric(20,10) not null,
|
||||
high numeric(20,10) not null,
|
||||
low numeric(20,10) not null,
|
||||
close numeric(20,10) not null,
|
||||
volume numeric(28,10),
|
||||
source text not null default 'api',
|
||||
created_at timestamptz not null default now(),
|
||||
constraint candles_pk primary key (id, ts_open),
|
||||
constraint candles_timeframe_check check (timeframe in ('1m', '5m', '15m', '1h', '4h', '1d')),
|
||||
constraint candles_price_order_check check (high >= low),
|
||||
constraint candles_open_range_check check (open <= high and open >= low),
|
||||
constraint candles_close_range_check check (close <= high and close >= low),
|
||||
constraint candles_time_order_check check (ts_close > ts_open),
|
||||
constraint candles_unique_bar unique (instrument_id, timeframe, ts_open)
|
||||
) partition by range (ts_open);
|
||||
```
|
||||
|
||||
## 7.2 为什么主键这样定义
|
||||
|
||||
由于 PostgreSQL 分区表上的唯一约束/主键需要包含分区键,第一版草案建议:
|
||||
|
||||
- 主键使用 `(id, ts_open)`
|
||||
- 真正业务幂等约束使用 `unique (instrument_id, timeframe, ts_open)`
|
||||
|
||||
这样可以同时满足:
|
||||
|
||||
- 保留自增主键
|
||||
- 兼容分区表限制
|
||||
- 保证单根 K 线不会重复写入
|
||||
|
||||
## 7.3 主表索引 SQL
|
||||
|
||||
```sql
|
||||
create index if not exists idx_candles_instrument_tf_ts_open_desc
|
||||
on candles (instrument_id, timeframe, ts_open desc);
|
||||
|
||||
create index if not exists idx_candles_instrument_tf_ts_close_desc
|
||||
on candles (instrument_id, timeframe, ts_close desc);
|
||||
|
||||
create index if not exists idx_candles_instrument_tf_source_ts_open_desc
|
||||
on candles (instrument_id, timeframe, source, ts_open desc);
|
||||
```
|
||||
|
||||
## 7.4 分区初始化 SQL 草案
|
||||
|
||||
例如先建立 2026-04 与 2026-05 两个分区:
|
||||
|
||||
```sql
|
||||
create table if not exists candles_2026_04
|
||||
partition of candles
|
||||
for values from ('2026-04-01 00:00:00+00') to ('2026-05-01 00:00:00+00');
|
||||
|
||||
create table if not exists candles_2026_05
|
||||
partition of candles
|
||||
for values from ('2026-05-01 00:00:00+00') to ('2026-06-01 00:00:00+00');
|
||||
```
|
||||
|
||||
## 7.5 分区级索引补充
|
||||
|
||||
若实际运行中发现分区继承索引不足以覆盖主要查询,可显式补:
|
||||
|
||||
```sql
|
||||
create index if not exists idx_candles_2026_04_instrument_tf_ts_open_desc
|
||||
on candles_2026_04 (instrument_id, timeframe, ts_open desc);
|
||||
|
||||
create index if not exists idx_candles_2026_05_instrument_tf_ts_open_desc
|
||||
on candles_2026_05 (instrument_id, timeframe, ts_open desc);
|
||||
```
|
||||
|
||||
## 7.6 后续自动建分区建议
|
||||
|
||||
后续可以增加一个月度分区初始化脚本,例如:
|
||||
|
||||
- 每月月底预建下两个月分区
|
||||
- 避免行情入库时因缺少分区而失败
|
||||
|
||||
但这不属于第一波 migration DDL 主体,可放到运维脚本或 cron 任务中。
|
||||
|
||||
---
|
||||
|
||||
## 8. `market_sessions` SQL 草案
|
||||
|
||||
## 8.1 建表 SQL
|
||||
|
||||
```sql
|
||||
create table if not exists market_sessions (
|
||||
id bigserial primary key,
|
||||
instrument_id bigint not null references instruments(id),
|
||||
session_date date not null,
|
||||
timezone text not null,
|
||||
session_code text not null,
|
||||
start_ts timestamptz not null,
|
||||
end_ts timestamptz not null,
|
||||
high numeric(20,10),
|
||||
low numeric(20,10),
|
||||
midpoint numeric(20,10),
|
||||
status text not null default 'closed',
|
||||
meta jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint market_sessions_unique_key unique (instrument_id, session_date, session_code, timezone),
|
||||
constraint market_sessions_status_check check (status in ('open', 'closed', 'incomplete', 'invalid')),
|
||||
constraint market_sessions_code_check check (session_code in ('ASIA', 'LONDON', 'NY_AM', 'NY_LUNCH', 'NY_PM')),
|
||||
constraint market_sessions_time_order_check check (end_ts > start_ts),
|
||||
constraint market_sessions_price_order_check check (
|
||||
high is null or low is null or high >= low
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## 8.2 索引与 trigger
|
||||
|
||||
```sql
|
||||
create index if not exists idx_market_sessions_instrument_date
|
||||
on market_sessions (instrument_id, session_date desc);
|
||||
|
||||
create index if not exists idx_market_sessions_instrument_code_date
|
||||
on market_sessions (instrument_id, session_code, session_date desc);
|
||||
|
||||
create index if not exists idx_market_sessions_status
|
||||
on market_sessions (status);
|
||||
|
||||
create trigger trg_market_sessions_set_updated_at
|
||||
before update on market_sessions
|
||||
for each row
|
||||
execute function set_updated_at();
|
||||
```
|
||||
|
||||
## 8.3 说明
|
||||
|
||||
- `session_date` 是业务会话所属日,不等同于 `start_ts::date`
|
||||
- `midpoint` 可存 session 中点,减少重复计算
|
||||
- `status` 预留不完整或异常会话标记
|
||||
|
||||
---
|
||||
|
||||
## 9. `swings` SQL 草案
|
||||
|
||||
## 9.1 建表 SQL
|
||||
|
||||
```sql
|
||||
create table if not exists swings (
|
||||
id bigserial primary key,
|
||||
instrument_id bigint not null references instruments(id),
|
||||
timeframe text not null,
|
||||
kind text not null,
|
||||
strength text not null,
|
||||
price numeric(20,10) not null,
|
||||
ts timestamptz not null,
|
||||
left_bars integer not null,
|
||||
right_bars integer not null,
|
||||
confirmed boolean not null default true,
|
||||
meta jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
constraint swings_kind_check check (kind in ('swing_high', 'swing_low')),
|
||||
constraint swings_strength_check check (strength in ('ST', 'IT', 'LT')),
|
||||
constraint swings_left_bars_check check (left_bars > 0),
|
||||
constraint swings_right_bars_check check (right_bars > 0)
|
||||
);
|
||||
```
|
||||
|
||||
## 9.2 索引 SQL
|
||||
|
||||
```sql
|
||||
create index if not exists idx_swings_instrument_tf_ts_desc
|
||||
on swings (instrument_id, timeframe, ts desc);
|
||||
|
||||
create index if not exists idx_swings_instrument_tf_kind_ts_desc
|
||||
on swings (instrument_id, timeframe, kind, ts desc);
|
||||
|
||||
create index if not exists idx_swings_instrument_tf_strength_ts_desc
|
||||
on swings (instrument_id, timeframe, strength, ts desc);
|
||||
|
||||
create index if not exists idx_swings_confirmed_ts_desc
|
||||
on swings (instrument_id, timeframe, confirmed, ts desc);
|
||||
```
|
||||
|
||||
## 9.3 可选幂等约束建议
|
||||
|
||||
若后续想减少同一 swing 被重复写入,可考虑增加唯一约束:
|
||||
|
||||
```sql
|
||||
unique (instrument_id, timeframe, kind, strength, ts, price)
|
||||
```
|
||||
|
||||
但第一版建议先不急着加,原因:
|
||||
|
||||
- 不同算法版本可能在同一时间点重建结构
|
||||
- 后续也可能需要靠 `meta.recalc_batch_id` 区分重算来源
|
||||
- 先观察真实数据再决定是否收紧
|
||||
|
||||
---
|
||||
|
||||
## 10. 推荐 seed SQL 草案
|
||||
|
||||
## 10.1 `venues` seed
|
||||
|
||||
```sql
|
||||
insert into venues (code, name, timezone)
|
||||
values
|
||||
('OKX', 'OKX', 'UTC'),
|
||||
('LMAX', 'LMAX', 'Europe/London')
|
||||
on conflict (code) do nothing;
|
||||
```
|
||||
|
||||
## 10.2 `instruments` seed
|
||||
|
||||
```sql
|
||||
insert into instruments (
|
||||
venue_id,
|
||||
symbol,
|
||||
asset_class,
|
||||
base_asset,
|
||||
quote_asset,
|
||||
price_scale,
|
||||
qty_scale,
|
||||
tick_size,
|
||||
lot_size
|
||||
)
|
||||
select v.id, x.symbol, x.asset_class, x.base_asset, x.quote_asset, x.price_scale, x.qty_scale, x.tick_size, x.lot_size
|
||||
from venues v
|
||||
join (
|
||||
values
|
||||
('OKX', 'BTC-USDT-SWAP', 'perpetual', 'BTC', 'USDT', 1, 3, 0.1::numeric, 0.001::numeric),
|
||||
('OKX', 'ETH-USDT-SWAP', 'perpetual', 'ETH', 'USDT', 2, 3, 0.01::numeric, 0.001::numeric),
|
||||
('LMAX', 'XAUUSD', 'spot', 'XAU', 'USD', 2, 2, 0.01::numeric, 0.01::numeric)
|
||||
) as x(venue_code, symbol, asset_class, base_asset, quote_asset, price_scale, qty_scale, tick_size, lot_size)
|
||||
on v.code = x.venue_code
|
||||
on conflict (venue_id, symbol) do nothing;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 第一波最小验证 SQL 草案
|
||||
|
||||
## 11.1 唯一约束验证
|
||||
|
||||
```sql
|
||||
-- candles 重复写入应失败
|
||||
insert into candles (
|
||||
instrument_id, timeframe, ts_open, ts_close, open, high, low, close, volume
|
||||
) values (
|
||||
1, '1m', '2026-04-01 00:00:00+00', '2026-04-01 00:01:00+00',
|
||||
100, 101, 99, 100.5, 10
|
||||
);
|
||||
|
||||
insert into candles (
|
||||
instrument_id, timeframe, ts_open, ts_close, open, high, low, close, volume
|
||||
) values (
|
||||
1, '1m', '2026-04-01 00:00:00+00', '2026-04-01 00:01:00+00',
|
||||
100, 101, 99, 100.5, 10
|
||||
);
|
||||
```
|
||||
|
||||
第二条应因 `candles_unique_bar` 失败。
|
||||
|
||||
## 11.2 约束验证
|
||||
|
||||
```sql
|
||||
-- 非法 timeframe 应失败
|
||||
insert into candles (
|
||||
instrument_id, timeframe, ts_open, ts_close, open, high, low, close
|
||||
) values (
|
||||
1, '2m', '2026-04-01 00:00:00+00', '2026-04-01 00:02:00+00',
|
||||
100, 101, 99, 100
|
||||
);
|
||||
```
|
||||
|
||||
```sql
|
||||
-- 非法 swing kind 应失败
|
||||
insert into swings (
|
||||
instrument_id, timeframe, kind, strength, price, ts, left_bars, right_bars
|
||||
) values (
|
||||
1, '1m', 'high', 'ST', 100, '2026-04-01 00:10:00+00', 2, 2
|
||||
);
|
||||
```
|
||||
|
||||
## 11.3 分区验证
|
||||
|
||||
```sql
|
||||
select tableoid::regclass, instrument_id, timeframe, ts_open
|
||||
from candles
|
||||
where ts_open = '2026-04-01 00:00:00+00';
|
||||
```
|
||||
|
||||
应看到记录落到 `candles_2026_04`。
|
||||
|
||||
---
|
||||
|
||||
## 12. 回滚草案建议
|
||||
|
||||
第一波回滚顺序建议:
|
||||
|
||||
```text
|
||||
swings
|
||||
-> market_sessions
|
||||
-> candles partitions
|
||||
-> candles
|
||||
-> instruments
|
||||
-> venues
|
||||
-> set_updated_at function
|
||||
```
|
||||
|
||||
回滚示例:
|
||||
|
||||
```sql
|
||||
drop table if exists swings;
|
||||
drop table if exists market_sessions;
|
||||
drop table if exists candles_2026_05;
|
||||
drop table if exists candles_2026_04;
|
||||
drop table if exists candles;
|
||||
drop table if exists instruments;
|
||||
drop table if exists venues;
|
||||
drop function if exists set_updated_at();
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 实际生产环境不应直接照抄物理删除
|
||||
- 先停写入任务,再确认无下游依赖,再执行回滚
|
||||
- 若已有真实数据,应先备份再处理
|
||||
|
||||
---
|
||||
|
||||
## 13. 第一波完成标准
|
||||
|
||||
满足以下条件后,可认为 migration SQL draft 文档完成:
|
||||
|
||||
- 已给出基础表的建表 SQL 草案
|
||||
- 已给出唯一约束和 check constraint 草案
|
||||
- 已给出核心索引草案
|
||||
- 已给出 `candles` 分区建表与初始化草案
|
||||
- 已给出最小 seed 与验证 SQL 草案
|
||||
- 已能直接作为正式 migration 编写参考
|
||||
|
||||
---
|
||||
|
||||
## 14. 推荐的下一步
|
||||
|
||||
完成本文件后,建议立即继续:
|
||||
|
||||
1. 把上述 SQL 草案拆成正式 migration 文件
|
||||
2. 单独补一份 `verify_base_tables.sql`
|
||||
3. 单独补一份 `rollback_base_tables.sql`
|
||||
4. 再进入 `dealing_ranges` / `liquidity_pools` 的第二波 migration 草案
|
||||
@@ -1,711 +0,0 @@
|
||||
# AI ICT PostgreSQL Migration 拆分执行方案 v1
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档把 `AI_ICT_PostgreSQL_Schema_v1.md` 中定义的数据库设计拆分为可执行的 migration 批次,确保:
|
||||
|
||||
- 按依赖顺序落地
|
||||
- 每一批都可单独验证
|
||||
- 出问题时可以局部回滚
|
||||
- 不让 migration 任务停留在“已开始但不可执行”的状态
|
||||
|
||||
适用范围:AI ICT 项目第一阶段 MVP。
|
||||
|
||||
关联文档:
|
||||
|
||||
1. `AI_ICT_总体设计文档_v1.md`
|
||||
2. `AI_ICT_PostgreSQL_Schema_v1.md`
|
||||
3. `AI_ICT_MVP_规则规格书_v1.md`
|
||||
4. `AI_ICT_开发路线图_v1.md`
|
||||
5. `AI_ICT_执行检查清单_v1.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 拆分原则
|
||||
|
||||
1. 先基础字典,再时间序列,再结构事件,再信号执行,再回测分析
|
||||
2. 每个 batch 只处理一个明确依赖层级,避免一次 migration 混入过多对象
|
||||
3. 高数据量对象优先独立拆分,尤其是 `candles`
|
||||
4. 每个 batch 都要配套验证 SQL / 回滚说明 / 必要 seed 数据
|
||||
5. 第一版优先可落地,不对全部事件表过早分区
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体依赖顺序
|
||||
|
||||
```text
|
||||
B0 基础约定/公共函数
|
||||
-> B1 venues
|
||||
-> B2 instruments
|
||||
-> B3 candles
|
||||
-> B4 market_sessions
|
||||
-> B5 swings
|
||||
-> B6 dealing_ranges + liquidity_pools
|
||||
-> B7 displacement_events
|
||||
-> B8 structure_events + fvg_zones
|
||||
-> B9 bias_snapshots
|
||||
-> B10 trade_signals
|
||||
-> B11 trade_executions
|
||||
-> B12 backtest_runs
|
||||
-> B13 backtest_results
|
||||
```
|
||||
|
||||
依赖说明:
|
||||
|
||||
- `instruments` 依赖 `venues`
|
||||
- `candles`、`market_sessions`、`swings` 依赖 `instruments`
|
||||
- `dealing_ranges` 依赖 `instruments`,并引用 `swings`
|
||||
- `structure_events` 引用 `swings` 与 `displacement_events`
|
||||
- `fvg_zones` 引用 `displacement_events`
|
||||
- `bias_snapshots` 引用 `dealing_ranges`
|
||||
- `trade_signals` 引用 `bias_snapshots`
|
||||
- `trade_executions` 引用 `trade_signals`
|
||||
- `backtest_runs` 依赖 `instruments`
|
||||
- `backtest_results` 依赖 `backtest_runs`,并可引用 `trade_signals`
|
||||
|
||||
---
|
||||
|
||||
## 4. migration 批次拆分
|
||||
|
||||
## B0. 基础约定与公共函数
|
||||
|
||||
### 目标
|
||||
|
||||
建立后续所有 migration 共享的数据库前置条件。
|
||||
|
||||
### 内容
|
||||
|
||||
- 若项目决定使用非 `public`,先建立目标 schema
|
||||
- 建立 `updated_at` 自动更新时间的公共 trigger function(若采用)
|
||||
- 明确第一版统一使用 `text + check constraint` 替代数据库 enum
|
||||
- 明确所有时间字段使用 `timestamptz`
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- 公共函数可创建成功
|
||||
- 后续表可以复用这些函数/约定
|
||||
- 时区约定已在 migration 注释中写明
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 删除本批次新增函数、trigger 模板和非业务 schema
|
||||
- 本批不涉及业务数据,回滚成本最低
|
||||
|
||||
---
|
||||
|
||||
## B1. `venues`
|
||||
|
||||
### 目标
|
||||
|
||||
建立交易所字典表。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `venues`
|
||||
- 主键 `id`
|
||||
- 唯一键 `code`
|
||||
- 默认状态 `status = 'active'`
|
||||
- 可插入初始 seed:`OKX`、`LMAX`
|
||||
|
||||
### 依赖
|
||||
|
||||
- B0
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- 表可成功创建
|
||||
- `code` 唯一约束生效
|
||||
- 基础 seed 可查询
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 若无下游依赖,可先删 seed 再 drop 表
|
||||
- 若 `instruments` 已创建,必须先回退下游表
|
||||
|
||||
---
|
||||
|
||||
## B2. `instruments`
|
||||
|
||||
### 目标
|
||||
|
||||
建立统一交易品种表。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `instruments`
|
||||
- 外键 `venue_id references venues(id)`
|
||||
- 唯一键 `unique (venue_id, symbol)`
|
||||
- `meta jsonb default '{}'`
|
||||
- 插入 BTC / ETH / XAU 的基础 instrument seed
|
||||
|
||||
### 依赖
|
||||
|
||||
- B1
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- `venue + symbol` 可唯一查询
|
||||
- 不存在的 `venue_id` 无法插入
|
||||
- `meta` 默认值生效
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 若无下游表,可删 seed 后 drop
|
||||
- 若已有 `candles`、`market_sessions`、`swings` 等表,需先逆序回滚下游
|
||||
|
||||
---
|
||||
|
||||
## B3. `candles`
|
||||
|
||||
### 目标
|
||||
|
||||
建立时间序列主表与首批月分区。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `candles` 主表
|
||||
- 唯一键 `unique (instrument_id, timeframe, ts_open)`
|
||||
- 索引:
|
||||
- `(instrument_id, timeframe, ts_open desc)`
|
||||
- `(instrument_id, timeframe, ts_close desc)`
|
||||
- 按 `ts_open` 建月分区
|
||||
- 至少预建当前月、下月分区
|
||||
|
||||
### 为什么单独拆批
|
||||
|
||||
- 数据量最大
|
||||
- 分区和唯一键策略最关键
|
||||
- 后续行情入库直接依赖这一批
|
||||
|
||||
### 依赖
|
||||
|
||||
- B2
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- 可插入样例 K 线
|
||||
- 重复 `(instrument_id, timeframe, ts_open)` 会被拒绝
|
||||
- 数据能正确进入对应月分区
|
||||
- 关键查询能命中索引
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 先删测试数据
|
||||
- 先 drop 子分区,再 drop 主表
|
||||
- 若已有采集任务写入,生产环境不应直接回滚,应停写后重建
|
||||
|
||||
---
|
||||
|
||||
## B4. `market_sessions`
|
||||
|
||||
### 目标
|
||||
|
||||
建立 session 切分与 pivot 数据表。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `market_sessions`
|
||||
- 唯一键 `unique (instrument_id, session_date, session_code, timezone)`
|
||||
- `meta jsonb default '{}'`
|
||||
- 约定 session code:`ASIA`、`LONDON`、`NY_AM`、`NY_LUNCH`、`NY_PM`
|
||||
|
||||
### 依赖
|
||||
|
||||
- B2
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- 同一 instrument/date/session/timezone 不能重复写入
|
||||
- 可查询 `high` / `low` / `midpoint`
|
||||
- 时区与会话区间表达一致
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 若未被上游程序依赖,可直接删测试数据并 drop
|
||||
- 若已有 session 生成器运行,先停任务再回滚
|
||||
|
||||
---
|
||||
|
||||
## B5. `swings`
|
||||
|
||||
### 目标
|
||||
|
||||
建立结构高低点表。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `swings`
|
||||
- check constraint:
|
||||
- `kind in ('swing_high', 'swing_low')`
|
||||
- `strength in ('ST', 'IT', 'LT')`
|
||||
- 索引:
|
||||
- `(instrument_id, timeframe, ts desc)`
|
||||
- `(instrument_id, timeframe, kind, ts desc)`
|
||||
- `(instrument_id, timeframe, strength, ts desc)`
|
||||
|
||||
### 依赖
|
||||
|
||||
- B2
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- `kind` / `strength` 约束生效
|
||||
- 能按 timeframe 查询最近 swing
|
||||
- 非法枚举值会被拒绝
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 若 `dealing_ranges` / `structure_events` 尚未创建,可直接回滚
|
||||
- 否则需先回滚依赖表
|
||||
|
||||
---
|
||||
|
||||
## B6. `dealing_ranges` + `liquidity_pools`
|
||||
|
||||
### 目标
|
||||
|
||||
建立基础规则层中最核心的两个结果表。
|
||||
|
||||
### 合并原因
|
||||
|
||||
- 同属基础规则层
|
||||
- 通常在同一轮规则计算中产出和消费
|
||||
- 依赖层级接近
|
||||
|
||||
### 内容
|
||||
|
||||
#### `dealing_ranges`
|
||||
- 创建表
|
||||
- 引用 `swings(id)`
|
||||
- 索引:
|
||||
- `(instrument_id, timeframe, ts_start desc)`
|
||||
- `(instrument_id, timeframe, state, ts_start desc)`
|
||||
|
||||
#### `liquidity_pools`
|
||||
- 创建表
|
||||
- check constraint:
|
||||
- `side in ('buy_side', 'sell_side')`
|
||||
- `status in ('untouched', 'swept', 'consumed')`
|
||||
- 索引:
|
||||
- `(instrument_id, timeframe, status, created_ts desc)`
|
||||
- `(instrument_id, timeframe, side, created_ts desc)`
|
||||
|
||||
### 依赖
|
||||
|
||||
- B5
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- `dealing_ranges.high_swing_id` / `low_swing_id` 能关联 `swings`
|
||||
- `liquidity_pools` 状态和方向约束生效
|
||||
- 可查询当前有效 range
|
||||
- 可查询当前流动性池
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 先停基础规则重算任务
|
||||
- 再按 `liquidity_pools -> dealing_ranges` 顺序回滚
|
||||
- 若 `bias_snapshots` 已存在,先回退下游
|
||||
|
||||
---
|
||||
|
||||
## B7. `displacement_events`
|
||||
|
||||
### 目标
|
||||
|
||||
建立有效位移事件表。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `displacement_events`
|
||||
- 添加 `direction`、`has_fvg`、`is_valid`
|
||||
- 索引:
|
||||
- `(instrument_id, timeframe, end_ts desc)`
|
||||
- `(instrument_id, timeframe, direction, end_ts desc)`
|
||||
|
||||
### 依赖
|
||||
|
||||
- B2
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- 可查询最近 displacement
|
||||
- 默认值正常
|
||||
- 可被 `structure_events` 与 `fvg_zones` 引用
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 若 B8 尚未执行,可直接回滚
|
||||
- 否则先回滚 B8
|
||||
|
||||
---
|
||||
|
||||
## B8. `structure_events` + `fvg_zones`
|
||||
|
||||
### 目标
|
||||
|
||||
建立核心信号链路中的结构事件与 FVG 区域表。
|
||||
|
||||
### 合并原因
|
||||
|
||||
- 都属于信号上游对象
|
||||
- 都依赖 `displacement_events`
|
||||
- 一般同一轮规则识别会一起生成
|
||||
|
||||
### 内容
|
||||
|
||||
#### `structure_events`
|
||||
- 创建表
|
||||
- check constraint:
|
||||
- `event_type in ('MSB', 'MSS')`
|
||||
- `direction in ('bullish', 'bearish')`
|
||||
- 引用 `swings(id)` 与 `displacement_events(id)`
|
||||
|
||||
#### `fvg_zones`
|
||||
- 创建表
|
||||
- check constraint:
|
||||
- `direction in ('bullish', 'bearish')`
|
||||
- `status in ('open', 'touched', 'mitigated', 'invalidated')`
|
||||
- 引用 `displacement_events(id)`
|
||||
|
||||
### 依赖
|
||||
|
||||
- B5、B7
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- `structure_events` 能关联 swing 与 displacement
|
||||
- `fvg_zones` 能关联 displacement
|
||||
- 状态与方向约束生效
|
||||
- 可查询最近 MSS/MSB 与 open FVG
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 先删测试事件
|
||||
- 先回退信号层依赖,再回滚本批次
|
||||
|
||||
---
|
||||
|
||||
## B9. `bias_snapshots`
|
||||
|
||||
### 目标
|
||||
|
||||
建立 bias 快照表。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `bias_snapshots`
|
||||
- check constraint:
|
||||
- `daily_bias in ('bullish', 'bearish', 'neutral')`
|
||||
- `execution_intensity in ('low', 'medium', 'high')`
|
||||
- 引用 `dealing_ranges(id)`
|
||||
- 索引:
|
||||
- `(instrument_id, snapshot_ts desc)`
|
||||
- `(instrument_id, daily_bias, snapshot_ts desc)`
|
||||
|
||||
### 依赖
|
||||
|
||||
- B6
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- 能关联有效 `range_id`
|
||||
- `reason_codes` / `meta` 默认值正常
|
||||
- 可按 instrument 查询最近 bias
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 若 `trade_signals` 尚未创建,可直接回滚
|
||||
- 否则先回滚 B10
|
||||
|
||||
---
|
||||
|
||||
## B10. `trade_signals`
|
||||
|
||||
### 目标
|
||||
|
||||
建立候选交易信号主表。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `trade_signals`
|
||||
- 引用 `bias_snapshots(id)`
|
||||
- 索引:
|
||||
- `(instrument_id, created_ts desc)`
|
||||
- `(instrument_id, status, created_ts desc)`
|
||||
- `(instrument_id, model_code, created_ts desc)`
|
||||
- 保留 `invalidations jsonb`、`evidence jsonb`
|
||||
- 核心字段显式建列:entry/stop/tp/rr/status/side
|
||||
|
||||
### 依赖
|
||||
|
||||
- B9
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- 信号可关联 bias snapshot
|
||||
- 能按 `status` 查询待确认/active 信号
|
||||
- `evidence` 可承载结构化引用
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 若 `trade_executions` 与 `backtest_results` 尚未创建,可直接回滚
|
||||
- 否则先回退下游依赖
|
||||
|
||||
---
|
||||
|
||||
## B11. `trade_executions`
|
||||
|
||||
### 目标
|
||||
|
||||
建立半自动执行记录表。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `trade_executions`
|
||||
- 引用 `trade_signals(id)`
|
||||
- check constraint:
|
||||
- `execution_mode in ('paper', 'semi_auto', 'live')`
|
||||
- `status in ('pending', 'open', 'partially_closed', 'closed', 'cancelled', 'rejected')`
|
||||
- 索引:
|
||||
- `(signal_id)`
|
||||
- `(status, created_at desc)`
|
||||
- `(execution_mode, created_at desc)`
|
||||
|
||||
### 依赖
|
||||
|
||||
- B10
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- execution 必须关联有效 signal
|
||||
- 可查询 open / closed / cancelled 状态
|
||||
- entry、stop、tp、size、risk_pct 可完整记录
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 先停执行写入流程
|
||||
- 删测试执行记录
|
||||
- 再回滚结构
|
||||
|
||||
---
|
||||
|
||||
## B12. `backtest_runs`
|
||||
|
||||
### 目标
|
||||
|
||||
建立回测任务元数据与结果摘要表。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `backtest_runs`
|
||||
- 引用 `instruments(id)`
|
||||
- 索引:
|
||||
- `(model_code, started_ts desc)`
|
||||
- `(instrument_id, started_ts desc)`
|
||||
- `config`、`summary` 使用 `jsonb`
|
||||
|
||||
### 依赖
|
||||
|
||||
- B2
|
||||
- 执行顺序上建议放在 B11 之后,以便阶段验收统一
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- 可记录一次回测启动/结束
|
||||
- 可按 model 查询最近回测
|
||||
- `summary` 可保存聚合统计
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 若 `backtest_results` 尚未创建,可直接回滚
|
||||
- 否则先回滚 B13
|
||||
|
||||
---
|
||||
|
||||
## B13. `backtest_results`
|
||||
|
||||
### 目标
|
||||
|
||||
建立回测明细结果表。
|
||||
|
||||
### 内容
|
||||
|
||||
- 创建 `backtest_results`
|
||||
- 引用 `backtest_runs(id)`
|
||||
- 可空引用 `trade_signals(id)`
|
||||
- check constraint:
|
||||
- `outcome in ('win', 'loss', 'breakeven', 'missed')`
|
||||
- 索引:
|
||||
- `(backtest_run_id)`
|
||||
- `(outcome)`
|
||||
|
||||
### 依赖
|
||||
|
||||
- B12
|
||||
- 若引用真实 signal,还依赖 B10
|
||||
|
||||
### 验证检查点
|
||||
|
||||
- 可按 `backtest_run_id` 查询所有结果
|
||||
- outcome 约束生效
|
||||
- `tp1_hit`、`tp2_hit`、`invalidated_before_entry` 可正确记录
|
||||
|
||||
### 回滚说明
|
||||
|
||||
- 先删除明细结果
|
||||
- 再回滚 `backtest_runs`
|
||||
- 生产环境优先逻辑废弃而非直接物理删除历史
|
||||
|
||||
---
|
||||
|
||||
## 5. 建议的 migration 文件命名
|
||||
|
||||
```text
|
||||
001_base_conventions.sql
|
||||
002_create_venues.sql
|
||||
003_create_instruments.sql
|
||||
004_create_candles.sql
|
||||
005_create_candles_partitions.sql
|
||||
006_create_market_sessions.sql
|
||||
007_create_swings.sql
|
||||
008_create_dealing_ranges.sql
|
||||
009_create_liquidity_pools.sql
|
||||
010_create_displacement_events.sql
|
||||
011_create_structure_events.sql
|
||||
012_create_fvg_zones.sql
|
||||
013_create_bias_snapshots.sql
|
||||
014_create_trade_signals.sql
|
||||
015_create_trade_executions.sql
|
||||
016_create_backtest_runs.sql
|
||||
017_create_backtest_results.sql
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 文档按 batch 归类
|
||||
- 实际文件可在同一 batch 内继续细拆
|
||||
- `candles` 主表和分区建议拆成两个 migration 文件
|
||||
|
||||
---
|
||||
|
||||
## 6. 每批次统一验证模板
|
||||
|
||||
每个 batch 执行后至少完成以下检查:
|
||||
|
||||
1. 表是否创建成功
|
||||
2. 主键/唯一键/外键是否生效
|
||||
3. check constraint 是否生效
|
||||
4. 默认值是否符合设计
|
||||
5. 一条合法样例插入是否成功
|
||||
6. 一条非法样例插入是否被拒绝
|
||||
7. 关键查询是否命中预期索引
|
||||
8. 验证结果是否记录到执行检查清单
|
||||
|
||||
建议每批配套文件:
|
||||
|
||||
- `verify_XXX.sql`
|
||||
- `rollback_XXX.sql`
|
||||
- `seed_XXX.sql`(如需要)
|
||||
|
||||
---
|
||||
|
||||
## 7. 回滚策略总则
|
||||
|
||||
1. 严格按依赖逆序回滚
|
||||
2. 有真实业务数据后,先停写、备份、评估,再做物理回滚
|
||||
3. seed 与测试数据分开管理
|
||||
4. 分区表先回滚子分区,再回滚主表
|
||||
5. 代码已依赖该 migration 时,回滚前必须同步停用相关任务或服务
|
||||
|
||||
推荐逆序:
|
||||
|
||||
```text
|
||||
B13 -> B12 -> B11 -> B10 -> B9 -> B8 -> B7 -> B6 -> B5 -> B4 -> B3 -> B2 -> B1 -> B0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 与开发阶段的对应关系
|
||||
|
||||
### 阶段 0:项目基础设施初始化
|
||||
|
||||
- B0
|
||||
- B1
|
||||
- B2
|
||||
|
||||
### 阶段 1:基础数据层
|
||||
|
||||
- B3
|
||||
- B4
|
||||
|
||||
### 阶段 2:基础规则层
|
||||
|
||||
- B5
|
||||
- B6
|
||||
|
||||
### 阶段 3:核心信号层
|
||||
|
||||
- B7
|
||||
- B8
|
||||
- B9
|
||||
- B10
|
||||
|
||||
### 阶段 4:回测与验证层
|
||||
|
||||
- B12
|
||||
- B13
|
||||
|
||||
### 阶段 5:半自动执行层
|
||||
|
||||
- B11
|
||||
|
||||
说明:
|
||||
|
||||
- 从纯数据库依赖上,`backtest_runs` 可更早建
|
||||
- 从交付节奏和验收上,按上面顺序更清晰
|
||||
|
||||
---
|
||||
|
||||
## 9. 最小可用落地顺序
|
||||
|
||||
若当前目标是尽快启动 MVP,可先完成:
|
||||
|
||||
1. B0
|
||||
2. B1
|
||||
3. B2
|
||||
4. B3
|
||||
5. B4
|
||||
6. B5
|
||||
7. B6
|
||||
|
||||
完成后即可支撑:
|
||||
|
||||
- 行情入库
|
||||
- session 生成
|
||||
- swing 识别
|
||||
- dealing range 识别
|
||||
- liquidity pool 识别
|
||||
|
||||
之后继续:
|
||||
|
||||
8. B7
|
||||
9. B8
|
||||
10. B9
|
||||
11. B10
|
||||
12. B11
|
||||
13. B12
|
||||
14. B13
|
||||
|
||||
---
|
||||
|
||||
## 10. 任务关闭标准
|
||||
|
||||
本 migration 拆分文档完成后,应满足:
|
||||
|
||||
- 已写入本地 `.md`
|
||||
- 路径存在且可读取
|
||||
- 批次顺序与 schema 依赖一致
|
||||
- 每个批次都包含验证检查点
|
||||
- 每个批次都包含回滚说明
|
||||
- 能直接作为后续 migration 编写参考
|
||||
@@ -1,518 +0,0 @@
|
||||
# AI ICT Session Generator 规格说明 v1
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档定义 market session generator 的输入、时间规则、切分逻辑、计算方法、完整性检查和落库输出,用于指导 `market_sessions` 相关开发实现。
|
||||
|
||||
目标:
|
||||
|
||||
- 统一时区与交易日边界
|
||||
- 生成稳定、可重复的 session segmentation
|
||||
- 计算 session high / low / midpoint
|
||||
- 识别数据缺失与异常情况
|
||||
- 结果结构化输出到 `market_sessions`
|
||||
|
||||
适用范围:AI ICT 项目第一阶段 MVP。
|
||||
|
||||
关联文档:
|
||||
|
||||
1. `AI_ICT_总体设计文档_v1.md`
|
||||
2. `AI_ICT_PostgreSQL_Schema_v1.md`
|
||||
3. `AI_ICT_MVP_规则规格书_v1.md`
|
||||
4. `AI_ICT_开发路线图_v1.md`
|
||||
5. `AI_ICT_执行检查清单_v1.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计目标与边界
|
||||
|
||||
### 2.1 目标
|
||||
|
||||
session generator 负责把标准化 candles 按固定会话切分,生成后续规则与执行依赖的 session 数据。
|
||||
|
||||
输出必须支持:
|
||||
|
||||
- session high
|
||||
- session low
|
||||
- midpoint
|
||||
- session 状态
|
||||
- 完整性元数据
|
||||
- 后续 rule-engine 的 session pivot 查询
|
||||
|
||||
### 2.2 不负责的内容
|
||||
|
||||
第一版 session generator 不负责:
|
||||
|
||||
- 自动判断 bias
|
||||
- 自动判断 sweep / MSS / FVG
|
||||
- 直接产生交易信号
|
||||
- 直接生成 execution permission 最终结论
|
||||
|
||||
这些能力由后续 rule-engine / signal-service 基于 `market_sessions` 再加工。
|
||||
|
||||
---
|
||||
|
||||
## 3. 输入与输出
|
||||
|
||||
### 3.1 输入
|
||||
|
||||
最小输入:
|
||||
|
||||
- `instrument_id`
|
||||
- `symbol`
|
||||
- `timezone config`
|
||||
- 标准化 `candles`
|
||||
- 指定 `session_date` 或待重算的日期范围
|
||||
|
||||
candles 最低要求字段:
|
||||
|
||||
- `instrument_id`
|
||||
- `timeframe`
|
||||
- `ts_open`
|
||||
- `ts_close`
|
||||
- `open`
|
||||
- `high`
|
||||
- `low`
|
||||
- `close`
|
||||
|
||||
### 3.2 输出
|
||||
|
||||
输出到 `market_sessions` 表,字段遵循已有 schema:
|
||||
|
||||
- `instrument_id`
|
||||
- `session_date`
|
||||
- `timezone`
|
||||
- `session_code`
|
||||
- `start_ts`
|
||||
- `end_ts`
|
||||
- `high`
|
||||
- `low`
|
||||
- `midpoint`
|
||||
- `status`
|
||||
- `meta`
|
||||
|
||||
---
|
||||
|
||||
## 4. 时间与时区规范
|
||||
|
||||
## 4.1 存储原则
|
||||
|
||||
- 数据库存储统一使用 `timestamptz`
|
||||
- 所有 `start_ts` / `end_ts` 均保存为 UTC 对应绝对时间
|
||||
- session 定义使用业务时区解释,不直接使用服务器本地时区
|
||||
|
||||
## 4.2 第一版默认业务时区
|
||||
|
||||
第一版默认业务时区统一为:
|
||||
|
||||
- `America/New_York`
|
||||
|
||||
原因:
|
||||
|
||||
- 当前默认 session 模型围绕纽约交易时段定义
|
||||
- LONDON / NY_AM / NY_LUNCH / NY_PM 都依赖纽约时区解释
|
||||
- 能统一 crypto 与 XAU 的 session 观察口径
|
||||
|
||||
## 4.3 交易日定义
|
||||
|
||||
第一版 `session_date` 定义为“业务时区下该组 session 所属日期”。
|
||||
|
||||
例如:
|
||||
|
||||
- `ASIA 2026-04-23` 指的是在 `America/New_York` 语义下,属于 2026-04-23 交易日的 ASIA 会话
|
||||
- 虽然其 UTC 时间可能跨自然日,但 `session_date` 不按 UTC 日历切换,而按业务时区日历归属
|
||||
|
||||
## 4.4 DST 处理原则
|
||||
|
||||
必须支持 `America/New_York` 夏令时切换。
|
||||
|
||||
规则:
|
||||
|
||||
1. session 定义始终以业务时区本地时钟解释
|
||||
2. 转换到数据库时,再映射为 UTC `start_ts` / `end_ts`
|
||||
3. 不允许通过手写固定 UTC 偏移硬编码 session
|
||||
4. DST 切换日必须通过时区库计算,不能人工推导
|
||||
|
||||
---
|
||||
|
||||
## 5. Session 定义与切分规则
|
||||
|
||||
### 5.1 默认 session 列表
|
||||
|
||||
第一版采用以下默认 session:
|
||||
|
||||
- ASIA: 20:00 - 00:00
|
||||
- LONDON: 02:00 - 05:00
|
||||
- NY_AM: 09:30 - 11:00
|
||||
- NY_LUNCH: 12:00 - 13:00
|
||||
- NY_PM: 13:30 - 16:00
|
||||
|
||||
时区基准:`America/New_York`。
|
||||
|
||||
### 5.2 Session 边界约定
|
||||
|
||||
统一采用:
|
||||
|
||||
- `start_ts` inclusive
|
||||
- `end_ts` exclusive
|
||||
|
||||
即:
|
||||
|
||||
```text
|
||||
start_ts <= candle.ts_open < end_ts
|
||||
```
|
||||
|
||||
这样可以避免相邻 session 在边界 candle 上重复归属。
|
||||
|
||||
### 5.3 跨午夜处理
|
||||
|
||||
`ASIA: 20:00 - 00:00` 会跨本地日内晚间到午夜边界。
|
||||
|
||||
处理规则:
|
||||
|
||||
- 如果 session 结束时间小于等于开始时间,则视为跨日 session
|
||||
- `end_ts` 应自动落到下一自然日的 00:00
|
||||
- `session_date` 仍归属该交易日定义,不因为 UTC 或本地午夜而拆成两段
|
||||
|
||||
### 5.4 单 candle 归属原则
|
||||
|
||||
一根 candle 在第一版中只能归属一个 session。
|
||||
|
||||
归属依据:
|
||||
|
||||
- 用 `candle.ts_open` 判断所属 session
|
||||
- 不使用 `ts_close` 参与归属判定
|
||||
- 若某 candle 不落在任何 session 区间,则不进入 `market_sessions` 聚合
|
||||
|
||||
### 5.5 推荐运行粒度
|
||||
|
||||
第一版建议以基础数据主时间框架生成 session,推荐:
|
||||
|
||||
- `1m` 为最优
|
||||
- `5m` 可作为退化版本
|
||||
|
||||
原因:
|
||||
|
||||
- NY_AM、NY_LUNCH 等窗口较短
|
||||
- 更细粒度有利于后续 pivot、sweep、session high/low 分析
|
||||
|
||||
---
|
||||
|
||||
## 6. Session 聚合逻辑
|
||||
|
||||
### 6.1 选取数据范围
|
||||
|
||||
给定 `instrument_id + session_date + session_code + timezone`:
|
||||
|
||||
1. 计算业务时区下的本地 `start_local` / `end_local`
|
||||
2. 转换成 UTC `start_ts` / `end_ts`
|
||||
3. 查询满足以下条件的 candles:
|
||||
|
||||
```text
|
||||
instrument_id = ?
|
||||
timeframe = configured_base_tf
|
||||
candle.ts_open >= start_ts
|
||||
candle.ts_open < end_ts
|
||||
```
|
||||
|
||||
### 6.2 high / low 计算
|
||||
|
||||
- `high = max(candle.high)`
|
||||
- `low = min(candle.low)`
|
||||
|
||||
前提:至少存在一根有效 candle。
|
||||
|
||||
### 6.3 midpoint 计算
|
||||
|
||||
第一版 midpoint 定义为:
|
||||
|
||||
```text
|
||||
midpoint = (high + low) / 2
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- midpoint 不是 VWAP
|
||||
- midpoint 不是 open/close 平均值
|
||||
- midpoint 是 session range 的几何中点
|
||||
|
||||
### 6.4 扩展元数据
|
||||
|
||||
建议在 `meta` 写入:
|
||||
|
||||
- `timeframe_used`
|
||||
- `candle_count`
|
||||
- `expected_candle_count`
|
||||
- `completeness_ratio`
|
||||
- `is_dst`
|
||||
- `source`
|
||||
- `generated_at`
|
||||
- `data_gaps`
|
||||
- `rebuild_reason`
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据完整性检查
|
||||
|
||||
### 7.1 检查目标
|
||||
|
||||
session generator 不能只算出 high / low,还必须判断该 session 是否足够完整,避免缺失 candles 导致 pivot 失真。
|
||||
|
||||
### 7.2 期望 K 线数量
|
||||
|
||||
应按 session 时长与 `configured_base_tf` 计算理论 candle 数量。
|
||||
|
||||
示例:
|
||||
|
||||
- `ASIA 20:00 - 00:00`,若 `1m` 粒度,理论 240 根
|
||||
- `LONDON 02:00 - 05:00`,若 `1m` 粒度,理论 180 根
|
||||
- `NY_AM 09:30 - 11:00`,若 `1m` 粒度,理论 90 根
|
||||
|
||||
### 7.3 完整性比率
|
||||
|
||||
```text
|
||||
completeness_ratio = actual_candle_count / expected_candle_count
|
||||
```
|
||||
|
||||
### 7.4 第一版状态建议
|
||||
|
||||
`status` 建议值:
|
||||
|
||||
- `closed`:会话已结束且完整性达标
|
||||
- `partial`:会话已结束但数据缺失
|
||||
- `open`:会话尚未结束,仍在生成中
|
||||
- `empty`:窗口内无有效 candle
|
||||
- `invalid`:时间配置或输入异常,结果不可用
|
||||
|
||||
### 7.5 第一版完整性阈值
|
||||
|
||||
建议:
|
||||
|
||||
- `>= 0.98` 视为 `closed`
|
||||
- `> 0 and < 0.98` 视为 `partial`
|
||||
- `= 0` 视为 `empty`
|
||||
|
||||
是否采用 0.98 可配置,但第一版文档默认使用该值。
|
||||
|
||||
### 7.6 Gap 检查
|
||||
|
||||
除了 candle 数量,还应检查时间连续性:
|
||||
|
||||
- 相邻 `ts_open` 间隔应等于 timeframe 步长
|
||||
- 若出现跳点,则记录 gap 段
|
||||
- 若出现重复 candle,应在上游 `candles` 唯一约束阶段拦截;session generator 不负责去重修复
|
||||
|
||||
建议记录到 `meta.data_gaps`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"expected": "2026-04-23T13:15:00Z",
|
||||
"actual": "2026-04-23T13:17:00Z",
|
||||
"missing_count": 1
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 生成流程规范
|
||||
|
||||
### 8.1 全量重算
|
||||
|
||||
适用场景:
|
||||
|
||||
- 初始化历史数据
|
||||
- 修复 session 定义
|
||||
- 迁移后重建 session 数据
|
||||
|
||||
流程:
|
||||
|
||||
1. 读取日期范围
|
||||
2. 按 instrument 逐日生成 session 窗口
|
||||
3. 查询 candles
|
||||
4. 计算 high / low / midpoint
|
||||
5. 执行完整性检查
|
||||
6. upsert 到 `market_sessions`
|
||||
|
||||
### 8.2 增量更新
|
||||
|
||||
适用场景:
|
||||
|
||||
- 新 candles 持续进入
|
||||
- 当天 session 持续刷新
|
||||
|
||||
流程:
|
||||
|
||||
1. 找到受影响 `instrument + session_date + session_code`
|
||||
2. 重新聚合整个 session 窗口
|
||||
3. 用 upsert 覆盖原记录
|
||||
4. 若会话尚未结束,状态保持 `open`
|
||||
5. 会话结束后再转为 `closed` / `partial` / `empty`
|
||||
|
||||
### 8.3 Upsert 规则
|
||||
|
||||
基于唯一键:
|
||||
|
||||
```text
|
||||
(instrument_id, session_date, session_code, timezone)
|
||||
```
|
||||
|
||||
建议行为:
|
||||
|
||||
- 不做 append-only
|
||||
- 直接 upsert 最新聚合结果
|
||||
- `updated_at` 反映最近刷新时间
|
||||
|
||||
---
|
||||
|
||||
## 9. `market_sessions` 输出规范
|
||||
|
||||
### 9.1 字段映射
|
||||
|
||||
- `instrument_id`:来自上游 instrument
|
||||
- `session_date`:业务时区下的交易日
|
||||
- `timezone`:第一版固定 `America/New_York`
|
||||
- `session_code`:ASIA / LONDON / NY_AM / NY_LUNCH / NY_PM
|
||||
- `start_ts`:本地 session 起点转 UTC
|
||||
- `end_ts`:本地 session 终点转 UTC
|
||||
- `high`:session 期间最高价
|
||||
- `low`:session 期间最低价
|
||||
- `midpoint`:`(high + low) / 2`
|
||||
- `status`:`open` / `closed` / `partial` / `empty` / `invalid`
|
||||
- `meta`:完整性与生成元数据
|
||||
|
||||
### 9.2 约束要求
|
||||
|
||||
写入前必须保证:
|
||||
|
||||
- `high >= low`
|
||||
- 当 `status in ('closed', 'partial', 'open')` 时,`high` 与 `low` 不应为空
|
||||
- 当 `status in ('empty', 'invalid')` 时,`high` / `low` / `midpoint` 可为空
|
||||
|
||||
### 9.3 推荐 meta 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"timeframe_used": "1m",
|
||||
"expected_candle_count": 90,
|
||||
"actual_candle_count": 89,
|
||||
"completeness_ratio": 0.9889,
|
||||
"is_dst": true,
|
||||
"source": "session-generator",
|
||||
"generated_at": "2026-04-23T14:05:00Z",
|
||||
"data_gaps": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 异常与边界情况
|
||||
|
||||
### 10.1 空窗口
|
||||
|
||||
若 session 时间窗口内完全没有 candle:
|
||||
|
||||
- 写入一条 `status = empty` 的记录
|
||||
- `high` / `low` / `midpoint` 留空
|
||||
- 在 `meta` 记录原因
|
||||
|
||||
### 10.2 未结束会话
|
||||
|
||||
若当前时间仍早于 `end_ts`:
|
||||
|
||||
- 允许生成实时中的 session
|
||||
- `status = open`
|
||||
- 可用于盘中监控,但不能视为最终 session pivot
|
||||
|
||||
### 10.3 DST 切换日
|
||||
|
||||
DST 切换导致 UTC 映射变化时:
|
||||
|
||||
- 仍按本地 session 时钟定义生成
|
||||
- 允许 `expected_candle_count` 因本地时间异常而变化,但必须有明确计算依据
|
||||
- `meta.is_dst` 与相关说明必须保留
|
||||
|
||||
### 10.4 上游 candle 重建
|
||||
|
||||
若 candles 历史数据被修复或补齐:
|
||||
|
||||
- 必须触发对应日期 session 重算
|
||||
- 不允许沿用旧 session 结果
|
||||
|
||||
### 10.5 非连续交易市场
|
||||
|
||||
若后续接入存在休市机制的 market:
|
||||
|
||||
- 第一版仍沿用统一 session 规则
|
||||
- 节假日与休市判断暂由外部日历模块处理
|
||||
- session generator 只消费“应该参与生成的日期集合”
|
||||
|
||||
---
|
||||
|
||||
## 11. 验证与验收要求
|
||||
|
||||
### 11.1 功能验证
|
||||
|
||||
至少验证:
|
||||
|
||||
- 能为同一 instrument 连续生成 5 个 session_code
|
||||
- 同一天查询能拿到完整 session 列表
|
||||
- midpoint 计算正确
|
||||
- 重复执行生成任务结果稳定
|
||||
|
||||
### 11.2 数据验证
|
||||
|
||||
至少验证:
|
||||
|
||||
- `session_date` 归属正确
|
||||
- `start_ts` / `end_ts` 对应的业务时区时间正确
|
||||
- `high` / `low` 与原始 candle 对齐
|
||||
- partial / empty 状态能正确触发
|
||||
|
||||
### 11.3 人工抽查
|
||||
|
||||
至少抽查:
|
||||
|
||||
- 一个正常交易日
|
||||
- 一个存在 gap 的交易日
|
||||
- 一个 DST 切换附近交易日
|
||||
|
||||
### 11.4 与检查清单对齐
|
||||
|
||||
任务关闭前至少满足:
|
||||
|
||||
- session generator 规格已写入本地 `.md`
|
||||
- 路径存在
|
||||
- 已检查标题、章节、关键内容
|
||||
- 规格足以指导后续实现与测试
|
||||
|
||||
---
|
||||
|
||||
## 12. 推荐实现顺序
|
||||
|
||||
建议按以下顺序落地:
|
||||
|
||||
1. 固定 `America/New_York` session 定义
|
||||
2. 完成窗口计算与 UTC 转换
|
||||
3. 完成 candles 聚合
|
||||
4. 完成 high / low / midpoint 计算
|
||||
5. 完成 completeness / gap 检查
|
||||
6. 完成 `market_sessions` upsert
|
||||
7. 再补增量重算与异常记录
|
||||
|
||||
这样可以先让 session 数据可落库,再逐步增强完整性判断。
|
||||
|
||||
---
|
||||
|
||||
## 13. 最终结论
|
||||
|
||||
session generator 是全部后续 ICT 结构识别的重要上游。
|
||||
|
||||
第一版实现必须保证三件事:
|
||||
|
||||
1. 时间边界稳定
|
||||
2. 聚合结果可重复
|
||||
3. 缺失数据可识别
|
||||
|
||||
只有在这三点成立后,`market_sessions` 才能可靠支撑 liquidity pools、session pivots、sweep 判定和执行窗口过滤。
|
||||
@@ -1,590 +0,0 @@
|
||||
# AI ICT Sweep 识别规格书 v1
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档定义 AI ICT 项目第一阶段 MVP 的 sweep identification 规则,用于指导:
|
||||
|
||||
- `sweep_events` 识别模块开发
|
||||
- liquidity pool 与 sweep 的交互判定
|
||||
- wick / close 行为判断
|
||||
- displacement、MSS、signal 等下游模块引用
|
||||
- 本地验证、抽样复盘与后续回测
|
||||
|
||||
本文档只覆盖 sweep 的识别与状态维护,不覆盖 displacement、MSS、FVG 的完整规则实现。
|
||||
|
||||
关联文档:
|
||||
|
||||
1. `AI_ICT_MVP_规则规格书_v1.md`
|
||||
2. `AI_ICT_PostgreSQL_Schema_v1.md`
|
||||
3. `AI_ICT_开发路线图_v1.md`
|
||||
4. `AI_ICT_执行检查清单_v1.md`
|
||||
5. `AI_ICT_Dealing_Range_Spec_v1.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
sweep 模块需要满足以下目标:
|
||||
|
||||
1. 只围绕结构化 liquidity pool 识别,不做自由解释
|
||||
2. 能区分 wick-only 刺穿与 close-back-inside 的行为差异
|
||||
3. 能对 sweep 做分层验证,而不是一刀切二元判断
|
||||
4. 能与后续 displacement 事件建立明确链接
|
||||
5. 能维护 sweep 的状态流转,避免同一 pool 被重复误判
|
||||
6. 输出结构化、可回测、可审计
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心定义
|
||||
|
||||
## 3.1 Liquidity Pool
|
||||
|
||||
sweep 的前提是存在已识别的 liquidity pool。
|
||||
|
||||
第一版 pool 来源:
|
||||
|
||||
- `prior_high`
|
||||
- `prior_low`
|
||||
- `equal_highs`
|
||||
- `equal_lows`
|
||||
- `session_high`
|
||||
- `session_low`
|
||||
|
||||
每个 pool 至少有:
|
||||
|
||||
- `side`
|
||||
- `price_low`
|
||||
- `price_high`
|
||||
- `status`
|
||||
- `timeframe`
|
||||
|
||||
## 3.2 Sweep
|
||||
|
||||
sweep 是价格对某一侧 liquidity pool 的刺穿与清理行为。
|
||||
|
||||
第一版中,sweep 必须满足:
|
||||
|
||||
- 有明确目标 pool
|
||||
- 价格对 pool 发生有效交互
|
||||
- 交互后能够输出结构化结果
|
||||
|
||||
## 3.3 Side Swept
|
||||
|
||||
`sweep.side_swept` 表示被清理的是哪一侧流动性:
|
||||
|
||||
- `buy_side`:上方流动性被清理,通常对应扫高
|
||||
- `sell_side`:下方流动性被清理,通常对应扫低
|
||||
|
||||
## 3.4 Wick Sweep
|
||||
|
||||
价格仅用 wick 刺穿 pool 区间,但收盘没有稳定站在区间外,可记为 wick sweep 候选。
|
||||
|
||||
## 3.5 Close Back Inside
|
||||
|
||||
价格刺穿 pool 后,最终收盘重新回到 pool 内侧或原结构区间内,记为 `close_back_inside = true`。
|
||||
|
||||
这通常是第一版更优先关注的 sweep 形态。
|
||||
|
||||
---
|
||||
|
||||
## 4. pool 交互规则
|
||||
|
||||
## 4.1 sweep 必须依赖 pool
|
||||
|
||||
第一版不允许没有 pool 就直接定义 sweep。
|
||||
|
||||
即:
|
||||
|
||||
- 先有 `liquidity_pools`
|
||||
- 再判断 candles 是否与 pool 发生 sweep 交互
|
||||
|
||||
## 4.2 交互方向
|
||||
|
||||
### 扫 buy-side liquidity
|
||||
|
||||
若目标 pool 的 `side = buy_side`,则价格必须向上刺穿 pool:
|
||||
|
||||
- `high` 触及或突破 `pool.price_low`
|
||||
- 并进一步进入或突破 `pool.price_high`
|
||||
|
||||
### 扫 sell-side liquidity
|
||||
|
||||
若目标 pool 的 `side = sell_side`,则价格必须向下刺穿 pool:
|
||||
|
||||
- `low` 触及或跌破 `pool.price_high`
|
||||
- 并进一步进入或跌破 `pool.price_low`
|
||||
|
||||
## 4.3 第一版触发判定
|
||||
|
||||
为了兼容区间型 pool,第一版将“发生交互”定义为:
|
||||
|
||||
### 对 buy_side pool
|
||||
|
||||
候选 candle 满足:
|
||||
|
||||
- `high >= pool.price_low`
|
||||
- 且至少有一部分价格行为进入或突破 `pool.price_high`
|
||||
|
||||
### 对 sell_side pool
|
||||
|
||||
候选 candle 满足:
|
||||
|
||||
- `low <= pool.price_high`
|
||||
- 且至少有一部分价格行为进入或突破 `pool.price_low`
|
||||
|
||||
更简化地说:
|
||||
|
||||
- 上方池要被“扫进去/扫过去”
|
||||
- 下方池要被“打进去/打过去”
|
||||
|
||||
## 4.4 pool 优先级
|
||||
|
||||
若同一 candle 同时与多个 pool 交互,第一版建议按如下优先级选择主 sweep 对象:
|
||||
|
||||
1. 时间更近的 pool
|
||||
2. timeframe 更接近当前规则处理周期的 pool
|
||||
3. `equal_highs / equal_lows` 优先于普通 `prior_high / prior_low`
|
||||
4. session pool 在 session 相关策略中可优先
|
||||
5. 若仍冲突,取离当前价格更近者
|
||||
|
||||
MVP 最小实现建议:
|
||||
|
||||
- 一根 candle 只认一个主 pool
|
||||
- 其余命中记录到 `meta.candidate_pool_ids`
|
||||
|
||||
---
|
||||
|
||||
## 5. wick / close 行为规则
|
||||
|
||||
## 5.1 wick 刺穿
|
||||
|
||||
候选 candle 只要 wick 进入或越过目标 pool,即可形成 sweep 候选。
|
||||
|
||||
例如:
|
||||
|
||||
- 扫 buy-side:`high` 突破 pool 上沿
|
||||
- 扫 sell-side:`low` 跌破 pool 下沿
|
||||
|
||||
## 5.2 close_back_inside
|
||||
|
||||
第一版更强的 sweep 条件是 `close_back_inside = true`。
|
||||
|
||||
### 对 buy_side pool
|
||||
|
||||
候选 candle 扫高后,收盘重新回到 pool 下方或回到结构内部,记为 close back inside。
|
||||
|
||||
### 对 sell_side pool
|
||||
|
||||
候选 candle 扫低后,收盘重新回到 pool 上方或回到结构内部,记为 close back inside。
|
||||
|
||||
## 5.3 close-outside 的处理
|
||||
|
||||
若价格刺穿 pool 后收盘仍留在外部:
|
||||
|
||||
- 可视为已发生 liquidity interaction
|
||||
- 但第一版不优先定义为高质量 sweep
|
||||
- 更可能属于延续突破,而不是清扫后回收
|
||||
|
||||
因此:
|
||||
|
||||
- wick sweep + close outside = 弱候选
|
||||
- wick sweep + close back inside = 更强候选
|
||||
|
||||
## 5.4 多根 K 线确认
|
||||
|
||||
第一版 sweep 的发生时点以首根有效交互 candle 为准,不要求等待多根 K 线才能记录 sweep event。
|
||||
|
||||
但后续验证等级可以引用后续 1~N 根 candle 的行为。
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证分层
|
||||
|
||||
## 6.1 设计原则
|
||||
|
||||
第一版不把所有 sweep 都视为同质量信号,而是分层:
|
||||
|
||||
- L1:弱 sweep
|
||||
- L2:有效 sweep
|
||||
- L3:强 sweep
|
||||
|
||||
## 6.2 L1 弱 sweep
|
||||
|
||||
满足:
|
||||
|
||||
- 与 pool 有明确 wick 交互
|
||||
- 但未出现 close back inside
|
||||
- 或缺少后续 displacement 支持
|
||||
|
||||
用途:
|
||||
|
||||
- 可记录
|
||||
- 默认不直接作为入场依据
|
||||
|
||||
## 6.3 L2 有效 sweep
|
||||
|
||||
满足:
|
||||
|
||||
- 与 pool 发生明确刺穿
|
||||
- `close_back_inside = true`
|
||||
- pool 原始状态为 `untouched`
|
||||
- 没有明显数据缺口或结构冲突
|
||||
|
||||
用途:
|
||||
|
||||
- 可进入后续 displacement / MSS 观察链路
|
||||
|
||||
## 6.4 L3 强 sweep
|
||||
|
||||
满足:
|
||||
|
||||
- 已满足 L2
|
||||
- 在限定窗口内出现有效 displacement
|
||||
- displacement 方向与 sweep 后预期反应一致
|
||||
- 可形成后续 MSS / FVG 链路候选
|
||||
|
||||
用途:
|
||||
|
||||
- 可作为 `SWEEP_MSS_FVG` 模型的高质量前置条件
|
||||
|
||||
## 6.5 第一版最小实现建议
|
||||
|
||||
若系统复杂度有限,可先实现:
|
||||
|
||||
- `weak`
|
||||
- `valid`
|
||||
|
||||
两层,然后把强 sweep 通过 `displacement_after = true` 额外标记。
|
||||
|
||||
---
|
||||
|
||||
## 7. displacement linkage 规则
|
||||
|
||||
## 7.1 sweep 与 displacement 的关系
|
||||
|
||||
sweep 本身只表示流动性被清理;是否具有更高质量,要看后续是否出现 displacement。
|
||||
|
||||
## 7.2 方向一致性
|
||||
|
||||
### 扫 buy-side 之后
|
||||
|
||||
若 sweep 清理的是上方 buy-side liquidity,则高质量后续反应通常是向下 displacement。
|
||||
|
||||
### 扫 sell-side 之后
|
||||
|
||||
若 sweep 清理的是下方 sell-side liquidity,则高质量后续反应通常是向上 displacement。
|
||||
|
||||
## 7.3 displacement 窗口
|
||||
|
||||
第一版建议在 sweep 之后的有限窗口内寻找 displacement:
|
||||
|
||||
- M1 / M5:后续 3~5 根 candle
|
||||
- M15 / H1:后续 2~4 根 candle
|
||||
- H4 及以上:后续 1~3 根 candle
|
||||
|
||||
若窗口内没有有效 displacement:
|
||||
|
||||
- sweep 仍可保留
|
||||
- 但验证等级不升为强 sweep
|
||||
|
||||
## 7.4 链接方式
|
||||
|
||||
若 sweep 已匹配 displacement,则建议记录:
|
||||
|
||||
- `displacement_after = true`
|
||||
- `meta.displacement_event_id`
|
||||
- `meta.displacement_delay_bars`
|
||||
|
||||
若未来正式建 `sweep_events` 表,也建议保留显式 `displacement_event_id` 字段。
|
||||
|
||||
---
|
||||
|
||||
## 8. 状态流转规则
|
||||
|
||||
## 8.1 pool 状态
|
||||
|
||||
pool 原始状态:
|
||||
|
||||
- `untouched`
|
||||
- `swept`
|
||||
- `consumed`
|
||||
|
||||
## 8.2 sweep 对 pool 的影响
|
||||
|
||||
第一版建议:
|
||||
|
||||
- 识别到有效 sweep 后,目标 pool 更新为 `swept`
|
||||
- 若后续结构继续完全消耗该区域,可进一步更新为 `consumed`
|
||||
|
||||
## 8.3 sweep 自身状态建议
|
||||
|
||||
若使用独立 `sweep_events` 存储,建议状态:
|
||||
|
||||
- `candidate`
|
||||
- `weak`
|
||||
- `valid`
|
||||
- `confirmed`
|
||||
- `invalidated`
|
||||
- `archived`
|
||||
|
||||
### 含义
|
||||
|
||||
- `candidate`:仅发生初步 pool interaction
|
||||
- `weak`:发生 wick sweep,但不够强
|
||||
- `valid`:close back inside 成立
|
||||
- `confirmed`:已获得 displacement 或后续链路确认
|
||||
- `invalidated`:后续价格行为证明该 sweep 不再有参考意义
|
||||
- `archived`:历史保留
|
||||
|
||||
## 8.4 最小状态机
|
||||
|
||||
MVP 第一版若要降复杂度,至少支持:
|
||||
|
||||
- `weak`
|
||||
- `valid`
|
||||
- `confirmed`
|
||||
|
||||
并允许:
|
||||
|
||||
```text
|
||||
weak -> valid -> confirmed
|
||||
weak -> invalidated
|
||||
valid -> invalidated
|
||||
confirmed -> archived
|
||||
```
|
||||
|
||||
## 8.5 防重规则
|
||||
|
||||
同一 pool 在未重置前,不应被重复生成多条同向主 sweep 事件。
|
||||
|
||||
第一版建议:
|
||||
|
||||
- 同一 `pool_id + direction_window` 只保留一条主 sweep
|
||||
- 若后续出现更高质量版本,可更新状态或新增更高版本并归档旧记录
|
||||
|
||||
---
|
||||
|
||||
## 9. 边界与异常情况
|
||||
|
||||
## 9.1 只触及不刺穿
|
||||
|
||||
若价格只触碰 pool 边缘但没有进入区间,不记 sweep。
|
||||
|
||||
## 9.2 同时扫上下两侧
|
||||
|
||||
极端大波动 candle 可能同时扫到上下两侧 pool。
|
||||
|
||||
第一版处理:
|
||||
|
||||
- 不在同一根 candle 上同时生成两个主 sweep
|
||||
- 标记为冲突场景
|
||||
- `meta.conflict = 'dual_side_sweep'`
|
||||
- 默认不作为高质量 sweep
|
||||
|
||||
## 9.3 已被 swept 的 pool
|
||||
|
||||
若 pool 已是 `swept` 状态,第一版默认不重复生成新的主 sweep。
|
||||
|
||||
除非:
|
||||
|
||||
- 明确发生新一轮结构重置
|
||||
- 或 pool 被重新生成了新 id
|
||||
|
||||
## 9.4 数据缺口
|
||||
|
||||
若 sweep 发生窗口存在明显缺失 candle:
|
||||
|
||||
- 可记录 candidate
|
||||
- 但不升级为高质量 sweep
|
||||
- `meta.data_gap = true`
|
||||
|
||||
## 9.5 假突破与延续突破
|
||||
|
||||
若刺穿后持续单边离开 pool,且没有 close back inside:
|
||||
|
||||
- 更偏向 breakout / continuation
|
||||
- 仍可记为弱 interaction
|
||||
- 默认不视为有效 sweep
|
||||
|
||||
## 9.6 多个 timeframe 冲突
|
||||
|
||||
若低周期识别 sweep,但高周期叙事不支持:
|
||||
|
||||
- sweep 事件本身仍可记录
|
||||
- 是否进入 signal 链路交给 bias / structure 层过滤
|
||||
|
||||
---
|
||||
|
||||
## 10. 存储模型
|
||||
|
||||
## 10.1 MVP 第一版建议
|
||||
|
||||
虽然当前 schema 文档未正式定义 `sweep_events` 表,但 sweep 属于核心信号层,建议新增独立存储模型,而不是只塞进 JSON。
|
||||
|
||||
建议新增 `sweep_events` 表,至少包含:
|
||||
|
||||
- `instrument_id`
|
||||
- `timeframe`
|
||||
- `pool_id`
|
||||
- `side_swept`
|
||||
- `sweep_high`
|
||||
- `sweep_low`
|
||||
- `close_back_inside`
|
||||
- `validation_tier`
|
||||
- `status`
|
||||
- `ts`
|
||||
- `meta`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
## 10.2 字段语义
|
||||
|
||||
- `pool_id`:被扫 liquidity pool
|
||||
- `side_swept`:buy_side / sell_side
|
||||
- `sweep_high`:本次 sweep 涉及的最高价格
|
||||
- `sweep_low`:本次 sweep 涉及的最低价格
|
||||
- `close_back_inside`:是否收回结构内部
|
||||
- `validation_tier`:weak / valid / strong
|
||||
- `status`:事件当前状态
|
||||
- `ts`:首个成立 sweep 的 candle 时间
|
||||
|
||||
## 10.3 meta 建议内容
|
||||
|
||||
适合放入 `meta`:
|
||||
|
||||
- `candidate_pool_ids`
|
||||
- `pool_type`
|
||||
- `close_outside`
|
||||
- `displacement_event_id`
|
||||
- `displacement_delay_bars`
|
||||
- `data_gap`
|
||||
- `conflict`
|
||||
- `selection_reason`
|
||||
- `recalc_batch_id`
|
||||
|
||||
不应只放在 `meta`:
|
||||
|
||||
- `pool_id`
|
||||
- `side_swept`
|
||||
- `close_back_inside`
|
||||
- `validation_tier`
|
||||
- `status`
|
||||
- `ts`
|
||||
|
||||
## 10.4 若暂不建独立表
|
||||
|
||||
若第一版暂时不能新增 `sweep_events` 表,可临时把 sweep 结果写入:
|
||||
|
||||
- `trade_signals.evidence`
|
||||
- 或规则引擎中间结果存储
|
||||
|
||||
但这只应作为过渡方案,不应作为长期主方案。
|
||||
|
||||
---
|
||||
|
||||
## 11. 处理流程建议
|
||||
|
||||
## 11.1 基础识别流程
|
||||
|
||||
1. 读取当前 timeframe 的 active liquidity pools
|
||||
2. 检查新 candle 是否与某个 pool 发生交互
|
||||
3. 判断是否进入/突破 pool 区间
|
||||
4. 记录 wick 行为与 close 行为
|
||||
5. 生成 candidate / weak / valid sweep
|
||||
6. 在后续窗口中检查 displacement
|
||||
7. 更新 validation tier 与 status
|
||||
8. 同步更新目标 pool 状态
|
||||
|
||||
## 11.2 最小 MVP 实现
|
||||
|
||||
建议先做:
|
||||
|
||||
1. pool interaction 判定
|
||||
2. wick 与 close back inside 判定
|
||||
3. weak / valid 两层验证
|
||||
4. displacement_after 标记
|
||||
5. pool 状态从 untouched 到 swept 的更新
|
||||
|
||||
再增强:
|
||||
|
||||
- 独立 `sweep_events` 表
|
||||
- 更完整状态机
|
||||
- 多 pool 冲突管理
|
||||
- 与 MSS / FVG 的强绑定引用
|
||||
|
||||
---
|
||||
|
||||
## 12. 验证检查点
|
||||
|
||||
## 12.1 功能验证
|
||||
|
||||
- [ ] 能从已有 liquidity pool 中识别 sweep
|
||||
- [ ] 同一批输入重复运行结果一致
|
||||
- [ ] wick 行为与 close 行为能正确区分
|
||||
- [ ] displacement linkage 可正确标记
|
||||
|
||||
## 12.2 规则验证
|
||||
|
||||
- [ ] 没有 pool 不会生成正式 sweep
|
||||
- [ ] 只触碰不进入 pool 区间不会误判为 sweep
|
||||
- [ ] close back inside 成立时会升级验证等级
|
||||
- [ ] close outside 默认不会误判为高质量 sweep
|
||||
- [ ] pool 已 swept 时不会重复生成主 sweep
|
||||
|
||||
## 12.3 状态验证
|
||||
|
||||
- [ ] sweep 生成后 pool 状态可更新为 `swept`
|
||||
- [ ] 高质量 sweep 可升级为 confirmed
|
||||
- [ ] 无效后续行为可把 sweep 标记为 invalidated
|
||||
- [ ] 历史 sweep 可归档但不丢失
|
||||
|
||||
## 12.4 存储验证
|
||||
|
||||
- [ ] `pool_id` 能正确关联 liquidity pool
|
||||
- [ ] `side_swept` 与 pool.side 逻辑一致
|
||||
- [ ] `validation_tier` 与 `status` 可正确查询
|
||||
- [ ] `meta` 能记录 displacement 和异常信息
|
||||
|
||||
## 12.5 人工抽样验证
|
||||
|
||||
每个核心 timeframe 至少抽查:
|
||||
|
||||
- 1 个扫 buy-side 的有效样例
|
||||
- 1 个扫 sell-side 的有效样例
|
||||
- 1 个 wick-only 弱 sweep 样例
|
||||
- 1 个 close back inside 的有效 sweep 样例
|
||||
- 1 个后续带 displacement 的强样例
|
||||
- 1 个假突破但非有效 sweep 的反例
|
||||
|
||||
## 12.6 与路线图对齐的完成标准
|
||||
|
||||
满足以下条件才算完成:
|
||||
|
||||
- 能识别流动性刺穿
|
||||
- 能识别 close back inside
|
||||
- 能区分弱 sweep 与有效 sweep
|
||||
- 可作为 displacement / MSS / signal 的上游输入
|
||||
|
||||
---
|
||||
|
||||
## 13. MVP 范围内不做的事
|
||||
|
||||
第一版暂不包含:
|
||||
|
||||
- 基于订单流或逐笔成交的微观 sweep 识别
|
||||
- AI 自由叙事判定 sweep
|
||||
- 多市场 SMT 联动 sweep 作为核心条件
|
||||
- 同一事件跨多个 timeframe 自动合并
|
||||
- 复杂置信度模型打分
|
||||
|
||||
---
|
||||
|
||||
## 14. 推荐的最小实现顺序
|
||||
|
||||
1. 先实现 pool interaction
|
||||
2. 再实现 wick / close back inside 判定
|
||||
3. 再实现 weak / valid 分层
|
||||
4. 再实现 displacement_after 标记
|
||||
5. 最后补 sweep 独立存储与完整状态机
|
||||
|
||||
这样可以先满足阶段 3 的最小可用目标,再逐步增强。
|
||||
@@ -1,491 +0,0 @@
|
||||
# AI ICT Swing Identification 规格说明 v1
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档定义 swing identification 模块的识别规则、强度分层、确认时机、参数化方式、边界情况、落库结构与验证要求,用于指导 `swings` 表和相关规则引擎实现。
|
||||
|
||||
目标:
|
||||
|
||||
- 统一 swing high / swing low 判定口径
|
||||
- 建立 ST / IT / LT 强度层级
|
||||
- 明确 provisional / confirmed 确认时机
|
||||
- 支持按 timeframe 参数化
|
||||
- 为 dealing range、liquidity、MSS/MSB 提供稳定上游结构点
|
||||
|
||||
适用范围:AI ICT 项目第一阶段 MVP。
|
||||
|
||||
关联文档:
|
||||
|
||||
1. `AI_ICT_总体设计文档_v1.md`
|
||||
2. `AI_ICT_PostgreSQL_Schema_v1.md`
|
||||
3. `AI_ICT_MVP_规则规格书_v1.md`
|
||||
4. `AI_ICT_开发路线图_v1.md`
|
||||
5. `AI_ICT_执行检查清单_v1.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计目标与边界
|
||||
|
||||
### 2.1 目标
|
||||
|
||||
swing identification 负责从标准化 candles 中提取结构高低点,作为后续 dealing range、liquidity pools、structure events 的基础输入。
|
||||
|
||||
输出必须支持:
|
||||
|
||||
- `swing_high`
|
||||
- `swing_low`
|
||||
- `ST / IT / LT` 强度分层
|
||||
- provisional / confirmed 状态
|
||||
- 多 timeframe 稳定重算
|
||||
|
||||
### 2.2 不负责的内容
|
||||
|
||||
第一版 swing 模块不负责:
|
||||
|
||||
- 直接计算 dealing range
|
||||
- 直接判定 liquidity pool
|
||||
- 直接判定 MSB / MSS
|
||||
- 直接生成 trade signal
|
||||
|
||||
这些能力依赖 swing 结果,但由后续模块单独实现。
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心概念
|
||||
|
||||
### 3.1 Swing High
|
||||
|
||||
`swing_high` 指某一根 candle 的高点,相对于其左右观察窗口内的其他 candles,构成局部显著高点。
|
||||
|
||||
### 3.2 Swing Low
|
||||
|
||||
`swing_low` 指某一根 candle 的低点,相对于其左右观察窗口内的其他 candles,构成局部显著低点。
|
||||
|
||||
### 3.3 Left / Right Bars
|
||||
|
||||
- `left_bars`:候选 pivot 左侧参与比较的 bars 数量
|
||||
- `right_bars`:候选 pivot 右侧参与比较的 bars 数量
|
||||
|
||||
### 3.4 Provisional / Confirmed
|
||||
|
||||
- `provisional`:左侧条件满足,但右侧确认窗口尚未走完
|
||||
- `confirmed`:左右侧窗口都满足条件,pivot 已确认
|
||||
|
||||
---
|
||||
|
||||
## 4. 输入与输出
|
||||
|
||||
### 4.1 输入
|
||||
|
||||
最小输入:
|
||||
|
||||
- `instrument_id`
|
||||
- `timeframe`
|
||||
- 标准化 `candles`
|
||||
- 当前 swing 参数配置
|
||||
- 待重算时间范围
|
||||
|
||||
candles 最低要求字段:
|
||||
|
||||
- `ts_open`
|
||||
- `high`
|
||||
- `low`
|
||||
- `close`
|
||||
|
||||
### 4.2 输出
|
||||
|
||||
输出到 `swings` 表,遵循既有 schema:
|
||||
|
||||
- `instrument_id`
|
||||
- `timeframe`
|
||||
- `kind`
|
||||
- `strength`
|
||||
- `price`
|
||||
- `ts`
|
||||
- `left_bars`
|
||||
- `right_bars`
|
||||
- `confirmed`
|
||||
- `meta`
|
||||
|
||||
---
|
||||
|
||||
## 5. Pivot 判定规则
|
||||
|
||||
### 5.1 基础判定窗口
|
||||
|
||||
给定某根 candle `i`,若要判断它是否为 pivot:
|
||||
|
||||
- 左侧比较范围:`i - left_bars` 到 `i - 1`
|
||||
- 右侧比较范围:`i + 1` 到 `i + right_bars`
|
||||
|
||||
### 5.2 Swing High 判定
|
||||
|
||||
第一版推荐规则:
|
||||
|
||||
```text
|
||||
candle[i].high > max(left window highs)
|
||||
and
|
||||
candle[i].high >= max(right window highs)
|
||||
```
|
||||
|
||||
即:
|
||||
|
||||
- 对左侧使用严格大于
|
||||
- 对右侧使用大于等于
|
||||
|
||||
这样做的目的:
|
||||
|
||||
- 避免在左侧已有同价高点时重复生成 pivot
|
||||
- 在右侧出现相同高点时,优先保留更早出现的候选点
|
||||
|
||||
### 5.3 Swing Low 判定
|
||||
|
||||
第一版推荐规则:
|
||||
|
||||
```text
|
||||
candle[i].low < min(left window lows)
|
||||
and
|
||||
candle[i].low <= min(right window lows)
|
||||
```
|
||||
|
||||
即:
|
||||
|
||||
- 对左侧使用严格小于
|
||||
- 对右侧使用小于等于
|
||||
|
||||
### 5.4 使用 wick 还是 close
|
||||
|
||||
第一版 swing 判定统一基于:
|
||||
|
||||
- high pivot 使用 `high`
|
||||
- low pivot 使用 `low`
|
||||
|
||||
不使用 close 来定义 swing 本身。
|
||||
|
||||
原因:
|
||||
|
||||
- swing 的本质是结构极值点
|
||||
- wick 对流动性与局部极值更敏感
|
||||
- close 更适合后续 displacement / MSS 过滤,而不是 swing 原始识别
|
||||
|
||||
---
|
||||
|
||||
## 6. ST / IT / LT 强度层级
|
||||
|
||||
### 6.1 ST 定义
|
||||
|
||||
`ST`(Short-Term)是最基础的局部 pivot。
|
||||
|
||||
第一版默认:
|
||||
|
||||
- 先识别全部 `ST` swings
|
||||
- 所有更高层级都建立在 `ST` 基础上
|
||||
|
||||
### 6.2 IT 定义
|
||||
|
||||
`IT`(Intermediate-Term)是对 `ST` 的再筛选。
|
||||
|
||||
推荐规则:
|
||||
|
||||
- 某个 `ST swing_high` 若高于其相邻的前后 `ST swing_high`,可升级为 `IT swing_high`
|
||||
- 某个 `ST swing_low` 若低于其相邻的前后 `ST swing_low`,可升级为 `IT swing_low`
|
||||
|
||||
也就是:
|
||||
|
||||
- `IT` 不是直接从 candles 识别
|
||||
- `IT` 从已确认的 `ST` 序列递归提取
|
||||
|
||||
### 6.3 LT 定义
|
||||
|
||||
`LT`(Long-Term)是对 `IT` 的再筛选。
|
||||
|
||||
推荐规则:
|
||||
|
||||
- 某个 `IT swing_high` 若高于其相邻的前后 `IT swing_high`,升级为 `LT swing_high`
|
||||
- 某个 `IT swing_low` 若低于其相邻的前后 `IT swing_low`,升级为 `LT swing_low`
|
||||
|
||||
### 6.4 层级生成顺序
|
||||
|
||||
统一采用:
|
||||
|
||||
```text
|
||||
candles -> ST -> IT -> LT
|
||||
```
|
||||
|
||||
不允许跳层直接从 candles 识别 LT。
|
||||
|
||||
### 6.5 层级意义
|
||||
|
||||
- `ST`:局部结构、短期波动、触发细节
|
||||
- `IT`:中层结构、主工作区间候选
|
||||
- `LT`:大级别结构、主 draw 与大范围边界参考
|
||||
|
||||
---
|
||||
|
||||
## 7. 确认时机
|
||||
|
||||
### 7.1 ST 确认
|
||||
|
||||
某个 ST pivot 只有在右侧 `right_bars` 数量的 candles 都已经存在后,才能标记 `confirmed = true`。
|
||||
|
||||
因此:
|
||||
|
||||
- 当前最新若干根 bars 附近的 pivot 只能是 provisional
|
||||
- 不能提前把未走完右窗的点当成 confirmed
|
||||
|
||||
### 7.2 IT / LT 确认
|
||||
|
||||
`IT` / `LT` 的确认建立在下一级 swing 已确认的前提上。
|
||||
|
||||
即:
|
||||
|
||||
- 未确认的 ST 不能用于确认 IT
|
||||
- 未确认的 IT 不能用于确认 LT
|
||||
|
||||
### 7.3 实时与历史差异
|
||||
|
||||
- 历史回算时,可一次性给出完整 confirmed 结果
|
||||
- 实时增量时,最新区域必须允许 provisional 存在
|
||||
- 后续新增 candles 到来后,允许 provisional 升级为 confirmed
|
||||
|
||||
---
|
||||
|
||||
## 8. Timeframe 参数化
|
||||
|
||||
### 8.1 参数化原则
|
||||
|
||||
不同 timeframe 的波动噪声不同,pivot 窗口必须参数化。
|
||||
|
||||
建议按 timeframe 单独配置:
|
||||
|
||||
- `st_left_bars`
|
||||
- `st_right_bars`
|
||||
- `it_neighbor_span`
|
||||
- `lt_neighbor_span`
|
||||
- `min_price_distance`(可选)
|
||||
- `min_bar_distance`(可选)
|
||||
|
||||
### 8.2 第一版建议参数
|
||||
|
||||
以下为建议默认值,后续可调:
|
||||
|
||||
```text
|
||||
M1: ST = 2/2
|
||||
M5: ST = 2/2
|
||||
M15: ST = 3/3
|
||||
H1: ST = 3/3
|
||||
H4: ST = 4/4
|
||||
D1: ST = 5/5
|
||||
```
|
||||
|
||||
IT / LT 默认可先使用“前后各 1 个同级 swing 比较”的方式生成。
|
||||
|
||||
### 8.3 噪声过滤参数
|
||||
|
||||
第一版可选增加两类过滤:
|
||||
|
||||
1. `min_bar_distance`
|
||||
- 两个同向 swing 之间至少相隔若干 bars
|
||||
|
||||
2. `min_price_distance`
|
||||
- 两个同向 swing 之间至少相隔最小价格距离或最小 ATR 比例
|
||||
|
||||
若未启用过滤,第一版仍可先按纯 pivot 规则运行。
|
||||
|
||||
---
|
||||
|
||||
## 9. 边界与异常情况
|
||||
|
||||
### 9.1 数据不足
|
||||
|
||||
若当前窗口不足以覆盖 `left_bars + right_bars + 1`:
|
||||
|
||||
- 不生成 confirmed swing
|
||||
- 可选择不输出,或仅输出 provisional
|
||||
- 第一版更建议不输出,避免过早污染结构
|
||||
|
||||
### 9.2 相等高点 / 相等低点
|
||||
|
||||
当多个相邻 candles 出现相同 high 或 low:
|
||||
|
||||
- 按 5.2 / 5.3 的不对称比较规则处理
|
||||
- 若仍无法唯一确定,优先保留更早出现的 candle
|
||||
- 同一价格平台不应生成多个完全重复的同级 swing
|
||||
|
||||
### 9.3 外包 K 线
|
||||
|
||||
若某一根 candle 同时包住左右多根 bars:
|
||||
|
||||
- 只要满足 pivot 条件,仍允许成为 swing
|
||||
- 不因实体大小过大而自动剔除
|
||||
- 但可在 `meta` 中记录 bar range 异常信息
|
||||
|
||||
### 9.4 极端噪声行情
|
||||
|
||||
若短时间内产生大量相邻 swings:
|
||||
|
||||
- 先按规则落地 ST
|
||||
- 通过 IT / LT 递归筛选降低噪声
|
||||
- 如仍过密,再启用 `min_bar_distance` / `min_price_distance`
|
||||
|
||||
### 9.5 上游 candle 修复
|
||||
|
||||
若 candles 历史数据被补齐、修正或重采样:
|
||||
|
||||
- 必须触发对应 timeframe swing 重算
|
||||
- 不允许保留旧 swing 结果
|
||||
|
||||
### 9.6 跨 timeframe 独立性
|
||||
|
||||
不同 timeframe 的 swing 独立生成:
|
||||
|
||||
- M15 swing 不直接映射为 H1 swing
|
||||
- 高 timeframe 必须基于该 timeframe 自己的 candles 计算
|
||||
|
||||
---
|
||||
|
||||
## 10. 存储模型
|
||||
|
||||
### 10.1 `swings` 字段映射
|
||||
|
||||
- `instrument_id`:所属交易品种
|
||||
- `timeframe`:对应时间框架
|
||||
- `kind`:`swing_high` / `swing_low`
|
||||
- `strength`:`ST` / `IT` / `LT`
|
||||
- `price`:pivot 价格;high 用 candle.high,low 用 candle.low
|
||||
- `ts`:pivot candle 的时间戳,建议使用 `ts_open`
|
||||
- `left_bars`:左窗参数
|
||||
- `right_bars`:右窗参数
|
||||
- `confirmed`:是否确认
|
||||
- `meta`:额外说明
|
||||
|
||||
### 10.2 推荐 meta 内容
|
||||
|
||||
建议在 `meta` 中记录:
|
||||
|
||||
- `source_timeframe`
|
||||
- `pivot_index`
|
||||
- `calc_version`
|
||||
- `is_provisional`
|
||||
- `neighbor_refs`
|
||||
- `price_distance`
|
||||
- `bar_distance`
|
||||
- `rebuild_reason`
|
||||
|
||||
### 10.3 唯一性建议
|
||||
|
||||
第一版建议逻辑唯一键为:
|
||||
|
||||
```text
|
||||
(instrument_id, timeframe, strength, kind, ts)
|
||||
```
|
||||
|
||||
如暂不建数据库唯一约束,也应在写入层保证 upsert 口径一致。
|
||||
|
||||
### 10.4 更新策略
|
||||
|
||||
建议:
|
||||
|
||||
- 历史 confirmed 区域使用幂等 upsert
|
||||
- 最新 provisional 区域允许被覆盖更新
|
||||
- 若同一 `ts` 的 provisional 后续转 confirmed,应更新而不是插入新记录
|
||||
|
||||
---
|
||||
|
||||
## 11. 生成流程规范
|
||||
|
||||
### 11.1 历史全量重算
|
||||
|
||||
流程:
|
||||
|
||||
1. 读取指定 `instrument + timeframe + date range`
|
||||
2. 生成 ST 候选并确认
|
||||
3. 从 ST 提取 IT
|
||||
4. 从 IT 提取 LT
|
||||
5. 写入 `swings`
|
||||
6. 对结果做去重与顺序检查
|
||||
|
||||
### 11.2 增量刷新
|
||||
|
||||
流程:
|
||||
|
||||
1. 只拉取受影响的最近窗口 candles
|
||||
2. 重算尾部 ST
|
||||
3. 级联刷新受影响的 IT / LT
|
||||
4. upsert 到 `swings`
|
||||
|
||||
### 11.3 结果顺序要求
|
||||
|
||||
同一 `instrument + timeframe + strength` 下:
|
||||
|
||||
- swings 必须按时间可排序
|
||||
- 不要求 high / low 严格交替,但若连续同向 swing 过多,应能被后续过滤发现
|
||||
|
||||
---
|
||||
|
||||
## 12. 验证与验收要求
|
||||
|
||||
### 12.1 功能验证
|
||||
|
||||
至少验证:
|
||||
|
||||
- 能按 timeframe 产出稳定 swing 列表
|
||||
- 同一份 candles 重复计算结果一致
|
||||
- 最新尾部区域能正确区分 provisional / confirmed
|
||||
- ST / IT / LT 之间数量呈递减关系
|
||||
|
||||
### 12.2 数据验证
|
||||
|
||||
至少验证:
|
||||
|
||||
- `swing_high` 的价格等于对应 candle.high
|
||||
- `swing_low` 的价格等于对应 candle.low
|
||||
- `confirmed` 只在右侧窗口完成后出现
|
||||
- 不同 timeframe 结果互不污染
|
||||
|
||||
### 12.3 人工抽查
|
||||
|
||||
至少抽查:
|
||||
|
||||
- 一个趋势行情样本
|
||||
- 一个震荡行情样本
|
||||
- 一个 equal highs / equal lows 附近样本
|
||||
- 一个最新尾部存在 provisional 的样本
|
||||
|
||||
### 12.4 与下游一致性检查
|
||||
|
||||
至少确认:
|
||||
|
||||
- dealing range 可引用 swing 结果
|
||||
- liquidity pools 可从 prior highs / lows 提取候选
|
||||
- MSS / MSB 使用的参考 swing 与图表直觉一致
|
||||
|
||||
---
|
||||
|
||||
## 13. 推荐实现顺序
|
||||
|
||||
建议按以下顺序实现:
|
||||
|
||||
1. 完成 ST 基础 pivot 识别
|
||||
2. 完成 confirmed / provisional 状态管理
|
||||
3. 完成 ST 结果幂等落库
|
||||
4. 完成 IT 递归提取
|
||||
5. 完成 LT 递归提取
|
||||
6. 再补噪声过滤参数
|
||||
|
||||
这样可以先保证下游有可用的基础结构点,再逐步增加层级复杂度。
|
||||
|
||||
---
|
||||
|
||||
## 14. 最终结论
|
||||
|
||||
swing identification 是全部结构分析的基础层。
|
||||
|
||||
第一版必须保证四件事:
|
||||
|
||||
1. pivot 口径明确
|
||||
2. 确认时机严格
|
||||
3. 多层级生成可重复
|
||||
4. 写入结果可稳定重算
|
||||
|
||||
只有这四点成立后,dealing range、liquidity pools、MSS/MSB 才有稳定可靠的结构基础。
|
||||
@@ -0,0 +1,229 @@
|
||||
# AI ICT 当前开发方案 v2
|
||||
|
||||
## 1. 方案目标
|
||||
|
||||
本文档用于替代已经过时的“项目启动期”规划文档,在当前代码现实下重新定义项目需求边界、优先级和开发顺序。
|
||||
|
||||
当前项目不是从零开始,而是已经形成两条主线:
|
||||
|
||||
- 交易主线:已打通从市场数据到回测、报告、paper/semi-auto execution 的最小闭环。
|
||||
- 训练主线:已形成对外产品模块,公开成熟度通过 M40。
|
||||
|
||||
因此,新方案必须以“质量增强、状态收敛、可验证推进”为主,而不是重复基础搭建。
|
||||
|
||||
## 2. 当前项目现实
|
||||
|
||||
### 2.1 交易主线现状
|
||||
|
||||
已具备:
|
||||
|
||||
- `OKX -> candles -> market_sessions -> swings -> dealing_ranges -> liquidity_pools -> sweep_events -> displacement_events -> MSS -> FVG -> bias -> signals -> backtest`
|
||||
- execution / settlement / report / dashboard 的第一版闭环
|
||||
- offline checks、质量门槛、脚本静态检查和无人流程入口
|
||||
|
||||
当前主要问题不再是“有没有模块”,而是:
|
||||
|
||||
1. 回测是否真正无未来函数、足够接近真实决策过程。
|
||||
2. 结构状态是否足够稳定,便于 signal 和 training 复用。
|
||||
3. execution / report 是否已经建立可信的后验解释链。
|
||||
|
||||
### 2.2 训练主线现状
|
||||
|
||||
已具备:
|
||||
|
||||
- 公开 API 路由
|
||||
- Training dashboard 面板
|
||||
- offline package import/export
|
||||
- public error contract
|
||||
- privacy contract coverage
|
||||
- 文档成熟度通过 `M40`
|
||||
|
||||
结论:
|
||||
|
||||
- Training 不是空白模块。
|
||||
- 当前不应优先再扩大量新功能。
|
||||
- Training 的新增能力应建立在交易规则语义稳定的前提上。
|
||||
|
||||
## 3. 对旧方案的判断
|
||||
|
||||
### 3.1 仍然合理、继续保留的部分
|
||||
|
||||
以下原则继续成立:
|
||||
|
||||
1. 双主线边界必须严格保持:Training 只读消费 Trading,上游和下游不能反向污染。
|
||||
2. 先规则、再信号、再回测、再执行的工程顺序是对的。
|
||||
3. offline validation 和可回滚原则必须继续保持。
|
||||
4. 当前阶段仍不应进入真实全自动交易。
|
||||
|
||||
### 3.2 需要明确修正的部分
|
||||
|
||||
以下旧文档判断已不再合理:
|
||||
|
||||
1. 把项目视作“刚启动,需要先建骨架和第一批表”的规划已过期。
|
||||
2. 把 Training 当作尚未落地的 MVP 0->1 项目已不准确;它已是已交付模块,当前应以维护和输入质量提升为主。
|
||||
3. 继续优先补更多 planning/checklist 文档,不如直接提升交易主线的可验证质量。
|
||||
4. 在 replay/backtest 可信度不足前,继续扩展 execution 自动化或大型 Training 新功能,收益低且风险高。
|
||||
|
||||
## 4. 新的开发优先级
|
||||
|
||||
当前建议优先级:
|
||||
|
||||
### P0 文档治理与当前入口收敛
|
||||
|
||||
目标:
|
||||
|
||||
- 清理重复、过期、历史快照文档
|
||||
- 只保留当前有效入口
|
||||
- 固定 active / archive 边界
|
||||
|
||||
状态:
|
||||
|
||||
- 本轮执行
|
||||
|
||||
### P1 交易主线回测真实性
|
||||
|
||||
目标:
|
||||
|
||||
- 让回测从“结果可算”提升到“过程可信”
|
||||
|
||||
重点:
|
||||
|
||||
1. 把 replay/no-lookahead 能力接入 backtest 路径
|
||||
2. 明确 frame、decision point、confirmation window 的统一口径
|
||||
3. 让 backtest 结果可解释地对应当时可见的信息集合
|
||||
|
||||
为什么先做这个:
|
||||
|
||||
- 这是交易主线当前最大真实性缺口
|
||||
- 也是 Training 案例生成和评分可信度的上游基础
|
||||
|
||||
### P2 交易规则状态质量
|
||||
|
||||
目标:
|
||||
|
||||
- 提高结构事件的稳定性和下游可用性
|
||||
|
||||
重点:
|
||||
|
||||
1. liquidity equal highs / lows merge 与去重
|
||||
2. MSS accepted / rejected / invalidated 语义收敛
|
||||
3. FVG mitigation / invalidation / gap warning 元数据向下游传播
|
||||
4. signal evidence completeness 与 session gate 继续收紧
|
||||
|
||||
### P3 执行与报告语义闭环
|
||||
|
||||
目标:
|
||||
|
||||
- 让 execution 和 report 更贴近真实后验复盘
|
||||
|
||||
重点:
|
||||
|
||||
1. position lifecycle / settlement 语义继续细化
|
||||
2. backtest / execution / report 的指标口径统一
|
||||
3. 增加按 session、weekday、symbol 的质量拆分统计
|
||||
|
||||
### P4 Training 维护与小步增强
|
||||
|
||||
目标:
|
||||
|
||||
- 保持 M40 公开契约稳定,同时只做低风险增强
|
||||
|
||||
重点:
|
||||
|
||||
1. 保持 dashboard / API / package / privacy / error contract 绿色
|
||||
2. 优先做 case quality 提升,而不是扩更多前台功能
|
||||
3. 只有当 P1-P2 稳定后,才扩展新的自动 case generator 能力
|
||||
|
||||
## 5. 新阶段划分
|
||||
|
||||
## 阶段 A:文档治理
|
||||
|
||||
交付物:
|
||||
|
||||
- 新文档索引
|
||||
- archive 结构
|
||||
- 过期/重复/历史文档归档
|
||||
|
||||
验收:
|
||||
|
||||
- `docs/` 根目录阅读路径清晰
|
||||
- active 文档与 archive 文档边界明确
|
||||
|
||||
## 阶段 B:Replay-backed Backtest
|
||||
|
||||
交付物:
|
||||
|
||||
- replay/no-lookahead 路径接入 backtest 服务或独立 backtest 入口
|
||||
- 定向单元测试
|
||||
- offline 验证命令
|
||||
|
||||
验收:
|
||||
|
||||
- 回测在 decision point 之后不读取未来信息
|
||||
- replay 结果可输出质量门槛与失败原因
|
||||
|
||||
## 阶段 C:Rule State Hardening
|
||||
|
||||
交付物:
|
||||
|
||||
- liquidity / MSS / FVG 状态增强
|
||||
- signal evidence / session gating 补强
|
||||
|
||||
验收:
|
||||
|
||||
- 结构状态更可追踪
|
||||
- signal 过滤理由更清晰
|
||||
|
||||
## 阶段 D:Execution / Report Hardening
|
||||
|
||||
交付物:
|
||||
|
||||
- 更细 execution lifecycle
|
||||
- 更一致的 backtest/report 统计
|
||||
|
||||
验收:
|
||||
|
||||
- report 能更直接对应 backtest 与 execution 结果
|
||||
|
||||
## 阶段 E:Training Upstream Quality Lift
|
||||
|
||||
交付物:
|
||||
|
||||
- 基于更稳定规则事件的 case quality 增强
|
||||
- 必要时扩展训练案例生成器
|
||||
|
||||
验收:
|
||||
|
||||
- 不破坏 M40 公开契约
|
||||
- 训练案例更贴近真实高质量结构事件
|
||||
|
||||
## 6. 当前非目标
|
||||
|
||||
当前明确不做:
|
||||
|
||||
1. 真实全自动下单
|
||||
2. 以 Training 用户行为反向影响交易决策
|
||||
3. 大规模新增 Training 产品面功能
|
||||
4. 仅为“文档完整”而继续堆积启动期/计划期文档
|
||||
|
||||
## 7. 当前建议的下一步开发任务
|
||||
|
||||
按照本方案,下一步应该直接进入:
|
||||
|
||||
`Replay-backed Backtest`
|
||||
|
||||
具体第一刀:
|
||||
|
||||
1. 为 backtest 增加 replay/no-lookahead 入口或模式。
|
||||
2. 让回测在每个 decision point 只看到当时可见 candles / swings / 结构状态。
|
||||
3. 为该路径补 focused unit tests 和 offline validation。
|
||||
|
||||
## 8. 当前验证基线
|
||||
|
||||
在进入下一轮代码开发前,继续以以下命令作为最小验证基线:
|
||||
|
||||
```bash
|
||||
.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
|
||||
```
|
||||
@@ -0,0 +1,153 @@
|
||||
# AI ICT 文档索引与命名规范 v2
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档用于在当前代码现实下重建一份清晰、可维护的文档入口,解决以下问题:
|
||||
|
||||
1. 根目录 `docs/` 文件过多,阅读入口不清晰。
|
||||
2. 存在重复命名、双语平行版本、历史快照和过期规划混放的问题。
|
||||
3. 旧规划文档仍停留在“项目启动期”,和当前代码已进入交易闭环与 Training M40 阶段不一致。
|
||||
|
||||
本版规则:
|
||||
|
||||
- `docs/` 根目录只保留当前仍有直接指导意义的 active / canonical 项目文档。
|
||||
- 历史和过期规划统一进入 `docs/archive/`。
|
||||
- 通用工作流辅助文档统一进入 `docs/workflow/`。
|
||||
- 同一主题只允许一个当前主文件名。
|
||||
|
||||
## 2. 当前推荐阅读顺序
|
||||
|
||||
第一次进入项目,按以下顺序阅读:
|
||||
|
||||
1. `AI_ICT_当前开发方案_v2.md`
|
||||
2. `AI_ICT_双主线研发治理计划_v1.md`
|
||||
3. `AI_ICT_主线进度摘要_v1.md`
|
||||
4. `training_case_engine_m1_m6_product_readme.md`
|
||||
5. `AI_ICT_总体设计文档_v1.md`
|
||||
6. `AI_ICT_PostgreSQL_Schema_v1.md`
|
||||
7. `AI_ICT_MVP_规则规格书_v1.md`
|
||||
|
||||
如果要进入具体模块开发,再按模块规格书继续读:
|
||||
|
||||
- `AI_ICT_OKX_数据接入规格书_v1.md`
|
||||
- `AI_ICT_Session_生成器规格书_v1.md`
|
||||
- `AI_ICT_Swing_识别规格书_v1.md`
|
||||
- `AI_ICT_Dealing_Range_规格书_v1.md`
|
||||
- `AI_ICT_Liquidity_Pool_规格书_v1.md`
|
||||
- `AI_ICT_Sweep_识别规格书_v1.md`
|
||||
- `AI_ICT_Displacement_规格书_v1.md`
|
||||
- `AI_ICT_MSS_判定规格书_v1.md`
|
||||
- `AI_ICT_FVG_规格书_v1.md`
|
||||
|
||||
## 3. 当前 active / canonical 文档
|
||||
|
||||
### 3.1 项目治理与当前主计划
|
||||
|
||||
- `AI_ICT_当前开发方案_v2.md`
|
||||
- 当前唯一的开发优先级与阶段计划入口。
|
||||
- `AI_ICT_双主线研发治理计划_v1.md`
|
||||
- 交易主线和训练主线的边界治理规则。
|
||||
- `AI_ICT_主线进度摘要_v1.md`
|
||||
- 当前代码闭环和结构状态的高层摘要。
|
||||
|
||||
### 3.2 训练产品当前入口
|
||||
|
||||
- `training_case_engine_m1_m6_product_readme.md`
|
||||
- 当前对外可见的 Training M1-M40 成熟度、验证方式、公开契约入口。
|
||||
- `AI_ICT_Training_Case_Engine_外部产品化MVP计划_v1.md`
|
||||
- Training 的产品边界和核心需求来源。
|
||||
- `AI_ICT_Training_Case_Engine_开发方案_v1.md`
|
||||
- Training 模块内部结构与实体设计背景。
|
||||
|
||||
### 3.3 系统设计与数据库/规则主参考
|
||||
|
||||
- `AI_ICT_总体设计文档_v1.md`
|
||||
- `AI_ICT_PostgreSQL_Schema_v1.md`
|
||||
- `AI_ICT_MVP_规则规格书_v1.md`
|
||||
- `AI_ICT_Migration_拆分计划_v1.md`
|
||||
- `AI_ICT_数据库_Migration_SQL_草案_v1.md`
|
||||
- `AI_ICT_第一批正式_Migration_文件草案_v1.md`
|
||||
- `AI_ICT_第二批正式_Migration_文件草案_v1.md`
|
||||
|
||||
### 3.4 交易模块规格书主参考
|
||||
|
||||
- `AI_ICT_OKX_数据接入规格书_v1.md`
|
||||
- `AI_ICT_Session_生成器规格书_v1.md`
|
||||
- `AI_ICT_Swing_识别规格书_v1.md`
|
||||
- `AI_ICT_Dealing_Range_规格书_v1.md`
|
||||
- `AI_ICT_Liquidity_Pool_规格书_v1.md`
|
||||
- `AI_ICT_Sweep_识别规格书_v1.md`
|
||||
- `AI_ICT_Displacement_规格书_v1.md`
|
||||
- `AI_ICT_MSS_判定规格书_v1.md`
|
||||
- `AI_ICT_FVG_规格书_v1.md`
|
||||
|
||||
### 3.5 工作流辅助文档
|
||||
|
||||
以下文档仍保留,但已移出项目主阅读路径,统一放入 `docs/workflow/`:
|
||||
|
||||
- `AI_协作自动化工作规程_v1.md`
|
||||
- `AI_自动化协作迁移清单_v1.md`
|
||||
- `CLAUDE_template_autonomy_first.md`
|
||||
- `automation_workflow_migration_checklist.md`
|
||||
- `标准_CLAUDE_md_模板_v1.md`
|
||||
|
||||
## 4. 已归档类别
|
||||
|
||||
### 4.1 deleted duplicate-language / duplicate-theme files
|
||||
|
||||
以下重复文件已直接删除,不再保留归档副本:
|
||||
|
||||
- `AI_ICT_Codebase_Skeleton_Spec_v1.md`
|
||||
- `AI_ICT_Dealing_Range_Spec_v1.md`
|
||||
- `AI_ICT_PostgreSQL_Migration_SQL_Drafts_v1.md`
|
||||
- `AI_ICT_PostgreSQL_Migration_Split_Plan_v1.md`
|
||||
- `AI_ICT_Session_Generator_Spec_v1.md`
|
||||
- `AI_ICT_Sweep_Identification_Spec_v1.md`
|
||||
- `AI_ICT_Swing_Identification_Spec_v1.md`
|
||||
|
||||
对应 canonical:
|
||||
|
||||
- `AI_ICT_项目代码骨架规范_v1.md`
|
||||
- `AI_ICT_Dealing_Range_规格书_v1.md`
|
||||
- `AI_ICT_数据库_Migration_SQL_草案_v1.md`
|
||||
- `AI_ICT_Migration_拆分计划_v1.md`
|
||||
- `AI_ICT_Session_生成器规格书_v1.md`
|
||||
- `AI_ICT_Sweep_识别规格书_v1.md`
|
||||
- `AI_ICT_Swing_识别规格书_v1.md`
|
||||
|
||||
删除依据:
|
||||
|
||||
- 当前仓库已有明确主版本。
|
||||
- Git 历史已经保留被删版本。
|
||||
- 再保留物理副本只会继续制造阅读噪音。
|
||||
|
||||
### 4.2 obsolete plans
|
||||
|
||||
以下文件已进入 `docs/archive/obsolete_plans/`:
|
||||
|
||||
- `AI_ICT_开发路线图_v1.md`
|
||||
- `AI_ICT_项目启动清单_v1.md`
|
||||
- `AI_ICT_执行检查清单_v1.md`
|
||||
- `AI_ICT_文档索引与命名规范_v1.md`
|
||||
- `AI_ICT_重复文档清理计划_v1.md`
|
||||
|
||||
这些文件的问题不是“没有价值”,而是都建立在项目尚处启动期或清理前的状态上,已不适合作为当前入口。
|
||||
|
||||
### 4.3 history
|
||||
|
||||
以下文件已进入 `docs/archive/history/`:
|
||||
|
||||
- `AI_ICT_主线开发检查点_v1.md`
|
||||
- `AI_ICT_当前工作断点_2026-04-24.md`
|
||||
- `AI_ICT_报告样本_2026-04-24.md`
|
||||
- `AI_ICT_离线自动化推进记录_2026-04-29.md`
|
||||
|
||||
这些文件保留历史上下文,但不再作为当前需求和计划入口。
|
||||
|
||||
## 5. 后续命名规则
|
||||
|
||||
1. 同一主题只保留一个当前 canonical 文件名。
|
||||
2. 重大重写使用 `v2`、`v3`,不要再创建平行同义文件。
|
||||
3. 双语平行版本不再在根目录长期并存;确定为重复后直接删除。
|
||||
4. 含明确日期的工作断点、报告样本、推进记录,默认进入 `archive/history/`。
|
||||
5. 只要文档不再匹配当前代码阶段,就必须归档或重写,不能继续留在根目录冒充当前计划。
|
||||
@@ -0,0 +1,15 @@
|
||||
# Documentation Archive
|
||||
|
||||
This directory keeps files that are no longer part of the active project reading path.
|
||||
|
||||
Archive groups:
|
||||
|
||||
- `history/`: dated snapshots, temporary reports, and one-off continuity notes.
|
||||
- `obsolete_plans/`: planning documents that no longer match the current code and product stage.
|
||||
|
||||
Archive rules:
|
||||
|
||||
- Archived files are kept for traceability and historical reference.
|
||||
- Pure duplicate documents are deleted instead of archived once their canonical replacements are recorded in `docs/AI_ICT_文档索引与命名规范_v2.md`.
|
||||
- New project work should not cite archived files as canonical requirements.
|
||||
- Active project reading should start from `docs/AI_ICT_文档索引与命名规范_v2.md`.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Workflow Docs
|
||||
|
||||
This directory contains generic Codex / Claude workflow helpers rather than AI ICT product requirements.
|
||||
|
||||
Contents here are still useful for team workflow migration and prompt/template reuse, but they are not part of the current project development reading path.
|
||||
|
||||
For project requirements and engineering plans, start from `docs/AI_ICT_文档索引与命名规范_v2.md`.
|
||||
@@ -11,8 +11,10 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.services.backtest import (
|
||||
BACKTEST_MODE_ENTRY_THEN_OUTCOME,
|
||||
BacktestCostConfig,
|
||||
BacktestQualityGate,
|
||||
BacktestReplayConfig,
|
||||
BacktestService,
|
||||
evaluate_backtest_quality,
|
||||
summarize_backtest_outcomes,
|
||||
@@ -47,6 +49,21 @@ def build_quality_gate_from_env(env=None) -> BacktestQualityGate:
|
||||
)
|
||||
|
||||
|
||||
def build_backtest_mode_from_env(env=None) -> str:
|
||||
env = os.environ if env is None else env
|
||||
value = str(env.get("BACKTEST_MODE") or "").strip()
|
||||
return value or BACKTEST_MODE_ENTRY_THEN_OUTCOME
|
||||
|
||||
|
||||
def build_replay_config_from_env(env=None) -> BacktestReplayConfig:
|
||||
env = os.environ if env is None else env
|
||||
defaults = BacktestReplayConfig()
|
||||
return BacktestReplayConfig(
|
||||
left_bars=_get_int(env, "BACKTEST_REPLAY_LEFT_BARS", defaults.left_bars),
|
||||
right_bars=_get_int(env, "BACKTEST_REPLAY_RIGHT_BARS", defaults.right_bars),
|
||||
)
|
||||
|
||||
|
||||
def run_backtest_once(
|
||||
service: BacktestService,
|
||||
instrument_id: int = 1,
|
||||
@@ -55,12 +72,16 @@ def run_backtest_once(
|
||||
quality_gate: Optional[BacktestQualityGate] = None,
|
||||
require_quality_gate: bool = False,
|
||||
output_json: bool = False,
|
||||
backtest_mode: str = BACKTEST_MODE_ENTRY_THEN_OUTCOME,
|
||||
replay_config: Optional[BacktestReplayConfig] = None,
|
||||
) -> int:
|
||||
run_id, outcomes = service.run_backtest(
|
||||
instrument_id=instrument_id,
|
||||
limit=limit,
|
||||
cost_config=cost_config,
|
||||
quality_gate=quality_gate,
|
||||
mode=backtest_mode,
|
||||
replay_config=replay_config,
|
||||
)
|
||||
summary = summarize_backtest_outcomes(outcomes)
|
||||
quality_gate_result = evaluate_backtest_quality(summary, gate=quality_gate)
|
||||
@@ -96,6 +117,8 @@ def main(env=None) -> int:
|
||||
quality_gate=build_quality_gate_from_env(env=env),
|
||||
require_quality_gate=build_require_quality_gate_from_env(env=env),
|
||||
output_json=output_json,
|
||||
backtest_mode=build_backtest_mode_from_env(env=env),
|
||||
replay_config=build_replay_config_from_env(env=env),
|
||||
)
|
||||
except SQLAlchemyError as exc:
|
||||
summary = {
|
||||
|
||||
+7
-3
@@ -1,15 +1,19 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.main import main as app_main
|
||||
from src.api.server import serve_api
|
||||
|
||||
|
||||
def main() -> int:
|
||||
app_main()
|
||||
def main(env=None) -> int:
|
||||
env = os.environ if env is None else env
|
||||
host = env.get("AI_ICT_API_HOST", "0.0.0.0")
|
||||
port = int(env.get("AI_ICT_API_PORT", "8000"))
|
||||
serve_api(host=host, port=port)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.run_backtest import (
|
||||
build_backtest_mode_from_env,
|
||||
build_cost_config_from_env,
|
||||
build_replay_config_from_env,
|
||||
build_quality_gate_from_env,
|
||||
build_require_quality_gate_from_env,
|
||||
)
|
||||
@@ -32,6 +34,8 @@ def main(env=None) -> int:
|
||||
run_execution=True,
|
||||
backtest_cost_config=build_cost_config_from_env(env=env),
|
||||
backtest_quality_gate=build_quality_gate_from_env(env=env),
|
||||
backtest_mode=build_backtest_mode_from_env(env=env),
|
||||
backtest_replay_config=build_replay_config_from_env(env=env),
|
||||
signal_required_evidence_keys=build_signal_evidence_keys_from_env(env=env),
|
||||
execution_account_state=build_account_risk_state_from_env(env=env),
|
||||
)
|
||||
|
||||
+16
-1
@@ -16,4 +16,19 @@ def get_recent_signals(instrument_id: int, limit: int = 20) -> list[dict]:
|
||||
)
|
||||
with SessionLocal() as session:
|
||||
rows = session.execute(sql, {"instrument_id": instrument_id, "limit": limit}).mappings().all()
|
||||
return [dict(row) for row in rows]
|
||||
return [normalize_signal_row(dict(row)) for row in rows]
|
||||
|
||||
|
||||
def normalize_signal_row(row: dict) -> dict:
|
||||
payload = dict(row)
|
||||
meta = dict(payload.get("meta") or {})
|
||||
payload["structure_state"] = meta.get("structure_state")
|
||||
payload["mss_state"] = meta.get("mss_state")
|
||||
payload["mss_status_reason"] = meta.get("mss_status_reason")
|
||||
payload["fvg_status"] = meta.get("fvg_status")
|
||||
payload["fvg_status_reason"] = meta.get("fvg_status_reason")
|
||||
payload["execution_permission"] = meta.get("execution_permission")
|
||||
payload["fvg_fill_pct"] = float(meta.get("fvg_fill_pct") or 0.0)
|
||||
payload["fvg_touch_count"] = int(meta.get("fvg_touch_count") or 0)
|
||||
payload["fvg_data_quality_warnings"] = list(meta.get("fvg_data_quality_warnings") or [])
|
||||
return payload
|
||||
|
||||
@@ -4,8 +4,10 @@ from typing import Optional
|
||||
|
||||
from src.api import get_health, get_recent_backtest_summaries, get_recent_signals
|
||||
from src.services.backtest import (
|
||||
BACKTEST_MODE_ENTRY_THEN_OUTCOME,
|
||||
BacktestCostConfig,
|
||||
BacktestQualityGate,
|
||||
BacktestReplayConfig,
|
||||
BacktestService,
|
||||
evaluate_backtest_quality,
|
||||
summarize_backtest_outcomes,
|
||||
@@ -53,6 +55,8 @@ def run_closed_loop(
|
||||
run_execution: bool = True,
|
||||
backtest_cost_config: Optional[BacktestCostConfig] = None,
|
||||
backtest_quality_gate: Optional[BacktestQualityGate] = None,
|
||||
backtest_mode: str = BACKTEST_MODE_ENTRY_THEN_OUTCOME,
|
||||
backtest_replay_config: Optional[BacktestReplayConfig] = None,
|
||||
signal_required_evidence_keys: tuple[str, ...] = MINIMAL_SIGNAL_EVIDENCE_KEYS,
|
||||
execution_account_state: Optional[AccountRiskState] = None,
|
||||
dependencies: Optional[dict] = None,
|
||||
@@ -123,6 +127,8 @@ def run_closed_loop(
|
||||
limit=backtest_limit,
|
||||
cost_config=backtest_cost_config,
|
||||
quality_gate=backtest_quality_gate,
|
||||
mode=backtest_mode,
|
||||
replay_config=backtest_replay_config,
|
||||
)
|
||||
backtest_summary = summarize_backtest_outcomes(outcomes)
|
||||
backtest_quality_gate_result = evaluate_backtest_quality(backtest_summary, gate=backtest_quality_gate)
|
||||
|
||||
@@ -30,7 +30,7 @@ class BiasRepository:
|
||||
where instrument_id = :instrument_id
|
||||
and timeframe = :timeframe
|
||||
and event_type = 'MSS'
|
||||
order by ts desc
|
||||
order by ts desc, id desc
|
||||
limit 1
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -80,6 +80,43 @@ class LiquidityPoolRepository:
|
||||
).mappings().first()
|
||||
return dict(row) if row else None
|
||||
|
||||
def fetch_recent_pools_at_level(
|
||||
self,
|
||||
instrument_id: int,
|
||||
timeframe: str,
|
||||
side: str,
|
||||
price_low: Decimal,
|
||||
price_high: Decimal,
|
||||
limit: int = 10,
|
||||
) -> list[dict]:
|
||||
sql = text(
|
||||
"""
|
||||
select id, side, pool_type, price_low, price_high, reference_ids, created_ts, meta
|
||||
from liquidity_pools
|
||||
where instrument_id = :instrument_id
|
||||
and timeframe = :timeframe
|
||||
and status = 'untouched'
|
||||
and side = :side
|
||||
and price_low = :price_low
|
||||
and price_high = :price_high
|
||||
order by created_ts desc, id desc
|
||||
limit :limit
|
||||
"""
|
||||
)
|
||||
with SessionLocal() as session:
|
||||
rows = session.execute(
|
||||
sql,
|
||||
{
|
||||
"instrument_id": instrument_id,
|
||||
"timeframe": timeframe,
|
||||
"side": side,
|
||||
"price_low": price_low,
|
||||
"price_high": price_high,
|
||||
"limit": limit,
|
||||
},
|
||||
).mappings().all()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def insert_pool(
|
||||
self,
|
||||
instrument_id: int,
|
||||
|
||||
@@ -32,10 +32,10 @@ class SignalRepository:
|
||||
from fvg_zones
|
||||
where instrument_id = :instrument_id
|
||||
and timeframe = :timeframe
|
||||
and status in ('open', 'touched')
|
||||
order by
|
||||
case when related_displacement_id is not null then 0 else 1 end,
|
||||
end_ts desc
|
||||
end_ts desc,
|
||||
id desc
|
||||
limit 1
|
||||
"""
|
||||
)
|
||||
@@ -51,8 +51,7 @@ class SignalRepository:
|
||||
where instrument_id = :instrument_id
|
||||
and timeframe = :timeframe
|
||||
and event_type = 'MSS'
|
||||
and accepted = true
|
||||
order by ts desc
|
||||
order by ts desc, id desc
|
||||
limit 1
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -70,7 +70,6 @@ class StructureRepository:
|
||||
where instrument_id = :instrument_id
|
||||
and timeframe = :timeframe
|
||||
and event_type = 'MSS'
|
||||
and accepted = true
|
||||
order by ts desc, id desc
|
||||
limit 1
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Backtest service package."""
|
||||
|
||||
from src.services.backtest.backtest_service import (
|
||||
BACKTEST_MODE_ENTRY_THEN_OUTCOME,
|
||||
BACKTEST_MODE_REPLAY_NO_LOOKAHEAD,
|
||||
BacktestOutcome,
|
||||
BacktestCostConfig,
|
||||
BacktestReplayConfig,
|
||||
BacktestService,
|
||||
BacktestQualityGate,
|
||||
BacktestSignal,
|
||||
@@ -13,6 +16,7 @@ from src.services.backtest.backtest_service import (
|
||||
estimate_trade_cost_r,
|
||||
evaluate_backtest_quality,
|
||||
evaluate_signal_outcome,
|
||||
evaluate_signal_outcome_with_replay,
|
||||
summarize_backtest_outcomes,
|
||||
)
|
||||
from src.services.backtest.replay import (
|
||||
@@ -28,7 +32,10 @@ from src.services.backtest.replay import (
|
||||
|
||||
__all__ = [
|
||||
"BacktestOutcome",
|
||||
"BACKTEST_MODE_ENTRY_THEN_OUTCOME",
|
||||
"BACKTEST_MODE_REPLAY_NO_LOOKAHEAD",
|
||||
"BacktestCostConfig",
|
||||
"BacktestReplayConfig",
|
||||
"BacktestService",
|
||||
"BacktestQualityGate",
|
||||
"BacktestSignal",
|
||||
@@ -39,6 +46,7 @@ __all__ = [
|
||||
"estimate_trade_cost_r",
|
||||
"evaluate_backtest_quality",
|
||||
"evaluate_signal_outcome",
|
||||
"evaluate_signal_outcome_with_replay",
|
||||
"REPLAY_REQUIRED_CANDLE_FIELDS",
|
||||
"ReplayFrame",
|
||||
"ReplaySwing",
|
||||
|
||||
@@ -3,9 +3,16 @@ from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from src.repositories.backtest_repository import BacktestRepository
|
||||
from src.services.backtest.replay import ReplaySwing, iter_historical_replay
|
||||
|
||||
|
||||
SAME_CANDLE_PRIORITY = "stop_first"
|
||||
BACKTEST_MODE_ENTRY_THEN_OUTCOME = "entry_then_outcome"
|
||||
BACKTEST_MODE_REPLAY_NO_LOOKAHEAD = "replay_no_lookahead"
|
||||
SUPPORTED_BACKTEST_MODES = {
|
||||
BACKTEST_MODE_ENTRY_THEN_OUTCOME,
|
||||
BACKTEST_MODE_REPLAY_NO_LOOKAHEAD,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -41,6 +48,12 @@ class BacktestCostConfig:
|
||||
fixed_cost_r: float = 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BacktestReplayConfig:
|
||||
left_bars: int = 2
|
||||
right_bars: int = 2
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BacktestQualityGate:
|
||||
min_total_signals: int = 100
|
||||
@@ -61,9 +74,14 @@ class BacktestService:
|
||||
limit: int = 100,
|
||||
cost_config: Optional[BacktestCostConfig] = None,
|
||||
quality_gate: Optional[BacktestQualityGate] = None,
|
||||
mode: str = BACKTEST_MODE_ENTRY_THEN_OUTCOME,
|
||||
replay_config: Optional[BacktestReplayConfig] = None,
|
||||
) -> tuple[int, list[BacktestOutcome]]:
|
||||
cost_config = cost_config or BacktestCostConfig()
|
||||
quality_gate = quality_gate or BacktestQualityGate()
|
||||
replay_config = replay_config or BacktestReplayConfig()
|
||||
if mode not in SUPPORTED_BACKTEST_MODES:
|
||||
raise ValueError(f"invalid_backtest_mode:{mode}")
|
||||
signals = self.repository.fetch_candidate_signals(instrument_id=instrument_id, limit=limit)
|
||||
run_id = self.repository.insert_backtest_run(
|
||||
model_code='SWEEP_MSS_FVG',
|
||||
@@ -71,11 +89,12 @@ class BacktestService:
|
||||
timeframe_set={'bias_tf': '1m', 'setup_tf': '1m', 'trigger_tf': '1m'},
|
||||
config={
|
||||
'limit': limit,
|
||||
'mode': 'entry_then_outcome',
|
||||
'mode': mode,
|
||||
'same_candle_priority': 'stop_first',
|
||||
'session_filter': 'mark_outside',
|
||||
'cost_model': cost_config.__dict__,
|
||||
'quality_gate': quality_gate.__dict__,
|
||||
'replay': replay_config.__dict__ if mode == BACKTEST_MODE_REPLAY_NO_LOOKAHEAD else None,
|
||||
},
|
||||
)
|
||||
outcomes: list[BacktestOutcome] = []
|
||||
@@ -114,8 +133,11 @@ class BacktestService:
|
||||
'rr_tp2': float(signal.get('rr_tp2') or 0.0),
|
||||
'cost_r': 0.0,
|
||||
'cost_model': cost_config.__dict__,
|
||||
'backtest_mode': mode,
|
||||
'r_multiple': 0.0,
|
||||
'weekday': _extract_weekday(signal['created_ts']),
|
||||
'replay_left_bars': replay_config.left_bars if mode == BACKTEST_MODE_REPLAY_NO_LOOKAHEAD else None,
|
||||
'replay_right_bars': replay_config.right_bars if mode == BACKTEST_MODE_REPLAY_NO_LOOKAHEAD else None,
|
||||
},
|
||||
)
|
||||
outcomes.append(record)
|
||||
@@ -148,6 +170,8 @@ class BacktestService:
|
||||
),
|
||||
future,
|
||||
cost_config=cost_config,
|
||||
mode=mode,
|
||||
replay_config=replay_config,
|
||||
)
|
||||
if session_filtered:
|
||||
record.error_tags.append('outside_session_window')
|
||||
@@ -186,8 +210,133 @@ def evaluate_signal_outcome(
|
||||
signal: BacktestSignal,
|
||||
future_candles: list[dict],
|
||||
cost_config: Optional[BacktestCostConfig] = None,
|
||||
mode: str = BACKTEST_MODE_ENTRY_THEN_OUTCOME,
|
||||
replay_config: Optional[BacktestReplayConfig] = None,
|
||||
) -> BacktestOutcome:
|
||||
if mode == BACKTEST_MODE_REPLAY_NO_LOOKAHEAD:
|
||||
return evaluate_signal_outcome_with_replay(
|
||||
signal=signal,
|
||||
future_candles=future_candles,
|
||||
cost_config=cost_config,
|
||||
replay_config=replay_config,
|
||||
)
|
||||
cost_config = cost_config or BacktestCostConfig()
|
||||
steps = [
|
||||
_BacktestEvaluationStep(
|
||||
index=index,
|
||||
candle=candle,
|
||||
ts=_extract_candle_ts(candle),
|
||||
known_candles_count=index + 1,
|
||||
confirmed_swings_available=0,
|
||||
last_confirmed_swing=None,
|
||||
)
|
||||
for index, candle in enumerate(future_candles)
|
||||
]
|
||||
return _evaluate_signal_outcome_steps(
|
||||
signal=signal,
|
||||
steps=steps,
|
||||
cost_config=cost_config,
|
||||
mode=BACKTEST_MODE_ENTRY_THEN_OUTCOME,
|
||||
replay_config=None,
|
||||
)
|
||||
|
||||
|
||||
def evaluate_signal_outcome_with_replay(
|
||||
signal: BacktestSignal,
|
||||
future_candles: list[dict],
|
||||
cost_config: Optional[BacktestCostConfig] = None,
|
||||
replay_config: Optional[BacktestReplayConfig] = None,
|
||||
) -> BacktestOutcome:
|
||||
cost_config = cost_config or BacktestCostConfig()
|
||||
replay_config = replay_config or BacktestReplayConfig()
|
||||
try:
|
||||
steps = _build_replay_evaluation_steps(future_candles=future_candles, replay_config=replay_config)
|
||||
except ValueError as exc:
|
||||
return BacktestOutcome(
|
||||
signal_id=signal.signal_id,
|
||||
outcome='missed',
|
||||
mfe=0.0,
|
||||
mae=0.0,
|
||||
tp1_hit=False,
|
||||
tp2_hit=False,
|
||||
invalidated_before_entry=True,
|
||||
error_tags=['invalid_replay_candles'],
|
||||
meta={
|
||||
'entry_triggered': False,
|
||||
'same_candle_priority': SAME_CANDLE_PRIORITY,
|
||||
'same_candle_ambiguous': False,
|
||||
'signal_side': signal.side,
|
||||
'rr_tp1': signal.rr_tp1,
|
||||
'rr_tp2': signal.rr_tp2,
|
||||
'mfe_r': 0.0,
|
||||
'mae_r': 0.0,
|
||||
'cost_r': 0.0,
|
||||
'cost_model': cost_config.__dict__,
|
||||
'backtest_mode': BACKTEST_MODE_REPLAY_NO_LOOKAHEAD,
|
||||
'r_multiple': 0.0,
|
||||
'replay_error': str(exc),
|
||||
'replay_frame_count': 0,
|
||||
'replay_entry_frame_index': None,
|
||||
'replay_exit_frame_index': None,
|
||||
'replay_known_candles_at_entry': 0,
|
||||
'replay_confirmed_swing_count_at_entry': 0,
|
||||
'replay_last_confirmed_swing_at_entry': None,
|
||||
'replay_confirmed_swing_count_total': 0,
|
||||
'replay_left_bars': replay_config.left_bars,
|
||||
'replay_right_bars': replay_config.right_bars,
|
||||
},
|
||||
)
|
||||
return _evaluate_signal_outcome_steps(
|
||||
signal=signal,
|
||||
steps=steps,
|
||||
cost_config=cost_config,
|
||||
mode=BACKTEST_MODE_REPLAY_NO_LOOKAHEAD,
|
||||
replay_config=replay_config,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _BacktestEvaluationStep:
|
||||
index: int
|
||||
candle: dict
|
||||
ts: object
|
||||
known_candles_count: int
|
||||
confirmed_swings_available: int
|
||||
last_confirmed_swing: Optional[dict]
|
||||
|
||||
|
||||
def _build_replay_evaluation_steps(
|
||||
future_candles: list[dict],
|
||||
replay_config: BacktestReplayConfig,
|
||||
) -> list[_BacktestEvaluationStep]:
|
||||
steps: list[_BacktestEvaluationStep] = []
|
||||
confirmed_swings: list[ReplaySwing] = []
|
||||
for frame in iter_historical_replay(
|
||||
future_candles,
|
||||
left_bars=replay_config.left_bars,
|
||||
right_bars=replay_config.right_bars,
|
||||
):
|
||||
confirmed_swings.extend(frame.new_swings)
|
||||
steps.append(
|
||||
_BacktestEvaluationStep(
|
||||
index=frame.index,
|
||||
candle=frame.candle,
|
||||
ts=_extract_candle_ts(frame.candle),
|
||||
known_candles_count=len(frame.known_candles),
|
||||
confirmed_swings_available=len(confirmed_swings),
|
||||
last_confirmed_swing=_serialize_replay_swing(confirmed_swings[-1]) if confirmed_swings else None,
|
||||
)
|
||||
)
|
||||
return steps
|
||||
|
||||
|
||||
def _evaluate_signal_outcome_steps(
|
||||
signal: BacktestSignal,
|
||||
steps: list[_BacktestEvaluationStep],
|
||||
cost_config: BacktestCostConfig,
|
||||
mode: str,
|
||||
replay_config: Optional[BacktestReplayConfig],
|
||||
) -> BacktestOutcome:
|
||||
entry_mid = (signal.entry_low + signal.entry_high) / 2
|
||||
entry_triggered = False
|
||||
tp1_hit = False
|
||||
@@ -196,8 +345,11 @@ def evaluate_signal_outcome(
|
||||
same_candle_ambiguous = False
|
||||
mfe = 0.0
|
||||
mae = 0.0
|
||||
entry_step: Optional[_BacktestEvaluationStep] = None
|
||||
exit_step: Optional[_BacktestEvaluationStep] = None
|
||||
|
||||
for candle in future_candles:
|
||||
for step in steps:
|
||||
candle = step.candle
|
||||
high = float(candle['high'])
|
||||
low = float(candle['low'])
|
||||
entry_now = low <= signal.entry_high and high >= signal.entry_low
|
||||
@@ -207,6 +359,7 @@ def evaluate_signal_outcome(
|
||||
if entry_now:
|
||||
entry_triggered = True
|
||||
entry_triggered_this_candle = True
|
||||
entry_step = step
|
||||
else:
|
||||
continue
|
||||
|
||||
@@ -230,11 +383,13 @@ def evaluate_signal_outcome(
|
||||
|
||||
if stop_hit:
|
||||
stopped_out = True
|
||||
exit_step = step
|
||||
break
|
||||
if tp1_now:
|
||||
tp1_hit = True
|
||||
if tp2_now:
|
||||
tp2_hit = True
|
||||
exit_step = step
|
||||
break
|
||||
|
||||
error_tags: list[str] = []
|
||||
@@ -271,6 +426,37 @@ def evaluate_signal_outcome(
|
||||
cost_r=cost_r,
|
||||
)
|
||||
|
||||
meta = {
|
||||
'entry_triggered': entry_triggered,
|
||||
'same_candle_priority': SAME_CANDLE_PRIORITY,
|
||||
'same_candle_ambiguous': same_candle_ambiguous,
|
||||
'signal_side': signal.side,
|
||||
'rr_tp1': signal.rr_tp1,
|
||||
'rr_tp2': signal.rr_tp2,
|
||||
'mfe_r': mfe_r,
|
||||
'mae_r': mae_r,
|
||||
'cost_r': cost_r,
|
||||
'cost_model': cost_config.__dict__,
|
||||
'backtest_mode': mode,
|
||||
'r_multiple': r_multiple,
|
||||
}
|
||||
if mode == BACKTEST_MODE_REPLAY_NO_LOOKAHEAD:
|
||||
meta.update(
|
||||
{
|
||||
'replay_frame_count': len(steps),
|
||||
'replay_entry_frame_index': entry_step.index if entry_step else None,
|
||||
'replay_exit_frame_index': exit_step.index if exit_step else None,
|
||||
'replay_entry_ts': _normalize_meta_ts(entry_step.ts) if entry_step else None,
|
||||
'replay_exit_ts': _normalize_meta_ts(exit_step.ts) if exit_step else None,
|
||||
'replay_known_candles_at_entry': entry_step.known_candles_count if entry_step else 0,
|
||||
'replay_confirmed_swing_count_at_entry': entry_step.confirmed_swings_available if entry_step else 0,
|
||||
'replay_last_confirmed_swing_at_entry': entry_step.last_confirmed_swing if entry_step else None,
|
||||
'replay_confirmed_swing_count_total': steps[-1].confirmed_swings_available if steps else 0,
|
||||
'replay_left_bars': replay_config.left_bars if replay_config else None,
|
||||
'replay_right_bars': replay_config.right_bars if replay_config else None,
|
||||
}
|
||||
)
|
||||
|
||||
return BacktestOutcome(
|
||||
signal_id=signal.signal_id,
|
||||
outcome=outcome,
|
||||
@@ -280,22 +466,32 @@ def evaluate_signal_outcome(
|
||||
tp2_hit=tp2_hit,
|
||||
invalidated_before_entry=False,
|
||||
error_tags=error_tags,
|
||||
meta={
|
||||
'entry_triggered': entry_triggered,
|
||||
'same_candle_priority': SAME_CANDLE_PRIORITY,
|
||||
'same_candle_ambiguous': same_candle_ambiguous,
|
||||
'signal_side': signal.side,
|
||||
'rr_tp1': signal.rr_tp1,
|
||||
'rr_tp2': signal.rr_tp2,
|
||||
'mfe_r': mfe_r,
|
||||
'mae_r': mae_r,
|
||||
'cost_r': cost_r,
|
||||
'cost_model': cost_config.__dict__,
|
||||
'r_multiple': r_multiple,
|
||||
},
|
||||
meta=meta,
|
||||
)
|
||||
|
||||
|
||||
def _serialize_replay_swing(swing: ReplaySwing) -> dict:
|
||||
return {
|
||||
'kind': swing.kind,
|
||||
'price': swing.price,
|
||||
'ts': _normalize_meta_ts(swing.ts),
|
||||
'source_index': swing.source_index,
|
||||
'confirmed_at_index': swing.confirmed_at_index,
|
||||
'left_bars': swing.left_bars,
|
||||
'right_bars': swing.right_bars,
|
||||
}
|
||||
|
||||
|
||||
def _extract_candle_ts(candle: dict):
|
||||
return candle.get('ts_open') or candle.get('ts_close')
|
||||
|
||||
|
||||
def _normalize_meta_ts(value):
|
||||
if hasattr(value, 'isoformat'):
|
||||
return value.isoformat()
|
||||
return value
|
||||
|
||||
|
||||
def estimate_trade_cost_r(
|
||||
signal: BacktestSignal,
|
||||
outcome: str,
|
||||
|
||||
+114
-17
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
@@ -26,7 +27,7 @@ class FVGService:
|
||||
def detect_fvg(self, instrument_id: int, timeframe: str, limit: int = 200) -> list[FVGCandidate]:
|
||||
candles = self.repository.fetch_recent_candles(instrument_id=instrument_id, timeframe=timeframe, limit=limit)
|
||||
displacements = self.repository.fetch_recent_displacement(instrument_id=instrument_id, timeframe=timeframe, limit=50)
|
||||
displacement_by_end = {row['end_ts']: row['id'] for row in displacements}
|
||||
displacement_by_end = _build_displacement_context_index(displacements)
|
||||
candidates: list[FVGCandidate] = []
|
||||
if len(candles) < 3:
|
||||
return candidates
|
||||
@@ -169,9 +170,16 @@ def compute_fvg_fill_state(
|
||||
width = max(upper - lower, 1e-9)
|
||||
fill_max = 0.0
|
||||
first_touch_ts = None
|
||||
last_touch_ts = None
|
||||
touch_count = 0
|
||||
invalidated = False
|
||||
gap_count = 0
|
||||
previous_candle = None
|
||||
|
||||
for candle in candles:
|
||||
if _has_candle_gap(previous_candle, candle):
|
||||
gap_count += 1
|
||||
|
||||
high = float(candle['high'])
|
||||
low = float(candle['low'])
|
||||
close = float(candle['close'])
|
||||
@@ -179,9 +187,12 @@ def compute_fvg_fill_state(
|
||||
overlap_high = min(upper, high)
|
||||
overlap = max(0.0, overlap_high - overlap_low)
|
||||
fill_pct = min(1.0, overlap / width)
|
||||
current_touch_ts = candle.get('ts_open') or candle.get('ts_close')
|
||||
current_touch_ts = _normalize_meta_ts(candle.get('ts_open') or candle.get('ts_close'))
|
||||
if fill_pct > 0 and first_touch_ts is None:
|
||||
first_touch_ts = _normalize_meta_ts(current_touch_ts)
|
||||
first_touch_ts = current_touch_ts
|
||||
if fill_pct > 0:
|
||||
last_touch_ts = current_touch_ts
|
||||
touch_count += 1
|
||||
fill_max = max(fill_max, fill_pct)
|
||||
|
||||
if direction == 'bullish' and close < lower:
|
||||
@@ -190,26 +201,64 @@ def compute_fvg_fill_state(
|
||||
invalidated = True
|
||||
|
||||
if invalidated:
|
||||
return fill_max, 'invalidated', {
|
||||
'first_touch_ts': first_touch_ts,
|
||||
'mitigation_threshold': mitigation_threshold,
|
||||
'invalidated': True,
|
||||
'width': width,
|
||||
}
|
||||
return fill_max, 'invalidated', _build_fvg_state_meta(
|
||||
first_touch_ts=first_touch_ts,
|
||||
last_touch_ts=last_touch_ts,
|
||||
touch_count=touch_count,
|
||||
mitigation_threshold=mitigation_threshold,
|
||||
invalidated=True,
|
||||
width=width,
|
||||
status_reason='structure_break',
|
||||
gap_count=gap_count,
|
||||
)
|
||||
if fill_max >= mitigation_threshold:
|
||||
return fill_max, 'mitigated', {
|
||||
'first_touch_ts': first_touch_ts,
|
||||
'mitigation_threshold': mitigation_threshold,
|
||||
'invalidated': False,
|
||||
'width': width,
|
||||
}
|
||||
return fill_max, 'mitigated', _build_fvg_state_meta(
|
||||
first_touch_ts=first_touch_ts,
|
||||
last_touch_ts=last_touch_ts,
|
||||
touch_count=touch_count,
|
||||
mitigation_threshold=mitigation_threshold,
|
||||
invalidated=False,
|
||||
width=width,
|
||||
status_reason='mitigation_threshold_reached',
|
||||
gap_count=gap_count,
|
||||
)
|
||||
previous_candle = candle
|
||||
|
||||
status = 'touched' if fill_max > 0 else 'open'
|
||||
return fill_max, status, {
|
||||
return fill_max, status, _build_fvg_state_meta(
|
||||
first_touch_ts=first_touch_ts,
|
||||
last_touch_ts=last_touch_ts,
|
||||
touch_count=touch_count,
|
||||
mitigation_threshold=mitigation_threshold,
|
||||
invalidated=False,
|
||||
width=width,
|
||||
status_reason='partial_fill' if fill_max > 0 else 'no_touch',
|
||||
gap_count=gap_count,
|
||||
)
|
||||
|
||||
|
||||
def _build_fvg_state_meta(
|
||||
*,
|
||||
first_touch_ts,
|
||||
last_touch_ts,
|
||||
touch_count: int,
|
||||
mitigation_threshold: float,
|
||||
invalidated: bool,
|
||||
width: float,
|
||||
status_reason: str,
|
||||
gap_count: int,
|
||||
) -> dict:
|
||||
return {
|
||||
'first_touch_ts': first_touch_ts,
|
||||
'last_touch_ts': last_touch_ts,
|
||||
'touch_count': touch_count,
|
||||
'mitigation_threshold': mitigation_threshold,
|
||||
'invalidated': False,
|
||||
'invalidated': invalidated,
|
||||
'width': width,
|
||||
'status_reason': status_reason,
|
||||
'has_data_gap': gap_count > 0,
|
||||
'gap_count': gap_count,
|
||||
'data_quality_warnings': ['candle_gap_detected'] if gap_count > 0 else [],
|
||||
}
|
||||
|
||||
|
||||
@@ -220,5 +269,53 @@ def _normalize_meta_ts(value):
|
||||
return value
|
||||
|
||||
|
||||
def _build_displacement_context_index(displacements: list[dict]) -> dict[object, int]:
|
||||
by_end_ts: dict[object, dict] = {}
|
||||
for row in displacements:
|
||||
end_ts = row.get('end_ts')
|
||||
if end_ts is None:
|
||||
continue
|
||||
existing = by_end_ts.get(end_ts)
|
||||
if existing is None or int(row.get('id') or 0) > int(existing.get('id') or 0):
|
||||
by_end_ts[end_ts] = row
|
||||
return {end_ts: int(row['id']) for end_ts, row in by_end_ts.items()}
|
||||
|
||||
|
||||
def _has_candle_gap(previous_candle: Optional[dict], candle: dict) -> bool:
|
||||
if not previous_candle:
|
||||
return False
|
||||
previous_close = _coerce_ts_ordering_value(previous_candle.get('ts_close'))
|
||||
current_open = _coerce_ts_ordering_value(candle.get('ts_open'))
|
||||
if previous_close is None or current_open is None:
|
||||
return False
|
||||
return current_open > previous_close
|
||||
|
||||
|
||||
def _coerce_ts_ordering_value(value) -> Optional[float]:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
if isinstance(value, datetime):
|
||||
return value.timestamp()
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip()
|
||||
if normalized.endswith('Z'):
|
||||
normalized = normalized[:-1] + '+00:00'
|
||||
try:
|
||||
return datetime.fromisoformat(normalized).timestamp()
|
||||
except ValueError:
|
||||
try:
|
||||
return float(normalized)
|
||||
except ValueError:
|
||||
return None
|
||||
if hasattr(value, 'timestamp'):
|
||||
try:
|
||||
return float(value.timestamp())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _same_number(left, right) -> bool:
|
||||
return math.isclose(float(left or 0.0), float(right or 0.0), rel_tol=1e-4, abs_tol=1e-4)
|
||||
|
||||
@@ -106,15 +106,14 @@ class LiquidityPoolService:
|
||||
def persist_candidates(self, instrument_id: int, timeframe: str, candidates: list[LiquidityPoolCandidate]) -> int:
|
||||
persisted = 0
|
||||
for candidate in candidates:
|
||||
latest = self.repository.fetch_latest_pool(
|
||||
matching_pools = self.repository.fetch_recent_pools_at_level(
|
||||
instrument_id=instrument_id,
|
||||
timeframe=timeframe,
|
||||
side=candidate.side,
|
||||
pool_type=candidate.pool_type,
|
||||
price_low=candidate.price_low,
|
||||
price_high=candidate.price_high,
|
||||
)
|
||||
if latest and pool_matches_candidate(latest, candidate):
|
||||
if any(pool_matches_candidate(pool, candidate) for pool in matching_pools):
|
||||
continue
|
||||
self.repository.insert_pool(
|
||||
instrument_id=instrument_id,
|
||||
@@ -212,7 +211,7 @@ def _dedupe_exact_candidates(candidates: list[LiquidityPoolCandidate]) -> list[L
|
||||
candidate.pool_type,
|
||||
str(candidate.price_low),
|
||||
str(candidate.price_high),
|
||||
tuple(candidate.reference_ids),
|
||||
tuple(sorted(set(candidate.reference_ids))),
|
||||
)
|
||||
if key in seen:
|
||||
continue
|
||||
|
||||
@@ -9,13 +9,26 @@ class IntradayReportService:
|
||||
fvg_zone: Optional[dict],
|
||||
trade_signal: Optional[dict],
|
||||
) -> dict:
|
||||
structure_state, structure_status_reason = _resolve_structure_state(structure_event)
|
||||
fvg_status, fvg_status_reason = _resolve_fvg_state(fvg_zone)
|
||||
signal_structure_state = _get_signal_context_value(trade_signal, "structure_state")
|
||||
signal_mss_status_reason = _get_signal_context_value(trade_signal, "mss_status_reason")
|
||||
signal_fvg_status_reason = _get_signal_context_value(trade_signal, "fvg_status_reason")
|
||||
signal_fvg_data_quality_warnings = list(_get_signal_context_value(trade_signal, "fvg_data_quality_warnings", []))
|
||||
invalidations: list[str] = []
|
||||
if trade_signal:
|
||||
invalidations.extend(trade_signal.get("invalidations") or [])
|
||||
if fvg_zone and (fvg_zone.get("status") in {"mitigated", "invalidated"} or (fvg_zone.get("meta") or {}).get("invalidated")):
|
||||
invalidations.append("fvg_no_longer_actionable")
|
||||
if structure_event and not structure_event.get("accepted", True):
|
||||
invalidations.append("structure_not_accepted")
|
||||
if fvg_status == "mitigated":
|
||||
invalidations.append("fvg_mitigated")
|
||||
if fvg_status == "invalidated":
|
||||
invalidations.append("fvg_invalidated")
|
||||
if structure_state == "rejected":
|
||||
invalidations.append("mss_rejected")
|
||||
if structure_state == "invalidated":
|
||||
invalidations.append("mss_invalidated")
|
||||
if signal_structure_state not in {None, "aligned"}:
|
||||
invalidations.append(signal_structure_state)
|
||||
invalidations.extend(signal_fvg_data_quality_warnings)
|
||||
|
||||
return {
|
||||
"report_type": "intraday_explanation",
|
||||
@@ -28,18 +41,76 @@ class IntradayReportService:
|
||||
"event_type": structure_event.get("event_type") if structure_event else None,
|
||||
"direction": structure_event.get("direction") if structure_event else None,
|
||||
"accepted": structure_event.get("accepted") if structure_event else None,
|
||||
"state": structure_state,
|
||||
"status_reason": structure_status_reason,
|
||||
},
|
||||
"fvg": {
|
||||
"direction": fvg_zone.get("direction") if fvg_zone else None,
|
||||
"status": fvg_zone.get("status") if fvg_zone else None,
|
||||
"status": fvg_status,
|
||||
"status_reason": fvg_status_reason,
|
||||
"fill_pct": float(fvg_zone.get("fill_pct") or 0.0) if fvg_zone else None,
|
||||
"data_quality_warnings": list(dict((fvg_zone or {}).get("meta") or {}).get("data_quality_warnings") or []),
|
||||
},
|
||||
"signal": {
|
||||
"present": trade_signal is not None,
|
||||
"status": trade_signal.get("status") if trade_signal else None,
|
||||
"side": trade_signal.get("side") if trade_signal else None,
|
||||
"session_code": trade_signal.get("session_code") if trade_signal else None,
|
||||
"structure_state": signal_structure_state,
|
||||
"mss_status_reason": signal_mss_status_reason,
|
||||
"fvg_status_reason": signal_fvg_status_reason,
|
||||
"fvg_data_quality_warnings": signal_fvg_data_quality_warnings,
|
||||
"is_valid": trade_signal is not None and len(invalidations) == 0,
|
||||
},
|
||||
"invalidations": sorted(set(invalidations)),
|
||||
}
|
||||
|
||||
|
||||
def _resolve_structure_state(structure_event: Optional[dict]) -> tuple[Optional[str], Optional[str]]:
|
||||
if not structure_event:
|
||||
return None, None
|
||||
meta = dict(structure_event.get("meta") or {})
|
||||
state = meta.get("state")
|
||||
status_reason = meta.get("status_reason")
|
||||
if state not in {"accepted", "rejected", "invalidated"}:
|
||||
if structure_event.get("accepted") is True:
|
||||
state = "accepted"
|
||||
status_reason = status_reason or "retest_held"
|
||||
elif meta.get("reclaimed_old_control"):
|
||||
state = "invalidated"
|
||||
status_reason = status_reason or "old_control_reclaimed"
|
||||
elif structure_event.get("accepted") is False:
|
||||
state = "rejected"
|
||||
status_reason = status_reason or "retest_not_observed"
|
||||
return state, status_reason
|
||||
|
||||
|
||||
def _resolve_fvg_state(fvg_zone: Optional[dict]) -> tuple[Optional[str], Optional[str]]:
|
||||
if not fvg_zone:
|
||||
return None, None
|
||||
meta = dict(fvg_zone.get("meta") or {})
|
||||
invalidated = bool(meta.get("invalidated")) or fvg_zone.get("status") == "invalidated"
|
||||
if invalidated:
|
||||
status = "invalidated"
|
||||
else:
|
||||
status = fvg_zone.get("status")
|
||||
status_reason = meta.get("status_reason")
|
||||
if not status_reason:
|
||||
if status == "invalidated":
|
||||
status_reason = "structure_break"
|
||||
elif status == "mitigated":
|
||||
status_reason = "mitigation_threshold_reached"
|
||||
elif status == "touched":
|
||||
status_reason = "partial_fill"
|
||||
elif status == "open":
|
||||
status_reason = "no_touch"
|
||||
return status, status_reason
|
||||
|
||||
|
||||
def _get_signal_context_value(trade_signal: Optional[dict], key: str, default=None):
|
||||
if not trade_signal:
|
||||
return default
|
||||
if key in trade_signal:
|
||||
return trade_signal.get(key, default)
|
||||
meta = dict(trade_signal.get("meta") or {})
|
||||
return meta.get(key, default)
|
||||
|
||||
@@ -33,6 +33,10 @@ class PostTradeReviewService:
|
||||
improvement_notes.append("review_signal_invalidations")
|
||||
if trade_signal.get("status") != "awaiting_confirmation":
|
||||
improvement_notes.append("prefer_confirmed_execution_window")
|
||||
if _get_signal_context_value(trade_signal, "structure_state") not in {None, "aligned"}:
|
||||
improvement_notes.append("review_signal_structure_state")
|
||||
if _get_signal_context_value(trade_signal, "fvg_data_quality_warnings", []):
|
||||
improvement_notes.append("review_signal_data_quality_warnings")
|
||||
|
||||
if backtest_result:
|
||||
if backtest_result.get("outcome") == "loss":
|
||||
@@ -48,7 +52,24 @@ class PostTradeReviewService:
|
||||
"execution_status": trade_execution.get("status") if trade_execution else None,
|
||||
"signal_status": trade_signal.get("status") if trade_signal else None,
|
||||
"backtest_outcome": backtest_result.get("outcome") if backtest_result else None,
|
||||
"signal_context": {
|
||||
"structure_state": _get_signal_context_value(trade_signal, "structure_state"),
|
||||
"mss_state": _get_signal_context_value(trade_signal, "mss_state"),
|
||||
"mss_status_reason": _get_signal_context_value(trade_signal, "mss_status_reason"),
|
||||
"fvg_status": _get_signal_context_value(trade_signal, "fvg_status"),
|
||||
"fvg_status_reason": _get_signal_context_value(trade_signal, "fvg_status_reason"),
|
||||
"fvg_data_quality_warnings": list(_get_signal_context_value(trade_signal, "fvg_data_quality_warnings", [])),
|
||||
},
|
||||
"execution_audit": execution_audit,
|
||||
"error_types": sorted(set(error_types)),
|
||||
"improvement_notes": sorted(set(improvement_notes)),
|
||||
}
|
||||
|
||||
|
||||
def _get_signal_context_value(trade_signal: Optional[dict], key: str, default=None):
|
||||
if not trade_signal:
|
||||
return default
|
||||
if key in trade_signal:
|
||||
return trade_signal.get(key, default)
|
||||
meta = dict(trade_signal.get("meta") or {})
|
||||
return meta.get(key, default)
|
||||
|
||||
@@ -83,6 +83,7 @@ class SignalService:
|
||||
invalidations = ['mss invalidated', 'fvg fully mitigated']
|
||||
if not session_decision['execution_permission']:
|
||||
invalidations.append('session_mismatch')
|
||||
fvg_meta = dict(fvg.get('meta') or {})
|
||||
evidence = {
|
||||
'bias_snapshot_id': bias['id'],
|
||||
'fvg_id': fvg['id'],
|
||||
@@ -110,9 +111,15 @@ class SignalService:
|
||||
'session_filter': session_decision['reason'],
|
||||
'execution_permission': session_decision['execution_permission'],
|
||||
'structure_quality': structure_alignment['reason'],
|
||||
'structure_state': structure_alignment['structure_state'],
|
||||
'mss_state': structure_alignment['mss_state'],
|
||||
'mss_status_reason': structure_alignment['mss_status_reason'],
|
||||
'evidence_quality': evidence_quality,
|
||||
'fvg_status': fvg['status'],
|
||||
'fvg_status': structure_alignment['fvg_status'],
|
||||
'fvg_status_reason': structure_alignment['fvg_status_reason'],
|
||||
'fvg_fill_pct': float(fvg.get('fill_pct') or 0.0),
|
||||
'fvg_touch_count': int(fvg_meta.get('touch_count') or 0),
|
||||
'fvg_data_quality_warnings': list(fvg_meta.get('data_quality_warnings') or []),
|
||||
'mss_displacement_event_id': mss.get('displacement_event_id'),
|
||||
'rr_filter': rr_decision,
|
||||
},
|
||||
@@ -175,29 +182,169 @@ def _same_number(left, right) -> bool:
|
||||
return math.isclose(float(left or 0.0), float(right or 0.0), rel_tol=1e-4, abs_tol=1e-4)
|
||||
|
||||
|
||||
def _build_signal_context_result(
|
||||
*,
|
||||
is_actionable: bool,
|
||||
reason: str,
|
||||
structure_state: str,
|
||||
mss_state: str,
|
||||
mss_status_reason: str,
|
||||
fvg_status: str,
|
||||
fvg_status_reason: str,
|
||||
side: Optional[str] = None,
|
||||
) -> dict[str, object]:
|
||||
result = {
|
||||
'is_actionable': is_actionable,
|
||||
'reason': reason,
|
||||
'structure_state': structure_state,
|
||||
'mss_state': mss_state,
|
||||
'mss_status_reason': mss_status_reason,
|
||||
'fvg_status': fvg_status,
|
||||
'fvg_status_reason': fvg_status_reason,
|
||||
}
|
||||
if side is not None:
|
||||
result['side'] = side
|
||||
return result
|
||||
|
||||
|
||||
def _resolve_mss_state(mss: dict) -> tuple[str, str]:
|
||||
meta = dict(mss.get('meta') or {})
|
||||
state = meta.get('state')
|
||||
status_reason = meta.get('status_reason')
|
||||
if state not in {'accepted', 'rejected', 'invalidated'}:
|
||||
if bool(mss.get('accepted')):
|
||||
state = 'accepted'
|
||||
status_reason = status_reason or 'retest_held'
|
||||
elif meta.get('reclaimed_old_control'):
|
||||
state = 'invalidated'
|
||||
status_reason = status_reason or 'old_control_reclaimed'
|
||||
else:
|
||||
state = 'rejected'
|
||||
status_reason = status_reason or 'retest_not_observed'
|
||||
return str(state), str(status_reason or 'unknown')
|
||||
|
||||
|
||||
def _resolve_fvg_state(fvg: dict) -> tuple[str, str]:
|
||||
meta = dict(fvg.get('meta') or {})
|
||||
invalidated = bool(meta.get('invalidated')) or fvg.get('status') == 'invalidated'
|
||||
if invalidated:
|
||||
status = 'invalidated'
|
||||
else:
|
||||
status = str(fvg.get('status') or 'open')
|
||||
status_reason = meta.get('status_reason')
|
||||
if not status_reason:
|
||||
if status == 'invalidated':
|
||||
status_reason = 'structure_break'
|
||||
elif status == 'mitigated':
|
||||
status_reason = 'mitigation_threshold_reached'
|
||||
elif status == 'touched':
|
||||
status_reason = 'partial_fill'
|
||||
else:
|
||||
status_reason = 'no_touch'
|
||||
return status, str(status_reason)
|
||||
|
||||
|
||||
def validate_signal_context(bias: dict, fvg: dict, mss: dict) -> dict[str, object]:
|
||||
if not mss.get('accepted'):
|
||||
return {'is_actionable': False, 'reason': 'mss_not_accepted'}
|
||||
mss_state, mss_status_reason = _resolve_mss_state(mss)
|
||||
fvg_status, fvg_status_reason = _resolve_fvg_state(fvg)
|
||||
|
||||
if mss_state == 'invalidated':
|
||||
return _build_signal_context_result(
|
||||
is_actionable=False,
|
||||
reason='mss_invalidated',
|
||||
structure_state='mss_invalidated',
|
||||
mss_state=mss_state,
|
||||
mss_status_reason=mss_status_reason,
|
||||
fvg_status=fvg_status,
|
||||
fvg_status_reason=fvg_status_reason,
|
||||
)
|
||||
if mss_state != 'accepted' or not mss.get('accepted'):
|
||||
return _build_signal_context_result(
|
||||
is_actionable=False,
|
||||
reason='mss_rejected',
|
||||
structure_state='mss_rejected',
|
||||
mss_state=mss_state,
|
||||
mss_status_reason=mss_status_reason,
|
||||
fvg_status=fvg_status,
|
||||
fvg_status_reason=fvg_status_reason,
|
||||
)
|
||||
|
||||
expected_side = 'buy' if mss['direction'] == 'bullish' else 'sell'
|
||||
if fvg['direction'] != mss['direction']:
|
||||
return {'is_actionable': False, 'reason': 'fvg_mss_direction_mismatch'}
|
||||
return _build_signal_context_result(
|
||||
is_actionable=False,
|
||||
reason='fvg_mss_direction_mismatch',
|
||||
structure_state='fvg_mss_direction_mismatch',
|
||||
mss_state=mss_state,
|
||||
mss_status_reason=mss_status_reason,
|
||||
fvg_status=fvg_status,
|
||||
fvg_status_reason=fvg_status_reason,
|
||||
)
|
||||
|
||||
draw_side = bias.get('draw_side')
|
||||
if draw_side and ((expected_side == 'buy' and draw_side != 'buy_side') or (expected_side == 'sell' and draw_side != 'sell_side')):
|
||||
return {'is_actionable': False, 'reason': 'bias_structure_mismatch'}
|
||||
return _build_signal_context_result(
|
||||
is_actionable=False,
|
||||
reason='bias_structure_mismatch',
|
||||
structure_state='bias_structure_mismatch',
|
||||
mss_state=mss_state,
|
||||
mss_status_reason=mss_status_reason,
|
||||
fvg_status=fvg_status,
|
||||
fvg_status_reason=fvg_status_reason,
|
||||
)
|
||||
|
||||
if fvg['status'] not in {'open', 'touched'}:
|
||||
return {'is_actionable': False, 'reason': 'fvg_not_actionable'}
|
||||
|
||||
fvg_meta = fvg.get('meta') or {}
|
||||
if fvg_meta.get('invalidated'):
|
||||
return {'is_actionable': False, 'reason': 'fvg_invalidated'}
|
||||
if fvg_status == 'invalidated':
|
||||
return _build_signal_context_result(
|
||||
is_actionable=False,
|
||||
reason='fvg_invalidated',
|
||||
structure_state='fvg_invalidated',
|
||||
mss_state=mss_state,
|
||||
mss_status_reason=mss_status_reason,
|
||||
fvg_status=fvg_status,
|
||||
fvg_status_reason=fvg_status_reason,
|
||||
)
|
||||
if fvg_status == 'mitigated':
|
||||
return _build_signal_context_result(
|
||||
is_actionable=False,
|
||||
reason='fvg_mitigated',
|
||||
structure_state='fvg_mitigated',
|
||||
mss_state=mss_state,
|
||||
mss_status_reason=mss_status_reason,
|
||||
fvg_status=fvg_status,
|
||||
fvg_status_reason=fvg_status_reason,
|
||||
)
|
||||
if fvg_status not in {'open', 'touched'}:
|
||||
return _build_signal_context_result(
|
||||
is_actionable=False,
|
||||
reason='fvg_not_actionable',
|
||||
structure_state='fvg_not_actionable',
|
||||
mss_state=mss_state,
|
||||
mss_status_reason=mss_status_reason,
|
||||
fvg_status=fvg_status,
|
||||
fvg_status_reason=fvg_status_reason,
|
||||
)
|
||||
|
||||
if mss.get('displacement_event_id') and fvg.get('related_displacement_id') and mss['displacement_event_id'] != fvg['related_displacement_id']:
|
||||
return {'is_actionable': False, 'reason': 'displacement_context_mismatch'}
|
||||
return _build_signal_context_result(
|
||||
is_actionable=False,
|
||||
reason='displacement_context_mismatch',
|
||||
structure_state='displacement_context_mismatch',
|
||||
mss_state=mss_state,
|
||||
mss_status_reason=mss_status_reason,
|
||||
fvg_status=fvg_status,
|
||||
fvg_status_reason=fvg_status_reason,
|
||||
)
|
||||
|
||||
return {'is_actionable': True, 'reason': 'aligned', 'side': expected_side}
|
||||
return _build_signal_context_result(
|
||||
is_actionable=True,
|
||||
reason='aligned',
|
||||
structure_state='aligned',
|
||||
mss_state=mss_state,
|
||||
mss_status_reason=mss_status_reason,
|
||||
fvg_status=fvg_status,
|
||||
fvg_status_reason=fvg_status_reason,
|
||||
side=expected_side,
|
||||
)
|
||||
|
||||
|
||||
def validate_rr_thresholds(rr_tp1: float, rr_tp2: float) -> dict[str, object]:
|
||||
|
||||
@@ -36,8 +36,6 @@ class MSSService:
|
||||
persisted = 0
|
||||
latest = self.repository.fetch_latest_mss_event(instrument_id=instrument_id, timeframe=timeframe)
|
||||
for candidate in candidates:
|
||||
if not candidate.accepted:
|
||||
continue
|
||||
if latest and mss_event_matches_candidate(latest, candidate):
|
||||
continue
|
||||
self.repository.insert_mss_event(
|
||||
@@ -84,7 +82,18 @@ def detect_mss_candidates(
|
||||
ordered_candles = sorted(candles, key=lambda row: row['ts_open'])
|
||||
candle_index_by_close = {row['ts_close']: idx for idx, row in enumerate(ordered_candles)}
|
||||
|
||||
for displacement in sorted(displacements, key=lambda row: row['end_ts'], reverse=True):
|
||||
seen_displacement_keys: set[tuple[object, object]] = set()
|
||||
ordered_displacements = sorted(
|
||||
displacements,
|
||||
key=lambda row: (row['end_ts'], int(row.get('id') or 0)),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
for displacement in ordered_displacements:
|
||||
displacement_key = (displacement.get('direction'), displacement.get('end_ts'))
|
||||
if displacement_key in seen_displacement_keys:
|
||||
continue
|
||||
seen_displacement_keys.add(displacement_key)
|
||||
direction = displacement['direction']
|
||||
if direction == 'bullish':
|
||||
reference_swing = _latest_swing_before_ts(swing_highs, displacement['end_ts'])
|
||||
@@ -110,7 +119,7 @@ def detect_mss_candidates(
|
||||
candle_index_by_close=candle_index_by_close,
|
||||
acceptance_window=acceptance_window,
|
||||
)
|
||||
if candidate and candidate.accepted:
|
||||
if candidate:
|
||||
candidates.append(candidate)
|
||||
break
|
||||
return candidates
|
||||
@@ -138,8 +147,6 @@ def _build_mss_candidate(
|
||||
|
||||
retest_candles = ordered_candles[displacement_idx + 1 : displacement_idx + 1 + acceptance_window]
|
||||
acceptance = evaluate_mss_acceptance(direction, broken_level, retest_candles)
|
||||
if not acceptance['has_retest']:
|
||||
return None
|
||||
|
||||
return MSSCandidate(
|
||||
direction=direction,
|
||||
@@ -150,10 +157,13 @@ def _build_mss_candidate(
|
||||
ts=displacement['end_ts'],
|
||||
meta={
|
||||
'acceptance_window_candles': acceptance_window,
|
||||
'acceptance_window_complete': len(retest_candles) >= acceptance_window,
|
||||
'has_retest': acceptance['has_retest'],
|
||||
'retest_count': acceptance['retest_count'],
|
||||
'held_boundary': acceptance['held_boundary'],
|
||||
'reclaimed_old_control': acceptance['reclaimed_old_control'],
|
||||
'state': acceptance['state'],
|
||||
'status_reason': acceptance['status_reason'],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -187,12 +197,24 @@ def evaluate_mss_acceptance(direction: str, broken_level: float, candles: list[d
|
||||
held_boundary = False
|
||||
break
|
||||
|
||||
if reclaimed_old_control:
|
||||
state = 'invalidated'
|
||||
status_reason = 'old_control_reclaimed'
|
||||
elif has_retest:
|
||||
state = 'accepted'
|
||||
status_reason = 'retest_held'
|
||||
else:
|
||||
state = 'rejected'
|
||||
status_reason = 'retest_not_observed'
|
||||
|
||||
return {
|
||||
'accepted': has_retest and held_boundary and not reclaimed_old_control,
|
||||
'accepted': state == 'accepted',
|
||||
'has_retest': has_retest,
|
||||
'retest_count': retest_count,
|
||||
'held_boundary': held_boundary,
|
||||
'reclaimed_old_control': reclaimed_old_control,
|
||||
'state': state,
|
||||
'status_reason': status_reason,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from src.api import get_health
|
||||
from src.api.backtest import get_recent_backtest_summaries, normalize_backtest_summary_row
|
||||
from src.api.server import route_request
|
||||
from src.api.signals import get_recent_signals
|
||||
from src.services.backtest import BacktestCostConfig, BacktestQualityGate
|
||||
from src.services.backtest import BacktestCostConfig, BacktestQualityGate, BacktestReplayConfig
|
||||
from src.services.execution import AccountRiskState
|
||||
from src.main import run_closed_loop
|
||||
|
||||
@@ -47,11 +47,35 @@ class ApiTests(unittest.TestCase):
|
||||
self.assertIn("session_timezone", health)
|
||||
|
||||
def test_get_recent_signals_uses_session_query(self) -> None:
|
||||
rows = [{"id": 1, "side": "buy", "status": "awaiting_confirmation"}]
|
||||
rows = [{
|
||||
"id": 1,
|
||||
"side": "buy",
|
||||
"status": "awaiting_confirmation",
|
||||
"meta": {
|
||||
"structure_state": "aligned",
|
||||
"mss_state": "accepted",
|
||||
"mss_status_reason": "retest_held",
|
||||
"fvg_status": "open",
|
||||
"fvg_status_reason": "no_touch",
|
||||
"execution_permission": True,
|
||||
"fvg_fill_pct": 0.0,
|
||||
"fvg_touch_count": 0,
|
||||
"fvg_data_quality_warnings": [],
|
||||
},
|
||||
}]
|
||||
with patch("src.api.signals.SessionLocal", return_value=FakeSession(rows)):
|
||||
result = get_recent_signals(instrument_id=1, limit=1)
|
||||
|
||||
self.assertEqual(result, rows)
|
||||
self.assertEqual(result[0]["id"], 1)
|
||||
self.assertEqual(result[0]["structure_state"], "aligned")
|
||||
self.assertEqual(result[0]["mss_state"], "accepted")
|
||||
self.assertEqual(result[0]["mss_status_reason"], "retest_held")
|
||||
self.assertEqual(result[0]["fvg_status"], "open")
|
||||
self.assertEqual(result[0]["fvg_status_reason"], "no_touch")
|
||||
self.assertTrue(result[0]["execution_permission"])
|
||||
self.assertEqual(result[0]["fvg_fill_pct"], 0.0)
|
||||
self.assertEqual(result[0]["fvg_touch_count"], 0)
|
||||
self.assertEqual(result[0]["fvg_data_quality_warnings"], [])
|
||||
|
||||
def test_get_recent_backtest_summaries_uses_session_query(self) -> None:
|
||||
rows = [{"id": 10, "summary": {"total_signals": 3}}]
|
||||
@@ -365,6 +389,42 @@ class ClosedLoopTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(backtest_service.last_kwargs["quality_gate"], quality_gate)
|
||||
|
||||
def test_run_closed_loop_passes_backtest_replay_mode_and_config(self) -> None:
|
||||
backtest_service = FakeBacktestService()
|
||||
replay_config = BacktestReplayConfig(left_bars=1, right_bars=1)
|
||||
deps = {
|
||||
"session_generator": FakeSessionGenerator(),
|
||||
"swing_service": FakeSwingService(),
|
||||
"range_service": FakeRangeService(),
|
||||
"liquidity_service": FakeLiquidityService(),
|
||||
"sweep_service": FakeSweepService(),
|
||||
"displacement_service": FakeDisplacementService(),
|
||||
"mss_service": FakeMSSService(),
|
||||
"fvg_service": FakeFVGService(),
|
||||
"bias_service": FakeBiasService(),
|
||||
"signal_service": FakeSignalService(),
|
||||
"execution_service": FakeExecutionService(),
|
||||
"backtest_service": backtest_service,
|
||||
"pre_market_report_service": FakePreMarketReportService(),
|
||||
"intraday_report_service": FakeIntradayReportService(),
|
||||
"post_trade_review_service": FakePostTradeReviewService(),
|
||||
"get_recent_signals": lambda instrument_id, limit: [{"id": 1, "session_code": "LONDON", "status": "awaiting_confirmation", "side": "buy", "invalidations": []}],
|
||||
"get_recent_backtest_summaries": lambda instrument_id, limit: [{"id": 55, "summary": {"total_signals": 1}}],
|
||||
}
|
||||
|
||||
run_closed_loop(
|
||||
instrument_id=1,
|
||||
timeframe="1m",
|
||||
backtest_limit=10,
|
||||
run_execution=True,
|
||||
backtest_mode="replay_no_lookahead",
|
||||
backtest_replay_config=replay_config,
|
||||
dependencies=deps,
|
||||
)
|
||||
|
||||
self.assertEqual(backtest_service.last_kwargs["mode"], "replay_no_lookahead")
|
||||
self.assertEqual(backtest_service.last_kwargs["replay_config"], replay_config)
|
||||
|
||||
def test_run_closed_loop_passes_execution_account_state(self) -> None:
|
||||
execution_service = FakeExecutionService()
|
||||
account_state = AccountRiskState(daily_pnl_pct=-0.01, consecutive_losses=1)
|
||||
|
||||
@@ -323,6 +323,26 @@ class ScriptFallbackTests(unittest.TestCase):
|
||||
self.assertEqual(quality_gate.max_drawdown_r, 6.0)
|
||||
self.assertEqual(quality_gate.max_consecutive_losses, 3)
|
||||
|
||||
def test_run_pipeline_passes_backtest_replay_mode_and_config(self) -> None:
|
||||
captured = {}
|
||||
|
||||
def fake_run_closed_loop(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"ok": True, "backtest_quality_gate": {"passed": True}}
|
||||
|
||||
output = io.StringIO()
|
||||
env = {
|
||||
"BACKTEST_MODE": "replay_no_lookahead",
|
||||
"BACKTEST_REPLAY_LEFT_BARS": "1",
|
||||
"BACKTEST_REPLAY_RIGHT_BARS": "3",
|
||||
}
|
||||
with patch("scripts.run_pipeline.run_closed_loop", side_effect=fake_run_closed_loop), patch.dict(os.environ, env, clear=False), contextlib.redirect_stdout(output):
|
||||
run_pipeline.main()
|
||||
|
||||
self.assertEqual(captured["backtest_mode"], "replay_no_lookahead")
|
||||
self.assertEqual(captured["backtest_replay_config"].left_bars, 1)
|
||||
self.assertEqual(captured["backtest_replay_config"].right_bars, 3)
|
||||
|
||||
def test_run_pipeline_passes_signal_evidence_mode(self) -> None:
|
||||
captured = {}
|
||||
|
||||
|
||||
@@ -10,7 +10,14 @@ from sqlalchemy.exc import OperationalError
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
|
||||
|
||||
from scripts import run_backtest
|
||||
from scripts.run_backtest import build_cost_config_from_env, build_quality_gate_from_env, build_require_quality_gate_from_env, run_backtest_once
|
||||
from scripts.run_backtest import (
|
||||
build_backtest_mode_from_env,
|
||||
build_cost_config_from_env,
|
||||
build_quality_gate_from_env,
|
||||
build_replay_config_from_env,
|
||||
build_require_quality_gate_from_env,
|
||||
run_backtest_once,
|
||||
)
|
||||
from src.services.backtest import BacktestOutcome
|
||||
|
||||
|
||||
@@ -80,6 +87,20 @@ class RunBacktestScriptTests(unittest.TestCase):
|
||||
self.assertEqual(gate.max_drawdown_r, 6.0)
|
||||
self.assertEqual(gate.max_consecutive_losses, 3)
|
||||
|
||||
def test_build_backtest_mode_and_replay_config_from_env(self) -> None:
|
||||
self.assertEqual(build_backtest_mode_from_env({}), "entry_then_outcome")
|
||||
self.assertEqual(build_backtest_mode_from_env({"BACKTEST_MODE": "replay_no_lookahead"}), "replay_no_lookahead")
|
||||
|
||||
replay_config = build_replay_config_from_env(
|
||||
{
|
||||
"BACKTEST_REPLAY_LEFT_BARS": "1",
|
||||
"BACKTEST_REPLAY_RIGHT_BARS": "3",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(replay_config.left_bars, 1)
|
||||
self.assertEqual(replay_config.right_bars, 3)
|
||||
|
||||
def test_run_backtest_once_allows_failed_quality_gate_when_not_required(self) -> None:
|
||||
service = FakeBacktestService([])
|
||||
output = io.StringIO()
|
||||
@@ -140,6 +161,28 @@ class RunBacktestScriptTests(unittest.TestCase):
|
||||
self.assertFalse(payload["quality_gate"]["passed"])
|
||||
self.assertIn("sample_size_too_small", payload["quality_gate"]["reasons"])
|
||||
|
||||
def test_run_backtest_once_passes_replay_mode_and_config(self) -> None:
|
||||
service = FakeBacktestService([])
|
||||
output = io.StringIO()
|
||||
replay_config = build_replay_config_from_env(
|
||||
{
|
||||
"BACKTEST_REPLAY_LEFT_BARS": "1",
|
||||
"BACKTEST_REPLAY_RIGHT_BARS": "1",
|
||||
}
|
||||
)
|
||||
|
||||
with contextlib.redirect_stdout(output):
|
||||
exit_code = run_backtest_once(
|
||||
service=service,
|
||||
backtest_mode="replay_no_lookahead",
|
||||
replay_config=replay_config,
|
||||
)
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertEqual(service.calls[0]["mode"], "replay_no_lookahead")
|
||||
self.assertEqual(service.calls[0]["replay_config"].left_bars, 1)
|
||||
self.assertEqual(service.calls[0]["replay_config"].right_bars, 1)
|
||||
|
||||
def test_main_reports_database_unavailable(self) -> None:
|
||||
output = io.StringIO()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from src.services.backtest import (
|
||||
BacktestCostConfig,
|
||||
BacktestOutcome,
|
||||
BacktestQualityGate,
|
||||
BacktestReplayConfig,
|
||||
BacktestService,
|
||||
BacktestSignal,
|
||||
build_cumulative_r_curve,
|
||||
@@ -17,6 +18,7 @@ from src.services.backtest import (
|
||||
estimate_trade_cost_r,
|
||||
evaluate_backtest_quality,
|
||||
evaluate_signal_outcome,
|
||||
evaluate_signal_outcome_with_replay,
|
||||
summarize_backtest_outcomes,
|
||||
)
|
||||
from src.repositories.backtest_repository import BacktestRepository
|
||||
@@ -133,6 +135,41 @@ class BacktestOutcomeTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(outcome.meta["r_multiple"], 2.5 - expected_cost_r)
|
||||
self.assertEqual(outcome.meta["cost_model"]["fee_rate"], 0.0005)
|
||||
|
||||
def test_replay_no_lookahead_tracks_visible_candles_and_confirmed_swings_at_entry(self) -> None:
|
||||
signal = BacktestSignal(
|
||||
signal_id=5,
|
||||
side="buy",
|
||||
entry_low=96.0,
|
||||
entry_high=97.0,
|
||||
stop_loss=95.0,
|
||||
tp1=100.0,
|
||||
tp2=103.0,
|
||||
rr_tp1=1.0,
|
||||
rr_tp2=3.0,
|
||||
)
|
||||
|
||||
outcome = evaluate_signal_outcome_with_replay(
|
||||
signal,
|
||||
[
|
||||
{"ts_open": 1, "ts_close": 2, "open": 97.7, "high": 98.0, "low": 97.5, "close": 97.8},
|
||||
{"ts_open": 2, "ts_close": 3, "open": 97.0, "high": 99.0, "low": 96.2, "close": 98.0},
|
||||
{"ts_open": 3, "ts_close": 4, "open": 97.2, "high": 98.0, "low": 96.5, "close": 97.4},
|
||||
{"ts_open": 4, "ts_close": 5, "open": 98.5, "high": 103.5, "low": 97.0, "close": 103.0},
|
||||
],
|
||||
replay_config=BacktestReplayConfig(left_bars=1, right_bars=1),
|
||||
)
|
||||
|
||||
self.assertEqual(outcome.outcome, "win")
|
||||
self.assertEqual(outcome.meta["backtest_mode"], "replay_no_lookahead")
|
||||
self.assertEqual(outcome.meta["replay_entry_frame_index"], 1)
|
||||
self.assertEqual(outcome.meta["replay_known_candles_at_entry"], 2)
|
||||
self.assertEqual(outcome.meta["replay_confirmed_swing_count_at_entry"], 0)
|
||||
self.assertEqual(outcome.meta["replay_confirmed_swing_count_total"], 2)
|
||||
self.assertEqual(outcome.meta["replay_last_confirmed_swing_at_entry"], None)
|
||||
self.assertEqual(outcome.meta["replay_exit_frame_index"], 3)
|
||||
self.assertEqual(outcome.meta["replay_left_bars"], 1)
|
||||
self.assertEqual(outcome.meta["replay_right_bars"], 1)
|
||||
|
||||
def test_summary_includes_expectancy_and_profit_factor_r(self) -> None:
|
||||
outcomes = [
|
||||
BacktestOutcome(
|
||||
@@ -326,6 +363,72 @@ class BacktestServiceRunTests(unittest.TestCase):
|
||||
self.assertEqual(repository.run_config["quality_gate"]["min_total_signals"], 100)
|
||||
self.assertEqual(repository.run_config["quality_gate"]["min_executed_signals"], 30)
|
||||
|
||||
def test_run_backtest_persists_replay_mode_and_replay_meta(self) -> None:
|
||||
case = self
|
||||
|
||||
class FakeBacktestRepository:
|
||||
def __init__(self) -> None:
|
||||
self.results = []
|
||||
self.run_config = {}
|
||||
|
||||
def fetch_candidate_signals(self, instrument_id: int, limit: int):
|
||||
case.assertEqual(instrument_id, 1)
|
||||
case.assertEqual(limit, 10)
|
||||
return [
|
||||
{
|
||||
"id": 8,
|
||||
"created_ts": datetime(2026, 4, 29, 9, 0, tzinfo=timezone.utc),
|
||||
"expires_ts": datetime(2026, 4, 29, 10, 0, tzinfo=timezone.utc),
|
||||
"side": "buy",
|
||||
"entry_low": 96.0,
|
||||
"entry_high": 97.0,
|
||||
"stop_loss": 95.0,
|
||||
"tp1": 100.0,
|
||||
"tp2": 103.0,
|
||||
"rr_tp1": 1.0,
|
||||
"rr_tp2": 3.0,
|
||||
}
|
||||
]
|
||||
|
||||
def insert_backtest_run(self, **kwargs):
|
||||
self.run_config = kwargs["config"]
|
||||
return 77
|
||||
|
||||
def fetch_sessions_covering_window(self, **kwargs):
|
||||
return [{"session_code": "LONDON"}]
|
||||
|
||||
def fetch_future_candles(self, **kwargs):
|
||||
return [
|
||||
{"ts_open": 1, "ts_close": 2, "open": 97.7, "high": 98.0, "low": 97.5, "close": 97.8},
|
||||
{"ts_open": 2, "ts_close": 3, "open": 97.0, "high": 99.0, "low": 96.2, "close": 98.0},
|
||||
{"ts_open": 3, "ts_close": 4, "open": 97.2, "high": 98.0, "low": 96.5, "close": 97.4},
|
||||
{"ts_open": 4, "ts_close": 5, "open": 98.5, "high": 103.5, "low": 97.0, "close": 103.0},
|
||||
]
|
||||
|
||||
def insert_backtest_result(self, **kwargs):
|
||||
self.results.append(kwargs)
|
||||
|
||||
def update_backtest_run_summary(self, backtest_run_id: int, summary: dict) -> None:
|
||||
return None
|
||||
|
||||
repository = FakeBacktestRepository()
|
||||
service = BacktestService(repository=repository)
|
||||
|
||||
run_id, outcomes = service.run_backtest(
|
||||
instrument_id=1,
|
||||
limit=10,
|
||||
mode="replay_no_lookahead",
|
||||
replay_config=BacktestReplayConfig(left_bars=1, right_bars=1),
|
||||
)
|
||||
|
||||
self.assertEqual(run_id, 77)
|
||||
self.assertEqual(len(outcomes), 1)
|
||||
self.assertEqual(repository.run_config["mode"], "replay_no_lookahead")
|
||||
self.assertEqual(repository.run_config["replay"]["left_bars"], 1)
|
||||
self.assertEqual(repository.run_config["replay"]["right_bars"], 1)
|
||||
self.assertEqual(repository.results[0]["meta"]["backtest_mode"], "replay_no_lookahead")
|
||||
self.assertEqual(repository.results[0]["meta"]["replay_confirmed_swing_count_total"], 2)
|
||||
|
||||
|
||||
class BacktestRepositorySerializationTests(unittest.TestCase):
|
||||
def test_insert_backtest_result_serializes_datetime_in_meta(self) -> None:
|
||||
|
||||
@@ -5,7 +5,7 @@ import unittest
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
|
||||
|
||||
from src.services.fvg import compute_fvg_fill_state, fvg_matches_candidate, fvg_state_matches_update
|
||||
from src.services.fvg.fvg_service import FVGCandidate
|
||||
from src.services.fvg.fvg_service import FVGCandidate, FVGService
|
||||
|
||||
|
||||
class FVGStateTests(unittest.TestCase):
|
||||
@@ -20,7 +20,13 @@ class FVGStateTests(unittest.TestCase):
|
||||
self.assertEqual(status, "touched")
|
||||
self.assertAlmostEqual(fill_pct, 0.2)
|
||||
self.assertEqual(meta["first_touch_ts"], 1)
|
||||
self.assertEqual(meta["last_touch_ts"], 1)
|
||||
self.assertEqual(meta["touch_count"], 1)
|
||||
self.assertFalse(meta["invalidated"])
|
||||
self.assertEqual(meta["status_reason"], "partial_fill")
|
||||
self.assertFalse(meta["has_data_gap"])
|
||||
self.assertEqual(meta["gap_count"], 0)
|
||||
self.assertEqual(meta["data_quality_warnings"], [])
|
||||
|
||||
def test_normalizes_datetime_touch_timestamp_for_meta(self) -> None:
|
||||
fill_pct, status, meta = compute_fvg_fill_state(
|
||||
@@ -33,6 +39,7 @@ class FVGStateTests(unittest.TestCase):
|
||||
self.assertEqual(status, "touched")
|
||||
self.assertAlmostEqual(fill_pct, 0.2)
|
||||
self.assertEqual(meta["first_touch_ts"], "2026-04-24T12:00:00+00:00")
|
||||
self.assertEqual(meta["last_touch_ts"], "2026-04-24T12:00:00+00:00")
|
||||
|
||||
def test_marks_fvg_mitigated_when_fully_filled(self) -> None:
|
||||
fill_pct, status, meta = compute_fvg_fill_state(
|
||||
@@ -45,6 +52,8 @@ class FVGStateTests(unittest.TestCase):
|
||||
self.assertEqual(status, "mitigated")
|
||||
self.assertEqual(fill_pct, 1.0)
|
||||
self.assertFalse(meta["invalidated"])
|
||||
self.assertEqual(meta["status_reason"], "mitigation_threshold_reached")
|
||||
self.assertEqual(meta["touch_count"], 1)
|
||||
|
||||
def test_marks_fvg_invalidated_when_structure_breaks_through_zone(self) -> None:
|
||||
fill_pct, status, meta = compute_fvg_fill_state(
|
||||
@@ -57,6 +66,27 @@ class FVGStateTests(unittest.TestCase):
|
||||
self.assertEqual(status, "invalidated")
|
||||
self.assertGreater(fill_pct, 0.0)
|
||||
self.assertTrue(meta["invalidated"])
|
||||
self.assertEqual(meta["status_reason"], "structure_break")
|
||||
|
||||
def test_records_data_gap_warning_when_post_fvg_candles_skip_forward(self) -> None:
|
||||
fill_pct, status, meta = compute_fvg_fill_state(
|
||||
direction="bullish",
|
||||
upper=105.0,
|
||||
lower=100.0,
|
||||
candles=[
|
||||
{"ts_open": 1, "ts_close": 2, "high": 104.0, "low": 103.0, "close": 103.5},
|
||||
{"ts_open": 4, "ts_close": 5, "high": 104.5, "low": 103.5, "close": 104.0},
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(status, "touched")
|
||||
self.assertAlmostEqual(fill_pct, 0.2)
|
||||
self.assertEqual(meta["first_touch_ts"], 1)
|
||||
self.assertEqual(meta["last_touch_ts"], 4)
|
||||
self.assertEqual(meta["touch_count"], 2)
|
||||
self.assertTrue(meta["has_data_gap"])
|
||||
self.assertEqual(meta["gap_count"], 1)
|
||||
self.assertEqual(meta["data_quality_warnings"], ["candle_gap_detected"])
|
||||
|
||||
|
||||
class FVGIdempotencyTests(unittest.TestCase):
|
||||
@@ -114,5 +144,31 @@ class FVGIdempotencyTests(unittest.TestCase):
|
||||
self.assertTrue(fvg_state_matches_update(row, 0.9327354260089334, "touched", meta))
|
||||
|
||||
|
||||
class FVGDetectionTests(unittest.TestCase):
|
||||
def test_detect_fvg_collapses_duplicate_displacement_context_to_latest_id(self) -> None:
|
||||
class StubRepo:
|
||||
def fetch_recent_candles(self, instrument_id, timeframe, limit=200):
|
||||
return [
|
||||
{"ts_open": 1, "ts_close": 2, "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.5},
|
||||
{"ts_open": 2, "ts_close": 3, "open": 100.5, "high": 103.0, "low": 100.2, "close": 102.8},
|
||||
{"ts_open": 3, "ts_close": 4, "open": 103.5, "high": 106.0, "low": 104.0, "close": 105.5},
|
||||
]
|
||||
|
||||
def fetch_recent_displacement(self, instrument_id, timeframe, limit=50):
|
||||
return [
|
||||
{"id": 7, "direction": "bullish", "end_ts": 3, "is_valid": True},
|
||||
{"id": 9, "direction": "bullish", "end_ts": 3, "is_valid": True},
|
||||
]
|
||||
|
||||
service = FVGService(repository=StubRepo())
|
||||
|
||||
candidates = service.detect_fvg(instrument_id=1, timeframe="1m")
|
||||
|
||||
self.assertEqual(len(candidates), 1)
|
||||
self.assertEqual(candidates[0].direction, "bullish")
|
||||
self.assertEqual(candidates[0].related_displacement_id, 9)
|
||||
self.assertTrue(candidates[0].meta["has_displacement_context"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -4,7 +4,7 @@ import unittest
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
|
||||
|
||||
from src.services.liquidity import LiquidityPoolCandidate, build_equal_pool_candidates, dedupe_candidates, pool_matches_candidate
|
||||
from src.services.liquidity import LiquidityPoolCandidate, LiquidityPoolService, build_equal_pool_candidates, dedupe_candidates, pool_matches_candidate
|
||||
|
||||
|
||||
class EqualLiquidityPoolTests(unittest.TestCase):
|
||||
@@ -144,6 +144,47 @@ class LiquidityIdempotencyTests(unittest.TestCase):
|
||||
|
||||
self.assertTrue(pool_matches_candidate(pool, candidate))
|
||||
|
||||
def test_persist_candidates_skips_equivalent_merged_level_when_pool_type_differs(self) -> None:
|
||||
candidate = LiquidityPoolCandidate(
|
||||
side="buy_side",
|
||||
pool_type="equal_highs",
|
||||
price_low=Decimal("100.00"),
|
||||
price_high=Decimal("100.20"),
|
||||
reference_ids=[1, 2],
|
||||
created_ts="2026-04-21T00:00:00Z",
|
||||
meta={"merged_pool_types": ["equal_highs", "prior_high"], "source_pool_types": ["equal_highs", "prior_high"]},
|
||||
)
|
||||
|
||||
class StubRepo:
|
||||
def __init__(self):
|
||||
self.insert_calls = 0
|
||||
|
||||
def fetch_recent_pools_at_level(self, instrument_id, timeframe, side, price_low, price_high, limit=10):
|
||||
return [
|
||||
{
|
||||
"side": "buy_side",
|
||||
"pool_type": "prior_high",
|
||||
"price_low": Decimal("100.00"),
|
||||
"price_high": Decimal("100.20"),
|
||||
"reference_ids": [1, 2],
|
||||
"meta": {
|
||||
"merged_pool_types": ["equal_highs", "prior_high"],
|
||||
"source_pool_types": ["equal_highs", "prior_high"],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
def insert_pool(self, **kwargs):
|
||||
self.insert_calls += 1
|
||||
|
||||
repo = StubRepo()
|
||||
service = LiquidityPoolService(repository=repo)
|
||||
|
||||
persisted = service.persist_candidates(instrument_id=1, timeframe="1m", candidates=[candidate])
|
||||
|
||||
self.assertEqual(persisted, 0)
|
||||
self.assertEqual(repo.insert_calls, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -22,6 +22,8 @@ class MSSAcceptanceTests(unittest.TestCase):
|
||||
self.assertTrue(result["has_retest"])
|
||||
self.assertTrue(result["held_boundary"])
|
||||
self.assertFalse(result["reclaimed_old_control"])
|
||||
self.assertEqual(result["state"], "accepted")
|
||||
self.assertEqual(result["status_reason"], "retest_held")
|
||||
|
||||
def test_bearish_acceptance_fails_when_close_reclaims_old_control(self) -> None:
|
||||
result = evaluate_mss_acceptance(
|
||||
@@ -37,6 +39,25 @@ class MSSAcceptanceTests(unittest.TestCase):
|
||||
self.assertTrue(result["has_retest"])
|
||||
self.assertFalse(result["held_boundary"])
|
||||
self.assertTrue(result["reclaimed_old_control"])
|
||||
self.assertEqual(result["state"], "invalidated")
|
||||
self.assertEqual(result["status_reason"], "old_control_reclaimed")
|
||||
|
||||
def test_acceptance_marks_rejected_when_retest_is_not_observed(self) -> None:
|
||||
result = evaluate_mss_acceptance(
|
||||
direction="bullish",
|
||||
broken_level=100.0,
|
||||
candles=[
|
||||
{"low": 100.2, "high": 102.0, "close": 101.7},
|
||||
{"low": 100.4, "high": 103.1, "close": 102.6},
|
||||
],
|
||||
)
|
||||
|
||||
self.assertFalse(result["accepted"])
|
||||
self.assertFalse(result["has_retest"])
|
||||
self.assertTrue(result["held_boundary"])
|
||||
self.assertFalse(result["reclaimed_old_control"])
|
||||
self.assertEqual(result["state"], "rejected")
|
||||
self.assertEqual(result["status_reason"], "retest_not_observed")
|
||||
|
||||
def test_detect_mss_candidates_only_returns_confirmed_acceptance(self) -> None:
|
||||
swings = [
|
||||
@@ -61,6 +82,56 @@ class MSSAcceptanceTests(unittest.TestCase):
|
||||
self.assertTrue(candidates[0].accepted)
|
||||
self.assertEqual(candidates[0].direction, "bullish")
|
||||
self.assertEqual(candidates[0].meta["retest_count"], 1)
|
||||
self.assertEqual(candidates[0].meta["state"], "accepted")
|
||||
self.assertEqual(candidates[0].meta["status_reason"], "retest_held")
|
||||
|
||||
def test_detect_mss_candidates_collapses_duplicate_displacement_context_to_latest_id(self) -> None:
|
||||
swings = [
|
||||
{"id": 1, "kind": "swing_high", "price": 100.0, "ts": 2},
|
||||
{"id": 2, "kind": "swing_low", "price": 95.0, "ts": 1},
|
||||
]
|
||||
displacements = [
|
||||
{"id": 9, "direction": "bullish", "end_ts": 5, "is_valid": True},
|
||||
{"id": 11, "direction": "bullish", "end_ts": 5, "is_valid": True},
|
||||
]
|
||||
candles = [
|
||||
{"ts_open": 1, "ts_close": 2, "open": 97.0, "high": 100.0, "low": 96.0, "close": 98.0},
|
||||
{"ts_open": 2, "ts_close": 3, "open": 98.0, "high": 99.0, "low": 95.0, "close": 96.0},
|
||||
{"ts_open": 3, "ts_close": 4, "open": 96.0, "high": 102.0, "low": 95.5, "close": 101.3},
|
||||
{"ts_open": 4, "ts_close": 5, "open": 101.3, "high": 103.0, "low": 100.5, "close": 102.2},
|
||||
{"ts_open": 5, "ts_close": 6, "open": 102.2, "high": 102.8, "low": 99.9, "close": 100.4},
|
||||
{"ts_open": 6, "ts_close": 7, "open": 100.4, "high": 103.5, "low": 100.2, "close": 103.0},
|
||||
]
|
||||
|
||||
candidates = detect_mss_candidates(swings=swings, displacements=displacements, candles=candles, acceptance_window=3)
|
||||
|
||||
self.assertEqual(len(candidates), 1)
|
||||
self.assertEqual(candidates[0].displacement_event_id, 11)
|
||||
self.assertEqual(candidates[0].direction, "bullish")
|
||||
|
||||
def test_detect_mss_candidates_returns_latest_rejected_state_when_retest_is_missing(self) -> None:
|
||||
swings = [
|
||||
{"id": 1, "kind": "swing_high", "price": 100.0, "ts": 2},
|
||||
{"id": 2, "kind": "swing_low", "price": 95.0, "ts": 1},
|
||||
]
|
||||
displacements = [
|
||||
{"id": 9, "direction": "bullish", "end_ts": 5, "is_valid": True},
|
||||
]
|
||||
candles = [
|
||||
{"ts_open": 1, "ts_close": 2, "open": 97.0, "high": 100.0, "low": 96.0, "close": 98.0},
|
||||
{"ts_open": 2, "ts_close": 3, "open": 98.0, "high": 99.0, "low": 95.0, "close": 96.0},
|
||||
{"ts_open": 3, "ts_close": 4, "open": 96.0, "high": 102.0, "low": 95.5, "close": 101.3},
|
||||
{"ts_open": 4, "ts_close": 5, "open": 101.3, "high": 103.0, "low": 100.5, "close": 102.2},
|
||||
{"ts_open": 5, "ts_close": 6, "open": 102.2, "high": 103.4, "low": 100.3, "close": 102.9},
|
||||
{"ts_open": 6, "ts_close": 7, "open": 102.9, "high": 104.1, "low": 100.2, "close": 103.6},
|
||||
]
|
||||
|
||||
candidates = detect_mss_candidates(swings=swings, displacements=displacements, candles=candles, acceptance_window=3)
|
||||
|
||||
self.assertEqual(len(candidates), 1)
|
||||
self.assertFalse(candidates[0].accepted)
|
||||
self.assertEqual(candidates[0].meta["state"], "rejected")
|
||||
self.assertEqual(candidates[0].meta["status_reason"], "retest_not_observed")
|
||||
|
||||
|
||||
class MSSIdempotencyTests(unittest.TestCase):
|
||||
@@ -121,6 +192,39 @@ class MSSIdempotencyTests(unittest.TestCase):
|
||||
self.assertEqual(persisted, 0)
|
||||
self.assertEqual(repo.insert_calls, 0)
|
||||
|
||||
def test_persist_mss_records_rejected_state_for_downstream_consumers(self) -> None:
|
||||
candidate = MSSCandidate(
|
||||
direction="bullish",
|
||||
broken_level=100.0,
|
||||
reference_swing_id=10,
|
||||
displacement_event_id=20,
|
||||
accepted=False,
|
||||
ts="t",
|
||||
meta={"state": "rejected", "status_reason": "retest_not_observed"},
|
||||
)
|
||||
|
||||
class StubRepo:
|
||||
def __init__(self):
|
||||
self.insert_calls = 0
|
||||
self.last_insert = None
|
||||
|
||||
def fetch_latest_mss_event(self, instrument_id, timeframe):
|
||||
return None
|
||||
|
||||
def insert_mss_event(self, **kwargs):
|
||||
self.insert_calls += 1
|
||||
self.last_insert = kwargs
|
||||
|
||||
repo = StubRepo()
|
||||
service = MSSService(repository=repo)
|
||||
|
||||
persisted = service.persist_mss(instrument_id=1, timeframe="1m", candidates=[candidate])
|
||||
|
||||
self.assertEqual(persisted, 1)
|
||||
self.assertEqual(repo.insert_calls, 1)
|
||||
self.assertFalse(repo.last_insert["accepted"])
|
||||
self.assertEqual(repo.last_insert["meta"]["state"], "rejected")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -31,17 +31,51 @@ class PreMarketReportTests(unittest.TestCase):
|
||||
|
||||
|
||||
class IntradayReportTests(unittest.TestCase):
|
||||
def test_builds_intraday_explanation_and_invalidations(self) -> None:
|
||||
def test_builds_intraday_explanation_with_richer_structure_and_fvg_state(self) -> None:
|
||||
report = IntradayReportService().build_report(
|
||||
sweep_event={"side_swept": "buy_side", "close_back_inside": True},
|
||||
structure_event={"event_type": "MSS", "direction": "bearish", "accepted": True},
|
||||
fvg_zone={"direction": "bearish", "status": "mitigated", "fill_pct": 1.0, "meta": {}},
|
||||
trade_signal={"status": "awaiting_confirmation", "side": "sell", "session_code": "NY_AM", "invalidations": ["session_mismatch"]},
|
||||
structure_event={
|
||||
"event_type": "MSS",
|
||||
"direction": "bearish",
|
||||
"accepted": False,
|
||||
"meta": {"state": "invalidated", "status_reason": "old_control_reclaimed"},
|
||||
},
|
||||
fvg_zone={
|
||||
"direction": "bearish",
|
||||
"status": "mitigated",
|
||||
"fill_pct": 1.0,
|
||||
"meta": {
|
||||
"status_reason": "mitigation_threshold_reached",
|
||||
"data_quality_warnings": ["candle_gap_detected"],
|
||||
},
|
||||
},
|
||||
trade_signal={
|
||||
"status": "awaiting_confirmation",
|
||||
"side": "sell",
|
||||
"session_code": "NY_AM",
|
||||
"invalidations": ["session_mismatch"],
|
||||
"meta": {
|
||||
"structure_state": "fvg_mitigated",
|
||||
"mss_status_reason": "old_control_reclaimed",
|
||||
"fvg_status_reason": "mitigation_threshold_reached",
|
||||
"fvg_data_quality_warnings": ["candle_gap_detected"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(report["report_type"], "intraday_explanation")
|
||||
self.assertFalse(report["signal"]["is_valid"])
|
||||
self.assertIn("fvg_no_longer_actionable", report["invalidations"])
|
||||
self.assertEqual(report["structure"]["state"], "invalidated")
|
||||
self.assertEqual(report["structure"]["status_reason"], "old_control_reclaimed")
|
||||
self.assertEqual(report["fvg"]["status"], "mitigated")
|
||||
self.assertEqual(report["fvg"]["status_reason"], "mitigation_threshold_reached")
|
||||
self.assertEqual(report["signal"]["structure_state"], "fvg_mitigated")
|
||||
self.assertEqual(report["signal"]["mss_status_reason"], "old_control_reclaimed")
|
||||
self.assertEqual(report["signal"]["fvg_status_reason"], "mitigation_threshold_reached")
|
||||
self.assertEqual(report["signal"]["fvg_data_quality_warnings"], ["candle_gap_detected"])
|
||||
self.assertIn("fvg_mitigated", report["invalidations"])
|
||||
self.assertIn("mss_invalidated", report["invalidations"])
|
||||
self.assertIn("candle_gap_detected", report["invalidations"])
|
||||
self.assertIn("session_mismatch", report["invalidations"])
|
||||
|
||||
|
||||
@@ -49,7 +83,18 @@ class PostTradeReviewTests(unittest.TestCase):
|
||||
def test_builds_post_trade_review(self) -> None:
|
||||
report = PostTradeReviewService().build_report(
|
||||
trade_execution={"status": "rejected", "meta": {"risk_checks": ["session_not_allowed"]}},
|
||||
trade_signal={"status": "awaiting_context", "invalidations": ["session_mismatch"]},
|
||||
trade_signal={
|
||||
"status": "awaiting_context",
|
||||
"invalidations": ["session_mismatch"],
|
||||
"meta": {
|
||||
"structure_state": "mss_invalidated",
|
||||
"mss_state": "invalidated",
|
||||
"mss_status_reason": "old_control_reclaimed",
|
||||
"fvg_status": "touched",
|
||||
"fvg_status_reason": "partial_fill",
|
||||
"fvg_data_quality_warnings": ["candle_gap_detected"],
|
||||
},
|
||||
},
|
||||
backtest_result={"outcome": "loss", "invalidated_before_entry": False, "error_tags": ["same_candle_entry_exit_ambiguous"]},
|
||||
)
|
||||
|
||||
@@ -59,7 +104,12 @@ class PostTradeReviewTests(unittest.TestCase):
|
||||
self.assertIn("session_not_allowed", report["error_types"])
|
||||
self.assertEqual(report["execution_audit"]["audit_status"], "review")
|
||||
self.assertEqual(report["execution_audit"]["lifecycle_state"], "rejected")
|
||||
self.assertEqual(report["signal_context"]["structure_state"], "mss_invalidated")
|
||||
self.assertEqual(report["signal_context"]["mss_status_reason"], "old_control_reclaimed")
|
||||
self.assertEqual(report["signal_context"]["fvg_status_reason"], "partial_fill")
|
||||
self.assertIn("inspect_stop_and_structure_context", report["improvement_notes"])
|
||||
self.assertIn("review_signal_structure_state", report["improvement_notes"])
|
||||
self.assertIn("review_signal_data_quality_warnings", report["improvement_notes"])
|
||||
self.assertIn("review_same_candle_ambiguity_cases", report["improvement_notes"])
|
||||
|
||||
|
||||
|
||||
@@ -90,12 +90,12 @@ class RuntimeScriptJsonTests(unittest.TestCase):
|
||||
self.assertEqual(exit_code, 2)
|
||||
run_migrations.assert_called_once_with(env={"MIGRATIONS_OUTPUT_JSON": "1"})
|
||||
|
||||
def test_run_dev_preserves_zero_exit_code(self) -> None:
|
||||
with patch("scripts.run_dev.app_main") as app_main:
|
||||
exit_code = run_dev.main()
|
||||
def test_run_dev_uses_env_and_returns_when_server_stops(self) -> None:
|
||||
with patch("scripts.run_dev.serve_api") as serve_api:
|
||||
exit_code = run_dev.main(env={"AI_ICT_API_HOST": "127.0.0.1", "AI_ICT_API_PORT": "8124"})
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
app_main.assert_called_once_with()
|
||||
serve_api.assert_called_once_with(host="127.0.0.1", port=8124)
|
||||
|
||||
def test_run_api_server_uses_env_and_returns_when_server_stops(self) -> None:
|
||||
with patch("scripts.run_api_server.serve_api") as serve_api:
|
||||
|
||||
@@ -46,41 +46,76 @@ class SignalSessionFilterTests(unittest.TestCase):
|
||||
|
||||
|
||||
class SignalStructureAlignmentTests(unittest.TestCase):
|
||||
def test_rejects_unaccepted_mss(self) -> None:
|
||||
def test_rejects_rejected_mss_with_explicit_state_reason(self) -> None:
|
||||
result = validate_signal_context(
|
||||
bias={"draw_side": "buy_side"},
|
||||
fvg={"direction": "bullish", "status": "open", "meta": {}},
|
||||
mss={"direction": "bullish", "accepted": False},
|
||||
mss={"direction": "bullish", "accepted": False, "meta": {"state": "rejected", "status_reason": "retest_not_observed"}},
|
||||
)
|
||||
|
||||
self.assertFalse(result["is_actionable"])
|
||||
self.assertEqual(result["reason"], "mss_not_accepted")
|
||||
self.assertEqual(result["reason"], "mss_rejected")
|
||||
self.assertEqual(result["structure_state"], "mss_rejected")
|
||||
self.assertEqual(result["mss_state"], "rejected")
|
||||
self.assertEqual(result["mss_status_reason"], "retest_not_observed")
|
||||
|
||||
def test_rejects_invalidated_mss_with_explicit_state_reason(self) -> None:
|
||||
result = validate_signal_context(
|
||||
bias={"draw_side": "buy_side"},
|
||||
fvg={"direction": "bullish", "status": "open", "meta": {}},
|
||||
mss={"direction": "bullish", "accepted": False, "meta": {"state": "invalidated", "status_reason": "old_control_reclaimed"}},
|
||||
)
|
||||
|
||||
self.assertFalse(result["is_actionable"])
|
||||
self.assertEqual(result["reason"], "mss_invalidated")
|
||||
self.assertEqual(result["structure_state"], "mss_invalidated")
|
||||
self.assertEqual(result["mss_state"], "invalidated")
|
||||
self.assertEqual(result["mss_status_reason"], "old_control_reclaimed")
|
||||
|
||||
def test_rejects_mitigated_or_invalidated_fvg(self) -> None:
|
||||
mitigated = validate_signal_context(
|
||||
bias={"draw_side": "buy_side"},
|
||||
fvg={"direction": "bullish", "status": "mitigated", "meta": {}},
|
||||
mss={"direction": "bullish", "accepted": True},
|
||||
fvg={"direction": "bullish", "status": "mitigated", "meta": {"status_reason": "mitigation_threshold_reached"}},
|
||||
mss={"direction": "bullish", "accepted": True, "meta": {"state": "accepted", "status_reason": "retest_held"}},
|
||||
)
|
||||
invalidated = validate_signal_context(
|
||||
bias={"draw_side": "buy_side"},
|
||||
fvg={"direction": "bullish", "status": "touched", "meta": {"invalidated": True}},
|
||||
mss={"direction": "bullish", "accepted": True},
|
||||
fvg={"direction": "bullish", "status": "touched", "meta": {"invalidated": True, "status_reason": "structure_break"}},
|
||||
mss={"direction": "bullish", "accepted": True, "meta": {"state": "accepted", "status_reason": "retest_held"}},
|
||||
)
|
||||
|
||||
self.assertEqual(mitigated["reason"], "fvg_not_actionable")
|
||||
self.assertEqual(mitigated["reason"], "fvg_mitigated")
|
||||
self.assertEqual(mitigated["structure_state"], "fvg_mitigated")
|
||||
self.assertEqual(mitigated["fvg_status_reason"], "mitigation_threshold_reached")
|
||||
self.assertEqual(invalidated["reason"], "fvg_invalidated")
|
||||
self.assertEqual(invalidated["structure_state"], "fvg_invalidated")
|
||||
self.assertEqual(invalidated["fvg_status_reason"], "structure_break")
|
||||
|
||||
def test_accepts_aligned_structure(self) -> None:
|
||||
result = validate_signal_context(
|
||||
bias={"draw_side": "sell_side"},
|
||||
fvg={"direction": "bearish", "status": "touched", "meta": {}, "related_displacement_id": 7},
|
||||
mss={"direction": "bearish", "accepted": True, "displacement_event_id": 7},
|
||||
fvg={
|
||||
"direction": "bearish",
|
||||
"status": "touched",
|
||||
"meta": {"status_reason": "partial_fill", "touch_count": 1},
|
||||
"related_displacement_id": 7,
|
||||
},
|
||||
mss={
|
||||
"direction": "bearish",
|
||||
"accepted": True,
|
||||
"displacement_event_id": 7,
|
||||
"meta": {"state": "accepted", "status_reason": "retest_held"},
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(result["is_actionable"])
|
||||
self.assertEqual(result["side"], "sell")
|
||||
self.assertEqual(result["reason"], "aligned")
|
||||
self.assertEqual(result["structure_state"], "aligned")
|
||||
self.assertEqual(result["mss_state"], "accepted")
|
||||
self.assertEqual(result["mss_status_reason"], "retest_held")
|
||||
self.assertEqual(result["fvg_status"], "touched")
|
||||
self.assertEqual(result["fvg_status_reason"], "partial_fill")
|
||||
|
||||
|
||||
class SignalRRFilterTests(unittest.TestCase):
|
||||
@@ -116,6 +151,13 @@ class SignalRRFilterTests(unittest.TestCase):
|
||||
self.assertGreaterEqual(candidate.rr_tp2, 2.0)
|
||||
self.assertTrue(candidate.meta["evidence_quality"]["is_complete"])
|
||||
self.assertEqual(candidate.meta["rr_filter"]["reason"], "rr_ok")
|
||||
self.assertEqual(candidate.meta["structure_state"], "aligned")
|
||||
self.assertEqual(candidate.meta["mss_state"], "accepted")
|
||||
self.assertEqual(candidate.meta["mss_status_reason"], "retest_held")
|
||||
self.assertEqual(candidate.meta["fvg_status"], "open")
|
||||
self.assertEqual(candidate.meta["fvg_status_reason"], "no_touch")
|
||||
self.assertEqual(candidate.meta["fvg_touch_count"], 0)
|
||||
self.assertEqual(candidate.meta["fvg_data_quality_warnings"], [])
|
||||
|
||||
def test_build_signal_rejects_candidate_when_required_evidence_is_missing(self) -> None:
|
||||
service = SignalService(
|
||||
@@ -180,7 +222,14 @@ class _SignalStubRepository:
|
||||
return self.fvg
|
||||
|
||||
def fetch_latest_mss(self, instrument_id: int, timeframe: str):
|
||||
return {"id": 3, "direction": "bullish", "accepted": True, "broken_level": 100.0, "displacement_event_id": 7}
|
||||
return {
|
||||
"id": 3,
|
||||
"direction": "bullish",
|
||||
"accepted": True,
|
||||
"broken_level": 100.0,
|
||||
"displacement_event_id": 7,
|
||||
"meta": {"state": "accepted", "status_reason": "retest_held"},
|
||||
}
|
||||
|
||||
def fetch_session_at(self, instrument_id: int, ts):
|
||||
return {"session_code": "LONDON"}
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "ai-ict"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "psycopg", extra = ["binary"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "sqlalchemy" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.27" },
|
||||
{ name = "psycopg", extras = ["binary"], specifier = ">=3.1" },
|
||||
{ name = "pydantic", specifier = ">=2.7" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.2" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.4.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f8/450fe3c5938fa737ea4d22699772e6e34e8e24431a47bf4e8a1ceed4a98e/greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", size = 235017, upload-time = "2026-04-27T12:22:26.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
binary = [
|
||||
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-binary"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c0/b389119dd754483d316805260f3e73cdcad97925839107cc7a296f6132b1/psycopg_binary-3.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a89bb9ee11177b2995d87186b1d9fa892d8ea725e85eab28c6525e4cc14ee048", size = 4609740, upload-time = "2026-02-18T16:47:51.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e3/9976eef20f61840285174d360da4c820a311ab39d6b82fa09fbb545be825/psycopg_binary-3.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f7d0cf072c6fbac3795b08c98ef9ea013f11db609659dcfc6b1f6cc31f9e181", size = 4676837, upload-time = "2026-02-18T16:47:55.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/f2/d28ba2f7404fd7f68d41e8a11df86313bd646258244cb12a8dd83b868a97/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:90eecd93073922f085967f3ed3a98ba8c325cbbc8c1a204e300282abd2369e13", size = 5497070, upload-time = "2026-02-18T16:47:59.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/2f/6c5c54b815edeb30a281cfcea96dc93b3bb6be939aea022f00cab7aa1420/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dac7ee2f88b4d7bb12837989ca354c38d400eeb21bce3b73dac02622f0a3c8d6", size = 5172410, upload-time = "2026-02-18T16:48:05.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/75/8206c7008b57de03c1ada46bd3110cc3743f3fd9ed52031c4601401d766d/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62cf8784eb6d35beaee1056d54caf94ec6ecf2b7552395e305518ab61eb8fd2", size = 6763408, upload-time = "2026-02-18T16:48:13.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/5a/ea1641a1e6c8c8b3454b0fcb43c3045133a8b703e6e824fae134088e63bd/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a39f34c9b18e8f6794cca17bfbcd64572ca2482318db644268049f8c738f35a6", size = 5006255, upload-time = "2026-02-18T16:48:22.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/fb/538df099bf55ae1637d52d7ccb6b9620b535a40f4c733897ac2b7bb9e14c/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:883d68d48ca9ff3cb3d10c5fdebea02c79b48eecacdddbf7cce6e7cdbdc216b8", size = 4532694, upload-time = "2026-02-18T16:48:27.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/d1/00780c0e187ea3c13dfc53bd7060654b2232cd30df562aac91a5f1c545ac/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:cab7bc3d288d37a80aa8c0820033250c95e40b1c2b5c57cf59827b19c2a8b69d", size = 4222833, upload-time = "2026-02-18T16:48:31.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/34/a07f1ff713c51d64dc9f19f2c32be80299a2055d5d109d5853662b922cb4/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:56c767007ca959ca32f796b42379fc7e1ae2ed085d29f20b05b3fc394f3715cc", size = 3952818, upload-time = "2026-02-18T16:48:35.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/67/d33f268a7759b4445f3c9b5a181039b01af8c8263c865c1be7a6444d4749/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:da2f331a01af232259a21573a01338530c6016dcfad74626c01330535bcd8628", size = 4258061, upload-time = "2026-02-18T16:48:41.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/3b/0d8d2c5e8e29ccc07d28c8af38445d9d9abcd238d590186cac82ee71fc84/psycopg_binary-3.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:19f93235ece6dbfc4036b5e4f6d8b13f0b8f2b3eeb8b0bd2936d406991bcdd40", size = 3558915, upload-time = "2026-02-18T16:48:46.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/15/021be5c0cbc5b7c1ab46e91cc3434eb42569f79a0592e67b8d25e66d844d/psycopg_binary-3.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6698dbab5bcef8fdb570fc9d35fd9ac52041771bfcfe6fd0fc5f5c4e36f1e99d", size = 4591170, upload-time = "2026-02-18T16:48:55.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/54/a60211c346c9a2f8c6b272b5f2bbe21f6e11800ce7f61e99ba75cf8b63e1/psycopg_binary-3.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:329ff393441e75f10b673ae99ab45276887993d49e65f141da20d915c05aafd8", size = 4670009, upload-time = "2026-02-18T16:49:03.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/53/ac7c18671347c553362aadbf65f92786eef9540676ca24114cc02f5be405/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eb072949b8ebf4082ae24289a2b0fd724da9adc8f22743409d6fd718ddb379df", size = 5469735, upload-time = "2026-02-18T16:49:10.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/c3/4f4e040902b82a344eff1c736cde2f2720f127fe939c7e7565706f96dd44/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:263a24f39f26e19ed7fc982d7859a36f17841b05bebad3eb47bb9cd2dd785351", size = 5152919, upload-time = "2026-02-18T16:49:16.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e7/d929679c6a5c212bcf738806c7c89f5b3d0919f2e1685a0e08d6ff877945/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5152d50798c2fa5bd9b68ec68eb68a1b71b95126c1d70adaa1a08cd5eefdc23d", size = 6738785, upload-time = "2026-02-18T16:49:22.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/b0/09703aeb69a9443d232d7b5318d58742e8ca51ff79f90ffe6b88f1db45e7/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d6a1e56dd267848edb824dbeb08cf5bac649e02ee0b03ba883ba3f4f0bd54f2", size = 4979008, upload-time = "2026-02-18T16:49:27.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/a6/e662558b793c6e13a7473b970fee327d635270e41eded3090ef14045a6a5/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73eaaf4bb04709f545606c1db2f65f4000e8a04cdbf3e00d165a23004692093e", size = 4508255, upload-time = "2026-02-18T16:49:31.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/7f/0f8b2e1d5e0093921b6f324a948a5c740c1447fbb45e97acaf50241d0f39/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:162e5675efb4704192411eaf8e00d07f7960b679cd3306e7efb120bb8d9456cc", size = 4189166, upload-time = "2026-02-18T16:49:35.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ec/ce2e91c33bc8d10b00c87e2f6b0fb570641a6a60042d6a9ae35658a3a797/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:fab6b5e37715885c69f5d091f6ff229be71e235f272ebaa35158d5a46fd548a0", size = 3924544, upload-time = "2026-02-18T16:49:41.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/2f/7718141485f73a924205af60041c392938852aa447a94c8cbd222ff389a1/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4aab31bd6d1057f287c96c0effca3a25584eb9cc702f282ecb96ded7814e830", size = 4235297, upload-time = "2026-02-18T16:49:46.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/f9/1add717e2643a003bbde31b1b220172e64fbc0cb09f06429820c9173f7fc/psycopg_binary-3.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:59aa31fe11a0e1d1bcc2ce37ed35fe2ac84cd65bb9036d049b1a1c39064d0f14", size = 3547659, upload-time = "2026-02-18T16:49:52.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.46.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.49"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2026.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user