Files
dzentra_bot/docs/migrations/build_022.md

28 KiB
Raw Blame History

Build 022 — Перевод валидации торгового символа на канонический Instrument

Engineering Build Record


Контроль документа

Свойство Значение
Документ Build 022 — Перевод валидации торгового символа на канонический Instrument
Тип документа Engineering Build Record
Build 022
Статус Завершён
Подсистема Market Data Acquisition / Exchange Integration
Проект Dzentra
Язык Русский
Предыдущий Build Build 021 — Перевод первой группы потребителей
Следующий этап Build 023+ — Поэтапный перевод остальных потребителей

1. Назначение Build

Build 022 продолжает поэтапную миграцию Dzentra с legacy-модели биржевого инструмента ExchangeSymbol на каноническую модель:

Instrument

Основная задача Build 022:

Перевести механизм валидации торгового символа ExchangeService.validate_symbol() и непосредственно связанный с ним runtime-status с legacy-модели ExchangeSymbol на канонический справочник Instrument, сохранив существующий внешний контракт и работоспособность старого бота.

После завершения Build 022 метод:

ExchangeService.validate_symbol()

больше не использует:

get_exchange_symbols()

для поиска и валидации торгового инструмента.

Каноническим источником данных теперь является:

get_instruments()

который возвращает:

tuple[Instrument, ...]

2. Архитектурный контекст

До начала миграции в системе существовала следующая цепочка:

Exchange API
    ↓
exchangeInfo
    ↓
ExchangeService.get_exchange_symbols()
    ↓
list[ExchangeSymbol]
    ↓
ExchangeService.validate_symbol()
    ↓
SymbolValidationResult.symbol_info
    ↓
ExchangeSymbol | None

После внедрения нового Market Data Acquisition Layer в системе появился канонический pipeline:

Exchange API
    ↓
Instrument Document Source
    ↓
Instrument Document Handler
    ↓
Instrument Feed
    ↓
Instrument Feed Registry
    ↓
Instrument Acquisition Service
    ↓
Instrument Store
    ↓
tuple[Instrument, ...]

На предыдущих этапах был создан новый канонический API:

ExchangeService.get_instruments() -> tuple[Instrument, ...]

Build 021 перевёл первого внешнего потребителя:

src/telegram/ui/currency_ui.py

с:

get_exchange_symbols()
ExchangeSymbol

на:

get_instruments()
Instrument

Build 022 продолжает миграцию на более глубоком уровне и переводит на Instrument механизм валидации торговых символов.


3. Состояние до Build 022

До Build 022 метод:

ExchangeService.validate_symbol()

использовал:

symbols = self.get_exchange_symbols()

и выполнял поиск среди:

list[ExchangeSymbol]

Результат валидации имел контракт:

@dataclass(slots=True)
class SymbolValidationResult:
    requested_symbol: str
    normalized_symbol: str

    is_valid: bool
    message: str

    symbol_info: ExchangeSymbol | None

Таким образом, несмотря на наличие канонического Instrument Store, валидация торгового символа всё ещё зависела от legacy-проекции:

Instrument Store
    ↓
Instrument
    ↓
compatibility mapper
    ↓
ExchangeSymbol
    ↓
validate_symbol()

Это создавало лишний промежуточный слой:

Instrument → ExchangeSymbol → validation

вместо прямой канонической цепочки:

Instrument → validation

4. Целевое состояние Build 022

После Build 022 архитектура валидации выглядит следующим образом:

Exchange API
    ↓
Market Data Acquisition
    ↓
Instrument Store
    ↓
ExchangeService.get_instruments()
    ↓
tuple[Instrument, ...]
    ↓
ExchangeService.validate_symbol()
    ↓
SymbolValidationResult
    ↓
Instrument | None

Главное архитектурное правило Build 022:

validate_symbol() обязан работать непосредственно с каноническим справочником Instrument и не должен использовать legacy-метод get_exchange_symbols().


5. Изменённые компоненты

