feat: add market data architecture and complete migration through build 039

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit a996f2f797
443 changed files with 80452 additions and 1335 deletions

View File

@@ -0,0 +1,589 @@
# 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
```