630 lines
17 KiB
Markdown
630 lines
17 KiB
Markdown
# 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 и существующими потребителями работающего бота. |