feat: add market data architecture and complete migration through build 039
This commit is contained in:
544
docs/migrations/build_016.md
Normal file
544
docs/migrations/build_016.md
Normal file
@@ -0,0 +1,544 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user