diff --git a/.github/workflows/test-indicators.yml b/.github/workflows/test-indicators.yml index a5a0932a2..ada36f373 100644 --- a/.github/workflows/test-indicators.yml +++ b/.github/workflows/test-indicators.yml @@ -2,7 +2,7 @@ name: Indicators on: push: - branches: ["main"] + branches: ["main","v3"] pull_request: types: [opened, synchronize, reopened] @@ -27,29 +27,17 @@ jobs: # identifying primary configuration so only one reports coverage IS_PRIMARY: ${{ matrix.os == 'ubuntu-latest' && matrix.dotnet-version == '8.x' }} - # .NET SDK versions in the matrix that support `ga` quality spec - # versions before 5.x do not support it - SUPPORT_GA: ${{ contains(fromJson('["6.x", "8.x"]'), matrix.dotnet-version) }} - steps: - name: Checkout source uses: actions/checkout@v4 - name: Setup .NET - id: dotnet-new uses: actions/setup-dotnet@v4 - if: env.SUPPORT_GA == 'true' with: dotnet-version: ${{ matrix.dotnet-version }} dotnet-quality: "ga" - - name: Setup .NET (older) - uses: actions/setup-dotnet@v4 - if: env.SUPPORT_GA == 'false' - with: - dotnet-version: ${{ matrix.dotnet-version }} - - name: Build library run: > dotnet build @@ -58,36 +46,24 @@ jobs: -warnAsError - name: Test indicators - env: - ALPACA_KEY: ${{ secrets.ALPACA_KEY }} - ALPACA_SECRET: ${{ secrets.ALPACA_SECRET }} run: > - dotnet test tests/indicators/Tests.Indicators.csproj + dotnet test --configuration Release + --settings tests/tests.unit.runsettings + --results-directory ./test-results --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage" - --results-directory ./test-indicators # the remaining steps are only needed from one primary instance - - name: Test other items - if: env.IS_PRIMARY == 'true' - run: > - dotnet test tests/other/Tests.Other.csproj - --configuration Release - --no-build - --verbosity normal - --logger trx - --results-directory ./test-other - - name: Post test summary uses: dorny/test-reporter@v1 if: env.IS_PRIMARY == 'true' && always() with: name: Test results - path: ./test-indicators/**/*.trx + path: ./test-results/**/*.trx reporter: dotnet-trx - name: Publish coverage to Codacy @@ -95,4 +71,4 @@ jobs: if: env.IS_PRIMARY == 'true' with: project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - coverage-reports: ./test-indicators/**/coverage.cobertura.xml + coverage-reports: ./test-results/**/coverage.cobertura.xml diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml new file mode 100644 index 000000000..6da929b33 --- /dev/null +++ b/.github/workflows/test-integration.yml @@ -0,0 +1,58 @@ +name: Indicators + +on: + push: + branches: ["main","v3"] + + pull_request: + types: [opened, synchronize, reopened] + +jobs: + test: + name: integration tests + runs-on: ubuntu-latest + + permissions: + contents: read + actions: read + checks: write + + steps: + + - name: Checkout source + uses: actions/checkout@v4 + + - name: Setup .NET + id: dotnet-new + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.x" + dotnet-quality: "ga" + + - name: Build library + run: > + dotnet build + --configuration Release + --property:ContinuousIntegrationBuild=true + -warnAsError + + - name: Test integrations + env: + ALPACA_KEY: ${{ secrets.ALPACA_KEY }} + ALPACA_SECRET: ${{ secrets.ALPACA_SECRET }} + run: > + dotnet test + --configuration Release + --settings tests/tests.integration.runsettings + --results-directory ./test-results + --no-build + --verbosity normal + --logger trx + + - name: Post test summary + uses: dorny/test-reporter@v1 + if: always() + with: + name: Test results + path: ./test-results/**/*.trx + reporter: dotnet-trx diff --git a/Stock.Indicators.sln b/Stock.Indicators.sln index 8d241723d..2241c476b 100644 --- a/Stock.Indicators.sln +++ b/Stock.Indicators.sln @@ -9,24 +9,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configurat .gitattributes = .gitattributes .gitignore = .gitignore src\gitversion.yml = src\gitversion.yml + tests\tests.integration.runsettings = tests\tests.integration.runsettings + tests\tests.unit.runsettings = tests\tests.unit.runsettings EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Indicators", "src\Indicators.csproj", "{8D0F1781-EDA3-4C51-B05D-D33FF1156E49}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Indicators", "tests\indicators\Tests.Indicators.csproj", "{11CD6C7E-871F-4903-AEAD-58E034C6521D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Other", "tests\other\Tests.Other.csproj", "{97905D26-4854-41FF-A4F7-CE042B2ACD02}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Performance", "tests\performance\Tests.Performance.csproj", "{3BD4837B-D197-41FD-A286-A3256D0770E1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.Application", "tests\application\Test.Application.csproj", "{14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}" - ProjectSection(ProjectDependencies) = postProject - {11CD6C7E-871F-4903-AEAD-58E034C6521D} = {11CD6C7E-871F-4903-AEAD-58E034C6521D} - {8D0F1781-EDA3-4C51-B05D-D33FF1156E49} = {8D0F1781-EDA3-4C51-B05D-D33FF1156E49} - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.Simulation", "tests\simulate\Test.Simulation.csproj", "{9C9045D2-9928-41F8-97FC-ECCBDD3B9868}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.Application", "tests\external\application\Test.Application.csproj", "{F98B09DA-0E0B-41D1-B7B6-3A40EE6C1DBE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Integration", "tests\external\integration\Tests.Integration.csproj", "{88C59340-F6C6-497B-A7F3-08ACCC8873EE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.PublicApi", "tests\external\public-api\Tests.PublicApi.csproj", "{D6CF664F-8232-4EE9-B044-CA34A0BA522E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,22 +41,26 @@ Global {11CD6C7E-871F-4903-AEAD-58E034C6521D}.Debug|Any CPU.Build.0 = Debug|Any CPU {11CD6C7E-871F-4903-AEAD-58E034C6521D}.Release|Any CPU.ActiveCfg = Release|Any CPU {11CD6C7E-871F-4903-AEAD-58E034C6521D}.Release|Any CPU.Build.0 = Release|Any CPU - {97905D26-4854-41FF-A4F7-CE042B2ACD02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97905D26-4854-41FF-A4F7-CE042B2ACD02}.Debug|Any CPU.Build.0 = Debug|Any CPU - {97905D26-4854-41FF-A4F7-CE042B2ACD02}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97905D26-4854-41FF-A4F7-CE042B2ACD02}.Release|Any CPU.Build.0 = Release|Any CPU {3BD4837B-D197-41FD-A286-A3256D0770E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3BD4837B-D197-41FD-A286-A3256D0770E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {3BD4837B-D197-41FD-A286-A3256D0770E1}.Release|Any CPU.ActiveCfg = Release|Any CPU {3BD4837B-D197-41FD-A286-A3256D0770E1}.Release|Any CPU.Build.0 = Release|Any CPU - {14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}.Release|Any CPU.Build.0 = Release|Any CPU {9C9045D2-9928-41F8-97FC-ECCBDD3B9868}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9C9045D2-9928-41F8-97FC-ECCBDD3B9868}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C9045D2-9928-41F8-97FC-ECCBDD3B9868}.Release|Any CPU.ActiveCfg = Release|Any CPU {9C9045D2-9928-41F8-97FC-ECCBDD3B9868}.Release|Any CPU.Build.0 = Release|Any CPU + {F98B09DA-0E0B-41D1-B7B6-3A40EE6C1DBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F98B09DA-0E0B-41D1-B7B6-3A40EE6C1DBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F98B09DA-0E0B-41D1-B7B6-3A40EE6C1DBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F98B09DA-0E0B-41D1-B7B6-3A40EE6C1DBE}.Release|Any CPU.Build.0 = Release|Any CPU + {88C59340-F6C6-497B-A7F3-08ACCC8873EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88C59340-F6C6-497B-A7F3-08ACCC8873EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88C59340-F6C6-497B-A7F3-08ACCC8873EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88C59340-F6C6-497B-A7F3-08ACCC8873EE}.Release|Any CPU.Build.0 = Release|Any CPU + {D6CF664F-8232-4EE9-B044-CA34A0BA522E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6CF664F-8232-4EE9-B044-CA34A0BA522E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6CF664F-8232-4EE9-B044-CA34A0BA522E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6CF664F-8232-4EE9-B044-CA34A0BA522E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/contributing.md b/docs/contributing.md index 4524cb9fe..4b08b08f2 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -66,7 +66,7 @@ Running the `Tests.Performance` console application in `Release` mode will produ dotnet run -c Release # run individual performance benchmark -dotnet run -c Release --filter *.GetAdx +dotnet run -c Release --filter *.ToAdx ``` ## Documentation diff --git a/src/_common/Quotes/Quote.Converters.cs b/src/_common/Quotes/Quote.Converters.cs index 0a03e5e13..7db6c964c 100644 --- a/src/_common/Quotes/Quote.Converters.cs +++ b/src/_common/Quotes/Quote.Converters.cs @@ -6,7 +6,7 @@ public static partial class Quotes { /* LISTS */ - // convert TQuote type list to built-in Quote type list + // convert TQuote type list to built-in Quote type list (public API only) public static IReadOnlyList ToQuoteList( this IReadOnlyList quotes) where TQuote : IQuote @@ -27,7 +27,7 @@ internal static List ToQuoteDList( /* TYPES */ - // convert any IQuote type to native Quote type + // convert any IQuote type to native Quote type (public API only) public static Quote ToQuote(this TQuote quote) where TQuote : IQuote diff --git a/src/s-z/Sma/Sma.Utilities.cs b/src/s-z/Sma/Sma.Utilities.cs index f8700d185..fe202fc86 100644 --- a/src/s-z/Sma/Sma.Utilities.cs +++ b/src/s-z/Sma/Sma.Utilities.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; namespace Skender.Stock.Indicators; @@ -22,19 +23,20 @@ public static partial class Sma /// if incalculable /// values are in range. /// - internal static double? Average( + public static double? Average( // public API only this IReadOnlyList values, int lookbackPeriods, int? endIndex = null) where T : IReusable + { + ArgumentNullException.ThrowIfNull(values); - // TODO: unused SMA utility, either make public or remove - - => Increment( + return Increment( values, lookbackPeriods, endIndex ?? values.Count - 1) .NaN2Null(); + } /// /// Simple moving average calculation @@ -69,9 +71,10 @@ internal static double Increment( // TODO: apply this SMA increment method more widely in other indicators (see EMA example) } + [ExcludeFromCodeCoverage] // experimental SIMD code internal static double[] Increment(this double[] prices, int period) { - // TODO: is this used (probably just an experiment, has rounding errors) + // TODO: remove/consider experiment, has rounding errors int count = prices.Length - period + 1; double[] sma = new double[count]; diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..321dcb53d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,80 @@ +# Testing + +Tests are split into different projects for isolation of purpose. + +```bash +# runs all unit +# and integration tests +dotnet test +``` + +> When developing locally, we recommend that you normally _unload_ the external test projects shown below, except when testing externalities. + +## Unit tests + +> `indicators/Tests.Indicators.csproj` unit tests library + +Our primary full unit test project covers the entire utility of the library. In most IDE, you can [manually select](https://learn.microsoft.com/en-us/visualstudio/test/configure-unit-tests-by-using-a-dot-runsettings-file?view=vs-2022#manually-select-the-run-settings-file) the `tests/tests.unit.runsettings` for isolation for local IDE dev/test efficiency, or use the _unload_ approach described above. + +```bash +# CLI equivalent +dotnet test --settings tests/tests.unit.runsettings +``` + +## Performance tests + +> `tests/performance/Tests.Performance.csproj` benchmark tests + +Running the `Tests.Performance` console application in `Release` mode will produce [benchmark performance data](https://dotnet.stockindicators.dev/performance/) that we include on our documentation site. + +```bash +# run all performance benchmarks (~15-20 minutes) +dotnet run -c Release + +# run individual performance benchmark +dotnet run -c Release --filter *.ToAdx + +# run cohorts of performance benchmarks +dotnet run -c Release --filter ** +``` + +```bash +# to see all cohorts +dotnet run --list +... +# Available Benchmarks: + #0 Incrementals + #1 SeriesIndicators + #2 StreamExternal + #3 StreamIndicators + #4 Utility + #5 UtilityMaths + +# to see all tests +dotnet run --list flat +``` + +## External tests + +All external integration and API tests can be run with one CLI + +```bash +# CLI equivalent +dotnet test --settings tests/tests.integration.runsettings +``` + +Since we assume tests are non-integration tests by default, set the category attribute on any new test classes that contain integration tests. This can be applied uniquely to `[TestMethod]` as well. + +```csharp +[TestClass, TestCategory("Integration")] +public class MyIntegrationTests : TestBase +... +``` + +### Public API tests + +> `external/public-api/Tests.PublicApi.csproj` E2E libary external tests + +### Integration tests + +> `external/integration/Tests.Integration.csproj` connected to Live 3rd-party API diff --git a/tests/application/Test.Application.csproj b/tests/application/Test.Application.csproj deleted file mode 100644 index a727a0290..000000000 --- a/tests/application/Test.Application.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - Exe - net8.0 - enable - - - - - - - - - Always - - - - diff --git a/tests/application/GlobalUsings.cs b/tests/external/application/GlobalUsings.cs similarity index 100% rename from tests/application/GlobalUsings.cs rename to tests/external/application/GlobalUsings.cs diff --git a/tests/application/Program.cs b/tests/external/application/Program.cs similarity index 91% rename from tests/application/Program.cs rename to tests/external/application/Program.cs index 620267b46..c5a305fee 100644 --- a/tests/application/Program.cs +++ b/tests/external/application/Program.cs @@ -1,6 +1,6 @@ namespace Test.Application; -internal class Program +public class Program { private static void Main(string[] args) { @@ -11,18 +11,20 @@ private static void Main(string[] args) string scenario = "C"; + Go go = new(); + switch (scenario) { - case "A": Do.QuoteHub(); break; - case "B": Do.EmaHub(); break; - case "C": Do.MultipleSubscribers(); break; + case "A": go.QuoteHub(); break; + case "B": go.EmaHub(); break; + case "C": go.MultipleSubscribers(); break; } } } -public class Do +public class Go { - private static readonly bool verbose = true; // turn this off when profiling + private readonly bool verbose = true; private static readonly QuoteHub provider = new(); @@ -30,9 +32,9 @@ public class Do private static readonly int quotesLength = quotesList.Count; - internal Do() + public Go() { - if (!verbose) + if (verbose) { Prefill(); } @@ -47,7 +49,7 @@ private static void Prefill() } } - internal static void QuoteHub() + internal void QuoteHub() { EmaHub emaHub = provider.ToEma(14); @@ -83,7 +85,7 @@ private static void SendToConsole(Quote q) } - internal static void EmaHub() + internal void EmaHub() { EmaHub emaHub = provider.ToEma(14); @@ -131,7 +133,7 @@ private static void SendToConsole(Quote q, EmaHub emaHub) Console.WriteLine(m); } - internal static void MultipleSubscribers() + internal void MultipleSubscribers() { SmaHub smaHub = provider.ToSma(3); EmaHub emaHub = provider.ToEma(5); diff --git a/tests/application/README.md b/tests/external/application/README.md similarity index 100% rename from tests/application/README.md rename to tests/external/application/README.md diff --git a/tests/external/application/Test.Application.csproj b/tests/external/application/Test.Application.csproj new file mode 100644 index 000000000..86cd6da93 --- /dev/null +++ b/tests/external/application/Test.Application.csproj @@ -0,0 +1,28 @@ + + + + Exe + net8.0 + + enable + enable + + true + latest + AllEnabledByDefault + true + true + true + + + + + + + + + Always + + + + diff --git a/tests/application/_testdata/TestData.Getter.cs b/tests/external/application/_testdata/TestData.Getter.cs similarity index 100% rename from tests/application/_testdata/TestData.Getter.cs rename to tests/external/application/_testdata/TestData.Getter.cs diff --git a/tests/application/_testdata/TestData.Imports.cs b/tests/external/application/_testdata/TestData.Imports.cs similarity index 100% rename from tests/application/_testdata/TestData.Imports.cs rename to tests/external/application/_testdata/TestData.Imports.cs diff --git a/tests/application/_testdata/data/compare.csv b/tests/external/application/_testdata/data/compare.csv similarity index 100% rename from tests/application/_testdata/data/compare.csv rename to tests/external/application/_testdata/data/compare.csv diff --git a/tests/application/_testdata/data/default.csv b/tests/external/application/_testdata/data/default.csv similarity index 100% rename from tests/application/_testdata/data/default.csv rename to tests/external/application/_testdata/data/default.csv diff --git a/tests/application/_testdata/data/intraday.csv b/tests/external/application/_testdata/data/intraday.csv similarity index 100% rename from tests/application/_testdata/data/intraday.csv rename to tests/external/application/_testdata/data/intraday.csv diff --git a/tests/application/_testdata/data/longest.csv b/tests/external/application/_testdata/data/longest.csv similarity index 100% rename from tests/application/_testdata/data/longest.csv rename to tests/external/application/_testdata/data/longest.csv diff --git a/tests/application/_testdata/data/longish.csv b/tests/external/application/_testdata/data/longish.csv similarity index 100% rename from tests/application/_testdata/data/longish.csv rename to tests/external/application/_testdata/data/longish.csv diff --git a/tests/external/integration/GlobalUsings.cs b/tests/external/integration/GlobalUsings.cs new file mode 100644 index 000000000..de3d38d3d --- /dev/null +++ b/tests/external/integration/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; + +[assembly: Parallelize] diff --git a/tests/other/Tests.Other.csproj b/tests/external/integration/Tests.Integration.csproj similarity index 77% rename from tests/other/Tests.Other.csproj rename to tests/external/integration/Tests.Integration.csproj index 7d31c15e1..de6f586ca 100644 --- a/tests/other/Tests.Other.csproj +++ b/tests/external/integration/Tests.Integration.csproj @@ -2,30 +2,35 @@ net8.0 - enable false + enable + enable + true latest AllEnabledByDefault true true true + + true + + - + + + all runtime; build; native; contentfiles; analyzers - - - + - diff --git a/tests/external/integration/WilliamsR.Tests.cs b/tests/external/integration/WilliamsR.Tests.cs new file mode 100644 index 000000000..149d5dedd --- /dev/null +++ b/tests/external/integration/WilliamsR.Tests.cs @@ -0,0 +1,40 @@ +using Skender.Stock.Indicators; + +namespace Tests.Indicators; + +[TestClass, TestCategory("Integration")] +public class WilliamsRTests +{ + [TestMethod] + public async Task Issue1127() + { + // initialize + IEnumerable feedQuotes = await FeedData // live quotes + .GetQuotes("A", 365 * 3) + .ConfigureAwait(false); + + IReadOnlyList quotes = feedQuotes.ToList(); + int length = quotes.Count; + + // get indicators + IReadOnlyList resultsList = quotes + .ToWilliamsR(14); + + Console.WriteLine($"%R from {length} quotes."); + + // analyze boundary + for (int i = 0; i < length; i++) + { + Quote q = quotes[i]; + WilliamsResult r = resultsList[i]; + + Console.WriteLine($"{q.Timestamp:s} {r.WilliamsR}"); + + if (r.WilliamsR is not null) + { + Assert.IsTrue(r.WilliamsR <= 0); + Assert.IsTrue(r.WilliamsR >= -100); + } + } + } +} diff --git a/tests/external/integration/_common/Helper.LiveQuotes.cs b/tests/external/integration/_common/Helper.LiveQuotes.cs new file mode 100644 index 000000000..690f8b5cd --- /dev/null +++ b/tests/external/integration/_common/Helper.LiveQuotes.cs @@ -0,0 +1,75 @@ +using Alpaca.Markets; + +using Skender.Stock.Indicators; + +namespace Tests.Indicators; + +internal static class FeedData +{ + internal static async Task> GetQuotes(string symbol) + => await GetQuotes(symbol, 365 * 2) + .ConfigureAwait(false); + + internal static async Task> GetQuotes(string symbol, int days) + { + /* This won't run if environment variables not set. + Use FeedData.InconclusiveIfNotSetup() in tests. + + (1) get your API keys + https://alpaca.markets/docs/market-data/getting-started/ + + (2) manually install in your environment (replace value) + + setx ALPACA_KEY "y0ur_Alp@ca_K3Y_v@lue" + setx ALPACA_SECRET "y0ur_Alp@ca_S3cret_v@lue" + + ****************************************************/ + + // get and validate keys + string? alpacaApiKey = Environment.GetEnvironmentVariable("ALPACA_KEY"); + string? alpacaSecret = Environment.GetEnvironmentVariable("ALPACA_SECRET"); + + if (string.IsNullOrEmpty(alpacaApiKey) || string.IsNullOrEmpty(alpacaSecret)) + { + Assert.Inconclusive("Data feed unusable. Environment variables missing."); + } + + ArgumentException.ThrowIfNullOrEmpty(nameof(alpacaApiKey)); + ArgumentException.ThrowIfNullOrEmpty(nameof(alpacaSecret)); + + // connect to Alpaca REST API + SecretKey secretKey = new(alpacaApiKey, alpacaSecret); + + IAlpacaDataClient client = Environments + .Paper + .GetAlpacaDataClient(secretKey); + + // compose request + // (excludes last 15 minutes for free delayed quotes) + DateTime into = DateTime.Now.Subtract(TimeSpan.FromMinutes(16)); + DateTime from = into.Subtract(TimeSpan.FromDays(days)); + + HistoricalBarsRequest request = new(symbol, from, into, BarTimeFrame.Day); + + // fetch minute-bar quotes in Alpaca's format + IPage barSet = await client + .ListHistoricalBarsAsync(request) + .ConfigureAwait(false); + + // convert library compatible quotes + IEnumerable quotes = barSet + .Items + .Select(bar => new Quote + ( + Timestamp: bar.TimeUtc, + Open: bar.Open, + High: bar.High, + Low: bar.Low, + Close: bar.Close, + Volume: bar.Volume + )) + .OrderBy(x => x.Timestamp); + + return quotes; + } +} diff --git a/tests/other/Convergence.Tests.cs b/tests/external/public-api/Convergence.Tests.cs similarity index 99% rename from tests/other/Convergence.Tests.cs rename to tests/external/public-api/Convergence.Tests.cs index 541bb7cca..ebe75e0a1 100644 --- a/tests/other/Convergence.Tests.cs +++ b/tests/external/public-api/Convergence.Tests.cs @@ -1,6 +1,6 @@ namespace Behavioral; -[TestClass] +[TestClass, TestCategory("Integration")] public class Convergence : TestBase { private static readonly int[] QuotesQuantities = diff --git a/tests/other/GlobalUsings.cs b/tests/external/public-api/GlobalUsings.cs similarity index 100% rename from tests/other/GlobalUsings.cs rename to tests/external/public-api/GlobalUsings.cs diff --git a/tests/external/public-api/Tests.PublicApi.csproj b/tests/external/public-api/Tests.PublicApi.csproj new file mode 100644 index 000000000..abd2b8ede --- /dev/null +++ b/tests/external/public-api/Tests.PublicApi.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + false + + enable + disable + + true + latest + AllEnabledByDefault + true + true + true + + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + diff --git a/tests/other/Custom.Indicator.Tests.cs b/tests/external/public-api/customizable/Custom.Indicator.Tests.cs similarity index 99% rename from tests/other/Custom.Indicator.Tests.cs rename to tests/external/public-api/customizable/Custom.Indicator.Tests.cs index 133d4d488..abe0ae097 100644 --- a/tests/other/Custom.Indicator.Tests.cs +++ b/tests/external/public-api/customizable/Custom.Indicator.Tests.cs @@ -6,7 +6,7 @@ namespace Customization; // CUSTOM INDICATORS -[TestClass] +[TestClass, TestCategory("Integration")] public class CustomIndicators { private static readonly CultureInfo EnglishCulture = new("en-US", false); diff --git a/tests/other/Custom.Quotes.Tests.cs b/tests/external/public-api/customizable/Custom.Quotes.Tests.cs similarity index 99% rename from tests/other/Custom.Quotes.Tests.cs rename to tests/external/public-api/customizable/Custom.Quotes.Tests.cs index 3b50b542d..e1c29af25 100644 --- a/tests/other/Custom.Quotes.Tests.cs +++ b/tests/external/public-api/customizable/Custom.Quotes.Tests.cs @@ -5,7 +5,7 @@ namespace Customization; // CUSTOM QUOTES -[TestClass] +[TestClass, TestCategory("Integration")] public class CustomQuotes { private static readonly CultureInfo EnglishCulture diff --git a/tests/other/Custom.Results.Tests.cs b/tests/external/public-api/customizable/Custom.Results.Tests.cs similarity index 86% rename from tests/other/Custom.Results.Tests.cs rename to tests/external/public-api/customizable/Custom.Results.Tests.cs index d7b4c7f60..ea8f1ccc3 100644 --- a/tests/other/Custom.Results.Tests.cs +++ b/tests/external/public-api/customizable/Custom.Results.Tests.cs @@ -5,7 +5,7 @@ namespace Customization; // CUSTOM RESULTS -[TestClass] +[TestClass, TestCategory("Integration")] public class CustomResults { private static readonly CultureInfo EnglishCulture @@ -74,13 +74,4 @@ List emaResults EmaResult r = emaResults.Find(x => x.Timestamp == findDate); Assert.AreEqual(249.3519m, Math.Round((decimal)r.Ema, 4)); } - - [TestMethod] - public void CustomReusable() => Assert.Inconclusive("Test not implemented"); - - [TestMethod] - public void CustomReusableInherited() => Assert.Inconclusive("Test not implemented"); - - [TestMethod] - public void CustomInheritedEma() => Assert.Inconclusive("Test not implemented"); } diff --git a/tests/other/Sut.CustomItems.cs b/tests/external/public-api/customizable/Sut.CustomItems.cs similarity index 100% rename from tests/other/Sut.CustomItems.cs rename to tests/external/public-api/customizable/Sut.CustomItems.cs diff --git a/tests/other/PublicApi.Interface.Tests.cs b/tests/external/public-api/indicators/PublicApi.Interface.Tests.cs similarity index 99% rename from tests/other/PublicApi.Interface.Tests.cs rename to tests/external/public-api/indicators/PublicApi.Interface.Tests.cs index 9dad62942..fc80b25cc 100644 --- a/tests/other/PublicApi.Interface.Tests.cs +++ b/tests/external/public-api/indicators/PublicApi.Interface.Tests.cs @@ -6,6 +6,7 @@ namespace PublicApi; // PUBLIC API (INTERFACES) [TestClass] +[TestCategory("Integration")] public class UserInterface { private static readonly IReadOnlyList quotes = Data.GetDefault(); diff --git a/tests/indicators/README.md b/tests/indicators/README.md new file mode 100644 index 000000000..c44b96c5e --- /dev/null +++ b/tests/indicators/README.md @@ -0,0 +1,82 @@ +# Testing + +Tests are split into different projects for isolation of purpose. + +```bash +# runs all unit +# and integration tests +dotnet test +``` + +> When developing locally, we recommend that you normally _unload_ the external test projects shown below, except when testing externalities. + +## Unit tests + +> `indicators/Tests.Indicators.csproj` unit tests library + +Our primary full unit test project covers the entire utility of the library. In most IDE, you can [manually select](https://learn.microsoft.com/en-us/visualstudio/test/configure-unit-tests-by-using-a-dot-runsettings-file?view=vs-2022#manually-select-the-run-settings-file) the `tests/tests.unit.runsettings` for isolation for local IDE dev/test efficiency, or use the _unload_ approach described above. + +```bash +# CLI equivalent +dotnet test --settings tests/tests.unit.runsettings +``` + +## Performance tests + +> `tests/performance/Tests.Performance.csproj` benchmark tests + +Running the `Tests.Performance` console application in `Release` mode will produce [benchmark performance data](https://dotnet.stockindicators.dev/performance/) that we include on our documentation site. + +```bash +# run all performance benchmarks (~15-20 minutes) +dotnet run -c Release + +# run individual performance benchmark +dotnet run -c Release --filter *.ToAdx + +# run cohorts of performance benchmarks +dotnet run -c Release --filter ** +``` + +```bash +# to see all cohorts +dotnet run --list +... +# Available Benchmarks: + #0 Incrementals + #1 SeriesIndicators + #2 StreamExternal + #3 StreamIndicators + #4 Utility + #5 UtilityMaths + +# to see all tests +dotnet run --list flat +``` + +## External tests + +All external integration and API tests can be run with one CLI + +```bash +# CLI equivalent +dotnet test --settings tests/tests.integration.runsettings +``` +Since we assume tests are non-integration tests by default, set the category attribute on any new test classes that contain integration tests. This can be applied uniquely to `[TestMethod]` as well. + +```csharp +[TestClass, TestCategory("Integration")] +public class MyIntegrationTests : TestBase +... +``` + +### Public API tests + +> `external/tests.Indicators.csproj` exercises real-world scenarios against a directly loaded package. + + + +### Integration tests + +- `indicators/tests.Indicators.csproj` unit tests the main NuGet library + diff --git a/tests/indicators/TestBase.cs b/tests/indicators/TestBase.cs index 5486c5f0e..adf3a165f 100644 --- a/tests/indicators/TestBase.cs +++ b/tests/indicators/TestBase.cs @@ -4,7 +4,7 @@ // GLOBALS & INITIALIZATION OF TEST DATA [assembly: CLSCompliant(true)] -[assembly: InternalsVisibleTo("Tests.Other")] // these use test data +[assembly: InternalsVisibleTo("Tests.PublicApi")] // these use test data [assembly: InternalsVisibleTo("Tests.Performance")] [assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/tests/indicators/Tests.Indicators.csproj b/tests/indicators/Tests.Indicators.csproj index 7f6a08c48..ef89704b7 100644 --- a/tests/indicators/Tests.Indicators.csproj +++ b/tests/indicators/Tests.Indicators.csproj @@ -2,9 +2,11 @@ net8.0 - enable false + enable + disable + true latest AllEnabledByDefault @@ -16,9 +18,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers diff --git a/tests/indicators/_common/Generics/BinarySettingsTests.cs b/tests/indicators/_common/Generics/BinarySettingsTests.cs index aa2b01b78..8aee04b11 100644 --- a/tests/indicators/_common/Generics/BinarySettingsTests.cs +++ b/tests/indicators/_common/Generics/BinarySettingsTests.cs @@ -69,5 +69,26 @@ public void Equality() BinarySettings sut = new(); Assert.AreEqual(0b00000000, sut.Settings); Assert.AreEqual(0b11111111, sut.Mask); + + object obj = new BinarySettings(0b01100010); + BinarySettings sutA = new(0b01100010); + BinarySettings sutB = new(0b01100010); + BinarySettings sutC = new(0b01101010); // different + + Assert.AreEqual(sutA, sutB); + + // object equality + Assert.IsTrue(obj.Equals(sutA)); + Assert.IsTrue(sutA.Equals(obj)); + + // struct equality + Assert.IsTrue(sutA.Equals(sutB)); + Assert.IsTrue(sutB.Equals(sutA)); + Assert.IsFalse(sutB.Equals(sutC)); + + // operator equality + Assert.IsTrue(sutA == sutB); + Assert.IsFalse(sutA == sutC); + Assert.IsTrue(sutB != sutC); } } diff --git a/tests/indicators/_common/Generics/Transforms.Tests.cs b/tests/indicators/_common/Generics/Transforms.Tests.cs index 60a8da66b..719e810c1 100644 --- a/tests/indicators/_common/Generics/Transforms.Tests.cs +++ b/tests/indicators/_common/Generics/Transforms.Tests.cs @@ -14,7 +14,7 @@ public void ToCollection() Assert.IsNotNull(collection); Assert.AreEqual(502, collection.Count); - Assert.AreEqual(245.28m, collection.LastOrDefault().Close); + Assert.AreEqual(245.28m, collection[^1].Close); } // null ToCollection diff --git a/tests/indicators/_common/Observables/StreamHub.CacheMgmt.Tests.cs b/tests/indicators/_common/Observables/StreamHub.CacheMgmt.Tests.cs index 2ebd45acf..188ba1470 100644 --- a/tests/indicators/_common/Observables/StreamHub.CacheMgmt.Tests.cs +++ b/tests/indicators/_common/Observables/StreamHub.CacheMgmt.Tests.cs @@ -3,12 +3,6 @@ namespace Observables; [TestClass] public class CacheManagement : TestBase { - [TestMethod] - public void ModifyWithAnalysis() => Assert.Inconclusive("test not implemented"); - - [TestMethod] - public void ModifyWithAct() => Assert.Inconclusive("test not implemented"); - [TestMethod] public void Remove() { @@ -24,9 +18,6 @@ public void Remove() observer.Results[19].Sma.Should().BeApproximately(214.5260, precision: DoublePrecision); } - [TestMethod] // TODO: tests should include all Act enum methods - public void ActInstructions() => Assert.Inconclusive("test not implemented"); - [TestMethod] public void ActAddOld() // late arrival { diff --git a/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs b/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs index 8d837fe98..03ee23803 100644 --- a/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs +++ b/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs @@ -1,5 +1,3 @@ -using System.Collections.ObjectModel; - // quote list converters namespace Utilities; @@ -29,4 +27,64 @@ public void ToSortedList() DateTime spotDate = DateTime.ParseExact("03/16/2017", "MM/dd/yyyy", TestBase.invariantCulture); Assert.AreEqual(spotDate, h[50].Timestamp); } + + [TestMethod] + public void ToQuoteList() + { + // setup + IReadOnlyList quotes + = Quotes.Take(5).ToList(); + + IReadOnlyList myQuotes = quotes + .Select(x => new MyQuote { + Timestamp = x.Timestamp, + Open = x.Open, + High = x.High, + Low = x.Low, + Close = x.Close, + Volume = x.Volume + }).ToList(); + + // sut + IReadOnlyList sut + = myQuotes.ToQuoteList(); + + // assert is same as original + sut.Should().BeEquivalentTo(quotes); + } + + [TestMethod] + public void ToQuote() + { + // setup + Quote q = Quotes[0]; + + MyQuote myQuote = new() { + Timestamp = q.Timestamp, + Open = q.Open, + High = q.High, + Low = q.Low, + Close = q.Close, + Volume = q.Volume + }; + + // sut + Quote sut = myQuote.ToQuote(); + + // assert value based equality + sut.Should().Be(q); + sut.Value.Should().Be(q.Value); + } + + private class MyQuote : IQuote + { + public DateTime Timestamp { get; set; } + public decimal Open { get; set; } + public decimal High { get; set; } + public decimal Low { get; set; } + public decimal Close { get; set; } + public decimal Volume { get; set; } + + public double Value => (double)Close; + } } diff --git a/tests/indicators/_common/Use (QuotePart)/QuotePart.Utilities.Tests.cs b/tests/indicators/_common/Use (QuotePart)/QuotePart.Utilities.Tests.cs index bbf643f4b..ed56cd917 100644 --- a/tests/indicators/_common/Use (QuotePart)/QuotePart.Utilities.Tests.cs +++ b/tests/indicators/_common/Use (QuotePart)/QuotePart.Utilities.Tests.cs @@ -3,8 +3,16 @@ namespace Utilities; [TestClass] public class QuoteParts : TestBase { - // this is an alias of Quotes.Use() - // so we're only testing the base utilities here + [TestMethod] + public void Instantiation() + { + Quote q = Quotes[1]; + + QuotePart sut0 = new(q.Timestamp, (double)q.Close); + QuotePart sut1 = new(q); + + sut1.Should().Be(sut0); + } [TestMethod] public void ConvertQuote() diff --git a/tests/indicators/s-z/Sma/Sma.StaticSeries.Tests.cs b/tests/indicators/s-z/Sma/Sma.StaticSeries.Tests.cs index d66fe61ff..2f2caaa58 100644 --- a/tests/indicators/s-z/Sma/Sma.StaticSeries.Tests.cs +++ b/tests/indicators/s-z/Sma/Sma.StaticSeries.Tests.cs @@ -1,7 +1,7 @@ namespace StaticSeries; [TestClass] -public class Sma : StaticSeriesTestBase +public partial class Sma : StaticSeriesTestBase { [TestMethod] public override void Standard() diff --git a/tests/indicators/s-z/Sma/Sma.Utilities.Tests.cs b/tests/indicators/s-z/Sma/Sma.Utilities.Tests.cs new file mode 100644 index 000000000..dd8e344b6 --- /dev/null +++ b/tests/indicators/s-z/Sma/Sma.Utilities.Tests.cs @@ -0,0 +1,23 @@ +namespace StaticSeries; + +public partial class Sma : StaticSeriesTestBase +{ + [TestMethod] + public void Average() + { + QuotePart[] results = new[] + { + new QuotePart(Quotes[0].Timestamp, 0.0), + new QuotePart(Quotes[1].Timestamp, 4.0), + new QuotePart(Quotes[2].Timestamp, 8.0) + }; + + // sut + double? mid = results.Average(2, 1); + double? end = results.Average(2); + + // assert + mid.Should().Be(2); + end.Should().Be(6); + } +} diff --git a/tests/performance/Tests.Performance.csproj b/tests/performance/Tests.Performance.csproj index 43bc26589..18fb733e2 100644 --- a/tests/performance/Tests.Performance.csproj +++ b/tests/performance/Tests.Performance.csproj @@ -5,10 +5,6 @@ Exe Performance.Program - false - None - true - enable true @@ -17,6 +13,11 @@ true true true + + false + None + true + diff --git a/tests/simulate/Test.Simulation.csproj b/tests/simulate/Test.Simulation.csproj index 914048054..6c4315d35 100644 --- a/tests/simulate/Test.Simulation.csproj +++ b/tests/simulate/Test.Simulation.csproj @@ -3,8 +3,16 @@ Exe net8.0 + enable enable + + true + latest + AllEnabledByDefault + true + true + true diff --git a/tests/simulate/Utilities.cs b/tests/simulate/Utilities.cs index 941f5a659..4cf5a9ba2 100644 --- a/tests/simulate/Utilities.cs +++ b/tests/simulate/Utilities.cs @@ -1,20 +1,23 @@ using Skender.Stock.Indicators; namespace Utilities; +#pragma warning disable CA5394 // Do not use insecure randomness internal static class Util { internal static List Setup(int quantityToStream, int quotesPerMinute) { + // Log the simulation rate Console.WriteLine($"Simulating {quotesPerMinute:N0} quotes per minute"); PrintHeader(); + // Generate and return a list of quotes using Geometric Brownian Motion return new RandomGbm(bars: quantityToStream); } internal static void PrintHeader() { - // dislay header + // Display header for the output Console.WriteLine(); Console.WriteLine(""" Date Close price SMA(3) EMA(5) EMA(7,HL2) SMA/EMA(8) @@ -29,19 +32,22 @@ internal static void PrintData( EmaHub useChain, EmaHub emaChain) { - // send output to console + // Format the initial part of the output string with timestamp and close price string m = $"{q.Timestamp:yyyy-MM-dd HH:mm} {q.Close,11:N2}"; + // Get the latest results from the hubs SmaResult s = smaHub.Results[^1]; EmaResult e = emaHub.Results[^1]; EmaResult u = useChain.Results[^1]; EmaResult c = emaChain.Results[^1]; + // Append SMA result if available if (s.Sma is not null) { m += $"{s.Sma,12:N1}"; } + // Append EMA results if available if (e.Ema is not null) { m += $"{e.Ema,12:N1}"; @@ -57,6 +63,7 @@ internal static void PrintData( m += $"{c.Ema,12:N1}"; } + // Output the formatted string to the console Console.WriteLine(m); } } @@ -80,13 +87,13 @@ internal static void PrintData( /// Seed: starting value of the random series; should not be 0. /// -internal class RandomGbm : List +internal sealed class RandomGbm : List { private readonly double _volatility; private readonly double _drift; private double _seed; - internal RandomGbm( + public RandomGbm( int bars = 250, double volatility = 1.0, double drift = 0.01, diff --git a/tests/tests.integration.runsettings b/tests/tests.integration.runsettings new file mode 100644 index 000000000..30d2fbffd --- /dev/null +++ b/tests/tests.integration.runsettings @@ -0,0 +1,5 @@ + + + TestCategory=Integration + + diff --git a/tests/tests.unit.runsettings b/tests/tests.unit.runsettings new file mode 100644 index 000000000..c807c9384 --- /dev/null +++ b/tests/tests.unit.runsettings @@ -0,0 +1,5 @@ + + + TestCategory!=Integration + + \ No newline at end of file