Files
dzentra_bot/docs/migrations/build_017.md

18 KiB
Raw Permalink Blame History

Build 017 — Переключение validate_symbol() на новый механизм разрешения символов

Статус

COMPLETE


Цель Build

Перевести существующий метод ExchangeService.validate_symbol() с legacy-алгоритма поиска инструмента на новый канонический механизм разрешения символов, расположенный в подсистеме src/market_data/acquisition/.

При этом необходимо сохранить без изменений существующий внешний контракт legacy-системы SymbolValidationResult и обеспечить полную обратную совместимость существующего бота.


Архитектурный контекст

До Build 017 метод ExchangeService.validate_symbol() самостоятельно выполнял разрешение символа через symbol_candidates() и вложенный цикл по результату get_exchange_symbols().

Фактически внутри ExchangeService находилась собственная логика поиска соответствующего торгового инструмента.

После завершения Build 016 канонические функции normalize_symbol() и symbol_candidates() уже были перенесены в:

src/market_data/acquisition/symbols.py

Build 017 продолжает этот переход и выносит непосредственно алгоритм разрешения символа в новую подсистему Market Data Acquisition.


Реализованная архитектура

Каноническая логика работы с идентификаторами инструментов теперь находится в:

src/market_data/acquisition/symbols.py

Файл содержит:

normalize_symbol()
symbol_candidates()
resolve_symbol_index()

Распределение ответственности:

normalize_symbol()
        │
        ▼
Нормализация входного идентификатора
        │
        ▼
symbol_candidates()
        │
        ▼
Формирование упорядоченного набора кандидатов
        │
        ▼
resolve_symbol_index()
        │
        ▼
Поиск первого подходящего символа
в доступной последовательности
        │
        ▼
ExchangeService.validate_symbol()
        │
        ▼
Формирование legacy SymbolValidationResult

Таким образом:

  • market_data/acquisition/symbols.py отвечает за каноническую логику разрешения идентификатора;
  • ExchangeService.validate_symbol() отвечает за сохранение legacy API и формирование SymbolValidationResult;
  • get_exchange_symbols() остаётся compatibility boundary между новой моделью Instrument и legacy-моделью ExchangeSymbol.

Новый канонический resolver

В файле:

src/market_data/acquisition/symbols.py

добавлена функция:

def resolve_symbol_index(
    raw_symbol: str,
    available_symbols: Sequence[str],
) -> int | None:

Её ответственность:

  1. Получить исходный идентификатор инструмента.
  2. Сформировать кандидаты через symbol_candidates().
  3. Последовательно проверить кандидатов.
  4. Последовательно проверить доступные символы.
  5. Вернуть индекс первого совпавшего символа.
  6. Вернуть None, если соответствие отсутствует.

Возврат индекса, а не самого объекта, позволяет resolver оставаться независимым от:

ExchangeSymbol
Instrument
SymbolValidationResult
ExchangeService

и работать только со строковыми идентификаторами.


Сохранённый порядок разрешения

Build 017 сохраняет существующую семантику legacy-реализации.

Приоритет определяется в следующем порядке:

1. Порядок кандидатов из symbol_candidates()
2. Порядок available_symbols
3. Первое найденное совпадение

Это означает, что resolver сохраняет:

  • приоритет исходного нормализованного значения;
  • приоритет декодированного варианта %2F;
  • приоритет варианта без внутренних пробелов;
  • порядок инструментов источника;
  • возврат первого совпадения при наличии дубликатов.

Изменение ExchangeService.validate_symbol()

Метод:

ExchangeService.validate_symbol()

сохранил существующий публичный контракт:

def validate_symbol(
    self,
    raw_symbol: str,
) -> SymbolValidationResult:

Сохраняются все существующие сценарии результата:

Пустой символ

Возвращается:

SymbolValidationResult(
    requested_symbol=requested,
    normalized_symbol="",
    is_valid=False,
    message="Символ пустой.",
    symbol_info=None,
)

Mock mode

При отключённой реальной бирже возвращается успешный результат без symbol_info:

SymbolValidationResult(
    requested_symbol=requested,
    normalized_symbol=requested,
    is_valid=True,
    message="Mock mode active.",
    symbol_info=None,
)

Найденный символ

При успешном разрешении возвращается исходный объект ExchangeSymbol из списка get_exchange_symbols():

SymbolValidationResult(
    requested_symbol=requested,
    normalized_symbol=normalize_symbol(symbol_info.symbol),
    is_valid=True,
    message="Символ найден в exchangeInfo.",
    symbol_info=symbol_info,
)

