589 lines
14 KiB
Markdown
589 lines
14 KiB
Markdown
# 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
|
||
``` |