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:
- Theory: calculating a strategy on every price update with
- Example strategy that processes every real-time tick
- Insights when using
calc_on_every_tick: comparing real-time with historical performance
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
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
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.
//@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) lowestLow = lowest(low, lowestLen) 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.
calc_on_every_tick, the strategy evaluates real-time price bars as if they were already closed. This means built-in variables like
closereturn 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
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
Both input variables are used when calculating the highest high and lowest low:
highestHigh = highest(high, highestLen) lowestLow = lowest(low, lowestLen)
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.
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 (
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
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 too (Pine Script Language Tutorial, n.d.). This means each expression has to be
true before the value of
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.
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)
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.
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
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
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
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:
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:
Not long after that several long trades were executed:
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:
Next another short signal happens:
And that position is closed later on by an enter long trade:
This process continues until there are so many trades on the chart that TradingView doesn’t show their order names anymore:
A little while later, when there are less orders in the current chart view, the order names come back:
These multiple orders for a new higher high or lower low continue for a while:
And the strategy stops trading at the end of the time range (12:00 hour):
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:
The script achieved a net profit of -0.03 pound with a profit factor of -0.259:
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):
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:
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:
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:
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):
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:
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:
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_tickenabled 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.
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.
- For more on calculating a TradingView strategy with every real-time tick, see the
calc_on_every_tickargument of the
strategy()function, where we discuss this feature in depth.
- Besides calculating with every tick, a strategy can also perform one additional intra-bar calculation when an order fills. This extra calculation is also performed on historical price bars, although there’s still a real-time and historical difference with a single intra-bar calculation.
- TradingView strategies can open positions bigger than we configured with the strategy’s pyramiding setting. This risk is especially present when the strategy calculates with every real-time price update since then a lot of orders can be submitted in a short period of time. To learn more about this risk, see pyramiding and submitting several entry orders.
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/