Files
dzentra_bot/docs/migrations/build_039.md

20 KiB
Raw Permalink Blame History

Build 039 — Завершение миграции consumers рыночной цены на каноническую модель Quote

Статус: Завершён
Проект: Dzentra
Подсистема: Market Data Acquisition / Exchange Integration / Trading Consumers
Тип изменения: Миграция legacy price/snapshot consumers
Язык документа: Русский


1. Цель Build 039

Цель Build 039 — завершить миграцию существующих consumers рыночной цены и рыночных snapshot-моделей с legacy-контрактов на каноническую модель:

src.market_data.acquisition.models.quote.Quote

и соответствующий новый pipeline получения, хранения и использования котировок.

Build 039 должен устранить использование следующих legacy-сущностей и методов:

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. новая каноническая модель котировки:
Quote
  1. новый Market Data Acquisition pipeline;

  2. новый Quote Store;

  3. новый cache-контракт на основе Quote;

  4. legacy-модели:

TickerPrice
MarketPriceSnapshot
  1. legacy-методы получения цены и snapshot:
get_price()
get_market_snapshot()
get_fresh_market_snapshot()
refresh_price_cache()
refresh_market_snapshot_cache()
_get_real_price()
  1. consumers, которые продолжали зависеть от старых контрактов.

Такое состояние создавало несколько параллельных представлений одной и той же рыночной информации и препятствовало завершению миграции Market Data Acquisition.


3. Архитектурный принцип Build 039

После Build 039 каноническим представлением текущей рыночной котировки является:

Quote

из:

src.market_data.acquisition.models.quote

Архитектурный поток:

Dzengi API / Stream
        ↓
Market Data Acquisition
        ↓
Quote
        ↓
Quote Store / Market Price Cache
        ↓
ExchangeService
        ↓
Trading / Telegram / Diagnostics consumers

Legacy-модели TickerPrice и MarketPriceSnapshot больше не должны использоваться в src и tests.


4. Каноническая модель Quote

Каноническая модель расположена в:

app/src/market_data/acquisition/models/quote.py

Она является единым представлением текущей котировки инструмента и содержит данные, необходимые downstream-consumers:

symbol
last_price
bid_price
ask_price
exchange_timestamp
received_at
source

Использование нескольких параллельных price/snapshot-моделей для одной и той же задачи после Build 039 не допускается.


5. Изменённые области проекта

В рамках Build 039 были затронуты следующие основные области.

5.1. Exchange Integration

Основные файлы:

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

Основные файлы:

app/src/storage/quote_store.py

Quote Store используется как канонический storage-контракт для текущих котировок.

Store работает с моделью:

Quote

и не должен зависеть от:

TickerPrice
MarketPriceSnapshot

5.3. Telegram UI

Основные файлы:

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

Основные файлы:

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

Основной файл:

app/src/trading/debug/execution.py

Debug execution переведён на новый quote-контракт.

Legacy-вызов:

get_fresh_market_snapshot()

удалён.


5.6. Trading Diagnostics

Основной файл:

app/src/trading/diagnostics/snapshot.py

Diagnostics consumer переведён на актуальное представление рыночной котировки.


5.7. Trading Strategies

Основные файлы:

app/src/trading/strategies/scalp.py
app/src/trading/strategies/trend.py

Стратегии переведены с legacy snapshot API на каноническую модель Quote.

После миграции стратегии не должны использовать:

get_market_snapshot()

6. ExecutionPriceSnapshot

В рамках Build 039 необходимо различать две сущности:

Quote

и:

ExecutionPriceSnapshot

Quote является канонической моделью текущей рыночной котировки.

ExecutionPriceSnapshot является специализированным downstream-контрактом execution layer и может существовать отдельно, поскольку представляет данные в форме, непосредственно необходимой исполнению торговых операций.

Таким образом:

Quote
    ↓
ExecutionPriceSnapshot

не является дублированием legacy-модели MarketPriceSnapshot.

ExecutionPriceSnapshot сохраняется как специализированная execution-модель.


7. Runtime status и stale market data

Во время Build 039 особое внимание было уделено методу:

ExchangeService.get_symbol_runtime_status()

