Build 060.18: implement Trade Stream Consistency Controller

This commit is contained in:
2026-07-20 19:16:19 +03:00
parent fe308b9e60
commit 618b716e71
7 changed files with 525 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
# app/tests/unit/market_data/acquisition/consistency/test_trade_stream_consistency_controller.py
from __future__ import annotations
from datetime import datetime, timezone
from decimal import Decimal
import pytest
from src.market_data.acquisition.consistency.trade_stream_consistency_controller import (
TradeStreamConsistencyController,
)
from src.market_data.acquisition.consistency.trade_stream_exceptions import (
TradeConsistencyError,
TradeOrderingError,
)
from src.market_data.acquisition.models.trade import (
Trade,
TradeAggressorSide,
)
def _trade(
*,
symbol: str = "BTCUSD",
trade_id: int = 100,
price: Decimal = Decimal("50000.00"),
quantity: Decimal = Decimal("0.25"),
aggressor_side: TradeAggressorSide = TradeAggressorSide.BUY,
) -> Trade:
return Trade(
symbol=symbol,
trade_id=trade_id,
price=price,
quantity=quantity,
executed_at=datetime(
2026,
1,
1,
12,
0,
tzinfo=timezone.utc,
),
aggressor_side=aggressor_side,
source="dzengi",
)
def test_creates_state_for_first_symbol() -> None:
controller = TradeStreamConsistencyController()
trade = _trade(symbol="BTCUSD")
result = controller.accept(trade)
assert result == trade
def test_reuses_state_for_same_symbol() -> None:
controller = TradeStreamConsistencyController()
first_trade = _trade(
symbol="BTCUSD",
trade_id=100,
)
second_trade = _trade(
symbol="BTCUSD",
trade_id=101,
)
controller.accept(first_trade)
result = controller.accept(second_trade)
assert result == second_trade
def test_keeps_independent_state_per_symbol() -> None:
controller = TradeStreamConsistencyController()
btc_trade = _trade(
symbol="BTCUSD",
trade_id=100,
)
eth_trade = _trade(
symbol="ETHUSD",
trade_id=100,
)
btc_result = controller.accept(btc_trade)
eth_result = controller.accept(eth_trade)
assert btc_result == btc_trade
assert eth_result == eth_trade
def test_returns_none_for_duplicate_trade() -> None:
controller = TradeStreamConsistencyController()
trade = _trade()
controller.accept(trade)
result = controller.accept(trade)
assert result is None
def test_propagates_ordering_error() -> None:
controller = TradeStreamConsistencyController()
controller.accept(
_trade(trade_id=100),
)
with pytest.raises(TradeOrderingError):
controller.accept(
_trade(trade_id=99),
)
def test_propagates_consistency_error() -> None:
controller = TradeStreamConsistencyController()
controller.accept(
_trade(
trade_id=100,
price=Decimal("50000.00"),
),
)
with pytest.raises(TradeConsistencyError):
controller.accept(
_trade(
trade_id=100,
price=Decimal("50001.00"),
),
)

View File

@@ -0,0 +1,192 @@
from __future__ import annotations
from datetime import datetime, timezone
from decimal import Decimal
import pytest
from src.market_data.acquisition.consistency.trade_stream_exceptions import (
TradeConsistencyError,
TradeOrderingError,
)
from src.market_data.acquisition.consistency.trade_stream_state import (
TradeStreamState,
)
from src.market_data.acquisition.models.trade import (
Trade,
TradeAggressorSide,
)
def _trade(
*,
symbol: str = "BTCUSD",
trade_id: int = 100,
price: Decimal = Decimal("50000.00"),
quantity: Decimal = Decimal("0.25"),
executed_at: datetime | None = None,
aggressor_side: TradeAggressorSide = TradeAggressorSide.BUY,
source: str = "dzengi",
) -> Trade:
return Trade(
symbol=symbol,
trade_id=trade_id,
price=price,
quantity=quantity,
executed_at=executed_at
or datetime(
2026,
1,
1,
12,
0,
tzinfo=timezone.utc,
),
aggressor_side=aggressor_side,
source=source,
)
def test_trade_stream_state_uses_slots() -> None:
state = TradeStreamState(symbol="BTCUSD")
assert not hasattr(state, "__dict__")
def test_accepts_first_trade() -> None:
state = TradeStreamState(symbol="BTCUSD")
trade = _trade()
result = state.accept(trade)
assert result == trade
assert state.last_trade_id == trade.trade_id
def test_accepts_trade_with_greater_trade_id() -> None:
state = TradeStreamState(symbol="BTCUSD")
first_trade = _trade(trade_id=100)
second_trade = _trade(trade_id=101)
state.accept(first_trade)
result = state.accept(second_trade)
assert result == second_trade
assert state.last_trade_id == second_trade.trade_id
def test_accepts_trade_with_gap() -> None:
state = TradeStreamState(symbol="BTCUSD")
first_trade = _trade(trade_id=100)
trade_after_gap = _trade(trade_id=105)
state.accept(first_trade)
result = state.accept(trade_after_gap)
assert result == trade_after_gap
assert state.last_trade_id == trade_after_gap.trade_id
def test_returns_none_for_identical_duplicate() -> None:
state = TradeStreamState(symbol="BTCUSD")
trade = _trade()
state.accept(trade)
result = state.accept(trade)
assert result is None
assert state.last_trade_id == trade.trade_id
def test_raises_consistency_error_for_conflicting_duplicate() -> None:
state = TradeStreamState(symbol="BTCUSD")
original_trade = _trade(
trade_id=100,
price=Decimal("50000.00"),
)
conflicting_trade = _trade(
trade_id=100,
price=Decimal("50001.00"),
)
state.accept(original_trade)
with pytest.raises(TradeConsistencyError):
state.accept(conflicting_trade)
def test_raises_ordering_error_for_older_trade() -> None:
state = TradeStreamState(symbol="BTCUSD")
current_trade = _trade(trade_id=100)
older_trade = _trade(trade_id=99)
state.accept(current_trade)
with pytest.raises(TradeOrderingError):
state.accept(older_trade)
def test_returns_none_for_duplicate_still_inside_window() -> None:
state = TradeStreamState(
symbol="BTCUSD",
deduplication_window_size=3,
)
first_trade = _trade(trade_id=100)
second_trade = _trade(trade_id=101)
third_trade = _trade(trade_id=102)
state.accept(first_trade)
state.accept(second_trade)
state.accept(third_trade)
result = state.accept(first_trade)
assert result is None
assert state.last_trade_id == third_trade.trade_id
def test_raises_ordering_error_after_trade_leaves_window() -> None:
state = TradeStreamState(
symbol="BTCUSD",
deduplication_window_size=2,
)
first_trade = _trade(trade_id=100)
second_trade = _trade(trade_id=101)
third_trade = _trade(trade_id=102)
state.accept(first_trade)
state.accept(second_trade)
state.accept(third_trade)
with pytest.raises(TradeOrderingError):
state.accept(first_trade)
def test_rejects_unexpected_symbol() -> None:
state = TradeStreamState(symbol="BTCUSD")
trade = _trade(symbol="ETHUSD")
with pytest.raises(ValueError):
state.accept(trade)
def test_rejects_empty_symbol() -> None:
with pytest.raises(ValueError):
TradeStreamState(symbol="")
@pytest.mark.parametrize(
"window_size",
[
0,
-1,
],
)
def test_rejects_non_positive_window_size(
window_size: int,
) -> None:
with pytest.raises(ValueError):
TradeStreamState(
symbol="BTCUSD",
deduplication_window_size=window_size,
)