build 039: complete Quotes Feed migration foundation
This commit is contained in:
564
docs/migrations/build_017.md
Normal file
564
docs/migrations/build_017.md
Normal file
@@ -0,0 +1,564 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user