Для открытого рынка выполняется проверка актуальности рыночной котировки.

Актуальный поток:

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:

age_seconds > 60

При возрасте ровно:

60.0

рынок остаётся в статусе OPEN.

При возрасте:

61.0

возвращается:

ExchangeStatusCode.BREAK
reason = "market_data_stale"
raw_status = "STALE_MARKET_DATA"

8. Формат времени stale market data

Для stale market status используется exchange timestamp котировки.

Преобразование выполняется следующим образом:

exchange_timestamp_ms = (
    int(quote.exchange_timestamp.timestamp() * 1000)
    if quote.exchange_timestamp is not None
    else None
)

После этого timestamp передаётся в:

self._format_exchange_time(exchange_timestamp_ms)

Это необходимо для сохранения существующего формата пользовательского сообщения.

Пример:

10.07.2026 12:00:00

Недопустимо передавать объект datetime непосредственно в функцию, ожидающую числовой timestamp в миллисекундах.


9. Сохранение legacy-поведения форматирования

Во время реализации Build 039 было выявлено критически важное правило миграции.

Две функции, решающие похожую задачу, не являются автоматически эквивалентными.

Например:

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}с"

и:

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 использовались и актуализировались следующие основные тесты:

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 некоторые тесты всё ещё содержали искусственные методы-заглушки вида:

def get_market_snapshot(self, *_: object, **__: object) -> object:
    raise AssertionError("legacy get_market_snapshot() must not be used")

и:

def get_fresh_market_snapshot(self, *_: object, **__: object) -> object:
    raise AssertionError("legacy get_fresh_market_snapshot() must not be used")

После завершения миграции эти заглушки были удалены из тестов, поскольку финальный архитектурный grep должен подтверждать полное отсутствие legacy API во всём дереве:

src
tests

Это касается файлов:

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-файлов:

python -m py_compile \
  src/integrations/exchange/service.py \
  tests/unit/integrations/exchange/test_service_symbol_runtime_status.py

Проверка завершилась успешно.


13. Целевая проверка runtime status

Выполнена команда:

python -m pytest \
  tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
  -q

Финальный результат:

34 passed in 1.44s

14. Полный regression suite

После завершения всех изменений выполнен полный набор тестов:

python -m pytest -q

Финальный результат:

606 passed in 1.63s

Все тесты успешно пройдены.


15. Финальный архитектурный grep

Выполнена команда:

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

Финальный результат:

<пусто>

Это подтверждает отсутствие legacy price/snapshot API в:

src
tests

16. Критерии завершения Build 039

Build 039 считается завершённым, поскольку выполнены все критерии:

  • TickerPrice удалён из src и tests;
  • MarketPriceSnapshot удалён из src и tests;
  • get_price() удалён из src и tests;
  • get_market_snapshot() удалён из src и tests;
  • get_fresh_market_snapshot() удалён из src и tests;
  • refresh_price_cache() удалён из src и tests;
  • refresh_market_snapshot_cache() удалён из src и tests;
  • _get_real_price() удалён из src и tests;
  • trading consumers используют новый quote-контракт;
  • Telegram consumers используют новый quote-контракт;
  • diagnostics consumers используют новый quote-контракт;
  • стратегии используют новый quote-контракт;
  • runtime status использует каноническую модель Quote;
  • stale market data detection сохранена;
  • формат exchange timestamp восстановлен;
  • legacy UI-поведение сохранено;
  • целевые тесты проходят;
  • полный regression suite проходит;
  • финальный архитектурный grep пуст.

17. Итоговое состояние после Build 039

После завершения Build 039 архитектура текущих котировок приведена к следующему состоянию:

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.

Перед заменой необходимо сравнивать:

тип входных данных
тип возвращаемого значения
формат результата
точность
единицы измерения
граничные условия
fallback-поведение
exception semantics
временные зоны
формат времени
UI-представление

Любое намеренное изменение поведения должно быть явно зафиксировано в документации соответствующего Build и покрыто тестами.


19. Статус Build

Build 039: COMPLETED
Regression suite: 606 passed
Legacy grep: empty
Canonical quote model: Quote

Build 039 полностью завершён.