Pine Script offers robust capabilities for automating trading strategies, from simple moving average crossovers to complex multi-factor systems. A common requirement for many strategies is the ability to manage or close positions precisely at the end of the trading session or at the close of a specific bar. This article delves into how to effectively process orders on close using Pine Script, focusing on practical implementation and best practices for intermediate to senior developers.
Introduction to Processing Orders on Close in Pine Script
Processing orders ‘on close’ refers to the execution of a trading order (typically a market order) at the closing price of the current trading period (bar).
Understanding the Concept of ‘Orders on Close’ (Market Close Orders)
In traditional trading, a Market Close order is designed to execute as close as possible to the closing price of a security’s trading session. In the context of Pine Script’s backtesting and execution model, triggering an order on the close of the current bar means that if your strategy’s condition for placing an order becomes true during the processing of the current bar, the order will be simulated to fill at that bar’s closing price.
This is distinct from orders triggered during the bar (intrabar) which would fill at the price at the moment the condition was met (if process_orders_on_close=false or not specified, and using strategy.entry or strategy.exit with when condition that evaluates true intrabar), or limit/stop orders which wait for a specific price level.
Why Process Orders on Close? Benefits and Use Cases
Processing orders on close offers several strategic advantages:
- Session Management: Automatically close all positions at the end of the trading day or week to avoid overnight/weekend risk or comply with specific trading rules.
- Simplifying Logic: For strategies that make decisions based on the final outcome of a bar (e.g., using the closing price of a pattern or indicator), executing on the close naturally aligns the order timing with the signal.
- Reducing Intrabar Noise: By waiting for the bar to close, you filter out potential whipsaws or false signals that might occur during the bar’s formation.
- Backtesting Accuracy: Simulating fills precisely at the closing price provides a clear and consistent model, particularly useful for strategies heavily reliant on end-of-day data or bar patterns.
Use cases include end-of-day strategies, session-based trading, and strategies that use indicators calculated solely on closing prices.
Overview of Pine Script Functions for Order Placement and Management
Pine Script provides several built-in functions for managing trades. Those most relevant to processing orders on close are:
strategy.entry(): Used to initiate a new position (long or short).strategy.exit(): Used to exit a specific entry, typically with stop-loss or take-profit levels, or a simplewhencondition.strategy.close(): Specifically designed to close an existing position initiated by a givenstrategy.entrycall, or the entire open position if noidis specified.strategy.close_all(): Closes all currently open positions, regardless of how they were entered.strategy.cancel(): Cancels pending orders (limit, stop) previously submitted bystrategy.entryorstrategy.exit.
While strategy.entry and strategy.exit can execute on the close if their when condition becomes true on the last history bar or if process_orders_on_close=true (which is the default behavior for strategies submitted via the strategy() declaration), strategy.close and strategy.close_all are explicitly designed with closing existing positions in mind and are prime candidates for end-of-bar position management.
Implementing Orders on Close Using strategy.close Function
The strategy.close() function is the most direct way to close an existing position or part of it based on a condition met on the current bar, resulting in an execution at that bar’s close price.
Basic Syntax and Usage of strategy.close
The fundamental syntax for strategy.close is:
strategy.close(id, when, qty, comment, order_action, alert_message)
id: Theidstring of thestrategy.entrycall that opened the position you want to close. If omitted,strategy.closeattempts to close the entire open position, regardless of entry ID.when: Aboolexpression. The position is closed on the close of the current bar if this condition istrueon that bar.qty: The number of contracts/shares/lots to close. If omitted orna, the entire remaining position for the specifiedid(or the entire open position ifidis omitted) is closed.comment: A string comment for the order.order_action: Optional string, typically not needed for a simplestrategy.closewhich implies an opposing action to the current position.alert_message: A string for alerts triggered by this order.
When the when condition is met, Pine Script queues a market order that will be filled at the close price of the bar where the condition became true.
Setting Conditions for Closing Positions at Market Close
The power of strategy.close comes from its when argument. This bool expression determines when the closing order is triggered. Common conditions for closing positions on the close of a bar include:
- Time-based Close: Closing at the end of a specific session.
- Indicator-based Close: Closing when an indicator crosses a threshold on the closing price.
- Price-based Close: Closing when price breaks a level based on the bar’s close.
- Combined Conditions: Any combination of the above or other logic.
Example using a time condition:
// Check if the current time is after a certain point in the session
isEndOfSession = time('1', '1500-1600') // True for bars closing between 15:00 and 16:00 EST on 1-minute chart
// Assume a long position was opened with id "MyEntry"
// Close the long position with id "MyEntry" if at the end of the session
strategy.close("MyEntry", when = isEndOfSession, comment = "Close on Session End")
// To close any open position at the end of the session:
// strategy.close(when = isEndOfSession, comment = "Close Any Position on Session End")
This checks the time and, if it falls within the specified window, triggers the strategy.close on that bar’s close.
Specifying Quantity and Other Order Parameters
By default, strategy.close(id, when=...) or strategy.close(when=...) will close the entire open position associated with the id or the total open position, respectively. You can specify a qty argument to close only a portion of the position.
// Example: Close half the position when a condition is met
halfPositionQty = strategy.position_size / 2
closeHalfCondition = close > ta.sma(close, 20) // Example condition
strategy.close("MyEntry", when = closeHalfCondition, qty = halfPositionQty, comment = "Close Half Position")
The comment parameter is useful for adding descriptions that appear in the Strategy Tester and order logs, helping with debugging and analysis.
Advanced Order Management Techniques
Managing orders on close can involve more than just a single strategy.close call. Advanced techniques include coordinating strategy.cancel and strategy.close_all, and integrating stop-loss/take-profit logic.
Using strategy.cancel to Manage Existing Orders Before Close
If you have pending limit or stop orders (strategy.exit or strategy.entry with limit or stop price) that you do not want to be active when your ‘on close’ condition is met, you should cancel them first.
Suppose you have a stop-loss order active, but your end-of-day logic dictates that the position must be closed regardless of price at the session end. You need to cancel the stop-loss order before triggering the strategy.close.
// Assume an entry "MyEntry" and an exit "MyExit" with a stop-loss were placed earlier
// strategy.entry("MyEntry", strategy.long, ...)
// strategy.exit("MyExit", from_entry="MyEntry", stop=...)
isEndOfDay = time('1', '1545-1600') // Example: Last 15 mins of session
// If end of day condition is met...
if isEndOfDay
// Cancel the pending stop-loss/take-profit orders for this entry
strategy.cancel("MyExit")
// Close the position on the close of this bar
strategy.close("MyEntry", comment = "Forced Close on Day End")
It’s crucial to handle the sequence: cancel pending exits before initiating the close-on-close order for the same position. Pine Script processes orders sequentially in the script.
Combining strategy.close_all to Flatten Positions at Close
While strategy.close(id=...) targets a specific entry, strategy.close_all() closes every single open position. This is extremely useful for completely flattening the portfolio at the end of a trading period.
// Check if it's the last bar of the week's trading session
isEndOfWeek = dayofweek == friday and time('D', '1500-1600') // Example: Friday afternoon for Daily chart
// If end of week condition is met, close all open positions
if isEndOfWeek
strategy.close_all(comment = "Close All on Week End")
strategy.close_all() is simpler than tracking individual entry IDs if your goal is a complete portfolio reset at a specific time.
Implementing Stop-Loss and Take-Profit with Orders on Close
You can combine standard stop-loss (stop) and take-profit (limit) orders from strategy.exit with a time-based strategy.close. The strategy.exit orders provide intra-bar or price-level based protection/profit-taking, while the strategy.close acts as a time-based or session-end cleanup.
Consider a strategy where you want a stop-loss active, but if the stop-loss hasn’t been hit by the end of the day, you close the position anyway:
// Assume an entry "MyEntry" was placed
// strategy.entry("MyEntry", strategy.long, ...)
// Define Stop Loss Price (example: 2 ATR below entry price)
stopLossPrice = strategy.position_avg_price - ta.atr(14) * 2
// Define End of Day condition
isEndOfDay = time('1', '1555-1600') // Example: Last 5 mins of session
// Place initial stop-loss using strategy.exit (can fill intrabar or on close if hit)
strategy.exit("Exit_SL", from_entry="MyEntry", stop=stopLossPrice, comment="Stop Loss")
// If End of Day condition is met, close the position on the current bar's close.
// This effectively overrides the strategy.exit if the stop hasn't been hit yet.
if isEndOfDay
// No need to cancel the SL first if using strategy.close on the same entry ID.
// strategy.close will attempt to close the position regardless of pending exits for that ID.
strategy.close("MyEntry", comment="End of Day Close")
In this setup, if the price drops to stopLossPrice, the strategy.exit will trigger. If the end-of-day time arrives before the stop-loss is hit, the strategy.close will trigger, closing the position at the market close price.
Practical Examples and Strategies
Let’s look at a couple of code examples to solidify these concepts.
Simple Example: Closing Long Position at the End of the Trading Day
This script enters a long position and always closes it during the last 30 minutes of the trading day.
//@version=5
strategy("End of Day Close Example", overlay=true)
// --- Strategy Logic ---
// Entry Condition (example: simple Moving Average crossover)
maShort = ta.sma(close, 10)
maLong = ta.sma(close, 30)
entryCondition = ta.crossover(maShort, maLong)
// Exit Condition (Time-based)
isEndOfDayWindow = time('D', '1530-1600') // True for bars closing between 15:30 and 16:00 EST on the Daily session
// --- Order Execution ---
// Enter Long Position
strategy.entry("Enter Long", strategy.long, when = entryCondition)
// Close the Long Position if the End of Day window is active
strategy.close("Enter Long", when = isEndOfDayWindow, comment = "EOD Close")
// Optional: Close any shorts too if strategy supports both directions
// strategy.close("Enter Short", when = isEndOfDayWindow, comment = "EOD Close")
This demonstrates the basic use of time() to define a session window and triggering strategy.close within that window.
More Complex Example: Dynamic Order Sizing Based on ATR and Trailing Stop
This example combines percentage risk-based sizing with a trailing stop and an end-of-day close.
//@version=5
strategy("ATR Risk & EOD Close", overlay=true,
hline = 0, // Add a horizontal line at 0 for strategy testing visualization
process_orders_on_close=true // Explicitly setting, though default for strategies
)
// --- Inputs ---
entryPeriod = input.int(20, "Entry MA Period")
exitPeriod = input.int(5, "Exit MA Period")
riskPercent = input.float(1.0, "Risk Per Trade (%) / 100", minval=0.1, maxval=10.0) * 0.01 // Convert % to decimal
atrPeriod = input.int(14, "ATR Period")
eodCloseTime = input.string("1555-1600", "End of Day Close Time (24hr)") // e.g., "1555-1600"
// --- Calculations ---
shortMA = ta.sma(close, entryPeriod)
longMA = ta.sma(close, exitPeriod) // Using a shorter MA for trailing stop logic
atrValue = ta.atr(atrPeriod)
// --- Strategy Logic ---
// Entry: Crossover of MAs
longEntryCondition = ta.crossover(shortMA, longMA)
// Exit 1: Trailing Stop based on shorter MA
trailingStopLevel = low - longMA // Example logic: trail stop below the short MA
// Exit 2: End of Day Close
isEndOfDay = time('D', eodCloseTime) // Use the input time string
// --- Position Sizing (ATR based) ---
// Calculate dollar risk per share/contract. Use ATR as volatility measure.
// Assuming entry at current close price (approximation for backtesting)
entryPriceApprox = close
// Stop loss could be a fixed multiple of ATR below entry, or below a level like the trailing stop
// For this example, let's assume stop is X*ATR below entry.
stopDistance = atrValue * 2 // Risk 2 ATR distance
dollarRiskPerShare = stopDistance // Simplified: assuming 1 unit = 1 share/contract
// Account size and risk calculation
capital = strategy.initial_capital + strategy.netprofit
dollarRiskPerTrade = capital * riskPercent
// Calculate position size. Avoid division by zero or non-positive risk.
calculatedQty = dollarRiskPerTrade / dollarRiskPerShare
if dollarRiskPerShare <= 0
calculatedQty = 0
positionSize = math.max(1, math.floor(calculatedQty)) // Ensure minimum size is 1, use whole numbers
// --- Order Execution ---
// Enter Long Position with calculated size
strategy.entry("Long", strategy.long, qty = positionSize, when = longEntryCondition)
// Exit using a Trailing Stop (will execute intrabar or on close if hit)
// Need to set a 'stop' price for strategy.exit. Using the trailingStopLevel directly might not work
// as it needs to be evaluated *when* the order is placed. A common pattern is to calculate the stop level
// based on entry price or a previous bar's state.
// More robust trailing stop requires tracking the stop price on each bar:
var float trailingStopPrice = na
if strategy.position_size > 0 // If long position is open
// Calculate the potential new trailing stop price for this bar
currentTrailingStopCandleLevel = close - ta.atr(atrPeriod) // Example: Trailing 1 ATR below close
// Update trailing stop: it only moves up for a long position
if na(trailingStopPrice)
trailingStopPrice := currentTrailingStopCandleLevel
else
trailingStopPrice := math.max(trailingStopPrice, currentTrailingStopCandleLevel)
// Submit or update the trailing stop exit order
strategy.exit("Exit_TS", from_entry="Long", stop=trailingStopPrice, comment="Trailing Stop")
// Close any open Long position if the End of Day window is active
// This order is placed on the bar *within* the EOD window and fills on that bar's close.
// It will override the trailing stop if the EOD condition is met first.
strategy.close("Long", when = isEndOfDay, comment = "EOD Close")
// Plotting (Optional)
plot(shortMA, color=color.blue, title="Short MA")
plot(longMA, color=color.red, title="Long MA")
// Plot Trailing Stop Price if position is open
plot(strategy.position_size > 0 ? trailingStopPrice : na, color=color.green, style=plot.style_cross, linewidth=2, title="Trailing Stop Price")
This example shows calculating position size dynamically, implementing a simple trailing stop logic that updates each bar, and adding a non-conditional end-of-day close using strategy.close.
Backtesting and Optimization Considerations
When backtesting strategies that use orders on close:
- Verify Fills: In the Strategy Tester results, check that ‘On Close’ orders are indeed filled at the bar’s close price.
- Session Definition: Be precise with the
time()function arguments (resolution and session string) to ensure the ‘on close’ condition triggers on the correct bars (e.g., the last bar of a specific session). - Order Conflicts: Be aware of potential conflicts between
strategy.exit(like a stop-loss) andstrategy.close. Pine Script generally prioritizes based on the order of execution within the script and the type of order. Astrategy.closeexplicitly targeting an entry ID usually overrides a pendingstrategy.exitfor the same ID on the same bar if thestrategy.closecondition is met. - Optimization: When optimizing parameters, consider how they interact with the closing logic. For instance, an EOD close time might significantly impact results depending on the typical price action near the session end.
Troubleshooting and Best Practices
Even experienced developers encounter issues. Here’s how to handle common pitfalls and optimize your scripts.
Common Errors and How to Avoid Them
- Incorrect
idinstrategy.close: Using anidthat doesn’t match an active entry will result in no order being placed and a warning in the console. Ensure yourstrategy.entryandstrategy.closeIDs match exactly. - Condition Not Met on Last Bar: Your
whencondition forstrategy.closemust evaluate totrueon the specific bar whose close price you want the order to execute at. If yourtime()window or other logic doesn’t cover the final desired bar, the order won’t trigger. - Order of Operations: If canceling orders before closing, ensure
strategy.cancelis called beforestrategy.closein your script’s logic flow for the same bar. - Misunderstanding
process_orders_on_close: Whilestrategy.closeinherently uses the closing price, theprocess_orders_on_closeargument in thestrategy()declaration affects howstrategy.entryandstrategy.exitorders withwhenconditions are processed when they trigger during a bar. The defaulttruemeans their conditions are evaluated only at the close of the bar, simplifying logic and often aligning fills to the close price anyway. Setting it tofalseenables intrabar order processing forstrategy.entry/strategy.exitwhenconditions (ifcalc_on_every_tick=true), butstrategy.closestill targets the close price.
Ensuring Order Execution at Market Close
To guarantee an order executes at the close of a specific bar, your strategy.close or strategy.close_all call must have its when condition turn true on that exact bar. The time() function is your most reliable tool for time-based closures at session ends.
Using time('D', 'HHMM-HHMM') with the session time of your instrument (e.g., ‘0930-1600’ for NYSE stocks) and specifying a window like ‘1559-1600’ or even just ‘1600-1600’ on a 1-minute chart ensures the condition is true only on the final bar of the day’s session. For daily charts, a condition like dayofweek == friday can signal the end of the trading week.
Tips for Optimizing Script Performance
While strategy.close itself is performant, complex when conditions evaluated on every bar can impact performance, especially on lower timeframes or with high-lookback indicators. Here are some tips:
- Optimize
whenConditions: Make your conditions as efficient as possible. Avoid recalculating complex variables if they aren’t needed for the ‘on close’ logic. - Use
time()Efficiently: Thetime()function is generally efficient for checking session times. - Avoid Redundant Calls: Only call
strategy.closeorstrategy.close_allwhen potentially needed, insideifblocks based on thewhencondition. - Profile Your Script: Use the Pine Script debugger and profiler to identify any bottlenecks.
Processing orders on close is a fundamental technique for building robust and predictable trading strategies in Pine Script. By understanding strategy.close, strategy.close_all, and how to integrate them with time and other conditions, you can implement powerful session management and exit logic in your automated trading systems.