# 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()
```