812 lines
19 KiB
Markdown
812 lines
19 KiB
Markdown
# Dzentra — Instrument Reference Data Migration — Build 013
|
||
|
||
> Статус: Завершён
|
||
|
||
## Название
|
||
|
||
**Проверка эквивалентности старой и новой реализации**
|
||
|
||
---
|
||
|
||
## Цель
|
||
|
||
Доказать эквивалентность существующей legacy-реализации обработки `exchangeInfo` и новой подсистемы Instrument Reference Data до начала переключения production-потребителей.
|
||
|
||
Проверка должна подтвердить, что один и тот же исходный документ `exchangeInfo`, обработанный двумя независимыми путями, приводит к эквивалентным результатам в части полей, существующих одновременно в legacy-модели `ExchangeSymbol` и новой модели `Instrument`.
|
||
|
||
Build 013 не изменяет production-код и не переключает существующий runtime на новую реализацию.
|
||
|
||
---
|
||
|
||
## Исходная архитектура проверки
|
||
|
||
Один и тот же сохранённый документ используется обеими реализациями:
|
||
|
||
```text
|
||
один exchangeInfo document
|
||
│
|
||
├──→ legacy implementation
|
||
│ │
|
||
│ ├──→ _extract_exchange_symbols_raw()
|
||
│ │
|
||
│ └──→ _parse_exchange_symbol()
|
||
│ │
|
||
│ ↓
|
||
│ list[ExchangeSymbol]
|
||
│
|
||
└──→ new implementation
|
||
│
|
||
└──→ DzengiInstrumentDocumentHandler
|
||
│
|
||
├──→ schema validation
|
||
├──→ parser
|
||
├──→ value validation
|
||
└──→ mapper
|
||
│
|
||
↓
|
||
tuple[Instrument, ...]
|
||
|
||
│
|
||
↓
|
||
equivalence comparator
|
||
│
|
||
↓
|
||
structured comparison report
|
||
```
|
||
|
||
Сетевые запросы в проверке не выполняются.
|
||
|
||
Обе реализации получают один и тот же сохранённый документ, что исключает влияние изменений данных биржи между двумя отдельными REST-запросами.
|
||
|
||
---
|
||
|
||
## Реализованные файлы
|
||
|
||
Созданы:
|
||
|
||
```text
|
||
app/tests/support/instrument_reference_equivalence.py
|
||
|
||
app/tests/unit/market_data/acquisition/
|
||
└── test_equivalence_comparator.py
|
||
|
||
app/tests/integration/market_data/acquisition/
|
||
└── test_instrument_reference_equivalence.py
|
||
```
|
||
|
||
Production-код не изменялся.
|
||
|
||
Не создавался production-модуль:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/equivalence.py
|
||
```
|
||
|
||
Это принципиальное архитектурное решение: механизм проверки эквивалентности является временным миграционным инструментом и не должен создавать зависимость новой production-подсистемы от legacy-модели `ExchangeSymbol`.
|
||
|
||
---
|
||
|
||
## Проверяемые реализации
|
||
|
||
### Legacy implementation
|
||
|
||
Проверяется существующая логика:
|
||
|
||
```text
|
||
ExchangeService._extract_exchange_symbols_raw()
|
||
ExchangeService._parse_exchange_symbol()
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
list[ExchangeSymbol]
|
||
```
|
||
|
||
Для запуска legacy parsing logic используется:
|
||
|
||
```python
|
||
service = object.__new__(ExchangeService)
|
||
```
|
||
|
||
Это позволяет проверить существующие parsing helpers без запуска:
|
||
|
||
```text
|
||
ExchangeService.__init__()
|
||
load_settings()
|
||
JournalService()
|
||
REST request
|
||
exchange symbols cache
|
||
```
|
||
|
||
На корректном документе используемые parsing helpers не требуют `settings` или `journal`.
|
||
|
||
Legacy-логика не копируется и не переписывается внутри теста.
|
||
|
||
---
|
||
|
||
### New implementation
|
||
|
||
Проверяется существующий Handler:
|
||
|
||
```text
|
||
DzengiInstrumentDocumentHandler
|
||
```
|
||
|
||
Вызов:
|
||
|
||
```python
|
||
DzengiInstrumentDocumentHandler().handle_instrument_document(document)
|
||
```
|
||
|
||
Через него запускается новая pipeline:
|
||
|
||
```text
|
||
schema validation
|
||
↓
|
||
parser
|
||
↓
|
||
value validation
|
||
↓
|
||
mapper
|
||
↓
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
Таким образом Build 013 проверяет реальную реализацию, созданную в предыдущих Build, а не её тестовую копию.
|
||
|
||
---
|
||
|
||
## Источник тестовых данных
|
||
|
||
Для integration-проверки используется сохранённый реальный sample:
|
||
|
||
```text
|
||
app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json
|
||
```
|
||
|
||
Один и тот же JSON document передаётся обеим реализациям.
|
||
|
||
Это гарантирует корректность сравнения:
|
||
|
||
```text
|
||
same input
|
||
↓
|
||
legacy implementation
|
||
↓
|
||
new implementation
|
||
↓
|
||
equivalence comparison
|
||
```
|
||
|
||
---
|
||
|
||
## Общие поля моделей
|
||
|
||
Legacy-модель:
|
||
|
||
```text
|
||
ExchangeSymbol
|
||
```
|
||
|
||
Новая модель:
|
||
|
||
```text
|
||
Instrument
|
||
```
|
||
|
||
Сравниваются только поля, существующие одновременно в обеих моделях:
|
||
|
||
```text
|
||
symbol
|
||
name
|
||
status
|
||
base_asset
|
||
quote_asset
|
||
market_modes
|
||
market_type
|
||
tick_size
|
||
step_size
|
||
min_qty
|
||
min_notional
|
||
```
|
||
|
||
Всего:
|
||
|
||
```text
|
||
11 общих полей
|
||
```
|
||
|
||
---
|
||
|
||
## Поля новой модели, не участвующие в проверке эквивалентности
|
||
|
||
Новая модель `Instrument` содержит дополнительные поля:
|
||
|
||
```text
|
||
asset_type
|
||
order_types
|
||
base_asset_precision
|
||
quote_asset_precision
|
||
tick_value
|
||
max_qty
|
||
country
|
||
sector
|
||
industry
|
||
trading_hours
|
||
```
|
||
|
||
Их отсутствие в legacy-модели `ExchangeSymbol` не является нарушением эквивалентности.
|
||
|
||
Эти поля являются расширением новой внутренней модели Instrument Reference Data.
|
||
|
||
---
|
||
|
||
## Каноническое сравнение числовых значений
|
||
|
||
Legacy implementation хранит числовые ограничения как:
|
||
|
||
```text
|
||
float | None
|
||
```
|
||
|
||
Новая модель хранит их как:
|
||
|
||
```text
|
||
Decimal | None
|
||
```
|
||
|
||
Для корректного сравнения обе стороны приводятся к общей канонической форме:
|
||
|
||
```python
|
||
Decimal(str(value))
|
||
```
|
||
|
||
Пример:
|
||
|
||
```text
|
||
legacy:
|
||
0.0001
|
||
|
||
new:
|
||
Decimal("0.0001")
|
||
|
||
canonical comparison:
|
||
Decimal("0.0001") == Decimal("0.0001")
|
||
```
|
||
|
||
Таким образом различие представления:
|
||
|
||
```text
|
||
float
|
||
Decimal
|
||
```
|
||
|
||
не считается различием значения.
|
||
|
||
Для сравнения не используется:
|
||
|
||
```text
|
||
math.isclose()
|
||
```
|
||
|
||
Поскольку биржевые ограничения являются точными справочными десятичными значениями, а не измерениями с допустимой погрешностью.
|
||
|
||
---
|
||
|
||
## Сравнение market_modes
|
||
|
||
Legacy-модель использует:
|
||
|
||
```python
|
||
list[str]
|
||
```
|
||
|
||
Новая модель использует:
|
||
|
||
```python
|
||
tuple[str, ...]
|
||
```
|
||
|
||
Обе стороны приводятся к:
|
||
|
||
```python
|
||
tuple(value)
|
||
```
|
||
|
||
Поэтому:
|
||
|
||
```text
|
||
["REGULAR"]
|
||
```
|
||
|
||
эквивалентно:
|
||
|
||
```text
|
||
("REGULAR",)
|
||
```
|
||
|
||
Тип контейнера не считается расхождением.
|
||
|
||
Порядок значений сохраняется и участвует в сравнении.
|
||
|
||
Например:
|
||
|
||
```text
|
||
("REGULAR", "CLOSE_ONLY")
|
||
```
|
||
|
||
не эквивалентно:
|
||
|
||
```text
|
||
("CLOSE_ONLY", "REGULAR")
|
||
```
|
||
|
||
---
|
||
|
||
## Сравнение строковых значений
|
||
|
||
Следующие поля сравниваются точно:
|
||
|
||
```text
|
||
symbol
|
||
name
|
||
status
|
||
base_asset
|
||
quote_asset
|
||
market_type
|
||
```
|
||
|
||
Не выполняется дополнительная нормализация:
|
||
|
||
```text
|
||
lower()
|
||
upper()
|
||
casefold()
|
||
alias mapping
|
||
```
|
||
|
||
Причина: Build 013 должен обнаруживать реальные различия интерпретации между двумя реализациями, а не скрывать их дополнительной логикой comparator.
|
||
|
||
---
|
||
|
||
## Проверка состава инструментов
|
||
|
||
До сравнения полей проверяются:
|
||
|
||
```text
|
||
дубликаты symbol в legacy implementation;
|
||
дубликаты symbol в new implementation;
|
||
symbol существует только в legacy;
|
||
symbol существует только в new.
|
||
```
|
||
|
||
После проверки дубликатов строятся индексы:
|
||
|
||
```text
|
||
symbol → ExchangeSymbol
|
||
symbol → Instrument
|
||
```
|
||
|
||
Для построения индексов используется сохранение первого встретившегося объекта.
|
||
|
||
Дубликаты при этом уже отдельно фиксируются как mismatch и не могут быть незаметно скрыты перезаписью значения в словаре.
|
||
|
||
---
|
||
|
||
## Модель расхождения
|
||
|
||
Каждое найденное расхождение представлено структурой:
|
||
|
||
```text
|
||
InstrumentReferenceMismatch
|
||
```
|
||
|
||
Поля:
|
||
|
||
```text
|
||
kind
|
||
symbol
|
||
field
|
||
legacy_value
|
||
new_value
|
||
message
|
||
```
|
||
|
||
Поддерживаемые категории:
|
||
|
||
```text
|
||
duplicate_legacy
|
||
duplicate_new
|
||
missing_in_legacy
|
||
missing_in_new
|
||
field_mismatch
|
||
```
|
||
|
||
Пример расхождения поля:
|
||
|
||
```text
|
||
kind: field_mismatch
|
||
symbol: ETH/EUR_LEVERAGE
|
||
field: min_notional
|
||
legacy: None
|
||
new: Decimal("2")
|
||
```
|
||
|
||
---
|
||
|
||
## Модель итогового отчёта
|
||
|
||
Результат сравнения представлен структурой:
|
||
|
||
```text
|
||
InstrumentReferenceEquivalenceReport
|
||
```
|
||
|
||
Поля:
|
||
|
||
```text
|
||
legacy_count
|
||
new_count
|
||
compared_count
|
||
mismatches
|
||
```
|
||
|
||
Также предоставляется свойство:
|
||
|
||
```text
|
||
is_equivalent
|
||
```
|
||
|
||
Логика:
|
||
|
||
```text
|
||
mismatches == ()
|
||
↓
|
||
is_equivalent == True
|
||
```
|
||
|
||
При наличии хотя бы одного mismatch:
|
||
|
||
```text
|
||
is_equivalent == False
|
||
```
|
||
|
||
---
|
||
|
||
## Диагностический отчёт
|
||
|
||
Метод:
|
||
|
||
```text
|
||
InstrumentReferenceEquivalenceReport.format()
|
||
```
|
||
|
||
формирует человекочитаемый диагностический отчёт.
|
||
|
||
Успешный результат имеет вид:
|
||
|
||
```text
|
||
Instrument Reference Data equivalence report
|
||
legacy_count: 51
|
||
new_count: 51
|
||
compared_count: 51
|
||
mismatches: 0
|
||
is_equivalent: True
|
||
```
|
||
|
||
При обнаружении различий отчёт содержит для каждого mismatch:
|
||
|
||
```text
|
||
kind
|
||
symbol
|
||
field
|
||
legacy value
|
||
new value
|
||
message
|
||
```
|
||
|
||
Это позволяет анализировать все обнаруженные расхождения, а не только первое.
|
||
|
||
---
|
||
|
||
## Unit-тесты comparator
|
||
|
||
Создан файл:
|
||
|
||
```text
|
||
app/tests/unit/market_data/acquisition/test_equivalence_comparator.py
|
||
```
|
||
|
||
Реализовано 11 тестов.
|
||
|
||
Проверяются:
|
||
|
||
1. полностью эквивалентные инструменты;
|
||
2. эквивалентность `float` и `Decimal`;
|
||
3. эквивалентность `list` и `tuple` для `market_modes`;
|
||
4. несовпадение строкового поля;
|
||
5. несовпадение числового поля;
|
||
6. символ отсутствует в новой реализации;
|
||
7. символ отсутствует в legacy-реализации;
|
||
8. duplicate symbol в legacy;
|
||
9. duplicate symbol в новой реализации;
|
||
10. несколько расхождений одновременно;
|
||
11. корректное формирование диагностического отчёта.
|
||
|
||
Результат:
|
||
|
||
```text
|
||
11 passed in 0.02s
|
||
```
|
||
|
||
---
|
||
|
||
## Integration-проверка реального exchangeInfo sample
|
||
|
||
Создан файл:
|
||
|
||
```text
|
||
app/tests/integration/market_data/acquisition/
|
||
└── test_instrument_reference_equivalence.py
|
||
```
|
||
|
||
Проверяется полный путь:
|
||
|
||
```text
|
||
all.json
|
||
│
|
||
├──→ legacy parser
|
||
│ ↓
|
||
│ list[ExchangeSymbol]
|
||
│
|
||
└──→ DzengiInstrumentDocumentHandler
|
||
↓
|
||
tuple[Instrument, ...]
|
||
│
|
||
↓
|
||
compare_instrument_reference_data()
|
||
│
|
||
↓
|
||
InstrumentReferenceEquivalenceReport
|
||
```
|
||
|
||
Основное утверждение:
|
||
|
||
```python
|
||
assert report.is_equivalent, report.format()
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
1 passed in 0.10s
|
||
```
|
||
|
||
Проверка подтвердила эквивалентность legacy- и новой реализации на одном и том же реальном `exchangeInfo` sample.
|
||
|
||
---
|
||
|
||
## Проверка синтаксиса
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m py_compile \
|
||
tests/support/instrument_reference_equivalence.py \
|
||
tests/unit/market_data/acquisition/test_equivalence_comparator.py \
|
||
tests/integration/market_data/acquisition/test_instrument_reference_equivalence.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
успешно
|
||
```
|
||
|
||
Ошибок синтаксиса не обнаружено.
|
||
|
||
---
|
||
|
||
## Полный регрессионный прогон
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
python -m pytest -q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
192 passed in 0.15s
|
||
```
|
||
|
||
Регрессий существующего проекта не обнаружено.
|
||
|
||
---
|
||
|
||
## Проверка отсутствия production-зависимостей
|
||
|
||
Выполнена команда:
|
||
|
||
```bash
|
||
grep -RIn \
|
||
--exclude-dir="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
-E "compare_instrument_reference_data|InstrumentReferenceMismatch|InstrumentReferenceEquivalenceReport" \
|
||
src tests
|
||
```
|
||
|
||
Результат подтвердил, что все сущности механизма проверки эквивалентности находятся только в:
|
||
|
||
```text
|
||
tests/support/
|
||
tests/unit/
|
||
tests/integration/
|
||
```
|
||
|
||
В каталоге:
|
||
|
||
```text
|
||
src/
|
||
```
|
||
|
||
упоминаний нет.
|
||
|
||
Следовательно:
|
||
|
||
```text
|
||
production-код не зависит от comparator;
|
||
новая acquisition-подсистема не зависит от legacy-модели ради runtime;
|
||
ExchangeService не изменён;
|
||
Telegram UI не изменён;
|
||
AutoTrade не изменён;
|
||
trading runtime не изменён.
|
||
```
|
||
|
||
---
|
||
|
||
## Изменения production-кода
|
||
|
||
В рамках Build 013 production-код не изменялся.
|
||
|
||
Не изменены:
|
||
|
||
```text
|
||
app/src/integrations/exchange/service.py
|
||
app/src/integrations/exchange/models.py
|
||
|
||
app/src/market_data/acquisition/
|
||
app/src/telegram/
|
||
app/src/trading/
|
||
```
|
||
|
||
Не создавался:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/equivalence.py
|
||
```
|
||
|
||
---
|
||
|
||
## Обратная совместимость
|
||
|
||
Полностью сохранены:
|
||
|
||
```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 013 намеренно не делает
|
||
|
||
Build 013 не выполняет:
|
||
|
||
```text
|
||
изменение legacy parser;
|
||
изменение нового parser;
|
||
изменение mapper;
|
||
переключение get_exchange_symbols();
|
||
создание compatibility mapper Instrument → ExchangeSymbol;
|
||
перенос кэша;
|
||
изменение validate_symbol();
|
||
изменение get_symbol_runtime_status();
|
||
подключение InstrumentAcquisitionService к production runtime;
|
||
изменение Telegram UI;
|
||
изменение AutoTrade.
|
||
```
|
||
|
||
Эти изменения относятся к последующим Build утверждённого Migration Plan.
|
||
|
||
---
|
||
|
||
## Классификация изменений
|
||
|
||
| Изменение | Классификация |
|
||
|---|---|
|
||
| Comparator legacy/new | Миграционная проверка |
|
||
| Структурированный отчёт | Улучшение диагностируемости |
|
||
| Unit-тесты comparator | Обязательная проверка надёжности |
|
||
| Integration-тест на одном sample | Обязательная проверка эквивалентности |
|
||
| Новый production-модуль | Не создавался |
|
||
| Изменение legacy-кода | Отсутствует |
|
||
| Изменение новой acquisition pipeline | Отсутствует |
|
||
| Изменение runtime-поведения | Отсутствует |
|
||
|
||
---
|
||
|
||
## Итоговые проверки
|
||
|
||
| Проверка | Результат |
|
||
|---|---|
|
||
| Unit-тесты comparator | `11 passed in 0.02s` |
|
||
| Integration-тест реального sample | `1 passed in 0.10s` |
|
||
| `py_compile` | Успешно |
|
||
| Полный `pytest` | `192 passed in 0.15s` |
|
||
| Отсутствие production-интеграции comparator | Подтверждено |
|
||
|
||
---
|
||
|
||
## Условие завершения Build 013
|
||
|
||
Все условия выполнены:
|
||
|
||
```text
|
||
[✓] Comparator обнаруживает категории различий.
|
||
|
||
[✓] Legacy и new implementations получают один и тот же документ.
|
||
|
||
[✓] Legacy parser запускается без REST-запроса и кэша.
|
||
|
||
[✓] New pipeline запускается через реальный DzengiInstrumentDocumentHandler.
|
||
|
||
[✓] Дубликаты проверяются до сравнения полей.
|
||
|
||
[✓] Проверяются все 11 общих полей.
|
||
|
||
[✓] Реальный exchangeInfo sample проходит без mismatches.
|
||
|
||
[✓] Unit-тесты comparator проходят.
|
||
|
||
[✓] Integration-тест проходит.
|
||
|
||
[✓] Синтаксическая проверка проходит.
|
||
|
||
[✓] Полный набор тестов проекта проходит.
|
||
|
||
[✓] Production-код не изменён.
|
||
|
||
[✓] Обратная совместимость полностью сохранена.
|
||
```
|
||
|
||
---
|
||
|
||
## Результат
|
||
|
||
```text
|
||
BUILD 013 — COMPLETE
|
||
```
|
||
|
||
Проверка подтвердила эквивалентность существующей legacy-реализации и новой Instrument Reference Data pipeline на одном и том же реальном документе `exchangeInfo`.
|
||
|
||
Можно переходить к следующему этапу утверждённого Migration Plan:
|
||
|
||
```text
|
||
Build 014 — Compatibility mapper Instrument → ExchangeSymbol
|
||
``` |