60 KiB
Build 060.6 — REST Trade Mapper
Engineering Migration Report
Контроль документа
| Свойство | Значение |
|---|---|
| Build | 060.6 |
| Название | REST Trade Mapper |
| Статус | Завершён |
| Проект | Dzentra |
| Подсистема | Market Data Acquisition |
| Компонент | Trades Feed / Time & Sales |
| Версия документа | 1.0 |
| Дата завершения | 2026-07-19 |
Цель Build
После завершения предыдущих Build серии 060 в системе уже существовали:
- транспортная модель агрегированной сделки REST API;
- проверка структуры REST-документа;
- parser транспортной модели;
- проверка корректности значений transport-объектов.
Однако весь полученный конвейер всё ещё работал исключительно с транспортными сущностями биржи.
После окончания Build 060.5 полный pipeline выглядел следующим образом:
REST JSON
│
▼
Schema Validation
│
▼
Parser
│
▼
tuple[DzengiRestAggTrade]
│
▼
Value Validation
Полученный результат был пригоден только для внутренних компонентов Acquisition Layer.
Остальная система Dzentra не должна зависеть от формата REST API конкретной биржи.
Следовательно необходим последний уровень преобразования, переводящий транспортную модель во внутреннюю предметную модель проекта.
Именно эту задачу решает Build 060.6.
В рамках данного Build реализуется исключительно Mapper.
Он завершает построение первого полноценного REST Pipeline для получения исторических сделок.
Build намеренно не включает:
- REST client;
- REST endpoint;
- REST polling;
- Feed;
- Registry;
- Runtime integration;
- Acquisition Service;
- Time & Sales Feed;
- хранение истории сделок;
- дедупликацию;
- сортировку;
- агрегацию;
- объединение REST и WebSocket Trades;
- production-интеграцию.
Все перечисленные задачи относятся к последующим Build.
Архитектурный контекст
В Dzentra принята единая многоуровневая модель обработки любых внешних рыночных данных.
Независимо от источника данные проходят одинаковые стадии обработки.
Transport
│
▼
Schema Validation
│
▼
Parser
│
▼
Value Validation
│
▼
Mapper
│
▼
Canonical Model
Такая архитектура уже используется для:
- Instrument;
- Quote;
- Candle;
- WebSocket Candle.
Build 060.6 переносит этот же принцип на REST Trades.
После его завершения REST Trade Pipeline становится полностью согласованным с остальными Acquisition Pipeline проекта.
Исходное состояние
До начала Build 060.6 проект уже содержал:
Canonical Trade
DzengiRestAggTrade
TradeSchemaError
TradeParseError
TradeValueError
validate_rest_agg_trades_schema()
parse_rest_agg_trades()
validate_rest_agg_trade_values()
Таким образом все необходимые проверки транспортного уровня уже были реализованы.
Отсутствовал только переход к внутренней модели.
Файл:
app/src/market_data/acquisition/adapters/dzengi/mapper.py
содержал mapper для:
- Instrument;
- REST Quote;
- WebSocket Quote;
- REST Candle;
- WebSocket Candle.
Однако Trade Mapper отсутствовал полностью.
Также отсутствовало специализированное исключение уровня Mapping.
Unit-тесты не содержали проверок преобразования REST Trade в Canonical Trade.
Следовательно Build мог быть реализован как полностью additive change без изменения существующего поведения системы.
Предварительный архитектурный аудит
Перед началом реализации были повторно проанализированы:
app/src/market_data/acquisition/adapters/dzengi/mapper.py
app/src/market_data/acquisition/adapters/dzengi/models.py
app/src/market_data/acquisition/models/trade.py
app/src/market_data/acquisition/exceptions.py
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
docs/migrations/build_060_1.md
docs/migrations/build_060_2.md
docs/migrations/build_060_3.md
docs/migrations/build_060_4.md
docs/migrations/build_060_5.md
Кроме анализа существующего кода была повторно проверена вся архитектура Acquisition Pipeline.
Аудит подтвердил следующие выводы:
- транспортная модель уже полностью сформирована;
- parser завершён;
- value validation завершена;
- Canonical Trade уже утверждён Build 060.1;
- mapper должен стать единственной точкой создания Canonical Trade;
- mapper не должен изменять архитектуру предыдущих Build;
- mapper должен использовать существующие соглашения остальных mapper проекта;
- mapper должен возвращать immutable tuple;
- mapper должен быть полностью независимым от REST JSON.
Никакие изменения предыдущих Build не требуются.
Архитектурная задача Build
Главная задача Build заключается не в преобразовании типов.
Его настоящая задача — завершить разделение двух независимых моделей данных.
До Build 060.6 система всё ещё использовала транспортную модель биржи:
DzengiRestAggTrade
После Build система должна работать исключительно с внутренней моделью:
Trade
Таким образом все последующие компоненты Dzentra полностью перестают зависеть от REST API Dzengi.
Это является одним из ключевых принципов архитектуры всей подсистемы Market Data Acquisition.
Рассмотренные архитектурные решения
Перед реализацией были рассмотрены несколько вариантов построения последнего слоя REST Pipeline.
Вариант 1
Использовать транспортную модель непосредственно внутри системы.
Например:
DzengiRestAggTrade
вместо
Trade
Данный вариант был отклонён.
Причины:
- транспортная модель описывает внешний REST API;
- любое изменение биржи приведёт к изменению внутренней модели системы;
- нарушается принцип изоляции внешнего контракта;
- остальные Acquisition Pipeline уже используют Canonical Models;
- появляется зависимость бизнес-логики от конкретной биржи.
Использование транспортной модели за пределами Acquisition Layer противоречит утверждённой архитектуре Dzentra.
Вариант 2
Создавать Canonical Trade непосредственно в Parser.
Например:
REST JSON
│
▼
Parser
│
▼
Trade
На первый взгляд такой подход уменьшает количество слоёв.
Однако он был отклонён.
Причины:
- parser начинает выполнять две независимые задачи;
- смешиваются parsing и mapping;
- невозможно независимо тестировать parser;
- parser начинает зависеть от предметной модели;
- нарушается единообразие остальных Acquisition Pipeline.
В Dzentra Parser отвечает исключительно за преобразование документа в транспортную модель.
Создание предметных объектов относится только к Mapper.
Вариант 3
Объединить Value Validation и Mapper.
Например:
Value Validation
│
▼
Trade
Вариант также был отклонён.
Причины:
Value Validation отвечает исключительно за проверку корректности значений.
Mapper отвечает исключительно за преобразование одной модели данных в другую.
Объединение этих задач привело бы к нарушению принципа Single Responsibility и существенно усложнило бы повторное использование Validation Layer.
Поэтому оба слоя остаются полностью независимыми.
Рассмотренные архитектурные решения (продолжение)
Назначение Mapper
Перед реализацией Build был повторно рассмотрен вопрос:
нужен ли отдельный Mapper вообще?
На первый взгляд после завершения Value Validation транспортная модель уже содержит корректные данные.
Возникает естественный вопрос:
почему не использовать её непосредственно в остальной системе?
После анализа было подтверждено, что Mapper остаётся обязательным элементом архитектуры.
Причины:
- транспортная модель принадлежит внешнему API;
- предметная модель принадлежит Dzentra;
- транспортная модель может измениться вместе с REST API;
- предметная модель должна оставаться стабильной;
- последующие уровни системы не должны знать о существовании Dzengi.
Таким образом Mapper является границей между внешним контрактом и внутренней моделью предметной области.
Почему Mapper создаёт Canonical Trade
Build 060.1 ввёл единственную утверждённую модель исполненной сделки:
Trade
Начиная с этого момента все остальные подсистемы Dzentra должны работать исключительно с ней.
После завершения Build 060.6 никакие компоненты выше Acquisition Layer больше не должны использовать:
DzengiRestAggTrade
Тем самым достигается полная независимость внутренних модулей от конкретной биржи.
Почему Mapper не выполняет Validation
Несмотря на то что Mapper повторно преобразует некоторые поля, он намеренно не занимается их проверкой.
Например:
price
quantity
timestamp
к моменту вызова Mapper уже прошли:
- Schema Validation;
- Parser;
- Value Validation.
Следовательно Mapper получает гарантированно корректные данные.
Повторная проверка:
- увеличила бы объём кода;
- ухудшила читаемость;
- нарушила бы разделение ответственности;
- усложнила сопровождение.
Именно поэтому Build 060.6 использует уже проверенные значения без дополнительной бизнес-валидации.
Почему Mapper всё же создаёт Decimal
Во время проектирования обсуждался вопрос:
если Value Validation уже проверила корректность значения,
нужно ли повторно выполнять:
str
↓
Decimal
Ответ — да.
Причина заключается в разделении обязанностей.
Value Validation подтверждает только возможность такого преобразования.
Она не создаёт предметные объекты.
Создание экземпляров Decimal относится исключительно к этапу построения Canonical Model.
Поэтому преобразование выполняется именно внутри Mapper.
Почему Decimal не создаётся раньше
Рассматривался альтернативный вариант.
После завершения Value Validation транспортная модель могла бы уже содержать:
Decimal
вместо
str
Данный вариант был отклонён.
Причины:
транспортная модель должна максимально точно отражать внешний REST API.
REST API возвращает строки.
Следовательно transport layer также обязан использовать строки.
Любое изменение типа означало бы скрытый Mapping.
Это нарушило бы архитектуру Acquisition Pipeline.
Почему создаётся datetime
REST API возвращает время сделки в виде:
timestamp (milliseconds)
Предметная модель использует:
datetime
Следовательно именно Mapper отвечает за переход:
timestamp
↓
datetime
Это преобразование является частью формирования внутренней модели.
Никакие предыдущие Build его не выполняют.
Почему используется UTC
При проектировании обсуждались несколько вариантов представления времени.
Рассматривались:
- naive datetime;
- локальное время системы;
- UTC datetime.
Утверждён третий вариант.
Причины:
- все остальные модели проекта используют UTC;
- отсутствует зависимость от локальной временной зоны;
- упрощается дальнейшее сравнение событий;
- полностью исключаются ошибки перехода между часовыми поясами.
Следовательно Mapper всегда создаёт timezone-aware UTC datetime.
Почему Mapper получает symbol параметром
Во время проектирования обсуждался ещё один вопрос.
REST endpoint вызывается примерно следующим образом:
GET /api/v2/aggTrades?symbol=BTCUSDT
При этом внутри каждого элемента массива символ отсутствует.
Рассматривались несколько вариантов.
Вариант 1
Добавить symbol в транспортную модель.
Отклонён.
Причины:
это изменило бы внешний контракт REST API;
транспортная модель перестала бы точно описывать полученный JSON.
Вариант 2
Не сохранять symbol вообще.
Отклонён.
Причины:
Canonical Trade всегда должен содержать символ инструмента.
Без него сделка становится неполной.
Утверждённое решение
Mapper принимает symbol отдельным параметром.
Например:
map_dzengi_rest_agg_trades_to_trades(
trades,
symbol="BTC/USDT",
)
Такое решение позволяет одновременно:
- сохранить чистоту транспортной модели;
- корректно построить Canonical Trade.
Почему выполняется strip()
Mapper использует:
symbol.strip()
Это решение также обсуждалось отдельно.
Причины:
символ инструмента может поступать из внешнего слоя;
внешний слой потенциально способен содержать случайные пробелы;
Canonical Model должна содержать нормализованное значение.
При этом Mapper не изменяет сам идентификатор инструмента.
Удаляются только внешние пробельные символы.
Почему buyer_is_maker преобразуется в TradeAggressorSide
REST API использует поле:
buyer_is_maker
Это транспортная характеристика конкретного REST endpoint.
Предметная модель Dzentra использует другое понятие:
TradeAggressorSide
Следовательно Mapper обязан выполнить интерпретацию транспортного признака.
Используется следующее соответствие:
buyer_is_maker = False
↓
BUY
buyer_is_maker = True
↓
SELL
Таким образом транспортная особенность REST API полностью исчезает после завершения Mapper.
Дальнейшие компоненты системы работают исключительно с предметной моделью.
Почему Mapper не сортирует сделки
Рассматривалась возможность автоматически сортировать сделки по времени исполнения.
Вариант отклонён.
Причины:
Mapper не должен менять порядок данных;
Mapper не должен принимать бизнес-решения;
Mapper обязан сохранять последовательность объектов, полученную от предыдущего слоя.
Любая сортировка относится к более высокому уровню обработки данных.
Поэтому Build 060.6 намеренно сохраняет исходный порядок элементов.
Почему Mapper не удаляет дубликаты
Аналогично обсуждалась автоматическая дедупликация сделок.
Она также была отклонена.
Причины:
Mapper не знает источник появления дубликатов;
Mapper не знает стратегию обработки повторных сделок;
REST и WebSocket могут использовать разные правила объединения данных.
Следовательно дедупликация относится исключительно к будущим компонентам Feed.
Build 060.6 не содержит подобной логики.
Новый публичный API
Build добавляет новую публичную функцию:
map_dzengi_rest_agg_trades_to_trades()
Назначение функции:
- принимает набор проверенных транспортных моделей;
- создаёт immutable набор Canonical Trade;
- сохраняет порядок входных элементов;
- не изменяет семантику данных.
Сигнатура имеет вид:
tuple[DzengiRestAggTrade]
│
▼
tuple[Trade]
Функция является единственной публичной точкой входа Mapper.
Все остальные функции Build имеют внутренний характер и используются исключительно как вспомогательные.
Новое исключение
Build 060.6 вводит новый специализированный тип исключения:
TradeMappingError
Назначение исключения — локализовать ошибки уровня Mapping.
Таким образом полностью завершается иерархия ошибок REST Trade Pipeline:
TradeSchemaError
│
TradeParseError
│
TradeValueError
│
TradeMappingError
Каждый этап обработки теперь имеет собственный тип исключений.
Это позволяет точно определить уровень возникновения ошибки и существенно упрощает диагностику при дальнейшем развитии системы.
Семантика Mapper
После завершения Build 060.6 Mapper становится единственной точкой, в которой транспортная модель преобразуется во внутреннюю предметную модель Dzentra.
На вход Mapper получает исключительно полностью подготовленные объекты.
Они уже прошли:
- проверку структуры;
- parser;
- проверку обязательных полей;
- проверку типов;
- проверку диапазонов;
- проверку корректности числовых значений.
Следовательно Mapper может полностью сосредоточиться исключительно на преобразовании моделей данных.
Его работа сводится к последовательному созданию каждого экземпляра Canonical Trade.
Общая схема преобразования
Для каждой транспортной модели выполняется одинаковая последовательность действий.
DzengiRestAggTrade
│
├──────── aggregate_trade_id ───────► trade_id
│
├──────── price ────────────────────► Decimal
│
├──────── quantity ─────────────────► Decimal
│
├──────── timestamp ────────────────► datetime (UTC)
│
├──────── buyer_is_maker ───────────► TradeAggressorSide
│
└──────── symbol (argument) ────────► symbol
│
▼
Trade
После создания объекта транспортная модель больше не используется.
Вся дальнейшая работа системы выполняется исключительно с Canonical Trade.
Преобразование aggregate_trade_id
Транспортная модель содержит поле:
aggregate_trade_id
Предметная модель использует:
trade_id
Во время проектирования обсуждался вопрос:
нужно ли сохранять первоначальное название?
Решение было отрицательным.
Причины:
Canonical Trade не должен зависеть от конкретного REST endpoint.
Внутренняя модель должна использовать единый термин:
trade_id
Следовательно Mapper выполняет простое переименование поля.
Никаких дополнительных преобразований значения не производится.
Преобразование price
REST API возвращает цену как транспортное числовое значение.
После завершения предыдущих Build уже известно, что значение:
- существует;
- корректно;
- конечно;
- может быть преобразовано в Decimal.
Mapper создаёт окончательное значение:
Decimal
Полученный объект помещается непосредственно в Canonical Trade.
После этого транспортное представление полностью исчезает.
Преобразование quantity
Поле quantity проходит абсолютно аналогичную обработку.
Mapper получает проверенное транспортное значение.
Создаётся:
Decimal
Полученный экземпляр используется внутри Canonical Trade.
Дополнительные проверки не выполняются.
Преобразование timestamp
Наиболее важным преобразованием Build является обработка времени сделки.
REST API использует миллисекундный Unix Timestamp.
Например:
1783537921471
Внутренняя модель использует:
datetime
Mapper выполняет преобразование:
milliseconds
↓
seconds
↓
datetime
↓
UTC
После завершения Mapper транспортное представление времени полностью исчезает.
Все последующие компоненты работают только с datetime.
Преобразование buyer_is_maker
REST API использует транспортный признак:
buyer_is_maker
Однако для анализа рынка подобное представление неудобно.
Предметная модель использует понятие стороны агрессора.
Поэтому Mapper выполняет следующую интерпретацию.
Если:
buyer_is_maker = False
создаётся
TradeAggressorSide.BUY
Если:
buyer_is_maker = True
создаётся
TradeAggressorSide.SELL
В результате все остальные компоненты Dzentra работают уже не с транспортным булевым признаком, а с предметной характеристикой сделки.
Преобразование symbol
Canonical Trade всегда содержит символ инструмента.
Поскольку REST API не включает его в элементы массива,
Mapper получает его отдельным аргументом.
Перед сохранением выполняется:
strip()
Это гарантирует отсутствие случайных пробелов.
Никаких других преобразований идентификатора инструмента не производится.
Поле source
Build 060.1 закрепил наличие поля:
source
внутри Canonical Trade.
Mapper устанавливает значение:
dzengi
Причины такого решения:
- источник данных известен заранее;
- он не зависит от содержимого REST ответа;
- остальные компоненты могут определить происхождение сделки без анализа transport layer.
В дальнейшем аналогичный подход позволит объединять сделки, поступающие из различных источников.
Immutable-поведение
Build 060.6 полностью сохраняет принятые архитектурные принципы проекта.
Каждый созданный объект:
Trade
остаётся immutable.
После завершения Mapper сделка не может быть изменена.
Это обеспечивает:
- предсказуемость обработки;
- отсутствие скрытых побочных эффектов;
- безопасную передачу объекта между подсистемами;
- возможность повторного использования экземпляров без копирования.
Внутренние функции Mapper
Публичный API Build состоит только из одной функции.
Остальные функции являются внутренними.
_map_dzengi_rest_agg_trade()
Выполняет преобразование одной транспортной модели в один Canonical Trade.
Она инкапсулирует всю логику создания предметного объекта.
Использование отдельной функции позволяет:
- упростить публичный API;
- повысить читаемость кода;
- локализовать логику преобразования одной сделки;
- упростить unit-тестирование.
_required_trade_decimal()
Инкапсулирует создание объекта Decimal.
Несмотря на то что данные уже проверены,
Build намеренно использует отдельную функцию.
Причины:
- единообразие с существующими mapper проекта;
- локализация обработки ошибок Decimal;
- возможность последующего рефакторинга без изменения публичного API.
_trade_timestamp_ms_to_utc_datetime()
Полностью изолирует преобразование времени.
Она отвечает исключительно за переход:
timestamp (milliseconds)
↓
UTC datetime
Такое разделение делает Mapper проще для чтения и уменьшает связность отдельных операций.
_trade_aggressor_side()
Инкапсулирует преобразование транспортного признака:
buyer_is_maker
в предметную модель:
TradeAggressorSide
Выделение отдельной функции позволяет:
- сделать основной Mapper более компактным;
- централизовать интерпретацию транспортного признака;
- исключить дублирование логики при дальнейшем развитии проекта.
Что намеренно не делает Mapper
Build 060.6 специально не реализует никакой дополнительной бизнес-логики.
Mapper намеренно:
не сортирует сделки;
не удаляет дубликаты;
не объединяет REST и WebSocket данные;
не выполняет агрегацию;
не рассчитывает статистику;
не изменяет цену;
не изменяет количество;
не изменяет идентификаторы;
не анализирует последовательность сделок;
не определяет рыночную структуру;
не вычисляет объёмы;
не создаёт свечи;
не формирует Time & Sales Feed;
не взаимодействует с Runtime;
не сохраняет данные.
Все перечисленные задачи относятся к последующим уровням архитектуры и не входят в ответственность Mapper.
Благодаря этому Build 060.6 остаётся небольшим, полностью изолированным и строго соответствует принципу Single Responsibility.
Изменённые файлы
В рамках Build 060.6 были изменены только три файла проекта.
app/src/market_data/acquisition/adapters/dzengi/mapper.py
app/src/market_data/acquisition/exceptions.py
app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
Кроме того, подготовлена инженерная документация Build:
docs/migrations/build_060_6.md
Никакие другие компоненты проекта не изменялись.
Build полностью соответствует ранее утверждённому принципу минимального локального изменения (Local Additive Change).
Изменения в exceptions.py
Файл:
app/src/market_data/acquisition/exceptions.py
получил новый специализированный тип исключения:
TradeMappingError
До Build 060.6 цепочка исключений выглядела следующим образом:
TradeSchemaError
↓
TradeParseError
↓
TradeValueError
После завершения Build архитектура стала полностью симметричной:
TradeSchemaError
↓
TradeParseError
↓
TradeValueError
↓
TradeMappingError
Теперь каждый слой REST Trade Pipeline имеет собственный тип ошибок.
Это значительно упрощает диагностику при сопровождении системы.
Назначение TradeMappingError
Несмотря на то что Mapper работает только с уже проверенными объектами,
полностью исключить ошибки преобразования невозможно.
Например:
- невозможность создать Decimal;
- невозможность построить datetime;
- внутренние ошибки преобразования.
Во всех подобных случаях используется исключительно:
TradeMappingError
Таким образом исключения предыдущих уровней не используются повторно.
Каждый слой отвечает только за собственные ошибки.
Изменения в mapper.py
Build 060.6 расширяет существующий файл:
app/src/market_data/acquisition/adapters/dzengi/mapper.py
Никакие существующие mapper не изменялись.
Все новые функции были добавлены дополнительно.
Таким образом Build полностью сохраняет обратную совместимость.
Новый публичный API
В файл добавлена функция:
map_dzengi_rest_agg_trades_to_trades()
Именно она становится официальной точкой входа Trade Mapper.
Она:
- принимает immutable tuple транспортных моделей;
- создаёт immutable tuple Canonical Trade;
- сохраняет порядок элементов;
- не изменяет семантику данных;
- не выполняет дополнительную validation.
После Build именно эта функция должна использоваться всеми последующими компонентами Acquisition Layer.
Внутренние функции Mapper
Кроме публичного API Build добавляет четыре внутренних функции.
_map_dzengi_rest_agg_trade()
_required_trade_decimal()
_trade_timestamp_ms_to_utc_datetime()
_trade_aggressor_side()
Каждая из них отвечает только за одну небольшую операцию.
Такое разделение полностью соответствует существующему стилю остальных mapper проекта.
Почему используется несколько небольших функций
Во время проектирования обсуждалась возможность реализации всего Mapper одной функцией.
Например:
map_dzengi_rest_agg_trades_to_trades(...)
которая сразу содержала бы всю логику.
Такой вариант был отклонён.
Причины:
- ухудшается читаемость;
- возрастает размер функции;
- усложняется unit-тестирование;
- возрастает вероятность случайных ошибок при дальнейшем сопровождении.
Использование небольших специализированных функций делает код значительно проще для сопровождения.
Изменения в unit-тестах
Файл:
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
получил новый раздел тестов Trade Mapper.
До Build 060.6 данный файл содержал проверки:
- Instrument Mapper;
- Quote Mapper;
- Candle Mapper.
После завершения Build в него были добавлены специализированные проверки преобразования REST Trade.
Существующие тесты не изменялись.
Build полностью additive.
Проверка корректного Mapping
Наиболее важной частью Build являются тесты успешного преобразования.
Они подтверждают корректность создания:
Trade
из:
DzengiRestAggTrade
Проверяется:
- корректность symbol;
- корректность trade_id;
- корректность price;
- корректность quantity;
- корректность datetime;
- корректность TradeAggressorSide;
- корректность source.
Тем самым подтверждается правильность полного построения Canonical Trade.
Проверка BUY и SELL
Отдельные unit-тесты подтверждают правильную интерпретацию транспортного признака:
buyer_is_maker
Проверяются обе возможные ситуации.
False
↓
BUY
True
↓
SELL
Тем самым полностью исключается вероятность обратного преобразования.
Проверка порядка элементов
Отдельный тест подтверждает,
что Mapper не изменяет порядок сделок.
Если транспортные модели расположены в определённой последовательности,
Canonical Trade создаются в точно таком же порядке.
Это является важной частью архитектурного контракта Mapper.
Проверка пустого набора
Mapper должен корректно работать даже при отсутствии сделок.
Для этого реализована отдельная проверка.
Вход:
tuple()
Результат:
tuple()
Никаких специальных объектов,
исключений
или дополнительных значений Build не создаёт.
Проверка нормализации symbol
Отдельный unit-тест подтверждает использование:
strip()
Если внешний слой передал:
" BTC/USDT "
то Canonical Trade получает:
"BTC/USDT"
При этом внутреннее содержимое строки не изменяется.
Удаляются исключительно внешние пробелы.
Проверка Decimal
Build содержит несколько отдельных тестов создания Decimal.
Проверяются различные варианты транспортных значений.
Например:
str
int
float
Во всех случаях результатом становится корректный объект:
Decimal
Это подтверждает независимость Mapper от конкретного представления транспортного числа.
Проверка datetime
Отдельный тест подтверждает,
что миллисекундный timestamp корректно преобразуется в:
UTC datetime
Также подтверждается,
что полученный объект содержит информацию о временной зоне.
Тем самым исключается появление naive datetime внутри Canonical Trade.
Проверка immutable
Поскольку Canonical Trade является immutable,
Build содержит специальный тест,
подтверждающий невозможность изменения созданного объекта.
Это гарантирует,
что Mapper полностью сохраняет архитектурные требования Build 060.1.
Проверка ошибок Mapping
Несмотря на предварительную validation,
Build содержит специальные тесты,
проверяющие работу:
TradeMappingError
Проверяются ситуации,
при которых невозможно корректно выполнить преобразование.
Например:
- некорректный Decimal;
- бесконечность;
- NaN;
- невозможность создать datetime.
Хотя подобные ситуации практически недостижимы после Value Validation,
их наличие существенно повышает устойчивость Mapper к ошибкам при дальнейшем развитии проекта.
Что намеренно не тестируется
Build 060.6 специально не тестирует:
- работу REST API;
- Schema Validation;
- Parser;
- Value Validation;
- Runtime;
- Feed;
- Registry;
- Acquisition Service;
- WebSocket Trades.
Каждый из перечисленных компонентов имеет собственные Build и собственные наборы unit-тестов.
Таким образом тесты Build 060.6 полностью сосредоточены исключительно на Mapper.
Что намеренно не изменялось
Build 060.6 специально не изменяет:
app/src/market_data/acquisition/models/trade.py
app/src/market_data/acquisition/adapters/dzengi/parser.py
app/src/market_data/acquisition/validation/schema.py
app/src/market_data/acquisition/validation/values.py
app/src/market_data/acquisition/runtime/
app/src/market_data/acquisition/feeds/
app/src/market_data/acquisition/service.py
app/src/market_data/acquisition/registry.py
Также Build намеренно не включает:
- REST client;
- выполнение REST-запросов;
- получение исторических сделок;
- объединение REST и WebSocket сделок;
- синхронизацию потоков данных;
- хранение истории сделок;
- построение Time & Sales Feed;
- обработку Live Feed;
- Runtime Integration;
- Production Integration.
Все перечисленные задачи реализуются отдельными Build согласно утверждённой дорожной карте.
Таким образом Build 060.6 остаётся полностью локальным и не выходит за пределы своей архитектурной ответственности.
Проверка компиляции
После завершения реализации была выполнена проверка компиляции проекта.
Команда выполнялась из каталога:
~/vsprojects/dzentra_bot/app
При активированном виртуальном окружении:
source .venv/bin/activate
Выполнена команда:
python -m compileall src
Результат:
успешно
Все модули проекта успешно скомпилированы.
Ошибок синтаксиса не обнаружено.
Build не нарушил корректность структуры проекта.
Проверка локальных unit-тестов
После завершения реализации Mapper были выполнены специализированные тесты.
Команда:
python -m pytest \
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py \
-q
Результат:
42 passed in 0.04s
Проверки подтвердили корректную работу:
- Trade Mapper;
- Instrument Mapper;
- Quote Mapper;
- Candle Mapper;
- новых тестов Build 060.6.
Ни одна существующая проверка не была нарушена.
Полный regression suite
После завершения Build выполнен полный набор тестов проекта.
Команда:
python -m pytest -q
Результат:
1092 passed in 4.39s
Регрессий не обнаружено.
Все ранее реализованные Build продолжают работать без изменений.
Это подтверждает, что Build 060.6 полностью соответствует принципу additive change.
Проверка форматирования
После завершения работы выполнена команда:
git diff --check
Вывод отсутствует.
Это подтверждает отсутствие:
- trailing whitespace;
- лишних пробелов;
- нарушений форматирования;
- ошибок оформления diff.
Build соответствует принятым требованиям оформления исходного кода.
Контроль размещения новой функциональности
После завершения Build вся новая функциональность сосредоточена только в предназначенных для неё компонентах.
Mapper расположен исключительно в:
src/market_data/acquisition/adapters/dzengi/mapper.py
Новое исключение расположено исключительно в:
src/market_data/acquisition/exceptions.py
Проверки расположены исключительно в:
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
Другие подсистемы проекта Build не затрагивает.
Таким образом архитектурная изоляция полностью сохранена.
Состояние Git
После завершения Build выполнена команда:
git status
Для Build 060.6 зафиксированы изменения:
modified:
app/src/market_data/acquisition/adapters/dzengi/mapper.py
app/src/market_data/acquisition/exceptions.py
app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
untracked:
docs/migrations/build_060_6.md
Дополнительно в рабочем каталоге присутствует архитектурная контрольная точка:
docs/migrations/build_060_transition_&_architecture_checkpoint.md
Она не относится к реализации Build 060.6 и учитывается отдельно при формировании commit.
Ветка разработки:
main
опережает origin/main.
Данное состояние не связано непосредственно с реализацией Build 060.6.
Фактический diff
В Build добавлены следующие основные компоненты.
В файл:
app/src/market_data/acquisition/adapters/dzengi/mapper.py
добавлены:
map_dzengi_rest_agg_trades_to_trades()
_map_dzengi_rest_agg_trade()
_required_trade_decimal()
_trade_timestamp_ms_to_utc_datetime()
_trade_aggressor_side()
В файл:
app/src/market_data/acquisition/exceptions.py
добавлено новое исключение:
TradeMappingError
Файл:
tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py
расширен новым набором специализированных unit-тестов.
Все изменения являются полностью additive.
Существующее поведение системы не изменялось.
Архитектурный результат
После завершения Build 060.6 REST Pipeline исторических сделок впервые становится полностью завершённым.
Полная архитектура выглядит следующим образом.
REST JSON
│
▼
Schema Validation
│
▼
ValidatedRestAggTradesDocument
│
▼
Parser
│
▼
tuple[DzengiRestAggTrade]
│
▼
Value Validation
│
▼
Trade Mapper
│
▼
tuple[Trade]
Каждый слой имеет собственную ответственность.
Каждый слой имеет собственный тип ошибок.
Каждый слой может развиваться независимо от остальных.
Таким образом архитектура REST Trades становится полностью согласованной с общей архитектурой Market Data Acquisition.
Влияние на последующие Build
Build 060.6 завершает формирование базового REST Pipeline.
Все последующие Build могут работать уже исключительно с канонической моделью:
Trade
Это позволяет реализовывать:
- REST Trade Feed;
- Time & Sales Feed;
- объединение REST и WebSocket Trade;
- кэширование сделок;
- хранение истории;
- анализ потока сделок;
- расчёт Order Flow;
- Footprint;
- Delta;
- Volume Profile;
- дальнейшие компоненты Market Intelligence.
При этом ни один из этих компонентов больше не зависит от формата REST API Dzengi.
Критерии завершения
Build 060.6 считается полностью завершённым, поскольку:
- проведён архитектурный аудит;
- реализован отдельный Mapper;
- реализовано специализированное исключение Mapping Layer;
- завершено разделение Transport Model и Canonical Model;
- реализовано преобразование всех полей Trade;
- реализовано создание Decimal;
- реализовано создание UTC datetime;
- реализовано преобразование buyer_is_maker в TradeAggressorSide;
- реализована нормализация symbol;
- сохранён immutable-подход;
- сохранён порядок сделок;
- отсутствует скрытая бизнес-логика;
- отсутствует сортировка;
- отсутствует дедупликация;
- отсутствует изменение транспортной модели;
- compile проверка успешно пройдена;
- локальные unit-тесты успешно пройдены;
- полный regression suite успешно пройден;
- проверка форматирования успешно пройдена;
- scope Build не расширен.
Архитектурные инварианты
После завершения Build 060.6 следующие свойства подсистемы считаются архитектурным контрактом и не должны изменяться последующими Build без отдельного архитектурного решения.
Инвариант 1
Transport Layer остаётся полностью независимым от предметной модели.
Транспортные модели не должны использовать:
- Trade;
- TradeAggressorSide;
- Decimal;
- datetime;
- любую бизнес-логику.
Инвариант 2
Schema Validation отвечает исключительно за проверку структуры документа.
Она не выполняет:
- parsing;
- mapping;
- преобразование типов;
- бизнес-валидацию.
Инвариант 3
Parser отвечает исключительно за построение transport-моделей.
Parser не создаёт:
- Canonical Models;
- Decimal;
- datetime;
- бизнес-объекты.
Инвариант 4
Value Validation отвечает исключительно за корректность значений.
Она не изменяет транспортные модели и не создаёт предметные объекты.
Инвариант 5
Mapper остаётся единственной точкой построения Canonical Trade.
Никакие другие компоненты не должны создавать объект Trade из REST transport-модели.
Инвариант 6
Все последующие компоненты системы работают исключительно с Canonical Trade.
Использование DzengiRestAggTrade за пределами Acquisition Layer не допускается.
Инвариант 7
Mapper не выполняет:
- сортировку;
- дедупликацию;
- агрегацию;
- расчёт аналитики;
- бизнес-решения.
Инвариант 8
Trade остаётся immutable.
Последующие Build не должны нарушать свойства:
- frozen;
- slots;
- неизменяемость объекта.
Инвариант 9
Все даты внутри Canonical Trade представлены только в UTC.
Использование naive datetime не допускается.
Инвариант 10
Каждый уровень Acquisition Pipeline имеет собственный тип исключений.
Объединение нескольких уровней обработки под одним типом исключений не допускается.
Итог
Build 060.6 завершён успешно.
Текущее состояние REST Trade Pipeline:
Transport Model — реализована
Schema Validation — реализована
Parser — реализован
Value Validation — реализована
Mapper — реализован
Canonical Trade — используется
TradeMappingError — реализован
Compile check — успешно
Target tests — 42 passed
Full regression suite — 1092 passed
Whitespace check — успешно
Production Integration — намеренно не выполнялась
После завершения Build 060.6 подсистема получения исторических сделок впервые получила полностью законченный Acquisition Pipeline.
Теперь все последующие компоненты проекта могут работать исключительно с канонической моделью Trade, полностью изолированной от внешнего REST API биржи.
Следующий этап
Следующим этапом развития серии Build 060 становится интеграция построенного Pipeline в рабочую подсистему получения данных.
Наиболее логичным продолжением является:
Build 060.7 — REST Trades Client
На этом этапе будет реализован специализированный клиент получения агрегированных сделок через REST API Dzengi, использующий полностью сформированный Pipeline:
REST Request
↓
REST Response
↓
Schema Validation
↓
Parser
↓
Value Validation
↓
Mapper
↓
tuple[Trade]
Build 060.7 станет первым этапом, на котором сформированный Pipeline начнёт использоваться в реальном процессе получения исторических сделок.