发明者量化PINE语言入门教程
配套视频教程:
【量化交易入门太难?使用trading view Pine语言从小白到Quant大神--Pine语言初探】
发明者量化交易平台支持Pine语言编写策略,支持回测、实盘运行Pine语言策略,兼容Pine语言的较低版本。在发明者量化交易平台(FMZ.COM)上的策略广场中有搜集、移植的众多Pine策略(脚本)。
FMZ不仅支持了Pine语言,同时也支持Pine语言强大的画图功能。FMZ平台上的各项功能、丰富实用的工具、高效便捷的管理,也进一步增强了Pine策略(脚本)的实用性。FMZ基于对Pine语言的兼容,同时也对Pine语言进行了一定程度的扩展、优化、裁剪。在正式进入教程之前,我们一起来看下FMZ上的Pine语言和原版的Pine有哪些改动。
简单概述一些比较明显的不同:
-
1、FMZ上的Pine策略,代码开头的版本标识
//@version和代码开始的strategy、indicator语句并不强制要求编写,FMZ暂时不支持import导入library的功能。可能看到有些策略是这样写的:
pine//@version=5 indicator("My Script", overlay = true) src = close a = ta.sma(src, 5) b = ta.sma(src, 50) c = ta.cross(a, b) plot(a, color = color.blue) plot(b, color = color.black) plotshape(c, color = color.red)或者是这样写的:
pine//@version=5 strategy("My Strategy", overlay=true) longCondition = ta.crossover(ta.sma(close, 14), ta.sma(close, 28)) if (longCondition) strategy.entry("My Long Entry Id", strategy.long) shortCondition = ta.crossunder(ta.sma(close, 14), ta.sma(close, 28)) if (shortCondition) strategy.entry("My Short Entry Id", strategy.short)在FMZ上可以简化为:
pinesrc = close a = ta.sma(src, 5) b = ta.sma(src, 50) c = ta.cross(a, b) plot(a, color = color.blue, overlay=true) plot(b, color = color.black, overlay=true) plotshape(c, color = color.red, overlay=true)或者:
pinelongCondition = ta.crossover(ta.sma(close, 14), ta.sma(close, 28)) if (longCondition) strategy.entry("My Long Entry Id", strategy.long) shortCondition = ta.crossunder(ta.sma(close, 14), ta.sma(close, 28)) if (shortCondition) strategy.entry("My Short Entry Id", strategy.short) -
2、策略(脚本)一些交易相关的设置由FMZ策略界面上的「Pine语言交易类库」参数设置。
-
收盘价模型与实时价模型
在trading view上,我们可以通过strategy函数的calc_on_every_tick参数去设置策略脚本在价格每次变动时实时执行策略逻辑,此时calc_on_every_tick参数应当设置为true。默认calc_on_every_tick参数是false,即在策略当前K线BAR完全走完时才去执行策略逻辑。
在FMZ上则是通过,「Pine语言交易类库」模板的参数去设置。 -
策略执行时的价格、下单量等数值精度控制在FMZ上是需要指定的
在trading view上因为只能模拟测试,所以没有实盘下单时的精度问题。在FMZ上是可以实盘运行Pine策略的。那么就需要策略可以灵活指定交易品种的价格精度、下单数量精度。这些精度设置即控制相关数据的小数位数,避免数据不符合交易所报单要求从而无法下单。 -
期货合约代码
在FMZ上交易品种如果是合约,是有2个属性的。分别为「交易对」、「合约代码」,在实盘和回测时除了需要明确设置交易对,也需要在「Pine语言交易类库」模板的参数「品种代码」中设置具体的合约代码。例如永续合约就填写swap,合约代码要具体看操作的交易所是否有这种合约。例如有的交易所有季度合约,这里就可以填写quarter。这些合约代码和FMZ的Javascript/python/c++语言API文档上定义的期货合约代码一致。
其它设置例如,最小下单量、默认下单量等可以参看Pine语言文档中关于「Pine语言交易类库」参数的介绍。
-
-
3、
runtime.debug、runtime.log、runtime.errorFMZ扩展的函数,用于调试。FMZ平台上增加了3个函数用于调试。
-
runtime.debug:在控制台打印变量信息,一般来说用不到该函数。 -
runtime.log:在日志输出内容。FMZ PINE语言特有函数。pineruntime.log(1, 2, 3, close, high, ...),可以传多个参数。 -
runtime.error:调用时,会导致运行时错误,并带有在message参数中指定的错误消息。pineruntime.error(message)
-
-
4、部分画图函数中扩展了
overlay参数在FMZ上的Pine语言,画图函数
plot、plotshape、plotchar等增加了overlay参数支持,允许指定画在主图或者副图。overlay设置true画在主图,设置为false画在副图。使得FMZ上的Pine策略运行时可以主图、副图同时画图。 -
5、
syminfo.mintick内置变量的取值syminfo.mintick内置变量的定义为当前品种的最小刻度值。在FMZ实盘/回测界面上「Pine语言交易类库」中的模板参数定价货币精度可以控制该值。定价货币精度设置2即交易时价格精确到小数点第二位,此时价格最小变动单位为0.01。syminfo.mintick的值即为0.01。 -
6、FMZ PINE Script中的均价均为包含手续费的价格
例如:下单价格为8000,卖出方向,数量1手(个、张),成交后均价不是8000,低于8000(成本中包含了手续费)。
Pine语言基础
开始学习Pine语言基础时,可能有些例子中的指令、代码语法我们并不熟悉。看不懂没关系,我们可以先熟悉概念,理解测试目的,也可以查询FMZ的Pine语言文档查看说明。然后跟随教程一步一步循序渐进熟悉各种语法、指令、函数、内置变量。
模型执行
在入门学习Pine语言时,是非常有必要了解Pine语言脚本程序执行过程等相关概念的。Pine语言策略是基于图表运行的,可以理解为Pine语言策略为一系列的计算和操作,在图表上以时间序列的先后顺序从图表已经加载的最早数据开始执行。图表初始加载的数据量是有限的。实盘时通常这个数据量上限是基于交易所接口返回的最大数据量决定,回测时数据量上限是基于回测系统数据源提供的数据决定。图表上最左边的第一个K线Bar,即图表数据集的第一个数据,其索引值为0。可以通过Pine语言的内置变量bar_index引用到Pine脚本执行时当前的K线Bar的索引值。
pine
plot(bar_index, "bar_index")
plot函数是我们将来使用较多的函数之一。用途很简单,就是根据传入的参数在图表上画线,传入的数据是bar_index,线命名为bar_index。可以看到在第一根Bar上名称为bar_index的线的值为0,随着Bar增加向右依次增加1。
根据策略的设置不同,策略的模型执行方式也不同,分为收盘价模型和实时价模型。收盘价模型、实时价模型的概念在之前我们也简单介绍过。
-
收盘价模型
策略代码执行时,当前K线Bar的周期完全执行完成,K线闭合时即K线周期已经走完。此时执行一遍Pine策略逻辑,触发的交易信号将在下一根K线Bar开始时执行。
-
实时价模型
策略代码执行时,当前K线Bar不论是否闭合,每次行情变动就执行一遍Pine策略逻辑,触发的交易信号立即执行。
当Pine语言策略在图表上从左至右执行时,图表上的K线Bar是分为历史Bar和实时Bar的:
-
历史Bar
策略设置为「实盘价模型」开始执行时,图表上除了最右侧的那一根K线Bar之外所有K线Bar都是
历史Bar。策略逻辑在每根历史Bar上仅执行一次。
策略设置为「收盘价模型」开始执行时,图表上所有Bar都是历史Bar。策略逻辑在每根历史Bar上仅执行一次。基于历史Bar的计算:
策略代码在历史Bar收盘状态下执行一次,然后策略代码继续在下一个历史Bar执行,直到所有历史Bar都执行一次。 -
实时Bar
当策略执行到最右边的最后一根K线Bar上时,该Bar为实时Bar。当实时Bar闭合之后,这根Bar就变成了一个经过的实时Bar(变成了历史Bar)。图表最右侧会产生新的实时Bar。
策略设置为「实时价模型」开始执行时,在实时Bar上每次行情变动都会执行一次策略逻辑。
策略设置为「收盘价模型」开始执行时,图表上不显示实时Bar。基于实时Bar的计算:
如果设置策略为「收盘价模型」图表不显示实时Bar,策略代码只在当前Bar收盘时执行一次。
如果设置策略为「实盘价模型」在实时Bar上的计算和历史Bar就完全不同了,在实盘Bar上每次行情变动都会执行一次策略代码。例如内置变量high、low、close在历史Bar上是确定的,在实时Bar上可能每次行情变动时这些值是会发生变化的。所以基于这些值计算的指标等数据也是会实时变动的。在实时Bar上close始终代表当前最新价格,high和low始终代表自当前实时Bar开始以来达到的最高高点和最低低点。这些内置变量代表实时Bar最后一次更新时的最终值。实时Bar上执行策略时的回滚机制(实时价模型):
在实时Bar执行时,策略的每次新迭代执行前重置用户定义的变量称为回滚。我们来以一个例子理解回滚机制,如下测试代码。注意:
/*backtest ... .. . */包裹的内容为FMZ平台上以代码形式保存的回测配置信息。
pine/*backtest start: 2022-06-03 09:00:00 end: 2022-06-08 15:00:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Bitfinex","currency":"BTC_USD"}] */ var n = 0 if not barstate.ishistory runtime.log("n + 1之前, n:", n, " 当前bar_index:", bar_index) n := n + 1 runtime.log("n + 1之后, n:", n, " 当前bar_index:", bar_index) plot(n, title="n")我们只考察在实时Bar时执行的场景,所以用了
not barstate.ishistory表达式限制只在实时Bar时对变量n累加,并且在执行累加操作前后使用runtime.log函数输出信息在策略日志中。从使用画图函数plot画出的曲线n可以看到在策略处于历史Bar运行时n一直是0。当执行到实时Bar时触发了n累加1的操作,并且在实时Bar上每轮执行策略时都执行了n累加1的操作。可以从日志信息中观察到每轮重新执行策略代码时n都被重置为前一个Bar执行策略最终提交的值。当实时Bar上最后一次执行策略代码时会提交n值更新,所以可以看到图表上从实时Bar开始,曲线n随着每次Bar增加时曲线n的值增加1。总结一下:
1、策略在实时Bar开始执行时,每次行情更新就执行一次策略代码。
2、在实时Bar上执行时,每次执行策略代码之前都会回滚变量。
3、在实时Bar上执行时,变量在收盘更新时提交一次。由于数据回滚,所以图表上的曲线等画图操作也是可能引起重绘的,例如我们修改一下刚才的测试代码,实盘测试:
pinevar n = 0 if not barstate.ishistory runtime.log("n + 1之前, n:", n, " 当前bar_index:", bar_index) n := open > close ? n + 1 : n runtime.log("n + 1之后, n:", n, " 当前bar_index:", bar_index) plot(n, title="n")我们只修改了这句:
n := open > close ? n + 1 : n,当前实时Bar为阴线(即开盘价高于收盘价)时才给n累加1。可以看到在第一张图(时刻A)中由于当时开盘价格高于收盘价格(阴线)所以n累加了1,图表曲线n显示的值为5。然后行情变动、价格更新如同第二张图(时刻B)中显示。此时开盘价格低于收盘价格(阳线),n值回滚并且也没有累加1。图表中曲线n也立即重绘,此时曲线上的n值为4。所以在实时Bar上显示的金叉、死叉等信号都是不确定的,有可能会变化的。 -
函数中的变量上下文
下面我们来一起研究一下Pine语言函数中的变量。根据一些Pine教程上的描述,函数中的变量与函数外的变量有这样的差异:
Pine函数中使用的系列变量的历史是通过对函数的每次连续调用创建的。如果没有在脚本运行的每个柱上调用函数,这将导致函数本地块内部与外部系列的历史值之间存在差异。因此,如果没有在每个柱上调用函数,则使用相同索引值在函数内部和外部引用的系列将不会引用相同的历史点。
是不是有些难以读懂?没关系,我们通过一个在FMZ上运行的测试代码来弄明白这个问题:
pine/*backtest start: 2022-06-03 09:00:00 end: 2022-06-08 15:00:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Bitfinex","currency":"BTC_USD"}] */ f(a) => a[1] f2() => close[1] oneBarInTwo = bar_index % 2 == 0 plotchar(oneBarInTwo ? f(close) : na, title = "f(close)", color = color.red, location = location.absolute, style = shape.xcross, overlay = true, char = "A") plotchar(oneBarInTwo ? f2() : na, title = "f2()", color = color.green, location = location.absolute, style = shape.circle, overlay = true, char = "B") plot(close[2], title = "close[2]", color = color.red, overlay = true) plot(close[1], title = "close[1]", color = color.green, overlay = true)回测运行截图
测试代码比较简单,主要是来考察两种方式引用的数据,即:
f(a) => a[1]和f2() => close[1]。-
f(a) => a[1]:使用传参数的方式,函数最后返回a[1]。 -
f2() => close[1]:直接使用内置变量close,函数最后返回close[1]。
[]符号用于对数据系列变量历史值的引用操作,close[1]即引用当前收盘价前一个Bar上的收盘价数据。我们的测试代码一共在图表上画出4种数据:-
plotchar(oneBarInTwo ? f(close) : na, title = "f(close)", color = color.red, location = location.absolute, style = shape.xcross, overlay = true, char = "A")
画一个字符“A”,颜色为红色,当oneBarInTwo为真时才画出,画出的位置(Y轴上)为:f(close)返回的值。 -
plotchar(oneBarInTwo ? f2() : na, title = "f2()", color = color.green, location = location.absolute, style = shape.circle, overlay = true, char = "B")
画一个字符“B”,颜色为绿色,当oneBarInTwo为真时才画出,画出的位置(Y轴上)为:f2()返回的值。 -
plot(close[2], title = "close[2]", color = color.red, overlay = true)
画线,颜色为红色,画出的位置(Y轴上)为:close[2]即当前Bar前数第2根(向左数2根)Bar上的收盘价。 -
plot(close[1], title = "close[1]", color = color.green, overlay = true)
画线,颜色为绿色,画出的位置(Y轴上)为:close[1]即当前Bar前数第1根(向左数1根)Bar上的收盘价。
通过策略回测运行截图可以看到,虽然画A标记使用的函数
f(a) => a[1]和画B标记使用的函数f2() => close[1]都是使用[1]来引用数据系列上的历史数据,但是图表上"A"和"B"的标记位置是完全不同的。"A"标记的位置总是落在红色的线上,也就是策略中代码plot(close[2], title = "close[2]", color = color.red, overlay = true)画出的线上,其画线使用的数据是close[2]。原因就是通过K线Bar的索引,即内置变量
bar_index计算是否画"A"和"B"标记。"A"和"B"标记并不是在每根K线Bar上都画图(画图时调用函数计算)。函数f(a) => a[1]这种方式引用的值,如果函数不是每根Bar上都调用就会与函数f2() => close[1]这种方式引用的值不相同(即使都使用[1]这样相同的索引)。 -
-
一些内置函数需要在每个Bar上计算才能正确计算其结果
以一个简单例子说明这种情况:
pineres = close > close[1] ? ta.barssince(close < close[1]) : -1 plot(res, style = plot.style_histogram, color=res >= 0 ? color.red : color.blue)我们将函数调用代码
ta.barssince(close < close[1])写在一个三元操作符condition ? value1 : value2中。这就导致了只在close > close[1]时去调用ta.barssince函数。可偏偏ta.barssince函数是计算从最近一次close < close[1]成立时的K线数量。调用ta.barssince函数时都是close > close[1],即当前收盘价大于上一根Bar的收盘价,函数ta.barssince被调用时其条件close < close[1]都不成立,也就没有最近一次成立的位置。ta.barssince : 调用时,如果在当前K线之前从未满足该条件,则该函数返回na。
如图:
所以画图时,只画出了res变量有值时的数据(-1)。
要避免这个问题,我们只用把
ta.barssince(close < close[1])函数调用从三元操作符中拿出来,写在任何可能的条件分支外部。使其在每根K线Bar上都执行计算。a = ta.barssince(close < close[1]) res = close > close[1] ? a : -1 plot(res, style = plot.style_histogram, color=res >= 0 ? color.red : color.blue)
时间序列
时间序列这个概念在Pine语言中非常重要,是我们学习Pine语言时必须要弄明白的一个概念。时间序列不是一种类型而是用于随时间存储变量的连续值的基本结构,我们知道Pine脚本是基于图表的,图表中展示的最基本的内容就是K线图。时间序列其中每个值都与一个K线Bar的时间戳关联。open是一个Pine语言的内置变量(built-in),其结构为储存每根K线Bar开盘价的时间序列。可以理解为open这个时间序列结构代表了当前K线图从开始的第一根Bar到当前脚本执行的这根Bar时所有K线Bar的开盘价。如果当前K线图是5分钟周期,那么我们在Pine策略代码中引用(或者使用)open时就是在使用策略代码当前执行时的K线Bar的开盘价。如果要引用时间序列中的历史值需要使用[]操作符。当Pine策略在某根K线Bar上执行时,使用open[1]表示引用open时间序列上当前脚本执行的这根K线Bar的前一根K线Bar的开盘价(即上一个K线周期的开盘价)。
-
时间序列上的变量非常方便用于计算
我们以内置函数ta.cum举例子:ta.cum Cumulative (total) sum of `source`. In other words it's a sum of all elements of `source`. ta.cum(source) → series float RETURNS Total sum series. ARGUMENTS source (series int/float) SEE ALSO math.sum测试代码:
pinev1 = 1 v2 = ta.cum(v1) plot(v1, title="v1") plot(v2, title="v2") plot(bar_index+1, title="bar_index")有很多类似
ta.cum这样的内置函数可以直接处理时间序列上的数据,例如ta.cum就是把传入的变量在每个K线Bar上对应的值累加起来,接下来我们使用一个图表来方便理解。策略运行过程 内置变量 bar_index v1 v2 策略运行在第1根K线Bar 0 1 1 策略运行在第2根K线Bar 1 1 2 策略运行在第3根K线Bar 2 1 3 ... ... ... ... 策略运行在第N+1根K线Bar N 1 N+1 可以看到,其实v1、v2甚至bar_index都是时间序列结构,在每根Bar上都有对应的数据。这个测试代码不论用「实时价模型」还是「收盘价模型」区别仅仅为图表上是否显示实时Bar。为了回测速度我们使用「收盘价模型」回测测试。
因为v1这个变量在每一根Bar上都是1,
ta.cum(v1)函数在第一根K线Bar上执行时由于只有第一根Bar,所以计算结果为1,赋值给变量v2。
当ta.cum(v1)在第二根K线Bar上执行时,已经有2根K线Bar了(第一根对应的内置变量bar_index是0,第二根对应的内置变量bar_index是1),所以计算结果为2,赋值给变量v2,以此类推。实际上可以观察到v2就是图表中K线Bar的数量,由于K线的索引bar_index是从0开始递增,那么bar_index + 1实际上也就是K线Bar的数量。观察图表也可以看到线v2和bar_index确实是重合的。同样我也可以用
ta.cum内置函数计算当前图表上所有Bar的收盘价之和,那么只用这样写就可以了:ta.cum(close),当策略运行到最右侧的实时Bar时ta.cum(close)计算出的结果就是图表上所有Bar的收盘价之和了(没有运行到最右侧时,只是累加到了当前Bar而已)。时间序列上的变量也可以使用运算符进行运算,例如代码:
ta.sma(high - low, 14),把内置变量high(K线Bar最高价)减去low(K线Bar最低价),最后使用ta.sma函数求平均值。 -
函数调用结果也会在时间序列中留下值的痕迹
v1 = ta.highest(high, 10)[1] v2 = ta.highest(high[1], 10) plot(v1, title="v1", overlay=true) plot(v2, title="v2", overlay=true)该测试代码在回测时测试运行,可以观察到
v1和v2的值是相同的,图表上画出的线也是完全重合的。函数调用计算出的结果在时间序列中会留下值的痕迹,例如代码ta.highest(high, 10)[1]其中的ta.highest(high, 10)函数调用计算出的结果也是可以用[1]来引用其历史值的。基于当前Bar的上一根Bar对应的ta.highest(high, 10)计算结果就是ta.highest(high[1], 10)。所以ta.highest(high[1], 10)和ta.highest(high, 10)[1]完全等价。使用另一种画图函数输出信息验证:
a = ta.highest(close, 10)[1] b = ta.highest(close[1], 10) plotchar(true, title="a", char=str.tostring(a), location=location.abovebar, color=color.red, overlay=true) plotchar(true, title="b", char=str.tostring(b), location=location.belowbar, color=color.green, overlay=true)可以看到时间序列中变量a和变量b的值显示在对应的Bar的上方和下方。在学习过程中可以保留这个画图代码,因为在测试、试验时可能经常需要在图表上输出信息用于观察。
脚本结构
一般结构
在教程开始部分我们总结过一些FMZ上的Pine和Trading View上的Pine语言使用方面的不同点,FMZ上的Pine代码编写时可以省略版本号、indicator()、strategy()、并且暂时不支持library()。当然为了兼容较早版本的Pine脚本,策略编写时写上诸如://@version=5,indicator(),strategy()也是可以的。一些策略设置也可以在strategy()函数中传参设置。
<version>
<declaration_statement>
<code>
<version>版本控制信息可省略。
注释
Pine语言使用//作为单行注释符,由于Pine语言没有多行注释符。FMZ扩展了注释符/**/用于多行注释。
代码
脚本中不是注释或编译器指令的行是语句,它实现了脚本的算法。一个语句可以是这些内容之一。
- 变量声明
- 变量的重新赋值
- 函数声明
- 内置函数调用,用户定义的函数调用
if,for,while或switch等结构
语句可以以多种方式排列
- 有些语句可以用一行来表达,比如大多数变量声明、只包含一个函数调用的行或单行函数声明。其他的,像结构,总是需要多行,因为它们需要一个局部的块。
- 脚本的全局范围内的语句(即不属于局部块的部分)不能以
空格或制表符(tab键)开始。它们的第一个字符也必须是该行的第一个字符。在行的第一个位置开始的行,根据定义成为脚本的全局范围的一部分。 - 结构或多行函数声明总是需要一个
local block。一个本地块必须缩进一个制表符或四个空格(否则,会被解析为上一行的串联代码,即被判定为上一行代码的连续内容),每个局部块定义了一个不同的局部范围。 - 多个单行语句可以通过使用逗号(,)作为分隔符在一行中串联起来。
- 一行中可以包含注释,也可以只是注释。
- 行也可以被包起来(在多行上继续)。
例如,包括三个局部块,一个在自定义函数声明中,两个在变量声明中使用if结构,如下代码:
pine
indicator("", "", true) // 声明语句(全局范围),可以省略不写
barIsUp() => // 函数声明(全局范围)
close > open // 本地块(本地范围)
plotColor = if barIsUp() // 变量声明 (全局范围)
color.green // 本地块 (本地范围)
else
color.red // 本地块 (本地范围)
runtime.log("color", color = plotColor) // 调用一个内置函数输出日志 (全局范围)
换行代码
长行可以被分割在多行上,或被 "包裹 "起来。被包裹的行必须缩进任何数量的空格,只要它不是4的倍数(这些边界用于缩进局部块)。
pine
a = open + high + low + close
可以被包装成(注意每行缩进的空格数量都不是4的倍数):
pine
a = open +
high +
low +
close
一个长的plot()调用可以被包装成。
pine
close1 = request.security(syminfo.tickerid, "D", close) // syminfo.tickerid 当前交易对的日线级别收盘价数据系列
close2 = request.security(syminfo.tickerid, "240", close) // syminfo.tickerid 当前交易对的240分钟级别收盘价数据系列
plot(ta.correlation(close, open, 100), // 一行长的plot()调用可以被包装
color = color.new(color.purple, 40),
style = plot.style_area,
trackprice = true)
用户定义的函数声明中的语句也可以被包装。但是,由于局部块在语法上必须以缩进开始(4个空格或1个制表符),当把它分割到下一行时,语句的延续部分必须以一个以上的缩进开始(不等于4个空格的倍数)。比如说:
pine
test(c, o) =>
ret = c > o ?
(c > o+5000 ?
1 :
0):
(c < o-5000 ?
-1 :
0)
a = test(close, open)
plot(a, title="a")
标识符和运算符
标识符
在认识变量之前,我们首先要了解“标识符”的概念。通俗的讲“标识符”是用来当做函数和变量的名称的(用于命名变量、函数)。函数在我们之后的教程中会了解到,我们首先学习一下“标识符”。
- 1、标识符必须以大写
(A-Z)或小写(a-z)字母或下划线(_)开头,作为标识符的第一个字符。 - 2、标识符第一个字符之后的下一个字符可以是字母、下划线或数字。
- 3、标识符的命名是区分大小写的。
例如以下命名的标识符:
pine
fmzVar
_fmzVar
fmz666Var
funcName
MAX_LEN
max_len
maxLen
3barsDown // 错误的命名!使用了数字字符作为标识符的开头字符
如同大部分的编程语言一样,Pine语言也有书写建议。通常建议对标识符的命名时:
- 1、全部字母大写用于命名常量。
- 2、使用小驼峰规则用于其它标识符命名。
pine
// 命名变量、常量
GREEN_COLOR = #4CAF50
MAX_LOOKBACK = 100
int fastLength = 7
// 命名函数
zeroOne(boolValue) => boolValue ? 1 : 0
运算符
运算符是编程语言中用于构建表达式的一些运算符号,而表达式则是我们编写策略时为了某种计算目的设计的计算规则。Pine语言中的运算符按照功能分类为:
赋值运算符、算数运算符、比较运算符、逻辑运算符、? : 三元运算符、[]历史引用运算符。
以算数运算符*为例,区别于Trading View 上的Pine语言运算符返回结果导致的类型问题,有以下测试代码:
pine
//@version=5
indicator("")
lenInput = input.int(14, "Length")
factor = year > 2020 ? 3 : 1
adjustedLength = lenInput * factor
ma = ta.ema(close, adjustedLength) // Compilation error!
plot(ma)
在Trading View上执行这个脚本时会编译报错,原因是adjustedLength = lenInput * factor相乘之后结果为series int类型(系列),然而ta.ema函数的第二个参数不支持这种类型。但是在FMZ上没有此类的严格限制,以上代码是可以正常运行的。
下面我们来一起看看各种运算符的使用。
赋值运算符
赋值运算符有2种:=、:=,我们在教程开始部分的几个例子里也见过了。
=运算符用于给变量初始化或者声明时赋值。使用=初始化、声明赋值之后的变量将在之后的每个Bar上以该值开始。这些都是有效的变量声明:
a = close // 使用内置变量赋值给a
b = 10000 // 使用数值赋值
c = "test" // 使用字符串赋值
d = color.green // 使用颜色值赋值
plot(a, title="a")
plot(b, title="b")
plotchar(true, title="c", char=str.tostring(c), color=d, overlay=true)
注意a = close赋值语句,在每个Bar上变量a都是当前该Bar的收盘价(close)。其它的变量b、c、d是不变的,可以在FMZ上的回测系统中测试,由画图可以看出结果。
:=用于将值重新赋值给现有变量,可以简单理解为使用:=操作符是用来修改已经声明过、初始化过的变量值。
如果使用:=操作符给未初始化或者声明的变量赋值会引发错误,例如:
pine
a := 0
所以,:=赋值操作符一般是用于已有变量的重新赋值,例如:
pine
a = close > open
b = 0
if a
b := b + 1
plot(b)
判断如果close > open(即当前BAR是阳线),a变量就是真值(true)。就会执行if语句的本地块中的代码b := b + 1,使用赋值操作符:=给b重新赋值,累加一个1。然后再使用plot函数在图表上画出变量b在时间序列各个BAR上的值,连成线。
我们是不是认为出现一个阳线BAR,b就会持续累加1呢?当然不是,这里我们给变量b声明、初始化为0的时候没有使用任何关键字指定。这句b=0是在每个BAR上都执行的,所以可以看到这个代码的运行结果是每次都把b变量重置为0,如果a变量为真值,即符合close > open那么本轮执行代码时b是会累加1,plot函数画图的时候b为1,但是下一轮执行代码的时候b就重新赋值为0了。这里也是Pine语言初学者容易踩坑的地方。
讲到赋值运算符,这里就必须扩展讲解两个关键字:var、varip
-
var
其实这个关键字,我们在之前的教程中也已经见过、用过,只不过当时都没有详细探讨。我们先来看下这个关键字的描述:
var 是用于分配和一次性初始化变量的关键字。通常,不包含关键字var的变量赋值语法会导致每次更新数据时都会覆盖变量的值。 与此相反,当使用关键字var分配变量时,尽管数据更新,它们仍可以“保持状态”
我们还是使用这个例子,只不过我们给b赋值时使用
var关键字。pinea = close > open var b = 0 if a b := b + 1 plot(b)var关键字让b变量只执行了最初的第一次赋值,之后每次执行策略逻辑的时候都不会再把b重置为0了,所以从运行时画出的线可以观察出b即为回测到当前K线BAR时出现过的阳线BAR的数量。var声明的变量不仅可以写在全局范围,也可以写在代码块中,例如这个例子:
pinestrategy(overlay=true) var a = close var b = 0.0 var c = 0.0 var green_bars_count = 0 if close > open var x = close b := x green_bars_count := green_bars_count + 1 if green_bars_count >= 10 var y = close c := y plot(a, title = "a") plot(b, title = "b") plot(c, title = "c")变量'a'保持系列中第一根柱线的收盘价。
变量'b'保持系列中第一个“绿色”价格棒的收盘价。
变量'c'保持系列中第十个“绿色”条的收盘价。 -
varip
varip这个关键字我们第一次看到,我们可以看下这个关键字的描述:varip(var intrabar persist)是用于分配和一次性初始化变量的关键词。它与var关键词相似,但是使用varip声明的变量在实时K线更新之间保留其值。
是不是比较难以理解?没关系,我们通过例子来讲解,就很容易明白了。
strategy(overlay=true) // 测试 var varip var i = 0 varip ii = 0 // 将策略逻辑每轮改变的i、ii打印在图上 plotchar(true, title="ii", char=str.tostring(ii), location=location.abovebar, color=color.red) plotchar(true, title="i", char=str.tostring(i), location=location.belowbar, color=color.green) // 每轮逻辑执行都给i、ii递增1 i := i + 1 ii := ii + 1这个测试代码在「收盘价模型」、「实时价模型」上有不同表现:
实时价模型:
还记得我们之前讲解的策略执行时分为历史BAR阶段,实时BAR阶段吗?当在实时价模型,历史K线阶段时var、varip声明的变量i、ii在策略代码每轮执行时都会执行递增操作。所以可以看到回测结果K线BAR上显示的数字逐个都是递增1的。当历史K线阶段结束,开始实时K线阶段。var、varip声明的变量则开始发生不同的变化。因为是实时价模型,在一根K线BAR内每次价格变动都会执行一遍策略代码,i := i + 1和ii := ii + 1都会执行一次。区别是ii每次都修改。i虽然每次也修改,但是下一轮执行策略逻辑时会恢复之前的值(记得之前「模型执行」章节我们讲解的回滚机制吗?),直到当前K线BAR走完才更新确定i的值(即下一轮执行策略逻辑时不再恢复之前的值)。所以可以看到变量i依然是每根BAR增加1。但是变量ii每根BAR就累加了好几次。收盘价模型:
由于收盘价模型是每根K线BAR走完时才执行一次策略逻辑。所以在收盘价模型时,历史K线阶段和实时K线阶段,var、varip声明的变量在以上例子中递增表现完全一致,都是每根K线BAR递增1。
算数运算符
| 运算符 | 说明 |
|---|---|
| + | 加法 |
| - | 减法 |
| * | 乘法 |
| / | 除法 |
| % | 求模 |
+、-操作符可以用作二元操作符,也可以当做一元操作符。其它的算数运算符只能用作二元操作符,如果用作一元操作符会报错。
1、算数运算符两侧都是数值类型,结果为数值类型,整型还是浮点数具体看运算结果。
2、如果其中有操作数是字符串,操作符是+,则计算结果为字符串,数值会转换成字符串形式,然后字符串拼接在一起。如果是其它算数运算符,则会尝试将字符串转换为数值,然后运算。
3、如果其中有操作数是na,则计算结果为空值na,在FMZ上打印的时候会显示NaN。
pine
a = 1 + 1
b = 1 + 1.1
c = 1 + "1.1"
d = "1" + "1.1"
e = 1 + na
runtime.log("a:", a, ", b:", b, ", c:", c, ", d:", d, ", e:", e)
// a: 2 , b: 2.1 , c: 11.1 , d: 11.1 , e: NaN
FMZ上的Pine语言这里和Trading View上的Pine语言有一点差别,FMZ上的Pine语言对于变量类型要求并不是十分苛刻、严格。例如:
pine
a = 1 * "1.1"
b = "1" / "1.1"
c = 5 % "A"
plot(a)
plot(b)
plot(c)
在FMZ上是可以运行的,但是在trading view上就会报类型错误。对于算数运算符两边的操作数都是字符串时,系统会把字符串转换为数值之后计算。如果是非数值字符串无法计算时,系统运算结果就为空值na。
比较运算符
比较操作符都是二元操作符。
| 运算符 | 说明 |
|---|---|
| < | 小于 |
| > | 大于 |
| <= | 小于等于 |
| >= | 大于等于 |
| == | 相等 |
| != | 不相等 |
测试例子:
pine
a = 1 > 2
b = 1 < 2
c = "1" <= 2
d = "1" >= 2
e = 1 == 1
f = 2 != 1
g = open > close
h = na > 1
i = 1 > na
runtime.log("a:", a, ", b:", b, ", c:", c, ", d:", d, ", e:", e, ", f:", f, ", g:", g, ", h:", h, ", i:", i)
// a: false , b: true , c: true , d: false , e: true , f: true , g: false , h: false , i: false
可以看到,比较运算符使用是很简单的,不过这个也是我们在编写策略时用的最多的操作符。既可以比较数值,也可以比较内置变量,例如close、open等。
和运算操作符一样,在FMZ上与Trading View的Pine是有区别的,FMZ没有特别严苛的要求类型,所以此类语句d = "1" >= 2 在FMZ上不会报错,执行时会先将字符串转换为数值,然后比较运算。在Trading View上会报错。
逻辑运算符
| 运算符 | 代码符号 | 说明 |
|---|---|---|
| 非 | not | 一元操作符,非运算 |
| 与 | and | 二元操作符,与(且)运算 |
| 或 | or | 二元操作符,或运算 |
讲到逻辑运算符,那么一定就要讲讲真值表了。和我们高中时学习的一样,只不过这里我们在回测系统进行测试、学习:
pine
a = 1 == 1 // 使用比较运算符构成的表达式,结果为布尔值
b = 1 != 1
c = not b // 逻辑非操作符
d = not a // 逻辑非操作符
runtime.log("测试逻辑操作符:and", "#FF0000")
runtime.log("a:", a, ", c:", c, ", a and c:", a and c)
runtime.log("a:", a, ", b:", b, ", a and b:", a and b)
runtime.log("b:", b, ", c:", c, ", b and c:", b and c)
runtime.log("d:", d, ", b:", b, ", d and b:", d and b)
runtime.log("测试逻辑操作符:or", "#FF0000")
runtime.log("a:", a, ", c:", c, ", a or c:", a or c)
runtime.log("a:", a, ", b:", b, ", a or b:", a or b)
runtime.log("b:", b, ", c:", c, ", b or c:", b or c)
runtime.log("d:", d, ", b:", b, ", d or b:", d or b)
runtime.error("stop")
为了不让回测系统一直不停的打印信息影响观察,我们使用runtime.error("stop")语句在执行一遍打印之后,就抛出异常错误让回测停止,之后就可以观察输出的信息了,可以发现打印的内容和真值表其实是一样的。
三元运算符
使用三元运算符? : 和操作数组合起来的三元表达式condition ? valueWhenConditionIsTrue : valueWhenConditionIsFalse我们在之前的课程上也已经熟悉过了。所谓三元表达式、三元运算符意思是其中的操作数一共有三个。
condition ? valueWhenConditionIsTrue : valueWhenConditionIsFalse中,condition就是判断条件,如果为真则表达式的值为:valueWhenConditionIsTrue。如果condition为假则表达式的值为valueWhenConditionIsFalse。
虽然没什么实际用途,但是方便演示的例子:
pine
a = close > open
b = a ? "阳线" : "阴线"
c = not a ? "阴线" : "阳线"
plotchar(a, location=location.abovebar, color=color.red, char=b, overlay=true)
plotchar(not a, location=location.belowbar, color=color.green, char=c, overlay=true)
如果碰到十字星怎么办,没关系!三元表达式也是可以嵌套的,在之前的教程里我们也这么干过了。
pine
a = close > open
b = a ? math.abs(close-open) > 30 ? "阳线" : "十字星" : math.abs(close-open) > 30 ? "阴线" : "十字星"
c = not a ? math.abs(close-open) > 30 ? "阴线" : "十字星" : math.abs(close-open) > 30 ? "阳线" : "十字星"
plotchar(a, location=location.abovebar, color=color.red, char=b, overlay=true)
plotchar(not a, location=location.belowbar, color=color.green, char=c, overlay=true)
其实就是相当于把condition ? valueWhenConditionIsTrue : valueWhenConditionIsFalse中的valueWhenConditionIsTrue、valueWhenConditionIsFalse,也使用另外的三元表达式代替。
历史运算符
使用历史运算符[],引用时间序列上的历史值。这些历史值是变量在脚本运行时当前K线BAR之前的K线BAR上的值。[]运算符用于变量、表达式、函数调用之后。[]这个方括号中的数值就是我们要引用的历史数据距离当前K线BAR的偏移量。例如我要引用上一根K线BAR的收盘价,就书写为:close[1]。
我们在之前的课程中都已经见过类似这样的写法:
pine
high[10]
ta.sma(close, 10)[1]
ta.highest(high, 10)[20]
close > nz(close[1], open)
[]运算符只能在同一值上使用一次,所以这样写是错误的,会报错:
pine
a = close[1][2] // 错误
可能看到这里,有些同学会说,操作符[]就是用于系列结构的,看起来系列结构(series)和数组差不多嘛!
下面我们来通过一个例子来说明一下Pine语言中的系列(series)和数组并不同。
pine
strategy("test", overlay=true)
a = close
b = close[1]
c = b[1]
plot(a, title="a")
plot(b, title="b")
plot(c, title="c")
虽然说a = close[1][2]这样写会报错,但是:
pine
b = close[1]
c = b[1]
分开写就不会报错了,如果按照通常的数组来理解,b = close[1]赋值之后,b应该是一个数值,然而c = b[1],b依然可以再次被使用历史操作符引用历史值。可见Pine语言中的系列(series)概念并不是数组那么简单。可以理解为close的上一根Bar上的历史值(赋值给b),b也是一个时间序列结构(time series),可以继续引用其历史值。所以我们看到画出的三条线a、b、c中,b线慢于a线一个BAR,c线慢于b线一个BAR。c线慢于a线2个BAR。
我们可以把图表拉倒最左侧,观察发现在第一根K线上,b和c的值均为空值(na)。那是因为当脚本在第一根K线BAR上执行时,向前引用一个、两个周期的历史值时是没有的,其不存在。所以我们在编写策略时需要经常注意,引用历史数据时是否会引用到空值,如果不加小心使用了空值会引起一系列的计算差别,甚至可能影响到实时BAR。通常我们会在代码中使用na、nz内置函数加以判断(其实我们之前的学习中也接触过nz、na函数,还记得在哪个章节吗?)具体处理空值的情况,例如:
pine
close > nz(close[1], open) // 当引用close内置变量前一个BAR的历史值时,如果不存在,则使用open内置变量
这便是一种对于可能引用到空值(na)的处理。
运算符优先级
我们已经学习了很多Pine语言的运算符,这些运算符通过和操作数各种各样的组合就形成了表达式。那么在表达式中计算的时候,这些运算的优先级是怎么样的呢?好比我们上学的时候学习的四则运算,有乘除法优先计算乘除法,再计算加减法。Pine语言中表达式也是一样的。
| 优先级 | 运算符 |
|---|---|
| 9 | [] |
| 8 | 一元运算符时的+ 、-和not |
| 7 | *、/、% |
| 6 | 二元运算符时的+、- |
| 5 | >、<、>=、<= |
| 4 | ==、!= |
| 3 | and |
| 2 | or |
| 1 | ?: |
优先级高的表达式部分先进行运算,如果优先级相同则从左到右运算。如果要强制先运算某个部分,可以使用()包裹住强制先进行运算该部分表达式。
变量
变量声明
我们之前已经学习过了“标识符”的概念,“标识符”就是作为变量的名称来给变量命名的。所以也说:变量是保存值的标识符。那么如何声明一个变量呢?声明变量又有哪些规则?
-
声明模式:
在声明变量时最先写的就是「声明模式」,变量的声明模式有三种即:
1、使用关键字var。
2、使用关键字varip。
3、什么都不写。var、varip关键字其实我们在之前的「赋值运算符」章节已经学习过了,这里不再赘述。如果变量的声明模式什么都不写,例如语句:i = 1,其实我们之前也讲过,这样声明的变量并且赋值,是在每个K线BAR上都执行的。 -
类型
FMZ上的Pine语言对于类型要求并不严苛,一般可以省略。不过为了兼容Trading View上的脚本策略,声明变量时也是可以带类型的。例如:int i = 0 float f = 1.1在Trading View上的类型是要求比较严苛的,如果使用以下代码在Trading View上则会报错:
baseLine0 = na // compile time error! -
标识符
标识符即变量名称,标识符的命名在之前章节已经讲过,可以回看:https://www.fmz.com/bbs-topic/9390#标识符
总结一下,声明一个变量可以写作:
// [<declaration_mode>] [<type>] <identifier> = value
声明模式 类型 标识符 = 值
这里使用赋值运算符:=在变量声明时给变量赋值。赋值时,值可以是字符串、数值、表达式、函数调用、if、 for、while或switch等结构(这些结构关键字、语句用法我们后续课程中会详细讲解,其实我们已经在之前的课程里学会了简单的if语句赋值,可以回顾看看)。
这里我们着重讲解一下input函数,这个函数是我们在设计编写策略时会很频繁用到的一个函数。也是设计策略时非常关键的函数。
input函数:
input函数,参数defval、title、tooltip、inline、group
在FMZ上的input函数和在Trading View上的有些不同,不过该函数都是作为策略参数的赋值输入使用。下面我们来通过一个例子详细说明input函数在FMZ上的使用:
pine
param1 = input(10, title="参数1名称", tooltip="参数1的描述信息", group="分组名称A")
param2 = input("close", title="参数2名称", tooltip="参数2的描述信息", group="分组名称A")
param3 = input(color.red, title="参数3名称", tooltip="参数3的描述信息", group="分组名称B")
param4 = input(close, title="参数4名称", tooltip="参数4的描述信息", group="分组名称B")
param5 = input(true, title="参数5名称", tooltip="参数5的描述信息", group="分组名称C")
ma = ta.ema(param4, param1)
plot(ma, title=param2, color=param3, overlay=param5)
在声明变量时给变量赋值,经常使用的就是input函数,在FMZ上input函数会在FMZ策略界面自动画出用于设置策略参数的控件。FMZ上支持的控件目前有数值输入框、文本输入框、下拉框、布尔值勾选。并且可以设置策略参数分组、设置参数的提示文本信息等功能。
我们介绍input函数的几个主要参数:
- defval :作为input函数设置的策略参数选项的默认值,支持Pine语言的内置变量、数值、字符串
- title :策略在实盘/回测的策略界面上显示的参数名称。
- tooltip :策略参数的提示信息,当鼠标悬停在策略参数上会显示出这个参数设置的文本信息。
- group :策略参数分组名称,可以给参数分组。
除了单独的变量声明、赋值,Pine语言中还有声明一组变量并且赋值的写法:
[变量A,变量B,变量C] = 函数 或者 ```if```、 ```for```、```while```或```switch```等结构
最常见的就是我们使用ta.macd函数计算MACD指标时,由于MACD指标是一个多线的指标,计算出三组数据。所以就可以写为:
pine
[dif,dea,column] = ta.macd(close, 12, 26, 9)
plot(dif, title="dif")
plot(dea, title="dea")
plot(column, title="column", style=plot.style_histogram)
我们使用以上代码就很容易画出MACD图表,不止是内置函数可以返回多个变量,编写的自定义函数也可以返回多个数据。
pine
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)
使用if等结构作为多个变量赋值的写法也和上面的自定义函数方式类似,有兴趣也可以试下。
[ema10, ema20] = if true
fast = ta.ema(close, 10)
slow = ta.ema(close, 20)
[fast, slow]
plot(ema10, title="ema10", color=color.fuchsia, overlay=true)
plot(ema20, title="ema20", color=color.aqua, overlay=true)
条件结构
一些函数是无法写在条件分支的本地代码块里的,主要有以下几个函数:
barcolor(), fill(), hline(), indicator(), plot(), plotcandle(), plotchar(), plotshape()
Trading View上会编译报错。FMZ上限制不是那么严苛,但是也建议遵循Trading View上的规范书写。例如这样虽然在FMZ上不报错,不过不建议这样写。
pine
strategy("test", overlay=true)
if close > open
plot(close, title="close")
else
plot(open, title="open")
if语句
讲解例子:
pine
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。
pine
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 ,例子讲解:
pine
// 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.string、input.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分支返回值的类型兼容问题。
pine
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的另一种使用方式,即不带表达式的写法。
pine
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语句返回最后一次循环执行时的返回值。如果没有任何代码执行则返回空值。
下面我们用一个简单例子来展示:
pine
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循环一致。我们也通过一个简单的例子来说明使用。
pine
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、计算均值
使用循环结构设计时:
pine
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循环求和,然后计算均值。
直接使用内置函数计算均线:
pine
plot(ta.sma(close, length), title="ta.sma", overlay=true)
直接使用内置函数ta.sma,计算均线指标,显然对于计算均线使用内置函数更加简单。在图表上对比可以看到计算出的结果完全一致。
2、求和
还是使用上面的例子来说明。
使用循环结构设计时:
pine
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来计算。
直接使用内置函数计算求和:
pine
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语句。
我还是用计算均线的例子来演示:
pine
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循环使用也是非常简单的,还可以设计一些计算逻辑是无法用内置函数代替的,例如计算阶乘:
pine
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。
我们用一个简单的代码来说明:
pine
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> a、float[] b声明数组或者只声明一个变量都可以被赋值数组,例如:
pine
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.new和array.from函数。Pine语言中还有很多和类型相关的与array.new类似的函数:array.new_int()、array.new_bool()、array.new_color()、array.new_string()等。
var关键字也可以作用与数组的声明模式,使用var关键字声明的数组仅仅在第一根BAR上被初始化。我们通过一个例子来观察:
pine
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的第一个参数为要处理的数组,第二个参数为指定的索引,第三个参数为要写入的元素。
使用以下这个简单的例子来说明:
pine
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语句都可以实现。
pine
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")
pine
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")
pine
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分支的本地范围内声明
历史数据引用
对于数组中元素的使用,以下的方式是等效的,我们通过以下例子可以看到在图表上画出两组线,每组两条,每组的两条线数值是完全相同的。
pine
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()。
我们使用以下例子来测试这些数组的增加、删除操作函数。
pine
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线级别的移动平均线观察对比。
pine
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()找到最后一次出现的值。
数组计算相关函数的测试例子:
pine
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")
这些都是比较常用的数组计算函数。
操作相关函数的例子:
pine
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、函数可以写成单行、多行。最后一条语句的返回值为当前函数返回值,返回值可以返回元组形式。
之前的教程内容我们也多次使用过自定义函数,例如设计成单行的自定义函数:
pine
barIsUp() => close > open
该函数返回当前BAR是否为阳线。
设计成多行的自定义函数:
pine
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均线计算的函数。
还有,可以返回两个变量的自定义函数例子:
pine
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函数中,pyramiding、default_qty_value参数。使用以下代码测试:
pine
/*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的入场持仓仓位。主要参数有:id,when,qty,qty_percent。
参数:
id:需要平仓的入场ID,就是我们使用strategy.entry等入场下单函数开仓时指定的ID。when:执行条件。qty:平仓数量。qty_percent:平仓百分比。
通过一个例子来熟悉这个函数的使用细节:
代码中/*backtest ... */是FMZ.COM国际站回测时的配置信息,可以删掉,设置自己需要测试的市场、品种、时间范围等信息。
pine
/*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:执行条件。
我们使用一个例子来观察:
pine
/*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.close和strategy.close_all函数是以当前市场价格立即平仓。strategy.exit函数会根据参数设置进行计划平仓。
参数:
id:当前这个平仓条件单的订单标识符ID。from_entry:用于指定要进行平仓操作的入场ID。qty:平仓数量。qty_percent:平仓百分比,范围:0 ~ 100。profit:利润目标,以点数表示。loss:止损目标,以点数表示。limit:利润目标,以价格指定。stop:止损目标,以价格指定。when:执行条件。
使用一个测试策略来理解各个参数使用。
pine
/*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_price、trail_points、trail_offset也可以在本例子中测试学习其用法。
5、strategy.cancel
strategy.cancel函数用来取消/停用所有预挂单的命令。这些函数strategy.order, strategy.entry , strategy.exit可以产生入场ID。该函数主要参数为:id、when。
参数:
id:所要取消的入场ID。when:执行条件。
这个函数很好理解,就是用来取消没有成交的入场命令的。
pine
/*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:执行条件。
pine
/*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条件退场函数。构造一个类似网格交易的脚本。例子非常简单,仅用于学习:
pine
/*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)
策略范例
本教程中的策略范例仅为教学策略,指导策略设计思路使用,并不做任何交易指导、建议。教学策略请勿实盘。
超级趋势指标策略
pine
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语言策略界面上设置出两个参数控件,如图:
可以看到,控件上的默认值就是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
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构建上轨、下轨。
根据代码中的三元表达式更新lowerBand和upperBand。
pine
lowerBand := lowerBand > prevLowerBand or close[1] < prevLowerBand ? lowerBand : prevLowerBand
upperBand := upperBand < prevUpperBand or close[1] > prevUpperBand ? upperBand : prevUpperBand
lowerBand:下轨线,用于判断上行趋势是否发生变化。upperBand:上轨线,用于判断下行趋势是否发生变化。lowerBand和upperBand一直都在被计算,只是在这个自定义函数最后判断当前趋势方向。
pine
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时,信号条件触发后做空。
pine
superTrend := direction == -1 ? lowerBand : upperBand
[superTrend, direction]
最后,根据方向选择返回具体的超级趋势指标价格数据和方向数据。
动态平衡策略
pine
/*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)
我们继续学习一些Pine语言策略设计范例,这次我们来看一个动态平衡策略。所谓动态平衡策略就是把BaseCurrency(交易品种)的金额和QuoteCurrency(计价币)的金额始终做平衡处理。哪种资产的相对价格升高,账户中持有的价值增大就卖出哪种资产。如果某种资产的相对价格降低,账户中持有的价值减少,就买入这种资产。这个就是所谓的动态平衡策略。其实动态平衡策略就会一种网格策略,在震荡行情中表现良好。但是在趋势行情中就会持续亏损,需要等待价格回归才可以慢慢减少亏损以致盈利,不过好处在于动态平衡策略可以始终捕获行情中的震荡走势。
缺点就如这个策略的回测图表上显示的一样,在价格大趋势上涨(或者大跌)的阶段策略浮亏比较大。所以这种策略对于现货策略还好,期货上使用则需要把控好风险。
我们来看一下策略代码设计:
我们使用了一种简化设计,在策略中模拟了一个balance(即QuoteCurrency资产数量)和stocks(即BaseCurrency资产数量)平衡信息。我们并不去读取账户中真正的资产数量,我们仅仅是用模拟的金额去计算合适买入,卖出。然后影响这个动态平衡策略拉出的网格的关键参数就是maxDiffValue,这个参数就是进行平衡的判断标准。在当前价格下,只有当BaseCurrency和QuoteCurrency偏差超过maxDiffValue时才去进行平衡,卖出价格高的资产,买入价格低的资产,重新让资产平衡。
策略交易信号触发必须是在实时BAR阶段才有意义,所以策略交易条件if判断中都设置了not barstate.ishistory。当根据当前价格计算,balance价值超过了stocks价值时进行买入。反之进行卖出操作。执行交易语句之后更新balance和stocks变量,然后以待下一次平衡触发。
以上策略回测的信息里包含了策略回测起始时间的品种价格,价格为1458,所以我特意设置了参数balance为:4374(1458*3),设置参数stocks为:3。让资产开始时处于平衡状态。
带跟踪止损止盈的超级趋势策略
在之前的课程中我们学习过strategy.exit头寸退场函数,其中的跟踪止损止盈功能我们没有举例讲解。本节策略设计范例我们就使用strategy.exit函数的跟踪止损止盈功能来对一个超级趋势策略做优化。
首先我们来看strategy.exit函数的跟踪止损止盈参数:
1、trail_price参数:触发放置跟踪止盈止损平仓单这个逻辑行为的位置(以价格指定位置)。
2、trail_offset参数:执行跟踪止损止盈行为之后,放置的平仓单距离最高价(做多时)或者最低价(做空时)的距离。
3、trail_points参数:如同trail_price参数,只不过是以盈利点数为指定位置。
是不是不容易理解,没关系!我们来通过一个策略回测场景来理解学习,其实很简单的。
pine
/*backtest
start: 2022-09-23 00:00:00
end: 2022-09-23 08:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Binance","currency":"ETH_USDT"}]
args: [["RunMode",1,358374],["ZPrecision",0,358374]]
*/
strategy("test", overlay = true)
varip a = na
varip highPrice = na
varip isTrade = false
varip offset = 30
if not barstate.ishistory and not isTrade
strategy.entry("test 1", strategy.long, 1)
strategy.exit("exit 1", "test 1", 1, trail_price=close+offset, trail_offset=offset)
a := close + offset
runtime.log("每点价格为:", syminfo.mintick, ",当前close:", close)
isTrade := true
if close > a and not barstate.ishistory
highPrice := na(highPrice) ? close : highPrice
highPrice := close > highPrice ? close : highPrice
plot(a, "trail_price 触发线")
plot(strategy.position_size>0 ? highPrice : na, "当前最高价")
plot(strategy.position_size>0 ? highPrice-syminfo.mintick*offset : na, "移动止损触发线")
策略开始执行时立即多头入场,然后立即下一个strategy.exit出场订单(指定了跟踪止损止盈参数),当行情变动价格上涨超过trail_price触发线时,开始执行跟踪止损止盈逻辑,止损止盈线(蓝色)开始跟随最高价动态调整,蓝色线位置就是止损止盈触发平仓的价位,最后当行情变动价格跌破蓝色线即触发平仓。这样结合图表上画出的线是不是就很容易理解了。
那么我们使用这个功能来优化一种超级趋势策略,我们只用给策略入场订单指定一个strategy.exit出场计划单,就可以增加这种跟踪止损止盈功能。
pine
if not barstate.ishistory and findOrderIdx("open") >= 0 and state == 1
trail_price := strategy.position_size > 0 ? close + offset : close - offset
strategy.exit("exit", "open", 1, trail_price=trail_price, trail_offset=offset)
runtime.log("每点价格为:", syminfo.mintick, ",当前close:", close, ",trail_price:", trail_price)
state := 2
tradeBarIndex := bar_index
完整的策略代码:
pine
/*backtest
start: 2022-05-01 00:00:00
end: 2022-09-27 00:00:00
period: 1d
basePeriod: 5m
exchanges: [{"eid":"Binance","currency":"ETH_USDT"}]
args: [["RunMode",1,358374],["ZPrecision",0,358374]]
*/
varip trail_price = na
varip offset = input(50, "offset")
varip tradeBarIndex = 0
// 0 : idle , 1 current_open , 2 current_close
varip state = 0
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
if strategy.position_size == 0
trail_price := na
state := 0
[superTrendPrice, dir] = ta.supertrend(input(2, "atr系数"), input(20, "atr周期"))
if ((dir[1] < 0 and dir[2] > 0) or (superTrendPrice[1] > superTrendPrice[2])) and state == 0 and tradeBarIndex != bar_index
strategy.entry("open", strategy.long, 1)
state := 1
else if ((dir[1] > 0 and dir[2] < 0) or (superTrendPrice[1] < superTrendPrice[2])) and state == 0 and tradeBarIndex != bar_index
strategy.entry("open", strategy.short, 1)
state := 1
// 反向信号,全平
if strategy.position_size > 0 and dir[2] < 0 and dir[1] > 0
strategy.cancel_all()
strategy.close_all()
runtime.log("趋势反转,多头全平")
else if strategy.position_size < 0 and dir[2] > 0 and dir[1] < 0
strategy.cancel_all()
strategy.close_all()
runtime.log("趋势反转,空头全平")
if not barstate.ishistory and findOrderIdx("open") >= 0 and state == 1
trail_price := strategy.position_size > 0 ? close + offset : close - offset
strategy.exit("exit", "open", 1, trail_price=trail_price, trail_offset=offset)
runtime.log("每点价格为:", syminfo.mintick, ",当前close:", close, ",trail_price:", trail_price)
state := 2
tradeBarIndex := bar_index
plot(superTrendPrice, "superTrendPrice", color=dir>0 ? color.red : color.green, overlay=true)
- 1




















