# 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
```