news 2026/11/3 18:05:17

从零搭建实时tick行情监控面板:架构、性能与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零搭建实时tick行情监控面板:架构、性能与避坑指南

tick-stock-panel这个项目,说白了就是我自己给自己做的“看盘台”。起因很简单:有一段时间做短线复盘和日内盯盘,需要同时关注几十只票的tick级成交情况,但当时用的行情App切来切去实在不方便,关键的大单往往就在切换页面的瞬间溜走了。于是我用浏览器搭了一个实时行情面板,把关注的股票成交明细、快照、大单异动全部聚合到一个窗口里。这篇文章把这个项目的完整链路写清楚,包括数据源选型、后端结构、前端渲染,以及实际跑起来之后踩过的几个坑。如果你正需要一个实时行情监控工具,或者想了解一下实时数据面板从零到一的搭建思路,可以参考这条路线。

1. 明知App就能看行情,我还是要自建面板的三个理由

先说清楚“tick数据”到底是什么。逐笔成交就是交易所撮合成交后产生的一笔一笔交易明细,一次成交就是一条tick。和分钟K线不同,tick级数据能看到每一笔成交的精确时间、成交价格、成交量、成交额,还能看出这一笔是主动买还是主动卖。对于想观察盘中细节的人而言,这个粒度是分时图和一分钟K线还原不出来的。

市场上有太多现成的看盘软件,手机端、PC端都有,那我为什么还非得自己折腾一个?三个原因:

第一个原因,多标的并行监控的需求,现成App满足得不好。手机上的行情软件,主界面通常一次只能聚焦一只股票,想要看另一只,必须退出当前页面再搜索。哪怕很多软件支持自选股列表,列表里也只有快照数据,比如最新价、涨跌幅、成交量,想看某一只的逐笔成交,还是得单独点进去。如果我持仓结构比较分散,比如同时看十只以上的票,靠手工切换页面基本盯不过来。自建面板的核心价值,就是把几十只票的明细、快照、预警放在同一屏上,扫一眼就有结论。

第二个原因,默认的视觉逻辑跟我的习惯不匹配。盘中盯逐笔,我最关心的是三件事:大单、主动买卖方向、价格异动。但普通软件的一列成交记录里,这些信息要么被密集的数字淹没了,要么需要自己拿眼睛去分辨。自己搭面板就可以重新定义“什么值得被强调”:单笔超过一定金额的直接整行高亮,主动买的暖色、主动卖的冷色,涨停和跌停价附近触碰时用背景色提示。这些东西在自建面板里就是一行CSS的事,在现成软件里却很难调整。

第三个原因,监控结果要能编程化使用。看盘软件做好看不难,难的是把行情和脚本、数据库、告警接起来。比如我想在某个票连续出现三笔大单主动买入时触发一个声音提醒,或者把每天盘中的关键成交写进本地文件做复盘,这些需求现成软件几乎不支持。自己写面板,行情流到了自己手里,怎么处理都是自由。我做tick-stock-panel的同时,顺手就把“大单异动日志”和“成交数据落盘”这两个需求一起实现了。

不过我也给自己划了一条明确的边界:这个面板只做行情监控和事后复盘,不做交易下单。下单涉及券商接口、风控、交易规则,和看盘是两套逻辑,混在一起只会让代码和心态都变复杂。面板的定位就是“看得更快、更准、更舒服”。

2. 行情源选型:延迟、字段、限频的一次次实测

做这类项目,第一个绕不开的问题就是:数据从哪来。我前前后后试过几条路,各有取舍,这里把这轮实测的结论直接放出来。

2.1 三类主流行情源的差异

第一类是基于HTTP的免费快照接口。这类接口在很多免费数据源里都能碰到,一次请求返回若干只股票的最新快照,字段一般包括最新价、涨跌幅、成交量、成交额、买卖五档。优点是接入简单,不需要维持长连接,写个轮询循环就行;缺点是延迟通常在1到3秒之间,而且绝大多数快照接口不提供逐笔成交,只有按分钟聚合的量。逐笔要想拿到,得靠另一类通道。

第二类是基于WebSocket的实时推送行情。这类通道建立连接后,服务器会持续推送最新成交和快照,延迟可以做到几百毫秒甚至更低,部分通道还提供真正的逐笔数据。缺点是免费通道的选择不多,而且连接稳定性、推送字段的完整性都得自己实测验证,不能拿文档当保证。

第三类是机构级的专业行情终端接口。数据质量确实最好,逐笔和十档盘口都有,但成本也高,个人开发者一般不会作为首选。我自己做的这个项目就不是为高频交易准备的,目标是“复盘够用、盯盘够快”,所以这一路当时只做了调研,没有实际接入。

