# Build 036 — Перевод read-only и UI-потребителей

**Статус:** Завершён  
**Результат:** Успешно  
**Полная регрессия:** `614 passed`

---

## 1. Назначение Build

Цель Build 036 — перевести read-only и UI-потребителей рыночной котировки с legacy-представлений:

- `TickerPrice`;
- `dict[str, object]` из `get_market_snapshot()`;

на каноническую внутреннюю модель:

`Quote`

Build продолжает миграцию подсистемы рыночных данных на целевую архитектуру:

    Market Data
        ↓
    Market Intelligence
        ↓
    Decision
        ↓
    Execution
        ↓
    Exchange

В рамках Build 036 изменяется исключительно read-only контур.

Execution pricing, торговые стратегии, signal runtime и execution quality не переводятся и не изменяются.

---

## 2. Исходное состояние

До Build 036 read-only и UI-потребители получали текущую рыночную котировку через несколько legacy-интерфейсов.

Основные варианты:

    UI / diagnostics
            ↓
    ExchangeService.get_price()
            ↓
    TickerPrice

или:

    UI / diagnostics
            ↓
    ExchangeService.get_market_snapshot()
            ↓
    dict[str, object]

При этом после предыдущих Build каноническая модель `Quote` уже существовала и использовалась внутри новой инфраструктуры:

    REST Quotes Feed
            ↓
    Quote

    WebSocket quote adapter
            ↓
    Quote

    MarketPriceCache
            ↓
    Quote Store
            ↓
    Quote

Таким образом, read-only потребители продолжали зависеть от compatibility-представлений, несмотря на наличие канонической модели котировки.

---

## 3. Целевое состояние

После Build 036 read-only и UI-потребители получают канонический объект:

`Quote`

через публичный facade:

`ExchangeService.get_quote()`

Целевая цепочка чтения:

    read-only / UI consumer
            ↓
    ExchangeService.get_quote()
            ↓
    MarketPriceCache.get_quote()
            ↓
    Quote Store
            ↓
    canonical Quote

При отсутствии свежей котировки используется REST fallback:

    ExchangeService.get_quote()
            ↓
    REST Quotes Feed
            ↓
    canonical Quote
            ↓
    MarketPriceCache.set_quote()
            ↓
    Quote Store
            ↓
    canonical Quote

---

## 4. Архитектурный принцип

Read-only и UI-потребители не обращаются напрямую к:

`Quote Store`

Доступ выполняется через:

`ExchangeService.get_quote()`

Это позволяет сохранить единый facade, отвечающий за:

- выбор default symbol;
- нормализацию и валидацию символа;
- нормализацию `runtime_key`;
- поддержку mock mode;
- чтение канонической котировки из cache/store;
- проверку свежести;
- REST fallback;
- преобразование внутренних ошибок в `ExchangeError`.

Целевая граница:

    UI / diagnostics
            ↓
    ExchangeService
            ↓
    MarketPriceCache
            ↓
    Quote Store

UI не должен самостоятельно знать:

- структуру ключей Quote Store;
- `source_name`;
- правила `runtime_key`;
- freshness policy;
- правила REST fallback;
- внутреннюю обработку ошибок Acquisition Layer.

---

## 5. Изменённые production-файлы

В рамках Build 036 изменены:

    src/integrations/exchange/market_cache.py
    src/integrations/exchange/service.py

    src/telegram/ui/currency_ui.py
    src/telegram/handlers/auto/ui.py
    src/telegram/handlers/debug_auto/ui.py

    src/trading/diagnostics/snapshot.py

---

## 6. Изменённые и добавленные тесты

Изменены:

    tests/unit/integrations/exchange/test_market_cache.py
    tests/unit/telegram/ui/test_currency_ui.py

Добавлен:

    tests/unit/integrations/exchange/test_service_quote.py

После Build 036 общее количество тестов увеличилось:

    после Build 035: 608 passed
    после Build 036: 614 passed

Добавлено:

    6 тестов

---

## 7. Изменения в MarketPriceCache

Файл:

`src/integrations/exchange/market_cache.py`

Добавлен канонический read API:

`MarketPriceCache.get_quote()`

Его назначение — вернуть непосредственно канонический объект `Quote`, сохранённый в Quote Store.

Цепочка:

    MarketPriceCache.get_quote()
            ↓
    QuoteStore.get()
            ↓
    Quote | None

Метод:

- нормализует `symbol`;
- нормализует `runtime_key`;
- читает котировку из собственного namespace Quote Store;
- возвращает канонический `Quote`;
- не создаёт `MarketPriceSnapshot`;
- не выполняет преобразование `Decimal` в `float`;
- сохраняет канонический объект котировки.

Legacy API:

`MarketPriceCache.get_price()`

сохранён для compatibility-потребителей, которые ещё не переведены на `Quote`.

---

## 8. Изменения в ExchangeService

Файл:

`src/integrations/exchange/service.py`

Добавлен публичный канонический read API:

`ExchangeService.get_quote()`

Метод сохраняет обязанности facade и отвечает за:

1. выбор default symbol;
2. поддержку mock mode;
3. валидацию символа;
4. нормализацию `runtime_key`;
5. чтение канонической котировки из MarketPriceCache;
6. проверку свежести;
7. REST fallback при cache miss или stale quote;
8. сохранение свежей котировки в Quote Store через MarketPriceCache;
9. возврат канонического `Quote`.

Целевая цепочка:

    ExchangeService.get_quote()
            ↓
    validate_symbol()
            ↓
    MarketPriceCache.get_quote()
            ↓
    freshness check
            ↓
    Quote

При отсутствии свежей котировки:

    ExchangeService.get_quote()
            ↓
    REST Quotes Feed
            ↓
    Quote
            ↓
    MarketPriceCache.set_quote()
            ↓
    Quote Store

---

## 9. Политика свежести

Для read-only и UI-потребителей сохранена существующая политика свежести рыночной котировки.

Возраст котировки определяется по:

`Quote.received_at`

Если сохранённая котировка достаточно свежая, возвращается существующий канонический объект.

Если котировка отсутствует или устарела, выполняется REST fallback через новый Quotes Feed.

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

    fresh cached Quote
            ↓
    вернуть Quote без REST-запроса

    stale cached Quote
            ↓
    REST Quotes Feed
            ↓
    сохранить новый Quote
            ↓
    вернуть новый Quote

---

## 10. REST fallback

REST fallback использует новую каноническую цепочку Acquisition Layer:

    Dzengi REST API
            ↓
    GET /api/v1/ticker/24hr
            ↓
    DzengiQuoteDocumentSource
            ↓
    DzengiQuoteDocumentHandler
            ↓
    schema validation
            ↓
    parsing
            ↓
    value validation
            ↓
    mapping
            ↓
    canonical Quote

Полученный объект сохраняется:

    Quote
        ↓
    MarketPriceCache.set_quote()
        ↓
    Quote Store

Не используется лишний цикл преобразований:

    Quote
        ↓
    dict
        ↓
    float
        ↓
    новый Quote

Канонический объект остаётся `Quote` на всём новом пути.

---

## 11. Mock mode

`ExchangeService.get_quote()` сохраняет поддержку режима:

`exchange_enabled = False`

В этом режиме потребителю также возвращается канонический:

`Quote`

Mock-котировка содержит:

- `symbol`;
- `last_price`;
- `bid_price`;
- `ask_price`;
- `exchange_timestamp`;
- `received_at`;
- `source`.

Таким образом, потребители `get_quote()` не должны знать, работает приложение с реальной биржей или в mock mode.

---

## 12. Обработка ошибок

Новый canonical read API сохраняет существующую границу ошибок ExchangeService.

Ошибки Acquisition Layer:

- не передаются напрямую UI-потребителям;
- логируются в контексте `ticker/24hr`;
- преобразуются во внешний `ExchangeError`;
- сохраняют исходную ошибку через `__cause__`.

Граница остаётся следующей:

    Acquisition error
            ↓
    ExchangeService
            ↓
    ExchangeError
            ↓
    UI / diagnostics consumer

---

## 13. Перевод currency_ui.py

Файл:

`src/telegram/ui/currency_ui.py`

До Build 036 использовался legacy API:

`ExchangeService.get_price()`

Возвращаемая модель:

`TickerPrice`

Для расчёта использовалось:

`ticker.price`

После Build 036 используется:

`ExchangeService.get_quote()`

и каноническое поле:

`quote.last_price`

Целевая цепочка:

    currency_ui
        ↓
    ExchangeService.get_quote()
        ↓
    Quote.last_price

Локальная логика расчёта стоимости баланса, обработка `ExchangeError` и кэширование рассчитанных цен сохранены.

---

## 14. Перевод auto/ui.py

Файл:

`src/telegram/handlers/auto/ui.py`

До Build 036 UI получал legacy market snapshot:

`ExchangeService.get_market_snapshot()`

и работал с:

`dict[str, object]`

Основные поля:

- `last_price`;
- `bid_price`;
- `ask_price`.

После Build 036 UI получает:

`Quote`

через:

`ExchangeService.get_quote()`

Используются канонические поля:

- `quote.last_price`;
- `quote.bid_price`;
- `quote.ask_price`.

В результате UI больше не зависит от legacy market snapshot dict для получения текущей рыночной котировки.

---

## 15. Перевод debug_auto/ui.py

Файл:

`src/telegram/handlers/debug_auto/ui.py`

В debug UI разделены две разные сущности:

1. текущая рыночная котировка;
2. execution snapshot.

После Build 036 market-секция использует:

`Quote`

и читает:

- `last_price`;
- `bid_price`;
- `ask_price`;
- `source`;
- `received_at`;
- `exchange_timestamp`.

Execution-секция продолжает использовать:

`ExecutionPriceSnapshot`

Это намеренное разделение.

Build 036 не изменяет execution semantics.

Целевая схема:

    Debug UI
       ├── Market section
       │       ↓
       │     Quote
       │
       └── Execution section
               ↓
          ExecutionPriceSnapshot

---

## 16. Перевод trading/diagnostics/snapshot.py

Файл:

