diff --git a/data/updown.duckdb.wal b/data/updown.duckdb.wal index ce02206..9ea6134 100644 Binary files a/data/updown.duckdb.wal and b/data/updown.duckdb.wal differ diff --git a/src/poly_updown/clob_ws.py b/src/poly_updown/clob_ws.py index fd1a7d9..5f16cab 100644 --- a/src/poly_updown/clob_ws.py +++ b/src/poly_updown/clob_ws.py @@ -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) diff --git a/src/poly_updown/live.py b/src/poly_updown/live.py index 809c005..56a1a2e 100644 --- a/src/poly_updown/live.py +++ b/src/poly_updown/live.py @@ -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 盘口尚未到达") diff --git a/src/poly_updown/prices.py b/src/poly_updown/prices.py index 6892a7d..42008f3 100644 --- a/src/poly_updown/prices.py +++ b/src/poly_updown/prices.py @@ -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( diff --git a/src/poly_updown/web/app.js b/src/poly_updown/web/app.js index badf275..2fbfd50 100644 --- a/src/poly_updown/web/app.js +++ b/src/poly_updown/web/app.js @@ -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}`; } diff --git a/src/poly_updown/web/index.html b/src/poly_updown/web/index.html index 0d2e5ee..e726c5e 100644 --- a/src/poly_updown/web/index.html +++ b/src/poly_updown/web/index.html @@ -95,7 +95,7 @@
--
-
RTDS 延迟
+
RTDS 源年龄
--
diff --git a/src/poly_updown/web/styles.css b/src/poly_updown/web/styles.css index 7cf1527..90d662f 100644 --- a/src/poly_updown/web/styles.css +++ b/src/poly_updown/web/styles.css @@ -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%; + } +} diff --git a/tests/test_prices.py b/tests/test_prices.py index 727b09c..c13b31e 100644 --- a/tests/test_prices.py +++ b/tests/test_prices.py @@ -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( {