Files
dzentra_bot/docs/migrations/build_026.md

21 KiB
Raw Permalink Blame History

Build 026 — Аудит текущего контура Quotes Feed

Статус: Завершён
Подсистема: Market Data Acquisition
Функциональный модуль: Quotes Feed
Проект: Dzentra


1. Цель Build

Провести полный аудит существующего контура получения, обработки, кэширования и потребления текущих рыночных котировок перед началом миграции в целевую подсистему:

src/market_data/acquisition/

Основная задача Build — определить:

  • где сейчас реализовано получение котировок;
  • какие REST- и WebSocket-источники используются;
  • какие модели представляют котировку;
  • где выполняются parsing, validation и mapping;
  • как работает оперативный кэш котировок;
  • какие компоненты являются фактическими потребителями ценовых данных;
  • какие обязанности относятся непосредственно к Quotes Feed;
  • какие обязанности должны остаться за пределами Acquisition;
  • в какой последовательности выполнять безопасную миграцию без нарушения работы существующего бота.

2. Итог аудита

Текущий бот уже имеет функционально работающий контур получения и использования котировок.

Котировки поступают из двух источников:

  1. REST API;
  2. WebSocket depth stream.

При этом архитектурно логика распределена между:

src/integrations/exchange/service.py
src/integrations/exchange/rest_client.py
src/integrations/exchange/ws_client.py
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
src/integrations/exchange/market_cache.py
src/integrations/exchange/models.py

Единой канонической модели Quote в production-контуре пока нет.

Одна и та же концепция текущей рыночной котировки представлена несколькими различными контрактами:

TickerPrice
ExecutionPriceSnapshot
MarketPriceSnapshot
dict[str, object]

Это подтверждает необходимость поэтапной миграции в каноническую модель Quotes Feed.


3. Текущий REST-контур котировок

Основная реализация находится в:

src/integrations/exchange/service.py

Используются следующие методы:

refresh_price_cache()
refresh_market_snapshot_cache()
get_price()
get_market_snapshot()
get_execution_snapshot()
get_fresh_market_snapshot()
_get_real_price()

Основной источник данных:

GET /api/v1/ticker/24hr

Из ответа используются поля:

lastPrice
bidPrice
askPrice

Текущая цепочка выглядит следующим образом:

ExchangeService.get_fresh_market_snapshot()
        ↓
ExchangeRestClient.get_json()
        ↓
GET /api/v1/ticker/24hr
        ↓
lastPrice / bidPrice / askPrice
        ↓
legacy dict snapshot

REST-транспорт реализован в:

src/integrations/exchange/rest_client.py

4. Текущий WebSocket-контур котировок

В проекте существуют две реализации обработки WebSocket/depth-данных:

src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py

WebSocket-транспорт находится в:

src/integrations/exchange/ws_client.py

Для получения данных используется:

ExchangeWebSocketClient.stream_depth()

Из depth payload извлекаются:

best bid
best ask

После чего рассчитывается:

midpoint = (best_bid + best_ask) / 2

Результат записывается в:

MarketPriceCache

5. Текущие модели котировок

5.1. TickerPrice

Находится в:

src/integrations/exchange/models.py

Текущий контракт:

@dataclass(slots=True)
class TickerPrice:
    symbol: str
    price: float
    source: str
    updated_at: str

Используется как упрощённое представление текущей цены инструмента.


5.2. ExecutionPriceSnapshot

Находится в:

src/integrations/exchange/models.py

Содержит:

symbol
last_price
bid_price
ask_price
updated_at
source
is_fresh
age_seconds
freshness_status
spread_percent

Эта модель относится прежде всего к execution layer и не должна становиться канонической моделью Quotes Feed.


5.3. MarketPriceSnapshot

Находится в:

src/integrations/exchange/market_cache.py

Содержит:

symbol
price
bid_price
ask_price
updated_at
source
runtime_key
received_monotonic

Одновременно выполняет роль:

  • модели записи кэша;
  • контейнера рыночной цены;
  • источника информации о возрасте записи.

5.4. Словарные snapshot-контракты

Ряд методов ExchangeService возвращает:

dict[str, object]

с ключами:

symbol
last_price
bid_price
ask_price
updated_at
source
age_seconds

Такие словарные контракты используются многими существующими потребителями и должны быть удалены только после их полного перевода на новые типизированные контракты.


6. Основные архитектурные проблемы

6.1. Отсутствует единая каноническая модель Quote

Файл:

src/market_data/acquisition/models/quote.py

существует в целевой структуре, но текущий production-контур ещё не использует единую каноническую модель Quote.

Вместо неё используются:

TickerPrice
ExecutionPriceSnapshot
MarketPriceSnapshot
dict[str, object]

Целевая архитектура должна иметь одну внутреннюю каноническую модель котировки.


6.2. ExchangeService перегружен обязанностями

