19 KiB
Dzentra — Instrument Reference Data Migration — Build 014
Статус: Завершён
Название
Compatibility mapper Instrument → ExchangeSymbol
Цель
Создать временный compatibility-слой, преобразующий новую внутреннюю модель:
Instrument
в существующую legacy-модель:
ExchangeSymbol
Целевая цепочка:
new acquisition pipeline
↓
tuple[Instrument, ...]
↓
compatibility mapper
↓
list[ExchangeSymbol]
Compatibility mapper необходим для последующего переключения:
ExchangeService.get_exchange_symbols()
на новую Instrument Reference Data pipeline без изменения существующего публичного контракта:
def get_exchange_symbols(self) -> list[ExchangeSymbol]:
...
Build 014 создаёт только compatibility-границу.
Переключение ExchangeService.get_exchange_symbols() в рамках этого Build не выполняется.
Причина создания compatibility-слоя
К началу Build 014 завершены:
Build 001–012
↓
создана новая независимая Instrument Reference Data acquisition pipeline
Build 013
↓
доказана эквивалентность legacy- и новой реализации
на одном и том же реальном exchangeInfo sample
Новая pipeline возвращает:
tuple[Instrument, ...]
Существующий legacy-контракт возвращает:
list[ExchangeSymbol]
Существующие production-потребители продолжают ожидать:
ExchangeSymbol
Поэтому прямое переключение невозможно без временного преобразования:
Instrument
↓
ExchangeSymbol
Реализованные файлы
Создан production-файл:
app/src/market_data/acquisition/compatibility.py
Создан unit-тест:
app/tests/unit/market_data/acquisition/test_compatibility.py
Другие файлы в рамках Build 014 не изменялись.
Размещение compatibility mapper
Compatibility mapper размещён в:
app/src/market_data/acquisition/compatibility.py
Он намеренно не размещён в:
app/src/market_data/acquisition/adapters/dzengi/
поскольку преобразование:
Instrument → ExchangeSymbol
не зависит от формата Dzengi.
Он также не размещён в:
app/src/integrations/exchange/
поскольку новый миграционный код не должен расширять legacy-подсистему.
Архитектурная граница имеет следующий вид:
new acquisition model
↓
compatibility.py
↓
legacy integration model
Compatibility mapper является временным слоем и должен быть удалён после полного перевода production-потребителей:
Build 024 — Удаление compatibility-слоя
Реализованные функции
Созданы две функции:
def map_instrument_to_exchange_symbol(
instrument: Instrument,
) -> ExchangeSymbol:
...
и:
def map_instruments_to_exchange_symbols(
instruments: tuple[Instrument, ...],
) -> list[ExchangeSymbol]:
...
Первая функция преобразует один объект:
Instrument
↓
ExchangeSymbol
Вторая преобразует полный immutable-набор:
tuple[Instrument, ...]
↓
list[ExchangeSymbol]
Архитектурные зависимости
Compatibility mapper сознательно зависит от обеих моделей:
from src.integrations.exchange.models import ExchangeSymbol
from src.market_data.acquisition.models.instrument import Instrument
Это допустимая временная зависимость:
новая acquisition-подсистема
↓
compatibility boundary
↓
legacy contract
Никакие другие компоненты новой acquisition pipeline не изменялись для добавления зависимости от ExchangeSymbol.
Re-export через:
app/src/market_data/acquisition/__init__.py
не добавлялся.
Соответствие полей
Compatibility mapper переносит все 11 полей, общих для Instrument и ExchangeSymbol:
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 содержит дополнительные поля:
asset_type
order_types
base_asset_precision
quote_asset_precision
tick_value
max_qty
country
sector
industry
trading_hours
Эти поля отсутствуют в legacy-модели:
ExchangeSymbol
Поэтому они намеренно не переносятся через compatibility boundary.
Это ожидаемая потеря расширенной информации:
полная новая модель Instrument
↓
ограниченный legacy-контракт ExchangeSymbol
Compatibility mapper не:
расширяет ExchangeSymbol;
создаёт дополнительные атрибуты;
переносит данные в несоответствующие поля;
изменяет модель Instrument.
Преобразование Decimal → float
Новая модель Instrument использует:
Decimal | None
Legacy-модель ExchangeSymbol использует:
float | None
Преобразованию подлежат:
tick_size
step_size
min_qty
min_notional
Правило преобразования:
None
↓
None
и:
Decimal("0.0001")
↓
0.0001
Для этого реализован внутренний helper:
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
Новая модель использует:
tuple[str, ...]
Legacy-модель использует:
list[str]
Compatibility mapper выполняет:
list(instrument.market_modes)
Например:
Instrument:
("REGULAR", "CLOSE_ONLY")
↓
ExchangeSymbol:
["REGULAR", "CLOSE_ONLY"]
Порядок значений сохраняется.
Для каждого результата создаётся новый независимый список.
Изменение:
exchange_symbol.market_modes.append("ADDED_IN_LEGACY")
не изменяет:
instrument.market_modes
и не влияет на другие объекты ExchangeSymbol, созданные из того же Instrument.
Преобразование полного набора инструментов
Функция:
map_instruments_to_exchange_symbols()
принимает:
tuple[Instrument, ...]
и возвращает:
list[ExchangeSymbol]
Порядок инструментов сохраняется.
Например:
(
BTC/USD_LEVERAGE,
ETH/USD_LEVERAGE,
XRP/USD_LEVERAGE,
)
преобразуется в:
[
BTC/USD_LEVERAGE,
ETH/USD_LEVERAGE,
XRP/USD_LEVERAGE,
]
Для пустого входного набора:
()
возвращается:
[]
Обработка ошибок
Новый тип исключения в рамках Build 014 не создавался.
Compatibility mapper получает уже построенную и проверенную модель:
Instrument
и выполняет только:
чтение полей;
Decimal → float;
tuple → list;
создание ExchangeSymbol.
Существующая ошибка:
InstrumentReferenceMappingError
не переиспользуется, поскольку она относится к другому направлению преобразования:
Dzengi raw model
↓
Instrument
Создание отдельной категории ошибки без конкретного реального сценария не требуется.
Unit-тесты
Создан файл:
app/tests/unit/market_data/acquisition/test_compatibility.py
Реализовано 12 тестов.
Проверяются:
- преобразование одного
InstrumentвExchangeSymbol; - перенос всех 11 общих legacy-полей;
- преобразование
Decimal → float; - сохранение
Noneдля отсутствующих числовых значений; - преобразование
tuple[str, ...] → list[str]дляmarket_modes; - сохранение порядка
market_modes; - создание независимого списка
market_modes; - преобразование нескольких инструментов;
- сохранение порядка инструментов;
- пустой
tupleпреобразуется в пустойlist; - исходный
Instrumentне изменяется; - результат compatibility mapper эквивалентен исходным
Instrumentпо comparator Build 013.
Round-trip проверка через comparator Build 013
Для дополнительной проверки используется уже созданный в Build 013 comparator:
tuple[Instrument, ...]
↓
map_instruments_to_exchange_symbols()
↓
list[ExchangeSymbol]
↓
compare_instrument_reference_data()
↓
InstrumentReferenceEquivalenceReport
Основная проверка:
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-тестов
Выполнена команда:
python -m pytest \
tests/unit/market_data/acquisition/test_compatibility.py \
-q
Результат:
12 passed in 0.02s
Все тесты compatibility mapper успешно пройдены.
Проверка синтаксиса
Выполнена команда:
python -m py_compile \
src/market_data/acquisition/compatibility.py \
tests/unit/market_data/acquisition/test_compatibility.py
Результат:
успешно
Ошибок синтаксиса не обнаружено.
Полный регрессионный прогон
Выполнена команда:
python -m pytest -q
Результат:
204 passed in 0.19s
До Build 014 полный набор содержал:
192 passed
В Build 014 добавлено:
12 новых тестов
Итого:
192 + 12 = 204
Все предыдущие тесты продолжают проходить.
Регрессий существующего проекта не обнаружено.
Проверка отсутствия преждевременного production-использования
Выполнена команда:
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "map_instrument_to_exchange_symbol|map_instruments_to_exchange_symbols" \
src tests
Результат подтвердил, что функции compatibility mapper используются только в:
src/market_data/acquisition/compatibility.py
tests/unit/market_data/acquisition/test_compatibility.py
Других production-потребителей нет.
В частности:
ExchangeService.get_exchange_symbols()
ещё не использует compatibility mapper.
Это подтверждает отсутствие преждевременного переключения production runtime.
Изменения production-кода
В рамках Build 014 создан только один новый production-файл:
app/src/market_data/acquisition/compatibility.py
Не изменялись:
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/
Обратная совместимость
Полностью сохранены:
ExchangeService.get_exchange_symbols()
ExchangeService.validate_symbol()
ExchangeService.get_symbol_runtime_status()
Также не изменены:
сигнатуры существующих production-методов;
legacy imports;
формат runtime-ошибок;
Telegram UI;
автоторговля;
существующее runtime-поведение;
exchange symbols cache.
Что Build 014 намеренно не делает
Build 014 не выполняет:
переключение 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.
Переключение:
ExchangeService.get_exchange_symbols()
относится строго к следующему этапу:
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
Все условия выполнены:
[✓] Создан 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 не переключён преждевременно.
[✓] Обратная совместимость полностью сохранена.
Результат
BUILD 014 — COMPLETE
Создана временная compatibility-граница:
Instrument
↓
ExchangeSymbol
Она позволяет на следующем этапе переключить:
ExchangeService.get_exchange_symbols()
на новую Instrument Reference Data acquisition pipeline без изменения существующего публичного контракта:
def get_exchange_symbols(self) -> list[ExchangeSymbol]:
...
Следующий этап утверждённого Migration Plan:
Build 015 — Переключение get_exchange_symbols()