在行情面板中加入 K 线:一次结构升级的实现过程
系列文章 · Demo#2
在上一篇《用 Ticker API 写一个行情面板:一次完整的实现过程》中,我用
REST Ticker + 定时刷新,完成了一个可以长期运行的行情展示面板。
那个版本解决的是"看一眼实时行情"的问题:无论是美股、港股,还是外汇、指数,只要通过统一的行情
API 拉取数据,就可以稳定展示当前价格、涨跌幅与波动区间。
但当这个面板真正跑起来之后,我很快意识到——它只能告诉我"现在",却无法回答"它是怎么走到这里的"。
对于一个真正可用的行情系统来说,无论是做股票分析、量化研究,还是单纯查看历史走势,K
线都是不可或缺的一部分。
这篇文章,就是在原有实时行情面板的基础上,加入 K
线数据,完成一次结构升级。
一、让列表给图表让位
Demo#1 上线后,我盯着那个行情列表看了很久。
它可以稳定展示多市场实时行情数据,包括美股、港股、外汇等不同品种。但每次我点开某个品种,都会下意识地想知道:
它这几天是怎么涨上来的?
于是我决定在原有结构上做一次升级,而不是新开一个页面。
我没有做路由跳转,而是选择让列表"让位"------点击某一行后,左侧收缩成产品列表,右侧展开
K 线详情。
二、我只是想把图画出来
一开始目标很简单:
把蜡烛图显示出来。
我直接去接 /kline 接口,然后丢进图表里。
真实接入行情 API 后,第一个报错是:
map is not a function
我才回去认真看了一遍官方文档
接口实际上是:
- /kline ------ 历史 K 线数据\
- /kline/latest ------ 当前周期实时更新
真正数组在 data.klines 里。
那一刻我意识到:
永远不要假设接口结构。
三、图表开始"反抗"我
排查后我发现:
- API 返回倒序数据\
- 数值是字符串\
- 图表只接受最小结构
K 线转换逻辑大概是这样的:- const rawKlines = result.data.klines
- const klineData = rawKlines.map(item => ({
- time: Math.floor(item.time / 1000), // 毫秒转秒
- open: parseFloat(item.open),
- high: parseFloat(item.high),
- low: parseFloat(item.low),
- close: parseFloat(item.close)
- }))
- klineData.sort((a, b) => a.time - b.time) // 升序排序
复制代码 这一段看起来简单,但它让我第一次真正理解:
图表库并不是黑盒,它要求的是"结构正确的时间序列"。
四、当价格动而图不动时
/kline 只返回已经完成的周期。
当前正在形成的这一根,在历史数据里是不存在的。
于是逻辑变成:- // 加载历史数据
- historyData = await fetchKLine(symbol, interval, 75)
- // 获取最新K线
- latestKline = await fetchLatestKLine(symbol, interval)
- if latestKline exists:
- existingIndex = historyData.findIndex(item => item.time === latestKline.time)
- if existingIndex >= 0:
- historyData[existingIndex] = latestKline // 更新现有K线
- else:
- historyData.push(latestKline) // 添加新K线
复制代码 从那一刻起,我开始理解:
行情系统的核心不是"画图",而是处理时间。实时行情是点,K 线是区间。
五、预加载策略的演进
最初的逻辑很简单:- chart.onScroll(() => {
- if scrollToLeft:
- fetchKLine(symbol, interval, 50)
- })
复制代码 能用,但滚动会卡顿。
后来我改成了"buffer 策略":- |------------------ 75 ------------------|
- |------ 50 visible ------|-- 25 buffer --|
- 当可视区域消耗掉一半 buffer 时 → 触发加载
复制代码 对应逻辑:- const KLINE_CONFIG = {
- initialLoad: 75,
- batchLoad: 25,
- triggerRatio: 0.5
- }
- // 首次加载
- fetchKLine(symbol, interval, KLINE_CONFIG.initialLoad)
- // 监听可见范围变化
- onVisibleRangeChange():
- leftBufferCount = visibleRange.from
- minBufferCount = batchLoad * triggerRatio
-
- if leftBufferCount < minBufferCount:
- preloadHistoricalData(symbol, interval, KLINE_CONFIG.batchLoad)
复制代码 这一刻我意识到:功能完成,不等于体验完成。
六、Resize 给我的提醒
有一次我关闭浏览器的开发者工具。
图表变宽了,但加载范围没变。
于是我开始用 ResizeObserver:- const resizeObserver = new ResizeObserver(entries => {
- if chartInstance && chartEl:
- // 保存当前可见范围
- currentRange = chartInstance.timeScale().getVisibleLogicalRange()
-
- // 更新图表尺寸
- chartInstance.applyOptions({
- width: chartEl.clientWidth,
- height: chartEl.clientHeight
- })
-
- // 恢复可见范围
- if currentRange:
- chartInstance.timeScale().setVisibleLogicalRange(currentRange)
-
- // 检查是否需要预加载
- loadMoreHistoricalData()
- })
复制代码 逻辑范围和视觉范围,是两回事。图变宽,单位时间内展示的 K 线数量会变化。
这一步让我第一次认真思考"视图驱动的数据加载"。
七、不是所有错误都值得被看到
我最终做了一个决定:- // 首次加载历史数据 - 失败显示错误
- async function fetchKLine(symbol, interval, limit, startTime, endTime, silent = false):
- try:
- // 加载数据
- ...
- catch error:
- if not silent:
- klineErrorByKey[key] = error.message // 显示错误
- updateKLineUI()
- else:
- console.warn('预加载失败(不影响显示)') // 静默失败
- // 预加载历史数据 - 使用静默模式
- preloadHistoricalData(symbol, interval, count):
- await fetchKLine(symbol, interval, count, startTime, endTime, true) // silent = true
- // 获取最新K线 - 静默失败
- fetchLatestKLine(symbol, interval):
- try:
- // 获取最新K线
- ...
- catch error:
- console.warn('获取最新K线失败(不影响显示)')
- return null
复制代码
- 首次加载是核心能力\
- 预加载是增强能力\
- latest 是增强能力
增强层失败,不应该影响主图。
八、它已经不像一个 Demo
当我开始处理这些边界时,我突然意识到:
我已经不再只是实现一个功能。
在 Demo #1 里,我解决的是实时行情快照。
在 Demo #2 里,我开始处理历史行情与时间结构。
但它还不够。
因为 K 线依然是静止的。
这就是现在的 Demo #2。
下一步,它应该动起来了。
源码与示例
完整Demo代码已开源:
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |