Modified Bollinger Bands: RMS Width and Pine Script | Pineify
This implementation places symmetric bands around a selected moving average. Its width is the root mean square distance of the latest source values from the current basis. That equals the population standard deviation only when the basis is the arithmetic mean of that same window. The runnable example uses close, SMA 20, and a 1.5 multiplier.
Inspect the Pine ScriptThe exact formula: one current basis for the whole window
Let B be the current moving-average value, N the length, and m the multiplier. This code computes D = sqrt(sum((source[i] − B)², i = 0…N−1) / N), then upper = B + mD and lower = B − mD.
Every historical source value is compared with the same current B. The loop does not subtract each historical basis B[i]. With SMA, B is the window mean and D is the population standard deviation. With EMA, VAR, or another basis, D is generally an RMS distance around that basis.
For a complete finite window with arithmetic mean μ and population variance V, expanding the squares gives D² = V + (μ − B)². Changing the center can widen the bands even when the source window is unchanged. This is not an exponentially weighted variance calculation.
Worked window: identical prices, different centers
This synthetic example uses source values 98, 100, and 102, length 3, and multiplier 1.5. Their mean is 100 and population variance is 8/3. Centers 101 and 103 are assumed comparison values, not claimed outputs from a particular EMA history.
| Basis B | D | Lower | Upper |
|---|---|---|---|
| 100 | 1.632993 | 97.55051 | 102.44949 |
| 101 | 1.914854 | 98.127719 | 103.872281 |
| 103 | 3.41565 | 97.876525 | 108.123475 |
With B = 101, D² = 8/3 + 1. With B = 103, D² = 8/3 + 9. The higher center can even produce a lower lower-band value because its RMS width grows. The change does not prove that the market became more volatile.
For a fixed basis and source window, width = upper − lower = 2mD. Increasing m from 1.5 to 2 increases width by one third. It does not mathematically halve the number of band touches; that count depends on the data and the chosen touch definition.
What the copied script actually exposes
The helper supports these exact case-sensitive strings: SMA, EMA, RMA, HMA, WMA, TMA, VAR, WWMA, ZLEMA, TSF, and VWMA. The final call is fixed at close, 20, 1.5, and SMA. There are no input functions, so these four choices are not settings-panel controls in this copy. Edit the call or explicitly add inputs in a separate adaptation.
The output is an orange basis, two blue bands, and a blue fill. There are no signals, alerts, order management, higher-timeframe requests, or close-confirmation gates. Current close and the plotted bands can change before the bar closes.
The RMS loop requires N available source observations and a valid basis. It does not skip missing values within that fixed window. Recursive MA choices can retain history from before the latest N bars; matching length alone does not match initialization or values.
The shared Visual Editor helper and this standalone example must be compared separately. This example uses direct source-minus-basis subtraction. The current shared helper has a special small-residual branch that is absent here, so exact numerical equivalence is not asserted.
Alternative averages have their own contracts
VAR uses a nine-bar directional-change ratio, independent of the selected MA length. Its coefficient is 2/(N+1) times the absolute ratio of summed upward-minus-downward changes to their total. It measures directional balance, not trading volume or a direct volatility estimate.
This VAR helper initializes the previous output to zero. For an all-flat source of 100, the ratio becomes zero, alpha stays zero, and the output remains zero. Once the RMS window is complete, D is 100 and multiplier 1.5 gives bands −150 and 150. That is an initialization consequence, not 100 units of actual price dispersion. Verify or deliberately revise this branch before using it.
WWMA also starts from zero and is not automatically identical to a differently seeded RMA. TMA applies two SMAs; length 20 uses lengths 10 and 11. ZLEMA uses the local half-length lag convention. TSF adds a regression slope to the fitted current endpoint; it is a projection, not a known future price. VWMA depends on usable volume. None of these choices guarantees less lag or fewer false signals.
See the moving-average helper guide for branch and seed details.
Bollinger Bands defaults are not immutable rules
John Bollinger’s own rules identify 20 periods and two standard deviations as defaults. They also discuss exponential bands using exponential treatment for both the center and dispersion. Substituting this helper’s EMA basis while retaining its unweighted RMS loop is a different construction.
Do not infer that a 1.5 multiplier contains 87% of actual chart prices or that each outer band is touched 13% of the time. The rolling observations are not a fixed independent normal population, the current price affects its own bands, and non-SMA bases change the dispersion measure. A band touch alone does not establish a reversal.
ATR-based channels measure something different: true range includes distances from the previous close and can increase after a gap. ATR is not immune to gaps. Compare the exact smoothing and input definitions rather than assigning universal “good” or “poor” gap handling.
Reference: TradingView ATR calculation.
Specify squeezes and breakouts without impossible tests
A width cannot be strictly below the minimum of a window that already includes that same width. For a new low, compare with the previous window’s minimum; for membership at the current minimum, use equality under a defined numerical tolerance. The same issue applies to being strictly above the current maximum.
For normalized width, define how to handle a zero or negative basis before dividing. For a band touch, choose high/low touch or closing-price breach, and specify current bands versus previous-bar bands. These choices change the observations.
A narrow historical width does not determine a breakout direction or guarantee a breakout within one to three bars. Evaluate any trading rule with explicit observation time, next executable price, exits, sizing, costs, and an independent evaluation period. This page establishes no measured win rate.
Pine Script v6: implementation and limits
This complete standalone example retains the eleven-branch helper and direct RMS loop. The default call is fixed SMA 20 with multiplier 1.5. Syntax validation does not repair the documented initialization limitations of alternative branches or establish equivalence with other Bollinger implementations.
// 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="Modified Bollinger Bands", overlay=true, 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
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
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_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_pine_stdev(src, length, ma) =>
sumOfSquareDeviations = 0.0
for i = 0 to length - 1
sum = (src[i] + (-ma))
sumOfSquareDeviations := sumOfSquareDeviations + sum * sum
stdev = math.sqrt(sumOfSquareDeviations / length)
p_ta_modified_bollinger_bands(series float src, simple int len, series float mult, simple string ma_type) =>
ma = p_ta_ma(src, len, ma_type)
sd = p_ta_pine_stdev(src, len, ma)
basis = ma
upper_band = basis + mult * sd
lower_band = basis - mult * sd
[basis, upper_band, lower_band]
//#endregion
//#region --- Inputs
//#endregion
//#region --- Indicators
[p_ind_1_basis, p_ind_1_upper, p_ind_1_lower] = p_ta_modified_bollinger_bands(close, 20, 1.5, "SMA")
//#endregion
//#region --- Indicator Plots
plot(p_ind_1_basis, "Modified BB - Basis", color.rgb(255, 109, 0, 0), 1)
p_ind_1_upper_plot = plot(p_ind_1_upper, "Modified BB - Upper", color.rgb(41, 98, 255, 0), 1)
p_ind_1_lower_plot = plot(p_ind_1_lower, "Modified BB - Lower", color.rgb(41, 98, 255, 0), 1)
fill(p_ind_1_upper_plot, p_ind_1_lower_plot, title = "Modified BB - Band", color=color.rgb(33, 150, 243, 90))
//#endregion
Specify a checkable adaptation in Pineify
Ask the Pine Script AI Coding Agent to preserve or deliberately replace the RMS definition, expose only the inputs you need, and define initialization and missing-data handling. Verify the three-price example, flat data, and the chosen MA branch before adding any event or strategy rules.
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.