В текущем состоянии ExchangeService одновременно:

  • вызывает REST API;
  • получает ticker response;
  • разбирает поля ответа;
  • проверяет значения;
  • создаёт snapshot;
  • читает кэш;
  • обновляет кэш;
  • оценивает freshness;
  • создаёт execution snapshot;
  • поддерживает legacy API для существующих потребителей.

Эти обязанности должны быть постепенно разделены между:

adapters/dzengi/
validation/
models/
handlers/
feeds/
service.py
storage/
execution/

6.3. WebSocket parsing дублируется

Сходная логика присутствует одновременно в:

src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py

Дублируются следующие операции:

  • извлечение вложенного payload;
  • извлечение bids;
  • извлечение asks;
  • получение первой цены;
  • преобразование значения в float;
  • проверка положительности цены;
  • расчёт midpoint.

Эта логика должна быть централизована в Dzengi adapter:

src/market_data/acquisition/adapters/dzengi/parser.py
src/market_data/acquisition/adapters/dzengi/mapper.py

6.4. Quotes Feed и Order Book Feed частично смешаны

Метод:

stream_depth()

получает depth-сообщение, относящееся к данным стакана.

Однако текущие потребители используют из него только:

best bid
best ask

Для Quotes Feed это допустимый источник Level I quote.

При этом полный depth не должен переноситься в Quotes Feed, поскольку полный стакан относится к отдельной будущей подсистеме:

Order Book Feed

Таким образом, Quotes Feed должен получать из depth только необходимую информацию верхнего уровня:

best bid
best ask

и формировать из неё канонический Quote.


6.5. Кэш расположен в integration layer

Текущий кэш находится в:

src/integrations/exchange/market_cache.py

Он отвечает одновременно за:

  • модель snapshot;
  • хранение;
  • runtime partitioning;
  • возраст записи;
  • форматирование локального времени.

В целевой архитектуре хранение котировок не должно принадлежать Acquisition или exchange integration layer.

Канонический Quote Store должен находиться в storage layer.


6.6. Внутреннее время представлено UI-строкой

Текущее представление:

DD.MM.YYYY HH:MM:SS

например:

10.07.2026 12:00:00

является человекочитаемым UI-представлением, а не подходящим внутренним временным контрактом.

Каноническая модель должна хранить машинное время, например:

exchange_timestamp_ms
received_timestamp_ms

или timezone-aware datetime.

Форматирование времени для пользователя должно происходить только на UI-границе.


6.7. REST client содержит дублирование

В:

src/integrations/exchange/rest_client.py

существуют два метода:

get_payload()
get_json()

которые в значительной степени дублируют транспортную реализацию.

Исправление этого дублирования не является задачей первого этапа Quotes Feed.

Однако при дальнейшем развитии Dzengi REST adapter не следует создавать дополнительное дублирование транспорта.


6.8. Текущий WebSocket не является обычной push-subscription

Метод:

stream_depth()

работает следующим образом:

открыть постоянное WebSocket-соединение
        ↓
отправить новый request
        ↓
получить один response
        ↓
сделать sleep
        ↓
повторить request

Таким образом, текущая реализация ближе к polling поверх постоянного WebSocket-соединения, чем к классической push-subscription.

Кроме того, запуск WebSocket stream из:

src/main.py

временно отключён, поскольку runtime probe не подтвердил рабочий endpoint с WebSocket Upgrade 101.

Поэтому на текущем этапе архитектурно зафиксировано:

REST       — рабочий основной источник котировок
WebSocket  — сохраняемый экспериментальный или резервный транспорт

Первая версия нового Quotes Feed не должна зависеть от гарантированной доступности WebSocket.


7. Граница ответственности канонической модели Quote

Каноническая модель должна представлять непосредственно полученную рыночную котировку.

В неё должны входить данные уровня:

symbol
last_price
bid_price
ask_price
exchange_timestamp
received_timestamp
source

Дополнительно могут быть предусмотрены:

sequence_id
event_id

но только если соответствующий источник Dzengi действительно предоставляет такие значения.


8. Что не должно входить в базовую модель Quote

В каноническую модель не следует помещать:

runtime_key
age_seconds
is_fresh
freshness_status
spread_percent
execution side
entry price
UI-formatted updated_at

Причины:

Поле Правильная ответственность
runtime_key Storage
age_seconds Storage / Access layer
is_fresh Политика конкретного потребителя
freshness_status Runtime / consumer policy
spread_percent Производная метрика
execution side Execution layer
entry price Execution layer
updated_at в UI-формате UI formatting

9. Фактические потребители котировок

9.1. Потребители get_price()

src/telegram/ui/currency_ui.py
src/telegram/handlers/auto/ui.py
src/trading/auto/execution_quality.py

9.2. Потребители get_market_snapshot()

