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