18 KiB
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:
Её ответственность:
- Получить исходный идентификатор инструмента.
- Сформировать кандидаты через
symbol_candidates(). - Последовательно проверить кандидатов.
- Последовательно проверить доступные символы.
- Вернуть индекс первого совпавшего символа.
- Вернуть
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 выполняются следующие гарантии:
- Каноническая логика разрешения символов принадлежит
market_data/acquisition. ExchangeService.validate_symbol()больше не содержит собственного алгоритма поиска совпадения.validate_symbol()продолжает использоватьget_exchange_symbols()как compatibility boundary.- Публичный контракт
SymbolValidationResultне изменён. symbol_infoпродолжает иметь типExchangeSymbol | None.- Object identity найденного
ExchangeSymbolсохраняется. - Порядок кандидатов сохраняется.
- Порядок доступных символов сохраняется.
- При дубликатах возвращается первый найденный объект.
- Новый cache не введён.
- Существующий
_exchange_symbols_cacheпродолжает работать без изменений. - Legacy facade
symbol_utils.pyсохранён. - Все существующие тесты проекта проходят.
Что намеренно не входит в 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