TradingView indicator guides

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 Script

Follow 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 barSourceiSvIV
1100UnavailableUnavailableUnavailable
21011/600
310211/36117
410391/216194/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.

Pine Script v6
// 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 ——————————————————————————————————————————————————————
Existing indicator chart preview
Existing visual preview. Verify exact values against the source; the image is not a performance result.

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 Agent

Educational indicator analysis. No measured trading performance is established here. Syntax validation does not prove formula equivalence, execution assumptions or profitability.

Implementation questions

No. Its maximum new-source weight is 1/6, about 16.67%. The remaining 83.33% belongs to the prior output when vI is 1.

The copied helper replaces missing prior output with zero. With zero directional movement, its guarded normalization yields vI=0 and freezes that zero seed. This is a known boundary of this implementation.

No. Its one-value normalization range is always zero, forcing vI to zero. In this zero-seeded helper the output stays zero after valid inputs become available.

No. For finite readings it means the output exactly equals its preceding value. Frozen adaptation, equal source and output, or initialization behavior can cause that equality.

No. This exported configuration fixes close and length 6, with coloring enabled. Pineify’s Visual Editor exposes source, length and drawing controls separately.

No. Syntax validation cannot establish initialization suitability, market interpretation or profitable trading behavior. A different seed policy is a separate behavior change.