544 lines
13 KiB
Markdown
544 lines
13 KiB
Markdown
# 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
|
||
``` |