发明者量化PINE语言入门教程

Author: 小小梦, Created: 2022-05-30 16:23:43, Updated: 2022-09-28 17:10:21

讲解例子:

var lineColor = na

n = if bar_index > 10 and bar_index <= 20
    lineColor := color.green
else if bar_index > 20 and bar_index <= 30
    lineColor := color.blue
else if bar_index > 30 and bar_index <= 40
    lineColor := color.orange
else if bar_index > 40
    lineColor := color.black
else 
    lineColor := color.red
    
plot(close, title="close", color=n, linewidth=5, overlay=true)
plotchar(true, title="bar_index", char=str.tostring(bar_index), location=location.abovebar, color=color.red, overlay=true)

重点:判断用的表达式,返回布尔值。注意缩进。最多只能有一个else分支。所有分支表达式都不为真,也没有else分支,则返回na。

x = if close > open
    close
plot(x, title="x")

由于当K线BAR为阴线时,即close < open时,if语句后的表达式为假(false),则不执行if的本地代码块。这个时候也没有else分支,if语句就返回了na。x被赋值为na。在画图上就无法画出这个点,我们通过回测画图也可以观察到。

switch语句

switch语句也是一种分支结构的语句,用来设计根据某些条件执行不同的路径。switch语句一般来说有以下几个关键知识点。

1、switch语句和if语句一样,也可以返回值。 2、和其它语言中的switch语句不一样,执行switch结构时,只执行其代码中的一个本地块,所以break声明是不必要的(即不需要写break之类的关键字)。 3、switch的每个分支都可以写一个本地代码块,这个本地代码块的最后一行即为返回值(它可以是一个值的元组)。如果没有任何分支被的本地代码块被执行,则返回na。 4、switch结构中的表达式判断位置,可以写字符串、变量、表达式或函数调用。 5、switch允许指定一个返回值,该值作为结构中没有其它情况执行时使用的默认值。

switch分为两种形式,我们逐一来看例子,了解其用法。

1、带有表达式的 switch ,例子讲解:

// input.string: defval, title, options, tooltip
func = input.string("EMA", title="指标名称", tooltip="选择要使用的指标函数名称", options=["EMA", "SMA", "RMA", "WMA"])

// input.int: defval, title, options, tooltip
// param1 = input.int(10, title="周期参数")
fastPeriod = input.int(10, title="快线周期参数", options=[5, 10, 20])
slowPeriod = input.int(20, title="慢线周期参数", options=[20, 25, 30])

data = input(close, title="数据", tooltip="选择使用收盘价、开盘价、最高价...")
fastColor = color.red
slowColor = color.red

[fast, slow] = switch func
    "EMA" =>
        fastLine = ta.ema(data, fastPeriod)
        slowLine = ta.ema(data, slowPeriod)
        fastColor := color.red
        slowColor := color.red
        [fastLine, slowLine]
    "SMA" =>
        fastLine = ta.sma(data, fastPeriod)
        slowLine = ta.sma(data, slowPeriod)
        fastColor := color.green
        slowColor := color.green
        [fastLine, slowLine]
    "RMA" =>
        fastLine = ta.rma(data, fastPeriod)
        slowLine = ta.rma(data, slowPeriod)
        fastColor := color.blue
        slowColor := color.blue
        [fastLine, slowLine]
    =>
        runtime.error("error")
        
plot(fast, title="fast" + fastPeriod, color=fastColor, overlay=true)
plot(slow, title="slow" + slowPeriod, color=slowColor, overlay=true)

之前我们学习了input函数,这里我们继续学习两个和input类似的函数:input.stringinput.int函数。 input.string用来返回字符串,input.int函数用来返回整型数值。例子中其实就新增了一个options参数的用法,options参数可以传入一个可选值组成的数组。例如例子中的options=["EMA", "SMA", "RMA", "WMA"]options=[5, 10, 20](注意一个是字符串类型,一个是数值类型)。这样策略界面上的控件就不是需要输入具体数值了,而是控件变为下拉框,选择options参数中提供的这些选项。

变量func的值就为一个字符串,变量func作为switch的表达式(可以是变量、函数调用、表达式),来确定执行switch中的哪个分支。如果变量func无法和switch中的任一个分支上的表达式匹配(即相等),则执行默认的分支代码块,会执行runtime.error("error")函数导致策略抛出异常停止。

我们上面的测试代码中在switch的默认分支代码块的最后一行runtime.error之后,我们并没有加入[na, na]这样的代码来兼容返回值,在trading view上是需要考虑该问题的,如果类型不一致会报错。但是在FMZ上由于没有严格要求类型,所以是可以省略这种兼容代码的。所以在FMZ上不用考虑if、switch分支返回值的类型兼容问题。

strategy("test", overlay=true)
x = if close > open
    close
else
    "open"
plotchar(true, title="x", char=str.tostring(x), location=location.abovebar, color=color.red)

在FMZ上不会报错,在trading view上会报错。因为if分支返回的类型不一致。

2、没有表达式的switch

接下来我们看switch的另一种使用方式,即不带表达式的写法。

up = close > open     // up = close < open 
down = close < open 
var upOfCount = 0 
var downOfCount = 0 

msgColor = switch
    up  => 
        upOfCount += 1 
        color.green 
    down => 
        downOfCount += 1
        color.red

plotchar(up, title="up", char=str.tostring(upOfCount), location=location.abovebar, color=msgColor, overlay=true)
plotchar(down, title="down", char=str.tostring(downOfCount), location=location.belowbar, color=msgColor, overlay=true)

测试代码例子就可以看到,switch会匹配执行分支条件上为真的本地代码块。一般来说switch语句之后的分支条件必须是互斥的。就是说例子中up和down不能同时为true。因为switch只能执行一个分支的本地代码块,有兴趣可以把代码中这行语句:up = close > open // up = close < open更换成注释里的内容,回测观察下结果。会发现switch分支只能执行第一个分支。除此之外还需要注意尽量不要把函数调用写在switch的分支中,函数无法在每个BAR上被调用可能引起一些数据计算的问题(除非如同「带有表达式的 switch」例子中,执行分支是确定的,在策略运行中是不会被更改的)。

