700 lines
18 KiB
Markdown
700 lines
18 KiB
Markdown
# 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
|
||
``` |