资源加载中... loading...

使用JavaScript入门商品期货量化交易

Author: 雨幕(youquant), Created: 2023-08-15 17:04:31, Updated: 2024-09-12 09:04:45

[TOC]

img

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

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语言将会报错。

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

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”或者‘优宽量化’等。

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

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

对象

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

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的数组可以存放任何数据类型,

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

运算符

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语言的优先级也是如此,如下面代码所示:

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

function add(numl, num2){
    return numl+num2;
}
add(1,2);

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

条件语句

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

if 语句

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

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

if…else 语句

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

// 语法
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语句是一种流程控制语句,它可以根据不同的条件执行不同的代码块。它的基本语法如下:

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

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

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 循环是很方便的,如下面代码所示:

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

While循环

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

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 线数组。

/*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语句可以中断某一次循环,然后继续下一次循环。 如下面代码所示:

// 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中,作用域分为全局作用域和函数作用域。

全局作用域

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

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

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

局部作用域

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

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

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

作用域链

当在函数内访问一个变量时,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”。

注释

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

// 单行注释

/*
*  多行注释
*/

语句

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

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线数据,看看是什么样子的。

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 线数据输出到日志中,以便于开发者进行调试和分析。

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

时间: 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循环。

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线柱周期还没有完成)。

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级别数据的策略来说,是真实记录的数据,并非模拟生成,所以使用实盘级别回测更贴近真实。

我们使用代码尝试一下:

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需要大于一分钟的策略周期。

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线数据。
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()函数获取当前合约对应的市场当前行情。

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()可以用来获取当前合约对应的市场的订单薄数据。

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++语言。

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库。

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函数指标,我们进行一下代码的编写。

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 指标的示例代码:

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

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

更多内容