Files
dzentra_bot/docs/migrations/build_060_1.md

903 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.