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