Based on the provided “PSAR Trend Filter” indicator, here is the architectural blueprint for its transformation into a production-grade automated execution framework.
1. Execution Triggers (Entry & Direction)¶
The core of the execution logic will be the filtered signals generated by the original script. We will translate these signals into concrete order commands.
Long Entry Condition: A long position is initiated when the
buySignalboolean becomestrue. This requires all the following user-selected conditions to be met simultaneously:ta.crossover(close, psar): The bar’s closing price crosses above the Parabolic SAR value.trendLongOk: If the EMA filter is active, the close must be above the EMA.strengthOk: If the ADX filter is active, the ADX value must be greater than or equal toadxMin.candleLongOk: If the candle filter is active, the bar must be bullish (close > open).cooldownOk: The specified number of bars (cooldownBars) must have passed since the last signal.
Short Entry Condition: A short position is initiated when the
sellSignalboolean becomestrue. This requires:ta.crossunder(close, psar): The bar’s closing price crosses below the Parabolic SAR value.trendShortOk: If the EMA filter is active, the close must be below the EMA.strengthOk: If the ADX filter is active, the ADX value must be greater than or equal toadxMin.candleShortOk: If the candle filter is active, the bar must be bearish (close < open).cooldownOk: The specified number of bars must have passed since the last signal.
Execution Nuances:
Execute at “Close” of the bar: The original script includes a
confirmOnCloseinput. For a production system, this is the only reliable method. We will set the strategy to calculate on bar close (calc_on_every_tick=false) and execute the order on the open of the next bar. This prevents signal repainting and ensures that the backtest results are realistically achievable. Attempting to execute on real-time price action (process_orders_on_close=false) introduces significant discrepancies between backtested performance and live results due to the unpredictable nature of intra-bar price movement.
Signal Reversals: The PSAR is an “always-in” system by nature. A buy signal can only occur after a period of being below the PSAR, and vice-versa. To handle this professionally, we will use a single trade identifier for both long and short entries (e.g.,
strategy.entry("PSAR_Enter", strategy.long)andstrategy.entry("PSAR_Enter", strategy.short)). This instructs the Pine Script execution engine to automatically close any open position in the opposite direction before opening the new one. This is the most efficient way to handle reversals.
2. Multi-Tiered Exit Logic¶
A simple reversal is insufficient for robust risk management. We will implement a multi-layered exit system that operates independently of new entry signals.
Initial Stop Loss (Volatility-Based): Instead of a fixed percentage, the stop loss will be dynamically calculated using the Average True Range (ATR), which reflects current market volatility.
Logic: Upon a Long entry, the initial stop loss will be placed at
entry_price - (ATR * multiplier).Logic: Upon a Short entry, the initial stop loss will be placed at
entry_price + (ATR * multiplier).Implementation: We will add inputs for
atrLength(e.g., 14) andatrMultiplier(e.g., 2.5). The stop loss is calculated before the entry order is placed to determine position size.
Take Profit / Trailing Stop:
Initial Profit Target (Scaling Out): A simple and effective method is to set an initial profit target based on a multiple of the initial risk (an R-multiple). For example, at a 1.5R profit, we can close 50% of the position. This locks in profit and reduces the risk on the remaining position.
Dynamic Trailing Mechanism: After the first profit target is hit, the stop loss on the remaining position should be moved to breakeven. From that point, a trailing stop is activated. The PSAR itself is a natural trailing stop. Alternatively, an ATR-based trailing stop can be used, where the stop is continuously updated to be
high - (ATR * multiplier)for longs orlow + (ATR * multiplier)for shorts. The PSAR is often preferred as it’s integral to the system’s logic.
Time-Based Exits:
End of Session: For instruments with defined trading sessions (e.g., futures, stocks), a non-negotiable rule is to close all open positions a few minutes before the session ends to avoid overnight risk and funding charges. We will implement a time-based check to
strategy.close_all()at a specified time (e.g., 15 minutes before market close).Stagnation Exit: Capital should not be held hostage in a non-performing trade. We will add a “max bars in trade” rule. If a position has been open for more than bars (e.g., 50 bars) without hitting its stop loss or take profit, it will be closed. This frees up capital for more promising opportunities.
3. Capital Allocation & Risk Management¶
Position sizing is arguably more important than the entry signal itself. We will move from a fixed-lot system to a dynamic, risk-based model.
Risk-Based Sizing: The core of professional risk management. The strategy will risk a fixed percentage of account equity on every single trade.
Formula:
Position Size = (Account Equity * Risk Percentage) / (Distance to Stop Loss in currency)Logic:
Define a
riskPercentinput (e.g., 1.0 for 1% risk per trade).Before entry, calculate the stop loss price using the ATR method described above.
Calculate the risk-per-share/contract:
riskPerUnit = abs(entry_price - stop_loss_price).Calculate the total capital to risk:
capitalAtRisk = strategy.equity * (riskPercent / 100).Calculate the final position size:
positionSize = capitalAtRisk / riskPerUnit.This calculated
positionSizeis then used in thestrategy.entry()command. This ensures that whether the stop is wide (low volatility) or tight (high volatility), the maximum potential loss is always the same percentage of our equity.
Pyramiding & Scaling:
Pyramiding (Scaling In): The
useCooldownfeature in the original script actively prevents pyramiding. To allow for it,useCooldownmust befalse. A rule for adding to a position could be: “If in a profitable long trade and price pulls back to and holds above theemaValue, add a new position (e.g., 50% of the initial size), with its stop loss placed at the same level as the original trade’s stop.” The strategy’spyramidingparameter must be set to a value greater than 1.Scaling Out: As defined in the exit logic, we will use
strategy.exit()with aqty_percentparameter. For example, to close half the position at the first take-profit level, we would usestrategy.exit("TP1/SL", from_entry="PSAR_Enter", qty_percent=50, limit=takeProfit1Price, stop=initialStopLossPrice).
4. Implementation Snippet (Pine Logic)¶
This snippet demonstrates the transformation into a strategy with the described professional-grade components.
//@version=5
// 1. STRATEGY DECLARATION: Professional parameters for slippage, commission, and execution.
strategy("PSAR Execution Framework",
overlay=true,
pyramiding=0, // Set to >0 to allow pyramiding
initial_capital=100000,
commission_type=strategy.commission.percent,
commission_value=0.04, // Realistic commission
slippage=2, // Realistic slippage in ticks
process_orders_on_close=true, // Ensures execution on next bar's open for reliability
calc_on_every_tick=false) // Prevents repainting
// --- Original Inputs (start, increment, maximum, filters, etc.) ---
// [User would paste the original input section here]
start = input.float(0.02, "PSAR Start")
increment = input.float(0.02, "PSAR Increment")
maximum = input.float(0.20, "PSAR Maximum")
useEMAFilter = input.bool(true, "Use EMA Trend Filter")
emaLength = input.int(50, "EMA Length")
useADXFilter = input.bool(true, "Use ADX Strength Filter")
adxLength = input.int(14, "ADX Length")
adxMin = input.float(15.0, "Minimum ADX")
useCooldown = input.bool(true, "Use Cooldown Between Signals")
cooldownBars = input.int(1, "Cooldown Bars")
// --- 2. NEW RISK & EXIT MANAGEMENT INPUTS ---
const string riskGroup = "Risk & Exit Management"
riskPercent = input.float(1.0, title="Risk Per Trade %", minval=0.1, maxval=10, step=0.1, group=riskGroup)
useAtrStop = input.bool(true, title="Use ATR for Stop Loss", group=riskGroup)
atrLength = input.int(14, title="ATR Length", group=riskGroup)
atrMultiplier = input.float(2.5, title="ATR Multiplier for Stop", group=riskGroup)
useTimeExit = input.bool(true, title="Use End of Session Exit", group=riskGroup)
exitTime = input.session("1545-1600", title="Exit Session", group=riskGroup) // Example for US Equities
// --- Original Calculations (PSAR, EMA, ADX) ---
psar = ta.sar(start, increment, maximum)
emaValue = ta.ema(close, emaLength)
[_, _, adxValue] = ta.dmi(adxLength, adxLength)
atrValue = ta.atr(atrLength)
// --- Original Signal Logic ---
rawBuySignal = ta.crossover(close, psar)
rawSellSignal = ta.crossunder(close, psar)
trendLongOk = not useEMAFilter or close > emaValue
trendShortOk = not useEMAFilter or close < emaValue
strengthOk = not useADXFilter or (not na(adxValue) and adxValue >= adxMin)
var int lastSignalBar = 0
cooldownOk = not useCooldown or (bar_index - lastSignalBar > cooldownBars)
buySignal = rawBuySignal and trendLongOk and strengthOk and cooldownOk
sellSignal = rawSellSignal and trendShortOk and strengthOk and cooldownOk
if buySignal or sellSignal
lastSignalBar := bar_index
// --- 3. DYNAMIC POSITION SIZING & EXECUTION LOGIC ---
// Calculate Stop Loss Price *before* entry
stopLossDistance = useAtrStop ? atrValue * atrMultiplier : ta.valuewhen(rawBuySignal or rawSellSignal, psar, 0) - close
longStopPrice = close - stopLossDistance
shortStopPrice = close + stopLossDistance
// Calculate Position Size based on risk
capitalToRisk = (riskPercent / 100) * strategy.equity
positionSize = capitalToRisk / (stopLossDistance * syminfo.pointvalue)
positionSize := positionSize < 1 ? 1 : positionSize // Ensure minimum size is 1 contract/share
// ENTRY LOGIC
if (buySignal and strategy.opentrades == 0)
// Entry command with calculated size
strategy.entry("PSAR_Enter", strategy.long, qty=positionSize)
// Exit command to place the initial Stop Loss
strategy.exit("SL", from_entry="PSAR_Enter", stop=longStopPrice)
if (sellSignal and strategy.opentrades == 0)
// Entry command with calculated size
strategy.entry("PSAR_Enter", strategy.short, qty=positionSize)
// Exit command to place the initial Stop Loss
strategy.exit("SL", from_entry="PSAR_Enter", stop=shortStopPrice)
// EXIT LOGIC
// Time-based Exit
isWithinExitTime = time(timeframe.period, exitTime)
if useTimeExit and isWithinExitTime
strategy.close_all(comment="End of Session Exit")
// Update trailing stop based on PSAR on each bar
if strategy.position_size > 0
strategy.exit("PSAR_Trail", from_entry="PSAR_Enter", stop=psar)
if strategy.position_size < 0
strategy.exit("PSAR_Trail", from_entry="PSAR_Enter", stop=psar)
// --- PLOTTING for visualization ---
plot(psar, "PSAR", color=color.gray, style=plot.style_cross)
plot(strategy.opentrades.entry_price(0), "Entry Price", color.blue, style=plot.style_linebr)
plot(strategy.opentrades.stop_price(0), "Stop Price", color.red, style=plot.style_linebr)