1275 lines
27 KiB
Markdown
1275 lines
27 KiB
Markdown
# 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.
|
||
``` |