1071 lines
24 KiB
Markdown
1071 lines
24 KiB
Markdown
# 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` без совместимого переходного контракта. |