Символ не найден

Возвращается прежний отрицательный результат:

SymbolValidationResult(
    requested_symbol=requested,
    normalized_symbol=requested,
    is_valid=False,
    message=f"Символ '{requested}' не найден в exchangeInfo.",
    symbol_info=None,
)

Таким образом, потребители validate_symbol() не требуют изменений.


Новый поток данных

После Build 017 полный путь разрешения символа выглядит следующим образом:

raw_symbol
    │
    ▼
ExchangeService.validate_symbol()
    │
    ▼
normalize_symbol()
    │
    ▼
ExchangeService.get_exchange_symbols()
    │
    ▼
tuple[Instrument, ...]
    │
    ▼
Compatibility mapper
    │
    ▼
list[ExchangeSymbol]
    │
    ▼
resolve_symbol_index()
    │
    ▼
matched index
    │
    ▼
исходный ExchangeSymbol
    │
    ▼
SymbolValidationResult

При этом validate_symbol():

  • не обращается напрямую к acquisition adapter;
  • не создаёт InstrumentAcquisitionService;
  • не выполняет REST-запрос;
  • не выполняет parsing;
  • не выполняет validation входного exchangeInfo;
  • не выполняет mapping Instrument → ExchangeSymbol;
  • не управляет cache;
  • использует только публичный legacy boundary get_exchange_symbols().

Сохранение compatibility boundary

Build 017 намеренно не переводит validate_symbol() на прямую работу с Instrument.

Текущий compatibility boundary остаётся следующим:

Instrument Acquisition
        │
        ▼
tuple[Instrument, ...]
        │
        ▼
compatibility.py
        │
        ▼
list[ExchangeSymbol]
        │
        ▼
ExchangeService.get_exchange_symbols()
        │
        ▼
ExchangeService.validate_symbol()
        │
        ▼
SymbolValidationResult

Это позволяет продолжать поэтапную миграцию без нарушения работы существующего бота.


Сохранение SymbolValidationResult

Legacy-модель:

src/integrations/exchange/models.py

остаётся без изменений.

Контракт:

class SymbolValidationResult:
    requested_symbol: str
    normalized_symbol: str
    is_valid: bool
    message: str
    symbol_info: ExchangeSymbol | None

сохранён полностью.

Это важно, поскольку validate_symbol() используется существующими runtime-компонентами, включая:

  • получение статуса торгового инструмента;
  • получение комиссии;
  • получение свечей;
  • получение цены;
  • market snapshot;
  • execution snapshot;
  • свежий REST snapshot;
  • market stream;
  • market data runner;
  • Telegram handlers.

Сохранение object identity

При успешном разрешении символа validate_symbol() возвращает тот же экземпляр ExchangeSymbol, который находится в результате get_exchange_symbols().

То есть сохраняется условие:

result.symbol_info is symbol

Это предотвращает:

  • создание лишних копий legacy-моделей;
  • изменение object identity;
  • расхождение между cache и результатом validation;
  • скрытые изменения поведения существующих потребителей.

Изменённые файлы

src/market_data/acquisition/symbols.py

Добавлена каноническая функция:

resolve_symbol_index()

Функция выполняет независимое разрешение строкового идентификатора по последовательности доступных символов.

src/integrations/exchange/service.py

Метод:

validate_symbol()

переключён со встроенного двойного цикла на:

resolve_symbol_index()

При этом сохранены:

  • вызов get_exchange_symbols();
  • SymbolValidationResult;
  • тексты сообщений;
  • mock mode;
  • порядок разрешения;
  • возврат исходного объекта ExchangeSymbol.

tests/unit/market_data/acquisition/test_symbols.py

Добавлены unit-тесты для resolve_symbol_index().

tests/unit/integrations/exchange/test_service_validate_symbol.py

Добавлен отдельный набор unit-тестов для публичного legacy-контракта ExchangeService.validate_symbol().


Тестовое покрытие

Канонический symbol resolver

Выполнена команда:

python -m pytest \
  tests/unit/market_data/acquisition/test_symbols.py \
  -q

Результат:

41 passed in 0.02s

Проверены:

  • точное совпадение;
  • регистронезависимое совпадение;
  • внешние пробелы;
  • %2F;
  • внутренние пробелы;
  • отсутствующий символ;
  • пустой запрос;
  • пустая последовательность доступных символов;
  • приоритет кандидатов;
  • порядок доступных символов;
  • первый дубликат;
  • отсутствие изменения входной последовательности.

