Variable Moving Average: Adaptive Weight and Seed Limits
This Variable Moving Average changes its update weight using a normalized, smoothed directional-imbalance series. The copied configuration uses close and length 6. Its weight is at most 1/6, not 86%, and its zero-seeded recurrence has important startup and flat-price limitations. Inspect those limits before using the line in a trading rule.
Inspect the Pine ScriptFollow the directional-ratio stages
Set k = 1/N. Split the source change into P = max(S − S_previous, 0) and M = max(S_previous − S, 0). Smooth both recursively with coefficient k. Divide each smoothed side by their sum, using zero when that sum is zero, and smooth these two ratios again with k.
Next take the absolute difference of the two smoothed ratios divided by their sum, again using zero for a zero denominator. Smooth that imbalance with k to obtain iS. Over the latest N iS values, find its highest and lowest values. The internal vI is (iS − lowest)/(highest − lowest), or zero when that range is zero. This vI is a normalized position within its local range, not the separately named Vortex Indicator.
The output update is V = (1 − a) × V_previous + a × S, where a = vI/N. The actual code substitutes zero for an unavailable V_previous. Source and recursive state availability therefore matter, especially at startup.
The adaptation is not a direct volatility magnitude measurement. On valid data, multiplying every source price by a positive constant scales both directional-movement sides equally; their normalized ratios, iS and vI remain unchanged. The algorithm reads no ATR, standard deviation, volume or wick extremes when source is close. Large absolute price changes alone do not imply a larger vI.
Check the maximum update weight
On complete finite data with positive integer N, the normalized vI lies between 0 and 1, so a lies between 0 and 1/N. At N = 6 and vI = 1, the new source gets 16.6667% weight and the previous output gets 83.3333%. At vI = 0.3, the new-source weight is 5%; at vI = 0 it is zero.
For an illustrative already-initialized previous value of 100 and current source 106: vI = 1 gives V = 101; vI = 0.3 gives V = 100.3; vI = 0 gives V = 100. These are conditional arithmetic examples, not claims that the internal vI will take those values on a specified market bar.
EMA(6) has coefficient 2/7, about 28.5714%, which is greater than this VMA(6)'s maximum. EMA(11) has coefficient 1/6; that matches one update coefficient only when vI = 1 and does not match initialization or the entire history. Changing VMA to length 3 does not make it an EMA(6), because vI still varies and its own window changes. No fixed speed or whipsaw advantage follows from the indicator name.
Do not overlook the zero seed
The source below retains the current helper behavior. It does not seed V from the first available price. Its nz(V_previous) fallback is zero. With a constant source of 100 from the start of loaded history and N = 6, the first bar lacks a prior source. On the second bar the directional values are zero, iS = 0 and vI = 0, so V becomes 0 and remains 0. That is an initialization artifact, not a meaningful average near the traded price.
A separate synthetic sequence 100, 101, 102, 103 with N = 6 produces the following recurrence. No earlier history is supplied.
| Loaded bar | Source | iS | vI | V |
|---|---|---|---|---|
| 1 | 100 | Unavailable | Unavailable | Unavailable |
| 2 | 101 | 1/6 | 0 | 0 |
| 3 | 102 | 11/36 | 1 | 17 |
| 4 | 103 | 91/216 | 1 | 94/3 ≈ 31.3333 |
For length 1, highest(iS, 1) equals lowest(iS, 1) whenever defined, so vI is zero. With the zero seed, the recurrence stays at zero after it obtains valid inputs, even if the source changes. Do not treat length 1 as a source-following version of this helper.
Loading a longer history changes the recursive starting state. A fixed number of elapsed bars does not guarantee the seed has become irrelevant, because updates can freeze when a = 0. A source-seeded revision needs its own initialization, missing-data and flat-range policy; it would change behavior and must be checked separately. The code here is a reference to inspect, not a claim that this limitation has been fixed.
Read colors as output comparisons
The line is green when its current value exceeds its previous value, red when lower, and blue otherwise. A finite blue value means exact equality, not a near-flat tolerance or proof of market congestion. The fallback color also does not establish validity when a comparison lacks history. Candle colors simply reuse the line color; they do not independently check whether the candle rose.
For valid initialized values and 0 < a < 1, V − V_previous = a(S − V_previous), while S − V = (1 − a)(S − V_previous). A rising V therefore already implies source above V in this same-source configuration. Combining those observations is not independent confirmation. If a = 0, the line can remain flat while price is far above or below it.
Green does not imply that close rose from the previous close. The line can still move toward a source that fell but remains above the previous line. Conversely, a flat blue line can result from the zero-range normalization or seed limitation rather than a low-risk range. Color changes are not orders, stops or evidence of profitability.
Match the version and test the boundaries
LazyBear's January 2015 publication describes an adaptive VMA and provides trend-color options. Its public source has unguarded directional and range divisions. The helper here adds zero-denominator guards, and the fixed exported configuration enables coloring. Those differences affect exceptional data and initialization; matching the title alone is insufficient.
This copy has no length or source input in TradingView Settings: it calls p_ta_vma(close, 6). The time-conversion helpers are unused by that call; they do not add session filtering or multi-timeframe behavior. The line and candle colors update intrabar, with no confirmation gate, alert conditions or strategy orders.
Start a comparison with constant source, a monotonic source, a reversal and missing data. Inspect iS, vI and V separately in a diagnostic revision, and distinguish a formula check from a strategy test. For any trading test define bar-close timing, entries, exits, size and costs, then use a separate evaluation period. No best timeframe or guaranteed reduction in false signals is established here.
Compare the distinct mechanisms in EMA and KAMA. Platform references: recursive execution and history and open-bar behavior.
Pine Script v6: implementation and limits
This is the complete fixed exported example. Its zero-seed and length-one limitations are described above; no correction to the underlying helper is implied. It draws the VMA and colors chart bars but does not add alerts, orders or confirmation. Compare startup and flat-price behavior before using it.
// 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="Variable Moving Average", overlay=true, max_labels_count=500)
//#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_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
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
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_vma(series float source, simple int length) =>
k = 1.0/length
pdm = math.max((source - source[1]), 0)
mdm = math.max((source[1] - source), 0)
pdmS = 0.0
mdmS = 0.0
pdmS := ((1 - k)*nz(pdmS[1]) + k*pdm)
mdmS := ((1 - k)*nz(mdmS[1]) + k*mdm)
s = pdmS + mdmS
pdi = s == 0 ? 0 : pdmS/s
mdi = s == 0 ? 0 : mdmS/s
pdiS = 0.0
mdiS = 0.0
pdiS := ((1 - k)*nz(pdiS[1]) + k*pdi)
mdiS := ((1 - k)*nz(mdiS[1]) + k*mdi)
d = math.abs(pdiS - mdiS)
s1 = pdiS + mdiS
iS = 0.0
iS := ((1 - k)*nz(iS[1]) + k*(s1 == 0 ? 0 : d/s1))
hhv = ta.highest(iS, length)
llv = ta.lowest(iS, length)
d1 = hhv - llv
vI = d1 == 0 ? 0 : (iS - llv)/d1
vma = 0.0
vma := (1 - k*vI)*nz(vma[1]) + k*vI*source
vma
//#endregion —————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Constants
// Input Groups
string P_GP_1 = ""
//#endregion —————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Inputs
//#endregion ———————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Price Data
//#endregion ———————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Indicators
p_ind_1 = p_ta_vma(close, 6) // VMA
//#endregion ———————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Conditions
//#endregion ———————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Indicator Plots
// VMA
p_ind_1_color = true ? (p_ind_1 > p_ind_1[1] ? color.rgb(76, 175, 80, 0) : p_ind_1 < p_ind_1[1] ? color.rgb(242, 54, 69, 0) : color.rgb(41, 98, 255, 0)) : color.rgb(41, 98, 255, 0)
plot(p_ind_1, title="VMA", color=p_ind_1_color, linewidth=3)
barcolor(true ? p_ind_1_color : na, title="VMA - Bar Color")
//#endregion ————————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Custom Plots
//#endregion —————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Alert
//#endregion ——————————————————————————————————————————————————————
Specify a checkable adaptation in Pineify
Pineify's Visual Editor exposes source, VMA Length (default 6), trend-color and bar-color controls, and one VMA output. For a different initialization policy, provide the exact source to the AI Coding Agent, specify how the first valid value and zero directional range should behave, and request explicit diagnostics. Review the changed formula, run a syntax check and compare synthetic outputs before backtesting. Access depends on the current product plan.
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.