775 lines
18 KiB
Markdown
775 lines
18 KiB
Markdown
# Dzentra — Instrument Reference Data Migration
|
||
|
||
## Build 004 — Dzengi exchangeInfo Parser
|
||
|
||
**Статус:** Завершён
|
||
**Подсистема:** Market Data Acquisition
|
||
**Компонент:** Dzengi Adapter / exchangeInfo Parser
|
||
**Проект:** Dzentra
|
||
**Язык:** Русский
|
||
**Python:** 3.12
|
||
|
||
---
|
||
|
||
## 1. Цель Build 004
|
||
|
||
Цель Build 004 — реализовать parser для ответа Dzengi `exchangeInfo`, который преобразует уже структурно проверенный документ из Build 003 в типизированные транспортные модели Dzengi, созданные в Build 002.
|
||
|
||
Целевая цепочка после завершения Build 004:
|
||
|
||
```text
|
||
Raw JSON document
|
||
↓
|
||
validate_exchange_info_schema()
|
||
↓
|
||
ValidatedExchangeInfoDocument
|
||
↓
|
||
parse_exchange_info()
|
||
↓
|
||
DzengiExchangeInfoResponse
|
||
```
|
||
|
||
Build 004 не подключает новую реализацию к существующему `ExchangeService` и не изменяет поведение работающего бота.
|
||
|
||
---
|
||
|
||
## 2. Почему Build 004 выполняется именно сейчас
|
||
|
||
До начала Build 004 уже были завершены необходимые предыдущие этапы.
|
||
|
||
### Build 001 — внутренняя модель Instrument
|
||
|
||
Создана внутренняя типизированная модель:
|
||
|
||
```text
|
||
Instrument
|
||
```
|
||
|
||
Она представляет инструмент внутри новой архитектуры Dzentra и не зависит от формата конкретной биржи.
|
||
|
||
### Build 002 — транспортные модели Dzengi
|
||
|
||
Созданы типизированные модели сырого ответа `exchangeInfo`:
|
||
|
||
```text
|
||
DzengiExchangeInfoResponse
|
||
DzengiExchangeInfoPayload
|
||
DzengiExchangeInfoSymbol
|
||
DzengiRateLimit
|
||
DzengiInstrumentFilter
|
||
DzengiLotSizeFilter
|
||
DzengiMinNotionalFilter
|
||
DzengiUnknownFilter
|
||
```
|
||
|
||
### Build 003 — структурная валидация exchangeInfo
|
||
|
||
Создан слой проверки структуры сырого JSON-документа:
|
||
|
||
```text
|
||
validate_exchange_info_schema()
|
||
```
|
||
|
||
Его результат:
|
||
|
||
```text
|
||
ValidatedExchangeInfoDocument
|
||
```
|
||
|
||
Таким образом, только после Build 001–003 стало безопасно реализовать parser, не смешивая:
|
||
|
||
- проверку структуры JSON;
|
||
- разбор транспортного ответа;
|
||
- предметное преобразование в `Instrument`;
|
||
- сетевой REST-доступ;
|
||
- кэширование;
|
||
- legacy-совместимость.
|
||
|
||
---
|
||
|
||
## 3. Реализованная архитектурная цепочка
|
||
|
||
На текущем этапе действует следующая архитектура:
|
||
|
||
```text
|
||
Raw Dzengi JSON
|
||
↓
|
||
Schema Validation
|
||
↓
|
||
ValidatedExchangeInfoDocument
|
||
↓
|
||
Dzengi Parser
|
||
↓
|
||
DzengiExchangeInfoResponse
|
||
```
|
||
|
||
Полная целевая цепочка миграции пока ещё не завершена:
|
||
|
||
```text
|
||
Dzengi REST API
|
||
↓
|
||
Dzengi REST Adapter
|
||
↓
|
||
Schema Validation
|
||
↓
|
||
Parser
|
||
↓
|
||
Dzengi Raw Models
|
||
↓
|
||
Mapper
|
||
↓
|
||
Instrument
|
||
↓
|
||
Instrument Handler
|
||
↓
|
||
Instrument Feed
|
||
↓
|
||
Market Data Acquisition Service
|
||
↓
|
||
Compatibility Layer
|
||
↓
|
||
ExchangeService
|
||
↓
|
||
Existing Consumers
|
||
```
|
||
|
||
Build 004 реализует только участок:
|
||
|
||
```text
|
||
ValidatedExchangeInfoDocument
|
||
↓
|
||
Parser
|
||
↓
|
||
Dzengi Raw Models
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Файлы Build 004
|
||
|
||
### Реализован
|
||
|
||
```text
|
||
app/src/market_data/acquisition/adapters/dzengi/parser.py
|
||
```
|
||
|
||
### Использованы существующие файлы
|
||
|
||
```text
|
||
app/src/market_data/acquisition/adapters/dzengi/models.py
|
||
app/src/market_data/acquisition/validation/schema.py
|
||
app/src/market_data/acquisition/exceptions.py
|
||
```
|
||
|
||
### Добавлены тесты
|
||
|
||
```text
|
||
app/tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Публичный контракт parser
|
||
|
||
Основной публичный вход Build 004:
|
||
|
||
```python
|
||
def parse_exchange_info(
|
||
document: ValidatedExchangeInfoDocument,
|
||
) -> DzengiExchangeInfoResponse:
|
||
...
|
||
```
|
||
|
||
Parser намеренно не принимает произвольный сырой `dict`.
|
||
|
||
Корректная последовательность вызовов:
|
||
|
||
```python
|
||
validated = validate_exchange_info_schema(raw_document)
|
||
response = parse_exchange_info(validated)
|
||
```
|
||
|
||
Это обеспечивает явное разделение ответственности между Build 003 и Build 004.
|
||
|
||
---
|
||
|
||
## 6. Разделение ответственности
|
||
|
||
### Build 003 — Schema Validation
|
||
|
||
Отвечает за структурную корректность документа:
|
||
|
||
- корневой объект должен быть JSON-объектом;
|
||
- `payload`, если используется wrapped-формат, должен быть объектом;
|
||
- `symbols` должен быть массивом;
|
||
- каждый элемент `symbols` должен быть объектом;
|
||
- `filters` должен быть массивом;
|
||
- другие структурные ограничения проверяются до parser.
|
||
|
||
Build 003 не создаёт транспортные модели Dzengi.
|
||
|
||
### Build 004 — Parser
|
||
|
||
Отвечает за преобразование структурно проверенного документа в:
|
||
|
||
```text
|
||
DzengiExchangeInfoResponse
|
||
```
|
||
|
||
и вложенные типизированные транспортные модели.
|
||
|
||
Parser не отвечает за:
|
||
|
||
- HTTP-запросы;
|
||
- кэширование;
|
||
- предметную модель `Instrument`;
|
||
- нормализацию символа для бизнес-логики;
|
||
- runtime-статус инструмента;
|
||
- UI;
|
||
- legacy-совместимость.
|
||
|
||
---
|
||
|
||
## 7. Поддерживаемые форматы exchangeInfo
|
||
|
||
Архитектура поддерживает два формата ответа.
|
||
|
||
### Unwrapped
|
||
|
||
```json
|
||
{
|
||
"timezone": "UTC",
|
||
"serverTime": 1783537921471,
|
||
"rateLimits": [],
|
||
"exchangeFilters": [],
|
||
"symbols": []
|
||
}
|
||
```
|
||
|
||
После Build 003:
|
||
|
||
```text
|
||
is_wrapped: False
|
||
status: None
|
||
correlation_id: None
|
||
```
|
||
|
||
После Build 004 документ преобразуется в:
|
||
|
||
```text
|
||
DzengiExchangeInfoResponse
|
||
└── payload: DzengiExchangeInfoPayload
|
||
```
|
||
|
||
### Wrapped
|
||
|
||
```json
|
||
{
|
||
"status": "OK",
|
||
"correlationId": "2",
|
||
"payload": {
|
||
"timezone": "UTC",
|
||
"serverTime": 1783537921471,
|
||
"rateLimits": [],
|
||
"exchangeFilters": [],
|
||
"symbols": []
|
||
}
|
||
}
|
||
```
|
||
|
||
После Build 003:
|
||
|
||
```text
|
||
is_wrapped: True
|
||
status: OK
|
||
correlation_id: 2
|
||
```
|
||
|
||
После Build 004 метаданные оболочки сохраняются в:
|
||
|
||
```text
|
||
DzengiExchangeInfoResponse.status
|
||
DzengiExchangeInfoResponse.correlation_id
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Разбор инструмента
|
||
|
||
Каждый элемент массива `symbols` преобразуется в:
|
||
|
||
```text
|
||
DzengiExchangeInfoSymbol
|
||
```
|
||
|
||
Поддерживаются следующие поля:
|
||
|
||
```text
|
||
symbol
|
||
name
|
||
status
|
||
|
||
asset_type
|
||
|
||
base_asset
|
||
base_asset_precision
|
||
|
||
quote_asset
|
||
quote_asset_id
|
||
quote_precision
|
||
|
||
order_types
|
||
filters
|
||
|
||
market_modes
|
||
market_type
|
||
|
||
country
|
||
sector
|
||
industry
|
||
trading_hours
|
||
|
||
tick_size
|
||
tick_value
|
||
|
||
trading_fee
|
||
exchange_fee
|
||
|
||
long_rate
|
||
short_rate
|
||
swap_charge_interval
|
||
|
||
min_sl_gap
|
||
max_sl_gap
|
||
min_tp_gap
|
||
max_tp_gap
|
||
```
|
||
|
||
На этом этапе поля сохраняют транспортную семантику Dzengi и ещё не преобразуются в предметную модель `Instrument`.
|
||
|
||
---
|
||
|
||
## 9. Разбор filters
|
||
|
||
Parser преобразует известные типы фильтров в отдельные типизированные модели.
|
||
|
||
### LOT_SIZE
|
||
|
||
Преобразуется в:
|
||
|
||
```text
|
||
DzengiLotSizeFilter
|
||
```
|
||
|
||
Поля:
|
||
|
||
```text
|
||
filter_type
|
||
min_qty
|
||
max_qty
|
||
step_size
|
||
```
|
||
|
||
Пример:
|
||
|
||
```text
|
||
DzengiLotSizeFilter(
|
||
filter_type='LOT_SIZE',
|
||
min_qty='0.0001',
|
||
max_qty='1000',
|
||
step_size='0.0001',
|
||
)
|
||
```
|
||
|
||
### MIN_NOTIONAL
|
||
|
||
Преобразуется в:
|
||
|
||
```text
|
||
DzengiMinNotionalFilter
|
||
```
|
||
|
||
Поле:
|
||
|
||
```text
|
||
min_notional
|
||
```
|
||
|
||
### Неизвестные фильтры
|
||
|
||
Неизвестный `filterType` не отбрасывается автоматически.
|
||
|
||
Он преобразуется в:
|
||
|
||
```text
|
||
DzengiUnknownFilter
|
||
```
|
||
|
||
Это позволяет сохранить неизвестные скалярные поля транспортного ответа без добавления неподтверждённой предметной семантики.
|
||
|
||
---
|
||
|
||
## 10. Числовые значения
|
||
|
||
Build 004 сохраняет важное разделение между транспортным и предметным слоями.
|
||
|
||
Например, значения фильтра:
|
||
|
||
```json
|
||
{
|
||
"minQty": "0.0001",
|
||
"maxQty": "1000",
|
||
"stepSize": "0.0001"
|
||
}
|
||
```
|
||
|
||
в транспортной модели остаются:
|
||
|
||
```text
|
||
min_qty='0.0001'
|
||
max_qty='1000'
|
||
step_size='0.0001'
|
||
```
|
||
|
||
Parser не выполняет преждевременное преобразование этих значений в `float`.
|
||
|
||
Преобразование в точный предметный числовой тип должно выполняться на следующем архитектурном этапе при построении `Instrument`.
|
||
|
||
Это позволяет избежать потери точности и сохраняет исходную семантику ответа Dzengi.
|
||
|
||
---
|
||
|
||
## 11. Обработка ошибок
|
||
|
||
Для ошибок parser используется отдельное исключение:
|
||
|
||
```text
|
||
InstrumentReferenceParseError
|
||
```
|
||
|
||
Оно объявлено в:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/exceptions.py
|
||
```
|
||
|
||
Иерархия:
|
||
|
||
```text
|
||
MarketDataAcquisitionError
|
||
└── InstrumentReferenceParseError
|
||
```
|
||
|
||
`InstrumentReferenceParseError` используется только:
|
||
|
||
- в `parser.py`;
|
||
- в unit-тестах parser.
|
||
|
||
На момент завершения Build 004 это исключение не используется:
|
||
|
||
- `ExchangeService`;
|
||
- Telegram UI;
|
||
- runtime-кодом;
|
||
- автоторговлей;
|
||
- существующими production-потребителями.
|
||
|
||
---
|
||
|
||
## 12. Классификация изменений
|
||
|
||
| Изменение | Классификация | Влияние на поведение |
|
||
|---|---|---|
|
||
| Реализация `exchangeInfo` parser | Обязательное архитектурное изменение | Нет |
|
||
| Типизированное преобразование symbols | Обязательное архитектурное изменение | Нет |
|
||
| Типизированный разбор известных filters | Обязательное архитектурное изменение | Нет |
|
||
| Сохранение неизвестных filters | Улучшение надёжности | Нет |
|
||
| Отдельное `InstrumentReferenceParseError` | Улучшение надёжности | Нет |
|
||
| Сохранение числовых строк без преобразования в `float` | Улучшение надёжности | Нет |
|
||
| Поддержка wrapped и unwrapped форматов | Улучшение надёжности | Нет |
|
||
|
||
Изменений поведения работающего бота в Build 004 нет.
|
||
|
||
---
|
||
|
||
## 13. Обратная совместимость
|
||
|
||
Build 004 не изменяет существующие публичные интерфейсы.
|
||
|
||
Без изменений продолжают работать:
|
||
|
||
```text
|
||
ExchangeService.get_exchange_symbols()
|
||
ExchangeService.validate_symbol()
|
||
ExchangeService.get_symbol_runtime_status()
|
||
ExchangeService.get_symbol_market_status()
|
||
```
|
||
|
||
Также без изменений остаются:
|
||
|
||
```text
|
||
ExchangeSymbol
|
||
SymbolValidationResult
|
||
normalize_symbol()
|
||
symbol_candidates()
|
||
```
|
||
|
||
Новая реализация parser пока не подключена к существующему `ExchangeService`.
|
||
|
||
Следовательно:
|
||
|
||
- Telegram UI не изменён;
|
||
- автоторговля не изменена;
|
||
- runtime-проверки символа не изменены;
|
||
- существующий кэш инструментов не изменён;
|
||
- старые импорты продолжают работать;
|
||
- production-поведение бота сохранено.
|
||
|
||
---
|
||
|
||
## 14. Выполненные проверки
|
||
|
||
### Проверка 1 — unit-тесты parser
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m pytest \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py \
|
||
-q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
19 passed in 0.02s
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASS
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 2 — синтаксическая компиляция
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m py_compile \
|
||
src/market_data/acquisition/exceptions.py \
|
||
src/market_data/acquisition/adapters/dzengi/parser.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
Команда завершена без ошибок.
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASS
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 3 — ручная цепочка schema → parser
|
||
|
||
Была проверена цепочка:
|
||
|
||
```text
|
||
raw dict
|
||
↓
|
||
validate_exchange_info_schema()
|
||
↓
|
||
ValidatedExchangeInfoDocument
|
||
↓
|
||
parse_exchange_info()
|
||
↓
|
||
DzengiExchangeInfoResponse
|
||
```
|
||
|
||
Полученный результат:
|
||
|
||
```text
|
||
Symbol: BTC/USD_LEVERAGE
|
||
Status: TRADING
|
||
Asset type: CRYPTOCURRENCY
|
||
Base asset: BTC
|
||
Quote asset: USD
|
||
Tick size: 0.05
|
||
Filters: (DzengiLotSizeFilter(filter_type='LOT_SIZE', min_qty='0.0001', max_qty='1000', step_size='0.0001'),)
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASS
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 4 — полный набор тестов проекта
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m pytest -q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
50 passed in 0.05s
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASS
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 5 — контроль зависимостей parser
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
grep -RIn \
|
||
--exclude-dir="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
-E "parse_exchange_info|_parse_exchange_info|DzengiExchangeInfoParseError" \
|
||
src tests
|
||
```
|
||
|
||
Подтверждено:
|
||
|
||
- `parse_exchange_info()` определён в новом `parser.py`;
|
||
- используется только unit-тестами нового parser;
|
||
- не используется существующим `ExchangeService`;
|
||
- не используется UI;
|
||
- не используется runtime;
|
||
- не используется автоторговлей.
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASS
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 6 — контроль использования parse-ошибки
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
grep -RIn \
|
||
--exclude-dir="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
-E "InstrumentReferenceParseError" \
|
||
src tests
|
||
```
|
||
|
||
Подтверждено:
|
||
|
||
- исключение объявлено в `exceptions.py`;
|
||
- используется parser;
|
||
- используется unit-тестами parser;
|
||
- не подключено к production-потребителям.
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASS
|
||
```
|
||
|
||
---
|
||
|
||
## 15. Итог Build 004
|
||
|
||
Build 004 успешно завершён.
|
||
|
||
Реализован типизированный parser:
|
||
|
||
```text
|
||
ValidatedExchangeInfoDocument
|
||
↓
|
||
parse_exchange_info()
|
||
↓
|
||
DzengiExchangeInfoResponse
|
||
```
|
||
|
||
Подтверждено:
|
||
|
||
```text
|
||
Build 001 — Instrument domain model PASS
|
||
Build 002 — Dzengi raw transport models PASS
|
||
Build 003 — exchangeInfo schema validation PASS
|
||
Build 004 — Dzengi exchangeInfo parser PASS
|
||
```
|
||
|
||
Общий результат тестов после завершения Build 004:
|
||
|
||
```text
|
||
50 passed in 0.05s
|
||
```
|
||
|
||
Работающий бот не затронут.
|
||
|
||
---
|
||
|
||
## 16. Условие завершения Build 004
|
||
|
||
Build 004 считается завершённым, поскольку выполнены все условия:
|
||
|
||
- parser реализован;
|
||
- parser принимает только `ValidatedExchangeInfoDocument`;
|
||
- parser возвращает `DzengiExchangeInfoResponse`;
|
||
- основные поля инструмента сохраняются;
|
||
- известные filters типизированы;
|
||
- неизвестные filters сохраняются;
|
||
- ошибки parser имеют отдельный тип;
|
||
- unit-тесты проходят;
|
||
- полный набор тестов проходит;
|
||
- production-потребители не изменены;
|
||
- обратная совместимость сохранена.
|
||
|
||
---
|
||
|
||
## 17. Следующий этап
|
||
|
||
Следующий этап миграции:
|
||
|
||
```text
|
||
Build 005 — Проверка значений Instrument Reference Data
|
||
```
|
||
|
||
Его задача — преобразовать:
|
||
|
||
```text
|
||
DzengiExchangeInfoSymbol
|
||
↓
|
||
Mapper
|
||
↓
|
||
Instrument
|
||
```
|
||
|
||
При этом Build 005 должен:
|
||
|
||
- использовать модель `Instrument`, созданную в Build 001;
|
||
- использовать транспортную модель `DzengiExchangeInfoSymbol` из Build 002;
|
||
- не выполнять HTTP-запросы;
|
||
- не заниматься кэшированием;
|
||
- не подключаться к `ExchangeService`;
|
||
- не менять production-поведение бота;
|
||
- явно определить правила преобразования числовых значений в `Decimal`;
|
||
- явно определить соответствие полей Dzengi полям внутренней модели `Instrument`;
|
||
- отдельно классифицировать любые потенциальные изменения поведения.
|
||
|
||
До написания кода Build 005 необходимо сначала проанализировать существующие модели и контракты, относящиеся к преобразованию `DzengiExchangeInfoSymbol` в `Instrument`. |