Refactor T3 Moving Average Implementation and Remove Unused Tests

- Deleted DebugTulip.Tests.cs as it was no longer needed.
- Refactored T3.cs to encapsulate parameters in a struct for better organization and readability.
- Updated methods in T3.cs to use the new Parameters struct, improving clarity and reducing redundancy.
- Enhanced T3.md documentation to provide clearer explanations of the T3 moving average and its parameters.
- Removed Wma.Coverage.Tests.cs as it was obsolete.
- Added new tests in IndicatorExtensions.Tests.cs to validate logic methods and ensure correct calculations.
- Updated IndicatorExtensions.cs to improve method organization and add new functionality for handling chart coordinates.
- Refactored mocks in TradingPlatformMocks.cs to align with new chart interface definitions.
This commit is contained in:
Miha Kralj
2025-12-07 17:32:01 -08:00
parent 94d06b0749
commit 3975ff2d7f
10 changed files with 330 additions and 850 deletions
+131 -31
View File
@@ -1,5 +1,6 @@
using Xunit;
using TradingPlatform.BusinessLayer;
using TradingPlatform.BusinessLayer.Chart;
using System.Drawing;
using System.Reflection;
@@ -15,14 +16,14 @@ public class IndicatorExtensionsTests
}
}
private sealed class TestCoordinatesConverter : ICoordinatesConverter
private sealed class TestCoordinatesConverter : IChartWindowCoordinatesConverter
{
private readonly DateTime _time;
public TestCoordinatesConverter(DateTime time) => _time = time;
public DateTime GetTime(int x) => _time;
public double GetChartX(DateTime time) => 0;
public double GetChartY(double value) => 0;
public double GetChartX(DateTime time) => 10; // Return a fixed X for testing
public double GetChartY(double value) => value; // Return value as Y for testing
}
[Fact]
@@ -108,21 +109,8 @@ public class IndicatorExtensionsTests
}
[Fact]
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
public void PaintMethods_DoNotThrow_WithValidGraphics()
public void LogicMethods_CalculateCorrectly()
{
// This test attempts to verify that paint methods don't crash.
// It requires System.Drawing.Common to be functional.
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
// Skip on non-Windows if System.Drawing is not fully supported (GDI+)
return;
}
using var bitmap = new Bitmap(100, 100);
using var graphics = Graphics.FromImage(bitmap);
var indicator = new TestIndicator();
indicator.CurrentChart = new MockChart();
@@ -133,9 +121,92 @@ public class IndicatorExtensionsTests
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 105);
}
// Setup converter to return a time that exists in our data (e.g. the middle bar)
// We added bars at now, now+1min, ..., now+19min.
// Let's return now+10min.
// Setup converter
var validTime = now.AddMinutes(10);
var converter = new TestCoordinatesConverter(validTime);
indicator.CurrentChart.MainWindow.CoordinatesConverter = converter;
var clientRect = new Rectangle(0, 0, 100, 100);
// 1. Test GetHLineY
int y = IndicatorExtensions.GetHLineY(converter, 50.0);
Assert.Equal(50, y); // Since our mock returns value as Y
// 2. Test GetSmoothCurvePoints
var series = new LineSeries("Test", Color.Blue, 1, LineStyle.Solid);
for (int i = 0; i < 20; i++) series.AddValue();
for (int i = 0; i < 20; i++) series.SetValue(100 + i, i);
var points = IndicatorExtensions.GetSmoothCurvePoints(indicator, converter, clientRect, series);
Assert.NotEmpty(points);
// Verify points logic: X should be 10 + halfBarWidth, Y should be value
// MockChart.BarsWidth defaults to something? Let's assume 0 or check logic.
// In GetSmoothCurvePoints: barX + halfBarWidth.
// Our mock GetChartX returns 10.
// 3. Test GetHistogramRectangles
var histSeries = new LineSeries("Hist", Color.Blue, 1, LineStyle.Solid);
for (int i = 0; i < 20; i++) histSeries.AddValue();
for (int i = 0; i < 20; i++)
{
double val = (i % 2 == 0) ? 10.0 : -10.0;
histSeries.SetValue(val, i);
}
var rects = IndicatorExtensions.GetHistogramRectangles(indicator, converter, clientRect, histSeries);
Assert.NotEmpty(rects);
// Check value at offset 9 (i=9 in setup loop)
// i=9 is odd -> -10.0 (Negative)
// Color should be Red (150, 255, 0, 0)
var first = rects.First();
Assert.Equal(Color.FromArgb(150, 255, 0, 0), first.Color);
// Verify geometry
// Value is -10. GetChartY(-10) -> -10.
// GetChartY(0) -> 0.
// Height = Abs(0 - (-10)) = 10.
// Y = 0 (since negative bars start at 0 and go down? No, GDI+ coords usually go down.
// But here we are testing the logic in GetHistogramRectangles:
// else { new Rectangle(barX, barY0, ...) } -> Y = barY0 = 0.
Assert.Equal(0, first.Rect.Y);
Assert.Equal(10, first.Rect.Height);
}
[Fact]
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
public void PaintMethods_DoNotThrow_WithValidGraphics()
{
// This test attempts to verify that paint methods don't crash.
// It requires System.Drawing.Common to be functional.
// On non-Windows, this might fail if libgdiplus is not installed.
// We'll try-catch the PlatformNotSupportedException to allow the test to pass (but not cover) on those systems.
try
{
using var bitmap = new Bitmap(100, 100);
using var graphics = Graphics.FromImage(bitmap);
RunPaintTests(graphics);
}
catch (TypeInitializationException) { return; } // System.Drawing.Common not supported
catch (PlatformNotSupportedException) { return; } // GDI+ not available
catch (DllNotFoundException) { return; } // libgdiplus not found
}
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
private void RunPaintTests(Graphics graphics)
{
var indicator = new TestIndicator();
indicator.CurrentChart = new MockChart();
// Add some data
var now = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 105);
}
// Setup converter
var validTime = now.AddMinutes(10);
indicator.CurrentChart.MainWindow.CoordinatesConverter = new TestCoordinatesConverter(validTime);
@@ -145,20 +216,49 @@ public class IndicatorExtensionsTests
// Test PaintHLine
IndicatorExtensions.PaintHLine(indicator, args, 100, pen);
// Test PaintSmoothCurve
var series = new LineSeries("Test", Color.Blue, 1, LineStyle.Solid);
for (int i = 0; i < 20; i++) series.AddValue(); // Fill with NaNs or values
for (int i = 0; i < 20; i++) series.SetValue(100 + i, i); // Set some values
IndicatorExtensions.PaintSmoothCurve(indicator, args, series, 0);
// Test PaintSmoothCurve with different LineStyles and Warmup
foreach (LineStyle style in Enum.GetValues(typeof(LineStyle)))
{
var series = new LineSeries("Test", Color.Blue, 1, style);
for (int i = 0; i < 20; i++) series.AddValue();
for (int i = 0; i < 20; i++) series.SetValue(100 + i, i);
// Test with warmup and cold values
IndicatorExtensions.PaintSmoothCurve(indicator, args, series, warmupPeriod: 5, showColdValues: true);
// Test without cold values
IndicatorExtensions.PaintSmoothCurve(indicator, args, series, warmupPeriod: 5, showColdValues: false);
}
// Test PaintHistogram
IndicatorExtensions.PaintHistogram(indicator, args, series, 0);
// Test PaintHistogram with Positive and Negative values
var histSeries = new LineSeries("Hist", Color.Blue, 1, LineStyle.Solid);
for (int i = 0; i < 20; i++) histSeries.AddValue();
for (int i = 0; i < 20; i++)
{
// Alternate positive and negative
double val = (i % 2 == 0) ? 10.0 : -10.0;
histSeries.SetValue(val, i);
}
IndicatorExtensions.PaintHistogram(indicator, args, histSeries, 0);
// Test DrawText
IndicatorExtensions.DrawText(indicator, args, "Test Text");
// Assert that we reached the end without throwing
// If we got here, no exception was thrown
}
[Fact]
public void GetInputValue_DefaultCase_ReturnsClose()
{
TestIndicator indicator = new();
DateTime now = new(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
UpdateArgs args = new(UpdateReason.NewBar);
// Cast to an invalid SourceType to trigger default case
SourceType invalidType = (SourceType)999;
var result = IndicatorExtensions.GetInputValue(indicator, args, invalidType);
Assert.Equal(105, result.Value); // Should default to Close (105)
}
}
+92 -41
View File
@@ -1,7 +1,10 @@
using TradingPlatform.BusinessLayer;
using TradingPlatform.BusinessLayer.Chart;
using System.Drawing;
using System.Drawing.Drawing2D;
#nullable disable
namespace QuanTAlib;
public enum SourceType
@@ -88,6 +91,11 @@ public static class IndicatorExtensions
}
#pragma warning disable CA1416 // Validate platform compatibility
public static int GetHLineY(IChartWindowCoordinatesConverter converter, double value)
{
return (int)converter.GetChartY(value);
}
public static void PaintHLine(this Indicator indicator, PaintChartEventArgs args, double value, Pen pen)
{
if (indicator.CurrentChart == null)
@@ -101,7 +109,7 @@ public static class IndicatorExtensions
gr.SetClip(clientRect);
int leftX = clientRect.Left;
int rightX = clientRect.Right;
int Y = (int)converter.GetChartY(value);
int Y = GetHLineY(converter, value);
using (pen)
{
@@ -109,6 +117,39 @@ public static class IndicatorExtensions
}
}
public static List<Point> GetSmoothCurvePoints(Indicator indicator, IChartWindowCoordinatesConverter converter, Rectangle clientRect, LineSeries series)
{
if (indicator == null) throw new ArgumentNullException(nameof(indicator));
if (converter == null) throw new ArgumentNullException(nameof(converter));
var data = indicator.HistoricalData;
if (data == null) return new List<Point>();
var lastTime = data.Time(data.Count - 1);
var firstTime = data.Time(0);
IChartWindowCoordinatesConverter safeConverter = converter!;
DateTime tLeft = safeConverter.GetTime(clientRect.Left);
DateTime leftTime = tLeft > lastTime ? tLeft : lastTime;
DateTime tRight = safeConverter.GetTime(clientRect.Right);
DateTime rightTime = tRight < firstTime ? tRight : firstTime;
int leftIndex = (int)data.GetIndexByTime(leftTime.Ticks) + 1;
int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks);
List<Point> allPoints = new List<Point>();
for (int i = rightIndex; i < leftIndex; i++)
{
int barX = (int)converter.GetChartX(data.Time(i));
int barY = (int)converter.GetChartY(series[i]);
int halfBarWidth = indicator.CurrentChart.BarsWidth / 2;
Point point = new Point(barX + halfBarWidth, barY);
allPoints.Add(point);
}
return allPoints;
}
public static void PaintSmoothCurve(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true, double tension = 0.2)
{
if (!series.Visible || indicator.CurrentChart == null)
@@ -121,26 +162,13 @@ public static class IndicatorExtensions
var clientRect = mainWindow.ClientRectangle;
gr.SetClip(clientRect);
DateTime leftTime = new[] { converter.GetTime(clientRect.Left), indicator.HistoricalData.Time(indicator!.Count - 1) }.Max();
DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min();
int leftIndex = (int)indicator.HistoricalData.GetIndexByTime(leftTime.Ticks) + 1;
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
List<Point> allPoints = new List<Point>();
for (int i = rightIndex; i < leftIndex; i++)
{
int barX = (int)converter.GetChartX(indicator.HistoricalData.Time(i));
int barY = (int)converter.GetChartY(series[i]);
int halfBarWidth = indicator.CurrentChart.BarsWidth / 2;
Point point = new Point(barX + halfBarWidth, barY);
allPoints.Add(point);
}
List<Point> allPoints = GetSmoothCurvePoints(indicator, converter, clientRect, series);
if (allPoints.Count > 1)
{
if (allPoints.Count < 2) return;
DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min();
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
using (Pen defaultPen = new(series.Color, series.Width) { DashStyle = ConvertLineStyleToDashStyle(series.Style) })
using (Pen coldPen = new(series.Color, series.Width) { DashStyle = DashStyle.Dot })
@@ -164,6 +192,47 @@ public static class IndicatorExtensions
}
}
public static List<(Rectangle Rect, Color Color)> GetHistogramRectangles(Indicator indicator, IChartWindowCoordinatesConverter converter, Rectangle clientRect, LineSeries series)
{
if (indicator == null) throw new ArgumentNullException(nameof(indicator));
if (converter == null) throw new ArgumentNullException(nameof(converter));
var data = indicator.HistoricalData;
if (data == null) return new List<(Rectangle, Color)>();
var lastTime = data.Time(data.Count - 1);
var firstTime = data.Time(0);
IChartWindowCoordinatesConverter safeConverter = converter!;
DateTime tLeft = safeConverter.GetTime(clientRect.Left);
DateTime leftTime = tLeft > lastTime ? tLeft : lastTime;
DateTime tRight = safeConverter.GetTime(clientRect.Right);
DateTime rightTime = tRight < firstTime ? tRight : firstTime;
int leftIndex = (int)data.GetIndexByTime(leftTime.Ticks) + 1;
int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks);
var result = new List<(Rectangle, Color)>();
for (int i = rightIndex; i < leftIndex; i++)
{
int barX = (int)converter.GetChartX(data.Time(i));
int barY = (int)converter.GetChartY(series[i]);
int barY0 = (int)converter.GetChartY(0);
int HistBarWidth = indicator.CurrentChart.BarsWidth - 2;
if (series[i] > 0)
{
result.Add((new Rectangle(barX, barY, HistBarWidth, Math.Abs(barY - barY0)), Color.FromArgb(150, 0, 255, 0)));
}
else
{
result.Add((new Rectangle(barX, barY0, HistBarWidth, Math.Abs(barY0 - barY)), Color.FromArgb(150, 255, 0, 0)));
}
}
return result;
}
public static void PaintHistogram(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true)
{
if (!series.Visible || indicator.CurrentChart == null)
@@ -176,32 +245,14 @@ public static class IndicatorExtensions
var clientRect = mainWindow.ClientRectangle;
gr.SetClip(clientRect);
DateTime leftTime = new[] { converter.GetTime(clientRect.Left), indicator.HistoricalData.Time(indicator!.Count - 1) }.Max();
DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min();
var rects = GetHistogramRectangles(indicator, converter, clientRect, series);
int leftIndex = (int)indicator.HistoricalData.GetIndexByTime(leftTime.Ticks) + 1;
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
for (int i = rightIndex; i < leftIndex; i++)
foreach (var (rect, color) in rects)
{
int barX = (int)converter.GetChartX(indicator.HistoricalData.Time(i));
int barY = (int)converter.GetChartY(series[i]);
int barY0 = (int)converter.GetChartY(0);
int HistBarWidth = indicator.CurrentChart.BarsWidth - 2;
if (series[i] > 0)
using (Brush hist = new SolidBrush(color))
{
using (Brush hist = new SolidBrush(Color.FromArgb(150, 0, 255, 0)))
{
gr.FillRectangle(hist, barX, barY, HistBarWidth, Math.Abs(barY - barY0));
}
}
else
{
using (Brush hist = new SolidBrush(Color.FromArgb(150, 255, 0, 0)))
{
gr.FillRectangle(hist, barX, barY0, HistBarWidth, Math.Abs(barY0 - barY));
}
gr.FillRectangle(hist, rect);
}
}
}
+58 -47
View File
@@ -3,9 +3,11 @@
using System.Drawing;
namespace TradingPlatform.BusinessLayer;
namespace TradingPlatform.BusinessLayer
{
using TradingPlatform.BusinessLayer.Chart;
#region Enums
#region Enums
/// <summary>
/// Specifies the style of indicator line.
@@ -185,7 +187,7 @@ public class HistoricalData
for (int i = 0; i < _items.Count; i++)
{
if (_items[i].TicksLeft == ticks)
return i;
return Count - 1 - i;
}
return -1;
}
@@ -354,60 +356,68 @@ public class PaintChartEventArgs : EventArgs
#endregion
#region Chart
/// <summary>
/// Chart interface
/// </summary>
public interface IChart
{
ChartWindow MainWindow { get; }
ChartWindow[] Windows { get; }
int BarsWidth { get; }
#region Chart
}
/// <summary>
/// Chart window
/// </summary>
public class ChartWindow
namespace TradingPlatform.BusinessLayer.Chart
{
public Rectangle ClientRectangle { get; set; }
public ICoordinatesConverter CoordinatesConverter { get; set; } = new MockCoordinatesConverter();
/// <summary>
/// Coordinates converter interface
/// </summary>
public interface IChartWindowCoordinatesConverter
{
DateTime GetTime(int x);
double GetChartX(DateTime time);
double GetChartY(double value);
}
}
/// <summary>
/// Coordinates converter interface
/// </summary>
public interface ICoordinatesConverter
namespace TradingPlatform.BusinessLayer
{
DateTime GetTime(int x);
double GetChartX(DateTime time);
double GetChartY(double value);
}
using TradingPlatform.BusinessLayer.Chart;
/// <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>
/// Chart interface
/// </summary>
public interface IChart
{
ChartWindow MainWindow { get; }
ChartWindow[] Windows { get; }
int BarsWidth { get; }
}
/// <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;
}
/// <summary>
/// Chart window
/// </summary>
public class ChartWindow
{
public Rectangle ClientRectangle { get; set; }
public IChartWindowCoordinatesConverter CoordinatesConverter { get; set; } = new MockCoordinatesConverter();
}
#endregion
/// <summary>
/// Mock coordinates converter
/// </summary>
public class MockCoordinatesConverter : IChartWindowCoordinatesConverter
{
public DateTime GetTime(int x) => DateTime.UtcNow;
public double GetChartX(DateTime time) => 0;
public double GetChartY(double value) => 0;
}
#region Indicator Base
/// <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
@@ -488,3 +498,4 @@ public abstract class Indicator
}
#endregion
}