34 KiB
Build 060.2 — REST Trade Transport Model
Engineering Migration Report
Контроль документа
| Свойство | Значение |
|---|---|
| Build | 060.2 |
| Название | REST Trade Transport Model |
| Статус | Завершён |
| Проект | Dzentra |
| Подсистема | Market Data Acquisition |
| Компонент | Trades Feed / Time & Sales |
| Версия документа | 1.0 |
| Дата завершения | 2026-07-18 |
Цель Build
После завершения Build 060.1 в системе появился первый канонический контракт исполненной сделки:
Trade
Следующим этапом необходимо было начать построение REST-пайплайна получения исторических сделок.
Первым слоем этого пайплайна является транспортная модель, описывающая один элемент ответа REST API Dzengi.
Build 060.2 вводит такую модель.
Она должна описывать внешний контракт REST API максимально точно и при этом не выполнять никаких преобразований данных.
Build ограничен исключительно модельным уровнем.
В рамках Build намеренно не реализуются:
- REST document source;
- REST schema validation;
- REST parser;
- REST value validation;
- REST mapper;
- Canonical Trade mapping;
- REST client;
- feed;
- registry;
- protocol;
- acquisition service;
- runtime;
- production-интеграция.
Архитектурный контекст
В Dzentra используется многоуровневая модель обработки рыночных данных.
Каждый внешний источник проходит одинаковую цепочку преобразований:
Transport
↓
Schema Validation
↓
Parser
↓
Value Validation
↓
Mapper
↓
Canonical Model
Такой подход уже используется для остальных типов рыночных данных и теперь переносится на Trades Feed.
Build 060.1 создал внутреннюю предметную модель:
Trade
Однако REST API биржи использует собственный внешний контракт.
Следовательно, непосредственное создание Trade из REST JSON нарушило бы архитектурный принцип разделения ответственности.
Перед появлением parser и mapper необходим отдельный слой, описывающий исключительно внешний формат данных.
Именно этот слой реализуется в Build 060.2.
Исходное состояние
До начала Build 060.2 в проекте уже существовали транспортные модели:
ExchangeInfo
Kline
WebSocket OHLC
Однако транспортной модели агрегированной сделки REST API ещё не существовало.
Файл:
app/src/market_data/acquisition/adapters/dzengi/models.py
содержал модели:
- ExchangeInfo;
- Instrument Filters;
- Kline;
- WebSocket OHLC.
Контракт REST AggTrades отсутствовал.
Unit-тесты также не содержали проверок транспортной модели сделки.
Таким образом Build мог быть реализован как полностью additive change без изменения существующего поведения системы.
Предварительный архитектурный аудит
Перед реализацией были проанализированы:
app/src/market_data/acquisition/adapters/dzengi/models.py
app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py
app/src/market_data/acquisition/models/trade.py
docs/migrations/build_060_1.md
Кроме анализа существующего кода была повторно проверена спецификация REST API агрегированных сделок.
Используемый REST элемент имеет следующий вид:
{
"a": 2134857062,
"p": "64497.25",
"q": "0.005",
"T": 1784218066823,
"m": false
}
Аудит подтвердил следующие выводы:
- транспортная модель должна описывать один элемент массива;
- транспортная модель не должна выполнять parsing;
- транспортная модель не должна выполнять validation;
- транспортная модель не должна выполнять mapping;
- транспортная модель должна использовать существующий тип
DzengiRawNumeric; - timestamp должен оставаться типом
int; - transport boolean должен сохраняться без интерпретации;
- модель должна использовать
@dataclass(frozen=True, slots=True).
Никаких изменений существующих моделей Build не требует.
Рассмотренные архитектурные решения
Перед реализацией были рассмотрены несколько вариантов представления транспортной модели.
Вариант 1
Использовать сразу Canonical Trade.
Например:
Trade(
...
)
Данный вариант был отклонён.
Причины:
- нарушается разделение transport и domain;
- parser начинает зависеть от внутренней модели;
- mapper становится ненужным;
- невозможно отделить ошибки транспорта от ошибок бизнес-преобразования.
Использование Canonical Trade непосредственно на транспортном уровне противоречит архитектуре Acquisition Pipeline.
Вариант 2
Использовать словарь (dict).
Например:
dict[str, Any]
Вариант отклонён.
Причины:
- отсутствует строгий контракт;
- отсутствует типизация;
- снижается читаемость parser;
- невозможно использовать dataclass conventions проекта;
- увеличивается вероятность ошибок при дальнейшем mapping.
Использование dataclass полностью соответствует существующей архитектуре проекта.
Вариант 3
Создать отдельную immutable transport model.
DzengiRestAggTrade
Именно этот вариант утверждён.
Причины:
- полностью отделяет transport layer;
- не смешивает внешний API с предметной моделью;
- сохраняет единый стиль существующих моделей;
- обеспечивает строгую типизацию;
- позволяет независимо развивать parser и mapper.
Почему используется отдельная транспортная модель
REST API является внешним контрактом биржи.
Любое изменение этого API не должно автоматически изменять внутренние предметные модели Dzentra.
Поэтому транспортная модель выполняет только одну задачу:
точно описывает внешний формат данных.
Она ничего не знает о:
- Canonical Trade;
- Decimal;
- datetime;
- TradeAggressorSide;
- предметной логике;
- runtime.
Все последующие преобразования выполняются отдельными Build.
Рассмотренные архитектурные решения (продолжение)
Название модели
Рассматривались несколько вариантов.
Вариант 1
AggTrade
Вариант отклонён.
Причины:
- название не указывает источник данных;
- в дальнейшем появится WebSocket Transport Model;
- становится невозможным различить REST и WebSocket transport-контракты только по имени класса.
Вариант 2
RestTrade
Вариант также отклонён.
Причины:
- не отражает, что используется именно endpoint
aggTrades; - в будущем могут появиться другие REST-модели сделок;
- название становится слишком общим.
Вариант 3
DzengiRestAggTrade
Именно этот вариант утверждён.
Причины:
- явно указывает биржу;
- явно указывает transport;
- явно указывает используемый REST endpoint;
- согласуется с существующими моделями Dzengi;
- исключает неоднозначность при появлении WebSocket Transport Model.
Название идентификатора сделки
Рассматривались следующие варианты.
id
trade_id
aggregate_trade_id
Вариант id
Отклонён.
Причины:
- слишком общее имя;
- не отражает предметную семантику;
- легко спутать с внутренними идентификаторами других объектов.
Вариант trade_id
Отклонён.
Причины:
В Build 060.1 уже утверждено поле
trade_id
для Canonical Trade.
Использование того же имени на транспортном уровне создавало бы ложное ощущение, что REST уже предоставляет канонический идентификатор сделки.
Фактически REST возвращает идентификатор агрегированной сделки конкретного endpoint.
Следовательно транспортная модель должна сохранить эту семантику.
Утверждённое решение
Используется
aggregate_trade_id
Причины:
- отражает фактическое назначение поля;
- полностью соответствует REST API;
- не смешивается с Canonical Trade;
- позволяет Mapper самостоятельно принять решение о дальнейшем использовании этого значения.
Название цены
Рассматривались:
price
execution_price
trade_price
Выбрано:
price
Причины:
- полностью соответствует существующим моделям проекта;
- название предметное;
- не требует дополнительных уточнений;
- одинаково подходит для REST и WebSocket transport.
Название количества
Рассматривались:
quantity
size
volume
size
Отклонено.
Причины:
- это WebSocket transport-термин;
- Build 060.2 описывает REST контракт;
- Canonical Trade уже использует quantity.
volume
Отклонено.
Причины:
- термин неоднозначен;
- volume чаще относится к агрегированному объёму свечи;
- REST API использует количество конкретной сделки.
Утверждённое решение
Используется
quantity
Причины:
- совпадает с предметной терминологией;
- соответствует Canonical Trade;
- одинаково понятно независимо от transport.
Представление времени сделки
Наиболее серьёзное обсуждение вызвал timestamp.
Рассматривались несколько вариантов.
timestamp
executed_at
datetime
trade_time
Использование datetime
Вариант отклонён.
Причины:
Build 060.2 описывает транспортный слой.
REST API возвращает миллисекундный timestamp.
Преобразование его в datetime является задачей Mapper.
Следовательно транспортная модель не должна менять тип данных.
Использование executed_at
Также отклонено.
Причины:
Поле
executed_at
уже является частью Canonical Trade.
Оно описывает предметное время исполнения.
REST же возвращает только транспортный timestamp.
До этапа Mapper это ещё не предметное время.
Использование timestamp
Утверждено.
Поле имеет вид
timestamp: int
Причины:
- полностью отражает транспортный контракт;
- отсутствует скрытая логика преобразования;
- parser получает исходные данные;
- Mapper самостоятельно создаёт datetime.
Представление стороны сделки
REST API содержит поле
"m": false
которое означает
buyer_is_maker
Build 060.2 должен решить, как представить эту информацию.
Вариант Enum
Например
TradeAggressorSide
отклонён.
Причины:
это уже предметная модель Build 060.1.
Transport layer не должен знать о внутреннем Enum.
Вариант bool с именем m
Например
m: bool
отклонён.
Причины:
- имя ничего не говорит разработчику;
- требуется знание документации REST API;
- ухудшается читаемость parser.
Утверждённое решение
Используется
buyer_is_maker: bool
Причины:
- семантика REST полностью сохранена;
- отсутствует неоднозначность;
- Mapper получает всю необходимую информацию;
- транспортная модель остаётся максимально близкой к внешнему контракту.
Почему используется DzengiRawNumeric
REST API возвращает цену и количество строками.
Однако существующие transport-модели Dzentra уже используют общий тип:
DzengiRawNumeric
который допускает:
str
int
float
Использование существующего alias обеспечивает единообразие transport-моделей проекта.
Кроме того, Build 060.2 не должен заниматься проверкой корректности числовых значений.
Эта задача полностью переносится на Build 060.5.
Окончательно утверждённый контракт
После завершения обсуждения был утверждён следующий dataclass.
@dataclass(frozen=True, slots=True)
class DzengiRestAggTrade:
aggregate_trade_id: int
price: DzengiRawNumeric
quantity: DzengiRawNumeric
timestamp: int
buyer_is_maker: bool
Контракт является минимальным.
Он содержит только те поля, которые реально присутствуют внутри одного элемента REST aggTrades.
Никакие дополнительные сведения Build 060.2 не добавляет.
Семантика транспортной модели
Транспортная модель описывает один элемент массива, возвращаемого REST endpoint:
GET /api/v2/aggTrades
Каждое поле модели соответствует одному полю внешнего REST API.
Модель не интерпретирует значения и не выполняет их преобразование.
aggregate_trade_id
aggregate_trade_id: int
Идентификатор агрегированной сделки, возвращаемый REST API.
Соответствует JSON-полю:
a
Build 060.2 не делает предположений относительно:
- глобальной уникальности значения;
- возможности его использования в качестве Canonical Trade ID;
- совпадения с WebSocket Trade ID.
Все подобные решения относятся исключительно к будущему Mapper.
price
price: DzengiRawNumeric
Исходное значение цены сделки.
Соответствует JSON-полю:
p
Build 060.2 намеренно не выполняет:
- преобразование в Decimal;
- проверку формата строки;
- проверку положительности;
- проверку точности.
Все проверки выполняются позже.
quantity
quantity: DzengiRawNumeric
Количество исполненного инструмента.
Соответствует JSON-полю:
q
Модель не проверяет:
- минимальный размер сделки;
- допустимую точность;
- знак значения.
Эти проверки относятся к Value Validation.
timestamp
timestamp: int
Миллисекундная временная метка сделки.
Соответствует JSON-полю:
T
Build 060.2 специально не выполняет:
int
↓
datetime
Подобное преобразование относится к обязанностям Mapper.
Таким образом transport layer всегда содержит исходное значение API.
buyer_is_maker
buyer_is_maker: bool
Соответствует JSON-полю:
m
Поле хранится без каких-либо преобразований.
Build 060.2 намеренно не вычисляет:
TradeAggressorSide
Причина проста.
REST API предоставляет transport boolean.
Canonical Model использует предметный Enum.
Преобразование одного представления в другое является обязанностью Mapper.
Поля, намеренно не включённые в модель
symbol
Поле отсутствует.
Причины:
В REST endpoint:
GET /api/v2/aggTrades
символ инструмента передаётся параметром запроса.
Внутри элемента массива его нет.
Добавление поля
symbol
искусственно изменило бы внешний контракт.
Транспортная модель должна описывать исключительно полученный JSON.
trade_id
Не добавлено.
Причины:
Build 060.1 уже закрепил:
Trade.trade_id
как часть Canonical Model.
На транспортном уровне имеется только:
aggregate_trade_id
Смешивание этих понятий нарушило бы разделение уровней архитектуры.
executed_at
Не добавлено.
Причины:
REST API возвращает:
timestamp
Преобразование в предметное время исполнения должно происходить позже.
Decimal
Не используется.
Причины:
Transport layer не выполняет преобразование типов.
Использование Decimal означало бы скрытый parser.
Это противоречит принятой архитектуре.
datetime
Не используется.
Причины:
Transport layer не занимается интерпретацией времени.
Datetime появляется только после Mapper.
TradeAggressorSide
Не используется.
Причины:
Transport layer ничего не знает о предметной модели.
Булево значение REST API должно сохраняться в исходном виде.
Изменённые файлы
В рамках Build изменены только:
app/src/market_data/acquisition/adapters/dzengi/models.py
app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py
docs/migrations/build_060_2.md
Другие компоненты системы не изменялись.
Изменения в models.py
Файл
app/src/market_data/acquisition/adapters/dzengi/models.py
получил новый immutable dataclass:
DzengiRestAggTrade
Никакие существующие модели не изменялись.
Build добавляет исключительно новый транспортный контракт.
Модель использует уже существующий alias:
DzengiRawNumeric
что обеспечивает единообразие transport layer.
Изменения в unit-тестах
Файл
app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py
расширен тремя новыми тестами.
Они проверяют:
- корректное сохранение полного REST transport contract;
- отсутствие скрытого преобразования числовых значений;
- immutable-поведение;
- использование
slots=True.
Build не изменяет существующие тесты ExchangeInfo и других моделей.
Все новые проверки являются полностью additive.
Что намеренно не изменялось
Build 060.2 специально не изменяет:
app/src/market_data/acquisition/models/trade.py
app/src/market_data/acquisition/models/__init__.py
app/src/market_data/acquisition/runtime/
app/src/market_data/acquisition/validation/
app/src/market_data/acquisition/feeds/
app/src/market_data/acquisition/protocol.py
app/src/market_data/acquisition/service.py
app/src/market_data/acquisition/registry.py
Также Build не включает:
- REST client;
- document source;
- parser;
- schema validation;
- value validation;
- mapper;
- runtime integration;
- feed integration;
- production integration;
- WebSocket изменения;
- изменение Canonical Trade.
Все перечисленные компоненты реализуются отдельными Build согласно утверждённой дорожной карте.
Проверка компиляции
Команды выполнялись из каталога:
~/vsprojects/dzentra_bot/app
Активировано виртуальное окружение:
source .venv/bin/activate
Выполнена команда:
python -m compileall src
Результат:
успешно
Все каталоги проекта были успешно обработаны.
Ошибок компиляции не обнаружено.
Проверка локальных unit-тестов
После реализации транспортной модели были выполнены специализированные тесты:
python -m pytest \
tests/unit/market_data/acquisition/adapters/dzengi/test_models.py \
-q
Результат:
9 passed in 0.03s
Это подтверждает корректность:
- новой транспортной модели;
- существующих моделей ExchangeInfo;
- существующих транспортных типов;
- новых unit-тестов Build 060.2.
Полный regression suite
После завершения Build выполнен полный набор тестов проекта.
Команда:
python -m pytest -q
Результат:
993 passed in 3.00s
Регрессий не обнаружено.
Добавление новой транспортной модели не повлияло на существующее поведение системы.
Проверка форматирования
Выполнена команда:
git diff --check
Вывод отсутствует.
Это подтверждает отсутствие:
- trailing whitespace;
- ошибочных пустых строк;
- нарушений форматирования diff.
Контроль размещения новой модели
После завершения Build транспортная модель агрегированной сделки располагается исключительно в предусмотренном месте:
src/market_data/acquisition/adapters/dzengi/models.py
Её использование подтверждено только специализированными unit-тестами:
tests/unit/market_data/acquisition/adapters/dzengi/test_models.py
Другие компоненты системы Build 060.2 не затрагивает.
Таким образом транспортный контракт остаётся полностью изолированным.
Состояние Git
После завершения Build выполнена команда:
git status
Для Build 060.2 зафиксированы изменения:
modified:
app/src/market_data/acquisition/adapters/dzengi/models.py
modified:
app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py
untracked:
docs/migrations/build_060_2.md
Дополнительно в рабочем каталоге присутствует архитектурная контрольная точка:
docs/migrations/build_060_transition_&_architecture_checkpoint.md
Она не относится к реализации Build 060.2 и учитывается отдельно при формировании commit.
Ветка разработки:
main
опережает origin/main.
Данное состояние не связано с реализацией транспортной модели и не изменялось в рамках Build.
Фактический diff модели
В файл:
app/src/market_data/acquisition/adapters/dzengi/models.py
добавлен новый dataclass:
@dataclass(frozen=True, slots=True)
class DzengiRestAggTrade:
aggregate_trade_id: int
price: DzengiRawNumeric
quantity: DzengiRawNumeric
timestamp: int
buyer_is_maker: bool
Существующие модели:
- ExchangeInfo;
- Instrument Filters;
- Kline;
- WebSocket OHLC;
не изменялись.
Build является полностью additive.
Фактический diff unit-тестов
В файл:
tests/unit/market_data/acquisition/adapters/dzengi/test_models.py
добавлены три новых теста.
Они подтверждают:
- корректность хранения полного транспортного контракта;
- сохранение исходных числовых типов;
- immutable-поведение;
- использование
slots=True.
Все ранее существовавшие тесты продолжают выполняться без изменений.
Архитектурный результат
После завершения Build 060.2 в подсистеме Market Data Acquisition появился первый транспортный контракт агрегированной сделки REST API.
Архитектура REST-пайплайна приобрела следующий вид:
REST API
│
▼
DzengiRestAggTrade
│
▼
Schema Validation
│
▼
Parser
│
▼
Value Validation
│
▼
Mapper
│
▼
Trade
Таким образом Build окончательно разделил:
- внешний REST API;
- внутреннюю предметную модель;
- будущие этапы обработки.
Каждый уровень теперь имеет собственную ответственность.
Влияние на последующие Build
Build 060.2 создаёт фундамент для следующих этапов серии 060.
Прежде всего он позволяет реализовать:
Build 060.3
REST Trade Schema Validation
Новая транспортная модель станет входным контрактом валидатора схемы.
Далее на её основе будут построены:
Build 060.4
REST Trade Parser
Build 060.5
REST Trade Value Validation
Build 060.6
REST Trade Mapper
До появления Build 060.2 реализация этих компонентов была невозможна без нарушения архитектурных принципов.
Критерии завершения
Build 060.2 считается завершённым, поскольку:
- проведён предварительный архитектурный аудит;
- подтверждены существующие conventions transport layer;
- утверждён минимальный REST Transport Contract;
- создан immutable dataclass;
- используется
slots=True; - используется существующий тип
DzengiRawNumeric; - timestamp сохраняется как
int; - transport boolean сохраняется без преобразования;
Decimalнамеренно не используется;datetimeнамеренно не используется;TradeAggressorSideнамеренно не используется;symbolнамеренно отсутствует;- parser не реализован;
- validation не реализована;
- mapper не реализован;
- runtime не изменён;
- compile проверка успешно пройдена;
- локальные unit-тесты успешно пройдены;
- полный regression suite успешно пройден;
git diff --checkне выявил ошибок;- scope Build не расширен.
Итог
Build 060.2 завершён успешно.
Текущее состояние:
DzengiRestAggTrade — реализован
Immutable contract — подтверждён
slots=True — подтверждён
DzengiRawNumeric — используется
timestamp остаётся int
transport boolean сохранён
Parser — отсутствует
Schema Validation — отсутствует
Value Validation — отсутствует
Mapper — отсутствует
Compile check — успешно
Target tests — 9 passed
Full regression suite — 993 passed
Whitespace check — чисто
Production integration — намеренно не выполнялась
Build создал полноценный транспортный контракт агрегированной сделки REST API.
Полученная модель полностью изолирована от внутренней предметной модели и готова к использованию следующими слоями Acquisition Pipeline.
Следующий этап
Следующий Build должен оставаться минимальным и additive.
Его scope должен быть ограничен исключительно проверкой структуры транспортного документа.
Наиболее логичное продолжение серии:
Build 060.3 — REST Trade Schema Validation
На этом этапе будет реализована проверка корректности структуры одного элемента DzengiRestAggTrade без выполнения parsing, преобразования типов и построения канонической модели Trade.