Build 060.23: integrate Trade Stream Acquisition

This commit is contained in:
2026-07-28 07:20:09 +03:00
parent 9953bab993
commit ee8765b716
7 changed files with 2989 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
# app/src/market_data/acquisition/trade_stream_acquisition_protocol.py
from __future__ import annotations
"""
Публичный контракт интеграции Trade Stream в Acquisition Layer.
Build 060.23 вводит единую сервисную границу между:
- Trade Subscription Layer;
- Acquisition Runtime Service;
- Unified WebSocket Adapter;
- Trade Stream Consistency.
Контракт не определяет production WebSocket transport, receive-loop,
reconnect, recovery, хранение или публикацию канонических Trade.
"""
from typing import Protocol, runtime_checkable
from src.market_data.acquisition.models.trade import Trade
@runtime_checkable
class TradeStreamAcquisitionServiceProtocol(Protocol):
"""
Контракт сервиса интеграции Trade Stream.
Сервис отвечает за:
- передачу команды подписки в Acquisition Runtime;
- преобразование одного входящего WebSocket-документа;
- передачу канонического Trade в Consistency Layer;
- возврат согласованного Trade либо None для дубликата.
Сервис не владеет WebSocket lifecycle и не запускает receive-loop.
"""
async def subscribe(
self,
symbols: tuple[str, ...],
*,
correlation_id: str | None = None,
) -> None:
"""
Передать в Acquisition Runtime команду подписки Trade Stream.
Args:
symbols:
Символы торговых инструментов для подписки.
correlation_id:
Необязательный идентификатор транспортного запроса.
При отсутствии значение создаётся Subscription Builder.
"""
...
def handle_message(
self,
document: object,
) -> Trade | None:
"""
Обработать один входящий WebSocket-документ.
Args:
document:
Сырой документ, полученный из WebSocket Runtime.
Returns:
Канонический Trade после проверки согласованности;
None, если сообщение не относится к Trade либо является
корректным дубликатом.
"""
...

View File

@@ -0,0 +1,111 @@
# app/src/market_data/acquisition/trade_stream_acquisition_service.py
from __future__ import annotations
"""
Сервис интеграции Trade Stream с инфраструктурой Acquisition Runtime.
Build 060.23 вводит сервисную границу между:
- Trade Subscription Layer;
- Acquisition Runtime Service;
- Unified WebSocket Adapter;
- Trade Stream Consistency.
На текущем этапе реализована передача команды подписки.
Обработка входящих сообщений будет добавлена следующим шагом Build.
"""
from src.market_data.acquisition.trade_stream_message_adapter_protocol import (
TradeStreamMessageAdapterProtocol,
)
from src.market_data.acquisition.consistency.trade_stream_protocol import (
TradeStreamConsistencyProtocol,
)
from src.market_data.acquisition.models.trade import Trade
from src.market_data.acquisition.runtime.acquisition_runtime_service_protocol import (
AcquisitionRuntimeServiceProtocol,
)
from src.market_data.acquisition.subscriptions.trades import (
build_trade_subscribe_command,
)
from src.market_data.acquisition.trade_stream_acquisition_protocol import (
TradeStreamAcquisitionServiceProtocol,
)
class TradeStreamAcquisitionService(
TradeStreamAcquisitionServiceProtocol,
):
"""
Координатор инфраструктуры получения Trade Stream.
Сервис объединяет:
- Acquisition Runtime;
- Unified WebSocket Adapter;
- Trade Stream Consistency.
При этом сам сервис не реализует транспорт,
жизненный цикл WebSocket либо Recovery.
"""
def __init__(
self,
runtime_service: AcquisitionRuntimeServiceProtocol,
adapter: TradeStreamMessageAdapterProtocol,
consistency_controller: TradeStreamConsistencyProtocol,
) -> None:
self._runtime_service = runtime_service
self._adapter = adapter
self._consistency_controller = consistency_controller
async def subscribe(
self,
symbols: tuple[str, ...],
*,
correlation_id: str | None = None,
) -> None:
"""
Передать в Acquisition Runtime команду подписки Trade Stream.
Args:
symbols:
Символы торговых инструментов для подписки.
correlation_id:
Необязательный идентификатор транспортного запроса.
При отсутствии значение создаётся Subscription Builder.
"""
command = build_trade_subscribe_command(
symbols,
correlation_id=correlation_id,
)
await self._runtime_service.dispatch(command)
def handle_message(
self,
document: object,
) -> Trade | None:
"""
Обработать один входящий WebSocket-документ.
Документ преобразуется через Unified Adapter. Только канонический
Trade передаётся в Trade Stream Consistency.
Args:
document:
Сырой WebSocket-документ.
Returns:
Trade после проверки согласованности;
None, если сообщение не относится к Trade либо является
корректным дубликатом.
"""
result = self._adapter.map_message(document)
if not isinstance(result, Trade):
return None
return self._consistency_controller.accept(result)

