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