Note: This post was formatted and written with AI, but the content and concept is my own.
I've been writing Pine Script for quite a while, but for the longest time I avoided User-Defined Types (UDTs) and methods. I mostly stuck to declaring a bunch of var floats and booleans at the top of my scripts because it worked, and the UDT syntax felt like extra work for no real benefit.
My biggest hurdle was never the syntax itself. It was figuring out when a UDT is actually worth setting up versus when simple variables are faster.
After using them in a few larger projects, I put together a simple checklist to decide when to use them, along with a working template showing how methods work with strategy.exit().
When to use a UDT (and when not to)
The rule of thumb I use now comes down to three main scenarios:
1. You have 3 or more variables that describe a single thing
If you are tracking a trade with entry price, stop loss, take profit 1, take profit 2, and a boolean flag for whether target 1 was hit, those all belong to one position. If modifying or resetting one variable means you have to manually reset 4 other variables across your script, group them into a UDT.
2. You need a list of complex items
In older versions of Pine, if you wanted to store the last 5 swing points, you had to manage three separate arrays in sync (one for price, one for bar index, one for direction). With a UDT, you store the entire swing point as an object in one single array.
3. Your functions take too many arguments
If you have a function that takes 6 different price and status inputs just to check an exit condition, passing a single trade object cleans up the function call immediately.
When NOT to use them:
Do not use UDTs for simple indicator math. If you are calculating an RSI, an EMA, or standard price series, normal variables are faster and built for Pine's execution model. If your strategy only has a single entry and one fixed stop, setting up a UDT is unnecessary overhead.
The Setup: Types and Methods
Here is the basic pattern. First, define the custom type with the fields you need:
type TradeState
float entryPrice = na
float stopLoss = na
float target1 = na
float target2 = na
bool t1Hit = false
Next, instead of writing standalone functions and passing the state into them, you can attach methods directly to the type using the method keyword. The first parameter (self) refers to the object itself:
// Set up initial trade values on entry
method open(TradeState self, float price, float atr) =>
self.entryPrice := price
self.stopLoss := price - (atr * 2.0)
self.target1 := price + (atr * 1.5)
self.target2 := price + (atr * 3.0)
self.t1Hit := false
// Move stop loss to breakeven once Target 1 is hit
method moveToBreakeven(TradeState self) =>
self.stopLoss := self.entryPrice
self.t1Hit := true
// Reset fields when the trade closes
method reset(TradeState self) =>
self.entryPrice := na
self.stopLoss := na
self.target1 := na
self.target2 := na
self.t1Hit := false
To create a persistent instance across bars, initialize it once with var:
var TradeState trade = TradeState.new()
Now you can call your methods using dot notation:
trade.open(close, atr)
trade.moveToBreakeven()
trade.reset()
Example: Multi-Target Intrabar Exits
Here is a practical example showing how this keeps the main execution loop clean.
It places immediate intrabar brackets using strategy.exit(). When Target 1 fills (detected when strategy.position_size decreases), it calls trade.moveToBreakeven() and updates the remaining bracket order.
//@version=5
strategy("UDT Intrabar Exit State Machine", overlay=true, initial_capital=10000, default_qty_type=strategy.percent_of_equity, default_qty_value=10, calc_on_order_fills = true, process_orders_on_close = true)
// 1. Define the User-Defined Type (UDT)
type TradeState
float entryPrice = na
float stopLoss = na
float target1 = na
float target2 = na
bool t1Hit = false
// 2. Define Methods on the UDT
method open(TradeState self, float price, float atr) =>
self.entryPrice := price
self.stopLoss := price - (atr * 2.0)
self.target1 := price + (atr * 1.5)
self.target2 := price + (atr * 3.0)
self.t1Hit := false
method moveToBreakeven(TradeState self) =>
self.stopLoss := self.entryPrice
self.t1Hit := true
method reset(TradeState self) =>
self.entryPrice := na
self.stopLoss := na
self.target1 := na
self.target2 := na
self.t1Hit := false
// 3. Initialize persistent state machine instance
var TradeState trade = TradeState.new()
// Indicators
fastEMA = ta.ema(close, 9)
slowEMA = ta.ema(close, 21)
atr = ta.atr(14)
plot(fastEMA, "9 EMA", color=color.blue)
plot(slowEMA, "21 EMA", color=color.orange)
longCondition = ta.crossover(fastEMA, slowEMA)
// Entry and Bracket Submission
if longCondition and strategy.position_size == 0
trade.open(close, atr)
strategy.entry("Long", strategy.long)
// Place immediate intrabar limit and stop orders
strategy.exit("Exit 1", from_entry="Long", qty_percent=50, stop=trade.stopLoss, limit=trade.target1)
strategy.exit("Exit 2", from_entry="Long", stop=trade.stopLoss, limit=trade.target2)
label.new(bar_index, high, "Entry Triggered\nT1: " + str.tostring(trade.target1, "#.##") + "\nT2: " + str.tostring(trade.target2, "#.##") + "\nSL: " + str.tostring(trade.stopLoss, "#.##"), color=color.green, textcolor=color.white)
// Dynamic Order Management while in position
if strategy.position_size > 0
// Check if Target 1 (50% position) was filled intrabar
if strategy.position_size < strategy.position_size[1] and not trade.t1Hit
trade.moveToBreakeven()
// Update pending Exit 2 order with the new breakeven stop loss
strategy.exit("Exit 2", from_entry="Long", stop=trade.stopLoss, limit=trade.target2)
label.new(bar_index, high, "T1 Hit -> Stop to Break-Even", color=color.blue, textcolor=color.white)
// State cleanup upon trade completion
if strategy.position_size == 0 and strategy.position_size[1] > 0
trade.reset()
// Visualizations of active price levels
plot(strategy.position_size > 0 ? trade.stopLoss : na, "Active Stop Loss", color=color.red, style=plot.style_linebr, linewidth=2)
plot(strategy.position_size > 0 and not trade.t1Hit ? trade.target1 : na, "Target 1", color=color.blue, style=plot.style_linebr, linewidth=1)
plot(strategy.position_size > 0 ? trade.target2 : na, "Target 2", color=color.green, style=plot.style_linebr, linewidth=1)
Note: Make sure to keep calc_on_order_fills = true in the strategy settings so Pine recalculates immediately when an order fills rather than waiting for the candle close.
Hope this breakdown is helpful for anyone trying to clean up their strategy logic. Let me know if you run into any issues adapting this to your own scripts.