One feature of TradingView strategies is a calculation that happens immediately after an order fills. Such an intra-bar calculation makes our script quicker. But how does it affect the strategy’s behaviour during backtesting and real-time trading?

In this article:

Intra-bar calculation after an order fills: real-time and historical behaviour

We configure a TradingView strategy programmatically with the strategy() function, and that function has to be added to every strategy script (Pine Script Language Tutorial, n.d.). Its title argument, which names the strategy, is also something we always need to set (TradingView, n.d.).

Another, optional argument of that function is calc_on_order_fills. When set to true, the strategy performs one additional intra-bar calculation after an order fills (TradingView, n.d.). In contrast, the default behaviour of TradingView strategies is to only calculate on the close of historical and real-time price bars (Pine Script Language Tutorial, n.d.).

The intra-bar calculation with calc_on_order_fills enabled happens after an order fills and before the price bar closes. With this we have the opportunity to perform an additional action in this time window, like submitting another order or updating the profit target. And so with this feature we can create a more responsive script, especially on high chart resolutions where there’s a lot of time between filling an order and the bar closing.

However, the calc_on_order_fills argument does have a disadvantage. During backtesting, TradingView calculates with the bar’s open, high, low, and close price; but in real-time trading, the additional intra-bar calculation can happen with every real-time price update (Pine Script Language Tutorial, n.d.). Consequently, on historical data at most 4 orders can be filled per bar (2 on the open, 1 on the high, and 1 on the low; Pine Script Language Tutorial, n.d.),. But during real-time trading there can be as many additional intra-bar calculations as there are real-time price updates.

A strategy that uses calc_on_order_fills therefore behaves differently on historical bars than with real-time prices. This has three important consequences:

  • On real-time data the additional calculations happen with a price that’s close to the order’s fill price. After all, two subsequent real-time price updates have practically the same price while the range between a historical price bar’s open and high is much bigger. This means:
    • When we submit an additional market order during the extra intra-bar calculation, this order fills at a different price in real time compared to its fill price in backtesting.
    • And that additional order also fills in real time with much less slippage than the backtest suggests.
  • With real-time data there are more opportunities for an intra-bar calculation than on historical price bars. Let’s say we use the additional intra-bar calculation after an order fills to submit a market order. On historical bars, at most 4 of those orders can be filled (Pine Script Language Tutorial, n.d.). But with real-time data, dozens of orders can fill during that same bar, simply because each real-time price update provides yet another opportunity to submit an order. This also means that:
    • The higher the time frame, the bigger the difference between historical and real-time performance. This happens because a high chart resolution gives more real-time price updates per bar (and thus more opportunities for additional intra-bar calculations), whereas historical price bars have the same number of intra-bar calculations regardless of the chart’s time frame.
  • When we use the additional intra-bar calculation to submit yet another order, a cascading effect can happen that causes a big difference between backtest and real-time performance. That is, when each intra-bar calculation triggers another order which, when filled, causes yet another intra-bar calculation, then the strategy calculates a lot of times (and fills a lot of orders). But on historical bars the number of intra-bar calculations remain limited to less than a handful while in real time dozens of orders can fill per bar.

For a more practical understanding of how the calc_on_order_fills argument influences a TradingView strategy’s behaviour, let’s look at a programming example that uses this argument. Then we’ll have this example strategy run on real-time data and perform a historical backtest to see how the script’s behaviour differs.

Example: submitting an extra order after the previous order fills

The basic TradingView strategy below goes long when the bar’s close crosses above the 12-bar EMA (Exponential Moving Average) and goes short if the bar drops below that moving average. Once there’s an open position, we increase the position up to a maximum of 20 entries in the same direction as long as the bar remains above or below the EMA.

A quick view of the strategy’s behaviour is given below: the first chart shows the script’s behaviour in real time while the second chart shows the backtest of the same price bars. After discussing the code, we’ll look at more charts that show the difference between real-time and historical performance.

Performance of the TradingView strategy in real time Performance of the TradingView strategy on historical data
strategy(title="Intra-bar calculations - example", overlay=true,
     pyramiding=20, calc_on_order_fills=true)

// Calculate & plot moving average
emaValue = ema(close, 12)

