22 KiB
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 проект находился в следующем состоянии:
-
все основные consumers текущей рыночной цены были переведены на
Quote; -
legacy-модели:
TickerPrice
MarketPriceSnapshot
были удалены;
-
legacy-методы получения и обновления price/snapshot были удалены;
-
полный regression suite успешно проходил:
606 passed
-
контрольный grep по основным legacy-контрактам был пустым;
-
при дополнительном анализе 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 полностью завершён.