Files
easy_tdx_max/src/easy_tdx/web/routers/stream.py
T
GitHub 33d44ba5d6 feat(webui)!: 行情终端重大升级——市场看板/自选/个股与板块弹窗 + SSE 实时推送
展示层对标专业看盘终端(tick-stock-panel 模式),数据全部来自通达信协议直连:

- 市场看板:五大指数实时条(迷你分时)、涨跌统计、四维情绪雷达、
  全市场涨跌分布直方图(约 5500 只 22 桶、鼠标跟随浮窗)、涨停雷达、
  行业/概念热冷榜(可下钻)、涨幅/跌幅/成交额/换手四联排行榜、异动雷达
- 自选行情:6 位代码即加(symbol-info 自动取名)、SSE 实时全表刷新、
  行内迷你分时;SQLite 持久化(watchlist.db,幂等加删)
- 个股弹窗:五档盘口 + 1/3/5 日分时 + 日K(MA/BOLL/EMA + MACD/KDJ/RSI
  前端本地计算)+ 一键加自选 + 一键全策略寻优
- 板块弹窗:板块分时/日K + 成分股涨跌榜直达个股
- SSE 架构:QuoteStreamer 单循环轮询 fan-out(独立队列 + 背压丢旧、
  无订阅休眠、盘中 8s/盘外 60s 降频);前端 pinia 单连接指数退避重连
- fix(codec): SH 000 系大盘指数与 881/885 板块指数报价按 2 位小数解析
  (曾缩小 10 倍:科创50 1647.53→164.753);880 统计指数保持 3 位
- fix: 前端批量五档路径 /security/quotes→/quotes、SSE 五档白名单
  bid1_vol→bid_vol1、MAC 排行 close 列归一化、日K v-if 撑满、
  寻优查看跳转 /→/backtest、FastAPI 0.141 _IncludedRouter 测试适配

新增 27 个单测(streamer/自选/小数位语义),全套 1078 passed
2026-09-01 17:45:07 +08:00

63 lines
2.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""实时行情 SSE 路由。
``GET /api/v1/stream/quotes`` → ``text/event-stream``
- 事件 ``data`` 载荷:``{"type": "quotes_updated", "ts", "count", "quotes": [...]}``
(quotes 为指数 + 全部自选的快照,字段见 quote_streamer._QUOTE_FIELDS)。
- 每 15s 一条 SSE 注释行(``: keepalive``)防中间层掐空闲连接。
- 客户端断开由 ASGI cancel → generator finally 反注册队列。
零额外依赖:不用 sse-starletteStreamingResponse + asyncio.Queue 足够。
"""
from __future__ import annotations
import asyncio
import json
import logging
from fastapi import APIRouter, Request
from fastapi.responses import StreamingResponse
logger = logging.getLogger(__name__)
router = APIRouter(tags=["stream"])
_KEEPALIVE_SECONDS = 15.0
@router.get("/stream/quotes")
async def stream_quotes(request: Request) -> StreamingResponse:
"""订阅实时行情推送(指数 + 自选,快照式全量推送)。"""
streamer = getattr(request.app.state, "quote_streamer", None)
if streamer is None:
from fastapi import HTTPException
raise HTTPException(status_code=503, detail="行情推送服务未启动")
qid, queue = streamer.subscribe()
async def event_gen(): # type: ignore[no-untyped-def]
try:
# 首帧 hello:告诉前端连接可用 + 当前订阅规模
hello = {"type": "hello", "subscribers": streamer.subscriber_count}
yield f"data: {json.dumps(hello, ensure_ascii=False)}\n\n"
while True:
try:
msg = await asyncio.wait_for(queue.get(), timeout=_KEEPALIVE_SECONDS)
yield f"data: {json.dumps(msg, ensure_ascii=False)}\n\n"
except asyncio.TimeoutError:
yield ": keepalive\n\n"
finally:
streamer.unsubscribe(qid)
return StreamingResponse(
event_gen(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # nginx 反代时不缓冲
},
)