Files
dzentra_bot/docs/migrations/build_040.md

854 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 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-сущности и методы:
```text
TickerPrice
MarketPriceSnapshot
get_price()
get_market_snapshot()
get_fresh_market_snapshot()
refresh_price_cache()
refresh_market_snapshot_cache()
_get_real_price()
```
После завершения миграции был обнаружен один оставшийся активный consumer удалённого метода:
```text
src/integrations/exchange/market_data_runner.py
```
который продолжал вызывать:
```python
ExchangeService().refresh_market_snapshot_cache
```
Таким образом, целью Build 040 являлось не изменение runtime-архитектуры и не новый рефакторинг, а минимальное восстановление разорванного контракта на основе канонической модели:
```text
src.market_data.acquisition.models.quote.Quote
```
При этом необходимо было сохранить существующую логику работы старого бота без изменений.
---
## 2. Исходное состояние
После Build 039 проект находился в следующем состоянии:
1. все основные consumers текущей рыночной цены были переведены на `Quote`;
2. legacy-модели:
```text
TickerPrice
MarketPriceSnapshot
```
были удалены;
3. legacy-методы получения и обновления price/snapshot были удалены;
4. полный regression suite успешно проходил:
```text
606 passed
```
5. контрольный grep по основным legacy-контрактам был пустым;
6. при дополнительном анализе runtime-компонентов был обнаружен оставшийся вызов:
```python
ExchangeService().refresh_market_snapshot_cache
```
в:
```text
app/src/integrations/exchange/market_data_runner.py
```
Метод уже отсутствовал в `ExchangeService`, поэтому существовал разорванный runtime-контракт, который не проявлялся в предыдущем полном наборе тестов.
---
## 3. Архитектурный принцип Build 040
Build 040 сохраняет утверждённый архитектурный поток:
```text
Exchange / Market Data Sources
Market Data Acquisition
Canonical Quote
Quote Store / Market Price Cache
ExchangeService
MarketDataRunner / Trading Consumers
```
Канонической моделью текущей котировки остаётся:
```python
Quote
```
из:
```text
src.market_data.acquisition.models.quote
```
Build 040 не вводит новую параллельную price/snapshot-модель и не восстанавливает удалённые legacy-контракты.
Вместо этого добавляется минимальный публичный compatibility-метод:
```python
refresh_quote_cache()
```
который использует существующий канонический acquisition pipeline.
---
## 4. Главное требование сохранения поведения
Основным требованием Build 040 являлось:
> Не менять логику работы текущих файлов и существующего runtime.
Поэтому перед реализацией была восстановлена фактическая старая реализация удалённого метода:
```python
refresh_market_snapshot_cache()
```
из истории Git.
Это позволило определить реальный контракт старого метода без предположений.
Старая реализация выполняла:
```text
нормализация runtime_key
принудительное получение свежего REST snapshot
проверка ценовых полей
запись результата в MarketPriceCache
возврат свежего snapshot
```
Критически важно:
- cache перед REST-запросом не читался;
- всегда выполнялось принудительное получение свежих данных;
- результат сохранялся в cache соответствующего runtime;
- возвращался свежий результат;
- ошибки не подавлялись самим refresh-методом;
- mock-режим сохранялся.
---
## 5. Восстановление старого контракта через Git
Для определения фактического поведения удалённого метода была использована история Git.
Команда:
```bash
git log -S"def refresh_market_snapshot_cache" \
--oneline \
-- src/integrations/exchange/service.py
```
Результат:
```text
a996f2f feat: add market data architecture and complete migration through build 039
e97dcd3 07.4.3.16 — Production Execution Pricing Layer
```
Старая реализация была извлечена командой:
```bash
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
Существующий метод:
```python
ExchangeService.get_quote()
```
реализует cache-first поведение:
```text
валидация символа
чтение MarketPriceCache
если Quote свежий
возврат cached Quote
иначе REST acquisition
```
Такое поведение корректно для обычных consumers, но не подходит для:
```python
MarketDataRunner._rest_fallback_once()
```
После отказа WebSocket runner должен проверить именно доступность REST-механизма.
Если использовать `get_quote()`, возможна ситуация:
```text
WebSocket отключился
в cache остаётся свежий WebSocket Quote
get_quote() возвращает cached Quote
REST-запрос фактически не выполняется
runner ошибочно устанавливает REST state = AVAILABLE
```
Поэтому Build 040 вводит отдельный принудительный refresh-контракт.
---
## 7. Новый публичный контракт refresh_quote_cache()
В `ExchangeService` добавлен метод:
```python
def refresh_quote_cache(
self,
symbol: str | None = None,
*,
runtime_key: str | None = None,
) -> Quote:
```
Его ответственность:
```text
нормализовать runtime_key
определить symbol
если exchange disabled
получить mock Quote
иначе
валидировать symbol
принудительно получить свежий Quote через REST acquisition
сохранить тот же Quote в MarketPriceCache
вернуть тот же Quote
```
Метод не читает cache перед получением свежей котировки.
---
## 8. Сохранение mock-поведения
Старый метод:
```python
refresh_market_snapshot_cache()
```
через:
```python
get_fresh_market_snapshot()
```
поддерживал режим:
```text
exchange_enabled = False
```
В этом режиме создавались mock-данные, которые затем сохранялись в cache.
Новый метод сохраняет эквивалентное поведение через:
```python
mock_quote(symbol_to_use)
```
При выключенной бирже:
- REST acquisition не выполняется;
- validation через exchange reference data не требуется;
- создаётся канонический `Quote`;
- тот же объект сохраняется в `MarketPriceCache`;
- тот же объект возвращается consumer.
---
## 9. Принудительное получение свежего Quote
В real-режиме новый метод использует:
```python
self._get_fresh_quote(
validation.normalized_symbol,
)
```
Этот метод вызывает существующий канонический pipeline:
```text
DzengiQuoteDocumentSource
DzengiQuoteDocumentHandler
QuotesFeed
QuoteFeedRegistry
QuoteAcquisitionService
Quote
```
Таким образом, Build 040 не добавляет новый способ получения котировок и не дублирует Market Data Acquisition.
---
## 10. Почему QuoteAcquisitionService не изменялся
В ходе анализа было подтверждено, что:
```python
QuoteAcquisitionService
```
намеренно отвечает только за получение канонической модели через зарегистрированный feed.
Он не должен выполнять:
- runtime isolation;
- cache management;
- fallback orchestration;
- validation торгового runtime;
- выбор `runtime_key`;
- compatibility-поведение старого бота.
Поэтому Build 040 не изменяет:
```text
app/src/market_data/acquisition/service.py
app/src/market_data/acquisition/feeds/quotes_feed.py
```
Compatibility и runtime semantics остаются в:
```text
ExchangeService
```
---
## 11. Изменение MarketDataRunner
В:
```text
app/src/integrations/exchange/market_data_runner.py
```
был изменён только один runtime-вызов.
До Build 040:
```python
ExchangeService().refresh_market_snapshot_cache
```
После Build 040:
```python
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-контекстов через:
```python
runtime_key
```
Перед использованием ключ нормализуется существующим методом:
```python
self._runtime_key(runtime_key)
```
Котировка сохраняется через:
```python
MarketPriceCache.set_quote(
quote,
runtime_key=normalized_runtime_key,
)
```
Это сохраняет независимость runtime-контекстов, включая:
```text
auto
debug_auto
default
```
и другие существующие значения, если они используются текущим runtime.
---
## 13. Сохранение identity канонического Quote
Каноническая модель:
```python
Quote
```
является immutable-моделью:
```python
@dataclass(frozen=True, slots=True)
```
Build 040 не создаёт копию полученного объекта.
Последовательность:
```text
fresh Quote
MarketPriceCache.set_quote(quote)
return quote
```
В cache сохраняется тот же экземпляр, который возвращается из метода.
Это зафиксировано тестами.
---
## 14. Изменённые production-файлы
В рамках Build 040 изменены только два production-файла:
```text
app/src/integrations/exchange/service.py
app/src/integrations/exchange/market_data_runner.py
```
### 14.1. service.py
Добавлен новый публичный метод:
```python
refresh_quote_cache()
```
Существующие методы не подвергались сопутствующему рефакторингу.
В частности, не изменялись:
```text
get_quote()
get_execution_snapshot()
_get_fresh_quote()
_load_quote_via_acquisition()
```
### 14.2. market_data_runner.py
Изменена только ссылка на удалённый legacy refresh-метод.
Остальная логика runner сохранена.
---
## 15. Изменённые тестовые файлы
В рамках Build 040 изменены:
```text
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
Тестами зафиксировано поведение:
```python
MarketDataRunner._rest_fallback_once()
```
При успешном refresh:
```text
refresh_quote_cache() вызывается
передаётся текущий symbol
передаётся context.runtime_key
last_rest_state = AVAILABLE
last_rest_error_key = None
```
При ошибке сохраняется существующее поведение runner:
```text
last_rest_state = UNAVAILABLE
```
и существующая обработка ошибки не изменяется.
---
## 18. Целевая проверка Build 040
Выполнена команда:
```bash
python -m pytest -q \
tests/unit/integrations/exchange/test_service_quote.py \
tests/unit/integrations/exchange/test_market_data_runner.py
```
Финальный результат:
```text
18 passed in 0.11s
```
Все целевые тесты Build 040 успешно пройдены.
---
## 19. Связанные регрессионные тесты
Выполнена команда:
```bash
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
```
Финальный результат:
```text
107 passed in 1.81s
```
Связанные компоненты работают без регрессий.
---
## 20. Полный regression suite
После завершения Build 040 выполнен полный набор тестов:
```bash
python -m pytest -q
```
Финальный результат:
```text
614 passed in 2.03s
```
Все тесты успешно пройдены.
По сравнению с Build 039 количество тестов увеличилось:
```text
606 → 614
```
Добавлено:
```text
8 тестов
```
---
## 21. Финальный архитектурный grep
Выполнена команда:
```bash
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "refresh_market_snapshot_cache|refresh_price_cache|MarketPriceSnapshot|TickerPrice" \
src tests
```
Финальный результат:
```text
<пусто>
```
Это подтверждает отсутствие следующих legacy-контрактов:
```text
refresh_market_snapshot_cache
refresh_price_cache
MarketPriceSnapshot
TickerPrice
```
в:
```text
src
tests
```
---
## 22. Что намеренно не изменялось
В Build 040 намеренно не изменялись:
```text
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 считается завершённым, поскольку выполнены все критерии:
- [x] найден оставшийся активный вызов удалённого `refresh_market_snapshot_cache`;
- [x] фактический старый контракт восстановлен из Git;
- [x] новый контракт реализован на канонической модели `Quote`;
- [x] добавлен `refresh_quote_cache()`;
- [x] новый метод не использует cache-first поведение;
- [x] fresh Quote принудительно получается через существующий acquisition pipeline;
- [x] runtime isolation сохранена;
- [x] mock-поведение сохранено;
- [x] exception semantics сохранена;
- [x] тот же экземпляр `Quote` сохраняется и возвращается;
- [x] `MarketDataRunner` переключён на новый метод;
- [x] WebSocket-first логика не изменена;
- [x] lifecycle runner не изменён;
- [x] целевые тесты проходят;
- [x] связанные регрессионные тесты проходят;
- [x] полный regression suite проходит;
- [x] финальный legacy grep пуст.
---
## 24. Итоговое состояние после Build 040
После завершения Build 040 runtime-поток получения котировки имеет следующий вид:
```text
WebSocket available
DzengiWebSocketQuoteAdapter
Quote
MarketPriceCache
```
При отказе WebSocket:
```text
WebSocket unavailable
MarketDataRunner._rest_fallback_once()
ExchangeService.refresh_quote_cache()
Quote Acquisition Pipeline
Fresh REST Quote
MarketPriceCache
REST state = AVAILABLE
```
При ошибке REST refresh:
```text
REST acquisition error
ExchangeError
MarketDataRunner existing error handling
REST state = UNAVAILABLE
```
Таким образом, Build 040 восстанавливает разорванный fallback-контракт, не изменяя существующую runtime-логику.
---
## 25. Зафиксированный принцип для следующих Builds
Build 040 подтверждает принцип, зафиксированный после Build 039:
> При миграции нельзя считать новую функцию эквивалентной старой только потому, что они решают похожую задачу.
Перед заменой legacy-компонента необходимо восстановить и сравнить фактическое поведение:
```text
порядок операций
чтение cache
запись cache
принудительность refresh
тип возвращаемого значения
identity результата
runtime isolation
mock-поведение
fallback-поведение
exception semantics
side effects
граничные условия
```
Если старый контракт доступен в истории Git, его необходимо использовать как источник фактического поведения вместо предположений.
---
## 26. Статус Build
```text
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 полностью завершён.