Files
dzentra_bot/docs/migrations/build_009.md

27 KiB
Raw Permalink Blame History

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 тестов.

Проверены следующие сценарии:

  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

Команда:

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.