Files
dzentra_bot/docs/migrations/build_060_1.md

21 KiB
Raw Blame History

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 должен следовать conventions Candle и 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 и WebSocket buyer имеют противоположную логику;
  • произвольная строка допускает неконтролируемые значения;
  • 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.