build 039: complete Quotes Feed migration foundation
This commit is contained in:
739
docs/migrations/build_036.md
Normal file
739
docs/migrations/build_036.md
Normal file
@@ -0,0 +1,739 @@
|
||||
# Build 036 — Перевод read-only и UI-потребителей
|
||||
|
||||
**Статус:** Завершён
|
||||
**Результат:** Успешно
|
||||
**Полная регрессия:** `614 passed`
|
||||
|
||||
---
|
||||
|
||||
## 1. Назначение Build
|
||||
|
||||
Цель Build 036 — перевести read-only и UI-потребителей рыночной котировки с legacy-представлений:
|
||||
|
||||
- `TickerPrice`;
|
||||
- `dict[str, object]` из `get_market_snapshot()`;
|
||||
|
||||
на каноническую внутреннюю модель:
|
||||
|
||||
`Quote`
|
||||
|
||||
Build продолжает миграцию подсистемы рыночных данных на целевую архитектуру:
|
||||
|
||||
Market Data
|
||||
↓
|
||||
Market Intelligence
|
||||
↓
|
||||
Decision
|
||||
↓
|
||||
Execution
|
||||
↓
|
||||
Exchange
|
||||
|
||||
В рамках Build 036 изменяется исключительно read-only контур.
|
||||
|
||||
Execution pricing, торговые стратегии, signal runtime и execution quality не переводятся и не изменяются.
|
||||
|
||||
---
|
||||
|
||||
## 2. Исходное состояние
|
||||
|
||||
До Build 036 read-only и UI-потребители получали текущую рыночную котировку через несколько legacy-интерфейсов.
|
||||
|
||||
Основные варианты:
|
||||
|
||||
UI / diagnostics
|
||||
↓
|
||||
ExchangeService.get_price()
|
||||
↓
|
||||
TickerPrice
|
||||
|
||||
или:
|
||||
|
||||
UI / diagnostics
|
||||
↓
|
||||
ExchangeService.get_market_snapshot()
|
||||
↓
|
||||
dict[str, object]
|
||||
|
||||
При этом после предыдущих Build каноническая модель `Quote` уже существовала и использовалась внутри новой инфраструктуры:
|
||||
|
||||
REST Quotes Feed
|
||||
↓
|
||||
Quote
|
||||
|
||||
WebSocket quote adapter
|
||||
↓
|
||||
Quote
|
||||
|
||||
MarketPriceCache
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
Quote
|
||||
|
||||
Таким образом, read-only потребители продолжали зависеть от compatibility-представлений, несмотря на наличие канонической модели котировки.
|
||||
|
||||
---
|
||||
|
||||
## 3. Целевое состояние
|
||||
|
||||
После Build 036 read-only и UI-потребители получают канонический объект:
|
||||
|
||||
`Quote`
|
||||
|
||||
через публичный facade:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
Целевая цепочка чтения:
|
||||
|
||||
read-only / UI consumer
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
MarketPriceCache.get_quote()
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
При отсутствии свежей котировки используется REST fallback:
|
||||
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
REST Quotes Feed
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
---
|
||||
|
||||
## 4. Архитектурный принцип
|
||||
|
||||
Read-only и UI-потребители не обращаются напрямую к:
|
||||
|
||||
`Quote Store`
|
||||
|
||||
Доступ выполняется через:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
Это позволяет сохранить единый facade, отвечающий за:
|
||||
|
||||
- выбор default symbol;
|
||||
- нормализацию и валидацию символа;
|
||||
- нормализацию `runtime_key`;
|
||||
- поддержку mock mode;
|
||||
- чтение канонической котировки из cache/store;
|
||||
- проверку свежести;
|
||||
- REST fallback;
|
||||
- преобразование внутренних ошибок в `ExchangeError`.
|
||||
|
||||
Целевая граница:
|
||||
|
||||
UI / diagnostics
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
MarketPriceCache
|
||||
↓
|
||||
Quote Store
|
||||
|
||||
UI не должен самостоятельно знать:
|
||||
|
||||
- структуру ключей Quote Store;
|
||||
- `source_name`;
|
||||
- правила `runtime_key`;
|
||||
- freshness policy;
|
||||
- правила REST fallback;
|
||||
- внутреннюю обработку ошибок Acquisition Layer.
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменённые production-файлы
|
||||
|
||||
В рамках Build 036 изменены:
|
||||
|
||||
src/integrations/exchange/market_cache.py
|
||||
src/integrations/exchange/service.py
|
||||
|
||||
src/telegram/ui/currency_ui.py
|
||||
src/telegram/handlers/auto/ui.py
|
||||
src/telegram/handlers/debug_auto/ui.py
|
||||
|
||||
src/trading/diagnostics/snapshot.py
|
||||
|
||||
---
|
||||
|
||||
## 6. Изменённые и добавленные тесты
|
||||
|
||||
Изменены:
|
||||
|
||||
tests/unit/integrations/exchange/test_market_cache.py
|
||||
tests/unit/telegram/ui/test_currency_ui.py
|
||||
|
||||
Добавлен:
|
||||
|
||||
tests/unit/integrations/exchange/test_service_quote.py
|
||||
|
||||
После Build 036 общее количество тестов увеличилось:
|
||||
|
||||
после Build 035: 608 passed
|
||||
после Build 036: 614 passed
|
||||
|
||||
Добавлено:
|
||||
|
||||
6 тестов
|
||||
|
||||
---
|
||||
|
||||
## 7. Изменения в MarketPriceCache
|
||||
|
||||
Файл:
|
||||
|
||||
`src/integrations/exchange/market_cache.py`
|
||||
|
||||
Добавлен канонический read API:
|
||||
|
||||
`MarketPriceCache.get_quote()`
|
||||
|
||||
Его назначение — вернуть непосредственно канонический объект `Quote`, сохранённый в Quote Store.
|
||||
|
||||
Цепочка:
|
||||
|
||||
MarketPriceCache.get_quote()
|
||||
↓
|
||||
QuoteStore.get()
|
||||
↓
|
||||
Quote | None
|
||||
|
||||
Метод:
|
||||
|
||||
- нормализует `symbol`;
|
||||
- нормализует `runtime_key`;
|
||||
- читает котировку из собственного namespace Quote Store;
|
||||
- возвращает канонический `Quote`;
|
||||
- не создаёт `MarketPriceSnapshot`;
|
||||
- не выполняет преобразование `Decimal` в `float`;
|
||||
- сохраняет канонический объект котировки.
|
||||
|
||||
Legacy API:
|
||||
|
||||
`MarketPriceCache.get_price()`
|
||||
|
||||
сохранён для compatibility-потребителей, которые ещё не переведены на `Quote`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Изменения в ExchangeService
|
||||
|
||||
Файл:
|
||||
|
||||
`src/integrations/exchange/service.py`
|
||||
|
||||
Добавлен публичный канонический read API:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
Метод сохраняет обязанности facade и отвечает за:
|
||||
|
||||
1. выбор default symbol;
|
||||
2. поддержку mock mode;
|
||||
3. валидацию символа;
|
||||
4. нормализацию `runtime_key`;
|
||||
5. чтение канонической котировки из MarketPriceCache;
|
||||
6. проверку свежести;
|
||||
7. REST fallback при cache miss или stale quote;
|
||||
8. сохранение свежей котировки в Quote Store через MarketPriceCache;
|
||||
9. возврат канонического `Quote`.
|
||||
|
||||
Целевая цепочка:
|
||||
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
validate_symbol()
|
||||
↓
|
||||
MarketPriceCache.get_quote()
|
||||
↓
|
||||
freshness check
|
||||
↓
|
||||
Quote
|
||||
|
||||
При отсутствии свежей котировки:
|
||||
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
REST Quotes Feed
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
|
||||
---
|
||||
|
||||
## 9. Политика свежести
|
||||
|
||||
Для read-only и UI-потребителей сохранена существующая политика свежести рыночной котировки.
|
||||
|
||||
Возраст котировки определяется по:
|
||||
|
||||
`Quote.received_at`
|
||||
|
||||
Если сохранённая котировка достаточно свежая, возвращается существующий канонический объект.
|
||||
|
||||
Если котировка отсутствует или устарела, выполняется REST fallback через новый Quotes Feed.
|
||||
|
||||
Таким образом:
|
||||
|
||||
fresh cached Quote
|
||||
↓
|
||||
вернуть Quote без REST-запроса
|
||||
|
||||
stale cached Quote
|
||||
↓
|
||||
REST Quotes Feed
|
||||
↓
|
||||
сохранить новый Quote
|
||||
↓
|
||||
вернуть новый Quote
|
||||
|
||||
---
|
||||
|
||||
## 10. REST fallback
|
||||
|
||||
REST fallback использует новую каноническую цепочку Acquisition Layer:
|
||||
|
||||
Dzengi REST API
|
||||
↓
|
||||
GET /api/v1/ticker/24hr
|
||||
↓
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parsing
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapping
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
Полученный объект сохраняется:
|
||||
|
||||
Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
|
||||
Не используется лишний цикл преобразований:
|
||||
|
||||
Quote
|
||||
↓
|
||||
dict
|
||||
↓
|
||||
float
|
||||
↓
|
||||
новый Quote
|
||||
|
||||
Канонический объект остаётся `Quote` на всём новом пути.
|
||||
|
||||
---
|
||||
|
||||
## 11. Mock mode
|
||||
|
||||
`ExchangeService.get_quote()` сохраняет поддержку режима:
|
||||
|
||||
`exchange_enabled = False`
|
||||
|
||||
В этом режиме потребителю также возвращается канонический:
|
||||
|
||||
`Quote`
|
||||
|
||||
Mock-котировка содержит:
|
||||
|
||||
- `symbol`;
|
||||
- `last_price`;
|
||||
- `bid_price`;
|
||||
- `ask_price`;
|
||||
- `exchange_timestamp`;
|
||||
- `received_at`;
|
||||
- `source`.
|
||||
|
||||
Таким образом, потребители `get_quote()` не должны знать, работает приложение с реальной биржей или в mock mode.
|
||||
|
||||
---
|
||||
|
||||
## 12. Обработка ошибок
|
||||
|
||||
Новый canonical read API сохраняет существующую границу ошибок ExchangeService.
|
||||
|
||||
Ошибки Acquisition Layer:
|
||||
|
||||
- не передаются напрямую UI-потребителям;
|
||||
- логируются в контексте `ticker/24hr`;
|
||||
- преобразуются во внешний `ExchangeError`;
|
||||
- сохраняют исходную ошибку через `__cause__`.
|
||||
|
||||
Граница остаётся следующей:
|
||||
|
||||
Acquisition error
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
ExchangeError
|
||||
↓
|
||||
UI / diagnostics consumer
|
||||
|
||||
---
|
||||
|
||||
## 13. Перевод currency_ui.py
|
||||
|
||||
Файл:
|
||||
|
||||
`src/telegram/ui/currency_ui.py`
|
||||
|
||||
До Build 036 использовался legacy API:
|
||||
|
||||
`ExchangeService.get_price()`
|
||||
|
||||
Возвращаемая модель:
|
||||
|
||||
`TickerPrice`
|
||||
|
||||
Для расчёта использовалось:
|
||||
|
||||
`ticker.price`
|
||||
|
||||
После Build 036 используется:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
и каноническое поле:
|
||||
|
||||
`quote.last_price`
|
||||
|
||||
Целевая цепочка:
|
||||
|
||||
currency_ui
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
Quote.last_price
|
||||
|
||||
Локальная логика расчёта стоимости баланса, обработка `ExchangeError` и кэширование рассчитанных цен сохранены.
|
||||
|
||||
---
|
||||
|
||||
## 14. Перевод auto/ui.py
|
||||
|
||||
Файл:
|
||||
|
||||
`src/telegram/handlers/auto/ui.py`
|
||||
|
||||
До Build 036 UI получал legacy market snapshot:
|
||||
|
||||
`ExchangeService.get_market_snapshot()`
|
||||
|
||||
и работал с:
|
||||
|
||||
`dict[str, object]`
|
||||
|
||||
Основные поля:
|
||||
|
||||
- `last_price`;
|
||||
- `bid_price`;
|
||||
- `ask_price`.
|
||||
|
||||
После Build 036 UI получает:
|
||||
|
||||
`Quote`
|
||||
|
||||
через:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
Используются канонические поля:
|
||||
|
||||
- `quote.last_price`;
|
||||
- `quote.bid_price`;
|
||||
- `quote.ask_price`.
|
||||
|
||||
В результате UI больше не зависит от legacy market snapshot dict для получения текущей рыночной котировки.
|
||||
|
||||
---
|
||||
|
||||
## 15. Перевод debug_auto/ui.py
|
||||
|
||||
Файл:
|
||||
|
||||
`src/telegram/handlers/debug_auto/ui.py`
|
||||
|
||||
В debug UI разделены две разные сущности:
|
||||
|
||||
1. текущая рыночная котировка;
|
||||
2. execution snapshot.
|
||||
|
||||
После Build 036 market-секция использует:
|
||||
|
||||
`Quote`
|
||||
|
||||
и читает:
|
||||
|
||||
- `last_price`;
|
||||
- `bid_price`;
|
||||
- `ask_price`;
|
||||
- `source`;
|
||||
- `received_at`;
|
||||
- `exchange_timestamp`.
|
||||
|
||||
Execution-секция продолжает использовать:
|
||||
|
||||
`ExecutionPriceSnapshot`
|
||||
|
||||
Это намеренное разделение.
|
||||
|
||||
Build 036 не изменяет execution semantics.
|
||||
|
||||
Целевая схема:
|
||||
|
||||
Debug UI
|
||||
├── Market section
|
||||
│ ↓
|
||||
│ Quote
|
||||
│
|
||||
└── Execution section
|
||||
↓
|
||||
ExecutionPriceSnapshot
|
||||
|
||||
---
|
||||
|
||||
## 16. Перевод trading/diagnostics/snapshot.py
|
||||
|
||||
Файл:
|
||||
|
||||
`src/trading/diagnostics/snapshot.py`
|
||||
|
||||
До Build 036 диагностика использовала:
|
||||
|
||||
`ExchangeService.get_market_snapshot()`
|
||||
|
||||
После Build 036 используется:
|
||||
|
||||
`ExchangeService.get_quote()`
|
||||
|
||||
Для выбора диагностической цены сохраняется существующая семантика:
|
||||
|
||||
BUY
|
||||
↓
|
||||
quote.ask_price
|
||||
|
||||
SELL
|
||||
↓
|
||||
quote.bid_price
|
||||
|
||||
другое состояние
|
||||
↓
|
||||
quote.last_price
|
||||
|
||||
Build не изменяет торговые решения и используется только для read-only диагностики.
|
||||
|
||||
---
|
||||
|
||||
## 17. Сохранённые legacy API
|
||||
|
||||
Build 036 не удаляет:
|
||||
|
||||
- `ExchangeService.get_price()`;
|
||||
- `ExchangeService.get_market_snapshot()`;
|
||||
- `ExchangeService.get_execution_snapshot()`;
|
||||
- `ExchangeService.get_fresh_market_snapshot()`;
|
||||
- `MarketPriceCache.get_price()`.
|
||||
|
||||
Они сохраняются для ещё не переведённых compatibility-потребителей.
|
||||
|
||||
Удаление legacy API возможно только после полного перевода всех зависимых компонентов и отдельной проверки использования.
|
||||
|
||||
---
|
||||
|
||||
## 18. Что намеренно не изменялось
|
||||
|
||||
Build 036 не затрагивает:
|
||||
|
||||
src/trading/execution/pricing.py
|
||||
src/trading/auto/signal_runtime.py
|
||||
src/trading/auto/execution_quality.py
|
||||
src/trading/strategies/trend.py
|
||||
src/trading/strategies/scalp.py
|
||||
src/trading/debug/execution.py
|
||||
|
||||
Эти компоненты относятся к:
|
||||
|
||||
- execution pricing;
|
||||
- signal runtime;
|
||||
- execution quality;
|
||||
- strategy decisions;
|
||||
- debug execution semantics.
|
||||
|
||||
Их перевод должен выполняться отдельно.
|
||||
|
||||
---
|
||||
|
||||
## 19. Что не входит в Build 036
|
||||
|
||||
В рамках Build 036 не выполнялись:
|
||||
|
||||
- перевод execution pricing;
|
||||
- перевод signal runtime;
|
||||
- перевод execution quality;
|
||||
- перевод торговых стратегий;
|
||||
- удаление `TickerPrice`;
|
||||
- удаление `get_price()`;
|
||||
- удаление `get_market_snapshot()`;
|
||||
- удаление legacy market snapshot dict layer;
|
||||
- удаление `ExecutionPriceSnapshot`;
|
||||
- удаление `MarketPriceCache`;
|
||||
- удаление legacy parsing helpers.
|
||||
|
||||
---
|
||||
|
||||
## 20. Проверка синтаксиса
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
python -m py_compile \
|
||||
src/integrations/exchange/market_cache.py \
|
||||
src/integrations/exchange/service.py \
|
||||
src/telegram/ui/currency_ui.py \
|
||||
src/telegram/handlers/auto/ui.py \
|
||||
src/telegram/handlers/debug_auto/ui.py \
|
||||
src/trading/diagnostics/snapshot.py \
|
||||
tests/unit/integrations/exchange/test_market_cache.py \
|
||||
tests/unit/integrations/exchange/test_service_quote.py \
|
||||
tests/unit/telegram/ui/test_currency_ui.py
|
||||
|
||||
Результат:
|
||||
|
||||
`Успешно`
|
||||
|
||||
Ошибок синтаксиса не обнаружено.
|
||||
|
||||
---
|
||||
|
||||
## 21. Специализированные тесты
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_market_cache.py \
|
||||
tests/unit/integrations/exchange/test_service_quote.py \
|
||||
tests/unit/telegram/ui/test_currency_ui.py \
|
||||
-q
|
||||
|
||||
Результат:
|
||||
|
||||
`44 passed in 0.29s`
|
||||
|
||||
---
|
||||
|
||||
## 22. Полная регрессия
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
`python -m pytest -q`
|
||||
|
||||
Результат:
|
||||
|
||||
`614 passed in 0.27s`
|
||||
|
||||
Полная тестовая регрессия проекта успешно пройдена.
|
||||
|
||||
---
|
||||
|
||||
## 23. Итоговая архитектура после Build 036
|
||||
|
||||
После завершения Build 036 read-only и UI-контур использует следующую архитектуру:
|
||||
|
||||
┌──────────────────────────────┐
|
||||
│ currency_ui │
|
||||
│ auto UI │
|
||||
│ debug UI market section │
|
||||
│ trading diagnostics │
|
||||
└──────────────┬───────────────┘
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
MarketPriceCache.get_quote()
|
||||
↓
|
||||
Quote Store
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
При cache miss или stale quote:
|
||||
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
REST Quotes Feed
|
||||
↓
|
||||
canonical Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote()
|
||||
↓
|
||||
Quote Store
|
||||
|
||||
---
|
||||
|
||||
## 24. Результат Build
|
||||
|
||||
Build 036 успешно завершён.
|
||||
|
||||
Достигнуты следующие результаты:
|
||||
|
||||
- добавлен канонический read API `MarketPriceCache.get_quote()`;
|
||||
- добавлен публичный facade `ExchangeService.get_quote()`;
|
||||
- сохранены symbol validation, mock mode, freshness policy и REST fallback;
|
||||
- `currency_ui.py` переведён с `TickerPrice` на `Quote`;
|
||||
- `auto/ui.py` переведён с legacy market snapshot dict на `Quote`;
|
||||
- market-секция debug UI переведена на `Quote`;
|
||||
- execution-секция debug UI оставлена на `ExecutionPriceSnapshot`;
|
||||
- trading diagnostics переведена на `Quote`;
|
||||
- execution-контур не изменён;
|
||||
- legacy API сохранены для последующей миграции;
|
||||
- специализированные тесты успешно пройдены;
|
||||
- полная регрессия успешно пройдена.
|
||||
|
||||
Итог:
|
||||
|
||||
До Build 036:
|
||||
|
||||
UI / diagnostics
|
||||
↓
|
||||
TickerPrice / market snapshot dict
|
||||
|
||||
После Build 036:
|
||||
|
||||
UI / diagnostics
|
||||
↓
|
||||
ExchangeService.get_quote()
|
||||
↓
|
||||
canonical Quote
|
||||
|
||||
---
|
||||
|
||||
## 25. Следующий этап
|
||||
|
||||
Следующий этап миграции:
|
||||
|
||||
**Build 037 — Перевод execution-потребителей**
|
||||
|
||||
Его задача — отдельно проанализировать и перевести execution-sensitive потребителей канонической рыночной котировки без изменения торговой семантики и без преждевременного удаления legacy compatibility API.
|
||||
Reference in New Issue
Block a user