复盘侠QScript编程入门

从零开始学习QScript编程语言

概述

QScript语言拥有如下特点:

编程基础

数据类型

QScript语言包含12种数据类型:

名称 说明
Numeric 数值型
Integer 整型
NumericSeries 数值序列
NumericRef 数值引用
String 字符串
StringSeries 字符串序列
StringRef 字符串引用
Bool 布尔型
BoolSeries 布尔型序列
BoolRef 布尔型引用
NumericArray 数组型
NumericArrayRef 数组型引用

其中,Numeric, String, Bool, Integer, NumericArray为基本类型。其他7种数据类型为扩展类型。

策略指标和用户函数

QScript的代码包括策略指标和用户函数两种类型。

策略指标可以被加载到一个图表中运行。每当合约价格发生变化,或一根K线走完,指标的代码就会被执行一次。

用户函数是一行或者多行代码集合,它实现了一个相对完整的功能。例如Average函数实现了对一个价格数组求移动平均的功能。函数不能被加载到图表中运行,只能在代码中被调用。

参数

参数是一个预先声明的变量,用来存放输入参数的值,在声明之后,您就可以在接下来的程序中使用该参数的名称来引用其值。参数的好处在于您可以在调用公式应用的时候才指定相应的参数,而不需要重新编译。

指标参数

在指标中,只能使用Numeric,Integer,String,Bool四种基本数据类型作为参数。每个参数都必须设置初始值。在程序中不能修改参数的值。

指标的程序示例如下:

Params Numeric P1(10.5); Integer P2(20); String P3("hello"); Bool P4(True); Vars ... ...

上述指标在运行前,会弹出参数设置对话框,可以在对话框中修改参数的值。

函数参数

函数可以使用任意一种数据类型作为参数。函数参数的初始值可以设置,也可以不设置。

函数的参数可以在代码中被引用,若希望调用该函数的上层代码对应的变量在调用函数结束后也得到修改,可以使用运用数据类型(XXXXRef)。

示例程序如下:

//Myfun函数代码 Params NumericRef P1; //函数调用后,上层代码对应的变量值会被修改 Numeric P2(5.2); Begin P1 = 100; P2 = 6.5; End

以下是调用函数Myfun的示例代码:

//调用MyFun的代码片断 Numeric a; Numeric b; ... ... a=10; b=20; Myfun(a,b); //调用函数后a变为100,而b仍然是20 ... ...

函数的参数定义还需要注意以下两点:

//Function的代码片断 Params Numeric P1(3); Numeric P2(5); Numeric P3; Numeric P4(10); Begin ... ... ... ... End //以上代码,由于P3没有设置初始值,所以P1,P2的初始值也无效。等价于下边的代码 Params Numeric P1; Numeric P2; Numeric P3; Numeric P4(10); Begin ... ... ... ... End

变量

变量是一个存储值的地址,当变量被声明之后,就可以在脚本中使用变量,可以对其赋值,也可以在其他地方引用变量的值进行计算,要对变量进行操作,直接使用变量名称即可。

变量的主要用处在于它可以存放计算或比较的结果,以方便在之后的代码中直接引用运算的值,而无需重现计算过程。

变量分为全局变量和局部变量两种。全局变量在公式被加载起将一直存在,直到公式被停止运行。局部变量在每次代码运行结束都会被自动销毁,下次运行代码时会被重新赋予初始值。

全局变量需要定义在GlobalVars区域;局部变量需要定义在Vars区域;为方便起见,除了序列变量外,其他变量都可以定义在以Begin开头的程序体内部。定义在Begin区域和定义在Vars区域意义是一样的。但序列变量只能定义在GlobalVars或Vars区域中。

只有交易指令才有GlobalVars区域,函数中不能定义GlobalVars。

引用类型(NumericRef,StringRef,BoolRef)不能定义为变量,只能定义为参数。

变量的定义示例如下:

GlobalVars Bool G_1; String G_2("Hello"); NumericSeries G_3(1); Params ... ... Vars Numeric P1; //不设置初始值,默认值为0 Numeric P2(10); //设置初始值示例,P2初始值为10 Bool P3(true); //P3初始值为true NumericSeries P4; //不设置初始值,P4为"" String P5("hello"); //P5为"hello" Begin Numeric tmp; Bool tmp2; String tmpString = "Hello,World"; End

运算

运算包括算术运算,关系运算,逻辑运算。进行运算时,扩展数据类型和基础数据类型没有任何区别。

数值型变量、字符串变量可以进行算术运算和关系运算,不能进行逻辑运算。

布尔型变量不能进行算术运算和关系运算,只能进行逻辑运算。

算术运算

数值型变量

数值型变量可以进行加、减、乘、除、求余数、乘方运算。下面列表中为数值型运算符:

运算符 描述
+ 两数相加
- 两数相减
* 两数相乘
/ 两数相除
% 求余数

示例代码如下:

Numeric a=5; Numeric b=6.5; Numeric c=20; a = b+c; a = a+3; b=(b+a-100)/5
字符串变量

两个字符串之间可以做相加运算。运算结果返回一个将两个源串连接在一起的字符串(源串并没发生变化)。示例如下:

String a="Hello"; String b="World"; String c=a+" "; //"Hello " c = c+b; //"Hello World" c=c+"!"; //"Hello World!"

关系运算

关系运算值对运算符两端的数据进行比较,关系运算符列表如下:

符号 说明
== 相等
>, >= 大于,大于等于
<, <= 小于,小于等于
!= 不等

字符串变量只能进行"=="和"!="运算。其意义是判断两个字符串内容是否相同,不区分大小写。关系运算的示例如下:

Bool A=(3+5) > (9+2); //A为false Bool B= (9>=9); //B为true Bool C = (9 != 9) //C为false Bool d=("abc" == "ABC") //d为true Bool e=("abc" == "ABCD") //e为false

逻辑运算

只有Bool型变量才可以进行逻辑运算。逻辑运算符列表如下:

符号 说明
&& (AND) 与运算,A&&B,AB同为true时返回true,否则返回false
|| (OR) 或运算,A||B,AB都为false则返回false,否则返回true
! (NOT) 非运算,!A,若A为true,返回false;若A为false,返回true。

语句

分支语句

采用if,else实现分支语句。用于根据一定的条件执行一段代码。若分支条件后边只有一行语句,可以不使用大括号;若需要控制多行语句,必须使用大括号。else语句可以省略。

示例如下:

Bool A=3>5; Numeric B=3; if(A) //若A为true,则设置B为5,否则,设置B为6 B=5; //满足条件时执行1条语句,可以不写大括号 else B=6; if(!A) //若A不为true { B= B+1; B+=5; } else { B=B-1; B-=2; } if(B>1) { A=true; } else { A=false; }

循环语句

循环语句用于循环执行某一段代码。若循环执行的代码段仅有1行,可以省略大括号。使用示例如下:

Numeric A=0; Numeric B=2; For A=0 to 10 { B=B+1; //这个代码总共会执行11次,最终结果:B为13,A为11 Print(A); //A依次为0,1,2,...10 } B=5; For A=10 downto 8 { B=B-2 ; //代码共执行了3次。最终结果:B为-1,A为7 } //也支持c语言的循环语句写法 for(int i=0;i<10;i++) { Print(Text(i)); }
注意:编写循环语句不当,会导致程序一只运行循环代码,永远不会退出。导致整个系统僵死。这种情况叫作死循环。
Numeric A=5; For A=5 to 10 { A=8; }

分析上述代码,发现A始终为8,永远不会大于10,因此会一直循环下去。在编写循环语句的时候应当特别注意保证不存在死循环。

除了使用For语句编写循环外,还可以使用While语句,示例代码如下:

Numeric a=10; while(a>0) //如果a大于0,就执行循环,否则退出循环 { a=a-1; }

调用函数

函数MyFun代码如下:

Params Numeric A; Numeric B; Begin return A+B; End

调用示例如下:

Number A; Numeric B; A = MyFun(3,5); //A=8 B = MyFun(A,10); //B=18 A = MyFun(A,A); //A=16
包含缺省参数的函数调用

某些函数的参数包含缺省值,若想使用参数的缺省值进行运算,可以省略输入该参数,但需要注意的是:如果省略输入1个缺省参数,该参数后边的所有参数也必须省略:

Params Numeric A; Numeric B(5); Numeric C(6); Begin return A+B+C; End

调用示例如下:

Number A; A = MyFun(8); //相当于8+5+6=19 A = MyFun(10,12); //相当于10+12+6=28 A=MyFun(10, ,20); //错误的调用方式,编译通不过,应当写成A=MyFun(10,5,20);

如果一个函数,没有参数,调用的时候,可以不写小括号。比如:CurrentBar()和CurrentBar,是一样的。

包含引用类型参数的函数调用

一些函数的参数为Ref数据类型,我们称这些参数为引用参数。使用引用参数的好处是可以在函数内部修改该参数对应的变量。输入的引用参数必须是一个变量,而不能是一个数值,或字符串。

Params Numeric A; NumericRef B; StringRef C; Begin A=7; B=8; C="Fine,Thanks!"; Return A+B; End

调用示例如下:

Numeric P1=1; Numeric P2=2; String P3="How are you?"; Numeric P4; P4 = MyFun(P1,P2,P3); //调用结果: //P4=15; //P1仍然为1,因为MyFun的第一个参数不是引用参数,即使函数将其修改为7,函数返回时,P1仍然为1。 //P2=8,因为MyFun的第二个参数为引用参数。 //P3为"Fine,Thanks!"

序列变量和序列函数

系统提供三个序列变量类型:NumericSeries,StringSeries,BoolSeries

序列变量完全拥有三个基本变量(Numeric,String,Bool)的功能。同时,序列变量还可以用于获取以前周期的值,这称为对序列变量进行回溯。

如:内置变量Close就是一个序列变量。

Numeric A=Close; 表示获取当前周期的收盘价,也可以写成 Numeric A=Close[0]

Numeric B=Close[1]; 表示获取上一周期的收盘价

Numeric C=Close[3]; 表示获取三周期前的收盘价

如果需要对某个计算结果进行回溯访问,就将该变量定义为序列变量,但需要注意,序列变量由于需要保存历史数据,因此比普通变量要占用更多的内存空间。

如果在一个函数的参数或变量中定义了序列变量,我们称这个函数为序列函数。序列函数中保存了每一周期的计算结果。由于序列函数可能被多个主函数多次调用,若使用不当,会导致序列函数中的历史数据发生混乱。为避免这个问题,使用序列函数必须遵循一些规则,具体如下:

数组

为方便用户实现对跨周期历史数据的获取,系统提供了数组类型NumericArray和数组引用类型NumericArrayRef。

数组的使用例子如下:

Params Vars NumericArray arr; //注意,数组不能设置初始值 Begin ArrAdd(arr,3); //添加一个数据,arr中的数据{3} ArrAdd(arr,15.6); //添加一个数据,arr中的数据{3, 15.6} ArrRevers(arr); //反转数组,arr中的数据{15.6, 3} arr[1]=5; //修改一个元素的值,arr中的数据{15.6, 5} ArrClear(arr); //arr被清空 //获取周线的收盘价历史数据 arr = HisData(Enum_Data_Close,Enum_Period_Week); if(ArrLength(arr)>1) //判断是否成功获取到了周线收盘价历史数据 { Print(arr[0]); //打印当前时刻的周线价格 Print(arr[1]); //打印上周的周线价格 } End

如上例所示,数组和序列变量都可以进行历史数据的回溯。但他们有着本质的区别,具体表现在:

常见问题及编程技巧

序列函数的使用需要注意哪些问题?

请尽量避免在分支或循环语句中使用序列函数。如果确有必要这样使用,请确保以下两点:

1.尽量保证序列函数在每个周期被调用的次数都相同。

2.一定要保证序列函数在每个周期被调用的顺序是相同的。

如下例,在N周期,先计算10周期均线,后计算20周期均线;在N+1周期,则先计算20周期均线,后计算10周期均线。这将导致XAverage中的序列变量无法正确取得上一周期的运算结果(N+1周期计算20日均线时,错误地将N周期的10日均线运算结果读取出来进行计算。

Param Var NumericSeries A; //10周期均线 NumericSeries B; //20周期均线 Bool Con(false); Begin if(B) { A=XAverage(Close,10); B=XAverage(Close,20); } else { B=XAverage(Close,20); A=XAverage(Close,10); } Con=!Con; PlotNumeric("MA10",A); PlotNumeric("MA20",B); End

序列变量是程序化交易编程语言与一般编程语言(如:C++)的一个显著差别,他是为了简化金融统计运算(如:计算移动平均)而设计实现的。

序列变量(如:NumericSeries)拥有数据回溯的功能,它可以用中括号的方式访问以前K线的数据,如:High表示本周期的最高价,High[1]就表示上一根K线的最高价。

为了让序列变量拥有回溯功能,我们在后台做了这样的工作:

1.用户代码运行之前,将序列变量从一个存储空间里取出来。并让序列变量的初始值等于上一周期的值。
2.用户代码运行结束后,将序列变量保存到存储空间。
3.下次运行时,如果发现是一根新生成的K线,则将序列变量的长度自动增加1。新增加的元素值等于上一周期的值。

由此可见,序列变量是一个长度自动变化的数组,他的长度始终和图表中K线的数量是一致的。下面是使用序列变量求指数平均(EMA)的函数源代码:

//指数平均得计算公式为: //当前周期的指数平均值=平滑系数*(当前价格-上周期指数平均值)+上周期指数平均值 //平滑系数=2/(周期单位+1) Params Numeric Price(10); //价格 Numeric Length(10); //周期单位 Vars Numeric sFcactor; //平滑系数 NumericSeries EMAValue; //当前周期的指数平均值,定义为序列变量。 Begin sFcactor = 2 / ( Length + 1 ); //求平滑系数 if (CurrentBar == 0 ) { //如果当前是第一根K线,也就是说没有上一周期的EMA值,则直接让他等于当前价格 EMAValue = Price; }else { //当前周期的指数平均值=平滑系数*(当前价格-上周期指数平均值)+上周期指数平均值 EMAValue = sFcactor * ( Price - EMAValue[1] ) + EMAValue[1]; } Return EMAValue; End

跨周期跨合约历史数据获取

系统提供了HisData函数供用户方便获取跨周期、跨合约的历史数据。

HisData函数的返回值为一个数组(NumericArray)。数组中的数据按照时间由近到远的顺序存储。

数组与序列变量有着本质的区别。序列变量必须依赖图表,而数组完全脱离了图表。

可以使用内置的"数组函数"对数组进行操作,具体使用方法详见函数手册。

不能将一个数组作为函数的参数传给一个序列变量,如不能使用Average函数计算数组的均值。而应当使用iMA函数计算。

跨周期示例如下:

Params String Kind("ZCE CF 205"); //品种名称 Vars NumericArray arr1; NumericArray arr2; Begin arr1=HisData(Enum_Data_Close,Enum_Period_Min15,Kind); //取15分钟收盘价 arr2=HisData(Enum_Data_Open,Enum_Period_Day,Kind); //取日线的上周期开盘价 if(ArrLength(arr1)==0) { Print("暂未获取到15分钟数据"); } Else { Numeric MA1=iMA(arr1); //求15分钟线的当前MA值 Print(Kind+"的15分钟线当前MA值为:"+Text(MA1)); } if(ArrLength(arr2)==0) { Print("暂未获取到日线数据"); } Else { Numeric MA2=iMA(arr2,20,1); //求日线前一周期的MA值 Print(Kind+"的日线前一周期MA值为:"+Text(MA2)); } End

时间判断

QScript语言采用浮点数来表示时间,如2012年12月31日23时59分59秒,表示为20121231.235959

对于任何计算机语言,存储浮点数都会产生误差,也就是说,计算机不能精确地存储20121231.235959,可能将该时间存储为20121231.235959001。

ETL语言中,时间可以精确到小数点后8位。为此,判断两个时间是否相等,请不要用运算符 "=="。

示例:判断当前时间是否为14:59:59

错误写法:
if(CurrentTime == 0.145959) { .... }
由于CurrentTime返回的时间只能精确到小数点后8位,不能和0.235959000000000绝对相等,所以if语句可能得不到执行

正确写法:
if(CurrentTime >0.145958 && CurrentTime<0.15) { .... }
当前时间大于14:59:58 并且小于15:00:00

推荐写法:采用函数 TimeDiff
TimeDiff:返回两个时间相差的秒数,该返回值为整数,顾可以使用==进行判断
if(TimeDff(CurrentTime,0.145959)==0) { .... }

怎样在编写用户函数时调用另一个用户函数?

不能在用户函数中直接调用其他用户函数。如果确实需要这样做,请将另一个用户函数转化成内建函数。需要注意的是,一旦转化为内建函数,这个函数就再也不能修改或删除。

为什么我编写了一个用户函数,但是在交易指令中无法调用?

可能是由于这个用户函数仅仅保存了,但是没有被编译生成。在编写或修改了函数以后,需要对函数进行重新生成。否则用户函数将不能真正得到更新,也就无法在交易指令中被调用。

在编写用户函数时,怎样让函数返回多个值?

函数可以用return语句返回一个值,如果需要返回多个值,请定义一些引用参数(如NumericRef),利用这些参数返回需要的值。例如:

Params NumericRef a; StringRef b; Vars Begin a = 5; b = "hello,world"; return 0; End