Files
dzentra_bot/docs/migrations/build_033.md

864 lines
22 KiB
Markdown
Raw 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 033 — Перенос MarketPriceCache на Quote Store
**Engineering Build Record**
---
## Контроль документа
| Свойство | Значение |
|---|---|
| Документ | Build 033 — Перенос MarketPriceCache на Quote Store |
| Тип документа | Engineering Build Record |
| Статус | **Completed** |
| Подсистема | Market Data / Storage / Legacy Exchange Integration |
| Проект | Dzentra |
| Язык | Русский |
| Предыдущий этап | Build 032 — Канонический Quote Store |
| Следующий этап | Build 034 — Dzengi WebSocket quote parsing и адаптер |
---
## 1. Назначение Build
Цель Build 033 — перевести legacy-компонент `MarketPriceCache` с собственного внутреннего хранилища котировок на канонический `Quote Store`, сохранив полную обратную совместимость с существующими потребителями.
До Build 033 `MarketPriceCache` самостоятельно владел runtime-состоянием котировок:
```python
_prices: dict[tuple[str, str], MarketPriceSnapshot] = {}
```
Это создавало отдельный контур хранения рыночных цен параллельно с введённым в Build 032 каноническим `Quote Store`.
После Build 033 единственным владельцем состояния котировок, доступных через `MarketPriceCache`, становится канонический `Quote Store`.
Целевая переходная архитектура:
```text
Legacy consumers
MarketPriceCache
compatibility facade
Canonical Quote
InMemoryQuoteStore
```
Сам `MarketPriceCache` сохраняется временно как compatibility facade до его окончательного удаления на Build 039.
---
## 2. Архитектурный контекст
Build 033 является частью последовательного перехода Quotes Feed на новую архитектуру Market Data Acquisition:
```text
Build 026 — Аудит текущего контура Quotes Feed
Build 027 — Каноническая модель Quote и специализированные контракты
Build 028 — Dzengi REST quote models, parser и validation
Build 029 — Dzengi mapper и Quotes Handler
Build 030 — Quotes Feed и регистрация в Acquisition Service
Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
Build 032 — Канонический Quote Store
Build 033 — Перенос MarketPriceCache на Quote Store
Build 034 — Dzengi WebSocket quote parsing и адаптер
Build 035 — Перевод market runtime на Quotes Feed
Build 036 — Перевод read-only и UI-потребителей
Build 037 — Перевод execution-потребителей
Build 038 — Удаление legacy TickerPrice и market snapshot dict layer
Build 039 — Удаление legacy quote parsing и MarketPriceCache
Build 040 — Финальная архитектурная проверка Quotes Feed
```
Build 033 не переводит непосредственных потребителей `MarketPriceCache` на новые API. Эта миграция выполняется последующими Build.
Задача текущего этапа — устранить независимое legacy-хранилище котировок без нарушения работы существующего бота.
---
## 3. Исходное состояние
До Build 033 класс:
```text
src/integrations/exchange/market_cache.py
```
содержал собственное class-level хранилище:
```python
class MarketPriceCache:
_prices: dict[tuple[str, str], MarketPriceSnapshot] = {}
```
Ключ записи формировался из:
```text
(runtime_key, symbol)
```
`MarketPriceCache` самостоятельно выполнял:
- запись текущей цены;
- хранение `bid_price`;
- хранение `ask_price`;
- хранение `updated_at`;
- хранение фактического источника данных;
- изоляцию по `runtime_key`;
- вычисление возраста snapshot;
- очистку записей по символу и runtime.
При этом после Build 032 уже существовал канонический:
```text
InMemoryQuoteStore
```
работающий с моделью:
```text
Quote
```
Таким образом, существовали два отдельных механизма хранения котировок:
```text
MarketPriceCache
└── собственный dict[tuple[str, str], MarketPriceSnapshot]
Quote Store
└── каноническое хранилище Quote
```
Build 033 устранил это дублирование для контура `MarketPriceCache`.
---
## 4. Выполненные изменения
### 4.1. Изменённый исходный файл
Изменён:
```text
src/integrations/exchange/market_cache.py
```
### 4.2. Добавленный тестовый файл
Добавлен:
```text
tests/unit/integrations/exchange/test_market_cache.py
```
### 4.3. Файлы, не потребовавшие изменений
В рамках Build 033 не изменялись:
```text
src/storage/quote_store.py
src/storage/exceptions.py
src/market_data/acquisition/models/quote.py
tests/unit/storage/test_quote_store.py
src/integrations/exchange/service.py
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
```
Это подтверждает сохранение существующих публичных контрактов и минимальный scope миграции.
---
## 5. Новая роль MarketPriceCache
После Build 033 `MarketPriceCache` больше не является самостоятельным владельцем runtime-состояния котировок.
Его новая роль:
```text
Legacy compatibility facade
```
Он обеспечивает совместимость между существующими legacy-потребителями и канонической моделью хранения котировок.
Логика записи:
```text
Legacy caller
MarketPriceCache.set_price(...)
Canonical Quote
QuoteStoreProtocol.set(...)
```
Логика чтения:
```text
Legacy caller
MarketPriceCache.get_price(...)
QuoteStoreProtocol.get(...)
Canonical Quote
MarketPriceSnapshot
Legacy caller
```
Таким образом, `MarketPriceSnapshot` остаётся только временной compatibility model.
---
## 6. Устранение собственного хранилища MarketPriceCache
До Build 033:
```python
_prices: dict[tuple[str, str], MarketPriceSnapshot] = {}
```
После Build 033 `MarketPriceCache` использует канонический контракт:
```text
QuoteStoreProtocol
```
и реализацию:
```text
InMemoryQuoteStore
```
Собственное независимое хранилище `_prices` устранено.
Это является главным архитектурным результатом Build 033.
---
## 7. Преобразование legacy-входа в канонический Quote
Публичный legacy-контракт записи сохранён:
```python
MarketPriceCache.set_price(
symbol=...,
price=...,
bid_price=...,
ask_price=...,
updated_at=...,
source=...,
runtime_key=...,
)
```
Внутри compatibility facade эти данные преобразуются в каноническую модель:
```text
Quote
```
Основное соответствие полей:
| Legacy `MarketPriceCache` | Канонический `Quote` |
|---|---|
| `symbol` | `symbol` |
| `price` | `last_price` |
| `bid_price` | `bid_price` |
| `ask_price` | `ask_price` |
| `updated_at` | каноническое timestamp-представление |
| `source` | `source` |
| время получения | `received_at` |
На legacy-границе сохраняется использование `float`.
Внутри канонической модели используются точные числовые значения `Decimal`.
Таким образом, преобразование имеет вид:
```text
Legacy float values
MarketPriceCache
Decimal values
Canonical Quote
```
---
## 8. Чтение через MarketPriceSnapshot
Существующие потребители ожидают от:
```python
MarketPriceCache.get_price(...)
```
объект:
```text
MarketPriceSnapshot
```
Поэтому Build 033 не удаляет эту модель.
При чтении выполняется обратное compatibility-преобразование:
```text
QuoteStore
Quote
MarketPriceSnapshot
```
Сохраняются legacy-поля:
```text
symbol
price
bid_price
ask_price
updated_at
source
runtime_key
```
Также сохранены методы:
```python
age_seconds()
has_bid_ask()
```
Благодаря этому существующие потребители не потребовали изменений.
---
## 9. Сохранение семантики свежести
Legacy-потребители используют:
```python
cached_price.age_seconds()
```
для определения возраста котировки.
Build 033 сохраняет этот публичный контракт.
Возраст snapshot определяется на основе канонической информации о времени получения котировки.
Таким образом, freshness-семантика больше не требует отдельного независимого хранилища состояния внутри `MarketPriceCache`.
Существующие вызовы:
```python
cached_price.age_seconds()
```
продолжают работать без изменений.
---
## 10. Сохранение семантики bid/ask
Legacy-модель предоставляет:
```python
has_bid_ask()
```
Этот контракт сохранён.
Он продолжает использоваться существующими execution-потребителями для проверки наличия корректных положительных значений:
```text
bid_price
ask_price
```
Build 033 не требует изменения существующих потребителей этой проверки.
---
## 11. Изоляция runtime_key
Сохранена существующая изоляция котировок по:
```text
runtime_key
```
Например:
```text
auto
debug_auto
default
```
Котировки одного инструмента в разных runtime остаются независимыми.
Концептуальный ключ хранения:
```text
source_name
+
runtime_key
+
symbol
```
Это позволяет одновременно хранить:
```text
BTC/USD_LEVERAGE + auto
BTC/USD_LEVERAGE + debug_auto
BTC/USD_LEVERAGE + default
```
как независимые runtime-записи.
---
## 12. Нормализация runtime_key и symbol
Сохранено существующее поведение нормализации.
Символ нормализуется в uppercase:
```text
btc/usd_leverage
BTC/USD_LEVERAGE
```
`runtime_key` нормализуется в lowercase:
```text
AUTO
auto
```
Это сохраняет прежнюю семантику `MarketPriceCache`.
---
## 13. Разделение source_name и Quote.source
Build 033 сохраняет архитектурное различие между:
```text
source_name
```
и:
```text
Quote.source
```
`source_name` определяет namespace хранения.
`Quote.source` определяет фактическое происхождение котировки.
Например:
```text
Storage namespace:
legacy-market-price-cache
Actual quote source:
ws_depth:auto
```
или:
```text
Storage namespace:
legacy-market-price-cache
Actual quote source:
market-polling
```
Это предотвращает смешивание:
- идентичности storage namespace;
- provenance рыночных данных.
---
## 14. Сохранение семантики clear()
Полностью сохранены существующие варианты очистки.
### Полная очистка facade namespace
```python
MarketPriceCache.clear()
```
Очищает все записи, принадлежащие `MarketPriceCache`.
### Очистка символа во всех runtime
```python
MarketPriceCache.clear("BTC/USD_LEVERAGE")
```
Очищает указанный символ во всех runtime внутри namespace facade.
### Очистка runtime по всем символам
```python
MarketPriceCache.clear(runtime_key="auto")
```
Очищает все символы указанного runtime.
### Точечная очистка
```python
MarketPriceCache.clear(
"BTC/USD_LEVERAGE",
runtime_key="auto",
)
```
Очищает только конкретную запись.
---
## 15. Изоляция от других владельцев Quote Store
Критически важное требование Build 033:
```text
MarketPriceCache.clear()
```
не должен удалять котировки, записанные другими владельцами или источниками в канонический `Quote Store`.
Поэтому операции facade ограничиваются собственным storage namespace.
Архитектурно:
```text
Quote Store
├── legacy-market-price-cache
│ ├── auto
│ ├── debug_auto
│ └── default
└── other-source
└── ...
```
Очистка:
```python
MarketPriceCache.clear()
```
затрагивает только:
```text
legacy-market-price-cache
```
и не изменяет данные других namespace.
---
## 16. Обратная совместимость
Build 033 не изменил публичные вызовы:
```python
MarketPriceCache.set_price(...)
MarketPriceCache.get_price(...)
MarketPriceCache.clear(...)
```
Не изменены существующие production-потребители:
```text
src/integrations/exchange/service.py
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
```
Также сохранены legacy-контракты:
```python
MarketPriceSnapshot.age_seconds()
MarketPriceSnapshot.has_bid_ask()
```
Это позволило выполнить архитектурную миграцию без изменения поведения работающего бота.
---
## 17. Тестовое покрытие
Добавлен специализированный тестовый файл:
```text
tests/unit/integrations/exchange/test_market_cache.py
```
Тестами проверяются:
- соответствие `MarketPriceCache` каноническому `QuoteStoreProtocol`;
- запись канонического `Quote`;
- чтение через legacy `MarketPriceSnapshot`;
- сохранение `symbol`;
- сохранение `price`;
- сохранение `bid_price`;
- сохранение `ask_price`;
- сохранение `source`;
- сохранение `runtime_key`;
- нормализация символа;
- нормализация `runtime_key`;
- изоляция разных runtime;
- изоляция разных символов;
- замена предыдущей котировки новой;
- полная очистка facade namespace;
- очистка по символу;
- очистка по runtime;
- точечная очистка;
- вычисление возраста snapshot;
- legacy-проверка `has_bid_ask()`;
- преобразование timestamp;
- защита внешних записей другого `source_name` от очистки через `MarketPriceCache`.
---
## 18. Проверка компиляции
Выполнена команда:
```bash
python -m py_compile \
src/integrations/exchange/market_cache.py \
tests/unit/integrations/exchange/test_market_cache.py
```
Результат:
```text
SUCCESS
```
Ошибок компиляции нет.
---
## 19. Специализированные тесты
Выполнена команда:
```bash
python -m pytest \
tests/unit/integrations/exchange/test_market_cache.py \
tests/unit/storage/test_quote_store.py \
-q
```
Результат:
```text
76 passed in 0.04s
```
Все специализированные тесты успешно пройдены.
---
## 20. Регрессионная проверка потребителей
Выполнена команда:
```bash
python -m pytest \
tests/unit/integrations/exchange/test_service_quotes_facade.py \
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
tests/unit/integrations/exchange/test_market_stream.py \
tests/unit/integrations/exchange/test_market_data_runner.py \
-q
```
Результат:
```text
47 passed in 0.13s
```
Регрессионный контур существующих потребителей полностью сохранён.
---
## 21. Полная регрессионная проверка проекта
Выполнена команда:
```bash
python -m pytest -q
```
Результат:
```text
578 passed in 0.28s
```
Все тесты проекта успешно пройдены.
Регрессий не обнаружено.
---
## 22. Архитектурный результат
До Build 033:
```text
Legacy consumers
MarketPriceCache
Private _prices dict
MarketPriceSnapshot
```
Параллельно существовал:
```text
Canonical Quote
Quote Store
```
После Build 033:
```text
Legacy consumers
MarketPriceCache
compatibility facade
Canonical Quote
Quote Store
```
При чтении:
```text
Quote Store
Canonical Quote
MarketPriceSnapshot
compatibility model
Legacy consumer
```
Таким образом, независимое legacy-хранилище котировок устранено.
---
## 23. Что намеренно не входит в Build 033
Build 033 не выполняет:
- удаление `MarketPriceCache`;
- удаление `MarketPriceSnapshot`;
- перевод WebSocket parsing на новый Dzengi quote adapter;
- перевод `MarketDataRunner` на `Quotes Feed`;
- перевод UI-потребителей на канонический `Quote`;
- перевод execution-потребителей на канонический `Quote`;
- удаление `TickerPrice`;
- удаление legacy market snapshot dict layer;
- удаление legacy quote parsing.
Эти изменения выполняются последующими этапами утверждённого плана.
---
## 24. Условия завершения
Build 033 считается завершённым, поскольку выполнены все обязательные условия:
- [x] `MarketPriceCache` больше не владеет собственным `_prices` dict.
- [x] Канонический `Quote Store` используется для хранения котировок facade.
- [x] `set_price()` преобразует legacy-вход в канонический `Quote`.
- [x] `get_price()` возвращает совместимый `MarketPriceSnapshot`.
- [x] Сохранён контракт `age_seconds()`.
- [x] Сохранён контракт `has_bid_ask()`.
- [x] Сохранена изоляция по `runtime_key`.
- [x] Сохранена нормализация символа.
- [x] Сохранена семантика `clear()`.
- [x] Очистка facade не затрагивает другие storage namespace.
- [x] Production-потребители не потребовали изменений.
- [x] Специализированные тесты успешно пройдены.
- [x] Регрессионные тесты потребителей успешно пройдены.
- [x] Полный набор тестов проекта успешно пройден.
- [x] Обратная совместимость работающего бота сохранена.
---
## 25. Статус Build
**Build 033 — Completed.**
Канонический `Quote Store` теперь является владельцем состояния котировок, доступных через legacy `MarketPriceCache`.
`MarketPriceCache` сохранён только как временный compatibility facade для существующих потребителей.
Следующий этап:
```text
Build 034 — Dzengi WebSocket quote parsing и адаптер
```