View File

@@ -0,0 +1,40 @@
# app/src/market_data/acquisition/trade_stream_message_adapter_protocol.py
from __future__ import annotations
"""
Контракт адаптера входящих сообщений Trade Stream.
Build 060.23 отделяет сервис координации Acquisition
от конкретной реализации WebSocket-протокола биржи.
"""
from typing import Protocol, runtime_checkable
from src.market_data.acquisition.models.candle_close import (
CandleCloseEvent,
)
from src.market_data.acquisition.models.quote import Quote
from src.market_data.acquisition.models.trade import Trade
TradeStreamMappedMessage = Quote | CandleCloseEvent | Trade
@runtime_checkable
class TradeStreamMessageAdapterProtocol(Protocol):
"""
Контракт преобразования одного входящего WebSocket-документа.
Реализация может быть exchange-specific, но вызывающий сервис
зависит только от результата преобразования.
"""
def map_message(
self,
document: object,
) -> TradeStreamMappedMessage:
"""
Преобразовать один WebSocket-документ в каноническую модель.
"""
...

View File

@@ -0,0 +1,117 @@
# app/tests/unit/market_data/acquisition/test_dzengi_trade_stream_acquisition_integration.py
from __future__ import annotations
from datetime import datetime, timezone
from decimal import Decimal
from src.market_data.acquisition.adapters.dzengi.websocket import (
DzengiUnifiedWebSocketAdapter,
)
from src.market_data.acquisition.models.trade import (
Trade,
TradeAggressorSide,
)
from src.market_data.acquisition.runtime.websocket_protocol import (
AcquisitionRuntimeCommand,
)
from src.market_data.acquisition.trade_stream_acquisition_service import (
TradeStreamAcquisitionService,
)
class FakeRuntimeService:
def __init__(self) -> None:
self.commands: list[AcquisitionRuntimeCommand] = []
async def dispatch(
self,
command: AcquisitionRuntimeCommand,
) -> None:
self.commands.append(command)
class RecordingConsistencyController:
def __init__(self) -> None:
self.accepted_trades: list[Trade] = []
def accept(
self,
trade: Trade,
) -> Trade | None:
self.accepted_trades.append(trade)
return trade
def _dzengi_trade_document() -> object:
return {
"status": "OK",
"destination": "internal.trade",
"payload": {
"id": 2134857062,
"price": "64497.25",
"size": "0.005",
"ts": 1784218066823,
"symbol": "BTC/USD_LEVERAGE",
"buyer": True,
"orderId": "order-123",
},
}
def test_dzengi_trade_document_passes_complete_acquisition_pipeline() -> None:
runtime = FakeRuntimeService()
consistency = RecordingConsistencyController()
service = TradeStreamAcquisitionService(
runtime_service=runtime,
adapter=DzengiUnifiedWebSocketAdapter(),
consistency_controller=consistency,
)
result = service.handle_message(
_dzengi_trade_document(),
)
assert isinstance(result, Trade)
assert consistency.accepted_trades == [result]
assert result.symbol == "BTC/USD_LEVERAGE"
assert result.trade_id == 2134857062
assert result.price == Decimal("64497.25")
assert result.quantity == Decimal("0.005")
assert result.executed_at == datetime.fromtimestamp(
1784218066823 / 1000,
tz=timezone.utc,
)
assert result.aggressor_side is TradeAggressorSide.BUY
assert result.source == "dzengi_websocket_trade"
def test_dzengi_duplicate_trade_is_rejected_by_real_consistency_pipeline() -> None:
from src.market_data.acquisition.consistency.trade_stream_consistency_controller import (
TradeStreamConsistencyController,
)
from src.market_data.acquisition.consistency.trade_stream_state_store import (
TradeStreamStateStore,
)
runtime = FakeRuntimeService()
consistency = TradeStreamConsistencyController(
state_store=TradeStreamStateStore(),
)
service = TradeStreamAcquisitionService(
runtime_service=runtime,
adapter=DzengiUnifiedWebSocketAdapter(),
consistency_controller=consistency,
)
document = _dzengi_trade_document()
first_result = service.handle_message(document)
duplicate_result = service.handle_message(document)
assert isinstance(first_result, Trade)
assert duplicate_result is None

