In our previous repainting explanation and overview, we discussed the fundamentals of repainting and how this can affect you. In most cases during backtesting, it will not affect you. However, there is one corner case that can still present a problem. Namely, strategies that utilize data from upper timeframes can still be subject to repainting issues when forward testing. This post will attempt to address that issue and provide a simple example snippet which you can then copy into your own scripts.
Background and Recap
In its simplest form, indicator repainting happens to all indicators which rely on the close price of a candle for their calculation. During live trading or forward testing, the close value of the current bar is constantly changing from as soon as the bar opens until the moment it closes. The final close price is not known until right at the end of the bar. This means that the indicator is constantly “repainting” during the bar. You will see that indicators are constantly moving with each tick received.
This quote above is from the post: Tradingview: Indicator Repainting. To get a good understanding of what indicator repainting is all about, take a look at that post first and then come back here!
Upper Timeframe Repainting: The Issue
First, let’s take a look at the issue in question. Copy the following simple example code and place it on a 1 minute chart of your choosing:
//@version=3 strategy("Dual RSI - RPB Strat") // Other time frame otf = input(defval="15", title="Second Momentum Timeframe", type=resolution) otf_period = input(defval=14, title="Look Back Period (2nd Timeframe)", type=integer) ctf_period = input(defval=14, title="Look Back Period (Chart Timeframe)", type=integer) ob = input(defval=70, title="Overbought Area", type=integer) os = input(defval=30, title="Oversold Area", type=integer) //Get the data otf_rsi = security(tickerid, otf, rsi(close, otf_period)) //Calculate RSI Values ctf_rsi = rsi(close, ctf_period) //Plot hline(ob, title='Overbought Line', color=black, linestyle=dashed, linewidth=1) hline(os, title='Oversold Line', color=black, linestyle=dashed, linewidth=1) plot(otf_rsi, title='OTF RSI', color=blue, style=line, linewidth=3) plot(ctf_rsi, title='CTF RSI', color=green, style=line, linewidth=3)
Once loaded, you will notice that for historical data, the lines move in nice blocky 15 minute steps. Now go away and have a cup of tea. Come back in 30 minutes or so. You will see a chart which resembles something like this:
In our previous tutorial, we noted that by default, a strategy will only update at the end of each bar to avoid repainting issues. If you hit refresh on the browser, you will certainly see the upper timeframe repaint. So what is happening here?
Our strategy repaints on the upper timeframe data because we are taking 1-minute samples/snapshots on each bar of our chart timeframe. When we have live data and take a sample, the current close of the upper timeframe has not completed yet. As such, you are given the latest value Tradingview has for that candle. This value is not the final close value but rather, the current close value. As a result we get can end up with a wiggly line instead of the nice straight ones we see with historical data.
Also, take a moment to notice how the chart timeframe (the green line) did NOT repaint. This shows you that the issue is limited to upper timeframes only.
Note that if you want to see how how the sampling is working, you can place another timeframe side by side and make a comparison of the final values in each period. Note the example image below uses a 1-minute chart vs a 10-minute chart.
As we can see, at the end of the 10 minute period, we have the same value as the indicator placed directly on the upper timeframe chart. Here we can see that even though the line moves around, the two values finally meet at the end of the period.
This is all nice an logical. However, if you have performed all of your backtesting on historical data (where this sampled data is not available), then this can cause the strategy to perform unexpectedly. Furthermore, if you have an entry which relies on looking at an upper timeframe value, then it could feasibly enter halfway through the 10-minute bar. Lastly, when you refresh your browser, the data becomes historical and your trade disappears. The classic repainting problem!
//@version=3 strategy("Dual RSI - RPB Study") // Other time frame otf = input(defval="15", title="Second Momentum Timeframe", type=resolution) otf_period = input(defval=14, title="Look Back Period (2nd Timeframe)", type=integer) ctf_period = input(defval=14, title="Look Back Period (Chart Timeframe)", type=integer) ob = input(defval=70, title="Overbought Area", type=integer) os = input(defval=30, title="Oversold Area", type=integer) // Function to dectect a new bar is_newbar(res) => t = time(res) change(t) != 0 ? true : false // Check how many bars are in our upper timeframe since_new_bar = barssince(is_newbar(otf)) otf_total_bars = na otf_total_bars := since_new_bar == 0 ? since_new_bar : otf_total_bars //Get the data otf_rsi = security(tickerid, otf, rsi(close, otf_period)) //Calculate RSI Values ctf_rsi = rsi(close, ctf_period) final_otf_rsi = na final_otf_rsi := barstate.isrealtime ? since_new_bar == otf_total_bars ? otf_rsi : final_otf_rsi : otf_rsi //Plot hline(ob, title='Overbought Line', color=black, linestyle=dashed, linewidth=1) hline(os, title='Oversold Line', color=black, linestyle=dashed, linewidth=1) plot(final_otf_rsi, title='OTF RSI', color=blue, style=line, linewidth=3) plot(ctf_rsi, title='CTF RSI', color=green, style=line, linewidth=3) //plot(otf_rsi, title='Non Adjusted OTF', color=blue, linewidth=3)
The secret sauce in this example is determining how many bars are in our upper timeframe and then only updating our upper timeframe RSI value during the last bar. At this point, we get the closing price for the last bar of the entire upper period. As we saw above, this is the correct and final close for the upper timeframe.
Once we have the last close, for all of our other 1-minute bars, we can simply reference the previous value of the same line. We do this until it is time to update the final value again. By doing this, we essentially ignore the upper timeframe until it actually closes.
This is achieved with the line:
final_otf_rsi := barstate.isrealtime ? since_new_bar == otf_total_bars ? otf_rsi : final_otf_rsi : otf_rsi
It is worth noting that this solution will only work for STRATEGY scripts. This is because we take the current value on the last bar of the period. Since we know from the last post that study scripts will continuously update throughout a bar, it means if you copy this directly to a study script, the last bar of each upper timeframe period could repaint during the bar.
After placing the example code on the chart, you should be left with something that looks like this:
As we can see, during historical data, the orange line (the line without the fix) and blue line (the line with the fix) are identical, creating the brown line you see. Following this, we see the yellow line breaks off and begins to update every bar. However, the blue line continues to behave in the same manner as it did during historical data.
To further verify, we can comment out the orange line, and then check that the blue line is updating at the same intervals. Some solutions will actually be plotted with one bar delay. Here we can see that the update times are exactly the same.
The orange vertical lines show the times which the plot updated. By looking at these we can see that the plot is updating at the same intervals.
Find This Post Useful?
If this post saved you time and effort, please consider support the site! There are many ways to support us and some won’t even cost you a penny.
Backtest Rookies is a registered with Brave publisher!
Brave users can drop us a tip.
Alternatively, support us by switching to Brave using this referral link and we will receive some BAT!
Enjoying the content and thinking of subscribing to Tradingview? Support this site by clicking the referral link before you sign up!