The Fisher-KPP Double-Pass Smoothed Wave Indicator

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.

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);
 

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
1330 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