Files
dzentra_bot/docs/migrations/build_022.md

1169 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` на каноническую модель:
```text
Instrument
```
Основная задача Build 022:
> Перевести механизм валидации торгового символа `ExchangeService.validate_symbol()` и непосредственно связанный с ним runtime-status с legacy-модели `ExchangeSymbol` на канонический справочник `Instrument`, сохранив существующий внешний контракт и работоспособность старого бота.
После завершения Build 022 метод:
```python
ExchangeService.validate_symbol()
```
больше не использует:
```python
get_exchange_symbols()
```
для поиска и валидации торгового инструмента.
Каноническим источником данных теперь является:
```python
get_instruments()
```
который возвращает:
```python
tuple[Instrument, ...]
```
---
## 2. Архитектурный контекст
До начала миграции в системе существовала следующая цепочка:
```text
Exchange API
exchangeInfo
ExchangeService.get_exchange_symbols()
list[ExchangeSymbol]
ExchangeService.validate_symbol()
SymbolValidationResult.symbol_info
ExchangeSymbol | None
```
После внедрения нового Market Data Acquisition Layer в системе появился канонический pipeline:
```text
Exchange API
Instrument Document Source
Instrument Document Handler
Instrument Feed
Instrument Feed Registry
Instrument Acquisition Service
Instrument Store
tuple[Instrument, ...]
```
На предыдущих этапах был создан новый канонический API:
```python
ExchangeService.get_instruments() -> tuple[Instrument, ...]
```
Build 021 перевёл первого внешнего потребителя:
```text
src/telegram/ui/currency_ui.py
```
с:
```python
get_exchange_symbols()
ExchangeSymbol
```
на:
```python
get_instruments()
Instrument
```
Build 022 продолжает миграцию на более глубоком уровне и переводит на `Instrument` механизм валидации торговых символов.
---
## 3. Состояние до Build 022
До Build 022 метод:
```python
ExchangeService.validate_symbol()
```
использовал:
```python
symbols = self.get_exchange_symbols()
```
и выполнял поиск среди:
```python
list[ExchangeSymbol]
```
Результат валидации имел контракт:
```python
@dataclass(slots=True)
class SymbolValidationResult:
requested_symbol: str
normalized_symbol: str
is_valid: bool
message: str
symbol_info: ExchangeSymbol | None
```
Таким образом, несмотря на наличие канонического `Instrument Store`, валидация торгового символа всё ещё зависела от legacy-проекции:
```text
Instrument Store
Instrument
compatibility mapper
ExchangeSymbol
validate_symbol()
```
Это создавало лишний промежуточный слой:
```text
Instrument → ExchangeSymbol → validation
```
вместо прямой канонической цепочки:
```text
Instrument → validation
```
---
## 4. Целевое состояние Build 022
После Build 022 архитектура валидации выглядит следующим образом:
```text
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 были изменены следующие файлы:
```text
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
Поле:
```python
symbol_info
```
содержало:
```python
ExchangeSymbol | None
```
Контракт:
```python
@dataclass(slots=True)
class SymbolValidationResult:
requested_symbol: str
normalized_symbol: str
is_valid: bool
message: str
symbol_info: ExchangeSymbol | None
```
### После Build 022
Поле:
```python
symbol_info
```
содержит каноническую модель:
```python
Instrument | None
```
Итоговый контракт:
```python
@dataclass(slots=True)
class SymbolValidationResult:
requested_symbol: str
normalized_symbol: str
is_valid: bool
message: str
symbol_info: Instrument | None
```
Для type checking используется импорт:
```python
if TYPE_CHECKING:
from src.market_data.acquisition.models.instrument import Instrument
```
Это позволяет избежать ненужной runtime-зависимости модели exchange integration от реализации acquisition model при сохранении корректной типизации.
---
## 7. Изменение `validate_symbol()`
### До Build 022
Метод получал legacy-справочник:
```python
symbols = self.get_exchange_symbols()
```
и выполнял поиск среди объектов:
```python
ExchangeSymbol
```
Архитектурная цепочка имела вид:
```text
validate_symbol()
get_exchange_symbols()
compatibility projection
ExchangeSymbol
```
### После Build 022
Метод получает канонический справочник:
```python
instruments = self.get_instruments()
```
и выполняет поиск непосредственно среди:
```python
Instrument
```
Новая цепочка:
```text
validate_symbol()
get_instruments()
Instrument Store
Instrument
```
Legacy compatibility projection больше не участвует в валидации торгового символа.
---
## 8. Сохранённая семантика валидации
Build 022 не меняет пользовательскую и runtime-семантику `validate_symbol()`.
Сохранены следующие сценарии.
### 8.1. Пустой символ
Вход:
```text
" "
```
Результат:
```python
SymbolValidationResult(
requested_symbol="",
normalized_symbol="",
is_valid=False,
message="Символ пустой.",
symbol_info=None,
)
```
---
### 8.2. Mock mode
При:
```python
exchange_enabled=False
```
символ принимается без обращения к каноническому справочнику.
Пример результата:
```python
SymbolValidationResult(
requested_symbol="BTC/USD_LEVERAGE",
normalized_symbol="BTC/USD_LEVERAGE",
is_valid=True,
message="Mock mode active.",
symbol_info=None,
)
```
---
### 8.3. Регистронезависимый поиск
Следующие значения считаются эквивалентными:
```text
BTC/USD_LEVERAGE
btc/usd_leverage
```
---
### 8.4. Игнорирование внешних пробелов
Следующие значения считаются эквивалентными:
```text
BTC/USD_LEVERAGE
btc/usd_leverage
```
---
### 8.5. Поддержка encoded separator
Поддерживается представление:
```text
BTC%2FUSD_LEVERAGE
```
наряду с:
```text
BTC/USD_LEVERAGE
```
---
### 8.6. Поддержка внутренних пробелов
Поддерживается нормализация значения:
```text
btc / usd_leverage
```
до:
```text
BTC/USD_LEVERAGE
```
---
### 8.7. Отсутствующий инструмент
Если инструмент отсутствует в каноническом справочнике, возвращается неуспешный результат:
```python
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 успешный результат:
```python
validation = service.validate_symbol(
"BTC/USD_LEVERAGE"
)
```
содержит:
```python
validation.symbol_info
```
типа:
```python
Instrument
```
а не:
```python
ExchangeSymbol
```
Дополнительно сохранена identity-семантика.
Если канонический справочник содержит объект:
```python
instrument
```
то после успешной валидации выполняется:
```python
validation.symbol_info is instrument
```
То есть `validate_symbol()`:
- не создаёт копию `Instrument`;
- не создаёт `ExchangeSymbol`;
- не выполняет compatibility mapping;
- возвращает исходный канонический объект из справочника.
---
## 10. Перевод runtime-status
Метод:
```python
ExchangeService.get_symbol_runtime_status()
```
использует:
```python
validation = self.validate_symbol(symbol_to_use)
```
и получает статус торгового инструмента через:
```python
validation.symbol_info.status
```
После изменения контракта:
```python
SymbolValidationResult.symbol_info
```
runtime-status теперь фактически получает канонический:
```python
Instrument
```
вместо legacy:
```python
ExchangeSymbol
```
При этом внешний контракт runtime-status не изменён.
Сохраняются:
```text
ExchangeRuntimeStatus
ExchangeStatusCode
get_symbol_market_status()
```
и существующая классификация состояний рынка.
---
## 11. Сохранённая классификация runtime-status
Build 022 сохраняет существующую классификацию биржевых статусов.
### Открытый рынок
Следующие статусы классифицируются как открытый рынок:
```text
TRADING
OPEN
ACTIVE
ENABLED
ONLINE
```
Результат:
```text
ExchangeStatusCode.OPEN
reason = "market_open"
```
---
### Перерыв или остановка торгов
Следующие статусы классифицируются как перерыв:
```text
BREAK
CLOSED
HALT
HALTED
PAUSED
SUSPENDED
DISABLED
SETTLING
POST_ONLY
```
Результат:
```text
ExchangeStatusCode.BREAK
reason = "market_break"
```
---
### Инструмент недоступен для торговли
Следующие статусы классифицируются как неторгуемые:
```text
NOT_TRADABLE
TRADING_DISABLED
MARKET_DISABLED
UNAVAILABLE_FOR_TRADING
CLOSE_ONLY
REDUCE_ONLY
VIEW_ONLY
```
Результат:
```text
ExchangeStatusCode.BREAK
reason = "market_not_tradable"
```
---
### Неизвестный статус
Неизвестные значения классифицируются как:
```text
ExchangeStatusCode.UNKNOWN
reason = "market_status_unknown"
```
---
## 12. Сохранение проверки freshness
Build 022 не изменяет существующую проверку свежести рыночных данных.
Для открытого рынка runtime-status продолжает проверять:
```python
get_fresh_market_snapshot()
```
Если данные устарели, возвращается:
```text
ExchangeStatusCode.BREAK
reason = "market_data_stale"
raw_status = "STALE_MARKET_DATA"
```
Проверка freshness выполняется только для инструмента, который сначала классифицирован как открытый для торговли.
Для закрытого или приостановленного рынка получение snapshot не выполняется.
---
## 13. Сохранение legacy runtime-контракта
Метод:
```python
get_symbol_market_status()
```
продолжает возвращать legacy dictionary contract.
Пример:
```python
{
"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()` обязан использовать:
```python
get_instruments()
```
---
### Правило 2
`validate_symbol()` не должен использовать:
```python
get_exchange_symbols()
```
---
### Правило 3
`validate_symbol()` не должен напрямую вызывать:
```python
_load_instruments_via_acquisition()
```
Правильная цепочка:
```text
validate_symbol()
get_instruments()
Instrument Store
Acquisition Pipeline при необходимости
```
Неправильная цепочка:
```text
validate_symbol()
_load_instruments_via_acquisition()
```
---
### Правило 4
Успешная валидация должна возвращать исходный канонический:
```python
Instrument
```
---
### Правило 5
Legacy API:
```python
get_exchange_symbols()
```
может временно существовать только как compatibility layer для ещё не переведённых потребителей.
Он больше не является каноническим источником данных для `validate_symbol()`.
---
## 15. Тестовое покрытие `validate_symbol()`
После Build 022 тесты подтверждают следующие сценарии:
```text
пустой символ;
mock mode;
точное совпадение Instrument;
регистронезависимый поиск;
игнорирование внешних пробелов;
поддержка encoded separator;
поддержка внутренних пробелов;
отсутствующий инструмент;
возврат исходного объекта Instrument;
нормализация фактически найденного символа;
сохранение success message;
единственный вызов get_instruments();
запрет использования legacy get_exchange_symbols();
запрет прямого вызова acquisition;
сохранение приоритета кандидатов;
возврат первого дубликата;
сохранение типа SymbolValidationResult;
возврат канонического Instrument.
```
Результат:
```text
18 passed in 0.11s
```
---
## 16. Тестовое покрытие runtime-status
Тесты runtime-status были переведены с:
```python
ExchangeSymbol
```
на:
```python
Instrument
```
и подтверждают сохранение существующей runtime-семантики.
Проверяются:
```text
mock status;
explicit symbol;
default symbol;
invalid symbol;
exchange unavailable;
классификация открытого рынка;
классификация остановленного рынка;
классификация неторгуемого инструмента;
неизвестные статусы;
freshness только для открытого рынка;
stale market data;
граница freshness threshold;
отсутствующий age;
ошибка получения snapshot;
использование нормализованного символа;
сохранение legacy dictionary contract.
```
Результат:
```text
34 passed in 0.09s
```
---
## 17. Проверка компиляции
Выполнена команда:
```bash
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
```
Результат:
```text
Ошибок компиляции нет.
```
---
## 18. Полный regression suite
После завершения Build 022 выполнен полный набор тестов:
```bash
python -m pytest -q
```
Результат:
```text
462 passed in 0.24s
```
Это подтверждает, что переход:
```text
validate_symbol()
ExchangeSymbol → Instrument
```
не нарушил существующую функциональность проекта.
---
## 19. Состояние legacy-слоя после Build 022
После Build 022 legacy-компоненты всё ещё существуют:
```python
ExchangeSymbol
get_exchange_symbols()
map_instruments_to_exchange_symbols()
_exchange_symbols_projection_cache
```
Это ожидаемое состояние.
Они сохраняются для ещё не переведённых потребителей и не должны удаляться до завершения их поэтапной миграции.
Текущая архитектура:
```text
┌─────────────────────┐
│ 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 не удаляются:
```python
ExchangeSymbol
get_exchange_symbols()
map_instrument_to_exchange_symbol()
map_instruments_to_exchange_symbols()
_exchange_symbols_projection_cache
```
Также Build 022 не выполняет массовый перевод всех оставшихся потребителей.
Не входят в scope Build 022 отдельные runtime-потребители:
```text
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 достигнуто следующее состояние:
```text
Instrument Acquisition Pipeline
Instrument Store
get_instruments()
├── currency_ui.py
├── validate_symbol()
│ ↓
│ SymbolValidationResult
│ ↓
│ Instrument | None
└── runtime-status
```
Ключевой результат:
> Валидация торгового символа больше не зависит от legacy-модели `ExchangeSymbol` и работает непосредственно с каноническим справочником `Instrument`.
При этом:
```text
старый бот продолжает работать;
внешний контракт validate_symbol() сохранён;
runtime-status сохранён;
legacy dictionary contract сохранён;
полный regression suite проходит;
legacy compatibility layer остаётся доступен для ещё не переведённых потребителей.
```
---
## 22. Критерии завершения Build 022
Build 022 считается завершённым, поскольку выполнены все критерии:
- [x] `SymbolValidationResult.symbol_info` переведён на `Instrument | None`.
- [x] `validate_symbol()` использует `get_instruments()`.
- [x] `validate_symbol()` больше не использует `get_exchange_symbols()`.
- [x] `validate_symbol()` не вызывает acquisition pipeline напрямую.
- [x] Успешная валидация возвращает исходный канонический `Instrument`.
- [x] Сохранена существующая нормализация торговых символов.
- [x] Сохранена семантика mock mode.
- [x] Сохранена семантика invalid symbol.
- [x] Сохранён runtime-status.
- [x] Сохранена классификация биржевых статусов.
- [x] Сохранена freshness-проверка.
- [x] Сохранён legacy dictionary contract.
- [x] Целевые тесты `validate_symbol()` проходят: `18 passed`.
- [x] Целевые тесты runtime-status проходят: `34 passed`.
- [x] `py_compile` проходит без ошибок.
- [x] Полный regression suite проходит: `462 passed`.
---
## 23. Следующий этап
Следующий этап:
```text
Build 023+ — Поэтапный перевод остальных потребителей
```
Перед началом следующего Build необходимо определить очередную минимальную группу потребителей, которая всё ещё зависит от legacy reference data:
```python
ExchangeSymbol
get_exchange_symbols()
SymbolValidationResult.symbol_info
```
Рекомендуемый принцип дальнейшей миграции:
```text
один логически связанный набор потребителей
перевод на Instrument
целевые тесты
полный regression suite
grep-аудит
документ Build
следующий этап
```
Полное удаление:
```python
ExchangeSymbol
get_exchange_symbols()
map_instruments_to_exchange_symbols()
_exchange_symbols_projection_cache
```
должно выполняться только после того, как последний реальный потребитель будет переведён на канонический `Instrument`.
---
## 24. Финальный результат Build 022
```text
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.
```