799 lines
21 KiB
Markdown
799 lines
21 KiB
Markdown
# 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 027–040.
|
||
|
||
---
|
||
|
||
## 16. Статус завершения
|
||
|
||
**Build 026 завершён полностью.**
|
||
|
||
Дополнительных изменений кода в рамках Build 026 не требуется.
|
||
|
||
Следующий этап:
|
||
|
||
```text
|
||
Build 027 — Каноническая модель Quote и специализированные контракты
|
||
``` |