Files
dzentra_bot/docs/migrations/build_040.md

22 KiB
Raw Permalink Blame History

Build 040 — Восстановление принудительного REST refresh котировки на канонической модели Quote

Статус: Завершён
Проект: Dzentra
Подсистема: Market Data Acquisition / Exchange Integration / Market Data Runtime
Тип изменения: Восстановление runtime-контракта после миграции legacy price/snapshot API
Язык документа: Русский


1. Цель Build 040

Цель Build 040 — восстановить принудительный REST refresh текущей рыночной котировки для MarketDataRunner после завершения Build 039 и удаления legacy price/snapshot API.

В Build 039 были удалены legacy-сущности и методы:

TickerPrice
MarketPriceSnapshot
get_price()
get_market_snapshot()
get_fresh_market_snapshot()
refresh_price_cache()
refresh_market_snapshot_cache()
_get_real_price()

После завершения миграции был обнаружен один оставшийся активный consumer удалённого метода:

src/integrations/exchange/market_data_runner.py

который продолжал вызывать:

ExchangeService().refresh_market_snapshot_cache

Таким образом, целью Build 040 являлось не изменение runtime-архитектуры и не новый рефакторинг, а минимальное восстановление разорванного контракта на основе канонической модели:

src.market_data.acquisition.models.quote.Quote

При этом необходимо было сохранить существующую логику работы старого бота без изменений.


2. Исходное состояние

После Build 039 проект находился в следующем состоянии:

  1. все основные consumers текущей рыночной цены были переведены на Quote;

  2. legacy-модели:

TickerPrice
MarketPriceSnapshot

были удалены;

  1. legacy-методы получения и обновления price/snapshot были удалены;

  2. полный regression suite успешно проходил:

606 passed
  1. контрольный grep по основным legacy-контрактам был пустым;

  2. при дополнительном анализе runtime-компонентов был обнаружен оставшийся вызов:

ExchangeService().refresh_market_snapshot_cache

в:

app/src/integrations/exchange/market_data_runner.py

Метод уже отсутствовал в ExchangeService, поэтому существовал разорванный runtime-контракт, который не проявлялся в предыдущем полном наборе тестов.


3. Архитектурный принцип Build 040

Build 040 сохраняет утверждённый архитектурный поток:

Exchange / Market Data Sources
        ↓
Market Data Acquisition
        ↓
Canonical Quote
        ↓
Quote Store / Market Price Cache
        ↓
ExchangeService
        ↓
MarketDataRunner / Trading Consumers

Канонической моделью текущей котировки остаётся:

Quote

из:

src.market_data.acquisition.models.quote

Build 040 не вводит новую параллельную price/snapshot-модель и не восстанавливает удалённые legacy-контракты.

Вместо этого добавляется минимальный публичный compatibility-метод:

refresh_quote_cache()

который использует существующий канонический acquisition pipeline.


4. Главное требование сохранения поведения

Основным требованием Build 040 являлось:

Не менять логику работы текущих файлов и существующего runtime.

Поэтому перед реализацией была восстановлена фактическая старая реализация удалённого метода:

refresh_market_snapshot_cache()

из истории Git.

Это позволило определить реальный контракт старого метода без предположений.

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

нормализация runtime_key
        ↓
принудительное получение свежего REST snapshot
        ↓
проверка ценовых полей
        ↓
запись результата в MarketPriceCache
        ↓
возврат свежего snapshot

Критически важно:

  • cache перед REST-запросом не читался;
  • всегда выполнялось принудительное получение свежих данных;
  • результат сохранялся в cache соответствующего runtime;
  • возвращался свежий результат;
  • ошибки не подавлялись самим refresh-методом;
  • mock-режим сохранялся.

5. Восстановление старого контракта через Git

Для определения фактического поведения удалённого метода была использована история Git.

Команда:

git log -S"def refresh_market_snapshot_cache" \
  --oneline \
  -- src/integrations/exchange/service.py

Результат:

a996f2f feat: add market data architecture and complete migration through build 039
e97dcd3 07.4.3.16 — Production Execution Pricing Layer

Старая реализация была извлечена командой:

git show a996f2f^:app/src/integrations/exchange/service.py \
  | grep -n -A45 -B10 "def refresh_market_snapshot_cache"

Восстановленный контракт подтвердил, что новый метод должен быть принудительным refresh-механизмом и не должен использовать обычную cache-first логику get_quote().


6. Почему get_quote() не подходит для REST fallback

Существующий метод:

ExchangeService.get_quote()

реализует cache-first поведение:

валидация символа
        ↓
чтение MarketPriceCache
        ↓
если Quote свежий
        ↓
возврат cached Quote
        ↓
иначе REST acquisition

Такое поведение корректно для обычных consumers, но не подходит для:

MarketDataRunner._rest_fallback_once()

После отказа WebSocket runner должен проверить именно доступность REST-механизма.

Если использовать get_quote(), возможна ситуация:

WebSocket отключился
        ↓
в cache остаётся свежий WebSocket Quote
        ↓
get_quote() возвращает cached Quote
        ↓
REST-запрос фактически не выполняется
        ↓
runner ошибочно устанавливает REST state = AVAILABLE

Поэтому Build 040 вводит отдельный принудительный refresh-контракт.


7. Новый публичный контракт refresh_quote_cache()

В ExchangeService добавлен метод:

def refresh_quote_cache(
    self,
    symbol: str | None = None,
    *,
    runtime_key: str | None = None,
) -> Quote:

Его ответственность:

нормализовать runtime_key
        ↓
определить symbol
        ↓
если exchange disabled
    получить mock Quote
иначе
    валидировать symbol
        ↓
    принудительно получить свежий Quote через REST acquisition
        ↓
сохранить тот же Quote в MarketPriceCache
        ↓
вернуть тот же Quote

Метод не читает cache перед получением свежей котировки.


8. Сохранение mock-поведения

Старый метод:

refresh_market_snapshot_cache()

через:

get_fresh_market_snapshot()

поддерживал режим:

exchange_enabled = False

В этом режиме создавались mock-данные, которые затем сохранялись в cache.

Новый метод сохраняет эквивалентное поведение через:

mock_quote(symbol_to_use)

При выключенной бирже:

  • REST acquisition не выполняется;
  • validation через exchange reference data не требуется;
  • создаётся канонический Quote;
  • тот же объект сохраняется в MarketPriceCache;
  • тот же объект возвращается consumer.

9. Принудительное получение свежего Quote

В real-режиме новый метод использует:

self._get_fresh_quote(
    validation.normalized_symbol,
)

Этот метод вызывает существующий канонический pipeline:

DzengiQuoteDocumentSource
        ↓
DzengiQuoteDocumentHandler
        ↓
QuotesFeed
        ↓
QuoteFeedRegistry
        ↓
QuoteAcquisitionService
        ↓
Quote

Таким образом, Build 040 не добавляет новый способ получения котировок и не дублирует Market Data Acquisition.


10. Почему QuoteAcquisitionService не изменялся

В ходе анализа было подтверждено, что:

QuoteAcquisitionService

намеренно отвечает только за получение канонической модели через зарегистрированный feed.

Он не должен выполнять:

  • runtime isolation;
  • cache management;
  • fallback orchestration;
  • validation торгового runtime;
  • выбор runtime_key;
  • compatibility-поведение старого бота.

Поэтому Build 040 не изменяет:

app/src/market_data/acquisition/service.py
app/src/market_data/acquisition/feeds/quotes_feed.py

Compatibility и runtime semantics остаются в:

ExchangeService

11. Изменение MarketDataRunner

В:

app/src/integrations/exchange/market_data_runner.py

был изменён только один runtime-вызов.

До Build 040:

ExchangeService().refresh_market_snapshot_cache

После Build 040:

ExchangeService().refresh_quote_cache

