Algo Trading

Debugging Pine Script Indicators: Guide

By Christopher Downie9 min read
Debugging Pine Script Indicators: Guide

Debug Pine Script indicators by reproducing the problem, inspecting intermediate values and checking when the script executes. A script that compiles can still calculate the wrong result, use an unfinished higher-timeframe value or preserve a variable that should update on every bar.

This guide uses Pine Script v6 syntax and TradingView’s current documentation. Start with the exact error message or a specific bar where actual output differs from expected output. Preserve a working copy, change one thing at a time and keep a short record of the symbol, timeframe, inputs and chart type used to reproduce the issue.

Identify the Type of Error First

ProblemTypical symptomUseful first check
Compilation errorThe editor cannot build the scriptRead the first diagnostic; check declarations, syntax, scope and types
Runtime errorThe script starts but stops while calculatingInspect the failing operation, input bounds, history and resource limits
Logical errorThe script runs but produces unexpected valuesPlot intermediate series and compare a known bar manually
Timing or repainting issueA signal changes during a bar or after reloadCheck realtime execution, requested data and confirmation timing

Fix the first meaningful compiler diagnostic before chasing the rest; one missing delimiter can cause several later messages. Use descriptive variable names and define every value before referencing it. The official debugging guide explains how plots, drawings and Pine Logs help inspect behavior after compilation.

Keep global statements such as plots outside local conditional blocks. Local blocks use indentation to define scope; line wrapping has its own rules, so “indent everything by four spaces” is not a general fix. If you want a conditional plot, calculate a series that is either a value or na, then pass it to a global plot() call.

Understand Bar-by-Bar and Realtime Execution

Pine indicators calculate across historical bars and can execute repeatedly as an open realtime bar changes. The execution model explains rollback: values on an unfinished bar can be recalculated before the closing result is committed. A condition seen intrabar may disappear before that bar closes.

Use barstate.isconfirmed when the intended signal requires the chart bar to close. That choice changes signal timing; it is not a universal switch that makes every data request non-repainting. In particular, confirmation of the chart bar does not necessarily mean a higher-timeframe bar has finished.

Calculate history-dependent functions consistently. TradingView’s v6 migration guide explains how short-circuit conditions can prevent an expression from executing. Compute moving averages and crossover series on every bar first, then apply a debug toggle or event condition to the resulting values. Do not put a needed history calculation only inside an occasional debug branch.

Use Plots, Labels and Pine Logs Together

The following standalone example compares price with its own moving average. It keeps calculations in one function, plots the average, exposes numeric diagnostics in the Data Window and creates labels and logs for confirmed crosses. It is a debugging example, not a trading recommendation or an order-execution strategy.

//@version=6
indicator("Price/SMA debugging example", overlay = true, max_labels_count = 50)

int length = input.int(20, "SMA length", minval = 1)
bool debug = input.bool(true, "Debug outputs")

calcSignals(float source, int period) =>
    float average = ta.sma(source, period)
    bool crossedUp = ta.crossover(source, average)
    bool crossedDown = ta.crossunder(source, average)
    [average, crossedUp, crossedDown]

[smaValue, crossUp, crossDown] = calcSignals(close, length)
bool ready = not na(smaValue) and not na(smaValue[1])
bool confirmedCross = ready and barstate.isconfirmed and (crossUp or crossDown)

plot(smaValue, "SMA", color = color.orange)
plot(debug ? close - smaValue : na, "Close minus SMA", display = display.data_window)
plot(debug ? (ready ? 1 : 0) : na, "History ready", display = display.data_window)

if debug and confirmedCross
    string direction = crossUp ? "Up" : "Down"
    string details = direction + " cross\nClose: " + str.tostring(close, format.mintick) + "\nSMA: " + str.tostring(smaValue, format.mintick)
    label.new(bar_index, high, details, color = color.navy, textcolor = color.white)
    log.info("{0} cross at bar {1}: close={2}, SMA={3}", direction, bar_index, close, smaValue)

if debug and barstate.islastconfirmedhistory and not ready
    log.warning("Not enough valid history for the selected SMA length and crossover check.")

The ready condition checks that the current and previous moving-average values exist. Early na values can be normal warm-up behavior; replacing them indiscriminately with zero can change the calculation. With the default length, inspect the transition from insufficient history to a valid average before interpreting crosses.

Read numeric values in the Data Window

In TradingView’s plot documentation, display.data_window directs output to the Data Window. By contrast, display.none hides the plot from the pane, status line and Data Window. Transparency can make a chart line faint, but it is not the same as selecting its output location.

Move across bars and compare Close minus SMA with the plotted average. Keep units meaningful: a price-valued average and an RSI oscillator are different quantities, so crossing them does not provide a sensible generic price/SMA debugging example.

Use labels for selected events

Labels can display dynamic text from local scope, which makes them useful for marking a specific event or bar. The example limits label history to 50 rather than creating an unbounded visual record. A label on the latest bar alone cannot explain earlier events; choose the condition according to what you are investigating.

Use Pine Logs for detailed inspection

Open Pine Logs through the editor’s More menu or the script’s More menu when available. log.info(), log.warning() and log.error() create messages with different severity levels. Calling log.error() does not itself stop execution; use runtime.error() when you deliberately need to halt an invalid configuration.

TradingView documents that Pine Logs are available to personal scripts; published scripts cannot generate logs directly. Use a personal working copy for debugging. Filter by the relevant event or time range, and avoid logging every tick unless those repeated executions are the issue being investigated.

Fix Persistent Variables and Higher-Timeframe Requests

A common mistake is declaring a changing requested value with var and never reassigning it. The variable declaration documentation explains that var preserves an initialized value across bars until reassignment. It does not mean “cache this expression and automatically refresh it.”

For a requested series that must continue updating, use a normal declaration or explicit reassignment as appropriate. The same applies to derived values: declaring the quotient with var can freeze that result too. Check the numerator and denominator separately, handle unavailable values and guard division where the denominator can be zero.

The next standalone example requests the previous confirmed close and SMA from one strictly higher timeframe. It groups the two expressions in a tuple so they share the request context, and intentionally rejects an equal or lower timeframe.

//@version=6
indicator("Confirmed higher-timeframe debug", overlay = true)
string higherTimeframe = input.timeframe("1W", "Higher timeframe")

if timeframe.in_seconds(higherTimeframe) <= timeframe.in_seconds()
    runtime.error("Choose a timeframe strictly higher than the chart timeframe.")

[confirmedClose, confirmedSma] = request.security(syminfo.tickerid, higherTimeframe, [close[1], ta.sma(close, 20)[1]], lookahead = barmerge.lookahead_on)
float distance = confirmedClose - confirmedSma

plot(confirmedClose, "Previous confirmed HTF close", color = color.blue)
plot(confirmedSma, "Previous confirmed HTF SMA", color = color.orange)
plot(distance, "Confirmed HTF close minus SMA", display = display.data_window)

The other-timeframes documentation describes pairing an expression offset of one bar with barmerge.lookahead_on for confirmed higher-timeframe values. Here the offset belongs inside the requested context. Using lookahead without that offset can introduce future information on historical bars.

This method deliberately waits for the higher-timeframe bar to finish, so the value can lag the developing bar. It is not a method for lower-timeframe intrabar analysis. Use standard time-based charts for this example, keep the requested timeframe strictly higher and compare values around a higher-timeframe boundary and after reload.

Check symbol validity and actual data availability. Do not invent a symbol because it resembles a product name, and do not assume a chart data source exposes every series your model needs. A missing or unsupported dataset should be identified explicitly.

Handle History, Request and Loop Limits

A history-buffer error needs a diagnosis of the expression and required lookback. Setting max_bars_back can help when the required history is known, but a larger buffer is not a universal repair and uses resources. Bound user inputs and dynamic offsets, and consider missing history at the start of the dataset.

TradingView’s current limitations page documents up to 40 unique request.*() calls, or 64 on its Ultimate plan. This is TradingView’s plan terminology. Repeated equivalent requests do not necessarily create additional unique requests, while changing request contexts can. A tuple can combine expressions from one context; arrays do not simply bypass the limit.

The same documentation sets a 500 ms limit for a loop on one bar, with a separate limit on total script execution. A loop that is acceptable on one bar can still be too expensive across the dataset. These limits are implementation constraints, not performance targets.

  • Reduce the test range while isolating the problem, then restore and validate the intended range.
  • Prefer an appropriate built-in calculation when it matches the required semantics.
  • Break early only when the remaining iterations cannot change the required result.
  • Measure the complete script rather than assuming a shorter-looking loop is faster.

Use the Pine Profiler for performance investigation after the results are correct. Retest outputs after optimization: skipping calculations or changing initialization can improve apparent speed by changing what the script actually computes.

Use a Repeatable Debugging Checklist

CheckExpected evidence
Minimal reproductionA small script or configuration still shows the issue
Known-bar calculationIntermediate values agree with a manual calculation on selected bars
Warm-up and inputsMinimum inputs, insufficient history and unavailable values behave intentionally
TimingConfirmed and developing-bar behavior is understood
Requested contextSymbol, timeframe, offsets and data boundaries match the intended rule
RegressionThe change fixes the target issue without changing unrelated outputs

Keep stable versions and a concise change log. Modular functions help isolate calculations, but history-dependent functions still need consistent execution. Compare before and after using the same dataset and settings. A successful compile confirms syntax and type compatibility; it does not prove calculation accuracy or profitability.

Where LuxAlgo Fits in the Workflow

For research on LuxAlgo, begin with native charts and documented data coverage. Reproduce the intended observation on the chosen instrument and timeframe before asking for more complex logic.

Compare the same hypothesis across native charts while keeping source data and timeframe assumptions clear.

Ask Quant, our coding agent to build or revise a clearly specified indicator. Inspect the generated code and run it yourself. Include the expected result, the observed difference and the smallest example that demonstrates it. Treat generated fixes as changes to verify, not proof that the indicator is correct.

Example prompt: “Review this indicator logic. Explain which values should update every bar, identify unavailable inputs and show diagnostics for the expected signal. Keep the code inspectable so I can review it, run it and compare the result on the same symbol and timeframe.”

Organize related charts and indicator experiments in a LuxAlgo workspace.

If the indicator becomes part of a strategy, use native strategy testing with realistic timing and costs, and review compatible recorded outcomes in the native journal. Debugging the calculation and evaluating a trading hypothesis are separate tasks.

LuxAlgo native journal dashboard for reviewing recorded trades
Review recorded outcomes when an indicator is used within a tested trading process.

For scripts written for TradingView, verify behavior in TradingView’s own editor and runtime. Pine Script® also runs on LuxAlgo through PineTS, our open-source runtime.

Video: Inspecting Pine Script Output

The original The Art of Trading tutorial from February 16, 2023 demonstrates inspecting script output with the Data Window and labels. It predates Pine v6 and promotes the creator’s courses, so use it for the debugging approach and the current official documentation above for syntax and platform behavior.

Frequently Asked Questions

How do I find a Pine Script indicator error?

Start with the first compiler diagnostic or a specific bar with an incorrect result. Reduce the script, inspect intermediate values with plots or logs and check the execution timing and data context.

Why does display.none not show values in the Data Window?

It hides plot output from the pane, status line and Data Window. Use display.data_window when the purpose is to inspect numeric values there without drawing the series in the pane.

Does var automatically refresh a requested value?

No. It preserves the initialized value across bars until reassignment. Use a normal declaration or intentional reassignment for values that must continue updating.

Does log.error stop a Pine script?

No. It creates an error-level log message. Use runtime.error when an invalid condition should deliberately halt execution.

Does a successful compile prove an indicator is correct?

No. Compilation checks syntax and types. Test calculations, history, inputs, timing and data requests separately, including behavior on developing bars and after reload.

Learn to trade smarter.

Market analysis and techniques that build your edge, one email a week.

Don’t worry, no spam here. See our privacy policy for more info.

Christopher Downie
Christopher Downie

Content & Product Strategist at LuxAlgo || Background in Computer Science || 7 years experience in retail CFD trading.

Read next