feat: add market data architecture and complete migration through build 039
This commit is contained in:
589
docs/migrations/build_018.md
Normal file
589
docs/migrations/build_018.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user