# Build 022 — Перевод валидации торгового символа на канонический `Instrument` **Engineering Build Record** --- ## Контроль документа | Свойство | Значение | |---|---| | Документ | Build 022 — Перевод валидации торгового символа на канонический `Instrument` | | Тип документа | Engineering Build Record | | Build | 022 | | Статус | **Завершён** | | Подсистема | Market Data Acquisition / Exchange Integration | | Проект | Dzentra | | Язык | Русский | | Предыдущий Build | Build 021 — Перевод первой группы потребителей | | Следующий этап | Build 023+ — Поэтапный перевод остальных потребителей | --- ## 1. Назначение Build Build 022 продолжает поэтапную миграцию Dzentra с legacy-модели биржевого инструмента `ExchangeSymbol` на каноническую модель: ```text Instrument ``` Основная задача Build 022: > Перевести механизм валидации торгового символа `ExchangeService.validate_symbol()` и непосредственно связанный с ним runtime-status с legacy-модели `ExchangeSymbol` на канонический справочник `Instrument`, сохранив существующий внешний контракт и работоспособность старого бота. После завершения Build 022 метод: ```python ExchangeService.validate_symbol() ``` больше не использует: ```python get_exchange_symbols() ``` для поиска и валидации торгового инструмента. Каноническим источником данных теперь является: ```python get_instruments() ``` который возвращает: ```python tuple[Instrument, ...] ``` --- ## 2. Архитектурный контекст До начала миграции в системе существовала следующая цепочка: ```text Exchange API ↓ exchangeInfo ↓ ExchangeService.get_exchange_symbols() ↓ list[ExchangeSymbol] ↓ ExchangeService.validate_symbol() ↓ SymbolValidationResult.symbol_info ↓ ExchangeSymbol | None ``` После внедрения нового Market Data Acquisition Layer в системе появился канонический pipeline: ```text Exchange API ↓ Instrument Document Source ↓ Instrument Document Handler ↓ Instrument Feed ↓ Instrument Feed Registry ↓ Instrument Acquisition Service ↓ Instrument Store ↓ tuple[Instrument, ...] ``` На предыдущих этапах был создан новый канонический API: ```python ExchangeService.get_instruments() -> tuple[Instrument, ...] ``` Build 021 перевёл первого внешнего потребителя: ```text src/telegram/ui/currency_ui.py ``` с: ```python get_exchange_symbols() ExchangeSymbol ``` на: ```python get_instruments() Instrument ``` Build 022 продолжает миграцию на более глубоком уровне и переводит на `Instrument` механизм валидации торговых символов. --- ## 3. Состояние до Build 022 До Build 022 метод: ```python ExchangeService.validate_symbol() ``` использовал: ```python symbols = self.get_exchange_symbols() ``` и выполнял поиск среди: ```python list[ExchangeSymbol] ``` Результат валидации имел контракт: ```python @dataclass(slots=True) class SymbolValidationResult: requested_symbol: str normalized_symbol: str is_valid: bool message: str symbol_info: ExchangeSymbol | None ``` Таким образом, несмотря на наличие канонического `Instrument Store`, валидация торгового символа всё ещё зависела от legacy-проекции: ```text Instrument Store ↓ Instrument ↓ compatibility mapper ↓ ExchangeSymbol ↓ validate_symbol() ``` Это создавало лишний промежуточный слой: ```text Instrument → ExchangeSymbol → validation ``` вместо прямой канонической цепочки: ```text Instrument → validation ``` --- ## 4. Целевое состояние Build 022 После Build 022 архитектура валидации выглядит следующим образом: ```text Exchange API ↓ Market Data Acquisition ↓ Instrument Store ↓ ExchangeService.get_instruments() ↓ tuple[Instrument, ...] ↓ ExchangeService.validate_symbol() ↓ SymbolValidationResult ↓ Instrument | None ``` Главное архитектурное правило Build 022: > `validate_symbol()` обязан работать непосредственно с каноническим справочником `Instrument` и не должен использовать legacy-метод `get_exchange_symbols()`. --- ## 5. Изменённые компоненты В рамках Build 022 были изменены следующие файлы: ```text app/src/integrations/exchange/models.py app/src/integrations/exchange/service.py app/tests/unit/integrations/exchange/test_service_validate_symbol.py app/tests/unit/integrations/exchange/test_service_symbol_runtime_status.py ``` --- ## 6. Изменение `SymbolValidationResult` ### До Build 022 Поле: ```python symbol_info ``` содержало: ```python ExchangeSymbol | None ``` Контракт: ```python @dataclass(slots=True) class SymbolValidationResult: requested_symbol: str normalized_symbol: str is_valid: bool message: str symbol_info: ExchangeSymbol | None ``` ### После Build 022 Поле: ```python symbol_info ``` содержит каноническую модель: ```python Instrument | None ``` Итоговый контракт: ```python @dataclass(slots=True) class SymbolValidationResult: requested_symbol: str normalized_symbol: str is_valid: bool message: str symbol_info: Instrument | None ``` Для type checking используется импорт: ```python if TYPE_CHECKING: from src.market_data.acquisition.models.instrument import Instrument ``` Это позволяет избежать ненужной runtime-зависимости модели exchange integration от реализации acquisition model при сохранении корректной типизации. --- ## 7. Изменение `validate_symbol()` ### До Build 022 Метод получал legacy-справочник: ```python symbols = self.get_exchange_symbols() ``` и выполнял поиск среди объектов: ```python ExchangeSymbol ``` Архитектурная цепочка имела вид: ```text validate_symbol() ↓ get_exchange_symbols() ↓ compatibility projection ↓ ExchangeSymbol ``` ### После Build 022 Метод получает канонический справочник: ```python instruments = self.get_instruments() ``` и выполняет поиск непосредственно среди: ```python Instrument ``` Новая цепочка: ```text validate_symbol() ↓ get_instruments() ↓ Instrument Store ↓ Instrument ``` Legacy compatibility projection больше не участвует в валидации торгового символа. --- ## 8. Сохранённая семантика валидации Build 022 не меняет пользовательскую и runtime-семантику `validate_symbol()`. Сохранены следующие сценарии. ### 8.1. Пустой символ Вход: ```text " " ``` Результат: ```python SymbolValidationResult( requested_symbol="", normalized_symbol="", is_valid=False, message="Символ пустой.", symbol_info=None, ) ``` --- ### 8.2. Mock mode При: ```python exchange_enabled=False ``` символ принимается без обращения к каноническому справочнику. Пример результата: ```python SymbolValidationResult( requested_symbol="BTC/USD_LEVERAGE", normalized_symbol="BTC/USD_LEVERAGE", is_valid=True, message="Mock mode active.", symbol_info=None, ) ``` --- ### 8.3. Регистронезависимый поиск Следующие значения считаются эквивалентными: ```text BTC/USD_LEVERAGE btc/usd_leverage ``` --- ### 8.4. Игнорирование внешних пробелов Следующие значения считаются эквивалентными: ```text BTC/USD_LEVERAGE btc/usd_leverage ``` --- ### 8.5. Поддержка encoded separator Поддерживается представление: ```text BTC%2FUSD_LEVERAGE ``` наряду с: ```text BTC/USD_LEVERAGE ``` --- ### 8.6. Поддержка внутренних пробелов Поддерживается нормализация значения: ```text btc / usd_leverage ``` до: ```text BTC/USD_LEVERAGE ``` --- ### 8.7. Отсутствующий инструмент Если инструмент отсутствует в каноническом справочнике, возвращается неуспешный результат: ```python SymbolValidationResult( requested_symbol="XRP/USD_LEVERAGE", normalized_symbol="XRP/USD_LEVERAGE", is_valid=False, message=( "Символ 'XRP/USD_LEVERAGE' " "не найден в exchangeInfo." ), symbol_info=None, ) ``` Сообщение сохранено для обратной совместимости. --- ### 8.8. Сохранение приоритета кандидатов Если разные исходные значения после нормализации могут соответствовать одному символу, сохраняется существующий порядок выбора кандидатов. В частности, точное исходное представление продолжает иметь приоритет в предусмотренных legacy-сценариях. --- ### 8.9. Сохранение первого дубликата Если канонический справочник содержит несколько эквивалентных инструментов, возвращается первый подходящий объект согласно существующему порядку справочника. --- ## 9. Канонический объект в результате валидации После Build 022 успешный результат: ```python validation = service.validate_symbol( "BTC/USD_LEVERAGE" ) ``` содержит: ```python validation.symbol_info ``` типа: ```python Instrument ``` а не: ```python ExchangeSymbol ``` Дополнительно сохранена identity-семантика. Если канонический справочник содержит объект: ```python instrument ``` то после успешной валидации выполняется: ```python validation.symbol_info is instrument ``` То есть `validate_symbol()`: - не создаёт копию `Instrument`; - не создаёт `ExchangeSymbol`; - не выполняет compatibility mapping; - возвращает исходный канонический объект из справочника. --- ## 10. Перевод runtime-status Метод: ```python ExchangeService.get_symbol_runtime_status() ``` использует: ```python validation = self.validate_symbol(symbol_to_use) ``` и получает статус торгового инструмента через: ```python validation.symbol_info.status ``` После изменения контракта: ```python SymbolValidationResult.symbol_info ``` runtime-status теперь фактически получает канонический: ```python Instrument ``` вместо legacy: ```python ExchangeSymbol ``` При этом внешний контракт runtime-status не изменён. Сохраняются: ```text ExchangeRuntimeStatus ExchangeStatusCode get_symbol_market_status() ``` и существующая классификация состояний рынка. --- ## 11. Сохранённая классификация runtime-status Build 022 сохраняет существующую классификацию биржевых статусов. ### Открытый рынок Следующие статусы классифицируются как открытый рынок: ```text TRADING OPEN ACTIVE ENABLED ONLINE ``` Результат: ```text ExchangeStatusCode.OPEN reason = "market_open" ``` --- ### Перерыв или остановка торгов Следующие статусы классифицируются как перерыв: ```text BREAK CLOSED HALT HALTED PAUSED SUSPENDED DISABLED SETTLING POST_ONLY ``` Результат: ```text ExchangeStatusCode.BREAK reason = "market_break" ``` --- ### Инструмент недоступен для торговли Следующие статусы классифицируются как неторгуемые: ```text NOT_TRADABLE TRADING_DISABLED MARKET_DISABLED UNAVAILABLE_FOR_TRADING CLOSE_ONLY REDUCE_ONLY VIEW_ONLY ``` Результат: ```text ExchangeStatusCode.BREAK reason = "market_not_tradable" ``` --- ### Неизвестный статус Неизвестные значения классифицируются как: ```text ExchangeStatusCode.UNKNOWN reason = "market_status_unknown" ``` --- ## 12. Сохранение проверки freshness Build 022 не изменяет существующую проверку свежести рыночных данных. Для открытого рынка runtime-status продолжает проверять: ```python get_fresh_market_snapshot() ``` Если данные устарели, возвращается: ```text ExchangeStatusCode.BREAK reason = "market_data_stale" raw_status = "STALE_MARKET_DATA" ``` Проверка freshness выполняется только для инструмента, который сначала классифицирован как открытый для торговли. Для закрытого или приостановленного рынка получение snapshot не выполняется. --- ## 13. Сохранение legacy runtime-контракта Метод: ```python get_symbol_market_status() ``` продолжает возвращать legacy dictionary contract. Пример: ```python { "code": "BREAK", "status": "BREAK", "symbol": "BTC/USD", "is_open": False, "is_available": True, "is_auth_ok": True, "title": "Перерыв в торгах", "message": "Торги временно остановлены.", "ui_line": "⏸️ Перерыв в торгах", "reason": "market_break", "raw_status": "BREAK", "raw_error": None, } ``` Таким образом, Build 022 меняет внутренний источник reference data, но не ломает существующий внешний runtime-контракт старого бота. --- ## 14. Архитектурные ограничения, закреплённые тестами Build 022 вводит и закрепляет следующие архитектурные правила. ### Правило 1 `validate_symbol()` обязан использовать: ```python get_instruments() ``` --- ### Правило 2 `validate_symbol()` не должен использовать: ```python get_exchange_symbols() ``` --- ### Правило 3 `validate_symbol()` не должен напрямую вызывать: ```python _load_instruments_via_acquisition() ``` Правильная цепочка: ```text validate_symbol() ↓ get_instruments() ↓ Instrument Store ↓ Acquisition Pipeline при необходимости ``` Неправильная цепочка: ```text validate_symbol() ↓ _load_instruments_via_acquisition() ``` --- ### Правило 4 Успешная валидация должна возвращать исходный канонический: ```python Instrument ``` --- ### Правило 5 Legacy API: ```python get_exchange_symbols() ``` может временно существовать только как compatibility layer для ещё не переведённых потребителей. Он больше не является каноническим источником данных для `validate_symbol()`. --- ## 15. Тестовое покрытие `validate_symbol()` После Build 022 тесты подтверждают следующие сценарии: ```text пустой символ; mock mode; точное совпадение Instrument; регистронезависимый поиск; игнорирование внешних пробелов; поддержка encoded separator; поддержка внутренних пробелов; отсутствующий инструмент; возврат исходного объекта Instrument; нормализация фактически найденного символа; сохранение success message; единственный вызов get_instruments(); запрет использования legacy get_exchange_symbols(); запрет прямого вызова acquisition; сохранение приоритета кандидатов; возврат первого дубликата; сохранение типа SymbolValidationResult; возврат канонического Instrument. ``` Результат: ```text 18 passed in 0.11s ``` --- ## 16. Тестовое покрытие runtime-status Тесты runtime-status были переведены с: ```python ExchangeSymbol ``` на: ```python Instrument ``` и подтверждают сохранение существующей runtime-семантики. Проверяются: ```text mock status; explicit symbol; default symbol; invalid symbol; exchange unavailable; классификация открытого рынка; классификация остановленного рынка; классификация неторгуемого инструмента; неизвестные статусы; freshness только для открытого рынка; stale market data; граница freshness threshold; отсутствующий age; ошибка получения snapshot; использование нормализованного символа; сохранение legacy dictionary contract. ``` Результат: ```text 34 passed in 0.09s ``` --- ## 17. Проверка компиляции Выполнена команда: ```bash python -m py_compile \ src/integrations/exchange/models.py \ src/integrations/exchange/service.py \ tests/unit/integrations/exchange/test_service_validate_symbol.py \ tests/unit/integrations/exchange/test_service_symbol_runtime_status.py ``` Результат: ```text Ошибок компиляции нет. ``` --- ## 18. Полный regression suite После завершения Build 022 выполнен полный набор тестов: ```bash python -m pytest -q ``` Результат: ```text 462 passed in 0.24s ``` Это подтверждает, что переход: ```text validate_symbol() ↓ ExchangeSymbol → Instrument ``` не нарушил существующую функциональность проекта. --- ## 19. Состояние legacy-слоя после Build 022 После Build 022 legacy-компоненты всё ещё существуют: ```python ExchangeSymbol get_exchange_symbols() map_instruments_to_exchange_symbols() _exchange_symbols_projection_cache ``` Это ожидаемое состояние. Они сохраняются для ещё не переведённых потребителей и не должны удаляться до завершения их поэтапной миграции. Текущая архитектура: ```text ┌─────────────────────┐ │ Instrument Store │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ get_instruments() │ └──────────┬──────────┘ │ ┌─────────────────┴─────────────────┐ │ │ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────┐ │ Canonical consumers │ │ Compatibility layer │ └─────────────────────┘ └──────────┬──────────┘ │ │ │ ▼ │ ┌─────────────────────┐ │ │ ExchangeSymbol │ │ └──────────┬──────────┘ │ │ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────┐ │ currency_ui.py │ │ Legacy consumers │ │ validate_symbol() │ │ not migrated yet │ │ runtime-status │ │ │ └─────────────────────┘ └─────────────────────┘ ``` --- ## 20. Что не входило в Build 022 Build 022 намеренно не выполняет полное удаление legacy-слоя. В рамках этого Build не удаляются: ```python ExchangeSymbol get_exchange_symbols() map_instrument_to_exchange_symbol() map_instruments_to_exchange_symbols() _exchange_symbols_projection_cache ``` Также Build 022 не выполняет массовый перевод всех оставшихся потребителей. Не входят в scope Build 022 отдельные runtime-потребители: ```text src/integrations/exchange/market_stream.py src/integrations/exchange/market_data_runner.py src/telegram/handlers/market.py ``` Их миграция должна выполняться поэтапно в следующих Build с отдельной проверкой контрактов и regression suite. --- ## 21. Итоговое состояние после Build 022 После завершения Build 022 достигнуто следующее состояние: ```text Instrument Acquisition Pipeline ↓ Instrument Store ↓ get_instruments() ↓ ├── currency_ui.py │ ├── validate_symbol() │ ↓ │ SymbolValidationResult │ ↓ │ Instrument | None │ └── runtime-status ``` Ключевой результат: > Валидация торгового символа больше не зависит от legacy-модели `ExchangeSymbol` и работает непосредственно с каноническим справочником `Instrument`. При этом: ```text старый бот продолжает работать; внешний контракт validate_symbol() сохранён; runtime-status сохранён; legacy dictionary contract сохранён; полный regression suite проходит; legacy compatibility layer остаётся доступен для ещё не переведённых потребителей. ``` --- ## 22. Критерии завершения Build 022 Build 022 считается завершённым, поскольку выполнены все критерии: - [x] `SymbolValidationResult.symbol_info` переведён на `Instrument | None`. - [x] `validate_symbol()` использует `get_instruments()`. - [x] `validate_symbol()` больше не использует `get_exchange_symbols()`. - [x] `validate_symbol()` не вызывает acquisition pipeline напрямую. - [x] Успешная валидация возвращает исходный канонический `Instrument`. - [x] Сохранена существующая нормализация торговых символов. - [x] Сохранена семантика mock mode. - [x] Сохранена семантика invalid symbol. - [x] Сохранён runtime-status. - [x] Сохранена классификация биржевых статусов. - [x] Сохранена freshness-проверка. - [x] Сохранён legacy dictionary contract. - [x] Целевые тесты `validate_symbol()` проходят: `18 passed`. - [x] Целевые тесты runtime-status проходят: `34 passed`. - [x] `py_compile` проходит без ошибок. - [x] Полный regression suite проходит: `462 passed`. --- ## 23. Следующий этап Следующий этап: ```text Build 023+ — Поэтапный перевод остальных потребителей ``` Перед началом следующего Build необходимо определить очередную минимальную группу потребителей, которая всё ещё зависит от legacy reference data: ```python ExchangeSymbol get_exchange_symbols() SymbolValidationResult.symbol_info ``` Рекомендуемый принцип дальнейшей миграции: ```text один логически связанный набор потребителей ↓ перевод на Instrument ↓ целевые тесты ↓ полный regression suite ↓ grep-аудит ↓ документ Build ↓ следующий этап ``` Полное удаление: ```python ExchangeSymbol get_exchange_symbols() map_instruments_to_exchange_symbols() _exchange_symbols_projection_cache ``` должно выполняться только после того, как последний реальный потребитель будет переведён на канонический `Instrument`. --- ## 24. Финальный результат Build 022 ```text Build 022: ЗАВЕРШЁН Канонический справочник: Instrument Store ↓ get_instruments() ↓ Instrument Переведённые потребители: ├── currency_ui.py ├── validate_symbol() └── runtime-status через validate_symbol() Legacy compatibility: ├── ExchangeSymbol ├── get_exchange_symbols() ├── compatibility mapper └── projection cache Статус legacy compatibility: Временно сохранён для ещё не переведённых потребителей. Regression: 462 passed. ```