Files
dzentra_bot/docs/migrations/build_014.md

19 KiB
Raw Permalink Blame History

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 001012
    ↓
создана новая независимая 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 тестов.

Проверяются:

  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:

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