build 043: finalize Quotes Feed architecture verification
This commit is contained in:
718
docs/migrations/build_039.md
Normal file
718
docs/migrations/build_039.md
Normal file
@@ -0,0 +1,718 @@
|
||||
# Build 039 — Завершение миграции consumers рыночной цены на каноническую модель Quote
|
||||
|
||||
**Статус:** Завершён
|
||||
**Проект:** Dzentra
|
||||
**Подсистема:** Market Data Acquisition / Exchange Integration / Trading Consumers
|
||||
**Тип изменения:** Миграция legacy price/snapshot consumers
|
||||
**Язык документа:** Русский
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build 039
|
||||
|
||||
Цель Build 039 — завершить миграцию существующих consumers рыночной цены и рыночных snapshot-моделей с legacy-контрактов на каноническую модель:
|
||||
|
||||
```text
|
||||
src.market_data.acquisition.models.quote.Quote
|
||||
```
|
||||
|
||||
и соответствующий новый pipeline получения, хранения и использования котировок.
|
||||
|
||||
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(
|
||||
```
|
||||
|
||||
При этом необходимо сохранить:
|
||||
|
||||
- работоспособность существующего торгового бота;
|
||||
- существующее пользовательское поведение;
|
||||
- существующие UI-контракты;
|
||||
- форматирование значений;
|
||||
- форматирование возраста данных;
|
||||
- форматирование времени;
|
||||
- fallback-поведение;
|
||||
- exception semantics;
|
||||
- существующую торговую логику, не относящуюся непосредственно к миграции котировок.
|
||||
|
||||
---
|
||||
|
||||
## 2. Исходное состояние
|
||||
|
||||
До Build 039 в проекте одновременно существовали:
|
||||
|
||||
1. новая каноническая модель котировки:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
2. новый Market Data Acquisition pipeline;
|
||||
|
||||
3. новый Quote Store;
|
||||
|
||||
4. новый cache-контракт на основе `Quote`;
|
||||
|
||||
5. legacy-модели:
|
||||
|
||||
```text
|
||||
TickerPrice
|
||||
MarketPriceSnapshot
|
||||
```
|
||||
|
||||
6. legacy-методы получения цены и snapshot:
|
||||
|
||||
```text
|
||||
get_price()
|
||||
get_market_snapshot()
|
||||
get_fresh_market_snapshot()
|
||||
refresh_price_cache()
|
||||
refresh_market_snapshot_cache()
|
||||
_get_real_price()
|
||||
```
|
||||
|
||||
7. consumers, которые продолжали зависеть от старых контрактов.
|
||||
|
||||
Такое состояние создавало несколько параллельных представлений одной и той же рыночной информации и препятствовало завершению миграции Market Data Acquisition.
|
||||
|
||||
---
|
||||
|
||||
## 3. Архитектурный принцип Build 039
|
||||
|
||||
После Build 039 каноническим представлением текущей рыночной котировки является:
|
||||
|
||||
```python
|
||||
Quote
|
||||
```
|
||||
|
||||
из:
|
||||
|
||||
```text
|
||||
src.market_data.acquisition.models.quote
|
||||
```
|
||||
|
||||
Архитектурный поток:
|
||||
|
||||
```text
|
||||
Dzengi API / Stream
|
||||
↓
|
||||
Market Data Acquisition
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
Quote Store / Market Price Cache
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
Trading / Telegram / Diagnostics consumers
|
||||
```
|
||||
|
||||
Legacy-модели `TickerPrice` и `MarketPriceSnapshot` больше не должны использоваться в `src` и `tests`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Каноническая модель Quote
|
||||
|
||||
Каноническая модель расположена в:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/models/quote.py
|
||||
```
|
||||
|
||||
Она является единым представлением текущей котировки инструмента и содержит данные, необходимые downstream-consumers:
|
||||
|
||||
```text
|
||||
symbol
|
||||
last_price
|
||||
bid_price
|
||||
ask_price
|
||||
exchange_timestamp
|
||||
received_at
|
||||
source
|
||||
```
|
||||
|
||||
Использование нескольких параллельных price/snapshot-моделей для одной и той же задачи после Build 039 не допускается.
|
||||
|
||||
---
|
||||
|
||||
## 5. Изменённые области проекта
|
||||
|
||||
В рамках Build 039 были затронуты следующие основные области.
|
||||
|
||||
### 5.1. Exchange Integration
|
||||
|
||||
Основные файлы:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/market_cache.py
|
||||
app/src/integrations/exchange/market_data_runner.py
|
||||
app/src/integrations/exchange/market_stream.py
|
||||
app/src/integrations/exchange/mock_data.py
|
||||
app/src/integrations/exchange/models.py
|
||||
app/src/integrations/exchange/service.py
|
||||
app/src/integrations/exchange/status.py
|
||||
```
|
||||
|
||||
Основные изменения:
|
||||
|
||||
- переход cache на каноническую модель `Quote`;
|
||||
- переход market data runner на `Quote`;
|
||||
- переход market stream на `Quote`;
|
||||
- переход mock-котировок на `Quote`;
|
||||
- удаление legacy price/snapshot-моделей;
|
||||
- перевод `ExchangeService` на канонические методы получения котировок;
|
||||
- сохранение runtime-status поведения;
|
||||
- сохранение legacy dict-контракта там, где он ещё необходим существующему UI.
|
||||
|
||||
---
|
||||
|
||||
### 5.2. Storage
|
||||
|
||||
Основные файлы:
|
||||
|
||||
```text
|
||||
app/src/storage/quote_store.py
|
||||
```
|
||||
|
||||
Quote Store используется как канонический storage-контракт для текущих котировок.
|
||||
|
||||
Store работает с моделью:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
и не должен зависеть от:
|
||||
|
||||
```text
|
||||
TickerPrice
|
||||
MarketPriceSnapshot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.3. Telegram UI
|
||||
|
||||
Основные файлы:
|
||||
|
||||
```text
|
||||
app/src/telegram/handlers/auto/ui.py
|
||||
app/src/telegram/handlers/debug_auto/ui.py
|
||||
app/src/telegram/ui/currency_ui.py
|
||||
```
|
||||
|
||||
Consumers пользовательского интерфейса переведены на новые quote-контракты.
|
||||
|
||||
Критически важное требование:
|
||||
|
||||
> Миграция внутренней модели данных не должна сама по себе изменять пользовательское представление данных.
|
||||
|
||||
Поэтому при миграции должны сохраняться существующие форматы:
|
||||
|
||||
- цены;
|
||||
- возраста данных;
|
||||
- времени обновления;
|
||||
- UI-строк;
|
||||
- fallback-значений.
|
||||
|
||||
---
|
||||
|
||||
### 5.4. Trading Auto
|
||||
|
||||
Основные файлы:
|
||||
|
||||
```text
|
||||
app/src/trading/auto/execution_quality.py
|
||||
app/src/trading/auto/signal_runtime.py
|
||||
```
|
||||
|
||||
Торговые consumers переведены с legacy market snapshot API на канонические quote-контракты.
|
||||
|
||||
Торговая логика, не относящаяся непосредственно к источнику котировки, не должна изменяться в рамках Build 039.
|
||||
|
||||
---
|
||||
|
||||
### 5.5. Trading Debug
|
||||
|
||||
Основной файл:
|
||||
|
||||
```text
|
||||
app/src/trading/debug/execution.py
|
||||
```
|
||||
|
||||
Debug execution переведён на новый quote-контракт.
|
||||
|
||||
Legacy-вызов:
|
||||
|
||||
```text
|
||||
get_fresh_market_snapshot()
|
||||
```
|
||||
|
||||
удалён.
|
||||
|
||||
---
|
||||
|
||||
### 5.6. Trading Diagnostics
|
||||
|
||||
Основной файл:
|
||||
|
||||
```text
|
||||
app/src/trading/diagnostics/snapshot.py
|
||||
```
|
||||
|
||||
Diagnostics consumer переведён на актуальное представление рыночной котировки.
|
||||
|
||||
---
|
||||
|
||||
### 5.7. Trading Strategies
|
||||
|
||||
Основные файлы:
|
||||
|
||||
```text
|
||||
app/src/trading/strategies/scalp.py
|
||||
app/src/trading/strategies/trend.py
|
||||
```
|
||||
|
||||
Стратегии переведены с legacy snapshot API на каноническую модель `Quote`.
|
||||
|
||||
После миграции стратегии не должны использовать:
|
||||
|
||||
```text
|
||||
get_market_snapshot()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. ExecutionPriceSnapshot
|
||||
|
||||
В рамках Build 039 необходимо различать две сущности:
|
||||
|
||||
```text
|
||||
Quote
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
`Quote` является канонической моделью текущей рыночной котировки.
|
||||
|
||||
`ExecutionPriceSnapshot` является специализированным downstream-контрактом execution layer и может существовать отдельно, поскольку представляет данные в форме, непосредственно необходимой исполнению торговых операций.
|
||||
|
||||
Таким образом:
|
||||
|
||||
```text
|
||||
Quote
|
||||
↓
|
||||
ExecutionPriceSnapshot
|
||||
```
|
||||
|
||||
не является дублированием legacy-модели `MarketPriceSnapshot`.
|
||||
|
||||
`ExecutionPriceSnapshot` сохраняется как специализированная execution-модель.
|
||||
|
||||
---
|
||||
|
||||
## 7. Runtime status и stale market data
|
||||
|
||||
Во время Build 039 особое внимание было уделено методу:
|
||||
|
||||
```python
|
||||
ExchangeService.get_symbol_runtime_status()
|
||||
```
|
||||
|
||||
Для открытого рынка выполняется проверка актуальности рыночной котировки.
|
||||
|
||||
Актуальный поток:
|
||||
|
||||
```text
|
||||
validate_symbol()
|
||||
↓
|
||||
build_market_status_from_symbol_status()
|
||||
↓
|
||||
если рынок открыт
|
||||
↓
|
||||
_get_fresh_quote()
|
||||
↓
|
||||
exchange_timestamp
|
||||
↓
|
||||
_exchange_timestamp_age_seconds()
|
||||
↓
|
||||
если возраст > 60 секунд
|
||||
↓
|
||||
build_market_stale_status()
|
||||
```
|
||||
|
||||
Порог stale market data:
|
||||
|
||||
```text
|
||||
age_seconds > 60
|
||||
```
|
||||
|
||||
При возрасте ровно:
|
||||
|
||||
```text
|
||||
60.0
|
||||
```
|
||||
|
||||
рынок остаётся в статусе `OPEN`.
|
||||
|
||||
При возрасте:
|
||||
|
||||
```text
|
||||
61.0
|
||||
```
|
||||
|
||||
возвращается:
|
||||
|
||||
```text
|
||||
ExchangeStatusCode.BREAK
|
||||
reason = "market_data_stale"
|
||||
raw_status = "STALE_MARKET_DATA"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Формат времени stale market data
|
||||
|
||||
Для stale market status используется exchange timestamp котировки.
|
||||
|
||||
Преобразование выполняется следующим образом:
|
||||
|
||||
```python
|
||||
exchange_timestamp_ms = (
|
||||
int(quote.exchange_timestamp.timestamp() * 1000)
|
||||
if quote.exchange_timestamp is not None
|
||||
else None
|
||||
)
|
||||
```
|
||||
|
||||
После этого timestamp передаётся в:
|
||||
|
||||
```python
|
||||
self._format_exchange_time(exchange_timestamp_ms)
|
||||
```
|
||||
|
||||
Это необходимо для сохранения существующего формата пользовательского сообщения.
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
10.07.2026 12:00:00
|
||||
```
|
||||
|
||||
Недопустимо передавать объект `datetime` непосредственно в функцию, ожидающую числовой timestamp в миллисекундах.
|
||||
|
||||
---
|
||||
|
||||
## 9. Сохранение legacy-поведения форматирования
|
||||
|
||||
Во время реализации Build 039 было выявлено критически важное правило миграции.
|
||||
|
||||
Две функции, решающие похожую задачу, не являются автоматически эквивалентными.
|
||||
|
||||
Например:
|
||||
|
||||
```python
|
||||
def _format_age(value: object) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
|
||||
try:
|
||||
age = max(0.0, float(value))
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
|
||||
if age < 1:
|
||||
return f"{age:.2f}с"
|
||||
|
||||
if age < 10:
|
||||
return f"{age:.1f}с"
|
||||
|
||||
total_seconds = int(age)
|
||||
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours}ч {minutes:02d}м"
|
||||
|
||||
if minutes > 0:
|
||||
return f"{minutes}м {seconds:02d}с"
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```python
|
||||
def _format_age(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
|
||||
try:
|
||||
age = max(0.0, float(value))
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
|
||||
if age < 1:
|
||||
return "< 1 сек."
|
||||
|
||||
return f"{age:.1f} сек."
|
||||
```
|
||||
|
||||
неэквивалентны.
|
||||
|
||||
Они различаются по:
|
||||
|
||||
- формату значений меньше секунды;
|
||||
- единицам измерения;
|
||||
- точности;
|
||||
- представлению минут;
|
||||
- представлению часов.
|
||||
|
||||
Следовательно, при миграции consumer нельзя заменять старую функцию новой только на основании сходства назначения.
|
||||
|
||||
Необходимо сравнивать фактические контракты поведения.
|
||||
|
||||
---
|
||||
|
||||
## 10. Тесты Build 039
|
||||
|
||||
В рамках Build 039 использовались и актуализировались следующие основные тесты:
|
||||
|
||||
```text
|
||||
app/tests/unit/integrations/exchange/test_market_cache.py
|
||||
app/tests/unit/integrations/exchange/test_market_data_runner.py
|
||||
app/tests/unit/integrations/exchange/test_market_stream.py
|
||||
app/tests/unit/integrations/exchange/test_service_execution_quote.py
|
||||
app/tests/unit/integrations/exchange/test_service_quote.py
|
||||
app/tests/unit/integrations/exchange/test_service_symbol_runtime_status.py
|
||||
app/tests/unit/storage/test_quote_store.py
|
||||
app/tests/unit/trading/auto/test_execution_quality.py
|
||||
app/tests/unit/trading/auto/test_signal_runtime_quote.py
|
||||
app/tests/unit/trading/debug/test_execution.py
|
||||
app/tests/unit/trading/strategies/test_scalp_quote.py
|
||||
app/tests/unit/trading/strategies/test_trend_quote.py
|
||||
```
|
||||
|
||||
Тесты должны проверять не только отсутствие ошибок выполнения, но и отсутствие возврата к legacy API.
|
||||
|
||||
---
|
||||
|
||||
## 11. Исправление legacy-guard тестов
|
||||
|
||||
После переключения production consumers некоторые тесты всё ещё содержали искусственные методы-заглушки вида:
|
||||
|
||||
```python
|
||||
def get_market_snapshot(self, *_: object, **__: object) -> object:
|
||||
raise AssertionError("legacy get_market_snapshot() must not be used")
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```python
|
||||
def get_fresh_market_snapshot(self, *_: object, **__: object) -> object:
|
||||
raise AssertionError("legacy get_fresh_market_snapshot() must not be used")
|
||||
```
|
||||
|
||||
После завершения миграции эти заглушки были удалены из тестов, поскольку финальный архитектурный grep должен подтверждать полное отсутствие legacy API во всём дереве:
|
||||
|
||||
```text
|
||||
src
|
||||
tests
|
||||
```
|
||||
|
||||
Это касается файлов:
|
||||
|
||||
```text
|
||||
tests/unit/trading/auto/test_execution_quality.py
|
||||
tests/unit/trading/auto/test_signal_runtime_quote.py
|
||||
tests/unit/trading/strategies/test_trend_quote.py
|
||||
tests/unit/trading/strategies/test_scalp_quote.py
|
||||
tests/unit/trading/debug/test_execution.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Проверка синтаксиса
|
||||
|
||||
В ходе реализации выполнялась проверка изменённых Python-файлов:
|
||||
|
||||
```bash
|
||||
python -m py_compile \
|
||||
src/integrations/exchange/service.py \
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py
|
||||
```
|
||||
|
||||
Проверка завершилась успешно.
|
||||
|
||||
---
|
||||
|
||||
## 13. Целевая проверка runtime status
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest \
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
|
||||
-q
|
||||
```
|
||||
|
||||
Финальный результат:
|
||||
|
||||
```text
|
||||
34 passed in 1.44s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Полный regression suite
|
||||
|
||||
После завершения всех изменений выполнен полный набор тестов:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Финальный результат:
|
||||
|
||||
```text
|
||||
606 passed in 1.63s
|
||||
```
|
||||
|
||||
Все тесты успешно пройдены.
|
||||
|
||||
---
|
||||
|
||||
## 15. Финальный архитектурный grep
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "TickerPrice|MarketPriceSnapshot|get_price\(|get_market_snapshot\(|get_fresh_market_snapshot\(|refresh_price_cache\(|refresh_market_snapshot_cache\(|_get_real_price\(" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Финальный результат:
|
||||
|
||||
```text
|
||||
<пусто>
|
||||
```
|
||||
|
||||
Это подтверждает отсутствие legacy price/snapshot API в:
|
||||
|
||||
```text
|
||||
src
|
||||
tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Критерии завершения Build 039
|
||||
|
||||
Build 039 считается завершённым, поскольку выполнены все критерии:
|
||||
|
||||
- [x] `TickerPrice` удалён из `src` и `tests`;
|
||||
- [x] `MarketPriceSnapshot` удалён из `src` и `tests`;
|
||||
- [x] `get_price()` удалён из `src` и `tests`;
|
||||
- [x] `get_market_snapshot()` удалён из `src` и `tests`;
|
||||
- [x] `get_fresh_market_snapshot()` удалён из `src` и `tests`;
|
||||
- [x] `refresh_price_cache()` удалён из `src` и `tests`;
|
||||
- [x] `refresh_market_snapshot_cache()` удалён из `src` и `tests`;
|
||||
- [x] `_get_real_price()` удалён из `src` и `tests`;
|
||||
- [x] trading consumers используют новый quote-контракт;
|
||||
- [x] Telegram consumers используют новый quote-контракт;
|
||||
- [x] diagnostics consumers используют новый quote-контракт;
|
||||
- [x] стратегии используют новый quote-контракт;
|
||||
- [x] runtime status использует каноническую модель `Quote`;
|
||||
- [x] stale market data detection сохранена;
|
||||
- [x] формат exchange timestamp восстановлен;
|
||||
- [x] legacy UI-поведение сохранено;
|
||||
- [x] целевые тесты проходят;
|
||||
- [x] полный regression suite проходит;
|
||||
- [x] финальный архитектурный grep пуст.
|
||||
|
||||
---
|
||||
|
||||
## 17. Итоговое состояние после Build 039
|
||||
|
||||
После завершения Build 039 архитектура текущих котировок приведена к следующему состоянию:
|
||||
|
||||
```text
|
||||
Exchange / Market Data Sources
|
||||
↓
|
||||
Market Data Acquisition
|
||||
↓
|
||||
Canonical Quote
|
||||
↓
|
||||
Quote Store / Market Price Cache
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Trading Auto │
|
||||
│ Trading Strategies │
|
||||
│ Trading Debug │
|
||||
│ Trading Diagnostics │
|
||||
│ Telegram UI │
|
||||
│ Execution │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Legacy price/snapshot API полностью удалён из production-кода и тестов.
|
||||
|
||||
Build 039 завершает миграцию consumers текущей рыночной цены на каноническую модель `Quote`.
|
||||
|
||||
---
|
||||
|
||||
## 18. Зафиксированный принцип для следующих Builds
|
||||
|
||||
Начиная с Build 039, при любой последующей миграции необходимо соблюдать следующее правило:
|
||||
|
||||
> Замена legacy-компонента новым каноническим компонентом не должна незаметно изменять существующее внешнее или внутреннее поведение consumers.
|
||||
|
||||
Перед заменой необходимо сравнивать:
|
||||
|
||||
```text
|
||||
тип входных данных
|
||||
тип возвращаемого значения
|
||||
формат результата
|
||||
точность
|
||||
единицы измерения
|
||||
граничные условия
|
||||
fallback-поведение
|
||||
exception semantics
|
||||
временные зоны
|
||||
формат времени
|
||||
UI-представление
|
||||
```
|
||||
|
||||
Любое намеренное изменение поведения должно быть явно зафиксировано в документации соответствующего Build и покрыто тестами.
|
||||
|
||||
---
|
||||
|
||||
## 19. Статус Build
|
||||
|
||||
```text
|
||||
Build 039: COMPLETED
|
||||
Regression suite: 606 passed
|
||||
Legacy grep: empty
|
||||
Canonical quote model: Quote
|
||||
```
|
||||
|
||||
Build 039 полностью завершён.
|
||||
Reference in New Issue
Block a user