В рамках Build 022 были изменены следующие файлы:

app/src/integrations/exchange/models.py

app/src/integrations/exchange/service.py

app/tests/unit/integrations/exchange/test_service_validate_symbol.py

app/tests/unit/integrations/exchange/test_service_symbol_runtime_status.py

6. Изменение SymbolValidationResult

До Build 022

Поле:

symbol_info

содержало:

ExchangeSymbol | None

Контракт:

@dataclass(slots=True)
class SymbolValidationResult:
    requested_symbol: str
    normalized_symbol: str

    is_valid: bool
    message: str

    symbol_info: ExchangeSymbol | None

После Build 022

Поле:

symbol_info

содержит каноническую модель:

Instrument | None

Итоговый контракт:

@dataclass(slots=True)
class SymbolValidationResult:
    requested_symbol: str
    normalized_symbol: str

    is_valid: bool
    message: str

    symbol_info: Instrument | None

Для type checking используется импорт:

if TYPE_CHECKING:
    from src.market_data.acquisition.models.instrument import Instrument

Это позволяет избежать ненужной runtime-зависимости модели exchange integration от реализации acquisition model при сохранении корректной типизации.


7. Изменение validate_symbol()

До Build 022

Метод получал legacy-справочник:

symbols = self.get_exchange_symbols()

и выполнял поиск среди объектов:

ExchangeSymbol

Архитектурная цепочка имела вид:

validate_symbol()
    ↓
get_exchange_symbols()
    ↓
compatibility projection
    ↓
ExchangeSymbol

После Build 022

Метод получает канонический справочник:

instruments = self.get_instruments()

и выполняет поиск непосредственно среди:

Instrument

Новая цепочка:

validate_symbol()
    ↓
get_instruments()
    ↓
Instrument Store
    ↓
Instrument

Legacy compatibility projection больше не участвует в валидации торгового символа.


8. Сохранённая семантика валидации

Build 022 не меняет пользовательскую и runtime-семантику validate_symbol().

Сохранены следующие сценарии.

8.1. Пустой символ

Вход:

"   "

Результат:

SymbolValidationResult(
    requested_symbol="",
    normalized_symbol="",
    is_valid=False,
    message="Символ пустой.",
    symbol_info=None,
)

8.2. Mock mode

При:

exchange_enabled=False

символ принимается без обращения к каноническому справочнику.

Пример результата:

SymbolValidationResult(
    requested_symbol="BTC/USD_LEVERAGE",
    normalized_symbol="BTC/USD_LEVERAGE",
    is_valid=True,
    message="Mock mode active.",
    symbol_info=None,
)

8.3. Регистронезависимый поиск

Следующие значения считаются эквивалентными:

BTC/USD_LEVERAGE
btc/usd_leverage

8.4. Игнорирование внешних пробелов

Следующие значения считаются эквивалентными:

BTC/USD_LEVERAGE
  btc/usd_leverage

8.5. Поддержка encoded separator

Поддерживается представление:

BTC%2FUSD_LEVERAGE

наряду с:

BTC/USD_LEVERAGE

8.6. Поддержка внутренних пробелов

Поддерживается нормализация значения:

btc / usd_leverage

до:

BTC/USD_LEVERAGE

8.7. Отсутствующий инструмент

Если инструмент отсутствует в каноническом справочнике, возвращается неуспешный результат:

SymbolValidationResult(
    requested_symbol="XRP/USD_LEVERAGE",
    normalized_symbol="XRP/USD_LEVERAGE",
    is_valid=False,
    message=(
        "Символ 'XRP/USD_LEVERAGE' "
        "не найден в exchangeInfo."
    ),
    symbol_info=None,
)

Сообщение сохранено для обратной совместимости.


8.8. Сохранение приоритета кандидатов

Если разные исходные значения после нормализации могут соответствовать одному символу, сохраняется существующий порядок выбора кандидатов.

В частности, точное исходное представление продолжает иметь приоритет в предусмотренных legacy-сценариях.


