Refactor TBar struct for improved equality comparison and string representation; update Benchmark program structure for better organization; modify Averages project file to include specific source files; add Directory.Build.props for common project settings; implement comprehensive tests for Ema, Sma, and Wma indicators; create mock classes for TradingPlatform.BusinessLayer to facilitate testing; enhance Quantower test project configuration for better test management.

This commit is contained in:
Miha Kralj
2025-12-01 18:40:23 -08:00
parent 626a2afa9b
commit 1d145d0622
12 changed files with 1098 additions and 114175 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="*.cs" />
<Compile Include="IndicatorExtensions.cs" />
<Compile Include="..\lib\core\**\*.cs" Exclude="..\lib\core\**\*.Tests.cs" />
<Compile Include="..\lib\averages\**\*.cs" Exclude="..\lib\averages\**\*.Tests.cs" />
<Reference Include="TradingPlatform.BusinessLayer">
+16
View File
@@ -0,0 +1,16 @@
<Project>
<!-- Set project-specific intermediate output paths before SDK import -->
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'Averages'">
<BaseIntermediateOutputPath>obj\Averages\</BaseIntermediateOutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'Quantower.Tests'">
<BaseIntermediateOutputPath>obj\Tests\</BaseIntermediateOutputPath>
</PropertyGroup>
<!-- Common settings for all quantower projects -->
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+169
View File
@@ -0,0 +1,169 @@
using Xunit;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class EmaIndicatorTests
{
[Fact]
public void EmaIndicator_Constructor_SetsDefaults()
{
var indicator = new EmaIndicator();
Assert.Equal(10, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("EMA - Exponential Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void EmaIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new EmaIndicator { Period = 20 };
Assert.Equal(20, indicator.MinHistoryDepths);
Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void EmaIndicator_ShortName_IncludesPeriodAndSource()
{
var indicator = new EmaIndicator { Period = 15 };
Assert.Contains("EMA", indicator.ShortName);
Assert.Contains("15", indicator.ShortName);
}
[Fact]
public void EmaIndicator_Initialize_CreatesInternalEma()
{
var indicator = new EmaIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void EmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new EmaIndicator { Period = 3 };
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 EmaIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new EmaIndicator { Period = 3 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
// Process first update
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
// Line series should have values
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void EmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new EmaIndicator { Period = 3 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
// Process historical bar first
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstValue = indicator.LinesSeries[0].GetValue(0);
// Update with new tick (same bar data - simulates intrabar update)
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
// Both values should be finite
Assert.True(double.IsFinite(firstValue));
Assert.True(double.IsFinite(secondValue));
}
[Fact]
public void EmaIndicator_MultipleUpdates_ProducesCorrectEmaSequence()
{
var indicator = new EmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105, 107, 106 };
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)));
}
// EMA should be smoothing the values
// Last EMA value should be between first and last close
double lastEma = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastEma >= 100 && lastEma <= 110);
}
[Fact]
public void EmaIndicator_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 EmaIndicator { Period = 3, 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 EmaIndicator_Period_CanBeChanged()
{
var indicator = new EmaIndicator { Period = 5 };
Assert.Equal(5, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
Assert.Equal(20, indicator.MinHistoryDepths);
}
}
+490
View File
@@ -0,0 +1,490 @@
// Mock types for TradingPlatform.BusinessLayer to enable testing
// These are minimal implementations for unit testing purposes only
using System.Drawing;
namespace TradingPlatform.BusinessLayer;
#region Enums
/// <summary>
/// Specifies the style of indicator line.
/// </summary>
public enum LineStyle
{
Solid,
Dash,
Dot,
DashDot,
Histogramm,
Points,
Columns,
StepLine
}
/// <summary>
/// Price data types
/// </summary>
public enum PriceType
{
Open,
High,
Low,
Close,
Median,
Typical,
Weighted,
Bid,
BidSize,
Ask,
AskSize,
Last,
Volume,
Ticks,
AggressorFlag,
TickDirection,
BidTickDirection,
AskTickDirection,
OpenInterest,
Mark,
FundingRate,
QuoteAssetVolume
}
/// <summary>
/// Seek origin for historical data
/// </summary>
public enum SeekOriginHistory
{
Begin,
End
}
/// <summary>
/// Update reason for indicator
/// </summary>
public enum UpdateReason
{
Unknown,
HistoricalBar,
NewTick,
NewBar
}
#endregion
#region Attributes
/// <summary>
/// Attribute for input parameters
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class InputParameterAttribute : Attribute
{
public string Name { get; }
public int SortIndex { get; }
public double Minimum { get; }
public double Maximum { get; }
public double Increment { get; }
public int DecimalPlaces { get; }
public IComparable[]? Variants { get; }
public InputParameterAttribute(
string name = "",
int sortIndex = 0,
double minimum = int.MinValue,
double maximum = int.MaxValue,
double increment = 0.01,
int decimalPlaces = 2,
object[]? variants = null)
{
Name = name;
SortIndex = sortIndex;
Minimum = minimum;
Maximum = maximum;
Increment = increment;
DecimalPlaces = decimalPlaces;
Variants = variants?.Cast<IComparable>().ToArray();
}
}
#endregion
#region History Item
/// <summary>
/// History item interface
/// </summary>
public interface IHistoryItem
{
DateTime TimeLeft { get; }
long TicksLeft { get; set; }
long TicksRight { get; set; }
double this[PriceType priceType] { get; }
}
/// <summary>
/// Mock history item for testing
/// </summary>
public class MockHistoryItem : IHistoryItem
{
public DateTime TimeLeft { get; set; }
public long TicksLeft { get; set; }
public long TicksRight { get; set; }
public double Open { get; set; }
public double High { get; set; }
public double Low { get; set; }
public double Close { get; set; }
public double Volume { get; set; }
public double this[PriceType priceType] => priceType switch
{
PriceType.Open => Open,
PriceType.High => High,
PriceType.Low => Low,
PriceType.Close => Close,
PriceType.Volume => Volume,
PriceType.Median => (High + Low) / 2,
PriceType.Typical => (High + Low + Close) / 3,
PriceType.Weighted => (High + Low + Close + Close) / 4,
_ => Close
};
}
#endregion
#region Historical Data
/// <summary>
/// Mock historical data for testing
/// </summary>
public class HistoricalData
{
private readonly List<IHistoryItem> _items = new();
public int Count => _items.Count;
public IHistoryItem this[int offset, SeekOriginHistory origin = SeekOriginHistory.End]
{
get
{
int index = origin == SeekOriginHistory.End
? Count - 1 - offset
: offset;
return _items[index];
}
}
public DateTime Time(int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End)
{
return this[offset, origin].TimeLeft;
}
public long GetIndexByTime(long ticks)
{
for (int i = 0; i < _items.Count; i++)
{
if (_items[i].TicksLeft == ticks)
return i;
}
return -1;
}
public void Add(IHistoryItem item)
{
_items.Add(item);
}
public void AddBar(DateTime time, double open, double high, double low, double close, double volume = 0)
{
_items.Add(new MockHistoryItem
{
TimeLeft = time,
TicksLeft = time.Ticks,
TicksRight = time.Ticks,
Open = open,
High = high,
Low = low,
Close = close,
Volume = volume
});
}
public void Clear() => _items.Clear();
}
#endregion
#region Update Args
/// <summary>
/// Update arguments for indicator
/// </summary>
public class UpdateArgs
{
public UpdateReason Reason { get; }
public UpdateArgs(UpdateReason reason)
{
Reason = reason;
}
}
#endregion
#region Line Series
/// <summary>
/// Base class for lines
/// </summary>
public abstract class Line
{
public string Name { get; set; }
public Color Color { get; set; }
public int Width { get; set; }
public LineStyle Style { get; set; }
public bool Visible { get; set; } = true;
protected Line(string name, Color color, int width, LineStyle style)
{
Name = name;
Color = color;
Width = width;
Style = style;
}
}
/// <summary>
/// Line series for indicator output
/// </summary>
public class LineSeries : Line
{
private readonly List<double> _values = new();
private readonly List<Color> _markers = new();
public int TimeShift { get; set; }
public int DrawBegin { get; set; }
public bool ShowLineMarker { get; set; } = true;
public LineSeries(string name, Color color, int width, LineStyle style)
: base(name, color, width, style)
{
}
public double this[int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End]
{
get => GetValue(offset, origin);
set => SetValue(value, offset, origin);
}
public double GetValue(int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End)
{
if (_values.Count == 0)
return double.NaN;
int index = origin == SeekOriginHistory.End
? _values.Count - 1 - offset
: offset;
if (index < 0 || index >= _values.Count)
return double.NaN;
return _values[index];
}
public void SetValue(double value, int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End)
{
EnsureCapacity(offset + 1);
int index = origin == SeekOriginHistory.End
? _values.Count - 1 - offset
: offset;
_values[index] = value;
}
public void SetMarker(int offset, Color color)
{
EnsureMarkerCapacity(offset + 1);
int index = _markers.Count - 1 - offset;
if (index >= 0 && index < _markers.Count)
_markers[index] = color;
}
internal void AddValue()
{
_values.Add(double.NaN);
_markers.Add(Color.Transparent);
}
private void EnsureCapacity(int count)
{
while (_values.Count < count)
_values.Add(double.NaN);
}
private void EnsureMarkerCapacity(int count)
{
while (_markers.Count < count)
_markers.Add(Color.Transparent);
}
public int Count => _values.Count;
public IReadOnlyList<double> Values => _values;
}
#endregion
#region Paint Chart Event Args
/// <summary>
/// Paint chart event arguments
/// </summary>
public class PaintChartEventArgs : EventArgs
{
public Graphics Graphics { get; }
public Rectangle ClipRectangle { get; }
public int WindowIndex { get; }
public PaintChartEventArgs(Graphics graphics, Rectangle clipRectangle, int windowIndex = 0)
{
Graphics = graphics;
ClipRectangle = clipRectangle;
WindowIndex = windowIndex;
}
}
#endregion
#region Chart
/// <summary>
/// Chart interface
/// </summary>
public interface IChart
{
ChartWindow MainWindow { get; }
ChartWindow[] Windows { get; }
int BarsWidth { get; }
}
/// <summary>
/// Chart window
/// </summary>
public class ChartWindow
{
public Rectangle ClientRectangle { get; set; }
public ICoordinatesConverter CoordinatesConverter { get; set; } = new MockCoordinatesConverter();
}
/// <summary>
/// Coordinates converter interface
/// </summary>
public interface ICoordinatesConverter
{
DateTime GetTime(int x);
double GetChartX(DateTime time);
double GetChartY(double value);
}
/// <summary>
/// Mock coordinates converter
/// </summary>
public class MockCoordinatesConverter : ICoordinatesConverter
{
public DateTime GetTime(int x) => DateTime.UtcNow;
public double GetChartX(DateTime time) => 0;
public double GetChartY(double value) => 0;
}
/// <summary>
/// Mock chart for testing
/// </summary>
public class MockChart : IChart
{
public ChartWindow MainWindow { get; } = new();
public ChartWindow[] Windows { get; } = new[] { new ChartWindow() };
public int BarsWidth { get; set; } = 10;
}
#endregion
#region Indicator Base
/// <summary>
/// Watchlist indicator interface
/// </summary>
public interface IWatchlistIndicator
{
int MinHistoryDepths { get; }
}
/// <summary>
/// Base class for indicators
/// </summary>
public abstract class Indicator
{
private readonly List<LineSeries> _lineSeries = new();
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public virtual string ShortName => Name;
public virtual string SourceCodeLink => string.Empty;
public bool SeparateWindow { get; set; }
public bool OnBackGround { get; set; }
public HistoricalData HistoricalData { get; set; } = new();
public IChart? CurrentChart { get; set; }
public int Count => HistoricalData.Count;
public LineSeries[] LinesSeries => _lineSeries.ToArray();
protected void AddLineSeries(LineSeries series)
{
_lineSeries.Add(series);
}
/// <summary>
/// Called when indicator is initialized
/// </summary>
protected virtual void OnInit()
{
}
/// <summary>
/// Called on each update
/// </summary>
protected virtual void OnUpdate(UpdateArgs args)
{
}
/// <summary>
/// Called for chart painting
/// </summary>
public virtual void OnPaintChart(PaintChartEventArgs args)
{
}
/// <summary>
/// Initialize the indicator (for testing)
/// </summary>
public void Initialize()
{
OnInit();
}
/// <summary>
/// Process an update (for testing)
/// </summary>
public void ProcessUpdate(UpdateArgs args)
{
// Ensure line series have capacity for new data
foreach (var series in _lineSeries)
{
series.AddValue();
}
OnUpdate(args);
}
}
#endregion
+35
View File
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Drawing.Common" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<!-- Include mock types -->
<Compile Include="Mocks\*.cs" />
<!-- Include test files -->
<Compile Include="*.Tests.cs" />
<!-- Include core library types -->
<Compile Include="..\lib\core\**\*.cs" Exclude="..\lib\core\**\*.Tests.cs" />
<!-- Include averages implementations -->
<Compile Include="..\lib\averages\**\*.cs" Exclude="..\lib\averages\**\*.Tests.cs;..\lib\averages\**\*.Validation.Tests.cs" />
<!-- Include IndicatorExtensions -->
<Compile Include="IndicatorExtensions.cs" />
</ItemGroup>
</Project>
+136
View File
@@ -0,0 +1,136 @@
using Xunit;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class SmaIndicatorTests
{
[Fact]
public void SmaIndicator_Constructor_SetsDefaults()
{
var indicator = new SmaIndicator();
Assert.Equal(10, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("SMA - Simple Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void SmaIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new SmaIndicator { Period = 20 };
Assert.Equal(20, indicator.MinHistoryDepths);
Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void SmaIndicator_ShortName_IncludesPeriodAndSource()
{
var indicator = new SmaIndicator { Period = 15 };
Assert.Contains("SMA", indicator.ShortName);
Assert.Contains("15", indicator.ShortName);
}
[Fact]
public void SmaIndicator_SourceCodeLink_IsValid()
{
var indicator = new SmaIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink);
Assert.Contains("Sma.Quantower.cs", indicator.SourceCodeLink);
}
[Fact]
public void SmaIndicator_Initialize_CreatesInternalSma()
{
var indicator = new SmaIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void SmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new SmaIndicator { Period = 3 };
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 SmaIndicator_MultipleUpdates_ProducesCorrectSmaSequence()
{
var indicator = new SmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105 };
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)));
}
// Last SMA(3) should be average of last 3 values: (103 + 105 + 104) / 3 ≈ 104
// Actually: (104 + 103 + 105) / 3 = 104
double lastSma = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastSma >= 103 && lastSma <= 105);
}
[Fact]
public void SmaIndicator_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 SmaIndicator { Period = 3, 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 SmaIndicator_Period_CanBeChanged()
{
var indicator = new SmaIndicator { Period = 5 };
Assert.Equal(5, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
Assert.Equal(20, indicator.MinHistoryDepths);
}
}
+144
View File
@@ -0,0 +1,144 @@
using Xunit;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class WmaIndicatorTests
{
[Fact]
public void WmaIndicator_Constructor_SetsDefaults()
{
var indicator = new WmaIndicator();
Assert.Equal(10, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("WMA - Weighted Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void WmaIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new WmaIndicator { Period = 20 };
Assert.Equal(20, indicator.MinHistoryDepths);
Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void WmaIndicator_ShortName_IncludesPeriodAndSource()
{
var indicator = new WmaIndicator { Period = 15 };
Assert.Contains("WMA", indicator.ShortName);
Assert.Contains("15", indicator.ShortName);
}
[Fact]
public void WmaIndicator_SourceCodeLink_IsValid()
{
var indicator = new WmaIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink);
Assert.Contains("Wma.Quantower.cs", indicator.SourceCodeLink);
}
[Fact]
public void WmaIndicator_Initialize_CreatesInternalWma()
{
var indicator = new WmaIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void WmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new WmaIndicator { Period = 3 };
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 WmaIndicator_MultipleUpdates_ProducesCorrectWmaSequence()
{
var indicator = new WmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105 };
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)));
}
// WMA gives more weight to recent values
// With weights [1, 2, 3] for period 3: (104*1 + 103*2 + 105*3) / 6 = 625/6 ≈ 104.17
double lastWma = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastWma >= 103 && lastWma <= 106);
}
[Fact]
public void WmaIndicator_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 WmaIndicator { Period = 3, 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 WmaIndicator_Period_CanBeChanged()
{
var indicator = new WmaIndicator { Period = 5 };
Assert.Equal(5, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
Assert.Equal(20, indicator.MinHistoryDepths);
}
[Fact]
public void WmaIndicator_DescriptionIsSet()
{
var indicator = new WmaIndicator();
Assert.Contains("Weighted", indicator.Description);
}
}