При этом не изменялись:

  • asyncio.to_thread;
  • передаваемый symbol;
  • runtime_key;
  • last_rest_state;
  • last_rest_error_key;
  • WebSocket-first поведение;
  • cooldown;
  • retry-логика;
  • journal events;
  • event titles;
  • lifecycle runner;
  • exception handling.

12. Runtime isolation

Новый метод сохраняет существующее разделение runtime-контекстов через:

runtime_key

Перед использованием ключ нормализуется существующим методом:

self._runtime_key(runtime_key)

Котировка сохраняется через:

MarketPriceCache.set_quote(
    quote,
    runtime_key=normalized_runtime_key,
)

Это сохраняет независимость runtime-контекстов, включая:

auto
debug_auto
default

и другие существующие значения, если они используются текущим runtime.


13. Сохранение identity канонического Quote

Каноническая модель:

Quote

является immutable-моделью:

@dataclass(frozen=True, slots=True)

Build 040 не создаёт копию полученного объекта.

Последовательность:

fresh Quote
    ↓
MarketPriceCache.set_quote(quote)
    ↓
return quote

В cache сохраняется тот же экземпляр, который возвращается из метода.

Это зафиксировано тестами.


14. Изменённые production-файлы

В рамках Build 040 изменены только два production-файла:

app/src/integrations/exchange/service.py
app/src/integrations/exchange/market_data_runner.py

14.1. service.py

Добавлен новый публичный метод:

refresh_quote_cache()

Существующие методы не подвергались сопутствующему рефакторингу.

В частности, не изменялись:

get_quote()
get_execution_snapshot()
_get_fresh_quote()
_load_quote_via_acquisition()

14.2. market_data_runner.py

Изменена только ссылка на удалённый legacy refresh-метод.

Остальная логика runner сохранена.


15. Изменённые тестовые файлы

В рамках Build 040 изменены:

app/tests/unit/integrations/exchange/test_service_quote.py
app/tests/unit/integrations/exchange/test_market_data_runner.py

Добавлены тесты нового refresh-контракта и REST fallback runner.


16. Тесты refresh_quote_cache()

Тестами зафиксировано следующее поведение:

  • принудительное получение свежего Quote;
  • отсутствие cache-first поведения;
  • замена существующей cache-записи новым объектом;
  • сохранение того же экземпляра Quote;
  • использование правильного runtime_key;
  • нормализация runtime key;
  • использование default runtime key;
  • сохранение ExchangeError для невалидного символа;
  • отсутствие REST acquisition при невалидном символе;
  • сохранение exception semantics acquisition pipeline;
  • отсутствие обновления cache при ошибке;
  • сохранение mock-поведения.

17. Тесты REST fallback MarketDataRunner

Тестами зафиксировано поведение:

MarketDataRunner._rest_fallback_once()

При успешном refresh:

refresh_quote_cache() вызывается
        ↓
передаётся текущий symbol
        ↓
передаётся context.runtime_key
        ↓
last_rest_state = AVAILABLE
        ↓
last_rest_error_key = None

При ошибке сохраняется существующее поведение runner:

last_rest_state = UNAVAILABLE

и существующая обработка ошибки не изменяется.


18. Целевая проверка Build 040

Выполнена команда:

python -m pytest -q \
  tests/unit/integrations/exchange/test_service_quote.py \
  tests/unit/integrations/exchange/test_market_data_runner.py

Финальный результат:

18 passed in 0.11s

Все целевые тесты Build 040 успешно пройдены.


19. Связанные регрессионные тесты

Выполнена команда:

python -m pytest -q \
  tests/unit/integrations/exchange/test_service_execution_quote.py \
  tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
  tests/unit/integrations/exchange/test_market_cache.py \
  tests/unit/integrations/exchange/test_market_stream.py \
  tests/unit/storage/test_quote_store.py

Финальный результат:

107 passed in 1.81s

Связанные компоненты работают без регрессий.


20. Полный regression suite

После завершения Build 040 выполнен полный набор тестов:

python -m pytest -q

Финальный результат:

614 passed in 2.03s

Все тесты успешно пройдены.

