feat: add market data architecture and complete migration through build 039

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit a996f2f797
443 changed files with 80452 additions and 1335 deletions

View File

@@ -0,0 +1,775 @@
# 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 001003 стало безопасно реализовать 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`.