循环结构

for语句

返回值 = for 计数 = 起始计数 to 最终计数 by 步长
    语句                                            // 注释:语句里可以有break,continue
    语句                                            // 注释:最后一条语句为返回值

for语句使用非常简单,for循环可以最终返回一个值(或者返回多个值,以[a, b, c]这样的形式)。如同以上伪代码中赋值给「返回值」位置的变量。for语句之后跟随一个「计数」变量用于控制循环次数、引用其它值等。「计数」变量在循环开始之前被赋值为「初始计数」,然后根据「步长」设置递增,当「计数」变量大于「最终计数」时循环停止。

for循环中使用的break关键字:当执行了break语句后,循环就停止了。 for循环中使用的continue关键字:当执行了continue语句后,循环会忽略continue之后的代码,直接执行下一轮循环。for语句返回最后一次循环执行时的返回值。如果没有任何代码执行则返回空值。

下面我们用一个简单例子来展示:

ret = for i = 0 to 10       // 可以增加by关键字修改步长,暂时FMZ不支持 i = 10 to 0 这样的反向循环
    // 可以增加条件设置,使用continue跳过,break跳出
    runtime.log("i:", i)
    i                       // 如果这行不写,就返回空值,因为没有可返回的变量
    
runtime.log("ret:", ret)
runtime.error("stop")

for … in 语句

for ... in语句有两种形式,以下面的伪代码来说明。

返回值 = for 数组元素 in 数组 
    语句                        // 注释:语句里可以有break,continue
    语句                        // 注释:最后一条语句为返回值
返回值 = for [索引变量, 索引变量对应的数组元素] in 数组
    语句                        // 注释:语句里可以有break,continue
    语句                        // 注释:最后一条语句为返回值 

可以看到两种形式的主要差别就在于for关键字之后跟随的内容,一种是使用一个变量作为引用数组元素的变量。一种是使用一个包含索引变量,数组元素变量的元组的结构来引用。其它的返回值规则,使用break、continue等规则和for循环一致。我们也通过一个简单的例子来说明使用。

