build 039: complete Quotes Feed migration foundation

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit 7b62873832
443 changed files with 80452 additions and 1335 deletions

View File

@@ -0,0 +1,799 @@
# 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 и специализированные контракты
```