Starting with Pine v4, indicators and strategies can create drawing objects on the chart. Three types of drawings are currently supported: label, line, and boxes. You will find one instance of each on the following chart:
Note
On TradingView charts, a complete set of Drawing Tools allows users to create and modify drawings using mouse actions. While they may look similar to drawing objects created with Pine code, they are essentially different entities. Drawing objects created using Pine code cannot be modified with mouse actions.
The line, label, and box drawings in Pine v4 allow you to create indicators with more sophisticated visual components, e.g., pivot points, support/resistance levels, zig zag lines, labels containing dynamic text, etc.
In contrast to indicator plots (plots are created with functions plot
, plotshape
, plotchar
),
drawing objects can be created on historical bars as well as in the future, where no bars exist yet.
Pine drawing objects are created with the label.new , line.new and box.new functions. While each function has many parameters, only the coordinates are mandatory. This is an example of code used to create a label on every bar:
//@version=4
study("My Script", overlay=true)
label.new(bar_index, high)
The label is created with the parameters x=bar_index
(the index of the current bar,
bar_index) and y=high
(high price of the current bar).
When a new bar opens, a new label is created on it. Label objects created on previous bars stay on the chart
until the indicator deletes them with an explicit call of the label.delete
function, or until the automatic garbage collection process removes them.
Here is a modified version of the same script that shows the values of the x
and y
coordinates used to create the labels:
//@version=4
study("My Script", overlay=true)
label.new(bar_index, high, style=label.style_none,
text="x=" + tostring(bar_index) + "\ny=" + tostring(high))
In this example labels are shown without background coloring (because of parameter style=label.style_none
) but with
dynamically created text (text="x=" + tostring(bar_index) + "\ny=" + tostring(high)
) that prints label coordinates.
This is an example of code that creates line objects on a chart:
//@version=4
study("My Script", overlay=true)
line.new(x1=bar_index[1], y1=low[1], x2=bar_index, y2=high)
This is an example of code that creates box objects on a chart:
//@version=4
study("My Script", overlay=true)
box.new(left=bar_index[1], top=low[1], right=bar_index, bottom=high)
Drawing objects are subject to both commit and rollback actions, which affect the behavior of a script when it executes in the realtime bar, Execution model.
This script demonstrates the effect of rollback when running in the realtime bar:
//@version=4
study("My Script", overlay=true)
label.new(bar_index, high)
While label.new
creates a new label on every iteration of the script when price changes in the realtime bar,
the most recent label created in the script’s previous iteration is also automatically deleted because of rollback before the next iteration. Only the last label created before the realtime bar’s close will be committed, and will thus persist.
Drawing objects are positioned on the chart according to x and y coordinates using a combination of 4 parameters: x
, y
, xloc
and yloc
. The value of xloc
determines whether x
will hold a bar index or time value. When yloc=yloc.price
, y
holds a price. y
is ignored when yloc
is set to yloc.abovebar or yloc.belowbar.
If a drawing object uses xloc.bar_index, then
the x-coordinate is treated as an absolute bar index. The bar index of the current bar can be obtained from the built-in variable bar_index
. The bar index of previous bars is bar_index[1]
, bar_index[2]
and so on. xloc.bar_index
is the default value for x-location parameters of both label and line drawings.
If a drawing object uses xloc.bar_time, then
the x-coordinate is treated as a UNIX time in milliseconds. The start time of the current bar can be obtained from the built-in variable time
.
The bar time of previous bars is time[1]
, time[2]
and so on. Time can also be set to an absolute time point with the
timestamp function.
The xloc.bar_time
and xloc.bar_index
modes makes it possible to place a drawing object in the future, to the right of the current bar. For example:
//@version=4
study("My Script", overlay=true)
dt = time - time[1]
if barstate.islast
label.new(time + 3*dt, close, xloc=xloc.bar_time)
This code places a label object in the future. X-location logic works identically for label, line, and box drawings.
Example for xloc.bar_index
:
//@version=4
study("My Script", overlay=true)
label.new(bar_index+100, high)
In contrast, y-location logic is different for label and line or box drawings. Pine’s line and box drawings always use yloc.price, so their y-coordinate is always treated as an absolute price value.
Label drawings have additional y-location values: yloc.abovebar and
yloc.belowbar.
When they are used, the value of the y
parameter is ignored and the drawing object is placed above or below the bar.
A drawing object can be modified after its creation. The label.new
, line.new
, and box.new
functions return
a reference to the created drawing object (of type series label, series line and series box respectively).
This reference can then be used as the first argument to the label.set_*
, line.set_*
, or box.set_*
functions used to modify drawings.
For example:
//@version=4
study("My Script", overlay=true)
l = label.new(bar_index, na)
if close >= open
label.set_text(l, "green")
label.set_color(l, color.green)
label.set_yloc(l, yloc.belowbar)
label.set_style(l, label.style_label_up)
else
label.set_text(l, "red")
label.set_color(l, color.red)
label.set_yloc(l, yloc.abovebar)
label.set_style(l, label.style_label_down)
This simple script first creates a label on the current bar and then it writes a reference to it in a variable l
.
Then, depending on whether the current bar is rising or falling (condition close >= open
), a number of label drawing properties are modified:
text, color, y coordinate location (yloc
) and label style.
One may notice that na
is passed as the y
argument to the label.new
function call. The reason for this is that
the example’s label uses either yloc.belowbar
or yloc.abovebar
y-locations, which don’t require a y value.
A finite value for y
is needed only if a label uses yloc.price
.
The available setter functions for label drawings are:
- label.set_color — changes color of label
- label.set_size — changes size of label
- label.set_style — changes style of label
- label.set_text — changes text of label
- label.set_textcolor — changes color of text
- label.set_x — changes x-coordinate of label
- label.set_y — changes y-coordinate of label
- label.set_xy — changes both x and y coordinates of label
- label.set_xloc — changes x-location of label
- label.set_yloc — changes y-location of label
- label.set_tooltip — changes tooltip of label
The available setter functions for line drawings are:
- line.set_color — changes color of line
- line.set_extend — changes attribute that makes:
extend.none
- a line segmentextend.left
/extend.right
- a rayextend.both
- an endless line- line.set_style — changes style of line
- line.set_width — changes width of line
- line.set_xloc — changes x-location of line (both x1 and x2)
- line.set_x1 — changes x1-coordinate of line
- line.set_y1 — changes y1-coordinate of line
- line.set_xy1 — changes both x1 and y1 coordinates of line
- line.set_x2 — changes x2-coordinate of line
- line.set_y2 — changes y2-coordinate of line
- line.set_xy2 — changes both x2 and y2 coordinates of line at once
The available setter functions for box drawings are:
- box.set_border_color — changes border color of the box
- box.set_bgcolor — changes background color of the box
- box.set_extend — changes attribute that makes:
extend.none
- the horizontal borders start at the left border and end at the right borderextend.left
/extend.right
- the horizontal borders are extended indefinitely to the left/right of the boxextend.both
- the horizontal borders are extended on both sides- box.set_border_style — changes border style of the box
- box.set_border_width — changes border width of the box
- box.set_bottom — changes bottom coordinate of the box
- box.set_right — changes right coordinate of the box
- box.set_rightbottom — changes both right and bottom coordinates of the box at once
- box.set_top — changes top coordinate of the box
- box.set_left — changes left coordinate of the box
- box.set_lefttop — changes both left and top coordinates of the box at once
Various styles can be applied to labels with either the label.new or label.set_style function:
Label style name | Label | Label with text |
---|---|---|
label.style_none |
||
label.style_xcross |
||
label.style_cross |
||
label.style_triangleup |
||
label.style_triangledown |
||
label.style_flag |
||
label.style_circle |
||
label.style_arrowup |
||
label.style_arrowdown |
||
label.style_label_up |
||
label.style_label_down |
||
label.style_square |
||
label.style_diamond |
Various styles can be applied to lines with either the line.new, box.new, line.set_style or box.set_border_style function:
Line style name | Line | Box |
---|---|---|
line.style_solid |
||
line.style_dotted |
||
line.style_dashed |
||
line.style_arrow_left |
Not supported | |
line.style_arrow_right |
Not supported | |
line.style_arrow_both |
Not supported |
The label.delete, line.delete and box.delete functions delete label, line, or box drawing objects from the chart.
Here is Pine code that keeps just one label drawing object on the current bar, deleting the old ones:
//@version=4
study("Last Bar Close 1", overlay=true)
c = close >= open ? color.lime : color.red
l = label.new(bar_index, na,
text=tostring(close), color=c,
style=label.style_label_down, yloc=yloc.abovebar)
label.delete(l[1])
On every new bar update of the “Last Bar Close 1” study, a new label object is created and written to variable l
.
Variable l
is of type series label, so the []
operator is used to get the previous bar’s label object.
That previous label is then passed to the label.delete
function to delete it.
Functions label.delete
and line.delete
do nothing if the na
value is used as an id, which makes code like the following unnecessary:
if not na(l[1])
label.delete(l[1])
The previous script’s behavior can be reproduced using another approach:
//@version=4
study("Last Bar Close 2", overlay=true)
var label l = na
label.delete(l)
c = close >= open ? color.lime : color.red
l := label.new(bar_index, na,
text=tostring(close), color=c,
style=label.style_label_down, yloc=yloc.abovebar)
When the study “Last Bar Close 2” gets a new bar update, variable l
is still referencing the old label object created on the previous bar. This label is deleted with the label.delete(l)
call. A new label is then created and its id saved to l
. Using this approach there is no need to use the []
operator.
Note the use of the new Pine v4 var keyword. It creates variable l
and initializes it with the na
value only once. label.delete(l)
would have no object to delete if it weren’t for the fact that l
is initialized only once.
There is yet another way to achieve the same objective as in the two previous scripts, this time by modifying the label rather than deleting it:
//@version=4
study("Last Bar Close 3", overlay=true)
var label l = label.new(bar_index, na,
style=label.style_label_down, yloc=yloc.abovebar)
c = close >= open ? color.lime : color.red
label.set_color(l, c)
label.set_text(l, tostring(close))
label.set_x(l, bar_index)
Once again, the use of new var keyword is essential. It is what allows the label.new
call to be
executed only once, on the very first historical bar.
//@version=4
study("Pivot Points Standard", overlay=true)
higherTF = input("D", type=input.resolution)
prevCloseHTF = security(syminfo.tickerid, higherTF, close[1], lookahead=true)
prevOpenHTF = security(syminfo.tickerid, higherTF, open[1], lookahead=true)
prevHighHTF = security(syminfo.tickerid, higherTF, high[1], lookahead=true)
prevLowHTF = security(syminfo.tickerid, higherTF, low[1], lookahead=true)
pLevel = (prevHighHTF + prevLowHTF + prevCloseHTF) / 3
r1Level = pLevel * 2 - prevLowHTF
s1Level = pLevel * 2 - prevHighHTF
var line r1Line = na
var line pLine = na
var line s1Line = na
if pLevel[1] != pLevel
line.set_x2(r1Line, bar_index)
line.set_x2(pLine, bar_index)
line.set_x2(s1Line, bar_index)
line.set_extend(r1Line, extend.none)
line.set_extend(pLine, extend.none)
line.set_extend(s1Line, extend.none)
r1Line := line.new(bar_index, r1Level, bar_index, r1Level, extend=extend.right)
pLine := line.new(bar_index, pLevel, bar_index, pLevel, width=3, extend=extend.right)
s1Line := line.new(bar_index, s1Level, bar_index, s1Level, extend=extend.right)
label.new(bar_index, r1Level, "R1", style=label.style_none)
label.new(bar_index, pLevel, "P", style=label.style_none)
label.new(bar_index, s1Level, "S1", style=label.style_none)
if not na(pLine) and line.get_x2(pLine) != bar_index
line.set_x2(r1Line, bar_index)
line.set_x2(pLine, bar_index)
line.set_x2(s1Line, bar_index)
//@version=4
study("Pivot Points High Low", shorttitle="Pivots HL", overlay=true)
lenH = input(title="Length High", type=input.integer, defval=10, minval=1)
lenL = input(title="Length Low", type=input.integer, defval=10, minval=1)
fun(src, len, isHigh, _style, _yloc, _color) =>
p = nz(src[len])
isFound = true
for i = 0 to len - 1
if isHigh and src[i] > p
isFound := false
if not isHigh and src[i] < p
isFound := false
for i = len + 1 to 2 * len
if isHigh and src[i] >= p
isFound := false
if not isHigh and src[i] <= p
isFound := false
if isFound
label.new(bar_index[len], p, tostring(p), style=_style, yloc=_yloc, color=_color)
fun(high, lenH, true, label.style_label_down, yloc.abovebar, color.lime)
fun(low, lenL, false, label.style_label_up, yloc.belowbar, color.red)
//@version=4
study("Linear Regression", shorttitle="LinReg", overlay=true)
upperMult = input(title="Upper Deviation", defval=2)
lowerMult = input(title="Lower Deviation", defval=-2)
useUpperDev = input(title="Use Upper Deviation", defval=true)
useLowerDev = input(title="Use Lower Deviation", defval=true)
showPearson = input(title="Show Pearson's R", defval=true)
extendLines = input(title="Extend Lines", defval=false)
len = input(title="Count", defval=100)
src = input(title="Source", defval=close)
extend = extendLines ? extend.right : extend.none
calcSlope(src, len) =>
if not barstate.islast or len <= 1
[float(na), float(na), float(na)]
else
sumX = 0.0
sumY = 0.0
sumXSqr = 0.0
sumXY = 0.0
for i = 0 to len - 1
val = src[i]
per = i + 1.0
sumX := sumX + per
sumY := sumY + val
sumXSqr := sumXSqr + per * per
sumXY := sumXY + val * per
slope = (len * sumXY - sumX * sumY) / (len * sumXSqr - sumX * sumX)
average = sumY / len
intercept = average - slope * sumX / len + slope
[slope, average, intercept]
[s, a, i] = calcSlope(src, len)
startPrice = i + s * (len - 1)
endPrice = i
var line baseLine = na
if na(baseLine) and not na(startPrice)
baseLine := line.new(bar_index - len + 1, startPrice, bar_index, endPrice, width=1, extend=extend, color=color.red)
else
line.set_xy1(baseLine, bar_index - len + 1, startPrice)
line.set_xy2(baseLine, bar_index, endPrice)
na
calcDev(src, len, slope, average, intercept) =>
upDev = 0.0
dnDev = 0.0
stdDevAcc = 0.0
dsxx = 0.0
dsyy = 0.0
dsxy = 0.0
periods = len - 1
daY = intercept + (slope * periods) / 2
val = intercept
for i = 0 to periods
price = high[i] - val
if (price > upDev)
upDev := price
price := val - low[i]
if (price > dnDev)
dnDev := price
price := src[i]
dxt = price - average
dyt = val - daY
price := price - val
stdDevAcc := stdDevAcc + price * price
dsxx := dsxx + dxt * dxt
dsyy := dsyy + dyt * dyt
dsxy := dsxy + dxt * dyt
val := val + slope
stdDev = sqrt(stdDevAcc / (periods == 0 ? 1 : periods))
pearsonR = dsxx == 0 or dsyy == 0 ? 0 : dsxy / sqrt(dsxx * dsyy)
[stdDev, pearsonR, upDev, dnDev]
[stdDev, pearsonR, upDev, dnDev] = calcDev(src, len, s, a, i)
upperStartPrice = startPrice + (useUpperDev ? upperMult * stdDev : upDev)
upperEndPrice = endPrice + (useUpperDev ? upperMult * stdDev : upDev)
var line upper = na
lowerStartPrice = startPrice + (useLowerDev ? lowerMult * stdDev : -dnDev)
lowerEndPrice = endPrice + (useLowerDev ? lowerMult * stdDev : -dnDev)
var line lower = na
if na(upper) and not na(upperStartPrice)
upper := line.new(bar_index - len + 1, upperStartPrice, bar_index, upperEndPrice, width=1, extend=extend, color=#0000ff)
else
line.set_xy1(upper, bar_index - len + 1, upperStartPrice)
line.set_xy2(upper, bar_index, upperEndPrice)
na
if na(lower) and not na(lowerStartPrice)
lower := line.new(bar_index - len + 1, lowerStartPrice, bar_index, lowerEndPrice, width=1, extend=extend, color=#0000ff)
else
line.set_xy1(lower, bar_index - len + 1, lowerStartPrice)
line.set_xy2(lower, bar_index, lowerEndPrice)
na
// Pearson's R
var label r = na
transparent = color.new(color.white, 100)
label.delete(r[1])
if showPearson and not na(pearsonR)
r := label.new(bar_index - len + 1, lowerStartPrice, tostring(pearsonR, "#.################"), color=transparent, textcolor=#0000ff, size=size.normal, style=label.style_label_up)
//@version=4
study("Zig Zag", overlay=true)
dev_threshold = input(title="Deviation (%)", type=input.float, defval=5, minval=1, maxval=100)
depth = input(title="Depth", type=input.integer, defval=10, minval=1)
pivots(src, length, isHigh) =>
p = nz(src[length])
if length == 0
[bar_index, p]
else
isFound = true
for i = 0 to length - 1
if isHigh and src[i] > p
isFound := false
if not isHigh and src[i] < p
isFound := false
for i = length + 1 to 2 * length
if isHigh and src[i] >= p
isFound := false
if not isHigh and src[i] <= p
isFound := false
if isFound and length * 2 <= bar_index
[bar_index[length], p]
else
[int(na), float(na)]
[iH, pH] = pivots(high, floor(depth / 2), true)
[iL, pL] = pivots(low, floor(depth / 2), false)
calc_dev(base_price, price) =>
100 * (price - base_price) / base_price
var line lineLast = na
var int iLast = 0
var float pLast = 0
var bool isHighLast = true // otherwise the last pivot is a low pivot
var int linesCount = 0
pivotFound(dev, isHigh, index, price) =>
if isHighLast == isHigh and not na(lineLast)
// same direction
if isHighLast ? price > pLast : price < pLast
if linesCount <= 1
line.set_xy1(lineLast, index, price)
line.set_xy2(lineLast, index, price)
[lineLast, isHighLast, false]
else
[line(na), bool(na), false]
else // reverse the direction (or create the very first line)
if na(lineLast)
id = line.new(index, price, index, price, color=color.red, width=2)
[id, isHigh, true]
else
// price move is significant
if abs(dev) >= dev_threshold
id = line.new(iLast, pLast, index, price, color=color.red, width=2)
[id, isHigh, true]
else
[line(na), bool(na), false]
if not na(iH) and not na(iL) and iH == iL
dev1 = calc_dev(pLast, pH)
[id2, isHigh2, isNew2] = pivotFound(dev1, true, iH, pH)
if isNew2
linesCount := linesCount + 1
if not na(id2)
lineLast := id2
isHighLast := isHigh2
iLast := iH
pLast := pH
dev2 = calc_dev(pLast, pL)
[id1, isHigh1, isNew1] = pivotFound(dev2, false, iL, pL)
if isNew1
linesCount := linesCount + 1
if not na(id1)
lineLast := id1
isHighLast := isHigh1
iLast := iL
pLast := pL
else
if not na(iH)
dev1 = calc_dev(pLast, pH)
[id, isHigh, isNew] = pivotFound(dev1, true, iH, pH)
if isNew
linesCount := linesCount + 1
if not na(id)
lineLast := id
isHighLast := isHigh
iLast := iH
pLast := pH
else
if not na(iL)
dev2 = calc_dev(pLast, pL)
[id, isHigh, isNew] = pivotFound(dev2, false, iL, pL)
if isNew
linesCount := linesCount + 1
if not na(id)
lineLast := id
isHighLast := isHigh
iLast := iL
pLast := pL
Drawing objects consume server resources, which is why there is a limit to the total number of drawings per study or strategy. When too many drawings are created, old ones are automatically deleted by the Pine runtime, in a process referred to as garbage collection.
This code creates a drawing on every bar:
//@version=4
study("My Script", overlay=true)
label.new(bar_index, high)
Scrolling the chart left, one will see there are no drawings after approximately 50 bars:
You can change the drawing limit to a value in range from 1 to 500 using the max_lines_count, max_labels_count, or max_boxes_count parameters for the study and strategy functions:
//@version=4
study("My Script", overlay=true, max_labels_count=100)
label.new(bar_index, high)
Objects positioned using xloc.bar_index cannot be drawn further than 500 bars into the future.
Pine code sometimes uses additional symbols and/or timeframes with the security function. Drawing functions can only be used in the main symbol’s context.
Use of barstate.isrealtime
in combination with drawings may sometimes produce unexpected results.
This code’s intention, for example, is to ignore all historical bars and create a label drawing on the realtime bar:
//@version=4
study("My Script", overlay=true)
if barstate.isrealtime
label.new(bar_index[10], na, text="Label", yloc=yloc.abovebar)
It will, however, fail at runtime. The reason for the error is that Pine cannot determine the buffer size
for historical values of the time
plot, even though the time
built-in variable isn’t mentioned in the code.
This is due to the fact that the built-in variable bar_index
uses the time
series in its inner workings.
Accessing the value of the bar index 10 bars back requires that the history buffer size of the time
series
be of size 10 or more.
In Pine, there is a mechanism that automaticaly detects the required historical buffer size for most cases.
Autodetection works by letting Pine code access historical values any number of bars back for a limited duration.
In this script’s case, the if barstate.isrealtime
condition prevents any such accesses to occur,
so the required historical buffer size cannot be inferred and the code fails.
The solution to this conundrum is to use the max_bars_back function to explicitly set the historical buffer size for the time
series:
//@version=4
study("My Script", overlay=true)
max_bars_back(time, 10)
if barstate.isrealtime
label.new(bar_index[10], na, text="Label", yloc=yloc.abovebar)
Such occurrences are confusing, but rare. In time, the Pine team hopes to eliminate them.