1Copy the scriptPress Copy script above, or the copy button on the code below.
2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
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.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
In-sample ends
time
timestamp("2025-09-01T00:00 +0000")
splitT
Fast average
int
15
fastLen
Slow average
int
150
slowLen
Stop = ATR x
float
2.0
atrMult
Target = R x
float
2.0
rr
Shade in-sample and out-of-sample
bool
true
shade
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.
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
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
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
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
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
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