Every serious trader hits the same wall: you want RSI in one pane, volume in another, and moving averages on the price chart — but TradingView limits how many indicators you can load (3 on the free plan). The solution? Build one Pine Script indicator that outputs to multiple chart panels simultaneously.
This tutorial walks through every technique available in Pine Script v5 and v6 to create multi-panel layouts from a single script. By the end, you'll have a working "Swiss Army Knife" indicator that plots across the main chart and its own separate pane at the same time.
> Need more indicator slots? Upgrade your TradingView plan to unlock up to 25 indicators per chart — essential for complex multi-panel setups.
The Problem: One Indicator, One Pane?
By default, a Pine Script indicator lives in exactly one place:
overlay = true→ plots on the main price chartoverlay = false→ plots in a separate pane below the chart
//@version=6
indicator("My Indicator", overlay = false) // separate pane only
plot(ta.rsi(close, 14), "RSI")
This means if you want RSI below *and* an EMA on the price chart, you traditionally needed two separate indicators — eating into your indicator limit.
That changed with force_overlay.
Step 1: Understanding force_overlay — The Key to Multi-Panel Output
Introduced in mid-2024 and available in both Pine Script v5 and v6, force_overlay lets individual plot elements display on the main chart pane even when the script itself occupies a separate pane.
Here's the core concept:
//@version=6
indicator("Multi-Panel Demo", overlay = false)
// This plots in the indicator's own pane (below the chart)
plot(ta.rsi(close, 14), "RSI", color.purple)
// This plots on the MAIN price chart, despite overlay = false
plot(ta.ema(close, 21), "EMA 21", color.orange, force_overlay = true)
What's happening:
- The indicator lives in its own pane (
overlay = false) - RSI renders in that pane normally
- The EMA "escapes" to the main chart via
force_overlay = true
Which Functions Support force_overlay?
Almost every visual function in Pine Script supports it:
| Function | force_overlay Support |
|---|---|
plot() | ✅ |
plotshape() | ✅ |
plotchar() | ✅ |
plotarrow() | ✅ |
plotcandle() | ✅ |
plotbar() | ✅ |
bgcolor() | ✅ |
hline() | ✅ |
fill() | ✅ |
label.new() | ✅ |
line.new() | ✅ |
box.new() | ✅ |
table.new() | ✅ |
Step 2: Build a Combined RSI + EMA + Volume Indicator
Let's build something practical — a single indicator that shows:
1. Main chart: EMA crossover lines + buy/sell signals
2. Separate pane: RSI with overbought/oversold zones 3. Separate pane: Volume (normalized) in the same pane as RSI
//@version=6
indicator("Multi-Panel Swiss Knife", overlay = false)
// ─── INPUTS ───
int rsiLen = input.int(14, "RSI Length")
int emaFast = input.int(9, "Fast EMA")
int emaSlow = input.int(21, "Slow EMA")
// ─── CALCULATIONS ───
float rsiVal = ta.rsi(close, rsiLen)
float emaF = ta.ema(close, emaFast)
float emaS = ta.ema(close, emaSlow)
bool bullCross = ta.crossover(emaF, emaS)
bool bearCross = ta.crossunder(emaF, emaS)
// ─── PANE: RSI (this indicator's own pane) ───
plot(rsiVal, "RSI", color.new(color.purple, 0), 2)
hline(70, "Overbought", color.red, hline.style_dashed)
hline(30, "Oversold", color.green, hline.style_dashed)
hline(50, "Midline", color.gray, hline.style_dotted)
// Color the RSI pane background on extremes
bgcolor(rsiVal > 70 ? color.new(color.red, 90) : rsiVal < 30 ? color.new(color.green, 90) : na)
// ─── MAIN CHART: EMA lines (force_overlay) ───
plot(emaF, "Fast EMA", color.new(color.blue, 0), 2, force_overlay = true)
plot(emaS, "Slow EMA", color.new(color.orange, 0), 2, force_overlay = true)
// ─── MAIN CHART: Buy/Sell signals (force_overlay) ───
plotshape(bullCross, "Buy Signal", shape.triangleup,
location.belowbar, color.green, size = size.small,
force_overlay = true)
plotshape(bearCross, "Sell Signal", shape.triangledown,
location.abovebar, color.red, size = size.small,
force_overlay = true)
// ─── MAIN CHART: Highlight background on crossover ───
bgcolor(bullCross ? color.new(color.green, 85) : bearCross ? color.new(color.red, 85) : na,
force_overlay = true)
Like what you're reading? Try it yourself — this link supports ChartedTrader at no cost to you.
Get TradingView Pro — unlock more indicators per chart →Result: One indicator uses one slot, but you see:
- RSI oscillator in its own pane below
- EMA lines rendered directly on the price chart
- Buy/sell arrows on the price candles
Step 3: Add a Multi-Symbol Comparison Panel
Want to compare multiple assets in the same pane? Use request.security() to pull data from other symbols:
//@version=6
indicator("Multi-Symbol RSI Comparison", overlay = false)
// ─── INPUTS ───
string sym1 = input.symbol("BINANCE:BTCUSDT", "Symbol 1")
string sym2 = input.symbol("BINANCE:ETHUSDT", "Symbol 2")
string sym3 = input.symbol("BINANCE:SOLUSDT", "Symbol 3")
int len = input.int(14, "RSI Length")
// ─── FETCH RSI FROM EACH SYMBOL ───
float rsi1 = request.security(sym1, timeframe.period, ta.rsi(close, len))
float rsi2 = request.security(sym2, timeframe.period, ta.rsi(close, len))
float rsi3 = request.security(sym3, timeframe.period, ta.rsi(close, len))
// ─── PLOT ALL IN ONE PANE ───
plot(rsi1, "BTC RSI", color.orange, 2)
plot(rsi2, "ETH RSI", color.blue, 2)
plot(rsi3, "SOL RSI", color.purple, 2)
hline(70, "OB", color.red, hline.style_dashed)
hline(30, "OS", color.green, hline.style_dashed)
// ─── MAIN CHART: Label current values ───
var table infoTable = table.new(position.top_right, 3, 2,
bgcolor = color.new(color.black, 70),
force_overlay = true)
if barstate.islast
table.cell(infoTable, 0, 0, "BTC RSI", text_color = color.orange)
table.cell(infoTable, 1, 0, "ETH RSI", text_color = color.blue)
table.cell(infoTable, 2, 0, "SOL RSI", text_color = color.purple)
table.cell(infoTable, 0, 1, str.tostring(rsi1, "#.0"), text_color = color.white)
table.cell(infoTable, 1, 1, str.tostring(rsi2, "#.0"), text_color = color.white)
table.cell(infoTable, 2, 1, str.tostring(rsi3, "#.0"), text_color = color.white)
This gives you a single pane comparing RSI across three symbols, plus a floating info table on the main chart.
Step 4: Simulate Multiple Sub-Panes with Visual Zones
Pine Script doesn't support creating multiple separate panes from one indicator. But you can visually divide a single pane into zones using hline() boundaries and scaled data:
//@version=6
indicator("Dual Zone Panel", overlay = false)
// ─── RSI: Scaled to upper zone (50–100 range) ───
float rsiRaw = ta.rsi(close, 14)
float rsiScaled = 50 + (rsiRaw / 100) * 50 // Maps 0–100 → 50–100
// ─── Stochastic: Scaled to lower zone (0–50 range) ───
float stochRaw = ta.stoch(close, high, low, 14)
float stochScaled = (stochRaw / 100) * 50 // Maps 0–100 → 0–50
// ─── Visual separator ───
hline(50, "─── Separator ───", color.new(color.white, 30), hline.style_solid)
// ─── Upper zone: RSI ───
plot(rsiScaled, "RSI", color.purple, 2)
hline(85, "RSI OB", color.new(color.red, 60), hline.style_dotted)
hline(65, "RSI OS", color.new(color.green, 60), hline.style_dotted)
// ─── Lower zone: Stochastic ───
plot(stochScaled, "Stoch %K", color.teal, 2)
hline(40, "Stoch OB", color.new(color.red, 60), hline.style_dotted)
hline(10, "Stoch OS", color.new(color.green, 60), hline.style_dotted)
// ─── Labels for clarity ───
var label rsiLabel = label.new(na, na, "RSI Zone ↑", style = label.style_none,
textcolor = color.purple, size = size.small)
var label stochLabel = label.new(na, na, "Stoch Zone ↓", style = label.style_none,
textcolor = color.teal, size = size.small)
if barstate.islast
label.set_xy(rsiLabel, bar_index + 3, 90)
label.set_xy(stochLabel, bar_index + 3, 25)
Result: One pane, two visually separated zones — RSI on top, Stochastic on the bottom, divided by a line at 50.
Step 5: The Full Dashboard — Putting It All Together
Here's a production-ready indicator that combines everything:
//@version=6
indicator("📊 Multi-Chart Dashboard", overlay = false, max_labels_count = 50)
// ─── INPUTS ───
int rsiLen = input.int(14, "RSI Length", group = "Oscillators")
int stochLen = input.int(14, "Stoch Length", group = "Oscillators")
int emaFast = input.int(9, "Fast EMA", group = "Trend")
int emaSlow = input.int(21, "Slow EMA", group = "Trend")
string sym2 = input.symbol("", "Compare Symbol", group = "Multi-Symbol")
// ─── CALCULATIONS ───
float rsiVal = ta.rsi(close, rsiLen)
float stochK = ta.stoch(close, high, low, stochLen)
float stochD = ta.sma(stochK, 3)
float emaF = ta.ema(close, emaFast)
float emaS = ta.ema(close, emaSlow)
bool bullX = ta.crossover(emaF, emaS)
bool bearX = ta.crossunder(emaF, emaS)
// ─── PANE: RSI (upper zone, 50–100) ───
float rsiDisplay = 50 + (rsiVal / 100) * 50
plot(rsiDisplay, "RSI", color.new(color.purple, 0), 2)
// ─── PANE: Stochastic (lower zone, 0–50) ───
float stochDisplay = (stochK / 100) * 50
float stochDDisplay = (stochD / 100) * 50
plot(stochDisplay, "%K", color.new(color.teal, 0), 2)
plot(stochDDisplay, "%D", color.new(color.orange, 0), 1, plot.style_line)
// ─── PANE: Zone separator and levels ───
hline(50, "───────────", color.new(color.white, 40), hline.style_solid)
hline(85, "RSI OB", color.new(color.red, 70), hline.style_dotted)
hline(65, "RSI OS", color.new(color.green, 70), hline.style_dotted)
hline(40, "Stoch OB", color.new(color.red, 70), hline.style_dotted)
hline(10, "Stoch OS", color.new(color.green, 70), hline.style_dotted)
// ─── MAIN CHART: EMAs ───
plot(emaF, "Fast EMA", color.new(color.blue, 0), 2, force_overlay = true)
plot(emaS, "Slow EMA", color.new(color.orange, 0), 2, force_overlay = true)
// ─── MAIN CHART: Signals ───
plotshape(bullX, "Buy", shape.triangleup, location.belowbar,
color.green, size = size.small, force_overlay = true)
plotshape(bearX, "Sell", shape.triangledown, location.abovebar,
color.red, size = size.small, force_overlay = true)
// ─── MAIN CHART: Info table ───
var table dash = table.new(position.top_right, 2, 4,
bgcolor = color.new(color.black, 75), border_width = 1,
border_color = color.new(color.gray, 60),
force_overlay = true)
if barstate.islast
table.cell(dash, 0, 0, "RSI", text_color = color.purple, text_size = size.small)
table.cell(dash, 1, 0, str.tostring(rsiVal, "#.0"),
text_color = rsiVal > 70 ? color.red : rsiVal < 30 ? color.green : color.white,
text_size = size.small)
table.cell(dash, 0, 1, "Stoch %K", text_color = color.teal, text_size = size.small)
table.cell(dash, 1, 1, str.tostring(stochK, "#.0"), text_color = color.white,
text_size = size.small)
table.cell(dash, 0, 2, "EMA Trend", text_color = color.blue, text_size = size.small)
table.cell(dash, 1, 2, emaF > emaS ? "▲ Bullish" : "▼ Bearish",
text_color = emaF > emaS ? color.green : color.red,
text_size = size.small)
// Optional second symbol comparison
if sym2 != ""
float sym2Close = request.security(sym2, timeframe.period, close)
float sym2Rsi = request.security(sym2, timeframe.period, ta.rsi(close, rsiLen))
table.cell(dash, 0, 3, sym2, text_color = color.yellow, text_size = size.small)
table.cell(dash, 1, 3, str.tostring(sym2Rsi, "#.0"),
text_color = color.white, text_size = size.small)
v5 vs v6: What Changed for Multi-Chart Layouts?
| Feature | Pine Script v5 | Pine Script v6 |
|---|---|---|
force_overlay | ✅ Available | ✅ Available |
request.security() | ✅ | ✅ (same behavior) |
request.footprint() | ❌ | ✅ New in Jan 2026 |
| Line wrapping | Strict 4-space rules | ✅ Relaxed (Dec 2025) |
| AI code assistant | ❌ | ✅ Built-in |
| Migration required? | No | Recommended for new features |
force_overlay works in both v5 and v6. If you're writing new code, use v6 — future features will only land there. If you have existing v5 scripts, they'll keep working without changes.
Tips and Limitations
What You Can Do
- ✅ Plot on the main chart from a pane indicator (
force_overlay = true) - ✅ Draw labels, lines, boxes, and tables on the main chart from a pane
- ✅ Compare multiple symbols in one pane using
request.security() - ✅ Visually split a single pane into zones with scaled data
What You Can't Do (Yet)
- ❌ Create multiple *separate* panes from one indicator
- ❌ Control pane ordering or height from Pine Script
- ❌ Use
force_overlayto plot on *another indicator's* pane - ❌ Plot from an overlay indicator into a separate pane (only pane → main chart works)
Performance Tips
- Each
request.security()call counts toward TradingView's limit (40 per script) force_overlayplots consume the same resources as regular plots- Use
max_labels_countandmax_lines_countto avoid memory limits - Group related inputs using the
groupparameter for cleaner Settings UI
Conclusion
The force_overlay parameter is the single most important feature for building multi-panel Pine Script indicators. Combined with data scaling for visual sub-zones and request.security() for multi-symbol data, you can pack a full trading dashboard into one indicator slot.
This matters most on TradingView's free plan (3 indicators) but is useful even on paid plans — fewer indicators means faster chart loading and a cleaner workspace.
Ready to build your own multi-panel indicators? Start your free TradingView trial to get access to the Pine Script editor, more indicator slots, and real-time data across global markets.---
*This tutorial uses Pine Script v6 syntax. All force_overlay examples are backward-compatible with v5 — just change the version comment to //@version=5.*