testArray = array.from(10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
for ele in testArray            // 修改成 [i, ele]的形式:for [i, ele] in testArray , runtime.log("ele:", ele, ", i:", i)
    runtime.log("ele:", ele)

runtime.error("stop")

当需要使用索引时,就使用for [i, ele] in testArray的写法。

for循环应用

当可以使用Pine语言提供的内置函数完成一些循环逻辑计算时,可以使用循环结构直接编写,也可以使用内置函数处理。我们举两个例子。

1、计算均值

使用循环结构设计时:

length = 5
var a = array.new(length)
array.push(a, close)

if array.size(a) >= length
	array.remove(a, 0)
	
sum = 0 	
for ele in a
    sum += ele 

avg = sum / length
plot(avg, title="avg", overlay=true)

例子中使用了for循环求和,然后计算均值。

直接使用内置函数计算均线:

plot(ta.sma(close, length), title="ta.sma", overlay=true)

直接使用内置函数ta.sma,计算均线指标,显然对于计算均线使用内置函数更加简单。在图表上对比可以看到计算出的结果完全一致。

2、求和

还是使用上面的例子来说明。

使用循环结构设计时:

length = 5
var a = array.new(length)
array.push(a, close)

if array.size(a) >= length
	array.remove(a, 0)
	
sum = 0 	
for ele in a
    sum += ele 

avg = sum / length
plot(avg, title="avg", overlay=true)
plot(ta.sma(close, length), title="ta.sma", overlay=true)

对于计算数组所有的元素的和可以使用循环来处理,也可以使用内置函数array.sum来计算。 直接使用内置函数计算求和:

length = 5
var a = array.new(length)
array.push(a, close)

if array.size(a) >= length
	array.remove(a, 0)
	
plot(array.sum(a) / length, title="avg", overlay=true)
plot(ta.sma(close, length), title="ta.sma", overlay=true)

可以看到算出的数据,使用plot画在图表上显示完全一致。

那既然用内置函数就可以完成这些工作,为什么还要设计循环?使用循环主要是基于这3点的应用: 1、对于数组的一些操作、计算。 2、回顾历史,例如,找出有多少过去的高点高于当前BAR的高点。由于当前BAR的高 点仅在脚本运行的BAR上已知,因此需要一个循环来及时返回并分析过去的BAR。 3、使用Pine语言的内置函数无法完成对过去BAR的计算的情况。

while 语句

while语句让循环部分的代码一直执行,直到while结构中的判断条件为假(false)。

返回值 = while 判断条件
    语句                    // 注释:语句里可以有break,continue
    语句                    // 注释:最后一条语句为返回值

while的其它规则和for循环类似,循环体本地代码块最后一行是返回值,可以返回多个值。当「循环条件」为真时执行循环,条件为假时停止循环。循环体中也可以使用break、continue语句。

我还是用计算均线的例子来演示:

length = 10

sma(data, length) => 
    i = 0 
    sum = 0 
    while i < 10 
        sum += data[i]
        i += 1
        sum / length

plot(sma(close, length), title="sma", overlay=true)
plot(ta.sma(close, length), title="ta.sma", overlay=true)

可以看到while循环使用也是非常简单的,还可以设计一些计算逻辑是无法用内置函数代替的,例如计算阶乘:

counter = 5
fact = 1

ret = while counter > 0
    fact := fact * counter
    counter := counter - 1
    fact

plot(ret, title="ret")  // ret = 5 * 4 * 3 * 2 * 1

数组

Pine语言中的数组和其它编程语言中的数组定义类似,Pine的数组是一维数组。通常用来储存连续的一系列数据。数组其中储存的单个数据叫做数组的元素,这些元素的类型可以是:整型、浮点型、字符串、颜色值、布尔值。FMZ上的Pine语言并不十分严格要求类型,甚至可以一个数组中同时储存字符串和数值。由于数组的底层也是系列结构,如果使用历史运算符引用的是前一个BAR上的数组状态。所以引用数组中的某个元素时不使用历史操作符[]而是需要使用array.get()array.set()函数。数组中元素的索引顺序为数组第一个元素的索引为0,下一个元素的索引递增1。

我们用一个简单的代码来说明:

var a = array.from(0)
if bar_index == 0 
    runtime.log("当前BAR上的a值:", a, ", 上1根BAR上的a,即a[1]值:", a[1])
else if bar_index == 1 
    array.push(a, bar_index)
    runtime.log("当前BAR上的a值:", a, ", 上1根BAR上的a,即a[1]值:", a[1])
else if bar_index == 2
    array.push(a, bar_index)
    runtime.log("当前BAR上的a值:", a, ", 上1根BAR上的a,即a[1]值:", a[1], ", 向前数2根BAR上的a,即a[2]值:", a[2])
else if bar_index == 3 
    array.push(a, bar_index)
    runtime.log("当前BAR上的a值:", a, ", 上1根BAR上的a,即a[1]值:", a[1], ", 向前数2根BAR上的a,即a[2]值:", a[2], ", 向前数3根BAR上的a,即a[3]值:", a[3])
else if bar_index == 4 
    // 使用array.get 按索引获取元素,使用array.set按索引修改元素
    runtime.log("数组修改前:", array.get(a, 0), array.get(a, 1), array.get(a, 2), array.get(a, 3))
    array.set(a, 1, 999)
    runtime.log("数组修改后:", array.get(a, 0), array.get(a, 1), array.get(a, 2), array.get(a, 3))

声明数组

使用array<int> afloat[] b声明数组或者只声明一个变量都可以被赋值数组,例如:

array<int> a = array.new(3, bar_index)
float[] b = array.new(3, close)
c = array.from("hello", "fmz", "!")
runtime.log("a:", a)
runtime.log("b:", b)
runtime.log("c:", c)
runtime.error("stop")

给数组变量初始化一般使用array.newarray.from函数。Pine语言中还有很多和类型相关的与array.new类似的函数:array.new_int()array.new_bool()array.new_color()array.new_string()等。

var关键字也可以作用与数组的声明模式,使用var关键字声明的数组仅仅在第一根BAR上被初始化。我们通过一个例子来观察:

var a = array.from(0)
b = array.from(0)

if bar_index == 1
    array.push(a, bar_index)
    array.push(b, bar_index)
else if bar_index == 2 
    array.push(a, bar_index)
    array.push(b, bar_index)
else if barstate.islast
    runtime.log("a:", a)
    runtime.log("b:", b)
    runtime.error("stop")

可以看到a数组的变动都持续确定了下来,没有被重置过。b数组则在每个BAR上都被初始化。最终在barstate.islast为真时打印的时候仍然只有一个元素,数值0。

读取、写入数组中的元素

使用array.get获取数组中指定索引位置的元素,使用array.set修改数组中指定索引位置的元素。

array.get的第一个参数为要处理的数组,第二个参数为指定的索引。 array.set的第一个参数为要处理的数组,第二个参数为指定的索引,第三个参数为要写入的元素。

使用以下这个简单的例子来说明:

lookbackInput = input.int(100)
FILL_COLOR = color.green

var fillColors = array.new(5)
if barstate.isfirst
    array.set(fillColors, 0, color.new(FILL_COLOR, 70))
    array.set(fillColors, 1, color.new(FILL_COLOR, 75))
    array.set(fillColors, 2, color.new(FILL_COLOR, 80))
    array.set(fillColors, 3, color.new(FILL_COLOR, 85))
    array.set(fillColors, 4, color.new(FILL_COLOR, 90))

lastHiBar = - ta.highestbars(high, lookbackInput)
fillNo = math.min(lastHiBar / (lookbackInput / 5), 4)

bgcolor(array.get(fillColors, int(fillNo)), overlay=true)
plot(lastHiBar, title="lastHiBar")
plot(fillNo, title="fillNo")

该例子初始化了基础色绿色,声明并初始化了一个数组用来保存颜色,然后对颜色值赋予不同的透明度(使用color.new函数)。通过计算当前BAR距离100个回看周期内high最大值的距离,计算颜色等级。距离最近100回看周期内high的最大值越近,等级越高,对应的颜色值越深(透明度低)。很多类似的策略用这样的方式表示当前价格在N个回看周期内的级别。

遍历数组元素

如何遍历一个数组,我们用到我们之前学习的for/for in/while语句都可以实现。

a = array.from(1, 2, 3, 4, 5, 6)

for i = 0 to (array.size(a) == 0 ? na : array.size(a) - 1)
    array.set(a, i, i)
    
runtime.log(a)
runtime.error("stop")
a = array.from(1, 2, 3, 4, 5, 6)

i = 0
while i < array.size(a)
    array.set(a, i, i)
    i += 1

runtime.log(a)
runtime.error("stop")
a = array.from(1, 2, 3, 4, 5, 6)

for [i, ele] in a 
    array.set(a, i, i)

runtime.log(a)
runtime.error("stop")

这三种遍历方式执行结果相同。

数组可以在脚本的全局范围内声明,也可以在函数或if分支的本地范围内声明

历史数据引用

对于数组中元素的使用,以下的方式是等效的,我们通过以下例子可以看到在图表上画出两组线,每组两条,每组的两条线数值是完全相同的。

a = array.new_float(1)
array.set(a, 0, close)
closeA1 = array.get(a, 0)[1]
closeB1 = close[1]
plot(closeA1, "closeA1", color.red, 6)
plot(closeB1, "closeB1", color.black, 2)

ma1 = ta.sma(array.get(a, 0), 20)
ma2 = ta.sma(close, 20)
plot(ma1, "ma1", color.aqua, 6)
plot(ma2, "ma2", color.black, 2)

数组的增加、删除操作函数

1、数组的增加操作相关函数:

array.unshift()array.insert()array.push()

2、数组的删除操作相关函数:

array.remove()array.shift()array.pop()array.clear()

我们使用以下例子来测试这些数组的增加、删除操作函数。

a = array.from("A", "B", "C")
ret = array.unshift(a, "X")
runtime.log("数组a:", a, ", ret:", ret)

ret := array.insert(a, 1, "Y")
runtime.log("数组a:", a, ", ret:", ret)

ret := array.push(a, "D")
runtime.log("数组a:", a, ", ret:", ret)

ret := array.remove(a, 2)
runtime.log("数组a:", a, ", ret:", ret)

ret := array.shift(a)
runtime.log("数组a:", a, ", ret:", ret)

ret := array.pop(a)
runtime.log("数组a:", a, ", ret:", ret)

ret := array.clear(a)
runtime.log("数组a:", a, ", ret:", ret)

runtime.error("stop")

增加、删除的应用:数组作为队列

使用数组,以及数组的一些增加、删除函数我们可以构造出「队列」数据结构。队列可以用于计算tick价格的移动平均值,可能有的同学会问:“为什么要构造队列结构呢?我们之前用数组不是就可以算平均值吗?”

队列是一种在编程领域经常使用的结构,队列的特点就是:

先进入队列的元素,先出队列。

这样就可以确保队列中存在的数据都是最新的数据,并且队列的长度不会无限膨胀(无限膨胀的代码只能中午的时候写,因为“早”、“晚”会出问题)。

以下例子我们使用一个队列结构记录每次tick价格,计算出tick级别的移动平均价,然后和1分钟K线级别的移动平均线观察对比。

strategy("test", overlay=true)

varip a = array.new_float(0)
var length = 10

if not barstate.ishistory
    array.push(a, close)

    if array.size(a) > length
        array.shift(a)

sum = 0.0
for [index, ele] in a 
    sum += ele

avgPrice = array.size(a) == length ? sum / length : na

plot(avgPrice, title="avgPrice")
plot(ta.sma(close, length), title="ta.sma")

注意,声明a数组时我们指定了声明模式,使用了关键字varip。这样每次价格变动都会被记录在a数组中。

常用的数组计算、操作函数

计算相关函数:

array.avg()求数组中所有元素的平均值、array.min()求数组中最小的元素、array.max()求数组中最大的元素、array.stdev()求数组中所有元素的标准差、array.sum()求数组中所有元素的和。

操作相关函数: array.concat()合并或连接两个数组。 array.copy()复制数组。 array.join将数组中的所有元素连接成一个字符串。 array.sort()按升序或降序排序。 array.reverse()反转数组。 array.slice()对数组进行切片。 array.includes()判断元素。 array.indexof()返回参数传入的值首次出现的索引。如果找不到该值,则返回 -1。 array.lastindexof()找到最后一次出现的值。

数组计算相关函数的测试例子:

a = array.from(3, 2, 1, 4, 5, 6, 7, 8, 9)

runtime.log("数组a的算数平均:", array.avg(a))
runtime.log("数组a中的最小元素:", array.min(a))
runtime.log("数组a中的最大元素:", array.max(a))
runtime.log("数组a中的标准差:", array.stdev(a))
runtime.log("数组a的所有元素总和:", array.sum(a))
runtime.error("stop")

这些都是比较常用的数组计算函数。

操作相关函数的例子:

a = array.from(1, 2, 3, 4, 5, 6)
b = array.from(11, 2, 13, 4, 15, 6)

runtime.log("数组a:", a, ", 数组b:", b)
runtime.log("数组a,数组b连接在一起:", array.concat(a, b))
c = array.copy(b)

runtime.log("复制一个数组b,赋值给变量c,变量c:", c)

runtime.log("使用array.join处理数组c,给每个元素中间增加符号+,连接所有元素结果为字符串:", array.join(c, "+"))
runtime.log("排序数组b,按从小到大顺序,使用参数order.ascending:", array.sort(b, order.ascending))     // array.sort函数修改原数组
runtime.log("排序数组b,按从大到小顺序,使用参数order.descending:", array.sort(b, order.descending))   // array.sort函数修改原数组

runtime.log("数组a:", a, ", 数组b:", b)
array.reverse(a)   // 此函数修改原数组
runtime.log("反转数组a中的所有元素顺序,反转之后数组a为:", a)    

runtime.log("截取数组a,索引0 ~ 索引3,遵循左闭右开区间规则:", array.slice(a, 0, 3))
runtime.log("在数组b中搜索元素11:", array.includes(b, 11))
runtime.log("在数组a中搜索元素100:", array.includes(a, 100))
runtime.log("将数组a和数组b连接,搜索其中第一次出现元素2的索引位置:", array.indexof(array.concat(a, b), 2), " , 参考观察 array.concat(a, b):", array.concat(a, b))
runtime.log("将数组a和数组b连接,搜索其中最后一次出现元素6的索引位置:", array.lastindexof(array.concat(a, b), 6), " , 参考观察 array.concat(a, b):", array.concat(a, b))

runtime.error("stop")

函数

自定义函数

Pine语言可以设计自定义函数,一般来说Pine语言的自定义函数有以下规则:

1、所有函数都在脚本的全局范围内定义。不能在另一个函数中声明一个函数。 2、不允许函数在自己的代码中调用自己(递归)。 3、原则上所有PINE语言内置画图函数( barcolor()、 fill()、 hline()、plot()、 plotbar()、 plotcandle())不能在自定义函数内调用。 4、函数可以写成单行、多行。最后一条语句的返回值为当前函数返回值,返回值可以返回元组形式。

之前的教程内容我们也多次使用过自定义函数,例如设计成单行的自定义函数:

barIsUp() => close > open

该函数返回当前BAR是否为阳线。

设计成多行的自定义函数:

sma(data, length) => 
    i = 0 
    sum = 0 
    while i < 10 
        sum += data[i]
        i += 1
        sum / length

plot(sma(close, length), title="sma", overlay=true)
plot(ta.sma(close, length), title="ta.sma", overlay=true)

我们自己用自定义函数实现的一个sma均线计算的函数。

还有,可以返回两个变量的自定义函数例子:

twoEMA(data, fastPeriod, slowPeriod) =>
    fast = ta.ema(data, fastPeriod)
    slow = ta.ema(data, slowPeriod)
    [fast, slow]

[ema10, ema20] = twoEMA(close, 10, 20)
plot(ema10, title="ema10", overlay=true)
plot(ema20, title="ema20", overlay=true)

一个函数可以计算出快线、慢线,两条EMA均线指标。

内置函数

内置函数可以很方便地在FMZ PINE Script 文档中查询。

Pine语言内置函数分类:

1、字符串处理函数str.系列。 2、颜色值处理函数color.系列。 3、参数输入函数input.系列。 4、指标计算函数ta.系列。 5、画图函数plot.系列。 6、数组处理函数array.系列。 7、交易相关函数strategy.系列。 8、数学运算相关函数math.系列。 9、其它函数(时间处理、非plot系列画图函数、request.系列函数、类型处理函数等)。

交易函数

strategy.系列函数是我们在设计策略中经常用到的函数,这些函数和策略具体运行时执行交易操作息息相关。


1、strategy.entry

strategy.entry函数是我们写策略时比较重要的一个下单函数,该函数比较重要的几个参数为:id, direction, qty, when等。

参数:

  • id:可以理解为给某个交易头寸起个名字用于引用。可以引用这个id撤销、修改订单、平仓。
  • direction:如果下单方向是做多(买入)该参数就传strategy.long这个内置变量,如果要做空(卖出)就传strategy.short这个变量。
  • qty:指定下单的量,如果不传这个参数使用的就是默认下单量。
  • when:执行条件,可以指定这个参数来控制当前这个下单操作是否触发。
  • limit:指定订单限价价格。
  • stop:止损价格。

strategy.entry函数的具体执行细节受strategy函数调用时的参数设置控制,也可以通过「Pine语言交易类库模版参数」设置控制,Pine语言交易类库模版参数控制的交易细节更多,具体可以查看链接的文档。

这里重点讲解一下strategy函数中,pyramidingdefault_qty_value参数。使用以下代码测试:

/*backtest
start: 2022-07-03 00:00:00
end: 2022-07-09 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
*/

strategy(title = "open long example", pyramiding = 3, default_qty_value=0.1, overlay=true)

ema10 = ta.ema(close, 10)

findOrderIdx(idx) =>
    if strategy.opentrades == 0 
        false 
    else 
        ret = false 
        for i = 0 to strategy.opentrades - 1 
            if strategy.opentrades.entry_id(i) == idx
                ret := true 
                break
        ret 
        

if not findOrderIdx("long1")
    strategy.entry("long1", strategy.long)

if not findOrderIdx("long2")
    strategy.entry("long2", strategy.long, 0.2, when = close > ema10)

if not findOrderIdx("long3")
    strategy.entry("long3", strategy.long, 0.2, limit = low[1])
    strategy.entry("long3", strategy.long, 0.3, limit = low[1])

if not findOrderIdx("long4")
    strategy.entry("long4", strategy.long, 0.2)

plot(ema10, title="ema10", color=color.red)

代码开头/*backtest ... */包裹部分为回测设置,是为了记录当时回测设置时间等信息,便于调试,并非策略代码。

代码中:strategy(title = "open long example", pyramiding = 3, default_qty_value=0.1, overlay=true),当我们指定pyramiding参数为3时我们就设置了同一个方向交易最多3次。所以例子中四次strategy.entry下单操作中有一次没有执行。由于我们也指定了default_qty_value参数为0.1,所以ID标识为“long1”的这次strategy.entry下单操作的下单量为默认设置的0.1。strategy.entry函数调用时我们指定的direction均为strategy.long,所以回测测试时下单均为买单。

注意代码中strategy.entry("long3", ...下单操作调用了两次,对于相同ID:“long3”来说。第一个strategy.entry下单操作没有成交,第二次调用strategy.entry函数为修改这个ID的订单(回测测试时显示的数据也可以看出这个限价订单下单量被修改为了0.3)。另一种情况,举例子如果当第一次ID为“long3”的订单成交了,继续按照这个成交的ID“long3”去使用strategy.entry函数下单,那么订单头寸都会累计在ID“long3”上。


2、strategy.close

strategy.close函数用于平仓指定标识ID的入场持仓仓位。主要参数有:idwhenqtyqty_percent

参数:

  • id:需要平仓的入场ID,就是我们使用strategy.entry等入场下单函数开仓时指定的ID。
  • when:执行条件。
  • qty:平仓数量。
  • qty_percent:平仓百分比。

通过一个例子来熟悉这个函数的使用细节: 代码中/*backtest ... */是FMZ.COM国际站回测时的配置信息,可以删掉,设置自己需要测试的市场、品种、时间范围等信息。

/*backtest
start: 2022-07-03 00:00:00
end: 2022-07-09 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
*/

strategy("close Demo", pyramiding=3)

var enableStop = false 
if enableStop
    runtime.error("stop")

strategy.entry("long1", strategy.long, 0.2)
if strategy.opentrades >= 3 
    strategy.close("long1")                   // 多个入场订单,不指定qty参数,全部平仓
    // strategy.close()                          // 不指定id参数,会平掉当前的持仓
    // strategy.close("long2")                   // 如果指定一个不存在的id则什么都不操作
    // strategy.close("long1", qty=0.15)         // 指定qty参数平仓
    // strategy.close("long1", qty_percent=50)   // qty_percent设置50即为平掉long1标识仓位的50%持仓
    // strategy.close("long1", qty_percent=80, when=close<open)  // 指定when参数,修改为close>open就不触发了
    enableStop := true

测试策略展示了开始连续三次做多入场,入场标识ID均为“long1”,然后使用strategy.close函数的不同参数设置平仓时回测出的不同结果。可以发现strategy.close这个函数没有参数可以指定平仓下单价格,这个函数主要用于立即以当前市场价格平仓。


3、strategy.close_all

strategy.close_all函数用于平掉当前所有持仓,由于Pine语言脚本持仓只能有一个方向,即如果有和当前持仓方向相反的信号触发会平掉当前持仓再根据信号触发开仓。所以strategy.close_all被调用时会平掉当前方向上的所有持仓。strategy.close_all函数的主要参数为:when

参数:

  • when:执行条件。

我们使用一个例子来观察:

/*backtest
start: 2022-07-03 00:00:00
end: 2022-07-09 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
*/

strategy("closeAll Demo")

var enableStop = false 
if enableStop
    runtime.error("stop")

strategy.entry("long", strategy.long, 0.2, when=strategy.position_size==0 and close>open)
strategy.entry("short", strategy.short, 0.3, when=strategy.position_size>0 and close<open)

if strategy.position_size < 0 
    strategy.close_all()
    enableStop := true 

测试代码在开始的时候,持仓量是0(即strategy.position_size==0为真),所以符合when参数设置的条件时只执行ID为“long”的strategy.entry入场函数。持有多仓之后strategy.position_size大于0,这时ID为“short”的入场函数才可能被执行,由于当前持有多头仓位,这个时候出现的这个做空反向信号会导致平掉多头持仓后再反向开空。接着我们在if条件里写了当strategy.position_size < 0时,即持有空头持仓时平掉当前持有方向的全部持仓。并且标记enableStop := true。让策略停止执行以便于观察日志。

可以发现strategy.close_all这个函数没有参数可以指定平仓下单价格,这个函数主要用于立即以当前市场价格平仓。


4、strategy.exit

strategy.exit函数被用于入场持仓的平仓操作,与该函数不同的是strategy.closestrategy.close_all函数是以当前市场价格立即平仓。strategy.exit函数会根据参数设置进行计划平仓。

参数:

  • id:当前这个平仓条件单的订单标识符ID。
  • from_entry:用于指定要进行平仓操作的入场ID。
  • qty:平仓数量。
  • qty_percent:平仓百分比,范围:0 ~ 100。
  • profit:利润目标,以点数表示。
  • loss:止损目标,以点数表示。
  • limit:利润目标,以价格指定。
  • stop:止损目标,以价格指定。
  • when:执行条件。

使用一个测试策略来理解各个参数使用。

/*backtest
start: 2022-07-03 00:00:00
end: 2022-07-09 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
args: [["RunMode",1,358374],["ZPrecision",0,358374]]
*/

strategy("strategy.exit Demo", pyramiding=3)

varip isExit = false 

findOrderIdx(idx) =>
    ret = -1 
    if strategy.opentrades == 0 
        ret
    else 
        for i = 0 to strategy.opentrades - 1 
            if strategy.opentrades.entry_id(i) == idx
                ret := i 
                break
        ret

strategy.entry("long1", strategy.long, 0.1, limit=1, when=findOrderIdx("long1") < 0)
strategy.entry("long2", strategy.long, 0.2, when=findOrderIdx("long2") < 0)
strategy.entry("long3", strategy.long, 0.3, when=findOrderIdx("long3") < 0)

if not isExit and strategy.opentrades > 0
    // strategy.exit("exitAll")          // 如果仅仅指定一个id参数,则该退场订单无效,参数profit, limit, loss, stop等出场条件也至少需要设置一个,否则也无效
    strategy.exit("exit1", "long1", profit=50)                    // 由于long1入场订单没有成交,因此ID为exit1的出场订单也处于暂待状态,直到对应的入场订单成交才会放置exit1
    strategy.exit("exit2", "long2", qty=0.1, profit=100)          // 指定参数qty,平掉ID为long2的持仓中0.1个持仓
    strategy.exit("exit3", "long3", qty_percent=50, limit=strategy.opentrades.entry_price(findOrderIdx("long3")) + 1000)   // 指定参数qty_percent,平掉ID为long3的持仓中50%的持仓
    isExit := true 

if bar_index == 0 
    runtime.log("每点价格为:", syminfo.mintick)    // 每点价格和Pine语言模板参数上「定价货币精度」参数设置有关

使用实时价模型回测测试,这个测试策略开始执行了3个入场操作(strategy.entry函数),“long1”故意设置了limit参数,挂单价格为1使其无法成交。然后测试条件出场函数strategy.exit。使用了按点数止盈、按价格止盈,使用了平固定数量仓位,使用了按百分比平仓。鉴于篇幅例子中只演示了止盈。止损操作也是同理的。strategy.exit函数还有更加复杂的跟踪止损参数:trail_pricetrail_pointstrail_offset也可以在本例子中测试学习其用法。


5、strategy.cancel

strategy.cancel函数用来取消/停用所有预挂单的命令。这些函数strategy.order, strategy.entry , strategy.exit可以产生入场ID。该函数主要参数为:idwhen

参数:

  • id:所要取消的入场ID。
  • when:执行条件。

这个函数很好理解,就是用来取消没有成交的入场命令的。

/*backtest
start: 2022-07-03 00:00:00
end: 2022-07-09 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
*/

strategy("strategy.cancel Demo", pyramiding=3)

var isStop = false 
if isStop 
    runtime.error("stop")

strategy.entry("long1", strategy.long, 0.1, limit=1)
strategy.entry("long2", strategy.long, 0.2, limit=2)
strategy.entry("long3", strategy.long, 0.3, limit=3)

if not barstate.ishistory and close < open 
    strategy.cancel("long1")
    strategy.cancel("long2")
    strategy.cancel("long3")
    isStop := true 

6、strategy.cancel_all

strategy.cancel_all函数和strategy.cancel函数类似。取消/停用所有预挂单命令。可以指定when参数。

参数:

  • when:执行条件。
/*backtest
start: 2022-07-03 00:00:00
end: 2022-07-09 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"BTC_USDT"}]
*/

strategy("strategy.cancel Demo", pyramiding=3)

var isStop = false 
if isStop 
    runtime.error("stop")

strategy.entry("long1", strategy.long, 0.1, limit=1)
strategy.entry("long2", strategy.long, 0.2, limit=2)
strategy.entry("long3", strategy.long, 0.3, limit=3)

if not barstate.ishistory and close < open 
    strategy.cancel_all()
    isStop := true 

7、strategy.order

strategy.order函数的功能、参数设置等几乎与strategy.entry一致,区别为strategy.order函数不受strategy函数的pyramiding参数设置影响,没有下单次数限制。

参数:

  • id:可以理解为给某个交易头寸起个名字用于引用。可以引用这个id撤销、修改订单、平仓。
  • direction:如果下单方向是做多(买入)该参数就传strategy.long这个内置变量,如果要做空(卖出)就传strategy.short这个变量。
  • qty:指定下单的量,如果不传这个参数使用的就是默认下单量。
  • when:执行条件,可以指定这个参数来控制当前这个下单操作是否触发。
  • limit:指定订单限价价格。
  • stop:止损价格。

我们就使用strategy.order没有下单次数限制这个特性,结合strategy.exit条件退场函数。构造一个类似网格交易的脚本。例子非常简单,仅用于学习:

/*backtest
start: 2021-03-01 00:00:00
end: 2022-08-30 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Binance","currency":"ETH_USDT"}]
args: [["ZPrecision",0,358374]]
*/

varip beginPrice = -1

if not barstate.ishistory
    if beginPrice == -1 or (math.abs(close - beginPrice) > 1000 and strategy.opentrades == 0) 
        beginPrice := close
    
    for i = 0 to 20
        strategy.order("buy"+i, strategy.long, 0.01, limit=beginPrice-i*200, when=(beginPrice-i*200)<close)
        strategy.exit("coverBuy"+i, "buy"+i, qty=0.01, profit=200)
        
        strategy.order("sell"+i, strategy.short, 0.01, limit=beginPrice+i*200, when=(beginPrice+i*200)>close)
        strategy.exit("coverSell"+i, "sell"+i, qty=0.01, profit=200)

策略范例

本教程中的策略范例仅为教学策略,指导策略设计思路使用,并不做任何交易指导、建议。教学策略请勿实盘。

超级趋势指标策略

strategy("supertrend", overlay=true)

[supertrend, direction] = ta.supertrend(input(5, "factor"), input.int(10, "atrPeriod"))

plot(direction < 0 ? supertrend : na, "Up direction", color = color.green, style=plot.style_linebr)
plot(direction > 0 ? supertrend : na, "Down direction", color = color.red, style=plot.style_linebr)

if direction < 0
    if supertrend > supertrend[2]
        strategy.entry("entry long", strategy.long)
    else if strategy.position_size < 0
        strategy.close_all()
else if direction > 0
    if supertrend < supertrend[3]
        strategy.entry("entry short", strategy.short)
    else if strategy.position_size > 0
        strategy.close_all()

使用Pine语言编写趋势策略非常简单,这里我们就以一个超级趋势指标来设计一个简单的趋势跟踪策略。我们来一起分析这个策略源码。

首先策略代码开始使用strategy函数做了一些简单的设置:strategy("supertrend", overlay=true),只是设置了一个策略标题“supertrend”。设置了overlay参数为true,让画出的指标线等内容显示在主图上。我们设计一个Pine策略或者学习一个Pine策略脚本首先要看的是策略界面参数设计,我们看「超级趋势指标策略」的源码,其中有我们之前课程学习过的input函数

[supertrend, direction] = ta.supertrend(input(5, “factor”), input.int(10, “atrPeriod”))

input函数调用直接被用作ta.supertrend指标函数的参数用来计算超级趋势指标。其中:

  • input(5, “factor”)
  • input.int(10, “atrPeriod”)

函数默认会在Pine语言策略界面上设置出两个参数控件,如图:

img

可以看到,控件上的默认值就是input函数和input系列函数(这里是input.int)的第一个参数,这些在之前的章节也有讲解。通过这两个函数我们就可以在策略界面上设置ta.supertrend函数的参数了。超级趋势指标函数计算出一个价格数据supertrend和一个方向数据direction。然后就使用plot函数画图,注意在画图的时候是根据超级趋势指标的方向画图,只画出当前的方向。当direction为-1时当前行情趋势是向上的行情,当direction为1时当前行情为向下的趋势。所以我们可以看到plot函数画图的时候判断direction大于、小于0。

接下来的if ... else if逻辑就是交易信号的判断了,当表达式direction < 0为真时说明当前行情处于上行阶段,此时如果超级趋势指标中的价格数据supertrend高于向前数2根BAR上的超级趋势指标价格(即supertrend[2],还记得历史操作符引用某个变量历史数据吧)以此作为做多的入场信号。还记得吗?如果当前有持仓,此时进行反向下单函数调用会先平掉之前的持仓,再根据当前的交易方向开仓。另外即是说supertrend > supertrend[2]条件没有达成,只要此时strategy.position_size < 0即持有空头仓位,也是会触发strategy.close_all()函数执行,进行全部平仓的。

direction > 0当处于下行趋势阶段也是同理,如果有多头持仓会全部平仓,然后符合条件supertrend < supertrend[3]时触发做空信号,这里为什么设置为[3]引用向前数第三根BAR上的超级趋势指标价格数据呢?可能是策略作者有意为之,毕竟有些市场例如合约交易市场做空风险略大于做多风险。

对于ta.supertrend指标,是不是有些同学很感兴趣它是如何判断当前趋势是上行?还是下行呢?

其实这个指标也可以用Pine语言的自定义函数形式实现的:

pine_supertrend(factor, atrPeriod) =>
	src = hl2
	atr = ta.atr(atrPeriod)
	upperBand = src + factor * atr
	lowerBand = src - factor * atr
	prevLowerBand = nz(lowerBand[1])
	prevUpperBand = nz(upperBand[1])

	lowerBand := lowerBand > prevLowerBand or close[1] < prevLowerBand ? lowerBand : prevLowerBand
	upperBand := upperBand < prevUpperBand or close[1] > prevUpperBand ? upperBand : prevUpperBand
	int direction = na
	float superTrend = na
	prevSuperTrend = superTrend[1]
	if na(atr[1])
		direction := 1
	else if prevSuperTrend == prevUpperBand
		direction := close > upperBand ? -1 : 1
	else
		direction := close < lowerBand ? 1 : -1
	superTrend := direction == -1 ? lowerBand : upperBand
	[superTrend, direction]

这个自定义函数是和内置函数ta.supertrend一模一样的算法,当然算出的指标数据也是一模一样。 从这个自定义函数算法我们可以看到,Pine内置的超级趋势指标计算使用的是hl2内置变量(最高价、最低价相加然后除以2,即最高价最低价的平均值),然后根据参数atrPeriod计算一定周期的ATR指标(波幅)。然后使用hl2和ATR构建上轨、下轨。

根据代码中的三元表达式更新lowerBandupperBand

    lowerBand := lowerBand > prevLowerBand or close[1] < prevLowerBand ? lowerBand : prevLowerBand
    upperBand := upperBand < prevUpperBand or close[1] > prevUpperBand ? upperBand : prevUpperBand

lowerBand:下轨线,用于判断上行趋势是否发生变化。upperBand:上轨线,用于判断下行趋势是否发生变化。lowerBand和upperBand一直都在被计算,只是在这个自定义函数最后判断当前趋势方向。

    else if prevSuperTrend == prevUpperBand
        direction := close > upperBand ? -1 : 1
    else
        direction := close < lowerBand ? 1 : -1

这里判断如果上一根BAR上超级趋势的价格值是prevUpperBand,即上轨线,说明当前为下行趋势。如果此时close超过upperBand价格突破,认为此时趋势发生转变,转换为上行趋势。direction方向变量被设置为-1(上行趋势)。否则依然被设置为1(下行趋势)。所以你才会看到在超级趋势策略中if direction < 0时,信号条件触发后做多。direction > 0时,信号条件触发后做空。

    superTrend := direction == -1 ? lowerBand : upperBand
    [superTrend, direction]

最后,根据方向选择返回具体的超级趋势指标价格数据和方向数据。

动态平衡策略

/*backtest
start: 2021-03-01 00:00:00
end: 2022-09-08 00:00:00
period: 1h
basePeriod: 15m
exchanges: [{"eid":"Binance","currency":"ETH_USDT"}]
args: [["v_input_1",4374],["v_input_2",3],["v_input_3",300],["ZPrecision",0,358374]]
*/

varip balance = input(50000, "balance")
varip stocks = input(0, "stocks")

maxDiffValue = input(1000, "maxDiffValue")


if balance - close * stocks > maxDiffValue and not barstate.ishistory
    // more balance , open long 
    tradeAmount = (balance - close * stocks) / 2 / close
    strategy.order("long", strategy.long, tradeAmount)
    balance := balance - tradeAmount * close
    stocks := stocks + tradeAmount
    runtime.log("balance:", balance, ", stocks:", stocks, ", tradeAmount:", tradeAmount)

else if close * stocks - balance > maxDiffValue and not barstate.ishistory
    // more stocks , open short 
    tradeAmount = (close * stocks - balance) / 2 / close
    strategy.order("short", strategy.short, tradeAmount)
    balance := balance + tradeAmount * close
    stocks := stocks - tradeAmount
    runtime.log("balance:", balance, ", stocks:", stocks, ", tradeAmount:", tradeAmount)

plot(balance, title="balance value(quoteCurrency)", color=color.red)
plot(stocks*close, title="stocks value(quoteCurrency)", color=color.blue)

img

img

我们继续学习一些Pine语言策略设计范例,这次我们来看一个动态平衡策略。所谓动态平衡策略就是把BaseCurrency(交易品种)的金额和QuoteCurrency(计价币)的金额始终做平衡处理。哪种资产的相对价格升高,账户中持有的价值增大就卖出哪种资产。如果某种资产的相对价格降低,账户中持有的价值减少,就买入这种资产。这个就是所谓的动态平衡策略。其实动态平衡策略就会一种网格策略,在震荡行情中表现良好。但是在趋势行情中就会持续亏损,需要等待价格回归才可以慢慢减少亏损以致盈利,不过好处在于动态平衡策略可以始终捕获行情中的震荡走势。

缺点就如这个策略的回测图表上显示的一样,在价格大趋势上涨(或者大跌)的阶段策略浮亏比较大。所以这种策略对于现货策略还好,期货上使用则需要把控好风险。

我们来看一下策略代码设计:

我们使用了一种简化设计,在策略中模拟了一个balance(即QuoteCurrency资产数量)和stocks(即BaseCurrency资产数量)平衡信息。我们并不去读取账户中真正的资产数量,我们仅仅是用模拟的金额去计算合适买入,卖出。然后影响这个动态平衡策略拉出的网格的关键参数就是maxDiffValue,这个参数就是进行平衡的判断标准。在当前价格下,只有当BaseCurrencyQuoteCurrency偏差超过maxDiffValue时才去进行平衡,卖出价格高的资产,买入价格低的资产,重新让资产平衡。

策略交易信号触发必须是在实时BAR阶段才有意义,所以策略交易条件if判断中都设置了not barstate.ishistory。当根据当前价格计算,balance价值超过了stocks价值时进行买入。反之进行卖出操作。执行交易语句之后更新balancestocks变量,然后以待下一次平衡触发。

以上策略回测的信息里包含了策略回测起始时间的品种价格,价格为1458,所以我特意设置了参数balance为:4374(1458*3),设置参数stocks为:3。让资产开始时处于平衡状态。

带跟踪止损止盈的超级趋势策略

在之前的课程中我们学习过strategy.exit头寸退场函数,其中的跟踪止损止盈功能我们没有举例讲解。本节策略设计范例我们就使用strategy.exit函数的跟踪止损止盈功能来对一个超级趋势策略做优化。

首先我们来看strategy.exit函数的跟踪止损止盈参数:

1、trail_price参数:触发放置跟踪止盈止损平仓单这个逻辑行为的位置(以价格指定


More