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