# Adaptive Trend Ensemble [BackQuant]
# Converted from Pine Script v6 to thinkScript - W. Kalvin
Author states:
Adaptive Trend Ensemble is an online-learning trend filter that combines eight different moving-average methods into one continuously weighted trend estimate.
Instead of selecting one moving average permanently, the indicator treats each method as an independent forecasting expert. Every bar, each expert is evaluated according to whether its previous slope correctly anticipated the direction of the latest price move.
Experts that were directionally correct retain more influence. Experts that were wrong lose influence through a multiplicative penalty. The weights are then normalised and used to blend all eight moving-average values into one adaptive ensemble line.
The indicator therefore attempts to answer two separate questions:
- Which smoothing method has recently aligned best with price direction?*
- How strongly do the weighted methods currently agree on the direction of trend?
The final output includes:
- A dynamically weighted ensemble trend line.
- Bullish and bearish trend-state colouring.
- A gradient between price and the ensemble.
- A consensus-driven glow.
- Trend-coloured candles.
- A live label showing the leading expert and its current weight.
- Alerts when the ensemble trend changes direction.
This is not a fixed moving average and it is not a simple average of several indicators. The contribution of each expert changes over time according to its recent directional performance.
Original Tradingivew script:
https://www.tradingview.com/script/hnrtyc7O-Adaptive-Trend-Ensemble-BackQuant/
Here is the ThinkOrSwim script:
Code:
# Adaptive Trend Ensemble [BackQuant]
# Converted from Pine Script v6 to thinkScript - W. Kalvin
# -------------------------------------------------------------------
# Inputs
# -------------------------------------------------------------------
input src = close;
input len = 30; # Base Lookback Length for all Moving Averages
# Online Learning Settings
input eta = 2.0; # Learning Rate (η)
input lossMode = {default "Directional (0/1)", "Magnitude-weighted"};
input floorW = 0.0; # Weight Floor
# UI Settings
input smooth = 25; # Line Smoothing (EMA)
input paintBar = yes; # Color Bars
input showClouds = yes; # Display Cloud Fill
# -------------------------------------------------------------------
# Moving Average Experts Bank (8 Base Models)
# -------------------------------------------------------------------
# 0: SMA
def e0 = Average(src, len);
# 1: EMA
def e1 = ExpAverage(src, len);
# 2: WMA
def e2 = WMA(src, len);
# 3: HMA (Fixed: HullMovingAvg instead of HullMovingAverage)
def e3 = HullMovingAvg(price = src, length = len);
# 4: DEMA
def e4 = 2 * ExpAverage(src, len) - ExpAverage(ExpAverage(src, len), len);
# 5: RMA (Wilder's Smoothing)
def e5 = WildersAverage(src, len);
# 6: ALMA Approximation (Replaced GaussianFilter with double-smoothed WMA)
def alma_wma = WMA(src, Floor(len * 0.85));
def e6 = WMA(alma_wma, Floor(len * 0.5));
# 7: LSMA (Linear Regression Curve / Inertia)
def e7 = Inertia(src, len);
# -------------------------------------------------------------------
# Directional Predictions & Loss Metrics
# -------------------------------------------------------------------
# Current directions (+1 or -1)
def d0 = if e0 > e0[1] then 1 else -1;
def d1 = if e1 > e1[1] then 1 else -1;
def d2 = if e2 > e2[1] then 1 else -1;
def d3 = if e3 > e3[1] then 1 else -1;
def d4 = if e4 > e4[1] then 1 else -1;
def d5 = if e5 > e5[1] then 1 else -1;
def d6 = if e6 > e6[1] then 1 else -1;
def d7 = if e7 > e7[1] then 1 else -1;
# Previous directions (Predictions made on prior bar)
def pd0 = d0[1];
def pd1 = d1[1];
def pd2 = d2[1];
def pd3 = d3[1];
def pd4 = d4[1];
def pd5 = d5[1];
def pd6 = d6[1];
def pd7 = d7[1];
# Realized move & Loss scale
def realized = Sign(close - close[1]);
def atrN = ATR(len);
def move = if atrN > 0 then AbsValue(close - close[1]) / atrN else 0.0;
# Indicator readiness guard
def isReady = BarNumber() > len + 2;
# Calculation of directional errors (1.0 = Wrong prediction, 0.0 = Correct)
def wrong0 = if pd0 == realized then 0.0 else 1.0;
def wrong1 = if pd1 == realized then 0.0 else 1.0;
def wrong2 = if pd2 == realized then 0.0 else 1.0;
def wrong3 = if pd3 == realized then 0.0 else 1.0;
def wrong4 = if pd4 == realized then 0.0 else 1.0;
def wrong5 = if pd5 == realized then 0.0 else 1.0;
def wrong6 = if pd6 == realized then 0.0 else 1.0;
def wrong7 = if pd7 == realized then 0.0 else 1.0;
# Loss penalty per expert
def lossMult = if lossMode == lossMode."Magnitude-weighted" then Min(move, 3.0) else 1.0;
def loss0 = wrong0 * lossMult;
def loss1 = wrong1 * lossMult;
def loss2 = wrong2 * lossMult;
def loss3 = wrong3 * lossMult;
def loss4 = wrong4 * lossMult;
def loss5 = wrong5 * lossMult;
def loss6 = wrong6 * lossMult;
def loss7 = wrong7 * lossMult;
# -------------------------------------------------------------------
# Hedge Multiplicative-Weights Online Learning Model
# -------------------------------------------------------------------
# Recursive un-normalized updates: w_i = w_i[1] * exp(-eta * loss_i)
rec raw_w0 = if !isReady then 0.125 else raw_w0[1] * Exp(-eta * loss0);
rec raw_w1 = if !isReady then 0.125 else raw_w1[1] * Exp(-eta * loss1);
rec raw_w2 = if !isReady then 0.125 else raw_w2[1] * Exp(-eta * loss2);
rec raw_w3 = if !isReady then 0.125 else raw_w3[1] * Exp(-eta * loss3);
rec raw_w4 = if !isReady then 0.125 else raw_w4[1] * Exp(-eta * loss4);
rec raw_w5 = if !isReady then 0.125 else raw_w5[1] * Exp(-eta * loss5);
rec raw_w6 = if !isReady then 0.125 else raw_w6[1] * Exp(-eta * loss6);
rec raw_w7 = if !isReady then 0.125 else raw_w7[1] * Exp(-eta * loss7);
def sumW = raw_w0 + raw_w1 + raw_w2 + raw_w3 + raw_w4 + raw_w5 + raw_w6 + raw_w7;
# Normalization & Floor Adjustments
def norm0 = if sumW > 0 then raw_w0 / sumW else 0.125;
def norm1 = if sumW > 0 then raw_w1 / sumW else 0.125;
def norm2 = if sumW > 0 then raw_w2 / sumW else 0.125;
def norm3 = if sumW > 0 then raw_w3 / sumW else 0.125;
def norm4 = if sumW > 0 then raw_w4 / sumW else 0.125;
def norm5 = if sumW > 0 then raw_w5 / sumW else 0.125;
def norm6 = if sumW > 0 then raw_w6 / sumW else 0.125;
def norm7 = if sumW > 0 then raw_w7 / sumW else 0.125;
def w0 = floorW + (1.0 - floorW * 8) * norm0;
def w1 = floorW + (1.0 - floorW * 8) * norm1;
def w2 = floorW + (1.0 - floorW * 8) * norm2;
def w3 = floorW + (1.0 - floorW * 8) * norm3;
def w4 = floorW + (1.0 - floorW * 8) * norm4;
def w5 = floorW + (1.0 - floorW * 8) * norm5;
def w6 = floorW + (1.0 - floorW * 8) * norm6;
def w7 = floorW + (1.0 - floorW * 8) * norm7;
# -------------------------------------------------------------------
# Ensemble Blending and Consensus
# -------------------------------------------------------------------
def ensVal = (w0 * e0) + (w1 * e1) + (w2 * e2) + (w3 * e3) + (w4 * e4) + (w5 * e5) + (w6 * e6) + (w7 * e7);
# Smoothed line
plot Ensemble = ExpAverage(ensVal, smooth);
# Trend evaluation
def trend = if Ensemble > Ensemble[1] then 1 else if Ensemble < Ensemble[1] then -1 else trend[1];
Ensemble.AssignValueColor(if trend == 1 then Color.GREEN else if trend == -1 then Color.RED else Color.GRAY);
Ensemble.SetLineWeight(3);
# -------------------------------------------------------------------
# Visual Enhancements (Glow, Clouds, and Bar Coloring)
# -------------------------------------------------------------------
def glowRange = atrN * 0.06;
plot GlowUpper = Ensemble + glowRange;
plot GlowLower = Ensemble - glowRange;
GlowUpper.SetDefaultColor(Color.CURRENT);
GlowLower.SetDefaultColor(Color.CURRENT);
GlowUpper.HideTitle();
GlowLower.HideTitle();
# Cloud Fills
AddCloud(if showClouds then GlowUpper else Double.NaN, GlowLower, Color.DARK_GRAY, Color.DARK_GRAY);
AddCloud(if showClouds and close > Ensemble then close else Double.NaN, Ensemble, Color.DARK_GREEN, Color.DARK_GREEN);
AddCloud(if showClouds and close < Ensemble then Ensemble else Double.NaN, close, Color.DARK_RED, Color.DARK_RED);
# Bar Coloring
AssignPriceColor(if paintBar then (if trend == 1 then Color.GREEN else if trend == -1 then Color.RED else Color.CURRENT) else Color.CURRENT);
# -------------------------------------------------------------------
# Alerts
# -------------------------------------------------------------------
def bullFlip = trend == 1 and trend[1] == -1;
def bearFlip = trend == -1 and trend[1] == 1;
Alert(bullFlip, "Adaptive Trend Ensemble: Turned Bullish", Alert.BAR, Sound.Chimes);
Alert(bearFlip, "Adaptive Trend Ensemble: Turned Bearish", Alert.BAR, Sound.Bell);
Last edited by a moderator: