Indicator · Pine Script v6 · On the chart

Walk-Forward Lab

by jayadevrana

Walk-Forward Lab is an educational overlay indicator that simulates trade execution and walk-forward testing directly within Pine Script.

View on GitHub

GitHub stars1on the repository
Pine versionv6current
Lines of code139
Settings6inputs
LicenceMPL-2.0
Last update4 days ago26 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

139 lines of Pine Script v6 from scripts/walk-forward.pine.txt, published under MPL-2.0. Keep the author's credit and licence lines when you reuse it.

walk-forward.pine.txt
//@version=6
// Lesson 19. An indicator, not a strategy: the backtest is run by hand inside the
// script, so every trade it counts is visible in the code rather than hidden in
// the engine. table, plot, plotshape, fill and bgcolor render on every build.
indicator("Walk-Forward Lab", overlay = true)

// 1. Settings. The split date is the only input that matters in this lesson.
// Left of it is the data you fitted on. Right of it is the test.
splitT  = input.time(timestamp("2025-09-01T00:00 +0000"), "In-sample ends")
fastLen = input.int(15, "Fast average", minval = 2)
slowLen = input.int(150, "Slow average", minval = 5)
atrMult = input.float(2.0, "Stop = ATR x", step = 0.5)
rr      = input.float(2.0, "Target = R x", step = 0.5)
shade   = input.bool(true, "Shade in-sample and out-of-sample")

// 2. The line in time. One boolean decides which half a bar belongs to.
// Teal behind the bars means in sample: the settings have already seen this data.
// Yellow means out of sample: this is the only part that is evidence of anything.
inSample = time < splitT
bgcolor(not shade ? na : inSample ? color.new(color.teal, 90) :
  color.new(color.orange, 90), title = "Sample shading")

// 3. A deliberately ordinary entry, and one open trade tracked by hand. ta.ema,
// ta.atr and ta.crossover are called on every single bar, never inside an if, so
// they keep a correct running state no matter what the trade logic is doing. A
// trade ends at the stop for minus one R, or at the target for plus rr.
atr  = ta.atr(14)
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
xUp  = ta.crossover(fast, slow)
var float entry = na
var float stop  = na
var float targ  = na
var array<float> rs = array.new<float>()
var array<int>   ts = array.new<int>()
var float lastR  = na
var bool  opened = false
var bool  closed = false
opened := false
closed := false
if na(entry)
    if xUp and atr > 0
        entry  := close
        stop   := close - atrMult * atr
        targ   := close + atrMult * atr * rr
        opened := true
else
    if low <= stop or high >= targ
        lastR := low <= stop ? -1.0 : rr
        array.push(rs, lastR)
        array.push(ts, time)
        entry  := na
        closed := true

// 4. Two buckets, filled on the last bar from the trade list and the timestamp
// each trade carries. Slot 0 is the in-sample half, slot 1 is out of sample, so
// the same code measures both halves and nothing can leak from one to the other.
var array<float> nTrade = array.new_float(2, 0)
var array<float> nWin   = array.new_float(2, 0)
var array<float> sumR   = array.new_float(2, 0)
var array<float> grossW = array.new_float(2, 0)
var array<float> grossL = array.new_float(2, 0)
if barstate.islast
    array.fill(nTrade, 0.0)
    array.fill(nWin, 0.0)
    array.fill(sumR, 0.0)
    array.fill(grossW, 0.0)
    array.fill(grossL, 0.0)
    if array.size(rs) > 0
        for i = 0 to array.size(rs) - 1
            r = array.get(rs, i)
            b = array.get(ts, i) < splitT ? 0 : 1
            array.set(nTrade, b, array.get(nTrade, b) + 1)
            array.set(sumR, b, array.get(sumR, b) + r)
            if r > 0
                array.set(nWin, b, array.get(nWin, b) + 1)
                array.set(grossW, b, array.get(grossW, b) + r)
            else
                array.set(grossL, b, array.get(grossL, b) - r)

// 5. What you actually see on the chart. The ribbon between the two averages is the
// signal, tinted by which half it is in, and the little markers are the entries and
// the closed trades, so you can watch winners thin out after the line.
p1 = plot(fast, "Fast average", color = color.new(color.silver, 30), linewidth = 2)
p2 = plot(slow, "Slow average", color = color.new(color.gray, 30), linewidth = 2)
fill(p1, p2, color = inSample ? color.new(color.teal, 80) :
  color.new(color.orange, 80), title = "Trend ribbon")
plotshape(opened, "Entry", shape.circle, location.belowbar,
  color.new(color.blue, 0), size = size.tiny)
plotshape(closed and lastR > 0, "Winner", shape.triangleup, location.belowbar,
  color.new(color.teal, 0), size = size.tiny)
plotshape(closed and lastR <= 0, "Loser", shape.xcross, location.abovebar,
  color.new(color.red, 0), size = size.tiny)

// 6. Small helpers for the report. A function may not assign to a global in Pine,
// so each of these only reads its arguments and hands back a value or a cell.
var table t = table.new(position.top_right, 3, 8, border_width = 1)
put(c, r, s, col, bg) =>
    table.cell(t, c, r, s, text_color = col, text_size = size.normal, bgcolor = bg)
pf(b) =>
    l = array.get(grossL, b)
    l > 0 ? array.get(grossW, b) / l : na
per(b) =>
    n = array.get(nTrade, b)
    n > 0 ? array.get(sumR, b) / n : na
