# 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 ```