Build 060.18: implement Trade Stream Consistency Controller
This commit is contained in:
1
app/src/market_data/acquisition/consistency/__init__.py
Normal file
1
app/src/market_data/acquisition/consistency/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# app/src/market_data/acquisition/consistency/__init__.py
|
||||
@@ -0,0 +1,45 @@
|
||||
# app/src/market_data/acquisition/consistency/trade_stream_consistency_controller.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from src.market_data.acquisition.consistency.trade_stream_protocol import (
|
||||
TradeStreamConsistencyProtocol,
|
||||
)
|
||||
from src.market_data.acquisition.consistency.trade_stream_state import (
|
||||
TradeStreamState,
|
||||
)
|
||||
from src.market_data.acquisition.models.trade import Trade
|
||||
|
||||
|
||||
class TradeStreamConsistencyController(
|
||||
TradeStreamConsistencyProtocol,
|
||||
):
|
||||
"""
|
||||
Контроллер проверки согласованности Canonical Trade Stream.
|
||||
|
||||
Для каждого торгового инструмента поддерживается
|
||||
независимое состояние проверки.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._states: dict[str, TradeStreamState] = {}
|
||||
|
||||
def accept(
|
||||
self,
|
||||
trade: Trade,
|
||||
) -> Trade | None:
|
||||
state = self._get_state(trade.symbol)
|
||||
|
||||
return state.accept(trade)
|
||||
|
||||
def _get_state(
|
||||
self,
|
||||
symbol: str,
|
||||
) -> TradeStreamState:
|
||||
state = self._states.get(symbol)
|
||||
|
||||
if state is None:
|
||||
state = TradeStreamState(symbol=symbol)
|
||||
self._states[symbol] = state
|
||||
|
||||
return state
|
||||
@@ -0,0 +1,17 @@
|
||||
# app/src/market_data/acquisition/consistency/trade_stream_exceptions.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from src.market_data.acquisition.exceptions import (
|
||||
MarketDataAcquisitionError,
|
||||
)
|
||||
|
||||
|
||||
# Нарушение монотонности Canonical Trade Stream.
|
||||
class TradeOrderingError(MarketDataAcquisitionError):
|
||||
pass
|
||||
|
||||
|
||||
# Обнаружен конфликтующий дубликат сделки.
|
||||
class TradeConsistencyError(MarketDataAcquisitionError):
|
||||
pass
|
||||
@@ -0,0 +1,29 @@
|
||||
# app/src/market_data/acquisition/consistency/trade_stream_protocol.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from src.market_data.acquisition.models.trade import Trade
|
||||
|
||||
|
||||
# Контракт проверки согласованности Canonical Trade Stream.
|
||||
@runtime_checkable
|
||||
class TradeStreamConsistencyProtocol(Protocol):
|
||||
def accept(
|
||||
self,
|
||||
trade: Trade,
|
||||
) -> Trade | None:
|
||||
"""
|
||||
Проверить согласованность поступившей сделки.
|
||||
|
||||
Возвращает исходную модель Trade, если сделка принята.
|
||||
|
||||
Возвращает None, если обнаружен полный дубликат.
|
||||
|
||||
Генерирует TradeOrderingError при нарушении порядка.
|
||||
|
||||
Генерирует TradeConsistencyError при обнаружении
|
||||
конфликтующего дубликата.
|
||||
"""
|
||||
...
|
||||
@@ -0,0 +1,103 @@
|
||||
# app/src/market_data/acquisition/consistency/trade_stream_state.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from src.market_data.acquisition.models.trade import Trade
|
||||
from src.market_data.acquisition.consistency.trade_stream_exceptions import (
|
||||
TradeConsistencyError,
|
||||
TradeOrderingError,
|
||||
)
|
||||
|
||||
DEFAULT_DEDUPLICATION_WINDOW_SIZE = 10_000
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TradeStreamState:
|
||||
"""
|
||||
Состояние проверки согласованности Canonical Trade Stream
|
||||
для одного торгового инструмента.
|
||||
"""
|
||||
|
||||
symbol: str
|
||||
deduplication_window_size: int = DEFAULT_DEDUPLICATION_WINDOW_SIZE
|
||||
|
||||
last_trade_id: int | None = None
|
||||
|
||||
_trade_window: deque[int] = field(init=False, repr=False)
|
||||
_trades: dict[int, Trade] = field(init=False, repr=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.symbol:
|
||||
raise ValueError("symbol must not be empty")
|
||||
|
||||
if self.deduplication_window_size <= 0:
|
||||
raise ValueError(
|
||||
"deduplication_window_size must be positive"
|
||||
)
|
||||
|
||||
self._trade_window = deque(
|
||||
maxlen=self.deduplication_window_size,
|
||||
)
|
||||
self._trades = {}
|
||||
|
||||
def accept(
|
||||
self,
|
||||
trade: Trade,
|
||||
) -> Trade | None:
|
||||
"""
|
||||
Проверить сделку на согласованность.
|
||||
|
||||
Возвращает принятую сделку.
|
||||
|
||||
Возвращает None при полном дубликате.
|
||||
|
||||
Генерирует исключение при нарушении порядка
|
||||
либо конфликтующем дубликате.
|
||||
"""
|
||||
|
||||
if trade.symbol != self.symbol:
|
||||
raise ValueError(
|
||||
f"Unexpected symbol: {trade.symbol!r}"
|
||||
)
|
||||
|
||||
trade_id = trade.trade_id
|
||||
|
||||
if self.last_trade_id is not None:
|
||||
if trade_id < self.last_trade_id:
|
||||
previous = self._trades.get(trade_id)
|
||||
|
||||
if previous is None:
|
||||
raise TradeOrderingError()
|
||||
|
||||
if previous == trade:
|
||||
return None
|
||||
|
||||
raise TradeConsistencyError()
|
||||
|
||||
previous = self._trades.get(trade_id)
|
||||
|
||||
if previous is not None:
|
||||
if previous == trade:
|
||||
return None
|
||||
|
||||
raise TradeConsistencyError()
|
||||
|
||||
self._append(trade)
|
||||
|
||||
self.last_trade_id = trade_id
|
||||
|
||||
return trade
|
||||
|
||||
def _append(
|
||||
self,
|
||||
trade: Trade,
|
||||
) -> None:
|
||||
if len(self._trade_window) == self.deduplication_window_size:
|
||||
oldest_trade_id = self._trade_window[0]
|
||||
self._trades.pop(oldest_trade_id, None)
|
||||
|
||||
self._trade_window.append(trade.trade_id)
|
||||
self._trades[trade.trade_id] = trade
|
||||
Reference in New Issue
Block a user