Build 060.1: add canonical Trade model
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
# app/src/market_data/acquisition/models/trade.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
62
app/tests/unit/market_data/acquisition/models/test_trade.py
Normal file
62
app/tests/unit/market_data/acquisition/models/test_trade.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# app/tests/unit/market_data/acquisition/models/test_trade.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import FrozenInstanceError
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.market_data.acquisition.models.trade import Trade, TradeAggressorSide
|
||||||
|
|
||||||
|
|
||||||
|
def _trade(
|
||||||
|
*,
|
||||||
|
aggressor_side: TradeAggressorSide = TradeAggressorSide.BUY,
|
||||||
|
) -> Trade:
|
||||||
|
return Trade(
|
||||||
|
symbol="BTC/USD_LEVERAGE",
|
||||||
|
trade_id=2134857062,
|
||||||
|
price=Decimal("64497.25"),
|
||||||
|
quantity=Decimal("0.005"),
|
||||||
|
executed_at=datetime(2026, 7, 16, 11, 27, 46, 823000, tzinfo=timezone.utc),
|
||||||
|
aggressor_side=aggressor_side,
|
||||||
|
source="dzengi",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_stores_canonical_values() -> None:
|
||||||
|
trade = _trade()
|
||||||
|
|
||||||
|
assert trade.symbol == "BTC/USD_LEVERAGE"
|
||||||
|
assert trade.trade_id == 2134857062
|
||||||
|
assert trade.price == Decimal("64497.25")
|
||||||
|
assert trade.quantity == Decimal("0.005")
|
||||||
|
assert trade.executed_at.tzinfo is timezone.utc
|
||||||
|
assert trade.aggressor_side is TradeAggressorSide.BUY
|
||||||
|
assert trade.source == "dzengi"
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_supports_sell_aggressor_side() -> None:
|
||||||
|
trade = _trade(aggressor_side=TradeAggressorSide.SELL)
|
||||||
|
|
||||||
|
assert trade.aggressor_side is TradeAggressorSide.SELL
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_aggressor_side_has_stable_values() -> None:
|
||||||
|
assert TradeAggressorSide.BUY.value == "buy"
|
||||||
|
assert TradeAggressorSide.SELL.value == "sell"
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_is_frozen() -> None:
|
||||||
|
trade = _trade()
|
||||||
|
|
||||||
|
with pytest.raises(FrozenInstanceError):
|
||||||
|
setattr(trade, "price", Decimal("1"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_uses_slots() -> None:
|
||||||
|
trade = _trade()
|
||||||
|
|
||||||
|
assert not hasattr(trade, "__dict__")
|
||||||
902
docs/migrations/build_060_1.md
Normal file
902
docs/migrations/build_060_1.md
Normal file
@@ -0,0 +1,902 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user