build 040: restore quote REST fallback after build 039
This commit is contained in:
@@ -395,7 +395,7 @@ class MarketDataRunner:
|
|||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
ExchangeService().refresh_market_snapshot_cache,
|
ExchangeService().refresh_quote_cache,
|
||||||
symbol,
|
symbol,
|
||||||
runtime_key=context.runtime_key,
|
runtime_key=context.runtime_key,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -806,6 +806,33 @@ class ExchangeService:
|
|||||||
)
|
)
|
||||||
return quote
|
return quote
|
||||||
|
|
||||||
|
# Принудительно обновить Quote cache через свежий REST Quotes Feed.
|
||||||
|
def refresh_quote_cache(
|
||||||
|
self,
|
||||||
|
symbol: str | None = None,
|
||||||
|
*,
|
||||||
|
runtime_key: str | None = None,
|
||||||
|
) -> Quote:
|
||||||
|
symbol_to_use = symbol or self.settings.default_symbol
|
||||||
|
normalized_runtime_key = self._runtime_key(runtime_key)
|
||||||
|
|
||||||
|
if not self.settings.exchange_enabled:
|
||||||
|
quote = mock_quote(symbol_to_use)
|
||||||
|
else:
|
||||||
|
validation = self.validate_symbol(symbol_to_use)
|
||||||
|
if not validation.is_valid:
|
||||||
|
raise ExchangeError(validation.message)
|
||||||
|
|
||||||
|
quote = self._get_fresh_quote(
|
||||||
|
validation.normalized_symbol
|
||||||
|
)
|
||||||
|
|
||||||
|
MarketPriceCache.set_quote(
|
||||||
|
quote,
|
||||||
|
runtime_key=normalized_runtime_key,
|
||||||
|
)
|
||||||
|
return quote
|
||||||
|
|
||||||
# Получить snapshot, пригодный для execution layer.
|
# Получить snapshot, пригодный для execution layer.
|
||||||
def get_execution_snapshot(
|
def get_execution_snapshot(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -325,3 +325,100 @@ def test_run_websocket_raises_after_five_invalid_quotes(
|
|||||||
"BTC/USD_LEVERAGE",
|
"BTC/USD_LEVERAGE",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rest_fallback_once_refreshes_quote_cache(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from src.integrations.exchange.market_data_runner import MarketRuntimeContext
|
||||||
|
|
||||||
|
calls: list[tuple[str, str | None]] = []
|
||||||
|
|
||||||
|
class Service:
|
||||||
|
def refresh_quote_cache(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
*,
|
||||||
|
runtime_key: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
calls.append((symbol, runtime_key))
|
||||||
|
|
||||||
|
context = MarketRuntimeContext(
|
||||||
|
runtime_key="debug_auto",
|
||||||
|
task=None,
|
||||||
|
interval_seconds=1,
|
||||||
|
symbol_provider=lambda: "BTC/USD_LEVERAGE",
|
||||||
|
screen=None,
|
||||||
|
action="market_data",
|
||||||
|
runtime_label=None,
|
||||||
|
last_rest_state="UNAVAILABLE",
|
||||||
|
last_rest_error_key="previous-error",
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(runner_module, "ExchangeService", Service)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
MarketDataRunner,
|
||||||
|
"_can_log_runtime_event",
|
||||||
|
lambda _: False,
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
MarketDataRunner._rest_fallback_once(
|
||||||
|
context,
|
||||||
|
"BTC/USD_LEVERAGE",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert calls == [("BTC/USD_LEVERAGE", "debug_auto")]
|
||||||
|
assert context.last_rest_state == "AVAILABLE"
|
||||||
|
assert context.last_rest_error_key is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_rest_fallback_once_preserves_unavailable_error_state(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from src.integrations.exchange.market_data_runner import MarketRuntimeContext
|
||||||
|
|
||||||
|
class Service:
|
||||||
|
def refresh_quote_cache(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
*,
|
||||||
|
runtime_key: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
del symbol, runtime_key
|
||||||
|
raise RuntimeError("REST unavailable")
|
||||||
|
|
||||||
|
context = MarketRuntimeContext(
|
||||||
|
runtime_key="auto",
|
||||||
|
task=None,
|
||||||
|
interval_seconds=1,
|
||||||
|
symbol_provider=lambda: "BTC/USD_LEVERAGE",
|
||||||
|
screen=None,
|
||||||
|
action="market_data",
|
||||||
|
runtime_label=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(runner_module, "ExchangeService", Service)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
MarketDataRunner,
|
||||||
|
"_can_log_runtime_event",
|
||||||
|
lambda _: False,
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
MarketDataRunner._rest_fallback_once(
|
||||||
|
context,
|
||||||
|
"BTC/USD_LEVERAGE",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert context.last_rest_state == "UNAVAILABLE"
|
||||||
|
assert context.last_rest_error_key == (
|
||||||
|
"BTC/USD_LEVERAGE:RuntimeError:REST unavailable"
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from typing import cast
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
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.market_cache import MarketPriceCache
|
||||||
@@ -116,3 +117,179 @@ def test_get_quote_wraps_acquisition_error(
|
|||||||
service._get_fresh_quote("BTC/USD_LEVERAGE")
|
service._get_fresh_quote("BTC/USD_LEVERAGE")
|
||||||
|
|
||||||
assert exc_info.value.__cause__ is original
|
assert exc_info.value.__cause__ is original
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_quote_cache_forces_fresh_quote_and_preserves_identity(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
service = _service()
|
||||||
|
cached = _quote()
|
||||||
|
fresh = _quote()
|
||||||
|
MarketPriceCache.set_quote(cached, runtime_key="auto")
|
||||||
|
requested_symbols: list[str] = []
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
service,
|
||||||
|
"validate_symbol",
|
||||||
|
lambda symbol: SimpleNamespace(
|
||||||
|
is_valid=True,
|
||||||
|
normalized_symbol=(requested_symbols.append(symbol) or fresh.symbol),
|
||||||
|
message="",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh)
|
||||||
|
|
||||||
|
result = service.refresh_quote_cache(
|
||||||
|
" btc/usd_leverage ",
|
||||||
|
runtime_key="auto",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert requested_symbols == [" btc/usd_leverage "]
|
||||||
|
assert result is fresh
|
||||||
|
assert result is not cached
|
||||||
|
assert MarketPriceCache.get_quote(fresh.symbol, runtime_key="auto") is fresh
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_quote_cache_uses_default_runtime_key(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
service = _service()
|
||||||
|
fresh = _quote()
|
||||||
|
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.refresh_quote_cache(fresh.symbol)
|
||||||
|
|
||||||
|
assert result is fresh
|
||||||
|
assert MarketPriceCache.get_quote(fresh.symbol, runtime_key="auto") is fresh
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_quote_cache_normalizes_runtime_key(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
service = _service()
|
||||||
|
fresh = _quote()
|
||||||
|
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.refresh_quote_cache(
|
||||||
|
fresh.symbol,
|
||||||
|
runtime_key=" Debug_Auto ",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is fresh
|
||||||
|
assert (
|
||||||
|
MarketPriceCache.get_quote(
|
||||||
|
fresh.symbol,
|
||||||
|
runtime_key="debug_auto",
|
||||||
|
)
|
||||||
|
is fresh
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_quote_cache_rejects_invalid_symbol(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
service = _service()
|
||||||
|
monkeypatch.setattr(
|
||||||
|
service,
|
||||||
|
"validate_symbol",
|
||||||
|
lambda _: SimpleNamespace(
|
||||||
|
is_valid=False,
|
||||||
|
normalized_symbol="UNKNOWN/USD",
|
||||||
|
message="Символ не найден.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
service,
|
||||||
|
"_get_fresh_quote",
|
||||||
|
lambda _: (_ for _ in ()).throw(
|
||||||
|
AssertionError("Fresh Quote must not be requested.")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ExchangeError, match="Символ не найден"):
|
||||||
|
service.refresh_quote_cache("UNKNOWN/USD", runtime_key="auto")
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_quote_cache_does_not_update_cache_on_acquisition_error(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
service = _service()
|
||||||
|
cached = _quote()
|
||||||
|
original = RuntimeError("unavailable")
|
||||||
|
MarketPriceCache.set_quote(cached, runtime_key="auto")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
service,
|
||||||
|
"validate_symbol",
|
||||||
|
lambda _: SimpleNamespace(
|
||||||
|
is_valid=True,
|
||||||
|
normalized_symbol=cached.symbol,
|
||||||
|
message="",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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.refresh_quote_cache(cached.symbol, runtime_key="auto")
|
||||||
|
|
||||||
|
assert exc_info.value.__cause__ is original
|
||||||
|
assert MarketPriceCache.get_quote(cached.symbol, runtime_key="auto") is cached
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_quote_cache_uses_and_stores_mock_quote(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
service = _service(exchange_enabled=False)
|
||||||
|
mock = _quote()
|
||||||
|
monkeypatch.setattr(service_module, "mock_quote", lambda _: mock)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
service,
|
||||||
|
"validate_symbol",
|
||||||
|
lambda _: (_ for _ in ()).throw(
|
||||||
|
AssertionError("Validation is not expected in mock mode.")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
service,
|
||||||
|
"_get_fresh_quote",
|
||||||
|
lambda _: (_ for _ in ()).throw(
|
||||||
|
AssertionError("REST is not expected in mock mode.")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = service.refresh_quote_cache(
|
||||||
|
"BTC/USD_LEVERAGE",
|
||||||
|
runtime_key="debug_auto",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is mock
|
||||||
|
assert (
|
||||||
|
MarketPriceCache.get_quote(
|
||||||
|
mock.symbol,
|
||||||
|
runtime_key="debug_auto",
|
||||||
|
)
|
||||||
|
is mock
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user