Fisher-KPP Double-Pass Smoothed Wave Indicator For ThinkOrSwim

whoDAT

Active member
Plus

The Fisher-KPP Double-Pass Smoothed Wave Indicator​

The Fisher-KPP Double-Pass Smoothed Wave indicator is an advanced, non-linear trend-following and mean-reversion algorithm designed for the Thinkorswim (TOS) platform. By combining partial differential equations (PDEs) from physical chemistry and biology with a high-performance backtesting engine, this system is designed to model, filter, and trade macro price waves.

IHAp1GY.png

1. Scientific Foundation & Background​

The mathematical engine of this indicator is inspired by the Fisher-Kolmogorov-Petrovsky-Piskunov (Fisher-KPP) equation, which historically describes the physical propagation of reaction-diffusion waves (such as a beneficial genetic trait spreading through a population, or a flame front propagating).
Screenshot 2026-07-14 204025.png

In physics, the Fisher-KPP equation describes how a self-sustaining wave front moves through a medium without losing its shape (like a drop of dye spreading in water).
This indicator replaces "space" with "historical price data." It uses two opposing forces to model trend momentum:
  • Diffusion (The Reaction Filter): This acts as your low-pass filter. It bleeds new price changes into the system's memory, dampening random market noise.
  • Reaction (The Accelerator): This is the logistic term (r u (1 - u)). When the market is in choppy equilibrium (u approximates 0.5), this term is at its maximum sensitivity, ready to aggressively accelerate the wave if a breakout occurs. As the trend matures and reaches saturation (u --> 1.00), this term decays to zero, locking the wave in and preventing whipsaws.
In this financial adaptation, price action serves as the "reaction trigger" that initiates a trend, while time-series memory acts as the "diffusion medium."
  • The Double-Pass Architecture:Instead of using classic lagging indicators, the system runs two recursive Fisher-KPP loops in sequence.
    • Pass 1 maps standardized, noise-filtered price displacement into a logistic sigmoid curve, modeling the initial kinetic reaction to a price move. This builds a smooth baseline wave.
    • Pass 2 ingests the heavily smoothed output of Pass 1 and calculates its momentum velocity to drive a secondary wave. It uses the velocity (rate of change) of that first wave to trigger its reaction engine.
  • The Zero-Lag Generational Filter:Each pass is conditioned by ultra-smooth, long-period (120-period) exponential moving averages. By processing price transitions through these dual non-linear stages, the final wave (smoothed_u2) stays extremely smooth without introducing the catastrophic lag associated with standard moving averages of similar lengths.
    • Because the input to Pass 2 is already smooth, its velocity is clean.
    • This allows the final signal wave (u2) to turn instantly at major market pivots with virtually zero lag.
      Graph that highlights Pass 2 (smoothed_u2): To show the responsiveness and smoothing of this technique.

      Screenshot 2026-07-14 222646.png



      2. Tuning Parameters (The Core Inputs).

      To adapt the speed, sensitivity, and stability of the wave propagation to different instruments, you can adjust the following tuning inputs:
      InputDefaultMathematical FunctionPractical Impact on Signals
      srcvwapPrice basis for calculations.Using vwap rather than close anchors the initial displacement to volume-weighted institutional value.
      length40Period for the Hull-filtered True Range.Establishes the threshold for "market noise." A higher value makes the baseline standardization less sensitive to brief volatility spikes.
      selection_advantage0.40The r growth factor in the logistic equation.Controls how aggressively the wave reacts to price displacement. Higher values make the wave reach its boundary zones faster, generating earlier signals.
      diffusion_coefficient0.20The D spatial coefficient.Controls the rate at which current wave values bleed into historical values. High diffusion speeds up wave transitions, while low diffusion dampens them.
      wave_inertia0.80Memory feedback factor.Sets the weight of past values u against new inputs. At 0.80, the wave maintains high memory, smoothing out whipsaws but delaying exits.
      smooth_pass1_length & smooth_pass2_length120Post-computation smoothing filters.The primary macro filters. Defaulted to 120 to guarantee that only major structural waves trigger trading signals.
  • Structural Thresholds: Long entries trigger when the wave enters oversold territory (< 0.48) and curls upward. Short entries trigger when the wave enters overbought territory (> 0.52) and curls downward. Exits are dynamically executed when the wave returns to its mathematical equilibrium state (0.50).

3. The Backtest Profit Engine & Its Inputs​

Built directly into the indicator, this state machine processes historical data to simulate realistic trading performance, displaying real-time metrics in a clean on-chart dashboard.
  • Dynamic Price Bars & Alternating Signal Execution: The system automatically paints the chart bars to match your trade position state (Cyan for Long, Orange for Short). It features an alternating signal lock (lastSignalDir) that strictly prevents duplicate, compounding entries in the same direction, ensuring a clean "Long --> Flat --> Short" transition sequence.
  • Next-Bar Entry Logic: Rather than unrealistically entering at the close of the signal bar, the profit engine registers the trigger and executes the trade at the Open price of the next bar, reflecting true slippage and execution realities.
  • Engine Inputs:
    • Take_Profit_Dollars ($100000): The absolute dollar target per contract before the engine registers a profit-target exit. The $100,000 selection will allow the trade to fully play out.
    • Stop_Loss_Dollars ($100000): The absolute dollar risk allowed per trade before registering a stop-out. The $100,000 selection will allow the trade to fully play out.
    • Trade_Commission ($4.50): Deducts round-trip transaction costs from every trade result to ensure the Net PnL is accurate and realistic.
    • TicksLostToSpread ($0.0): Allows you to simulate additional slippage for highly liquid or illiquid instruments.
    • Display_Mode (Dollars / Pips): Toggles the entire dashboard, labels, and chart bubbles between fiat currency reporting and tick/pip/point increments.
The profit engine automatically extracts the chart instrument’s structural properties via TOS APIs (TickSize() and TickValue()) to seamlessly handle contract multiplier conversions across different asset classes.

4. Performance Profile: /ES Futures and /MNQ Futures on the 15-Minute Timeframe​

This system could be useful for trading the E-mini S&P 500 futures (/ES) or Micro E-mini NASDAQ-100 futures (/MNQ) on a 15-minute intraday chart.

