# Build 060.1 — Canonical Trade Model **Engineering Migration Report** --- # Контроль документа | Свойство | Значение | |---|---| | Build | 060.1 | | Название | Canonical Trade Model | | Статус | Завершён | | Проект | Dzentra | | Подсистема | Market Data Acquisition | | Компонент | Trades Feed / Time & Sales | | Версия документа | 1.0 | | Дата завершения | 2026-07-18 | --- # Цель Build Ввести первый стабильный канонический контракт одной исполненной биржевой сделки: ```text Trade ``` Новая модель должна стать единым внутренним представлением сделки в подсистеме `Market Data Acquisition` независимо от способа доставки исходных данных. Она предназначена для последующего формирования из двух подтверждённых источников: ```text REST GET /api/v2/aggTrades ``` и ```text WebSocket trades.subscribe → internal.trade ``` Build 060.1 ограничен только модельным уровнем. В рамках Build не реализуются: - REST Parser; - WebSocket Parser; - schema validation; - value validation; - REST Mapper; - WebSocket Mapper; - document source; - handler; - feed; - protocol; - registry; - acquisition service; - WebSocket Runtime; - production-интеграция. --- # Архитектурный контекст До начала Build 060.1 в Dzentra уже существовали канонические модели рыночных данных: ```text Candle Quote ``` Они используют единые conventions: - `@dataclass(frozen=True, slots=True)`; - `Decimal` для числовых рыночных значений; - `datetime` для времени; - source-independent названия полей; - отсутствие parsing и validation внутри модели; - отсутствие transport-specific полей; - отсутствие mutable-состояния. Build 057 предварительно подтвердил, что REST и WebSocket Trade API описывают одни и те же биржевые сделки. Сопоставление transport-полей: | REST | WebSocket | Каноническая семантика | |---|---|---| | `a` | `id` | `trade_id` | | `p` | `price` | `price` | | `q` | `size` | `quantity` | | `T` | `ts` | `executed_at` | | `m` | `buyer` | `aggressor_side` | Для стороны агрессора подтверждено правило: ```text buyer == not m ``` Следовательно, REST и WebSocket должны преобразовываться в одну модель `Trade`, а не в отдельные типы. --- # Исходное состояние До Build 060.1 файл: ```text app/src/market_data/acquisition/models/trade.py ``` существовал, но не содержал модели `Trade`. Файл: ```text app/src/market_data/acquisition/models/__init__.py ``` не использовался для экспорта существующих Canonical Models. Отдельный unit-тест для Trade отсутствовал. Таким образом Build мог быть реализован как строго локальное additive change без изменения существующего production-поведения. --- # Предварительный архитектурный аудит Перед реализацией были проверены: ```text app/src/market_data/acquisition/models/candle.py app/src/market_data/acquisition/models/quote.py app/src/market_data/acquisition/models/trade.py app/src/market_data/acquisition/models/__init__.py docs/migrations/build_057.md app/tests/unit/market_data/acquisition/models/ ``` Аудит подтвердил: - `Trade` должен следовать conventions `Candle` и `Quote`; - модель должна быть immutable; - модель должна использовать `slots=True`; - `price` и `quantity` должны иметь тип `Decimal`; - время исполнения должно иметь тип `datetime`; - модель не должна выполнять validation; - модель не должна выполнять normalization; - `models/__init__.py` не должен изменяться; - REST и WebSocket должны использовать один Canonical Trade; - transport boolean не должен попадать во внутренний контракт. --- # Рассмотренные архитектурные решения ## Название идентификатора Рассматривались: ```text trade_id exchange_trade_id ``` Выбрано: ```text trade_id ``` Причины: - поле остаётся предметным и кратким; - источник уже фиксируется отдельно через `source`; - модель не должна быть жёстко привязана к конкретной бирже; - имя одинаково применимо к REST и WebSocket. --- ## Название количества Рассматривались: ```text quantity size ``` Выбрано: ```text quantity ``` Причины: - `size` является именем WebSocket transport-поля; - REST использует `q`; - `quantity` является source-independent предметным названием; - имя согласуется с общей терминологией торговых систем. --- ## Представление стороны сделки Рассматривались: ```text bool str Enum ``` Выбран отдельный Enum: ```python class TradeAggressorSide(Enum): BUY = "buy" SELL = "sell" ``` Причины: - boolean неоднозначен без знания transport-семантики; - REST `m` и WebSocket `buyer` имеют противоположную логику; - произвольная строка допускает неконтролируемые значения; - Enum явно фиксирует допустимый предметный контракт. --- ## Название поля стороны Рассматривались: ```text side buyer aggressor aggressor_side ``` Выбрано: ```text aggressor_side ``` Причины: - `buyer` является transport-полем WebSocket; - `side` может ошибочно трактоваться как сторона ордера или позиции; - `aggressor_side` точно отражает участника, инициировавшего исполнение. --- ## Название времени Рассматривались: ```text timestamp trade_time executed_at ``` Выбрано: ```text executed_at ``` Причины: - поле описывает предметное время исполнения; - имя не связано с transport-форматом; - тип `datetime` уже делает слово `timestamp` избыточным; - название согласуется с событийной семантикой сделки. --- # Окончательно утверждённый контракт Добавлен предметный Enum: ```python class TradeAggressorSide(Enum): BUY = "buy" SELL = "sell" ``` Добавлена каноническая immutable-модель: ```python @dataclass(frozen=True, slots=True) class Trade: symbol: str trade_id: int price: Decimal quantity: Decimal executed_at: datetime aggressor_side: TradeAggressorSide source: str ``` --- # Семантика Canonical Trade ## `symbol` ```python symbol: str ``` Канонический идентификатор торгового инструмента. Модель не выполняет нормализацию символа и не проверяет его существование. --- ## `trade_id` ```python trade_id: int ``` Идентификатор исполненной сделки, предоставленный источником рыночных данных. В исследованных REST и WebSocket контрактах значение совпадает. --- ## `price` ```python price: Decimal ``` Цена фактического исполнения сделки. Используется `Decimal`, чтобы исключить потерю точности, характерную для `float`. --- ## `quantity` ```python quantity: Decimal ``` Исполненное количество инструмента. Каноническое имя не зависит от REST-поля `q` или WebSocket-поля `size`. --- ## `executed_at` ```python executed_at: datetime ``` Время фактического исполнения сделки. Преобразование миллисекундного transport timestamp в timezone-aware `datetime` должно выполняться будущим mapper-слоем, а не моделью. --- ## `aggressor_side` ```python aggressor_side: TradeAggressorSide ``` Сторона участника, инициировавшего исполнение: ```text TradeAggressorSide.BUY TradeAggressorSide.SELL ``` Transport-поля `m` и `buyer` не сохраняются внутри Canonical Trade. --- ## `source` ```python source: str ``` Логический источник рыночных данных. Поле не означает способ доставки и не должно содержать transport-различие вида: ```text rest websocket ``` если оба пути относятся к одному и тому же рыночному источнику. --- # Поля, намеренно не включённые в модель ## `received_at` Поле не добавлено. Причины: - оно описывает acquisition/runtime, а не саму сделку; - для REST history и WebSocket realtime оно имеет различную практическую семантику; - его наличие сделало бы модель зависимой от способа получения данных. --- ## `order_id` Поле не добавлено. Причины: - присутствует в исследованном WebSocket событии; - отсутствует в REST `aggTrades`; - не имеет общего контракта для обоих transport-путей; - является биржевым implementation detail. --- ## `buyer` Поле не добавлено. Причины: - является WebSocket transport boolean; - требует знания внешней семантики; - заменено предметным `TradeAggressorSide`. --- ## `m` Поле не добавлено. Причины: - является REST transport-полем; - означает `isBuyerMaker`; - имеет обратную семантику относительно WebSocket `buyer`; - должно устраняться mapper-слоем. --- ## `size` Поле не добавлено. Причина: - это имя WebSocket transport-поля; - в Canonical Trade используется source-independent `quantity`. --- ## Исходные timestamp-поля Не добавлены: ```text T ts timestamp_ms ``` Transport timestamp должен быть преобразован в: ```text executed_at: datetime ``` до создания Canonical Trade. --- # Изменённые файлы В рамках Build изменены или созданы только: ```text app/src/market_data/acquisition/models/trade.py app/tests/unit/market_data/acquisition/models/test_trade.py docs/migrations/build_060_1.md ``` --- # Изменения в `trade.py` Файл: ```text app/src/market_data/acquisition/models/trade.py ``` получил: - imports `dataclass`, `datetime`, `Decimal`, `Enum`; - предметный Enum `TradeAggressorSide`; - immutable-модель `Trade`; - комментарии на русском языке; - группировку полей в стиле существующих Canonical Models. Модель не содержит: - методов; - `__post_init__`; - validation; - parsing; - normalization; - transport mapping; - runtime metadata. --- # Изменения в unit-тестах Создан файл: ```text app/tests/unit/market_data/acquisition/models/test_trade.py ``` Тесты проверяют: - сохранение `symbol`; - сохранение `trade_id`; - сохранение `price`; - сохранение `quantity`; - сохранение `executed_at`; - сохранение `aggressor_side`; - сохранение `source`; - поддержку `TradeAggressorSide.BUY`; - поддержку `TradeAggressorSide.SELL`; - стабильность значений Enum; - immutable-поведение; - наличие `slots=True`; - отсутствие скрытого преобразования значений моделью. --- # Что намеренно не изменялось Build 060.1 намеренно не изменяет: ```text app/src/market_data/acquisition/models/__init__.py app/src/market_data/acquisition/protocol.py app/src/market_data/acquisition/registry.py app/src/market_data/acquisition/service.py app/src/market_data/acquisition/feeds/trades_feed.py app/src/market_data/acquisition/handlers/trades_handler.py app/src/market_data/acquisition/runtime/ app/src/market_data/acquisition/adapters/ app/src/market_data/acquisition/validation/ ``` Также не выполнялись: - REST-интеграция; - WebSocket-интеграция; - создание transport-моделей; - создание parser; - создание mapper; - создание handler; - создание feed; - регистрация нового feed; - изменение публичных protocol; - подключение к runtime; - изменение Candle Feed; - изменение Quote; - реорганизация каталогов; - cleanup unrelated-кода. --- # Проверка компиляции Команды выполнялись из каталога: ```text ~/vsprojects/dzentra_bot/app ``` Активировано окружение: ```bash source .venv/bin/activate ``` Выполнена команда: ```bash python -m compileall src ``` Результат: ```text успешно ``` Все каталоги `src` были обработаны без ошибок компиляции. --- # Полный regression suite Выполнена команда: ```bash python -m pytest -q ``` Результат: ```text 990 passed in 4.74s ``` Регрессий не обнаружено. --- # Проверка форматирования Выполнена команда: ```bash git diff --check ``` Вывод отсутствует. Это подтверждает отсутствие: - trailing whitespace; - whitespace errors; - некорректных пустых строк в diff. --- # Контроль размещения новой модели Выполнена команда: ```bash grep -RIn \ --exclude-dir="__pycache__" \ --exclude="*.pyc" \ "TradeAggressorSide\|class Trade" \ src tests ``` Результат подтвердил наличие нового контракта только в утверждённых местах: ```text src/market_data/acquisition/models/trade.py tests/unit/market_data/acquisition/models/test_trade.py ``` Обнаружены: ```text class TradeAggressorSide(Enum) class Trade ``` и их использования в целевом unit-тесте. Production-потребители `Trade` в рамках Build 060.1 не добавлялись. --- # Состояние Git Выполнена команда: ```bash git status ``` Для Build 060.1 зафиксированы: ```text modified: app/src/market_data/acquisition/models/trade.py untracked: app/tests/unit/market_data/acquisition/models/test_trade.py docs/migrations/build_060_1.md ``` Дополнительно в рабочем дереве присутствует архитектурная контрольная точка: ```text docs/migrations/build_060_transition_&_architecture_checkpoint.md ``` Она не является реализационным файлом Build 060.1 и должна учитываться отдельно при формировании commit согласно принятой стратегии репозитория. Ветка: ```text main ``` на момент проверки опережала `origin/main` на 27 локальных commits. Это состояние не связано с реализацией Canonical Trade и не изменялось в рамках Build. --- # Фактический diff модели В `trade.py` добавлено 31 строка. Основной diff: ```python from dataclasses import dataclass from datetime import datetime from decimal import Decimal from enum import Enum class TradeAggressorSide(Enum): BUY = "buy" SELL = "sell" @dataclass(frozen=True, slots=True) class Trade: symbol: str trade_id: int price: Decimal quantity: Decimal executed_at: datetime aggressor_side: TradeAggressorSide source: str ``` Рабочий код других компонентов не изменялся. --- # Архитектурный результат После Build 060.1 в Dzentra существует третий базовый канонический тип рыночных данных: ```text Candle Quote Trade ``` Новый контракт: - не зависит от Dzengi; - не зависит от REST; - не зависит от WebSocket; - не содержит transport-семантику; - immutable; - memory-efficient за счёт `slots=True`; - использует точные числовые типы; - выражает сторону агрессора отдельным предметным Enum; - готов для последующего использования обоими transport-путями. Целевая будущая схема: ```text Dzengi REST aggTrades ↓ REST document source ↓ REST schema validation ↓ REST parser ↓ value validation ↓ REST mapper ↓ Trade ``` и: ```text Dzengi WebSocket internal.trade ↓ WebSocket parser ↓ WebSocket schema validation ↓ value validation ↓ WebSocket mapper ↓ Trade ``` Оба пути должны завершаться одной и той же канонической моделью. --- # Критерии завершения Build 060.1 считается завершённым, поскольку: - проведён архитектурный аудит; - утверждён минимальный Canonical Trade contract; - добавлен `TradeAggressorSide`; - добавлен immutable `Trade`; - модель использует `Decimal`; - модель использует `datetime`; - transport-поля исключены; - `received_at` не добавлен; - `order_id` не добавлен; - `models/__init__.py` не изменён; - добавлены целевые unit-тесты; - `python -m compileall src` проходит; - полный test suite проходит; - результат полного suite: `990 passed in 4.74s`; - `git diff --check` чистый; - grep подтверждает локальность новой модели; - scope Build не расширен. --- # Итог **Build 060.1 завершён успешно.** Текущее состояние: ```text TradeAggressorSide — реализован Trade — реализован Immutable contract — подтверждён slots=True — подтверждён Decimal fields — подтверждены executed_at datetime — подтверждён Transport independence — сохранена Compile check — успешно Full regression suite — 990 passed Whitespace check — чисто Production integration — намеренно не выполнялась ``` Build создал стабильную модельную основу для дальнейшей реализации Trades Feed без изменения существующего runtime и production-поведения. --- # Следующий этап Следующий Build должен быть отдельным, минимальным и additive. Его точный scope должен быть утверждён до написания кода. Наиболее логичное продолжение ветки Trades Feed: ```text Build 060.2 — REST Trade Transport Foundation ``` Предполагаемые направления будущего этапа: - REST Trade document contract; - REST schema validation; - REST parser; - REST value validation; - REST mapper в Canonical Trade; - целевые unit-тесты. WebSocket Trade integration, feed, handler, protocol, registry, service и runtime должны оставаться отдельными последующими Build.