Hull MA 2nd Derivative by Mashume work, heading towards back and forward test

daemonx

New member
@mashume
I came across your original Hull MA work about 2 years ago. I was just beginning to learn Thinkscript, but I remember really liking the math that you used, especially with concavity. It was elegant and intellectually intriguing, so it always stuck with me. The Z-score applied to divergence is the indicator's most underappreciated feature. You're normalizing the magnitude of curvature, not price itself, which has cool implications. I'm still an amateur at best in Thinkscript, but getting better. I recently came back to the indicator, and wanted to see if I could apply any enhancements that would help me provide enough guardrails to properly back and forward test your work.

The look ahead bias you originally encoded in MA_Max / MA_Min is an immediate back test dis-qualifier. HMA[-1] peeks one bar into the future. In ToS charting this renders correctly because the chart has already painted future bars, but in any back testing engine this resolves on data that doesn't exist yet. Every MA_Max/MA_Min signal is a phantom. The turning_point logic doesn't have this problem; it only uses concavity[1] and concavity; so signals derived from that path are clean. The fix has to be that true local maxima/minima must be confirmed one bar late, which is a little more "honest" in terms of charting. I also think (and added) another gaurdrail of a session VWAP that disqualifies or validates long or short positions based on distance from that VWAP as an important addition to incorporate into the future testing. I've been working on it for about a week now, and I think I'm pretty close to turning this into a testable script. I mainly trade SPX (using /ES for session VWAP), I think so far that this script is best on a 5 minute interval, so I've tried to tune it to that. Here is a pic of it in action:

Screenshot 2026-03-27 142000.png


I wanted to ask you a few questions if you were game to answer. First, here is my attempt at it:

Code:
#
# Hull MA Concavity System — Commander Deviation Build
# Author: xda3monx (Commander System)
# Version: 2026-03-27 V2
# Based on: Seth Urion / Mashume V4 (2020-05-01)
#
# CHANGES FROM V1:
#  [FIX]  session_start aligned to 945 (was 1100 — inconsistent with lower study)
#  [FIX]  Label 4 unicode
#  [NEW]  Session VWAP plot — RTH-anchored, volume-weighted (upper price overlay)
#  [NEW]  VWAP neutral band plots — optional ±vwap_neutral_band visual reference
#  [NEW]  Three-zone VWAP structural filter — above / neutral / below
#  [NEW]  Dynamic Z threshold — elevated when signal is counter-structure
#  [NEW]  VWAP gate rejection state in Label 4 — diagnostic for blocked signals
#  [NEW]  VWAP zone as Label 5
#
# CHANGES FROM ORIGINAL V4:
#  [FIX]  Lookahead bias in MA_Max/MA_Min removed (backtest-safe)
#  [FIX]  Z-score color logic corrected (OB=Red, OS=Green, intuitive)
#  [FIX]  Divergence normalized to basis points (price-agnostic, SPX-safe)
#  [FIX]  stddev_len and zlength unified — one lookback for both
#  [NEW]  State persistence gate: N bars must hold before signal confirmed
#  [NEW]  Z-score momentum gate: Z must move in signal direction
#  [NEW]  Session time window filter with background highlight
#  [NEW]  Raw turning points (dots) separated from gated entry arrows
#  [NEW]  Exhaustion exit markers (boolean wedges at |Z| > 3.0)
#  [NEW]  Composite status label (State | Z-score | Divergence bp | Gate)
#  [TUNE] HMA_Length default: 55 → 34 (5-min intraday optimized)
#  [TUNE] lookback default: 2 → 3 (less noisy second derivative)
#
# Licensed under GPL v3
# ─────────────────────────────────────────────────────────────────

declare upper;

# ═══════════════════════════════════════════
# INPUTS
# ═══════════════════════════════════════════

input price              = HLC3;
input HMA_Length         = 34;       # 34 bars x 5-min = 170min lookback
input lookback           = 3;        # Second derivative smoothing
input zlength            = 21;       # Unified Z-score + StdDev lookback
input min_z              = 1.2;      # Minimum |Z-score| for valid signal
input persist_bars       = 2;        # New state must hold N bars before signal fires
input show_session       = yes;      # Highlight active trading window
input session_start      = 945;      # Active window open  (ET, 24hr) — matches lower study
input session_end        = 1430;     # Active window close (ET, 24hr)

