1463 lines
74 KiB
Markdown
1463 lines
74 KiB
Markdown
# Build 060.29 — Market Data Access and Replay Architecture
|
||
|
||
**Статус:** Accepted
|
||
|
||
**Build:** 060.29
|
||
|
||
**Подсистема:** Market Data / Historical Access and Replay
|
||
|
||
**Дата начала:** 2026-08-02
|
||
|
||
**Дата завершения:** 2026-08-02
|
||
|
||
**Версия документа:** 1.9
|
||
|
||
---
|
||
|
||
## 1. Назначение
|
||
|
||
Build 060.29 добавляет безопасное чтение сохранённых Canonical Market
|
||
Data и их детерминированное воспроизведение.
|
||
|
||
Build опирается на:
|
||
|
||
- PostgreSQL Canonical Storage из 060.27;
|
||
- Persistent Checkpoint и Startup Recovery из 060.28;
|
||
- существующие неизменяемые Canonical `Trade`, `Quote` и `Candle`.
|
||
|
||
Итоговая цепочка должна выглядеть так:
|
||
|
||
```text
|
||
PostgreSQL Canonical Market Data
|
||
↓
|
||
Historical Access
|
||
↓
|
||
bounded immutable ReplayPlan
|
||
↓
|
||
ReplaySession + Virtual Clock
|
||
↓
|
||
Canonical consumer
|
||
```
|
||
|
||
Historical Access и Replay являются read-side подсистемами. Они не
|
||
заменяют Acquisition Runtime, не продвигают operational checkpoint и не
|
||
записывают повторно воспроизводимые события в Market Data Storage.
|
||
|
||
---
|
||
|
||
## 2. Статус подэтапов
|
||
|
||
| Подэтап | Название | Статус |
|
||
|---|---|---|
|
||
| 060.29.0 | Architecture, Boundaries and Ordering Policy | Accepted |
|
||
| 060.29.1 | Historical Access and Replay Contracts | Accepted |
|
||
| 060.29.2 | Global Replay Sequence Migration and PostgreSQL Trade History | Accepted |
|
||
| 060.29.3 | Quote/Candle History and Snapshot Plan Builder | Accepted |
|
||
| 060.29.4 | Deterministic Replay Clock | Accepted |
|
||
| 060.29.5 | Replay Session and Engine | Accepted |
|
||
| 060.29.6 | Consumer and Composition Integration | Accepted |
|
||
| 060.29.7 | PostgreSQL Replay and Failure Verification | Accepted |
|
||
| 060.29.8 | Final Regression and Acceptance | Accepted |
|
||
|
||
Подэтапы 060.29.0–060.29.6 приняты после отдельных приёмочных
|
||
read-only review. Реализация 060.29.2 ограничена versioned migration 9,
|
||
совместимостью существующих writers и отдельным PostgreSQL Trade
|
||
History reader. 060.29.3 добавляет Quote/Candle History и отдельный
|
||
bounded Replay snapshot. 060.29.4 добавляет детерминированные UTC-часы
|
||
без wall clock, reset и скрытого lifecycle. На этом этапе Build оставался
|
||
`In Progress`. Контракт 060.29.5 утверждён; отдельный `ReplayEngine` не
|
||
создаётся, а принятый playback engine реализован внутри
|
||
`ReplaySession.run()`. Контракт 060.29.6 утверждён: composition использует
|
||
обязательную фабрику consumer, один синхронный blocking
|
||
`ReplaySessionFactory`, новый граф зависимостей на каждый вызов и не
|
||
выполняет автоматический startup. Реализация 060.29.6 принята после
|
||
чистого формального read-only review.
|
||
|
||
Контракт 060.29.7 утверждён как verification-only: новые тесты используют
|
||
существующий безопасный opt-in PostgreSQL harness, реальные Storage,
|
||
Historical Access, ReplayPlan Builder, Composition и Session, но не
|
||
добавляют production consumer, Bootstrap wiring или automatic startup.
|
||
Production-код заранее не изменяется.
|
||
|
||
060.29.7 принят после исправления найденных Pylance/Pyright diagnostics,
|
||
введения обязательного static gate, полной повторной регрессии и
|
||
чистого формального read-only review. Единственная production-правка
|
||
подэтапа уточняет статическое сужение уже проверенного точного union-типа
|
||
без изменения runtime-контракта или поведения Replay.
|
||
|
||
060.29.8 выполнил финальную verification-only матрицу без изменения
|
||
production-кода. Повторены статическая проверка, unit, PostgreSQL,
|
||
integration, opt-out, fixed stress и полная offline-регрессия. Три
|
||
независимых read-only review не выявили открытых findings; подэтап и
|
||
Build 060.29 приняты.
|
||
|
||
---
|
||
|
||
## 3. Исходные инварианты 060.27–060.28
|
||
|
||
1. Canonical Market Data уже прошли transport validation и mapping.
|
||
2. `MarketDataStorage` остаётся фасадом только для записи.
|
||
3. Checkpoint reads остаются узкой границей Startup Hydration и не
|
||
превращаются в Historical API.
|
||
4. Production Runtime сохраняет Canonical Trade до продвижения
|
||
operational checkpoint.
|
||
5. Live и Recovery используют одного владельца записи Trades.
|
||
6. PostgreSQL pool имеет явный Application lifecycle.
|
||
7. Consistency, Recovery и Runtime не зависят от `psycopg`.
|
||
8. Trade ID использует signed 32-bit rollover-aware контракт.
|
||
9. Retention по умолчанию выключен и запускается только явно.
|
||
|
||
---
|
||
|
||
## 4. Принятые архитектурные решения
|
||
|
||
### 4.1. Отдельная read-side граница
|
||
|
||
Historical Access размещается отдельно от существующего write-only
|
||
Storage API:
|
||
|
||
```text
|
||
market_data.access contracts
|
||
↑
|
||
PostgreSQL History adapters
|
||
```
|
||
|
||
Существующие `TradeStorageProtocol`, `QuoteStorageProtocol`,
|
||
`CandleStorageProtocol`, `TradeCheckpointStorageProtocol` и
|
||
`MarketDataStorage` не расширяются историческими запросами.
|
||
|
||
PostgreSQL implementation зависит от DB-neutral read-контрактов.
|
||
Контракты не импортируют `psycopg`, pool, Bootstrap, Runtime, Recovery,
|
||
Telegram или Trading.
|
||
|
||
### 4.2. Scope типов данных
|
||
|
||
Historical Access предусматривает чтение трёх уже существующих
|
||
Canonical таблиц:
|
||
|
||
- Trades;
|
||
- Quotes;
|
||
- Candle revisions.
|
||
|
||
Это не подключает persistent Quote/Candle consumers и не объявляет их
|
||
Production Runtime завершённым. Полная production-проверка
|
||
`Live → Storage → Historical → Replay` в этом Build обязательна для
|
||
Trades. Quote/Candle read-path проверяется на repository и PostgreSQL
|
||
уровне.
|
||
|
||
### 4.3. Совместимость Live и Replay
|
||
|
||
Live и Replay используют одинаковые Canonical classes и значения:
|
||
|
||
```text
|
||
Trade | Quote | Candle
|
||
```
|
||
|
||
PostgreSQL reader создаёт новый Canonical объект, поэтому identity с
|
||
первоначальным Live-объектом не обещается. После материализации
|
||
Historical record и созданный из него Replay event используют один и
|
||
тот же payload без повторного копирования.
|
||
|
||
Replay не создаёт `ReplayTrade`, не меняет `source` на `replay` и не
|
||
восстанавливает transport document. Технический Replay envelope не
|
||
объявляется существующим Live Runtime event-контрактом. Совместимость
|
||
гарантируется на уровне точных Canonical payload classes и значений.
|
||
|
||
### 4.4. Гарантируемый порядок
|
||
|
||
Build гарантирует детерминированную event-time хронологию Canonical
|
||
данных. Он не обещает точное повторение порядка исходных сетевых
|
||
пакетов, потому что raw transport arrival log не сохраняется.
|
||
|
||
Оси времени:
|
||
|
||
| Тип | Historical Query | Replay |
|
||
|---|---|---|
|
||
| Trade | `executed_at` | `executed_at` |
|
||
| Quote | `received_at` | `received_at` |
|
||
| Candle revision | `open_time` | `observed_at` |
|
||
|
||
`exchange_timestamp` Quote не используется как Replay time, чтобы не
|
||
создавать look-ahead. Candle revision воспроизводится в момент её
|
||
наблюдения, а не задним числом в `open_time`.
|
||
|
||
---
|
||
|
||
## 5. Архитектурная граница и направление импортов
|
||
|
||
```text
|
||
Canonical Models
|
||
↑ ↑
|
||
Historical contracts Replay contracts
|
||
↑ ↑
|
||
PostgreSQL readers → ReplayPlan builder
|
||
↓
|
||
ReplaySession
|
||
↓
|
||
async consumer
|
||
```
|
||
|
||
Запрещённые зависимости:
|
||
|
||
```text
|
||
access/replay → acquisition.runtime
|
||
access/replay → storage write facade
|
||
access/replay → bootstrap/application
|
||
access/replay → telegram/trading
|
||
access/replay → runtime_events/EventBus
|
||
```
|
||
|
||
Replay не вызывает `TradeStreamConsistencyController`. Иначе
|
||
воспроизведение изменило бы durable history и operational checkpoint.
|
||
|
||
---
|
||
|
||
## 6. Historical contracts
|
||
|
||
### 6.1. Time range
|
||
|
||
Все публичные запросы используют timezone-aware полуоткрытый диапазон:
|
||
|
||
```text
|
||
[start_time, end_time)
|
||
```
|
||
|
||
Время нормализуется в UTC. Требуется строгое
|
||
`start_time < end_time`.
|
||
|
||
### 6.2. Records
|
||
|
||
Historical record хранит точный Canonical payload и технические поля,
|
||
которые не должны добавляться в Canonical model:
|
||
|
||
```text
|
||
TradeHistoryRecord
|
||
├── venue
|
||
├── trade: Trade
|
||
├── first_observed_at
|
||
├── last_observed_at
|
||
├── observation_sources
|
||
├── replay_sequence
|
||
└── canonical_schema_version
|
||
|
||
QuoteHistoryRecord
|
||
├── venue
|
||
├── quote: Quote
|
||
├── observation_sources
|
||
├── replay_sequence
|
||
└── canonical_schema_version
|
||
|
||
CandleRevisionHistoryRecord
|
||
├── venue
|
||
├── candle: Candle
|
||
├── observed_at
|
||
├── is_final
|
||
├── observation_sources
|
||
├── replay_sequence
|
||
└── canonical_schema_version
|
||
```
|
||
|
||
Records являются immutable, используют `slots` и не копируют Canonical
|
||
payload.
|
||
|
||
### 6.3. Queries и pages
|
||
|
||
Каждый запрос содержит один `venue`, один canonical `symbol`, один
|
||
UTC-диапазон и bounded page limit. Candle query дополнительно содержит
|
||
case-sensitive `interval`.
|
||
|
||
Пустая страница является корректным результатом. Повреждённая строка,
|
||
неизвестная версия Canonical schema или неверный enum являются
|
||
integrity error; строка не пропускается молча.
|
||
|
||
---
|
||
|
||
## 7. Global replay sequence, ordering и keyset
|
||
|
||
### 7.1. Причина нового durable tie-breaker
|
||
|
||
Текущие таблицы не содержат подходящего immutable порядка Replay:
|
||
|
||
- signed `trade_id` нельзя сортировать обычным числовым сравнением на
|
||
границах rollover;
|
||
- `first_observed_at` может измениться при обновлении provenance;
|
||
- между разными типами данных нет общего устойчивого tie-breaker.
|
||
|
||
060.29.2 добавляет одну глобальную положительную
|
||
`replay_sequence` для Trades, Quotes и Candle revisions.
|
||
|
||
Для новых строк sequence назначается только первой durable-вставке и не
|
||
изменяется при duplicate/provenance update. Пропуски sequence после
|
||
rollback допустимы: важен порядок, а не непрерывность значений.
|
||
|
||
### 7.2. Детерминированный backfill существующих строк
|
||
|
||
До migration 060.29.2 база не сохраняла global ordinal. Поэтому точный
|
||
первоначальный transport, insertion или commit order существующих строк
|
||
восстановить невозможно.
|
||
|
||
Backfill использует один явно фиксированный порядок:
|
||
|
||
```text
|
||
Replay event_time
|
||
→ data_type rank: Trade=1, Quote=2, Candle revision=3
|
||
→ venue COLLATE "C"
|
||
→ symbol COLLATE "C"
|
||
→ полная durable identity типа
|
||
```
|
||
|
||
Полная identity:
|
||
|
||
```text
|
||
Trade: executed_at, trade_id
|
||
Quote: received_at
|
||
Candle revision: interval COLLATE "C", open_time, observed_at
|
||
```
|
||
|
||
Этот порядок воспроизводим для одинакового committed dataset, но не
|
||
выдаётся за исторический порядок сетевых пакетов. В частности, для уже
|
||
существующих Trades с одинаковым `executed_at` backfill не может
|
||
доказать первоначальный rollover order.
|
||
|
||
Для новых строк immutable sequence сохраняет порядок первой записи при
|
||
одинаковом event time. Batch writer обязан назначать sequence в порядке
|
||
входного Canonical batch, даже если блокировки строк берутся в другом
|
||
безопасном порядке. Между конкурентными транзакциями global sequence
|
||
остаётся устойчивым tie-breaker, но не объявляется commit chronology.
|
||
|
||
### 7.3. Stable order
|
||
|
||
Historical и Replay ordering используют:
|
||
|
||
```text
|
||
(event_time, replay_sequence)
|
||
```
|
||
|
||
`trade_id` не входит в cursor и order key. Для новых строк одинаковый
|
||
timestamp сохраняет durable insertion order на границах:
|
||
|
||
```text
|
||
INT32_MAX → INT32_MIN
|
||
-1 → 0
|
||
```
|
||
|
||
Для backfilled строк порядок остаётся детерминированным по policy
|
||
раздела 7.2, но не заявляется точным первоначальным rollover order.
|
||
|
||
### 7.4. Pagination
|
||
|
||
Historical Queries используют только forward keyset pagination.
|
||
`OFFSET` запрещён.
|
||
|
||
Структурированный cursor содержит:
|
||
|
||
- тип данных;
|
||
- query scope;
|
||
- исходный временной диапазон;
|
||
- последнее `event_time`;
|
||
- последнее `replay_sequence`;
|
||
- version.
|
||
|
||
Cursor другого типа, symbol, venue, interval или окна отклоняется.
|
||
Page limit можно менять между запросами с тем же cursor.
|
||
|
||
### 7.5. Shared sequence и durable schema
|
||
|
||
Migration 9 `add_global_market_data_replay_sequence` создаёт один общий
|
||
PostgreSQL sequence:
|
||
|
||
```text
|
||
market_data.replay_sequence
|
||
BIGINT
|
||
MINVALUE 1
|
||
CACHE 1
|
||
NO CYCLE
|
||
OWNED BY NONE
|
||
```
|
||
|
||
Отдельная registry/counter table не создаётся. Единственным
|
||
распределителем ordinal для новых Trades, Quotes и Candle revisions
|
||
является shared sequence. Его нельзя привязать через `OWNED BY` к одной
|
||
колонке, потому что он обслуживает сразу три таблицы.
|
||
|
||
Каждая из таблиц получает колонку `replay_sequence BIGINT`, общий
|
||
`DEFAULT nextval(...)`, `NOT NULL` и именованный `CHECK (> 0)`. После
|
||
backfill изменение уже назначенного значения запрещает database trigger
|
||
на partitioned parent. Тот же контракт обязан действовать в default,
|
||
ранее созданных monthly и будущих partitions.
|
||
|
||
Один cross-table `UNIQUE` constraint для трёх partitioned tables в
|
||
PostgreSQL недоступен. Глобальная уникальность обеспечивается shared
|
||
sequence и контролируемыми repositories. Ручная вставка явно заданного
|
||
`replay_sequence` вне этих adapters не входит в поддерживаемый контракт.
|
||
|
||
### 7.6. Блокирующая migration и атомарный backfill
|
||
|
||
Migration 9 выполняется существующим `StorageMigrationRunner` в одной
|
||
transaction. Сначала runner владеет общим
|
||
`STORAGE_MIGRATION_ADVISORY_LOCK_ID`. Затем первым statement migration 9,
|
||
до любых table locks, берётся единый
|
||
`MARKET_DATA_PARTITION_ADVISORY_LOCK_ID`. Этот же partition advisory lock
|
||
используют `PostgresMarketDataPartitionManager` и
|
||
`PostgresMarketDataRetentionService`.
|
||
|
||
Полный обязательный порядок блокировок:
|
||
|
||
```text
|
||
STORAGE_MIGRATION_ADVISORY_LOCK_ID
|
||
→ MARKET_DATA_PARTITION_ADVISORY_LOCK_ID
|
||
→ ACCESS EXCLUSIVE trades
|
||
→ ACCESS EXCLUSIVE quotes
|
||
→ ACCESS EXCLUSIVE candle_revisions
|
||
```
|
||
|
||
Так migration, Partition Manager и Retention сериализуют partition DDL
|
||
до захвата table locks. Ни один участник не может одновременно держать
|
||
lock default partition и ждать parent table, пока migration держит
|
||
parent и ждёт default. Это исключает deadlock `parent ↔ default`.
|
||
|
||
После двух advisory locks для parent tables и их partitions рекурсивно
|
||
берётся `ACCESS EXCLUSIVE` lock в одном порядке:
|
||
|
||
```text
|
||
trades → quotes → candle_revisions
|
||
```
|
||
|
||
Блокировка запрещает конкурентные writes и partition DDL до завершения
|
||
migration. Так новая строка не может появиться между построением
|
||
глобального порядка, установкой defaults и включением ограничений.
|
||
|
||
Backfill строит transaction-local mapping через один `UNION ALL` всех
|
||
трёх типов и назначает `row_number()` по policy раздела 7.2. Обновление
|
||
строк выполняется по полной durable primary identity. Использование
|
||
`ctid` и зависимость от физического порядка строк или порядка вычисления
|
||
`nextval()` запрещены.
|
||
|
||
После backfill sequence переводится за максимальное назначенное
|
||
значение. Для пустого dataset следующий вызов возвращает `1`. Только
|
||
после этого устанавливаются `DEFAULT`, `NOT NULL`, positive checks,
|
||
immutable triggers и partitioned keyset indexes:
|
||
|
||
```text
|
||
Trades: (venue, symbol, executed_at, replay_sequence)
|
||
Quotes: (venue, symbol, received_at, replay_sequence)
|
||
Candles: (venue, symbol, interval, open_time, replay_sequence)
|
||
Candles replay preparation:
|
||
(venue, symbol, interval, observed_at, replay_sequence)
|
||
```
|
||
|
||
Существующий Trade index с `trade_id` в migration 9 не удаляется.
|
||
Ошибка любого statement откатывает schema, backfill и запись версии 9;
|
||
повторный запуск начинается с согласованного состояния.
|
||
|
||
> **Внимание: migration 9 является блокирующей.** Её длительность
|
||
> пропорциональна числу сохранённых строк, стоимости backfill и созданию
|
||
> indexes. Перед применением к большой production database обязательны
|
||
> замер времени и дополнительного дискового места на её актуальной
|
||
> копии, проверенный backup, rollback plan и согласованное maintenance
|
||
> window. Online staged migration с chunked backfill, concurrent indexes
|
||
> и поэтапным включением ограничений в 060.29.2 не реализуется. Если она
|
||
> понадобится, это будет отдельное архитектурное и deployment-решение.
|
||
|
||
### 7.7. Совместимость writers
|
||
|
||
Single Trade, атомарная запись Trade с checkpoint, Quote и Candle
|
||
используют database default. Duplicate либо provenance update сохраняет
|
||
первоначальный `replay_sequence` и не включает эту колонку в `UPDATE`.
|
||
|
||
Trade batch сначала полностью валидируется. Затем writer назначает по
|
||
одному sequence каждому элементу строго в исходном Canonical tuple.
|
||
Только связанные пары `(prepared_trade, replay_sequence)` сортируются по
|
||
существующему identity lock order и записываются. Поэтому безопасный
|
||
порядок захвата row locks не меняет порядок событий внутри исходного
|
||
batch, включая `INT32_MAX → INT32_MIN` и `-1 → 0`.
|
||
|
||
Sequence не является transaction counter: duplicate, conflict или
|
||
rollback могут оставить пропуск. Между конкурентными transactions он
|
||
показывает порядок выдачи ordinal, но не обещает commit chronology.
|
||
|
||
### 7.8. PostgreSQL Trade History reader
|
||
|
||
060.29.2 добавляет отдельный synchronous provider-based adapter
|
||
`PostgresTradeHistoryRepository`. Он реализует только
|
||
`TradeHistoryReaderProtocol`, не расширяет write facade, не читает
|
||
checkpoint и не владеет lifecycle PostgreSQL pool.
|
||
|
||
Query использует один parameterized `SELECT`, полуоткрытый фильтр
|
||
`[start_time, end_time)`, forward keyset predicate
|
||
`(executed_at, replay_sequence) > (...)`, тот же ascending `ORDER BY` и
|
||
`LIMIT query.limit + 1`. `OFFSET`, `SELECT *`, `FOR UPDATE` и любые
|
||
writes запрещены. `next_cursor` указывает на последний возвращённый
|
||
record только тогда, когда получена лишняя строка.
|
||
|
||
Reader явно проецирует все Canonical и provenance columns и строго
|
||
проверяет signed Trade ID, Decimal values, aggressor side, UTC times,
|
||
Canonical schema version, observation sources, query scope и positive
|
||
sequence. Повреждённая строка не пропускается.
|
||
|
||
Обычная страница является текущим committed view одного statement.
|
||
Repository не выполняет `commit`, `rollback` и не меняет isolation
|
||
level: transaction принадлежит connection provider. Это позволяет
|
||
обычным Historical callers явно владеть границей чтения. 060.29.3
|
||
повторно использует тот же строгий row mapper, но строит Replay через
|
||
отдельный snapshot query внутри короткой `READ ONLY REPEATABLE READ`
|
||
transaction.
|
||
|
||
Неверный тип query отклоняется до получения connection как
|
||
`MarketDataAccessValidationError`. Повреждённая строка или page
|
||
invariant дают `MarketDataAccessIntegrityError`; backend/provider error
|
||
даёт `MarketDataAccessOperationError` с исходной причиной. Существующие
|
||
Access errors не переоборачиваются, а `KeyboardInterrupt` и
|
||
`SystemExit` не перехватываются.
|
||
|
||
### 7.9. PostgreSQL Quote/Candle History readers
|
||
|
||
060.29.3 добавляет независимые `PostgresQuoteHistoryRepository` и
|
||
`PostgresCandleRevisionHistoryRepository`. Они используют тот же
|
||
provider-based lifecycle и ту же строгую политику ошибок, что и Trade
|
||
History reader, но сохраняют собственные публичные оси истории:
|
||
|
||
```text
|
||
Quote: (received_at, replay_sequence)
|
||
Candle revision: (open_time, replay_sequence)
|
||
```
|
||
|
||
Quote query использует полуоткрытый диапазон `received_at` и явно
|
||
проецирует Canonical payload, provenance, global sequence и schema
|
||
version. Candle query дополнительно требует один case-sensitive
|
||
`interval` и использует полуоткрытый диапазон `open_time`. Оба reader
|
||
используют forward keyset pagination и `LIMIT query.limit + 1`.
|
||
|
||
Строгая материализация PostgreSQL-строк вынесена в общий внутренний
|
||
модуль. Trade, Quote, Candle History и Replay Plan Builder поэтому
|
||
одинаково проверяют Canonical values, UTC, provenance, schema version и
|
||
positive global sequence. Извлечение Trade-маппера не меняет публичное
|
||
поведение принятого 060.29.2.
|
||
|
||
`MarketDataHistoricalAccess` является DB-neutral фасадом трёх reader
|
||
protocols. Его создание не выполняет I/O; фасад не владеет pool и не
|
||
переоборачивает ошибки конкретного reader.
|
||
|
||
---
|
||
|
||
## 8. Snapshot и concurrent writes
|
||
|
||
Historical pages являются текущим committed view. Между страницами
|
||
Live/Recovery могут добавить строки, а Retention может удалить данные.
|
||
Поэтому обычная пагинация не обещает межстраничный snapshot.
|
||
|
||
Replay не читает медленно меняющиеся страницы во время playback.
|
||
Сначала в одной короткой транзакции:
|
||
|
||
```text
|
||
READ ONLY
|
||
REPEATABLE READ
|
||
```
|
||
|
||
материализуется bounded `ReplayPlan`. После этого cursor, transaction и
|
||
connection закрываются, а Replay работает только с immutable tuple в
|
||
памяти.
|
||
|
||
Долговременная PostgreSQL transaction на период виртуального времени
|
||
запрещена. Превышение `max_records` является явной ошибкой, а не
|
||
частичным либо молчаливо обрезанным Replay.
|
||
|
||
### 8.1. Отдельный PostgreSQL Replay snapshot query
|
||
|
||
Публичный Candle History query нельзя использовать как источник Replay:
|
||
он выбирает свечи по `open_time`, тогда как конкретная ревизия должна
|
||
воспроизводиться по `observed_at`.
|
||
|
||
`PostgresReplayPlanBuilder` выполняет один отдельный parameterized
|
||
`UNION ALL` query только для запрошенных типов:
|
||
|
||
```text
|
||
Trade → replay_at = executed_at
|
||
Quote → replay_at = received_at
|
||
Candle revision → replay_at = observed_at
|
||
```
|
||
|
||
Static SQL branches выбираются только по `ReplayDataType`; venue,
|
||
symbols, временной диапазон и intervals всегда передаются параметрами.
|
||
Итоговый query сортирует все типы по
|
||
`(replay_at, replay_sequence)` и запрашивает `max_records + 1` строк.
|
||
Лишняя строка вызывает `ReplayPlanLimitExceededError`; частичный plan не
|
||
создаётся.
|
||
|
||
Builder владеет только короткими cursor и transaction внутри одного
|
||
вызова `create_plan()`. Первой SQL-командой transaction устанавливается:
|
||
|
||
```sql
|
||
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY;
|
||
```
|
||
|
||
Pool и connection provider принадлежат приложению. После materialization
|
||
transaction и connection закрыты, а возвращённый `ReplayPlan` не зависит
|
||
от PostgreSQL. Повреждённые строки дают
|
||
`MarketDataAccessIntegrityError`, backend/provider failures —
|
||
`MarketDataAccessOperationError` с исходной причиной, превышение лимита
|
||
остаётся отдельной Replay-ошибкой. `BaseException` не перехватывается.
|
||
|
||
---
|
||
|
||
## 9. Replay contracts
|
||
|
||
### 9.1. ReplayEvent и ReplayPlan
|
||
|
||
`ReplayEvent` содержит:
|
||
|
||
- `venue`;
|
||
- UTC `replay_at`;
|
||
- immutable `replay_sequence`;
|
||
- точный Canonical payload;
|
||
- `is_final` только для Candle revision.
|
||
|
||
`ReplayPlan` хранит исходный immutable request и полностью
|
||
материализованный tuple событий. Пустой plan является допустимым no-op.
|
||
Непустой plan строго возрастает по `(replay_at, replay_sequence)` и не
|
||
содержит повторяющихся global sequence.
|
||
|
||
### 9.2. Clock
|
||
|
||
Контракты разделяются:
|
||
|
||
- `MarketDataClockProtocol` — только чтение текущего времени;
|
||
- `ReplayClockProtocol` — управляемое `advance_to()`.
|
||
|
||
Одна Replay Session владеет одними Virtual Clock. Стартовое время равно
|
||
`request.start_time`, включая пустой plan. Время не может уменьшаться.
|
||
|
||
060.29.4 добавляет concrete `DeterministicReplayClock` с единственным
|
||
изменяемым состоянием — текущим UTC-временем. Clock не читает системное
|
||
время, не выполняет sleep, I/O и не создаёт задач.
|
||
|
||
Начальное значение и каждый `advance_to()` принимают timezone-aware
|
||
`datetime`, совместимый с уже принятыми Replay time contracts, и
|
||
канонизируют его в обычный UTC `datetime`. Naive datetime запрещён.
|
||
|
||
Переход вперёд изменяет `now`. Переход на тот же абсолютный момент
|
||
разрешён как идемпотентный no-op: несколько событий могут иметь один
|
||
`replay_at` и различаться только `replay_sequence`. Только переход назад
|
||
вызывает `ReplayClockError`; после любой ошибки состояние не меняется.
|
||
|
||
Clock не знает о `ReplayPlan`, sequence и верхней границе request. У него
|
||
нет `reset()`, `advance_by()`, `start()` и `stop()`. Один экземпляр
|
||
принадлежит одной Session и не разделяется между Session или OS threads.
|
||
Внутренние lock и owner registry не добавляются; управление
|
||
последовательно и синхронно выполняет будущая Replay Session.
|
||
|
||
### 9.3. Consumer и Session
|
||
|
||
Одна Session имеет одного async consumer. Доставка выполняется строго
|
||
последовательно:
|
||
|
||
```text
|
||
clock.advance_to(event.replay_at)
|
||
↓
|
||
await consumer.consume(event)
|
||
```
|
||
|
||
Fan-out, fire-and-forget и error isolation в Build не добавляются.
|
||
|
||
060.29.5 добавляет concrete `ReplaySession`. Отдельный класс
|
||
`ReplayEngine` не создаётся: engine является последовательным циклом
|
||
внутри `ReplaySession.run()`.
|
||
|
||
Конструктор получает уже материализованный точный `ReplayPlan`, один
|
||
`ReplayClockProtocol` и один `ReplayConsumerProtocol`. Clock передаётся
|
||
Session в исключительное владение и до запуска обязан находиться в
|
||
каноническом UTC-времени `plan.request.time_range.start_time`.
|
||
Несовпадение считается ошибкой dependency и даёт
|
||
`MarketDataReplayValidationError`; Session не выполняет скрытый reset.
|
||
|
||
Plan сохраняется по identity и может использоваться для создания других
|
||
независимых Session. Public `clock` предоставляет consumer-facing
|
||
границу `MarketDataClockProtocol`, а lifecycle consumer остаётся у
|
||
caller/composition.
|
||
|
||
Session является one-shot и имеет состояния:
|
||
|
||
```text
|
||
CREATED → RUNNING → COMPLETED
|
||
↘ FAILED
|
||
↘ CANCELLED
|
||
```
|
||
|
||
Повторный, конкурентный или рекурсивный `run()` запрещён. Session
|
||
переводится в `RUNNING` до первого `await`, поэтому внутри одного event
|
||
loop второй caller немедленно получает `ReplaySessionStateError` без
|
||
lock и без влияния на первый запуск. Для повторения создаются новые
|
||
Session и Clock.
|
||
|
||
Empty plan штатно проходит `CREATED → RUNNING → COMPLETED`, не вызывает
|
||
Clock или consumer и оставляет время в `request.start_time`. `run()` не
|
||
возвращает Result DTO или progress: успешный результат равен `None`.
|
||
|
||
### 9.4. Consumer Factory и Composition
|
||
|
||
060.29.6 добавляет только две новые публичные границы:
|
||
|
||
```text
|
||
ReplayConsumerFactoryProtocol
|
||
ReplaySessionFactory.prepare_session(request)
|
||
```
|
||
|
||
`ReplayConsumerFactoryProtocol` синхронно создаёт отдельный consumer для
|
||
одной Session и получает:
|
||
|
||
- точный immutable `ReplayPlan`;
|
||
- Clock этой же Session через read-only `MarketDataClockProtocol`.
|
||
|
||
Factory обязана возвращать новый stateful consumer при каждом вызове,
|
||
не выполнять I/O, не создавать task и не запускать lifecycle consumer.
|
||
Default/no-op consumer не предоставляется: конкретный consumer выбирает
|
||
вызывающая подсистема с реальным use case.
|
||
|
||
`ReplaySessionFactory` получает только `ReplayPlanBuilderProtocol` и
|
||
`ReplayConsumerFactoryProtocol`. Конструктор сохраняет зависимости и не
|
||
выполняет I/O. Явный `prepare_session()` выполняет строго одну
|
||
последовательность:
|
||
|
||
```text
|
||
exact ReplayPlanRequest validation
|
||
↓
|
||
plan_builder.create_plan(request)
|
||
↓
|
||
exact ReplayPlan + plan.request identity validation
|
||
↓
|
||
fresh DeterministicReplayClock(request.start_time)
|
||
↓
|
||
consumer_factory.create_consumer(plan, тот же Clock)
|
||
↓
|
||
fresh ReplaySession(plan, тот же Clock, consumer)
|
||
```
|
||
|
||
Метод `prepare_session()` намеренно является синхронным и потенциально
|
||
блокирующим: concrete PostgreSQL builder выполняет чтение snapshot в
|
||
потоке вызывающего кода. Composition не скрывает SQL через
|
||
`asyncio.to_thread()`, потому что cancellation asyncio task не остановит
|
||
уже выполняющийся SQL в worker thread. Выбор отдельного worker остаётся
|
||
у внешнего caller.
|
||
|
||
Возвращается обычная `ReplaySession` в состоянии `CREATED`.
|
||
Composition не вводит Result DTO, отдельный `ReplayEngine`, service,
|
||
registry, cache, lock или single-flight. Caller отдельно и явно
|
||
выполняет `await session.run()` и владеет этой coroutine/task.
|
||
|
||
Каждый вызов `prepare_session()` создаёт новый Clock, consumer и
|
||
Session. Один immutable plan может быть повторно возвращён injected
|
||
builder, но изменяемые части графа между Session не разделяются. Один и
|
||
тот же concrete Clock по identity передаётся consumer factory через
|
||
read-only Protocol и Session через управляющий Protocol.
|
||
|
||
Composition не сериализует параллельные вызовы. Thread-safety общего
|
||
injected builder и consumer factory является их внешним контрактом;
|
||
два уже созданных графа не разделяют изменяемые Session/Clock/consumer.
|
||
|
||
Ошибки builder и consumer factory распространяются без обёртки, retry и
|
||
частичного результата. Неверный тип request, неверный exact plan или
|
||
подмена `plan.request` дают `MarketDataReplayValidationError` до создания
|
||
следующей зависимости. Class objects и async implementations вместо
|
||
синхронных dependency instances отклоняются при создании composition.
|
||
|
||
060.29.6 не меняет Bootstrap, settings, SQL, PostgreSQL pool, Runtime,
|
||
Telegram или startup приложения. Подготовка выполняется только после
|
||
явного вызова `prepare_session()`, а playback — только после явного
|
||
вызова `run()`.
|
||
|
||
---
|
||
|
||
## 10. Lifecycle, cancellation и ошибки
|
||
|
||
1. Создание contracts, access facade и `ReplaySessionFactory` не
|
||
выполняет I/O; явный `prepare_session()` является blocking-границей.
|
||
2. Historical readers используют существующий managed pool, но не
|
||
открывают и не закрывают его.
|
||
3. ReplayPlan полностью отделяется от PostgreSQL до playback.
|
||
4. Caller владеет coroutine task `ReplaySession.run()`.
|
||
5. Session не создаёт скрытую root/background task.
|
||
6. Consumer error распространяется вызывающему коду и останавливает
|
||
доставку следующих событий.
|
||
7. `CancelledError` не проглатывается.
|
||
8. Clock error является terminal для Session.
|
||
9. Частично доставленный consumer prefix нельзя транзакционно отменить;
|
||
повтор выполняется новой Session по явному решению caller.
|
||
10. Clock переводится до consumer; после consumer failure или
|
||
cancellation остаётся во времени начатого события.
|
||
11. Исходные error и cancellation распространяются без обёртки; Session
|
||
только фиксирует `FAILED` или `CANCELLED`.
|
||
12. Session не создаёт task, lock, connection, retry или cleanup и не
|
||
управляет lifecycle consumer.
|
||
|
||
---
|
||
|
||
## 11. Scope 060.29.0–060.29.1
|
||
|
||
### 060.29.0
|
||
|
||
- этот architecture document;
|
||
- read/write boundary;
|
||
- event-time и ordering policy;
|
||
- global replay sequence policy;
|
||
- snapshot, resource и failure policy;
|
||
- разбиение Build и acceptance criteria.
|
||
|
||
### 060.29.1
|
||
|
||
- immutable Historical records;
|
||
- time range, queries, typed cursors и pages;
|
||
- runtime-checkable Historical reader protocols;
|
||
- отдельная error hierarchy;
|
||
- Replay request, event, materialized plan и session state;
|
||
- clock, consumer, plan builder и session Protocol-контракты;
|
||
- DB-neutral unit-тесты.
|
||
|
||
060.29.0–060.29.1 не выполняют SQL, network I/O, filesystem I/O и не
|
||
создают asyncio tasks.
|
||
|
||
---
|
||
|
||
## 12. Следующие подэтапы
|
||
|
||
### 060.29.2
|
||
|
||
- migration 9 с shared global `replay_sequence` без registry table;
|
||
- блокирующий атомарный backfill существующих строк;
|
||
- immutable sequence и partitioned keyset indexes;
|
||
- совместимость single/batch writers без изменения checkpoint semantics;
|
||
- отдельный PostgreSQL Trade Historical reader;
|
||
- unit- и opt-in PostgreSQL verification.
|
||
|
||
Детальный утверждённый контракт закреплён в разделах 7.5–7.8.
|
||
|
||
### 060.29.3
|
||
|
||
- Quote History по `(received_at, replay_sequence)`;
|
||
- Candle History по `(open_time, replay_sequence)` и одному interval;
|
||
- общий DB-neutral Historical Access facade;
|
||
- единые строгие PostgreSQL row mappers;
|
||
- отдельный snapshot query с Candle replay axis `observed_at`;
|
||
- короткая `READ ONLY REPEATABLE READ` transaction;
|
||
- bounded immutable ReplayPlan и fail-fast `max_records + 1`.
|
||
|
||
### 060.29.4
|
||
|
||
- concrete `DeterministicReplayClock`;
|
||
- UTC normalization без чтения wall clock;
|
||
- разрешённый равный переход и запрет движения назад;
|
||
- single-owner policy без lock, reset и lifecycle;
|
||
- public export и DB-neutral unit-тесты.
|
||
|
||
### 060.29.5
|
||
|
||
- concrete `ReplaySession` без отдельного `ReplayEngine`;
|
||
- injected Clock/consumer и fail-fast проверка начального времени;
|
||
- one-shot lifecycle и последовательный playback loop;
|
||
- error/cancellation propagation без retry и rollback;
|
||
- DB-neutral async unit-тесты и public export.
|
||
|
||
### 060.29.6
|
||
|
||
- `ReplayConsumerFactoryProtocol` для обязательного concrete consumer;
|
||
- единый `ReplaySessionFactory.prepare_session()`;
|
||
- явная blocking composition
|
||
`request → plan → fresh Clock → fresh consumer → fresh Session`;
|
||
- один Clock по identity для Session и consumer через разные
|
||
Protocol-границы;
|
||
- отсутствие default consumer, скрытого `to_thread`, cache и
|
||
автоматического startup;
|
||
- DB-neutral unit-тесты composition, identity, ошибок и конкурентности.
|
||
|
||
### 060.29.7
|
||
|
||
- verification-only integration без предварительного production diff;
|
||
- существующий opt-in harness только для локальной `dzentra_test_*` БД;
|
||
- локальный путь
|
||
`Loopback Trade Runtime → Storage → Historical Access → Replay`;
|
||
- mixed-type PostgreSQL snapshot и caller-owned playback;
|
||
- реальная граница `READ ONLY REPEATABLE READ` при concurrent commit;
|
||
- возврат PostgreSQL resources до первого consumer call;
|
||
- limit, integrity, provider/backend и consumer failure paths;
|
||
- независимые графы двух concurrent blocking preparations;
|
||
- десятикратный повтор нового PostgreSQL target.
|
||
|
||
### 060.29.8
|
||
|
||
- обязательный Pyright gate, compileall и проверка зависимостей;
|
||
- расширенный Access/Replay/Storage/migration unit-набор;
|
||
- десятикратный PostgreSQL Replay target;
|
||
- полный PostgreSQL Storage и общий integration-наборы;
|
||
- отдельная opt-out проверка без подключения к базе;
|
||
- fixed stress и полная offline-регрессия;
|
||
- итоговый read-only review архитектуры, SQL, ordering, lifecycle,
|
||
обработки ошибок, освобождения ресурсов и документации.
|
||
|
||
Подэтап остаётся verification-only. Dzengi live и acquisition soak не
|
||
являются обязательными: Build не добавляет live endpoint wiring или
|
||
долгоживущие Replay-задачи, а детерминированный путь
|
||
`Loopback Runtime → Storage → Historical Access → Replay` проверен на
|
||
локальной PostgreSQL.
|
||
|
||
---
|
||
|
||
## 13. Test strategy
|
||
|
||
### Обязательная статическая типизация
|
||
|
||
- Pyright `1.1.411` запускается в режиме `standard`, совпадающем с
|
||
используемым Pylance;
|
||
- каноническая команда — `scripts/check_python_types.sh`;
|
||
- допустимый результат — только `0 errors, 0 warnings`;
|
||
- проверяются Market Data, Storage, Bootstrap, Runtime Events и все их
|
||
unit/integration/stress/live/support-тесты;
|
||
- gate входит в обычную offline pytest-регрессию через отдельный
|
||
static-тест и поэтому не может быть пропущен при приёмке Build;
|
||
- `type: ignore`, отключение диагностик или ослабление режима не
|
||
используются для исправления новых ошибок;
|
||
- область проверки в следующих Build может только расширяться.
|
||
|
||
### Contracts
|
||
|
||
- frozen/slots;
|
||
- строгие типы, включая запрет `bool` вместо `int`;
|
||
- UTC normalization и запрет naive datetime;
|
||
- полуоткрытые временные границы;
|
||
- canonical symbol, venue и case-sensitive Candle interval;
|
||
- provenance и Canonical schema version;
|
||
- cursor scope/version;
|
||
- typed pages и строгий order key;
|
||
- runtime-checkable Protocols.
|
||
|
||
### Ordering
|
||
|
||
- одинаковое event time и разные sequence;
|
||
- `INT32_MAX → INT32_MIN`;
|
||
- `-1 → 0`;
|
||
- provenance update не меняет sequence;
|
||
- размер страницы не меняет итоговый порядок.
|
||
|
||
### Migration и writers
|
||
|
||
- детерминированный mixed-type backfill независимо от physical order;
|
||
- shared sequence во всех parent/default/existing/future partitions;
|
||
- direct update sequence запрещён trigger;
|
||
- duplicate и provenance update сохраняют первоначальный ordinal;
|
||
- batch input order сохраняется независимо от identity lock order;
|
||
- concurrent migration callers, writer wait и partition-manager race;
|
||
- partial failure откатывает migration 9, clean retry проходит;
|
||
- gaps после duplicate и transaction rollback считаются корректными.
|
||
|
||
### PostgreSQL Trade History
|
||
|
||
- start inclusive и end exclusive;
|
||
- empty, exact-limit и extra-row pages;
|
||
- смена page limit не создаёт skips или duplicates;
|
||
- equal timestamp и signed rollover упорядочиваются по sequence;
|
||
- default и monthly partitions дают один стабильный результат;
|
||
- повреждённая строка и неизвестная schema version не пропускаются;
|
||
- DB failure сохраняет cause и не оставляет connection/cursor;
|
||
- чтение не меняет Trade, provenance, checkpoint или sequence.
|
||
|
||
### PostgreSQL Quote/Candle History и Replay snapshot
|
||
|
||
- Quote start inclusive и end exclusive по `received_at`;
|
||
- Candle History start inclusive и end exclusive по `open_time`;
|
||
- case-sensitive Candle interval и устойчивые keyset pages;
|
||
- строгая проверка Quote prices и Candle OHLCV/revision metadata;
|
||
- Candle с `open_time` вне Replay range и `observed_at` внутри включается;
|
||
- Candle с `open_time` внутри и `observed_at` вне исключается;
|
||
- static branches включают только запрошенные data types;
|
||
- один global order для одинакового времени разных типов;
|
||
- `max_records + 1` даёт ошибку без частичного plan;
|
||
- transaction mode, cleanup, backend cause и `BaseException` проверены.
|
||
|
||
### Replay lifecycle
|
||
|
||
- Protocol, slots, initial `CREATED` и read-only properties;
|
||
- fail-fast dependency validation и точное начальное время Clock;
|
||
- empty plan без вызовов Clock/consumer;
|
||
- точный порядок tuple, Clock-before-consumer и event identity;
|
||
- одинаковое `replay_at` с разными global sequence;
|
||
- последовательная backpressure без параллельной доставки;
|
||
- consumer/Clock failure и исходная error identity;
|
||
- cancellation без доставки suffix и без проглатывания;
|
||
- concurrent/repeated/reentrant `run()` rejection;
|
||
- независимые Session и Clock при общем immutable plan;
|
||
- отсутствие hidden tasks, retries, cleanup и PostgreSQL connections.
|
||
|
||
### Deterministic Replay Clock
|
||
|
||
- начальное UTC-время и normalization разных UTC offsets;
|
||
- запрет non-datetime и naive datetime;
|
||
- прямой переход с сохранением микросекунд;
|
||
- повторный переход на тот же абсолютный момент;
|
||
- несколько событий с одинаковым `replay_at`;
|
||
- обратный переход через `ReplayClockError` без изменения `now`;
|
||
- независимость Clock разных Session;
|
||
- read-only `now`, `__slots__` и отсутствие reset/lifecycle/tasks/I/O.
|
||
|
||
### Consumer и Composition
|
||
|
||
- runtime-checkable `ReplayConsumerFactoryProtocol` и public exports;
|
||
- fail-fast constructor validation, включая class objects и async
|
||
implementations;
|
||
- exact request до builder и identity request в созданном plan;
|
||
- порядок builder → Clock → consumer → Session;
|
||
- один Clock по identity для consumer factory и Session;
|
||
- новый Clock, consumer и Session при каждом вызове;
|
||
- пустой plan без автоматического запуска consumer;
|
||
- blocking preparation в caller thread без скрытого `to_thread`;
|
||
- ошибки builder/factory по identity, без retry и wrapping;
|
||
- отсутствие hidden tasks, lifecycle calls, cache и automatic startup;
|
||
- независимые графы для двух concurrent callers.
|
||
|
||
### PostgreSQL Replay and Failure Verification
|
||
|
||
- opt-in отключён без точного флага и явного локального DSN;
|
||
- destructive reset разрешён только для проверенной `dzentra_test_*` БД;
|
||
- local Loopback Trade проходит Runtime, Storage, History и Replay;
|
||
- Trade, Quote и Candle revisions воспроизводятся в global order;
|
||
- empty snapshot остаётся штатным no-op;
|
||
- test-only barrier фиксирует snapshot до concurrent commit без `sleep`;
|
||
- первая Session не видит post-snapshot commit, следующая видит его;
|
||
- pool с `max_size=1` освобождается до playback и может быть переоткрыт;
|
||
- limit/integrity/provider/backend errors не создают consumer или Session;
|
||
- consumer failure/cancellation происходят после освобождения DB;
|
||
- Replay не меняет Market Data или persistent checkpoint;
|
||
- два blocking caller реально участвуют и получают отдельные графы;
|
||
- threads, tasks, cursors, transactions и connections закрываются;
|
||
- новый target проходит однократно и десять раз с
|
||
`ResourceWarning` как error.
|
||
|
||
---
|
||
|
||
## 14. Вне scope
|
||
|
||
- raw exchange documents и точный transport arrival log;
|
||
- загрузка рыночной истории до первого запуска persistent storage;
|
||
- автоматический historical backfill;
|
||
- пользовательские ордера, fills, позиции и private account operations;
|
||
- аналитические вычисления и Feature Engineering;
|
||
- стратегии, Risk, Portfolio и Order Management;
|
||
- полноценный Backtesting;
|
||
- wall-clock pacing, speed, pause, seek и resume;
|
||
- автоматический Retention Scheduler;
|
||
- persistent Quote/Candle production consumers;
|
||
- distributed lease, leader election и HA failover;
|
||
- Telegram/HTTP public API;
|
||
- автоматический запуск Replay вместе с приложением;
|
||
- online staged migration global sequence без maintenance window.
|
||
|
||
---
|
||
|
||
## 15. Граница доступного периода
|
||
|
||
После 060.29 база предоставляет запросы и Replay только для данных,
|
||
которые действительно находятся в PostgreSQL.
|
||
|
||
Начальная граница production Trade history:
|
||
|
||
```text
|
||
первый успешный запуск
|
||
MARKET_DATA_STORAGE_ENABLED=true
|
||
и включённого Production Trade Stream
|
||
```
|
||
|
||
060.28 заполняет bounded downtime gap после уже известного persistent
|
||
checkpoint, если нужные Trades ещё доступны в REST API Dzengi. Он не
|
||
загружает всю историю до первого checkpoint.
|
||
|
||
Конечная граница Trade history постоянно продвигается работающим
|
||
Runtime. Retention по умолчанию выключен, поэтому программного срока
|
||
удаления истории нет. Фактический период всё равно ограничен:
|
||
|
||
- временем первого production-запуска storage;
|
||
- периодами, когда Runtime или база были недоступны;
|
||
- глубиной истории, доступной Recovery API;
|
||
- ручной очисткой, будущей Retention policy и объёмом диска.
|
||
|
||
Для Quotes и Candle revisions production consumers пока не подключены.
|
||
Historical Access возвращает только строки, фактически записанные через
|
||
их repositories; полнота production Quote/Candle history не заявляется.
|
||
|
||
Каждая Historical page отражает committed-состояние на время
|
||
собственного запроса. Последовательная pagination даёт доступ ко всем
|
||
фактически сохранённым строкам диапазона только при неизменном dataset
|
||
между страницами. Live/Recovery-запись и Retention могут изменить этот
|
||
набор; единый межстраничный snapshot и полная биржевая история без
|
||
отдельного completeness metadata не гарантируются. Initial backfill
|
||
потребует нового согласованного Build.
|
||
|
||
---
|
||
|
||
## 16. Acceptance criteria
|
||
|
||
060.29.0–060.29.1 могут быть приняты, если:
|
||
|
||
1. Read contracts не меняют write-only Storage API.
|
||
2. Нет SQL, `psycopg`, repository, Runtime или Bootstrap implementation.
|
||
3. Все public value objects immutable и slotted.
|
||
4. Query windows aware, UTC и half-open.
|
||
5. Cursor строго связан с query scope.
|
||
6. `trade_id` не входит в Historical order key.
|
||
7. Records и Replay events сохраняют identity Canonical payload.
|
||
8. ReplayPlan строго упорядочен и bounded.
|
||
9. Clock/Consumer/Session представлены только Protocol-контрактами.
|
||
10. Комментарии и docstrings новых файлов написаны по-русски.
|
||
11. Целевые unit-тесты проходят.
|
||
12. Отдельный read-only review не выявляет findings.
|
||
|
||
060.29.2 может быть принят, если:
|
||
|
||
1. Migration 9 атомарно и детерминированно заполняет global sequence.
|
||
2. Shared sequence является единственным allocator; registry table нет.
|
||
3. Все partitions получают positive `NOT NULL` sequence и keyset indexes.
|
||
4. Provenance update и duplicate не меняют уже записанный ordinal.
|
||
5. Trade batch сохраняет input order до identity lock sorting.
|
||
6. Migration concurrency, blocking и rollback проверены на PostgreSQL.
|
||
7. Trade History использует half-open range и forward keyset pagination.
|
||
8. Reader строго проверяет строки и сохраняет error cause.
|
||
9. Reader не владеет pool lifecycle и не меняет Runtime/Bootstrap.
|
||
10. Unit, PostgreSQL target и полная регрессия проходят.
|
||
11. Отдельный acceptance read-only review не выявляет findings.
|
||
|
||
060.29.3 может быть принят, если:
|
||
|
||
1. Quote History использует `received_at` и forward keyset pagination.
|
||
2. Candle History использует `open_time` и case-sensitive interval.
|
||
3. Все три reader используют единые строгие PostgreSQL row mappers.
|
||
4. Historical Access facade не выполняет I/O и не владеет pool.
|
||
5. Replay Builder использует отдельный snapshot query, а не Candle
|
||
History pagination.
|
||
6. Candle Replay фильтруется по `observed_at`, а не по `open_time`.
|
||
7. Snapshot materializes в одной короткой `READ ONLY REPEATABLE READ`
|
||
transaction и закрывает все ресурсы до возврата plan.
|
||
8. SQL branches статичны, а пользовательские значения параметризованы.
|
||
9. Итоговый порядок строго возрастает по
|
||
`(replay_at, replay_sequence)` для всех типов.
|
||
10. `max_records + 1` даёт `ReplayPlanLimitExceededError` без частичного
|
||
результата.
|
||
11. Unit, PostgreSQL target и полная регрессия проходят.
|
||
12. Отдельный acceptance read-only review не выявляет findings.
|
||
|
||
060.29.4 может быть принят, если:
|
||
|
||
1. Clock начинается с переданного aware времени и хранит его в UTC.
|
||
2. Clock не читает wall clock и не выполняет I/O или sleep.
|
||
3. `advance_to()` разрешает прямой и равный переход.
|
||
4. Обратный переход даёт `ReplayClockError` и не меняет `now`.
|
||
5. Ошибка типа или naive datetime не меняет состояние.
|
||
6. Один Clock не содержит shared Session state или внутренних lock.
|
||
7. Нет reset, relative advance, lifecycle и скрытых asyncio tasks.
|
||
8. Clock соответствует `MarketDataClockProtocol` и
|
||
`ReplayClockProtocol`.
|
||
9. В подэтапе нет Replay Session, consumer delivery или composition.
|
||
10. Unit и полная регрессия проходят.
|
||
11. Отдельный acceptance read-only review не выявляет findings.
|
||
|
||
060.29.5 может быть принят, если:
|
||
|
||
1. Concrete `ReplaySession` соответствует `ReplaySessionProtocol` и не
|
||
вводит отдельный `ReplayEngine`.
|
||
2. Exact `ReplayPlan`, `ReplayClockProtocol` и `ReplayConsumerProtocol`
|
||
проверяются до первого запуска.
|
||
3. Clock начинается точно в каноническом UTC
|
||
`plan.request.time_range.start_time`; reset не выполняется.
|
||
4. `run()` переводит `CREATED → RUNNING` до первого `await` и допускает
|
||
только один запуск.
|
||
5. Для каждого события Clock продвигается до одного последовательного
|
||
`await consumer.consume(event)`.
|
||
6. Empty plan завершается без вызовов dependency и не меняет Clock.
|
||
7. Ошибка переводит Session в `FAILED`, cancellation — в `CANCELLED`, а
|
||
исходный объект исключения распространяется без обёртки.
|
||
8. Повторный, конкурентный и рекурсивный запуск дают
|
||
`ReplaySessionStateError`, не повреждая активный или terminal state.
|
||
9. Session не создаёт tasks, locks, I/O, retries, rollback или cleanup.
|
||
10. В подэтапе нет Plan Builder composition, выбора consumer, Bootstrap
|
||
или автоматического startup.
|
||
11. Unit и полная регрессия проходят.
|
||
12. Отдельный acceptance read-only review не выявляет findings.
|
||
|
||
060.29.6 может быть принят, если:
|
||
|
||
1. `ReplayConsumerFactoryProtocol` является единственной новой границей
|
||
выбора concrete consumer.
|
||
2. `ReplaySessionFactory` проверяет синхронные dependency instances и не
|
||
выполняет I/O или создание объектов графа в конструкторе.
|
||
3. `prepare_session()` является синхронной blocking-границей и не
|
||
использует скрытый `to_thread` или background task.
|
||
4. Builder получает точный исходный request ровно один раз, а результат
|
||
обязан быть точным `ReplayPlan` с тем же request по identity.
|
||
5. Порядок сборки равен plan → Clock → consumer → Session.
|
||
6. Consumer factory и Session получают один Clock по identity через
|
||
разные Protocol-границы.
|
||
7. Каждый вызов создаёт новые Clock, consumer и Session без cache,
|
||
registry, lock и single-flight.
|
||
8. Подготовленная Session остаётся в `CREATED`; consumer, lifecycle и
|
||
playback не запускаются автоматически.
|
||
9. Default/no-op consumer, отдельный ReplayEngine, Result DTO, Bootstrap
|
||
и settings не добавлены.
|
||
10. Ошибки dependencies и cancellation распространяются без обёртки,
|
||
retry и частично возвращённой Session.
|
||
11. Unit и полная offline-регрессия проходят.
|
||
12. Отдельный acceptance read-only review не выявляет findings.
|
||
|
||
060.29.7 может быть принят, если:
|
||
|
||
1. Подэтап остаётся verification-only, если тесты не выявили реальный
|
||
production defect.
|
||
2. Используется существующий opt-in PostgreSQL harness с проверкой
|
||
локального endpoint и имени `dzentra_test_*`.
|
||
3. Local Trade проходит Production Runtime, PostgreSQL Storage,
|
||
Historical Access, ReplayPlan, Composition и Session.
|
||
4. Mixed Trade/Quote/Candle plan воспроизводится в точном global order,
|
||
а consumer наблюдает уже продвинутое время Clock.
|
||
5. Реальный `REPEATABLE READ` snapshot не видит commit после
|
||
зафиксированной точки, а следующая Session видит его полностью.
|
||
6. PostgreSQL cursor, transaction и connection освобождены до первого
|
||
consumer call; подготовленная Session работает после закрытия pool.
|
||
7. Limit, integrity, provider/backend errors не создают consumer или
|
||
частичную Session, а pool после контролируемой ошибки остаётся либо
|
||
снова становится пригодным к работе.
|
||
8. Consumer failure и cancellation сохраняют принятый lifecycle Session
|
||
без удержания PostgreSQL resources и изменения durable данных.
|
||
9. Два concurrent caller действительно выполняют real preparation и
|
||
получают разные Plan, Clock, consumer и Session.
|
||
10. Нет незавершённых PostgreSQL resources, threads или asyncio tasks.
|
||
11. Обязательный Pyright gate проходит с нулём errors и warnings.
|
||
12. Новый target проходит один раз и десять раз, полный PostgreSQL
|
||
Storage integration, Replay unit и offline regression проходят.
|
||
13. Отдельный acceptance read-only review не выявляет findings.
|
||
|
||
060.29.8 может быть принят, если:
|
||
|
||
1. Pyright завершается с `0 errors, 0 warnings`, static pytest gate,
|
||
compileall и `pip check` проходят.
|
||
2. Расширенный Access/Replay/Storage/migration unit-набор проходит.
|
||
3. Двенадцать PostgreSQL Replay-сценариев стабильно проходят десять
|
||
последовательных запусков с `ResourceWarning` как ошибкой.
|
||
4. Полный PostgreSQL Storage и общий integration-наборы проходят с
|
||
`ResourceWarning` как ошибкой.
|
||
5. Без opt-in flag и DSN весь PostgreSQL Storage-набор пропускается и не
|
||
устанавливает соединение с базой.
|
||
6. Fixed stress и полная offline-регрессия проходят.
|
||
7. Нет необъяснённых skips, незавершённых tasks, threads, cursors,
|
||
transactions или PostgreSQL connections.
|
||
8. Replay не изменяет Canonical Market Data, operational checkpoint или
|
||
production lifecycle.
|
||
9. Итоговые read-only review архитектуры, SQL, тестов и документации не
|
||
выявляют findings P0–P3.
|
||
10. Подэтап не меняет production-код, если матрица не воспроизвела
|
||
реальный дефект.
|
||
|
||
---
|
||
|
||
## 17. ADR
|
||
|
||
### ADR-060.29-001 — Read API не расширяет write-only Storage facade
|
||
|
||
Historical Access получает отдельные DB-neutral contracts и adapters.
|
||
|
||
### ADR-060.29-002 — Replay использует event-time chronology
|
||
|
||
Порядок исходных transport packets не заявляется без raw arrival log.
|
||
|
||
### ADR-060.29-003 — Global immutable replay sequence
|
||
|
||
Один PostgreSQL sequence без registry table применяется ко всем
|
||
Canonical Market Data и не изменяется при provenance update.
|
||
|
||
### ADR-060.29-004 — Replay materializes bounded snapshot
|
||
|
||
PostgreSQL transaction завершается до начала виртуального playback.
|
||
|
||
### ADR-060.29-005 — Canonical payload не заменяется Replay DTO
|
||
|
||
Replay envelope сохраняет точный `Trade`, `Quote` или `Candle`.
|
||
|
||
### ADR-060.29-006 — Session является one-shot и caller-owned
|
||
|
||
Session не создаёт скрытую задачу и не скрывает consumer/cancellation
|
||
errors.
|
||
|
||
### ADR-060.29-007 — Migration 9 использует maintenance window
|
||
|
||
060.29.2 выбирает атомарную блокирующую migration. Online staged rollout
|
||
для большой production database требует отдельного решения.
|
||
|
||
### ADR-060.29-008 — Candle History и Replay используют разные оси
|
||
|
||
Публичная Candle History выбирает ревизии по `open_time`. Replay Plan
|
||
Builder использует отдельный snapshot query и выбирает те же ревизии по
|
||
`observed_at`, то есть в момент, когда они фактически стали известны.
|
||
|
||
### ADR-060.29-009 — Replay engine находится внутри one-shot Session
|
||
|
||
060.29.5 не вводит отдельный `ReplayEngine`. Последовательный playback
|
||
выполняет `ReplaySession.run()`, а создание plan, fresh Clock и consumer
|
||
остаётся явной задачей Composition 060.29.6.
|
||
|
||
### ADR-060.29-010 — Composition разделяет preparation и playback
|
||
|
||
060.29.6 использует один синхронный blocking
|
||
`ReplaySessionFactory.prepare_session()` без скрытого worker и
|
||
автоматического startup. Каждый вызов создаёт новый Clock, обязательный
|
||
consumer и `ReplaySession`; caller отдельно владеет `session.run()`.
|
||
|
||
### ADR-060.29-011 — PostgreSQL acceptance остаётся verification-only
|
||
|
||
060.29.7 не добавляет production consumer или новые runtime-границы.
|
||
Он проверяет уже принятые Storage, Historical, snapshot, Composition и
|
||
Session на локальной PostgreSQL. Production-код может измениться только
|
||
при воспроизводимом нарушении принятого контракта и только вместе с
|
||
минимальным regression-тестом; test hooks в production запрещены.
|
||
|
||
---
|
||
|
||
## 18. Test evidence
|
||
|
||
Итоговая приёмочная проверка 060.29.0–060.29.1:
|
||
|
||
```text
|
||
Historical Access + Replay unit target: 111 passed
|
||
Full offline regression: 2416 passed, 69 deselected
|
||
Historical Access exact-type re-review: clean
|
||
Replay exact-type re-review: clean
|
||
Combined acceptance read-only review: clean
|
||
```
|
||
|
||
Подэтапы 060.29.0–060.29.1 приняты. На этом этапе Build 060.29 оставался
|
||
`In Progress`.
|
||
|
||
Итоговая приёмочная проверка 060.29.2:
|
||
|
||
```text
|
||
Target unit: 227 passed
|
||
Full Market Data Storage unit: 290 passed
|
||
New 060.29.2 PostgreSQL integration: 12 passed
|
||
Full PostgreSQL Storage integration: 63 passed
|
||
Full integration with PostgreSQL: 76 passed
|
||
Full offline regression: 2461 passed, 81 deselected
|
||
Compileall через временный pycache: clean
|
||
Migration acceptance read-only review: clean
|
||
Reader/writer acceptance read-only review: clean
|
||
Повторный целевой набор review: 90 passed
|
||
```
|
||
|
||
Подэтапы 060.29.2–060.29.3 приняты. На этом этапе Build 060.29 оставался
|
||
`In Progress`.
|
||
|
||
Итоговая приёмочная проверка 060.29.3:
|
||
|
||
```text
|
||
Historical Access + Replay unit target: 270 passed
|
||
Expanded Access/Replay/Storage unit: 446 passed
|
||
New 060.29.3 PostgreSQL integration: 2 passed
|
||
Full PostgreSQL Storage integration: 65 passed
|
||
Full integration with PostgreSQL: 78 passed
|
||
Full offline regression: 2581 passed, 83 deselected
|
||
Compileall через временный pycache: clean
|
||
Повторный целевой unit-набор: 270 passed
|
||
Formal acceptance read-only review: clean
|
||
```
|
||
|
||
Подэтапы 060.29.3–060.29.4 приняты после отдельных формальных
|
||
приёмочных read-only review.
|
||
|
||
Проверка реализации Clock 060.29.4:
|
||
|
||
```text
|
||
Deterministic Replay Clock unit target: 22 passed
|
||
Full Replay unit target: 100 passed
|
||
Historical Access + Replay unit target: 292 passed
|
||
Full offline regression: 2603 passed, 83 deselected
|
||
Compileall через временный pycache: clean
|
||
git diff --check: clean
|
||
Короткий post-fix read-only review: clean
|
||
Formal acceptance read-only review: clean
|
||
```
|
||
|
||
Контракт и реализация 060.29.5 приняты после исправления fail-fast
|
||
проверки class objects и чистого повторного read-only review.
|
||
|
||
Проверка реализации Replay Session 060.29.5:
|
||
|
||
```text
|
||
Replay Session unit target: 34 passed
|
||
Full Replay unit target: 134 passed
|
||
Historical Access + Replay unit target: 326 passed
|
||
Full offline regression: 2637 passed, 83 deselected
|
||
Compileall через временный pycache: clean
|
||
git diff --check: clean
|
||
Короткий implementation read-only review: clean
|
||
Post-fix acceptance read-only review: clean
|
||
```
|
||
|
||
Проверка реализации Consumer and Composition Integration 060.29.6:
|
||
|
||
```text
|
||
Replay Session Factory unit target: 43 passed
|
||
Full Replay unit target: 177 passed
|
||
Historical Access + Replay unit target: 369 passed
|
||
Full offline regression: 2680 passed, 83 deselected
|
||
Compileall через временный pycache: clean
|
||
Tracked и untracked whitespace checks: clean
|
||
Implementation read-only review: clean
|
||
Formal acceptance read-only review: clean
|
||
```
|
||
|
||
Подэтап 060.29.6 принят. На этом этапе Build 060.29 оставался
|
||
`In Progress`.
|
||
|
||
Проверка реализации PostgreSQL Replay and Failure Verification 060.29.7:
|
||
|
||
```text
|
||
Новый PostgreSQL Replay target: 12 passed
|
||
Десятикратный прогон target: 10 × 12 passed
|
||
Full PostgreSQL Storage integration: 77 passed
|
||
Full integration with PostgreSQL: 90 passed
|
||
Historical Access + Replay unit target: 369 passed
|
||
PostgreSQL opt-out: 77 skipped
|
||
Pyright mandatory gate: 0 errors, 0 warnings
|
||
Pyright pytest gate: 1 passed
|
||
Full offline regression: 2681 passed, 95 deselected
|
||
Compileall через временный pycache: clean
|
||
Два повторных read-only review: clean
|
||
Production-код в 060.29.7: только static narrowing fix;
|
||
контракт и поведение неизменны
|
||
```
|
||
|
||
Подэтап 060.29.7 принят после чистого формального приёмочного read-only
|
||
review. На этом этапе Build 060.29 оставался `In Progress` до выполнения
|
||
060.29.8.
|
||
|
||
Финальная приёмочная проверка 060.29.8:
|
||
|
||
```text
|
||
Pyright mandatory gate: 0 errors, 0 warnings
|
||
Pyright pytest gate: 1 passed
|
||
Expanded Access/Replay/Storage unit: 545 passed
|
||
PostgreSQL Replay target repeated: 10 × 12 passed
|
||
Full PostgreSQL Storage integration: 77 passed
|
||
Full integration with PostgreSQL: 90 passed
|
||
PostgreSQL opt-out isolation: 77 skipped
|
||
Fixed stress target: 3 passed, 1 deselected
|
||
Full offline regression: 2681 passed, 95 deselected
|
||
pip check: clean
|
||
Compileall через временный pycache: clean
|
||
Tracked/untracked whitespace checks: clean
|
||
Три независимых read-only review: clean
|
||
Production-код в 060.29.8: без изменений
|
||
```
|
||
|
||
В integration и stress наборах `ResourceWarning` считался ошибкой.
|
||
Подэтап 060.29.8 и весь Build 060.29 приняты.
|
||
|
||
---
|
||
|
||
## 19. Следующий Build
|
||
|
||
Build 060.29 завершён и принят. Следующий этап —
|
||
[Build 060.30](build_060_30_architecture.md), итоговый аудит и
|
||
документальное закрытие ветки Trades Feed.
|