# Build 027 — Каноническая модель Quote и специализированные контракты

## Статус

**Завершён**

---

## Цель Build

Создать каноническую внутреннюю модель текущей рыночной котировки `Quote` и специализированные контракты подсистемы `Quotes Feed`.

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

- источником сырого документа котировки;
- обработчиком документа;
- готовым потоком котировок;
- потребителями `Market Data Acquisition`.

При этом существующий legacy-контур получения и использования цен не должен изменяться.

---

## Место в плане миграции

Build 027 является вторым этапом миграции подсистемы `Quotes Feed`.

Полный утверждённый план:

```text
Build 026 — Аудит текущего контура Quotes Feed
Build 027 — Каноническая модель Quote и специализированные контракты
Build 028 — Dzengi REST quote models, parser и validation
Build 029 — Dzengi mapper и Quotes Handler
Build 030 — Quotes Feed и регистрация в Acquisition Service
Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade
Build 032 — Канонический Quote Store
Build 033 — Перенос MarketPriceCache на Quote Store
Build 034 — Dzengi WebSocket quote parsing и адаптер
Build 035 — Перевод market runtime на Quotes Feed
Build 036 — Перевод read-only и UI-потребителей
Build 037 — Перевод execution-потребителей
Build 038 — Удаление legacy TickerPrice и market snapshot dict layer
Build 039 — Удаление legacy quote parsing и MarketPriceCache
Build 040 — Финальная архитектурная проверка Quotes Feed
```

---

## Архитектурная граница Build

Build 027 ограничен двумя задачами:

1. создание канонической модели `Quote`;
2. создание специализированных контрактов `Quotes Feed`.

В рамках Build не реализуются:

- REST-запрос котировки Dzengi;
- модели REST-ответа Dzengi;
- parsing ответа `ticker/24hr`;
- schema validation ответа Dzengi;
- value validation полей котировки;
- mapping модели Dzengi в `Quote`;
- `QuotesHandler`;
- `QuotesFeed`;
- регистрация потока в `Acquisition Service`;
- хранение котировок;
- WebSocket parsing;
- изменение `ExchangeService`;
- изменение `MarketPriceCache`;
- изменение market runtime;
- перевод UI-потребителей;
- перевод execution-потребителей.

Эти изменения относятся к следующим Build.

---

## Изменённые файлы

```text
src/market_data/acquisition/
├── models/
│   └── quote.py
└── protocol.py
```

Всего изменено:

- **2 файла**.

---

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

Файл:

```text
src/market_data/acquisition/models/quote.py
```

Создана независимая от конкретной биржи immutable-модель:

```python
@dataclass(frozen=True, slots=True)
class Quote:
    symbol: str

    last_price: Decimal
    bid_price: Decimal
    ask_price: Decimal

    exchange_timestamp: datetime | None
    received_at: datetime

    source: str
```

### Назначение модели

`Quote` представляет текущий рыночный факт о котировке одного инструмента.

Модель является внутренней моделью слоя:

```text
Market Data
    ↓
Market Data Acquisition
    ↓
Quotes Feed
    ↓
Quote
```

Она не зависит от:

- API Dzengi;
- формата `ticker/24hr`;
- WebSocket-сообщений;
- legacy-моделей `integrations/exchange`;
- UI;
- Execution;
- конкретного способа хранения данных.

---

## 2. Поля модели Quote

### `symbol`

```python
symbol: str
```

Каноническое обозначение инструмента.

Пример:

```text
BTC/USD
```

Поле не должно содержать транспортное или биржевое представление, специфичное для конкретного API, если оно отличается от канонического обозначения Dzentra.

---

### `last_price`

```python
last_price: Decimal
```

Последняя известная цена инструмента, полученная от источника.

---

### `bid_price`

```python
bid_price: Decimal
```

Лучшая доступная цена покупки.

---

### `ask_price`

```python
ask_price: Decimal
```

Лучшая доступная цена продажи.

---

### `exchange_timestamp`

```python
exchange_timestamp: datetime | None
```

Время рыночного события на стороне источника данных.

Поле является optional, поскольку конкретный источник или endpoint может не предоставлять достоверный timestamp события.

---

### `received_at`

```python
received_at: datetime
```

Время получения рыночных данных системой Dzentra.

Это позволяет независимо от наличия `exchange_timestamp` фиксировать момент поступления данных в систему.

---

### `source`

```python
source: str
```

Идентификатор источника рыночных данных.

