Files
dzentra_bot/docs/migrations/build_060_2.md

34 KiB
Raw Permalink Blame History

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.