Edward

Quant Trading Guide: Parabolic SAR Trend Following Strategy with Full Code

Trading Strategy


Parabolic SAR (Stop and Reverse, abbreviated PSAR) is a classic trend-following technical indicator created by J. Welles Wilder Jr., the inventor of RSI and ADX. Unlike position-sizing recovery systems such as Martingale, PSAR focuses purely on tracking market momentum and identifying trend reversal points, delivering mechanical entry/exit signals without emotional bias.

The core logic of PSAR lies in dynamic trailing stop dots plotted on price charts:

  • Dots below price = Bullish uptrend, valid long holding signal
  • Dots above price = Bearish downtrend, exit all long positions
  • An acceleration factor (AF) tightens SAR levels as trends strengthen, locking in floating profits automatically

This article walks through a long-only PSAR daily trading strategy built for Tesla (TSLA) stock, complete with full ALGOGENE Python API script, calculation breakdown, execution rules, and key limitations for live deployment.




Core Mathematical Mechanism of Parabolic SAR

PSAR relies on three fixed core parameters:

  1. Initial Acceleration Factor (AF): 0.02
  2. AF increment per new extreme price high/low: +0.02
  3. AF maximum cap: 0.2 (AF stops rising once hit)

Standard PSAR Formula

PSAR_current = PSAR_prev - AF_prev x (PSAR_prev - EP_prev)


  • EP (Extreme Point): Highest high in bull trend, lowest low in bear trend
  • Reversal rule: If price crosses PSAR dot, reset AF back to 0.02 and flip trend state
  • Bull trend: Track new higher highs to raise EP and lift AF
  • Bear trend: Track new lower lows to lower EP and lift AF

Strategy Trading Rules (Long-Only Variant)

This script is strictly long-only, no short positions:

  • Long Entry Trigger: Calculated PSAR sits below the latest daily close (bull trend confirmed), net position ≤ 0
  • If existing short positions exist (edge case), fully close before opening long
  • Long Exit Trigger: Calculated PSAR sits above the latest daily close (bear trend flip), close all open long lots
  • Execution Frequency: Run signal calculation once per trading day (24-hour cooldown to avoid duplicate trades)
  • Data Requirement: Minimum 3 completed daily bars to initialize PSAR trend, EP and AF state


ALGOGENE Strategy Script Walkthrough

We implement the PSAR logic via ALGOGENE’s official Python backtest API, targeting TSLA daily bars. Below is the full annotated script, split into functional modules for easy customization.


Backtest Setting

You can configure the below parameters to test this PSAR long-only strategy:

  • Instrument: TSLA (US Stock)
  • Data Interval: Daily (1D)
  • Backtest Period: Customizable (e.g., 2023–2025)
  • Initial Capital: US$100,000
  • Leverage: 1x (cash equity trading)
  • Base Trade Volume: 1.0 contract (adjust self.trade_volume in code for position sizing)
  • Indicator Fixed Params: AF Start=0.02, AF Step=0.02, AF Max=0.2

Full Complete Strategy Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
from AlgoAPI import AlgoAPIUtil, AlgoAPI_Backtest
from datetime import datetime, timedelta


