A free rate of change indicator for ThinkorSwim that marks the ROC's extreme zones and plots arrows when it turns there with an RSI breakout signal.
Download IndicatorIn this tutorial I build a rate of change indicator for ThinkorSwim, starting from the built-in Rate of Change study. I turn the ROC into a range-bound oscillator: one line at its highest value on the chart, one at its lowest, and a dashed line halfway to each, so you can see when the ROC is in an extreme zone.
Then I add a green arrow when the ROC turns up in the lower zone on the same bar the built-in RSI fires its bullish breakout signal, and a red arrow for the bearish side. The video is unrehearsed, bugs and chart bubble tests included, so it doubles as a lesson in turning an idea into thinkScript for anyone who wants to make a built-in study smarter.
The rate of change (ROC) indicator measures how far price has moved, in percent, over a set number of bars. The textbook formula is the close minus the close n bars ago, divided by the close n bars ago, times 100. A reading of 5 means price is 5% higher than n bars ago, and -5 means it is 5% lower.
ThinkorSwim’s built-in Rate of Change study uses 14 bars by default and writes the same formula as (price / price[length] - 1) * 100. It plots the ROC around a zero line and shades it from light red near its recent lows to yellow near its recent highs.
The ROC has no fixed upper limit. The RSI stays between 0 and 100, but a ROC reading that is extreme on one chart can be ordinary on another, so the raw number doesn’t tell you whether a reading is extreme for that chart. That’s the gap this indicator fills.
Our rate of change indicator keeps the built-in ROC line and zero line. The table shows the 4 lines and 2 arrows it adds:
| Plot | Code | Look | What it marks |
|---|---|---|---|
| highestROC | HighestAll(ROC) | Solid red, weight 3 | The highest ROC on the chart |
| lowestROC | LowestAll(ROC) | Solid green, weight 3 | The lowest ROC on the chart |
| midPtHighestROC | highestROC/2 | Dashed light red, weight 1 | The start of the upper extreme zone |
| midPtLowestROC | lowestROC/2 | Dashed light green, weight 1 | The start of the lower extreme zone |
| bullSignal | ROC turns up in the lower zone and the RSI fires UpSignal | Green up arrow, weight 3 | Bullish signal |
| bearSignal | ROC turns down in the upper zone and the RSI fires DownSignal | Red down arrow, weight 3 | Bearish signal |
The rate of change idea started on a daily chart of Amazon, with the built-in ROC in the first lower panel and the built-in RSI in the second.
The plan had 2 parts. First, turn the ROC into a range-bound oscillator by marking its high and low, then split the distance from zero to each in half and call the outer halves extreme. Second, flag the bars where the RSI gives a breakout signal while the ROC sits in an extreme zone.
I marked 4 examples on that Amazon chart before writing any code:
In plain terms, a bullish signal says the ROC is slowing down. It is moving from a negative reading toward a positive one, or at least falling less quickly, while the RSI says price is moving up out of oversold territory.
Before coding, I said up front that I had no idea whether this indicator would give an edge. The video is about the process of turning an idea into code, and I wanted it to work on any timeframe.
The rate of change indicator installs as one study in a lower panel:
TI_ROCSTUDY.ts.The rate of change code adds range lines, extreme zones and signals to ThinkorSwim’s built-in ROC, one block at a time. I tested each block before moving to the next, often with a chart bubble, which is a good habit when you build your own studies.
plot highestROC = HighestAll(ROC);
plot lowestROC = LowestAll(ROC);
HighestAll and LowestAll return the highest and lowest ROC across every bar loaded on the chart. They give the ROC a top and a bottom, so it reads like a range-bound oscillator.
plot midPtHighestROC = highestROC/2;
plot midPtLowestROC = lowestROC/2;
Each dashed line sits halfway between zero and the highest or lowest ROC. The full formula is the highest ROC minus 0, divided by 2, so the code skips the minus zero. The space between a dashed line and its solid line is the extreme zone.
highestROC.setLineWeight(3);
highestROC.setDefaultColor(Color.Red);
midPtHighestROC.setLineWeight(1);
midPtHighestROC.setStyle(Curve.Short_Dash);
midPtHighestROC.setDefaultColor(Color.light_red);
The outer lines are red and green at weight 3, and the halfway lines light red and light green, dashed, at weight 1. I set the colors in code so they stay the same on any platform setup.
def extremeHigh = ROC < HighestROC and ROC >= midPtHighestROC;
def extremeLow = ROC > LowestROC and ROC <= midPtLowestROC;
extremeHigh is true when the ROC is below the red line and at or above the upper dashed line. extremeLow is the mirror image. I used greater than or equal to and less than or equal to on the dashed lines so a reading right on the line counts.
To check extremeHigh, I added a temporary yellow chart bubble that printed only when it was true, then commented it out once the bubbles matched the zone.
def RSIUpSignal = RSI().upSignal;
def RSIDownSignal = RSI().downSignal;
The RSI part doesn’t copy any RSI code. RSI() calls ThinkorSwim’s built-in RSI study, and the name after the dot picks one of its plots. UpSignal fires when the RSI crosses above its oversold line and DownSignal when it crosses below its overbought line.
To test it, I put a chart bubble on RSIUpSignal and loaded the RSI with its breakout signals set to yes. Every green RSI arrow had a bubble. The test line is still in the download, commented out:
#AddChartBubble(RSIUpSignal, ROC, RSIUpSignal, color.yellow);
Our Dynamic RSI indicator also calls the built-in RSI study instead of copying it, with reference RSI(RSI_Period), and builds Bollinger Bands on it.
def ROCUpSignal = extremeLow and (ROC > ROC[1]);
def ROCDownSignal = extremeHigh and (ROC < ROC[1]);
ROCUpSignal is true when the ROC is in the lower extreme zone and higher than on the previous bar, so it has stopped falling and started to turn. ROCDownSignal is the mirror in the upper zone. While testing, I plotted each one as arrows on its own to confirm it.
plot bullSignal = if ROCUpSignal and RSIUpSignal then ROC else double.nan;
plot bearSignal = if ROCDownSignal and RSIDownSignal then ROC else double.nan;
bullSignal.setPaintingStrategy(PaintingStrategy.Arrow_Up);
A signal needs both conditions on the same bar. The then ROC else double.nan part puts the arrow on the ROC line. Without it the arrows sit on the zero line, which is what happened on my first try.
The rate of change indicator has 3 inputs. Everything else sits in the code.
| Setting | Default | What it does |
|---|---|---|
| length | 14 | How many bars back the ROC compares the close with. |
| colorNormLength | 14 | Bars used to shade the ROC line from light red to yellow. Colors only. |
| price | close | The price the ROC measures. |
| Halfway divisor (in the code) | 2 | Where the dashed lines sit between zero and the outer lines. |
| RSI settings (in the code) | Built-in defaults | RSI() with empty parentheses uses the built-in RSI’s own inputs, including its overbought and oversold lines. |
Dividing by a larger number than 2 moves the dashed lines toward zero, so more bars count as extreme and more bars can produce arrows. In the video I mentioned a sensitivity factor as one way to improve the indicator.
One way to add it is to replace that 2 with an input. This edit is my suggestion and is not in the video or the download. Put the input line under the other inputs and replace the two midpoint lines:
input sensitivity = 2;
plot midPtHighestROC = highestROC / sensitivity;
plot midPtLowestROC = lowestROC / sensitivity;
With sensitivity at 2 the indicator plots exactly as the download does. Keep it above 1: at 1 or below, the dashed lines reach or pass the solid ones and the extreme zones disappear.
HighestAll and LowestAll read every bar loaded on the chart, so the range lines depend on your chart’s time range. A 1-year daily chart and a 5-year daily chart of the same stock can draw different red and green lines, and so different extreme zones and arrows.
The lines also move when the ROC sets a new high or low, which can change which past bars count as extreme. Before you judge an old arrow, check the chart’s time range. Because extremeHigh needs the ROC below the red line, the one bar that sets the highest ROC never counts as extreme, and the same goes for the lowest.
Our rate of change scans show the same effect in the Scan tab. A 30-minute bearish signal on Apple appeared on the chart only after I cut the chart’s history from 90 days to 15 days.
The finished rate of change indicator plotted fewer arrows on the daily Amazon chart than my 4 planning examples. Two sets of signals showed up, both while the ROC was still in an extreme zone. My guess in the video was that in the earlier examples, the RSI signal and the ROC turn didn’t line up on the same bar.
On a 5-minute chart, the arrows plotted the way I expected. I loaded the RSI alongside to confirm each green arrow came with an RSI breakout signal and a rising ROC. At the end I pointed out that you can switch to another market, such as the S&P futures, and the indicator should keep plotting the same way.
One turn the indicator missed was a bearish one that came after the extreme edge had already been crossed. To catch it, the code would need context: remember that the extreme edge was crossed, then accept a turn lower just below it. I left that for a separate tutorial.
A rate of change arrow marks momentum turning at an extreme on the same bar the RSI breaks out. A bullish arrow says the ROC has stopped falling inside the lower zone while the RSI climbs out of oversold. In the planning examples, what followed ranged from a rally to a small bounce or a slight pullback, so not every arrow leads to a big move.
Treat an arrow as a read on momentum and check it against price levels before acting. Decide where your stop goes first, and size the position so a stop-out costs a small, fixed share of your account.
For more ways to work with momentum, our Smarter MACD starts from the built-in MACD code the same way, and if a change of your own throws an error, our guide on how to fix bugs in thinkScript code can help you track it down.
#
# TD Ameritrade IP Company, Inc. (c) 2007-2022
#
# Modified by TOS Indicators
#Home of the Volatility Box
#Indicator: Rate of Change x RSI
#Code written in 2022
#Full Tutorial: https://www.tosindicators.com/indicators/rate-of-change
declare lower;
input length = 14;
input colorNormLength = 14;
input price = close;
assert(length > 0, "'length' must be positive: " + length);
plot ROC = if price[length] != 0 then (price / price[length] - 1) * 100 else 0;
plot ZeroLine = 0;
ROC.DefineColor("Highest", Color.YELLOW);
ROC.DefineColor("Lowest", Color.LIGHT_RED);
ROC.AssignNormGradientColor(colorNormLength, ROC.color("Lowest"), ROC.color("Highest"));
ZeroLine.SetDefaultColor(GetColor(5));
plot highestROC = HighestAll(ROC);
plot lowestROC = LowestAll(ROC);
plot midPtHighestROC = highestROC/2;
plot midPtLowestROC = lowestROC/2;
highestROC.setLineWeight(3);
lowestROC.setLineWeight(3);
highestROC.setDefaultColor(Color.Red);
lowestROC.setDefaultColor(Color.Green);
midPtHighestROC.setLineWeight(1);
midPtLowestROC.setLineWeight(1);
midPtHighestROC.setStyle(Curve.Short_Dash);
midPtLowestROC.setStyle(Curve.Short_Dash);
midPtHighestROC.setDefaultColor(Color.light_red);
midPtLowestROC.setDefaultColor(Color.light_green);
def extremeHigh = ROC < HighestROC and ROC >= midPtHighestROC;
def extremeLow = ROC > LowestROC and ROC <= midPtLowestROC;
def RSIUpSignal = RSI().upSignal;
def RSIDownSignal = RSI().downSignal;
def ROCUpSignal = extremeLow and (ROC > ROC[1]);
def ROCDownSignal = extremeHigh and (ROC < ROC[1]);
plot bullSignal = if ROCUpSignal and RSIUpSignal then ROC else double.nan;
plot bearSignal = if ROCDownSignal and RSIDownSignal then ROC else double.nan;
bullSignal.setPaintingStrategy(PaintingStrategy.Arrow_Up);
bullSignal.setDefaultColor(Color.Green);
bullSignal.setLineWeight(3);
bearSignal.setPaintingStrategy(PaintingStrategy.Arrow_Down);
bearSignal.setDefaultColor(Color.Red);
bearSignal.setLineWeight(3);
#AddChartBubble(RSIUpSignal, ROC, RSIUpSignal, color.yellow);Want the .ts file and the one-click import? Both come with a free account.
Here are some resources that you may find useful:
Join 40,000+ traders using institutional-grade tools for ThinkOrSwim.
Get the Bundle