# 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 и адаптер ```