# VWAP filter inputs
# NOTE: use_vwap_filter must be set to no on SPX cash index (no volume data)
# Valid on /ES, /MES, and any volume-bearing instrument
input use_vwap_filter    = yes;      # Disable on SPX cash — no volume available
input vwap_neutral_band  = 15;       # Points either side of VWAP = structural neutral zone
input vwap_elevated_z    = 2.0;      # Z threshold required when trading counter-structure
input show_vwap_bands    = yes;      # Plot neutral band reference lines on chart

# ═══════════════════════════════════════════
# CORE ENGINE: Hull MA
# Declared as plot — appears as underlay
# beneath 4-state colored segments
# ═══════════════════════════════════════════

plot HMA = HullMovingAvg(price = price, length = HMA_Length);
HMA.SetDefaultColor(Color.DARK_GRAY);
HMA.SetLineWeight(1);
HMA.SetStyle(Curve.SHORT_DASH);

# ═══════════════════════════════════════════
# SECOND DERIVATIVE ENGINE
# delta      = recent slope over lookback bars
# next_bar   = linear projection of that slope
# concavity  = sign of (actual - projected)
#              +1 = bending upward  (concave up)
#              -1 = bending downward (concave down)
# ═══════════════════════════════════════════

def delta         = HMA[1] - HMA[lookback + 1];
def delta_per_bar = delta / lookback;
def next_bar      = HMA[1] + delta_per_bar;
def concavity     = if HMA > next_bar then 1 else -1;

# ═══════════════════════════════════════════
# DIVERGENCE — NORMALIZED (basis points)
# Scaled to price level — instrument-agnostic.
# 1 basis point = 0.01% of current HMA value
# ═══════════════════════════════════════════

def divergence = HMA - next_bar;
def norm_div   = if HMA != 0 then (divergence / HMA) * 10000 else 0;

# ═══════════════════════════════════════════
# Z-SCORE OF NORMALIZED DIVERGENCE
# Measures statistical charge of current curvature
# relative to recent history (zlength bars).
# High |Z| = statistically extreme curvature
# ═══════════════════════════════════════════

def z_mean   = Average(norm_div, zlength);
def z_std    = StDev(norm_div, zlength);
def zscore   = if z_std != 0 then (norm_div - z_mean) / z_std else 0;
def z_rising = zscore > zscore[1];

# ═══════════════════════════════════════════
# STATE MACHINE: 4-STATE CONCAVITY
#
# CCU_I  Concave Up,   Increasing  Green       Strong Bull
# CCU_D  Concave Up,   Decreasing  Dark Green  Bull Waning
# CCD_I  Concave Down, Increasing  Dark Orange Bear Waning
# CCD_D  Concave Down, Decreasing  Red         Strong Bear
#
# Rendering order: HMA underlay declared first,
# colored segments declared after (render on top)
# ═══════════════════════════════════════════

plot CCU_I = if concavity == 1  and HMA >  HMA[1] then HMA else Double.NaN;
CCU_I.SetDefaultColor(Color.GREEN);
CCU_I.SetLineWeight(3);

plot CCU_D = if concavity == 1  and HMA <= HMA[1] then HMA else Double.NaN;
CCU_D.SetDefaultColor(Color.DARK_GREEN);
CCU_D.SetLineWeight(3);

plot CCD_I = if concavity == -1 and HMA >= HMA[1] then HMA else Double.NaN;
CCD_I.SetDefaultColor(Color.DARK_ORANGE);
CCD_I.SetLineWeight(3);

plot CCD_D = if concavity == -1 and HMA <  HMA[1] then HMA else Double.NaN;
CCD_D.SetDefaultColor(Color.RED);
CCD_D.SetLineWeight(3);

