# 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