mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 10:08:05 +00:00
Add Savitzky-Golay Moving Average (SGMA) Indicator Implementation
- Implemented SgmaIndicator class in C# with properties for Period, Degree, and Source. - Added unit tests for SgmaIndicator covering constructor defaults, initialization, and various update scenarios. - Created a new Quantower adapter for the SGMA indicator, including input parameters and line series setup. - Removed legacy SGMA implementation and tests to streamline the codebase. - Updated project files to include new indicator and tests in the build process. - Generated a missing indicators report and outlined a plan for oscillator documentation rewrite.
This commit is contained in:
+154
-87
@@ -1,131 +1,198 @@
|
||||
# PGO: Pretty Good Oscillator
|
||||
|
||||
> "Good enough to trade, honest enough not to pretend otherwise." — Mark Johnson (probably)
|
||||
> "Good enough to trade, honest enough not to pretend otherwise."
|
||||
|
||||
## Introduction
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Category** | Oscillator |
|
||||
| **Inputs** | High, Low, Close (TBar) |
|
||||
| **Parameters** | `period` (default 14) |
|
||||
| **Outputs** | PGO value (`Last`) |
|
||||
| **Output range** | Unbounded (typically -5 to +5, in ATR multiples) |
|
||||
| **Warmup period** | `period` |
|
||||
|
||||
The Pretty Good Oscillator (PGO) measures how far the current price has deviated from its Simple Moving Average, expressed in Average True Range (ATR) units. A reading of +2.0 means price is two ATRs above the SMA; -3.0 means three ATRs below. The volatility normalization makes PGO readings comparable across instruments and timeframes, unlike raw price-minus-average oscillators that scale with price level.
|
||||
### Key takeaways
|
||||
|
||||
- Measures price displacement from its SMA, **normalized by ATR**, producing a dimensionless ratio comparable across instruments and timeframes.
|
||||
- A reading of +2.0 means price is two ATRs above the mean; -3.0 means three ATRs below. The same threshold carries the same statistical weight regardless of price level.
|
||||
- Uses **True Range** (which captures gap risk) rather than standard deviation, making it more responsive to overnight gaps and limit moves than Bollinger-based alternatives.
|
||||
- Single-parameter design (both SMA and ATR share the same period) keeps configuration simple while the ATR denominator automatically adapts to volatility regimes.
|
||||
- EMA-based ATR with **warmup compensation** ensures accurate normalization from the first bar.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Mark Johnson introduced PGO in the late 1990s as a practical alternative to oscillators that produce instrument-dependent readings. The core insight: dividing by ATR creates a dimensionless ratio. A stock at $500 and a penny stock at $2 can both produce a PGO of +3.0, and that reading carries the same statistical meaning in both cases. The name "Pretty Good" reflects Johnson's deliberately modest positioning: not the ultimate oscillator, but a reliable workhorse that normalizes displacement by realized volatility rather than by standard deviation (like Bollinger %B) or by price level (like CFO).
|
||||
Mark Johnson introduced the Pretty Good Oscillator in the late 1990s as a practical alternative to oscillators that produce instrument-dependent readings. The core insight: dividing by ATR creates a dimensionless ratio. A $500 stock and a $2 penny stock can both produce a PGO of +3.0, and that reading carries the same statistical meaning. The deliberately modest name reflects Johnson's positioning: not the ultimate oscillator, but a reliable workhorse that normalizes displacement by realized volatility.
|
||||
|
||||
The indicator shares conceptual DNA with z-scores and Bollinger Bands but uses ATR (which captures gap risk through True Range) rather than standard deviation (which doesn't). This makes PGO more responsive to overnight gaps and limit moves.
|
||||
The indicator shares conceptual DNA with z-scores and Bollinger %B but makes a different normalization choice. Bollinger uses standard deviation, which treats upside and downside gaps identically and ignores the previous close. PGO uses ATR, which incorporates the gap between the previous close and the current bar's range. In markets where overnight gaps represent real risk (futures, individual stocks), this distinction matters.
|
||||
|
||||
## Architecture and Physics
|
||||
## What It Measures and Why It Matters
|
||||
|
||||
### 1. Simple Moving Average (SMA)
|
||||
PGO answers a simple question: "How far is price from its average, measured in units of typical bar-to-bar movement?" A reading of +3.0 means price has moved three typical ranges above its mean. This is statistically unusual regardless of whether the instrument is a penny stock or a major index, which is the indicator's principal advantage over raw price-minus-average oscillators.
|
||||
|
||||
Standard arithmetic mean over the lookback period:
|
||||
|
||||
$$\text{SMA}_t = \frac{1}{N} \sum_{i=0}^{N-1} \text{Close}_{t-i}$$
|
||||
|
||||
Implemented as a running sum with O(1) incremental updates via RingBuffer.
|
||||
|
||||
### 2. True Range (TR)
|
||||
|
||||
$$\text{TR}_t = \max\bigl(\text{High}_t - \text{Low}_t,\; |\text{High}_t - \text{Close}_{t-1}|,\; |\text{Low}_t - \text{Close}_{t-1}|\bigr)$$
|
||||
|
||||
Captures both intrabar range and gap risk from the previous close.
|
||||
|
||||
### 3. Average True Range (ATR)
|
||||
|
||||
Exponential Moving Average of TR with warmup compensation:
|
||||
|
||||
$$\text{ATR}_t = \text{EMA}(\text{TR}, N) \cdot \frac{1}{1 - (1 - \alpha)^t}$$
|
||||
|
||||
where $\alpha = 1/N$. The compensation factor corrects the EMA bias during the initial warmup period, converging to 1.0 as $t \to \infty$.
|
||||
|
||||
### 4. PGO Computation
|
||||
|
||||
$$\text{PGO}_t = \frac{\text{Close}_t - \text{SMA}_t}{\text{ATR}_t}$$
|
||||
|
||||
When $\text{ATR} = 0$ (constant price), PGO returns 0.0 by convention.
|
||||
The ATR denominator automatically adjusts for volatility regime changes. During quiet periods, ATR contracts and the same price displacement produces a larger PGO reading, flagging the move as statistically significant. During volatile periods, ATR expands and price needs to move further from the mean to register the same PGO level. This self-calibration means fixed overbought/oversold thresholds (±3.0) are more stable across market conditions than thresholds on non-normalized oscillators.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
The PGO is a volatility-normalized displacement measure. In continuous terms:
|
||||
### Core Formula
|
||||
|
||||
$$\text{PGO} = \frac{P - \bar{P}}{\sigma_{\text{ATR}}}$$
|
||||
Simple Moving Average of Close:
|
||||
|
||||
where $\bar{P}$ is the rolling mean and $\sigma_{\text{ATR}}$ is the ATR-based volatility estimate.
|
||||
$$SMA_t = \frac{1}{N} \sum_{i=0}^{N-1} Close_{t-i}$$
|
||||
|
||||
True Range:
|
||||
|
||||
$$TR_t = \max(H_t - L_t, \; |H_t - C_{t-1}|, \; |L_t - C_{t-1}|)$$
|
||||
|
||||
Average True Range via EMA with warmup compensation:
|
||||
|
||||
$$ATR_t = EMA(TR, N) \cdot \frac{1}{1 - (1-\alpha)^t}$$
|
||||
|
||||
where $\alpha = 1/N$.
|
||||
|
||||
Pretty Good Oscillator:
|
||||
|
||||
$$PGO_t = \frac{Close_t - SMA_t}{ATR_t}$$
|
||||
|
||||
When $ATR = 0$ (constant price), PGO returns 0.0 by convention.
|
||||
|
||||
### Parameter Mapping
|
||||
|
||||
| Parameter | Default | Range | Effect |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Period ($N$) | 14 | 1-500 | Controls both SMA lookback and ATR smoothing. Larger periods produce smoother, slower oscillations. |
|
||||
| Parameter | Formula role | Default | Constraint |
|
||||
|-----------|-------------|---------|------------|
|
||||
| `period` | Lookback for SMA and smoothing factor for ATR EMA | 14 | > 0 |
|
||||
|
||||
### Transfer Function
|
||||
### Warmup Period
|
||||
|
||||
PGO has no recursive (IIR) component in its numerator; SMA is pure FIR. The ATR denominator uses EMA (IIR) with transfer function:
|
||||
$$W = period$$
|
||||
|
||||
$$H(z) = \frac{\alpha}{1 - (1-\alpha)z^{-1}}$$
|
||||
Default configuration (period=14) warms up in 14 bars.
|
||||
|
||||
This gives the denominator exponential decay characteristics while the numerator remains finite-impulse.
|
||||
## Architecture & Physics
|
||||
|
||||
## Performance Profile
|
||||
### 1. SMA via Running Sum
|
||||
|
||||
| Operation | Complexity | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| SMA update | O(1) | RingBuffer running sum |
|
||||
| TR calculation | O(1) | Three comparisons |
|
||||
| ATR (EMA) update | O(1) | FMA-optimized IIR |
|
||||
| PGO computation | O(1) | Single division |
|
||||
| **Total per bar** | **O(1)** | Zero allocations in hot path |
|
||||
The [`RingBuffer`](lib/oscillators/pgo/Pgo.cs:26) maintains a circular buffer of close prices. [`SmaSum`](lib/oscillators/pgo/Pgo.cs:29) tracks the running total with O(1) incremental updates: subtract the oldest value, add the new value.
|
||||
|
||||
### Quality Metrics
|
||||
### 2. ATR via Compensated EMA
|
||||
|
||||
| Metric | Score (1-10) | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| Noise rejection | 5 | SMA has no frequency selectivity |
|
||||
| Lag | 6 | SMA lag = (N-1)/2 bars; ATR smoothing adds minimal lag |
|
||||
| Sensitivity | 7 | ATR normalization adapts to volatility regimes |
|
||||
| Simplicity | 9 | Two components, one parameter |
|
||||
| Cross-instrument comparability | 9 | Dimensionless output |
|
||||
True Range is computed from High, Low, Close, and previous Close using three comparisons. The EMA of TR uses [`Math.FusedMultiplyAdd`](lib/oscillators/pgo/Pgo.cs:158) for the `alpha * (tr - ema) + ema` pattern. During warmup, the geometric decay factor $e = e \times (1 - \alpha)$ provides bias correction.
|
||||
|
||||
## Interpretation
|
||||
### 3. Single-Parameter Design
|
||||
|
||||
### Overbought/Oversold Levels
|
||||
Both SMA lookback and ATR smoothing use the same `period`, which means a single parameter controls the entire indicator. This simplifies optimization and reduces overfitting risk compared to dual-parameter designs.
|
||||
|
||||
- **Above +3.0**: Price is 3 ATRs above the mean. Statistically extended; reversal probability increases.
|
||||
- **Below -3.0**: Price is 3 ATRs below the mean. Statistically depressed; bounce probability increases.
|
||||
- **Between -1.0 and +1.0**: Normal range; no directional bias.
|
||||
### 4. Dual Update Overloads
|
||||
|
||||
### Zero Line Crossovers
|
||||
[`Update(TBar)`](lib/oscillators/pgo/Pgo.cs:115) provides full OHLC context for proper True Range computation. [`Update(TValue)`](lib/oscillators/pgo/Pgo.cs:176) creates a synthetic bar with H=L=C=value, producing TR=0, which is documented as suboptimal.
|
||||
|
||||
- PGO crosses above zero: price crosses above SMA (bullish momentum shift).
|
||||
- PGO crosses below zero: price crosses below SMA (bearish momentum shift).
|
||||
### 5. Edge Cases
|
||||
|
||||
### Divergence Analysis
|
||||
- **Zero ATR**: When all bars have identical OHLC, ATR = 0 and PGO returns 0.0.
|
||||
- **NaN/Infinity inputs**: Last-valid substitution for close; high/low fall back to close.
|
||||
- **No previous close**: First bar uses close as the previous close, producing TR = High - Low only.
|
||||
- **Correction (isNew=false)**: SMA sum is recalculated from the buffer to maintain accuracy.
|
||||
|
||||
- **Bullish divergence**: Price makes lower lows while PGO makes higher lows. ATR-normalized displacement is contracting despite new price lows — sellers exhausting.
|
||||
- **Bearish divergence**: Price makes higher highs while PGO makes lower highs. Despite new highs, displacement relative to volatility is shrinking.
|
||||
## Interpretation and Signals
|
||||
|
||||
### Signal Zones
|
||||
|
||||
| Zone | PGO value | Meaning |
|
||||
|------|-----------|---------|
|
||||
| Extreme overbought | > +3.0 | Price is 3+ ATRs above mean; statistically extended |
|
||||
| Overbought | +2.0 to +3.0 | Above-average displacement; caution warranted |
|
||||
| Neutral | -1.0 to +1.0 | Normal range; no directional bias |
|
||||
| Oversold | -3.0 to -2.0 | Below-average displacement; caution warranted |
|
||||
| Extreme oversold | < -3.0 | Price is 3+ ATRs below mean; statistically depressed |
|
||||
|
||||
### Signal Patterns
|
||||
|
||||
- **Zero-line crossover (bullish)**: PGO crosses above zero; price crosses above SMA.
|
||||
- **Zero-line crossover (bearish)**: PGO crosses below zero; price crosses below SMA.
|
||||
- **Bullish divergence**: Price makes lower lows while PGO makes higher lows; ATR-normalized displacement is contracting despite new price lows.
|
||||
- **Bearish divergence**: Price makes higher highs while PGO makes lower highs; displacement relative to volatility is shrinking.
|
||||
- **Extreme reversion**: PGO beyond ±3.0 reverting toward zero suggests mean reversion is underway.
|
||||
|
||||
### Practical Notes
|
||||
|
||||
- The ±3.0 thresholds are guidelines, not hard rules. Fat-tailed distributions (common in finance) produce more extreme readings than Gaussian models suggest.
|
||||
- In strong trends, PGO may persist at ±2 to ±4 for extended periods. This confirms trend strength rather than signaling imminent reversal.
|
||||
- PGO requires proper OHLC data for meaningful ATR computation. Feeding close-only data (H=L=C) produces TR=0 and a meaningless oscillator.
|
||||
|
||||
## Related Indicators
|
||||
|
||||
- [**Bollinger %B**](../bbb/Bbb.md): Normalizes price position by standard deviation (Bollinger Bands) rather than ATR.
|
||||
- [**CFO**](../cfo/Cfo.md): Normalizes regression residual by price level rather than volatility.
|
||||
- [**Williams %R**](../willr/Willr.md): Normalizes price position within the high-low range, bounded to [-100, 0].
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Validated | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| Skender | - | No PGO implementation |
|
||||
| TA-Lib | - | No PGO implementation |
|
||||
| Tulip | - | No PGO implementation |
|
||||
| Ooples | - | Not verified |
|
||||
| Self-consistency | ✔️ | Batch/streaming/span agree; component identity verified |
|
||||
No external library provides a PGO implementation. Validation uses component identity checks and cross-mode consistency.
|
||||
|
||||
Cross-validation: PGO is verified against manual SMA + ATR computation. Streaming, batch (TBarSeries), and span paths produce identical results within floating-point tolerance ($10^{-10}$).
|
||||
| Check | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| Streaming vs Batch vs Span | ✅ | All three modes agree within 1e-10 |
|
||||
| Constant price | ✅ | Constant OHLC bars produce PGO = 0.0 |
|
||||
| Rising prices | ✅ | Produces positive PGO |
|
||||
| Falling prices | ✅ | Produces negative PGO |
|
||||
| Component identity | ✅ | Manual SMA + ATR computation matches PGO within 1e-10 |
|
||||
| Multi-period consistency | ✅ | Different periods produce distinct results |
|
||||
| Determinism | ✅ | Two identical runs produce bit-identical results |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Key Optimizations
|
||||
|
||||
- **O(1) SMA update**: Running sum with RingBuffer avoids re-scanning the window.
|
||||
- **FMA in ATR EMA**: `Math.FusedMultiplyAdd(alpha, tr - ema, ema)` for the EMA update.
|
||||
- **Precomputed constants**: `_alpha` and `_decay` computed once in constructor.
|
||||
- **Warmup compensation**: Geometric decay factor corrects EMA bias without extra state.
|
||||
- **State copy pattern**: `_s`/`_ps` record struct for zero-allocation bar correction.
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count per bar |
|
||||
|-----------|--------------|
|
||||
| Comparisons | 3 (TR components) |
|
||||
| Additions | 3 (SMA sum, EMA, PGO) |
|
||||
| Subtractions | 2 (oldest SMA removal, close - SMA) |
|
||||
| Multiplications | 1 (warmup compensator) |
|
||||
| FMA calls | 1 (ATR EMA) |
|
||||
| Divisions | 2 (SMA average, PGO normalization) |
|
||||
| Abs | 2 (TR components) |
|
||||
| Max | 2 (TR selection) |
|
||||
|
||||
### SIMD Analysis (Batch Mode)
|
||||
|
||||
| Aspect | Status |
|
||||
|--------|--------|
|
||||
| SMA running sum | Scalar (sequential accumulation) |
|
||||
| TR computation | Scalar (3 comparisons + abs per bar) |
|
||||
| ATR EMA | Scalar (IIR recursion, sequential dependency) |
|
||||
| PGO division | Scalar (data-dependent divisor) |
|
||||
| Vectorization potential | Low — IIR chain and conditional TR prevent SIMD |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Ignoring ATR=0**: Constant-price instruments produce zero ATR. Division by zero must be guarded (returns 0.0).
|
||||
2. **Comparing across periods**: PGO(14) and PGO(50) are not directly comparable. Longer periods smooth more aggressively, producing smaller absolute readings.
|
||||
3. **Using without OHLC data**: PGO requires High/Low/Close for True Range. Feeding only close prices produces TR=0 (synthetic bars with H=L=C), making the oscillator meaningless.
|
||||
4. **Fixed overbought/oversold thresholds**: The ±3.0 levels are guidelines. Fat-tailed distributions (common in finance) produce more extreme readings than Gaussian models suggest.
|
||||
5. **SMA lag in trending markets**: SMA introduces (N-1)/2 bars of lag. In strong trends, PGO may show persistent readings of ±2-4 without mean reversion. This is a feature, not a bug — it confirms trend strength.
|
||||
6. **Warmup period**: PGO requires N bars to fill the SMA buffer and begin producing valid readings. ATR warmup is handled by exponential compensation but converges asymptotically.
|
||||
7. **Not a standalone signal**: PGO measures displacement, not direction. Combine with trend filters (e.g., moving average slope) for directional context.
|
||||
1. **Using without OHLC data.** PGO requires High/Low/Close for True Range. Feeding close-only data produces TR=0, making ATR=0 and PGO=0 (worthless). Always provide full bar data.
|
||||
2. **Fixed thresholds on fat-tailed data.** The ±3.0 levels assume approximately normal displacement. Financial returns have fat tails; PGO can reach ±5 or beyond during tail events. Calibrate thresholds per instrument.
|
||||
3. **Fading strong trends.** PGO at +3.0 in a strong uptrend is a sign of strength, not an automatic sell signal. Combine with trend direction indicators before trading mean reversion.
|
||||
4. **Comparing across different periods.** PGO(14) and PGO(50) are not directly comparable. Longer periods produce smoother, smaller-magnitude readings.
|
||||
5. **Ignoring SMA lag.** SMA introduces (N-1)/2 bars of lag. In fast-moving markets, the SMA component drags the PGO reading, making it slow to register sharp reversals.
|
||||
6. **Assuming ATR is volatility.** ATR measures range, not statistical volatility. It can underestimate risk in markets with large intrabar reversals that end near unchanged.
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Why use ATR instead of standard deviation for normalization?**
|
||||
A: ATR captures gap risk (via True Range's inclusion of the previous close) that standard deviation misses. In overnight-gap-prone instruments, ATR gives a more complete picture of actual price movement than close-to-close standard deviation.
|
||||
|
||||
**Q: What happens when I feed TValue instead of TBar?**
|
||||
A: The `Update(TValue)` overload creates a synthetic bar with H=L=C=value, which produces TR=0. This means ATR decays toward zero and PGO becomes unreliable. Always prefer the `Update(TBar)` overload for meaningful results.
|
||||
|
||||
**Q: How does PGO compare to Bollinger %B?**
|
||||
A: Both normalize price displacement against a volatility measure. Bollinger %B uses standard deviation (Gaussian assumption, ignores gaps) and outputs [0, 1] typically. PGO uses ATR (captures gaps) and outputs unbounded ATR multiples. PGO is more suitable for gapping instruments; %B is more suitable when you want a bounded indicator.
|
||||
|
||||
## References
|
||||
|
||||
- Johnson, M. "Pretty Good Oscillator." Technical analysis community publication.
|
||||
- Wilder, J.W. "New Concepts in Technical Trading Systems." Trend Research, 1978. (ATR foundation)
|
||||
- PineScript reference implementation: `pgo.pine`
|
||||
- Johnson, Mark. "Pretty Good Oscillator." Technical analysis community publication.
|
||||
- Wilder, J. Welles. *New Concepts in Technical Trading Systems*. Trend Research, 1978. (ATR foundation)
|
||||
- [PineScript reference](pgo.pine)
|
||||
|
||||
Reference in New Issue
Block a user