24 KiB
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:
DzengiExchangeInfoSymbol
↓
Instrument
Также реализовано преобразование полного ответа exchangeInfo:
DzengiExchangeInfoResponse
↓
tuple[Instrument, ...]
После завершения Build 006 pipeline Instrument Reference Data выглядит следующим образом:
Dzengi raw JSON
↓
Schema Validation
↓
Parser
↓
Dzengi Raw Models
↓
Value Validation
↓
Mapper
↓
Instrument
Build 006 не подключает новый mapper к существующему ExchangeService и не меняет поведение работающего бота.
2. Почему Build 006 выполняется именно сейчас
До начала Build 006 были завершены необходимые предыдущие этапы:
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 отвечает только за преобразование:
DzengiExchangeInfoSymbol
↓
Instrument
и:
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 изменены:
app/src/market_data/acquisition/exceptions.py
app/src/market_data/acquisition/adapters/dzengi/mapper.py
Создан тестовый файл:
app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
Не изменялись:
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
В файл:
app/src/market_data/acquisition/exceptions.py
добавлена ошибка:
class InstrumentReferenceMappingError(MarketDataAcquisitionError):
pass
Итоговая иерархия ошибок Instrument Reference Data:
MarketDataAcquisitionError
├── InstrumentReferenceSchemaError
├── InstrumentReferenceParseError
├── InstrumentReferenceValueError
└── InstrumentReferenceMappingError
InstrumentReferenceMappingError обозначает ошибки, возникшие непосредственно при преобразовании source-specific raw-модели во внутреннюю модель Instrument.
Примеры:
несколько LOT_SIZE filters
несколько MIN_NOTIONAL filters
невозможность преобразования значения в Decimal
неконечное числовое значение
6. Публичный контракт mapper
В файле:
app/src/market_data/acquisition/adapters/dzengi/mapper.py
реализованы две публичные функции.
6.1. Преобразование одного инструмента
def map_dzengi_symbol_to_instrument(
symbol: DzengiExchangeInfoSymbol,
) -> Instrument:
...
Функция преобразует один объект:
DzengiExchangeInfoSymbol
в один объект:
Instrument
6.2. Преобразование полного ответа exchangeInfo
def map_dzengi_exchange_info_to_instruments(
response: DzengiExchangeInfoResponse,
) -> tuple[Instrument, ...]:
...
Функция преобразует все элементы:
DzengiExchangeInfoResponse.payload.symbols
в immutable-последовательность:
tuple[Instrument, ...]
Порядок инструментов сохраняется.
7. Правила преобразования полей
Реализовано следующее соответствие:
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:
tick_size
tick_value
step_size
min_qty
max_qty
min_notional
Используется преобразование:
Decimal(str(value))
Это позволяет избежать дополнительной двоичной погрешности при непосредственном преобразовании float в Decimal.
Примеры:
0.01
→ Decimal("0.01")
"0.001"
→ Decimal("0.001")
0
→ Decimal("0")
0.00000001
→ Decimal("1E-8")
Если исходное значение отсутствует:
None → None
9. Защита Decimal-преобразования
Mapper предполагает, что перед его вызовом уже выполнена:
Value Validation
Однако публичная функция mapper может быть вызвана напрямую.
Поэтому непосредственно в mapper оставлена локальная защита преобразования в Decimal.
Если значение невозможно преобразовать:
"not-a-number"
возникает:
InstrumentReferenceMappingError
Также отклоняются неконечные значения:
NaN
Infinity
-Infinity
Это не повторение полной Value Validation. Mapper защищает только собственную непосредственную обязанность — корректное создание Decimal.
10. Извлечение LOT_SIZE
Из:
DzengiLotSizeFilter
извлекаются:
min_qty
max_qty
step_size
Пример исходного фильтра:
DzengiLotSizeFilter(
filter_type="LOT_SIZE",
min_qty="0.001",
max_qty="1000",
step_size="0.001",
)
Результат:
Instrument.min_qty
= Decimal("0.001")
Instrument.max_qty
= Decimal("1000")
Instrument.step_size
= Decimal("0.001")
Если LOT_SIZE отсутствует:
min_qty → None
max_qty → None
step_size → None
11. Извлечение MIN_NOTIONAL
Из:
DzengiMinNotionalFilter
извлекается:
min_notional
Пример исходного фильтра:
DzengiMinNotionalFilter(
filter_type="MIN_NOTIONAL",
min_notional="2",
)
Результат:
Instrument.min_notional
= Decimal("2")
Если MIN_NOTIONAL отсутствует:
min_notional → None
12. Неизвестные filters
Неизвестные фильтры представлены raw-моделью:
DzengiUnknownFilter
Mapper не переносит их во внутреннюю модель Instrument и не выбрасывает из-за них ошибку.
Например:
DzengiUnknownFilter(
filter_type="FUTURE_FILTER",
fields=(
("enabled", True),
("limit", 10),
),
)
не препятствует созданию Instrument.
Это сознательное архитектурное решение:
Raw Models
сохраняют неизвестные source-specific данные
Instrument
содержит только известные канонические поля Dzentra
Неизвестные данные не теряются на транспортном уровне, но не загрязняют source-independent модель.
13. Защита от дублирующихся известных filters
Mapper не допускает неоднозначного выбора значения.
Если один инструмент содержит несколько фильтров:
LOT_SIZE
возникает:
InstrumentReferenceMappingError
Аналогичное правило действует для нескольких фильтров:
MIN_NOTIONAL
Mapper не выбирает молча первый или последний фильтр, поскольку это могло бы привести к использованию неверных торговых ограничений.
14. Нормализация необязательных текстовых полей
Для следующих optional-полей применяется предметная нормализация:
asset_type
country
sector
industry
trading_hours
Правила:
None
→ None
""
→ None
" "
→ None
" DE "
→ "DE"
Это соответствует фактическому поведению Dzengi API, где некоторые справочные поля могут присутствовать как пустые строки.
Например:
country=""
sector=""
industry=""
преобразуются во внутреннюю модель как:
country=None
sector=None
industry=None
15. Обязательные строки не нормализуются mapper
Следующие поля переносятся без изменения:
symbol
name
status
base_asset
quote_asset
market_type
Причины:
- parser сохраняет транспортное значение;
- Value Validation уже проверяет обязательность и непустоту;
- mapper не должен незаметно менять идентификаторы или статусы источника.
16. Сохранение порядка последовательностей
Mapper сохраняет исходный порядок:
market_modes
order_types
Например:
("REGULAR", "CLOSE_ONLY", "EXTENDED")
остаётся:
("REGULAR", "CLOSE_ONLY", "EXTENDED")
А:
("MARKET", "LIMIT", "STOP")
остаётся:
("MARKET", "LIMIT", "STOP")
Mapper не выполняет:
sorting
deduplication
set conversion
17. Поля Dzengi, которые сознательно не входят в Instrument
Следующие source-specific данные сохранены в DzengiExchangeInfoSymbol, но не переносятся в текущую модель Instrument:
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
Они не потеряны на транспортном уровне.
В дальнейшем эти данные могут использоваться отдельными предметными контрактами:
fees
financing
execution constraints
provider-specific identifiers
Build 006 не смешивает эти области с базовой моделью Instrument.
18. Immutable-результат
Модель:
Instrument
объявлена как:
@dataclass(frozen=True, slots=True)
Тестами подтверждено, что попытка изменения уже созданного объекта приводит к:
FrozenInstanceError
Это обеспечивает стабильность справочного value object после mapping.
19. Реализованные тестовые сценарии
Создан файл:
app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
Реализовано 22 теста.
Проверены:
- полное преобразование
DzengiExchangeInfoSymbol → Instrument; - преобразование полного
DzengiExchangeInfoResponse; - возврат
tuple[Instrument, ...]; - сохранение порядка инструментов;
- точное преобразование чисел в
Decimal; - извлечение
LOT_SIZE; - извлечение
MIN_NOTIONAL; - работа при отсутствии filters;
- работа при отсутствии optional numeric values;
- игнорирование неизвестных filters;
- преобразование пустого
asset_typeвNone; - преобразование пустого
countryвNone; - преобразование пустого
sectorвNone; - преобразование пустого
industryвNone; - преобразование пустого
trading_hoursвNone; - очистка внешних пробелов optional text;
- сохранение порядка
market_modes; - сохранение порядка
order_types; - отклонение нескольких
LOT_SIZE; - отклонение нескольких
MIN_NOTIONAL; - отклонение неконечных числовых значений;
- immutable-поведение итогового
Instrument.
20. Выполненные проверки
Проверка 1 — unit-тесты mapper
Команда:
python -m pytest \
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py \
-q
Результат:
22 passed in 0.03s
Статус:
PASSED
Проверка 2 — Python compilation
Команда:
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
Результат:
Команда завершилась без ошибок и без вывода.
Статус:
PASSED
Проверка 3 — полный pipeline на реальном Dzengi sample
Проверена цепочка:
JSON
↓
Schema Validation
↓
Parser
↓
Value Validation
↓
Mapper
↓
Instrument
Результат:
Instruments: 51
Первый инструмент:
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 -',
)
Подтверждено:
51 инструмент успешно прошёл полный pipeline.
Статус:
PASSED
Проверка 4 — полный набор тестов проекта
Команда:
python -m pytest -q
Результат:
106 passed in 0.06s
Статус:
PASSED
Проверка 5 — отсутствие production-интеграции
Команда:
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "map_dzengi_symbol_to_instrument|map_dzengi_exchange_info_to_instruments|InstrumentReferenceMappingError" \
src tests
Подтверждено, что mapper используется только в:
src/market_data/acquisition/adapters/dzengi/mapper.py
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
Новая ошибка объявлена в:
src/market_data/acquisition/exceptions.py
Не обнаружено подключения к:
src/integrations/exchange/*
src/telegram/*
src/trading/*
Статус:
PASSED
21. Архитектура после Build 006
После завершения Build 006 реализована следующая часть новой подсистемы:
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()
Рабочая цепочка:
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 не подключён к существующим компонентам:
ExchangeService
ExchangeSymbol
SymbolValidationResult
Telegram UI
AutoTrade
Market Stream
Market Data Runner
Execution Quality
Поэтому старый бот продолжает работать по прежнему пути.
Новая подсистема строится параллельно и пока не заменяет legacy-реализацию.
Это соответствует стратегии миграции:
Сначала построить и протестировать новый путь.
Затем подключать его постепенно.
Старый рабочий путь не удалять до подтверждения новой реализации.
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 завершён успешно.
Реализовано:
DzengiExchangeInfoSymbol
↓
Instrument
и:
DzengiExchangeInfoResponse
↓
tuple[Instrument, ...]
Подтверждено:
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
Итоговый статус:
BUILD 006 — COMPLETE
25. Следующий этап
Следующий этап необходимо определить по утверждённому плану миграции.
Новый pipeline уже умеет преобразовывать сохранённый JSON-документ в:
tuple[Instrument, ...]
Следующий Build должен добавить следующий минимальный слой, не подключая сразу новый путь ко всем legacy-потребителям и не изменяя работающий ExchangeService без совместимого переходного контракта.