build 039: complete Quotes Feed migration foundation

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit 7b62873832
443 changed files with 80452 additions and 1335 deletions

View File

@@ -0,0 +1,890 @@
# 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()
```