# 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 в системе появился первый канонический контракт исполненной сделки: ```text 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 используется многоуровневая модель обработки рыночных данных. Каждый внешний источник проходит одинаковую цепочку преобразований: ```text Transport ↓ Schema Validation ↓ Parser ↓ Value Validation ↓ Mapper ↓ Canonical Model ``` Такой подход уже используется для остальных типов рыночных данных и теперь переносится на Trades Feed. Build 060.1 создал внутреннюю предметную модель: ```text Trade ``` Однако REST API биржи использует собственный внешний контракт. Следовательно, непосредственное создание `Trade` из REST JSON нарушило бы архитектурный принцип разделения ответственности. Перед появлением parser и mapper необходим отдельный слой, описывающий исключительно внешний формат данных. Именно этот слой реализуется в Build 060.2. --- # Исходное состояние До начала Build 060.2 в проекте уже существовали транспортные модели: ```text ExchangeInfo Kline WebSocket OHLC ``` Однако транспортной модели агрегированной сделки REST API ещё не существовало. Файл: ```text app/src/market_data/acquisition/adapters/dzengi/models.py ``` содержал модели: - ExchangeInfo; - Instrument Filters; - Kline; - WebSocket OHLC. Контракт REST AggTrades отсутствовал. Unit-тесты также не содержали проверок транспортной модели сделки. Таким образом Build мог быть реализован как полностью additive change без изменения существующего поведения системы. --- # Предварительный архитектурный аудит Перед реализацией были проанализированы: ```text 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 элемент имеет следующий вид: ```json { "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. Например: ```python Trade( ... ) ``` Данный вариант был отклонён. Причины: - нарушается разделение transport и domain; - parser начинает зависеть от внутренней модели; - mapper становится ненужным; - невозможно отделить ошибки транспорта от ошибок бизнес-преобразования. Использование Canonical Trade непосредственно на транспортном уровне противоречит архитектуре Acquisition Pipeline. --- ## Вариант 2 Использовать словарь (`dict`). Например: ```python dict[str, Any] ``` Вариант отклонён. Причины: - отсутствует строгий контракт; - отсутствует типизация; - снижается читаемость parser; - невозможно использовать dataclass conventions проекта; - увеличивается вероятность ошибок при дальнейшем mapping. Использование dataclass полностью соответствует существующей архитектуре проекта. --- ## Вариант 3 Создать отдельную immutable transport model. ```python DzengiRestAggTrade ``` Именно этот вариант утверждён. Причины: - полностью отделяет transport layer; - не смешивает внешний API с предметной моделью; - сохраняет единый стиль существующих моделей; - обеспечивает строгую типизацию; - позволяет независимо развивать parser и mapper. --- # Почему используется отдельная транспортная модель REST API является внешним контрактом биржи. Любое изменение этого API не должно автоматически изменять внутренние предметные модели Dzentra. Поэтому транспортная модель выполняет только одну задачу: точно описывает внешний формат данных. Она ничего не знает о: - Canonical Trade; - Decimal; - datetime; - TradeAggressorSide; - предметной логике; - runtime. Все последующие преобразования выполняются отдельными Build. # Рассмотренные архитектурные решения (продолжение) ## Название модели Рассматривались несколько вариантов. ### Вариант 1 ```text AggTrade ``` Вариант отклонён. Причины: - название не указывает источник данных; - в дальнейшем появится WebSocket Transport Model; - становится невозможным различить REST и WebSocket transport-контракты только по имени класса. --- ### Вариант 2 ```text RestTrade ``` Вариант также отклонён. Причины: - не отражает, что используется именно endpoint `aggTrades`; - в будущем могут появиться другие REST-модели сделок; - название становится слишком общим. --- ### Вариант 3 ```text DzengiRestAggTrade ``` Именно этот вариант утверждён. Причины: - явно указывает биржу; - явно указывает transport; - явно указывает используемый REST endpoint; - согласуется с существующими моделями Dzengi; - исключает неоднозначность при появлении WebSocket Transport Model. --- # Название идентификатора сделки Рассматривались следующие варианты. ```text id trade_id aggregate_trade_id ``` --- ## Вариант `id` Отклонён. Причины: - слишком общее имя; - не отражает предметную семантику; - легко спутать с внутренними идентификаторами других объектов. --- ## Вариант `trade_id` Отклонён. Причины: В Build 060.1 уже утверждено поле ```text trade_id ``` для Canonical Trade. Использование того же имени на транспортном уровне создавало бы ложное ощущение, что REST уже предоставляет канонический идентификатор сделки. Фактически REST возвращает идентификатор агрегированной сделки конкретного endpoint. Следовательно транспортная модель должна сохранить эту семантику. --- ## Утверждённое решение Используется ```python aggregate_trade_id ``` Причины: - отражает фактическое назначение поля; - полностью соответствует REST API; - не смешивается с Canonical Trade; - позволяет Mapper самостоятельно принять решение о дальнейшем использовании этого значения. --- # Название цены Рассматривались: ```text price execution_price trade_price ``` Выбрано: ```text price ``` Причины: - полностью соответствует существующим моделям проекта; - название предметное; - не требует дополнительных уточнений; - одинаково подходит для REST и WebSocket transport. --- # Название количества Рассматривались: ```text quantity size volume ``` --- ## size Отклонено. Причины: - это WebSocket transport-термин; - Build 060.2 описывает REST контракт; - Canonical Trade уже использует quantity. --- ## volume Отклонено. Причины: - термин неоднозначен; - volume чаще относится к агрегированному объёму свечи; - REST API использует количество конкретной сделки. --- ## Утверждённое решение Используется ```text quantity ``` Причины: - совпадает с предметной терминологией; - соответствует Canonical Trade; - одинаково понятно независимо от transport. --- # Представление времени сделки Наиболее серьёзное обсуждение вызвал timestamp. Рассматривались несколько вариантов. ```text timestamp executed_at datetime trade_time ``` --- ## Использование datetime Вариант отклонён. Причины: Build 060.2 описывает транспортный слой. REST API возвращает миллисекундный timestamp. Преобразование его в datetime является задачей Mapper. Следовательно транспортная модель не должна менять тип данных. --- ## Использование executed_at Также отклонено. Причины: Поле ```text executed_at ``` уже является частью Canonical Trade. Оно описывает предметное время исполнения. REST же возвращает только транспортный timestamp. До этапа Mapper это ещё не предметное время. --- ## Использование timestamp Утверждено. Поле имеет вид ```python timestamp: int ``` Причины: - полностью отражает транспортный контракт; - отсутствует скрытая логика преобразования; - parser получает исходные данные; - Mapper самостоятельно создаёт datetime. --- # Представление стороны сделки REST API содержит поле ```json "m": false ``` которое означает ```text buyer_is_maker ``` Build 060.2 должен решить, как представить эту информацию. --- ## Вариант Enum Например ```python TradeAggressorSide ``` отклонён. Причины: это уже предметная модель Build 060.1. Transport layer не должен знать о внутреннем Enum. --- ## Вариант bool с именем m Например ```python m: bool ``` отклонён. Причины: - имя ничего не говорит разработчику; - требуется знание документации REST API; - ухудшается читаемость parser. --- ## Утверждённое решение Используется ```python buyer_is_maker: bool ``` Причины: - семантика REST полностью сохранена; - отсутствует неоднозначность; - Mapper получает всю необходимую информацию; - транспортная модель остаётся максимально близкой к внешнему контракту. --- # Почему используется DzengiRawNumeric REST API возвращает цену и количество строками. Однако существующие transport-модели Dzentra уже используют общий тип: ```python DzengiRawNumeric ``` который допускает: ```text str int float ``` Использование существующего alias обеспечивает единообразие transport-моделей проекта. Кроме того, Build 060.2 не должен заниматься проверкой корректности числовых значений. Эта задача полностью переносится на Build 060.5. --- # Окончательно утверждённый контракт После завершения обсуждения был утверждён следующий dataclass. ```python @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: ```text GET /api/v2/aggTrades ``` Каждое поле модели соответствует одному полю внешнего REST API. Модель не интерпретирует значения и не выполняет их преобразование. --- ## aggregate_trade_id ```python aggregate_trade_id: int ``` Идентификатор агрегированной сделки, возвращаемый REST API. Соответствует JSON-полю: ```text a ``` Build 060.2 не делает предположений относительно: - глобальной уникальности значения; - возможности его использования в качестве Canonical Trade ID; - совпадения с WebSocket Trade ID. Все подобные решения относятся исключительно к будущему Mapper. --- ## price ```python price: DzengiRawNumeric ``` Исходное значение цены сделки. Соответствует JSON-полю: ```text p ``` Build 060.2 намеренно не выполняет: - преобразование в Decimal; - проверку формата строки; - проверку положительности; - проверку точности. Все проверки выполняются позже. --- ## quantity ```python quantity: DzengiRawNumeric ``` Количество исполненного инструмента. Соответствует JSON-полю: ```text q ``` Модель не проверяет: - минимальный размер сделки; - допустимую точность; - знак значения. Эти проверки относятся к Value Validation. --- ## timestamp ```python timestamp: int ``` Миллисекундная временная метка сделки. Соответствует JSON-полю: ```text T ``` Build 060.2 специально не выполняет: ```text int ↓ datetime ``` Подобное преобразование относится к обязанностям Mapper. Таким образом transport layer всегда содержит исходное значение API. --- ## buyer_is_maker ```python buyer_is_maker: bool ``` Соответствует JSON-полю: ```text m ``` Поле хранится без каких-либо преобразований. Build 060.2 намеренно не вычисляет: ```text TradeAggressorSide ``` Причина проста. REST API предоставляет transport boolean. Canonical Model использует предметный Enum. Преобразование одного представления в другое является обязанностью Mapper. --- # Поля, намеренно не включённые в модель ## symbol Поле отсутствует. Причины: В REST endpoint: ```text GET /api/v2/aggTrades ``` символ инструмента передаётся параметром запроса. Внутри элемента массива его нет. Добавление поля ```text symbol ``` искусственно изменило бы внешний контракт. Транспортная модель должна описывать исключительно полученный JSON. --- ## trade_id Не добавлено. Причины: Build 060.1 уже закрепил: ```text Trade.trade_id ``` как часть Canonical Model. На транспортном уровне имеется только: ```text aggregate_trade_id ``` Смешивание этих понятий нарушило бы разделение уровней архитектуры. --- ## executed_at Не добавлено. Причины: REST API возвращает: ```text timestamp ``` Преобразование в предметное время исполнения должно происходить позже. --- ## Decimal Не используется. Причины: Transport layer не выполняет преобразование типов. Использование Decimal означало бы скрытый parser. Это противоречит принятой архитектуре. --- ## datetime Не используется. Причины: Transport layer не занимается интерпретацией времени. Datetime появляется только после Mapper. --- ## TradeAggressorSide Не используется. Причины: Transport layer ничего не знает о предметной модели. Булево значение REST API должно сохраняться в исходном виде. --- # Изменённые файлы В рамках Build изменены только: ```text 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 Файл ```text app/src/market_data/acquisition/adapters/dzengi/models.py ``` получил новый immutable dataclass: ```python DzengiRestAggTrade ``` Никакие существующие модели не изменялись. Build добавляет исключительно новый транспортный контракт. Модель использует уже существующий alias: ```python DzengiRawNumeric ``` что обеспечивает единообразие transport layer. --- # Изменения в unit-тестах Файл ```text app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py ``` расширен тремя новыми тестами. Они проверяют: - корректное сохранение полного REST transport contract; - отсутствие скрытого преобразования числовых значений; - immutable-поведение; - использование `slots=True`. Build не изменяет существующие тесты ExchangeInfo и других моделей. Все новые проверки являются полностью additive. --- # Что намеренно не изменялось Build 060.2 специально не изменяет: ```text 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 согласно утверждённой дорожной карте. # Проверка компиляции Команды выполнялись из каталога: ```text ~/vsprojects/dzentra_bot/app ``` Активировано виртуальное окружение: ```bash source .venv/bin/activate ``` Выполнена команда: ```bash python -m compileall src ``` Результат: ```text успешно ``` Все каталоги проекта были успешно обработаны. Ошибок компиляции не обнаружено. --- # Проверка локальных unit-тестов После реализации транспортной модели были выполнены специализированные тесты: ```bash python -m pytest \ tests/unit/market_data/acquisition/adapters/dzengi/test_models.py \ -q ``` Результат: ```text 9 passed in 0.03s ``` Это подтверждает корректность: - новой транспортной модели; - существующих моделей ExchangeInfo; - существующих транспортных типов; - новых unit-тестов Build 060.2. --- # Полный regression suite После завершения Build выполнен полный набор тестов проекта. Команда: ```bash python -m pytest -q ``` Результат: ```text 993 passed in 3.00s ``` Регрессий не обнаружено. Добавление новой транспортной модели не повлияло на существующее поведение системы. --- # Проверка форматирования Выполнена команда: ```bash git diff --check ``` Вывод отсутствует. Это подтверждает отсутствие: - trailing whitespace; - ошибочных пустых строк; - нарушений форматирования diff. --- # Контроль размещения новой модели После завершения Build транспортная модель агрегированной сделки располагается исключительно в предусмотренном месте: ```text src/market_data/acquisition/adapters/dzengi/models.py ``` Её использование подтверждено только специализированными unit-тестами: ```text tests/unit/market_data/acquisition/adapters/dzengi/test_models.py ``` Другие компоненты системы Build 060.2 не затрагивает. Таким образом транспортный контракт остаётся полностью изолированным. --- # Состояние Git После завершения Build выполнена команда: ```bash git status ``` Для Build 060.2 зафиксированы изменения: ```text 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 ``` Дополнительно в рабочем каталоге присутствует архитектурная контрольная точка: ```text docs/migrations/build_060_transition_&_architecture_checkpoint.md ``` Она не относится к реализации Build 060.2 и учитывается отдельно при формировании commit. Ветка разработки: ```text main ``` опережает `origin/main`. Данное состояние не связано с реализацией транспортной модели и не изменялось в рамках Build. --- # Фактический diff модели В файл: ```text app/src/market_data/acquisition/adapters/dzengi/models.py ``` добавлен новый dataclass: ```python @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-тестов В файл: ```text tests/unit/market_data/acquisition/adapters/dzengi/test_models.py ``` добавлены три новых теста. Они подтверждают: - корректность хранения полного транспортного контракта; - сохранение исходных числовых типов; - immutable-поведение; - использование `slots=True`. Все ранее существовавшие тесты продолжают выполняться без изменений. --- # Архитектурный результат После завершения Build 060.2 в подсистеме Market Data Acquisition появился первый транспортный контракт агрегированной сделки REST API. Архитектура REST-пайплайна приобрела следующий вид: ```text REST API │ ▼ DzengiRestAggTrade │ ▼ Schema Validation │ ▼ Parser │ ▼ Value Validation │ ▼ Mapper │ ▼ Trade ``` Таким образом Build окончательно разделил: - внешний REST API; - внутреннюю предметную модель; - будущие этапы обработки. Каждый уровень теперь имеет собственную ответственность. --- # Влияние на последующие Build Build 060.2 создаёт фундамент для следующих этапов серии 060. Прежде всего он позволяет реализовать: ```text Build 060.3 REST Trade Schema Validation ``` Новая транспортная модель станет входным контрактом валидатора схемы. Далее на её основе будут построены: ```text 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 завершён успешно.** Текущее состояние: ```text 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 должен быть ограничен исключительно проверкой структуры транспортного документа. Наиболее логичное продолжение серии: ```text Build 060.3 — REST Trade Schema Validation ``` На этом этапе будет реализована проверка корректности структуры одного элемента `DzengiRestAggTrade` без выполнения parsing, преобразования типов и построения канонической модели `Trade`.