Files
dzentra_bot/docs/migrations/build_009.md

1275 lines
27 KiB
Markdown
Raw Permalink 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 009 — Instrument Handler
**Статус:** Завершён
**Подсистема:** `market_data/acquisition`
**Область:** Instrument Reference Data
**Тип изменения:** Изолированное добавление processing handler без подключения к production runtime
**Результат полного набора тестов:** `132 passed`
---
## 1. Цель Build 009
Цель Build 009 — реализовать конкретный обработчик документа Instrument Reference Data для формата Dzengi `exchangeInfo`, соответствующий созданному в Build 007 протоколу:
```python
class InstrumentDocumentHandler(Protocol):
def handle_instrument_document(
self,
document: object,
) -> tuple[Instrument, ...]:
...
```
Реализован класс:
```text
DzengiInstrumentDocumentHandler
```
Архитектурная граница Build 009:
```text
object
DzengiInstrumentDocumentHandler
tuple[Instrument, ...]
```
Handler объединяет уже реализованные стадии обработки:
```text
object
validate_exchange_info_schema()
ValidatedExchangeInfoDocument
parse_exchange_info()
DzengiExchangeInfoResponse
validate_exchange_info_values()
map_dzengi_exchange_info_to_instruments()
tuple[Instrument, ...]
```
Build 009 не выполняет получение документа по сети, не создаёт Feed, не работает с Registry и не подключается к production runtime.
---
## 2. Почему Build 009 выполняется именно сейчас
До начала Build 009 были завершены:
```text
Build 001 — внутренняя модель Instrument Reference Data
Build 002 — raw-модели ответа Dzengi
Build 003 — структурная валидация exchangeInfo
Build 004 — parser exchangeInfo
Build 005 — value validation
Build 006 — mapper Dzengi → Instrument
Build 007 — Protocol и Exceptions
Build 008 — Dzengi REST Adapter
```
После Build 008 существовали две отдельные части будущей acquisition-цепочки.
Получение документа:
```text
Dzengi REST API
ExchangeRestClient.get_payload()
DzengiInstrumentDocumentSource
object
```
Обработка документа:
```text
object
validate_exchange_info_schema()
parse_exchange_info()
validate_exchange_info_values()
map_dzengi_exchange_info_to_instruments()
tuple[Instrument, ...]
```
До Build 009 processing pipeline представлял собой набор отдельных функций.
Следующим необходимым шагом стало объединение этих функций в конкретную реализацию контракта:
```text
InstrumentDocumentHandler
```
---
## 3. Изменённые файлы
В рамках Build 009 реализован production-файл:
```text
app/src/market_data/acquisition/handlers/instrument_handler.py
```
Создан тестовый файл:
```text
app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py
```
Другие production-файлы не изменялись.
---
## 4. Реализованный Instrument Handler
В файле:
```text
app/src/market_data/acquisition/handlers/instrument_handler.py
```
реализован класс:
```python
class DzengiInstrumentDocumentHandler:
...
```
Его публичный метод:
```python
def handle_instrument_document(
self,
document: object,
) -> tuple[Instrument, ...]:
...
```
соответствует контракту:
```text
InstrumentDocumentHandler
```
---
## 5. Реализованный processing pipeline
Внутри `DzengiInstrumentDocumentHandler` последовательно вызываются четыре ранее реализованные стадии:
```python
validated_document = validate_exchange_info_schema(document)
response = parse_exchange_info(validated_document)
validate_exchange_info_values(response)
return map_dzengi_exchange_info_to_instruments(response)
```
Полная последовательность:
```text
сырой декодированный JSON-документ
schema validation
проверенный структурный документ
parser
Dzengi raw-модели
value validation
проверенные Dzengi raw-модели
mapper
внутренние source-independent модели Instrument
```
Таким образом, Handler является orchestration boundary для обработки одного документа, но не реализует самостоятельно логику отдельных стадий.
---
## 6. Разделение ответственности
Каждая стадия сохраняет собственную ответственность.
### Schema Validation
```text
validate_exchange_info_schema()
```
Отвечает за структуру JSON-документа:
```text
корневой тип;
wrapped/unwrapped формат;
payload;
symbols;
filters;
типы JSON-полей.
```
### Parser
```text
parse_exchange_info()
```
Отвечает за преобразование проверенного документа в:
```text
DzengiExchangeInfoResponse
DzengiExchangeInfoPayload
DzengiExchangeInfoSymbol
DzengiInstrumentFilter
```
### Value Validation
```text
validate_exchange_info_values()
```
Отвечает за допустимость значений:
```text
обязательные непустые строки;
числовые ограничения;
finite numbers;
положительные значения;
отношение minQty ≤ maxQty;
семантику filters.
```
### Mapper
```text
map_dzengi_exchange_info_to_instruments()
```
Отвечает за преобразование source-specific raw-моделей Dzengi во внутренние source-independent модели:
```text
Instrument
```
### Instrument Handler
```text
DzengiInstrumentDocumentHandler
```
Отвечает только за правильную последовательность вызова этих стадий.
---
## 7. Почему Handler является source-specific
Конкретный Handler использует:
```text
Dzengi parser
Dzengi raw models
Dzengi mapper
```
Поэтому класс явно называется:
```text
DzengiInstrumentDocumentHandler
```
а не:
```text
InstrumentHandler
```
Это сохраняет важную архитектурную границу:
```text
общий Protocol
InstrumentDocumentHandler
конкретная source-specific реализация
DzengiInstrumentDocumentHandler
```
В будущем другой источник может предоставить собственную реализацию:
```text
AnotherExchangeInstrumentDocumentHandler
```
при сохранении общего контракта:
```text
InstrumentDocumentHandler
```
---
## 8. Отсутствие нового Handler exception
В Build 009 намеренно не создавалась ошибка:
```text
InstrumentReferenceHandlerError
```
Handler сохраняет без дополнительного wrapping специализированные ошибки отдельных стадий:
```text
InstrumentReferenceSchemaError
InstrumentReferenceParseError
InstrumentReferenceValueError
InstrumentReferenceMappingError
```
Это позволяет точно определить этап отказа:
```text
Schema Error
→ документ имеет неверную структуру
Parse Error
→ проверенный документ невозможно корректно преобразовать
в Dzengi raw-модели
Value Error
→ структура корректна, но значения недопустимы
Mapping Error
→ raw-модель корректна, но её невозможно однозначно
преобразовать во внутреннюю модель Instrument
```
Добавление общей Handler-ошибки поверх этих исключений ухудшило бы диагностируемость.
---
## 9. Отсутствие try/except в Handler
В `DzengiInstrumentDocumentHandler` намеренно отсутствует:
```python
try:
...
except Exception:
...
```
Handler не скрывает и не переклассифицирует ошибки нижележащих стадий.
Например:
```text
validate_exchange_info_schema()
InstrumentReferenceSchemaError
выходит из Handler без изменения
```
Аналогично:
```text
parse_exchange_info()
InstrumentReferenceParseError
```
```text
validate_exchange_info_values()
InstrumentReferenceValueError
```
```text
map_dzengi_exchange_info_to_instruments()
InstrumentReferenceMappingError
```
---
## 10. Отсутствие Dependency Injection функций pipeline
В Build 009 не добавлялась передача через конструктор:
```text
schema validator;
parser;
value validator;
mapper.
```
Причины:
```text
все четыре компонента уже реализованы как чистые функции;
существует одна утверждённая последовательность обработки;
unit-тесты могут проверять реальный pipeline;
нет нескольких production-реализаций этих стадий;
нет текущей необходимости конфигурировать pipeline.
```
Добавление dependency injection четырёх функций на текущем этапе создало бы лишнюю абстракцию без практической потребности.
---
## 11. Поддержка wrapped и unwrapped документов
Handler поддерживает оба формата, уже утверждённые в Build 003.
### Unwrapped
```json
{
"timezone": "UTC",
"serverTime": 1783537921471,
"rateLimits": [],
"exchangeFilters": [],
"symbols": []
}
```
### Wrapped
```json
{
"status": "OK",
"correlationId": "2",
"payload": {
"timezone": "UTC",
"serverTime": 1783537921471,
"rateLimits": [],
"exchangeFilters": [],
"symbols": []
}
}
```
Handler не реализует отдельную логику определения формата.
Эта ответственность остаётся у:
```text
validate_exchange_info_schema()
```
---
## 12. Возвращаемый тип
Handler возвращает:
```python
tuple[Instrument, ...]
```
а не:
```python
list[Instrument]
```
Это соответствует:
```text
InstrumentDocumentHandler Protocol
```
и принятой модели immutable sequences в новой подсистеме.
При отсутствии инструментов:
```json
{
"symbols": []
}
```
возвращается:
```python
()
```
---
## 13. Точное преобразование числовых значений
Тестами подтверждено, что после прохождения полного Handler pipeline числовые значения внутренней модели представлены через:
```python
Decimal
```
Проверены:
```text
tick_size
tick_value
step_size
min_qty
max_qty
min_notional
```
Пример:
```python
assert instrument.tick_size == Decimal("0.05")
assert instrument.tick_value == Decimal("3878.86")
assert instrument.step_size == Decimal("0.0001")
assert instrument.min_qty == Decimal("0.0001")
assert instrument.max_qty == Decimal("1000")
assert instrument.min_notional == Decimal("1")
```
Это подтверждает, что Handler корректно проводит документ через уже реализованный mapper без потери точности внутренней модели.
---
## 14. Тестирование специализированных ошибок
Тестами проверено прохождение четырёх специализированных типов ошибок.
### Schema Error
Использован невалидный корневой документ:
```python
[]
```
Ожидаемая ошибка:
```text
InstrumentReferenceSchemaError
```
Ошибка возникает на стадии:
```text
validate_exchange_info_schema()
```
---
### Parse Error
Использовано:
```python
"baseAssetPrecision": True
```
Значение проходит структурную границу JSON, но не может быть принято parser как корректное целочисленное значение precision.
Ожидаемая ошибка:
```text
InstrumentReferenceParseError
```
Ошибка возникает на стадии:
```text
parse_exchange_info()
```
---
### Value Error
Использовано:
```python
"tickSize": 0
```
Документ успешно проходит:
```text
schema validation
parser
```
но отклоняется:
```text
value validation
```
Ожидаемая ошибка:
```text
InstrumentReferenceValueError
```
---
### Mapping Error
Для проверки mapper использованы два корректных фильтра:
```text
LOT_SIZE
LOT_SIZE
```
Каждый фильтр отдельно является корректным:
```json
{
"filterType": "LOT_SIZE",
"minQty": "0.0001",
"maxQty": "1000",
"stepSize": "0.0001"
}
```
Документ успешно проходит:
```text
schema validation
parser
value validation
```
но mapper обнаруживает неоднозначность:
```text
несколько фильтров LOT_SIZE
```
и выбрасывает:
```text
InstrumentReferenceMappingError
```
---
## 15. Исправление первоначального теста Mapping Error
Первоначально тест использовал:
```python
"minQty": None
```
и ожидал:
```text
InstrumentReferenceMappingError
```
Однако тест завершился ошибкой:
```text
Failed: DID NOT RAISE InstrumentReferenceMappingError
```
Анализ показал, что по текущему контракту:
```text
minQty=None
```
является допустимым значением.
Последовательность была корректной:
```text
parser
допускает None
value validation
допускает отсутствующий minQty
mapper
преобразует None → None
```
Следовательно, production-код работал правильно, а неверным был тестовый сценарий.
Тест был исправлен без изменения production-кода.
Вместо `minQty=None` использованы два корректных `LOT_SIZE`, что гарантированно вызывает ошибку именно на mapper.
---
## 16. Что Handler не делает
Build 009 сознательно не выполняет:
```text
HTTP-запросы;
создание DzengiInstrumentDocumentSource;
создание Instrument Feed;
создание Registry;
создание Acquisition Service;
кэширование;
retry;
логирование;
формирование Telegram-сообщений;
проверку exchange_enabled;
создание legacy ExchangeSymbol;
изменение ExchangeService;
production-подключение.
```
Handler не знает:
```text
откуда был получен документ;
когда был получен документ;
нужно ли повторять запрос;
куда сохранять результат;
кто является потребителем результата.
```
Его единственная ответственность:
```text
object
полный processing pipeline
tuple[Instrument, ...]
```
---
## 17. Отсутствие transport-зависимости
`DzengiInstrumentDocumentHandler` не импортирует:
```text
DzengiInstrumentDocumentSource
ExchangeRestClient
urllib
httpx
requests
```
Он не выполняет сетевые операции.
Архитектурная граница остаётся:
```text
Build 008
DzengiInstrumentDocumentSource
object
Build 009
DzengiInstrumentDocumentHandler
tuple[Instrument, ...]
```
Соединение Source и Handler относится к следующему этапу:
```text
Build 010 — Instrument Feed
```
---
## 18. Реализованные тестовые сценарии
Создан файл:
```text
app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py
```
Реализовано 9 тестов.
Проверены следующие сценарии:
1. `DzengiInstrumentDocumentHandler` соответствует `InstrumentDocumentHandler`;
2. корректно обрабатывается valid unwrapped document;
3. корректно обрабатывается valid wrapped document;
4. возвращаются точные `Decimal`-значения;
5. для пустого `symbols` возвращается пустой `tuple`;
6. сохраняется `InstrumentReferenceSchemaError`;
7. сохраняется `InstrumentReferenceParseError`;
8. сохраняется `InstrumentReferenceValueError`;
9. сохраняется `InstrumentReferenceMappingError`.
---
## 19. Выполненные проверки
### Проверка 1 — unit-тесты Instrument Handler
Команда:
```bash
python -m pytest \
tests/unit/market_data/acquisition/handlers/test_instrument_handler.py \
-q
```
Первоначальный результат:
```text
........F [100%]
FAILED test_handler_preserves_mapping_error
1 failed, 8 passed in 0.04s
```
Причина:
```text
тест ожидал InstrumentReferenceMappingError для minQty=None,
но такое значение допустимо текущим контрактом.
```
Production-код не изменялся.
После исправления тестового сценария:
```bash
python -m pytest \
tests/unit/market_data/acquisition/handlers/test_instrument_handler.py \
-q
```
получен результат:
```text
......... [100%]
9 passed in 0.02s
```
Статус:
```text
PASSED
```
---
### Проверка 2 — Python compilation
Команда:
```bash
python -m py_compile \
src/market_data/acquisition/handlers/instrument_handler.py \
tests/unit/market_data/acquisition/handlers/test_instrument_handler.py
```
Результат:
```text
Команда завершилась без ошибок и без вывода.
```
Статус:
```text
PASSED
```
---
### Проверка 3 — полный набор тестов проекта
Команда:
```bash
python -m pytest -q
```
Результат:
```text
.................................................................................................................................... [100%]
132 passed in 0.08s
```
Статус:
```text
PASSED
```
---
### Проверка 4 — отсутствие преждевременной production-интеграции
Команда:
```bash
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "DzengiInstrumentDocumentHandler|InstrumentDocumentHandler" \
src tests
```
Полученные production-использования:
```text
src/market_data/acquisition/protocol.py:
InstrumentDocumentHandler
src/market_data/acquisition/handlers/instrument_handler.py:
DzengiInstrumentDocumentHandler
```
Остальные использования находятся исключительно в unit-тестах:
```text
tests/unit/market_data/acquisition/test_protocol.py
tests/unit/market_data/acquisition/handlers/test_instrument_handler.py
```
Не обнаружено подключения к:
```text
ExchangeService
Telegram UI
AutoTrade
Trading runtime
другим production-потребителям
```
Статус:
```text
PASSED
```
---
## 20. Архитектура после Build 009
После завершения Build 009 новая часть подсистемы имеет следующую структуру:
```text
market_data/
└── acquisition/
├── exceptions.py
│ ├── MarketDataAcquisitionError
│ ├── InstrumentReferenceTransportError
│ ├── InstrumentReferenceSchemaError
│ ├── InstrumentReferenceParseError
│ ├── InstrumentReferenceValueError
│ └── InstrumentReferenceMappingError
├── protocol.py
│ ├── InstrumentDocumentSource
│ ├── InstrumentDocumentHandler
│ └── InstrumentFeedProtocol
├── models/
│ └── instrument.py
│ └── Instrument
├── validation/
│ ├── schema.py
│ └── values.py
├── handlers/
│ └── instrument_handler.py
│ └── DzengiInstrumentDocumentHandler
└── adapters/
└── dzengi/
├── models.py
├── parser.py
├── mapper.py
└── rest.py
├── _PayloadRestClient
└── DzengiInstrumentDocumentSource
```
На текущем этапе ещё не реализованы:
```text
feeds/instrument_feed.py
registry.py
service.py
```
---
## 21. Полная архитектурная цепочка после Build 009
После Build 009 реализованы две изолированные части.
### Transport
```text
Dzengi REST API
ExchangeRestClient.get_payload()
DzengiInstrumentDocumentSource
object
```
### Processing
```text
object
DzengiInstrumentDocumentHandler
├── validate_exchange_info_schema()
│ ↓
├── parse_exchange_info()
│ ↓
├── validate_exchange_info_values()
│ ↓
└── map_dzengi_exchange_info_to_instruments()
tuple[Instrument, ...]
```
Они пока намеренно не соединены в production-коде.
Следующая архитектурная связь:
```text
DzengiInstrumentDocumentSource
DzengiInstrumentDocumentHandler
```
будет реализована через общий Protocol в:
```text
Build 010 — Instrument Feed
```
---
## 22. Влияние на legacy-систему
Build 009 не подключён к существующим компонентам:
```text
ExchangeService
ExchangeSymbol
SymbolValidationResult
Telegram UI
AutoTrade
Market Stream
Market Data Runner
Execution Quality
```
Не изменены:
```text
ExchangeService.get_exchange_symbols()
ExchangeService.validate_symbol()
ExchangeService.get_symbol_runtime_status()
normalize_symbol()
symbol_candidates()
```
Старый production-путь продолжает работать без изменений.
Новый Handler существует изолированно и пока вызывается только unit-тестами.
---
## 23. Обратная совместимость
Подтверждено сохранение:
```text
сигнатур существующих методов;
старых импортов;
существующего формата legacy-ошибок;
Telegram UI;
автоторговли;
runtime-поведения;
legacy ExchangeSymbol;
legacy-кэша.
```
Build 009 имеет полную обратную совместимость.
---
## 24. Классификация изменений
| Изменение | Классификация |
|---|---|
| `DzengiInstrumentDocumentHandler` | Обязательное архитектурное изменение |
| Объединение schema → parser → values → mapper | Обязательное архитектурное изменение |
| Сохранение специализированных ошибок | Улучшение диагностируемости |
| Новый Handler exception | Не создаётся |
| Dependency injection функций pipeline | Не добавляется |
| HTTP-зависимость | Отсутствует |
| Изменение legacy-кода | Отсутствует |
| Изменение production-поведения | Отсутствует |
---
## 25. Условие завершения Build 009
Все условия выполнены:
```text
DzengiInstrumentDocumentHandler
реализован;
InstrumentDocumentHandler
соблюдён;
schema validation
выполняется первой;
parser
получает только структурно проверенный документ;
value validation
выполняется после parser;
mapper
выполняется только после успешной value validation;
tuple[Instrument, ...]
возвращается как итоговый результат;
специализированные ошибки
сохраняются без дополнительного wrapping;
HTTP-зависимость
отсутствует;
unit-тесты
проходят;
полный pytest
проходит;
Handler
не подключён к production runtime.
```
---
## 26. Итог Build 009
Build 009 завершён успешно.
Реализовано:
```text
DzengiInstrumentDocumentHandler
object
schema validation
parser
value validation
mapper
tuple[Instrument, ...]
```
Подтверждено:
```text
9 Instrument Handler tests passed
132 total project tests passed
Python compilation passed
No premature production integration detected
Legacy bot behavior unchanged
```
Итоговый статус:
```text
BUILD 009 — COMPLETE
```
---
## 27. Следующий этап
Следующий этап утверждённого плана:
```text
Build 010 — Instrument Feed
```
Его задача — соединить уже реализованные общие контракты:
```text
InstrumentDocumentSource
InstrumentDocumentHandler
InstrumentFeedProtocol
```
Предполагаемая архитектурная граница Build 010:
```text
InstrumentDocumentSource
fetch_instrument_document()
object
InstrumentDocumentHandler
handle_instrument_document()
tuple[Instrument, ...]
```
Build 010 не должен:
```text
создавать Registry;
создавать Acquisition Service;
изменять ExchangeService;
переключать legacy get_exchange_symbols();
создавать compatibility mapper;
переносить кэш;
подключаться к Telegram UI;
изменять AutoTrade или trading runtime.
```