This commit is contained in:
aaron 2026-05-22 09:21:35 +08:00
parent 3fa876360b
commit 15106cd755
8 changed files with 282 additions and 11 deletions

Binary file not shown.

View File

@ -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)

View File

@ -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 盘口尚未到达")

View File

@ -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(

View File

@ -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}`;
}

View File

@ -95,7 +95,7 @@
<dd id="recorderPath">--</dd>
</div>
<div>
<dt>RTDS 延迟</dt>
<dt>RTDS 源年龄</dt>
<dd id="rtdsLag">--</dd>
</div>
<div>

View File

@ -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%;
}
}

View File

@ -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(
{