feat: push chart tail candles over SSE for faster market refresh

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-08 12:24:25 +08:00
parent 4918699276
commit e68e29629e
4 changed files with 141 additions and 49 deletions
+25 -4
View File
@@ -1,4 +1,4 @@
"""行情区 K 线:后台轮询订阅 + SSE 版本通知(对齐监控区 board)。"""
"""行情区 K 线:后台轮询订阅 + SSE 推送尾部 K 线(对齐监控区 board)。"""
from __future__ import annotations
@@ -17,6 +17,7 @@ HUB_CHART_SSE_HEARTBEAT_SEC = float(os.getenv("HUB_CHART_SSE_HEARTBEAT_SEC", "25
HUB_CHART_WATCH_TTL_SEC = float(os.getenv("HUB_CHART_WATCH_TTL_SEC", "45"))
HUB_CHART_POSITION_TIMEFRAME = (os.getenv("HUB_CHART_POSITION_TIMEFRAME", "5m") or "5m").strip()
HUB_CHART_MAX_SERIES_PER_TICK = max(1, int(os.getenv("HUB_CHART_MAX_SERIES_PER_TICK", "24")))
HUB_CHART_SSE_TAIL_BARS = max(5, min(int(os.getenv("HUB_CHART_SSE_TAIL_BARS", "30")), 120))
PollFn = Callable[[], Awaitable[dict[str, Any]]]
@@ -56,6 +57,7 @@ class ChartPollStore:
self._watch_until: dict[str, float] = {}
self._position_keys: set[str] = set()
self._series: dict[str, SeriesState] = {}
self._push_tails: dict[str, dict[str, Any]] = {}
self._subscribers: list[asyncio.Queue[str | None]] = []
self._task: asyncio.Task | None = None
self._stop = asyncio.Event()
@@ -134,8 +136,8 @@ class ChartPollStore:
}
return out
def event_dict(self) -> dict[str, Any]:
return {
def event_dict(self, *, tails: dict[str, dict[str, Any]] | None = None) -> dict[str, Any]:
out: dict[str, Any] = {
"chart_version": self.version,
"updated_at": self.updated_at,
"polling": self.polling,
@@ -144,7 +146,12 @@ class ChartPollStore:
"series": self.series_event_dict(),
"poll_interval_sec": HUB_CHART_POLL_INTERVAL,
"position_timeframe": HUB_CHART_POSITION_TIMEFRAME,
"push_tails": True,
}
tail_map = tails if tails is not None else self._push_tails
if tail_map:
out["tails"] = tail_map
return out
def series_version(self, exchange_key: str, symbol: str, timeframe: str) -> int:
key = series_key(exchange_key, symbol, timeframe)
@@ -199,6 +206,8 @@ class ChartPollStore:
ok: bool,
fetched: int = 0,
error: str | None = None,
candles: list[dict[str, Any]] | None = None,
price_tick: Any = None,
) -> None:
key = series_key(exchange_key, symbol, timeframe)
st = self._series.setdefault(key, SeriesState())
@@ -206,10 +215,22 @@ class ChartPollStore:
st.updated_at = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
st.fetched = int(fetched or 0)
st.error = error if not ok else None
if ok and candles:
tail = list(candles[-HUB_CHART_SSE_TAIL_BARS :])
if tail:
self._push_tails[key] = {
"series_version": st.version,
"updated_at": st.updated_at,
"fetched": st.fetched,
"candles": tail,
"price_tick": price_tick,
}
def _broadcast(self, *, close: bool = False) -> None:
dead: list[asyncio.Queue[str | None]] = []
payload = None if close else json.dumps(self.event_dict(), ensure_ascii=False)
tails_snap = dict(self._push_tails)
self._push_tails.clear()
payload = None if close else json.dumps(self.event_dict(tails=tails_snap), ensure_ascii=False)
for q in self._subscribers:
try:
q.put_nowait(payload)