Debug Pine Script with Logging

Fact checked by
Mike Christensen, CFOA
March 11, 2026
Practical guide to debugging Pine Script strategies using v6 runtime logging with log.info(), log.warning(), and log.error() functions.

Bottom Line

  • Use log.info() to trace variable values entry conditions and calculation results during strategy execution
  • Use log.warning() to flag unusual conditions like unexpected na values or extreme position sizes
  • Use log.error() to catch critical failures like failed data requests or impossible state combinations
  • Log strategically on confirmed bars and specific events rather than every tick to maintain performance

Pine Script strategies fail silently. Your entry conditions might never trigger, your exit logic might fire on the wrong bar, your position sizing might calculate an impossible quantity, and the strategy will simply sit there producing a flat equity curve with no explanation. Without proper debugging tools, finding these problems means staring at code, adding temporary plot() hacks, or manually calculating values bar by bar.

Pine Script v6 changes this with runtime logging1. The log.info(), log.warning(), and log.error() functions send structured messages to the Pine Logs pane, giving you a detailed execution trace without cluttering your chart2. This guide focuses specifically on debugging strategies, walking through the most common bugs and showing you exactly how to find and fix them with logging.

Common Strategy Bugs

Before diving into logging techniques, it helps to know what you are looking for. These are the bugs that waste the most time for Pine Script strategy developers.

  • Entry conditions that never evaluate to true because of incorrect boolean logic or na values propagating through calculations
  • Exit orders that do not execute because the entry ID string does not match between strategy.entry() and strategy.exit()
  • Position sizing that calculates zero or negative quantities, causing orders to be silently rejected
  • Strategies that behave differently on historical bars versus realtime bars due to repainting or tick-level execution differences
  • Indicators that return na on early bars, causing cascading na values through dependent calculations

All of these bugs have one thing in common: they are invisible on the chart. The strategy does not crash. It does not throw an error. It just does not do what you expect. Logging makes the invisible visible.

Setting Up Logging

The three logging functions share the same syntax. The first argument is a formatting string with placeholders in curly braces. Subsequent arguments provide the values to insert into those placeholders3. The formatting syntax matches str.format().

//@version=6
strategy("Debug Demo", overlay = true, process_orders_on_close = true)

// Basic logging syntax:
// log.info("Message with {0} and {1}", value0, value1)
// log.warning("Warning about {0}", someValue)
// log.error("Critical: {0}", errorCondition)

// Each placeholder {N} is replaced by the Nth value argument (zero-indexed).
log.info("Bar {0}: Open={1}, High={2}, Low={3}, Close={4}",
     bar_index, open, high, low, close)

This simple example logs the OHLC values for every bar. While you would never leave this in a production script, it demonstrates the basic pattern. The Pine Logs pane will show one entry per bar with the formatted message, and clicking any entry navigates the chart to that bar4.

Number Formatting

Raw float values can produce long decimal strings that are hard to read. Use format specifiers inside the placeholders to control precision.

float rsiVal = ta.rsi(close, 14)
float atrVal = ta.atr(14)

// Without formatting: "RSI: 54.28571428571429"
log.info("RSI: {0}", rsiVal)

// With formatting: "RSI: 54.29"
log.info("RSI: {0,number,#.##}", rsiVal)

// Price formatting: "ATR: 2.3500"
log.info("ATR: {0,number,#.####}", atrVal)

The number format pattern uses # for optional digits and 0 for required digits. The pattern #.## displays up to two decimal places, dropping trailing zeros. The pattern #.0000 always shows exactly four decimal places3. Choose the format that matches the precision relevant to your debugging context.

Debugging Entry Conditions

The most common strategy bug is an entry condition that never triggers. The fix starts with logging each component of the condition separately to find which part evaluates to false or na.

//@version=6
strategy("Entry Debug", overlay = true)

int fastLen = input.int(9, "Fast EMA")
int slowLen = input.int(21, "Slow EMA")

float emaFast = ta.ema(close, fastLen)
float emaSlow = ta.ema(close, slowLen)
bool crossUp  = ta.crossover(emaFast, emaSlow)
bool noPosition = strategy.opentrades == 0

// Log the state of each component on every confirmed bar
if barstate.isconfirmed
    log.info("Bar {0} | emaFast={1,number,#.##} emaSlow={2,number,#.##} crossUp={3} noPosition={4}",
         bar_index, emaFast, emaSlow, crossUp, noPosition)

// Entry logic
bool entryCondition = crossUp and noPosition
if entryCondition
    log.info(">>> ENTRY TRIGGERED on bar {0} at price {1}", bar_index, close)
    strategy.entry("Long", strategy.long)

With this logging in place, you can open the Pine Logs pane and scan through the messages. If crossUp is always false, the EMA values might be too close together or the fast period might be longer than the slow period. If noPosition is always false, a previous position never closed. If both are occasionally true but never on the same bar, you have a timing issue.

The entry trigger log is separated from the component log so you can filter by message text. When debugging, you can quickly scan for "ENTRY TRIGGERED" to see how many entries occurred and on which bars.

Catching na Propagation

Many entry conditions fail because an intermediate calculation returns na, and any comparison involving na evaluates to false4. Use log.warning() to flag na values before they propagate.

float customMA = ta.sma(close, 200)

if barstate.isconfirmed
    if na(customMA)
        log.warning("customMA is na on bar {0}. Need {1} bars of history.", bar_index, 200)
    if na(emaFast)
        log.warning("emaFast is na on bar {0}.", bar_index)

A 200-period SMA requires 200 bars of data before it produces a value. If your strategy uses this SMA in an entry condition, no entries will occur during the first 200 bars. This is expected behavior, but logging makes it explicit rather than leaving you to guess why the first 200 bars have no trades.

Tracking Position State

Once entries are working, the next area to debug is position management. Log the position state after each entry and exit to confirm the strategy is behaving as expected.

// Log after every entry fill
if strategy.opentrades > 0 and strategy.opentrades != strategy.opentrades[1]
    log.info("POSITION OPENED | Size: {0} | Avg Price: {1,number,#.####} | Bar: {2}",
         strategy.position_size, strategy.position_avg_price, bar_index)

// Log after every exit fill
if strategy.closedtrades > strategy.closedtrades[1]
    int lastTrade = strategy.closedtrades - 1
    float entryPrice = strategy.closedtrades.entry_price(lastTrade)
    float exitPrice  = strategy.closedtrades.exit_price(lastTrade)
    float profit     = strategy.closedtrades.profit(lastTrade)
    log.info("POSITION CLOSED | Entry: {0,number,#.####} | Exit: {1,number,#.####} | P&L: {2,number,#.##}",
         entryPrice, exitPrice, profit)

The first block detects new position opens by comparing the current number of open trades to the previous bar's count. When the count increases, a new position was filled. The second block detects closed trades the same way and logs the entry price, exit price, and profit for the most recently closed trade.

This gives you a trade-by-trade log that you can cross-reference against the Strategy Tester's trade list. If the values do not match, you have identified a discrepancy to investigate further.

Exit Order Mismatches

A common exit bug is mismatched entry IDs. The strategy.exit() function requires a from_entry argument that matches the string used in strategy.entry()3. If these do not match, the exit order is never placed.

// BUG: "Long" vs "long" - case-sensitive mismatch
strategy.entry("Long", strategy.long)
strategy.exit("Exit", from_entry = "long", profit = 100)  // This will NEVER execute

// FIX: Use consistent naming
string ENTRY_ID = "Long"
strategy.entry(ENTRY_ID, strategy.long)
strategy.exit("Exit", from_entry = ENTRY_ID, profit = 100)

// Log to verify
if strategy.opentrades > 0
    log.info("Open trade entry name: {0}", strategy.opentrades.entry_id(0))

Using a constant string variable for the entry ID eliminates this class of bug entirely. Log the entry ID from strategy.opentrades.entry_id() to confirm what the strategy actually recorded as the position's name3.

Catching Data Issues

Strategies that use external data from request.security() or request.financial() can silently break when the requested data is unavailable. Use log.error() to flag these failures immediately.

// Request data from another symbol
float spyClose = request.security("SPY", timeframe.period, close)
float vixClose = request.security("CBOE:VIX", timeframe.period, close)

if barstate.isconfirmed
    if na(spyClose)
        log.error("SPY data request returned na on bar {0}. Check symbol availability.", bar_index)
    if na(vixClose)
        log.error("VIX data request returned na on bar {0}. Check symbol/exchange prefix.", bar_index)

    // Guard against using na data in calculations
    if not na(spyClose) and not na(vixClose)
        float ratio = spyClose / vixClose
        log.info("SPY/VIX ratio: {0,number,#.##}", ratio)

