Type/to search
8
Follow
1363
Followers
设计一个多图表画线类库
Discussions
Created 2022-03-31 11:28:35  Updated 2023-09-20 10:04:46
 2
 1943

img

在编写、设计策略时创建图表画图等操作是经常用到的,对于单一自定义图表我们可以使用「画线类库」(不熟悉FMZ上模板类库的概念的同学可以查询一下FMZ API文档),非常方便进行画图操作。但是对于需要多个图表的场景这个模板类库就不能满足需求了。那么我们学习这个画线类库的设计思路,在这个基础上设计一个多图表版本的画线类库。

设计「模板类库」的导出函数

借鉴于「画线类库」的导出函数设计,多图表的画线类库我们也设计类似的导出函数。

  • $.PlotMultRecords
    用于画K线图表,参数设计:cfgName, seriesName, records, extension。
    cfgName :作为独立的图表,配置对象的名称。
    seriesName :当前要画图的K线数据系列的名称。
    records :传入的K线数据。
    extension :图表尺寸的配置信息,例如传入:{layout: 'single', col: 6, height: '600px'},即让配置对象名为cfgName的图表单独显示,宽度6,高度600px。

  • $.PlotMultLine
    用于画线,参数设计:cfgName, seriesName, dot, ts, extension
    cfgName :作为独立的图表,配置对象的名称。
    seriesName :当前要画线的数据系列的名称。
    dot :要画的线上的点的纵坐标值。
    ts :时间戳,即x时间轴上的值。
    extension :图表尺寸的配置信息。

  • $.PlotMultHLine
    用于画水平线,参数设计:cfgName, value, label, color, style
    cfgName :图表配置对象名称。
    value :水平线的纵坐标值。
    label :水平线上的显示文本。
    color :线的颜色。
    style :线的样式,例如:Solid ShortDash ShortDot ShortDashDot ShortDashDotDot Dot Dash LongDash DashDot LongDashDot LongDashDotDot

  • $.PlotMultTitle
    用于修改图表的标题、副标题。参数设计:cfgName, title, chartTitle
    cfgName :图表配置对象名称。
    title :副标题。
    chartTitle :图表标题。

  • $.PlotMultFlag
    画flag小图标,参数设计:cfgName, seriesName, ts, text, title, shape, color, onSeriesName
    cfgName :图表配置对象名称。
    seriesName :数据系列名称。
    ts :时间戳
    text :小图标中的文本。
    title :小图标的标题。
    shape :小图标形状。
    color :小图标颜色。
    onSeriesName :基于哪个数据系列上显示,值为数据系列的id。

  • $.GetArrCfg
    返回图表配置对象数组。

测试函数设计

为了便于理解,我直接把注释写在测试函数上,说明每个函数调用的作用。

