Add Choppiness Index (CHOP) implementation and tests

- Implemented ChopIndicator for Quantower with configurable period and cold value display.
- Created Chop class for calculating the Choppiness Index with detailed documentation.
- Added comprehensive unit tests for Chop functionality, covering various market conditions and edge cases.
- Developed markdown documentation for CHOP, detailing its historical context, mathematical foundation, and usage examples.
- Established a remediation plan for channel indicators documentation, identifying gaps and prioritizing updates.
This commit is contained in:
Miha Kralj
2026-02-05 19:42:49 -08:00
parent 95838a6435
commit 26280ce80b
73 changed files with 8485 additions and 5254 deletions
+86 -182
View File
@@ -2,240 +2,144 @@
> "Remove the trend, reveal the cycles."
The Detrended Synthetic Price (DSP) indicator, developed by John Ehlers, is a cycle analysis tool that removes trend components to expose underlying price cycles. By differencing two exponential moving averages (fast and slow), DSP creates a zero-centered oscillator that highlights momentum shifts.
The Detrended Synthetic Price (DSP) indicator creates a zero-centered oscillator by subtracting a half-cycle EMA from a quarter-cycle EMA. Developed by John Ehlers, this "synthetic" price highlights underlying cyclical movement, identifying momentum shifts when the faster EMA crosses the slower one.
## Historical Context
John Ehlers introduced the Detrended Synthetic Price as part of his cycle analysis toolkit. The indicator builds on the MACD concept but uses EMA periods derived from cycle theory: quarter-cycle (fast) and half-cycle (slow) lengths. This mathematical relationship helps isolate cycle components while suppressing trend noise.
John Ehlers introduced the DSP as part of his research into cycle analytics for traders. While many indicators (like MACD) use arbitrary periods (12/26), DSP is grounded in cycle theory. Ehlers posits that to effectively isolate a cycle, one should filter data based on the dominant cycle period.
The "synthetic" in the name refers to how DSP synthesizes a detrended view of price by subtracting the slower-reacting EMA from the faster one. When the fast EMA exceeds the slow EMA, price momentum is bullish; when below, momentum is bearish.
Unlike traditional oscillators that bound between fixed levels, DSP oscillates around zero with amplitude proportional to price volatility and cycle strength.
The use of period/4 and period/2 roughly corresponds to extracting the cycle's momentum while cancelling out longer-term trends. This makes DSP particularly effective for cycle-based trading strategies.
## Architecture & Physics
DSP uses dual EMA smoothing with bias correction during warmup to produce accurate values from the first bar.
DSP utilizes a dual EMA architecture, calibrated to specific fractions of the cycle period.
### Core Components
1. **Period Parameter**: Base cycle length (default 40)
2. **Fast EMA**: Smoothing with period = max(2, round(period/4)) - quarter cycle
3. **Slow EMA**: Smoothing with period = max(3, round(period/2)) - half cycle
4. **Bias Correction**: Warmup decay factors eliminate EMA initialization bias
5. **State Record**: Maintains EMA values and warmup factors for rollback support
### Period Derivation
For period = 40:
- Fast period = max(2, round(40/4)) = 10
- Slow period = max(3, round(40/2)) = 20
For period = 4 (minimum):
- Fast period = max(2, round(4/4)) = max(2, 1) = 2
- Slow period = max(3, round(4/2)) = max(3, 2) = 3
### Calculation Flow
For each update:
1. Calculate fast alpha: $\alpha_f = 2 / (p_f + 1)$
2. Calculate slow alpha: $\alpha_s = 2 / (p_s + 1)$
3. Update fast EMA with bias correction
4. Update slow EMA with bias correction
5. DSP = corrected_fast_ema - corrected_slow_ema
## Mathematical Foundation
### EMA Alpha Calculation
### 1. Component Periods
$$
\alpha = \frac{2}{period + 1}
P_{fast} = \max(2, \text{round}(P / 4))
$$
For fast period 10: $\alpha_f = \frac{2}{11} \approx 0.1818$
For slow period 20: $\alpha_s = \frac{2}{21} \approx 0.0952$
### EMA Update (with bias correction)
The raw EMA recursion:
$$
EMA^{raw}_t = \alpha \cdot P_t + (1 - \alpha) \cdot EMA^{raw}_{t-1}
P_{slow} = \max(3, \text{round}(P / 2))
$$
The warmup decay factor tracks bias:
### 2. Alpha Coefficients
$$
e_t = (1 - \alpha) \cdot e_{t-1}
\alpha_{fast} = \frac{2}{P_{fast} + 1}
$$
Starting with $e_0 = 1$, this converges to 0 as the EMA warms up.
The bias-corrected EMA:
$$
EMA_t = \frac{EMA^{raw}_t}{1 - e_t}
\alpha_{slow} = \frac{2}{P_{slow} + 1}
$$
### DSP Formula
### 3. EMA Updates (with Bias Correction)
$$
DSP_t = EMA^{fast}_t - EMA^{slow}_t
EMA_{raw} = \alpha \cdot Price + (1 - \alpha) \cdot EMA_{raw\_prev}
$$
where both EMAs are bias-corrected.
$$
EMA_{corrected} = \frac{EMA_{raw}}{1 - (1-\alpha)^n}
$$
### Properties
### 4. DSP Calculation
- **Range**: Unbounded, oscillates around zero
- **Zero Crossing**: Indicates momentum shift
- **Positive Values**: Fast EMA > Slow EMA (bullish momentum)
- **Negative Values**: Fast EMA < Slow EMA (bearish momentum)
- **Warmup**: IsHot when $e_{slow} < 0.05$ (5% remaining bias)
### Example Calculation
For period = 40 with constant price 100:
After warmup, both EMAs converge to 100:
- Fast EMA = 100
- Slow EMA = 100
- DSP = 100 - 100 = 0
For uptrend (price rising steadily):
- Fast EMA responds quicker, stays closer to current price
- Slow EMA lags behind
- DSP > 0 (positive momentum)
$$
DSP = EMA_{fast} - EMA_{slow}
$$
## Performance Profile
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | ~8 ns/bar | O(1) constant time |
| **Allocations** | 0 | Zero-allocation in hot path |
| **Complexity** | O(1) | Fixed operations per update |
| **Accuracy** | 10 | Exact EMA with bias correction |
### Operation Count (Streaming Mode, per Bar)
### Operation Count (per update)
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| FMA (EMA updates) | 2 | 4 | 8 |
| MUL (decay factors) | 2 | 3 | 6 |
| DIV (bias correction) | 2 | 15 | 30 |
| SUB (DSP = fast - slow) | 1 | 1 | 1 |
| **Total** | **7** | — | **~45 cycles** |
| Operation | Count | Notes |
| :--- | :---: | :--- |
| ADD/SUB | ~6 | EMA updates and DSP calculation |
| MUL | ~6 | Alpha multiplications |
| DIV | 2 | Bias correction divisions |
| FMA | 4 | Fused multiply-add for EMA |
### Complexity Analysis
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 10/10 | Exact EMA with bias correction |
| **Timeliness** | 8/10 | Faster than traditional MACD |
| **Overshoot** | 7/10 | EMA smoothing reduces overshoot |
| **Smoothness** | 8/10 | Dual EMA provides good smoothing |
- **Streaming:** O(1) per bar—fixed calculation depth
- **Memory:** O(1)—only EMA state variables
- **Warmup:** ~2 × slow period for convergence
## Validation
| Library | Status | Notes |
| :--- | :--- | :--- |
| **TA-Lib** | N/A | Not available in TA-Lib |
| **Skender** | N/A | Not available in Skender |
| **Tulip** | N/A | Not available in Tulip |
| **PineScript** | ✅ | Validated against original DSP implementation |
| :--- | :---: | :--- |
| TA-Lib | N/A | Not standard |
| Skender | N/A | Not standard |
| PineScript | ✅ | Matches Ehlers' reference logic |
DSP is validated through mathematical properties:
- Constant price produces zero DSP
- Uptrend produces positive DSP
- Downtrend produces negative DSP
- Oscillates around zero for cyclic price patterns
## Usage & Pitfalls
## Common Pitfalls
- **Zero crossing** indicates cycle phase change—above zero is bullish, below zero is bearish
- **Period should match market cycle**—if market cycle is 20 bars, use period 20 not 40
- **Not normalized**—amplitude reflects absolute price difference, varies by asset
- **Whipsaws** occur in ranging markets with cycles shorter than the setting
- **Divergence** (higher price highs with lower DSP highs) suggests cycle energy loss
- **Use FusedMultiplyAdd** for optimal precision in EMA recursion
1. **Period Selection**: The period parameter represents the dominant cycle length. Use half the detected cycle period for optimal results. Default 40 works for daily data.
## API
2. **Comparison to MACD**: DSP differs from MACD in period derivation. MACD uses arbitrary 12/26 periods; DSP uses cycle-theory-based period/4 and period/2.
```mermaid
classDiagram
class Dsp {
+int Period
+double Value
+bool IsHot
+Dsp(int period)
+Dsp(ITValuePublisher source, int period)
+TValue Update(TValue input, bool isNew)
+void Reset()
}
```
3. **Warmup Behavior**: DSP includes bias correction, so early values are usable. IsHot indicates when the slow EMA bias drops below 5%.
### Class: `Dsp`
4. **Amplitude Interpretation**: DSP amplitude scales with price level. A $1 stock and $100 stock with identical percentage moves will have 100x different DSP amplitudes.
| Parameter | Type | Default | Range | Description |
| :--- | :--- | :--- | :--- | :--- |
| `period` | `int` | `40` | `≥4` | Dominant cycle period |
5. **Zero Crossings**: Not all zero crossings are tradeable. Use in conjunction with cycle analysis or additional confirmation.
### Properties
6. **Trending Markets**: In strong trends, DSP stays positive or negative for extended periods. Cycle analysis is most effective in ranging markets.
- `Value` (`double`): The current DSP value (oscillates around 0)
- `IsHot` (`bool`): Returns `true` when warmup is complete
## Usage
### Methods
- `Update(TValue input, bool isNew)`: Updates the indicator with a new data point
## C# Example
```csharp
using QuanTAlib;
// Create a 40-period DSP indicator
// Create DSP for a 40-bar cycle
var dsp = new Dsp(period: 40);
// Update with new values
var result = dsp.Update(new TValue(DateTime.UtcNow, 100.0));
// Update with streaming data
foreach (var bar in quotes)
{
var result = dsp.Update(new TValue(bar.Date, bar.Close));
if (dsp.IsHot)
{
Console.WriteLine($"{bar.Date}: DSP = {result.Value:F4}");
// Cycle phase detection
if (result.Value > 0)
Console.WriteLine(" → Bullish cycle phase");
else
Console.WriteLine(" → Bearish cycle phase");
}
}
// Access the last calculated DSP value
Console.WriteLine($"DSP: {dsp.Last.Value}");
// Chained usage
var source = new TSeries();
var dspChained = new Dsp(source, period: 40);
// Static batch calculation
var output = Dsp.Calculate(source, period: 40);
// Span-based calculation
Span<double> outputSpan = stackalloc double[source.Count];
Dsp.Batch(source.Values, outputSpan, period: 40);
// Batch calculation
var output = Dsp.Calculate(sourceSeries, period: 40);
```
## Applications
### Cycle Detection
DSP zero crossings help identify cycle turning points:
- DSP crosses above zero: cycle trough (potential buy)
- DSP crosses below zero: cycle peak (potential sell)
### Trend Filtering
Use DSP sign to filter trades with trend direction:
- DSP > 0: Only take long trades
- DSP < 0: Only take short trades
### Momentum Confirmation
DSP slope confirms momentum strength:
- Rising DSP: Increasing bullish momentum
- Falling DSP: Increasing bearish momentum
### Divergence Analysis
Like other oscillators, DSP divergences signal potential reversals:
- Price higher high, DSP lower high: bearish divergence
- Price lower low, DSP higher low: bullish divergence
## Comparison to Related Indicators
### DSP vs MACD
| Feature | DSP | MACD |
| :--- | :--- | :--- |
| Period basis | Cycle theory (P/4, P/2) | Arbitrary (12, 26) |
| Signal line | None (optional) | 9-period EMA |
| Bias correction | Yes | No |
| Histogram | No | Yes (MACD - Signal) |
### DSP vs Detrended Price Oscillator (DPO)
| Feature | DSP | DPO |
| :--- | :--- | :--- |
| Calculation | Fast EMA - Slow EMA | Price - SMA shifted |
| Time alignment | Current | Shifted back period/2 + 1 |
| Leading/Lagging | Leading | Centered (neither) |
## References
- Ehlers, J.F. (2001). *Rocket Science for Traders*. Wiley.
- Ehlers, J.F. (2004). *Cybernetic Analysis for Stocks and Futures*. Wiley.
- TradingView PineScript: DSP implementation in cycle analysis scripts.