Arrays are one of the key building blocks of advanced Pine Script development. Once you move beyond simple indicators and strategy conditions, arrays give you the ability to store custom data, manage rolling windows, rank values, and calculate statistics that Pine does not provide automatically.
Most Pine Script code starts with built-in series such as close, high, low, or indicator outputs like ta.ema() and ta.rsi(). That works well for many scripts, but sooner or later you reach problems that require more control.
You may want to store the last 50 closes manually. You may want to rank the current bar against recent history. You may want to keep a list of custom trade results, breakout ranges, or signal scores. That is where arrays become useful.
An array is a collection of values that you manage yourself. You decide what gets added, when it gets removed, and how it gets analysed. That extra control makes arrays one of the most important concepts in advanced Pine Script.
Most Pine Script code starts with built-in series such as close, high, low, or indicator outputs like ta.ema() and ta.rsi(). That works well for many scripts, but sooner or later you reach problems that require more control.
You may want to store the last 50 closes manually. You may want to rank the current bar against recent history. You may want to keep a list of custom trade results, breakout ranges, or signal scores. That is where arrays become useful.
An array is a collection of values that you manage yourself. You decide what gets added, when it gets removed, and how it gets analysed. That extra control makes arrays one of the most important concepts in advanced Pine Script.
A standard Pine series is managed for you by TradingView. If you write close[1], Pine already knows you want the previous bar’s close.
Arrays are different.
With arrays, you build your own storage structure. That means arrays are especially useful when you need logic such as:
The simplest way to think about arrays is this:
Use series when Pine already gives you the structure you need. Use arrays when you need to build that structure yourself.
If you want an array to keep its contents from one bar to the next, declare it with var.
Without var, the array is recreated every time the script runs. That means your script appears to be storing history, but it is actually resetting repeatedly.
//@version=6
indicator("Persistent Array Demo", overlay=false)
var array<float> closes = array.new<float>(0)
if barstate.isconfirmed
array.push(closes, close)
plot(array.size(closes), "Array Size")
This array starts empty, then stores one additional closing price each time a bar confirms. Because it is declared with var, the array keeps growing over time rather than resetting on every bar.
In real scripts, you often do not want the array to grow forever. You usually want a fixed-size rolling window.
That is where the queue pattern comes in:
Why this matters
This is one of the most reusable array patterns in Pine Script. It gives you a manually controlled rolling dataset that you can analyse however you like.
You can use the same structure for:
//@version=6
indicator("Rolling Queue Demo", overlay=false)
window = input.int(20, "Window Size", minval=1)
var array<float> closes = array.new<float>(0)
if barstate.isconfirmed
array.push(closes, close)
if array.size(closes) > window
array.shift(closes)
plot(array.size(closes), "Stored Samples")
Here is a stronger example that teaches several array concepts at once:
//@version=6
indicator("Arrays: Rolling Statistics + Percentile", overlay=false)
window = input.int(50, "Window Size", minval=5)
showStats = input.bool(true, "Show Stats Table")
getMedian(array<float> src) =>
float result = na
int n = array.size(src)
if n > 0
array<float> tmp = array.copy(src)
array.sort(tmp, order.ascending)
int mid = int(math.floor((n - 1) / 2.0))
result := n % 2 == 1 ? array.get(tmp, mid) : (array.get(tmp, mid) + array.get(tmp, mid + 1)) / 2.0
result
getPercentileRank(array<float> src, float value) =>
float pct = na
int n = array.size(src)
if n > 0 and not na(value)
int below = 0
int equal = 0
for i = 0 to n - 1
float v = array.get(src, i)
if value > v
below += 1
else if value == v
equal += 1
pct := ((below + equal * 0.5) / n) * 100.0
pct
var array<float> closes = array.new<float>(0)
var float percentile = na
if barstate.isconfirmed
percentile := getPercentileRank(closes, close)
array.push(closes, close)
if array.size(closes) > window
array.shift(closes)
samples = array.size(closes)
avgClose = samples > 0 ? array.avg(closes) : na
maxClose = samples > 0 ? array.max(closes) : na
minClose = samples > 0 ? array.min(closes) : na
stdDev = samples > 1 ? array.stdev(closes) : na
median = samples > 0 ? getMedian(closes) : na
rangeVal = samples > 0 ? maxClose - minClose : na
zScore = samples > 1 and stdDev != 0 ? (close - avgClose) / stdDev : na
plot(percentile, "Close Percentile vs Previous Window", color=color.purple, linewidth=2)
hline(80, "High Percentile", color=color.red)
hline(50, "Mid", color=color.gray)
hline(20, "Low Percentile", color=color.green)
var table stats = table.new(position.top_right, 2, 7, border_width=1)
if barstate.islast and showStats
table.cell(stats, 0, 0, "Samples")
table.cell(stats, 1, 0, str.tostring(samples))
table.cell(stats, 0, 1, "Average")
table.cell(stats, 1, 1, str.tostring(avgClose, format.mintick))
table.cell(stats, 0, 2, "Median")
table.cell(stats, 1, 2, str.tostring(median, format.mintick))
table.cell(stats, 0, 3, "Min / Max")
table.cell(stats, 1, 3, str.tostring(minClose, format.mintick) + " / " + str.tostring(maxClose, format.mintick))
table.cell(stats, 0, 4, "Std Dev")
table.cell(stats, 1, 4, str.tostring(stdDev, format.mintick))
table.cell(stats, 0, 5, "Range")
table.cell(stats, 1, 5, str.tostring(rangeVal, format.mintick))
table.cell(stats, 0, 6, "Z-Score")
table.cell(stats, 1, 6, str.tostring(zScore, format.mintick))
The closes array acts as a manually controlled rolling dataset. Each confirmed bar adds a new close, and if the array grows beyond the chosen window size, the oldest value is removed.
That gives you a fixed-size collection of recent market data.
The script loops through the stored closes and checks how many values are below the current close. That lets you answer a useful question:
Where does today’s close sit relative to the recent distribution of closes?
That kind of ranking is not something most traders calculate with built-in series functions alone. Arrays make it straightforward.
To calculate median, the script makes a copy of the array and sorts the copy rather than sorting the live array directly.
That matters because sorting the original array would change the stored order of your rolling dataset. In many scripts, that is not what you want.
The table shows that arrays are not just useful for producing one line on a chart. They can also support richer analysis such as average, median, min, max, range, and z-score.
This is exactly the kind of thing that starts to make Pine scripts feel more like real analytical tools.
Arrays are especially powerful in strategy development because they let you build logic that goes beyond standard indicators.
For example, you could adapt the same pattern to:
This is the real value of arrays. They let you create custom data structures that match the exact problem you are solving.
This is the most common mistake. Without var, the array does not persist correctly across bars.
Before reading values or looping through the array, make sure it actually contains enough data. Empty or undersized arrays can cause errors or invalid calculations.
If you only need a sorted version for a calculation, copy the array first. Otherwise you mutate the stored data structure itself.
If you want stable logic, use barstate.isconfirmed so the array updates only once the bar has closed.
Arrays are manually managed. They do not behave like close[1], high[2], or built-in historical references. You must control their contents explicitly.
Because you can render examples directly in TradingView, I would use three visuals for this article:
Use a chart where price trends strongly and the percentile line stays mostly above 80 or below 20. This makes the ranking concept visually obvious.
Use a choppy market where the percentile oscillates around the middle. This helps readers understand that the ranking is relative to recent price history, not absolute price direction.
Capture a clean screenshot with the table visible. This reinforces that arrays can support deeper analysis, not just a single plotted output.
If you only remember one thing from this article, remember this:
Arrays are what you use when Pine’s normal series model is not enough.
They let you build rolling datasets, track custom values, sort data, rank conditions, and calculate statistics that are difficult or impossible with series alone. Once you are comfortable with var, array.push(), array.shift(), array.get(), array.size(), array.copy(), and array.sort(), a large part of advanced Pine Script becomes much more accessible.
If you understand the basic rolling queue pattern shown here, the next natural concepts to explore are:
Arrays are one of the foundations of advanced Pine Script development. Once you understand them properly, you can start building custom ranking models, signal history trackers, and more sophisticated strategy logic that goes far beyond standard indicators.
//@version=6
indicator("Arrays: Rolling Statistics + Percentile", overlay=false)
window = input.int(50, "Window Size", minval=5)
showStats = input.bool(true, "Show Stats Table")
getMedian(array<float> src) =>
float result = na
int n = array.size(src)
if n > 0
array<float> tmp = array.copy(src)
array.sort(tmp, order.ascending)
int mid = int(math.floor((n - 1) / 2.0))
result := n % 2 == 1 ? array.get(tmp, mid) : (array.get(tmp, mid) + array.get(tmp, mid + 1)) / 2.0
result
getPercentileRank(array<float> src, float value) =>
float pct = na
int n = array.size(src)
if n > 0 and not na(value)
int below = 0
int equal = 0
for i = 0 to n - 1
float v = array.get(src, i)
if value > v
below += 1
else if value == v
equal += 1
pct := ((below + equal * 0.5) / n) * 100.0
pct
var array<float> closes = array.new<float>(0)
var float percentile = na
if barstate.isconfirmed
percentile := getPercentileRank(closes, close)
array.push(closes, close)
if array.size(closes) > window
array.shift(closes)
samples = array.size(closes)
avgClose = samples > 0 ? array.avg(closes) : na
maxClose = samples > 0 ? array.max(closes) : na
minClose = samples > 0 ? array.min(closes) : na
stdDev = samples > 1 ? array.stdev(closes) : na
median = samples > 0 ? getMedian(closes) : na
rangeVal = samples > 0 ? maxClose - minClose : na
zScore = samples > 1 and stdDev != 0 ? (close - avgClose) / stdDev : na
plot(percentile, "Close Percentile vs Previous Window", color=color.purple, linewidth=2)
hline(80, "High Percentile", color=color.red)
hline(50, "Mid", color=color.gray)
hline(20, "Low Percentile", color=color.green)
var table stats = table.new(position.top_right, 2, 7, border_width=1)
if barstate.islast and showStats
table.cell(stats, 0, 0, "Samples")
table.cell(stats, 1, 0, str.tostring(samples))
table.cell(stats, 0, 1, "Average")
table.cell(stats, 1, 1, str.tostring(avgClose, format.mintick))
table.cell(stats, 0, 2, "Median")
table.cell(stats, 1, 2, str.tostring(median, format.mintick))
table.cell(stats, 0, 3, "Min / Max")
table.cell(stats, 1, 3, str.tostring(minClose, format.mintick) + " / " + str.tostring(maxClose, format.mintick))
table.cell(stats, 0, 4, "Std Dev")
table.cell(stats, 1, 4, str.tostring(stdDev, format.mintick))
table.cell(stats, 0, 5, "Range")
table.cell(stats, 1, 5, str.tostring(rangeVal, format.mintick))
table.cell(stats, 0, 6, "Z-Score")
table.cell(stats, 1, 6, str.tostring(zScore, format.mintick))