Files
dzentra_bot/docs/migrations/build_017.md

564 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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