The standard behaviour of a TradingView strategy is to calculate on the close of each price bar. But our trading script can also process every real-time price update. With that setting, however, the strategy’s backtest performance becomes irrelevant. Let’s see why that’s the case.

In this article:

Configuring a TradingView strategy’s calculation frequency

Specifying the settings of a TradingView strategy programmatically is done with the strategy() function, which has to be present in every strategy’s code (Pine Script Language Tutorial, n.d.). And its title argument, which specifies the strategy’s name, always needs to be set too (TradingView, n.d.).

Another argument of strategy() is calc_on_every_tick. This argument defaults to false, and in that case the strategy calculates on the close of every bar during backtesting and real-time trading (Pine Script Language Tutorial, n.d.). But when we set calc_on_every_tick to true, then the strategy calculates on every real-time price update (TradingView, n.d.).

That feature allows for much greater speed: instead of ‘waiting’ till the bar closes, the strategy can perform its calculations as many times as there are price updates in a bar. This makes the time between subsequent calculations much shorter, especially on higher time frames. However, these intra-bar calculations are not done on historical data (Pine Script Language Tutorial, n.d.). On those historical bars, the strategy calculates only at the bar’s close (meaning, calculating just once per bar) – regardless of whether calc_on_every_tick is enabled or disabled.

This has an important consequence: with calc_on_every_tick enabled, the backtest results of nearly all strategies are unusable and completely irrelevant. Because TradingView calculates those strategies differently on historical bars compared to real-time data, it often seems we have two different strategies when we compare the historical and real-time performance of a strategy. In fact, with calc_on_every_tick=true only results collected when the strategy runs on real-time data give any indication of how the strategy can perform during live trading.

The difference that we typically see when comparing historical to real-time performance of those strategies is much quicker scaling into the maximum position size set by the pyramiding setting. Furthermore, on real-time data an order condition can be true inside the bar, only to be invalidated when the bar closes. That means the strategy would have traded that bar in real time, but won’t trade the same bar during the historical backtest (because there wasn’t an order signal when the bar closed).

For a better understanding of how backtest results of a strategy that calculates with every real-time tick cannot be compared with real-time performance, let’s first create an example strategy. After that we’ll see how it behaves on real-time data and then perform a historical backtest on those same bars to see the difference.

Example strategy: trading extreme prices with every real-time tick

The example strategy below trades whenever the highest high or lowest low is penetrated. That is, when the bar’s high is above the highest high of the preceding 15 bars, we submit an enter long market order. Likewise, with a bar’s low less than the lowest low of the previous 15 bars, we initiate a short position. Since there are no other exits, we’re always in the market with this strategy. We furthermore allow up to 7 entries in the same direction and have the strategy calculate on every real-time tick.

A quick view of the strategy’s behaviour on real-time data (first chart) and historical data (second chart) is given below. After discussing the code we’ll take a closer look at the difference between real-time behaviour and historical performance.

Example of the TradingView strategy's behaviour on real-time data Example of the TradingView strategy's behaviour on historical data
//@version=2
strategy(title="Trading with every tick", overlay=true,
     calc_on_every_tick=true, pyramiding=7)

// Inputs
highestLen = input(title="Highest Length", type=integer, defval=15)
lowestLen = input(title="Lowest Length", type=integer, defval=15)

// Compute values
highestHigh = highest(high, highestLen)[1]
lowestLow   = lowest(low, lowestLen)[1]

timeFilter = (year == 2016) and (month == 7) and 
     (dayofmonth == 15) and (time(period, "830-1200") > 0)

// Plot values
plot(series=highestHigh, color=green)
plot(series=lowestLow, color=red)

bgcolor(color=timeFilter ? orange : na, transp=90)

// Submit orders
if (timeFilter and high > highestHigh)
    strategy.entry(id="Enter Long", long=true)

if (timeFilter and low < lowestLow)
    strategy.entry(id="Enter Short", long=false)

We start with a comment saying @version=2. This specifies that the script should use the second version of Pine, which makes using if statements possible (Pine Script Language Tutorial, n.d.).