src/telegram/handlers/auto/ui.py
src/telegram/handlers/debug_auto/ui.py
src/trading/auto/signal_runtime.py
src/trading/auto/execution_quality.py
src/trading/strategies/trend.py
src/trading/strategies/scalp.py
src/trading/diagnostics/snapshot.py

9.3. Потребители get_execution_snapshot()

src/trading/execution/pricing.py
src/telegram/handlers/debug_auto/ui.py

9.4. Потребители get_fresh_market_snapshot()

src/integrations/exchange/service.py
src/trading/debug/execution.py

Кроме того, этот метод используется внутри runtime-проверки статуса инструмента.


10. Текущий MarketPriceCache

Реализация находится в:

src/integrations/exchange/market_cache.py

Основные операции:

MarketPriceCache.set_price()
MarketPriceCache.get_price()
MarketPriceCache.clear()

Ключ записи:

(runtime_key, symbol)

Кэш используется из:

src/integrations/exchange/service.py
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py

На текущем этапе MarketPriceCache нельзя удалять, поскольку он является частью рабочего production-контура.

Он будет заменён только после появления канонического Quote Store и перевода всех производителей и потребителей.


11. Целевая архитектурная цепочка REST Quotes Feed

Dzengi GET /api/v1/ticker/24hr
        ↓
adapters/dzengi/rest.py
        ↓
adapters/dzengi/models.py
        ↓
adapters/dzengi/parser.py
        ↓
validation/schema.py
        ↓
validation/values.py
        ↓
adapters/dzengi/mapper.py
        ↓
models/quote.py
        ↓
handlers/quotes_handler.py
        ↓
feeds/quotes_feed.py
        ↓
acquisition/service.py
        ↓
legacy ExchangeService facade
        ↓
существующие потребители бота

12. Целевая архитектурная цепочка WebSocket Quotes Feed

Dzengi WebSocket depth message
        ↓
adapters/dzengi/websocket.py
        ↓
adapters/dzengi/parser.py
        ↓
извлечение best bid / best ask
        ↓
validation/
        ↓
adapters/dzengi/mapper.py
        ↓
models/quote.py
        ↓
handlers/quotes_handler.py
        ↓
feeds/quotes_feed.py
        ↓
Quote Store
        ↓
runtime consumers

Полный order book при этом не является частью Quotes Feed и должен в будущем обрабатываться отдельной подсистемой:

Order Book Feed

13. Принцип безопасной миграции

Миграция должна выполняться без одномоментной замены рабочего контура.

Основной принцип:

новая реализация создаётся параллельно
        ↓
покрывается тестами
        ↓
подключается под существующий facade
        ↓
потребители переводятся поэтапно
        ↓
legacy удаляется только после подтверждения отсутствия потребителей

На переходном этапе сохраняются:

ExchangeService.get_price()
ExchangeService.get_market_snapshot()
ExchangeService.get_execution_snapshot()
ExchangeService.get_fresh_market_snapshot()
MarketPriceCache
TickerPrice
ExecutionPriceSnapshot

Удаление допускается только в соответствующих поздних Build после полного перевода потребителей.


14. Утверждённый план миграции Quotes Feed

Build 026 — Аудит текущего контура Quotes Feed
Build 027 — Каноническая модель Quote и специализированные контракты
Build 028 — Dzengi REST quote models, parser и validation
Build 029 — Dzengi mapper и Quotes Handler
Build 030 — Quotes Feed и регистрация в Acquisition Service
Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
Build 032 — Канонический Quote Store
Build 033 — Перенос MarketPriceCache на Quote Store
Build 034 — Dzengi WebSocket quote parsing и адаптер
Build 035 — Перевод market runtime на Quotes Feed
Build 036 — Перевод read-only и UI-потребителей
Build 037 — Перевод execution-потребителей
Build 038 — Удаление legacy TickerPrice и market snapshot dict layer
Build 039 — Удаление legacy quote parsing и MarketPriceCache
Build 040 — Финальная архитектурная проверка Quotes Feed

Положение WebSocket-этапа после рабочего REST-контура является намеренным.

Бот должен сохранить гарантированный рабочий способ получения котировок даже при отсутствии подтверждённого production WebSocket endpoint.


15. Результат Build 026

В результате Build 026:

  • полностью определён существующий REST-контур котировок;
  • полностью определён существующий WebSocket/depth-контур;
  • найдены все текущие модели ценовых данных;
  • определены прямые производители и потребители котировок;
  • проанализирован MarketPriceCache;
  • обнаружено дублирование WebSocket parsing;
  • определена граница между Quotes Feed и Order Book Feed;
  • определена граница между Acquisition, Storage, Execution и UI;
  • подтверждена необходимость сохранения legacy facade на время миграции;
  • определена безопасная последовательность Build 027040.

16. Статус завершения

Build 026 завершён полностью.

Дополнительных изменений кода в рамках Build 026 не требуется.

Следующий этап:

Build 027 — Каноническая модель Quote и специализированные контракты