PINE Language Introductory Tutorial of FMZ Quant

Author: Lydia, Created: 2022-09-23 15:23:34, Updated: 2024-02-27 16:47:41

istorical value can continue to be referenced. So we see that in the drawn three lines a, b, and c, line b is one BAR slower than line a, and line c is one BAR slower than line b. Line c is 2-BAR slower than line a.

We can pull the chart to the far left and observe that on the first K-line, both the values ​​of b and c are null (na). This is because when the script is executed on the first K-line BAR, it does not exist when referring to the historical value of one or two periods forward, which does not exist. Therefore, we need to be careful when writing strategies to check whether referencing historical data will result in null values. If the null value is used carelessly, it will cause a series of calculation differences, and may even affect the real-time BAR. Usually we will use built-in functions na, nz to judge in the code (in fact, we have also encountered nz, ```na`` in our previous vedios, do you remember which chapter it is?) deal with the case of null values, for example:

close > nz(close[1], open)    // When referencing the historical value of the previous BAR of the close built-in variable, if it does not exist, the open built-in variable is used

This is a way to handle possible references to null values ​​(na).

Operator Priority

We have learned a lot of operators in Pine language. These operators form expressions through various combinations with operands. So what is the priority of these operations when evaluating in expressions? Just like the arithmetic we learned in school, multiplication and division are calculated first, followed by addition and subtraction. The same goes for expressions in Pine language.

Priority Operators
9 []
8 +-andnot in the unary operator
7 */%
6 +- in the binary operator
5 ><>=<=
4 ==!=
3 and
2 or
1 ?:

High-priority expressions are calculated first, and if the priorities are the same, it is evaluated from left to right. If you want to force a certain part to be evaluated first, you can use () to wrap the expression to force the part to be evaluated first.

Variables

Variable Declaration

We have already studied the concept of “marker” before, which is used as the name of a variable, that is, a variable is a marker that holds a value. So how do we declare a variable? What are the rules for declaring variables?

  • Declaration Mode: The first thing to write when declaring a variable is the “declaration mode”. There are three declaration modes for variables:

    1. Use the keyword var.
    2. Use the keyword varip.
    3. Write nothing.

    The var and varip keywords have actually been studied in our previous chapter on Assignment Operators, so we won’t go into details here. If nothing is written for the variable declaration mode, sucha as the statement: i = 1, as we have also mentioned before, such a variable declared and assigned is executed on every K-line BAR.

  • Type The Pine language on FMZ is not strict about types, and it can generally be omitted. However, in order to be compatible with the scripting strategy on Trading View, variables can also be declared with types. For example:

    int i = 0 
    float f = 1.1
    

    The type requirements on Trading View are quite strict, and an error will be reported if the following code is used on Trading View:

    baseLine0 = na          // compile time error!
    
  • Marker Markers are variable names. The naming of markers has been mentioned in previous chapters, so you can review it here: https://www.fmz.com/bbs-topic/9637#markers

In summary, declaring a variable can be written as:

// [<declaration_mode>] [<type>] <marker> = value 
   declaration mode             type     marker      = value

The assignment operator is used here: = assigns a value to a variable when it is declared. When assigning, the value can be a string, number, expression, function call, if, for, while, or switch and other structures (these structural keywords and statement usage will be explained in detail in subsequent courses. In fact, we have learned simple if statement assignments in previous courses and you can review them).

Here we focus on the input function, which is a function that we will use frequently when designing and writing strategies. It is also a very critical function when designing strategies.

input function:

input function, parameters: defval、title、tooltip、inline、group

The input function on FMZ is somewhat different from that on Trading View, but this function is used as the assignment input of strategy parameters. Let’s use an example to illustrate the use of the input function on FMZ in detail:

param1 = input(10, title="name of param1", tooltip="description for param1", group="group name A")
param2 = input("close", title="name of param2", tooltip="description for param2", group="group name A")
param3 = input(color.red, title="name of param3", tooltip="description for param3", group="group name B")
param4 = input(close, title="name of param4", tooltip="description for param4", group="group name B")
param5 = input(true, title="name of param5", tooltip="description for param5", group="group name C")

ma = ta.ema(param4, param1)
plot(ma, title=param2, color=param3, overlay=param5)

The input function is often used to assign values to variables when declaring them. The input function on FMZ draws controls for setting strategy parameters automatically in the FMZ strategy interface. The controls supported on FMZ currently include numeric input boxes, text input boxes, drop-down boxes, and Boolean checkboxes. And you can set the strategy parameter grouping, set the parameter prompt text message and other functions.

img

We introduce several main parameters of the input function:

  • defval : The default value for the strategy parameter options set by the input function, supporting Pine language built-in variables, numerical values, and strings.
  • title : The parameter name of the strategy displayed on the strategy interface during live trading/backtesting.
  • tooltip : The tooltip information for strategy parameters, when the mouse hovers over the strategy parameter, the text information of the parameter setting will be displayed.
  • group : The name of the strategy parameter group, which can be used to strategy parameters.

In addition to individual variable declaration and assignment, there is also a way of declaring a group of variables and assigning them in the Pine language:

[Variable A, Variable B, Variable C] = function or structure, such as ```if```, ```for```, ```while``` or ```switch```

The most common one is when we use the ta.macd function to calculate the MACD indicator, since the MACD indicator is a multi-line indicator, three sets of data are calculated. So it can be written as:

[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)

We can draw the MACD chart using the above code easily. Not only the built-in functions can return to multiple variables, but also the written custom functions can return to multiple data.

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)

The writing method of using if and other structures as multiple variable assignments is also similar to the above custom function, and you can try it if you are interested.

[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)

Condition Structure

Some functions cannot be written in the local code block of the conditional branch, mainly including the following functions:

barcolor(), fill(), hline(), indicator(), plot(), plotcandle(), plotchar(), plotshape()

Trading View will compile with errors, FMZ is not as restrictive, but it is recommended to follow the specifications of Trading View. For example, although this does not report an error on FMZ, it is not recommended.

strategy("test", overlay=true)
if close > open 
    plot(close, title="close")
else 
    plot(open, title="open")

if statement

Explanation example:

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)

Key point: Expressions used for judgments that return Boolean values. Note the indentation. There can be at most one else branch. If all branch expressions are not true and there is no else branch, return na.

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

switch statement

The switch statement is also a branch-structured statement, which is used to design different paths to be executed according to certain conditions. Generally, the switch statement has the following key knowledge points.

  1. The switch statement, like the if statement, can also return a value.
  2. Unlike switch statements in other languages, when a switch construct is executed, only a local block of its code is executed, so the break statement is unnecessary (ie, there is no need to write keywords like break).
  3. Each branch of switch can write a local code block, the last line of this local code block is the return value (it can be a tuple of values). Returns na if none of the branched local code blocks were executed.
  4. The expression in the switch structure determines the position can write a string, variable, expression or function call.
  5. switch allows specifying a return value that acts as the default value to be used when there is no other case in the structure to execute.

There are two forms of switch, let’s look at the examples one by one to understand their usage.

  1. A switch with expressions - example explanation:
// input.string: defval, title, options, tooltip
func = input.string("EMA", title="indicator name", tooltip="select the name of the indicator function to be used", options=["EMA", "SMA", "RMA", "WMA"])

// input.int: defval, title, options, tooltip
// param1 = input.int(10, title="period parameter")
fastPeriod = input.int(10, title="fastPeriod parameter", options=[5, 10, 20])
slowPeriod = input.int(20, title="slowPeriod parameter", options=[20, 25, 30])

data = input(close, title="data", tooltip="select the closing price, opening price, highest price...")
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)

We learned the input function before, here we continue to learn two functions similar to input: input.string, input.int functions. input.string is used to return a string, and the input.int function is used to return an integer value. In the example, there is a new usage of the options parameter. The options parameter can be passed an array of optional values, such as options=["EMA", "SMA", "RMA", "WMA"] and options=[5, 10, 20] in the example (note that one is string type, the other is a numeric type). In this way, the controls on the strategy interface do not need to input specific values, but the controls become drop-down boxes to select these options provided in the options parameter.

The value of the variable func is a string, and the variable func is used as the expression for switch (which can be a variable, function call, or expression) to determine which branch in the switch is executed. If the variable func cannot match (ie, equal) the expression on any branch in the switch, the default branch code block will be executed, and the runtime.error("error") function will be executed, causing the strategy to throw an exception and stop.

In our test code above, after the last line of runtime.error in the default branch code block of switch, we did not add code like [na, na] to be compatible with the return value. This problem needs to be considered on Trading View. If the type is inconsistent, an error will be reported. But on FMZ, since the type is not strictly required, this compatibility code can be omitted. Therefore, there is no need to consider the type compatibility of the return value of if and switch branches on FMZ.

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)

No error will be reported on FMZ, but an error will be reported on trading view. Because the type returned by the if branch is inconsistent.

  1. switch without expressions

Next, let’s look at another way to use switch, that is, without expressions.

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)

As we can see from the test code example, switch will match the execution of the local code block that is true on the branch condition. In general, the branch conditions following a switch statement must be mutually exclusive. That is to say, up and down in the example cannot be true at the same time. Because switch can only execute the local code block of one branch, if you are interested, you can replace this line in the code: up = close > open // up = close < open to the content in the comment, and backtest to observe the result. You will find that the switch branch can only execute the first branch. In addition, it is necessary to pay attention not to write function calls in the branch of switch as much as possible, the function cannot be called on each BAR may cause some data calculation problems (unless as in the example of "switch with expressions", the execution branch is deterministic and will not be changed during strategy operation).

Loop Structure

for statement

return value = for count = start count to final count by step length
    statement                                            // Note: There can be break and continue in the statement
    statement                                            // Note: The last statement is the return value

The for statement is very simple to use, the for loop can finally return a value (or multiple values, in the form of [a, b, c]). Like the variable assigned to the “return value” position in the pseudocode above. The for statement is followed by a “count” variable used to control the number of loops, refer to other values, etc. The “count” variable is assigned the “initial count” before the loop starts, then increments according to the “step length” setting, and the loop stops when the “count” variable is greater than the “final count”.

The break keyword used in the for loop: the loop stops when the break statement is executed. The continue keyword used in the for loop: When the continue statement is executed, the loop will ignore the code after continue and execute the next round of the loop directly. The for statement returns to the return value from the last execution of the loop. And it returns null if no code is executed.

Then we demonstrate with a simple example:

ret = for i = 0 to 10       // We can increase the keyword by to modify the step length, FMZ does not support reverse loops such as i = 10 to 0 for now
    // We can add condition settings, use continue to skip, use break to jump out
    runtime.log("i:", i)
    i                       // If this line is not written, it will return null because there is no variable to return
    
runtime.log("ret:", ret)
runtime.error("stop")

for … in statement

The for ... in statement has two forms, we will illustrate them in the following pseudocode.

return value = for array element in array 
    statement                        // Note: There can be break and continue in the statement
    statement                        // Note: The last statement is the return value
Return value = for [index variable, array element corresponding to index variable] in array
    statement                        // Note: There can be break and continue in the statement
    statement                        // Note: The last statement is the return value 

We can see that the main difference between the two forms is the content that follows the for keyword, one is to use a variable as a variable that refers to the elements of the array, the other is to use a structure containing index variables, tuples of array element variables as references. For other return value rules, such as using break, continue, etc., are consistent with for loops. We also illustrate the use with a simple example.

testArray = array.from(10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
for ele in testArray            // Modify it to the form of [i, ele]: for [i, ele] in testArray, runtime.log("ele:", ele, ", i:", i)
    runtime.log("ele:", ele)

runtime.error("stop")

When it need to use the index, use the grammar for [i, ele] in testArray.

Application of for loops

We can use the built-in functions provided in the Pine language to complete some of the loop logic calculations, either written by using the loop structure directly or processed using the built-in functions. Let’s take two examples:

  1. Calculate the mean value

When designing with a loop structure:

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)

The example uses a for loop to calculate the sum and then calculate the mean value.

Calculate the moving average directly using the built-in function:

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

Use the built-in function ta.sma directly to calculate the moving average indicator. Obviously, it is simpler to use the built-in function for calculating the moving average. By comparing on the chart, it can be seen that the calculated results are exactly the same.

  1. Summation

We still use the example above to illustrate.

When designing with a loop structure:

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)

For calculating the sum of all elements in an array, we can use a loop to process it, or use the built-in function array.sum to calculate. Calculate the sum directly using the built-in function:

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)

We can see the calculated data is exactly the same as that displayed on the chart using plot.

So why design loops when we can do all these with built-in functions? The use of loops is mainly based on the application of these 3 points:

  1. For some operations and calculations for arrays.
  2. To review the history, for example, to find out how many past high points are higher than the current BAR’s high point. Since the high point of the current BAR is only known at the BAR where the script is running, a loop is needed to return and analyze the past BARs in time.
  3. When Pine language built-in functions cannot complete calculations for past BARs.

while statemnet

The while statement keeps the code in the loop section executing until the judgment condition in the while structure is false.

return value = while judgment condition
    statement                    // Note: There can be break and continue in the statement
    statement                    // Note: The last statement is the return value

Other rules of while are similar to those of the for loop. The last line of the local code block of the loop body is the return value, which can return multiple values. Execute the loop when the “loop condition” is true, and stop the loop when the condition is false. The break and continue statements can also be used in the loop body.

We will still use the example of calculating moving averages for demonstration:

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)

We can see that the while loop is also very simple to use, and it is also possible to design some calculation logic that cannot be replaced by the built-in functions, such as calculating factorial:

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

Arrays

The definition of arrays in the Pine language is similar to that in other programming languages. Pine’s arrays are one-dimensional arrays. Usually it’s used to store a continuous series of data. The single data stored in the array is called the element of the array, and the types of these elements can be: integer, floating point, string, color value, boolean value. The Pine language on FMZ is not very strict about types, and it can even store strings and numbers in an array at the same time. Since the underlying structure of the array is also a series structure, if the historical operator is used to refer to the array state on the previous BAR. So instead of using the historical operator [] ​​to refer to an element in the array, we need to use the functions array.get() and array.set(). The index order of the elements in the array is that the index of the first element of the array is 0, and the index of the next element is incremented by 1.

We illustrate it with a simple code:

var a = array.from(0)
if bar_index == 0 
    runtime.log("current value a on BAR:", a, ", a on the last BAR, i.e. the value of a[1]:", a[1])
else if bar_index == 1 
    array.push(a, bar_index)
    runtime.log("current value a on BAR:", a, ", a on the last BAR, i.e. the value of a[1]:", a[1])
else if bar_index == 2
    array.push(a, bar_index)
    runtime.log("current value a on BAR:", a, ", a on the last BAR, i.e. the value of a[1]:", a[1], ", a on the last second BAR, i.e. the value of a[2]:", a[2])
else if bar_index == 3 
    array.push(a, bar_index)
    runtime.log("current value a on BAR:", a, ", a on the last BAR, i.e. the value of a[1]:", a[1], ", a on the last second BAR, i.e. the value of a[2]:", a[2], ", a on the last third BAR, i.e. the value of a[3]:", a[3])
else if bar_index == 4 
    // Obtain elements by index using array.get, modify elements by index using array.set
    runtime.log("Before array modification:", array.get(a, 0), array.get(a, 1), array.get(a, 2), array.get(a, 3))
    array.set(a, 1, 999)
    runtime.log("After array modification:", array.get(a, 0), array.get(a, 1), array.get(a, 2), array.get(a, 3))

Declare an array

Use array<int> a, float[] b to declare an array or just declare a variable that can be assigned an array, for example:

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 variables are initialized by using the array.new and array.from functions. There are also many types-related functions similar to array.new in the Pine language: array.new_int(), array.new_bool(), array.new_color(), array.new_string(), etc.

The var keyword also works with the array declaration mode. Arrays declared with the var keyword are initialized only on the first BAR. Let’s observe with an example:

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")

It can be seen that the changes of the array a have been continuously determined and have not been reset. The array b is initialized on each BAR. Finally, when barstate.islast is true, there is still only one element printed with a value of 0.

Read and write elements in an array

Use array.get to get the element at the specified index position in the array, and use array.set to modify the element at the specified index position in the array.

The first parameter of array.get is the array to be processed, and the second parameter is the specified index. The first parameter to array.set is the array to be processed, the second parameter is the specified index, and the third parameter is the element to be written.

We use the following simple example to illustrate:

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")

The example initializes the base color green, declares and initializes an array to store colors, and then assigns different transparency values to the colors (by using the color.new function). The color level is calculated by calculating the distance of the current BAR from the maximum value of high in 100 lookback periods. The closer the distance to the maximum value of HIGH in the last 100 lookback cycles, the higher the rank and the darker (lower transparency) the corresponding color value. Many similar strategies use this method to represent the current price level within N lookback periods.

Iterate through array elements

How to iterate through an array, we can use the for/for in/while statements that we have learned before.

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

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

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

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

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

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

These three traversal methods have the same execution results.

Arrays can be declared in the global scope of a script, or in the local scope of a function or if branch.

Historical data references

For the use of elements in arrays, the following ways are equivalent. We can see by the following example that two sets of lines are drawn on the chart, two in each set, and the two lines in each set have exactly the same value.

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)

Functions for adding and deleting arrays

  1. Functions related to the adding operation of arrays:

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

  1. Functions related to the deleting operation of arrays:

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

We use the following example to test these array adding and deleting operation functions.

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

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

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

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

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

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

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

runtime.error("stop")

Application of additions, deletions: arrays as queues

We can construct a “queue” data structure by using arrays and some functions of adding and deleting arrays. Queues can be used to calculate the moving average of tick prices. Someone may ask, “Why should we construct a queue structure? Didn’t we use arrays to calculate the average before?”

A queue is a structure that is often used in the field of programming, the characteristics of a queue are:

The element that enters the queue first, leaves the queue first.

In this way, it ensures that the data in the queue is the latest data, and that the length of the queue will not expand indefinitely.

In the following example, we use a queue structure to record the price of each tick, calculate the mobile average price at the tick level, and then compare it with the moving average at the 1-minute K-line level.

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")

Note that when declaring array a, we specify the declaration mode and use the keyword variant. In this way, each price change will be recorded in array a.

Array calculation and operation functions commonly used

Calculate correlation functions:

array.avg() calculates the average value of all elements in an array, array.min() calculates the smallest element in an array, array.max()calculates the smallest element in an array, array.stdev() calculates the standard deviation of all elements in an array, array.sum() calculates the standard deviation of all elements in an array.

Operation-related functions: array.concat() to merge or concatenate two arrays. array.copy() to copy the array. array.join to concatenates all the elements of an array into a string. array.sort() to sort by ascending or descending order. array.reverse() to reverse the array. array.slice() to slice the array. array.includes() to judge the element. array.indexof() to return to the index of the first occurrence of the value passed in as a parameter. If the value is not found, -1 will be returned. array.lastindexof() to find the last occurrence of the value.

Test examples of array calculation-related functions:

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

runtime.log("Arithmetic average of the array a:", array.avg(a))
runtime.log("The minimum element in the array a:", array.min(a))
runtime.log("The maximum element in the array a:", array.max(a))
runtime.log("Standard deviation in array a:", array.stdev(a))
runtime.log("Sum of all elements of the array a:", array.sum(a))
runtime.error("stop")

These are commonly used array calculation functions.

Examples of operation-related functions:

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

runtime.log("array a: ", a, ", array b: ", b)
runtime.log("array a, array b is concatenated with:", array.concat(a, b))
c = array.copy(b)

runtime.log("Copy an array b and assign it to the variable c, variable c:", c)

runtime.log("use array.join to process the array c, add the symbol + to the middle of each element, concatenating all elements results in a string:", array.join(c, "+"))
runtime.log("Sort the array b, in order from smallest to largest, using the parameter order.ascending:", array.sort(b, order.ascending))     // array.sort function modifies the original array
runtime.log("Sort the array b, in order from largest to smallest, using the parameter order.descending:", array.sort(b, order.descending))   // array.sort function modifies the original array

runtime.log("array a:", a, ", array b:", b)
array.reverse(a)   // This function modifies the original array
runtime.log("reverse the order of all elements in the array a, after reversing, the array a is:", a)    

runtime.log("Intercept array a, index 0~index 3, and follow the rule of left-closed and right-open interval:", array.slice(a, 0, 3))
runtime.log("Search for element 11 in array b:", array.includes(b, 11))
runtime.log("Search for element 100 in array a:", array.includes(a, 100))
runtime.log("Connect array a and array b, and search the index position of the first occurrence of element 2:", array.indexof(array.concat(a, b), 2), " , observe array.concat(a, b):", array.concat(a, b))
runtime.log("Connect array a and array b, and search the index position of the last occurrence of element 6:", array.lastindexof(array.concat(a, b), 6), " , observe array.concat(a, b):", array.concat(a, b))

runtime.error("stop")

Functions

Custom Functions

The Pine language can be designed with custom functions. In general, the following rules applied to custom functions in the Pine language:

  1. All functions are defined in the global scope of the script. A function cannot be declared within another function.
  2. Functions are not allowed to call themselves in their own code (recursion).
  3. In principle, all PINE language built-in drawing functions (barcolor(), fill(), hline(), plot(), plotbar(), plotcandle()) cannot be called in custom functions.
  4. Functions can be written as single line or multiple lines. The return value of the last statement is the return value of the current function, which can be returned in tuple form.

We have also used the custom functions for many times in our previous tutorials, such as those designed as a single line:

barIsUp() => close > open

Whether the current BAR is a positive line when the function returns.

Custom functions designed to be multiple line:

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)

We use a custom function to realize a function of sma average calculation.

In addition, two examples of custom functions that we can return:

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)

One function can calculate the fast line, slow line and two EMA averages.

Built-in functions

Built-in functions can be easily found in the FMZ PINE Script document.

Classification of built-in functions in the Pine language:

  1. String processing function str. series.
  2. The color value processing function color. series.
  3. Parameter input function input. series.
  4. Indicator calculation function ta. series.
  5. Drawing function plot. series.
  6. Array handling function array. series.
  7. Trading-related functions of the strategy. series.
  8. Math operations related functions math. series.
  9. Other functions (time handling, non-plot series drawing functions, request. series functions, type handling functions, etc.)

Trading Functions

The strategy. series of functions are functions that we often use in the design of strategies, and these functions are closely related to the execution of trading operations when the strategy is running specifically.


  1. strategy.entry

strategy.entry function is a more important function when we write a strategy to place an order, several important parameters for the function are: id, direction, qty, when, etc.

Parameters:

  • id: This can be understood as giving a name to a trading position for referencing. This id can be referenced to cancel, modify orders and close positions.
  • direction: If the direction of the order is long (buy), pass in the built-in variable strategy.long, and if you want to go short (sell), pass in the variable strategy.short.
  • qty: Specify the amount of orders to be placed, if this parameter is not passed, the default amount of orders will be used.
  • when: Execution condition, you can specify this parameter to control whether this current order operation is triggered or not.
  • limit: Specify the order limit price.
  • stop: Stop loss price.

The specific execution details of the strategy.entry function are controlled by the parameter settings when the strategy function is called, and it can also be controlled by the [“Pine Language Trade-Class Library Template Arguments”](https://www.fmz.com/bbs-topic/9293#template- arguments-of-pine-language-trade-class-library) setting control, Pine language trade-class library template arguments control more details of the transaction, you can check the linked documentation for details.

We focus on the the pyramiding, default_qty_value parameters in the strategy function. We use the following code for test:

/*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)

The part at the beginning of the code /* backtest... */ is a backtest setting, which is used to record the backtest setting time and other information at that time for debugging, not the startegy code.

In the code: strategy(title = "open long example", pyramiding = 3, default_qty_value=0.1, overlay=true), when we specify the pyramiding parameter as 3, we set the maximum number of trades in the same direction to 3. So one of the four strategy.entry order operations in the example is not executed. Since we also specified the default_qty_value parameter to be 0.1, this strategy.entry operation with ID long1 has a default order size of 0.1. strategy.entry function call when we specify the direction as strategy.long, so the backtest test orders are all buy orders.

Note that the order operation strategy.entry("long3", ... in the code is called twice, for the same ID: long3, the first strategy.entry order operation was not filled, and the second call to the strategy.entry function was to modify the order for this ID (the data shown in the backtest test also shows that the order quantity for this limit order was modified to 0.3). Another case, for example, if the first order with ID “long3” is filled, continue to use the strategy.entry function to place orders according to the ID “long3”, then the order positions will be accumulated on the ID “long3”.


  1. strategy.close

The strategy.close function is used to close the entry position with the specified identification ID. The main parameters are: id, when, qty, qty_percent.

Parameters:

  • id: The entry ID that needs to be closed is the ID that we specify when we open a position using an entry order function, such as strategy.entry.
  • when: Execution conditions.
  • qty: Number of closed positions.
  • qty_percent: Percentage of closed positions.

Let’s familiarize with the details of the use of this function through an example: The /*backtest ... */ in the code is the configuration information for FMZ.COM international website backtest, you can delete it and set the market, variety, time range and other information you need to test.

/*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")                   // Multiple entry orders, no qty parameters specified, close all
    // strategy.close()                          // Without specifying the id parameter, the current position will be closed
    // strategy.close("long2")                   // If a non-existent id is specified then nothing is done
    // strategy.close("long1", qty=0.15)         // Specify qty parameters to close a position
    // strategy.close("long1", qty_percent=50)   // qty_percent is set to 50 to close 50% of the positions marked by long1
    // strategy.close("long1", qty_percent=80, when=close<open)  // Specify the parameter when, change it to close>open and it won't trigger
    enableStop := true

The test strategy shows three consecutive long entries with the entry ID “long1”, and then use different parameters of the strategy.close function to set the different results of the backtest when closing a position. It can be found that strategy.close function has no parameters to specify the price of the order to close the position, this function is mainly used to close the position immediately at the current market price.


  1. strategy.close_all

The function strategy.close_all is used to close all current positions, because the Pine language script positions can only have a direction, that is, if there is a signal triggered in the opposite direction of the current position will close the current position and then open it according to the signal trigger. So strategy.close_all will close all the positions in the current direction when it is called. The main parameter of the strategy.close_all function is: when.

Parameters:

  • when: Execution conditions.

Let’s use an example to observe:

/*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 

The test code starts with a position number of 0 (i.e. strategy.position_size==0 is true), so when the conditions set by the when parameter are met, only the strategy.entry entry function with ID “long” is executed. After holding a long position, strategy.position_size is greater than 0, then the entry function with ID “short” may be executed, since the current long position is held, this shorting reversal signal at this time will result in closing the long position and then opening the short position in the opposite direction. Then we write in the if condition that when strategy.position_size < 0, i.e. when holding a short position, all positions in the current holding direction will be closed. And mark enableStop := true. Stops the strategy execution so that the log can be observed.

It can be found that the function strategy.close_all has no parameters to specify the price of closing the order, this function is mainly used to immediately close the position at the current market price.


  1. strategy.exit

The strategy.exit function is used to close an entry position. Unlike this function, the strategy.close and strategy.close_all functions close a position immediately at the current market price. The strategy.exit function will close the position according to the parameter settings.

Parameters:

  • id: The order identifier ID of the current closeout condition order.
  • from_entry: Used to specify the entry ID of the position to be closed.
  • qty: Number of closed positions.
  • qty_percent: Percentage of closed positions, range: 0 ~ 100.
  • profit: Profit target, expressed in points.
  • loss: Stop loss target, expressed in points.
  • limit: Profit target, specified by price.
  • stop: Stop loss target, specified by price.
  • when: Execution conditions.

Use a test strategy to understand the parameter usage.

/*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")          // If only one id parameter is specified, the exit order is invalid, and the parameters profit, limit, loss, stop and other exit conditions also need to be set at least one, otherwise it is also invalid
    strategy.exit("exit1", "long1", profit=50)                    // Since the long1 entry order is not filled, the exit order with ID exit1 is also on hold until the corresponding entry order is filled before exit1 is placed
    strategy.exit("exit2", "long2", qty=0.1, profit=100)          // Specify the parameter qty to close 0.1 positions in the position with ID long2
    strategy.exit("exit3", "long3", qty_percent=50, limit=strategy.opentrades.entry_price(findOrderIdx("long3")) + 1000)   // Specify the parameter qty_percent to close 50% of the positions in the position with ID long3
    isExit := true 

if bar_index == 0 
    runtime.log("The price per point:", syminfo.mintick)    // The price per point is related to the "Pricing Currency Precision" parameter setting on the Pine language template parameters

We use the real-time price model for backtest, the test strategy starts with 3 entry operations (strategy.entry function), and long1 is intentionally set with limit parameter with a pending order price of 1, so that it cannot be filled. Then test the conditional exit function strategy.exit. We used the ‘‘take profit by point’’ and ‘‘take profit by price’’, the close a fixed number of positions, and the close positions by percentage. Given the length of the example, only the “take profit” is demonstrated. The stop-loss operation is also the same. The strategy.exit function also has more complex trailing stop parameters: trail_price, trail_points, trail_offset can also be tested in this example to learn their usage.


  1. strategy.cancel

The strategy.cancel functions are used to cancel/stop all pre-pending orders. These functions: strategy.order, strategy.entry , strategy.exit can generate entry IDs. The main parameters of this function are: id, when.

Parameters:

  • id: The admission ID to be cancelled.
  • when: Execution conditions.

This function is easy to understood, and it is used to cancel entry orders that are not filled.

/*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

More