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