Volume Trend Tracker (VTT): Vortex Formula and Color Rules
Pineify labels this component Volume Trend Tracker, but its calculation uses high, low and close—not trading volume. It smooths two Vortex ratios and plots their absolute difference. Four named conditions choose the color, with a white fallback when none matches. A “strong” or “weak” label describes those conditions, not a measured trading advantage.
Inspect the Pine ScriptResolve the name and the calculation
The product configuration links to Vortex Trend Tracker by ProfitProgrammers, published July 3, 2019. That publication describes EMA smoothing of Vortex values and a difference histogram. This page documents the exact Pineify helper below; it does not assert that every later script with the same abbreviation behaves identically.
For each chart bar, positive movement is abs(high − previous low), and negative movement is abs(low − previous high). Sum each over N bars and divide by the same N-bar sum of true range. Here ta.atr(1) supplies the one-bar true range. It includes gaps through the previous close; neither movement expression reads volume.
Then P = EMA(positive ratio, M), Q = EMA(negative ratio, M), and histogram height = abs(P − Q). The fixed call uses N = 30 and M = 10. The height is nonnegative whenever both values are available. A red histogram is not a negative plotted value, and these ratios are not restricted to the interval 0 to 1.
Verify the ratios before interpreting colors
Consider four hypothetical high/low/close bars: (10, 8, 9), (11, 9, 10), (12, 10, 11), and (10, 7, 8). For bars 2 through 4, the positive movements are 3, 3, 0; negative movements are 1, 1, 5; and true ranges are 2, 2, 4.
With N = 2, bar 3 has ratios 6/4 = 1.5 and 2/4 = 0.5. On bar 4 they become 3/6 = 0.5 and 6/6 = 1. This directly disproves a 0–1 upper bound. With M = 2 and the first valid ratios used as EMA seeds, bar 4 gives P = (2/3 × 0.5) + (1/3 × 1.5) = 5/6 and Q = (2/3 × 1) + (1/3 × 0.5) = 5/6.
At that tie, Q has risen, so the literal strong-down condition is true although the histogram height is zero. This calculation illustrates the equality rule; it is not a forecast. The retained script has no explicit complete-window or zero-denominator guard, so inspect startup and missing-data behavior separately instead of assuming every dataset follows the complete-data fixture.
The exact color classification
| Color | Helper condition | What it checks |
|---|---|---|
| Dark green | P > Q and P ≥ previous P | P leads and is rising or exactly unchanged. |
| Light green | P > Q and P < previous P | P leads but has declined. |
| Dark red | Q ≥ P and Q > previous Q | Q rises and leads or ties. |
| Light red | Q > P and Q < previous Q | Q leads but has declined. |
| White fallback | None of the above | Includes some flat or equal-value cases. |
The inequalities are asymmetric. If Q exceeds P but Q is exactly unchanged, neither red condition is true: the bar is white. If P exceeds Q and P is unchanged, it is dark green. If P equals Q and Q is not rising, no named condition is true. Unavailable comparisons also do not establish a named state. White is the code’s fallback color, not a separately validated “neutral market” signal, and may be difficult to see against a white chart.
The helper returns both EMA series, the difference and four Boolean outputs, but this pasted script plots only the histogram. Its settings do not contain switches for plotting the two underlying lines.
Color and height answer different questions
A label checks the dominant line’s slope, not the slope of the difference. In a hypothetical state transition, P falls from 1.5 to 1.4 while Q falls from 1.2 to 1.0. The color is light green, but the height grows from 0.3 to 0.4. Conversely, P rising from 1.2 to 1.3 while Q rises from 0.5 to 1.2 produces dark green with a shrinking height of 0.7 to 0.1.
These classifier examples are assumed EMA values, not claims that a particular market generated them. They show why “strong” cannot be substituted for “histogram expanding,” and “weak” cannot be substituted for “reversal next.” A short height means the two smoothed ratios are close; it does not by itself establish low volatility, low volume or a directionless price path.
Two datasets with the same OHLC and different volumes produce the same VTT. Under valid finite data, multiplying all prices by a positive constant scales both directional distances and true range equally, leaving the ratios unchanged. Adding a constant to all prices also leaves those differences unchanged. The output is not a price target or a percentage return.
Apply timing and data checks before a strategy
The script calls the helper with fixed lengths 30 and 10 and contains no input functions, alerts, orders, pivot-divergence rules or bar-close gate. Current high, low and close can change during a live bar, so both color and height can change. Unused time-range helpers do not create session filtering or a multi-timeframe calculation.
The shared denominator can be zero in flat data, and the example does not explicitly reject missing OHLC windows. More smoothing changes the recursive response but does not establish a fixed two-bar delay or a guaranteed reduction in false signals. No universal minimum chart timeframe, win rate or optimal settings are demonstrated here.
If you test a color transition as a filter, decide whether white interrupts the sequence, whether equality counts as a reversal, and whether an entry requires a newly changed state or may recur on every qualifying bar. Require the intended confirmation time, then specify price rules, exits, sizing and costs separately. The indicator supplies none of those execution decisions.
Compare the unsmoothed construction in the Vortex Indicator guide and the platform’s live-bar behavior. An extra EMA or price filter still depends on related market data; it is not independent evidence of future profit.
Pine Script v6: implementation and limits
The full calculation and its original color rules are retained for inspection. The helper returns seven values; the single plot displays only the absolute difference with conditional coloring. Startup, zero-range behavior, equality and live-bar limitations are part of this implementation rather than silently corrected assumptions.
// 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 Trend Tracker", 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
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_vortex(posSum, negSum, vLen) =>
tRange = math.sum(ta.atr(1), vLen)
vmPlus = posSum / tRange
vmNeg = negSum / tRange
[vmPlus, vmNeg]
p_ta_vtt(simple int length, simple int ema_length) =>
[vPlus, vNeg] = p_ta_vortex(math.sum(math.abs(high - low[1]), length), math.sum(math.abs(low - high[1]), length), length)
plusEMA = ta.ema(vPlus, ema_length)
negEMA = ta.ema(vNeg, ema_length)
vDiff = math.abs(plusEMA - negEMA)
is_strong_uptrend = plusEMA > negEMA and plusEMA >= plusEMA[1]
is_weak_uptrend = plusEMA > negEMA and plusEMA < plusEMA[1]
is_strong_downtrend = negEMA >= plusEMA and negEMA > negEMA[1]
is_weak_downtrend = negEMA > plusEMA and negEMA < negEMA[1]
[plusEMA, negEMA, vDiff, is_strong_uptrend, is_weak_uptrend, is_strong_downtrend, is_weak_downtrend]
//#endregion —————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Constants
// Input Groups
string P_GP_1 = ""
//#endregion —————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Inputs
//#endregion ————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Price Data
//#endregion ——————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Indicators
[p_ind_1_plusEMA, p_ind_1_negEMA, p_ind_1_diff, p_ind_1_isStrongUptrend, p_ind_1_isWeakUptrend, p_ind_1_isStrongDowntrend, p_ind_1_isWeakDowntrend] = p_ta_vtt(30, 10) // VTT
//#endregion ————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Conditions
//#endregion ————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Indicator Plots
// VTT
plot(p_ind_1_diff, "VTT", style = plot.style_histogram, color = p_ind_1_isStrongUptrend ? color.rgb(27, 94, 32, 0) : p_ind_1_isWeakUptrend ? color.rgb(165, 214, 167, 0) : p_ind_1_isStrongDowntrend ? color.rgb(183, 28, 28, 0) : p_ind_1_isWeakDowntrend ? color.rgb(199, 106, 114, 0) : #FFFFFF, linewidth = 3)
//#endregion ————————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Custom Plots
//#endregion ————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Alert
//#endregion ————————————————————————————————————————————————————————
Specify a checkable adaptation in Pineify
Ask the Pine Script AI Coding Agent to expose N and M as inputs, show P and Q in the Data Window, and make the equality, missing-data and close-confirmation policies explicit. Verify both the numerical fixture and the flat-line classifier cases before evaluating signals on market data. The Visual Editor has a VTT configuration, but that does not make its controls available inside this fixed standalone copy or establish free access to every product workflow.
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.