28 KiB
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.