feat: push chart tail candles over SSE for faster market refresh
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user