build 039: complete Quotes Feed migration foundation
This commit is contained in:
630
docs/migrations/build_030.md
Normal file
630
docs/migrations/build_030.md
Normal file
@@ -0,0 +1,630 @@
|
||||
# Build 030 — Quotes Feed и регистрация в Acquisition Service
|
||||
|
||||
**Статус:** Завершён
|
||||
**Подсистема:** Market Data Acquisition
|
||||
**Вертикаль:** Quotes Feed
|
||||
**Проект:** Dzentra
|
||||
**Тип изменения:** Архитектурная миграция без изменения поведения legacy runtime
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build
|
||||
|
||||
Цель Build 030 — собрать ранее реализованные компоненты Quotes Feed в завершённую прикладную цепочку получения канонической котировки и зарегистрировать эту цепочку в слое Acquisition Service.
|
||||
|
||||
Build должен обеспечить следующий поток данных:
|
||||
|
||||
```text
|
||||
Dzengi REST /api/v1/ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
На данном этапе новый Quotes Feed существует параллельно с legacy-контуром и ещё не подключается к `ExchangeService`, `MarketPriceCache`, market runtime, UI или Execution.
|
||||
|
||||
---
|
||||
|
||||
## 2. Предпосылки
|
||||
|
||||
К началу Build 030 были завершены предыдущие этапы:
|
||||
|
||||
```text
|
||||
Build 027 — Каноническая модель Quote и специализированные контракты
|
||||
Build 028 — Dzengi REST quote models, parser и validation
|
||||
Build 029 — Dzengi mapper и Quotes Handler
|
||||
```
|
||||
|
||||
В результате уже существовали:
|
||||
|
||||
- каноническая модель `Quote`;
|
||||
- контракт `QuoteDocumentSource`;
|
||||
- контракт `QuoteDocumentHandler`;
|
||||
- контракт `QuoteFeedProtocol`;
|
||||
- транспортная модель ответа Dzengi;
|
||||
- schema validation;
|
||||
- parser;
|
||||
- value validation;
|
||||
- mapper;
|
||||
- `DzengiQuoteDocumentHandler`;
|
||||
- специализированные исключения Quotes Feed.
|
||||
|
||||
Не хватало orchestration-слоя, связывающего эти компоненты в завершённый pipeline.
|
||||
|
||||
---
|
||||
|
||||
## 3. Границы Build
|
||||
|
||||
В Build 030 изменены следующие production-файлы:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/rest.py
|
||||
src/market_data/acquisition/feeds/quotes_feed.py
|
||||
src/market_data/acquisition/registry.py
|
||||
src/market_data/acquisition/service.py
|
||||
```
|
||||
|
||||
Добавлен новый файл тестов:
|
||||
|
||||
```text
|
||||
tests/unit/market_data/acquisition/feeds/test_quotes_feed.py
|
||||
```
|
||||
|
||||
Расширены существующие тесты:
|
||||
|
||||
```text
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py
|
||||
tests/unit/market_data/acquisition/test_registry.py
|
||||
tests/unit/market_data/acquisition/test_service.py
|
||||
```
|
||||
|
||||
Следующие компоненты намеренно не изменялись:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/market_cache.py
|
||||
src/integrations/exchange/market_stream.py
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
Также не изменялись:
|
||||
|
||||
- UI-потребители;
|
||||
- Execution-потребители;
|
||||
- торговые стратегии;
|
||||
- runtime-контур;
|
||||
- Quote Store;
|
||||
- legacy market snapshot dict layer.
|
||||
|
||||
Эти изменения относятся к следующим Build.
|
||||
|
||||
---
|
||||
|
||||
## 4. Реализованная архитектура
|
||||
|
||||
### 4.1. REST source
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/rest.py
|
||||
```
|
||||
|
||||
реализован специализированный источник:
|
||||
|
||||
```python
|
||||
DzengiQuoteDocumentSource
|
||||
```
|
||||
|
||||
Его ответственность ограничена получением сырого транспортного документа текущей котировки.
|
||||
|
||||
Целевая операция:
|
||||
|
||||
```python
|
||||
fetch_quote_document(symbol: str) -> object
|
||||
```
|
||||
|
||||
Источник выполняет запрос:
|
||||
|
||||
```text
|
||||
GET /api/v1/ticker/24hr
|
||||
```
|
||||
|
||||
с параметрами:
|
||||
|
||||
```python
|
||||
{
|
||||
"symbol": symbol,
|
||||
}
|
||||
```
|
||||
|
||||
REST source:
|
||||
|
||||
- принимает торговый символ;
|
||||
- передаёт его REST-клиенту без изменения;
|
||||
- получает декодированный транспортный документ;
|
||||
- возвращает исходный payload;
|
||||
- преобразует транспортные ошибки в специализированную ошибку Quotes Feed.
|
||||
|
||||
REST source не выполняет:
|
||||
|
||||
- schema validation;
|
||||
- parsing;
|
||||
- value validation;
|
||||
- mapping;
|
||||
- кэширование;
|
||||
- retry;
|
||||
- нормализацию торгового символа.
|
||||
|
||||
---
|
||||
|
||||
## 5. Quotes Feed
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/feeds/quotes_feed.py
|
||||
```
|
||||
|
||||
реализован:
|
||||
|
||||
```python
|
||||
QuotesFeed
|
||||
```
|
||||
|
||||
Основная операция:
|
||||
|
||||
```python
|
||||
load_quote(symbol: str) -> Quote
|
||||
```
|
||||
|
||||
Внутренняя последовательность:
|
||||
|
||||
```text
|
||||
symbol
|
||||
↓
|
||||
QuoteDocumentSource.fetch_quote_document(symbol)
|
||||
↓
|
||||
raw document
|
||||
↓
|
||||
QuoteDocumentHandler.handle_quote_document(document)
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
`QuotesFeed` является orchestration-компонентом и не дублирует обязанности других слоёв.
|
||||
|
||||
Он не выполняет:
|
||||
|
||||
- транспортные запросы самостоятельно;
|
||||
- schema validation;
|
||||
- parsing;
|
||||
- value validation;
|
||||
- mapping;
|
||||
- нормализацию символа;
|
||||
- retry;
|
||||
- кэширование;
|
||||
- сохранение в Store;
|
||||
- обращение к `ExchangeService`.
|
||||
|
||||
Ошибки source и handler не переоборачиваются повторно.
|
||||
|
||||
---
|
||||
|
||||
## 6. Quote Feed Registry
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/registry.py
|
||||
```
|
||||
|
||||
добавлен отдельный реестр:
|
||||
|
||||
```python
|
||||
QuoteFeedRegistry
|
||||
```
|
||||
|
||||
Существующий:
|
||||
|
||||
```python
|
||||
InstrumentFeedRegistry
|
||||
```
|
||||
|
||||
сохранён без архитектурного объединения с Quotes Feed.
|
||||
|
||||
Это позволяет:
|
||||
|
||||
- не изменять стабильный Instrument Reference Data contour;
|
||||
- сохранить изоляцию вертикалей Acquisition;
|
||||
- минимизировать область регрессии;
|
||||
- избежать преждевременной универсализации registry.
|
||||
|
||||
`QuoteFeedRegistry` обеспечивает:
|
||||
|
||||
- регистрацию `QuoteFeedProtocol`;
|
||||
- получение зарегистрированного Feed по имени источника;
|
||||
- нормализацию внешних пробелов имени источника;
|
||||
- запрет пустого имени;
|
||||
- запрет повторной регистрации;
|
||||
- runtime-проверку соответствия `QuoteFeedProtocol`;
|
||||
- сохранение identity зарегистрированного объекта.
|
||||
|
||||
Ошибки registry представлены специализированным типом:
|
||||
|
||||
```python
|
||||
QuoteFeedRegistryError
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Quote Acquisition Service
|
||||
|
||||
В файле:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/service.py
|
||||
```
|
||||
|
||||
добавлен отдельный прикладной сервис:
|
||||
|
||||
```python
|
||||
QuoteAcquisitionService
|
||||
```
|
||||
|
||||
Основная операция:
|
||||
|
||||
```python
|
||||
load_quote(
|
||||
source_name: str,
|
||||
symbol: str,
|
||||
) -> Quote
|
||||
```
|
||||
|
||||
Внутренняя последовательность:
|
||||
|
||||
```text
|
||||
source_name
|
||||
↓
|
||||
QuoteFeedRegistry.get(source_name)
|
||||
↓
|
||||
QuoteFeedProtocol
|
||||
↓
|
||||
load_quote(symbol)
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Сервис:
|
||||
|
||||
- выбирает Feed через registry;
|
||||
- передаёт `symbol` выбранному Feed без изменения;
|
||||
- возвращает канонический `Quote`;
|
||||
- не копирует полученную модель;
|
||||
- не выполняет retry;
|
||||
- не перехватывает и не переоборачивает ошибки registry или Feed.
|
||||
|
||||
Существующий:
|
||||
|
||||
```python
|
||||
InstrumentAcquisitionService
|
||||
```
|
||||
|
||||
не изменяет свою ответственность и продолжает обслуживать Instrument Reference Data.
|
||||
|
||||
---
|
||||
|
||||
## 8. Dependency Injection
|
||||
|
||||
В Build 030 сохранён уже применяемый в Instrument Reference Data подход явной сборки зависимостей.
|
||||
|
||||
Пример архитектурной сборки:
|
||||
|
||||
```python
|
||||
source = DzengiQuoteDocumentSource(...)
|
||||
handler = DzengiQuoteDocumentHandler(...)
|
||||
feed = QuotesFeed(
|
||||
source=source,
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
registry = QuoteFeedRegistry()
|
||||
registry.register("dzengi", feed)
|
||||
|
||||
service = QuoteAcquisitionService(
|
||||
registry=registry,
|
||||
)
|
||||
```
|
||||
|
||||
В Build намеренно не добавлены:
|
||||
|
||||
- глобальный singleton registry;
|
||||
- автоматическая регистрация при импорте;
|
||||
- скрытая сборка production pipeline внутри `QuoteAcquisitionService`;
|
||||
- глобальное mutable-состояние для Feed.
|
||||
|
||||
Такое решение сохраняет:
|
||||
|
||||
- dependency injection;
|
||||
- тестируемость;
|
||||
- явные зависимости;
|
||||
- изоляцию composition root от application service.
|
||||
|
||||
Фактическое подключение production pipeline к legacy facade отложено до Build 031.
|
||||
|
||||
---
|
||||
|
||||
## 9. Ответственности компонентов
|
||||
|
||||
| Компонент | Ответственность |
|
||||
|---|---|
|
||||
| `DzengiQuoteDocumentSource` | Получение сырого REST-документа котировки |
|
||||
| `DzengiQuoteDocumentHandler` | Полная обработка документа до канонической модели |
|
||||
| `QuotesFeed` | Оркестрация source → handler |
|
||||
| `QuoteFeedRegistry` | Регистрация и выбор Quotes Feed |
|
||||
| `QuoteAcquisitionService` | Прикладная точка получения `Quote` через выбранный Feed |
|
||||
| `Quote` | Каноническое внутреннее представление текущей котировки |
|
||||
|
||||
---
|
||||
|
||||
## 10. Полная цепочка обработки
|
||||
|
||||
После завершения Build 030 REST Quotes Feed имеет следующую структуру:
|
||||
|
||||
```text
|
||||
GET /api/v1/ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
raw object
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
validate_dzengi_quote_schema()
|
||||
↓
|
||||
parse_dzengi_quote_document()
|
||||
↓
|
||||
DzengiQuotePayload
|
||||
↓
|
||||
validate_dzengi_quote_values()
|
||||
↓
|
||||
map_dzengi_quote()
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
```
|
||||
|
||||
Таким образом, транспортный формат Dzengi полностью изолирован от внешних потребителей Acquisition.
|
||||
|
||||
---
|
||||
|
||||
## 11. Архитектурные ограничения
|
||||
|
||||
Build 030 намеренно не реализует следующие функции:
|
||||
|
||||
```text
|
||||
ExchangeService facade integration
|
||||
Quote Store
|
||||
MarketPriceCache migration
|
||||
WebSocket quote parsing
|
||||
market runtime migration
|
||||
read-only consumer migration
|
||||
UI consumer migration
|
||||
Execution consumer migration
|
||||
legacy TickerPrice removal
|
||||
legacy market snapshot dict removal
|
||||
MarketPriceCache removal
|
||||
```
|
||||
|
||||
Они относятся к следующим этапам:
|
||||
|
||||
```text
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Тестовое покрытие
|
||||
|
||||
Build 030 покрывает следующие сценарии.
|
||||
|
||||
### 12.1. REST source
|
||||
|
||||
Проверяется:
|
||||
|
||||
- использование endpoint `/api/v1/ticker/24hr`;
|
||||
- передача `symbol` в query parameters;
|
||||
- возврат исходного payload;
|
||||
- однократный вызов REST-клиента;
|
||||
- поддержка dependency injection REST-клиента;
|
||||
- создание стандартного REST-клиента при отсутствии injected client;
|
||||
- преобразование транспортной ошибки в `QuoteTransportError`;
|
||||
- сохранение исходной ошибки через `__cause__`.
|
||||
|
||||
### 12.2. Quotes Feed
|
||||
|
||||
Проверяется:
|
||||
|
||||
- соответствие `QuoteFeedProtocol`;
|
||||
- однократный вызов source;
|
||||
- передача `symbol` без изменения;
|
||||
- однократный вызов handler;
|
||||
- передача исходного документа handler без изменения;
|
||||
- возврат `Quote` без копирования;
|
||||
- отсутствие retry;
|
||||
- отсутствие повторного переоборачивания ошибок.
|
||||
|
||||
### 12.3. Quote Feed Registry
|
||||
|
||||
Проверяется:
|
||||
|
||||
- регистрация корректного Feed;
|
||||
- получение Feed по имени;
|
||||
- нормализация внешних пробелов имени;
|
||||
- запрет пустого имени;
|
||||
- запрет повторной регистрации;
|
||||
- проверка соответствия `QuoteFeedProtocol`;
|
||||
- сохранение identity объекта;
|
||||
- специализированные ошибки registry.
|
||||
|
||||
### 12.4. Quote Acquisition Service
|
||||
|
||||
Проверяется:
|
||||
|
||||
- передача `source_name` registry;
|
||||
- передача `symbol` Feed без изменения;
|
||||
- однократное обращение к registry;
|
||||
- однократный вызов Feed;
|
||||
- возврат `Quote` без копирования;
|
||||
- сохранение ошибок registry;
|
||||
- сохранение ошибок Feed;
|
||||
- отсутствие retry.
|
||||
|
||||
---
|
||||
|
||||
## 13. Проверка компиляции
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/market_data/acquisition/adapters/dzengi/rest.py \
|
||||
src/market_data/acquisition/feeds/quotes_feed.py \
|
||||
src/market_data/acquisition/registry.py \
|
||||
src/market_data/acquisition/service.py \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \
|
||||
tests/unit/market_data/acquisition/feeds/test_quotes_feed.py \
|
||||
tests/unit/market_data/acquisition/test_registry.py \
|
||||
tests/unit/market_data/acquisition/test_service.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
Успешно.
|
||||
Ошибок компиляции нет.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Специализированные тесты
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \
|
||||
tests/unit/market_data/acquisition/feeds/test_quotes_feed.py \
|
||||
tests/unit/market_data/acquisition/test_registry.py \
|
||||
tests/unit/market_data/acquisition/test_service.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
89 passed in 0.06s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Полная регрессия
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
498 passed in 0.25s
|
||||
```
|
||||
|
||||
Регрессий не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## 16. Результат Build
|
||||
|
||||
Build 030 завершён полностью.
|
||||
|
||||
Создана завершённая и протестированная вертикаль REST Quotes Feed:
|
||||
|
||||
```text
|
||||
Dzengi REST API
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Новая вертикаль пока работает независимо от legacy runtime, что обеспечивает безопасную поэтапную миграцию без изменения поведения работающего торгового бота.
|
||||
|
||||
---
|
||||
|
||||
## 17. Следующий этап
|
||||
|
||||
Следующий этап утверждённого плана:
|
||||
|
||||
```text
|
||||
Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
|
||||
```
|
||||
|
||||
Его цель — переключить REST-получение текущей котировки внутри существующего `ExchangeService` на новый канонический Quotes Feed, сохранив текущие публичные интерфейсы и поведение legacy-потребителей.
|
||||
|
||||
Целевая переходная схема:
|
||||
|
||||
```text
|
||||
Legacy consumer
|
||||
↓
|
||||
ExchangeService facade
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
Dzengi /api/v1/ticker/24hr
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
legacy-compatible projection
|
||||
↓
|
||||
Legacy consumer
|
||||
```
|
||||
|
||||
До завершения последующих этапов `ExchangeService` остаётся совместимым фасадом между новой архитектурой Market Data Acquisition и существующими потребителями работающего бота.
|
||||
Reference in New Issue
Block a user