Files
dzentra_bot/docs/migrations/build_006.md

1071 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Build 006 — Dzengi → Instrument Mapper
**Статус:** Завершён
**Подсистема:** `market_data/acquisition`
**Область:** Instrument Reference Data
**Тип изменения:** Изолированное расширение новой архитектуры без подключения к production runtime
**Результат полного набора тестов:** `106 passed`
---
## 1. Цель Build 006
Цель Build 006 — реализовать преобразование source-specific raw-моделей Dzengi во внутреннюю source-independent модель Dzentra:
```text
DzengiExchangeInfoSymbol
Instrument
```
Также реализовано преобразование полного ответа `exchangeInfo`:
```text
DzengiExchangeInfoResponse
tuple[Instrument, ...]
```
После завершения Build 006 pipeline Instrument Reference Data выглядит следующим образом:
```text
Dzengi raw JSON
Schema Validation
Parser
Dzengi Raw Models
Value Validation
Mapper
Instrument
```
Build 006 не подключает новый mapper к существующему `ExchangeService` и не меняет поведение работающего бота.
---
## 2. Почему Build 006 выполняется именно сейчас
До начала Build 006 были завершены необходимые предыдущие этапы:
```text
Build 001 — внутренняя модель Instrument
Build 002 — транспортные модели Dzengi
Build 003 — структурная валидация exchangeInfo
Build 004 — parser exchangeInfo
Build 005 — value validation
```
К началу Build 006 входные данные mapper уже:
- структурно проверены;
- преобразованы в типизированные raw-модели;
- проверены на допустимость значений;
- изолированы от legacy-кода.
Поэтому mapper может отвечать только за преобразование модели источника во внутреннюю модель Dzentra.
---
## 3. Архитектурная граница Build 006
Build 006 отвечает только за преобразование:
```text
DzengiExchangeInfoSymbol
Instrument
```
и:
```text
DzengiExchangeInfoResponse
tuple[Instrument, ...]
```
Mapper не выполняет:
- REST-запросы;
- schema validation;
- parsing JSON;
- полную value validation;
- нормализацию пользовательского ввода символа;
- определение runtime-статуса рынка;
- управление кэшем;
- создание legacy-модели `ExchangeSymbol`;
- интеграцию с `ExchangeService`;
- интеграцию с Telegram UI;
- интеграцию с автоторговлей.
Таким образом, mapper остаётся изолированным слоем между source-specific моделями адаптера Dzengi и source-independent моделью Dzentra.
---
## 4. Изменённые файлы
В рамках Build 006 изменены:
```text
app/src/market_data/acquisition/exceptions.py
app/src/market_data/acquisition/adapters/dzengi/mapper.py
```
Создан тестовый файл:
```text
app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
```
Не изменялись:
```text
app/src/market_data/acquisition/models/instrument.py
app/src/market_data/acquisition/adapters/dzengi/models.py
app/src/market_data/acquisition/adapters/dzengi/parser.py
app/src/market_data/acquisition/validation/schema.py
app/src/market_data/acquisition/validation/values.py
app/src/integrations/exchange/*
app/src/telegram/*
app/src/trading/*
```
Работающий legacy-код бота не изменён.
---
## 5. Новая ошибка mapper
В файл:
```text
app/src/market_data/acquisition/exceptions.py
```
добавлена ошибка:
```python
class InstrumentReferenceMappingError(MarketDataAcquisitionError):
pass
```
Итоговая иерархия ошибок Instrument Reference Data:
```text
MarketDataAcquisitionError
├── InstrumentReferenceSchemaError
├── InstrumentReferenceParseError
├── InstrumentReferenceValueError
└── InstrumentReferenceMappingError
```
`InstrumentReferenceMappingError` обозначает ошибки, возникшие непосредственно при преобразовании source-specific raw-модели во внутреннюю модель `Instrument`.
Примеры:
```text
несколько LOT_SIZE filters
несколько MIN_NOTIONAL filters
невозможность преобразования значения в Decimal
неконечное числовое значение
```
---
## 6. Публичный контракт mapper
В файле:
```text
app/src/market_data/acquisition/adapters/dzengi/mapper.py
```
реализованы две публичные функции.
### 6.1. Преобразование одного инструмента
```python
def map_dzengi_symbol_to_instrument(
symbol: DzengiExchangeInfoSymbol,
) -> Instrument:
...
```
Функция преобразует один объект:
```text
DzengiExchangeInfoSymbol
```
в один объект:
```text
Instrument
```
### 6.2. Преобразование полного ответа exchangeInfo
```python
def map_dzengi_exchange_info_to_instruments(
response: DzengiExchangeInfoResponse,
) -> tuple[Instrument, ...]:
...
```
Функция преобразует все элементы:
```text
DzengiExchangeInfoResponse.payload.symbols
```
в immutable-последовательность:
```text
tuple[Instrument, ...]
```
Порядок инструментов сохраняется.
---
## 7. Правила преобразования полей
Реализовано следующее соответствие:
```text
DzengiExchangeInfoSymbol.symbol
→ Instrument.symbol
name
→ name
status
→ status
base_asset
→ base_asset
quote_asset
→ quote_asset
asset_type
→ asset_type
market_type
→ market_type
market_modes
→ market_modes
order_types
→ order_types
base_asset_precision
→ base_asset_precision
quote_precision
→ quote_asset_precision
tick_size
→ tick_size
tick_value
→ tick_value
country
→ country
sector
→ sector
industry
→ industry
trading_hours
→ trading_hours
```
---
## 8. Преобразование числовых значений в Decimal
Следующие значения преобразуются во внутренний тип `Decimal`:
```text
tick_size
tick_value
step_size
min_qty
max_qty
min_notional
```
Используется преобразование:
```python
Decimal(str(value))
```
Это позволяет избежать дополнительной двоичной погрешности при непосредственном преобразовании `float` в `Decimal`.
Примеры:
```text
0.01
→ Decimal("0.01")
"0.001"
→ Decimal("0.001")
0
→ Decimal("0")
0.00000001
→ Decimal("1E-8")
```
Если исходное значение отсутствует:
```text
None → None
```
---
## 9. Защита Decimal-преобразования
Mapper предполагает, что перед его вызовом уже выполнена:
```text
Value Validation
```
Однако публичная функция mapper может быть вызвана напрямую.
Поэтому непосредственно в mapper оставлена локальная защита преобразования в `Decimal`.
Если значение невозможно преобразовать:
```text
"not-a-number"
```
возникает:
```text
InstrumentReferenceMappingError
```
Также отклоняются неконечные значения:
```text
NaN
Infinity
-Infinity
```
Это не повторение полной Value Validation. Mapper защищает только собственную непосредственную обязанность — корректное создание `Decimal`.
---
## 10. Извлечение LOT_SIZE
Из:
```text
DzengiLotSizeFilter
```
извлекаются:
```text
min_qty
max_qty
step_size
```
Пример исходного фильтра:
```python
DzengiLotSizeFilter(
filter_type="LOT_SIZE",
min_qty="0.001",
max_qty="1000",
step_size="0.001",
)
```
Результат:
```text
Instrument.min_qty
= Decimal("0.001")
Instrument.max_qty
= Decimal("1000")
Instrument.step_size
= Decimal("0.001")
```
Если `LOT_SIZE` отсутствует:
```text
min_qty → None
max_qty → None
step_size → None
```
---
## 11. Извлечение MIN_NOTIONAL
Из:
```text
DzengiMinNotionalFilter
```
извлекается:
```text
min_notional
```
Пример исходного фильтра:
```python
DzengiMinNotionalFilter(
filter_type="MIN_NOTIONAL",
min_notional="2",
)
```
Результат:
```text
Instrument.min_notional
= Decimal("2")
```
Если `MIN_NOTIONAL` отсутствует:
```text
min_notional → None
```
---
## 12. Неизвестные filters
Неизвестные фильтры представлены raw-моделью:
```text
DzengiUnknownFilter
```
Mapper не переносит их во внутреннюю модель `Instrument` и не выбрасывает из-за них ошибку.
Например:
```python
DzengiUnknownFilter(
filter_type="FUTURE_FILTER",
fields=(
("enabled", True),
("limit", 10),
),
)
```
не препятствует созданию `Instrument`.
Это сознательное архитектурное решение:
```text
Raw Models
сохраняют неизвестные source-specific данные
Instrument
содержит только известные канонические поля Dzentra
```
Неизвестные данные не теряются на транспортном уровне, но не загрязняют source-independent модель.
---
## 13. Защита от дублирующихся известных filters
Mapper не допускает неоднозначного выбора значения.
Если один инструмент содержит несколько фильтров:
```text
LOT_SIZE
```
возникает:
```text
InstrumentReferenceMappingError
```
Аналогичное правило действует для нескольких фильтров:
```text
MIN_NOTIONAL
```
Mapper не выбирает молча первый или последний фильтр, поскольку это могло бы привести к использованию неверных торговых ограничений.
---
## 14. Нормализация необязательных текстовых полей
Для следующих optional-полей применяется предметная нормализация:
```text
asset_type
country
sector
industry
trading_hours
```
Правила:
```text
None
→ None
""
→ None
" "
→ None
" DE "
→ "DE"
```
Это соответствует фактическому поведению Dzengi API, где некоторые справочные поля могут присутствовать как пустые строки.
Например:
```text
country=""
sector=""
industry=""
```
преобразуются во внутреннюю модель как:
```text
country=None
sector=None
industry=None
```
---
## 15. Обязательные строки не нормализуются mapper
Следующие поля переносятся без изменения:
```text
symbol
name
status
base_asset
quote_asset
market_type
```
Причины:
- parser сохраняет транспортное значение;
- Value Validation уже проверяет обязательность и непустоту;
- mapper не должен незаметно менять идентификаторы или статусы источника.
---
## 16. Сохранение порядка последовательностей
Mapper сохраняет исходный порядок:
```text
market_modes
order_types
```
Например:
```text
("REGULAR", "CLOSE_ONLY", "EXTENDED")
```
остаётся:
```text
("REGULAR", "CLOSE_ONLY", "EXTENDED")
```
А:
```text
("MARKET", "LIMIT", "STOP")
```
остаётся:
```text
("MARKET", "LIMIT", "STOP")
```
Mapper не выполняет:
```text
sorting
deduplication
set conversion
```
---
## 17. Поля Dzengi, которые сознательно не входят в Instrument
Следующие source-specific данные сохранены в `DzengiExchangeInfoSymbol`, но не переносятся в текущую модель `Instrument`:
```text
quote_asset_id
trading_fee
exchange_fee
long_rate
short_rate
swap_charge_interval
min_sl_gap
max_sl_gap
min_tp_gap
max_tp_gap
```
Они не потеряны на транспортном уровне.
В дальнейшем эти данные могут использоваться отдельными предметными контрактами:
```text
fees
financing
execution constraints
provider-specific identifiers
```
Build 006 не смешивает эти области с базовой моделью `Instrument`.
---
## 18. Immutable-результат
Модель:
```text
Instrument
```
объявлена как:
```python
@dataclass(frozen=True, slots=True)
```
Тестами подтверждено, что попытка изменения уже созданного объекта приводит к:
```text
FrozenInstanceError
```
Это обеспечивает стабильность справочного value object после mapping.
---
## 19. Реализованные тестовые сценарии
Создан файл:
```text
app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
```
Реализовано 22 теста.
Проверены:
1. полное преобразование `DzengiExchangeInfoSymbol → Instrument`;
2. преобразование полного `DzengiExchangeInfoResponse`;
3. возврат `tuple[Instrument, ...]`;
4. сохранение порядка инструментов;
5. точное преобразование чисел в `Decimal`;
6. извлечение `LOT_SIZE`;
7. извлечение `MIN_NOTIONAL`;
8. работа при отсутствии filters;
9. работа при отсутствии optional numeric values;
10. игнорирование неизвестных filters;
11. преобразование пустого `asset_type` в `None`;
12. преобразование пустого `country` в `None`;
13. преобразование пустого `sector` в `None`;
14. преобразование пустого `industry` в `None`;
15. преобразование пустого `trading_hours` в `None`;
16. очистка внешних пробелов optional text;
17. сохранение порядка `market_modes`;
18. сохранение порядка `order_types`;
19. отклонение нескольких `LOT_SIZE`;
20. отклонение нескольких `MIN_NOTIONAL`;
21. отклонение неконечных числовых значений;
22. immutable-поведение итогового `Instrument`.
---
## 20. Выполненные проверки
### Проверка 1 — unit-тесты mapper
Команда:
```bash
python -m pytest \
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py \
-q
```
Результат:
```text
22 passed in 0.03s
```
Статус:
```text
PASSED
```
---
### Проверка 2 — Python compilation
Команда:
```bash
python -m py_compile \
src/market_data/acquisition/exceptions.py \
src/market_data/acquisition/adapters/dzengi/mapper.py \
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
```
Результат:
```text
Команда завершилась без ошибок и без вывода.
```
Статус:
```text
PASSED
```
---
### Проверка 3 — полный pipeline на реальном Dzengi sample
Проверена цепочка:
```text
JSON
Schema Validation
Parser
Value Validation
Mapper
Instrument
```
Результат:
```text
Instruments: 51
```
Первый инструмент:
```text
Instrument(
symbol='ETH/EUR_LEVERAGE',
name='ETH/EUR',
status='TRADING',
base_asset='ETH',
quote_asset='EUR',
asset_type='CRYPTOCURRENCY',
market_type='LEVERAGE',
market_modes=('REGULAR',),
order_types=('LIMIT', 'MARKET', 'STOP'),
base_asset_precision=3,
quote_asset_precision=3,
tick_size=Decimal('0.01'),
tick_value=Decimal('18.3415'),
step_size=Decimal('0.001'),
min_qty=Decimal('0.001'),
max_qty=Decimal('1000'),
min_notional=Decimal('2'),
country=None,
sector=None,
industry=None,
trading_hours='UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -;Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -',
)
```
Подтверждено:
```text
51 инструмент успешно прошёл полный pipeline.
```
Статус:
```text
PASSED
```
---
### Проверка 4 — полный набор тестов проекта
Команда:
```bash
python -m pytest -q
```
Результат:
```text
106 passed in 0.06s
```
Статус:
```text
PASSED
```
---
### Проверка 5 — отсутствие production-интеграции
Команда:
```bash
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "map_dzengi_symbol_to_instrument|map_dzengi_exchange_info_to_instruments|InstrumentReferenceMappingError" \
src tests
```
Подтверждено, что mapper используется только в:
```text
src/market_data/acquisition/adapters/dzengi/mapper.py
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
```
Новая ошибка объявлена в:
```text
src/market_data/acquisition/exceptions.py
```
Не обнаружено подключения к:
```text
src/integrations/exchange/*
src/telegram/*
src/trading/*
```
Статус:
```text
PASSED
```
---
## 21. Архитектура после Build 006
После завершения Build 006 реализована следующая часть новой подсистемы:
```text
market_data/
└── acquisition/
├── exceptions.py
├── models/
│ └── instrument.py
│ └── Instrument
├── validation/
│ ├── schema.py
│ │ └── validate_exchange_info_schema()
│ │
│ └── values.py
│ └── validate_exchange_info_values()
└── adapters/
└── dzengi/
├── models.py
│ ├── DzengiExchangeInfoResponse
│ ├── DzengiExchangeInfoPayload
│ ├── DzengiExchangeInfoSymbol
│ ├── DzengiLotSizeFilter
│ ├── DzengiMinNotionalFilter
│ └── DzengiUnknownFilter
├── parser.py
│ └── parse_exchange_info()
└── mapper.py
├── map_dzengi_symbol_to_instrument()
└── map_dzengi_exchange_info_to_instruments()
```
Рабочая цепочка:
```text
Raw JSON document
validate_exchange_info_schema()
ValidatedExchangeInfoDocument
parse_exchange_info()
DzengiExchangeInfoResponse
validate_exchange_info_values()
map_dzengi_exchange_info_to_instruments()
tuple[Instrument, ...]
```
---
## 22. Влияние на legacy-систему
Build 006 не подключён к существующим компонентам:
```text
ExchangeService
ExchangeSymbol
SymbolValidationResult
Telegram UI
AutoTrade
Market Stream
Market Data Runner
Execution Quality
```
Поэтому старый бот продолжает работать по прежнему пути.
Новая подсистема строится параллельно и пока не заменяет legacy-реализацию.
Это соответствует стратегии миграции:
```text
Сначала построить и протестировать новый путь.
Затем подключать его постепенно.
Старый рабочий путь не удалять до подтверждения новой реализации.
```
---
## 23. Классификация изменений
| Изменение | Классификация |
|---|---|
| Создание Dzengi mapper | Обязательное архитектурное изменение |
| Преобразование raw-модели в `Instrument` | Обязательное архитектурное изменение |
| Преобразование чисел в `Decimal` | Обязательное архитектурное изменение |
| Извлечение `LOT_SIZE` | Обязательное архитектурное изменение |
| Извлечение `MIN_NOTIONAL` | Обязательное архитектурное изменение |
| Защита от дублирующихся известных filters | Улучшение надёжности |
| Пустые optional text → `None` | Предметная нормализация |
| Игнорирование unknown filters в `Instrument` | Разграничение source/domain layers |
| Новая `InstrumentReferenceMappingError` | Улучшение диагностируемости |
| Изменение production-поведения | Отсутствует |
---
## 24. Итог Build 006
Build 006 завершён успешно.
Реализовано:
```text
DzengiExchangeInfoSymbol
Instrument
```
и:
```text
DzengiExchangeInfoResponse
tuple[Instrument, ...]
```
Подтверждено:
```text
22 mapper tests passed
106 total project tests passed
51 real Dzengi instruments successfully mapped
Python compilation passed
No production integration detected
Legacy bot behavior unchanged
```
Итоговый статус:
```text
BUILD 006 — COMPLETE
```
---
## 25. Следующий этап
Следующий этап необходимо определить по утверждённому плану миграции.
Новый pipeline уже умеет преобразовывать сохранённый JSON-документ в:
```text
tuple[Instrument, ...]
```
Следующий Build должен добавить следующий минимальный слой, не подключая сразу новый путь ко всем legacy-потребителям и не изменяя работающий `ExchangeService` без совместимого переходного контракта.