2.2 我最终采用了“WS主力 + HTTP兜底”的双通道方案

实测下来,纯用HTTP轮询的问题是延迟波动大。行情平稳的时候大概一秒拉一次,勉强能看;行情剧烈波动的时候,接口响应变慢,等到数据回来,盘面已经往前走了好几笔。纯用WebSocket的问题则相反,连接偶尔断,断的时候如果没有任何兜底,面板就白屏了。

所以最终方案是让两条通道共存:正常运行时以WebSocket推送为主,面板上几乎所有tick都走这条路;同时保留一个低频的HTTP轮询,间隔设成3到5秒,只用来拉取自定义股票列表的快照做校准。两者之间用一个内存缓存层做衔接,谁的数据先到就用谁的,并记录数据到达的本地时间,前端可以根据这个时间判断行情新鲜度。

这套组合跑下来,效果比我预想中稳定。连续盯一个完整交易时段,WebSocket断线的次数并不多,断线后HTTP兜底能保证面板不处于“瞎”的状态,重连成功后又能马上切换回实时通道。对于个人项目来说,性价比是够的。

选型时还有一个容易忽略的点:限频。免费的HTTP快照接口不会要求你每秒只能请求多少次,但如果请求频率过高,很容易被临时限流,表现为突然返回空数据或直接超时。我建议轮询间隔至少控制在1秒以上,并且不要一次性请求全市场几千只票,只请求自选列表里的几十只,压力完全在可控范围内。如果你发现行情源频繁抽风,先别怪对方,回头看看自己的请求节奏是不是太快了。

2.3 统一内部Tick格式,屏蔽不同源的数据差异

不同行情源返回的字段名、单位、时间格式都不一样。为了不让上层代码跟着数据源走,我在后端定义了一个统一的内部Tick对象:

  • symbol:证券代码,统一带上交易所前缀,例如“SH600000”
  • ts:成交时间,精确到毫秒
  • price:成交价格,统一用浮点数
  • volume:成交量,统一用“股”作为单位
  • amount:成交金额,统一用“元”
  • side:主动方向,B代表主动买,S代表主动卖,T代表中性

行情接入层拿到原始数据后,第一件事就是转成这个结构。后续无论是写日志、做预警还是推给前端,都只认这个标准格式。这个抽象层虽然多写了一点代码,但好处是换数据源时完全不需要动核心逻辑,直接换适配器就行。

3. 后端管道:从秒级轮询改成实时推送的架构变化

后端我最初写得很糙,一个Python脚本,每秒请求一次接口,然后往前端输出JSON。当时想的很简单,能显示就行。结果数据量稍微上来就发现不对劲:页面要主动反复刷新,体验很差;而且轮询方式下,前端并不知道数据什么时候会变化,只能盲目地拉,浪费带宽和计算。

3.1 三层结构:接入层、分发层、查询层

后来重构成三个模块:

接入层负责对接各个行情源,每个行情源一个适配器。适配器只做一件事:把原始数据解析成内部Tick对象,然后交给核心层。HTTP轮询适配器内部是一个asyncio循环,按照配置好的间隔不断拉快照;WebSocket适配器内部是一个长连接循环,收到推送消息后逐条解析。

分发层是核心,维护一张订阅表,记录每只股票和当前所有前端连接的关系。每当接入层传来一条新的Tick,分发层做三件事:更新该股票的最新快照缓存、推送给所有订阅了这只股票的前端连接、写入本地日志(如果开启了落盘)。分发层还负责心跳检查,定期清理失活的连接。

查询层面向外部提供两类入口:一是REST接口,用于前端打开页面时拉一次全量快照;二是WebSocket入口,用于后续所有实时增量推送。

为什么选asyncio而不是多线程?因为场景几乎是纯IO密集型的。行情推送、前端连接、数据库写入,没有CPU密集计算,用协程完全够。实测下来,同时维护几十个前端连接和处理上千条tick的推送,进程内存只占几十到一两百MB,CPU占用基本可以忽略。

3.2 订阅与分发的核心代码骨架

这里分享的是最精简的订阅分发逻辑,其他细节比如日志、预警都是挂在同样的事件回调上。

