You have an indicator that mostly works. The trend filter is right, the markers land where you want them, and you spent a weekend getting it there. Then you notice one thing: the markers look different after you reload the chart, or a warning appears, or a value is stuck at zero. You know where the problem probably lives, and you are afraid that touching it will break the three parts that are fine. So you leave it alone, or you paste the whole script into a chatbot and get back 200 lines that look nothing like what you had.
That fear is reasonable. The way out is not to be braver, it is to change the way you edit. This post shows a method for fixing a half-working Pine Script indicator one change at a time, with a way to prove after each change that only the thing you meant to alter has moved. We will apply it to a real example with two classic bugs, and then look at how the same discipline maps onto an AI editor that edits only the lines that change.
Why a working-ish indicator is hard to touch
A broken script that fails to compile is easy. The compiler tells you a line number and you fix it. The hard case is a script that compiles, plots, and is wrong in a way that only shows up sometimes. Those bugs hide inside logic that is otherwise correct, and the logic is tangled: one variable feeds three conditions, and one condition feeds a plot, an alert, and a label.
There are three reasons editing such a script feels dangerous. First, you do not have a written record of what "working" means, so you cannot tell whether a change preserved it. Second, Pine Script re-runs your whole script on every bar, so a small change to one series can shift every value after it. Third, some bugs only appear on the live bar, which means your chart on historical data looks fine while the real-time behavior is different. A script can look right on every bar you can scroll to and still be wrong.
The method below attacks all three. It gives you a written contract, a way to compare old against new on the same chart, and a habit of making one change per step.
The example: a pullback marker with two hidden bugs
Here is the indicator we will refactor. It marks a bar when price is in an uptrend (a fast EMA above a slow EMA, and price above the previous day's close) and the RSI drops below 40 after being at or above it. It compiles and it draws triangles.
//@version=6
indicator("Pullback Markers", overlay = true)
fastLen = input.int(20, "Fast EMA")
slowLen = input.int(50, "Slow EMA")
rsiLen = input.int(14, "RSI length")
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
rsiVal = 0.0
if barstate.isconfirmed
rsiVal := ta.rsi(close, rsiLen)
dailyClose = request.security(syminfo.tickerid, "D", close)
uptrend = fast > slow and close > dailyClose
pullback = uptrend and rsiVal < 40 and rsiVal[1] >= 40
plot(fast, "Fast", color = color.teal)
plot(slow, "Slow", color = color.orange)
plotshape(pullback, "Pullback", shape.triangleup, location.belowbar, color.green)Nothing in this script is unusual. People write code like this every day, and each of its two problems looks reasonable when you write it. We will come back to what they are. First, the method.
Step one: write the contract before you touch anything
Before changing a line, write down in plain sentences what the indicator is supposed to do. Not what the code does, what you want. For this script the contract is short: draw the fast and slow EMAs, and draw a triangle below a bar when the trend is up and RSI has just crossed below its level. That is three sentences, and it already tells you something useful. The contract says the trend uses the previous day's close for comparison. It does not say the script should be allowed to use today's close before today is over, which is exactly what one of the bugs does.
The contract also lists what must not change. Here that is the EMA lengths, the visual style of the plots, and the RSI crossing logic. Anything on that list is off limits during this refactor, and if a fix seems to require changing it, you stop and decide on purpose instead of drifting.
If you are describing the indicator to an AI, this contract is also the best prompt you can write, because it states intent separately from implementation. Our post on how to describe a complex indicator so the AI gets it right the first time goes deeper on that.
Step two: find the bug by looking, not by reading
Reading code for a bug you cannot yet name is slow. It is faster to make the bug visible. The cheapest tools are bgcolor, plotchar, and a label on the last bar. Our post on debugging Pine Script when there is no error message covers these patterns in detail; here we only need one: plot the suspicious value where you can see it.
If you plot rsiVal in its own pane, two things stand out. On historical bars it follows the RSI curve as expected. But the value is initialized to 0.0 and only assigned on confirmed bars, so on a live, unconfirmed bar it sits at zero. A reading of zero is below 40, so the condition rsiVal < 40 is true, and the previous bar's value is still the real RSI. On the live bar, a marker can appear that the confirmed bar will never produce, and then vanish.
The second thing is a structural problem you will not see in a plot. The ta.rsi call sits inside an if block. Pine Script's ta.* functions keep internal state from bar to bar, so they need to be evaluated on every bar to produce correct history. Calling one conditionally means the calculation skips bars, and the compiler warns you about exactly this. The script runs, but the numbers are no longer the numbers RSI would give you.
The second bug is in the request.security call. Asking for close on the daily timeframe without a lag returns the daily close that belongs to the current day. On a historical intraday bar, that value is the finished close of the day, which the bar could not have known at that moment. On the live bar, the same expression returns today's still-moving close. The result is the same indicator giving different answers on history and in real time. We explain the mechanics in how request.security works and in the post on the lookahead setting that makes indicators lie.
Note that you now have two separate, named bugs, each with a one-line description. That is progress. A vague feeling of "the markers are off" became two defects you can fix independently.
Step three: build a parity check before you fix anything
This is the step that removes the fear. Instead of fixing the script and hoping, you keep the old logic and the new logic in the same script and draw only the bars where they disagree. If you change one thing and the disagreement appears only where you predicted, you have proof. If it appears somewhere unexpected, you caught a regression in seconds instead of three days later.
Here is a parity check for the first fix, moving ta.rsi out of the if block. The old condition stays as pullbackOld, the new one is pullbackNew, and a red background marks any bar where they differ.
//@version=6
indicator("Parity check: RSI fix", overlay = true)
fastLen = input.int(20, "Fast EMA")
slowLen = input.int(50, "Slow EMA")
rsiLen = input.int(14, "RSI length")
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
dailyClose = request.security(syminfo.tickerid, "D", close)
uptrend = fast > slow and close > dailyClose
// Old: ta.rsi evaluated only on confirmed bars
rsiOld = 0.0
if barstate.isconfirmed
rsiOld := ta.rsi(close, rsiLen)
pullbackOld = uptrend and rsiOld < 40 and rsiOld[1] >= 40
// New: ta.rsi evaluated on every bar
rsiNew = ta.rsi(close, rsiLen)
pullbackNew = uptrend and ta.crossunder(rsiNew, 40)
differs = pullbackOld != pullbackNew
var int differCount = 0
if differs
differCount += 1
bgcolor(differs ? color.new(color.red, 70) : na)
plotshape(pullbackNew, "New", shape.triangleup, location.belowbar, color.green)
plot(differCount, "Differences so far", display = display.data_window)Load this on a chart and scroll. Most of the history should show no red at all, because on confirmed historical bars the two versions are meant to agree. Any red you do see is either the warm-up period at the start of the chart, where the RSI has not settled, or a bar where the old code's skipped calculation really did produce a different number. Either way you are now looking at the exact bars where your edit mattered, instead of imagining them.
The count in the Data Window gives you a single number to watch. If a change you believed was harmless raises it by hundreds, you know before you ship.
Notice what we did to the crossing logic: rsiVal < 40 and rsiVal[1] >= 40 became ta.crossunder(rsiNew, 40). These are the same condition, since ta.crossunder(x, y) is true when x < y and x[1] >= y. Swapping to the built-in is optional and a purist might say it violates "one change per step". It is shown here because the parity check will prove the equivalence for you, which is the point of having one.
Step four: one change, one check, one commit
Now fix the second bug using the same loop. Swap the parity check's old and new halves: the old daily close stays as dailyClose, the new version asks for the previous day's close.
The new line is prevDailyClose = request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on), and it replaces dailyClose in the trend condition. Requesting close[1] with lookahead_on is the documented pattern for getting a higher-timeframe value that was already final when the current bar opened. Here the parity check will light up red on a large share of intraday bars, and that is correct: this fix intentionally changes behavior. The condition close > dailyClose compared price against a value from the future on history, and the new version compares against yesterday's close. The red tells you how many bars had their answer changed by removing the future leak.
This is the second advantage of the parity check. It separates changes that should be invisible from changes that should be visible. The RSI fix should produce close to no red. The daily close fix should produce plenty. If you swap those expectations, you know something is off. Also see how a related repainting trap works in barstate.isconfirmed, which is the other way people try to solve live-bar problems.
Between steps, save a copy of the script version that passed. In the Pine Editor you can save as a new version, and in any editor you can keep a plain text copy. The goal is the same: a known good point to return to, so that a bad step costs you one undo.
The refactored script
After both fixes, with the parity checks removed, the indicator reads like this. Compare it with the original and notice how little changed. The inputs, the EMAs, and the plots are identical.
//@version=6
indicator("Pullback Markers", overlay = true)
fastLen = input.int(20, "Fast EMA", minval = 1)
slowLen = input.int(50, "Slow EMA", minval = 1)
rsiLen = input.int(14, "RSI length", minval = 1)
rsiLevel = input.float(40.0, "RSI pullback level")
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
rsiVal = ta.rsi(close, rsiLen)
prevDailyClose = request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on)
uptrend = fast > slow and close > prevDailyClose
pullback = uptrend and ta.crossunder(rsiVal, rsiLevel)
plot(fast, "Fast", color = color.teal)
plot(slow, "Slow", color = color.orange)
plotshape(pullback, "Pullback", shape.triangleup, location.belowbar, color.green)Two additions here, the minval arguments and the rsiLevel input, are cleanup that the contract allowed. The hard-coded 40 was a magic number repeated in two places, and now it is one input. These are the kind of changes you make after the real bugs are fixed and verified, not before, because mixing cleanup with bug fixes makes the parity check harder to read.
What a surgical edit looks like in an AI editor
Everything above works with any editor and a lot of patience. The reason people avoid it is that it takes discipline to do by hand, and a general chatbot tends to do the opposite: you describe one problem and it rewrites the whole script, which is how a working trend filter gets quietly changed.
PineScripter is built around the other approach. A follow-up request changes the affected lines instead of rewriting the whole script, and the Code Changes view shows a line-by-line record of what each edit changed. For the example above, the useful instruction is the contract itself: "the RSI should be calculated on every bar, and the daily comparison should use yesterday's close. Do not change anything else." The Code Changes view then lets you confirm that only those lines moved, which is the same check the parity script performs, applied to the text. You can read more on the AI code editor page.
Two other pieces map onto the method. The built-in linting engine checks every script for compile errors the same way TradingView does, and the AI fixes the errors it finds on its own, so a step that introduces a typo does not send you into a copy and paste loop. And the plain-English explanation of what a script does is a fast way to write the contract for a script someone else wrote, or one you wrote months ago.
For the visual side, the Chart runs your script inside PineScripter on real market data (308 crypto spot markets) and draws its plots, shapes and labels, so you can see a parity check without pasting into TradingView first. Replay steps through a chart run one bar at a time, which is useful for watching the exact bar where the old and new versions disagree. These cover crypto spot markets only, so for stocks or forex you still check in TradingView itself.
None of this replaces your judgment. The AI can tell you what changed; only you know whether the change matches what you meant. That is why the contract comes first.
A checklist you can reuse
When you next face an indicator you are afraid to touch, the process is short enough to keep in your head. Write the contract, including what must not change. Make the bug visible with a plot or a background color. Name each defect in one sentence. Build a parity check that keeps the old logic beside the new. Fix one defect, read the red, and decide whether the red matches your prediction. Save the passing version. Repeat. Do cleanup last, in its own pass.
A related note on scope: if a fix starts requiring you to touch something on the "must not change" list, that is a signal to stop and decide, not to push through. Often it means the bug is really two bugs, or that the contract was wrong. Both are better discovered at that point than after the alert has fired on the wrong bar. If an alert is part of the picture, why your Pine Script alert fires on the wrong bar is a good next read.
If your script will not compile at all, start with Pine Script won't compile? common errors and how to fix them instead; this method is for scripts that run but misbehave. And if you would like an editor that keeps edits narrow and shows you the diff, you can look at the plans on the pricing page.
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.
Frequently asked questions
Why does my Pine Script indicator look right on history but wrong in real time?
Usually a value depends on information that was not final yet. Common causes are request.security returning the current higher-timeframe close, and logic that reads values differently on the live bar than on confirmed bars. Plot the suspect value and compare a reload with the live bar.
Why should ta.rsi not be called inside an if block?
ta functions keep state from bar to bar, so they need to run on every bar to give correct history. Calling one conditionally skips bars, the compiler warns about it, and the numbers can differ from a normal RSI. Calculate the value on every bar and use the result inside the condition.
What is a parity check in Pine Script?
It is a version of the script that keeps the old logic and the new logic side by side and marks only the bars where they disagree, for example with bgcolor. It shows whether an edit changed what you meant it to change and nothing else.
How do I get yesterday close without looking ahead?
Request close[1] from the daily timeframe with lookahead set to barmerge.lookahead_on. That returns the previous completed daily close on every intraday bar, on history and in real time.
Can an AI editor change only part of my script?
PineScripter edits the affected lines on a follow-up request instead of rewriting the whole script, and its Code Changes view shows what each edit changed line by line. You should still read the diff and confirm it matches your intent.
Describe your indicator in plain English and get Pine Script that compiles →
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.