Composite Trend Oscillator
Updated
TradingView

For free use on the TradingView platform

CODE DUELLO:

Have you ever stopped to wonder what the underlying filters contained within complex algorithms are actually providing for you? Wouldn't it be nice to actually visually inspect for that? Those would require some kind of wild west styled quick draw duel or some comparison method as a proper 'code duello'. Then it can be determined which filter can 'draw' the quickest from it's computational holster with the least amount of lag and smoothness.

In Pine we can do so, discovering how beneficial that would be. This can be accomplished by quickly switching from one filter to another by input() back and forth, requiring visual memory. A better way could be done by placing two indicators added to the chart and then eventually placed into one indicator pane on top of each other.By adding a filter() helper function that calls other moving average functions chosen for comparison, it can put to the test which moving average is the best drawing filter suited to our expected needs. PhiSmoother was formerly debuted and now it is utilized in a more complex environment in a multitude of ways along side other commonly utilized filters. Now, you the reader, get to judge for yourself...

FILTER VERSATILITY:

Having the capability to adjust between various smoothing methods such as PhiSmoother, TEMA, DEMA, WMA, EMA, and SMA on historical market data within the code provides an advantage. Each of these filter methods offers distinct advantages and hinderances. PhiSmoother stands out often by having superb noise rejection, while also being able to manipulate the fine-tuning of the phase or lag of the indicator, enhancing responsiveness to price movements.

The following are more well-known classic filters. TEMA (Triple Exponential Moving Average) and DEMA (Double Exponential Moving Average) offer reduced transient response times to price changes fluctuations. WMA (Weighted Moving Average) assigns more weight to recent data points, making it particularly useful for reduced lag. EMA (Exponential Moving Average) strikes a balance between responsiveness and computational efficiency, making it a popular choice. SMA (Simple Moving Average) provides a straightforward calculation based on the arithmetic mean of the data. VWMA and RMA have both been excluded for varying reasons, both being unworthy of having explanation here.

By allowing for adjustment refinements between these filter methods, traders may garner the flexibility to adapt their analysis to different market dynamics, optimizing their algorithms for improved decision-making and performance on demand.

INDICATOR INTRODUCTION:

ChartPrime's Composite Trend Oscillator operates as an oscillator based on the concept of a moving average ribbon. It utilizes up to 32 filters with progressively longer periods to assess trend direction and strength. Embedded within this indicator is an alternative view that utilizes the separation of the ribbon filaments to assess volatility. Both versions are excellent candidates for trend and momentum, both offering visualization of polarity, directional coloring, and filter crossings. Anyone who has former experience using RSI or stochastics may have ease of understanding applying this to their chart.

COMPOSITE CLUSTER MODES EXPLAINED:

In Trend Strength mode, the oscillator behavior signifies market direction and movement strength. When the oscillator is rising and above zero, the market is within a bullish phase, and visa versa. If the signal filter crosses the composite trend, this indicates a potential dynamic shift signaling a possible reversal. When the oscillator is teetering on its extremities, the market is more inclined to reverse later.

With Volatility mode, the oscillator undergoes a transformation, displaying an unbounded oscillator driven by market volatility. While it still employs the same scoring mechanism, it is now scaled according to the strength of the market move. This can aid with identification of ranging scenarios. However, one side effect is that the oscillator no longer has minimum or maximum boundaries. This can still be advantageous when considering divergences.

NOTEWORTHY SETTINGS FEATURES:

The following input settings described offer comprehensive control over the indicator's behavior and visualization.Common Controls:

  • Price Source Selection - The indicator offers flexibility in choosing the price source for analysis. Traders can select from multiple options.
  • Composite Cluster Mode - Choose between "Trend Strength" and "Volatility" modes, providing insights into trend directionality or volatility weighting.
  • Cluster Filter and Length - Selects a filter for the cluster composition. This includes a length parameter adjustment.

Cluster Options:

  • Cluster Dispersion - Users can adjust the separation between moving averages in the cluster, influencing the sensitivity of the analysis.
  • Cluster Trimming - By modifying upper and lower trim parameters, traders can adjust the sensitivity of the moving averages within the cluster, enhancing its adaptability.
  • PostSmooth Filter and Length - Choose a filter to refine the composite cluster's post-smoothing with a length parameter adjustment.
  • Signal Filter and Length - Users can select a filter for the lagging signal plot, also having a length parameter adjustment.
  • Transition Easing - Sensitivity adjustment to influence the transition between bullish and bearish colors.

    Enjoy


// This Source Code Form is subject to the terms of the Attribution-NonCommercial-ShareAlike 4.0 International License - (https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt)
// © ChartPrime

//@version=5
indicator("Composite Trend Oscillator [ChartPrime]", precision=0, timeframe='', timeframe_gaps = true)

PhiSmoother(series float source, simple int length, simple float phase=3.7)=>
    var array coefs = na
    var int length_1 = length - 1
    var float      W = 0.0
    if na(coefs)
        coefs := array.new()
        const float SQRT_PIx2  = math.sqrt(2.0 * math.pi)
        const float MULTIPLIER = -0.5 / 0.93
        var float     length_2 = length * 0.52353
        for int i=0 to length_1
            float alpha = (i + phase - length_2) * MULTIPLIER
            float  beta = 1.0 / (0.2316419 * math.abs(alpha) + 1.0)
            float   phi = (math.exp(math.pow(alpha, 2) * -0.5)
                         *-0.398942280) * beta *
                         ( 0.319381530  + beta *
                         (-0.356563782  + beta *
                         ( 1.781477937  + beta *
                         (-1.821255978  + beta
                         * 1.330274429)))) + 1.011
            if alpha < 0.0
                phi := 1.0 - phi
            float weight = phi / SQRT_PIx2
            coefs.push(weight)
            W += weight
    float sma2 = math.avg(source, nz(source[1], source))
    float E = 0.0
    for int i=0 to length_1
        E += coefs.get(i) * sma2[i]
    E / W

ema(series float source, simple float length)=>
    float alpha = 2.0 / (length + 1)
    var float smoothed = na
    smoothed := alpha * source + (1.0 - alpha) * nz(smoothed[1], source)

dema(series float source, simple float length)=>
    float ema1 = ema(source, length)
    float ema2 = ema(  ema1, length)
    2.0 * ema1 - ema2

tema(series float source, simple float length)=>
    float ema1 = ema(source, length)
    float ema2 = ema(  ema1, length)
    float ema3 = ema(  ema2, length)
    (ema1 - ema2) * 3.0 + ema3

wma(series float source, simple int length)=>
    float weight_sum = length * 0.5 * (length + 1)
    float sum = 0.0
    for int i=0 to length - 1
        sum += source[i] * (length - i)
    sum / weight_sum

sma(series float source, simple int length)=>
    float sum = ta.cum(source)
    if bar_index < length - 1
        sum / (bar_index + 1)
    else
        (sum - sum[length]) / length

filter(series float source,
       simple int   length,
       simple float  phase,
       simple string style)=>
    if length > 1
        switch style
            "PhiSmoother" => PhiSmoother(source, length, phase)
            "EMA"         =>         ema(source, length)
            "DEMA"        =>        dema(source, length)
            "TEMA"        =>        tema(source, length)
            "WMA"         =>         wma(source, length)
            =>                       sma(source, length) // "SMA"
    else
        source

method get_score(series array source)=>
    array scores = array.new()
    for int i=0 to source.size() - 1
        float current = source.get(i)
        int score_sum = 0
        for j = 0 to source.size() - 1
            float check = source.get(j)
            int polarity = i < j ? 1 : -1
            if i != j
                if current > check
                    score_sum += polarity
                else
                    score_sum -= polarity
        scores.push(score_sum)
    scores

method net_score(series array scores)=>
    int   value    = scores.size() - 1
    float netScore = ((scores.avg() + value) / (value * 2.0) - 0.5) * 200.0
    netScore

method get_color(series float              score,
                 simple float  transition_easing,
                 simple bool     volatility_mode,
                 simple color     rising_bullish,
                 simple color    falling_bullish,
                 simple color    falling_bearish,
                 simple color     rising_bearish,
                 simple color  rising_transition,
                 simple color falling_transition)=>
    var color grad = na
    float    delta = score - nz(score[1], score)
    color  bullish = delta >= 0.0 ? rising_bullish : falling_bullish
    color  bearish = delta >  0.0 ? rising_bearish : falling_bearish
    color  transit = score >  0.0 ? (delta >= 0.0 ? rising_transition : falling_transition)
                                  : (delta >= 0.0 ? falling_transition : rising_transition)
    if volatility_mode
        float easing =   0.01 * transition_easing
        float   crms = easing * math.sqrt(ta.cum(math.pow(math.abs(score), 2)) / (bar_index + 1))
        grad := if score > 0.0
            transition_easing > 0.0 ? color.from_gradient(score,  0.0, crms, transit, bullish) : bullish
        else
            transition_easing > 0.0 ? color.from_gradient(score, -crms, 0.0, bearish, transit) : bearish
    else
        grad := if score > 0.0
            transition_easing > 0.0 ? color.from_gradient(score,  0.0, transition_easing, transit, bullish) : bullish
        else
            transition_easing > 0.0 ? color.from_gradient(score, -transition_easing, 0.0, bearish, transit) : bearish
    grad



string common = "Common Controls"
float  source = input.source(           close,                 "Source", group=common)
string   mode = input.string("Trend Strength", "Composite Cluster Mode", group=common, options=["Trend Strength", "Volatility"], tooltip="Trend Strength visualizes the directionality of the filter cluster. Volatility weights the score to the bandwidth of the cluster.")
string filter = input.string(   "PhiSmoother",         "Cluster Filter", group=common, options=["PhiSmoother", "EMA", "DEMA", "TEMA", "WMA", "SMA"], tooltip="Choose a filter to build the moving average cluster with.")
float   phase = input.float (             3.7,   "   PhiSmoother Phase", group=common,  minval=0.0, step=0.1, tooltip="This allows for subtle adjustment (tweaking) of the phase/lag for PhiSmoother")

string cluster = "Cluster Options"
int    spacing = input.int(3,    "Cluster Dispersion", group=cluster,                 minval=1, maxval=10, tooltip="Choose the separation between the moving averages in the cluster.")
int upper_trim = input.int(0, "Cluster Trim - Upper:", group=cluster, inline="trim",  minval=0, maxval=31)
int lower_trim = input.int(0,              "  Lower:", group=cluster, inline="trim",  minval=0, maxval=31, tooltip="The 'Upper' parameter modifies the shortest period of the moving averages, whereas 'Lower' parameter adjusts the longest period. Increasing the upper value reduces sensitivity, while increasing the lower value heightens sensitivity.")

string output           = "Composite Post Smoothing"
string post_smooth_filt = input.string("PhiSmoother", "PostSmooth - Filter:", group=output, inline="post", options=["PhiSmoother", "EMA", "DEMA", "TEMA", "WMA", "SMA"])
int    post_smooth_len  = input.int   (            1,            "  Length:", group=output, inline="post",  minval=1, tooltip="Period of the cluster's post smoothing.")

string signal        = "Composite Signal Settings"
string signal_filter = input.string("PhiSmoother", "Signal - Filter:", group=signal, inline="signal", options=["PhiSmoother", "EMA", "DEMA", "TEMA", "WMA", "SMA"])
int    signal_length = input.int   (           20,        "  Length:", group=signal, inline="signal",  minval=1, tooltip="Period of the momentum signal plot.")
color  signal_color  = input.color (    #0044FF,     "Filter Color", group=signal)

string threshold = "Threshold Levels"
float upperLevel = input.float( 75.00, "Levels - Upper:", group=threshold, inline="level", minval=  1.0, maxval=99.0, step=2.0)
float lowerLevel = input.float(-75.00,        "  Lower:", group=threshold, inline="level", minval=-99.0, maxval=-1.0, step=2.0, tooltip="Fine-tune the thresholds to your liking")

string colors            = "Coloring Preferences"
bool  candle_color       = input.bool (    false,      "Candle Coloring", group=colors)
float transition_easing  = input.float(     50.0,    "Transition Easing", group=colors, maxval=  100.0, minval=0.0, step=5.0, tooltip="Adjust the sensitivity to ranging conditions.")
bool  fill_bg            = input.bool (     true, "Fill Background     ", group=colors, inline= "fill")
int   fill_alpha         = input.int  (       85,                     "", group=colors, inline= "fill", minval=0, maxval=100)
color rising_bullish     = input.color(#FFCC00,     "Bullish Color   ", group=colors, inline= "bull")
color falling_bullish    = input.color(#FDf0B9,                     "", group=colors, inline= "bull")
color rising_transition  = input.color(#9598A1,     "Transition Color", group=colors, inline="range")
color falling_transition = input.color(#CCCCCC,                     "", group=colors, inline="range")
color falling_bearish    = input.color(#5500CC,     "Bearish Color   ", group=colors, inline= "bear")
color rising_bearish     = input.color(#B580FF,                     "", group=colors, inline= "bear")

var bool VOLATILITY_MODE_ON = mode == "Volatility"

array filter_cluster = array.new(34)
filter_cluster.set( 0,        source)
filter_cluster.set( 1, filter(source,      spacing, phase, filter))
filter_cluster.set( 2, filter(source, 2  * spacing, phase, filter))
filter_cluster.set( 3, filter(source, 3  * spacing, phase, filter))
filter_cluster.set( 4, filter(source, 4  * spacing, phase, filter))
filter_cluster.set( 6, filter(source, 5  * spacing, phase, filter))
filter_cluster.set( 7, filter(source, 6  * spacing, phase, filter))
filter_cluster.set( 8, filter(source, 7  * spacing, phase, filter))
filter_cluster.set( 9, filter(source, 8  * spacing, phase, filter))
filter_cluster.set(10, filter(source, 9  * spacing, phase, filter))
filter_cluster.set(11, filter(source, 10 * spacing, phase, filter))
filter_cluster.set(12, filter(source, 11 * spacing, phase, filter))
filter_cluster.set(13, filter(source, 12 * spacing, phase, filter))
filter_cluster.set(14, filter(source, 13 * spacing, phase, filter))
filter_cluster.set(15, filter(source, 14 * spacing, phase, filter))
filter_cluster.set(16, filter(source, 15 * spacing, phase, filter))
filter_cluster.set(17, filter(source, 16 * spacing, phase, filter))
filter_cluster.set(18, filter(source, 17 * spacing, phase, filter))
filter_cluster.set(19, filter(source, 18 * spacing, phase, filter))
filter_cluster.set(20, filter(source, 19 * spacing, phase, filter))
filter_cluster.set(21, filter(source, 20 * spacing, phase, filter))
filter_cluster.set(22, filter(source, 21 * spacing, phase, filter))
filter_cluster.set(23, filter(source, 22 * spacing, phase, filter))
filter_cluster.set(24, filter(source, 23 * spacing, phase, filter))
filter_cluster.set(25, filter(source, 24 * spacing, phase, filter))
filter_cluster.set(26, filter(source, 25 * spacing, phase, filter))
filter_cluster.set(27, filter(source, 26 * spacing, phase, filter))
filter_cluster.set(28, filter(source, 27 * spacing, phase, filter))
filter_cluster.set(29, filter(source, 28 * spacing, phase, filter))
filter_cluster.set(30, filter(source, 29 * spacing, phase, filter))
filter_cluster.set(31, filter(source, 30 * spacing, phase, filter))
filter_cluster.set(32, filter(source, 31 * spacing, phase, filter))
filter_cluster.set(33, filter(source, 32 * spacing, phase, filter))

if upper_trim > 0
    for int i=0 to math.min(upper_trim - 1, filter_cluster.size() - 1)
        if  filter_cluster.size() > 2
            filter_cluster.shift()
        else
            break
if lower_trim > 0
    for int i=0 to math.min(lower_trim - 1, filter_cluster.size() - 1)
        if  filter_cluster.size() > 2
            filter_cluster.pop()
        else
            break

float ribbon_max   = filter_cluster.max()
float ribbon_min   = filter_cluster.min()
float ribbon_width = ribbon_max - ribbon_min
float ribbon_rank  = VOLATILITY_MODE_ON ? nz(ribbon_width / math.avg(ribbon_max, ribbon_min)) : 1

array score = filter_cluster.get_score()
float    net_score = filter(score.net_score() * ribbon_rank, post_smooth_len, 3.7, post_smooth_filt)
color   main_color = net_score.get_color(transition_easing, VOLATILITY_MODE_ON, rising_bullish, falling_bullish, falling_bearish, rising_bearish, rising_transition, falling_transition)
float signal_value = signal_length < 2 ? na : filter(ta.sma(net_score, 2), signal_length, 3.7, signal_filter)



top    = hline(VOLATILITY_MODE_ON ? na :      100.0,    "Top",       #FF0000)
upper  = hline(VOLATILITY_MODE_ON ? na : upperLevel, "+Level",  rising_bullish, hline.style_dotted, 2)
center = hline(                                 0.0, "Center",       #CCCCCC)
lower  = hline(VOLATILITY_MODE_ON ? na : lowerLevel, "+Level", falling_bearish, hline.style_dotted, 2)
bottom = hline(VOLATILITY_MODE_ON ? na :     -100.0, "Bottom",       #00FF00)
const color invisible = #00000000
fill(   top, upper,      100.0, upperLevel, #800000, invisible)
fill(center, upper, upperLevel,        0.0, color.new( rising_bullish, 100),  color.new(rising_bullish, fill_bg ? fill_alpha : 100))
fill(center, lower,        0.0, lowerLevel, color.new(falling_bearish, fill_bg ? fill_alpha : 100), color.new(falling_bearish, 100))
fill(bottom, lower, lowerLevel,     -100.0, invisible, #008000)

barcolor(candle_color ? main_color : na)

net  = plot(net_score, "Score", main_color, 3)
zero = plot(      0.0,      "", invisible)
plot(signal_value, "Signal", signal_color, 1)
fill(net, zero,  net_score > 0.0 ? net_score : 0.0,
                 net_score > 0.0 ? 0.0 : net_score,
                 net_score > 0.0 ? color.new(rising_bullish, fill_bg and VOLATILITY_MODE_ON ? fill_alpha : 100) : color.new(falling_bearish, 100),
                 net_score > 0.0 ? color.new(rising_bullish, 100) : color.new(falling_bearish, fill_bg and VOLATILITY_MODE_ON ? fill_alpha : 100))

Get access to our Exclusive
premium indicators