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