txt(v, d) =>
    na(v) ? "-" : str.tostring(v, d == 0 ? "#" : d == 1 ? "#.#" : "#.##")

// 7. The report, drawn once on the last bar. Read it left to right: the middle
// column is the settings on the data they were fitted to, the right column is them
// on data they never saw, and the bottom row is how much of the edge survived.
if barstate.islast
    head = color.new(color.blue, 20)
    put(0, 0, "metric", color.white, head)
    put(1, 0, "IN SAMPLE", color.white, color.new(color.teal, 40))
    put(2, 0, "OUT OF SAMPLE", color.white, color.new(color.orange, 40))
    for b = 0 to 1
        n  = array.get(nTrade, b)
        w  = array.get(nWin, b)
        bg = b == 0 ? color.new(color.teal, 80) : color.new(color.orange, 80)
        put(b + 1, 1, txt(n, 0), color.white, bg)
        put(b + 1, 2, n > 0 ? txt(w / n * 100, 1) + "%" : "-", color.white, bg)
        put(b + 1, 3, txt(array.get(sumR, b), 1) + "R", color.white, bg)
        put(b + 1, 4, txt(per(b), 2) + "R", color.white, bg)
        put(b + 1, 5, txt(pf(b), 2), color.white, bg)
    put(0, 1, "trades", color.silver, color.new(color.black, 60))
    put(0, 2, "win rate", color.silver, color.new(color.black, 60))
    put(0, 3, "total R", color.silver, color.new(color.black, 60))
    put(0, 4, "R per trade", color.silver, color.new(color.black, 60))
    put(0, 5, "profit factor", color.silver, color.new(color.black, 60))
    keep = na(per(0)) or per(0) == 0 or na(per(1)) ? na : per(1) / per(0) * 100
    kcol = na(keep) ? color.silver : keep >= 100 ? color.teal : color.red
    put(0, 6, "edge kept out of sample", color.white, color.new(color.black, 40))
    put(1, 6, "", color.white, color.new(color.black, 40))
    put(2, 6, txt(keep, 0) + "%", kcol, color.new(color.black, 40))
    put(0, 7, "fitted on the left of the line, tested on the right", color.silver,
      color.new(color.black, 60))
    put(1, 7, "", color.silver, color.new(color.black, 60))
    put(2, 7, "", color.silver, color.new(color.black, 60))

Overview

Walk-Forward Lab evaluates EMA fast and slow line crossovers using ATR-based stop-loss levels and risk-reward ratio targets. A user-defined cutoff timestamp splits chart history into in-sample and out-of-sample segments, shaded in distinct background colors. The script manually records trades into Pine Script arrays on every historical bar and generates performance summaries on the final bar to help compare fitted versus out-of-sample performance without relying on the strategy engine.

Details

Published by

jayadevrana on GitHub

Draws

On the price chart

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

algorithmic-tradingbacktestingema-crossoverpine-scriptpine-script-v6pinescriptrsitechnical-analysistrading-automationtrading-indicators

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
In-sample endstimetimestamp("2025-09-01T00:00 +0000")splitT
Fast averageint15fastLen
Slow averageint150slowLen
Stop = ATR xfloat2.0atrMult
Target = R xfloat2.0rr
Shade in-sample and out-of-samplebooltrueshade
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

947 stars

The Adaptive Laguerre Filter calculates a smoothed, dynamically adjusted trendline plotted directly on the main price chart. It dynamically adjusts its damping factor (alpha) based on recent price range variations and median percentile calculations over a spec

Indicator v4 TrendMoving averages
Updated 12 days ago · GPL-3.0 GitHub repository of Adaptive Laguerre Filter
330 stars

This script plots three moving averages on the price chart: a fast EMA or SMA with 10 periods, a slow EMA or SMA with 20 periods, and a trend EMA or SMA with 100 periods. It signals trend changes by drawing a white cross when the fast moving average crosses ab

Indicator v4 TrendMoving averages
Updated 2 months ago · None stated GitHub repository of 3 EMA/SMA + Cross [Alorse]
318 stars

This indicator plots four customizable moving averages directly on the main price chart using default lengths of 3, 8, 21, and 50 periods. It fills the space between adjacent averages in green or maroon depending on trend alignment. Additionally, it computes B

Indicator v5 Moving averagesTrend
Updated 2 months ago · MIT GitHub repository of Multiple Moving Averages

NVDA Trend Ribbon

by longsurf-ai
280 stars

This indicator plots three exponential moving averages and ATR volatility envelopes on the price chart. It compares the fast and slow EMAs to shade a trend ribbon and marks confirmed upward and downward crossovers with triangle labels. Settings change the thre

Indicator v6 TrendMoving averages
Updated 3 days ago · MIT GitHub repository of NVDA Trend Ribbon
257 stars

Donald SPX - Options Trading is an overlay indicator designed for intraday SPX analysis. It plots a 200-period Weighted Moving Average (WMA) on the price chart while evaluating multi-timeframe market structure across Daily, 15-minute, 5-minute, and 2-minute ba

Indicator v5 Moving averagesTrend Repaints
Updated 7 weeks ago · None stated GitHub repository of Donald SPX - Options Trading
228 stars

This indicator computes moving average and RSI bias signals across the current timeframe and four higher timeframes. It sums biases from the MA and RSI to derive an overall bullish or bearish signal. The results appear in a configurable table that displays eac

Indicator v6 TrendMoving averages
Updated 3 months ago · None stated GitHub repository of Multi-Timeframe Analysis Template

More Trend scripts

Related on ForexR

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers