build 039: complete Quotes Feed migration foundation

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

View File

@@ -0,0 +1,850 @@
# 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-поведение работающего бота.