Configuring a TradingView strategy is done by hand or programmatically. One setting is the strategy’s pyramiding, which specifies how many additional entries in the same direction are allowed. But confusingly enough, TradingView doesn’t always respect that setting. Why does that happen and how can we solve that?

In this article:

TradingView’s pyramiding and multiple orders in the same direction

We configure a TradingView strategy with code or by hand, and with the former our script is always added with the correct settings to the chart. The strategy() function specifies a strategy’s settings programmatically. Each strategy script needs to use this function (Pine Script Language Tutorial, n.d.) and its title argument, which sets the strategy’s name, always has to be defined too (TradingView, n.d.).

Another argument of that function is pyramiding, and this one configures a strategy’s pyramiding. That is, it sets how many entries in the same direction are allowed (TradingView, n.d.). This argument has a default value of 0, and in that case only one entry in the same direction is allowed.

But that’s not to say that TradingView always respects our pyramiding setting (see Pine Script Language Tutorial, n.d.). Say we set pyramiding to 3. In that case, confusingly enough positions can still have more than 3 entries in the same direction. There are two situations in which this can happen, and each relates to the function that we use to submit additional entry orders:

  • When the strategy.entry() function generates an order, it takes the strategy’s pyramiding setting into account by checking the size of the currently open position. But when we submit several orders quickly after each other, each can be approved individually – but when they’re all filled, the open position can be bigger than allowed by the pyramiding setting (see Pine Script Language Tutorial, n.d.). We explore this behaviour and the solutions for it in TradingView doesn’t respect the pyramiding setting with strategy.entry().
  • We can also submit entry orders with strategy.order(), but this function doesn’t take the strategy’s pyramiding setting into account (TradingView, n.d.). So orders generated by this function are filled regardless of the current number of open entries. As we’ll see in this article, that means we’ll need to manage the strategy’s pyramiding ourselves and cannot rely on the pyramiding argument of the strategy() function.
Note: The difference between strategy.entry() and strategy.order() is that the latter doesn’t take the strategy’s pyramiding and risk management settings into account (Pine Script Language Tutorial, n.d.). This gives more flexibility – orders generated with strategy.order() are always submitted – but provides risks too if we don’t take these features into account.

The programming example below uses the strategy.order() function to generate multiple entries in the same direction. After discussing that script, we’ll explore how to adjust it so that orders submitted with that function don’t open a position that’s too big.

Example: opening bigger positions than the pyramiding setting

The example strategy below opens a long position whenever the 5-bar SMA (Simple Moving Average) crosses above the 10-bar SMA while the 9-bar RSI (Relative Strength Index) is above 50 at that time. When the strategy is already long and the RSI crosses above 70, we’ll add an additional long contract to the position. With the pyramiding setting we allow one additional entry in the same direction. We exit positions whenever the quick moving average drops below the slower one while the RSI is below 50.

The chart below shows the strategy’s behaviour with the initial entry order and exit long order highlighted. After discussing the code of this long-only strategy, we examine more charts and explore how strategy.order() creates bigger positions than allowed by the pyramid setting.

Example TradingView strategy: multiple entries in the same direction despite the pyramiding setting
//@version=2
strategy(title="Pyramiding and strategy.order()", pyramiding=1)

// Input options
quickMALen = input(title="Quick MA Length", type=integer, defval=5)
slowMALen  = input(title="Slow MA Length", type=integer, defval=10)
rsiLen     = input(title="RSI Length", type=integer, defval=9)

// Compute values
quickMA  = sma(close, quickMALen)
slowMA   = sma(close, slowMALen)
rsiValue = rsi(close, rsiLen)