# core/bus.py —— 一个极简的Tick分发总线 import asyncio from collections import defaultdict class TickBus: def __init__(self): self.clients = defaultdict(set) # symbol -> list[websocket] self.cache = {} # symbol -> latest tick dict async def subscribe(self, ws, symbols): for sym in symbols: self.clients[sym].add(ws) # 订阅成功后立即把缓存快照推过去 if sym in self.cache: await ws.send_json({"snapshot": self.cache[sym]}) def unsubscribe(self, ws, symbols): for sym in symbols: self.clients[sym].discard(ws) async def publish(self, tick): # 更新缓存 self.cache[tick["symbol"]] = tick # 推送给所有订阅该标的的连接 for ws in list(self.clients.get(tick["symbol"], ())): try: await ws.send_json({"tick": tick}) except Exception: # 连接已经失效,交给外层统一清理 await self.unsubscribe(ws, [tick["symbol"]])

这里有一个很容易踩的细节:ws.send_json如果连接断开会抛异常,但异常处理不能只在这里做,因为并发场景下,同一批连接可能同时在分发层和清理协程里被操作。我的做法是,分发层只做推送尝试,真正的清理统一由一个巡检协程兜底,避免在发送过程中反复删改订阅表导致状态不一致。

3.3 数据落盘与回放的基础设计

逐笔数据转瞬即逝,但复盘时又特别希望“时光倒流”。我给面板加了一个可选的落盘功能:订阅里开会把每一条Tick写入SQLite,表结构就是内部Tick字段加一个本地接收时间。因为是纯追加写,速度很快,实测一个交易时段下来,单标的几千条记录,整个数据库文件不到几MB。落盘时不需要保证100%不丢失,盘后写个脚本把当天数据汇总成CSV就够了。

回放功能的实现也偷了点巧:把落盘数据读出来,用定时器按原始时间间隔逐条重新推给前端。前端完全感知不到“这是历史数据”还是“实时数据”,只是速度慢了一些。这样复盘时就能用同一套界面,回看当天盘中每一个特殊节点。

3.4 背压和数据过期策略

WebSocket推送出去时,如果某个前端连接因为网络原因迟迟收不到消息,消息队列会越积越多。我的处理是:队列只保留最近100条,超过的部分直接丢弃。前端断线重连后,会先通过REST接口拉一次全量快照,把当前价格和最新成交一次性补回来,不会因为丢失几秒的tick而产生数据空洞。

丢弃老数据的思路在做行情系统时很重要。行情数据是强时效性的,晚到的一秒数据,价值基本为零。与其让积压数据把内存和带宽拖垮,不如让前端快速重新对齐。这里的取舍和“丢消息会出错”的直觉相反,但实时系统里“及时一致性”优先于“最终一致性”。

4. 前端排版:让逐笔成交信息一眼能读,而不是数字堆在屏幕上

后端把数据管好后,真正决定这个面板好不好用的,其实是前端。我的核心诉求只有一个:长时间盯盘不累,扫一眼就能知道当前市场在发生什么。页面设计围绕这个目标拆成了几个模块。

4.1 面板布局与信息层级

顶部是一个全局状态栏,显示当前时间、WebSocket连接状态、最后一条数据到达时间、总延迟毫秒数。只要看到“最后数据到达时间”超过3秒,第一反应就是去看连接状态,而不是怀疑数据出了问题。

左侧是自选股列表,每一行显示代码、名称、最新价、涨跌幅、当前成交量。点击任意一行,中间主区域就会切换到该标的的详情视图。这里的列表和中间详情区是联动关系,切换标的时,前端会先向REST接口发一个快照请求,然后转成WS订阅,保证切换后马上有数据。

中间主区域分上下两块。上方是1分钟K线图,用tick数据本地聚合出来的K线比外部接口的分时数据更灵活;下方是逐笔成交滚动表格,这是整个面板信息密度最高的地方。右侧是一个事件日志区,预警信息和手动标记的大单都会在这里滚动显示。

4.2 颜色、高亮与信息密度控制

颜色按A股习惯,红涨绿跌。tick的方向字段直接影响数据的呈现:主动买用暖色,主动卖用冷色,这笔成交是主力主动吃卖单还是主动性抛售,一眼就能区分。

大单高亮是我最看重的功能。我在前端定义一个“大单阈值”参数,比如单笔成交金额超过50万元就算大单。超过阈值的tick整行加粗,并用更亮的背景色标记;超过100万元的再上一档,用更强的底色和加粗字体。加入这个规则后,盯盘时注意力就可以集中在真正重要的成交上,而不是被密密麻麻的小单淹没。

价格异动也有对应的视觉反馈:最新价突破当日最高价、触及涨停价、或者在极短时间内连续跳动超过设定幅度时,对应区域会有一次闪烁。闪烁用CSS动画实现,持续0.5秒后消失,不会造成持续干扰。

