21 KiB
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
Ввести первый стабильный канонический контракт одной исполненной биржевой сделки:
Trade
Новая модель должна стать единым внутренним представлением сделки в подсистеме Market Data Acquisition независимо от способа доставки исходных данных.
Она предназначена для последующего формирования из двух подтверждённых источников:
REST GET /api/v2/aggTrades
и
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 уже существовали канонические модели рыночных данных:
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 |
Для стороны агрессора подтверждено правило:
buyer == not m
Следовательно, REST и WebSocket должны преобразовываться в одну модель Trade, а не в отдельные типы.
Исходное состояние
До Build 060.1 файл:
app/src/market_data/acquisition/models/trade.py
существовал, но не содержал модели Trade.
Файл:
app/src/market_data/acquisition/models/__init__.py
не использовался для экспорта существующих Canonical Models.
Отдельный unit-тест для Trade отсутствовал.
Таким образом Build мог быть реализован как строго локальное additive change без изменения существующего production-поведения.
Предварительный архитектурный аудит
Перед реализацией были проверены:
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должен следовать conventionsCandleиQuote;- модель должна быть immutable;
- модель должна использовать
slots=True; priceиquantityдолжны иметь типDecimal;- время исполнения должно иметь тип
datetime; - модель не должна выполнять validation;
- модель не должна выполнять normalization;
models/__init__.pyне должен изменяться;- REST и WebSocket должны использовать один Canonical Trade;
- transport boolean не должен попадать во внутренний контракт.
Рассмотренные архитектурные решения
Название идентификатора
Рассматривались:
trade_id
exchange_trade_id
Выбрано:
trade_id
Причины:
- поле остаётся предметным и кратким;
- источник уже фиксируется отдельно через
source; - модель не должна быть жёстко привязана к конкретной бирже;
- имя одинаково применимо к REST и WebSocket.
Название количества
Рассматривались:
quantity
size
Выбрано:
quantity
Причины:
sizeявляется именем WebSocket transport-поля;- REST использует
q; quantityявляется source-independent предметным названием;- имя согласуется с общей терминологией торговых систем.
Представление стороны сделки
Рассматривались:
bool
str
Enum
Выбран отдельный Enum:
class TradeAggressorSide(Enum):
BUY = "buy"
SELL = "sell"
Причины:
- boolean неоднозначен без знания transport-семантики;
- REST
mи WebSocketbuyerимеют противоположную логику; - произвольная строка допускает неконтролируемые значения;
- Enum явно фиксирует допустимый предметный контракт.
Название поля стороны
Рассматривались:
side
buyer
aggressor
aggressor_side
Выбрано:
aggressor_side
Причины:
buyerявляется transport-полем WebSocket;sideможет ошибочно трактоваться как сторона ордера или позиции;aggressor_sideточно отражает участника, инициировавшего исполнение.
Название времени
Рассматривались:
timestamp
trade_time
executed_at
Выбрано:
executed_at
Причины:
- поле описывает предметное время исполнения;
- имя не связано с transport-форматом;
- тип
datetimeуже делает словоtimestampизбыточным; - название согласуется с событийной семантикой сделки.
Окончательно утверждённый контракт
Добавлен предметный Enum:
class TradeAggressorSide(Enum):
BUY = "buy"
SELL = "sell"
Добавлена каноническая immutable-модель:
@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
symbol: str
Канонический идентификатор торгового инструмента.
Модель не выполняет нормализацию символа и не проверяет его существование.
trade_id
trade_id: int
Идентификатор исполненной сделки, предоставленный источником рыночных данных.
В исследованных REST и WebSocket контрактах значение совпадает.
price
price: Decimal
Цена фактического исполнения сделки.
Используется Decimal, чтобы исключить потерю точности, характерную для float.
quantity
quantity: Decimal
Исполненное количество инструмента.
Каноническое имя не зависит от REST-поля q или WebSocket-поля size.
executed_at
executed_at: datetime
Время фактического исполнения сделки.
Преобразование миллисекундного transport timestamp в timezone-aware datetime должно выполняться будущим mapper-слоем, а не моделью.
aggressor_side
aggressor_side: TradeAggressorSide
Сторона участника, инициировавшего исполнение:
TradeAggressorSide.BUY
TradeAggressorSide.SELL
Transport-поля m и buyer не сохраняются внутри Canonical Trade.
source
source: str
Логический источник рыночных данных.
Поле не означает способ доставки и не должно содержать transport-различие вида:
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-поля
Не добавлены:
T
ts
timestamp_ms
Transport timestamp должен быть преобразован в:
executed_at: datetime
до создания Canonical Trade.
Изменённые файлы
В рамках Build изменены или созданы только:
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
Файл:
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-тестах
Создан файл:
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 намеренно не изменяет:
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-кода.
Проверка компиляции
Команды выполнялись из каталога:
~/vsprojects/dzentra_bot/app
Активировано окружение:
source .venv/bin/activate
Выполнена команда:
python -m compileall src
Результат:
успешно
Все каталоги src были обработаны без ошибок компиляции.
Полный regression suite
Выполнена команда:
python -m pytest -q
Результат:
990 passed in 4.74s
Регрессий не обнаружено.
Проверка форматирования
Выполнена команда:
git diff --check
Вывод отсутствует.
Это подтверждает отсутствие:
- trailing whitespace;
- whitespace errors;
- некорректных пустых строк в diff.
Контроль размещения новой модели
Выполнена команда:
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
"TradeAggressorSide\|class Trade" \
src tests
Результат подтвердил наличие нового контракта только в утверждённых местах:
src/market_data/acquisition/models/trade.py
tests/unit/market_data/acquisition/models/test_trade.py
Обнаружены:
class TradeAggressorSide(Enum)
class Trade
и их использования в целевом unit-тесте.
Production-потребители Trade в рамках Build 060.1 не добавлялись.
Состояние Git
Выполнена команда:
git status
Для Build 060.1 зафиксированы:
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
Дополнительно в рабочем дереве присутствует архитектурная контрольная точка:
docs/migrations/build_060_transition_&_architecture_checkpoint.md
Она не является реализационным файлом Build 060.1 и должна учитываться отдельно при формировании commit согласно принятой стратегии репозитория.
Ветка:
main
на момент проверки опережала origin/main на 27 локальных commits.
Это состояние не связано с реализацией Canonical Trade и не изменялось в рамках Build.
Фактический diff модели
В trade.py добавлено 31 строка.
Основной diff:
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 существует третий базовый канонический тип рыночных данных:
Candle
Quote
Trade
Новый контракт:
- не зависит от Dzengi;
- не зависит от REST;
- не зависит от WebSocket;
- не содержит transport-семантику;
- immutable;
- memory-efficient за счёт
slots=True; - использует точные числовые типы;
- выражает сторону агрессора отдельным предметным Enum;
- готов для последующего использования обоими transport-путями.
Целевая будущая схема:
Dzengi REST aggTrades
↓
REST document source
↓
REST schema validation
↓
REST parser
↓
value validation
↓
REST mapper
↓
Trade
и:
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 завершён успешно.
Текущее состояние:
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:
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.