Files
dzentra_bot/docs/migrations/build_005.md

1067 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Build 005 — Value Validation для Instrument Reference Data
**Статус:** Завершён
**Подсистема:** `market_data/acquisition`
**Область:** Instrument Reference Data
**Тип изменения:** Изолированное расширение новой архитектуры без подключения к production runtime
**Результат полного набора тестов:** `84 passed`
---
## 1. Цель Build 005
Цель Build 005 — реализовать отдельный слой проверки допустимости значений, полученных после:
1. проверки структуры исходного документа `exchangeInfo`;
2. преобразования проверенного документа в типизированные raw-модели адаптера Dzengi.
Build 005 добавляет третий этап обработки Instrument Reference Data:
```text
raw JSON
schema validation
parser
value validation
```
Value validation проверяет не структуру JSON и не типы полей исходного документа, а предметную допустимость уже распарсенных значений.
Например:
```text
tickSize = 0
```
может быть корректным числовым значением с точки зрения JSON и parser layer, но недопустимым значением для шага цены торгового инструмента.
---
## 2. Почему Build 005 реализован именно на этом этапе
До Build 005 были завершены следующие этапы миграции:
```text
Build 001
Каноническая source-independent модель Instrument
Build 002
Raw transport models адаптера Dzengi
Build 003
Schema validation исходного exchangeInfo
Build 004
Parser: validated document → Dzengi raw models
Build 005
Value validation Dzengi raw models
```
Такой порядок позволяет строго разделить ответственность слоёв.
### Schema validation отвечает за:
```text
Есть ли ожидаемые JSON-объекты?
Есть ли массив symbols?
Являются ли элементы symbols объектами?
Является ли filters массивом?
```
### Parser отвечает за:
```text
Как преобразовать проверенный JSON-документ
в типизированные raw-модели Dzengi?
```
### Value validation отвечает за:
```text
Допустимы ли конкретные значения полей
с точки зрения контракта Instrument Reference Data?
```
Это исключает смешивание:
- проверки JSON-структуры;
- транспортного parsing;
- проверки предметных значений;
- source-independent mapping.
---
## 3. Изменённые файлы
В рамках Build 005 изменены:
```text
app/src/market_data/acquisition/exceptions.py
app/src/market_data/acquisition/validation/values.py
```
Добавлен тестовый файл:
```text
app/tests/unit/market_data/acquisition/validation/test_values.py
```
---
## 4. Новая типизированная ошибка
В файл:
```text
app/src/market_data/acquisition/exceptions.py
```
добавлена ошибка:
```python
class InstrumentReferenceValueError(MarketDataAcquisitionError):
pass
```
Иерархия ошибок Instrument Reference Data теперь выглядит так:
```text
MarketDataAcquisitionError
├── InstrumentReferenceSchemaError
├── InstrumentReferenceParseError
└── InstrumentReferenceValueError
```
Каждый этап обработки имеет собственную категорию ошибки:
| Этап | Ошибка |
|---|---|
| Schema validation | `InstrumentReferenceSchemaError` |
| Parsing | `InstrumentReferenceParseError` |
| Value validation | `InstrumentReferenceValueError` |
Это позволяет будущему orchestration/service layer точно определять, на каком этапе обработки произошла ошибка.
---
## 5. Главная функция Build 005
В файл:
```text
app/src/market_data/acquisition/validation/values.py
```
добавлена функция:
```python
def validate_exchange_info_values(
response: DzengiExchangeInfoResponse,
) -> None:
```
Функция принимает:
```python
DzengiExchangeInfoResponse
```
то есть результат работы parser layer из Build 004.
При корректных значениях функция возвращает:
```python
None
```
При обнаружении недопустимого значения выбрасывается:
```python
InstrumentReferenceValueError
```
---
## 6. Проверяемые обязательные строки инструмента
Для каждого `DzengiExchangeInfoSymbol` проверяются следующие обязательные строковые поля:
```text
symbol
name
status
baseAsset
quoteAsset
marketType
```
Они не должны быть пустыми или состоять только из пробелов.
Пример недопустимого значения:
```python
symbol=" "
```
Результат:
```text
InstrumentReferenceValueError:
$.payload.symbols[0].symbol не должен быть пустым.
```
---
## 7. Проверка `orderTypes` и `marketModes`
Каждый элемент следующих последовательностей проверяется на непустое строковое значение:
```text
orderTypes
marketModes
```
Например:
```python
order_types=("LIMIT", " ")
```
отклоняется с ошибкой:
```text
$.payload.symbols[0].orderTypes[1] не должен быть пустым.
```
Аналогично:
```python
market_modes=("REGULAR", "")
```
отклоняется как недопустимое значение.
---
## 8. Проверка integer-полей
Следующие optional integer-поля не могут быть отрицательными:
```text
baseAssetPrecision
quotePrecision
swapChargeInterval
```
Допустимо:
```text
None
0
1
2
...
```
Недопустимо:
```text
-1
```
При нарушении выбрасывается:
```text
InstrumentReferenceValueError
```
---
## 9. Проверка `tickSize`
Для:
```text
tickSize
```
применяется правило:
```text
tickSize > 0
```
Допустимы только положительные конечные числа.
Отклоняются:
```text
0
отрицательные значения
NaN
+Infinity
-Infinity
```
Это правило подтверждено анализом реального sample Dzengi:
```text
tickSize:
present=51
min=1E-8
max=1
zero=0
negative=0
invalid=[]
```
---
## 10. Проверка конечности числовых значений
Для optional numeric-полей проверяется, что значение является конечным числом.
Проверяются:
```text
tickValue
tradingFee
exchangeFee
longRate
shortRate
minSLGap
maxSLGap
minTPGap
maxTPGap
```
Отклоняются:
```text
NaN
+Infinity
-Infinity
```
При этом нулевые значения не запрещаются автоматически.
---
## 11. Проверка LOT_SIZE
Для:
```text
DzengiLotSizeFilter
```
проверяются:
```text
minQty
maxQty
stepSize
```
Если значение присутствует, оно должно:
1. корректно преобразовываться в `Decimal`;
2. быть конечным;
3. быть строго больше нуля.
Дополнительно проверяется отношение:
```text
minQty <= maxQty
```
Если:
```text
minQty > maxQty
```
выбрасывается:
```text
InstrumentReferenceValueError
```
---
## 12. Проверка MIN_NOTIONAL
Для:
```text
DzengiMinNotionalFilter
```
проверяется:
```text
minNotional >= 0
```
Нулевое значение разрешено.
Отрицательное значение отклоняется.
---
## 13. Использование Decimal
Строковые и числовые значения ограничений торгового инструмента преобразуются для проверки через:
```python
Decimal(str(value))
```
Это позволяет избежать ненужной потери точности при обработке таких значений, как:
```text
0.0001
0.00000001
6.9E-7
```
Именно такой подход соответствует уже принятому контракту канонической модели `Instrument`, где торговые числовые ограничения представлены через `Decimal`.
---
## 14. Особенность полей country, sector и industry
В ходе первой проверки Build 005 была обнаружена ошибка первоначальной реализации.
Изначально к полям:
```text
country
sector
industry
```
применялось правило:
```text
если значение присутствует, строка не должна быть пустой
```
Однако анализ реального ответа Dzengi и уже созданных raw-моделей показал, что Dzengi штатно возвращает:
```python
country=""
sector=""
industry=""
```
Поэтому пустая строка для этих полей является допустимым транспортным состоянием.
После исправления Build 005:
```text
country=""
sector=""
industry=""
```
не считаются ошибкой.
Это соответствует реальному контракту Dzengi и не выполняет преждевременную нормализацию transport-specific данных.
---
## 15. Проверка отрицательных longRate и shortRate
Отрицательные значения:
```text
longRate
shortRate
```
являются допустимыми.
Это подтверждено анализом реального sample Dzengi:
```text
longRate:
present=51
min=-0.0684932
max=0.1389493
negative=43
shortRate:
present=51
min=-0.1608693
max=0.01
negative=38
```
Поэтому value validation проверяет только:
```text
значение является корректным конечным числом
```
но не требует:
```text
value >= 0
```
---
## 16. Проверка допустимости нулевых optional numeric values
Нулевые значения разрешены для полей, для которых реальный контракт Dzengi допускает `0`.
В частности:
```text
tickValue
tradingFee
exchangeFee
minSLGap
maxSLGap
minTPGap
maxTPGap
```
Это соответствует анализу реального sample:
```text
tickValue:
zero=1
tradingFee:
zero=40
minSLGap:
zero=51
minTPGap:
zero=51
```
Value validation не вводит ограничений, которые не подтверждены фактическими данными или контрактом источника.
---
## 17. Проверка неизвестных instrument filters
Для неизвестного фильтра инструмента:
```python
DzengiUnknownFilter
```
поле:
```text
filterType
```
должно быть непустым.
Это позволяет сохранить forward compatibility с новыми типами фильтров Dzengi, одновременно предотвращая попадание полностью неопределённых instrument filters в дальнейшую обработку.
---
## 18. Особенность global exchange filters
Для:
```text
exchangeFilters
```
сохранено более мягкое поведение.
Реальный transport contract может содержать global filter без meaningful `filterType`, поэтому пустое значение не отклоняется автоматически.
Таким образом, Build 005 различает:
```text
instrument-level filters
```
и:
```text
global exchange filters
```
и не навязывает им одинаковые ограничения без подтверждения реального контракта.
---
## 19. Результаты анализа реального sample Dzengi
Перед реализацией Build 005 был выполнен анализ реального `exchangeInfo` sample.
Получены следующие результаты.
### Обязательные строки
Пустые значения отсутствуют для:
```text
symbol
name
status
baseAsset
quoteAsset
marketType
```
### Precision
```text
baseAssetPrecision:
count=51
min=2
max=8
invalid=[]
quotePrecision:
count=51
min=2
max=8
invalid=[]
```
### Числовые диапазоны
```text
tickSize:
present=51
min=1E-8
max=1
zero=0
negative=0
invalid=[]
tickValue:
present=39
min=0
max=50665.5
zero=1
negative=0
invalid=[]
minQty:
present=51
min=0.0001
max=1
zero=0
negative=0
invalid=[]
maxQty:
present=51
min=100
max=10000000
zero=0
negative=0
invalid=[]
stepSize:
present=51
min=0.0001
max=1
zero=0
negative=0
invalid=[]
minNotional:
present=39
min=6.9E-7
max=507
zero=0
negative=0
invalid=[]
tradingFee:
present=51
min=0
max=0.075
zero=40
negative=0
invalid=[]
exchangeFee:
present=0
min=None
max=None
zero=0
negative=0
invalid=[]
longRate:
present=51
min=-0.0684932
max=0.1389493
zero=0
negative=43
invalid=[]
shortRate:
present=51
min=-0.1608693
max=0.01
zero=0
negative=38
invalid=[]
minSLGap:
present=51
min=0
max=0
zero=51
negative=0
invalid=[]
maxSLGap:
present=51
min=20.0
max=50.0
zero=0
negative=0
invalid=[]
minTPGap:
present=51
min=0
max=0
zero=51
negative=0
invalid=[]
maxTPGap:
present=51
min=20.0
max=50.0
zero=0
negative=0
invalid=[]
```
### Проверка отношения minQty и maxQty
Не обнаружено ни одного случая:
```text
minQty > maxQty
```
Результат:
```text
[]
```
---
## 20. Реализованные тесты
Добавлен файл:
```text
app/tests/unit/market_data/acquisition/validation/test_values.py
```
В нём реализовано:
```text
34 теста
```
Тестами покрыты:
- корректный полный `exchangeInfo`;
- пустые обязательные строки;
- пустой `orderType`;
- пустой `marketMode`;
- отрицательные precision values;
- отрицательный `swapChargeInterval`;
- нулевой `tickSize`;
- отрицательный `tickSize`;
- `NaN`;
- `+Infinity`;
- `-Infinity`;
- нечисловой `minQty`;
- неположительный `minQty`;
- неположительный `maxQty`;
- неположительный `stepSize`;
- `minQty > maxQty`;
- отрицательный `minNotional`;
- нулевой `minNotional`;
- отрицательные `longRate`;
- отрицательные `shortRate`;
- допустимые нулевые optional numeric values;
- rate limits;
- неизвестные instrument filters;
- global exchange filters.
---
## 21. Выполненные проверки
### Проверка 1 — unit-тесты Build 005
Команда:
```bash
python -m pytest \
tests/unit/market_data/acquisition/validation/test_values.py \
-q
```
Результат:
```text
34 passed in 0.02s
```
---
### Проверка 2 — Python compilation
Команда:
```bash
python -m py_compile \
src/market_data/acquisition/exceptions.py \
src/market_data/acquisition/validation/values.py \
tests/unit/market_data/acquisition/validation/test_values.py
```
Результат:
```text
успешно, без ошибок
```
---
### Проверка 3 — полная ручная цепочка обработки
Проверена последовательность:
```text
raw JSON
validate_exchange_info_schema()
parse_exchange_info()
validate_exchange_info_values()
```
Результат:
```text
Validation result: None
Symbol: BTC/USD_LEVERAGE
Filters: (
DzengiLotSizeFilter(
filter_type='LOT_SIZE',
min_qty='0.0001',
max_qty='1000',
step_size='0.0001'
),
DzengiMinNotionalFilter(
filter_type='MIN_NOTIONAL',
min_notional='1'
)
)
```
Это подтверждает совместимость Build 003, Build 004 и Build 005.
---
### Проверка 4 — полный набор тестов проекта
Команда:
```bash
python -m pytest -q
```
Результат:
```text
84 passed in 0.06s
```
Регрессий не обнаружено.
---
### Проверка 5 — контроль области использования
Выполнен поиск:
```bash
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "validate_exchange_info_values|InstrumentReferenceValueError" \
src tests
```
Подтверждено:
- `validate_exchange_info_values()` используется только в собственном модуле и unit-тестах;
- `InstrumentReferenceValueError` используется только в `exceptions.py`, `values.py` и unit-тестах;
- новый validator ещё не подключён к legacy `ExchangeService`;
- UI не изменён;
- runtime не изменён;
- автоторговля не изменена;
- production-поведение работающего бота не изменено.
---
## 22. Архитектурная граница Build 005
Build 005 намеренно не реализует:
```text
REST client новой подсистемы
mapping Dzengi raw models → Instrument
instrument feed
instrument handler
registry
service orchestration
cache
legacy compatibility adapter
переключение production consumers
удаление старого ExchangeSymbol
```
Эти задачи относятся к следующим этапам миграции.
Build 005 отвечает только за:
```text
проверку допустимости значений
в уже распарсенных raw-моделях Dzengi exchangeInfo
```
---
## 23. Влияние на работающего legacy-бота
Build 005 не изменяет production-поведение существующего бота.
Старый runtime продолжает использовать существующие компоненты:
```text
src/integrations/exchange/models.py
src/integrations/exchange/service.py
src/integrations/exchange/status.py
src/integrations/exchange/symbol_utils.py
```
Новая реализация находится отдельно:
```text
src/market_data/acquisition/
```
На момент завершения Build 005 она ещё не подключена к production consumers.
Это соответствует принятой стратегии миграции:
```text
сначала построить и протестировать новый путь параллельно,
затем переключать consumers поэтапно,
не ломая работающего бота
```
---
## 24. Итог Build 005
Build 005 успешно завершён.
Реализовано:
```text
InstrumentReferenceValueError
validate_exchange_info_values()
валидация обязательных строк
валидация orderTypes
валидация marketModes
валидация precision
валидация tickSize
валидация finite numeric values
валидация LOT_SIZE
валидация MIN_NOTIONAL
валидация rate limits
поддержка отрицательных longRate/shortRate
поддержка допустимых нулевых значений
поддержка пустых country/sector/industry
проверка unknown filters
34 unit-теста
```
Финальный результат:
```text
84 passed in 0.06s
```
Регрессий не обнаружено.
Legacy production runtime не изменён.
---
## 25. Текущее состояние pipeline после Build 005
На момент завершения Build 005 построена следующая часть новой подсистемы:
```text
Dzengi exchangeInfo raw JSON
validate_exchange_info_schema()
ValidatedExchangeInfoDocument
parse_exchange_info()
DzengiExchangeInfoResponse
validate_exchange_info_values()
validated Dzengi raw models
```
Следующий архитектурный шаг должен продолжить pipeline от проверенных source-specific raw-моделей к канонической source-independent модели:
```text
validated Dzengi raw models
mapper
Instrument
```
Таким образом, естественным следующим этапом является реализация mapping layer:
```text
DzengiExchangeInfoSymbol → Instrument
```
при сохранении изоляции от legacy production runtime до завершения и проверки нового пути.