mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 12:08:05 +00:00
Refactor indicators to include "Ehlers" in names and descriptions for clarity
- Updated the name and description of the Hilbert Trendline (HTIT) to "Ehlers Hilbert Transform Instantaneous Trend (HTIT)". - Changed the name and description of the MESA Adaptive Moving Average (MAMA) to "Ehlers MESA Adaptive Moving Average". - Modified the Center of Gravity (CG) indicator to "Ehlers Center of Gravity (CG)". - Renamed the Detrended Synthetic Price (DSP) to "Ehlers Detrended Synthetic Price (DSP)". - Updated the Autocorrelation Periodogram (EACP) to "Ehlers Autocorrelation Periodogram (EACP)". - Changed the Homodyne Discriminator (HOMOD) to "Ehlers Homodyne Discriminator (HOMOD)". - Updated the Hilbert Transform Dominant Cycle Period and Phase indicators to include "Ehlers" in their names. - Renamed the Hilbert Transform Phasor Components to "Ehlers Hilbert Transform Phasor Components (HT_PHASOR)". - Updated the SineWave indicator to "Ehlers Hilbert Transform SineWave (HT_SINE)". - Changed the Phasor Analysis indicator to "Ehlers Hilbert Transform Phasor Components (HT_PHASOR)". - Updated the SSF-Based Detrended Synthetic Price to "Ehlers SSF Detrended Synthetic Price (SSFDSP)". - Renamed the Ultimate Channel to "Ehlers Ultimate Channel (UCHANNEL)". - Added new indicators: Moving Average Variable Period (MAVP), Ehlers Predictive Moving Average (PMA), Ehlers Reverse EMA (REVERSEEMA), and Ehlers Trendflex Indicator (TRENDFLEX). - Updated various SVG badges to reflect changes in classes, comments, source files, lines of code, methods, and public types.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class DecoIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DecoIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new DecoIndicator();
|
||||
|
||||
Assert.Equal(30, indicator.ShortPeriod);
|
||||
Assert.Equal(60, indicator.LongPeriod);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("DECO - Ehlers Decycler Oscillator", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecoIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new DecoIndicator { ShortPeriod = 10, LongPeriod = 20 };
|
||||
|
||||
Assert.Equal(0, DecoIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecoIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new DecoIndicator { ShortPeriod = 10, LongPeriod = 30 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("DECO", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("30", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecoIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new DecoIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Deco.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecoIndicator_Initialize_CreatesInternalDeco()
|
||||
{
|
||||
var indicator = new DecoIndicator { ShortPeriod = 5, LongPeriod = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecoIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new DecoIndicator { ShortPeriod = 5, LongPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecoIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new DecoIndicator { ShortPeriod = 5, LongPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Add a new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
|
||||
var newArgs = new UpdateArgs(UpdateReason.NewBar);
|
||||
indicator.ProcessUpdate(newArgs);
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecoIndicator_ProcessUpdate_DifferentSources()
|
||||
{
|
||||
foreach (SourceType source in new[] { SourceType.Close, SourceType.Open, SourceType.High, SourceType.Low })
|
||||
{
|
||||
var indicator = new DecoIndicator { ShortPeriod = 5, LongPeriod = 10, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(value), $"Source {source} produced non-finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecoIndicator_Reinitialize_ResetsState()
|
||||
{
|
||||
var indicator = new DecoIndicator { ShortPeriod = 5, LongPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Re-initialize should reset
|
||||
indicator.Initialize();
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class DecoIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Short Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int ShortPeriod { get; set; } = 30;
|
||||
|
||||
[InputParameter("Long Period", sortIndex: 2, 2, 2000, 1, 0)]
|
||||
public int LongPeriod { get; set; } = 60;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput(sortIndex: 3)]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Deco _deco = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"DECO ({ShortPeriod},{LongPeriod})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/deco/Deco.Quantower.cs";
|
||||
|
||||
public DecoIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "DECO - Ehlers Decycler Oscillator";
|
||||
Description = "Ehlers' Decycler Oscillator isolates intermediate cycles via dual HP filters";
|
||||
|
||||
_series = new LineSeries("DECO", Color.Yellow, 2, LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_deco = new Deco(ShortPeriod, LongPeriod);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var priceSelector = Source.GetPriceSelector();
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double price = priceSelector(item);
|
||||
|
||||
TValue input = new(item.TimeLeft, price);
|
||||
TValue result = _deco.Update(input, args.IsNewBar());
|
||||
|
||||
if (!_deco.IsHot && !ShowColdValues)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_series.SetValue(result.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class DecoTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
// ── A) Constructor validation ──
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectly()
|
||||
{
|
||||
var deco = new Deco();
|
||||
Assert.Equal("Deco(30,60)", deco.Name);
|
||||
Assert.Equal(30, deco.ShortPeriod);
|
||||
Assert.Equal(60, deco.LongPeriod);
|
||||
Assert.Equal(60, deco.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters_SetsCorrectly()
|
||||
{
|
||||
var deco = new Deco(shortPeriod: 10, longPeriod: 40);
|
||||
Assert.Equal("Deco(10,40)", deco.Name);
|
||||
Assert.Equal(10, deco.ShortPeriod);
|
||||
Assert.Equal(40, deco.LongPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroShortPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Deco(shortPeriod: 0));
|
||||
Assert.Equal("shortPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeShortPeriod_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Deco(shortPeriod: -1));
|
||||
Assert.Equal("shortPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_LongNotGreaterThanShort_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Deco(shortPeriod: 30, longPeriod: 30));
|
||||
Assert.Equal("longPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_LongLessThanShort_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Deco(shortPeriod: 30, longPeriod: 20));
|
||||
Assert.Equal("longPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
// ── B) Basic calculation ──
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsFiniteValue()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
TValue result = default;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
result = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
||||
}
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_MatchesReturnValue()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
var result = deco.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(result.Value, deco.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Name_AccessibleAfterUpdate()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
_ = deco.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Contains("Deco", deco.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstTwoBars_ReturnZero()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
var r0 = deco.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
var r1 = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(1), 101.0));
|
||||
Assert.Equal(0.0, r0.Value);
|
||||
Assert.Equal(0.0, r1.Value);
|
||||
}
|
||||
|
||||
// ── C) State + bar correction ──
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_True_AdvancesState()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
var r1 = deco.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
||||
var r2 = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(1), 101.0), isNew: true);
|
||||
Assert.True(double.IsFinite(r1.Value));
|
||||
Assert.True(double.IsFinite(r2.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_False_RewritesLastBar()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
deco.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), isNew: true);
|
||||
}
|
||||
|
||||
var before = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(10), 120.0), isNew: true);
|
||||
var correction = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(10), 115.0), isNew: false);
|
||||
|
||||
Assert.NotEqual(before.Value, correction.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoreState()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
deco.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), isNew: true);
|
||||
}
|
||||
|
||||
_ = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(10), 120.0), isNew: true);
|
||||
|
||||
var restored = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(10), 110.0), isNew: false);
|
||||
var again = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(10), 110.0), isNew: false);
|
||||
|
||||
Assert.Equal(restored.Value, again.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
deco.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
||||
}
|
||||
|
||||
deco.Reset();
|
||||
|
||||
Assert.False(deco.IsHot);
|
||||
Assert.Equal(0.0, deco.Last.Value);
|
||||
}
|
||||
|
||||
// ── D) Warmup / convergence ──
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsWhenWarmupReached()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
deco.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
||||
Assert.False(deco.IsHot);
|
||||
}
|
||||
deco.Update(new TValue(DateTime.UtcNow.AddSeconds(10), 110.0));
|
||||
Assert.True(deco.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_EqualsLongPeriod()
|
||||
{
|
||||
var deco = new Deco(20, 60);
|
||||
Assert.Equal(60, deco.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ── E) Robustness ──
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
deco.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
||||
}
|
||||
|
||||
var result = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(5), double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValidValue()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
deco.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
||||
}
|
||||
|
||||
var result = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(5), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_NaN_Safe()
|
||||
{
|
||||
double[] src = [100, 101, double.NaN, 103, 104, 105, 106, 107, 108, 109];
|
||||
double[] output = new double[src.Length];
|
||||
Deco.Batch(src, output, 3, 6);
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// ── F) Consistency (4 modes match) ──
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
int shortP = 10, longP = 20;
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
// 1. Streaming
|
||||
var streaming = new Deco(shortP, longP);
|
||||
var streamResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
// 2. Batch TSeries
|
||||
TSeries batchSeries = Deco.Batch(source, shortP, longP);
|
||||
|
||||
// 3. Batch Span
|
||||
var spanOutput = new double[source.Count];
|
||||
Deco.Batch(source.Values, spanOutput, shortP, longP);
|
||||
|
||||
// 4. Event-based
|
||||
var eventSource = new TSeries();
|
||||
var eventIndicator = new Deco(eventSource, shortP, longP);
|
||||
var eventResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
eventSource.Add(source[i]);
|
||||
eventResults[i] = eventIndicator.Last.Value;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchSeries.Values[i], Tolerance);
|
||||
Assert.Equal(streamResults[i], spanOutput[i], Tolerance);
|
||||
Assert.Equal(streamResults[i], eventResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
// ── G) Span API tests ──
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_Throws()
|
||||
{
|
||||
double[] src = [1, 2, 3];
|
||||
double[] output = new double[2];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Deco.Batch(src, output, 1, 2));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ZeroShortPeriod_Throws()
|
||||
{
|
||||
double[] src = [1, 2, 3];
|
||||
double[] output = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Deco.Batch(src, output, 0, 2));
|
||||
Assert.Equal("shortPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LongNotGreater_Throws()
|
||||
{
|
||||
double[] src = [1, 2, 3];
|
||||
double[] output = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Deco.Batch(src, output, 5, 5));
|
||||
Assert.Equal("longPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_NoOp()
|
||||
{
|
||||
double[] src = [];
|
||||
double[] output = [];
|
||||
var ex = Record.Exception(() => Deco.Batch(src, output, 5, 10));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_MatchesTSeries()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 7);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
int shortP = 10, longP = 20;
|
||||
|
||||
TSeries batchTs = Deco.Batch(source, shortP, longP);
|
||||
var spanOutput = new double[source.Count];
|
||||
Deco.Batch(source.Values, spanOutput, shortP, longP);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchTs.Values[i], spanOutput[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
// ── H) Chainability ──
|
||||
|
||||
[Fact]
|
||||
public void PubEvent_FiresOnUpdate()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
int firedCount = 0;
|
||||
deco.Pub += (object? _, in TValueEventArgs _) => firedCount++;
|
||||
|
||||
deco.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(1, firedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chained_Constructor_ReceivesEvents()
|
||||
{
|
||||
var src = new TSeries();
|
||||
var deco = new Deco(src, 5, 10);
|
||||
|
||||
src.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
src.Add(new TValue(DateTime.UtcNow.AddSeconds(1), 101.0));
|
||||
src.Add(new TValue(DateTime.UtcNow.AddSeconds(2), 102.0));
|
||||
|
||||
Assert.True(double.IsFinite(deco.Last.Value));
|
||||
}
|
||||
|
||||
// ── Additional: Oscillator behavior ──
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ProducesZeroOutput()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var result = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
||||
if (i >= 2)
|
||||
{
|
||||
Assert.Equal(0.0, result.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonLinearInput_NonZeroOutput()
|
||||
{
|
||||
// Use quadratic input (non-zero second derivative) since HP filter
|
||||
// removes linear trends (which have zero second derivative)
|
||||
var deco = new Deco(5, 10);
|
||||
TValue last = default;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
last = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i * i * 0.1));
|
||||
}
|
||||
Assert.NotEqual(0.0, last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 99);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
var (results, indicator) = Deco.Calculate(source, 10, 20);
|
||||
Assert.Equal(source.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_InitializesState()
|
||||
{
|
||||
var deco = new Deco(5, 10);
|
||||
double[] primeData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111];
|
||||
deco.Prime(primeData);
|
||||
Assert.True(deco.IsHot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class DecoValidationTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
[Fact]
|
||||
public void StreamingMatchesBatch_DefaultParams()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
// Streaming
|
||||
var deco = new Deco(30, 60);
|
||||
var streaming = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streaming[i] = deco.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
// Batch span
|
||||
var batch = new double[source.Count];
|
||||
Deco.Batch(source.Values, batch, 30, 60);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batch[i], streaming[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamingMatchesBatch_ShortPeriods()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 50.0, mu: 0.01, sigma: 0.3, seed: 7);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var deco = new Deco(5, 15);
|
||||
var streaming = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streaming[i] = deco.Update(source[i]).Value;
|
||||
}
|
||||
|
||||
var batch = new double[source.Count];
|
||||
Deco.Batch(source.Values, batch, 5, 15);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batch[i], streaming[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantPrice_OscillatesAtZero()
|
||||
{
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 50.0));
|
||||
}
|
||||
|
||||
var deco = new Deco(10, 20);
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var result = deco.Update(source[i]);
|
||||
if (i >= 2)
|
||||
{
|
||||
Assert.Equal(0.0, result.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deterministic_SameInputSameOutput()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var deco1 = new Deco(10, 30);
|
||||
var deco2 = new Deco(10, 30);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var r1 = deco1.Update(source[i]);
|
||||
var r2 = deco2.Update(source[i]);
|
||||
Assert.Equal(r1.Value, r2.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectionalCorrectness_UpTrend()
|
||||
{
|
||||
// Exponential growth produces non-zero HP output (linear ramp has zero second-difference)
|
||||
var deco = new Deco(5, 10);
|
||||
double lastVal = 0;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var result = deco.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 * Math.Exp(0.02 * i)));
|
||||
lastVal = result.Value;
|
||||
}
|
||||
// Exponential uptrend produces non-zero DECO
|
||||
Assert.NotEqual(0.0, lastVal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymmetryCheck_OppositeInputs()
|
||||
{
|
||||
// Sinusoidal inputs with opposite phase should produce opposite-sign DECO values
|
||||
var decoUp = new Deco(5, 10);
|
||||
var decoDown = new Deco(5, 10);
|
||||
|
||||
double lastUp = 0, lastDown = 0;
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
double phase = 2.0 * Math.PI * i / 20.0; // period=20 bars
|
||||
var rUp = decoUp.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + 10.0 * Math.Sin(phase)));
|
||||
var rDown = decoDown.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 - 10.0 * Math.Sin(phase)));
|
||||
lastUp = rUp.Value;
|
||||
lastDown = rDown.Value;
|
||||
}
|
||||
// Opposite-phase sinusoidal inputs should produce opposite-sign DECO values
|
||||
Assert.True(lastUp * lastDown < 0,
|
||||
$"Expected opposite signs: up={lastUp}, down={lastDown}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HpFilter_Components_SumCorrectly()
|
||||
{
|
||||
// Verify that the HP_long and HP_short filters produce sensible output:
|
||||
// For constant input, both HP outputs should be zero, hence DECO = 0
|
||||
var source = new double[50];
|
||||
Array.Fill(source, 42.0);
|
||||
var output = new double[50];
|
||||
Deco.Batch(source, output, 10, 20);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
Assert.Equal(0.0, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_NoOverflow()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 1000.0, mu: 0.1, sigma: 0.5, seed: 55);
|
||||
var bars = gbm.Fetch(5000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var output = new double[source.Count];
|
||||
var ex = Record.Exception(() => Deco.Batch(source.Values, output, 30, 60));
|
||||
Assert.Null(ex);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Non-finite at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateMethod_ReturnsConsistentResults()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries source = bars.Close;
|
||||
|
||||
var (results, indicator) = Deco.Calculate(source, 15, 30);
|
||||
Assert.Equal(source.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.True(double.IsFinite(results.Values[^1]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// DECO: Decycler Oscillator
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Ehlers' Decycler Oscillator isolates market cycles by computing the difference
|
||||
/// between two 2-pole Butterworth high-pass filters with different cutoff periods.
|
||||
/// The shorter HP filter passes more cycle content; the longer HP filter passes less.
|
||||
/// Their difference reveals the intermediate-frequency band where tradable cycles live.
|
||||
///
|
||||
/// Formula (Ehlers, TASC September 2015, Equation 4-2):
|
||||
/// <code>
|
||||
/// α = (cos(0.707 × 360/period) + sin(0.707 × 360/period) - 1) / cos(0.707 × 360/period)
|
||||
/// HP[n] = (1 - α/2)² × (x[n] - 2×x[n-1] + x[n-2]) + 2×(1-α)×HP[n-1] - (1-α)²×HP[n-2]
|
||||
/// DECO = HP_long - HP_short
|
||||
/// </code>
|
||||
///
|
||||
/// The 0.707 factor (1/√2) places the filter at the -3 dB point of the Butterworth response.
|
||||
///
|
||||
/// References:
|
||||
/// John F. Ehlers, "Decyclers", Technical Analysis of Stocks & Commodities, September 2015
|
||||
/// John F. Ehlers, "Cycle Analytics for Traders", Wiley, 2013, Chapter 4
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Deco : AbstractBase
|
||||
{
|
||||
private readonly int _shortPeriod;
|
||||
private readonly int _longPeriod;
|
||||
|
||||
// Precomputed HP filter coefficients for short-period filter
|
||||
private readonly double _a1Short; // (1 - α/2)²
|
||||
private readonly double _b1Short; // 2 × (1 - α)
|
||||
private readonly double _c1Short; // -(1 - α)²
|
||||
|
||||
// Precomputed HP filter coefficients for long-period filter
|
||||
private readonly double _a1Long;
|
||||
private readonly double _b1Long;
|
||||
private readonly double _c1Long;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double HpShort1,
|
||||
double HpShort2,
|
||||
double HpLong1,
|
||||
double HpLong2,
|
||||
double Price1,
|
||||
double Price2,
|
||||
int Count,
|
||||
double LastValidValue);
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
public override bool IsHot => _s.Count >= WarmupPeriod;
|
||||
|
||||
/// <summary>Short-period HP cutoff.</summary>
|
||||
public int ShortPeriod => _shortPeriod;
|
||||
|
||||
/// <summary>Long-period HP cutoff.</summary>
|
||||
public int LongPeriod => _longPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Decycler Oscillator with specified cutoff periods.
|
||||
/// </summary>
|
||||
/// <param name="shortPeriod">Short HP cutoff period (must be > 0).</param>
|
||||
/// <param name="longPeriod">Long HP cutoff period (must be > shortPeriod).</param>
|
||||
public Deco(int shortPeriod = 30, int longPeriod = 60)
|
||||
{
|
||||
if (shortPeriod <= 0)
|
||||
{
|
||||
throw new ArgumentException("Short period must be greater than 0.", nameof(shortPeriod));
|
||||
}
|
||||
if (longPeriod <= shortPeriod)
|
||||
{
|
||||
throw new ArgumentException("Long period must be greater than short period.", nameof(longPeriod));
|
||||
}
|
||||
|
||||
_shortPeriod = shortPeriod;
|
||||
_longPeriod = longPeriod;
|
||||
|
||||
// Precompute Butterworth HP coefficients: α = (cos(0.707×360/p) + sin(0.707×360/p) - 1) / cos(0.707×360/p)
|
||||
double rad = 0.707 * 2.0 * Math.PI; // 0.707 × 360° in radians
|
||||
|
||||
double argShort = rad / shortPeriod;
|
||||
double alphaShort = (Math.Cos(argShort) + Math.Sin(argShort) - 1.0) / Math.Cos(argShort);
|
||||
double oneMinusAlphaHalfShort = 1.0 - alphaShort * 0.5;
|
||||
double oneMinusAlphaShort = 1.0 - alphaShort;
|
||||
_a1Short = oneMinusAlphaHalfShort * oneMinusAlphaHalfShort;
|
||||
_b1Short = 2.0 * oneMinusAlphaShort;
|
||||
_c1Short = -(oneMinusAlphaShort * oneMinusAlphaShort);
|
||||
|
||||
double argLong = rad / longPeriod;
|
||||
double alphaLong = (Math.Cos(argLong) + Math.Sin(argLong) - 1.0) / Math.Cos(argLong);
|
||||
double oneMinusAlphaHalfLong = 1.0 - alphaLong * 0.5;
|
||||
double oneMinusAlphaLong = 1.0 - alphaLong;
|
||||
_a1Long = oneMinusAlphaHalfLong * oneMinusAlphaHalfLong;
|
||||
_b1Long = 2.0 * oneMinusAlphaLong;
|
||||
_c1Long = -(oneMinusAlphaLong * oneMinusAlphaLong);
|
||||
|
||||
Name = $"Deco({shortPeriod},{longPeriod})";
|
||||
WarmupPeriod = longPeriod;
|
||||
|
||||
_s = default;
|
||||
_ps = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a chained Decycler Oscillator.
|
||||
/// </summary>
|
||||
public Deco(ITValuePublisher source, int shortPeriod = 30, int longPeriod = 60) : this(shortPeriod, longPeriod)
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew) { _ps = _s; } else { _s = _ps; }
|
||||
var s = _s;
|
||||
|
||||
double value = input.Value;
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
value = double.IsFinite(s.LastValidValue) ? s.LastValidValue : 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
s = s with { LastValidValue = value };
|
||||
}
|
||||
|
||||
double hpShort, hpLong;
|
||||
|
||||
if (s.Count < 2)
|
||||
{
|
||||
// Not enough history for 2-pole HP — output zero
|
||||
hpShort = 0.0;
|
||||
hpLong = 0.0;
|
||||
s = s with
|
||||
{
|
||||
HpShort1 = 0.0,
|
||||
HpShort2 = 0.0,
|
||||
HpLong1 = 0.0,
|
||||
HpLong2 = 0.0,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// HP[n] = a1*(x[n] - 2*x[n-1] + x[n-2]) + b1*HP[n-1] + c1*HP[n-2]
|
||||
double diff = value - 2.0 * s.Price1 + s.Price2;
|
||||
hpShort = Math.FusedMultiplyAdd(_a1Short, diff, Math.FusedMultiplyAdd(_b1Short, s.HpShort1, _c1Short * s.HpShort2));
|
||||
hpLong = Math.FusedMultiplyAdd(_a1Long, diff, Math.FusedMultiplyAdd(_b1Long, s.HpLong1, _c1Long * s.HpLong2));
|
||||
|
||||
s = s with
|
||||
{
|
||||
HpShort2 = s.HpShort1,
|
||||
HpShort1 = hpShort,
|
||||
HpLong2 = s.HpLong1,
|
||||
HpLong1 = hpLong,
|
||||
};
|
||||
}
|
||||
|
||||
// DECO = HP_long - HP_short (long-period HP passes fewer cycles → more trend-like)
|
||||
double deco = hpLong - hpShort;
|
||||
|
||||
_s = s with { Price2 = s.Price1, Price1 = value, Count = s.Count + 1 };
|
||||
|
||||
Last = new TValue(input.Time, deco);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) { return []; }
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.Values, vSpan, _shortPeriod, _longPeriod);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Replay to set internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, value));
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_s = default;
|
||||
_ps = default;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates DECO for an entire series.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int shortPeriod = 30, int longPeriod = 60)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.Values, vSpan, shortPeriod, longPeriod);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Span-based batch DECO calculation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int shortPeriod = 30, int longPeriod = 60)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length.", nameof(output));
|
||||
}
|
||||
if (shortPeriod <= 0)
|
||||
{
|
||||
throw new ArgumentException("Short period must be greater than 0.", nameof(shortPeriod));
|
||||
}
|
||||
if (longPeriod <= shortPeriod)
|
||||
{
|
||||
throw new ArgumentException("Long period must be greater than short period.", nameof(longPeriod));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0) { return; }
|
||||
|
||||
double rad = 0.707 * 2.0 * Math.PI;
|
||||
|
||||
double argShort = rad / shortPeriod;
|
||||
double alphaShort = (Math.Cos(argShort) + Math.Sin(argShort) - 1.0) / Math.Cos(argShort);
|
||||
double omahShort = 1.0 - alphaShort * 0.5;
|
||||
double omaShort = 1.0 - alphaShort;
|
||||
double a1S = omahShort * omahShort;
|
||||
double b1S = 2.0 * omaShort;
|
||||
double c1S = -(omaShort * omaShort);
|
||||
|
||||
double argLong = rad / longPeriod;
|
||||
double alphaLong = (Math.Cos(argLong) + Math.Sin(argLong) - 1.0) / Math.Cos(argLong);
|
||||
double omahLong = 1.0 - alphaLong * 0.5;
|
||||
double omaLong = 1.0 - alphaLong;
|
||||
double a1L = omahLong * omahLong;
|
||||
double b1L = 2.0 * omaLong;
|
||||
double c1L = -(omaLong * omaLong);
|
||||
|
||||
double hpS1 = 0, hpS2 = 0, hpL1 = 0, hpL2 = 0;
|
||||
double price1 = 0, price2 = 0;
|
||||
double lastValid = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (!double.IsFinite(val)) { val = lastValid; } else { lastValid = val; }
|
||||
|
||||
if (i < 2)
|
||||
{
|
||||
output[i] = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
double diff = val - 2.0 * price1 + price2;
|
||||
double hpS = Math.FusedMultiplyAdd(a1S, diff, Math.FusedMultiplyAdd(b1S, hpS1, c1S * hpS2));
|
||||
double hpL = Math.FusedMultiplyAdd(a1L, diff, Math.FusedMultiplyAdd(b1L, hpL1, c1L * hpL2));
|
||||
output[i] = hpL - hpS;
|
||||
hpS2 = hpS1; hpS1 = hpS;
|
||||
hpL2 = hpL1; hpL1 = hpL;
|
||||
}
|
||||
|
||||
price2 = price1;
|
||||
price1 = val;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates DECO and returns both results and a primed indicator.
|
||||
/// </summary>
|
||||
public static (TSeries Results, Deco Indicator) Calculate(TSeries source,
|
||||
int shortPeriod = 30, int longPeriod = 60)
|
||||
{
|
||||
var ind = new Deco(shortPeriod, longPeriod);
|
||||
var results = ind.Update(source);
|
||||
return (results, ind);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
# DECO: Ehlers Decycler Oscillator
|
||||
|
||||
## Overview
|
||||
|
||||
The Decycler Oscillator (DECO) is a DSP-based oscillator developed by John F. Ehlers that isolates intermediate-frequency market cycles. It computes the difference between two 2-pole Butterworth high-pass filters with different cutoff periods, revealing the spectral band between the two cutoff frequencies.
|
||||
|
||||
## Origin
|
||||
|
||||
- **Author:** John F. Ehlers
|
||||
- **Source:** "Decyclers", Technical Analysis of Stocks & Commodities, September 2015
|
||||
- **Category:** Oscillator / Digital Signal Processing
|
||||
|
||||
## Formula
|
||||
|
||||
The DECO uses two 2-pole Butterworth high-pass filters:
|
||||
|
||||
```
|
||||
α = (cos(0.707 × 2π/period) + sin(0.707 × 2π/period) - 1) / cos(0.707 × 2π/period)
|
||||
HP[n] = (1 - α/2)² × (x[n] - 2×x[n-1] + x[n-2]) + 2×(1-α) × HP[n-1] - (1-α)² × HP[n-2]
|
||||
|
||||
DECO = HP_long - HP_short
|
||||
```
|
||||
|
||||
The 0.707 factor (1/√2) places the filter response at the -3 dB Butterworth design point.
|
||||
|
||||
### Transfer Function
|
||||
|
||||
Each HP filter has the z-domain transfer function:
|
||||
|
||||
```
|
||||
H(z) = (1-α/2)² × (1 - 2z⁻¹ + z⁻²) / (1 - 2(1-α)z⁻¹ + (1-α)²z⁻²)
|
||||
```
|
||||
|
||||
The DECO output is the difference H_long(z) - H_short(z), which forms a bandpass response isolating cycles between the short and long cutoff periods.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Range | Description |
|
||||
|-----------|------|---------|-------|-------------|
|
||||
| shortPeriod | int | 30 | > 0 | Short HP cutoff period (bars) |
|
||||
| longPeriod | int | 60 | > shortPeriod | Long HP cutoff period (bars) |
|
||||
|
||||
## Interpretation
|
||||
|
||||
The Decycler Oscillator provides several analytical perspectives:
|
||||
|
||||
- **Zero-Line Crossovers:**
|
||||
- Crossing above zero indicates bullish cycle momentum
|
||||
- Crossing below zero indicates bearish cycle momentum
|
||||
- The zero-crossing timing is relatively lag-free
|
||||
|
||||
- **Band Isolation:**
|
||||
- The oscillator extracts only cycles within the frequency band defined by the two cutoff periods
|
||||
- Shorter cycles and longer trends are both rejected
|
||||
- This makes the oscillator highly selective
|
||||
|
||||
- **Divergence Analysis:**
|
||||
- Bullish divergence: price makes lower lows while DECO makes higher lows
|
||||
- Bearish divergence: price makes higher highs while DECO makes lower highs
|
||||
- Indicates potential trend reversal
|
||||
|
||||
- **Multiple Instance Analysis:**
|
||||
- Ehlers recommends using multiple DECO instances with different period pairs
|
||||
- Crossovers between instances with different coefficients can identify trend reversals
|
||||
|
||||
## Warmup Period
|
||||
|
||||
The indicator requires `longPeriod` bars before producing reliable output. The first two bars always output zero (insufficient price history for the 2-pole HP filter).
|
||||
|
||||
## Properties
|
||||
|
||||
- **Range:** Unbounded (oscillates around zero)
|
||||
- **Complexity:** O(1) per bar (pure IIR filter, no lookback buffer needed)
|
||||
- **Memory:** O(1) — only stores filter state variables
|
||||
|
||||
## Related Indicators
|
||||
|
||||
- **Decycler (DECYCLER):** The low-pass complement — removes cycles, keeps trend
|
||||
- **SSF-DSP:** Similar concept using Super Smooth Filters instead of HP filters
|
||||
- **Roofing Filter:** HP + SSF combination for cycle isolation
|
||||
- **BandPass Filter:** Ehlers' direct bandpass approach
|
||||
|
||||
## References
|
||||
|
||||
1. Ehlers, J. F. (2015). "Decyclers." *Technical Analysis of Stocks & Commodities*, September 2015.
|
||||
2. Ehlers, J. F. (2013). *Cycle Analytics for Traders*. Wiley. Chapter 4.
|
||||
@@ -0,0 +1,69 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Decycler Oscillator (DECO)", "DECO", overlay=false)
|
||||
|
||||
//@function Calculates Decycler Oscillator using dual 2-pole Butterworth high-pass filters
|
||||
//@param source Source series to calculate DECO from
|
||||
//@param short_period Short cycle cutoff period for high-pass filter
|
||||
//@param long_period Long cycle cutoff period for high-pass filter
|
||||
//@returns DECO value (HP(longPeriod) - HP(shortPeriod))
|
||||
deco(series float source, simple int short_period, simple int long_period) =>
|
||||
if short_period <= 0 or long_period <= 0
|
||||
runtime.error("All periods must be positive")
|
||||
if short_period >= long_period
|
||||
runtime.error("Short period must be less than long period")
|
||||
|
||||
float src = na(source) ? 0.0 : source
|
||||
|
||||
// Butterworth 2-pole HP coefficient: alpha = (cos(x) + sin(x) - 1) / cos(x)
|
||||
// where x = 0.707 * 2pi / period
|
||||
float rad = 0.707 * 2.0 * math.pi
|
||||
|
||||
float arg_short = rad / short_period
|
||||
float alpha_s = (math.cos(arg_short) + math.sin(arg_short) - 1.0) / math.cos(arg_short)
|
||||
float omah_s = 1.0 - alpha_s * 0.5
|
||||
float oma_s = 1.0 - alpha_s
|
||||
float a1_s = omah_s * omah_s
|
||||
float b1_s = 2.0 * oma_s
|
||||
float c1_s = -(oma_s * oma_s)
|
||||
|
||||
float arg_long = rad / long_period
|
||||
float alpha_l = (math.cos(arg_long) + math.sin(arg_long) - 1.0) / math.cos(arg_long)
|
||||
float omah_l = 1.0 - alpha_l * 0.5
|
||||
float oma_l = 1.0 - alpha_l
|
||||
float a1_l = omah_l * omah_l
|
||||
float b1_l = 2.0 * oma_l
|
||||
float c1_l = -(oma_l * oma_l)
|
||||
|
||||
// 2-pole HP: HP[n] = a1*(x - 2*x[1] + x[2]) + b1*HP[1] + c1*HP[2]
|
||||
float diff_src = nz(src) - 2.0 * nz(src[1]) + nz(src[2])
|
||||
|
||||
var float hp_s = 0.0
|
||||
var float hp_s1 = 0.0
|
||||
var float hp_l = 0.0
|
||||
var float hp_l1 = 0.0
|
||||
|
||||
float new_hp_s = bar_index < 2 ? 0.0 : a1_s * diff_src + b1_s * hp_s + c1_s * hp_s1
|
||||
float new_hp_l = bar_index < 2 ? 0.0 : a1_l * diff_src + b1_l * hp_l + c1_l * hp_l1
|
||||
|
||||
hp_s1 := hp_s
|
||||
hp_s := new_hp_s
|
||||
hp_l1 := hp_l
|
||||
hp_l := new_hp_l
|
||||
|
||||
na(source) ? na : new_hp_l - new_hp_s
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_short_period = input.int(30, "Short Period", minval=1, maxval=500, tooltip="Short cycle cutoff period for high-pass filter")
|
||||
i_long_period = input.int(60, "Long Period", minval=2, maxval=1000, tooltip="Long cycle cutoff period for high-pass filter")
|
||||
i_source = input.source(close, "Source", tooltip="Price series to analyze")
|
||||
|
||||
// Calculation
|
||||
result = deco(i_source, i_short_period, i_long_period)
|
||||
|
||||
// Plot
|
||||
plot(result, "DECO", color=color.yellow, linewidth=2)
|
||||
hline(0, "Zero Line", color=color.gray, linestyle=hline.style_dotted)
|
||||
Reference in New Issue
Block a user