Files
dzentra_bot/docs/migrations/build_015.md

14 KiB
Raw Permalink Blame History

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:

  1. создавал ExchangeRestClient;

  2. выполнял прямой REST-запрос:

    /api/v1/exchangeInfo
    
  3. извлекал массив symbols;

  4. преобразовывал каждый элемент в legacy-модель ExchangeSymbol;

  5. сохранял результат в 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]:

Его задача:

  1. создать источник Instrument Reference Data для Dzengi;
  2. создать обработчик документа;
  3. собрать InstrumentFeed;
  4. зарегистрировать feed;
  5. выполнить acquisition через InstrumentAcquisitionService;
  6. получить канонические модели Instrument;
  7. преобразовать их в 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.

При ошибке:

  1. ошибка логируется через:

    self._log_exchange_error(...)
    
  2. используется legacy endpoint identifier:

    exchangeInfo
    
  3. вызывающему коду возвращается совместимая 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