29 KiB
Build 060.27 — Persistent Market Data Storage Architecture
Статус: Completed
Build: 060.27
Подсистема: Market Data Acquisition / Persistent Storage
Дата начала: 2026-07-31
Дата завершения: 2026-08-01
Версия документа: 1.5
1. Назначение
Build 060.27 создаёт долговременное PostgreSQL-хранилище Canonical Market Data. Данные должны сохраняться независимо от времени жизни процесса и стать основой для Persistent Checkpoint, исторических запросов и Replay в следующих Build.
В Build реализуется production-интеграция только для Trades. Для Quotes и Candles создаются схема, repository-контракты и Storage API, но существующие legacy/runtime consumers пока к ним не подключаются.
2. Статус подэтапов
| Подэтап | Название | Статус |
|---|---|---|
| 060.27.0 | Storage Contract and Boundaries | Accepted |
| 060.27.1 | PostgreSQL Schema, Migrations and Pool | Accepted |
| 060.27.2 | Persistent Trade Repository | Accepted |
| 060.27.3 | Quote and Candle Repositories | Accepted |
| 060.27.4 | Storage API and Retention | Accepted |
| 060.27.5 | Trade Runtime Persistence Integration | Accepted |
| 060.27.6 | Bootstrap and Lifecycle Integration | Accepted |
| 060.27.7 | Integration and Failure Verification | Accepted |
| 060.27.8 | Final Regression and Acceptance | Accepted |
060.27.0–060.27.1 прошли отдельный read-only review. После исправления cleanup при системном прерывании и удаления двух дублирующих индексов повторный review завершён без findings.
060.27.2 прошёл отдельный read-only review конкурентных вставок, Canonical conflict, provenance, batch atomicity и error propagation. Review завершён без findings.
060.27.3 прошёл отдельный read-only review Quote timestamp collision, Candle revision identity, interval case, concurrent provenance и transaction rollback. Review завершён без findings.
060.27.4 принят после исправления проверки фактического PostgreSQL partition bound и изоляции Retention от concurrent writers. Повторный read-only review concurrent creation, default-row move, DDL safety, точности cutoff и полного rollback завершён без findings.
060.27.5 принят после отдельного read-only review durable-before- checkpoint ordering, duplicate provenance, fatal error propagation, cancellation-safe live offload и единственного владельца записи. Review завершён без findings; целевые тесты и полная регрессия прошли.
060.27.6 реализует disabled-by-default Storage feature flag, общий bootstrap graph и управляемый Application lifecycle. Целевые тесты и полная регрессия прошли. Отдельный read-only review feature flag, отсутствия Storage I/O при composition, partial startup, shutdown order, repeated cancellation и task ownership завершён без findings.
060.27.7 добавляет безопасный opt-in PostgreSQL harness и выполняет
реальные schema, repository, partition, retention, Runtime persistence и
failure-сценарии на одноразовой локальной базе. Настоящий PostgreSQL
выявил ошибку параметризации partition DDL; границы CHECK и ATTACH теперь
встраиваются как безопасные psycopg.sql.Literal.
Первичный review выявил обход local-only проверки через PostgreSQL service/неявный endpoint и недостаточное доказательство одновременного участия concurrent callers. Harness теперь запрещает service, неявные и смешанные local/remote endpoints, а непосредственно перед destructive cleanup повторно сверяет точное имя базы и identity контрольного соединения. Migration и partition tests удерживают advisory lock, пока PostgreSQL не подтвердит ожидание обеих caller-сессий; Trade writers синхронизируются общим барьером. Повторный review завершён без findings, подэтап принят.
060.27.8 выполнил финальную verification-only приёмку без изменения production-кода. Целевой unit-набор, новый одноразовый PostgreSQL, десять повторов PostgreSQL integration, общий integration, fixed stress и полная offline-регрессия прошли. Итоговый read-only review не выявил новых findings; подэтап и Build 060.27 приняты.
3. Принятые архитектурные решения
- В production runtime этого Build подключается только хранение Trades.
- Для Quotes и Candles создаётся storage foundation без изменения существующего legacy/runtime wiring.
- Operational checkpoint можно продвигать только после успешного сохранения соответствующего Trade.
- Ошибка включённого persistent storage является фатальной: Runtime не должен скрывать потерю истории и продолжать работу как будто всё успешно.
- Raw exchange documents в Build 060.27 не сохраняются. Хранилище принимает только прошедшие validation и mapping Canonical Models.
- Canonical history по умолчанию не удаляется. Retention включается только явной настройкой и отдельной операцией.
- Идентичность Trade задаётся кортежем
venue,symbol,trade_id,executed_at. Включение времени защищает от коллизии после полного цикла signed 32-bit Trade ID. - Market Data размещаются в PostgreSQL schema
market_data, изменения схемы выполняются упорядоченными SQL-миграциями, соединения выдаёт один явно управляемый pool.
4. Архитектурная граница
Canonical Trade / Quote / Candle
│
▼
market_data.storage contracts
│
▼
PostgreSQL repositories (060.27.2–060.27.3)
│
▼
storage.PostgresConnectionPool
│
▼
PostgreSQL schema market_data
Storage зависит от Canonical Models. Canonical Models, Consistency,
Recovery и Transport не зависят от PostgreSQL или psycopg.
Trade Runtime использует общий acquisition-side контракт
TradeObservationSinkProtocol. Его Storage adapter знает о
TradeStorageProtocol, venue и UTC clock, но Consistency и Runtime не
знают о PostgreSQL repository или pool:
Live / Recovery
│
▼
shared TradeStreamConsistencyController
│ validated observation
▼
TradeObservationSinkProtocol
│
▼
TradeStorageObservationSink → TradeStorageProtocol
Контракты возвращают явный результат записи:
INSERTED— новое событие сохранено;DUPLICATE— полностью совпадающее событие уже существовало;PROVENANCE_UPDATED— Canonical payload совпал, но обновлена информация о наблюдении события.
Несовпадающий payload под тем же ключом не является дубликатом и должен
приводить к MarketDataStorageConflictError.
5. Модель хранения
5.1. Trades
Таблица market_data.trades хранит Canonical Trade и технические поля
provenance:
identity = venue + symbol + trade_id + executed_at
provenance = source + observation_sources
+ first_observed_at + last_observed_at
source сохраняет первый зафиксированный путь получения Trade.
observation_sources накапливает уникальные Canonical source names,
например WebSocket и REST Recovery, не превращая различие transport
provenance в конфликт биржевого факта.
trade_id ограничен signed 32-bit диапазоном. Это соответствует
единому rollover-aware контракту Build 060.26, но база не пытается
сортировать события только по числовому ID. Исторический порядок
определяется прежде всего executed_at.
5.2. Quotes
Таблица market_data.quotes хранит неизменяемые Canonical snapshots.
Ключ снимка: venue, symbol, received_at. Nullable
exchange_timestamp сохраняется как часть payload, но не используется
как обязательный ключ.
source сохраняет первый зафиксированный путь получения, а
observation_sources — уникальные источники того же snapshot. Два
разных payload с одним received_at являются явным timestamp conflict;
repository не изменяет полученное время и не создаёт скрытый surrogate.
5.3. Candles
Свеча может меняться до закрытия интервала. Поэтому
market_data.candle_revisions не перезаписывает строку на месте, а
хранит последовательность наблюдавшихся ревизий. Ключ:
venue + symbol + interval + open_time + observed_at
Флаг is_final отделяет промежуточную ревизию от финальной.
Изменение содержимого или is_final сохраняется как новая ревизия с
новым observed_at. Разный source той же ревизии обновляет только
observation_sources. Регистр interval сохраняется, потому что имена
рыночных интервалов могут быть чувствительны к регистру.
5.4. Партиционирование и retention
Все три таблицы объявлены как range-partitioned по времени события и имеют default partition. Это даёт безопасную запись до появления автоматического partition manager.
Default partition остаётся страховкой для записи до создания нужного
месяца. PostgresMarketDataPartitionManager под transaction-level
advisory lock создаёт отдельную месячную таблицу, блокирует default
partition, переносит подходящие строки и подключает таблицу как
partition в одной транзакции.
Созданные Dzentra partitions регистрируются в
market_data.partition_registry. Существующая незарегистрированная
таблица, отсутствующая зарегистрированная таблица или detached
partition считаются ошибкой конфигурации и не принимаются молча.
Canonical retention по умолчанию не ограничен.
PostgresMarketDataRetentionService вообще не обращается к базе при
выключенной policy. Включённый явный запуск удаляет целиком только
зарегистрированные истёкшие partitions, затем удаляет строки до точного
UTC cutoff из оставшихся/default partitions. Все настроенные типы
очищаются в одной транзакции.
6. Версионированные миграции
StorageMigrationRunner:
- получает соединение через переданный provider;
- берёт PostgreSQL transaction-level advisory lock;
- создаёт таблицу истории
public.storage_schema_migrations; - сверяет уже применённые версии и их неизменяемые имена;
- применяет отсутствующие версии строго по порядку;
- записывает версию в историю в той же транзакции.
Неизвестная версия в базе или другое имя уже применённой версии считается ошибкой. Миграции не должны молча принимать расхождение между кодом и фактической схемой.
Начальный набор:
| Версия | Назначение |
|---|---|
| 1 | schema market_data |
| 2 | partitioned Canonical Trades |
| 3 | partitioned Canonical Quotes |
| 4 | partitioned immutable Candle revisions |
| 5 | multiple Trade observation sources |
| 6 | Quote and Candle observation sources |
| 7 | managed monthly partition registry |
7. Connection Pool lifecycle
PostgresConnectionPool имеет явный lifecycle:
construct → open → borrow/return connections → close
Конструктор не открывает сеть. open() ждёт готовности минимального
числа соединений и при ошибке освобождает частично созданный pool.
connection() до успешного open() запрещён. close() идемпотентен.
В 060.27.6 pool, migration runner, Trade repository и observation sink
собираются без сетевых операций. Application запускает blocking startup
через owned asyncio.to_thread task до Telegram и Trade Runtime:
pool.open → migrations.run → Telegram + Trade Runtime
Shutdown выполняется в обратном порядке зависимостей. Сначала полностью останавливается Trade Runtime и завершаются его Storage-вызовы, затем закрывается pool и после этого bot session.
8. Обработка ошибок
- ошибки lifecycle pool оборачиваются в
PostgresConnectionPoolError; - ошибки запуска или расхождения миграций оборачиваются в
StorageMigrationError; - repository validation, conflict и database operations используют
иерархию
MarketDataStorageError; KeyboardInterruptиSystemExitне превращаются в storage errors;- ошибка включённого persistent storage фатальна и не скрывается;
- ошибка cleanup не заменяет исходную startup/runtime ошибку;
- cancellation ждёт уже начатый blocking lifecycle operation.
9. Границы реализации 060.27.0–060.27.8
Реализовано:
- отдельный пакет
market_data.storage; - write-only Protocol contracts для Trades, Quotes и Candles;
- результаты одиночной и пакетной идемпотентной записи;
- отдельная иерархия ошибок Market Data Storage;
- versioned migration runner с advisory lock;
- partitioned PostgreSQL schema для трёх Canonical типов;
- явно открываемый и закрываемый PostgreSQL connection pool;
- синхронный
PostgresTradeRepositoryбез ownership pool lifecycle; - одиночная и атомарная пакетная запись Canonical Trades;
- exact duplicate, provenance update и canonical conflict semantics;
- стабильный порядок batch-записи и rollback всей пачки при ошибке;
- синхронные
PostgresQuoteRepositoryиPostgresCandleRepository; - immutable Quote snapshot и Candle revision conflict semantics;
- multiple-source provenance для Trades, Quotes и Candle revisions;
- общие validation helpers трёх PostgreSQL repositories;
- единый write-only
MarketDataStoragefacade без lifecycle ownership; - UTC monthly partition descriptors для трёх Canonical типов;
- транзакционное создание, перенос и attach managed partitions;
- реестр Dzentra-managed partitions в migration 7;
- disabled-by-default retention с отдельным явным запуском;
- удаление полных partitions и точная очистка partial/default данных;
- общий advisory lock для partition lifecycle и retention;
- закреплённые версии
psycopgиpsycopg-pool; - общий optional
TradeObservationSinkProtocolдля Live и Recovery; - единственный persistence owner в
TradeStreamConsistencyController; - durable write до продвижения operational checkpoint;
- запись корректных дубликатов для обновления provenance;
- выполнение синхронной live-записи вне asyncio event loop;
- cancellation-safe ожидание уже начатой live-записи при shutdown;
- отдельный
MarketDataStorageSettingsи безопасный feature flag; - единый bootstrap graph pool → migrations → Trade repository → sink;
- использование одного Settings snapshot и существующих
DB_*; - Storage startup до Telegram и Trade Runtime;
- partial startup cleanup при migration failure;
- закрытие pool только после полной остановки Trade Runtime;
- cancellation-safe Application startup и shutdown tasks.
- opt-in harness только для явно указанной локальной disposable базы;
- отсутствие fallback PostgreSQL integration-тестов на application
DB_*; - real PostgreSQL verification миграций, repositories и транзакций;
- real concurrent migration, Trade write и partition callers;
- real partition move, retention cutoff и multi-type rollback;
- local WebSocket/REST Runtime persistence в настоящий PostgreSQL;
- real reconnect/recovery provenance через общий Trade repository;
- fatal persistence failure без ложного продвижения checkpoint;
- проверка Application ownership pool и отсутствия оставшихся соединений.
Не реализовано на этом шаге:
- чтение истории;
- автоматический partition/retention scheduling;
- подключение legacy Quote/Candle consumers.
10. Критерии приёмки 060.27.0–060.27.8
- Storage contracts не импортируют Runtime, Consistency или Recovery.
- Конструктор pool не выполняет сетевых операций.
- Частично открытый pool очищается при ошибке startup.
- Повторный
close()безопасен. - Миграции выполняются один раз, последовательно и атомарно.
- Concurrent migration runners сериализуются advisory lock.
- Schema содержит явные idempotency keys и Canonical constraints.
- Trade Runtime не импортирует PostgreSQL repository или pool.
- Целевые тесты и обычная регрессия проходят успешно.
- Источник доставки не является частью Canonical Trade conflict.
- Один конфликт или database error откатывает весь Trade batch.
- Repository не владеет pool lifecycle и не импортирует Runtime.
- Quote timestamp collision и Candle revision conflict не скрываются.
- Quote/Candle source обновляет provenance, а не Canonical payload.
- Существующие Quote/Candle consumers не подключены к PostgreSQL.
- Storage facade делегирует записи и не владеет pool lifecycle.
- Имена таблиц и колонок выбираются только из закрытого набора и
экранируются через
psycopg.sql.Identifier. - Повторное создание месяца идемпотентно, а расхождение реестра и PostgreSQL является явной ошибкой.
- Неудачный setup partition откатывает создание, перенос и registry.
- Retention требует отдельного
enabledи положительного окна. - Выключенный retention не получает соединение и ничего не удаляет.
- Ошибка очистки одного типа откатывает всю retention-операцию.
- Runtime, Scheduler и существующие consumers не импортируют новый Storage API.
- Live и Recovery используют один persistence sink через общий
TradeStreamConsistencyController. - Validated Trade сохраняется до изменения checkpoint и dedup window.
- Корректный дубликат сохраняется для provenance без продвижения checkpoint.
- Ordering error, conflicting duplicate и invalid Trade не передаются в Storage.
- Ошибка Storage не скрывается и не продвигает checkpoint.
- Live persistence выполняется через owned
asyncio.to_threadtask при закрытом live-processing gate. - Shutdown ждёт уже начатую live-запись и не оставляет её фоновой.
- Отсутствующий optional sink сохраняет прежнее поведение Composition.
- Bootstrap/settings не изменены, PostgreSQL pool не открывается.
- Целевой набор и полная обычная регрессия проходят успешно.
- Storage feature flag выключен по умолчанию и не строит зависимости.
- Включённый Storage требует включённый Trade Stream.
- Pool settings валидируются только при включённом Storage.
- Bootstrap graph не открывает PostgreSQL и не запускает migrations.
- Migration runner и Trade repository используют один pool identity.
- Один observation sink передаётся общим Live/Recovery controller.
- Pool открывается и migrations завершаются до root Runtime tasks.
- Migration failure закрывает открытый pool и не запускает Runtime.
- Blocking startup/shutdown выполняются вне asyncio event loop.
- Cancellation ждёт завершения уже начатой blocking операции.
- Trade Runtime полностью остановлен до закрытия pool.
- Storage cleanup error не заменяет исходную Runtime ошибку.
- Retention и partition scheduling автоматически не запускаются.
- PostgreSQL integration требует точный opt-in flag и отдельный DSN.
- Integration DSN принимает только localhost/local socket и database
name с префиксом
dzentra_test_. - Harness не читает application
DB_*и не управляет Docker скрытно. - Migrations 1–7 реально применяются в PostgreSQL и повторный запуск ничего не меняет.
- Два real migration runners применяют каждую версию ровно один раз.
- Trades, Quotes и Candle revisions проходят real insert, duplicate, provenance и conflict paths.
- Trade batch реально откатывает предшествующую вставку при конфликте.
- Два concurrent Trade writers сходятся к одной Canonical строке.
- Partition DDL использует безопасные SQL literals вместо запрещённых
PostgreSQL parameters в
ALTER TABLE. - Real partition manager переносит default rows и сериализует callers.
- Real retention соблюдает точный cutoff и общий transaction rollback.
- Live и reconnect/recovery Runtime сохраняют Trades в один repository.
- Закрытый pool приводит к fatal Runtime error без продвижения checkpoint и без записи проблемного Trade.
- Application открывает и мигрирует Storage до Runtime, а закрывает pool после Runtime; после shutdown нет owned tasks и pool connections.
- Выключенный opt-in PostgreSQL-набор не требует локальной базы и корректно пропускается обычной regression-командой.
- Destructive cleanup непосредственно перед DDL повторно проверяет точное имя disposable базы и специальный control application name.
- Concurrent migration и partition tests подтверждают в PostgreSQL, что обе caller-сессии действительно ожидают один advisory lock.
10.1. Opt-in PostgreSQL verification
Тесты никогда не используют production DB_*. Первый запуск требует
явного одноразового PostgreSQL и выполняется командой вида:
DZENTRA_RUN_POSTGRES_TESTS=1 \
DZENTRA_TEST_POSTGRES_DSN=postgresql://test:test@127.0.0.1:55432/dzentra_test_060_27 \
.venv/bin/python -m pytest -q -m integration \
tests/integration/market_data/storage
Harness проверяет имя базы и каждый local endpoint до соединения,
запрещает service/implicit fallback, берёт session advisory lock и
очищает только market_data и собственную таблицу migration history.
Перед каждым destructive cleanup он повторно подтверждает фактическое
имя disposable базы и identity контрольного соединения.
11. Финальная приёмка 060.27.8
Финальный этап выполнялся по verification-only правилу: production-код не изменялся, потому что проверки не выявили нового дефекта.
Результаты на 2026-08-01:
Targeted Storage/Runtime unit suite: 307 passed
PostgreSQL control run: 15 passed
PostgreSQL repeated ten times: 150 passed
Combined integration suite: 26 passed
Fixed stress: 3 passed, 1 deselected
PostgreSQL suite without opt-in: 15 skipped
Full offline regression: 2119 passed, 31 deselected
git diff --check: clean
Untracked files diff check: clean
Во всех integration и stress проверках ResourceWarning считался
ошибкой. Одноразовый PostgreSQL 16 использовал временное tmpfs
хранилище и после проверки был остановлен и удалён.
Итоговый read-only review повторно проверил архитектурные зависимости, SQL/DDL safety, durable-before-checkpoint ordering, fatal persistence failure, lifecycle pool и Runtime, opt-in isolation и test coverage. Новых findings не обнаружено.
12. Следующий Build
Build 060.27 завершён и принят. Следующий этап — Build 060.28, Persistent Checkpoint and Startup Recovery.