Тестирование ExchangeService.validate_symbol()

Выполнена команда:

python -m pytest \
  tests/unit/integrations/exchange/test_service_validate_symbol.py \
  -q

Результат:

16 passed in 0.08s

Проверены:

  • отклонение пустого символа;
  • mock mode;
  • точное совпадение;
  • регистронезависимость;
  • внешние пробелы;
  • encoded separator %2F;
  • внутренние пробелы;
  • отсутствующий символ;
  • возврат исходного ExchangeSymbol;
  • нормализация фактически найденного символа;
  • сохранение success message;
  • единственный вызов get_exchange_symbols();
  • отсутствие прямого обращения к acquisition;
  • сохранение candidate priority;
  • возврат первого duplicate;
  • сохранение типа SymbolValidationResult.

Проверка компиляции

Выполнена команда:

python -m py_compile \
  src/market_data/acquisition/symbols.py \
  src/integrations/exchange/service.py \
  tests/unit/market_data/acquisition/test_symbols.py \
  tests/unit/integrations/exchange/test_service_validate_symbol.py

Результат:

Успешно.
Ошибок компиляции нет.

Полный regression suite

Выполнена команда:

python -m pytest -q

Результат:

299 passed in 0.17s

Регрессий в существующем проекте не обнаружено.


Финальная архитектурная проверка

Выполнен поиск:

grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  -E "validate_symbol|resolve_symbol_index|symbol_candidates|normalize_symbol|SymbolValidationResult|symbol_info" \
  src tests

Проверка подтвердила:

  • каноническая реализация normalize_symbol() находится в src/market_data/acquisition/symbols.py;
  • каноническая реализация symbol_candidates() находится там же;
  • resolve_symbol_index() находится там же;
  • ExchangeService.validate_symbol() использует resolve_symbol_index();
  • старого двойного цикла внутри validate_symbol() больше нет;
  • ExchangeService использует канонические функции новой подсистемы;
  • legacy facade src/integrations/exchange/symbol_utils.py сохранён;
  • SymbolValidationResult сохранён;
  • symbol_info: ExchangeSymbol | None сохранён;
  • новый cache не добавлен;
  • прямого обращения validate_symbol() к acquisition adapter нет;
  • дублирования алгоритма разрешения символа не обнаружено.

Архитектурные гарантии после Build 017

После завершения Build 017 выполняются следующие гарантии:

  1. Каноническая логика разрешения символов принадлежит market_data/acquisition.
  2. ExchangeService.validate_symbol() больше не содержит собственного алгоритма поиска совпадения.
  3. validate_symbol() продолжает использовать get_exchange_symbols() как compatibility boundary.
  4. Публичный контракт SymbolValidationResult не изменён.
  5. symbol_info продолжает иметь тип ExchangeSymbol | None.
  6. Object identity найденного ExchangeSymbol сохраняется.
  7. Порядок кандидатов сохраняется.
  8. Порядок доступных символов сохраняется.
  9. При дубликатах возвращается первый найденный объект.
  10. Новый cache не введён.
  11. Существующий _exchange_symbols_cache продолжает работать без изменений.
  12. Legacy facade symbol_utils.py сохранён.
  13. Все существующие тесты проекта проходят.

Что намеренно не входит в Build 017

Build 017 не выполняет:

  • удаление SymbolValidationResult;
  • перевод всех потребителей на Instrument;
  • удаление ExchangeSymbol;
  • удаление compatibility mapper;
  • удаление _exchange_symbols_cache;
  • прямую работу validate_symbol() с tuple[Instrument, ...];
  • создание нового instrument cache;
  • изменение публичного API ExchangeService;
  • изменение runtime-логики бота;
  • изменение торговой логики.

Эти изменения должны выполняться отдельными контролируемыми Build-шагами.


Итог

Build 017 завершён успешно.

Реализовано переключение:

ExchangeService.validate_symbol()
    │
    ▼
legacy inline symbol matching

на:

ExchangeService.validate_symbol()
    │
    ▼
market_data.acquisition.resolve_symbol_index()

При этом полностью сохранены:

  • публичный legacy-контракт;
  • SymbolValidationResult;
  • ExchangeSymbol;
  • symbol_info;
  • object identity;
  • порядок разрешения;
  • mock mode;
  • compatibility boundary;
  • cache;
  • работа существующего бота.

Финальный результат:

BUILD 017 — COMPLETE

Полный regression suite:

299 passed in 0.17s