build 039: complete Quotes Feed migration foundation

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit 7b62873832
443 changed files with 80452 additions and 1335 deletions

View 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
```