External Integration 🌐
Pine Script lives inside TradingView, but trading rarely stays inside one platform. You need data from other symbols, signals sent to bots, alerts pushed to your phone, and strategies that talk to brokers through webhooks. This chapter covers every integration technique available in Pine Script v5: pulling in external data with request.security(), formatting structured alert payloads, building multi-symbol screeners, and connecting to external services through webhooks.
Working with External Data
The request.security() Function
The request.security() function is the primary gateway to external data in Pine Script. It lets you fetch price data, indicator values, or any calculated expression from a different symbol or timeframe.
The basic syntax is:
request.security(symbol, timeframe, expression, gaps, lookahead)
- symbol -- the ticker identifier (e.g.,
"BTCUSD","AAPL", orsyminfo.tickerid) - timeframe -- the resolution string (e.g.,
"D"for daily,"60"for hourly,""for the chart timeframe) - expression -- any Pine Script expression to evaluate on that symbol/timeframe
- gaps -- how to handle gaps between bars (
barmerge.gaps_offfills them,barmerge.gaps_onleavesna) - lookahead -- controls whether future data leaks into historical bars
Here is a complete example that fetches BTC and ETH closing prices and plots their rolling correlation:
//@version=5
indicator("BTC-ETH Correlation", overlay=false)
// Fetch closing prices from two symbols on the current timeframe
btcClose = request.security("BTCUSD", timeframe.period, close)
ethClose = request.security("ETHUSD", timeframe.period, close)
// Calculate 50-bar Pearson correlation
corrLength = input.int(50, "Correlation Length", minval=10)
correlation = ta.correlation(btcClose, ethClose, corrLength)
// Plot correlation with reference lines
plot(correlation, "BTC-ETH Correlation", color=color.yellow, linewidth=2)
hline(0.8, "Strong Positive", color=color.green, linestyle=hline.style_dashed)
hline(0.0, "Zero", color=color.gray, linestyle=hline.style_dotted)
hline(-0.8, "Strong Negative", color=color.red, linestyle=hline.style_dashed)
// Background color based on correlation strength
bgColor = correlation > 0.8 ? color.new(color.green, 90) : correlation < -0.8 ? color.new(color.red, 90) : na
bgcolor(bgColor)
This script requests close data for both BTCUSD and ETHUSD, computes a 50-bar rolling Pearson correlation, and plots it with visual reference zones. When the correlation is above 0.8, the background turns green; below -0.8, it turns red.
Understanding Lookahead and Repainting
The lookahead parameter in request.security() is one of the most misunderstood features in Pine Script. It controls whether the function returns data from a higher-timeframe bar before that bar has actually closed.
barmerge.lookahead_off(default) -- returns the value from the previous completed higher-timeframe bar. This is safe for trading signals because it only uses confirmed data.barmerge.lookahead_on-- returns the value from the current (possibly incomplete) higher-timeframe bar. On historical bars this "peeks" into the future, causing repainting: signals that appear on historical data but would not have existed in real-time.
//@version=5
indicator("Lookahead Comparison", overlay=true)
// SAFE: uses only confirmed daily close (previous daily bar)
dailySafe = request.security(syminfo.tickerid, "D", close, lookahead=barmerge.lookahead_off)
// DANGEROUS: peeks at current daily bar's close (repaints on historical data)
dailyUnsafe = request.security(syminfo.tickerid, "D", close, lookahead=barmerge.lookahead_on)
plot(dailySafe, "Safe Daily Close", color=color.green, linewidth=2)
plot(dailyUnsafe, "Unsafe Daily Close", color=color.red, linewidth=1)
On a lower timeframe chart (e.g., 1-hour), the green line steps only when a new daily bar confirms. The red line shows the final daily close value across all intraday bars -- something you could not have known in real time. Always use barmerge.lookahead_off for any signal you plan to trade on. The only acceptable use of lookahead_on is for visual reference levels (like showing today's open on an intraday chart), never for entry or exit decisions.
Multi-Timeframe Analysis
Multi-timeframe (MTF) analysis is one of the most powerful applications of request.security(). You can evaluate conditions across daily, weekly, and monthly timeframes to find trend alignment.
The tuple return pattern lets you fetch multiple values in a single request.security() call, which is both efficient and clean:
//@version=5
indicator("MTF Trend Alignment", overlay=true)
// Inputs
fastLen = input.int(10, "Fast MA Length")
slowLen = input.int(30, "Slow MA Length")
// Define a function that returns trend data as a tuple
getTrend() =>
fastMA = ta.ema(close, fastLen)
slowMA = ta.ema(close, slowLen)
trendUp = fastMA > slowMA
[fastMA, slowMA, trendUp]
// Fetch trend data from three timeframes using tuples
[dFast, dSlow, dUp] = request.security(syminfo.tickerid, "D", getTrend())
[wFast, wSlow, wUp] = request.security(syminfo.tickerid, "W", getTrend())
[mFast, mSlow, mUp] = request.security(syminfo.tickerid, "M", getTrend())
// Count how many timeframes are bullish
bullCount = (dUp ? 1 : 0) + (wUp ? 1 : 0) + (mUp ? 1 : 0)
// Color-coded background: green=all bullish, red=all bearish, gray=mixed
bgCol = bullCount == 3 ? color.new(color.green, 85) : bullCount == 0 ? color.new(color.red, 85) : color.new(color.gray, 93)
bgcolor(bgCol)
// Display alignment status in a label on the last bar
if barstate.islast
alignText = "Daily: " + (dUp ? "BULL" : "BEAR") + "\nWeekly: " + (wUp ? "BULL" : "BEAR") + "\nMonthly: " + (mUp ? "BULL" : "BEAR") + "\nAlignment: " + str.tostring(bullCount) + "/3"
label.new(bar_index, high, alignText, style=label.style_label_down, color=bullCount == 3 ? color.green : bullCount == 0 ? color.red : color.gray, textcolor=color.white)
This indicator evaluates a fast/slow EMA crossover on three timeframes. By returning a tuple from getTrend(), each request.security() call fetches three values at once. The background color shows full alignment (green for all bullish, red for all bearish) and a label on the last bar displays the breakdown.
Multi-Symbol Screener
Pine Script can monitor multiple symbols simultaneously and display their status in a table. This is useful for scanning a basket of stocks or crypto assets without leaving your chart.
//@version=5
indicator("Multi-Symbol Screener", overlay=true)
// Define symbols to monitor
sym1 = input.symbol("AAPL", "Symbol 1")
sym2 = input.symbol("MSFT", "Symbol 2")
sym3 = input.symbol("GOOGL", "Symbol 3")
sym4 = input.symbol("AMZN", "Symbol 4")
sym5 = input.symbol("TSLA", "Symbol 5")
sym6 = input.symbol("NVDA", "Symbol 6")
// Function to compute screener data for a symbol
getScreenerData() =>
rsiVal = ta.rsi(close, 14)
[macdLine, signalLine, histLine] = ta.macd(close, 12, 26, 9)
macdBull = macdLine > signalLine
sma50 = ta.sma(close, 50)
trendUp = close > sma50
[rsiVal, macdBull, trendUp, close]
// Fetch data for all symbols
[rsi1, macd1, trend1, price1] = request.security(sym1, "D", getScreenerData())
[rsi2, macd2, trend2, price2] = request.security(sym2, "D", getScreenerData())
[rsi3, macd3, trend3, price3] = request.security(sym3, "D", getScreenerData())
[rsi4, macd4, trend4, price4] = request.security(sym4, "D", getScreenerData())
[rsi5, macd5, trend5, price5] = request.security(sym5, "D", getScreenerData())
[rsi6, macd6, trend6, price6] = request.security(sym6, "D", getScreenerData())
// Build a table on the last bar
if barstate.islast
var table screenTbl = table.new(position.top_right, 5, 7, bgcolor=color.new(color.black, 30), border_width=1, border_color=color.gray)
// Header row
table.cell(screenTbl, 0, 0, "Symbol", text_color=color.white, text_size=size.small)
table.cell(screenTbl, 1, 0, "Price", text_color=color.white, text_size=size.small)
table.cell(screenTbl, 2, 0, "RSI(14)", text_color=color.white, text_size=size.small)
table.cell(screenTbl, 3, 0, "MACD", text_color=color.white, text_size=size.small)
table.cell(screenTbl, 4, 0, "Trend", text_color=color.white, text_size=size.small)
// Helper to fill one row
fillRow(tbl, row, sym, price, rsi, macdBull, trendUp) =>
rsiColor = rsi > 70 ? color.red : rsi < 30 ? color.green : color.gray
table.cell(tbl, 0, row, sym, text_color=color.white, text_size=size.small)
table.cell(tbl, 1, row, str.tostring(price, format.mintick), text_color=color.white, text_size=size.small)
table.cell(tbl, 2, row, str.tostring(rsi, "#.0"), text_color=color.white, bgcolor=rsiColor, text_size=size.small)
table.cell(tbl, 3, row, macdBull ? "BULL" : "BEAR", text_color=color.white, bgcolor=macdBull ? color.green : color.red, text_size=size.small)
table.cell(tbl, 4, row, trendUp ? "UP" : "DOWN", text_color=color.white, bgcolor=trendUp ? color.green : color.red, text_size=size.small)
fillRow(screenTbl, 1, sym1, price1, rsi1, macd1, trend1)
fillRow(screenTbl, 2, sym2, price2, rsi2, macd2, trend2)
fillRow(screenTbl, 3, sym3, price3, rsi3, macd3, trend3)
fillRow(screenTbl, 4, sym4, price4, rsi4, macd4, trend4)
fillRow(screenTbl, 5, sym5, price5, rsi5, macd5, trend5)
fillRow(screenTbl, 6, sym6, price6, rsi6, macd6, trend6)
This screener fetches daily RSI, MACD direction, and 50-SMA trend for six symbols and renders the results in a color-coded table in the top-right corner. Each request.security() call uses the tuple pattern to retrieve all four values in one request, keeping us well within TradingView's limits.
Data Export with Alerts
Structured Alert Messages
Pine Script cannot write files directly, but it can send structured data through TradingView's alert system. By formatting alert messages as JSON, you can pipe them into any webhook receiver, logging system, or trading bot.
//@version=5
indicator("Structured Alert Export", overlay=true)
// Compute signals
fastMA = ta.sma(close, 10)
slowMA = ta.sma(close, 30)
bullCross = ta.crossover(fastMA, slowMA)
bearCross = ta.crossunder(fastMA, slowMA)
// Build JSON payload
buildPayload(signalType) =>
payload = "{"
payload += '"symbol":"' + syminfo.ticker + '",'
payload += '"exchange":"' + syminfo.prefix + '",'
payload += '"price":' + str.tostring(close) + ','
payload += '"signal":"' + signalType + '",'
payload += '"timeframe":"' + timeframe.period + '",'
payload += '"volume":' + str.tostring(volume) + ','
payload += '"timestamp":' + str.tostring(time) + ''
payload += "}"
payload
// Fire alerts on crossover events
if bullCross
alert(buildPayload("BUY"), alert.freq_once_per_bar_close)
if bearCross
alert(buildPayload("SELL"), alert.freq_once_per_bar_close)
// Visual markers
plotshape(bullCross, "Buy", shape.triangleup, location.belowbar, color.green, size=size.small)
plotshape(bearCross, "Sell", shape.triangledown, location.abovebar, color.red, size=size.small)
When a moving average crossover occurs, this script fires an alert with a complete JSON payload containing symbol, exchange, price, signal type, timeframe, volume, and timestamp. The alert.freq_once_per_bar_close constant ensures the alert fires only once when the bar closes, preventing duplicate messages.
Alert Conditions vs alert()
Pine Script provides two different alert mechanisms, and they serve different purposes:
alertcondition()-- available only in indicators. It defines a named condition that users can select when creating alerts in the TradingView UI. The message is static (set at compile time).alert()-- available in both indicators and strategies. It fires programmatically with a dynamic message string. This is what you need for webhook integration.
//@version=5
indicator("Alert Types Demo", overlay=true)
rsiVal = ta.rsi(close, 14)
// alertcondition: user selects this in the alert creation dialog
alertcondition(rsiVal > 70, title="RSI Overbought", message="RSI is above 70 on {{ticker}}")
alertcondition(rsiVal < 30, title="RSI Oversold", message="RSI is below 30 on {{ticker}}")
// alert(): fires programmatically with dynamic content
if rsiVal > 70 and rsiVal[1] <= 70
alert('{"event":"overbought","rsi":' + str.tostring(rsiVal, "#.00") + ',"symbol":"' + syminfo.ticker + '"}', alert.freq_once_per_bar_close)
if rsiVal < 30 and rsiVal[1] >= 30
alert('{"event":"oversold","rsi":' + str.tostring(rsiVal, "#.00") + ',"symbol":"' + syminfo.ticker + '"}', alert.freq_once_per_bar_close)
plot(rsiVal, "RSI", color=color.orange)
hline(70, "Overbought")
hline(30, "Oversold")
The alertcondition() calls create selectable alert options in the TradingView UI. The alert() calls fire automatically and send dynamic JSON payloads -- useful when the alert message needs to include real-time values like the exact RSI reading.
Webhook Integration
Creating Webhook Payloads
TradingView alerts can send HTTP POST requests to any webhook URL. When you create an alert and set a webhook URL in the notifications tab, TradingView sends the alert message as the request body. This is the bridge between Pine Script and the outside world.
//@version=5
strategy("Webhook Strategy", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100)
// Strategy logic
fastEMA = ta.ema(close, 9)
slowEMA = ta.ema(close, 21)
atrVal = ta.atr(14)
longCondition = ta.crossover(fastEMA, slowEMA)
shortCondition = ta.crossunder(fastEMA, slowEMA)
// Build webhook JSON for different actions
webhookPayload(action) =>
msg = "{"
msg += '"action":"' + action + '",'
msg += '"symbol":"' + syminfo.ticker + '",'
msg += '"exchange":"' + syminfo.prefix + '",'
msg += '"price":' + str.tostring(close) + ','
msg += '"atr":' + str.tostring(atrVal, "#.00") + ','
msg += '"position_size":' + str.tostring(strategy.position_size) + ','
msg += '"timestamp":' + str.tostring(timenow)
msg += "}"
msg
// Execute strategy orders and send webhooks
if longCondition
strategy.entry("Long", strategy.long)
alert(webhookPayload("BUY"), alert.freq_once_per_bar_close)
if shortCondition
strategy.close("Long")
alert(webhookPayload("SELL"), alert.freq_once_per_bar_close)
plot(fastEMA, "Fast EMA", color=color.green)
plot(slowEMA, "Slow EMA", color=color.red)
Each time the strategy enters or exits, an alert fires with a JSON body that a webhook receiver can parse. To set this up in TradingView, create an alert on the strategy, paste your webhook URL into the Notifications tab, and the alert message will be POSTed to that URL.
Common Webhook Formats
Different trading platforms expect different JSON structures. Here are complete examples for three popular formats:
Generic REST API format:
//@version=5
strategy("Generic REST Webhook", overlay=true, default_qty_type=strategy.fixed, default_qty_value=1)
longCond = ta.crossover(ta.sma(close, 10), ta.sma(close, 30))
shortCond = ta.crossunder(ta.sma(close, 10), ta.sma(close, 30))
restPayload(side, qty) =>
msg = "{"
msg += '"side":"' + side + '",'
msg += '"symbol":"' + syminfo.ticker + '",'
msg += '"type":"market",'
msg += '"quantity":' + str.tostring(qty)
msg += "}"
msg
if longCond
strategy.entry("Long", strategy.long)
alert(restPayload("buy", 1), alert.freq_once_per_bar_close)
if shortCond
strategy.close("Long")
alert(restPayload("sell", 1), alert.freq_once_per_bar_close)
3Commas bot signal format:
//@version=5
strategy("3Commas Webhook", overlay=true, default_qty_type=strategy.fixed, default_qty_value=1)
longCond = ta.crossover(ta.ema(close, 9), ta.ema(close, 21))
shortCond = ta.crossunder(ta.ema(close, 9), ta.ema(close, 21))
// 3Commas expects a specific JSON structure with email_token and bot_id
// Replace placeholder values with your actual 3Commas credentials
botId = "YOUR_BOT_ID"
emailToken = "YOUR_EMAIL_TOKEN"
threeCommasPayload(action) =>
msg = "{"
msg += '"message_type":"bot",'
msg += '"bot_id":"' + botId + '",'
msg += '"email_token":"' + emailToken + '",'
msg += '"delay_seconds":0,'
msg += '"action":"' + action + '"'
msg += "}"
msg
if longCond
strategy.entry("Long", strategy.long)
alert(threeCommasPayload("start_long_deal"), alert.freq_once_per_bar_close)
if shortCond
strategy.close("Long")
alert(threeCommasPayload("close_at_market_price"), alert.freq_once_per_bar_close)
Autoview format:
//@version=5
strategy("Autoview Webhook", overlay=true, default_qty_type=strategy.fixed, default_qty_value=1)
longCond = ta.crossover(ta.rsi(close, 14), 30)
shortCond = ta.crossunder(ta.rsi(close, 14), 70)
// Autoview uses a simple command string format
autoviewPayload(side) =>
msg = "{"
msg += '"exchange":"binance",'
msg += '"symbol":"' + syminfo.ticker + '",'
msg += '"side":"' + side + '",'
msg += '"type":"market",'
msg += '"amount":"100%"'
msg += "}"
msg
if longCond
strategy.entry("Long", strategy.long)
alert(autoviewPayload("buy"), alert.freq_once_per_bar_close)
if shortCond
strategy.close("Long")
alert(autoviewPayload("sell"), alert.freq_once_per_bar_close)
Each format targets a different receiver. The key principle is the same: build a well-formed JSON string inside Pine Script and pass it to alert(). The webhook receiver parses this JSON and executes the corresponding action.
Real-Time Integration Patterns
State Management
When integrating with external data, you need to track values across bars and handle the difference between confirmed and unconfirmed data. The var keyword persists values across bars, and barstate.isconfirmed tells you whether the current bar is finalized.
//@version=5
indicator("State Tracking", overlay=false)
// Persistent arrays to track external data history
var float[] confirmedPrices = array.new_float(0)
var int maxHistory = 50
// Fetch external symbol data
externalPrice = request.security("SPX", "D", close)
// Only record confirmed bar data to avoid repainting
if barstate.isconfirmed and not na(externalPrice)
if array.size(confirmedPrices) >= maxHistory
array.shift(confirmedPrices)
array.push(confirmedPrices, externalPrice)
// Calculate statistics on confirmed data only
avgPrice = array.size(confirmedPrices) > 0 ? array.avg(confirmedPrices) : na
stdPrice = array.size(confirmedPrices) > 1 ? array.stdev(confirmedPrices) : na
plot(externalPrice, "SPX Daily Close", color=color.blue)
plot(avgPrice, "Average", color=color.orange)
plot(avgPrice + stdPrice, "Upper Band", color=color.gray)
plot(avgPrice - stdPrice, "Lower Band", color=color.gray)
By gating data collection behind barstate.isconfirmed, this script only records finalized daily closes from the SPX index. This prevents the array from capturing intrabar fluctuations that would skew your statistics.
Safe External Requests
request.security() can return na when the requested symbol is unavailable, the market is closed, or data simply has not loaded yet. Robust scripts must handle this gracefully using na checks and fallback values -- Pine Script does not have try/catch blocks.
//@version=5
indicator("Safe External Data", overlay=true)
// Request external data
rawBTC = request.security("BTCUSD", "D", close)
rawGold = request.security("XAUUSD", "D", close)
// Persistent fallback: store the last known good value
var float lastBTC = na
var float lastGold = na
// Update fallback when valid data arrives
safeBTC = not na(rawBTC) ? rawBTC : lastBTC
safeGold = not na(rawGold) ? rawGold : lastGold
// Store for next bar's fallback
if not na(rawBTC)
lastBTC := rawBTC
if not na(rawGold)
lastGold := rawGold
// Safely compute ratio only when both values are available
btcGoldRatio = not na(safeBTC) and not na(safeGold) and safeGold != 0 ? safeBTC / safeGold : na
// Display the ratio
plot(btcGoldRatio, "BTC/Gold Ratio", color=color.yellow, linewidth=2)
// Visual warning when using fallback data
isUsingFallback = na(rawBTC) or na(rawGold)
bgcolor(isUsingFallback ? color.new(color.orange, 95) : na)
This pattern uses var variables to remember the last known good value. When fresh data is na, the script falls back to the stored value instead of breaking. The background highlights bars where fallback data is being used, so you always know when you are looking at stale information.
Rate Limits and Best Practices
TradingView enforces a limit of 40 request.security() calls per script. Each call to request.security() counts toward this limit regardless of whether it fetches one value or a tuple of values. This makes tuples essential for efficient scripts.
Instead of making five separate calls for OHLCV data, combine them into a single tuple call:
//@version=5
indicator("Efficient Data Fetching", overlay=false)
// BAD: 5 separate calls = 5 toward the 40-call limit
// badOpen = request.security("AAPL", "D", open)
// badHigh = request.security("AAPL", "D", high)
// badLow = request.security("AAPL", "D", low)
// badClose = request.security("AAPL", "D", close)
// badVolume = request.security("AAPL", "D", volume)
// GOOD: 1 tuple call = 1 toward the 40-call limit
[aaplOpen, aaplHigh, aaplLow, aaplClose, aaplVol] = request.security("AAPL", "D", [open, high, low, close, volume])
// Now use all five values freely
dailyRange = aaplHigh - aaplLow
dailyChange = aaplClose - aaplOpen
avgVol = ta.sma(aaplVol, 20)
plot(dailyRange, "AAPL Daily Range", color=color.blue)
plot(dailyChange, "AAPL Daily Change", color=dailyChange > 0 ? color.green : color.red)
By wrapping [open, high, low, close, volume] in a single tuple, you consume only one call out of your 40-call budget instead of five. This is critical when building multi-symbol screeners -- a 10-symbol screener with tuple calls uses only 10 calls, but without tuples you could easily exceed the limit.
Performance Considerations
Caching Expensive Request Results
The var keyword declares a variable that retains its value across bars and only initializes once. This is useful for caching computed results from external data so you do not re-derive them every bar.
//@version=5
indicator("Cached External Data", overlay=false)
// Fetch daily data from external symbol
[extClose, extVolume] = request.security("MSFT", "D", [close, volume])
// Cache: only recompute when the daily bar changes
var float cachedVWAP = na
var float prevDayClose = na
// Detect when a new daily bar has started
newDay = na(prevDayClose) or extClose != prevDayClose
if newDay and not na(extClose) and not na(extVolume) and extVolume != 0
cachedVWAP := extClose // Simplified: in practice, compute your expensive metric here
prevDayClose := extClose
plot(cachedVWAP, "Cached Metric", color=color.purple, linewidth=2)
plot(extClose, "MSFT Daily Close", color=color.gray)
Reducing Update Frequency
For data that does not need to update on every tick, you can gate calculations behind barstate.isconfirmed or check whether higher-timeframe data has actually changed:
//@version=5
indicator("Update Frequency Control", overlay=true)
// External data -- only meaningful at bar close
externalRSI = request.security("QQQ", "D", ta.rsi(close, 14))
// Only update display when bar confirms
var float displayRSI = na
if barstate.isconfirmed and not na(externalRSI)
displayRSI := externalRSI
// Show on a label only at bar close to avoid flickering
var label rsiLabel = na
if barstate.islast
if not na(rsiLabel)
label.delete(rsiLabel)
labelColor = displayRSI > 70 ? color.red : displayRSI < 30 ? color.green : color.gray
rsiLabel := label.new(bar_index, high, "QQQ RSI: " + str.tostring(displayRSI, "#.0"), style=label.style_label_down, color=labelColor, textcolor=color.white)
By storing the RSI in a var variable and updating it only on confirmed bars, the label does not flicker with every tick. This approach is cleaner and reduces unnecessary computation on real-time bars.
Practice Exercises
Exercise 1: 5-Symbol Correlation Matrix
Build an indicator that calculates the pairwise correlation between 5 symbols and displays the results in a color-coded table. Use ta.correlation() with a 50-bar window. Green cells indicate strong positive correlation (> 0.7), red cells indicate strong negative correlation (< -0.7), and gray cells for everything in between.
Exercise 2: Webhook Alert System
Create a strategy that generates structured JSON webhook alerts for three events: BUY (on entry), SELL (on exit), and CLOSE (on end of day). Each alert should include: action, symbol, price, quantity, stop loss level, and take profit level. Test by checking the alert message preview in TradingView's alert dialog.
Exercise 3: Multi-Timeframe Confluence Indicator
Build an indicator that checks the trend direction (EMA 20 vs EMA 50) on 3 timeframes (1H, 4H, Daily). Display a table showing each timeframe's trend. Only generate a "Strong Buy" alert when all three timeframes are bullish, and a "Strong Sell" alert when all three are bearish. Use barstate.isconfirmed to avoid false signals on unconfirmed bars.
- Use tuples aggressively -- every
request.security()call counts toward the 40-call limit, but a tuple of 10 values still counts as one call. - Always use
barmerge.lookahead_offfor trading signals. Reservelookahead_ononly for visual reference levels. - Gate data with
barstate.isconfirmedwhen building arrays of external data to prevent repainting artifacts. - Format JSON carefully -- a missing comma or unescaped quote will cause webhook receivers to reject your payload. Test your JSON output in TradingView's alert preview before going live.
- Use
alert.freq_once_per_bar_closefor most trading alerts to avoid duplicate signals from intrabar price fluctuations. - Store fallback values with
varso your script degrades gracefully whenrequest.security()returnsnainstead of crashing.
- Using
barmerge.lookahead_onfor trade signals causes repainting -- your backtest will look profitable but real-time results will differ. - Exceeding the 40
request.security()call limit causes a compilation error. Count your calls carefully and use tuples to consolidate. - Calling
request.security()inside local scopes (likeifblocks or loops) is not allowed -- all calls must be at the global scope. - Pine Script does not have
try/catchorruntime.error(). Always usena()checks and fallback values for error handling. - Webhook payloads with unescaped special characters (quotes, backslashes) will fail silently. Always test your JSON structure.
- Forgetting to use
varfor fallback storage means the fallback resets tonaevery bar, defeating the purpose.
Next Steps
Ready to learn about performance optimization? Move on to the next chapter! 🚀