Files
dzentra_bot/docs/migrations/build_026.md

799 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Build 026 — Аудит текущего контура Quotes Feed
**Статус:** Завершён
**Подсистема:** Market Data Acquisition
**Функциональный модуль:** Quotes Feed
**Проект:** Dzentra
---
## 1. Цель Build
Провести полный аудит существующего контура получения, обработки, кэширования и потребления текущих рыночных котировок перед началом миграции в целевую подсистему:
```text
src/market_data/acquisition/
```
Основная задача Build — определить:
- где сейчас реализовано получение котировок;
- какие REST- и WebSocket-источники используются;
- какие модели представляют котировку;
- где выполняются parsing, validation и mapping;
- как работает оперативный кэш котировок;
- какие компоненты являются фактическими потребителями ценовых данных;
- какие обязанности относятся непосредственно к Quotes Feed;
- какие обязанности должны остаться за пределами Acquisition;
- в какой последовательности выполнять безопасную миграцию без нарушения работы существующего бота.
---
## 2. Итог аудита
Текущий бот уже имеет функционально работающий контур получения и использования котировок.
Котировки поступают из двух источников:
1. REST API;
2. WebSocket depth stream.
При этом архитектурно логика распределена между:
```text
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-контуре пока нет.
Одна и та же концепция текущей рыночной котировки представлена несколькими различными контрактами:
```text
TickerPrice
ExecutionPriceSnapshot
MarketPriceSnapshot
dict[str, object]
```
Это подтверждает необходимость поэтапной миграции в каноническую модель Quotes Feed.
---
## 3. Текущий REST-контур котировок
Основная реализация находится в:
```text
src/integrations/exchange/service.py
```
Используются следующие методы:
```python
refresh_price_cache()
refresh_market_snapshot_cache()
get_price()
get_market_snapshot()
get_execution_snapshot()
get_fresh_market_snapshot()
_get_real_price()
```
Основной источник данных:
```text
GET /api/v1/ticker/24hr
```
Из ответа используются поля:
```text
lastPrice
bidPrice
askPrice
```
Текущая цепочка выглядит следующим образом:
```text
ExchangeService.get_fresh_market_snapshot()
ExchangeRestClient.get_json()
GET /api/v1/ticker/24hr
lastPrice / bidPrice / askPrice
legacy dict snapshot
```
REST-транспорт реализован в:
```text
src/integrations/exchange/rest_client.py
```
---
## 4. Текущий WebSocket-контур котировок
В проекте существуют две реализации обработки WebSocket/depth-данных:
```text
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
```
WebSocket-транспорт находится в:
```text
src/integrations/exchange/ws_client.py
```
Для получения данных используется:
```python
ExchangeWebSocketClient.stream_depth()
```
Из depth payload извлекаются:
```text
best bid
best ask
```
После чего рассчитывается:
```text
midpoint = (best_bid + best_ask) / 2
```
Результат записывается в:
```text
MarketPriceCache
```
---
## 5. Текущие модели котировок
### 5.1. TickerPrice
Находится в:
```text
src/integrations/exchange/models.py
```
Текущий контракт:
```python
@dataclass(slots=True)
class TickerPrice:
symbol: str
price: float
source: str
updated_at: str
```
Используется как упрощённое представление текущей цены инструмента.
---
### 5.2. ExecutionPriceSnapshot
Находится в:
```text
src/integrations/exchange/models.py
```
Содержит:
```text
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
Находится в:
```text
src/integrations/exchange/market_cache.py
```
Содержит:
```text
symbol
price
bid_price
ask_price
updated_at
source
runtime_key
received_monotonic
```
Одновременно выполняет роль:
- модели записи кэша;
- контейнера рыночной цены;
- источника информации о возрасте записи.
---
### 5.4. Словарные snapshot-контракты
Ряд методов `ExchangeService` возвращает:
```python
dict[str, object]
```
с ключами:
```text
symbol
last_price
bid_price
ask_price
updated_at
source
age_seconds
```
Такие словарные контракты используются многими существующими потребителями и должны быть удалены только после их полного перевода на новые типизированные контракты.
---
## 6. Основные архитектурные проблемы
### 6.1. Отсутствует единая каноническая модель Quote
Файл:
```text
src/market_data/acquisition/models/quote.py
```
существует в целевой структуре, но текущий production-контур ещё не использует единую каноническую модель `Quote`.
Вместо неё используются:
```text
TickerPrice
ExecutionPriceSnapshot
MarketPriceSnapshot
dict[str, object]
```
Целевая архитектура должна иметь одну внутреннюю каноническую модель котировки.
---
### 6.2. ExchangeService перегружен обязанностями
В текущем состоянии `ExchangeService` одновременно:
- вызывает REST API;
- получает ticker response;
- разбирает поля ответа;
- проверяет значения;
- создаёт snapshot;
- читает кэш;
- обновляет кэш;
- оценивает freshness;
- создаёт execution snapshot;
- поддерживает legacy API для существующих потребителей.
Эти обязанности должны быть постепенно разделены между:
```text
adapters/dzengi/
validation/
models/
handlers/
feeds/
service.py
storage/
execution/
```
---
### 6.3. WebSocket parsing дублируется
Сходная логика присутствует одновременно в:
```text
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
```
Дублируются следующие операции:
- извлечение вложенного payload;
- извлечение `bids`;
- извлечение `asks`;
- получение первой цены;
- преобразование значения в `float`;
- проверка положительности цены;
- расчёт midpoint.
Эта логика должна быть централизована в Dzengi adapter:
```text
src/market_data/acquisition/adapters/dzengi/parser.py
src/market_data/acquisition/adapters/dzengi/mapper.py
```
---
### 6.4. Quotes Feed и Order Book Feed частично смешаны
Метод:
```python
stream_depth()
```
получает depth-сообщение, относящееся к данным стакана.
Однако текущие потребители используют из него только:
```text
best bid
best ask
```
Для Quotes Feed это допустимый источник Level I quote.
При этом полный depth не должен переноситься в Quotes Feed, поскольку полный стакан относится к отдельной будущей подсистеме:
```text
Order Book Feed
```
Таким образом, Quotes Feed должен получать из depth только необходимую информацию верхнего уровня:
```text
best bid
best ask
```
и формировать из неё канонический `Quote`.
---
### 6.5. Кэш расположен в integration layer
Текущий кэш находится в:
```text
src/integrations/exchange/market_cache.py
```
Он отвечает одновременно за:
- модель snapshot;
- хранение;
- runtime partitioning;
- возраст записи;
- форматирование локального времени.
В целевой архитектуре хранение котировок не должно принадлежать Acquisition или exchange integration layer.
Канонический Quote Store должен находиться в storage layer.
---
### 6.6. Внутреннее время представлено UI-строкой
Текущее представление:
```text
DD.MM.YYYY HH:MM:SS
```
например:
```text
10.07.2026 12:00:00
```
является человекочитаемым UI-представлением, а не подходящим внутренним временным контрактом.
Каноническая модель должна хранить машинное время, например:
```text
exchange_timestamp_ms
received_timestamp_ms
```
или timezone-aware `datetime`.
Форматирование времени для пользователя должно происходить только на UI-границе.
---
### 6.7. REST client содержит дублирование
В:
```text
src/integrations/exchange/rest_client.py
```
существуют два метода:
```python
get_payload()
get_json()
```
которые в значительной степени дублируют транспортную реализацию.
Исправление этого дублирования не является задачей первого этапа Quotes Feed.
Однако при дальнейшем развитии Dzengi REST adapter не следует создавать дополнительное дублирование транспорта.
---
### 6.8. Текущий WebSocket не является обычной push-subscription
Метод:
```python
stream_depth()
```
работает следующим образом:
```text
открыть постоянное WebSocket-соединение
отправить новый request
получить один response
сделать sleep
повторить request
```
Таким образом, текущая реализация ближе к polling поверх постоянного WebSocket-соединения, чем к классической push-subscription.
Кроме того, запуск WebSocket stream из:
```text
src/main.py
```
временно отключён, поскольку runtime probe не подтвердил рабочий endpoint с WebSocket Upgrade 101.
Поэтому на текущем этапе архитектурно зафиксировано:
```text
REST — рабочий основной источник котировок
WebSocket — сохраняемый экспериментальный или резервный транспорт
```
Первая версия нового Quotes Feed не должна зависеть от гарантированной доступности WebSocket.
---
## 7. Граница ответственности канонической модели Quote
Каноническая модель должна представлять непосредственно полученную рыночную котировку.
В неё должны входить данные уровня:
```text
symbol
last_price
bid_price
ask_price
exchange_timestamp
received_timestamp
source
```
Дополнительно могут быть предусмотрены:
```text
sequence_id
event_id
```
но только если соответствующий источник Dzengi действительно предоставляет такие значения.
---
## 8. Что не должно входить в базовую модель Quote
В каноническую модель не следует помещать:
```text
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()`
```text
src/telegram/ui/currency_ui.py
src/telegram/handlers/auto/ui.py
src/trading/auto/execution_quality.py
```
---
### 9.2. Потребители `get_market_snapshot()`
```text
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()`
```text
src/trading/execution/pricing.py
src/telegram/handlers/debug_auto/ui.py
```
---
### 9.4. Потребители `get_fresh_market_snapshot()`
```text
src/integrations/exchange/service.py
src/trading/debug/execution.py
```
Кроме того, этот метод используется внутри runtime-проверки статуса инструмента.
---
## 10. Текущий MarketPriceCache
Реализация находится в:
```text
src/integrations/exchange/market_cache.py
```
Основные операции:
```python
MarketPriceCache.set_price()
MarketPriceCache.get_price()
MarketPriceCache.clear()
```
Ключ записи:
```text
(runtime_key, symbol)
```
Кэш используется из:
```text
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
```text
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
```text
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 и должен в будущем обрабатываться отдельной подсистемой:
```text
Order Book Feed
```
---
## 13. Принцип безопасной миграции
Миграция должна выполняться без одномоментной замены рабочего контура.
Основной принцип:
```text
новая реализация создаётся параллельно
покрывается тестами
подключается под существующий facade
потребители переводятся поэтапно
legacy удаляется только после подтверждения отсутствия потребителей
```
На переходном этапе сохраняются:
```text
ExchangeService.get_price()
ExchangeService.get_market_snapshot()
ExchangeService.get_execution_snapshot()
ExchangeService.get_fresh_market_snapshot()
MarketPriceCache
TickerPrice
ExecutionPriceSnapshot
```
Удаление допускается только в соответствующих поздних Build после полного перевода потребителей.
---
## 14. Утверждённый план миграции Quotes Feed
```text
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 не требуется.
Следующий этап:
```text
Build 027 — Каноническая модель Quote и специализированные контракты
```