890 lines
19 KiB
Markdown
890 lines
19 KiB
Markdown
# 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()
|
||
``` |