找回密码
 立即注册
首页 业界区 业界 在行情面板中加入 K 线:一次结构升级的实现过程 ...

在行情面板中加入 K 线:一次结构升级的实现过程

敖可 4 天前
在行情面板中加入 K 线:一次结构升级的实现过程

系列文章 · Demo#2
在上一篇《用 Ticker API 写一个行情面板:一次完整的实现过程》中,我用
REST Ticker + 定时刷新,完成了一个可以长期运行的行情展示面板。
那个版本解决的是"看一眼实时行情"的问题:无论是美股、港股,还是外汇、指数,只要通过统一的行情
API 拉取数据,就可以稳定展示当前价格、涨跌幅与波动区间。
但当这个面板真正跑起来之后,我很快意识到——它只能告诉我"现在",却无法回答"它是怎么走到这里的"。
对于一个真正可用的行情系统来说,无论是做股票分析、量化研究,还是单纯查看历史走势,K
线都是不可或缺的一部分。
这篇文章,就是在原有实时行情面板的基础上,加入 K
线数据,完成一次结构升级。
一、让列表给图表让位

Demo#1 上线后,我盯着那个行情列表看了很久。
它可以稳定展示多市场实时行情数据,包括美股、港股、外汇等不同品种。但每次我点开某个品种,都会下意识地想知道:
它这几天是怎么涨上来的?
于是我决定在原有结构上做一次升级,而不是新开一个页面。
我没有做路由跳转,而是选择让列表"让位"------点击某一行后,左侧收缩成产品列表,右侧展开
K 线详情。
1.png

二、我只是想把图画出来

一开始目标很简单:
把蜡烛图显示出来。
我直接去接 /kline 接口,然后丢进图表里。
真实接入行情 API 后,第一个报错是:
map is not a function
我才回去认真看了一遍官方文档
接口实际上是:

  • /kline ------ 历史 K 线数据\
  • /kline/latest ------ 当前周期实时更新
真正数组在 data.klines 里。
那一刻我意识到:
永远不要假设接口结构。
三、图表开始"反抗"我

排查后我发现:

  • API 返回倒序数据\
  • 数值是字符串\
  • 图表只接受最小结构
K 线转换逻辑大概是这样的:
  1. const rawKlines = result.data.klines
  2. const klineData = rawKlines.map(item => ({
  3.     time: Math.floor(item.time / 1000),  // 毫秒转秒
  4.     open: parseFloat(item.open),
  5.     high: parseFloat(item.high),
  6.     low: parseFloat(item.low),
  7.     close: parseFloat(item.close)
  8. }))
  9. klineData.sort((a, b) => a.time - b.time)  // 升序排序
复制代码
这一段看起来简单,但它让我第一次真正理解:
图表库并不是黑盒,它要求的是"结构正确的时间序列"。
四、当价格动而图不动时

/kline 只返回已经完成的周期。
当前正在形成的这一根,在历史数据里是不存在的。
于是逻辑变成:
  1. // 加载历史数据
  2. historyData = await fetchKLine(symbol, interval, 75)
  3. // 获取最新K线
  4. latestKline = await fetchLatestKLine(symbol, interval)
  5. if latestKline exists:
  6.     existingIndex = historyData.findIndex(item => item.time === latestKline.time)
  7.     if existingIndex >= 0:
  8.         historyData[existingIndex] = latestKline  // 更新现有K线
  9.     else:
  10.         historyData.push(latestKline)  // 添加新K线
复制代码
从那一刻起,我开始理解:
行情系统的核心不是"画图",而是处理时间。实时行情是点,K 线是区间。
五、预加载策略的演进

最初的逻辑很简单:
  1. chart.onScroll(() => {
  2.     if scrollToLeft:
  3.         fetchKLine(symbol, interval, 50)
  4. })
复制代码
能用,但滚动会卡顿。
后来我改成了"buffer 策略":
  1. |------------------ 75 ------------------|
  2. |------ 50 visible ------|-- 25 buffer --|
  3. 当可视区域消耗掉一半 buffer 时 → 触发加载
复制代码
对应逻辑:
  1. const KLINE_CONFIG = {
  2.     initialLoad: 75,
  3.     batchLoad: 25,
  4.     triggerRatio: 0.5
  5. }
  6. // 首次加载
  7. fetchKLine(symbol, interval, KLINE_CONFIG.initialLoad)
  8. // 监听可见范围变化
  9. onVisibleRangeChange():
  10.     leftBufferCount = visibleRange.from
  11.     minBufferCount = batchLoad * triggerRatio
  12.    
  13.     if leftBufferCount < minBufferCount:
  14.         preloadHistoricalData(symbol, interval, KLINE_CONFIG.batchLoad)
复制代码
这一刻我意识到:功能完成,不等于体验完成。
六、Resize 给我的提醒

有一次我关闭浏览器的开发者工具。
图表变宽了,但加载范围没变。
于是我开始用 ResizeObserver:
  1. const resizeObserver = new ResizeObserver(entries => {
  2.     if chartInstance && chartEl:
  3.         // 保存当前可见范围
  4.         currentRange = chartInstance.timeScale().getVisibleLogicalRange()
  5.         
  6.         // 更新图表尺寸
  7.         chartInstance.applyOptions({
  8.             width: chartEl.clientWidth,
  9.             height: chartEl.clientHeight
  10.         })
  11.         
  12.         // 恢复可见范围
  13.         if currentRange:
  14.             chartInstance.timeScale().setVisibleLogicalRange(currentRange)
  15.             
  16.             // 检查是否需要预加载
  17.             loadMoreHistoricalData()
  18. })
复制代码
逻辑范围和视觉范围,是两回事。图变宽,单位时间内展示的 K 线数量会变化。
这一步让我第一次认真思考"视图驱动的数据加载"。
七、不是所有错误都值得被看到

我最终做了一个决定:
  1. // 首次加载历史数据 - 失败显示错误
  2. async function fetchKLine(symbol, interval, limit, startTime, endTime, silent = false):
  3.     try:
  4.         // 加载数据
  5.         ...
  6.     catch error:
  7.         if not silent:
  8.             klineErrorByKey[key] = error.message  // 显示错误
  9.             updateKLineUI()
  10.         else:
  11.             console.warn('预加载失败(不影响显示)')  // 静默失败
  12. // 预加载历史数据 - 使用静默模式
  13. preloadHistoricalData(symbol, interval, count):
  14.     await fetchKLine(symbol, interval, count, startTime, endTime, true)  // silent = true
  15. // 获取最新K线 - 静默失败
  16. fetchLatestKLine(symbol, interval):
  17.     try:
  18.         // 获取最新K线
  19.         ...
  20.     catch error:
  21.         console.warn('获取最新K线失败(不影响显示)')
  22.         return null
复制代码

  • 首次加载是核心能力\
  • 预加载是增强能力\
  • latest 是增强能力
增强层失败,不应该影响主图。
八、它已经不像一个 Demo

当我开始处理这些边界时,我突然意识到:
我已经不再只是实现一个功能。
在 Demo #1 里,我解决的是实时行情快照。
在 Demo #2 里,我开始处理历史行情与时间结构。
但它还不够。
因为 K 线依然是静止的。
2.gif

这就是现在的 Demo #2。
下一步,它应该动起来了。
源码与示例

完整Demo代码已开源:


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册