关于信息密度,我的建议是宁少勿多。逐笔成交表每一行只显示四列:时间、价格、成交量、方向。单位统一,数字右对齐,不需要的列一律不展示。当窗口中可以容纳的信息变少,大脑的负担反而会降下来。很多人做监控面板的误区是想把所有数据都塞进去,结果什么都看不清。

4.3 渲染性能:为什么不能用一条条DOM堆积

面板的逐笔成交更新频率,在行情活跃时每秒可能来几十条甚至上百条。如果每来一条都直接操作一次DOM,用不了多久页面就会卡顿。我踩过这个坑:最初直接用一个数组往表格里append新行,大约跑到几千条之后,滑动和输入都已经明显掉帧。

后来的解法是批量更新加虚拟滚动。WebSocket收到消息后,先把tick推进一个待处理数组,下一次浏览器requestAnimationFrame回调时,一次性把所有待处理数据追加到表格里。这样渲染频率和刷新率对齐,避免了每条数据都触发一次重排。同时,表格只渲染可视区域内的行,数据总量哪怕有上万行,DOM数量始终保持在几十到几百个。

// 批量追加tick的示意 let pendingTicks = []; let schedulerRunning = false; socket.onmessage = (e) => { const data = JSON.parse(e.data); if (data.tick) { pendingTicks.push(data.tick); } if (!schedulerRunning) { schedulerRunning = true; requestAnimationFrame(() => { appendTicks(pendingTicks); pendingTicks = []; schedulerRunning = false; }); } };

这个优化看起来很小,但对长时间运行的页面影响非常大。优化完成后,即使持续跑一整个交易日,内存也不会因为DOM数量膨胀而失控。

5. 上线两天就暴露的四个坑,以及现在的处理方式

任何实时系统都要面对同一个残忍事实:上线前你觉得一切完美,上线后所有隐藏问题开始排队出现。面板跑了两天,我先后遇到下面四个问题。

5.1 连接断流后的重连风暴

第一次遇到WebSocket断开时,我没想太多,直接让前端自动重连。结果服务器端恢复后,前端十几个页面同时发起重连请求,又被行情源当成突发访问限制了。这就是重连风暴:所有客户端在同一时间点涌向服务端,本来是为了恢复可用性,结果反而把服务压垮。

解决方案是指数退避加随机抖动。每次重连前等待的时间按照“1秒、2秒、4秒、8秒……”递增,并且加入一个0到1秒的随机偏移,避免所有连接排在同一时刻重连。同时在后端设置一个“断线护栏”,如果重连失败超过五次,就进入冷静期,只保留HTTP兜底轮询,不再尝试建立WebSocket连接。

5.2 时间戳错乱:跨日和延迟数据

这个问题一开始让我排查了好久。行情数据里的时间戳,有些源给的是无时区时间,有些给的是毫秒级Unix时间,有的干脆只有时分秒没有日期。盘中看着还好,但跨过午盘休市或者隔夜后,数据排序就乱了。

后续统一了两套时间:一是数据源自带的成交时间,二是本地接收到消息的时间,两者都记录到Tick对象里。前端排序一律用本地接收时间,成交时间只作为展示字段。当“本地接收时间”和“成交时间”的差值超过5秒时,前端会在该条数据上打一个“延迟”标记,显示为灰色。这样即使数据源本身有点问题,面板上也不会出现时间倒退或数据跳变的现象。

5.3 停牌、除权除息、新股的边界状态

停牌的票没有成交,这是最简单的边界。复杂的是除权除息和上市首日这两个场景。除权除息当天,股价开盘价会有一个向下跳空,如果还按照前一天的收盘价算涨跌幅,数据就是错的。上市首日就更特殊了,没有昨收基准,涨跌幅计算规则和正常交易日完全不同。

我的处理方式是维护一个“基本面状态”字段:正常、停牌、除权日、上市首日。状态由每日盘前通过快照接口预取一次,盘中再根据快照字段做二次修正。前端展示涨跌幅时,会根据这个状态选择对应的基准价。这个小状态字段看起来不起眼,但极大地避免了复盘时被假信号误导。

5.4 浏览器后台标签页的节流陷阱

面板如果在电脑上开成一个小窗口,顺手去干别的事,把标签切到后台,浏览器会定时器节流,setInterval和requestAnimationFrame在后台都被拉长到一秒一次甚至更长。这个对纯前端渲染影响不大,但会影响预警提示的及时性,明明触发了一个异动提醒,声音却延迟了好几秒。

解决思路是把预警判断放到后端,而不是依赖前端定时器。后端自己就能扫Tick流是否符合预警条件,符合就直接推送一条alert事件到前端。前端只负责接收和展示,不负责判断。这样即使标签页在后台,WebSocket消息依然会正常到达,声音提醒也能准点触发。

