build 042: remove MarketPriceCache compatibility facade
This commit is contained in:
@@ -1,69 +0,0 @@
|
|||||||
# app/src/integrations/exchange/market_cache.py
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from src.market_data.acquisition.models.quote import Quote
|
|
||||||
from src.storage.quote_store import InMemoryQuoteStore, QuoteStoreProtocol
|
|
||||||
|
|
||||||
|
|
||||||
_MARKET_PRICE_CACHE_SOURCE_NAME = "legacy-market-price-cache"
|
|
||||||
|
|
||||||
|
|
||||||
class MarketPriceCache:
|
|
||||||
# Временный compatibility facade над каноническим Quote Store.
|
|
||||||
_store: QuoteStoreProtocol = InMemoryQuoteStore()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_quote(
|
|
||||||
cls,
|
|
||||||
quote: Quote,
|
|
||||||
*,
|
|
||||||
runtime_key: str = "default",
|
|
||||||
) -> None:
|
|
||||||
cls._store.set(
|
|
||||||
_MARKET_PRICE_CACHE_SOURCE_NAME,
|
|
||||||
quote,
|
|
||||||
runtime_key=cls._normalize_runtime_key(runtime_key),
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_quote(
|
|
||||||
cls,
|
|
||||||
symbol: str,
|
|
||||||
*,
|
|
||||||
runtime_key: str = "default",
|
|
||||||
) -> Quote | None:
|
|
||||||
return cls._store.get(
|
|
||||||
_MARKET_PRICE_CACHE_SOURCE_NAME,
|
|
||||||
cls._normalize_symbol(symbol),
|
|
||||||
runtime_key=cls._normalize_runtime_key(runtime_key),
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def clear(
|
|
||||||
cls,
|
|
||||||
symbol: str | None = None,
|
|
||||||
*,
|
|
||||||
runtime_key: str | None = None,
|
|
||||||
) -> None:
|
|
||||||
cls._store.clear(
|
|
||||||
source_name=_MARKET_PRICE_CACHE_SOURCE_NAME,
|
|
||||||
symbol=(
|
|
||||||
cls._normalize_symbol(symbol)
|
|
||||||
if symbol is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
runtime_key=(
|
|
||||||
cls._normalize_runtime_key(runtime_key)
|
|
||||||
if runtime_key is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _normalize_symbol(symbol: str) -> str:
|
|
||||||
return str(symbol).strip().upper()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _normalize_runtime_key(runtime_key: str) -> str:
|
|
||||||
return str(runtime_key).strip().lower()
|
|
||||||
@@ -9,7 +9,6 @@ from dataclasses import dataclass
|
|||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from src.core.types import JsonDict
|
from src.core.types import JsonDict
|
||||||
from src.integrations.exchange.market_cache import MarketPriceCache
|
|
||||||
from src.integrations.exchange.service import ExchangeService
|
from src.integrations.exchange.service import ExchangeService
|
||||||
from src.integrations.exchange.ws_client import ExchangeWebSocketClient
|
from src.integrations.exchange.ws_client import ExchangeWebSocketClient
|
||||||
from src.market_data.acquisition.adapters.dzengi.websocket import (
|
from src.market_data.acquisition.adapters.dzengi.websocket import (
|
||||||
@@ -18,6 +17,10 @@ from src.market_data.acquisition.adapters.dzengi.websocket import (
|
|||||||
from src.market_data.acquisition.exceptions import (
|
from src.market_data.acquisition.exceptions import (
|
||||||
MarketDataAcquisitionError,
|
MarketDataAcquisitionError,
|
||||||
)
|
)
|
||||||
|
from src.storage.quote_store import (
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
|
get_quote_store,
|
||||||
|
)
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
|
|
||||||
|
|
||||||
@@ -178,7 +181,10 @@ class MarketDataRunner:
|
|||||||
runtime_key=context.runtime_key,
|
runtime_key=context.runtime_key,
|
||||||
cache_symbol=cache_symbol,
|
cache_symbol=cache_symbol,
|
||||||
):
|
):
|
||||||
MarketPriceCache.clear(cache_symbol)
|
get_quote_store().clear(
|
||||||
|
source_name=QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
|
symbol=cache_symbol,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
market_status = ExchangeService().get_symbol_market_status(symbol)
|
market_status = ExchangeService().get_symbol_market_status(symbol)
|
||||||
@@ -371,7 +377,8 @@ class MarketDataRunner:
|
|||||||
|
|
||||||
valid_payload_count += 1
|
valid_payload_count += 1
|
||||||
|
|
||||||
MarketPriceCache.set_quote(
|
get_quote_store().set(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
quote,
|
quote,
|
||||||
runtime_key=context.runtime_key,
|
runtime_key=context.runtime_key,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from src.core.config import load_settings
|
from src.core.config import load_settings
|
||||||
from src.integrations.exchange.market_cache import MarketPriceCache
|
|
||||||
from src.integrations.exchange.service import ExchangeService
|
from src.integrations.exchange.service import ExchangeService
|
||||||
from src.integrations.exchange.ws_client import ExchangeWebSocketClient
|
from src.integrations.exchange.ws_client import ExchangeWebSocketClient
|
||||||
from src.market_data.acquisition.adapters.dzengi.websocket import (
|
from src.market_data.acquisition.adapters.dzengi.websocket import (
|
||||||
@@ -13,10 +12,14 @@ from src.market_data.acquisition.adapters.dzengi.websocket import (
|
|||||||
from src.market_data.acquisition.exceptions import (
|
from src.market_data.acquisition.exceptions import (
|
||||||
MarketDataAcquisitionError,
|
MarketDataAcquisitionError,
|
||||||
)
|
)
|
||||||
|
from src.storage.quote_store import (
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
|
get_quote_store,
|
||||||
|
)
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
|
|
||||||
|
|
||||||
# запускает постоянный websocket-поток рынка и обновляет MarketPriceCache
|
# запускает постоянный websocket-поток рынка и обновляет Quote Store
|
||||||
async def start_market_stream() -> None:
|
async def start_market_stream() -> None:
|
||||||
settings = load_settings()
|
settings = load_settings()
|
||||||
journal = JournalService()
|
journal = JournalService()
|
||||||
@@ -52,7 +55,8 @@ async def start_market_stream() -> None:
|
|||||||
if quote.symbol.strip().upper() != symbol.strip().upper():
|
if quote.symbol.strip().upper() != symbol.strip().upper():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
MarketPriceCache.set_quote(
|
get_quote_store().set(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
quote,
|
quote,
|
||||||
runtime_key="default",
|
runtime_key="default",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from src.core.numbers import safe_float
|
|||||||
from src.core.types import NumericLike
|
from src.core.types import NumericLike
|
||||||
from src.integrations.exchange.balance_parser import parse_account_balances
|
from src.integrations.exchange.balance_parser import parse_account_balances
|
||||||
from src.integrations.exchange.exceptions import ExchangeError
|
from src.integrations.exchange.exceptions import ExchangeError
|
||||||
from src.integrations.exchange.market_cache import MarketPriceCache
|
|
||||||
from src.integrations.exchange.mock_data import (
|
from src.integrations.exchange.mock_data import (
|
||||||
mock_balance_summary,
|
mock_balance_summary,
|
||||||
mock_exchange_health,
|
mock_exchange_health,
|
||||||
@@ -71,6 +70,10 @@ from src.storage.instrument_store import (
|
|||||||
InMemoryInstrumentStore,
|
InMemoryInstrumentStore,
|
||||||
InstrumentStoreProtocol,
|
InstrumentStoreProtocol,
|
||||||
)
|
)
|
||||||
|
from src.storage.quote_store import (
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
|
get_quote_store,
|
||||||
|
)
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
|
|
||||||
|
|
||||||
@@ -787,7 +790,8 @@ class ExchangeService:
|
|||||||
if not validation.is_valid:
|
if not validation.is_valid:
|
||||||
raise ExchangeError(validation.message)
|
raise ExchangeError(validation.message)
|
||||||
|
|
||||||
cached_quote = MarketPriceCache.get_quote(
|
cached_quote = get_quote_store().get(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
validation.normalized_symbol,
|
validation.normalized_symbol,
|
||||||
runtime_key=normalized_runtime_key,
|
runtime_key=normalized_runtime_key,
|
||||||
)
|
)
|
||||||
@@ -800,7 +804,8 @@ class ExchangeService:
|
|||||||
return cached_quote
|
return cached_quote
|
||||||
|
|
||||||
quote = self._get_fresh_quote(validation.normalized_symbol)
|
quote = self._get_fresh_quote(validation.normalized_symbol)
|
||||||
MarketPriceCache.set_quote(
|
get_quote_store().set(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
quote,
|
quote,
|
||||||
runtime_key=normalized_runtime_key,
|
runtime_key=normalized_runtime_key,
|
||||||
)
|
)
|
||||||
@@ -827,7 +832,8 @@ class ExchangeService:
|
|||||||
validation.normalized_symbol
|
validation.normalized_symbol
|
||||||
)
|
)
|
||||||
|
|
||||||
MarketPriceCache.set_quote(
|
get_quote_store().set(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
quote,
|
quote,
|
||||||
runtime_key=normalized_runtime_key,
|
runtime_key=normalized_runtime_key,
|
||||||
)
|
)
|
||||||
@@ -855,7 +861,8 @@ class ExchangeService:
|
|||||||
if not validation.is_valid:
|
if not validation.is_valid:
|
||||||
raise ExchangeError(validation.message)
|
raise ExchangeError(validation.message)
|
||||||
|
|
||||||
quote = MarketPriceCache.get_quote(
|
quote = get_quote_store().get(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
validation.normalized_symbol,
|
validation.normalized_symbol,
|
||||||
runtime_key=normalized_runtime_key,
|
runtime_key=normalized_runtime_key,
|
||||||
)
|
)
|
||||||
@@ -873,7 +880,8 @@ class ExchangeService:
|
|||||||
quote = self._get_fresh_quote(
|
quote = self._get_fresh_quote(
|
||||||
validation.normalized_symbol
|
validation.normalized_symbol
|
||||||
)
|
)
|
||||||
MarketPriceCache.set_quote(
|
get_quote_store().set(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
quote,
|
quote,
|
||||||
runtime_key=normalized_runtime_key,
|
runtime_key=normalized_runtime_key,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -213,3 +213,14 @@ class InMemoryQuoteStore:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return normalized_symbol
|
return normalized_symbol
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME = "legacy-market-price-cache"
|
||||||
|
_SHARED_QUOTE_STORE: QuoteStoreProtocol = InMemoryQuoteStore()
|
||||||
|
|
||||||
|
|
||||||
|
def get_quote_store() -> QuoteStoreProtocol:
|
||||||
|
"""Вернуть общий runtime-экземпляр канонического Quote Store."""
|
||||||
|
return _SHARED_QUOTE_STORE
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
# app/tests/unit/integrations/exchange/test_market_cache.py
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from decimal import Decimal
|
|
||||||
from typing import Iterator
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from src.integrations.exchange.market_cache import MarketPriceCache
|
|
||||||
from src.market_data.acquisition.models.quote import Quote
|
|
||||||
from src.storage.quote_store import InMemoryQuoteStore, QuoteStoreProtocol
|
|
||||||
|
|
||||||
|
|
||||||
def _quote(symbol: str = "BTC/USD_LEVERAGE") -> Quote:
|
|
||||||
return Quote(
|
|
||||||
symbol=symbol,
|
|
||||||
last_price=Decimal("100"),
|
|
||||||
bid_price=Decimal("99"),
|
|
||||||
ask_price=Decimal("101"),
|
|
||||||
exchange_timestamp=None,
|
|
||||||
received_at=datetime.now(timezone.utc),
|
|
||||||
source="test",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def reset_market_price_cache() -> Iterator[None]:
|
|
||||||
original_store = MarketPriceCache._store
|
|
||||||
MarketPriceCache._store = InMemoryQuoteStore()
|
|
||||||
yield
|
|
||||||
MarketPriceCache._store = original_store
|
|
||||||
|
|
||||||
|
|
||||||
def test_market_price_cache_uses_quote_store_protocol() -> None:
|
|
||||||
assert isinstance(MarketPriceCache._store, QuoteStoreProtocol)
|
|
||||||
|
|
||||||
|
|
||||||
def test_set_and_get_quote_preserve_identity() -> None:
|
|
||||||
quote = _quote()
|
|
||||||
MarketPriceCache.set_quote(quote, runtime_key="auto")
|
|
||||||
assert MarketPriceCache.get_quote(quote.symbol, runtime_key="auto") is quote
|
|
||||||
|
|
||||||
|
|
||||||
def test_symbol_and_runtime_key_are_normalized() -> None:
|
|
||||||
quote = _quote("BTC/USD_LEVERAGE")
|
|
||||||
MarketPriceCache.set_quote(quote, runtime_key=" AUTO ")
|
|
||||||
assert MarketPriceCache.get_quote(" btc/usd_leverage ", runtime_key="auto") is quote
|
|
||||||
|
|
||||||
|
|
||||||
def test_runtime_keys_are_isolated() -> None:
|
|
||||||
auto = _quote()
|
|
||||||
debug = Quote(
|
|
||||||
symbol=auto.symbol, last_price=Decimal("200"), bid_price=Decimal("199"),
|
|
||||||
ask_price=Decimal("201"), exchange_timestamp=None,
|
|
||||||
received_at=datetime.now(timezone.utc), source="debug",
|
|
||||||
)
|
|
||||||
MarketPriceCache.set_quote(auto, runtime_key="auto")
|
|
||||||
MarketPriceCache.set_quote(debug, runtime_key="debug_auto")
|
|
||||||
assert MarketPriceCache.get_quote(auto.symbol, runtime_key="auto") is auto
|
|
||||||
assert MarketPriceCache.get_quote(auto.symbol, runtime_key="debug_auto") is debug
|
|
||||||
|
|
||||||
|
|
||||||
def test_clear_targeted_and_all() -> None:
|
|
||||||
btc = _quote("BTC/USD_LEVERAGE")
|
|
||||||
eth = _quote("ETH/USD_LEVERAGE")
|
|
||||||
MarketPriceCache.set_quote(btc, runtime_key="auto")
|
|
||||||
MarketPriceCache.set_quote(eth, runtime_key="auto")
|
|
||||||
MarketPriceCache.clear(btc.symbol, runtime_key="auto")
|
|
||||||
assert MarketPriceCache.get_quote(btc.symbol, runtime_key="auto") is None
|
|
||||||
assert MarketPriceCache.get_quote(eth.symbol, runtime_key="auto") is eth
|
|
||||||
MarketPriceCache.clear()
|
|
||||||
assert MarketPriceCache.get_quote(eth.symbol, runtime_key="auto") is None
|
|
||||||
@@ -205,14 +205,15 @@ def test_run_websocket_uses_canonical_quote_adapter(
|
|||||||
mapped_documents.append(document)
|
mapped_documents.append(document)
|
||||||
return quote
|
return quote
|
||||||
|
|
||||||
class Cache:
|
class Store:
|
||||||
@classmethod
|
def set(
|
||||||
def set_quote(
|
self,
|
||||||
cls,
|
source_name: str,
|
||||||
value: Quote,
|
value: Quote,
|
||||||
*,
|
*,
|
||||||
runtime_key: str = "default",
|
runtime_key: str = "default",
|
||||||
) -> None:
|
) -> None:
|
||||||
|
assert source_name == runner_module.QUOTE_RUNTIME_SOURCE_NAME
|
||||||
stored_quotes.append((value, runtime_key))
|
stored_quotes.append((value, runtime_key))
|
||||||
|
|
||||||
context = MarketRuntimeContext(
|
context = MarketRuntimeContext(
|
||||||
@@ -247,7 +248,7 @@ def test_run_websocket_uses_canonical_quote_adapter(
|
|||||||
)
|
)
|
||||||
monkeypatch.setattr(runner_module, "ExchangeWebSocketClient", Client)
|
monkeypatch.setattr(runner_module, "ExchangeWebSocketClient", Client)
|
||||||
monkeypatch.setattr(runner_module, "DzengiWebSocketQuoteAdapter", Adapter)
|
monkeypatch.setattr(runner_module, "DzengiWebSocketQuoteAdapter", Adapter)
|
||||||
monkeypatch.setattr(runner_module, "MarketPriceCache", Cache)
|
monkeypatch.setattr(runner_module, "get_quote_store", lambda: Store())
|
||||||
|
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
MarketDataRunner._run_websocket(
|
MarketDataRunner._run_websocket(
|
||||||
|
|||||||
@@ -357,14 +357,15 @@ def test_start_market_stream_maps_message_to_quote(
|
|||||||
mapped_documents.append(document)
|
mapped_documents.append(document)
|
||||||
return quote
|
return quote
|
||||||
|
|
||||||
class Cache:
|
class Store:
|
||||||
@classmethod
|
def set(
|
||||||
def set_quote(
|
self,
|
||||||
cls,
|
source_name: str,
|
||||||
value: Quote,
|
value: Quote,
|
||||||
*,
|
*,
|
||||||
runtime_key: str = "default",
|
runtime_key: str = "default",
|
||||||
) -> None:
|
) -> None:
|
||||||
|
assert source_name == stream_module.QUOTE_RUNTIME_SOURCE_NAME
|
||||||
stored_quotes.append((value, runtime_key))
|
stored_quotes.append((value, runtime_key))
|
||||||
|
|
||||||
monkeypatch.setattr(stream_module, "load_settings", lambda: settings)
|
monkeypatch.setattr(stream_module, "load_settings", lambda: settings)
|
||||||
@@ -379,7 +380,7 @@ def test_start_market_stream_maps_message_to_quote(
|
|||||||
monkeypatch.setattr(stream_module, "ExchangeService", lambda: service)
|
monkeypatch.setattr(stream_module, "ExchangeService", lambda: service)
|
||||||
monkeypatch.setattr(stream_module, "ExchangeWebSocketClient", Client)
|
monkeypatch.setattr(stream_module, "ExchangeWebSocketClient", Client)
|
||||||
monkeypatch.setattr(stream_module, "DzengiWebSocketQuoteAdapter", Adapter)
|
monkeypatch.setattr(stream_module, "DzengiWebSocketQuoteAdapter", Adapter)
|
||||||
monkeypatch.setattr(stream_module, "MarketPriceCache", Cache)
|
monkeypatch.setattr(stream_module, "get_quote_store", lambda: Store())
|
||||||
monkeypatch.setattr(stream_module.asyncio, "sleep", _raise_stop_stream)
|
monkeypatch.setattr(stream_module.asyncio, "sleep", _raise_stop_stream)
|
||||||
|
|
||||||
with pytest.raises(StopStream):
|
with pytest.raises(StopStream):
|
||||||
@@ -416,9 +417,8 @@ def test_start_market_stream_skips_invalid_adapter_message(
|
|||||||
del document
|
del document
|
||||||
raise QuoteSchemaError("Invalid message.")
|
raise QuoteSchemaError("Invalid message.")
|
||||||
|
|
||||||
class Cache:
|
class Store:
|
||||||
@classmethod
|
def set(self, *args: object, **kwargs: object) -> None:
|
||||||
def set_quote(cls, *args: object, **kwargs: object) -> None:
|
|
||||||
nonlocal cache_calls
|
nonlocal cache_calls
|
||||||
cache_calls += 1
|
cache_calls += 1
|
||||||
|
|
||||||
@@ -434,7 +434,7 @@ def test_start_market_stream_skips_invalid_adapter_message(
|
|||||||
monkeypatch.setattr(stream_module, "ExchangeService", lambda: service)
|
monkeypatch.setattr(stream_module, "ExchangeService", lambda: service)
|
||||||
monkeypatch.setattr(stream_module, "ExchangeWebSocketClient", Client)
|
monkeypatch.setattr(stream_module, "ExchangeWebSocketClient", Client)
|
||||||
monkeypatch.setattr(stream_module, "DzengiWebSocketQuoteAdapter", Adapter)
|
monkeypatch.setattr(stream_module, "DzengiWebSocketQuoteAdapter", Adapter)
|
||||||
monkeypatch.setattr(stream_module, "MarketPriceCache", Cache)
|
monkeypatch.setattr(stream_module, "get_quote_store", lambda: Store())
|
||||||
monkeypatch.setattr(stream_module.asyncio, "sleep", _raise_stop_stream)
|
monkeypatch.setattr(stream_module.asyncio, "sleep", _raise_stop_stream)
|
||||||
|
|
||||||
with pytest.raises(StopStream):
|
with pytest.raises(StopStream):
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ from typing import cast
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.core.config import Settings
|
from src.core.config import Settings
|
||||||
from src.integrations.exchange.market_cache import MarketPriceCache
|
import src.integrations.exchange.service as service_module
|
||||||
from src.integrations.exchange.service import ExchangeService
|
from src.integrations.exchange.service import ExchangeService
|
||||||
from src.market_data.acquisition.models.quote import Quote
|
from src.market_data.acquisition.models.quote import Quote
|
||||||
|
from src.storage.quote_store import QUOTE_RUNTIME_SOURCE_NAME
|
||||||
|
|
||||||
|
|
||||||
def _service() -> ExchangeService:
|
def _service() -> ExchangeService:
|
||||||
@@ -55,7 +56,13 @@ def test_execution_snapshot_reads_canonical_cached_quote(
|
|||||||
quote = _quote()
|
quote = _quote()
|
||||||
|
|
||||||
monkeypatch.setattr(service, "validate_symbol", lambda _: _valid_validation())
|
monkeypatch.setattr(service, "validate_symbol", lambda _: _valid_validation())
|
||||||
monkeypatch.setattr(MarketPriceCache, "get_quote", lambda *_, **__: quote)
|
class Store:
|
||||||
|
def get(self, source_name, symbol, *, runtime_key="default"):
|
||||||
|
assert source_name == QUOTE_RUNTIME_SOURCE_NAME
|
||||||
|
del symbol, runtime_key
|
||||||
|
return quote
|
||||||
|
|
||||||
|
monkeypatch.setattr(service_module, "get_quote_store", lambda: Store())
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
service,
|
service,
|
||||||
"_get_fresh_quote",
|
"_get_fresh_quote",
|
||||||
@@ -89,13 +96,18 @@ def test_execution_snapshot_uses_fresh_quote_for_stale_cache(
|
|||||||
stored: list[tuple[Quote, str]] = []
|
stored: list[tuple[Quote, str]] = []
|
||||||
|
|
||||||
monkeypatch.setattr(service, "validate_symbol", lambda _: _valid_validation())
|
monkeypatch.setattr(service, "validate_symbol", lambda _: _valid_validation())
|
||||||
monkeypatch.setattr(MarketPriceCache, "get_quote", lambda *_, **__: stale)
|
class Store:
|
||||||
|
def get(self, source_name, symbol, *, runtime_key="default"):
|
||||||
|
assert source_name == QUOTE_RUNTIME_SOURCE_NAME
|
||||||
|
del symbol, runtime_key
|
||||||
|
return stale
|
||||||
|
|
||||||
|
def set(self, source_name, quote, *, runtime_key="default"):
|
||||||
|
assert source_name == QUOTE_RUNTIME_SOURCE_NAME
|
||||||
|
stored.append((quote, runtime_key))
|
||||||
|
|
||||||
|
monkeypatch.setattr(service_module, "get_quote_store", lambda: Store())
|
||||||
monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh)
|
monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh)
|
||||||
monkeypatch.setattr(
|
|
||||||
MarketPriceCache,
|
|
||||||
"set_quote",
|
|
||||||
lambda quote, *, runtime_key: stored.append((quote, runtime_key)),
|
|
||||||
)
|
|
||||||
|
|
||||||
result = service.get_execution_snapshot(
|
result = service.get_execution_snapshot(
|
||||||
"BTC/USD_LEVERAGE",
|
"BTC/USD_LEVERAGE",
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ import pytest
|
|||||||
import src.integrations.exchange.service as service_module
|
import src.integrations.exchange.service as service_module
|
||||||
from src.core.config import Settings
|
from src.core.config import Settings
|
||||||
from src.integrations.exchange.exceptions import ExchangeError
|
from src.integrations.exchange.exceptions import ExchangeError
|
||||||
from src.integrations.exchange.market_cache import MarketPriceCache
|
|
||||||
from src.integrations.exchange.service import ExchangeService
|
from src.integrations.exchange.service import ExchangeService
|
||||||
from src.market_data.acquisition.models.quote import Quote
|
from src.market_data.acquisition.models.quote import Quote
|
||||||
from src.storage.quote_store import InMemoryQuoteStore
|
import src.storage.quote_store as quote_store_module
|
||||||
|
from src.storage.quote_store import (
|
||||||
|
InMemoryQuoteStore,
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
|
get_quote_store,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _service(*, exchange_enabled: bool = True) -> ExchangeService:
|
def _service(*, exchange_enabled: bool = True) -> ExchangeService:
|
||||||
@@ -46,11 +50,13 @@ def _quote(*, age_seconds: float = 0.0) -> Quote:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def reset_store():
|
def reset_store(monkeypatch: pytest.MonkeyPatch):
|
||||||
original = MarketPriceCache._store
|
monkeypatch.setattr(
|
||||||
MarketPriceCache._store = InMemoryQuoteStore()
|
quote_store_module,
|
||||||
|
"_SHARED_QUOTE_STORE",
|
||||||
|
InMemoryQuoteStore(),
|
||||||
|
)
|
||||||
yield
|
yield
|
||||||
MarketPriceCache._store = original
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_quote_returns_fresh_cached_identity(
|
def test_get_quote_returns_fresh_cached_identity(
|
||||||
@@ -58,7 +64,8 @@ def test_get_quote_returns_fresh_cached_identity(
|
|||||||
) -> None:
|
) -> None:
|
||||||
service = _service()
|
service = _service()
|
||||||
quote = _quote()
|
quote = _quote()
|
||||||
MarketPriceCache.set_quote(quote, runtime_key="auto")
|
get_quote_store().set(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,quote, runtime_key="auto")
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
service,
|
service,
|
||||||
"validate_symbol",
|
"validate_symbol",
|
||||||
@@ -83,7 +90,8 @@ def test_get_quote_refreshes_stale_quote(
|
|||||||
service = _service()
|
service = _service()
|
||||||
stale = _quote(age_seconds=10)
|
stale = _quote(age_seconds=10)
|
||||||
fresh = _quote()
|
fresh = _quote()
|
||||||
MarketPriceCache.set_quote(stale, runtime_key="auto")
|
get_quote_store().set(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,stale, runtime_key="auto")
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
service,
|
service,
|
||||||
"validate_symbol",
|
"validate_symbol",
|
||||||
@@ -98,7 +106,8 @@ def test_get_quote_refreshes_stale_quote(
|
|||||||
result = service.get_quote(fresh.symbol, runtime_key="auto")
|
result = service.get_quote(fresh.symbol, runtime_key="auto")
|
||||||
|
|
||||||
assert result is fresh
|
assert result is fresh
|
||||||
assert MarketPriceCache.get_quote(fresh.symbol, runtime_key="auto") is fresh
|
assert get_quote_store().get(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,fresh.symbol, runtime_key="auto") is fresh
|
||||||
|
|
||||||
|
|
||||||
def test_get_quote_wraps_acquisition_error(
|
def test_get_quote_wraps_acquisition_error(
|
||||||
@@ -125,7 +134,8 @@ def test_refresh_quote_cache_forces_fresh_quote_and_preserves_identity(
|
|||||||
service = _service()
|
service = _service()
|
||||||
cached = _quote()
|
cached = _quote()
|
||||||
fresh = _quote()
|
fresh = _quote()
|
||||||
MarketPriceCache.set_quote(cached, runtime_key="auto")
|
get_quote_store().set(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,cached, runtime_key="auto")
|
||||||
requested_symbols: list[str] = []
|
requested_symbols: list[str] = []
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
@@ -147,7 +157,8 @@ def test_refresh_quote_cache_forces_fresh_quote_and_preserves_identity(
|
|||||||
assert requested_symbols == [" btc/usd_leverage "]
|
assert requested_symbols == [" btc/usd_leverage "]
|
||||||
assert result is fresh
|
assert result is fresh
|
||||||
assert result is not cached
|
assert result is not cached
|
||||||
assert MarketPriceCache.get_quote(fresh.symbol, runtime_key="auto") is fresh
|
assert get_quote_store().get(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,fresh.symbol, runtime_key="auto") is fresh
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_quote_cache_uses_default_runtime_key(
|
def test_refresh_quote_cache_uses_default_runtime_key(
|
||||||
@@ -169,7 +180,8 @@ def test_refresh_quote_cache_uses_default_runtime_key(
|
|||||||
result = service.refresh_quote_cache(fresh.symbol)
|
result = service.refresh_quote_cache(fresh.symbol)
|
||||||
|
|
||||||
assert result is fresh
|
assert result is fresh
|
||||||
assert MarketPriceCache.get_quote(fresh.symbol, runtime_key="auto") is fresh
|
assert get_quote_store().get(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,fresh.symbol, runtime_key="auto") is fresh
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_quote_cache_normalizes_runtime_key(
|
def test_refresh_quote_cache_normalizes_runtime_key(
|
||||||
@@ -195,7 +207,8 @@ def test_refresh_quote_cache_normalizes_runtime_key(
|
|||||||
|
|
||||||
assert result is fresh
|
assert result is fresh
|
||||||
assert (
|
assert (
|
||||||
MarketPriceCache.get_quote(
|
get_quote_store().get(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
fresh.symbol,
|
fresh.symbol,
|
||||||
runtime_key="debug_auto",
|
runtime_key="debug_auto",
|
||||||
)
|
)
|
||||||
@@ -234,7 +247,8 @@ def test_refresh_quote_cache_does_not_update_cache_on_acquisition_error(
|
|||||||
service = _service()
|
service = _service()
|
||||||
cached = _quote()
|
cached = _quote()
|
||||||
original = RuntimeError("unavailable")
|
original = RuntimeError("unavailable")
|
||||||
MarketPriceCache.set_quote(cached, runtime_key="auto")
|
get_quote_store().set(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,cached, runtime_key="auto")
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
service,
|
service,
|
||||||
"validate_symbol",
|
"validate_symbol",
|
||||||
@@ -255,7 +269,8 @@ def test_refresh_quote_cache_does_not_update_cache_on_acquisition_error(
|
|||||||
service.refresh_quote_cache(cached.symbol, runtime_key="auto")
|
service.refresh_quote_cache(cached.symbol, runtime_key="auto")
|
||||||
|
|
||||||
assert exc_info.value.__cause__ is original
|
assert exc_info.value.__cause__ is original
|
||||||
assert MarketPriceCache.get_quote(cached.symbol, runtime_key="auto") is cached
|
assert get_quote_store().get(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,cached.symbol, runtime_key="auto") is cached
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_quote_cache_uses_and_stores_mock_quote(
|
def test_refresh_quote_cache_uses_and_stores_mock_quote(
|
||||||
@@ -286,7 +301,8 @@ def test_refresh_quote_cache_uses_and_stores_mock_quote(
|
|||||||
|
|
||||||
assert result is mock
|
assert result is mock
|
||||||
assert (
|
assert (
|
||||||
MarketPriceCache.get_quote(
|
get_quote_store().get(
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
mock.symbol,
|
mock.symbol,
|
||||||
runtime_key="debug_auto",
|
runtime_key="debug_auto",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ import pytest
|
|||||||
|
|
||||||
from src.market_data.acquisition.models.quote import Quote
|
from src.market_data.acquisition.models.quote import Quote
|
||||||
from src.storage.exceptions import QuoteStoreError, StorageError
|
from src.storage.exceptions import QuoteStoreError, StorageError
|
||||||
|
import src.storage.quote_store as quote_store_module
|
||||||
from src.storage.quote_store import (
|
from src.storage.quote_store import (
|
||||||
InMemoryQuoteStore,
|
InMemoryQuoteStore,
|
||||||
|
QUOTE_RUNTIME_SOURCE_NAME,
|
||||||
QuoteStoreProtocol,
|
QuoteStoreProtocol,
|
||||||
|
get_quote_store,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -446,3 +449,31 @@ def test_quote_store_error_inherits_storage_error() -> None:
|
|||||||
error = QuoteStoreError("test")
|
error = QuoteStoreError("test")
|
||||||
|
|
||||||
assert isinstance(error, StorageError)
|
assert isinstance(error, StorageError)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_shared_quote_store_matches_protocol() -> None:
|
||||||
|
assert isinstance(get_quote_store(), QuoteStoreProtocol)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shared_quote_store_returns_same_instance() -> None:
|
||||||
|
assert get_quote_store() is get_quote_store()
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_source_name_preserves_compatibility_namespace() -> None:
|
||||||
|
assert QUOTE_RUNTIME_SOURCE_NAME == "legacy-market-price-cache"
|
||||||
|
|
||||||
|
|
||||||
|
def test_shared_quote_store_can_be_replaced_in_test(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
replacement = InMemoryQuoteStore()
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
quote_store_module,
|
||||||
|
"_SHARED_QUOTE_STORE",
|
||||||
|
replacement,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert get_quote_store() is replacement
|
||||||
|
|||||||
Reference in New Issue
Block a user