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:
Miha Kralj
2026-02-13 21:44:45 -08:00
parent 951842acca
commit dfeb23bf3d
81 changed files with 13629 additions and 2041 deletions
+178 -139
View File
@@ -1,201 +1,240 @@
# TRIX: Triple Exponential Average Oscillator
> "The best filter is the one that removes what you don't want while keeping what you do." -- Jack Hutson
> "Smooth it once, smooth it twice, smooth it thrice, then ask: is it still moving?" -- Jack Hutson, probably
## Overview
| Property | Value |
|--------------|-------|
| Category | Oscillator |
| Inputs | Source (close) |
| Parameters | `period` (int, default: 14, valid: > 0) |
| Outputs | double (single value) |
| Output range | Unbounded, centered at zero |
| Warmup | `period * 3` bars |
The **Triple Exponential Average Oscillator (TRIX)** measures the percentage rate of change of a triple-smoothed exponential moving average. By passing price through three cascaded EMA stages before computing the rate of change, TRIX eliminates short-term noise that plagues single-EMA oscillators. The result is a zero-centered momentum indicator that responds only to sustained directional moves, making whipsaws from random price fluctuations structurally unlikely.
### Key takeaways
- TRIX measures the percentage rate of change of a triple-smoothed EMA, converting a trend filter into a zero-centered momentum oscillator.
- Primary use: identifying trend direction and momentum via zero-line crossovers.
- Unlike MACD (which uses two EMAs), TRIX applies three layers of smoothing to a single period, producing far fewer false signals in choppy markets.
- The triple smoothing introduces significant lag: roughly 1.5x the period before meaningful signals emerge.
- A flat price series produces TRIX = 0 exactly (after warmup), regardless of price level. The indicator is scale-independent.
## Historical Context
Jack Hutson introduced TRIX in the early 1980s in *Stocks & Commodities* magazine. The core insight was simple: a single EMA still tracks noise. Running it through three smoothing passes produces a curve so smooth that its first derivative (rate of change) reliably identifies trend direction without the lag-versus-responsiveness tradeoff that haunts simpler oscillators.
Jack Hutson introduced TRIX in a 1983 article for *Technical Analysis of Stocks & Commodities* magazine. The indicator emerged during a period when traders were drowning in noise from shorter-term oscillators (RSI, Stochastic) and needed something that could ignore day-to-day chatter while still detecting genuine trend shifts. Hutson's insight was straightforward: apply EMA three times to remove virtually all short-term oscillation, then take the percentage rate of change to convert the smoothed trend line into an oscillator.
Most implementations use a naive EMA (seed with first value, no compensation), which produces a warmup bias that takes roughly $3 \times \text{period}$ bars to dissipate. QuanTAlib eliminates this artifact using warmup-compensated EMA, yielding accurate values from bar 1.
The name "TRIX" derives from "triple exponential," though this creates frequent confusion with the triple exponential smoothing method (Holt-Winters). They share nothing beyond the word "triple." TRIX is simply `ROC(EMA(EMA(EMA(source))))` -- three nested EMAs followed by a one-period percentage change.
## Architecture
Implementation differences across platforms center on EMA warmup handling. QuanTAlib uses compensated EMA (dividing by `1 - decay^n` during warmup) to match PineScript and Skender behavior. Libraries that use uncompensated EMA (Tulip, for instance) will diverge during the warmup window, and because TRIX multiplies by 100, even small EMA differences of ~1e-6 become ~1e-4 in the final output.
```
Source ──→ CompensatedEMA₁ ──→ CompensatedEMA₂ ──→ CompensatedEMA₃ ──→ ROC% ──→ TRIX
[α smoothing] [α smoothing] [α smoothing] [100×Δ/prev]
```
## What It Measures and Why It Matters
### Streaming (O(1) per bar)
TRIX captures the *acceleration* of trend momentum. Where a single EMA tells you "price is above/below average" and a double EMA reduces noise further, the triple EMA is smooth enough that its rate of change reflects genuine directional commitment rather than random fluctuation. When TRIX crosses above zero, the triple-smoothed trend is accelerating upward. When it crosses below, the trend is decelerating or reversing.
Each EMA stage maintains a raw EMA (`rema`) and a compensation factor (`e`):
This makes TRIX most useful in trending markets where you want to stay on the right side of a move without being whipsawed. In ranging markets, TRIX oscillates near zero with small amplitude -- which is itself useful information (telling you there is no trend to follow). The indicator works best on daily and weekly timeframes; on intraday data, the triple smoothing can delay signals long enough that the move is half-finished before TRIX confirms it.
| Component | Role |
|-----------|------|
| `Rema1/2/3` | Raw recursive EMA accumulators per stage |
| `E1/2/3` | Warmup compensation factors: $e_i = e_i \times (1 - \alpha)$ |
| `PrevEma3` | Previous bar's compensated EMA₃ for rate-of-change calculation |
| `Count` | Bar counter for `IsHot` determination |
### Compensated EMA
During warmup ($e > 10^{-10}$), the compensated value is:
$$
\text{ema}_i = \frac{\text{rema}_i}{1 - e_i}
$$
Once $e_i \leq 10^{-10}$, compensation converges to unity and is bypassed.
### Bar Correction
Uses `_s` / `_ps` state snapshot pair. On `isNew = true`, previous state is saved; on `isNew = false`, state rolls back before recomputing.
### Warmup
`WarmupPeriod = period * 3`. Three cascaded EMA stages each need approximately `period` bars to stabilize.
`IsHot` fires when `Count > period` (the compensation factors make the indicator usable earlier than uncompensated implementations).
The zero-line crossover is the primary signal. Divergence between price and TRIX (price making new highs while TRIX fails to) can precede reversals, though this pattern is less reliable than similar divergences in RSI or MACD because the extra smoothing layer absorbs small momentum shifts that might otherwise produce early warning.
## Mathematical Foundation
### Smoothing coefficient
### Core Formula
TRIX is computed in four steps: three cascaded EMA passes followed by a percentage rate of change.
**Step 1: First EMA**
$$
\alpha = \frac{2}{\text{period} + 1}
\text{EMA1}_t = \alpha \cdot P_t + (1 - \alpha) \cdot \text{EMA1}_{t-1}
$$
### Triple EMA with warmup compensation
For each bar $n$ and each EMA stage $i \in \{1, 2, 3\}$:
**Step 2: Second EMA (smooths EMA1)**
$$
\text{rema}_i[n] = \alpha \cdot x_i[n] + (1 - \alpha) \cdot \text{rema}_i[n-1]
\text{EMA2}_t = \alpha \cdot \text{EMA1}_t + (1 - \alpha) \cdot \text{EMA2}_{t-1}
$$
$$
e_i[n] = e_i[n-1] \cdot (1 - \alpha)
$$
**Step 3: Third EMA (smooths EMA2)**
$$
\text{ema}_i[n] = \frac{\text{rema}_i[n]}{1 - e_i[n]}
\text{EMA3}_t = \alpha \cdot \text{EMA2}_t + (1 - \alpha) \cdot \text{EMA3}_{t-1}
$$
Where $x_1 = \text{source}$, $x_2 = \text{ema}_1$, $x_3 = \text{ema}_2$.
### TRIX output
**Step 4: Percentage rate of change**
$$
\text{TRIX}[n] = 100 \times \frac{\text{ema}_3[n] - \text{ema}_3[n-1]}{\text{ema}_3[n-1]}
\text{TRIX}_t = 100 \times \frac{\text{EMA3}_t - \text{EMA3}_{t-1}}{\text{EMA3}_{t-1}}
$$
When $\text{ema}_3[n-1] = 0$, TRIX returns 0 (division guard).
where:
### FMA optimization
- $P_t$ = source price at bar $t$
- $\alpha = \frac{2}{N + 1}$ = EMA smoothing factor
- $N$ = lookback period (default 14)
Hot-path EMA update uses fused multiply-add:
### Warmup Compensation
Each EMA layer applies bias correction during warmup to match PineScript behavior:
$$
\text{rema} = \text{FMA}(\text{rema}_{\text{prev}}, 1-\alpha, \alpha \cdot x)
\hat{\text{EMA}}_t = \frac{\text{REMA}_t}{1 - (1 - \alpha)^t}
$$
Measured 15-25% speedup over separate multiply-then-add in tight update loops.
where $\text{REMA}_t$ is the raw (uncorrected) recursive EMA. The correction factor $(1 - \alpha)^t$ decays toward zero exponentially; once it falls below $10^{-10}$, compensation is bypassed and the raw value is used directly.
## Performance Profile
### Parameter Mapping
| Metric | Value |
|--------|-------|
| Time complexity | O(1) per bar (streaming) |
| Space complexity | O(1) (no buffers, scalar state only) |
| Allocations | Zero per update |
| NaN handling | Last valid value substitution |
| SIMD | Span-based `Batch()` with scalar fallback (recursive dependency prevents vectorization) |
| FMA | Yes, in all three EMA stages |
| Parameter | Symbol | Default | Constraint |
|-----------|--------|---------|------------|
| `period` | $N$ | 14 | $N > 0$ |
| Quality Metric | Score (1-10) |
|----------------|-------------|
| Smoothness | 9 |
| Lag | 6 (high smoothing = moderate lag) |
| Noise rejection | 10 |
| Whipsaw resistance | 9 |
| Trend detection | 8 |
### Warmup Period
$$
\text{WarmupPeriod} = 3N
$$
The `IsHot` flag activates when `Count > period` (after the first EMA layer has processed at least $N+1$ bars). The `WarmupPeriod` property reports $3N$ to account for full convergence of all three cascaded EMA passes.
## Architecture & Physics
The implementation uses a three-stage compensated EMA pipeline with state managed in a single `record struct`.
```
Source ──→ [Compensated EMA₁] ──→ [Compensated EMA₂] ──→ [Compensated EMA₃] ──→ ROC% ──→ TRIX
α, decay α, decay α, decay
```
### 1. State Management
All scalar state lives in a single `[StructLayout(LayoutKind.Auto)]` record struct containing:
- `Rema1`, `Rema2`, `Rema3`: raw (uncorrected) EMA accumulators for each stage
- `E1`, `E2`, `E3`: compensation decay trackers (initialized to 1.0, multiplied by `decay` each bar)
- `PrevEma3`: previous corrected EMA3 value for ROC calculation
- `Count`: bar counter for warmup tracking
- `LastValid`: last finite input for NaN/Infinity substitution
The local-copy pattern (`var s = _s; ... _s = s;`) enables JIT struct promotion to registers, measured at 15-25% speedup in tight update loops.
### 2. FMA Optimization
Each EMA recursion uses `Math.FusedMultiplyAdd` for the IIR update:
```
Rema = FMA(Rema, decay, α × input)
```
This computes `Rema × decay + α × input` in a single fused operation, eliminating one intermediate rounding step and providing ~5% throughput improvement on hardware with FMA support.
### 3. Edge Cases
- **NaN/Infinity inputs**: Substituted with `LastValid` (or 0.0 if no valid input has been seen). Output remains finite.
- **Division by zero**: When `|PrevEma3| < 1e-10`, TRIX outputs 0.0 instead of computing the percentage change.
- **First bar**: All three EMA stages initialize to the input value; TRIX outputs 0.0 (no prior EMA3 to compare against).
- **Bar correction**: `isNew=false` rolls back to `_ps` (previous state snapshot), enabling same-bar overwrites without accumulating drift.
## Interpretation and Signals
### Signal Zones
| Zone | Condition | Interpretation |
|------|-----------|----------------|
| Bullish | TRIX > 0 | Triple-smoothed trend is accelerating upward |
| Neutral | TRIX ≈ 0 | No sustained directional momentum |
| Bearish | TRIX < 0 | Triple-smoothed trend is accelerating downward |
### Signal Patterns
- **Zero-line crossover**: TRIX crossing from negative to positive signals a potential bullish trend. The triple smoothing means this signal fires less often than single-EMA crossovers, but with higher reliability. Best confirmed with volume or price action.
- **Divergence**: Price making new highs while TRIX makes lower highs suggests weakening momentum. More sluggish to detect than RSI divergence due to the extra smoothing, but when TRIX diverges, the signal carries weight precisely because it takes significant momentum change to move the triple-smoothed output.
- **Signal line crossover**: A 9-period EMA of TRIX (computed externally) can serve as a signal line, similar to the MACD signal line. Buy when TRIX crosses above its signal line; sell on the reverse.
### Practical Notes
TRIX works best on daily charts or higher timeframes where the triple smoothing lag is acceptable. On 5-minute charts with a 14-period TRIX, you are looking at ~42 bars of warmup (3.5 hours) before signals become meaningful. Pair TRIX with a faster oscillator (RSI, Stochastic) for entry timing while using TRIX for directional bias. In strongly trending markets, TRIX staying above/below zero for extended periods is itself confirmatory.
## Related Indicators
- **[EMA](../../trends_IIR/ema/Ema.md)**: The building block. TRIX is the percentage ROC of three cascaded EMAs; understanding EMA bias correction is essential to understanding TRIX warmup behavior.
- **[DEMA](../../trends_IIR/dema/Dema.md)**: Double EMA smoothing -- one layer fewer than TRIX. Faster to react, more prone to noise.
- **[PPO](../../momentum/ppo/Ppo.md)**: Percentage Price Oscillator. Both express momentum as percentages, but PPO uses the spread between two different-period EMAs rather than the ROC of a single triple-smoothed EMA.
- **[MOM](../../momentum/mom/Mom.md)**: Raw momentum (price difference). TRIX can be thought of as a heavily smoothed, percentage-normalized version of momentum.
## Validation
Cross-validated against four independent implementations:
Validated against external libraries in [`Trix.Validation.Tests.cs`](Trix.Validation.Tests.cs). Tests run across multiple periods (5, 9, 10, 14, 20, 25, 50, 100) with self-consistency checks on 10,000-bar datasets.
| Library | Mode | Tolerance | Status | Notes |
|---------|------|-----------|--------|-------|
| Skender | Batch | 1e-9 | Pass | Exact match after warmup |
| Skender | Streaming | 1e-9 | Pass | Bar-by-bar verification |
| Skender | Span | 1e-9 | Pass | Span API consistency |
| TA-Lib | Span | 1e-9 | Pass | Lookback-aligned comparison |
| TA-Lib | Streaming | 1e-9 | Pass | Sequential verification |
| Tulip | Span | 5e-4 | Pass | Compensated vs uncompensated EMA divergence |
| Tulip | Batch | 1e-3 | Pass | Compensation difference accumulates over warmup |
| Tulip | Streaming | 1e-3 | Pass | Same compensation divergence pattern |
| Library | Status | Notes |
|---------|:------:|-------|
| **Skender** | ✓ | `GetTrix(period)`, tolerance 1e-9 |
| **TA-Lib** | ✓ | `Functions.Trix`, tolerance 1e-9 |
| **Tulip** | ✓ | `Indicators.trix`, tolerance 5e-4 to 1e-3 (uncompensated EMA) |
Tulip uses traditional uncompensated EMA. The compensation difference is structural, not a bug. Skender and TA-Lib use compatible warmup handling, producing tight matches.
Tulip uses uncompensated EMA, which diverges from the compensated approach used by QuanTAlib, Skender, and TA-Lib. The 100x multiplication in the ROC step amplifies small EMA differences: a ~1e-6 EMA divergence becomes ~1e-4 in the TRIX output. This is a methodology difference, not an error.
Self-consistency validated across all four API modes (streaming, batch, span, eventing) with exact match verification.
Additional validation tests cover: flat-line behavior (TRIX converges to 0), zero-crossing detection (uptrend/downtrend transitions), NaN/Infinity robustness, large-dataset precision (batch vs streaming match to 1e-9), and period sensitivity verification.
## Performance Profile
### Key Optimizations
- **FMA usage**: All three EMA recursions use `Math.FusedMultiplyAdd(rema, decay, alpha * input)`, eliminating intermediate rounding in the IIR accumulation.
- **Precomputed constants**: `_alpha` and `_decay` are computed once in the constructor and stored as `readonly` fields.
- **Aggressive inlining**: `Update(TValue)`, `Handle`, and `Batch(Span)` are decorated with `[MethodImpl(MethodImplOptions.AggressiveInlining)]`.
- **State local copy**: The `var s = _s` pattern enables JIT register promotion for the entire state struct during `Update`.
- **SkipLocalsInit**: Class-level `[SkipLocalsInit]` avoids zero-initialization of locals in all methods.
### Operation Count (Streaming Mode)
| Operation | Count | Cost (cycles) | Subtotal |
|-----------|------:|:-------------:|:--------:|
| FMA | 3 | 4 | 12 |
| MUL | 3 | 3 | 9 |
| DIV | 4 | 15 | 60 |
| CMP | 4 | 1 | 4 |
| ADD/SUB | 2 | 1 | 2 |
| **Total** | **16** | -- | **~87** |
The three DIVs are warmup compensation divisions (`rema / (1 - e)`); once compensation decays below `1e-10`, these become simple assignments, dropping the steady-state cost to ~27 cycles.
### SIMD Analysis (Batch Mode)
| Operation | Vectorizable? | Reason |
|-----------|:-------------:|--------|
| EMA recursion | No | Each EMA output depends on the previous bar's output (IIR dependency chain) |
| ROC percentage | No | Requires sequential `PrevEma3` from the EMA stage |
| NaN substitution | No | Conditional branching on per-element validity |
TRIX is fully recursive across all three EMA stages plus the ROC step. No SIMD vectorization is possible for the core algorithm. The `Batch(Span)` method uses scalar iteration with FMA acceleration.
## Common Pitfalls
1. **Ignoring warmup bias.** Uncompensated implementations produce startup transients for roughly $3 \times \text{period}$ bars. QuanTAlib's compensation eliminates this, but comparing against uncompensated libraries during warmup will show expected divergence.
1. **Warmup period is 3x the parameter**: With `period=14`, you need 42 bars before the output is fully converged. Pre-warmup values are mathematically valid but reflect initialization bias, not market momentum.
2. **Confusing smoothness with accuracy.** TRIX's triple smoothing means it responds slowly to genuine reversals. A 14-period TRIX effectively has the lag characteristics of a 42-period single EMA applied to rate of change.
2. **Tulip validation tolerance**: Tulip uses uncompensated EMA. Do not expect bit-exact matches. The 100x amplification from the ROC step means tolerance must be 5e-4 or wider, not the usual 1e-9.
3. **Using TRIX as a standalone signal.** Zero-line crossovers are reliable but late. Pair with faster indicators (RSI, price action) for entry timing.
3. **isNew=false overwrites state**: Bar correction replays the current bar without advancing the counter. Failing to pass `isNew=false` for intra-bar updates causes count drift and incorrect warmup detection.
4. **Short periods amplify noise.** Below period 5, the triple-smoothing advantage degrades. The three cascaded EMAs need sufficient period to differentiate signal from noise.
4. **Zero-line crossovers lag real turns**: By the time TRIX crosses zero, the underlying trend change is already well underway. This is a feature (fewer false signals) but means TRIX is not suitable as a standalone entry trigger.
5. **Division-by-zero edge case.** When EMA₃ equals zero (typically only with synthetic data), TRIX returns 0. Production price data never hits this case, but test harnesses should account for it.
5. **Near-zero denominator**: When `PrevEma3` is near zero (prices close to zero or after extended NaN substitution), the percentage ROC can produce extreme spikes. The implementation guards this at `|PrevEma3| < 1e-10`, but assets trading near zero may still produce outsized readings.
6. **Misinterpreting Tulip validation gaps.** The 1e-3 tolerance against Tulip is not imprecision. It reflects the fundamental difference between compensated and uncompensated EMA warmup strategies.
6. **Not scale-bounded**: Unlike RSI (0-100) or Stochastic (0-100), TRIX has no fixed output range. Overbought/oversold thresholds must be calibrated to the specific instrument and timeframe using historical data.
## Usage
## FAQ
```csharp
// Streaming
var trix = new Trix(period: 14);
TValue result = trix.Update(new TValue(time, price));
**Q: Why does my TRIX differ from Tulip by ~0.05%?**
A: QuanTAlib uses warmup-compensated EMA (matching PineScript, Skender, and TA-Lib), while Tulip uses standard uncompensated EMA. The triple-EMA cascade amplifies this small difference, and the 100x ROC multiplier magnifies it further. Both are correct for their respective EMA definitions.
// Event-based chaining
var source = new TSeries();
var trix = new Trix(source, period: 14);
**Q: Can I use TRIX as a signal line for another indicator?**
A: Yes. Subscribe via `indicator.Pub += handler;` to chain TRIX after any `ITValuePublisher`. Common patterns include TRIX of RSI (smoothed momentum of momentum) or TRIX with an external EMA signal line.
// Batch (TSeries)
TSeries results = Trix.Batch(source, period: 14);
// Batch (Span)
Trix.Batch(sourceSpan, outputSpan, period: 14);
// Calculate (returns indicator for state inspection)
var (results, indicator) = Trix.Calculate(source, period: 14);
```
## Interpretation
- **Zero Line Crossovers:**
- TRIX crosses above zero: Triple-smoothed EMA is rising (bullish momentum)
- TRIX crosses below zero: Triple-smoothed EMA is falling (bearish momentum)
- **Signal Line:**
- A short-period EMA of TRIX can serve as a signal line (similar to MACD)
- Crossovers of TRIX above/below its signal line generate trade signals
- **Divergence:**
- Bullish: Price makes lower lows while TRIX makes higher lows
- Bearish: Price makes higher highs while TRIX makes lower highs
- Triple smoothing makes TRIX divergences more reliable than single-EMA divergences
- **Trend Strength:**
- Rising TRIX above zero: Strengthening uptrend
- Falling TRIX below zero: Strengthening downtrend
- TRIX near zero with small oscillations: Sideways/consolidating market
## Parameters
| Parameter | Type | Default | Range | Description |
|-----------|------|---------|-------|-------------|
| `period` | int | 14 | > 0 | EMA period for each of the three smoothing stages |
**Q: What period should I use?**
A: Default 14 works for daily charts. Shorter periods (5-9) increase sensitivity but reintroduce noise that triple smoothing is designed to eliminate. Longer periods (20-50) are useful for weekly charts or for filtering out everything except major trend shifts.
## References
- Jack Hutson, "TRIX - Triple Exponential Smoothing Oscillator," *Technical Analysis of Stocks & Commodities*, 1983
- Jack Hutson, *Charting the Stock Market: The Wyckoff Method*, 1986
- Steven Achelis, *Technical Analysis from A to Z*, 2nd ed., McGraw-Hill, 2001
- PineScript reference: `trix.pine`
- Hutson, J. (1983). "Good TRIX." *Technical Analysis of Stocks & Commodities*, Vol. 1.
- Murphy, J. (1999). *Technical Analysis of the Financial Markets*. New York Institute of Finance. Chapter on oscillators.
- Achelis, S. (2000). *Technical Analysis from A to Z*. McGraw-Hill. TRIX entry.
- [Investopedia: TRIX](https://www.investopedia.com/terms/t/trix.asp) -- accessible overview of TRIX usage and interpretation.