Visualizing trading activity directly on the price chart is paramount for analysis and review. While Pine Script’s strategy functions handle execution, displaying visual cues for specific events, such as opening a new position, significantly enhances a strategy’s readability and facilitates debugging.
Understanding the Importance of Visual Position Tracking
When backtesting or running a strategy in real-time, quickly identifying where and when a position was initiated provides valuable context. A visual marker on the chart allows traders to correlate price action, indicator values, and other chart elements with the exact moment of entry. This is crucial for understanding strategy performance and identifying potential areas for improvement.
Brief Overview of Pine Script’s Label Functionality
Pine Script offers built-in functions for drawing textual labels on the chart. The primary function is label.new(), which creates a label. Labels can be customized in terms of text, color, style, size, and position. Importantly, labels persist across bars unless explicitly deleted or managed. For dynamic strategies, managing labels means knowing when to create them (on entry) and when to remove or modify them (on exit).
Detecting New Positions
Accurately detecting the moment a new position is established is the first step in placing an entry label. Simply calling a strategy.entry function does not guarantee a position will open on that exact bar, especially in historical backtesting where execution depends on fill rules and available liquidity (simulated).
Identifying Entry Conditions Using strategy.entry and strategy.order
Your strategy logic will contain conditions that trigger entry orders using strategy.entry or strategy.order. These functions signal the intent to enter a position. While you can use the same conditions to trigger label creation, a more robust method confirms the actual change in position status.
Employing Boolean Variables to Track Position Status
A common pattern is to use boolean variables that become true on the bar where your entry conditions are met. However, relying only on these variables can be misleading if the order doesn’t fill or partially fills. These variables are better used in conjunction with position size checks.
Confirming Position Open with strategy.position_size
The most reliable way to confirm a new position has opened on the current bar is to check the value of strategy.position_size. This built-in variable holds the current number of contracts/shares in the position. A change from zero (or a different size/direction) on the previous bar (strategy.position_size[1]) to a non-zero size on the current bar (strategy.position_size) indicates a new position was established or an existing one was significantly modified in size or reversed.
- Entry Detection:
strategy.position_size != 0 and strategy.position_size[1] == 0 - Long Entry:
strategy.position_size > 0 and strategy.position_size[1] <= 0 - Short Entry:
strategy.position_size < 0 and strategy.position_size[1] >= 0
Using these conditions ensures your label is placed only when the strategy officially registers a position change.
Creating and Displaying Labels for New Positions
Once you’ve detected a new position using the strategy.position_size check, you can create a label using the label.new() function.
Using the label.new() Function to Create Labels
The basic syntax for label.new involves specifying its mandatory arguments:
label.new(
x,
y,
text,
color = #00000000,
style = label.style_none,
textcolor = #000000,
size = size.normal,
tooltip = "",
xloc = xloc.bar_index,
yloc = yloc.price
)
For entry labels, x will typically be the bar_index of the entry bar, and y will be related to the entry price or the bar’s price range.
Setting Label Properties: Text, Color, Style, and Location
- Text: Use the
textargument. You can combine strings and numbers (converted withtostring()) to display dynamic information like entry price, date, or position size. - Color: Set the
colorargument. Usecolor.greenfor long entries andcolor.redfor short entries for clarity. - Style: Choose a
label.style_....label.style_label_downorlabel.style_label_upcan point to the entry bar. - Location:
xlocdetermines the x-coordinate reference (xloc.bar_indexis standard for specific bars).ylocdetermines the y-coordinate reference (yloc.priceuses the price scale,yloc.percent_from_bottom/_topuse the chart pane height). For entries,yloc.pricecombined with the entry price or a price offset is most intuitive.
Dynamically Updating Label Text with Position Information
You can construct informative label text using string concatenation. Access variables like close, open, high, low, bar_index, time, strategy.position_size, and potentially the entry price if you capture it.
string entryText = "Entry\nPrice: " + str.tostring(close, format.price) + "\nSize: " + str.tostring(math.abs(strategy.position_size))
(Note: Getting the exact filled entry price in Pine Script can be complex, often involving tracking fills via strategy.closedtrades or capturing close price on the signal bar as an approximation).
Positioning Labels Relative to Entry Price and Time
Use x = bar_index and y = price_level where price_level is the price you want the label aligned with (e.g., high + indicator_offset for longs, low - indicator_offset for shorts, or close as an approximation of the entry price). Use xloc.bar_index and yloc.price.
Advanced Label Management Techniques
Managing labels effectively is key to keeping your chart clean and informative, especially during backtesting with numerous trades.
Conditional Label Display Based on Chart Scale and Zoom Level
While label.new itself doesn’t directly support zoom-based visibility, you can use variables that track the number of bars on the chart or calculate bar spacing to decide whether to draw labels on every entry or only on significant ones if the chart is zoomed out significantly. This is less common for basic entry labels but useful for reducing clutter on dense charts.
Deleting or Modifying Labels When Positions are Closed
Failing to delete old labels leads to severe chart clutter. When a position is closed (detected by strategy.position_size changing to 0 from a non-zero value on the previous bar), you should delete the corresponding entry label. This requires storing the label ID when it’s created.
var label entryLabel = na
// Assuming long entry detection logic
if strategy.position_size > 0 and strategy.position_size[1] <= 0
// Delete previous potential label if one existed (e.g., from reversal)
if not na(entryLabel)
label.delete(entryLabel)
// Create new entry label and store its ID
entryLabel := label.new(bar_index, low - syminfo.mintick * 5, text="Long Entry", color=color.green, style=label.style_label_up)
// Assuming exit detection logic (position closed)
if strategy.position_size == 0 and strategy.position_size[1] != 0
// Delete the entry label when position is closed
if not na(entryLabel[1]) // Check label from previous bar where position was open
label.delete(entryLabel[1])
entryLabel := na // Reset label variable
Note the use of var label entryLabel = na to maintain the label ID across bars and accessing entryLabel[1] to refer to the label created on the previous bar where the position was active.
Optimizing Label Placement to Avoid Overlap
For simple entry/exit labels, placing them slightly offset from the high/low of the entry bar usually works. If you have multiple trades on the same bar or very close together, labels might overlap. More advanced techniques involve checking the coordinates of existing labels before placing a new one or dynamically adjusting the offset. However, for typical entry point visualization, a fixed small offset is often sufficient.
Using Arrays to Manage Multiple Labels (Optional)
If your strategy can have multiple independent positions open simultaneously (e.g., pyramiding, different symbols), a single var label variable is insufficient. You would need to use label arrays (array.new_label(), array.push(), array.pop(), array.get(), array.size()) to store and manage the IDs of multiple active labels. This adds complexity and is typically only necessary for advanced multi-position strategies.
Practical Examples and Code Snippets
Here are a few examples demonstrating how to implement entry labels.
Example 1: Simple Label for Long Position Entry
This basic example shows how to place a green ‘Entry’ label below the bar low when a long position is opened.
//@version=5
strategy("Simple Entry Label Example", overlay=true)
// Entry Condition Placeholder
longCondition = ta.crossover(ta.sma(close, 15), ta.sma(close, 50))
// Submit entry order
if longCondition
strategy.entry("MyLongEntry", strategy.long)
// Variable to hold the label ID across bars
var label entryLabel = na
// Check for new long position opening on this bar
bool newLongPosition = strategy.position_size > 0 and strategy.position_size[1] <= 0
// If a new long position opened, create a label
if newLongPosition
// Delete any old label first if managing only one
if not na(entryLabel)
label.delete(entryLabel)
// Create a new label
entryLabel := label.new(
x = bar_index,
y = low - syminfo.mintick * 5, // Place label slightly below the low
text = "Long Entry",
color = color.green,
style = label.style_label_up, // Arrow pointing up
textcolor = color.white,
size = size.small,
xloc = xloc.bar_index,
yloc = yloc.price
)
// Check for position closing (becomes flat or reverses)
bool positionClosed = strategy.position_size == 0 and strategy.position_size[1] != 0
bool positionReversed = (strategy.position_size > 0 and strategy.position_size[1] < 0) or (strategy.position_size < 0 and strategy.position_size[1] > 0)
// If position closed or reversed, delete the label from the previous bar
if (positionClosed or positionReversed) and not na(entryLabel[1])
label.delete(entryLabel[1])
entryLabel := na // Reset label variable for next entry
// Note: This example assumes only one position type (long) or one position at a time.
// For long/short or multiple positions, more complex label management is needed.
Example 2: Label with Dynamic Price and Time Information
This example enhances the label text to include the entry bar’s closing price and time.
//@version=5
strategy("Dynamic Entry Label Example", overlay=true)
// Entry Condition Placeholder
longCondition = ta.crossover(ta.sma(close, 15), ta.sma(close, 50))
shortCondition = ta.crossunder(ta.sma(close, 15), ta.sma(close, 50))
// Submit entry orders
if longCondition
strategy.entry("MyLongEntry", strategy.long)
if shortCondition
strategy.entry("MyShortEntry", strategy.short)
// Variables to hold label IDs
var label longEntryLabel = na
var label shortEntryLabel = na
// Check for new position openings
bool newLongPosition = strategy.position_size > 0 and strategy.position_size[1] <= 0
bool newShortPosition = strategy.position_size < 0 and strategy.position_size[1] >= 0
// If a new long position opened, create/update label
if newLongPosition
// Delete old label if it exists
if not na(longEntryLabel)
label.delete(longEntryLabel)
if not na(shortEntryLabel)
label.delete(shortEntryLabel) // Delete opposing label on reversal
// Create dynamic text
string entryTimeStr = time("dd MMM yy HH:mm") // Format time as desired
string entryPriceStr = str.tostring(close, format.price) // Use bar close as price approx.
string entryText = "Long Entry\n" + entryTimeStr + "\n@" + entryPriceStr
// Create new label
longEntryLabel := label.new(
x = bar_index,
y = low - syminfo.mintick * 10, // Place label below the low
text = entryText,
color = color.green,
style = label.style_label_up,
textcolor = color.white,
size = size.small
)
shortEntryLabel := na // Ensure short label var is reset
// If a new short position opened, create/update label
if newShortPosition
// Delete old label if it exists
if not na(shortEntryLabel)
label.delete(shortEntryLabel)
if not na(longEntryLabel)
label.delete(longEntryLabel) // Delete opposing label on reversal
// Create dynamic text
string entryTimeStr = time("dd MMM yy HH:mm")
string entryPriceStr = str.tostring(close, format.price)
string entryText = "Short Entry\n" + entryTimeStr + "\n@" + entryPriceStr
// Create new label
shortEntryLabel := label.new(
x = bar_index,
y = high + syminfo.mintick * 10, // Place label above the high
text = entryText,
color = color.red,
style = label.style_label_down,
textcolor = color.white,
size = size.small
)
longEntryLabel := na // Ensure long label var is reset
// Check for position closing (becomes flat)
bool positionFlat = strategy.position_size == 0 and strategy.position_size[1] != 0
// If position became flat, delete the appropriate label from the previous bar
if positionFlat
if strategy.position_size[1] > 0 and not na(longEntryLabel[1])
label.delete(longEntryLabel[1])
if strategy.position_size[1] < 0 and not na(shortEntryLabel[1])
label.delete(shortEntryLabel[1])
// Reset vars if needed (already handled by setting to na on reversal/entry)
// Note: This handles long, short, and becoming flat. Still assumes only one position at a time.
Example 3: Label Displaying Position Size and Risk
This example expands the dynamic text to show the position size and a simplified notion of risk (e.g., stop loss level if set).
//@version=5
strategy("Size/Risk Label Example", overlay=true, pyramiding=1) // Allow pyramiding for size example
// Entry Conditions and Stop Loss (Example Placeholder)
longCondition = ta.crossover(ta.sma(close, 15), ta.sma(close, 50))
shortCondition = ta.crossunder(ta.sma(close, 15), ta.sma(close, 50))
longStopLoss = low * 0.98 // Example SL 2% below low
shortStopLoss = high * 1.02 // Example SL 2% above high
// Submit entry orders with stops
if longCondition
strategy.entry("MyLongEntry", strategy.long, qty=100)
strategy.exit("ExitLong", from_entry="MyLongEntry", stop=longStopLoss)
if shortCondition
strategy.entry("MyShortEntry", strategy.short, qty=100)
strategy.exit("ExitShort", from_entry="MyShortEntry", stop=shortStopLoss)
// Variables to hold label IDs
var label longEntryLabel = na
var label shortEntryLabel = na
// Check for new position openings
bool newLongPosition = strategy.position_size > 0 and strategy.position_size[1] <= 0
bool newShortPosition = strategy.position_size < 0 and strategy.position_size[1] >= 0
bool positionChanged = newLongPosition or newShortPosition
// Get last entry price and current stop loss price (simplistic)
// In reality, you'd need to store entry price/stop loss when position opens.
// For this example, we'll use the current bar's close/calculated stop as approximation.
float currentEntryPrice = close
float currentStopLossPrice = strategy.position_size > 0 ? longStopLoss : (strategy.position_size < 0 ? shortStopLoss : na)
// If position status changed, manage labels
if positionChanged
// Delete old labels
if not na(longEntryLabel)
label.delete(longEntryLabel)
if not na(shortEntryLabel)
label.delete(shortEntryLabel)
// Create dynamic text
string posType = strategy.position_size > 0 ? "Long" : "Short"
string posSize = str.tostring(math.abs(strategy.position_size))
string entryPriceStr = str.tostring(currentEntryPrice, format.price)
string stopLossPriceStr = not na(currentStopLossPrice) ? str.tostring(currentStopLossPrice, format.price) : "N/A"
string entryText = posType + " Entry\nSize: " + posSize + "\n@" + entryPriceStr + "\nSL: " + stopLossPriceStr
// Create new label based on position direction
if strategy.position_size > 0 // Long
longEntryLabel := label.new(
x = bar_index,
y = low - syminfo.mintick * 10,
text = entryText,
color = color.green,
style = label.style_label_up,
textcolor = color.white,
size = size.small
)
shortEntryLabel := na
else if strategy.position_size < 0 // Short
shortEntryLabel := label.new(
x = bar_index,
y = high + syminfo.mintick * 10,
text = entryText,
color = color.red,
style = label.style_label_down,
textcolor = color.white,
size = size.small
)
longEntryLabel := na
// Check for position closing (becomes flat)
bool positionFlat = strategy.position_size == 0 and strategy.position_size[1] != 0
// If position became flat, delete the appropriate label from the previous bar
if positionFlat
if strategy.position_size[1] > 0 and not na(longEntryLabel[1])
label.delete(longEntryLabel[1])
if strategy.position_size[1] < 0 and not na(shortEntryLabel[1])
label.delete(shortEntryLabel[1])
longEntryLabel := na
shortEntryLabel := na
Common Pitfalls and Troubleshooting
- Labels based on Order, not Position: A common mistake is placing labels immediately after calling
strategy.entry. This doesn’t guarantee a fill. Always confirm the position status change usingstrategy.position_sizeto trigger label creation. - Label Clutter: Forgetting to delete old labels is the quickest way to make your chart unusable during backtesting. Implement deletion logic when positions close or reverse.
- Incorrect Bar Index/Price: Ensure labels are placed at the correct
bar_index(usually the current one) and a relevantyprice level related to the entry bar, usingxloc.bar_indexandyloc.price. - Label Variable Scope: Use the
varkeyword for your label ID variables (var label myLabel = na) so their state persists across bars, allowing you to reference or delete them later.
By following these guidelines and utilizing the strategy.position_size variable for precise detection, you can effectively display informative labels for new positions, greatly enhancing the visual clarity and analysis of your Pine Script strategies.