8.9. Сохранение первого дубликата

Если канонический справочник содержит несколько эквивалентных инструментов, возвращается первый подходящий объект согласно существующему порядку справочника.


9. Канонический объект в результате валидации

После Build 022 успешный результат:

validation = service.validate_symbol(
    "BTC/USD_LEVERAGE"
)

содержит:

validation.symbol_info

типа:

Instrument

а не:

ExchangeSymbol

Дополнительно сохранена identity-семантика.

Если канонический справочник содержит объект:

instrument

то после успешной валидации выполняется:

validation.symbol_info is instrument

То есть validate_symbol():

  • не создаёт копию Instrument;
  • не создаёт ExchangeSymbol;
  • не выполняет compatibility mapping;
  • возвращает исходный канонический объект из справочника.

10. Перевод runtime-status

Метод:

ExchangeService.get_symbol_runtime_status()

использует:

validation = self.validate_symbol(symbol_to_use)

и получает статус торгового инструмента через:

validation.symbol_info.status

После изменения контракта:

SymbolValidationResult.symbol_info

runtime-status теперь фактически получает канонический:

Instrument

вместо legacy:

ExchangeSymbol

При этом внешний контракт runtime-status не изменён.

Сохраняются:

ExchangeRuntimeStatus
ExchangeStatusCode
get_symbol_market_status()

и существующая классификация состояний рынка.


11. Сохранённая классификация runtime-status

Build 022 сохраняет существующую классификацию биржевых статусов.

Открытый рынок

Следующие статусы классифицируются как открытый рынок:

TRADING
OPEN
ACTIVE
ENABLED
ONLINE

Результат:

ExchangeStatusCode.OPEN
reason = "market_open"

Перерыв или остановка торгов

Следующие статусы классифицируются как перерыв:

BREAK
CLOSED
HALT
HALTED
PAUSED
SUSPENDED
DISABLED
SETTLING
POST_ONLY

Результат:

ExchangeStatusCode.BREAK
reason = "market_break"

Инструмент недоступен для торговли

Следующие статусы классифицируются как неторгуемые:

NOT_TRADABLE
TRADING_DISABLED
MARKET_DISABLED
UNAVAILABLE_FOR_TRADING
CLOSE_ONLY
REDUCE_ONLY
VIEW_ONLY

Результат:

ExchangeStatusCode.BREAK
reason = "market_not_tradable"

Неизвестный статус

Неизвестные значения классифицируются как:

ExchangeStatusCode.UNKNOWN
reason = "market_status_unknown"

12. Сохранение проверки freshness

Build 022 не изменяет существующую проверку свежести рыночных данных.

Для открытого рынка runtime-status продолжает проверять:

get_fresh_market_snapshot()

Если данные устарели, возвращается:

ExchangeStatusCode.BREAK
reason = "market_data_stale"
raw_status = "STALE_MARKET_DATA"

Проверка freshness выполняется только для инструмента, который сначала классифицирован как открытый для торговли.

Для закрытого или приостановленного рынка получение snapshot не выполняется.


13. Сохранение legacy runtime-контракта

Метод:

get_symbol_market_status()

продолжает возвращать legacy dictionary contract.

Пример:

{
    "code": "BREAK",
    "status": "BREAK",
    "symbol": "BTC/USD",
    "is_open": False,
    "is_available": True,
    "is_auth_ok": True,
    "title": "Перерыв в торгах",
    "message": "Торги временно остановлены.",
    "ui_line": "⏸️ Перерыв в торгах",
    "reason": "market_break",
    "raw_status": "BREAK",
    "raw_error": None,
}

Таким образом, Build 022 меняет внутренний источник reference data, но не ломает существующий внешний runtime-контракт старого бота.


14. Архитектурные ограничения, закреплённые тестами

Build 022 вводит и закрепляет следующие архитектурные правила.

Правило 1

validate_symbol() обязан использовать:

get_instruments()

Правило 2

validate_symbol() не должен использовать:

get_exchange_symbols()

