864 lines
22 KiB
Markdown
864 lines
22 KiB
Markdown
# 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 и адаптер
|
||
``` |