How to Process Orders on Close in TradingView Pine Script?

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 simple when condition.
  • strategy.close(): Specifically designed to close an existing position initiated by a given strategy.entry call, or the entire open position if no id is specified.
  • strategy.close_all(): Closes all currently open positions, regardless of how they were entered.
  • strategy.cancel(): Cancels pending orders (limit, stop) previously submitted by strategy.entry or strategy.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: The id string of the strategy.entry call that opened the position you want to close. If omitted, strategy.close attempts to close the entire open position, regardless of entry ID.
  • when: A bool expression. The position is closed on the close of the current bar if this condition is true on that bar.
  • qty: The number of contracts/shares/lots to close. If omitted or na, the entire remaining position for the specified id (or the entire open position if id is omitted) is closed.
  • comment: A string comment for the order.
  • order_action: Optional string, typically not needed for a simple strategy.close which 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:

  1. Time-based Close: Closing at the end of a specific session.
  2. Indicator-based Close: Closing when an indicator crosses a threshold on the closing price.
  3. Price-based Close: Closing when price breaks a level based on the bar’s close.
  4. 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) and strategy.close. Pine Script generally prioritizes based on the order of execution within the script and the type of order. A strategy.close explicitly targeting an entry ID usually overrides a pending strategy.exit for the same ID on the same bar if the strategy.close condition 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 id in strategy.close: Using an id that doesn’t match an active entry will result in no order being placed and a warning in the console. Ensure your strategy.entry and strategy.close IDs match exactly.
  • Condition Not Met on Last Bar: Your when condition for strategy.close must evaluate to true on the specific bar whose close price you want the order to execute at. If your time() 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.cancel is called before strategy.close in your script’s logic flow for the same bar.
  • Misunderstanding process_orders_on_close: While strategy.close inherently uses the closing price, the process_orders_on_close argument in the strategy() declaration affects how strategy.entry and strategy.exit orders with when conditions are processed when they trigger during a bar. The default true means their conditions are evaluated only at the close of the bar, simplifying logic and often aligning fills to the close price anyway. Setting it to false enables intrabar order processing for strategy.entry/strategy.exit when conditions (if calc_on_every_tick=true), but strategy.close still 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 when Conditions: Make your conditions as efficient as possible. Avoid recalculating complex variables if they aren’t needed for the ‘on close’ logic.
  • Use time() Efficiently: The time() function is generally efficient for checking session times.
  • Avoid Redundant Calls: Only call strategy.close or strategy.close_all when potentially needed, inside if blocks based on the when condition.
  • 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.


Leave a Reply