Правило 3

validate_symbol() не должен напрямую вызывать:

_load_instruments_via_acquisition()

Правильная цепочка:

validate_symbol()
    ↓
get_instruments()
    ↓
Instrument Store
    ↓
Acquisition Pipeline при необходимости

Неправильная цепочка:

validate_symbol()
    ↓
_load_instruments_via_acquisition()

Правило 4

Успешная валидация должна возвращать исходный канонический:

Instrument

Правило 5

Legacy API:

get_exchange_symbols()

может временно существовать только как compatibility layer для ещё не переведённых потребителей.

Он больше не является каноническим источником данных для validate_symbol().


15. Тестовое покрытие validate_symbol()

После Build 022 тесты подтверждают следующие сценарии:

пустой символ;
mock mode;
точное совпадение Instrument;
регистронезависимый поиск;
игнорирование внешних пробелов;
поддержка encoded separator;
поддержка внутренних пробелов;
отсутствующий инструмент;
возврат исходного объекта Instrument;
нормализация фактически найденного символа;
сохранение success message;
единственный вызов get_instruments();
запрет использования legacy get_exchange_symbols();
запрет прямого вызова acquisition;
сохранение приоритета кандидатов;
возврат первого дубликата;
сохранение типа SymbolValidationResult;
возврат канонического Instrument.

Результат:

18 passed in 0.11s

16. Тестовое покрытие runtime-status

Тесты runtime-status были переведены с:

ExchangeSymbol

на:

Instrument

и подтверждают сохранение существующей runtime-семантики.

Проверяются:

mock status;
explicit symbol;
default symbol;
invalid symbol;
exchange unavailable;
классификация открытого рынка;
классификация остановленного рынка;
классификация неторгуемого инструмента;
неизвестные статусы;
freshness только для открытого рынка;
stale market data;
граница freshness threshold;
отсутствующий age;
ошибка получения snapshot;
использование нормализованного символа;
сохранение legacy dictionary contract.

Результат:

34 passed in 0.09s

17. Проверка компиляции

Выполнена команда:

python -m py_compile \
  src/integrations/exchange/models.py \
  src/integrations/exchange/service.py \
  tests/unit/integrations/exchange/test_service_validate_symbol.py \
  tests/unit/integrations/exchange/test_service_symbol_runtime_status.py

Результат:

Ошибок компиляции нет.

18. Полный regression suite

После завершения Build 022 выполнен полный набор тестов:

python -m pytest -q

Результат:

462 passed in 0.24s

Это подтверждает, что переход:

validate_symbol()
    ↓
ExchangeSymbol → Instrument

не нарушил существующую функциональность проекта.


19. Состояние legacy-слоя после Build 022

После Build 022 legacy-компоненты всё ещё существуют:

ExchangeSymbol
get_exchange_symbols()
map_instruments_to_exchange_symbols()
_exchange_symbols_projection_cache

Это ожидаемое состояние.

Они сохраняются для ещё не переведённых потребителей и не должны удаляться до завершения их поэтапной миграции.

Текущая архитектура:

                         ┌─────────────────────┐
                         │  Instrument Store   │
                         └──────────┬──────────┘
                                    │
                                    ▼
                         ┌─────────────────────┐
                         │  get_instruments()  │
                         └──────────┬──────────┘
                                    │
                  ┌─────────────────┴─────────────────┐
                  │                                   │
                  ▼                                   ▼
       ┌─────────────────────┐             ┌─────────────────────┐
       │ Canonical consumers │             │ Compatibility layer │
       └─────────────────────┘             └──────────┬──────────┘
                  │                                   │
                  │                                   ▼
                  │                        ┌─────────────────────┐
                  │                        │   ExchangeSymbol    │
                  │                        └──────────┬──────────┘
                  │                                   │
                  ▼                                   ▼
       ┌─────────────────────┐             ┌─────────────────────┐
       │ currency_ui.py      │             │ Legacy consumers    │
       │ validate_symbol()   │             │ not migrated yet    │
       │ runtime-status      │             │                     │
       └─────────────────────┘             └─────────────────────┘

