# 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.
```