plot(series=emaValue, color=orange, linewidth=2)

// Determine trading conditions
timeFilter = (year == 2016) and (month == 7) and (dayofmonth > 10)

enterLong = timeFilter and crossover(close, emaValue)
addToLong = (strategy.position_size > 0) and (close > emaValue)

enterShort = timeFilter and crossunder(close, emaValue)
addToShort = (strategy.position_size < 0) and (close < emaValue)

// Submit orders
strategy.entry(id="Enter Long", long=true, when=enterLong)
strategy.entry(id="Extra Long Order", long=true, when=addToLong)

strategy.entry(id="Enter Short", long=false, when=enterShort)
strategy.entry(id="Extra Short Order", long=false, when=addToShort)

We start by configuring the strategy’s settings programmatically with strategy(). With the title argument we name the strategy and with overlay set to true our strategy displays on the chart’s instrument (TradingView, n.d.). To allow multiple entries in the same direction we set pyramiding to 20. And, with calc_on_order_fills=true, our strategy performs an extra intra-bar calculation when an order fills (TradingView, n.d.).

Then we calculate and plot the exponential moving average:

emaValue = ema(close, 12)

plot(series=emaValue, color=orange, linewidth=2)

We calculate the average with ema(), a function that requires two arguments: a series of values to calculate on and the length of the moving average (TradingView, n.d.). Here we set those arguments to the bar’s closing prices (close) and 12. With the assignment operator (=) we store that 12-bar EMA in the emaValue variable for use later.

Next we plot the average with plot(). That function displays the values of its series argument as a line by default on the chart (TradingView, n.d.). We set that argument to the emaValue variable. The function’s color argument is set to the orange basic TradingView colour and linewidth is given the value of 2. That latter argument specifies the plot’s size starting from 1 as the default size (TradingView, n.d.), and so 2 makes the line a bit bigger than normal.

After that we create a time filter:

timeFilter = (year == 2016) and (month == 7) and (dayofmonth > 10)

This timeFilter variable is determined quite arbitrarily and doesn’t necessarily add something to the strategy’s long and short logic. So why did we add it to the script? TradingView currently allows strategy scripts to generate at most 2,000 orders, and triggers an error when we cross that limit:

Example of the order limit error message in TradingView

Since our strategy allows up to 20 entries in the same direction, it generates a lot of orders and that makes the script reach the order limit quickly. To limit the amount of bars that are backtested (and thereby limiting the number of orders), we use a time filter here that only makes the strategy trades from July 8, 2016, onward.

To implement that filter programmatically, we set the timeFilter variable to the value of three true/false expressions combined with the and logical operator. That operator returns true whenever the value on its left and the value on its right are both true too; when one or both values are false, then and returns false too (Pine Script Language Tutorial, n.d.). This means each of the three true/false expressions has to be true before the timeFilter variable is assigned true too.

The expressions checks whether the year built-in variable (which returns the current bar’s year in exchange time zone; TradingView, n.d.) equals (==) 2016. The second expression checks whether the month variable (that returns the current bar’s month in exchange time zone; TradingView, n.d.) is equal to (==) 7. And then the last expression evaluates whether dayofmonth returns a value greater than (>) 10. This latter variable returns the current bar’s date in the exchange’s time zone (TradingView, n.d.).

So combined, these first three true/false expressions require that the bar’s date is in July 2016 while the day is 11 or later. When each of these three situations is indeed the case, timeFilter is set to true; otherwise, this variable is assigned false.

Note: Depending on when you run the above script, you might need to update the time filter to prevent the strategy from running into the order limitation.

With the time filter made, we create the long trading conditions:

enterLong = timeFilter and crossover(close, emaValue)
addToLong = (strategy.position_size > 0) and (close > emaValue)

Both enterLong and addToLong variables are set to a value based on the combination of two true/false expressions with the and logical operator. And so each has to be true before the variable is assigned true too, and when just one of the expressions is false, then the variable becomes false too.

The first of those two expressions is the timeFilter variable, and this way we incorporate our time filter into the strategy’s enter long logic. The second expression that sets the value of enterLong checks whether the bar’s close crossed above the 12-bar EMA. We evaluate this with crossover(). That function works on two arguments and 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 less than the second argument (TradingView, n.d.). Since we use crossover() with close and emaValue, the function returns true when the bar’s closing price crossed above the 12-bar EMA and returns false when such a crossover didn’t happen.

