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,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 и существующими потребителями работающего бота.