# 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
```