简介:面向微信小程序股票行情开发场景的 HQChart 源码包,适合需要实现沪深/港股 K 线与走势图的前端开发者、量化爱好者及项目实践者参考。它将行情图表的页面组织、数据转换、交互手势与通达信语法指标解析整合到可运行的小程序工程中,便于对照理解从数据获取到图表渲染的完整链路。压缩包共 72 个文件、约 1.14MB,以 21 个脚本、13 个示意图、9 个页面、7 个样式文件及若干配置与模板文件为主,分别承担逻辑实现、界面样式、页面结构与素材展示,目录中包含分时图、历史 K 线、成交明细等模块用例。已有 1081 人学习,说明其在股票图表小程序实践中有一定参考面。读者可重点研读行情模块的组织方式、图表绘制与缩放平移处理、指标公式计算及微信端性能优化思路,并借助示例页面与样式资源快速搭建自己的行情看板原型。
1. 拿一个行情图当轮子用:微信小程序里画沪深港 K 线的现实解法
做微信小程序行情页的人,多半都会遇到同一个问题:用 canvas 从零画 K 线,先把蜡烛体、影线、坐标轴、刻度画出来,再补拖动、双指缩放、长按十字光标、主图和副图联动,最后还要自己算 MA、MACD、BOLL 并保证和主图对齐——一个页面能占掉两周排期,真机一测还卡。这个压缩包里的 HQChart 就是用来解决这件事的:一套纯 JavaScript 的金融图表引擎,把 K 线图、分时走势图、技术指标、画线工具封装好,并且带了微信小程序的运行入口,沪深 A 股和港股都能配置。它适合两类人:被排期推着要在小程序里出行情图的业务开发,以及不想长期维护自研图表库、只希望换掉数据源就能上线的前端。下面按结构、跑通、指标、避坑、改造的顺序往下拆。
2. 项目结构拆解:从压缩包到小程序页面,文件是怎么分层的
2.1 解压后先认文件:四类产物分别管什么
压缩包解开,根目录大致是下面这个形态(不同分支的文件名可能略有出入,认职责就行):
HQChart-master/ ├── hqchart.core.js # 未压缩核心,带注释,排查问题就啃它 ├── hqchart.min.js # 压缩版,web 端 script 直引,小程序不要用 ├── umychart/ # 跨端/小程序入口产物 │ └── umychart.wx.js # 小程序封装,剔除了 document、window 依赖 ├── webhqchart/ # web 端示例与配置演示页 ├── testcase/ # 各种配置项的用例集合 └── README.md这张表建议贴在工程 README 里,团队里换人接手时能少问很多问题:
| 文件/目录 | 职责 | 什么时候用 |
|---|---|---|
| hqchart.core.js | 完整引擎,含指标解析、绘制、交互 | 小程序首选,方便断点调试 |
| hqchart.min.js | 同上的压缩产物 | 只用于 web 端减小体积 |
| umychart.wx.js | 针对小程序环境的适配层 | 小程序实际挂载入口 |
| webhqchart / testcase | 配置样例 | 改配色、改窗口布局时对照 |
有个容易被忽略的点:hqchart.core.js和umychart.wx.js不是二选一的关系,前者是引擎本体,后者是把引擎接到小程序 canvas 上的适配代码。我一般两个都放进工程,适配层出问题就翻适配层,画得不对就翻引擎。别图省事只扔一个 min 版进去,出了渲染问题你连堆栈都看不懂。
2.2 小程序端的落地路径:把浏览器 canvas 搬到 wx canvas
HQChart 的原始形态是给浏览器写的,直接扔进小程序会有几类硬冲突:没有document和window,canvas.getContext的取法不同,图片加载要走canvas.createImage(),requestAnimationFrame要换成canvas.requestAnimationFrame或定时器,字体测量拿不到measureText的完整度量。适配层解决的就是这些。
我在自己工程里通常这样放:
miniprogram/ ├── pages/quote/ │ ├── quote.wxml │ ├── quote.js │ ├── quote.wxss │ └── quote.json ├── libs/hqchart/ │ ├── hqchart.core.js │ └── umychart.wx.js ├── services/ │ └── quote-data.js # 数据适配层,后面细讲 └── app.jsoncanvas 节点的初始化是整条链路的第一个关键步骤,也是最容易出问题的地方:
// pages/quote/quote.js Page({ data: { canvasId: 'kline' }, onReady() { // 必须等 onReady,onLoad 时节点还没渲染,select 拿不到 node wx.createSelectorQuery().in(this) .select('#kline') .fields({ node: true, size: true }) .exec((res) => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = wx.getWindowInfo().pixelRatio; // 物理像素与逻辑像素分离,不乘 dpr 在真机上线条会糊 canvas.width = res[0].width * dpr; canvas.height = res[0].height * dpr; ctx.scale(dpr, dpr); // 把画布尺寸、上下文交给适配层,函数名以你下载版本的导出为准 this.chart = this.initChart({ canvas, ctx, width: res[0].width, height: res[0].height, dpr }); }); } });三个参数值得说清楚。node是 canvas 节点本身,引擎内部加载图片、创建离屏画布都要用它,只给ctx不够。dpr是设备像素比,iOS 常见 2 或 3,安卓从 1.5 到 3.5 都有;不处理会让十字光标和高亮线的位置偏移,属于典型的“看着能跑、细看全歪”的玄学问题。width/height用res[0].width而不是canvas.width,因为后者已经被你乘过 dpr 了。
2.3 数据层与渲染层的分界:一个适配器接口
架构上,HQChart 只做两件事:把数据画成图,把用户手势翻译成图表状态。数据从哪来、字段叫什么,全由业务方提供。这条边界如果不提前划好,后期换数据源会非常麻烦。我一般先定一个固定形状的适配器:
// services/quote-data.js // 图表引擎只认这几个钩子,底下接哪家数据源都能换 const quoteData = { // 1. 历史 K 线:日/周/月/分钟都走这里,靠 period 区分 requestHistoryKLine({ symbol, period, count, endDate }) {}, // 2. 分时走势:当日或最近 N 日,返回均价线要用的 avprice requestMinuteLine({ symbol, dayCount }) {}, // 3. 实时快照:当前价、涨跌幅,刷新标题栏用,不参与画图 requestRealtime({ symbol }) {}, // 4. 代码表:搜索结果、自选列表,切股票时用 requestSymbolList({ keyword, market }) {} }; module.exports = quoteData;四个钩子对应四种请求频率:历史 K 线低频、翻页才拉;分时走势按日拉;实时快照高频轮询;代码表基本是本地缓存。把它们拆开的好处是缓存策略可以各自设计,翻 K 线时不会因为快照轮询把已经拉过的历史数据冲掉。
3. 画出第一根 K 线:canvas 挂载与行情数据适配的完整链路
3.1 页面骨架:wxml 结构与画布尺寸约束
页面结构不需要复杂,但 canvas 所在的容器必须给明确高度,这是新手最常犯的错误:
<view class="quote-page"> <view class="quote-header"> <text class="stock-name">{{stockName}}</text> <text class="stock-price">{{price}}</text> </view> <view class="period-bar"> <view wx:for="{{periods}}" wx:key="key" >/* quote.wxss */ .quote-page { display: flex; flex-direction: column; height: 100vh; } .kline-canvas { flex: 1; width: 100%; height: 0; } /* height:0 + flex:1 是最稳的写法 */.kline-canvas如果只写flex:1不写height:0,在某些安卓机上 canvas 会撑到父容器之外,你拿到的size.height是错的,画出来只有一条线。这个坑值得记进项目 README。
3.2 数据接进去:字段约定和单位统一
引擎对数据格式是有硬要求的:数组必须从旧到新升序,字段名固定,少一个yclose整张图的涨跌颜色就全反了。我用一个转换函数把业务接口的返回映射过去:
// services/normalize.js // 把业务接口的字段翻译成引擎要的字段,一处改,全局生效 function toKLineData(rawList) { return rawList .map((it) => ({ date: Number(String(it.tradeDate).replace(/-/g, '')), // 20240105,不能带横杠 yclose: it.prevClose, // 昨收,涨跌幅和阴阳线判断都靠它 open: it.open, high: it.high, low: it.low, close: it.close, vol: it.volume, // 单位统一成「手」,别和接口返回的「股」混用 amount: it.amount })) .sort((a, b) => a.date - b.date); // 接口偶尔倒序返回,必须兜底排序 }date用数字而不是字符串,是因为引擎内部要做日期轴刻度和跨周期聚合,字符串比较会出错。yclose漏了会怎样?第一根 K 线的涨跌颜色随机,后续所有涨跌幅、复权计算全部偏移,而且不会报错,只能靠肉眼发现。vol的单位不一致更隐蔽:成交量副图的柱子高度差 100 倍,看起来“有图”,实际完全没法看。
港股还有额外的精度问题。港股价格是三位小数,接口返回12.345,如果中间层用toFixed(2)处理过,画出来的影线就和真实成交价对不上。这类问题排查起来很费时间,所以在转换层统一做一次精度校验,比事后翻日志省事得多。
3.3 切市场与切周期:Symbol 格式和缓存键
Symbol的写法是第二个高频出错点,三个市场三种规则:
| 市场 | 格式 | 示例 | 注意 |
|---|---|---|---|
| 沪市 | 6 位代码 +.sh | 600000.sh | 别写成.ss |
| 深市 | 6 位代码 +.sz | 000001.sz | 创业板、科创板同理 |
| 港股 | 5 位代码补零 +.hk | 00700.hk | 700.hk引擎认不出 |
切周期时,Period枚举和缓存键要一起处理:
const PERIOD_MAP = { day: 0, week: 1, month: 2, min1: 4, min5: 5, min15: 6, min30: 7, min60: 8 }; onPeriodChange(e) { const key = e.currentTarget.dataset.key; const period = PERIOD_MAP[key]; if (period === undefined) return; // 缓存键必须带上复权类型,前复权和不复权是两份完全不同的数据 const cacheKey = `${this.data.symbol}_${period}_${this.data.fqType}`; const cached = getCache(cacheKey); this.setData({ periodKey: key }); if (cached) { this.chart.SetOption({ Symbol: this.data.symbol, KLine: { Period: period } }, cached); return; } this.loadAndRender({ symbol: this.data.symbol, period }); }Period的枚举值如果你手上的版本和我这份不一致,改这张表就行,别去改引擎。缓存键把symbol / period / fqType三段拼在一起,是防止切换后看到上一条数据的唯一可靠办法,后面避坑章还会提到。
4. 通达信语法引擎:一句 MA(C,5) 是怎么变成主图那根线的
4.1 指标入口:Windows 数组与 String 字段
引擎的指标系统挂在配置的Windows数组上,一个元素代表一个窗口:数组第 0 个是主图,后面依次是副图。每个窗口通过String字段接收通达信语法的公式文本:
const option = { Type: '历史K线图', Symbol: '600000.sh', IsAutoUpdate: false, KLine: { Period: 0, PageSize: 90, Right: 5, MaxReqeustDataCount: 800 }, Windows: [ // 主图:内置 MA,Modify=false 表示不给用户改参数 { Index: 'MA', Modify: false, Change: false }, // 副图一:内置成交量 { Index: 'VOL', Modify: false, Change: false }, // 副图二:自定义通达信公式 { Name: '双均线信号', Modify: true, // 允许用户调参数 Change: true, // 允许用户换成别的指标 String: 'MA5:MA(C,5);\nMA20:MA(C,20);' } ] };Index是内置指标的唯一标识,Name是自定义指标的显示名。Modify控制参数面板能不能改,Change控制指标下拉框能不能换。两个都设成false的窗口,用户只能看不能动,适合把产品定好的主图锁死。
String里的公式是标准通达信写法:MA5:冒号前面是输出线名,会显示在左上角图例里;MA(C,5)是函数调用,C是收盘价序列。分号不能省,多行之间用\n分隔,写成一行多个分号也能解析,但调试时看不出是哪一句出错。
4.2 公式实例:从均线到信号标注
几个常用的写法,我一般先在一个测试页里验证公式能跑通,再抄进项目:
// 主图叠加:两条均线 + 金叉死叉箭头 const mainScript = [ 'MA5:MA(C,5);', 'MA20:MA(C,20);', 'DRAWICON(CROSS(MA5,MA20),L*0.99,1);', // 金叉,在最低价下方画向上箭头 'DRAWICON(CROSS(MA20,MA5),H*1.01,2);' // 死叉,在最高价上方画向下箭头 ].join('\n');CROSS(A,B)判断 A 上穿 B,只在交叉那一根返回真,正好用来做信号点。L*0.99是把标注位置往下偏一点,避免箭头和影线重叠。DRAWICON第三个参数是图标编号,1和2分别对应向上、向下箭头,编号表在引擎源码里能查到。
再举个副图指标,用来展示成交量均线和放量标记:
// 副图:量能均线 + 放量柱 const volScript = [ 'VMA5:MA(V,5);', 'VMA20:MA(V,20);', 'VOLUME:VOL,VOLSTICK;' // 保留成交量柱,和均线叠在一起看 ].join('\n');V是成交量序列,VOLSTICK让这条线画成柱状而不是折线。副图里同时出现柱和线是完全正常的,引擎按输出顺序叠着画。
4.3 参数化与自定义指标注册
硬编码数字的公式在产品化时活不长,用户一定会问“能不能改周期”。Args就是干这个的:
{ Name: '可调双均线', Modify: true, Change: true, Args: [ { Name: 'N1', Value: 5 }, { Name: 'N2', Value: 20 } ], String: 'MA_FAST:MA(C,N1);\nMA_SLOW:MA(C,N2);' }Args数组里每个元素是一个可编辑参数,Name必须在String里被引用,否则参数面板改了没反应。用户点参数面板改N1,引擎会重新解析整条公式并重算,不需要你手动触发重绘。
如果产品要求指标可保存、可分享,就把用户改过的Args和String一起存进本地存储,下次进页面时按存储的配置重建Windows数组。这块我建议加一个版本号字段,公式结构升级后能识别老配置并做迁移,不然用户存了一堆旧公式,新版打开就是一片空白。
5. 避坑排查:canvas 渲染与行情数据的 5 个翻车现场
5.1 渲染与交互类问题
现象一:页面一片空白,控制台没报错。原因通常是三个中的一个:canvas 容器高度为 0,select拿到的size.height是 0;初始化写在onLoad里,节点还没挂载;dpr 没处理,画布实际尺寸远小于可视区,内容画在可视区之外。解决顺序是先打印res[0].width/height,确认不是 0,再把初始化挪到onReady,最后检查canvas.width是否等于逻辑宽 × dpr。这三步能覆盖九成空白问题。
现象二:真机上拖动很卡,K 线一根根往上跳。原因一般不是绘制慢,而是数据量太大加上状态更新过频。一次拉 5000 根历史 K 线、每帧都调setData更新标题栏价格、IsAutoUpdate开着又没控制刷新频率,三者叠加必卡。解决方法是把单次请求的MaxReqeustDataCount压到 800 左右,翻页再取;标题栏的实时价格单独用一个节流函数,100 到 300 毫秒一次;拖动过程中先关掉自动更新,手指抬起再恢复。
现象三:手指在图上滑动,整页跟着滚动,十字光标出不来。小程序的触摸事件默认会冒泡给页面滚动,图表需要独占手势。原因是没有拦住默认行为。在 canvas 上绑定catchtouchmove而不是bindtouchmove,catch会阻止冒泡;同时给页面容器加overflow: hidden。如果用了页面级的下拉刷新,还要在图表区域内临时关掉,否则长按十字光标会触发刷新。
5.2 数据与指标类问题
现象四:沪深正常,切到港股图就空了。三个可能:Symbol没补零到 5 位,700.hk应该写成00700.hk;港股价格三位小数被中间层截断;港股交易日历和 A 股不同,节假日请求返回空数组但代码没判空就直接渲染。解决是按市场做一次代码格式化,精度全程保留三位小数,请求返回空数组时保留上一帧画面并提示,而不是直接清空。
现象五:切完周期回来,指标线和 K 线对不上,箭头标在了错误的位置。这多半是数据顺序或缓存键的问题。接口偶尔倒序返回,公式里的C序列就反了,CROSS判断出来的位置整体偏移;另一种是缓存键没带周期和复权类型,切到周线拿到的是日线缓存,指标按日线算出来却画在周线上。解决是靠转换函数里那次强制排序兜底,缓存键按symbol_period_fqType三段拼。改完拿一个已知的交易日核对一遍收盘价,比盯着图看快得多。
6. 二次开发:换数据源、改配色、瘦身打包的实操路径
拿到这份代码之后最容易走上歪路的地方,是直接改引擎源码。我见过有人为了改一根均线的颜色,把hqchart.core.js里的绘制函数改了三处,后来升级版本直接全丢。正确的做法是先把改动分层:
| 改造点 | 落手位置 | 风险 |
|---|---|---|
| 换数据源 | services/quote-data.js四个钩子 | 低,只动业务层 |
| 改配色/字体 | 初始化时的 Theme/Border 配置 | 低,纯配置 |
| 改窗口布局 | Windows数组与Args | 低,配置化 |
| 加自定义指标 | 公式字符串或独立指标文件 | 中,注意函数支持范围 |
| 改绘制逻辑 | 引擎源码 | 高,升级会丢 |
换数据源这件事,关键是让适配层可注入。我一般把quoteData做成初始化时传进去的对象,再配一个 mock 实现,本地开发不连后端也能出图:
// services/mock-data.js // 本地开发用,固定生成 200 根随机游走的 K 线,方便调样式和指标 function buildMockKLine(count = 200, startPrice = 10) { const list = []; let price = startPrice; const base = new Date('2024-01-02').getTime(); for (let i = 0; i < count; i++) { const open = price; const close = +(open + (Math.random() - 0.5) * 0.6).toFixed(2); const high = +(Math.max(open, close) + Math.random() * 0.2).toFixed(2); const low = +(Math.min(open, close) - Math.random() * 0.2).toFixed(2); list.push({ date: Number(formatDate(base + i * 86400000)), yclose: open, open, high, low, close, vol: Math.floor(Math.random() * 100000), amount: Math.floor(Math.random() * 1000000) }); price = close; } return list; }这段 mock 有两个用处:一是调 Theme 配色时不用等后端;二是验证指标公式,因为随机数据波动大,CROSS会频繁触发,箭头位置对不对一眼能看出来。上线前把注入对象换成真实实现就行,页面代码一行不改。
配色走配置而不是改源码。主图背景、网格线、涨跌色、十字光标线通常都在初始化配置里,A 股习惯红涨绿跌,港股和部分海外用户习惯反过来,这个用一份配置表按市场切换就能解决。字体同理,小程序里能用的字体有限,别指望加载自定义字体文件,用系统字体加字号区分层级最稳。
打包瘦身主要看两点。一是只引hqchart.core.js和适配层,不要顺手把 min 版和 web 示例也打进分包;二是用不到的内置指标(比如期货相关的那些)可以在初始化时限制Windows的可选范围,虽然不能减小文件体积,但能减少运行时的解析和内存占用,小程序的内存上限来得比你想的快。
验证方法我固定用一条:挑一个交易日,把接口返回的开高低收、昨收、成交量打印出来,和行情软件逐个对一遍,再看 K 线图和成交量副图的数值是否一致。图能画出来不等于画对了,字段错位、精度丢失、复权没对齐这几类问题都不会报错,只会静静地把错误的线画给你看。
早几年我图快,改完配置直接上真机瞄一眼就发了,结果港股用户反馈影线位置全错,回头查是中间层对价格做了两位小数处理。从那以后我每次接新的行情数据源,都强制走一遍“打印原始字段 → 对齐一个交易日 → 真机看三档缩放”的流程,虽然多花二十分钟,但省掉的是上线后没法复现的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取