feat: add market data architecture and complete migration through build 039

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit a996f2f797
443 changed files with 80452 additions and 1335 deletions

View File

@@ -0,0 +1,118 @@
# app/tests/unit/integrations/exchange/test_service_quote.py
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from types import SimpleNamespace
from typing import cast
import pytest
from src.core.config import Settings
from src.integrations.exchange.exceptions import ExchangeError
from src.integrations.exchange.market_cache import MarketPriceCache
from src.integrations.exchange.service import ExchangeService
from src.market_data.acquisition.models.quote import Quote
from src.storage.quote_store import InMemoryQuoteStore
def _service(*, exchange_enabled: bool = True) -> ExchangeService:
service = object.__new__(ExchangeService)
service.settings = cast(
Settings,
SimpleNamespace(
exchange_enabled=exchange_enabled,
default_symbol="BTC/USD_LEVERAGE",
),
)
return service
def _quote(*, age_seconds: float = 0.0) -> Quote:
return Quote(
symbol="BTC/USD_LEVERAGE",
last_price=Decimal("100"),
bid_price=Decimal("99"),
ask_price=Decimal("101"),
exchange_timestamp=None,
received_at=(
datetime.now(timezone.utc)
- timedelta(seconds=age_seconds)
),
source="test",
)
@pytest.fixture(autouse=True)
def reset_store():
original = MarketPriceCache._store
MarketPriceCache._store = InMemoryQuoteStore()
yield
MarketPriceCache._store = original
def test_get_quote_returns_fresh_cached_identity(
monkeypatch: pytest.MonkeyPatch,
) -> None:
service = _service()
quote = _quote()
MarketPriceCache.set_quote(quote, runtime_key="auto")
monkeypatch.setattr(
service,
"validate_symbol",
lambda _: SimpleNamespace(
is_valid=True,
normalized_symbol=quote.symbol,
message="",
),
)
monkeypatch.setattr(
service,
"_get_fresh_quote",
lambda _: (_ for _ in ()).throw(AssertionError("REST not expected")),
)
assert service.get_quote(quote.symbol, runtime_key="auto") is quote
def test_get_quote_refreshes_stale_quote(
monkeypatch: pytest.MonkeyPatch,
) -> None:
service = _service()
stale = _quote(age_seconds=10)
fresh = _quote()
MarketPriceCache.set_quote(stale, runtime_key="auto")
monkeypatch.setattr(
service,
"validate_symbol",
lambda _: SimpleNamespace(
is_valid=True,
normalized_symbol=fresh.symbol,
message="",
),
)
monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh)
result = service.get_quote(fresh.symbol, runtime_key="auto")
assert result is fresh
assert MarketPriceCache.get_quote(fresh.symbol, runtime_key="auto") is fresh
def test_get_quote_wraps_acquisition_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
service = _service()
original = RuntimeError("unavailable")
monkeypatch.setattr(
service,
"_load_quote_via_acquisition",
lambda _: (_ for _ in ()).throw(original),
)
monkeypatch.setattr(service, "_log_exchange_error", lambda **_: None)
with pytest.raises(ExchangeError) as exc_info:
service._get_fresh_quote("BTC/USD_LEVERAGE")
assert exc_info.value.__cause__ is original