When request.security() cannot retrieve data for a given bar, it returns na3. Without logging, this na silently propagates through your calculations and causes conditions to evaluate incorrectly. With log.error(), you get an immediate notification in the Pine Logs pane that points to the exact bar where data was missing.

Division by Zero Guards

Division by zero does not crash Pine Script. Instead, it produces na, which can silently break downstream logic4. Log a warning whenever a denominator is zero or na.

float avgVolume = ta.sma(volume, 20)
float relativeVolume = na

if barstate.isconfirmed
    if na(avgVolume) or avgVolume == 0
        log.warning("Average volume is {0} on bar {1}. Relative volume cannot be calculated.",
             na(avgVolume) ? "na" : "zero", bar_index)
    else
        relativeVolume := volume / avgVolume

Performance-Conscious Logging

Logging every tick on every bar generates thousands of messages and can slow down script execution. Use these patterns to log only the information you need, when you need it.

Log Only on Confirmed Bars

For most debugging purposes, you only care about the final values at bar close, not the intermediate values during realtime updates. Wrap your logging calls in a barstate.isconfirmed check3.

if barstate.isconfirmed
    log.info("Confirmed bar {0}: Close={1}", bar_index, close)

During historical execution, every bar is confirmed, so this logs every bar. During realtime execution, it logs only once per bar when the bar closes, rather than on every tick3. This dramatically reduces the number of log entries on live charts.

Log Only on Events

Instead of logging state on every bar, log only when something meaningful happens: an entry signal fires, a position opens, a threshold is crossed, or an unusual condition is detected.

// Only log when RSI crosses a threshold
float rsiVal = ta.rsi(close, 14)
if ta.crossover(rsiVal, 70)
    log.info("RSI crossed above 70 on bar {0}. Value: {1,number,#.##}", bar_index, rsiVal)
if ta.crossunder(rsiVal, 30)
    log.info("RSI crossed below 30 on bar {0}. Value: {1,number,#.##}", bar_index, rsiVal)

Log Only on Visible Bars

If you are debugging a visual issue on a specific chart range, restrict logging to the visible bars using chart.left_visible_bar_time and chart.right_visible_bar_time3.

bool isVisible = time >= chart.left_visible_bar_time and time <= chart.right_visible_bar_time
if isVisible and barstate.isconfirmed
    log.info("Visible bar {0}: Close={1}", bar_index, close)

This is especially useful on large datasets where logging every bar would produce tens of thousands of entries. By limiting to visible bars, you focus the log output on the exact range you are investigating.

Complete Debugging Walkthrough

Let us walk through debugging a real strategy that has a bug. Here is a strategy that should enter long on an EMA crossover and exit with a trailing stop, but it is not producing any trades.

The Buggy Strategy

//@version=6
strategy("Buggy Strategy", overlay = true)

float fastMA = ta.ema(close, 9)
float slowMA = ta.ema(close, 21)
float atrVal = ta.atr(14)

// Entry
if ta.crossover(fastMA, slowMA) and close > ta.sma(close, 200)
    strategy.entry("long", strategy.long)

// Exit: trailing stop
if strategy.opentrades > 0
    float trailPrice = strategy.position_avg_price - atrVal * 2
    strategy.exit("trail exit", "Long", stop = trailPrice)

Running this on a chart produces zero trades. Let us add logging to find the problem.

Adding Debug Logs

//@version=6
strategy("Buggy Strategy - Debug", overlay = true)

float fastMA  = ta.ema(close, 9)
float slowMA  = ta.ema(close, 21)
float longMA  = ta.sma(close, 200)
float atrVal  = ta.atr(14)

// Debug: Check entry components
bool crossCondition  = ta.crossover(fastMA, slowMA)
bool trendCondition  = close > longMA
bool entryCondition  = crossCondition and trendCondition

if barstate.isconfirmed and crossCondition
    log.info("Crossover detected on bar {0}. trendCondition={1}, longMA={2,number,#.##}, close={3,number,#.##}",
         bar_index, trendCondition, longMA, close)

if barstate.isconfirmed and na(longMA)
    if bar_index < 200
        log.warning("longMA is na on bar {0}. Waiting for 200 bars of data.", bar_index)

// Entry
if entryCondition
    log.info(">>> ENTRY on bar {0} at {1,number,#.####}", bar_index, close)
    strategy.entry("long", strategy.long)

