552 lines
14 KiB
Markdown
552 lines
14 KiB
Markdown
# Build 015 — Переключение `get_exchange_symbols()` на новый Acquisition Pipeline
|
||
|
||
## Статус
|
||
|
||
**COMPLETE**
|
||
|
||
---
|
||
|
||
## Цель
|
||
|
||
Переключить существующий публичный метод:
|
||
|
||
```python
|
||
ExchangeService.get_exchange_symbols()
|
||
```
|
||
|
||
с прямого legacy-получения и обработки `exchangeInfo` на новый стандартизированный Instrument Reference Data acquisition pipeline, сохранив при этом существующий внешний контракт и работоспособность старого бота.
|
||
|
||
---
|
||
|
||
## Исходное состояние
|
||
|
||
До Build 015 метод:
|
||
|
||
```python
|
||
ExchangeService.get_exchange_symbols()
|
||
```
|
||
|
||
самостоятельно выполнял весь цикл обработки `exchangeInfo`:
|
||
|
||
1. создавал `ExchangeRestClient`;
|
||
2. выполнял прямой REST-запрос:
|
||
|
||
```text
|
||
/api/v1/exchangeInfo
|
||
```
|
||
|
||
3. извлекал массив `symbols`;
|
||
4. преобразовывал каждый элемент в legacy-модель `ExchangeSymbol`;
|
||
5. сохранял результат в class-level cache:
|
||
|
||
```python
|
||
_exchange_symbols_cache
|
||
```
|
||
|
||
Таким образом, transport, validation, parsing, mapping и compatibility logic были сосредоточены внутри legacy `ExchangeService`.
|
||
|
||
---
|
||
|
||
## Реализованное изменение
|
||
|
||
Метод:
|
||
|
||
```python
|
||
ExchangeService.get_exchange_symbols()
|
||
```
|
||
|
||
переключён на новый Instrument Reference Data acquisition pipeline.
|
||
|
||
Теперь production-путь использует следующую цепочку:
|
||
|
||
```text
|
||
ExchangeService.get_exchange_symbols()
|
||
│
|
||
▼
|
||
_load_exchange_symbols_via_acquisition()
|
||
│
|
||
▼
|
||
DzengiInstrumentDocumentSource
|
||
│
|
||
▼
|
||
DzengiInstrumentDocumentHandler
|
||
│
|
||
▼
|
||
InstrumentFeed
|
||
│
|
||
▼
|
||
InstrumentFeedRegistry
|
||
│
|
||
▼
|
||
InstrumentAcquisitionService
|
||
│
|
||
▼
|
||
Instrument
|
||
│
|
||
▼
|
||
map_instruments_to_exchange_symbols()
|
||
│
|
||
▼
|
||
ExchangeSymbol
|
||
```
|
||
|
||
---
|
||
|
||
## Новый production-путь
|
||
|
||
В `ExchangeService` используется отдельный compatibility bridge:
|
||
|
||
```python
|
||
def _load_exchange_symbols_via_acquisition(
|
||
self,
|
||
) -> list[ExchangeSymbol]:
|
||
```
|
||
|
||
Его задача:
|
||
|
||
1. создать источник Instrument Reference Data для Dzengi;
|
||
2. создать обработчик документа;
|
||
3. собрать `InstrumentFeed`;
|
||
4. зарегистрировать feed;
|
||
5. выполнить acquisition через `InstrumentAcquisitionService`;
|
||
6. получить канонические модели `Instrument`;
|
||
7. преобразовать их в legacy-модели `ExchangeSymbol`.
|
||
|
||
Это позволяет старому боту продолжать использовать существующий контракт:
|
||
|
||
```python
|
||
list[ExchangeSymbol]
|
||
```
|
||
|
||
при том, что фактическим источником данных уже является новая архитектура `market_data/acquisition`.
|
||
|
||
---
|
||
|
||
## Сохранённый публичный контракт
|
||
|
||
Сигнатура метода не изменилась:
|
||
|
||
```python
|
||
def get_exchange_symbols(self) -> list[ExchangeSymbol]:
|
||
```
|
||
|
||
Это принципиально важно для безопасной поэтапной миграции.
|
||
|
||
Существующие потребители не требуют немедленного изменения и продолжают работать через прежний API.
|
||
|
||
В частности, существующий UI продолжает использовать:
|
||
|
||
```python
|
||
exchange_service.get_exchange_symbols()
|
||
```
|
||
|
||
без знания о внутреннем переходе на новый acquisition pipeline.
|
||
|
||
---
|
||
|
||
## Сохранение cache semantics
|
||
|
||
Сохранён существующий class-level cache:
|
||
|
||
```python
|
||
_exchange_symbols_cache: list[ExchangeSymbol] | None = None
|
||
```
|
||
|
||
Поведение осталось прежним:
|
||
|
||
```text
|
||
Первый вызов
|
||
│
|
||
▼
|
||
Новый acquisition pipeline
|
||
│
|
||
▼
|
||
Compatibility mapping
|
||
│
|
||
▼
|
||
_exchange_symbols_cache
|
||
│
|
||
▼
|
||
list[ExchangeSymbol]
|
||
```
|
||
|
||
Последующие вызовы:
|
||
|
||
```text
|
||
_exchange_symbols_cache
|
||
│
|
||
▼
|
||
list[ExchangeSymbol]
|
||
```
|
||
|
||
без повторного обращения к acquisition pipeline.
|
||
|
||
Cache заполняется только после успешной загрузки данных.
|
||
|
||
При ошибке acquisition cache остаётся незаполненным.
|
||
|
||
---
|
||
|
||
## Поведение при отключённой бирже
|
||
|
||
Сохранено прежнее поведение:
|
||
|
||
```python
|
||
if not self.settings.exchange_enabled:
|
||
return []
|
||
```
|
||
|
||
Новый acquisition pipeline в этом случае не вызывается.
|
||
|
||
---
|
||
|
||
## Обработка ошибок
|
||
|
||
Ошибки нового acquisition pipeline проходят через существующую систему `ExchangeService`.
|
||
|
||
При ошибке:
|
||
|
||
1. ошибка логируется через:
|
||
|
||
```python
|
||
self._log_exchange_error(...)
|
||
```
|
||
|
||
2. используется legacy endpoint identifier:
|
||
|
||
```text
|
||
exchangeInfo
|
||
```
|
||
|
||
3. вызывающему коду возвращается совместимая `ExchangeError`.
|
||
|
||
Это сохраняет существующее поведение старого бота и его журналирования.
|
||
|
||
---
|
||
|
||
## Удаление прямого legacy REST-пути
|
||
|
||
После Build 015 метод:
|
||
|
||
```python
|
||
get_exchange_symbols()
|
||
```
|
||
|
||
больше не выполняет прямой вызов:
|
||
|
||
```python
|
||
ExchangeRestClient().get_json("/api/v1/exchangeInfo")
|
||
```
|
||
|
||
Фактический REST transport теперь инкапсулирован в:
|
||
|
||
```text
|
||
src/market_data/acquisition/adapters/dzengi/rest.py
|
||
```
|
||
|
||
через:
|
||
|
||
```python
|
||
DzengiInstrumentDocumentSource
|
||
```
|
||
|
||
и константу:
|
||
|
||
```python
|
||
_EXCHANGE_INFO_PATH = "/api/v1/exchangeInfo"
|
||
```
|
||
|
||
Таким образом, ownership получения Instrument Reference Data перенесён из:
|
||
|
||
```text
|
||
integrations/exchange
|
||
```
|
||
|
||
в:
|
||
|
||
```text
|
||
market_data/acquisition
|
||
```
|
||
|
||
---
|
||
|
||
## Legacy helpers
|
||
|
||
В `ExchangeService` временно остаются legacy helpers:
|
||
|
||
```python
|
||
_extract_exchange_symbols_raw()
|
||
_parse_exchange_symbol()
|
||
_parse_exchange_symbol_status()
|
||
_parse_market_modes()
|
||
_extract_filter_value()
|
||
```
|
||
|
||
Они больше не являются частью нового production-пути `get_exchange_symbols()`.
|
||
|
||
Их немедленное удаление не выполнялось в Build 015, поскольку миграция проводится поэтапно и без ненужного расширения scope текущего Build.
|
||
|
||
Удаление legacy helpers должно выполняться отдельным контролируемым этапом после подтверждения отсутствия production-зависимостей и завершения необходимых migration/equivalence проверок.
|
||
|
||
---
|
||
|
||
## Добавленные тесты
|
||
|
||
Создан файл:
|
||
|
||
```text
|
||
tests/unit/integrations/exchange/test_service_exchange_symbols.py
|
||
```
|
||
|
||
Тестами проверяются:
|
||
|
||
- возврат пустого списка при отключённой бирже;
|
||
- отсутствие вызова acquisition pipeline при отключённой бирже;
|
||
- возврат существующего cache;
|
||
- отсутствие повторного acquisition при наличии cache;
|
||
- загрузка через новый acquisition pipeline;
|
||
- заполнение `_exchange_symbols_cache`;
|
||
- повторное использование cache;
|
||
- сохранение legacy-типа `ExchangeSymbol`;
|
||
- сохранение порядка инструментов;
|
||
- корректное распространение ошибок;
|
||
- отсутствие заполнения cache при ошибке;
|
||
- сохранение существующего error logging;
|
||
- отсутствие прямого legacy REST-вызова из `get_exchange_symbols()`;
|
||
- корректная сборка нового acquisition pipeline;
|
||
- использование compatibility mapper;
|
||
- корректное поведение пустого результата.
|
||
|
||
---
|
||
|
||
## Исправление статической типизации теста
|
||
|
||
После первоначального завершения Build 015 в файле:
|
||
|
||
```text
|
||
tests/unit/integrations/exchange/test_service_exchange_symbols.py
|
||
```
|
||
|
||
были обнаружены две ошибки статической типизации Pylance.
|
||
|
||
### Типизация yield-fixture
|
||
|
||
Исходная аннотация:
|
||
|
||
```python
|
||
@pytest.fixture(autouse=True)
|
||
def reset_exchange_symbols_cache() -> None:
|
||
```
|
||
|
||
была некорректна, поскольку функция содержит `yield` и является генератором.
|
||
|
||
Исправлено на:
|
||
|
||
```python
|
||
@pytest.fixture(autouse=True)
|
||
def reset_exchange_symbols_cache() -> Iterator[None]:
|
||
ExchangeService._exchange_symbols_cache = None
|
||
|
||
yield
|
||
|
||
ExchangeService._exchange_symbols_cache = None
|
||
```
|
||
|
||
Добавлен импорт:
|
||
|
||
```python
|
||
from collections.abc import Iterator
|
||
```
|
||
|
||
### Типизация тестовых settings
|
||
|
||
Тестовый helper создаёт `ExchangeService` без вызова его конструктора:
|
||
|
||
```python
|
||
service = object.__new__(ExchangeService)
|
||
```
|
||
|
||
Для изоляции теста используется `SimpleNamespace`, тогда как production-атрибут:
|
||
|
||
```python
|
||
service.settings
|
||
```
|
||
|
||
типизирован как `Settings`.
|
||
|
||
Для явного обозначения тестовой границы применён `cast`:
|
||
|
||
```python
|
||
service.settings = cast(
|
||
Settings,
|
||
_settings(
|
||
exchange_enabled=exchange_enabled,
|
||
),
|
||
)
|
||
```
|
||
|
||
Таким образом:
|
||
|
||
- production-код не изменялся;
|
||
- тестовая изоляция сохранена;
|
||
- `# type: ignore` не использовался;
|
||
- ошибки Pylance устранены.
|
||
|
||
---
|
||
|
||
## Результаты окончательной проверки
|
||
|
||
### Проверка компиляции
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m py_compile \
|
||
src/integrations/exchange/service.py \
|
||
tests/unit/integrations/exchange/test_service_exchange_symbols.py
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
Ошибок нет.
|
||
```
|
||
|
||
---
|
||
|
||
### Unit-тесты Build 015
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m pytest \
|
||
tests/unit/integrations/exchange/test_service_exchange_symbols.py \
|
||
-q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
16 passed in 0.07s
|
||
```
|
||
|
||
---
|
||
|
||
### Полный regression suite
|
||
|
||
Команда:
|
||
|
||
```bash
|
||
python -m pytest -q
|
||
```
|
||
|
||
Результат:
|
||
|
||
```text
|
||
220 passed in 0.15s
|
||
```
|
||
|
||
---
|
||
|
||
## Проверка production-пути
|
||
|
||
Выполнен поиск:
|
||
|
||
```bash
|
||
grep -RIn \
|
||
--exclude-dir="__pycache__" \
|
||
--exclude="*.pyc" \
|
||
-E "get_exchange_symbols|_exchange_symbols_cache|_load_exchange_symbols_via_acquisition|DzengiInstrumentDocumentSource|InstrumentAcquisitionService|map_instruments_to_exchange_symbols|ExchangeRestClient.*exchangeInfo|exchangeInfo" \
|
||
src tests
|
||
```
|
||
|
||
Проверка подтвердила:
|
||
|
||
- `get_exchange_symbols()` использует `_load_exchange_symbols_via_acquisition()`;
|
||
- новый production-путь использует `DzengiInstrumentDocumentSource`;
|
||
- используется `InstrumentAcquisitionService`;
|
||
- используется compatibility mapper `map_instruments_to_exchange_symbols()`;
|
||
- class-level cache `_exchange_symbols_cache` сохранён;
|
||
- прямой legacy REST-вызов `exchangeInfo` удалён из `get_exchange_symbols()`;
|
||
- существующие внешние потребители продолжают работать через прежний публичный контракт.
|
||
|
||
---
|
||
|
||
## Архитектурный результат
|
||
|
||
До Build 015:
|
||
|
||
```text
|
||
Legacy consumer
|
||
│
|
||
▼
|
||
ExchangeService.get_exchange_symbols()
|
||
│
|
||
▼
|
||
ExchangeRestClient
|
||
│
|
||
▼
|
||
exchangeInfo
|
||
│
|
||
▼
|
||
Legacy parsing
|
||
│
|
||
▼
|
||
ExchangeSymbol
|
||
```
|
||
|
||
После Build 015:
|
||
|
||
```text
|
||
Legacy consumer
|
||
│
|
||
▼
|
||
ExchangeService.get_exchange_symbols()
|
||
│
|
||
▼
|
||
Instrument Acquisition Pipeline
|
||
│
|
||
▼
|
||
Canonical Instrument
|
||
│
|
||
▼
|
||
Compatibility Mapper
|
||
│
|
||
▼
|
||
ExchangeSymbol
|
||
```
|
||
|
||
Таким образом:
|
||
|
||
- новый `market_data/acquisition` стал фактическим production-владельцем получения Instrument Reference Data;
|
||
- legacy `ExchangeService` сохраняет прежний публичный API;
|
||
- существующий бот продолжает работать без массового изменения потребителей;
|
||
- создан безопасный compatibility boundary между новой и старой архитектурой;
|
||
- переход выполнен без регрессий.
|
||
|
||
---
|
||
|
||
## Итог
|
||
|
||
```text
|
||
BUILD 015 — COMPLETE
|
||
```
|
||
|
||
Build 015 завершён.
|
||
|
||
`ExchangeService.get_exchange_symbols()` успешно переключён на новый Instrument Reference Data acquisition pipeline с сохранением:
|
||
|
||
- существующего публичного контракта;
|
||
- legacy-модели `ExchangeSymbol`;
|
||
- cache semantics;
|
||
- обработки ошибок;
|
||
- журналирования;
|
||
- существующих потребителей старого бота.
|
||
|
||
Окончательные результаты проверки:
|
||
|
||
```text
|
||
py_compile — успешно
|
||
16 passed in 0.07s
|
||
220 passed in 0.15s
|
||
``` |