# ═══════════════════════════════════════════
# STATE AGE COUNTER
# Counts bars since last concavity flip.
# Recursive def — valid ThinkScript pattern.
# Used for persistence gate below.
# ═══════════════════════════════════════════

def flip     = concavity != concavity[1];
def stateAge = if flip then 1 else stateAge[1] + 1;

# ═══════════════════════════════════════════
# TURNING POINTS — RAW (unfiltered, no lookahead)
# White dots = every concavity flip, no gates.
# Dot-to-arrow ratio is the primary tuning metric.
# ═══════════════════════════════════════════

plot raw_turn = if flip then HMA else Double.NaN;
raw_turn.SetPaintingStrategy(PaintingStrategy.POINTS);
raw_turn.SetDefaultColor(Color.WHITE);
raw_turn.SetLineWeight(2);

# ═══════════════════════════════════════════
# LOCAL PEAKS & TROUGHS — LOOKAHEAD-FREE
# Confirmed one bar after occurrence.
# Visual: marker appears one bar right of peak.
# Backtest-safe — no future data used.
# ═══════════════════════════════════════════

def confirmed_peak   = HMA[1] > HMA[2] and HMA[1] > HMA;
def confirmed_trough = HMA[1] < HMA[2] and HMA[1] < HMA;

plot MA_Max = if confirmed_peak   then HMA[1] else Double.NaN;
MA_Max.SetDefaultColor(Color.WHITE);
MA_Max.SetPaintingStrategy(PaintingStrategy.SQUARES);
MA_Max.SetLineWeight(3);

plot MA_Min = if confirmed_trough then HMA[1] else Double.NaN;
MA_Min.SetDefaultColor(Color.WHITE);
MA_Min.SetPaintingStrategy(PaintingStrategy.TRIANGLES);
MA_Min.SetLineWeight(3);

# ═══════════════════════════════════════════
# SESSION TIME GATE
# Background tint marks active trading window.
# HMA_34 on 5-min needs ~34 bars to stabilize
# (~2.8hrs). session_start = 945 provides
# 15min of warmup before HMA is queried.
# ═══════════════════════════════════════════

def inSession = SecondsFromTime(session_start) >= 0 and
                SecondsTillTime(session_end)   >  0;

AssignBackgroundColor(
    if show_session and inSession
    then CreateColor(15, 22, 15)
    else Color.CURRENT
);

# ═══════════════════════════════════════════
# SESSION VWAP — structural context layer
#
# Anchored to 9:30 RTH open daily.
# Resets via startRTH on first bar at/after 0930.
# IMPORTANT: requires volume data.
#   Valid on:  /ES, /MES, equities
#   Invalid on: SPX cash index (set use_vwap_filter = no)
#
# Three structural zones:
#   vwapZone =  1  Price above VWAP — sellers in control
#   vwapZone =  0  Neutral band — equilibrium, both directions valid
#   vwapZone = -1  Price below VWAP — buyers oversold
#
# Dynamic Z threshold:
#   Trading WITH structure  → min_z (standard gate)
#   Trading AGAINST structure → vwap_elevated_z (raised bar)
# ═══════════════════════════════════════════

def startRTH    = SecondsFromTime(0930) >= 0 and SecondsFromTime(0930)[1] < 0;
rec cumVol      = if startRTH then volume
                  else cumVol[1] + volume;
rec cumVolPrice = if startRTH then volume * HLC3
                  else cumVolPrice[1] + (volume * HLC3);

def sessionVWAP = if cumVol > 0 then cumVolPrice / cumVol else Double.NaN;
def vwapValid   = use_vwap_filter and !IsNaN(sessionVWAP) and
                  SecondsFromTime(0930) >= 0;

def vwapDist    = if vwapValid then close - sessionVWAP else 0;

def vwapZone    = if !vwapValid                          then  0
                  else if vwapDist >  vwap_neutral_band  then  1
                  else if vwapDist < -vwap_neutral_band  then -1
                  else                                         0;

# Dynamic Z thresholds — raised when signal is counter-structure
def longZReq    = if vwapZone ==  1 then vwap_elevated_z else min_z;
def shortZReq   = if vwapZone == -1 then vwap_elevated_z else min_z;

