build 039: complete Quotes Feed migration foundation

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit 7b62873832
443 changed files with 80452 additions and 1335 deletions

View File

@@ -0,0 +1,864 @@
# 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 и адаптер
```