`src/trading/diagnostics/snapshot.py`

До Build 036 диагностика использовала:

`ExchangeService.get_market_snapshot()`

После Build 036 используется:

`ExchangeService.get_quote()`

Для выбора диагностической цены сохраняется существующая семантика:

    BUY
        ↓
    quote.ask_price

    SELL
        ↓
    quote.bid_price

    другое состояние
        ↓
    quote.last_price

Build не изменяет торговые решения и используется только для read-only диагностики.

---

## 17. Сохранённые legacy API

Build 036 не удаляет:

- `ExchangeService.get_price()`;
- `ExchangeService.get_market_snapshot()`;
- `ExchangeService.get_execution_snapshot()`;
- `ExchangeService.get_fresh_market_snapshot()`;
- `MarketPriceCache.get_price()`.

Они сохраняются для ещё не переведённых compatibility-потребителей.

Удаление legacy API возможно только после полного перевода всех зависимых компонентов и отдельной проверки использования.

---

## 18. Что намеренно не изменялось

Build 036 не затрагивает:

    src/trading/execution/pricing.py
    src/trading/auto/signal_runtime.py
    src/trading/auto/execution_quality.py
    src/trading/strategies/trend.py
    src/trading/strategies/scalp.py
    src/trading/debug/execution.py

Эти компоненты относятся к:

- execution pricing;
- signal runtime;
- execution quality;
- strategy decisions;
- debug execution semantics.

Их перевод должен выполняться отдельно.

---

## 19. Что не входит в Build 036

В рамках Build 036 не выполнялись:

- перевод execution pricing;
- перевод signal runtime;
- перевод execution quality;
- перевод торговых стратегий;
- удаление `TickerPrice`;
- удаление `get_price()`;
- удаление `get_market_snapshot()`;
- удаление legacy market snapshot dict layer;
- удаление `ExecutionPriceSnapshot`;
- удаление `MarketPriceCache`;
- удаление legacy parsing helpers.

---

## 20. Проверка синтаксиса

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

    python -m py_compile \
      src/integrations/exchange/market_cache.py \
      src/integrations/exchange/service.py \
      src/telegram/ui/currency_ui.py \
      src/telegram/handlers/auto/ui.py \
      src/telegram/handlers/debug_auto/ui.py \
      src/trading/diagnostics/snapshot.py \
      tests/unit/integrations/exchange/test_market_cache.py \
      tests/unit/integrations/exchange/test_service_quote.py \
      tests/unit/telegram/ui/test_currency_ui.py

Результат:

`Успешно`

Ошибок синтаксиса не обнаружено.

---

## 21. Специализированные тесты

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

    python -m pytest \
      tests/unit/integrations/exchange/test_market_cache.py \
      tests/unit/integrations/exchange/test_service_quote.py \
      tests/unit/telegram/ui/test_currency_ui.py \
      -q

Результат:

`44 passed in 0.29s`

---

## 22. Полная регрессия

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

`python -m pytest -q`

Результат:

`614 passed in 0.27s`

Полная тестовая регрессия проекта успешно пройдена.

---

## 23. Итоговая архитектура после Build 036

После завершения Build 036 read-only и UI-контур использует следующую архитектуру:

    ┌──────────────────────────────┐
    │ currency_ui                  │
    │ auto UI                      │
    │ debug UI market section      │
    │ trading diagnostics          │
    └──────────────┬───────────────┘
                   ↓
         ExchangeService.get_quote()
                   ↓
         MarketPriceCache.get_quote()
                   ↓
              Quote Store
                   ↓
            canonical Quote

При cache miss или stale quote:

    ExchangeService.get_quote()
            ↓
    REST Quotes Feed
            ↓
    canonical Quote
            ↓
    MarketPriceCache.set_quote()
            ↓
    Quote Store

---

## 24. Результат Build

Build 036 успешно завершён.

Достигнуты следующие результаты:

- добавлен канонический read API `MarketPriceCache.get_quote()`;
- добавлен публичный facade `ExchangeService.get_quote()`;
- сохранены symbol validation, mock mode, freshness policy и REST fallback;
- `currency_ui.py` переведён с `TickerPrice` на `Quote`;
- `auto/ui.py` переведён с legacy market snapshot dict на `Quote`;
- market-секция debug UI переведена на `Quote`;
- execution-секция debug UI оставлена на `ExecutionPriceSnapshot`;
- trading diagnostics переведена на `Quote`;
- execution-контур не изменён;
- legacy API сохранены для последующей миграции;
- специализированные тесты успешно пройдены;
- полная регрессия успешно пройдена.

Итог:

    До Build 036:

    UI / diagnostics
            ↓
    TickerPrice / market snapshot dict

    После Build 036:

    UI / diagnostics
            ↓
    ExchangeService.get_quote()
            ↓
    canonical Quote

---

## 25. Следующий этап

Следующий этап миграции:

**Build 037 — Перевод execution-потребителей**

Его задача — отдельно проанализировать и перевести execution-sensitive потребителей канонической рыночной котировки без изменения торговой семантики и без преждевременного удаления legacy compatibility API.