feat: add market data architecture and complete migration through build 039
This commit is contained in:
700
docs/migrations/build_027.md
Normal file
700
docs/migrations/build_027.md
Normal file
@@ -0,0 +1,700 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user