# Build 018 — Переключение `get_symbol_runtime_status()` ## Статус **COMPLETE** --- ## Цель Перевести определение торгового состояния инструмента, используемое методом: ```python ExchangeService.get_symbol_runtime_status() ``` на каноническую классификацию статусов из подсистемы: ```text src/market_data/acquisition/ ``` при полном сохранении существующего внешнего контракта `ExchangeRuntimeStatus`, поведения legacy-кода, UI и runtime-потребителей. --- ## Исходное состояние До Build 018 классификация биржевых статусов инструмента находилась непосредственно в legacy exchange-слое: ```text src/integrations/exchange/status.py ``` и основывалась на локальных наборах: ```python OPEN_STATUSES BREAK_STATUSES ``` Метод: ```python build_market_status_from_symbol_status() ``` самостоятельно определял одно из состояний: ```text OPEN NOT_TRADABLE BREAK UNKNOWN ``` Это означало, что предметная классификация торгового состояния инструмента оставалась внутри legacy exchange integration layer. --- ## Архитектурное решение Каноническая классификация статуса инструмента перенесена в: ```text src/market_data/acquisition/models/status.py ``` Добавлены следующие сущности: ```python InstrumentTradingState InstrumentStatusClassification classify_instrument_status() ``` Теперь архитектурная цепочка имеет вид: ```text raw exchange status ↓ classify_instrument_status() ↓ InstrumentStatusClassification ↓ build_market_status_from_symbol_status() ↓ ExchangeRuntimeStatus ↓ legacy runtime / UI / trading consumers ``` Таким образом: - `market_data/acquisition` отвечает за предметную классификацию состояния инструмента; - `integrations/exchange/status.py` сохраняет compatibility-функцию преобразования результата в существующий `ExchangeRuntimeStatus`; - существующие runtime-потребители продолжают работать без изменения публичного контракта. --- ## Добавленный файл ```text src/market_data/acquisition/models/status.py ``` Файл содержит каноническую модель классификации торгового состояния инструмента. Основные сущности: ```python class InstrumentTradingState(StrEnum): OPEN = "OPEN" NOT_TRADABLE = "NOT_TRADABLE" BREAK = "BREAK" UNKNOWN = "UNKNOWN" ``` ```python @dataclass(frozen=True, slots=True) class InstrumentStatusClassification: state: InstrumentTradingState normalized_status: str | None ``` Основная функция: ```python def classify_instrument_status( raw_status: str | None, ) -> InstrumentStatusClassification: ``` Она выполняет: 1. нормализацию входного статуса; 2. классификацию открытого рынка; 3. классификацию неторгуемого инструмента; 4. классификацию временной остановки торгов; 5. возврат `UNKNOWN` для неизвестного или отсутствующего статуса. --- ## Канонические состояния Подсистема `market_data/acquisition` различает четыре предметных состояния: ```text OPEN NOT_TRADABLE BREAK UNKNOWN ``` ### `OPEN` Инструмент доступен для торговли. Поддерживаются существующие legacy-статусы: ```text TRADING OPEN ACTIVE ENABLED ONLINE ``` ### `NOT_TRADABLE` Инструмент существует, но недоступен для обычной торговли. Поддерживаются: ```text NOT_TRADABLE TRADING_DISABLED MARKET_DISABLED UNAVAILABLE_FOR_TRADING CLOSE_ONLY REDUCE_ONLY VIEW_ONLY ``` ### `BREAK` Торги временно остановлены или приостановлены. Поддерживаются: ```text BREAK CLOSED HALT HALTED PAUSED SUSPENDED DISABLED SETTLING POST_ONLY ``` ### `UNKNOWN` Используется для: - неизвестного статуса; - пустой строки; - `None`; - значения, отсутствующего в известных классификационных наборах. --- ## Изменение legacy exchange-слоя Из файла: ```text src/integrations/exchange/status.py ``` удалена собственная предметная классификация через публичные наборы: ```python OPEN_STATUSES BREAK_STATUSES ``` Вместо неё используются: ```python from src.market_data.acquisition.models.status import ( InstrumentTradingState, classify_instrument_status, ) ``` Функция: ```python build_market_status_from_symbol_status() ``` теперь сначала вызывает: ```python classification = classify_instrument_status(raw_status) ``` а затем преобразует каноническое состояние в существующий legacy-контракт: ```text InstrumentTradingState.OPEN ↓ ExchangeRuntimeStatus(code=OPEN) InstrumentTradingState.NOT_TRADABLE ↓ ExchangeRuntimeStatus(code=BREAK, reason=market_not_tradable) InstrumentTradingState.BREAK ↓ ExchangeRuntimeStatus(code=BREAK, reason=market_break) InstrumentTradingState.UNKNOWN ↓ ExchangeRuntimeStatus(code=UNKNOWN, reason=market_status_unknown) ``` --- ## Сохранённый публичный контракт Build 018 не изменяет структуру: ```python ExchangeRuntimeStatus ``` Сохранены поля: ```text code is_open is_available is_auth_ok title message ui_line reason symbol raw_status raw_error ``` Также сохранён compatibility-метод: ```python ExchangeRuntimeStatus.as_dict() ``` Это позволяет не изменять существующие runtime-, UI- и trading-потребители. --- ## Поведение `get_symbol_runtime_status()` Метод: ```python ExchangeService.get_symbol_runtime_status() ``` сохранил существующий внешний контракт и последовательность обработки. Архитектурно поток остаётся следующим: ```text requested symbol ↓ validate_symbol() ↓ ExchangeSymbol ↓ raw symbol status ↓ build_market_status_from_symbol_status() ↓ classify_instrument_status() ↓ ExchangeRuntimeStatus ``` Для открытого рынка дополнительно сохраняется проверка свежести рыночного snapshot: ```text OPEN ↓ get_fresh_market_snapshot() ↓ age_seconds > 60 ↓ STALE_MARKET_DATA / BREAK ``` Stale threshold сохранён: ```text 60 секунд ``` Проверка stale market data выполняется только для рынка, первоначально классифицированного как `OPEN`. --- ## Сохранённое поведение Build 018 сохраняет следующие legacy-сценарии: - mock exchange; - явный `symbol`; - использование `default_symbol`, если аргумент равен `None`; - invalid symbol; - ошибка при validation; - `OPEN`; - `NOT_TRADABLE`; - `BREAK`; - `UNKNOWN`; - stale market data; - отсутствие `age_seconds`; - ошибка получения snapshot; - нормализованный matched symbol; - существующий `ExchangeRuntimeStatus`; - существующие `reason`; - существующие UI-тексты; - существующие `raw_status`. --- ## Кэширование и сетевые запросы Build 018 не добавляет: - новый cache; - новый REST-запрос; - дополнительную загрузку `exchangeInfo`; - дополнительный acquisition service; - отдельный status feed runtime. `get_symbol_runtime_status()` продолжает использовать существующий путь: ```text validate_symbol() ↓ get_exchange_symbols() ↓ existing ExchangeService cache ↓ Instrument Acquisition path ``` Таким образом, миграция не создаёт параллельного источника Instrument Reference Data. --- ## Пустые status feed-файлы На момент Build 018 следующие файлы существуют, но остаются пустыми: ```text src/market_data/acquisition/models/status.py src/market_data/acquisition/handlers/status_handler.py src/market_data/acquisition/feeds/status_feed.py ``` После Build 018 файл: ```text src/market_data/acquisition/models/status.py ``` получил реализацию канонической классификации торгового состояния инструмента. Файлы: ```text src/market_data/acquisition/handlers/status_handler.py src/market_data/acquisition/feeds/status_feed.py ``` в рамках Build 018 намеренно не реализуются. Причина: текущая задача не создаёт отдельный Status Feed и не должна вводить новый источник сетевых запросов или параллельный runtime-путь. --- ## Добавленные тесты Добавлен тестовый файл: ```text tests/unit/market_data/acquisition/models/test_instrument_status.py ``` Он проверяет каноническую классификацию: - все открытые статусы; - все неторгуемые статусы; - все break-статусы; - регистронезависимость; - удаление внешних пробелов; - неизвестный статус; - пустой статус; - `None`; - immutable-контракт результата. Также расширено покрытие: ```text tests/unit/integrations/exchange/test_status.py ``` для проверки compatibility-преобразования: ```text InstrumentStatusClassification ↓ ExchangeRuntimeStatus ``` Целевой regression-набор метода находится в: ```text tests/unit/integrations/exchange/test_service_symbol_runtime_status.py ``` Дублирующий тестовый файл: ```text tests/unit/integrations/exchange/test_service_runtime_status.py ``` удалён. --- ## Проверка целевого runtime-контракта Выполнена команда: ```bash python -m pytest \ tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \ -q ``` Результат: ```text 34 passed in 0.09s ``` --- ## Проверка компиляции Выполнена команда: ```bash python -m py_compile \ src/market_data/acquisition/models/status.py \ src/integrations/exchange/status.py \ src/integrations/exchange/service.py \ tests/unit/market_data/acquisition/models/test_instrument_status.py \ tests/unit/integrations/exchange/test_status.py \ tests/unit/integrations/exchange/test_service_symbol_runtime_status.py ``` Результат: ```text Успешно. Ошибок компиляции нет. ``` --- ## Полный regression suite Выполнена команда: ```bash python -m pytest -q ``` Результат: ```text 387 passed in 0.20s ``` --- ## Архитектурный результат После Build 018 ответственность разделена следующим образом: ```text market_data/acquisition/models/status.py ↓ каноническая предметная классификация статуса инструмента integrations/exchange/status.py ↓ compatibility mapping в legacy ExchangeRuntimeStatus integrations/exchange/service.py ↓ runtime orchestration, validation и stale market data check runtime / UI / trading consumers ↓ продолжают использовать существующий ExchangeRuntimeStatus ``` В результате: - предметная классификация статуса инструмента больше не принадлежит legacy exchange-слою; - дублирование `OPEN_STATUSES` / `BREAK_STATUSES` устранено; - `ExchangeRuntimeStatus` сохранён как compatibility boundary; - существующие потребители не требуют массового рефакторинга; - новый REST-путь не создан; - новый cache не создан; - поведение работающего бота сохранено. --- ## Итог ```text BUILD 018 — COMPLETE ``` Build 018 завершён при полном прохождении целевых тестов, компиляции и общего regression suite: ```text 34 targeted tests passed 387 total tests passed ```