# Build 029 — Dzengi Mapper и Quotes Handler ## Статус **Завершён** --- ## 1. Цель Build 029 Цель Build 029 — реализовать преобразование специализированной модели REST-котировки Dzengi в каноническую модель `Quote` и создать обработчик полного цикла преобразования сырого REST-документа в проверенную внутреннюю модель котировки. Build является частью поэтапной миграции подсистемы: **Quotes Feed** в новую архитектуру: ```text src/market_data/acquisition/ ``` На данном этапе реализованы: - специализированный Dzengi quote mapper; - преобразование `DzengiTicker24hrResponse` в канонический `Quote`; - преобразование цен в `Decimal`; - преобразование биржевого timestamp в timezone-aware UTC `datetime`; - фиксация времени получения котировки; - специализированный `QuotesHandler`; - полная handler-цепочка обработки сырого REST-документа. Подключение `Quotes Feed`, registry, `Acquisition Service`, `Quote Store`, `ExchangeService` facade и runtime-потребителей в данный Build не входит. --- ## 2. Место Build 029 в плане миграции 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 029 продолжает фундамент, созданный в Builds 027–028. После его завершения сформирована цепочка: ```text raw Dzengi REST document ↓ schema validation ↓ Dzengi quote parser ↓ value validation ↓ DzengiTicker24hrResponse ↓ Dzengi quote mapper ↓ canonical Quote ``` --- ## 3. Исходное состояние перед Build 029 До начала Build 029 уже были реализованы: ### Build 027 ```text src/market_data/acquisition/models/quote.py src/market_data/acquisition/protocol.py ``` Были определены: - каноническая модель `Quote`; - контракт источника сырого quote-документа; - контракт обработчика quote-документа; - контракт готового `Quotes Feed`. ### Build 028 ```text src/market_data/acquisition/adapters/dzengi/models.py src/market_data/acquisition/adapters/dzengi/parser.py src/market_data/acquisition/validation/schema.py src/market_data/acquisition/validation/values.py src/market_data/acquisition/exceptions.py ``` Были реализованы: - модель REST-ответа `/api/v1/ticker/24hr`; - parser quote payload; - schema validation; - value validation; - специализированные ошибки Acquisition layer. Отсутствовал слой, преобразующий проверенную Dzengi-specific модель в канонический `Quote`, а также единая точка оркестрации всей цепочки обработки сырого документа. --- ## 4. Изменённые и добавленные файлы В рамках Build 029 изменены только два исходных файла: ```text src/market_data/acquisition/adapters/dzengi/mapper.py src/market_data/acquisition/handlers/quotes_handler.py ``` Добавлены два специализированных файла тестов: ```text tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py tests/unit/market_data/acquisition/handlers/test_quotes_handler.py ``` Другие файлы в рамках фактически применённого Build 029 не изменялись. --- ## 5. Dzengi Quote Mapper В файле: ```text src/market_data/acquisition/adapters/dzengi/mapper.py ``` реализовано преобразование: ```text DzengiTicker24hrResponse ↓ Quote ``` Mapper является архитектурной границей между: ```text exchange-specific adapter model ``` и: ```text canonical Acquisition model ``` Его ответственность: - принять проверенную модель `DzengiTicker24hrResponse`; - преобразовать биржевые значения цен в канонический тип; - преобразовать биржевой timestamp; - определить источник данных; - зафиксировать время получения котировки; - создать канонический `Quote`. Mapper не должен: - выполнять REST-запрос; - разбирать сырой JSON payload; - выполнять schema validation сырого документа; - управлять store или cache; - обращаться к `ExchangeService`; - содержать UI-логику; - содержать execution-логику. --- ## 6. Преобразование модели Dzengi в канонический Quote Исходная модель адаптера содержит данные, соответствующие REST-ответу Dzengi: ```text symbol lastPrice bidPrice askPrice closeTime ``` После parsing и validation эти данные представлены специализированной моделью: ```text DzengiTicker24hrResponse ``` Mapper преобразует её в: ```text Quote ``` с канонической семантикой: ```text symbol last_price bid_price ask_price source_timestamp received_at source ``` Таким образом, API-specific имена: ```text lastPrice bidPrice askPrice closeTime ``` не выходят за пределы Dzengi adapter layer. --- ## 7. Использование Decimal для цен Цены преобразуются в `Decimal`. Целевая семантика: ```text last_price: Decimal bid_price: Decimal ask_price: Decimal ``` Это решение исключает ненужную потерю точности при преобразовании рыночных цен через бинарный `float`. Архитектурная цепочка: ```text Dzengi string price ↓ Decimal ↓ canonical Quote ``` Например: ```text "64159.45" ↓ Decimal("64159.45") ``` Mapper не должен сначала преобразовывать строку в `float`, а затем создавать `Decimal`, поскольку такой путь способен внести артефакты двоичного представления числа. --- ## 8. Преобразование биржевого timestamp Поле Dzengi: ```text closeTime ``` содержит Unix timestamp в миллисекундах. Mapper преобразует его в timezone-aware UTC `datetime`. Семантика преобразования: ```text closeTime milliseconds ↓ UTC datetime ↓ Quote.source_timestamp ``` Использование timezone-aware значения необходимо для однозначного представления времени рыночного события и последующих операций: - freshness calculation; - sequence validation; - event ordering; - диагностика задержек; - сопоставление данных из нескольких источников. --- ## 9. Время получения котировки Помимо биржевого времени события, канонический `Quote` содержит время фактического получения данных платформой: ```text received_at ``` Разделение двух временных характеристик принципиально: ```text source_timestamp ``` означает время, указанное источником данных; ```text received_at ``` означает время, когда котировка была преобразована во внутреннюю модель платформы. Это создаёт фундамент для последующего определения: - возраста котировки; - сетевой задержки; - freshness; - stale data; - задержки между биржей и локальной системой. --- ## 10. Источник котировки Канонический `Quote` получает идентификатор источника: ```text dzengi ``` Это позволяет внутренней модели не зависеть от конкретного adapter-класса, сохраняя при этом происхождение рыночных данных. Целевая модель допускает дальнейшую работу с несколькими источниками: ```text Dzengi Binance Coinbase другие источники ``` При этом приоритетным источником для торговых решений остаётся биржа исполнения. --- ## 11. Quotes Handler В файле: ```text src/market_data/acquisition/handlers/quotes_handler.py ``` реализован специализированный обработчик quote-документа. Его ответственность — оркестрировать существующие специализированные стадии обработки: ```text raw document ↓ schema validation ↓ parser ↓ value validation ↓ mapper ↓ Quote ``` Handler является единой точкой преобразования: ```text object → Quote ``` Он не должен самостоятельно дублировать внутреннюю реализацию: - schema validation; - parsing; - value validation; - mapping. Вместо этого handler координирует специализированные компоненты. --- ## 12. Полная цепочка обработки После Build 029 полный путь REST-документа выглядит следующим образом: ```text { "askPrice": "64159.55", "bidPrice": "64159.45", "closeTime": 1783887270312, "lastPrice": "64159.45", "symbol": "BTC/USD_LEVERAGE" } ↓ schema validation ↓ Dzengi REST quote parser ↓ DzengiTicker24hrResponse ↓ value validation ↓ Dzengi quote mapper ↓ Quote( symbol=..., last_price=..., bid_price=..., ask_price=..., source_timestamp=..., received_at=..., source=... ) ``` Таким образом, верхние слои платформы больше не обязаны знать формат ответа Dzengi. --- ## 13. Архитектурные решения Build 029 ### 13.1. Mapper изолирует специфику Dzengi Только adapter layer знает о: ```text DzengiTicker24hrResponse lastPrice bidPrice askPrice closeTime ``` После mapping верхние слои работают исключительно с: ```text Quote ``` --- ### 13.2. Handler не зависит от ExchangeService Новый `QuotesHandler` не использует: ```text src.integrations.exchange.service.ExchangeService ``` Направление зависимостей остаётся правильным: ```text external Dzengi payload ↓ Acquisition adapter ↓ Acquisition handler ↓ canonical Quote ``` Обратной зависимости новой подсистемы от legacy integration layer нет. --- ### 13.3. Handler не является Feed `QuotesHandler` отвечает только за преобразование документа: ```text object → Quote ``` Он не отвечает за получение документа от биржи. Получение данных будет ответственностью: ```text src/market_data/acquisition/feeds/quotes_feed.py ``` на следующем этапе миграции. --- ### 13.4. Handler не является Store `QuotesHandler` не сохраняет котировки. Хранение будет реализовано отдельно: ```text Build 032 — Канонический Quote Store ``` Такое разделение предотвращает смешивание: ```text acquisition processing storage ``` --- ### 13.5. Build не изменяет production runtime В Build 029 не изменены: ```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 ``` Не переведены: ```text UI consumers execution consumers strategy consumers diagnostics consumers ``` Работающий бот продолжает использовать прежний runtime-контур. --- ## 14. Что намеренно не реализовано В Build 029 не входят: ```text Quotes Feed регистрация Quotes Feed подключение к Acquisition Service подключение нового REST Quotes Feed к ExchangeService facade Quote Store перенос MarketPriceCache на Quote Store WebSocket quote parsing WebSocket quote adapter перевод market runtime перевод read-only потребителей перевод UI-потребителей перевод execution-потребителей удаление TickerPrice удаление market snapshot dict layer удаление legacy quote parsing удаление MarketPriceCache ``` Каждая из этих задач выполняется только в соответствующем последующем Build. --- ## 15. Проверки Выполнена синтаксическая проверка: ```bash python -m py_compile \ src/market_data/acquisition/adapters/dzengi/mapper.py \ src/market_data/acquisition/handlers/quotes_handler.py \ tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py \ tests/unit/market_data/acquisition/handlers/test_quotes_handler.py ``` Результат: ```text Успешно. ``` Выполнены специализированные тесты Build 029: ```bash python -m pytest \ tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py \ tests/unit/market_data/acquisition/handlers/test_quotes_handler.py \ -q ``` Результат: ```text 12 passed in 0.03s ``` Выполнена полная регрессия проекта: ```bash python -m pytest -q ``` Результат: ```text 457 passed in 0.26s ``` Регрессий не обнаружено. Количество тестов увеличилось: ```text После Build 028: 445 passed После Build 029: 457 passed ``` Добавлено: ```text 12 специализированных тестов ``` --- ## 16. Критерии завершения Build 029 Build 029 считается завершённым, поскольку выполнены все необходимые условия: - [x] реализован специализированный Dzengi quote mapper; - [x] `DzengiTicker24hrResponse` преобразуется в канонический `Quote`; - [x] API-specific имена не выходят за пределы adapter layer; - [x] цены преобразуются в `Decimal`; - [x] не используется промежуточное преобразование цен через `float`; - [x] `closeTime` преобразуется в timezone-aware UTC `datetime`; - [x] фиксируется `received_at`; - [x] сохраняется источник котировки; - [x] реализован специализированный `QuotesHandler`; - [x] handler оркестрирует полный цикл обработки сырого документа; - [x] handler не дублирует ответственность parser; - [x] handler не дублирует ответственность validation; - [x] handler не дублирует ответственность mapper; - [x] новая реализация не зависит от `ExchangeService`; - [x] новая реализация не зависит от `MarketPriceCache`; - [x] production runtime не изменён; - [x] специализированные тесты проходят; - [x] полная регрессия проходит. --- ## 17. Итог В результате Build 029 завершён слой преобразования REST-котировки Dzengi во внутреннюю каноническую модель платформы: ```text Dzengi REST payload ↓ schema validation ↓ parser ↓ value validation ↓ DzengiTicker24hrResponse ↓ mapper ↓ canonical Quote ``` Также создан единый специализированный обработчик: ```text QuotesHandler ``` который предоставляет операцию: ```text raw document → canonical Quote ``` При этом сохранены ключевые архитектурные свойства миграции: - новая реализация развивается параллельно legacy-контуру; - работающий бот не сломан; - `ExchangeService` не изменён; - `MarketPriceCache` не изменён; - runtime-потребители не изменены; - Dzengi-specific формат изолирован внутри adapter layer; - верхние слои получают каноническую модель `Quote`; - mapping и orchestration разделены по ответственности; - сохранена возможность безопасного поэтапного переключения системы. **Build 029 завершён.** Следующий этап: ```text Build 030 — Quotes Feed и регистрация в Acquisition Service ```