mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-15 09:08:04 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user