diff --git a/app/src/market_data/acquisition/adapters/dzengi/rest_trade_adapter.py b/app/src/market_data/acquisition/adapters/dzengi/rest_trade_adapter.py new file mode 100644 index 0000000..cbcc608 --- /dev/null +++ b/app/src/market_data/acquisition/adapters/dzengi/rest_trade_adapter.py @@ -0,0 +1,41 @@ +# src/market_data/acquisition/adapters/dzengi/rest_trade_adapter.py + +from __future__ import annotations + +from src.market_data.acquisition.adapters.dzengi.mapper import ( + map_dzengi_rest_agg_trades_to_trades, +) +from src.market_data.acquisition.adapters.dzengi.parser import ( + parse_rest_agg_trades, +) +from src.market_data.acquisition.models.trade import Trade +from src.market_data.acquisition.validation.schema import ( + ValidatedRestAggTradesDocument, +) +from src.market_data.acquisition.validation.values import ( + validate_rest_agg_trade_values, +) + + +def adapt_rest_agg_trades_document( + document: ValidatedRestAggTradesDocument, + *, + symbol: str, +) -> tuple[Trade, ...]: + """ + Преобразовать структурно проверенный документ Dzengi aggTrades + в канонический immutable-набор Trade. + + Функция последовательно выполняет parsing, value validation + и mapping. Schema validation должна быть выполнена вызывающим слоем. + Исключения отдельных этапов не перехватываются и не оборачиваются. + """ + + trades = parse_rest_agg_trades(document) + + validate_rest_agg_trade_values(trades) + + return map_dzengi_rest_agg_trades_to_trades( + trades, + symbol=symbol, + ) \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py new file mode 100644 index 0000000..c6ead04 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py @@ -0,0 +1,188 @@ +# tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py + +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal +from types import MappingProxyType + +import pytest + +from src.market_data.acquisition.adapters.dzengi import rest_trade_adapter +from src.market_data.acquisition.adapters.dzengi.rest_trade_adapter import ( + adapt_rest_agg_trades_document, +) +from src.market_data.acquisition.exceptions import ( + TradeMappingError, + TradeParseError, + TradeValueError, +) +from src.market_data.acquisition.models.trade import TradeAggressorSide +from src.market_data.acquisition.validation.schema import ( + ValidatedRestAggTradesDocument, +) + + +def test_adapt_rest_agg_trades_document_returns_canonical_trades() -> None: + document = ValidatedRestAggTradesDocument( + items=( + MappingProxyType( + { + "a": 101, + "p": "43210.50", + "q": "0.125", + "T": 1_700_000_000_000, + "m": False, + } + ), + MappingProxyType( + { + "a": 102, + "p": "43211.75", + "q": "0.250", + "T": 1_700_000_001_000, + "m": True, + } + ), + ) + ) + + result = adapt_rest_agg_trades_document( + document, + symbol=" BTCUSDT ", + ) + + assert isinstance(result, tuple) + assert len(result) == 2 + + first_trade = result[0] + + assert first_trade.symbol == "BTCUSDT" + assert first_trade.trade_id == 101 + assert first_trade.price == Decimal("43210.50") + assert first_trade.quantity == Decimal("0.125") + assert first_trade.executed_at == datetime( + 2023, + 11, + 14, + 22, + 13, + 20, + tzinfo=timezone.utc, + ) + assert first_trade.aggressor_side is TradeAggressorSide.BUY + assert first_trade.source == "dzengi" + + second_trade = result[1] + + assert second_trade.symbol == "BTCUSDT" + assert second_trade.trade_id == 102 + assert second_trade.price == Decimal("43211.75") + assert second_trade.quantity == Decimal("0.250") + assert second_trade.executed_at == datetime( + 2023, + 11, + 14, + 22, + 13, + 21, + tzinfo=timezone.utc, + ) + assert second_trade.aggressor_side is TradeAggressorSide.SELL + assert second_trade.source == "dzengi" + + +def test_adapt_rest_agg_trades_document_propagates_parse_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + document = ValidatedRestAggTradesDocument(items=()) + expected_error = TradeParseError("test parse error") + + def raise_parse_error( + _: ValidatedRestAggTradesDocument, + ) -> tuple[()]: + raise expected_error + + monkeypatch.setattr( + rest_trade_adapter, + "parse_rest_agg_trades", + raise_parse_error, + ) + + with pytest.raises(TradeParseError) as exc_info: + adapt_rest_agg_trades_document( + document, + symbol="BTCUSDT", + ) + + assert exc_info.value is expected_error + + +def test_adapt_rest_agg_trades_document_propagates_value_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + document = ValidatedRestAggTradesDocument(items=()) + expected_error = TradeValueError("test value error") + + monkeypatch.setattr( + rest_trade_adapter, + "parse_rest_agg_trades", + lambda _: (), + ) + + def raise_value_error(_: tuple[object, ...]) -> None: + raise expected_error + + monkeypatch.setattr( + rest_trade_adapter, + "validate_rest_agg_trade_values", + raise_value_error, + ) + + with pytest.raises(TradeValueError) as exc_info: + adapt_rest_agg_trades_document( + document, + symbol="BTCUSDT", + ) + + assert exc_info.value is expected_error + + +def test_adapt_rest_agg_trades_document_propagates_mapping_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + document = ValidatedRestAggTradesDocument(items=()) + expected_error = TradeMappingError("test mapping error") + + monkeypatch.setattr( + rest_trade_adapter, + "parse_rest_agg_trades", + lambda _: (), + ) + monkeypatch.setattr( + rest_trade_adapter, + "validate_rest_agg_trade_values", + lambda _: None, + ) + + def raise_mapping_error( + _: tuple[object, ...], + *, + symbol: str, + ) -> tuple[()]: + del symbol + raise expected_error + + monkeypatch.setattr( + rest_trade_adapter, + "map_dzengi_rest_agg_trades_to_trades", + raise_mapping_error, + ) + + with pytest.raises(TradeMappingError) as exc_info: + adapt_rest_agg_trades_document( + document, + symbol="BTCUSDT", + ) + + assert exc_info.value is expected_error \ No newline at end of file diff --git a/docs/migrations/build_060_5.md b/docs/migrations/build_060_5.md new file mode 100644 index 0000000..9c6f094 --- /dev/null +++ b/docs/migrations/build_060_5.md @@ -0,0 +1,328 @@ +# Build 060.5 — REST Trade Value Validation + +## Цель + +Реализовать слой проверки допустимости значений (`Value Validation`) для REST aggTrades после завершения этапов структурной проверки (`Schema Validation`) и преобразования в transport-модели (`REST Parser`). + +Build завершает третий этап конвейера обработки REST Trades и обеспечивает, что все transport-модели содержат только корректные значения перед передачей в Mapper. + +--- + +# Место Build в общей архитектуре + +До Build 060.5 конвейер выглядел следующим образом: + +```text +REST JSON + │ + ▼ +Schema Validation + │ + ▼ +ValidatedRestAggTradesDocument + │ + ▼ +REST Parser + │ + ▼ +tuple[DzengiRestAggTrade] +``` + +После Build 060.5: + +```text +REST JSON + │ + ▼ +Schema Validation + │ + ▼ +ValidatedRestAggTradesDocument + │ + ▼ +REST Parser + │ + ▼ +tuple[DzengiRestAggTrade] + │ + ▼ +REST Trade Value Validation + │ + ▼ +tuple[DzengiRestAggTrade] +``` + +Следующим этапом станет: + +```text +Mapper + │ + ▼ +Canonical Trade +``` + +--- + +# Архитектурная ответственность + +Value Validation отвечает исключительно за проверку допустимости значений transport-моделей. + +Validator: + +- не изменяет transport-модель; +- не выполняет mapping; +- не преобразует данные в canonical-модель; +- не нормализует значения; +- не возвращает преобразованные объекты. + +При успешной проверке функция завершается без результата. + +При обнаружении ошибки выбрасывается специализированное исключение. + +--- + +# Добавлено новое исключение + +Файл: + +```text +src/market_data/acquisition/exceptions.py +``` + +Добавлено: + +```python +TradeValueError +``` + +Назначение: + +- ошибки проверки допустимости значений REST Trade transport-моделей. + +--- + +# Новый публичный API + +Файл: + +```text +src/market_data/acquisition/validation/values.py +``` + +Добавлена функция: + +```python +validate_rest_agg_trade_values( + trades: tuple[DzengiRestAggTrade, ...], +) -> None +``` + +Ответственность функции: + +- проверить все transport-модели; +- при первой ошибке выбросить `TradeValueError`; +- при отсутствии ошибок успешно завершиться. + +--- + +# Внутренняя архитектура + +Реализация построена по тому же принципу, что уже используется для Instrument, Quote и Candle. + +```text +validate_rest_agg_trade_values() + │ + ▼ + _validate_rest_agg_trade() + │ + ├────────────► _trade_positive_int() + ├────────────► _trade_positive_decimal() + └────────────► _trade_decimal() +``` + +Каждая helper-функция отвечает только за одну проверку. + +--- + +# Проверяемые поля + +## aggregate_trade_id + +Проверяется: + +- тип int (bool исключается); +- значение больше нуля. + +--- + +## price + +Проверяется: + +- успешное преобразование в Decimal; +- конечность числа; +- значение больше нуля. + +Поддерживаются значения: + +- str +- int +- float + +--- + +## quantity + +Проверяется аналогично полю price. + +--- + +## timestamp + +Проверяется: + +- тип int (bool исключается); +- значение больше нуля. + +--- + +## buyer_is_maker + +Дополнительная проверка отсутствует. + +Корректность типа bool уже гарантируется предыдущим этапом — REST Parser. + +--- + +# Что НЕ входит в Value Validation + +Build сознательно не выполняет: + +- преобразование transport-моделей; +- mapping; +- создание Canonical Trade; +- преобразование чисел в Decimal для дальнейшей обработки; +- проверку возраста сделки; +- проверку порядка timestamp; +- проверку последовательности aggregateTradeId; +- проверку дубликатов. + +Все перечисленные задачи относятся к последующим слоям Acquisition Pipeline. + +--- + +# Диагностика ошибок + +Все сообщения содержат полный путь до ошибочного поля. + +Примеры: + +```text +$[0].price должно быть больше нуля. + +$[1].quantity должно быть корректным числом. + +$[2].timestamp должно быть целым числом больше нуля. +``` + +Такой формат полностью соответствует существующей архитектуре Validation Layer. + +--- + +# Unit Tests + +Добавлены тесты для: + +## Позитивных сценариев + +- одна корректная сделка; +- несколько корректных сделок; +- пустой tuple; +- строковые числовые значения; +- int; +- float; +- оба значения buyer_is_maker. + +--- + +## Негативных сценариев + +Проверяются: + +- aggregate_trade_id ≤ 0; +- timestamp ≤ 0; +- price ≤ 0; +- quantity ≤ 0; +- NaN; +- Infinity; +- -Infinity; +- некорректные числовые строки; +- корректное формирование пути ошибки. + +--- + +# Результаты проверки + +Target tests: + +```text +68 passed +``` + +Полная регрессия проекта: + +```text +1072 passed +``` + +Дополнительно выполнено: + +```text +python -m compileall src +``` + +Результат: + +```text +OK +``` + +Также выполнено: + +```text +git diff --check +``` + +Ошибок форматирования не обнаружено. + +--- + +# Архитектурный результат Build + +После завершения Build 060.5 REST Trades получили полностью независимый трехуровневый pipeline проверки данных. + +```text +REST JSON + │ + ▼ +Schema Validation + │ + ▼ +ValidatedRestAggTradesDocument + │ + ▼ +REST Parser + │ + ▼ +tuple[DzengiRestAggTrade] + │ + ▼ +REST Trade Value Validation + │ + ▼ +tuple[DzengiRestAggTrade] +``` + +Каждый этап отвечает исключительно за собственную область ответственности. + +Следующий Build (060.6) впервые переведет transport-модели в внутреннюю каноническую модель `Trade`, независимую от конкретной биржи. \ No newline at end of file diff --git a/docs/migrations/build_060_6.md b/docs/migrations/build_060_6.md index ea3d56a..3f11632 100644 --- a/docs/migrations/build_060_6.md +++ b/docs/migrations/build_060_6.md @@ -2185,42 +2185,42 @@ Production Integration — намеренно не выполнялась # Следующий этап -Следующим этапом развития серии Build 060 становится интеграция построенного Pipeline в рабочую подсистему получения данных. - -Наиболее логичным продолжением является: +Следующим этапом развития серии Build 060 становится: ```text -Build 060.7 — REST Trades Client +Build 060.7 — REST Trade Adapter ``` -На этом этапе будет реализован специализированный клиент получения агрегированных сделок через REST API Dzengi, использующий полностью сформированный Pipeline: +На этом этапе будет реализован адаптер REST-сделок, который объединит уже созданные компоненты обработки: ```text -REST Request - -↓ - -REST Response - -↓ - -Schema Validation - -↓ - +Validated REST document + ↓ Parser - -↓ - + ↓ Value Validation - -↓ - + ↓ Mapper - -↓ - + ↓ tuple[Trade] ``` -Build 060.7 станет первым этапом, на котором сформированный Pipeline начнёт использоваться в реальном процессе получения исторических сделок. \ No newline at end of file +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-доступа. \ No newline at end of file diff --git a/docs/migrations/build_060_7.md b/docs/migrations/build_060_7.md new file mode 100644 index 0000000..e138297 --- /dev/null +++ b/docs/migrations/build_060_7.md @@ -0,0 +1,1734 @@ +# Build 060.7 — REST Trade Adapter + +**Engineering Migration Report** + +--- + +# Контроль документа + +| Свойство | Значение | +|---|---| +| Build | 060.7 | +| Название | REST Trade Adapter | +| Статус | Завершён | +| Проект | Dzentra | +| Подсистема | Market Data Acquisition | +| Компонент | Trades Feed / Time & Sales | +| Версия документа | 1.0 | +| Дата завершения | 2026-07-19 | + +--- + +# Цель Build + +После завершения Build 060.6 подсистема получения исторических сделок уже содержала полностью сформированный конвейер преобразования транспортных данных. + +К этому моменту были реализованы: + +- транспортная модель агрегированной сделки REST API; +- проверка структуры REST-документа; +- parser транспортной модели; +- проверка корректности значений; +- mapper транспортной модели в Canonical Trade. + +После завершения предыдущего этапа полный pipeline имел следующий вид: + +```text +REST JSON + + │ + + ▼ + +Schema Validation + + │ + + ▼ + +ValidatedRestAggTradesDocument + + │ + + ▼ + +Parser + + │ + + ▼ + +tuple[DzengiRestAggTrade] + + │ + + ▼ + +Value Validation + + │ + + ▼ + +Mapper + + │ + + ▼ + +tuple[Trade] +``` + +Несмотря на завершённость всех отдельных компонентов, в архитектуре отсутствовал единый публичный интерфейс, объединяющий их в последовательную операцию обработки документа. + +Каждый вызывающий компонент должен был самостоятельно помнить: + +- какую функцию вызвать первой; +- какую второй; +- какой тип возвращает каждая стадия; +- какой порядок обработки является корректным. + +Подобная схема противоречит одной из основных целей Acquisition Layer — предоставить простой и безопасный интерфейс обработки рыночных данных. + +Следовательно возникла необходимость в отдельном компоненте, который не реализует новую бизнес-логику, а исключительно объединяет уже существующие стадии обработки. + +Именно эту задачу решает Build 060.7. + +В рамках данного Build реализуется исключительно REST Trade Adapter. + +Build намеренно **не включает**: + +- HTTP client; +- REST endpoint; +- получение документа; +- REST polling; +- Feed; +- Registry; +- Runtime Integration; +- Acquisition Service; +- Time & Sales Feed; +- объединение REST и WebSocket сделок; +- хранение истории; +- дедупликацию; +- сортировку; +- агрегацию; +- production-интеграцию. + +Все перечисленные задачи относятся к последующим Build. + +--- + +# Архитектурный контекст + +Во всей подсистеме Market Data Acquisition используется единый принцип построения pipeline. + +Каждый слой выполняет только одну строго определённую задачу. + +```text +Transport + + │ + + ▼ + +Schema Validation + + │ + + ▼ + +Parser + + │ + + ▼ + +Value Validation + + │ + + ▼ + +Mapper + + │ + + ▼ + +Canonical Model +``` + +Такой подход уже используется для: + +- Instrument; +- REST Quote; +- WebSocket Quote; +- REST Candle; +- WebSocket Candle. + +Однако для REST Trades отсутствовал последний объединяющий уровень. + +Вызов каждого слоя выполнялся вручную. + +Build 060.7 вводит единый Adapter Layer, который становится официальной точкой входа в REST Trade Pipeline. + +После завершения Build архитектура принимает следующий вид: + +```text +REST JSON + + │ + + ▼ + +Schema Validation + + │ + + ▼ + +ValidatedRestAggTradesDocument + + │ + + ▼ + +REST Trade Adapter + + │ + + ├──────── Parser + + ├──────── Value Validation + + └──────── Mapper + + │ + + ▼ + +tuple[Trade] +``` + +Таким образом Adapter становится исключительно координатором уже существующих компонентов. + +Сам Adapter не содержит собственной бизнес-логики. + +--- + +# Исходное состояние + +До начала Build 060.7 проект уже содержал следующие компоненты. + +```text +ValidatedRestAggTradesDocument + +DzengiRestAggTrade + +Trade + +validate_rest_agg_trades_schema() + +parse_rest_agg_trades() + +validate_rest_agg_trade_values() + +map_dzengi_rest_agg_trades_to_trades() +``` + +Все необходимые стадии обработки документа уже существовали. + +Отсутствовал только компонент, объединяющий их в единую последовательность. + +При этом ни один из существующих модулей не должен был изменяться. + +Это позволяло реализовать Build как полностью additive change. + +--- + +# Предварительный архитектурный аудит + +Перед началом реализации был повторно проанализирован существующий код проекта. + +Проверены следующие файлы: + +```text +app/src/market_data/acquisition/adapters/dzengi/parser.py + +app/src/market_data/acquisition/adapters/dzengi/mapper.py + +app/src/market_data/acquisition/validation/schema.py + +app/src/market_data/acquisition/validation/values.py + +app/src/market_data/acquisition/adapters/dzengi/websocket.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 + +docs/migrations/build_060_6.md +``` + +Кроме анализа кода был повторно выполнен аудит всей архитектуры Acquisition Pipeline. + +В результате были подтверждены следующие выводы. + +- Schema Validation уже является самостоятельным архитектурным слоем. +- Parser отвечает исключительно за построение transport-моделей. +- Value Validation отвечает исключительно за корректность значений. +- Mapper отвечает исключительно за построение Canonical Trade. +- Ни один из существующих компонентов не должен изменять собственную ответственность. +- Новый компонент должен выполнять исключительно композицию существующих этапов. + +Именно эти выводы легли в основу проектирования Build 060.7. + +--- + +# Архитектурная задача Build + +Главная задача Build заключается не в добавлении новой функциональности. + +Все необходимые операции обработки документа уже существуют. + +Задача Build состоит в создании единой точки входа в REST Trade Pipeline. + +После завершения Build вызывающий код должен работать только с одной публичной функцией: + +```python +adapt_rest_agg_trades_document(...) +``` + +Внутри неё последовательно выполняются: + +```text +Parser + +↓ + +Value Validation + +↓ + +Mapper +``` + +При этом вызывающий код полностью освобождается от знания внутренней структуры pipeline. + +Такое решение уменьшает связанность компонентов и существенно упрощает дальнейшее сопровождение системы. + +# Рассмотренные архитектурные решения + +Перед началом реализации Build было рассмотрено несколько вариантов построения REST Trade Adapter. + +Основная задача заключалась не в написании кода, а в определении его архитектурной ответственности. + +Именно на этом этапе было принято несколько решений, которые впоследствии стали архитектурными инвариантами всей подсистемы Acquisition. + +--- + +## Вариант 1 + +Разместить всю последовательность обработки непосредственно внутри будущего REST Source. + +Например: + +```text +HTTP + + │ + + ▼ + +JSON + + │ + + ▼ + +Schema Validation + + │ + + ▼ + +Parser + + │ + + ▼ + +Value Validation + + │ + + ▼ + +Mapper + + │ + + ▼ + +Trade +``` + +На первый взгляд подобная схема выглядит простой. + +Однако после анализа существующей архитектуры данный вариант был отклонён. + +Причины: + +- REST Source начинает заниматься обработкой данных; +- источник данных становится зависимым от транспортной модели; +- становится невозможно использовать Adapter повторно; +- нарушается разделение ответственности между слоями получения и обработки данных. + +В Dzentra источник данных должен отвечать исключительно за получение документа. + +Вся обработка документа относится к отдельному компоненту. + +--- + +## Вариант 2 + +Передавать в Adapter исходный REST JSON. + +Например: + +```python +adapt_rest_agg_trades_document( + document: object, + symbol="BTC/USDT", +) +``` + +При таком подходе Adapter самостоятельно вызывал бы: + +```text +validate_rest_agg_trades_schema() + +↓ + +parse_rest_agg_trades() + +↓ + +validate_rest_agg_trade_values() + +↓ + +map_dzengi_rest_agg_trades_to_trades() +``` + +Именно такой вариант первоначально рассматривался во время проектирования. + +После повторного анализа существующей архитектуры он был отклонён. + +Причины: + +Schema Validation уже является самостоятельным архитектурным уровнем. + +Если Adapter начинает самостоятельно выполнять проверку структуры документа, + +он получает сразу две ответственности: + +- обработку документа; +- проверку структуры документа. + +Это противоречит принятой архитектуре Acquisition Layer. + +Кроме того, подобный подход нарушал бы единообразие остальных pipeline проекта. + +Следовательно Schema Validation должна оставаться отдельным независимым этапом. + +--- + +## Утверждённое решение + +Adapter получает уже проверенный документ: + +```text +ValidatedRestAggTradesDocument +``` + +После этого выполняются только следующие операции: + +```text +Parser + +↓ + +Value Validation + +↓ + +Mapper +``` + +Таким образом Build полностью сохраняет разделение ответственности между слоями. + +--- + +# Почему Adapter не выполняет Schema Validation + +Во время проектирования данный вопрос обсуждался отдельно. + +На первый взгляд логично было бы сделать Adapter полностью автономным. + +То есть предоставить ему возможность принимать произвольный REST документ. + +Однако после анализа существующего проекта было подтверждено, + +что подобное решение нарушило бы уже сформированную архитектуру. + +К моменту начала Build в системе уже существовали независимые функции: + +```python +validate_quote_schema() + +validate_candle_schema() + +validate_rest_agg_trades_schema() + +validate_websocket_ohlc_schema() +``` + +Все они образуют самостоятельный слой Schema Validation. + +Следовательно Adapter не должен дублировать их ответственность. + +После Build 060.7 последовательность обработки выглядит следующим образом: + +```text +REST Document Source + +↓ + +Schema Validation + +↓ + +ValidatedRestAggTradesDocument + +↓ + +REST Trade Adapter +``` + +Такое разделение полностью соответствует архитектуре остальных компонентов проекта. + +--- + +# Почему Adapter реализован функцией + +Во время проектирования первоначально предполагалось реализовать Adapter как отдельный класс. + +Например: + +```python +class DzengiRestTradeAdapter: +``` + +Такой подход выглядит привычным для многих проектов. + +Однако после анализа существующего кода Acquisition Layer возник вопрос: + +имеет ли Adapter собственное состояние? + +Ответ оказался отрицательным. + +Adapter: + +- не хранит состояние; +- не содержит конфигурацию; +- не управляет ресурсами; +- не использует Dependency Injection; +- не предоставляет расширяемый интерфейс; +- не содержит полиморфизма. + +Фактически единственной его задачей является последовательный вызов уже существующих функций. + +Следовательно класс не предоставляет никаких преимуществ. + +Использование класса лишь увеличило бы объём кода и усложнило архитектуру. + +Поэтому было принято решение отказаться от него. + +--- + +# Новый архитектурный принцип + +Именно во время реализации Build 060.7 был окончательно сформулирован новый архитектурный принцип проекта. + +Он выглядит следующим образом. + +```text +Stateful + + ↓ + +Class +``` + +```text +Stateless + + ↓ + +Function +``` + +Именно этот принцип теперь применяется при проектировании новых компонентов Market Data Acquisition. + +--- + +# Почему выбран функциональный API + +После отказа от класса оставалось определить форму публичного интерфейса. + +Было принято решение использовать одну функцию: + +```python +adapt_rest_agg_trades_document() +``` + +Причины такого решения. + +Во-первых, + +весь Acquisition Layer уже использует функциональный стиль. + +Например: + +```python +parse_rest_agg_trades() + +validate_rest_agg_trade_values() + +map_dzengi_rest_agg_trades_to_trades() +``` + +Adapter становится естественным продолжением этой цепочки. + +Во-вторых, + +функция значительно проще тестируется. + +Она полностью детерминирована. + +При одинаковых входных данных всегда возвращает одинаковый результат. + +В-третьих, + +отсутствует необходимость создавать экземпляры класса, + +что делает публичный API проще для использования. + +--- + +# Почему файл называется rest_trade_adapter.py + +Во время проектирования обсуждалось несколько вариантов имени файла. + +Например: + +```text +trade_adapter.py +``` + +или + +```text +dzengi_rest_trade_adapter.py +``` + +После анализа структуры проекта было принято решение использовать: + +```text +rest_trade_adapter.py +``` + +Причины. + +Каталог уже содержит информацию о конкретной бирже: + +```text +adapters/dzengi/ +``` + +Следовательно повторять слово: + +```text +dzengi +``` + +в имени файла не требуется. + +Кроме того, + +имя: + +```text +trade_adapter.py +``` + +оказалось слишком общим. + +В будущем в проекте могут появиться: + +- websocket_trade_adapter.py; +- replay_trade_adapter.py; +- simulated_trade_adapter.py. + +Название: + +```text +rest_trade_adapter.py +``` + +однозначно определяет назначение компонента и соответствует принятому правилу уникальности имён файлов. + +--- + +# Новый публичный API + +Build 060.7 вводит единственную публичную функцию. + +```python +adapt_rest_agg_trades_document() +``` + +Назначение функции: + +- принимает документ, уже прошедший Schema Validation; +- запускает Parser; +- выполняет Value Validation; +- выполняет Mapper; +- возвращает immutable tuple Canonical Trade. + +Сигнатура имеет следующий вид: + +```text +ValidatedRestAggTradesDocument + + │ + + ▼ + +tuple[Trade] +``` + +Внутренняя реализация полностью скрыта от вызывающего кода. + +После завершения Build именно эта функция становится официальной точкой входа REST Trade Pipeline. + +# Семантика REST Trade Adapter + +После завершения Build 060.7 Adapter становится единственной точкой композиции всех ранее реализованных компонентов обработки REST Trade. + +При этом сам Adapter намеренно остаётся максимально простым. + +Его ответственность ограничивается исключительно последовательным вызовом уже существующих функций. + +Полная последовательность обработки выглядит следующим образом. + +```text +ValidatedRestAggTradesDocument + + │ + + ▼ + +parse_rest_agg_trades() + + │ + + ▼ + +tuple[DzengiRestAggTrade] + + │ + + ▼ + +validate_rest_agg_trade_values() + + │ + + ▼ + +tuple[DzengiRestAggTrade] + + │ + + ▼ + +map_dzengi_rest_agg_trades_to_trades() + + │ + + ▼ + +tuple[Trade] +``` + +Adapter не выполняет никаких дополнительных преобразований. + +Он лишь организует последовательность уже существующих этапов. + +--- + +# Почему Adapter не содержит собственной логики + +Во время проектирования обсуждался естественный вопрос. + +Если Adapter является самостоятельным компонентом, + +не должен ли он выполнять хотя бы часть бизнес-логики? + +Ответ оказался отрицательным. + +Все необходимые операции уже реализованы в предыдущих Build. + +Повторное выполнение каких-либо проверок внутри Adapter привело бы к: + +- дублированию кода; +- нарушению принципа Single Responsibility; +- усложнению сопровождения; +- появлению двух различных реализаций одной и той же логики. + +Поэтому Adapter сознательно остаётся максимально "тонким". + +Его задача — исключительно композиция. + +--- + +# Почему Adapter не перехватывает исключения + +Во время реализации рассматривался вариант, + +при котором Adapter преобразовывал бы ошибки всех нижележащих компонентов в единый тип исключения. + +Например: + +```text +TradeAdapterError +``` + +После анализа архитектуры данный вариант был отклонён. + +Причины. + +Каждый уровень Acquisition Pipeline уже обладает собственным специализированным типом исключений. + +```text +TradeSchemaError + +↓ + +TradeParseError + +↓ + +TradeValueError + +↓ + +TradeMappingError +``` + +Если Adapter начнёт преобразовывать их в единый тип, + +будет потеряна информация о том, + +на каком именно этапе возникла ошибка. + +Это существенно ухудшит диагностику системы. + +Поэтому Adapter намеренно не перехватывает исключения. + +Все ошибки пробрасываются вызывающему компоненту без изменений. + +--- + +# Почему Adapter не изменяет порядок выполнения этапов + +Последовательность вызова компонентов также обсуждалась отдельно. + +Теоретически можно было изменить порядок: + +```text +Parser + +↓ + +Mapper + +↓ + +Validation +``` + +или + +```text +Validation + +↓ + +Parser +``` + +Оба варианта были отклонены. + +Причины очевидны. + +Parser невозможно выполнить до проверки структуры документа. + +Mapper невозможно выполнить до проверки корректности значений. + +Следовательно единственная корректная последовательность выглядит следующим образом. + +```text +Schema Validation + +↓ + +Parser + +↓ + +Value Validation + +↓ + +Mapper +``` + +Никакой другой порядок не соответствует архитектуре Acquisition Layer. + +--- + +# Почему Adapter возвращает tuple + +Все предыдущие Build закрепили использование immutable-коллекций. + +Поэтому Adapter также возвращает: + +```python +tuple[Trade] +``` + +Использование списка не рассматривалось. + +Причины. + +Immutable-коллекция: + +- безопаснее при передаче между слоями; +- исключает случайное изменение результата; +- соответствует остальным компонентам Acquisition; +- делает контракт функции полностью предсказуемым. + +Таким образом Build полностью сохраняет существующий архитектурный стиль проекта. + +--- + +# Что намеренно не делает Adapter + +Build 060.7 специально не добавляет никакой дополнительной обработки данных. + +Adapter намеренно: + +не выполняет HTTP-запросы; + +не получает REST-документ; + +не выполняет Schema Validation; + +не сортирует сделки; + +не удаляет дубликаты; + +не объединяет REST и WebSocket данные; + +не агрегирует сделки; + +не рассчитывает статистику; + +не анализирует последовательность сделок; + +не создаёт свечи; + +не взаимодействует с Runtime; + +не взаимодействует с Feed; + +не взаимодействует с Registry; + +не сохраняет данные; + +не ведёт журнал обработки. + +Все перечисленные задачи относятся к другим архитектурным слоям и намеренно исключены из области ответственности Adapter. + +--- + +# Изменённые файлы + +В рамках Build 060.7 был добавлен один новый файл проекта. + +```text +app/src/market_data/acquisition/adapters/dzengi/rest_trade_adapter.py +``` + +Также был добавлен новый набор unit-тестов. + +```text +app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py +``` + +Кроме подготовки инженерной документации Build никакие другие файлы проекта не изменялись. + +Build полностью соответствует принятому принципу **Local Additive Change**. + +--- + +# Изменения в rest_trade_adapter.py + +Новый файл содержит единственную публичную функцию. + +```python +adapt_rest_agg_trades_document() +``` + +Внутри неё отсутствует какая-либо собственная логика обработки. + +Функция последовательно вызывает: + +```python +parse_rest_agg_trades() + +↓ + +validate_rest_agg_trade_values() + +↓ + +map_dzengi_rest_agg_trades_to_trades() +``` + +После завершения последнего этапа результат сразу возвращается вызывающему компоненту. + +Таким образом Adapter становится исключительно координационным уровнем REST Trade Pipeline. + +--- + +# Изменения в unit-тестах + +Для нового Adapter создан отдельный файл тестов. + +```text +tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py +``` + +Это соответствует принятому соглашению проекта, + +согласно которому каждый самостоятельный компонент Acquisition Layer имеет собственный независимый набор unit-тестов. + +Существующие тесты: + +- Parser; +- Value Validation; +- Mapper; + +не изменялись. + +Build полностью additive. + +--- + +# Проверяемые сценарии + +Новый набор тестов проверяет исключительно ответственность Adapter. + +Подтверждаются следующие сценарии. + +- успешная обработка корректного документа; +- корректная передача symbol в Mapper; +- корректная обработка пустого документа; +- корректное пробрасывание исключений нижележащих компонентов. + +Тем самым подтверждается, + +что Adapter не изменяет семантику работы уже существующих этапов обработки. + +При этом логика Parser, Validation и Mapper продолжает проверяться их собственными независимыми тестами. + +# Что намеренно не изменялось + +Build 060.7 специально не изменяет существующие компоненты Acquisition Pipeline. + +Без изменений остаются: + +```text +app/src/market_data/acquisition/adapters/dzengi/parser.py + +app/src/market_data/acquisition/adapters/dzengi/mapper.py + +app/src/market_data/acquisition/validation/schema.py + +app/src/market_data/acquisition/validation/values.py + +app/src/market_data/acquisition/models/trade.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; +- выполнение HTTP-запросов; +- получение исторических сделок; +- REST polling; +- объединение REST и WebSocket Trade; +- Time & Sales Feed; +- Runtime Integration; +- хранение истории сделок; +- кэширование; +- дедупликацию; +- сортировку; +- агрегацию; +- вычисление аналитики; +- построение свечей; +- взаимодействие с Trading Layer. + +Все перечисленные задачи реализуются отдельными Build согласно утверждённой дорожной карте. + +Таким образом Build 060.7 остаётся полностью локальным и не выходит за пределы собственной архитектурной ответственности. + +--- + +# Проверка компиляции + +После завершения реализации была выполнена проверка компиляции проекта. + +Команда выполнялась из каталога: + +```text +~/vsprojects/dzentra_bot/app +``` + +При активированном виртуальном окружении: + +```bash +source .venv/bin/activate +``` + +Выполнена команда: + +```bash +python -m compileall src +``` + +Результат: + +```text +успешно +``` + +Все модули проекта успешно скомпилированы. + +Ошибок синтаксиса не обнаружено. + +Build не нарушил корректность структуры проекта. + +--- + +# Проверка локальных unit-тестов + +После завершения реализации Adapter были выполнены специализированные unit-тесты. + +Команда: + +```bash +python -m pytest \ +tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py \ +-q +``` + +Результат: + +```text +4 passed in 0.02s +``` + +Проверки подтвердили: + +- корректную последовательность вызова компонентов; +- корректную передачу параметра symbol; +- корректную обработку пустого документа; +- корректное пробрасывание исключений. + +Ни одна существующая проверка проекта не была нарушена. + +--- + +# Полный regression suite + +После завершения Build выполнен полный набор тестов проекта. + +Команда: + +```bash +python -m pytest +``` + +Результат: + +```text +1096 passed in 2.59s +``` + +Регрессий не обнаружено. + +Все ранее реализованные Build продолжают работать без изменений. + +Это подтверждает, + +что Build 060.7 полностью соответствует принципу **Local Additive Change**. + +--- + +# Проверка форматирования + +После завершения работы выполнена команда: + +```bash +git diff --check +``` + +Вывод отсутствует. + +Это подтверждает отсутствие: + +- trailing whitespace; +- лишних пробелов; +- нарушений форматирования; +- ошибок оформления diff. + +Build соответствует принятым требованиям оформления исходного кода. + +--- + +# Контроль размещения новой функциональности + +После завершения Build вся новая функциональность сосредоточена только в предназначенных для неё компонентах. + +Adapter расположен исключительно в: + +```text +src/market_data/acquisition/adapters/dzengi/rest_trade_adapter.py +``` + +Unit-тесты расположены исключительно в: + +```text +tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py +``` + +Другие подсистемы проекта Build не затрагивает. + +Архитектурная изоляция полностью сохранена. + +--- + +# Состояние Git + +После завершения Build выполнена команда: + +```bash +git status +``` + +Для Build 060.7 зафиксированы изменения: + +```text +added: + +app/src/market_data/acquisition/adapters/dzengi/rest_trade_adapter.py + +app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py + +untracked: + +docs/migrations/build_060_7.md +``` + +Ветка разработки: + +```text +main +``` + +опережает `origin/main`. + +Данное состояние соответствует текущему процессу разработки и не связано с архитектурой Build. + +--- + +# Фактический diff + +В рамках Build добавлен новый компонент: + +```text +app/src/market_data/acquisition/adapters/dzengi/rest_trade_adapter.py +``` + +Публичный API Build: + +```python +adapt_rest_agg_trades_document() +``` + +Функция объединяет: + +```text +parse_rest_agg_trades() + +↓ + +validate_rest_agg_trade_values() + +↓ + +map_dzengi_rest_agg_trades_to_trades() +``` + +Кроме того, + +добавлен отдельный набор unit-тестов: + +```text +tests/unit/market_data/acquisition/adapters/dzengi/test_rest_trade_adapter.py +``` + +Все изменения являются полностью additive. + +Никакое существующее поведение системы не изменялось. + +--- + +# Архитектурный результат + +После завершения Build 060.7 REST Trade Pipeline впервые получает официальную единую точку входа. + +Полная архитектура теперь выглядит следующим образом. + +```text +REST JSON + + │ + + ▼ + +Schema Validation + + │ + + ▼ + +ValidatedRestAggTradesDocument + + │ + + ▼ + +REST Trade Adapter + + │ + + ▼ + +Parser + + │ + + ▼ + +Value Validation + + │ + + ▼ + +Mapper + + │ + + ▼ + +tuple[Trade] +``` + +Каждый слой обладает собственной зоной ответственности. + +Ни один слой не выполняет задачи соседнего. + +Adapter становится исключительно координатором существующих компонентов. + +Это полностью соответствует принципам: + +- Single Responsibility; +- Local Additive Change; +- Transport Isolation; +- Canonical Model Separation. + +Кроме того, + +Build 060.7 окончательно закрепляет разделение между: + +- получением документа; +- обработкой документа; +- построением предметной модели. + +Именно это разделение станет основой следующего этапа развития REST Trade Pipeline. + +# Влияние на последующие Build + +Build 060.7 завершает формирование логического слоя обработки REST Trade Document. + +После его окончания все последующие компоненты Acquisition Layer могут использовать единый публичный интерфейс: + +```python +adapt_rest_agg_trades_document() +``` + +При этом они полностью освобождаются от необходимости знать внутреннее устройство Pipeline. + +Следующие Build будут работать исключительно через данный Adapter. + +Это позволяет изменять внутреннюю реализацию Parser, Validation или Mapper без изменения внешнего API обработки документа. + +Таким образом Adapter становится стабильной границей между верхними и нижними слоями Acquisition. + +--- + +# Критерии завершения + +Build 060.7 считается полностью завершённым, поскольку: + +- проведён повторный архитектурный аудит существующего Pipeline; +- реализован отдельный REST Trade Adapter; +- определена единственная публичная точка входа обработки документа; +- сохранено разделение ответственности между Schema Validation, Parser, Value Validation и Mapper; +- Adapter не содержит собственной бизнес-логики; +- Adapter не выполняет HTTP-запросы; +- Adapter не выполняет Schema Validation; +- Adapter не изменяет порядок обработки данных; +- Adapter не сортирует сделки; +- Adapter не удаляет дубликаты; +- Adapter не агрегирует данные; +- Adapter не изменяет транспортные модели; +- Adapter не перехватывает исключения нижележащих компонентов; +- compile-проверка успешно пройдена; +- локальные unit-тесты успешно пройдены; +- полный regression suite успешно пройден; +- проверка форматирования успешно пройдена; +- scope Build не расширен. + +--- + +# Архитектурные инварианты + +После завершения Build 060.7 следующие свойства REST Trade Pipeline считаются архитектурным контрактом проекта. + +Изменение любого из перечисленных инвариантов требует отдельного архитектурного решения. + +--- + +## Инвариант 1 + +Schema Validation остаётся самостоятельным слоем Acquisition Pipeline. + +Никакие Adapter не должны выполнять проверку структуры документа самостоятельно. + +Полная последовательность всегда начинается с отдельного этапа Schema Validation. + +--- + +## Инвариант 2 + +REST Trade Adapter получает только уже проверенный документ. + +Тип входного параметра: + +```text +ValidatedRestAggTradesDocument +``` + +Использование необработанного REST JSON внутри Adapter не допускается. + +--- + +## Инвариант 3 + +REST Trade Adapter отвечает исключительно за композицию существующих компонентов. + +Он не содержит собственной бизнес-логики. + +--- + +## Инвариант 4 + +REST Trade Adapter не изменяет последовательность обработки. + +Pipeline всегда имеет следующий вид: + +```text +Schema Validation + + │ + + ▼ + +Parser + + │ + + ▼ + +Value Validation + + │ + + ▼ + +Mapper +``` + +Изменение порядка выполнения данных этапов не допускается. + +--- + +## Инвариант 5 + +REST Trade Adapter не перехватывает исключения нижележащих компонентов. + +Ошибки каждого уровня должны сохранять собственный специализированный тип. + +--- + +## Инвариант 6 + +REST Trade Adapter не создаёт предметные объекты самостоятельно. + +Создание экземпляров: + +```text +Trade +``` + +остаётся исключительной ответственностью Mapper. + +--- + +## Инвариант 7 + +REST Trade Adapter не зависит от механизма получения документа. + +Он не знает: + +- каким способом был получен REST JSON; +- из какого HTTP-клиента он поступил; +- выполнялся ли запрос повторно; +- использовалось ли кэширование. + +Эти вопросы относятся исключительно к Source Layer. + +--- + +## Инвариант 8 + +Stateless-компоненты реализуются функциями. + +Если компонент: + +- не хранит состояние; +- не содержит конфигурации; +- не управляет жизненным циклом; +- не требует Dependency Injection; +- не реализует полиморфизм, + +он должен быть реализован функцией. + +--- + +## Инвариант 9 + +Stateful-компоненты реализуются классами. + +Класс используется только в случаях, когда компонент: + +- хранит состояние; +- управляет ресурсами; +- содержит конфигурацию; +- инкапсулирует зависимости; +- предоставляет расширяемый интерфейс. + +Использование класса при отсутствии состояния считается нарушением архитектурного стиля проекта. + +--- + +## Инвариант 10 + +REST Trade Adapter является единственной официальной точкой запуска полного REST Trade Pipeline. + +Все последующие компоненты системы должны использовать именно его. + +Самостоятельный последовательный вызов: + +```text +Parser + +↓ + +Value Validation + +↓ + +Mapper +``` + +за пределами Adapter не допускается. + +--- + +# Итог + +**Build 060.7 завершён успешно.** + +Текущее состояние REST Trade Pipeline: + +```text +Transport Model — реализована + +Schema Validation — реализована + +Parser — реализован + +Value Validation — реализована + +Mapper — реализован + +REST Trade Adapter — реализован + +Canonical Trade — используется + +Compile check — успешно + +Target tests — 4 passed + +Full regression suite — 1096 passed + +Whitespace check — успешно + +Production Integration — намеренно не выполнялась +``` + +После завершения Build 060.7 подсистема получения исторических сделок получила единый координационный слой обработки документа. + +Вся логика преобразования REST-документа в канонические сделки теперь доступна через единый публичный интерфейс, при этом все ранее реализованные компоненты сохранили независимость и собственную архитектурную ответственность. + +--- + +# Следующий этап + +Следующим этапом развития серии Build 060 становится: + +```text +Build 060.8 — REST Trades Document Source +``` + +На данном этапе будет реализован компонент, отвечающий исключительно за получение документа из REST API и его структурную проверку. + +После завершения Build 060.8 полный REST Pipeline примет следующий вид: + +```text +HTTP Client + + │ + + ▼ + +REST API + + │ + + ▼ + +JSON + + │ + + ▼ + +Schema Validation + + │ + + ▼ + +ValidatedRestAggTradesDocument + + │ + + ▼ + +REST Trade Adapter + + │ + + ▼ + +Parser + + │ + + ▼ + +Value Validation + + │ + + ▼ + +Mapper + + │ + + ▼ + +tuple[Trade] +``` + +Таким образом будут окончательно разделены три независимые архитектурные области: + +- получение документа; +- обработка документа; +- построение канонической модели. + +После этого REST-подсистема получения сделок будет полностью подготовлена к построению **Trades Feed (Time & Sales)** и последующему объединению исторических REST-сделок с потоком WebSocket в рамках единого конвейера обработки рыночных данных. \ No newline at end of file