20. Что не входило в Build 022

Build 022 намеренно не выполняет полное удаление legacy-слоя.

В рамках этого Build не удаляются:

ExchangeSymbol
get_exchange_symbols()
map_instrument_to_exchange_symbol()
map_instruments_to_exchange_symbols()
_exchange_symbols_projection_cache

Также Build 022 не выполняет массовый перевод всех оставшихся потребителей.

Не входят в scope Build 022 отдельные runtime-потребители:

src/integrations/exchange/market_stream.py

src/integrations/exchange/market_data_runner.py

src/telegram/handlers/market.py

Их миграция должна выполняться поэтапно в следующих Build с отдельной проверкой контрактов и regression suite.


21. Итоговое состояние после Build 022

После завершения Build 022 достигнуто следующее состояние:

Instrument Acquisition Pipeline
    ↓
Instrument Store
    ↓
get_instruments()
    ↓
    ├── currency_ui.py
    │
    ├── validate_symbol()
    │       ↓
    │   SymbolValidationResult
    │       ↓
    │   Instrument | None
    │
    └── runtime-status

Ключевой результат:

Валидация торгового символа больше не зависит от legacy-модели ExchangeSymbol и работает непосредственно с каноническим справочником Instrument.

При этом:

старый бот продолжает работать;
внешний контракт validate_symbol() сохранён;
runtime-status сохранён;
legacy dictionary contract сохранён;
полный regression suite проходит;
legacy compatibility layer остаётся доступен для ещё не переведённых потребителей.

22. Критерии завершения Build 022

Build 022 считается завершённым, поскольку выполнены все критерии:

  • SymbolValidationResult.symbol_info переведён на Instrument | None.
  • validate_symbol() использует get_instruments().
  • validate_symbol() больше не использует get_exchange_symbols().
  • validate_symbol() не вызывает acquisition pipeline напрямую.
  • Успешная валидация возвращает исходный канонический Instrument.
  • Сохранена существующая нормализация торговых символов.
  • Сохранена семантика mock mode.
  • Сохранена семантика invalid symbol.
  • Сохранён runtime-status.
  • Сохранена классификация биржевых статусов.
  • Сохранена freshness-проверка.
  • Сохранён legacy dictionary contract.
  • Целевые тесты validate_symbol() проходят: 18 passed.
  • Целевые тесты runtime-status проходят: 34 passed.
  • py_compile проходит без ошибок.
  • Полный regression suite проходит: 462 passed.

23. Следующий этап

Следующий этап:

Build 023+ — Поэтапный перевод остальных потребителей

Перед началом следующего Build необходимо определить очередную минимальную группу потребителей, которая всё ещё зависит от legacy reference data:

ExchangeSymbol
get_exchange_symbols()
SymbolValidationResult.symbol_info

Рекомендуемый принцип дальнейшей миграции:

один логически связанный набор потребителей
    ↓
перевод на Instrument
    ↓
целевые тесты
    ↓
полный regression suite
    ↓
grep-аудит
    ↓
документ Build
    ↓
следующий этап

Полное удаление:

ExchangeSymbol
get_exchange_symbols()
map_instruments_to_exchange_symbols()
_exchange_symbols_projection_cache

должно выполняться только после того, как последний реальный потребитель будет переведён на канонический Instrument.


24. Финальный результат Build 022

Build 022: ЗАВЕРШЁН

Канонический справочник:
Instrument Store
    ↓
get_instruments()
    ↓
Instrument

Переведённые потребители:
    ├── currency_ui.py
    ├── validate_symbol()
    └── runtime-status через validate_symbol()

Legacy compatibility:
    ├── ExchangeSymbol
    ├── get_exchange_symbols()
    ├── compatibility mapper
    └── projection cache

Статус legacy compatibility:
Временно сохранён для ещё не переведённых потребителей.

Regression:
462 passed.