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],
|
asset_ids: list[str],
|
||||||
state: ClobBookState,
|
state: ClobBookState,
|
||||||
on_event=None,
|
on_event=None,
|
||||||
|
receive_timeout_s: float = 15.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
subscribe = {
|
subscribe = {
|
||||||
"assets_ids": asset_ids,
|
"assets_ids": asset_ids,
|
||||||
@ -92,7 +93,8 @@ async def clob_market_stream(
|
|||||||
try:
|
try:
|
||||||
async with session.ws_connect(CLOB_WS, heartbeat=20) as ws:
|
async with session.ws_connect(CLOB_WS, heartbeat=20) as ws:
|
||||||
await ws.send_json(subscribe)
|
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:
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
payload = msg.json()
|
payload = msg.json()
|
||||||
items = payload if isinstance(payload, list) else [payload]
|
items = payload if isinstance(payload, list) else [payload]
|
||||||
@ -103,6 +105,10 @@ async def clob_market_stream(
|
|||||||
on_event(item)
|
on_event(item)
|
||||||
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
||||||
break
|
break
|
||||||
|
elif msg.type == aiohttp.WSMsgType.CLOSE:
|
||||||
|
break
|
||||||
|
except (TimeoutError, asyncio.TimeoutError):
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
except Exception:
|
except Exception:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
|||||||
@ -494,11 +494,11 @@ def _health_dict(
|
|||||||
add(1, f"状态刷新偏慢 {state_age_ms} ms")
|
add(1, f"状态刷新偏慢 {state_age_ms} ms")
|
||||||
|
|
||||||
if rtds_lag_ms is None:
|
if rtds_lag_ms is None:
|
||||||
add(1, "RTDS 延迟未知")
|
add(1, "RTDS 源时间未知")
|
||||||
elif rtds_lag_ms > 30_000:
|
elif rtds_lag_ms > 180_000:
|
||||||
add(2, f"RTDS 延迟过高 {rtds_lag_ms} ms")
|
add(2, f"RTDS 源时间过旧 {rtds_lag_ms} ms")
|
||||||
elif rtds_lag_ms > 10_000:
|
elif rtds_lag_ms > 90_000:
|
||||||
add(1, f"RTDS 延迟偏高 {rtds_lag_ms} ms")
|
add(1, f"RTDS 源时间偏旧 {rtds_lag_ms} ms")
|
||||||
|
|
||||||
if clob_book_age_ms is None:
|
if clob_book_age_ms is None:
|
||||||
add(1, "CLOB 盘口尚未到达")
|
add(1, "CLOB 盘口尚未到达")
|
||||||
|
|||||||
@ -91,9 +91,17 @@ async def polymarket_chainlink_btc_stream(
|
|||||||
async def resilient_btc_price_stream(
|
async def resilient_btc_price_stream(
|
||||||
session: aiohttp.ClientSession,
|
session: aiohttp.ClientSession,
|
||||||
) -> AsyncIterator[PriceTick]:
|
) -> AsyncIterator[PriceTick]:
|
||||||
|
last_source_timestamp_ms: int | None = None
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async for tick in polymarket_chainlink_btc_stream(session):
|
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
|
yield tick
|
||||||
except Exception:
|
except Exception:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
@ -128,7 +136,11 @@ def _parse_chainlink_message(message: dict) -> list[PriceTick]:
|
|||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
if "data" in payload:
|
if "data" in payload:
|
||||||
ticks = []
|
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")
|
value = item.get("value")
|
||||||
if value is not None:
|
if value is not None:
|
||||||
ticks.append(
|
ticks.append(
|
||||||
|
|||||||
@ -172,7 +172,7 @@ function renderHealth(health) {
|
|||||||
els.healthText.textContent = health?.label || "--";
|
els.healthText.textContent = health?.label || "--";
|
||||||
els.healthDetail.textContent = health?.issues?.length
|
els.healthDetail.textContent = health?.issues?.length
|
||||||
? health.issues.join(" · ")
|
? health.issues.join(" · ")
|
||||||
: "RTDS / CLOB / DuckDB 正常";
|
: "接收 / CLOB / DuckDB 正常";
|
||||||
els.healthCard.className = `metric health-card ${status}`;
|
els.healthCard.className = `metric health-card ${status}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -95,7 +95,7 @@
|
|||||||
<dd id="recorderPath">--</dd>
|
<dd id="recorderPath">--</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>RTDS 延迟</dt>
|
<dt>RTDS 源年龄</dt>
|
||||||
<dd id="rtdsLag">--</dd>
|
<dd id="rtdsLag">--</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background:
|
background:
|
||||||
linear-gradient(90deg, rgba(20, 18, 14, 0.045) 1px, transparent 1px),
|
linear-gradient(90deg, rgba(20, 18, 14, 0.045) 1px, transparent 1px),
|
||||||
@ -452,14 +453,50 @@ dd {
|
|||||||
font-size: 20px;
|
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) {
|
@media (max-width: 980px) {
|
||||||
.ticker-grid,
|
|
||||||
.main-grid,
|
.main-grid,
|
||||||
.facts {
|
.facts {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ticker-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-card {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
.market-panel,
|
.market-panel,
|
||||||
|
.book-panel,
|
||||||
|
.edge-panel,
|
||||||
.log-panel,
|
.log-panel,
|
||||||
.replay-panel,
|
.replay-panel,
|
||||||
.paper-panel {
|
.paper-panel {
|
||||||
@ -467,7 +504,7 @@ dd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.paper-summary {
|
.paper-summary {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
@ -481,3 +518,200 @@ dd {
|
|||||||
gap: 3px;
|
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
|
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:
|
def test_parse_chainlink_rtds_ignores_other_symbols() -> None:
|
||||||
assert _parse_chainlink_message(
|
assert _parse_chainlink_message(
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user