# VWAP line — plots at price level on upper chart
plot VWAP_Line = if vwapValid then sessionVWAP else Double.NaN;
VWAP_Line.SetDefaultColor(Color.VIOLET);
VWAP_Line.SetLineWeight(2);
VWAP_Line.SetStyle(Curve.FIRM);

# Neutral band reference lines — visual zone boundary
plot VWAP_Upper = if show_vwap_bands and vwapValid
                  then sessionVWAP + vwap_neutral_band else Double.NaN;
VWAP_Upper.SetDefaultColor(Color.DARK_GRAY);
VWAP_Upper.SetLineWeight(1);
VWAP_Upper.SetStyle(Curve.SHORT_DASH);

plot VWAP_Lower = if show_vwap_bands and vwapValid
                  then sessionVWAP - vwap_neutral_band else Double.NaN;
VWAP_Lower.SetDefaultColor(Color.DARK_GRAY);
VWAP_Lower.SetLineWeight(1);
VWAP_Lower.SetStyle(Curve.SHORT_DASH);

# ═══════════════════════════════════════════
# GATED ENTRY SIGNALS
#
# Long valid when ALL conditions true:
#   1. Concavity flipped to +1 AND held persist_bars
#   2. Z-score >= longZReq  (min_z or elevated if above VWAP)
#   3. Z-score is rising    (curvature building, not decaying)
#   4. Inside session window
#
# Short valid when: mirror conditions with shortZReq
# ═══════════════════════════════════════════

def confirmedBull = concavity == 1  and stateAge == persist_bars;
def confirmedBear = concavity == -1 and stateAge == persist_bars;

def longSignal  = confirmedBull and zscore >= longZReq  and  z_rising and inSession;
def shortSignal = confirmedBear and zscore <= -shortZReq and !z_rising and inSession;

# VWAP gate diagnostic — signal almost fired but VWAP elevated the threshold
def longVWAPBlocked  = confirmedBull and z_rising   and inSession
                       and zscore >= min_z and zscore <  longZReq;
def shortVWAPBlocked = confirmedBear and !z_rising  and inSession
                       and zscore <= -min_z and zscore > -shortZReq;

# ═══════════════════════════════════════════
# EXHAUSTION EXIT WARNINGS
# |Z| > 3.0 = statistically overextended.
# Fires ONCE on threshold crossing.
# SCALE-OUT / EXIT signal — not entry.
# ═══════════════════════════════════════════

def bullExhaust     = concavity == 1  and zscore >  3;
def bearExhaust     = concavity == -1 and zscore < -3;
def bullExhaustEdge = bullExhaust  and !bullExhaust[1];
def bearExhaustEdge = bearExhaust  and !bearExhaust[1];

plot BullExitWarn = if bullExhaustEdge then 1 else Double.NaN;
BullExitWarn.SetDefaultColor(Color.YELLOW);
BullExitWarn.SetPaintingStrategy(PaintingStrategy.BOOLEAN_WEDGE_DOWN);
BullExitWarn.SetLineWeight(3);

plot BearExitWarn = if bearExhaustEdge then 1 else Double.NaN;
BearExitWarn.SetDefaultColor(Color.YELLOW);
BearExitWarn.SetPaintingStrategy(PaintingStrategy.BOOLEAN_WEDGE_UP);
BearExitWarn.SetLineWeight(3);

# ═══════════════════════════════════════════
# SIGNAL ARROWS — GATED ENTRIES ONLY
# Cyan  arrow up   = Long  (all gates passed)
# Orange arrow dn  = Short (all gates passed)
# ═══════════════════════════════════════════

plot BuyArrow = if longSignal  then low  else Double.NaN;
BuyArrow.SetDefaultColor(Color.CYAN);
BuyArrow.SetPaintingStrategy(PaintingStrategy.ARROW_UP);
BuyArrow.SetLineWeight(3);

plot SellArrow = if shortSignal then high else Double.NaN;
SellArrow.SetDefaultColor(Color.DARK_ORANGE);
SellArrow.SetPaintingStrategy(PaintingStrategy.ARROW_DOWN);
SellArrow.SetLineWeight(3);

