Files
dzentra_bot/docs/migrations/build_027.md

700 lines
18 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 027 — Каноническая модель Quote и специализированные контракты
## Статус
**Завершён**
---
## Цель Build
Создать каноническую внутреннюю модель текущей рыночной котировки `Quote` и специализированные контракты подсистемы `Quotes Feed`.
Build должен сформировать независимую от конкретной биржи модель рыночной котировки и определить архитектурные границы между:
- источником сырого документа котировки;
- обработчиком документа;
- готовым потоком котировок;
- потребителями `Market Data Acquisition`.
При этом существующий legacy-контур получения и использования цен не должен изменяться.
---
## Место в плане миграции
Build 027 является вторым этапом миграции подсистемы `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
```
---
## Архитектурная граница Build
Build 027 ограничен двумя задачами:
1. создание канонической модели `Quote`;
2. создание специализированных контрактов `Quotes Feed`.
В рамках Build не реализуются:
- REST-запрос котировки Dzengi;
- модели REST-ответа Dzengi;
- parsing ответа `ticker/24hr`;
- schema validation ответа Dzengi;
- value validation полей котировки;
- mapping модели Dzengi в `Quote`;
- `QuotesHandler`;
- `QuotesFeed`;
- регистрация потока в `Acquisition Service`;
- хранение котировок;
- WebSocket parsing;
- изменение `ExchangeService`;
- изменение `MarketPriceCache`;
- изменение market runtime;
- перевод UI-потребителей;
- перевод execution-потребителей.
Эти изменения относятся к следующим Build.
---
## Изменённые файлы
```text
src/market_data/acquisition/
├── models/
│ └── quote.py
└── protocol.py
```
Всего изменено:
- **2 файла**.
---
## 1. Каноническая модель Quote
Файл:
```text
src/market_data/acquisition/models/quote.py
```
Создана независимая от конкретной биржи immutable-модель:
```python
@dataclass(frozen=True, slots=True)
class Quote:
symbol: str
last_price: Decimal
bid_price: Decimal
ask_price: Decimal
exchange_timestamp: datetime | None
received_at: datetime
source: str
```
### Назначение модели
`Quote` представляет текущий рыночный факт о котировке одного инструмента.
Модель является внутренней моделью слоя:
```text
Market Data
Market Data Acquisition
Quotes Feed
Quote
```
Она не зависит от:
- API Dzengi;
- формата `ticker/24hr`;
- WebSocket-сообщений;
- legacy-моделей `integrations/exchange`;
- UI;
- Execution;
- конкретного способа хранения данных.
---
## 2. Поля модели Quote
### `symbol`
```python
symbol: str
```
Каноническое обозначение инструмента.
Пример:
```text
BTC/USD
```
Поле не должно содержать транспортное или биржевое представление, специфичное для конкретного API, если оно отличается от канонического обозначения Dzentra.
---
### `last_price`
```python
last_price: Decimal
```
Последняя известная цена инструмента, полученная от источника.
---
### `bid_price`
```python
bid_price: Decimal
```
Лучшая доступная цена покупки.
---
### `ask_price`
```python
ask_price: Decimal
```
Лучшая доступная цена продажи.
---
### `exchange_timestamp`
```python
exchange_timestamp: datetime | None
```
Время рыночного события на стороне источника данных.
Поле является optional, поскольку конкретный источник или endpoint может не предоставлять достоверный timestamp события.
---
### `received_at`
```python
received_at: datetime
```
Время получения рыночных данных системой Dzentra.
Это позволяет независимо от наличия `exchange_timestamp` фиксировать момент поступления данных в систему.
---
### `source`
```python
source: str
```
Идентификатор источника рыночных данных.
Пример:
```text
dzengi
```
Модель не фиксирует конкретный набор допустимых источников на уровне класса `Quote`.
---
## 3. Использование Decimal
Для канонических цен используется:
```python
Decimal
```
а не:
```python
float
```
Это позволяет избежать привязки новой внутренней модели к ограничениям legacy-кода и уменьшает риск потери точности при работе с денежными значениями.
Legacy-потребители при необходимости смогут получать преобразованное значение `float` через compatibility/facade-слой на следующих этапах миграции.
---
## 4. Immutable-модель
Модель объявлена как:
```python
@dataclass(frozen=True, slots=True)
```
Это означает:
- экземпляр `Quote` не изменяется после создания;
- исключается случайная мутация рыночного факта;
- модель имеет компактное представление через `slots`;
- объект подходит для передачи между слоями системы как immutable value object.
Такой подход соответствует уже принятому направлению построения канонических моделей `Market Data Acquisition`.
---
## 5. Что сознательно не включено в Quote
В каноническую модель не включены поля:
```text
is_fresh
age_seconds
freshness_status
spread_percent
runtime_key
received_monotonic
```
Причина: эти значения не являются исходным фактом котировки.
Они относятся к другим обязанностям системы.
### Freshness
```text
is_fresh
age_seconds
freshness_status
```
Это runtime-оценка актуальности данных.
Она должна вычисляться на основании времени получения или хранения котировки, а не быть частью исходного объекта `Quote`.
### Spread
```text
spread_percent
```
Это производное значение:
```text
ask_price - bid_price
```
или его процентное представление.
Оно может быть вычислено отдельным processing/runtime-компонентом.
### Runtime identity
```text
runtime_key
```
Это идентификатор runtime-контекста, а не свойство рыночной котировки.
### Monotonic clock
```text
received_monotonic
```
Это внутренний технический механизм runtime/storage-слоя для измерения возраста данных.
Он не должен загрязнять каноническую модель рыночного факта.
---
## 6. Специализированные контракты Quotes Feed
В файл:
```text
src/market_data/acquisition/protocol.py
```
добавлены три специализированных контракта:
```text
QuoteDocumentSource
QuoteDocumentHandler
QuoteFeedProtocol
```
Архитектурная цепочка:
```text
QuoteDocumentSource
сырой документ
QuoteDocumentHandler
Quote
QuoteFeedProtocol
Acquisition Service
```
---
## 7. QuoteDocumentSource
Контракт:
```python
@runtime_checkable
class QuoteDocumentSource(Protocol):
def fetch_quote_document(
self,
symbol: str,
) -> object:
"""
Получить декодированный транспортный документ текущей котировки.
Источник не выполняет schema validation, parsing, value validation
или mapping во внутреннюю модель Quote.
"""
...
```
### Ответственность
`QuoteDocumentSource` отвечает только за получение сырого декодированного транспортного документа.
Он не должен:
- проверять схему;
- проверять значения;
- выполнять mapping;
- создавать `Quote`;
- хранить котировку;
- вычислять freshness;
- обслуживать UI или Execution.
Для Dzengi конкретная реализация будет создана на следующих этапах.
---
## 8. QuoteDocumentHandler
Контракт:
```python
@runtime_checkable
class QuoteDocumentHandler(Protocol):
def handle_quote_document(
self,
document: object,
) -> Quote:
"""
Преобразовать сырой документ в проверенную внутреннюю модель Quote.
"""
...
```
### Ответственность
`QuoteDocumentHandler` определяет границу между сырым внешним документом и проверенной канонической моделью `Quote`.
Конкретная реализация должна организовать последовательность:
```text
сырой документ
schema validation
parser
value validation
mapper
Quote
```
Сам контракт не зависит от конкретной биржи.
---
## 9. QuoteFeedProtocol
Контракт:
```python
@runtime_checkable
class QuoteFeedProtocol(Protocol):
def load_quote(
self,
symbol: str,
) -> Quote:
"""
Получить внутреннюю модель текущей котировки инструмента.
"""
...
```
### Ответственность
`QuoteFeedProtocol` представляет готовый поток получения канонической текущей котировки для `Acquisition Service`.
Потребитель этого контракта не должен знать:
- какая биржа является источником;
- используется REST или другой транспорт;
- как устроен внешний payload;
- как выполняется parsing;
- как выполняется validation;
- как выполняется mapping.
Для потребителя существует только операция:
```text
symbol → Quote
```
---
## 10. Соответствие паттерну Instrument Reference Data
Build 027 продолжает архитектурный подход, уже реализованный для `Instrument Reference Data`.
### Instrument Reference Data
```text
InstrumentDocumentSource
InstrumentDocumentHandler
InstrumentFeedProtocol
Instrument
```
### Quotes Feed
```text
QuoteDocumentSource
QuoteDocumentHandler
QuoteFeedProtocol
Quote
```
Таким образом, новая вертикаль `Quotes Feed` строится в соответствии с уже принятой архитектурой `Market Data Acquisition`, без создания альтернативного или параллельного архитектурного подхода.
---
## 11. Legacy-контур
Build 027 не изменяет существующие legacy-компоненты:
```text
src/integrations/exchange/models.py
src/integrations/exchange/service.py
src/integrations/exchange/market_cache.py
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
src/integrations/exchange/ws_client.py
```
Продолжают работать без изменений:
```text
TickerPrice
ExecutionPriceSnapshot
MarketPriceSnapshot
MarketPriceCache
ExchangeService.get_price()
ExchangeService.get_market_snapshot()
ExchangeService.get_execution_snapshot()
ExchangeService.get_fresh_market_snapshot()
```
На данном этапе новая модель `Quote` существует параллельно legacy-контуру и ещё не используется работающим ботом.
Это соответствует утверждённой стратегии безопасной миграции:
```text
создать новый контур
проверить новый контур
подключить его под legacy facade
поэтапно перевести потребителей
удалить legacy только после полного переключения
```
---
## 12. Обратная совместимость
Build 027 полностью обратно совместим с существующим ботом.
Не изменены:
- публичные методы `ExchangeService`;
- форматы legacy snapshot;
- `MarketPriceCache`;
- market runtime;
- Telegram UI;
- trading strategies;
- Execution;
- существующие модели интеграционного слоя.
Новая модель и контракты пока не участвуют в runtime работающего приложения.
---
## 13. Проверка синтаксиса
Выполнена команда:
```bash
python -m py_compile \
src/market_data/acquisition/models/quote.py \
src/market_data/acquisition/protocol.py
```
Результат:
```text
Успешно.
Ошибок синтаксиса и импортов не обнаружено.
```
---
## 14. Полная регрессия
Выполнена команда:
```bash
python -m pytest -q
```
Результат:
```text
423 passed in 0.24s
```
Все существующие тесты проекта проходят.
Регрессий не обнаружено.
---
## 15. Критерии завершения
Build 027 считается завершённым, поскольку выполнены все его критерии:
- [x] создана каноническая модель `Quote`;
- [x] модель не зависит от Dzengi;
- [x] цены представлены через `Decimal`;
- [x] модель immutable;
- [x] разделены `exchange_timestamp` и `received_at`;
- [x] runtime-поля не включены в каноническую модель;
- [x] создан `QuoteDocumentSource`;
- [x] создан `QuoteDocumentHandler`;
- [x] создан `QuoteFeedProtocol`;
- [x] сохранён архитектурный паттерн существующей вертикали `Instrument`;
- [x] legacy-контур не изменён;
- [x] синтаксическая проверка проходит;
- [x] полная регрессия проходит;
- [x] `423` теста проходят успешно.
---
## Итог
В рамках Build 027 создан фундамент канонической вертикали `Quotes Feed`.
Теперь архитектура содержит независимое представление текущей рыночной котировки:
```text
Quote
```
и три специализированных контракта:
```text
QuoteDocumentSource
QuoteDocumentHandler
QuoteFeedProtocol
```
Целевая архитектурная цепочка сформирована как:
```text
Внешний источник
QuoteDocumentSource
сырой транспортный документ
QuoteDocumentHandler
schema validation
parser
value validation
mapper
Quote
QuoteFeedProtocol
Acquisition Service
```
Build 027 завершён без изменения поведения работающего бота и без преждевременного вмешательства в legacy-контур.
Следующий этап:
```text
Build 028 — Dzengi REST quote models, parser и validation
```