# Build 016 — Перевод `normalize_symbol()` / `symbol_candidates()` ## Статус **COMPLETE** --- ## Цель Build Перенести каноническую реализацию функций нормализации и формирования кандидатов торгового символа из legacy-подсистемы: ```text src/integrations/exchange/symbol_utils.py ``` в новую подсистему Market Data Acquisition: ```text src/market_data/acquisition/symbols.py ``` при этом: - полностью сохранить существующее поведение; - не нарушить работу legacy-кода; - сохранить старый import path; - исключить дублирование реализации; - обеспечить постепенную миграцию без остановки работающего бота. --- ## Исходное состояние До Build 016 функции: ```python normalize_symbol() symbol_candidates() ``` были реализованы непосредственно в: ```text src/integrations/exchange/symbol_utils.py ``` Их использовал: ```text src/integrations/exchange/service.py ``` В частности, функции участвовали в: - нормализации запрошенного торгового символа; - проверке существования инструмента; - формировании альтернативных представлений символа; - сопоставлении символов с данными `exchangeInfo`; - поиске торговой комиссии для инструмента. Legacy-зависимость выглядела следующим образом: ```text ExchangeService │ ▼ src.integrations.exchange.symbol_utils │ ├── normalize_symbol() └── symbol_candidates() ``` --- ## Целевая архитектура После Build 016 каноническая реализация находится в: ```text src/market_data/acquisition/symbols.py ``` Legacy-модуль: ```text src/integrations/exchange/symbol_utils.py ``` сохранён как compatibility facade. Итоговая зависимость: ```text ExchangeService │ ▼ src.integrations.exchange.symbol_utils │ │ compatibility facade ▼ src.market_data.acquisition.symbols │ ├── normalize_symbol() └── symbol_candidates() ``` Это позволяет сохранить существующий production-код без массового изменения импортов и одновременно установить новую каноническую точку владения логикой. --- ## Созданный файл Создан: ```text src/market_data/acquisition/symbols.py ``` Он содержит единственную каноническую реализацию: ```python def normalize_symbol(raw_symbol: str) -> str: ... def symbol_candidates(raw_symbol: str) -> list[str]: ... ``` --- ## Изменённый legacy-модуль Файл: ```text src/integrations/exchange/symbol_utils.py ``` больше не содержит собственной реализации алгоритмов. Он импортирует функции непосредственно из: ```text src.market_data.acquisition.symbols ``` и предоставляет их через прежний import path. Таким образом: ```python legacy_normalize_symbol is new_normalize_symbol ``` и: ```python legacy_symbol_candidates is new_symbol_candidates ``` возвращают: ```text True ``` Это подтверждает отсутствие копирования или дублирования функций. --- ## Зафиксированный контракт `normalize_symbol()` Функция сохраняет существующее legacy-поведение. ### Нормализация регистра Пример: ```text btc/usd ``` преобразуется в: ```text BTC/USD ``` ### Удаление внешних пробелов Пример: ```text btc/usd ``` преобразуется в: ```text BTC/USD ``` ### Внутренние пробелы не удаляются Функция `normalize_symbol()` не выполняет удаление внутренних пробелов. ### `%2F` не декодируется Пример: ```text BTC%2FUSD ``` остаётся: ```text BTC%2FUSD ``` Декодирование разделителя выполняется только на этапе формирования кандидатов. ### Суффикс `_LEVERAGE` не добавляется автоматически Функция не модифицирует семантику инструмента и не добавляет leverage-суффикс. ### Существующий `_LEVERAGE` сохраняется Если суффикс уже присутствует в исходном символе, он сохраняется. --- ## Зафиксированный контракт `symbol_candidates()` Функция формирует упорядоченный список допустимых представлений символа. ### Пустое значение Для пустого значения возвращается: ```python [] ``` ### Первый кандидат Первым всегда является результат: ```python normalize_symbol(raw_symbol) ``` ### Декодирование `%2F` Если символ содержит: ```text %2F ``` добавляется кандидат с: ```text / ``` ### Удаление обычных внутренних пробелов После декодирования разделителя формируется вариант без обычных пробелов. ### Порядок преобразований сохраняется Порядок кандидатов является частью compatibility-контракта: ```text 1. Нормализованное исходное значение. 2. Значение после замены %2F на /. 3. Значение после удаления обычных пробелов. ``` ### Дубликаты не добавляются Если очередное преобразование не изменило значение, новый кандидат не создаётся. ### Каждый вызов возвращает новый список Результат не переиспользует mutable list между вызовами. ### Исходная строка не изменяется Функция не мутирует входное значение. ### Табуляция не удаляется Удаляются только обычные пробелы: ```text " " ``` Внутренняя табуляция сохраняется. ### Перевод строки не удаляется Внутренний символ новой строки сохраняется. ### `_LEVERAGE` сохраняется Функция не изменяет существующий leverage-суффикс. --- ## Добавленные тесты Создан: ```text tests/unit/market_data/acquisition/test_symbols.py ``` Тесты фиксируют канонический контракт новой реализации. Результат: ```text 29 passed in 0.02s ``` Также создан: ```text tests/unit/integrations/exchange/test_symbol_utils.py ``` Этот набор тестов проверяет compatibility facade и эквивалентность legacy и новой реализации. Результат: ```text 22 passed in 0.01s ``` --- ## Проверка identity compatibility Отдельно подтверждено, что legacy facade возвращает непосредственно те же функции: ```python assert legacy_normalize_symbol is new_normalize_symbol assert legacy_symbol_candidates is new_symbol_candidates ``` Следовательно: - отдельной legacy-реализации больше нет; - wrapper-функции отсутствуют; - поведение не может разойтись из-за двух независимых реализаций. --- ## Проверка компиляции Выполнена команда: ```bash python -m py_compile \ src/market_data/acquisition/symbols.py \ src/integrations/exchange/symbol_utils.py \ tests/unit/market_data/acquisition/test_symbols.py \ tests/unit/integrations/exchange/test_symbol_utils.py ``` Результат: ```text Успешно. Синтаксических ошибок нет. ``` --- ## Полный regression suite Выполнена команда: ```bash python -m pytest -q ``` Результат: ```text 271 passed in 0.17s ``` До Build 016 полный набор проекта содержал: ```text 220 passed ``` Build 016 добавил: ```text 51 test ``` Итог: ```text 220 + 51 = 271 ``` Все тесты проекта проходят успешно. --- ## Финальная проверка зависимостей Выполнена команда: ```bash grep -RIn \ --exclude-dir="__pycache__" \ --exclude="*.pyc" \ -E "normalize_symbol|symbol_candidates|def normalize_symbol|def symbol_candidates" \ src tests ``` Проверка подтвердила: - `normalize_symbol()` реализована только в: ```text src/market_data/acquisition/symbols.py ``` - `symbol_candidates()` реализована только там же; - `src/integrations/exchange/symbol_utils.py` содержит только compatibility imports и exports; - `ExchangeService` продолжает использовать существующий стабильный legacy import path; - дублирование реализации отсутствует. --- ## Состояние `ExchangeService` В рамках Build 016 файл: ```text src/integrations/exchange/service.py ``` не требовал изменения import path. Он продолжает использовать: ```python from src.integrations.exchange.symbol_utils import ( normalize_symbol, symbol_candidates, ) ``` Это сделано намеренно. Legacy import path остаётся стабильным, а фактическая реализация уже принадлежит новой подсистеме: ```text src.market_data.acquisition ``` Такой подход соответствует принятой стратегии постепенной миграции: ```text сначала новая каноническая реализация ↓ затем compatibility facade ↓ старый production-код продолжает работать ↓ последующая миграция потребителей выполняется отдельно ``` --- ## Что не изменялось В рамках Build 016 намеренно не изменялись: - публичный контракт `ExchangeService`; - сигнатуры `normalize_symbol()`; - сигнатуры `symbol_candidates()`; - логика `validate_symbol()`; - формат `SymbolValidationResult`; - порядок кандидатов; - алгоритм сопоставления символов; - поведение mock mode; - production-поведение работающего бота. --- ## Архитектурный результат После Build 016 ответственность распределена следующим образом: ```text market_data/acquisition/symbols.py │ └── каноническая логика нормализации и формирования кандидатов символа integrations/exchange/symbol_utils.py │ └── compatibility facade для legacy-кода integrations/exchange/service.py │ └── существующий production consumer ``` Таким образом, новая подсистема получила владение логикой идентификации торговых символов без нарушения существующих зависимостей. --- ## Итоговый статус Все критерии Build 016 выполнены: - каноническая реализация перенесена в `market_data`; - legacy import path сохранён; - дублирование реализации устранено; - существующий контракт зафиксирован тестами; - compatibility facade проверен; - identity функций подтверждена; - компиляция успешна; - полный regression suite успешен; - production-поведение не изменено. ```text BUILD 016 — COMPLETE ```