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