- 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.
4.4 KiB
HOMOD: Homodyne Discriminator
"The homodyne discriminator reveals instantaneous frequency by multiplying a signal with its delayed self — the phase rotation between samples directly encodes the cycle period."
The Homodyne Discriminator (HOMOD) estimates the dominant cycle period of a market using homodyne mixing—multiplying the signal by a delayed version of itself. This technique exposes the angular phase change between bars, allowing calculation of the instantaneous period at every time step.
Historical Context
In Rocket Science for Traders and Cybernetic Analysis for Stocks and Futures, John Ehlers introduced signal processing concepts novel to technical analysis. The Homodyne Discriminator was presented as a superior alternative to the Hilbert Transform Discriminator for cycle measurement.
It offers better noise rejection and stability while maintaining reasonable responsiveness, making it practical for real-time trading applications.
Architecture & Physics
The algorithm is a complex pipeline of filters and transformations designed to isolate the analytic signal.
1. Pre-Processing (4-Bar WMA)
Smooth = \frac{4P_t + 3P_{t-1} + 2P_{t-2} + P_{t-3}}{10}
2. Analytic Signal Generation
In-Phase (I) and Quadrature (Q) components via Hilbert Transform:
I_2 = I_1 - JQ
Q_2 = Q_1 + JI
Smoothed with EMA (α = 0.2).
3. Homodyne Mixing
Multiplying complex signal z_t by its conjugate delayed by one bar:
Real = (I_2 \cdot I_{2,prev}) + (Q_2 \cdot Q_{2,prev})
Imag = (I_2 \cdot Q_{2,prev}) - (Q_2 \cdot I_{2,prev})
4. Period Extraction
\theta = \operatorname{atan2}(Imag, Real)
Period = \frac{2\pi}{\theta}
Clamped to [MinPeriod, MaxPeriod] and smoothed.
Performance Profile
Operation Count (Streaming Mode, per Bar)
| Operation | Count | Cost (cycles) | Subtotal |
|---|---|---|---|
| MUL (Hilbert taps) | 14 | 3 | 42 |
| MUL (homodyne mix) | 4 | 3 | 12 |
| ADD/SUB | 20 | 1 | 20 |
| ATAN2 | 1 | 25 | 25 |
| DIV | 2 | 15 | 30 |
| Total | 41 | — | ~129 cycles |
Complexity Analysis
- Streaming: O(1) per bar—fixed filter depth
- Memory: O(1)—state struct with history variables
- Warmup: ~2 × MaxPeriod bars for convergence
Validation
| Library | Status | Notes |
|---|---|---|
| TA-Lib | N/A | Not implemented |
| Skender | N/A | Not implemented |
| PineScript | ✅ | Matches Ehlers' reference code |
Usage & Pitfalls
- Output is period in bars—not an oscillator like RSI, but a measurement like ATR
- Long settling time (~2 × MaxPeriod)—early values unreliable
- Trending markets make "cycle" ill-defined—period drifts to MaxPeriod
- Check for cycling (ADX or trend filter) before trusting period values
- High noise causes jitter—pre-smooth extremely noisy data
- Use for adaptive tuning:
Stochastic(length: homod.DominantCycle)
API
classDiagram
class Homod {
+double MinPeriod
+double MaxPeriod
+double DominantCycle
+bool IsHot
+Homod(double minPeriod, double maxPeriod)
+Homod(ITValuePublisher source, double minPeriod, double maxPeriod)
+TValue Update(TValue input, bool isNew)
+void Reset()
}
Class: Homod
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
minPeriod |
double |
6.0 |
>0 |
Minimum period to detect |
maxPeriod |
double |
50.0 |
>minPeriod |
Maximum period to detect |
Properties
DominantCycle(double): Current dominant cycle period in barsIsHot(bool): Returnstruewhen warmup is complete
Methods
Update(TValue input, bool isNew): Updates the indicator with a new data point
C# Example
using QuanTAlib;
// Configure for cycles between 6 and 50 bars
var homod = new Homod(minPeriod: 6, maxPeriod: 50);
// Update with streaming data
foreach (var bar in quotes)
{
var result = homod.Update(new TValue(bar.Date, bar.Close));
if (homod.IsHot)
{
double period = homod.DominantCycle;
Console.WriteLine($"{bar.Date}: Dominant Cycle = {period:F1} bars");
// Use cycle to tune Stochastic
int adaptiveLength = (int)Math.Round(period);
var adaptiveStoch = new Stochastic(adaptiveLength);
}
}
// Batch calculation
var output = Homod.Calculate(sourceSeries, minPeriod: 6, maxPeriod: 50);