# ═══════════════════════════════════════════
# COMPOSITE STATUS LABELS
#
# Label 1: Concavity state
# Label 2: Z-score + color zone
# Label 3: Normalized divergence (bp)
# Label 4: Gate status / active signal / VWAP block
# Label 5: VWAP structural zone
# ═══════════════════════════════════════════

# Label 1: Concavity state
AddLabel(yes,
    if   concavity == 1  and HMA >  HMA[1] then "CCU_I ++"
    else if concavity == 1  and HMA <= HMA[1] then "CCU_D +-"
    else if concavity == -1 and HMA >= HMA[1] then "CCD_I -+"
    else                                           "CCD_D --",
    if   concavity == 1  and HMA >  HMA[1] then Color.GREEN
    else if concavity == 1  and HMA <= HMA[1] then Color.DARK_GREEN
    else if concavity == -1 and HMA >= HMA[1] then Color.DARK_ORANGE
    else Color.RED
);

# Label 2: Z-score — OB warm (yellow to red), OS cool (cyan to green)
AddLabel(yes,
    "Z: " + Round(zscore, 2),
    if   zscore >  3.0 then Color.RED
    else if zscore >  2.0 then Color.ORANGE
    else if zscore >  1.2 then Color.YELLOW
    else if zscore < -3.0 then Color.GREEN
    else if zscore < -2.0 then Color.DARK_GREEN
    else if zscore < -1.2 then Color.CYAN
    else Color.GRAY
);

# Label 3: Normalized divergence in basis points
AddLabel(yes,
    "D: " + Round(norm_div, 1) + "bp",
    if norm_div > norm_div[1] and concavity == 1  then Color.GREEN
    else if norm_div < norm_div[1] and concavity == 1  then Color.DARK_GREEN
    else if norm_div < norm_div[1] and concavity == -1 then Color.RED
    else Color.DARK_ORANGE
);

# Label 4: Gate status — priority ordered, VWAP blocks now surfaced
# [FIX] Replaced  (Tier 3) and ◆ (Tier 2) with ASCII-safe characters
AddLabel(yes,
    if      longSignal          then "▲ LONG ENTRY"
    else if shortSignal         then "▼ SHORT ENTRY"
    else if bullExhaust         then "[!] BULL EXHAUST"
    else if bearExhaust         then "[!] BEAR EXHAUST"
    else if longVWAPBlocked     then "▲ VWAP GATE"
    else if shortVWAPBlocked    then "▼ VWAP GATE"
    else if !inSession          then "[.] OFF-WINDOW"
    else if confirmedBull and zscore < min_z  then "▲ Z LOW"
    else if confirmedBull and !z_rising       then "▲ Z FADING"
    else if confirmedBear and zscore > -min_z then "▼ Z LOW"
    else if confirmedBear and z_rising        then "▼ Z FADING"
    else                                           "-- STANDBY",
    if      longSignal          then Color.CYAN
    else if shortSignal         then Color.DARK_ORANGE
    else if bullExhaust or
            bearExhaust         then Color.YELLOW
    else if longVWAPBlocked     then Color.BLUE
    else if shortVWAPBlocked    then Color.DARK_ORANGE
    else if !inSession          then Color.DARK_GRAY
    else if confirmedBull       then Color.DARK_GREEN
    else if confirmedBear       then Color.DARK_ORANGE
    else Color.DARK_GRAY
);

# Label 5: VWAP structural zone
AddLabel(use_vwap_filter,
    "VWAP: " +
    (if   !vwapValid         then "INIT"
     else if vwapZone ==  1  then "ABOVE +" + Round(vwapDist, 0) + "pt"
     else if vwapZone == -1  then "BELOW "  + Round(vwapDist, 0) + "pt"
     else                         "NEAR +"  + Round(AbsValue(vwapDist), 0) + "pt"),
    if   !vwapValid         then Color.DARK_GRAY
    else if vwapZone ==  1  then Color.DARK_ORANGE
    else if vwapZone == -1  then Color.CYAN
    else Color.WHITE
);

