RSI, RSI Moving Average and Choppiness: Pine Script Guide
This script puts RSI, a moving average of RSI, and Choppiness Index on one shared pane and scale. It does not split the pane into upper and lower regions or automatically filter RSI signals. The RSI color compares it with its average; the blue CHOP line is a separate descriptive calculation.
Inspect the Pine ScriptThree outputs, three different questions
The default call calculates RSI(close, 14), SMA(RSI, 9), and CHOP over 14 bars. RSI summarizes smoothed gains versus losses. Its moving average smooths that RSI series, not the price. CHOP compares accumulated true range with the window’s high-low span and does not provide direction.
The RSI line is green only when RSI is strictly above its MA. Otherwise it is red, including equality and an unavailable MA. The orange MA and blue CHOP are drawn in the same indicator pane as RSI. Their visual intersections do not establish comparable economic meanings.
TradingView’s RSI documentation also describes smoothing controls for its standard RSI. An RSI average is not a feature unique to this combination. TradingView attributes CHOP to E.W. Dreiss; no invention date or proprietary origin for this particular combination is established here.
The actual Choppiness calculation
TR = max(high − low, abs(high − previous close), abs(low − previous close))
A = sum of ATR(1) over N bars
W = highest high over N bars − lowest low over N bars
CHOP = 100 × log10(A / W) / log10(N)
ATR(1) uses the bar’s true range, not a 14-period ATR averaged again. The numerator and denominator both use the CHOP length; RSI length and MA length are separate. The helper adds ATR values explicitly in a loop. It does not clamp the result or guard a zero range, invalid logarithm argument, missing history or N = 1.
The 61.8 and 38.2 lines are common reference levels, not calibrated probabilities or guarantees that crossovers will work. CHOP is scale-invariant under a consistent positive price multiplier. A larger absolute price range alone does not determine whether CHOP will rise or fall.
Reproduce CHOP and its gap boundary
Use three-bar synthetic windows with N = 3. In the first, the bars span 100–102, 102–104 and 104–106, each closing at its high, with the preceding close at 100. Each true range is 2, so A = 6, W = 6 and CHOP = 0.
In a second window, all three bars span 100–102 and close at 101, with the preceding close at 101. A remains 6 but W = 2, giving CHOP = 100. Repeated travel across a narrow span changes the ratio without changing the summed true range.
For an edge case, let the close before the window be 50, and all three bars have high 101, low 100 and close 100. Their true ranges are 51, 1 and 1. Thus A = 53 and W = 1, yielding 100 × log10(53) / log10(3) = 361.391544. The first bar includes a gap from outside the high-low window, so this unbounded formula can exceed the customary 0–100 range. Do not silently clip it or interpret it as a probability. A constant window with W = 0 instead makes the ratio undefined.
These examples explain the displayed formula rather than claiming any market result. A news gap can affect numerator and denominator differently; there is no general “wait N/2 bars” rule that proves the next reading valid.
RSI position is not slope or acceleration
Use hypothetical RSI readings 10, 20, 90, 80 with an SMA length of 3. At the last reading, the average is (20 + 90 + 80) / 3 = 63.333333. The RSI is green because 80 exceeds that average even though it fell from 90. A long flat RSI sequence at 50 has SMA 50 and is red because equality goes to the second color branch.
The code colors a state on every bar; it does not detect or mark crossover events. A move from green to red can occur at equality rather than a strict crossing below. Nor is a short period above the average proof of acceleration, buying pressure or a profitable entry.
With uninterrupted valid values, RSI length 14 first needs 15 closes and its SMA(9) first needs nine valid RSI readings, reaching 23 closes. The RSI can therefore appear before its MA is available, colored red by the fallback branch. This initialization color is not a bearish conclusion.
MA choices and threshold wiring
The dispatcher contains eleven exact string cases: SMA, EMA, RMA, HMA, WMA, TMA, VAR, WWMA, ZLEMA, TSF and VWMA. The default is SMA. These are not interchangeable implementations: VWMA needs volume; HMA and regression-based TSF may overshoot an input range; custom WWMA starts recursively from zero rather than using the RMA SMA seed.
TMA uses two SMAs with lengths ceil(L/2) and floor(L/2) + 1. For L = 9 that is SMA(5) of SMA(5), with nine-sample triangular weights. It is a different weighting from SMA(9), not a proven fixed reduction in crosses. The custom VAR uses a nine-change CMO multiplier and a zero recursive seed; a constant input can leave it at zero. Inspect each branch before treating it as an alternative default.
The helper accepts chop_upper and chop_lower but never reads them. In this copied script, the two hlines separately contain literal 61.8 and 38.2, so changing only the helper arguments will not change those lines or any output. Edit the hline values as well if changing the display. Pineify’s separate Visual Editor drawing implementation does wire its threshold fields to the hlines; this copied example has fixed values.
The script has no input declarations, alerts, strategy orders or confirmed-bar gate. Its unused time-range helpers do not apply a session filter. All three calculations can update while the chart bar is open.
Test a combination as an explicit hypothesis
- Validate the three outputs independently, including the gap example, warmup and equality color. Record exact source, session, timeframe and parameters.
- Define whether a rule uses RSI above its MA, a fresh crossover, or a close-confirmed crossover. These produce different events.
- If CHOP is a filter, specify threshold, state versus crossing, and treatment of missing or out-of-range readings. The existing plot applies no such filter automatically.
- Evaluate with trade execution timing, costs, sizing and an untouched test period. Compare filtered and unfiltered results on the same sample instead of assigning an unsupported win-rate improvement.
Adding the three lines does not create three independent confirmations. RSI and its average share the same series, and CHOP also uses the chart’s price history. No universal best timeframe, fixed crossover lag, or measured performance is established here.
Pine Script v6: implementation and limits
The example retains its eleven-case MA dispatcher and fixed default call. ATR(1) is evaluated once on every bar before the history-summing loop to avoid a scope-consistency diagnostic. Its helper thresholds are unused, its hlines are separately fixed, and its CHOP formula has no zero-range guard. Review the stated limits rather than assuming they are handled elsewhere.
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Pineify
//======================================================================//
// ____ _ _ __ //
// | _ (_)_ __ ___(_)/ _|_ _ //
// | |_) | | '_ \ / _ \ | |_| | | | //
// | __/| | | | | __/ | _| |_| | //
// |_| |_|_| |_|\___|_|_| \__, | //
// |___/ //
//======================================================================//
//@version=6
indicator(title="RSI RSI MA and Choppiness Index", overlay=false, max_labels_count=500)
//#region —————————————————————————————————————————————————— Custom Code
//#endregion ————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Common Dependence
p_comm_time_range_to_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
int start_unix_time = na
int end_unix_time = na
int start_time_hour = na
int start_time_minute = na
int end_time_hour = na
int end_time_minute = na
if str.length(time_range) == 11
// Format: hh:mm-hh:mm
start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
start_time_minute := math.floor(str.tonumber(str.substring(time_range, 3, 5)))
end_time_hour := math.floor(str.tonumber(str.substring(time_range, 6, 8)))
end_time_minute := math.floor(str.tonumber(str.substring(time_range, 9, 11)))
else if str.length(time_range) == 9
// Format: hhmm-hhmm
start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
start_time_minute := math.floor(str.tonumber(str.substring(time_range, 2, 4)))
end_time_hour := math.floor(str.tonumber(str.substring(time_range, 5, 7)))
end_time_minute := math.floor(str.tonumber(str.substring(time_range, 7, 9)))
start_unix_time := timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), start_time_hour, start_time_minute, 0)
end_unix_time := timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), end_time_hour, end_time_minute, 0)
[start_unix_time, end_unix_time]
p_comm_time_range_to_start_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
int start_time_hour = na
int start_time_minute = na
if str.length(time_range) == 11
// Format: hh:mm-hh:mm
start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
start_time_minute := math.floor(str.tonumber(str.substring(time_range, 3, 5)))
else if str.length(time_range) == 9
// Format: hhmm-hhmm
start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
start_time_minute := math.floor(str.tonumber(str.substring(time_range, 2, 4)))
timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), start_time_hour, start_time_minute, 0)
p_comm_time_range_to_end_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
int end_time_hour = na
int end_time_minute = na
if str.length(time_range) == 11
end_time_hour := math.floor(str.tonumber(str.substring(time_range, 6, 8)))
end_time_minute := math.floor(str.tonumber(str.substring(time_range, 9, 11)))
else if str.length(time_range) == 9
end_time_hour := math.floor(str.tonumber(str.substring(time_range, 5, 7)))
end_time_minute := math.floor(str.tonumber(str.substring(time_range, 7, 9)))
timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), end_time_hour, end_time_minute, 0)
p_comm_timeframe_to_seconds(simple string tf) =>
float seconds = 0
tf_lower = str.lower(tf)
value = str.tonumber(str.substring(tf_lower, 0, str.length(tf_lower) - 1))
if str.endswith(tf_lower, 's')
seconds := value
else if str.endswith(tf_lower, 'd')
seconds := value * 86400
else if str.endswith(tf_lower, 'w')
seconds := value * 604800
else if str.endswith(tf_lower, 'm')
seconds := value * 2592000
else
seconds := str.tonumber(tf_lower) * 60
seconds
p_custom_sources() =>
[open, high, low, close, volume]
//#endregion —————————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Ta Dependence
p_ta_Var_Func(src, length) =>
valpha = 2 / (length + 1)
vud1 = src > src[1] ? src - src[1] : 0
vdd1 = src < src[1] ? src[1] - src : 0
vUD = math.sum(vud1, 9)
vDD = math.sum(vdd1, 9)
vCMO = nz((vUD - vDD) / (vUD + vDD))
VAR = 0.0
VAR := nz(valpha * math.abs(vCMO) * src) + (1 - valpha * math.abs(vCMO)) * nz(VAR[1])
VAR
p_ta_Wwma_Func(src, length) =>
wwalpha = 1 / length
WWMA = 0.0
WWMA := wwalpha * src + (1 - wwalpha) * nz(WWMA[1])
WWMA
p_ta_Zlema_Func(src, length) =>
zxLag = length / 2 == math.round(length / 2) ? length / 2 : (length - 1) / 2
zxEMAData = src + src - src[zxLag]
ZLEMA = ta.ema(zxEMAData, length)
ZLEMA
p_ta_Tsf_Func(src, length) =>
lrc = ta.linreg(src, length, 0)
lrc1 = ta.linreg(src, length, 1)
lrs = lrc - lrc1
TSF = ta.linreg(src, length, 0) + lrs
TSF
p_ta_ma(series float source, simple int length, simple string ma_type) =>
switch ma_type
"SMA" => ta.sma(source, length)
"EMA" => ta.ema(source, length)
"RMA" => ta.rma(source, length)
"HMA" => ta.hma(source, length)
"WMA" => ta.wma(source, length)
"TMA" => ta.sma(ta.sma(source, math.ceil(length / 2)), math.floor(length / 2) + 1)
"VAR" => p_ta_Var_Func(source, length)
"WWMA" => p_ta_Wwma_Func(source, length)
"ZLEMA" => p_ta_Zlema_Func(source, length)
"TSF" => p_ta_Tsf_Func(source, length)
"VWMA" => ta.vwma(source, length)
p_ta_rsi_rsi_ma_chop(series float rsi_source, simple int rsi_length, simple int rsi_ma_length, simple string rsi_ma_type, simple int chop_length, simple float chop_upper, simple float chop_lower) =>
// RSI Calculation
rsi = ta.rsi(rsi_source, rsi_length)
// RSI MA Calculation
rsi_ma = p_ta_ma(rsi, rsi_ma_length, rsi_ma_type)
// Choppiness Index Calculation
atr_one = ta.atr(1)
atr_sum = 0.0
for i = 0 to chop_length - 1
atr_sum := atr_sum + atr_one[i]
high_max = ta.highest(high, chop_length)
low_min = ta.lowest(low, chop_length)
chop = 100 * math.log10(atr_sum / (high_max - low_min)) / math.log10(chop_length)
[rsi, rsi_ma, chop]
//#endregion —————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Constants
// Input Groups
string P_GP_1 = ""
//#endregion —————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Inputs
//#endregion ———————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Price Data
//#endregion ———————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Indicators
[p_ind_1_rsi, p_ind_1_rsiMa, p_ind_1_chop] = p_ta_rsi_rsi_ma_chop(close, 14, 9, "SMA", 14, 61.8, 38.2) // RSI RSI MA CHOP
//#endregion ———————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Conditions
//#endregion ———————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Indicator Plots
// RSI RSI MA CHOP
// RSI and RSI MA dynamic coloring
p_ind_1_rsi_color = p_ind_1_rsi > p_ind_1_rsiMa ? color.rgb(76, 175, 80, 0) : color.red
plot(p_ind_1_rsi, title="RSI RSI MA CHOP - RSI", color=p_ind_1_rsi_color, linewidth=2)
plot(p_ind_1_rsiMa, title="RSI RSI MA CHOP - RSI MA", color=color.rgb(255, 152, 0, 0), linewidth=1)
// RSI levels
hline(70, title="RSI RSI MA CHOP - RSI Overbought", color=color.rgb(242, 54, 69, 0))
hline(30, title="RSI RSI MA CHOP - RSI Oversold", color=color.rgb(76, 175, 80, 0))
// Choppiness Index
plot(p_ind_1_chop, title="RSI RSI MA CHOP - Choppiness Index", color=color.rgb(41, 98, 255, 0), linewidth=1)
hline(61.8, title="RSI RSI MA CHOP - Choppy", color=color.rgb(242, 54, 69, 0))
hline(38.2, title="RSI RSI MA CHOP - Trending", color=color.rgb(76, 175, 80, 0))
//#endregion ————————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Custom Plots
//#endregion —————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Alert
//#endregion ——————————————————————————————————————————————————————
Specify a checkable adaptation in Pineify
Request explicit parameter inputs, named validity flags for RSI/MA/CHOP, and threshold values wired consistently to any lines and conditions. If requesting an alert or strategy, specify closed-bar timing and undefined-value behavior. Validate the final source and reproduce the synthetic checks in TradingView; syntax success alone does not establish indicator equivalence or an investment result.
Open Pine Script AI Coding AgentEducational indicator analysis. No measured trading performance is established here. Syntax validation does not prove formula equivalence, execution assumptions or profitability.