[TOC]
Supporting video tutorial: https://www.youtube.com/watch?v=CA3SwJQb_1g
FMZ Quant Trading Platform supports Pine language strategy writing, backtesting, and live trading of Pine language strategies, and it is compatible with lower versions of Pine language. There are many Pine strategies (scripts) collected and transplanted in the Strategy Square on the FMZ Quant Trading Platform (FMZ.COM).
FMZ supports not only the Pine language, but also the powerful drawing function of the Pine language. The various functions, rich and practical tools, efficient and convenient management on the FMZ platform further enhance the practicability of the Pine strategy (script). Based on the compatibility with the Pine language, FMZ also expands, optimizes and trims the Pine language to a certain extent. Before entering the tutorial officially, let’s take a look at what changes have been made to Pine language on FMZ compared to the original version.
A brief overview of some of the obvious differences:
//@version
and the strategy
, indicator
statements at the beginning of the code are not mandatory to write, FMZ does not support import
to import library
function for now.It may be seen that some strategies written like this:
//@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)
Or write it like this:
//@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)
On FMZ it can be simplified to:
src = 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)
Or:
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)
Closing price model and real-time price model
On trading view, we can use the calc_on_every_tick
parameter of the strategy
function to set the strategy script to execute the strategy logic in real time when the price changes everytime. At this time, the calc_on_every_tick
parameter should be set to true
. The calc_on_every_tick
default parameter is false
, that is, the strategy logic is executed only when the current K-line BAR of the strategy is completely completed.
On FMZ, it is set by the parameters of the “Pine Language Trading Class Library” template.
Numerical precision control, such as price and order amount when the strategy is executed needs to be specified on FMZ On trading view, there is no accuracy problem when placing real trading orders, because it can only be tested in simulation. On FMZ, it is possible to run the Pine strategy in real trading. Then the strategy needs to be able to specify the price accuracy and order amount accuracy of the trading variety flexibly. The accuracy settings control the number of decimal places in the relevant data to prevent the data from not meeting the exchange’s order requirements and thus failing to place an order.
Futures contract code
If the trading product on FMZ is a contract, it has two attributes, they are “Trading Pair” and “Contract Code” respectively. In addition to setting the trading pair explicitly, it is also necessary to set the specific contract code in the parameter “Variety Code” of the “Pine Language Trading Class Library” template during the real trading and backtesting. For example, for a perpetual contract, fill in swap
, and the contract code depends on whether the operating exchange has such a contract. For example, some exchanges have quarterly contracts, you can fill in quarter
here. These contract codes are consistent with the futures contract codes defined in FMZ’s Javascript/python/c++ language API document.
For other settings, such as the minimum order amount, default order amount, etc., please refer to the parameter introduction on “Template arguments of pine language trade class library” in the Pine language documentation.
runtime.debug
, runtime.log
, runtime.error
used for debugging.3 functions have been added to the FMZ platform for debugging.
runtime.debug
: Print variable information on the console, which is generally not used with this function.
runtime.log
: output in the log. PINE language-specific functions on FMZ.
runtime.log(1, 2, 3, close, high, ...), Multiple parameters can be passed.
runtime.error
: It will result in a runtime error with the error message specified in the message parameter when called.
runtime.error(message)
overlay
parameter is extended in some of the drawing functionsIn the Pine language on FMZ, the drawing functions plot
, plotshape
, plotchar
, etc. have added the overlay
parameter support, allowing to specify the drawing on the main chart or sub-chart. overlay
is set to true
to draw on the main chart, and false
is set to draw on the sub-chart, which enables the Pine strategy on FMZ to draw the main chart and the sub-chart at the same time.
syminfo.mintick
built-in variableThe built-in variable of syminfo.mintick
is defined as the minimum tick value for the current symbol. This value can be controlled by the template parameter pricing currency precision in the “Pine Language Trading Class Library” on the FMZ bot/backtest interface. Pricing currency accurancy setting 2 means that the price is accurate to the second decimal place when trading, and the minimum price change unit is 0.01. The value of syminfo.mintick
is 0.01.
For example: the order price is 8000, the selling direction, the quantity is 1 lot (piece, sheet), the average price after the transaction is not 8000, but lower than 8000 (the cost includes the handling fee).
When starting to learn the basics of the Pine language, there may be some examples of instructions and code grammar that we are not familiar with. It doesn’t matter if you don’t understand it, we can get familiar with the concepts first and understand the purpose of the test, or you can check the Pine language documentation on FMZ for instructions. Then follow the tutorial step by step to familiarize yourself with various grammars, instructions, functions, and built-in variables.
When starting to learn the Pine language, it is very necessary to understand the related concepts such as the execution process of the Pine language script program. The Pine language strategy runs based on the chart. It can be understood that the Pine language strategy is a series of calculations and operations, which are executed on the chart in the order of time series from the earliest data that has been loaded on the chart. The amount of data that the chart initially loads is limited. In the real trading, the maximum amount of the data is usually determined based on the maximum data volume returned by the exchange interface, and the maximum amount of the data during backtesting is determined based on the data provided by the data source of the backtesting system. The leftmost K-line Bar on the chart, that is, the first data of the chart data set, has an index value of 0. The index value of the current K-line Bar when the Pine script is executed can be referenced through the built-in variable bar_index
in the Pine language.
plot(bar_index, "bar_index")
The plot
function is one of the functions we will use more in the future. The use is very simple, it is to draw a line on the chart according to the input parameters, the input data is bar_index
, and the line is named as bar_index
. It can be seen that the value of the line named bar_index on the first Bar is 0, and it increases by 1 to the right as the Bar increases.
Because the settings of the strategy are different, the model execution methods of the strategy are different, they can be divided into closing price model
and real-time price model
. We have also briefly introduced the concepts of them before.
Closing price model
When the strategy code is executed, the period of the current K-line Bar is completely executed, and when the K-line is closed, the K-line period has been completed. At this point, the Pine strategy logic is executed once, and the triggered trading signal will be executed at the beginning of the next K-line Bar.
Real-time price model
When the strategy code is executed, regardless of whether the current K-line Bar is closed or not, the Pine strategy logic will be executed when the market changes every time, and the triggered trading signal will be executed immediately.
When the Pine language strategy is executed from left to right on the chart, the K-line Bars on the chart are divided into Historical Bars
and Real-time Bars
:
Historical Bar
When the strategy is set to “Tick Model” and starts executing, all K-line Bars on the chart except the rightmost one are Historical Bars
. The strategy logic is executed only once on each historical bar
.
When the strategy is set to “Bar Model” and starts executing, all bars on the chart are historical bars
. The strategy logic is executed only once on each historical bar
.
Calculation based on historical Bars: The strategy code is executed once in the closing state of the historical bar, and then the strategy code continues to be executed in the next historical bar until all historical bars are executed once.
Real-time Bar
When the strategy is executed to the last K-line Bar on the far right, the Bar is a real-time Bar. After the real-time bar closed, the bar becomes a passed real-time bar (becomes a historical bar). A new real-time Bar will be generated at the far right of the chart.
When the strategy is set to “Tick Model” and starts executing, the strategy logic will be executed once for each market change on real-time bar. When the strategy is set to “Bar Model” and starts executing, the real-time bar will not be displayed on the chart.
Calculation based on real-time Bar:
If the strategy is set to “Bar Model” and the chart does not display the real-time bars, the strategy code will only be executed once when the current bar closes.
If the strategy is set to “Tick Model”, the calculation on the real-time bar is completely different from the historical bar, and the strategy code will be executed once for each market change on live trading bars. For example, the built-in variables high
, low
, close
are determined on historical Bars, and these values may change every time when the market changes on real-time Bars. Therefore, data such as indicators calculated based on these values will also change in real-time. On a real-time Bar, close
always represents the current latest price, and high
and low
always represent the highest point and lowest point reached since the start of the current real-time bar. These built-in variables represent the final value of the real-time Bar when it was last updated.
Rollback mechanism when executing strategies on real-time Bar (real-time price model): During real-time Bar execution, resetting user-defined variables before each new iteration of the strategy is called rollback. Let’s understand the rollback mechanism with an example of the following test code.
Attention:
/*backtest
...
..
.
*/
The content of the package is the backtest configuration information saved in the form of code on the FMZ platform.
/*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("before n + 1, n:", n, " current bar_index:", bar_index)
n := n + 1
runtime.log("after n + 1, n:", n, " current bar_index:", bar_index)
plot(n, title="n")
We only examine the scene executed during real-time Bars, so we use the not barstate.ishistory
expression to limit the accumulation of the variable n only in the real-time Bar, and use runtime.log
function to output the information in the strategy log before and after the accumulation operation. From the curve n drawn using the drawing function plot
, it can be seen that n is always 0 when the strategy is running in historical Bars. When the real-time Bar is executed, the operation of adding 1 to n is triggered, and the operation of adding 1 to n is executed when the strategy is executed in each round of the real-time Bar. It can be observed from the log message that n will be reset to the value finally submitted by the previous Bar execution strategy when the strategy code is re-executed in each round. The n value update will be submitted when the strategy code is executed on the real-time Bar for the last time, so you can see that the value of curve n increases by 1 with each increase of Bar starting from the real-time Bar on the chart.
Summary:
Due to the data rollback, drawing operations, such as curves on the chart may also cause redrawing. For example, let’s modify the test code just now for live trading:
var n = 0
if not barstate.ishistory
runtime.log("before n + 1, n:", n, " current bar_index:", bar_index)
n := open > close ? n + 1 : n
runtime.log("after n + 1, n:", n, " current bar_index:", bar_index)
plot(n, title="n")
Screenshot of time A
Screenshot of time B
We only modified the sentence: n := open > close ? n + 1 : n
, only add 1 to n when the current real-time Bar is a negative line (that is, the opening price is higher than the closing price). It can be seen that in the first chart (time A), since the opening price was higher than the closing price (negative line) at that time, n was accumulated by 1, and the value of n displayed on the chart curve was 5. Then the market changed and the price updated as shown in the second chart (time B). At this time, the opening price is lower than the closing price (positive line), and the n value rolls back without incrementing by 1. The curve of n in the chart is also redrawn immediately, and the value of n on the curve is 4. Therefore, the signals, such as crossup and crossdown displayed on real-time bars, are uncertain and may change.
Variable context in functions
Let’s study the variables in the Pine language function together. According to some descriptions on Pine tutorials, the variables in the function have the following differences from the variables outside the function:
The history of series variables used in the Pine function is created with each successive call to the function. If the function is not called on every bar on which the script runs, this will result in a discrepancy between the historical values of the series inside and outside the function’s local block. Therefore, if the function is not called on each bar, the series referenced inside and outside the function with the same index value will not refer to the same historical point.
Is it a little hard to understand? Nevermind, we will figure it out with a test code running on FMZ:
/*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)
Screenshot of backtest running
The test code is relatively simple, mainly to examine the data referenced by two methods, namely: f(a) => a[1]
and f2() => close[1]
.
f(a) => a[1]
: Use the method of passing parameters, the function returns to a[1]
finally.
f2() => close[1]
: Use the built-in variable close
directly, and the function returns to close[1]
finally.
The []
symbol is used to refer to the historical value of the data series variable, and close[1] refers to the closing price data on the Bar before the current closing price. Our test code draws a total of 4 types of data on the chart:
plotchar(oneBarInTwo ? f(close) : na, title = "f(close)", color = color.red, location = location.absolute, style = shape.xcross, overlay = true, char = "A")
Draw a character “A”, the color is red, it is drawn when oneBarInTwo is true, and the drawn position (on the Y axis) is: the value returned by f(close)
.
plotchar(oneBarInTwo ? f2() : na, title = "f2()", color = color.green, location = location.absolute, style = shape.circle, overlay = true, char = "B")
Draw a character “B”, the color is green, it is drawn only when oneBarInTwo is true, and the drawn position (on the Y axis) is: the value returned by f2()
.
plot(close[2], title = "close[2]", color = color.red, overlay = true)
Draw a line, the color is red, and the drawn position (on the Y-axis) is: close[2]
, which is the closing price of the second bar before the current bar (counting 2 bars to the left).
plot(close[1], title = "close[1]", color = color.green, overlay = true)
Draw a line, the color is green, and the drawn position (on the Y-axis) is: close[1]
, which is the closing price of the first bar before the current bar (counting 1 bar to the left).
It can be seen from the screenshot of the strategy backtesting that although both the function f(a) => a[1]
used to draw the A marker and the function f2() => close[1]
used to draw the B marker use [1] to refer to the historical data on the data series, the marker positions of “A” and “B” on the chart are completely different. The position of the “A” marker always falls on the red line, which is the line drawn by the code in the strategy plot(close[2], title = "close[2]", color = color.red, overlay = true)
, the data used to draw the line is close[2]
.
The reason is to calculate whether to draw the “A” and “B” markers through the index of the K-line Bar, that is, the built-in variable bar_index
. The “A” and “B” markers are not drawn on each K-line Bar (the function calculation is called when drawing). The value referenced by the function f(a) => a[1]
will not be the same as the value referenced by the function f2() => close[1]
if the function is not called on every Bar (even if they both use the same index like [1]).
Some built-in functions need to be calculated on each Bar in order to calculate their results correctly
To illustrate this situation with a simple example:
res = close > close[1] ? ta.barssince(close < close[1]) : -1
plot(res, style = plot.style_histogram, color=res >= 0 ? color.red : color.blue)
We write the function call code ta.barssince(close < close[1])
in a ternary operator condition ? value1 : value2
. This causes the ta.barssince function to be called only when close > close[1]
. But the ta.barssince
function is to calculate the number of K-lines since the last time close < close[1]
was established. When the ta.barssince function is called, it is always close > close[1], that is, the current closing price is greater than the closing price of the previous Bar. When the function ta.barssince is called, the condition close < close[1] is not established, and there is no recent position where it holds.
ta.barssince: When called, the function returns na if the condition has never been met before the current K-line.
As shown in the chart:
So when the chart is drawn, only the data with a value for the res variable (-1) is drawn.
To avoid this problem, we just take the ta.barssince(close < close[1])
function call out of the ternary operator and write it outside any possible conditional branches, making it perform calculations on each K-line 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)
The concept of time series is very important in the Pine language, and it is a concept that we must understand when we learn the Pine language. Time series is not a type but a basic structure for storing continuous values of variables over time. We know that Pine scripts are based on charts, and the most basic content displayed in the chart is the K-line chart. Time series where each value is associated with a timestamp of a K-line Bar. open
is a built-in variable (built-in) of the Pine language, and its structure is to store the time series of the opening price of each K-line Bar. It can be understood that the time series structure of open
represents the opening prices of all K-line Bars from the first Bar at the beginning of the current K-line chart to the Bar where the current script is executed. If the current K-line chart is a 5-minute period, when we quote (or use) open
in the Pine strategy code, it is the opening price of the K-line Bar when the strategy code is executed currently. If you want to refer to historical values in a time series, you need to use the []
operator. When the Pine strategy is executed on a certain K-line Bar, use open[1]
to refer to the opening price of the previous K-line Bar (i.e., the opening price of the previous K-line period) that references the open
time series on which this K-line Bar is being executed currently by the script.
Variables on time series are very convenient for computing
Let’s take the built-in function ta.cum
as an example:
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
Test code:
v1 = 1
v2 = ta.cum(v1)
plot(v1, title="v1")
plot(v2, title="v2")
plot(bar_index+1, title="bar_index")
There are many built-in functions like ta.cum
that can process data on time series directly. For example, ta.cum
is the accumulation of the values corresponding to the variables passed in on each K-line Bar, and next we use a chart to make it easier to understand.
Strategy operation process | Built-in variable bar_index | v1 | v2 |
---|---|---|---|
The strategy runs on the first K-line Bar | 0 | 1 | 1 |
The strategy runs on the second K-line Bar | 1 | 1 | 2 |
The strategy runs on the third K-line Bar | 2 | 1 | 3 |
… | … | … | … |
The strategy runs on the N+1th K-line Bar | N | 1 | N+1 |
It can be seen that v1, v2 and even bar_index are all time series structures, and there is corresponding data on each bar. Whether the test code uses the “Bar model” or the “Tick model”, the only difference is whether the real-time Bar is displayed on the chart. For quick backtest, we use the “Tick model” to test.
Because the variable v1 is 1 on each Bar, when the ta.cum(v1)
function is executed on the first K-line Bar, there is only the first Bar, so the calculation result is 1 and assigned to variable v2.
When ta.cum(v1)
is executed on the second K-line Bar, there are already 2 K-line Bars (the built-in variable bar_index corresponding to the first one is 0, and the second one corresponding to the built-in variable bar_index is 1), so the calculation result is 2, which is assigned to the variable v2, and so on. In fact, it can be observed that v2 is the number of K-line Bars in the chart, since the index of the K-line bar_index
is incremented from 0, then bar_index + 1
is actually the number of K-line Bars. On the chart, we can also see that the lines v2
and bar_index
do indeed overlap.
Similarly, I can also use the ta.cum
built-in function to calculate the sum of the closing prices for all Bars on the current chart. All I need to do is write it like this: ta.cum(close)
, When the strategy runs to the real-time Bar on the far right, the result calculated by ta.cum(close)
is the sum of the closing prices of all Bars on the chart (if not running to the far right, it only accumulates up to the current Bar).
Variables on the time series can also be calculated using operators, such as the code: ta.sma(high - low, 14)
, subtract the built-in variable high
(the highest price of the K-line Bar) from low
(the lowest price of K-line Bar), and finally use the ta.sma
function to calculate the average value.
The result of a function call will also leave traces of values in the time series.
v1 = ta.highest(high, 10)[1]
v2 = ta.highest(high[1], 10)
plot(v1, title="v1", overlay=true)
plot(v2, title="v2", overlay=true)
The test code runs during backtesting, and it can be observed that the values of v1
and v2
are the same, and the lines drawn on the chart are also completely coincident. The result calculated by the function call will leave traces of the value in the time series, such as the ta.highest(high, 10)
in the code ta.highest(high, 10)[1]
. The result calculated by the function call can also use [1] to refer to its historical value. Based on the ta.highest(high, 10)
corresponding to the previous bar of the current Bar, the calculation result is ta.highest(high[1], 10)
. So ta.highest(high[1], 10)
and ta.highest(high, 10)[1]
are exactly equivalent.
Use another drawing function to output information verification:
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)
We can see that the values of variable a and variable b in the time series are displayed above and below the corresponding Bars. We can keep this drawing code during the learning process, because we may often need to output information on the chart for observation during backtesting and experimentation.
In the beginning part of the tutorial, we have summarized some differences in using Pine language on FMZ and Trading View. When writing Pine code on FMZ, you can omit the version number, indicator()
, strategy()
, and library()
is currently not supported. Of course, in order to be compatible with earlier versions of Pine scripts, strategies such as: //@version=5
, indicator()
, strategy()
can also be written. Some strategy settings can also be set by passing parameters in the strategy()
function.
<version>
<declaration_statement>
<code>
The <version>
version control information can be omitted.
The Pine language uses //
as a single-line comment symbol, since the Pine language does not have a multi-line comment symbol. FMZ extends the comment symbol /**/
for multi-line comments.
Lines in the script that are not comments or compiler directives are statements, which implement the script’s algorithm. A statement can be one of these contents.
if
, for
, while
or switch
structureStatements can be arranged in various ways
space
or ```tab`` (tab key). Their first character must also be the first character of the line. Lines starting at the first position, by definition, become part of the global scope of the script.local block
. A local block must be indented by one tab or four spaces (otherwise, it will be parsed as the concatenated code of the previous line, that is, judged to be the continuous content of the previous line of code), and each local block defines a different local scope.For example, it includes three local blocks, one in the custom function declaration, and two in the variable declaration using the if structure, as follows:
indicator("", "", true) // declaration statement (global scope), can be omitted
barIsUp() => // function declaration (global scope)
close > open // local block (local scope)
plotColor = if barIsUp() // variable declaration (global scope)
color.green // local block (local scope)
else
color.red // local block (local scope)
runtime.log("color", color = plotColor) // Call a built-in function to output the log (global scope)
Long lines can be split over multiple lines, or “wrapped” up. A wrapped line must be indented by any number of spaces, as long as it is not a multiple of 4 (these boundaries are used to indent local blocks).
a = open + high + low + close
It can be wrapped as (note that the number of spaces indented per line cannot be a multiple of 4):
a = open +
high +
low +
close
A long plot() call can be wrapped as:
close1 = request.security(syminfo.tickerid, "D", close) // syminfo.tickerid daily level closing price data series for the current trading pair
close2 = request.security(syminfo.tickerid, "240", close) // syminfo.tickerid 240-minute level closing price data series for the current trading pair
plot(ta.correlation(close, open, 100), // line-long plot() calls can be wrapped
color = color.new(color.purple, 40),
style = plot.style_area,
trackprice = true)
Statements in user-defined function declarations can also be wrapped. However, since a local block must begin with an indentation in grammar (4 spaces or 1 tab), when splitting it onto the next line, the continuation of a statement must begin with more than one indentation (not equal to 4 multiples of spaces). For example:
test(c, o) =>
ret = c > o ?
(c > o+5000 ?
1 :
0):
(c < o-5000 ?
-1 :
0)
a = test(close, open)
plot(a, title="a")
Before recognizing variables, we must understand the concept of “markers” first. In layman’s terms, “marker” is used as the name of function and variable (used to name variables and functions). Functions will be seen in our later tutorials, let’s learn about “markers” first.
(A-Z)
or lowercase (a-z)
letter or an underscore (_)
as the first character of the marker.Such as the following named markers:
fmzVar
_fmzVar
fmz666Var
funcName
MAX_LEN
max_len
maxLen
3barsDown // Wrong naming! It used a numeric character as the leading character of the marker
Like most programming languages, Pine language also has writing suggestions. When naming identifiers, it is generally recommended to:
// name variables, constants
GREEN_COLOR = #4CAF50
MAX_LOOKBACK = 100
int fastLength = 7
// name functions
zeroOne(boolValue) => boolValue ? 1 : 0
Operators are some operation symbols used in programming languages to construct expressions, and expressions are computational rules designed for certain computational purposes when we write strategies. Operators in the Pine language are classified by function as:
Assignment operators, arithmetic operators, comparison operators, logical operators, ? :
ternary operators, []
historical reference operators.
Taking the arithmetic operator *
as an example, it is different from the type problem caused by the return result of the Pine language operator on Trading View. The following test code is provided:
//@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)
When executing this script on Trading View, a compilation error will occur. The reason is that after multiplying adjustedLength = lenInput * factor
, the result is series int
type (series), but the second parameter of the function ta.ema
does not support this type. But there are no such strict restrictions on FMZ, the above code can run normally.
Let’s take a look at the use of various operators together.
There are 2 types of assignment operators: =
, :=
, which we have seen in several examples at the beginning part of the tutorial.
The =
operator is used to assign a value to a variable when it is initialized or declared. Variables that are initialized, declared and assigned with =
will start with that value on each subsequent Bar. These are valid variable declarations:
a = close // Use built-in variables to assign values to a
b = 10000 // Use numerical assignment
c = "test" // Use string assignment
d = color.green // Use color value assignment
plot(a, title="a")
plot(b, title="b")
plotchar(true, title="c", char=str.tostring(c), color=d, overlay=true)
Note that the assignment statement a = close
, the variable a on each Bar is the current closing price (close) of the Bar. Other variables b
, c
, d
are unchanged and can be tested in the backtest system on FMZ, and the results can be seen on the chart.
:=
is used to reassign values to existing variables. It can be simply understood that the :=
operator is used to modify the values of variables that have been declared and initialized.
If we use the :=
operator to assign a value to an uninitialized or declared variable, it will cause an error, for example:
a := 0
Therefore, the :=
assignment operator is generally used to reassign existing variables, for example:
a = close > open
b = 0
if a
b := b + 1
plot(b)
Judging if close > open
(that is, the current BAR is a positive line), the variable a is true. The code in the local block of the if statement b := b + 1
is executed, and the assignment operator :=
is used to reassign to b, and 1 is added. Then we use the plot function to draw the value of variable b on each BAR of the time series on the chart, and connect them into a line.
Do we think that when a positive line BAR appears, b will continue to accumulate by 1? Of course not, here we declare and initialize the variable b as 0 without using any keyword designation. This sentence b=0
is executed on each BAR, so we can see that the result of this code is to reset the b variable to 0 every time, if the variable a is true, that is, in line with close > open
, then b will be incremented by 1 when the code is executed in this round, and b is 1 when the plot function draws, but b is reassigned to 0 when the code is executed in the next round. This is also the place where Pine language beginners are prone to pitfalls.
When it comes to assignment operators, we must expand on two keywords: var
, varip
var
In fact, we have seen and used this keyword in previous tutorials, but we did not discuss it in details at that time. Let’s look at the description of this keyword first:
var is a keyword used for allocating and one-time initialization of variables. In general, variable assignment grammar that does not contain the keyword var causes the variable’s value to be overwritten every time the data is updated. In contrast, when variables are assigned by using the keyword var, they can “keep state” despite data updates.
We still use this example, but we use the var
keyword when assigning a value to b here.
a = close > open
var b = 0
if a
b := b + 1
plot(b)
The var
keyword allows the variable b to perform the initial assignment only, and then it will not reset b to 0 every time the strategy logic is executed, so it can be observed from the line drawn at runtime that b is the number of positive line BARs that have appeared when the current K line BAR was backtested.
Variables declared by var can be written not only in the global scope, but also in code blocks, such as this example:
strategy(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")
The variable ‘a’ holds the closing price of the first bar in the series. The variable ‘b’ holds the closing price of the first ‘green’ price bar in the series. The variable ‘c’ holds the closing price of the tenth ‘green’ bar in the series.
varip
We see the keyword varip
for the first time, we can look at the description of this keyword:
varip (var intrabar persist) is a keyword for assigning and one-time initialization of variables. It is similar to the var keyword, but variables declared with varip retain their values when real-time K-line updates.
Is it difficult to understand? It doesn’t matter, we explain it through an example, it is easy to understand.
strategy(overlay=true)
// test var varip
var i = 0
varip ii = 0
// Print the i and ii changed in each round of the strategy logic on the chart
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)
// Increment i and ii by 1 for each round of logic execution
i := i + 1
ii := ii + 1
This test code has different performances on “Bar Model” and “Tick Model”:
Bar Model:
Do you remember that the strategy execution we explained earlier is divided into historical BAR stage and real-time BAR stage? In the Bar Model, the historical K-line stage, the variables i
, ii
declared in var
, varip
perform incremental operations at each round of execution of the strategy code. Therefore, it can be seen that the numbers displayed on the K-line BAR of the backtest result are incremented by 1 one by one. When the historical K-line stage ends, the real-time K-line stage begins. The variables declared by var and varip begin to undergo different changes. Because it is Bar Model, the strategy code will be executed once for each price change in a K-line BAR, i := i + 1
and ii := ii + 1
will be executed once. The difference is that ii is modified every time. Although i is modified every time, the previous value will be restored when the strategy logic is executed in the next round (remember the rollback mechanism we explained in the previous “Model Execution” chapter?), and the value of i will not be updated until the current K-line BAR is completed (that is, the previous value will not be restored when the strategy logic is executed in the next round). So it can be seen that the variable i is still increased by 1 for each BAR. But variable ii is accumulated several times for each BAR.
Tick Model: Since the Tick Model executes the strategy logic only once per K-line BAR. So in the closing price model, the variables declared by var and varip behave exactly the same in the above example incrementing by 1 for each K-line BAR during the historical K-line stage and the real-time K-line stage.
Operators | Description |
---|---|
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Division |
% | Modulo |
The +
and -
operators can be used as binary operators or as unary operators. Other arithmetic operators can only be used as binary operators and it will report an error if it was used as unary operators.
+
, the result of the calculation is a string, the value will be converted to the string form, and then the strings are stitched together. If it is other arithmetic operator, it will try to convert the string to a value and then carry on the operation.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
The Pine language on FMZ is a little different from the Pine language on Trading View, the Pine language on FMZ is not very strict about variable types. For example:
a = 1 * "1.1"
b = "1" / "1.1"
c = 5 % "A"
plot(a)
plot(b)
plot(c)
It works on FMZ, but it reports a type error on the Trading View. If both operands of the arithmetic operator are strings, the system converts the strings to numeric values and then calculates them. If a non-numeric string cannot be computed, the result of the system operation is a null value–na.
The comparison operators are all binary operators.
Operators | Description |
---|---|
< | < |
> | > |
<= | <= |
>= | >= |
== | == |
!= | != |
Test example:
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
As we can see, the comparison operator is very simple to use, but this is also the operator we use the most when writing strategies. Both numeric values and built-in variables can be compared, such as close
, open
, etc.
As with the operator, there is a difference about Pine language between FMZ and Trading View. FMZ does not have particularly strict requirements for types, therefore, such statements d = "1" >= 2
will not report an error on FMZ, and it will be executed by converting the string to a value first and then comparing the operation. On Trading View, it will report an error.
Operators | Code Symbols | Description |
---|---|---|
not | not | Unary operator, not operations |
and | and | Binary operators, and operations |
or | or | Binary operators, or operations |
When it comes to logical operators, then we must talk about true value tables. The same as we learned in high school, here we just test and learn in our backtesting system:.
a = 1 == 1 // An expression formed by using comparison operators, the result is a Boolean value
b = 1 != 1
c = not b // Logical not operators
d = not a // Logical not operators
runtime.log("test the logical operator: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("test the logical operator: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")
In order not to overprint messages, we throw an error with the runtime.error("stop")
and make it stop after printing once. After that, we can observe the output information, and we can find that the printed content is actually the same as the true value table.
Ternary expressions using the ternary operator ? :
combined with operands condition ? valueWhenConditionIsTrue : valueWhenConditionIsFalse
We have also used them in the previous lessons. The so-called ternary expression, ternary operator means that there are three operands in it.
In the condition ? valueWhenConditionIsTrue : valueWhenConditionIsFalse
, condition
is the judgment condition. If it is true, the value of the expression is: valueWhenConditionIsTrue
. If condition
is false, then the value of the expression is valueWhenConditionIsFalse
.
Example of a convenient demonstration, although of little practical use:
a = close > open
b = a ? "positive line" : "negative line"
c = not a ? "negative line" : "positive line"
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)
What to do if we encounter a doji? It doesn’t matter! Ternary expressions can also be nested, as we did in the previous tutorial.
a = close > open
b = a ? math.abs(close-open) > 30 ? "positive line" : "doji" : math.abs(close-open) > 30 ? "negative line" : "doji"
c = not a ? math.abs(close-open) > 30 ? "negative line" : "doji" : math.abs(close-open) > 30 ? "positive line" : "doji"
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)
In fact, it is equivalent to replacing valueWhenConditionIsTrue
and valueWhenConditionIsFalse
in condition ? valueWhenConditionIsTrue : valueWhenConditionIsFalse
with another ternary expression.
Use the historical operator []
to refer to historical values on a time series. These historical values are the values of the variable on the K-line bar before the current K-line bar when the script was running. The []
operator is used after variables, expressions, and function calls. The value in []
square brackets is the offset of the historical data we want to reference from the current K-line BAR. For example, if I want to quote the closing price of the last K-line BAR, we write it as: close[1]
.
We’ve seen something like this in the previous lessons:
high[10]
ta.sma(close, 10)[1]
ta.highest(high, 10)[20]
close > nz(close[1], open)
The []
operator can only be used once on the same value, so it is wrong to write it like this, and an error will be reported:
a = close[1][2] // error
Here, someone may say that the operator []
is used for series structure, it seems that series structure (series) is similar to array!
Let’s use an example to illustrate the difference between series and arrays in the Pine language.
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]
will report an error, but:
b = close[1]
c = b[1]
But if written separately, it will not report an error. If we understand it according to the usual array, after the assignment of b = close [1]
, b should be a value, but c = b[1]
, b can still be used to refer to the historical value again by using the history operator. It can be seen that the concept of series in the Pine language is not as simple as an array. It can be understood as the historical value on the last bar of close (assigned to b), b is also a time series structure (time series), and its h