For the addToLong variable we first evaluate whether the strategy is long already. We use strategy.position_size for that, a built-in variable that returns both the size and direction of the strategy’s current position. That is, when the strategy is long, this variable returns a positive value with the number of open long contracts; if the script is short, it returns a negative value with the short position size; and when the strategy is flat, strategy.position_size returns 0 (TradingView, n.d.).

So that variable returns a value that’s greater than (>) 0 when the strategy is currently long. The second expression evaluates whether the bar’s closing price (close) is above (>) the 12-bar moving average (emaValue). Only when both situations happened – being long and a close above the EMA – will the addToLong variable be assigned a value of true. We’ll use this variable later on in the script to submit an additional enter long order.

The other two true/false variables define our short conditions:

enterShort = timeFilter and crossunder(close, emaValue)
addToShort = (strategy.position_size < 0) and (close < emaValue)

Both enterShort and addToShort variables are set to the opposite of our long conditions. That is, before enterShort is set to true we require that the timeFilter variable returns true and that the bar’s closing price dropped below the 12-bar EMA.

We evaluate that latter with the crossunder() function, which returns true when the value of its first argument is now below that of the second argument, while on the previous bar the opposite was the case (TradingView, n.d.). And so by using this function with the close and emaValue variables, it returns true whenever the bar’s close fell below the moving average. When both situations (the time filter and cross under) happen, then enterShort holds true and false otherwise.

For the addToShort variable we require that the strategy is short, which it is when strategy.position_size returns a value that’s less than (<) 0. The second requirement before we’ll submit a short scale-in order is that the bar’s closing price is below the 12-bar EMA (close < emaValue). Our addToShort variable is assigned true when both things occurred, and holds false when one or both aren’t the case.

With both long and short conditions set, we generate the strategy’s orders:

strategy.entry(id="Enter Long", long=true, when=enterLong)
strategy.entry(id="Extra Long Order", long=true, when=addToLong)

strategy.entry(id="Enter Short", long=false, when=enterShort)
strategy.entry(id="Extra Short Order", long=false, when=addToShort)

We submit the orders with strategy.entry(). This function opens a position with a market order by default and scales into a position up to the strategy’s pyramiding setting. And when the strategy already has a position in the opposite direction, reverses that position (TradingView, n.d.).

Each strategy.entry() function call uses the same three arguments. With id we specify the order identifier, and this name displays on the chart and in the ‘Strategy Tester’ window. The long argument, when set to true, makes the function submit an enter long order, whereas long=false has it open a short position. And the when argument accepts a true/false value to define when the order should be submitted: only if this argument is set to a value of true will strategy.entry() submit our order (TradingView, n.d.).

The first strategy.entry() function call creates a long order (long=true) that’s named “Enter Long” and submitted when the enterLong variable is true on the current bar. The next statement uses strategy.entry() to generate an order named “Extra Long Order” and this order is send off whenever the addToLong variable is true.

The last two strategy.entry() function calls are practically the same. The first submits an “Enter Short” order (long=false) whenever the enterShort variable is true. And in the last line of the programming example we submit the “Extra Short Order” when addToShort is true to add to an existing short position.

This ends our discussion of the programming example. Now let’s look at its behaviour on real-time and historical data.

Example: trading a TradingView strategy on real-time data

With the above example strategy running in real-time, after a while the chart looks like:

Example: the TradingView strategy running on real-time data

Because of the high number (20) of entries in the same direction, the script also has plenty of intra-bar calculation opportunities. After all, since we’ve enabled the calc_on_order_fills argument the strategy calculates an extra time after an order fills. For instance, in the above chart there are several bars with multiple entries per bar. And the last short trade displayed here even had its maximum number of entries reached within a single bar.

For a better visual grasp of how many orders the strategy executed, we can disable the order labels to reduce the clutter on the chart. For that we click on the gear icon ( ) displayed to the right of the strategy name:

Opening the strategy settings window in TradingView

This opens the window with the strategy settings. There we go to the ‘Style’ tab and disable the ‘Signal Labels’ option, followed by clicking ‘OK’.

