发明者量化PINE语言入门教程-初探与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
的功能。
可能看到有些策略是这样写的:
或者是这样写的:
在FMZ上可以简化为:
或者:
2、策略(脚本)一些交易相关的设置由FMZ策略界面上的「Pine语言交易类库」参数设置。
其它设置例如,最小下单量、默认下单量等可以参看Pine语言文档中关于「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文档上定义的期货合约代码一致。3、
runtime.debug
、runtime.log
、runtime.error
FMZ扩展的函数,用于调试。FMZ平台上增加了3个函数用于调试。
runtime.debug
:在控制台打印变量信息,一般来说用不到该函数。runtime.log
:在日志输出内容。FMZ PINE语言特有函数。runtime.log(1, 2, 3, close, high, ...),可以传多个参数。
runtime.error
:调用时,会导致运行时错误,并带有在message参数中指定的错误消息。runtime.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的索引值。

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执行时,策略的每次新迭代执行前重置用户定义的变量称为回滚。我们来以一个例子理解回滚机制,如下测试代码。注意:
包裹的内容为FMZ平台上以代码形式保存的回测配置信息。


我们只考察在实时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上执行时,变量在收盘更新时提交一次。
由于数据回滚,所以图表上的曲线等画图操作也是可能引起重绘的,例如我们修改一下刚才的测试代码,实盘测试:
时刻A的截图

时刻B的截图

我们只修改了这句:
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上运行的测试代码来弄明白这个问题:
回测运行截图

测试代码比较简单,主要是来考察两种方式引用的数据,即:
f(a) => a[1]
和f2() => close[1]
。[]
符号用于对数据系列变量历史值的引用操作,close[1]即引用当前收盘价前一个Bar上的收盘价数据。我们的测试代码一共在图表上画出4种数据:通过策略回测运行截图可以看到,虽然画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]这样相同的索引)。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上的收盘价。f(a) => a[1]
:使用传参数的方式,函数最后返回a[1]
。f2() => close[1]
:直接使用内置变量close
,函数最后返回close[1]
。一些内置函数需要在每个Bar上计算才能正确计算其结果
以一个简单例子说明这种情况:
我们将函数调用代码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上都执行计算。
