Files
dzentra_bot/docs/migrations/build_015.md

552 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```