# Build 006 — Dzengi → Instrument Mapper **Статус:** Завершён **Подсистема:** `market_data/acquisition` **Область:** Instrument Reference Data **Тип изменения:** Изолированное расширение новой архитектуры без подключения к production runtime **Результат полного набора тестов:** `106 passed` --- ## 1. Цель Build 006 Цель Build 006 — реализовать преобразование source-specific raw-моделей Dzengi во внутреннюю source-independent модель Dzentra: ```text DzengiExchangeInfoSymbol ↓ Instrument ``` Также реализовано преобразование полного ответа `exchangeInfo`: ```text DzengiExchangeInfoResponse ↓ tuple[Instrument, ...] ``` После завершения Build 006 pipeline Instrument Reference Data выглядит следующим образом: ```text Dzengi raw JSON ↓ Schema Validation ↓ Parser ↓ Dzengi Raw Models ↓ Value Validation ↓ Mapper ↓ Instrument ``` Build 006 не подключает новый mapper к существующему `ExchangeService` и не меняет поведение работающего бота. --- ## 2. Почему Build 006 выполняется именно сейчас До начала Build 006 были завершены необходимые предыдущие этапы: ```text Build 001 — внутренняя модель Instrument Build 002 — транспортные модели Dzengi Build 003 — структурная валидация exchangeInfo Build 004 — parser exchangeInfo Build 005 — value validation ``` К началу Build 006 входные данные mapper уже: - структурно проверены; - преобразованы в типизированные raw-модели; - проверены на допустимость значений; - изолированы от legacy-кода. Поэтому mapper может отвечать только за преобразование модели источника во внутреннюю модель Dzentra. --- ## 3. Архитектурная граница Build 006 Build 006 отвечает только за преобразование: ```text DzengiExchangeInfoSymbol ↓ Instrument ``` и: ```text DzengiExchangeInfoResponse ↓ tuple[Instrument, ...] ``` Mapper не выполняет: - REST-запросы; - schema validation; - parsing JSON; - полную value validation; - нормализацию пользовательского ввода символа; - определение runtime-статуса рынка; - управление кэшем; - создание legacy-модели `ExchangeSymbol`; - интеграцию с `ExchangeService`; - интеграцию с Telegram UI; - интеграцию с автоторговлей. Таким образом, mapper остаётся изолированным слоем между source-specific моделями адаптера Dzengi и source-independent моделью Dzentra. --- ## 4. Изменённые файлы В рамках Build 006 изменены: ```text app/src/market_data/acquisition/exceptions.py app/src/market_data/acquisition/adapters/dzengi/mapper.py ``` Создан тестовый файл: ```text app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py ``` Не изменялись: ```text app/src/market_data/acquisition/models/instrument.py app/src/market_data/acquisition/adapters/dzengi/models.py app/src/market_data/acquisition/adapters/dzengi/parser.py app/src/market_data/acquisition/validation/schema.py app/src/market_data/acquisition/validation/values.py app/src/integrations/exchange/* app/src/telegram/* app/src/trading/* ``` Работающий legacy-код бота не изменён. --- ## 5. Новая ошибка mapper В файл: ```text app/src/market_data/acquisition/exceptions.py ``` добавлена ошибка: ```python class InstrumentReferenceMappingError(MarketDataAcquisitionError): pass ``` Итоговая иерархия ошибок Instrument Reference Data: ```text MarketDataAcquisitionError ├── InstrumentReferenceSchemaError ├── InstrumentReferenceParseError ├── InstrumentReferenceValueError └── InstrumentReferenceMappingError ``` `InstrumentReferenceMappingError` обозначает ошибки, возникшие непосредственно при преобразовании source-specific raw-модели во внутреннюю модель `Instrument`. Примеры: ```text несколько LOT_SIZE filters несколько MIN_NOTIONAL filters невозможность преобразования значения в Decimal неконечное числовое значение ``` --- ## 6. Публичный контракт mapper В файле: ```text app/src/market_data/acquisition/adapters/dzengi/mapper.py ``` реализованы две публичные функции. ### 6.1. Преобразование одного инструмента ```python def map_dzengi_symbol_to_instrument( symbol: DzengiExchangeInfoSymbol, ) -> Instrument: ... ``` Функция преобразует один объект: ```text DzengiExchangeInfoSymbol ``` в один объект: ```text Instrument ``` ### 6.2. Преобразование полного ответа exchangeInfo ```python def map_dzengi_exchange_info_to_instruments( response: DzengiExchangeInfoResponse, ) -> tuple[Instrument, ...]: ... ``` Функция преобразует все элементы: ```text DzengiExchangeInfoResponse.payload.symbols ``` в immutable-последовательность: ```text tuple[Instrument, ...] ``` Порядок инструментов сохраняется. --- ## 7. Правила преобразования полей Реализовано следующее соответствие: ```text DzengiExchangeInfoSymbol.symbol → Instrument.symbol name → name status → status base_asset → base_asset quote_asset → quote_asset asset_type → asset_type market_type → market_type market_modes → market_modes order_types → order_types base_asset_precision → base_asset_precision quote_precision → quote_asset_precision tick_size → tick_size tick_value → tick_value country → country sector → sector industry → industry trading_hours → trading_hours ``` --- ## 8. Преобразование числовых значений в Decimal Следующие значения преобразуются во внутренний тип `Decimal`: ```text tick_size tick_value step_size min_qty max_qty min_notional ``` Используется преобразование: ```python Decimal(str(value)) ``` Это позволяет избежать дополнительной двоичной погрешности при непосредственном преобразовании `float` в `Decimal`. Примеры: ```text 0.01 → Decimal("0.01") "0.001" → Decimal("0.001") 0 → Decimal("0") 0.00000001 → Decimal("1E-8") ``` Если исходное значение отсутствует: ```text None → None ``` --- ## 9. Защита Decimal-преобразования Mapper предполагает, что перед его вызовом уже выполнена: ```text Value Validation ``` Однако публичная функция mapper может быть вызвана напрямую. Поэтому непосредственно в mapper оставлена локальная защита преобразования в `Decimal`. Если значение невозможно преобразовать: ```text "not-a-number" ``` возникает: ```text InstrumentReferenceMappingError ``` Также отклоняются неконечные значения: ```text NaN Infinity -Infinity ``` Это не повторение полной Value Validation. Mapper защищает только собственную непосредственную обязанность — корректное создание `Decimal`. --- ## 10. Извлечение LOT_SIZE Из: ```text DzengiLotSizeFilter ``` извлекаются: ```text min_qty max_qty step_size ``` Пример исходного фильтра: ```python DzengiLotSizeFilter( filter_type="LOT_SIZE", min_qty="0.001", max_qty="1000", step_size="0.001", ) ``` Результат: ```text Instrument.min_qty = Decimal("0.001") Instrument.max_qty = Decimal("1000") Instrument.step_size = Decimal("0.001") ``` Если `LOT_SIZE` отсутствует: ```text min_qty → None max_qty → None step_size → None ``` --- ## 11. Извлечение MIN_NOTIONAL Из: ```text DzengiMinNotionalFilter ``` извлекается: ```text min_notional ``` Пример исходного фильтра: ```python DzengiMinNotionalFilter( filter_type="MIN_NOTIONAL", min_notional="2", ) ``` Результат: ```text Instrument.min_notional = Decimal("2") ``` Если `MIN_NOTIONAL` отсутствует: ```text min_notional → None ``` --- ## 12. Неизвестные filters Неизвестные фильтры представлены raw-моделью: ```text DzengiUnknownFilter ``` Mapper не переносит их во внутреннюю модель `Instrument` и не выбрасывает из-за них ошибку. Например: ```python DzengiUnknownFilter( filter_type="FUTURE_FILTER", fields=( ("enabled", True), ("limit", 10), ), ) ``` не препятствует созданию `Instrument`. Это сознательное архитектурное решение: ```text Raw Models сохраняют неизвестные source-specific данные Instrument содержит только известные канонические поля Dzentra ``` Неизвестные данные не теряются на транспортном уровне, но не загрязняют source-independent модель. --- ## 13. Защита от дублирующихся известных filters Mapper не допускает неоднозначного выбора значения. Если один инструмент содержит несколько фильтров: ```text LOT_SIZE ``` возникает: ```text InstrumentReferenceMappingError ``` Аналогичное правило действует для нескольких фильтров: ```text MIN_NOTIONAL ``` Mapper не выбирает молча первый или последний фильтр, поскольку это могло бы привести к использованию неверных торговых ограничений. --- ## 14. Нормализация необязательных текстовых полей Для следующих optional-полей применяется предметная нормализация: ```text asset_type country sector industry trading_hours ``` Правила: ```text None → None "" → None " " → None " DE " → "DE" ``` Это соответствует фактическому поведению Dzengi API, где некоторые справочные поля могут присутствовать как пустые строки. Например: ```text country="" sector="" industry="" ``` преобразуются во внутреннюю модель как: ```text country=None sector=None industry=None ``` --- ## 15. Обязательные строки не нормализуются mapper Следующие поля переносятся без изменения: ```text symbol name status base_asset quote_asset market_type ``` Причины: - parser сохраняет транспортное значение; - Value Validation уже проверяет обязательность и непустоту; - mapper не должен незаметно менять идентификаторы или статусы источника. --- ## 16. Сохранение порядка последовательностей Mapper сохраняет исходный порядок: ```text market_modes order_types ``` Например: ```text ("REGULAR", "CLOSE_ONLY", "EXTENDED") ``` остаётся: ```text ("REGULAR", "CLOSE_ONLY", "EXTENDED") ``` А: ```text ("MARKET", "LIMIT", "STOP") ``` остаётся: ```text ("MARKET", "LIMIT", "STOP") ``` Mapper не выполняет: ```text sorting deduplication set conversion ``` --- ## 17. Поля Dzengi, которые сознательно не входят в Instrument Следующие source-specific данные сохранены в `DzengiExchangeInfoSymbol`, но не переносятся в текущую модель `Instrument`: ```text quote_asset_id trading_fee exchange_fee long_rate short_rate swap_charge_interval min_sl_gap max_sl_gap min_tp_gap max_tp_gap ``` Они не потеряны на транспортном уровне. В дальнейшем эти данные могут использоваться отдельными предметными контрактами: ```text fees financing execution constraints provider-specific identifiers ``` Build 006 не смешивает эти области с базовой моделью `Instrument`. --- ## 18. Immutable-результат Модель: ```text Instrument ``` объявлена как: ```python @dataclass(frozen=True, slots=True) ``` Тестами подтверждено, что попытка изменения уже созданного объекта приводит к: ```text FrozenInstanceError ``` Это обеспечивает стабильность справочного value object после mapping. --- ## 19. Реализованные тестовые сценарии Создан файл: ```text app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py ``` Реализовано 22 теста. Проверены: 1. полное преобразование `DzengiExchangeInfoSymbol → Instrument`; 2. преобразование полного `DzengiExchangeInfoResponse`; 3. возврат `tuple[Instrument, ...]`; 4. сохранение порядка инструментов; 5. точное преобразование чисел в `Decimal`; 6. извлечение `LOT_SIZE`; 7. извлечение `MIN_NOTIONAL`; 8. работа при отсутствии filters; 9. работа при отсутствии optional numeric values; 10. игнорирование неизвестных filters; 11. преобразование пустого `asset_type` в `None`; 12. преобразование пустого `country` в `None`; 13. преобразование пустого `sector` в `None`; 14. преобразование пустого `industry` в `None`; 15. преобразование пустого `trading_hours` в `None`; 16. очистка внешних пробелов optional text; 17. сохранение порядка `market_modes`; 18. сохранение порядка `order_types`; 19. отклонение нескольких `LOT_SIZE`; 20. отклонение нескольких `MIN_NOTIONAL`; 21. отклонение неконечных числовых значений; 22. immutable-поведение итогового `Instrument`. --- ## 20. Выполненные проверки ### Проверка 1 — unit-тесты mapper Команда: ```bash python -m pytest \ tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py \ -q ``` Результат: ```text 22 passed in 0.03s ``` Статус: ```text PASSED ``` --- ### Проверка 2 — Python compilation Команда: ```bash python -m py_compile \ src/market_data/acquisition/exceptions.py \ src/market_data/acquisition/adapters/dzengi/mapper.py \ tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py ``` Результат: ```text Команда завершилась без ошибок и без вывода. ``` Статус: ```text PASSED ``` --- ### Проверка 3 — полный pipeline на реальном Dzengi sample Проверена цепочка: ```text JSON ↓ Schema Validation ↓ Parser ↓ Value Validation ↓ Mapper ↓ Instrument ``` Результат: ```text Instruments: 51 ``` Первый инструмент: ```text Instrument( symbol='ETH/EUR_LEVERAGE', name='ETH/EUR', status='TRADING', base_asset='ETH', quote_asset='EUR', asset_type='CRYPTOCURRENCY', market_type='LEVERAGE', market_modes=('REGULAR',), order_types=('LIMIT', 'MARKET', 'STOP'), base_asset_precision=3, quote_asset_precision=3, tick_size=Decimal('0.01'), tick_value=Decimal('18.3415'), step_size=Decimal('0.001'), min_qty=Decimal('0.001'), max_qty=Decimal('1000'), min_notional=Decimal('2'), country=None, sector=None, industry=None, trading_hours='UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -;Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -', ) ``` Подтверждено: ```text 51 инструмент успешно прошёл полный pipeline. ``` Статус: ```text PASSED ``` --- ### Проверка 4 — полный набор тестов проекта Команда: ```bash python -m pytest -q ``` Результат: ```text 106 passed in 0.06s ``` Статус: ```text PASSED ``` --- ### Проверка 5 — отсутствие production-интеграции Команда: ```bash grep -RIn \ --exclude-dir="__pycache__" \ --exclude="*.pyc" \ -E "map_dzengi_symbol_to_instrument|map_dzengi_exchange_info_to_instruments|InstrumentReferenceMappingError" \ src tests ``` Подтверждено, что mapper используется только в: ```text src/market_data/acquisition/adapters/dzengi/mapper.py tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py ``` Новая ошибка объявлена в: ```text src/market_data/acquisition/exceptions.py ``` Не обнаружено подключения к: ```text src/integrations/exchange/* src/telegram/* src/trading/* ``` Статус: ```text PASSED ``` --- ## 21. Архитектура после Build 006 После завершения Build 006 реализована следующая часть новой подсистемы: ```text market_data/ └── acquisition/ ├── exceptions.py │ ├── models/ │ └── instrument.py │ └── Instrument │ ├── validation/ │ ├── schema.py │ │ └── validate_exchange_info_schema() │ │ │ └── values.py │ └── validate_exchange_info_values() │ └── adapters/ └── dzengi/ ├── models.py │ ├── DzengiExchangeInfoResponse │ ├── DzengiExchangeInfoPayload │ ├── DzengiExchangeInfoSymbol │ ├── DzengiLotSizeFilter │ ├── DzengiMinNotionalFilter │ └── DzengiUnknownFilter │ ├── parser.py │ └── parse_exchange_info() │ └── mapper.py ├── map_dzengi_symbol_to_instrument() └── map_dzengi_exchange_info_to_instruments() ``` Рабочая цепочка: ```text Raw JSON document ↓ validate_exchange_info_schema() ↓ ValidatedExchangeInfoDocument ↓ parse_exchange_info() ↓ DzengiExchangeInfoResponse ↓ validate_exchange_info_values() ↓ map_dzengi_exchange_info_to_instruments() ↓ tuple[Instrument, ...] ``` --- ## 22. Влияние на legacy-систему Build 006 не подключён к существующим компонентам: ```text ExchangeService ExchangeSymbol SymbolValidationResult Telegram UI AutoTrade Market Stream Market Data Runner Execution Quality ``` Поэтому старый бот продолжает работать по прежнему пути. Новая подсистема строится параллельно и пока не заменяет legacy-реализацию. Это соответствует стратегии миграции: ```text Сначала построить и протестировать новый путь. Затем подключать его постепенно. Старый рабочий путь не удалять до подтверждения новой реализации. ``` --- ## 23. Классификация изменений | Изменение | Классификация | |---|---| | Создание Dzengi mapper | Обязательное архитектурное изменение | | Преобразование raw-модели в `Instrument` | Обязательное архитектурное изменение | | Преобразование чисел в `Decimal` | Обязательное архитектурное изменение | | Извлечение `LOT_SIZE` | Обязательное архитектурное изменение | | Извлечение `MIN_NOTIONAL` | Обязательное архитектурное изменение | | Защита от дублирующихся известных filters | Улучшение надёжности | | Пустые optional text → `None` | Предметная нормализация | | Игнорирование unknown filters в `Instrument` | Разграничение source/domain layers | | Новая `InstrumentReferenceMappingError` | Улучшение диагностируемости | | Изменение production-поведения | Отсутствует | --- ## 24. Итог Build 006 Build 006 завершён успешно. Реализовано: ```text DzengiExchangeInfoSymbol ↓ Instrument ``` и: ```text DzengiExchangeInfoResponse ↓ tuple[Instrument, ...] ``` Подтверждено: ```text 22 mapper tests passed 106 total project tests passed 51 real Dzengi instruments successfully mapped Python compilation passed No production integration detected Legacy bot behavior unchanged ``` Итоговый статус: ```text BUILD 006 — COMPLETE ``` --- ## 25. Следующий этап Следующий этап необходимо определить по утверждённому плану миграции. Новый pipeline уже умеет преобразовывать сохранённый JSON-документ в: ```text tuple[Instrument, ...] ``` Следующий Build должен добавить следующий минимальный слой, не подключая сразу новый путь ко всем legacy-потребителям и не изменяя работающий `ExchangeService` без совместимого переходного контракта.