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
+29
View File
@@ -5,8 +5,37 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// Defines the smoothing method applied to the final STC output.
/// </summary>
public enum StcSmoothing { None = 0, Ema = 1, Sigmoid = 2, Digital = 3 }
/// <summary>
/// STC: Schaff Trend Cycle - A cycle oscillator that combines MACD and Stochastic to detect market trends with improved speed and accuracy.
/// </summary>
/// <remarks>
/// The Schaff Trend Cycle (STC), developed by Doug Schaff, is an oscillator that moves between 0 and 100.
/// It identifies market trends and cycles by applying a Stochastic calculation to the MACD line,
/// and then smoothing the result. This results in an indicator that is faster than MACD and smoother than Stochastic.
///
/// Algorithm:
/// 1. Calculate MACD = Exponential Moving Average (Fast) - Exponential Moving Average (Slow).
/// 2. Calculate %K (Stoch K) of the MACD over a specified period.
/// 3. Smooth %K with a fast average to get %D (Stoch D).
/// 4. Re-calculate %K of the %D value (Stoch of Stoch).
/// 5. Smooth the result again to produce the final STC value.
///
/// Properties:
/// - Ranges from 0 to 100.
/// - High values (>75) indicate overbought conditions.
/// - Low values (<25) indicate oversold conditions.
/// - Signals are generated when the indicator crosses these thresholds.
/// - Minimizes false signals found in traditional MACD or Stochastic indicators.
///
/// Key Insight:
/// By performing a double stochastic calculation on the MACD (Stochastic of the Stochastic of MACD),
/// STC emphasizes the cyclic nature of trends while reducing noise.
/// </remarks>
[SkipLocalsInit]
public sealed class Stc : AbstractBase
{
+150 -145
View File
@@ -1,190 +1,195 @@
# STC: Schaff Trend Cycle
> "Because MACD is a trend indicator, it has the same problems as all trend indicators: lag. The STC solves this by using a Cycle component to identify trends faster."
> "By applying the Stochastic twice to MACD, we reveal the cycle hidden within the trend itself."
The Schaff Trend Cycle (STC) is a technical indicator developed by **Doug Schaff** in the 1990s. It combines the trend-following benefits of the **MACD** (Moving Average Convergence Divergence) with the cyclic sensitivity of the **Stochastic Oscillator**. By applying a double-smoothing stochastic process to the MACD line, the STC attempts to identify overbought and oversold conditions with greater accuracy and speed than MACD alone, while minimizing the "whipsaws" common in fast stochastics.
The Schaff Trend Cycle is a cyclometric oscillator that improves upon MACD by passing it through a double-Stochastic process. This recursive normalization detects market cycles with greater speed and accuracy, producing a bounded 0-100 indicator that reaches extremes earlier than MACD while avoiding Stochastic jitter.
## Historical Context
In the late 90s, Doug Schaff sought to solve the pivotal problem of currency trading: trends are profitable, but trend indicators lag. Oscillators are timely, but noisy. Schaff's insight was to treat the specific "trendiness" of price (measured by MACD) as the *source* data for a cycle analysis (Stochastic).
Doug Schaff developed the STC in the 1990s while trading currency markets. He observed that the MACD, while excellent at identifying trends, suffered from lag—by the time it signaled, much of the move had already occurred. Conversely, the Stochastic oscillator was fast but noisy, generating numerous false signals.
The result is a bounded oscillator (0-100) that moves in distinct "regimes": stabilizing at 0 in downtrends, 100 in uptrends, and cycling cleanly between them during reversals. It is particularly noted for its "sigmoid" wave shape, often spending extended time at extremes rather than oscillating sinusoidally.
Schaff's insight was that trends themselves move in cycles. By applying the Stochastic normalization formula recursively to MACD values, he could extract the cyclical phase of the trend. The "Stochastic of a Stochastic" creates a self-normalizing oscillator that converges toward a square wave in steady-state conditions.
The STC found particular popularity in forex trading where its speed advantage over MACD proved valuable in the 24-hour market. The indicator's tendency to "flatline" at extremes (0 or 100) during strong trends—initially seen as a limitation—became recognized as a feature: it signals trend continuation rather than reversal.
## Architecture & Physics
The STC is essentially a **recursive fractal**: it applies the Stochastic formula to the MACD, smoothes the result, and then applies the Stochastic formula *again* to that smoothed result.
The algorithm implements a deep signal processing pipeline with recursive Stochastic normalization.
1. **MACD Foundation**: The core signal is the difference between Fast and Slow EMAs of price.
2. **First Derivative (Stoch #1)**: Normalizes the MACD into a 0-100 range based on its recent range (`Cycle Length`).
3. **Smoothing**: An EMA (typically length 3, factor 0.5) is applied to Stoch #1.
4. **Second Derivative (Stoch #2)**: The Stochastic formula is applied again to the *smoothed Stoch #1*.
5. **Final Smoothing**: The result is smoothed again (or transformed via Sigmoid/Digital logic).
**Step 1: MACD Construction**
This "Stoch of a Stoch of MACD" architecture filters out high-frequency noise while compressing the trend signal into a binary-like wave. The inertia of the double-smoothing creates a "heavy" indicator that resists changing direction until the evidence is overwhelming, reducing false signals.
Fast and slow EMAs generate the trend signal:
### The Smoothing Challenge
$$\alpha_f = \frac{2}{\text{fastLength} + 1}, \quad \alpha_s = \frac{2}{\text{slowLength} + 1}$$
Standard STC uses a simple EMA for smoothing. However, QuanTAlib offers three modes to adapt the signal shape to modern algorithmic needs:
$$\text{EMA}_f = \alpha_f P_t + (1 - \alpha_f)\text{EMA}_{f,t-1}$$
$$\text{EMA}_s = \alpha_s P_t + (1 - \alpha_s)\text{EMA}_{s,t-1}$$
* **EMA (Standard)**: Classic Schaff behavior.
* **Sigmoid**: Applies a logistic function to force values to extremes, creating a "square wave" effect that reduces noise in the middle range (40-60).
* **Digital**: A strict trinary output (0, 100, or Hold) for hard-logic trading systems.
$$\text{MACD}_t = \text{EMA}_f - \text{EMA}_s$$
## Mathematical Foundation
**Step 2: First Stochastic (%K₁)**
The calculation involves a cascade of EMAs and Normalizations.
Normalize MACD within its recent range:
### 1. MACD
$$\%K_1 = 100 \times \frac{\text{MACD}_t - \min(\text{MACD}_{t-k:t})}{\max(\text{MACD}_{t-k:t}) - \min(\text{MACD}_{t-k:t})}$$
$$ \text{MACD} = \text{EMA}(Close, L_{fast}) - \text{EMA}(Close, L_{slow}) $$
**Step 3: First Smoothing (%D₁)**
### 2. First Stochastic (%K1) on MACD
EMA smooth the first Stochastic:
$$ \%K_1 = 100 \times \frac{\text{MACD} - \text{LLV}(\text{MACD}, L_{k})}{\text{HHV}(\text{MACD}, L_{k}) - \text{LLV}(\text{MACD}, L_{k})} $$
$$\%D_1 = \alpha_d \cdot \%K_1 + (1 - \alpha_d) \cdot \%D_{1,t-1}$$
### 3. Smoothed %D1
**Step 4: Second Stochastic (%K₂)**
$$ \%D_1 = \text{EMA}(\%K_1, L_{d}) $$
Apply Stochastic normalization again to %D₁:
### 4. Second Stochastic (%K2) on %D1
$$\%K_2 = 100 \times \frac{\%D_1 - \min(\%D_{1,t-k:t})}{\max(\%D_{1,t-k:t}) - \min(\%D_{1,t-k:t})}$$
$$ \%K_2 = 100 \times \frac{\%D_1 - \text{LLV}(\%D_1, L_{k})}{\text{HHV}(\%D_1, L_{k}) - \text{LLV}(\%D_1, L_{k})} $$
**Step 5: Final Output**
### 5. Final STC Output
Apply selected smoothing method to %K₂:
Depending on `StcSmoothing`:
$$\text{STC}_t = \text{Smooth}(\%K_2)$$
* **None**: $\text{STC} = \%K_2$
* **EMA**: $\text{STC} = \text{EMA}(\%K_2, 3)$
* **Sigmoid**: $\text{STC} = \frac{100}{1 + e^{-0.1 \times (\%K_2 - 50)}}$
* **Digital**:
$$
\text{STC} = \begin{cases}
100 & \text{if } \%K_2 > 75 \\
0 & \text{if } \%K_2 < 25 \\
\text{STC}_{prev} & \text{otherwise}
\end{cases}
$$
Smoothing options: None, EMA, Sigmoid, Digital (threshold-based)
## Performance Profile
STC is computationally intensive due to the multiple layers of history required (MACD history -> Stoch history -> Stoch history).
### Operation Count (Streaming Mode, per Bar)
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | 120 ns/bar | Moderate. Requires valid MACD & Stoch history buffers. |
| **Allocations** | 0 | Zero-allocation in hot path (RingBuffers used). |
| **Complexity** | O(1) | Lookbacks are fixed windows, managed via rolling updates. |
| **Accuracy** | 9/10 | Matches PineScript/Standard implementations precisely. |
| **Timeliness** | 7/10 | Double smoothing induces lag, but Cycle logic compensates. |
| **Smoothness** | 10/10 | Extremely smooth, almost binary oscillatory behavior. |
| Operation | Count | Cost (cycles) | Subtotal |
|-----------|------:|------:|------:|
| FMA | 8 | 5 | 40 |
| MUL | 12 | 4 | 48 |
| ADD/SUB | 20 | 1 | 20 |
| DIV | 4 | 15 | 60 |
| MIN/MAX scan | 2×k | 2 | ~40 |
| Clamp | 4 | 3 | 12 |
| **Total** | — | — | **~220** |
### Complexity Analysis
- **Time:** $O(k)$ per bar for min/max scanning (optimized with incremental tracking)
- **Space:** $O(k)$ — two ring buffers of size kPeriod
- **Latency:** slowLength + kPeriod bars warmup
## Validation
Compared against Skender.Stock.Indicators (Standard EMA mode).
| Library | Status | Notes |
| :--- | :--- | :--- |
| **Pinescript** | ✅ | Core logic matches `stc.pine`. |
| **Skender** | ✅ | Validated against `GetStc(10, 23, 50)`. |
| **TA-Lib** | N/A | Not available in standard TA-Lib. |
|---------|--------|-------|
| Manual Calculation | ✅ Match | Step-by-step pipeline verified |
| TradingView | ✅ Match | Cross-validated against TV implementation |
| Quantower | ✅ Match | `Stc.Quantower.Tests.cs` adapter tests |
## Usage
## Usage & Pitfalls
- **Flatlining Expected:** STC stays at 0 or 100 during strong trends—this is trend continuation, not broken data
- **Cycle Length:** kPeriod ≈ fastLength/2 targets the cycle within the MACD trend
- **Threshold Zones:** Below 25 = oversold, above 75 = overbought
- **Smoothing Modes:** EMA (default), Sigmoid (S-curve), Digital (square wave), None
- **Recursive Dependencies:** Cannot be vectorized with SIMD due to sequential state
- **Square Wave Convergence:** In steady trends, output approaches binary 0/100 behavior
## API
```mermaid
classDiagram
class AbstractBase {
<<abstract>>
+Name string
+WarmupPeriod int
+IsHot bool
+Last TValue
+Update(TValue input, bool isNew) TValue
+Reset() void
}
class Stc {
+IsNew bool
+Stc(int kPeriod, int dPeriod, int fastLength, int slowLength, StcSmoothing smoothing)
+Stc(ITValuePublisher source, int kPeriod, int dPeriod, int fastLength, int slowLength, StcSmoothing smoothing)
+Update(TValue input, bool isNew) TValue
+Update(TSeries source) TSeries
+Prime(ReadOnlySpan~double~ source, TimeSpan? step) void
+Reset() void
+Calculate(TSeries source, int kPeriod, int dPeriod, int fastLength, int slowLength, StcSmoothing smoothing)$ TSeries
+Calculate(ReadOnlySpan~double~ source, Span~double~ output, ...)$ void
}
class StcSmoothing {
<<enumeration>>
None
Ema
Sigmoid
Digital
}
AbstractBase <|-- Stc
Stc ..> StcSmoothing
```
### Class: `Stc`
Schaff Trend Cycle oscillator with configurable smoothing.
### Properties
| Name | Type | Description |
|------|------|-------------|
| `IsHot` | `bool` | True after warmup complete |
| `IsNew` | `bool` | Whether last update was a new bar |
| `Last` | `TValue` | Most recent STC output (0-100) |
### Methods
| Name | Returns | Description |
|------|---------|-------------|
| `Update(TValue, bool)` | `TValue` | Updates state with new price value |
| `Calculate(TSeries, ...)` | `TSeries` | Static factory with all parameters |
| `Calculate(span, span, ...)` | `void` | Zero-allocation span-based calculation |
| `Reset()` | `void` | Clears all internal state |
## C# Example
```csharp
using QuanTAlib;
// 1. Standard STC (K=10, D=3, Fast=23, Slow=50, Sigmoid Smoothing)
var stc = new Stc(kPeriod: 10, dPeriod: 3, fastLength: 23, slowLength: 50, smoothing: StcSmoothing.Sigmoid);
// Create STC with standard parameters
var stc = new Stc(
kPeriod: 10, // Stochastic lookback
dPeriod: 3, // Smoothing period
fastLength: 23, // Fast EMA for MACD
slowLength: 50, // Slow EMA for MACD
smoothing: StcSmoothing.Ema
);
// 2. Feed data
stc.Update(new TValue(time, price));
// 3. Access result
double value = stc.Last.Value;
// 4. Chain from another indicator
var macd = new Macd(26, 50, 9);
var stcFromMacd = new Stc(source: macd, kPeriod: 10, dPeriod: 3);
```
## C# Implementation Considerations
### Dual RingBuffer Architecture
The implementation uses two `RingBuffer` instances to track rolling windows of MACD values and first-stage Stochastic values. This enables O(1) min/max updates in most cases, avoiding full window scans on every bar.
```csharp
private readonly RingBuffer _macdBuf;
private readonly RingBuffer _stoch1Buf;
```
### Incremental Min/Max Updates
The `UpdateMinMax` method implements an optimized algorithm that:
- **Expands** min/max immediately when a new value exceeds boundaries
- **Contracts** lazily only when the removed value was the extremum
- Falls back to a full scan only when necessary (removed value matched min or max)
This approach reduces O(n) scans to O(1) for expanding markets and typical mid-range removals.
### State Struct with Sequential Layout
All scalar state is packed into a `[StructLayout(LayoutKind.Sequential)]` struct for cache-friendly access:
```csharp
private struct State
// Process price data
foreach (var bar in bars)
{
public double FastEma;
public double SlowEma;
public double Stoch1Ema;
public double Stoch2Ema;
public double PrevStc;
public double LastFiniteInput;
public bool HasFiniteInput;
public double MacdMin;
public double MacdMax;
public double Stoch1Min;
public double Stoch1Max;
var result = stc.Update(new TValue(bar.Time, bar.Close));
if (stc.IsHot)
{
double value = result.Value;
// Signal interpretation
if (value > 75)
Console.WriteLine("Overbought zone");
else if (value < 25)
Console.WriteLine("Oversold zone");
// Note: Flatlining at 0 or 100 indicates strong trend
if (value == 100)
Console.WriteLine("Strong uptrend continuation");
else if (value == 0)
Console.WriteLine("Strong downtrend continuation");
}
}
// Static calculation with different smoothing
var results = Stc.Calculate(
prices,
kPeriod: 10,
dPeriod: 3,
fastLength: 23,
slowLength: 50,
smoothing: StcSmoothing.Digital // Square wave output
);
```
### FusedMultiplyAdd for EMA Smoothing
All EMA calculations use `Math.FusedMultiplyAdd` for hardware-optimized precision:
```csharp
fastEma = Math.FusedMultiplyAdd(_fastAlpha, x - fastEma, fastEma);
slowEma = Math.FusedMultiplyAdd(_slowAlpha, x - slowEma, slowEma);
```
This pattern `FMA(alpha, x - ema, ema)` computes `ema + alpha * (x - ema)` in a single fused operation.
### Bar Correction via State Snapshot
The `_s` / `_ps` pattern enables bar correction when `isNew=false`:
```csharp
if (isNew) _ps = _s; // snapshot before mutation
else _s = _ps; // rollback to previous state
```
RingBuffer contents are also corrected via `UpdateNewest()` rather than `Add()`.
### Multiple Smoothing Modes
The final output stage supports four smoothing algorithms via the `StcSmoothing` enum:
- **EMA**: Standard exponential smoothing
- **Sigmoid**: Logistic transform `100 / (1 + exp(-0.1 * (x - 50)))`
- **Digital**: Trinary output (0/100/hold) with hysteresis zones at 25/75
- **None**: Raw second-stage Stochastic value
### Static Calculate for Batch Processing
The `Calculate(ReadOnlySpan<double>, Span<double>, ...)` method provides allocation-free batch computation using local array buffers instead of RingBuffers, suitable for backtesting scenarios.
### Memory Efficiency
- **Two RingBuffers**: `2 × kPeriod × 8` bytes (~160 bytes for default k=10)
- **State struct**: ~88 bytes of scalar values
- **Total per instance**: ~250 bytes typical