По сравнению с Build 039 количество тестов увеличилось:

606 → 614

Добавлено:

8 тестов

21. Финальный архитектурный grep

Выполнена команда:

grep -RIn \
  --exclude-dir="__pycache__" \
  --exclude="*.pyc" \
  -E "refresh_market_snapshot_cache|refresh_price_cache|MarketPriceSnapshot|TickerPrice" \
  src tests

Финальный результат:

<пусто>

Это подтверждает отсутствие следующих legacy-контрактов:

refresh_market_snapshot_cache
refresh_price_cache
MarketPriceSnapshot
TickerPrice

в:

src
tests

22. Что намеренно не изменялось

В Build 040 намеренно не изменялись:

Market Data Acquisition architecture
Quote model
QuoteAcquisitionService
QuotesFeed
Quote Store
MarketPriceCache contract
get_quote()
get_execution_snapshot()
WebSocket adapter
market_stream.py
MarketDataRunner lifecycle
MarketDataRunner retry logic
runtime state transitions
journal events
event titles
Telegram UI
trading strategies
execution logic
diagnostics logic

Также не выполнялись:

  • переносы файлов;
  • переименования каталогов;
  • изменение утверждённой структуры проекта;
  • добавление новых abstraction layers;
  • добавление override-параметров «на будущее»;
  • удаление дополнительных legacy-компонентов вне границ Build 040.

23. Критерии завершения Build 040

Build 040 считается завершённым, поскольку выполнены все критерии:

  • найден оставшийся активный вызов удалённого refresh_market_snapshot_cache;
  • фактический старый контракт восстановлен из Git;
  • новый контракт реализован на канонической модели Quote;
  • добавлен refresh_quote_cache();
  • новый метод не использует cache-first поведение;
  • fresh Quote принудительно получается через существующий acquisition pipeline;
  • runtime isolation сохранена;
  • mock-поведение сохранено;
  • exception semantics сохранена;
  • тот же экземпляр Quote сохраняется и возвращается;
  • MarketDataRunner переключён на новый метод;
  • WebSocket-first логика не изменена;
  • lifecycle runner не изменён;
  • целевые тесты проходят;
  • связанные регрессионные тесты проходят;
  • полный regression suite проходит;
  • финальный legacy grep пуст.

24. Итоговое состояние после Build 040

После завершения Build 040 runtime-поток получения котировки имеет следующий вид:

WebSocket available
        ↓
DzengiWebSocketQuoteAdapter
        ↓
Quote
        ↓
MarketPriceCache

При отказе WebSocket:

WebSocket unavailable
        ↓
MarketDataRunner._rest_fallback_once()
        ↓
ExchangeService.refresh_quote_cache()
        ↓
Quote Acquisition Pipeline
        ↓
Fresh REST Quote
        ↓
MarketPriceCache
        ↓
REST state = AVAILABLE

При ошибке REST refresh:

REST acquisition error
        ↓
ExchangeError
        ↓
MarketDataRunner existing error handling
        ↓
REST state = UNAVAILABLE

Таким образом, Build 040 восстанавливает разорванный fallback-контракт, не изменяя существующую runtime-логику.


25. Зафиксированный принцип для следующих Builds

Build 040 подтверждает принцип, зафиксированный после Build 039:

При миграции нельзя считать новую функцию эквивалентной старой только потому, что они решают похожую задачу.

Перед заменой legacy-компонента необходимо восстановить и сравнить фактическое поведение:

порядок операций
чтение cache
запись cache
принудительность refresh
тип возвращаемого значения
identity результата
runtime isolation
mock-поведение
fallback-поведение
exception semantics
side effects
граничные условия

Если старый контракт доступен в истории Git, его необходимо использовать как источник фактического поведения вместо предположений.


26. Статус Build

Build 040: COMPLETED
Target tests: 18 passed
Related regression tests: 107 passed
Full regression suite: 614 passed
Legacy grep: empty
Canonical quote model: Quote
REST fallback contract: refresh_quote_cache()

Build 040 полностью завершён.