Build 060.20_1: align Trade Stream state ownership

This commit is contained in:
2026-07-25 10:41:01 +03:00
parent 14f698d48a
commit ad6631f773
14 changed files with 4311 additions and 431 deletions

View File

@@ -8,10 +8,10 @@ from src.market_data.acquisition.consistency.trade_stream_protocol import (
from src.market_data.acquisition.consistency.trade_stream_state import (
TradeStreamState,
)
from src.market_data.acquisition.models.trade import Trade
from src.market_data.acquisition.runtime.trade.trade_runtime_protocol import (
TradeRuntimeProtocol,
from src.market_data.acquisition.consistency.trade_stream_state_store_protocol import (
TradeStreamStateStoreProtocol,
)
from src.market_data.acquisition.models.trade import Trade
class TradeStreamConsistencyController(
@@ -20,18 +20,16 @@ class TradeStreamConsistencyController(
"""
Контроллер проверки согласованности Canonical Trade Stream.
Для каждого торгового инструмента поддерживается
независимое состояние проверки, которое хранится
в Trade Runtime Registry.
Для каждого торгового инструмента используется
специализированное хранилище состояний
Trade Stream Consistency.
"""
_RUNTIME_NAMESPACE = "consistency"
def __init__(
self,
runtime: TradeRuntimeProtocol,
state_store: TradeStreamStateStoreProtocol,
) -> None:
self._runtime = runtime
self._state_store = state_store
def accept(
self,
@@ -45,24 +43,11 @@ class TradeStreamConsistencyController(
self,
symbol: str,
) -> TradeStreamState:
key = self._runtime_key(symbol)
if self._runtime.is_registered(key):
return self._runtime.get(key)
state = TradeStreamState(symbol=symbol)
self._runtime.register(key, state)
return state
@classmethod
def _runtime_key(
cls,
symbol: str,
) -> str:
"""
Возвращает ключ Runtime Registry
для состояния проверки Trade Stream.
"""
Возвращает состояние проверки Trade Stream
для указанного торгового инструмента.
return f"{cls._RUNTIME_NAMESPACE}:{symbol}"
При первом обращении состояние автоматически
создаётся специализированным хранилищем.
"""
return self._state_store.get_or_create(symbol)

View File

@@ -0,0 +1,129 @@
# app/src/market_data/acquisition/consistency/trade_stream_state_store.py
from __future__ import annotations
"""
Хранилище состояний Trade Stream Consistency.
Build 060.20.1 переносит владение инфраструктурным состоянием
из общего Runtime Registry в специализированное хранилище
подсистемы проверки согласованности потока сделок.
Хранилище отвечает исключительно за жизненный цикл объектов
TradeStreamState и не содержит логики проверки последовательности
или восстановления потока сделок.
"""
from src.market_data.acquisition.consistency.trade_stream_state import (
TradeStreamState,
)
from src.market_data.acquisition.consistency.trade_stream_state_store_exceptions import (
TradeStreamStateNotFoundError,
)
from src.market_data.acquisition.consistency.trade_stream_state_store_protocol import (
TradeStreamStateStoreProtocol,
)
class TradeStreamStateStore(TradeStreamStateStoreProtocol):
"""
Хранилище состояний Trade Stream.
Для каждого торгового инструмента существует
единственный экземпляр TradeStreamState.
"""
def __init__(self) -> None:
"""
Создаёт пустое хранилище состояний.
"""
self._states: dict[str, TradeStreamState] = {}
def get_or_create(
self,
symbol: str,
) -> TradeStreamState:
"""
Возвращает существующее состояние либо создаёт новое.
Args:
symbol:
Идентификатор торгового инструмента.
Returns:
Экземпляр TradeStreamState.
"""
state = self._states.get(symbol)
if state is None:
state = TradeStreamState(symbol=symbol)
self._states[symbol] = state
return state
def get(
self,
symbol: str,
) -> TradeStreamState:
"""
Возвращает существующее состояние.
Args:
symbol:
Идентификатор торгового инструмента.
Raises:
TradeStreamStateNotFoundError:
Если состояние отсутствует.
"""
try:
return self._states[symbol]
except KeyError as error:
raise TradeStreamStateNotFoundError(
f"Состояние Trade Stream для символа {symbol!r} не найдено."
) from error
def contains(
self,
symbol: str,
) -> bool:
"""
Проверяет наличие состояния.
Args:
symbol:
Идентификатор торгового инструмента.
Returns:
True, если состояние существует.
"""
return symbol in self._states
def remove(
self,
symbol: str,
) -> None:
"""
Удаляет состояние.
Args:
symbol:
Идентификатор торгового инструмента.
Raises:
TradeStreamStateNotFoundError:
Если состояние отсутствует.
"""
if symbol not in self._states:
raise TradeStreamStateNotFoundError(
f"Состояние Trade Stream для символа "
f"{symbol!r} не найдено."
)
del self._states[symbol]
def clear(self) -> None:
"""
Полностью очищает хранилище состояний.
"""
self._states.clear()

View File

@@ -0,0 +1,28 @@
# app/src/market_data/acquisition/consistency/trade_stream_state_store_exceptions.py
from __future__ import annotations
"""
Исключения хранилища состояний Trade Stream Consistency.
Build 060.20.1 вводит специализированное хранилище
состояния проверки согласованности потока сделок.
Исключения относятся исключительно к управлению
TradeStreamState и не описывают ошибки проверки
последовательности сделок.
"""
class TradeStreamStateStoreError(Exception):
"""
Базовое исключение хранилища состояний Trade Stream.
"""
class TradeStreamStateNotFoundError(
TradeStreamStateStoreError,
):
"""
Состояние торгового инструмента отсутствует.
"""

View File

@@ -0,0 +1,107 @@
# app/src/market_data/acquisition/consistency/trade_stream_state_store_protocol.py
from __future__ import annotations
"""
Протокол хранилища состояния Trade Stream Consistency.
Build 060.20.1 заменяет универсальный Trade Runtime Registry
специализированным типизированным хранилищем состояния
Canonical Trade Stream.
TradeStreamStateStoreProtocol определяет минимальный контракт
доступа к состоянию проверки согласованности потока сделок.
Протокол не описывает внутреннюю реализацию хранения
и не содержит бизнес-логики Trade Stream Consistency.
"""
from typing import Protocol, runtime_checkable
from src.market_data.acquisition.consistency.trade_stream_state import (
TradeStreamState,
)
@runtime_checkable
class TradeStreamStateStoreProtocol(Protocol):
"""
Протокол хранилища состояний Trade Stream Consistency.
"""
def get_or_create(
self,
symbol: str,
) -> TradeStreamState:
"""
Возвращает существующее состояние торгового инструмента
либо создаёт новое состояние при первом обращении.
Args:
symbol:
Идентификатор торгового инструмента.
Returns:
Существующий либо созданный TradeStreamState.
"""
...
def get(
self,
symbol: str,
) -> TradeStreamState:
"""
Возвращает существующее состояние торгового инструмента.
Args:
symbol:
Идентификатор торгового инструмента.
Returns:
Зарегистрированный TradeStreamState.
Raises:
TradeStreamStateNotFoundError:
Если состояние торгового инструмента отсутствует.
"""
...
def contains(
self,
symbol: str,
) -> bool:
"""
Проверяет наличие состояния торгового инструмента.
Args:
symbol:
Идентификатор торгового инструмента.
Returns:
True, если состояние существует,
иначе False.
"""
...
def remove(
self,
symbol: str,
) -> None:
"""
Удаляет состояние торгового инструмента.
Args:
symbol:
Идентификатор торгового инструмента.
Raises:
TradeStreamStateNotFoundError:
Если состояние торгового инструмента отсутствует.
"""
...
def clear(self) -> None:
"""
Полностью очищает хранилище состояний.
"""
...

View File

@@ -1,43 +0,0 @@
# app/src/market_data/acquisition/runtime/trade/trade_runtime_exceptions.py
from __future__ import annotations
"""
Инфраструктурные исключения Trade Runtime.
Build 060.20 вводит инфраструктурный слой Trade Runtime.
Данный модуль содержит только инфраструктурные исключения,
связанные с регистрацией и получением Runtime-компонентов.
Бизнес-исключения отдельных Runtime-модулей (Recovery,
Stream Consistency и других) должны оставаться
внутри соответствующих подсистем.
"""
class TradeRuntimeError(Exception):
"""
Базовый класс для всех инфраструктурных исключений Trade Runtime.
"""
class RuntimeAlreadyRegisteredError(TradeRuntimeError):
"""
Вызывается при попытке повторной регистрации Runtime-компонента
под уже существующим ключом.
"""
class RuntimeNotRegisteredError(TradeRuntimeError):
"""
Вызывается при попытке получить Runtime-компонент,
который отсутствует в TradeRuntimeRegistry.
"""
class InvalidRuntimeComponentError(TradeRuntimeError):
"""
Вызывается при попытке зарегистрировать объект,
который не может являться Runtime-компонентом.
"""

View File

@@ -1,85 +0,0 @@
# app/src/market_data/acquisition/runtime/trade/trade_runtime_protocol.py
from __future__ import annotations
"""
Протокол Trade Runtime.
Build 060.20 вводит единый Runtime Layer для управления
жизненным циклом компонентов, обеспечивающих обработку
биржевого потока сделок.
TradeRuntimeProtocol определяет минимальный контракт,
которому должна соответствовать реализация Runtime Registry.
Протокол не описывает внутреннюю реализацию хранения
компонентов и не содержит бизнес-логики.
"""
from typing import Any, Protocol
class TradeRuntimeProtocol(Protocol):
"""
Протокол реестра Runtime-компонентов.
"""
def register(self, key: str, component: Any) -> None:
"""
Регистрирует Runtime-компонент.
Args:
key:
Уникальный идентификатор Runtime-компонента.
component:
Экземпляр Runtime-компонента.
"""
...
def unregister(self, key: str) -> None:
"""
Удаляет Runtime-компонент из реестра.
Args:
key:
Уникальный идентификатор Runtime-компонента.
"""
...
def get(self, key: str) -> Any:
"""
Возвращает зарегистрированный Runtime-компонент.
Args:
key:
Уникальный идентификатор Runtime-компонента.
Returns:
Зарегистрированный Runtime-компонент.
Raises:
RuntimeNotRegisteredError:
Если компонент отсутствует в реестре.
"""
...
def is_registered(self, key: str) -> bool:
"""
Проверяет наличие Runtime-компонента в реестре.
Args:
key:
Уникальный идентификатор Runtime-компонента.
Returns:
True, если компонент зарегистрирован,
иначе False.
"""
...
def clear(self) -> None:
"""
Полностью очищает реестр Runtime-компонентов.
"""
...

View File

@@ -1,132 +0,0 @@
# app/src/market_data/acquisition/runtime/trade/trade_runtime_registry.py
from __future__ import annotations
"""
Реестр компонентов Trade Runtime.
Build 060.20 вводит единый инфраструктурный реестр,
который владеет экземплярами Runtime-компонентов
и предоставляет их по уникальным строковым ключам.
Реестр не управляет бизнес-состоянием компонентов
и не содержит логики Stream Consistency, Recovery
или других подсистем обработки потока сделок.
"""
from typing import Any
from src.market_data.acquisition.runtime.trade.trade_runtime_exceptions import (
InvalidRuntimeComponentError,
RuntimeAlreadyRegisteredError,
RuntimeNotRegisteredError,
)
from src.market_data.acquisition.runtime.trade.trade_runtime_protocol import (
TradeRuntimeProtocol,
)
class TradeRuntimeRegistry(TradeRuntimeProtocol):
"""
Реестр Runtime-компонентов потока сделок.
Каждый компонент регистрируется под уникальным строковым ключом.
Повторная регистрация под тем же ключом запрещена.
"""
def __init__(self) -> None:
"""
Создаёт пустой реестр Runtime-компонентов.
"""
self._components: dict[str, Any] = {}
def register(self, key: str, component: Any) -> None:
"""
Регистрирует Runtime-компонент под уникальным ключом.
Args:
key:
Уникальный идентификатор Runtime-компонента.
component:
Экземпляр Runtime-компонента.
Raises:
InvalidRuntimeComponentError:
Если вместо Runtime-компонента передан None.
RuntimeAlreadyRegisteredError:
Если указанный ключ уже используется.
"""
if component is None:
raise InvalidRuntimeComponentError(
f"Runtime-компонент для ключа {key!r} не может быть None."
)
if key in self._components:
raise RuntimeAlreadyRegisteredError(
f"Runtime-компонент с ключом {key!r} уже зарегистрирован."
)
self._components[key] = component
def unregister(self, key: str) -> None:
"""
Удаляет Runtime-компонент из реестра.
Args:
key:
Уникальный идентификатор Runtime-компонента.
Raises:
RuntimeNotRegisteredError:
Если компонент с указанным ключом отсутствует.
"""
if key not in self._components:
raise RuntimeNotRegisteredError(
f"Runtime-компонент с ключом {key!r} не зарегистрирован."
)
del self._components[key]
def get(self, key: str) -> Any:
"""
Возвращает зарегистрированный Runtime-компонент.
Args:
key:
Уникальный идентификатор Runtime-компонента.
Returns:
Зарегистрированный Runtime-компонент.
Raises:
RuntimeNotRegisteredError:
Если компонент с указанным ключом отсутствует.
"""
try:
return self._components[key]
except KeyError as error:
raise RuntimeNotRegisteredError(
f"Runtime-компонент с ключом {key!r} не зарегистрирован."
) from error
def is_registered(self, key: str) -> bool:
"""
Проверяет наличие Runtime-компонента в реестре.
Args:
key:
Уникальный идентификатор Runtime-компонента.
Returns:
True, если компонент зарегистрирован,
иначе False.
"""
return key in self._components
def clear(self) -> None:
"""
Удаляет из реестра все зарегистрированные Runtime-компоненты.
"""
self._components.clear()

View File

@@ -21,22 +21,22 @@ from src.market_data.acquisition.models.trade import (
Trade,
TradeAggressorSide,
)
from src.market_data.acquisition.runtime.trade.trade_runtime_registry import (
TradeRuntimeRegistry,
from src.market_data.acquisition.consistency.trade_stream_state_store import (
TradeStreamStateStore,
)
@pytest.fixture
def runtime() -> TradeRuntimeRegistry:
return TradeRuntimeRegistry()
def state_store() -> TradeStreamStateStore:
return TradeStreamStateStore()
@pytest.fixture
def controller(
runtime: TradeRuntimeRegistry,
state_store: TradeStreamStateStore,
) -> TradeStreamConsistencyController:
return TradeStreamConsistencyController(
runtime=runtime,
state_store=state_store,
)
@@ -68,32 +68,32 @@ def _trade(
def test_creates_state_for_first_symbol(
controller: TradeStreamConsistencyController,
runtime: TradeRuntimeRegistry,
state_store: TradeStreamStateStore,
) -> None:
trade = _trade(symbol="BTCUSD")
result = controller.accept(trade)
assert result == trade
assert runtime.is_registered("consistency:BTCUSD")
assert state_store.contains("BTCUSD")
def test_registers_trade_stream_state(
controller: TradeStreamConsistencyController,
runtime: TradeRuntimeRegistry,
state_store: TradeStreamStateStore,
) -> None:
controller.accept(
_trade(symbol="BTCUSD"),
)
state = runtime.get("consistency:BTCUSD")
state = state_store.get("BTCUSD")
assert isinstance(state, TradeStreamState)
def test_reuses_state_for_same_symbol(
controller: TradeStreamConsistencyController,
runtime: TradeRuntimeRegistry,
state_store: TradeStreamStateStore,
) -> None:
first_trade = _trade(
symbol="BTCUSD",
@@ -105,10 +105,10 @@ def test_reuses_state_for_same_symbol(
)
controller.accept(first_trade)
first_state = runtime.get("consistency:BTCUSD")
first_state = state_store.get("BTCUSD")
result = controller.accept(second_trade)
second_state = runtime.get("consistency:BTCUSD")
second_state = state_store.get("BTCUSD")
assert result == second_trade
assert second_state is first_state
@@ -116,7 +116,7 @@ def test_reuses_state_for_same_symbol(
def test_keeps_independent_state_per_symbol(
controller: TradeStreamConsistencyController,
runtime: TradeRuntimeRegistry,
state_store: TradeStreamStateStore,
) -> None:
btc_trade = _trade(
symbol="BTCUSD",
@@ -130,8 +130,8 @@ def test_keeps_independent_state_per_symbol(
btc_result = controller.accept(btc_trade)
eth_result = controller.accept(eth_trade)
btc_state = runtime.get("consistency:BTCUSD")
eth_state = runtime.get("consistency:ETHUSD")
btc_state = state_store.get("BTCUSD")
eth_state = state_store.get("ETHUSD")
assert btc_result == btc_trade
assert eth_result == eth_trade

View File

@@ -0,0 +1,143 @@
# app/tests/unit/market_data/acquisition/consistency/test_trade_stream_state_store.py
from __future__ import annotations
import pytest
from src.market_data.acquisition.consistency.trade_stream_state import (
TradeStreamState,
)
from src.market_data.acquisition.consistency.trade_stream_state_store import (
TradeStreamStateStore,
)
from src.market_data.acquisition.consistency.trade_stream_state_store_exceptions import (
TradeStreamStateNotFoundError,
)
from src.market_data.acquisition.consistency.trade_stream_state_store_protocol import (
TradeStreamStateStoreProtocol,
)
def test_store_implements_protocol() -> None:
store = TradeStreamStateStore()
assert isinstance(
store,
TradeStreamStateStoreProtocol,
)
def test_store_is_empty_after_creation() -> None:
store = TradeStreamStateStore()
assert store.contains("BTCUSD") is False
def test_get_or_create_creates_trade_stream_state() -> None:
store = TradeStreamStateStore()
state = store.get_or_create("BTCUSD")
assert isinstance(
state,
TradeStreamState,
)
assert state.symbol == "BTCUSD"
assert store.contains("BTCUSD") is True
def test_get_or_create_returns_same_instance_for_same_symbol() -> None:
store = TradeStreamStateStore()
first_state = store.get_or_create("BTCUSD")
second_state = store.get_or_create("BTCUSD")
assert second_state is first_state
def test_get_or_create_returns_independent_states_for_different_symbols() -> None:
store = TradeStreamStateStore()
btc_state = store.get_or_create("BTCUSD")
eth_state = store.get_or_create("ETHUSD")
assert btc_state is not eth_state
assert btc_state.symbol == "BTCUSD"
assert eth_state.symbol == "ETHUSD"
def test_get_returns_existing_state() -> None:
store = TradeStreamStateStore()
created_state = store.get_or_create("BTCUSD")
returned_state = store.get("BTCUSD")
assert returned_state is created_state
def test_get_raises_not_found_error_for_missing_symbol() -> None:
store = TradeStreamStateStore()
with pytest.raises(
TradeStreamStateNotFoundError,
match="BTCUSD",
):
store.get("BTCUSD")
def test_remove_deletes_existing_state() -> None:
store = TradeStreamStateStore()
store.get_or_create("BTCUSD")
store.remove("BTCUSD")
assert store.contains("BTCUSD") is False
with pytest.raises(
TradeStreamStateNotFoundError,
match="BTCUSD",
):
store.get("BTCUSD")
def test_remove_does_not_delete_other_symbol_state() -> None:
store = TradeStreamStateStore()
store.get_or_create("BTCUSD")
eth_state = store.get_or_create("ETHUSD")
store.remove("BTCUSD")
assert store.contains("BTCUSD") is False
assert store.get("ETHUSD") is eth_state
def test_remove_raises_not_found_error_for_missing_symbol() -> None:
store = TradeStreamStateStore()
with pytest.raises(
TradeStreamStateNotFoundError,
match="BTCUSD",
):
store.remove("BTCUSD")
def test_clear_removes_all_states() -> None:
store = TradeStreamStateStore()
store.get_or_create("BTCUSD")
store.get_or_create("ETHUSD")
store.clear()
assert store.contains("BTCUSD") is False
assert store.contains("ETHUSD") is False
def test_clear_is_idempotent_for_empty_store() -> None:
store = TradeStreamStateStore()
store.clear()
store.clear()
assert store.contains("BTCUSD") is False

View File

@@ -1,126 +0,0 @@
# app/tests/unit/market_data/acquisition/runtime/trade/test_trade_runtime_registry.py
from __future__ import annotations
import pytest
from src.market_data.acquisition.runtime.trade.trade_runtime_exceptions import (
InvalidRuntimeComponentError,
RuntimeAlreadyRegisteredError,
RuntimeNotRegisteredError,
)
from src.market_data.acquisition.runtime.trade.trade_runtime_registry import (
TradeRuntimeRegistry,
)
class DummyComponent:
"""Тестовый Runtime-компонент."""
@pytest.fixture
def component() -> DummyComponent:
"""Создаёт тестовый Runtime-компонент."""
return DummyComponent()
def test_registry_is_empty_after_creation() -> None:
registry = TradeRuntimeRegistry()
assert registry.is_registered("component") is False
def test_is_registered_returns_false_for_unknown_key() -> None:
registry = TradeRuntimeRegistry()
assert registry.is_registered("unknown") is False
def test_register_component(component: DummyComponent) -> None:
registry = TradeRuntimeRegistry()
registry.register("component", component)
assert registry.is_registered("component") is True
def test_get_returns_registered_component(
component: DummyComponent,
) -> None:
registry = TradeRuntimeRegistry()
registry.register("component", component)
assert registry.get("component") is component
def test_is_registered_returns_true_after_registration(
component: DummyComponent,
) -> None:
registry = TradeRuntimeRegistry()
registry.register("component", component)
assert registry.is_registered("component") is True
def test_register_none_raises_invalid_runtime_component_error() -> None:
registry = TradeRuntimeRegistry()
with pytest.raises(InvalidRuntimeComponentError):
registry.register("component", None)
def test_register_duplicate_key_raises_runtime_already_registered_error(
component: DummyComponent,
) -> None:
registry = TradeRuntimeRegistry()
registry.register("component", component)
with pytest.raises(RuntimeAlreadyRegisteredError):
registry.register("component", DummyComponent())
def test_unregister_component(
component: DummyComponent,
) -> None:
registry = TradeRuntimeRegistry()
registry.register("component", component)
registry.unregister("component")
assert registry.is_registered("component") is False
with pytest.raises(RuntimeNotRegisteredError):
registry.get("component")
def test_unregister_unknown_component_raises_runtime_not_registered_error() -> None:
registry = TradeRuntimeRegistry()
with pytest.raises(RuntimeNotRegisteredError):
registry.unregister("unknown")
def test_clear_removes_all_components() -> None:
registry = TradeRuntimeRegistry()
registry.register("component_1", DummyComponent())
registry.register("component_2", DummyComponent())
registry.register("component_3", DummyComponent())
registry.clear()
assert registry.is_registered("component_1") is False
assert registry.is_registered("component_2") is False
assert registry.is_registered("component_3") is False
with pytest.raises(RuntimeNotRegisteredError):
registry.get("component_1")
with pytest.raises(RuntimeNotRegisteredError):
registry.get("component_2")
with pytest.raises(RuntimeNotRegisteredError):
registry.get("component_3")