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
-87
View File
@@ -1,87 +0,0 @@
using System;
using Xunit;
namespace QuanTAlib.Tests;
public class DemaCoverageTests
{
[Fact]
public void Dema_CompensationLogic_IsTriggered()
{
// Compensation happens when E <= 1e-10
// E starts at 1.0 and decays by (1-alpha) each step.
// We need enough steps to reach 1e-10.
// If period=10, alpha ~ 0.18, decay ~ 0.81
// 0.81^n <= 1e-10 => n * log(0.81) <= -10
// n * -0.09 <= -10 => n >= 111
int count = 200;
int period = 10;
var dema = new Dema(period);
for (int i = 0; i < count; i++)
{
dema.Update(new TValue(DateTime.UtcNow, 100.0));
}
// Just ensuring no exception and value is correct
Assert.Equal(100.0, dema.Last.Value, 1e-9);
}
[Fact]
public void Dema_IsHot_Logic()
{
// IsHot happens when E <= 0.05
// 0.81^n <= 0.05 => n >= 14
int period = 10;
var dema = new Dema(period);
Assert.False(dema.IsHot);
for (int i = 0; i < 50; i++)
{
dema.Update(new TValue(DateTime.UtcNow, 100.0));
if (i > 30) // Should be hot by now
{
Assert.True(dema.IsHot);
}
}
}
[Fact]
public void Dema_StaticCalculate_Alpha_Coverage()
{
double[] source = new double[100];
double[] output = new double[100];
for(int i=0; i<100; i++) source[i] = 100.0;
// Use alpha overload
Dema.Calculate(source.AsSpan(), output.AsSpan(), 0.1);
Assert.Equal(100.0, output[^1], 1e-9);
}
[Fact]
public void Dema_Reset_ClearsState()
{
var dema = new Dema(10);
dema.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.NotEqual(0, dema.Last.Value);
dema.Reset();
Assert.Equal(0, dema.Last.Value);
Assert.False(dema.IsHot);
}
[Fact]
public void Dema_Constructor_Alpha_Validation()
{
Assert.Throws<ArgumentException>(() => new Dema(0.0));
Assert.Throws<ArgumentException>(() => new Dema(1.1));
var dema = new Dema(0.5);
Assert.NotNull(dema);
}
}
-77
View File
@@ -1,77 +0,0 @@
using System;
using Xunit;
namespace QuanTAlib.Tests;
public class HmaCoverageTests
{
[Fact]
public void Hma_CalculateIntermediate_Simd_Coverage()
{
// CalculateIntermediate uses SIMD if length >= Vector256<double>.Count (4)
int count = 100;
int period = 10;
double[] source = new double[count];
double[] output = new double[count];
for(int i=0; i<count; i++) source[i] = 100.0;
Hma.Calculate(source.AsSpan(), output.AsSpan(), period);
Assert.Equal(100.0, output[^1], 1e-9);
}
[Fact]
public void Hma_CalculateIntermediate_Scalar_Coverage()
{
// Force scalar path by using small length
int count = 3;
int period = 2; // Min period is 2
double[] source = new double[count];
double[] output = new double[count];
for(int i=0; i<count; i++) source[i] = 100.0;
Hma.Calculate(source.AsSpan(), output.AsSpan(), period);
Assert.Equal(100.0, output[^1], 1e-9);
}
[Fact]
public void Hma_Reset_ClearsState()
{
var hma = new Hma(10);
hma.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.NotEqual(0, hma.Last.Value);
hma.Reset();
Assert.Equal(0, hma.Last.Value);
Assert.False(hma.IsHot);
}
[Fact]
public void Hma_UpdateSeries_RestoresState()
{
var hma = new Hma(10);
var series = new TSeries();
for(int i=0; i<20; i++) series.Add(DateTime.UtcNow.AddMinutes(i), 100.0);
hma.Update(series);
// After batch update, the instance state should be consistent with the end of the series
// So next update should continue correctly
var nextVal = hma.Update(new TValue(DateTime.UtcNow.AddMinutes(20), 100.0));
Assert.Equal(100.0, nextVal.Value, 1e-9);
}
[Fact]
public void Hma_Constructor_Validation()
{
Assert.Throws<ArgumentException>(() => new Hma(1));
Assert.Throws<ArgumentException>(() => new Hma(0));
var hma = new Hma(2);
Assert.NotNull(hma);
}
}
-229
View File
@@ -1,229 +0,0 @@
using System;
using Xunit;
namespace QuanTAlib.Tests;
public class SmaCoverageTests
{
[Fact]
public void Sma_ResyncLogic_IsTriggeredAndCorrect()
{
// ResyncInterval is 1000.
int count = 2500;
int period = 10;
var sma = new Sma(period);
double constantValue = 100.0;
for (int i = 0; i < count; i++)
{
sma.Update(new TValue(DateTime.UtcNow, constantValue));
if (i >= period)
{
Assert.Equal(constantValue, sma.Last.Value, 1e-9);
}
}
}
[Fact]
public void Sma_SpanCalc_LargeDataset_TriggersResync()
{
int count = 5000;
int period = 10;
double[] source = new double[count];
double[] output = new double[count];
for (int i = 0; i < count; i++)
{
source[i] = 100.0;
}
Sma.Calculate(source.AsSpan(), output.AsSpan(), period);
for (int i = period; i < count; i++)
{
Assert.Equal(100.0, output[i], 1e-9);
}
}
[Fact]
public void Sma_SpanCalc_SimdThreshold_Boundary()
{
// SimdThreshold is 256.
int[] lengths = { 250, 256, 260 };
int period = 10;
foreach (int len in lengths)
{
double[] source = new double[len];
double[] output = new double[len];
for (int i = 0; i < len; i++) source[i] = 100.0;
Sma.Calculate(source.AsSpan(), output.AsSpan(), period);
Assert.Equal(100.0, output[^1], 1e-9);
}
}
[Fact]
public void Sma_SpanCalc_Simd_WithResync()
{
int count = 3000;
int period = 5;
double[] source = new double[count];
double[] output = new double[count];
// Linear increase: 0, 1, 2, ...
for (int i = 0; i < count; i++) source[i] = i;
Sma.Calculate(source.AsSpan(), output.AsSpan(), period);
// SMA(5) of x-4, x-3, x-2, x-1, x
// = (5x - 10) / 5 = x - 2
for (int i = period; i < count; i++)
{
double expected = i - 2.0;
Assert.Equal(expected, output[i], 1e-9);
}
}
[Fact]
public void Sma_Constructor_ThrowsOnInvalidPeriod()
{
Assert.Throws<ArgumentException>(() => new Sma(0));
Assert.Throws<ArgumentException>(() => new Sma(-1));
}
[Fact]
public void Sma_StaticCalculate_ThrowsOnInvalidArgs()
{
double[] source = new double[10];
double[] output = new double[5]; // Mismatch
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), output.AsSpan(), 5));
double[] output2 = new double[10];
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), output2.AsSpan(), 0));
}
[Fact]
public void Sma_Calculate_EmptyInput_DoesNothing()
{
Sma.Calculate(ReadOnlySpan<double>.Empty, Span<double>.Empty, 5);
// Should not throw
}
[Fact]
public void Sma_Update_WithNaN_UsesLastValid()
{
var sma = new Sma(5);
sma.Update(new TValue(DateTime.UtcNow, 1.0));
sma.Update(new TValue(DateTime.UtcNow, 2.0));
sma.Update(new TValue(DateTime.UtcNow, double.NaN)); // Should use 2.0
// Buffer: 1, 2, 2
// SMA(3) = (1 + 2 + 2) / 3 = 5/3 = 1.666...
Assert.Equal(5.0/3.0, sma.Last.Value, 1e-9);
}
[Fact]
public void Sma_Update_IsNewFalse_UpdatesLastValue()
{
var sma = new Sma(3);
sma.Update(new TValue(DateTime.UtcNow, 1.0));
sma.Update(new TValue(DateTime.UtcNow, 2.0));
// Update existing with 3.0 (replaces 2.0)
sma.Update(new TValue(DateTime.UtcNow, 3.0), isNew: false);
// Buffer should be: 1, 3
// SMA = (1 + 3) / 2 = 2
Assert.Equal(2.0, sma.Last.Value, 1e-9);
}
[Fact]
public void Sma_TSeries_Empty_ReturnsEmpty()
{
var sma = new Sma(5);
var result = sma.Update(new TSeries());
Assert.Empty(result);
}
[Fact]
public void Sma_TSeries_WithNaN_RestoresStateCorrectly()
{
var sma = new Sma(3);
var series = new TSeries();
series.Add(new TValue(DateTime.UtcNow, 1.0));
series.Add(new TValue(DateTime.UtcNow, 2.0));
series.Add(new TValue(DateTime.UtcNow, double.NaN));
series.Add(new TValue(DateTime.UtcNow, 4.0));
sma.Update(series);
// Buffer: 2.0, 2.0 (from NaN), 4.0
// SMA(3) = (2 + 2 + 4) / 3 = 8/3 = 2.666...
// Let's add one more value to verify state is correct
sma.Update(new TValue(DateTime.UtcNow, 5.0));
// Buffer: 2.0, 4.0, 5.0
// SMA(3) = (2 + 4 + 5) / 3 = 11/3 = 3.666...
Assert.Equal(11.0/3.0, sma.Last.Value, 1e-9);
}
[Fact]
public void Sma_Reset_ClearsState()
{
var sma = new Sma(3);
sma.Update(new TValue(DateTime.UtcNow, 1.0));
sma.Update(new TValue(DateTime.UtcNow, 2.0));
sma.Update(new TValue(DateTime.UtcNow, 3.0));
sma.Reset();
Assert.Equal(0, sma.Last.Value);
// Start fresh
sma.Update(new TValue(DateTime.UtcNow, 10.0));
// Buffer: 10
// SMA = 10
Assert.Equal(10.0, sma.Last.Value);
}
[Fact]
public void Sma_Calculate_ScalarFallback_WithNaN()
{
// Force scalar path by including NaN, even with large dataset
int count = 1000;
double[] source = new double[count];
double[] output = new double[count];
for (int i = 0; i < count; i++) source[i] = 1.0;
source[500] = double.NaN; // This should trigger HasNonFiniteValues -> true
Sma.Calculate(source.AsSpan(), output.AsSpan(), 10);
// Check around the NaN
// Index 500 is NaN, so it uses previous valid (1.0)
// So effectively the stream is all 1.0s
Assert.Equal(1.0, output[500], 1e-9);
Assert.Equal(1.0, output[501], 1e-9);
}
[Fact]
public void Sma_Constructor_WithSource_Subscribes()
{
var source = new Sma(10); // Just using Sma as a publisher
var sma = new Sma(source, 5);
source.Update(new TValue(DateTime.UtcNow, 10.0));
Assert.Equal(10.0, sma.Last.Value);
}
}
-37
View File
@@ -1,37 +0,0 @@
using System;
using System.Reflection;
using Tulip;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public class DebugTulipTests
{
private readonly ITestOutputHelper _output;
public DebugTulipTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void ListTulipIndicators()
{
var type = typeof(Tulip.Indicators);
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Static);
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
_output.WriteLine("Tulip Indicators (Properties):");
foreach (var p in properties)
{
_output.WriteLine(p.Name);
}
_output.WriteLine("Tulip Indicators (Fields):");
foreach (var f in fields)
{
_output.WriteLine(f.Name);
}
}
}
+41 -25
View File
@@ -34,8 +34,22 @@ public sealed class T3 : ITValuePublisher
public static State New() => new() { IsInitialized = false }; public static State New() => new() { IsInitialized = false };
} }
private readonly double _alpha; private readonly struct Parameters
private readonly double _c1, _c2, _c3, _c4; {
public readonly double Alpha;
public readonly double C1, C2, C3, C4;
public Parameters(double alpha, double c1, double c2, double c3, double c4)
{
Alpha = alpha;
C1 = c1;
C2 = c2;
C3 = c3;
C4 = c4;
}
}
private readonly Parameters _params;
private State _state = State.New(); private State _state = State.New();
private State _p_state = State.New(); private State _p_state = State.New();
private double _lastValidValue; private double _lastValidValue;
@@ -57,17 +71,19 @@ public sealed class T3 : ITValuePublisher
if (period <= 0) if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period)); throw new ArgumentException("Period must be greater than 0", nameof(period));
_alpha = 2.0 / (period + 1); double alpha = 2.0 / (period + 1);
// Precompute coefficients // Precompute coefficients
double v = vfactor; double v = vfactor;
double v2 = v * v; double v2 = v * v;
double v3 = v2 * v; double v3 = v2 * v;
_c1 = -v3; double c1 = -v3;
_c2 = 3.0 * (v2 + v3); double c2 = 3.0 * (v2 + v3);
_c3 = -3.0 * (2.0 * v2 + v + v3); double c3 = -3.0 * (2.0 * v2 + v + v3);
_c4 = 1.0 + 3.0 * v + 3.0 * v2 + v3; double c4 = 1.0 + 3.0 * v + 3.0 * v2 + v3;
_params = new Parameters(alpha, c1, c2, c3, c4);
Name = $"T3({period}, {vfactor:F2})"; Name = $"T3({period}, {vfactor:F2})";
} }
@@ -118,7 +134,7 @@ public sealed class T3 : ITValuePublisher
} }
double val = GetValidValue(input.Value); double val = GetValidValue(input.Value);
val = Compute(val, _alpha, _c1, _c2, _c3, _c4, ref _state); val = Compute(val, _params, ref _state);
Last = new TValue(input.Time, val); Last = new TValue(input.Time, val);
Pub?.Invoke(Last); Pub?.Invoke(Last);
return Last; return Last;
@@ -142,21 +158,21 @@ public sealed class T3 : ITValuePublisher
State state = _state; State state = _state;
double lastValidValue = _lastValidValue; double lastValidValue = _lastValidValue;
CalculateCore(sourceValues, vSpan, _alpha, _c1, _c2, _c3, _c4, ref state, ref lastValidValue); CalculateCore(sourceValues, vSpan, _params, ref state, ref lastValidValue);
_state = state; _state = state;
_lastValidValue = lastValidValue; _lastValidValue = lastValidValue;
sourceTimes.CopyTo(tSpan); sourceTimes.CopyTo(tSpan);
_p_state = _state; _p_state = _state;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]); Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v); return new TSeries(t, v);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double Compute(double input, double alpha, double c1, double c2, double c3, double c4, ref State state) private static double Compute(double input, in Parameters p, ref State state)
{ {
if (!state.IsInitialized) if (!state.IsInitialized)
{ {
@@ -165,20 +181,19 @@ public sealed class T3 : ITValuePublisher
} }
else else
{ {
state.E1 += alpha * (input - state.E1); state.E1 += p.Alpha * (input - state.E1);
state.E2 += alpha * (state.E1 - state.E2); state.E2 += p.Alpha * (state.E1 - state.E2);
state.E3 += alpha * (state.E2 - state.E3); state.E3 += p.Alpha * (state.E2 - state.E3);
state.E4 += alpha * (state.E3 - state.E4); state.E4 += p.Alpha * (state.E3 - state.E4);
state.E5 += alpha * (state.E4 - state.E5); state.E5 += p.Alpha * (state.E4 - state.E5);
state.E6 += alpha * (state.E5 - state.E6); state.E6 += p.Alpha * (state.E5 - state.E6);
} }
return c1 * state.E6 + c2 * state.E5 + c3 * state.E4 + c4 * state.E3; return p.C1 * state.E6 + p.C2 * state.E5 + p.C3 * state.E4 + p.C4 * state.E3;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateCore(ReadOnlySpan<double> source, Span<double> output, double alpha, private static void CalculateCore(ReadOnlySpan<double> source, Span<double> output, in Parameters p, ref State state, ref double lastValidValue)
double c1, double c2, double c3, double c4, ref State state, ref double lastValidValue)
{ {
int len = source.Length; int len = source.Length;
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
@@ -189,7 +204,7 @@ public sealed class T3 : ITValuePublisher
else else
val = lastValidValue; val = lastValidValue;
output[i] = Compute(val, alpha, c1, c2, c3, c4, ref state); output[i] = Compute(val, p, ref state);
} }
} }
@@ -212,7 +227,7 @@ public sealed class T3 : ITValuePublisher
throw new ArgumentException("Period must be greater than 0", nameof(period)); throw new ArgumentException("Period must be greater than 0", nameof(period));
if (source.Length != output.Length) if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length"); throw new ArgumentException("Source and output must have the same length");
double alpha = 2.0 / (period + 1); double alpha = 2.0 / (period + 1);
double v = vfactor; double v = vfactor;
double v2 = v * v; double v2 = v * v;
@@ -223,10 +238,11 @@ public sealed class T3 : ITValuePublisher
double c3 = -3.0 * (2.0 * v2 + v + v3); double c3 = -3.0 * (2.0 * v2 + v + v3);
double c4 = 1.0 + 3.0 * v + 3.0 * v2 + v3; double c4 = 1.0 + 3.0 * v + 3.0 * v2 + v3;
var p = new Parameters(alpha, c1, c2, c3, c4);
State state = State.New(); State state = State.New();
double lastValidValue = 0; double lastValidValue = 0;
CalculateCore(source, output, alpha, c1, c2, c3, c4, ref state, ref lastValidValue); CalculateCore(source, output, p, ref state, ref lastValidValue);
} }
/// <summary> /// <summary>
+8
View File
@@ -1,22 +1,27 @@
# T3: Tillson T3 Moving Average # T3: Tillson T3 Moving Average
## Overview and Purpose ## Overview and Purpose
The Tillson T3 Moving Average is an advanced technical indicator designed to provide superior smoothing with minimal lag. Developed by Tim Tillson and introduced in the January 1998 issue of Technical Analysis of Stocks & Commodities magazine, T3 implements a sophisticated six-stage EMA architecture with optimized coefficient distribution based on a volume factor parameter. The Tillson T3 Moving Average is an advanced technical indicator designed to provide superior smoothing with minimal lag. Developed by Tim Tillson and introduced in the January 1998 issue of Technical Analysis of Stocks & Commodities magazine, T3 implements a sophisticated six-stage EMA architecture with optimized coefficient distribution based on a volume factor parameter.
Unlike simpler moving averages or even triple-EMA approaches, T3 uses a unique mathematical framework that strategically combines multiple EMAs with precisely calculated coefficients. This approach creates a moving average that effectively reduces noise while preserving important trend information and minimizing lag. Unlike simpler moving averages or even triple-EMA approaches, T3 uses a unique mathematical framework that strategically combines multiple EMAs with precisely calculated coefficients. This approach creates a moving average that effectively reduces noise while preserving important trend information and minimizing lag.
## Core Concepts ## Core Concepts
* **Multi-stage smoothing:** Uses a six-stage EMA cascade with optimized coefficient distribution to achieve superior noise reduction while minimizing lag * **Multi-stage smoothing:** Uses a six-stage EMA cascade with optimized coefficient distribution to achieve superior noise reduction while minimizing lag
* **Volume factor customization:** Provides a parameter that allows traders to fine-tune the balance between smoothness and responsiveness * **Volume factor customization:** Provides a parameter that allows traders to fine-tune the balance between smoothness and responsiveness
* **Strategic coefficient weighting:** Employs a sophisticated formula that prevents overshooting at turning points while maintaining responsiveness * **Strategic coefficient weighting:** Employs a sophisticated formula that prevents overshooting at turning points while maintaining responsiveness
## Calculation and Mathematical Foundation ## Calculation and Mathematical Foundation
T3 works by running price data through a series of six EMAs, then combining the outputs of these EMAs using carefully calculated weights. These weights are determined by a "volume factor" parameter ($v$) that controls how much the indicator prioritizes smoothness versus responsiveness. T3 works by running price data through a series of six EMAs, then combining the outputs of these EMAs using carefully calculated weights. These weights are determined by a "volume factor" parameter ($v$) that controls how much the indicator prioritizes smoothness versus responsiveness.
### Formula ### Formula
$$ T3 = c_1 \cdot EMA_6 + c_2 \cdot EMA_5 + c_3 \cdot EMA_4 + c_4 \cdot EMA_3 $$ $$ T3 = c_1 \cdot EMA_6 + c_2 \cdot EMA_5 + c_3 \cdot EMA_4 + c_4 \cdot EMA_3 $$
Where: Where:
* $EMA_1$ through $EMA_6$ are exponential moving averages applied in sequence: * $EMA_1$ through $EMA_6$ are exponential moving averages applied in sequence:
* $EMA_1(x) = EMA(x)$ * $EMA_1(x) = EMA(x)$
* $EMA_n(x) = EMA(EMA_{n-1}(x))$ * $EMA_n(x) = EMA(EMA_{n-1}(x))$
@@ -28,6 +33,7 @@ Where:
* Default volume factor $v = 0.7$ * Default volume factor $v = 0.7$
## Parameters ## Parameters
| Parameter | Default | Range | Description | | Parameter | Default | Range | Description |
|-----------|---------|-------|-------------| |-----------|---------|-------|-------------|
| Period | 10 | > 0 | The smoothing period for the internal EMAs | | Period | 10 | > 0 | The smoothing period for the internal EMAs |
@@ -36,6 +42,7 @@ Where:
## C# Usage ## C# Usage
### Standard TSeries Usage ### Standard TSeries Usage
```csharp ```csharp
// Calculate T3 with period 10 and default volume factor 0.7 // Calculate T3 with period 10 and default volume factor 0.7
var t3 = T3.Calculate(sourceSeries, 10); var t3 = T3.Calculate(sourceSeries, 10);
@@ -47,6 +54,7 @@ Console.WriteLine($"T3 Value: {t3.Last.Value}");
``` ```
### Eventing and Reactive Support ### Eventing and Reactive Support
The `T3` class implements `ITValuePublisher`, allowing for event-driven updates. The `T3` class implements `ITValuePublisher`, allowing for event-driven updates.
```csharp ```csharp
-276
View File
@@ -1,276 +0,0 @@
using System;
using Xunit;
namespace QuanTAlib.Tests;
public class WmaCoverageTests
{
[Fact]
public void Wma_ResyncLogic_IsTriggeredAndCorrect()
{
// ResyncInterval is 1000. We need more than that to trigger it.
int count = 2500;
int period = 10;
var wma = new Wma(period);
// Use a constant value to make verification easy
// WMA of constant X is X
double constantValue = 100.0;
for (int i = 0; i < count; i++)
{
wma.Update(new TValue(DateTime.UtcNow, constantValue));
// After warmup, value should always be constantValue
if (i >= period)
{
Assert.Equal(constantValue, wma.Last.Value, 1e-9);
}
}
}
[Fact]
public void Wma_SpanCalc_LargeDataset_TriggersResync()
{
// ResyncInterval is 1000.
int count = 5000;
int period = 10;
double[] source = new double[count];
double[] output = new double[count];
// Fill with constant value
for (int i = 0; i < count; i++)
{
source[i] = 100.0;
}
Wma.Calculate(source.AsSpan(), output.AsSpan(), period);
// Verify all outputs after warmup are correct
for (int i = period; i < count; i++)
{
Assert.Equal(100.0, output[i], 1e-9);
}
}
[Fact]
public void Wma_SpanCalc_SimdThreshold_Boundary()
{
// SimdThreshold is 256.
// Test just below and just above to ensure both paths work
int[] lengths = { 250, 256, 260 };
int period = 10;
foreach (int len in lengths)
{
double[] source = new double[len];
double[] output = new double[len];
for (int i = 0; i < len; i++) source[i] = 100.0;
Wma.Calculate(source.AsSpan(), output.AsSpan(), period);
Assert.Equal(100.0, output[^1], 1e-9);
}
}
[Fact]
public void Wma_SpanCalc_Simd_WithResync()
{
// This targets the SIMD loop with resync
// Need length > SimdThreshold (256) and enough data to hit ResyncInterval (1000)
// But wait, the SIMD loop in CalculateSimdCore handles resync internally.
// The loop structure is:
// while (idx < simdEnd)
// nextSync = Math.Min(simdEnd, idx + ResyncInterval)
// ... process blocks ...
int count = 3000;
int period = 5;
double[] source = new double[count];
double[] output = new double[count];
// Use a pattern that isn't constant to verify calculation accuracy
// Linear increase: 0, 1, 2, ...
for (int i = 0; i < count; i++) source[i] = i;
Wma.Calculate(source.AsSpan(), output.AsSpan(), period);
// Verify a few points
// WMA(5) of x-4, x-3, x-2, x-1, x
// = (1*(x-4) + 2*(x-3) + 3*(x-2) + 4*(x-1) + 5*x) / 15
// = (x-4 + 2x-6 + 3x-6 + 4x-4 + 5x) / 15
// = (15x - 20) / 15
// = x - 20/15 = x - 1.333...
for (int i = period; i < count; i++)
{
double expected = i - (20.0 / 15.0);
Assert.Equal(expected, output[i], 1e-9);
}
}
[Fact]
public void Wma_Update_Resync_WithFloatingPointDrift()
{
// This test tries to accumulate error and see if resync fixes it (or at least doesn't break it)
// It's hard to deterministically cause drift, but we can ensure the code path is executed.
int period = 10;
var wma = new Wma(period);
// 1200 updates to trigger resync (at 1000)
for (int i = 0; i < 1200; i++)
{
wma.Update(new TValue(DateTime.UtcNow, 1.0));
}
Assert.Equal(1.0, wma.Last.Value, 1e-9);
}
[Fact]
public void Wma_Constructor_ThrowsOnInvalidPeriod()
{
Assert.Throws<ArgumentException>(() => new Wma(0));
Assert.Throws<ArgumentException>(() => new Wma(-1));
}
[Fact]
public void Wma_StaticCalculate_ThrowsOnInvalidArgs()
{
double[] source = new double[10];
double[] output = new double[5]; // Mismatch
Assert.Throws<ArgumentException>(() => Wma.Calculate(source.AsSpan(), output.AsSpan(), 5));
double[] output2 = new double[10];
Assert.Throws<ArgumentException>(() => Wma.Calculate(source.AsSpan(), output2.AsSpan(), 0));
}
[Fact]
public void Wma_Calculate_EmptyInput_DoesNothing()
{
Wma.Calculate(ReadOnlySpan<double>.Empty, Span<double>.Empty, 5);
// Should not throw
}
[Fact]
public void Wma_Update_WithNaN_UsesLastValid()
{
var wma = new Wma(5);
wma.Update(new TValue(DateTime.UtcNow, 1.0));
wma.Update(new TValue(DateTime.UtcNow, 2.0));
wma.Update(new TValue(DateTime.UtcNow, double.NaN)); // Should use 2.0
// Buffer: 1, 2, 2
// WMA(3) = (1*1 + 2*2 + 3*2) / 6 = (1 + 4 + 6) / 6 = 11/6 = 1.8333...
// Wait, period is 5.
// Buffer: 1, 2, 2
// Sum = 5, WSum = 1*1 + 2*2 + 3*2 = 11
// Divisor = 3*4/2 = 6
// Result = 11/6
Assert.Equal(11.0/6.0, wma.Last.Value, 1e-9);
}
[Fact]
public void Wma_Update_IsNewFalse_UpdatesLastValue()
{
var wma = new Wma(3);
wma.Update(new TValue(DateTime.UtcNow, 1.0));
wma.Update(new TValue(DateTime.UtcNow, 2.0));
// Update existing with 3.0 (replaces 2.0)
wma.Update(new TValue(DateTime.UtcNow, 3.0), isNew: false);
// Buffer should be: 1, 3
// Sum = 4, WSum = 1*1 + 2*3 = 7
// Divisor = 2*3/2 = 3
// Result = 7/3 = 2.333...
Assert.Equal(7.0/3.0, wma.Last.Value, 1e-9);
}
[Fact]
public void Wma_TSeries_Empty_ReturnsEmpty()
{
var wma = new Wma(5);
var result = wma.Update(new TSeries());
Assert.Empty(result);
}
[Fact]
public void Wma_TSeries_WithNaN_RestoresStateCorrectly()
{
// This tests the state restoration logic in Update(TSeries)
// specifically the loop that looks for _lastValidValue
var wma = new Wma(3);
var series = new TSeries();
series.Add(new TValue(DateTime.UtcNow, 1.0));
series.Add(new TValue(DateTime.UtcNow, 2.0));
series.Add(new TValue(DateTime.UtcNow, double.NaN));
series.Add(new TValue(DateTime.UtcNow, 4.0));
wma.Update(series);
// After processing series, internal state should match having processed these sequentially
// Last value was 4.0. Previous valid was 2.0 (since NaN used 2.0).
// Buffer: 2.0, 2.0 (from NaN), 4.0
// Let's add one more value to verify state is correct
wma.Update(new TValue(DateTime.UtcNow, 5.0));
// Buffer: 2.0, 4.0, 5.0
// WMA(3) = (1*2 + 2*4 + 3*5) / 6 = (2 + 8 + 15) / 6 = 25/6 = 4.1666...
Assert.Equal(25.0/6.0, wma.Last.Value, 1e-9);
}
[Fact]
public void Wma_Reset_ClearsState()
{
var wma = new Wma(3);
wma.Update(new TValue(DateTime.UtcNow, 1.0));
wma.Update(new TValue(DateTime.UtcNow, 2.0));
wma.Update(new TValue(DateTime.UtcNow, 3.0));
wma.Reset();
Assert.Equal(0, wma.Last.Value);
// Start fresh
wma.Update(new TValue(DateTime.UtcNow, 10.0));
// Buffer: 10
// WMA = 10
Assert.Equal(10.0, wma.Last.Value);
}
[Fact]
public void Wma_Calculate_ScalarFallback_WithNaN()
{
// Force scalar path by including NaN, even with large dataset
int count = 1000;
double[] source = new double[count];
double[] output = new double[count];
for (int i = 0; i < count; i++) source[i] = 1.0;
source[500] = double.NaN; // This should trigger HasNonFiniteValues -> true
Wma.Calculate(source.AsSpan(), output.AsSpan(), 10);
// Check around the NaN
// Index 500 is NaN, so it uses previous valid (1.0)
// So effectively the stream is all 1.0s
Assert.Equal(1.0, output[500], 1e-9);
Assert.Equal(1.0, output[501], 1e-9);
}
[Fact]
public void Wma_Constructor_WithSource_Subscribes()
{
var source = new Wma(10); // Just using Wma as a publisher
var wma = new Wma(source, 5);
source.Update(new TValue(DateTime.UtcNow, 10.0));
Assert.Equal(10.0, wma.Last.Value);
}
}
+131 -31
View File
@@ -1,5 +1,6 @@
using Xunit; using Xunit;
using TradingPlatform.BusinessLayer; using TradingPlatform.BusinessLayer;
using TradingPlatform.BusinessLayer.Chart;
using System.Drawing; using System.Drawing;
using System.Reflection; using System.Reflection;
@@ -15,14 +16,14 @@ public class IndicatorExtensionsTests
} }
} }
private sealed class TestCoordinatesConverter : ICoordinatesConverter private sealed class TestCoordinatesConverter : IChartWindowCoordinatesConverter
{ {
private readonly DateTime _time; private readonly DateTime _time;
public TestCoordinatesConverter(DateTime time) => _time = time; public TestCoordinatesConverter(DateTime time) => _time = time;
public DateTime GetTime(int x) => _time; public DateTime GetTime(int x) => _time;
public double GetChartX(DateTime time) => 0; public double GetChartX(DateTime time) => 10; // Return a fixed X for testing
public double GetChartY(double value) => 0; public double GetChartY(double value) => value; // Return value as Y for testing
} }
[Fact] [Fact]
@@ -108,21 +109,8 @@ public class IndicatorExtensionsTests
} }
[Fact] [Fact]
[System.Runtime.Versioning.SupportedOSPlatform("windows")] public void LogicMethods_CalculateCorrectly()
public void PaintMethods_DoNotThrow_WithValidGraphics()
{ {
// 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(); var indicator = new TestIndicator();
indicator.CurrentChart = new MockChart(); indicator.CurrentChart = new MockChart();
@@ -133,9 +121,92 @@ public class IndicatorExtensionsTests
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 105); 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) // Setup converter
// We added bars at now, now+1min, ..., now+19min. var validTime = now.AddMinutes(10);
// Let's return now+10min. 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); var validTime = now.AddMinutes(10);
indicator.CurrentChart.MainWindow.CoordinatesConverter = new TestCoordinatesConverter(validTime); indicator.CurrentChart.MainWindow.CoordinatesConverter = new TestCoordinatesConverter(validTime);
@@ -145,20 +216,49 @@ public class IndicatorExtensionsTests
// Test PaintHLine // Test PaintHLine
IndicatorExtensions.PaintHLine(indicator, args, 100, pen); IndicatorExtensions.PaintHLine(indicator, args, 100, pen);
// Test PaintSmoothCurve // Test PaintSmoothCurve with different LineStyles and Warmup
var series = new LineSeries("Test", Color.Blue, 1, LineStyle.Solid); foreach (LineStyle style in Enum.GetValues(typeof(LineStyle)))
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 var series = new LineSeries("Test", Color.Blue, 1, style);
for (int i = 0; i < 20; i++) series.AddValue();
IndicatorExtensions.PaintSmoothCurve(indicator, args, series, 0); 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 // Test PaintHistogram with Positive and Negative values
IndicatorExtensions.PaintHistogram(indicator, args, series, 0); 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 // Test DrawText
IndicatorExtensions.DrawText(indicator, args, "Test Text"); 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;
using TradingPlatform.BusinessLayer.Chart;
using System.Drawing; using System.Drawing;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
#nullable disable
namespace QuanTAlib; namespace QuanTAlib;
public enum SourceType public enum SourceType
@@ -88,6 +91,11 @@ public static class IndicatorExtensions
} }
#pragma warning disable CA1416 // Validate platform compatibility #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) public static void PaintHLine(this Indicator indicator, PaintChartEventArgs args, double value, Pen pen)
{ {
if (indicator.CurrentChart == null) if (indicator.CurrentChart == null)
@@ -101,7 +109,7 @@ public static class IndicatorExtensions
gr.SetClip(clientRect); gr.SetClip(clientRect);
int leftX = clientRect.Left; int leftX = clientRect.Left;
int rightX = clientRect.Right; int rightX = clientRect.Right;
int Y = (int)converter.GetChartY(value); int Y = GetHLineY(converter, value);
using (pen) 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) 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) if (!series.Visible || indicator.CurrentChart == null)
@@ -121,26 +162,13 @@ public static class IndicatorExtensions
var clientRect = mainWindow.ClientRectangle; var clientRect = mainWindow.ClientRectangle;
gr.SetClip(clientRect); 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(); List<Point> allPoints = GetSmoothCurvePoints(indicator, converter, clientRect, series);
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);
}
if (allPoints.Count > 1) 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 defaultPen = new(series.Color, series.Width) { DashStyle = ConvertLineStyleToDashStyle(series.Style) })
using (Pen coldPen = new(series.Color, series.Width) { DashStyle = DashStyle.Dot }) 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) public static void PaintHistogram(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true)
{ {
if (!series.Visible || indicator.CurrentChart == null) if (!series.Visible || indicator.CurrentChart == null)
@@ -176,32 +245,14 @@ public static class IndicatorExtensions
var clientRect = mainWindow.ClientRectangle; var clientRect = mainWindow.ClientRectangle;
gr.SetClip(clientRect); 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; foreach (var (rect, color) in rects)
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
for (int i = rightIndex; i < leftIndex; i++)
{ {
int barX = (int)converter.GetChartX(indicator.HistoricalData.Time(i)); using (Brush hist = new SolidBrush(color))
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.FromArgb(150, 0, 255, 0))) gr.FillRectangle(hist, rect);
{
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));
}
} }
} }
} }
+58 -47
View File
@@ -3,9 +3,11 @@
using System.Drawing; using System.Drawing;
namespace TradingPlatform.BusinessLayer; namespace TradingPlatform.BusinessLayer
{
using TradingPlatform.BusinessLayer.Chart;
#region Enums #region Enums
/// <summary> /// <summary>
/// Specifies the style of indicator line. /// Specifies the style of indicator line.
@@ -185,7 +187,7 @@ public class HistoricalData
for (int i = 0; i < _items.Count; i++) for (int i = 0; i < _items.Count; i++)
{ {
if (_items[i].TicksLeft == ticks) if (_items[i].TicksLeft == ticks)
return i; return Count - 1 - i;
} }
return -1; return -1;
} }
@@ -354,60 +356,68 @@ public class PaintChartEventArgs : EventArgs
#endregion #endregion
#region Chart #region Chart
/// <summary>
/// Chart interface
/// </summary>
public interface IChart
{
ChartWindow MainWindow { get; }
ChartWindow[] Windows { get; }
int BarsWidth { get; }
} }
/// <summary> namespace TradingPlatform.BusinessLayer.Chart
/// Chart window
/// </summary>
public class ChartWindow
{ {
public Rectangle ClientRectangle { get; set; } /// <summary>
public ICoordinatesConverter CoordinatesConverter { get; set; } = new MockCoordinatesConverter(); /// Coordinates converter interface
/// </summary>
public interface IChartWindowCoordinatesConverter
{
DateTime GetTime(int x);
double GetChartX(DateTime time);
double GetChartY(double value);
}
} }
/// <summary> namespace TradingPlatform.BusinessLayer
/// Coordinates converter interface
/// </summary>
public interface ICoordinatesConverter
{ {
DateTime GetTime(int x); using TradingPlatform.BusinessLayer.Chart;
double GetChartX(DateTime time);
double GetChartY(double value);
}
/// <summary> /// <summary>
/// Mock coordinates converter /// Chart interface
/// </summary> /// </summary>
public class MockCoordinatesConverter : ICoordinatesConverter public interface IChart
{ {
public DateTime GetTime(int x) => DateTime.UtcNow; ChartWindow MainWindow { get; }
public double GetChartX(DateTime time) => 0; ChartWindow[] Windows { get; }
public double GetChartY(double value) => 0; int BarsWidth { get; }
} }
/// <summary> /// <summary>
/// Mock chart for testing /// Chart window
/// </summary> /// </summary>
public class MockChart : IChart public class ChartWindow
{ {
public ChartWindow MainWindow { get; } = new(); public Rectangle ClientRectangle { get; set; }
public ChartWindow[] Windows { get; } = new[] { new ChartWindow() }; public IChartWindowCoordinatesConverter CoordinatesConverter { get; set; } = new MockCoordinatesConverter();
public int BarsWidth { get; set; } = 10; }
}
#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> /// <summary>
/// Watchlist indicator interface /// Watchlist indicator interface
@@ -488,3 +498,4 @@ public abstract class Indicator
} }
#endregion #endregion
}