class AlgoEvent:
    def __init__(self):
        # Target instrument
        self.instrument = "TSLA"

        # Strategy state
        self.first_day = True
        self.pre_acc = None
        self.pre_trend = None
        self.pre_EP = None
        self.pre_PSAR = None

        # Trading control
        self.lasttradetime = datetime(2000, 1, 1)

        # Position/order tracking
        self.open_orders = {}

        # User-adjustable trade size
        # In ALGOGENE, volume means number of contracts.
        # Adjust this according to your capital, leverage, and TSLA contract specification.
        self.trade_volume = 1.0

    def start(self, mEvt):
        self.evt = AlgoAPI_Backtest.AlgoEvtHandler(self, mEvt)
        self.evt.start()

    def on_bulkdatafeed(self, isSync, bd, ab):
        """
        Main strategy logic.
        This strategy is designed for daily-bar execution.
        """
        if self.instrument not in bd:
            return

        current_time = bd[self.instrument]["timestamp"]

        # Run once per day
        if current_time < self.lasttradetime + timedelta(hours=24):
            return

        self.lasttradetime = current_time

        # Get the latest 3 daily bars
        price = self.evt.getHistoricalBar(
            contract={"instrument": self.instrument},
            numOfBar=3,
            interval="D"
        )

        # Need at least 3 bars to reproduce the original logic
        if price is None or len(price) < 3:
            self.evt.consoleLog("Not enough historical data for PSAR calculation.")
            return

        # Convert returned dictionary into sorted list of bars
        timestamps = sorted(price.keys())
        bars = [price[t] for t in timestamps]

        # Previous day bar: bars[-2]
        # Latest completed daily bar: bars[-1]
        prev_bar = bars[-2]
        latest_bar = bars[-1]

        prev_open = prev_bar["o"]
        prev_high = prev_bar["h"]
        prev_low = prev_bar["l"]
        prev_close = prev_bar["c"]

        latest_high = latest_bar["h"]
        latest_low = latest_bar["l"]
        latest_close = latest_bar["c"]
        
        # Initialize PSAR state on the first valid day
        if self.first_day:
            self.pre_acc = 0.02

            if prev_close > prev_open:
                # Previous day was bullish
                self.pre_trend = "bull"
                self.pre_EP = prev_high
                self.pre_PSAR = prev_low
            else:
                # Previous day was bearish
                self.pre_trend = "bear"
                self.pre_EP = prev_low
                self.pre_PSAR = prev_high

            self.first_day = False
            self.evt.consoleLog(
                "PSAR initialized:",
                "trend =", self.pre_trend,
                "EP =", self.pre_EP,
                "PSAR =", self.pre_PSAR,
                "ACC =", self.pre_acc
            )
            return
        
        # Calculate current PSAR value
        # PSAR = previous PSAR - previous acceleration factor * (previous PSAR - previous EP)
        PSAR = self.pre_PSAR - self.pre_acc * (self.pre_PSAR - self.pre_EP)
        
        # SAR reversal adjustment
        if self.pre_trend == "bear" and latest_high > PSAR:
            PSAR = self.pre_EP

        if self.pre_trend == "bull" and latest_low < PSAR:
            PSAR = self.pre_EP

        # Determine current trend
        if PSAR > latest_close:
            trend = "bear"
        else:
            trend = "bull"

        # Calculate current extreme point
        if trend == "bear":
            EP = min(self.pre_EP, latest_low)
        else:
            EP = max(self.pre_EP, latest_high)

        # Calculate acceleration factor
        acc = self.pre_acc

        if trend == self.pre_trend and EP != self.pre_EP:
            acc += 0.02
            acc = min(acc, 0.2)

        if trend != self.pre_trend:
            acc = 0.02

        # Save current PSAR state for next calculation
        self.pre_EP = EP
        self.pre_PSAR = PSAR
        self.pre_acc = acc
        self.pre_trend = trend

        self.evt.consoleLog(
            current_time,
            "Close =", latest_close,
            "PSAR =", PSAR,
            "Trend =", trend,
            "EP =", EP,
            "ACC =", acc
        )

        # Execute trading logic
        net_volume = self.get_net_position()

        if trend == "bull":
            # Open long only if no existing long position
            if net_volume <= 0:
                # If there is any short position, close it first.
                # In the default long-only version, this normally should not happen.
                if net_volume < 0:
                    self.close_all_positions()

                self.open_long_position()

        elif trend == "bear":
            # Close long positions when bearish
            if net_volume > 0:
                self.close_all_positions()


    def open_long_position(self):
        """
        Open a long market order.
        """
        order = AlgoAPIUtil.OrderObject()
        order.instrument = self.instrument
        order.openclose = "open"
        order.buysell = 1
        order.ordertype = 0
        order.volume = self.trade_volume
        order.orderRef = "SAR_LONG"

        self.evt.sendOrder(order)
        self.evt.consoleLog("Send long order for", self.instrument, "volume =", self.trade_volume)

    def close_all_positions(self):
        """
        Close all outstanding opened orders for the target instrument.
        """
        pos, osOrder, pendOrder = self.evt.getSystemOrders()

        for tradeID in osOrder:
            order_info = osOrder[tradeID]

            if order_info["instrument"] == self.instrument:
                close_order = AlgoAPIUtil.OrderObject()
                close_order.tradeID = tradeID
                close_order.openclose = "close"

                self.evt.sendOrder(close_order)
                self.evt.consoleLog("Close order sent. tradeID =", tradeID)

    def get_net_position(self):
        """
        Get current net position for TSLA.
        Positive value means net long.
        Negative value means net short.
        Zero means no position.
        """
        pos, osOrder, pendOrder = self.evt.getSystemOrders()

        if self.instrument in pos:
            return pos[self.instrument]["netVolume"]
        return 0

    def on_openPositionfeed(self, op, oo, uo):
        """
        Update internal open order tracking.
        """
        self.open_orders = oo

Key Code Module Breakdown

  1. Initialization (__init__):
    • Stores persistent PSAR calculation state (AF, EP, trend, PSAR level), trade volume and instrument ticker. All parameters are fully editable for backtesting different assets.
  2. Bulk Market Data Handler (on_bulkdatafeed):

    Core strategy logic triggered on daily bar updates:

    • Restrict one trade signal per day to avoid overtrading
    • Pull 3 historical daily bars to initialize PSAR math
    • Iterative PSAR value, trend, EP and acceleration factor recalculation
    • Execute entry/exit based on bull/bear PSAR trend signals
  3. Order Execution Helpers
    • open_long_position: Submit market buy orders with fixed contract volume
    • close_all_positions: Batch close all existing open trades for TSLA
    • get_net_position: Helper to read current net exposure for position logic control

Core Advantages of the PSAR Trend Strategy

  1. Fully Mechanical & Rule-Based: No subjective price interpretation; all entry/exit decisions are computed mathematically, eliminating trader emotional bias.
  2. Built-In Dynamic Trailing Stop Function: PSAR dots automatically trail price and tighten as trends accelerate, locking incremental profits without manual stop-loss adjustments.
  3. Simple Maintenance & Low Computational Cost: Only requires OHLC daily price data, no complex multi-indicator stack; lightweight for ALGOGENE real-time and backtest engines.
  4. Clear Risk Boundaries: Strategy exits all long exposure immediately once a bearish PSAR flip triggers, avoiding deep drawdowns during trend reversals.

Critical Limitations & Risk Notes

PSAR has distinct market environment weaknesses that all quant traders must account for:

  1. Poor Performance in Sideways/Choppy Markets: In range-bound stocks, PSAR generates frequent false reversal signals, creating repeated small losing trades and eroding cumulative returns.
  2. Lagging Indicator Nature: PSAR confirms trend shifts only after price has already moved, meaning entries and exits occur slightly after market turning points.
  3. Long-Only Bias Restricts Market Exposure: This implementation excludes short selling; during extended bear markets, the strategy will hold zero positions and miss downside trading opportunities.
  4. No Built-In Volatility Filter: Raw PSAR signals do not measure trend strength. Pair with ADX (>25 threshold) as a filter to skip low-momentum ranging markets for improved results.
  5. Sensitive to Parameter Tuning: Default AF 0.02/0.2 works for large-cap stocks like TSLA; crypto, forex and small-cap equities require re-calibrated acceleration settings.


Conclusion

Parabolic SAR is a foundational trend-following tool for systematic retail and institutional traders, offering clear, automated entry and exit signals with native trailing stop functionality. Unlike Martingale’s high-risk capital recovery mechanics, PSAR prioritizes trend capture and risk control as its core design goal.

The provided ALGOGENE Python script delivers a ready-to-deploy long-only PSAR daily trading system for TSLA, with fully editable instrument, position size and core indicator parameters. For robust live performance, we recommend adding a trend strength filter (ADX) and out-of-sample backtesting across multiple market regimes to reduce false range-bound signals.

This code is built for educational demonstration of ALGOGENE’s market data and order execution APIs. All live trading deployments require additional risk management layers (maximum drawdown limits, position sizing caps, volatility filters).