1
This commit is contained in:
parent
3fa876360b
commit
15106cd755
Binary file not shown.
@ -82,6 +82,7 @@ async def clob_market_stream(
|
||||
asset_ids: list[str],
|
||||
state: ClobBookState,
|
||||
on_event=None,
|
||||
receive_timeout_s: float = 15.0,
|
||||
) -> None:
|
||||
subscribe = {
|
||||
"assets_ids": asset_ids,
|
||||
@ -92,7 +93,8 @@ async def clob_market_stream(
|
||||
try:
|
||||
async with session.ws_connect(CLOB_WS, heartbeat=20) as ws:
|
||||
await ws.send_json(subscribe)
|
||||
async for msg in ws:
|
||||
while True:
|
||||
msg = await ws.receive(timeout=receive_timeout_s)
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
payload = msg.json()
|
||||
items = payload if isinstance(payload, list) else [payload]
|
||||
@ -103,6 +105,10 @@ async def clob_market_stream(
|
||||
on_event(item)
|
||||
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
||||
break
|
||||
elif msg.type == aiohttp.WSMsgType.CLOSE:
|
||||
break
|
||||
except (TimeoutError, asyncio.TimeoutError):
|
||||
await asyncio.sleep(0.2)
|
||||
except Exception:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@ -494,11 +494,11 @@ def _health_dict(
|
||||
add(1, f"状态刷新偏慢 {state_age_ms} ms")
|
||||
|
||||
if rtds_lag_ms is None:
|
||||
add(1, "RTDS 延迟未知")
|
||||
elif rtds_lag_ms > 30_000:
|
||||
add(2, f"RTDS 延迟过高 {rtds_lag_ms} ms")
|
||||
elif rtds_lag_ms > 10_000:
|
||||
add(1, f"RTDS 延迟偏高 {rtds_lag_ms} ms")
|
||||
add(1, "RTDS 源时间未知")
|
||||
elif rtds_lag_ms > 180_000:
|
||||
add(2, f"RTDS 源时间过旧 {rtds_lag_ms} ms")
|
||||
elif rtds_lag_ms > 90_000:
|
||||
add(1, f"RTDS 源时间偏旧 {rtds_lag_ms} ms")
|
||||
|
||||
if clob_book_age_ms is None:
|
||||
add(1, "CLOB 盘口尚未到达")
|
||||
|
||||
@ -91,9 +91,17 @@ async def polymarket_chainlink_btc_stream(
|
||||
async def resilient_btc_price_stream(
|
||||
session: aiohttp.ClientSession,
|
||||
) -> AsyncIterator[PriceTick]:
|
||||
last_source_timestamp_ms: int | None = None
|
||||
while True:
|
||||
try:
|
||||
async for tick in polymarket_chainlink_btc_stream(session):
|
||||
if tick.source_timestamp_ms is not None:
|
||||
if (
|
||||
last_source_timestamp_ms is not None
|
||||
and tick.source_timestamp_ms <= last_source_timestamp_ms
|
||||
):
|
||||
continue
|
||||
last_source_timestamp_ms = tick.source_timestamp_ms
|
||||
yield tick
|
||||
except Exception:
|
||||
await asyncio.sleep(1)
|
||||
@ -128,7 +136,11 @@ def _parse_chainlink_message(message: dict) -> list[PriceTick]:
|
||||
now = datetime.now(timezone.utc)
|
||||
if "data" in payload:
|
||||
ticks = []
|
||||
for item in payload.get("data") or []:
|
||||
items = sorted(
|
||||
payload.get("data") or [],
|
||||
key=lambda item: _int_or_none(item.get("timestamp")) or 0,
|
||||
)
|
||||
for item in items:
|
||||
value = item.get("value")
|
||||
if value is not None:
|
||||
ticks.append(
|
||||
|
||||
@ -172,7 +172,7 @@ function renderHealth(health) {
|
||||
els.healthText.textContent = health?.label || "--";
|
||||
els.healthDetail.textContent = health?.issues?.length
|
||||
? health.issues.join(" · ")
|
||||
: "RTDS / CLOB / DuckDB 正常";
|
||||
: "接收 / CLOB / DuckDB 正常";
|
||||
els.healthCard.className = `metric health-card ${status}`;
|
||||
}
|
||||
|
||||
|
||||
@ -95,7 +95,7 @@
|
||||
<dd id="recorderPath">--</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>RTDS 延迟</dt>
|
||||
<dt>RTDS 源年龄</dt>
|
||||
<dd id="rtdsLag">--</dd>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
color: var(--ink);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(20, 18, 14, 0.045) 1px, transparent 1px),
|
||||
@ -452,14 +453,50 @@ dd {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1240px) {
|
||||
.shell {
|
||||
width: min(100% - 24px, 1120px);
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.ticker-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.signal-card,
|
||||
.ticker-grid .metric:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.main-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.market-panel,
|
||||
.paper-panel,
|
||||
.replay-panel,
|
||||
.log-panel {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.ticker-grid,
|
||||
.main-grid,
|
||||
.facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ticker-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.signal-card {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.market-panel,
|
||||
.book-panel,
|
||||
.edge-panel,
|
||||
.log-panel,
|
||||
.replay-panel,
|
||||
.paper-panel {
|
||||
@ -467,7 +504,7 @@ dd {
|
||||
}
|
||||
|
||||
.paper-summary {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.topbar {
|
||||
@ -481,3 +518,200 @@ dd {
|
||||
gap: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
body {
|
||||
background-size: 18px 18px;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: 100%;
|
||||
padding: 10px 10px 24px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
gap: 12px;
|
||||
padding: 8px 0 14px;
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 100%;
|
||||
font-size: clamp(36px, 13vw, 50px);
|
||||
line-height: 0.94;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.kicker,
|
||||
.label,
|
||||
.metric small,
|
||||
dt,
|
||||
.analytics-row.header,
|
||||
.paper-summary span {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-strip {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
justify-content: flex-start;
|
||||
padding: 8px 10px;
|
||||
box-shadow: 3px 3px 0 var(--ink);
|
||||
}
|
||||
|
||||
.ticker-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin: 14px 0 10px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
min-height: 92px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.metric strong {
|
||||
font-size: clamp(24px, 9vw, 34px);
|
||||
}
|
||||
|
||||
.metric small {
|
||||
white-space: normal;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.signal-card {
|
||||
box-shadow: 4px 4px 0 var(--ink);
|
||||
}
|
||||
|
||||
.health-card {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.ticker-grid .metric:last-child {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.main-grid {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 12px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
gap: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 5px 7px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.facts {
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.facts div,
|
||||
.paper-summary div {
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
dd {
|
||||
min-height: 18px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.edge-bars {
|
||||
gap: 14px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.edge-row {
|
||||
grid-template-columns: 64px 1fr 72px;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bar-track {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.book {
|
||||
min-height: auto;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.book-row {
|
||||
grid-template-columns: 36px minmax(88px, 1fr) 54px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.paper-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.paper-summary b {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.analytics-table {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.analytics-row {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.analytics-row.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.analytics-row span,
|
||||
.analytics-row b {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.event-log {
|
||||
max-height: 180px;
|
||||
}
|
||||
|
||||
.event {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 340px) {
|
||||
.ticker-grid,
|
||||
.paper-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.signal-card,
|
||||
.health-card,
|
||||
.ticker-grid .metric:last-child {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.edge-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bar-track {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,25 @@ def test_parse_chainlink_rtds_message() -> None:
|
||||
assert tick.price == 77124.45
|
||||
|
||||
|
||||
def test_parse_chainlink_rtds_sorts_batch_by_timestamp() -> None:
|
||||
ticks = _parse_chainlink_message(
|
||||
{
|
||||
"topic": "crypto_prices",
|
||||
"type": "update",
|
||||
"payload": {
|
||||
"symbol": "btc/usd",
|
||||
"data": [
|
||||
{"timestamp": 1779376676000, "value": 77124.45},
|
||||
{"timestamp": 1779376675000, "value": 77123.45},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert [tick.source_timestamp_ms for tick in ticks] == [1779376675000, 1779376676000]
|
||||
assert ticks[-1].price == 77124.45
|
||||
|
||||
|
||||
def test_parse_chainlink_rtds_ignores_other_symbols() -> None:
|
||||
assert _parse_chainlink_message(
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user