mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 13:58:04 +00:00
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:
+1
-1
@@ -402,7 +402,7 @@ FodyWeavers.xsd
|
|||||||
.sonarqube/
|
.sonarqube/
|
||||||
|
|
||||||
# Cline Memory Bank - exclude from git
|
# Cline Memory Bank - exclude from git
|
||||||
memory-bank/
|
ilspy/
|
||||||
|
|
||||||
# macOS resource forks and metadata
|
# macOS resource forks and metadata
|
||||||
._*
|
._*
|
||||||
|
|||||||
+88
-72
@@ -1,72 +1,88 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
VisualStudioVersion = 17.0.31903.59
|
# Visual Studio Version 17
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
VisualStudioVersion = 17.0.31903.59
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "quantalib", "lib\quantalib.csproj", "{F455234B-2A3C-140A-17C3-683D7820A733}"
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
EndProject
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "quantalib", "lib\quantalib.csproj", "{F455234B-2A3C-140A-17C3-683D7820A733}"
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{3A8DF596-E814-FECC-DD4B-D8EF8AAC1A0D}"
|
EndProject
|
||||||
EndProject
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{3A8DF596-E814-FECC-DD4B-D8EF8AAC1A0D}"
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuanTAlib.Tests", "lib\QuanTAlib.Tests.csproj", "{953F0406-DD9B-406E-993D-6D988D5F5423}"
|
EndProject
|
||||||
EndProject
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuanTAlib.Tests", "lib\QuanTAlib.Tests.csproj", "{953F0406-DD9B-406E-993D-6D988D5F5423}"
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "quantower", "quantower", "{6CF592EE-4302-E72F-3CB4-AB1D314DD5A8}"
|
EndProject
|
||||||
EndProject
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "quantower", "quantower", "{6CF592EE-4302-E72F-3CB4-AB1D314DD5A8}"
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Averages", "quantower\Averages.csproj", "{D8F03B19-F99F-475F-8951-85C9D2258B73}"
|
EndProject
|
||||||
EndProject
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Averages", "quantower\Averages.csproj", "{D8F03B19-F99F-475F-8951-85C9D2258B73}"
|
||||||
Global
|
EndProject
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quantower.Tests", "quantower\Quantower.Tests.csproj", "{576835AB-6453-4413-A2E7-54B6725CDF9D}"
|
||||||
Debug|Any CPU = Debug|Any CPU
|
EndProject
|
||||||
Debug|x64 = Debug|x64
|
Global
|
||||||
Debug|x86 = Debug|x86
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Release|Any CPU = Release|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
Release|x64 = Release|x64
|
Debug|x64 = Debug|x64
|
||||||
Release|x86 = Release|x86
|
Debug|x86 = Debug|x86
|
||||||
EndGlobalSection
|
Release|Any CPU = Release|Any CPU
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
Release|x64 = Release|x64
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
Release|x86 = Release|x86
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
EndGlobalSection
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|x64.ActiveCfg = Debug|Any CPU
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|x64.Build.0 = Debug|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|x86.Build.0 = Debug|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|x64.ActiveCfg = Release|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|x64.Build.0 = Release|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|x86.ActiveCfg = Release|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|x86.Build.0 = Release|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{F455234B-2A3C-140A-17C3-683D7820A733}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|x64.Build.0 = Debug|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|x86.Build.0 = Debug|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|Any CPU.Build.0 = Release|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|x64.ActiveCfg = Release|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|x64.Build.0 = Release|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|x86.ActiveCfg = Release|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|x86.Build.0 = Release|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{953F0406-DD9B-406E-993D-6D988D5F5423}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|x64.Build.0 = Debug|Any CPU
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|x86.Build.0 = Debug|Any CPU
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|x64.ActiveCfg = Release|Any CPU
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|x64.Build.0 = Release|Any CPU
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|x86.ActiveCfg = Release|Any CPU
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|x86.Build.0 = Release|Any CPU
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
EndGlobalSection
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|x64.Build.0 = Release|Any CPU
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
HideSolutionNode = FALSE
|
{D8F03B19-F99F-475F-8951-85C9D2258B73}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
GlobalSection(NestedProjects) = preSolution
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{953F0406-DD9B-406E-993D-6D988D5F5423} = {3A8DF596-E814-FECC-DD4B-D8EF8AAC1A0D}
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{D8F03B19-F99F-475F-8951-85C9D2258B73} = {6CF592EE-4302-E72F-3CB4-AB1D314DD5A8}
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
EndGlobalSection
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
SolutionGuid = {E6DB434C-508E-4231-B8A6-5EDD7FF87E22}
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
EndGlobalSection
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobal
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{576835AB-6453-4413-A2E7-54B6725CDF9D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{953F0406-DD9B-406E-993D-6D988D5F5423} = {3A8DF596-E814-FECC-DD4B-D8EF8AAC1A0D}
|
||||||
|
{D8F03B19-F99F-475F-8951-85C9D2258B73} = {6CF592EE-4302-E72F-3CB4-AB1D314DD5A8}
|
||||||
|
{576835AB-6453-4413-A2E7-54B6725CDF9D} = {6CF592EE-4302-E72F-3CB4-AB1D314DD5A8}
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {E6DB434C-508E-4231-B8A6-5EDD7FF87E22}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -67,8 +67,8 @@ public readonly struct TBar : IEquatable<TBar>
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]";
|
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]";
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
#pragma warning disable S1244 // Floating point equality is intentional for exact struct comparison
|
#pragma warning disable S1244 // Floating point equality is intentional for exact struct comparison
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool Equals(TBar other) =>
|
public bool Equals(TBar other) =>
|
||||||
Time == other.Time &&
|
Time == other.Time &&
|
||||||
Open == other.Open &&
|
Open == other.Open &&
|
||||||
@@ -76,10 +76,10 @@ public readonly struct TBar : IEquatable<TBar>
|
|||||||
Low == other.Low &&
|
Low == other.Low &&
|
||||||
Close == other.Close &&
|
Close == other.Close &&
|
||||||
Volume == other.Volume;
|
Volume == other.Volume;
|
||||||
#pragma warning restore S1244
|
|
||||||
|
|
||||||
public override bool Equals(object? obj) => obj is TBar other && Equals(other);
|
public override bool Equals(object? obj) => obj is TBar other && Equals(other);
|
||||||
public override int GetHashCode() => HashCode.Combine(Time, Open, High, Low, Close, Volume);
|
public override int GetHashCode() => HashCode.Combine(Time, Open, High, Low, Close, Volume);
|
||||||
public static bool operator ==(TBar left, TBar right) => left.Equals(right);
|
public static bool operator ==(TBar left, TBar right) => left.Equals(right);
|
||||||
public static bool operator !=(TBar left, TBar right) => !left.Equals(right);
|
public static bool operator !=(TBar left, TBar right) => !left.Equals(right);
|
||||||
|
#pragma warning restore S1244
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-10
@@ -10,18 +10,24 @@ using Skender.Stock.Indicators;
|
|||||||
using TALib;
|
using TALib;
|
||||||
using Tulip;
|
using Tulip;
|
||||||
|
|
||||||
var config = ManualConfig.Create(DefaultConfig.Instance)
|
|
||||||
.AddJob(Job.ShortRun
|
|
||||||
.WithToolchain(InProcessNoEmitToolchain.Instance)
|
|
||||||
.WithId(".NET 10.0"))
|
|
||||||
.AddColumn(StatisticColumn.Mean)
|
|
||||||
.AddColumn(StatisticColumn.StdDev)
|
|
||||||
.HideColumns(Column.Job, Column.Error, Column.RatioSD);
|
|
||||||
|
|
||||||
BenchmarkRunner.Run<IndicatorBenchmarks>(config);
|
|
||||||
|
|
||||||
namespace QuanTAlib.Benchmarks;
|
namespace QuanTAlib.Benchmarks;
|
||||||
|
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static void Main()
|
||||||
|
{
|
||||||
|
var config = ManualConfig.Create(DefaultConfig.Instance)
|
||||||
|
.AddJob(Job.ShortRun
|
||||||
|
.WithToolchain(InProcessNoEmitToolchain.Instance)
|
||||||
|
.WithId(".NET 10.0"))
|
||||||
|
.AddColumn(StatisticColumn.Mean)
|
||||||
|
.AddColumn(StatisticColumn.StdDev)
|
||||||
|
.HideColumns(Column.Job, Column.Error, Column.RatioSD);
|
||||||
|
|
||||||
|
BenchmarkRunner.Run<IndicatorBenchmarks>(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[MemoryDiagnoser]
|
[MemoryDiagnoser]
|
||||||
[MarkdownExporter, HtmlExporter]
|
[MarkdownExporter, HtmlExporter]
|
||||||
public class IndicatorBenchmarks
|
public class IndicatorBenchmarks
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="*.cs" />
|
<Compile Include="IndicatorExtensions.cs" />
|
||||||
<Compile Include="..\lib\core\**\*.cs" Exclude="..\lib\core\**\*.Tests.cs" />
|
<Compile Include="..\lib\core\**\*.cs" Exclude="..\lib\core\**\*.Tests.cs" />
|
||||||
<Compile Include="..\lib\averages\**\*.cs" Exclude="..\lib\averages\**\*.Tests.cs" />
|
<Compile Include="..\lib\averages\**\*.cs" Exclude="..\lib\averages\**\*.Tests.cs" />
|
||||||
<Reference Include="TradingPlatform.BusinessLayer">
|
<Reference Include="TradingPlatform.BusinessLayer">
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user