View File

@@ -0,0 +1,383 @@
# app/tests/unit/market_data/acquisition/test_trade_stream_acquisition_service.py
from __future__ import annotations
import asyncio
import json
from datetime import datetime, timezone
from decimal import Decimal
from typing import cast
import pytest
from src.market_data.acquisition.models.candle_close import (
CandleCloseEvent,
)
from src.market_data.acquisition.models.quote import Quote
from src.market_data.acquisition.models.trade import (
Trade,
TradeAggressorSide,
)
from src.market_data.acquisition.runtime.runtime_commands import (
SubscribeCommand,
)
from src.market_data.acquisition.runtime.transport_messages import (
TransportTextMessage,
)
from src.market_data.acquisition.runtime.websocket_protocol import (
AcquisitionRuntimeCommand,
)
from src.market_data.acquisition.trade_stream_acquisition_protocol import (
TradeStreamAcquisitionServiceProtocol,
)
from src.market_data.acquisition.trade_stream_acquisition_service import (
TradeStreamAcquisitionService,
)
from src.market_data.acquisition.trade_stream_message_adapter_protocol import (
TradeStreamMappedMessage,
)
class FakeRuntimeService:
def __init__(self) -> None:
self.commands: list[AcquisitionRuntimeCommand] = []
async def dispatch(
self,
command: AcquisitionRuntimeCommand,
) -> None:
self.commands.append(command)
class FakeMessageAdapter:
def __init__(
self,
result: TradeStreamMappedMessage,
*,
error: Exception | None = None,
) -> None:
self._result = result
self._error = error
self.documents: list[object] = []
def map_message(
self,
document: object,
) -> TradeStreamMappedMessage:
self.documents.append(document)
if self._error is not None:
raise self._error
return self._result
class FakeConsistencyController:
def __init__(
self,
result: Trade | None = None,
*,
use_input_trade: bool = True,
error: Exception | None = None,
) -> None:
self._result = result
self._use_input_trade = use_input_trade
self._error = error
self.accepted_trades: list[Trade] = []
def accept(
self,
trade: Trade,
) -> Trade | None:
self.accepted_trades.append(trade)
if self._error is not None:
raise self._error
if self._use_input_trade:
return trade
return self._result
def _trade(
*,
trade_id: int = 2134857062,
) -> Trade:
return Trade(
symbol="BTC/USD_LEVERAGE",
trade_id=trade_id,
price=Decimal("64497.25"),
quantity=Decimal("0.005"),
executed_at=datetime(
2026,
7,
16,
11,
27,
46,
823000,
tzinfo=timezone.utc,
),
aggressor_side=TradeAggressorSide.BUY,
source="dzengi",
)
def _non_trade_quote() -> Quote:
return cast(
Quote,
object.__new__(Quote),
)
def _non_trade_candle() -> CandleCloseEvent:
return cast(
CandleCloseEvent,
object.__new__(CandleCloseEvent),
)
def create_service(
*,
adapter_result: TradeStreamMappedMessage | None = None,
adapter_error: Exception | None = None,
consistency_result: Trade | None = None,
consistency_uses_input_trade: bool = True,
consistency_error: Exception | None = None,
) -> tuple[
TradeStreamAcquisitionService,
FakeRuntimeService,
FakeMessageAdapter,
FakeConsistencyController,
]:
runtime = FakeRuntimeService()
adapter = FakeMessageAdapter(
adapter_result if adapter_result is not None else _trade(),
error=adapter_error,
)
consistency = FakeConsistencyController(
consistency_result,
use_input_trade=consistency_uses_input_trade,
error=consistency_error,
)
service = TradeStreamAcquisitionService(
runtime_service=runtime,
adapter=adapter,
consistency_controller=consistency,
)
return (
service,
runtime,
adapter,
consistency,
)
def test_service_implements_protocol() -> None:
service, _, _, _ = create_service()
assert isinstance(
service,
TradeStreamAcquisitionServiceProtocol,
)
def test_subscribe_dispatches_subscribe_command() -> None:
service, runtime, _, _ = create_service()
asyncio.run(
service.subscribe(
(
"BTCUSDT",
"ETHUSDT",
)
)
)
assert len(runtime.commands) == 1
command = runtime.commands[0]
assert isinstance(
command,
SubscribeCommand,
)
def test_subscribe_preserves_symbols() -> None:
service, runtime, _, _ = create_service()
asyncio.run(
service.subscribe(
(
"BTCUSDT",
"ETHUSDT",
)
)
)
command = runtime.commands[0]
assert isinstance(
command,
SubscribeCommand,
)
assert "BTCUSDT" in command.subscription_key
assert "ETHUSDT" in command.subscription_key
def test_subscribe_accepts_correlation_id() -> None:
service, runtime, _, _ = create_service()
asyncio.run(
service.subscribe(
(
"BTCUSDT",
),
correlation_id="corr-123",
)
)
command = runtime.commands[0]
assert isinstance(command, SubscribeCommand)
assert isinstance(command.message, TransportTextMessage)
document = json.loads(command.message.payload)
assert document["correlationId"] == "corr-123"
def test_handle_message_passes_trade_to_consistency() -> None:
trade = _trade()
document = {"destination": "internal.trade"}
service, _, adapter, consistency = create_service(
adapter_result=trade,
)
result = service.handle_message(document)
assert adapter.documents == [document]
assert consistency.accepted_trades == [trade]
assert result is trade
def test_handle_message_preserves_consistency_result_identity() -> None:
adapted_trade = _trade(trade_id=100)
consistency_result = _trade(trade_id=101)
service, _, _, consistency = create_service(
adapter_result=adapted_trade,
consistency_result=consistency_result,
consistency_uses_input_trade=False,
)
result = service.handle_message(
{"destination": "internal.trade"},
)
assert consistency.accepted_trades == [adapted_trade]
assert result is consistency_result
def test_handle_message_returns_none_for_duplicate() -> None:
trade = _trade()
service, _, _, consistency = create_service(
adapter_result=trade,
consistency_result=None,
consistency_uses_input_trade=False,
)
result = service.handle_message(
{"destination": "internal.trade"},
)
assert consistency.accepted_trades == [trade]
assert result is None
def test_handle_message_ignores_quote() -> None:
quote = _non_trade_quote()
document = {"destination": "quote"}
service, _, adapter, consistency = create_service(
adapter_result=quote,
)
result = service.handle_message(document)
assert adapter.documents == [document]
assert consistency.accepted_trades == []
assert result is None
def test_handle_message_ignores_candle_close_event() -> None:
candle = _non_trade_candle()
document = {"destination": "ohlc"}
service, _, adapter, consistency = create_service(
adapter_result=candle,
)
result = service.handle_message(document)
assert adapter.documents == [document]
assert consistency.accepted_trades == []
assert result is None
def test_handle_message_calls_adapter_once() -> None:
document = {"destination": "internal.trade"}
service, _, adapter, _ = create_service()
service.handle_message(document)
assert adapter.documents == [document]
def test_handle_message_propagates_adapter_error() -> None:
original = RuntimeError("adapter failed")
service, _, adapter, consistency = create_service(
adapter_error=original,
)
with pytest.raises(RuntimeError, match="adapter failed") as exc_info:
service.handle_message(
{"destination": "internal.trade"},
)
assert exc_info.value is original
assert len(adapter.documents) == 1
assert consistency.accepted_trades == []
def test_handle_message_propagates_consistency_error() -> None:
original = RuntimeError("consistency failed")
trade = _trade()
service, _, adapter, consistency = create_service(
adapter_result=trade,
consistency_error=original,
)
with pytest.raises(
RuntimeError,
match="consistency failed",
) as exc_info:
service.handle_message(
{"destination": "internal.trade"},
)
assert exc_info.value is original
assert len(adapter.documents) == 1
assert consistency.accepted_trades == [trade]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,845 @@
# Build 060.23 — Trade Stream Acquisition Integration Architecture
**Статус:** Architecture Approved
**Build:** 060.23
**Название:**
Trade Stream Acquisition Integration
---
# 1. Назначение Build
Build 060.23 завершает построение инфраструктурного слоя Acquisition для Trades Feed.
После Build 060.22 Runtime уже обладает собственной сервисной моделью управления инфраструктурными командами:
- Runtime Commands;
- Runtime Events;
- Runtime Service;
- Runtime Protocols.
Однако в настоящий момент Runtime полностью изолирован от существующего Acquisition Layer.
Trades Feed продолжает работать как синхронный REST Feed:
```
REST document
TradesHandler
Trade
```
а Runtime существует отдельно:
```
Runtime Commands
Runtime Events
Runtime Service
```
В результате отсутствует единая точка, которая соединяет:
- Runtime;
- Subscription API;
- WebSocket Adapter;
- Consistency Layer.
Именно эту задачу решает Build 060.23.
---
# 2. Цели Build
Build вводит единый сервис интеграции Trade Stream.
Новый сервис становится входной точкой всей Runtime Acquisition Pipeline.
После завершения Build поток данных приобретает следующий вид:
```
Subscribe()
Subscription Builder
Acquisition Runtime Service
WebSocket Runtime
Unified Adapter
Trade
Consistency Controller
Trade | None
```
При этом Build намеренно не реализует:
- production WebSocket;
- reconnect;
- heartbeat;
- receive loop;
- runtime scheduler;
- recovery.
Все перечисленные компоненты остаются предметом следующих Build.
---
# 3. Архитектурная цель
Главная задача Build —
полностью отделить инфраструктуру Runtime
от обработки Trade.
После Build Runtime больше не знает:
- что такое Trade;
- что такое Feed;
- что такое Consistency;
- что такое Recovery.
Runtime работает исключительно с инфраструктурными командами.
Вся логика Trade переносится в отдельный слой Acquisition Integration.
---
# 4. Архитектурные принципы
Build следует тем же принципам, которые используются во всей новой архитектуре Acquisition.
## 4.1 Single Responsibility
Runtime отвечает исключительно за инфраструктуру соединения.
Trade Integration отвечает исключительно за обработку Trade Stream.
Consistency отвечает исключительно за целостность последовательности сделок.
Recovery отвечает исключительно за восстановление пропущенных данных.
Ни один слой не должен смешивать собственную ответственность с соседними.
---
## 4.2 Dependency Direction
Все зависимости направлены только вниз.
```
Trade Stream Acquisition Service
Runtime Service
Runtime Protocols
```
Обратные зависимости запрещены.
Runtime не может импортировать Acquisition.
---
## 4.3 Exchange Independence
Trade Integration не знает ничего о Dzengi.
Exchange-specific логика уже локализована внутри:
```
DzengiUnifiedWebSocketAdapter
```
Build не добавляет новых exchange-specific условий.
---
## 4.4 Canonical Pipeline
После Build существует единственный допустимый путь обработки Trade:
```
Raw Message
Unified Adapter
Trade
Consistency
Trade
```
Любые альтернативные пути считаются нарушением архитектуры.
---
# 5. Новые компоненты
Build вводит два новых компонента.
```
trade_stream_acquisition_protocol.py
trade_stream_acquisition_service.py
```
Оба файла располагаются внутри:
```
src/
└── market_data/
└── acquisition/
```
Build намеренно не изменяет существующие Feed.
Feed остаются независимыми клиентами Acquisition Layer.
---
# 6. Новая ответственность Acquisition Layer
После Build Acquisition становится полноценным координатором Runtime Pipeline.
Именно Acquisition теперь отвечает за:
- отправку Runtime Command;
- получение Trade;
- передачу Trade в Consistency;
- возврат согласованного результата.
Runtime при этом остаётся полностью инфраструктурным компонентом.
---
---
# 7. Архитектура зависимостей
После завершения Build зависимости между компонентами приобретают следующий вид.
```
+------------------------------------+
| Trade Stream Acquisition Service |
+------------------------------------+
│ │
│ │
▼ ▼
+---------------------------+ +---------------------------+
| Acquisition Runtime | | Dzengi Unified Adapter |
| Service | +---------------------------+
+---------------------------+ │
│ │
│ ▼
│ Quote | Trade | Candle
+---------------------------+
| Runtime Protocols |
+---------------------------+
Runtime Infrastructure
Trade
Trade Stream Consistency Controller
Trade | None
```
Таким образом Runtime перестаёт знать о типах рыночных данных.
Trade остаётся полностью внутри слоя Acquisition.
---
# 8. Обработка подписок
Новый сервис становится единственной точкой открытия Trade Stream.
Внешние компоненты больше не должны самостоятельно создавать Runtime Commands.
Правильная последовательность выглядит следующим образом.
```
symbols
build_trade_subscribe_command()
SubscribeCommand
AcquisitionRuntimeService.dispatch()
```
Сам сервис не сериализует сообщения.
Он использует уже существующий Subscription Builder.
Build не изменяет формат WebSocket сообщений.
---
# 9. Обработка входящих сообщений
Второй обязанностью нового сервиса становится обработка входящего транспортного сообщения.
Полная последовательность обработки выглядит следующим образом.
```
Raw WebSocket Document
DzengiUnifiedWebSocketAdapter
Quote
Trade
CandleCloseEvent
isinstance(result, Trade)
├──────────────► нет
Trade Stream Consistency Controller
Trade | None
```
Если адаптер возвращает:
```
Quote
```
или
```
CandleCloseEvent
```
сообщение считается неподходящим для данного сервиса.
Build не выполняет никаких дополнительных преобразований.
---
# 10. Взаимодействие с Consistency
Trade Integration не реализует собственную проверку последовательности.
Вся ответственность делегируется существующему компоненту.
```
Trade
TradeStreamConsistencyController.accept()
Trade | None
```
Если Controller возвращает:
```
None
```
это означает корректный дубликат.
Никаких дополнительных действий сервис не выполняет.
Если Controller возбуждает исключение:
- TradeConsistencyError;
- TradeOrderingError;
исключение полностью передаётся вызывающей стороне.
Build не изменяет модель ошибок Consistency Layer.
---
# 11. Взаимодействие с Runtime
Trade Stream Acquisition Service использует Runtime исключительно как инфраструктурный компонент.
Допустимыми являются только следующие Runtime Commands.
```
ConnectCommand
DisconnectCommand
SubscribeCommand
UnsubscribeCommand
SendTextCommand
SendBinaryCommand
```
Никакие Runtime Events в Build 060.23 не анализируются.
Publisher продолжает существовать исключительно как инфраструктурный контракт.
---
# 12. Использование Unified Adapter
Build намеренно использует существующий:
```
DzengiUnifiedWebSocketAdapter
```
а не специализированный Trade Adapter.
Причины данного решения:
• уже существует единая точка обработки WebSocket сообщений;
• отсутствует дублирование маршрутизации;
• Runtime остаётся полностью независимым от типа сообщения;
• в будущем Runtime сможет одинаково обслуживать:
- Quotes;
- Trades;
- Candles.
Таким образом новый сервис использует уже сформированную архитектуру, а не создаёт отдельную ветку обработки Trade.
---
# 13. Публичный API сервиса
Build вводит минимальный публичный интерфейс.
```python
class TradeStreamAcquisitionServiceProtocol(Protocol):
async def subscribe(
self,
symbols: tuple[str, ...],
*,
correlation_id: str | None = None,
) -> None:
...
def handle_message(
self,
document: object,
) -> Trade | None:
...
```
Оба метода являются частью единой ответственности сервиса.
Никаких дополнительных методов Build не вводит.
---
---
# 14. Architecture Decision Records (ADR)
## ADR-060.23-001
Trade Stream Integration располагается исключительно внутри слоя Acquisition.
### Причина
Runtime является инфраструктурным компонентом и не должен знать о типах рыночных данных.
### Следствие
Runtime никогда не импортирует:
- Trade;
- Trades Feed;
- Consistency;
- Recovery;
- Unified Adapter.
---
## ADR-060.23-002
Trade обрабатывается только после прохождения Unified Adapter.
### Причина
В системе уже существует единая exchange-specific точка преобразования транспортных сообщений.
```
DzengiUnifiedWebSocketAdapter
```
Build не создаёт альтернативных маршрутов обработки.
---
## ADR-060.23-003
Trade Stream Acquisition Service является единственной точкой подписки Trade Runtime.
### Причина
Внешние компоненты не должны самостоятельно формировать Runtime Commands.
Все команды создаются посредством существующих Subscription Builder.
---
## ADR-060.23-004
Trade Stream Acquisition Service не хранит состояние Trade Stream.
### Причина
Сервис выполняет исключительно координацию.
Состояние распределено между специализированными компонентами:
- Runtime Session;
- Subscription Manager;
- Trade Stream Consistency Controller.
Build не вводит нового состояния.
---
## ADR-060.23-005
Trade Stream Acquisition Service не публикует Trade.
### Причина
В проекте отсутствует утверждённая инфраструктура публикации канонического Trade Stream.
До появления такого компонента сервис возвращает:
```
Trade | None
```
не принимая решений о дальнейшей маршрутизации данных.
---
# 15. Что не входит в Build
Настоящий Build сознательно не включает:
- реализацию WebSocket Transport;
- реализацию WebSocket Session;
- production Subscription Manager;
- receive loop;
- reconnect;
- heartbeat;
- scheduler;
- Runtime Recovery;
- публикацию Runtime Events;
- публикацию Trade;
- хранение Trade;
- обработку Quote;
- обработку Candle;
- изменение существующего Trades Feed;
- изменение MarketDataRunner;
- изменение market_stream.py;
- изменение ExchangeWebSocketClient.
Все перечисленные задачи относятся к следующим Build.
---
# 16. План реализации
Реализация выполняется в несколько последовательных шагов.
## Этап 1
Создание нового контракта
```
trade_stream_acquisition_protocol.py
```
Контракт описывает публичный API сервиса.
---
## Этап 2
Создание реализации
```
trade_stream_acquisition_service.py
```
Сервис получает через конструктор зависимости:
- AcquisitionRuntimeServiceProtocol;
- DzengiUnifiedWebSocketAdapter;
- TradeStreamConsistencyProtocol.
---
## Этап 3
Интеграция подписки
Добавляется метод:
```
subscribe(...)
```
который:
- строит SubscribeCommand;
- передаёт его Runtime Service;
- не содержит exchange-specific логики.
---
## Этап 4
Интеграция обработки сообщений
Добавляется метод:
```
handle_message(...)
```
Последовательность обработки:
```
document
Unified Adapter
Trade
Consistency
Trade | None
```
---
## Этап 5
Покрытие тестами
Добавляются unit-тесты:
- проверка соответствия Protocol;
- проверка dispatch подписок;
- проверка обработки Trade;
- проверка игнорирования Quote;
- проверка игнорирования Candle;
- проверка передачи Trade в Consistency;
- проверка возврата None для корректного дубликата;
- проверка проброса исключений Consistency;
- проверка отсутствия скрытого состояния.
---
# 17. Definition of Done
Build считается завершённым после выполнения следующих условий.
✓ создан Protocol Trade Stream Acquisition Service;
✓ создана реализация сервиса;
✓ Runtime интегрирован через AcquisitionRuntimeServiceProtocol;
✓ Unified Adapter используется как единственная точка преобразования сообщений;
✓ Consistency используется как единственная точка проверки последовательности сделок;
✓ сервис не содержит exchange-specific логики;
✓ сервис не содержит собственного состояния;
✓ сервис не содержит логики reconnect;
✓ сервис не содержит логики recovery;
✓ сервис не взаимодействует с legacy Runtime;
✓ сервис полностью покрыт unit-тестами;
✓ существующие тесты Runtime, Feeds, Consistency и Recovery продолжают успешно проходить без изменений.
---
# 18. Архитектурное состояние после Build
После завершения Build 060.23 инфраструктура Acquisition приобретает завершённую форму.
```
Subscription Builder
Trade Stream Acquisition Service
Acquisition Runtime Service
Runtime Protocols
────────────────────────────────────
Raw WebSocket Message
DzengiUnifiedWebSocketAdapter
Trade
Trade Stream Consistency Controller
Trade | None
```
Таким образом завершается построение сервисного слоя интеграции Runtime и Acquisition.
Следующий этап — **Build 060.24 — Reconnect & Runtime Recovery**, в рамках которого будет реализовано управление жизненным циклом WebSocket-соединения, автоматическое восстановление подписок и интеграция Recovery Layer с Runtime Infrastructure.