851 lines
19 KiB
Markdown
851 lines
19 KiB
Markdown
# Dzentra — Instrument Reference Data Migration
|
||
|
||
## Build 003 — структурная валидация `exchangeInfo`
|
||
|
||
**Статус:** Завершён
|
||
**Подсистема:** Market Data Acquisition
|
||
**Область:** Instrument Reference Data
|
||
**Проект:** Dzentra
|
||
**Язык реализации:** Python 3.12
|
||
|
||
---
|
||
|
||
## 1. Цель Build 003
|
||
|
||
Цель Build 003 — создать отдельный слой структурной валидации сырого JSON-документа `exchangeInfo` до его преобразования в raw-модели Dzengi.
|
||
|
||
После завершения Build 003 формируется следующий архитектурный поток:
|
||
|
||
```text
|
||
JSON response
|
||
↓
|
||
Schema validation
|
||
↓
|
||
Parser
|
||
↓
|
||
Dzengi Raw Models
|
||
```
|
||
|
||
В рамках этого Build реализована только проверка формы и структуры входных данных.
|
||
|
||
Schema validation не выполняет:
|
||
|
||
- преобразование JSON в raw-модели Dzengi;
|
||
- преобразование camelCase в snake_case;
|
||
- преобразование строковых чисел в `Decimal`, `float` или `int`;
|
||
- нормализацию символов;
|
||
- проверку допустимости числовых значений;
|
||
- проверку торговой семантики статусов;
|
||
- создание внутренней модели `Instrument`;
|
||
- фильтрацию или отбрасывание инструментов.
|
||
|
||
---
|
||
|
||
## 2. Почему Build 003 выполняется перед parser
|
||
|
||
После Build 002 уже существуют типизированные raw-модели ответа Dzengi.
|
||
|
||
Следующий этап — parser, однако parser не должен одновременно:
|
||
|
||
- определять wrapped или unwrapped формат ответа;
|
||
- проверять тип корневого объекта;
|
||
- проверять наличие `symbols`;
|
||
- проверять тип `symbols`;
|
||
- проверять структуру элементов `symbols`;
|
||
- проверять структуру вложенных коллекций;
|
||
- создавать raw-модели.
|
||
|
||
Поэтому перед parser был выделен отдельный слой:
|
||
|
||
```text
|
||
validation/schema.py
|
||
```
|
||
|
||
Разделение ответственности теперь выглядит следующим образом:
|
||
|
||
```text
|
||
schema.py
|
||
Проверяет форму и структуру данных.
|
||
|
||
parser.py
|
||
Преобразует структурно корректные данные в raw-модели Dzengi.
|
||
|
||
values.py
|
||
Проверяет допустимость предметных значений.
|
||
|
||
mapper.py
|
||
Преобразует raw-модели Dzengi во внутреннюю модель Instrument.
|
||
```
|
||
|
||
Это является обязательным архитектурным изменением для утверждённой структуры Dzentra.
|
||
|
||
---
|
||
|
||
## 3. Изменённые и созданные файлы
|
||
|
||
В рамках Build 003 были изменены или созданы только следующие файлы:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/exceptions.py
|
||
app/src/market_data/acquisition/validation/schema.py
|
||
app/tests/unit/market_data/acquisition/validation/test_schema.py
|
||
```
|
||
|
||
Другие файлы проекта не изменялись.
|
||
|
||
---
|
||
|
||
## 4. Реализованные исключения
|
||
|
||
В файле:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/exceptions.py
|
||
```
|
||
|
||
добавлены:
|
||
|
||
```python
|
||
class MarketDataAcquisitionError(Exception):
|
||
pass
|
||
```
|
||
|
||
и:
|
||
|
||
```python
|
||
class InstrumentReferenceSchemaError(MarketDataAcquisitionError):
|
||
pass
|
||
```
|
||
|
||
Иерархия ошибок:
|
||
|
||
```text
|
||
Exception
|
||
↓
|
||
MarketDataAcquisitionError
|
||
↓
|
||
InstrumentReferenceSchemaError
|
||
```
|
||
|
||
`InstrumentReferenceSchemaError` используется для явного обозначения ошибки структуры документа Instrument Reference Data.
|
||
|
||
Это позволяет в последующих Build отличать структурную ошибку от:
|
||
|
||
- сетевой ошибки;
|
||
- ошибки HTTP;
|
||
- ошибки JSON-декодирования;
|
||
- ошибки значений;
|
||
- ошибки parser;
|
||
- ошибки mapper.
|
||
|
||
Исключение создано не «на будущее»: оно непосредственно используется schema validation в Build 003.
|
||
|
||
---
|
||
|
||
## 5. Реализованный контракт schema validation
|
||
|
||
В файле:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/validation/schema.py
|
||
```
|
||
|
||
создана модель:
|
||
|
||
```python
|
||
@dataclass(frozen=True, slots=True)
|
||
class ValidatedExchangeInfoDocument:
|
||
payload: Mapping[str, object]
|
||
is_wrapped: bool
|
||
status: object | None
|
||
correlation_id: object | None
|
||
```
|
||
|
||
Она представляет структурно проверенный документ `exchangeInfo`.
|
||
|
||
Модель содержит:
|
||
|
||
| Поле | Назначение |
|
||
|---|---|
|
||
| `payload` | Проверенный payload с данными `exchangeInfo` |
|
||
| `is_wrapped` | Признак wrapped/unwrapped формата |
|
||
| `status` | Верхнеуровневый статус wrapped-ответа |
|
||
| `correlation_id` | Верхнеуровневый `correlationId` wrapped-ответа |
|
||
|
||
Основная функция:
|
||
|
||
```python
|
||
validate_exchange_info_schema(document: object) -> ValidatedExchangeInfoDocument
|
||
```
|
||
|
||
принимает произвольный объект и либо:
|
||
|
||
- возвращает `ValidatedExchangeInfoDocument`;
|
||
- либо выбрасывает `InstrumentReferenceSchemaError`.
|
||
|
||
---
|
||
|
||
## 6. Поддерживаемые форматы ответа
|
||
|
||
### 6.1. Unwrapped-формат
|
||
|
||
Поддерживается непосредственный payload:
|
||
|
||
```json
|
||
{
|
||
"timezone": "UTC",
|
||
"serverTime": 1783537921471,
|
||
"symbols": []
|
||
}
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
is_wrapped = False
|
||
status = None
|
||
correlation_id = None
|
||
```
|
||
|
||
---
|
||
|
||
### 6.2. Wrapped-формат
|
||
|
||
Поддерживается ответ с вложенным `payload`:
|
||
|
||
```json
|
||
{
|
||
"status": "OK",
|
||
"correlationId": "2",
|
||
"payload": {
|
||
"timezone": "UTC",
|
||
"serverTime": 1783537921471,
|
||
"symbols": []
|
||
}
|
||
}
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
is_wrapped = True
|
||
status = "OK"
|
||
correlation_id = "2"
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Что проверяет schema validation
|
||
|
||
Build 003 проверяет следующие структурные свойства документа.
|
||
|
||
### 7.1. Корень документа
|
||
|
||
Корень должен быть JSON-объектом.
|
||
|
||
Отклоняются:
|
||
|
||
```json
|
||
[]
|
||
```
|
||
|
||
```json
|
||
null
|
||
```
|
||
|
||
```json
|
||
"invalid"
|
||
```
|
||
|
||
```json
|
||
123
|
||
```
|
||
|
||
---
|
||
|
||
### 7.2. Wrapped payload
|
||
|
||
Если присутствует ключ:
|
||
|
||
```text
|
||
payload
|
||
```
|
||
|
||
его значение должно быть JSON-объектом.
|
||
|
||
Например, отклоняется:
|
||
|
||
```json
|
||
{
|
||
"status": "OK",
|
||
"payload": []
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7.3. Наличие `symbols`
|
||
|
||
Payload должен содержать:
|
||
|
||
```text
|
||
symbols
|
||
```
|
||
|
||
Отсутствие `symbols` считается ошибкой структуры.
|
||
|
||
---
|
||
|
||
### 7.4. Тип `symbols`
|
||
|
||
`symbols` должен быть JSON-массивом.
|
||
|
||
Отклоняется:
|
||
|
||
```json
|
||
{
|
||
"symbols": {}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7.5. Элементы `symbols`
|
||
|
||
Каждый элемент `symbols` должен быть JSON-объектом.
|
||
|
||
Отклоняется:
|
||
|
||
```json
|
||
{
|
||
"symbols": [
|
||
"BTC/USD"
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7.6. `filters`
|
||
|
||
Если у инструмента присутствует:
|
||
|
||
```text
|
||
filters
|
||
```
|
||
|
||
то:
|
||
|
||
- `filters` должен быть JSON-массивом;
|
||
- каждый элемент `filters` должен быть JSON-объектом.
|
||
|
||
Отклоняется:
|
||
|
||
```json
|
||
{
|
||
"symbols": [
|
||
{
|
||
"symbol": "BTC/USD",
|
||
"filters": {}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Также отклоняется:
|
||
|
||
```json
|
||
{
|
||
"symbols": [
|
||
{
|
||
"symbol": "BTC/USD",
|
||
"filters": [
|
||
"LOT_SIZE"
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 7.7. `marketModes`
|
||
|
||
Если присутствует:
|
||
|
||
```text
|
||
marketModes
|
||
```
|
||
|
||
то:
|
||
|
||
- значение должно быть JSON-массивом;
|
||
- каждый элемент должен быть строкой.
|
||
|
||
---
|
||
|
||
### 7.8. `orderTypes`
|
||
|
||
Если присутствует:
|
||
|
||
```text
|
||
orderTypes
|
||
```
|
||
|
||
то:
|
||
|
||
- значение должно быть JSON-массивом;
|
||
- каждый элемент должен быть строкой.
|
||
|
||
---
|
||
|
||
### 7.9. `rateLimits`
|
||
|
||
Если присутствует:
|
||
|
||
```text
|
||
rateLimits
|
||
```
|
||
|
||
то:
|
||
|
||
- значение должно быть JSON-массивом;
|
||
- каждый элемент должен быть JSON-объектом.
|
||
|
||
---
|
||
|
||
### 7.10. `exchangeFilters`
|
||
|
||
Если присутствует:
|
||
|
||
```text
|
||
exchangeFilters
|
||
```
|
||
|
||
то:
|
||
|
||
- значение должно быть JSON-массивом;
|
||
- каждый элемент должен быть JSON-объектом.
|
||
|
||
---
|
||
|
||
## 8. Что намеренно не проверяется в Build 003
|
||
|
||
Build 003 не проверяет допустимость значений.
|
||
|
||
Следующие примеры не относятся к schema validation:
|
||
|
||
```json
|
||
{
|
||
"tickSize": -1
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"baseAssetPrecision": -5
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"status": ""
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"minQty": "not-a-number"
|
||
}
|
||
```
|
||
|
||
Такие проверки относятся к:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/validation/values.py
|
||
```
|
||
|
||
и должны реализовываться отдельно, без смешения структурной и предметной валидации.
|
||
|
||
---
|
||
|
||
## 9. Защита проверенного payload
|
||
|
||
Поле:
|
||
|
||
```python
|
||
payload: Mapping[str, object]
|
||
```
|
||
|
||
возвращается через:
|
||
|
||
```python
|
||
MappingProxyType
|
||
```
|
||
|
||
Это предотвращает случайное изменение корневого payload последующим parser.
|
||
|
||
Таким образом, schema validation передаёт следующему слою проверенное read-only представление данных.
|
||
|
||
---
|
||
|
||
## 10. Диагностические пути ошибок
|
||
|
||
Schema validation формирует ошибки с указанием пути до проблемного значения.
|
||
|
||
Примеры:
|
||
|
||
```text
|
||
$ должен быть JSON-объектом, получен list.
|
||
```
|
||
|
||
```text
|
||
$.payload.symbols должен быть JSON-массивом, получен dict.
|
||
```
|
||
|
||
```text
|
||
$.payload.symbols[0] должен быть JSON-объектом, получен str.
|
||
```
|
||
|
||
```text
|
||
$.payload.symbols[0].filters должен быть JSON-массивом, получен dict.
|
||
```
|
||
|
||
На текущем этапе для `symbols` используется унифицированный диагностический путь:
|
||
|
||
```text
|
||
$.payload.symbols
|
||
```
|
||
|
||
как для wrapped-, так и для unwrapped-формата.
|
||
|
||
Это сознательное упрощение текущего контракта и не влияет на production-поведение.
|
||
|
||
---
|
||
|
||
## 11. Реализованные тесты
|
||
|
||
Создан файл:
|
||
|
||
```text
|
||
app/tests/unit/market_data/acquisition/validation/test_schema.py
|
||
```
|
||
|
||
Он проверяет:
|
||
|
||
- корректный unwrapped-документ;
|
||
- корректный wrapped-документ;
|
||
- отклонение не-объекта в корне;
|
||
- отклонение некорректного wrapped payload;
|
||
- отсутствие `symbols`;
|
||
- некорректный тип `symbols`;
|
||
- некорректный элемент `symbols`;
|
||
- некорректный тип `filters`;
|
||
- некорректный элемент `filters`;
|
||
- некорректный тип `marketModes`;
|
||
- некорректный тип `orderTypes`;
|
||
- нестроковый элемент `marketModes`;
|
||
- нестроковый элемент `orderTypes`;
|
||
- некорректный тип `rateLimits`;
|
||
- некорректный элемент `rateLimits`;
|
||
- некорректный тип `exchangeFilters`;
|
||
- некорректный элемент `exchangeFilters`.
|
||
|
||
Итог:
|
||
|
||
```text
|
||
20 passed
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Выполненные проверки
|
||
|
||
### Проверка 1 — unit-тесты Build 003
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m pytest \
|
||
tests/unit/market_data/acquisition/validation/test_schema.py \
|
||
-q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
.................... [100%]
|
||
20 passed in 0.01s
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 2 — компиляция файлов Build 003
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m py_compile \
|
||
src/market_data/acquisition/exceptions.py \
|
||
src/market_data/acquisition/validation/schema.py \
|
||
tests/unit/market_data/acquisition/validation/test_schema.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
Ошибок нет.
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 3 — ручная проверка wrapped/unwrapped контрактов
|
||
|
||
Получен результат:
|
||
|
||
```text
|
||
Unwrapped:
|
||
is_wrapped: False
|
||
status: None
|
||
correlation_id: None
|
||
symbols: []
|
||
|
||
Wrapped:
|
||
is_wrapped: True
|
||
status: OK
|
||
correlation_id: 2
|
||
symbols: []
|
||
```
|
||
|
||
Подтверждено:
|
||
|
||
- unwrapped-формат определяется корректно;
|
||
- wrapped-формат определяется корректно;
|
||
- `status` сохраняется;
|
||
- `correlationId` сохраняется как `correlation_id`;
|
||
- `symbols` доступны через проверенный payload.
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 4 — типизированные ошибки
|
||
|
||
Проверены четыре некорректных документа.
|
||
|
||
Получен результат:
|
||
|
||
```text
|
||
1: InstrumentReferenceSchemaError: $ должен быть JSON-объектом, получен list.
|
||
2: InstrumentReferenceSchemaError: $.payload.symbols должен быть JSON-массивом, получен dict.
|
||
3: InstrumentReferenceSchemaError: $.payload.symbols[0] должен быть JSON-объектом, получен str.
|
||
4: InstrumentReferenceSchemaError: $.payload.symbols[0].filters должен быть JSON-массивом, получен dict.
|
||
```
|
||
|
||
Ни один ошибочный документ не был принят.
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 5 — полный набор тестов проекта
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m pytest -q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
............................... [100%]
|
||
31 passed in 0.04s
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 6 — отсутствие подключения к production-коду
|
||
|
||
Выполнен поиск:
|
||
|
||
```bash
|
||
grep -RIn \
|
||
--exclude-dir="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
-E "validate_exchange_info_schema|ValidatedExchangeInfoDocument|InstrumentReferenceSchemaError" \
|
||
src tests
|
||
```
|
||
|
||
Подтверждено, что новые сущности Build 003 используются только в:
|
||
|
||
```text
|
||
src/market_data/acquisition/exceptions.py
|
||
src/market_data/acquisition/validation/schema.py
|
||
tests/unit/market_data/acquisition/validation/test_schema.py
|
||
```
|
||
|
||
Они пока не подключены к:
|
||
|
||
```text
|
||
ExchangeService
|
||
legacy exchangeInfo
|
||
Telegram UI
|
||
runtime
|
||
автоторговле
|
||
market stream
|
||
production parser
|
||
production mapper
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
## 13. Обратная совместимость
|
||
|
||
Build 003 не изменяет:
|
||
|
||
```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()
|
||
```
|
||
|
||
Не изменено поведение:
|
||
|
||
- Telegram UI;
|
||
- автоторговли;
|
||
- runtime-проверок символа;
|
||
- legacy-кэша инструментов;
|
||
- получения цен;
|
||
- market stream;
|
||
- существующего `exchangeInfo`.
|
||
|
||
Новый schema validation пока изолирован от production-кода.
|
||
|
||
---
|
||
|
||
## 14. Точки обратной совместимости
|
||
|
||
После Build 003 продолжают действовать прежние точки совместимости:
|
||
|
||
```text
|
||
ExchangeService.get_exchange_symbols()
|
||
→ list[ExchangeSymbol]
|
||
|
||
ExchangeService.validate_symbol()
|
||
→ SymbolValidationResult
|
||
|
||
ExchangeService.get_symbol_runtime_status()
|
||
→ ExchangeRuntimeStatus
|
||
|
||
ExchangeService.get_symbol_market_status()
|
||
→ dict[str, object]
|
||
```
|
||
|
||
Ни одна из этих сигнатур не изменена.
|
||
|
||
---
|
||
|
||
## 15. Классификация изменений
|
||
|
||
| Изменение | Классификация |
|
||
|---|---|
|
||
| Создание schema validation | Обязательное архитектурное изменение |
|
||
| Создание `MarketDataAcquisitionError` | Обязательное архитектурное изменение |
|
||
| Создание `InstrumentReferenceSchemaError` | Обязательное архитектурное изменение |
|
||
| Поддержка wrapped/unwrapped форматов | Улучшение надёжности |
|
||
| Явные структурные ошибки | Улучшение надёжности |
|
||
| Read-only представление проверенного payload | Улучшение надёжности |
|
||
| Изменение production-поведения | Отсутствует |
|
||
| Изменение публичных legacy-интерфейсов | Отсутствует |
|
||
|
||
---
|
||
|
||
## 16. Условие завершения Build 003
|
||
|
||
Build 003 считается завершённым, потому что выполнены все условия:
|
||
|
||
- создан отдельный schema validation layer;
|
||
- поддержан wrapped-формат;
|
||
- поддержан unwrapped-формат;
|
||
- проверяется обязательная структура `symbols`;
|
||
- проверяются вложенные коллекции;
|
||
- ошибки типизированы;
|
||
- создан read-only контракт для передачи данных parser;
|
||
- написаны unit-тесты;
|
||
- все тесты Build проходят;
|
||
- полный набор тестов проекта проходит;
|
||
- production-код не изменён;
|
||
- обратная совместимость сохранена.
|
||
|
||
---
|
||
|
||
## 17. Итоговый статус
|
||
|
||
```text
|
||
Build 003 — COMPLETED
|
||
```
|
||
|
||
Итоговый набор тестов проекта:
|
||
|
||
```text
|
||
31 passed in 0.04s
|
||
```
|
||
|
||
Следующий этап:
|
||
|
||
```text
|
||
Build 004 — Parser exchangeInfo
|
||
```
|
||
|
||
На Build 004 новый parser должен:
|
||
|
||
1. принимать структурно проверенный `ValidatedExchangeInfoDocument`;
|
||
2. преобразовывать payload в raw-модели Dzengi, созданные в Build 002;
|
||
3. не выполнять повторную schema validation;
|
||
4. не создавать внутреннюю модель `Instrument`;
|
||
5. не подключаться к `ExchangeService`;
|
||
6. не менять production-поведение работающего бота.
|