diff --git a/app/src/market_data/acquisition/adapters/dzengi/mapper.py b/app/src/market_data/acquisition/adapters/dzengi/mapper.py index 834a645..2deb418 100644 --- a/app/src/market_data/acquisition/adapters/dzengi/mapper.py +++ b/app/src/market_data/acquisition/adapters/dzengi/mapper.py @@ -13,6 +13,7 @@ from src.market_data.acquisition.adapters.dzengi.models import ( DzengiLotSizeFilter, DzengiMinNotionalFilter, DzengiRawNumeric, + DzengiRestAggTrade, DzengiTicker24hrResponse, DzengiWebSocketOhlcEvent, DzengiWebSocketQuoteResponse, @@ -22,11 +23,16 @@ from src.market_data.acquisition.exceptions import ( CandleWebSocketMappingError, InstrumentReferenceMappingError, QuoteMappingError, + TradeMappingError, ) from src.market_data.acquisition.models.candle import Candle from src.market_data.acquisition.models.candle_close import CandleCloseEvent from src.market_data.acquisition.models.instrument import Instrument from src.market_data.acquisition.models.quote import Quote +from src.market_data.acquisition.models.trade import ( + Trade, + TradeAggressorSide, +) _DZENGI_SOURCE_NAME = "dzengi" @@ -506,4 +512,98 @@ def _required_candle_decimal( f"Поле {field_name} свечи должно быть конечным числом." ) - return result \ No newline at end of file + return result + + +def map_dzengi_rest_agg_trades_to_trades( + trades: tuple[DzengiRestAggTrade, ...], + *, + symbol: str, +) -> tuple[Trade, ...]: + """ + Преобразовать проверенные transport-модели Dzengi aggTrades + в канонический immutable-набор Trade. + + Функция предполагает, что до mapper уже были выполнены: + schema validation, parsing и value validation. + """ + + return tuple( + _map_dzengi_rest_agg_trade( + trade, + symbol=symbol, + ) + for trade in trades + ) + + +def _map_dzengi_rest_agg_trade( + trade: DzengiRestAggTrade, + *, + symbol: str, +) -> Trade: + return Trade( + symbol=symbol.strip(), + trade_id=trade.aggregate_trade_id, + price=_required_trade_decimal( + trade.price, + field_name="price", + ), + quantity=_required_trade_decimal( + trade.quantity, + field_name="quantity", + ), + executed_at=_trade_timestamp_ms_to_utc_datetime( + trade.timestamp, + ), + aggressor_side=_trade_aggressor_side( + trade.buyer_is_maker, + ), + source=_DZENGI_SOURCE_NAME, + ) + + +def _trade_aggressor_side( + buyer_is_maker: bool, +) -> TradeAggressorSide: + if buyer_is_maker: + return TradeAggressorSide.SELL + + return TradeAggressorSide.BUY + + +def _required_trade_decimal( + value: DzengiRawNumeric, + *, + field_name: str, +) -> Decimal: + try: + result = Decimal(str(value)) + except (InvalidOperation, ValueError) as exc: + raise TradeMappingError( + f"Поле {field_name} сделки невозможно " + "преобразовать в Decimal." + ) from exc + + if not result.is_finite(): + raise TradeMappingError( + f"Поле {field_name} сделки должно быть " + "конечным числом." + ) + + return result + + +def _trade_timestamp_ms_to_utc_datetime( + value: int, +) -> datetime: + try: + return datetime.fromtimestamp( + value / 1000, + tz=timezone.utc, + ) + except (OverflowError, OSError, ValueError) as exc: + raise TradeMappingError( + "Поле timestamp сделки невозможно " + "преобразовать в UTC datetime." + ) from exc \ No newline at end of file diff --git a/app/src/market_data/acquisition/exceptions.py b/app/src/market_data/acquisition/exceptions.py index 065ed64..3e00dbd 100644 --- a/app/src/market_data/acquisition/exceptions.py +++ b/app/src/market_data/acquisition/exceptions.py @@ -134,6 +134,12 @@ class TradeValueError(MarketDataAcquisitionError): pass +# Ошибка преобразования raw-модели источника +# во внутреннюю модель Trade. +class TradeMappingError(MarketDataAcquisitionError): + pass + + # Ошибка определения типа входящего WebSocket-сообщения # и выбора специализированного адаптера. class WebSocketMessageRoutingError(MarketDataAcquisitionError): diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py index a7bee04..2ede81d 100644 --- a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py @@ -3,12 +3,14 @@ from __future__ import annotations from dataclasses import FrozenInstanceError, replace +from datetime import datetime, timezone from decimal import Decimal import pytest from src.market_data.acquisition.adapters.dzengi.mapper import ( map_dzengi_exchange_info_to_instruments, + map_dzengi_rest_agg_trades_to_trades, map_dzengi_symbol_to_instrument, ) from src.market_data.acquisition.adapters.dzengi.models import ( @@ -17,10 +19,16 @@ from src.market_data.acquisition.adapters.dzengi.models import ( DzengiExchangeInfoSymbol, DzengiLotSizeFilter, DzengiMinNotionalFilter, + DzengiRestAggTrade, DzengiUnknownFilter, ) from src.market_data.acquisition.exceptions import ( InstrumentReferenceMappingError, + TradeMappingError, +) +from src.market_data.acquisition.models.trade import ( + Trade, + TradeAggressorSide, ) @@ -82,6 +90,23 @@ def _response( ) +def _rest_agg_trade( + *, + aggregate_trade_id: int = 101, + price: str | int | float = "123.45", + quantity: str | int | float = "0.25", + timestamp: int = 1783537921471, + buyer_is_maker: bool = False, +) -> DzengiRestAggTrade: + return DzengiRestAggTrade( + aggregate_trade_id=aggregate_trade_id, + price=price, + quantity=quantity, + timestamp=timestamp, + buyer_is_maker=buyer_is_maker, + ) + + def test_map_complete_dzengi_symbol_to_instrument() -> None: instrument = map_dzengi_symbol_to_instrument( _complete_symbol() @@ -404,4 +429,244 @@ def test_mapped_instrument_is_immutable() -> None: ) with pytest.raises(FrozenInstanceError): - instrument.status = "BREAK" # type: ignore[misc] \ No newline at end of file + instrument.status = "BREAK" # type: ignore[misc] + + +def test_map_dzengi_rest_agg_trade_to_trade() -> None: + trades = map_dzengi_rest_agg_trades_to_trades( + (_rest_agg_trade(),), + symbol="BTC/USDT", + ) + + assert isinstance(trades, tuple) + assert len(trades) == 1 + + trade = trades[0] + + assert isinstance(trade, Trade) + assert trade.symbol == "BTC/USDT" + assert trade.trade_id == 101 + assert trade.price == Decimal("123.45") + assert trade.quantity == Decimal("0.25") + assert trade.executed_at == datetime.fromtimestamp( + 1783537921471 / 1000, + tz=timezone.utc, + ) + assert trade.aggressor_side is TradeAggressorSide.BUY + assert trade.source == "dzengi" + + +def test_trade_mapper_maps_buyer_taker_to_buy_aggressor() -> None: + trades = map_dzengi_rest_agg_trades_to_trades( + ( + _rest_agg_trade( + buyer_is_maker=False, + ), + ), + symbol="BTC/USDT", + ) + + assert trades[0].aggressor_side is TradeAggressorSide.BUY + + +def test_trade_mapper_maps_seller_taker_to_sell_aggressor() -> None: + trades = map_dzengi_rest_agg_trades_to_trades( + ( + _rest_agg_trade( + buyer_is_maker=True, + ), + ), + symbol="BTC/USDT", + ) + + assert trades[0].aggressor_side is TradeAggressorSide.SELL + + +def test_trade_mapper_preserves_input_order() -> None: + trades = map_dzengi_rest_agg_trades_to_trades( + ( + _rest_agg_trade( + aggregate_trade_id=103, + ), + _rest_agg_trade( + aggregate_trade_id=101, + ), + _rest_agg_trade( + aggregate_trade_id=102, + ), + ), + symbol="BTC/USDT", + ) + + assert tuple( + trade.trade_id + for trade in trades + ) == ( + 103, + 101, + 102, + ) + + +def test_trade_mapper_maps_empty_tuple() -> None: + trades = map_dzengi_rest_agg_trades_to_trades( + (), + symbol="BTC/USDT", + ) + + assert trades == () + + +def test_trade_mapper_strips_symbol() -> None: + trades = map_dzengi_rest_agg_trades_to_trades( + (_rest_agg_trade(),), + symbol=" BTC/USDT ", + ) + + assert trades[0].symbol == "BTC/USDT" + + +@pytest.mark.parametrize( + ("price", "quantity", "expected_price", "expected_quantity"), + [ + ( + "123.4500", + "0.2500", + Decimal("123.4500"), + Decimal("0.2500"), + ), + ( + 123, + 2, + Decimal("123"), + Decimal("2"), + ), + ( + 123.5, + 0.25, + Decimal("123.5"), + Decimal("0.25"), + ), + ], +) +def test_trade_mapper_converts_numeric_values_to_decimal( + price: str | int | float, + quantity: str | int | float, + expected_price: Decimal, + expected_quantity: Decimal, +) -> None: + trades = map_dzengi_rest_agg_trades_to_trades( + ( + _rest_agg_trade( + price=price, + quantity=quantity, + ), + ), + symbol="BTC/USDT", + ) + + assert trades[0].price == expected_price + assert trades[0].quantity == expected_quantity + + +def test_trade_mapper_converts_timestamp_to_utc_datetime() -> None: + timestamp = 1783537921471 + + trades = map_dzengi_rest_agg_trades_to_trades( + ( + _rest_agg_trade( + timestamp=timestamp, + ), + ), + symbol="BTC/USDT", + ) + + assert trades[0].executed_at == datetime.fromtimestamp( + timestamp / 1000, + tz=timezone.utc, + ) + assert trades[0].executed_at.tzinfo is timezone.utc + + +def test_mapped_trade_is_immutable() -> None: + trade = map_dzengi_rest_agg_trades_to_trades( + (_rest_agg_trade(),), + symbol="BTC/USDT", + )[0] + + with pytest.raises(FrozenInstanceError): + trade.price = Decimal("1") # type: ignore[misc] + + +@pytest.mark.parametrize( + "field_name", + [ + "price", + "quantity", + ], +) +def test_trade_mapper_rejects_invalid_decimal_value( + field_name: str, +) -> None: + trade = _rest_agg_trade() + + invalid_trade = replace( + trade, + **{field_name: "not-a-number"}, + ) + + with pytest.raises( + TradeMappingError, + match=rf"{field_name}.*невозможно преобразовать в Decimal", + ): + map_dzengi_rest_agg_trades_to_trades( + (invalid_trade,), + symbol="BTC/USDT", + ) + + +@pytest.mark.parametrize( + ("field_name", "invalid_value"), + [ + ("price", float("nan")), + ("price", float("inf")), + ("price", float("-inf")), + ("quantity", float("nan")), + ("quantity", float("inf")), + ("quantity", float("-inf")), + ], +) +def test_trade_mapper_rejects_non_finite_decimal_value( + field_name: str, + invalid_value: float, +) -> None: + trade = _rest_agg_trade() + + invalid_trade = replace( + trade, + **{field_name: invalid_value}, + ) + + with pytest.raises( + TradeMappingError, + match=rf"{field_name}.*должно быть конечным числом", + ): + map_dzengi_rest_agg_trades_to_trades( + (invalid_trade,), + symbol="BTC/USDT", + ) + + +def test_trade_mapper_rejects_unrepresentable_timestamp() -> None: + trade = _rest_agg_trade( + timestamp=10**30, + ) + + with pytest.raises( + TradeMappingError, + match=r"timestamp.*невозможно преобразовать в UTC datetime", + ): + map_dzengi_rest_agg_trades_to_trades( + (trade,), + symbol="BTC/USDT", + ) diff --git a/docs/migrations/build_060_6.md b/docs/migrations/build_060_6.md new file mode 100644 index 0000000..ea3d56a --- /dev/null +++ b/docs/migrations/build_060_6.md @@ -0,0 +1,2226 @@ +# 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 становится интеграция построенного Pipeline в рабочую подсистему получения данных. + +Наиболее логичным продолжением является: + +```text +Build 060.7 — REST Trades Client +``` + +На этом этапе будет реализован специализированный клиент получения агрегированных сделок через REST API Dzengi, использующий полностью сформированный Pipeline: + +```text +REST Request + +↓ + +REST Response + +↓ + +Schema Validation + +↓ + +Parser + +↓ + +Value Validation + +↓ + +Mapper + +↓ + +tuple[Trade] +``` + +Build 060.7 станет первым этапом, на котором сформированный Pipeline начнёт использоваться в реальном процессе получения исторических сделок. \ No newline at end of file