# Dzentra — Instrument Reference Data Migration — Build 014 > Статус: Завершён ## Название **Compatibility mapper Instrument → ExchangeSymbol** --- ## Цель Создать временный compatibility-слой, преобразующий новую внутреннюю модель: ```text Instrument ``` в существующую legacy-модель: ```text ExchangeSymbol ``` Целевая цепочка: ```text new acquisition pipeline ↓ tuple[Instrument, ...] ↓ compatibility mapper ↓ list[ExchangeSymbol] ``` Compatibility mapper необходим для последующего переключения: ```text ExchangeService.get_exchange_symbols() ``` на новую Instrument Reference Data pipeline без изменения существующего публичного контракта: ```python def get_exchange_symbols(self) -> list[ExchangeSymbol]: ... ``` Build 014 создаёт только compatibility-границу. Переключение `ExchangeService.get_exchange_symbols()` в рамках этого Build не выполняется. --- ## Причина создания compatibility-слоя К началу Build 014 завершены: ```text Build 001–012 ↓ создана новая независимая Instrument Reference Data acquisition pipeline Build 013 ↓ доказана эквивалентность legacy- и новой реализации на одном и том же реальном exchangeInfo sample ``` Новая pipeline возвращает: ```python tuple[Instrument, ...] ``` Существующий legacy-контракт возвращает: ```python list[ExchangeSymbol] ``` Существующие production-потребители продолжают ожидать: ```text ExchangeSymbol ``` Поэтому прямое переключение невозможно без временного преобразования: ```text Instrument ↓ ExchangeSymbol ``` --- ## Реализованные файлы Создан production-файл: ```text app/src/market_data/acquisition/compatibility.py ``` Создан unit-тест: ```text app/tests/unit/market_data/acquisition/test_compatibility.py ``` Другие файлы в рамках Build 014 не изменялись. --- ## Размещение compatibility mapper Compatibility mapper размещён в: ```text app/src/market_data/acquisition/compatibility.py ``` Он намеренно не размещён в: ```text app/src/market_data/acquisition/adapters/dzengi/ ``` поскольку преобразование: ```text Instrument → ExchangeSymbol ``` не зависит от формата Dzengi. Он также не размещён в: ```text app/src/integrations/exchange/ ``` поскольку новый миграционный код не должен расширять legacy-подсистему. Архитектурная граница имеет следующий вид: ```text new acquisition model ↓ compatibility.py ↓ legacy integration model ``` Compatibility mapper является временным слоем и должен быть удалён после полного перевода production-потребителей: ```text Build 024 — Удаление compatibility-слоя ``` --- ## Реализованные функции Созданы две функции: ```python def map_instrument_to_exchange_symbol( instrument: Instrument, ) -> ExchangeSymbol: ... ``` и: ```python def map_instruments_to_exchange_symbols( instruments: tuple[Instrument, ...], ) -> list[ExchangeSymbol]: ... ``` Первая функция преобразует один объект: ```text Instrument ↓ ExchangeSymbol ``` Вторая преобразует полный immutable-набор: ```text tuple[Instrument, ...] ↓ list[ExchangeSymbol] ``` --- ## Архитектурные зависимости Compatibility mapper сознательно зависит от обеих моделей: ```python from src.integrations.exchange.models import ExchangeSymbol from src.market_data.acquisition.models.instrument import Instrument ``` Это допустимая временная зависимость: ```text новая acquisition-подсистема ↓ compatibility boundary ↓ legacy contract ``` Никакие другие компоненты новой acquisition pipeline не изменялись для добавления зависимости от `ExchangeSymbol`. Re-export через: ```text app/src/market_data/acquisition/__init__.py ``` не добавлялся. --- ## Соответствие полей Compatibility mapper переносит все 11 полей, общих для `Instrument` и `ExchangeSymbol`: ```text Instrument.symbol → ExchangeSymbol.symbol Instrument.name → ExchangeSymbol.name Instrument.status → ExchangeSymbol.status Instrument.base_asset → ExchangeSymbol.base_asset Instrument.quote_asset → ExchangeSymbol.quote_asset Instrument.market_modes → ExchangeSymbol.market_modes Instrument.market_type → ExchangeSymbol.market_type Instrument.tick_size → ExchangeSymbol.tick_size Instrument.step_size → ExchangeSymbol.step_size Instrument.min_qty → ExchangeSymbol.min_qty Instrument.min_notional → ExchangeSymbol.min_notional ``` Никакая повторная предметная интерпретация данных в compatibility mapper не выполняется. --- ## Поля новой модели, не представленные в legacy-модели Модель `Instrument` содержит дополнительные поля: ```text asset_type order_types base_asset_precision quote_asset_precision tick_value max_qty country sector industry trading_hours ``` Эти поля отсутствуют в legacy-модели: ```text ExchangeSymbol ``` Поэтому они намеренно не переносятся через compatibility boundary. Это ожидаемая потеря расширенной информации: ```text полная новая модель Instrument ↓ ограниченный legacy-контракт ExchangeSymbol ``` Compatibility mapper не: ```text расширяет ExchangeSymbol; создаёт дополнительные атрибуты; переносит данные в несоответствующие поля; изменяет модель Instrument. ``` --- ## Преобразование Decimal → float Новая модель `Instrument` использует: ```python Decimal | None ``` Legacy-модель `ExchangeSymbol` использует: ```python float | None ``` Преобразованию подлежат: ```text tick_size step_size min_qty min_notional ``` Правило преобразования: ```text None ↓ None ``` и: ```text Decimal("0.0001") ↓ 0.0001 ``` Для этого реализован внутренний helper: ```python def _decimal_to_float( value: Decimal | None, ) -> float | None: if value is None: return None return float(value) ``` Переход к `float` выполняется только на compatibility-границе. Внутри новой Instrument Reference Data pipeline точное десятичное представление через `Decimal` сохраняется. --- ## Преобразование market_modes Новая модель использует: ```python tuple[str, ...] ``` Legacy-модель использует: ```python list[str] ``` Compatibility mapper выполняет: ```python list(instrument.market_modes) ``` Например: ```text Instrument: ("REGULAR", "CLOSE_ONLY") ↓ ExchangeSymbol: ["REGULAR", "CLOSE_ONLY"] ``` Порядок значений сохраняется. Для каждого результата создаётся новый независимый список. Изменение: ```python exchange_symbol.market_modes.append("ADDED_IN_LEGACY") ``` не изменяет: ```python instrument.market_modes ``` и не влияет на другие объекты `ExchangeSymbol`, созданные из того же `Instrument`. --- ## Преобразование полного набора инструментов Функция: ```python map_instruments_to_exchange_symbols() ``` принимает: ```python tuple[Instrument, ...] ``` и возвращает: ```python list[ExchangeSymbol] ``` Порядок инструментов сохраняется. Например: ```text ( BTC/USD_LEVERAGE, ETH/USD_LEVERAGE, XRP/USD_LEVERAGE, ) ``` преобразуется в: ```text [ BTC/USD_LEVERAGE, ETH/USD_LEVERAGE, XRP/USD_LEVERAGE, ] ``` Для пустого входного набора: ```python () ``` возвращается: ```python [] ``` --- ## Обработка ошибок Новый тип исключения в рамках Build 014 не создавался. Compatibility mapper получает уже построенную и проверенную модель: ```text Instrument ``` и выполняет только: ```text чтение полей; Decimal → float; tuple → list; создание ExchangeSymbol. ``` Существующая ошибка: ```text InstrumentReferenceMappingError ``` не переиспользуется, поскольку она относится к другому направлению преобразования: ```text Dzengi raw model ↓ Instrument ``` Создание отдельной категории ошибки без конкретного реального сценария не требуется. --- ## Unit-тесты Создан файл: ```text app/tests/unit/market_data/acquisition/test_compatibility.py ``` Реализовано 12 тестов. Проверяются: 1. преобразование одного `Instrument` в `ExchangeSymbol`; 2. перенос всех 11 общих legacy-полей; 3. преобразование `Decimal → float`; 4. сохранение `None` для отсутствующих числовых значений; 5. преобразование `tuple[str, ...] → list[str]` для `market_modes`; 6. сохранение порядка `market_modes`; 7. создание независимого списка `market_modes`; 8. преобразование нескольких инструментов; 9. сохранение порядка инструментов; 10. пустой `tuple` преобразуется в пустой `list`; 11. исходный `Instrument` не изменяется; 12. результат compatibility mapper эквивалентен исходным `Instrument` по comparator Build 013. --- ## Round-trip проверка через comparator Build 013 Для дополнительной проверки используется уже созданный в Build 013 comparator: ```text tuple[Instrument, ...] ↓ map_instruments_to_exchange_symbols() ↓ list[ExchangeSymbol] ↓ compare_instrument_reference_data() ↓ InstrumentReferenceEquivalenceReport ``` Основная проверка: ```python legacy_symbols = map_instruments_to_exchange_symbols( instruments ) report = compare_instrument_reference_data( legacy_symbols, instruments, ) assert report.is_equivalent, report.format() ``` Это подтверждает, что compatibility mapper воспроизводит все 11 общих полей legacy-контракта. Comparator остаётся только в тестовом контуре. Production-код от comparator не зависит. --- ## Результат unit-тестов Выполнена команда: ```bash python -m pytest \ tests/unit/market_data/acquisition/test_compatibility.py \ -q ``` Результат: ```text 12 passed in 0.02s ``` Все тесты compatibility mapper успешно пройдены. --- ## Проверка синтаксиса Выполнена команда: ```bash python -m py_compile \ src/market_data/acquisition/compatibility.py \ tests/unit/market_data/acquisition/test_compatibility.py ``` Результат: ```text успешно ``` Ошибок синтаксиса не обнаружено. --- ## Полный регрессионный прогон Выполнена команда: ```bash python -m pytest -q ``` Результат: ```text 204 passed in 0.19s ``` До Build 014 полный набор содержал: ```text 192 passed ``` В Build 014 добавлено: ```text 12 новых тестов ``` Итого: ```text 192 + 12 = 204 ``` Все предыдущие тесты продолжают проходить. Регрессий существующего проекта не обнаружено. --- ## Проверка отсутствия преждевременного production-использования Выполнена команда: ```bash grep -RIn \ --exclude-dir="__pycache__" \ --exclude="*.pyc" \ -E "map_instrument_to_exchange_symbol|map_instruments_to_exchange_symbols" \ src tests ``` Результат подтвердил, что функции compatibility mapper используются только в: ```text src/market_data/acquisition/compatibility.py tests/unit/market_data/acquisition/test_compatibility.py ``` Других production-потребителей нет. В частности: ```text ExchangeService.get_exchange_symbols() ``` ещё не использует compatibility mapper. Это подтверждает отсутствие преждевременного переключения production runtime. --- ## Изменения production-кода В рамках Build 014 создан только один новый production-файл: ```text app/src/market_data/acquisition/compatibility.py ``` Не изменялись: ```text app/src/integrations/exchange/service.py app/src/integrations/exchange/models.py app/src/market_data/acquisition/service.py app/src/market_data/acquisition/registry.py app/src/market_data/acquisition/protocol.py app/src/market_data/acquisition/exceptions.py app/src/market_data/acquisition/models/instrument.py app/src/market_data/acquisition/adapters/dzengi/ app/src/market_data/acquisition/handlers/instrument_handler.py app/src/market_data/acquisition/feeds/instrument_feed.py app/src/telegram/ app/src/trading/ ``` --- ## Обратная совместимость Полностью сохранены: ```text ExchangeService.get_exchange_symbols() ExchangeService.validate_symbol() ExchangeService.get_symbol_runtime_status() ``` Также не изменены: ```text сигнатуры существующих production-методов; legacy imports; формат runtime-ошибок; Telegram UI; автоторговля; существующее runtime-поведение; exchange symbols cache. ``` --- ## Что Build 014 намеренно не делает Build 014 не выполняет: ```text переключение ExchangeService.get_exchange_symbols(); подключение InstrumentAcquisitionService к ExchangeService; создание production composition; изменение exchange symbols cache; изменение validate_symbol(); изменение get_symbol_runtime_status(); изменение normalize_symbol(); изменение symbol_candidates(); изменение legacy parser; удаление legacy parser; изменение ExchangeSymbol; изменение Instrument; изменение Telegram UI; изменение AutoTrade. ``` Переключение: ```text ExchangeService.get_exchange_symbols() ``` относится строго к следующему этапу: ```text Build 015 — Переключение get_exchange_symbols() ``` --- ## Классификация изменений | Изменение | Классификация | |---|---| | Compatibility mapper `Instrument → ExchangeSymbol` | Обязательное архитектурное изменение | | `Decimal → float` на legacy-границе | Обратная совместимость | | `tuple → list` для `market_modes` | Обратная совместимость | | Batch mapper | Обязательное архитектурное изменение | | Unit-тесты | Улучшение надёжности | | Round-trip проверка через Build 013 comparator | Миграционная проверка | | Новый exception | Не создавался | | Изменение legacy runtime | Отсутствует | | Изменение поведения | Отсутствует | --- ## Итоговые проверки | Проверка | Результат | |---|---| | Unit-тесты Compatibility Mapper | `12 passed in 0.02s` | | `py_compile` | Успешно | | Полный `pytest` | `204 passed in 0.19s` | | Round-trip эквивалентность | Подтверждена | | Независимость `market_modes` | Подтверждена | | Сохранение порядка инструментов | Подтверждено | | Отсутствие преждевременного production-использования | Подтверждено | --- ## Условие завершения Build 014 Все условия выполнены: ```text [✓] Создан compatibility mapper Instrument → ExchangeSymbol. [✓] Переносятся все 11 общих legacy-полей. [✓] Decimal корректно преобразуется в float только на legacy-границе. [✓] None сохраняется. [✓] market_modes преобразуется из tuple в независимый list. [✓] Порядок market_modes сохраняется. [✓] Batch mapper сохраняет порядок инструментов. [✓] Пустой tuple преобразуется в пустой list. [✓] Исходные Instrument не изменяются. [✓] Round-trip проверка через comparator Build 013 проходит. [✓] 12 unit-тестов проходят. [✓] Синтаксическая проверка проходит. [✓] Полный набор из 204 тестов проходит. [✓] ExchangeService ещё не использует compatibility mapper. [✓] Production runtime не переключён преждевременно. [✓] Обратная совместимость полностью сохранена. ``` --- ## Результат ```text BUILD 014 — COMPLETE ``` Создана временная compatibility-граница: ```text Instrument ↓ ExchangeSymbol ``` Она позволяет на следующем этапе переключить: ```text ExchangeService.get_exchange_symbols() ``` на новую Instrument Reference Data acquisition pipeline без изменения существующего публичного контракта: ```python def get_exchange_symbols(self) -> list[ExchangeSymbol]: ... ``` Следующий этап утверждённого Migration Plan: ```text Build 015 — Переключение get_exchange_symbols() ```