Screenshot 2026-07-14 215014.png


Screenshot 2026-07-14 220511.png


Code:
#  Fisher-KPP Double-Pass Smoothed Wave with Backtest Profit Engine
#  by whoDAT
#  7/2026

declare upper;

# --- Inputs: Fisher-KPP Engine ---
input src = vwap;
input length = 40;
input selection_advantage = 0.40; # 'r' factor
input diffusion_coefficient = 0.20; # 'D' factor
input wave_inertia = 0.80; # Memory factor

# Tuning knobs defaulted to 120 for macro stability and smooth wave propagation
input smooth_pass1_length = 120;
input smooth_pass2_length = 120;

# Structural trigger thresholds
def long_entry_zone = 0.48;
def short_entry_zone = 0.52;
def exit_line = 0.50;

# --- Inputs: Profit Engine & Visual Settings ---
input Take_Profit_Dollars = 100000;
input Stop_Loss_Dollars = 100000;
input Trade_Commission = 4.50;
input TicksLostToSpread = 0.0;
input ShowProfitBubbles = yes;
input ShowDebugBubbles = no;
input Display_Mode = {default "Dollars", "Pips"};

# --- 1. SAFE ACCUMULATION GATE & PRE-FLIGHT GUARD ---
# Prevents any calculations from executing until the longest moving average has loaded
def max_smoothing = Max(smooth_pass1_length, smooth_pass2_length);
def isReady = BarNumber() > Max(max_smoothing, length);

# --- 2. Background Variation & Market Noise ---
def price_change = src - src[1];
def raw_noise = MovingAverage(AverageType.HULL, TrueRange(high, src, low), length);
# Ensure noise is never NaN or Zero
def noise = if IsNaN(raw_noise) or raw_noise == 0 then 1 else raw_noise;

def smoothed_change = ExpAverage(price_change, 3);
def raw_displacement = smoothed_change / noise;
def standardized_displacement = if IsNaN(raw_displacement) then 0 else raw_displacement;

# --- 3. Pass 1: Raw Trait Mapping into Fisher-KPP Loop 1 ---
def raw_target_trait_1 = 1 / (1 + Exp(-standardized_displacement));
def target_trait_1 = if IsNaN(raw_target_trait_1) then 0.5 else raw_target_trait_1;

def u1;
# Protect the history steps inside CompoundValue from swallowing early-chart NaNs
def historical_u1 = CompoundValue(1,
    wave_inertia * (if IsNaN(u1[1]) then 0.5 else u1[1]) +
    (1 - wave_inertia) * (if IsNaN(target_trait_1[1]) then 0.5 else target_trait_1[1]),
    0.5
);

def spatial_diff_1 = (target_trait_1 - historical_u1) * diffusion_coefficient;
def logistic_react_1 = selection_advantage * standardized_displacement * historical_u1 * (1 - historical_u1);
u1 = Max(0.001, Min(0.999, historical_u1 + spatial_diff_1 + logistic_react_1));

# Generational Smooth applied to Pass 1 Output
def raw_smoothed_u1 = ExpAverage(u1, smooth_pass1_length);
def smoothed_u1 = if IsNaN(raw_smoothed_u1) then 0.5 else raw_smoothed_u1;

# --- 4. Pass 2: Secondary Fisher-KPP Loop (The Double Pass ---
def u2;
def historical_u2 = CompoundValue(1,
    wave_inertia * (if IsNaN(u2[1]) then 0.5 else u2[1]) +
    (1 - wave_inertia) * (if IsNaN(smoothed_u1[1]) then 0.5 else smoothed_u1[1]),
    0.5
);

def spatial_diff_2 = (smoothed_u1 - historical_u2) * diffusion_coefficient;

# The velocity of the smoothed first wave drives the secondary reaction phase
def raw_velocity = smoothed_u1 - smoothed_u1[1];
def wave1_velocity = if IsNaN(raw_velocity) then 0 else raw_velocity;

def logistic_react_2 = selection_advantage * wave1_velocity * historical_u2 * (1 - historical_u2);
u2 = Max(0.001, Min(0.999, historical_u2 + spatial_diff_2 + logistic_react_2));

# Generational Smooth applied to Pass 2 Output (The Signal Path)
def raw_smoothed_u2 = ExpAverage(u2, smooth_pass2_length);
def smoothed_u2 = if IsNaN(raw_smoothed_u2) then 0.5 else raw_smoothed_u2;

# --- 5. Fisher-KPP Wave Reversals ---
# Long Trigger: u2 is low (< 0.48) and starts increasing
def raw_buy_trigger = smoothed_u2 < long_entry_zone and smoothed_u2 > smoothed_u2[1];
# Short Trigger: u2 is high (> 0.52) and starts decreasing
def raw_sell_trigger = smoothed_u2 > short_entry_zone and smoothed_u2 < smoothed_u2[1];

# --- 6. Strict Directional Trigger Integration ---
def LongSignalRaw  = if !isReady then no else raw_buy_trigger;
def ShortSignalRaw = if !isReady then no else raw_sell_trigger;

# Strict alternating signal enforcement
rec lastSignalDir = CompoundValue(1, if !isReady then 0 else if LongSignalRaw then 1 else if ShortSignalRaw then -1 else lastSignalDir[1], 0);
def LongSignal  = isReady and LongSignalRaw and lastSignalDir[1] != 1;
def ShortSignal = isReady and ShortSignalRaw and lastSignalDir[1] != -1;

# --- 7. Quantitative Profit Engine Core ---
def ts = if !IsNaN(TickSize()) and TickSize() > 0 then TickSize() else 0.0001;
def tv = if !IsNaN(TickValue()) and TickValue() > 0 then TickValue() else 1.0;
def pipSize = if ts == 0.00001 then 0.0001 else if ts == 0.001 then 0.01 else ts;
def pv = tv * (pipSize / ts);

# Conversions for Target Pricing
def pointsToProfit = Take_Profit_Dollars / (tv / ts);
def pointsToLoss = Stop_Loss_Dollars / (tv / ts);

