build 039: complete Quotes Feed migration foundation

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit 7b62873832
443 changed files with 80452 additions and 1335 deletions

View File

@@ -0,0 +1,564 @@
# 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