// Plot values
plot(series=rsiValue, color=blue)
hline(price=50, color=#87CEEB)
hline(price=80, color=#87CEEB)
hline(price=20, color=#87CEEB)

// Order conditions
enterLong = crossover(quickMA, slowMA) and (rsiValue > 50)
addToLong = (strategy.position_size > 0) and crossover(rsiValue, 70)
exitLong  = crossunder(quickMA, slowMA) and (rsiValue < 50)

// Submit long orders
if (enterLong)
    strategy.order(id="Enter Long", long=true)

if (addToLong)
    strategy.order(id="Additional Long", long=true)

// Exit long position
if (exitLong)
    strategy.close_all()

We start with a comment saying @version=2. This way the script uses the second version of Pine, which makes it possible to use if statements later on in the code (Pine Script Language Tutorial, n.d.).

Then we programmatically configure the strategy with strategy(). With this function’s title argument we set the strategy’s name. And with the pyramiding argument set to 1 we allow one additional entry in the same direction (TradingView, n.d.).

Then we add several input options to the script:

quickMALen = input(title="Quick MA Length", type=integer, defval=5)
slowMALen  = input(title="Slow MA Length", type=integer, defval=10)
rsiLen     = input(title="RSI Length", type=integer, defval=9)

Configurable inputs are made with input(), and this function also returns the option’s current value (TradingView, n.d.). With the assignment operator (=) we store those values in a variable. That way we can use the input’s value in other parts of the script by referring to the variable.

Our three input options are all numerical integer inputs. These accept whole numbers only and are made by setting the type argument of the input() function to integer (Pine Script Language Tutorial, n.d.). Other arguments that they share are title and defval. The first sets the name that’s placed before the input in the script’s settings (see image further down below), while the latter gives the input a default value (TradingView, n.d.).

The first two inputs set the length of the moving averages and are named “Quick MA Length” and “Slow MA Length”. We give these default values of 5 and 10, and store their values in the quickMALen and slowMALen variables for use later on. The last input, “RSI Length”, begins with a value of 9 and is tracked with the rsiLen variable.

Next we compute the strategy’s values:

quickMA  = sma(close, quickMALen)
slowMA   = sma(close, slowMALen)
rsiValue = rsi(close, rsiLen)

We calculate the moving averages with sma(), a function that works on two arguments: a series of values to process and an integer that sets the number of bars to calculate on (TradingView, n.d.).

For the fast moving average we have sma() compute on the bar’s closing prices (close) with a length of quickMALen, our input variable that defaults to 5. We assign that moving average’s value to the quickMA variable. The slower moving average also uses closing prices but has its length set with slowMALen. This 10-bar SMA is stored in the slowMA variable.

We calculate the value of the Relative Strength Index with rsi(). That function uses two arguments: close and rsiLen, for the series of values to calculate on and the RSI length in number of bars (TradingView, n.d.). This 9-bar RSI is assigned to the rsiValue variable to use later on.

Then we plot the strategy’s values on the chart:

plot(series=rsiValue, color=blue)
hline(price=50, color=#87CEEB)
hline(price=80, color=#87CEEB)
hline(price=20, color=#87CEEB)

The plot() function plots the data of its series argument as a continuous line by default (TradingView, n.d.). Here we set that argument to the rsiValue variable to plot the 9-bar RSI. That line’s colour is set to the blue basic TradingView colour.

To help reading the RSI values, we also plot three lines with hline(). That function creates a horizontal line that’s at the price level set by its price argument (TradingView, n.d.). With the three hline() function calls we place lines at 50 (the level that triggers trades) as well as 80 and 20, the RSI overbought and oversold levels. Each line has its color argument set to #87CEEB, the hexadecimal colour value of sky blue.

After that we determine the order conditions:

enterLong = crossover(quickMA, slowMA) and (rsiValue > 50)
addToLong = (strategy.position_size > 0) and crossover(rsiValue, 70)
exitLong  = crossunder(quickMA, slowMA) and (rsiValue < 50)

We create three true/false variables here. Each is assigned a value by combining two true/false expressions with and. That logical operator only returns true when the value its left and the value on its right are both true too; when one or both expressions are false, then and returns false too (Pine Script Language Tutorial, n.d.). Since each of the two true/false expressions reflects a situation, both things have to occur at the same time before that variable becomes true.

The value of the enterLong variable depends on two things. First, the 5-bar moving average has to cross above the 10-bar SMA. We evaluate this with the crossover() function and the quickMA and slowMA variables. That function returns true when, on the current bar, the value of the first argument is greater than the second while, on the previous bar, the first argument’s value was still below that of the second (TradingView, n.d.).

The other situation that affects the value of enterLong is whether the 9-bar RSI (rsiValue) is greater than (>) 50. Since we combine both situations with and, our variable for opening a long position (enterLong) only holds true when there’s a moving average crossover on the current bar while the RSI is above 50.

The second true/false variable is addToLong. Before we add to an existing long position, two things have to happen. First, the strategy has to be long already. We check that by evaluating whether the strategy.position_size variable returns a value greater than (>) 0. That variable returns a positive value when the strategy is long, a negative number when the strategy is short, and 0 when the script currently doesn’t have an open position (TradingView, n.d.).

The other situation that sets the value of addToLong is whether the 9-bar RSI (rsiValue) has crossed above 70. We use the crossover() function to see whether such a crossover occurred. When this happened, and when the strategy is already long, then addToLong is assigned a value of true (and false otherwise).

Likewise, the exitLong true/false variable also depends on two situations before we’ll close our long position. The first is whether the 5-bar SMA (quickMA) dropped below the 10-bar moving average (slowMA). We evaluate this with crossunder(). That function returns true when the value of its first argument is less than the second argument on the current bar, while on the previous bar the first argument’s value was still above that of the second (TradingView, n.d.).

The other situation that exitLong depends on is whether the 9-bar RSI (rsiValue) is less than (<) 50. We use the exitLong variable later on so that when both of these things happen on the same bar, our script closes the open long position.

But before we can close positions, we have to submit the enter long orders:

if (enterLong)
    strategy.order(id="Enter Long", long=true)

if (addToLong)
    strategy.order(id="Additional Long", long=true)

We submit the strategy’s orders conditionally with two if statements. The first evaluates whether the enterLong variable is true, which is the case when the 5-bar SMA crossed above the 10-bar moving average and the 9-period RSI is above 50. In that situation, we submit an order with strategy.order().

That function is used with two arguments here. With id we specify the order identifier, and its value of “Enter Long” displays as the order name on the chart and in the ‘Strategy Tester’ window. And the long argument, when set to true, makes strategy.order() submit a buy (enter long) order (TradingView, n.d.).

The second if statement is similar. Here we evaluate the addToLong variable, which holds true when the strategy is long and the RSI crossed above 70. When that happens, we call strategy.order() to submit a second long order named “Additional Long”.

The last part of the example closes our open positions:

if (exitLong)
    strategy.close_all()

This if statement processes the exitLong variable, which holds true whenever the 5-bar SMA dropped below the 10-period moving average with the RSI below 50. When that exit long signal happens, we call strategy.close_all(). This function exits the strategy’s current market position. And without a position to flatten, this function doesn’t do anything (TradingView, n.d.).

Example charts: submitting TradingView orders with strategy.order()

Our above example strategy has the following input options:

Input options of the TradingView example strategy

Let’s verify the strategy’s pyramiding settings. For that we click on the gear icon ( ) that’s displayed to the right of the strategy name on the chart. This opens a window with strategy settings and there, in the ‘Properties’ tab section, we see that our strategy allows pyramiding with up to 1 entry in the same direction:

Manual setting of the TradingView example strategy

But that’s not what we see when we look at the chart:

Multiple entries with the TradingView strategy, despite the pyramiding setting

Here the strategy enters a long position (first orange arrow) and closes it several bars later (second arrow). But instead of filling one additional entry order while the long position is open, we see the script submitting 8 “Additional Long” orders.

This happens even though we set the pyramiding argument of the strategy() function to 1:

strategy(title="Pyramiding and strategy.order()", pyramiding=1)

On the other parts of the chart the strategy behaves in the same way:

Multiple entries with our TradingView strategy

Since we configured the strategy’s pyramiding with pyramiding=1, the maximum position size should be 2: the initial long entry plus one additional entry in the same direction (TradingView, n.d.). The ‘Strategy Tester’ window, however, shows that the maximum position size was 10 contracts:

Strategy performance summary of the TradingView example strategy

So our strategy is clearly not behaving like it should. How can we change its behaviour so it doesn’t scale into positions more often than it should?

  • First, we can submit our strategy’s orders with strategy.entry(). That function does respect the strategy’s pyramiding setting whereas strategy.order() doesn’t (TradingView, n.d.).
  • Second, we can add additional code to our strategy so that it doesn’t submit additional entry orders with strategy.order() when there are already enough entries in the current position.

While the first approach is the easiest to implement, the second is something that we can learn more from. So let’s see how we can take the strategy’s number of entries into account while submitting orders with strategy.order().

Example: track a TradingView strategy’s number of open entries

To prevent our example strategy from opening positions that are bigger than the strategy’s pyramiding setting specifies, we how we call strategy.order(). By not having that function execute repeatedly, the strategy won’t submit too many orders and we don’t end up with a position that’s too big. Since we use strategy.order() twice (with the initial order and the scale-in order), we have to make two changes too.

First, we should only submit our initial entry order when the strategy is flat. To see if that’s the case, we can use strategy.position_size. That built-in variable returns the direction and size of the strategy’s current market position: when the strategy is long, this variable returns a positive value with the number of open long contracts. If the strategy is short, then strategy.position_size returns a negative value with the short position size. And when the strategy is flat, it returns 0 (TradingView, n.d.).

Second, our scale-in order should only be submitted once and not repeatedly. After all, with pyramiding=1 we only want one entry in the same direction. To implement this, we retrieve how many orders in the same direction are currently filled. When that number is 1, only one order has filled and there’s still room for a second one. To retrieve how many entries there are in the current position, we use the strategy.opentrades built-in variable (TradingView, n.d.).

By implementing these two changes, the previous code example becomes:

//@version=2
strategy(title="Pyramiding and strategy.order()", pyramiding=1)

// Input options
quickMALen = input(title="Quick MA Length", type=integer, defval=5)
slowMALen  = input(title="Slow MA Length", type=integer, defval=10)
rsiLen     = input(title="RSI Length", type=integer, defval=9)

// Compute values
quickMA  = sma(close, quickMALen)
slowMA   = sma(close, slowMALen)
rsiValue = rsi(close, rsiLen)

// Plot values
plot(series=rsiValue, color=blue)
hline(price=50, color=#87CEEB)
hline(price=80, color=#87CEEB)
hline(price=20, color=#87CEEB)

// Order conditions
enterLong = (strategy.position_size == 0) and 
     crossover(quickMA, slowMA) and (rsiValue > 50)
addToLong = (strategy.opentrades == 1) and crossover(rsiValue, 70)
exitLong  = crossunder(quickMA, slowMA) and (rsiValue < 50)

// Submit long orders
if (enterLong)
    strategy.order(id="Enter Long", long=true)

if (addToLong)
    strategy.order(id="Additional Long", long=true)

// Exit long position
if (exitLong)
    strategy.close_all()

With the majority of the code untouched, let’s discuss the only two lines of code that we did change. Previously, the enterLong and addToLong variables had their values determined like this:

enterLong = crossover(quickMA, slowMA) and (rsiValue > 50)
addToLong = (strategy.position_size > 0) and crossover(rsiValue, 70)

Now in the updated example, we set their values as follows:

enterLong = (strategy.position_size == 0) and 
     crossover(quickMA, slowMA) and (rsiValue > 50)
addToLong = (strategy.opentrades == 1) and crossover(rsiValue, 70)

With an additional expression we made both the open long (enterLong) and scale-in order condition (addToLong) more strict. By adding those expressions to the existing ones with the and logical operator, each true/false expression has to be true before the entire condition is true. And when one of the several true/false expressions that determine the values of these variables is false, then enterLong or addToLong is assigned a value of false too (Pine Script Language Tutorial, n.d.).

For the enterLong variable we now require – besides a moving average crossover and RSI above 50 – that the strategy is currently flat. This is something we check with strategy.position_size, a variable that returns a value equal to (==) 0 when the strategy doesn’t have an open position (TradingView, n.d.). That means enterLong is only assigned true when the strategy is flat. Given that we use this variable later on as a requirement to submit an order with strategy.order(), it also means that function only submits an order when the strategy is flat to begin with.

The other variable, addToLong, that acts as a condition for another strategy.order() function call is changed too. To not open more than one additional entry in the same direction, we add an additional true/false expression that requires that strategy.opentrades returns a value that’s equal to (==) 1. That built-in variable returns the number of open entries in the strategy’s currently open position, and returns 0 when the strategy is flat (TradingView, n.d.). By seeing whether this variable returns a value that’s 1, we ensure that this expression only becomes true when there’s one entry in the current market position. This makes our strategy behave like the value we set to the pyramiding argument of the strategy() function, by only taking one entry order in the same direction.

Note: Our example strategy submits all orders with strategy.order(). This function does not take the strategy’s pyramiding into account whereas strategy.entry() does take that argument’s value into consideration (TradingView, n.d.). This means we don’t even need to use the pyramiding argument of the strategy() function since strategy.order() isn’t influenced by it. So with pyramiding set to 2 or 250, strategy.order() just submits orders.

The two changes of the enterLong and addToLong variables were the only changes we made. Now let’s see how the adjusted strategy behaves on the chart.

Limit the amount of TradingView entries by strategy.order()

To see the updated script perform in action, let’s compare how the strategy behaved previously and how it performs now. Earlier it generated orders like this:

Previously: multiple entries in the same direction with the TradingView strategy

Here we see a lot of additional trades between the initial entry and exit long order, even though we wanted one additional entry order. The updated script version behaves like:

Only one additional TradingView entry by filtering the entry orders programmatically

Here there’s one long entry (“Enter Long”), one scale-in order (“Additional Long”), and an exit long order (“Close Position”) – exactly as we intended.

Another example of the previous erroneous behaviour is:

Previously: multiple entries in the same direction, despite the pyramiding option

With the new, updated code the strategy only fills one additional order between each entry and exit order:

Now only one additional entry with each trading position in TradingView

We see this correct behaviour also in the ‘Strategy Tester’ window, where the maximum number of contracts held is now 2:

Strategy performance overview of the TradingView example strategy

Summary

The strategy() function configures the strategy’s properties and needs to be added to every strategy script. One optional argument of this function is pyramiding, which specifies how many entries in the same direction are allowed. This argument defaults to 0 (meaning, no additional entries) while pyramiding=2, for instance, allows two more entries in the same direction. When we submit an order with strategy.entry(), TradingView evaluates the pyramiding setting before approving the order. The strategy.order() function, however, doesn’t take the pyramiding setting into account. And so we can end up with a position that’s too big when submitting entry orders with strategy.order(). With additional logic in our order conditions, we can prevent that from happening. Two helpful built-in variables for this are strategy.position_size and strategy.opentrades. The first returns the size and direction of the strategy’s open position: positive values signal the number of open long contracts, negative values tell the short position size, and 0 is returned when the strategy is flat. strategy.opentrades, on the other hand, tells how many entries there are in the currently open position. This way we can do our own pyramiding filtering prior to submitting an order with strategy.order(). Since that function doesn’t take the pyramiding argument of strategy() into account, using that argument has no effect when our script only submits entry orders with strategy.order().

Learn more:


References

Pine Script Language Tutorial (n.d.). Retrieved on February 24, 2016, from https://docs.google.com/document/d/1sCfC873xJEMV7MGzt1L70JTStTE9kcG2q-LDuWWkBeY/

TradingView (n.d.). Script Language Reference Manual. Retrieved on April 29, 2016, from https://www.tradingview.com/study-script-reference/