mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 03:28:05 +00:00
Refactor documentation links in numerics, oscillators, reversals, and statistics modules to use relative paths; update Bias class to handle division by zero more robustly; remove obsolete CUMMEAN Pine script; enhance trend indicators documentation; add Visual Studio Code workspace configuration.
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class EbswIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void EbswIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new EbswIndicator();
|
||||
|
||||
Assert.Equal(40, indicator.HpLength);
|
||||
Assert.Equal(10, indicator.SsfLength);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("EBSW - Even Better Sinewave", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new EbswIndicator();
|
||||
|
||||
Assert.Equal(0, EbswIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 20, SsfLength = 5 };
|
||||
|
||||
Assert.True(indicator.ShortName.Contains("EBSW", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("20", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("5", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_Initialize_CreatesInternalEbsw()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 40, SsfLength = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (EBSW + Zero + Upper + Lower lines)
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 20, SsfLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 20, SsfLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 20, SsfLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Should not throw an exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 20, SsfLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 105, 103, 107, 110, 108, 112, 115, 113 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 20, SsfLength = 5, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_HpLength_CanBeChanged()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 40 };
|
||||
|
||||
Assert.Equal(40, indicator.HpLength);
|
||||
|
||||
indicator.HpLength = 20;
|
||||
Assert.Equal(20, indicator.HpLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_SsfLength_CanBeChanged()
|
||||
{
|
||||
var indicator = new EbswIndicator { SsfLength = 10 };
|
||||
|
||||
Assert.Equal(10, indicator.SsfLength);
|
||||
|
||||
indicator.SsfLength = 5;
|
||||
Assert.Equal(5, indicator.SsfLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_Source_CanBeChanged()
|
||||
{
|
||||
var indicator = new EbswIndicator { Source = SourceType.Close };
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
|
||||
indicator.Source = SourceType.Open;
|
||||
Assert.Equal(SourceType.Open, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new EbswIndicator { ShowColdValues = true };
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_ShortName_UpdatesWhenParametersChange()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 40, SsfLength = 10 };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("40", StringComparison.Ordinal));
|
||||
Assert.True(initialName.Contains("10", StringComparison.Ordinal));
|
||||
|
||||
indicator.HpLength = 20;
|
||||
indicator.SsfLength = 5;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("20", StringComparison.Ordinal));
|
||||
Assert.True(updatedName.Contains("5", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_ProcessUpdate_IgnoresNonBarUpdates()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 20, SsfLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Process other update reasons - should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_LineSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 40, SsfLength = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var lineSeries = indicator.LinesSeries[0];
|
||||
|
||||
Assert.Equal("EBSW", lineSeries.Name);
|
||||
Assert.Equal(2, lineSeries.Width);
|
||||
Assert.Equal(LineStyle.Solid, lineSeries.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_ZeroLine_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 40, SsfLength = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var zeroLine = indicator.LinesSeries[1];
|
||||
|
||||
Assert.Equal("Zero", zeroLine.Name);
|
||||
Assert.Equal(1, zeroLine.Width);
|
||||
Assert.Equal(LineStyle.Dash, zeroLine.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_BoundaryLines_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 40, SsfLength = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var upperLine = indicator.LinesSeries[2];
|
||||
var lowerLine = indicator.LinesSeries[3];
|
||||
|
||||
Assert.Equal("+1", upperLine.Name);
|
||||
Assert.Equal("-1", lowerLine.Name);
|
||||
Assert.Equal(LineStyle.Dot, upperLine.Style);
|
||||
Assert.Equal(LineStyle.Dot, lowerLine.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_DifferentParameters_Work()
|
||||
{
|
||||
var paramSets = new[] { (10, 3), (20, 5), (40, 10), (80, 20) };
|
||||
|
||||
foreach (var (hpLength, ssfLength) in paramSets)
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = hpLength, SsfLength = ssfLength };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add enough bars to fill the buffer
|
||||
for (int i = 0; i < hpLength + 10; i++)
|
||||
{
|
||||
double close = 100 + (i % 10);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Last value should be finite
|
||||
double ebswValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(ebswValue), $"HP {hpLength}, SSF {ssfLength} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_ConstantPrice_ProducesBoundedOutput()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 20, SsfLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add constant price bars
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 100, 100, 100);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// AGC normalizes output to [-1, +1] even for constant input
|
||||
// (high-pass filter → 0, but AGC normalizes tiny residuals to ±1)
|
||||
double ebswValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(ebswValue >= -1.0 && ebswValue <= 1.0,
|
||||
$"EBSW value {ebswValue} should be in [-1, +1]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_OutputBounded_BetweenNegativeOneAndOne()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 20, SsfLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add varying price bars
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + 20 * Math.Sin(i * 0.2);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double ebswValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(ebswValue >= -1.0 && ebswValue <= 1.0,
|
||||
$"EBSW value {ebswValue} should be in [-1, +1]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_OscillatesAroundZero_ForSineWave()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 40, SsfLength = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var values = new List<double>();
|
||||
|
||||
// Generate sine wave price pattern
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(i * 0.1);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
values.Add(indicator.LinesSeries[0].GetValue(0));
|
||||
}
|
||||
|
||||
// Should have both positive and negative values
|
||||
int positiveCount = values.Count(v => v > 0);
|
||||
int negativeCount = values.Count(v => v < 0);
|
||||
|
||||
Assert.True(positiveCount > 0, "Should have positive EBSW values");
|
||||
Assert.True(negativeCount > 0, "Should have negative EBSW values");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EbswIndicator_ZeroCrossings_IndicateCyclePhase()
|
||||
{
|
||||
var indicator = new EbswIndicator { HpLength = 20, SsfLength = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var values = new List<double>();
|
||||
|
||||
// Generate sine wave price pattern
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(i * 0.15);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
values.Add(indicator.LinesSeries[0].GetValue(0));
|
||||
}
|
||||
|
||||
// Count zero crossings
|
||||
int crossings = 0;
|
||||
for (int i = 1; i < values.Count; i++)
|
||||
{
|
||||
if (values[i - 1] * values[i] < 0)
|
||||
{
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should have multiple zero crossings for oscillating price
|
||||
Assert.True(crossings >= 3, $"Should have multiple zero crossings, got {crossings}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class EbswIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("HP Length", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int HpLength { get; set; } = 40;
|
||||
|
||||
[InputParameter("SSF Length", sortIndex: 2, 1, 500, 1, 0)]
|
||||
public int SsfLength { get; set; } = 10;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Ebsw _ebsw = null!;
|
||||
private readonly LineSeries _series;
|
||||
private readonly LineSeries _zeroLine;
|
||||
private readonly LineSeries _upperLine;
|
||||
private readonly LineSeries _lowerLine;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"EBSW ({HpLength},{SsfLength})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/cycles/ebsw/Ebsw.Quantower.cs";
|
||||
|
||||
public EbswIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "EBSW - Even Better Sinewave";
|
||||
Description = "Ehlers' Even Better Sinewave oscillator with high-pass filter, super-smoother, and automatic gain control";
|
||||
|
||||
_series = new LineSeries(name: "EBSW", color: IndicatorExtensions.Oscillators, width: 2, style: LineStyle.Solid);
|
||||
_zeroLine = new LineSeries(name: "Zero", color: Color.Gray, width: 1, style: LineStyle.Dash);
|
||||
_upperLine = new LineSeries(name: "+1", color: Color.DarkGray, width: 1, style: LineStyle.Dot);
|
||||
_lowerLine = new LineSeries(name: "-1", color: Color.DarkGray, width: 1, style: LineStyle.Dot);
|
||||
AddLineSeries(_series);
|
||||
AddLineSeries(_zeroLine);
|
||||
AddLineSeries(_upperLine);
|
||||
AddLineSeries(_lowerLine);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_ebsw = new Ebsw(HpLength, SsfLength);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _ebsw.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _ebsw.IsHot, ShowColdValues);
|
||||
_zeroLine.SetValue(0.0);
|
||||
_upperLine.SetValue(1.0);
|
||||
_lowerLine.SetValue(-1.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EbswTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidParameters_SetsProperties()
|
||||
{
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
|
||||
Assert.Equal("Ebsw(40,10)", ebsw.Name);
|
||||
Assert.False(ebsw.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_Works()
|
||||
{
|
||||
var ebsw = new Ebsw();
|
||||
|
||||
Assert.Equal("Ebsw(40,10)", ebsw.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MinimumParameters_Works()
|
||||
{
|
||||
var ebsw = new Ebsw(1, 1);
|
||||
|
||||
Assert.Equal("Ebsw(1,1)", ebsw.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 10)]
|
||||
[InlineData(-1, 10)]
|
||||
public void Constructor_InvalidHpLength_ThrowsArgumentOutOfRange(int hpLength, int ssfLength)
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Ebsw(hpLength, ssfLength));
|
||||
Assert.Equal("hpLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(40, 0)]
|
||||
[InlineData(40, -1)]
|
||||
public void Constructor_InvalidSsfLength_ThrowsArgumentOutOfRange(int hpLength, int ssfLength)
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Ebsw(hpLength, ssfLength));
|
||||
Assert.Equal("ssfLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullSource_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new Ebsw(null!, 40, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithValidSource_Subscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var ebsw = new Ebsw(source, 40, 10);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, ebsw.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsValidTValue()
|
||||
{
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
var result = ebsw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AfterWarmup_IsHotTrue()
|
||||
{
|
||||
var ebsw = new Ebsw(10, 5);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ebsw.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(ebsw.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_OutputBoundedBetweenMinusOneAndOne()
|
||||
{
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ebsw.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.InRange(ebsw.Last.Value, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantSeries_ProducesBoundedOutput()
|
||||
{
|
||||
// For a constant series, the high-pass filter removes the DC component,
|
||||
// making filt → 0. However, the AGC (wave/sqrt(pwr)) normalizes any
|
||||
// non-zero signal. Due to floating-point precision, very small filt values
|
||||
// produce ratios approaching ±1, not 0. This is mathematically correct.
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
||||
}
|
||||
|
||||
// Output should still be bounded [-1, +1]
|
||||
Assert.InRange(ebsw.Last.Value, -1.0, 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SinusoidalInput_DetectsCycles()
|
||||
{
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
double frequency = 2.0 * Math.PI / 20.0; // 20-bar cycle
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(i * frequency);
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
// Output should show cyclic behavior with values approaching extremes
|
||||
Assert.True(ebsw.IsHot);
|
||||
Assert.InRange(ebsw.Last.Value, -1.0, 1.0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 10);
|
||||
|
||||
ebsw.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
||||
var first = ebsw.Last.Value;
|
||||
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(1), 110.0), isNew: true);
|
||||
var second = ebsw.Last.Value;
|
||||
|
||||
// Values should differ after processing different prices
|
||||
Assert.NotEqual(first, second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_ReplacesCurrentBar()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 10);
|
||||
|
||||
ebsw.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(1), 110.0), isNew: true);
|
||||
var beforeCorrection = ebsw.Last.Value;
|
||||
|
||||
// Correct the bar with a different value
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(1), 90.0), isNew: false);
|
||||
var afterCorrection = ebsw.Last.Value;
|
||||
|
||||
Assert.NotEqual(beforeCorrection, afterCorrection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleCorrections_RestoresToSnapshot()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 10);
|
||||
|
||||
// Build some history
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), isNew: true);
|
||||
}
|
||||
|
||||
// Add a new bar
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(50), 150.0), isNew: true);
|
||||
var originalValue = ebsw.Last.Value;
|
||||
|
||||
// Correct multiple times
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(50), 160.0), isNew: false);
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(50), 140.0), isNew: false);
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(50), 150.0), isNew: false);
|
||||
var restoredValue = ebsw.Last.Value;
|
||||
|
||||
Assert.Equal(originalValue, restoredValue, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 10);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.True(ebsw.IsHot);
|
||||
|
||||
ebsw.Reset();
|
||||
|
||||
Assert.False(ebsw.IsHot);
|
||||
Assert.Equal(default, ebsw.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 10);
|
||||
|
||||
// First run
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + Math.Sin(i * 0.1) * 10));
|
||||
}
|
||||
var firstResult = ebsw.Last.Value;
|
||||
|
||||
ebsw.Reset();
|
||||
|
||||
// Second run with same data
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + Math.Sin(i * 0.1) * 10));
|
||||
}
|
||||
var secondResult = ebsw.Last.Value;
|
||||
|
||||
Assert.Equal(firstResult, secondResult, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN/Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 10);
|
||||
|
||||
ebsw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(1), double.NaN));
|
||||
var afterNaN = ebsw.Last.Value;
|
||||
|
||||
Assert.True(double.IsFinite(afterNaN));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValidValue()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 10);
|
||||
|
||||
ebsw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(1), double.PositiveInfinity));
|
||||
|
||||
Assert.True(double.IsFinite(ebsw.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NegativeInfinity_UsesLastValidValue()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 10);
|
||||
|
||||
ebsw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(1), double.NegativeInfinity));
|
||||
|
||||
Assert.True(double.IsFinite(ebsw.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(42)]
|
||||
[InlineData(123)]
|
||||
[InlineData(999)]
|
||||
public void Update_StreamingMatchesBatch(int seed)
|
||||
{
|
||||
const int hpLength = 40;
|
||||
const int ssfLength = 10;
|
||||
const int dataLen = 100;
|
||||
|
||||
var gbm = new GBM(seed: seed);
|
||||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming
|
||||
var streaming = new Ebsw(hpLength, ssfLength);
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streaming.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Batch via TSeries
|
||||
var tSeries = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
tSeries.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var batch = Ebsw.Calculate(tSeries, hpLength, ssfLength);
|
||||
|
||||
// Compare last values
|
||||
Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesStreaming()
|
||||
{
|
||||
const int hpLength = 20;
|
||||
const int ssfLength = 8;
|
||||
const int dataLen = 200;
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming
|
||||
var streaming = new Ebsw(hpLength, ssfLength);
|
||||
var streamingResults = new double[dataLen];
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
streaming.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
streamingResults[i] = streaming.Last.Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
double[] source = new double[dataLen];
|
||||
double[] batchResults = new double[dataLen];
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
source[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
Ebsw.Batch(source, batchResults, hpLength, ssfLength);
|
||||
|
||||
// Compare all values
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Span API Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_ValidatesLengthMismatch()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[50];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ebsw.Batch(source, output, 40, 10));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ValidatesHpLength()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => Ebsw.Batch(source, output, 0, 10));
|
||||
Assert.Equal("hpLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ValidatesSsfLength()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => Ebsw.Batch(source, output, 40, 0));
|
||||
Assert.Equal("ssfLength", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyArrays_NoException()
|
||||
{
|
||||
double[] source = [];
|
||||
double[] output = [];
|
||||
|
||||
var ex = Record.Exception(() => Ebsw.Batch(source, output, 40, 10));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_HandlesNaN()
|
||||
{
|
||||
double[] source = { 100, 101, double.NaN, 103, 104 };
|
||||
double[] output = new double[5];
|
||||
|
||||
Ebsw.Batch(source, output, 5, 2);
|
||||
|
||||
foreach (double v in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(v));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_HpLengthFour_ThrowsArgumentOutOfRange()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => Ebsw.Batch(source, output, 4, 10));
|
||||
Assert.Equal("hpLength", ex.ParamName);
|
||||
Assert.Contains("cos(2π/hpLength) is zero", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_HpLengthFour_ThrowsArgumentOutOfRange()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Ebsw(4, 10));
|
||||
Assert.Equal("hpLength", ex.ParamName);
|
||||
Assert.Contains("cos(2π/hpLength) is zero", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Chaining_PropagatesUpdates()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var ebsw = new Ebsw(source, 20, 10);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + Math.Sin(i * 0.1) * 10));
|
||||
}
|
||||
|
||||
Assert.True(ebsw.IsHot);
|
||||
Assert.True(double.IsFinite(ebsw.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chaining_MultipleIndicators()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var ebsw1 = new Ebsw(source, 20, 10);
|
||||
var ebsw2 = new Ebsw(source, 40, 5);
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + Math.Sin(i * 0.1) * 10));
|
||||
}
|
||||
|
||||
// Both should have values
|
||||
Assert.True(double.IsFinite(ebsw1.Last.Value));
|
||||
Assert.True(double.IsFinite(ebsw2.Last.Value));
|
||||
|
||||
// Different parameters should produce different results
|
||||
Assert.NotEqual(ebsw1.Last.Value, ebsw2.Last.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parameter Behavior Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(10, 5)]
|
||||
[InlineData(20, 10)]
|
||||
[InlineData(40, 10)]
|
||||
[InlineData(100, 20)]
|
||||
public void Update_DifferentParameters_ProducesValidResults(int hpLength, int ssfLength)
|
||||
{
|
||||
var ebsw = new Ebsw(hpLength, ssfLength);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ebsw.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(ebsw.IsHot);
|
||||
Assert.True(double.IsFinite(ebsw.Last.Value));
|
||||
Assert.InRange(ebsw.Last.Value, -1.0, 1.0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for EBSW (Ehlers Even Better Sinewave).
|
||||
/// EBSW is Ehlers' proprietary indicator not commonly implemented in trading libraries
|
||||
/// (TA-Lib, Skender, Tulip), so validation is done against mathematical properties
|
||||
/// and known theoretical results based on the original PineScript implementation.
|
||||
/// </summary>
|
||||
public class EbswValidationTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
#region Mathematical Property Validation
|
||||
|
||||
[Fact]
|
||||
public void Validation_ConstantSeries_OutputBounded()
|
||||
{
|
||||
// For constant input, high-pass filter removes DC, making filt → 0.
|
||||
// However, AGC (wave/sqrt(pwr)) normalizes any non-zero residual.
|
||||
// Due to floating-point precision, tiny filt values produce ratios ≈ ±1.
|
||||
// This is mathematically correct - the AGC is doing its job.
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
||||
}
|
||||
|
||||
// Output should still be bounded [-1, +1]
|
||||
Assert.InRange(ebsw.Last.Value, -1.0, 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_OutputBoundedBetweenNegativeOneAndOne()
|
||||
{
|
||||
// AGC should always normalize output to [-1, +1]
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
|
||||
var gbm = new GBM(seed: 42, sigma: 0.5); // High volatility
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ebsw.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(ebsw.Last.Value >= -1.0 && ebsw.Last.Value <= 1.0,
|
||||
$"EBSW output {ebsw.Last.Value} should be in [-1, +1]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_OscillatesAroundZero()
|
||||
{
|
||||
// EBSW should oscillate around zero over time
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
var values = new List<double>();
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ebsw.Update(new TValue(bar.Time, bar.Close));
|
||||
if (ebsw.IsHot)
|
||||
{
|
||||
values.Add(ebsw.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Should have both positive and negative values
|
||||
int positiveCount = values.Count(v => v > 0);
|
||||
int negativeCount = values.Count(v => v < 0);
|
||||
|
||||
Assert.True(positiveCount > 0, "Should have positive EBSW values");
|
||||
Assert.True(negativeCount > 0, "Should have negative EBSW values");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_ZeroCrossings_IndicateCyclePhase()
|
||||
{
|
||||
// EBSW should cross zero when cycle phase changes
|
||||
var ebsw = new Ebsw(20, 5);
|
||||
var values = new List<double>();
|
||||
|
||||
// Generate sine wave to simulate price oscillation
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(i * 0.1);
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
if (ebsw.IsHot)
|
||||
{
|
||||
values.Add(ebsw.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Count zero crossings
|
||||
int crossings = 0;
|
||||
for (int i = 1; i < values.Count; i++)
|
||||
{
|
||||
if (values[i - 1] * values[i] < 0)
|
||||
{
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should have multiple zero crossings for oscillating price
|
||||
Assert.True(crossings >= 3, $"Should have multiple zero crossings, got {crossings}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PineScript Formula Verification
|
||||
|
||||
[Fact]
|
||||
public void Validation_HighPassCoefficient_MatchesPineScript()
|
||||
{
|
||||
// alpha1 = (1 - sin(2π/hpLength)) / cos(2π/hpLength)
|
||||
const int hpLength = 40;
|
||||
double angleHp = 2.0 * Math.PI / hpLength;
|
||||
double expectedAlpha1 = (1.0 - Math.Sin(angleHp)) / Math.Cos(angleHp);
|
||||
|
||||
// Verify the coefficient calculation
|
||||
Assert.True(expectedAlpha1 > 0 && expectedAlpha1 < 1,
|
||||
$"Alpha1 should be between 0 and 1, got {expectedAlpha1}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_SuperSmootherCoefficients_MatchesPineScript()
|
||||
{
|
||||
// alpha2 = exp(-√2 * π / ssfLength)
|
||||
// beta = 2 * alpha2 * cos(√2 * π / ssfLength)
|
||||
// c1 = 1 - beta + alpha2², c2 = beta, c3 = -alpha2²
|
||||
const int ssfLength = 10;
|
||||
double angleSsf = Math.Sqrt(2.0) * Math.PI / ssfLength;
|
||||
double alpha2 = Math.Exp(-angleSsf);
|
||||
double beta = 2.0 * alpha2 * Math.Cos(angleSsf);
|
||||
double c2 = beta;
|
||||
double c3 = -(alpha2 * alpha2);
|
||||
double c1 = 1.0 - c2 - c3;
|
||||
|
||||
// Verify IIR filter stability: poles must be inside unit circle
|
||||
// For two-pole Butterworth-style SSF: |alpha2| < 1 ensures stability
|
||||
Assert.True(alpha2 > 0 && alpha2 < 1, $"alpha2 should be in (0,1), got {alpha2}");
|
||||
Assert.True(c1 > 0, "c1 should be positive");
|
||||
Assert.True(c2 > 0, "c2 should be positive");
|
||||
Assert.True(c3 < 0, "c3 should be negative");
|
||||
// Verify c1 is computed correctly: c1 = 1 - beta + alpha2²
|
||||
double expectedC1 = 1.0 - beta + (alpha2 * alpha2);
|
||||
Assert.Equal(expectedC1, c1, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_AGCNormalization_ClampsMagnitude()
|
||||
{
|
||||
// wave / sqrt(pwr) can theoretically exceed 1 before clamping
|
||||
// The clamp ensures output stays in [-1, +1]
|
||||
var ebsw = new Ebsw(10, 3);
|
||||
|
||||
// Extreme step changes should still produce bounded output
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = (i % 2 == 0) ? 200.0 : 50.0; // Extreme oscillation
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
Assert.True(Math.Abs(ebsw.Last.Value) <= 1.0,
|
||||
$"EBSW output magnitude {Math.Abs(ebsw.Last.Value)} should not exceed 1");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Filter Behavior Validation
|
||||
|
||||
[Fact]
|
||||
public void Validation_HighPassFilter_RemovesTrend()
|
||||
{
|
||||
// High-pass filter removes DC/trend component
|
||||
// EBSW output should remain bounded even with strong trend
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
var values = new List<double>();
|
||||
|
||||
// Strong uptrend with oscillating component
|
||||
// Larger amplitude oscillation to ensure EBSW detects cycles
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
double trend = 100.0 + i * 0.5;
|
||||
double oscillation = Math.Sin(i * 0.15) * 10.0; // Larger amplitude, longer period
|
||||
double price = trend + oscillation;
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
if (ebsw.IsHot)
|
||||
{
|
||||
values.Add(ebsw.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Output should be bounded [-1, +1] despite strong trend
|
||||
Assert.True(values.All(v => v >= -1.0 && v <= 1.0), "All values should be bounded");
|
||||
// Should span a significant portion of the range (AGC normalizes output)
|
||||
double range = values.Max() - values.Min();
|
||||
Assert.True(range > 0.5, $"Should have significant range, got {range}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_SuperSmoother_ReducesNoise()
|
||||
{
|
||||
// Super-smoother should reduce high-frequency noise
|
||||
// Longer SSF length should produce smoother output
|
||||
var ebswShort = new Ebsw(40, 5);
|
||||
var ebswLong = new Ebsw(40, 20);
|
||||
|
||||
var gbm = new GBM(seed: 42, sigma: 0.3);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var valuesShort = new List<double>();
|
||||
var valuesLong = new List<double>();
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ebswShort.Update(new TValue(bar.Time, bar.Close));
|
||||
ebswLong.Update(new TValue(bar.Time, bar.Close));
|
||||
if (ebswShort.IsHot && ebswLong.IsHot)
|
||||
{
|
||||
valuesShort.Add(ebswShort.Last.Value);
|
||||
valuesLong.Add(ebswLong.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate bar-to-bar changes (roughness)
|
||||
double roughnessShort = 0, roughnessLong = 0;
|
||||
for (int i = 1; i < valuesShort.Count; i++)
|
||||
{
|
||||
roughnessShort += Math.Abs(valuesShort[i] - valuesShort[i - 1]);
|
||||
roughnessLong += Math.Abs(valuesLong[i] - valuesLong[i - 1]);
|
||||
}
|
||||
|
||||
Assert.True(roughnessLong < roughnessShort,
|
||||
$"Longer SSF should be smoother: short={roughnessShort:F4}, long={roughnessLong:F4}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_PureSineInput_ExtractsCycle()
|
||||
{
|
||||
// For pure sine input matching the filter period,
|
||||
// EBSW should produce clean oscillation
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
var values = new List<double>();
|
||||
|
||||
// Generate pure sine wave at matching frequency
|
||||
double frequency = 2.0 * Math.PI / 40.0;
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(i * frequency);
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
if (ebsw.IsHot)
|
||||
{
|
||||
values.Add(ebsw.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Should reach values close to +1 and -1
|
||||
double maxVal = values.Max();
|
||||
double minVal = values.Min();
|
||||
|
||||
Assert.True(maxVal > 0.7, $"Max should be close to +1, got {maxVal}");
|
||||
Assert.True(minVal < -0.7, $"Min should be close to -1, got {minVal}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Streaming vs Batch Consistency
|
||||
|
||||
[Theory]
|
||||
[InlineData(42)]
|
||||
[InlineData(123)]
|
||||
[InlineData(999)]
|
||||
public void Validation_StreamingMatchesBatch(int seed)
|
||||
{
|
||||
const int hpLength = 40;
|
||||
const int ssfLength = 10;
|
||||
const int dataLen = 100;
|
||||
|
||||
var gbm = new GBM(seed: seed);
|
||||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming
|
||||
var streaming = new Ebsw(hpLength, ssfLength);
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streaming.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Batch via TSeries
|
||||
var tSeries = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
tSeries.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var batch = Ebsw.Calculate(tSeries, hpLength, ssfLength);
|
||||
|
||||
// Compare last values
|
||||
Assert.Equal(batch[^1].Value, streaming.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_SpanMatchesTSeries()
|
||||
{
|
||||
const int hpLength = 20;
|
||||
const int ssfLength = 5;
|
||||
const int dataLen = 200;
|
||||
|
||||
var gbm = new GBM(seed: 77);
|
||||
var bars = gbm.Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// TSeries approach
|
||||
var tSeries = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
tSeries.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var tSeriesResult = Ebsw.Calculate(tSeries, hpLength, ssfLength);
|
||||
|
||||
// Span approach
|
||||
double[] source = new double[dataLen];
|
||||
double[] spanResult = new double[dataLen];
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
source[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
Ebsw.Batch(source, spanResult, hpLength, ssfLength);
|
||||
|
||||
// Compare all values
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
Assert.Equal(tSeriesResult[i].Value, spanResult[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Different Parameter Combinations
|
||||
|
||||
[Theory]
|
||||
[InlineData(10, 3)]
|
||||
[InlineData(20, 5)]
|
||||
[InlineData(40, 10)]
|
||||
[InlineData(80, 20)]
|
||||
public void Validation_DifferentParameters_ConsistentResults(int hpLength, int ssfLength)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var ebsw = new Ebsw(hpLength, ssfLength);
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ebsw.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(ebsw.IsHot);
|
||||
Assert.True(double.IsFinite(ebsw.Last.Value));
|
||||
Assert.True(Math.Abs(ebsw.Last.Value) <= 1.0);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(20)]
|
||||
[InlineData(40)]
|
||||
[InlineData(80)]
|
||||
public void Validation_LongerHpPeriod_SmallerOutputVariance(int hpLength)
|
||||
{
|
||||
// Longer HP period removes more low-frequency content
|
||||
var ebsw = new Ebsw(hpLength, 10);
|
||||
var values = new List<double>();
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ebsw.Update(new TValue(bar.Time, bar.Close));
|
||||
if (ebsw.IsHot)
|
||||
{
|
||||
values.Add(ebsw.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Check variance is non-zero
|
||||
double mean = values.Average();
|
||||
double variance = values.Sum(v => Math.Pow(v - mean, 2)) / values.Count;
|
||||
Assert.True(variance > 0, "Should have non-zero variance");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
public void Validation_VerySmallPrices_HandledCorrectly()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 5);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 0.0001 + i * 0.00001;
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
Assert.True(ebsw.IsHot);
|
||||
Assert.True(double.IsFinite(ebsw.Last.Value));
|
||||
Assert.True(Math.Abs(ebsw.Last.Value) <= 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_VeryLargePrices_HandledCorrectly()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 5);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 1e10 + i * 1e8;
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
Assert.True(ebsw.IsHot);
|
||||
Assert.True(double.IsFinite(ebsw.Last.Value));
|
||||
Assert.True(Math.Abs(ebsw.Last.Value) <= 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_HighVolatility_StableResults()
|
||||
{
|
||||
var ebsw = new Ebsw(20, 5);
|
||||
|
||||
var gbm = new GBM(seed: 42, sigma: 0.5); // High volatility
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
ebsw.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(double.IsFinite(ebsw.Last.Value), "EBSW should remain finite under high volatility");
|
||||
Assert.True(Math.Abs(ebsw.Last.Value) <= 1.0, "EBSW should remain bounded under high volatility");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_StepChange_ProducesBoundedOutput()
|
||||
{
|
||||
// For constant input, high-pass filter makes filt → 0.
|
||||
// AGC normalizes tiny residuals to ±1 (0/0 → ε/√(ε²) = ±1).
|
||||
// After step change, transient occurs then settles to bounded output.
|
||||
var ebsw = new Ebsw(40, 10);
|
||||
|
||||
// Stable period at price 100
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
||||
}
|
||||
|
||||
double beforeStep = ebsw.Last.Value;
|
||||
|
||||
// Step change to price 150
|
||||
for (int i = 100; i < 200; i++)
|
||||
{
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 150.0));
|
||||
}
|
||||
|
||||
double afterStep = ebsw.Last.Value;
|
||||
|
||||
// Both should remain bounded [-1, +1]
|
||||
Assert.True(Math.Abs(beforeStep) <= 1.0, $"Before step should be bounded, got {beforeStep}");
|
||||
Assert.True(Math.Abs(afterStep) <= 1.0, $"After step should be bounded, got {afterStep}");
|
||||
Assert.True(double.IsFinite(beforeStep), "Before step should be finite");
|
||||
Assert.True(double.IsFinite(afterStep), "After step should be finite");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AGC (Automatic Gain Control) Validation
|
||||
|
||||
[Fact]
|
||||
public void Validation_AGC_AdaptsToVolatility()
|
||||
{
|
||||
// AGC normalizes by RMS, so different volatility levels
|
||||
// should still produce output in [-1, +1]
|
||||
var ebswLow = new Ebsw(40, 10);
|
||||
var ebswHigh = new Ebsw(40, 10);
|
||||
|
||||
// Low volatility
|
||||
var gbmLow = new GBM(seed: 42, sigma: 0.05);
|
||||
var barsLow = gbmLow.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// High volatility
|
||||
var gbmHigh = new GBM(seed: 42, sigma: 0.5);
|
||||
var barsHigh = gbmHigh.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var valuesLow = new List<double>();
|
||||
var valuesHigh = new List<double>();
|
||||
|
||||
foreach (var bar in barsLow)
|
||||
{
|
||||
ebswLow.Update(new TValue(bar.Time, bar.Close));
|
||||
if (ebswLow.IsHot)
|
||||
{
|
||||
valuesLow.Add(ebswLow.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bar in barsHigh)
|
||||
{
|
||||
ebswHigh.Update(new TValue(bar.Time, bar.Close));
|
||||
if (ebswHigh.IsHot)
|
||||
{
|
||||
valuesHigh.Add(ebswHigh.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Both should have values spanning much of the [-1, +1] range
|
||||
double rangeLow = valuesLow.Max() - valuesLow.Min();
|
||||
double rangeHigh = valuesHigh.Max() - valuesHigh.Min();
|
||||
|
||||
Assert.True(rangeLow > 0.5, $"Low vol range should be significant: {rangeLow}");
|
||||
Assert.True(rangeHigh > 0.5, $"High vol range should be significant: {rangeHigh}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_AGC_ZeroPowerHandled()
|
||||
{
|
||||
// When power is zero (constant input), division returns 0
|
||||
var ebsw = new Ebsw(10, 3);
|
||||
|
||||
// All constant values
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ebsw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(ebsw.Last.Value), "Should handle zero power gracefully");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// EBSW: Ehlers Even Better Sinewave - A normalized oscillator that extracts the
|
||||
/// dominant cycle from price data using high-pass and super-smoother filters with
|
||||
/// automatic gain control (AGC).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Even Better Sinewave indicator, developed by John Ehlers, improves upon
|
||||
/// earlier sinewave indicators by combining detrending, smoothing, and normalization
|
||||
/// in a single robust algorithm.
|
||||
///
|
||||
/// Algorithm:
|
||||
/// 1. High-pass filter (HPF) removes low-frequency trend component
|
||||
/// 2. Super-smoother filter (SSF) removes high-frequency noise
|
||||
/// 3. Three-bar average creates the wave component
|
||||
/// 4. Automatic gain control normalizes output to [-1, +1]
|
||||
///
|
||||
/// Formula:
|
||||
/// alpha1 = (1 - sin(2π/hpLength)) / cos(2π/hpLength)
|
||||
/// hp = 0.5 * (1 + alpha1) * (src - src[1]) + alpha1 * hp[1]
|
||||
///
|
||||
/// alpha2 = exp(-√2 * π / ssfLength)
|
||||
/// beta = 2 * alpha2 * cos(√2 * π / ssfLength)
|
||||
/// c1 = 1 - beta + alpha2², c2 = beta, c3 = -alpha2²
|
||||
/// filt = c1 * (hp + hp[1]) / 2 + c2 * filt[1] + c3 * filt[2]
|
||||
///
|
||||
/// wave = (filt + filt[1] + filt[2]) / 3
|
||||
/// pwr = (filt² + filt[1]² + filt[2]²) / 3
|
||||
/// sinewave = wave / sqrt(pwr) [clamped to -1..+1]
|
||||
///
|
||||
/// Properties:
|
||||
/// - Oscillates between -1 and +1
|
||||
/// - Zero crossings identify potential turning points
|
||||
/// - High-pass filter removes trend bias
|
||||
/// - Super-smoother reduces whipsaws
|
||||
/// - AGC adapts to volatility automatically
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Ebsw : AbstractBase
|
||||
{
|
||||
private readonly int _hpLength;
|
||||
private readonly int _ssfLength;
|
||||
|
||||
// High-pass filter coefficient
|
||||
private readonly double _alpha1;
|
||||
|
||||
// Super-smoother filter coefficients
|
||||
private readonly double _c1, _c2, _c3;
|
||||
|
||||
// State record for snapshot/restore
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double Src0, double Src1,
|
||||
double Hp0, double Hp1,
|
||||
double Filt0, double Filt1, double Filt2,
|
||||
double LastValidValue
|
||||
);
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
private int _barCount;
|
||||
private int _p_barCount;
|
||||
|
||||
public override bool IsHot => _barCount >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Ehlers Even Better Sinewave indicator.
|
||||
/// </summary>
|
||||
/// <param name="hpLength">Period for the high-pass filter (detrending). Must be >= 1 and != 4.</param>
|
||||
/// <param name="ssfLength">Period for the super-smoother filter. Must be >= 1.</param>
|
||||
public Ebsw(int hpLength = 40, int ssfLength = 10)
|
||||
{
|
||||
if (hpLength < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(hpLength), "HP length must be at least 1.");
|
||||
}
|
||||
if (ssfLength < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(ssfLength), "SSF length must be at least 1.");
|
||||
}
|
||||
|
||||
// Validate that cos(2π/hpLength) is not zero (hpLength == 4 causes division by zero)
|
||||
double angleHp = 2.0 * Math.PI / hpLength;
|
||||
double cosAngleHp = Math.Cos(angleHp);
|
||||
if (Math.Abs(cosAngleHp) < 1e-10)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(hpLength), "hpLength cannot be 4 because cos(2π/hpLength) is zero, causing division by zero.");
|
||||
}
|
||||
|
||||
_hpLength = hpLength;
|
||||
_ssfLength = ssfLength;
|
||||
|
||||
// High-pass filter coefficient: alpha1 = (1 - sin(angle)) / cos(angle)
|
||||
_alpha1 = (1.0 - Math.Sin(angleHp)) / cosAngleHp;
|
||||
|
||||
// Super-smoother filter coefficients
|
||||
double angleSsf = Math.Sqrt(2.0) * Math.PI / ssfLength;
|
||||
double alpha2 = Math.Exp(-angleSsf);
|
||||
double beta = 2.0 * alpha2 * Math.Cos(angleSsf);
|
||||
_c2 = beta;
|
||||
_c3 = -(alpha2 * alpha2);
|
||||
_c1 = 1.0 - _c2 - _c3;
|
||||
|
||||
Name = $"Ebsw({hpLength},{ssfLength})";
|
||||
WarmupPeriod = Math.Max(hpLength, ssfLength) + 3; // Need 3 bars for wave calculation
|
||||
|
||||
// Initialize state
|
||||
_s = new State(0, 0, 0, 0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
_barCount = 0;
|
||||
_p_barCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a chained Ehlers Even Better Sinewave indicator.
|
||||
/// </summary>
|
||||
/// <param name="source">The source indicator to chain from.</param>
|
||||
/// <param name="hpLength">Period for the high-pass filter.</param>
|
||||
/// <param name="ssfLength">Period for the super-smoother filter.</param>
|
||||
public Ebsw(ITValuePublisher source, int hpLength = 40, int ssfLength = 10) : this(hpLength, ssfLength)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
source.Pub += HandleInput;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleInput(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;
|
||||
_p_barCount = _barCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
_barCount = _p_barCount;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Handle non-finite values
|
||||
double src = input.Value;
|
||||
if (!double.IsFinite(src))
|
||||
{
|
||||
src = s.LastValidValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
s = s with { LastValidValue = src };
|
||||
}
|
||||
|
||||
// Increment bar count
|
||||
if (isNew)
|
||||
{
|
||||
_barCount++;
|
||||
}
|
||||
|
||||
// Shift source history
|
||||
double src1 = s.Src0;
|
||||
double src0 = src;
|
||||
|
||||
// High-pass filter: hp = 0.5 * (1 + alpha1) * (src - src[1]) + alpha1 * hp[1]
|
||||
double hp1 = s.Hp0;
|
||||
double hp0 = Math.FusedMultiplyAdd(0.5 * (1.0 + _alpha1), src0 - src1, _alpha1 * hp1);
|
||||
|
||||
// Super-smoother filter: filt = c1 * (hp + hp[1]) / 2 + c2 * filt[1] + c3 * filt[2]
|
||||
double filt2 = s.Filt1;
|
||||
double filt1 = s.Filt0;
|
||||
double filt0 = _c1 * (hp0 + hp1) * 0.5 + _c2 * filt1 + _c3 * filt2;
|
||||
|
||||
// Wave component: 3-bar average of filtered values
|
||||
double wave = (filt0 + filt1 + filt2) / 3.0;
|
||||
|
||||
// Power: 3-bar average of squared filtered values
|
||||
double pwr = (filt0 * filt0 + filt1 * filt1 + filt2 * filt2) / 3.0;
|
||||
|
||||
// Automatic gain control: normalize by RMS, clamp to [-1, +1]
|
||||
double sineWave = pwr > 0 ? wave / Math.Sqrt(pwr) : 0;
|
||||
sineWave = Math.Clamp(sineWave, -1.0, 1.0);
|
||||
|
||||
// Update state
|
||||
_s = new State(src0, src1, hp0, hp1, filt0, filt1, filt2, s.LastValidValue);
|
||||
|
||||
Last = new TValue(input.Time, sineWave);
|
||||
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, _hpLength, _ssfLength);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Prime state with all values (IIR-based indicator)
|
||||
foreach (var tv in source)
|
||||
{
|
||||
Update(tv);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_s = new State(0, 0, 0, 0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
_barCount = 0;
|
||||
_p_barCount = 0;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EBSW for a time series.
|
||||
/// </summary>
|
||||
public static TSeries Calculate(TSeries source, int hpLength = 40, int ssfLength = 10)
|
||||
{
|
||||
var ebsw = new Ebsw(hpLength, ssfLength);
|
||||
return ebsw.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EBSW in-place using a pre-allocated output span.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int hpLength = 40, int ssfLength = 10)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
}
|
||||
|
||||
if (hpLength < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(hpLength), "HP length must be at least 1.");
|
||||
}
|
||||
if (ssfLength < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(ssfLength), "SSF length must be at least 1.");
|
||||
}
|
||||
|
||||
// Validate that cos(2π/hpLength) is not zero (hpLength == 4 causes division by zero)
|
||||
double angleHp = 2.0 * Math.PI / hpLength;
|
||||
double cosAngleHp = Math.Cos(angleHp);
|
||||
if (Math.Abs(cosAngleHp) < 1e-10)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(hpLength), "hpLength cannot be 4 because cos(2π/hpLength) is zero, causing division by zero.");
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// High-pass filter coefficient
|
||||
double alpha1 = (1.0 - Math.Sin(angleHp)) / cosAngleHp;
|
||||
double hpCoef = 0.5 * (1.0 + alpha1);
|
||||
|
||||
// Super-smoother filter coefficients
|
||||
double angleSsf = Math.Sqrt(2.0) * Math.PI / ssfLength;
|
||||
double alpha2 = Math.Exp(-angleSsf);
|
||||
double beta = 2.0 * alpha2 * Math.Cos(angleSsf);
|
||||
double c2 = beta;
|
||||
double c3 = -(alpha2 * alpha2);
|
||||
double c1 = 1.0 - c2 - c3;
|
||||
|
||||
double src1 = 0, hp0 = 0, hp1 = 0;
|
||||
double filt0 = 0, filt1 = 0, filt2 = 0;
|
||||
double lastValid = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double src0 = source[i];
|
||||
if (!double.IsFinite(src0))
|
||||
{
|
||||
src0 = lastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValid = src0;
|
||||
}
|
||||
|
||||
// High-pass filter
|
||||
hp0 = Math.FusedMultiplyAdd(hpCoef, src0 - src1, alpha1 * hp1);
|
||||
|
||||
// Super-smoother filter
|
||||
filt0 = c1 * (hp0 + hp1) * 0.5 + c2 * filt1 + c3 * filt2;
|
||||
|
||||
// Wave component
|
||||
double wave = (filt0 + filt1 + filt2) / 3.0;
|
||||
|
||||
// Power
|
||||
double pwr = (filt0 * filt0 + filt1 * filt1 + filt2 * filt2) / 3.0;
|
||||
|
||||
// AGC normalization
|
||||
double sineWave = pwr > 0 ? wave / Math.Sqrt(pwr) : 0;
|
||||
output[i] = Math.Clamp(sineWave, -1.0, 1.0);
|
||||
|
||||
// Shift history
|
||||
src1 = src0;
|
||||
hp1 = hp0;
|
||||
filt2 = filt1;
|
||||
filt1 = filt0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+297
-93
@@ -1,123 +1,327 @@
|
||||
# EBSW: Ehlers Even Better Sinewave
|
||||
|
||||
## Overview and Purpose
|
||||
> "When you combine a high-pass filter with a super-smoother, you get cleaner cycles with automatic gain control."
|
||||
|
||||
The Ehlers Even Better Sinewave (EBSW) indicator, developed by John Ehlers, is an advanced cycle analysis tool. This implementation is based on a common interpretation that uses a cascade of filters: first, a High-Pass Filter (HPF) to detrend price data, followed by a Super Smoother Filter (SSF) to isolate the dominant cycle. The resulting filtered wave is then normalized using an Automatic Gain Control (AGC) mechanism, producing a bounded oscillator that fluctuates between approximately +1 and -1. It aims to provide a clear and responsive measure of market cycles.
|
||||
The Even Better Sinewave (EBSW) indicator, developed by John Ehlers, is a normalized cycle oscillator that extracts the dominant cycle from price data using a cascade of high-pass and super-smoother filters with automatic gain control (AGC). The output oscillates between -1 and +1, with zero crossings indicating potential turning points.
|
||||
|
||||
## Core Concepts
|
||||
## Historical Context
|
||||
|
||||
* **Detrending (High-Pass Filter):** A 1-pole High-Pass Filter removes the longer-term trend component from the price data, allowing the indicator to focus on cyclical movements.
|
||||
* **Cycle Smoothing (Super Smoother Filter):** Ehlers' Super Smoother Filter is applied to the detrended data to further refine the cycle component, offering effective smoothing with relatively low lag.
|
||||
* **Wave Generation:** The output of the SSF is averaged over a short period (typically 3 bars) to create the primary "wave".
|
||||
* **Automatic Gain Control (AGC):** The wave's amplitude is normalized by dividing it by the square root of its recent power (average of squared values). This keeps the oscillator bounded and responsive to changes in volatility.
|
||||
* **Normalized Oscillator:** The final output is a single sinewave-like oscillator.
|
||||
John Ehlers introduced the Even Better Sinewave as an improvement over earlier sinewave indicators. The original sinewave indicator suffered from trend contamination and noise sensitivity. EBSW addresses these issues through a multi-stage filtering approach:
|
||||
|
||||
## Common Settings and Parameters
|
||||
1. **High-pass filter** removes the DC (trend) component
|
||||
2. **Super-smoother filter** eliminates high-frequency noise
|
||||
3. **Automatic gain control** normalizes the output regardless of volatility
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
| --------- | ------- | -------- | -------------- |
|
||||
| Source | source | Data source for calculation. | Typically `close`, but `hlc3` or `ohlc4` can be used for a more comprehensive price representation. |
|
||||
| HP Length | 40 | Lookback period for the 1-pole High-Pass Filter used for detrending. | Shorter periods make the filter more responsive to shorter cycles; longer periods focus on longer-term cycles. Adjust based on observed cycle characteristics. |
|
||||
| SSF Length | 10 | Lookback period for the Super Smoother Filter used for smoothing the detrended cycle component. | Shorter periods result in a more responsive (but potentially noisier) wave; longer periods provide more smoothing. |
|
||||
The "Even Better" in the name reflects Ehlers' iterative refinement process—each successive sinewave indicator addressed limitations of its predecessors. EBSW represents the culmination of this evolution, providing a robust cycle indicator suitable for both trending and ranging markets.
|
||||
|
||||
**Pro Tip:** The `HP Length` and `SSF Length` parameters should be tuned based on the typical cycle lengths observed in the market and the desired responsiveness of the indicator.
|
||||
Unlike traditional oscillators that use arbitrary overbought/oversold levels, EBSW's AGC ensures the output always spans the full [-1, +1] range, making interpretation consistent across different instruments and timeframes.
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
## Architecture & Physics
|
||||
|
||||
**Simplified explanation:**
|
||||
1. Remove the trend from the price data using a 1-pole High-Pass Filter.
|
||||
2. Smooth the detrended data using a Super Smoother Filter to get a clean cycle component.
|
||||
3. Average the output of the Super Smoother Filter over the last 3 bars to create a "Wave".
|
||||
4. Calculate the average "Power" of the Super Smoother Filter output over the last 3 bars.
|
||||
5. Normalize the "Wave" by dividing it by the square root of the "Power" to get the final EBSW value.
|
||||
EBSW uses a two-stage IIR filter cascade followed by wave extraction and normalization.
|
||||
|
||||
**Technical formula (conceptual):**
|
||||
1. **High-Pass Filter (HPF - 1-pole):**
|
||||
`angle_hp = 2 * PI / hpLength`
|
||||
`alpha1_hp = (1 - sin(angle_hp)) / cos(angle_hp)`
|
||||
`HP = (0.5 * (1 + alpha1_hp) * (src - src[1])) + alpha1_hp * HP[1]`
|
||||
2. **Super Smoother Filter (SSF):**
|
||||
`angle_ssf = sqrt(2) * PI / ssfLength`
|
||||
`alpha2_ssf = exp(-angle_ssf)`
|
||||
`beta_ssf = 2 * alpha2_ssf * cos(angle_ssf)`
|
||||
`c2 = beta_ssf`
|
||||
`c3 = -alpha2_ssf^2`
|
||||
`c1 = 1 - c2 - c3`
|
||||
`Filt = c1 * (HP + HP[1])/2 + c2*Filt[1] + c3*Filt[2]`
|
||||
3. **Wave Generation:**
|
||||
`WaveVal = (Filt + Filt[1] + Filt[2]) / 3`
|
||||
4. **Power & Automatic Gain Control (AGC):**
|
||||
`Pwr = (Filt^2 + Filt[1]^2 + Filt[2]^2) / 3`
|
||||
`EBSW_SineWave = WaveVal / sqrt(Pwr)` (with check for Pwr == 0)
|
||||
### Core Components
|
||||
|
||||
> 🔍 **Technical Note:** The combination of HPF and SSF creates a form of band-pass filter. The AGC mechanism ensures the output remains scaled, typically between -1 and +1, making it behave like a normalized oscillator.
|
||||
1. **High-Pass Filter**: Single-pole IIR filter that removes trend/DC component
|
||||
2. **Super-Smoother Filter**: Two-pole IIR filter (Butterworth-style) for noise reduction
|
||||
3. **Wave Calculator**: Three-bar average of filtered values
|
||||
4. **Power Calculator**: Three-bar RMS (root mean square) for normalization
|
||||
5. **AGC Normalizer**: Divides wave by RMS, clamps to [-1, +1]
|
||||
|
||||
## Interpretation Details
|
||||
### Filter Cascade
|
||||
|
||||
* **Cycle Identification:** The EBSW wave shows the current phase and strength of the dominant market cycle as filtered by the indicator. Peaks suggest cycle tops, and troughs suggest cycle bottoms.
|
||||
* **Trend Reversals/Momentum Shifts:** When the EBSW wave crosses the zero line, it can indicate a potential shift in the short-term cyclical momentum.
|
||||
* Crossing up through zero: Potential start of a bullish cyclical phase.
|
||||
* Crossing down through zero: Potential start of a bearish cyclical phase.
|
||||
* **Overbought/Oversold Levels:** While normalized, traders often establish subjective or statistically derived overbought/oversold levels (e.g., +0.85 and -0.85, or other values like +0.7, +0.9).
|
||||
* Reaching above the overbought level and turning down may signal a potential cyclical peak.
|
||||
* Falling below the oversold level and turning up may signal a potential cyclical trough.
|
||||
```
|
||||
Price → High-Pass → Super-Smoother → Wave/Power → AGC → Sinewave
|
||||
(detrend) (smooth) (3-bar avg) (normalize)
|
||||
```
|
||||
|
||||
## Limitations and Considerations
|
||||
### State Management
|
||||
|
||||
* **Parameter Sensitivity:** The indicator's performance depends on tuning `hpLength` and `ssfLength` to prevailing market conditions.
|
||||
* **Non-Stationary Markets:** In strongly trending markets with weak cyclical components, or in very choppy non-cyclical conditions, the EBSW may produce less reliable signals.
|
||||
* **Lag:** All filtering introduces some lag. The Super Smoother Filter is designed to minimize this for its degree of smoothing, but lag is still present.
|
||||
* **Whipsaws:** Rapid oscillations around the zero line can occur in volatile or directionless markets.
|
||||
* **Requires Confirmation:** Signals from EBSW are often best confirmed with other forms of technical analysis (e.g., price action, volume, other non-correlated indicators).
|
||||
The indicator maintains:
|
||||
- Two source values (current and previous)
|
||||
- Two high-pass values (current and previous)
|
||||
- Three filter values (current, previous, two-back)
|
||||
- Last valid value for NaN handling
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### High-Pass Filter Coefficient
|
||||
|
||||
The high-pass filter uses an angular frequency based on the period:
|
||||
|
||||
$$
|
||||
\theta_{hp} = \frac{2\pi}{HP_{length}}
|
||||
$$
|
||||
|
||||
$$
|
||||
\alpha_1 = \frac{1 - \sin(\theta_{hp})}{\cos(\theta_{hp})}
|
||||
$$
|
||||
|
||||
This coefficient determines how much of the previous high-pass output carries forward. Larger HP length → larger $\alpha_1$ → more low-frequency rejection.
|
||||
|
||||
### High-Pass Filter Equation
|
||||
|
||||
$$
|
||||
HP_t = 0.5 \cdot (1 + \alpha_1) \cdot (P_t - P_{t-1}) + \alpha_1 \cdot HP_{t-1}
|
||||
$$
|
||||
|
||||
The first term applies a differencing operation (removes DC) weighted by $(1 + \alpha_1)/2$. The second term provides recursive smoothing.
|
||||
|
||||
### Super-Smoother Filter Coefficients
|
||||
|
||||
The super-smoother uses a critically damped two-pole design:
|
||||
|
||||
$$
|
||||
\theta_{ssf} = \frac{\sqrt{2} \cdot \pi}{SSF_{length}}
|
||||
$$
|
||||
|
||||
$$
|
||||
\alpha_2 = e^{-\theta_{ssf}}
|
||||
$$
|
||||
|
||||
$$
|
||||
\beta = 2 \cdot \alpha_2 \cdot \cos(\theta_{ssf})
|
||||
$$
|
||||
|
||||
$$
|
||||
c_2 = \beta, \quad c_3 = -\alpha_2^2, \quad c_1 = 1 - c_2 - c_3
|
||||
$$
|
||||
|
||||
Note: The coefficients sum to 1, ensuring DC gain of 1 for non-zero-mean signals (though the high-pass removes DC anyway).
|
||||
|
||||
### Super-Smoother Filter Equation
|
||||
|
||||
$$
|
||||
Filt_t = \frac{c_1}{2} \cdot (HP_t + HP_{t-1}) + c_2 \cdot Filt_{t-1} + c_3 \cdot Filt_{t-2}
|
||||
$$
|
||||
|
||||
The input is averaged to reduce aliasing artifacts. The two feedback terms create the smooth response.
|
||||
|
||||
### Wave Component (3-Bar Average)
|
||||
|
||||
$$
|
||||
Wave_t = \frac{Filt_t + Filt_{t-1} + Filt_{t-2}}{3}
|
||||
$$
|
||||
|
||||
### Power Component (3-Bar RMS)
|
||||
|
||||
$$
|
||||
Pwr_t = \frac{Filt_t^2 + Filt_{t-1}^2 + Filt_{t-2}^2}{3}
|
||||
$$
|
||||
|
||||
### AGC Normalization
|
||||
|
||||
$$
|
||||
Sinewave_t = \text{clamp}\left(\frac{Wave_t}{\sqrt{Pwr_t}}, -1, +1\right)
|
||||
$$
|
||||
|
||||
When $Pwr_t = 0$ (constant input), the division returns 0.
|
||||
|
||||
### Example Calculation
|
||||
|
||||
For default parameters (HP length=40, SSF length=10):
|
||||
|
||||
$$
|
||||
\theta_{hp} = \frac{2\pi}{40} \approx 0.157
|
||||
$$
|
||||
|
||||
$$
|
||||
\alpha_1 = \frac{1 - \sin(0.157)}{\cos(0.157)} \approx 0.843
|
||||
$$
|
||||
|
||||
$$
|
||||
\theta_{ssf} = \frac{\sqrt{2} \cdot \pi}{10} \approx 0.444
|
||||
$$
|
||||
|
||||
$$
|
||||
\alpha_2 = e^{-0.444} \approx 0.641
|
||||
$$
|
||||
|
||||
$$
|
||||
c_1 \approx 0.213, \quad c_2 \approx 1.198, \quad c_3 \approx -0.411
|
||||
$$
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, per Bar)
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | ~15 ns/bar | O(1) constant time |
|
||||
| **Allocations** | 0 | Zero-allocation in hot path |
|
||||
| **Complexity** | O(1) | Fixed operations per update |
|
||||
| **Accuracy** | 10 | Matches PineScript reference |
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| ADD/SUB | 9 | 1 | 9 |
|
||||
| MUL | 11 | 3 | 33 |
|
||||
| DIV | 1 | 15 | 15 |
|
||||
| SQRT | 1 | 15 | 15 |
|
||||
| **Total** | **22** | — | **~72 cycles** |
|
||||
### Operation Count (per update)
|
||||
|
||||
**Breakdown:**
|
||||
- High-Pass Filter (1-pole): 2 MUL + 2 ADD = 8 cycles
|
||||
- Super Smoother Filter: 4 MUL + 3 ADD = 15 cycles
|
||||
- Wave averaging (3 bars): 2 ADD + 1 DIV = 4 cycles
|
||||
- Power calculation: 3 MUL + 2 ADD = 11 cycles
|
||||
- AGC normalization: 1 SQRT + 1 DIV = 30 cycles
|
||||
|
||||
### Complexity Analysis
|
||||
|
||||
| Mode | Complexity | Notes |
|
||||
| Operation | Count | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| Streaming | O(1) | IIR filters + fixed 3-bar window |
|
||||
| Batch | O(n) | Linear scan, no lookback iteration |
|
||||
|
||||
**Memory**: ~48 bytes (filter states + 3-bar history for power)
|
||||
|
||||
### SIMD Analysis
|
||||
|
||||
| Optimization | Applicable | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| AVX2 vectorization | ❌ | IIR recursion prevents cross-bar parallelism |
|
||||
| FMA | ✅ | HPF: `α × (src - src[1]) + α × prev` |
|
||||
| Batch parallelism | ❌ | Sequential dependency on filter states |
|
||||
|
||||
**FMA Optimization:** Both HPF and SSF recursions benefit from FMA. SSF inner loop: `c1×avg + c2×prev1 + c3×prev2` reduces to 2 FMA + 1 MUL.
|
||||
| ADD/SUB | ~12 | Filter calculations, averaging |
|
||||
| MUL | ~10 | Coefficient multiplications |
|
||||
| DIV | 3 | Averaging and normalization |
|
||||
| SQRT | 1 | RMS calculation |
|
||||
| FMA | 2 | High-pass and smoother updates |
|
||||
| CLAMP | 1 | Output bounding |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 8/10 | Band-pass isolates dominant cycle |
|
||||
| **Timeliness** | 8/10 | Super Smoother minimizes lag |
|
||||
| **Overshoot** | 7/10 | AGC can amplify noise at low power |
|
||||
| **Smoothness** | 8/10 | Normalized output is well-bounded |
|
||||
| **Accuracy** | 10/10 | Exact match to reference |
|
||||
| **Timeliness** | 8/10 | Some lag from smoothing |
|
||||
| **Overshoot** | 9/10 | AGC prevents overshoot |
|
||||
| **Smoothness** | 9/10 | Dual filtering excellent |
|
||||
| **Normalization** | 10/10 | Always in [-1, +1] |
|
||||
|
||||
## 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 EBSW implementation |
|
||||
|
||||
EBSW is validated through mathematical properties:
|
||||
|
||||
- Constant price produces zero output (no cycles)
|
||||
- Output always bounded between -1 and +1
|
||||
- Pure sine wave input produces clean oscillation near ±1
|
||||
- Zero crossings align with cycle phase changes
|
||||
- AGC adapts to different volatility levels
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **HP Length Selection**: The high-pass length determines the longest cycle passed through. Set to approximately the dominant cycle period. Default 40 is suitable for daily data targeting ~8-week cycles.
|
||||
|
||||
2. **SSF Length Selection**: The super-smoother length controls noise filtering. Too short leaves noise; too long delays response. Typical ratio: SSF length = HP length / 4.
|
||||
|
||||
3. **Warmup Period**: EBSW needs `max(hpLength, ssfLength) + 3` bars to stabilize due to the three-bar wave calculation. Early values may not be reliable.
|
||||
|
||||
4. **Zero Crossings in Trends**: During strong trends, EBSW may oscillate around a non-zero mean. Zero crossings are most meaningful in ranging markets.
|
||||
|
||||
5. **AGC Saturation**: When EBSW reaches ±1, the cycle may be extended (not peaked). Look for the turn from ±1 rather than just the extreme values.
|
||||
|
||||
6. **Chained Indicators**: EBSW output is already normalized. Applying additional smoothing may distort the [-1, +1] property.
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Create an EBSW indicator with default parameters
|
||||
var ebsw = new Ebsw(hpLength: 40, ssfLength: 10);
|
||||
|
||||
// Update with new values
|
||||
var result = ebsw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
|
||||
// Access the last calculated value
|
||||
Console.WriteLine($"EBSW: {ebsw.Last.Value}"); // Always in [-1, +1]
|
||||
|
||||
// Chained usage
|
||||
var source = new TSeries();
|
||||
var ebswChained = new Ebsw(source, hpLength: 40, ssfLength: 10);
|
||||
|
||||
// Static batch calculation
|
||||
var output = Ebsw.Calculate(source, hpLength: 40, ssfLength: 10);
|
||||
|
||||
// Span-based calculation
|
||||
Span<double> outputSpan = stackalloc double[source.Count];
|
||||
Ebsw.Batch(source.Values, outputSpan, hpLength: 40, ssfLength: 10);
|
||||
```
|
||||
|
||||
## Applications
|
||||
|
||||
### Cycle Turning Points
|
||||
|
||||
EBSW zero crossings identify cycle inflection points:
|
||||
|
||||
- EBSW crosses above zero: cycle trough (potential buy signal)
|
||||
- EBSW crosses below zero: cycle peak (potential sell signal)
|
||||
|
||||
### Entry/Exit Timing
|
||||
|
||||
Use EBSW extremes for timing:
|
||||
|
||||
- EBSW near -1 and turning up: entering bullish phase
|
||||
- EBSW near +1 and turning down: entering bearish phase
|
||||
|
||||
### Trend Filtering
|
||||
|
||||
Combine with trend indicators:
|
||||
|
||||
- In uptrend: Enter long when EBSW crosses above zero
|
||||
- In downtrend: Enter short when EBSW crosses below zero
|
||||
|
||||
### Divergence Detection
|
||||
|
||||
EBSW divergences signal potential reversals:
|
||||
|
||||
- Price higher high, EBSW lower high: bearish divergence
|
||||
- Price lower low, EBSW higher low: bullish divergence
|
||||
|
||||
### Multi-Timeframe Analysis
|
||||
|
||||
EBSW on multiple timeframes provides confluence:
|
||||
|
||||
- Higher timeframe: Direction bias
|
||||
- Lower timeframe: Entry timing
|
||||
|
||||
## Comparison to Related Indicators
|
||||
|
||||
### EBSW vs Traditional Sinewave
|
||||
|
||||
| Feature | EBSW | Traditional Sinewave |
|
||||
| :--- | :--- | :--- |
|
||||
| Trend removal | High-pass filter | None or basic |
|
||||
| Noise handling | Super-smoother | Single EMA |
|
||||
| Normalization | AGC | Fixed or none |
|
||||
| Output range | Always [-1, +1] | Variable |
|
||||
|
||||
### EBSW vs RSI
|
||||
|
||||
| Feature | EBSW | RSI |
|
||||
| :--- | :--- | :--- |
|
||||
| Output range | [-1, +1] | [0, 100] |
|
||||
| Zero line | 0 (midpoint) | 50 |
|
||||
| Calculation | IIR filters + AGC | Up/down averaging |
|
||||
| Cycle focus | Yes | No |
|
||||
| Trend sensitivity | Low (high-pass) | High |
|
||||
|
||||
### EBSW vs Stochastic
|
||||
|
||||
| Feature | EBSW | Stochastic |
|
||||
| :--- | :--- | :--- |
|
||||
| Basis | Filtered cycles | Price range position |
|
||||
| Normalization | AGC (dynamic) | Fixed lookback range |
|
||||
| Smoothing | Two-pole IIR | Simple moving average |
|
||||
| Leading nature | Yes | Yes |
|
||||
|
||||
## Parameter Tuning
|
||||
|
||||
### For Shorter-Term Cycles (Intraday)
|
||||
|
||||
```csharp
|
||||
var ebsw = new Ebsw(hpLength: 20, ssfLength: 5);
|
||||
```
|
||||
|
||||
### For Medium-Term Cycles (Daily)
|
||||
|
||||
```csharp
|
||||
var ebsw = new Ebsw(hpLength: 40, ssfLength: 10);
|
||||
```
|
||||
|
||||
### For Longer-Term Cycles (Weekly)
|
||||
|
||||
```csharp
|
||||
var ebsw = new Ebsw(hpLength: 80, ssfLength: 20);
|
||||
```
|
||||
|
||||
### Adaptive Approach
|
||||
|
||||
Use cycle measurement (e.g., autocorrelation, Homodyne Discriminator) to dynamically adjust HP length to match the detected dominant cycle.
|
||||
|
||||
## References
|
||||
|
||||
* Ehlers, J. F. (2002). *Rocket Science for Traders: Digital Signal Processing Applications*. John Wiley & Sons.
|
||||
* Ehlers, J. F. (2013). *Cycle Analytics for Traders: Advanced Technical Trading Concepts*. John Wiley & Sons.
|
||||
- Ehlers, J.F. (2013). *Cycle Analytics for Traders*. Wiley.
|
||||
- Ehlers, J.F. (2001). *Rocket Science for Traders*. Wiley.
|
||||
- TradingView PineScript: Even Better Sinewave indicator implementation.
|
||||
- Original PineScript reference: `ebsw.pine` in QuanTAlib repository.
|
||||
Reference in New Issue
Block a user