- 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.
6.8 KiB
STC: Schaff Trend Cycle
"By applying the Stochastic twice to MACD, we reveal the cycle hidden within the trend itself."
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
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.
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 algorithm implements a deep signal processing pipeline with recursive Stochastic normalization.
Step 1: MACD Construction
Fast and slow EMAs generate the trend signal:
\alpha_f = \frac{2}{\text{fastLength} + 1}, \quad \alpha_s = \frac{2}{\text{slowLength} + 1}
\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}
\text{MACD}_t = \text{EMA}_f - \text{EMA}_s
Step 2: First Stochastic (%K₁)
Normalize MACD within its recent range:
\%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})}
Step 3: First Smoothing (%D₁)
EMA smooth the first Stochastic:
\%D_1 = \alpha_d \cdot \%K_1 + (1 - \alpha_d) \cdot \%D_{1,t-1}
Step 4: Second Stochastic (%K₂)
Apply Stochastic normalization again to %D₁:
\%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})}
Step 5: Final Output
Apply selected smoothing method to %K₂:
\text{STC}_t = \text{Smooth}(\%K_2)
Smoothing options: None, EMA, Sigmoid, Digital (threshold-based)
Performance Profile
Operation Count (Streaming Mode, per Bar)
| 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
| Library | Status | Notes |
|---|---|---|
| Manual Calculation | ✅ Match | Step-by-step pipeline verified |
| TradingView | ✅ Match | Cross-validated against TV implementation |
| Quantower | ✅ Match | Stc.Quantower.Tests.cs adapter tests |
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
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
using QuanTAlib;
// 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
);
// Process price data
foreach (var bar in bars)
{
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
);