Files
dzentra_bot/docs/migrations/build_032.md

22 KiB
Raw Blame History

Build 032 — Канонический Quote Store

Статус: Завершён
Результат: Успешно
Полная регрессия: 562 passed


1. Назначение Build

Цель Build 032 — создать каноническое оперативное хранилище текущих котировок Quote в Storage layer и подготовить архитектурную основу для последующего переноса legacy-механизма MarketPriceCache.

До выполнения Build 032 в проекте уже существовал канонический контур получения текущей котировки:

Dzengi REST API
    ↓
DzengiQuoteDocumentSource
    ↓
Dzengi quote parser
    ↓
Dzengi quote value validation
    ↓
Dzengi quote mapper
    ↓
QuotesHandler
    ↓
QuotesFeed
    ↓
QuoteAcquisitionService
    ↓
Quote

Однако канонического хранилища объектов Quote ещё не существовало.

Текущие runtime-котировки продолжал хранить legacy-компонент:

MarketPriceCache

Build 032 вводит новый независимый Storage-компонент:

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-файл

src/storage/exceptions.py

Добавлена специализированная ошибка:

QuoteStoreError

Добавлен production-файл

src/storage/quote_store.py

Содержит:

QuoteStoreProtocol
InMemoryQuoteStore

Добавлен тестовый файл

tests/unit/storage/test_quote_store.py

4. Целевая архитектура

После Build 032 Storage layer содержит два специализированных канонических хранилища:

src/storage/
├── exceptions.py
├── instrument_store.py
└── quote_store.py

Архитектурно:

Instrument
    ↓
InMemoryInstrumentStore

и:

Quote
    ↓
InMemoryQuoteStore

InstrumentStore хранит канонический справочник инструментов.

QuoteStore хранит канонические текущие котировки.


5. Контракт Quote Store

Канонический контракт представлен протоколом:

QuoteStoreProtocol

Он определяет три основные операции:

get()
set()
clear()

Концептуальный контракт:

@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 использует составной ключ:

source_name + runtime_key + symbol

Внутреннее представление:

tuple[str, str, str]

Примеры независимых записей:

("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.

Применяются следующие правила:

внешние пробелы удаляются;
регистр сохраняется;
пустое значение запрещено.

Пример:

"  dzengi  " → "dzengi"

При этом:

"DZENGI" != "dzengi"

Такое поведение соответствует существующей семантике:

InstrumentRegistry
QuoteRegistry
InstrumentStore

Quote Store не требует равенства:

source_name == quote.source

Это принципиально позволяет использовать алиасы источников:

dzengi
dzengi-demo
dzengi-prod
dzengi-primary

при сохранении канонического происхождения самой модели в:

quote.source

8. Семантика runtime_key

runtime_key разделяет независимые runtime-контексты.

Примеры:

default
auto
debug_auto

Правила нормализации:

внешние пробелы удаляются;
значение приводится к lowercase;
пустое значение запрещено.

Пример:

"  AUTO  " → "auto"

Таким образом:

AUTO
Auto
auto

адресуют один runtime namespace:

auto

Эта семантика соответствует существующему поведению legacy MarketPriceCache.


9. Семантика symbol

Символ используется как третья часть ключа Quote Store.

Правила нормализации:

внешние пробелы удаляются;
значение приводится к uppercase;
пустое значение запрещено.

Пример:

" btc/usd_leverage "

преобразуется в ключ:

BTC/USD_LEVERAGE

Нормализация применяется только к ключу хранения.

Сам объект Quote не изменяется.


10. Семантика set()

Метод:

set()

сохраняет канонический объект Quote.

Основные гарантии:

  • принимается объект Quote;
  • используется quote.symbol;
  • объект сохраняется без копирования;
  • Decimal не преобразуется в float;
  • datetime не преобразуется в строку;
  • timestamps не изменяются;
  • существующая запись с тем же ключом заменяется;
  • содержимое Quote не нормализуется повторно.

Пример:

store.set(
    "dzengi",
    quote,
    runtime_key="auto",
)

Если для ключа:

("dzengi", "auto", "BTC/USD_LEVERAGE")

уже существует запись, она заменяется новой.

Quote Store сохраняет identity объекта:

store.get(
    "dzengi",
    "BTC/USD_LEVERAGE",
    runtime_key="auto",
) is quote

11. Семантика get()

Метод:

get()

возвращает:

Quote | None

Если запись существует:

возвращается исходный сохранённый объект Quote

Если запись отсутствует:

None

Store не:

  • создаёт копию;
  • выполняет сетевой запрос;
  • обращается к Acquisition layer;
  • вычисляет freshness;
  • выполняет fallback.

12. Семантика clear()

Метод:

clear()

поддерживает полную, выборочную и комбинированную очистку.

12.1. Полная очистка

store.clear()

Удаляет все сохранённые котировки.


12.2. Очистка по источнику

store.clear(
    source_name="dzengi",
)

Удаляет все котировки указанного источника независимо от:

  • символа;
  • runtime-контекста.

12.3. Очистка по символу

store.clear(
    symbol="BTC/USD_LEVERAGE",
)

Удаляет указанный символ у всех:

  • источников;
  • runtime-контекстов.

12.4. Очистка по runtime

store.clear(
    runtime_key="auto",
)

Удаляет все котировки указанного runtime-контекста.


12.5. Точная очистка

store.clear(
    source_name="dzengi",
    symbol="BTC/USD_LEVERAGE",
    runtime_key="auto",
)

Удаляет только одну конкретную запись.


12.6. Комбинированная очистка

Поддерживаются комбинации фильтров.

Например:

store.clear(
    source_name="dzengi",
    runtime_key="auto",
)

Удаляет все котировки источника dzengi только из runtime:

auto

Остальные записи сохраняются.


12.7. Идемпотентность

Очистка отсутствующей записи не является ошибкой.

Например:

store.clear(
    source_name="unknown",
    symbol="UNKNOWN",
    runtime_key="unknown",
)

завершается без исключения.


13. Специализированная ошибка

В Storage layer добавлена ошибка:

QuoteStoreError

Иерархия:

Exception
    ↓
StorageError
    ↓
QuoteStoreError

Она используется для нарушений контракта Quote Store.

Примеры:

  • пустой source_name;
  • пустой runtime_key;
  • пустой symbol;
  • передача объекта неправильного типа.

Это позволяет отличать ошибки хранения котировок от:

  • ошибок Acquisition;
  • ошибок адаптера биржи;
  • транспортных ошибок;
  • ошибок Exchange facade;
  • ошибок Execution layer.

14. Сохранение канонической модели

Quote Store хранит непосредственно:

Quote

Хранилище не создаёт промежуточные представления типа:

MarketPriceSnapshot
dict[str, object]
TickerPrice

Архитектурно:

Quote
    ↓
Quote Store

а не:

Quote
    ↓
legacy dict
    ↓
MarketPriceSnapshot
    ↓
Store

Это принципиально для дальнейшего устранения legacy quote representations.


15. Сохранение точности чисел

Числовые поля канонического Quote используют:

Decimal

Quote Store сохраняет их без преобразования.

Не выполняется:

Decimal → float

Таким образом сохраняются:

  • точность котировок;
  • исходная числовая семантика;
  • единый канонический тип данных.

16. Сохранение временной семантики

Quote Store сохраняет временные поля модели без преобразования.

Не выполняется:

datetime → str

Store не:

  • форматирует timestamps;
  • переводит время в локальную строку;
  • вычисляет возраст записи;
  • определяет freshness.

Временная семантика остаётся частью канонической модели Quote.


17. Изоляция экземпляров

Разные экземпляры:

InMemoryQuoteStore()

имеют независимое состояние.

Пример:

first_store
    ↓
собственные записи

second_store
    ↓
собственные записи

Запись в одном экземпляре не появляется в другом.

Это отличает Quote Store от legacy MarketPriceCache, использующего class-level storage.


18. Ответственность Quote Store

Quote Store отвечает только за:

хранение уже созданных канонических Quote

Quote Store не отвечает за:

  • получение данных;
  • REST-запросы;
  • WebSocket-соединения;
  • parsing;
  • schema validation;
  • value validation;
  • sequence validation;
  • mapping;
  • retry;
  • reconnect;
  • freshness;
  • вычисление возраста;
  • выбор REST или WebSocket;
  • market status;
  • execution pricing.

Эти обязанности принадлежат другим компонентам архитектуры.


19. Тестовое покрытие

Добавлен файл:

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. Результаты проверки

Проверка синтаксиса:

python -m py_compile \
  src/storage/exceptions.py \
  src/storage/quote_store.py \
  tests/unit/storage/test_quote_store.py

Результат:

успешно

Специализированные тесты:

python -m pytest \
  tests/unit/storage/test_quote_store.py \
  tests/unit/storage/test_instrument_store.py \
  -q

Результат:

92 passed in 0.05s

Полная регрессия:

python -m pytest -q

Результат:

562 passed in 0.28s

21. Изменение количества тестов

До Build 032:

502 passed

После Build 032:

562 passed

Добавлено:

60 тестов

Полная регрессия осталась зелёной.


22. Состояние архитектуры после Build 032

После завершения Build 032 существуют два параллельных контура.

Канонический REST Quotes Feed:

Dzengi REST API
    ↓
DzengiQuoteDocumentSource
    ↓
Dzengi quote parser
    ↓
Dzengi quote value validation
    ↓
Dzengi quote mapper
    ↓
QuotesHandler
    ↓
QuotesFeed
    ↓
QuoteAcquisitionService
    ↓
Quote

Каноническое хранилище:

Quote
    ↓
InMemoryQuoteStore

При этом legacy runtime-контур пока продолжает использовать:

MarketPriceCache

То есть на момент завершения Build 032:

QuoteAcquisitionService
        ↓
      Quote

        и

InMemoryQuoteStore

существуют как канонические компоненты, но production pipeline ещё не переключён на новый Store.


23. Что не изменилось

Build 032 не изменил поведение работающего бота.

Не изменялись:

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.

Это соответствует принятому принципу миграции:

сначала создать новый канонический компонент
        ↓
проверить его изолированно
        ↓
подключить под существующие facade-контракты
        ↓
перевести потребителей
        ↓
удалить legacy только после полного переключения

24. Итог Build 032

Build 032 завершён успешно.

Создан канонический Storage-компонент для текущих котировок:

QuoteStoreProtocol
        ↓
InMemoryQuoteStore
        ↓
Quote

Достигнуты следующие архитектурные свойства:

каноническая модель хранения
изоляция источников
изоляция runtime-контекстов
изоляция символов
сохранение Decimal
сохранение datetime
отсутствие зависимости от биржи
отсутствие зависимости от Acquisition
отсутствие зависимости от ExchangeService
отсутствие зависимости от legacy MarketPriceCache
полная тестовая изоляция

Build завершён с полной зелёной регрессией:

562 passed

25. Следующий этап

Следующий этап утверждённого плана:

Build 033 — Перенос MarketPriceCache на Quote Store

Его задача — начать интеграцию канонического QuoteStore в существующий runtime-контур котировок без нарушения обратной совместимости работающего бота.

Целевая переходная схема:

legacy consumers
        ↓
ExchangeService facade
        ↓
MarketPriceCache compatibility layer
        ↓
Quote Store
        ↓
canonical Quote

После Build 033 MarketPriceCache должен перестать быть самостоятельным владельцем quote state и стать временным compatibility layer над каноническим QuoteStore.