Spaces:
Sleeping
Sleeping
File size: 7,129 Bytes
2e658e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | """Focused tests for the DS4 merge-nesting fix in dual_datasource_client.py.
Verifies: (1) DS4-owned fields are read from ds4["data"], not ds4 top level;
(2) DS2 fallback still fires only when a DS4 data field is genuinely
missing; (3) DS2 still cannot override a present/valid DS4 field once read
from the correct nested path; (4) top-level metadata (dataState,
noTradeGuard) is unaffected by the fix, since it was never part of the bug.
Mocked HTTP only -- no live network calls.
"""
import asyncio
import os
import sys
from datetime import datetime, timezone
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) # hermes_overlay/
import trading.dual_datasource_client as ddc
@pytest.fixture(autouse=True)
def _disable_live_binance(monkeypatch):
async def _unavailable(symbol, needed):
return {}, []
monkeypatch.setattr(ddc.binance_public, "get_binance_public_snapshot", _unavailable)
class _FakeResponse:
def __init__(self, payload, status_code=200):
self._payload = payload
self.status_code = status_code
def raise_for_status(self):
pass
def json(self):
return self._payload
def _make_fake_client(ds4_payload, ds2_payloads=None, ds4_raises=False):
ds2_payloads = ds2_payloads or {}
class _FakeClient:
async def __aenter__(self):
return self
async def __aexit__(self, *a):
return False
async def get(self, url, timeout=None, **_kwargs):
if "short-hunter/snapshot" in url:
if ds4_raises:
raise RuntimeError("boom")
return _FakeResponse(ds4_payload)
for key, payload in ds2_payloads.items():
if key in url:
return _FakeResponse(payload)
return _FakeResponse({})
return _FakeClient()
def _nested_ds4_payload(**overrides):
"""A realistic DS4 envelope: metadata top-level, market fields under data."""
base = {
"success": True,
"symbol": "BTCUSDT",
"dataState": "live",
"noTradeGuard": False,
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"warnings": [],
"errors": [],
"data": {
# contractType/status let _contract_verification() mark this a
# verified Futures instrument (see docs/DATA_PIPELINE_AND_CONTRACTS.md
# and docs/DEVELOPER_GUIDE.md: DS4 owns Futures verification, and a
# bare {"symbol": ...} is deliberately treated as unverified).
"contract": {"symbol": "XBTUSDTM", "contractType": "PERPETUAL", "status": "TRADING"},
"ticker": {"lastPrice": 64364.4, "change24h": None},
"ohlcv": [
{"timestamp": i, "open": 64360, "high": 64370, "low": 64350,
"close": 64364.4, "volume": 5}
for i in range(4)
],
"orderbook": {"bids": [[64364.4, 1]], "asks": [[64364.5, 1]]},
"funding": {"currentFundingRate": -8.2e-05},
"openInterest": {"openInterest": 27000928.0, "history": []},
"indicators": {"rsi14": 49.3},
"sentiment": None,
},
}
base.update(overrides)
return base
def test_merge_reads_futures_fields_from_nested_data(monkeypatch):
payload = _nested_ds4_payload()
monkeypatch.setattr(ddc.httpx, "AsyncClient", lambda *a, **kw: _make_fake_client(payload))
result = asyncio.run(ddc.get_market_context("BTCUSDT"))
merged = result["merged"]
assert merged["ticker"] == {"lastPrice": 64364.4, "change24h": None}
assert merged["funding"] == {"currentFundingRate": -8.2e-05}
assert merged["openInterest"] == {"openInterest": 27000928.0, "history": []}
assert merged["orderbook"] == {"bids": [[64364.4, 1]], "asks": [[64364.5, 1]]}
assert merged["contract"] == {
"symbol": "XBTUSDTM",
"contractType": "PERPETUAL",
"status": "TRADING",
}
assert result["sources"]["ticker"] == "datasource4"
assert result["sources"]["funding"] == "datasource4"
assert result["noTradeGuard"] is False
def test_ds2_fallback_fires_only_for_genuinely_missing_nested_field(monkeypatch):
payload = _nested_ds4_payload()
payload["data"]["orderbook"] = None # DS4 genuinely missing this field
ds2_payloads = {"trading/orderbook": {"bids": [[1, 1]], "asks": [[1, 1]]}}
monkeypatch.setattr(
ddc.httpx, "AsyncClient", lambda *a, **kw: _make_fake_client(payload, ds2_payloads)
)
result = asyncio.run(ddc.get_market_context("BTCUSDT"))
merged = result["merged"]
assert merged["orderbook"] == {"bids": [[1, 1]], "asks": [[1, 1]]}
assert result["sources"]["orderbook"] == "datasource2"
assert any("orderbook" in w and "Datasource 2 fallback" in w for w in result["warnings"])
# untouched DS4-owned fields still come from DS4, not DS2
assert result["sources"]["ticker"] == "datasource4"
def test_ds2_cannot_override_valid_nested_ds4_orderbook(monkeypatch):
payload = _nested_ds4_payload() # DS4 orderbook present and valid
ds2_payloads = {"trading/orderbook": {"bids": [[999, 1]], "asks": [[999, 1]]}}
monkeypatch.setattr(
ddc.httpx, "AsyncClient", lambda *a, **kw: _make_fake_client(payload, ds2_payloads)
)
result = asyncio.run(ddc.get_market_context("BTCUSDT"))
merged = result["merged"]
assert merged["orderbook"] == {"bids": [[64364.4, 1]], "asks": [[64364.5, 1]]}
assert result["sources"]["orderbook"] == "datasource4"
def test_ds4_data_key_missing_entirely_is_treated_as_no_data(monkeypatch):
payload = {
"success": False, "symbol": "BTCUSDT", "dataState": "PARTIAL",
"noTradeGuard": False, "timestamp": "x", "warnings": [], "errors": [],
# no "data" key at all -- malformed response
}
monkeypatch.setattr(ddc.httpx, "AsyncClient", lambda *a, **kw: _make_fake_client(payload))
result = asyncio.run(ddc.get_market_context("BTCUSDT"))
assert result["sources"]["ticker"] == "unavailable"
assert result["merged"]["ticker"] is None
assert result["noTradeGuard"] is True # missing critical fields after merge
assert any("Missing required Futures fields" in r for r in result["noTradeReasons"])
def test_top_level_metadata_unaffected_by_nesting_fix(monkeypatch):
"""dataState/noTradeGuard were never part of the bug -- must stay top-level reads."""
payload = _nested_ds4_payload(dataState="PARTIAL")
monkeypatch.setattr(ddc.httpx, "AsyncClient", lambda *a, **kw: _make_fake_client(payload))
result = asyncio.run(ddc.get_market_context("BTCUSDT"))
assert result["noTradeGuard"] is True
assert any("dataState='PARTIAL'" in r for r in result["noTradeReasons"])
# Stale DS4 data remains visible in primary for diagnostics, but is not
# silently promoted into the merged current-market view.
assert result["primary"]["data"]["ticker"] == {"lastPrice": 64364.4, "change24h": None}
assert result["merged"]["ticker"] is None
assert result["sources"]["ticker"] == "unavailable"
|