Build 060.23: integrate Trade Stream Acquisition
This commit is contained in:
@@ -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 либо является
|
||||||
|
корректным дубликатом.
|
||||||
|
"""
|
||||||
|
...
|
||||||
@@ -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)
|
||||||
@@ -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-документ в каноническую модель.
|
||||||
|
"""
|
||||||
|
...
|
||||||
@@ -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
|
||||||
@@ -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]
|
||||||
1419
docs/migrations/build_060_23.md
Normal file
1419
docs/migrations/build_060_23.md
Normal file
File diff suppressed because it is too large
Load Diff
845
docs/migrations/build_060_23_architecture.md
Normal file
845
docs/migrations/build_060_23_architecture.md
Normal 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.
|
||||||
Reference in New Issue
Block a user