// Debug: Check exit
if strategy.opentrades > 0
    float trailPrice = strategy.position_avg_price - atrVal * 2
    log.info("Open position. Avg price: {0,number,#.####}, Trail stop: {1,number,#.####}",
         strategy.position_avg_price, trailPrice)
    strategy.exit("trail exit", "Long", stop = trailPrice)

// Debug: Check for exit ID mismatch
if strategy.opentrades > 0
    string entryName = strategy.opentrades.entry_id(0)
    log.info("Entry ID in system: '{0}'", entryName)

Reading the Logs

After running the debug version, the Pine Logs pane reveals two critical findings.

Finding 1: The crossover detection logs show that crossCondition is true on several bars, but trendCondition is false on all of them. The 200-period SMA is na for the first 200 bars, and on subsequent bars, the close is below the SMA. This is a legitimate filter doing its job, but if you expected trades in the visible range, you now know why there are none.

Finding 2: If we temporarily remove the trend filter to allow entries, the logs reveal the real bug. The entry uses the ID string "long" (lowercase), but the exit references "Long" (uppercase). This case-sensitive mismatch means the exit order never attaches to the entry. The strategy enters positions but never exits them.

The Fixed Strategy

//@version=6
strategy("Fixed Strategy", overlay = true)

string ENTRY_ID = "Long"

float fastMA = ta.ema(close, 9)
float slowMA = ta.ema(close, 21)
float atrVal = ta.atr(14)

// Entry: removed 200 SMA filter or made it conditional
if ta.crossover(fastMA, slowMA)
    strategy.entry(ENTRY_ID, strategy.long)

// Exit: uses matching ENTRY_ID constant
if strategy.opentrades > 0
    float trailPrice = strategy.position_avg_price - atrVal * 2
    strategy.exit("Trail Exit", ENTRY_ID, stop = trailPrice)

The fix is simple: use a constant for the entry ID so both the entry and exit reference the exact same string. The strategy now enters and exits correctly.

Debugging Webhook Alerts

When your strategy sends alerts to an external system like TradersPost via webhooks, debugging extends beyond Pine Script5. The strategy might generate the right signals, but the webhook payload might be malformed, or the alert might fire on the wrong conditions.

Use logging to verify what your alert message contains before it is sent. Log the exact same string that goes into your alert() call so you can compare the Pine Logs output against what your webhook receiver reports.

string alertMsg = str.format(
     '{"action": "buy", "ticker": "{0}", "price": {1}}',
     syminfo.ticker, close
 )

if ta.crossover(ta.ema(close, 9), ta.ema(close, 21))
    log.info("Alert payload: {0}", alertMsg)
    alert(alertMsg, alert.freq_once_per_bar_close)

With TradersPost, you can test this workflow end-to-end. TradersPost accepts TradingView webhook alerts and routes them to your broker5. When something goes wrong, you can compare the log output in Pine Logs against the webhook history in your TradersPost dashboard to pinpoint whether the issue is in your Pine Script logic, the alert configuration, or the order routing.

TradersPost also provides its own execution logs, so you have debugging visibility across the entire chain: Pine Script generates the signal, TradingView sends the webhook, TradersPost processes the order, and the broker fills the trade5. If any link in that chain breaks, you can trace it through the combined logs.

Logging Best Practices

Keep these guidelines in mind to get the most out of Pine Script logging without degrading performance.

  • Use log.info() for routine tracing and variable inspection during development
  • Use log.warning() for conditions that are unusual but not necessarily broken, such as positions held longer than expected or calculations producing extreme values
  • Use log.error() for conditions that should never happen, such as impossible state combinations or critical calculation failures
  • Always wrap routine logging in barstate.isconfirmed to prevent duplicate entries on realtime bars
  • Remove or comment out verbose logging before publishing or sharing scripts, keeping only warning and error logs for production monitoring
  • Use descriptive prefixes like "ENTRY:", "EXIT:", "CALC:" in your log messages so you can visually scan the Pine Logs pane quickly

Logging is not just for finding bugs. It is a permanent part of your strategy development workflow. A well-instrumented strategy with strategic log.warning() and log.error() calls acts as its own monitoring system, alerting you to problems the moment they occur rather than after they have cost you money.

References

1 Pine Script Release Notes
2 Pine Script v5 to v6 Migration Guide
3 Pine Script v6 Language Reference
4 Pine Script User Manual
5 TradersPost - Automated Trading Platform

Ready to automate your trading? Try a free 7-day account:
Try it for free ->