build 043: finalize Quotes Feed architecture verification

This commit is contained in:
2026-07-14 22:16:39 +03:00
parent 225f07bc4b
commit a6325cf4cf
5 changed files with 2448 additions and 0 deletions

View 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 полностью завершён.