输入/搜索内容
3
关注
224
关注者
使用JavaScript入门商品期货量化交易
知识库
创建于 2023-08-15 17:04:31  更新于 2025-08-05 11:49:58
 0
 4157

img

教程指南:该教程为优宽量化平台《使用JavaScript入门商品期货量化交易》配套教程文本,请配合视频一起使用,如果有错误,请及时提醒,我们后续将会不断完善该教程~~

企业微信:

img

1:Javascript基础语法

欢迎大家来到Javascript商品期货量化交易语言课程。在本课程中,我们将主要讲解使用JavaScript进行中国商品期货量化交易。

导入

JavaScript是一门跨平台的编程语言,它能够兼容各种操作系统、浏览器和设备,并且可以与多种编程语言无缝集成。与其他量化语言相比,比如专用于量化交易的My语言,JavaScript的适用范围更广泛,可以用于Web前端开发、后端服务架构、移动应用程序等各种领域。这样就大大降低了学习成本和使用难度。其次,JavaScript拥有操作灵活的特点。通过创建自定义对象和方法,可以轻松实现多样化的策略。比如对比于pine语言只能实现单一品种的量化策略,JavaScript可以实现多品种的对冲或者复杂的高频交易策略。同时,JavaScript还提供了许多内置函数,如金融分析TA库,数据计算Math对象、日期对象等,方便开发者快速编写代码。并且JavaScript的事件驱动编程模型以及异步IO机制保证了在大量交易请求下的快速处理和响应。在处理大量数据的情况下,使用JavaScript的效率与Python几乎一致。相比其他语言,JavaScript也更容易上手和维护。另外,JavaScript还拥有可视化方面的丰富生态资源,通过各种可视化图表插件,可以帮助交易者更加直观地展示数据结果,有助于做出正确的投资决策。

优宽量化交易平台作为一款专业的金融量化交易平台,提供了一个开放的策略编写和回测系统。JavaScript语言的优点可以与YOUQUANT量化交易平台紧密集成。通过使用YOUQUANT提供的API和组件,开发者可以更加高效地进行量化交易。而且优宽平台还提供了丰富的历史数据和实时行情数据,可以帮助开发者更好地进行回测和实盘交易。总之,JavaScript在操作灵活、便于编写、数据处理效率高和可视化结果输出等多方面具有突出的优势。因此,在量化交易领域使用JavaScript进行编程是一个非常不错的选择。

从今天起,我们就要开始使用Javascript语言进行商品期货量化交易的学习了。在本系列教程中,我们将从基础开始,逐步学习JavaScript编程语言的相关知识,并结合实际案例进行讲解。接下来,我们将会介绍JavaScript编程语言的常用数据类型的基础语法。首先我们看第一个概念:

标识符

JavaScript中的一切,包括变量、函数名和操作符,都区分大小写,也就是说变量名小写的test和大写的Test是两个不同的变量。标识符的第一个字符必须是字母、下划线(_)、美元符($),后面的字符还可以是数字,如下面所示,都是合法的标识符,如果使用非法的字符开头,JavaScript语言将会报错。

例如下面前五个是合法的标识符,最后两个标识符使用数字或者其他非法字符会报错。

javascript
test=1; Test =10; test9 = test; _demo =demo(); $Demo =demo(test9); 3xy=3; // 报错 ?xy=3; // 报错

数据类型

JavaScript一共有5种数据类型,分别是:未定义(Undefined)、空(对象Null)、布尔值(Boolean)、数字(Number)、字符串(String)。

  • Undefined只有一个值,即特殊的“undefined”,它代表一个还没有设置的值。比如我们只定义一个变量,不给这个变量设置值,那么该变量的值就是“undefined”。

  • Null只有一个值,即特殊的“null”,它代表一个被设置为空的值。比如我们先创建一个变量,然后把变量的值设置为“null”,那么反问该变量返回的值就是“null”。

  • Boolean有两个值,即“true”和“false”,“true”代表真,“false”代表假。需要注意的是,“true”和“false”都是小写。

  • Number也就是数字类型,包括:正数、负数、整数、小数等等。除此之外“NaN”也是一个特殊的数字,它专门表示未返回数值的情况,比如:1除以0,返回“NaN”。

  • String你可以理解为文字,包含中文和英文,可以通过单引号或双引号来构造字符串。比如:“YOUQUANT”或者‘优宽量化’等。

javascript
str; // 得到的值是:undefined,因为str没有定义 var str = null; str; // 得到的值是:null var isTrue = false; isTrue; // 得到的值是:false var num = -0.15; num; // 得到的值是:-0.15 var strs = "优宽量化(YOUQUANT)"; strs; // 得到的值是:优宽量化(YOUQUANT)

声明变量

在JavaScript中,const、var、let是三种声明变量的方式。

  • const:定义常量,不允许重复赋值。常量必须在声明时赋值。
javascript
const PI = 3.14159; PI = 3.14; // 报错
  • var:定义变量,在全局或函数作用域范围内都有效。声明后可以重复赋值,并且可以不进行初始化赋值,那么此时变量值为undefined。
javascript
var myName = 'Tom'; myName = 'Jerry'; // 赋值成功
  • let:定义块级作用域变量,在大括号{}包裹的一段代码块里面定义,只在该代码块内有效。声明后可以进行重新赋值操作,但是不能再次进行声明,就是同一个作用域内不能有相同变量名的let声明;必须先声明才能使用。示例代码:
javascript
let x = 10; if (true) { let x = 20; // 例如我们在代码块内和外都设置一个let,而在块内重新设置不会影响外部作用域中的变量x,这个仅在这个代码块中有效 console.log(x); // 如果在块内打印这个值,会输出20} console.log(x); // 而在块外,会输出10

因为let和const都是块级作用域的,所以它们常被用来代替var。通过缩小变量的范围并强制执行变量类型,以便更好地编写,阅读和理解代码。

对象

对象你可以理解为一个存放各种数据的容器,容器中属性和值都是对应的。可以通过new操作符先把这个容器创建出来。并且可以给创建后的对象添加属性和方法,比如:

javascript
var obj = {}; // 首先创建一个对象obj obj.name = "google"; // 给obj对象添加一个name属性,name的值是:“google” obj.age = 19; // 给obj对象添加一个age属性,age的值是:19 obj // 获取obj对象,结果是:{name:"google",age:19} obj.name // 获取obj的name属性,结果是:google obj["age"] // 还可以通过变量的方式,获取obj的age属性,结果是:19

数组

数组也是一个存放各种数据的容器,只不过容器中的元素是从左往右有序排列的,第一位的元素是0,第二位的元素是1,以此类推。另外JavaScript的数组可以存放任何数据类型,

javascript
var arr = ["YOUQUANT"100,{name:"优宽量化"},true]; // 创建一个数组,里面包含各种类型的数据; arr[0]; // 获取arr的第一个下标,结果是“YOUQUANT” arr[2].name; // 获取arr的第二个下标对象的属性,结果是“优宽量化”

运算符

JavaScript有多种运算符,包括算术运算符、比较运算符和逻辑运算符。其中算术运算符就是加减乘除的数学运算,比较运算符可以比较两个值是否大于或者小于,逻辑运算符主要有:与、或、非。

javascript
// 首先看算数运算符,加减乘除 var x = 5; var y = 2; var z1 = x+y; // 在以上语句执行后,z1的值是:7 var z2 = x-y; // 在以上语句执行后,z2的值是:3 var z3 = x*y; // 在以上语句执行后,z3的值是:10 var z4 = x/y; // 在以上语句执行后,24的值是:2.5 // 比较运算符,大于小于,等于和不等于 x>y; // 结果是:true x<y; // 结果是:false x != y; // 结果是:true x == y; // 结果是:false //逻辑运算符 x>y&&x>y&&x>y; // 结果是:true x<y&&x>y&&x>y; // 结果是:false x<y||x<y||x>y; // 结果是:true !(x == y); // 结果是:true

前面两种类型都比较容易理解,我们来解释一下最后的逻辑运算符:

  • && 是逻辑与,代表“并且”的意思。只有当所有条件都为 true 时,最终结果才为 true

  • || 是逻辑或,代表“或者”的意思。只要有任意一个条件为 true,最终结果就是 true

  • ! 是逻辑非,代表“否定”的意思。

优先级

运算符存在优先级,中学数学告诉我们:①如果是同一级运算,一般按从左往右依次进行计算。②如果既有加减、又有乘除法,先算乘除法、再算加减。③如果有括号,先算括号里面的。④如果符合运算定律,可以利用运算定律进行简算。JavaScript语言的优先级也是如此,如下面代码所示:

javascript
var num = 100*(10-1)/(10+5); num; // 计算结果是:60 1>2 && (2>3 || 3<5); // 运算结果是:false 1>2 && 2>3 || 3<5; // 运算结果是:true

本节课我们介绍了JavaScript语言的变量结构,下节课我们将要学习JavaScript的语法结构,我们下节课再见!

2:JavaScript语法结构

本节课我们继续学习JavaScript语言的语法结构部分。大家不用太过于担心语法过于复杂,语法就是使用程序的语言将我们的想法表达出来。针对于量化策略,我们只需要掌握基本的语法逻辑,就可以实现量化策略的编写了。在构建量化策略时,我们需要使用JavaScript语言中的变量、数据类型、运算符、函数等语法结构,来实现对市场数据的处理和判断,从而生成买入卖出信号。例如,在股票市场中,我们可以先获取各种指标数据,如收盘价、成交量等,通过JavaScript语言中的运算符进行计算,得出均线、RSI等指标,然后使用条件语句对这些指标进行判断,决定是否发出买入或卖出信号。同时,我们也可以使用JavaScript语言中的函数和循环语句,对策略进行封装和优化,提高策略的可读性和执行效率。总之,编写量化策略并不需要掌握非常复杂的JavaScript语法,只需要掌握基本的语法逻辑和一些常用的函数和语句,再结合市场的实际情况进行分析和判断,就能够实现一个简单有效的量化策略。

让我们看第一个概念,函数。

函数

JavaScript中的函数跟我们中学学的函数没有本质的区别,你可以理解为传进去什么,通过函数的计算,输出什么,如下图所示:

javascript
function add(numl, num2){ return numl+num2; } add(12);

其中add是函数名,num1,num2为函数的形式参数,用于接收函数调用时传入的实际参数。函数体用于实现具体的功能逻辑,并通过return语句返回结果。在这个例子中,我们定义了两个数字相加的函数,使用这个函数,传入两个参数,通过函数计算返回的结果是3。需要注意的是,JavaScript函数可以定义在全局作用域或函数内部,也可以将函数赋值给变量,这些都是JavaScript函数的灵活应用。

条件语句

通常在写代码时,我们总是需要为不同的决定来执行不同的动作。我们可以在代码中使用条件语句来完成该任务。

if 语句

只有当指定条件为 true 时,该语句才会执行代码。请使用小写的 if 。 使用大写字母(IF)会生成错误!

javascript
// 语法 if (condition){ // 当条件为true 时执行的代码 } // 例子 if (time<20) { x="Good day"; // 当时间小于晚上20:00时,生成问候"Good day" }

if...else 语句

如果有多种条件,进行判定时,可以使用ifelse语句。

javascript
// 语法 if (condition1){ // 满足condition1时执行 } else if (condition2){ // 满足condition2时执行 } else { // 其他情况下执行 } // 例子 if (time<20){ // 如果当时间小于晚上20:00 x="Good day"; // 生成问候"Good day" } else { // 否则 x="Good evening"; // 生成问候"Good evening" }

switch语句

Switch语句是一种流程控制语句,它可以根据不同的条件执行不同的代码块。它的基本语法如下:

javascript
switch(expression) { case value1: // 当expression的值等于value1的时候执行这里的代码块 break; case value2: // 当expression的值等于value2的时候执行这里的代码块 break; ... default: // 当所有case都不匹配的时候执行这里的代码块 }

其中,expression是要进行判断的表达式,可以是任何可以返回值的表达式,而每个case后面的value则表示要匹配的值。如果expression的值等于某个value,则执行该value所对应的代码块。如果所有的case都不匹配,则执行default默认语句块中的代码。

javascript
var fruit = "apple"; switch (fruit) { case "banana": console.log("这是香蕉"); break; case "apple": console.log("这是苹果"); break; case "orange": console.log("这是橙子"); break; default: console.log("未知水果"); break; }

我们声明了一个名为 fruit 的变量,并赋值为 "apple"。然后使用 switch 语句,在匹配到 fruit 的值时输出相应的文本内容。如果 fruit 的值是 "banana",则输出 "这是香蕉";如果是 "apple",则输出 "这是苹果";如果是 "orange",则输出 "这是橙子";否则输出 "未知水果"。由于 fruit 的值为 "apple",所以程序会输出 "这是苹果" 到控制台上。

循环语句

第一个for循环

有时候我们需要获取最近几天的 K 线数据,就需要从 K 线数组中,根据 K 线数据的位置依次获取,那么使用 for 循环是很方便的,如下面代码所示:

javascript
function main() { // 使用for循环的写法 for (var i = 1; i < 6; i++) { Log([i]); // 依次会打印出1,2,3,4,5 } }

While循环

While 是最常用的一种循环语句,它会在满足条件的情况下一直执行某段代码,直到条件不再满足。Condition 是一个返回布尔值的表达式,当这个表达式的值为 true 时,循环体内的代码会被执行。每次执行完循环体内的代码后,condition 会再次被判断。如果 condition 的值仍为 true,则循环会继续执行,否则循环停止。

javascript
while (condition) { // 循环体内执行的代码 } var i = 1; // 定义一个变量 i,初始值为 1 while (i <= 10) { // 当 i 的值小于等于 10 时,执行循环体 console.log(i); // 输出 i 的值 i++; // 将 i 的值加 1 }

我们首先定义了一个变量i,初始值为1。然后使用while循环,在i的值小于等于 10 时执行循环体内的代码。在循环体内,我们首先输出变量i的值,然后将 i 的值加 1。这样可以保证每次循环,都会输出i的值并且i的值逐渐增加,最终达到循环结束的条件。最终,程序输出从 1 到 10 的所有整数。

我们都知道行情是在不断变化的,如果你想获取最新的K线数组,就得不断的去一遍又一遍地运行相同的代码,那么使用while循环,只要指定条件为true,循环就可以一直获取最新的 K 线数组。

javascript
/*backtest start: 2022-05-18 09:00:00 end: 2022-05-18 09:02:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] */ function main(){ exchange.SetContractType("MA888"); // 设置合约 while(true){ Log(exchange.GetRecords()); // 获取K线数组 } }

通过exchange.SetContractType("MA888")设置交易合约类型为 "MA888"。使用while循环,不断执行以下操作:通过exchange.GetRecords()获取最近的K线数组。使用Log()函数输出获取到的K线数组信息。以上使用到的函数是优宽量化内置的API函数,我们后续会逐一讲解。该段代码的作用是不断地获取交易所指定合约的最新K线数据,并将其输出到控制台上进行查看和分析。相应的策略可以在此基础上进行开发和实现。

break语句和continue语句

循环是有前提条件的,只有这个前提条件为“true”的时候,循环才会开始重复的做某些事,直到这个前提条件为“false” 的时候,循环才会结束。 但是break语句可以在循环执行的过程中立刻跳出循环;continue语句可以中断某一次循环,然后继续下一次循环。 如下面代码所示:

javascript
// break语句 var arr = []; for (i = 0; i < 10; i++) { if (i == 9) { break;} arr.push(i); } console.log(arr); // 结果是:[0,1,2,3,4,5,6,7,8] // continue语句 var arr = []; for (i = 0; i < 10; i++) { if (i == 3) { continue;} arr.push(i); } console.log(arr); // 结果是:[0,1,2,4,5,6,7,8,9]

首先看break语句的用法。在上面的代码中,对于变量 i 的取值从 0 到 9 进行循环遍历,每次循环时判断当前 i 的值是否等于 9,如果相等,则执行break语句,跳出整个循环体。因此,最后输出的数组 arr 只包含 0 到 8 这些元素。

再看continue语句的用法。在上面的代码中,对于变量 i 的取值从 0 到 9 进行循环遍历,每次循环时判断当前 i 的值是否等于 3,如果相等,则执行continue语句,跳过本轮循环的剩余部分,直接进入下一轮循环。因此,最后输出的数组 arr 不包含值是 3 的元素。

作用域

作用域是指能够访问某个变量或函数的代码区域。在JavaScript中,作用域分为全局作用域和函数作用域。

全局作用域

全局作用域是指在整个程序中都可以访问到的变量和函数。只要在程序的任何地方定义了一个变量或者函数,它就属于全局作用域,可以在任意地方被调用。

javascript
var name = "Tom"; // 全局变量 function sayHello() { console.log("Hello, " + name + "!"); } sayHello(); // 输出 "Hello, Tom!" 到控制台上

在上面的代码中,namesayHello() 函数都是在全局作用域中定义的,可以被任意地方访问。

局部作用域

局部作用域是指在函数内部定义的变量和函数。只有在函数内部才能访问这些变量和函数,不能在函数之外的地方使用。

javascript
function test() { var score = 90; // 局部变量 function printScore() { console.log("Your score is " + score); } printScore(); // 输出 "Your score is 90" 到控制台上 } test();

在上面的代码中,score 变量和 printScore() 函数都是在 test() 函数内部定义的,只能在函数内部使用。

作用域链

当在函数内访问一个变量时,JavaScript引擎会先搜索函数内部的局部作用域,如果找不到则向上查找,直到找到为止。这种搜索的顺序被称为作用域链。

javascript
var name = "Tom"; // 全局变量 function test() { var name = "Jerry"; // 局部变量 function printName() { console.log("My name is " + name); } printName(); // 输出 "My name is Jerry" 到控制台上 } test();

这段代码中name有全局变量和局部变量。在上面的代码中,当调用printName()函数时,JavaScript引擎会搜索函数内部的局部作用域,发现了名为name的局部变量,因此输出的结果是 "My name is Jerry"。

注释

注释包括单行注释和多行注释。单行注释以两个斜杠开头,而块注释,也就是多行注释,以斜杠和星号框住注释内容,如下面所示:

javascript
// 单行注释 /* * 多行注释 */

语句

每个语句都有一个分号结尾;虽然这不是必须的,但我们还是建议任何时候都不要省略它。因为加上分号,在某些情况下可以增加代码的性能。

javascript
a = 1; // 语句以分号结束 b(); // 语句以分号结束 a = 1 // 也可以省略分号,但是不建议 b() // 也可以省略分号,但是不建议

有关于JavaScript的语法结构就为大家介绍到这里了,其实语法没有那么复杂的,重要的是使用代码将我们的交易理念进行实现,打造出有用的交易系统。当然,JavaScript作为一门语言,还有很多其他的方面,例如异步编程、原型继承、模块化等等,这些将在后续的实战教学中为大家展开介绍。

3:获取k线数据

课程导入

在期货市场中,每一个品种的价格都随着时间的推移而发生变化。金融量化就是利用计算机来分析这些期货品种价格变化背后的规律,方便来预测未来的价格走势并做出最合适的投资决策。在这个过程中,数据起着至关重要的作用。对于金融量化来说,我们需要收集大量品种价格的历史数据,并把它们转换成数字形式,然后方便用计算机进行分析。通过对大量的历史数据进行分析,我们可以找到隐藏在价格波动中的规律和趋势,并根据这些规律和趋势制定投资策略。

在大多数情况下,使用JavaScript直接获取数据需要额外安装相关的模块,也可以调用相应的交易所或者第三方数据平台API接口。而这些操作都比较复杂,需要进行繁琐的配置。但是,在优宽量化平台上,用户可以直接通过编写JavaScript代码来获取期货数据,而无需进行额外的模块安装或者API配置。优宽量化平台内置了一系列期货数据获取函数,包括获取历史数据、实时数据、基本信息等等,并且提供了详细的文档和示例代码,方便我们快速上手和使用。

首先,我们需要打开优宽量化平台,当然这需要我们注册账号才可以登录。作为新手,我们要进入模拟回测的板块。这里我们可以使用历史数据来运行策略,检验我们的策略的逻辑、策略收益方向等基本情况在历史数据中盈利如何。对于模拟回测来说,这里是不需要配置交易所和托管者的,当然如果你想使用你的策略进行模拟实盘,或者真实实盘的运行,优宽量化平台也是支持的。我将在评论区附上实盘配置教程,大家可以谨慎尝试。

在优宽量化平台,额外的策略编辑器、库和模块是不需要安装的,我们在浏览器端就可以进行策略的编写。点击控制中心新建策略按钮,就到了我们的策略编写平台。这里我们看到了一个main入口函数,请注意这个是必须的,如果没有main函数,日志信息将会报错。我们需要将主要的逻辑策略放在这个函数中,方便系统识别我们的策略代码。

k线介绍

我们来介绍一下K线。K线(K-Line)即蜡烛图,是一种广泛应用于股票、期货、外汇等金融市场的技术分析工具。它通过绘制价格走势、开盘价、收盘价、最高价和最低价等信息,它是按照时间顺序形成一根根“蜡烛形状”的图表,来描绘出市场的交易情况与趋势。在趋势交易策略中,K线图可以用来确认和预测市场的趋势变化。通常使用较长时间周期的K线图,如日线、周线或月线等,判断市场是否呈现出明显的上升或下降趋势,并相应地制定投资决策。而在高频交易策略中,K线图通常被用于构建高频交易模型,预测未来的市场波动趋势并进行交易。同时,高频K线图也可以用于快速识别买卖机会,尤其在股票及期货领域中。高频交易的k线周期更短,一些投资者会通过以秒为单位或者分钟为单位的K线图发现价格的快速反弹或下跌机会。无论是趋势交易策略还是高频交易策略,K线都是一个重要的分析工具。投资者应该根据自己的投资风格、交易周期和市场情况选用相应的K线周期进行分析,并结合其他技术指标综合分析,这样可以获取更全面、准确的市场信息和交易信号。

一根k线的样子

我们看到的k线经常是蜡烛图的形式。从数字的角度,我们来了解一下k线数据是什么样子的。设置周期时间为一分钟,使用以下代码,读取k线数据,看看是什么样子的。

javascript
function main(){ exchange.SetContractType('rb888'); var records = exchange.GetRecords(); var timeStamp = new Date().getTime(); var timeStr = _D(timeStamp); Log('时间:', timeStr) Log('K线数据',records) }

这段 JavaScript 代码定义了一个名为main的函数,当该函数被调用时,会向请求指定期货品种的 K 线数据,并将获取到的数据打印到日志中。

具体来说,该函数的主要代码包括以下部分:

  • exchange.SetContractType('rb888'):设置交易所中当前操作的期货合约品种为“rb888”,就是螺纹钢主力期货,这是优宽量化平台定义的虚拟主力合约代码,例如现在呢,它就是rb2310合约,下一个主力合约周期会变为rb2401,这可以帮助策略主动的进行移仓换月。

  • var records = exchange.GetRecords():使用 exchange 对象的 GetRecords 方法获取当前期货品种的 K 线数据,并将获取到的数据保存到变量 records 中。

这里我们了解一下k线数据的获取。程序在使用 CTP 协议获取期货交易数据时,先需要与期货公司的前置机建立连接,并登录交易账号。之后,程序可以通过 CTP 接口提供的函数向前置机发出请求,例如查询特定合约的行情数据、订阅特定合约的行情更新等。此时,前置机会将请求转发到期货交易所的服务器,获取数据并返回给程序。在此过程中,前置机还需要对客户端的请求数据进行校验和过滤,以确保数据的合法性和安全可靠性。因此,CTP 协议使得程序能够直接与期货交易所交互,快速地获取数据并进行相应的操作,同时也要求程序和期货公司严格遵循协议规范,确保数据传输的安全和稳定。

  • var timeStamp = new Date().getTime():获取当前时间戳,即当前时间距离 1970 年 1 月 1 日 00:00:00的毫秒数。

  • var timeStr = _D(timeStamp):将时间戳转换为我们可读的日期格式,即'年-月-日 时:分:秒',并赋值给变量timeStr

  • Log('K线数据',records):使用Log函数将获取到的 K 线数据输出到日志中,以便于开发者进行调试和分析。

可以看到返回结果是这样的:

javascript
时间: 2023-05-26 09:00:22 K线数据 [{"Time":1685062800000,"Open":3424,"High":3424,"Low":3419,"Close":3419,"Volume":7798,"OpenInterest":2132694}]

这里看到返回的是一个数组的形式,这中间包含的都有 时间戳(这是从1970年1月1日以来的毫秒数)、开盘价(open)、最高价(high)、最低价(low)、收盘价(close)、成交量(volume)和持仓量(OpenInterest)。这就是一根K线柱的样子。

获取时间流k线

我们看到返回结果的时间是09:00,证明接收到了一根k线数据,就立马进行返回,然后结束了代码。金融数据在交易时间段是瞬息万变的,如果想在设定的时间周期内持续的获得数据,就要使用while循环。

javascript
function main(){ while (true){ exchange.SetContractType('rb888'); var records = exchange.GetRecords(); var timeStamp = new Date().getTime(); var timeStr = _D(timeStamp); Log('时间:', timeStr) Log('K线数据',records) } }

这里使用了一个while (true)无限循环,来持续不断地请求并输出 K 线数据。可以看到返回结果中以累计数组的形式每分钟都会返回三个或者四个k线数据。K线BAR是根据Time时间由远到近按顺序排列。从这段代码我们可以理解JavaScript中数据的获取机制。在策略运行期间内,每次k线数据更新,records也会更新,就获取到了实时的数据,方便策略的下一步运行。

获取固定周期k线

在我们写一些策略时经常有这样的场景,我们要在每根K线周期完成时处理一些操作,或者是打印一些日志。我们怎么实现这样的功能呢? 我们判断一根K线柱周期完成了,我们可以从K线数据中的时间属性入手,我们每一次获取一次K线数据,我们就判断一次这个K线数据的最后一个K线柱的数据中Time这个属性值是不是发生了变化,如果是发生变化,即代表有新的K线柱产生(证明新产生的K线柱的前一根K线柱周期完成),如果没有发生变化,即代表没有新的K线柱产生(当前的最后一根K线柱周期还没有完成)。

javascript
function main(){ var lastTime = 0 while (true){ exchange.SetContractType('rb888'); var records = exchange.GetRecords(); if (records[records.length - 1].Time != lastTime) { Log("新K线柱产生") Log('lastTime:',lastTime) Log('K线数据',records[records.length - 1]) lastTime = records[records.length - 1].Time // 一定要更新 lastTime ,这个至关重要。 } } }

所以我们要有一个变量用来记录K线数据的最后一根K线柱的时间。这里定义一个变量lastTime,用于记录上一次获取到的 K 线数据的时间。初始值为 0,这里使用if语句判断最后一个 K 线柱的时间是否和上一次获取到的 K 线柱的时间相同,如果不同则表示产生了新的 K 线柱。使用Log函数输出一条日志,表示新的 K 线柱已经产生。使用Log函数输出上一次获取到的 K 线柱的时间。records是按照顺序由远到近排列的,因此为获取最新的k线,需要根据索引[records.length - 1]进行获取。使用Log函数输出最新的 K 线柱的数据。这里将变量lastTime更新为最新的 K 线柱的时间,方便于下一次判断是否产生了新的 K 线柱。

在本节课的最后,为大家留下一个小测试,在优宽量化平台模拟回测参数这里,数据具有模拟级别和实盘识别两种类型,大家可以尝试一下有什么区别。另外,如果我想打印不同周期的数据,比如1秒,15秒的数据,参数应该怎样设置?下节课我将为大家进行解答,另外,在各类的期货软件中,我们经常可以看到vip用户可以解锁更多的数据进行分析,而在优宽量化平台,我们可以通过GetTicker()GetDepth()获得更详细的数据进行金融模型的搭建。下节课也将为大家进行介绍。

4:获取更多数据

上节课我们学习了如何在优宽量化平台,使用JavaScript语言获取k线数据。本节课我们先就上节课留下的疑问给大家解答一下。

实盘级 vs. 模拟级

对于实盘级tick和模拟级tick的区别,这其实涉及到优宽量化交易平台的回测机制。平台将回测模式分为实盘级回测和模拟级回测。

  • 模拟级别回测
    模拟级别回测是按照回测系统的底层K线数据,按照一定算法在给定的底层K线Bar的最高价、最低价、开盘价、收盘价的数值构成的框架内,模拟出ticker数据插值到这个Bar的时间序列中。

  • 实盘级别回测
    实盘级别回测是真实的ticker级别数据在Bar的时间序列中。对于基于ticker级别数据的策略来说,是真实记录的数据,并非模拟生成,所以使用实盘级别回测更贴近真实。

我们使用代码尝试一下:

javascript
function main(){ while (true){ exchange.SetContractType('rb888'); var records = exchange.GetRecords(); Log('K线数据',records) } }

设置模拟级tick,可以看到每分钟返回三到四个模拟级别的数据(注意这不是一定的,会根据k线的变化发生改变)。而设置实盘级tick,可以看到每秒钟都会返回两个实盘数据,数据量远远超过模拟级别的数据。所以呢,如果大家想进行高频策略的搭建,使用实盘级tick可能更加合适,当然在一定程度上会增加计算的复杂程度,降低回测的速度;而使用模拟级tick对于趋势策略更加友好一下,回测速度将会大大提升。

获取不同周期K线

下面我们来看下怎样获得任意周期的K线。在上一节使用exchange.GetRecords()函数时,这里面没有指定参数,所以按照实盘参数上设置的K线周期或者回测页面设置的K线周期返回对应的K线数据。如果想设定不同周期的参数,就要进行指定。

固定的参数Period有以下的选择:

  • PERIOD_M1:指1分钟
  • PERIOD_M5:指5分钟
  • PERIOD_M15:指15分钟
  • PERIOD_M30:指30分钟
  • PERIOD_H1:指1小时
  • PERIOD_D1:指一天
    ...

参数Period的值可以指定以上定义的标准周期,当然还可以传入数值,数值的单是为秒。这里我们首先定义period为15秒,请注意,这里要选择为实盘级tick。因为模拟级的底层K线周期为1分钟,所以获取固定周期的模拟级tick需要大于一分钟的策略周期。

javascript
function main(){ var lastTime = 0 while (true){ exchange.SetContractType('rb888'); var records = exchange.GetRecords(15); if (records[records.length - 1].Time != lastTime) { Log('K线数据',records[records.length - 1]) lastTime = records[records.length - 1].Time } } }

可以看到结果为每15s打印一个k线bar数据,这样就可以根据你的需要设定不同的k线周期。

:升级GetRecords函数,除了支持symbol参数直接指定请求的K线数据的品种信息。保留了原有的period参数用来指定K线周期,还增加了一个limit参数用来指定请求时期望的K线长度。同时也兼容旧版本的GetRecords函数只传入period周期参数的调用方式。

exchange.GetRecords()函数的调用方式:

  • exchange.GetRecords()
    不指定任何参数时请求当前合约代码对应的品种的K线数据,K线周期是策略回测界面或者实盘时设置的默认K线周期。
  • exchange.GetRecords(60 * 15)
    仅指定K线周期参数时,请求当前合约代码对应的品种的K线数据。
  • exchange.GetRecords("rb2410")
    仅指定品种信息时,请求指定品种的K线数据,K线周期是策略回测界面或者实盘时设置的默认K线周期。
  • exchange.GetRecords("MA888", 60 * 60)
    指定品种信息,指定具体K线周期请求K线数据。
  • exchange.GetRecords("i888", 60, 1000)
    指定品种信息,指定具体K线周期,指定期望获取的K线长度请求K线数据。
javascript
function main() { var symbols = ["MA888", "rb2410", "i2409", "hc2410"] Log("当前默认K线周期", exchange.GetPeriod()) for (var i = 0; i < 10; i++) { if (exchange.IO("status")) { var index = 0 for (var symbol of symbols) { var info = exchange.SetContractType(symbol) Log(info) var r = null if (index == 0) { exchange.SetMaxBarLen(2000) // 测试 r = exchange.GetRecords() // 测试不带参数 Log("索引:", index, ",合约:", symbols[index], ",BAR间隔:", r[1]["Time"] - r[0]["Time"], r[2]["Time"] - r[1]["Time"], "最新价格:", r[r.length - 1]["Close"]) } else if (index == 1) { r = exchange.GetRecords(symbol, 60 * 5) // 测试指定symbol period Log("索引:", index, ",合约:", symbols[index], ",BAR间隔:", r[1]["Time"] - r[0]["Time"], r[2]["Time"] - r[1]["Time"], "最新价格:", r[r.length - 1]["Close"]) } else if (index == 2) { // r = exchange.GetRecords(symbol, 60 * 60 * 24, 999) r = exchange.GetRecords(symbol, 60, 999) // 测试指定symbol period limit Log("索引:", index, ",合约:", symbols[index], ",BAR间隔:", r[1]["Time"] - r[0]["Time"], r[2]["Time"] - r[1]["Time"], "最新价格:", r[r.length - 1]["Close"]) } else if (index == 3) { r = exchange.GetRecords(symbol) // 测试指定symbol Log("索引:", index, ",合约:", symbols[index], ",BAR间隔:", r[1]["Time"] - r[0]["Time"], r[2]["Time"] - r[1]["Time"], "最新价格:", r[r.length - 1]["Close"]) } index++ if (info && r) { Log(symbol, "r.length:", r.length, "#FF0000") } } return } Sleep(1000) } }

获取更多数据

金融数据中通常会使用Bar级别和Tick级别来表示市场行情的时间间隔和粒度。Bar级别是按照固定时间间隔来划分的,所以每根K线包含了一定时间范围内的价格波动信息,能够比较好地反映出市场趋势和变化。而Tick级别则更注重于记录市场每次报价和成交的价格和数量,更适合进行短期交易和高频交易策略的分析和实现。在同样的时间范围内,Bar数据的数量要比Tick数据少得多。这是因为一个时间范围内可能只会有几次成交,但是它们会被归入同一根Bar中,而Tick级别则会记录所有的报价或成交信息。

优宽量化的回测中策略程序是完整的控制流程,程序是在按照一定的频率不停的轮询。各个行情、交易 API 返回的数据也是按照调用时刻,模拟实际运行时的情况,所以属于onTick级别,并不是其它回测系统的onBar级别,这样更好的支持了基于Ticker数据的策略的回测,也就是操作频率较高的策略。当然我们也可以设置成为onBar级别的数据,可以通过按照固定周期进行数据获取。

Ticker信息

为获取ticker数据,我们可以使用GetTicker()函数获取当前合约对应的市场当前行情。

javascript
function main(){ exchange.SetContractType("rb888") var ticker = exchange.GetTicker() Log(ticker) }

我们看到回测结果对比GetRecords返回的信息更多,包含以下字段:

  • Info: 交易所接口返回的原始数据,回测时无此属性。
  • Symbol: 品种代码。
  • Time:时间戳,表示数据产生的时间,单位为毫秒。
  • High:最高价,表示该交易品种在这个时间段内的最高价格。
  • Low:最低价,表示该交易品种在这个时间段内的最低价格。
  • Sell:卖一价,表示当前的卖一价信息。
  • Buy:买一价,表示当前的买一价信息。
  • Last:最新价格,表示该交易品种最近一次成交的价格。
  • Volume:成交量,表示该交易品种在这个时间段内的成交量。
  • OpenInterest:持仓量,表示该交易品种在这个时间段内的持仓量。

对比K线数据来说,ticker 数据具有更高的时效性,包含更为详细和准确的信息,但是呢,ticker 数据也有相应的缺点,主要表现为数据量大、噪声干扰等问题,需要我们进行有效的处理和过滤。

新版行情函数exchange.GetTicker()升级增加symbol参数,使得该函数可以脱离当前合约代码直接按照参数指定的合约代码,请求行情数据。简化了代码编写过程。同时依然兼容不传参的调用方式,最大程度兼容平台旧策略。

订单薄信息

市场是由买方和卖方共同构成的,而价格受供求关系的影响,为分析市场各个深度的供求关系,GetDepth()可以用来获取当前合约对应的市场的订单薄数据。

javascript
function main(){ exchange.SetContractType("rb888") var depth = exchange.GetDepth() Log("深度数据:", depth) }

根据回测结果深度数据,包含以下两个字段:

  • Asks:卖方深度信息,其中每一项表示每个价格的卖出量情况,包含以下两个字段:Price:价格,表示该价格的卖出报价;Amount:数量,表示该价格的卖出量。

  • Bids:买方深度信息,其中每一项表示每个价格的买入量情况,包含以下两个字段:Price:价格,表示该价格的买入报价;Amount:数量,表示该价格的买入量。

这里需要注意下,除去上期货品种(五档行情),大多数商品期货只包含一档的数据。具体地,卖方深度信息表示当前市场上正在出售某种交易品种的报价和数量,买方深度信息则表示当前市场上正在购买某种交易品种的报价和数量。另外,回测系统中,使用模拟级 Tick回测时exchange.GetDepth()函数返回的数据各档位均为模拟值。 回测系统中,使用实盘级 Tick回测时exchange.GetDepth()函数返回的数据为秒级别深度快照。通过深度数据,我们可以了解市场上的买盘和卖盘情况,从而判断市场的供求关系和价格趋势,进行相应的交易决策,如撤单、下单等,为后续的量化交易策略制定提供重要的依据。

GetTicker函数相同,exchange.GetDepth()函数此次也增加了symbol参数。可以实现在请求深度数据时直接指定品种。

市场成交记录

还有一种市场成交记录信息GetTrades()函数在商品期货中是不支持的,但是我们可以根据ticker数据反推逐笔交易历史。优宽量化平台策略广场里面有代码可以参考一下 链接

本节课我们主要讲解了实盘级tick和模拟级tick的区别,以及如何获取不同周期的K线和更多数据。对于高频交易策略,使用实盘级tick更为合适;而对于趋势策略,使用模拟级tick则更友好。同时,我们也可以利用GetTicker()函数获取当前合约的市场当前行情,以及使用GetDepth()获取市场订单薄数据,从而获得更多的交易信息。希望大家能够掌握以上内容,为后续量化策略的制定打下坚实的基础。

5:技术指标的计算

在上一节课中,我们掌握了金融数据的获取方法。今天,我们将进一步学习技术指标的计算。量化策略是通过分析和利用数据、算法、编程技术等手段来进行投资和交易的策略。而技术指标是量化策略中需要格外关注的数据。在金融市场中,技术指标是交易者评估市场走势、制定交易计划、采取交易决策的重要依据之一。通过对市场指标的计算和分析,交易者可以获取市场的趋势、震荡区间和关键价格点等信息,进而为交易提供有力的支持。

技术指标的计算是量化策略的核心步骤之一。技术指标一般都基于历史数据进行计算和推导,如均线指标、相对强弱指数(RSI)、布林带指标、移动平均协整模型(MACD)等。这些指标都是通过对历史价格、交易量等市场数据进行统计和计算得出来的,具有较高的参考价值,并且可以被广泛应用于各类交易市场。

首先我们了解一下均线,均线是股票、期货等金融资产价格分析中常用的技术指标,它是一种趋势线,可以平滑地反映出金融资产的价格走势。均线的计算方法是将一定时间内的收盘价加总,然后再除以该时间段,从而得到平均值。例如,5日均线就是过去5个交易日的收盘价的平均值。在设计策略中,计算指标时需要考虑K线长度是否满足指标参数。

在优宽量化中,技术指标的计算可以使用TA指标库和talib指标库,两个库都优化了常用指标算法,支持 JavaScript、 Python、 C++语言。

javascript
function main(){ while(True) exchange.SetContractType("MA888") r = exchange.GetRecords if len(r) < 10 : continue ma1 = TA.MA(r, 5) ma2 = TA.MA(r, 10) Log("五日均线", ma1[r.length-1]) Log("十日均线", ma2[r.length-1]) }

在上述代码中,我们使用了优宽量化提供的 TA 指标库中的 MA 移动平均函数,其中 r 代表 K 线数据,5 和 10 分别为我们要计算的5日均线和10日均线的时间间隔。

第二个例子我们来看下指数移动平均值的计算,这次我们使用talib库。

javascript
function main() { while(true){ exchange.SetContractType("MA888") r = exchange.GetRecords(); if (r.length < 10){ // 需要超过K线的长度10 return;} ret = talib.EMA(r,9); Log(ret); } }

技术指标是使用原始的数据(包括开盘价、最高价、最低价、收盘价、成交量等信息) 为基础,通过一定的数学计算得出的结果。优宽量化把常用的技术指标封装成了一个个函数,大家在编写策略时就不需要重新计算,可以提高策略开发效率。使用 talib.EMA 函数,我们以9作为周期计算了指数移动平均线。在均线系统中还有双指数移动平均线(talib.DEMA)、适应性移动平均线(talib.KAMA)等其他常用指标,这也是均线系统常用的工具,大家可以尝试下。通过计算不同类型,不同时间段的均线,我们可以观察和比较价格的趋势及其变化,有助于判断市场的涨跌和未来的价格走势。

下面我们来举例示范下KDJ(随机震荡指标)技术指标的计算。在量化交易中,KDJ是一种常用的技术指标。它是以收盘价和最高价、最低价计算出来的,并且可以反映当前价格与历史价格的相对关系,和价格的动量特征,这个指标被广泛应用于短线交易和日内交易中。在KDJ指标中,K、D、J 分别代表指标中的三个数值:

  • K:表示最近一段时间内收盘价与这段时间内最低价的比值,通常使用三天作为参数;
  • D:是 K 值的平均数,通常使用九天作为参数,是 K 值的平滑处理结果;
  • J:通过 K 和 D 的数值计算得到,通常采用 3 * D - 2 * K 的计算公式,可以看做是 K和D的加权平均数,取值范围一般为 0 ~ 100,可以用来判断当前价格的力度和趋势方向。

如果使用原始的JavaScript代码进行计算,可以想象分别计算三个指标的代码会比较复杂。而使用指标库函数非常简单,以TA.KDJ函数指标,我们进行一下代码的编写。

javascript
function main(){ while(true){ exchange.SetContractType('rb888') r = exchange.GetRecords() kdj = TA.KDJ(r, 9, 3, 3) Log("k:", kdj[0][r.length-1], "d:", kdj[1][r.length-1], "j:", kdj[2][r.length-1]) } }

这段代码是一个量化交易系统的示例,主要使用了 TA 指标库中的 KDJ 函数。具体来说,我们设置合约为 rb888,使用while不断读取它最新的k线数据;然后TA.KDJ函数接收 K 线数据,通过设置参数(9,3,3),返回对应的 KDJ 指标值计算结果。最后,因为返回结果是一个二维数组,所以通过数组下标取值方式,获取最新时刻的 KDJ 指标值,使用Log输出结果。

除了KDJ指标以外,还有许多其他常用的指标函数,例如MACD、RSI等。这些指标都是通过对历史市场数据进行计算和处理而得到的,并被广泛应用于量化交易中。下面是一个用 TA 库计算 MACD 和 RSI 指标的示例代码:

javascript
function main() { while (true) { exchange.SetContractType("rb888"); var records = exchange.GetRecords(PERIOD_D1); var macd = TA.MACD(records, 12, 26, 9); var rsi = TA.RSI(records, 14); Log("MACD:", macd[0][records.length - 1], "DIF:", macd[1][records.length - 1], "DEA:", macd[2][records.length - 1]); Log("RSI:", rsi[records.length - 1]); } }

该代码与之前的示例非常相似,也是使用while循环获取最新的 K 线数据,然后分别计算出周期为 12 和 26 的 MACD 指标和周期为 14 的 RSI 指标。通过数组下标取值方式获取最新时刻的 MACD 指标和 RSI 指标,然后使用 Log 函数输出。

大家可以注意到,在上述的技术指标计算中,我们使用的都是默认的参数。其实在量化交易中,参数的大小,往往决定着策略最后的绩效结果。通过优化外部参数,可以使策略及时适应当前的市场行情。另外策略交互也可以通过手动的方式,给机器人发出各种指令,方便策略维护。参数实际上就是变量,如果将变量固定的写到策略代码中,这样每次调试策略的时候,就需要在代码中修改这个变量,这样看起来非常不灵活。 并且没有办法对这个变量进行优化处理。那么外部参数就很好的解决了这个问题,在优宽量化平台中,策略参数是以全局变量形式使用。JavaScript语言中可以直接访问策略界面上设置的参数数值或者修改。

策略参数有以下几种,数值,字符串,下拉框,布尔值和加密字符串。

在策略编辑页面设置的不同种类的的参数:

  • 描述选项: 界面参数在策略界面上的名字。
  • 备注选项: 界面参数的详细描述。
  • 类型选项: 该界面参数的类型。
  • 默认值选项: 该界面参数的默认值。

这里我们设置了五种类型的参数作为范例给大家展示一下。我们举例示范下怎样使用。例如这段代码中,我们设置可调参数为策略周期'period'和期货品种'contract',在模拟回测界面我们可以进行自主的选择修改,然后代码就可以根据我们的需要呈现结果。

image

javascript
function main() { while (true) { exchange.SetContractType(contract); var records = exchange.GetRecords(); var rsi = TA.RSI(records, period); Log("RSI:", rsi[records.length - 1]); } }

另外,也可以设置一个参数,让另一个参数基于该参数的选择,实现显示与隐藏。 比如我们设置参数numberA,是一个数值类型。 我们让numberA基于一个参数: isShowA(布尔类型)的真假决定numberA显示与隐藏。 需要把numberA 变量在界面参数上设置为:numberA@isShowA。这样,不勾选 isShowA 参数,numberA参数就隐藏了。当我们的策略使用的参数比较多的时候,策略界面参数同时也具有分组功能,只用在开始分组的参数的描述开头加上 (?第一组)即可。参数分组多品种或多周期等投资组合中,可以很方便的针对不同的品种使用不同的参数组,并且无需在原有的参数上再次修改。大家注意下,这里的符号要使用英文的。

当然,参数的调优是使用指标函数时非常重要的一步。在实际操作中,不同的交易市场和品种都有自己的特点,因此同样的指标函数,在不同的市场和品种中可能需要不同的参数设置,以达到最佳的效果。这也是为什么在量化交易中,参数的调优显得尤为重要。通过对历史数据进行分析和模拟回测,可以找到最适合当前市场的参数设置,提高交易策略的成功率和盈利水平。在后续的量化策略交易中,我们将为大家展示指标函数的参数调优过程。

最后,温馨提醒下,在计算技术指标时,需要选择合适的统计方法和指标参数,同时还需要考虑指标的灵敏度和有效性,以确保计算结果的准确性和可行性。此外,在使用技术场指标进行投资和交易时,还需要结合其他数据和因素进行分析和判断,包括基本面因素、市场情绪、政策变化等,以做出更加精准和稳健的投资决策。总之,技术指标的计算是量化策略的重要组成部分,它可以为投资者和交易者提供有力的决策支持和参考依据,同时也需要不断地进行优化和改进,以适应市场的变化和发展。

6:交易函数

在之前的课程中,我们已经学习了如何通过API获取市场数据,并使用技术分析的方法来计算市场信号,进而呢,可以去选择开平仓的操作。在交易信号确定之后,编写交易函数来自动化执行这些操作是非常必要的。交易函数可以帮助我们更方便、快捷地执行交易策略,以及实现更加精准和稳定的交易决策。

在进行交易函数的编写时,需要考虑交易平台所提供的API和功能。交易函数的主要目的是执行交易操作,包括开多、开空、平多、平空、撤销订单等。一般而言,交易过程应该包括以下几个步骤:

  • 第一步,连接交易平台:通过API连接到交易平台,以便获取市场行情、下单、查询持仓等交易操作。
  • 第二步,执行交易操作:在获得市场行情和交易信号后,通过API发送相应的交易请求,完成开仓、平仓等操作。
  • 第三步,监控交易状态:对于正在持仓的交易,需要实时监测盈亏状态,并进行止损和止盈操作。

首先我们来看第一个步骤。

连接交易平台

商品期货策略需要检测与期货公司前置机连接状态,在获取行情之前需要订阅合约,这样才能获取订阅的行情。在优宽量化交易平台上,回测时模拟是和实盘一样的连接机制。在先前的课程中,我们了解、学习的行情接口因为篇幅有限,为了容易理解,并没有检测与前置机连接状态代码,其实一个完整的商品期货策略框架应该首先检查与前置机的连接状态:

javascript
function main(){ while(true){ // 需要在判断exchange.IO("status")函数返回true,即为真值时才可调用行情、交易等函数 if(exchange.IO("status")){ exchange.SetContractType("MA888") var ticker = exchange.GetTicker() Log("MA888 tickerBuy:", ticker.Buy) Log(_D(), "已经连接CTP !") } else { Log(_D(), "未连接CTP !") } } }

这段代码中exchange.IO函数用来调用协议、交易所等其它功能接口。exchange.IO("status")函数可以判断当前是否和期货公司前置机连接,如果连接成功返回 1,如果是非连接状态就返回0。在连接成功的状况下,Log函数在日志信息显示时间和“已经连接CTP”,这里的_D()函数会返回当前时间的字符串,连接失败会显示未连接。

执行交易操作

exchange.Buy()

交易函数都可以通过exchange对象调用。首先我们来看下买单函数exchange.Buy。该函数返回一个订单ID。参数值:Price为订单价格。Amount为订单数量,两个参数都是数值类型。exchange.Buy(Price, Amount)函数的返回值是订单编号,是字符串类型,可用于查询订单信息和取消订单。

javascript
exchange.Buy(Price, Amount)

exchange.Sell()

同样的,卖单函数为exchange.Sell,需要的参数和返回的结果和买单函数一样。

javascript
exchange.Sell(Price, Amount)

exchange.SetDirection()

期货下单时必须注意交易方向。exchange.SetDirection(Direction)用来设置exchange.Buy或者exchange.Sell函数进行期货下单的方向。这里面有特殊的 closebuy_todayclosesell_today,这是平今仓的指令,只有上海期货交易所的品种有平今仓指令,大连和郑州交易所的期货品种可以不区分。

下单函数SetDirection函数的参数设置的方向备注
exchange.Buy"buy"买入开多仓
exchange.Buy"closesell"买入平空仓
exchange.Buy"closesell_today"买入平空仓(今仓)
exchange.Sell"sell"卖出开空仓
exchange.Sell"closebuy"卖出平多仓
exchange.Sell"closebuy_today"卖出平多仓(今仓)

在设置好交易方向后,我们就可以进行开平仓的操作,我们使用代码示范下。

javascript
function main() { while (!exchange.IO("status")) { Sleep(1000) } // 设置合约代码 exchange.SetContractType("rb888") // 设置下单方向 exchange.SetDirection("buy") var id = exchange.Buy(100, 1) Log("id:", id) // 设置下单方向 exchange.SetDirection("closebuy") // 平掉仓位 var id2 = exchange.Sell(100, 1) Log("id:2", id2) }

在设置好交易方向后,我们就可以进行开平仓的操作,我们使用代码示范下。我们来看下交易逻辑。

设置合约代码为"rb888",即螺纹钢主力期货合约。接着设置交易方向为"buy",开多仓。使用exchange.Buy函数下单,买入1手期货合约,价格为ticker的卖单,所以会立即成交。将返回的订单编号存储在id变量中。

然后设置交易方向为"closebuy",平多仓。使用exchange.Sell函数下单,卖出1手期货合约。价格是ticker的买单,所以也会立即成交,将返回的订单编号存储在id2变量中。

使用Log函数输出订单编号id和id2,以便后续查看订单状态等信息。可以看到结果里,买入开多仓的id为1,卖出多仓的id为2。

这里只是给大家展示了一个开平仓的操作。需要注意的是,此代码中的交易策略比较简单,没有考虑风险控制等因素,只是演示了如何使用API函数进行期货交易的基本操作。在实际交易中,需要根据自己的交易策略和风险偏好进行相应的修改和补充。

exchange.CreateOrder()

exchange.CreateOrder()函数用于下单。该函数最大的功能是直接在该函数的参数中指定下单的品种、方向。这样就不再依赖系统当前设置的交易对、合约代码、交易方向等设置了。在多品种交易下单场景中、并发场景中极大程度的降低了设计复杂度。exchange.CreateOrder()函数的四个参数分别是symbol、side、price、amount。用来指定订单的合约代码、方向、价格、数量。

javascript
function main() { while(!exchange.IO("status")) { Sleep(1000) } // 调用CreateOrder函数下单 var id = exchange.CreateOrder("rb2410", "buy", 3500, 1) Log(id) }

exchange.GetOrder()

如果要查询订单的状态,可以根据订单号获取订单详情。参数值:Id为需要获取的订单号,参数Id为字符串类型。它的返回值是Order结构体。

javascript
exchange.GetOrder(orderId)

根据具体订单Id查询订单详细信息。

javascript
function main(){ while (!exchange.IO("status")) { Sleep(1000) } // 设置合约代码 exchange.SetContractType("rb888") // 设置下单方向 exchange.SetDirection("sell") var id = exchange.Sell(99999, 1) var order = exchange.GetOrder(id) Log("Id:", order.Id, "Price:", order.Price, "Amount:", order.Amount, "DealAmount:", order.DealAmount, "Status:", order.Status, "Type:", order.Type) }

我们来看下代码,方向和下单都是sell,所以是一个开空仓的操作。这里的下单价格只是举例,较大的价格不会成交,订单会处于待成交状态,在实盘中我们可以自行调整价格。订单建立后,参数id为订单号码,在GetOrder函数中填入我们想要查询的订单的号码。

重点看一下返回值结果:

  • Info:交易所接口应答的原始数据,回测时无此属性。
  • Symbol:品种代码。
  • Id:订单号。
  • Price:下单价格。
  • Amount:该订单中下单数量。
  • DealAmount:该订单中实际成交的合约张数。在订单成交之前,该值通常为 0。
  • AvgPrice:该订单的成交平均价格。成交之前,该值也是 0。
  • Type:表示订单类型,Type为1,即表示该订单是一个卖单,如果是0,就是买单。
  • Offset:持仓方向。开仓(Open是0)或平仓(Close是1)。
  • Status:订单状态。Status为0(ORDER_STATE_PENDING),表示该订单的状态为“未成交”;如果是1(ORDER_STATE_CLOSED),代表是成交;如果是2(ORDER_STATE_CANCELED),代表被撤销的订单;如果是3(ORDER_STATE_UNKNOWN),代表订单状态未知。
  • ContractType:期货合约类型,一般包含品种代码和到期日期等信息。

exchange.GetOrders()

函数exchange.GetOrders(),当不填写参数的时候,获取所有未完成的订单。可填写参数symbol,获取目标品种的订单信息。

返回值:Order结构体数组。当交易所对象exchange代表的账户当前交易对没有挂单(就是没有未完成的订单时)时,调用该函数将返回空数组([])。

以下代码设置了两个不能成交的单子,然后利用GetOrders获取到了所有未完成的订单的信息。

javascript
function main(){ while (!exchange.IO("status")) { Sleep(1000) } // 设置合约代码 exchange.SetContractType("rb888") // 设置下单方向 exchange.SetDirection("sell") exchange.Sell(99999, 1) exchange.Sell(88888, 1) // 不填写参数 var orders = exchange.GetOrders() Log("未完成订单一的信息,ID:", orders[0].Id, "Price:", orders[0].Price, "Amount:", orders[0].Amount, "DealAmount:", orders[0].DealAmount, "type:", orders[0].Type) Log("未完成订单二的信息,ID:", orders[1].Id, "Price:", orders[1].Price, "Amount:", orders[1].Amount, "DealAmount:", orders[1].DealAmount, "type:", orders[1].Type) // 填写参数 var rborder = exchange.GetOrders("rb888") Log(rborder) }

同样的,这里设置了两个不能成交的单子,然后利用GetOrders获取到了所有未完成的订单的信息。

exchange.GetHistoryOrders()

exchange.GetHistoryOrders()函数用于获取当前交易日内的所有合约的历史订单,支持查询指定合约的历史订单。exchange.GetHistoryOrders()函数有两种调用形式:

  • exchange.GetHistoryOrders()
    当不传任何参数时,获取所有合约的历史订单。
  • exchange.GetHistoryOrders("rb2410")
    当指定具体合约代码时,获取具体合约的历史订单。
javascript
function main(){ // 鉴于测试代码,不使用商品期货策略一般架构,这里仅仅判断exchange.IO("status")函数,判断连接期货公司前置机成功后立即执行测试代码。股票证券无需使用exchange.IO("status")判断连接状态 while (!exchange.IO("status")) { Sleep(1000) } var orders = exchange.GetHistoryOrders() Log(orders) }

注:

回测系统和实盘系统该函数返回结果有所区别:

  • 回测系统:返回策略运行期间所有完成订单,可使用since参数模拟本交易日生成订单;
  • 实盘系统:仅返回本交易日完成订单。

实盘下单小tips

在实盘中,为了确保交易成功,价格参数可以传-1。但是在回测系统中不支持。商品期货除了使用市价单还可以用限价单方式下单,可以使用一个较大的滑价确保和对手盘成交。

javascript
function main() { while(true) { if (exchange.IO("status")) { exchange.SetContractType("rb888") exchange.SetDirection("buy") // 获取当前行情 var ticker = exchange.GetTicker() // 拿到当前卖一价格 var currSell1Price = ticker.Sell // 加50元滑价,即为比出价卖出的挂单高50,要求买入1手 var id = exchange.Buy(currSell1Price + 50, 1) Log(exchange.GetOrder(id)) } else { Log("未连接") } } }

exchange.CancelOrder(orderId)

函数exchange.CancelOrder(orderId)可以根据订单 ID 取消订单。 比如下面的代码: 我们使用Sell函数下单了一个价格为99999不能成交的单子。 然后使用CancelOrder函数传入 id 参数,来取消了这个订单。最后使用GetOrder函数获取这个 id 的当前订单状态,可以看到打印出的订单信息,其中 Status属性为 2,代表被取消的订单。

javascript
function main(){ while (!exchange.IO("status")) { Sleep(1000) } // 设置合约代码 exchange.SetContractType("rb888") // 设置下单方向 exchange.SetDirection("sell") // 下单价格只是举例,较大的价格不会成交,订单会处于订单薄中待成交状态,具体测试可以自行调整价格 var id = exchange.Sell(99999, 1) exchange.CancelOrder(id) Log(exchange.GetOrder(id)) }

监控交易状态

在订单完成以后,我们来看下有关于查看交易状态的函数。

exchange.GetAccount()

第一个exchange.GetAccount(),它会返回交易所账户信息。具体包含以下几个字段:

  • Balance:账户可用余额,当前账户中可以用于交易的可用资金数量。在这个例子中,可用余额为模拟金额 100 万元。
  • FrozenBalance:账户冻结余额,是已经被冻结,无法用于交易的资金数量。
  • Stocks:账户持仓数量,表示已经持有的合约数量。
  • FrozenStocks:账户已冻结的持仓数量,指哪些被冻结的、无法进行交易的合约数量。
  • Equity:当前账户权益,包含可用资金、仓位保证金、盈亏等。
  • UPnL:当前所有持仓的盈亏总和。

需要注意的是,在实际交易中,账户资金和持仓情况会随着交易的进行而发生变化,因此需要及时查询和更新账户信息,以便进行下一步的交易操作。

javascript
function main(){ while (!exchange.IO("status")) { Sleep(1000) } // 获取账户资产信息,可以不用设置合约 var account = exchange.GetAccount() Log("账户信息,Balance:", account.Balance, "FrozenBalance:", account.FrozenBalance, "Stocks:", account.Stocks, "FrozenStocks:", account.FrozenStocks) }

exchange.GetPositions()

函数exchange.GetPositions(),获取所有合约当前持仓信息。exchange.GetPositions()函数不依赖于当前设置的合约代码,不传symbol参数时获取所有合约当前持仓信息。传入symbol参数时获取指定合约的持仓信息。该函数兼容exchange.GetPosition()调用。

javascript
// 不传参数 exchange.GetPositions() // 传入symbol参数 exchange.GetPositions(symbol)

函数返回结果为Position结构体:

  • Info:交易所接口应答的原始数据,回测时无此属性。
  • Symbol:品种代码。
  • Price:持仓合约的成本价格,就是当时该合约的买入或卖出价格。
  • Amount:持仓合约的数量,就是当前账户中所持有的该合约的数量。
  • FrozenAmount:被冻结的持仓数量,指被冻结而无法进行交易的合约数量。
  • Profit:当前合约的浮动盈亏。
  • Margin:当前持仓所占用的初始保证金数量。
  • MarginLevel:持仓杆杠大小,商品期货无法修改杠杆值。
  • Type:合约类型,仓位类型,可分为PD_LONG(多头仓位,如果区分今仓、昨仓,PD_LONG表示今仓), PD_SHORT(空头仓位,同理代表今日空仓), PD_LONG_YD(昨日多头仓位), PD_SHORT_YD(昨日空头仓位)。
  • ContractType:合约代码、股票代码。

需要注意的是,GetPositions函数获取的是所有持仓品种的持仓信息,如果没有持仓则返回空数组,所以使用该接口返回的数据前要先判断返回的数据是否为空数组。

javascript
/* 注意:GetPositions函数获取的是所有持仓品种的持仓信息,如果没有持仓则返回空数组,所以使用该接口返回的数据前要先判断返回的数据是否为空数组 */ function main(){ // 鉴于测试代码,不使用商品期货策略一般架构,这里仅仅判断exchange.IO("status")函数,判断连接期货公司前置机成功后立即执行测试代码。股票证券无需使用exchange.IO("status")判断连接状态 while(!exchange.IO("status")) { Sleep(1000) } var info = exchange.SetContractType("rb888") var ticker = exchange.GetTicker() exchange.SetDirection("buy") exchange.Buy(ticker.Last + info.PriceTick * 20, 2) var position = exchange.GetPositions() if(position.length>0){ Log("Amount:", position[0].Amount, "FrozenAmount:", position[0].FrozenAmount, "Price:", position[0].Price, "Profit:", position[0].Profit, "Type:", position[0].Type, "ContractType:", position[0].ContractType) } }

商品期货的持仓需要注意:

  • 回测系统
    回测系统不区分今仓、昨仓。GetPositions函数返回的持仓数据Position结构数组中,Position结构的Type属性仅为PD_LONG或者PD_SHORT
  • 实盘
    有交易所区分今仓、昨仓,例如上期所。有些交易所虽然也区分,但是不能指定平今还是平昨,有今仓优先平今仓。例如IF等一些品种只能先平今仓,所以今仓、昨仓仓位信息合并为一个并且不予区分。

好了,量化策略中交易函数的内容就讲到这里,在实际的工作中,使用交易函数需要综合考虑市场和策略的各种因素,注重实践和经验总结,才能够获得良好的交易效果和表现。

7:布林带通道策略

在前面一个阶段的课程中,我们从 JavaScript 语言的简介、基础语法、金融数据的获取和计算等方面为大家讲解实现交易策略的前提部分,本篇我们将继续前面的内容,从常用的策略模块、技术指标,一步一步帮助大家实现一个可行的日内量化交易策略。

策略简介

布林带也称为布林通道,英文简称BOLL。它是最常用的技术指标之一,由约翰·包宁杰(John Bollinger)在1980年代发明。理论上,价格总是围绕着价值在一定范围内上下波动,布林带正是根据这个理论基础,引入了“价格通道” 的概念。布林带的计算方式是利用统计学原理,先计算一段时间价格的“标准差” ,再由均线加/减2倍的标准差。给大家稍微解释一下,这里假设价格的波动符合正态分布,2倍的标准差就在95%的置信区间,所以属于正常范围的波动,这样就可以求出价格的“信赖区间” ,如果波动超过2倍的标准差,可以认为是均值,也就是价格发生了实质性的改变。其基本的型态是由三条轨道线组成的带状通道(中轨、上轨、下轨)。中轨为价格的平均成本,上轨和下轨分别代表价格的压力线和支撑线。

由于采用了标准差的概念,使得布林通道的宽度会根据近期价格的波动而做出动态调整。波动小,布林通道会变窄;波动大,布林通道会变宽。当BOLL通道由宽变窄,说明价格逐渐向均值回归。当BOLL通道由窄变宽,意味着行情开始发生变化,如果价格上穿上轨,表明买力增强,如果价格下穿下轨,表明卖力增强。

image

布林带指标计算方法

在所有的技术指标中,布林带的计算方法是比较复杂的一种,其中引进了统计学中的标准差概念,涉及到中轨线(MB)、上轨线(UP)和下轨线(DN)的计算。具体参数有两个,N是时间周期,K是标准层宽度系数。三条轨道具体的计算方法如下:

  • 中轨 = N 时间段的简单移动平均线
  • 上轨 = 中轨 + K × N 时间段的标准差
  • 下轨 = 中轨 − K × N 时间段的标准差

但是使用JavaScript的ta库,布林带的计算可以使用成熟的函数获取布林带的上轨,中轨和下轨。这里我们使用的是默认的参数,20个周期,标准差为2。然后使用log函数将三个轨道的数组打印出来。

在优宽量化工具中,获取布林带数组很简单,直接调用布林带的 API 就可以了,因为布林带数组是一个二维数组。二维数组其实很好理解,它就是数组中的数组,那么获取的顺序就是: 先获取数组中指定的数组,然后在从指定的数组中获取指定的元素。索引为0,1,2的元素就是布林带的上轨,中轨和下轨数组。

这里为了以防最新的k线没有走完,所以我们获取的是倒数第二根布林带轨道的值[r.length-2]。当然这只是技术指标的计算,技术指标是为了判断策略逻辑。布林线的使用方法有很多,可以单独使用,也可以和其他指标结合在一起使用。

javascript
/*backtest start: 2023-01-29 09:00:00 end: 2023-06-04 15:00:00 period: 1d basePeriod: 1h exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] */ function main(){ while(true){ exchange.SetContractType('rb888') var r = exchange.GetRecords(); //获取K线数组 var boll = TA.BOLL(r, 20, 2); //计算布林带指标 var upLine = boll[0];//获取上轨数组 var midLine = boll[1];//卖取中轨数组 var downLine = boll[2]; //获取下轨数组 Log('上轨:',upLine[r.length-2]) Log('中轨:',midLine[r.length-2]) Log('下轨:',downLine[r.length-2]) } }

本节教程我们将采用布林线一种最简单的使用方法。就是:当价格自下而上突破上轨,即突破上方压力线时,我们认为多方力量正在走强,一波上涨行情已经形成,买入开仓信号产生;当价格自上而下跌破下轨,就是跌破支撑线时,我们认为空方力量正在走强,一波下跌趋势已经形成,卖出开仓信号产生。

有开仓必然也有平仓,我们来看下平仓的逻辑。如果买入开仓后,价格又重新跌回到了布林线中轨,我们认为多方力量正在走弱,或者空方力量正在加强,卖出平仓信号产生;如果卖出开仓后,价格又重新涨回到布林线中轨,我们认为空方力量正在走弱,或者多方力量正在加强,买入平仓信号产生。因此可以总结市场的操作信号:

  • 多头开仓: 如果无持仓,并且收盘价大于上轨
  • 空头开仓: 如果无持仓,并且收盘价小于下轨
  • 多头平仓: 如果持多单,并且收盘价小于中轨
  • 空头平仓: 如果持空单,并且收盘价大于中轨

策略代码实现

javascript
/*backtest start: 2023-01-29 09:00:00 end: 2023-06-04 15:00:00 period: 1d basePeriod: 1h exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] */ function main(){ var mp = 0 //设置持仓信息 while(true){ exchange.SetContractType('rb888') var r = exchange.GetRecords(); //获取K线数组 if (r.length < 20){//需要超过K线的长度20 return;} var boll = TA.BOLL(r, period, width); //计算布林带指标 var upLine = boll[0];//获取上轨数组 var midLine = boll[1];//卖取中轨数组 var downLine = boll[2]; //获取下轨数组 var upPrice = upLine[upLine.length - 3]; //获取上上根K线上轨数值 var midPrice = midLine[midLine.length - 3]; //获取上上根K线中轨数值 var downPrice = downLine[downLine.length - 3]; //获取上上根K线下轨数值 recclose = r[r.length - 2].Close; //获取上根K线收盘价 if(mp == 0 && recclose > upPrice){//如果无持仓,并且收盘价大于上轨,开多 // 设置下单方向 exchange.SetDirection("buy"); exchange.Buy(recclose,1) mp = 1;} if(mp == 0 && recclose < downPrice){ //如果无持仓,并且收盘价小于下轨,开空 // 设置下单方向 exchange.SetDirection("sell"); exchange.Sell(recclose,1) mp = -1;} if(mp == 1 && (recclose < midPrice)){ //如果持多,并且收盘价小于中轨,平多 // 设置下单方向 exchange.SetDirection("closebuy"); exchange.Sell(recclose-5,1);//这里为了保证交易,所以设置限价单为最新的价格减5 mp = 0;} if(mp == -1 && (recclose > midPrice )){ //如果持空,并且收盘价大于中轨,平空 // 设置下单方向 exchange.SetDirection("closesell"); exchange.Buy(recclose+5,1);//同样的,为保证交易,这里设置限价单 mp = 0;} Sleep(1000) } }

这是一段使用JavaScript语言编写的期货交易策略代码。该代码实现的交易策略基于布林带指标,用于自动化期货交易。具体说明如下:

  • 首先开始一个无限循环,是为了获取最新K线数据。检查K线的长度是否达到20根或以上,如果没有就结束程序。

  • 使用 TA.BOLL 计算20根K线周期内的布林带指标,其中2表示标准差宽度系数。

  • 获取上轨、中轨、下轨的价格。为了防止最新的k线没有走完,所以我们使用倒数第二根k线[r.length-3],然后判断倒数第一根k线和布林带的关系,去形成买卖的信号。这样做是为了防止未来函数,就是在历史回测中使用还没有发生,但是已知的信号。

  • 获取前一根K线的收盘价 recclose,作为当前价格的参考。
    如果持仓 mp = 0(无持仓) 并且 recclose > upPrice,则开多仓。设置交易方向为 “buy”,然后使用exchange.Buy函数下单,买入1手期货合约。最后将mp值设为 1,表示现在持有多仓。

  • 如果持仓 mp = 0(无持仓)并且 recclose < downPrice,则开空仓。设置交易方向为 “sell”,然后使用exchange.Sell函数下单,卖出1手期货合约。最后将mp值设为 -1,表示现在持有空仓。

  • 如果持仓 mp = 1(持有多仓)并且 recclose < midPrice,则平仓。设置交易方向为 “closebuy”,然后使用exchange.Sell函数下单,卖出1手期货合约。这里为了保证交易,所以设置限价单为最新价格减1。最后将mp值设为 0,表示现在无持仓。

  • 如果持仓 mp = -1(持有空仓)并且 recclose > midPrice,则平仓。设置交易方向为 “closesell”,然后使用exchange.Buy函数下单,买入1手期货合约。这里同样的,为了保证交易,设置限价单为最新价格加一。最后将mp值设为 0,表示现在无持仓。使用Sleep(1000)函数延迟1秒后再重新获取最新K线数据,继续执行上述的交易策略。

上面这些就是我们学习开发一个完整的日内量化交易策略的每个步骤,包括:策略简介、布林带指标计算方法、策略逻辑、 买卖条件、 策略代码实现等。点击开始回测,我们看到我们的策略的回测结果。

通过这个策略案例,不仅熟悉优宽量化工具的编程方法,还可以根据这个模板改编成不同的策略。策略能否改进的更好呢,上节课我们提到参数的调整,这里我们设置布林带周期和标准差宽度为外部参数,进行参数调优。在参数编辑页面设置好参数以后,在回测页面这里我们设置参数区间,然后进行调参,看一下优化的结果。可以看到,当设置周期为18和标准差宽度为1的时候,取得了最高的胜率和收益,但是使用固定品种和固定周期可能陷入过拟合的风险,大家也需要考虑下。

量化交易策略无非是主观交易经验或系统的总结,如果我们在写策略之前,把主观交易中用到的经验或系统,分别写出来,然后再一条一条翻译成代码,你会发现写策略就会容易很多。大家可以尝试一下!

8:JavaScript画图函数:Chart函数(上)

在量化交易中,画图具有非常重要的作用。画图可以帮助我们更好地理解和分析市场情况,提高交易准确性,同时也可以验证和改进量化交易策略。首先,画图可以帮助我们进行行情分析。通过绘制K线图、均线、波动指标等图形,我们可以更直观地了解市场趋势、价格波动等情况,从而更好地预测市场走势和发现交易机会。其次,画图也可以帮助我们发现交易信号。例如,我们可以通过绘制支撑位、压力位、趋势线等图形来识别市场的趋势和反转点,从而产生相应的交易信号,提高交易准确性。此外,画图还可以帮助我们进行风险管理。例如,我们可以通过绘制止损线、趋势线、波动范围等图形来识别市场风险,并制定相应的止损策略,从而有效控制风险和保护资金。最后,画图还可以帮助我们验证和优化量化交易策略。通过回测和画图,我们可以确认某种策略在特定市场环境下的表现,并针对不同的市场情况进行优化和改进,提高交易效果和收益率。总之,画图在量化交易中具有极其重要的作用,可以帮助我们更好地理解市场行情、发现交易信号、进行风险管理和验证和优化量化交易策略等。因此,在进行量化交易时,合理运用画图工具可以大大提高交易准确性和效果。

在优宽量化平台,JavaScript语言有多种的画图方法,今天我们介绍chart画图函数。Chart(...)是自定义图表画图函数,专门用于绘制各种类型的交互式图表。它支持折线图、区域图、柱状图、饼图等多种类型的图表,并提供了丰富的工具和选项,可以满足各种需求。

首先我们讲下chart函数的使用方法,在使用chart画图函数时,我们需要使用数据和选项的配置对象来控制图表的显示和交互效果。

选项配置对象

选项配置对象用于控制图表的显示效果、交互效果等。以下是选项配置对象的常见属性:

  • chart: 表示图表的整体配置选项,包括类型(如折线图、柱状图、饼图等)、背景色、边框等。
  • title: 表示图表的标题配置选项,包括文本、样式等。
  • subtitle: 表示图表的副标题配置选项,包括文本、样式等。
  • legend: 表示图例(Legend)的配置选项,包括位置、样式等。
  • tooltip: 表示提示框(Tooltip)的配置选项,包括触发方式、内容格式等。
  • plotOptions: 表示系列(Series)的配置选项,包括类型、颜色、标签等。

示例

下面我们示范一下,如果我们想画两条双均线(五日均线和十日均线),首先我们可以定义选项配置对象:

javascript
var chart = { // 标记是否为一般图表,有兴趣的可以改成false运行看看 __isStock: true, // 缩放工具 tooltip: {xDateFormat: '%Y-%m-%d %H:%M:%S, %A'}, // 标题 title : { text : '均线'}, // 坐标轴横轴即:x轴,当前设置的类型是:时间 xAxis: { type: 'datetime'}, // 坐标轴纵轴即:y轴,默认数值随数据大小调整 yAxis : { // 标题 title: {text: '均线'}, // 是否启用右边纵轴 opposite: false }, }

该选项配置对象包含了对图表的完整配置。其中__isStock属性表示是否为一般图表,选择true为Highstocks,Highstocks是一个专门用来创建交互式股票图表和金融图表的JavaScript库。它是Highcharts图表库的一部分,并提供了更多的功能,包括支持股票指标、数据区域缩小、鼠标拖拽和滚轮缩放等。tooltip属性定义了缩放工具的格式,title属性定义了图表的标题,xAxis属性定义了X轴的配置,yAxis属性定义了Y轴的配置。

数据配置对象

数据配置对象用于控制图表的数据源。其中,最重要的属性是series,它是一个数组,每个元素表示一个系列(Series)的数据。

数据配置对象:

javascript
// 数据系列,该属性保存的是各个数据系列(线,K线图,标签等...) series : [ // 索引为0,data数组内存放的是该索引系列的数据 {name : "line1", id : "线1,五日均线", data : []}, // 索引为1,设置了dashStyle:'shortdash'即:设置虚线 {name : "line2", id : "线2,十日均线", dashStyle : 'shortdash', data : []} ]

该数据配置对象包含了两条均线数据,分别被定义在一个数组上。其中每个数据系列都有name、id、data属性。name属性用于图例显示的名称,id属性为数据系列的唯一标识符,data属性则为数据系列的数据。

双均线例子

我们将数据配置对象和选项配置对象的属性合并到一个名为chart的对象中。这个对象包含了所有的配置选项,从而实现了用一个对象控制整个图表的效果。

javascript
var chart = { // 标记是否为一般图表,有兴趣的可以改成false运行看看 __isStock: true, // 缩放工具 tooltip: {xDateFormat: '%Y-%m-%d %H:%M:%S, %A'}, // 标题 title : { text : '均线'}, // 坐标轴横轴即:x轴,当前设置的类型是:时间 xAxis: { type: 'datetime'}, // 坐标轴纵轴即:y轴,默认数值随数据大小调整 yAxis : { // 标题 title: {text: '均线'}, // 是否启用右边纵轴 opposite: false }, // 数据系列,该属性保存的是各个数据系列(线,K线图,标签等...) series : [ // 索引为0,data数组内存放的是该索引系列的数据 {name : "line1", id : "线1,五日均线", data : []}, // 索引为1,设置了dashStyle:'shortdash'即:设置虚线 {name : "line2", id : "线2,十日均线", dashStyle : 'shortdash', data : []} ] }

在chart对象设置完成以后,接着我们需要往里面添加数据。

javascript
function main(){ // 调用Chart函数,初始化图表 var ObjChart = Chart(chart) // 清空 ObjChart.reset() while(true){ exchange.SetContractType("rb888") // 获取本次轮询的时间戳,即一个毫秒的时间戳。用来确定写入到图表的X轴的位置 var nowTime = new Date().getTime() // 获取行情数据 var r = exchange.GetRecords() // 五日均线 var ave_5 = TA.MA(r, 5) // 十日均线 var ave_10 = TA.MA(r, 10) // 用时间戳作为X值,均价作为Y值传入索引0的数据序列 ObjChart.add(0, [nowTime, ave_5[r.length-1]]) // 同上 ObjChart.add(1, [nowTime, ave_10[r.length-1]]) Log(ave_5) Log(ave_10) } }

首先,通过Chart(chart)函数初始化了一个图表对象ObjChart,其中chart是一个选项配置对象,用于配置图表的各项属性。然后,通过ObjChart.reset()函数清空了图表中已经存在的数据。接下来,通过一个无限循环while(true)来实现不断获取并更新行情数据。在每次循环中,调用exchange.GetRecords()函数获取当前品种的K线行情数据,并用TA.MA()计算出五日和十日均线的值。然后,通过new Date().getTime()获取当前时间戳作为X值,将五日和十日均线的值作为Y值,调用ObjChart.add()函数把这些数据加入到图表中。其中,第一个参数0和1分别对应了选项配置对象中的两个数据序列,即五日均线和十日均线。第二个参数是一个包含X值(也就是时间)和Y值(就是最新时刻的均线值)的数组。总之,这段代码通过数据配置对象和选项配置对象来定义图表,然后不断获取K线数据,计算均线,将计算结果添加到图表上,最终实现了两条均线的效果。

image

这里呢,我们只涉及到均线的计算和绘图,下节课我们将研究下k线图的画法。Chart画图函数确实比较复杂,参数也比较多。不过,对于刚接触量化交易的人来说,掌握这些画图函数十分重要,因为它可以帮助我们更好地理解市场行情和策略效果。但是,对于刚入门量化学习的我们可能会被这些代码吓到,不知道从何下手。这时候,兴趣是最好的老师。只要抱着兴趣和学习的心态,慢慢琢磨、尝试,相信大家一定可以掌握这些画图函数的使用方法。

另外,优宽平台提供了许多丰富的讲解材料和策略案例,这些都可以帮助刚接触量化交易的人更好地入门和掌握相关知识。我们可以通过阅读官方文档、教程视频、参与社区讨论等方式获取更多的相关知识和经验。

9:JavaScript画图函数:Chart函数(下)

因为交易员需要关注多个市场、多种资产的行情数据,以及交易策略的实时表现,经常可以看到职业的交易员有多个屏幕显示不同维度的数据。观察不同维度的数据可以帮助交易员更好地跟踪多个市场、多种资产的行情情况,分析市场趋势和交易信号,以及实时评估交易策略的效果。本节课程呢,我们就是用JavaScript语言实现一些复杂的画图展示。

复杂图表的例子

在量化交易中,复杂图表是指包含多个技术指标的图表或者同时展示多个品种的监控图表等复杂的图表类型。这些图表可能会同时包含多条曲线、多个子图以及各种颜色和标记等元素,从而使其更加详细和全面地呈现市场行情和交易策略效果。复杂图表可以帮助我们更好地了解市场走势和交易机会,同时可以辅助我们进行交易决策,并提供更为细致的风险管理和位置管理。但是,这些图表可能也会比较难懂和使用,需要对技术指标的原理和用法有一定理解、熟练使用相应的量化工具和软件等。

黑色系铁矿石、螺纹钢和热卷是钢铁工业中的三个主要品种,它们的相关性比较强。一般来说,铁矿石作为钢铁生产的原材料,直接影响生产成本,价格上涨或下跌都会对螺纹钢产生一定的影响。而螺纹钢和热卷都是钢铁制品的重要品种,两者的价格都受到市场供需关系、宏观经济环境等多个因素的影响,所以它们之间的价格走势也具有很强的相关性。这段代码是一个基于量化交易框架的示例程序,主要用于实时监控这三个相关品种数据并绘制相应的图表。该段代码实现了对三个不同品种的均线和K线数据的获取和显示。具体来说,该段代码创建了三个图表配置对象 cfgA、cfgB 和 cfgC,分别代表要展示的三个品种 i888、rb888 和 hc888 的图表。每个图表配置对象中包含了图表的标题、x轴的类型等参数,其中每个对象中的数据系列用于展示蜡烛图。之后,使用 Chart 函数将这三个图表配置对象包装成一个 chart 对象。

javascript
var cfgA = { __isStock: true, title: { text: 'A' }, series: [{ type: 'candlestick', name: 'A', id: 'A', data: [] }, { type: 'line', yAxis: 0, name: "A_MA", data: [], }] } var cfgB = { __isStock: true, title: { text: 'B' }, series: [{ type: 'candlestick', name: 'B', id: 'B', data: [] }, { type: 'line', yAxis: 0, name: "B_MA", data: [], }] } var cfgC = { __isStock: true, title: { text: 'C' }, series: [{ type: 'candlestick', name: 'C', id: 'C', data: [] }, { type: 'line', yAxis: 0, name: "C_MA", data: [], }] } function main() { var symbols = ["rb888", "MA888", "i888"] var chart = Chart([cfgA, cfgB, cfgC]) chart.reset() var arrLastTime = [0, 0, 0] while (true) { if (exchange.IO("status")) { LogStatus("时间:", _D(), ",已经连接") for (var i = 0; i < symbols.length; i++) { exchange.SetContractType(symbols[i]) var r = exchange.GetRecords() var ma = TA.MA(r, 10) for (var j = 0; j < r.length; j++) { if (r[j].Time > arrLastTime[i]) { // 增加 chart.add(i * 2, [r[j].Time, r[j].Open, r[j].High, r[j].Low, r[j].Close]) chart.add(i * 2 + 1, [r[j].Time, ma[j]]) arrLastTime[i] = r[j].Time } else if (r[j].Time == arrLastTime[i]) { // 更新 chart.add(i * 2, [r[j].Time, r[j].Open, r[j].High, r[j].Low, r[j].Close], -1) chart.add(i * 2 + 1, [r[j].Time, ma[j]], -1) } } } chart.update([cfgA, cfgB, cfgC]) } Sleep(5000) } }

在进入 while 循环之后,通过 exchange.SetContractType 函数循环遍历每个品种,调用 exchange.GetRecords() 获取该品种最新一根 K 线数据并存储到变量中,并使用 TA.MA(r, 10) 函数计算10日移动平均线。然后遍历获取到的 K 线数据,根据时间戳判断是需要新增还是更新,根据判断结果使用 chart.add()方法将K线数据和对应的MA值添加或更新到对应的图表系列中去。其中,i * 2i * 2 +1表示三个合约的K线数据和对应的均线数据分别在系列数组中的索引位置,该索引位置与初始化的 cfgA、cfgB、cfgC 对象中的系列顺序是相对应的。

下面我们来研究下这段代码的细节:

  • extension是一个在图表开发中常用的属性,用于对图表进行进一步的自定义和细粒度控制。它可以包含一些子属性,比如在该示例中使用的layout、height、col等。

  • layout可以指定图表的布局方式,支持多种取值,例如示例代码中的single表示单独显示,不参与分组,正如我们图形中展示的一样,呈现纵向排列的状态。默认取值为group,表示与其他图表一起分组显示。这里我们设置为group看一下,可以看到,是分组折叠展示的。

  • height是一个数值型属性,指定了图表的高度。该属性只在layout为single时才有效,因为分组时采用的是自适应的方式。

  • col是一个数值型属性,指定了图表的宽度占据几个单元格。这个属性只在分组布局,也就是group中有意义,因为布局时每行通常包含12个单元格,可以将多个图表放在同一行实现紧凑排列的效果(比如有两个图一个宽度为8,一个为4,所有紧密的并排在一起,如果调大其中一个,那么另一个图像就会换行展示)。

cfgA、cfgB 和 cfgC 分别代表了三个不同品种(i888、rb888 和 hc888)的图表配置对象,三个配置是一样的。对于三个品种,配置对象都用来展示品种的均线走势图和蜡烛图。其中,title 表示图表标题,xAxis 表示 x 轴的类型是类别型轴,series时数据的配置对象,包含了两个系列,分别是均线线形图和蜡烛图。具体解释如下:name 是系列的名称。type 是系列的类型,这里定义了两个不同类型的系列,一个是线形图,另一个是蜡烛图,data 是系列的数据,是一个数组,用来存储该系列要展示的数据。series数据是有单独索引的,第一第二个数据索引属于铁矿石,第三第四属于螺纹钢,最后两个是热卷的。所以可以看到我们的数据添加过程是这样的。

这里我们解释下三个品种数据的添加过程,这段代码中使用了两个 for 循环。

第一个循环语句 for (var i = 0; i < symbols.length; i++) 遍历了 symbols 数组中的所有元素,该数组包含了要显示在图表上的三个品种的合约代码。通过调用 exchange.SetContractType(symbols[i]) 方法设置当前合约代码为数组 symbols 中的第 i 个元素。

第二个循环语句 for (var j = 0; j < r.length; j++) 遍历了 r 数组中的所有元素,该数组包含了当前品种最新的 K 线数据。通过遍历 r 数组并将每个元素添加到当前品种对应的系列中,实现了将最新的 K 线数据添加到图表中并刷新的效果。在每次添加完数据之后,通过更新arrLastTime数组来记录最新的时间戳,并用于判断下一次是否需要添加/更新数据。

这里的数据添加和更新也很有意思,如果上一根k线已经走完,就是新的时间戳大于上一个周期的时间戳,表示新的k线已经产生,这时候就要增加最新的数据。而如果此时的k线周期还没有完成,最新的k线数据还没有固定,这时候就要不断的更新最新的k线数据,add函数里最后使用-1进行k线数据的更新。综上,这两个for循环共同实现了将最新的 K 线数据添加到对应的系列中,用于刷新图表并展示最新的数据。

image

混合图表的例子

在量化交易中,混合图表是指同时展示多种不同类型或不同时间尺度的K线图、技术指标图、成交量图等多种图表,以便更全面地呈现市场行情和交易策略效果。混合图表可以帮助我们更好地了解市场走势和交易机会,同时可以帮助我们对策略的执行效果进行监控和评估。特别是对于一些复杂的交易策略,通过混合图表可以更好地展示策略在不同时间尺度上的表现和回测结果。在优宽平台中,我们提供了丰富的图表类型和工具,包括K线图、技术指标图、成交量图,同时也可以自由组合这些图表,并支持简单的操作交互,使用户可以方便而又详尽地观察市场情况和策略效果。

我们来看一个混合图表的例子。MACD 是一种技术分析指标,全称为“Moving Average Convergence Divergence”,中文翻译为“移动平均线收敛/发散指标”。它由两条曲线和一个柱形图组成,可以帮助分析价格趋势的变化情况,以及判断价格是处于超买还是超卖状态,从而提供交易信号。

在期货中,MACD 也是相当重要的分析工具之一。期货中的价格波动非常剧烈,因此需要使用技术分析工具来帮助交易者理性判断市场走势和价位变动方向,并制定相应的交易策略。MACD 在期货交易中被广泛应用,可作为趋势跟随和逆势交易的参考依据。在期货软件上,MACD 指标通常以两条线和一个柱形图的形式展示,其中包括了 DIF、DEA 和 MACD 三个指标数据。DIF 代表短期(12周期)EMA 值减去长期(26周期)EMA 值的差值。DEA 则是 DIF 的9周期 EMA 平均值,称为离差平均值。MACD 则是 DIF 与 DEA 差值的2倍,表示市场短期动量的差异程度。这些指标通常与 K 线图一起显示,交易者可以通过观察 MACD 指标的变化情况来判断市场走势,制定相应的交易策略。使用JavaScript语言我们可以呈现和期货软件几乎一样的可视化结果。

下面这段代码是展示铁矿石 MACD 指标的变化情况。整个代码分为两个部分:首先定义了一个chartCfg变量,其中包括了图表的标题、纵轴、数据序列等配置信息,用于初始化图表;然后在main函数中不断循环获取 i888 合约的 K 线数据,并计算出其对应的指标数据,然后将这些指标数据实时添加到已经初始化的图表上。

javascript
var chartCfg = { subtitle: { text: "铁矿石MACD指标", }, yAxis: [{ height: "60%", lineWidth: 2, 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

以上的就是使用chart画图的例子,我们的课程,不能面面俱到,还有很多的细节在link,大家都可以尝试探索下,构造出符合你的交易习惯的可视化面板。

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()函数结束。beginclose函数都是图表对象的方法。我们还使用plot()方法将成交量指标画在图表上。其中,第一个参数bar.Volume表示成交量数据,第二个参数"volume"表示成交量指标的名称。在这个例子中,我们只绘制了一个成交量指标,但是你可以通过添加更多的plot方法来绘制其他的指标线。

最后,通过Sleep函数设置程序每隔1秒钟执行一次,以保证实时更新数据。我们看一下回测结果,可以看到,交易量图表呈现了出来。总之,利用KLineChart函数,我们可以在不编写复杂的代码的情况下,轻松地创建自定义的策略图表。

image

复杂例子

javascript
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成交量在幅图中进行呈现,而三个指标overlaytrue的情况下在主图上进行呈现。最后,我们调用c.close()方法结束当前K线的绘制。这样,我们就可以得到一个实时更新的布林带指标和成交量指标的K线图了。

image

延伸

javascript
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”信号。

image

图表配置对象

javascript
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函数中没有进行设置,那么创建的图表就是默认样式。这里我们举例一下网格线的图表配置对象的设置。在回测结果中可以看到在图标背景中增加了网格线。

image

当然这只是一个简单的例子,我们设计的可以更加复杂。图表配置对象就是设置图表一些属性、外观等的数据结构,如例子中使用的就是一种网格线样式的配置。还有很多可以配置修改的选项,例如X轴、Y轴的相关设置,光标线的设置,提示信息的设置,技术指标样式设置,K线BAR样式的设置等等。大家可以自由探索下。

通过本节课程,我们学习了如何使用KLineChart在JavaScript语言的策略中进行自定义图表的画图。与chart函数相比,KLineChart更加简单、直观,也更加强大,支持绘制多种图形和指标线。需要注意的是策略自定义画图只能使用KLineChart()方式或者Chart()方式中的一种。希望通过本课程的学习,可以让大家更加灵活地进行策略图表的设计。

11:JavaScript画图:画线类库

在上一阶段的课程中,我们讲解了使用JavaScript的原生画图函数和接入Pine语言画图接口的klinechart函数,其实在优宽平台,JavaScript还有一种画图方法,画线类库。

模版类库的概念

在讲解画线类库的概念之前,我们首先介绍一下模版类库的概念。模板类库是优宽量化交易平台中可复用的代码模块,是策略代码的一种类别。你可以把模版类库想象成一个功能模块,使用模版类库的好处是,可以让你把精力放到编写策略逻辑上面。优宽量化交易平台支持把一些常用的、可复用的代码模块封装成独立的库(在优宽量化交易平台上叫做模板类库)。这样可以提高策略开发速度,不用编写重复的代码,降低策略交易部分和策略逻辑部分的耦合度,便于策略维护、优化和扩展。

创建一个模板类库和创建一个策略操作相同,在「策略库」中点击「新建策略」跳转到创建策略页面,区别是策略类型选择「模板类库」。然后给模板起个名字,保存就可以。模板类库设计主要有两个方面:第一是需要设计导出函数,导出函数为模板类库的接口函数$.,就是这个模板类库提供哪些功能,通过调用导出函数去使用这个模板类库提供的功能。第二是需要设计这个模板类库的参数,和普通策略一样,模板类库也可以设置参数,用于使用的时候,动态设置一些数值等参数数据。模版类库的使用也很简单。在我们的策略库里,创建或者复制模版类库以后,当然对于内置类库,比如交易类库,可以直接使用。然后在需要使用模版类库的时候,在模版引用板块,直接点击勾选就可以。在代码中,模版类库可以通过$.函数接口进行访问,直接使用它的功能。

画线类库

回到我们的画线类库。画线类库简化了策略图表画图的逻辑,可以直接调用封装好的函数。它在内置源码中帮助我们创建好了图表对象,所以使用的时候,不需要再重新设置图表配置。画线类库支持画多条线,支持K线图和flag小图标,当然我们也可以添加更多图形的支持(这个需要我们自己在源码中进行设置)。

我们可以来看一下画线类库源码。这段代码是一个用于绘制图表的JavaScript类库,使用原生的chart函数编写而成的。其中,变量 cfg 存储了图表的配置信息,包括图表的标题、Y轴辅助线、时间范围选择器、数据系列等。同时还定义了一些实用的函数,如 PlotHLine用于绘制水平辅助线,PlotRecords用于加载K线数据,PlotLine用于绘制线,PlotFlag用于绘制旗形标志,PlotTitle用于更新图表的标题。这其中的图表基本选项和数据配置参数已经设置完毕,因此使用的时候直接通过$.接口调用就可以进行绘图,节省了大量的时间和精力。

javascript
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线图上即可。

image

因此,isFirst变量的主要作用是控制均值线是否应该重新绘制,以及从哪个位置开始进行绘制。如果不加区分地每次都重新绘制均值线,则会导致K线图上的均值线出现重叠或断层等不符合实际的情况,影响了交易者的判断和分析。

多图表画线类库

在编写、设计策略时创建多图表画图是经常用到的,对于单一图表我们可以使用「画线类库」,非常方便进行画图操作。但是对于需要多个图表的场景这个模板类库就不能满足需求了。优宽平台根据画线类库的设计思路,在这个基础上设计了一个多图表版本的画线类库。大家可以尝试使用一下。

我们来看下源码中的接口函数,其中:

  • $.PlotMultRecords 用于画K线图表;
  • $.PlotMultLine 用于画线;对于这两个函数,需要注意里面的extension图表尺寸的配置信息,比如分组还是单独的显示,图表的高度和宽度等等。
  • $.PlotMultHLine 用于画水平线;这里面需要注意的参数是style,线的样式,比如实线,虚线等。
  • $.PlotMultTitle 用于修改图表标题和副标题;
  • $.PlotMultFlag 用于画flag小图标;
  • $.GetArrCfg 返回图表配置对象数组。

这些函数都需要传入一定的参数才能完成图表的绘制。同样地,首先复制画线类库到我们的策略库中。点击勾选就可以使用了。

javascript
/*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值较小"的小旗。

image

通过对比原生的chart画图函数,可以发现画线类库省去了复杂的图像配置,使代码更加简洁易读。我们可以更加专注于策略研究和优化,从而获得更好的交易结果。此外,画线类库还具有更强的扩展性和定制性,可以根据我们不同的需求进行扩展和优化,满足更加细致和复杂的图表需求。

希望我们的模版类库具有抛砖引玉的作用,大家有兴趣的,可以在源码基础上,继续增加支持的图表类型,继续升级,例如画盘口深度图,柱状图,饼图等等,打造出来专属于你的画线类库。

12:商品期货交易类库(上)

在前面的课程中,我们介绍了画图的模板类库,它对原生的JavaScript绘图函数进行了封装,方便我们进行图表的绘制。在量化交易中,时间是至关重要的,因为市场瞬息万变。一个好的交易函数可以快速响应市场信号并执行交易操作,从而提高交易的效率和准确性。为了实现这一目标,优宽平台封装了一些常用功能,例如开仓平仓、CTA函数、判断交叉等,并推出了实盘交易类库。

实盘交易类库的使用方法非常简单,作为一个内置的模板类库,我们只需勾选使用就可以。当然,如果你希望构建自己的交易类库,也可以从这个公开的类库源码开始。这份代码附带了详细的注释,供大家参考学习。通过使用交易类库,大家可以从繁琐的交易参数设置细节中解脱出来,更加专注于策略本身的编写,提升策略的执行效率。

CTA函数

首先我们介绍一下CTA函数。它相当于一个策略编写的整体框架。CTA函数用于实现基于交易所行情和持仓信息的自动交易策略。该函数可以根据一系列设定的规则和条件执行买入和卖出操作。该函数接受三个参数:contractType(合约类型)、onTick(回调函数)和interval(轮询间隔,可以省略)。

这里面我们讲一下onTick,它是一个回调函数。

  • onTick:当每次获取到行情时会调用该函数。回调函数接收一个包含以下属性的对象作为参数:
  • records:行情数据列表。
  • symbol:当前交易的合约代码。
  • detail:当前交易的合约详细信息。
  • account:交易账户信息。
  • position:当前交易合约的持仓信息。
  • positions:所有交易合约的持仓信息。

这里的回调函数应返回一个整数,表示需要执行的买卖操作数量,大于0表示买入,小于0表示卖出,等于0表示不进行任何操作。

javascript
//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种模式,第一种是针对单品种的操作:

javascript
var p = $.NewPositionManager();

这里的p为创建的单品种控制对象。请注意哈,这里的单品种操作不是指的是只能针对于单一的期货品种,而是按顺序one-by-one的操作。通过使用NewPositionManager对象提供的方法,可以方便地管理仓位和进行交易操作。可以传入一个可选的参数e,表示交易所对象,如果没有传入该参数,则默认使用当前交易所对象(exchange)。创建NewPositionManager对象后,可以使用其提供的方法进行仓位和交易管理。

javascript
GetAccount: 获取交易账户信息。 GetPosition方法:根据合约类型、方向和持仓信息列表,获取特定合约的持仓信息。 OpenLong方法:开多仓,传入合约类型和数量。 OpenShort方法:开空仓,传入合约类型和数量。 Cover方法:平仓,传入合约类型和数量。 CoverAll方法:平掉所有仓位。 Profit方法:计算特定合约的盈利情况。 ...
javascript
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_SHORTPD_LONG 是表示空头和多头仓位。因为目前是空头的仓位,所以多头仓位信息返回值将会是null。随后,代码调用 p.Cover() 方法对空头仓位进行平仓操作。最后使用Profit方法输出了仓位操作后的盈利情况。

点击开始回测,我们来看一下回测结果,首先看到istrading信息为true。9点下了一手螺纹钢期货合约,由于是空头仓位,所以多头返回的仓位信息为空值。然后10分钟后,平掉空头,获利89元。这就是一个使用交易类库下单的简单例子。相对于前面我们讲过的交易函数,可以发现使用交易类库可以更加方便快捷的下单,省去了交易参数设置的烦恼。

以上就是交易类库的简单用法,下节课我们将要学习多品种操作的异步处理方法和风控模块,我们下节课再见!

13:商品期货交易类库(下)

我们继续交易类库的学习,其实在在学习交易类库的过程中,我们不仅仅希望了解如何使用交易函数,更重要的是理解程序化交易背后的理论和逻辑机制。通过深入了解量化交易的原理和逻辑,我们可以更好地完善自己的量化策略,制定出符合个人交易思维的量化策略。

多品种操作

第二种是针对多品种的操作。 var q = $.NewTaskQueue(),q为创建的多品种队列控制对象。这里的多品种并不是意味着只能操作多个品种,而是多种操作同时处理,有点类似于并联的概念。这里涉及到了异步处理的概念。异步处理是一种编程模式,用于处理可能需要耗时的操作,在程序化交易中,市场信号的判断是很迅速的,然而价格的快速波动造成下单成交需要耗费更多的时间。在异步处理中,程序可以继续执行其他任务而不必等待比较耗时操作的完成。

这里给大家稍微解释一下同步处理和异步处理的不同。传统的同步处理方式会导致阻塞(blocking):就是当一个任务执行时,程序会一直等待该任务完成后再执行下一个任务。这可能导致程序运行效率低下,特别是在需要等待I/O操作完成时。而异步处理采用了非阻塞(non-blocking)方式,使得在执行比较耗费时间的操作时,程序可以同时执行其他任务。异步操作通常是通过回调函数等机制来实现的。

在异步处理中,当一个耗时任务启动后,程序立即将控制权返还给调用方,让其继续执行其他任务。然后,当耗时任务完成时,会触发相应的回调函数,用来处理任务的执行结果。由于异步处理不会阻塞主要流程,可以提高程序的性能和响应能力。相对于单品种的one-by-one串联的操作,异步处理可以较好的实现同时下达任务,各个任务在满足各自条件的情况下执行,所以各任务之间不会阻塞。因此,对于多品种或者多操作的复杂交易的场景,使用任务队列NewTaskQueue是非常适合的。

我们来举例示范一下。

javascript
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具体的参数是这样的:

javascript
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手。

这两个选项是根据个人或机构的风险偏好和交易策略来设定的。不同的交易者可能对风险管理有不同的要求,因此可以根据实际需要来调整这些参数。选择合适的工作日最多交易次数和单笔最多下单量可以帮助控制风险,确保交易活动在可控范围内进行。

一个好的交易函数可以集成风险管理工具和技术指标,帮助我们有效控制风险。通过在交易函数中实现止损、止盈、资金管理等功能,我们可以最大限度地降低交易风险,并保护资金免受不利市场波动的影响。在优宽平台,我们也有一些优秀的交易类库,比如止损类库,大家都可以学习参考下。

14:基于tick数据推算逐笔交易历史

在期货市场中,成交量和持仓的变化与价格涨跌之间存在密切的关系,这种关系常被用作技术分析工具。然而,交易所只提供总体的持仓量和成交量数据,而没有提供详细的开平方向信息,所以对于成交量和持仓量具体的组成和变化趋势,我们并不能得到具体的统计。

由于在商品期货市场,CTP协议没有提供订单流数据。所以如果想做一些,基于订单流变化数据的策略,可能会无从下手。幸运的是,在CTP协议给出的tick行情有足够的数据可以反推出订单流,但是需要注意的是,这里反推出的订单流,也只是tick切片之间的成交情况的合并信息。大家在应用于实盘的时候,需要谨慎的判断。优宽国内站上公开了一个反推算法(使用JavaScript语言实现),该算法对于我们理解价格涨跌,和仓位变化背后的逻辑关系,具有重要的学习意义。在本节课中,我们将对这个公开的代码,进行详细分析,以便更好地学习该算法的逻辑。通过深入研究这个算法,我们可以更好地理解市场动态和参与者行为,为我们的交易决策提供一些有价值的参考依据。

反推的逻辑机制

我们经常在交易明细里可以看到,不停滚动着8种“开平”信息,这里给大家讲解下这八种类型。

订单薄有卖单列表,买单列表。卖单订单包括:「卖出开空」或者「卖出平多」。买单订单包括:「买入开多」或者「买入平空」。

盘口订单下单方向1下单方向2
卖单卖出开空卖出平多
买单买入开多买入平空

4种订单类型的成交组合,会造成的持仓量变化,形成另外四种复合的“开平”信息:

方向类别卖出开空卖出平多
买入开多双开,持仓量增加多换,持仓量不变
买入平空空换,持仓量不变双平,持仓量减少

当买入开多和卖出开空的数量一致的时候,持仓量增加,可以定义为双开;

当买入开多和卖出平多的数量一致,持仓量不变,可以定义为多换;

当买入平空和卖出开空数量一致,这个时候是空换,持仓量不变;

最后买入平空和卖出平多数量一样,持仓量减少,定义为双平。

在介绍完这八种“开平”类型后,让我们了解一下怎样使用tick数据反推交易历史的逻辑机制。

交易历史反推主要通过比较前后两次tick的数据来进行分析。其中,我们会计算以下两个指标:1. 成交量的变动,2. 持仓量的变动。

持仓量的变动计算:将当前tick的持仓量减去前一次tick的持仓量,得到持仓量的变动值。

根据持仓量的变动值进行分类:

  • 如果持仓量的变动值大于0,表示持仓量增加,可以判断为增仓。
  • 如果持仓量的变动值小于0,表示持仓量减少,可以判断为减仓。
  • 如果持仓量的变动值等于0,则需要进行下一步判断。

成交量的变动计算:将当前tick的成交量减去前一次tick的成交量,得到成交量的变动值。

根据成交量的变动值进行分类:

  • 如果成交量的变动值大于0,表示有成交发生,可以判断为换手。
  • 如果成交量的变动值等于0,则表示没有新的成交发生。

根据持仓量和成交量的组合变化,可以得到如下情况:

  • 空闲(NONE):当某个交易品种的成交量变动值为0且持仓量变动值也为0时,我们可以称之为空闲状态。这表示在该时间段内,这个品种的交易没有发生。这是一种特殊的情况,需要额外的定义。
  • 换手(EXCHANGE):当某个交易品种的成交量变动值大于0,但是持仓量变动值为0时,我们可以称之为换手。这表示在该时间段内,这个品种的交易有成交,但没有新的持仓产生。包括多换和空换。
  • 单向增仓(OPEN):表示持仓量增加,并且成交量导致增加的净持仓量是单向开仓的交易。包括多开和空开。
  • 单向减仓(CLOSE):表示持仓量减少,并且成交量导致减少的净持仓量是单向平仓的交易。包括多平和空平。
  • 双向增仓(OPENFWDOUBLE):当持仓量增加的情况下,持仓量变动和成交量变动相同,表示持仓量增加,并且增加的持仓量中既有开多仓的交易,也有开空仓的交易。两者的数量一致,定义为双开。
  • 双向减仓(CLOSEFWDOUBLE):当持仓量下降的情况下,持仓量和成交量变动相同,表示持仓量减少,并且减少的持仓量中既有多平的交易,也有空平的交易,两者的数量一致,定义为双平。

可以看到,换手,单向增仓和单向减仓都有着不同的种类,所以我们需要价格的涨跌去进行进一步的区分。

在这以上三种情况中,如果价格上涨就意味着是多头的方向,包括多换,开多和空平;而如果价格下跌意味着空头的方向,包括空换,开空和多平;如果价格不变,意味着空闲,双向或者未知。这里价格涨跌的判断需要使用当前tick的最新价格和上一个tick的买价(判断下跌)或者卖价(判断上涨)。如果盘面盘口较大,最新成交价停留在盘口中间的某个位置,则需要和当前tick的盘口的买一价和卖一价比较。当然,特殊情况也一定要定义,未知Unkown,去描述无法判断的情况。了解完反推的逻辑之后,我们就可以编写代码去进行交易记录的反推。

首先我们定义一个构造函数,构造出用于计算逐笔成交的对象。接着定义逐笔成交信息的枚举类型的变量,包括8种正常“开平”信息和5种空闲或者未知的信息。这里定义涨为红色,跌为绿色,白色为价格不变。然后定义这些动作的枚举,首先利用持仓量和成交量定义delta_enum键,其中delta_enum_NONEEXCHANGEOPENFWDOUBLEOPENCLOSEFWDOUBLECLOSE分别表示空闲,换手,双开,增仓,双平,和减仓;然后使用价格的涨跌定义forward_enum键,forward_enum_UP表示价格上涨,forward_enum_DOWN表示下跌,forward_enum_MIDDLE表示持平。两个键的组合就可以定义对应的值,是一个包含两个元素的数组,分别是操作类型枚举值和颜色枚举值。

下面我们就是使用tick信息去定义delta_enum键和forward_enum键。首先利用当前tick(也就是info)和前一次tick(preinfo)去计算成交量变化值volume_delta和持仓量变化值open_interest_delta。然后利用两者我们刚才提到的关系去定义delta_enum键(这里定义为变量delta_forward),然后利用价格的涨跌去定义forward_enum键(这里定义为变量order_forward),最后根据两个键去选择相对应的值,包括操作类型枚举和颜色枚举。

开平信息持仓量/成交量价格
type_enum.NOCHANGE(空闲)delta_enum_NONE(空闲)forward_enum_UP(上涨)
type_enum.NOCHANGE(空闲)delta_enum_NONEforward_enum_DOWN(下跌)
type_enum.NOCHANGE(空闲)delta_enum_NONEforward_enum_MIDDLE(下跌)
type_enum.EXCHANGELONG(多换)delta_enum_EXCHANGE(换手)forward_enum_UP
type_enum.EXCHANGESHORT(空换)delta_enum_EXCHANGEforward_enum_DOWN
type_enum.EXCHANGEUNKOWN(未知)delta_enum_EXCHANGEforward_enum_MIDDLE
type_enum.OPENDOUBLE(双开)delta_enum_OPENFWDOUBLE(双向增仓)forward_enum_UP
type_enum.OPENDOUBLE(双开)delta_enum_OPENFWDOUBLEforward_enum_DOWN
type_enum.OPENDOUBLE(双开)delta_enum_OPENFWDOUBLEforward_enum_MIDDLE
type_enum.OPENLONG(多开)delta_enum_OPEN(单向增仓)forward_enum_UP
type_enum.OPENSHORT(空开)delta_enum_OPENforward_enum_DOWN
type_enum.OPENUNKOWN(未知)delta_enum_OPENforward_enum_MIDDLE
type_enum.CLOSEDOUBLE(双平)delta_enum_CLOSEFWDOUBLE(双向减仓)forward_enum_UP
type_enum.CLOSEDOUBLE(双平)delta_enum_CLOSEFWDOUBLEforward_enum_DOWN
type_enum.CLOSEDOUBLE(双平)delta_enum_CLOSEFWDOUBLEforward_enum_MIDDLE
type_enum.CLOSESHORT(空平)delta_enum_CLOSE(单向减仓)forward_enum_UP
type_enum.CLOSELONG(多平)delta_enum_CLOSEforward_enum_DOWN
type_enum.CLOSEUNKOWN(未知)delta_enum_CLOSEforward_enum_MIDDLE

最后这里设置了一个return,用来将preInfo重置为null,用于清空之前的tick数据,以便重新开始计算。总体的思路可以回顾一下,根据tick的成交量,持仓量和价格的关系,去定义两个键,然后根据两个键的匹配去选择相对应的开平类型,进而推断逐笔交易的历史。

这里我们实盘运行一下,可以看到随着时间的更新,相对应的交易记录信息不断的打印出来。需要注意的是,我们推算出来到结果是根据tick数据反推出来的,实际上盘面上交易过程是非常复杂、快速的,可能一次tick切片行情变动中混合了以上多种成交组合。为了进行实践的验证,我们可以根据利用逐笔交易数据计算出来的成交金额和CTP协议得出的结果对比,可以发现我们的逐笔成交量数据是可靠的。大家有兴趣可以尝试下,利用反推出来的交易记录数据去做更多的策略探索。

15:K线处理:底层机制

K线是一种用于表示一段时间内市场价格变动情况的图表。我们获取的k线数据,是根据CTP协议(中国金融期货交易所的交易接口协议)提供的tick数据合成的。作为伴随时间流的k线数组,每根K线包含开盘价、最高价、最低价和收盘价,交易量和持仓量关键数据。虽然我们可以直接使用现成的指标函数比如TA.MA()直接使用k线数据进行技术指标的计算,但这个过程实际上是一个黑盒,我们不能了解具体的底层K线处理机制。然而,了解底层K线的处理机制对于理解技术指标的计算过程和优化算法非常重要。

理解计算过程:了解底层K线的处理机制可以帮助我们理解技术指标的计算过程。通过了解每个步骤的具体实现逻辑和算法,我们可以更好地理解指标是如何被计算出来的,以及每个参数的含义和影响。

  • 优化算法:了解底层K线的处理机制可以帮助我们优化计算算法。通过深入了解K线数据的存储结构、遍历方式以及处理函数的性能特点,我们可以对算法进行改进,提高计算速度和效率。例如,使用更高效的数据结构、采用并行计算等方法,可以加快计算速度,提升策略的实时性。
  • 策略定制:了解底层K线的处理机制可以帮助我们自定义指标和策略。有时,现有的技术指标不能完全满足我们的需求,需要进行定制化开发。通过了解底层K线的处理机制,我们可以根据自己的需求编写新的计算逻辑,或者修改现有的指标算法,以实现更加符合我们策略的指标。
  • 容错处理:了解底层K线的处理机制可以帮助我们进行容错处理。在实际应用中,K线数据可能存在缺失、异常等情况,这可能会对计算结果产生较大影响。通过了解底层处理机制,我们可以针对特定的情况进行容错处理,例如使用默认值或插值法填补缺失数据,排除异常值等,以提高策略的稳定性和鲁棒性。

综上所述,了解底层K线的处理机制对于理解技术指标的计算过程、优化算法、策略定制和容错处理都非常重要。这样我们可以更好地理解和应用量化交易中的技术指标,提升交易策略的效果和稳定性。

今天,我们以移动平均值的计算为例,手写一个源码,探讨背后的k线处理机制。

javascript
function HANDMA(records, period) { var MA = []; // 存储移动平均值的数组 // 遍历数据数组 for (var i = 0; i < records.length; i++) { if (i < period-1) { MA.push(NaN); // 添加NaN } else { var sum = 0; // 计算每个数据点之前 period 个数据的和 for (var j = i - period + 1; j <= i; j++) { sum += records[j].Close; } // 计算当前数据点的移动平均值 var ma = sum / period; // 存储移动平均值到数组中 MA.push(ma); } } return MA; } function main(){ var c = KLineChart({}) while (true){ exchange.SetContractType('rb888'); var records = exchange.GetRecords(); MA_20_hand = HANDMA(records, 20); MA_20 = TA.MA(records, 20); Log('手动移动平均数据',MA_20_hand); Log('移动平均数据',MA_20); for (var i = 0 ; i < records.length ; i++) { var bar = records[i] c.begin(bar) c.plot(MA_20_hand[i], "手动移动平均", {overlay: true}) // 画在图表主图 c.plot(MA_20[i], "移动平均", {overlay: true}) // 画在图表主图 c.close() } } }

根据代码显示,移动平均值函数接受两个参数:records(包含K线数据的数组)和period(移动平均的周期)。函数通过遍历数据数组来计算移动平均值,并将结果存储在一个数组MA中。
具体的计算步骤如下:

  1. 创建一个空数组MA,MA是moving average,移动平均值,用于存储移动平均值。
  2. 遍历数据数组records。
  3. 如果当前索引i小于周期period-1,将nan添加到MA数组中,表示该位置的移动平均值暂时无法计算。否则,计算当前数据点之前period个数据的和。通过一个内部循环,从i - period + 1到i的范围,累加这些数据点的收盘价。
  4. 计算当前数据点的移动平均值,即将上一步计算的和除以周期period。
  5. 将移动平均值存储到MA数组中。
  6. 完成遍历后,返回MA数组作为结果。

在函数定义完成后,我们在主函数中,获取k线数据,然后调用HANDMA(records, 20)TA.MA(records, 20)函数分别计算了手动移动平均数据和移动平均数据,并将结果打印输出。使用KLineChart函数,我们进行了两条均线的呈现。

在回测结果中可以看到,在9点到9点30分之间,移动平均值设置20为周期,前20分钟内,移动平均值都是空值,当收集够足够的数量后,开始进行指标的计算。现成的指标和手动的指标两条线是重合的,证明我们的计算是没有问题的。

这里我们需要理解下,MA数组和K线数据的对应关系:
records是一个数组,其中包含了伴随时间流的多个K线记录。每个K线记录都包含了开盘价、最高价、最低价和收盘价等信息。

MA是一个用于存储移动平均值的数组,虽然在图中没有呈现,但是通过打印MA数组,它的长度与records数组相同。在计算移动平均之前的周期(period)内,MA数组中的元素都是空值(null)。这些空值用来表示移动平均计算过程中前期数据不足的情况。当然,我们也可以选择其他的处理方式来处理数据不足的情况。例如,可以使用累计周期的均值作为移动平均的替代值,或者使用其他的填充值来表示未达到k线数量的移动平均值。

在给定的代码中,当索引i小于移动平均的周期period-1时,也就是前期数据不足以计算移动平均值时,将nan添加到MA数组中。MA数组的第一个有效值元素对应着records数组中的第period个元素的移动平均值,然后伴随k线的更新,新的ma值被对应计算出来。这样就能确保移动平均值和K线数据之间的一一对应关系。通过这种对应关系,我们可以方便地从MA数组中获取每个K线数据对应的移动平均值,进行进一步的分析和处理。

代码确实不复杂,但重要的是我们需要深入理解底层K线的处理机制。当我们使用K线进行技术指标计算时,无论是MACD还是RSI,我们需要了解底层K线是如何被使用的,以及指标结果与K线的对应关系。我们可以更好地理解技术指标的计算原理和应用场景。

在同时,了解底层K线的处理机制可以帮助我们理解K线数据的结构和含义,以及K线的时间周期和采样频率。这对于正确地解读技术指标的结果至关重要。我们需要知道每个指标结果对应的是哪个K线数据,以及指标结果如何随着时间的推移而更新。我们可以看到,当我们设定的周期为分钟时,MA计算返回的结果也是以分钟作为周期。但是需要温馨提醒下,在最后一根k线没有完成的时候,MA最后的一个值也是不固定的。

当然这只是使用k线计算移动平均值的小例子,希望从这份代码出发,大家可以理解k线处理的底层机制,更进一步的帮助我们更好地理解一些复杂技术指标的计算过程。如果有兴趣的话,指标计算源码在优宽平台是公开的(link),我们可以参考学习,并根据实际需求,进行更适合我们交易理念的定制化的指标计算和策略优化。

16:K线处理:平均K线图算法

K线图是一种常见的股票或期货价格图形表达方式,它以矩形方块(称为K线实体)和细线(称为影线)来展示一段时间内的价格波动情况。K线图包含四个关键价格数据:开盘价(Open)、最高价(High)、最低价(Low)和收盘价(Close)。通过这些价格数据,可以进行以下的K线的处理:

  • 形态分析(Pattern Analysis):通过观察K线的形态和特征,如头肩顶、双底、三角形等,进行形态分析。这些形态模式可能表明市场的趋势反转或延续,并提供买入或卖出信号。
  • 技术指标(Technical Indicators):技术指标是根据价格和交易量等数据计算得出的一系列数学公式和统计数据。我们在前面的课程中讲到的一些技术指标包括移动平均线、macd等。这些指标可以帮助分析市场的超买超卖状态、价格动量和趋势等方面的信息。
  • 平均K线(Moving Average):平均K线使用简单移动平均线或指数移动平均线对股价进行平滑处理,以减少价格波动的影响。平均K线可以帮助识别长期趋势,并提供支撑和阻力水平的参考。
  • 合成K线(Synthetic K-line):合成K线是一种通过组合多个较短时间周期的K线图,生成一个更长周期的K线图的方法。例如,将五分钟K线合成为30分钟K线或小时K线等。合成K线可以降低噪音信号,并提供更清晰的趋势分析。

K线处理的目标是辅助分析股票或期货市场的价格走势,并给出交易决策的参考。不同的处理方法和技术可以结合使用,根据实际情况和个人偏好进行选择和应用。

在上一节课程中,我们学习了使用k线处理的底层机制,本节课我们将要学习k线处理之平均K线图,也就是Heikin-Ashi算法。常用的指标在talib等指标库中可以找到。但是对于一些比较冷门且实用的算法、指标就很难找到现成的,本节课,我们将利用JavaScript语言“手搓”这个算法函数,实现一个自定义的模版类库。

平均K线图(Heikin-Ashi)算法

介绍

平均K线图(Heikin-Ashi)是一种技术分析图表,常用于研究价格趋势和市场动态。它通过对每个价格点进行平均处理,以平滑价格波动,并提供更清晰的趋势信号。相比传统的K线图,平均K线图在绘制过程中考虑了前一根K线的信息,使得每根K线之间具有较强的连续性。这种平均处理有助于消除噪音和震荡,更准确地显示价格趋势。

平均K线图可以帮助交易者识别趋势的开始和结束,并捕捉价格的反转信号。常见的分析方法包括观察K线的颜色和形态,结合其他技术指标如移动平均线、趋势线、收敛/背离等,以提供交易决策的参考和确认。

计算方法

我们来看下计算方法,在这里我们将作为参数的K线数据中的开盘价、最高价、最低价、收盘价命名为:Open、High、Low、Close。将所要计算的平均K线图的开盘价、最高价、最低价、收盘价命名为:avgOpen、avgHigh、avgLow、avgClose。平均K线图的计算主要分两个部分的处理:

  • 第一部分初始Bar

因为这个算法是一个迭代算法,计算当前Bar的数据时需要引用到前一个Bar的数据(很多经典指标、算法都是这种迭代计算),所以第一根Bar的计算必然是与其后的迭代计算是不同的。根据这个指标的计算资料的描述,第一根平均K线Bar的计算方式是这样的:

javascript
avgOpen = (Open + Close) / 2 avgHigh = High avgLow = Low avgClose = (Open + High + Low + Close) / 4

平均开盘价 = (第一根K线开盘价和收盘价) 的平均值
平均最高价 = 第一根K线最高价
平均最低价 = 第一根K线最低价
平均收盘价 = (第一根开盘价 + 最高价 + 最低价 + 收盘价) / 4

  • 后续Bar迭代算法

除了第一根平均K线Bar之外,后续的平均K线Bar需要迭代计算。具体的计算方法是这样的,首先需要计算平均开盘价和平均收盘价。

平均开盘价 = (前一根K线平均开盘价 + 前一根K线平均收盘价) / 2
平均收盘价 = (开盘价 + 最高价 + 最低价 + 收盘价) / 4

接着利用平均开盘价和平均收盘价分别和最高价,最低价取最大值作为平均最高价,取最小值作为平均最低价。

平均最高价 = Math.max(最高价, 平均开盘价, 平均收盘价)
平均最低价 = Math.min(最低价, 平均开盘价, 平均收盘价)

javascript
avgOpen = (pre_avgOpen + pre_avgClose) / 2 avgHigh = Math.max(High, avgOpen, avgClose) avgLow = Math.min(Low, avgOpen, avgClose) avgClose = (Open + High + Low + Close) / 4

算法代码

在策略库创建一个策略,选择策略语言为JavaScript,选择策略类型为「模板类库」,命名为:JavaScript扩展指标库。如果还希望“手搓”一些其它指标算法,也可以直接加入到这个模板代码中。

javascript
// 使用JavaScript语言实现的扩展指标 /** * calcAvgRecords: 计算Heikin-Ashi,即平均K线图 * @param {Array<Object>} records - K线Bar数组 * @returns {Array<Object>} - 平均K线数组 */ function calcAvgRecords(records) { // 声明、初始化一个空数组,作为函数最终返回的变量 var ret = [] // 判断传入的K线数组参数records if (!Array.isArray(records)) { return null } // 遍历K线 for (var i = 0; i < records.length; i++) { var bar = records[i] var avgBar = {} if (i == 0) { // 处理第一根Bar,计算平均K线 avgBar.Open = (bar.Open + bar.Close) / 2 avgBar.High = bar.High avgBar.Low = bar.Low avgBar.Close = (bar.Open + bar.High + bar.Low + bar.Close) / 4 avgBar.Time = bar.Time } else { // 处理其它Bar,计算平均K线 avgBar.Open = (ret[i - 1].Open + ret[i - 1].Close) / 2 avgBar.Close = (bar.Open + bar.High + bar.Low + bar.Close) / 4 avgBar.High = Math.max(bar.High, avgBar.Open, avgBar.Close) avgBar.Low = Math.min(bar.Low, avgBar.Open, avgBar.Close) avgBar.Time = bar.Time } // 将计算出的平均K线Bar,依次放入数组ret中 ret.push(avgBar) } return ret } // 导出函数 $.CalcAvgRecords = calcAvgRecords

这段代码是一个计算平均K线的函数,接收一个K线数组作为参数,并返回一个包含平均K线的数组。函数首先声明并初始化一个空数组ret,用于存储最终结果。然后判断传入的K线数组records是否是一个数组,如果不是,则返回null。接下来,使用循环遍历每个K线对象。

在循环中,首先获取当前K线对象bar,然后声明一个空对象avgBar,用于存储计算得到的平均K线数据。
如果是第一根K线(即i == 0),则根据公式计算平均K线的各个属性值:开盘价(Open)为开盘价和收盘价的平均值,最高价(High)为原始K线的最高价,最低价(Low)为原始K线的最低价,收盘价(Close)为开盘价、最高价、最低价和收盘价的平均值,时间(Time)为原始K线的时间。

对于其他K线(即i > 0),则使用上一根计算得到的平均K线数据(ret[i - 1])来计算当前平均K线的各个属性值。开盘价(Open)为上一根平均K线的开盘价和收盘价的平均值,收盘价(Close)为当前原始K线的开盘价、最高价、最低价和收盘价的平均值,最高价(High)为当前原始K线的最高价、平均K线的开盘价和收盘价中的最大值,最低价(Low)为当前原始K线的最低价、平均K线的开盘价和收盘价中的最小值,时间(Time)为当前原始K线的时间。

最后,将计算得到的平均K线对象avgBar添加到数组ret中,并继续处理下一个K线对象。循环结束后,函数返回存储了所有平均K线数据的数组ret。

回测测试

javascript
function main() { var chart = KLineChart({}) while (true) { if (exchange.IO("status")) { // 设置螺纹钢主力合约 exchange.SetContractType("rb888") // 获取K线数据 var r = exchange.GetRecords() // 使用我们编写的算法函数,计算平均K线 var avgRecords = $.CalcAvgRecords(r) if (avgRecords) { // 使用KLineChart函数创建的对象画图,画出平均K线 avgRecords.forEach(function(bar, index) { chart.begin(bar) chart.close() }) } } Sleep(500) } }

运行这个测试代码,通过设置合约,获取数据,使用我们编写的算法函数,计算平均K线,并利用KLineChart画图。我们可以在模拟回测测试页面看到平均K线图的图表。

最后温馨提醒下,平均K线图在不同市场和不同情况下的有效性可能存在差异。平均K线图的平滑作用可以帮助过滤掉一些噪音和短期波动,提供更清晰的趋势信号。它特别适合应对相对稳定、趋势明显的市场情况,如有较长持续时间的上升或下降趋势;而在市场波动较大、行情不明确的情况下,可能无法捕捉到价格的小幅波动和短期反转信号,这个时候可能需要使用其他技术指标或方法来辅助分析。

17:K线处理:合成K线算法

前面两节的课程,我们讲解了k线处理的底层机制和平均k线的算法,今天我们来学习合成k线。

合成K线是将原始K线数据进行加工处理,生成更高周期的K线数据。它的作用主要有以下几个方面:

  • 数据压缩:合成K线可以将较低周期的原始K线数据进行压缩,生成较高周期的K线数据,从而减少数据量。这样可以降低数据存储和处理的成本,节省计算资源。

  • 信号过滤:通过合成K线,可以平滑原始K线数据中的噪声和小幅波动,从而过滤掉一些不必要的细节,使得价格走势更加清晰、规律更明显。这有助于有效识别和捕捉市场趋势,避免对短期波动做出错误决策。

  • 趋势分析:合成K线能够将较低周期的价格波动整合为较高周期的整体走势,更好地反映市场趋势的持续性和稳定性。通过分析合成K线的形态、趋势线和关键支撑阻力位,可以更精确地判断市场走势,并据此制定相应的交易策略。

  • 时间分析:合成K线能够将较低周期的价格行情映射到较高周期的时间尺度上,更好地观察和分析价格走势的时间特征。这对于判断市场行情的节奏、周期和重要时间点非常有帮助,有利于把握市场节奏并做出及时的交易决策。

我们举一个例子说明,在期货市场中,庄家洗盘是一种市场操纵行为,通常指庄家在短时间内通过大笔交易来推动市场价格,吸引、迷惑或激发其他交易者进行买入或卖出,从而达到牟利的目的。通过合成K线,我们可以更好地识别和分析庄家洗盘等操纵行为的影响,警觉潜在的风险和市场异常,从而在交易中做出更明智的决策。

综上所述,合成K线可以提供更高层次的数据视角,使得交易者能够更全面、准确地分析市场行情,并制定相应的交易策略,有助于提高交易决策的准确性和盈利能力。

在编写程序化交易策略时,根据不同的交易理念,经常会有需求使用一些非标准周期K线数据的情况,例如需要使用12分钟周期K线数据、半个小时K线周期数据等等。通常这类非标准周期是无法直接获取的。那么我们如何应对此类需求呢?

在我们理解底层k线的处理机制后,这个问题不再变得困难。非标准周期可以通过更小周期的数据,合并合成获取,可以想象一下,多个周期中的最高价,可以算作合成后的最高价,最低价算作合成后的最低价,开盘价不会变,就用合成这根K线原料数据的第一个开盘价,收盘价对应的是用合成这根K线的原料数据的最后一个的收盘价,时间就是取的开盘价的时间,成交量用该时间段内的交易量求和计算得出。

我们举例示范一下:

时间开盘价最高价最低价收盘价成交量
1688000400000370437083701370810574
168800046000037083713370637138572
1688000520000371337173711371611233
168800058000037163719371537179655
1688000640000371737243717372113698

这五个1分钟周期的数据,合成一根5分钟周期的数据,开盘价就是就是第一根
k线时间的开盘价:3704
收盘价是最后一根的收盘价:3721
最高价就找这里面最高的价格:3724
最低价就找这里面最低的价格:3701
成交量就是每根成交量的求和就可以52432

5分钟周期的起始时间就是第一根1分钟K线的起始时间,最后合成出的 一根5分钟K线是这样的数据:

时间开盘价最高价最低价收盘价成交量
1688000400000370437243701372152432

理解了初步的思路以后,我们来使用代码进行实现。

javascript
/*backtest start: 2023-06-26 09:00:00 end: 2023-06-27 09:00:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] mode: 1 */ function GetNewCycleRecords(sourceRecords, targetCycle) { // K线合成函数 var ret = [] // 检查周期数据 if (!sourceRecords || sourceRecords.length < 2) { return null } // 获取源K线数据的周期和倍数 var sourceLen = sourceRecords.length var sourceCycle = sourceRecords[sourceLen - 1].Time - sourceRecords[sourceLen - 2].Time var multiple = targetCycle / sourceCycle // 检查目标周期有效性 if (targetCycle % sourceCycle != 0) { Log("targetCycle:", targetCycle) Log("sourceCycle:", sourceCycle) throw "targetCycle is not an integral multiple of sourceCycle." } var isBegin = true //可以设置期货的开盘时间,为了简化代码,这里我们直接设置为true var count = 0 var high = 0 var low = 0 var open = 0 var close = 0 var time = 0 var vol = 0 // 设置为固定期货品种的开盘时间 for (var i = 0; i < sourceLen; i++) { if (isBegin) { if (count == 0) { high = sourceRecords[i].High low = sourceRecords[i].Low open = sourceRecords[i].Open close = sourceRecords[i].Close time = sourceRecords[i].Time vol = sourceRecords[i].Volume count++ } else if (count < multiple) { high = Math.max(high, sourceRecords[i].High) low = Math.min(low, sourceRecords[i].Low) close = sourceRecords[i].Close vol += sourceRecords[i].Volume count++ } if (count == multiple || i == sourceLen - 1) { ret.push({ High: high, Low: low, Open: open, Close: close, Time: time, Volume: vol, }) count = 0 } } } return ret } // 测试 function main() { while (true) { exchange.SetContractType('rb888') var r = exchange.GetRecords() var r2 = GetNewCycleRecords(r, 1000 * 60 * 5) Log('合成k线:', r2) if(r2!= null){ $.PlotRecords(r2, "合成k线") } Sleep(1000) // 每次循环间隔 1000 毫秒,防止访问K线接口获取数据过于频繁,导致交易所限制。 } }

这段代码是一个K线合成函数,用于将源K线数据按照指定的周期进行合成,生成新的K线数据。

函数接受两个参数:sourceRecords和targetCycle。sourceRecords是一个包含源K线数据的数组,每个元素都包含了K线的高、低、开、收、时间和成交量等信息。targetCycle是需要合成的目标周期,单位为时间。

该函数首先获取源K线数据的周期,即最后两个K线的时间差。然后通过计算目标周期与源周期的倍数关系,确定需要合成几个源K线形成一个新的K线。

其中if (!sourceRecords || sourceRecords.length < 2)表示如果sourceRecords为空或长度小于2,则返回null。这段代码的作用是确保源K线数据的有效性,如果数据不满足要求,则无法进行后续的合成操作。

这段代码的作用是检查目标周期(targetCycle)是否是源周期(sourceCycle)的整数倍。如果不是整数倍关系,那么会输出日志信息(Log)显示目标周期和源周期的值,并抛出一个错误(throw),提示"targetCycle is not an integral multiple of sourceCycle."(目标周期不是源周期的整数倍)。这是为了确保合成的新周期能够准确对应源周期,以保证数据的准确性和一致性。

接下来,函数通过遍历源K线数据的方式,根据目标周期进行合成。在合成过程中,使用isBegin变量表示是否达到期货的开盘时间(在这里我们直接设置为始终为true)。count变量用于计数,记录已经合成了多少个源K线。high、low、open、close、time和vol变量则分别用于保存合成K线的高、低、开、收、时间和成交量。

在期货开盘的时间,根据合成的规则,使用count变量确定是否达到目标周期或者走到最后一根k线的时候,合成一个新的K线,并将其添加到ret数组中。之后,重置计数器count为0,继续合成下一个K线。

最后,函数返回合成后的新K线数据数组ret。

整体的逻辑确实不复杂,但有一些细节我们需要注意下。确定使用原始k线的数目是通过计算目标周期与源周期的倍数关系,因此目标周期应该是源周期的整数倍,所以不能使用小周期去合成更小的周期的数据。

在合成过程中,我们遍历源K线数据数组,通过count变量记录已经合成了多少个源K线。

count变量有三个阶段:

  • 如果count为0,表示还没有开始合成一个周期的K线数据,所以将当前源K线数据的高、低、开、收、时间和成交量分别赋值给相应的变量(high、low、open、close、time和vol),并将计数器count加1。

  • 当count小于目标周期与源周期的倍数时,说明还没有达到一个完整的目标周期,可以继续合成。

  • 当count等于目标周期与源周期的倍数时,即达到一个完整的目标周期时,我们将当前合成的K线数据添加到ret数组中,并重置计数器count为0,以便继续合成下一个周期。

但是还有一种情况我们不能忽略,就是碰到不能达到一个完整的目标周期的时候,比如使用10分钟为周期进行合成数据,而期货市场在10点15分会中场停盘,10点十分以前是可以获取完整的目标周期的,在10点10分到10点15分,5分钟并不是一个完整的周期,这个时候我们选择使用10点10分到10点15的五分钟的数据进行合成。在代码中我们定义为如果已经遍历完所有的源K线数据(即i等于sourceLen - 1),需要将最后一段未合成的K线数据添加到ret数组中进行返回。

我们进行代码的测试,在主函数中,设置螺纹钢主力合约,通过 GetNewCycleRecords 函数 传入 原始K线数据 r , 和目标周期,1000(毫秒,就是一秒) * 60 * 5 即 目标合成的周期 是5 分钟线数据。然后使用画线类库进行画图。
在回测页面,可以看到在9点到10点之间,呈现12个5分钟合成k线。

我们的代码只是起到教学的作用,在实盘运行中,会碰到很多特殊的情况。例如不同的期货品种的开盘时间是不同的(日盘,夜盘,还有凌晨还在开盘的);第二种非标准周期,例如13分钟周期,就是不闭合的周期,这样的周期算出的数据不唯一,因为根据合成的数据起始点不同,合成出来的数据有差异。还有在夜盘开盘时,20点59分会有一根k线数据,表明集合竞价的k线,我们需要考虑下,因为第一根合成k线的时间会变为20点59分,所以造成夜盘周期的推移。这些实际情况,我们都需要考虑到,相信凭借大家的努力,都可以获得解决。

最后我们温馨提醒下,在期货市场中,合成K线有一些缺点需要我们注意:合成K线是通过对原始K线数据进行加工处理得到的,这个过程中可能会导致部分信息的损失。在同时,合成的过程中会有一定的延迟。这意味着合成K线上的信号和趋势可能比实际的市场行情稍有滞后,无法及时反映市场的变化和动态。甚至可能出现虚假信号的情况,由于合成K线对原始数据进行了整合和平滑处理,可能掩盖了一些短期的价格波动和噪音,使得市场走势看起来更加平稳。这可能会引发误判,导致交易者做出错误的决策。因此我们需要针对风险度不同的期货品种进行不同的合成k线处理。对于波动性较大的品种,比如纯碱和玻璃化工品种,合成K线的处理可能需要更加敏感和准确,以捕捉到更多的价格波动和噪音。而对于波动性较小的品种,比如玉米等农产品品种,则可以采用平滑处理,减少虚假信号的出现。

18:策略交互设计(一)

量化交易中的交互设计是指在量化交易系统或平台中,通过设计用户界面和用户体验,提供给交易者一种直观、高效的交互方式。它涉及到如何呈现交易数据和信息、如何操作交易功能和控件、如何反馈用户操作结果等方面。量化交易中的交互设计应该注重用户体验和效率,简洁明了、易于使用。通过合理的交互设计,可以提高交易者的满意度和系统的实用性。
优宽平台是一个提供量化交易策略开发和执行的在线平台,本平台交互设计是通过交互控件完成的。一个好的交互控件应具备良好的响应性能,快速响应用户的操作和指令;在同时提供及时的反馈,以确保用户对自己的操作有清晰的认知。在注重简洁、灵活和高效的同时,通过提供一定程度的用户定制能力,让用户根据自己的喜好和需求进行配置交互控件。优宽平台对以上的需求都进行了满足,下面我们就对优宽平台中的交互设计展开一个介绍。

首先我们来看下交互控制的设置。和策略参数一样,我们根据我们的需要进行不同类型参数的设置。策略交互控件同样具有五种类型,数字,布尔,字符,下拉框和按钮。我们举例示范下,我们可以设置一个按钮型,命名为action,描述为y一个操作;设置一个数值型,actionnumber,来定义指定操作的数量;布尔型ifaction,是或者否的选择;selected,下拉框,进行多项的选择;str,字符型,可以输入字符;在交互预览页面,我们就可以看到交互控件呈现的页面。

交互控件设置完成以后,在策略中我们通过全局函数GetCommand(),获取交互命令字符串。GetCommand()函数会获取策略交互界面发来的命令并清空缓存,没有命令就会返回空字符串。返回的命令格式为按钮名称加上参数,如果交互控件没有参数(例如不带输入框的按钮控件),那么命令就是按钮名称。

交互控件是在策略运行时才可以使用的,这就意味着在回测中,交互控件是不支持的。因此,首先我们创建实盘,定义策略标签,选择托管主机和运行策略,定义策略周期,添加交易平台,这样实时运行的实盘就创建成功了。

我们在实盘中运行一下这个控件测试代码,点开策略交互,显示出来我们刚才设置的五个按钮,我们分别点击一下。可以在日志信息中打印出来两个按钮的名称,如果有默认参数,就会附带上。按钮名称和按钮参数中间使用“:”进行分隔。如果我们只想打印参数,可以使用split函数。

这里我们先不设置控件需要进行的操作,只是使用log函数打印出来控件对应的操作,如果有数量的话,再加上数量。

javascript
function main() { while (true) { var cmd = GetCommand() if (cmd) { Log("cmd:", cmd) var arr = cmd.split(":") if (arr[0] == "action") { Log("操作,该控件不带数量") } else if (arr[0] == "actionnum") { Log("操作,该控件带数量:", arr[1]) } else if (arr[0] == "ifaction") { Log("是否进行操作", arr[1]) } else if (arr[0] == "selected") { Log("下拉框", arr[1]) } else if (arr[0] == "str") { Log("字符型", arr[1]) } Sleep(1000) } } }

我们在实盘中运行一下这个控件测试代码,点开策略交互,显示出来我们刚才设置的五个按钮,我们分别点击一下。可以在日志信息中打印出来两个按钮的名称,如果有默认参数,就会附带上。按钮名称和按钮参数中间使用“:”进行分隔。如果我们只想打印参数,可以使用split函数。

这里我们先不设置控件需要进行的操作,只是使用log函数打印出来控件对应的操作,如果有数量的话,再加上数量。可以看到,在实盘里,我们点击action按钮,会出现“买入,该控件不带数量”;点击actionnum,并设置数量2,会打印“操作,该控件带数量:2” 。请注意,这个数量2是字符型的,如果要确认为数值型,需要使用parseInt进行转换。其他的按钮大家可以自己修改探索一下。关于具体的使用场景,我们下面展开介绍。

接下来我们来看下交互控件的功能。

半自动化交易

我们来看第一个应用场景:半自动化交易。半自动化交易是一种结合量化交易和人工决策的交易方式。在半自动化交易中,交易者使用计算机程序或交易平台上的工具和功能来执行交易决策,但最终的交易操作需要交易者自己做出。交互控件在半自动化交易活动中尤其友好。半自动化交易需要快速决策和执行,交互控件的快速选择交易品种功能非常有用。通过简洁直观的界面设计,交易者可以迅速浏览可交易的品种列表或使用搜索功能快速找到所需的品种。交互控件还允许交易者预设常用的交易参数,如数量、价格、止损止盈等。预设参数功能极大地简化了操作流程,交易者只需在初始设置时输入一次参数,之后就能够随时使用这些预设参数进行快速下单,提升了下单的速度和效率。

同时,交互控件还具备错误提示和确认功能,为半自动化交易提供了额外的安全保障。在输入有误或存在潜在风险时,交互控件可以及时提醒并帮助纠正错误。此外,在交易最终执行前,交互控件要求交易者再次确认交易指令,以确保下单操作的准确性和安全性。这有助于半自动化交易避免因输入错误或误操作而造成的交易错误,进一步减少了风险。总体而言,交互控件为半自动化交易提供了便利、效率和安全性。这里我们举一个例子来示范使用交互控件来选择不同的期货合约,并进行开多仓和平多仓操作。

javascript
function main() { var contractlist = ['i2309', 'rb2309', 'hc2309']; var p = $.NewPositionManager(); var contract = null; // 将contract的声明放到while循环之前 while (true) { if (exchange.IO("status")) { LogStatus(_D()); var cmd = GetCommand(); if (cmd) { Log("cmd:", cmd); var arr = cmd.split(":"); if (arr[0] === "selected") { contract = contractlist[arr[1]]; // 不使用"var"关键字,直接赋值给外部声明的contract变量 Log("买入期货品种", contract); } else if (arr[0] === "buy") { p.OpenLong(contract, 1); } else if (arr[0] === "covernum") { var pos = p.GetPosition(contract, PD_LONG); if (pos == null || pos.Amount < parseInt(arr[1])) { Log('没有足够的多仓可平'); } else { Log("平多仓数量", arr[1]); p.Cover(contract, parseInt(arr[1])); } } } Sleep(1000); } else { LogStatus(_D(), "未连接CTP!"); Sleep(500); } } }

这里我们举一个例子来示范使用交互控件来选择不同的期货合约,并进行开多仓和平多仓操作。

这段代码首先创建一个合约列表 contractlist,其中包含了三个期货合约代码:'i2309', 'rb2309', 'hc2309'。

使用交易类库,创建一个新的单品种控制对象 p。
设置外部变量contract为空。
接着获取用户输入的命令,并将其存储在变量 cmd 中。
如果存在命令 (cmd),则执行以下逻辑:
记录命令信息到日志,输出命令内容。并将命令内容用冒号分割成一个数组 arr,这样为了获取不同命令的参数。
如果命令的第一个元素是 "selected",表示选择了某个期货品种,根据命令的对应元素从合约列表中获取对应的合约代码,打印到日志中。
在选择品种之后,我们需要使用$.IsTrading判断是否在交易时间,如果不在,需要及时提醒。

如果命令的第一个元素是 "buy",表示买入期货品种,使用单品种控制对象 p 执行开多仓的操作,买入数量为1。
如果命令的第一个元素是 "covernum",表示平多仓,首先通过 p 获取当前持仓的信息,如果持仓量为空或者持有的多仓数量不足,则输出 "没有足够的多仓可平";如果持有足够的多仓,先打印出来需要平多仓的数量,并使用cover执行平多仓的操作,平仓数量为命令的第二个元素。这里需要注意的是我们获取的arr[1]是一个字符型,需要转换为数值型。
关于每一个控件对应的操作我们就设置完成了,接下来我们要在实盘中查看一下控件的运行。

我们在实盘中,首先选择期货品种,日志信息显示选择成功,如果不在交易时间段,比如这个时间10点27,上午中间休息时间段,日志信息会显示不在交易时间。然后点击buy,点击一次开一手多仓,点击两次,开两手;然后我们使用covernum进行平仓,这里我们首先填写数字3,因为我们目前持有的多仓数目为2,所有会显示没有足够的多仓可平,我们改为数字1,日志信息中呈现我们平了一手多仓。

这就是使用交互控件进行交易的一个简单的例子。上面的代码示例演示了如何在策略中使用交互控件来实现期货交易的功能。通过设置不同类型的交互控件,我们可以提供给交易者一个直观、灵活的交互界面。用户可以通过点击按钮、选择下拉框等方式来输入指令,然后策略根据接收到的指令执行相应的交易操作。

请注意,这里只是为了展示控件的使用,还有很多不完善的地方。对于半自动化交易,使用交互控件可以使交易过程更加友好和灵活。通过在代码中集成交互控件,你可以与机器人进行交互,实现对交易的监控和操作,并根据需要进行自定义设置。例如,你可以添加一个交互控件来设置交易的执行时间,让用户选择在何时执行买入或平仓操作。这样,你可以根据市场的波动和行情状况,智能地调整交易时机,以提高交易的成功率和盈利能力。

19:策略交互设计(二)

如果你是一个“直觉型交易者”,厌烦于技术指标的死板或者频繁变动,但又经常感到懊恼没有参考某些市场的指标而冲动下单,交互控件可以帮助你增强交易决策。通过交互控件,你可以自定义并集成各种市场指标和数据源,以供参考和分析。例如,你可以设置下拉框控件来选择不同的指标,如移动平均线、相对强弱指标等。当你选择特定的指标后,交互控件可以在不停掉策略的情况下,实时选择不同的参数进行相应的计算和分析,并将结果反馈给你。并且交互控件还具有实时的调试功能,可以根据你的需要进行额外的策略代码的实时运行。通过交互控件的灵活组合和定制,你可以根据自己的交易风格和需求,获得更多个性化的交易参考和决策支持。

因此呢,交互控件不仅仅只有半自动化下单的功能,本节课我们来继续学习策略交互设计的实时修改参数和实时的调试功能,以及状态栏的自定义交互功能。

实时修改参数

在前面的课程中,我们介绍了策略参数的使用,主要针对于策略运行中的不同类型的参数进行控制。策略参数在策略运行中是不能被改动的,如果需要改动,则必须停掉策略修改参数才可以;而有的同学如果希望在交易过程中对策略实时的进行一定程度的控制和修改,交互控件可以帮助你完成这一目标。
让我们举一个例子试试看。假如你的日内策略是使用20秒均线作为基准。然而由于美联储加息,你预感今天市场波动比较剧烈,所以你希望改变你的策略,使用10秒作为参考基准。如果你不设置交互控件,改变参数需要你停掉实盘,修改参数,然后重启实盘;在美联储加息平稳后,重新修改均线参数为20。而如果你使用交互控件,这一个问题变得简单了起来。

首先在策略交互中设置一个交互控件period为数值型,默认值是20。回到代码,默认的period为20,因此策略在起始阶段,会使用默认周期为20,然后判断k线的数量是否满足计算均值,然后打印“当前周期为:20”,输出20周期的均值;然后在代码中书写交互控件控制语句,当点击交互控件修改period周期以后,就会调整均线计算的周期,然后打印出来对应period的值,进行新的period均线的计算。

javascript
function main() { while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已连接CTP!"); exchange.SetContractType('rb888') bars = exchange.GetRecords() var cmd = GetCommand(); if (cmd) { Log("cmd:", cmd); var arr = cmd.split(":"); period = parseInt(arr[1]) Log('当前周期为:', period) if (!bars || bars.length <= period) { continue } base = TA.MA(bars, period) Log('均线period: ', base[base.length - 1]) Sleep(1000); } } else { LogStatus(_D(), "未连接CTP!"); Sleep(500); } } }

我们在实盘中看下,为了快速呈现结果,我们使用秒为周期,策略开始,日志输出的是20周期的均线,这里我们修改一下,周期变为10,可以看到日志信息中开始输出10周期的均线。

这就是实时修改参数的一个小例子,确实实现了不需要停掉实盘,重新修改策略的参数。这种实时修改参数的技术可以帮助你根据市场情况、风险偏好等因素进行灵活的调整,从而优化和改进你的交易策略。与停止机器人并重新编译代码相比,它提供了更方便快捷的方式来调整参数,同时也可以避免中断实盘交易。

但需要注意的是,在实时修改参数时,要确保对参数的修改是合理和稳妥的。过于频繁或不恰当的参数调整可能会导致策略性能下降或产生意外的结果。因此,在使用这类功能时,建议谨慎评估和测试相关参数的变化对策略表现的影响,并确保在真实环境下具备足够的安全保障和风险控制措施。

实时调试功能

量化策略的实时调试功能可以帮助你在开发和优化策略过程中快速验证和调整策略逻辑,以及观察策略的实际表现。在策略运行过程中,你可以插入调试语句,查询关键变量、指标和决策点的值,这样就可以查看日志并分析策略逻辑的执行路径,以找出潜在的问题和改进点。

结合eval()函数,交互控件可以实现实时调试功能。eval()函数的定义和功能是可计算某个字符串,并执行其中的JavaScript代码,这样就相当于给策略一个后门,你可以随时调用JavaScript语句修改,或者查询策略的状态。当然这要在确保没有异常的情况下。

这里涉及到了交互控件的字符型控件。首先我们设置这个JavaScript交互控件。定义为字符型,然后设置默认代码为Log(_C(exchange.GetAccount))。_C是内置函数,该函数为重试函数,用于获取行情、获取未完成订单等接口的容错,该接口会一直调用指定函数直到成功返回。

回到代码主体,我们首先设置价格和数量都为0;在主函数中,首先打印初始的价格和数量;然后获取界面交互控件的消息。在判断有交互信息的时候,输出JavaScript的代码并打印出来。

控件代码的运行用到了try和catch语句。try和catch常常被用于异常处理。try关键字:try关键字标记一个代码块,表示要检测异常的范围。在这个代码块中,你可以放置可能会抛出异常的代码。这里我们执行eval函数。在这里,eval传入的参数JavaScript是一个字符串形式的代码。

catch关键字:如果在try中的代码执行过程中发生了异常,控制流将会跳转到catch块中。catch块用于捕获并处理异常。
e参数:在catch块中,e是一个表示异常的变量。通过使用e,你可以访问到抛出的异常对象,从而获取异常的相关信息。
整体来说,当eval(JavaScript)执行时,如果JavaScript包含的代码发生了异常,程序将会跳转到catch块,catch块中的代码将被执行。通过捕获异常并输出错误信息,你可以对异常进行处理,例如打印错误日志或采取其他适当的措施。

javascript
var price = 0; var amount = 0; function main() { Log("初始 price:", price, "初始 amount", amount); while(true){ var cmd = GetCommand(); // 调用API 获取界面交互控件的消息。 if (cmd) { // 判断是否有消息 var JavaScript = cmd.split(':', 2)[1]; // 分割 返回的消息 字符串,限制返回2个,把索引为1的 元素 赋值给 名为JavaScript 的变量 Log("执行调试代码:", JavaScript); // 输出 执行的代码 try { // 异常检测 eval(JavaScript); // 执行 eval函数,该函数执行传入的参数(代码)。 } catch(e) { // 抛出异常 Log("Exception", e); // 输出错误信息 } } Sleep(1000); } }

我们在实盘中进行测试,启动实盘,可以看到首先打印出来原始设置的价格和数量,都为0.点击控件进行调试,在这里我们首先使用默认的语句,exchange.GetAccount可以输出当前的账户信息。

javascript
Log(_C(exchange.GetAccount))

然后修改控件语句,设置新的价格和数量信息

javascript
price = 100; amount = 1;

现在我们查看下新的价格和数量是多少,日志里是不会呈现的,继续使用控件语句查看我们的修改是否成功

javascript
Log(price, amount)

可以看到日志信息打印出来了新的价格和数量。
我们也可以设置一个错误语句试试看:

javascript
log(price, amount)

这里使用了小写的log,是一个错误的语句。所以会抛出异常。
这就是一个简单的在线调试功能。eval()函数是一个JavaScript内置函数,用于将字符串作为代码进行执行。它可以实现动态执行代码的功能,但需要谨慎使用,因为存在安全风险。
在上面的代码中,通过eval(JavaScript)执行了传入的JavaScript代码,如果代码有异常,则会被catch块捕获,并输出异常信息。
虽然eval()函数提供了便利的功能,但由于它可以执行任意的JavaScript代码,所以可能会被滥用和攻击。你可以实现更多的功能,大家可以挖掘下。

状态栏交互功能

如果你认为交互控件的界面太过于死板,没关系,我们还可以定制个性化多功能交互控件页面,这里可以使用状态栏制定多功能交互面板。

我们先前使用状态栏,更多的是打印时间或者链接ctp信息,其实状态栏是可以定制化的,通过链接交互控件功能,我们就可以在状态栏进行我们需要的交互操作。

我们举一个比较粗糙的例子,在状态栏进行开仓和平仓的操作。

javascript
function main() { var tbl = { type: "table", title: "状态拦交互", cols: ["操作", "按钮"], rows: [ ["开仓操作", {"type": "button", "cmd": "open", "name": "开仓", "input": {"name": "开仓数量", "type": "number", "defValue": 1}}], ["平仓操作", {"type": "button", "cmd": "coverAll", "name": "全部平仓"}] ] } LogStatus(_D(), "\n", "`" + JavaScriptON.stringify(tbl) + "`") var p = $.NewPositionManager(); while (true) { if (exchange.IO("status")) { var cmd = GetCommand() if (cmd) { var arr = cmd.split(":"); if (arr[0] === "open") { p.OpenLong('rb2309', parseInt(arr[1])); } else if (arr[0] === "coverAll") { p.CoverAll('rb2309'); } } Sleep(1000); }else { Sleep(500); } } }

状态栏定制首先我们需要一个用于描述表格的对象tbl,这设计到了JavaScript语言网页设计中用于展示交互的功能,我们设计的很简单,

title: 表格的标题,这里是 "状态栏交互"。
cols: 表格的列名数组,这里是 ["操作", "按钮"]。
rows: 表格的行数据数组,每一行是一个数组,包含相应的单元格数据。
在这段代码中,tbl 定义了一个简单的表格,包括两列和两行。
第一列名为 "操作,第二列名为"按钮"。
第一行行名开仓操作,包含一个按钮,点击按钮时触发的命令是 "open",按钮的名称为"开仓",这里设置了一个input,需要用户手动输入开仓数量。
第二行行名是平仓操作,也包含一个按钮,点击按钮时触发的命令是 "coverAll",按钮的名称为 "全部平仓",这个按钮不需要提供其他的输入。
接下来我们在LogStatus中,通过这行代码输出表格对象的结构和内容。
交互控件的操作设置就是我们上述讲过的内容。
我们在实盘中看下,在状态栏里,可以看到点击open,填写数量,我们就可以开对应数量的多仓,然后点击coverall就可以全部平仓。
这里只是一个粗糙的展示,通过自定义的状态栏,我们可以进行更多分页,比如化工类,黑色系和农产品,操作可以有开仓,平仓和止盈等。这些功能的设计我们都可以实现。

总之,交互控件为交易提供了更多的灵活性和可操控性,使得交易变得更加智能化和个性化。通过合理地设计和使用交互控件,你可以实现更灵活的量化交易,提高交易的便捷性和效率。

20:策略中的定时设计

对于期货新手朋友来说,扛单是一个重要的话题。面对小额盈利时,新手会选择迫不及待的卖出。然而,在遭遇小额亏损时,由于人们不愿意承受亏损,常常坚信价格会回升,结果时间拉长,造成更大的亏损,只能忍痛割肉。如果有一个策略定时闹钟,可以帮助你在日内交易的结尾时间段,平仓所有仓位,无论盈利还是亏损。这个工具可以帮助你遵循纪律,避免情绪主导决策,有助于保护资金并控制风险,你愿意尝试一下吗?

其实类似的需求还有很多。如何给策略设计定时功能,让策略在指定的时间去处理某些任务。这样的需求在策略中要如何设计才好。一个策略里面可能要用到很多时间控制,这样来说我们把时间控制功能封装起来最好,这样可以最大程度的降低时间控制代码与策略的耦合性,让这个时间控制模块可以复用,并且在使用方面简洁易懂。

本节课我们来试着用代码创建一个闹钟。这个闹钟对象,用来设置指定的小时和分钟触发闹钟。该函数返回一个包含具有相关功能的对象。通过调用对象的方法,可以检查当前时间是否满足触发条件,并在满足条件时返回true。这个函数可以用于创建多个不同时间的闹钟对象,每个对象可以独立触发。

javascript
function CreateAlarmClock(triggerHour, triggerMinute) { var self = {} // 构造的对象 // 以下给构造的对象设置成员、函数 self.isTrigger = false // 当天是否触发过 self.triggerHour = triggerHour // 计划触发的小时 self.triggerMinute = triggerMinute // 计划触发的分钟 self.nowDay = new Date().getDay() // 当前时间是哪日 self.Check = function() { // 检查函数,检查触发,触发返回true,未触发返回false var t = new Date() // 获取当前时间对象 var hour = t.getHours() // 获取当前小数:0~23 var minute = t.getMinutes() // 获取当前分钟:0~59 var day = t.getDay() // 获取当前天数 if (day != self.nowDay) { // 判断,如果当前天,不等于记录的当天,重置触发标记为未触发,更新记录的天数 self.isTrigger = false self.nowDay = day } if (self.isTrigger == false && hour == self.triggerHour && minute >= self.triggerMinute) { // 判断时间是否触发,如果符合条件,设置标记isTrigger为true表示已经触发过 self.isTrigger = true return true } return false // 不符合触发条件,即为未触发 } return self // 返回构造完成的对象 } function main() { var q = $.NewTaskQueue() var p = $.NewPositionManager() var t_m = CreateAlarmClock(9, 5) var t_a = CreateAlarmClock(14, 58) var symbol = "MA888" while (true) { if (exchange.IO("status")) { exchange.SetContractType(symbol) var r = exchange.GetRecords() if(!r || r.length < 20) { Sleep(500) continue } if (t_m.Check()) { // 可以写: t1.Check() var fast = TA.MA(r, 10) var slow = TA.MA(r, 20) var direction = "" if (_Cross(fast, slow) == 1) { direction = "buy" } else if(_Cross(fast, slow) == -1) { direction = "sell" } if(direction != "") { q.pushTask(exchange, symbol, direction, 1, function(task, ret) { Log(task.desc, ret) }) } } if (t_a.Check()) { // 可以写: t.Check() p.CoverAll() } q.poll() LogStatus(_D()) } else { LogStatus(_D()) } Sleep(500) } }

首先创建函数名,包含两个参数,triggerhour和triggerminute,代表指定触发的小时和分钟数。

接着定义构造的对象self,然后给构造的对象设置成员、函数。

第一个isTrigger,判断当天是否触发过
第二个triggerHour,计划触发的小时
第三个triggerMinute,计划触发的分钟

nowDay,获取当前时间是哪日
接着我们定义一个检查函数check,检查触发,触发返回true,未触发返回false,
首先使用new Date获取当前时间对象t,
getHours获取当前小数:0~23
getMinutes获取当前分钟:0~59
getDay获取当前天
这是为了获取实时的时间,去和指定的时间进行比较,检查是否触发。

接下来我们使用if语句判断,如果当前天,不等于记录的当天(day != self.nowDay),重置触发标记为未触发(self.isTrigger = false),更新记录的天数(self.nowDay = day)

下面我们就要判断具体的时间是否触发,如果isTrigger为假,但是触发时间已经过去了(当前小时等于触发的小时 并且 当前的分钟大于等于触发的分钟),这个时候设置标记isTrigger为true表示已经触发过,使用return语句check会返回true,证明已经触发。

不符合触发条件,使用return返回false,就是未触发。
总结一下,这个Check方法,用于检查是否满足触发条件。该方法获取当前时间并与预设的触发时间进行比较。如果满足触发条件且当天尚未触发过闹钟,则将isTrigger标志设置为true,表示已触发,并返回true。否则,返回false表示未触发。

所以这里大家理解的关键点是,这里的check函数只是在精准达到触发时间点,返回一个true,在触发时间点之前和之后,check函数都是false,而istrigger在触发时间前一直是false,而在触发时间后一直是true。

最后我们返回构造完成的对象self。

通过使用构造函数,可以方便地创建多个独立的闹钟对象,并且每个对象都具有自己的触发时间和触发状态。
设计好构造“闹钟”对象的函数,在使用时只需一行代码即可创建一个“闹钟”对象。

例如创建一个对象t,每天下午两点五十八分触发。

javascript
var t = CreateAlarmClock(14, 58)

我们来示范一下这个闹钟的效果。在主函数中,通过不断调用t1对象的Check方法来检查是否触发闹钟。如果触发时间到达,则打印"触发时间到达"的消息;否则,打印"触发时间未达或已过"的消息。这样就实现了一个简单的闹钟程序,可以不断检查是否达到设定的触发时间。

设置回测时间是9点整到9点十分,可以看到,在9点5分,check返回true,打印“触发时间到达”,而在其他时间段,check返回false,打印“触发时间未达或已过”。而istrigger在触发时间前,一直是false,在触发时间后,一直是true。

闹钟定好以后,我们就要使用它去设计一个定时启停的策略。
我们的思路是这样的:期货在开盘的分钟以内,价格波动比较剧烈,容易造成技术指标的跳跃去形成虚假的交易信号,我们可以选择跳过这段时间,减少错误的操作,所以我们策略开始的时间不再是早上的九点钟,而是九点零五分。而作为日内的交易策略,在下午即将要收盘的时候(两点五十八分),平掉所有的仓位。

我们使用一个简单的日内趋势线交易策略,以天数为周期,如果快线(周期为10的均线)大于慢线(周期为20的均线),代表最近呈现上升的趋势,我们制定的日内策略就要开多仓;相反情况,如果快线小于慢线,代表最近呈现下降的趋势,我们就要开空仓。然后,在下午两点五十八分,不论盈亏,准时平掉日内的仓位。这个策略利用了最近的趋势进行交易,同时严格控制风险。在下午收盘之前,无论盈亏情况如何,我们都要准时平仓,以避免持仓过夜的风险。通过这样的操作,我们能够在日内交易中抓住短期趋势并及时离场,以保护资金并获取较稳定的收益。

在主函数中,我们创建两个对象,t_m,早上的闹钟(9点5分),t_a,下午的闹钟(2点58分)。

使用交易类库创建一个单品种对象p,和多品种控制对象q。
设定合约symbol,玻璃主力。
接着进入我们的循环,获取k线,如果数量不足,跳出。
判断是否满足开仓的时间点,当t_m.Check()为true,代表触发时间到,说明到了开仓时间9点5分,开始我们的日内趋势性交易策略。
定义快线和慢线,使用TA.MA,分别设置period是10和20 。
设置一个变量direction方向为空。
使用我们前面在交易类库中讲过的cross函数,判断金叉,就是快线超过慢线,设置方向为buy,开多头。
如果是死叉,快线下穿慢线,设置方向为sell,开空头。
当存在明显趋势,就是direction不为空的时候,利用多品种控制对象q进行对应开多或者开空的交易。
不要忘了这里多品种控制对象还需要q.poll轮询任务。

最后在下午结尾时间段,使用第二个闹钟t_a.Check(),如果触发,说明到了指定的时间两点五十八分,使用coverall全部平仓。

这个策略适用于趋势比较强的品种,我们试一下。在回测页面,设置时间为2022年11月份,到2023年7月13日,请注意,这里的底层K线周期需要设置为分钟,不能设置过大,否则可能直接跳过时间检测的点导致没有触发。运行这个策略,我们获得了1500多元的收益。

我们看一下回测结果,可以看到九点五分,我们开始了策略,下午两点五十八分,平掉了所有的仓位。这就是一个使用定时控制的日内交易策略。我们的闹钟实现了在固定时间点定时做某事的初衷。这个闹钟也可以用于多品种策略,在多品种策略中可以创建多个这样的“闹钟”对象,用于多品种的时间控制,互不影响。

我们的策略是一个日内的趋势性策略,重点是让大家了解策略的定时设计。如果我们想真正的实现一个日内的波段策略,可以从istrigger方面考虑。因为istrigger在触发时间前是false,而在触发时间后是true。两个闹钟的交叉时间段,第一个istrigger为true,第二个istrigger为false的时候,我们在这中间的时间段,可以编写我们的日内策略。

但是需要注意的是,回测测试运行,底层K线周期不能设置过大,否则可能直接跳过时间检测的点导致没有触发。

其实这个功能可以应用于很多方面,比如实盘定时启停机器人,后续课程我们将利用这一个策略,创建一个管理机器人的机器人教大家如何四折使用优宽量化。

21:策略中的止盈止损设计(一)

交易界有句古老的谚语:“会买是徒弟,会卖是师傅”。顾名思义怎么卖比怎么买更难,因为在买入的时候只需要判断行情是否开始就可以了;但是一旦买进之后,不但需要判断行情是否转向,还需要时刻控制风险。相信很多交易者都经历过过山车的行情,明明上车了最后还是以小赚甚至亏损的结果出局。或者本来可以以小亏出局,结果从小亏损积累成大亏损。所以从这点看,卖出比买入更为重要。

简单的说,卖出无非是两种情况:止盈和止损。如果运气很好,买入后价格开始上涨,这时就要考虑止盈的问题,否则可能账面上赚到了钱,没有在合适的位置获利了结,最后平亏出局。如果运气不好,买入后不久价格就开始下跌,这时就要考虑止损了,或者在开仓买入之前就应该考虑好止损的位置,否则小亏损会积累成大亏损。

从统计学的角度看,大部分亏损的单子,在之后的行情里都会回到成本价附近。但是如果遇到一次小概率大幅反向走势,可能会损失之前所有的利润或本金。因此对我们散户来说,可以大赚,可以小赚,也可以小亏,但是永远不能大亏。所以有这么一句话:止损让我们活着,止盈让我们活得更好。

今天我们就来介绍五种常见的止盈止损策略,分别是

  • 固定比例止盈止损
  • 移动止盈止损
  • 动态止盈止损
  • 波动性止盈止损
  • 时间止盈止损

固定比例止盈止损

固定比例止盈止损策略,是一种基于盈利和亏损的比例,设定止盈点和止损点的交易策略。它的思想很简单,该策略基于设定的固定比例来确定止盈和止损点。当价格达到预设的盈利目标时,进行止盈;当价格下跌达到预设的损失限制时,进行止损。

为了方便演示,本节课我们使用的都是多头止盈和止损的策略设计,对于空头的止盈止损策略,大家只需要改变少许参数就可以。

这个策略的思路是基于固定百分位止盈止损设计的。对于多头开仓,我们使用的都是当最新价格超越20均线时,进行入场。大家也可以设置别的入场信号,比如macd,双均线等等。
具体代码实现是这个样子的:

javascript
/*backtest start: 2023-07-10 09:00:00 end: 2023-07-10 11:10:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] mode: 1 */ // 固定百分位止盈止损设计 function main(){ var mp = 0; var buy_price = 0; var takeProfitPrice = 0; var stopLossPrice = 0; var p = $.NewPositionManager(); var c = KLineChart(); while(true){ if(exchange.IO("status")){ LogStatus(_D(), "已经连接CTP !"); exchange.SetContractType("MA888"); var records = exchange.GetRecords(); if (!records || records.length < 20) { Sleep(1000); // 等待获取足够多的K线数据 continue; } ma20 = TA.MA(records, 20) if (mp == 0 && records[records.length-1].Close > ma20[ma20.length-1]) { // 大于20均线,下多单 p.OpenLong('MA888', 1); pos = p.GetPosition("MA888", PD_LONG); buy_price = pos.Price; mp = pos.Amount; } var takeProfitRatio = 0.005; // 止盈比例 var stopLossRatio = 0.005; // 止损比例 // 根据开仓价计算止盈和止损价格 var takeProfitPrice = buy_price * (1 + takeProfitRatio); var stopLossPrice = buy_price * (1 - stopLossRatio); if (mp == 1 && records[records.length-1].Close > takeProfitPrice) { Log('止盈时间到'); p.Cover('MA888'); var buy_price = 0; var takeProfitPrice = 0; var stopLossPrice = 0; var mp = 0; } if (mp == 1 && records[records.length-1].Close < stopLossPrice) { Log('止损时间到'); p.Cover('MA888'); var buy_price = 0; var takeProfitPrice = 0; var stopLossPrice = 0; var mp = 0; } var buy_price_dw = buy_price == 0 ? NaN : buy_price; var takeProfitPrice_dw = takeProfitPrice == 0 ? NaN: takeProfitPrice; var stopLossPrice_dw = stopLossPrice == 0 ? NaN : stopLossPrice; for (var i = 0; i < records.length; i++) { var bar = records[i]; c.begin(bar); c.plot(ma20[ma20.length - 1], "均线20", { overlay: true }); c.plot(buy_price_dw, "买入线", { overlay: true }); c.plot(takeProfitPrice_dw, "止盈线", { overlay: true }); c.plot(stopLossPrice_dw, "止损线", { overlay: true }); c.close(); } } else { LogStatus(_D(), "未连接CTP !") } } }

首先设置一些变量,包括持仓变量,mp。实时买入价格buy_price,止盈价格takeProfitPrice,止损价格stopLossPrice,为了提升交易的成功率,我们这里使用了交易类库,同样这里为了方便画图,使用KLineChart函数。

按照固定的流程,首先检查ctp协议是否连接,接着设定合约甲醛主力,然后获取最新的K线数据和Ticker数据。
等收集够足够的k线数量后,使用移动平均线(MA)计算20均线(ma20)。

如果当前持仓为空(mp == 0),并且当前Ticker的买入价格大于20均线的最新值,就使用openlong执行下多单操作。下单的价格和实时的仓位信息的获取,可以通过getposition函数,通过price和amount属性,获取实时的买入价格和持仓数量。

固定的止盈止损比例是我们自己设定的,这里我们设定止盈比例(takeProfitRatio)和止损比例(stopLossRatio),都是0.005,这两个比例大家可以根据自己的交易理念进行修改。接下来根据买入价计算出止盈价格(takeProfitPrice)和止损价格(stopLossPrice)。这就是我们设定的止盈点位和止损点位。

接着进入我们的止盈和止损设计了。对于止盈操作,
如果持仓不为空(mp == 1)且K线数据的最新收盘价大于止盈价格,则执行止盈操作。使用交易类库的cover函数进行平仓。由于这一单已经完结,因此可以重新设置买入价,止盈价和止损价都为0,并将mp标记为0表示无持仓。

对于止损操作,如果持仓不为空(mp == 1)且K线数据的收盘价小于止损价格,则执行止损操作。止损平仓操作完成,同样重新设置mp,买入价,止盈价和止损价。

最后,使用klinechart函数,将20均线、买入价、止盈价和止损价绘制在图表上。在图表中,当不下单的时候,买入价、止盈价和止损价都是0,图表的跨度会非常大,比较难看。所以这里我们使用了三元表达式,当买入价、止盈价和止损价是0的时候,设置为nan。

循环进行上述操作,实现实时的交易决策和止盈止损操作。

在回测结果中可以看到,我们设置周期为分钟,当达到开仓条件,会执行开仓的操作,并且设置相对应的止盈和止损线,当最新的价格达到固定比例的止盈或者止损价格的时候,会进行对应的止盈或者止损的平仓操作。

总体来说,这个策略通过固定百分位止盈止损设计,根据价格与均线的关系进行交易决策,并且根据买入价设定止盈和止损价格,以限制风险和保护收益。

除去固定比例之外,我们也可以使用固定的点位变化进行固定点数止盈止损操作。当盈利或者亏损达到一定点数之后,进行迅速的平仓,实现止盈止损的目的。

移动止盈止损

固定的比例或者点位进行止盈止损有时候太过于死板。当我们主动止盈时,可能会遇到一大波行情,只赚到其中一小部分就错过了后续更大的利润。虽然这样的交易并不亏损,但心理上会有一种错失良机的遗憾感。为了解决这个问题,移动止盈止损策略采用了浮动止损的方法,就是在获得不同级别的浮动盈利之后,开启下一级主动止损模式。这样一方面可以持续享受较高的浮动盈利,同时也不断提高止损点位,用来实现更多盈利的目标。

这个策略实际上只有止损,没有止盈,随着盈利比例的提高,止损点也随之上移。这样可以更好地抓住市场的上涨趋势,同时尽量的减少亏损。

javascript
/*backtest start: 2023-07-10 09:00:00 end: 2023-07-11 15:00:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] */ // 移动止盈止损 function main() { var mp = 0; // 持仓状态,0表示空仓,1表示持仓 var buy_price = 0; // 开仓价格 var takeProfitRatio = 0.005; // 初始盈利比例 var stopLossRatio = 0.01; // 初始止盈止损比例 var p = $.NewPositionManager(); var c = KLineChart(); while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已经连接到CTP!"); exchange.SetContractType("MA888"); var records = exchange.GetRecords(); if (!records || records.length < 20) { Sleep(1000); // 等待获取足够多的K线数据 continue; } var ma20 = TA.MA(records, 20); // 计算20日均线 if (mp == 0 && records[records.length - 1].Close > ma20[ma20.length - 1]) { // 大于20日均线,下多单 p.OpenLong('MA888', 1); pos = p.GetPosition("MA888", PD_LONG); buy_price = pos.Price; mp = pos.Amount; } var takeProfitPrice = buy_price * (1 + takeProfitRatio); // 计算盈利价格 var stopLossPrice = takeProfitPrice * (1 - stopLossRatio); // 计算止盈止损价格 if (mp == 1 && records[records.length - 1].Close > takeProfitPrice) { Log("达到新的赢利点,更新比例和价格"); takeProfitRatio += 0.005; // 增加盈利比例 var takeProfitRatio = _N(takeProfitRatio); // 格式化浮点数 var takeProfitPrice = buy_price * (1 + takeProfitRatio); // 更新盈利价格 stopLossRatio -= 0.001; // 更新止盈止损比例 stopLossRatio = _N(stopLossRatio); // 格式化浮点数 stopLossPrice = takeProfitPrice * (1 - stopLossRatio); // 更新止盈止损价格 Log("更新盈利比例:",takeProfitRatio); Log("更新止盈止损比例:",stopLossRatio); } if (mp == 1 && records[records.length - 1].Close < stopLossPrice) { Log("达到止盈止损点,平仓并退出交易"); p.Cover('MA888') buy_price = 0; // 重置开仓价格 takeProfitRatio = 0.005; stopLossRatio = 0.01; mp = 0; } var buy_price_dw = buy_price == 0 ? NaN : buy_price; var takeProfitPrice_dw = takeProfitPrice == 0 ? NaN: takeProfitPrice; var stopLossPrice_dw = stopLossPrice == 0 ? NaN : stopLossPrice; for (var i = 0; i < records.length; i++) { var bar = records[i]; c.begin(bar); c.plot(ma20[ma20.length - 1], "均线20", { overlay: true }); c.plot(buy_price_dw, "买入线", { overlay: true }); c.plot(takeProfitPrice_dw, "止盈线", { overlay: true }); c.plot(stopLossPrice_dw, "止损线", { overlay: true }); c.close(); } Sleep(1000) } else { LogStatus(_D(), "未连接CTP!"); Sleep(1000) } } }

这个策略是一个移动止盈止损的交易策略。下面是这个策略的思路:

  • 初始化一些变量,包括持仓状态(mp)、开仓价格(buy_price)、初始盈利比例(takeProfitRatio)和止盈止损比例(stopLossRatio)。
  • 同样的固定程序,连接交易所,获取K线和行情数据。检查K线数据数量。
  • 计算20日均线(ma20)。
  • 空仓状态(mp == 0)使用20均线下多单,记录开仓价格并将持仓状态置为持仓(mp = 1)。
  • 根据初始盈利比例和止盈止损比例计算盈利价格(takeProfitPrice)和止损价格(stopLossPrice)。
  • 如果当前处于持仓状态(mp == 1)且最新的收盘价大于止盈价格,就要增加盈利比例,并更新盈利价格和止盈止损价格。

这里的关键是盈利比例和止盈止损比例的更新。

首先通过条件判断确认当前处于持仓状态(mp == 1)且当前收盘价超过上一个阶段的盈利价格。
如果满足条件,那么表示达到了新的盈利点,打印“达到新的赢利点,更新比例和价格”。
接着,将止盈比例(takeProfitRatio)增加0.005。
根据新的止盈比例,更新止盈价格(takeProfitPrice),计算方法为买入价格(buy_price)乘以(1 + 新的takeProfitRatio)。
新的盈利价格更新,这个时候减少止盈止损比例(stopLossRatio)0.001。

这里使用了_N格式化浮点数。最后,根据新的止盈价格和止损比例,更新止损价格(stopLossPrice),计算方法为止盈价格乘以(1 - stopLossRatio)。

最后显示更新后的止盈比例和止损比例。所以当价格呈现一定的上涨趋势,止盈止损的价格也会一直提升。一旦出现当前处于持仓状态且最新的收盘价小于止损价格的状况时,就会平仓并退出交易,重置开仓价格和相关变量。最后使用klinechart进行绘图的操作。

该策略的核心思想是根据价格与均线的关系判断买入和卖出时机,并根据盈利情况逐步调整比例和止盈止损比例。
在回测结果中可以看到,当价格达到新的盈利价格时,会增加盈利比例并更新盈利价格和止盈止损价格,从而保护已实现的盈利。而当价格达到止盈止损价格时,就会平仓并退出交易,可以限制损失。图表中我们也可以看到,相对于固定的止盈和止损点位,移动的止盈和止损线是会实时更新的。

通过使用移动止盈止损策略,我们可以更加灵活地应对市场变化,逐步提高止盈点位以获取更多的收益。这种策略能够在保护利润的同时,也给予市场更多的空间,以便捕捉更大的行情。当然,在实际操作中,需要基于市场情况和自身风险承受能力进行合理的设置和调整。
总之,通过采用浮动止损的移动止盈止损策略,我们可以在控制风险的同时,最大限度地提高利润。这种策略的灵活性和适应性,使得我们能够更好地应对市场的变化,规避踏空的遗憾感,并在交易中获得更多的盈利。

22:策略中的止盈止损设计(二)

本节课我们继续策略中的止盈和止损设计,本节课我们要介绍的三种策略是:动态止盈止损,波动性止盈止损和时间止盈止损。

动态止盈止损

动态止盈止损策略利用技术指标或其他市场信号来确定止盈和止损点。通过分析市场趋势指标,决定何时进行止盈和止损操作。因为指标实时计算的,所以这种策略能够根据市场情况作出灵活的决策。

移动止盈止损和动态止盈止损虽然有些相似,但在概念和实施方式上存在一些区别。移动止盈止损是通过逐步调整止盈和止损点位的数值来实现的。比如,当获得一定的浮动盈利后,将止盈点位从初始设定的水平A移动到更高的水平B,以获取更多的利润。而动态止盈止损是根据市场行情和交易情况进行实时调整的。对于止盈点位,可以根据技术指标或者其他因素来设定触发条件,当满足条件时进行止盈操作。对于止损点位,可以根据风险管理原则和市场情况来灵活设定。

动态止盈止损的策略有很多种,以下是其中几种常见的策略:
移动平均线策略:这个大家都很熟悉,该策略利用移动平均线来确定止盈和止损点位。

  • 波动带宽策略:这个前面我们也讲解过,代表策略,布林带等波动带宽指标来确定止盈和止损点位。
  • 技术指标策略:该策略使用技术指标作为判断依据来设定止盈和止损点位。常用的技术指标包括相对强弱指标(RSI)、移动平均收敛/扩展指标(MACD)等。根据技术指标的信号来调整止盈和止损点位,例如当RSI超过一定阈值时设定止盈点位,低于一定阈值设置止损点位。
  • 支撑阻力位策略:该策略基于价格的支撑位和阻力位来设定止盈和止损点位。

相对于直接使用价格来判断与止盈线和止损线的交叉,我们也可以使用技术指标来辅助判断止盈时机和止损时机。其中,相对强弱指标(RSI)是一种常用的技术指标之一。

RSI指标衡量了一段时间内价格上涨和下跌的强度,其数值范围在0到100之间。一般来说,当RSI数值超过70时,说明市场可能过热,即将出现调整或反转信号。此时可以考虑平仓并获利,进行止盈。当RSI数值低于30时,说明市场可能过冷,可能会有反弹或反转信号。此时可以考虑平仓止损,以防价格进一步下跌。

所以RSI指标进行止盈和止损时,可以将以下条件作为参考:

  • 止盈条件:当RSI指标的数值超过一个预设的阈值(如70)时,可以考虑进行止盈操作。
  • 止损条件:当RSI指标的数值低于一个预设的阈值(如30)时,可以考虑进行止损操作。

我们来看一下代码,因为我们使用rsi作为止盈和止损的信号判断,这里不需要设置买入价,止盈价和止损价,只需要设置持仓变量。

在连接ctp协议,设定主力合约获取k线数据,然后使用K线数据进行均线和rsi信号的计算,这里的rsi参数是14。同样的使用20均线作为多头开仓的信号,但是需要注意的是,由于我们使用了rsi信号,因此可以增加一个判断条件,当不处于超买区域,就是rsi小于70的时候,再进行开仓,这样可以避免在rsi在70左右徘徊的时候,频繁的开仓平仓造成手续费的浪费。我们使用的是倒数第二根rsi的值,这样是为了防止最新的k线没有走完,rsi会发生频繁变动。

然后我们设置开仓和平仓的信号,当持有仓位并且rsi大于70的时候,说明进入了超买区域,这个时候需要进行止盈,进行平仓。

另一方面,当rsi小于30的时候,进行止损。最后使用klinechart进行图表的绘制,我们将均线画在主图上,rsi和上限70还有下限30画在附图上,使用overlay=false就可以。

最后在回测结果中看到,当满足开仓条件后,使用实时计算出来的rsi数据,去判断止盈和止损的信号,当达到设定的阈值,会进行相应的止盈和止损操作。

可以发现,使用成熟的指标进行止盈止损的操作,似乎更加方便一点,不需要考虑买入线,止盈线和止损线的关系,只需要使用指标进行止盈止损的信号判断。但是需要注意的是,成熟指标可能在特定市场环境下失效或产生误导性的信号。并且单一指标的使用可能会导致误判或信号滞后。因此,必须对当前市场进行充分的分析和判断,结合其他技术指标和因素来确认止盈止损策略。

这些策略仅为示例,实际上还有许多其他动态止盈止损的策略,如基于市场走势形态、价格动能等的策略。选择适合自己的策略需要考虑个人的交易目标、风险承受能力和市场特点,同时建议在实践中进行更多策略参数的验证和调整。

波动性止盈止损策略

该策略基于市场的波动性来确定止盈和止损点。当市场波动性较高时,设定较宽松的止盈和止损点;而当市场波动性较低时,设定较紧密的止盈和止损点。这样可以根据市场行情的不同情况来动态调整止盈止损的幅度。

波动性止盈止损的原理是这样的:

  • 波动性测量:首先,需要度量市场的波动性。常用的指标包括标准差、平均真实范围(ATR)等。这些指标可以帮助确定价格的波动范围。

  • 止盈/止损点位:接着根据波动性指标,可以设定一个合理的止盈/止损点位。止盈/止损点位应该超过当前价格的波动范围,以确保能够捕捉到足够的利润和限制损失。例如,可以将点位设定在标准差或ATR的倍数之外。

随着市场的波动性变化,波动性止盈止损策略的止盈和止损点位是动态调整的。例如,如果市场波动增加,可以适当扩大止盈和止损点位;如果市场波动减小,可以适当缩小止盈和止损点位。

javascript
/*backtest start: 2023-07-10 09:00:00 end: 2023-07-10 11:10:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] mode: 1 */ // 波动止盈止损 function main(){ var mp = 0 var buy_price = 0 var takeProfitPrice = 0 var stopLossPrice = 0 var c = KLineChart() while(true){ if(exchange.IO("status")){ LogStatus(_D(), "已经连接CTP !") exchange.SetContractType("MA888") var records = exchange.GetRecords(); var ticker = exchange.GetTicker(); if (!records || records.length < 20) { Sleep(1000); // 等待获取足够多的K线数据 continue; } ma20 = TA.MA(records, 20) if (mp == 0 && ticker.Buy > ma20[ma20.length-1]) { // 大于20均线,下多单 exchange.SetDirection("buy") id = exchange.Buy(ticker.Buy + 2, 1) var order = exchange.GetOrder(id) var buy_price = order.Price mp = 1 } var std = talib.STDDEV(records, 14) //计算标准差 current_std = std[std.length-1] // 根据开仓价计算止盈和止损价格 var takeProfitPrice = buy_price + 2*current_std; //两倍标准差区间 var stopLossPrice = buy_price - 2*current_std; if (mp == 1 && records[records.length-1].Close > takeProfitPrice) { Log('止盈时间到') exchange.SetDirection("closebuy") exchange.Sell(records[records.length-1].Close, 1) buy_price = 0; // 重置开仓价格 var takeProfitPrice = 0; var stopLossPrice = 0; mp = 0 } if (mp == 1 && records[records.length-1].Close < stopLossPrice) { Log('止损时间到') exchange.SetDirection("closebuy") exchange.Sell(records[records.length-1].Close, 1) buy_price = 0; // 重置开仓价格 var takeProfitPrice = 0; var stopLossPrice = 0; mp = 0 } buy_price_dw = buy_price == 0 ? NaN : buy_price takeProfitPrice_dw = takeProfitPrice < 100 ? NaN: takeProfitPrice stopLossPrice_dw = stopLossPrice < 100 ? NaN : stopLossPrice for (var i = 0; i < records.length; i++) { var bar = records[i]; c.begin(bar); c.plot(ma20[ma20.length - 1], "均线20", { overlay: true }); c.plot(buy_price_dw, "买入线", { overlay: true }); c.plot(takeProfitPrice_dw, "止盈线", { overlay: true }); c.plot(stopLossPrice_dw, "止损线", { overlay: true }); c.plot(current_std, "std", { overlay: false }); c.close(); } } else { LogStatus(_D(), "未连接CTP !") } } }

这个代码是一个基于波动止盈止损的简单示例策略,适用于期货交易市场。以下是该策略的整体思路:
同样的,使用20周期均线作为基准,超过均线进行开多的(做多)操作。
使用标准差来计算止盈和止损价格。标准差是衡量价格波动性的指标,这里取当前标准差的两倍作为止盈和止损的价格区间。

实时波动性体现在这段代码中的两个方面:

标准差(std):在代码中使用了 talib.STDDEV() 函数计算标准差,以衡量价格的波动性。标准差是一种统计指标,用于测量数据的离散程度或波动性。通过计算价格数据的标准差,代码可以获取最近一段时间内价格的波动性信息。
波动性止盈和止损价格:在代码中根据开仓价计算了止盈(takeProfitPrice)和止损(stopLossPrice)价格,用于设置交易的止盈和止损条件。这里使用了当前标准差的两倍作为止盈和止损线的位置,即买入价加上两倍标准差和减去两倍标准差。
通过实时计算标准差并基于标准差设置止盈和止损价格,代码可以根据实时价格的波动性进行交易决策。如果当前价格超过止盈价格,则执行止盈操作;如果当前价格低于止损价格,则执行止损操作。这样就能够根据实时波动情况进行风险控制和收益保护,提高交易的效果。

可以在回测结果中看到,止盈和止损之间的距离并不是固定不变的,会随着价格的波动程度,区间进行动态的调整,因此在一定程度上具有更强的适用性。

这里的标准差周期(period)和标准差倍数(multiple)都是可以实时调整的,可以针对于不同种类的期货品种,例如波动性较大的化工类(纯碱,甲醇等),和波动性较小的农产品类(玉米和淀粉类)进行参数的调整。

时间止盈止损策略

时间止盈止损策略是一种基于持仓时间来设定止盈和止损条件的交易策略。它与传统的价格或指标止盈止损策略不同,它主要根据持仓时间来判断是否平仓。在时间止盈止损策略中,交易者会设定一个特定的时间阈值作为止盈时间和止损时间。当持仓时间达到设定的时间阈值时,交易者会平仓并退出交易。这种策略的核心思想是,在一段时间内,交易市场可能会出现波动和反转,因此设定一个时间限制来控制交易的持续时间,以避免过长时间的持仓可能导致的风险增加。

需要注意的是,时间止盈止损策略可能适用于某些特定的市场情况,但并不适用于所有情况。在使用该策略时,务必要结合市场趋势、价格波动等因素进行综合考虑,并严格控制风险,避免无谓的损失。同时,交易者也可以根据自己的交易风格和偏好,灵活调整时间止盈止损的阈值,并进行回测和优化,以寻找最适合自己的策略参数。我们前面课程讲到的《策略定时设计》中提到的日内趋势性策略就是这样的交易理念。大家可以重温复习下。

特定市场情况: 短期趋势交易:当市场处于短期明确的上升或下降趋势时,时间止盈止损策略可以在趋势延续的初期进行交易,并在设定的时间阈值内通过止盈或止损来退出交易。 波动性较高的市场:在市场波动性较高的情况下,价格会快速上涨或下跌,因此通过设置时间止盈止损可以避免在持仓时间过长时受到大幅波动的影响。 震荡市场:当市场处于震荡状态且没有明显的趋势时,价格反复上下波动,时间止盈止损策略可以帮助交易者利用市场的震荡进行频繁小幅的利润获取。

今天我们讲了五种止盈止损的设计,大家也不一定要严格纠结于这些概念的区别,并且市面上还存在许多止盈止损策略,重要的是理解这些策略的思想,以及如何用代码实现这些策略。希望通过本课程的学习,大家可以了解止盈止损策略的基本原理,理清其思路,并根据自己的交易理念,构建更适合自己的止盈止损策略。希望本课程能为你提供有益的指导和启发,帮助你在交易中更加有效地运用止盈止损策略。

23:定时启停机器人

大家都知道,由于中国商品期货交易时间规则限制,白天只有4个小时的交易时间,夜盘只有2~4个小时的交易时间,再加上某些品种还没有夜盘交易时间,综合下来平均一天只有6个小时的交易时间。如果使用优宽量化机器人24小时不停运行策略,则有一半多的时间是在无谓的消耗。所以很多人定个闹钟来启动和停止机器人,但这么做并不能保证每次的准时,而且每次需要登录网页并不方便。本节课我们就创建一个管理机器人的机器人,教大家如何四折使用优宽量化。

这个功能是通过优宽平台扩展API,调用托管者启动和停止方法来实现的。作为一个专业开放的量化平台,优宽量化支持程序化调用平台的各项功能,所以开放了平台的扩展API接口。平台扩展API为我们提供了更大的自由度和灵活性,大家能够根据自己的需求对平台的一些功能进行扩展和定制化,进而提升用户体验和业务价值。

目前扩展API的开放的功能有:

  • GetNodeList:
    返回请求中的API KEY对应的优宽量化交易平台账号下的托管者列表。

  • GetRobotGroupList:
    返回实盘分组列表。

  • GetPlatformList:
    返回交易所列表。

  • GetRobotList:
    返回实盘列表。

  • CommandRobot:
    该接口向指定实盘发送交互命令。

  • StopRobot:
    停止运行指定实盘。

  • RestartRobot:
    重启指定实盘。

  • GetRobotDetail:
    获取指定实盘详细信息。

  • GetAccount:
    返回账号的账户信息。

  • GetExchangeList:
    返回支持的交易所列表以及需要的配置信息。

  • DeleteNode:
    删除指定托管者节点。

  • DeleteRobot:
    删除指定实盘。

  • GetStrategyList:
    获取指定策略信息。

  • NewRobot:
    根据参数设置创建实盘。

  • PluginRun:
    使用扩展API接口调用调试工具功能。

  • GetRobotLogs:
    获取指定实盘的日志信息。

这些都是扩展api支持的功能。大家也不需要特别的记忆,只需要知道有这些功能,具体的方法查询可以到api页面。这些功能可以帮助开发者对优宽量化交易平台进行更好地管理、监控和调试。开发者可以根据自己的需求使用这些API,定制化平台的功能和业务逻辑,实现更丰富的交易策略和用户体验。

今天我们就以定时开启和停止实盘为例,讲解一下平台扩展API的使用方法。

需要提前说明的是,这个策略的运行是不需要在实盘中的,使用自己的电脑在后台运行就可以。这里为了方便展示,我们在优宽量化编辑器里编写策略,然后运行在自己的电脑中就可以。
首先我们需要申请优宽量化API:

打开账号设置,选择API接口标签,然后点击右上方创建新的ApiKey,默认“*”号即开启了所有权限。指定具体接口权限,需要输入对应的扩展API函数名,使用逗号间隔,例如:GetRobotDetail,DeleteRobot。就会赋予这个API KEY调用获取实盘详细信息接口、删除实盘接口的权限。创建完毕后页面就会显示“AccessKey”和“SecretKey”。

在优宽量化的扩展API中,可以使用StopRobot()函数来停止正在运行的机器人,使用RestartRobot()函数来启动已经停止的机器人,但是在这之前需要请求指定的URL,以及对“secretKey”和“accessKey”进行md5加密操作,才能调用StopRobot()函数和RestartRobot()。具体的步骤这样的:

python
import hashlib import requests from datetime import datetime import time import JavaScripton # 获取 API 请求参数 def getParam(version, ak, method, args): return { 'version': version, 'access_key': ak, 'method': method, 'args': JavaScripton.dumps(args), 'nonce': int(time.time() * 1000) } # 计算 MD5 def md5(param): paramUrl = f"{param['version']}|{param['method']}|{param['args']}|{param['nonce']}|{secretKey}" return hashlib.md5(paramUrl.encode('utf-8')).hexdigest() # 获取最终请求 URL def getFinalUrl(param): url = "https://www.youquant.com/api/v1?" return f"{url}access_key={accessKey}&nonce={param['nonce']}&args={param['args']}&sign={param['sign']}&version={param['version']}&method={param['method']}" # 获取 API 信息 def getAPIInfo(method, dateInfo): param = getParam("1.0.0", accessKey, method, dateInfo) md5Result = md5(param) param['sign'] = md5Result finalUrl = getFinalUrl(param) info = requests.get(finalUrl).JavaScripton() return info # 判断是否在交易时间段内 def isTrading(): currentTime = datetime.now() currentHour = currentTime.hour currentMinute = currentTime.minute if ( (currentHour >= 9 and currentHour < 11) or (currentHour == 11 and currentMinute <= 30) or (currentHour == 13 and currentMinute >= 30) or (currentHour >= 14 and currentHour < 15) or (currentHour >= 21 and currentHour < 23) ): return True else: return False # 填入你的 secretKey、accessKey 和 botId secretKey = ... accessKey = ... botId = ... # 主函数 def main(): while True: info = getAPIInfo('GetRobotDetail', [botId]) if isTrading(): if info['data']['result']['robot']['status'] == 4: getAPIInfo('RestartRobot', [botId]) print('启动策略') else: if info['data']['result']['robot']['status'] == 1: getAPIInfo('StopRobot', [botId]) print('停止策略') time.sleep(10) if __name__ == "__main__": main()
  • 首先导入需求的包:
  • 函数getParam:用于获取API请求参数对象。接收四个参数:version表示API版本号,ak表示访问密钥,method表示API方法,args表示API方法的参数。函数返回一个包含版本号、访问密钥、方法、参数和时间戳的字典对象。
  • 函数md5:用于计算MD5签名。接收一个参数param,该参数是getParam函数返回的参数对象。函数首先将版本号、方法、参数、时间戳和密钥拼接成一个字符串,然后对该字符串进行MD5加密,并以十六进制的形式返回加密结果。
  • 函数getFinalUrl:用于获取最终的请求URL。接收一个参数param,该参数是getParam函数返回的参数对象。函数将基础URL和参数对象中的访问密钥、时间戳、参数和签名拼接起来,形成完整的请求URL,并返回该URL。
  • 函数getAPIInfo:用于获取API信息。接收两个参数:method表示API方法,dateInfo表示API方法的参数。函数首先调用getParam函数获取参数对象,然后调用md5函数计算签名。接下来调用getFinalUrl函数获取最终的请求URL。最后,使用requests库向指定的URL发送HTTP请求,并将返回的信息解析为JavaScriptON格式后返回。

重启和停止实盘是需要判断是否处于交易时间段内的,函数isTrading:用于判断当前是否处于交易时间。函数获取当前时间的小时和分钟,并根据一系列条件判断是否在交易时间段内。如果满足条件,则返回True表示处于交易时间段内,否则返回False表示不处于交易时间段内。具体的判断条件包括上午9点至11点半、下午1点半到3点之间、晚上9点至11点之间。

其中secretKey和accessKey变量需要修改为刚才申请优宽量化API“AccessKey”和“SecretKey”,注意是字符串类型。全局变量botId则是指定的机器人ID号,数字类型。

接着就要设置我们的主函数了。该策略一共有3个可以修改的全局变量,分别是:secretKey、accessKey、botId。其中secretKey和accessKey变量使我们刚才申请的优宽量化扩展API,全局变量botId则是指定的实盘ID号,注意填写的时候前两个是字符串类型。最后一个数字类型。如下面的代码:

在主函数中,调用函数 getAPIInfo() 获取关于机器人的详细信息,保存在变量 info 中。
调用函数 isTrading() 判断当前市场是否在交易。

如果市场正在交易,并且视频的状态为4(就是暂停状态),则调用 getAPIInfo的('RestartRobot')方法 重启实盘,并输出日志'启动策略'。如果市场不在交易,并且实盘的状态为1(表示运行中状态),则调用 getAPIInfo的'StopRobot'方法停止实盘,并输出日志 '停止策略'。等待后继续下一次循环。

代码最后运行我们的主函数。我们保存这段代码,然后使用最简单的运行方法,将它运行在终端中,然后开始运行。现在处于实盘的运行时间,我们手动关闭实盘,可以看到在启停机器人,又为我们开启了实盘。

使用这种方式,我们就可以定时开启关闭实盘,而且不需要额外的花费。作为抛砖引玉,本节课讲解的策略只能管理一个机器人,不过相信你通读策略代码后,可以对该策略进行升级改进,可以升级为管理多个机器人,以及对优宽量化策略、托管者等诸多功能加以扩充。

使用这种方式,我们可以实现定时开启和关闭实盘,同时无需额外花费。作为抛砖引玉,本节课讲解的策略只能管理一个机器人,不过相信我们通读策略代码后,可以对该策略进行升级改进,可以升级为管理多个机器人。而且通过扩展api,我们也可以对优宽量化策略、托管者等功能进行更多的扩展和改进。这样可以更加灵活地管理和运行我们的实盘,提升整体策略的执行效率和功能性。

24:K线形态在策略中的应用:单K线形态

1990年,史蒂夫 · 尼森将古老的蜡烛图技术系统地介绍给了西方投资界,这一举动震惊了传统的技术分析方法,史蒂夫 · 尼森因此被誉为现代蜡烛图技术之父。蜡烛图不仅全球广泛普及,而且经久不衰,沿用至今。几乎在任何一个交易软件上都能看到它的身影,之所以如此流行,得益于其简单性和清晰性。

蜡烛线能够分割为不同的时段进行使用,不管是你想看1天、1小时、30分钟,都不成问题。蜡烛图用来描述特定时间内的价格波动状况。作为一段数据的容器。从最底层的Tick数据流开始,蜡烛线根据时间周期划分成一段一段,每个周期内的第一个价格就是开盘价,最后一个价格就是收盘价,周期内最高的那个价格就是最高价,周期内最低的那个价格就是最低价,每个容器里面都储存着开高低收、成交量、时间等数据。这就是蜡烛线,也就是k线的由来。

我们就来剖析一下蜡烛图,看看分别代表了什么?

  • 蜡烛线由一定时间以内的开盘价、收盘价、最高价和最低价组成;
  • 如果收盘价高于开盘价,我们称该蜡烛线为阳线,通常以红色蜡烛线表示,这里左边蜡烛线为阳线;
  • 如果收盘价低于开盘价,我们称该蜡烛线为阴线,通常以绿色蜡烛线表示,这里右边蜡烛线为阴线;
  • 蜡烛线的中空部分我们称为“实体”;
  • 蜡烛线上部分和下部分的细线我们称为影线;
  • 上影线的顶部为“最高点”;
  • 下影线的底部为“最低点”。

观察蜡烛线形态时可以同时考虑单个蜡烛线线和连续蜡烛线的情况。单个蜡烛线形态更多的是用于提供关于市场变化的一些信号,

比如这里的一些常见的形态:

Doji(十字线):开盘价与收盘价接近,没有实体或很小的实体。
Shooting Star(射击之星):上影线较长,实体同样没有或者很小。
Inverted Hammer(倒锤头线):上影线较长,实体较小。

观察连续蜡烛线时,可以关注以下一些常见的形态,连续蜡烛线形态则能揭示更多的趋势和模式。:

多头信号:如连续几个上涨的K线,或者多个看涨形态的组合。
空头信号:如连续几个下跌的K线,或者多个看跌形态的组合。
转变信号:比如这里的塔型顶形态、双顶/底形态等。

K线(蜡烛线)形态的判断存在主观性,不同的观察者可能会得出不同的结论。这可能导致在相同的数据上出现不一致的分析结果。因此可以使用标准化的技术分析库,确定各种不同形态的k线,这样可以在一定程度上减少人为主观因素对K线形态分析的影响。TA库通常提供了一系列已定义的技术指标和形态识别函数,包括各种常见的K线形态。通过使用这些标准化的函数,可以自动计算和识别K线的形态,减少个体观察者的主观偏见。

因此本课程将以单个K线和连续K线的形态,编写样例策略,来观察k线形态在策略中的应用。

单个K线的策略应用

首先我们来学习单个K线的形态在策略中的应用,其实单个k线的应用就是主要基于实体和影线的不同形态代表背后的多空趋势。

对于实体部分:

长阳线显示出价格的买盘强劲。阳线越长,价格收盘价就越高于开盘价。这意味着,价格自开盘价大幅走高,且买方力量强大。换句话说,市场多头力量超过空头,并最终推高价格。

长阴线显示价格卖盘强劲。阴线越长,价格收盘价就越低于开盘价。这意味着,价格自开盘价大幅下跌,且卖方力量强大。换一句话说,市场空头力量盖过多头,并最终推低价格。

对于影线部分:

上影线和下影线为交易者提供了重要的交易线索。

上影线显示出了价格的时段高点,而下影线则显示出了价格的时段低点。

蜡烛线越长,意味着买盘或卖盘越强。蜡烛线越短,意味着买盘或卖盘活动不强。

锤子线形态

本节课我们的第一个单k线示范形态是锤子线形态。

锤子线形态(Hammer Candlestick)是价格行为交易者在其职业生涯中最早学习的蜡烛图形态之一。从根本上说,锤子线形态是一种常见且非常强大的反转交易信号,通常可以暗示趋势的延伸或回撤阶段的结束。

锤子形态是单个k线模式。锤子线的技术形态是由K线实体较小而下影线较长的单根K线体现而出,K线的下影线会是K线实体部分的两倍以上,K线的上半部分处于实体部分,看起来K线就像一个锤子的形状。

下面是一个策略使用ta库判断锤子线形态作为多头入场的标志,并使用移动止盈止损类策略作为出场的标志,看一下锤子线形态是否真的有效。

javascript
/*backtest start: 2023-01-04 09:00:00 end: 2023-07-11 15:00:00 period: 1d basePeriod: 1d exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] */ // 移动止盈止损 function main() { var mp = 0; // 持仓状态,0表示空仓,1表示持仓 var buy_price = 0; // 开仓价格 var takeProfitRatio = 0.005; // 初始盈利比例 var stopLossRatio = 0.01; // 初始止盈止损比例 var p = $.NewPositionManager(); var c = KLineChart(); var symbol = 'SA888' while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已经连接到CTP!"); exchange.SetContractType(symbol); var records = exchange.GetRecords(); var longArr = talib.CDLHAMMER(records) // 调用talib库中的锤子线形态,返回数组 var long = longArr[longArr.length - 2] // 获取上根K线形态数据 if (mp == 0 && long > 0) { // 出现锤子线形态,下多单 Log('long',long) p.OpenLong(symbol, 1); pos = p.GetPosition(symbol, PD_LONG); buy_price = pos.Price; mp = pos.Amount; } var takeProfitPrice = buy_price * (1 + takeProfitRatio); // 计算盈利价格 var stopLossPrice = takeProfitPrice * (1 - stopLossRatio); // 计算止盈止损价格 if (mp == 1 && records[records.length - 1].Close > takeProfitPrice) { Log("达到新的赢利点,更新比例和价格"); takeProfitRatio += 0.005; // 增加盈利比例 var takeProfitRatio = _N(takeProfitRatio); // 格式化浮点数 var takeProfitPrice = buy_price * (1 + takeProfitRatio); // 更新盈利价格 stopLossRatio -= 0.001; // 更新止盈止损比例 var stopLossRatio = _N(stopLossRatio); // 格式化浮点数 var stopLossPrice = takeProfitPrice * (1 - stopLossRatio); // 更新止盈止损价格 Log("更新盈利比例:",takeProfitRatio); Log("更新止盈止损比例:",stopLossRatio); } if (mp == 1 && records[records.length - 1].Close < stopLossPrice) { Log("达到止盈止损点,平仓并退出交易"); p.Cover(symbol) var buy_price = 0; // 重置开仓价格 var takeProfitRatio = 0.005; var stopLossRatio = 0.01; var mp = 0; } for (var i = 0; i < records.length; i++) { var bar = records[i]; c.begin(bar); c.plot(long,{overlay: false}); c.plotchar(long > 0, {char: '锤子线', size: "15px", overlay: true}); c.close(); } Sleep(1000) } else { LogStatus(_D(), "未连接CTP!"); Sleep(1000) } } }

不需要额外的语句来描绘和判断锤子线形态,只需要使用ta库就可以。talib.CDLHAMMER(records)会计算锤子线形态,返回数组,当返回数组中出现正值的时候,我们判断为呈现锤子线形态,然后作为多头入场的信号,然后使用移动止盈止损功能作为出场的信号。

这里的画图同样使用klinechart,由于我们想在k线上标注锤子线,所以可以plotshape函数。

我们开看一下回测图像,可以看到当判断出现锤子线形态的时候,k线确实有一个很短或者几乎没有的上影线,而下影线都是较长的状态。但是是否真的作为一个底部反转的标志,经过观察整体的走势,效果并不明显。只是抓住了一个短暂的市场反转。

根据回测结果,可以看到只利用蜡烛图技术中的锤子线形态,作为趋势反转判断的依据说服力并不强。这是因为单独使用锤子线形态作为买卖依据可能存在一定的局限性。锤子线形态的有效性受市场环境的影响。因此,需要考虑整体市场走势和趋势状态和其他指标,用来确定确定是否适合使用锤子线形态作为交易信号。

裸K上下影线在交易策略中的应用

如果你认为ta库的k线形态判断对于你来说,相当于一个黑盒子,不能确定具体的使用方法,其实我们也可以利用k线上影线,实体和下影线的关系,自行编造函数去判断市场的形态。

一般情况下,上影线越长,证明阻力就越大,多头即将由强势转变为弱势,未来价格可能会回调或者下跌。反之下影线越长,证明支撑力就越大,空头即将由强势转变为弱势,未来价格可能会反弹或者上涨。所以

经典的K线理论告诉我们,在众多的K线形态图中,如果出现较长的上影线或下影线,就是市场即将要发生转势的时候,这也是判断市场趋势转变的重要参考之一。

总体来讲,长上影线和下影线出现的概率都是较低的,一旦出现就说明市场当时最强烈的趋势走向可能发生逆转,后市转而下跌的概率较大。不过在使用影线判断行情的时候,需要有个前提,那就是市场之前已经经历过较大的涨幅或者跌幅,积累了较大的能量。

根据上面的K线理论,我们就可以试着开发一个长上下影线的策略:

第一步:计算上影线长度UP

第二步:计算实体长度MIDDLE

第三步:计算下影线长度DOWN

多头开仓:下影线长度大于实体加上影线的和的BN倍

空头开仓:上影线长度大于实体加下影线的和的SN倍

多头平仓:上影线长度大于实体加下影线的和的SN倍

空头平仓:下影线长度大于实体加上影线的和的BN倍

注意,这里的BN和SN是系数,因为相对来说期货通常在上涨时涨的缓,下跌时跌的急,所以我们在做多或做空时分别给与不同的系数。

下面我们来编写代码,这里的重点是上影线长度UP,实体长度MIDDLE和下影线长度DOWN的计算。因为会出现阳线(收盘价大于开盘价)和阴线(收盘价小于开盘价),所以三个长度计算需要使用三元表达式。

javascript
function main() { var mp = 0; // 持仓标志,初始值为0 var p = $.NewPositionManager(); // 创建仓位管理器对象 var c = KLineChart(); // 创建K线图表对象 var symbol = 'SA888' while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已经连接CTP !"); exchange.SetContractType(symbol); var records = exchange.GetRecords(); if (!records || records.length < 2) { // 等待获取足够多的K线数据 Sleep(1000); continue; } var record = records[records.length - 2]; // 获取倒数第二根K线记录 var UP = record.High - (record.Close > record.Open ? record.Close : record.Open); // 上影线 var MIDDLE = (record.Close > record.Open ? record.Close : record.Open) - (record.Close > record.Open ? record.Open : record.Close); // K线实体 var DOWN = (record.Close > record.Open ? record.Open : record.Close) - record.Low; // 下影线 if (mp == 0 && DOWN > (MIDDLE + UP) * BN) { // 开多仓条件 p.OpenLong(symbol, 1); // 开多仓 mp = 1; // 更新持仓标志 } if (mp == 0 && UP > (MIDDLE + DOWN) * SN) { // 开空仓条件 p.OpenShort(symbol, 1); // 开空仓 mp = -1; // 更新持仓标志 } if (mp == 1 && UP > (MIDDLE + DOWN) * SN) { // 平多仓条件 p.Cover(symbol); // 平多仓 mp = 0; // 更新持仓标志 } if (mp == -1 && DOWN > (MIDDLE + UP) * BN) { // 平空仓条件 p.Cover(symbol); // 平空仓 mp = 0; // 更新持仓标志 } for (var i = 0; i < records.length; i++) { var bar = records[i]; c.begin(bar); c.plotchar( UP > (MIDDLE + DOWN) * SN, { char: '长上', size: "10px", overlay: true }); c.plotchar( DOWN > (MIDDLE + UP) * BN, { char: '长下', size: "10px", overlay: true }); c.close(); } Sleep(1000); } else { LogStatus(_D(), "未连接CTP !"); } } }

对于up值,第一个值high是确定的,第二个值当阳线的时候,是收盘价,否则当阴线的时候,是开盘价;middle值,对于阳线和阴线,要区分不同情况,同样使用三元表达式,确定算出来的值为正值;第三个down值,针对于阳线和阴线的不同情况,使用三元表达式进行区分。

接着我们定义市场的信号,当下影线长度大于上影线加实体的和的BN倍的时候,证明空头力量减弱,这个时候应该多头开仓或者空头平仓;而当上影线长度大于下影线长度加上实体的和的SN倍的时候,证明多方力量减弱,这个时候应该开平多或者开空。这里的BN和SN值不是固定的系数,可以进行调参。经过调参过后,针对于纯碱品种,开多平空BN是2,而开空平多SN的系数是1。

最后使用klinechart在图表中对于长上影线和长下影线进行标注。

我们来看下回测结果,从k线图可以看到,我们针对两种形态确实进行了明显的标注。而使用裸k针对于2023年上半年具有明显趋势的纯碱合约,确实取得了比较好的回测结果,但是当运用于实盘的时候,需要考虑不同的期货品种和市场环境,我们换一个期货品种,甲醇的主力合约,同一时间段的收益是负的,因此还是那句老话,实盘有风险,尝试需谨慎哈。

裸K简单直观,并且信息量丰富,是交易中最常见的基础数据和工具,长上下影线可以很好的衡量多头和空头的力量变化,并且其背后的原理也非常简单,这对初入门的量化新手非常友好。但是实战中的K线走势与理论并不会一模一样,也就是说我们在交易中追求的是一个大概率,而不是确定性。

可以总结发现,单一的k线形态无法完全捕捉市场的复杂性。应结合其他技术指标,如移动平均线、相对强弱指标(RSI)等,进行综合分析。这些指标可以提供额外的确认或过滤信号,增加交易决策的准确性。

25:K线形态在策略中的应用:多K线形态

本节课我们继续讲解K线形态在策略中的应用。除了使用单独的k线观察市场的反转,也可以通过观察多个K线形态的组合来判断市场的走势。

通过分析多个K线形态的组合构成,包括各种各样的形状和排列方式,可以提供更全面的市场走势判断。其中包括使用两根k线的吞没形态和平头顶部和平头底部形态。而三k线形态包括早晨之星和黄昏之星,白三兵和三只乌鸦,和两阳吃一阴与两阴吃一阳等等。当然还有使用五日k线的Breakaway 脱离形态等。这些形态的出现可以提供关于市场走势的信号,例如趋势反转、继续延续等。在我们的api文档里面还有很多k线的不同形态,大家有需要可以自行查找使用。

另外一种是结合不同的技术指标,例如移动平均线、相对强弱指标(RSI)、随机指标(Stochastic)等。通过观察这些指标和k线形态的交叉、背离等现象,可以更好地了解市场的趋势和反转点。

多k线形态

K线形态是通过观察价格走势的图表模式来进行分析的,它是根据特定的价格信息形成的图形模版进行识别,对于机器学习熟悉的朋友一定很熟悉,Pattern Recognition模式识别。针对于多个k线组成的模式,我们试着探索一下多种形态背后代表的市场走势预测。

本节课我们使用的例子是吞没形态。
吞没形态也叫抱线形态,它是由2根K线组成的复合形态(如上图所示),国内很多交易者会以“阳包阴”或者“阴包阳”来表述吞没形态。其中“阳包阴”为上涨吞没形态,“阴包阳”为下跌吞没形态。从图上看上涨吞没形态是一根大的阳线包住了前面的阴线,下跌吞没形态是一根大阴线包住了之前的阳线。

吞没形态是市场状态表象,但实际却蕴含了交易者之间的心里和资金博弈。它预示着市场价格走向即将反转,在实际使用中吞没形态会给分析和交易起到非常好的效果。尤其是在进出场点位上,或者止盈止损点位上,吞没形态都可以做比较好的参考。

本策略就来使用吞没形态作为一个开仓和平仓交易的信号。

javascript
/*backtest start: 2023-01-03 09:00:00 end: 2023-07-11 15:00:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] */ // 吞没形态 function main() { var mp = 0; // 持仓状态,0表示空仓,1表示持仓 var p = $.NewPositionManager(); var c = KLineChart(); var symbol = 'SA888'; while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已经连接到CTP!"); exchange.SetContractType(symbol); var records = exchange.GetRecords(); if (!records || records.length < 20) { // 等待获取足够多的K线数据 Sleep(1000); continue; } var atr = TA.ATR(records, 20)[records.length-2]; var engulfeds = talib.CDLENGULFING(records); //判断吞没形态 var engulfed = engulfeds[engulfeds.length-2]; if (mp == 0 && engulfed == 100 && atr > 30) { //空仓,上涨吞没开多 p.OpenLong(symbol, 1); mp = 1; } if (mp == 0 && engulfed == -100 && atr > 30) { //空仓,下跌吞没开空 p.OpenShort(symbol, 1); mp = -1; } if (mp == 1 && engulfed == -100) { //持多,下跌吞没平多 p.Cover(symbol); mp = 0; } if (mp == -1 && engulfed == 100) { //持空,上涨吞没平空 p.Cover(symbol); mp = 0; } for (var i = 0; i < records.length; i++) { var bar = records[i]; var currentEngulfed = engulfeds[i]; c.begin(bar); c.plotchar(currentEngulfed == 100, {char: '上涨吞没', size: "10px", overlay: true}); c.plotchar(currentEngulfed == -100, {char: '下跌吞没', size: "10px", overlay: true}); c.plot(currentEngulfed, '吞没指标'); c.close(); } Sleep(1000) } else { LogStatus(_D(), "未连接CTP!"); Sleep(1000) } } }

使用talib库进行吞没形态的判断。当出现上涨吞没的时候,吞没形态数组会返回正100,而当下跌吞没出现的时候,数组返回负100。

接着我们利用吞没形态进行开仓和平仓信号的判断。

当上涨吞没的时候,进行开多仓,而当下跌吞没的时候,进行平多仓;
与之相反的是,当下跌吞没的时候,开空仓,而当上涨吞没的时候,平掉我们的空仓。

另外,对于开仓,我们还有一个限制条件,就是atr。ATR 是一种技术指标,用于衡量价格波动的程度。它通常用于确定价格的波动幅度和设置止损/止盈水平。这里使用的atr周期是20。

这里我们增加了atr > 30,表示 Average True Range (ATR) 的值大于30。
在这个策略中,atr > 30的条件是为了过滤掉价格波动较小的情况,以避免在市场波动较小的时候频繁开仓。当前的ATR值大于30时,表示市场波动较大,可以考虑进行交易。

在代码最后,我们使用klinechart的plotchar函数,在吞没指标为100的时候,标注“上涨吞没”,而在指标为-100的时候,标注“下跌吞没”。

在回测分析中,在图表中可以看到,当出现上涨吞没信号时,表示今日的阳线完全包住了前一天的阴线;而当出现下跌吞没信号时,表示今日的阴线完全包住了前一天的阳线。这些信号被认为是市场价格反转的可能迹象。

然而,从整体上上看,这些信号有时候可能会出现的过于频繁,对应的收益图表中显示,尽管在某些情况下可能表现出较高的收益率,但是总体上显示收益的波动性较大,这意味着信号的稳定性较低。

这种不稳定性可能是由于多种因素导致的,例如市场环境的变化、特定交易策略的限制以及其他未考虑的因素。因此,在使用这些吞没信号作为交易决策依据时,需要谨慎,并结合其他技术指标和风险管理策略。

K线形态和技术指标的结合

这一部分我们来讲K线形态和技术指标的结合。

当我们结合K线形态和技术指标时,一个重要的概念是背离(Divergence)。背离是指价格走势与某个技术指标之间出现不一致的情况。它可以提供有关可能的市场转折点的信号。

在分析中,我们通常使用一种或多种技术指标来补充对价格走势的判断。例如,常用的技术指标有相对强弱指标(RSI)、移动平均线(MA)、随机指标(Stochastic Oscillator)等。

当价格走势形成新的高点或低点时,我们会观察技术指标是否呈现相应的高点或低点。如果价格走势形成了新的高点,但技术指标没有形成相应的高点,这就被称为“顶背离”(Bearish Divergence)。反之,如果价格走势形成了新的低点,但技术指标没有形成相应的低点,就被称为“底背离”(Bullish Divergence)。

具体来说,顶背离可以暗示着可能的市场顶部形成,表明买盘力量逐渐减弱,卖盘力量可能增强,市场可能转为下跌趋势。而底背离则可能暗示市场底部的形成,表明卖盘力量逐渐减弱,买盘力量可能增强,市场可能转为上涨趋势。

本节课我们可以以MACD指标和K线的背离为例进行讲解。

MACD指标(Moving Average Convergence Divergence)是我们的老朋友了,作为一种常用的趋势指标,它通过计算两个移动平均线之间的差异来显示价格动量的变化情况。MACD包括两条线,分别是快速线(MACD Line)和慢速线(Signal Line),以及一个柱状图。

快速线(MACD Line):快速线是MACD指标中的主要线条,也称为DIF线(Difference Line)。它通过计算短期移动平均线(通常是12日指数移动平均线)与长期移动平均线(通常是26日指数移动平均线)之间的差异而得出。快速线的数值反映了价格动量的变化情况。

慢速线(Signal Line):慢速线是MACD指标的辅助线条,有时也称为DEA线(Detrended Exponential Average)。它是对快速线进行平滑处理的结果,通常采用9日指数移动平均线来计算。慢速线的数值可以帮助判断价格走势的趋势以及买卖信号。

柱状图(Histogram):MACD柱状图是由快速线和慢速线之间的差值绘制而成的。柱状图的高低表示快速线与慢速线之间的差异程度。当快速线穿越慢速线向上时,柱状图呈现正值,可能表明买入信号;当快速线穿越慢速线向下时,柱状图呈现负值,可能表明卖出信号。

快速线(MACD Line):DIF线(Difference Line)。它通过计算短期移动平均线(通常是12日指数移动平均线)与长期移动平均线(通常是26日指数移动平均线)之间的差异而得出。

慢速线(Signal Line)DEA线(Detrended Exponential Average)。通常采用9日指数移动平均线来计算。

柱状图(Histogram):MACD柱状图是由快速线和慢速线之间的差值绘制而成的。

当MACD指标与K线形态出现背离时,可能会给出一些有意义的信号。背离的类型可以是底背离或顶背离,具体含义如下:

底背离(Bullish Divergence):当价格的低点形成新的低点,但MACD指标由负转正,就发生了底背离。这可能表明价格下跌动力的减弱,市场可能即将出现回升的机会。

顶背离(Bearish Divergence):当价格的高点形成新的高点,但MACD指标由正转负,就发生了顶背离。这可能表明价格上涨动力的减弱,市场可能即将出现下跌的机会。

注意:底背离和顶背离的判断方法有很多,大家可以根据自己的交易理念进行选择~~

在顶背离情况下,卖盘可能会接管市场控制权,因此平多开空符合这种预期。

在底背离情况下,买盘可能会接管市场控制权,因此平空开多符合这种预期。

我们根据这个思路来编写一下代码。

javascript
/*backtest start: 2022-07-18 09:00:00 end: 2023-07-24 15:00:00 period: 1d basePeriod: 1d exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] */ function main() { var mp = 0; // 持仓状态,0表示空仓,1表示持仓 var p = $.NewPositionManager(); var c = KLineChart(); var symbol = 'FG888'; while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已经连接到CTP!"); exchange.SetContractType(symbol); var records = exchange.GetRecords(); if (!records || records.length < 26) { // 等待获取足够多的K线数据 Sleep(1000); continue; } var macd = TA.MACD(records); var dif = macd[0]; var dea = macd[1]; var macd_value = macd[2]; var isBearishDivergence = records[records.length-2].Close > records[records.length-3].Close && macd_value[records.length-2] < 0 && macd_value[records.length-3] > 0; //价格升高,但是macd由正转负 var isBullishDivergence = records[records.length-2].Close < records[records.length-3].Close && macd_value[records.length-2] > 0 && macd_value[records.length-3] < 0; //价格降低,但是macd由负转正 if (mp == 0 && isBullishDivergence) { //空仓,底背离开多 p.OpenLong(symbol, 1); mp = 1; } if (mp == 0 && isBearishDivergence) { //空仓,顶背离开空 p.OpenShort(symbol, 1); mp = -1; } if (mp == 1 && isBearishDivergence) { //持多,顶背离平多 p.Cover(symbol); mp = 0; } if (mp == -1 && isBullishDivergence) { //持空,顶背离平空 p.Cover(symbol); mp = 0; } for (var i = 0; i < records.length; i++) { var bar = records[i]; c.begin(bar); c.plot(dif[i], 'DIF', {overlay: false}); c.plot(dea[i], 'DEA', {overlay: false}); c.plot(macd_value[i], title='MACD', {style : 'histogram', color: macd_value[i] > 0 ? 'red' : 'green', overlay : false}) c.plotchar(isBullishDivergence, {char: '底背离', size: "10px", overlay: true}); c.plotchar(isBearishDivergence, {char: '顶背离', size: "10px", overlay: true}); c.close(); } Sleep(1000) } else { LogStatus(_D(), "未连接CTP!"); Sleep(1000) } } }

在设置好合约,获取k线数据后,首先计算macd的三个值,可以直接使用内置函数,macd是一个两维数组,需要通过两个索引进行获取。接着我们来定义顶背离和底背离。

当收盘价高于前一根K线的收盘价,而MACD值由前一个正值转为负值,我们定义为isBearishDivergence,顶背离。

而当收盘价低于前一根K线的收盘价,但是MACD值由前一个负值转为正值,我们定义为isBullishDivergence,底背离。

如果变量isBearishDivergence的值为true,顶背离出现,可以根据这个信号和仓位,执行开空平多的交易策略。

与之相反的当我们定义为isBullishDivergence,底背离出现的时候,根据仓位,执行开多或者平空。

最后我们使用klinechart函数将macd的三个值和顶背离和底背离的信号在图表中进行呈现。

在图表中,我们对某些背离反转点进行了有效的判断,比如这里的底背离和顶背离的判断,但是整体来看,判断在有些时候是存在一些偏差的,而且该信号但并不能捕捉到所有的市场反转趋势。这说明了背离的出现并不意味着即将发生市场转折,但它可以作为一个警示信号,引起投资者的关注。因此,在使用K线形态进行市场走势分析时,结合技术指标的背离现象可以提供额外的参考,帮助投资者更全面地了解市场的可能走势。然而,投资决策需要综合考虑其他因素,不能仅仅依靠K线形态和背离现象进行判断。

最后我们总结下,K线形态分析作为技术分析的一部分,仍有一些缺点和需要注意的地方。

噪音和虚假信号:市场中存在许多噪音和虚假信号,这可能导致K线形态的错误判断。建议不仅仅依赖于K线形态,还要结合其他技术指标和市场背景进行综合分析。

过度解读:有时候人们可能过度解读某些特定的K线形态,给予其过高的重要性。然而,单单依靠K线形态本身并不能提供完整的市场分析。建议将K线形态作为辅助工具,与其他技术指标、趋势线等进行结合分析。

适用性限制:不同的K线形态在不同的市场和时间周期中的有效性可能会有所不同。某些形态可能在某些市场中表现出良好的预测能力,而在其他市场中则可能不那么有效。建议根据具体的市场和时间周期评估K线形态的适用性。

风险控制:K线形态分析应结合良好的风险控制措施。单纯依靠K线形态进行决策存在风险,因为市场有时候会出现意外的波动或突破。建议结合止损、止盈等风险管理工具,以保护投资和交易资本。

最后,啰嗦几句,K线形态分析只是技术分析的一种工具,网上的很多大v使用它断言一些有关市场趋势和反转信号。然而,它并非完美的预测工具,我们要对这个工具建立正确的认识,在使用的时候需要谨慎,并结合其他技术指标和市场信息进行综合分析和决策。

26:回测评价系统中的指标计算

怎样是一个好的量化策略呢?是收益率秒杀吗。其实一个好的量化策略不仅仅通过收益率来定义。一个高风险高收益的量化策略,在一个单边的行情中可能很吃香,但是遇到极端行情,可能会丢失全部的收益甚至出现极端的亏损。因此,一个好的策略应该在多个市场环境下保持一致的表现,并具备一定的稳定性。策略应该在不同周期和市场条件下表现良好,而不仅仅依赖于某个特定的行情。

因此,在我们制定好量化策略以后,我们可以使用多种指标来评估交易策略的性能。以下是一些常用的指标及其计算方法:

总收益率(Total Returns):表示交易策略的累计收益率。计算方法为总资产的变化百分比:(总资产 - 初始资产) / 初始资产。 年化收益率(Annualized Returns):表示交易策略在一年内的平均收益率。计算方法为总收益率除以回测期间的年交易天数,再乘以252(假设一年有252个交易日)。 夏普比率(Sharpe Ratio):衡量每单位风险所获得的超额收益。计算方法为年化收益率与无风险利率之差的比值除以策略收益率的标准差。 波动率(Volatility):反映交易策略收益的波动程度,衡量风险大小。常用的计算方法有历史波动率和收益率标准差。 最大回撤(Max Drawdown):衡量交易策略从最高点到最低点的最大损失。计算方法为从某个高点到后续低点期间资产净值的最大降幅百分比。 胜率(Winning Rate):表示交易策略盈利交易的比例。计算方法为盈利交易次数除以总交易次数。

这些指标可以帮助我们评估交易策略的风险和收益表现。在回测评价系统中,我们可以通过计算这些指标来得出对策略的综合评价,并根据评价结果做出相应的调整和优化。

这些指标算法在回测系统中都是现成的,当你跑完策略,这些指标会自动呈现。但是由于这些指标的算法是自动集成的,对于我们来说相当于一个黑箱,当我们无法了解指标算法的具体实现细节时,可能会出现以下几个问题:

误解指标含义:缺乏对指标算法的具体了解可能导致我们对指标含义的误解。我们可能无法准确理解指标所衡量的是什么,以及该如何正确解释和解读指标的结果。这可能影响我们对策略绩效的评估和决策。

不适用于特定情境:不同的策略和市场环境可能对指标有不同的要求。如果我们无法了解指标算法的具体实现细节,就无法判断指标是否适用于我们所关注的特定情境。这可能导致我们在评估策略时使用了不恰当或不准确的指标。

无法验证指标准确性:由于无法了解指标算法的实现细节,我们无法对其准确性进行验证。虽然这些指标通常是由专业人士和学术界制定并经过验证的,但我们无法直接验证其背后的具体计算方法。

本节内容我们就以YOUQUANT的回测系统中的回测绩效为例,剖析策略回测中的夏普率、最大回撤、收益率等指标算法,进而可以帮助你搭建自己的回测指标评价系统。

javascript
function returnAnalyze(totalAssets, profits, ts, te, period, yearDays) { // force by days period = 86400000 if (profits.length == 0) { return null } var totalReturns = profits[profits.length - 1][1] / totalAssets var yearRange = yearDays * 86400000 var annualizedReturns = (totalReturns * yearRange) / (te - ts) // MaxDrawDown var maxDrawdown = 0 // 最大回撤 var maxDrawdownTime = 0 // 最大回撤时间戳 var maxAssetsTime = 0 // 最大资产时间戳 var maxDrawdownStartTime = 0 // 最大回撤开始时间 var winningRate = 0 // 胜率 var winningResult = 0 var maxAssets = totalAssets for (var i = 0; i < profits.length; i++) { if (i == 0) { if (profits[i][1] > 0) { winningResult++ } } else { if (profits[i][1] > profits[i - 1][1]) { winningResult++ } } if ((profits[i][1] + totalAssets) > maxAssets) { maxAssets = profits[i][1] + totalAssets maxAssetsTime = profits[i][0] } if (maxAssets > 0) { var drawDown = 1 - (profits[i][1] + totalAssets) / maxAssets if (drawDown > maxDrawdown) { maxDrawdown = drawDown maxDrawdownTime = profits[i][0] maxDrawdownStartTime = maxAssetsTime } } } if (profits.length > 0) { winningRate = winningResult / profits.length } // trim profits var i = 0 var datas = [] var sum = 0 var preProfit = 0 var perRatio = 0 var rangeEnd = te if ((te - ts) % period > 0) { rangeEnd = (parseInt(te / period) + 1) * period } for (var n = ts; n < rangeEnd; n += period) { var dayProfit = 0.0 var cut = n + period while (i < profits.length && profits[i][0] < cut) { dayProfit += (profits[i][1] - preProfit) preProfit = profits[i][1] i++ } perRatio = ((dayProfit / totalAssets) * yearRange) / period sum += perRatio datas.push(perRatio) } var freeProfit = 0.03 // 无风险收益率 var sharpeRatio = 0 var volatility = 0 if (datas.length > 0) { var avg = sum / datas.length; var std = 0; for (i = 0; i < datas.length; i++) { std += Math.pow(datas[i] - avg, 2); } volatility = Math.sqrt(std / datas.length); if (volatility !== 0) { sharpeRatio = (annualizedReturns - freeProfit) / volatility } } return { totalAssets: totalAssets, yearDays: yearDays, totalReturns: totalReturns, annualizedReturns: annualizedReturns, sharpeRatio: sharpeRatio, volatility: volatility, maxDrawdown: maxDrawdown, maxDrawdownTime: maxDrawdownTime, maxAssetsTime: maxAssetsTime, maxDrawdownStartTime: maxDrawdownStartTime, winningRate: winningRate } }

我们直接上源码,这个计算指标的函数名定义为returnAnalyze,可以首先来看这个计算函数的输入:

totalAssets 这个参数是策略开始运行时的初始资产总计。 profits 这个参数是一个比较重要的参数,因为一系列的绩效指标计算都是围绕这个原始数据来进行的。这个参数是一个二维数组,每个元素是按照时间戳排列的,包含时间戳和对应的profit,用来记录着各个时刻收益的时间顺序的数据结构。 ts 是回测的开始时间戳。 te 回测的结束时间戳。 period 毫秒级别的计算周期。 yearDays 是一年的交易日,通常情况下定为252天:

接下来我们来看下这些变量是怎样获取以及定义的:

在策略开始的时候,totalAssets,period和yearDays可以定义为常量;

ts可以通过在策略开始的时候定义为常量,而te作为实时更新的变量,可以随着策略的运行进行更新。

这里的重点是profits数组的计算,因为原始的GetAccount和GetPosition都不能直接获取策略在运行期间的总体收益,因此需要我们进行一些处理。

GetAccount的数据结构是这样的,但是这里的Balance需要在一笔交易完成之后,才能获得更新;因此直接使用Balance和起始金额的差值进行收益的计算是不准确的,不能统计在持仓期间内的浮动盈亏的变化;

javascript
{ Info : {...}, // 请求交易所接口后,交易所接口应答的原始数据,回测时无此属性 Balance : 1000, // 可用余额 FrozenBalance : 0, // 挂单冻结的余额 Stocks : 1, // 传统期货、股票证券此属性固定为0 FrozenStocks : 0 // 传统期货、股票证券此属性固定为0 }

而GetPosition统计了每笔交易的收益状况,作为一个数组,它可能包含很多笔交易,而每笔交易包含的收益和保证金是我们统计实时收益所需要的属性。

javascript
{ Info : {...}, // 请求交易所接口后,交易所接口应答的原始数据,回测时无此属性 MarginLevel : 10, // 杆杠大小,商品期货无法修改杠杆值 Amount : 100, // 持仓量 FrozenAmount : 0, // 挂单平仓时,仓位冻结量 Price : 10000, // 持仓均价 Profit : 0, // 盯市盈亏 Type : 0, ContractType : "rb2201", // 合约代码、股票代码 Margin : 1 // 仓位占用的保证金 }

而GetPosition统计了每笔交易的收益状况,作为一个数组,它可能包含很多笔交易,而每笔交易包含的收益和保证金是我们统计实时收益所需要的属性。

因此,总体收益可以通过可用余额加上仓位的浮盈,保证金也需要占据金额,所以一并加上,最后减去初始的金额,就是实时的收益。

Balance + pos_profit + pos_margin - totalAssets

对于多品种策略来说,position可能包含很多的仓位,我们定义了一个函数calculateTotalProfit,使用遍历,将所有的浮盈和保证金相加。

这里需要注意的是position可能包含0个元素,所以这时候不能获取属性,需要使用一个三元表达式进行判断,如果没有仓位,那么仓位的浮盈和保证金都是0;如果有仓位,调用calculateTotalProfit计算浮盈和保证金。

输入变量定义好以后,接下来我们回到指标计算的函数,来具体解释一下函数中各个指标的计算步骤。

累计收益率(totalReturns):

通过profits索引获取最后一个收益点的收益。
将最后一个收益除以总资产,得到累计收益率。

年化收益率(annualizedReturns):

计算年交易日数的毫秒数:yearDays * 86400000。
将累计收益率乘以年交易日数的毫秒数。
再除以投资时长(te - ts),得到按年化的预期收益率。

接下来我们来计算:

maxDrawdown是最大回撤。它表示从最高峰值到最低谷值之间的资产损失的最大百分比。

maxDrawdownTime表示达到最大回撤时的时间戳,资产达到最低谷的时刻。

maxAssetsTime表示达到最大净值时的时间戳,资产达到最高峰值的时刻。

maxDrawdownStartTime表示最大回撤开始的时间戳,从最高峰值开始计算最大回撤的起始时刻。

winningRate表示胜率,在所有收益记录点中盈利的比例。

这些指标通过遍历profits数组来计算和更新。

首先,使用一个循环遍历profits数组中的每个收益记录点。在循环开始时,初始化一些变量,包括最大回撤(maxDrawdown)为0,最大资产(maxAssets)为初始净值(totalAssets),以及一些相关的时间戳变量。

在循环中,首先检查当前收益记录点的索引。如果是第一个点(i == 0),则判断其收益是否大于0,表示盈利,如果是则将胜利次数(winningResult)加1。对于其他点,只需要判断当前点的收益是否大于前一个点的收益,如果是,则将胜利次数加1。

接下来,通过比较当前收益记录点与最大资产的和与之前记录过的最大资产的大小,来更新最大资产和对应的时间戳。如果当前的和大于最大资产,则更新最大资产和最大资产时间。

然后,在确保最大资产大于0的情况下,计算当前回撤率(drawDown),即(1 - 当前资产和最大资产的比值)。如果当前回撤率大于之前记录的最大回撤,则更新最大回撤、最大回撤时间和最大回撤开始时间。

最后,如果profits数组不为空,则计算胜率(winningRate),即胜利次数除以总的收益记录点的数量。

通过循环遍历profits数组,并根据每个收益记录点更新相关变量,最终得到了最大回撤、最大回撤时间、最大净值时间、最大回撤开始时间和胜率这些指标的值。

这里的难点可能是夏普比率(sharpeRatio)的计算:

首先,进行trim profits,目的是计算和整理利润数据。

代码初始化了一些变量,包括 i、datas、sum、preProfit、perRatio 和 rangeEnd。其中 i 用于迭代遍历 profits 数组,datas 用于存储每个时间段的收益率,sum 则是所有收益率之和。

然后,根据投资时长范围 ts 和 te 以及时间段长度 period,把rangeEnd处理为period的整倍数。

接下来,通过循环计算每个时间段的收益率。循环从 ts 开始,每次增加 period,直到达到 rangeEnd。在每个时间段内,通过遍历 profits 数组,找到符合条件的收益点,并将其收益加到 dayProfit 中。同时更新 preProfit 为当前收益点的收益值。然后,根据公式 计算该时间段的收益率,并将其累加到 sum 中,并添加到 datas 数组中。

接下来,代码计算夏普比率。在判断datas数组的长度是否大于0的情况下,进入计算夏普比率的逻辑。首先计算收益率的平均值 avg,然后计算标准差 std。接着,根据公式计算volatility波动率。最后,如果波动率不为0,则根据公式

sharpeRatio = (annualizedReturns - freeProfit) / volatility

计算夏普比率。

最终通过函数返回这样的输出结果:

javascript
totalAssets:初始净值 yearDays:交易天数 totalReturns:累计收益率 annualizedReturns:年华收益率 sharpeRatio:夏普比率 volatility:波动率 maxDrawdown:最大回撤 maxDrawdownTime:最大回撤时的时间戳 maxAssetsTime:最大净值时的时间戳 maxDrawdownStartTime:最大回撤开始时间 winningRate:胜率

我们来举一个例子实际示范下回测指标的计算。

javascript
function main(){ var mp = 0; // 设置持仓信息 // 设置returnAnalyze输入变量 var profits = []; // 后续随着交易过程添加 var totalAssets = 1000000; // 初始金额 var period = 86400000; // 交易周期毫秒数 var yearDays = 252; // 年度交易天数 const initialTs = new Date().getTime(); // 策略起始时间ts变量 while(true){ exchange.SetContractType('rb888') var r = exchange.GetRecords(); // 获取K线数组 if (r.length < 20){ // 需要超过K线的长度20 return;} var boll = TA.BOLL(r, 20, 2); //计算布林带指标 var upLine = boll[0]; // 获取上轨数组 var midLine = boll[1]; // 卖取中轨数组 var downLine = boll[2]; // 获取下轨数组 var upPrice = upLine[upLine.length - 3]; // 获取上上根K线上轨数值 var midPrice = midLine[midLine.length - 3]; // 获取上上根K线中轨数值 var downPrice = downLine[downLine.length - 3]; // 获取上上根K线下轨数值 recclose = r[r.length - 2].Close; // 获取上根K线收盘价 if(mp == 0 && recclose > upPrice){ // 如果无持仓,并且收盘价大于上轨,开多 // 设置下单方向 exchange.SetDirection("buy"); exchange.Buy(recclose,1) mp = 1;} if(mp == 0 && recclose < downPrice){ // 如果无持仓,并且收盘价小于下轨,开空 // 设置下单方向 exchange.SetDirection("sell"); exchange.Sell(recclose,1) mp = -1;} if(mp == 1 && (recclose < midPrice)){ // 如果持多,并且收盘价小于中轨,平多 // 设置下单方向 exchange.SetDirection("closebuy"); exchange.Sell(recclose-5,1); // 这里为了保证交易,所以设置限价单为最新的价格减5 mp = 0;} if(mp == -1 && (recclose > midPrice )){ // 如果持空,并且收盘价大于中轨,平空 // 设置下单方向 exchange.SetDirection("closesell"); exchange.Buy(recclose+5,1); // 同样的,为保证交易,这里设置限价单 mp = 0;} // profits数组的计算,每个元素包含时间和收益 // 重点是profits数组的计算,因为原始的GetAccount和GetPosition都不能直接获取策略在运行期间的总体收益 // 通过组合GetAccount和GetPosition的数据,我使用了Balance + pos_profit + pos_margin - totalAssets方法计算实时的收益 var account = exchange.GetAccount(); var position = exchange.GetPosition(); function calculateTotalProfit(data) { var totalProfit = 0; var totalMargin = 0; for (let i = 0; i < data.length; i++) { totalProfit += data[i].Profit; totalMargin += data[i].Margin; } return [totalProfit, totalMargin]; } var pos_profit = position.length == 0 ? 0 : calculateTotalProfit(position)[0] var pos_margin = position.length == 0 ? 0 : calculateTotalProfit(position)[1] profit = account.Balance + pos_profit + pos_margin - totalAssets time = new Date().getTime(); profits.push([time, profit]); // 定义ts和te时间戳 const ts = initialTs; te = time; // 输入变量获取完成,带入函数进行计算 ret = returnAnalyze(totalAssets, profits, ts, te, period, yearDays) // 格式化指标为保留三位小数的百分比形式 var totalReturnsPercent = (ret.totalReturns * 100).toFixed(3) + "%"; var annualizedReturnsPercent = (ret.annualizedReturns * 100).toFixed(3) + "%"; var sharpeRatio = (ret.sharpeRatio).toFixed(3); var volatilityPercent = (ret.volatility * 100).toFixed(3) + "%"; var maxDrawdownPercent = (ret.maxDrawdown * 100).toFixed(3) + "%"; var winningRatePercent = (ret.winningRate * 100).toFixed(3) + "%"; // 构建结果的表格数据 var table = { type: "table", title: "分析结果", cols: ["指标", "值"], rows: [ ["总资产", ret.totalAssets.toString()], ["年交易天数", ret.yearDays.toString()], ["总收益率", totalReturnsPercent], ["年化收益率", annualizedReturnsPercent], ["夏普比率", sharpeRatio], ["波动率", volatilityPercent], ["最大回撤", maxDrawdownPercent], ["最大回撤时间", _D(ret.maxDrawdownTime)], ["最大资产时间", _D(ret.maxAssetsTime)], ["最大回撤起始时间", _D(ret.maxDrawdownStartTime)], ["胜率", winningRatePercent] ] }; // 状态栏进行输出 LogStatus("`" + JavaScriptON.stringify(table) + "`"); // 注意:由于原始指标计算的精度是天,这个策略计算的精度更小一点,因此个别计算出来的指标和回测系统的指标不一致。 Sleep(1000) } }

首先定义好回测系统指标计算的函数,returnAnalyze。

接着在主函数中,定义我们的交易策略是我们以前讲过的布林带交易策略。注意,为了计算各个回测指标,需要设置一些returnAnalyze输入变量。

在交易逻辑定义完成以后,我们需要获取returnAnalyze需要实时更新的te时间变量和profits收益变量。

接着我们来定义这些指标的输出格式,为了呈现的更加清晰,我们不使用日志,而使用状态栏进行指标的输出。首先格式化指标为保留三位小数的百分比形式,然后构建结果的表格数据,最后使用LogStatus函数进行输出。

我们点击回测,对比一下我们的计算结果和YOUQUANT内置回测系统计算出来的结果。可以发现有些指标不太一致,因为回测指标计算的精度是天,减少了日内的波动,而我们策略计算的精度更小一点,方便实时的查看,因此个别计算出来的指标和回测系统的指标不一致。

以上就是回测评价系统中的指标计算,大家初看起来,可能比较复杂,不过大家细细剖析一下,可以发现条理是非常清晰的,并且可修改的空间也很大,比如这里的胜率计算,我们是按照本时刻的收益是否大于前一时刻的收益来计算的,而我们更习惯的是,计算逐笔的胜率,就是一笔交易从开仓到平仓是否盈利。因此代码可以修改一下,打造出符合我们交易逻辑的回测指标评价系统。

27:策略进度恢复的设计(一)

在先前的课程中,我们讲解的策略都是运行在模拟回测系统中,策略直接出来回测结果,不受外部物理条件的干扰,是一种比较理性化的策略运行状态。然而,在实际实盘运行中,当我们部署托管者在自己的电脑上时,不经意的停电或者断网会造成我们实盘策略的停止;而部署在云服务上的托管者,也有一定的可能性收到DDOS攻击造成断网,造成实盘的停止。因此,怎样处理这种突发的情况,是我们实践性课程需要重点讲解的部分。

量化交易中的策略进度恢复是指在遇到系统故障、网络中断或其他异常情况导致交易程序中断时,恢复并继续执行交易策略的过程。策略进度恢复是保证交易系统的稳定性和连续性的关键环节。

策略进度恢复可以分为以下几种类型:

冷启动恢复:当整个交易系统需要重启时,包括策略引擎、数据接口、交易接口等,需要重新加载和初始化所有的策略和相关数据。

数据恢复:在交易过程中,如果数据源出现问题或中断,需要通过补齐缺失的数据或重新获取数据来使策略的计算和决策不受影响。

策略状态恢复:当交易程序因为故障或异常情况中断时,需要将策略的状态保存下来,以便在恢复后继续执行。这包括已经计算的指标、持仓信息、订单状态等。

交易状态恢复:如果交易接口中断,导致未完成的订单无法提交或确认,需要在恢复后重新查询和处理这些未完成的订单,确保交易的完整性和一致性。

策略进度恢复的重要性不言而喻。在量化交易中,每一次交易决策都可能产生利润或者亏损,因此无论是策略还是交易状态的丢失都可能导致损失。策略进度恢复能够保证交易系统的连续性和稳定性,避免因系统故障而产生额外的风险和错误交易。同时,对于高频交易或需要实时响应市场变化的策略来说,策略进度恢复更加重要,它能够尽快将系统恢复到正常运行状态,减少交易信号的延迟和错过交易机会的风险。

优宽量化作为专业的量化交易平台,非常重视策略进度恢复的设计和实现。而作为专业进阶的量化交易人,这些问题也需要未雨绸缪。相当于Pine语言和麦语言,这种高度封装的交易语言,可以自动实现策略的进度恢复,而JavaScript语言和python语言作为从底层搭建的量化系统,需要我们手动实现策略的进度恢复。因此,本节课程我们将讲解JavaScript语言的策略进度恢复的设计。

_G(K, V)函数的使用

首先,来介绍我们的策略进度恢复的好帮手,_G函数。作为一个优宽量化平台的内置函数,它的数据结构为KV表,K必须为字符串,它不区分大小写,V可以为任何可以JavaScriptON序列化的内容。它可以永久保存在本地文件,所以该函数实现了一个可保存的全局字典功能。它在回测和实盘中都是支持的。在模拟回测系统中,回测结束后,_G函数保存的数据会被清除。而在实盘系统中,每个实盘单独一个数据库,重启或者托管者退出后,_G函数保存的数据是一直存在的。所以它的使用很灵活,可以放入任何我们想要储存的数据,状态变量和持仓状态等。

我们举例示范一下。这里由于不使用接口获取数据的测试,就不需要使用exchange.IO("status")函数判断连接状态,也不用设置合约代码,因为这里仅仅是测试_G函数。首先,在_G函数中定义键K为“num”的字符串,然后值V为数字1,使用_G("num")可以打印该字典键对应的值1;当然键对应的值也可以更改,这里我们重新定义"num"键对应的值是字符串"ok",然后我们重新调用键,会返回新的值"ok";如果需要删除这个值对应的值,填入null就可以;如果想删除所有全局变量,可以直接填入null;在实盘运行中当调用_G()函数并且不传任何参数时,_G()函数返回当前实盘的ID。

javascript
function main(){ // 设置一个全局变量num,值为1 _G("num", 1) Log(_G("num")) // 返回1 // 更改一个全局变量num,值为字符串ok _G("num", "ok") Log(_G("num")) // 返回"ok" // 删除全局变量num _G("num", null) // 返回null // 返回全局变量num的值 Log(_G("num")) // 返回null // 删除所有全局变量 _G(null) Log(_G()) // 返回实盘ID }

持仓状态的恢复

在了解完_G函数以后,我们来进入第一个场景,持仓状态的恢复。持仓状态恢复的重要性在于确保交易系统的连续性和准确性。当交易程序中断时,在程序化策略中持仓信息可能会丢失,造成持仓信息和实际的仓位不一致的状况,这可能导致交易系统的错误决策和风险暴露。

通过持仓状态恢复,我们可以将中断前的持仓信息重新加载到交易系统中,确保交易系统在恢复后能够基于正确的持仓信息进行进一步的决策和风险管理。

此外,持仓状态恢复还对于交易流程的完整性和可追溯性至关重要。通过持仓状态的恢复,我们可以准确地记录交易系统的行为和决策过程,使得后续的回测、风险控制和复盘等工作能够进行。

在以往编写的策略中,为了展示的方便,我们大多使用的是虚拟持仓变量,在策略中断的时候,这个虚拟持仓变量会丢失,这样可能会造成实际仓位和虚拟持仓的不符,造成策略的运行冲突,出现错误。

另外,还有一种情况,就是在策略运行期间,即使我们保存了持仓的信息,我们手动的在交易软件中开仓或者平仓,同样会造成记录持仓和实际持仓的冲突。因此,我们需要考虑这两方面的问题。

所以呢,我们针对上述的问题,可以提出解决方案,这个方案首先可以时刻保持策略的持仓状态,即使策略中断也可以;第二,在策略开始的时候,需要检查策略保存的持仓状态和实际的持仓状态,是否有冲突,在有冲突的计划下,针对于不同类型的策略,我们需要制定好备用方案,比如修改策略保存的持仓变量,然后继续运行策略;或者发送警告,立即停止策略等等。

下面我们针对第一种场景,针对持仓状态的恢复进行设计,我们举例示范一下,布林带策略的持仓状态恢复的策略,我们这里举例示范的交易策略都很简单,重点是让大家可以了解在实盘运作中出现问题,我们解决的思路是什么,相信各位聪明的小伙伴们也许还有更好的解决方法,如果有兴趣的话,大家可以分享到我们优宽量化平台的文库或者社区板块,供大家一起瞻仰。

首先固定程序,连接交易所,获取k线数据,计算布林带指标。然后根据布林带指标和持有的仓位进行开平仓的操作。这里碰到我们的第一个重点,怎么获取和保存持仓状态变量。

使用以前的方法,在循环体之前,设置虚拟持仓变量mp=0,然后伴随策略运行,mp变量不断更新,但是突然遇到实盘停止,虚拟持仓变量将会丢失,当我们重启实盘,mp将会重新设置为0;如果目前的实际仓位是不为空的时候,会造成仓位信息的不一致,从而进行错误的操作,导致不必要的损失。

因此,我们可以将持仓状态变量始终保存在这个实盘当中,每次交易信号出现,使用这个持仓状态变量进行相应的操作。来让我们设置一下:

在策略开始的时候,我们直接获取持仓状态变量getPos,这个时候,这个_G函数的键值对还没有定义,因此返回的值会是一个空值,所以我们使用一个三元表达式,当为空值的时候,getPos是0,而当有对应键值的时候,返回有效值,然后将它赋值为mp变量;接下来我们使用保存的持仓状态变量mp进行交易的操作,并在每次操作完毕后,重新定义mp变量;最后不要忘了使用_G函数保存新的持仓状态变量。

我们在实盘中测试下,可以看到策略开始的时候,getPos变量返回0,而伴随交易的操作,getPos变量不断更新;接下来我们手动停掉实盘看下,然后重新打开,可以看到getPos的变量没有重置,依旧是实盘停掉之前的状态。这样,我们的第一个问题就解决了。即使我们短暂的停掉实盘,修改一下参数,然后我们重新开启实盘,当前的持仓状态变量不变,策略可以继续运行。

javascript
// 策略持仓状态恢复 function main() { var p = $.NewPositionManager(); var symbol = 'FG401'; while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已经连接到CTP!"); exchange.SetContractType(symbol); var records = exchange.GetRecords(); if (!records || records.length < 20) { Sleep(1000); // 等待获取足够多的K线数据 continue; } // 以防实盘运行停止或者手动平仓,需要保存策略持仓状态变量和对比实际的仓位 getPos = _G('getPos'); getPos = getPos == null ? 0 : getPos; Log('持仓保存状态:', getPos); mp = getPos; // 赋值mp为getPos if (records.length < 20){ // 需要超过K线的长度20 return;} var boll = TA.BOLL(records, 20, 2); //计算布林带指标 var upLine = boll[0]; // 获取上轨数组 var midLine = boll[1]; // 卖取中轨数组 var downLine = boll[2]; // 获取下轨数组 var upPrice = upLine[upLine.length - 3]; // 获取上上根K线上轨数值 var midPrice = midLine[midLine.length - 3]; // 获取上上根K线中轨数值 var downPrice = downLine[downLine.length - 3]; // 获取上上根K线下轨数值 recclose = records[records.length - 2].Close; // 获取上根K线收盘价 if(mp == 0 && recclose > upPrice){ // 如果无持仓,并且收盘价大于上轨,开多 // 设置下单方向 p.OpenLong(symbol, 1); mp = 1; } if (mp == 1 && (recclose < midPrice)) { // 如果持多,并且收盘价小于中轨,平多 p.Cover(symbol); mp = 0; } if (mp == 0 && recclose < downPrice) { // 如果无持仓,并且收盘价小于下轨,开空 p.OpenShort(symbol, 1); mp = -1; } if (mp == -1 && (recclose > midPrice )) { // 如果持空,并且收盘价大于中轨,平空 p.Cover(symbol); mp = 0; } _G('getPos', mp); // 使用_G更新持仓状态 Log('更新持仓状态:',_G('getPos')); Sleep(5000); } else { LogStatus(_D(), "未连接CTP!"); Sleep(1000); } } }

Position中需要使用的属性:

javascript
{ Amount : number, // 持仓量 Type : 0, ContractType : symbol, // 合约代码 ... }

下面,我们来探讨下第二个问题,在策略运行期间,我们手动的造成了实际持有的仓位和保存的仓位不符合的情况,应该怎样做?其实这也是一个仓位检查的问题,在策略运行初始,我们需要对照下实际仓位和保存仓位的信息是否一致。我们可以添加下面的代码进行仓位的检查。

首先GetPosition函数获取实际的仓位信息,GetPosition函数会有一些特殊的情况,我们需要处理下:当为空仓的时候,GetPosition函数也会返回空值,因此我们定义真实持仓变量realPos是0,第二种情况,GetPosition函数会返回很多品种的仓位信息,每一个品种的仓位信息占据一个索引位置,所以在检查到posList不为0 的情况下,我们需要使用轮询找到我们策略的目标品种(symbol),然后我们打印对应的品种,方向(Type),这里需要注意到是,当Type为0或者2,代表多头方向;Type为1或者3,代表空头方向;然后是打印amount,这里需要注意的是,仓位信息返回的amount都是正数;最后我们进行真实仓位realpos的计算,因为posList返回的Amount持仓数量都是正数,因此我们需要使用Type属性进行判断,当Type属性是0或者2,可以将realPos定义正数1乘以Amount;否则当Type属性返回1或者3的时候,将realPos定义为负数1乘以Amount。

然后我们使用持仓状态变量和真实持仓变量进行对比,如果两者是一致的,mp就赋值为持仓状态变量getPos,然后使用绿色字体打印正常信息;如果两者状态不一致,就要制定我们的PlanB,这里我们B计划是使用红色打印警告信息,直接使用throw抛错误。温馨提示,我们也可以使用别的计划,不抛出错误,而是将'getPos'键重新赋值为真实持仓状态变量,然后继续运行策略。针对于不同类型的策略,我们可以根据自己的需求进行进一步的优化。

下面的逻辑就一致了,赋值mp为更新后的持仓状态变量,然后执行交易逻辑,最后保存新的持仓状态变量。

我们在实盘中测试下,首先为策略正常运行状态,可以看到持仓状态变量和真实持仓是一致的,然后,我们停掉我们的实盘。来到YOUQUANT的交易终端,这是YOUQUANT搭建的一个实时的交易平台,可以手动进行,期货的真实或者模拟的实盘交易。在仓位列表中,可以看到目前的仓位为空,然后点击开一手仓位;开仓成功后,回到我们的持仓状态恢复策略中,可以看到红色字体显示两个仓位不一致,然后立刻发出警告,停止了实盘。

这就是我们策略进度恢复中,持仓状态恢复的一个简单的设计。当然还有很多不完善的地方。作为一个专业的量化交易人,如果我们想打造更加完美的交易系统,在尝试探索的过程中,一定会遇到问题和阻碍,其实把问题的细节一一列举清楚,通过查找资料和学习,发挥自己的主观能动性,相信大部分问题都可以解决,大家一起加油!

28:策略进度恢复的设计(二)

经过上节课的学习,我们了解了优宽量化平台的_G函数,以及如何使用它进行持仓进度的恢复。本节课,我们继续讲解策略进度恢复的设计中的状态变量的恢复。

状态变量的恢复

在量化交易中,状态变量是指用于驱动和执行交易策略的各种数据和指标。这些状态变量可以包括已经计算的指标、和交易信号等。状态变量的恢复指的是在交易程序中断后,通过保存已经计算的指标值,以便在程序恢复后能够继续使用这些指标进行决策和交易。

状态变量的恢复在量化交易中非常重要,它具有以下含义和重要性:

保持策略的连续性:量化交易策略通常基于历史数据和指标进行计算和决策。如果在中断后丢失了这些已经计算的指标,就无法保持策略的连续性。通过恢复状态变量,可以确保策略在中断前后的计算基础保持一致,避免中断导致的断层和错误。

准确的交易决策:已经计算的指标在量化交易中对决策非常重要。这些指标可以用于识别市场趋势、波动性、买卖信号等。如果在中断后丢失了这些指标,就无法做出准确的交易决策。通过恢复状态变量,可以保证策略在中断前后继续使用这些指标进行决策,提高交易的准确性和效果。

避免重复计算:某些指标的计算可能比较耗时,例如复杂的统计指标或大量历史数据的计算。如果在中断后需要重新计算这些指标,将浪费时间和计算资源。通过恢复状态变量,可以避免重复计算已经计算过的指标,提高交易程序的效率和响应速度。

这里我们举例示范,一个需要状态变量恢复的策略。这一类的策略需要我们及时保存,策略运行过程中的状态变量,这些状态变量,会在策略运行过程中不断地更新,比如我们的老朋友,移动止盈止损类策略。这个策略需要及时的保存我们的买入价,盈利等级比例和止盈止损率,如果遇到实盘突然的停止,这三个变量也会丢失,因此对应的止盈止损的操作无法完成。

上节课我们作为策略状态恢复的演示,所以展示的策略比较简洁。本节课我们将细化我们的讲解细节,努力实现一个实盘级的应用策略。

相信在上一节课的基础上,我们对于这个的问题的解决思路,可以更加清晰。我们在先前的止盈止损策略中,设置了三个实时更新的变量,开仓价,盈利等级和止盈止损率,他们都会伴随策略的进度进行实时的更新。如果没有进行状态变量的保存的设计,实盘一旦停止,这样造成的后果就是无法在实盘停止后,恢复先前的进度和状态。由于之前没有进行状态变量的保存,当实盘停止后,这三个伴随策略进度更新的变量会重新初始化,而且无法记录之前已经进行到的步骤。这可能导致需要从头开始执行这个策略,丢失了之前的进展和计算结果。当然,持仓状态的恢复也是必须的,不然如果实时的仓位和保存的仓位不一致,可能会导致错误的操作或者损失。

首先我们设置我们的合约是最新的甲醛主力合约,然后是全局变量虚拟持仓mp,实时保存仓位getPos,和三个状态变量买入价buy_price,盈利等级率profitLevelRatio,和止盈止损比率stopLossRatio。

接下来我们就要CTP连接的检查。设置我们的合约。

前面的工作完成以后,我们来进入我们的第一个选择,是否要选择恢复策略进度。如果我们想重新初始化策略,可以选择不恢复策略进度,我们可以平掉我们的仓位,初始化状态变量,重启策略。因此我们设置了一个策略的参数,recovPro。

如果选择恢复策略进度,相对于上一节课,为了方便的展示,我们将持仓状态检查放在while循环中,这次我们将只在策略开头部分,分别进行持仓状态的检查和状态变量的获取。

首先是持仓状态的获取和与真实仓位的比较,首先我们调用_G('getPos')查询是否保存有持仓状态变量,如果有将它赋值为getPos变量。然后我们获取我们的真实仓位realPos,利用我们上节课阐述的思路进行仓位的检查;根据真实仓位和保存仓位的一致状况,我们进行选择,如果仓位一致,继续运行策略;如果两个仓位不一致,可以选择红色警告,然后停止实盘;最后将虚拟持仓变量mp赋值为getPos.

接下来我们进行状态变量的恢复,同样先检查状态变量是否存在,不存在的话我们可以使用我们的初始设置变量;当然如果存在,使用_G进行状态变量的读取,然后分别使用索引0,1,2,恢复至保存的三个状态变量buy_price,profitLevelRatio和stopLossRatio。

这样,我们就可以在策略开头的部分完成持仓状态和状态变量的获取。

当然,不要忘了我们的第二个选择,不进行策略进度的恢复,也就是策略参数recovPro选择false。我们首先打印“重启实盘,状态变量重置” ,接着使用_G(null)清空所有状态变量,我们的目标品种仓位也要选择平掉。这样就可以解决,在上一步,如果出现仓位检查不一致的状况下,重新开启我们的策略。

在策略进度恢复设计完成以后,我们就要进入我们的策略主题循环部分了;

首先判断在连接前置机的状态下,否则我们的策略是无法运行的;

移动止盈止损类策略我们的思路还是一致的,伴随策略的进度,三个状态变量(开仓价,盈利等级和止盈止损率)也在实时的更新,重要的是最后我们要使用_G函数实时快照我们的状态变量,这样就可以在实盘遇到突发情况时,恢复我们的策略进度。

为了实时的展示这三个状态变量,我们首先整理成为table格式,然后使用LogStatus函数进行,这三个状态变量的实时展示。

这就是我们以前的移动止盈止损策略的加强版,它具有更多的灵活性,不仅可以实现策略的进度恢复,还增强了更多的容错能力。即使在实盘遭到暂停或其他干扰时,策略可以选择重启,也可以选择继续交易进度,提高了策略的实用性和交易稳定性。

我们在实盘中看下,首先我们设置不恢复策略进度,然后我们开启实盘,可以看到‘重启实盘,状态变量重置’字样,然后自动为我们进行了策略目标品种的平仓,和重置了状态变量的信息,接下来这个策略就可以顺利的进行;

然后我们停止策略,重新选择“恢复策略进度”,勾选为true,开启实盘,可以看到“开始恢复策略进度”字样,然后获取和检查实时的仓位信息,获取状态变量信息,呈现在状态栏中。

其实除了移动止盈止损类策略外,及时保存状态变量在大量的策略运行中非常重要的一步。比如套利策略,它是基于市场的价格差异进行交易,需要保存当前的套利机会、相关资产的价格和交易量等状态变量。在中断后,可以利用保存的状态变量来重新计算套利机会并执行相应的交易策略。

以上呢,就是状态变量恢复的一个简单设计,针对于不同类型的策略,我们可以选择不同的处理方法。希望本节课能给大家启发,帮助大家构建符合自己交易理念的状态变量恢复的设计。

29:策略进度恢复的设计(三)

前面两节课,我们学习了策略进度恢复中的持仓状态恢复和状态变量恢复,本节课我们来学习策略进度恢复设计中,数据恢复的应用:K线的存储和拼接。

数据的恢复

在交易过程中,数据源问题或中断可能会影响不同类型的策略。这里提供一个实际例子,说明数据恢复对于基于历史数据的策略的重要性:

假设有一个量化策略使用技术指标进行交易决策,其中包括计算移动平均线(MA),作为信号指标。该策略会根据短期MA和长期MA的交叉来进行买卖决策。

然而,在交易过程中,如果数据源出现问题或中断,导致历史数据缺失或不完整,那么计算移动平均线所需的数据将无法得到。这可能导致以下几种影响:

无法计算指标:策略所依赖的移动平均线指标无法计算,导致无法进行准确的买卖信号生成。

错误的信号判断:由于缺失了部分历史数据,可能导致移动平均线的计算结果与实际情况不符,从而导致信号判断错误。例如,如果缺失了最新的几个交易日的数据,可能导致移动平均线的计算结果滞后或不准确。

无法及时应对市场变化:如果数据源中断较长时间或数据缺失较多,策略可能错过了一些重要的市场变化或信号,无法及时调整仓位或执行交易操作。

为了解决这些问题,可以通过数据储存和恢复,来补齐缺失的历史数据,继续策略的顺利进行。

在优宽量化平台,在策略运行开始时,系统会为我们,提供了500根k线历史数据,方便我们使用历史数据,进行策略的运行。

注意:
需要指定固定的k线周期,比如1分钟,5分钟等等,才能拿到平台返回的k线数。

如果我们需要使用的k线周期是2000根。在策略开始的时候,我们需要等待1500根k线,加上平台开始提供的500根,需要收集够2000根k线以后,策略开始运行。而如果实盘一旦停掉再开启,我们需要再次等待1500根k线,才可以再次运行我们的策略。

这样确实很麻烦,有没有好的解决办法?这里同样可以使用我们的好帮手_G函数,帮助我们保留K线数据,然后等到策略重新开启的时候,就可以使用我们保留的k线,和优宽量化提供的k线进行衔接匹配,如果间隔的时间不长,就可以拼接我们所需要的k线数量,然后继续实盘的运行。

我们首先来学习下_G()函数怎么保存k线这种数据结构的。

什么情况下会需要2000根k线呢?
这对于小周期大参数的策略非常重要,例如我们想使用日线均线的波动,但是日线的k线不够灵敏,我们就可以使用小的分钟周期,大的均线参数,模拟分钟级日线的波动,可以更加灵敏。

javascript
function main() { //设定合约; symbol = 'rb2310'; //rTbl数据结构对象 var rTbl = { type: "table", title: "数据", cols: ["strTime", "Time", "High", "Open", "Low", "Close", "Volume"], rows: [] }; //时间戳数组 var timezone = []; //连接CTP while(!exchange.IO("status")) { LogStatus(_D(), "未连接CTP!"); Sleep(1000); } //获取k线 exchange.SetContractType(symbol); var r = _C(exchange.GetRecords); //依次添加k线,保留时间戳 for (var i = 0; i < r.length; i++) { var bar = r[i]; rTbl.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]); timezone.push(bar.Time); } Log('返回k线储存长度', rTbl.rows.length); //实时k线的保存 while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已经连接到CTP!\n", 'k线更新中'); var r = _C(exchange.GetRecords); var bar = r[r.length - 2]; //添加倒数第二根,以防最新的k线没有走完 if (!timezone.includes(bar.Time)) { rTbl.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]); timezone.push(bar.Time); _G('rTbl', rTbl); //实时验证保存效果 rData = _G('rTbl'); Log('k线储存时间戳: ',_D(rData.rows[rData.rows.length - 1][1])); Log('k线储存长度: ', rData.rows.length); } } else { LogStatus(_D(), "未连接CTP!"); } Sleep(1000); } }

首先,定义了一个名为symbol的变量,其值为字符串'rb2310',表示期货合约名称。

然后,创建了一个名为rTbl的对象,它是一个表格的结构,用来保存k线结构的数据。rTbl对象有以下属性:

javascript
type:字符串,定义表格类型为"table"。 title:字符串,定义表格的标题为"数据"。 cols:数组,定义表格的列名为["strTime"具体的时间, "Time"时间戳, "High", "Open", "Low", "Close", "Volume"]。 rows:空数组,用于存储具体的k线数据。

接下来,定义了一个空数组timezone,用于存储已经出现过的时间戳。

因为在策略开始的时候,系统会一次性的返回500根的k线数据,我们可以进行添加。在连接CTP后,设置期货合约。然后获取k线数据,使用轮询依次进行数据的添加,和时间戳的保存。

在初始的500根历史k线保存完以后,我们使用while循环进行实时k线的添加。

首先检查CTP连接状态。

调用GetRecords()函数获取最新的K线数据。然后获取实时的k线bar,这里为了防止最新的k线没有走完,我们设置需要添加的k线bar是倒数第二根k线。

伴随k线的更新,使用if检查,当前时间戳bar.Time,是否在timezone数组中,如果不存在,表示是最新更新的k线,将k线数据,和最新的时间戳分别添加到rtbl和timezone当中。然后立即使用_G函数保存rTbl。

为了验证保存效果,这里我们使用_G函数进行保存k线的读取,打印最新保存的一根k线的时间戳,显示当前已保存的K线数量。

这个循环会不断从CTP获取最新的K线数据,并将新的K线数据添加到表格数据中。同时,它会记录已经出现过的时间戳,确保不会重复存储相同的K线数据。

请注意,这里只是实现了k线的一次性保存工作,并没有实现k线的永久保存,因为每次停止实盘,再开启,rTbl数组都会重新更新,它是一个空的数组,所以_G保存的始终是最新的k线数据,这里只是让大家了解下怎样使用_G保存K线结构的数据。

在实盘中可以看到,首先我们获取到了最新系统返回的500根k线数据,然后伴随while循环,最新的时间戳k线不断添加,这里可以看到,我们每次添加的都是倒数第二根k线数据。通过这样的方法,我们就实现了k线这种数据结构的实时保存。

下面我们来举一个完整的例子,来实现k线数据的保存和拼接的策略。

javascript
/*backtest start: 2022-07-27 09:00:00 end: 2022-07-27 09:05:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] */ function main() { var symbol = 'MA309'; var rTbl = { type: "table", title: "数据", cols: ["strTime", "Time", "High", "Open", "Low", "Close", "Volume"], rows: [] }; var timezone = []; //实时k线时间戳 var gtimezone = []; //保存k线时间戳 //连接CTP while(!exchange.IO("status")) { LogStatus(_D(), "未连接CTP!"); Sleep(1000); } exchange.SetContractType(symbol); var r = _C(exchange.GetRecords); //读取保存k线; var getRdata = _G('rTbl'); //保存k线为空; if (getRdata == null) { Log('无存储数据'); for (var i = 0; i < r.length; i++) { var bar = r[i]; rTbl.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]); timezone.push(bar.Time); } Log('rTbl.rows储存长度', rTbl.rows.length); _G('rTbl', rTbl); Log('已储存新的r数据到rTbl中'); } if (getRdata != null) { Log('已有存储数据'); for (var j = 0; j < getRdata.rows.length; j++) { var gBartime = getRdata.rows[j][1]; gtimezone.push(gBartime); } for (var i = 0; i < r.length; i++) { var bar = r[i]; timezone.push(bar.Time); } var gLastTime = gtimezone[gtimezone.length - 1]; var nLastTime = timezone[timezone.length - 1]; Log('gtimezone时间', gLastTime); Log('timezone时间', nLastTime); if (gLastTime != nLastTime) { Log('k线有空缺,开始进行k线拼接'); var overlapIndex = -1; //交叉索引flag; for (var k = timezone.length - 2; k >= 0; k--) { if (timezone[k] == gLastTime) { overlapIndex = k; Log('找到最后重叠时间节点:' + timezone[overlapIndex] + ',开始拼接'); break; } } Log('停止时间:', _D(timezone[overlapIndex])) Log('最新时间:', _D(timezone[timezone.length - 1])) Log('间隔时间:', (nLastTime - timezone[overlapIndex])/60000) if (overlapIndex === -1) { throw('实盘暂停时间过长,无法进行K线拼接'); } else { for (var m = overlapIndex + 1; m < timezone.length; m++) { var bar = r[m]; getRdata.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]); } _G('rTbl', getRdata); Log('拼接长度:', getRdata.rows.length); Log('k线拼接已完成'); } } else { Log('储存数据和k线数据已同步,不需要进行k线拼接'); } } while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已经连接到CTP!\n", '储存数据和k线数据同步工作已完成'); var getRdata = _G('rTbl'); var r = _C(exchange.GetRecords); var bar = r[r.length - 2]; //添加倒数第二根,以防最新的k线没有走完 if (!timezone.includes(bar.Time)) { getRdata.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]); timezone.push(bar.Time); _G('rTbl', getRdata); Log('rTbl.rows更新长度', getRdata.rows.length); } //策略逻辑 //... }else{ LogStatus(_D(), "未连接CTP!"); } Sleep(1000); } }

相对于固定的持仓状态变量和状态变量,k线数据作为时间流数据,它是实时更新的。因此当实盘停止以后,再次运行策略,当拿到储存的k线数据的时候,是不能直接使用的,因为储存的k线和最新的k线之间,具有k线的缺失,就是实盘停止运行那段时间的缺失,因此我们使用我们储存的k线和实时返回的k线,需要进行k线的拼接,。

下面我们来讲下K线拼接的算法。首先我们讲解下思路。比如我们储存k线的时间戳是1,2,3,4,5,这五个时刻的k线数据,然而由于突发原因,实盘停止了6,7两个时刻,再重新开始策略,系统自动为我们返回了3,4,5,6,7这五个时刻的k线,因此我们需要找到这两者之间最后一个重叠的时间戳,也就是实盘停止的时刻,5,然后将最新返回的6,7这两个时刻的数据,也就是实盘停止的时间段,添加到我们存储的k线中,实现k线的拼接:时刻1到时刻7,接下来伴随新的k线的更新,最新的k线也不断的被存储起来。

思路了解以后,我们来使用代码进行实现,作为一个完整的算法,这个策略同时实现了k线的存储和拼接。

在策略的开始,对比于存储k线的策略,我们这里另外设置一个时间戳数组,gtimezone,代表储存的时间戳,然后同样的程序,连接CTP,设置好合约,获取k线。

为了进行k线的拼接,首先我们使用_G('rTbl')获取保存的k线数据;

这时候,我们进入我们的第一个选择,查看保存的k线数据是否为空?

如果我们储存的k线数据getRdata是null,就要开始储存r的数据到空的数组tTbl中;和刚才保存k线的算法一样,使用for循环对每根k线进行添加,然后使用_G进行k线的保存。最后提示信息'已储存新的r数据到rTbl中'。

如果getrdata不是null,证明我们先前在策略中进行过k线的保存,可以展开下面的操作:

第一步,获取两个时间戳数组,分别是保存k线的时间戳gtimezone,和最新返回的k线的时间戳timezone;然后打印两个时间戳数组最新的时刻;我们的数据拼接工作,就是要根据这两个时间戳之间有没有差异进行更新;

第二步,这里使用if判断两个时间戳中最新的时刻,是否一致,如果一致,证明储存的数据和k线数据是同步的,我们不需要进行数据的拼接。
如果两个最新的时间戳不是一致的,证明我们保存的k线具有缺失的部分,这里我们需要进入一个新的选择,数据能否恢复呢?

第三步,这需要比较gtimezone和timezone有没有重叠的部分,如果没有就说明系统最新返回的数据和我们保存的数据的间隔时间过长,返回的500根k线没有完全涵盖全部的缺失时间,所以无法进行有效的拼接;
如果gtimezone和timezone有重叠的部分,证明最新返回的timezone涵盖了缺失的时间,我们就要进行拼接;

这里我们首先设置交叉索引flag,overlapIndex变量,初始值定义为-1,然后通过在timezone中逆向查询,如果在timezone中找到gtimezone最新的时刻gLastTime,证明找到最后重叠时间节点,可以进行拼接,这个时候将overlapIndex定义为这个元素的位置。

然后将timezone中重叠索引,也就是overlapIndex以后的k线数据,添加到getRdata中,这样就可以完成k线的拼接,不要忘了及时的保存,这个时候需要保存的是getRdata变量。

另外一种情况,在timezone中找不到和gtimezone重叠的元素,overlapIndex一直为-1,证明没有找到重叠的部分,无法进行拼接。

这里的的重点关键在于查找两个时间戳中的重叠部分和重叠的元素索引,具体的实现细节,大家可以在代码中找到解决的思路。

以上这些都是策略开始的时刻,需要进行的工作,当k线拼接工作完成以后,伴随策略进度的更新,在while循环中,首先读取保存的k线数据,然后通过检查时间戳进行最新k线的保存工作。这里需要保存的位置是getRdata。

然后我们可以开始我们策略的运行,这里需要一个小小的注意,我们策略需要使用的k线是getRdata,不是GetRecords返回的r数据。

我们在实盘中看下,可以看到在没有保存k线的情况下,首先开始保存系统返回的500根k线,然后伴随k线更新,新的k线不断的被保存;

然后我们进入第二种情况,停止实盘,造成保存的k线缺失;重启实盘,首先检查两个最新的时间戳是否一致,然后检查两个时间戳数组是否有重叠部分,如果有重叠部分,证明可以进行缺失k线的填充,提示信息:实盘停止时间,也就是保存的k线最后的时间,最新时间,也就是返回k线最新的时间,两者的时间间隔,就是我们要填充的k线数量,然后我们进行数据拼接的工作,完成后会显示拼接长度和“k线拼接已完成”;如果没有重叠部分,证明实时返回的k线和保存的k线中间间隔过大,我们就会throw错误,停掉实盘。

请注意,这个策略为了方便教学,因此个别地方写的比较繁琐,相信各位小伙伴们对这个策略一定有更好的优化方法,大家可以大胆的改动。

这样完成了使用_G函数实现了k线数组的保存和拼接工作。这样在实盘停止以后,我们可以连接先前保存的k线数据,和新的优宽量化平台提供的数据,确保策略的无缝衔接。

这种情况对于小周期大参数的策略非常有效,比如我们需要2000根k线,当我们收集够足够的k线以后,进行策略的运行;而实盘一旦停止,我们可以使用我们保存的k线和最新的返回k线进行时间的匹配,计算我们需要的信号,不需要额外的等待。再提醒一下哈,实盘的暂停的时间要把握好,不然超过平台返回的k线数量,是无法进行k线数据拼接的,因为必然会有一段k线的空缺。

其实数据种类有很多,不仅仅是K线,比如以前我们讲过的交易记录,也可以进行实时的保存和读取,我们可以自行探索一下。

好了,这就是我们策略进度恢复的设计的一些范例,希望大家了解其中的思路。在自己的实盘运行中,永远有B计划拯救自己的量化交易系统。

30:多品种合约回调策略设计:区别于轮询架构的事件驱动架构

多品种(多个期货合约)量化策略是指同时在多个不同的期货合约上应用量化交易策略。这种策略基于对多个期货品种的市场行情数据进行分析和建模,用来制定交易信号和执行交易。

多品种量化策略的优点如下:

  • 分散风险:通过在多个期货品种上分散投资,降低了单一品种风险对整体投资组合的影响。当一个品种的价格波动较大或遭受损失时,其他品种可能表现更好,从而减少整体风险。

  • 增加机会:不同的期货合约在市场中具有不同的特点和波动性。通过同时研究并交易多个期货合约,可以捕捉到更多的交易机会,并且在市场变动时能够快速做出反应。

  • 提高收益稳定性:由于不同期货品种之间的相关性通常较低,因此,在一个品种表现不佳的时候,其他品种可能仍然能够带来正向收益,从而提高整体收益的稳定性。

  • 顺势交易:多品种策略可以根据不同期货合约的趋势和走势进行交易。当一个品种的趋势明显时,可以选择在该品种上建立仓位,从而跟随市场的走势而获取收益。

  • 套利机会:多品种量化策略可以通过不同期货合约之间的套利机会来获得利润。例如,通过同时买入一个合约并卖出另一个合约,从价差中获取利润。

需要注意的是,多品种(多个期货合约)量化策略也面临一些挑战,如数据处理和模型构建复杂性增加、风险控制的难度提高等。因此,在设计和实施多品种量化策略时,需要充分考虑到市场特点、投资者风险承受能力和相关技术工具的支持。

多品种策略设计的优点在于使用方便,一个策略程序控制交易多个品种,可以统一信息状态显示。交易多个品种相对分散了风险,增加了交易机会。缺点在于设计比较复杂,各个品种之间不能相互影响,对程序执行效率要求比较高。所以设计难度远大于设计一个单品种策略。优宽量化交易平台上提供了大量策略范例,给我们提供了丰富的参考代码,设计思路。

相对于以往使用轮询的多品种策略设计,策略的整体框架是基于不断循环合约列表,然后在检查到该品种最新的走势满足交易信号的时候,进行相应的交易操作,这样虽然易于设计,但是这并不是一个真正的多品种事件驱动策略。因为这是一个串联的模式,一次只能一个合约的信号判断和交易操作;如果在处理a合约的时候,b合约的信号现实触发的话,程序是无法顾及到的;这样对于趋势的策略确实影响不大,但是对于高频的策略,如果错过相应的信号触发,就不满足策略设计的初衷了。

因此本节课,我们从策略设计层面入手,剖析一个多品种合约回调策略设计,学习一些策略架构设计的经验。对于一个多品种合约的回调策略设计,我们首先需要了解行情的推送模式。在优宽量化平台,对于行情模式,可以使用mode参数进行切换:

  • exchange.IO("mode", 0)
    立即返回模式,如果当前还没有接收到交易所最新的行情数据推送,就立即返回旧的行情数据,如果有新的数据就返回新的数据。

  • exchange.IO("mode", 1)
    缓存模式(默认模式),如果当前还没有收到交易所最新的行情数据(同上一次接口获取的数据比较),就等待接收然后再返回,如果调用该函数之前收到了最新的行情数据,就立即返回最新的数据。

  • exchange.IO("mode", 2)
    强制更新模式,进入等待一直到接收到交易所下一次的最新推送数据后返回。

在同时,可以使用wait参数,设置阻塞:

exchange.IO("wait", Timeout),通过这样的设置,在当前交易所有任何品种更新行情信息,或订单成交时才返回信息,可带第二个参数(代表毫秒数)指定超时,超时会返回空值,正常返回EventTick/OrderEvent结构。
通过结合exchange.IO("mode", 0)函数使用,这样配合使用就可以使程序在有最新行情时进行响应,执行程序逻辑,这样的目的,是为了在程序中使用exchange.GetTicker()等函数调用时不阻塞)。

如果Timeout参数设置为-1,该函数设置成为了立即返回模式,在没有新事件的时候,返回空值,
如果Timeout参数设置为0,代表阻塞等待最新事件。

需要注意的是在使用exchange.IO("wait")时,必须至少已经订阅了一个当前处于交易状态的合约。还有这个函数,只支持商品期货实盘。

javascript
EventTick:{Event:"tick", Index:交易所索引, Nano:事件纳秒级时间, Symbol:合约名称, Ticker:行情数据}。 OrderTick:{Event:"order", Index:交易所索引, Nano:事件纳秒级时间, Order:订单信息}。

我们举例一个多品种行情回调的例子示范下:

javascript
var rTblMA = { type: "table", title: "hc2309", cols: ["strTime", "Time", "High", "Low", "Sell", "Buy", "Last", "Volume","OpenInterest", 'Symbol'], rows: [] }; var rTblrb = { type: "table", title: "rb2309", cols: ["strTime", "Time", "High", "Low", "Sell", "Buy", "Last", "Volume","OpenInterest", 'Symbol'], rows: [] }; var rTbli = { type: "table", title: "i2309", cols: ["strTime", "Time", "High", "Low", "Sell", "Buy", "Last", "Volume","OpenInterest", 'Symbol'], rows: [] }; function on_tick(symbol, ticker) { switch (symbol) { case "hc2309": rTblMA.rows.push([_D(ticker.Time), ticker.Time, ticker.High, ticker.Low, ticker.Sell, ticker.Buy, ticker.Last, ticker.Volume, ticker.OpenInterest, symbol]); if (rTblMA.rows.length > 10) { rTblMA.rows.shift(); } break; case "rb2309": rTblrb.rows.push([_D(ticker.Time), ticker.Time, ticker.High, ticker.Low, ticker.Sell, ticker.Buy, ticker.Last, ticker.Volume, ticker.OpenInterest, symbol]); if (rTblrb.rows.length > 10) { rTblrb.rows.shift(); } break; case "i2309": rTbli.rows.push([_D(ticker.Time), ticker.Time, ticker.High, ticker.Low, ticker.Sell, ticker.Buy, ticker.Last, ticker.Volume, ticker.OpenInterest, symbol]); if (rTbli.rows.length > 10) { rTbli.rows.shift(); } break; } } function main() { while(!exchange.IO("status")) { Sleep(1000); } _C(exchange.SetContractType, "hc2309"); _C(exchange.SetContractType, "rb2309"); _C(exchange.SetContractType, "i2309"); while(true) { var e = exchange.IO("wait", -1) if(e) { if(e.Event == "tick") { on_tick(e.Symbol, e.Ticker); } } LogStatus('`' + JavaScriptON.stringify([rTblMA, rTblrb, rTbli]) + '`') } }

我们举例一个多品种行情回调的例子示范下:

这里我们设置的多品种合约是黑色系类,包括热卷,螺纹钢,和铁矿石,然后设置wait函数,参数填写为-1,代表立即返回,因此当检查到最新的tick信息的时候,就是e.Event == "tick",该函数会执行on_tick函数,这里我们设置的on_tick函数是在我们初始设置的空图表中,不断的填充对应品种的最新10条的tick数据,然后使用LogStatus进行展示。

我们在实盘中运行下,可以看到由于我们设置了 exchange.IO("wait", -1),是立即返回模式,如果有新的信息就立即更新,在没有新事件时返回空值。我们在实盘中可以看到,三个品种的信息不是伴随轮询,一条条逐渐更新的,而是实时更新的,实现了多品种行情的并联展示。这对于交易决策和监控多个品种的交易机会非常有益。

下面我们就来举一个实例示范下,多品种合约的回调策略设计。上面的例子我们只使用到了tick数据的事件驱动的更新,这可以作为交易信号的判断,而交易操作,还需要另外一种状态变量的辨别,就是持仓状态,实时的持仓状态的获取,我们可以通过OrderEvent数据结构。

javascript
/*backtest start: 2022-07-27 09:00:00 end: 2022-07-27 09:05:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] */ function main() { var symbol = 'MA309'; var rTbl = { type: "table", title: "数据", cols: ["strTime", "Time", "High", "Open", "Low", "Close", "Volume"], rows: [] }; var timezone = []; //实时k线时间戳 var gtimezone = []; //保存k线时间戳 //连接CTP while(!exchange.IO("status")) { LogStatus(_D(), "未连接CTP!"); Sleep(1000); } exchange.SetContractType(symbol); var r = _C(exchange.GetRecords); //读取保存k线; var getRdata = _G('rTbl'); //保存k线为空; if (getRdata == null) { Log('无存储数据'); for (var i = 0; i < r.length; i++) { var bar = r[i]; rTbl.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]); timezone.push(bar.Time); } Log('rTbl.rows储存长度', rTbl.rows.length); _G('rTbl', rTbl); Log('已储存新的r数据到rTbl中'); } if (getRdata != null) { Log('已有存储数据'); for (var j = 0; j < getRdata.rows.length; j++) { var gBartime = getRdata.rows[j][1]; gtimezone.push(gBartime); } for (var i = 0; i < r.length; i++) { var bar = r[i]; timezone.push(bar.Time); } var gLastTime = gtimezone[gtimezone.length - 1]; var nLastTime = timezone[timezone.length - 1]; Log('gtimezone时间', gLastTime); Log('timezone时间', nLastTime); if (gLastTime != nLastTime) { Log('k线有空缺,开始进行k线拼接'); var overlapIndex = -1; //交叉索引flag; for (var k = timezone.length - 2; k >= 0; k--) { if (timezone[k] == gLastTime) { overlapIndex = k; Log('找到最后重叠时间节点:' + timezone[overlapIndex] + ',开始拼接'); break; } } Log('停止时间:', _D(timezone[overlapIndex])) Log('最新时间:', _D(timezone[timezone.length - 1])) Log('间隔时间:', (nLastTime - timezone[overlapIndex])/60000) if (overlapIndex === -1) { throw('实盘暂停时间过长,无法进行K线拼接'); } else { for (var m = overlapIndex + 1; m < timezone.length; m++) { var bar = r[m]; getRdata.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]); } _G('rTbl', getRdata); Log('拼接长度:', getRdata.rows.length); Log('k线拼接已完成'); } } else { Log('储存数据和k线数据已同步,不需要进行k线拼接'); } } while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已经连接到CTP!\n", '储存数据和k线数据同步工作已完成'); var getRdata = _G('rTbl'); var r = _C(exchange.GetRecords); var bar = r[r.length - 2]; //添加倒数第二根,以防最新的k线没有走完 if (!timezone.includes(bar.Time)) { getRdata.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]); timezone.push(bar.Time); _G('rTbl', getRdata); Log('rTbl.rows更新长度', getRdata.rows.length); } //策略逻辑 //... }else{ LogStatus(_D(), "未连接CTP!"); } Sleep(1000); } }

在先前的课程中,我们在回测系统中,order结构返回的信息比较少,其实在实际的交易场景中,作为一个交易的完整操作,从交易信号辨别,决定下单,向交易所发送请求,到最后完成下单,orderevent会返回一系列的事件:

这就是一条完整的order返回数据,这里面有几个重要的属性可以帮助我们判断订单的方向和完成的阶段状态:

  1. 第一阶段,订单提交,"StatusMsg"返回报单已提交,这里的重要属性Status代表(订单状态),Type表示(订单买卖类型),Offset表示(期货开平仓方向),Type和Offset决定操作的方向,而返回Status判断交易是否完成,这个时候Status是0,表示未完成状态;
  2. 在报单提交和报单完成之间,其实还有一系列的状态,"StatusMsg"会返回"未成交",这个时候Status仍旧是0,表示未完成;
  3. 最后交易完成,Status回返回1,表示订单完成状态;

这里的order事件返回也是事件驱动返回的,所以我们可以使用order事件进行持仓状态变量的实时判断;

我们实现了信号判断和持仓状态判断的事件驱动,对于交易的操作,我们也可以实现非堵塞的模式,还记得前面我们讲过的交易类库中的pushtask函数吗?这里我们可以使用上。每当信号触发,使用pushtask接受交易任务,放进任务队列;然后继续进行其他品种的信号判断和交易任务的执行。通过这样的方式,我们就可以实现一个多品种合约的事件驱动架构的设计。

我们来看下这个策略的具体架构:

首先我们来讲下这个策略的具体交易逻辑:作为一个事件驱动的策略,这个策略使用了ticker双均线作为基准。当持仓量为空,最新的ticker价格超过ticker慢线的时候,我们进行开多仓,跌破ticker慢线的时候,我们开空仓;当具有多仓的时候,最新的价格小于ticker快线,我们进行平多仓;具有空仓,价格大于ticker快线,我们平掉空仓。策略的交易确实很简单,下面我们来看怎样实现。

这里我们设置的多品种合约依旧是热卷,螺纹钢,和铁矿石,首先设置两个字典对象tickerList和SymbolPos,用来保存这三个品种的ticker数据和持仓数据。这里的持仓数据,每个品种有三个索引,在下面的讲解中我会为大家进行介绍。接下来我们也要使用交易类库中的多任务对象NewTaskQueue。

我们的交易操作是根据tick信息和order信息实时事件驱动的。这里的exchange.IO("wait", -1)设计表示是实时返回的机制,有信息立马进行返回。
接下来,我们就要设置on_tick和on_order函数了,当交易所返回tick或者order信息的时候,我们需要怎样的操作;

第一个on_tick函数,当不同品种tick数据更新的时候,用来搜集相应的ticker数据,并计算均线,决定交易信号的触发。这里我们使用对应的合约品种的键向tickerList中添加最新的ticker数据,我们设置的快线和慢线周期分别为50和20,然后等收集够足够的数量,我们计算相应的ticker均值。然后比较最新的价格ticker.Last和均值的突破作为交易的信号。在交易信号判断完成以后,我们接下来获取持仓状态的数据。这里我们获取了一个finmp和lock,分别代表交易完成时候的仓位状态和交易锁。finmp变量的设置我们放在on_order函数中进行讲解。这里我们首先讲下交易锁的功能。

当交易信号触发,在向交易所发送请求,等待order信息返回的中间,如果最新的ticker信息返回,这个时候由于交易信号的再次触发,交易函数会进行二次下单,这个时候我们可以设置一个类似锁的功能,在对应交易信息触发以后,pushtask进行下单,然后就设置该信号对应的操作已经是完成状态,不需要再进行重复的下单。这里的开多,开空,平多和平空,我们都使用这样的交易锁的设置。一个锁既然会锁上,必然也是需要打开的。下面在on_order函数中,我们进行持仓状态的判断和交易锁的解锁功能。

我们前面讲过,一个交易操作的完成,会返回不同类型的操作信息,最开始是"报单已提交"状态,如果这个时候,当利用Type,Status和Offset判断是开多的时候,我们就可以将持仓状态的第一个索引,代表“申请订单”的状态定义为1,但是这个时候,并不是实际完成了一手开多,需要等到Status变为 1,证明我们完成了该笔订单,这个时候可以将持仓状态的第二个索引定义为1,也就是实际的持仓状态为1,代表完成了多单;当交易完成,这个时候交易锁可以进行解锁了,也就是第三个索引从1变为0。

下面开空,平多和平空对应的持仓状态和交易锁状态的设置,整体的思路也是一致的。这样就可以实现了基于order事件驱动的持仓状态的改变。

当设计完成on_tick和on_order函数,我们就可以带入我们的主循环,当事件更新时,驱动不同的函数进行运行,完成相应的交易操作;不要忘了这里的q.poll()函数,执行任务队列。这样就是一个区别于轮询架构的事件驱动架构的多品种合约的设计。

为了展示不同的状态,我们这里设置了不同合约的ticker表和order事件表,可以伴随策略的更新,观察相应的状态变量的变化。

我们在实盘中看下,可以看到伴随不同的tick或者order事件,不同合约的事件信息状态栏不断进行更新,我们可以具体了解到基于时间的驱动架构是怎样完成的。

本策略为了教学讲解,因此个别地方设计的比较冗余,大家可以根据自己的想法对这个策略进行更好的优化。作为一个区别于轮询架构的策略,该策略的细节确实比较多,我们在应用于这类策略的时候,可以在实盘中多次模拟检验,搭建自己的事件驱动架构的多品种策略。

31:半自动化策略设计:商品期货计划委托工具

在做商品期货交量化易的时候,并非所有的都是全自动的交易策略,还有很多半自动的程序化交易工具,代替人工盯盘。半自动化,商品期货计划委托工具,作为一种用于执行商品期货交易策略的工具,它结合了人工决策,和自动化执行的特点。这个工具可以通过事先设置的条件,来自动触发交易委托,并且提供一定程度的自定义和灵活性,使我们能够根据自己的需求,进行调整。这类工具虽然算不上完整的策略,但是也是基于使用者的交易意图,有条理的进行交易,可以算的上是一种半自动化的交易工具。

使用半自动化商品期货计划委托工具的好处如下:

  1. 提高交易效率
  2. 减少情绪干扰
  3. 实现精确控制
  4. 提供数据分析

提高交易效率:半自动化工具允许交易者事先设置交易策略和条件,当市场满足这些条件时,工具会自动触发委托,减少了人工干预和执行的时间,提高了交易的效率。

减少情绪干扰:交易过程中情绪对决策的影响是普遍存在的,而半自动化工具能够帮助交易者避免情绪干扰,按照设定的规则执行交易,从而减少了冲动和情绪驱使的交易行为。

实现精确控制:半自动化工具可以根据设定的交易策略和条件执行委托,能够更加精确地控制入场点、止损点和止盈点等,避免过度依赖人工判断,提高了交易的准确性和一致性。

提供交易数据分析:半自动化工具通常会记录和保存交易数据,可以对历史交易进行分析和回顾,帮助交易者评估交易策略的有效性,并作出相应的优化和调整。

总之,半自动化商品期货计划委托工具具有执行效率高、减少情绪干扰、精确控制交易和提供数据分析等优点,能够帮助交易者更好地执行交易策略,并提高交易的效果和效率。

下面我们就一起在优宽量化平台来实现一个这样的策略设计。

对于半自动的交易工具可能会有很多需求,我们简单整理一些需求实现出来,对于更加高级、复杂的需求可以后续优化升级。

商品期货计划委托止盈止损工具(教学版)

  • 计划委托:
    制定委托任务,由策略参数设置的价格线,下单手数,多空方向,触发方式,确定任务。

  • 止盈
    计划委托订单成交以后,根据设置的止盈价格,创建计划止盈任务。

  • 止损
    计划委托订单成交以后,根据设置的止损价格,创建计划止损任务。

  • 策略进度的保存和恢复

一个完整的策略在实盘中是可以循环使用的,在止盈止损任务触发结束本轮交易以后,需要清空任务进度,为准备下一次开仓做好条件;在同时,如果遇到突发状况,实盘停止,需要及时保存策略进度,在实盘再次开启以后,继续运行策略。

有了以上需求,我们就可以逐一把功能实现,首先分析一下,止盈、止损动作是建立在开始的计划委托订单成交,有持仓以后,再产生的动作,所以止盈、止损是基于,第一个计划委托订单成交以后再创建。止损反手同样也是基于止损完成以后再产生的动作。

所以这里遇到第一个问题,我们设计的时候,如何让一个任务完成以后,自动创建另一个后续任务呢?
这个问题解决很简单,YOUQUANT量化交易平台提供了强大的模板类库,用自带的商品期货交易类库就可以轻松解决。还记得我们前面讲过的多任务对象$.NewTaskQueue(),它可以创建交易队列,用来控制多个对象。

我们来复习下如何使用这个对象 q?在pushtask函数中,在确定好交易品种,方向和数量以后,这里的function就是解决我们问题的回调函数。它可以在当前交易任务完成后,触发执行这个回调函数,这样我们把后续任务的创建操作,就可以写在这个回调函数中。

javascript
q.pushTask(exchange, task.taskSymbol, task.taskDirection, task.taskAmount, function(tradeTask, ret) { Log(tradeTask.desc, ret, "XX委托完成") if (ret) { // 回调,创建后续任务 // ... // .. // . } })

本节课,我们就尝试将以前的移动止盈止损策略放进我们的计划委托工具中,通过这样的方式,我们可以使用人为判断作为入场点,然后使用移动止盈止损策略,在争取实现较高收益的同时,又避免因为情绪引起的扛单。下面我们讲解下半自动化的移动止盈止损策略的设计。

javascript
/*backtest start: 2023-08-11 09:00:00 end: 2023-08-11 15:00:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] args: [["_EntrustSymbol","rb2310"],["_EntrustPrice",3600],["_EntrustAmount",1],["_IsRecovery",false]] */ // 全局变量 var q = $.NewTaskQueue(); var proTbl = { type: "table", title: "止盈止损状态变量更新", cols: ["strTime", "品种", "开仓价格", "开仓方向", "盈利等级", "盈利等级比率", "止盈止损比率", "止盈止损价格"], rows: [] }; // 任务处理对象 function TaskQueueProcess () { // 获取行情 exchange.SetContractType(_EntrustSymbol) var ticker = _C(exchange.GetTicker) var task = _TaskQueue[0] if (task.taskFinished == false && ticker.Last == task.taskPrice) { q.pushTask(exchange, task.taskSymbol, task.taskDirection, task.taskAmount, function(tradeTask, ret) { Log(tradeTask.desc, ret, "开仓委托完成") task.taskFinished = true; if (ret) { task.taskBuyprice = ret.price; task.taskSign = task.taskDirection == "buy" ? 1 : -1; task.level = 0; task.takeProfitRatio = 0.01 * task.taskSign; task.stopLossRatio = 0.01 * task.taskSign; task.taskStopPrice = task.taskBuyprice * (1 + task.takeProfitRatio) * (1 - task.stopLossRatio); // 回调,创建后续任务 var newTask = { taskType: 'STOPLOSS', taskSymbol: task.taskSymbol, taskDirection: task.taskDirection == "buy" ? "closebuy" : "closesell", taskAmount: task.taskAmount, taskFinished: false } _TaskQueue.push(newTask) proTbl.rows.push([_D(), task.taskSymbol, task.taskBuyprice, task.taskDirection, task.level, task.takeProfitRatio, task.stopLossRatio, task.taskStopPrice]); _G("_TaskQueue", _TaskQueue) Log("创建止盈止损任务", newTask, "#FF0000") } }); } if(_TaskQueue.length == 2){ var newTask = _TaskQueue[1] if (newTask.taskFinished == false && (newTask.taskDirection == "closebuy" ? ticker.Last > task.taskStopPrice : ticker.Last < task.taskStopPrice)) { //止盈止损线更新 task.level += 1; task.takeProfitRatio = _N(0.01 * task.taskSign + 0.001 * task.taskSign * task.level, 5); task.stopLossRatio = _N(0.01 * task.taskSign - 0.001 * task.taskSign * task.level, 5); task.taskStopPrice = task.taskBuyprice * (1 + task.takeProfitRatio) * (1 - task.stopLossRatio); Log("最新点位", ticker.Last) Log("止盈止损点更新", task, "#FF0000") proTbl.rows.push([_D(), task.taskSymbol, task.taskBuyprice, task.taskDirection, task.level, task.takeProfitRatio, task.stopLossRatio, task.taskStopPrice]) _G("_TaskQueue", _TaskQueue) } else if (newTask.taskFinished == false && (newTask.taskDirection == "closebuy" ? ticker.Last < task.taskStopPrice : ticker.Last > task.taskStopPrice)) { Log("到达止盈止损点位") q.pushTask(exchange, newTask.taskSymbol, newTask.taskDirection, newTask.taskAmount, function(tradeTask, ret) { newTask.taskFinished = true Log(tradeTask.desc, ret, "到达止盈止损点位") // 关闭止盈止损任务,清空状态变量 if(ret){ _G(null) throw('止盈止损任务已完成,请重新设置入场价格和方向#00FF00') } }) } } q.poll() } var _TaskQueue = []; function main() { if (_IsRecovery) { recoveryData = _G("_TaskQueue") if (recoveryData) { Log("恢复数据") _TaskQueue = recoveryData } else { _TaskQueue = [] Log("没有可用于恢复的数据") } }else{ _TaskQueue = [] Log("不使用状态变量恢复") } // 根据参数生成任务 if(_TaskQueue = []){ if (_EntrustSymbol == "null" || _EntrustPrice <= 0 || _EntrustAmount <= 0) { throw("没有设置委托合约或者委托价格无效或者委托数量无效") } else { var task = { taskType : 'ENTRUST', taskSymbol : _EntrustSymbol, taskPrice : _EntrustPrice, taskAmount : _EntrustAmount, taskDirection : _EntrustDirection == 0 ? "buy" : "sell", taskFinished : false } Log("请注意,创建委托任务", task, "#FF0000") _TaskQueue.push(task) _G("_TaskQueue", _TaskQueue) } } while (true) { if (exchange.IO("status")) { TaskQueueProcess() // 状态栏显示 LogStatus('`' + JavaScriptON.stringify(proTbl) + '`') } else { LogStatus(_D(), "未连接") } Sleep(1000*30) } }

相对于以往使用均线突破作为入场点,在半自动策略中,我们使用人为设置的入场点位,这就需要我们进行参数的设置;

一个入场点的判断,需要品种,价格,交易方向(开多还是开空),和数量的要求。所以我们在参数界面设置_EntrustSymbol代表合约,_EntrustPrice代表价格,_EntrustDirection代表方向,_EntrustAmount代表数量,在同时,以防实盘停止,丢失我们的状态变量,我们可以设置是否需要恢复策略进度,设置_IsRecovery变量。

接着我们来设置我们的main函数,在开头,我们设置策略进度恢复内容,如果设置_IsRecovery为true,证明需要恢复策略进度,我们使用_G进行任务队列的读取,_TaskQueue包含了移动止盈止损策略一系列的状态变量和交易任务。如果读取的状态变量recoveryData不为空,那么_TaskQueue使用我们保存的状态变量;如果保存的状态变量为空,或者选择不使用策略进度恢复,那么将_TaskQueue设置为空的子集,重启策略的运行。

在读取到_TaskQueue为空,我们需要根据委托的任务进行入场开仓的操作。这个时候需要做一个判断,如果初始参数设置的有误,包括开仓的品种,方向和数量,就要抛出错误;如果填写的参数没有错误的话,创建task任务对象,里面包括任务的类型,开仓的品种,价格,方向,数量,这些都是委托任务的变量,最后一个taskFinished,判断任务是否完成,这一个变量的设置很重要,我们将在策略主体循环中进行讲解。接着,我们需要向任务队列_TaskQueue中push这个task对象,然后进行保存。这样就完成了我们的开仓委托任务。

当然如果_TaskQueue不为空,就可以跳过这一步,直接读取我们先前的任务进度,直接进入我们的止盈止损的策略主题循环部分。

在设置好开仓委托任务以后,接下来,我们进入我们的主循环,在判断连接交易所的状态下,运行我们的交易逻辑函数TaskQueueProcess()。

在TaskQueueProcess()函数中,按照固定程序,设置好合约,因为我们想交易设置的更加灵敏,所以这里获取的是ticker数据。然后进入我们开仓交易的操作,获取_TaskQueue第一个元素task。在判断该开仓动作没有完成的情况下,当最新的ticker数据等于我们设置的买入价的时候,我们进行开仓;使用pushtask函数,根据设置好的品种taskSymbol,方向taskDirection,和数量taskAmount进行开仓的动作。开仓动作完成以后,就要立即设置我们的止盈止损的操作,所以设置我们回调函数。

这里为避免重复的开仓,需要设置taskFinished为true,然后在获取到回调结果以后,就是if(ret)为真,设置我们的买入价,就是ret.price,开仓方向有两个,因此对应的盈利等级线和止盈止损线的设置也是不同的,在多仓条件下,盈利等级线大于止盈止损线;而在空仓环境下,盈利等级线小于止盈止损线。所以我们设置了一个变量,taskSign,用来处理多仓和空仓下止盈止损线的计算;当开仓方向为buy,设置为正1,开仓方向为sell,设置为负1;然后设置一个等级,level,表明移动止盈止损的等级;

对于多仓和空仓不同方向的盈利等级比率takeProfitRatio,初始等级为0.01* task.taskSign。

对于止盈止损比率stopLossRatio,同样的思路,初级等级为0.01* task.taskSign。

最后来计算我们的止盈止损线了,使用开仓价乘以盈利等级率(1 + task.takeProfitRatio),再乘以止盈止损率(1 - task.stopLossRatio),就可以获得我们的初始止盈止损线了。

状态变量设置完成以后,接着创建我们的止盈止损平仓任务newTask,设置type为stoploss,设置好合约,数量,方向(根据开仓方向进行判断,如果开仓为buy开多,设置为closebuy,否则为平空closesell),最重要的设置taskFinished为false。然后在_TaskQueue中push这个newTask。这样就完成了后续任务的创建。记得使用_G保存更新的TaskQueue。

接下来就要进入我们的止盈止损线的更新,和到达止盈止损线后的平仓工作了。在判断后续任务添加完成以后,_TaskQueue.length是2。使用索引_TaskQueue[1]获取后续任务对象newTask。在判断newTask没有完成的情况下,taskFinished为false,根据开仓价和最新价格的走势,判断进行更新或者平仓的工作。

下面我们来看下止盈止损线更新的逻辑:在多仓环境下,taskDirection == "closebuy",如果最新的价格大于我们先前设置的止盈止损线,说明新的盈利等级已到达,我们需要更新止盈止损线;而对于空仓环境,如果最新的价格小于我们先前设置的止盈止损线,我们进行更新。

第一步更新等级变量level,每次递增1,然后随之更新盈利等级率,止盈止损率和止盈止损线。

对于多仓的盈利等级率,我们设置的taskSign都是正数,初始等级为0.01,然后随着最新价格的提升,加上0.001 * task.taskSign * task.level。

对于止盈止损比率,同样的思路,初级等级为0.01,然后随着盈利等级线的提升,这个时候需要减去0.001 * task.taskSign * task.level。

止盈止损线同样进行更新,按照开仓价乘以更新后的的盈利等级率,和止盈止损比率。

下面我们来看止盈止损线到达后的平仓操作:在多仓的环境下,如果最新的价格小于我们先前设置的止盈止损线,或者空仓环境,大于止盈止损线,我们就要进行平仓。使用pushtask,按照newtask对象中的种类,方向和数量进行平仓,然后设置newTask.taskFinished为真,关闭止盈止损任务,清空状态变量。因为该笔交易已经完成,这个时候我们可以选择使用throw停止实盘,防止继续计费。

因为我们使用的是pushtask交易函数,不要忘了设置q.poll()。

这里为了展示移动止盈止损不同等级和止盈止损线的更新,在每一步,TaskQueue更新的时候,包括后续任务创建和状态变量更新,我们记录下来,然后使用状态栏进行实时的动态展示。

我们回测测试下,首先根据自身对于行情的判断,人为设置我们理想的买入价格,开仓方向和数量,就可以等待到达点位后,进行止盈止损的操作。

在日志信息里可以看到,首先我们设置委托任务,所以当ticker价格达到这个点位,我们进行开仓,然后系统进行开仓和后续的止盈止损操作,伴随后续价格的走势,止盈止损点位不断更新。当到达止盈止损平仓条件以后,系统自动选择平仓,停止实盘。这样呢,就可以免去我们人工盯盘的烦恼,使用半自动化的方式进行我们的交易决策。这里我们设置的止盈止损线和买入价很接近,这对于谨慎型的交易者非常有利;如果我们是一个冒险型的交易者,对自己的点位判断比较自信,可以将止盈止损线的振幅更宽一点,在承受较大风险的同时,也可能获取更高的收益。

在先前的课程中,为了教学的方便,我们只使用了多仓的止盈止损的操作,在本节课程,我们讲解了完整的多仓和空仓的止盈止损操作。但是,策略仍然具有改进的地方,第一,初始的盈利等级比率和更新的盈利等级率的步长的设置,可以根据不同的品种和交易理解进行改进,第二,我们这里开仓的操作是使用参数完成的,其实我们可以使用交互控件,在不停止实盘的情况下,定义开仓的具体参数。这些问题都可以根据我们对于市场的理解,和前面所学过的知识,对这个系统进行更好的设置和完善。

本节课,我们介绍的是一个半自动化策略设计的框架,其实除去移动止盈止损策略以外,固定比例的止盈和止损操作通过回调函数也可以实现,具有交互功能的跨期套利也可以实现,在我们的文库中,都有现成的代码,大家可以尝试下。

32:关于商品期货套利的策略设计

商品期货的套利模型大家一定都有所耳闻,其实“套利”在现实生活中很常见。比如:便利店老板从批发市场以 0.5 元买入一瓶矿泉水,然后在店里以 1 元的价格出售,最后赚取 0.5 元的差价。这个过程其实就类似套利。商品期货的套利策略是指通过同时买入或卖出两个相关商品期货合约,来实现利差差价的交易策略。套利策略旨在利用不同市场之间,或者同一市场上不同合约之间的,价格差异,从中获取稳定的利润。

根据套利策略的具体方式和实施方式,商品期货的套利策略可以分为多种类型。以下是几种常见的商品期货套利策略:

  • 跨品种套利:基于不同但相关的商品期货合约之间的价格差异进行套利。例如,通过同时买入大豆期货合约和豆油期货合约,利用它们之间的相关性来获得利润。

  • 跨期套利:基于同一商品不同到期月份的,期货合约之间价格差异,进行套利。例如,买入近期到期的合约,同时卖出远期到期的合约,利用时间价值的变化来获取利润。

  • 跨市场套利:基于不同地理位置,或不同交易所的,同一商品期货合约之间的价格差异,进行套利。例如,通过在两个不同的市场上,同时进行买卖来获得利润。

  • 期现套利:基于同一商品现货市场与期货市场之间的价格差异,进行套利。例如,通过买入现货,并卖出期货合约,或买入期货合约,并同时卖出现货来获得利润。

套利策略通常是市场中的低风险交易,但是套利机会在市场中往往存在较短的时间,并且价格差异,通常会很快被市场参与者纠正。所以可以通过量化交易这种自动化和编程实施的方式,在减少人为错误和情绪影响的干扰下,捕捉到短暂的交易机会,从而提供稳定的回报。

在实施套利策略时,以下几点值得注意:

  • 选择适合自己的市场和品种:根据自身知识、经验以及对各个市场和品种的了解,选择适合自己的套利市场和品种。
    控制风险:无论采用何种套利模型,都需要进行有效的风险管理。设置适当的止损点和盈利目标,合理分配资金,控制仓位,避免过度杠杆操作。
  • 深入研究和分析:了解商品市场的基本面和技术面,掌握相关指标和数据,进行详尽的研究和分析。同时,关注市场消息和事件,灵活调整套利策略。
  • 效率和执行力:在套利交易中,时间和速度至关重要。快速反应市场变化,确保交易的执行效率和准确性。
  • 持续学习和改进:套利策略是一个不断学习和改进的过程。及时总结经验教训,保持学习的状态,根据市场情况进行调整和优化策略。

在上面讲到的四种套利模型中,并不是所有的套利策略都适用于散户投资者,相对于机构投资者,散户在进行跨市场套利和期现套利方面面临一些限制和困难。

首先,散户可能面临资金规模的限制。跨市场套利和期现套利通常需要较大的投资额来达到可观的利润,而散户的资金规模通常较小,难以承担这样的交易规模。此外,散户还需要面对市场准入的限制。某些市场或交易所可能对散户的准入设置了门槛,例如最低资金要求、特定的投资经验或专业资格认证等。这也增加了散户参与跨市场套利和期现套利的难度。

本节课我们将介绍两种常见的策略设计:跨品种套利和跨期套利。

首先我们需要来理解下这两种套利策略的合理性。套利策略通常蕴含着均值回归的思想。均值回归是一种常见的市场现象,指的是价格或者其他指标在一段时间内呈现出波动和偏离均值的趋势,但最终会回归到其长期平均水平。

在套利策略中,跨品种套利和跨期套利都利用了价格差异,存在的时机进行交易。一定时间内,价差是相关的跨期和跨品种之间是稳定的。在当价格差异扩大超过它的正常范围的时候,套利者会采取相应的行动,预测价格将回归到平均水平,从而获取利润。

因此首先我们可以使用可视化展示来验证均值回归在不同品种、不同期限合约的普遍存在性。

对于跨品种套利来说螺纹钢和热卷是最好的两个合约品种了。由于rb(螺纹钢)和hc(热卷)每张合约都是代表10吨货物,并且生产成本、原料等因素导致这两个品种价格相关性是很强的。当价格差出现异常时,是可以进行对冲套利的。
我们来看螺纹钢和热卷的主力价格差值图像。差值主要分布在85到110之间,并且以95为水平均线进行上下波动。

image

关于跨期套利,我们选择的是螺纹钢的两个临近的主力合约rb2310和rb2401,可以看到这两张合约的差价图在-30和-10区间波动,平均价差为-20左右。

image

需要注意的是,跨期和跨品种的差值并不是持续固定的,可能由于不同的市场变化,差值均线会发生变动。因此,当使用差值均值作为策略参数的时候,需要按照固定的时间间隔进行调参。

套利策略的逻辑

下面,我们来看下套利策略具体的实现逻辑。

javascript
套利品种:symbolA , symbolB 价差定义:Price(symbolA) - Price(symbolB) 开仓: 正套:价差大于阈值 A 时 , 做空symbolA,做多symbolB 反套:价差小于阈值 B 时 , 做多symbolA,做空symbolB 止盈: 正套;价差小于阈值 C 时,止盈。 反套;价差大于阈值 D 时,止盈。 止损: 正套;价差大于阈值 E时,止损。 反套;价差小于阈值 F时,止损。

这里我们挑选的套利品种为symbolA和symbolB。该套利原理是通过观察symbolA和symbolB的价格差异,进行正套或反套交易。

首先,定义价差为symbolA价格减去symbolB价格(Price - Price)。价差一般是分布在一个正常的稳定范围内的,所以可以设置参数:

javascript
A: 正套阈值 B: 反套阈值 C: 正套止盈阈值 D: 反套止盈阈值 E: 正套止损阈值 F: 反套止损阈值

根据这个价差与各个阈值参数的关系,可以采取以下操作:

正套交易:当价差大于阈值 A时,意味着symbolA价格相对于symbolB价格偏高,此时可以做空symbolA(卖出symbolA),同时做多symbolB(买入symbolB)。

反套交易:当价差小于阈值 B时,意味着symbolA价格相对于symbolB价格偏低,此时可以做多symbolA(买入symbolA),同时做空symbolB(卖出symbolB)。

止盈操作:对于正套交易,当价差小于阈值 C时,表示价差由较大回归到了正常水平,可以考虑止盈(平仓)。对于反套交易,当价差大于阈值 D时,表示价差由较小回归到了正常水平,可以考虑止盈(平仓)。

止损操作:对于正套交易,当价差大于阈值 E时,表示价差是在继续扩大的,可能市场发生了显著的变化,存在一定的风险,可以考虑止损(平仓)。对于反套交易,当价差小于阈值F时,表示价差一直在持续缩小,这也是风险的标志,可以考虑止损(平仓)。

这些操作和阈值的设定可以帮助我们在价差波动时识别套利机会,并决定何时进场、何时离场以获得利润或控制风险。请注意,具体的阈值参数需要根据实际市场情况和回测结果进行调整,以获得更好的交易效果。

了解完策略的交易思路以后,这里有一个关键问题,模型的参数应该怎样设置?在跨品种套利和跨期套利策略中,阈值参数的确定方法与一般套利策略有所不同。以下是一些常见的确定阈值参数的方法:

  • 相关性分析:对于跨品种套利策略,可以使用相关性分析来确定阈值参数。通过计算不同品种之间的价格相关性,可以找到相关性较高的品种组合。然后,根据历史数据分析,确定合适的价差或相关性阈值。

  • 协整检验:对于跨期套利策略,可以使用协整检验来确定阈值参数。协整关系指的是一组时间序列之间存在长期稳定的线性关系。通过对不同到期日的期货合约进行协整检验,可以确定合适的价差阈值。

  • 理论计算:对于跨期套利,可以使用利用国债利率可以帮助确定具体的价差阈值或套利条件。将套利交易的价差与国债利率之间的差异进行比较,当价差超过一定阈值或超过国债利率时,可以认为存在套利机会,请注意:这只限于正向市场,就是远期合约的价格高于近期合约。

  • 统计分析:对于跨品种套利和跨期套利策略,也可以使用统计分析方法来确定阈值参数。根据历史数据,计算价格差或价差比率的均值、标准差等统计指标,并根据统计学方法确定合理的阈值范围。

与一般套利策略类似,跨品种套利和跨期套利策略也需要进行实时监控和调整阈值参数。根据市场的变化和实际交易情况,可以对阈值参数进行动态调整,以适应不同的市场环境。

需要注意的是,阈值参数的确定过程可能需要经过多次尝试和优化。在实际应用中,建议使用回测和模拟交易等方法来评估不同阈值参数设置下的策略表现,并选择表现较好的参数组合。同时,还需要谨慎考虑交易成本、市场流动性和风险管理等因素,以确保策略的可行性和盈利能力。

跨品种套利模型

首先我们来看跨品种套利模型。这里我们挑选的套利品种为热卷和螺纹。价差的定义为热卷的价格减去螺纹钢的价格。

javascript
/*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}] */ var p = $.NewPositionManager(); var symbolA = 'hc888'; var symbolB = 'rb888'; function onTick() { // 获取合约A行情数据 exchange.SetContractType(symbolA); var tickA = exchange.GetTicker(); // 获取合约B行情数据 exchange.SetContractType(symbolB); var tickB = exchange.GetTicker(); if (!tickA || !tickB) { return; } // 分析持仓 var pos = exchange.GetPosition(); if (!pos) { return; } var longPosOfSymbolA = p.GetPosition(symbolA, PD_LONG); var shortPosOfSymbolA = p.GetPosition(symbolA, PD_SHORT); var longPosOfSymbolB = p.GetPosition(symbolB, PD_LONG); var shortPosOfSymbolB = p.GetPosition(symbolB, PD_SHORT); // 计算价差 var diff = tickA['Last'] - tickB['Last']; // 开仓 if (!longPosOfSymbolA && !shortPosOfSymbolA && !longPosOfSymbolB && !shortPosOfSymbolB) { if (diff > maxDiff) { // 空A合约,多B合约 Log("空A合约:", symbolA, ",多B合约:", symbolB, ", diff:", diff, ", maxDiff:", maxDiff, "#FF0000"); p.OpenShort(symbolA, 1); p.OpenLong(symbolB, 1); } else if (diff < minDiff) { // 多A合约,空B合约 Log("多A合约:", symbolA, ",空B合约:", symbolB, ", diff:", diff, ", minDiff:", minDiff, "#FF0000"); p.OpenLong(symbolA, 1); p.OpenShort(symbolB, 1); } } // 平仓 if (shortPosOfSymbolA && longPosOfSymbolB && !longPosOfSymbolA && !shortPosOfSymbolB) { // 持有A空头、B多头 if (diff < shortPosOfSymbolA.Price - longPosOfSymbolB.Price - stopProfit) { // 止盈 Log("持有A空头、B多头,止盈。", "diff:", diff, "持有差价:", shortPosOfSymbolA.Price - longPosOfSymbolB.Price); p.Cover(symbolA); p.Cover(symbolB); } else if (diff > shortPosOfSymbolA.Price - longPosOfSymbolB.Price + stopLoss) { // 止损 Log("持有A空头、B多头,止损。", "diff:", diff, "持有差价:", shortPosOfSymbolA.Price - longPosOfSymbolB.Price); p.Cover(symbolA); p.Cover(symbolB); } }else if(longPosOfSymbolA && shortPosOfSymbolB && !shortPosOfSymbolA && !longPosOfSymbolB) { // 持有A多头、B空头 if (diff > longPosOfSymbolA.Price - shortPosOfSymbolB.Price + stopProfit) { // 止盈 Log("持有A多头、B空头,止盈。", "diff:", diff, "持有差价:", longPosOfSymbolA.Price - shortPosOfSymbolB.Price); p.Cover(symbolA); p.Cover(symbolB); } else if (diff < longPosOfSymbolA.Price - shortPosOfSymbolB.Price - stopLoss) { // 止损 Log("持有A多头、B空头,止损。", "diff:", diff, "持有差价:", longPosOfSymbolA.Price - shortPosOfSymbolB.Price); p.Cover(symbolA); p.Cover(symbolB); } } // 画图 $.PlotLine("差价", diff); } function main() { while (true) { if (exchange.IO("status")) { onTick(); LogStatus(_D(), "已连接"); } else { LogStatus(_D(), "未连接"); } Sleep(500); } }

第一步,我们需要设置一些外部的参数,第一个,价差波动的上限,当价差大于这个值的时候,我们进行正套的交易,根据上一节课图像的显示,我们定义为110;第二个,价差波动的下限,当价差小于这个值的时候,我们进行反套的交易,我们定义为85;第三个,止盈点数,对于正套和反套,当价差回归于正常水平的时候,可以进行止盈平仓;最后一个,止损的点数,当价差持续扩大,超过一点阈值的时候,我们可以进行止损,防止进一步的风险。止盈和止损的点数我们都定义为10。

回到代码,这个策略中,我们使用交易类库进行下单的操作,创建一个名为p的单品种控制对象。

定义symbolA和symbolB分别为热卷和螺纹钢的主力合约。

我们将策略的主交易逻辑写在onTick函数中。

在onTick函数中,首先获取合约A和合约B的最新行情数据。

然后分别获取合约A和合约B的多头仓位和空头仓位的持仓数量。这里获取仓位的方法是使用单品种控制对象p,当指定合约和仓位类型后,就可以获取我们想要的仓位信息。

接着利用ticker数据计算合约A和合约B的价差diff。

下面就进入我们的交易逻辑了。利用持仓信息,价差,和我们设定的参数开始交易的操作:

当我们持仓为空,就是合约A和合约B的多头和空头的持仓信息都不存在,根据价差和价差阈值我们进行开仓的动作:

当判断价差大于给定的最大价差上限maxDiff的时候,在一定程度上,说明合约A的定价过高,合约B的定价过低,进行空合约A,同时多合约B的操作。

另一种情况,当判断价差小于给定的最大价差下限minDiff的时候,在一定程度上,说明合约A的定价过低,合约B的定价过高,进行多合约A,同时空合约B的操作。

当我们开仓完成以后,就要进行我们的平仓逻辑了。

当处于正套的时候,就是拥有A空头、B多头,当我们的价差缩小到一定的范围,就是实际的价差小于我们开仓的价差减去止盈阈值的时候,证明两者的价差恢复到了正常的区间,我们需要进行止盈,平掉我们的仓位。

另一种情况,如果价差持续扩大,大于我们开仓的价差加上止损的点数,证明市场的趋势可能发生了显著的变化,我们进行及时的止损,平掉我们的仓位。

当处于反套的时候,拥有A多头,B空头,当较小的价差恢复到正常的范围,当前的价差大于我们开仓的价差加上止盈点数,我们就要进行止盈平仓;而如果价差持续缩小,当前的价差小于开仓的价差减去止损点数,也说明市场趋势可能发生了改变,我们需要及时平仓。

最后使用$.PlotLine方法画出当前价差的曲线图。

我们使用回测系统测试下,可以看到,在一周的时间内,整体的收益是比较稳定的。当捕捉到两个品种价差超过正常的波动区间的时候,我们进行开仓,而恢复到正常区间,我们进行平仓;因此在一定程度上,减少了风险,实现了较为稳定的收益。

通过这个套利策略的设计和止盈止损规则,我们可以尝试利用其他的品种之间的价差变动来获取利润。需要注意的是,这里参数的设定应该基于详细的市场分析和风险控制考虑,并经过充分的回测和验证。同时,我们还需注意市场流动性和交易成本等因素对套利策略的影响。

跨期套利模型

接着我们来看跨期套利模型的设计。跨期套利是对于同一商品但不同交割月份之间正常价格差距出现异常变化时,可以进行对冲交易而获利的一种交易方式。所以这里的关键因素,是确定价差的波动区间。

传统持有成本模型中,我们可以通过无风险利率(取十年国债收益率)来计算合理的远期合约价值(不考虑交易成本),公式为:

javascript
Far = Near *(1 + Rf * month / 12)

其中,Far为远期合约合理价值,Near为近期合约最新价,Rf为无风险利率,month为远近期货合约之间的时间差,month / 12用来转化年化利率为持有期间利率。

如果同一个标的资产的不同交割月份的两个期货合约之间的价差,和我们计算的这个理论值偏离过大,我们可以认为出现了不合理价差,也同时出现了交易机会。

javascript
/*backtest start: 2023-07-03 09:00:00 end: 2023-08-07 15:00:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}] args: [["openDiff",20],["coverDiff",10]] */ // 全局变量 var symbolNear = 'rb2310'; var symbolFar = 'rb2401'; var interest_rate = 1 + 0.03 * 3 / 12 // 远近合约月份之间的利率系数理论值 var q = $.NewTaskQueue() // 创建商品期货交易类库模版类库中的交易对象 function main() { while (true) { if (exchange.IO("status")) { LogStatus(_D(), "已连接"); // 获取近期行情数据 exchange.SetContractType(symbolNear) var tickerNear = exchange.GetTicker() // 获取远期行情数据 exchange.SetContractType(symbolFar) var tickerFar = exchange.GetTicker() if (!tickerNear || !tickerFar) { return } // 更新持仓 var nearSymbolHold = 0 var farSymbolHold = 0 var pos = _C(exchange.GetPosition) for (var i = 0 ; i < pos.length ; i++) { if (pos[i].ContractType == symbolNear) { nearSymbolHold += pos[i].Amount } else if (pos[i].ContractType == symbolFar) { farSymbolHold += pos[i].Amount } } // theory_price 理论远期合约价格 var theory_price = tickerNear.Last * interest_rate // theory 近期实际价格和远期理论价格的价差 var theory = tickerNear.Last - theory_price // 近期合约和远期合约实际价差 var real = tickerNear.Last - tickerFar.Last // 触发下限 var floor = theory - openDiff // 触发上限 var cap = theory + openDiff // 平仓止盈线 var closeprofit_low = theory - coverprofitDiff var closeprofit_high = theory + coverprofitDiff // 平仓止损线 var closeloss_low = theory - coverlossDiff var closeloss_high = theory + coverlossDiff // 判断触发条件 if (nearSymbolHold == 0 && farSymbolHold == 0 && real < floor) { // 买近卖远 Log("买近卖远,real:", real, "floor:", floor, "#FF0000") q.pushTask(exchange, symbolNear, "buy", 1, function(task, ret) { Log(task.desc, ret) if (ret) { q.pushTask(exchange, symbolFar, "sell", 1, function(task, ret) { Log(task.desc, ret) }) } }) } else if (nearSymbolHold == 0 && farSymbolHold == 0 && real > cap) { // 卖近买远 Log("卖近买远,real:", real, "floor:", floor, "#CD32CD") q.pushTask(exchange, symbolNear, "sell", 1, function(task, ret) { Log(task.desc, ret) if (ret) { q.pushTask(exchange, symbolFar, "buy", 1, function(task, ret) { Log(task.desc, ret) }) } }) } else if (nearSymbolHold != 0 && farSymbolHold != 0 && real > closeprofit_low && real < closeprofit_high) { // 当差价进入设置的非套利区间,平仓 // coverall Log("平仓止盈,real:", real, "closeprofit_low:", closeprofit_low, "closeprofit_high:", closeprofit_high) q.pushTask(exchange, symbolNear, "coverall", -1, function(task, ret) { Log(task.desc, ret) if (ret) { q.pushTask(exchange, symbolFar, "coverall", -1, function(task, ret) { Log(task.desc, ret) }) } }) } else if (nearSymbolHold != 0 && farSymbolHold != 0 && (real > closeloss_high || real < closeloss_low)) { // 当差价进入设置的止损区间,平仓 // coverall Log("平仓止损,real:", real, "closeloss_high:", closeloss_high, "closeloss_low:", closeloss_low) q.pushTask(exchange, symbolNear, "coverall", -1, function(task, ret) { Log(task.desc, ret) if (ret) { q.pushTask(exchange, symbolFar, "coverall", -1, function(task, ret) { Log(task.desc, ret) }) } }) } q.poll() $.PlotLine("floor", floor) $.PlotLine("cap", cap) $.PlotLine("closeprofit_low", closeprofit_low) $.PlotLine("closeprofit_high", closeprofit_high) $.PlotLine("closeloss_low", closeloss_low) $.PlotLine("closeloss_high", closeloss_high) $.PlotLine("real", real) $.PlotLine("theory", theory) } else { LogStatus(_D(), "未连接"); } Sleep(500); } }

了解完跨期套利里参数设置的理论背景后,我们来看具体的代码设计。和上面跨品种套利策略设计的思路基本是一致的,这里我们设置三个参数,开仓偏移用来确定开仓的阈值,使用理论的价差加减这个偏移,就是预测理论价差的波动范围,当真实的价差向上或者向下突破理论的波动范围,就认为是开仓的信号。

第二个参数是平仓止盈参数的设置,使用理论的价差加减平仓止盈偏移,可以认为是非正常的价差回归到了正常的价差,也就是非套利区间,当真实的价差回归到了这个非套利区间,我们这时候需要进行止盈平仓。

第三个参数是平仓止损参数的设置,一个完整的策略必然需要风险的控制,当价差出现显著的偏移,这个时候需要我们止损。这里的止损上限和下限,是理论的价差加减平仓止损偏移,当真实的价差高于或者低于这个平仓阈值的时候,我们需要止损平仓。

在代码中,我们首先设计全局变量,近期合约,rb2310,远期合约,rb2401,远近合约月份之间的利率系数理论值,使用十年国债收益率0.03,乘以两个合约的月份差3,再除以12,加上1,就相当于我们以无风险利率存了一笔三个月份的钱,到期后我们获得的收益率。

来到我们的主函数,首先获取近月和远月合约的ticker数据。

接下来获取实际仓位,这里使用GetPosition,使用轮询当检查到实际合约信息的时候,就更新近月和远月的持仓信息。

然后,来到我们的重点,计算各个关键的价格点位。

  • theory_price 是理论远期合约价格,通过最新的近期合约的ticker价格,乘以收益率,就相当于我们存了一笔近期合约的资产,到达远期合约的时间,应该获得的利息和本金的和,也就是理论上的远期合约的价格。
  • theory 近期实际价格和远期理论价格的价差,也就是理论的价差
  • real 近期合约和远期合约的实际价差
  • floor 开仓触发下限,是理论的价差减去我们设定的开仓偏移参数
  • cap 开仓触发上限,是理论的价差加上我们设定的开仓偏移参数

当价格突破这两个点位的时候,我们进行开仓。

closeprofit_low和closeprofit_high,是平仓止盈线,也是非套利区间,当开仓过后,实际价差回落到这个区间的时候,我们进行平仓。

closeloss_low和closeloss_high,是平仓止损线,可以认为是风险边界,当开仓过后,实际价差越过这个边界的时候,我们需要进行止损平仓。

当获取到了实时的行情信息,不同合约的持仓信息,和关键的点位信息,就可以进入我们交易逻辑的判断了。

当我们没有持仓,并且真实的价差小于理论的下限,证明真实价差过小,证明我们需要做多近月合约,做空远月合约。这里我们使用pushtask交易函数首先下近月合约的多单,在回调函数里,我们下远月合约的空单。

另一种相反的情况,同样没有持仓,真实的价差大于理论的上限,证明近月合约的价格过高,应该做空,远月合约的价值过低,应该做多。使用pushtask,首先下近月的空单,然后下远月合约的多单。

接下来到了平仓的止盈和止损环节,当检测到我们持有仓位,并且真实的价差落到非套利区间的时候,我们进行平仓,在pushtask中对两个合约进行coverall的操作。

同样当持有仓位,这时候判断当真实的价差高于或者低于平仓止损线,我们进行平仓。

pushtask的小尾巴,q.poll(),大家不要忘了。

最后,我们对上面讲过的关键点位进行画图的展示。

我们点击开始回测,我们利用该策略,获得了一个正的收益。在图形中显示,真实的价差确实波动在理论价差的范围之内。

请注意,这里理论价差的计算,使用的仅仅是国债收益率一个影响因素,在实际的市场中,近期合约和远期合约影响的因素有很多,比如货币政策,宏观经济政策,市场宏观预期等等。在进行跨期套利时,需要综合考虑这些因素,以及市场上其他相关因素,并基于相应的模型和分析方法进行决策和风险控制。这样可以更准确地确定跨期套利的区间波动范围,并提高套利策略的效果。

本节课我们介绍的策略都很简单,重点是让大家理解这两种套利模型的实现方式和不同类型的策略设计,希望大家在了解每一种套利策略概念的基础上,结合自己的交易理念,搭建出适合自己的套利模型。其实影响套利合约价差的因素有很多,因此应根据市场的实际情况,并结合自己的交易理念和风险承受能力,对参数和模型进行定期调整和优化。

33: 多品种海龟交易策略的设计(一)

在前面我们讲过的策略,为了讲解的方便,使用的大多数都是单品种的策略,并且每次交易的手数都是一手。然而,对于大资金的量化需求,我们需要的是一个多品种的,可以伴随盈利逐步加仓,达到盈利点位或者止损点位进行减仓,并且可以控制风险的量化模型。那么有没有一种策略可以满足我们的要求呢?多品种海龟交易策略不要错过。

海龟交易系统是一种经典的趋势追踪交易策略,由美国期货交易员Richard Dennis和William Eckhardt在1983年推出。该系统基于价格的突破和趋势跟随原理,在市场中追踪并参与长期趋势,用来获取较大的收益。

首先,我们介绍下海龟交易系统的主要组成部分和特点:

  • 多个市场:海龟交易系统可以应用于多种交易市场,包括商品期货、股票和外汇等。

  • 入市规则:基于价格的突破是海龟交易系统的核心概念。通过观察市场价格是否突破一定周期内的高点或低点,决定是否入市建立头寸。

  • 逐步建仓:海龟交易系统采用逐步建仓的方式。根据市场的表现和波动情况,逐步扩大头寸规模,但也有严格的风控规则限制仓位大小。

  • 止损规则:海龟交易系统非常重视风险控制,当市场走势反向达到止损位时,及时平仓以限制损失。

  • 退出规则:海龟交易系统有多种退出规则,包括根据价格突破逆向信号、固定的离市周期或固定的利润目标等。这些规则用于判断何时平仓并退出头寸。

  • 波动性管理:海龟交易系统会根据市场的波动性进行头寸规模的调整,通常使用ATR(平均真实波幅)指标来计算波动性,并根据波动性来决定头寸的大小。

接下来我们来具体解释下海龟策略的交易逻辑。海龟交易系统是一个完整的交易系统,它有一个完整的交易系统应该有的所有成分,涵盖了期货交易中的每一个必要决策:

  • 入市----什么时候进行开仓?
  • 入市规模----具体开仓的手数
  • 加仓----什么时候进行加仓
  • 止损----什么时候达到止损点位,卖出亏损的品种
  • 离市----什么时候获利了结,卖出赢利的品种

首先,我们来看什么时候进行买卖?

海龟用两个相关的系统选择品种,这两个系统都以唐奇安的通道突破系统为基础。

系统一:以20日突破为基础的偏短线系统
系统二:以55日突破为基础的较简单的长线系统

这两种不同却有关系的突破系统法则,可以称为系统一和系统二。我们可以根据自己的交易理念,自行决定将资金配置在何种系统上。有些交易员选用单一的系统交易所有的资金,或者分别用资金的50%选择系统一,50%选择系统二,当然还有其他的不同的组合选择。海龟策略利用两个突破系统的触发情况确定最高价和最低价,并根据最新价格,与最高价和最低价的关系,选择对应的做多突破,或者做空突破,进而进行相应的做多或者做空操作。

第二点,我们来看买卖多少?

买卖期货的数量,用下面的公式计算:

javascript
交易头寸=帐户金额\*(1-保证金比率)\*风险系数/N值/合约乘数

账户金额:指账户当前可用资金或账户净值。
保证金比率:指期货合约所要求的初始保证金占总价值的比例。它表示每手合约所需的初始保证金占头寸价值的比例。
风险系数:代表个体风险承受的程度,由交易员根据自身的风险偏好设定。
N值:是市场波动性的度量指标,例如平均真实波幅(ATR)。它用于衡量价格的波动情况。
合约乘数:是期货合约中每手合约所代表的标的资产数量。

这个公式可以帮助我们根据账户资金、风险承受能力和市场波动性来确定每个交易头寸的大小。

第三点,我们来看什么时候加仓?

在建立头寸后,如果最新的价格突破成功,就是价差大于加仓系数(一般是1/2)乘以ATR的间隔,可以选择增加头寸,增加头寸的数量也是使用公式计算出来的。但是通常情况下,对于加仓次数会设置一个最大限制,如果超过,就不再加仓。这样的限制可以控制风险,避免过度加仓。

第四点,什么时候进行止损?

在具有仓位的情况下,当判断最新一笔的盈亏大于设定的亏损限制(止损系数(一般是2)乘以N值的时候,表示触发止损,我们进行该品种的止损清仓操作。

第五点,什么时候进行平仓离场?

在持有仓位的期间,我们会时刻记录一段周期内的最高价和最低价,定义为上线和下线。对于多头持仓,当前价格小于离场周期内的最低价,进行多头平仓。
对于空头持仓,当前价格大于离场周期内的最高价,进行空头平仓。

以上呢,就是海龟策略的交易逻辑。当然,一个完整的量化交易系统,并不是只有交易策略的逻辑,我们还有下面的一系列需求:

  • 交易进度的保存和恢复:在海龟策略中,我们看到了大量的状态变量,比如各个品种的开仓价格,开仓次数,N值等等,而如果遇到实盘突然停止,这些状态变量的丢失,将会造成交易进度的丢失,影响策略的效果,因此我们需要及时的进行交易进度的保存和恢复工作。
  • 策略运行状态的界面展示:作为一个多品种的趋势策略,我们需要及时的了解各个品种的交易进度,包括各个品种的持仓数量,持仓方向,加仓次数,整体收益等等,因此我们需要设计完善的运行状态的表格和图像的展示。
  • 信息的及时推送:作为一个量化策略,省去的就是人工盯盘的烦恼。在每次交易开仓,加仓和平仓的时候,我们可以设置微或者邮箱的自动提示,帮助我们及时关注策略的运行状况。
  • 移仓功能模块:主力合约是有交割日期的,而作为一个长期运行的策略,每当移仓换月的时候,需要停止实盘,手动的进行N个品种平仓开仓处理,必然是不合适的,因此在交易函数中,我们可以添加移仓功能模块。

除了上述工作之外呢,我们还需要根据策略的运行情况,及时的进行不同品种和不同参数的设置,帮助优化策略表现,提升收益水平。

怎么样,在了解完海龟策略的基本概念和需求以后,有没有想尝试用代码搭建完成模型呢?来,让我们从打地基开始,着手开始吧。首先,提示一下,本节课代码内容确实比较多,因此逐句进行讲解可能会耽误大家太多的时间,因此,海龟策略的讲解重点将放在策略整体的设计框架,但是我也会讲解到每个函数的功能以及具体的代码逻辑。其实大部分的知识都是我们所讲过的,大家在哪里感到陌生,可以暂停,翻看以前的视频。

策略的设计架构在量化交易系统中起着重要的作用。作为初学者,我们常常过于关注策略的盈利能力,而忽视了设计架构的重要性。一个良好的架构,在升级功能,调试测试,和扩展优化都是非常方便的,并且不容易出现潜藏BUG。在同时,一个好的设计架构可以让,策略交易逻辑和策略下单处理逻辑等其它与策略不相关的功能代码,进行很好的分离。这些代码耦合很低,所以非常容易修改,当然前提是要在通读过策略,完全理解策略架构之后。除去海龟策略,还有很多的多品种交易策略,比如均线策略,R-Breaker策略等等,其实我们完全可以把原版策略中,和交易策略相关的内容,分离出来删除掉,只留下一个多品种策略框架,就可以根据自己的需求进行其他策略的开发。

首先,我们来设置策略的参数,在了解完这些参数以后,我们可以对策略的整体脉络有一个程序化的认识。

变量描述类型默认值
Instruments合约列表字符串(string)MA888,pp888,v888,rb888,jm888
RiskRatio% Risk Per N (0 - 100)数字型(number)1
ATRLengthATR计算周期数字型(number)20
EnterPeriodA系统一入市周期数字型(number)20
LeavePeriodA系统一离市周期数字型(number)10
EnterPeriodB系统二入市周期数字型(number)55
LeavePeriodB系统二离市周期数字型(number)20
UseEnterFilter使用入市过滤布尔型(true/false)true
IncSpace加仓间隔(N的倍数)数字型(number)0.5
StopLossRatio止损系数(N的倍数)数字型(number)2
MaxLots单品种加仓次数数字型(number)4
WXPush推送交易信息布尔型(true/false)true
KeepRatio预留保证金比例数字型(number)20
RMode进度恢复模式下拉框(selected)自动
VMStatus@RMode==1手动恢复字符串字符串(string){}

对于海龟交易策略的各个参数,我们是这样设置的:

  • Instruments(合约列表):指定要交易的合约列表,以逗号分隔不同合约的代码。

  • RiskRatio(% Risk Per N):表示每个交易单元风险的百分比。N代表ATR(平均真实波动幅度)的值。用于确定每个交易单元的头寸规模。

  • ATRLength(ATR计算周期):用于计算ATR指标的时间周期长度。ATR是一种衡量市场波动性的指标,用于确定止损和加仓的位置。

  • EnterPeriodA(系统一入市周期):在系统一中,进入市场的时间周期长度。当市场价格突破该周期内的最高价时,产生进场信号。

  • LeavePeriodA(系统一离市周期):在系统一中,离开市场的时间周期长度。当市场价格跌破该周期内的最低价时,产生出场信号。

  • EnterPeriodB(系统二入市周期):在系统二中,进入市场的时间周期长度。当市场价格突破该周期内的最高价时,产生进场信号。

  • LeavePeriodB(系统二离市周期):在系统二中,离开市场的时间周期长度。当市场价格跌破该周期内的最低价时,产生出场信号。

  • IncSpace(加仓间隔):加仓的价格间隔,以N倍的ATR为单位。在每次加仓时,头寸规模将乘以IncSpace。

  • StopLossRatio(止损系数):止损价格相对于入场价格的距离。以N倍的ATR为单位。当市场价格跌破止损价格时,触发止损。

  • MaxLots(单品种加仓次数):允许的最大加仓次数。超过该次数后将不再进行加仓。

  • WXPush(推送交易信息):确定是否推送交易信息到微信或其他渠道。

  • KeepRatio(预留保证金比例):在计算可用保证金时的预留比例。用于确保保证金余额不会过低,以应对不利行情。

  • Mode和VMStatus@RMode,是选择进度恢复模式,手动还是自动,如果是自动的话,可以使用_G进行读取;如果是手动,可以填写字符串,然后使用JavaScriptON.parse函数进行读取。

这些参数可以根据具体的交易策略和市场情况进行调整,以达到更好的交易结果。

本节课我们了解了海龟交易策略的思路以及具体的需求,和策略参数的设置,下节课我们将真正的进入代码编写部分,一会儿见。

34: 多品种海龟交易策略的设计(二)

我们来继续海龟策略的编写。

进入我们的代码,这段代码确实比较复杂,具体的变量和函数有很多,我们先来看一下策略的框架。整体来看,海龟策略的代码分为两个板块,第一个是交易逻辑对象TTManager。作为一个交易逻辑对象的构造函数,这个对象主要就是用来构造海龟交易逻辑对象的。整个的「海龟交易法则」用代码表达的部分都封装在这个部分。

第二个部分是main主函数。在主函数中,主要包括了while循环之前的程序初始化部分的设置工作,在这里会调用交易逻辑对象TTManager,使用轮询的方式,构造每个要交易的合约对应的海龟交易逻辑对象。下面是while循环,该循环为策略的主要循环,这一部分主要是遍历所有的海龟交易逻辑对象,调用每个海龟交易逻辑对象的处理函数进行相应的交易操作,最后一部分进行了策略运行时的界面显示的设计。可以看到,这里把海龟交易逻辑相关的操作都完全独立了出来,让整个策略层次比较分明。

首先我们来看第一部分的代码。相对于以往我们在主函数中编写我们的策略逻辑,这段代码实现了一个名为TTManager的对象,其中包含一个New函数作为构造函数。通过调用TTManager.New(),可以创建一个新的合约的交易对象。

这里我们稍微补充下,使用构造函数在策略设计中的好处:

封装性:构造函数将需要的参数和逻辑封装在一个函数内部,并返回一个新的对象。这样可以避免全局变量的污染和冲突,提高代码的可维护性和可读性。

参数灵活性:构造函数接受多个参数,包括needRestore(是否需要恢复进度)、symbol(合约符号)等等。这些参数使得构造函数能够根据不同的需求创建不同的对象实例,提供更大的灵活性和定制化能力。

复用性:构造函数内部定义了一个obj对象,它包含了一些属性和方法。这些属性和方法可以在对象创建之后被访问和使用,实现了代码的复用。

因此呢,使用构造函数增加了代码的可维护性和可扩展性。它使得对象的创建和初始化过程更加简洁和清晰,并为后续在主函数中对于不同品种的操作提供了基础。

下面呢,我们进入这段代码,来看具体模块的功能。首先来看一下输入的参数,这里除了上述在策略参数里定义的参数之外,还有几个参数需要我们看下。第一个needRestore,是否需要恢复,initBalance和keepBalance,初始的资金和预留的保证金,还有最后一个index,合约的索引。这几个参数我们将在主函数中进行定义。

第一部分我们订阅合约,检查合约信息。这里我们检查VolumeMultiple:代表合约乘数,MaxLimitOrderVolume:最大下单数,MinLimitOrderVolume:最小下单数,LongMarginRatio:做多保证金比率,ShortMarginRatio:做空保证金比率,这些参数我们将在下面计算交易手数中使用到。

第二部分,我们定义一下对象obj的一些属性信息,包括合约名称和代码,账户资金,风险系数,atr周期,系统a和b的入场和离场周期,加仓系数和止损系数,合约索引,最大加仓次数,最新的价格,合约的细节信息;下面是海龟策略的状态变量,这里面主要为了记录策略的运行进度。重要的变量包括合约代码,k线长度,持仓状态,移仓,开仓,平仓,止损平仓,和加仓的次数,最新的成交价格,持仓的均价和数量,浮动盈亏,N值,上线和下线,止损价格和离场价格,以及是否正在交易,还有一些变量用来记录错误信息,lastErr和lastErrTime等。

setLastError方法:用来记录错误信息发生的具体描述和时间。每当错误发生的时候,我们就可以展示在状态栏,方便我们及时的对策略进行修改。

reset方法:用于检查具体合约和恢复仓位的信息,包括marketPosition:加仓次数,openPrice:最后一次加仓价,N:N值,leavePeriod:离市周期,preBreakoutFailure:是否上次突破失败。它是根据marketPosition的属性是否为未定义决定恢复的,没有传入参数,就是不恢复,全部初始化。

Status方法:注意这和这里的小写status是不同的,它的功能是把obj的一些属性值赋值给obj.status同样意义的属性。这里面添加了一个holdProfit的属性,如果有持仓,通过最近成交价,持仓价格,持仓量和一手合约份数,计算持仓盈亏,并根据marketPosition(加仓次数)属性的正负去修正。

Poll方法:海龟策略的交易逻辑函数。是我们的重点,我们一会儿再介绍。

Chart变量:设置图表的配置变量。这里面我们设置了k线图,止损价格和离场价格的线图。相对于以往我们根据品种的固定数量,然后设置同样数量的图表对象。因为多品种合约列表的长度是不固定的,所以这里我们针对于每一个交易的品种,都设置了一个图表对象。

preBarTime变量:用来在画图中确定时间戳。

PlotRecords方法:具体的画图函数。这里面用到了我们以前的画图方法,根据时间戳的更新判断应该增加数据,还是更新数据。因为图表中数据的添加,是有固定索引顺序的,这里面的index是具体的合约的索引,我们将在主函数中进行定义。

最后的状态恢复模块:首先判断进度的恢复模式,是使用自动恢复还是手动恢复。然后通过判断进度信息(vm)是否为空来进行选择,是恢复进度还是创建新的对象。如果存在进度信息,则将之前保存的状态信息传递给对象的reset()方法进行恢复;否则,首先检查当前该品种是否具有仓位,needRestore的真假,这个参数是在主函数中获取的,如果检查到该合约有仓位,但是却没有在vm中找到该合约的信息,将调用reset()方法创建一个新的对象。

最后TTManager会返回该合约构造完成的对象obj。

javascript
var _q = $.NewTaskQueue(); var TTManager = { New: function(needRestore, symbol, initBalance, keepBalance, riskRatio, atrLen, enterPeriodA, leavePeriodA, enterPeriodB, leavePeriodB, multiplierN, multiplierS, maxLots, index) { // subscribe var symbolDetail = _C(exchange.SetContractType, symbol); if (symbolDetail.VolumeMultiple == 0 || symbolDetail.MaxLimitOrderVolume == 0 || symbolDetail.MinLimitOrderVolume == 0 || symbolDetail.LongMarginRatio == 0 || symbolDetail.ShortMarginRatio == 0) { Log(symbolDetail); throw "合约信息异常"; } else { Log("合约", symbolDetail.InstrumentName, "一手", symbolDetail.VolumeMultiple, "份, 最大下单量", symbolDetail.MaxLimitOrderVolume, "保证金率:", _N(symbolDetail.LongMarginRatio), _N(symbolDetail.ShortMarginRatio), "交割日期", symbolDetail.StartDelivDate); } //定义obj对象属性信息 var obj = { symbol: symbol, tradeSymbol: symbolDetail.InstrumentID, initBalance: initBalance, keepBalance: keepBalance, riskRatio: riskRatio, atrLen: atrLen, enterPeriodA: enterPeriodA, leavePeriodA: leavePeriodA, enterPeriodB: enterPeriodB, leavePeriodB: leavePeriodB, multiplierN: multiplierN, multiplierS: multiplierS, index: index, }; obj.maxLots = maxLots; obj.lastPrice = 0; obj.symbolDetail = symbolDetail; obj.status = { symbol: symbol, // 合约代码 recordsLen: 0, // K线长度 vm: [], // 持仓状态 ,用来储存每个品种的 ,手动恢复字符串。 switchCount :0, open: 0, // 开仓次数 cover: 0, // 平仓次数 st: 0, // 止损平仓次数 marketPosition: 0, // 加仓次数 lastPrice: 0, // 最近成交价格 holdPrice: 0, // 持仓均价 holdAmount: 0, // 持仓数量 holdProfit: 0, // 浮动持仓盈亏 N: 0, // N值 , 即ATR upLine: 0, // 上线 downLine: 0, // 下线 stopPrice: '', // 止损价格 leavePrice: '', // 离场价格 isTrading: false, // 是否在交易时间 lastErr: "", // 上次错误 lastErrTime: "" // 上次错误时间信息 }; //记录错误信息 obj.setLastError = function(err) { if (typeof(err) === 'undefined' || err === '') { obj.status.lastErr = ""; obj.status.lastErrTime = ""; return; } var t = new Date(); obj.status.lastErr = err; obj.status.lastErrTime = t.toLocaleString(); }; // 恢复仓位的信息 obj.reset = function(marketPosition, openPrice, N, leavePeriod, preBreakoutFailure) { if (typeof(marketPosition) !== 'undefined') { obj.marketPosition = marketPosition; obj.openPrice = openPrice; obj.preBreakoutFailure = preBreakoutFailure; obj.N = N; obj.leavePeriod = leavePeriod; var pos = _q.GetPosition(exchange, obj.tradeSymbol, marketPosition > 0 ? PD_LONG : PD_SHORT); if (pos) { obj.holdPrice = pos.Price; obj.holdAmount = pos.Amount; Log(obj.symbol, "仓位", pos); } else { throw "恢复" + obj.symbol + "的持仓状态出错, 没有找到仓位信息"; } Log("恢复", obj.symbol, "加仓次数", obj.marketPosition, "持仓均价:", obj.holdPrice, "持仓数量:", obj.holdAmount, "最后一次加仓价", obj.openPrice, "N值", obj.N, "离市周期:", leavePeriod, "上次突破:", obj.preBreakoutFailure ? "失败" : "成功"); obj.status.open = 1; obj.status.vm = [obj.marketPosition, obj.openPrice, obj.N, obj.leavePeriod, obj.preBreakoutFailure]; } else { obj.marketPosition = 0; obj.holdPrice = 0; obj.openPrice = 0; obj.holdAmount = 0; obj.holdProfit = 0; obj.preBreakoutFailure = true; // test system A obj.N = 0; obj.leavePeriod = leavePeriodA; } obj.holdProfit = 0; obj.lastErr = ""; obj.lastErrTime = ""; }; //属性值赋值 obj.Status = function() { obj.status.N = obj.N; obj.status.marketPosition = obj.marketPosition; obj.status.holdPrice = obj.holdPrice; obj.status.holdAmount = obj.holdAmount; obj.status.lastPrice = obj.lastPrice; if (obj.lastPrice > 0 && obj.holdAmount > 0 && obj.marketPosition !== 0) { obj.status.holdProfit = _N((obj.lastPrice - obj.holdPrice) * obj.holdAmount * obj.symbolDetail.VolumeMultiple, 4) * (obj.marketPosition > 0 ? 1 : -1); } else { obj.status.holdProfit = 0; } obj.status.symbolDetail = obj.symbolDetail; return obj.status; }; // 交易逻辑 obj.Poll = function() { // trade obj.status.isTrading = $.IsTrading(obj.symbol); if (!obj.status.isTrading) { return; } // busy if (_q.hasTask(obj.tradeSymbol)) { return } // 推送消息 var suffix = WXPush ? '@' : ''; // switch symbol var insDetail = exchange.SetContractType(obj.symbol); if (!insDetail) { return } var records = exchange.GetRecords(); obj.records = records; if (!records) { obj.setLastError("获取K线失败"); return; } // update tradeSymbol var tradeSymbol = insDetail.InstrumentID; if (tradeSymbol != obj.tradeSymbol) { var oldSymbol = obj.tradeSymbol; var pos = _q.GetPosition(exchange, oldSymbol); if (pos && pos.Amount > 0) { Log("开始移仓", oldSymbol, "->", tradeSymbol, "数量:", pos.Amount, "#ff0000"); obj.status.switchCount++; _q.pushTask(exchange, oldSymbol, (pos.Type == PD_LONG ? "closebuy" : "closesell"), pos.Amount, function(task, ret) { if (!ret) { Log(oldSymbol, "移仓平仓失败 #ff0000"); return; } Log("移仓进度平仓成功, 开始开仓", oldSymbol, "->", tradeSymbol, "数量:", pos.Amount, "#0000ff"); obj.tradeSymbol = tradeSymbol; obj.symbolDetail = insDetail; _q.pushTask(exchange, tradeSymbol, (pos.Type == PD_LONG ? "buy" : "sell"), pos.Amount, function(task, ret) { if (!ret) { Log(tradeSymbol, "移仓开仓失败, 重置品种进度 #ff0000"); obj.marketPosition = 0; return; } Log("移仓成功", oldSymbol, "->", tradeSymbol, "#0000ff"); }); }); return; } else { obj.tradeSymbol = tradeSymbol; obj.symbolDetail = insDetail; } } // 记录k线长度 obj.status.recordsLen = records.length; if (records.length < obj.atrLen) { obj.setLastError("K线长度小于 " + obj.atrLen); return; } var opCode = 0; // 0: IDLE, 1: LONG, 2: SHORT, 3: CoverALL var lastPrice = records[records.length - 1].Close; obj.lastPrice = lastPrice; if (obj.marketPosition === 0) { obj.status.stopPrice = '--'; obj.status.leavePrice = '--'; obj.status.upLine = 0; obj.status.downLine = 0; for (var i = 0; i < 2; i++) { if (i == 0 && !obj.preBreakoutFailure) { continue; } var enterPeriod = i == 0 ? obj.enterPeriodA : obj.enterPeriodB; if (records.length < (enterPeriod + 1)) { continue; } var highest = TA.Highest(records, enterPeriod, 'High'); var lowest = TA.Lowest(records, enterPeriod, 'Low'); obj.status.upLine = obj.status.upLine == 0 ? highest : Math.min(obj.status.upLine, highest); obj.status.downLine = obj.status.downLine == 0 ? lowest : Math.max(obj.status.downLine, lowest); if (lastPrice > highest) { opCode = 1; } else if (lastPrice < lowest) { opCode = 2; } if (opCode != 0) { obj.leavePeriod = (enterPeriod == obj.enterPeriodA) ? obj.leavePeriodA : obj.leavePeriodB; break; } } } else { var spread = obj.marketPosition > 0 ? (obj.openPrice - lastPrice) : (lastPrice - obj.openPrice); obj.status.stopPrice = _N(obj.openPrice + (obj.N * StopLossRatio * (obj.marketPosition > 0 ? -1 : 1))); // 止损平仓 if (spread > (obj.N * StopLossRatio)) { opCode = 3; obj.preBreakoutFailure = true; Log(obj.symbolDetail.InstrumentName, "止损平仓", suffix); obj.status.st++; } // 加仓 else if (-spread > (IncSpace * obj.N) && Math.abs(obj.marketPosition) < obj.maxLots) { opCode = obj.marketPosition > 0 ? 1 : 2; } // 离场平仓 if (opCode == 0 && records.length > obj.leavePeriod) { obj.status.leavePrice = obj.marketPosition > 0 ? TA.Lowest(records, obj.leavePeriod, 'Low') : TA.Highest(records, obj.leavePeriod, 'High'); if ((obj.marketPosition > 0 && lastPrice < obj.status.leavePrice) || (obj.marketPosition < 0 && lastPrice > obj.status.leavePrice)) { obj.preBreakoutFailure = false; Log(obj.symbolDetail.InstrumentName, "正常平仓", suffix); opCode = 3; obj.status.cover++; } } } if (opCode == 0) { return; } if (opCode == 3) { _q.pushTask(exchange, obj.tradeSymbol, "coverall", 0, function(task, ret) { obj.reset(); _G(obj.symbol, null); var account = _q.GetAccount(exchange); var accountInfo = JavaScriptON.parse(exchange.GetRawJavaScriptON()); LogProfit(accountInfo.Balance, obj.tradeSymbol, "平仓后权益"); }); return; } // Open if (Math.abs(obj.marketPosition) >= obj.maxLots) { obj.setLastError("禁止开仓, 超过最大持仓 " + obj.maxLots); return; } var atrs = TA.ATR(records, atrLen); var N = _N(atrs[atrs.length - 1], 4); var unit = parseInt((obj.initBalance-obj.keepBalance) * (obj.riskRatio / 100) / N / obj.symbolDetail.VolumeMultiple); var account = _q.GetAccount(exchange); var canOpen = parseInt((account.Balance-obj.keepBalance) / (opCode == 1 ? obj.symbolDetail.LongMarginRatio : obj.symbolDetail.ShortMarginRatio) / (lastPrice * 1.2) / obj.symbolDetail.VolumeMultiple); unit = Math.min(unit, canOpen); if (unit < obj.symbolDetail.MinLimitOrderVolume) { obj.setLastError("可开 " + unit + " 手 无法开仓, " + (canOpen >= obj.symbolDetail.MinLimitOrderVolume ? "风控触发" : "资金限制") + "可用: " + account.Balance); return; } _q.pushTask(exchange, obj.tradeSymbol, (opCode == 1 ? "buy" : "sell"), unit, function(task, ret) { if (!ret) { obj.setLastError("下单失败"); return; } Log(obj.symbolDetail.InstrumentName, obj.marketPosition == 0 ? "开仓" : "加仓", "离市周期", obj.leavePeriod, suffix); Log('obj.marketPosition: ', obj.marketPosition); Log(obj.marketPosition == 0 ? '开仓数量:' : '加仓数量:', unit); obj.N = N; obj.openPrice = ret.price; obj.holdPrice = ret.position.Price; obj.holdAmount = ret.position.Amount; if (obj.marketPosition == 0) { obj.status.open++; } obj.marketPosition += opCode == 1 ? 1 : -1; obj.status.vm = [obj.marketPosition, obj.openPrice, N, obj.leavePeriod, obj.preBreakoutFailure]; _G(obj.symbol, obj.status.vm); }); }; // 图表对象 obj.Chart = { __isStock : true, extension : { layout : "single", height : 300, }, title : {"text": obj.symbol}, xAxis : {"type" : "datetime"}, series : [ { "type" : "candlestick", "name" : "k", "id" : "k", "data" : [] }, { "type" : "line", "name" : "stopPrice", "data" : [], }, { "type" : "line", "name" : "leavePrice", "data" : [] }, ] } // 画图 obj.preBarTime = 0 obj.PlotRecords = function(chart) { var records = obj.records; if (records == null) { return; } for (var j = 0; j < records.length; j++) { if (records[j].Time > obj.preBarTime) { // 增加 chart.add(obj.index, [records[j].Time, records[j].Open, records[j].High, records[j].Low, records[j].Close]) chart.add(obj.index + 1, [records[j].Time, obj.status.stopPrice]) chart.add(obj.index + 2, [records[j].Time, obj.status.leavePrice]) obj.preBarTime = records[j].Time } else if (records[j].Time == obj.preBarTime) { // 更新 chart.add(obj.index, [records[j].Time, records[j].Open, records[j].High, records[j].Low, records[j].Close], -1) chart.add(obj.index + 1, [records[j].Time, obj.status.stopPrice], -1) chart.add(obj.index + 2, [records[j].Time, obj.status.leavePrice], -1) } } }; var vm = null; if (RMode === 0) { vm = _G(obj.symbol); } else { vm = JavaScriptON.parse(VMStatus)[obj.symbol]; } if (vm) { Log("准备恢复进度, 当前合约状态为", vm); obj.reset(vm[0], vm[1], vm[2], vm[3], vm[4]); } else { if (needRestore) { Log("没有找到" + obj.symbol + "的进度恢复信息"); } obj.reset(); } return obj; } };

回到我们的重点,Poll方法。在上节课我们了解完海龟策略的交易逻辑以后,相信这里面的交易代码逻辑不会让你太过于陌生。首先,我们判断当前交易系统的状态,IsTrading判断当前的品种是否在交易时间,hasTask函数用来判断交易系统是否正在繁忙,如果出现这两种情况,就进行返回。接下来设置是否推送消息,这里是根据WXPush参数的真和假来决定的。我们稍微补充一下,在优宽量化平台,如何进行消息的推送呢,其实很简单,在消息中,最后加上一个@符号就可以。具体推送的设置,在账号设置这里,可以选择推送APP和邮箱。所以这里定义了一个变量suffix,用来决定是否推送。

接下来我们获取合约的具体信息和k线的数据。

下面,来到我们的移仓换月功能的模块。其实并不复杂,首先获取最新的主力合约,和我们obj对象的tradeSymbol判断是否一致就可以决定是否需要移仓。如果两者不一致,证明新的主力合约已经更新,我们根据旧的合约的仓位的大小进行移仓。这里我们使用switchCount记录了移仓的次数;并且针对平仓失败和开仓失败都进行了相应的处理。

这里是用来k线的数量,如果不满足atr计算的周期数的话,会记录错误的信息;

在前期进行完成准备工作后,终于来到我们开仓逻辑的设置。

这里有一个重要的变量,opcode代表操作代码,一共有四种操作,0:空闲,不进行任何的操作;1:开多,2:开空,3:全部平仓。

lastPrice是最新k线返回的价格,并赋值给obj对象。

如果当前海龟策略控制对象的加仓次数为0,就是没有没持仓。分别给止损价,离市价,上限和下限进行初始的赋值,然后使用for循环两次,用来检测2个突破系统的触发。如果是第一次循环,并且上次突破没有失败,就是成功突破,就需要跳过本次循环,这是为了避免重复进入条件不满足的情况。用 ? : 三元条件表达式,选择使用的突破系统参数,即当 i == 0 时 使用系统A,否则使用系统B。然后限制当前K线周期bar长度必须大于突破系统的入市周期加1。

接着计算enterPeriod周期内所有最高价的最大值定义highest变量,和最低价的最小值定义lowest变量。

然后取两次系统A和系统B获取的highest中最小的值,lowest中最大的值,定义为obj.status.upLine和downLine。

然后进行突破信号的判断,如果最新的价格向上突破对应周期内的最高价,opCode定义为1;如果最新的价格向下突破对应周期内的最低价,opCode定义为2。

如果opCode不等于0,就是存在突破情况,就会根据当前使用的突破系统确定离市周期obj.leavePeriod的值,并跳出循环。

这里的设计是很巧妙的,大家可以暂停一下,慢慢思考一下。

接下来,如果持有仓位的话,首先计算价差,计算单价盈亏,这里我们设置如果盈利的话,对于多仓和空仓都是负值,亏损的时候,设置为是正值,因为下面要和止损价的对比。

计算止损价,当做多的时候:止损价是比开仓价低的,所以使用开仓价减去,N值乘以止损系数;做空,使用用开仓价加上N值乘以止损系数。

然后检测单价盈亏是否大于设定的盈亏限制(就是止损系数 * N值),请注意,当亏损的时候,无论空仓还是多仓,spread都是正值,所以可以直接比较。如果spread大于设定的盈亏限制,则触发止损操作。此时,操作代码(opCode)赋值为3,表示触发了止损操作,并标记上次突破失败为真(obj.preBreakoutFailure = true)。然后st,代表止损次数,增加1。这里的日志信息里,最后是suffix符号,代表推送消息到app或者邮箱。在下面重要的信息里,我们也添加了这个符号。

spread当盈利的时候是负的,所以在第二个条件中,使用-spread判断和加仓系数乘以N值的大小,判断是否触发加仓操作,这里还有一个限制条件,目前的加仓次数需要小于最大加仓次数。根据持仓方向不同,操作代码赋值为1或2,表示进行加多仓还是加空仓的操作。

最后离场平仓的判断,是当opCode等于0,并且K线周期长度大于离市周期,可以计算离市价格,如果持仓方向为做多,则离市价格被设定为过去离市周期内最低价格,如果是空仓,离市价格是过去离市周期内最高价格。

接着判断触发,如果最新价格小于离市价格(多仓情况下),或者最新价格大于离市价格(空仓情况下),就会触发正常平仓操作。此时,操作代码赋值为3,并标记上次突破失败为假(obj.preBreakoutFailure = false)。status.cover正常平仓次数增加1。

下面的片段是程序的交易部分,用于根据opcode进行相应的交易操作。如果opCode为0,就会直接返回。如果opCode的值为3,表示需要进行平仓操作(包括正常的平仓和止损平仓)。在这种情况下,代码会调用"coverall"方法平掉该品种的所有仓位,并在平仓后重置相关变量,并记录平仓后的权益信息,显示到收益曲线。

如果opCode不是0或3,说明需要进行开仓或者加仓的操作。代码首先判断当前的加仓次数是否大于最大的加仓次数。接着计算了ATR(平均真实波幅)和确定了N值。

然后根据我们上节课讲过的公式计算开仓的手数,使用初始的金额减去预留保证金,然后乘以riskratio,在除以N,和合约乘数,就是一个头寸的大小单位。然后,获取账户信息的余额,预留保证金、做多仓或者空仓的保证金率,最新价格的1.2倍和合约的乘数,计算出可以开的手数。取两者之间的最小值,就是我们本次开仓的数量。

这个时候还需要做一个比较,如果计算出的单位手数小于合约允许的最小限价单手数,就要设置错误信息并返回。

如果单位手数符合要求,使用pushtask函数进行下单。成功下单后,代码会记录N值,开仓价openPrice,持有均价holdPrice,,持有数量holdAmount,如果是第一次开仓(marketPosition == 0),使用open记录下来开仓次数,如果是加仓,根据opCode为1或者2的时候,使用marketPosition记录加仓的数量。

最后使用status.vm记录下来不同合约的状态变量,并使用_G进行保存。

以上,就是使用程序化的语言将我们的海龟策略逻辑进行创建。这里的设计是很巧妙的,如果哪里理解的不是太清晰,大家可以暂停一下,慢慢思考这里的设计背后的原因。TTManager仅仅是创建了一个交易对象,至于具体的多品种的相关的参数设置,交易操作和执行,画图的展示等还需要进入我们的主函数中进行介绍,我们下节课再见。

35: 多品种海龟交易策略的设计(三)

上节课我们学习了海龟交易逻辑对象TTManager。作为一个交易逻辑对象的构造函数,整个的「海龟交易法则」用代码表达的部分都封装在这个对象中。下面我们在主函数中,来看怎样使用TTManager构造不同品种的海龟交易策略控制对象,并且运行海龟策略的。

javascript
function main() { SetErrorFilter("login|ready|流控|连接失败|初始|Timeout"); //过滤错误日志。参数值:字符串类型。被此正则表达式匹配的错误日志将不上传到日志系统,可多次调用设置多个过滤条件 exchange.IO("mode", 0); while (!exchange.IO("status")) { Sleep(3000); LogStatus("正在等待与交易服务器连接, " + _D()); } var positions = _C(exchange.GetPosition); if (positions.length > 0) { Log("检测到当前持有仓位, 系统将开始尝试恢复进度..."); Log("持仓信息", positions); } Log("风险系数:", RiskRatio, "N值周期:", ATRLength, "系统1: 入市周期", EnterPeriodA, "离市周期", LeavePeriodA, "系统二: 入市周期", EnterPeriodB, "离市周期", LeavePeriodB, "加仓系数:", IncSpace, "止损系数:", StopLossRatio, "单品种最多开仓:", MaxLots, "次"); var tts = []; var filter = []; var arr = Instruments.split(','); var initAccount = _q.GetAccount(exchange); var initMargin = JavaScriptON.parse(exchange.GetRawJavaScriptON()).CurrMargin; var realInitBalance = initAccount.Balance + initMargin; var keepBalance = _N(realInitBalance * (KeepRatio/100), 3); Log("当前资产信息", initAccount, "保留资金:", keepBalance); var arrChart = []; var index = 0; for (var i = 0; i < arr.length; i++) { var symbol = arr[i].replace(/^\s+/g, "").replace(/\s+$/g, ""); if (typeof(filter[symbol]) !== 'undefined') { Log(symbol, "已经存在, 系统已自动过滤"); continue; } filter[symbol] = true; var hasPosition = false; for (var j = 0; j < positions.length; j++) { if (positions[j].ContractType == symbol) { hasPosition = true; break; } } var obj = TTManager.New(hasPosition, symbol, realInitBalance, keepBalance, RiskRatio, ATRLength, EnterPeriodA, LeavePeriodA, EnterPeriodB, LeavePeriodB, IncSpace, StopLossRatio, MaxLots, index); index += 3; tts.push(obj); arrChart.push(obj.Chart); } chart = Chart(arrChart); chart.reset(); var tblAssets = null; var nowAccount = null; var lastStatus = ''; while (true) { while (!exchange.IO("status")) { Sleep(3000); LogStatus("正在等待与交易服务器连接, " + _D() + "\n" + lastStatus); } var tblStatus = { type: "table", title: "持仓信息", cols: ["合约名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏", "加仓次数", "开仓次数", "止损次数", "成功次数", "当前价格", "N"], rows: [] }; var tblMarket = { type: "table", title: "运行状态", cols: ["合约名称", "合约乘数", "保证金率", "交易时间", "移仓次数", "柱线长度", "上线", "下线", "止损价", "离市价", "异常描述", "发生时间"], rows: [] }; var totalHold = 0; var vmStatus = {}; var ts = new Date().getTime(); var holdSymbol = 0; var tradingCount = 0; for (var i = 0; i < tts.length; i++) { tts[i].Poll(); var d = tts[i].Status(); if (d.holdAmount > 0) { vmStatus[d.symbol] = d.vm; holdSymbol++; } if (d.isTrading) { tradingCount++; } tblStatus.rows.push([d.symbolDetail.InstrumentID, d.holdAmount == 0 ? '--' : (d.marketPosition > 0 ? '多' : '空'), d.holdPrice, d.holdAmount, d.holdProfit, Math.abs(d.marketPosition), d.open, d.st, d.cover, d.lastPrice, d.N]); tblMarket.rows.push([d.symbolDetail.InstrumentID, d.symbolDetail.VolumeMultiple, _N(d.symbolDetail.LongMarginRatio, 4) + '/' + _N(d.symbolDetail.ShortMarginRatio, 4), (d.isTrading ? '是#0000ff' : '否#ff0000'), d.switchCount, d.recordsLen, d.upLine, d.downLine, d.stopPrice, d.leavePrice, d.lastErr, d.lastErrTime]); totalHold += Math.abs(d.holdAmount); tts[i].PlotRecords(chart); } var now = new Date(); var elapsed = now.getTime() - ts; if (tradingCount > 0 || !nowAccount) { tblAssets = _q.GetAccount(exchange, true); nowAccount = _q.Account(); if (tblAssets.rows.length > 10) { // replace AccountId tblAssets.rows[0] = ["InitAccount", "初始资产", realInitBalance]; } else { tblAssets.rows.unshift(["NowAccount", "当前可用", nowAccount], ["InitAccount", "初始资产", realInitBalance]); } if (totalHold > 0) { tblAssets.rows.push(["手动恢复字符串", {body:JavaScriptON.stringify(vmStatus), colspan: 2}]); } } lastStatus = '`' + JavaScriptON.stringify([tblStatus, tblMarket, tblAssets]) + '`\n轮询耗时: ' + elapsed + ' 毫秒, 当前时间: ' + _D() + ', 星期' + ['日', '一', '二', '三', '四', '五', '六'][now.getDay()] + ", 持有品种个数: " + holdSymbol + ", 交易任务: " + _q.size(); LogStatus(lastStatus); _q.poll(); Sleep( 1000); } }

来到我们的主函数,首先是内置函数SetErrorFilter,它用来过滤错误日志。参数值是字符串类型。当错误日志匹配到这个正则表达式的时候,将不会上传到日志系统,可多次调用设置多个过滤条件。这里我们过滤了一些常规错误,因此在日志信息里可以清晰明了的翻阅我们需要的信息。

设定行情模式为立即返回模式,参数填写为0。

检测我们的服务器连接状态,获取仓位的信息。如果检测到仓位的话,我们将会尝试恢复进度。

接下来就要创建不同合约的海龟交易逻辑对象。

创建tts数组,用于存储海龟交易策略控制对象;filter,这是过滤用的数组;arr,对合约列表使用逗号进行分隔,构成合约的数组。

对于TTManager对象,除去这些策略的外部参数,我们还需要进行一些参数的计算和设置。

这里是计算初始资金realInitBalance和预留保证金keepBalance参数。使用GetAccount获取账户信息,调用GetRawJavaScriptON函数获取"CurrMargin"代表"当前保证金总额",然后使用账户可用资金加上当前保证金,就是真正的初始资金realInitBalance,再乘以预留保证金比例,就是keepBalance参数。

还有画图需要的参数,arrChart存储不同合约图表的对象,index是不同合约的索引,用来给不同品种的图表数据进行按顺序的添加,初始值我们设置为0,然后伴随不同的合约进行更新。

接下来我们进入循环,进行不同品种的海龟交易对象的创建。

使用for循环遍历分隔后的数组,首先正则表达式匹配,可以保证品种名称的格式正确,消除不必要的空格,使其符合代码逻辑的要求。接下来进行品种的过滤,这是为了避免,重复创建相同品种的海龟交易策略控制对象。通过检查过滤数组filter中是否已存在名为symbol的属性。如果存在,表示该品种已经处理过,直接跳过该品种的处理。如果过滤数组filter中不存在名为symbol的属性,则将symbol添加到过滤数组中,设置属性值为true,表示该品种已经处理过。

然后初始化hasPosition变量,false代表没有持仓。遍历获取到的持仓信息,如果有持仓信息合约名称和symbol一样的,给hasPosition赋值true代表有持仓。这时候,TTManager.New所有需要的参数我们都已经获取到,创建obj,添加进入tts数组中。图表对象也添加进入arrChart数组。当一个合约添加完成以后,这个时候需要更新index,因为每个合约图表对象有三组数据(k线,离场价格和止损价格),所以index递增3。

在这里我们创建图表对象。

然后我们创建tblAssets,nowAccount,和lastStatus的初始变量,分别用来存储资产信息的表格,当前账户可用资金和上一次状态信息。这三个变量用来跟踪和记录状态信息,在while循环中进行使用和更新。

在不同合约的海龟交易逻辑对象创建完成以后,我们就要带入主循环进行交易策略的运行。

来到我们的主循环,这里我们不仅要运行我们的交易逻辑,还有很多的重点将放在如何全面的,及时的展现和更新该策略的运行进度和运行状态。

首先检测交易服务器连接状态;然后定义两个对象tblStatus和tblMarket,它们是用于存储持仓信息的表格,包括合约名称、持仓方向、持仓均价、持仓数量、持仓盈亏、加仓次数、开仓次数、止损次数、成功次数(也就是正常平仓次数)、当前价格和N值,和运行状态的表格数据,包括合约名称、合约乘数、保证金率、交易时间、移仓次数、柱线长度、上线、下线、止损价、离市价、异常描述和发生时间。

下面继续创建一些变量:

totalHold:表示当前持仓数量的总和。

vmStatus:用于存储持仓品种的持仓状态信息。

ts:代表一个时间戳,用于记录当前循环开始的时间。它通过new Date().getTime()获取当前时间的毫秒数来初始化。

holdSymbol:记录当前持有仓位的品种数量。

tradingCount:表示当前正在进行交易的品种数量。

接下来对不同品种的海龟交易策略控制对象的数组tts进行操作。首先调用每个合约的海龟管理对象的Poll方法,进行交易的操作;然后调用对象的Status()方法,获取该对象的当前交易状态,并将其赋值给变量d。

如果当前对象有持仓,就会给空对象vmStatus,将 d.symbol 对应的值赋给 vmStatus 对象中的对应属性,接着持有的合约品种数量holdSymbol也会累计。

如果变量d的isTrading为真,那么将tradingCount自增1。

接着分别使用状态的信息更新tblStatus和tblMarket表格数据,

这里是累加所有品种的持仓数量的绝对值,用来计算总持仓数量totalHold。

调用tts[i]对象的PlotRecords()方法,将不同合约的图表数据进行添加和更新。

为了展示策略的运行效率,我们计算了轮询的间隔elapsed,使用轮询后的时间now减去轮询前的时间ts。

下面是更新资产信息表格tblAssets。
如果存在正在交易的品种(tradingCount > 0)或者nowAccount变量为空(即第一次循环),就会调用_q.GetAccount(exchange, true)获取账户的资产信息,并将结果赋值给tblAssets变量。

然后调用_q.Account()获取当前账户的可用资金,并将结果赋值给nowAccount变量。

这里我们调整了一下tblAssets,如果获取的表格的行数大于10,那么设置索引0的行数为初始资金信息,否则的话,在图表开始的位置插入当前可用资产,和初始的资产。

接下来我们进行"手动恢复字符串"的设置。如果存在持仓(totalHold > 0),将"手动恢复字符串"行信息添加到tblAssets表格中。该行包含不同持仓品种的持仓状态信息,通过JavaScriptON.stringify(vmStatus)将其转换为字符串,并设置colspan为2,用来跨越两列。如果我们选择手动恢复模式,可以使用这里的字符串。

我们整理状态栏的数据为lastStatus变量,包括三个图表。还有一行信息显示当前策略执行情况和状态(包括轮询耗时,当前时间,持有交易品种holdSymbol,和交易的任务数量)。使用LogStatus进行状态栏展示。

最后pushtaskpoll()函数大家不要遗忘。

在回测系统里,使用这五个品种我们模拟运行了多品种的海龟策略。我们看到持仓信息,运行状态和账户信息三个图表,和最后一行的策略状态信息的实时概览。另外,还有收益曲线的展示和各个合约的实时k线图,止损价和离市价的数据展示,这些图表和信息提供了全方位和多维度的视角,帮助我们深入了解策略的运行状态、资金情况和收益表现,方便我们进行策略优化和调整。

在本节课中,我们继续介绍策略主函数的设置和操作执行,更清晰地了解整个策略的运行逻辑。此外,我们还使用图表展示交易进度和账户资产信息等,使得策略的运行过程更加直观。通过这些完善的设计,希望能够有效地帮助大家理解多品种海龟交易策略。

海龟交易逻辑并不复杂,但是怎样将这种逻辑使用程序的语言一步步搭建起来,确实考验我们的能力和耐心,我们要做好各个品种,各个交易环节,程序的容错以及结果呈现等一系列的工作,希望通过这三节课的学习,可以帮助大家了解一下一个真正的实盘级别的大模型应该怎样搭建。当然,这对比于一个工业级的实盘大模型确实比较稚嫩,这里重要的是,理解并掌握这里的程序设计,学会一个复杂模型的搭建方式,希望大家都有所领会。

相关推荐
评论
全部评论 (0)
暂无数据
暂无数据
  • 1
iPhone 下载
社区
回测系统
© 2015 - ∞ YouQuant 豫ICP备19046564号