564 lines
18 KiB
Markdown
564 lines
18 KiB
Markdown
# 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 |