1092 lines
27 KiB
Markdown
1092 lines
27 KiB
Markdown
# Build 008 — Dzengi REST Adapter
|
||
|
||
**Статус:** Завершён
|
||
**Подсистема:** `market_data/acquisition`
|
||
**Область:** Instrument Reference Data
|
||
**Тип изменения:** Изолированное добавление transport adapter без подключения к production runtime
|
||
**Результат полного набора тестов:** `123 passed`
|
||
|
||
---
|
||
|
||
## 1. Цель Build 008
|
||
|
||
Цель Build 008 — реализовать конкретный транспортный источник Instrument Reference Data для Dzengi REST API, соответствующий созданному в Build 007 протоколу:
|
||
|
||
```python
|
||
class InstrumentDocumentSource(Protocol):
|
||
def fetch_instrument_document(self) -> object:
|
||
...
|
||
```
|
||
|
||
Реализован класс:
|
||
|
||
```text
|
||
DzengiInstrumentDocumentSource
|
||
```
|
||
|
||
Архитектурная граница Build 008:
|
||
|
||
```text
|
||
Dzengi REST API
|
||
↓
|
||
ExchangeRestClient
|
||
↓
|
||
DzengiInstrumentDocumentSource
|
||
↓
|
||
object
|
||
```
|
||
|
||
Build 008 отвечает только за получение декодированного транспортного документа.
|
||
|
||
Он не выполняет его структурную проверку, parsing, проверку значений или преобразование во внутренние модели `Instrument`.
|
||
|
||
---
|
||
|
||
## 2. Почему Build 008 выполняется именно сейчас
|
||
|
||
До начала Build 008 были завершены:
|
||
|
||
```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 007 были зафиксированы контракты будущей orchestration-цепочки:
|
||
|
||
```text
|
||
InstrumentDocumentSource
|
||
↓
|
||
InstrumentDocumentHandler
|
||
↓
|
||
InstrumentFeedProtocol
|
||
↓
|
||
Registry
|
||
↓
|
||
Acquisition Service
|
||
```
|
||
|
||
Следующим необходимым шагом стала конкретная реализация первого контракта:
|
||
|
||
```text
|
||
InstrumentDocumentSource
|
||
```
|
||
|
||
для источника Dzengi.
|
||
|
||
---
|
||
|
||
## 3. Анализ существующей реализации
|
||
|
||
Перед написанием кода были проанализированы:
|
||
|
||
```text
|
||
app/src/integrations/exchange/rest_client.py
|
||
app/src/integrations/exchange/exceptions.py
|
||
app/src/core/config.py
|
||
app/src/integrations/exchange/service.py
|
||
app/src/market_data/acquisition/adapters/dzengi/rest.py
|
||
```
|
||
|
||
Также выполнен поиск существующих вызовов `exchangeInfo`:
|
||
|
||
```bash
|
||
grep -RIn \
|
||
--exclude-dir="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
-E "exchangeInfo|exchange_info|get_exchange_symbols" \
|
||
src/integrations/exchange
|
||
```
|
||
|
||
Было подтверждено, что legacy-реализация использует:
|
||
|
||
```python
|
||
client.get_json("/api/v1/exchangeInfo")
|
||
```
|
||
|
||
в методе:
|
||
|
||
```text
|
||
ExchangeService.get_exchange_symbols()
|
||
```
|
||
|
||
Таким образом, фактически используемый работающим ботом endpoint:
|
||
|
||
```text
|
||
/api/v1/exchangeInfo
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Принятое решение по HTTP transport
|
||
|
||
В Build 008 не создавалась новая параллельная HTTP-реализация.
|
||
|
||
Вместо этого новый Dzengi REST Adapter временно переиспользует существующий:
|
||
|
||
```text
|
||
src.integrations.exchange.rest_client.ExchangeRestClient
|
||
```
|
||
|
||
Причины:
|
||
|
||
```text
|
||
клиент уже использует актуальный EXCHANGE_BASE_URL;
|
||
клиент уже применяет EXCHANGE_TIMEOUT_SEC;
|
||
клиент уже реализует HTTP GET;
|
||
клиент уже устанавливает необходимые HTTP headers;
|
||
клиент уже выполняет JSON decoding;
|
||
клиент уже преобразует HTTP/network ошибки в exchange exceptions;
|
||
создание второго HTTP transport дублировало бы существующую инфраструктуру;
|
||
legacy-код работающего бота не требуется изменять.
|
||
```
|
||
|
||
Это является временной переходной зависимостью.
|
||
|
||
Условие её будущего удаления:
|
||
|
||
```text
|
||
появление утверждённого общего transport-клиента
|
||
или
|
||
полный вывод legacy integrations/exchange из эксплуатации.
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Изменённые файлы
|
||
|
||
В рамках Build 008 изменён production-файл:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/adapters/dzengi/rest.py
|
||
```
|
||
|
||
Создан тестовый файл:
|
||
|
||
```text
|
||
app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py
|
||
```
|
||
|
||
Другие production-файлы не изменялись.
|
||
|
||
---
|
||
|
||
## 6. Реализованный Dzengi REST Adapter
|
||
|
||
В файле:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/adapters/dzengi/rest.py
|
||
```
|
||
|
||
реализован класс:
|
||
|
||
```python
|
||
class DzengiInstrumentDocumentSource:
|
||
...
|
||
```
|
||
|
||
Его публичный метод:
|
||
|
||
```python
|
||
def fetch_instrument_document(self) -> object:
|
||
...
|
||
```
|
||
|
||
соответствует контракту:
|
||
|
||
```text
|
||
InstrumentDocumentSource
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Endpoint exchangeInfo
|
||
|
||
В adapter зафиксирован endpoint:
|
||
|
||
```python
|
||
_EXCHANGE_INFO_PATH = "/api/v1/exchangeInfo"
|
||
```
|
||
|
||
Он соответствует фактически используемому legacy-кодом endpoint.
|
||
|
||
Путь вынесен в приватную константу, чтобы не дублировать строковый литерал внутри методов.
|
||
|
||
Build 008:
|
||
|
||
```text
|
||
не меняет версию endpoint;
|
||
не вводит альтернативный endpoint;
|
||
не изменяет legacy-вызов;
|
||
не переключает production runtime.
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Почему используется get_payload()
|
||
|
||
Legacy REST-клиент предоставляет два метода:
|
||
|
||
```python
|
||
get_payload() -> object
|
||
```
|
||
|
||
и:
|
||
|
||
```python
|
||
get_json() -> dict
|
||
```
|
||
|
||
Для нового adapter выбран:
|
||
|
||
```python
|
||
get_payload()
|
||
```
|
||
|
||
Это принципиальное архитектурное решение.
|
||
|
||
Контракт `InstrumentDocumentSource` возвращает:
|
||
|
||
```python
|
||
object
|
||
```
|
||
|
||
Следовательно, transport layer не должен утверждать, что полученный JSON обязательно является объектом `dict`.
|
||
|
||
Если внешний источник вернёт:
|
||
|
||
```text
|
||
dict
|
||
list
|
||
str
|
||
int
|
||
float
|
||
bool
|
||
None
|
||
```
|
||
|
||
REST adapter должен вернуть декодированный документ следующему слою без структурной интерпретации.
|
||
|
||
Проверка структуры относится к Build 003:
|
||
|
||
```text
|
||
validate_exchange_info_schema()
|
||
```
|
||
|
||
Поэтому архитектурная граница остаётся:
|
||
|
||
```text
|
||
Dzengi REST API
|
||
↓
|
||
ExchangeRestClient.get_payload()
|
||
↓
|
||
object
|
||
↓
|
||
Schema Validation
|
||
```
|
||
|
||
Использование `get_json()` преждевременно смешало бы:
|
||
|
||
```text
|
||
transport responsibility
|
||
```
|
||
|
||
и:
|
||
|
||
```text
|
||
schema validation responsibility
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Отсутствие преобразования документа
|
||
|
||
`DzengiInstrumentDocumentSource` возвращает результат:
|
||
|
||
```python
|
||
client.get_payload(_EXCHANGE_INFO_PATH)
|
||
```
|
||
|
||
без изменения.
|
||
|
||
Adapter не выполняет:
|
||
|
||
```text
|
||
копирование документа;
|
||
изменение полей;
|
||
извлечение payload;
|
||
извлечение symbols;
|
||
нормализацию значений;
|
||
преобразование коллекций;
|
||
создание raw-моделей;
|
||
создание Instrument.
|
||
```
|
||
|
||
Тестами подтверждено сохранение identity:
|
||
|
||
```python
|
||
assert result is document
|
||
```
|
||
|
||
как для `dict`, так и для `list`.
|
||
|
||
---
|
||
|
||
## 10. Dependency Injection
|
||
|
||
Adapter поддерживает передачу существующего REST-клиента:
|
||
|
||
```python
|
||
class DzengiInstrumentDocumentSource:
|
||
def __init__(
|
||
self,
|
||
client: _PayloadRestClient | None = None,
|
||
) -> None:
|
||
self._client = client
|
||
```
|
||
|
||
Поведение:
|
||
|
||
```text
|
||
client передан
|
||
↓
|
||
используется переданный объект
|
||
|
||
client не передан
|
||
↓
|
||
создаётся ExchangeRestClient()
|
||
```
|
||
|
||
Dependency injection необходим уже на текущем Build для:
|
||
|
||
```text
|
||
unit-тестирования без реальных сетевых запросов;
|
||
точной проверки вызываемого endpoint;
|
||
эмуляции transport errors;
|
||
проверки неизменности возвращаемого документа.
|
||
```
|
||
|
||
---
|
||
|
||
## 11. Приватный транспортный Protocol
|
||
|
||
Первоначально конструктор adapter был аннотирован следующим образом:
|
||
|
||
```python
|
||
client: ExchangeRestClient | None = None
|
||
```
|
||
|
||
При передаче тестового `StubRestClient` Pylance корректно обнаружил несовместимость типов:
|
||
|
||
```text
|
||
Argument of type "StubRestClient" cannot be assigned
|
||
to parameter "client" of type "ExchangeRestClient | None"
|
||
```
|
||
|
||
Оставлять такую диагностическую ошибку в Build было признано неправильным.
|
||
|
||
Для исправления создан минимальный приватный структурный контракт:
|
||
|
||
```python
|
||
class _PayloadRestClient(Protocol):
|
||
def get_payload(
|
||
self,
|
||
path: str,
|
||
params: dict[str, str] | None = None,
|
||
headers: dict[str, str] | None = None,
|
||
) -> object:
|
||
...
|
||
```
|
||
|
||
Теперь конструктор принимает:
|
||
|
||
```python
|
||
client: _PayloadRestClient | None = None
|
||
```
|
||
|
||
Это позволяет типобезопасно передавать:
|
||
|
||
```text
|
||
ExchangeRestClient;
|
||
StubRestClient;
|
||
любую другую реализацию с совместимым get_payload().
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Почему _PayloadRestClient является приватным
|
||
|
||
Protocol назван:
|
||
|
||
```text
|
||
_PayloadRestClient
|
||
```
|
||
|
||
с ведущим подчёркиванием сознательно.
|
||
|
||
На текущем этапе он нужен только как внутренняя типовая граница конкретного Dzengi REST Adapter.
|
||
|
||
Он:
|
||
|
||
```text
|
||
не является публичным контрактом всей acquisition subsystem;
|
||
не экспортируется через __init__.py;
|
||
не используется другими production-модулями;
|
||
не создаёт новый общий transport abstraction layer.
|
||
```
|
||
|
||
Если в будущем нескольким adapter потребуется общий HTTP transport contract, его выделение в публичный модуль должно выполняться отдельным архитектурным решением на основании реальной потребности.
|
||
|
||
---
|
||
|
||
## 13. Ленивое создание ExchangeRestClient
|
||
|
||
Если клиент не передан, он создаётся внутри:
|
||
|
||
```python
|
||
fetch_instrument_document()
|
||
```
|
||
|
||
а не в конструкторе `DzengiInstrumentDocumentSource`.
|
||
|
||
Последовательность:
|
||
|
||
```text
|
||
DzengiInstrumentDocumentSource()
|
||
↓
|
||
объект создаётся без немедленного создания ExchangeRestClient
|
||
|
||
fetch_instrument_document()
|
||
↓
|
||
создаётся ExchangeRestClient
|
||
↓
|
||
выполняется REST-запрос
|
||
```
|
||
|
||
Это важно, потому что конструктор legacy `ExchangeRestClient`:
|
||
|
||
```text
|
||
загружает настройки;
|
||
проверяет EXCHANGE_BASE_URL;
|
||
может выбросить исключение ещё до HTTP-запроса.
|
||
```
|
||
|
||
Создание клиента внутри `try` гарантирует, что ошибка его создания также преобразуется в:
|
||
|
||
```text
|
||
InstrumentReferenceTransportError
|
||
```
|
||
|
||
---
|
||
|
||
## 14. Transport error boundary
|
||
|
||
Build 007 добавил специализированную ошибку:
|
||
|
||
```python
|
||
InstrumentReferenceTransportError
|
||
```
|
||
|
||
Build 008 впервые использует её в production-коде новой подсистемы.
|
||
|
||
Любая ошибка, возникающая внутри transport boundary:
|
||
|
||
```python
|
||
except Exception as exc:
|
||
```
|
||
|
||
преобразуется в:
|
||
|
||
```python
|
||
raise InstrumentReferenceTransportError(
|
||
"Не удалось получить Instrument Reference Data "
|
||
f"от Dzengi: {exc}"
|
||
) from exc
|
||
```
|
||
|
||
Это обеспечивает единый публичный тип transport-ошибки для новой acquisition subsystem.
|
||
|
||
---
|
||
|
||
## 15. Какие ошибки оборачиваются
|
||
|
||
Тестами подтверждена обработка:
|
||
|
||
```text
|
||
ExchangeConnectionError
|
||
ExchangeResponseError
|
||
RuntimeError
|
||
ошибки создания ExchangeRestClient
|
||
```
|
||
|
||
Также текущая граница перехватывает другие `Exception`, возникающие при получении документа.
|
||
|
||
Наружу новой подсистемы не должны непосредственно выходить legacy-типы:
|
||
|
||
```text
|
||
ExchangeConnectionError
|
||
ExchangeResponseError
|
||
ExchangeError
|
||
```
|
||
|
||
На границе нового adapter они преобразуются в:
|
||
|
||
```text
|
||
InstrumentReferenceTransportError
|
||
```
|
||
|
||
---
|
||
|
||
## 16. Exception chaining
|
||
|
||
При преобразовании ошибки сохраняется исходное исключение:
|
||
|
||
```python
|
||
raise InstrumentReferenceTransportError(...) from exc
|
||
```
|
||
|
||
Благодаря этому:
|
||
|
||
```python
|
||
wrapped_error.__cause__ is original_error
|
||
```
|
||
|
||
Тестами подтверждено сохранение исходной причины.
|
||
|
||
Это важно для:
|
||
|
||
```text
|
||
диагностики;
|
||
логирования;
|
||
traceback;
|
||
будущего анализа transport failures.
|
||
```
|
||
|
||
---
|
||
|
||
## 17. Что REST Adapter не делает
|
||
|
||
Build 008 сознательно не выполняет:
|
||
|
||
```text
|
||
schema validation;
|
||
parsing;
|
||
value validation;
|
||
mapping;
|
||
создание Instrument;
|
||
создание Handler;
|
||
создание Feed;
|
||
создание Registry;
|
||
создание Acquisition Service;
|
||
кэширование;
|
||
retry;
|
||
логирование;
|
||
формирование Telegram-сообщений;
|
||
проверку exchange_enabled;
|
||
production-подключение.
|
||
```
|
||
|
||
В adapter отсутствуют вызовы:
|
||
|
||
```text
|
||
validate_exchange_info_schema()
|
||
parse_exchange_info()
|
||
validate_exchange_info_values()
|
||
map_dzengi_exchange_info_to_instruments()
|
||
```
|
||
|
||
---
|
||
|
||
## 18. Почему adapter не проверяет exchange_enabled
|
||
|
||
`DzengiInstrumentDocumentSource` является transport adapter.
|
||
|
||
Его ответственность:
|
||
|
||
```text
|
||
получить документ от конкретного внешнего источника.
|
||
```
|
||
|
||
Решение о том:
|
||
|
||
```text
|
||
разрешено ли приложению обращаться к бирже;
|
||
в каком режиме работает приложение;
|
||
нужно ли использовать mock;
|
||
нужно ли использовать cache;
|
||
когда именно запускать acquisition;
|
||
```
|
||
|
||
относится к orchestration или service layer.
|
||
|
||
Поэтому Build 008 не добавляет проверку:
|
||
|
||
```python
|
||
exchange_enabled
|
||
```
|
||
|
||
и не реализует mock-поведение.
|
||
|
||
---
|
||
|
||
## 19. Почему adapter не использует format_exchange_error_for_user()
|
||
|
||
В legacy-модуле существует:
|
||
|
||
```python
|
||
format_exchange_error_for_user()
|
||
```
|
||
|
||
Он предназначен для пользовательского представления ошибок.
|
||
|
||
REST adapter не является UI-слоем.
|
||
|
||
Поэтому он не должен:
|
||
|
||
```text
|
||
формировать пользовательский текст;
|
||
знать о Telegram UI;
|
||
классифицировать ошибку для экрана;
|
||
принимать решение о повторной попытке.
|
||
```
|
||
|
||
Его задача — предоставить точную предметную ошибку:
|
||
|
||
```text
|
||
InstrumentReferenceTransportError
|
||
```
|
||
|
||
---
|
||
|
||
## 20. Реализованные тестовые сценарии
|
||
|
||
Создан файл:
|
||
|
||
```text
|
||
app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py
|
||
```
|
||
|
||
Реализовано 10 тестов.
|
||
|
||
Проверены следующие сценарии:
|
||
|
||
1. `DzengiInstrumentDocumentSource` соответствует `InstrumentDocumentSource`;
|
||
2. вызывается endpoint `/api/v1/exchangeInfo`;
|
||
3. возвращаемый `dict` передаётся без изменения;
|
||
4. возвращаемый `list` передаётся без изменения;
|
||
5. injected client действительно используется;
|
||
6. `ExchangeConnectionError` преобразуется в `InstrumentReferenceTransportError`;
|
||
7. `ExchangeResponseError` преобразуется в `InstrumentReferenceTransportError`;
|
||
8. неизвестная `RuntimeError` также преобразуется в transport error;
|
||
9. ошибка создания `ExchangeRestClient` преобразуется в transport error;
|
||
10. adapter не преобразует wrapped document.
|
||
|
||
Дополнительно проверено:
|
||
|
||
```text
|
||
исходное исключение сохраняется в __cause__;
|
||
текст исходной ошибки сохраняется в transport error;
|
||
реальный сетевой запрос в unit-тестах не выполняется.
|
||
```
|
||
|
||
---
|
||
|
||
## 21. Выполненные проверки
|
||
|
||
### Проверка 1 — unit-тесты Dzengi REST Adapter
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m pytest \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \
|
||
-q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
.......... [100%]
|
||
10 passed in 0.02s
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 2 — Python compilation
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m py_compile \
|
||
src/market_data/acquisition/adapters/dzengi/rest.py \
|
||
tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
Команда завершилась без ошибок и без вывода.
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 3 — полный набор тестов проекта
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m pytest -q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
........................................................................................................................... [100%]
|
||
123 passed in 0.08s
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
### Проверка 4 — отсутствие преждевременной production-интеграции
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
grep -RIn \
|
||
--exclude-dir="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
-E "DzengiInstrumentDocumentSource|_PayloadRestClient|InstrumentReferenceTransportError" \
|
||
src tests
|
||
```
|
||
|
||
Подтверждено:
|
||
|
||
```text
|
||
DzengiInstrumentDocumentSource
|
||
используется только в новом Dzengi REST Adapter и его unit-тестах;
|
||
|
||
_PayloadRestClient
|
||
остаётся приватным контрактом внутри rest.py;
|
||
|
||
InstrumentReferenceTransportError
|
||
используется только внутри новой market_data/acquisition subsystem
|
||
и её unit-тестов.
|
||
```
|
||
|
||
Не обнаружено подключения к:
|
||
|
||
```text
|
||
ExchangeService
|
||
Telegram UI
|
||
AutoTrade
|
||
Trading runtime
|
||
другим production-потребителям
|
||
```
|
||
|
||
Статус:
|
||
|
||
```text
|
||
PASSED
|
||
```
|
||
|
||
---
|
||
|
||
## 22. Архитектура после Build 008
|
||
|
||
После завершения Build 008 новая часть подсистемы имеет следующую структуру:
|
||
|
||
```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
|
||
│
|
||
└── adapters/
|
||
└── dzengi/
|
||
├── models.py
|
||
├── parser.py
|
||
├── mapper.py
|
||
└── rest.py
|
||
├── _PayloadRestClient
|
||
└── DzengiInstrumentDocumentSource
|
||
```
|
||
|
||
На текущем этапе ещё не реализованы:
|
||
|
||
```text
|
||
instrument_handler.py
|
||
instrument_feed.py
|
||
registry.py
|
||
service.py
|
||
```
|
||
|
||
---
|
||
|
||
## 23. Полная архитектурная цепочка после Build 008
|
||
|
||
Теперь реализован транспортный этап:
|
||
|
||
```text
|
||
Dzengi REST API
|
||
↓
|
||
ExchangeRestClient.get_payload()
|
||
↓
|
||
DzengiInstrumentDocumentSource
|
||
↓
|
||
object
|
||
```
|
||
|
||
Уже реализован processing pipeline:
|
||
|
||
```text
|
||
object
|
||
↓
|
||
validate_exchange_info_schema()
|
||
↓
|
||
ValidatedExchangeInfoDocument
|
||
↓
|
||
parse_exchange_info()
|
||
↓
|
||
DzengiExchangeInfoResponse
|
||
↓
|
||
validate_exchange_info_values()
|
||
↓
|
||
map_dzengi_exchange_info_to_instruments()
|
||
↓
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
Однако эти две части пока намеренно не соединены.
|
||
|
||
Их соединение относится к:
|
||
|
||
```text
|
||
Build 009 — Instrument Handler
|
||
```
|
||
|
||
---
|
||
|
||
## 24. Влияние на legacy-систему
|
||
|
||
Build 008 не подключён к существующим компонентам:
|
||
|
||
```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-путь продолжает работать без изменений.
|
||
|
||
Новый adapter существует изолированно и пока вызывается только unit-тестами.
|
||
|
||
---
|
||
|
||
## 25. Обратная совместимость
|
||
|
||
Подтверждено сохранение:
|
||
|
||
```text
|
||
сигнатур существующих методов;
|
||
старых импортов;
|
||
существующего формата legacy-ошибок;
|
||
Telegram UI;
|
||
автоторговли;
|
||
runtime-поведения;
|
||
legacy ExchangeSymbol;
|
||
legacy-кэша.
|
||
```
|
||
|
||
Build 008 имеет полную обратную совместимость.
|
||
|
||
---
|
||
|
||
## 26. Классификация изменений
|
||
|
||
| Изменение | Классификация |
|
||
|---|---|
|
||
| `DzengiInstrumentDocumentSource` | Обязательное архитектурное изменение |
|
||
| Переиспользование `ExchangeRestClient` | Временная переходная зависимость |
|
||
| `_PayloadRestClient` | Необходимый приватный structural contract |
|
||
| Dependency injection клиента | Улучшение тестируемости и типизации |
|
||
| Использование `get_payload()` | Обязательное разделение transport и schema validation |
|
||
| `InstrumentReferenceTransportError` в production adapter | Обязательная граница ошибок новой подсистемы |
|
||
| Exception chaining | Улучшение диагностируемости |
|
||
| Изменение legacy REST-клиента | Отсутствует |
|
||
| Изменение production-поведения | Отсутствует |
|
||
|
||
---
|
||
|
||
## 27. Условие завершения Build 008
|
||
|
||
Все условия выполнены:
|
||
|
||
```text
|
||
DzengiInstrumentDocumentSource
|
||
реализован;
|
||
|
||
InstrumentDocumentSource
|
||
соблюдён;
|
||
|
||
endpoint /api/v1/exchangeInfo
|
||
вызывается через ExchangeRestClient.get_payload();
|
||
|
||
сырой декодированный документ
|
||
возвращается без преобразования;
|
||
|
||
transport exceptions
|
||
преобразуются в InstrumentReferenceTransportError;
|
||
|
||
исходная причина ошибки
|
||
сохраняется через exception chaining;
|
||
|
||
unit-тесты
|
||
не выполняют реальных сетевых запросов;
|
||
|
||
полный pytest
|
||
проходит;
|
||
|
||
adapter
|
||
не подключён к production runtime.
|
||
```
|
||
|
||
---
|
||
|
||
## 28. Итог Build 008
|
||
|
||
Build 008 завершён успешно.
|
||
|
||
Реализовано:
|
||
|
||
```text
|
||
DzengiInstrumentDocumentSource
|
||
_PayloadRestClient
|
||
transport error boundary
|
||
dependency injection REST-клиента
|
||
получение /api/v1/exchangeInfo через get_payload()
|
||
```
|
||
|
||
Подтверждено:
|
||
|
||
```text
|
||
10 REST adapter tests passed
|
||
123 total project tests passed
|
||
Python compilation passed
|
||
No premature production integration detected
|
||
Legacy bot behavior unchanged
|
||
```
|
||
|
||
Итоговый статус:
|
||
|
||
```text
|
||
BUILD 008 — COMPLETE
|
||
```
|
||
|
||
---
|
||
|
||
## 29. Следующий этап
|
||
|
||
Следующий этап утверждённого плана:
|
||
|
||
```text
|
||
Build 009 — Instrument Handler
|
||
```
|
||
|
||
Его задача — соединить уже реализованный processing pipeline:
|
||
|
||
```text
|
||
object
|
||
↓
|
||
validate_exchange_info_schema()
|
||
↓
|
||
parse_exchange_info()
|
||
↓
|
||
validate_exchange_info_values()
|
||
↓
|
||
map_dzengi_exchange_info_to_instruments()
|
||
↓
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
в конкретную реализацию контракта:
|
||
|
||
```text
|
||
InstrumentDocumentHandler
|
||
```
|
||
|
||
Предполагаемая архитектурная граница Build 009:
|
||
|
||
```text
|
||
object
|
||
↓
|
||
Instrument Handler
|
||
↓
|
||
tuple[Instrument, ...]
|
||
```
|
||
|
||
Build 009 не должен:
|
||
|
||
```text
|
||
самостоятельно выполнять HTTP-запросы;
|
||
создавать Instrument Feed;
|
||
создавать Registry;
|
||
создавать Acquisition Service;
|
||
изменять ExchangeService;
|
||
подключаться к production runtime.
|
||
``` |