Volume Moving Averages: Double-SMA Formula and Pine Script
The blue TRADE line is SMA(SMA(volume, 20), 10). The orange TREND line is SMA(SMA(volume, 100), 10). Both are smoothed twice, and there is no third signal plot. Their relationship describes averages of reported volume, not the direction of buying, the number of traders, or a measured probability of a breakout.
Inspect the Pine ScriptFollow both smoothing stages
Let F = SMA(volume, fastLength) and S = SMA(volume, slowLength). The helper returns TRADE = SMA(F, signalLength) and TREND = SMA(S, signalLength). The inner F and S values are not plotted. The word signal names the second smoothing length applied to both outputs; it is not a separate fast-only signal line.
The fixed call below uses lengths 20, 100 and 10. Its blue line is therefore not simply the latest 20-bar average. A label such as 2.5 million on that line is a weighted mean over the combined window, in the feed’s volume units. This is different from VWMA, which averages price using volume as weights.
Reproduce the calculation with five bars
Use hypothetical volumes 1, 2, 3, 4 and 5, with fastLength 2, slowLength 3 and signalLength 2. The fast inner SMA first appears on bar 2 at 1.5; subsequent values are 2.5, 3.5 and 4.5. The slow inner SMA first appears on bar 3 at 2, then 3 and 4.
| Bar | Volume | TRADE | TREND |
|---|---|---|---|
| 1 | 1 | Not ready | Not ready |
| 2 | 2 | Not ready | Not ready |
| 3 | 3 | 2 | Not ready |
| 4 | 4 | 3 | 2.5 |
| 5 | 5 | 4 | 3.5 |
On bar 4, TRADE = (2.5 + 3.5)/2 = 3, whereas TREND = (2 + 3)/2 = 2.5. This is an above/below relationship; bar 4 alone cannot establish a crossover because the preceding TREND value is unavailable. These synthetic numbers demonstrate arithmetic, not trading performance.
Understand the weights and initialization
For complete data, applying an N-bar SMA followed by an M-bar SMA uses N + M − 1 raw observations. Each pair of offsets contributes weight 1/(N × M). With N = M = 2, the three raw weights are 1/4, 1/2 and 1/4, not the equal thirds of SMA(3). With lengths 20 and 10, the 29 weights rise from 1/200 to 10/200, plateau, then fall symmetrically.
Consequently the default TRADE line needs 29 valid consecutive starting observations, and TREND needs 109 when every bar has finite volume. The mean ages of those weights are 14 and 54 bars. These are filter-weight properties, not promises that a crossover follows a price move by those delays.
Setting signalLength to 1 recovers the two inner SMAs. Setting fastLength equal to slowLength makes the outputs identical, so they cannot strictly cross. Multiplying every volume observation by the same positive factor multiplies both lines by that factor without changing their relative position. A change of reporting units halfway through the data is a different problem and can create an artificial shift.
Do not infer direction from total volume
Flat averages can represent stable high volume as easily as low volume. A constant sequence of 100,000 yields both lines at 100,000 once initialized. No comparison with an external baseline is implied. Likewise, a rising smoothed line does not require the newest volume bar to rise: with N = M = 2 and volumes 100, 200, 200, 150, the output rises from 175 to 187.5 while current volume falls from 200 to 150.
Identical volume sequences produce identical lines whether prices rise, fall or stay unchanged. The helper never reads price. A cross above is therefore not a bullish-volume label, while a cross below is not proof of selling. Reported volume also does not identify how many distinct traders participated, institutional activity, buyer-initiated trades, or “all sellers” transactions.
A price trend filter may form a separate research hypothesis, but it does not retroactively make the volume measure directional. Define entries, exits, fill timing and costs before evaluating such a hypothesis; neither a win rate nor a best timeframe is established here.
Inspect the provider and the current bar
TradingView documents that volume availability depends on the symbol and provider. Some feeds supply trade volume, some tick counts, and some no volume at all. Tick volume counts price updates rather than traded shares or contracts. Do not assume every forex, index or crypto symbol supplies the same measurement.
Check the exchange, session, chart interval and data history before comparing averages. A partial live bar has had less time to accumulate volume than a completed bar. Intraday activity can also vary systematically by time of day; these rolling windows do not normalize a lunchtime bar against prior lunchtimes.
The copy has no missing-data validity mask. Pine’s SMA can skip unavailable source observations, so the simple contiguous-window interpretation and the initialization counts above require complete data. A reported zero is a numeric observation and differs from unavailable volume. The live plots can change before close; the source includes no bar-close gate, crossover markers, alert conditions or orders.
Pine Script v6: implementation and limits
This retained example plots the two doubly smoothed series in a separate pane. Its time-range and custom-source helpers are unused; they do not add a session filter or a price source selector. All three lengths are fixed in the function call, and the script contains no execution system.
// 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="Volume Moving Averages", overlay=false, max_labels_count=500)
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]
p_ta_volume_moving_averages(simple int fastLength, simple int slowLength, simple int signalLength) =>
fastline = ta.sma(volume, fastLength)
slowline = ta.sma(volume, slowLength)
trade = ta.sma(fastline, signalLength)
trend = ta.sma(slowline, signalLength)
[trade, trend]
[p_ind_1_trade, p_ind_1_trend] = p_ta_volume_moving_averages(20, 100, 10)
plot(p_ind_1_trade, "Volume MA - TRADE", color.rgb(41, 98, 255, 0), 2)
plot(p_ind_1_trend, "Volume MA - TREND", color.rgb(255, 109, 0, 0), 2)
Specify a checkable adaptation in Pineify
Ask the Pine Script AI Coding Agent for explicit fast, slow and second-stage length inputs, clear TRADE/TREND labels, and a chosen missing-volume policy. If you add alerts, specify bar-close timing and test the five-bar arithmetic above before evaluating market data. The Visual Editor’s Volume Moving Averages component is marked paid; account access and generation limits should be checked in the product rather than inferred from this code example.
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.