Files
wickra/docs/wiki/indicators/trailing-stops/Indicator-Psar.md
T

247 lines
8.8 KiB
Markdown
Raw Normal View History

2026-05-22 16:18:04 +02:00
# PSAR (Parabolic SAR)
> Wilder's parabolic Stop-And-Reverse: a state-machine trailing stop that
> accelerates toward price as a trend extends and flips sides on a
> penetration of the SAR line.
## Quick reference
| Item | Value |
|---------------------|------------------------------------------------------------------------------------|
| Family | Trailing Stops |
2026-05-22 16:18:04 +02:00
| Input type | `Candle` (uses `high`, `low`) |
| Output type | `f64` |
| Output range | unbounded; bracketed by the prior two highs/lows |
| Default parameters | `af_start = 0.02`, `af_step = 0.02`, `af_max = 0.20` (Wilder) |
| Warmup period | `2` (state machine seeds on the 2nd candle) |
| Interpretation | trailing stop that "flips" sides on penetration; never tied to a fixed bar count |
## Formula
PSAR is a two-state machine — `Up` (long bias) and `Down` (short bias).
Each bar updates three pieces of state:
```
EP_t = extreme price reached so far in the current trend (max high in Up,
min low in Down)
AF_t = acceleration factor, bumped by af_step each time EP makes a new
extreme, capped at af_max
SAR_t = stop-and-reverse level
```
The transition is:
```
SAR_t = SAR_{t-1} + AF_{t-1} * (EP_{t-1} - SAR_{t-1})
# Wilder rule: SAR cannot penetrate today's or yesterday's range
if Up: SAR_t = min(SAR_t, low_{t-1}, low_t)
if Down: SAR_t = max(SAR_t, high_{t-1}, high_t)
# Reversal test
if Up and low_t <= SAR_t: flip to Down, SAR_t = EP_{t-1}, reset AF
if Down and high_t >= SAR_t: flip to Up, SAR_t = EP_{t-1}, reset AF
```
The exact step-by-step is `crates/wickra-core/src/indicators/psar.rs:75-141`.
## Parameters
| Name | Type | Default | Constraint | Source |
|------------|-------|---------|-------------------------------------------|---------------------------------------|
| `af_start` | `f64` | `0.02` | finite, `> 0`, `≤ af_max` | `Psar::new` (`psar.rs:39-50`) |
| `af_step` | `f64` | `0.02` | finite, `> 0` | `Psar::new` (`psar.rs:39-50`) |
| `af_max` | `f64` | `0.20` | finite, `> 0` | `Psar::new` (`psar.rs:39-50`) |
Python defaults from
`#[pyo3(signature = (af_start=0.02, af_step=0.02, af_max=0.20))]` in
`bindings/python/src/lib.rs`. `Psar::classic()` returns the same triple.
Validation errors:
- non-finite or non-positive AF parameter → `Error::NonPositiveMultiplier`
- `af_start > af_max``Error::InvalidPeriod { message: "af_start must be <= af_max" }`
## Inputs / Outputs
```rust
impl Indicator for Psar {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64>;
fn warmup_period(&self) -> usize { 2 }
}
```
- **Python streaming.** Returns `float | None`.
- **Python batch.** `PSAR.batch(high, low, close)` returns a 1-D
`np.ndarray`; the first row is `NaN` (warmup) and every subsequent
row holds the SAR level for that bar.
- **Node streaming.** Not exposed in the Node binding.
- **Node batch.** `psar.batch(high, low, close)` returns
`Array<number>` with `NaN` for the first row.
## Warmup
`warmup_period() == 2`. The very first candle seeds internal state
(`prev_high`, `prev_low`, `sar = low`, `ep = high`, `trend = Up`,
`af = af_start`) and returns `None`. The second candle produces the
first SAR value.
The seed trend is **always** `Up` (`psar.rs:83`); the indicator will
reverse to `Down` on the first qualifying penetration. There is no
look-ahead at the second candle's close — the seed is purely structural.
## Edge cases
- **First bar.** Always returns `None`; downstream code must tolerate
the first row being absent without crashing.
- **Pure uptrend.** With monotonically rising highs and lows, the SAR
remains below the lows and accelerates toward price as the EP makes
successive new highs. The pinned test `pure_uptrend_sar_below_lows`
asserts `SAR ≤ low` on every emitted bar of a 40-bar ramp.
- **Pure downtrend.** Symmetrically, with monotonically falling highs,
the SAR sits above the highs after the trend establishes.
`pure_downtrend_sar_above_highs` covers this.
- **Reversal mechanics.** When the trend flips, `SAR` is set to the
previous EP (not the calculated parabola value), AF is reset to
`af_start`, and the new EP is the current bar's high (Down→Up) or
low (Up→Down).
- **Choppy regime.** Frequent reversals cause many AF resets; SAR
becomes a poor stop in mean-reverting regimes and whipsaws.
- **NaN / infinity.** `Candle::new` rejects non-finite OHLC values.
`Psar::new` rejects non-finite AF parameters.
- **Reset.** `reset()` clears the initialised flag and resets `af` to
`af_start`, `sar` to `0.0`, `ep` to `0.0`; the next `update` re-seeds.
## Examples
### Rust
```rust
use wickra::{BatchExt, Candle, Indicator, Psar};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let candles: Vec<Candle> = (0..8)
.map(|i| {
let base = 100.0 + f64::from(i);
Candle::new(base, base + 0.5, base - 0.5, base + 0.25, 1.0, 0).unwrap()
})
.collect();
let mut p = Psar::classic(); // (0.02, 0.02, 0.20)
for (i, v) in p.batch(&candles).into_iter().enumerate() {
println!("i={i} -> {:?}", v);
}
Ok(())
}
```
Output:
```
i=0 -> None
i=1 -> Some(99.5)
i=2 -> Some(99.58)
i=3 -> Some(99.7552)
i=4 -> Some(100.054784)
i=5 -> Some(100.4993056)
i=6 -> Some(101.099388928)
i=7 -> Some(101.85547447808)
```
The SAR starts at `99.5` (the first candle's low) and accelerates
upward toward price as the EP makes new highs on every bar.
### Python
```python
import numpy as np
import wickra as ta
p = ta.PSAR() # defaults (0.02, 0.02, 0.20)
h = np.array([100.5, 101.5, 102.5, 103.5, 104.5, 105.5, 106.5, 107.5])
l = np.array([ 99.5, 100.5, 101.5, 102.5, 103.5, 104.5, 105.5, 106.5])
cl = np.array([100.25, 101.25, 102.25, 103.25, 104.25, 105.25, 106.25, 107.25])
print(p.batch(h, l, cl))
```
Output:
```
[ nan 99.5 99.58 99.7552 100.054784
100.4993056 101.09938893 101.85547448]
```
### Node
```js
const w = require('wickra');
const p = new w.PSAR(0.02, 0.02, 0.20);
console.log(p.batch(
[100.5, 101.5, 102.5, 103.5, 104.5, 105.5, 106.5, 107.5],
[ 99.5, 100.5, 101.5, 102.5, 103.5, 104.5, 105.5, 106.5],
[100.25, 101.25, 102.25, 103.25, 104.25, 105.25, 106.25, 107.25],
));
```
Output:
```
[
NaN,
99.5,
99.58,
99.7552,
100.054784,
100.4993056,
101.099388928,
101.85547447808
]
```
## Interpretation
- **Stop & reverse.** PSAR is a *trailing stop*, not a signal generator
in isolation: a long is exited (and a short is initiated) the bar
that price penetrates the SAR line.
- **Acceleration.** The further a trend extends without making new
extremes, the slower the SAR rises (or falls). When EP makes a new
extreme, AF bumps by `af_step` and the SAR closes the distance to
price more aggressively.
- **Whipsaw risk.** In sideways markets PSAR flips repeatedly; pair it
with a trend filter (ADX, slope of EMA) to skip trades when the
underlying isn't actually trending.
## Common pitfalls
- **The first bar always returns `None`.** Code that pre-allocates a
vector and does `out[i] = psar.update(c).unwrap()` will panic on
the very first input. Use `if let Some(...)` or skip the first
row explicitly.
- **Initial trend is hard-coded to `Up`.** The seed bar always sets
`trend = Up`, regardless of whether the data is in a downtrend.
Expect a near-immediate reversal to `Down` if you feed PSAR a
decisively bearish series — the first emitted SAR may look
"wrong" because it is the prior EP from the artificial `Up`
seed, not from a real bullish run.
- **Acceleration cap matters.** `af_max = 0.20` is Wilder's choice;
raising it produces an extremely tight stop near tops/bottoms but
exits good trends prematurely. Lowering it produces a forgiving
stop that gives back more open profit. Always re-validate strategy
PnL when you change `af_max`.
## References
- J. Welles Wilder Jr., *New Concepts in Technical Trading Systems*,
Trend Research, 1978. Chapter on the Parabolic SAR introduces the
state-machine recursion and the default `(0.02, 0.02, 0.20)`
parameters.
## See also
- [ATR](../volatility-bands/Indicator-Atr.md) — sister indicator from the same Wilder text.
- [Donchian Channels](../volatility-bands/Indicator-Donchian.md) — alternative breakout-style
2026-05-22 16:18:04 +02:00
trailing stop based on rolling extrema.
- [Keltner Channels](../volatility-bands/Indicator-Keltner.md) — envelope you can use as a
2026-05-22 16:18:04 +02:00
smoother stop boundary than PSAR in choppy regimes.