// test function main() { LogReset(10) var i = 0 var prePrintTs = 0 while (true) { var r = exchange.GetRecords() // 获取K线数据 var t = exchange.GetTicker() // 获取实时的tick数据 $.PlotMultRecords("chart1", "kline1", r, {layout: 'single', col: 6, height: '600px'}) // 创建一个名为chart1的K线图表,独立显示,宽度是6,高度是600px,K线数据系列名称为kline1,使用上面获取的r作为数据源画图 $.PlotMultRecords("chart2", "kline2", r, {layout: 'single', col: 6, height: '600px'}) // 创建第二个K线图表,名为chart2 $.PlotMultLine("chart2", "line1", t.Last, r[r.length - 1].Time) // 在K线图表即chart2上增加一条线,数据系列名称为line1,使用当前的tick数据的最新价Last作为线上的点的Y值。K线数据的最后一个BAR的时间戳作为X值 $.PlotMultLine("chart3", "line2", t.Last) // 创建一个只画线的图表,图表名称chart3,数据系列名称line2,使用实时tick数据的Last最新价格在当前时间(X值)画一个点(t.Last为Y值),注意图表不是独立显示 $.PlotMultLine("chart6", "line6", t.Time) // 创建一个只画线的图表chart6,注意图表不是独立显示,会和chart3在一起分页显示 $.PlotMultLine("chart4", "line3", t.Sell, new Date().getTime(), {layout: 'single', col: 4, height: '300px'}) // 创建一个只画线的图表chart4,独立显示,宽度4,高度300px $.PlotMultLine("chart5", "line4", t.Volume, new Date().getTime(), {layout: 'single', col: 8, height: '300px'}) // 创建一个只画线的图表chart5,独立显示,宽度8,高度300px $.PlotMultHLine("chart1", r[r.length - 1].Close, "HLine1", "blue", "ShortDot") // 给图表chart1增加水平横线 $.PlotMultHLine("chart4", t.Sell, "HLine2", "green") // 给图表chart4增加水平横线 $.PlotMultTitle("chart3", "change : chart3->test1", "test1") // 修改chart3的标题 var ts = new Date().getTime() if (ts - prePrintTs > 1000 * 20) { prePrintTs = ts // 触发时,给chart3图表上画小图标 $.PlotMultFlag("chart3", "flag1", new Date().getTime(), "flag test", "flag1") } if (i == 10) { Log("i == 10") // 触发时,给chart4,chart1上画小图标 $.PlotMultFlag("chart4", "flag2", new Date().getTime(), "flag test", "flag2") $.PlotMultFlag("chart1", "flag3", new Date().getTime(), "flag test", "flag3", "squarepin", "green", "kline1") } else if (i == 20) { Log("i == 20") // 触发时,给chart1上添加一条线,但是只画了这条线的一个点,X坐标时间戳,Y坐标为t.Last值 $.PlotMultLine("chart1", "line5", t.Last, r[r.length - 1].Time) } else if (i == 30) { Log("i == 30") // 触发时,给chart2上画小图标 $.PlotMultFlag("chart2", "flag4", new Date().getTime(), "flag test", "flag4", "circlepin", "black", "kline2") } Sleep(1000 * 5) i++ } }

运行测试

img

img

可以看到只用一行函数调用,就可以轻松画一个图表,并且可以多图表同时展示。

完整的策略源码

参数配置:
img

类库源码实现:

var registerInfo = {} var chart = null var arrCfg = [] function updateSeriesIdx() { var index = 0 var map = {} _.each(arrCfg, function(cfg) { _.each(cfg.series, function(series) { var key = cfg.name + "|" + series.name map[key] = index index++ }) }) for (var cfgName in registerInfo) { _.each(arrCfg, function(cfg, cfgIdx) { if (cfg.name == cfgName) { registerInfo[cfgName].cfgIdx = cfgIdx } }) for (var i in registerInfo[cfgName].seriesIdxs) { var seriesName = registerInfo[cfgName].seriesIdxs[i].seriesName var key = cfgName + "|" + seriesName if (typeof(map[key]) != "undefined") { registerInfo[cfgName].seriesIdxs[i].index = map[key] } if (registerInfo[cfgName].seriesIdxs[i].type == "candlestick") { registerInfo[cfgName].seriesIdxs[i].preBarTime = 0 } else if (registerInfo[cfgName].seriesIdxs[i].type == "line") { registerInfo[cfgName].seriesIdxs[i].preDotTime = 0 } else if (registerInfo[cfgName].seriesIdxs[i].type == "flag") { registerInfo[cfgName].seriesIdxs[i].preFlagTime = 0 } } } if (!chart) { chart = Chart(arrCfg) } chart.update(arrCfg) chart.reset() _G("registerInfo", registerInfo) _G("arrCfg", arrCfg) for (var cfgName in registerInfo) { for (var i in registerInfo[cfgName].seriesIdxs) { var buffer = registerInfo[cfgName].seriesIdxs[i].buffer var index = registerInfo[cfgName].seriesIdxs[i].index if (buffer && buffer.length != 0 && registerInfo[cfgName].seriesIdxs[i].type == "line" && registerInfo[cfgName].seriesIdxs[i].preDotTime == 0) { _.each(buffer, function(obj) { chart.add(index, [obj.ts, obj.dot]) registerInfo[cfgName].seriesIdxs[i].preDotTime = obj.ts }) } else if (buffer && buffer.length != 0 && registerInfo[cfgName].seriesIdxs[i].type == "flag" && registerInfo[cfgName].seriesIdxs[i].preFlagTime == 0) { _.each(buffer, function(obj) { chart.add(index, obj.data) registerInfo[cfgName].seriesIdxs[i].preFlagTime = obj.ts }) } } } } function checkBufferLen(buffer, maxLen) { while (buffer.length > maxLen) { buffer.shift() } } $.PlotMultRecords = function(cfgName, seriesName, records, extension) { if (typeof(cfgName) == "undefined") { throw "need cfgName!" } var index = -1 var eleIndex = -1 do { var cfgInfo = registerInfo[cfgName] if (typeof(cfgInfo) == "undefined") { var cfg = { name: cfgName, __isStock: true, title: { text: cfgName }, tooltip: { xDateFormat: '%Y-%m-%d %H:%M:%S, %A' }, legend: { enabled: true, }, plotOptions: { candlestick: { color: '#d75442', upColor: '#6ba583' } }, rangeSelector: { buttons: [{ type: 'hour', count: 1, text: '1h' }, { type: 'hour', count: 3, text: '3h' }, { type: 'hour', count: 8, text: '8h' }, { type: 'all', text: 'All' }], selected: 2, inputEnabled: true }, series: [{ type: 'candlestick', name: seriesName, id: seriesName, data: [] }], } if (typeof(extension) != "undefined") { cfg.extension = extension } registerInfo[cfgName] = { "cfgIdx": arrCfg.length, "seriesIdxs": [{ seriesName: seriesName, index: arrCfg.length, type: "candlestick", preBarTime: 0 }], } arrCfg.push(cfg) updateSeriesIdx() } if (!chart) { chart = Chart(arrCfg) } else { chart.update(arrCfg) } _.each(registerInfo[cfgName].seriesIdxs, function(ele, i) { if (ele.seriesName == seriesName && ele.type == "candlestick") { index = ele.index eleIndex = i } }) if (index == -1) { arrCfg[registerInfo[cfgName].cfgIdx].series.push({ type: 'candlestick', name: seriesName, id: seriesName, data: [] }) registerInfo[cfgName].seriesIdxs.push({ seriesName: seriesName, index: arrCfg.length, type: "candlestick", preBarTime: 0 }) updateSeriesIdx() } } while (index == -1) for (var i = 0; i < records.length; i++) { if (records[i].Time == registerInfo[cfgName].seriesIdxs[eleIndex].preBarTime) { chart.add(index, [records[i].Time, records[i].Open, records[i].High, records[i].Low, records[i].Close], -1) } else if (records[i].Time > registerInfo[cfgName].seriesIdxs[eleIndex].preBarTime) { registerInfo[cfgName].seriesIdxs[eleIndex].preBarTime = records[i].Time chart.add(index, [records[i].Time, records[i].Open, records[i].High, records[i].Low, records[i].Close]) } } return chart } $.PlotMultLine = function(cfgName, seriesName, dot, ts, extension) { if (typeof(cfgName) == "undefined") { throw "need cfgName!" } var index = -1 var eleIndex = -1 do { var cfgInfo = registerInfo[cfgName] if (typeof(cfgInfo) == "undefined") { var cfg = { name: cfgName, __isStock: true, title: { text: cfgName }, xAxis: { type: 'datetime' }, series: [{ type: 'line', name: seriesName, id: seriesName, data: [], }] } if (typeof(extension) != "undefined") { cfg.extension = extension } registerInfo[cfgName] = { "cfgIdx": arrCfg.length, "seriesIdxs": [{ seriesName: seriesName, index: arrCfg.length, type: "line", buffer: [], preDotTime: 0 }], } arrCfg.push(cfg) updateSeriesIdx() } if (!chart) { chart = Chart(arrCfg) } else { chart.update(arrCfg) } _.each(registerInfo[cfgName].seriesIdxs, function(ele, i) { if (ele.seriesName == seriesName && ele.type == "line") { index = ele.index eleIndex = i } }) if (index == -1) { arrCfg[registerInfo[cfgName].cfgIdx].series.push({ type: 'line', name: seriesName, id: seriesName, data: [], }) registerInfo[cfgName].seriesIdxs.push({ seriesName: seriesName, index: arrCfg.length, type: "line", buffer: [], preDotTime: 0 }) updateSeriesIdx() } } while (index == -1) if (typeof(ts) == "undefined") { ts = new Date().getTime() } var buffer = registerInfo[cfgName].seriesIdxs[eleIndex].buffer if (registerInfo[cfgName].seriesIdxs[eleIndex].preDotTime != ts) { registerInfo[cfgName].seriesIdxs[eleIndex].preDotTime = ts chart.add(index, [ts, dot]) buffer.push({ ts: ts, dot: dot }) checkBufferLen(buffer, maxBufferLen) } else { chart.add(index, [ts, dot], -1) buffer[buffer.length - 1].dot = dot } return chart } $.PlotMultHLine = function(cfgName, value, label, color, style) { if (typeof(cfgName) == "undefined" || typeof(registerInfo[cfgName]) == "undefined") { throw "need cfgName!" } var cfg = arrCfg[registerInfo[cfgName].cfgIdx] if (typeof(cfg.yAxis) == "undefined") { cfg.yAxis = { plotLines: [] } } else if (typeof(cfg.yAxis.plotLines) == "undefined") { cfg.yAxis.plotLines = [] } var obj = { value: value, color: color || 'red', width: 2, dashStyle: style || 'Solid', label: { name: label || '', text: (label + ":" + value) || '', align: 'center' }, } var found = false for (var i = 0; i < cfg.yAxis.plotLines.length; i++) { if (cfg.yAxis.plotLines[i].label.name == label) { cfg.yAxis.plotLines[i] = obj found = true } } if (!found) { cfg.yAxis.plotLines.push(obj) } if (!chart) { chart = Chart(arrCfg) } else { chart.update(arrCfg) } return chart } $.PlotMultTitle = function(cfgName, title, chartTitle) { if (typeof(cfgName) == "undefined" || typeof(registerInfo[cfgName]) == "undefined") { throw "need cfgName!" } var cfg = arrCfg[registerInfo[cfgName].cfgIdx] cfg.subtitle = { text: title } if (typeof(chartTitle) !== 'undefined') { cfg.title = { text: chartTitle } } if (chart) { chart.update(arrCfg) } return chart } $.PlotMultFlag = function(cfgName, seriesName, ts, text, title, shape, color, onSeriesName) { if (typeof(cfgName) == "undefined" || typeof(registerInfo[cfgName]) == "undefined") { throw "need cfgName!" } var index = -1 var eleIndex = -1 do { if (!chart) { chart = Chart(arrCfg) } else { chart.update(arrCfg) } _.each(registerInfo[cfgName].seriesIdxs, function(ele, i) { if (ele.seriesName == seriesName && ele.type == "flag") { index = ele.index eleIndex = i } }) if (index == -1) { arrCfg[registerInfo[cfgName].cfgIdx].series.push({ type: 'flags', name: seriesName, onSeries: onSeriesName || arrCfg[registerInfo[cfgName].cfgIdx].series[0].id, data: [] }) registerInfo[cfgName].seriesIdxs.push({ seriesName: seriesName, index: arrCfg.length, type: "flag", buffer: [], preFlagTime: 0 }) updateSeriesIdx() } } while (index == -1) if (typeof(ts) == "undefined") { ts = new Date().getTime() } var buffer = registerInfo[cfgName].seriesIdxs[eleIndex].buffer var obj = { x: ts, color: color, shape: shape, title: title, text: text } if (registerInfo[cfgName].seriesIdxs[eleIndex].preFlagTime != ts) { registerInfo[cfgName].seriesIdxs[eleIndex].preFlagTime = ts chart.add(index, obj) buffer.push({ ts: ts, data: obj }) checkBufferLen(buffer, maxBufferLen) } else { chart.add(index, obj, -1) buffer[buffer.length - 1].data = obj } return chart } $.GetArrCfg = function() { return arrCfg } $.removeChart = function(cfgName) { var index = -1 for (var i = 0; i < arrCfg.length; i++) { if (arrCfg[i].name == cfgName) { index = i break } } if (index != -1) { arrCfg.splice(index, 1) } if (typeof(registerInfo[cfgName]) != "undefined") { delete registerInfo[cfgName] } updateSeriesIdx() } function init() { if (isChartReset) { Log("重置图表", "#FF0000") chart = Chart(arrCfg) chart.reset() Log("清空持久化数据,key:", "registerInfo、arrCfg #FF0000") _G("registerInfo", null) _G("arrCfg", null) } else { var multChartRegisterInfo = _G("registerInfo") var multChartArrCfg = _G("arrCfg") if (multChartRegisterInfo && multChartArrCfg) { registerInfo = multChartRegisterInfo arrCfg = multChartArrCfg Log("恢复 registerInfo、arrCfg #FF0000") } else { Log("没有数据可以恢复 #FF0000") } } } function onexit() { _G("registerInfo", registerInfo) _G("arrCfg", arrCfg) Log("保存数据,key : registerInfo, arrCfg #FF0000") } // test function main() { LogReset(10) var i = 0 var prePrintTs = 0 var t = _C(exchange.GetTicker) while (true) { var r = _C(exchange.GetRecords) $.PlotMultRecords("chart1", "kline1", r, { layout: 'single', col: 6, height: '600px' }) $.PlotMultRecords("chart2", "kline2", r, { layout: 'single', col: 6, height: '600px' }) $.PlotMultLine("chart2", "line1", t.Last, r[r.length - 1].Time) $.PlotMultLine("chart3", "line2", 10 + i) $.PlotMultLine("chart6", "line6", 100 + i) $.PlotMultLine("chart4", "line3", 1000 + i, new Date().getTime(), { layout: 'single', col: 4, height: '300px' }) $.PlotMultLine("chart5", "line4", 10000 + i, new Date().getTime(), { layout: 'single', col: 8, height: '300px' }) $.PlotMultHLine("chart1", r[r.length - 1].Close, "HLine1", "blue", "ShortDot") $.PlotMultHLine("chart4", t.Sell, "HLine2", "green") $.PlotMultTitle("chart3", "change : chart3->test1", "test1") var ts = new Date().getTime() if (ts - prePrintTs > 1000 * 20) { prePrintTs = ts $.PlotMultFlag("chart3", "flag1", new Date().getTime(), "flag test" + i, "flag1") } if (i == 10) { Log("i == 3") $.PlotMultFlag("chart4", "flag2", new Date().getTime(), "flag test" + i, "flag2") $.PlotMultFlag("chart1", "flag3", new Date().getTime(), "flag test" + i, "flag3", "squarepin", "green", "kline1") } else if (i == 20) { Log("i == 8") $.PlotMultLine("chart1", "line5", t.Last, r[r.length - 1].Time) } else if (i == 30) { Log("i == 10") $.PlotMultFlag("chart2", "flag4", new Date().getTime(), "flag test" + i, "flag4", "circlepin", "black", "kline2") $.removeChart("chart1") } i++ Sleep(1000 * 1) } }

策略地址:https://www.fmz.com/strategy/353264

这里小小梦抛砖引玉,有兴趣的可以继续增加支持的图表类型,继续升级,例如画盘口深度图,柱状图,饼图等等。

Related Recommendations
Comment
All comments (2)

    梦大,超赞 [赞],图表名称如果用for遍历的变量代替,直接就会自动分页显示不同的表了。[呲牙]

    4 years ago

    是的,只要标题不同,就会分开显示了。不过要看模版导出函数传入的参数,可以指定叠加或者平铺。

    4 years ago
  • 1
iPhone Download
Forums
PINE Language
© 2015 - ∞ INVENTOR PTE LTD (SG)