# ═══════════════════════════════════════════
# ALERTS
# ═══════════════════════════════════════════

Alert(condition = longSignal,      text = "HMA-C: Long Entry (Gated)",      "alert type" = Alert.BAR, sound = Sound.Bell);
Alert(condition = shortSignal,     text = "HMA-C: Short Entry (Gated)",     "alert type" = Alert.BAR, sound = Sound.Chimes);
Alert(condition = bullExhaustEdge, text = "HMA-C: Bull Exhaustion — Exit",  "alert type" = Alert.BAR, sound = Sound.Ding);
Alert(condition = bearExhaustEdge, text = "HMA-C: Bear Exhaustion — Exit",  "alert type" = Alert.BAR, sound = Sound.Ding);
Alert(condition = MA_Max,          text = "HMA-C: Local Peak Confirmed",    "alert type" = Alert.BAR, sound = Sound.NoSound);
Alert(condition = MA_Min,          text = "HMA-C: Local Trough Confirmed",  "alert type" = Alert.BAR, sound = Sound.NoSound);

My questions are (I made an assumption that you have tested or observed/recorded in some fashion):

1. What instrument and timeframe did you originally design and test V4 against?
2. Was the Z-score gate a core design element or added later, and what threshold did you find produced the highest signal-to-noise ratio?
3. What was the intended interpretation of the lookback parameter; smoothing convenience or a specific physics metaphor?
4. On what market regimes did you observe it fail most consistently?
5. Did you ever test persist_bars > 1 and what effect did it have on signal quality versus latency?
6. Did the normalized basis point divergence calculation behave differently at extreme price levels?
7. Was there a specific reason you chose HullMovingAvg over DEMA or TEMA for the concavity calculation?
8. The lookahead bias in MA_Max/MA_Min; were you aware of it, and was it intentional for visual smoothness on non-backtested charts?
9. I unified stddev_len and zlength into a single parameter. Do you see a reason they should remain separate in certain configurations?
10. As mentioned earlier, I added a VWAP structural filter that elevates the Z threshold when price is significantly away from session VWAP. Does this conflict with how you envisioned the Z-score functioning as a standalone signal?
11. Probably the most meta and important question for me is: In all your use of this indicator, what was the one condition that produced the most reliable signal and the one condition that produced the most dangerous false signal?

Sorry, I know this is a lot, and I don't expect anything, thanks in advance for your original work, please let me know if you have any questions.
 

Similar threads

Not the exact question you're looking for?

Start a new thread and receive assistance from our community.

87k+ Posts
1013 Online
Create Post

Similar threads

Similar threads

The Market Trading Game Changer

Join 2,500+ subscribers inside the useThinkScript VIP Membership Club
  • Exclusive indicators
  • Proven strategies & setups
  • Private Discord community
  • ‘Buy The Dip’ signal alerts
  • Exclusive members-only content
  • Add-ons and resources
  • 1 full year of unlimited support

Frequently Asked Questions

What is useThinkScript?

useThinkScript is the #1 community of stock market investors using indicators and other tools to power their trading strategies. Traders of all skill levels use our forums to learn about scripting and indicators, help each other, and discover new ways to gain an edge in the markets.

How do I get started?

We get it. Our forum can be intimidating, if not overwhelming. With thousands of topics, tens of thousands of posts, our community has created an incredibly deep knowledge base for stock traders. No one can ever exhaust every resource provided on our site.

If you are new, or just looking for guidance, here are some helpful links to get you started.

What are the benefits of VIP Membership?
VIP members get exclusive access to these proven and tested premium indicators: Buy the Dip, Advanced Market Moves 2.0, Take Profit, and Volatility Trading Range. In addition, VIP members get access to over 50 VIP-only custom indicators, add-ons, and strategies, private VIP-only forums, private Discord channel to discuss trades and strategies in real-time, customer support, trade alerts, and much more. Learn all about VIP membership here.
How can I access the premium indicators?
To access the premium indicators, which are plug and play ready, sign up for VIP membership here.
Back
Top