//@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 rs = array.new() var array ts = array.new() 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 nTrade = array.new_float(2, 0) var array nWin = array.new_float(2, 0) var array sumR = array.new_float(2, 0) var array grossW = array.new_float(2, 0) var array 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))