Add Ultimate Oscillator implementation and documentation

- Introduced the Ultimate Oscillator (UltOsc) indicator with detailed mathematical foundation and performance profile.
- Added historical context and common pitfalls for better user understanding.
- Implemented Bilateral filter with enhanced update methods and batch calculations.
- Updated Blackman Moving Average (BLMA) with improved handling of NaN values and batch processing capabilities.
- Created unit tests for AmatIndicator to ensure proper functionality and signal generation.
- Integrated AmatIndicator into the Quantower platform with appropriate line series for trend and strength visualization.
- Updated project file to include new indicator implementations.
This commit is contained in:
Miha Kralj
2025-12-31 23:30:54 -08:00
parent a42c9acd0b
commit 11f4ec2497
18 changed files with 3471 additions and 66 deletions
+261
View File
@@ -0,0 +1,261 @@
using Xunit;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class AmatIndicatorTests
{
[Fact]
public void AmatIndicator_Constructor_SetsDefaults()
{
var indicator = new AmatIndicator();
Assert.Equal(10, indicator.FastPeriod);
Assert.Equal(50, indicator.SlowPeriod);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("AMAT - Archer Moving Averages Trends", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.False(indicator.OnBackGround);
}
[Fact]
public void AmatIndicator_MinHistoryDepths_IsSlowPeriod()
{
var indicator = new AmatIndicator { FastPeriod = 10, SlowPeriod = 50 };
Assert.Equal(50, indicator.MinHistoryDepths);
indicator = new AmatIndicator { FastPeriod = 5, SlowPeriod = 100 };
Assert.Equal(100, indicator.MinHistoryDepths);
}
[Fact]
public void AmatIndicator_ShortName_IncludesParameters()
{
var indicator = new AmatIndicator { FastPeriod = 10, SlowPeriod = 50 };
Assert.Equal("AMAT(10,50)", indicator.ShortName);
indicator = new AmatIndicator { FastPeriod = 5, SlowPeriod = 20 };
Assert.Equal("AMAT(5,20)", indicator.ShortName);
}
[Fact]
public void AmatIndicator_Initialize_CreatesLineSeries()
{
var indicator = new AmatIndicator { FastPeriod = 10, SlowPeriod = 50 };
indicator.Initialize();
// Should have 5 line series: Trend, Strength, Fast EMA, Slow EMA, Zero
Assert.Equal(5, indicator.LinesSeries.Count);
Assert.Equal("Trend", indicator.LinesSeries[0].Name);
Assert.Equal("Strength", indicator.LinesSeries[1].Name);
Assert.Equal("Fast EMA", indicator.LinesSeries[2].Name);
Assert.Equal("Slow EMA", indicator.LinesSeries[3].Name);
Assert.Equal("Zero", indicator.LinesSeries[4].Name);
}
[Fact]
public void AmatIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new AmatIndicator { FastPeriod = 3, SlowPeriod = 10 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// After one bar, all 5 series should have values
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.Equal(1, indicator.LinesSeries[1].Count);
Assert.Equal(1, indicator.LinesSeries[2].Count);
Assert.Equal(1, indicator.LinesSeries[3].Count);
Assert.Equal(1, indicator.LinesSeries[4].Count);
}
[Fact]
public void AmatIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new AmatIndicator { FastPeriod = 3, SlowPeriod = 10 };
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 AmatIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new AmatIndicator { FastPeriod = 3, SlowPeriod = 10 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
// NewTick should update without crashing
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void AmatIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new AmatIndicator { FastPeriod = 3, SlowPeriod = 10 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Add bars in uptrend
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(
now.AddMinutes(i),
100 + i * 2,
105 + i * 2,
95 + i * 2,
102 + i * 2);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.Equal(20, indicator.LinesSeries[0].Count);
// Check that values are finite
for (int i = 0; i < 20; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i)));
Assert.True(double.IsFinite(indicator.LinesSeries[1].GetValue(i)));
Assert.True(double.IsFinite(indicator.LinesSeries[2].GetValue(i)));
Assert.True(double.IsFinite(indicator.LinesSeries[3].GetValue(i)));
Assert.Equal(0, indicator.LinesSeries[4].GetValue(i)); // Zero line
}
}
[Fact]
public void AmatIndicator_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 AmatIndicator
{
FastPeriod = 3,
SlowPeriod = 10,
Source = source
};
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// All source types should produce values without crashing
Assert.Equal(1, indicator.LinesSeries[0].Count);
}
}
[Fact]
public void AmatIndicator_FastPeriod_CanBeChanged()
{
var indicator = new AmatIndicator();
indicator.FastPeriod = 5;
Assert.Equal(5, indicator.FastPeriod);
Assert.Equal("AMAT(5,50)", indicator.ShortName);
}
[Fact]
public void AmatIndicator_SlowPeriod_CanBeChanged()
{
var indicator = new AmatIndicator();
indicator.SlowPeriod = 100;
Assert.Equal(100, indicator.SlowPeriod);
Assert.Equal(100, indicator.MinHistoryDepths);
Assert.Equal("AMAT(10,100)", indicator.ShortName);
}
[Fact]
public void AmatIndicator_ShowColdValues_False_SetsNaN()
{
var indicator = new AmatIndicator
{
FastPeriod = 3,
SlowPeriod = 100,
ShowColdValues = false
};
indicator.Initialize();
var now = DateTime.UtcNow;
// Add a few bars (less than warmup)
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// With ShowColdValues = false, cold values should be NaN
// (before warmup is complete)
Assert.True(double.IsNaN(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void AmatIndicator_Uptrend_ProducesBullishSignal()
{
var indicator = new AmatIndicator { FastPeriod = 3, SlowPeriod = 10 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Create a strong uptrend
for (int i = 0; i < 30; i++)
{
double price = 100 + i * 5;
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// After warmup in uptrend, should show bullish (+1)
double lastTrend = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(1.0, lastTrend);
}
[Fact]
public void AmatIndicator_Downtrend_ProducesBearishSignal()
{
var indicator = new AmatIndicator { FastPeriod = 3, SlowPeriod = 10 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Create a strong downtrend
for (int i = 0; i < 30; i++)
{
double price = 200 - i * 5;
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// After warmup in downtrend, should show bearish (-1)
double lastTrend = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(-1.0, lastTrend);
}
}
+98
View File
@@ -0,0 +1,98 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
using static QuanTAlib.IndicatorExtensions;
namespace QuanTAlib;
/// <summary>
/// AMAT (Archer Moving Averages Trends) Quantower indicator.
/// Uses dual EMAs to identify trend direction and strength.
/// </summary>
public class AmatIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Fast Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
public int FastPeriod { get; set; } = 10;
[InputParameter("Slow Period", sortIndex: 11, minimum: 2, maximum: 1000, increment: 1, decimalPlaces: 0)]
public int SlowPeriod { get; set; } = 50;
[DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show Cold Values", sortIndex: 100)]
public bool ShowColdValues { get; set; } = true;
private Amat? _amat;
private Func<IHistoryItem, double>? _selector;
public int MinHistoryDepths => SlowPeriod;
public override string ShortName => $"AMAT({FastPeriod},{SlowPeriod})";
public AmatIndicator()
{
Name = "AMAT - Archer Moving Averages Trends";
Description = "Identifies trend direction using dual EMA alignment";
SeparateWindow = true;
OnBackGround = false;
}
protected override void OnInit()
{
_amat = new Amat(FastPeriod, SlowPeriod);
_selector = Source.GetPriceSelector();
// Trend line: +1 = bullish, -1 = bearish, 0 = neutral
AddLineSeries(new LineSeries("Trend", Momentum, 2, LineStyle.Histogramm));
// Strength line: percentage separation
AddLineSeries(new LineSeries("Strength", Color.FromArgb(255, 200, 128), 1, LineStyle.Solid));
// Fast EMA line
AddLineSeries(new LineSeries("Fast EMA", Color.FromArgb(100, 200, 100), 1, LineStyle.Solid));
// Slow EMA line
AddLineSeries(new LineSeries("Slow EMA", Color.FromArgb(200, 100, 100), 1, LineStyle.Solid));
// Zero line reference
AddLineSeries(new LineSeries("Zero", Color.Gray, 1, LineStyle.Dot));
}
protected override void OnUpdate(UpdateArgs args)
{
if (_amat == null || _selector == null) return;
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
bool isNew = args.IsNewBar();
TValue input = new(item.TimeLeft, value);
_amat.Update(input, isNew);
bool isHot = _amat.IsHot;
// Trend line
LinesSeries[0].SetValue(_amat.Last.Value, isHot, ShowColdValues);
// Strength line
LinesSeries[1].SetValue(_amat.Strength.Value, isHot, ShowColdValues);
// Fast EMA line
LinesSeries[2].SetValue(_amat.FastEma.Value, isHot, ShowColdValues);
// Slow EMA line
LinesSeries[3].SetValue(_amat.SlowEma.Value, isHot, ShowColdValues);
// Zero reference line
LinesSeries[4].SetValue(0);
// Color the trend histogram based on direction
if (isHot || ShowColdValues)
{
double trend = _amat.Last.Value;
Color trendColor = trend > 0 ? Color.Green :
trend < 0 ? Color.Red :
Color.Gray;
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(trendColor));
}
}
}
+6
View File
@@ -54,6 +54,12 @@
<Compile Include="..\lib\volatility\**\*.cs" Exclude="..\lib\volatility\**\*.Tests.cs;..\lib\volatility\**\*.Validation.Tests.cs;..\lib\volatility\**\obj\**;..\lib\volatility\**\bin\**" />
<!-- Include IndicatorExtensions -->
<Compile Include="IndicatorExtensions.cs" />
<!-- Include Quantower adapter implementations -->
<Compile Include="Momentum\*.cs" Exclude="Momentum\*.Tests.cs" />
<Compile Include="Volume\*.cs" Exclude="Volume\*.Tests.cs" />
<Compile Include="Statistics\*.cs" Exclude="Statistics\*.Tests.cs" />
<Compile Include="Volatility\*.cs" Exclude="Volatility\*.Tests.cs" />
<Compile Include="Trends\*.cs" Exclude="Trends\*.Tests.cs" />
</ItemGroup>
</Project>