using Xunit; using TradingPlatform.BusinessLayer; using TradingPlatform.BusinessLayer.Chart; using System.Drawing; using System.Reflection; namespace QuanTAlib.Tests; public class IndicatorExtensionsTests { private sealed class TestIndicator : Indicator { public TestIndicator() { Name = "Test Indicator"; } } 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) => 10; // Return a fixed X for testing public double GetChartY(double value) => value; // Return value as Y for testing } [Fact] public void DataSourceInputAttribute_HasCorrectDefaults() { IndicatorExtensions.DataSourceInputAttribute attr = new(); Assert.Equal("Data source", attr.Name); Assert.Equal(20, attr.SortIndex); Assert.NotNull(attr.Variants); Assert.NotEmpty(attr.Variants); } [Fact] public void GetInputValue_ReturnsCorrectValues_ForSourceTypes() { TestIndicator indicator = new(); DateTime now = new(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc); // Open=100, High=110, Low=90, Close=105, Volume=1000 const double open = 100; const double high = 110; const double low = 90; const double close = 105; const double volume = 1000; indicator.HistoricalData.AddBar(now, open, high, low, close, volume); // Ensure Count is updated (mock implementation detail) // The mock HistoricalData.Count reflects added items. // Indicator.Count => HistoricalData.Count. UpdateArgs args = new(UpdateReason.NewBar); // Test each SourceType Assert.Equal(open, IndicatorExtensions.GetInputValue(indicator, args, SourceType.Open).Value); Assert.Equal(high, IndicatorExtensions.GetInputValue(indicator, args, SourceType.High).Value); Assert.Equal(low, IndicatorExtensions.GetInputValue(indicator, args, SourceType.Low).Value); Assert.Equal(close, IndicatorExtensions.GetInputValue(indicator, args, SourceType.Close).Value); // HL2 = (110 + 90) / 2 = 100 Assert.Equal(100, IndicatorExtensions.GetInputValue(indicator, args, SourceType.HL2).Value); // OC2 = (100 + 105) / 2 = 102.5 Assert.Equal(102.5, IndicatorExtensions.GetInputValue(indicator, args, SourceType.OC2).Value); // OHL3 = (100 + 110 + 90) / 3 = 100 Assert.Equal(100, IndicatorExtensions.GetInputValue(indicator, args, SourceType.OHL3).Value); // HLC3 = (110 + 90 + 105) / 3 = 101.666... Assert.Equal(101.66666666666667, IndicatorExtensions.GetInputValue(indicator, args, SourceType.HLC3).Value, 5); // OHLC4 = (100 + 110 + 90 + 105) / 4 = 101.25 Assert.Equal(101.25, IndicatorExtensions.GetInputValue(indicator, args, SourceType.OHLC4).Value); // HLCC4 = (110 + 90 + 105 + 105) / 4 = 102.5 Assert.Equal(102.5, IndicatorExtensions.GetInputValue(indicator, args, SourceType.HLCC4).Value); } [Fact] public void GetInputBar_ReturnsCorrectBar() { TestIndicator indicator = new(); DateTime now = new(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc); const double open = 100; const double high = 110; const double low = 90; const double close = 105; const double volume = 1000; indicator.HistoricalData.AddBar(now, open, high, low, close, volume); UpdateArgs args = new(UpdateReason.NewBar); var bar = IndicatorExtensions.GetInputBar(indicator, args); Assert.Equal(now, bar.AsDateTime); Assert.Equal(open, bar.Open); Assert.Equal(high, bar.High); Assert.Equal(low, bar.Low); Assert.Equal(close, bar.Close); Assert.Equal(volume, bar.Volume); } [Fact] public void LogicMethods_CalculateCorrectly() { 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); 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); var args = new PaintChartEventArgs(graphics, new Rectangle(0, 0, 100, 100)); using var pen = new Pen(Color.Red); // Test PaintHLine IndicatorExtensions.PaintHLine(indicator, args, 100, pen); // 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 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"); } [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) } }