6. 下一步:预警、复盘、多市场,这个面板还能继续长大

目前面板已经能稳定完成实时监控和复盘回放。我自己最想继续做的几个方向,也顺手说一下,每个方向都可以独立扩展,不用推翻重来。

预警引擎是优先级最高的。现在我只做了一个简单的大单金额阈值判断,后续想扩展成可配置规则:比如“连续三笔大单主动买”“5分钟内涨幅超过2%”“跌破当日开盘价后快速拉起”这类条件。规则用Python脚本描述,挂在分发层的事件回调和数据聚合器上,命中后推给前端并触发声音提醒。声音直接用Web Audio API生成一个短促的提示音,不用额外引音频文件,浏览器兼容性也好。

复盘回放的数据层已经就绪,剩下的是交互打磨。比如增加回放速度调节、按时间区间跳转、在K线图上标记大单位置。这块做完之后,盘后研究就会比盘中盯盘更高效。

多市场支持是我留给未来的扩展点。A股、港股、美股、期货、数字货币的交易规则和行情粒度差异很大,不能用一个适配器硬撑。内部已经在Tick对象的symbol字段上加了市场前缀,后续每个市场写一个独立适配器,接入层多注册一个源就行。不同市场的颜色习惯也要做成配置项,比如美股是绿涨红跌,不能和A股混用。

最后再分享一个我实际用下来的心得:做这类实时数据面板,最重要的不是功能多,而是“刷新链路短”和“状态可感知”。链路短,指的是从行情源到眼睛之间的距离要尽可能短,中间不要有多余的轮询、缓存、重复渲染;状态可感知,指的是任何时候都得知道面板的行情是新鲜的还是过期的、连接是正常的还是断开的。这两点做到了,即使界面上只有几列最简单的数字,这个面板也是好用的。tick-stock-panel这个名字里的“panel”,对我来说就是一个随时能瞄一眼、能信任的实时窗口。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 18:03:28

深度学习在服装推荐系统中的应用与实践

1. 项目背景与核心价值服装推荐系统是近年来电商平台和时尚科技领域的热门研究方向。传统基于规则的推荐方式往往存在个性化程度低、更新滞后等问题。这个毕业设计项目通过引入深度学习技术,构建了一套能够自动学习用户偏好与服装特征的智能推荐系统。我在实际开发中…

作者头像 李华
网站建设 2026/11/3 18:02:23

C语言数据结构选择与查找算法优化实践

1. 题目背景与需求解析这道PTA(程序设计能力考试)题目考察的是C语言基础编程能力,属于典型的"信息查询"类问题。题目要求我们根据输入的考生信息,在给定准考证号时快速输出对应的考试座位号。在实际考试系统中&#xff…

作者头像 李华
网站建设 2026/11/3 18:02:14

MySQL分页查询中的幽灵数据问题分析与解决方案

1. 问题现象与初步排查最近在开发后台管理系统时,遇到了一个诡异的问题:分页查询时明明数据库里有数据,但接口返回的结果集却显示为空。这个问题在测试环境偶发出现,到了生产环境频率明显升高。作为一名有五年经验的Java后端开发者…

作者头像 李华
网站建设 2026/11/3 18:02:12

STM32 RTC万年历实战:VBAT供电、LSE起振与备份寄存器全链路解析

简介:本资源是一份基于STM32的电子万年历嵌入式项目源代码包,面向嵌入式初学者、单片机课程设计学生及STM32开发爱好者,解决RTC实时时钟模块配置、低功耗时间管理与人机交互显示等核心实践问题,适用于智能家居、教学实验与个人DIY…

作者头像 李华
网站建设 2026/11/3 18:00:36

豆包智能体聊天记录手动备份完整方案:从数据抓取到结构化存储

1. 为什么我要手动备份豆包智能体的聊天数据先说说我自己的情况。我在豆包上搭了三个智能体,一个用来做技术问答知识库,一个用来跑销售话术演练,还有一个是给团队新人做入职培训的。用了大概四个月,积累的对话记录少说也有几千条。…

作者头像 李华
网站建设 2026/11/3 17:59:13

动态因果推理:神经微分方程与图网络的融合应用

1. 项目背景与核心问题在传统因果推理研究中,我们常常假设因果机制是静态不变的。就像小时候玩的弹珠游戏,弹珠撞击的角度和力度决定了它的运动轨迹——这种因果关系被认为是固定不变的。但现实世界要复杂得多,就像观察一条河流的流动&#x…

作者头像 李华