Then we configure the strategy’s settings with strategy(). With this function’s title argument we name the strategy and, by setting the overlay argument to true, have the strategy display on the price chart (TradingView, n.d.). With calc_on_every_tick=true the strategy processes every real-time tick, and with the pyramiding argument we allow up to 7 entries in the same direction.

Note: When we enable calc_on_every_tick, the strategy evaluates real-time price bars as if they were already closed. This means built-in variables like close return the current, latest price of the real-time bar. That ‘closing’ price of the last bar of the chart is then updated with every tick. Since TradingView doesn’t know whether the recent tick is the last of the bar or if more price updates will follow, each new price update ‘extends’ the price bar’s close.

Next we create two input options:

highestLen = input(title="Highest Length", type=integer, defval=15)
lowestLen = input(title="Lowest Length", type=integer, defval=15)

User-configurable inputs are added with input(), and this function also returns the input’s current value (Pine Script Language Tutorial, n.d.). Here we store those values in a variable with the assignment operator (=). That way we can access the input’s value later on by referring to the variable.

Both input options are 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 the input() function calls have in common are title and defval. The first sets the name displayed before the input in the script’s settings; the second sets the input’s default value (TradingView, n.d.).

We name the first input “Highest Length” and track its value in the highestLen variable. The value of the “Lowest Length” input is assigned to the lowestLen variable.

Both input variables are used when calculating the highest high and lowest low:

highestHigh = highest(high, highestLen)[1]
lowestLow   = lowest(low, lowestLen)[1]

We set the highestHigh variable to the value returned by highest(). This function returns the highest value for a recent number of bars, and requires two arguments: a series of values and an integer with the amount of bars to calculate on (TradingView, n.d.). Here we set those arguments to the bar’s high prices (high) and the highestLen input variable, which we gave a default value of 15.

But when highest() calculates on high prices, it also uses the price of the current bar. That would be problematic for us since we go long when the bar’s high is greater than the highest high. And that situation cannot happen when the highest high includes the current bar’s high.

We solve that by placing the history referencing operator ([]) with a value of 1 just behind the highest() function call. This makes the function not return the highest high value for the current bar, but the function’s value on the previous bar (see Pine Script Language Tutorial, n.d.). That previous bar’s highest high value is what we store in the highestHigh variable for use later on.

We calculate the lowest low value in much the same way. For this we use lowest(), a function that can work on two arguments: a series of values to process and an integer that sets the number of bars to calculate on (TradingView, n.d.). We set those arguments to the bar’s low prices (low) and lowestLen, the input variable that we gave a standard value of 15 earlier. Then with the help of the history referencing operator we assign the 15-bar lowest low to the lowestLow variable.

Then we make a time filter for the strategy’s trades:

timeFilter = (year == 2016) and (month == 7) and 
     (dayofmonth == 15) and (time(period, "830-1200") > 0)

The purpose of this timeFilter true/false variable is to have the strategy only trade during a short period of time. That way the strategy doesn’t have to run on an extended period of time to collect its real-time results, and that makes it easier for us to compare those results with the historical backtest report. With this filter, our example strategy should only trade between 8:30 and 12:00 hour on July 15, 2016.

To translate that time window into code, we set the value of timeFilter to 4 true/false expressions joined together with the and logical operator. This operator returns true whenever the value on its left and the value on its right are both true too; now when one or both values are false, then and returns false too (Pine Script Language Tutorial, n.d.). This means each expression has to be true before the value of timeFilter is true too.

The first expression evaluates if the year variable, which returns the bar’s year in the exchange’s time zone (TradingView, n.d.), equals (==) 2016. With the second expression we check if month (the bar’s month in exchange time zone; TradingView, n.d.) is equal to (==) 7. And the third expression processes dayofmonth to see whether the current bar’s date in exchange time zone is 15 (TradingView, n.d.). Combined, these three expressions check if the bar’s date is July 15, 2016.

The last true/false expression that affects the value of timeFilter evaluates whether the bar’s time is within a certain time window (8:30 till 12:00 in the morning). For this we use time(), a function that either returns the current bar’s time when it falls inside the specified range, or a “not a number” (NaN) value when the bar is outside that range (Pine Script Language Tutorial, n.d.). This means time() returns some numerical value when the bar is inside the range, and a non-numerical value when it isn’t.