# Position State Handler
rec tradeState = CompoundValue(1,
    if !isReady then 0
    else if tradeState[1] == 0 then (
        if LongSignal[1] then open
        else if ShortSignal[1] then -open
        else 0
    )
    else if tradeState[1] > 0 then (
        if ShortSignal or (high - tradeState[1]) >= pointsToProfit or (tradeState[1] - low) >= pointsToLoss then 0
        else tradeState[1]
    )
    else (
        if LongSignal or (AbsValue(tradeState[1]) - low) >= pointsToProfit or (high - AbsValue(tradeState[1])) >= pointsToLoss then 0
        else tradeState[1]
    ), 0);

def pos = Sign(tradeState);

def longExit  = isReady and tradeState[1] > 0 and tradeState == 0;
def shortExit = isReady and tradeState[1] < 0 and tradeState == 0;
def tradeClosed = longExit or shortExit;

rec entryPrice = CompoundValue(1, if !isReady then 0 else if (tradeState crosses above 0 or tradeState crosses below 0) then open else entryPrice[1], 0);

# Synchronized Exit Execution
def exitPrice = if !tradeClosed then close
                else if longExit then (
                    if ShortSignal then close
                    else if (high - entryPrice[1]) >= pointsToProfit then entryPrice[1] + pointsToProfit
                    else entryPrice[1] - pointsToLoss
                )
                else (
                    if LongSignal then close
                    else if (entryPrice[1] - low) >= pointsToProfit then entryPrice[1] - pointsToProfit
                    else entryPrice[1] + pointsToLoss
                );

# Calculated Performance
def longResult  = if longExit and !IsNaN(entryPrice) and !IsNaN(exitPrice) then (exitPrice - entryPrice) / pipSize * pv - Trade_Commission else 0;
def shortResult = if shortExit and !IsNaN(entryPrice) and !IsNaN(exitPrice) then (entryPrice - exitPrice) / pipSize * pv - Trade_Commission else 0;

def currentTradeResult = longResult + shortResult;

# Performance Metrics Accumulation (Protected Against NaN Degradation)
rec totalPnl   = if IsNaN(totalPnl[1]) then 0 else totalPnl[1] + currentTradeResult;
rec tradeCount = if IsNaN(tradeCount[1]) then 0 else tradeCount[1] + (if tradeClosed then 1 else 0);

rec pLong  = if IsNaN(pLong[1]) then 0 else pLong[1] + longResult;
rec cLong  = if IsNaN(cLong[1]) then 0 else cLong[1] + (if longExit then 1 else 0);
rec wLong  = if IsNaN(wLong[1]) then 0 else wLong[1] + (if longExit and longResult > 0 then 1 else 0);

rec pShort = if IsNaN(pShort[1]) then 0 else pShort[1] + shortResult;
rec cShort = if IsNaN(cShort[1]) then 0 else cShort[1] + (if shortExit then 1 else 0);
rec wShort = if IsNaN(wShort[1]) then 0 else wShort[1] + (if shortExit and shortResult > 0 then 1 else 0);

rec mWin  = if IsNaN(mWin[1]) then 0 else if currentTradeResult > mWin[1] then currentTradeResult else mWin[1];
rec mLoss = if IsNaN(mLoss[1]) then 0 else if currentTradeResult < mLoss[1] then currentTradeResult else mLoss[1];

def wrL = if cLong > 0 then (wLong / cLong) * 100 else 0;
def wrS = if cShort > 0 then (wShort / cShort) * 100 else 0;

def isPips = Display_Mode == Display_Mode."Pips";

# --- 8. Chart Dashboard & Labels ---
AddLabel(yes, "Trades: " + tradeCount + " | Net: " + (if isPips then Round(totalPnl,1) + " P" else AsDollars(totalPnl)),
         if totalPnl >= 0 then Color.GREEN else Color.RED, Location.BOTTOM_LEFT);

AddLabel(cLong > 0, "Long Win: " + Round(wrL,1) + "%", Color.CYAN, Location.BOTTOM_LEFT);
AddLabel(cShort > 0, "Short Win: " + Round(wrS,1) + "%", Color.ORANGE, Location.BOTTOM_LEFT);

AddLabel(yes, "L Profit: " + (if isPips then Round(pLong,1)+" P" else AsDollars(pLong)), Color.CYAN, Location.BOTTOM_LEFT);
AddLabel(yes, "S Profit: " + (if isPips then Round(pShort,1)+" P" else AsDollars(pShort)), Color.ORANGE, Location.BOTTOM_LEFT);

AddLabel(tradeCount > 0, "Max Win: " + (if isPips then Round(mWin,1)+" P" else AsDollars(mWin)) +
         " | Max Loss: " + (if isPips then Round(mLoss,1)+" P" else AsDollars(mLoss)), Color.GRAY, Location.BOTTOM_LEFT);

AddLabel(LastSignalDir == 1 and pos != 0, "    .    .    KPP UP ", Color.LIGHT_GREEN);
AddLabel(LastSignalDir == -1 and pos != 0, "    .    .    KPP DOWN ", Color.LIGHT_RED);
AddLabel(pos == 0, "    .    .    KPP FLAT ", Color.LIGHT_ORANGE);

# --- 9. Execution Signals & Price Chart Visuals ---
plot UpArrow = if LongSignal then low - (ts*10) else Double.NaN;
UpArrow.SetPaintingStrategy(PaintingStrategy.ARROW_UP);
UpArrow.SetDefaultColor(Color.CYAN);
UpArrow.SetLineWeight(3);

plot DownArrow = if ShortSignal then high + (ts*10) else Double.NaN;
DownArrow.SetPaintingStrategy(PaintingStrategy.ARROW_DOWN);
DownArrow.SetDefaultColor(Color.ORANGE);
DownArrow.SetLineWeight(3);

# Dynamics Price Bars based on the Backtest Engine Position State
AssignPriceColor(if pos == 1 then Color.CYAN else if pos == -1 then Color.ORANGE else Color.CURRENT);

# --- 10. Transaction Bubbles ---
def showBubble = tradeClosed and !IsNaN(currentTradeResult);

AddChartBubble(ShowDebugBubbles and showBubble, high,
    "Type: " + (if longExit then "LONG" else "SHORT") +
    "\nIn: " + Round(entryPrice, 5) +
    "\nOut: " + Round(exitPrice, 5) +
    "\nResult: " + (if isPips then Round(currentTradeResult,1) + "P" else AsDollars(currentTradeResult)),
    Color.GRAY, no);

AddChartBubble(ShowProfitBubbles and showBubble, low,
    (if isPips then Round(currentTradeResult,1) + " P" else AsDollars(currentTradeResult)),
    if currentTradeResult >= 0 then Color.GREEN else Color.RED, yes);
 
Last edited by a moderator:
Made a lower chart version check it out

Code:
# ============================================================
# Fisher-KPP ES Propagation Engine V1.1
# Designed primarily for:
#   ES 5-minute chart
#   Retracement and propagation-resumption analysis
#
# V1.1 fixes:
#   - Correct cooldown initialization
#   - Minimum pullback duration enforced
#   - Long/short resume setup arming
#   - Armed setup reset after speed restart
#   - One exhaustion marker per exhaustion event
#   - Safer recursive-variable initialization
#   - Setup status dashboard label
#
# This is a practical analytical approximation and not a
# literal numerical PDE solver.
# ============================================================

declare lower;

# ============================================================
# GENERAL SETTINGS
# ============================================================

input setupMode = {
    default Balanced,
    Conservative,
    Aggressive
};

input sourceType = {
    default HLC3,
    Close,
    OHLC4
};

input showDashboard = yes;
input showSignals = yes;
input showWaveBackground = yes;
input paintPriceBars = no;

# ============================================================
# MARKET STATE ENGINE
# ============================================================

input meanLength = 34;
input momentumLength = 8;
input normalizationLength = 50;
input volumeLength = 20;
input atrLength = 14;

input useVWAP = yes;
input useVolume = yes;
input useRangeExpansion = yes;

input weightMean = 1.10;
input weightVWAP = 1.00;
input weightMomentum = 1.20;
input weightVolume = 0.65;
input weightRange = 0.55;

# ============================================================
# HIGHER-TIMEFRAME CONTEXT
# ============================================================

input useHTF = yes;
input htfAggregation = AggregationPeriod.FIFTEEN_MIN;
input htfFastLength = 21;
input htfSlowLength = 50;
input weightHTF = 0.75;

# ============================================================
# NONLINEAR DIFFUSION ENGINE
# ============================================================

input diffusionM = 1.35;
input diffusionP = 2.20;

input diffusionGain = 0.18;
input reactionGain = 0.16;
input anchorGain = 0.22;

# ============================================================
# PROPAGATION SPEED ENGINE
# ============================================================

input speedLength = 4;
input criticalLength = 40;
input criticalMultiplierInput = 1.25;
input speedSmoothLength = 3;

input bullStateInput = 0.62;
input bearStateInput = 0.38;

# ============================================================
# RETRACEMENT / RESUMPTION ENGINE
# ============================================================

input retraceWindow = 12;
input minimumPullbackBars = 1;

input requireVWAPAlignment = yes;
input requireHTFAlignment = yes;

input signalCooldown = 6;

# ============================================================
# EXHAUSTION ENGINE
# ============================================================

input showExhaustion = yes;
input exhaustionState = 0.72;
input exhaustionBars = 3;

# ============================================================
# DISPLAY SETTINGS
# ============================================================

input showRawState = yes;
input showThresholdClouds = yes;
input showSpeedLabel = yes;
input showStateLabels = yes;
input showSetupLabel = yes;
input showActionLabel = yes;

# ============================================================
# HELPER SCRIPTS
# ============================================================

script Clamp {
    input value = 0.0;
    input minimum = 0.0;
    input maximum = 1.0;

    plot result =
        Max(minimum, Min(maximum, value));
}

script SafeZScore {
    input value = 0.0;
    input length = 50;

    def average = Average(value, length);
    def deviation = StDev(value, length);

    plot result =
        if deviation > 0
        then (value - average) / deviation
        else 0;
}

script Logistic {
    input value = 0.0;

    def limitedValue =
        Max(-10.0, Min(10.0, value));

    plot result =
        1.0 / (1.0 + Exp(-limitedValue));
}

script SignedPower {
    input value = 0.0;
    input exponent = 1.0;

    plot result =
        if value > 0
        then Power(value, exponent)
        else if value < 0
        then -Power(AbsValue(value), exponent)
        else 0;
}

# ============================================================
# SOURCE
# ============================================================

def src =
    if sourceType == sourceType.Close
    then close
    else if sourceType == sourceType.OHLC4
    then (open + high + low + close) / 4
    else (high + low + close) / 3;

# ============================================================
# MODE ADJUSTMENTS
# ============================================================

def modeCriticalAdjustment =
    if setupMode == setupMode.Conservative
    then 1.20
    else if setupMode == setupMode.Aggressive
    then 0.82
    else 1.00;

def modeStateAdjustment =
    if setupMode == setupMode.Conservative
    then 0.025
    else if setupMode == setupMode.Aggressive
    then -0.025
    else 0.0;

def criticalMultiplier =
    criticalMultiplierInput *
    modeCriticalAdjustment;

def bullStateThreshold =
    Max(
        0.51,
        Min(
            0.90,
            bullStateInput + modeStateAdjustment
        )
    );

def bearStateThreshold =
    Max(
        0.10,
        Min(
            0.49,
            bearStateInput - modeStateAdjustment
        )
    );

# ============================================================
# BASE MARKET MEASUREMENTS
# ============================================================

def trueRangeValue =
    TrueRange(high, close, low);

def atr =
    Average(trueRangeValue, atrLength);

def safeATR =
    Max(atr, TickSize());

def adaptiveMean =
    ExpAverage(src, meanLength);

def sessionVWAP =
    reference VWAP().VWAP;

def meanDisplacement =
    (src - adaptiveMean) / safeATR;

def vwapDisplacement =
    if useVWAP and !IsNaN(sessionVWAP)
    then (src - sessionVWAP) / safeATR
    else 0;

def momentum =
    if !IsNaN(src[momentumLength])
    then (src - src[momentumLength]) / safeATR
    else 0;

def barRange =
    high - low;

def averageRange =
    Average(barRange, atrLength);

def rangeExpansion =
    if averageRange > 0
    then barRange / averageRange - 1.0
    else 0;

def averageVolume =
    Average(volume, volumeLength);

def relativeVolume =
    if averageVolume > 0
    then volume / averageVolume
    else 1.0;

def candleEfficiency =
    if barRange > 0
    then (close - open) / barRange
    else 0;

def volumePressure =
    candleEfficiency *
    Max(relativeVolume - 1.0, 0);

# ============================================================
# HIGHER-TIMEFRAME BIAS
# ============================================================

def htfClose =
    close(period = htfAggregation);

def htfHigh =
    high(period = htfAggregation);

def htfLow =
    low(period = htfAggregation);

def htfFast =
    ExpAverage(htfClose, htfFastLength);

def htfSlow =
    ExpAverage(htfClose, htfSlowLength);

def htfTrueRange =
    TrueRange(htfHigh, htfClose, htfLow);

def htfATR =
    Average(htfTrueRange, atrLength);

def htfBiasRaw =
    if htfATR > 0
    then (htfFast - htfSlow) / htfATR
    else 0;

def htfBias =
    Max(-2.0, Min(2.0, htfBiasRaw));

def htfBull =
    htfFast > htfSlow;

def htfBear =
    htfFast < htfSlow;

# ============================================================
# NORMALIZED COMPONENTS
# ============================================================

def meanComponent =
    Max(
        -3.0,
        Min(
            3.0,
            SafeZScore(
                meanDisplacement,
                normalizationLength
            )
        )
    );

def vwapComponent =
    Max(
        -3.0,
        Min(
            3.0,
            SafeZScore(
                vwapDisplacement,
                normalizationLength
            )
        )
    );

def momentumComponent =
    Max(
        -3.0,
        Min(
            3.0,
            SafeZScore(
                momentum,
                normalizationLength
            )
        )
    );

def volumeComponent =
    if useVolume
    then
        Max(
            -3.0,
            Min(
                3.0,
                SafeZScore(
                    volumePressure,
                    normalizationLength
                )
            )
        )
    else 0;

def rangeDirection =
    if close > open
    then 1
    else if close < open
    then -1
    else 0;

def rangeComponent =
    if useRangeExpansion
    then
        Max(
            -3.0,
            Min(
                3.0,
                SafeZScore(
                    rangeExpansion,
                    normalizationLength
                )
            )
        ) * rangeDirection
    else 0;

# ============================================================
# ACTIVE WEIGHT
# ============================================================

def activeWeight =
    weightMean +
    weightMomentum +
    (if useVWAP then weightVWAP else 0) +
    (if useVolume then weightVolume else 0) +
    (if useRangeExpansion then weightRange else 0) +
    (if useHTF then weightHTF else 0);

def weightedScore =
    weightMean * meanComponent +
    weightMomentum * momentumComponent +
    (if useVWAP
     then weightVWAP * vwapComponent
     else 0) +
    (if useVolume
     then weightVolume * volumeComponent
     else 0) +
    (if useRangeExpansion
     then weightRange * rangeComponent
     else 0) +
    (if useHTF
     then weightHTF * htfBias
     else 0);

def normalizedScore =
    if activeWeight > 0
    then weightedScore / activeWeight
    else 0;

# ============================================================
# RAW BOUNDED MARKET STATE
# ============================================================

def rawState =
    Logistic(normalizedScore * 2.25);

# ============================================================
# REACTION IMPULSE
# ============================================================

def impulseDirection =
    Max(
        -2.0,
        Min(
            2.0,
            0.45 * momentumComponent +
            0.30 * volumeComponent +
            0.25 * rangeComponent
        )
    );

def impulseStrength =
    Max(
        0.0,
        Min(
            2.0,
            AbsValue(momentumComponent) * 0.40 +
            AbsValue(volumeComponent) * 0.30 +
            AbsValue(rangeComponent) * 0.30
        )
    );

def directionalImpulse =
    if impulseDirection > 0
    then impulseStrength
    else if impulseDirection < 0
    then -impulseStrength
    else 0;

def reactionTerm =
    rawState *
    (1.0 - rawState) *
    directionalImpulse;

# ============================================================
# DOUBLY NONLINEAR DIFFUSION APPROXIMATION
# ============================================================

def safeRawState =
    Max(0.0001, Min(1.0, rawState));

def densityNow =
    Power(safeRawState, diffusionM);

def densityPrevious =
    Power(
        Max(
            0.0001,
            Min(
                1.0,
                if IsNaN(rawState[1])
                then rawState
                else rawState[1]
            )
        ),
        diffusionM
    );

def densityTwoBarsAgo =
    Power(
        Max(
            0.0001,
            Min(
                1.0,
                if IsNaN(rawState[2])
                then rawState
                else rawState[2]
            )
        ),
        diffusionM
    );

def densityCurvature =
    densityNow -
    2.0 * densityPrevious +
    densityTwoBarsAgo;

def nonlinearDiffusion =
    SignedPower(
        densityCurvature,
        diffusionP - 1.0
    );

# ============================================================
# RECURSIVE WAVE STATE
# ============================================================

rec waveState =
    if BarNumber() == 1
    then 0.50
    else
        Max(
            0.0,
            Min(
                1.0,
                waveState[1] +
                diffusionGain * nonlinearDiffusion +
                reactionGain * reactionTerm +
                anchorGain * (rawState - waveState[1])
            )
        );

# ============================================================
# PROPAGATION SPEED
# ============================================================

def rawWaveSpeed =
    if BarNumber() > speedLength
    then
        (waveState - waveState[speedLength]) /
        speedLength
    else 0;

def waveSpeed =
    ExpAverage(rawWaveSpeed, speedSmoothLength);

def stateChange =
    if BarNumber() > 1
    then waveState - waveState[1]
    else 0;

def speedNoise =
    StDev(stateChange, criticalLength);

def criticalSpeed =
    Max(
        speedNoise * criticalMultiplier,
        0.00001
    );

def normalizedSpeed =
    waveSpeed / criticalSpeed;

# ============================================================
# WAVE CLASSIFICATION
# ============================================================

def bullState =
    waveState >= bullStateThreshold;

def bearState =
    waveState <= bearStateThreshold;

def equilibriumState =
    !bullState and !bearState;

def bullWave =
    bullState and
    waveSpeed > criticalSpeed;

def bearWave =
    bearState and
    waveSpeed < -criticalSpeed;

def subcriticalBull =
    bullState and
    !bullWave;

def subcriticalBear =
    bearState and
    !bearWave;

def stalled =
    AbsValue(waveSpeed) <
    criticalSpeed * 0.35;

# ============================================================
# RECENT WAVE MEMORY
# ============================================================

rec barsSinceBullWave =
    if BarNumber() == 1
    then retraceWindow + 100
    else if bullWave
    then 0
    else
        Min(
            barsSinceBullWave[1] + 1,
            retraceWindow + 100
        );

rec barsSinceBearWave =
    if BarNumber() == 1
    then retraceWindow + 100
    else if bearWave
    then 0
    else
        Min(
            barsSinceBearWave[1] + 1,
            retraceWindow + 100
        );

def recentBullWave =
    barsSinceBullWave <= retraceWindow;

def recentBearWave =
    barsSinceBearWave <= retraceWindow;

# ============================================================
# RETRACEMENT STATE
# ============================================================

def bullPullback =
    recentBullWave and
    bullState and
    waveSpeed <= criticalSpeed;

def bearPullback =
    recentBearWave and
    bearState and
    waveSpeed >= -criticalSpeed;

# Count consecutive pullback bars.

rec bullPullbackBars =
    if BarNumber() == 1
    then 0
    else if bullPullback
    then bullPullbackBars[1] + 1
    else 0;

rec bearPullbackBars =
    if BarNumber() == 1
    then 0
    else if bearPullback
    then bearPullbackBars[1] + 1
    else 0;

# ============================================================
# ALIGNMENT FILTERS
# ============================================================

def vwapLongOK =
    !requireVWAPAlignment or
    !useVWAP or
    close > sessionVWAP;

def vwapShortOK =
    !requireVWAPAlignment or
    !useVWAP or
    close < sessionVWAP;

def htfLongOK =
    !requireHTFAlignment or
    !useHTF or
    htfBull;

def htfShortOK =
    !requireHTFAlignment or
    !useHTF or
    htfBear;

# ============================================================
# SPEED CROSSINGS
# ============================================================

def bullSpeedResume =
    waveSpeed > criticalSpeed and
    waveSpeed[1] <= criticalSpeed[1];

def bearSpeedResume =
    waveSpeed < -criticalSpeed and
    waveSpeed[1] >= -criticalSpeed[1];

# ============================================================
# ARMED RETRACEMENT SETUPS
# ============================================================

# A setup becomes armed only after its pullback lasts for the
# required number of consecutive bars.
#
# The setup is canceled when:
#   - The directional state is lost
#   - The parent wave becomes too old
#   - Speed crosses back through the critical threshold
#
# Using the previous armed value on the signal bar preserves
# the setup while avoiding circular references.

rec bullResumeArmed =
    if BarNumber() == 1
    then no
    else if !recentBullWave or !bullState
    then no
    else if bullSpeedResume and bullResumeArmed[1]
    then no
    else if bullPullback and
            bullPullbackBars >= minimumPullbackBars
    then yes
    else bullResumeArmed[1];

rec bearResumeArmed =
    if BarNumber() == 1
    then no
    else if !recentBearWave or !bearState
    then no
    else if bearSpeedResume and bearResumeArmed[1]
    then no
    else if bearPullback and
            bearPullbackBars >= minimumPullbackBars
    then yes
    else bearResumeArmed[1];

# ============================================================
# PRELIMINARY RESUME SIGNALS
# ============================================================

def preliminaryLongResume =
    bullSpeedResume and
    bullResumeArmed[1] and
    recentBullWave and
    bullState and
    vwapLongOK and
    htfLongOK;

def preliminaryShortResume =
    bearSpeedResume and
    bearResumeArmed[1] and
    recentBearWave and
    bearState and
    vwapShortOK and
    htfShortOK;

# ============================================================
# SIGNAL COOLDOWN
# ============================================================

# Initialize above the cooldown threshold so the first valid
# setup is allowed to trigger.

rec barsSinceLongSignal =
    if BarNumber() == 1
    then signalCooldown + 1
    else if preliminaryLongResume and
            barsSinceLongSignal[1] > signalCooldown
    then 0
    else barsSinceLongSignal[1] + 1;

rec barsSinceShortSignal =
    if BarNumber() == 1
    then signalCooldown + 1
    else if preliminaryShortResume and
            barsSinceShortSignal[1] > signalCooldown
    then 0
    else barsSinceShortSignal[1] + 1;

def longCooldownOK =
    barsSinceLongSignal[1] > signalCooldown;

def shortCooldownOK =
    barsSinceShortSignal[1] > signalCooldown;

def longResumeSignal =
    preliminaryLongResume and
    longCooldownOK;

def shortResumeSignal =
    preliminaryShortResume and
    shortCooldownOK;

# ============================================================
# EXHAUSTION CONDITIONS
# ============================================================

def speedFalling =
    Sum(
        waveSpeed < waveSpeed[1],
        exhaustionBars
    ) == exhaustionBars;

def speedRising =
    Sum(
        waveSpeed > waveSpeed[1],
        exhaustionBars
    ) == exhaustionBars;

def bullExhaustionCondition =
    waveState >= exhaustionState and
    waveSpeed > 0 and
    speedFalling and
    close >= Highest(close, exhaustionBars);

def bearExhaustionCondition =
    waveState <= 1.0 - exhaustionState and
    waveSpeed < 0 and
    speedRising and
    close <= Lowest(close, exhaustionBars);

# One marker and one alert per exhaustion event.

def bullExhaustion =
    bullExhaustionCondition and
    !bullExhaustionCondition[1];

def bearExhaustion =
    bearExhaustionCondition and
    !bearExhaustionCondition[1];

# ============================================================
# GLOBAL COLORS
# ============================================================

DefineGlobalColor(
    "BullWave",
    CreateColor(0, 220, 120)
);

DefineGlobalColor(
    "BearWave",
    CreateColor(235, 70, 70)
);

DefineGlobalColor(
    "BullRetrace",
    CreateColor(70, 165, 255)
);

DefineGlobalColor(
    "BearRetrace",
    CreateColor(255, 170, 50)
);

DefineGlobalColor(
    "BullStalled",
    CreateColor(70, 150, 110)
);

DefineGlobalColor(
    "BearStalled",
    CreateColor(160, 90, 90)
);

DefineGlobalColor(
    "Neutral",
    CreateColor(160, 160, 160)
);

# ============================================================
# MAIN STATE PLOT
# ============================================================

plot Wave =
    waveState;

Wave.SetLineWeight(3);

Wave.AssignValueColor(
    if bullWave
    then GlobalColor("BullWave")
    else if bearWave
    then GlobalColor("BearWave")
    else if bullPullback
    then GlobalColor("BullRetrace")
    else if bearPullback
    then GlobalColor("BearRetrace")
    else if subcriticalBull
    then GlobalColor("BullStalled")
    else if subcriticalBear
    then GlobalColor("BearStalled")
    else GlobalColor("Neutral")
);

# ============================================================
# RAW STATE
# ============================================================

plot RawMarketState =
    if showRawState
    then rawState
    else Double.NaN;

RawMarketState.SetDefaultColor(Color.DARK_GRAY);
RawMarketState.SetLineWeight(1);
RawMarketState.HideBubble();

# ============================================================
# THRESHOLDS
# ============================================================

plot BullThreshold =
    bullStateThreshold;

BullThreshold.SetDefaultColor(Color.DARK_GREEN);
BullThreshold.SetStyle(Curve.SHORT_DASH);
BullThreshold.HideBubble();

plot BearThreshold =
    bearStateThreshold;

BearThreshold.SetDefaultColor(Color.DARK_RED);
BearThreshold.SetStyle(Curve.SHORT_DASH);
BearThreshold.HideBubble();

plot Equilibrium =
    0.50;

Equilibrium.SetDefaultColor(Color.YELLOW);
Equilibrium.SetStyle(Curve.SHORT_DASH);
Equilibrium.HideBubble();

plot UpperBoundary =
    1.0;

UpperBoundary.SetDefaultColor(Color.BLACK);
UpperBoundary.HideBubble();
UpperBoundary.HideTitle();

plot LowerBoundary =
    0.0;

LowerBoundary.SetDefaultColor(Color.BLACK);
LowerBoundary.HideBubble();
LowerBoundary.HideTitle();

# ============================================================
# THRESHOLD CLOUDS
# ============================================================

AddCloud(
    if showThresholdClouds
    then UpperBoundary
    else Double.NaN,
    if showThresholdClouds
    then BullThreshold
    else Double.NaN,
    Color.DARK_GREEN,
    Color.DARK_GREEN
);

AddCloud(
    if showThresholdClouds
    then BearThreshold
    else Double.NaN,
    if showThresholdClouds
    then LowerBoundary
    else Double.NaN,
    Color.DARK_RED,
    Color.DARK_RED
);

# ============================================================
# REGIME BACKGROUND
# ============================================================

AssignBackgroundColor(
    if !showWaveBackground
    then Color.CURRENT
    else if bullWave
    then CreateColor(0, 35, 20)
    else if bearWave
    then CreateColor(40, 10, 10)
    else if bullPullback
    then CreateColor(10, 25, 45)
    else if bearPullback
    then CreateColor(45, 25, 5)
    else Color.CURRENT
);

# ============================================================
# SIGNAL MARKERS
# ============================================================

plot LongResume =
    if showSignals and longResumeSignal
    then 0.04
    else Double.NaN;

LongResume.SetPaintingStrategy(
    PaintingStrategy.ARROW_UP
);

LongResume.SetDefaultColor(Color.GREEN);
LongResume.SetLineWeight(4);
LongResume.HideBubble();
LongResume.HideTitle();

plot ShortResume =
    if showSignals and shortResumeSignal
    then 0.96
    else Double.NaN;

ShortResume.SetPaintingStrategy(
    PaintingStrategy.ARROW_DOWN
);

ShortResume.SetDefaultColor(Color.RED);
ShortResume.SetLineWeight(4);
ShortResume.HideBubble();
ShortResume.HideTitle();

# ============================================================
# EXHAUSTION MARKERS
# ============================================================

plot BullExhaustionMarker =
    if showExhaustion and bullExhaustion
    then 0.96
    else Double.NaN;

BullExhaustionMarker.SetPaintingStrategy(
    PaintingStrategy.POINTS
);

BullExhaustionMarker.SetDefaultColor(Color.ORANGE);
BullExhaustionMarker.SetLineWeight(5);
BullExhaustionMarker.HideBubble();
BullExhaustionMarker.HideTitle();

plot BearExhaustionMarker =
    if showExhaustion and bearExhaustion
    then 0.04
    else Double.NaN;

BearExhaustionMarker.SetPaintingStrategy(
    PaintingStrategy.POINTS
);

BearExhaustionMarker.SetDefaultColor(Color.ORANGE);
BearExhaustionMarker.SetLineWeight(5);
BearExhaustionMarker.HideBubble();
BearExhaustionMarker.HideTitle();

# ============================================================
# OPTIONAL PRICE-BAR COLORING
# ============================================================

AssignPriceColor(
    if !paintPriceBars
    then Color.CURRENT
    else if bullWave
    then GlobalColor("BullWave")
    else if bearWave
    then GlobalColor("BearWave")
    else if bullPullback
    then GlobalColor("BullRetrace")
    else if bearPullback
    then GlobalColor("BearRetrace")
    else Color.CURRENT
);

# ============================================================
# DASHBOARD LABELS
# ============================================================

AddLabel(
    showDashboard,
    "KPP PROPAGATION | " +
    (if setupMode == setupMode.Conservative
     then "CONSERVATIVE"
     else if setupMode == setupMode.Aggressive
     then "AGGRESSIVE"
     else "BALANCED"),
    Color.WHITE
);

AddLabel(
    showDashboard and showStateLabels,
    if bullWave
    then "REGIME: BULL WAVE"
    else if bearWave
    then "REGIME: BEAR WAVE"
    else if bullPullback
    then "REGIME: BULL RETRACE"
    else if bearPullback
    then "REGIME: BEAR RETRACE"
    else if subcriticalBull
    then "REGIME: BULL STALLED"
    else if subcriticalBear
    then "REGIME: BEAR STALLED"
    else if stalled
    then "REGIME: NO PROPAGATION"
    else "REGIME: TRANSITION",
    if bullWave
    then GlobalColor("BullWave")
    else if bearWave
    then GlobalColor("BearWave")
    else if bullPullback
    then GlobalColor("BullRetrace")
    else if bearPullback
    then GlobalColor("BearRetrace")
    else if subcriticalBull
    then GlobalColor("BullStalled")
    else if subcriticalBear
    then GlobalColor("BearStalled")
    else GlobalColor("Neutral")
);

AddLabel(
    showDashboard and showStateLabels,
    "STATE: " +
    AsText(Round(waveState * 100, 1)) +
    "%",
    if bullState
    then GlobalColor("BullWave")
    else if bearState
    then GlobalColor("BearWave")
    else Color.GRAY
);

AddLabel(
    showDashboard and showSpeedLabel,
    "SPEED: " +
    AsText(Round(normalizedSpeed, 2)) +
    "x",
    if normalizedSpeed > 1
    then Color.GREEN
    else if normalizedSpeed < -1
    then Color.RED
    else Color.GRAY
);

AddLabel(
    showDashboard,
    "VWAP: " +
    (if !useVWAP
     then "OFF"
     else if close > sessionVWAP
     then "ABOVE"
     else if close < sessionVWAP
     then "BELOW"
     else "AT"),
    if !useVWAP
    then Color.GRAY
    else if close > sessionVWAP
    then Color.GREEN
    else if close < sessionVWAP
    then Color.RED
    else Color.GRAY
);

AddLabel(
    showDashboard,
    "HTF: " +
    (if !useHTF
     then "OFF"
     else if htfBull
     then "BULL"
     else if htfBear
     then "BEAR"
     else "FLAT"),
    if !useHTF
    then Color.GRAY
    else if htfBull
    then Color.GREEN
    else if htfBear
    then Color.RED
    else Color.GRAY
);

# ============================================================
# SETUP STATUS
# ============================================================

AddLabel(
    showDashboard and showSetupLabel,
    if bullResumeArmed
    then
        "SETUP: LONG ARMED | " +
        AsText(bullPullbackBars) +
        " BARS"
    else if bearResumeArmed
    then
        "SETUP: SHORT ARMED | " +
        AsText(bearPullbackBars) +
        " BARS"
    else if bullPullback
    then
        "SETUP: LONG RETRACE | " +
        AsText(bullPullbackBars) +
        " BARS"
    else if bearPullback
    then
        "SETUP: SHORT RETRACE | " +
        AsText(bearPullbackBars) +
        " BARS"
    else
        "SETUP: NOT ARMED",
    if bullResumeArmed
    then Color.GREEN
    else if bearResumeArmed
    then Color.RED
    else if bullPullback
    then GlobalColor("BullRetrace")
    else if bearPullback
    then GlobalColor("BearRetrace")
    else Color.GRAY
);

# ============================================================
# ACTION STATUS
# ============================================================

AddLabel(
    showDashboard and showActionLabel,
    if longResumeSignal
    then "ACTION: LONG RESUME"
    else if shortResumeSignal
    then "ACTION: SHORT RESUME"
    else if bullExhaustion
    then "ACTION: BULL EXHAUSTION"
    else if bearExhaustion
    then "ACTION: BEAR EXHAUSTION"
    else if bullResumeArmed or bearResumeArmed
    then "ACTION: WAIT FOR RESTART"
    else if bullPullback
    then "ACTION: BUILDING LONG SETUP"
    else if bearPullback
    then "ACTION: BUILDING SHORT SETUP"
    else if bullWave
    then "ACTION: HOLD LONG BIAS"
    else if bearWave
    then "ACTION: HOLD SHORT BIAS"
    else "ACTION: WAIT",
    if longResumeSignal
    then Color.GREEN
    else if shortResumeSignal
    then Color.RED
    else if bullExhaustion or bearExhaustion
    then Color.ORANGE
    else if bullResumeArmed
    then Color.GREEN
    else if bearResumeArmed
    then Color.RED
    else if bullWave
    then GlobalColor("BullWave")
    else if bearWave
    then GlobalColor("BearWave")
    else Color.GRAY
);

# ============================================================
# CHART BUBBLES
# ============================================================

AddChartBubble(
    showSignals and longResumeSignal,
    0.04,
    "GO",
    Color.GREEN,
    no
);

AddChartBubble(
    showSignals and shortResumeSignal,
    0.96,
    "GO",
    Color.RED,
    yes
);

# ============================================================
# ALERTS
# ============================================================

Alert(
    longResumeSignal,
    "KPP bullish propagation resumed after a qualified retracement",
    Alert.BAR,
    Sound.Ding
);

Alert(
    shortResumeSignal,
    "KPP bearish propagation resumed after a qualified retracement",
    Alert.BAR,
    Sound.Ding
);

Alert(
    bullResumeArmed and !bullResumeArmed[1],
    "KPP bullish retracement qualified and armed",
    Alert.BAR,
    Sound.Chimes
);

Alert(
    bearResumeArmed and !bearResumeArmed[1],
    "KPP bearish retracement qualified and armed",
    Alert.BAR,
    Sound.Chimes
);

Alert(
    bullExhaustion,
    "KPP bullish propagation exhaustion",
    Alert.BAR,
    Sound.Ring
);

Alert(
    bearExhaustion,
    "KPP bearish propagation exhaustion",
    Alert.BAR,
    Sound.Ring
);
 

Attachments

  • kppscreen.png
    kppscreen.png
    65.3 KB · Views: 3

Join useThinkScript to post your question to a community of 21,000+ developers and traders.

Similar threads

Not the exact question you're looking for?

Start a new thread and receive assistance from our community.

87k+ Posts
1402 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