Пример:

```text
dzengi
```

Модель не фиксирует конкретный набор допустимых источников на уровне класса `Quote`.

---

## 3. Использование Decimal

Для канонических цен используется:

```python
Decimal
```

а не:

```python
float
```

Это позволяет избежать привязки новой внутренней модели к ограничениям legacy-кода и уменьшает риск потери точности при работе с денежными значениями.

Legacy-потребители при необходимости смогут получать преобразованное значение `float` через compatibility/facade-слой на следующих этапах миграции.

---

## 4. Immutable-модель

Модель объявлена как:

```python
@dataclass(frozen=True, slots=True)
```

Это означает:

- экземпляр `Quote` не изменяется после создания;
- исключается случайная мутация рыночного факта;
- модель имеет компактное представление через `slots`;
- объект подходит для передачи между слоями системы как immutable value object.

Такой подход соответствует уже принятому направлению построения канонических моделей `Market Data Acquisition`.

---

## 5. Что сознательно не включено в Quote

В каноническую модель не включены поля:

```text
is_fresh
age_seconds
freshness_status
spread_percent
runtime_key
received_monotonic
```

Причина: эти значения не являются исходным фактом котировки.

Они относятся к другим обязанностям системы.

### Freshness

```text
is_fresh
age_seconds
freshness_status
```

Это runtime-оценка актуальности данных.

Она должна вычисляться на основании времени получения или хранения котировки, а не быть частью исходного объекта `Quote`.

### Spread

```text
spread_percent
```

Это производное значение:

```text
ask_price - bid_price
```

или его процентное представление.

Оно может быть вычислено отдельным processing/runtime-компонентом.

### Runtime identity

```text
runtime_key
```

Это идентификатор runtime-контекста, а не свойство рыночной котировки.

### Monotonic clock

```text
received_monotonic
```

Это внутренний технический механизм runtime/storage-слоя для измерения возраста данных.

Он не должен загрязнять каноническую модель рыночного факта.

---

## 6. Специализированные контракты Quotes Feed

В файл:

```text
src/market_data/acquisition/protocol.py
```

добавлены три специализированных контракта:

```text
QuoteDocumentSource
QuoteDocumentHandler
QuoteFeedProtocol
```

Архитектурная цепочка:

```text
QuoteDocumentSource
        ↓
сырой документ
        ↓
QuoteDocumentHandler
        ↓
Quote
        ↓
QuoteFeedProtocol
        ↓
Acquisition Service
```

---

## 7. QuoteDocumentSource

Контракт:

```python
@runtime_checkable
class QuoteDocumentSource(Protocol):
    def fetch_quote_document(
        self,
        symbol: str,
    ) -> object:
        """
        Получить декодированный транспортный документ текущей котировки.

        Источник не выполняет schema validation, parsing, value validation
        или mapping во внутреннюю модель Quote.
        """
        ...
```

### Ответственность

`QuoteDocumentSource` отвечает только за получение сырого декодированного транспортного документа.

Он не должен:

- проверять схему;
- проверять значения;
- выполнять mapping;
- создавать `Quote`;
- хранить котировку;
- вычислять freshness;
- обслуживать UI или Execution.

Для Dzengi конкретная реализация будет создана на следующих этапах.

---

## 8. QuoteDocumentHandler

Контракт:

```python
@runtime_checkable
class QuoteDocumentHandler(Protocol):
    def handle_quote_document(
        self,
        document: object,
    ) -> Quote:
        """
        Преобразовать сырой документ в проверенную внутреннюю модель Quote.
        """
        ...
```

### Ответственность

`QuoteDocumentHandler` определяет границу между сырым внешним документом и проверенной канонической моделью `Quote`.

Конкретная реализация должна организовать последовательность:

```text
сырой документ
    ↓
schema validation
    ↓
parser
    ↓
value validation
    ↓
mapper
    ↓
Quote
```

Сам контракт не зависит от конкретной биржи.

---

## 9. QuoteFeedProtocol

Контракт:

```python
@runtime_checkable
class QuoteFeedProtocol(Protocol):
    def load_quote(
        self,
        symbol: str,
    ) -> Quote:
        """
        Получить внутреннюю модель текущей котировки инструмента.
        """
        ...
```

### Ответственность

`QuoteFeedProtocol` представляет готовый поток получения канонической текущей котировки для `Acquisition Service`.

Потребитель этого контракта не должен знать:

- какая биржа является источником;
- используется REST или другой транспорт;
- как устроен внешний payload;
- как выполняется parsing;
- как выполняется validation;
- как выполняется mapping.

Для потребителя существует только операция:

```text
symbol → Quote
```

---

## 10. Соответствие паттерну Instrument Reference Data

Build 027 продолжает архитектурный подход, уже реализованный для `Instrument Reference Data`.

### Instrument Reference Data

```text
InstrumentDocumentSource
        ↓
InstrumentDocumentHandler
        ↓
InstrumentFeedProtocol
        ↓
Instrument
```

### Quotes Feed

```text
QuoteDocumentSource
        ↓
QuoteDocumentHandler
        ↓
QuoteFeedProtocol
        ↓
Quote
```

Таким образом, новая вертикаль `Quotes Feed` строится в соответствии с уже принятой архитектурой `Market Data Acquisition`, без создания альтернативного или параллельного архитектурного подхода.

---

## 11. Legacy-контур

Build 027 не изменяет существующие legacy-компоненты:

```text
src/integrations/exchange/models.py
src/integrations/exchange/service.py
src/integrations/exchange/market_cache.py
src/integrations/exchange/market_stream.py
src/integrations/exchange/market_data_runner.py
src/integrations/exchange/ws_client.py
```

Продолжают работать без изменений:

```text
TickerPrice
ExecutionPriceSnapshot
MarketPriceSnapshot
MarketPriceCache
ExchangeService.get_price()
ExchangeService.get_market_snapshot()
ExchangeService.get_execution_snapshot()
ExchangeService.get_fresh_market_snapshot()
```

На данном этапе новая модель `Quote` существует параллельно legacy-контуру и ещё не используется работающим ботом.

Это соответствует утверждённой стратегии безопасной миграции:

```text
создать новый контур
    ↓
проверить новый контур
    ↓
подключить его под legacy facade
    ↓
поэтапно перевести потребителей
    ↓
удалить legacy только после полного переключения
```

---

## 12. Обратная совместимость

Build 027 полностью обратно совместим с существующим ботом.

Не изменены:

- публичные методы `ExchangeService`;
- форматы legacy snapshot;
- `MarketPriceCache`;
- market runtime;
- Telegram UI;
- trading strategies;
- Execution;
- существующие модели интеграционного слоя.

Новая модель и контракты пока не участвуют в runtime работающего приложения.

---

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

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

```bash
python -m py_compile \
  src/market_data/acquisition/models/quote.py \
  src/market_data/acquisition/protocol.py
```

Результат:

```text
Успешно.
Ошибок синтаксиса и импортов не обнаружено.
```

---

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

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

```bash
python -m pytest -q
```

Результат:

```text
423 passed in 0.24s
```

Все существующие тесты проекта проходят.

Регрессий не обнаружено.

---

## 15. Критерии завершения

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

- [x] создана каноническая модель `Quote`;
- [x] модель не зависит от Dzengi;
- [x] цены представлены через `Decimal`;
- [x] модель immutable;
- [x] разделены `exchange_timestamp` и `received_at`;
- [x] runtime-поля не включены в каноническую модель;
- [x] создан `QuoteDocumentSource`;
- [x] создан `QuoteDocumentHandler`;
- [x] создан `QuoteFeedProtocol`;
- [x] сохранён архитектурный паттерн существующей вертикали `Instrument`;
- [x] legacy-контур не изменён;
- [x] синтаксическая проверка проходит;
- [x] полная регрессия проходит;
- [x] `423` теста проходят успешно.

---

## Итог

В рамках Build 027 создан фундамент канонической вертикали `Quotes Feed`.

Теперь архитектура содержит независимое представление текущей рыночной котировки:

```text
Quote
```

и три специализированных контракта:

```text
QuoteDocumentSource
QuoteDocumentHandler
QuoteFeedProtocol
```

Целевая архитектурная цепочка сформирована как:

```text
Внешний источник
        ↓
QuoteDocumentSource
        ↓
сырой транспортный документ
        ↓
QuoteDocumentHandler
        ↓
schema validation
        ↓
parser
        ↓
value validation
        ↓
mapper
        ↓
Quote
        ↓
QuoteFeedProtocol
        ↓
Acquisition Service
```

Build 027 завершён без изменения поведения работающего бота и без преждевременного вмешательства в legacy-контур.

Следующий этап:

```text
Build 028 — Dzengi REST quote models, parser и validation
```