The time() function requires two arguments: the bar’s resolution and a string with the time range that the bar should fall in (TradingView, n.d.). We set these arguments to period, a built-in variable that returns the chart’s current resolution (TradingView, n.d.), and "830-1200" to specify the 8:30-12:00 time range. Now when time() returns a value that’s greater than (>) 0, we know that the current bar is inside the specified session.

So these 4 true/false expressions combined mean that the timeFilter variable holds true whenever the current bar falls inside the 8:30-12:00 session on July 15, 2016.

Next we plot the strategy’s values for visual confirmation:

plot(series=highestHigh, color=green)
plot(series=lowestLow, color=red)

bgcolor(color=timeFilter ? orange : na, transp=90)

The plot() function displays the values of its series argument as a line by default on the chart (TradingView, n.d.). Here we use that function twice. First we plot the 15-bar highest high (highestHigh) values on the chart in the green basic TradingView colour. And with the second plot() statement we display the 15-bar lowest low (lowestLow) values as a red line.

To help identify which price bars falls inside our time range and which ones don’t, we also colour the chart’s background from top to bottom. We do that with the bgcolor() function (Pine Script Language Tutorial, n.d.). Since we only want to colour the background of bars inside our time range (and not the background of bars outside the range), we need to colour the chart’s background conditionally.

For that we use the conditional (ternary) operator (?:) that works on three values. The first is a true/false expression that, when true, makes the operator return its second value. When that expression is false, then the operator returns its third and last value (Pine Script Language Tutorial, n.d.). This makes the conditional operator work like an if/else statement: ‘if this is true, then return A; else return B’.

In our case, the color argument of the bgcolor() function is set to the value returned by the conditional operator. And that makes our background change its colour conditionally. The expression that’s evaluated by the conditional operator here is the timeFilter variable. Now when the current price bar falls inside our time range, that variable holds true and the conditional operator returns the orange basic TradingView colour.

When timeFilter is false, the conditional operator returns na. That built-in variable returns a “not a number” value (TradingView, n.d.) which, when used as a colour, gives a transparent effect (Pine Script Language Tutorial, n.d.). This way we either colour the chart’s background when the bar is inside the time range, or apply no colour when the bar is outside that range.

We also set the transp argument of the bgcolor() function, and this argument specifies the transparency of the background with a value from 0 (no transparency) to 100 for full invisibility (TradingView, n.d.). With a value of 90 we create a highly transparent orange background.

We end our example by submitting the strategy’s orders:

if (timeFilter and high > highestHigh)
    strategy.entry(id="Enter Long", long=true)

if (timeFilter and low < lowestLow)
    strategy.entry(id="Enter Short", long=false)

To submit orders conditionally, we use two if statements. Both evaluate two true/false expressions that are combined with the and logical operator. This means both expressions have to be true before TradingView sees their combined result as true too (Pine Script Language Tutorial, n.d.).

The first if statement checks if the current bar falls inside our specified time range (timeFilter) and whether there’s a price breakout to the upside. In our example we defined that latter as the situation in which the bar’s high (high) is greater than (>) the highest high of the preceding 15 bars (highestHigh). Now when both situations occur, we submit an order with strategy.entry().

That function enters into a position with a market order by default and, when there’s already an open position in the other direction, reverses that open position (TradingView, n.d.). We use strategy.entry() with two arguments here. id specifies the order identifier, and this name appears on the chart and in the ‘Strategy Tester’ window. And the long argument, when set to a value of true, makes the function submit an enter long order while long=false has strategy.entry() submit an enter short order (TradingView, n.d.). We set these two arguments to “Enter Long” and true here to submit an enter long market order.

The next if statement and subsequent strategy.entry() function call are much the same. Here we evaluate whether the bar is in the specified time range (timeFilter) and whether the bar’s low is less than the lowest low of the previous 15 bars (low < lowestLow). When this is the case, we submit a short order (long=false) named “Enter Short” with strategy.entry().

This ends our discussion of the programming example. Now let’s see how the strategy behaves in real-time and when backtested on historical data.

Example charts: the TradingView strategy’s real-time performance

Our above example strategy creates the following input options in the script’s settings:

Example of the TradingView strategy's input options

The behaviour of the strategy in real-time is the following. Here it just began its trading period (8:30 till 12:00) with an enter short order:

Example of the TradingView strategy running on real-time data

Not long after that several long trades were executed:

Long trades executed by the TradingView strategy in real time

As the chart continues to create new price bars, we see that multiple long orders executed on the bar after the initial enter long order. And the short position also consists out of multiple orders:

When the TradingView strategy runs on real-time data, it enters repeatedly during the same bar

Next another short signal happens:

Example of a short signal when the strategy runs on real-time data

And that position is closed later on by an enter long trade:

Closing a long position in real-time by the TradingView strategy

This process continues until there are so many trades on the chart that TradingView doesn’t show their order names anymore:

The order names in TradingView disappear due to the high amount of roders

A little while later, when there are less orders in the current chart view, the order names come back:

When the number of orders reduce, the order names come back

These multiple orders for a new higher high or lower low continue for a while:

Again we see multiple orders being executed in real time with the TradingView strategy

And the strategy stops trading at the end of the time range (12:00 hour):

The TradingView strategy stops trading at the end of the time range

Now let’s look at the strategy’s performance: how did the strategy fare when calculating on every real-time tick? In the ‘Overview’ tab of the ‘Strategy Tester’ window we see that the strategy generated 91 trades:

Performance overview of the TradingView strategy when running on real-time data

The script achieved a net profit of -0.03 pound with a profit factor of -0.259:

Performance metrics of the example strategy when calculation on real-time price updates

Its behaviour of multiple trades per bar is visible in the ‘List of Trades’ tab too. The first 4 trades, for instance, all entered and exited on the same price bar (shown by the identical ‘Date/Time’ values):

Trade results when the TradingView example strategy runs on real-time price data

Now let’s see how the strategy performs during a historical backtest on the same time period.

Strategy performance: historical backtesting of the TradingView strategy

When we apply the example strategy to the chart after the time range of 8:30 till 12:00 on July 15, 2016 ends, we see the script submitting its trades shortly after 8:30:

Example chart: historical backtest of the TradingView strategy

This immediately shows the difference between real-time data and historical calculations: on historical data, the strategy submits one order per bar whereas in real-time it submitting multiple orders per bar.

Since we set the calc_on_every_tick argument to true, the strategy processes every real-time price update. But that strategy setting doesn’t affect how the script calculates on historical bars, and those historical calculations are only performed at the close of each price bar (Pine Script Language Tutorial, n.d.).

For our strategy, one consequence is that it takes a lot longer before the strategy reaches its maximum position size of the pyramiding setting on those historical price bars. Actually, it might not even reach that limit of 20 entries in the same direction at all. That’s because we need 20 price bars with an order signal on historical data. But with real-time data, we only need 20 price updates during a price bar on which the order condition was true.

Another difference is that, on historical data, orders fill on the bar after the one on which a new highest high or lowest low was reached. For instance:

Example of how the TradingView strategy behaves on historical price data

There’s also another reason why some of the trades happen on different bars than they did in real-time. The maximum number of entries in the same direction is reached much sooner in real-time – and then on any subsequent price bar on which the order condition is also true, the trade isn’t submitted. But with historical data, where the strategy only submits one order per bar, much more price bars can be traded.

So even though we didn’t change any of the strategy’s manual settings or its code, the trades look remarkably different compared to their real-time behaviour:

Example of how the TradingView strategy trades historical price data On this historical data, the strategy is also less active than in real time Here we see the TradingView strategy also behaving differently on historical than real-time data

So it’s not a big surprise that, when look at the ‘Strategy Tester’ window, the strategy performs different now on historical bars than it did with real-time data. For instance, in the ‘Overview’ tab we see 46 trades (instead of 91) and a profit factor of -0.197 (instead of -0.259):

Overview of the strategy performance when running on historical data

While this difference is modest, we did only trade one morning session. And backtesting that part of only one day caused our number of trades and profit factor to be roughly 50% different than the strategy would achieve in real-time.

In the ‘Performance Summary’ tab we see a net profit that’s one-third better (or, better said, less worse) than when the strategy traded on real-time data:

Performance metrics when the TradingView strategy runs on historical data

And there’s a difference in the ‘List of Trades’ tab too. When the strategy executed on real-time data, its first 4 orders all opened and closed at the same bar time. That happened since multiple orders were executed per bar. But on historical data, the strategy only calculates once per bar (and thus our strategy can submit an order once). And so now we see that each entry order fills on a different bar:

Overview of the historical trades made by the TradingView strategy

Now let’s see which insights we can draw from this comparison between the strategy’s real-time performance and its backtest results.

Insights: comparing real-time with historical strategy performance

A strategy that calculates with every real-time tick performs differently in real-time than during backtesting. Several things to keep in mind with this are:

  • In real time, subsequent orders execute at prices closely to each other. This happens because, when the strategy calculates with every price update, it has multiple opportunities to submit orders during that same bar. But on historical data the strategy calculates on bar close (Pine Script Language Tutorial, n.d.). And so there’s more time between orders then (namely, the duration of the price bar). The effect of orders close to each other in real-time versus more spaced in time during backtesting is more pronounced when the instrument has a high volatility or trends strongly, or when we trade a high time frame (like an hourly chart or higher).
  • During a backtest, orders fill at places where an order wouldn’t fill during real-time trading. This happens because a strategy with calc_on_every_tick enabled can submit a bunch of orders quickly after each other in real time, and so any subsequent entry signals won’t be traded due to the position sizing limit. On historical bars the strategy won’t reach its pyramiding limit that quickly, and so can trade more different bars. One consequence is that during strong, profitable trends the strategy performs better in real-time than it did during backtesting. That’s because, with filling multiple orders per bar, our script can open a big position sooner at the beginning of the trend while during a backtest it’s likely still filling orders when the trend is already long underway.
  • In real-time trading orders can fill at places where there wouldn’t be a trading signal during backtesting. This happens because during an intra-bar calculation an order condition can be true (like a close above a moving average). However, that order condition can be invalid when the bar closes. And so in real time the strategy would submit an order, but not during backtesting since the order condition wasn’t there when the bar closed.
  • The more volatile the market, the bigger the difference between backtest and real-time performance. When there aren’t a lot of price updates happening with each price bar, our strategy has less calculation opportunities and there’s also less room for submitting a lot of orders quickly after each other. And when the market moves sideways, then it doesn’t matter a whole lot how quickly an order fills since the entry prices will be more or less the same (as opposed to when the market trends strongly or fluctuates wildly).
  • The difference in real-time results and backtest performance is much less when the strategy doesn’t submit additional orders, when it only submits orders on bar close, or when the strategy’s pyramiding setting is turned off. This means our script’s characteristics (its code logic) and its settings strongly influence the extend to which there’s a big difference between real-time and backtest performance when calculating with every real-time price update.

So while calculating a strategy with every real-time tick is helpful because it makes the strategy respond quicker in real time, this feature cannot be backtested currently. This means the backtest performance of a strategy that uses the calc_on_every_tick argument is irrelevant and the only indication of how the script would perform is to have it run on real-time data.

Note: At the moment there isn’t a way to save or export the data from the ‘Strategy Tester’. So when we evaluate the performance of a strategy in real time, we’ll manually need to record its performance because the next time the chart or script loads, the data in the ‘Strategy Tester’ window is erased and recalculated.

Summary

We configure the settings of a TradingView strategy with strategy(), a function that has to be added to every strategy script. One of this function’s arguments is calc_on_every_tick. When this optional argument is true, the strategy calculates with every real-time price update. That makes it possible for the script to respond quicker to price action, and that way orders (or other actions) can be performed sooner. The default behaviour of TradingView strategies is to calculate on every bar’s close, and we get that behaviour by omitting the calc_on_every_tick argument from strategy() or by setting this argument to false. Now when a strategy calculates with every price update, it only does that on real-time data; when backtesting on historical bars the strategy still calculates on bar close. This means the performance of the strategy in real-time differs remarkably from its backtest performance. Because of that the only way to currently test a strategy that uses calc_on_every_tick=true is to have it run on real-time data.

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 July 15, 2016, from https://www.tradingview.com/study-script-reference/