title: {
text: 'i888',
color: '#333',
},
opposite: true,
labels: {
align: "right",
x: -3,
color: '#333',
}
},
{
title: {
text: '',
color: '#333',
},
top: '62%',
height: '40%',
offset: 0
},
{
title: {
text: '',
color: '#333',
},
top: '62%',
height: '40%',
offset: 0
},
{
title: {
text: '',
color: '#333',
},
top: "62%",
height: "40%",
offset: 0
}, ],
series: [{
type: 'candlestick',
name: 'i888',
data: [],
tooltip: {
xDateFormat: '%Y-%m-%d %H:%M:%S'
},
yAxis: 0,
color: b.Open > b.Close ? '#00ff00' : '#ff0000',
dataLabels: {
shadow: true // 添加阴影
}
}, {
type: 'line',
name: 'DIF',
data: [],
yAxis: 1,
lineWidth: 1
},
{
type: 'line',
name: 'DEA',
data: [],
yAxis: 1,
lineWidth: 1
},
{
type: 'column',
lineWidth: 2,
name: 'MACD',
data: [],
yAxis: 1,
zones: [{
value: 0,
color: "#00ff00"
}, {
color: "#ff0000"
}]
}]
};
function main() { let c = Chart(chartCfg); while (true) { Sleep(1000)
exchange.SetContractType('i888')
var records = exchange.GetRecords(); // 将K线数据存储到对应的变量中
var DIF = TA.MACD(records, 12, 26, 9)[0][records.length-1]
var DEA = TA.MACD(records, 12, 26, 9)[1][records.length-1]
var MACD = TA.MACD(records, 12, 26, 9)[2][records.length-1]
var b = records[records.length - 1]
c.add(0, [b.Time, b.Open, b.High, b.Low, b.Close])
//c.add([0, [new Date().getTime(), records[records.length-1].Close]]);
c.add([1, [new Date().getTime(), DIF]]);
c.add([2, [new Date().getTime(), DEA]]);
c.add([3, [new Date().getTime(), MACD]]);
}
}
首先,定义了一个包含图表配置信息的对象`chartCfg`,其中包括图表标题 subtitle、Y 轴 yAxis 和数据系列 series 等属性。其中,Y 轴主要由四个属性构成,分别对应蜡烛图、DIF 线、DEA 线和 MACD 柱形图等数据所在的 Y 轴上下文。每个 Y 轴的属性包括高度 height、轴线宽度 lineWidth、标题 title、opposite 属性等,用于控制 Y 轴的显示效果和位置等特性。
定义 main 函数,用于初始化图表,并不断添加新的数据到图表中。该函数使用 while 循环实现,每隔1秒钟获取一次最新的铁矿石价格数据,并将其存储在 records 变量中。然后,使用技术指标库 TA 中的 MACD 函数计算 DIF、DEA 和 MACD 三个指标的值,并将最新的指标值和价格数据添加到相应的数据系列中。
使用 Chart 对象的 add 方法将新的数据添加到图表中。其中,add 方法的参数包括数据系列的索引和数据点的数组,第一个元素为时间戳,第二个元素为具体的数值,分别将k线数据(包括最高,最低,开盘,收盘),dea,dif和macd的值输出到图表上。最后,通过 Sleep 方法使程序暂停 1 秒钟,然后再次执行循环,实现数据的实时更新和图表的动态展示。
我们需要看下这里的主图和副图的设计。chartCfg 的对象包括主图和副图配置信息。
主图中包含一个名为 “i888”的蜡烛图系列,用于显示铁矿石的价格信息。该数据系列的类型为 candlestick,表示使用蜡烛图的方式展示数据。数据系列的各种属性包括数据 data、Y 轴 yAxis、颜色 color、数据标签 dataLabels 等。
副图中包含三个数据系列,分别是 DIF 线、DEA 线和 MACD 柱形图。它们都是使用 line (就是DIF 线、DEA 线)或 column 类型(MACD)来展示数据,三个指标的 Y 轴坐标都是1,所以他们会在幅图中重叠呈现,和上面的标准的结果显示的一致。height: "60%" 是设置 Y 轴高度的属性。在主图中,Y 轴高度为图表总高度的 60%,副图中各个 Y 轴的高度为图表总高度的 40%。这样可以在一个图表中同时展示多个数据系列,更方便对比不同的数据。
![image](https://www.youquant.com![img](/upload/asset/2b0448c1b07610d5f8982.png))
以上的就是使用chart画图的例子,我们的课程,不能面面俱到,还有很多的细节在[link](https://api.highcharts.com/highstock/),大家都可以尝试探索下,构造出符合你的交易习惯的可视化面板。
# 10:JavaScript画图:KLineChart函数
在使用 JavaScript 或 Python 编写策略时,设计策略图表的显示是非常重要的。对编程不熟练或者对优宽平台使用的图表库不熟悉的小伙伴,经常会苦恼于自定义图表上画图的代码设计。我们在上个阶段,使用Chart函数进行图表设置的时候,在图表参数设置,数据导入等阶段,都需要编写复杂的代码。那么有没有一种画图方法,结构更加清晰,而且只用编写少量的代码,又可以画出丰富内容的策略图表呢?
Pine语言相信大家都听说过,它是一门高度封装的专门为交易而生的语言。Pine语言有丰富画图功能,它的画图方式非常的简单,并且功能也十分的强大。如果可以把Pine语言的画图接口接入到JavaScript语言的策略中进行使用,那么就极大得方便了我们设计策略的画图功能。于是优宽平台基于这种需求,升级了自定义画图功能,扩展了使用KLineChart函数进行自定义图表画图的方式。大家可以对比一下chart函数,KLineChart对于编程基础薄弱的我们,确实是一个更为友好地选择。
## 简单例子
```JavaScript
function main() {
var c = KLineChart()
while (true) {
if (exchange.IO("status")) {
exchange.SetContractType("rb888")
var bars = exchange.GetRecords()
if (!bars) {
Sleep(1000)
continue
}
for (var i = 0 ; i < bars.length ; i++) {
var bar = bars[i]
c.begin(bar)
c.plot(bar.Volume, "volume")
c.close()
}
}
Sleep(1000)
}
}
话不多说,让我们来尝试一下。可以看到下面的代码,相对于chart函数需要先设置一系列选项和参数的配置选项的图表对象,KLineChart非常简单和直观。你只需要在主函数中调用KLineChart()
函数创建一个图表对象c就可以了。接着,你可以利用while循环不断地获取交易数据并绘制图表。
画图操作要从begin()
函数开始,close()
函数结束。begin
、close
函数都是图表对象的方法。我们还使用plot()
方法将成交量指标画在图表上。其中,第一个参数bar.Volume
表示成交量数据,第二个参数"volume"表示成交量指标的名称。在这个例子中,我们只绘制了一个成交量指标,但是你可以通过添加更多的plot方法来绘制其他的指标线。
最后,通过Sleep
函数设置程序每隔1秒钟执行一次,以保证实时更新数据。我们看一下回测结果,可以看到,交易量图表呈现了出来。总之,利用KLineChart
函数,我们可以在不编写复杂的代码的情况下,轻松地创建自定义的策略图表。
function main() {
var c = KLineChart()
// 策略主循环
while(true) {
// 轮询间隔
Sleep(500)
if (exchange.IO("status")) {
exchange.SetContractType("rb888")
// 获取K线数据
var bars = exchange.GetRecords()
if (!bars || bars.length <= 20) {
continue
}
// 计算布林指标
var boll = TA.BOLL(bars)
bars.forEach(function(bar, index) {
c.begin(bar)
// 画图操作
c.plot(boll[0][index], "Boll_Up", {overlay: true}) // 画在图表主图
c.plot(boll[1][index], "Boll_Mid", {overlay: true}) // 画在图表主图
c.plot(boll[2][index], "Boll_Down", {overlay: true}) // 画在图表主图
c.plot(bar.Volume, "volume") // 画在图表副图
c.close()
})
}
}
}
下面我们来看一个复杂的例子。假如我们想在每根BAR的成交量线图的基础上,设计一个附带布林指标的图表,我们可以这样编写策略。
这是一个使用KLineChart
和布林带指标的交易策略示例代码。在主函数中,我们再次使用KLineChart
创建了一个名为“c”的图表对象。接下来进入循环,我们设置Sleep(500)
作为轮询间隔,一直等待交易所状态为“开启”时才会执行下面的代码。当交易所状态为“开启”时,我们调用exchange.SetContractType
函数,设置合约类型为“rb888”。然后使用exchange.GetRecords()
函数获取最新的K线数据。为了满足布林带参数计算的要求,如果K线数据不足20根,则跳过本次循环。接着,我们调用TA
函数来计算布林指标并将结果赋值给变量boll
。这个函数需要传入一个K线数据数组作为参数。所以我们使用bars作为参数。
之后,我们使用forEach
方法遍历每根K线,并在图表上绘制布林带指标及成交量指标。其中,第一行代码表示将布林带的上轨画在K线图主图上,第二行代码表示将布林带的中轨画在K线图主图上,第三行代码表示将布林带的下轨画在K线图主图上, 而最后同样使用plot()
表示将成交量指标画在K线图副图上。forEach
是一种循环遍历数组的方法,可以用于遍历数组中的每一个元素,并对其进行相应的操作。forEach
方法接受一个函数作为参数,该函数将被用于处理数组中的每个元素。
这里我们解释下计算出来的布林带指标的结构,它是一个二维的数组。在第一个维度,也就是布林带索引为0,1,2分别为上轨,中轨和下轨的指标数组,而在第二个维度,是各指标的实时的数值。另外,这里有一个参数overlay
,熟悉pine语言的朋友对这个参数一定不陌生,它可以决定参数在主图或者幅图呈现,如果不设置,默认为false,就是volume成交量在幅图中进行呈现,而三个指标overlay
为true
的情况下在主图上进行呈现。最后,我们调用c.close()
方法结束当前K线的绘制。这样,我们就可以得到一个实时更新的布林带指标和成交量指标的K线图了。
function main() {
var c = KLineChart()
// 策略主循环
while(true) {
// 轮询间隔
Sleep(500)
if (exchange.IO("status")) {
exchange.SetContractType("rb888")
// 获取K线数据
let bars = exchange.GetRecords()
if (!bars || bars.length <= 20) {
continue
}
// 计算布林指标
var boll = TA.BOLL(bars)
bars.forEach(function(bar, index) {
c.begin(bar)
// 画图操作
c.plot(boll[0][index], "Boll_Up", {overlay: true}) // 画在图表主图
c.plot(boll[1][index], "Boll_Mid", {overlay: true}) // 画在图表主图
c.plot(boll[2][index], "Boll_Down", {overlay: true}) // 画在图表主图
c.plot(bar.Volume, "volume") // 画在图表副图
c.hline(bar.Open, {overlay: true}) // 水平线
c.plotarrow(bar.Close - bar.Open, {overlay: true}) // 箭头
c.plotshape(bar.Close - bar.Open > 0, {style: 'square', overlay: true}) // 画方块标记
c.plotchar(bar.Close - bar.Open < 0, {char: '❄', size: "20px", overlay: true}) // 画出字符❄
if (boll[0][index] && bar.Close > boll[0][index]) {
c.signal("long", bar.Close, 1.5)
} else if (boll[2][index] && bar.Close < boll[2][index]) {
c.signal("closelong", bar.Close, 1.5)
}
c.close()
})
}
}
}
在begin函数和close函数之间就是我们调用的类似Pine语言画图方式的画图函数了。支持:barcolor bgcolor plot fill hline plotarrow plotshape plotchar plotcandle signal
,可以画线、画箭头、标记信息等。这些函数的参数和Pine语言对应的函数参数一致,画图功能也一致。大家可以在优宽平台pine语言文档查看一下使用方法。
在原先的代码基础上,我们再增加一些图像。除了绘制布林带指标之外,这里使用c.hline()
函数在当前价格下方画一条水平线。c.plotarrow
函数表示在 K 线图上绘制一个箭头,用于标记买卖信号。参数 bar.Close - bar.Open
表示收盘价减去开盘价的差值,收盘价相对开盘价的涨跌幅度决定箭头的方向和长短。
c.plotshape
函数表示当天的收盘价比开盘价高,在 K 线图上绘制一个方块标记。
c.plotchar
函数表示当天的收盘价比开盘价低,在 K 线图上绘制一个字符标记。
最后,使用c.signal()
函数返回交易信号,并在代码中设置了两种情况:当指标值存在并且收盘价大于布林带上轨时,发出“long”信号;当指标值存在并且收盘价小于布林带下轨时,发出“closelong”信号。
function main() {
var chartCfg = {
grid: {
show: true,
// 网格水平线
horizontal: {
show: true,
size: 2,
color: '#FF0000',
// 'solid'|'dash'
style: 'dash',
dashValue: [2, 2]
},
// 网格垂直线
vertical: {
show: true,
size: 2,
color: '#32CD32',
// 'solid'|'dash'
style: 'solid',
dashValue: [2, 2]
}
},
}
var c = KLineChart(chartCfg)
// 策略主循环
while(true) {
// 轮询间隔
Sleep(500)
if (exchange.IO("status")) {
exchange.SetContractType("rb888")
// 获取K线数据
var bars = _C(exchange.GetRecords)
bars.forEach(function(bar, index) {
c.begin(bar)
c.close()
})
}
}
}
Klinechart
可以进行图表配置对象,比如图表属性、外观等类似的设置。我们在上面的例子中,KLineChart
函数中没有进行设置,那么创建的图表就是默认样式。这里我们举例一下网格线的图表配置对象的设置。在回测结果中可以看到在图标背景中增加了网格线。
当然这只是一个简单的例子,我们设计的可以更加复杂。图表配置对象就是设置图表一些属性、外观等的数据结构,如例子中使用的就是一种网格线样式的配置。还有很多可以配置修改的选项,例如X轴、Y轴的相关设置,光标线的设置,提示信息的设置,技术指标样式设置,K线BAR样式的设置等等。大家可以自由探索下。
通过本节课程,我们学习了如何使用KLineChart
在JavaScript语言的策略中进行自定义图表的画图。与chart函数相比,KLineChart
更加简单、直观,也更加强大,支持绘制多种图形和指标线。需要注意的是策略自定义画图只能使用KLineChart()
方式或者Chart()
方式中的一种。希望通过本课程的学习,可以让大家更加灵活地进行策略图表的设计。
在上一阶段的课程中,我们讲解了使用JavaScript的原生画图函数和接入Pine语言画图接口的klinechart函数,其实在优宽平台,JavaScript还有一种画图方法,画线类库。
在讲解画线类库的概念之前,我们首先介绍一下模版类库的概念。模板类库是优宽量化交易平台中可复用的代码模块,是策略代码的一种类别。你可以把模版类库想象成一个功能模块,使用模版类库的好处是,可以让你把精力放到编写策略逻辑上面。优宽量化交易平台支持把一些常用的、可复用的代码模块封装成独立的库(在优宽量化交易平台上叫做模板类库)。这样可以提高策略开发速度,不用编写重复的代码,降低策略交易部分和策略逻辑部分的耦合度,便于策略维护、优化和扩展。
创建一个模板类库和创建一个策略操作相同,在「策略库」中点击「新建策略」跳转到创建策略页面,区别是策略类型选择「模板类库」。然后给模板起个名字,保存就可以。模板类库设计主要有两个方面:第一是需要设计导出函数,导出函数为模板类库的接口函数$.
,就是这个模板类库提供哪些功能,通过调用导出函数去使用这个模板类库提供的功能。第二是需要设计这个模板类库的参数,和普通策略一样,模板类库也可以设置参数,用于使用的时候,动态设置一些数值等参数数据。模版类库的使用也很简单。在我们的策略库里,创建或者复制模版类库以后,当然对于内置类库,比如交易类库,可以直接使用。然后在需要使用模版类库的时候,在模版引用板块,直接点击勾选就可以。在代码中,模版类库可以通过$.
函数接口进行访问,直接使用它的功能。
回到我们的画线类库。画线类库简化了策略图表画图的逻辑,可以直接调用封装好的函数。它在内置源码中帮助我们创建好了图表对象,所以使用的时候,不需要再重新设置图表配置。画线类库支持画多条线,支持K线图和flag小图标,当然我们也可以添加更多图形的支持(这个需要我们自己在源码中进行设置)。
我们可以来看一下画线类库源码。这段代码是一个用于绘制图表的JavaScript类库,使用原生的chart
函数编写而成的。其中,变量 cfg 存储了图表的配置信息,包括图表的标题、Y轴辅助线、时间范围选择器、数据系列等。同时还定义了一些实用的函数,如 PlotHLine
用于绘制水平辅助线,PlotRecords
用于加载K线数据,PlotLine
用于绘制线,PlotFlag
用于绘制旗形标志,PlotTitle
用于更新图表的标题。这其中的图表基本选项和数据配置参数已经设置完毕,因此使用的时候直接通过$.
接口调用就可以进行绘图,节省了大量的时间和精力。
var PreBarTime = 0
function PlotMA_Kline(records, param, isFirst){
var ma = TA.MA(records, param)
$.PlotRecords(records, "K线数据")
if(isFirst){
for(var i = records.length - 1; i >= 0; i--){
if(ma[i] !== null){
$.PlotLine("十日均值", ma[i], records[i].Time)
}
}
PreBarTime = records[records.length - 1].Time
} else {
if(PreBarTime !== records[records.length - 1].Time){
$.PlotLine("十日均值", ma[ma.length - 2], records[records.length - 2].Time)
PreBarTime = records[records.length - 1].Time
}
$.PlotLine("十日均值", ma[ma.length - 1], records[records.length - 1].Time)
}
}
function main(){
var isFirst = true
exchange.SetContractType('rb000')
while(1){
var records = exchange.GetRecords()
if(records && records.length > 10){
PlotMA_Kline(records, 10, isFirst)
isFirst = false
}
Sleep(1000)
}
}
我们来举一个使用画线类库画K线以及均线图表的范例。首先定义了一个全局变量PreBarTime,用于存储上一根K线的时间戳。函数PlotMA_Kline
接受三个参数:records表示K线数据的数组,param表示计算均线所需的周期数,isFirst表示当前是否为第一次调用该函数。在PlotMA_Kline
函数中,使用TA.MA函数计算出指定周期内每个K线的均线值。然后使用$.PlotRecords
函数将K线数据绘制到K线图上。
如果是第一次调用该函数,遍历K线数据数组,找到最近的一个不为空的均值线的位置,并将该位置之后的均值线画在K线图上。更新PreBarTime
变量的值,将其赋值为当前K线数据数组中最新一根K线的时间戳。如果不是第一次调用该函数,判断当前最新一根K线的时间戳是否与上一根K线的时间戳相同。如果不同,则说明已经出现了新的K线,此时需要将上一根K线的均值线绘制到K线图上。最后,使用$.PlotLine
函数将最新一根K线的均值线绘制到K线图上,并更新PreBarTime变量的值。
这里有一个特别的isFirst
变量,它是用于表示当前是否为第一次调用PlotMA_Kline
函数。在main函数中,首先将isFirst
变量赋值为true,然后不断循环获取K线数据,如果K线数据数组长度超过10,则调用PlotMA_Kline
函数进行均值线的绘制。最后通过Sleep
函数暂停1秒钟,再次进入循环。当isFirst
为true时,说明是第一次调用该函数,此时需要遍历K线数据数组,找到最近的一个不为空的均值线的位置,并将该位置之后的均值线画在K线图上。而当isFirst
为false时,说明曾经调用过该函数,只需要在新的K线数据出现时将上一根K线的均值线绘制到K线图上即可。
因此,isFirst
变量的主要作用是控制均值线是否应该重新绘制,以及从哪个位置开始进行绘制。如果不加区分地每次都重新绘制均值线,则会导致K线图上的均值线出现重叠或断层等不符合实际的情况,影响了交易者的判断和分析。
在编写、设计策略时创建多图表画图是经常用到的,对于单一图表我们可以使用「画线类库」,非常方便进行画图操作。但是对于需要多个图表的场景这个模板类库就不能满足需求了。优宽平台根据画线类库的设计思路,在这个基础上设计了一个多图表版本的画线类库。大家可以尝试使用一下。
我们来看下源码中的接口函数,其中:
$.PlotMultRecords
用于画K线图表;$.PlotMultLine
用于画线;对于这两个函数,需要注意里面的extension图表尺寸的配置信息,比如分组还是单独的显示,图表的高度和宽度等等。$.PlotMultHLine
用于画水平线;这里面需要注意的参数是style,线的样式,比如实线,虚线等。$.PlotMultTitle
用于修改图表标题和副标题;$.PlotMultFlag
用于画flag小图标;$.GetArrCfg
返回图表配置对象数组。这些函数都需要传入一定的参数才能完成图表的绘制。同样地,首先复制画线类库到我们的策略库中。点击勾选就可以使用了。
/*backtest
start: 2023-06-13 09:00:00
end: 2023-06-19 15:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
*/
// test
function main() {
while (true) {
exchange.SetContractType('rb888')
var r_rb = exchange.GetRecords() // 获取K线数据
var t_rb = exchange.GetTicker() // 获取实时的tick数据
exchange.SetContractType('hc888')
var r_hc = exchange.GetRecords() // 获取K线数据
var t_hc = exchange.GetTicker() // 获取实时的tick数据
$.PlotMultRecords("rb_K线", "螺纹钢K线", r_rb, {layout: 'single', col: 6, height: '600px'})
$.PlotMultRecords("hc_K线", "热卷K线", r_hc, {layout: 'single', col: 6, height: '600px'})
$.PlotMultLine("rb_newprice", "螺纹钢最新价", t_rb.Last,new Date().getTime())
$.PlotMultLine("hc_newprice", "热卷最新价", t_hc.Last,new Date().getTime())
$.PlotMultLine("diff", "差价", t_hc.Last-t_rb.Last, new Date().getTime(),{layout: 'single', height: '600px'})
$.PlotMultHLine("diff", t_hc.Last-t_rb.Last, "最新差价", "blue", "ShortDot") // 给图表diff增加水平横线
$.PlotMultTitle("diff", "更改标题 : diff->最新差价图", "最新差价图") // 修改chart3的标题
if (t_hc.Last-t_rb.Last > 105) {
$.PlotMultFlag("diff", "flag1", new Date().getTime(), "flag test", "差值较大")
} else if (t_hc.Last-t_rb.Last < 90) {
$.PlotMultFlag("diff", "flag2", new Date().getTime(), "flag test2", "差值较小")
}
Sleep(1000 )
}
}
这段代码是一个多图表模版类库的样例,主要用于获取螺纹钢和热卷的实时市场数据,并对比它们的差价。这里先设置为螺纹钢(rb888),再获取数据后再切换到热卷(hc888)。
然后获取K线数据和tick数据,并保存到变量相应的变量当中,主要用于计算差价并展示最新价格。
$.PlotMultRecords()
和$.PlotMultLine()
函数用于将K线,价格和差价数据以图表的形式展示出来,其中在最后差价图中使用 $.PlotMultHLine()
显示最新差价的水平横线。$.PlotMultTitle()
函数用于更改图表标题。
这个if语句主要用于判断差价是否达到一定的阈值,并进行相应的操作。在这个例子中,当差价大于105时,会使用$.PlotMultFlag
,在差价图上显示"diff值较大"的小旗。当差价小于90时,会显示"diff值较小"的小旗。
通过对比原生的chart画图函数,可以发现画线类库省去了复杂的图像配置,使代码更加简洁易读。我们可以更加专注于策略研究和优化,从而获得更好的交易结果。此外,画线类库还具有更强的扩展性和定制性,可以根据我们不同的需求进行扩展和优化,满足更加细致和复杂的图表需求。
希望我们的模版类库具有抛砖引玉的作用,大家有兴趣的,可以在源码基础上,继续增加支持的图表类型,继续升级,例如画盘口深度图,柱状图,饼图等等,打造出来专属于你的画线类库。
在前面的课程中,我们介绍了画图的模板类库,它对原生的JavaScript绘图函数进行了封装,方便我们进行图表的绘制。在量化交易中,时间是至关重要的,因为市场瞬息万变。一个好的交易函数可以快速响应市场信号并执行交易操作,从而提高交易的效率和准确性。为了实现这一目标,优宽平台封装了一些常用功能,例如开仓平仓、CTA函数、判断交叉等,并推出了实盘交易类库。
实盘交易类库的使用方法非常简单,作为一个内置的模板类库,我们只需勾选使用就可以。当然,如果你希望构建自己的交易类库,也可以从这个公开的类库源码开始。这份代码附带了详细的注释,供大家参考学习。通过使用交易类库,大家可以从繁琐的交易参数设置细节中解脱出来,更加专注于策略本身的编写,提升策略的执行效率。
首先我们介绍一下CTA函数。它相当于一个策略编写的整体框架。CTA函数用于实现基于交易所行情和持仓信息的自动交易策略。该函数可以根据一系列设定的规则和条件执行买入和卖出操作。该函数接受三个参数:contractType(合约类型)、onTick(回调函数)和interval(轮询间隔,可以省略)。
这里面我们讲一下onTick
,它是一个回调函数。
这里的回调函数应返回一个整数,表示需要执行的买卖操作数量,大于0表示买入,小于0表示卖出,等于0表示不进行任何操作。
//CTA函数
function main() {
$.CTA("FG888", function(st) {
if (st.records.length < 20) {
return
}
var emaSlow = TA.EMA(st.records, 20)
var emaFast = TA.EMA(st.records, 5)
var cross = $.Cross(emaFast, emaSlow);
LogStatus('可用保证金:', st.account.Balance)
if (st.position.amount <= 0 && cross > 2) {
Log("金叉周期: ", cross);
Log("当前持仓: ", st.position);
return st.position.amount < 0 ? 2 : 1
} else if (st.position.amount >= 0 && cross < -2) {
Log("死叉周期", cross);
Log("当前持仓: ", st.position);
return st.position.amount > 0 ? -2 : -1
}
});
}
这样讲解可能不太直观,我们举例示范一下。这是使用CTA库来进行金叉和死叉信号进行交易的例子。首先,调用了$.CTA
函数,参数为玻璃主力期货合约,和一个回调函数。在我们的回测测试中,经常使用期货代码+888,表示该品种的主力合约,如果我们不使用交易类库,直接将期货888代码,带入实盘运行,会出现,找不到该合约的错误提示。交易类库经过内置函数的映射,可以直接匹配888代码,为当前策略周期的,具体的主力合约代码。另外,CTA函数也可以映射到别的合约,如果我们设置i000/rb888,就是根据铁矿石的指数的K线,来交易螺纹钢的主力连续。另外,不同的主力合约是有一定的期限的,在策略运行的过程中,为确保策略长期的平稳运行,经常需要对主力合约进行更换。为解决移仓换月的麻烦,交易类库内置添加了,主力合约的自动移仓功能。
通过源码中可以看到,st包含了期货合约的综合数据,包括k线,账户和仓位等信息。通过st.records.length
判断市场数据的记录数量是否大于等于20,如果不满足条件,则直接返回,不进行后续的逻辑判断和操作。如果市场数据记录数量满足要求,接下来使用TA.EMA
函数计算了慢速(20周期)和快速(5周期)的指数移动平均线(EMA)。通过$.Cross
函数判断快速EMA和慢速EMA是否发生交叉。cross函数中需要两个数组进行交叉信号的判断,返回结果是上穿的周期数。正数为上穿周数,负数表示下穿的周数,0指当前价格一样。这里为了防止最新的k线没有走完,所以我们设定的阈值绝对值为2。如果快速EMA大于慢速EMA的时候交叉值大于2,则表示发生了金叉;如果快速EMA小于慢速EMA的时候交叉值小于-2,则表示发生了死叉。
这里我们使用了LogStatus
函数打印account.Balance
的信息。和log函数一样,它也是一个信息输出的函数,但是相对于log函数在日志呈现的信息可能过于繁杂,LogStatus
可以状态信息栏,更加清晰的显示实时的状态信息。
大家也许会好奇,没有常规的开仓或者平仓语句,交易是怎样进行的呢,其实这就是我们刚才提到的return返回值进行下单。根据当前持仓情况和交叉的方向,通过return语句针对仓位进行不同的交易操作。CTA函数中交易的操作是通过return语句开展的,交易操作参数为n。如果当前持有的仓位position.amount
为0,n就直接代表着开多(n为正)或者开空(n为负)的手数。如果当前的仓位position.amount
不是0,进行的交易操作需要和持有的仓位进行比较,然后进行开平仓的操作。我们举例讲解下,比如现在position.amount
为-3,代表持有三手的空仓,如果这时候n为2,就意味着需要平掉2手的空仓,还剩下1手的空仓不进行操作;而如果n为4,则代表需要全部平掉3手的空仓,然后再开一手多仓。所以在本例中,我们使用的就是这样的方法来进行对应开平仓的操作。这里面的逻辑确实有点复杂,大家可以去源码中进行查阅,加深一下理解。
在我们的例子中,策略的开平仓的逻辑是这样的,针对于金叉,如果当前仓位是空头或者没有的仓位,st.position.amount
小于等于0,这个时候需要进行不同的交易操作。这里使用了一个三元表达式,如果是空头仓位(position.amount
小于0),金叉出现应该首先平掉空仓,然后开一手多仓,所以这里的数字要填写为2;如果没有仓位(position.amount
=0),对应的数字是1,就是直接开一手多头仓位。针对于死叉也是一样,具有多头仓位的时候,首先需要平掉多头,然后再开一手空仓,数字填写为-2;没有仓位,直接开一手空头,填写数字为-1。我们看一下回测结果,可以看到伴随金叉和死叉指标,多头和空头不断的对调开平仓,符合策略的交易逻辑。
总体来说,这段代码使用了CTA函数进行了简单的均线交叉策略的编写,希望从这个例子中,大家可以学习认识下CTA函数。
交易类库具有2种模式,第一种是针对单品种的操作:
var p = $.NewPositionManager();
这里的p为创建的单品种控制对象。请注意哈,这里的单品种操作不是指的是只能针对于单一的期货品种,而是按顺序one-by-one的操作。通过使用NewPositionManager
对象提供的方法,可以方便地管理仓位和进行交易操作。可以传入一个可选的参数e,表示交易所对象,如果没有传入该参数,则默认使用当前交易所对象(exchange)。创建NewPositionManager
对象后,可以使用其提供的方法进行仓位和交易管理。
GetAccount: 获取交易账户信息。
GetPosition方法:根据合约类型、方向和持仓信息列表,获取特定合约的持仓信息。
OpenLong方法:开多仓,传入合约类型和数量。
OpenShort方法:开空仓,传入合约类型和数量。
Cover方法:平仓,传入合约类型和数量。
CoverAll方法:平掉所有仓位。
Profit方法:计算特定合约的盈利情况。
...
function main() {
var p = $.NewPositionManager();
Log('$.IsTrading("rb888"): ', $.IsTrading("rb888"));
p.OpenShort("rb888", 1);
Log('螺纹钢空头仓位', p.GetPosition("rb888", PD_SHORT));
Log('螺纹钢多头仓位', p.GetPosition("rb888", PD_LONG));
Sleep(60000 * 10);
p.Cover("rb888");
LogProfit(p.Profit());
}
我们举例示范一下:
首先,代码通过 $.NewPositionManager()
创建了一个名为 p 的单品种控制对象对象。并将contract的声明放到while循环之前。然后,代码使用 Log 函数输出了$.IsTrading("rb888")
的结果。$.IsTrading(symbol)
是交易类库中一个用于判断指定交易品种是否正在交易中的函数。IsTrading支持判断各种期货品种的交易时间段,比如夜盘或者周末假期等。
接下来,代码调用 p.OpenShort
方法开仓,以 “rb888” 和数量 1 开一手空头仓位。多头仓位可以使用OpenLong。相对于我们前面讲过的Exchange.buy
或者Exchange.sell
函数,这里对交易的代码细节进行了一些优化。这里我们不需要填写任何的价格,只需要填写品种和数量就可以。在模版参数里也有滑价点数,大家也可以根据需要调整下。请注意,不同的品种的跳动点数是不同的,比如大多数品种是一个单位的跳动,而有个别品种,比如铁矿石,跳动单位是0.5。另外,目前交易类库里的交易函数是不支持限价单的,如果需要限价单策略,可以使Exchange.buy
或者Exchange.sell
等交易函数。
紧接着,代码使用 Log 函数分别输出了 “rb888” 的空头仓位和多头仓位的信息。这里使用了GetPosition()
方法 用来获取指定交易品种和持仓类型的仓位信息。其中,PD_SHORT
和 PD_LONG
是表示空头和多头仓位。因为目前是空头的仓位,所以多头仓位信息返回值将会是null。随后,代码调用 p.Cover()
方法对空头仓位进行平仓操作。最后使用Profit方法输出了仓位操作后的盈利情况。
点击开始回测,我们来看一下回测结果,首先看到istrading信息为true。9点下了一手螺纹钢期货合约,由于是空头仓位,所以多头返回的仓位信息为空值。然后10分钟后,平掉空头,获利89元。这就是一个使用交易类库下单的简单例子。相对于前面我们讲过的交易函数,可以发现使用交易类库可以更加方便快捷的下单,省去了交易参数设置的烦恼。
以上就是交易类库的简单用法,下节课我们将要学习多品种操作的异步处理方法和风控模块,我们下节课再见!
我们继续交易类库的学习,其实在在学习交易类库的过程中,我们不仅仅希望了解如何使用交易函数,更重要的是理解程序化交易背后的理论和逻辑机制。通过深入了解量化交易的原理和逻辑,我们可以更好地完善自己的量化策略,制定出符合个人交易思维的量化策略。
第二种是针对多品种的操作。 var q = $.NewTaskQueue()
,q为创建的多品种队列控制对象。这里的多品种并不是意味着只能操作多个品种,而是多种操作同时处理,有点类似于并联的概念。这里涉及到了异步处理的概念。异步处理是一种编程模式,用于处理可能需要耗时的操作,在程序化交易中,市场信号的判断是很迅速的,然而价格的快速波动造成下单成交需要耗费更多的时间。在异步处理中,程序可以继续执行其他任务而不必等待比较耗时操作的完成。
这里给大家稍微解释一下同步处理和异步处理的不同。传统的同步处理方式会导致阻塞(blocking):就是当一个任务执行时,程序会一直等待该任务完成后再执行下一个任务。这可能导致程序运行效率低下,特别是在需要等待I/O操作完成时。而异步处理采用了非阻塞(non-blocking)方式,使得在执行比较耗费时间的操作时,程序可以同时执行其他任务。异步操作通常是通过回调函数等机制来实现的。
在异步处理中,当一个耗时任务启动后,程序立即将控制权返还给调用方,让其继续执行其他任务。然后,当耗时任务完成时,会触发相应的回调函数,用来处理任务的执行结果。由于异步处理不会阻塞主要流程,可以提高程序的性能和响应能力。相对于单品种的one-by-one串联的操作,异步处理可以较好的实现同时下达任务,各个任务在满足各自条件的情况下执行,所以各任务之间不会阻塞。因此,对于多品种或者多操作的复杂交易的场景,使用任务队列NewTaskQueue
是非常适合的。
我们来举例示范一下。
function main() {
Log('$.IsTrading("MA888"): ', $.IsTrading("MA888"));
Log("测试多任务队列");
// 多品种时使用交易队列来完成非阻塞的交易任务
var q = $.NewTaskQueue();
q.pushTask(exchange, "MA888", "buy", 3, function(task, ret) {
Log('task.desc: ', task.desc)
Log('ret: ', ret)
if (ret) {
q.pushTask(exchange, "MA888", "closebuy", 1, function(task, ret) {
Log("q task.desc: ", task.desc)
Log("q ret: ", ret)
q.pushTask(exchange, "MA888", "coverall", -1, 123, function(task, ret) {
Log("q task.desc: ", task.desc)
Log("q ret: ", ret)
Log("q task.arg: ", task.arg)
})
})
}
})
while (q.size() > 0) {
q.poll()
Sleep(1000)
}
}
我们来举例示范一下。这段代码是一个示例,展示了如何使用交易类库中的任务队列来处理多个非阻塞的交易任务。整体来说,这段代码中的异步处理体现在使用了任务队列$.NewTaskQueue()
以及q.pushTask()
方法来执行非阻塞的交易任务。
首先调用$.NewTaskQueue()
创建了一个任务队列q。然后使用q.pushTask()
方法将任务添加到队列中。其中,每个任务包含了交易相关信息和回调函数,用于处理任务完成后的返回结果。
pushTask具体的参数是这样的:
pushTask(symbol, action, amount, arg, onFinish)
其中symbol是期货合约,action是对应的操作,amount是操作的手数,arg是附加的参数,而onFinish是回调函数,也可以不传递。
如果设置了回调函数,如果当前任务成功执行,则继续调用q.pushTask()
添加下一个任务到队列中,并指定相应的回调函数。
在任务队列设置完成以后,while循环检查队列中是否还有待执行的任务。如果队列不为空,调用q.poll()
从队列中提取并执行下一个任务。在执行任务期间,使用Sleep(1000)
进行延迟等待,以便给交易操作留出时间。
通过使用任务队列和回调函数,可以实现交易任务的非阻塞执行。这种方式允许在等待交易结果的同时进行其他操作,提高了交易过程的效率和灵活性。了解到代码的整体思路以后,我们来看下具体的交易是怎样实现的:
函数Log('$.IsTrading("MA888"): ', $.IsTrading("MA888"));
首先判断MA888品种是否处于交易状态。
var q = $.NewTaskQueue();
创建一个新的任务队列对象q,用于存储交易任务队列。q.pushTask()
函数是将一个交易任务推入任务队列q中,当任务完成时,会执行回调函数内的代码。回调函数中的task参数代表当前任务,ret参数代表交易操作的结果。
首先来看第一个q.pushTask任务,这行代码将一个买入任务推入任务队列q中。使用buy操作买入MA888品种三手期货。当交易完成时,会执行定义的回调函数。在回调函数内部,使用log打印第一个任务的描述信息和交易操作的结果。
if (ret) {...}
使用条件语句判断交易操作的结果。如果结果为真(就是第一笔交易成功),则执行条件块内的代码。
第二个q.pushTask
任务。在条件块内,将一个平仓买入任务推入任务队列q中。该任务会平掉之前买入的一手。同样,当任务完成时,会执行定义的回调函数。
两个Log再次打印打印第二个任务的描述信息和平仓买入操作的结果。
第三个q.pushTask
任务是在上一个任务的回调函数内部,将一个全仓平仓任务推入任务队列q中。该任务会平掉所有持仓,参数-1表示全部平仓,参数123是任务的参数。当任务完成时,会执行定义的回调函数。
最后使用log函数打印出第三个任务的描述,结果和参数。
while (q.size() > 0) {...}
在任务队列定义完成以后,使用while循环,不断检查任务队列q是否还有未完成的任务。q.poll()
使用poll函数从任务队列q中取出一个任务,并执行该任务。执行任务后,任务会被移除队列。Sleep(1000)
暂停1秒,让程序等待一段时间再继续下一次循环。这样可以确保任务的逐步执行,而不是立即执行完所有任务。通过使用任务队列,可以实现非阻塞的交易任务处理。当一个任务完成时,会触发回调函数,并可以在回调函数中继续推入下一个交易任务。同时,通过循环和暂停的方式,确保任务按序执行。
我们来看下回测结果,可以看到,第一步收到任务对甲醛主力合约开多仓,数量为3。进行交易以价格2620买入了3手合约。返回任务的描述task.desc和任务结果ret,包括仓位的信息。第二步收到任务,平掉一手多仓。进行操作以价格2617卖出了1手合约。返回结果同样显示任务的描述和结果。第三步收到任务仓位全平。进行操作剩余2手合约的平仓。最后打印第三个任务的描述,结果和附加的参数。
大家是不是有些疑问,我们这里使用了一个异步处理的方式实现了一个onebyone按顺序的操作,其实这个例子是让大家理解任务队列的交易处理和回调函数的底层逻辑,大家也可以改下代码,比如第二第三个任务的回调函数可以去除,尝试实现异步的处理。
请注意,这段代码只是为了教学示范作用,具体的使用方式可能需要根据具体的交易类库进行调整和修改。另外,如果大家浏览交易类库的源码,可以发现这里的异步处理并不是真正的异步多线程操作。如果想进一步让策略程序真正并发执行,给JavaScript策略增加系统底层多线程支持,大家可以浏览下这篇帖子link。
下面我们来看下风控板块。交易类库不仅仅只有下单的作用,风险管理在资产管理中起着非常重要的作用,它有助于保护我们的利益并确保交易活动的安全性和可持续性。因此,我们可以熟悉一下风控板块。在模拟回测页面,我们可以看到是否开启风控的按钮,勾选为true。然后出现两个选项,“工作日最多交易次数”和“单笔最多下单量”。它们的作用是限制交易活动的频率和数量,以确保交易操作在可控范围内进行。
工作日最多交易次数:这个选项用于设定在一个工作日内最多可以进行的交易次数。设置一个合理的最大交易次数可以避免过度活跃的交易行为,降低交易成本手续费和风险。例如,如果将该选项设定为10,则表示在一个工作日内最多只能进行10次交易。这样呢,可以防止信号的频繁或者错误触发,造成的手续费的过度消耗。
单笔最多下单量:这个选项用于设定单笔交易中最多可以下单的数量。它可以限制交易的规模,以防止过度集中风险或错误操作。通过设定一个适当的最大下单量,可以确保每次交易的规模在可承受的范围内。例如,如果将该选项设定为10手,则表示单笔交易的最大下单量为10手。
这两个选项是根据个人或机构的风险偏好和交易策略来设定的。不同的交易者可能对风险管理有不同的要求,因此可以根据实际需要来调整这些参数。选择合适的工作日最多交易次数和单笔最多下单量可以帮助控制风险,确保交易活动在可控范围内进行。
一个好的交易函数可以集成风险管理工具和技术指标,帮助我们有效控制风险。通过在交易函数中实现止损、止盈、资金管理等功能,我们可以最大限度地降低交易风险,并保护资金免受不利市场波动的影响。在优宽平台,我们也有一些优秀的交易类库,比如止损类库,大家都可以学习参考下。
在期货市场中,成交量和持仓的变化与价格涨跌之间存在密切的关系,这种关系常被用作技术分析工具。然而,交易所只提供总体的持仓量和成交量数据,而没有提供详细的开平方向信息,所以对于成交量和持仓量具体的组成和变化趋势,我们并不能得到具体的统计。
由于在商品期货市场,CTP协议没有提供订单流数据。所以如果想做一些,基于订单流变化数据的策略,可能会无从下手。幸运的是,在CTP协议给出的tick行情有足够的数据可以反推出订单流,但是需要注意的是,这里反推出的订单流,也只是tick切片之间的成交情况的合并信息。大家在应用于实盘的时候,需要谨慎的判断。优宽国内站上公开了一个反推算法(使用JavaScript语言实现),该算法对于我们理解价格涨跌,和仓位变化背后的逻辑关系,具有重要的学习意义。在本节课中,我们将对这个公开的代码,进行详细分析,以便更好地学习该算法的逻辑。通过深入研究这个算法,我们可以更好地理解市场动态和参与者行为,为我们的交易决策提供一些有价值的参考依据。
我们经常在交易明细里可以看到,不停滚动着8种“开平”信息,这里给大家讲解下这八种类型。
订单薄有卖单列表,买单列表。卖单订单包括:「卖出开空」或者「卖出平多」。买单订单包括:「买入开多」或者「买入平空」。
盘口订单 | 下单方向1 | 下单方向2 |
---|---|---|
卖单 | 卖出开空 | 卖出平多 |
买单 | 买入开多 | 买入平空 |
4种订单类型的成交组合,会造成的持仓量变化,形成另外四种复合的“开平”信息:
方向类别 | 卖出开空 | 卖出平多 |
---|---|---|
买入开多 | 双开,持仓量增加 | 多换,持仓量不变 |
买入平空 |