# 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 выглядел следующим образом: ```text 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 принята единая многоуровневая модель обработки любых внешних рыночных данных. Независимо от источника данные проходят одинаковые стадии обработки. ```text 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 проект уже содержал: ```text Canonical Trade DzengiRestAggTrade TradeSchemaError TradeParseError TradeValueError validate_rest_agg_trades_schema() parse_rest_agg_trades() validate_rest_agg_trade_values() ``` Таким образом все необходимые проверки транспортного уровня уже были реализованы. Отсутствовал только переход к внутренней модели. Файл: ```text 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 без изменения существующего поведения системы. --- # Предварительный архитектурный аудит Перед началом реализации были повторно проанализированы: ```text 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 система всё ещё использовала транспортную модель биржи: ```text DzengiRestAggTrade ``` После Build система должна работать исключительно с внутренней моделью: ```text Trade ``` Таким образом все последующие компоненты Dzentra полностью перестают зависеть от REST API Dzengi. Это является одним из ключевых принципов архитектуры всей подсистемы Market Data Acquisition. --- # Рассмотренные архитектурные решения Перед реализацией были рассмотрены несколько вариантов построения последнего слоя REST Pipeline. ## Вариант 1 Использовать транспортную модель непосредственно внутри системы. Например: ```python DzengiRestAggTrade ``` вместо ```python Trade ``` Данный вариант был отклонён. Причины: - транспортная модель описывает внешний REST API; - любое изменение биржи приведёт к изменению внутренней модели системы; - нарушается принцип изоляции внешнего контракта; - остальные Acquisition Pipeline уже используют Canonical Models; - появляется зависимость бизнес-логики от конкретной биржи. Использование транспортной модели за пределами Acquisition Layer противоречит утверждённой архитектуре Dzentra. --- ## Вариант 2 Создавать Canonical Trade непосредственно в Parser. Например: ```text REST JSON │ ▼ Parser │ ▼ Trade ``` На первый взгляд такой подход уменьшает количество слоёв. Однако он был отклонён. Причины: - parser начинает выполнять две независимые задачи; - смешиваются parsing и mapping; - невозможно независимо тестировать parser; - parser начинает зависеть от предметной модели; - нарушается единообразие остальных Acquisition Pipeline. В Dzentra Parser отвечает исключительно за преобразование документа в транспортную модель. Создание предметных объектов относится только к Mapper. --- ## Вариант 3 Объединить Value Validation и Mapper. Например: ```text 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 ввёл единственную утверждённую модель исполненной сделки: ```text Trade ``` Начиная с этого момента все остальные подсистемы Dzentra должны работать исключительно с ней. После завершения Build 060.6 никакие компоненты выше Acquisition Layer больше не должны использовать: ```text DzengiRestAggTrade ``` Тем самым достигается полная независимость внутренних модулей от конкретной биржи. --- # Почему Mapper не выполняет Validation Несмотря на то что Mapper повторно преобразует некоторые поля, он намеренно не занимается их проверкой. Например: ```text price quantity timestamp ``` к моменту вызова Mapper уже прошли: - Schema Validation; - Parser; - Value Validation. Следовательно Mapper получает гарантированно корректные данные. Повторная проверка: - увеличила бы объём кода; - ухудшила читаемость; - нарушила бы разделение ответственности; - усложнила сопровождение. Именно поэтому Build 060.6 использует уже проверенные значения без дополнительной бизнес-валидации. --- # Почему Mapper всё же создаёт Decimal Во время проектирования обсуждался вопрос: если Value Validation уже проверила корректность значения, нужно ли повторно выполнять: ```text str ↓ Decimal ``` Ответ — да. Причина заключается в разделении обязанностей. Value Validation подтверждает только возможность такого преобразования. Она не создаёт предметные объекты. Создание экземпляров `Decimal` относится исключительно к этапу построения Canonical Model. Поэтому преобразование выполняется именно внутри Mapper. --- # Почему Decimal не создаётся раньше Рассматривался альтернативный вариант. После завершения Value Validation транспортная модель могла бы уже содержать: ```python Decimal ``` вместо ```python str ``` Данный вариант был отклонён. Причины: транспортная модель должна максимально точно отражать внешний REST API. REST API возвращает строки. Следовательно transport layer также обязан использовать строки. Любое изменение типа означало бы скрытый Mapping. Это нарушило бы архитектуру Acquisition Pipeline. --- # Почему создаётся datetime REST API возвращает время сделки в виде: ```text timestamp (milliseconds) ``` Предметная модель использует: ```text datetime ``` Следовательно именно Mapper отвечает за переход: ```text timestamp ↓ datetime ``` Это преобразование является частью формирования внутренней модели. Никакие предыдущие Build его не выполняют. --- # Почему используется UTC При проектировании обсуждались несколько вариантов представления времени. Рассматривались: - naive datetime; - локальное время системы; - UTC datetime. Утверждён третий вариант. Причины: - все остальные модели проекта используют UTC; - отсутствует зависимость от локальной временной зоны; - упрощается дальнейшее сравнение событий; - полностью исключаются ошибки перехода между часовыми поясами. Следовательно Mapper всегда создаёт timezone-aware UTC datetime. --- # Почему Mapper получает symbol параметром Во время проектирования обсуждался ещё один вопрос. REST endpoint вызывается примерно следующим образом: ```text GET /api/v2/aggTrades?symbol=BTCUSDT ``` При этом внутри каждого элемента массива символ отсутствует. Рассматривались несколько вариантов. --- ## Вариант 1 Добавить symbol в транспортную модель. Отклонён. Причины: это изменило бы внешний контракт REST API; транспортная модель перестала бы точно описывать полученный JSON. --- ## Вариант 2 Не сохранять symbol вообще. Отклонён. Причины: Canonical Trade всегда должен содержать символ инструмента. Без него сделка становится неполной. --- ## Утверждённое решение Mapper принимает symbol отдельным параметром. Например: ```python map_dzengi_rest_agg_trades_to_trades( trades, symbol="BTC/USDT", ) ``` Такое решение позволяет одновременно: - сохранить чистоту транспортной модели; - корректно построить Canonical Trade. --- # Почему выполняется strip() Mapper использует: ```python symbol.strip() ``` Это решение также обсуждалось отдельно. Причины: символ инструмента может поступать из внешнего слоя; внешний слой потенциально способен содержать случайные пробелы; Canonical Model должна содержать нормализованное значение. При этом Mapper не изменяет сам идентификатор инструмента. Удаляются только внешние пробельные символы. --- # Почему buyer_is_maker преобразуется в TradeAggressorSide REST API использует поле: ```text buyer_is_maker ``` Это транспортная характеристика конкретного REST endpoint. Предметная модель Dzentra использует другое понятие: ```text TradeAggressorSide ``` Следовательно Mapper обязан выполнить интерпретацию транспортного признака. Используется следующее соответствие: ```text buyer_is_maker = False ↓ BUY ``` ```text 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 добавляет новую публичную функцию: ```python map_dzengi_rest_agg_trades_to_trades() ``` Назначение функции: - принимает набор проверенных транспортных моделей; - создаёт immutable набор Canonical Trade; - сохраняет порядок входных элементов; - не изменяет семантику данных. Сигнатура имеет вид: ```python tuple[DzengiRestAggTrade] │ ▼ tuple[Trade] ``` Функция является единственной публичной точкой входа Mapper. Все остальные функции Build имеют внутренний характер и используются исключительно как вспомогательные. --- # Новое исключение Build 060.6 вводит новый специализированный тип исключения: ```python TradeMappingError ``` Назначение исключения — локализовать ошибки уровня Mapping. Таким образом полностью завершается иерархия ошибок REST Trade Pipeline: ```text TradeSchemaError │ TradeParseError │ TradeValueError │ TradeMappingError ``` Каждый этап обработки теперь имеет собственный тип исключений. Это позволяет точно определить уровень возникновения ошибки и существенно упрощает диагностику при дальнейшем развитии системы. # Семантика Mapper После завершения Build 060.6 Mapper становится единственной точкой, в которой транспортная модель преобразуется во внутреннюю предметную модель Dzentra. На вход Mapper получает исключительно полностью подготовленные объекты. Они уже прошли: - проверку структуры; - parser; - проверку обязательных полей; - проверку типов; - проверку диапазонов; - проверку корректности числовых значений. Следовательно Mapper может полностью сосредоточиться исключительно на преобразовании моделей данных. Его работа сводится к последовательному созданию каждого экземпляра Canonical Trade. --- # Общая схема преобразования Для каждой транспортной модели выполняется одинаковая последовательность действий. ```text 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 Транспортная модель содержит поле: ```python aggregate_trade_id ``` Предметная модель использует: ```python trade_id ``` Во время проектирования обсуждался вопрос: нужно ли сохранять первоначальное название? Решение было отрицательным. Причины: Canonical Trade не должен зависеть от конкретного REST endpoint. Внутренняя модель должна использовать единый термин: ```text trade_id ``` Следовательно Mapper выполняет простое переименование поля. Никаких дополнительных преобразований значения не производится. --- # Преобразование price REST API возвращает цену как транспортное числовое значение. После завершения предыдущих Build уже известно, что значение: - существует; - корректно; - конечно; - может быть преобразовано в Decimal. Mapper создаёт окончательное значение: ```python Decimal ``` Полученный объект помещается непосредственно в Canonical Trade. После этого транспортное представление полностью исчезает. --- # Преобразование quantity Поле quantity проходит абсолютно аналогичную обработку. Mapper получает проверенное транспортное значение. Создаётся: ```python Decimal ``` Полученный экземпляр используется внутри Canonical Trade. Дополнительные проверки не выполняются. --- # Преобразование timestamp Наиболее важным преобразованием Build является обработка времени сделки. REST API использует миллисекундный Unix Timestamp. Например: ```text 1783537921471 ``` Внутренняя модель использует: ```python datetime ``` Mapper выполняет преобразование: ```text milliseconds ↓ seconds ↓ datetime ↓ UTC ``` После завершения Mapper транспортное представление времени полностью исчезает. Все последующие компоненты работают только с datetime. --- # Преобразование buyer_is_maker REST API использует транспортный признак: ```text buyer_is_maker ``` Однако для анализа рынка подобное представление неудобно. Предметная модель использует понятие стороны агрессора. Поэтому Mapper выполняет следующую интерпретацию. Если: ```text buyer_is_maker = False ``` создаётся ```text TradeAggressorSide.BUY ``` Если: ```text buyer_is_maker = True ``` создаётся ```text TradeAggressorSide.SELL ``` В результате все остальные компоненты Dzentra работают уже не с транспортным булевым признаком, а с предметной характеристикой сделки. --- # Преобразование symbol Canonical Trade всегда содержит символ инструмента. Поскольку REST API не включает его в элементы массива, Mapper получает его отдельным аргументом. Перед сохранением выполняется: ```python strip() ``` Это гарантирует отсутствие случайных пробелов. Никаких других преобразований идентификатора инструмента не производится. --- # Поле source Build 060.1 закрепил наличие поля: ```python source ``` внутри Canonical Trade. Mapper устанавливает значение: ```text dzengi ``` Причины такого решения: - источник данных известен заранее; - он не зависит от содержимого REST ответа; - остальные компоненты могут определить происхождение сделки без анализа transport layer. В дальнейшем аналогичный подход позволит объединять сделки, поступающие из различных источников. --- # Immutable-поведение Build 060.6 полностью сохраняет принятые архитектурные принципы проекта. Каждый созданный объект: ```python 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() Полностью изолирует преобразование времени. Она отвечает исключительно за переход: ```text timestamp (milliseconds) ↓ UTC datetime ``` Такое разделение делает Mapper проще для чтения и уменьшает связность отдельных операций. --- ## _trade_aggressor_side() Инкапсулирует преобразование транспортного признака: ```text buyer_is_maker ``` в предметную модель: ```text TradeAggressorSide ``` Выделение отдельной функции позволяет: - сделать основной Mapper более компактным; - централизовать интерпретацию транспортного признака; - исключить дублирование логики при дальнейшем развитии проекта. --- # Что намеренно не делает Mapper Build 060.6 специально не реализует никакой дополнительной бизнес-логики. Mapper намеренно: не сортирует сделки; не удаляет дубликаты; не объединяет REST и WebSocket данные; не выполняет агрегацию; не рассчитывает статистику; не изменяет цену; не изменяет количество; не изменяет идентификаторы; не анализирует последовательность сделок; не определяет рыночную структуру; не вычисляет объёмы; не создаёт свечи; не формирует Time & Sales Feed; не взаимодействует с Runtime; не сохраняет данные. Все перечисленные задачи относятся к последующим уровням архитектуры и не входят в ответственность Mapper. Благодаря этому Build 060.6 остаётся небольшим, полностью изолированным и строго соответствует принципу Single Responsibility. # Изменённые файлы В рамках Build 060.6 были изменены только три файла проекта. ```text 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: ```text docs/migrations/build_060_6.md ``` Никакие другие компоненты проекта не изменялись. Build полностью соответствует ранее утверждённому принципу минимального локального изменения (Local Additive Change). --- # Изменения в exceptions.py Файл: ```text app/src/market_data/acquisition/exceptions.py ``` получил новый специализированный тип исключения: ```python TradeMappingError ``` До Build 060.6 цепочка исключений выглядела следующим образом: ```text TradeSchemaError ↓ TradeParseError ↓ TradeValueError ``` После завершения Build архитектура стала полностью симметричной: ```text TradeSchemaError ↓ TradeParseError ↓ TradeValueError ↓ TradeMappingError ``` Теперь каждый слой REST Trade Pipeline имеет собственный тип ошибок. Это значительно упрощает диагностику при сопровождении системы. --- # Назначение TradeMappingError Несмотря на то что Mapper работает только с уже проверенными объектами, полностью исключить ошибки преобразования невозможно. Например: - невозможность создать Decimal; - невозможность построить datetime; - внутренние ошибки преобразования. Во всех подобных случаях используется исключительно: ```python TradeMappingError ``` Таким образом исключения предыдущих уровней не используются повторно. Каждый слой отвечает только за собственные ошибки. --- # Изменения в mapper.py Build 060.6 расширяет существующий файл: ```text app/src/market_data/acquisition/adapters/dzengi/mapper.py ``` Никакие существующие mapper не изменялись. Все новые функции были добавлены дополнительно. Таким образом Build полностью сохраняет обратную совместимость. --- # Новый публичный API В файл добавлена функция: ```python map_dzengi_rest_agg_trades_to_trades() ``` Именно она становится официальной точкой входа Trade Mapper. Она: - принимает immutable tuple транспортных моделей; - создаёт immutable tuple Canonical Trade; - сохраняет порядок элементов; - не изменяет семантику данных; - не выполняет дополнительную validation. После Build именно эта функция должна использоваться всеми последующими компонентами Acquisition Layer. --- # Внутренние функции Mapper Кроме публичного API Build добавляет четыре внутренних функции. ```python _map_dzengi_rest_agg_trade() _required_trade_decimal() _trade_timestamp_ms_to_utc_datetime() _trade_aggressor_side() ``` Каждая из них отвечает только за одну небольшую операцию. Такое разделение полностью соответствует существующему стилю остальных mapper проекта. --- # Почему используется несколько небольших функций Во время проектирования обсуждалась возможность реализации всего Mapper одной функцией. Например: ```python map_dzengi_rest_agg_trades_to_trades(...) ``` которая сразу содержала бы всю логику. Такой вариант был отклонён. Причины: - ухудшается читаемость; - возрастает размер функции; - усложняется unit-тестирование; - возрастает вероятность случайных ошибок при дальнейшем сопровождении. Использование небольших специализированных функций делает код значительно проще для сопровождения. --- # Изменения в unit-тестах Файл: ```text 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 являются тесты успешного преобразования. Они подтверждают корректность создания: ```python Trade ``` из: ```python DzengiRestAggTrade ``` Проверяется: - корректность symbol; - корректность trade_id; - корректность price; - корректность quantity; - корректность datetime; - корректность TradeAggressorSide; - корректность source. Тем самым подтверждается правильность полного построения Canonical Trade. --- # Проверка BUY и SELL Отдельные unit-тесты подтверждают правильную интерпретацию транспортного признака: ```text buyer_is_maker ``` Проверяются обе возможные ситуации. ```text False ↓ BUY ``` ```text True ↓ SELL ``` Тем самым полностью исключается вероятность обратного преобразования. --- # Проверка порядка элементов Отдельный тест подтверждает, что Mapper не изменяет порядок сделок. Если транспортные модели расположены в определённой последовательности, Canonical Trade создаются в точно таком же порядке. Это является важной частью архитектурного контракта Mapper. --- # Проверка пустого набора Mapper должен корректно работать даже при отсутствии сделок. Для этого реализована отдельная проверка. Вход: ```python tuple() ``` Результат: ```python tuple() ``` Никаких специальных объектов, исключений или дополнительных значений Build не создаёт. --- # Проверка нормализации symbol Отдельный unit-тест подтверждает использование: ```python strip() ``` Если внешний слой передал: ```text " BTC/USDT " ``` то Canonical Trade получает: ```text "BTC/USDT" ``` При этом внутреннее содержимое строки не изменяется. Удаляются исключительно внешние пробелы. --- # Проверка Decimal Build содержит несколько отдельных тестов создания Decimal. Проверяются различные варианты транспортных значений. Например: ```text str int float ``` Во всех случаях результатом становится корректный объект: ```python Decimal ``` Это подтверждает независимость Mapper от конкретного представления транспортного числа. --- # Проверка datetime Отдельный тест подтверждает, что миллисекундный timestamp корректно преобразуется в: ```python UTC datetime ``` Также подтверждается, что полученный объект содержит информацию о временной зоне. Тем самым исключается появление naive datetime внутри Canonical Trade. --- # Проверка immutable Поскольку Canonical Trade является immutable, Build содержит специальный тест, подтверждающий невозможность изменения созданного объекта. Это гарантирует, что Mapper полностью сохраняет архитектурные требования Build 060.1. --- # Проверка ошибок Mapping Несмотря на предварительную validation, Build содержит специальные тесты, проверяющие работу: ```python 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 специально не изменяет: ```text 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 остаётся полностью локальным и не выходит за пределы своей архитектурной ответственности. --- # Проверка компиляции После завершения реализации была выполнена проверка компиляции проекта. Команда выполнялась из каталога: ```text ~/vsprojects/dzentra_bot/app ``` При активированном виртуальном окружении: ```bash source .venv/bin/activate ``` Выполнена команда: ```bash python -m compileall src ``` Результат: ```text успешно ``` Все модули проекта успешно скомпилированы. Ошибок синтаксиса не обнаружено. Build не нарушил корректность структуры проекта. --- # Проверка локальных unit-тестов После завершения реализации Mapper были выполнены специализированные тесты. Команда: ```bash python -m pytest \ tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py \ -q ``` Результат: ```text 42 passed in 0.04s ``` Проверки подтвердили корректную работу: - Trade Mapper; - Instrument Mapper; - Quote Mapper; - Candle Mapper; - новых тестов Build 060.6. Ни одна существующая проверка не была нарушена. --- # Полный regression suite После завершения Build выполнен полный набор тестов проекта. Команда: ```bash python -m pytest -q ``` Результат: ```text 1092 passed in 4.39s ``` Регрессий не обнаружено. Все ранее реализованные Build продолжают работать без изменений. Это подтверждает, что Build 060.6 полностью соответствует принципу additive change. --- # Проверка форматирования После завершения работы выполнена команда: ```bash git diff --check ``` Вывод отсутствует. Это подтверждает отсутствие: - trailing whitespace; - лишних пробелов; - нарушений форматирования; - ошибок оформления diff. Build соответствует принятым требованиям оформления исходного кода. --- # Контроль размещения новой функциональности После завершения Build вся новая функциональность сосредоточена только в предназначенных для неё компонентах. Mapper расположен исключительно в: ```text src/market_data/acquisition/adapters/dzengi/mapper.py ``` Новое исключение расположено исключительно в: ```text src/market_data/acquisition/exceptions.py ``` Проверки расположены исключительно в: ```text tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py ``` Другие подсистемы проекта Build не затрагивает. Таким образом архитектурная изоляция полностью сохранена. --- # Состояние Git После завершения Build выполнена команда: ```bash git status ``` Для Build 060.6 зафиксированы изменения: ```text 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 ``` Дополнительно в рабочем каталоге присутствует архитектурная контрольная точка: ```text docs/migrations/build_060_transition_&_architecture_checkpoint.md ``` Она не относится к реализации Build 060.6 и учитывается отдельно при формировании commit. Ветка разработки: ```text main ``` опережает `origin/main`. Данное состояние не связано непосредственно с реализацией Build 060.6. --- # Фактический diff В Build добавлены следующие основные компоненты. В файл: ```text app/src/market_data/acquisition/adapters/dzengi/mapper.py ``` добавлены: ```python map_dzengi_rest_agg_trades_to_trades() _map_dzengi_rest_agg_trade() _required_trade_decimal() _trade_timestamp_ms_to_utc_datetime() _trade_aggressor_side() ``` В файл: ```text app/src/market_data/acquisition/exceptions.py ``` добавлено новое исключение: ```python TradeMappingError ``` Файл: ```text tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py ``` расширен новым набором специализированных unit-тестов. Все изменения являются полностью additive. Существующее поведение системы не изменялось. --- # Архитектурный результат После завершения Build 060.6 REST Pipeline исторических сделок впервые становится полностью завершённым. Полная архитектура выглядит следующим образом. ```text 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 могут работать уже исключительно с канонической моделью: ```text 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: ```text 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 становится: ```text Build 060.7 — REST Trade Adapter ``` На этом этапе будет реализован адаптер REST-сделок, который объединит уже созданные компоненты обработки: ```text Validated REST document ↓ Parser ↓ Value Validation ↓ Mapper ↓ tuple[Trade] ``` Build 060.7 не должен самостоятельно выполнять HTTP-запросы и получать документ из внешнего источника. Получение исходного REST-документа будет выделено в следующий отдельный этап: ```text Build 060.8 — REST Trades Document Source ``` Такое разделение сохраняет независимость: * источника документа; * адаптера обработки документа; * transport-моделей; * validation; * parser; * mapper; * Canonical Trade. После завершения Build 060.7 система получит единый REST Trade Adapter, способный преобразовывать уже полученный REST-документ в канонические сделки, но ещё не связанный с конкретным механизмом HTTP-доступа.