Disabling the signal labels of a TradingView strategy

This changes our chart to the following, with each arrow signalling an order:

Example of the TradingView trading strategy

When we change the price axis’ scaling (by clicking on the price axis and dragging the mouse down) we can see even more of the strategy’s orders:

Zoomed out: the trades made by the TradingView strategy on real-time data

While this doesn’t make all orders visible, we can already see that our strategy is quite active and submits a bunch of orders quickly after each other. Now how does this strategy with the calc_on_order_fills argument enabled perform on historical price bars?

Example: computing the TradingView strategy on historical data

To backtest the bars that we previously traded in real time, we remove the strategy from the chart and re-add it. We don’t make any changes to its settings or code; the only difference is that now it calculates on previous bars instead of processing real-time price updates. For the same time period as the previous chart, the script trades as follows:

Example chart: the TradingView strategy calculating on historical data

The first thing you might notice here is that the strategy seems to trade more often. For instance, the last three price bars are now traded with short orders and this didn’t happen on the real-time chart. This occurs because on historical data the strategy can only fill a handful of trades at most (Pine Script Language Tutorial, n.d.), while on real-time data the strategy can calculate much more often per bar (and thus generate more orders too).

And so during backtesting we’ll have more historical bars on which orders fill, simply because it takes longer (that is, more price bars) to reach the strategy’s maximum number of entries in the same direction. We can see this more clearly when we turn off the signal labels like we did above:

TradingView trades on historical data with the signal label disabled

And when we adjust the price axis again, we see that at most 5 orders were filled per bar:

Zoomed out: example of the strategy running on historical data

Here is the same chart period with the orders executed in real time, and the orders are much more clustered here:

Zoomed out: example of the strategy running on real-time data

Since we added the strategy to a 3-minute stock chart during quiet trading hours, there weren’t a huge amount of price updates with every real-time bar. Should we add the script to a more active instrument and/or chart with a higher resolution, then we see more price updates per real-time bar. That would make our strategy submit orders more often and quicker than it did now on real-time data (and the difference with its historical performance would be even bigger).

Intra-bar calculations: comparing real-time with historical performance

Now what effect has the additional intra-bar calculation after an order fills on the historical and real-time performance of a TradingView strategy? A couple of insights are:

  • Since each order triggers another intra-bar calculation (which in turn can generate another order), the backtest and real-time difference is especially big when we allow multiple entries in the same direction. This is also what we saw in the example above.
  • The more volatile the instrument, the bigger the difference between backtest and real-time performance. This is because a more active instrument has more price updates per bar, and with each of those the strategy can perform an additional calculation (provided that an order is filled before that). And the more of these calculation opportunities, the bigger the difference between real-time and historical performance.
  • During a backtest, the strategy can trade on bars on which it wouldn’t trade in real time. This happens because the strategy’s maximum position size is reached much sooner during real-time trading (and so the amount of bars on which orders fill is less), while in backtesting the strategy can fill at most 5 orders per bar (and so the strategy trades much more bars to open its maximum position size). Because a strategy during a backtest will fill orders over a longer period of time, the fill prices can also differ significantly.

Besides the difference between backtest and real-time performance of a strategy that calculates an additional time when an order fills, there’s also a discrepancy when the strategy calculates with every real-time tick. That feature makes the backtest versus real-time difference even bigger, and we discuss it in backtest results are inaccurate when calculating on every tick.

Summary

We configure the settings of a strategy with strategy(), a function that needs to be added to the code of every TradingView strategy. One argument of strategy() is calc_on_order_fills that, when set to true, makes the strategy perform an additional intra-bar calculation after an order fills. This way the strategy can perform another task (like submitting another order or updating the profit target) after an order fills but before the price bar closes, which is the default moment when a TradingView strategy calculates. An effect of calc_on_order_fills is that the strategy can scale into a position quicker. However, there are at most 4 additional intra-bar calculations per historical price bar, while on real-time data there can be as many intra-bar calculations as there are price updates. This can cause a considerable difference between how the strategy performs on real-time data versus backtesting. And this discrepancy will be even more pronounced when trading a high time frame or price bars that have plenty of incoming real-time ticks.

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