Files
dzentra_bot/docs/migrations/build_014.md

890 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 001012
создана новая независимая 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()
```