Files
dzentra_bot/docs/migrations/build_032.md

1033 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 032 — Канонический Quote Store
**Статус:** Завершён
**Результат:** Успешно
**Полная регрессия:** `562 passed`
---
## 1. Назначение Build
Цель Build 032 — создать каноническое оперативное хранилище текущих котировок `Quote` в Storage layer и подготовить архитектурную основу для последующего переноса legacy-механизма `MarketPriceCache`.
До выполнения Build 032 в проекте уже существовал канонический контур получения текущей котировки:
```text
Dzengi REST API
DzengiQuoteDocumentSource
Dzengi quote parser
Dzengi quote value validation
Dzengi quote mapper
QuotesHandler
QuotesFeed
QuoteAcquisitionService
Quote
```
Однако канонического хранилища объектов `Quote` ещё не существовало.
Текущие runtime-котировки продолжал хранить legacy-компонент:
```text
MarketPriceCache
```
Build 032 вводит новый независимый Storage-компонент:
```text
canonical Quote
InMemoryQuoteStore
```
При этом существующий production pipeline не переключается на новое хранилище в рамках данного Build.
---
## 2. Архитектурная граница Build
Build 032 ограничен созданием канонического Quote Store.
В рамках Build:
- добавлен контракт `QuoteStoreProtocol`;
- добавлена in-memory реализация `InMemoryQuoteStore`;
- добавлена специализированная ошибка `QuoteStoreError`;
- реализовано хранение канонических объектов `Quote`;
- реализована изоляция по источнику данных;
- реализована изоляция по runtime-контексту;
- реализована изоляция по торговому инструменту;
- реализована полная, выборочная и комбинированная очистка;
- добавлен полный набор unit-тестов.
В рамках Build не выполнялись:
- изменение `ExchangeService`;
- изменение `MarketPriceCache`;
- подключение `QuoteStore` к `ExchangeService`;
- перенос данных из `MarketPriceCache`;
- изменение `QuotesFeed`;
- изменение `QuoteAcquisitionService`;
- изменение WebSocket-контура;
- изменение market runtime;
- изменение UI-потребителей;
- изменение execution-потребителей;
- удаление legacy-компонентов.
Эти изменения относятся к последующим Build утверждённого плана миграции Quotes Feed.
---
## 3. Изменённые файлы
### Изменён production-файл
```text
src/storage/exceptions.py
```
Добавлена специализированная ошибка:
```python
QuoteStoreError
```
### Добавлен production-файл
```text
src/storage/quote_store.py
```
Содержит:
```python
QuoteStoreProtocol
InMemoryQuoteStore
```
### Добавлен тестовый файл
```text
tests/unit/storage/test_quote_store.py
```
---
## 4. Целевая архитектура
После Build 032 Storage layer содержит два специализированных канонических хранилища:
```text
src/storage/
├── exceptions.py
├── instrument_store.py
└── quote_store.py
```
Архитектурно:
```text
Instrument
InMemoryInstrumentStore
```
и:
```text
Quote
InMemoryQuoteStore
```
`InstrumentStore` хранит канонический справочник инструментов.
`QuoteStore` хранит канонические текущие котировки.
---
## 5. Контракт Quote Store
Канонический контракт представлен протоколом:
```python
QuoteStoreProtocol
```
Он определяет три основные операции:
```text
get()
set()
clear()
```
Концептуальный контракт:
```python
@runtime_checkable
class QuoteStoreProtocol(Protocol):
def get(
self,
source_name: str,
symbol: str,
*,
runtime_key: str = "default",
) -> Quote | None:
...
def set(
self,
source_name: str,
quote: Quote,
*,
runtime_key: str = "default",
) -> None:
...
def clear(
self,
source_name: str | None = None,
symbol: str | None = None,
*,
runtime_key: str | None = None,
) -> None:
...
```
Контракт не зависит от:
- Dzengi;
- REST;
- WebSocket;
- `ExchangeService`;
- `MarketPriceCache`;
- UI;
- Execution layer.
---
## 6. Модель хранения
Quote Store использует составной ключ:
```text
source_name + runtime_key + symbol
```
Внутреннее представление:
```python
tuple[str, str, str]
```
Примеры независимых записей:
```text
("dzengi", "auto", "BTC/USD_LEVERAGE")
("dzengi", "debug_auto", "BTC/USD_LEVERAGE")
("secondary", "auto", "BTC/USD_LEVERAGE")
```
Все эти записи независимы друг от друга.
Такая модель позволяет одновременно хранить:
- котировки от разных поставщиков;
- котировки для разных runtime-контекстов;
- котировки разных инструментов.
---
## 7. Семантика `source_name`
`source_name` представляет namespace источника данных в Storage layer.
Применяются следующие правила:
```text
внешние пробелы удаляются;
регистр сохраняется;
пустое значение запрещено.
```
Пример:
```text
" dzengi " → "dzengi"
```
При этом:
```text
"DZENGI" != "dzengi"
```
Такое поведение соответствует существующей семантике:
```text
InstrumentRegistry
QuoteRegistry
InstrumentStore
```
Quote Store не требует равенства:
```text
source_name == quote.source
```
Это принципиально позволяет использовать алиасы источников:
```text
dzengi
dzengi-demo
dzengi-prod
dzengi-primary
```
при сохранении канонического происхождения самой модели в:
```python
quote.source
```
---
## 8. Семантика `runtime_key`
`runtime_key` разделяет независимые runtime-контексты.
Примеры:
```text
default
auto
debug_auto
```
Правила нормализации:
```text
внешние пробелы удаляются;
значение приводится к lowercase;
пустое значение запрещено.
```
Пример:
```text
" AUTO " → "auto"
```
Таким образом:
```text
AUTO
Auto
auto
```
адресуют один runtime namespace:
```text
auto
```
Эта семантика соответствует существующему поведению legacy `MarketPriceCache`.
---
## 9. Семантика `symbol`
Символ используется как третья часть ключа Quote Store.
Правила нормализации:
```text
внешние пробелы удаляются;
значение приводится к uppercase;
пустое значение запрещено.
```
Пример:
```text
" btc/usd_leverage "
```
преобразуется в ключ:
```text
BTC/USD_LEVERAGE
```
Нормализация применяется только к ключу хранения.
Сам объект `Quote` не изменяется.
---
## 10. Семантика `set()`
Метод:
```python
set()
```
сохраняет канонический объект `Quote`.
Основные гарантии:
- принимается объект `Quote`;
- используется `quote.symbol`;
- объект сохраняется без копирования;
- `Decimal` не преобразуется в `float`;
- `datetime` не преобразуется в строку;
- timestamps не изменяются;
- существующая запись с тем же ключом заменяется;
- содержимое `Quote` не нормализуется повторно.
Пример:
```python
store.set(
"dzengi",
quote,
runtime_key="auto",
)
```
Если для ключа:
```text
("dzengi", "auto", "BTC/USD_LEVERAGE")
```
уже существует запись, она заменяется новой.
Quote Store сохраняет identity объекта:
```python
store.get(
"dzengi",
"BTC/USD_LEVERAGE",
runtime_key="auto",
) is quote
```
---
## 11. Семантика `get()`
Метод:
```python
get()
```
возвращает:
```python
Quote | None
```
Если запись существует:
```text
возвращается исходный сохранённый объект Quote
```
Если запись отсутствует:
```python
None
```
Store не:
- создаёт копию;
- выполняет сетевой запрос;
- обращается к Acquisition layer;
- вычисляет freshness;
- выполняет fallback.
---
## 12. Семантика `clear()`
Метод:
```python
clear()
```
поддерживает полную, выборочную и комбинированную очистку.
### 12.1. Полная очистка
```python
store.clear()
```
Удаляет все сохранённые котировки.
---
### 12.2. Очистка по источнику
```python
store.clear(
source_name="dzengi",
)
```
Удаляет все котировки указанного источника независимо от:
- символа;
- runtime-контекста.
---
### 12.3. Очистка по символу
```python
store.clear(
symbol="BTC/USD_LEVERAGE",
)
```
Удаляет указанный символ у всех:
- источников;
- runtime-контекстов.
---
### 12.4. Очистка по runtime
```python
store.clear(
runtime_key="auto",
)
```
Удаляет все котировки указанного runtime-контекста.
---
### 12.5. Точная очистка
```python
store.clear(
source_name="dzengi",
symbol="BTC/USD_LEVERAGE",
runtime_key="auto",
)
```
Удаляет только одну конкретную запись.
---
### 12.6. Комбинированная очистка
Поддерживаются комбинации фильтров.
Например:
```python
store.clear(
source_name="dzengi",
runtime_key="auto",
)
```
Удаляет все котировки источника `dzengi` только из runtime:
```text
auto
```
Остальные записи сохраняются.
---
### 12.7. Идемпотентность
Очистка отсутствующей записи не является ошибкой.
Например:
```python
store.clear(
source_name="unknown",
symbol="UNKNOWN",
runtime_key="unknown",
)
```
завершается без исключения.
---
## 13. Специализированная ошибка
В Storage layer добавлена ошибка:
```python
QuoteStoreError
```
Иерархия:
```text
Exception
StorageError
QuoteStoreError
```
Она используется для нарушений контракта Quote Store.
Примеры:
- пустой `source_name`;
- пустой `runtime_key`;
- пустой `symbol`;
- передача объекта неправильного типа.
Это позволяет отличать ошибки хранения котировок от:
- ошибок Acquisition;
- ошибок адаптера биржи;
- транспортных ошибок;
- ошибок Exchange facade;
- ошибок Execution layer.
---
## 14. Сохранение канонической модели
Quote Store хранит непосредственно:
```python
Quote
```
Хранилище не создаёт промежуточные представления типа:
```text
MarketPriceSnapshot
dict[str, object]
TickerPrice
```
Архитектурно:
```text
Quote
Quote Store
```
а не:
```text
Quote
legacy dict
MarketPriceSnapshot
Store
```
Это принципиально для дальнейшего устранения legacy quote representations.
---
## 15. Сохранение точности чисел
Числовые поля канонического `Quote` используют:
```python
Decimal
```
Quote Store сохраняет их без преобразования.
Не выполняется:
```text
Decimal → float
```
Таким образом сохраняются:
- точность котировок;
- исходная числовая семантика;
- единый канонический тип данных.
---
## 16. Сохранение временной семантики
Quote Store сохраняет временные поля модели без преобразования.
Не выполняется:
```text
datetime → str
```
Store не:
- форматирует timestamps;
- переводит время в локальную строку;
- вычисляет возраст записи;
- определяет freshness.
Временная семантика остаётся частью канонической модели `Quote`.
---
## 17. Изоляция экземпляров
Разные экземпляры:
```python
InMemoryQuoteStore()
```
имеют независимое состояние.
Пример:
```text
first_store
собственные записи
second_store
собственные записи
```
Запись в одном экземпляре не появляется в другом.
Это отличает Quote Store от legacy `MarketPriceCache`, использующего class-level storage.
---
## 18. Ответственность Quote Store
Quote Store отвечает только за:
```text
хранение уже созданных канонических Quote
```
Quote Store не отвечает за:
- получение данных;
- REST-запросы;
- WebSocket-соединения;
- parsing;
- schema validation;
- value validation;
- sequence validation;
- mapping;
- retry;
- reconnect;
- freshness;
- вычисление возраста;
- выбор REST или WebSocket;
- market status;
- execution pricing.
Эти обязанности принадлежат другим компонентам архитектуры.
---
## 19. Тестовое покрытие
Добавлен файл:
```text
tests/unit/storage/test_quote_store.py
```
Проверены:
- соответствие `InMemoryQuoteStore` протоколу `QuoteStoreProtocol`;
- получение отсутствующей записи;
- сохранение `Quote`;
- получение сохранённого `Quote`;
- сохранение identity объекта;
- замена существующей записи;
- изоляция разных источников;
- изоляция разных runtime-контекстов;
- изоляция разных символов;
- нормализация внешних пробелов `source_name`;
- сохранение регистра `source_name`;
- lowercase-нормализация `runtime_key`;
- uppercase-нормализация символа;
- запрет пустого `source_name`;
- запрет пустого `runtime_key`;
- запрет пустого символа;
- запрет объекта неправильного типа;
- полная очистка;
- очистка по источнику;
- очистка по runtime;
- очистка по символу;
- точечная очистка;
- комбинированная очистка;
- идемпотентность очистки;
- независимость экземпляров Store;
- сохранение `Decimal`;
- сохранение `datetime`;
- наследование `QuoteStoreError` от `StorageError`.
---
## 20. Результаты проверки
Проверка синтаксиса:
```bash
python -m py_compile \
src/storage/exceptions.py \
src/storage/quote_store.py \
tests/unit/storage/test_quote_store.py
```
Результат:
```text
успешно
```
Специализированные тесты:
```bash
python -m pytest \
tests/unit/storage/test_quote_store.py \
tests/unit/storage/test_instrument_store.py \
-q
```
Результат:
```text
92 passed in 0.05s
```
Полная регрессия:
```bash
python -m pytest -q
```
Результат:
```text
562 passed in 0.28s
```
---
## 21. Изменение количества тестов
До Build 032:
```text
502 passed
```
После Build 032:
```text
562 passed
```
Добавлено:
```text
60 тестов
```
Полная регрессия осталась зелёной.
---
## 22. Состояние архитектуры после Build 032
После завершения Build 032 существуют два параллельных контура.
Канонический REST Quotes Feed:
```text
Dzengi REST API
DzengiQuoteDocumentSource
Dzengi quote parser
Dzengi quote value validation
Dzengi quote mapper
QuotesHandler
QuotesFeed
QuoteAcquisitionService
Quote
```
Каноническое хранилище:
```text
Quote
InMemoryQuoteStore
```
При этом legacy runtime-контур пока продолжает использовать:
```text
MarketPriceCache
```
То есть на момент завершения Build 032:
```text
QuoteAcquisitionService
Quote
и
InMemoryQuoteStore
```
существуют как канонические компоненты, но production pipeline ещё не переключён на новый Store.
---
## 23. Что не изменилось
Build 032 не изменил поведение работающего бота.
Не изменялись:
```text
src/integrations/exchange/service.py
src/integrations/exchange/market_cache.py
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
src/market_data/acquisition/service.py
```
Также не изменялись:
- Telegram UI;
- AutoTrade runtime;
- стратегии;
- diagnostics;
- debug runtime;
- execution pricing.
Это соответствует принятому принципу миграции:
```text
сначала создать новый канонический компонент
проверить его изолированно
подключить под существующие facade-контракты
перевести потребителей
удалить legacy только после полного переключения
```
---
## 24. Итог Build 032
Build 032 завершён успешно.
Создан канонический Storage-компонент для текущих котировок:
```text
QuoteStoreProtocol
InMemoryQuoteStore
Quote
```
Достигнуты следующие архитектурные свойства:
```text
каноническая модель хранения
изоляция источников
изоляция runtime-контекстов
изоляция символов
сохранение Decimal
сохранение datetime
отсутствие зависимости от биржи
отсутствие зависимости от Acquisition
отсутствие зависимости от ExchangeService
отсутствие зависимости от legacy MarketPriceCache
полная тестовая изоляция
```
Build завершён с полной зелёной регрессией:
```text
562 passed
```
---
## 25. Следующий этап
Следующий этап утверждённого плана:
```text
Build 033 — Перенос MarketPriceCache на Quote Store
```
Его задача — начать интеграцию канонического `QuoteStore` в существующий runtime-контур котировок без нарушения обратной совместимости работающего бота.
Целевая переходная схема:
```text
legacy consumers
ExchangeService facade
MarketPriceCache compatibility layer
Quote Store
canonical Quote
```
После Build 033 `MarketPriceCache` должен перестать быть самостоятельным владельцем quote state и стать временным compatibility layer над каноническим `QuoteStore`.