A practical walkthrough for taking a plain-English trading idea and expressing it as correct, non-repainting Pine Script v6 — not just code that compiles, but code that means what you think it means. This is educational material only, not financial advice, and trading carries a real risk of loss. The goal is clarity of logic, not any claim about performance.
Most broken indicators don't start as bad code. They start as a fuzzy sentence. "Buy when the trend is up and momentum is strong" feels like a rule, but it isn't one a computer can evaluate. Before you open the Pine Editor, the most useful thing you can do is rewrite your idea until every word maps to something measurable on a chart.
Take a concrete example. The fuzzy version is: "Go long when the fast average crosses above the slow average, but only if momentum confirms." That single sentence hides at least four decisions. How fast is fast? How slow is slow? What does "momentum confirms" mean numerically? And on which bar — the one still forming, or the one that already closed?
A disciplined rewrite looks like this:
Notice that the last line — timing — has nothing to do with your edge and everything to do with whether the indicator tells you the truth. That single decision is where most repaint bugs are born, and we'll come back to it. The point here is that this is the convert trading strategy to pine script step nobody skips for free: if a rule is still ambiguous in English, it will be ambiguous (or silently wrong) in code.
Pine's editor will happily give you a green checkmark on logic that is quietly lying to you. The compiler checks grammar, not intent. It cannot know that you meant "the bar that closed" when you wrote something that reads the bar that's still ticking. So the first mental model to adopt is that compilation is the lowest bar, not the finish line.
There are two failure modes that pass compilation and still ruin a strategy. The first is repainting: the indicator draws a signal during a live bar, then erases or moves it after the bar closes because the values it depended on changed. Your backtest looks immaculate because, in history, every bar is already closed — so the "signal" had perfect hindsight. In real time, it flickers. The second is look-ahead bias, which is subtler: pulling data from a higher timeframe or another symbol in a way that lets the current bar "see" information that wouldn't have existed yet.
Both bugs share a signature: the backtest looks unrealistically clean and the live behavior does not match it. If your equity curve looks too tidy, suspect the timing logic before you celebrate the idea. Tools exist to flag this class of mistake automatically — ForexCodes' Audit checks, for instance, look specifically for repaint and look-ahead patterns — but you can catch the majority by hand once you know the two questions to ask of every line: does this value change after the bar closes? and could this value know something the market didn't yet?
Treat "it compiled" as "the spelling is fine." Whether the sentence is true is a separate, harder check — and it's the one that actually protects you. None of this implies a correct indicator is a profitable one; correctness only means the code does what you described. Whether the idea has any edge is a different question entirely, and this article makes no claim that it does.
Once the English is unambiguous, translation becomes almost mechanical. The trick is to give every rule its own named boolean or series variable, so the final signal reads like your sentence. Don't cram the whole condition onto one line — you lose the ability to debug it, and you lose the one-to-one mapping back to your idea.
Use ta.ema for the averages, ta.rsi for momentum, and ta.crossover / ta.crossunder for the trigger. Each of these is namespaced in v6 — bare ema() or rsi() calls are v5-era syntax and won't compile. Here is the idea from the first section, expressed cleanly:
//@version=6
indicator("EMA Cross + RSI Filter", overlay = true)
// --- Inputs make every "how fast / how slow" decision explicit ---
fastLen = input.int(12, "Fast EMA Length", minval = 1)
slowLen = input.int(26, "Slow EMA Length", minval = 1)
rsiLen = input.int(14, "RSI Length", minval = 1)
rsiGate = input.int(50, "RSI Momentum Threshold")
// --- One named variable per rule ---
fastEma = ta.ema(close, fastLen)
slowEma = ta.ema(close, slowLen)
momentum = ta.rsi(close, rsiLen)
trigger = ta.crossover(fastEma, slowEma) // fast crosses above slow
momentumOk = momentum > rsiGate // "momentum confirms"
longSignal = trigger and momentumOk
plot(fastEma, "Fast EMA", color = color.aqua)
plot(slowEma, "Slow EMA", color = color.orange)Read the longSignal line out loud: "a crossover and momentum is okay." That is your original sentence, now executable. The inputs at the top are doing real work too — they turn every vague "fast" and "strong" into a number you can change without touching the logic. When a rule and its variable share a name, a wrong result points you straight at the wrong line.
The code above is logically faithful to the idea, but it is not yet safe to act on, because nothing in it pins the signal to a closed bar. During a live candle, close updates on every tick, so fastEma, slowEma, and momentum are all recomputed tick by tick. That means trigger (the crossover) and momentumOk can both turn true mid-bar and then turn false again before the candle closes — and longSignal flickers with them. That is the textbook repaint: an arrow that appears, then vanishes. For a visual indicator that may be acceptable; for anything you alert or trade on, it is not.
The fix is barstate.isconfirmed, which is only true on the final tick of a bar — i.e. once it has closed. Gate any signal you actually rely on behind it:
//@version=6
indicator("EMA Cross + RSI Filter (confirmed)", overlay = true)
fastLen = input.int(12, "Fast EMA Length", minval = 1)
slowLen = input.int(26, "Slow EMA Length", minval = 1)
rsiLen = input.int(14, "RSI Length", minval = 1)
rsiGate = input.int(50, "RSI Momentum Threshold")
fastEma = ta.ema(close, fastLen)
slowEma = ta.ema(close, slowLen)
momentum = ta.rsi(close, rsiLen)
rawSignal = ta.crossover(fastEma, slowEma) and momentum > rsiGate
// Only act once the bar is final — no repaint, no hindsight.
confirmedSignal = rawSignal and barstate.isconfirmed
plotshape(confirmedSignal, "Long", shape.triangleup, location.belowbar, color.green, size = size.small)
alertcondition(confirmedSignal, "Long Confirmed", "EMA cross + RSI confirmed on close")The same caution applies the moment you reach for higher-timeframe context. If you add request.security, request the prior, settled value and turn off look-ahead explicitly — for example, the same symbol on the daily timeframe: request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_off). Asking for close of the current higher-timeframe bar with look-ahead on is exactly how a backtest learns to peek at data that wasn't available yet. The discipline is the same in both cases: a signal is only honest if it could have fired, unchanged, in real time.
The whole process compresses into a loop you can run for any idea, from a one-line hunch to a multi-condition system. None of these steps are about being clever; they're about removing ambiguity before it hardens into a bug.
input — lengths, thresholds, directions. If you can't name it, you can't tune it.barstate.isconfirmed.request.security call, offset by [1] and set lookahead = barmerge.lookahead_off. Ask of every line: could this know something the market didn't?That last step is the one people skip, and it's the cheapest insurance you have. A few minutes watching a live chart will surface timing bugs that a thousand bars of clean backtest will happily hide.
One honest closing note: doing all of this correctly tells you that your code faithfully expresses your idea — and nothing more. It does not tell you the idea works. This is educational material, not financial advice; trading carries a real risk of loss, and a perfectly non-repainting indicator can still describe a perfectly losing strategy. Correctness is the floor you build on, not the edge itself.
Educational only — not financial advice. Trading involves substantial risk of loss.