diff --git a/.editorconfig b/.editorconfig index 8be070d53..6eba79cb1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,18 +15,9 @@ max_line_length = 150 trim_trailing_whitespace = true insert_final_newline = true -[*.{csproj,props,targets}] -indent_style = space -indent_size = 2 - -[*.yml] -indent_style = space -indent_size = 2 - [*.{cs,vb}] tab_width = 4 indent_size = 4 -end_of_line = lf #### Naming styles #### @@ -70,11 +61,6 @@ dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case - dotnet_code_quality_unused_parameters = all:suggestion dotnet_sort_system_directives_first = true @@ -87,7 +73,7 @@ dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion dotnet_style_prefer_auto_properties = true:silent dotnet_style_object_initializer = true:suggestion -dotnet_style_prefer_collection_expression = true:suggestion +dotnet_style_prefer_collection_expression = false:suggestion [*.cs] # ref: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/csharp-formatting-options diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 921dda23d..3b7f7d542 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -23,7 +23,7 @@ body: render: csharp placeholder: | // example (put your own code here) - IEnumerable results = quotes.GetEma(14); + IReadOnlyList results = quotes.GetEma(14); validations: required: false - type: textarea diff --git a/.github/workflows/test-examples.yml b/.github/workflows/test-examples.yml index 7b63a2fd1..4b81db50c 100644 --- a/.github/workflows/test-examples.yml +++ b/.github/workflows/test-examples.yml @@ -2,12 +2,12 @@ name: Examples on: push: - branches: ["main"] + branches: ["main","v3"] paths: - docs/examples/** pull_request: - branches: ["main"] + branches: ["main","v3"] paths: - docs/examples/** - ".github/workflows/test-examples.yml" diff --git a/.github/workflows/test-indicators.yml b/.github/workflows/test-indicators.yml index 72d8ef4d6..a5a0932a2 100644 --- a/.github/workflows/test-indicators.yml +++ b/.github/workflows/test-indicators.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [windows-latest, ubuntu-latest, macos-latest] - dotnet-version: ["2.0.x", "6.x", "8.x"] + dotnet-version: ["6.x", "8.x"] env: diff --git a/.github/workflows/test-performance.yml b/.github/workflows/test-performance.yml index 2335e0d63..6682f6b06 100644 --- a/.github/workflows/test-performance.yml +++ b/.github/workflows/test-performance.yml @@ -42,12 +42,29 @@ jobs: --configuration Release --property:ContinuousIntegrationBuild=true - - name: Benchmark indicators + - name: Test performance working-directory: tests/performance run: dotnet run -c Release + - name: Save test results + uses: actions/upload-artifact@v4 + with: + name: test-summaries + path: tests/performance/BenchmarkDotNet.Artifacts/results + - name: Publish summary working-directory: tests/performance/BenchmarkDotNet.Artifacts/results run: | echo "### Package version ${{ steps.gitversion.outputs.fullSemVer }}" >> $GITHUB_STEP_SUMMARY - cat Tests.Performance.IndicatorsStatic-report-github.md >> $GITHUB_STEP_SUMMARY + + echo "## Series indicators" >> $GITHUB_STEP_SUMMARY + cat Performance.SeriesIndicators-report-github.md >> $GITHUB_STEP_SUMMARY + + echo "## Stream indicators (with Quote caching)" >> $GITHUB_STEP_SUMMARY + cat Performance.StreamIndicators-report-github.md >> $GITHUB_STEP_SUMMARY + + echo "## Incremental indicators (with buffer)" >> $GITHUB_STEP_SUMMARY + cat Performance.Incrementals-report-github.md >> $GITHUB_STEP_SUMMARY + + echo "## Utilities" >> $GITHUB_STEP_SUMMARY + cat Performance.Utility-report-github.md >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/test-website-a11y.yml b/.github/workflows/test-website-a11y.yml index d7164fb04..a92abccc3 100644 --- a/.github/workflows/test-website-a11y.yml +++ b/.github/workflows/test-website-a11y.yml @@ -1,8 +1,10 @@ name: Website on: + push: + branches: ["v3"] pull_request: - branches: [main] + branches: ["main"] paths: - 'docs/**' - ".github/workflows/test-website-a11y.yml" diff --git a/.github/workflows/test-website-links.yml b/.github/workflows/test-website-links.yml index 4d9136ec2..19cddb540 100644 --- a/.github/workflows/test-website-links.yml +++ b/.github/workflows/test-website-links.yml @@ -1,8 +1,10 @@ name: Website on: + push: + branches: ["v3"] pull_request: - branches: [main] + branches: ["main"] paths: - 'docs/**' - ".github/workflows/test-website-links.yml" diff --git a/.gitignore b/.gitignore index 7a97cd5dc..a517802a1 100644 --- a/.gitignore +++ b/.gitignore @@ -111,12 +111,13 @@ $tf/ # Guidance Automation Toolkit *.gpState -# ReSharper is a .NET coding add-in +# ReSharper IDE extension _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user +*.DotSettings -# JustCode is a .NET coding add-in +# JustCode IDE extension .JustCode # TeamCity is a build add-in @@ -218,7 +219,7 @@ ClientBin/ *.publishsettings orleans.codegen.cs -# Including strong name files can present a security risk +# Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk @@ -314,7 +315,7 @@ __pycache__/ # OpenCover UI analysis results OpenCover/ -# Azure Stream Analytics local run output +# Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log @@ -323,11 +324,11 @@ ASALocalRun/ # NVidia Nsight GPU debugger configuration file *.nvuser -# MFractors (Xamarin productivity tool) working folder +# MFractors (Xamarin productivity tool) working folder .mfractor/ # Jekyll site _site/ # zip artifacts -.DS_Store \ No newline at end of file +.DS_Store diff --git a/Stock.Indicators.sln b/Stock.Indicators.sln index 42164b6bc..8d241723d 100644 --- a/Stock.Indicators.sln +++ b/Stock.Indicators.sln @@ -19,13 +19,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Other", "tests\other\ 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}") = "Observe.Streaming", "tests\observe\Observe.Streaming.csproj", "{14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}" +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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,6 +53,10 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/_data/aliases.yml b/docs/_data/aliases.yml index 9950409f0..5d1bf4b87 100644 --- a/docs/_data/aliases.yml +++ b/docs/_data/aliases.yml @@ -59,23 +59,23 @@ type: price-characteristic - title: HL2 - permalink: /indicators/BasicQuote/ + permalink: /indicators/quotepart/ type: price-transform - title: HLC3 - permalink: /indicators/BasicQuote/ + permalink: /indicators/quotepart/ type: price-transform - title: OC2 - permalink: /indicators/BasicQuote/ + permalink: /indicators/quotepart/ type: price-transform - title: OHL3 - permalink: /indicators/BasicQuote/ + permalink: /indicators/quotepart/ type: price-transform - title: OHLC4 - permalink: /indicators/BasicQuote/ + permalink: /indicators/quotepart/ type: price-transform - title: Price Channels diff --git a/docs/_includes/candle-properties.md b/docs/_includes/candle-properties.md index b601afa7f..0d514b063 100644 --- a/docs/_includes/candle-properties.md +++ b/docs/_includes/candle-properties.md @@ -2,7 +2,7 @@ | name | type | notes | -- |-- |-- -| `Date` | DateTime | Date +| `Timestamp` | DateTime | Close date | `Open` | decimal | Open price | `High` | decimal | High price | `Low` | decimal | Low price diff --git a/docs/_includes/candle-result.md b/docs/_includes/candle-result.md index 920aec1c3..efb0c4081 100644 --- a/docs/_includes/candle-result.md +++ b/docs/_includes/candle-result.md @@ -1,6 +1,6 @@ ### CandleResult -**`Date`** _`DateTime`_ - Date +**`Timestamp`** _`DateTime`_ - Time of last trade in candle period **`Price`** _`decimal`_ - Price of the most relevant OHLC candle element when a signal is present diff --git a/docs/_indicators/Adl.md b/docs/_indicators/Adl.md index 48acef9f0..eeb10b3e5 100644 --- a/docs/_indicators/Adl.md +++ b/docs/_indicators/Adl.md @@ -16,19 +16,11 @@ Created by Marc Chaikin, the [Accumulation/Distribution Line/Index](https://en.w ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetAdl(); - -// usage with optional overlay SMA of ADL (shown above) -IEnumerable results = - quotes.GetAdl(smaPeriods); ``` -## Parameters - -**`smaPeriods`** _`int`_ - Optional. Number of periods (`N`) in the moving average of ADL. Must be greater than 0, if specified. - -### Historical quotes requirements +## Historical quotes requirements You must have at least two historical quotes to cover the warmup periods; however, since this is a trendline, more is recommended. @@ -37,7 +29,7 @@ You must have at least two historical quotes to cover the warmup periods; howeve ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -46,7 +38,7 @@ IEnumerable ### AdlResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`MoneyFlowMultiplier`** _`double`_ - Money Flow Multiplier @@ -54,8 +46,6 @@ IEnumerable **`Adl`** _`double`_ - Accumulation Distribution Line (ADL) -**`AdlSma`** _`double`_ - Moving average (SMA) of ADL based on `smaPeriods` periods, if specified - > 🚩 **Warning**: absolute values in ADL and MFV are somewhat meaningless. Use with caution. ### Utilities diff --git a/docs/_indicators/Adx.md b/docs/_indicators/Adx.md index afcfe2ece..feb4e5612 100644 --- a/docs/_indicators/Adx.md +++ b/docs/_indicators/Adx.md @@ -16,7 +16,7 @@ Created by J. Welles Wilder, the Directional Movement Index (DMI) and [Average D ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetAdx(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `2×N+100` periods of `quotes` to cover the [warmup and c ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -45,7 +45,7 @@ IEnumerable ### AdxResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Pdi`** _`double`_ - Plus Directional Index (+DI) diff --git a/docs/_indicators/Alligator.md b/docs/_indicators/Alligator.md index c3433c43f..7c367f0d1 100644 --- a/docs/_indicators/Alligator.md +++ b/docs/_indicators/Alligator.md @@ -16,7 +16,7 @@ Created by Bill Williams, Alligator is a depiction of three smoothed moving aver ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetAlligator(jawPeriods,jawOffset,teethPeriods,teethOffset,lipsPeriods,lipsOffset); ``` @@ -43,7 +43,7 @@ You must have at least `JP+JO+100` periods of `quotes` to cover the [warmup and ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -55,7 +55,7 @@ IEnumerable ### AlligatorResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Jaw`** _`double`_ - Alligator's Jaw diff --git a/docs/_indicators/Alma.md b/docs/_indicators/Alma.md index d378287cf..c0624deea 100644 --- a/docs/_indicators/Alma.md +++ b/docs/_indicators/Alma.md @@ -16,7 +16,7 @@ Created by Arnaud Legoux and Dimitrios Kouzis-Loukas, [ALMA]({{site.github.repos ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetAlma(lookbackPeriods, offset, sigma); ``` @@ -37,7 +37,7 @@ You must have at least `N` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -47,7 +47,7 @@ IEnumerable ### AlmaResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Alma`** _`double`_ - Arnaud Legoux Moving Average diff --git a/docs/_indicators/Aroon.md b/docs/_indicators/Aroon.md index 14d8b3fb7..6d4b43417 100644 --- a/docs/_indicators/Aroon.md +++ b/docs/_indicators/Aroon.md @@ -16,7 +16,7 @@ Created by Tushar Chande, [Aroon](https://school.stockcharts.com/doku.php?id=tec ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetAroon(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -43,7 +43,7 @@ IEnumerable ### AroonResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`AroonUp`** _`double`_ - Based on last High price diff --git a/docs/_indicators/Atr.md b/docs/_indicators/Atr.md index 7b851348e..c8d2bbfc5 100644 --- a/docs/_indicators/Atr.md +++ b/docs/_indicators/Atr.md @@ -16,15 +16,15 @@ Created by J. Welles Wilder, True Range and [Average True Range](https://en.wiki ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetAtr(lookbackPeriods); // ATR with custom moving average -IEnumerable results = +IReadOnlyList results = quotes.GetTr().GetSmma(lookbackPeriods); // raw True Range (TR) only -IEnumerable results = +IReadOnlyList results = quote.GetTr(); ``` @@ -41,7 +41,7 @@ You must have at least `N+100` periods of `quotes` to cover the [warmup and conv ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -53,7 +53,7 @@ IEnumerable ### AtrResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Tr`** _`double`_ - True Range for current period diff --git a/docs/_indicators/AtrStop.md b/docs/_indicators/AtrStop.md index 742eba953..c1db90504 100644 --- a/docs/_indicators/AtrStop.md +++ b/docs/_indicators/AtrStop.md @@ -16,7 +16,7 @@ Created by Welles Wilder, the ATR Trailing Stop indicator attempts to determine ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetAtrStop(lookbackPeriods, multiplier, endType); ``` @@ -43,7 +43,7 @@ You must have at least `N+100` periods of `quotes` to cover the [warmup and conv ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -55,13 +55,15 @@ IEnumerable ### AtrStopResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` -**`AtrStop`** _`decimal`_ - ATR Trailing Stop line contains both Upper and Lower segments +**`AtrStop`** _`double`_ - ATR Trailing Stop line contains both Upper and Lower segments -**`BuyStop`** _`decimal`_ - Upper band only (green) +**`BuyStop`** _`double`_ - Upper band only (green) -**`SellStop`** _`decimal`_ - Lower band only (red) +**`SellStop`** _`double`_ - Lower band only (red) + +**`Atr`** _`double`_ - Average True Range `BuyStop` and `SellStop` values are provided to differentiate buy vs sell stop lines and to clearly demark trend reversal. `AtrStop` is the contiguous combination of both upper and lower line data. diff --git a/docs/_indicators/Awesome.md b/docs/_indicators/Awesome.md index cbade963f..3963f5d26 100644 --- a/docs/_indicators/Awesome.md +++ b/docs/_indicators/Awesome.md @@ -16,7 +16,7 @@ Created by Bill Williams, the Awesome Oscillator (aka Super AO) is a measure of ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetAwesome(fastPeriods, slowPeriods); ``` @@ -35,7 +35,7 @@ You must have at least `S` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -45,7 +45,7 @@ IEnumerable ### AwesomeResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Oscillator`** _`double`_ - Awesome Oscillator diff --git a/docs/_indicators/Beta.md b/docs/_indicators/Beta.md index bf47c3e59..b41d716cb 100644 --- a/docs/_indicators/Beta.md +++ b/docs/_indicators/Beta.md @@ -16,13 +16,13 @@ layout: indicator ```csharp // C# usage syntax -IEnumerable results = quotesEval +IReadOnlyList results = quotesEval .GetBeta(quotesMarket, lookbackPeriods, type); ``` ## Parameters -**`quotesMarket`** _`IEnumerable`_ - [Historical quotes]({{site.baseurl}}/guide/#historical-quotes) market data should be at any consistent frequency (day, hour, minute, etc). This `market` quotes will be used to establish the baseline. +**`quotesMarket`** _`IReadOnlyList`_ - [Historical quotes]({{site.baseurl}}/guide/#historical-quotes) market data should be at any consistent frequency (day, hour, minute, etc). This `market` quotes will be used to establish the baseline. **`lookbackPeriods`** _`int`_ - Number of periods (`N`) in the lookback window. Must be greater than 0 to calculate; however we suggest a larger period for statistically appropriate sample size and especially when using Beta +/-. @@ -51,7 +51,7 @@ You must have at least `N` periods of `quotesEval` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -61,7 +61,7 @@ IEnumerable ### BetaResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Beta`** _`double`_ - Beta coefficient based diff --git a/docs/_indicators/BollingerBands.md b/docs/_indicators/BollingerBands.md index 94837ad65..4052c8793 100644 --- a/docs/_indicators/BollingerBands.md +++ b/docs/_indicators/BollingerBands.md @@ -16,7 +16,7 @@ Created by John Bollinger, [Bollinger Bands](https://en.wikipedia.org/wiki/Bolli ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetBollingerBands(lookbackPeriods, standardDeviations); ``` @@ -35,7 +35,7 @@ You must have at least `N` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -45,7 +45,7 @@ IEnumerable ### BollingerBandsResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Sma`** _`double`_ - Simple moving average (SMA) of price (center line) diff --git a/docs/_indicators/Bop.md b/docs/_indicators/Bop.md index 6fa6f02c9..39b193bc9 100644 --- a/docs/_indicators/Bop.md +++ b/docs/_indicators/Bop.md @@ -16,7 +16,7 @@ Created by Igor Levshin, the [Balance of Power](https://school.stockcharts.com/d ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetBop(smoothPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -43,7 +43,7 @@ IEnumerable ### BopResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Bop`** _`double`_ - Balance of Power diff --git a/docs/_indicators/Cci.md b/docs/_indicators/Cci.md index 9b9c27cb6..d8eb341aa 100644 --- a/docs/_indicators/Cci.md +++ b/docs/_indicators/Cci.md @@ -16,7 +16,7 @@ Created by Donald Lambert, the [Commodity Channel Index](https://en.wikipedia.or ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetCci(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N+1` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -43,7 +43,7 @@ IEnumerable ### CciResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Cci`** _`double`_ - Commodity Channel Index diff --git a/docs/_indicators/ChaikinOsc.md b/docs/_indicators/ChaikinOsc.md index 7bc660857..398aea5dd 100644 --- a/docs/_indicators/ChaikinOsc.md +++ b/docs/_indicators/ChaikinOsc.md @@ -16,7 +16,7 @@ Created by Marc Chaikin, the [Chaikin Oscillator](https://en.wikipedia.org/wiki/ ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetChaikinOsc(fastPeriods, slowPeriods); ``` @@ -35,7 +35,7 @@ You must have at least `2×S` or `S+100` periods of `quotes`, whichever is more, ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -47,7 +47,7 @@ IEnumerable ### ChaikinOscResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`MoneyFlowMultiplier`** _`double`_ - Money Flow Multiplier diff --git a/docs/_indicators/Chandelier.md b/docs/_indicators/Chandelier.md index bbcaec658..e257ed37f 100644 --- a/docs/_indicators/Chandelier.md +++ b/docs/_indicators/Chandelier.md @@ -17,7 +17,7 @@ Created by Charles Le Beau, the [Chandelier Exit](https://school.stockcharts.com ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetChandelier(lookbackPeriods, multiplier, type); ``` @@ -44,7 +44,7 @@ You must have at least `N+1` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -54,7 +54,7 @@ IEnumerable ### ChandelierResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`ChandelierExit`** _`double`_ - Exit line diff --git a/docs/_indicators/Chop.md b/docs/_indicators/Chop.md index 491f1bfb0..8604480d1 100644 --- a/docs/_indicators/Chop.md +++ b/docs/_indicators/Chop.md @@ -15,7 +15,7 @@ Created by E.W. Dreiss, the Choppiness Index measures the trendiness or choppine ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetChop(lookbackPeriods); ``` @@ -32,7 +32,7 @@ You must have at least `N+1` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -42,7 +42,7 @@ IEnumerable ### ChopResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Chop`** _`double`_ - Choppiness Index diff --git a/docs/_indicators/Cmf.md b/docs/_indicators/Cmf.md index 8a254c0b4..b1bd1abbf 100644 --- a/docs/_indicators/Cmf.md +++ b/docs/_indicators/Cmf.md @@ -16,7 +16,7 @@ Created by Marc Chaikin, [Chaikin Money Flow](https://en.wikipedia.org/wiki/Chai ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetCmf(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N+1` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -43,7 +43,7 @@ IEnumerable ### CmfResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`MoneyFlowMultiplier`** _`double`_ - Money Flow Multiplier diff --git a/docs/_indicators/Cmo.md b/docs/_indicators/Cmo.md index a57e08b48..40eb2a605 100644 --- a/docs/_indicators/Cmo.md +++ b/docs/_indicators/Cmo.md @@ -16,7 +16,7 @@ Created by Tushar Chande, the [Chande Momentum Oscillator](https://www.investope ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetCmo(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N+1` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -43,7 +43,7 @@ IEnumerable ### CmoResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Cmo`** _`double`_ - Chande Momentum Oscillator diff --git a/docs/_indicators/ConnorsRsi.md b/docs/_indicators/ConnorsRsi.md index df6a07227..746d5fdb5 100644 --- a/docs/_indicators/ConnorsRsi.md +++ b/docs/_indicators/ConnorsRsi.md @@ -16,7 +16,7 @@ Created by Laurence Connors, the [ConnorsRSI](https://alvarezquanttrading.com/wp ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetConnorsRsi(rsiPeriods, streakPeriods, rankPeriods); ``` @@ -37,7 +37,7 @@ IEnumerable results = ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -49,7 +49,7 @@ IEnumerable ### ConnorsRsiResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Rsi`** _`double`_ - `RSI(R)` of the price. diff --git a/docs/_indicators/Correlation.md b/docs/_indicators/Correlation.md index 7f0a307a3..740374667 100644 --- a/docs/_indicators/Correlation.md +++ b/docs/_indicators/Correlation.md @@ -16,13 +16,13 @@ Created by Karl Pearson, the [Correlation Coefficient](https://en.wikipedia.org/ ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotesA.GetCorrelation(quotesB, lookbackPeriods); ``` ## Parameters -**`quotesB`** _`IEnumerable`_ - [Historical quotes]({{site.baseurl}}/guide/#historical-quotes) (B) must have at least the same matching date elements of `quotesA`. +**`quotesB`** _`IReadOnlyList`_ - [Historical quotes]({{site.baseurl}}/guide/#historical-quotes) (B) must have at least the same matching date elements of `quotesA`. **`lookbackPeriods`** _`int`_ - Number of periods (`N`) in the lookback period. Must be greater than 0 to calculate; however we suggest a larger period for statistically appropriate sample size. @@ -30,12 +30,12 @@ IEnumerable results = You must have at least `N` periods for both versions of `quotes` to cover the warmup periods. Mismatch histories will produce a `InvalidQuotesException`. Historical price quotes should have a consistent frequency (day, hour, minute, etc). -`quotesA` is an `IEnumerable` collection of historical price quotes. It should have a consistent frequency (day, hour, minute, etc). See [the Guide]({{site.baseurl}}/guide/#historical-quotes) for more information. +`quotesA` is an `IReadOnlyList` collection of historical price quotes. It should have a consistent frequency (day, hour, minute, etc). See [the Guide]({{site.baseurl}}/guide/#historical-quotes) for more information. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -45,7 +45,7 @@ IEnumerable ### CorrResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`VarianceA`** _`double`_ - Variance of A diff --git a/docs/_indicators/Dema.md b/docs/_indicators/Dema.md index 6cd007d5e..7c40c943e 100644 --- a/docs/_indicators/Dema.md +++ b/docs/_indicators/Dema.md @@ -18,7 +18,7 @@ Created by Patrick G. Mulloy, the [Double exponential moving average](https://en ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetDema(lookbackPeriods); ``` @@ -35,7 +35,7 @@ You must have at least `3×N` or `2×N+100` periods of `quotes`, whichever is mo ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -47,7 +47,7 @@ IEnumerable ### DemaResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Dema`** _`double`_ - Double exponential moving average diff --git a/docs/_indicators/Doji.md b/docs/_indicators/Doji.md index 42c32d134..6b7af9d17 100644 --- a/docs/_indicators/Doji.md +++ b/docs/_indicators/Doji.md @@ -16,7 +16,7 @@ type: candlestick-pattern ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetDoji(maxPriceChangePercent); ``` @@ -33,7 +33,7 @@ You must have at least one historical quote; however, more is typically provided ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. diff --git a/docs/_indicators/Donchian.md b/docs/_indicators/Donchian.md index 8f1c1d9a8..64de9aa65 100644 --- a/docs/_indicators/Donchian.md +++ b/docs/_indicators/Donchian.md @@ -17,7 +17,7 @@ Created by Richard Donchian, [Donchian Channels](https://en.wikipedia.org/wiki/D ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetDonchian(lookbackPeriods); ``` @@ -34,7 +34,7 @@ You must have at least `N+1` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -44,7 +44,7 @@ IEnumerable ### DonchianResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`UpperBand`** _`decimal`_ - Upper line is the highest High over `N` periods diff --git a/docs/_indicators/Dpo.md b/docs/_indicators/Dpo.md index 17a346fdb..b843e96f3 100644 --- a/docs/_indicators/Dpo.md +++ b/docs/_indicators/Dpo.md @@ -16,7 +16,7 @@ layout: indicator ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetDpo(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N` historical quotes to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -43,7 +43,7 @@ IEnumerable ### DpoResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Sma`** _`double`_ - Simple moving average offset by `N/2+1` periods diff --git a/docs/_indicators/Dynamic.md b/docs/_indicators/Dynamic.md index 05a8e9fdc..c3c4b9dfd 100644 --- a/docs/_indicators/Dynamic.md +++ b/docs/_indicators/Dynamic.md @@ -16,7 +16,7 @@ Created by John R. McGinley, the [McGinley Dynamic](https://www.investopedia.com ```csharp // C# usage syntax (with Close price) -IEnumerable results = +IReadOnlyList results = quotes.GetDynamic(lookbackPeriods, kFactor); ``` @@ -41,7 +41,7 @@ You must have at least `2` periods of `quotes`, to cover the [warmup and converg ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -53,7 +53,7 @@ IEnumerable ### DynamicResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Dynamic`** _`double`_ - McGinley Dynamic diff --git a/docs/_indicators/ElderRay.md b/docs/_indicators/ElderRay.md index a3fadf142..f32c5b2f9 100644 --- a/docs/_indicators/ElderRay.md +++ b/docs/_indicators/ElderRay.md @@ -16,7 +16,7 @@ Created by Alexander Elder, the [Elder-ray Index](https://www.investopedia.com/t ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetElderRay(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `2×N` or `N+100` periods of `quotes`, whichever is more, ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -45,7 +45,7 @@ IEnumerable ### ElderRayResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Ema`** _`double`_ - Exponential moving average diff --git a/docs/_indicators/Ema.md b/docs/_indicators/Ema.md index 56c29d04a..3c041a4f9 100644 --- a/docs/_indicators/Ema.md +++ b/docs/_indicators/Ema.md @@ -16,7 +16,7 @@ layout: indicator ```csharp // C# usage syntax (with Close price) -IEnumerable results = +IReadOnlyList results = quotes.GetEma(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `2×N` or `N+100` periods of `quotes`, whichever is more, ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -45,7 +45,7 @@ IEnumerable ### EmaResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Ema`** _`double`_ - Exponential moving average diff --git a/docs/_indicators/Epma.md b/docs/_indicators/Epma.md index f37865059..26df4c8c9 100644 --- a/docs/_indicators/Epma.md +++ b/docs/_indicators/Epma.md @@ -16,7 +16,7 @@ Endpoint Moving Average (EPMA), also known as Least Squares Moving Average (LSMA ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetEpma(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -43,7 +43,7 @@ IEnumerable ### EpmaResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Epma`** _`double`_ - Endpoint moving average diff --git a/docs/_indicators/Fcb.md b/docs/_indicators/Fcb.md index f0ccdc7f6..71caada0b 100644 --- a/docs/_indicators/Fcb.md +++ b/docs/_indicators/Fcb.md @@ -16,7 +16,7 @@ Created by Edward William Dreiss, Fractal Chaos Bands outline high and low price ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetFcb(windowSpan); ``` @@ -35,7 +35,7 @@ You must have at least `2×S+1` periods of `quotes` to cover the warmup periods; ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -45,7 +45,7 @@ IEnumerable ### FcbResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`UpperBand`** _`decimal`_ - FCB upper band diff --git a/docs/_indicators/FisherTransform.md b/docs/_indicators/FisherTransform.md index 99a71ad22..294eb7c01 100644 --- a/docs/_indicators/FisherTransform.md +++ b/docs/_indicators/FisherTransform.md @@ -16,7 +16,7 @@ Created by John Ehlers, the [Fisher Transform](https://www.investopedia.com/term ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetFisherTransform(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N` periods of `quotes` to cover the [warmup and converge ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -44,7 +44,7 @@ IEnumerable ### FisherTransformResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Fisher`** _`double`_ - Fisher Transform diff --git a/docs/_indicators/ForceIndex.md b/docs/_indicators/ForceIndex.md index 37535c53f..c7df2c284 100644 --- a/docs/_indicators/ForceIndex.md +++ b/docs/_indicators/ForceIndex.md @@ -16,7 +16,7 @@ Created by Alexander Elder, the [Force Index](https://en.wikipedia.org/wiki/Forc ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetForceIndex(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N+100` for `2×N` periods of `quotes`, whichever is more ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -45,7 +45,7 @@ IEnumerable ### ForceIndexResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`ForceIndex`** _`double`_ - Force Index diff --git a/docs/_indicators/Fractal.md b/docs/_indicators/Fractal.md index 78087ebf0..b8f4a8cfe 100644 --- a/docs/_indicators/Fractal.md +++ b/docs/_indicators/Fractal.md @@ -16,7 +16,7 @@ Created by Larry Williams, [Fractal](https://www.investopedia.com/terms/f/fracta ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetFractal(windowSpan); ``` @@ -43,7 +43,7 @@ You must have at least `2×S+1` periods of `quotes` to cover the warmup periods; ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -55,7 +55,7 @@ IEnumerable ### FractalResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`FractalBear`** _`decimal`_ - Value indicates a **high** point; otherwise `null` is returned. diff --git a/docs/_indicators/Gator.md b/docs/_indicators/Gator.md index 13e72fe9c..0f19d6b48 100644 --- a/docs/_indicators/Gator.md +++ b/docs/_indicators/Gator.md @@ -16,11 +16,11 @@ Created by Bill Williams, the Gator Oscillator is an expanded oscillator view of ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetGator(); // with custom Alligator configuration -IEnumerable results = quotes +IReadOnlyList results = quotes .GetAlligator([see Alligator docs]) .GetGator(); ``` @@ -34,7 +34,7 @@ If using default settings, you must have at least 121 periods of `quotes` to cov ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -46,7 +46,7 @@ IEnumerable ### GatorResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Upper`** _`double`_ - Absolute value of Alligator `Jaw-Teeth` diff --git a/docs/_indicators/HeikinAshi.md b/docs/_indicators/HeikinAshi.md index 2d7a1e171..906056792 100644 --- a/docs/_indicators/HeikinAshi.md +++ b/docs/_indicators/HeikinAshi.md @@ -16,7 +16,7 @@ Created by Munehisa Homma, [Heikin-Ashi](https://en.wikipedia.org/wiki/Candlesti ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetHeikinAshi(); ``` @@ -29,7 +29,7 @@ You must have at least two periods of `quotes` to cover the warmup periods; howe ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -39,7 +39,7 @@ IEnumerable ### HeikinAshiResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Open`** _`decimal`_ - Modified open price @@ -58,7 +58,7 @@ IEnumerable - .ToQuotes() to convert to a `Quote` collection. Example: ```csharp - IEnumerable results = quotes + IReadOnlyList results = quotes .GetHeikinAshi() .ToQuotes(); ``` diff --git a/docs/_indicators/Hma.md b/docs/_indicators/Hma.md index bde67341b..6dc3e8cf2 100644 --- a/docs/_indicators/Hma.md +++ b/docs/_indicators/Hma.md @@ -16,7 +16,7 @@ Created by Alan Hull, the [Hull Moving Average](https://alanhull.com/hull-moving ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetHma(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N+(integer of SQRT(N))-1` periods of `quotes` to cover t ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -43,7 +43,7 @@ IEnumerable ### HmaResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Hma`** _`double`_ - Hull moving average diff --git a/docs/_indicators/HtTrendline.md b/docs/_indicators/HtTrendline.md index e27e52baa..12e7586f5 100644 --- a/docs/_indicators/HtTrendline.md +++ b/docs/_indicators/HtTrendline.md @@ -16,7 +16,7 @@ Created by John Ehlers, the Hilbert Transform Instantaneous Trendline is a 5-per ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetHtTrendline(); ``` @@ -29,7 +29,7 @@ You must have at least `100` periods of `quotes` to cover the [warmup and conver ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -42,7 +42,7 @@ IEnumerable ### HtlResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`DcPeriods`** _`int`_ - Dominant cycle periods (smoothed) diff --git a/docs/_indicators/Hurst.md b/docs/_indicators/Hurst.md index 0595ecca5..6ca508984 100644 --- a/docs/_indicators/Hurst.md +++ b/docs/_indicators/Hurst.md @@ -16,7 +16,7 @@ The [Hurst Exponent](https://en.wikipedia.org/wiki/Hurst_exponent) (`H`) is part ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetHurst(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N+1` periods of `quotes` to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -43,7 +43,7 @@ IEnumerable ### HurstResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`HurstExponent`** _`double`_ - Hurst Exponent (`H`) diff --git a/docs/_indicators/Ichimoku.md b/docs/_indicators/Ichimoku.md index ea4ce2618..fa4beb8f9 100644 --- a/docs/_indicators/Ichimoku.md +++ b/docs/_indicators/Ichimoku.md @@ -16,15 +16,15 @@ Created by Goichi Hosoda (細田悟一, Hosoda Goichi), [Ichimoku Cloud](https:/ ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetIchimoku(tenkanPeriods, kijunPeriods, senkouBPeriods); // usage with custom offset -IEnumerable results = +IReadOnlyList results = quotes.GetIchimoku(tenkanPeriods, kijunPeriods, senkouBPeriods, offsetPeriods); // usage with different custom offsets -IEnumerable results = +IReadOnlyList results = quotes.GetIchimoku(tenkanPeriods, kijunPeriods, senkouBPeriods, senkouOffset, chikouOffset); ``` @@ -53,7 +53,7 @@ You must have at least the greater of `T`,`K`, `S`, and offset periods for `quot ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -63,7 +63,7 @@ IEnumerable ### IchimokuResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`TenkanSen`** _`decimal`_ - Conversion / signal line diff --git a/docs/_indicators/Kama.md b/docs/_indicators/Kama.md index 24b53c843..d07ddd23a 100644 --- a/docs/_indicators/Kama.md +++ b/docs/_indicators/Kama.md @@ -16,7 +16,7 @@ Created by Perry Kaufman, [KAMA](https://school.stockcharts.com/doku.php?id=tech ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetKama(erPeriods, fastPeriods, slowPeriods); ``` @@ -37,7 +37,7 @@ You must have at least `6×E` or `E+100` periods of `quotes`, whichever is more, ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -49,7 +49,7 @@ IEnumerable ### KamaResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`ER`** _`double`_ - Efficiency Ratio is the fractal efficiency of price changes diff --git a/docs/_indicators/Keltner.md b/docs/_indicators/Keltner.md index d56ed4ed5..35ee9836a 100644 --- a/docs/_indicators/Keltner.md +++ b/docs/_indicators/Keltner.md @@ -16,7 +16,7 @@ Created by Chester W. Keltner, [Keltner Channels](https://en.wikipedia.org/wiki/ ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetKeltner(emaPeriods, multiplier, atrPeriods); ``` @@ -37,7 +37,7 @@ You must have at least `2×N` or `N+100` periods of `quotes`, whichever is more, ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -49,7 +49,7 @@ IEnumerable ### KeltnerResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`UpperBand`** _`double`_ - Upper band of Keltner Channel diff --git a/docs/_indicators/Kvo.md b/docs/_indicators/Kvo.md index 257b741ae..03f3a52ba 100644 --- a/docs/_indicators/Kvo.md +++ b/docs/_indicators/Kvo.md @@ -16,7 +16,7 @@ Created by Stephen Klinger, the [Klinger Volume Oscillator](https://www.investop ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetKvo(shortPeriods, longPeriods, signalPeriods); ``` @@ -37,7 +37,7 @@ You must have at least `L+100` periods of `quotes` to cover the [warmup and conv ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -49,7 +49,7 @@ IEnumerable ### KvoResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Oscillator`** _`double`_ - Klinger Oscillator diff --git a/docs/_indicators/MaEnvelopes.md b/docs/_indicators/MaEnvelopes.md index 88ba0c220..d7b76248a 100644 --- a/docs/_indicators/MaEnvelopes.md +++ b/docs/_indicators/MaEnvelopes.md @@ -16,7 +16,7 @@ layout: indicator ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetMaEnvelopes(lookbackPeriods, percentOffset, movingAverageType); ``` @@ -61,7 +61,7 @@ These are the supported moving average types: ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -73,7 +73,7 @@ IEnumerable ### MaEnvelopeResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Centerline`** _`double`_ - Moving average diff --git a/docs/_indicators/Macd.md b/docs/_indicators/Macd.md index 72014585a..6428d0d97 100644 --- a/docs/_indicators/Macd.md +++ b/docs/_indicators/Macd.md @@ -16,7 +16,7 @@ Created by Gerald Appel, [MACD](https://en.wikipedia.org/wiki/MACD) is a simple ```csharp // C# usage syntax (with Close price) -IEnumerable results = +IReadOnlyList results = quotes.GetMacd(fastPeriods, slowPeriods, signalPeriods); ``` @@ -37,7 +37,7 @@ You must have at least `2×(S+P)` or `S+P+100` worth of `quotes`, whichever is m ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -49,7 +49,7 @@ IEnumerable ### MacdResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Macd`** _`double`_ - The MACD line is the difference between slow and fast moving averages (`MACD = FastEma - SlowEma`) diff --git a/docs/_indicators/Mama.md b/docs/_indicators/Mama.md index ce985f3e5..1e8e6697f 100644 --- a/docs/_indicators/Mama.md +++ b/docs/_indicators/Mama.md @@ -16,7 +16,7 @@ Created by John Ehlers, the [MAMA](https://mesasoftware.com/papers/MAMA.pdf) ind ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetMama(fastLimit, slowLimit); ``` @@ -35,7 +35,7 @@ You must have at least `50` periods of `quotes` to cover the [warmup and converg ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -47,7 +47,7 @@ IEnumerable ### MamaResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Mama`** _`decimal`_ - MESA adaptive moving average (MAMA) diff --git a/docs/_indicators/Marubozu.md b/docs/_indicators/Marubozu.md index 75abe1d69..d29b57109 100644 --- a/docs/_indicators/Marubozu.md +++ b/docs/_indicators/Marubozu.md @@ -16,7 +16,7 @@ type: candlestick-pattern ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetMarubozu(minBodyPercent); ``` @@ -33,7 +33,7 @@ You must have at least one historical quote; however, more is typically provided ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. diff --git a/docs/_indicators/Mfi.md b/docs/_indicators/Mfi.md index 16f58a2b0..01168929e 100644 --- a/docs/_indicators/Mfi.md +++ b/docs/_indicators/Mfi.md @@ -16,7 +16,7 @@ Created by Quong and Soudack, the [Money Flow Index](https://en.wikipedia.org/wi ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetMfi(lookbackPeriods); ``` @@ -33,7 +33,7 @@ You must have at least `N+1` historical quotes to cover the warmup periods. ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -43,7 +43,7 @@ IEnumerable ### MfiResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Mfi`** _`decimal`_ - Money Flow Index diff --git a/docs/_indicators/Obv.md b/docs/_indicators/Obv.md index 4da972f7d..f53f24094 100644 --- a/docs/_indicators/Obv.md +++ b/docs/_indicators/Obv.md @@ -16,19 +16,11 @@ Popularized by Joseph Granville, [On-balance Volume](https://en.wikipedia.org/wi ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetObv(); - -// usage with optional overlay SMA of OBV (shown above) -IEnumerable results = - quotes.GetObv(smaPeriods); ``` -## Parameters - -**`smaPeriods`** _`int`_ - Optional. Number of periods (`N`) in the moving average of OBV. Must be greater than 0, if specified. - -### Historical quotes requirements +## Historical quotes requirements You must have at least two historical quotes to cover the warmup periods; however, since this is a trendline, more is recommended. @@ -37,22 +29,20 @@ You must have at least two historical quotes to cover the warmup periods; howeve ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. - It always returns the same number of elements as there are in the historical quotes. - It does not return a single incremental indicator value. -- The first period OBV will have `0` value since there's not enough data to calculate. +- The first period OBV will have a `0` value since there's not enough data to calculate. ### ObvResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Obv`** _`double`_ - On-balance Volume -**`ObvSma`** _`double`_ - Moving average (SMA) of OBV based on `smaPeriods` periods, if specified - > 🚩 **Warning**: absolute values in OBV are somewhat meaningless. Use with caution. ### Utilities diff --git a/docs/_indicators/ParabolicSar.md b/docs/_indicators/ParabolicSar.md index ee3bebee7..b3ab3d4b4 100644 --- a/docs/_indicators/ParabolicSar.md +++ b/docs/_indicators/ParabolicSar.md @@ -16,11 +16,11 @@ Created by J. Welles Wilder, [Parabolic SAR](https://en.wikipedia.org/wiki/Parab ```csharp // C# usage syntax (standard) -IEnumerable results = +IReadOnlyList results = quotes.GetParabolicSar(accelerationStep, maxAccelerationFactor); // alternate usage with custom initial Factor -IEnumerable results = +IReadOnlyList results = quotes.GetParabolicSar(accelerationStep, maxAccelerationFactor, initialFactor); ``` @@ -41,7 +41,7 @@ You must have at least two historical quotes to cover the warmup periods; howeve ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -51,7 +51,7 @@ IEnumerable ### ParabolicSarResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`Sar`** _`double`_ - Stop and Reverse value diff --git a/docs/_indicators/PivotPoints.md b/docs/_indicators/PivotPoints.md index 727dc0d1e..62f717ba4 100644 --- a/docs/_indicators/PivotPoints.md +++ b/docs/_indicators/PivotPoints.md @@ -17,7 +17,7 @@ layout: indicator ```csharp // C# usage syntax -IEnumerable results = +IReadOnlyList results = quotes.GetPivotPoints(windowSize, pointType); ``` @@ -58,7 +58,7 @@ You must have at least `2` windows of `quotes` to cover the warmup periods. For ## Response ```csharp -IEnumerable +IReadOnlyList ``` - This method returns a time series of all available indicator values for the `quotes` provided. @@ -72,7 +72,7 @@ IEnumerable ### PivotPointsResult -**`Date`** _`DateTime`_ - Date from evaluated `TQuote` +**`Timestamp`** _`DateTime`_ - date from evaluated `TQuote` **`R3`** _`decimal`_ - Resistance level 3 diff --git a/docs/_indicators/Pivots.md b/docs/_indicators/Pivots.md index 006f7d3cb..929a0098a 100644 --- a/docs/_indicators/Pivots.md +++ b/docs/_indicators/Pivots.md @@ -16,7 +16,7 @@ Pivots is an extended customizable version of = e.Signal - && trdQty != 1) - { - // emulates BTC + BTO - rlzGain += trdGain; - trdQty = 1; - trdPrice = q.Close; - cross = "LONG"; - } - - // check for SHORT event - // condition: Stoch RSI was >= 80 and Stoch RSI crosses under Signal - else if (l.StochRsi >= 80 - && l.StochRsi > l.Signal - && e.StochRsi <= e.Signal - && trdQty != -1) - { - // emulates STC + STO - rlzGain += trdGain; - trdQty = -1; - trdPrice = q.Close; - cross = "SHORT"; - } - - if (cross != string.Empty) - { - Console.WriteLine( - $"{q.Date,10:yyyy-MM-dd} " + - $"{q.Close,10:c2}" + - $"{e.StochRsi,7:N1}" + - $"{e.Signal,7:N1}" + - $"{cross,7}" + - $"{rlzGain + trdGain,13:c2}"); - } - } - } +/************************************************************ - private static Collection GetQuotesFromFeed() - { - /************************************************************ + This is a basic 20-year backtest-style analysis of + Stochastic RSI. It will buy-to-open (BTO) one share + when the Stoch RSI (%K) is below 20 and crosses over the + Signal (%D). The reverse Sell-to-Close (STC) and + Sell-To-Open (STO) occurs when the Stoch RSI is above 80 and + crosses below the Signal. - We're mocking a data provider here by simply importing a - JSON file, a similar format of many public APIs. + As a result, there will always be one open LONG or SHORT + position that is opened and closed at signal crossover + points in the overbought and oversold regions of the indicator. - This approach will vary widely depending on where you are - getting your quote history. + ************************************************************/ - See https://github.com/DaveSkender/Stock.Indicators/discussions/579 - for free or inexpensive market data providers and examples. +// Fetch historical quotes from data provider. +// We're mocking with a simple JSON file import +string json = File.ReadAllText("quotes.data.json"); - The return type of IEnumerable can also be List - or ICollection or other IEnumerable compatible types. +Collection quotes = JsonSerializer + .Deserialize>(json) + .ToSortedCollection(); - ************************************************************/ +// Calculate Stochastic RSI +IReadOnlyList resultsList = + quotes + .GetStochRsi(14, 14, 3) + .ToList(); + +// initialize +decimal trdPrice = 0; +decimal trdQty = 0; +decimal rlzGain = 0; + +Console.WriteLine(" Date Close StRSI Signal Cross Net Gains"); +Console.WriteLine("-------------------------------------------------------"); + +// roll through source values +for (int i = 1; i < quotes.Count; i++) +{ + Quote q = quotes[i]; - string json = File.ReadAllText("quotes.data.json"); + StochRsiResult e = resultsList[i]; // evaluation period + StochRsiResult l = resultsList[i - 1]; // last (prior) period + string cross = string.Empty; - Collection quotes = JsonSerializer - .Deserialize>(json) - .ToSortedCollection(); + // unrealized gain on open trade + decimal trdGain = trdQty * (q.Close - trdPrice); - return quotes; + // check for LONG event + // condition: Stoch RSI was <= 20 and Stoch RSI crosses over Signal + if (l.StochRsi <= 20 + && l.StochRsi < l.Signal + && e.StochRsi >= e.Signal + && trdQty != 1) + { + // emulates BTC + BTO + rlzGain += trdGain; + trdQty = 1; + trdPrice = q.Close; + cross = "LONG"; + } + + // check for SHORT event + // condition: Stoch RSI was >= 80 and Stoch RSI crosses under Signal + else if (l.StochRsi >= 80 + && l.StochRsi > l.Signal + && e.StochRsi <= e.Signal + && trdQty != -1) + { + // emulates STC + STO + rlzGain += trdGain; + trdQty = -1; + trdPrice = q.Close; + cross = "SHORT"; + } + + if (cross != string.Empty) + { + Console.WriteLine( + $"{q.Date,10:yyyy-MM-dd} " + + $"{q.Close,10:c2}" + + $"{e.StochRsi,7:N1}" + + $"{e.Signal,7:N1}" + + $"{cross,7}" + + $"{rlzGain + trdGain,13:c2}"); } } diff --git a/docs/examples/ConsoleApp/Program.cs b/docs/examples/ConsoleApp/Program.cs index e8099bc19..b8f5185f8 100644 --- a/docs/examples/ConsoleApp/Program.cs +++ b/docs/examples/ConsoleApp/Program.cs @@ -3,94 +3,63 @@ using System.Text.Json; using Skender.Stock.Indicators; -namespace ConsoleApp; +/* BASIC CONSOLE APP | SIMPLE MOVING AVERAGE */ -public static class Program -{ - public static void Main() - { - // fetch historical quotes from data provider - IEnumerable quotes = GetQuotesFromFeed(); - - // calculate 10-period SMA - IEnumerable results = quotes.GetSma(10); - - // show results - Console.WriteLine("SMA Results ---------------------------"); - - foreach (SmaResult r in results.TakeLast(10)) - { - // only showing last 10 records for brevity - Console.WriteLine($"SMA on {r.Date:u} was ${r.Sma:N3}"); - } - - // optionally, you can lookup individual values by date - DateTime lookupDate = DateTime - .Parse("2021-08-12T17:08:17.9746795+02:00", CultureInfo.InvariantCulture); - - double? specificSma = results.Find(lookupDate).Sma; - - Console.WriteLine(); - Console.WriteLine("SMA on Specific Date ------------------"); - Console.WriteLine($"SMA on {lookupDate:u} was ${specificSma:N3}"); +// Fetch historical quotes from data provider. +// We're mocking with a simple JSON file import +string json = File.ReadAllText("quotes.data.json"); - // analyze results (compare to quote values) - Console.WriteLine(); - Console.WriteLine("SMA Analysis --------------------------"); +Collection quotes = JsonSerializer + .Deserialize>(json) + .ToSortedCollection(); - /************************************************************ - Results are usually returned with the same number of - elements as the provided quotes; see individual indicator - docs for more information. +// Calculate 10-period SMA +IEnumerable results = quotes.GetSma(10); - As such, converting to List means they can be indexed - with the same ordinal position. - ************************************************************/ +// show results +Console.WriteLine("SMA Results ---------------------------"); - List quotesList = quotes - .ToList(); - - List resultsList = results - .ToList(); - - for (int i = quotesList.Count - 25; i < quotesList.Count; i++) - { - // only showing ~25 records for brevity - - Quote q = quotesList[i]; - SmaResult r = resultsList[i]; +foreach (SmaResult r in results.TakeLast(10)) +{ + // only showing last 10 records for brevity + Console.WriteLine($"SMA on {r.Date:u} was ${r.Sma:N3}"); +} - bool isBullish = (double)q.Close > r.Sma; +// optionally, you can lookup individual values by date +DateTime lookupDate = DateTime + .Parse("2021-08-12T17:08:17.9746795+02:00", CultureInfo.InvariantCulture); - Console.WriteLine($"SMA on {r.Date:u} was ${r.Sma:N3}" - + $" and Bullishness is {isBullish}"); - } - } +double? specificSma = results.Find(lookupDate).Sma; - private static Collection GetQuotesFromFeed() - { - /************************************************************ +Console.WriteLine(); +Console.WriteLine("SMA on Specific Date ------------------"); +Console.WriteLine($"SMA on {lookupDate:u} was ${specificSma:N3}"); - We're mocking a data provider here by simply importing a - JSON file, a similar format of many public APIs. +// analyze results (compare to quote values) +Console.WriteLine(); +Console.WriteLine("SMA Analysis --------------------------"); - This approach will vary widely depending on where you are - getting your quote history. +/************************************************************ + Results are usually returned with the same number of + elements as the provided quotes; see individual indicator + docs for more information. - See https://github.com/DaveSkender/Stock.Indicators/discussions/579 - for free or inexpensive market data providers and examples. + As such, converting to List means they can be indexed + with the same ordinal position. + ************************************************************/ - The return type of IEnumerable can also be List - or ICollection or other IEnumerable compatible types. +IReadOnlyList resultsList + = results.ToList(); - ************************************************************/ +for (int i = quotes.Count - 25; i < quotes.Count; i++) +{ + // only showing ~25 records for brevity - string json = File.ReadAllText("quotes.data.json"); + Quote q = quotes[i]; + SmaResult r = resultsList[i]; - Collection quotes = JsonSerializer - .Deserialize>(json) - .ToSortedCollection(); + bool isBullish = (double)q.Close > r.Sma; - return quotes; - } + Console.WriteLine($"SMA on {r.Date:u} was ${r.Sma:N3}" + + $" and Bullishness is {isBullish}"); } diff --git a/docs/examples/CustomIndicators/AtrWma.cs b/docs/examples/CustomIndicators/AtrWma.cs index 2f0412f23..9414070b2 100644 --- a/docs/examples/CustomIndicators/AtrWma.cs +++ b/docs/examples/CustomIndicators/AtrWma.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using Skender.Stock.Indicators; namespace Custom.Stock.Indicators; @@ -18,24 +17,24 @@ public sealed class AtrWmaResult : ResultBase, IReusableResult public static class CustomIndicators { // Custom ATR WMA calculation - public static IEnumerable GetAtrWma( - this IEnumerable quotes, + public static IReadOnlyList GetAtrWma( + this IReadOnlyList quotes, int lookbackPeriods) where TQuote : IQuote { // sort quotes and convert to collection or list - Collection quotesList = quotes + IReadOnlyList quotesList = quotes .ToSortedCollection(); // initialize results List results = new(quotesList.Count); // perform pre-requisite calculations to get ATR values - List atrResults = quotes + IReadOnlyList atrResults = quotes .GetAtr(lookbackPeriods) .ToList(); - // roll through quotes + // roll through source values for (int i = 0; i < quotesList.Count; i++) { TQuote q = quotesList[i]; diff --git a/docs/examples/CustomIndicators/README.md b/docs/examples/CustomIndicators/README.md index ca5bccc03..23073a902 100644 --- a/docs/examples/CustomIndicators/README.md +++ b/docs/examples/CustomIndicators/README.md @@ -21,14 +21,14 @@ using Skender.Stock.Indicators; namespace Custom.Stock.Indicators; // custom results class -public class AtrWmaResult : ResultBase, IReusableResult +public class AtrWmaResult : ResultBase, IReusable { // date property is inherited here, // so you only need to add custom items public double? AtrWma { get; set; } // to enable further chaining - double? IReusableResult.Value => AtrWma; + double? IReusable.Value => AtrWma; } ``` @@ -44,8 +44,8 @@ namespace Custom.Stock.Indicators; public static class CustomIndicator { // Custom ATR WMA calculation - public static IEnumerable GetAtrWma( - this IEnumerable quotes, + public static IReadOnlyList GetAtrWma( + this IReadOnlyList quotes, int lookbackPeriods) where TQuote : IQuote { @@ -61,7 +61,7 @@ public static class CustomIndicator .GetAtr(lookbackPeriods) .ToList(); - // roll through quotes + // roll through source values for (int i = 0; i < quotesList.Count; i++) { TQuote q = quotesList[i]; @@ -107,10 +107,10 @@ using Custom.Stock.Indicators; // your custom library [..] // fetch historical quotes from your feed (your method) -IEnumerable quotes = GetQuotesFromFeed("MSFT"); +IReadOnlyList quotes = GetQuotesFromFeed("MSFT"); // calculate 10-period ATR WMA -IEnumerable results = quotes.GetAtrWma(10); +IReadOnlyList results = quotes.GetAtrWma(10); // use results as needed for your use case (example only) foreach (AtrWmaResult r in results) diff --git a/docs/examples/CustomIndicatorsUsage/Program.cs b/docs/examples/CustomIndicatorsUsage/Program.cs index 5439a977e..11eae1f42 100644 --- a/docs/examples/CustomIndicatorsUsage/Program.cs +++ b/docs/examples/CustomIndicatorsUsage/Program.cs @@ -3,74 +3,44 @@ using Custom.Stock.Indicators; using Skender.Stock.Indicators; -namespace CustomIndicatorsUsage; - // USE CUSTOM INDICATORS exactly the same as // other indicators in the library -public static class Program -{ - public static void Main() - { - // fetch historical quotes from data provider - IEnumerable quotes = GetQuotesFromFeed(); - - // calculate 10-period custom AtrWma - IEnumerable results = quotes - .GetAtrWma(10); - - // show results - Console.WriteLine("ATR WMA Results ---------------------------"); - - foreach (AtrWmaResult r in results.Take(30)) - { - // only showing first 30 records for brevity - Console.WriteLine($"ATR WMA on {r.Date:u} was ${r.AtrWma:N3}"); - } - - // optional: demo of a converter (nulls to NaN) - - Console.WriteLine(); - Console.WriteLine("ATR WMA Results with NaN (optional) -------"); +// Fetch historical quotes from data provider. +// We're mocking with a simple JSON file import +string json = File.ReadAllText("quotes.data.json"); - // tip: converting ToList() and using For loops is faster to iterate - List resultsList = results - .Take(30) - .ToList(); +Collection quotes = JsonSerializer + .Deserialize>(json) + .ToSortedCollection(); - for (int i = 0; i < resultsList.Count; i++) - { - AtrWmaResult r = resultsList[i]; - r.AtrWma = r.AtrWma.Null2NaN(); - - Console.WriteLine($"ATR WMA on {r.Date:u} was ${r.AtrWma:N3}"); - } - } - - private static Collection GetQuotesFromFeed() - { - /************************************************************ - - We're mocking a data provider here by simply importing a - JSON file, a similar format of many public APIs. +// Calculate 10-period custom AtrWma +IReadOnlyList results = quotes + .GetAtrWma(10); - This approach will vary widely depending on where you are - getting your quote history. +// Show results +Console.WriteLine("ATR WMA Results ---------------------------"); - See https://github.com/DaveSkender/Stock.Indicators/discussions/579 - for free or inexpensive market data providers and examples. +foreach (AtrWmaResult r in results.Take(30)) +{ + // only showing first 30 records for brevity + Console.WriteLine($"ATR WMA on {r.Date:u} was ${r.AtrWma:N3}"); +} - The return type of IEnumerable can also be List - or ICollection or other IEnumerable compatible types. +// optional: demo of a converter (nulls to NaN) - ************************************************************/ +Console.WriteLine(); +Console.WriteLine("ATR WMA Results with NaN (optional) -------"); - string json = File.ReadAllText("quotes.data.json"); +// TIP: converting ToList() and using For loops is faster to iterate +List resultsList = results + .Take(30) + .ToList(); - Collection quotes = JsonSerializer - .Deserialize>(json) - .ToSortedCollection(); +for (int i = 0; i < resultsList.Count; i++) +{ + AtrWmaResult r = resultsList[i]; + r.AtrWma = r.AtrWma.Null2NaN(); - return quotes; - } + Console.WriteLine($"ATR WMA on {r.Date:u} was ${r.AtrWma:N3}"); } diff --git a/docs/examples/Examples.sln b/docs/examples/Examples.sln index ce9820ae4..7ae0622cc 100644 --- a/docs/examples/Examples.sln +++ b/docs/examples/Examples.sln @@ -13,7 +13,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomIndicatorsUsage", "Cu EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObserveStream", "ObserveStream\ObserveStream.csproj", "{5FAD383B-DFCD-42FD-A847-53D772876595}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UseQuoteApi", "UseQuoteApi\UseQuoteApi.csproj", "{E6B2E0AE-6457-47F3-9BA5-01F4AA84118A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UseQuoteApi", "UseQuoteApi\UseQuoteApi.csproj", "{E6B2E0AE-6457-47F3-9BA5-01F4AA84118A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BF2C9401-10F2-4D06-A610-A2372336FCA8}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -45,6 +50,10 @@ Global {E6B2E0AE-6457-47F3-9BA5-01F4AA84118A}.Debug|Any CPU.Build.0 = Debug|Any CPU {E6B2E0AE-6457-47F3-9BA5-01F4AA84118A}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6B2E0AE-6457-47F3-9BA5-01F4AA84118A}.Release|Any CPU.Build.0 = Release|Any CPU + {9B9ED801-0F01-4851-A086-9A3805584BE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B9ED801-0F01-4851-A086-9A3805584BE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B9ED801-0F01-4851-A086-9A3805584BE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B9ED801-0F01-4851-A086-9A3805584BE2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/examples/ObserveStream/Program.cs b/docs/examples/ObserveStream/Program.cs index 80b9a54be..ef63e0efd 100644 --- a/docs/examples/ObserveStream/Program.cs +++ b/docs/examples/ObserveStream/Program.cs @@ -1,125 +1,107 @@ using Alpaca.Markets; using Skender.Stock.Indicators; -namespace ObserveStream; +string symbol = "BTC/USD"; +Console.WriteLine($"STREAMING QUOTES FOR {symbol}"); +Console.WriteLine(); -internal class Program +// get and validate keys, see README.md +string ALPACA_KEY = Environment.GetEnvironmentVariable("ALPACA_KEY"); +string ALPACA_SECRET = Environment.GetEnvironmentVariable("ALPACA_SECRET"); + +if (string.IsNullOrEmpty(ALPACA_KEY)) { - private static async Task Main(string[] args) - { - if (args.Length != 0) - { - Console.WriteLine(args); - } + throw new ArgumentNullException( + ALPACA_KEY, + $"API KEY missing, use `setx ALPACA_KEY \"MY-ALPACA-KEY\"` to set."); +} + +if (string.IsNullOrEmpty(ALPACA_SECRET)) +{ + throw new ArgumentNullException( + ALPACA_SECRET, + $"API SECRET missing, use `setx AlpacaApiSecret \"MY-ALPACA-SECRET\"` to set."); +} + +// initialize our quote provider and a few subscribers +QuoteProvider provider = new(); + +SmaObserver sma = provider.GetSma(3); +EmaObserver ema = provider.GetEma(5); +EmaObserver emaChain = provider + .Use(CandlePart.HL2) + .GetEma(7); + +// connect to Alpaca WebSocket +SecretKey secretKey = new(ALPACA_KEY, ALPACA_SECRET); + +IAlpacaCryptoStreamingClient client + = Environments + .Paper + .GetAlpacaCryptoStreamingClient(secretKey); + +await client.ConnectAndAuthenticateAsync(); + +// TODO: is this needed? +AutoResetEvent[] waitObjects = [new AutoResetEvent(false)]; + +IAlpacaDataSubscription quoteSubscription + = client.GetMinuteBarSubscription(symbol); - string symbol = "BTC/USD"; - Console.WriteLine($"STREAMING QUOTES FOR {symbol}"); - Console.WriteLine(); +await client.SubscribeAsync(quoteSubscription); - await SubscribeToQuotes(symbol); +// console display header +Console.WriteLine("A new quote will be shown when they arrive every minute."); +Console.WriteLine("PLEASE WAIT > 8 MINUTES BEFORE EXITING TO SEE ALL 3 INDICATORS CALCULATED."); +Console.WriteLine("Press any key to EXIT the process and to see results."); +Console.WriteLine(); + +Console.WriteLine("Date Close price SMA(3) EMA(5) EMA(7,HL2)"); +Console.WriteLine("----------------------------------------------------------------------"); + +// handle new quotes +quoteSubscription.Received += (q) => +{ + // add to our provider + provider.Add(new Quote + { + Date = q.TimeUtc, + Open = q.Open, + High = q.High, + Low = q.Low, + Close = q.Close, + Volume = q.Volume + }); + + // display live results + string liveMessage = $"{q.TimeUtc:u} ${q.Close:N2}"; + + SmaResult s = sma.Results.Last(); + EmaResult e = ema.Results.Last(); + EmaResult c = emaChain.Results.Last(); + + if (s.Sma is not null) + { + liveMessage += $"{s.Sma,12:N1}"; } - public static async Task SubscribeToQuotes(string symbol) + if (e.Ema is not null) { - // get and validate keys, see README.md - string ALPACA_KEY = Environment.GetEnvironmentVariable("ALPACA_KEY"); - string ALPACA_SECRET = Environment.GetEnvironmentVariable("ALPACA_SECRET"); - - if (string.IsNullOrEmpty(ALPACA_KEY)) - { - throw new ArgumentNullException( - ALPACA_KEY, - $"API KEY missing, use `setx ALPACA_KEY \"MY-ALPACA-KEY\"` to set."); - } - - if (string.IsNullOrEmpty(ALPACA_SECRET)) - { - throw new ArgumentNullException( - ALPACA_SECRET, - $"API SECRET missing, use `setx AlpacaApiSecret \"MY-ALPACA-SECRET\"` to set."); - } - - // initialize our quote provider and a few subscribers - QuoteProvider provider = new(); - - SmaObserver sma = provider.GetSma(3); - EmaObserver ema = provider.GetEma(5); - EmaObserver emaChain = provider - .Use(CandlePart.HL2) - .GetEma(7); - - // connect to Alpaca WebSocket - SecretKey secretKey = new(ALPACA_KEY, ALPACA_SECRET); - - IAlpacaCryptoStreamingClient client - = Environments - .Paper - .GetAlpacaCryptoStreamingClient(secretKey); - - await client.ConnectAndAuthenticateAsync(); - - // TODO: is this needed? - AutoResetEvent[] waitObjects = [new AutoResetEvent(false)]; - - IAlpacaDataSubscription quoteSubscription - = client.GetMinuteBarSubscription(symbol); - - await client.SubscribeAsync(quoteSubscription); - - // console display header - Console.WriteLine("A new quote will be shown when they arrive every minute."); - Console.WriteLine("PLEASE WAIT > 8 MINUTES BEFORE EXITING TO SEE ALL 3 INDICATORS CALCULATED."); - Console.WriteLine("Press any key to EXIT the process and to see results."); - Console.WriteLine(); - - Console.WriteLine("Date Close price SMA(3) EMA(5) EMA(7,HL2)"); - Console.WriteLine("----------------------------------------------------------------------"); - - // handle new quotes - quoteSubscription.Received += (q) => - { - // add to our provider - provider.Add(new Quote - { - Date = q.TimeUtc, - Open = q.Open, - High = q.High, - Low = q.Low, - Close = q.Close, - Volume = q.Volume - }); - - // display live results - string liveMessage = $"{q.TimeUtc:u} ${q.Close:N2}"; - - SmaResult s = sma.Results.Last(); - EmaResult e = ema.Results.Last(); - EmaResult c = emaChain.Results.Last(); - - if (s.Sma is not null) - { - liveMessage += $"{s.Sma,12:N1}"; - } - - if (e.Ema is not null) - { - liveMessage += $"{e.Ema,12:N1}"; - } - - if (c.Ema is not null) - { - liveMessage += $"{c.Ema,12:N1}"; - } - - Console.WriteLine(liveMessage); - }; - - // to stop watching on key press - Console.ReadKey(); - - // terminate subscriptions - provider.EndTransmission(); - await client.UnsubscribeAsync(quoteSubscription); - await client.DisconnectAsync(); + liveMessage += $"{e.Ema,12:N1}"; } -} + + if (c.Ema is not null) + { + liveMessage += $"{c.Ema,12:N1}"; + } + + Console.WriteLine(liveMessage); +}; + +// to stop watching on key press +Console.ReadKey(); + +// terminate subscriptions +provider.EndTransmission(); +await client.UnsubscribeAsync(quoteSubscription); +await client.DisconnectAsync(); diff --git a/docs/examples/README.md b/docs/examples/README.md index 6fabc94ad..c49860719 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -78,7 +78,7 @@ decimal rlzGain = 0; Console.WriteLine(" Date Close StRSI Signal Cross Net Gains"); Console.WriteLine("-------------------------------------------------------"); -// roll through history +// roll through source values for (int i = 1; i < quotesList.Count; i++) { Quote q = quotesList[i]; diff --git a/docs/examples/UseQuoteApi/Program.cs b/docs/examples/UseQuoteApi/Program.cs index 024a2c0a1..74ece1876 100644 --- a/docs/examples/UseQuoteApi/Program.cs +++ b/docs/examples/UseQuoteApi/Program.cs @@ -1,131 +1,114 @@ using Alpaca.Markets; using Skender.Stock.Indicators; -namespace UseQuoteApi; +string symbol = "AAPL"; -internal class Program +/************************************************************ + + We're using Alpaca SDK for .NET to access their public APIs. + + This approach will vary widely depending on where you are + getting your quote history. + + See https://github.com/DaveSkender/Stock.Indicators/discussions/579 + for free or inexpensive market data providers and examples. + + The return type of IEnumerable can also be List + or ICollection or other IEnumerable compatible types. + + ************************************************************/ + +// get and validate keys, see README.md +string ALPACA_KEY = Environment.GetEnvironmentVariable("ALPACA_KEY"); +string ALPACA_SECRET = Environment.GetEnvironmentVariable("ALPACA_SECRET"); + +if (string.IsNullOrEmpty(ALPACA_KEY)) { - private static async Task Main() - { - string symbol = "AAPL"; + throw new ArgumentNullException( + ALPACA_KEY, + $"API KEY missing, use `setx ALPACA_KEY \"MY-ALPACA-KEY\"` to set."); +} - // fetch historical quotes from data provider - IEnumerable quotes = await GetQuotesFromFeed(symbol); +if (string.IsNullOrEmpty(ALPACA_SECRET)) +{ + throw new ArgumentNullException( + ALPACA_SECRET, + $"API SECRET missing, use `setx AlpacaApiSecret \"MY-ALPACA-SECRET\"` to set."); +} - // calculate 10-period SMA - IEnumerable results = quotes.GetSma(10); +// connect to Alpaca REST API +SecretKey secretKey = new(ALPACA_KEY, ALPACA_SECRET); - if (!results.Any() || results == null) - { - throw new NullReferenceException("No indicator results were returned."); - } +IAlpacaDataClient client = Environments.Paper.GetAlpacaDataClient(secretKey); - // show results - Console.WriteLine($"{symbol} Results ------- (last 10 of {results.Count()}) --"); +// compose request +// (excludes last 15 minutes for free delayed quotes) +DateTime into = DateTime.Now.Subtract(TimeSpan.FromMinutes(16)); +DateTime from = into.Subtract(TimeSpan.FromDays(1000)); - foreach (SmaResult r in results.TakeLast(10)) - { - // only showing last 10 records for brevity - Console.WriteLine($"SMA on {r.Date:u} was ${r.Sma:N3}"); - } +HistoricalBarsRequest request = new(symbol, from, into, BarTimeFrame.Minute); - // analyze results (compare to quote values) - Console.WriteLine(); - Console.WriteLine($"{symbol} Analysis --------------------------"); +// fetch minute-bar quotes in Alpaca's format +IPage barSet = await client.ListHistoricalBarsAsync(request); - /************************************************************ - Results are usually returned with the same number of - elements as the provided quotes; see individual indicator - docs for more information. +// convert library compatible quotes +List quotes = barSet + .Items + .Select(bar => new Quote + { + Date = bar.TimeUtc, + Open = bar.Open, + High = bar.High, + Low = bar.Low, + Close = bar.Close, + Volume = bar.Volume + }) + .OrderBy(x => x.Date) + .ToList(); - As such, converting to List means they can be indexed - with the same ordinal position. - ************************************************************/ - List quotesList = quotes - .ToList(); +// calculate 10-period SMA +IEnumerable results = quotes.GetSma(10); - List resultsList = results - .ToList(); +if (!results.Any() || results == null) +{ + throw new NullReferenceException("No indicator results were returned."); +} + +// show results +Console.WriteLine($"{symbol} Results ------- (last 10 of {results.Count()}) --"); - for (int i = quotesList.Count - 25; i < quotesList.Count; i++) - { - // only showing ~25 records for brevity +foreach (SmaResult r in results.TakeLast(10)) +{ + // only showing last 10 records for brevity + Console.WriteLine($"SMA on {r.Date:u} was ${r.Sma:N3}"); +} - Quote q = quotesList[i]; - SmaResult r = resultsList[i]; +// analyze results (compare to quote values) +Console.WriteLine(); +Console.WriteLine($"{symbol} Analysis --------------------------"); - bool isBullish = (double)q.Close > r.Sma; +/************************************************************ + Results are usually returned with the same number of + elements as the provided quotes; see individual indicator + docs for more information. - Console.WriteLine($"SMA on {r.Date:u} was ${r.Sma:N3}" - + $" and Bullishness is {isBullish}"); - } - } + As such, converting to List means they can be indexed + with the same ordinal position. + ************************************************************/ - private static async Task> GetQuotesFromFeed(string symbol) - { - /************************************************************ - - We're using Alpaca SDK for .NET to access their public APIs. - - This approach will vary widely depending on where you are - getting your quote history. - - See https://github.com/DaveSkender/Stock.Indicators/discussions/579 - for free or inexpensive market data providers and examples. - - The return type of IEnumerable can also be List - or ICollection or other IEnumerable compatible types. - - ************************************************************/ - - // get and validate keys, see README.md - string ALPACA_KEY = Environment.GetEnvironmentVariable("ALPACA_KEY"); - string ALPACA_SECRET = Environment.GetEnvironmentVariable("ALPACA_SECRET"); - - if (string.IsNullOrEmpty(ALPACA_KEY)) - { - throw new ArgumentNullException( - ALPACA_KEY, - $"API KEY missing, use `setx ALPACA_KEY \"MY-ALPACA-KEY\"` to set."); - } - - if (string.IsNullOrEmpty(ALPACA_SECRET)) - { - throw new ArgumentNullException( - ALPACA_SECRET, - $"API SECRET missing, use `setx AlpacaApiSecret \"MY-ALPACA-SECRET\"` to set."); - } - - // connect to Alpaca REST API - SecretKey secretKey = new(ALPACA_KEY, ALPACA_SECRET); - - 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(1000)); - - HistoricalBarsRequest request = new(symbol, from, into, BarTimeFrame.Minute); - - // fetch minute-bar quotes in Alpaca's format - IPage barSet = await client.ListHistoricalBarsAsync(request); - - // convert library compatible quotes - IEnumerable quotes = barSet - .Items - .Select(bar => new Quote - { - Date = bar.TimeUtc, - Open = bar.Open, - High = bar.High, - Low = bar.Low, - Close = bar.Close, - Volume = bar.Volume - }) - .OrderBy(x => x.Date); // optional - - return quotes; - } +List resultsList = results + .ToList(); + +for (int i = quotes.Count - 25; i < quotes.Count; i++) +{ + // only showing ~25 records for brevity + + Quote q = quotes[i]; + SmaResult r = resultsList[i]; + + bool isBullish = (double)q.Close > r.Sma; + + Console.WriteLine($"SMA on {r.Date:u} was ${r.Sma:N3}" + + $" and Bullishness is {isBullish}"); } diff --git a/docs/pages/guide.md b/docs/pages/guide.md index e075c2d2a..40b1adad4 100644 --- a/docs/pages/guide.md +++ b/docs/pages/guide.md @@ -15,7 +15,6 @@ layout: page
  • Example usage
  • Historical quotes
  • Using custom quote classes
  • -
  • Using custom results classes
  • Generating indicator of indicators
  • Candlestick patterns
  • Creating custom indicators
  • @@ -44,7 +43,7 @@ Most indicators require that you provide historical quote data and additional co You must get historical quotes from your own market data provider. For clarification, the `GetQuotesFromFeed()` method shown in the example below **is not part of this library**, but rather an example to represent your own acquisition of historical quotes. -Historical price data can be provided as a `List`, `IEnumerable`, or `ICollection` of the `Quote` class ([see below](#historical-quotes)); however, it can also be supplied as a generic [custom TQuote type](#using-custom-quote-classes) if you prefer to use your own quote model. +Historical price data can be provided as a `List`, `IReadOnlyList`, or `ICollection` of the `Quote` class ([see below](#historical-quotes)); however, it can also be supplied as a generic [custom TQuote type](#using-custom-quote-classes) if you prefer to use your own quote model. For additional configuration parameters, default values are provided when there is an industry standard. You can, of course, override these and provide your own values. @@ -58,16 +57,16 @@ using Skender.Stock.Indicators; [..] // fetch historical quotes from your feed (your method) -IEnumerable quotes = GetQuotesFromFeed("MSFT"); +IReadOnlyList quotes = GetQuotesFromFeed("MSFT"); // calculate 20-period SMA -IEnumerable results = quotes +IReadOnlyList results = quotes .GetSma(20); // use results as needed for your use case (example only) foreach (SmaResult r in results) { - Console.WriteLine($"SMA on {r.Date:d} was ${r.Sma:N4}"); + Console.WriteLine($"SMA on {r.Timestamp:d} was ${r.Sma:N4}"); } ``` @@ -90,11 +89,11 @@ More examples available: ## Historical quotes -You must provide historical price quotes to the library in the standard OHLCV `IEnumerable` or a compatible `List` or `ICollection` format. It should have a consistent period frequency (day, hour, minute, etc). See [using custom quote classes](#using-custom-quote-classes) if you prefer to use your own quote class. +You must provide historical price quotes to the library in the standard OHLCV `IReadOnlyList` or a compatible `List` or `ICollection` format. It should have a consistent period frequency (day, hour, minute, etc). See [using custom quote classes](#using-custom-quote-classes) if you prefer to use your own quote class. | name | type | notes | -- |-- |-- -| `Date` | DateTime | Date +| `Timestamp` | DateTime | Close date | `Open` | decimal | Open price | `High` | decimal | High price | `Low` | decimal | Low price @@ -117,17 +116,19 @@ Each indicator will need different amounts of price `quotes` to calculate. You ### Using custom quote classes -If you would like to use your own custom `MyCustomQuote` class, to avoid needing to transpose into the library `Quote` class, you only need to add the `IQuote` interface. +If you would like to use your own custom `MyCustomQuote` class, to avoid needing to transpose into the built-in library `Quote` class, you only need to add the `IQuote` interface and ensure that you've implemented a correct and compatible quote `record` or class. + +> 🚩 **IMPORTANT!** +> Your custom quote class needs to be [equatable using property values](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type). Since this can be complicated to setup, we've provided the shown `EquatableQuote` base class. You can exclude this base and write your own `IEquatable` interface by only implementing the `IQuote` interface; however, if you do not define it fully with `==` and `!=` operator overrides correctly, it may cause problems with streaming overflow handling. **We recommend using the equatable `record` class** type for your custom quote class. ```csharp using Skender.Stock.Indicators; -[..] - -public class MyCustomQuote : IQuote +/// EASY METHOD (use record class) +public record class MyCustomQuote : IQuote { // required base properties - public DateTime Date { get; set; } + public DateTime Timestamp { get; set; } public decimal Open { get; set; } public decimal High { get; set; } public decimal Low { get; set; } @@ -136,83 +137,109 @@ public class MyCustomQuote : IQuote // custom properties public int MyOtherProperty { get; set; } + + // required mapping method for equality + public bool Equals(IQuote? other) + => base.Equals(other); } ``` ```csharp -// fetch historical quotes from your favorite feed -IEnumerable myQuotes = GetQuotesFromFeed("MSFT"); - -// example: get 20-period simple moving average -IEnumerable results = myQuotes.GetSma(20); -``` +/// EASY METHOD (use our base equatable class) +public class MyCustomQuote : EquatableQuote, IQuote +{ + // required inherited base properties do not need to be redefined, + // however, if you prefer to explicitly define for clarity, + // use the override keyword (optional) -#### Using custom quote property names + public override DateTime Timestamp { get; set; } + public override decimal Open { get; set; } + public override decimal High { get; set; } + public override decimal Low { get; set; } + public override decimal Close { get; set; } + public override decimal Volume { get; set; } -If you have a model that has different properties names, but the same meaning, you only need to map them. For example, if your class has a property called `CloseDate` instead of `Date`, it could be represented like this: + // custom properties + public int MyOtherProperty { get; set; } +} +``` ```csharp -public class MyCustomQuote : IQuote // + ISeries +/// HARD METHOD (define your own equatable overrides) +public class MyCustomQuote : IQuote { // required base properties - DateTime ISeries.Date => CloseDate; + 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; } - decimal IQuote.Volume => Vol; + public decimal Volume { get; set; } // custom properties public int MyOtherProperty { get; set; } - public DateTime CloseDate { get; set; } - public decimal Vol { get; set; } + + // equatable overrides + public override bool Equals(object? obj) => this.Equals(obj); + public bool Equals(IQuote? other) => this.Equals(other); + public bool Equals(MyCustomQuote? other) { ... } + public static bool operator ==( ... ) { ... } + public static bool operator !=( ... ) { ... } + public override int GetHashCode() + => HashCode.Combine( + Timestamp, Open, High, Low, Close, Volume); } ``` -Note the use of explicit interface (property declaration is `ISeries.Date`), this is because having two properties that expose the same information can be confusing, this way `Date` property is only accessible when working with the included `Quote` type, while if you are working with a `MyCustomQuote` the `Date` property will be hidden, avoiding confusion. +```csharp +// USAGE +// fetch historical quotes from your favorite feed +IReadOnlyList myQuotes = GetQuotesFromFeed("MSFT"); -For more information on explicit interfaces, refer to the [C# Programming Guide](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interfaces/explicit-interface-implementation). +// example: get 20-period simple moving average +IReadOnlyList results = myQuotes.GetSma(20); +``` -## Using custom results classes +#### Using custom quote property names -The indicator result classes can be customized in your code. There are many ways to do this, but the benefit of using derived `ResultBase` is that your custom class will inherit all of the [utility results extension methods]({{site.baseurl}}/utilities/#utilities-for-indicator-results). Here's one example: +If you have a model that has different properties names, but the same meaning, you only need to map them. For example, if your class has a property called `CloseDate` instead of `Timestamp`, it could be represented like this: ```csharp -// your custom class with an EMA profile -public class MyEma : ResultBase +// if using record type +public record class MyCustomQuote : IQuote { - // my properties - public int MyId { get; set; } - public double? Ema { get; set; } -} - -public void MyClass(){ - - // fetch historical quotes from your feed (your method) - IEnumerable quotes = GetQuotesFromFeed("SPY"); + // redirect required base properties + // with your custom properties + public DateTime Timestamp => CloseDate; + public decimal Volume => Vol; - // compute indicator - INumerable emaResults = quotes.GetEma(14); - - // convert to my Ema class list [using LINQ] - List myEmaResults = emaResults - .Select(e => new MyEma - { - MyId = 123, - Date = e.Date, - Ema = e.Ema - }) - .ToList(); + // custom properties + public int MyOtherProperty { get; set; } + public DateTime CloseDate { get; set; } + public decimal Vol { get; set; } +} +``` - // randomly selecting first record from the - // collection here for the example - MyEma r = myEmaResults.FirstOrDefault(); +```csharp +// if using inherited equatable class type +public class MyCustomQuote : EquatableQuote, IQuote +{ + // override inherited, required base properties + // with your custom properties + public override DateTime Timestamp => CloseDate; + public override decimal Volume => Vol; - // use your custom quote data - Console.WriteLine($"On {r.Date}, EMA was {r.Ema} for my EMA ID {r.MyId}."); + // custom properties + public int MyOtherProperty { get; set; } + public DateTime CloseDate { get; set; } + public decimal Vol { get; set; } } ``` +Note the use of explicit interface (property declaration is `ISeries.Timestamp`), this is because having two properties that expose the same information can be confusing, this way `Timestamp` property is only accessible when working with the included `Quote` type, while if you are working with a `MyCustomQuote` the `Timestamp` property will be hidden, avoiding confusion. + +For more information on explicit interfaces, refer to the [C# Programming Guide](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interfaces/explicit-interface-implementation). + ## Generating indicator of indicators If you want to compute an indicator of indicators, such as an SMA of an ADX or an [RSI of an OBV](https://medium.com/@robswc/this-is-what-happens-when-you-combine-the-obv-and-rsi-indicators-6616d991773d), use _**chaining**_ to calculate an indicator from prior results. @@ -220,17 +247,17 @@ Example: ```csharp // fetch historical quotes from your feed (your method) -IEnumerable quotes = GetQuotesFromFeed("SPY"); +IReadOnlyList quotes = GetQuotesFromFeed("SPY"); // calculate RSI of OBV -IEnumerable results +IReadOnlyList results = quotes .GetObv() .GetRsi(14); // or with two separate operations -IEnumerable obvResults = quotes.GetObv(); -IEnumerable rsiOfObv = obvResults.GetRsi(14); +IReadOnlyList obvResults = quotes.GetObv(); +IReadOnlyList rsiOfObv = obvResults.GetRsi(14); ``` ## Candlestick patterns diff --git a/docs/pages/home.md b/docs/pages/home.md index a9dba6239..b82ad38e3 100644 --- a/docs/pages/home.md +++ b/docs/pages/home.md @@ -40,7 +40,7 @@ You'll get all of the industry standard indicators out-of-the-box. Additionally ```csharp // example: get 20-period simple moving average -IEnumerable results = quotes.GetSma(20); +IReadOnlyList results = quotes.GetSma(20); ``` See more [usage examples]({{site.baseurl}}/guide/#example-usage). @@ -51,13 +51,13 @@ Optional chaining enables advanced uses cases; such as, indicator of indicators, ```csharp // example: advanced chaining (RSI of OBV) -IEnumerable results +IReadOnlyList results = quotes .GetObv() .GetRsi(14); // example: use any candle variant -IEnumerable results +IReadOnlyList results = quotes .Use(CandlePart.HL2) .GetEma(20); @@ -67,10 +67,9 @@ See the [guide]({{site.baseurl}}/guide/#content) and the [full list of indicator ## Optimized for modern .NET frameworks -Our [NuGet library](https://www.nuget.org/packages/Skender.Stock.Indicators) directly targets all current frameworks for peak performance, including the .NET Standard for older framework compatibility. +Our [NuGet library](https://www.nuget.org/packages/Skender.Stock.Indicators) directly targets all current frameworks for peak performance. -- .NET 8.0, 7.0, 6.0 -- .NET Standard 2.1, 2.0 +- .NET 8.0, 6.0 The compiled library package is [Common Language Specification (CLS) compliant](https://docs.microsoft.com/en-us/dotnet/standard/common-type-system) and can be used in other programming languages, including Python and everything in the .NET universe. diff --git a/docs/pages/utilities.md b/docs/pages/utilities.md index f227ba937..e86794df1 100644 --- a/docs/pages/utilities.md +++ b/docs/pages/utilities.md @@ -28,13 +28,9 @@ var results = quotes {% include candlepart-options.md %} -### Using tuple quotes - -`quotes.ToTupleCollection()` is a method for converting any `TQuote` collection to a simple [tuple](https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/value-tuples) `(DateTime, double)` formatted `Collection`. Most indicators in our library will accept this tuple format. With that said, there are many indicators that also require the full OHLCV quote format, so it cannot be used universally. - ### Sort quotes -`quotes.ToSortedCollection()` sorts any collection of `TQuote` or tuple `(DateTime, double)` and returns it as a `Collection` sorted by ascending `Date`. You do not need to sort quotes before using library indicators; however, if you are creating [custom indicators]({{site.baseurl}}/custom-indicators/#content) it's important to analyze `quotes` in a proper sequence. +`quotes.ToSortedList()` sorts any collection of `TQuote` or `ISeries` and returns it as a `IReadOnlyList` sorted by ascending `Timestamp`. You **do need to sort quotes** before using library indicators. ### Resize quote history @@ -42,7 +38,7 @@ var results = quotes ```csharp // aggregate into larger bars -IEnumerable dayBarQuotes = +IReadOnlyList dayBarQuotes = minuteBarQuotes.Aggregate(PeriodSize.Day); ``` @@ -50,7 +46,7 @@ An alternate version of this utility is provided where you can use any native `T ```csharp // alternate usage with TimeSpan -IEnumerable dayBarQuotes = +IReadOnlyList dayBarQuotes = minuteBarQuotes.Aggregate(TimeSpan timeSpan); ``` @@ -80,18 +76,18 @@ IEnumerable dayBarQuotes = CandleProperties candle = quote.ToCandle(); // collection of quotes -IEnumerable candles = quotes.ToCandles(); +IReadOnlyList candles = quotes.ToCandles(); ``` {% include candle-properties.md %} ### Validate quote history -`quotes.Validate()` is an advanced check of your `IEnumerable quotes`. It will check for duplicate dates and other bad data and will throw an `InvalidQuotesException` if validation fails. This comes at a small performance cost, so we did not automatically add these advanced checks in the indicator methods. Of course, you can and should do your own validation of `quotes` prior to using it in this library. Bad historical quotes can produce unexpected results. +`quotes.Validate()` is an advanced check of your `IReadOnlyList quotes`. It will check for duplicate dates and other bad data and will throw an `InvalidQuotesException` if validation fails. This comes at a small performance cost, so we did not automatically add these advanced checks in the indicator methods. Of course, you can and should do your own validation of `quotes` prior to using it in this library. Bad historical quotes can produce unexpected results. ```csharp // advanced validation -IEnumerable validatedQuotes = quotes.Validate(); +IReadOnlyList validatedQuotes = quotes.Validate(); // and can be used inline with chaining var results = quotes @@ -108,7 +104,7 @@ var results = quotes ```csharp // example: only show Marubozu signals -IEnumerable results +IReadOnlyList results = quotes.GetMarubozu(..).Condense(); ``` @@ -120,11 +116,13 @@ IEnumerable results ```csharp // calculate indicator series -IEnumerable results = quotes.GetSma(20); +IReadOnlyList results = quotes.GetSma(20); // find result on a specific date DateTime lookupDate = [..] // the date you want to find SmaResult result = results.Find(lookupDate); + +// throws 'InvalidOperationException' when not found ``` ### Remove warmup periods @@ -133,12 +131,12 @@ SmaResult result = results.Find(lookupDate); ```csharp // auto remove recommended warmup periods -IEnumerable results = +IReadOnlyList results = quotes.GetAdx(14).RemoveWarmupPeriods(); // remove a specific quantity of periods int n = 14; -IEnumerable results = +IReadOnlyList results = quotes.GetAdx(n).RemoveWarmupPeriods(n+100); ``` @@ -148,19 +146,9 @@ See [individual indicator pages]({{site.baseurl}}/indicators/#content) for infor > > 🚩 **Warning**: without a specified `removePeriods` value, this utility will reverse-engineer the pruning amount. When there are unusual results or when chaining multiple indicators, there will be an erroneous increase in the amount of pruning. If you want more certainty, use a specific number for `removePeriods`. Using this method on chained indicators without `removePeriods` is strongly discouraged. -### Using tuple results - -`results.ToTupleCollection()` converts results to a simpler `(DateTime Date, double? Value)` [tuple](https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/value-tuples) `Collection`. - -`results.ToTupleNaN()` converts results to simpler `(DateTime Date, double Value)` tuple `Collection` with `null` values converted to `double.NaN`. - -`results.ToTupleChainable()` is a specialty converter used to prepare [custom indicators]({{site.baseurl}}/custom-indicators/#content) for chaining by removing `null` warmup periods and converting all remaining `null` values to `double.NaN`. - -> 🚩 **Warning**: warmup periods are pruned when using `.ToTupleChainable()`, resulting in fewer records. - ### Sort results -`results.ToSortedCollection()` sorts any collection of indicator results and returns it as a `Collection` sorted by ascending `Date`. Results from the library indicators are already sorted, so you'd only potentially need this if you're creating [custom indicators]({{site.baseurl}}/custom-indicators/#content). +`results.ToSortedList()` sorts any collection of indicator results and returns it as a `IReadOnlyList` sorted by ascending `Timestamp`. Results from the library indicators are already sorted, so you'd only potentially need this if you're creating [custom indicators]({{site.baseurl}}/custom-indicators/#content). ## Utilities for numerical analysis diff --git a/src/GlobalSuppressions.cs b/src/GlobalSuppressions.cs index 751a398ee..ed7d4e0c5 100644 --- a/src/GlobalSuppressions.cs +++ b/src/GlobalSuppressions.cs @@ -1,25 +1,16 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage( - "Naming", - "CA1716:Identifiers should not match keywords", - Justification = "Not really an issue.", - Scope = "member", - Target = "~P:Skender.Stock.Indicators.ISeries.Date")] - -[assembly: SuppressMessage( - "Naming", - "CA1716:Identifiers should not match keywords", - Justification = "Not really an issue.", - Scope = "member", - Target = "~P:Skender.Stock.Indicators.Quote.Date")] + "Maintainability", + "CA1510:Use ArgumentNullException throw helper", + Justification = "Does not support .NET Standard.")] [assembly: SuppressMessage( "Naming", - "CA1716:Identifiers should not match keywords", + "CA1720:Identifier contains type name", Justification = "Not really an issue.", Scope = "member", - Target = "~P:Skender.Stock.Indicators.IBasicData.Date")] + Target = "~F:Skender.Stock.Indicators.ChandelierType.Long")] [assembly: SuppressMessage("Naming", "CA1720:Identifier contains type name" @@ -34,39 +25,14 @@ Scope = "member", Target = "~F:Skender.Stock.Indicators.ChandelierType.Short")] -[assembly: SuppressMessage( - "StyleCop.CSharp.SpacingRules", - "SA1008:Opening parenthesis should be spaced correctly", - Justification = "Not compatible with `or` statement (analyzer bug)", - Scope = "member", - Target = "~M:Skender.Stock.Indicators.ResultUtility.Condense``1(System.Collections.Generic.IEnumerable{``0})~System.Collections.Generic.IEnumerable{``0}")] - [assembly: SuppressMessage( "Maintainability", "CA1510:Use ArgumentNullException throw helper", Justification = "Can only use with .NET 6 or later. We support .NET Framework and .NET Standard.")] -[assembly: SuppressMessage( - "StyleCop.CSharp.SpacingRules", - "SA1010:Opening square brackets should be spaced correctly", - Justification = "Invalid for new C# 12 [ collection ] syntax.")] - -[assembly: SuppressMessage( - "Naming", - "CA1725:Parameter names should match base declaration", - Justification = "The microsoft OnError implementation uses reserved word Error", - Scope = "member", - Target = "~M:Skender.Stock.Indicators.QuoteObserver.OnError(System.Exception)")] - [assembly: SuppressMessage( "Naming", "CA1716:Identifiers should not match keywords", - Justification = "The microsoft OnError implementation uses reserved word Error", + Justification = "Temporary, during deprecation period.", Scope = "member", - Target = "~M:Skender.Stock.Indicators.QuoteObserver.OnError(System.Exception)")] - -[assembly: SuppressMessage( - "Naming", - "CA1716:Identifiers should not match keywords", - Justification = "The microsoft OnError implementation uses reserved word Error", - Scope = "member", Target = "~M:Skender.Stock.Indicators.TupleObserver.OnError(System.Exception)")] + Target = "~P:Skender.Stock.Indicators.ISeries.Date")] diff --git a/src/Indicators.csproj b/src/Indicators.csproj index d78154c16..c54bdd1e7 100644 --- a/src/Indicators.csproj +++ b/src/Indicators.csproj @@ -1,7 +1,7 @@ - net8.0;net7.0;net6.0;netstandard2.1;netstandard2.0 + net8.0 Dave Skender Stock Indicators for .NET diff --git a/src/_common/BinarySettings.cs b/src/_common/BinarySettings.cs new file mode 100644 index 000000000..4f8f9e63f --- /dev/null +++ b/src/_common/BinarySettings.cs @@ -0,0 +1,111 @@ +namespace Skender.Stock.Indicators; + +/// +/// Binary on/off switches for high performance access +/// to behaviors and characteristics. +/// +/// +/// Initializes a new instance of the struct. +/// The Mask parameter is optional and defaults to 0b11111111 where all bits +/// pass through to "combinor" sets. +/// +/// Example of accessing a specific bit: +/// +/// BinarySettings settings = new(0b00000001); // bit 0 is set to 1 +/// bool isBit0Set = settings[0]; // true +/// bool isBit1Set = settings[1]; // false +/// +/// +/// Example of re/setting a specific bit: +/// +/// BinarySettings settings = new(0); +/// settings = settings with { [0] = true }; // set bit 0 to true +/// settings = settings with { [1] = false }; // set bit 1 to false +/// +/// +/// +/// Binary settings. +/// Default is 0b00000000 (binary literal of 0). +/// +/// +/// Mask for settings inheritence. +/// Default is 0b11111111 (binary literal of 255). +/// +[Serializable] +public readonly struct BinarySettings( + byte settings, + byte mask = 0b11111111) : IEquatable +{ + public byte Settings { get; } = settings; + public byte Mask { get; } = mask; + + // use default settings (none) and mask + // important: this explicit parameterless ctor required for struct + public BinarySettings() : this(settings: 0b00000000) { } + + /// + /// Gets the value of the bit at the specified index. + /// + /// The index of the bit to get. + /// True if the bit is set; otherwise, false. + public bool this[short index] + => (Settings & (1 << index)) != 0; + + /// + /// Combines the current settings with another instance + /// using a bitwise OR operation, excluding the bits masked by the parent settings. + /// + /// The parent instance to combine with. + /// + /// A new instance with combined settings. + /// Notably, it does not modify the current read-only instance. + /// + /// + /// + /// The mask is used to determine which bits from the parent settings should be excluded + /// during the combination. By default, the mask is set to 0b11111111, meaning all bits + /// are included. If a different mask is provided, the corresponding bits in the parent + /// settings will be excluded based on the mask. + /// + /// In other words, the mask you provide on instantiation will determine which bits are + /// genetic material passed on to the "combinor" child settings. The child settings will inherit the + /// bits from the parent settings that the parent decides to pass along. + /// + /// + /// Usage example (default mask): + /// + /// BinarySettings srcSettings = new(0b01101001); + /// BinarySettings defSettings = new(0b00000010); + /// BinarySettings newSettings = defSettings.Combine(srcSettings); // result: 0b01101011 + /// + /// Using a custom mask: + /// + /// BinarySettings customMaskSettings = new(0b01101001, 0b11111110); // do not pass 0th bit value + /// BinarySettings newSettingsWithCustomMask = defSettings.Combine(customMaskSettings); // result: 0b01101010 + /// + /// + public BinarySettings Combine(BinarySettings parentSettings) + { + // add parent bits according to their mask template + byte maskedParentSettings = (byte)(parentSettings.Settings & parentSettings.Mask); + + // combine the settings + return new BinarySettings((byte)(Settings | maskedParentSettings), parentSettings.Mask); + } + + public override bool Equals(object? obj) + => obj is BinarySettings other && Equals(other); + + public bool Equals(BinarySettings other) + => Settings == other.Settings && Mask == other.Mask; + + public override int GetHashCode() + => HashCode.Combine(Settings, Mask); + + public static bool operator ==(BinarySettings left, BinarySettings right) + => left.Equals(right); + + public static bool operator !=(BinarySettings left, BinarySettings right) + => !(left == right); +} + diff --git a/src/_common/Candles/Candles.Models.cs b/src/_common/Candles/Candles.Models.cs new file mode 100644 index 000000000..3ed861d29 --- /dev/null +++ b/src/_common/Candles/Candles.Models.cs @@ -0,0 +1,63 @@ +namespace Skender.Stock.Indicators; + +// CANDLESTICK MODELS + +[Serializable] +public record CandleProperties +( + DateTime Timestamp, + decimal Open, + decimal High, + decimal Low, + decimal Close, + decimal Volume +) : Quote(Timestamp, Open, High, Low, Close, Volume) +{ + // raw sizes + public decimal? Size => High - Low; + public decimal? Body => Open > Close ? Open - Close : Close - Open; + public decimal? UpperWick => High - (Open > Close ? Open : Close); + public decimal? LowerWick => (Open > Close ? Close : Open) - Low; + + // percent sizes + public double? BodyPct => Size != 0 ? (double?)(Body / Size) : 1; + public double? UpperWickPct => Size != 0 ? (double?)(UpperWick / Size) : 1; + public double? LowerWickPct => Size != 0 ? (double?)(LowerWick / Size) : 1; + + // directional info + public bool IsBullish => Close > Open; + public bool IsBearish => Close < Open; +} + +[Serializable] +public record CandleResult : ISeries +{ + public CandleResult( + DateTime timestamp, + IQuote quote, + Match match, + decimal? price) + { + Timestamp = timestamp; + Price = price; + Match = match; + Candle = quote.ToCandle(); + } + + public CandleResult( + DateTime timestamp, + CandleProperties candle, + Match match, + decimal? price) + { + Timestamp = timestamp; + Price = price; + Match = match; + Candle = candle; + } + + public DateTime Timestamp { get; init; } + public decimal? Price { get; init; } + public Match Match { get; init; } + public CandleProperties Candle { get; init; } +} diff --git a/src/_common/Candles/Candles.Utilities.cs b/src/_common/Candles/Candles.Utilities.cs new file mode 100644 index 000000000..82d278637 --- /dev/null +++ b/src/_common/Candles/Candles.Utilities.cs @@ -0,0 +1,27 @@ +namespace Skender.Stock.Indicators; + +public static partial class Utility +{ + public static IReadOnlyList Condense( + this IReadOnlyList candleResults) => candleResults + .Where(candle => candle.Match != Match.None) + .ToList(); + + public static CandleProperties ToCandle( + this TQuote quote) + where TQuote : IQuote => new( + Timestamp: quote.Timestamp, + Open: quote.Open, + High: quote.High, + Low: quote.Low, + Close: quote.Close, + Volume: quote.Volume); + + // convert/sort quotes into candles list + public static IReadOnlyList ToCandles( + this IReadOnlyList quotes) + where TQuote : IQuote => quotes + .Select(x => x.ToCandle()) + .OrderBy(x => x.Timestamp) + .ToList(); +} diff --git a/src/_common/Candles/Candles.cs b/src/_common/Candles/Candles.cs deleted file mode 100644 index bda0ef617..000000000 --- a/src/_common/Candles/Candles.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Skender.Stock.Indicators; - -public static class Candlesticks -{ - public static IEnumerable Condense( - this IEnumerable candleResults) => candleResults - .Where(candle => candle.Match != Match.None) - .ToList(); - - public static CandleProperties ToCandle( - this TQuote quote) - where TQuote : IQuote => new() { - Date = quote.Date, - Open = quote.Open, - High = quote.High, - Low = quote.Low, - Close = quote.Close, - Volume = quote.Volume - }; - - // convert/sort quotes into candles list - public static IEnumerable ToCandles( - this IEnumerable quotes) - where TQuote : IQuote - { - List candlesList = quotes - .Select(x => x.ToCandle()) - .OrderBy(x => x.Date) - .ToList(); - - // validate - return candlesList; - } - - // convert/sort quotes into candle results - internal static List ToCandleResults( - this IEnumerable quotes) - where TQuote : IQuote - { - List candlesList = quotes - .Select(x => new CandleResult(x.Date) { - Match = Match.None, - Candle = x.ToCandle() - }) - .OrderBy(x => x.Date) - .ToList(); - - // validate - return candlesList; - } -} diff --git a/src/_common/Candles/Models.cs b/src/_common/Candles/Models.cs deleted file mode 100644 index f8faeb996..000000000 --- a/src/_common/Candles/Models.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CANDLESTICK MODELS - -[Serializable] -public class CandleProperties : Quote -{ - // raw sizes - public decimal? Size => High - Low; - public decimal? Body => (Open > Close) ? (Open - Close) : (Close - Open); - public decimal? UpperWick => High - (Open > Close ? Open : Close); - public decimal? LowerWick => (Open > Close ? Close : Open) - Low; - - // percent sizes - public double? BodyPct => (Size != 0) ? (double?)(Body / Size) : 1; - public double? UpperWickPct => (Size != 0) ? (double?)(UpperWick / Size) : 1; - public double? LowerWickPct => (Size != 0) ? (double?)(LowerWick / Size) : 1; - - // directional info - public bool IsBullish => Close > Open; - public bool IsBearish => Close < Open; -} - -[Serializable] -public class CandleResult : ResultBase -{ - public CandleResult(DateTime date) - { - Date = date; - Candle = new CandleProperties(); - } - - public decimal? Price { get; set; } - public Match Match { get; set; } - public CandleProperties Candle { get; set; } -} diff --git a/src/_common/Enums.cs b/src/_common/Enums.cs index ba68801c3..f254baaea 100644 --- a/src/_common/Enums.cs +++ b/src/_common/Enums.cs @@ -1,8 +1,37 @@ namespace Skender.Stock.Indicators; // SHARED ENUMERATIONS -// note: indicator unique ENUMS specified in indicator models +// note: indicator unique ENUMS filed with their models +/// +/// Cache action instruction or outcome +/// +internal enum Act +{ + /// + /// Adds item to end of cache or rebuild if older. + /// + Add, + + /// + /// Does nothing to cache (aborted). + /// + Ignore, + + /// + /// Insert item without rebuilding cache. + /// + Insert, + + /// + /// Reset and rebuild from marker position. + /// + Rebuild +} + +/// +/// Part or value of a quote candle +/// public enum CandlePart { Open, @@ -17,12 +46,18 @@ public enum CandlePart OHLC4 } +/// +/// Candle close or high/low wick values +/// public enum EndType { Close = 0, HighLow = 1 } +/// +/// Candlestick pattern matching type +/// public enum Match { BullConfirmed = 200, @@ -35,6 +70,9 @@ public enum Match BearConfirmed = -200 } +/// +/// Moving average type +/// public enum MaType { ALMA, @@ -50,6 +88,10 @@ public enum MaType WMA } +/// +/// Period size. Usually referring to the +/// time period represented in a quote candle. +/// public enum PeriodSize { Month, @@ -65,11 +107,3 @@ public enum PeriodSize TwoMinutes, OneMinute } - -public enum SyncType -{ - Prepend, - AppendOnly, - RemoveOnly, - FullMatch -} diff --git a/src/_common/Generics/Pruning.cs b/src/_common/Generics/Pruning.cs index 1cdc85e16..997957a91 100644 --- a/src/_common/Generics/Pruning.cs +++ b/src/_common/Generics/Pruning.cs @@ -1,13 +1,12 @@ namespace Skender.Stock.Indicators; // REMOVE AND PRUNING of SERIES -public static class Pruning + +public static partial class Utility { // REMOVE SPECIFIC PERIODS - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable series, + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList series, int removePeriods) => removePeriods < 0 ? throw new ArgumentOutOfRangeException(nameof(removePeriods), removePeriods, @@ -16,7 +15,7 @@ public static IEnumerable RemoveWarmupPeriods( // REMOVE PERIODS internal static List Remove( - this IEnumerable series, + this IReadOnlyList series, int removePeriods) { List seriesList = series.ToList(); @@ -25,17 +24,17 @@ internal static List Remove( { return []; } - else - { - if (removePeriods > 0) - { - for (int i = 0; i < removePeriods; i++) - { - seriesList.RemoveAt(0); - } - } + if (removePeriods <= 0) + { return seriesList; } + + for (int i = 0; i < removePeriods; i++) + { + seriesList.RemoveAt(0); + } + + return seriesList; } } diff --git a/src/_common/Generics/Seek.cs b/src/_common/Generics/Seek.cs index 0b0387b71..8397739af 100644 --- a/src/_common/Generics/Seek.cs +++ b/src/_common/Generics/Seek.cs @@ -4,22 +4,37 @@ namespace Skender.Stock.Indicators; public static class Seeking { - // FIND SERIES by DATE - /// - /// - public static TSeries? Find( - this IEnumerable series, + // FIND by DATE + public static T? Find( + this IReadOnlyList series, DateTime lookupDate) - where TSeries : ISeries => series - .FirstOrDefault(x => x.Date == lookupDate); + where T : ISeries + { + ArgumentNullException.ThrowIfNull(series); - // FIND INDEX by DATE - /// - /// - public static int FindIndex( - this List series, - DateTime lookupDate) - where TSeries : ISeries => series == null - ? -1 - : series.FindIndex(x => x.Date == lookupDate); + int low = 0; + int high = series.Count - 1; + + while (low <= high) + { + int mid = (low + high) >> 1; + DateTime midTimestamp = series[mid].Timestamp; + + if (midTimestamp == lookupDate) + { + return series[mid]; // found + } + else if (midTimestamp < lookupDate) + { + low = mid + 1; + } + else + { + high = mid - 1; + } + } + + // not found + return default; + } } diff --git a/src/_common/Generics/Series.Model.cs b/src/_common/Generics/Series.Model.cs deleted file mode 100644 index 8f0c4e77e..000000000 --- a/src/_common/Generics/Series.Model.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Skender.Stock.Indicators; - -public interface ISeries -{ - public DateTime Date { get; } -} diff --git a/src/_common/Generics/Sorting.cs b/src/_common/Generics/Sorting.cs index 49522ed3e..56ed632c6 100644 --- a/src/_common/Generics/Sorting.cs +++ b/src/_common/Generics/Sorting.cs @@ -1,22 +1,13 @@ -using System.Collections.ObjectModel; - namespace Skender.Stock.Indicators; // SORTED of SERIES -public static class Sorting +public static partial class Utility { - public static Collection ToSortedCollection( - this IEnumerable series) - where TSeries : ISeries - => series - .OrderBy(x => x.Date) - .ToCollection(); - - internal static List ToSortedList( + public static IReadOnlyList ToSortedList( this IEnumerable series) where TSeries : ISeries => series - .OrderBy(x => x.Date) + .OrderBy(x => x.Timestamp) .ToList(); } diff --git a/src/_common/Generics/Transforms.cs b/src/_common/Generics/Transforms.cs index 2e7385dc9..11e6b3ffd 100644 --- a/src/_common/Generics/Transforms.cs +++ b/src/_common/Generics/Transforms.cs @@ -4,15 +4,12 @@ namespace Skender.Stock.Indicators; // GENERIC TRANSFORMS -public static class Transforms +public static partial class Utility { // TO COLLECTION internal static Collection ToCollection(this IEnumerable source) { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } + ArgumentNullException.ThrowIfNull(source); Collection collection = [.. source]; diff --git a/src/_common/Generics/info.xml b/src/_common/Generics/info.xml deleted file mode 100644 index 3d2e60d7a..000000000 --- a/src/_common/Generics/info.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - Finds time series values on a specific date. - - See documentation for more information. - - - Any series type. - Time series to evaluate. - Exact date to lookup. - First record in the series on the date specified. - - - - Finds time series index on a specific date. - - See documentation for more information. - - - Any series type. - Time series to evaluate. - Exact date to lookup. - - First index in the series of the date specified or -1 if not found. - - - - Removes a specific quantity from the beginning of the time series list. - - See documentation for more information. - - - Any series type. - Collection to evaluate. - Exact quantity to remove from the beginning of the series. - Time series, pruned. - Invalid parameter value provided. - - \ No newline at end of file diff --git a/src/_common/Globals.cs b/src/_common/Globals.cs index 8d9c8893a..620494b62 100644 --- a/src/_common/Globals.cs +++ b/src/_common/Globals.cs @@ -1,24 +1,5 @@ -using System.Globalization; using System.Runtime.CompilerServices; [assembly: CLSCompliant(true)] -[assembly: InternalsVisibleTo("Tests.Indicators")] +[assembly: InternalsVisibleTo("Tests.Indicators")] // these test internals [assembly: InternalsVisibleTo("Tests.Performance")] - -namespace Skender.Stock.Indicators; - -/// Technical indicators and overlays. See -/// -/// the Guide for more information. -public static partial class Indicator -{ - private static readonly CultureInfo invCulture = CultureInfo.InvariantCulture; - private static readonly Calendar invCalendar = invCulture.Calendar; - - // Gets the DTFI properties required by GetWeekOfYear. - private static readonly CalendarWeekRule invCalendarWeekRule - = invCulture.DateTimeFormat.CalendarWeekRule; - - private static readonly DayOfWeek invFirstDayOfWeek - = invCulture.DateTimeFormat.FirstDayOfWeek; -} diff --git a/src/_common/ISeries.cs b/src/_common/ISeries.cs new file mode 100644 index 000000000..3483eddde --- /dev/null +++ b/src/_common/ISeries.cs @@ -0,0 +1,24 @@ +namespace Skender.Stock.Indicators; + +/// +/// Time-series base interface. +/// +public interface ISeries +{ + // TODO: consider adding (long) UnixDate (seconds) to ISeries + + /// + /// Date/time of record. + /// + /// + /// For types, this is the + /// date/time from the matching aggregate quote period. + /// For types, this is the + /// Close/end date and time of the OHLCV aggregate period. + /// From a practical perspective, Timestamp is the correlation ID. + /// + DateTime Timestamp { get; } + + [Obsolete("Deprecated. Use 'Timestamp' instead.")] + DateTime Date => Timestamp; +} diff --git a/src/_common/Incrementals/IIncremental.cs b/src/_common/Incrementals/IIncremental.cs new file mode 100644 index 000000000..90de40106 --- /dev/null +++ b/src/_common/Incrementals/IIncremental.cs @@ -0,0 +1,52 @@ +namespace Skender.Stock.Indicators; + +public interface IAddReusable +{ + /// + /// Converts an incremental value into + /// the next incremental indicator value + /// and added it to the list. + /// + /// Date context + /// Next value + void Add(DateTime timestamp, double value); + + /// + /// Converts an incremental reusable value into + /// the next incremental indicator value + /// and added it to the list. + /// + /// Next value + void Add(IReusable value); + + /// + /// Converts batch of reusable values into + /// the next incremental indicator values + /// and added them to the list. + /// + /// + /// Chronologically ordered batch of IReusable info + /// + void Add(IReadOnlyList values); +} + +public interface IAddQuote +{ + /// + /// Converts an incremental quote into + /// the next incremental indicator value + /// and added it to the list. + /// + /// Next quote value + void Add(IQuote quote); + + /// + /// Converts batch of quotes into + /// the next incremental indicator values + /// and added them to the list. + /// + /// + /// Chronologically ordered batch of quotes + /// + void Add(IReadOnlyList quotes); +} diff --git a/src/_common/Math/NullMath.cs b/src/_common/Math/NullMath.cs index 71aad8c1c..a9a197922 100644 --- a/src/_common/Math/NullMath.cs +++ b/src/_common/Math/NullMath.cs @@ -1,23 +1,28 @@ namespace Skender.Stock.Indicators; -// NULLABLE SYSTEM.MATH -// System.Math does not allow or handle null input values. -// Instead of putting a lot of inline defensive code -// we're building nullable equivalents here. +/// +/// Nullable System. functions. +/// +/// +/// System.Math infamously does not allow +/// or handle nullable input values. +/// Instead of adding repetitive inline defensive code, +/// we're using these equivalents. Most are simple wrappers. +/// public static class NullMath { public static double? Abs(this double? value) - => (value is null) + => value is null ? null : value < 0 ? (double)-value : (double)value; public static decimal? Round(this decimal? value, int digits) - => (value is null) + => value is null ? null : Math.Round((decimal)value, digits); public static double? Round(this double? value, int digits) - => (value is null) + => value is null ? null : Math.Round((double)value, digits); @@ -30,8 +35,13 @@ public static decimal Round(this decimal value, int digits) public static double Null2NaN(this double? value) => value ?? double.NaN; + public static double Null2NaN(this decimal? value) + => value is null + ? double.NaN + : (double)value; + public static double? NaN2Null(this double? value) - => (value is not null and double.NaN) + => value is double.NaN ? null : value; diff --git a/src/_common/Math/Numerix.cs b/src/_common/Math/Numerical.cs similarity index 70% rename from src/_common/Math/Numerix.cs rename to src/_common/Math/Numerical.cs index b41698b0b..bba184344 100644 --- a/src/_common/Math/Numerix.cs +++ b/src/_common/Math/Numerical.cs @@ -1,58 +1,50 @@ namespace Skender.Stock.Indicators; -public static class Numerix +public static class Numerical { // STANDARD DEVIATION public static double StdDev(this double[] values) { - // validate parameters - if (values is null) + ArgumentNullException.ThrowIfNull( + values, "StdDev values cannot be null."); + + int n = values.Length; + + if (n <= 1) { - throw new ArgumentNullException(nameof(values), "StdDev values cannot be null."); + return 0; } - double sd = 0; - int n = values.Length; - if (n > 1) + double sum = 0; + + for (int i = 0; i < n; i++) { - double sum = 0; - for (int i = 0; i < n; i++) - { - sum += values[i]; - } - - double avg = sum / n; - - double sumSq = 0; - for (int i = 0; i < n; i++) - { - double v = values[i]; - sumSq += (v - avg) * (v - avg); - } - - sd = Math.Sqrt(sumSq / n); + sum += values[i]; } - return sd; + double avg = sum / n; + + double sumSq = 0; + for (int i = 0; i < n; i++) + { + double v = values[i]; + sumSq += (v - avg) * (v - avg); + } + + return Math.Sqrt(sumSq / n); } // SLOPE of BEST FIT LINE public static double Slope(double[] x, double[] y) { // validate parameters - if (x is null) - { - throw new ArgumentNullException(nameof(x), "Slope X values cannot be null."); - } - - if (y is null) - { - throw new ArgumentNullException(nameof(y), "Slope Y values cannot be null."); - } + ArgumentNullException.ThrowIfNull(x, "Slope X values cannot be null."); + ArgumentNullException.ThrowIfNull(y, "Slope Y values cannot be null."); if (x.Length != y.Length) { - throw new ArgumentException("Slope X and Y arrays must be the same size."); + throw new ArgumentException( + "Slope X and Y arrays must be the same size."); } int length = x.Length; @@ -89,7 +81,8 @@ public static double Slope(double[] x, double[] y) } // DATE ROUNDING - internal static DateTime RoundDown(this DateTime dateTime, TimeSpan interval) + internal static DateTime RoundDown( + this DateTime dateTime, TimeSpan interval) => interval == TimeSpan.Zero ? dateTime : dateTime diff --git a/src/_common/Observables/ChainProvider.cs b/src/_common/Observables/ChainProvider.cs deleted file mode 100644 index 85c2b3af4..000000000 --- a/src/_common/Observables/ChainProvider.cs +++ /dev/null @@ -1,194 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TUPLE OBSERVER and TUPLE PROVIDER (CHAIN STREAM) - -public abstract class ChainProvider - : TupleObserver, IObservable<(DateTime Date, double Value)> -{ - // fields - private readonly List> observers; - - // initialize - protected ChainProvider() - { - observers = []; - ProtectedChain = []; - Warmup = true; - } - - // properties - internal IEnumerable<(DateTime Date, double Value)> Output => ProtectedChain; - - internal List<(DateTime Date, double Value)> ProtectedChain { get; set; } - - private int OverflowCount { get; set; } - - private bool Warmup { get; set; } - - // METHODS - - // subscribe observer - public IDisposable Subscribe(IObserver<(DateTime Date, double Value)> observer) - { - if (!observers.Contains(observer)) - { - observers.Add(observer); - } - - return new Unsubscriber(observers, observer); - } - - // close all observations - public void EndTransmission() - { - foreach (IObserver<(DateTime Date, double Value)> observer in observers.ToArray()) - { - if (observers.Contains(observer)) - { - observer.OnCompleted(); - } - } - - observers.Clear(); - } - - // add one - internal void SendToChain(TResult result) - where TResult : IReusableResult - { - // candidate result - (DateTime Date, double Value) r = new(result.Date, result.Value.Null2NaN()); - - int length = ProtectedChain.Count; - - // initialize - if (length == 0 && result.Value != null) - { - // add new tuple - ProtectedChain.Add(r); - Warmup = false; - - // notify observers - NotifyObservers(r); - return; - } - - // do not proceed until first non-null Value recieved - if (Warmup && result.Value == null) - { - return; - } - else - { - Warmup = false; - } - - (DateTime lastDate, _) = ProtectedChain[length - 1]; - - // add tuple - if (r.Date > lastDate) - { - // add new tuple - ProtectedChain.Add(r); - - // notify observers - NotifyObservers(r); - } - - // same date or tuple recieved - else if (r.Date <= lastDate) - { - // check for overflow condition - // where same tuple continues (possible circular condition) - if (r.Date == lastDate) - { - OverflowCount++; - - if (OverflowCount > 100) - { - string msg = "A repeated Chain update exceeded the 100 attempt threshold. " - + "Check and remove circular chains or check your Chain provider."; - - EndTransmission(); - - throw new OverflowException(msg); - } - } - else - { - OverflowCount = 0; - } - - // seek old tuple - int foundIndex = ProtectedChain - .FindIndex(x => x.Date == r.Date); - - // found - if (foundIndex >= 0) - { - ProtectedChain[foundIndex] = r; - } - - // add missing tuple - else - { - ProtectedChain.Add(r); - - // re-sort cache - ProtectedChain = ProtectedChain - .ToSortedList(); - } - - // let observer handle old + duplicates - NotifyObservers(r); - } - } - - // add many - internal void SendToChain(IEnumerable results) - where TResult : IReusableResult - { - List added = results - .ToSortedList(); - - for (int i = 0; i < added.Count; i++) - { - SendToChain(added[i]); - } - } - - // notify observers - private void NotifyObservers((DateTime Date, double Value) tuple) - { - List> obsList = observers.ToList(); - - for (int i = 0; i < obsList.Count; i++) - { - IObserver<(DateTime Date, double Value)> obs = obsList[i]; - obs.OnNext(tuple); - } - } - - // unsubscriber - private class Unsubscriber : IDisposable - { - private readonly List> observers; - private readonly IObserver<(DateTime Date, double Value)> observer; - - // identify and save observer - public Unsubscriber(List> observers, IObserver<(DateTime Date, double Value)> observer) - { - this.observers = observers; - this.observer = observer; - } - - // remove single observer - public void Dispose() - { - if (observer != null && observers.Contains(observer)) - { - observers.Remove(observer); - } - } - } -} diff --git a/src/_common/Observables/IStreamHub.cs b/src/_common/Observables/IStreamHub.cs new file mode 100644 index 000000000..f73b1250d --- /dev/null +++ b/src/_common/Observables/IStreamHub.cs @@ -0,0 +1,128 @@ +namespace Skender.Stock.Indicators; + +// STREAM HUB INTERFACE + +/// +/// Streaming hub: management of observer +/// and observable indicator data +/// +/// +/// Type of inbound provider data. +/// +/// +/// Type of outbound indicator data. +/// +public interface IStreamHub + where TIn : ISeries +{ + /// + /// Read-only list of the stored cache values. + /// + IReadOnlyList Results { get; } + + /// + /// The cache and provider failed and is no longer operational. + /// + /// + /// This occurs when there is an overflow condition + /// from a circular chain or + /// when there were too many sequential duplicates. + /// + /// Use + /// to remove this flag. + /// + /// + bool IsFaulted { get; } + + /// + /// Resets the flag and + /// overflow counter. Use this after recovering + /// from an error. + /// + /// + /// You may also need to + /// , or + /// . + /// + void ResetFault(); + + /// + /// Add a single new item. + /// We'll determine if it's new or an update. + /// + /// + /// New item to add + /// + void Add(TIn newIn); + + /// + /// Add a batch of new items. + /// We'll determine if they're new or updated. + /// + /// + /// Batch of new items to add + /// + void Add(IEnumerable batchIn); + + /// + /// Insert a new item without rebuilding the cache. + /// + /// + /// This is used in situations when inserting an older item + /// and where newer cache entries do not need to be rebuilt. + /// Typically, this is only used for provider-only hubs. + /// + /// + /// Item to insert + /// + void Insert(TIn newIn); + + /// + /// Delete an item from the cache. + /// + /// Cached item to delete + /// + void Remove(TOut cachedItem); + + /// + /// Delete an item from the cache, from a specific position. + /// + /// Position in cache to delete + /// + void RemoveAt(int cacheIndex); + + /// + /// Deletes newer cached records from point in time (inclusive). + /// + /// + /// For observers, if your intention is to rebuild from a provider, + /// use alternate . + /// + /// + /// All periods (inclusive) after this DateTime will be removed. + /// + /// + /// Notify subscribers of the delete point. + /// + void RemoveRange(DateTime fromTimestamp, bool notify); + + /// + /// Deletes newer cached records from an index position (inclusive). + /// + /// + /// For observers, if your intention is to rebuild from a provider, + /// use alternate . + /// + /// From index, inclusive + /// + /// Notify subscribers of the delete position. + /// + void RemoveRange(int fromIndex, bool notify); + + /// + /// Returns a short text label for the hub + /// with parameter values, e.g. "EMA(10)" + /// + /// String label + string ToString(); +} diff --git a/src/_common/Observables/IStreamObservable.cs b/src/_common/Observables/IStreamObservable.cs new file mode 100644 index 000000000..394678c6f --- /dev/null +++ b/src/_common/Observables/IStreamObservable.cs @@ -0,0 +1,108 @@ +namespace Skender.Stock.Indicators; + +// STREAM (OBSERVABLE) INTERFACE + +#region chain and quote variants + +/// +public interface IQuoteProvider : IChainProvider + where T : IQuote +{ + IReadOnlyList Quotes { get; } +} + +/// +public interface IChainProvider : IStreamObservable + where T : IReusable; +#endregion + +/// +/// Provider of data + management of and notification to observing subscribers. +/// +/// +/// The object that provides notification information. +/// +public interface IStreamObservable +{ + /// + /// Hub observable properties and behaviors. + /// + /// + /// This struct holds cumulative overrides for + /// a streaming hub. Observer hubs inherit these values cumulatively when + /// instantiated as a , except where masked. + /// + /// Default settings are a binary set of 0b00000000, where 1 values represent + /// exceptional (atypical) behaviors. + /// + /// + /// + /// 0 + /// + /// Disable observer: a non-observing observable (e.g. base provider). + /// + /// + /// + /// 1 + /// + /// Allow duplicates: + /// bypass rebuild analysis and duplicate prevention when caching new results. + /// + /// + /// + /// 2-7 + /// [unused positions] + /// + /// + /// + BinarySettings Properties { get; } + + /// + /// Current number of subscribers + /// + int ObserverCount { get; } + + /// + /// Provider currently has subscribers + /// + bool HasObservers { get; } + + /// + /// Checks if a specific observer is subscribed + /// + /// + /// Subscriber IStreamObserver reference + /// + /// True if subscribed/registered + bool HasSubscriber(IStreamObserver observer); + + /// + /// Notifies the provider that an observer is to receive notifications. + /// + /// + /// The object that is to receive notifications. + /// + /// + /// A reference to an interface that allows observers + /// to stop receiving notifications before the provider + /// has finished sending them. + /// + IDisposable Subscribe(IStreamObserver observer); + + /// + /// Unsubscribe from the data provider. + /// + /// + bool Unsubscribe(IStreamObserver observer); + + /// + /// Unsubscribe all observers (subscribers) + /// + void EndTransmission(); + + /// + /// Get a readonly reference of the observable cache. + /// + /// Read-only list of cached items. + IReadOnlyList GetCacheRef(); +} diff --git a/src/_common/Observables/IStreamObserver.cs b/src/_common/Observables/IStreamObserver.cs new file mode 100644 index 000000000..32398e668 --- /dev/null +++ b/src/_common/Observables/IStreamObserver.cs @@ -0,0 +1,117 @@ +namespace Skender.Stock.Indicators; + +// STREAM (OBSERVER) INTERFACE + +/// +/// Management of observing + processing of streamed inbound data. +/// +/// +/// The object that provides notification information. +/// +public interface IStreamObserver +{ + /// + /// Current state of subscription to provider. + /// + bool IsSubscribed { get; } + + /// + /// Unsubscribe from the data provider. + /// + void Unsubscribe(); + + /// + /// Provides the observer with new data. + /// + /// + /// The current notification information. + /// + /// + /// Notify subscribers of the new item. + /// + /// + /// Provider index hint, if known. + /// + void OnAdd(T item, bool notify, int? indexHint); + + /// + /// Provides the observer with starting point in timeline + /// to rebuild and cascade to all its own subscribers. + /// + /// + /// Starting point in timeline to rebuild. + /// + void OnChange(DateTime fromTimestamp); + + /// + /// Provides the observer with errors from the provider + /// that have produced a faulted state. + /// + /// + /// An exception with additional information about the error. + /// + void OnError(Exception exception); + + /// + /// Provides the observer with final notice that the data + /// provider has finished sending push-based notifications. + /// + /// + /// Completion indicates that publisher will never send + /// additional data. This is only used for finite data + /// streams; and is different from faulted OnError(). + /// + void OnCompleted(); + + /// + /// Full reset of the provider subscription. + /// + /// + /// This unsubscribes from the provider, + /// rebuilds the cache, resets faulted states, + /// and then re-subscribes to the provider. + /// + /// This is done automatically on hub + /// instantiation, so it's only needed if you + /// want to manually reset the hub. + /// + /// + /// If you only need to rebuild the cache, + /// use instead. + /// + /// + void Reinitialize(); + + /// + /// Resets the entire results cache + /// and rebuilds it from provider sources, + /// with cascading updates to subscribers. + /// + /// + /// This is different from . + /// It does not reset the provider subscription. + /// + void Rebuild(); + + /// + /// Resets the results cache from a point in time + /// and rebuilds it from provider sources, + /// with cascading updates to subscribers. + /// + /// + /// All periods (inclusive) after this date/time + /// will be removed and recalculated. + /// + void Rebuild(DateTime fromTimestamp); + + /// + /// Resets the results cache from an index position + /// and rebuilds it from provider sources, + /// with cascading updates to subscribers. + /// + /// + /// All periods (inclusive) after this index position + /// will be removed and recalculated. + /// + void Rebuild(int fromIndex); +} diff --git a/src/_common/Observables/QuoteObserver.cs b/src/_common/Observables/QuoteObserver.cs deleted file mode 100644 index 628364594..000000000 --- a/src/_common/Observables/QuoteObserver.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Skender.Stock.Indicators; - -// OBSERVER of QUOTES (BOILERPLATE) - -public abstract class QuoteObserver : IObserver -{ - // fields - private IDisposable? unsubscriber; - - // properites - internal QuoteProvider? Supplier { get; set; } - - // methods - public virtual void Subscribe() - => unsubscriber = Supplier != null - ? Supplier.Subscribe(this) - : throw new ArgumentNullException(nameof(Supplier)); - - public virtual void OnCompleted() => Unsubscribe(); - - public virtual void OnError(Exception error) => throw error; - - public virtual void OnNext(Quote value) - { - // » handle new quote with override in observer - } - - public virtual void Unsubscribe() => unsubscriber?.Dispose(); -} diff --git a/src/_common/Observables/QuoteProvider.cs b/src/_common/Observables/QuoteProvider.cs deleted file mode 100644 index 9f55669fb..000000000 --- a/src/_common/Observables/QuoteProvider.cs +++ /dev/null @@ -1,186 +0,0 @@ -namespace Skender.Stock.Indicators; - -// QUOTES as PROVIDER - -public class QuoteProvider : IObservable -{ - // fields - private readonly List> observers; - - // initialize - public QuoteProvider() - { - observers = []; - ProtectedQuotes = []; - } - - // properties - public IEnumerable Quotes => ProtectedQuotes; - - internal List ProtectedQuotes { get; private set; } - - private int OverflowCount { get; set; } - - // METHODS - - // add one - public void Add(Quote quote) - { - // validate quote - if (quote == null) - { - throw new ArgumentNullException(nameof(quote), "Quote cannot be null."); - } - - int length = ProtectedQuotes.Count; - - if (length == 0) - { - // add new quote - ProtectedQuotes.Add(quote); - - // notify observers - NotifyObservers(quote); - - return; - } - - Quote last = ProtectedQuotes[length - 1]; - - // add quote - if (quote.Date > last.Date) - { - // add new quote - ProtectedQuotes.Add(quote); - - // notify observers - NotifyObservers(quote); - } - - // same date or quote recieved - else if (quote.Date <= last.Date) - { - // check for overflow condition - // where same quote continues (possible circular condition) - if (quote.Date == last.Date) - { - OverflowCount++; - - if (OverflowCount > 100) - { - string msg = "A repeated Quote update exceeded the 100 attempt threshold. " - + "Check and remove circular chains or check your Quote provider."; - - EndTransmission(); - - throw new OverflowException(msg); - } - } - else - { - OverflowCount = 0; - } - - // seek old quote - int foundIndex = ProtectedQuotes - .FindIndex(x => x.Date == quote.Date); - - // found - if (foundIndex >= 0) - { - Quote old = ProtectedQuotes[foundIndex]; - - old.Open = quote.Open; - old.High = quote.High; - old.Low = quote.Low; - old.Close = quote.Close; - old.Volume = quote.Volume; - } - - // add missing quote - else - { - ProtectedQuotes.Add(quote); - - // re-sort cache - ProtectedQuotes = ProtectedQuotes - .ToSortedList(); - } - - // let observer handle old + duplicates - NotifyObservers(quote); - } - } - - // add many - public void Add(IEnumerable quotes) - { - List added = quotes - .ToSortedList(); - - for (int i = 0; i < added.Count; i++) - { - Add(added[i]); - } - } - - // subscribe observer - public IDisposable Subscribe(IObserver observer) - { - if (!observers.Contains(observer)) - { - observers.Add(observer); - } - - return new Unsubscriber(observers, observer); - } - - // close all observations - public void EndTransmission() - { - foreach (IObserver observer in observers.ToArray()) - { - if (observers.Contains(observer)) - { - observer.OnCompleted(); - } - } - - observers.Clear(); - } - - // notify observers - private void NotifyObservers(Quote quote) - { - List> obsList = observers.ToList(); - - for (int i = 0; i < obsList.Count; i++) - { - IObserver obs = obsList[i]; - obs.OnNext(quote); - } - } - - // unsubscriber - private class Unsubscriber : IDisposable - { - private readonly List> observers; - private readonly IObserver observer; - - // identify and save observer - public Unsubscriber(List> observers, IObserver observer) - { - this.observers = observers; - this.observer = observer; - } - - // remove single observer - public void Dispose() - { - if (observer != null && observers.Contains(observer)) - { - observers.Remove(observer); - } - } - } -} diff --git a/src/_common/Observables/StreamHub.Observable.cs b/src/_common/Observables/StreamHub.Observable.cs new file mode 100644 index 000000000..5f6ec2269 --- /dev/null +++ b/src/_common/Observables/StreamHub.Observable.cs @@ -0,0 +1,119 @@ +namespace Skender.Stock.Indicators; + +// STREAM HUB (OBSERVABLE) + +public abstract partial class StreamHub : IStreamObservable +{ + private readonly HashSet> _observers = new(); + + /// + public bool HasObservers => _observers.Count > 0; + + /// + public int ObserverCount => _observers.Count; + + /// + public IReadOnlyList ReadCache => Cache; + + /// + public virtual BinarySettings Properties { get; init; } = new(0); // default 0b00000000 + + #region SUBSCRIPTION SERVICES + + /// + public IDisposable Subscribe(IStreamObserver observer) + { + _observers.Add(observer); + return new Unsubscriber(_observers, observer); + } + + /// + public bool Unsubscribe(IStreamObserver observer) + => _observers.Remove(observer); + + /// + public bool HasSubscriber(IStreamObserver observer) + => _observers.Contains(observer); + + /// + /// A disposable subscription to the stream provider. + /// Unsubscribed with + /// + /// + /// Registry of all subscribers (by ref) + /// + /// + /// Your unique subscription as provided. + /// + private class Unsubscriber( + ISet> observers, + IStreamObserver observer) : IDisposable + { + private readonly ISet> _observers = observers; + private readonly IStreamObserver _observer = observer; + + /// + /// Remove single observer. + /// + public void Dispose() => _observers.Remove(_observer); + } + + /// + public void EndTransmission() + { + foreach (IStreamObserver observer + in _observers.ToArray()) + { + if (_observers.Contains(observer)) + { + // subscriber removes itself + observer.OnCompleted(); + } + } + + _observers.Clear(); + } + #endregion + + #region SUBSCRIBER NOTIFICATIONS + + /// + /// Sends new TSeries item to subscribers. + /// + /// TSeries item to send. + /// Provider index hint. + private void NotifyObserversOnAdd(TOut item, int? indexHint) + { + // send to subscribers + foreach (IStreamObserver o in _observers.ToArray()) + { + o.OnAdd(item, notify: true, indexHint); + } + } + + /// + /// Sends rebuilds point in time to all subscribers. + /// + /// Rebuild starting positions. + private void NotifyObserversOnChange(DateTime fromTimestamp) + { + foreach (IStreamObserver o in _observers.ToArray()) + { + o.OnChange(fromTimestamp); + } + } + + /// + /// Sends error (exception) to all subscribers. + /// + /// The exception to send. + private void NotifyObserversOnError(Exception exception) + { + // send to subscribers + foreach (IStreamObserver o in _observers.ToArray()) + { + o.OnError(exception); + } + } + #endregion +} diff --git a/src/_common/Observables/StreamHub.Observer.cs b/src/_common/Observables/StreamHub.Observer.cs new file mode 100644 index 000000000..c2b374336 --- /dev/null +++ b/src/_common/Observables/StreamHub.Observer.cs @@ -0,0 +1,66 @@ +namespace Skender.Stock.Indicators; + +// STREAM HUB (OBSERVER) + +public abstract partial class StreamHub : IStreamObserver +{ + /// + public bool IsSubscribed => Provider.HasSubscriber(this); + + /// + /// Data provider that this observer subscribes to. + /// + protected IStreamObservable Provider { get; init; } + + /// + /// Subscription token for managing the subscription lifecycle. + /// + private IDisposable? Subscription { get; set; } + + /// + /// Lock object to ensure thread safety during unsubscription. + /// + private readonly object _unsubscribeLock = new(); + + // Observer methods + + /// + public virtual void OnAdd(TIn item, bool notify, int? indexHint) + { + // Convert the input item to the output type and append it to the cache. + // Override this method if the input and output types are not indexed 1:1. + + (TOut result, int _) = ToIndicator(item, indexHint); // TODO: make this return array, loop appendation? + AppendCache(result, notify); + } + + /// + public void OnChange(DateTime fromTimestamp) + => Rebuild(fromTimestamp); + + /// + public void OnError(Exception exception) + => throw exception; + + /// + public void OnCompleted() + => Unsubscribe(); + + /// + public void Unsubscribe() + { + // Ensure thread-safety for EndTransmission > OnCompleted-type race conditions + // see https://learn.microsoft.com/en-us/dotnet/standard/events/observer-design-pattern-best-practices + + lock (_unsubscribeLock) + { + if (IsSubscribed) + { + Provider.Unsubscribe(this); + } + + Subscription?.Dispose(); + Subscription = null; // ensure the ref is cleared + } + } +} diff --git a/src/_common/Observables/StreamHub.Utilities.cs b/src/_common/Observables/StreamHub.Utilities.cs new file mode 100644 index 000000000..49b1c528c --- /dev/null +++ b/src/_common/Observables/StreamHub.Utilities.cs @@ -0,0 +1,201 @@ +namespace Skender.Stock.Indicators; + +// STREAM HUB (STATIC UTILITIES) + +public static class StreamHub +{ + /// + /// Try to find index position of the provided timestamp + /// + /// + /// Timestamp to seek + /// + /// Index of timestamp or -1 when not found + /// + /// True if found + internal static bool TryFindIndex( + this IReadOnlyList cache, + DateTime timestamp, + out int index) + where T : ISeries + { + index = cache.GetIndex(timestamp, false); + return index != -1; + } + + /// + /// Get the cache index based on item equality. + /// + /// + /// + /// Time-series object to find in cache + /// + /// + /// Throw exception when item is not found + /// + /// Index position + /// + /// When items is not found (should never happen). + /// + internal static int GetIndex( + this IReadOnlyList cache, + T cachedItem, + bool throwOnFail) + where T : ISeries + { + int low = 0; + int high = cache.Count - 1; + int firstMatchIndex = -1; + DateTime targetTimestamp = cachedItem.Timestamp; + + while (low <= high) + { + int mid = (low + high) >> 1; + int comparison = cache[mid].Timestamp.CompareTo(targetTimestamp); + + if (comparison == 0) + { + // Found a match by Timestamp, + // store the index of the first match + if (firstMatchIndex == -1) + { + firstMatchIndex = mid; + } + + // Verify with Equals for an exact match + if (cache[mid].Equals(cachedItem)) + { + return mid; // exact match found + } + + high = mid - 1; // continue searching to the left + } + else if (comparison < 0) + { + low = mid + 1; + } + else + { + high = mid - 1; + } + } + + // If a timestamp match was found but no exact + // match, try to find an exact match in the range + // of duplicate timestamps (e.g. Renko bricks), + // biased towards later duplicates. + if (firstMatchIndex != -1) + { + // Find the last occurrence of the matching timestamp + for (int i = cache.Count - 1; i >= firstMatchIndex; i--) + { + if (cache[i].Timestamp == targetTimestamp + && cache[i].Equals(cachedItem)) + { + return i; // exact match found among duplicates + } + } + } + + // not found + return throwOnFail + ? throw new ArgumentException( + "Matching source history not found.", nameof(cachedItem)) + : -1; + } + + /// + /// Get the cache index based on a timestamp. + /// + /// + /// Only use this when you are looking for a point in time + /// without a matching item for context. In most cases + /// is more appropriate. + /// + /// + /// + /// Timestamp of cached item + /// + /// + /// Throw exception when timestamp is not found + /// + /// Index position + /// + /// When timestamp is not found (should never happen). + /// + internal static int GetIndex( + this IReadOnlyList cache, + DateTime timestamp, + bool throwOnFail) + where T : ISeries + { + int low = 0; + int high = cache.Count - 1; + + while (low <= high) + { + int mid = (low + high) >> 1; + DateTime midTimestamp = cache[mid].Timestamp; + + if (midTimestamp == timestamp) + { + return mid; + } + else if (midTimestamp < timestamp) + { + low = mid + 1; + } + else + { + high = mid - 1; + } + } + + // not found + return throwOnFail + ? throw new ArgumentException( + "Matching source history not found.", nameof(timestamp)) + : -1; + } + + /// + /// Get the first cache index on or after a timestamp. + /// + /// + /// Only use this when you are looking for a point in time + /// without a matching item for context. In most cases + /// is more appropriate. + /// + /// + /// + /// Timestamp of cached item + /// + /// First index position or -1 if not found + internal static int GetIndexGte( + this IReadOnlyList cache, + DateTime timestamp) + where T : ISeries + { + int low = 0; + int high = cache.Count; + while (low < high) + { + int mid = low + ((high - low) / 2); + if (cache[mid].Timestamp < timestamp) + { + low = mid + 1; + } + else + { + high = mid; + } + } + + // At this point, low is the index of the first + // element that is greater than or equal to timestamp + // or Cache.Count if all elements are less than timestamp. + // If low is equal to Cache.Count, it means there are + // no elements greater than or equal to timestamp. + return low < cache.Count ? low : -1; + } +} diff --git a/src/_common/Observables/StreamHub.cs b/src/_common/Observables/StreamHub.cs new file mode 100644 index 000000000..53c6302f4 --- /dev/null +++ b/src/_common/Observables/StreamHub.cs @@ -0,0 +1,408 @@ +namespace Skender.Stock.Indicators; + +// STREAM HUB (BASE/CACHE) + +/// +public abstract partial class StreamHub : IStreamHub + where TIn : ISeries + where TOut : ISeries +{ + #region constructor + + /// + /// Streaming data provider + /// + private protected StreamHub(IStreamObservable provider) + { + // store provider reference + Provider = provider; + + // set provider cache reference + ProviderCache = provider.GetCacheRef(); + + // inherit settings (reinstantiate struct on heap) + Properties = Properties.Combine(provider.Properties); + } + + #endregion + + #region PROPERTIES + + /// + public IReadOnlyList Results => Cache; + + /// + public bool IsFaulted { get; private set; } + + /// + /// Cache of stored values (base). + /// + internal List Cache { get; } = new(); + + /// + /// Current count of repeated caching attempts. + /// An overflow condition is triggered after 100. + /// + internal byte OverflowCount { get; private set; } + + /// + /// Reference to this hub's provider's cache. + /// + protected IReadOnlyList ProviderCache { get; } + + /// + /// Most recent item saved to cache. + /// + private TOut? LastItem { get; set; } + + #endregion + + // reset fault flag and condition + /// + public void ResetFault() + { + OverflowCount = 0; + IsFaulted = false; + } + + // fetch cache reference + /// + public IReadOnlyList GetCacheRef() => Cache; + + public abstract override string ToString(); + + /// + /// Converts incremental value into + /// an indicator candidate and cache position. + /// + /// New item from provider + /// Provider index hint + /// Cacheable item candidate and index hint + protected abstract (TOut result, int index) + ToIndicator(TIn item, int? indexHint); + + #region ADD & ANALYZE + + public void Add(TIn newIn) + => OnAdd(newIn, notify: true, null); + + public void Add(IEnumerable batchIn) + { + foreach (TIn newIn in batchIn.OrderBy(x => x.Timestamp)) + { + OnAdd(newIn, notify: true, null); + } + } + + public void Insert(TIn newIn) + { + // note: should only be used when newer timestamps + // are not impacted by the insertion of an older item + + // generate candidate result + (TOut result, int index) = ToIndicator(newIn, null); + + // insert, then rebuild observers (no self-rebuild) + if (index > 0) + { + // check overflow/duplicates + if (IsOverflowing(result)) + { + return; // duplicate found + } + + Cache.Insert(index, result); + NotifyObserversOnChange(result.Timestamp); + } + + // normal add + else + { + AppendCache(result, notify: true); + } + } + + /// + /// Perform appropriate caching action after analysis. + /// It will add if new, ignore if duplicate, or rebuild if late-arrival. + /// + /// TSeries item to cache. + /// + /// Notify subscribers of change (send to observers). + /// This is disabled for bulk operations like rebuild. + /// + protected void AppendCache(TOut result, bool notify) + { + // check overflow/duplicates + if (IsOverflowing(result)) + { + return; + } + + bool bypassRebuild = Properties[1]; // forced add/caching w/o rebuild + + // consider timeline + Act act = bypassRebuild || Cache.Count == 0 || result.Timestamp > Cache[^1].Timestamp + ? Act.Add + : Act.Rebuild; + + // fulfill action + switch (act) + { + // add to cache + case Act.Add: + Add(result, notify); + break; + + // rebuild cache + case Act.Rebuild: + Rebuild(result.Timestamp); + break; + + // would never happen + default: + throw new InvalidOperationException(); + } + } + + /// + /// Add item to cache and notify observers. + /// + /// Item to add to end of cache + /// Inherited notification instructions. + private void Add(TOut item, bool notify) + { + // notes: + // 1. Should only be called from AppendCache() + // 2. Notify has to be disabled for bulk operations, like rebuild. + // 3. Forced caching (rebuild analysis bypass) is inherited property. + + // add to cache + Cache.Add(item); + IsFaulted = false; + + // notify subscribers + if (notify) + { + NotifyObserversOnAdd(item, Cache.Count - 1); + } + } + + /// + /// Validate outbound item and compare to prior cached item, + /// to gracefully manage and prevent overflow conditions. + /// + /// Cacheable time-series object + /// + /// True if item is repeating and duplicate was suppressed. + /// + /// + /// Too many sequential duplicates were detected. + /// + private bool IsOverflowing(TOut item) + { + // skip first arrival + if (LastItem is null) + { + LastItem = item; + return false; + } + + // track/check for overflow condition + if (item.Timestamp == LastItem.Timestamp && item.Equals(LastItem)) + { + // ^^ using progressive check to avoid Equals() on every item + + OverflowCount++; + + // handle overflow + if (OverflowCount > 100) + { + const string msg = """ + A repeated stream update exceeded the 100 attempt threshold. + Check and remove circular chains or check your stream provider. + Provider terminated. + """; + + IsFaulted = true; + + // emit error + OverflowException exception = new(msg); + NotifyObserversOnError(exception); + throw exception; + } + + // bypass duplicate prevention + // when forced caching is enabled + if (Properties[1]) + { + return false; + + // note: will still overflow + // when the 100 limit is reached + } + + return true; + } + + // not repeating + OverflowCount = 0; + LastItem = item; + return false; + } + #endregion + + #region REMOVE & REMOVE RANGE + + /// remove cached item + /// + public void Remove(TOut cachedItem) + { + Cache.Remove(cachedItem); + NotifyObserversOnChange(cachedItem.Timestamp); + } + + /// remove cached item at index position + /// + public void RemoveAt(int cacheIndex) + { + TOut cachedItem = Cache[cacheIndex]; + Cache.RemoveAt(cacheIndex); + NotifyObserversOnChange(cachedItem.Timestamp); + } + + /// remove cache range from timestamp + /// + public void RemoveRange(DateTime fromTimestamp, bool notify) + { + // rollback internal state + RollbackState(fromTimestamp); + + // remove cache entries + Cache.RemoveAll(c => c.Timestamp >= fromTimestamp); + + // notify observers + if (notify) + { + NotifyObserversOnChange(fromTimestamp); + } + } + + /// remove cache range from index + /// + public void RemoveRange(int fromIndex, bool notify) + { + // nothing to do + if (Cache.Count == 0 || fromIndex >= Cache.Count) + { + return; + } + + // remove cache entries + DateTime fromTimestamp = fromIndex <= 0 + ? DateTime.MinValue + : Cache[fromIndex].Timestamp; + + RemoveRange(fromTimestamp, notify); + } + #endregion + + #region REBUILD & REINITIALIZE + + // full reset + /// + public void Reinitialize() + { + Unsubscribe(); + ResetFault(); + Rebuild(); + Subscription = Provider.Subscribe(this); + + // TODO: make reinitialization abstract, + // and build initial Cache from faster static method + + // TODO: evaluate race condition between rebuild + // and subscribe; will it miss any high frequency data? + } + + // rebuild cache + /// + public void Rebuild() + => Rebuild(DateTime.MinValue); + + // rebuild cache from timestamp + /// + public void Rebuild(DateTime fromTimestamp) + { + // clear cache + RemoveRange(fromTimestamp, notify: false); + + // get provider position + int provIndex = ProviderCache.GetIndexGte(fromTimestamp); + + // rebuild + if (provIndex >= 0) + { + for (int i = provIndex; i < ProviderCache.Count; i++) + { + OnAdd(ProviderCache[i], notify: false, i); + } + } + + // notify observers + NotifyObserversOnChange(fromTimestamp); + } + + // rebuild cache from index + /// + public void Rebuild(int fromIndex) + { + // find timestamp + DateTime fromTimestamp = fromIndex <= 0 || Cache.Count == 0 + ? DateTime.MinValue + : Cache[fromIndex].Timestamp; + + // rebuild & notify + Rebuild(fromTimestamp); + } + + /// + /// Rollback internal state to a point in time. + /// Behavior varies by indicator. + /// + /// + /// Override when indicator needs to rollback state to a + /// point in time (e.g. when rebuilding cache). Example: + /// + /// + /// + /// Point in time to restore. + /// + protected virtual void RollbackState(DateTime timestamp) + { + // note: override when rollback is needed + // default: do nothing + // see AtrStopHub() for example + } + #endregion +} + +#region chain and quote variants + +/// +public abstract class QuoteProvider( + IStreamObservable provider +) : StreamHub(provider), IQuoteProvider + where TIn : IReusable + where TOut : IQuote +{ + public IReadOnlyList Quotes => Cache; +}; + +/// +public abstract class ChainProvider( + IStreamObservable provider +) : StreamHub(provider), IChainProvider + where TIn : IReusable + where TOut : IReusable; +#endregion diff --git a/src/_common/Observables/TupleObserver.cs b/src/_common/Observables/TupleObserver.cs deleted file mode 100644 index ae6809f85..000000000 --- a/src/_common/Observables/TupleObserver.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Skender.Stock.Indicators; - -// OBSERVER of TUPLES (BOILERPLATE) - -public abstract class TupleObserver : IObserver<(DateTime Date, double Value)> -{ - // fields - private IDisposable? unsubscriber; - - // properites - internal TupleProvider? Supplier { get; set; } - - // methods - public virtual void Subscribe() - => unsubscriber = Supplier != null - ? Supplier.Subscribe(this) - : throw new ArgumentNullException(nameof(Supplier)); - - public virtual void OnCompleted() => Unsubscribe(); - - public virtual void OnError(Exception error) => throw error; - - public virtual void OnNext((DateTime Date, double Value) value) - { - // » handle new quote with override in observer - } - - public virtual void Unsubscribe() => unsubscriber?.Dispose(); -} diff --git a/src/_common/Observables/TupleProvider.cs b/src/_common/Observables/TupleProvider.cs deleted file mode 100644 index 3304496c6..000000000 --- a/src/_common/Observables/TupleProvider.cs +++ /dev/null @@ -1,174 +0,0 @@ -namespace Skender.Stock.Indicators; - -// QUOTE OBSERVER and TUPLE PROVIDER - -public abstract class TupleProvider - : QuoteObserver, IObservable<(DateTime Date, double Value)> -{ - // fields - private readonly List> observers; - - // initialize - protected TupleProvider() - { - observers = []; - ProtectedTuples = []; - } - - // properties - internal IEnumerable<(DateTime Date, double Value)> Output => ProtectedTuples; - - internal List<(DateTime Date, double Value)> ProtectedTuples { get; set; } - - private int OverflowCount { get; set; } - - // METHODS - - // subscribe observer - public IDisposable Subscribe(IObserver<(DateTime Date, double Value)> observer) - { - if (!observers.Contains(observer)) - { - observers.Add(observer); - } - - return new Unsubscriber(observers, observer); - } - - // close all observations - public void EndTransmission() - { - foreach (IObserver<(DateTime Date, double Value)> observer in observers.ToArray()) - { - if (observers.Contains(observer)) - { - observer.OnCompleted(); - } - } - - observers.Clear(); - } - - // add one - internal void AddSend((DateTime Date, double Value) tuple) - { - int length = ProtectedTuples.Count; - - if (length == 0) - { - // add new tuple - ProtectedTuples.Add(tuple); - - // notify observers - NotifyObservers(tuple); - return; - } - - (DateTime lastDate, _) = ProtectedTuples[length - 1]; - - // add tuple - if (tuple.Date > lastDate) - { - // add new tuple - ProtectedTuples.Add(tuple); - - // notify observers - NotifyObservers(tuple); - } - - // same date or tuple recieved - else if (tuple.Date <= lastDate) - { - // check for overflow condition - // where same tuple continues (possible circular condition) - if (tuple.Date == lastDate) - { - OverflowCount++; - - if (OverflowCount > 100) - { - string msg = "A repeated Tuple update exceeded the 100 attempt threshold. " - + "Check and remove circular chains or check your Tuple provider."; - - EndTransmission(); - - throw new OverflowException(msg); - } - } - else - { - OverflowCount = 0; - } - - // seek old tuple - int foundIndex = ProtectedTuples - .FindIndex(x => x.Date == tuple.Date); - - // found - if (foundIndex >= 0) - { - ProtectedTuples[foundIndex] = tuple; - } - - // add missing tuple - else - { - ProtectedTuples.Add(tuple); - - // re-sort cache - ProtectedTuples = ProtectedTuples - .ToSortedList(); - } - - // let observer handle old + duplicates - NotifyObservers(tuple); - } - } - - // add many - internal void AddSend(IEnumerable<(DateTime Date, double Value)> tuples) - { - List<(DateTime Date, double Value)> added = tuples - .ToSortedList(); - - for (int i = 0; i < added.Count; i++) - { - AddSend(added[i]); - } - } - - // notify observers - private void NotifyObservers((DateTime Date, double Value) tuple) - { - List> obsList = observers.ToList(); - - for (int i = 0; i < obsList.Count; i++) - { - IObserver<(DateTime Date, double Value)> obs = obsList[i]; - obs.OnNext(tuple); - } - } - - // unsubscriber - private class Unsubscriber : IDisposable - { - private readonly List> observers; - private readonly IObserver<(DateTime Date, double Value)> observer; - - // identify and save observer - public Unsubscriber(List> observers, IObserver<(DateTime Date, double Value)> observer) - { - this.observers = observers; - this.observer = observer; - } - - // remove single observer - public void Dispose() - { - if (observer != null && observers.Contains(observer)) - { - observers.Remove(observer); - } - } - } -} diff --git a/src/_common/ObsoleteV2.cs b/src/_common/ObsoleteV2.cs deleted file mode 100644 index 15a6dca2f..000000000 --- a/src/_common/ObsoleteV2.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; - -namespace Skender.Stock.Indicators; - -// OBSOLETE IN v2.0.0 -public static partial class Indicator -{ -#pragma warning disable CA1002 // Do not expose generic lists - - // 2.4.1 - [ExcludeFromCodeCoverage] - [Obsolete("Rename 'ToBasicTuple(..)' to 'ToTuple(..)' to fix.", false)] - public static List<(DateTime, double)> ToBasicTuple( - this IEnumerable quotes, - CandlePart candlePart) - where TQuote : IQuote - => quotes.ToTuple(candlePart); - - [ExcludeFromCodeCoverage] - [Obsolete("Rename 'ToResultTuple(..)' to 'ToTuple(..)' to fix.", false)] - public static List<(DateTime Date, double Value)> ToResultTuple( - this IEnumerable basicData) - => basicData.ToTuple(); - - // v2.4.8 - [ExcludeFromCodeCoverage] - [Obsolete("Rename 'ToTupleCollection(..)' to 'ToTupleChainable(..)' to fix.", false)] - public static Collection<(DateTime Date, double Value)> ToTupleCollection( - this IEnumerable reusable) - => reusable - .ToTupleChainable(); - - [ExcludeFromCodeCoverage] - [Obsolete("Rename 'ToTupleCollection(NullTo..)' to either 'ToTupleNaN(..)' or 'ToTupleNull(..)' to fix.", false)] - public static Collection<(DateTime Date, double? Value)> ToTupleCollection( - this IEnumerable reusable, NullTo nullTo) - { - List reList = reusable.ToSortedList(); - int length = reList.Count; - - Collection<(DateTime Date, double? Value)> results = []; - - for (int i = 0; i < length; i++) - { - IReusableResult r = reList[i]; - results.Add(new(r.Date, r.Value.Null2NaN())); - } - - return results; - } - - // v2.4.10 - [ExcludeFromCodeCoverage] - [Obsolete("Change 'GetStarcBands()' to 'GetStarcBands(20)' to fix.", false)] - public static IEnumerable GetStarcBands( - this IEnumerable quotes) - where TQuote : IQuote - => quotes.GetStarcBands(20); - -#pragma warning restore CA1002 // Do not expose generic lists -} - -// v2.4.8 (see above) -public enum NullTo -{ - NaN, - Null -} diff --git a/src/_common/ObsoleteV3.cs b/src/_common/ObsoleteV3.cs new file mode 100644 index 000000000..c0a2a245e --- /dev/null +++ b/src/_common/ObsoleteV3.cs @@ -0,0 +1,134 @@ +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; + +// ReSharper disable all + +namespace Skender.Stock.Indicators; + +// OBSOLETE IN v3 +public static partial class Indicator +{ + // GENERAL INDICATOR METHODS + + [ExcludeFromCodeCoverage] + [Obsolete("Use alternate 'ToAlligator' variant. Tuple arguments were removed.", false)] // v3.0.0 + public static IEnumerable GetAlligator( + this IEnumerable<(DateTime d, double v)> priceTuples, + int jawPeriods = 13, + int jawOffset = 8, + int teethPeriods = 8, + int teethOffset = 5, + int lipsPeriods = 5, + int lipsOffset = 3) + => priceTuples + .Select(t => new QuotePart(t.d, t.v)) + .ToList() + .ToAlligator( + jawPeriods, jawOffset, + teethPeriods, teethOffset, + lipsPeriods, lipsOffset); + + [ExcludeFromCodeCoverage] + [Obsolete("Replace `GetEma(..)` with `ToEma(..)`", false)] // v3.0.0 + public static IEnumerable GetEma( + this IReadOnlyList quotes, int lookbackPeriods) + where TQuote : IQuote + => quotes.ToSortedList().ToEma(lookbackPeriods); + + // REMOVAL OF INTEGRATED SMAs (evaluates to ERRORs) + + [ExcludeFromCodeCoverage] + [Obsolete("Use a chained `results.ToSma(smaPeriods)` to generate a moving average.", true)] // v3.0.0 + public static IEnumerable GetAdl( + this IReadOnlyList quotes, int smaPeriods) + where TQuote : IQuote + => quotes.ToSortedList().ToAdl(); + + [ExcludeFromCodeCoverage] + [Obsolete("Use a chained `results.ToSma(smaPeriods)` to generate a moving average.", true)] // v3.0.0 + public static IEnumerable GetObv( + this IReadOnlyList quotes, int smaPeriods) + where TQuote : IQuote + => quotes.ToObv(); + + [ExcludeFromCodeCoverage] + [Obsolete("Use a chained `results.ToSma(smaPeriods)` to generate a moving average.", true)] // v3.0.0 + public static IEnumerable GetPrs( + this IEnumerable quotesEval, IEnumerable quotesBase, int lookbackPeriods, int smaPeriods) + where TQuote : IQuote + => quotesEval + .ToSortedList() + .Use(CandlePart.Close) + .ToPrs( + quotesBase.ToSortedList() + .Use(CandlePart.Close), lookbackPeriods); + + [ExcludeFromCodeCoverage] + [Obsolete("Use a chained `results.ToSma(smaPeriods)` to generate a moving average.", true)] // v3.0.0 + public static IEnumerable GetRoc( + this IReadOnlyList quotes, int lookbackPeriods, int smaPeriods) + where TQuote : IQuote + => quotes.Use(CandlePart.Close).ToRoc(lookbackPeriods); + + [ExcludeFromCodeCoverage] + [Obsolete("Use a chained `results.ToSma(smaPeriods)` to generate a moving average.", true)] // v3.0.0 + public static IEnumerable GetStdDev( + this IReadOnlyList quotes, int lookbackPeriods, int smaPeriods) + where TQuote : IQuote + => quotes.Use(CandlePart.Close).ToStdDev(lookbackPeriods); + + [ExcludeFromCodeCoverage] + [Obsolete("Use a chained `results.ToSma(smaPeriods)` to generate a moving average.", true)] // v3.0.0 + public static IEnumerable GetTrix( + this IReadOnlyList quotes, int lookbackPeriods, int smaPeriods) + where TQuote : IQuote + => quotes.Use(CandlePart.Close).ToTrix(lookbackPeriods); + + // UTILITIES + + [ExcludeFromCodeCoverage] + [Obsolete("This method no longer defaults to Close. Rename Use() to Use(CandlePart.Close) for an explicit conversion.", false)] // v3.0.0 + public static IEnumerable<(DateTime Timestamp, double Value)> Use( + this IReadOnlyList quotes) + where TQuote : IQuote + => quotes.Select(x => (x.Timestamp, x.Value)); + + [ExcludeFromCodeCoverage] + [Obsolete("Refactor to use `ToSortedList()`", true)] // v3.0.0 + public static Collection ToSortedCollection( + this IReadOnlyList series) + where TSeries : ISeries + => series + .OrderBy(x => x.Timestamp) + .ToCollection(); + + [ExcludeFromCodeCoverage] + [Obsolete("Refactor to use `ToReusable()`", true)] // v3.0.0 + public static Collection<(DateTime Timestamp, double Value)> ToTupleChainable( + this IEnumerable reusable) + where TResult : IReusable + => reusable.Select(x => (x.Timestamp, x.Value)).OrderBy(x => x.Timestamp).ToCollection(); + + [ExcludeFromCodeCoverage] + [Obsolete("Refactor to use `List.First(c => c.Timestamp == lookupDate)`", false)] // v3.0.0 + public static TSeries Find(this IEnumerable series, DateTime lookupDate) + where TSeries : ISeries => series.First(x => x.Timestamp == lookupDate); + + [ExcludeFromCodeCoverage] + [Obsolete("Refactor to use `List.FindIndex(c => c.Timestamp == lookupDate)`", false)] // v3.0.0 + public static int FindIndex(this List series, DateTime lookupDate) + where TSeries : ISeries => series?.FindIndex(x => x.Timestamp == lookupDate) ?? -1; +} + +// CLASSES AND INTERFACES + +[Obsolete("Rename `IReusableResult` to `IReusable`", true)] // v3.0.0 +public interface IReusableResult : IReusable; + +[ExcludeFromCodeCoverage] +[Obsolete("Rename `BasicData` to `QuotePart`", true)] // v3.0.0 +public sealed class BasicData : IReusable +{ + public DateTime Timestamp { get; set; } + public double Value { get; set; } +} diff --git a/src/_common/ObsoleteV3.md b/src/_common/ObsoleteV3.md new file mode 100644 index 000000000..9561066da --- /dev/null +++ b/src/_common/ObsoleteV3.md @@ -0,0 +1,107 @@ +# v3 migration guide + +We've discontinued all bridge features for v1 backwards compatibility. +Correct all Warnings from this library before migrating from v2 to v3. +If you are still using v1, migrate to v2 first, to ease the transition to v3. + +Where possible, we've added bridge features to ease transition from v2 to v3. +You will see supporting migration Warning in your compiler with additional instructions for [deprecated changes](#deprecation-changes). + +In addition there are [breaking changes](#breaking-changes) that will require your attention. + +## Deprecation changes + +See your compiler `Warning` to identify these in your code. + +- All static time-series API method prefix were renamed from `GetX()` to `ToX()` to better reflect their purpose as a conversion utility. The former "Get" prefix innaccurately implied a retrieval operation. + +- `Use()` method parameter `candlePart` is now required and no longer defaults to `CandlePart.Close`. +- `Use()` now returns a chainable `QuotePart` instead of a tuple. These also replace the redundant `GetBaseQuote()` and `BaseQuote` items, respectively. + +- `UlcerIndexResult` property `UI` was renamed to `UlcerIndex` + +- **Deprecated 'GetX' tuple interfaces**. + +- **Deprecated internal signals**: several indicators were originally built with integrated but optional + moving averages, often by specifying an optional `smaPeriods` parameter. With more moving average chaining options, + these are obsolete, so we've removed them for simplification. These were persisted to avoid breaking your code; + however, you will see a compiler `Warnings` to help you identify areas to refactor. Check for use in ADL, OBV, ROC, STDDEV, TRIX, and others. + Future versions will not support the old API and will produce compiler `Errors`. + + ```csharp + // To refactor, here's an example replacement for ADL: + + var results = quotes.ToAdl(10); + var adlSma = results.ToSma(5); + + // ref: old usage example + + var results = quotes + .GetAdl(lookbackPeriods: 10, smaPeriods: 5); + ``` + +## Breaking changes + +Not all, but some of these will be shown as compiler `Errors` in your code. +Items marked with 🚩 require special attention since they will not produce compiler Errors or Warnings. + +- all v1 backwards compatibility accommodations were removed. + +- no longer supporting .NET Standard 2.0 for older .NET Framework compatibility. + +### Common breaking changes + +- `Quote` type (built-in) was changed to an _**immutable**_ `record` type; and its `IQuote` interface `Date` property was widely renamed to `Timestamp`, to avoid a conflict with a C# reserved name. This will break your implementation if you were using `Quote` as an inherited base type. To fix, define your custom `TQuote` type on the `IQuote` interface instead (example below). + +- `IQuote` is now a reusable (chainable) type. It auto-selects `Close` price as the _default_ consumed value. + +- `TQuote` custom quote types now have to implement the `IReusable` interface to support chaining operations. The best way to fix is to change your `TQuote` to implement the `IReusable.Value` pointer to your `IQuote.Close` price. See [the Guide](/guide) for more information. Example: + + ```csharp + public record MyCustomQuote ( + + // `IQuote` properties + DateTime Timestamp, + decimal Open, + decimal High, + decimal Low, + decimal MyClose, // custom + decimal Volume, + + // custom properties + string? MyCustomProperty = default + + ) : IQuote // » IQuote now inherits an IReusable + { + // custom mapped properties + decimal IQuote.Close + => MyClose; + + // `IReusable` requires a default 'Value' property. + // Map it to your 'Close' price. + double IReusable.Value + => (double)Close; + } + ``` + +- `IReusableResult` was renamed to `IReusable` since it is no longer limited to _result_ types. + +- 🚩 `IReusableResult.Value` property was changed to non-nullable and returns `double.NaN` instead of `null` for incalculable periods. The standard results (e.g. `EmaResult.Ema`) continue to return `null` for incalculable periods. This was done to improve internal chaining and streaming performance. + +- Indicator return types, like `EmaResult`, were changed from `sealed class` to _**immutable**_ `record` types to improve internal chaining and streaming performance. This should not impact negatively; however, these can now be inherited as base classes. + +### Less common breaking changes + +- Return type for the `Use()` utility method was renamed from `UseResult` to `QuotePart` for clarity and for of its wider purpose. + +- `Numerixs` class was renamed to `Numerical`. + +- `GetBaseQuote()` indicator and related `BasicData` return types were removed since they are redundant to the `Use()` method and `QuotePart` return types, respectively. + +- `AtrStopResult` values were changed from `decimal` to `double` numeric return types. + +- `SyncSeries()` utility function and related `SyncType` enum were removed. This was primarily an internal utility, but was part of the public API to support user who wanted to build custom indicator development. Internally, we've refactored indicators to auto-initialize and heal, so they no longer require re-sizing to support explicit warmup periods. + +- `ToTupleCollection()` utility method was deprecated. This was available to support custom indicator development, but is no longer needed. We've discontinued using _tuples_ as an interface to chainable indicators. + +- `Find()` and `FindIndex()` utility methods were removed. These were redundant to the native C# `List.Find()` method and `List.FindIndex()` methods, respectively. diff --git a/src/_common/Quotes/Quote.Aggregates.cs b/src/_common/Quotes/Quote.Aggregates.cs index 8ce2dc29f..8af1b3ccd 100644 --- a/src/_common/Quotes/Quote.Aggregates.cs +++ b/src/_common/Quotes/Quote.Aggregates.cs @@ -2,45 +2,45 @@ namespace Skender.Stock.Indicators; // QUOTE UTILITIES -public static partial class QuoteUtility +public static partial class Quotes { // aggregation (quantization) /// /// - public static IEnumerable Aggregate( - this IEnumerable quotes, + public static IReadOnlyList Aggregate( + this IReadOnlyList quotes, PeriodSize newSize) where TQuote : IQuote { - if (newSize != PeriodSize.Month) - { - // parameter conversion - TimeSpan newTimeSpan = newSize.ToTimeSpan(); - - // convert - return quotes.Aggregate(newTimeSpan); - } - else // month + if (newSize == PeriodSize.Month) { return quotes - .OrderBy(x => x.Date) - .GroupBy(x => new DateTime(x.Date.Year, x.Date.Month, 1)) - .Select(x => new Quote { - Date = x.Key, - Open = x.First().Open, - High = x.Max(t => t.High), - Low = x.Min(t => t.Low), - Close = x.Last().Close, - Volume = x.Sum(t => t.Volume) - }); + .OrderBy(x => x.Timestamp) + .GroupBy(x => new DateTime(x.Timestamp.Year, x.Timestamp.Month, 1)) + .Select(x => new Quote( + Timestamp: x.Key, + Open: x.First().Open, + High: x.Max(t => t.High), + Low: x.Min(t => t.Low), + Close: x.Last().Close, + Volume: x.Sum(t => t.Volume))) + .ToList(); } + + // parameter conversion + TimeSpan newTimeSpan = newSize.ToTimeSpan(); + + // convert + return quotes.Aggregate(newTimeSpan); + + // month } // aggregation (quantization) using TimeSpan /// /// - public static IEnumerable Aggregate( - this IEnumerable quotes, + public static IReadOnlyList Aggregate( + this IReadOnlyList quotes, TimeSpan timeSpan) where TQuote : IQuote { @@ -52,15 +52,15 @@ public static IEnumerable Aggregate( // return aggregation return quotes - .OrderBy(x => x.Date) - .GroupBy(x => x.Date.RoundDown(timeSpan)) - .Select(x => new Quote { - Date = x.Key, - Open = x.First().Open, - High = x.Max(t => t.High), - Low = x.Min(t => t.Low), - Close = x.Last().Close, - Volume = x.Sum(t => t.Volume) - }); + .OrderBy(x => x.Timestamp) + .GroupBy(x => x.Timestamp.RoundDown(timeSpan)) + .Select(x => new Quote( + Timestamp: x.Key, + Open: x.First().Open, + High: x.Max(t => t.High), + Low: x.Min(t => t.Low), + Close: x.Last().Close, + Volume: x.Sum(t => t.Volume))) + .ToList(); } } diff --git a/src/_common/Quotes/Quote.Converters.cs b/src/_common/Quotes/Quote.Converters.cs index 6e0941d5b..0a03e5e13 100644 --- a/src/_common/Quotes/Quote.Converters.cs +++ b/src/_common/Quotes/Quote.Converters.cs @@ -1,122 +1,52 @@ -using System.Collections.ObjectModel; -using System.Globalization; - namespace Skender.Stock.Indicators; -// QUOTE UTILITIES +// QUOTE UTILITIES (CONVERTERS) -public static partial class QuoteUtility +public static partial class Quotes { + /* LISTS */ - // TUPLE QUOTES - - // convert quotes to tuple list - public static Collection<(DateTime, double)> ToTupleCollection( - this IEnumerable quotes, - CandlePart candlePart) + // convert TQuote type list to built-in Quote type list + public static IReadOnlyList ToQuoteList( + this IReadOnlyList quotes) where TQuote : IQuote - => quotes - .ToTuple(candlePart) - .ToCollection(); - - internal static List<(DateTime, double)> ToTuple( - this IEnumerable quotes, - CandlePart candlePart) - where TQuote : IQuote => quotes - .OrderBy(x => x.Date) - .Select(x => x.ToTuple(candlePart)) - .ToList(); - - // convert tuples to list, with sorting - public static Collection<(DateTime, double)> ToSortedCollection( - this IEnumerable<(DateTime date, double value)> tuples) - => tuples - .ToSortedList() - .ToCollection(); - internal static List<(DateTime, double)> ToSortedList( - this IEnumerable<(DateTime date, double value)> tuples) - => tuples - .OrderBy(x => x.date) + => quotes + .OrderBy(x => x.Timestamp) + .Select(x => x.ToQuote()) .ToList(); - // DOUBLE QUOTES - - // convert to quotes in double precision - internal static List ToQuoteD( - this IEnumerable quotes) - where TQuote : IQuote => quotes - .Select(x => new QuoteD { - Date = x.Date, - Open = (double)x.Open, - High = (double)x.High, - Low = (double)x.Low, - Close = (double)x.Close, - Volume = (double)x.Volume - }) - .OrderBy(x => x.Date) - .ToList(); + // convert TQuote type list to QuoteD type list + internal static List ToQuoteDList( + this IReadOnlyList quotes) + where TQuote : IQuote - // convert quoteD list to tuples - internal static List<(DateTime, double)> ToTuple( - this List qdList, - CandlePart candlePart) => qdList - .OrderBy(x => x.Date) - .Select(x => x.ToTuple(candlePart)) + => quotes + .Select(x => x.ToQuoteD()) .ToList(); - /* ELEMENTS */ - - // convert TQuote element to basic tuple - internal static (DateTime date, double value) ToTuple( - this TQuote q, - CandlePart candlePart) - where TQuote : IQuote => candlePart switch { - CandlePart.Open => (q.Date, (double)q.Open), - CandlePart.High => (q.Date, (double)q.High), - CandlePart.Low => (q.Date, (double)q.Low), - CandlePart.Close => (q.Date, (double)q.Close), - CandlePart.Volume => (q.Date, (double)q.Volume), - CandlePart.HL2 => (q.Date, (double)(q.High + q.Low) / 2), - CandlePart.HLC3 => (q.Date, (double)(q.High + q.Low + q.Close) / 3), - CandlePart.OC2 => (q.Date, (double)(q.Open + q.Close) / 2), - CandlePart.OHL3 => (q.Date, (double)(q.Open + q.High + q.Low) / 3), - CandlePart.OHLC4 => (q.Date, (double)(q.Open + q.High + q.Low + q.Close) / 4), - _ => throw new ArgumentOutOfRangeException(nameof(candlePart), candlePart, "Invalid candlePart provided."), - }; + /* TYPES */ - // convert TQuote element to basic double class - internal static BasicData ToBasicData( - this TQuote q, - CandlePart candlePart) - where TQuote : IQuote => candlePart switch { - CandlePart.Open => new BasicData { Date = q.Date, Value = (double)q.Open }, - CandlePart.High => new BasicData { Date = q.Date, Value = (double)q.High }, - CandlePart.Low => new BasicData { Date = q.Date, Value = (double)q.Low }, - CandlePart.Close => new BasicData { Date = q.Date, Value = (double)q.Close }, - CandlePart.Volume => new BasicData { Date = q.Date, Value = (double)q.Volume }, - CandlePart.HL2 => new BasicData { Date = q.Date, Value = (double)(q.High + q.Low) / 2 }, - CandlePart.HLC3 => new BasicData { Date = q.Date, Value = (double)(q.High + q.Low + q.Close) / 3 }, - CandlePart.OC2 => new BasicData { Date = q.Date, Value = (double)(q.Open + q.Close) / 2 }, - CandlePart.OHL3 => new BasicData { Date = q.Date, Value = (double)(q.Open + q.High + q.Low) / 3 }, - CandlePart.OHLC4 => new BasicData { Date = q.Date, Value = (double)(q.Open + q.High + q.Low + q.Close) / 4 }, - _ => throw new ArgumentOutOfRangeException(nameof(candlePart), candlePart, "Invalid candlePart provided."), - }; + // convert any IQuote type to native Quote type + public static Quote ToQuote(this TQuote quote) + where TQuote : IQuote - // convert quoteD element to basic tuple - internal static (DateTime, double) ToTuple( - this QuoteD q, - CandlePart candlePart) => candlePart switch { - CandlePart.Open => (q.Date, q.Open), - CandlePart.High => (q.Date, q.High), - CandlePart.Low => (q.Date, q.Low), - CandlePart.Close => (q.Date, q.Close), - CandlePart.Volume => (q.Date, q.Volume), - CandlePart.HL2 => (q.Date, (q.High + q.Low) / 2), - CandlePart.HLC3 => (q.Date, (q.High + q.Low + q.Close) / 3), - CandlePart.OC2 => (q.Date, (q.Open + q.Close) / 2), - CandlePart.OHL3 => (q.Date, (q.Open + q.High + q.Low) / 3), - CandlePart.OHLC4 => (q.Date, (q.Open + q.High + q.Low + q.Close) / 4), - _ => throw new ArgumentOutOfRangeException(nameof(candlePart), candlePart, "Invalid candlePart provided."), - }; + => new( + Timestamp: quote.Timestamp, + Open: quote.Open, + High: quote.High, + Low: quote.Low, + Close: quote.Close, + Volume: quote.Volume); + + // convert to quote in double precision + internal static QuoteD ToQuoteD(this IQuote quote) + + => new( + Timestamp: quote.Timestamp, + Open: (double)quote.Open, + High: (double)quote.High, + Low: (double)quote.Low, + Close: (double)quote.Close, + Volume: (double)quote.Volume); } diff --git a/src/_common/Quotes/Quote.Exceptions.cs b/src/_common/Quotes/Quote.Exceptions.cs index 23cec65b1..2ba860334 100644 --- a/src/_common/Quotes/Quote.Exceptions.cs +++ b/src/_common/Quotes/Quote.Exceptions.cs @@ -1,5 +1,6 @@ namespace Skender.Stock.Indicators; +[Serializable] public class InvalidQuotesException : ArgumentOutOfRangeException { public InvalidQuotesException() diff --git a/src/_common/Quotes/Quote.Models.cs b/src/_common/Quotes/Quote.Models.cs index 6a5c96a26..46e65e5b6 100644 --- a/src/_common/Quotes/Quote.Models.cs +++ b/src/_common/Quotes/Quote.Models.cs @@ -2,33 +2,109 @@ namespace Skender.Stock.Indicators; // QUOTE MODELS -public interface IQuote : ISeries +/// +/// Quote interface for standard OHLCV aggregate period. +/// This is commonly known as a "bar" or "candle" and represents +/// and asset price range over a specific time range, +/// +/// If implementing your own custom TQuote:IQuote type: +/// +/// +/// For chaining compatibility ( +/// compliance), add the following TQuote property +/// (pointer) to your price. +/// +/// double IReusable.Value => (double)Close; +/// +/// +/// +/// TIP: If you do not need a custom quote type, +/// use the built-in . +/// +/// +public interface IQuote : IReusable { - public decimal Open { get; } - public decimal High { get; } - public decimal Low { get; } - public decimal Close { get; } - public decimal Volume { get; } + /// + /// Aggregate bar's first tick price + /// + decimal Open { get; } + + /// + /// Aggregate bar's highest tick price + /// + decimal High { get; } + + /// + /// Aggregate bar's lowest tick price + /// + decimal Low { get; } + + /// + /// Aggregate bar's last tick price + /// + decimal Close { get; } + + /// + /// Aggregate bar's tick volume + /// + decimal Volume { get; } } +/// +/// Built-in Quote type, representing an OHLCV aggregate price period. +/// +/// +/// Close date/time of the aggregate period +/// +/// +/// Aggregate bar's first tick price +/// +/// +/// Aggregate bar's highest tick price +/// +/// +/// Aggregate bar's lowest tick price +/// +/// +/// Aggregate bar's last tick price +/// +/// +/// Aggregate bar's tick volume +/// +/// [Serializable] -public class Quote : IQuote +public record Quote +( + DateTime Timestamp, + decimal Open, + decimal High, + decimal Low, + decimal Close, + decimal Volume +) : IQuote { - public DateTime Date { 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; + + // TODO: add [Obsolete] auto-getter/setter for 'Date' property + // but only for a short transition period. See if there can be + // a full overload of 'Quote' that has the 'Date' property and + // can support new(){ ... } initialization. } +/// +/// Double-point precision Quote, for internal use only. +/// +/// [Serializable] -internal class QuoteD +internal record QuoteD +( + DateTime Timestamp, + double Open, + double High, + double Low, + double Close, + double Volume +) : IReusable { - internal DateTime Date { get; set; } - internal double Open { get; set; } - internal double High { get; set; } - internal double Low { get; set; } - internal double Close { get; set; } - internal double Volume { get; set; } + public double Value => Close; } diff --git a/src/_common/Quotes/Quote.StreamHub.cs b/src/_common/Quotes/Quote.StreamHub.cs new file mode 100644 index 000000000..6f43feb02 --- /dev/null +++ b/src/_common/Quotes/Quote.StreamHub.cs @@ -0,0 +1,72 @@ +namespace Skender.Stock.Indicators; + +#region hub initializer + +public static partial class Quotes +{ + public static QuoteHub ToQuote( + this IQuoteProvider quoteProvider) + where TQuote : IQuote => new(quoteProvider); +} +#endregion + +/// +/// Quote provider (abstract base) +/// +public class QuoteHub + : QuoteProvider + where TQuote : IQuote +{ + public QuoteHub() : base(new EmptyQuoteProvider()) { } + + public QuoteHub( + IQuoteProvider provider) + : base(provider) + { + Reinitialize(); + } + + // METHODS + + protected override (TQuote result, int index) + ToIndicator(TQuote item, int? indexHint) + { + int index = indexHint + ?? Cache.GetIndexGte(item.Timestamp); + + return (item, index == -1 ? Cache.Count : index); + } + + public override string ToString() + => $"QUOTES<{typeof(TQuote).Name}>: {Quotes.Count} items"; +} + +/// +/// Empty quote provider for base Quote Hub initialization. +/// +/// Internal use only. Do not use directly. +/// +public class EmptyQuoteProvider + : IQuoteProvider + where TQuote : IQuote +{ + /// + /// Default quote provider is parent-less Quote Hub. + /// It does not transfer its setting to its children. + /// + public BinarySettings Properties { get; } = new(0b00000001, 0b11111110); + public int ObserverCount => 0; + public bool HasObservers => false; + public IReadOnlyList Quotes { get; } = Array.Empty(); + public IReadOnlyList GetCacheRef() => Array.Empty(); + public bool HasSubscriber(IStreamObserver observer) => false; + + public IDisposable Subscribe(IStreamObserver observer) + => throw new InvalidOperationException(); + + public bool Unsubscribe(IStreamObserver observer) + => throw new InvalidOperationException(); + + public void EndTransmission() + => throw new InvalidOperationException(); +} diff --git a/src/_common/Quotes/Quote.Validation.cs b/src/_common/Quotes/Quote.Validation.cs index 540b4a7c6..5713b6105 100644 --- a/src/_common/Quotes/Quote.Validation.cs +++ b/src/_common/Quotes/Quote.Validation.cs @@ -2,35 +2,60 @@ namespace Skender.Stock.Indicators; -// QUOTE UTILITIES +// QUOTE UTILITIES: VALIDATION -public static partial class QuoteUtility +public static partial class Quotes { - private static readonly CultureInfo invCulture = CultureInfo.InvariantCulture; + private static readonly CultureInfo invariantCulture + = CultureInfo.InvariantCulture; - // VALIDATION - /// - /// - public static IEnumerable Validate( - this IEnumerable quotes) + /// + /// Check that quotes are valid and in ascending order. + /// + /// IQuote type + /// List of quotes + /// Valid list of quotes + /// + /// List of quotes cannot be a null reference. + /// + /// + /// Duplicate or out of sequence quotes found. + /// + public static IReadOnlyList Validate( + this IReadOnlyList quotes) where TQuote : IQuote { - // we cannot rely on date consistency when looking back, so we force sort - List quotesList = quotes.ToSortedList(); + ArgumentNullException.ThrowIfNull(quotes); - // check for duplicates - DateTime lastDate = DateTime.MinValue; - foreach (TQuote q in quotesList) + if (quotes.Count == 0) { - if (lastDate == q.Date) + return quotes; + } + + DateTime lastDate = quotes[0].Timestamp; + for (int i = 1; i < quotes.Count; i++) + { + DateTime currentDate = quotes[i].Timestamp; + + if (lastDate == currentDate) { - throw new InvalidQuotesException( - $"Duplicate date found on {q.Date.ToString("o", invCulture)}."); + string msg = + $"Duplicate date found on {currentDate.ToString("o", invariantCulture)}."; + + throw new InvalidQuotesException(nameof(quotes), msg); + } + + if (lastDate > currentDate) + { + string msg = + $"Quotes are out of sequence on {currentDate.ToString("o", invariantCulture)}."; + + throw new InvalidQuotesException(nameof(quotes), msg); } - lastDate = q.Date; + lastDate = currentDate; } - return quotesList; + return quotes; } } diff --git a/src/_common/Quotes/Use.Api.cs b/src/_common/Quotes/Use.Api.cs deleted file mode 100644 index f9a3f2a45..000000000 --- a/src/_common/Quotes/Use.Api.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Skender.Stock.Indicators; - -// USE (API) - -public static partial class QuoteUtility -{ - // convert TQuotes to basic double tuple list - /// - /// - public static IEnumerable<(DateTime Date, double Value)> Use( - this IEnumerable quotes, - CandlePart candlePart = CandlePart.Close) - where TQuote : IQuote => quotes - .Select(x => x.ToTuple(candlePart)); - - // OBSERVER, from Quote Provider - public static UseObserver Use( - this QuoteProvider provider, - CandlePart candlePart = CandlePart.Close) - => new(provider, candlePart); -} diff --git a/src/_common/Quotes/Use.Observer.cs b/src/_common/Quotes/Use.Observer.cs deleted file mode 100644 index 935b27753..000000000 --- a/src/_common/Quotes/Use.Observer.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace Skender.Stock.Indicators; - -// USE (STREAMING) -public class UseObserver : TupleProvider -{ - public UseObserver( - QuoteProvider? provider, - CandlePart candlePart) - { - Supplier = provider; - CandlePartSelection = candlePart; - Initialize(); - } - - // PROPERTIES - - public IEnumerable<(DateTime Date, double Value)> Results => ProtectedTuples; - - private CandlePart CandlePartSelection { get; set; } - - // NON-STATIC METHODS - - // handle quote arrival - public override void OnNext(Quote value) => HandleArrival(value); - - // add new quote - internal void HandleArrival(Quote quote) - { - // candidate result - (DateTime date, double value) r = quote.ToTuple(CandlePartSelection); - - // initialize - int length = ProtectedTuples.Count; - - if (length == 0) - { - AddSend(r); - return; - } - - // check against last entry - (DateTime lastDate, _) = ProtectedTuples[length - 1]; - - // add new - if (r.date > lastDate) - { - AddSend(r); - } - - // update last - else if (r.date == lastDate) - { - ProtectedTuples[length - 1] = r; - } - - // late arrival - else - { - AddSend(r); - throw new NotImplementedException(); - } - } - - // calculate initial cache of quotes - private void Initialize() - { - if (Supplier != null) - { - ProtectedTuples = Supplier - .ProtectedQuotes - .ToTuple(CandlePartSelection); - } - - Subscribe(); - } -} diff --git a/src/_common/Quotes/info.xml b/src/_common/Quotes/info.xml index 2da909c1a..721479e5b 100644 --- a/src/_common/Quotes/info.xml +++ b/src/_common/Quotes/info.xml @@ -1,35 +1,7 @@ - - - Optionally select which candle part to use in the calculation. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - The OHLCV element or simply calculated value type. - Time series of Quote tuple values. - Invalid candle part provided. - - - - Validate historical quotes. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - Time series of historical quote values. - Validation check failed. - + Converts historical quotes into larger bar sizes. @@ -45,6 +17,7 @@ Time series of historical quote values. Invalid parameter value provided. + Converts historical quotes into larger bar sizes. @@ -60,4 +33,5 @@ Time series of historical quote values. Invalid parameter value provided. - \ No newline at end of file + + diff --git a/src/_common/Results/Result.Models.cs b/src/_common/Results/Result.Models.cs deleted file mode 100644 index f031f7d84..000000000 --- a/src/_common/Results/Result.Models.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RESULT MODELS - -public interface IReusableResult : ISeries -{ - public double? Value { get; } -} - -[Serializable] -public abstract class ResultBase : ISeries -{ - public DateTime Date { get; set; } -} diff --git a/src/_common/Results/Result.Syncing.cs b/src/_common/Results/Result.Syncing.cs deleted file mode 100644 index a39b0c9f9..000000000 --- a/src/_common/Results/Result.Syncing.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RESULTS UTILITIES - -public static partial class ResultUtility -{ - // SYNCHRONIZING RESULTS - RESIZE TO MATCH OTHER - /// - /// - public static IEnumerable SyncIndex( - this IEnumerable syncMe, - IEnumerable toMatch, - SyncType syncType = SyncType.FullMatch) - where TResultA : ISeries - where TResultB : ISeries - { - // initialize - List syncMeList = syncMe.ToSortedList(); - List toMatchList = toMatch.ToSortedList(); - - if (syncMeList.Count == 0 || toMatchList.Count == 0) - { - return new List(); - } - - bool prepend = false; - bool append = false; - bool remove = false; - - switch (syncType) - { - case SyncType.Prepend: - prepend = true; - break; - - case SyncType.AppendOnly: - prepend = append = true; - break; - - case SyncType.RemoveOnly: - remove = true; - break; - - case SyncType.FullMatch: - prepend = append = remove = true; - break; - - default: - throw new ArgumentOutOfRangeException(nameof(syncType)); - } - - Type type = syncMeList[0].GetType(); - - // add plugs for missing values - if (prepend || append) - { - List toAppend = []; - - for (int i = 0; i < toMatchList.Count; i++) - { - TResultB? m = toMatchList[i]; - TResultA? r = syncMeList.Find(m.Date); - - if (r is null) - { - TResultA? n = (TResultA?)Activator.CreateInstance(type, m.Date); - if (n != null) - { - toAppend.Add(n); - } - } - else if (!append) - { - break; - } - } - - syncMeList.AddRange(toAppend); - } - - // remove unmatched results - if (remove) - { - List toRemove = []; - - for (int i = 0; i < syncMeList.Count; i++) - { - TResultA? r = syncMeList[i]; - TResultB? m = toMatchList.Find(r.Date); - - if (m is null) - { - toRemove.Add(r); - } - } - - syncMeList.RemoveAll(x => toRemove.Contains(x)); - } - - return syncMeList.ToSortedList(); - } -} diff --git a/src/_common/Results/Result.Utilities.cs b/src/_common/Results/Result.Utilities.cs deleted file mode 100644 index eca3f3ac3..000000000 --- a/src/_common/Results/Result.Utilities.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Skender.Stock.Indicators; - -// RESULTS UTILITIES - -public static partial class ResultUtility -{ - // CONDENSE (REMOVE null and NaN results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) - where TResult : IReusableResult - { - List resultsList = results - .ToList(); - - resultsList - .RemoveAll(match: - x => x.Value is null or (not null and double.NaN)); - - return resultsList.ToSortedList(); - } - - // CONVERT TO TUPLE (default with pruning) - /// - /// - public static Collection<(DateTime Date, double Value)> ToTupleChainable( - this IEnumerable reusable) - where TResult : IReusableResult - => reusable - .ToTuple() - .ToCollection(); - - internal static List<(DateTime Date, double Value)> ToTuple( - this IEnumerable reusable) - where TResult : IReusableResult - { - List<(DateTime date, double value)> prices = []; - List reList = reusable.ToList(); - - // find first non-nulled - int first = reList.FindIndex(x => x.Value != null); - - for (int i = first; i < reList.Count; i++) - { - IReusableResult r = reList[i]; - prices.Add(new(r.Date, r.Value.Null2NaN())); - } - - return prices.OrderBy(x => x.date).ToList(); - } - - // CONVERT TO TUPLE with non-nullable NaN value option and no pruning - /// - /// - public static Collection<(DateTime Date, double Value)> ToTupleNaN( - this IEnumerable reusable) - where TResult : IReusableResult - { - List reList = reusable.ToSortedList(); - int length = reList.Count; - - Collection<(DateTime Date, double Value)> results = []; - - for (int i = 0; i < length; i++) - { - IReusableResult r = reList[i]; - results.Add(new(r.Date, r.Value.Null2NaN())); - } - - return results; - } -} diff --git a/src/_common/Results/info.xml b/src/_common/Results/info.xml deleted file mode 100644 index 11ad40769..000000000 --- a/src/_common/Results/info.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - Converts results into a reusable tuple with warmup periods removed and nulls converted to NaN. - See - documentation for more information. - - - Any reusable result type. - Indicator results to evaluate. - Collection of non-nullable tuple time series of results, without null warmup periods. - - - Converts results into a tuple collection with non-nullable NaN to replace null values. - See - documentation for more information. - - Any reusable result type. - Indicator results to evaluate. - Collection of tuple time series of - results with specified handling of nulls, without pruning. - - - Removes the recommended quantity of results from the beginning of the results list - using a reverse-engineering approach. See - documentation for more information. - - Indicator - results to evaluate. - Time - series of results, pruned. - - - Removes non-essential records containing null values with unique consideration for - this indicator. See - documentation for more information. - - Indicator results to evaluate. - Time series of - indicator results, condensed. - - - Removes non-essential records containing null or NaN values. See - documentation for more information. - - Any result type. - Indicator results to evaluate. - Time series of indicator results, - condensed. - - - - Forces indicator results to have the same date-based records as another result baseline. - - This utility is undocumented. - - - Any indicator result series type to be transformed. - Any indicator result series type to be matched. - The indicator result series to be modified. - The indicator result series to compare for matching. - Synchronization behavior See options in SyncType enum. - Indicator result series, synchronized to a comparator match. - - Invalid parameter value provided. - - - \ No newline at end of file diff --git a/src/_common/Reusable/IReusable.cs b/src/_common/Reusable/IReusable.cs new file mode 100644 index 000000000..fdd04b2a7 --- /dev/null +++ b/src/_common/Reusable/IReusable.cs @@ -0,0 +1,13 @@ +namespace Skender.Stock.Indicators; + +/// +/// A time-series type that identifies +/// a single chainable value. +/// +public interface IReusable : ISeries +{ + /// + /// Value that is passed to chained indicators. + /// + double Value { get; } +} diff --git a/src/_common/Reusable/Reusable.Utilities.cs b/src/_common/Reusable/Reusable.Utilities.cs new file mode 100644 index 000000000..d0439f591 --- /dev/null +++ b/src/_common/Reusable/Reusable.Utilities.cs @@ -0,0 +1,66 @@ +namespace Skender.Stock.Indicators; + +// REUSABLE TYPE UTILITIES + +public static partial class Reusable +{ + // convert IQuote type list to IReusable list + public static IReadOnlyList ToReusableList( + this IReadOnlyList quotes, + CandlePart candlePart) + where TQuote : IQuote + + => quotes + .OrderBy(x => x.Timestamp) + .Select(x => x.ToReusable(candlePart)) + .ToList(); + + /// + /// Removes non-essential records containing null or NaN values. + /// + /// Any reusable result type. + /// Indicator results to evaluate. + /// Time series of indicator results, condensed. + public static IReadOnlyList Condense( + this IReadOnlyList results) + where T : IReusable + { + List resultsList = results + .ToList(); + + resultsList + .RemoveAll(match: + x => double.IsNaN(x.Value)); + + return resultsList; + } + + /// + /// Removes the recommended quantity of results from the beginning + /// of the results list using a reverse-engineering approach. + /// + /// Any reusable result type. + /// Indicator results to evaluate. + /// Time series of results, pruned. + internal static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) + where T : IReusable + { + // this is the default implementation, it will + // be overridden in the specific indicator class + + int removePeriods = results + .ToList() + .FindIndex(x => !double.IsNaN(x.Value)); + + return results.Remove(removePeriods); + + // TODO: remove specific indicator 'RemoveWarmupPeriods()' methods + // that are now redundant to this generic method (not all are). + // Note: Some or all of these may already be removed. + } + + // convert TQuote element to a basic chainable class + internal static IReusable ToReusable(this IQuote q, CandlePart candlePart) + => q.ToQuotePart(candlePart); +} diff --git a/src/_common/Use (QuotePart)/QuotePart.Models.cs b/src/_common/Use (QuotePart)/QuotePart.Models.cs new file mode 100644 index 000000000..8bd3e4851 --- /dev/null +++ b/src/_common/Use (QuotePart)/QuotePart.Models.cs @@ -0,0 +1,19 @@ +namespace Skender.Stock.Indicators; + +/// +/// Chainable component part of an . +/// +[Serializable] +public record QuotePart +( + DateTime Timestamp, + double Value +) : IReusable +{ + public QuotePart(IReusable reusable) + : this(reusable?.Timestamp ?? default, + reusable?.Value ?? default) + { } + + public double Value { get; } = Value; +} diff --git a/src/_common/Use (QuotePart)/QuotePart.StaticSeries.cs b/src/_common/Use (QuotePart)/QuotePart.StaticSeries.cs new file mode 100644 index 000000000..53821b9f0 --- /dev/null +++ b/src/_common/Use (QuotePart)/QuotePart.StaticSeries.cs @@ -0,0 +1,45 @@ +namespace Skender.Stock.Indicators; + +// USE / QUOTE CONVERTER (SERIES) + +public static partial class QuoteParts +{ + /// + /// Converts to + /// an list. + /// + /// + /// Use this conversion if indicator needs to + /// use something other than the default Close price. + /// + /// + /// Sorted list of IQuote or IReusable items + /// + /// List of IReusable items + public static IReadOnlyList ToQuotePart( + this IReadOnlyList quotes, + CandlePart candlePart) + where TQuote : IQuote + { + ArgumentNullException.ThrowIfNull(quotes); + int length = quotes.Count; + List result = new(length); + + for (int i = 0; i < length; i++) + { + result.Add(quotes[i].ToQuotePart(candlePart)); + } + return result; + } + + // QuotePart alias + /// + public static IReadOnlyList Use( + this IReadOnlyList quotes, + CandlePart candlePart) + where TQuote : IQuote + => ToQuotePart(quotes, candlePart); + + // TODO: should we deprecate Use in favor of "ToQuotePart"? + // Probably not, this is a fairly simple alias. +} diff --git a/src/_common/Use (QuotePart)/QuotePart.StreamHub.cs b/src/_common/Use (QuotePart)/QuotePart.StreamHub.cs new file mode 100644 index 000000000..4e7448fbe --- /dev/null +++ b/src/_common/Use (QuotePart)/QuotePart.StreamHub.cs @@ -0,0 +1,56 @@ +namespace Skender.Stock.Indicators; + +// USE / QUOTE CONVERTER (STREAM HUB) + +#region hub interface and initializer +public interface IQuotePartHub +{ + CandlePart CandlePartSelection { get; } + + // TODO: consider renaming to IBarPartHub, with IQuote to IBar +} + +public static partial class QuoteParts +{ + public static QuotePartHub ToQuotePart( + this IQuoteProvider quoteProvider, + CandlePart candlePart) + where TIn : IQuote + => new(quoteProvider, candlePart); +} +#endregion + +public class QuotePartHub + : ChainProvider, IQuotePartHub + where TQuote : IQuote +{ + #region constructors + + internal QuotePartHub( + IQuoteProvider provider, + CandlePart candlePart) + : base(provider) + { + CandlePartSelection = candlePart; + + Reinitialize(); + } + #endregion + + public CandlePart CandlePartSelection { get; init; } + + // METHODS + + protected override (QuotePart result, int index) + ToIndicator(TQuote item, int? indexHint) + { + // candidate result + QuotePart r + = item.ToQuotePart(CandlePartSelection); + + return (r, indexHint ?? Cache.Count); + } + + public override string ToString() + => $"QUOTE-PART({CandlePartSelection.ToString().ToUpperInvariant()})"; +} diff --git a/src/_common/Use (QuotePart)/QuotePart.Utilities.cs b/src/_common/Use (QuotePart)/QuotePart.Utilities.cs new file mode 100644 index 000000000..487cc7173 --- /dev/null +++ b/src/_common/Use (QuotePart)/QuotePart.Utilities.cs @@ -0,0 +1,78 @@ +namespace Skender.Stock.Indicators; + +// QUOTEPART TYPE UTILITIES + +public static partial class QuoteParts +{ + // convert TQuote element to a basic QuotePart class + internal static QuotePart ToQuotePart(this IQuote q, CandlePart candlePart) + => new(q.Timestamp, q.ToQuotePartValue(candlePart)); + + // convert IQuote to value based on CandlePart + internal static double ToQuotePartValue(this IQuote q, CandlePart candlePart) + + => candlePart switch { + + CandlePart.Open => (double)q.Open, + CandlePart.High => (double)q.High, + CandlePart.Low => (double)q.Low, + CandlePart.Close => (double)q.Close, + CandlePart.Volume => (double)q.Volume, + CandlePart.HL2 => (double)(q.High + q.Low) / 2, + CandlePart.HLC3 => (double)(q.High + q.Low + q.Close) / 3, + CandlePart.OC2 => (double)(q.Open + q.Close) / 2, + CandlePart.OHL3 => (double)(q.Open + q.High + q.Low) / 3, + CandlePart.OHLC4 => (double)(q.Open + q.High + q.Low + q.Close) / 4, + + _ => throw new ArgumentOutOfRangeException( + nameof(candlePart), candlePart, "Invalid candlePart provided.") + }; + + // conditional HL2 value if IQuote type + internal static double Hl2OrValue( + this T item) + where T : IReusable + => item.QuotePartOrValue(CandlePart.HL2); + + // conditional CandlePart value if IQuote type + internal static double QuotePartOrValue( + this T item, CandlePart candlePart) + where T : IReusable + => item is IQuote q + ? q.ToQuotePartValue(candlePart) + : item.Value; + + /// + /// Uses CandlePart to sort and convert a list from an + /// to and type. + /// + /// + /// Use this conversion if your source list needs to + /// conditionally use something other than Close price + /// as the IReusable value. + /// + /// If you provide a list of + /// IReusable items that are not IQuote, it will simply + /// cast itself from . + /// + /// + /// + /// List of IQuote or IReusable items + /// + /// List of IReusable items + internal static IReadOnlyList ToPreferredList( + this IReadOnlyList items, CandlePart candlePart) + where T : IReusable + { + ArgumentNullException.ThrowIfNull(items); + + if (items is IReadOnlyList quotes) + { + return quotes.ToQuotePart(candlePart); + } + else + { + return items.Cast().ToList(); + } + } +} diff --git a/src/a-d/Adl/Adl.Api.cs b/src/a-d/Adl/Adl.Api.cs deleted file mode 100644 index 578643174..000000000 --- a/src/a-d/Adl/Adl.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ACCUMULATION/DISTRIBUTION LINE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetAdl( - this IEnumerable quotes, - int? smaPeriods = null) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcAdl(smaPeriods); -} diff --git a/src/a-d/Adl/Adl.Models.cs b/src/a-d/Adl/Adl.Models.cs index d8bbe1f88..4e62a66ee 100644 --- a/src/a-d/Adl/Adl.Models.cs +++ b/src/a-d/Adl/Adl.Models.cs @@ -1,17 +1,13 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class AdlResult : ResultBase, IReusableResult +public record AdlResult +( + DateTime Timestamp, + double Adl, + double? MoneyFlowMultiplier = null, + double? MoneyFlowVolume = null +) : IReusable { - public AdlResult(DateTime date) - { - Date = date; - } - - public double? MoneyFlowMultiplier { get; set; } - public double? MoneyFlowVolume { get; set; } - public double Adl { get; set; } - public double? AdlSma { get; set; } - - double? IReusableResult.Value => Adl; + public double Value => Adl; } diff --git a/src/a-d/Adl/Adl.Series.cs b/src/a-d/Adl/Adl.Series.cs deleted file mode 100644 index 57d125d0f..000000000 --- a/src/a-d/Adl/Adl.Series.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ACCUMULATION/DISTRIBUTION LINE (SERIES) -public static partial class Indicator -{ - internal static List CalcAdl( - this List qdList, - int? smaPeriods) - { - // check parameter arguments - ValidateAdl(smaPeriods); - - // initialize - List results = new(qdList.Count); - double prevAdl = 0; - - // roll through quotes - for (int i = 0; i < qdList.Count; i++) - { - QuoteD q = qdList[i]; - - double mfm = (q.High == q.Low) ? 0 : (q.Close - q.Low - (q.High - q.Close)) / (q.High - q.Low); - double mfv = mfm * q.Volume; - double adl = mfv + prevAdl; - - AdlResult r = new(q.Date) { - MoneyFlowMultiplier = mfm, - MoneyFlowVolume = mfv, - Adl = adl - }; - results.Add(r); - - prevAdl = adl; - - // optional SMA - if (smaPeriods != null && i + 1 >= smaPeriods) - { - double? sumSma = 0; - for (int p = i + 1 - (int)smaPeriods; p <= i; p++) - { - sumSma += results[p].Adl; - } - - r.AdlSma = sumSma / smaPeriods; - } - } - - return results; - } - - // parameter validation - private static void ValidateAdl( - int? smaPeriods) - { - // check parameter arguments - if (smaPeriods is not null and <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for ADL."); - } - } -} diff --git a/src/a-d/Adl/Adl.StaticSeries.cs b/src/a-d/Adl/Adl.StaticSeries.cs new file mode 100644 index 000000000..10f833ed4 --- /dev/null +++ b/src/a-d/Adl/Adl.StaticSeries.cs @@ -0,0 +1,33 @@ +namespace Skender.Stock.Indicators; + +// ACCUMULATION/DISTRIBUTION LINE (SERIES) + +public static partial class Adl +{ + public static IReadOnlyList ToAdl( + this IReadOnlyList source) + where TQuote : IQuote + { + ArgumentNullException.ThrowIfNull(source); + + // initialize + int length = source.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + AdlResult r = Increment( + source[i].Timestamp, + source[i].High, + source[i].Low, + source[i].Close, + source[i].Volume, + i > 0 ? results[i - 1].Adl : 0); + + results.Add(r); + } + + return results; + } +} diff --git a/src/a-d/Adl/Adl.StreamHub.cs b/src/a-d/Adl/Adl.StreamHub.cs new file mode 100644 index 000000000..e336e51b3 --- /dev/null +++ b/src/a-d/Adl/Adl.StreamHub.cs @@ -0,0 +1,48 @@ +namespace Skender.Stock.Indicators; + +// ACCUMULATION/DISTRIBUTION LINE (STREAM HUB) + +#region hub initializer + +public static partial class Adl +{ + public static AdlHub ToAdl( + this IQuoteProvider quoteProvider) + where TIn : IQuote + => new(quoteProvider); +} +#endregion + +public class AdlHub : ChainProvider + where TIn : IQuote +{ + #region constructors + + internal AdlHub(IQuoteProvider provider) + : base(provider) + { + Reinitialize(); + } + #endregion + + // METHODS + + protected override (AdlResult result, int index) + ToIndicator(TIn item, int? indexHint) + { + int i = indexHint ?? ProviderCache.GetIndex(item, true); + + // candidate result + AdlResult r = Adl.Increment( + item.Timestamp, + item.High, + item.Low, + item.Close, + item.Volume, + i > 0 ? Cache[i - 1].Value : 0); + + return (r, i); + } + + public override string ToString() => Cache.Count == 0 ? "ADL" : $"ADL({Cache[0].Timestamp:d})"; +} diff --git a/src/a-d/Adl/Adl.Utilities.cs b/src/a-d/Adl/Adl.Utilities.cs new file mode 100644 index 000000000..d964a392a --- /dev/null +++ b/src/a-d/Adl/Adl.Utilities.cs @@ -0,0 +1,60 @@ +namespace Skender.Stock.Indicators; + +// ACCUMULATION/DISTRIBUTION LINE (UTILITIES) + +/// +/// See the +/// Stock Indicators for .NET online guide for more information. +/// +public static partial class Adl +{ + /// + /// Get the next incremental Accumulation/Distribution Line(ADL) value. + /// + /// timestamp + /// High price, current period + /// Low price, current period + /// Close price, current period + /// Volume, current period + /// New ADL result value + /// + /// Last ADL value, from prior period + /// + public static AdlResult Increment( + DateTime timestamp, + double high, + double low, + double close, + double volume, + double prevAdl) + { + double mfm = high - low == 0 + ? 0 + : (close - low - (high - close)) + / (high - low); + + double mfv = mfm * volume; + double adl = mfv + prevAdl; + + return new AdlResult( + Timestamp: timestamp, + Adl: adl, + MoneyFlowMultiplier: mfm, + MoneyFlowVolume: mfv); + } + + internal static AdlResult Increment( + DateTime timestamp, + decimal high, + decimal low, + decimal close, + decimal volume, + double prevAdl) + => Increment( + timestamp, + (double)high, + (double)low, + (double)close, + (double)volume, + prevAdl); +} diff --git a/src/a-d/Adl/info.xml b/src/a-d/Adl/info.xml deleted file mode 100644 index ac4e19d27..000000000 --- a/src/a-d/Adl/info.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Accumulation/Distribution Line (ADL) is a rolling accumulation of Chaikin Money Flow Volume. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - Optional. Number of periods in the moving average of ADL. - Time series of ADL values. - Invalid parameter value provided. - \ No newline at end of file diff --git a/src/a-d/Adx/Adx.Api.cs b/src/a-d/Adx/Adx.Api.cs deleted file mode 100644 index 4d1ceac96..000000000 --- a/src/a-d/Adx/Adx.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// AVERAGE DIRECTIONAL INDEX (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetAdx( - this IEnumerable quotes, - int lookbackPeriods = 14) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcAdx(lookbackPeriods); -} diff --git a/src/a-d/Adx/Adx.Models.cs b/src/a-d/Adx/Adx.Models.cs index 3c179a51c..11a3bb56d 100644 --- a/src/a-d/Adx/Adx.Models.cs +++ b/src/a-d/Adx/Adx.Models.cs @@ -1,17 +1,14 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class AdxResult : ResultBase, IReusableResult +public record AdxResult +( + DateTime Timestamp, + double? Pdi = null, + double? Mdi = null, + double? Adx = null, + double? Adxr = null +) : IReusable { - public AdxResult(DateTime date) - { - Date = date; - } - - public double? Pdi { get; set; } - public double? Mdi { get; set; } - public double? Adx { get; set; } - public double? Adxr { get; set; } - - double? IReusableResult.Value => Adx; + public double Value => Adx.Null2NaN(); } diff --git a/src/a-d/Adx/Adx.Series.cs b/src/a-d/Adx/Adx.StaticSeries.cs similarity index 61% rename from src/a-d/Adx/Adx.Series.cs rename to src/a-d/Adx/Adx.StaticSeries.cs index 1c1b8d870..45839098a 100644 --- a/src/a-d/Adx/Adx.Series.cs +++ b/src/a-d/Adx/Adx.StaticSeries.cs @@ -1,17 +1,25 @@ namespace Skender.Stock.Indicators; // AVERAGE DIRECTIONAL INDEX (SERIES) -public static partial class Indicator + +public static partial class Adx { - internal static List CalcAdx( - this List qdList, - int lookbackPeriods) + public static IReadOnlyList ToAdx( + this IReadOnlyList quotes, + int lookbackPeriods = 14) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcAdx(lookbackPeriods); + + private static List CalcAdx( + this IReadOnlyList source, + int lookbackPeriods = 14) { // check parameter arguments - ValidateAdx(lookbackPeriods); + Validate(lookbackPeriods); // initialize - int length = qdList.Count; + int length = source.Count; List results = new(length); double prevHigh = 0; @@ -27,13 +35,10 @@ internal static List CalcAdx( double sumMdm = 0; double sumDx = 0; - // roll through quotes + // roll through source values for (int i = 0; i < length; i++) { - QuoteD q = qdList[i]; - - AdxResult r = new(q.Date); - results.Add(r); + QuoteD q = source[i]; // skip first period if (i == 0) @@ -41,6 +46,8 @@ internal static List CalcAdx( prevHigh = q.High; prevLow = q.Low; prevClose = q.Close; + + results.Add(new(Timestamp: q.Timestamp)); continue; } @@ -69,6 +76,7 @@ internal static List CalcAdx( // skip DM initialization period if (i < lookbackPeriods) { + results.Add(new(Timestamp: q.Timestamp)); continue; } @@ -77,6 +85,7 @@ internal static List CalcAdx( double pdm; double mdm; + // TODO: update healing, without requiring specific indexing if (i == lookbackPeriods) { trs = sumTr; @@ -85,17 +94,18 @@ internal static List CalcAdx( } else { - trs = prevTrs - (prevTrs / lookbackPeriods) + tr; - pdm = prevPdm - (prevPdm / lookbackPeriods) + pdm1; - mdm = prevMdm - (prevMdm / lookbackPeriods) + mdm1; + trs = prevTrs - prevTrs / lookbackPeriods + tr; + pdm = prevPdm - prevPdm / lookbackPeriods + pdm1; + mdm = prevMdm - prevMdm / lookbackPeriods + mdm1; } prevTrs = trs; prevPdm = pdm; prevMdm = mdm; - if (trs is 0) + if (trs == 0) { + results.Add(new(Timestamp: q.Timestamp)); continue; } @@ -103,57 +113,51 @@ internal static List CalcAdx( double pdi = 100 * pdm / trs; double mdi = 100 * mdm / trs; - r.Pdi = pdi; - r.Mdi = mdi; - // calculate ADX - double dx = (pdi == mdi) + double dx = pdi - mdi == 0 ? 0 - : (pdi + mdi != 0) + : pdi + mdi != 0 ? 100 * Math.Abs(pdi - mdi) / (pdi + mdi) : double.NaN; - double adx; + double adx = double.NaN; + double adxr = double.NaN; - if (i > (2 * lookbackPeriods) - 1) + if (i > 2 * lookbackPeriods - 1) { - adx = ((prevAdx * (lookbackPeriods - 1)) + dx) / lookbackPeriods; - r.Adx = adx.NaN2Null(); + adx = (prevAdx * (lookbackPeriods - 1) + dx) / lookbackPeriods; - double? priorAdx = results[i + 1 - lookbackPeriods].Adx; + double priorAdx = results[i - lookbackPeriods + 1].Adx.Null2NaN(); - r.Adxr = (adx + priorAdx).NaN2Null() / 2; + adxr = (adx + priorAdx) / 2; prevAdx = adx; } // initial ADX - else if (i == (2 * lookbackPeriods) - 1) + else if (i == 2 * lookbackPeriods - 1) { sumDx += dx; adx = sumDx / lookbackPeriods; - r.Adx = adx.NaN2Null(); prevAdx = adx; } // ADX initialization period + // TODO: update healing, without requiring specific indexing else { sumDx += dx; } - } - return results; - } + AdxResult r = new( + Timestamp: q.Timestamp, + Pdi: pdi, + Mdi: mdi, + Adx: adx.NaN2Null(), + Adxr: adxr.NaN2Null()); - // parameter validation - private static void ValidateAdx( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for ADX."); + results.Add(r); } + + return results; } } diff --git a/src/a-d/Adx/Adx.Utilities.cs b/src/a-d/Adx/Adx.Utilities.cs index 9f815a1ac..1611769cd 100644 --- a/src/a-d/Adx/Adx.Utilities.cs +++ b/src/a-d/Adx/Adx.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// AVERAGE DIRECTIONAL INDEX (UTILITIES) + +public static partial class Adx { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +15,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove((2 * n) + 100); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for ADX."); + } + } } diff --git a/src/a-d/Adx/info.xml b/src/a-d/Adx/info.xml index 36ca24a0a..578fe7127 100644 --- a/src/a-d/Adx/info.xml +++ b/src/a-d/Adx/info.xml @@ -1,18 +1,33 @@ - - Directional Movement Index (DMI) and Average Directional Movement Index (ADX) is a measure of price directional movement. - It includes upward and downward indicators, and is often used to measure strength of trend. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - Number of periods in the lookback window. - Time series of ADX and Plus/Minus Directional values. - Invalid parameter value provided. + + + Directional Movement Index (DMI) and Average Directional Movement Index (ADX) is a measure of price directional movement. + It includes upward and downward indicators, and is often used to measure strength of trend. + + See + documentation + for more information. + + + Configurable Quote type. See Guide for more information. + Historical price quotes. + Number of periods in the lookback window. + Time series of ADX and Plus/Minus Directional values. + Invalid parameter value provided. + + + + Get the next incremental Average Directional Movement Index (ADX) result. + + See + documentation + for more information. + + + Configurable Quote type. See Guide for more information. + Historical price quote. + New AdxResult value. + diff --git a/src/a-d/Alligator/Alligator.Api.cs b/src/a-d/Alligator/Alligator.Api.cs deleted file mode 100644 index 9fdbb8842..000000000 --- a/src/a-d/Alligator/Alligator.Api.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Skender.Stock.Indicators; - -// WILLIAMS ALLIGATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetAlligator( - this IEnumerable quotes, - int jawPeriods = 13, - int jawOffset = 8, - int teethPeriods = 8, - int teethOffset = 5, - int lipsPeriods = 5, - int lipsOffset = 3) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.HL2) - .CalcAlligator( - jawPeriods, - jawOffset, - teethPeriods, - teethOffset, - lipsPeriods, - lipsOffset); - - // SERIES, from CHAIN - public static IEnumerable GetAlligator( - this IEnumerable results, - int jawPeriods = 13, - int jawOffset = 8, - int teethPeriods = 8, - int teethOffset = 5, - int lipsPeriods = 5, - int lipsOffset = 3) => results - .ToTuple() - .CalcAlligator( - jawPeriods, - jawOffset, - teethPeriods, - teethOffset, - lipsPeriods, - lipsOffset) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetAlligator( - this IEnumerable<(DateTime, double)> priceTuples, - int jawPeriods = 13, - int jawOffset = 8, - int teethPeriods = 8, - int teethOffset = 5, - int lipsPeriods = 5, - int lipsOffset = 3) => priceTuples - .ToSortedList() - .CalcAlligator( - jawPeriods, - jawOffset, - teethPeriods, - teethOffset, - lipsPeriods, - lipsOffset); -} diff --git a/src/a-d/Alligator/Alligator.Models.cs b/src/a-d/Alligator/Alligator.Models.cs index 75f928142..3b689004b 100644 --- a/src/a-d/Alligator/Alligator.Models.cs +++ b/src/a-d/Alligator/Alligator.Models.cs @@ -1,14 +1,10 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class AlligatorResult : ResultBase -{ - public AlligatorResult(DateTime date) - { - Date = date; - } - - public double? Jaw { get; set; } - public double? Teeth { get; set; } - public double? Lips { get; set; } -} +public record AlligatorResult +( + DateTime Timestamp, + double? Jaw, + double? Teeth, + double? Lips +) : ISeries; diff --git a/src/a-d/Alligator/Alligator.Series.cs b/src/a-d/Alligator/Alligator.Series.cs deleted file mode 100644 index 7115eb881..000000000 --- a/src/a-d/Alligator/Alligator.Series.cs +++ /dev/null @@ -1,185 +0,0 @@ -namespace Skender.Stock.Indicators; - -// WILLIAMS ALLIGATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcAlligator( - this List<(DateTime Date, double Value)> tpList, - int jawPeriods, - int jawOffset, - int teethPeriods, - int teethOffset, - int lipsPeriods, - int lipsOffset) - { - // check parameter arguments - ValidateAlligator( - jawPeriods, - jawOffset, - teethPeriods, - teethOffset, - lipsPeriods, - lipsOffset); - - // initialize - int length = tpList.Count; - double[] pr = new double[length]; // median price - - List results = - tpList - .Select(x => new AlligatorResult(x.Date)) - .ToList(); - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime _, double value) = tpList[i]; - pr[i] = value; - - // only calculate jaw if the array offset is still in valid range - if (i + jawOffset < length) - { - AlligatorResult jawResult = results[i + jawOffset]; - - // calculate alligator's jaw - // first value: calculate SMA - if (i + 1 == jawPeriods) - { - double sumMedianPrice = 0; - for (int p = i + 1 - jawPeriods; p <= i; p++) - { - sumMedianPrice += pr[p]; - } - - jawResult.Jaw = sumMedianPrice / jawPeriods; - } - - // remaining values: SMMA - else if (i + 1 > jawPeriods) - { - double? prevValue = results[i + jawOffset - 1].Jaw; - jawResult.Jaw = ((prevValue * (jawPeriods - 1)) + pr[i]) / jawPeriods; - } - - jawResult.Jaw = jawResult.Jaw.NaN2Null(); - } - - // only calculate teeth if the array offset is still in valid range - if (i + teethOffset < length) - { - AlligatorResult teethResult = results[i + teethOffset]; - - // calculate alligator's teeth - // first value: calculate SMA - if (i + 1 == teethPeriods) - { - double sumMedianPrice = 0; - for (int p = i + 1 - teethPeriods; p <= i; p++) - { - sumMedianPrice += pr[p]; - } - - teethResult.Teeth = sumMedianPrice / teethPeriods; - } - - // remaining values: SMMA - else if (i + 1 > teethPeriods) - { - double? prevValue = results[i + teethOffset - 1].Teeth; - teethResult.Teeth = ((prevValue * (teethPeriods - 1)) + pr[i]) / teethPeriods; - } - - teethResult.Teeth = teethResult.Teeth.NaN2Null(); - } - - // only calculate lips if the array offset is still in valid range - if (i + lipsOffset < length) - { - AlligatorResult lipsResult = results[i + lipsOffset]; - - // calculate alligator's lips - // first value: calculate SMA - if (i + 1 == lipsPeriods) - { - double sumMedianPrice = 0; - for (int p = i + 1 - lipsPeriods; p <= i; p++) - { - sumMedianPrice += pr[p]; - } - - lipsResult.Lips = sumMedianPrice / lipsPeriods; - } - - // remaining values: SMMA - else if (i + 1 > lipsPeriods) - { - double? prevValue = results[i + lipsOffset - 1].Lips; - lipsResult.Lips = ((prevValue * (lipsPeriods - 1)) + pr[i]) / lipsPeriods; - } - - lipsResult.Lips = lipsResult.Lips.NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidateAlligator( - int jawPeriods, - int jawOffset, - int teethPeriods, - int teethOffset, - int lipsPeriods, - int lipsOffset) - { - // check parameter arguments - if (jawPeriods <= teethPeriods) - { - throw new ArgumentOutOfRangeException(nameof(jawPeriods), jawPeriods, - "Jaw lookback periods must be greater than Teeth lookback periods for Alligator."); - } - - if (teethPeriods <= lipsPeriods) - { - throw new ArgumentOutOfRangeException(nameof(teethPeriods), teethPeriods, - "Teeth lookback periods must be greater than Lips lookback periods for Alligator."); - } - - if (lipsPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lipsPeriods), lipsPeriods, - "Lips lookback periods must be greater than 0 for Alligator."); - } - - if (jawOffset <= 0) - { - throw new ArgumentOutOfRangeException(nameof(jawOffset), jawOffset, - "Jaw offset periods must be greater than 0 for Alligator."); - } - - if (teethOffset <= 0) - { - throw new ArgumentOutOfRangeException(nameof(teethOffset), teethOffset, - "Jaw offset periods must be greater than 0 for Alligator."); - } - - if (lipsOffset <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lipsOffset), lipsOffset, - "Jaw offset periods must be greater than 0 for Alligator."); - } - - if (jawPeriods + jawOffset <= teethPeriods + teethOffset) - { - throw new ArgumentOutOfRangeException(nameof(jawPeriods), jawPeriods, - "Jaw lookback + offset are too small for Alligator."); - } - - if (teethPeriods + teethOffset <= lipsPeriods + lipsOffset) - { - throw new ArgumentOutOfRangeException(nameof(teethPeriods), teethPeriods, - "Teeth lookback + offset are too small for Alligator."); - } - } -} diff --git a/src/a-d/Alligator/Alligator.StaticSeries.cs b/src/a-d/Alligator/Alligator.StaticSeries.cs new file mode 100644 index 000000000..f35217b70 --- /dev/null +++ b/src/a-d/Alligator/Alligator.StaticSeries.cs @@ -0,0 +1,143 @@ +namespace Skender.Stock.Indicators; + +// WILLIAMS ALLIGATOR (SERIES) + +public static partial class Alligator +{ + // SERIES, from CHAIN + /// + /// Williams Alligator is an indicator that transposes multiple moving averages, + /// showing chart patterns that creator Bill Williams compared to an alligator's + /// feeding habits when describing market movement. + /// + /// + /// T must be or type + /// + /// Time-series values to transform. + /// Lookback periods for the Jaw line. + /// Offset periods for the Jaw line. + /// Lookback periods for the Teeth line. + /// Offset periods for the Teeth line. + /// Lookback periods for the Lips line. + /// Offset periods for the Lips line. + /// Time series of Alligator values. + /// + /// Invalid parameter value provided. + /// + public static IReadOnlyList ToAlligator( + this IReadOnlyList source, + int jawPeriods = 13, + int jawOffset = 8, + int teethPeriods = 8, + int teethOffset = 5, + int lipsPeriods = 5, + int lipsOffset = 3) + where T : IReusable + { + // check parameter arguments + Validate( + jawPeriods, + jawOffset, + teethPeriods, + teethOffset, + lipsPeriods, + lipsOffset); + + // prefer HL2 when IQuote + IReadOnlyList values + = source.ToPreferredList(CandlePart.HL2); + + // initialize + int length = values.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + double jaw = double.NaN; + double lips = double.NaN; + double teeth = double.NaN; + + // calculate alligator's jaw, when in range + if (i >= jawPeriods + jawOffset - 1) + { + // first/reset value: calculate SMA + if (results[i - 1].Jaw is null) + { + double sum = 0; + for (int p = i - jawPeriods - jawOffset + 1; p <= i - jawOffset; p++) + { + sum += values[p].Value; + } + + jaw = sum / jawPeriods; + } + + // remaining values: SMMA + else + { + double prevJaw = results[i - 1].Jaw.Null2NaN(); + + jaw = ((prevJaw * (jawPeriods - 1)) + values[i - jawOffset].Value) / jawPeriods; + } + } + + // calculate alligator's teeth, when in range + if (i >= teethPeriods + teethOffset - 1) + { + // first/reset value: calculate SMA + if (results[i - 1].Teeth is null) + { + double sum = 0; + for (int p = i - teethPeriods - teethOffset + 1; p <= i - teethOffset; p++) + { + sum += values[p].Value; + } + + teeth = sum / teethPeriods; + } + + // remaining values: SMMA + else + { + double prevTooth = results[i - 1].Teeth.Null2NaN(); + + teeth = ((prevTooth * (teethPeriods - 1)) + values[i - teethOffset].Value) / teethPeriods; + } + } + + // calculate alligator's lips, when in range + if (i >= lipsPeriods + lipsOffset - 1) + { + // first/reset value: calculate SMA + if (results[i - 1].Lips is null) + { + double sum = 0; + for (int p = i - lipsPeriods - lipsOffset + 1; p <= i - lipsOffset; p++) + { + sum += values[p].Value; + } + + lips = sum / lipsPeriods; + } + + // remaining values: SMMA + else + { + double prevLips = results[i - 1].Lips.Null2NaN(); + + lips = ((prevLips * (lipsPeriods - 1)) + values[i - lipsOffset].Value) / lipsPeriods; + } + } + + // result + results.Add(new AlligatorResult( + values[i].Timestamp, + jaw.NaN2Null(), + teeth.NaN2Null(), + lips.NaN2Null())); + } + + return results; + } +} diff --git a/src/a-d/Alligator/Alligator.StreamHub.cs b/src/a-d/Alligator/Alligator.StreamHub.cs new file mode 100644 index 000000000..8ade9b1ac --- /dev/null +++ b/src/a-d/Alligator/Alligator.StreamHub.cs @@ -0,0 +1,177 @@ +namespace Skender.Stock.Indicators; + +// WILLIAMS ALLIGATOR (STREAM HUB) + +#region hub interface and initializer + +public interface IAlligatorHub +{ + int JawPeriods { get; } + int JawOffset { get; } + int TeethPeriods { get; } + int TeethOffset { get; } + int LipsPeriods { get; } + int LipsOffset { get; } +} + +public static partial class Alligator +{ + // HUB, from Chain Provider + public static AlligatorHub ToAlligator( + this IChainProvider chainProvider, + int jawPeriods = 13, + int jawOffset = 8, + int teethPeriods = 8, + int teethOffset = 5, + int lipsPeriods = 5, + int lipsOffset = 3) + where TIn : IReusable + => new( + chainProvider, + jawPeriods, + jawOffset, + teethPeriods, + teethOffset, + lipsPeriods, + lipsOffset); +} +#endregion + +public class AlligatorHub + : StreamHub, IAlligatorHub + where TIn : IReusable +{ + #region constructors + + private readonly string hubName; + + internal AlligatorHub( + IChainProvider provider, + int jawPeriods, int jawOffset, + int teethPeriods, int teethOffset, + int lipsPeriods, int lipsOffset) + : base(provider) + { + Alligator.Validate( + jawPeriods, jawOffset, + teethPeriods, teethOffset, + lipsPeriods, lipsOffset); + + JawPeriods = jawPeriods; + JawOffset = jawOffset; + TeethPeriods = teethPeriods; + TeethOffset = teethOffset; + LipsPeriods = lipsPeriods; + LipsOffset = lipsOffset; + + hubName = $"ALLIGATOR({jawPeriods},{jawOffset},{teethPeriods},{teethOffset},{lipsPeriods},{lipsOffset})"; + + Reinitialize(); + } + #endregion + + public int JawPeriods { get; init; } + public int JawOffset { get; init; } + public int TeethPeriods { get; init; } + public int TeethOffset { get; init; } + public int LipsPeriods { get; init; } + public int LipsOffset { get; init; } + + // METHODS + + public override string ToString() => hubName; + + protected override (AlligatorResult result, int index) + ToIndicator(TIn item, int? indexHint) + { + double jaw = double.NaN; + double lips = double.NaN; + double teeth = double.NaN; + + int i = indexHint ?? ProviderCache.GetIndex(item, true); + + // calculate alligator's jaw, when in range + if (i >= JawPeriods + JawOffset - 1) + { + // first/reset value: calculate SMA + if (Cache[i - 1].Jaw is null) + { + double sum = 0; + for (int p = i - JawPeriods - JawOffset + 1; p <= i - JawOffset; p++) + { + sum += ProviderCache[p].Hl2OrValue(); + } + + jaw = sum / JawPeriods; + } + + // remaining values: SMMA + else + { + double prevJaw = Cache[i - 1].Jaw.Null2NaN(); + double newVal = ProviderCache[i - JawOffset].Hl2OrValue(); + + jaw = ((prevJaw * (JawPeriods - 1)) + newVal) / JawPeriods; + } + } + + // calculate alligator's teeth, when in range + if (i >= TeethPeriods + TeethOffset - 1) + { + // first/reset value: calculate SMA + if (Cache[i - 1].Teeth is null) + { + double sum = 0; + for (int p = i - TeethPeriods - TeethOffset + 1; p <= i - TeethOffset; p++) + { + sum += ProviderCache[p].Hl2OrValue(); + } + + teeth = sum / TeethPeriods; + } + + // remaining values: SMMA + else + { + double prevTooth = Cache[i - 1].Teeth.Null2NaN(); + double newVal = ProviderCache[i - TeethOffset].Hl2OrValue(); + + teeth = ((prevTooth * (TeethPeriods - 1)) + newVal) / TeethPeriods; + } + } + + // calculate alligator's lips, when in range + if (i >= LipsPeriods + LipsOffset - 1) + { + // first/reset value: calculate SMA + if (Cache[i - 1].Lips is null) + { + double sum = 0; + for (int p = i - LipsPeriods - LipsOffset + 1; p <= i - LipsOffset; p++) + { + sum += ProviderCache[p].Hl2OrValue(); + } + + lips = sum / LipsPeriods; + } + + // remaining values: SMMA + else + { + double prevLips = Cache[i - 1].Lips.Null2NaN(); + double newVal = ProviderCache[i - LipsOffset].Hl2OrValue(); + + lips = ((prevLips * (LipsPeriods - 1)) + newVal) / LipsPeriods; + } + } + + // candidate result + AlligatorResult r = new( + item.Timestamp, + jaw.NaN2Null(), + teeth.NaN2Null(), + lips.NaN2Null()); + + return (r, i); + } +} diff --git a/src/a-d/Alligator/Alligator.Utilities.cs b/src/a-d/Alligator/Alligator.Utilities.cs index e45557794..03050cb34 100644 --- a/src/a-d/Alligator/Alligator.Utilities.cs +++ b/src/a-d/Alligator/Alligator.Utilities.cs @@ -1,11 +1,11 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// WILLIAMS ALLIGATOR (UTILITIES) + +public static partial class Alligator { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( + public static IReadOnlyList Condense( this IEnumerable results) { List resultsList = results @@ -19,10 +19,8 @@ public static IEnumerable Condense( } // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -30,4 +28,63 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int jawPeriods, + int jawOffset, + int teethPeriods, + int teethOffset, + int lipsPeriods, + int lipsOffset) + { + // check parameter arguments + if (jawPeriods <= teethPeriods) + { + throw new ArgumentOutOfRangeException(nameof(jawPeriods), jawPeriods, + "Jaw lookback periods must be greater than Teeth lookback periods for Alligator."); + } + + if (teethPeriods <= lipsPeriods) + { + throw new ArgumentOutOfRangeException(nameof(teethPeriods), teethPeriods, + "Teeth lookback periods must be greater than Lips lookback periods for Alligator."); + } + + if (lipsPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lipsPeriods), lipsPeriods, + "Lips lookback periods must be greater than 0 for Alligator."); + } + + if (jawOffset <= 0) + { + throw new ArgumentOutOfRangeException(nameof(jawOffset), jawOffset, + "Jaw offset periods must be greater than 0 for Alligator."); + } + + if (teethOffset <= 0) + { + throw new ArgumentOutOfRangeException(nameof(teethOffset), teethOffset, + "Jaw offset periods must be greater than 0 for Alligator."); + } + + if (lipsOffset <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lipsOffset), lipsOffset, + "Jaw offset periods must be greater than 0 for Alligator."); + } + + if (jawPeriods + jawOffset <= teethPeriods + teethOffset) + { + throw new ArgumentOutOfRangeException(nameof(jawPeriods), jawPeriods, + "Jaw lookback + offset are too small for Alligator."); + } + + if (teethPeriods + teethOffset <= lipsPeriods + lipsOffset) + { + throw new ArgumentOutOfRangeException(nameof(teethPeriods), teethPeriods, + "Teeth lookback + offset are too small for Alligator."); + } + } } diff --git a/src/a-d/Alligator/info.xml b/src/a-d/Alligator/info.xml deleted file mode 100644 index fff1c5de1..000000000 --- a/src/a-d/Alligator/info.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - Williams Alligator is an indicator that transposes multiple moving averages, - showing chart patterns that creator Bill Williams compared to an alligator's - feeding habits when describing market movement. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - Lookback periods for the Jaw line. - Offset periods for the Jaw line. - Lookback periods for the Teeth line. - Offset periods for the Teeth line. - Lookback periods for the Lips line. - Offset periods for the Lips line. - Time series of Alligator values. - Invalid parameter value provided. - diff --git a/src/a-d/Alma/Alma.Api.cs b/src/a-d/Alma/Alma.Api.cs deleted file mode 100644 index ba18029a1..000000000 --- a/src/a-d/Alma/Alma.Api.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ARNAUD LEGOUX MOVING AVERAGE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetAlma( - this IEnumerable quotes, - int lookbackPeriods = 9, - double offset = 0.85, - double sigma = 6) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcAlma(lookbackPeriods, offset, sigma); - - // SERIES, from CHAIN - public static IEnumerable GetAlma( - this IEnumerable results, - int lookbackPeriods = 9, - double offset = 0.85, - double sigma = 6) => results - .ToTuple() - .CalcAlma(lookbackPeriods, offset, sigma) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetAlma( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods = 9, - double offset = 0.85, - double sigma = 6) => priceTuples - .ToSortedList() - .CalcAlma(lookbackPeriods, offset, sigma); -} diff --git a/src/a-d/Alma/Alma.Models.cs b/src/a-d/Alma/Alma.Models.cs index 1ce7e3570..419a23d56 100644 --- a/src/a-d/Alma/Alma.Models.cs +++ b/src/a-d/Alma/Alma.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class AlmaResult : ResultBase, IReusableResult +public record AlmaResult +( + DateTime Timestamp, + double? Alma +) : IReusable { - public AlmaResult(DateTime date) - { - Date = date; - } - - public double? Alma { get; set; } - - double? IReusableResult.Value => Alma; + public double Value => Alma.Null2NaN(); } diff --git a/src/a-d/Alma/Alma.Series.cs b/src/a-d/Alma/Alma.Series.cs deleted file mode 100644 index 6644e1caf..000000000 --- a/src/a-d/Alma/Alma.Series.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ARNAUD LEGOUX MOVING AVERAGE (SERIES) -public static partial class Indicator -{ - internal static List CalcAlma( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double offset, - double sigma) - { - // check parameter arguments - ValidateAlma(lookbackPeriods, offset, sigma); - - // initialize - List results = new(tpList.Count); - - // determine price weights - double m = offset * (lookbackPeriods - 1); - double s = lookbackPeriods / sigma; - - double[] weight = new double[lookbackPeriods]; - double norm = 0; - - for (int i = 0; i < lookbackPeriods; i++) - { - double wt = Math.Exp(-((i - m) * (i - m)) / (2 * s * s)); - weight[i] = wt; - norm += wt; - } - - // roll through quotes - for (int i = 0; i < tpList.Count; i++) - { - (DateTime date, double _) = tpList[i]; - - AlmaResult r = new(date); - results.Add(r); - - if (i + 1 >= lookbackPeriods) - { - double? weightedSum = 0; - int n = 0; - - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - (DateTime _, double pValue) = tpList[p]; - weightedSum += weight[n] * pValue; - n++; - } - - r.Alma = (weightedSum / norm).NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidateAlma( - int lookbackPeriods, - double offset, - double sigma) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for ALMA."); - } - - if (offset is < 0 or > 1) - { - throw new ArgumentOutOfRangeException(nameof(offset), offset, - "Offset must be between 0 and 1 for ALMA."); - } - - if (sigma <= 0) - { - throw new ArgumentOutOfRangeException(nameof(sigma), sigma, - "Sigma must be greater than 0 for ALMA."); - } - } -} diff --git a/src/a-d/Alma/Alma.StaticSeries.cs b/src/a-d/Alma/Alma.StaticSeries.cs new file mode 100644 index 000000000..372f68c9e --- /dev/null +++ b/src/a-d/Alma/Alma.StaticSeries.cs @@ -0,0 +1,63 @@ +namespace Skender.Stock.Indicators; + +// ARNAUD LEGOUX MOVING AVERAGE (SERIES) + +public static partial class Alma +{ + public static IReadOnlyList ToAlma( + this IReadOnlyList source, + int lookbackPeriods = 9, + double offset = 0.85, + double sigma = 6) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods, offset, sigma); + + // initialize + int length = source.Count; + List results = new(length); + + // determine price weight constants + double m = offset * (lookbackPeriods - 1); + double s = lookbackPeriods / sigma; + + double[] weight = new double[lookbackPeriods]; + double norm = 0; + + for (int i = 0; i < lookbackPeriods; i++) + { + double wt = Math.Exp(-((i - m) * (i - m)) / (2 * s * s)); + weight[i] = wt; + norm += wt; + } + + // roll through source values + for (int i = 0; i < length; i++) + { + double alma = double.NaN; + + if (i + 1 >= lookbackPeriods) + { + double weightedSum = 0; + int n = 0; + + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + T ps = source[p]; + weightedSum += weight[n] * ps.Value; + n++; + } + + alma = weightedSum / norm; + } + + results.Add( + new(Timestamp: source[i].Timestamp, + Alma: alma.NaN2Null())); + } + + return results; + } +} diff --git a/src/a-d/Alma/Alma.Utilities.cs b/src/a-d/Alma/Alma.Utilities.cs index 305067db9..da7f11eb8 100644 --- a/src/a-d/Alma/Alma.Utilities.cs +++ b/src/a-d/Alma/Alma.Utilities.cs @@ -1,17 +1,32 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// ARNAUD LEGOUX MOVING AVERAGE (UTILITIES) + +public static partial class Alma { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods, + double offset, + double sigma) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Alma != null); + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for ALMA."); + } + + if (offset is < 0 or > 1) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, + "Offset must be between 0 and 1 for ALMA."); + } - return results.Remove(removePeriods); + if (sigma <= 0) + { + throw new ArgumentOutOfRangeException(nameof(sigma), sigma, + "Sigma must be greater than 0 for ALMA."); + } } } diff --git a/src/a-d/Aroon/Aroon.Api.cs b/src/a-d/Aroon/Aroon.Api.cs deleted file mode 100644 index ca259c74b..000000000 --- a/src/a-d/Aroon/Aroon.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// AROON OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetAroon( - this IEnumerable quotes, - int lookbackPeriods = 25) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcAroon(lookbackPeriods); -} diff --git a/src/a-d/Aroon/Aroon.Models.cs b/src/a-d/Aroon/Aroon.Models.cs index 50fdc2ddf..5c4adcdda 100644 --- a/src/a-d/Aroon/Aroon.Models.cs +++ b/src/a-d/Aroon/Aroon.Models.cs @@ -1,16 +1,13 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class AroonResult : ResultBase, IReusableResult +public record AroonResult +( + DateTime Timestamp, + double? AroonUp, + double? AroonDown, + double? Oscillator +) : IReusable { - public AroonResult(DateTime date) - { - Date = date; - } - - public double? AroonUp { get; set; } - public double? AroonDown { get; set; } - public double? Oscillator { get; set; } - - double? IReusableResult.Value => Oscillator; + public double Value => Oscillator.Null2NaN(); } diff --git a/src/a-d/Aroon/Aroon.Series.cs b/src/a-d/Aroon/Aroon.Series.cs deleted file mode 100644 index 4af941670..000000000 --- a/src/a-d/Aroon/Aroon.Series.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Skender.Stock.Indicators; - -// AROON OSCILLATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcAroon( - this List qdList, - int lookbackPeriods) - { - // check parameter arguments - ValidateAroon(lookbackPeriods); - - // initialize - List results = new(qdList.Count); - - // roll through quotes - for (int i = 0; i < qdList.Count; i++) - { - QuoteD q = qdList[i]; - - AroonResult r = new(q.Date); - results.Add(r); - - // add aroons - if (i + 1 > lookbackPeriods) - { - double? lastHighPrice = 0; - double? lastLowPrice = double.MaxValue; - int lastHighIndex = 0; - int lastLowIndex = 0; - - for (int p = i + 1 - lookbackPeriods - 1; p <= i; p++) - { - QuoteD d = qdList[p]; - - if (d.High > lastHighPrice) - { - lastHighPrice = d.High; - lastHighIndex = p + 1; - } - - if (d.Low < lastLowPrice) - { - lastLowPrice = d.Low; - lastLowIndex = p + 1; - } - } - - r.AroonUp = 100d * (lookbackPeriods - (i + 1 - lastHighIndex)) / lookbackPeriods; - r.AroonDown = 100d * (lookbackPeriods - (i + 1 - lastLowIndex)) / lookbackPeriods; - r.Oscillator = r.AroonUp - r.AroonDown; - } - } - - return results; - } - - // parameter validation - private static void ValidateAroon( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Aroon."); - } - } -} diff --git a/src/a-d/Aroon/Aroon.StaticSeries.cs b/src/a-d/Aroon/Aroon.StaticSeries.cs new file mode 100644 index 000000000..4fcdbe3be --- /dev/null +++ b/src/a-d/Aroon/Aroon.StaticSeries.cs @@ -0,0 +1,73 @@ +namespace Skender.Stock.Indicators; + +// AROON OSCILLATOR (SERIES) + +public static partial class Aroon +{ + public static IReadOnlyList ToAroon( + this IReadOnlyList quotes, + int lookbackPeriods = 25) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcAroon(lookbackPeriods); + + private static List CalcAroon( + this IReadOnlyList source, + int lookbackPeriods) + { + // check parameter arguments + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + double? aroonUp = null; + double? aroonDown = null; + + // add aroons + if (i + 1 > lookbackPeriods) + { + double? lastHighPrice = 0; + double? lastLowPrice = double.MaxValue; + int lastHighIndex = 0; + int lastLowIndex = 0; + + for (int p = i + 1 - lookbackPeriods - 1; p <= i; p++) + { + QuoteD d = source[p]; + + if (d.High > lastHighPrice) + { + lastHighPrice = d.High; + lastHighIndex = p + 1; + } + + if (d.Low < lastLowPrice) + { + lastLowPrice = d.Low; + lastLowIndex = p + 1; + } + } + + aroonUp = 100d * (lookbackPeriods - (i + 1 - lastHighIndex)) / lookbackPeriods; + aroonDown = 100d * (lookbackPeriods - (i + 1 - lastLowIndex)) / lookbackPeriods; + } + + AroonResult r = new( + Timestamp: q.Timestamp, + AroonUp: aroonUp, + AroonDown: aroonDown, + Oscillator: aroonUp - aroonDown); + + results.Add(r); + + } + + return results; + } +} diff --git a/src/a-d/Aroon/Aroon.Utilities.cs b/src/a-d/Aroon/Aroon.Utilities.cs index 619c2ac3b..32f51769d 100644 --- a/src/a-d/Aroon/Aroon.Utilities.cs +++ b/src/a-d/Aroon/Aroon.Utilities.cs @@ -1,17 +1,16 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +public static partial class Aroon { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Oscillator != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Aroon."); + } } } diff --git a/src/a-d/Atr/Atr.Api.cs b/src/a-d/Atr/Atr.Api.cs deleted file mode 100644 index 82bd946f5..000000000 --- a/src/a-d/Atr/Atr.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// AVERAGE TRUE RANGE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetAtr( - this IEnumerable quotes, - int lookbackPeriods = 14) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcAtr(lookbackPeriods); -} diff --git a/src/a-d/Atr/Atr.Models.cs b/src/a-d/Atr/Atr.Models.cs index d2980015c..67097be0f 100644 --- a/src/a-d/Atr/Atr.Models.cs +++ b/src/a-d/Atr/Atr.Models.cs @@ -1,16 +1,13 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class AtrResult : ResultBase, IReusableResult +public record AtrResult +( + DateTime Timestamp, + double? Tr = null, + double? Atr = null, + double? Atrp = null +) : IReusable { - public AtrResult(DateTime date) - { - Date = date; - } - - public double? Tr { get; set; } - public double? Atr { get; set; } - public double? Atrp { get; set; } - - double? IReusableResult.Value => Atrp; + public double Value => Atrp.Null2NaN(); } diff --git a/src/a-d/Atr/Atr.Series.cs b/src/a-d/Atr/Atr.Series.cs deleted file mode 100644 index d04a72d2b..000000000 --- a/src/a-d/Atr/Atr.Series.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace Skender.Stock.Indicators; - -// AVERAGE TRUE RANGE (SERIES) -public static partial class Indicator -{ - // calculate series - internal static List CalcAtr( - this List qdList, - int lookbackPeriods) - { - // check parameter arguments - ValidateAtr(lookbackPeriods); - - // initialize - List results = new(qdList.Count); - double prevAtr = double.NaN; - double prevClose = double.NaN; - double sumTr = 0; - - // roll through quotes - for (int i = 0; i < qdList.Count; i++) - { - double hmpc; - double lmpc; - QuoteD q = qdList[i]; - - AtrResult r = new(q.Date); - results.Add(r); - - if (i > 0) - { - hmpc = Math.Abs(q.High - prevClose); - lmpc = Math.Abs(q.Low - prevClose); - } - else - { - prevClose = q.Close; - continue; - } - - double tr = Math.Max(q.High - q.Low, Math.Max(hmpc, lmpc)); - r.Tr = tr; - - if (i > lookbackPeriods) - { - // calculate ATR - double atr = ((prevAtr * (lookbackPeriods - 1)) + tr) / lookbackPeriods; - r.Atr = atr; - r.Atrp = (q.Close == 0) ? null : atr / q.Close * 100; - prevAtr = atr; - } - else if (i == lookbackPeriods) - { - // initialize ATR - sumTr += tr; - double atr = sumTr / lookbackPeriods; - r.Atr = atr; - r.Atrp = (q.Close == 0) ? null : atr / q.Close * 100; - prevAtr = atr; - } - else - { - // only used for periods before ATR initialization - sumTr += tr; - } - - prevClose = q.Close; - } - - return results; - } - - // parameter validation - private static void ValidateAtr( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for Average True Range."); - } - } -} diff --git a/src/a-d/Atr/Atr.StaticSeries.cs b/src/a-d/Atr/Atr.StaticSeries.cs new file mode 100644 index 000000000..10db80315 --- /dev/null +++ b/src/a-d/Atr/Atr.StaticSeries.cs @@ -0,0 +1,90 @@ +namespace Skender.Stock.Indicators; + +// AVERAGE TRUE RANGE (SERIES) + +public static partial class Atr +{ + public static IReadOnlyList ToAtr( + this IReadOnlyList quotes, + int lookbackPeriods = 14) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcAtr(lookbackPeriods); + + internal static List CalcAtr( + this IReadOnlyList source, + int lookbackPeriods) + { + // check parameter arguments + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + double prevAtr = double.NaN; + double prevClose = double.NaN; + double sumTr = 0; + + // skip first period + if (length > 0) + { + QuoteD q = source[0]; + results.Add(new(Timestamp: q.Timestamp)); + prevClose = q.Close; + } + + // roll through source values + for (int i = 1; i < length; i++) + { + QuoteD q = source[i]; + + double hmpc = Math.Abs(q.High - prevClose); + double lmpc = Math.Abs(q.Low - prevClose); + + double tr = Math.Max(q.High - q.Low, Math.Max(hmpc, lmpc)); + + double atr; + double? atrp; + + if (i > lookbackPeriods) + { + // calculate ATR + atr = ((prevAtr * (lookbackPeriods - 1)) + tr) / lookbackPeriods; + atrp = q.Close == 0 ? null : atr / q.Close * 100; + prevAtr = atr; + } + + // TODO: update healing, without requiring specific indexing, + // have had trouble gettng this one to work when evaluating previous ATR values + else if (i == lookbackPeriods) + { + // initialize ATR + sumTr += tr; + atr = sumTr / lookbackPeriods; + atrp = q.Close == 0 ? null : atr / q.Close * 100; + prevAtr = atr; + } + + // only used for initialization periods + else + { + sumTr += tr; + + atr = double.NaN; + atrp = null; + } + + AtrResult r = new( + Timestamp: q.Timestamp, + Tr: tr.NaN2Null(), + Atr: atr.NaN2Null(), + Atrp: atrp); + + results.Add(r); + + prevClose = q.Close; + } + + return results; + } +} diff --git a/src/a-d/Atr/Atr.StreamHub.cs b/src/a-d/Atr/Atr.StreamHub.cs new file mode 100644 index 000000000..d37d45062 --- /dev/null +++ b/src/a-d/Atr/Atr.StreamHub.cs @@ -0,0 +1,98 @@ +namespace Skender.Stock.Indicators; + +// AVERAGE TRUE RANGE (STREAM HUB) + +#region hub interface and initializer + +public interface IAtrHub +{ + int LookbackPeriods { get; } +} + +public static partial class Atr +{ + public static AtrHub ToAtr( + this IQuoteProvider quoteProvider, + int lookbackPeriods = 14) + where TIn : IQuote + => new(quoteProvider, lookbackPeriods); +} +#endregion + +public class AtrHub + : ChainProvider, IAtrHub + where TIn : IQuote +{ + #region constructors + + private readonly string hubName; + + internal AtrHub(IQuoteProvider provider, + int lookbackPeriods) + : base(provider) + { + Atr.Validate(lookbackPeriods); + LookbackPeriods = lookbackPeriods; + hubName = $"ATR({lookbackPeriods})"; + + Reinitialize(); + } + #endregion + + public int LookbackPeriods { get; init; } + + // METHODS + + public override string ToString() => hubName; + + protected override (AtrResult result, int index) + ToIndicator(TIn item, int? indexHint) + { + int i = indexHint ?? ProviderCache.GetIndex(item, true); + + // skip incalculable periods + if (i == 0) + { + return (new AtrResult(item.Timestamp), i); + } + + AtrResult r; + + // re-initialize as average TR, if necessary + if (Cache[i - 1].Atr is null && i >= LookbackPeriods) + { + double sumTr = 0; + double tr = double.NaN; + + for (int p = i - LookbackPeriods + 1; p <= i; p++) + { + tr = Tr.Increment( + (double)ProviderCache[p].High, + (double)ProviderCache[p].Low, + (double)ProviderCache[p - 1].Close); + + sumTr += tr; + } + + double atr = sumTr / LookbackPeriods; + + r = new AtrResult( + item.Timestamp, + tr, + atr, + atr / (double)item.Close * 100); + } + + // calculate ATR (normally) + else + { + r = Atr.Increment( + LookbackPeriods, + item, + (double)ProviderCache[i - 1].Close, + Cache[i - 1].Atr); + } + + return (r, i); + } +} diff --git a/src/a-d/Atr/Atr.Utilities.cs b/src/a-d/Atr/Atr.Utilities.cs index 700f6f584..febeb0aa6 100644 --- a/src/a-d/Atr/Atr.Utilities.cs +++ b/src/a-d/Atr/Atr.Utilities.cs @@ -1,17 +1,53 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +public static partial class Atr { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // increment + internal static double Increment( + int lookbackPeriods, + double high, + double low, + double prevClose, + double prevAtr) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Atr != null); + double tr = Tr.Increment(high, low, prevClose); + return ((prevAtr * (lookbackPeriods - 1)) + tr) / lookbackPeriods; - return results.Remove(removePeriods); + // TODO: this may be unused, verify before making public + } + + // increment + public static AtrResult Increment( + int lookbackPeriods, + TQuote quote, + double prevClose, + double? prevAtr) + where TQuote : IQuote + { + double high = (double)quote.High; + double low = (double)quote.Low; + double close = (double)quote.Close; + + double tr = Tr.Increment(high, low, prevClose); + double atr = (((prevAtr ?? double.NaN) * (lookbackPeriods - 1)) + tr) / lookbackPeriods; + double atrp = close == 0 ? double.NaN : atr / close * 100; + + return new AtrResult( + quote.Timestamp, + tr, + atr.NaN2Null(), + atrp.NaN2Null()); + } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for Average True Range."); + } } } diff --git a/src/a-d/AtrStop/AtrStop.Api.cs b/src/a-d/AtrStop/AtrStop.Api.cs deleted file mode 100644 index ce988ee85..000000000 --- a/src/a-d/AtrStop/AtrStop.Api.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ATR TRAILING STOP (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetAtrStop( - this IEnumerable quotes, - int lookbackPeriods = 21, - double multiplier = 3, - EndType endType = EndType.Close) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcAtrStop(lookbackPeriods, multiplier, endType); -} diff --git a/src/a-d/AtrStop/AtrStop.Models.cs b/src/a-d/AtrStop/AtrStop.Models.cs index 13366f719..35e4938e1 100644 --- a/src/a-d/AtrStop/AtrStop.Models.cs +++ b/src/a-d/AtrStop/AtrStop.Models.cs @@ -1,14 +1,28 @@ namespace Skender.Stock.Indicators; +/// +/// ATR Trailing Stop result +/// +/// +/// Date corresponding to evaluated price data +/// +/// +/// Trailing stop line (includes both buy and sell stops) +/// +/// +/// Stop line (buy to close) short position +/// +/// +/// Stop line (sell to close) long position +/// +/// +/// Average True Range (ATR) +/// [Serializable] -public sealed class AtrStopResult : ResultBase -{ - public AtrStopResult(DateTime date) - { - Date = date; - } - - public decimal? AtrStop { get; set; } - public decimal? BuyStop { get; set; } - public decimal? SellStop { get; set; } -} +public record AtrStopResult( + DateTime Timestamp, + double? AtrStop = null, + double? BuyStop = null, + double? SellStop = null, + double? Atr = null +) : ISeries; diff --git a/src/a-d/AtrStop/AtrStop.Series.cs b/src/a-d/AtrStop/AtrStop.Series.cs deleted file mode 100644 index 9da4ababf..000000000 --- a/src/a-d/AtrStop/AtrStop.Series.cs +++ /dev/null @@ -1,111 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ATR TRAILING STOP (SERIES) -public static partial class Indicator -{ - internal static List CalcAtrStop( - this List qdList, - int lookbackPeriods, - double multiplier, - EndType endType) - { - // check parameter arguments - ValidateAtrStop(lookbackPeriods, multiplier); - - // initialize - List results = new(qdList.Count); - List atrResults = qdList.CalcAtr(lookbackPeriods); - - bool isBullish = true; - double? upperBand = null; - double? lowerBand = null; - - // roll through quotes - for (int i = 0; i < qdList.Count; i++) - { - QuoteD q = qdList[i]; - - AtrStopResult r = new(q.Date); - results.Add(r); - - if (i >= lookbackPeriods) - { - double? atr = atrResults[i].Atr; - QuoteD p = qdList[i - 1]; - - double? upperEval; - double? lowerEval; - - // potential bands for CLOSE - if (endType == EndType.Close) - { - upperEval = q.Close + (multiplier * atr); - lowerEval = q.Close - (multiplier * atr); - } - - // potential bands for HIGH/LOW - else - { - upperEval = q.High + (multiplier * atr); - lowerEval = q.Low - (multiplier * atr); - } - - // initial values - if (i == lookbackPeriods) - { - isBullish = q.Close >= p.Close; - - upperBand = upperEval; - lowerBand = lowerEval; - } - - // new upper band - if (upperEval < upperBand || p.Close > upperBand) - { - upperBand = upperEval; - } - - // new lower band - if (lowerEval > lowerBand || p.Close < lowerBand) - { - lowerBand = lowerEval; - } - - // trailing stop - if (q.Close <= (isBullish ? lowerBand : upperBand)) - { - r.AtrStop = (decimal?)upperBand; - r.BuyStop = (decimal?)upperBand; - isBullish = false; - } - else - { - r.AtrStop = (decimal?)lowerBand; - r.SellStop = (decimal?)lowerBand; - isBullish = true; - } - } - } - - return results; - } - - // parameter validation - private static void ValidateAtrStop( - int lookbackPeriods, - double multiplier) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for ATR Trailing Stop."); - } - - if (multiplier <= 0) - { - throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, - "Multiplier must be greater than 0 for ATR Trailing Stop."); - } - } -} diff --git a/src/a-d/AtrStop/AtrStop.StaticSeries.cs b/src/a-d/AtrStop/AtrStop.StaticSeries.cs new file mode 100644 index 000000000..b4b840f48 --- /dev/null +++ b/src/a-d/AtrStop/AtrStop.StaticSeries.cs @@ -0,0 +1,120 @@ +namespace Skender.Stock.Indicators; + +// ATR TRAILING STOP (SERIES) + +public static partial class AtrStop +{ + public static IReadOnlyList ToAtrStop( + this IReadOnlyList quotes, + int lookbackPeriods = 21, + double multiplier = 3, + EndType endType = EndType.Close) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcAtrStop(lookbackPeriods, multiplier, endType); + + private static List CalcAtrStop( + this IReadOnlyList source, + int lookbackPeriods, + double multiplier, + EndType endType) + { + // check parameter arguments + Validate(lookbackPeriods, multiplier); + + // initialize + int length = source.Count; + List results = new(length); + List atrResults = source.CalcAtr(lookbackPeriods); + + // prevailing direction and bands + bool isBullish = true; + double upperBand = double.MaxValue; + double lowerBand = double.MinValue; + + // roll through source values + for (int i = 0; i < length; i++) + { + // handle warmup periods + if (i < lookbackPeriods) + { + results.Add(new(Timestamp: source[i].Timestamp)); + continue; + } + + QuoteD q = source[i]; + QuoteD p = source[i - 1]; + + // initialize direction on first evaluation + if (i == lookbackPeriods) + { + isBullish = q.Close >= p.Close; + } + + // evaluate bands + double upperEval; + double lowerEval; + double atr = atrResults[i].Atr ?? double.NaN; + + // potential bands for CLOSE + if (endType == EndType.Close) + { + upperEval = q.Close + (multiplier * atr); + lowerEval = q.Close - (multiplier * atr); + } + + // potential bands for HIGH/LOW + else + { + upperEval = q.High + (multiplier * atr); + lowerEval = q.Low - (multiplier * atr); + } + + // new upper band: can only go down, or reverse + if (upperEval < upperBand || p.Close > upperBand) + { + upperBand = upperEval; + } + + // new lower band: can only go up, or reverse + if (lowerEval > lowerBand || p.Close < lowerBand) + { + lowerBand = lowerEval; + } + + // trailing stop: based on direction + + AtrStopResult r; + + // the upper band (short / buy-to-stop) + if (q.Close <= (isBullish ? lowerBand : upperBand)) + { + isBullish = false; + + r = new( + Timestamp: q.Timestamp, + AtrStop: upperBand, + BuyStop: upperBand, + SellStop: null, + Atr: atr); + } + + // the lower band (long / sell-to-stop) + else + { + isBullish = true; + + r = new( + Timestamp: q.Timestamp, + AtrStop: lowerBand, + BuyStop: null, + SellStop: lowerBand, + Atr: atr); + } + + results.Add(r); + } + + return results; + } +} diff --git a/src/a-d/AtrStop/AtrStop.StreamHub.cs b/src/a-d/AtrStop/AtrStop.StreamHub.cs new file mode 100644 index 000000000..20c0e7d0e --- /dev/null +++ b/src/a-d/AtrStop/AtrStop.StreamHub.cs @@ -0,0 +1,205 @@ +namespace Skender.Stock.Indicators; + +// ATR TRAILING STOP (STREAM HUB) + +#region hub interface and initializer + +public interface IAtrStopHub +{ + int LookbackPeriods { get; } + double Multiplier { get; } + EndType EndType { get; } +} + +public static partial class AtrStop +{ + public static AtrStopHub ToAtrStop( + this IQuoteProvider quoteProvider, + int lookbackPeriods = 21, + double multiplier = 3, + EndType endType = EndType.Close) + where TIn : IQuote + => new(quoteProvider, lookbackPeriods, multiplier, endType); +} +#endregion + +public class AtrStopHub + : StreamHub, IAtrStopHub + where TIn : IQuote +{ + #region constructors + + private readonly string hubName; + + internal AtrStopHub( + IQuoteProvider provider, + int lookbackPeriods, + double multiplier, + EndType endType) : base(provider) + { + AtrStop.Validate(lookbackPeriods, multiplier); + + LookbackPeriods = lookbackPeriods; + Multiplier = multiplier; + EndType = endType; + hubName = $"ATR-STOP({lookbackPeriods},{multiplier},{endType.ToString().ToUpperInvariant()})"; + + Reinitialize(); + } + #endregion + + public int LookbackPeriods { get; init; } + public double Multiplier { get; init; } + public EndType EndType { get; init; } + + // prevailing direction and band thresholds + private bool IsBullish { get; set; } = true; + private double UpperBand { get; set; } = double.MaxValue; + private double LowerBand { get; set; } = double.MinValue; + + // METHODS + + public override string ToString() => hubName; + + protected override (AtrStopResult result, int index) + ToIndicator(TIn item, int? indexHint) + { + // reminder: should only process "new" instructions + + int i = indexHint ?? ProviderCache.GetIndex(item, true); + + // handle warmup periods + if (i < LookbackPeriods) + { + return (new AtrStopResult(item.Timestamp), i); + } + + QuoteD newQ = item.ToQuoteD(); + double prevClose = (double)ProviderCache[i - 1].Close; + + // initialize direction on first evaluation + if (i == LookbackPeriods) + { + IsBullish = newQ.Close >= prevClose; + } + + // calculate ATR + double atr; + + if (Cache[i - 1].Atr is not null) + { + atr = Atr.Increment( + LookbackPeriods, + newQ.High, + newQ.Low, + prevClose, + Cache[i - 1].Atr ?? double.NaN); + } + + // initialize ATR + else + { + double sumTr = 0; + + for (int p = i - LookbackPeriods + 1; p <= i; p++) + { + sumTr += Tr.Increment( + (double)ProviderCache[p].High, + (double)ProviderCache[p].Low, + (double)ProviderCache[p - 1].Close); + } + + atr = sumTr / LookbackPeriods; + } + + // evaluate bands + double upperEval; + double lowerEval; + + // potential bands for CLOSE + if (EndType == EndType.Close) + { + upperEval = newQ.Close + (Multiplier * atr); + lowerEval = newQ.Close - (Multiplier * atr); + } + + // potential bands for HIGH/LOW + else + { + upperEval = newQ.High + (Multiplier * atr); + lowerEval = newQ.Low - (Multiplier * atr); + } + + // new upper band: can only go down, or reverse + if (upperEval < UpperBand || prevClose > UpperBand) + { + UpperBand = upperEval; + } + + // new lower band: can only go up, or reverse + if (lowerEval > LowerBand || prevClose < LowerBand) + { + LowerBand = lowerEval; + } + + // trailing stop: based on direction + + AtrStopResult r; + + // the upper band (short / buy-to-stop) + if (newQ.Close <= (IsBullish ? LowerBand : UpperBand)) + { + IsBullish = false; + + r = new AtrStopResult( + Timestamp: newQ.Timestamp, + AtrStop: UpperBand, + BuyStop: UpperBand, + SellStop: null, + Atr: atr); + } + + // the lower band (long / sell-to-stop) + else + { + IsBullish = true; + + r = new AtrStopResult( + Timestamp: newQ.Timestamp, + AtrStop: LowerBand, + BuyStop: null, + SellStop: LowerBand, + Atr: atr); + } + + return (r, i); + } + + /// + /// Restore prior ATR Stop + /// + /// + protected override void RollbackState(DateTime timestamp) + { + int i = ProviderCache.GetIndexGte(timestamp); + + // restore prior stop point + if (i > LookbackPeriods) + { + AtrStopResult resetStop = Cache[i - 1]; + + // prevailing direction and bands + IsBullish = resetStop.AtrStop >= resetStop.SellStop; + UpperBand = resetStop.BuyStop ?? default; + LowerBand = resetStop.SellStop ?? default; + } + + // or reset if no prior stop found + else + { + IsBullish = default; + UpperBand = default; + LowerBand = default; + } + } +} diff --git a/src/a-d/AtrStop/AtrStop.Utilities.cs b/src/a-d/AtrStop/AtrStop.Utilities.cs index 62d910708..be667403f 100644 --- a/src/a-d/AtrStop/AtrStop.Utilities.cs +++ b/src/a-d/AtrStop/AtrStop.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// ATR TRAILING STOP (UTILITIES) + +public static partial class AtrStop { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -17,10 +18,9 @@ public static IEnumerable Condense( } // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -28,4 +28,23 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int lookbackPeriods, + double multiplier) + { + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for ATR Trailing Stop."); + } + + if (multiplier <= 0) + { + throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, + "Multiplier must be greater than 0 for ATR Trailing Stop."); + } + } } diff --git a/src/a-d/Awesome/Awesome.Api.cs b/src/a-d/Awesome/Awesome.Api.cs deleted file mode 100644 index bc1af8ec4..000000000 --- a/src/a-d/Awesome/Awesome.Api.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Skender.Stock.Indicators; - -// AWESOME OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetAwesome( - this IEnumerable quotes, - int fastPeriods = 5, - int slowPeriods = 34) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.HL2) - .CalcAwesome(fastPeriods, slowPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetAwesome( - this IEnumerable results, - int fastPeriods = 5, - int slowPeriods = 34) => results - .ToTuple() - .CalcAwesome(fastPeriods, slowPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetAwesome( - this IEnumerable<(DateTime, double)> priceTuples, - int fastPeriods = 5, - int slowPeriods = 34) => priceTuples - .ToSortedList() - .CalcAwesome(fastPeriods, slowPeriods); -} diff --git a/src/a-d/Awesome/Awesome.Models.cs b/src/a-d/Awesome/Awesome.Models.cs index a0dd84a03..ed1dd78de 100644 --- a/src/a-d/Awesome/Awesome.Models.cs +++ b/src/a-d/Awesome/Awesome.Models.cs @@ -1,15 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class AwesomeResult : ResultBase, IReusableResult +public record AwesomeResult +( + DateTime Timestamp, + double? Oscillator, + double? Normalized +) : IReusable { - public AwesomeResult(DateTime date) - { - Date = date; - } - - public double? Oscillator { get; set; } - public double? Normalized { get; set; } - - double? IReusableResult.Value => Oscillator; + public double Value => Oscillator.Null2NaN(); } diff --git a/src/a-d/Awesome/Awesome.Series.cs b/src/a-d/Awesome/Awesome.Series.cs deleted file mode 100644 index 6cdda1620..000000000 --- a/src/a-d/Awesome/Awesome.Series.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Skender.Stock.Indicators; - -// AWESOME OSCILLATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcAwesome( - this List<(DateTime, double)> tpList, - int fastPeriods, - int slowPeriods) - { - // check parameter arguments - ValidateAwesome(fastPeriods, slowPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - double[] pr = new double[length]; - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - pr[i] = value; - - AwesomeResult r = new(date); - results.Add(r); - - if (i + 1 >= slowPeriods) - { - double sumSlow = 0; - double sumFast = 0; - - for (int p = i + 1 - slowPeriods; p <= i; p++) - { - sumSlow += pr[p]; - - if (p >= i + 1 - fastPeriods) - { - sumFast += pr[p]; - } - } - - r.Oscillator = ((sumFast / fastPeriods) - (sumSlow / slowPeriods)).NaN2Null(); - r.Normalized = (pr[i] != 0) ? 100 * r.Oscillator / pr[i] : null; - } - } - - return results; - } - - // parameter validation - private static void ValidateAwesome( - int fastPeriods, - int slowPeriods) - { - // check parameter arguments - if (fastPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, - "Fast periods must be greater than 0 for Awesome Oscillator."); - } - - if (slowPeriods <= fastPeriods) - { - throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, - "Slow periods must be larger than Fast Periods for Awesome Oscillator."); - } - } -} diff --git a/src/a-d/Awesome/Awesome.StaticSeries.cs b/src/a-d/Awesome/Awesome.StaticSeries.cs new file mode 100644 index 000000000..b8f38f542 --- /dev/null +++ b/src/a-d/Awesome/Awesome.StaticSeries.cs @@ -0,0 +1,63 @@ +namespace Skender.Stock.Indicators; + +// AWESOME OSCILLATOR (SERIES) + +public static partial class Awesome +{ + public static IReadOnlyList ToAwesome( + this IReadOnlyList source, + int fastPeriods = 5, + int slowPeriods = 34) + where T : IReusable + { + // check parameter arguments + Validate(fastPeriods, slowPeriods); + + // prefer HL2 when IQuote + IReadOnlyList values + = source.ToPreferredList(CandlePart.HL2); + + // initialize + int length = values.Count; + List results = new(length); + double[] pr = new double[length]; + + // roll through source values + for (int i = 0; i < length; i++) + { + IReusable s = values[i]; + pr[i] = s.Value; + + double? oscillator = null; + double? normalized = null; + + if (i >= slowPeriods - 1) + { + double sumSlow = 0; + double sumFast = 0; + + for (int p = i + 1 - slowPeriods; p <= i; p++) + { + sumSlow += pr[p]; + + if (p >= i + 1 - fastPeriods) + { + sumFast += pr[p]; + } + } + + oscillator = ((sumFast / fastPeriods) - (sumSlow / slowPeriods)).NaN2Null(); + normalized = pr[i] != 0 ? 100 * oscillator / pr[i] : null; + } + + AwesomeResult r = new( + Timestamp: s.Timestamp, + Oscillator: oscillator, + Normalized: normalized); + + results.Add(r); + } + + return results; + } +} diff --git a/src/a-d/Awesome/Awesome.Utilities.cs b/src/a-d/Awesome/Awesome.Utilities.cs index 8a19fc3fa..95396cb3d 100644 --- a/src/a-d/Awesome/Awesome.Utilities.cs +++ b/src/a-d/Awesome/Awesome.Utilities.cs @@ -1,17 +1,23 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +public static partial class Awesome { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int fastPeriods, + int slowPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Oscillator != null); + // check parameter arguments + if (fastPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, + "Fast periods must be greater than 0 for Awesome Oscillator."); + } - return results.Remove(removePeriods); + if (slowPeriods <= fastPeriods) + { + throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, + "Slow periods must be larger than Fast Periods for Awesome Oscillator."); + } } } diff --git a/src/a-d/BasicQuote/BasicData.Models.cs b/src/a-d/BasicQuote/BasicData.Models.cs deleted file mode 100644 index 2c09980cc..000000000 --- a/src/a-d/BasicQuote/BasicData.Models.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -public interface IBasicData -{ - public DateTime Date { get; } - public double Value { get; } -} - -public class BasicData : ISeries, IBasicData, IReusableResult -{ - public DateTime Date { get; set; } - public double Value { get; set; } - - double? IReusableResult.Value => Value; -} diff --git a/src/a-d/BasicQuote/BasicQuote.Api.cs b/src/a-d/BasicQuote/BasicQuote.Api.cs deleted file mode 100644 index ddd3ab978..000000000 --- a/src/a-d/BasicQuote/BasicQuote.Api.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Skender.Stock.Indicators; - -public static partial class Indicator -{ - // BASE QUOTE (simplified quote) - /// - /// - public static IEnumerable GetBaseQuote( - this IEnumerable quotes, - CandlePart candlePart = CandlePart.Close) - where TQuote : IQuote => quotes - .Select(q => q.ToBasicData(candlePart)) - .OrderBy(x => x.Date); -} diff --git a/src/a-d/BasicQuote/info.xml b/src/a-d/BasicQuote/info.xml deleted file mode 100644 index 60943f9e8..000000000 --- a/src/a-d/BasicQuote/info.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - A simple quote transform. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - The OHLCV element or simply calculated value type. - Time series of Basic Quote values. - Invalid candle part provided. - \ No newline at end of file diff --git a/src/a-d/Beta/Beta.Api.cs b/src/a-d/Beta/Beta.Api.cs deleted file mode 100644 index 34d379e79..000000000 --- a/src/a-d/Beta/Beta.Api.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Skender.Stock.Indicators; - -// BETA COEFFICIENT (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetBeta( - this IEnumerable quotesEval, - IEnumerable quotesMarket, - int lookbackPeriods, - BetaType type = BetaType.Standard) - where TQuote : IQuote - { - List<(DateTime, double)> tpListEval - = quotesEval.ToTuple(CandlePart.Close); - - List<(DateTime, double)> tpListMrkt - = quotesMarket.ToTuple(CandlePart.Close); - - // to enable typical 'this' extension - return CalcBeta(tpListEval, tpListMrkt, lookbackPeriods, type); - } - - // SERIES, from CHAINS (both inputs reusable) - public static IEnumerable GetBeta( - this IEnumerable evalResults, - IEnumerable mrktResults, - int lookbackPeriods, - BetaType type = BetaType.Standard) - { - List<(DateTime Date, double Value)> tpListEval - = evalResults.ToTuple(); - - List<(DateTime Date, double Value)> tpListMrkt - = mrktResults.ToTuple(); - - return CalcBeta(tpListEval, tpListMrkt, lookbackPeriods, type) - .SyncIndex(evalResults, SyncType.Prepend); - } - - // SERIES, from TUPLE - public static IEnumerable GetBeta( - this IEnumerable<(DateTime, double)> evalTuple, - IEnumerable<(DateTime, double)> mrktTuple, - int lookbackPeriods, - BetaType type = BetaType.Standard) - { - List<(DateTime, double)> tpListEval - = evalTuple.ToSortedList(); - - List<(DateTime, double)> tpListMrkt - = mrktTuple.ToSortedList(); - - return CalcBeta(tpListEval, tpListMrkt, lookbackPeriods, type); - } -} diff --git a/src/a-d/Beta/Beta.Models.cs b/src/a-d/Beta/Beta.Models.cs index 2a1dc5257..5d62fd0eb 100644 --- a/src/a-d/Beta/Beta.Models.cs +++ b/src/a-d/Beta/Beta.Models.cs @@ -1,22 +1,18 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class BetaResult : ResultBase, IReusableResult +public record BetaResult( + DateTime Timestamp, + double? Beta = null, + double? BetaUp = null, + double? BetaDown = null, + double? Ratio = null, + double? Convexity = null, + double? ReturnsEval = null, + double? ReturnsMrkt = null +) : IReusable { - public BetaResult(DateTime date) - { - Date = date; - } - - public double? Beta { get; set; } - public double? BetaUp { get; set; } - public double? BetaDown { get; set; } - public double? Ratio { get; set; } - public double? Convexity { get; set; } - public double? ReturnsEval { get; set; } - public double? ReturnsMrkt { get; set; } - - double? IReusableResult.Value => Beta; + public double Value => Beta.Null2NaN(); } public enum BetaType diff --git a/src/a-d/Beta/Beta.Series.cs b/src/a-d/Beta/Beta.Series.cs deleted file mode 100644 index e36854367..000000000 --- a/src/a-d/Beta/Beta.Series.cs +++ /dev/null @@ -1,173 +0,0 @@ -namespace Skender.Stock.Indicators; - -// BETA COEFFICIENT (SERIES) -public static partial class Indicator -{ - // NOTE: sequence swapped from API - internal static List CalcBeta( - List<(DateTime, double)> tpListEval, - List<(DateTime, double)> tpListMrkt, - int lookbackPeriods, - BetaType type = BetaType.Standard) - { - // check parameter arguments - ValidateBeta(tpListEval, tpListMrkt, lookbackPeriods); - - // initialize - int length = tpListEval.Count; - List results = new(length); - - bool calcSd = type is BetaType.All or BetaType.Standard; - bool calcUp = type is BetaType.All or BetaType.Up; - bool calcDn = type is BetaType.All or BetaType.Down; - - // convert quotes to returns - double[] evalReturns = new double[length]; - double[] mrktReturns = new double[length]; - double prevE = 0; - double prevM = 0; - - for (int i = 0; i < length; i++) - { - (DateTime eDate, double eValue) = tpListEval[i]; - (DateTime mDate, double mValue) = tpListMrkt[i]; - - if (eDate != mDate) - { - throw new InvalidQuotesException(nameof(tpListEval), eDate, - "Date sequence does not match. Beta requires matching dates in provided quotes."); - } - - evalReturns[i] = prevE != 0 ? (eValue / prevE) - 1d : 0; - mrktReturns[i] = prevM != 0 ? (mValue / prevM) - 1d : 0; - - prevE = eValue; - prevM = mValue; - } - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double _) = tpListEval[i]; - - BetaResult r = new(date) { - ReturnsEval = evalReturns[i], - ReturnsMrkt = mrktReturns[i] - }; - results.Add(r); - - // skip warmup periods - if (i < lookbackPeriods) - { - continue; - } - - // calculate beta variants - if (calcSd) - { - r.CalcBetaWindow( - i, lookbackPeriods, mrktReturns, evalReturns, BetaType.Standard); - } - - if (calcDn) - { - r.CalcBetaWindow( - i, lookbackPeriods, mrktReturns, evalReturns, BetaType.Down); - } - - if (calcUp) - { - r.CalcBetaWindow( - i, lookbackPeriods, mrktReturns, evalReturns, BetaType.Up); - } - - // ratio and convexity - if (type == BetaType.All && r.BetaUp != null && r.BetaDown != null) - { - r.Ratio = (r.BetaDown != 0) ? r.BetaUp / r.BetaDown : null; - r.Convexity = (r.BetaUp - r.BetaDown) * (r.BetaUp - r.BetaDown); - } - } - - return results; - } - - // calculate beta - private static void CalcBetaWindow( - this BetaResult r, - int i, - int lookbackPeriods, - double[] mrktReturns, - double[] evalReturns, - BetaType type) - { - // note: BetaType.All is ineligible for this method - - // initialize - CorrResult c = new(r.Date); - - List dataA = new(lookbackPeriods); - List dataB = new(lookbackPeriods); - - for (int p = i - lookbackPeriods + 1; p <= i; p++) - { - double a = mrktReturns[p]; - double b = evalReturns[p]; - - if (type is BetaType.Standard - || (type is BetaType.Down && a < 0) - || (type is BetaType.Up && a > 0)) - { - dataA.Add(a); - dataB.Add(b); - } - } - - if (dataA.Count > 0) - { - // calculate correlation, covariance, and variance - c.PeriodCorrelation(dataA.ToArray(), dataB.ToArray()); - - // calculate beta - if (c.Covariance != null && c.VarianceA != null && c.VarianceA != 0) - { - double? beta = (c.Covariance / c.VarianceA).NaN2Null(); - - if (type == BetaType.Standard) - { - r.Beta = beta; - } - else if (type == BetaType.Down) - { - r.BetaDown = beta; - } - else if (type == BetaType.Up) - { - r.BetaUp = beta; - } - } - } - } - - // parameter validation - private static void ValidateBeta( - List<(DateTime, double)> tpListEval, - List<(DateTime, double)> tpListMrkt, - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Beta."); - } - - // check quotes - if (tpListEval.Count != tpListMrkt.Count) - { - throw new InvalidQuotesException( - nameof(tpListEval), - "Eval quotes should have the same number of Market quotes for Beta."); - } - } -} diff --git a/src/a-d/Beta/Beta.StaticSeries.cs b/src/a-d/Beta/Beta.StaticSeries.cs new file mode 100644 index 000000000..d8e3280a6 --- /dev/null +++ b/src/a-d/Beta/Beta.StaticSeries.cs @@ -0,0 +1,163 @@ +namespace Skender.Stock.Indicators; + +// BETA COEFFICIENT (SERIES) + +public static partial class Beta +{ + public static IReadOnlyList ToBeta( + this IReadOnlyList sourceEval, + IReadOnlyList sourceMrkt, + int lookbackPeriods, + BetaType type = BetaType.Standard) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(sourceEval); + ArgumentNullException.ThrowIfNull(sourceMrkt); + Validate(sourceEval, sourceMrkt, lookbackPeriods); + + // initialize + int length = sourceEval.Count; + List results = new(length); + + bool calcSd = type is BetaType.All or BetaType.Standard; + bool calcUp = type is BetaType.All or BetaType.Up; + bool calcDn = type is BetaType.All or BetaType.Down; + + // convert quotes to returns + double[] evalReturns = new double[length]; + double[] mrktReturns = new double[length]; + double prevE = 0; + double prevM = 0; + + for (int i = 0; i < length; i++) + { + T eval = sourceEval[i]; + T mrkt = sourceMrkt[i]; + + if (eval.Timestamp != mrkt.Timestamp) + { + throw new InvalidQuotesException( + nameof(sourceEval), eval.Timestamp, + "Timestamp sequence does not match. " + + "Beta requires matching dates in provided quotes."); + } + + evalReturns[i] = prevE != 0 ? (eval.Value / prevE) - 1d : 0; + mrktReturns[i] = prevM != 0 ? (mrkt.Value / prevM) - 1d : 0; + + prevE = eval.Value; + prevM = mrkt.Value; + } + + // roll through source values + for (int i = 0; i < length; i++) + { + T eval = sourceEval[i]; + + // skip warmup periods + if (i < lookbackPeriods) + { + results.Add(new( + Timestamp: eval.Timestamp, + ReturnsEval: evalReturns[i], + ReturnsMrkt: mrktReturns[i])); + + continue; + } + + double? beta = null; + double? betaUp = null; + double? betaDown = null; + double? ratio = null; + double? convexity = null; + + // calculate beta variants + if (calcSd) + { + beta = CalcBetaWindow(i, lookbackPeriods, + mrktReturns, evalReturns, BetaType.Standard); + } + + if (calcDn) + { + betaDown = CalcBetaWindow(i, lookbackPeriods, + mrktReturns, evalReturns, BetaType.Down); + } + + if (calcUp) + { + betaUp = CalcBetaWindow(i, lookbackPeriods, + mrktReturns, evalReturns, BetaType.Up); + } + + // ratio and convexity + if (type == BetaType.All && betaUp != null && betaDown != null) + { + ratio = betaDown != 0 ? betaUp / betaDown : null; + convexity = (betaUp - betaDown) * (betaUp - betaDown); + } + + results.Add( + new(Timestamp: eval.Timestamp, + ReturnsEval: evalReturns[i], + ReturnsMrkt: mrktReturns[i], + Beta: beta, + BetaUp: betaUp, + BetaDown: betaDown, + Ratio: ratio, + Convexity: convexity)); + } + + return results; + } + + // calculate beta + private static double? CalcBetaWindow( + int i, + int lookbackPeriods, + double[] mrktReturns, + double[] evalReturns, + BetaType type) + { + // note: BetaType.All is ineligible for this method + + // initialize + double? beta = null; + List dataA = new(lookbackPeriods); + List dataB = new(lookbackPeriods); + + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + double a = mrktReturns[p]; + double b = evalReturns[p]; + + if (type is BetaType.Standard + || (type is BetaType.Down && a < 0) + || (type is BetaType.Up && a > 0)) + { + dataA.Add(a); + dataB.Add(b); + } + } + + if (dataA.Count <= 0) + { + return beta; + } + + // calculate correlation, covariance, and variance + CorrResult c = Correlation.PeriodCorrelation( + default, + [.. dataA], + [.. dataB]); + + // calculate beta + if (c.VarianceA != 0) + { + beta = (c.Covariance / c.VarianceA).NaN2Null(); + } + + return beta; + } +} diff --git a/src/a-d/Beta/Beta.Utilities.cs b/src/a-d/Beta/Beta.Utilities.cs index ca341e6c2..45a096dcf 100644 --- a/src/a-d/Beta/Beta.Utilities.cs +++ b/src/a-d/Beta/Beta.Utilities.cs @@ -1,17 +1,27 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +public static partial class Beta { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + IReadOnlyList sourceEval, + IReadOnlyList sourceMrkt, + int lookbackPeriods) + where T : ISeries { - int removePeriods = results - .ToList() - .FindIndex(x => x.Beta != null); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Beta."); + } - return results.Remove(removePeriods); + // check quotes + if (sourceEval.Count != sourceMrkt.Count) + { + throw new InvalidQuotesException( + nameof(sourceEval), + "Eval quotes should have the same number of Market quotes for Beta."); + } } } diff --git a/src/a-d/BollingerBands/BollingerBands.Api.cs b/src/a-d/BollingerBands/BollingerBands.Api.cs deleted file mode 100644 index 5ad5ca75e..000000000 --- a/src/a-d/BollingerBands/BollingerBands.Api.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Skender.Stock.Indicators; - -// BOLLINGER BANDS (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetBollingerBands( - this IEnumerable quotes, - int lookbackPeriods = 20, - double standardDeviations = 2) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcBollingerBands(lookbackPeriods, standardDeviations); - - // SERIES, from CHAIN - public static IEnumerable GetBollingerBands( - this IEnumerable results, - int lookbackPeriods = 20, - double standardDeviations = 2) => results - .ToTuple() - .CalcBollingerBands(lookbackPeriods, standardDeviations) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetBollingerBands( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods = 20, - double standardDeviations = 2) => priceTuples - .ToSortedList() - .CalcBollingerBands(lookbackPeriods, standardDeviations); -} diff --git a/src/a-d/BollingerBands/BollingerBands.Models.cs b/src/a-d/BollingerBands/BollingerBands.Models.cs index 9260a0618..7f577f160 100644 --- a/src/a-d/BollingerBands/BollingerBands.Models.cs +++ b/src/a-d/BollingerBands/BollingerBands.Models.cs @@ -1,20 +1,16 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class BollingerBandsResult : ResultBase, IReusableResult +public record BollingerBandsResult +( + DateTime Timestamp, + double? Sma = null, + double? UpperBand = null, + double? LowerBand = null, + double? PercentB = null, + double? ZScore = null, + double? Width = null +) : IReusable { - public BollingerBandsResult(DateTime date) - { - Date = date; - } - - public double? Sma { get; set; } - public double? UpperBand { get; set; } - public double? LowerBand { get; set; } - - public double? PercentB { get; set; } - public double? ZScore { get; set; } - public double? Width { get; set; } - - double? IReusableResult.Value => PercentB; + public double Value => PercentB.Null2NaN(); } diff --git a/src/a-d/BollingerBands/BollingerBands.Series.cs b/src/a-d/BollingerBands/BollingerBands.Series.cs deleted file mode 100644 index 26802686a..000000000 --- a/src/a-d/BollingerBands/BollingerBands.Series.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace Skender.Stock.Indicators; - -// BOLLINGER BANDS (SERIES) -public static partial class Indicator -{ - internal static List CalcBollingerBands( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double standardDeviations) - { - // check parameter arguments - ValidateBollingerBands(lookbackPeriods, standardDeviations); - - // initialize - List results = new(tpList.Count); - - // roll through quotes - for (int i = 0; i < tpList.Count; i++) - { - (DateTime date, double value) = tpList[i]; - - BollingerBandsResult r = new(date); - results.Add(r); - - if (i + 1 >= lookbackPeriods) - { - double[] window = new double[lookbackPeriods]; - double sum = 0; - int n = 0; - - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - (DateTime _, double pValue) = tpList[p]; - window[n] = pValue; - sum += pValue; - n++; - } - - double? periodAvg = (sum / lookbackPeriods).NaN2Null(); - double? stdDev = window.StdDev().NaN2Null(); - - r.Sma = periodAvg; - r.UpperBand = periodAvg + (standardDeviations * stdDev); - r.LowerBand = periodAvg - (standardDeviations * stdDev); - - r.PercentB = (r.UpperBand == r.LowerBand) ? null - : (value - r.LowerBand) / (r.UpperBand - r.LowerBand); - - r.ZScore = (stdDev == 0) ? null : (value - r.Sma) / stdDev; - r.Width = (periodAvg == 0) ? null : (r.UpperBand - r.LowerBand) / periodAvg; - } - } - - return results; - } - - // parameter validation - private static void ValidateBollingerBands( - int lookbackPeriods, - double standardDeviations) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for Bollinger Bands."); - } - - if (standardDeviations <= 0) - { - throw new ArgumentOutOfRangeException(nameof(standardDeviations), standardDeviations, - "Standard Deviations must be greater than 0 for Bollinger Bands."); - } - } -} diff --git a/src/a-d/BollingerBands/BollingerBands.StaticSeries.cs b/src/a-d/BollingerBands/BollingerBands.StaticSeries.cs new file mode 100644 index 000000000..8945cb590 --- /dev/null +++ b/src/a-d/BollingerBands/BollingerBands.StaticSeries.cs @@ -0,0 +1,71 @@ +namespace Skender.Stock.Indicators; + +// BOLLINGER BANDS (SERIES) + +public static partial class BollingerBands +{ + public static IReadOnlyList ToBollingerBands( + this IReadOnlyList source, + int lookbackPeriods = 20, + double standardDeviations = 2) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods, standardDeviations); + + // initialize + int length = source.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + if (i >= lookbackPeriods - 1) + { + double[] window = new double[lookbackPeriods]; + double sum = 0; + int n = 0; + + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + T ps = source[p]; + window[n] = ps.Value; + sum += ps.Value; + n++; + } + + double? sma = (sum / lookbackPeriods).NaN2Null(); + double? stdDev = window.StdDev().NaN2Null(); + + double? upperBand = sma + (standardDeviations * stdDev); + double? lowerBand = sma - (standardDeviations * stdDev); + + results.Add(new BollingerBandsResult( + + Timestamp: s.Timestamp, + + Sma: sma, + UpperBand: upperBand, + LowerBand: lowerBand, + + PercentB: upperBand - lowerBand == 0 ? null + : (s.Value - lowerBand) / (upperBand - lowerBand), + + ZScore: stdDev == 0 ? null : (s.Value - sma) / stdDev, + Width: sma == 0 ? null : (upperBand - lowerBand) / sma + )); + } + + // initization period + else + { + results.Add(new(s.Timestamp)); + } + } + + return results; + } +} diff --git a/src/a-d/BollingerBands/BollingerBands.Utilities.cs b/src/a-d/BollingerBands/BollingerBands.Utilities.cs index a7f86e769..a90db9fe1 100644 --- a/src/a-d/BollingerBands/BollingerBands.Utilities.cs +++ b/src/a-d/BollingerBands/BollingerBands.Utilities.cs @@ -1,12 +1,11 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +public static partial class BollingerBands { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -14,4 +13,23 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int lookbackPeriods, + double standardDeviations) + { + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for Bollinger Bands."); + } + + if (standardDeviations <= 0) + { + throw new ArgumentOutOfRangeException(nameof(standardDeviations), standardDeviations, + "Standard Deviations must be greater than 0 for Bollinger Bands."); + } + } } diff --git a/src/a-d/Bop/Bop.Api.cs b/src/a-d/Bop/Bop.Api.cs deleted file mode 100644 index ef788b3db..000000000 --- a/src/a-d/Bop/Bop.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// BALANCE OF POWER (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetBop( - this IEnumerable quotes, - int smoothPeriods = 14) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcBop(smoothPeriods); -} diff --git a/src/a-d/Bop/Bop.Models.cs b/src/a-d/Bop/Bop.Models.cs index 74df18931..2abc7e6b1 100644 --- a/src/a-d/Bop/Bop.Models.cs +++ b/src/a-d/Bop/Bop.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class BopResult : ResultBase, IReusableResult +public record BopResult +( + DateTime Timestamp, + double? Bop +) : IReusable { - public BopResult(DateTime date) - { - Date = date; - } - - public double? Bop { get; set; } - - double? IReusableResult.Value => Bop; + public double Value => Bop.Null2NaN(); } diff --git a/src/a-d/Bop/Bop.Series.cs b/src/a-d/Bop/Bop.Series.cs deleted file mode 100644 index fb75f1988..000000000 --- a/src/a-d/Bop/Bop.Series.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace Skender.Stock.Indicators; - -// BALANCE OF POWER (SERIES) -public static partial class Indicator -{ - internal static List CalcBop( - this List qdList, - int smoothPeriods) - { - // check parameter arguments - ValidateBop(smoothPeriods); - - // initialize - int length = qdList.Count; - List results = new(length); - - double[] raw = qdList - .Select(x => (x.High != x.Low) ? - ((x.Close - x.Open) / (x.High - x.Low)) : double.NaN) - .ToArray(); - - // roll through quotes - for (int i = 0; i < length; i++) - { - BopResult r = new(qdList[i].Date); - results.Add(r); - - if (i >= smoothPeriods - 1) - { - double sum = 0; - for (int p = i - smoothPeriods + 1; p <= i; p++) - { - sum += raw[p]; - } - - r.Bop = (sum / smoothPeriods).NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidateBop( - int smoothPeriods) - { - // check parameter arguments - if (smoothPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smoothPeriods), smoothPeriods, - "Smoothing periods must be greater than 0 for BOP."); - } - } -} diff --git a/src/a-d/Bop/Bop.StaticSeries.cs b/src/a-d/Bop/Bop.StaticSeries.cs new file mode 100644 index 000000000..37a1c7b7c --- /dev/null +++ b/src/a-d/Bop/Bop.StaticSeries.cs @@ -0,0 +1,53 @@ +namespace Skender.Stock.Indicators; + +// BALANCE OF POWER (SERIES) + +public static partial class Bop +{ + public static IReadOnlyList ToBop( + this IReadOnlyList quotes, + int smoothPeriods = 14) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcBop(smoothPeriods); + + private static List CalcBop( + this IReadOnlyList source, + int smoothPeriods) + { + // check parameter arguments + Validate(smoothPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double[] raw = source + .Select(x => x.High - x.Low != 0 ? + (x.Close - x.Open) / (x.High - x.Low) : double.NaN) + .ToArray(); + + // roll through source values + for (int i = 0; i < length; i++) + { + double? bop = null; + + if (i >= smoothPeriods - 1) + { + double sum = 0; + for (int p = i - smoothPeriods + 1; p <= i; p++) + { + sum += raw[p]; + } + + bop = (sum / smoothPeriods).NaN2Null(); + } + + results.Add(new( + Timestamp: source[i].Timestamp, + Bop: bop)); + } + + return results; + } +} diff --git a/src/a-d/Bop/Bop.Utilities.cs b/src/a-d/Bop/Bop.Utilities.cs index 7ccb26c47..488440947 100644 --- a/src/a-d/Bop/Bop.Utilities.cs +++ b/src/a-d/Bop/Bop.Utilities.cs @@ -1,17 +1,18 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// BALANCE OF POWER (UTILITIES) + +public static partial class Bop { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int smoothPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Bop != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (smoothPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(smoothPeriods), smoothPeriods, + "Smoothing periods must be greater than 0 for BOP."); + } } } diff --git a/src/a-d/Cci/Cci.Api.cs b/src/a-d/Cci/Cci.Api.cs deleted file mode 100644 index a7ec674ef..000000000 --- a/src/a-d/Cci/Cci.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// COMMODITY CHANNEL INDEX (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetCci( - this IEnumerable quotes, - int lookbackPeriods = 20) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcCci(lookbackPeriods); -} diff --git a/src/a-d/Cci/Cci.Models.cs b/src/a-d/Cci/Cci.Models.cs index 2f3f7a449..f2a819eb3 100644 --- a/src/a-d/Cci/Cci.Models.cs +++ b/src/a-d/Cci/Cci.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class CciResult : ResultBase, IReusableResult +public record CciResult +( + DateTime Timestamp, + double? Cci +) : IReusable { - public CciResult(DateTime date) - { - Date = date; - } - - public double? Cci { get; set; } - - double? IReusableResult.Value => Cci; + public double Value => Cci.Null2NaN(); } diff --git a/src/a-d/Cci/Cci.Series.cs b/src/a-d/Cci/Cci.StaticSeries.cs similarity index 59% rename from src/a-d/Cci/Cci.Series.cs rename to src/a-d/Cci/Cci.StaticSeries.cs index 93bad6f21..e854ce386 100644 --- a/src/a-d/Cci/Cci.Series.cs +++ b/src/a-d/Cci/Cci.StaticSeries.cs @@ -1,28 +1,35 @@ namespace Skender.Stock.Indicators; // COMMODITY CHANNEL INDEX (SERIES) -public static partial class Indicator + +public static partial class Cci { - internal static List CalcCci( - this List qdList, + public static IReadOnlyList ToCci( + this IReadOnlyList quotes, + int lookbackPeriods = 20) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcCci(lookbackPeriods); + + private static List CalcCci( + this IReadOnlyList source, int lookbackPeriods) { // check parameter arguments - ValidateCci(lookbackPeriods); + Validate(lookbackPeriods); // initialize - int length = qdList.Count; + int length = source.Count; List results = new(length); double[] tp = new double[length]; - // roll through quotes + // roll through source values for (int i = 0; i < length; i++) { - QuoteD q = qdList[i]; + QuoteD q = source[i]; tp[i] = (q.High + q.Low + q.Close) / 3d; - CciResult r = new(q.Date); - results.Add(r); + double? cci = null; if (i + 1 >= lookbackPeriods) { @@ -44,23 +51,15 @@ internal static List CalcCci( avgDv /= lookbackPeriods; - r.Cci = (avgDv == 0) ? null + cci = avgDv == 0 ? null : ((tp[i] - avgTp) / (0.015 * avgDv)).NaN2Null(); } + + results.Add(new( + Timestamp: q.Timestamp, + Cci: cci)); } return results; } - - // parameter validation - private static void ValidateCci( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Commodity Channel Index."); - } - } } diff --git a/src/a-d/Cci/Cci.Utilities.cs b/src/a-d/Cci/Cci.Utilities.cs index ff33b1556..34bce451c 100644 --- a/src/a-d/Cci/Cci.Utilities.cs +++ b/src/a-d/Cci/Cci.Utilities.cs @@ -1,17 +1,19 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// COMMODITY CHANNEL INDEX (UTILITIES) + +public static partial class Cci { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Cci != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Commodity Channel Index."); + } } + } diff --git a/src/a-d/ChaikinOsc/ChaikinOsc.Api.cs b/src/a-d/ChaikinOsc/ChaikinOsc.Api.cs deleted file mode 100644 index 0f1973bf8..000000000 --- a/src/a-d/ChaikinOsc/ChaikinOsc.Api.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHAIKIN OSCILLATOR -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetChaikinOsc( - this IEnumerable quotes, - int fastPeriods = 3, - int slowPeriods = 10) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcChaikinOsc(fastPeriods, slowPeriods); -} diff --git a/src/a-d/ChaikinOsc/ChaikinOsc.Models.cs b/src/a-d/ChaikinOsc/ChaikinOsc.Models.cs index 686e2ed0b..d30df99b6 100644 --- a/src/a-d/ChaikinOsc/ChaikinOsc.Models.cs +++ b/src/a-d/ChaikinOsc/ChaikinOsc.Models.cs @@ -1,17 +1,14 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class ChaikinOscResult : ResultBase, IReusableResult +public record ChaikinOscResult +( + DateTime Timestamp, + double? MoneyFlowMultiplier, + double? MoneyFlowVolume, + double? Adl, + double? Oscillator +) : IReusable { - public ChaikinOscResult(DateTime date) - { - Date = date; - } - - public double? MoneyFlowMultiplier { get; set; } - public double? MoneyFlowVolume { get; set; } - public double? Adl { get; set; } - public double? Oscillator { get; set; } - - double? IReusableResult.Value => Oscillator; + public double Value => Oscillator.Null2NaN(); } diff --git a/src/a-d/ChaikinOsc/ChaikinOsc.Series.cs b/src/a-d/ChaikinOsc/ChaikinOsc.Series.cs deleted file mode 100644 index 77b9e27c7..000000000 --- a/src/a-d/ChaikinOsc/ChaikinOsc.Series.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHAIKIN OSCILLATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcChaikinOsc( - this List qdList, - int fastPeriods, - int slowPeriods) - { - // check parameter arguments - ValidateChaikinOsc(fastPeriods, slowPeriods); - - // money flow - List results = qdList.CalcAdl(null) - .Select(r => new ChaikinOscResult(r.Date) { - MoneyFlowMultiplier = r.MoneyFlowMultiplier, - MoneyFlowVolume = r.MoneyFlowVolume, - Adl = r.Adl - }) - .ToList(); - - // EMA of ADL - List<(DateTime Date, double)> tpAdl = results - .Select(x => ( - x.Date, (double)(x.Adl ?? double.NaN))) - .ToList(); - - List adlEmaSlow = tpAdl.CalcEma(slowPeriods); - List adlEmaFast = tpAdl.CalcEma(fastPeriods); - - // add Oscillator - for (int i = slowPeriods - 1; i < results.Count; i++) - { - ChaikinOscResult r = results[i]; - - EmaResult f = adlEmaFast[i]; - EmaResult s = adlEmaSlow[i]; - - r.Oscillator = f.Ema - s.Ema; - } - - return results; - } - - // parameter validation - private static void ValidateChaikinOsc( - int fastPeriods, - int slowPeriods) - { - // check parameter arguments - if (fastPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, - "Fast lookback periods must be greater than 0 for Chaikin Oscillator."); - } - - if (slowPeriods <= fastPeriods) - { - throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, - "Slow lookback periods must be greater than Fast lookback period for Chaikin Oscillator."); - } - } -} diff --git a/src/a-d/ChaikinOsc/ChaikinOsc.StaticSeries.cs b/src/a-d/ChaikinOsc/ChaikinOsc.StaticSeries.cs new file mode 100644 index 000000000..f3d1b3b6c --- /dev/null +++ b/src/a-d/ChaikinOsc/ChaikinOsc.StaticSeries.cs @@ -0,0 +1,46 @@ +namespace Skender.Stock.Indicators; + +// CHAIKIN OSCILLATOR (SERIES) + +public static partial class ChaikinOsc +{ + public static IReadOnlyList ToChaikinOsc( + this IReadOnlyList quotes, + int fastPeriods = 3, + int slowPeriods = 10) + where TQuote : IQuote + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(quotes); + Validate(fastPeriods, slowPeriods); + + // initialize + int length = quotes.Count; + List results = new(length); + + // money flow + IReadOnlyList adlResults = quotes.ToAdl(); + + // fast/slow EMA of ADL + IReadOnlyList adlEmaSlow = adlResults.ToEma(slowPeriods); + IReadOnlyList adlEmaFast = adlResults.ToEma(fastPeriods); + + // roll through source values + for (int i = 0; i < length; i++) + { + AdlResult a = adlResults[i]; + EmaResult f = adlEmaFast[i]; + EmaResult s = adlEmaSlow[i]; + + results.Add(new( + Timestamp: a.Timestamp, + MoneyFlowMultiplier: a.MoneyFlowMultiplier, + MoneyFlowVolume: a.MoneyFlowVolume, + Adl: a.Adl, + Oscillator: f.Ema - s.Ema + )); + } + + return results; + } +} diff --git a/src/a-d/ChaikinOsc/ChaikinOsc.Utilities.cs b/src/a-d/ChaikinOsc/ChaikinOsc.Utilities.cs index d63404607..3e21a64c0 100644 --- a/src/a-d/ChaikinOsc/ChaikinOsc.Utilities.cs +++ b/src/a-d/ChaikinOsc/ChaikinOsc.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// CHAIKIN OSCILLATOR (UTILITIES) + +public static partial class ChaikinOsc { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int s = results .ToList() @@ -14,4 +15,23 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(s + 100); } + + // parameter validation + internal static void Validate( + int fastPeriods, + int slowPeriods) + { + // check parameter arguments + if (fastPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, + "Fast lookback periods must be greater than 0 for Chaikin Oscillator."); + } + + if (slowPeriods <= fastPeriods) + { + throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, + "Slow lookback periods must be greater than Fast lookback period for Chaikin Oscillator."); + } + } } diff --git a/src/a-d/Chandelier/Chandelier.Api.cs b/src/a-d/Chandelier/Chandelier.Api.cs deleted file mode 100644 index 5aba2b735..000000000 --- a/src/a-d/Chandelier/Chandelier.Api.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHANDELIER EXIT (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetChandelier( - this IEnumerable quotes, - int lookbackPeriods = 22, - double multiplier = 3, - ChandelierType type = ChandelierType.Long) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcChandelier(lookbackPeriods, multiplier, type); -} diff --git a/src/a-d/Chandelier/Chandelier.Models.cs b/src/a-d/Chandelier/Chandelier.Models.cs index 2239680cb..8fc6b4209 100644 --- a/src/a-d/Chandelier/Chandelier.Models.cs +++ b/src/a-d/Chandelier/Chandelier.Models.cs @@ -1,16 +1,13 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class ChandelierResult : ResultBase, IReusableResult +public record ChandelierResult +( + DateTime Timestamp, + double? ChandelierExit +) : IReusable { - public ChandelierResult(DateTime date) - { - Date = date; - } - - public double? ChandelierExit { get; set; } - - double? IReusableResult.Value => ChandelierExit; + public double Value => ChandelierExit.Null2NaN(); } public enum ChandelierType diff --git a/src/a-d/Chandelier/Chandelier.Series.cs b/src/a-d/Chandelier/Chandelier.StaticSeries.cs similarity index 53% rename from src/a-d/Chandelier/Chandelier.Series.cs rename to src/a-d/Chandelier/Chandelier.StaticSeries.cs index bdfe189fe..8f13ebeb8 100644 --- a/src/a-d/Chandelier/Chandelier.Series.cs +++ b/src/a-d/Chandelier/Chandelier.StaticSeries.cs @@ -1,31 +1,40 @@ namespace Skender.Stock.Indicators; // CHANDELIER EXIT (SERIES) -public static partial class Indicator + +public static partial class Chandelier { - internal static List CalcChandelier( - this List qdList, + public static IReadOnlyList ToChandelier( + this IReadOnlyList quotes, + int lookbackPeriods = 22, + double multiplier = 3, + ChandelierType type = ChandelierType.Long) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcChandelier(lookbackPeriods, multiplier, type); + + private static List CalcChandelier( + this IReadOnlyList source, int lookbackPeriods, double multiplier, ChandelierType type) { // check parameter arguments - ValidateChandelier(lookbackPeriods, multiplier); + Validate(lookbackPeriods, multiplier); // initialize - int length = qdList.Count; + int length = source.Count; List results = new(length); - List atrResult = qdList - .CalcAtr(lookbackPeriods) - .ToList(); - // roll through quotes + IReadOnlyList atrResult + = source.CalcAtr(lookbackPeriods); + + // roll through source values for (int i = 0; i < length; i++) { - QuoteD q = qdList[i]; + QuoteD q = source[i]; - ChandelierResult r = new(q.Date); - results.Add(r); + double? exit = null; // add exit values if (i >= lookbackPeriods) @@ -39,14 +48,14 @@ internal static List CalcChandelier( double maxHigh = 0; for (int p = i + 1 - lookbackPeriods; p <= i; p++) { - QuoteD d = qdList[p]; + QuoteD d = source[p]; if (d.High > maxHigh) { maxHigh = d.High; } } - r.ChandelierExit = maxHigh - (atr * multiplier); + exit = maxHigh - (atr * multiplier); break; case ChandelierType.Short: @@ -54,41 +63,26 @@ internal static List CalcChandelier( double minLow = double.MaxValue; for (int p = i + 1 - lookbackPeriods; p <= i; p++) { - QuoteD d = qdList[p]; + QuoteD d = source[p]; if (d.Low < minLow) { minLow = d.Low; } } - r.ChandelierExit = minLow + (atr * multiplier); + exit = minLow + (atr * multiplier); break; default: throw new ArgumentOutOfRangeException(nameof(type)); } } - } - - return results; - } - // parameter validation - private static void ValidateChandelier( - int lookbackPeriods, - double multiplier) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Chandelier Exit."); + results.Add(new( + Timestamp: q.Timestamp, + ChandelierExit: exit)); } - if (multiplier <= 0) - { - throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, - "Multiplier must be greater than 0 for Chandelier Exit."); - } + return results; } } diff --git a/src/a-d/Chandelier/Chandelier.Utilities.cs b/src/a-d/Chandelier/Chandelier.Utilities.cs index a98e81b0e..f61fe73ec 100644 --- a/src/a-d/Chandelier/Chandelier.Utilities.cs +++ b/src/a-d/Chandelier/Chandelier.Utilities.cs @@ -1,17 +1,25 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// CHANDELIER EXIT (UTILITIES) + +public static partial class Chandelier { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods, + double multiplier) { - int removePeriods = results - .ToList() - .FindIndex(x => x.ChandelierExit != null); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Chandelier Exit."); + } - return results.Remove(removePeriods); + if (multiplier <= 0) + { + throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, + "Multiplier must be greater than 0 for Chandelier Exit."); + } } } diff --git a/src/a-d/Chop/Chop.Api.cs b/src/a-d/Chop/Chop.Api.cs deleted file mode 100644 index 6535d5928..000000000 --- a/src/a-d/Chop/Chop.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHOPPINESS INDEX (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetChop( - this IEnumerable quotes, - int lookbackPeriods = 14) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcChop(lookbackPeriods); -} diff --git a/src/a-d/Chop/Chop.Models.cs b/src/a-d/Chop/Chop.Models.cs index a444acc58..27aeb760a 100644 --- a/src/a-d/Chop/Chop.Models.cs +++ b/src/a-d/Chop/Chop.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class ChopResult : ResultBase, IReusableResult +public record ChopResult +( + DateTime Timestamp, + double? Chop +) : IReusable { - public ChopResult(DateTime date) - { - Date = date; - } - - public double? Chop { get; set; } - - double? IReusableResult.Value => Chop; + public double Value => Chop.Null2NaN(); } diff --git a/src/a-d/Chop/Chop.Series.cs b/src/a-d/Chop/Chop.Series.cs deleted file mode 100644 index 1329d2e23..000000000 --- a/src/a-d/Chop/Chop.Series.cs +++ /dev/null @@ -1,79 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHOPPINESS INDEX (SERIES) -public static partial class Indicator -{ - internal static List CalcChop( - this List qdList, - int lookbackPeriods) - { - // check parameter arguments - ValidateChop(lookbackPeriods); - - // initialize - double sum; - double high; - double low; - double range; - - int length = qdList.Count; - List results = new(length); - double[] trueHigh = new double[length]; - double[] trueLow = new double[length]; - double[] trueRange = new double[length]; - - // roll through quotes - for (int i = 0; i < qdList.Count; i++) - { - ChopResult r = new(qdList[i].Date); - results.Add(r); - - if (i > 0) - { - trueHigh[i] = Math.Max(qdList[i].High, qdList[i - 1].Close); - trueLow[i] = Math.Min(qdList[i].Low, qdList[i - 1].Close); - trueRange[i] = trueHigh[i] - trueLow[i]; - - // calculate CHOP - - if (i >= lookbackPeriods) - { - // reset measurements - sum = trueRange[i]; - high = trueHigh[i]; - low = trueLow[i]; - - // iterate over lookback window - for (int j = 1; j < lookbackPeriods; j++) - { - sum += trueRange[i - j]; - high = Math.Max(high, trueHigh[i - j]); - low = Math.Min(low, trueLow[i - j]); - } - - range = high - low; - - // calculate CHOP - if (range != 0) - { - r.Chop = 100 * (Math.Log(sum / range) / Math.Log(lookbackPeriods)); - } - } - } - } - - return results; - } - - // parameter validation - private static void ValidateChop( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for CHOP."); - } - } -} diff --git a/src/a-d/Chop/Chop.StaticSeries.cs b/src/a-d/Chop/Chop.StaticSeries.cs new file mode 100644 index 000000000..2237f86c1 --- /dev/null +++ b/src/a-d/Chop/Chop.StaticSeries.cs @@ -0,0 +1,73 @@ +namespace Skender.Stock.Indicators; + +// CHOPPINESS INDEX (SERIES) + +public static partial class Chop +{ + public static IReadOnlyList ToChop( + this IReadOnlyList quotes, + int lookbackPeriods = 14) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcChop(lookbackPeriods); + + private static List CalcChop( + this IReadOnlyList source, + int lookbackPeriods) + { + // check parameter arguments + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + double[] trueHigh = new double[length]; + double[] trueLow = new double[length]; + double[] trueRange = new double[length]; + + // roll through source values + for (int i = 0; i < length; i++) + { + double? chop = null; + + if (i > 0) + { + trueHigh[i] = Math.Max(source[i].High, source[i - 1].Close); + trueLow[i] = Math.Min(source[i].Low, source[i - 1].Close); + trueRange[i] = trueHigh[i] - trueLow[i]; + + // calculate CHOP + + if (i >= lookbackPeriods) + { + // reset measurements + double sum = trueRange[i]; + double high = trueHigh[i]; + double low = trueLow[i]; + + // iterate over lookback window + for (int j = 1; j < lookbackPeriods; j++) + { + sum += trueRange[i - j]; + high = Math.Max(high, trueHigh[i - j]); + low = Math.Min(low, trueLow[i - j]); + } + + double range = high - low; + + // calculate CHOP + if (range != 0) + { + chop = 100 * (Math.Log(sum / range) / Math.Log(lookbackPeriods)); + } + } + } + + results.Add(new( + Timestamp: source[i].Timestamp, + Chop: chop)); + } + + return results; + } +} diff --git a/src/a-d/Chop/Chop.Utilities.cs b/src/a-d/Chop/Chop.Utilities.cs index 3fed5751b..08bdb70dc 100644 --- a/src/a-d/Chop/Chop.Utilities.cs +++ b/src/a-d/Chop/Chop.Utilities.cs @@ -1,17 +1,18 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// CHOPPINESS INDEX (UTILITIES) + +public static partial class Chop { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Chop != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for CHOP."); + } } } diff --git a/src/a-d/Cmf/Cmf.Api.cs b/src/a-d/Cmf/Cmf.Api.cs deleted file mode 100644 index 02933f9ab..000000000 --- a/src/a-d/Cmf/Cmf.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHAIKIN MONEY FLOW (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetCmf( - this IEnumerable quotes, - int lookbackPeriods = 20) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcCmf(lookbackPeriods); -} diff --git a/src/a-d/Cmf/Cmf.Models.cs b/src/a-d/Cmf/Cmf.Models.cs index 914ac667a..6159a061f 100644 --- a/src/a-d/Cmf/Cmf.Models.cs +++ b/src/a-d/Cmf/Cmf.Models.cs @@ -1,16 +1,13 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class CmfResult : ResultBase, IReusableResult +public record CmfResult +( + DateTime Timestamp, + double? MoneyFlowMultiplier, + double? MoneyFlowVolume, + double? Cmf +) : IReusable { - public CmfResult(DateTime date) - { - Date = date; - } - - public double? MoneyFlowMultiplier { get; set; } - public double? MoneyFlowVolume { get; set; } - public double? Cmf { get; set; } - - double? IReusableResult.Value => Cmf; + public double Value => Cmf.Null2NaN(); } diff --git a/src/a-d/Cmf/Cmf.Series.cs b/src/a-d/Cmf/Cmf.Series.cs deleted file mode 100644 index d3eb8e391..000000000 --- a/src/a-d/Cmf/Cmf.Series.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHAIKIN MONEY FLOW (SERIES) -public static partial class Indicator -{ - internal static List CalcCmf( - this List qdList, - int lookbackPeriods) - { - // convert quotes - List<(DateTime, double)> tpList = qdList.ToTuple(CandlePart.Volume); - - // check parameter arguments - ValidateCmf(lookbackPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - List adlResults = qdList.CalcAdl(null).ToList(); - - // roll through quotes - for (int i = 0; i < length; i++) - { - AdlResult adl = adlResults[i]; - - CmfResult r = new(adl.Date) { - MoneyFlowMultiplier = adl.MoneyFlowMultiplier, - MoneyFlowVolume = adl.MoneyFlowVolume - }; - results.Add(r); - - if (i >= lookbackPeriods - 1) - { - double? sumMfv = 0; - double? sumVol = 0; - - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - (DateTime _, double pValue) = tpList[p]; - sumVol += pValue; - - AdlResult d = adlResults[p]; - sumMfv += d.MoneyFlowVolume; - } - - double? avgMfv = sumMfv / lookbackPeriods; - double? avgVol = sumVol / lookbackPeriods; - - if (avgVol != 0) - { - r.Cmf = avgMfv / avgVol; - } - } - } - - return results; - } - - // parameter validation - private static void ValidateCmf( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Chaikin Money Flow."); - } - } -} diff --git a/src/a-d/Cmf/Cmf.StaticSeries.cs b/src/a-d/Cmf/Cmf.StaticSeries.cs new file mode 100644 index 000000000..063d60b5b --- /dev/null +++ b/src/a-d/Cmf/Cmf.StaticSeries.cs @@ -0,0 +1,68 @@ +namespace Skender.Stock.Indicators; + +// CHAIKIN MONEY FLOW (SERIES) + +public static partial class Cmf +{ + public static IReadOnlyList ToCmf( + this IReadOnlyList quotes, + int lookbackPeriods = 20) + where TQuote : IQuote => quotes + .ToSortedList() + .CalcCmf(lookbackPeriods); + + private static List CalcCmf( + this IReadOnlyList source, + int lookbackPeriods) + where TQuote : IQuote + { + // get volume array + double[] volume + = source.Select(v => (double)v.Volume).ToArray(); + + // check parameter arguments + Validate(lookbackPeriods); + + // initialize + int length = volume.Length; + List results = new(length); + IReadOnlyList adlResults = source.ToAdl(); + + // roll through source values + for (int i = 0; i < length; i++) + { + AdlResult adl = adlResults[i]; + double? cmf = null; + + if (i >= lookbackPeriods - 1) + { + double? sumMfv = 0; + double? sumVol = 0; + + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + sumVol += volume[p]; + + AdlResult d = adlResults[p]; + sumMfv += d.MoneyFlowVolume; + } + + double? avgMfv = sumMfv / lookbackPeriods; + double? avgVol = sumVol / lookbackPeriods; + + if (avgVol != 0) + { + cmf = avgMfv / avgVol; + } + } + + results.Add(new( + Timestamp: adl.Timestamp, + MoneyFlowMultiplier: adl.MoneyFlowMultiplier, + MoneyFlowVolume: adl.MoneyFlowVolume, + Cmf: cmf)); + } + + return results; + } +} diff --git a/src/a-d/Cmf/Cmf.Utilities.cs b/src/a-d/Cmf/Cmf.Utilities.cs index 69a6f1ac6..360263a04 100644 --- a/src/a-d/Cmf/Cmf.Utilities.cs +++ b/src/a-d/Cmf/Cmf.Utilities.cs @@ -1,17 +1,19 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// CHAIKIN MONEY FLOW (UTILITIES) + +public static partial class Cmf { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Cmf != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Chaikin Money Flow."); + } } + } diff --git a/src/a-d/Cmo/Cmo.Api.cs b/src/a-d/Cmo/Cmo.Api.cs deleted file mode 100644 index 68aa1f18c..000000000 --- a/src/a-d/Cmo/Cmo.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHANDE MOMENTUM OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetCmo( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcCmo(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetCmo( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcCmo(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetCmo( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcCmo(lookbackPeriods); -} diff --git a/src/a-d/Cmo/Cmo.Models.cs b/src/a-d/Cmo/Cmo.Models.cs index b7caef49e..2a9053965 100644 --- a/src/a-d/Cmo/Cmo.Models.cs +++ b/src/a-d/Cmo/Cmo.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class CmoResult : ResultBase, IReusableResult +public record CmoResult +( + DateTime Timestamp, + double? Cmo = null +) : IReusable { - public CmoResult(DateTime date) - { - Date = date; - } - - public double? Cmo { get; set; } - - double? IReusableResult.Value => Cmo; + public double Value => Cmo.Null2NaN(); } diff --git a/src/a-d/Cmo/Cmo.Series.cs b/src/a-d/Cmo/Cmo.Series.cs deleted file mode 100644 index a45f8545c..000000000 --- a/src/a-d/Cmo/Cmo.Series.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHANDE MOMENTUM OSCILLATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcCmo( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateCmo(lookbackPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - List<(bool? isUp, double value)> ticks = new(length); - double prevValue = double.NaN; - - // add initial record - if (length > 0) - { - results.Add(new CmoResult(tpList[0].Item1)); - ticks.Add((null, double.NaN)); - - prevValue = tpList[0].Item2; - } - - // roll through quotes - for (int i = 1; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - CmoResult r = new(date); - results.Add(r); - ticks.Add(( - value > prevValue ? true - : value < prevValue ? false - : null, Math.Abs(value - prevValue))); - - if (i >= lookbackPeriods) - { - double sH = 0; - double sL = 0; - - for (int p = i - lookbackPeriods + 1; p <= i; p++) - { - (bool? isUp, double pDiff) = ticks[p]; - - if (isUp is null) - { - continue; - } - else if (isUp == true) - { - sH += pDiff; - } - else - { - sL += pDiff; - } - } - - r.Cmo = (sH + sL != 0) - ? (100 * (sH - sL) / (sH + sL)).NaN2Null() - : null; - } - - prevValue = value; - } - - return results; - } - - // parameter validation - private static void ValidateCmo( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for CMO."); - } - } -} diff --git a/src/a-d/Cmo/Cmo.StaticSeries.cs b/src/a-d/Cmo/Cmo.StaticSeries.cs new file mode 100644 index 000000000..8cd1d448f --- /dev/null +++ b/src/a-d/Cmo/Cmo.StaticSeries.cs @@ -0,0 +1,93 @@ +namespace Skender.Stock.Indicators; + +// CHANDE MOMENTUM OSCILLATOR (SERIES) + +public static partial class Cmo +{ + public static IReadOnlyList ToCmo( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + List<(bool? isUp, double value)> ticks = new(length); + + // discontinue of empty + if (length == 0) + { + return results; + } + + // initialize, add first records + double prevValue = source[0].Value; + + results.Add(new(source[0].Timestamp)); + ticks.Add((null, double.NaN)); + + // roll through remaining values + for (int i = 1; i < length; i++) + { + T s = source[i]; + double? cmo = null; + + // determine tick direction and size + (bool? isUp, double value) tick = (null, Math.Abs(s.Value - prevValue)); + + tick.isUp = double.IsNaN(tick.value) || s.Value == prevValue + ? null + : s.Value > prevValue; + + ticks.Add(tick); + + // calculate CMO + if (i >= lookbackPeriods) + { + double sH = 0; + double sL = 0; + + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + (bool? isUp, double pDiff) = ticks[p]; + + if (double.IsNaN(pDiff)) + { + sH = double.NaN; + sL = double.NaN; + break; + } + + // up + + if (isUp == true) + { + sH += pDiff; + } + + // down + else + { + sL += pDiff; + } + } + + cmo = sH + sL != 0 + ? (100 * (sH - sL) / (sH + sL)).NaN2Null() + : null; + } + + results.Add(new( + Timestamp: s.Timestamp, + Cmo: cmo)); + + prevValue = s.Value; + } + + return results; + } +} diff --git a/src/a-d/Cmo/Cmo.Utilities.cs b/src/a-d/Cmo/Cmo.Utilities.cs index 936073359..5ed18a20a 100644 --- a/src/a-d/Cmo/Cmo.Utilities.cs +++ b/src/a-d/Cmo/Cmo.Utilities.cs @@ -1,17 +1,18 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// CHANDE MOMENTUM OSCILLATOR (UTILITIES) + +public static partial class Cmo { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Cmo != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for CMO."); + } } } diff --git a/src/a-d/ConnorsRsi/ConnorsRsi.Api.cs b/src/a-d/ConnorsRsi/ConnorsRsi.Api.cs deleted file mode 100644 index f7eba8633..000000000 --- a/src/a-d/ConnorsRsi/ConnorsRsi.Api.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CONNORS RSI (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetConnorsRsi( - this IEnumerable quotes, - int rsiPeriods = 3, - int streakPeriods = 2, - int rankPeriods = 100) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcConnorsRsi(rsiPeriods, streakPeriods, rankPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetConnorsRsi( - this IEnumerable results, - int rsiPeriods = 3, - int streakPeriods = 2, - int rankPeriods = 100) => results - .ToTuple() - .CalcConnorsRsi(rsiPeriods, streakPeriods, rankPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetConnorsRsi( - this IEnumerable<(DateTime, double)> priceTuples, - int rsiPeriods = 3, - int streakPeriods = 2, - int rankPeriods = 100) => priceTuples - .ToSortedList() - .CalcConnorsRsi(rsiPeriods, streakPeriods, rankPeriods); -} diff --git a/src/a-d/ConnorsRsi/ConnorsRsi.Models.cs b/src/a-d/ConnorsRsi/ConnorsRsi.Models.cs index 8f7556e57..9a8f29a08 100644 --- a/src/a-d/ConnorsRsi/ConnorsRsi.Models.cs +++ b/src/a-d/ConnorsRsi/ConnorsRsi.Models.cs @@ -1,19 +1,15 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class ConnorsRsiResult : ResultBase, IReusableResult +public record ConnorsRsiResult +( + DateTime Timestamp, + double Streak, + double? Rsi = null, + double? RsiStreak = null, + double? PercentRank = null, + double? ConnorsRsi = null +) : IReusable { - public ConnorsRsiResult(DateTime date) - { - Date = date; - } - - public double? Rsi { get; set; } - public double? RsiStreak { get; set; } - public double? PercentRank { get; set; } - public double? ConnorsRsi { get; set; } - - // internal use only - internal int Streak { get; set; } - double? IReusableResult.Value => ConnorsRsi; + public double Value => ConnorsRsi.Null2NaN(); } diff --git a/src/a-d/ConnorsRsi/ConnorsRsi.Series.cs b/src/a-d/ConnorsRsi/ConnorsRsi.Series.cs deleted file mode 100644 index 918df784c..000000000 --- a/src/a-d/ConnorsRsi/ConnorsRsi.Series.cs +++ /dev/null @@ -1,157 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CONNORS RSI (SERIES) -public static partial class Indicator -{ - internal static List CalcConnorsRsi( - this List<(DateTime, double)> tpList, - int rsiPeriods, - int streakPeriods, - int rankPeriods) - { - // check parameter arguments - ValidateConnorsRsi(rsiPeriods, streakPeriods, rankPeriods); - - // initialize - List results = tpList.CalcStreak(rsiPeriods, rankPeriods); - int startPeriod = Math.Max(rsiPeriods, Math.Max(streakPeriods, rankPeriods)) + 2; - int length = results.Count; - - // RSI of streak - List<(DateTime Date, double Streak)> bdStreak = results - .Remove(Math.Min(length, 1)) - .Select(x => (x.Date, (double)x.Streak)) - .ToList(); - - List rsiStreak = CalcRsi(bdStreak, streakPeriods); - - // compose final results - for (int p = streakPeriods + 2; p < length; p++) - { - ConnorsRsiResult r = results[p]; - RsiResult k = rsiStreak[p - 1]; - - r.RsiStreak = k.Rsi; - - if (p + 1 >= startPeriod) - { - r.ConnorsRsi = (r.Rsi + r.RsiStreak + r.PercentRank) / 3; - } - } - - return results; - } - - // calculate baseline streak and rank - private static List CalcStreak( - this List<(DateTime Date, double Streak)> tpList, - int rsiPeriods, - int rankPeriods) - { - // initialize - List rsiResults = CalcRsi(tpList, rsiPeriods); - - int length = tpList.Count; - List results = new(length); - double[] gain = new double[length]; - - double lastClose = double.NaN; - int streak = 0; - - // compose interim results - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - ConnorsRsiResult r = new(date) { - Rsi = rsiResults[i].Rsi - }; - results.Add(r); - - // bypass for first record - if (i == 0) - { - lastClose = value; - continue; - } - - // streak of up or down - if (value == lastClose) - { - streak = 0; - } - else if (value > lastClose) - { - if (streak >= 0) - { - streak++; - } - else - { - streak = 1; - } - } - else // h.Value < lastClose - { - if (streak <= 0) - { - streak--; - } - else - { - streak = -1; - } - } - - r.Streak = streak; - - // percentile rank - gain[i] = (lastClose <= 0) ? double.NaN - : (value - lastClose) / lastClose; - - if (i + 1 > rankPeriods) - { - int qty = 0; - for (int p = i - rankPeriods; p <= i; p++) - { - if (gain[p] < gain[i]) - { - qty++; - } - } - - r.PercentRank = 100 * qty / rankPeriods; - } - - lastClose = value; - } - - return results; - } - - // parameter validation - private static void ValidateConnorsRsi( - int rsiPeriods, - int streakPeriods, - int rankPeriods) - { - // check parameter arguments - if (rsiPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(rsiPeriods), rsiPeriods, - "RSI period for Close price must be greater than 1 for ConnorsRsi."); - } - - if (streakPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(streakPeriods), streakPeriods, - "RSI period for Streak must be greater than 1 for ConnorsRsi."); - } - - if (rankPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(rankPeriods), rankPeriods, - "Percent Rank periods must be greater than 1 for ConnorsRsi."); - } - } -} diff --git a/src/a-d/ConnorsRsi/ConnorsRsi.StaticSeries.cs b/src/a-d/ConnorsRsi/ConnorsRsi.StaticSeries.cs new file mode 100644 index 000000000..9f224edca --- /dev/null +++ b/src/a-d/ConnorsRsi/ConnorsRsi.StaticSeries.cs @@ -0,0 +1,168 @@ +namespace Skender.Stock.Indicators; + +// CONNORS RSI (SERIES) + +public static partial class ConnorsRsi +{ + public static IReadOnlyList ToConnorsRsi( + this IReadOnlyList source, + int rsiPeriods = 3, + int streakPeriods = 2, + int rankPeriods = 100) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(rsiPeriods, streakPeriods, rankPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + int startPeriod + = Math.Max(rsiPeriods, Math.Max(streakPeriods, rankPeriods)) + 2; + + IReadOnlyList streakInfo + = source.CalcStreak(rsiPeriods, rankPeriods); + + // RSI of streak + IReadOnlyList rsiStreak = streakInfo + .Select(si => new QuotePart(si.Timestamp, si.Streak)) + .ToList() + .ToRsi(streakPeriods); + + // compose final results + for (int i = 0; i < length; i++) + { + if (i >= streakPeriods + 2) + { + ConnorsRsiResult sInfo = streakInfo[i]; + RsiResult sRsi = rsiStreak[i]; + + double? crsi = null; + + if (i >= startPeriod - 1) + { + crsi = (sInfo.Rsi + sRsi.Rsi + sInfo.PercentRank) / 3; + } + + results.Add(sInfo with { + ConnorsRsi = crsi, + RsiStreak = sRsi.Rsi + }); + } + + // warmup periods + else + { + ConnorsRsiResult sInfo = streakInfo[i]; + results.Add(sInfo); + } + } + + return results; + } + + // calculate baseline streak and rank + private static List CalcStreak( + this IReadOnlyList source, + int rsiPeriods, + int rankPeriods) + where T : IReusable + { + // initialize + IReadOnlyList rsiResults = source.ToRsi(rsiPeriods); + + int length = source.Count; + List results = new(length); + double[] gain = new double[length]; + + double streak = 0; + double prevPrice = double.NaN; + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + double? percentRank = null; + + // bypass for first record + if (i == 0) + { + prevPrice = s.Value; + results.Add(new(s.Timestamp, 0)); + continue; + } + + // streak of up or down + if (double.IsNaN(s.Value) || double.IsNaN(prevPrice)) + { + streak = double.NaN; + } + else if (s.Value > prevPrice) + { + if (streak >= 0) + { + streak++; + } + else + { + streak = 1; + } + } + else if (s.Value < prevPrice) + { + if (streak <= 0) + { + streak--; + } + else + { + streak = -1; + } + } + else + { + streak = 0; + } + + // percentile rank + gain[i] = double.IsNaN(s.Value) || double.IsNaN(prevPrice) || prevPrice <= 0 + ? double.NaN + : (s.Value - prevPrice) / prevPrice; + + if (i > rankPeriods - 1 && !double.IsNaN(gain[i])) + { + int qty = 0; + bool isViableRank = true; + for (int p = i - rankPeriods; p <= i; p++) + { + // rank is not viable if there + // are incalculable gain values + if (double.IsNaN(gain[p])) + { + isViableRank = false; + break; + } + + if (gain[p] < gain[i]) + { + qty++; + } + } + + percentRank = isViableRank ? 100 * qty / rankPeriods : null; + } + + results.Add(new ConnorsRsiResult( + Timestamp: s.Timestamp, + Streak: streak, + Rsi: rsiResults[i].Rsi, + PercentRank: percentRank)); + + prevPrice = s.Value; + } + + return results; + } +} diff --git a/src/a-d/ConnorsRsi/ConnorsRsi.Utilities.cs b/src/a-d/ConnorsRsi/ConnorsRsi.Utilities.cs index be0489840..6fe17591f 100644 --- a/src/a-d/ConnorsRsi/ConnorsRsi.Utilities.cs +++ b/src/a-d/ConnorsRsi/ConnorsRsi.Utilities.cs @@ -1,17 +1,32 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// CONNORS RSI (UTILITIES) + +public static partial class ConnorsRsi { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int rsiPeriods, + int streakPeriods, + int rankPeriods) { - int n = results - .ToList() - .FindIndex(x => x.ConnorsRsi != null); + // check parameter arguments + if (rsiPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(rsiPeriods), rsiPeriods, + "RSI period for Close price must be greater than 1 for ConnorsRsi."); + } + + if (streakPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(streakPeriods), streakPeriods, + "RSI period for Streak must be greater than 1 for ConnorsRsi."); + } - return results.Remove(n); + if (rankPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(rankPeriods), rankPeriods, + "Percent Rank periods must be greater than 1 for ConnorsRsi."); + } } } diff --git a/src/a-d/Correlation/Correlation.Api.cs b/src/a-d/Correlation/Correlation.Api.cs deleted file mode 100644 index 709e77697..000000000 --- a/src/a-d/Correlation/Correlation.Api.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CORRELATION COEFFICIENT (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetCorrelation( - this IEnumerable quotesA, - IEnumerable quotesB, - int lookbackPeriods) - where TQuote : IQuote - { - List<(DateTime, double)> tpListA - = quotesA.ToTuple(CandlePart.Close); - - List<(DateTime, double)> tpListB - = quotesB.ToTuple(CandlePart.Close); - - return CalcCorrelation(tpListA, tpListB, lookbackPeriods); - } - - // SERIES, from CHAINS (both inputs reusable) - public static IEnumerable GetCorrelation( - this IEnumerable quotesA, - IEnumerable quotesB, - int lookbackPeriods) - { - List<(DateTime Date, double Value)> tpListA - = quotesA.ToTuple(); - - List<(DateTime Date, double Value)> tpListB - = quotesB.ToTuple(); - - return CalcCorrelation(tpListA, tpListB, lookbackPeriods) - .SyncIndex(quotesA, SyncType.Prepend); - } - - // SERIES, from TUPLE - public static IEnumerable GetCorrelation( - this IEnumerable<(DateTime, double)> tuplesA, - IEnumerable<(DateTime, double)> tuplesB, - int lookbackPeriods) - { - List<(DateTime, double)> tpListA = tuplesA.ToSortedList(); - List<(DateTime, double)> tpListB = tuplesB.ToSortedList(); - - return CalcCorrelation(tpListA, tpListB, lookbackPeriods); - } -} diff --git a/src/a-d/Correlation/Correlation.Models.cs b/src/a-d/Correlation/Correlation.Models.cs index 04ef2516b..08f4f0808 100644 --- a/src/a-d/Correlation/Correlation.Models.cs +++ b/src/a-d/Correlation/Correlation.Models.cs @@ -1,18 +1,15 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class CorrResult : ResultBase, IReusableResult +public record CorrResult +( + DateTime Timestamp, + double? VarianceA = null, + double? VarianceB = null, + double? Covariance = null, + double? Correlation = null, + double? RSquared = null +) : IReusable { - public CorrResult(DateTime date) - { - Date = date; - } - - public double? VarianceA { get; set; } - public double? VarianceB { get; set; } - public double? Covariance { get; set; } - public double? Correlation { get; set; } - public double? RSquared { get; set; } - - double? IReusableResult.Value => Correlation; + public double Value => Correlation.Null2NaN(); } diff --git a/src/a-d/Correlation/Correlation.Series.cs b/src/a-d/Correlation/Correlation.Series.cs deleted file mode 100644 index bd60e26a6..000000000 --- a/src/a-d/Correlation/Correlation.Series.cs +++ /dev/null @@ -1,119 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CORRELATION COEFFICIENT (SERIES) -public static partial class Indicator -{ - internal static List CalcCorrelation( - this List<(DateTime, double)> tpListA, - List<(DateTime, double)> tpListB, - int lookbackPeriods) - { - // check parameter arguments - ValidateCorrelation(tpListA, tpListB, lookbackPeriods); - - // initialize - int length = tpListA.Count; - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime aDate, double _) = tpListA[i]; - (DateTime bDate, double _) = tpListB[i]; - - if (aDate != bDate) - { - throw new InvalidQuotesException(nameof(tpListA), aDate, - "Date sequence does not match. Correlation requires matching dates in provided histories."); - } - - CorrResult r = new(aDate); - results.Add(r); - - // calculate correlation - if (i >= lookbackPeriods - 1) - { - double[] dataA = new double[lookbackPeriods]; - double[] dataB = new double[lookbackPeriods]; - int z = 0; - - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - dataA[z] = tpListA[p].Item2; - dataB[z] = tpListB[p].Item2; - - z++; - } - - r.PeriodCorrelation(dataA, dataB); - } - } - - return results; - } - - // calculate correlation - private static void PeriodCorrelation( - this CorrResult r, - double[] dataA, - double[] dataB) - { - int length = dataA.Length; - double sumA = 0; - double sumB = 0; - double sumA2 = 0; - double sumB2 = 0; - double sumAB = 0; - - for (int i = 0; i < length; i++) - { - double a = dataA[i]; - double b = dataB[i]; - - sumA += a; - sumB += b; - sumA2 += a * a; - sumB2 += b * b; - sumAB += a * b; - } - - double avgA = sumA / length; - double avgB = sumB / length; - double avgA2 = sumA2 / length; - double avgB2 = sumB2 / length; - double avgAB = sumAB / length; - - double varA = avgA2 - (avgA * avgA); - double varB = avgB2 - (avgB * avgB); - double cov = avgAB - (avgA * avgB); - double divisor = Math.Sqrt(varA * varB); - - r.VarianceA = varA.NaN2Null(); - r.VarianceB = varB.NaN2Null(); - r.Covariance = cov.NaN2Null(); - r.Correlation = (divisor == 0) ? null : (cov / divisor).NaN2Null(); - r.RSquared = r.Correlation * r.Correlation; - } - - // parameter validation - private static void ValidateCorrelation( - List<(DateTime, double)> quotesA, - List<(DateTime, double)> quotesB, - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Correlation."); - } - - // check quotes - if (quotesA.Count != quotesB.Count) - { - throw new InvalidQuotesException( - nameof(quotesB), - "B quotes should have at least as many records as A quotes for Correlation."); - } - } -} diff --git a/src/a-d/Correlation/Correlation.StaticSeries.cs b/src/a-d/Correlation/Correlation.StaticSeries.cs new file mode 100644 index 000000000..e82a4df70 --- /dev/null +++ b/src/a-d/Correlation/Correlation.StaticSeries.cs @@ -0,0 +1,114 @@ +namespace Skender.Stock.Indicators; + +// CORRELATION COEFFICIENT (SERIES) + +public static partial class Correlation +{ + public static IReadOnlyList ToCorrelation( + this IReadOnlyList sourceA, + IReadOnlyList sourceB, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(sourceA); + ArgumentNullException.ThrowIfNull(sourceB); + Validate(sourceA, sourceB, lookbackPeriods); + + // initialize + int length = sourceA.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + T a = sourceA[i]; + T b = sourceB[i]; + + if (a.Timestamp != b.Timestamp) + { + throw new InvalidQuotesException( + nameof(sourceA), a.Timestamp, + "Timestamp sequence does not match. " + + "Correlation requires matching dates in provided histories."); + } + + CorrResult r; + + // calculate correlation + if (i >= lookbackPeriods - 1) + { + double[] dataA = new double[lookbackPeriods]; + double[] dataB = new double[lookbackPeriods]; + int z = 0; + + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + dataA[z] = sourceA[p].Value; + dataB[z] = sourceB[p].Value; + + z++; + } + + r = PeriodCorrelation(a.Timestamp, dataA, dataB); + } + else + { + r = new(Timestamp: a.Timestamp); + } + + results.Add(r); + } + + return results; + } + + // calculate correlation + internal static CorrResult PeriodCorrelation( + DateTime timestamp, + double[] dataA, + double[] dataB) + { + int length = dataA.Length; + double sumA = 0; + double sumB = 0; + double sumA2 = 0; + double sumB2 = 0; + double sumAb = 0; + + for (int i = 0; i < length; i++) + { + double a = dataA[i]; + double b = dataB[i]; + + sumA += a; + sumB += b; + sumA2 += a * a; + sumB2 += b * b; + sumAb += a * b; + } + + double avgA = sumA / length; + double avgB = sumB / length; + double avgA2 = sumA2 / length; + double avgB2 = sumB2 / length; + double avgAb = sumAb / length; + + double varA = avgA2 - avgA * avgA; + double varB = avgB2 - avgB * avgB; + double cov = avgAb - avgA * avgB; + double divisor = Math.Sqrt(varA * varB); + + double? corr = divisor == 0 + ? null + : (cov / divisor).NaN2Null(); + + return new( + Timestamp: timestamp, + VarianceA: varA.NaN2Null(), + VarianceB: varB.NaN2Null(), + Covariance: cov.NaN2Null(), + Correlation: corr, + RSquared: corr * corr); + } +} diff --git a/src/a-d/Correlation/Correlation.Utilities.cs b/src/a-d/Correlation/Correlation.Utilities.cs index c5071fd63..00df00337 100644 --- a/src/a-d/Correlation/Correlation.Utilities.cs +++ b/src/a-d/Correlation/Correlation.Utilities.cs @@ -1,17 +1,27 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +public static partial class Correlation { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + IReadOnlyList sourceA, + IReadOnlyList sourceB, + int lookbackPeriods) + where T : ISeries { - int removePeriods = results - .ToList() - .FindIndex(x => x.Correlation != null); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Correlation."); + } - return results.Remove(removePeriods); + // check quotes + if (sourceA.Count != sourceB.Count) + { + throw new InvalidQuotesException( + nameof(sourceB), + "B quotes should have at least as many records as A quotes for Correlation."); + } } } diff --git a/src/a-d/Dema/Dema.Api.cs b/src/a-d/Dema/Dema.Api.cs deleted file mode 100644 index ebd5a475e..000000000 --- a/src/a-d/Dema/Dema.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// DOUBLE EXPONENTIAL MOVING AVERAGE - DEMA (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetDema( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcDema(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetDema( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcDema(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetDema( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcDema(lookbackPeriods); -} diff --git a/src/a-d/Dema/Dema.Models.cs b/src/a-d/Dema/Dema.Models.cs index 9d8fe3379..3dc5047f1 100644 --- a/src/a-d/Dema/Dema.Models.cs +++ b/src/a-d/Dema/Dema.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class DemaResult : ResultBase, IReusableResult +public record DemaResult +( + DateTime Timestamp, + double? Dema = null +) : IReusable { - public DemaResult(DateTime date) - { - Date = date; - } - - public double? Dema { get; set; } - - double? IReusableResult.Value => Dema; + public double Value => Dema.Null2NaN(); } diff --git a/src/a-d/Dema/Dema.Series.cs b/src/a-d/Dema/Dema.Series.cs deleted file mode 100644 index 624d013c2..000000000 --- a/src/a-d/Dema/Dema.Series.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Skender.Stock.Indicators; - -// DOUBLE EXPONENTIAL MOVING AVERAGE - DEMA (SERIES) -public static partial class Indicator -{ - internal static List CalcDema( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateDema(lookbackPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - - double k = 2d / (lookbackPeriods + 1); - double? lastEma1 = 0; - double? lastEma2; - int initPeriods = Math.Min(lookbackPeriods, length); - - for (int i = 0; i < initPeriods; i++) - { - (DateTime _, double value) = tpList[i]; - lastEma1 += value; - } - - lastEma1 /= lookbackPeriods; - lastEma2 = lastEma1; - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - DemaResult r = new(date); - results.Add(r); - - if (i > lookbackPeriods - 1) - { - double? ema1 = lastEma1 + (k * (value - lastEma1)); - double? ema2 = lastEma2 + (k * (ema1 - lastEma2)); - - r.Dema = ((2d * ema1) - ema2).NaN2Null(); - - lastEma1 = ema1; - lastEma2 = ema2; - } - else if (i == lookbackPeriods - 1) - { - r.Dema = (2d * lastEma1) - lastEma2; - } - } - - return results; - } - - // parameter validation - private static void ValidateDema( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for DEMA."); - } - } -} diff --git a/src/a-d/Dema/Dema.StaticSeries.cs b/src/a-d/Dema/Dema.StaticSeries.cs new file mode 100644 index 000000000..0a10cb5a9 --- /dev/null +++ b/src/a-d/Dema/Dema.StaticSeries.cs @@ -0,0 +1,69 @@ +namespace Skender.Stock.Indicators; + +// DOUBLE EXPONENTIAL MOVING AVERAGE (SERIES) + +public static partial class Dema +{ + public static IReadOnlyList ToDema( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double k = 2d / (lookbackPeriods + 1); + double lastEma1 = double.NaN; + double lastEma2 = double.NaN; + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + // skip incalculable periods + if (i < lookbackPeriods - 1) + { + results.Add(new(s.Timestamp)); + continue; + } + + double ema1; + double ema2; + + // when no prior EMA, reset as SMA + if (double.IsNaN(lastEma2)) + { + double sum = 0; + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + T ps = source[p]; + sum += ps.Value; + } + + ema1 = ema2 = sum / lookbackPeriods; + } + + // normal DEMA + else + { + ema1 = lastEma1 + (k * (s.Value - lastEma1)); + ema2 = lastEma2 + (k * (ema1 - lastEma2)); + } + + results.Add(new DemaResult( + Timestamp: s.Timestamp, + Dema: ((2d * ema1) - ema2).NaN2Null())); + + lastEma1 = ema1; + lastEma2 = ema2; + } + + return results; + } +} diff --git a/src/a-d/Dema/Dema.Utilities.cs b/src/a-d/Dema/Dema.Utilities.cs index 34641fa54..e04ddcead 100644 --- a/src/a-d/Dema/Dema.Utilities.cs +++ b/src/a-d/Dema/Dema.Utilities.cs @@ -1,17 +1,30 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// DOUBLE EXPONENTIAL MOVING AVERAGE (UTILITIES) + +public static partial class Dema { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() .FindIndex(x => x.Dema != null) + 1; - return results.Remove((2 * n) + 100); + return results.Remove(2 * n + 100); + } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for DEMA."); + } } } diff --git a/src/a-d/Doji/Doji.Api.cs b/src/a-d/Doji/Doji.Api.cs deleted file mode 100644 index 991a04349..000000000 --- a/src/a-d/Doji/Doji.Api.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Skender.Stock.Indicators; - -// DOJI (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetDoji( - this IEnumerable quotes, - double maxPriceChangePercent = 0.1) - where TQuote : IQuote => quotes - .CalcDoji(maxPriceChangePercent); -} diff --git a/src/a-d/Doji/Doji.Series.cs b/src/a-d/Doji/Doji.Series.cs deleted file mode 100644 index 3fa081375..000000000 --- a/src/a-d/Doji/Doji.Series.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace Skender.Stock.Indicators; - -// DOJI (SERIES) -public static partial class Indicator -{ - /// - /// - internal static List CalcDoji( - this IEnumerable quotes, - double maxPriceChangePercent) - where TQuote : IQuote - { - // check parameter arguments - ValidateDoji(maxPriceChangePercent); - - // initialize - List results = quotes.ToCandleResults(); - maxPriceChangePercent /= 100; - int length = results.Count; - - // roll through candles - for (int i = 0; i < length; i++) - { - CandleResult r = results[i]; - - // check for current signal - if (r.Candle.Open != 0 - && Math.Abs((double)(r.Candle.Close / r.Candle.Open) - 1d) <= maxPriceChangePercent) - { - r.Price = r.Candle.Close; - r.Match = Match.Neutral; - } - } - - return results; - } - - // parameter validation - private static void ValidateDoji( - double maxPriceChangePercent) - { - // check parameter arguments - if (maxPriceChangePercent is < 0 or > 0.5) - { - throw new ArgumentOutOfRangeException(nameof(maxPriceChangePercent), maxPriceChangePercent, - "Maximum Percent Change must be between 0 and 0.5 for Doji (0% to 0.5%)."); - } - } -} diff --git a/src/a-d/Doji/Doji.StaticSeries.cs b/src/a-d/Doji/Doji.StaticSeries.cs new file mode 100644 index 000000000..982d4f3eb --- /dev/null +++ b/src/a-d/Doji/Doji.StaticSeries.cs @@ -0,0 +1,60 @@ +namespace Skender.Stock.Indicators; + +// DOJI (SERIES) + +public static partial class Doji +{ + /// + /// Doji is a single candlestick pattern where open and close price + /// are virtually identical, representing market indecision. + /// + /// Configurable Quote type. + /// See Guide for more information. + /// Historical price quotes. + /// + /// Optional.Maximum absolute percent difference in open and close price. + /// + /// Time series of Doji values. + /// + /// Invalid parameter value provided. + /// + public static IReadOnlyList ToDoji( + this IReadOnlyList quotes, + double maxPriceChangePercent = 0.1) + where TQuote : IQuote + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(quotes); + Validate(maxPriceChangePercent); + + // initialize + int length = quotes.Count; + List results = new(length); + + maxPriceChangePercent /= 100; + + // roll through candles + for (int i = 0; i < length; i++) + { + TQuote q = quotes[i]; + decimal? matchPrice = null; + Match matchType = Match.None; + + // check for current signal + if (q.Open != 0 + && Math.Abs((double)(q.Close / q.Open) - 1d) <= maxPriceChangePercent) + { + matchPrice = q.Close; + matchType = Match.Neutral; + } + + results.Add(new CandleResult( + timestamp: q.Timestamp, + quote: q, + match: matchType, + price: matchPrice)); + } + + return results; + } +} diff --git a/src/a-d/Doji/Doji.Utilities.cs b/src/a-d/Doji/Doji.Utilities.cs new file mode 100644 index 000000000..eb8b3988c --- /dev/null +++ b/src/a-d/Doji/Doji.Utilities.cs @@ -0,0 +1,18 @@ +namespace Skender.Stock.Indicators; + +// DOJI (UTILITIES) + +public static partial class Doji +{ + // parameter validation + internal static void Validate( + double maxPriceChangePercent) + { + // check parameter arguments + if (maxPriceChangePercent is < 0 or > 0.5) + { + throw new ArgumentOutOfRangeException(nameof(maxPriceChangePercent), maxPriceChangePercent, + "Maximum Percent Change must be between 0 and 0.5 for Doji (0% to 0.5%)."); + } + } +} diff --git a/src/a-d/Doji/info.xml b/src/a-d/Doji/info.xml deleted file mode 100644 index c063a5d45..000000000 --- a/src/a-d/Doji/info.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Doji is a single candlestick pattern where open and close price are virtually identical, representing market indecision. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - Optional. Maximum absolute percent difference in open and close price. - Time series of Doji values. - Invalid parameter value provided. - \ No newline at end of file diff --git a/src/a-d/Donchian/Donchian.Api.cs b/src/a-d/Donchian/Donchian.Api.cs deleted file mode 100644 index d303e68f8..000000000 --- a/src/a-d/Donchian/Donchian.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// DONCHIAN CHANNEL (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetDonchian( - this IEnumerable quotes, - int lookbackPeriods = 20) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcDonchian(lookbackPeriods); -} diff --git a/src/a-d/Donchian/Donchian.Models.cs b/src/a-d/Donchian/Donchian.Models.cs index 33594a24e..f20ad4ba5 100644 --- a/src/a-d/Donchian/Donchian.Models.cs +++ b/src/a-d/Donchian/Donchian.Models.cs @@ -1,15 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class DonchianResult : ResultBase -{ - public DonchianResult(DateTime date) - { - Date = date; - } - - public decimal? UpperBand { get; set; } - public decimal? Centerline { get; set; } - public decimal? LowerBand { get; set; } - public decimal? Width { get; set; } -} +public record DonchianResult +( + DateTime Timestamp, + decimal? UpperBand = null, + decimal? Centerline = null, + decimal? LowerBand = null, + decimal? Width = null +) : ISeries; diff --git a/src/a-d/Donchian/Donchian.Series.cs b/src/a-d/Donchian/Donchian.Series.cs deleted file mode 100644 index 4e7a87194..000000000 --- a/src/a-d/Donchian/Donchian.Series.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Skender.Stock.Indicators; - -// DONCHIAN CHANNEL (SERIES) -public static partial class Indicator -{ - internal static List CalcDonchian( - this List quotesList, - int lookbackPeriods) - where TQuote : IQuote - { - // check parameter arguments - ValidateDonchian(lookbackPeriods); - - // initialize - int length = quotesList.Count; - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - TQuote q = quotesList[i]; - - DonchianResult r = new(q.Date); - results.Add(r); - - if (i >= lookbackPeriods) - { - decimal highHigh = 0; - decimal lowLow = decimal.MaxValue; - - // high/low over prior periods - for (int p = i - lookbackPeriods; p < i; p++) - { - TQuote d = quotesList[p]; - - if (d.High > highHigh) - { - highHigh = d.High; - } - - if (d.Low < lowLow) - { - lowLow = d.Low; - } - } - - r.UpperBand = highHigh; - r.LowerBand = lowLow; - r.Centerline = (r.UpperBand + r.LowerBand) / 2m; - r.Width = (r.Centerline == 0) ? null - : (r.UpperBand - r.LowerBand) / r.Centerline; - } - } - - return results; - } - - // parameter validation - private static void ValidateDonchian( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Donchian Channel."); - } - } -} diff --git a/src/a-d/Donchian/Donchian.StaticSeries.cs b/src/a-d/Donchian/Donchian.StaticSeries.cs new file mode 100644 index 000000000..483ac4a34 --- /dev/null +++ b/src/a-d/Donchian/Donchian.StaticSeries.cs @@ -0,0 +1,66 @@ +namespace Skender.Stock.Indicators; + +// DONCHIAN CHANNEL (SERIES) + +public static partial class Donchian +{ + public static IReadOnlyList ToDonchian( + this IReadOnlyList quotes, + int lookbackPeriods = 20) + where TQuote : IQuote + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(quotes); + Validate(lookbackPeriods); + + // initialize + int length = quotes.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + TQuote q = quotes[i]; + + if (i >= lookbackPeriods) + { + decimal highHigh = 0; + decimal lowLow = decimal.MaxValue; + + // high/low over prior periods + for (int p = i - lookbackPeriods; p < i; p++) + { + TQuote d = quotes[p]; + + if (d.High > highHigh) + { + highHigh = d.High; + } + + if (d.Low < lowLow) + { + lowLow = d.Low; + } + } + + decimal u = highHigh; + decimal l = lowLow; + decimal c = (u + l) / 2m; + + results.Add(new DonchianResult( + Timestamp: q.Timestamp, + UpperBand: u, + LowerBand: l, + Centerline: c, + Width: c == 0 ? null : (u - l) / c)); + } + else + { + results.Add(new(q.Timestamp)); + + } + } + + return results; + } +} diff --git a/src/a-d/Donchian/Donchian.Utilities.cs b/src/a-d/Donchian/Donchian.Utilities.cs index d5806064a..5cd392a2e 100644 --- a/src/a-d/Donchian/Donchian.Utilities.cs +++ b/src/a-d/Donchian/Donchian.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// DONCHIAN CHANNEL (UTILITIES) + +public static partial class Donchian { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -19,10 +20,9 @@ public static IEnumerable Condense( } // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -30,4 +30,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Donchian Channel."); + } + } } diff --git a/src/a-d/Dpo/Dpo.Api.cs b/src/a-d/Dpo/Dpo.Api.cs deleted file mode 100644 index 83b245ea6..000000000 --- a/src/a-d/Dpo/Dpo.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// DETRENDED PRICE OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetDpo( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcDpo(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetDpo( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcDpo(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetDpo( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcDpo(lookbackPeriods); -} diff --git a/src/a-d/Dpo/Dpo.Models.cs b/src/a-d/Dpo/Dpo.Models.cs index 0526c7ae6..a6150372e 100644 --- a/src/a-d/Dpo/Dpo.Models.cs +++ b/src/a-d/Dpo/Dpo.Models.cs @@ -1,15 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class DpoResult : ResultBase, IReusableResult +public record DpoResult +( + DateTime Timestamp, + double? Dpo = null, + double? Sma = null + ) : IReusable { - public DpoResult(DateTime date) - { - Date = date; - } - - public double? Sma { get; set; } - public double? Dpo { get; set; } - - double? IReusableResult.Value => Dpo; + public double Value => Dpo.Null2NaN(); } diff --git a/src/a-d/Dpo/Dpo.Series.cs b/src/a-d/Dpo/Dpo.Series.cs deleted file mode 100644 index b63e00ccb..000000000 --- a/src/a-d/Dpo/Dpo.Series.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace Skender.Stock.Indicators; - -// DETRENDED PRICE OSCILLATOR (SERIES) -public static partial class Indicator -{ - // calculate series - internal static List CalcDpo( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateDpo(lookbackPeriods); - - // initialize - int length = tpList.Count; - int offset = (lookbackPeriods / 2) + 1; - List sma = tpList.GetSma(lookbackPeriods).ToList(); - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - DpoResult r = new(date); - results.Add(r); - - if (i >= lookbackPeriods - offset - 1 && i < length - offset) - { - SmaResult s = sma[i + offset]; - r.Sma = s.Sma; - r.Dpo = s.Sma is null ? null : (value - s.Sma).NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidateDpo( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for DPO."); - } - } -} diff --git a/src/a-d/Dpo/Dpo.StaticSeries.cs b/src/a-d/Dpo/Dpo.StaticSeries.cs new file mode 100644 index 000000000..1298baca3 --- /dev/null +++ b/src/a-d/Dpo/Dpo.StaticSeries.cs @@ -0,0 +1,50 @@ +namespace Skender.Stock.Indicators; + +// DETRENDED PRICE OSCILLATOR (SERIES) + +public static partial class Dpo +{ + public static IReadOnlyList ToDpo( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + int offset = (lookbackPeriods / 2) + 1; + + IReadOnlyList sma + = source.ToSma(lookbackPeriods); + + // roll through source values + for (int i = 0; i < length; i++) + { + T src = source[i]; + + double? dpoSma = null; + double? dpoVal = null; + + if (i >= lookbackPeriods - offset - 1 && i < length - offset) + { + SmaResult s = sma[i + offset]; + dpoSma = s.Sma; + dpoVal = s.Sma is null ? null : src.Value - s.Sma; + } + + DpoResult r = new( + Timestamp: src.Timestamp, + Dpo: dpoVal, + Sma: dpoSma); + + results.Add(r); + } + + return results; + } +} diff --git a/src/a-d/Dpo/Dpo.Utilities.cs b/src/a-d/Dpo/Dpo.Utilities.cs new file mode 100644 index 000000000..d6170d70c --- /dev/null +++ b/src/a-d/Dpo/Dpo.Utilities.cs @@ -0,0 +1,19 @@ +namespace Skender.Stock.Indicators; + +// DETRENDED PRICE OSCILLATOR (UTILITIES) + +public static partial class Dpo +{ + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for DPO."); + } + } + +} diff --git a/src/a-d/Dynamic/Dynamic.Api.cs b/src/a-d/Dynamic/Dynamic.Api.cs deleted file mode 100644 index 73ca72dc8..000000000 --- a/src/a-d/Dynamic/Dynamic.Api.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Skender.Stock.Indicators; - -// McGINLEY DYNAMIC -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetDynamic( - this IEnumerable quotes, - int lookbackPeriods, - double kFactor = 0.6) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcDynamic(lookbackPeriods, kFactor); - - // SERIES, from CHAIN - public static IEnumerable GetDynamic( - this IEnumerable results, - int lookbackPeriods, - double kFactor = 0.6) => results - .ToTuple() - .CalcDynamic(lookbackPeriods, kFactor) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetDynamic( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods, - double kFactor = 0.6) => priceTuples - .ToSortedList() - .CalcDynamic(lookbackPeriods, kFactor); -} diff --git a/src/a-d/Dynamic/Dynamic.Models.cs b/src/a-d/Dynamic/Dynamic.Models.cs index 9fd3d6294..f120c7156 100644 --- a/src/a-d/Dynamic/Dynamic.Models.cs +++ b/src/a-d/Dynamic/Dynamic.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class DynamicResult : ResultBase, IReusableResult +public record DynamicResult +( + DateTime Timestamp, + double? Dynamic +) : IReusable { - public DynamicResult(DateTime date) - { - Date = date; - } - - public double? Dynamic { get; set; } - - double? IReusableResult.Value => Dynamic; + public double Value => Dynamic.Null2NaN(); } diff --git a/src/a-d/Dynamic/Dynamic.Series.cs b/src/a-d/Dynamic/Dynamic.Series.cs deleted file mode 100644 index 9d0f47e01..000000000 --- a/src/a-d/Dynamic/Dynamic.Series.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace Skender.Stock.Indicators; - -// MONEY FLOW INDEX (SERIES) -public static partial class Indicator -{ - internal static List CalcDynamic( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double kFactor) - { - // check parameter arguments - ValidateDynamic(lookbackPeriods, kFactor); - - // initialize - int iStart = 1; - int length = tpList.Count; - List results = new(length); - - if (length == 0) - { - return results; - } - - double prevMD = tpList[0].Item2; - - // roll through quotes, to get preliminary data - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - DynamicResult r = new(date); - results.Add(r); - - // re-initialize if value is NaN - if (double.IsNaN(value) || prevMD == 0) - { - prevMD = value; - iStart = i + lookbackPeriods; - } - else - { - double md = prevMD + ((value - prevMD) / - (kFactor * lookbackPeriods * Math.Pow(value / prevMD, 4))); - - if (i >= iStart) - { - r.Dynamic = md.NaN2Null(); - } - - prevMD = md; - } - } - - return results; - } - - // parameter validation - private static void ValidateDynamic( - int lookbackPeriods, - double kFactor) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for DYNAMIC."); - } - - if (kFactor <= 0) - { - throw new ArgumentOutOfRangeException(nameof(kFactor), kFactor, - "K-Factor range adjustment must be greater than 0 for DYNAMIC."); - } - } -} diff --git a/src/a-d/Dynamic/Dynamic.StaticSeries.cs b/src/a-d/Dynamic/Dynamic.StaticSeries.cs new file mode 100644 index 000000000..03c56c40e --- /dev/null +++ b/src/a-d/Dynamic/Dynamic.StaticSeries.cs @@ -0,0 +1,44 @@ +namespace Skender.Stock.Indicators; + +// McGINLEY DYNAMIC (SERIES) + +public static partial class MgDynamic +{ + public static IReadOnlyList ToDynamic( + this IReadOnlyList source, + int lookbackPeriods, + double kFactor = 0.6) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods, kFactor); + + // initialize + int length = source.Count; + List results = new(length); + + // skip first period + if (length > 0) + { + results.Add(new(source[0].Timestamp, null)); + } + + // roll through source values + for (int i = 1; i < length; i++) + { + double? dyn = Increment( + lookbackPeriods, + kFactor, + newVal: source[i].Value, + prevDyn: results[i - 1].Dynamic ?? source[i - 1].Value + ).NaN2Null(); + + results.Add(new DynamicResult( + Timestamp: source[i].Timestamp, + Dynamic: dyn)); + } + + return results; + } +} diff --git a/src/a-d/Dynamic/Dynamic.Utilities.cs b/src/a-d/Dynamic/Dynamic.Utilities.cs new file mode 100644 index 000000000..5228d3c4d --- /dev/null +++ b/src/a-d/Dynamic/Dynamic.Utilities.cs @@ -0,0 +1,36 @@ +namespace Skender.Stock.Indicators; + +// McGINLEY DYNAMIC (UTILITIES) + +public static partial class MgDynamic +{ + // increment calculation + public static double Increment( + int lookbackPeriods, + double kFactor, + double newVal, + double prevDyn) + => prevDyn + ( + (newVal - prevDyn) + / (kFactor * lookbackPeriods * Math.Pow(newVal / prevDyn, 4))); + + // parameter validation + internal static void Validate( + int lookbackPeriods, + double kFactor) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for DYNAMIC."); + } + + if (kFactor <= 0) + { + throw new ArgumentOutOfRangeException(nameof(kFactor), kFactor, + "K-Factor range adjustment must be greater than 0 for DYNAMIC."); + } + } + +} diff --git a/src/e-k/ElderRay/ElderRay.Api.cs b/src/e-k/ElderRay/ElderRay.Api.cs deleted file mode 100644 index 6721637af..000000000 --- a/src/e-k/ElderRay/ElderRay.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ELDER-RAY (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetElderRay( - this IEnumerable quotes, - int lookbackPeriods = 13) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcElderRay(lookbackPeriods); -} diff --git a/src/e-k/ElderRay/ElderRay.Models.cs b/src/e-k/ElderRay/ElderRay.Models.cs index 1ffd29543..275bf9580 100644 --- a/src/e-k/ElderRay/ElderRay.Models.cs +++ b/src/e-k/ElderRay/ElderRay.Models.cs @@ -1,16 +1,13 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class ElderRayResult : ResultBase, IReusableResult +public record ElderRayResult +( + DateTime Timestamp, + double? Ema, + double? BullPower, + double? BearPower +) : IReusable { - public ElderRayResult(DateTime date) - { - Date = date; - } - - public double? Ema { get; set; } - public double? BullPower { get; set; } - public double? BearPower { get; set; } - - double? IReusableResult.Value => BullPower + BearPower; + public double Value => (BullPower + BearPower).Null2NaN(); } diff --git a/src/e-k/ElderRay/ElderRay.Series.cs b/src/e-k/ElderRay/ElderRay.Series.cs deleted file mode 100644 index ba8f366d4..000000000 --- a/src/e-k/ElderRay/ElderRay.Series.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ELDER-RAY (SERIES) -public static partial class Indicator -{ - internal static List CalcElderRay( - this List qdList, - int lookbackPeriods) - { - // check parameter arguments - ValidateElderRay(lookbackPeriods); - - // initialize with EMA - List results = qdList - .ToTuple(CandlePart.Close) - .CalcEma(lookbackPeriods) - .Select(x => new ElderRayResult(x.Date) { - Ema = x.Ema - }) - .ToList(); - - // roll through quotes - for (int i = lookbackPeriods - 1; i < qdList.Count; i++) - { - QuoteD q = qdList[i]; - ElderRayResult r = results[i]; - - r.BullPower = q.High - r.Ema; - r.BearPower = q.Low - r.Ema; - } - - return results; - } - - // parameter validation - private static void ValidateElderRay( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Elder-ray Index."); - } - } -} diff --git a/src/e-k/ElderRay/ElderRay.StaticSeries.cs b/src/e-k/ElderRay/ElderRay.StaticSeries.cs new file mode 100644 index 000000000..785223db6 --- /dev/null +++ b/src/e-k/ElderRay/ElderRay.StaticSeries.cs @@ -0,0 +1,44 @@ +namespace Skender.Stock.Indicators; + +// ELDER-RAY (SERIES) + +public static partial class ElderRay +{ + public static IReadOnlyList ToElderRay( + this IReadOnlyList quotes, + int lookbackPeriods = 13) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcElderRay(lookbackPeriods); + + private static List CalcElderRay( + this IReadOnlyList source, + int lookbackPeriods) + { + // check parameter arguments + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + // EMA + IReadOnlyList emaResults + = source.ToEma(lookbackPeriods); + + // roll through source values + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + EmaResult e = emaResults[i]; + + results.Add(new( + Timestamp: e.Timestamp, + Ema: e.Ema, + BullPower: q.High - e.Ema, + BearPower: q.Low - e.Ema)); + } + + return results; + } +} diff --git a/src/e-k/ElderRay/ElderRay.Utilities.cs b/src/e-k/ElderRay/ElderRay.Utilities.cs index d98fa8fa7..5e789103b 100644 --- a/src/e-k/ElderRay/ElderRay.Utilities.cs +++ b/src/e-k/ElderRay/ElderRay.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// ELDER-RAY (UTILITIES) + +public static partial class ElderRay { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +15,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(n + 100); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Elder-ray Index."); + } + } } diff --git a/src/e-k/Ema/Ema.Api.cs b/src/e-k/Ema/Ema.Api.cs deleted file mode 100644 index 765314080..000000000 --- a/src/e-k/Ema/Ema.Api.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace Skender.Stock.Indicators; - -// EXPONENTIAL MOVING AVERAGE (API) - -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetEma( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcEma(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetEma( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcEma(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetEma( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcEma(lookbackPeriods); - - // OBSERVER, from Quote Provider - /// - /// - public static EmaObserver GetEma( - this QuoteProvider provider, - int lookbackPeriods) - { - UseObserver useObserver = provider - .Use(CandlePart.Close); - - return new(useObserver, lookbackPeriods); - } - - // OBSERVER, from Chain Provider - /// - /// - public static EmaObserver GetEma( - this TupleProvider tupleProvider, - int lookbackPeriods) - => new(tupleProvider, lookbackPeriods); -} diff --git a/src/e-k/Ema/Ema.Increments.cs b/src/e-k/Ema/Ema.Increments.cs new file mode 100644 index 000000000..0df4706d5 --- /dev/null +++ b/src/e-k/Ema/Ema.Increments.cs @@ -0,0 +1,90 @@ +namespace Skender.Stock.Indicators; + +// EXPONENTIAL MOVING AVERAGE (INCREMENTING LIST) + +/// +/// Exponential Moving Average (EMA) +/// from incremental reusable values. +/// +public class EmaList : List, IEma, IAddQuote, IAddReusable +{ + private readonly Queue _buffer; + private double _bufferSum; + + public EmaList(int lookbackPeriods) + { + Ema.Validate(lookbackPeriods); + LookbackPeriods = lookbackPeriods; + K = 2d / (lookbackPeriods + 1); + + _buffer = new(lookbackPeriods); + _bufferSum = 0; + } + + public int LookbackPeriods { get; init; } + public double K { get; init; } + + public void Add(DateTime timestamp, double value) + { + // update buffer + if (_buffer.Count == LookbackPeriods) + { + _bufferSum -= _buffer.Dequeue(); + } + _buffer.Enqueue(value); + _bufferSum += value; + + // add nulls for incalculable periods + if (Count < LookbackPeriods - 1) + { + base.Add(new EmaResult(timestamp)); + return; + } + + // re/initialize as SMA + if (this[^1].Ema is null) + { + base.Add(new EmaResult( + timestamp, + _bufferSum / LookbackPeriods)); + return; + } + + // calculate EMA normally + base.Add(new EmaResult( + timestamp, + Ema.Increment(K, this[^1].Ema, value))); + } + + public void Add(IReusable value) + { + ArgumentNullException.ThrowIfNull(value); + Add(value.Timestamp, value.Value); + } + + public void Add(IReadOnlyList values) + { + ArgumentNullException.ThrowIfNull(values); + + for (int i = 0; i < values.Count; i++) + { + Add(values[i].Timestamp, values[i].Value); + } + } + + public void Add(IQuote quote) + { + ArgumentNullException.ThrowIfNull(quote); + Add(quote.Timestamp, quote.Value); + } + + public void Add(IReadOnlyList quotes) + { + ArgumentNullException.ThrowIfNull(quotes); + + for (int i = 0; i < quotes.Count; i++) + { + Add(quotes[i]); + } + } +} diff --git a/src/e-k/Ema/Ema.Models.cs b/src/e-k/Ema/Ema.Models.cs index 9d94b7d9f..7b5fca5c0 100644 --- a/src/e-k/Ema/Ema.Models.cs +++ b/src/e-k/Ema/Ema.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class EmaResult : ResultBase, IReusableResult +public record EmaResult +( + DateTime Timestamp, + double? Ema = null +) : IReusable { - public EmaResult(DateTime date) - { - Date = date; - } - - public double? Ema { get; set; } - - double? IReusableResult.Value => Ema; + public double Value => Ema.Null2NaN(); } diff --git a/src/e-k/Ema/Ema.Observer.cs b/src/e-k/Ema/Ema.Observer.cs deleted file mode 100644 index 88367dcc9..000000000 --- a/src/e-k/Ema/Ema.Observer.cs +++ /dev/null @@ -1,144 +0,0 @@ -namespace Skender.Stock.Indicators; - -// EXPONENTIAL MOVING AVERAGE (STREAMING) - -public class EmaObserver : ChainProvider -{ - public EmaObserver( - TupleProvider provider, - int lookbackPeriods) - { - Supplier = provider; - ProtectedResults = []; - - LookbackPeriods = lookbackPeriods; - K = 2d / (lookbackPeriods + 1); - - Initialize(); - } - - // PROPERTIES - - public IEnumerable Results => ProtectedResults; - internal List ProtectedResults { get; set; } - - private double WarmupValue { get; set; } - private int LookbackPeriods { get; set; } - private double K { get; set; } - - // STATIC METHODS - - // parameter validation - internal static void Validate( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for EMA."); - } - } - - // incremental calculation - internal static double Increment(double newValue, double lastEma, double k) - => lastEma + (k * (newValue - lastEma)); - - // NON-STATIC METHODS - - // handle quote arrival - public override void OnNext((DateTime Date, double Value) value) => Add(value); - - // add new tuple quote - internal void Add((DateTime Date, double Value) tuple) - { - // candidate result (empty) - EmaResult r = new(tuple.Date); - - // initialize - int length = ProtectedResults.Count; - - if (length == 0) - { - ProtectedResults.Add(r); - WarmupValue += tuple.Value; - SendToChain(r); - return; - } - - // check against last entry - EmaResult last = ProtectedResults[length - 1]; - - // initialization periods - if (length < LookbackPeriods - 1) - { - // add if not duplicate - if (last.Date != r.Date) - { - ProtectedResults.Add(r); - WarmupValue += tuple.Value; - } - - return; - } - - // initialize with SMA - if (length == LookbackPeriods - 1) - { - WarmupValue += tuple.Value; - r.Ema = (WarmupValue / LookbackPeriods).NaN2Null(); - ProtectedResults.Add(r); - SendToChain(r); - return; - } - - // add new - if (r.Date > last.Date) - { - // calculate incremental value - double lastEma = (last.Ema == null) ? double.NaN : (double)last.Ema; - double newEma = Increment(tuple.Value, lastEma, K); - - r.Ema = newEma.NaN2Null(); - ProtectedResults.Add(r); - SendToChain(r); - return; - } - - // update last - else if (r.Date == last.Date) - { - // get prior last EMA - EmaResult prior = ProtectedResults[length - 2]; - - double priorEma = (prior.Ema == null) ? double.NaN : (double)prior.Ema; - last.Ema = Increment(tuple.Value, priorEma, K); - SendToChain(last); - return; - } - - // late arrival - else - { - // heal - throw new NotImplementedException(); - } - } - - // calculate with provider cache - private void Initialize() - { - if (Supplier != null) - { - List<(DateTime, double)> tuples = Supplier - .ProtectedTuples; - - for (int i = 0; i < tuples.Count; i++) - { - Add(tuples[i]); - } - - Subscribe(); - } - } -} diff --git a/src/e-k/Ema/Ema.Series.cs b/src/e-k/Ema/Ema.Series.cs deleted file mode 100644 index 77952750d..000000000 --- a/src/e-k/Ema/Ema.Series.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace Skender.Stock.Indicators; - -// EXPONENTIAL MOVING AVERAGE (SERIES) -public static partial class Indicator -{ - internal static List CalcEma( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - EmaObserver.Validate(lookbackPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - - double lastEma = 0; - double k = 2d / (lookbackPeriods + 1); - int initPeriods = Math.Min(lookbackPeriods, length); - - for (int i = 0; i < initPeriods; i++) - { - (DateTime _, double value) = tpList[i]; - lastEma += value; - } - - lastEma /= lookbackPeriods; - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - EmaResult r = new(date); - results.Add(r); - - if (i + 1 > lookbackPeriods) - { - double ema = EmaObserver.Increment(value, lastEma, k); - r.Ema = ema.NaN2Null(); - lastEma = ema; - } - else if (i == lookbackPeriods - 1) - { - r.Ema = lastEma.NaN2Null(); - } - } - - return results; - } -} diff --git a/src/e-k/Ema/Ema.StaticSeries.cs b/src/e-k/Ema/Ema.StaticSeries.cs new file mode 100644 index 000000000..49ad66637 --- /dev/null +++ b/src/e-k/Ema/Ema.StaticSeries.cs @@ -0,0 +1,52 @@ +namespace Skender.Stock.Indicators; + +// EXPONENTIAL MOVING AVERAGE (SERIES) + +public static partial class Ema +{ + public static IReadOnlyList ToEma( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + EmaResult[] results = new EmaResult[length]; + + double lastEma = double.NaN; + double k = 2d / (lookbackPeriods + 1); + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + // skip incalculable periods + if (i < lookbackPeriods - 1) + { + results[i] = new EmaResult(Timestamp: s.Timestamp); + continue; + } + + double ema = !double.IsNaN(lastEma) + + // calculate EMA (normally) + ? Ema.Increment(k, lastEma, s.Value) + + // when no prior EMA, reset as SMA + : Sma.Increment(source, lookbackPeriods, i); + + results[i] = new EmaResult( + Timestamp: s.Timestamp, + Ema: ema.NaN2Null()); + + lastEma = ema; + } + + return new List(results); + } +} diff --git a/src/e-k/Ema/Ema.StreamHub.cs b/src/e-k/Ema/Ema.StreamHub.cs new file mode 100644 index 000000000..c66697259 --- /dev/null +++ b/src/e-k/Ema/Ema.StreamHub.cs @@ -0,0 +1,76 @@ +namespace Skender.Stock.Indicators; + +// EXPONENTIAL MOVING AVERAGE (STREAM HUB) + +#region hub interface and initializer + +public interface IEma +{ + int LookbackPeriods { get; } + double K { get; } +} + +public static partial class Ema +{ + // HUB, from Chain Provider + public static EmaHub ToEma( + this IChainProvider chainProvider, + int lookbackPeriods) + where T : IReusable + => new(chainProvider, lookbackPeriods); +} +#endregion + +public class EmaHub + : ChainProvider, IEma + where TIn : IReusable +{ + #region constructors + + private readonly string hubName; + + internal EmaHub( + IChainProvider provider, + int lookbackPeriods) : base(provider) + { + Ema.Validate(lookbackPeriods); + LookbackPeriods = lookbackPeriods; + K = 2d / (lookbackPeriods + 1); + hubName = $"EMA({lookbackPeriods})"; + + Reinitialize(); + } + #endregion + + public int LookbackPeriods { get; init; } + public double K { get; init; } + + // METHODS + + public override string ToString() => hubName; + + protected override (EmaResult result, int index) + ToIndicator(TIn item, int? indexHint) + { + int i = indexHint ?? ProviderCache.GetIndex(item, true); + + double ema = i >= LookbackPeriods - 1 + ? Cache[i - 1].Ema is not null + + // normal + ? Ema.Increment(K, Cache[i - 1].Value, item.Value) + + // re/initialize as SMA + : Sma.Increment(ProviderCache, LookbackPeriods, i) + + // warmup periods are never calculable + : double.NaN; + + // candidate result + EmaResult r = new( + Timestamp: item.Timestamp, + Ema: ema.NaN2Null()); + + return (r, i); + } +} diff --git a/src/e-k/Ema/Ema.Utilities.cs b/src/e-k/Ema/Ema.Utilities.cs index 50beab90a..6a8f8f1f0 100644 --- a/src/e-k/Ema/Ema.Utilities.cs +++ b/src/e-k/Ema/Ema.Utilities.cs @@ -1,12 +1,33 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// EXPONENTIAL MOVING AVERAGE (UTILITIES) + +public static partial class Ema { + public static double Increment( + double k, + double lastEma, + double newPrice) + => lastEma + k * (newPrice - lastEma); + + public static double Increment( + int lookbackPeriods, + double lastEma, + double newPrice) + { + double k = 2d / (lookbackPeriods + 1); + return Increment(k, lastEma, newPrice); + } + + public static double? Increment( + double k, + double? lastEma, + double newPrice) + => lastEma + k * (newPrice - lastEma); + // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +35,17 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(n + 100); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for EMA."); + } + } } diff --git a/src/e-k/Ema/info.xml b/src/e-k/Ema/info.xml index 5e3e3be73..a0b526913 100644 --- a/src/e-k/Ema/info.xml +++ b/src/e-k/Ema/info.xml @@ -1,6 +1,7 @@ + Exponential Moving Average (EMA) of price or any other specified OHLCV element. @@ -10,38 +11,10 @@ for more information. - Configurable Quote type. See Guide for more information. + Configurable Quote type. See Guide for more information. Historical price quotes. Number of periods in the lookback window. Time series of EMA values. Invalid parameter value provided. - - - Establish an observable streaming Exponential Moving Average (EMA). - - See - documentation - for more information. - - - Observable quote provider. - Number of periods in the lookback window. - Observable EMA instance. - Invalid parameter value provided. - - - - Chain from an observable streaming Exponential Moving Average (EMA). - - See - documentation - for more information. - - - Observable from chained indicator. - Number of periods in the lookback window. - Observable EMA instance. - Invalid parameter value provided. - - \ No newline at end of file + diff --git a/src/e-k/Epma/Epma.Api.cs b/src/e-k/Epma/Epma.Api.cs deleted file mode 100644 index b999b8a4f..000000000 --- a/src/e-k/Epma/Epma.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ENDPOINT MOVING AVERAGE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetEpma( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcEpma(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetEpma( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcEpma(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetEpma( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcEpma(lookbackPeriods); -} diff --git a/src/e-k/Epma/Epma.Models.cs b/src/e-k/Epma/Epma.Models.cs index eeabaabf6..afd3c6e3c 100644 --- a/src/e-k/Epma/Epma.Models.cs +++ b/src/e-k/Epma/Epma.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class EpmaResult : ResultBase, IReusableResult +public record EpmaResult +( + DateTime Timestamp, + double? Epma +) : IReusable { - public EpmaResult(DateTime date) - { - Date = date; - } - - public double? Epma { get; set; } - - double? IReusableResult.Value => Epma; + public double Value => Epma.Null2NaN(); } diff --git a/src/e-k/Epma/Epma.Series.cs b/src/e-k/Epma/Epma.Series.cs deleted file mode 100644 index 76269778c..000000000 --- a/src/e-k/Epma/Epma.Series.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ENDPOINT MOVING AVERAGE (SERIES) -public static partial class Indicator -{ - // calculate series - internal static List CalcEpma( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateEpma(lookbackPeriods); - - // initialize - List slopeResults = tpList - .CalcSlope(lookbackPeriods) - .ToList(); - - int length = slopeResults.Count; - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - SlopeResult s = slopeResults[i]; - - EpmaResult r = new(s.Date) { - Epma = ((s.Slope * (i + 1)) + s.Intercept).NaN2Null() - }; - - results.Add(r); - } - - return results; - } - - // parameter validation - private static void ValidateEpma( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Epma."); - } - } -} diff --git a/src/e-k/Epma/Epma.StaticSeries.cs b/src/e-k/Epma/Epma.StaticSeries.cs new file mode 100644 index 000000000..131d601be --- /dev/null +++ b/src/e-k/Epma/Epma.StaticSeries.cs @@ -0,0 +1,33 @@ +namespace Skender.Stock.Indicators; + +// ENDPOINT MOVING AVERAGE (SERIES) + +public static partial class Epma +{ + public static IReadOnlyList ToEpma( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + IReadOnlyList slope + = source.ToSlope(lookbackPeriods); + + // roll through source values + for (int i = 0; i < length; i++) + { + results.Add(new EpmaResult( + Timestamp: slope[i].Timestamp, + Epma: ((slope[i].Slope * (i + 1)) + slope[i].Intercept).NaN2Null())); + } + + return results; + } +} diff --git a/src/e-k/Epma/Epma.Utilities.cs b/src/e-k/Epma/Epma.Utilities.cs index 76b47b966..49bfc8370 100644 --- a/src/e-k/Epma/Epma.Utilities.cs +++ b/src/e-k/Epma/Epma.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// ENDPOINT MOVING AVERAGE (UTILITIES) + +public static partial class Epma { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -14,4 +15,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Epma."); + } + } } diff --git a/src/e-k/Fcb/Fcb.Api.cs b/src/e-k/Fcb/Fcb.Api.cs deleted file mode 100644 index ca60aa0a3..000000000 --- a/src/e-k/Fcb/Fcb.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// FRACTAL CHAOS BANDS (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetFcb( - this IEnumerable quotes, - int windowSpan = 2) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcFcb(windowSpan); -} diff --git a/src/e-k/Fcb/Fcb.Models.cs b/src/e-k/Fcb/Fcb.Models.cs index ba4c20d4f..6dead53fd 100644 --- a/src/e-k/Fcb/Fcb.Models.cs +++ b/src/e-k/Fcb/Fcb.Models.cs @@ -1,13 +1,9 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class FcbResult : ResultBase -{ - public FcbResult(DateTime date) - { - Date = date; - } - - public decimal? UpperBand { get; set; } - public decimal? LowerBand { get; set; } -} +public record FcbResult +( + DateTime Timestamp, + decimal? UpperBand, + decimal? LowerBand +) : ISeries; diff --git a/src/e-k/Fcb/Fcb.Series.cs b/src/e-k/Fcb/Fcb.Series.cs deleted file mode 100644 index 6689bc1d7..000000000 --- a/src/e-k/Fcb/Fcb.Series.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Skender.Stock.Indicators; - -// FRACTAL CHAOS BANDS (SERIES) -public static partial class Indicator -{ - internal static List CalcFcb( - this List quotesList, - int windowSpan) - where TQuote : IQuote - { - // check parameter arguments - ValidateFcb(windowSpan); - - // initialize - List fractals = quotesList - .CalcFractal(windowSpan, windowSpan, EndType.HighLow) - .ToList(); - - int length = fractals.Count; - List results = new(length); - decimal? upperLine = null; - decimal? lowerLine = null; - - // roll through quotes - for (int i = 0; i < length; i++) - { - FractalResult f = fractals[i]; - - FcbResult r = new(f.Date); - results.Add(r); - - if (i >= 2 * windowSpan) - { - FractalResult fp = fractals[i - windowSpan]; - - upperLine = fp.FractalBear ?? upperLine; - lowerLine = fp.FractalBull ?? lowerLine; - - r.UpperBand = upperLine; - r.LowerBand = lowerLine; - } - } - - return results; - } - - // parameter validation - private static void ValidateFcb( - int windowSpan) - { - // check parameter arguments - if (windowSpan < 2) - { - throw new ArgumentOutOfRangeException(nameof(windowSpan), windowSpan, - "Window span must be at least 2 for FCB."); - } - } -} diff --git a/src/e-k/Fcb/Fcb.StaticSeries.cs b/src/e-k/Fcb/Fcb.StaticSeries.cs new file mode 100644 index 000000000..36ca6553e --- /dev/null +++ b/src/e-k/Fcb/Fcb.StaticSeries.cs @@ -0,0 +1,47 @@ +namespace Skender.Stock.Indicators; + +// FRACTAL CHAOS BANDS (SERIES) + +public static partial class Fcb +{ + public static IReadOnlyList ToFcb( + this IReadOnlyList quotes, + int windowSpan = 2) + where TQuote : IQuote + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(quotes); + Validate(windowSpan); + + // initialize + int length = quotes.Count; + List results = new(length); + + IReadOnlyList fractals = quotes + .ToFractal(windowSpan, windowSpan, EndType.HighLow); + + decimal? upperLine = null; + decimal? lowerLine = null; + + // roll through source values + for (int i = 0; i < length; i++) + { + FractalResult f = fractals[i]; + + if (i >= 2 * windowSpan) + { + FractalResult fp = fractals[i - windowSpan]; + + upperLine = fp.FractalBear ?? upperLine; + lowerLine = fp.FractalBull ?? lowerLine; + } + + results.Add(new( + Timestamp: f.Timestamp, + UpperBand: upperLine, + LowerBand: lowerLine)); + } + + return results; + } +} diff --git a/src/e-k/Fcb/Fcb.Utilities.cs b/src/e-k/Fcb/Fcb.Utilities.cs index 386420a5f..6a3dc52b4 100644 --- a/src/e-k/Fcb/Fcb.Utilities.cs +++ b/src/e-k/Fcb/Fcb.Utilities.cs @@ -1,12 +1,12 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// FRACTAL CHAOS BANDS (UTILITIES) + +public static partial class Fcb { - // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -18,11 +18,9 @@ public static IEnumerable Condense( return resultsList.ToSortedList(); } - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -30,4 +28,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int windowSpan) + { + // check parameter arguments + if (windowSpan < 2) + { + throw new ArgumentOutOfRangeException(nameof(windowSpan), windowSpan, + "Window span must be at least 2 for FCB."); + } + } } diff --git a/src/e-k/FisherTransform/FisherTransform.Api.cs b/src/e-k/FisherTransform/FisherTransform.Api.cs deleted file mode 100644 index 34c2db32a..000000000 --- a/src/e-k/FisherTransform/FisherTransform.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// FISHER TRANSFORM (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetFisherTransform( - this IEnumerable quotes, - int lookbackPeriods = 10) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.HL2) - .CalcFisherTransform(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetFisherTransform( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcFisherTransform(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetFisherTransform( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcFisherTransform(lookbackPeriods); -} diff --git a/src/e-k/FisherTransform/FisherTransform.Models.cs b/src/e-k/FisherTransform/FisherTransform.Models.cs index 379d958be..a02dc5ea3 100644 --- a/src/e-k/FisherTransform/FisherTransform.Models.cs +++ b/src/e-k/FisherTransform/FisherTransform.Models.cs @@ -1,15 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class FisherTransformResult : ResultBase, IReusableResult +public record FisherTransformResult +( + DateTime Timestamp, + double? Fisher, + double? Trigger +) : IReusable { - public FisherTransformResult(DateTime date) - { - Date = date; - } - - public double? Fisher { get; set; } - public double? Trigger { get; set; } - - double? IReusableResult.Value => Fisher; + public double Value => Fisher.Null2NaN(); } diff --git a/src/e-k/FisherTransform/FisherTransform.Series.cs b/src/e-k/FisherTransform/FisherTransform.Series.cs deleted file mode 100644 index c7862ba9d..000000000 --- a/src/e-k/FisherTransform/FisherTransform.Series.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace Skender.Stock.Indicators; - -// FISHER TRANSFORM (SERIES) -public static partial class Indicator -{ - internal static List CalcFisherTransform( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateFisherTransform(lookbackPeriods); - - // initialize - int length = tpList.Count; - double[] pr = new double[length]; // median price - double[] xv = new double[length]; // price transform "value" - List results = new(length); - - // roll through quotes - for (int i = 0; i < tpList.Count; i++) - { - (DateTime date, double value) = tpList[i]; - pr[i] = value; - - double minPrice = pr[i]; - double maxPrice = pr[i]; - - for (int p = Math.Max(i - lookbackPeriods + 1, 0); p <= i; p++) - { - minPrice = Math.Min(pr[p], minPrice); - maxPrice = Math.Max(pr[p], maxPrice); - } - - FisherTransformResult r = new(date); - results.Add(r); - - if (i > 0) - { - xv[i] = maxPrice != minPrice - ? (0.33 * 2 * (((pr[i] - minPrice) / (maxPrice - minPrice)) - 0.5)) - + (0.67 * xv[i - 1]) - : 0; - - xv[i] = (xv[i] > 0.99) ? 0.999 : xv[i]; - xv[i] = (xv[i] < -0.99) ? -0.999 : xv[i]; - - r.Fisher = ((0.5 * Math.Log((1 + xv[i]) / (1 - xv[i]))) - + (0.5 * results[i - 1].Fisher)).NaN2Null(); - - r.Trigger = results[i - 1].Fisher; - } - else - { - xv[i] = 0; - r.Fisher = 0; - } - } - - return results; - } - - // parameter validation - private static void ValidateFisherTransform( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Fisher Transform."); - } - } -} diff --git a/src/e-k/FisherTransform/FisherTransform.StaticSeries.cs b/src/e-k/FisherTransform/FisherTransform.StaticSeries.cs new file mode 100644 index 000000000..a9f542ba8 --- /dev/null +++ b/src/e-k/FisherTransform/FisherTransform.StaticSeries.cs @@ -0,0 +1,72 @@ +namespace Skender.Stock.Indicators; + +// FISHER TRANSFORM (SERIES) + +public static partial class FisherTransform +{ + public static IReadOnlyList ToFisherTransform( + this IReadOnlyList source, + int lookbackPeriods = 10) + where T : IReusable + { + // check parameter arguments + Validate(lookbackPeriods); + + // prefer HL2 when IQuote + IReadOnlyList values + = source.ToPreferredList(CandlePart.HL2); + + // initialize + int length = values.Count; + double[] pr = new double[length]; // median price + double[] xv = new double[length]; // price transform "value" + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + IReusable s = values[i]; + pr[i] = s.Value; + + double minPrice = pr[i]; + double maxPrice = pr[i]; + + for (int p = Math.Max(i - lookbackPeriods + 1, 0); p <= i; p++) + { + minPrice = Math.Min(pr[p], minPrice); + maxPrice = Math.Max(pr[p], maxPrice); + } + + double? fisher; + double? trigger = null; + + if (i > 0) + { + xv[i] = maxPrice - minPrice != 0 + ? (0.33 * 2 * (((pr[i] - minPrice) / (maxPrice - minPrice)) - 0.5)) + + (0.67 * xv[i - 1]) + : 0; + + xv[i] = xv[i] > 0.99 ? 0.999 : xv[i]; + xv[i] = xv[i] < -0.99 ? -0.999 : xv[i]; + + fisher = ((0.5 * Math.Log((1 + xv[i]) / (1 - xv[i]))) + + (0.5 * results[i - 1].Fisher)).NaN2Null(); + + trigger = results[i - 1].Fisher; + } + else + { + xv[i] = 0; + fisher = 0; + } + + results.Add(new( + Timestamp: s.Timestamp, + Trigger: trigger, + Fisher: fisher)); + } + + return results; + } +} diff --git a/src/e-k/FisherTransform/FisherTransform.Utilities.cs b/src/e-k/FisherTransform/FisherTransform.Utilities.cs new file mode 100644 index 000000000..8d192b5db --- /dev/null +++ b/src/e-k/FisherTransform/FisherTransform.Utilities.cs @@ -0,0 +1,18 @@ +namespace Skender.Stock.Indicators; + +// FISHER TRANSFORM (UTILITIES) + +public static partial class FisherTransform +{ + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Fisher Transform."); + } + } +} diff --git a/src/e-k/ForceIndex/ForceIndex.Api.cs b/src/e-k/ForceIndex/ForceIndex.Api.cs deleted file mode 100644 index 9d5e0c7e4..000000000 --- a/src/e-k/ForceIndex/ForceIndex.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// FORCE INDEX (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetForceIndex( - this IEnumerable quotes, - int lookbackPeriods = 2) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcForceIndex(lookbackPeriods); -} diff --git a/src/e-k/ForceIndex/ForceIndex.Models.cs b/src/e-k/ForceIndex/ForceIndex.Models.cs index 06f061c4a..0a02fcac7 100644 --- a/src/e-k/ForceIndex/ForceIndex.Models.cs +++ b/src/e-k/ForceIndex/ForceIndex.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class ForceIndexResult : ResultBase, IReusableResult +public record ForceIndexResult +( + DateTime Timestamp, + double? ForceIndex = null +) : IReusable { - public ForceIndexResult(DateTime date) - { - Date = date; - } - - public double? ForceIndex { get; set; } - - double? IReusableResult.Value => ForceIndex; + public double Value => ForceIndex.Null2NaN(); } diff --git a/src/e-k/ForceIndex/ForceIndex.Series.cs b/src/e-k/ForceIndex/ForceIndex.Series.cs deleted file mode 100644 index fa1c23515..000000000 --- a/src/e-k/ForceIndex/ForceIndex.Series.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace Skender.Stock.Indicators; - -// FORCE INDEX (SERIES) -public static partial class Indicator -{ - internal static List CalcForceIndex( - this List qdList, - int lookbackPeriods) - { - // check parameter arguments - ValidateForceIndex(lookbackPeriods); - - // initialize - int length = qdList.Count; - List results = new(length); - double? prevClose = null; - double? prevFI = null; - double? sumRawFI = 0; - double k = 2d / (lookbackPeriods + 1); - - // roll through quotes - for (int i = 0; i < length; i++) - { - QuoteD q = qdList[i]; - - ForceIndexResult r = new(q.Date); - results.Add(r); - - // skip first period - if (i == 0) - { - prevClose = q.Close; - continue; - } - - // raw Force Index - double? rawFI = q.Volume * (q.Close - prevClose); - prevClose = q.Close; - - // calculate EMA - if (i > lookbackPeriods) - { - r.ForceIndex = prevFI + (k * (rawFI - prevFI)); - } - - // initialization period - else - { - sumRawFI += rawFI; - - // first EMA value - if (i == lookbackPeriods) - { - r.ForceIndex = sumRawFI / lookbackPeriods; - } - } - - prevFI = r.ForceIndex; - } - - return results; - } - - // parameter validation - private static void ValidateForceIndex( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Force Index."); - } - } -} diff --git a/src/e-k/ForceIndex/ForceIndex.StaticSeries.cs b/src/e-k/ForceIndex/ForceIndex.StaticSeries.cs new file mode 100644 index 000000000..1bea16987 --- /dev/null +++ b/src/e-k/ForceIndex/ForceIndex.StaticSeries.cs @@ -0,0 +1,71 @@ +namespace Skender.Stock.Indicators; + +// FORCE INDEX (SERIES) + +public static partial class ForceIndex +{ + public static IReadOnlyList ToForceIndex( + this IReadOnlyList quotes, + int lookbackPeriods = 2) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcForceIndex(lookbackPeriods); + + private static List CalcForceIndex( + this IReadOnlyList source, + int lookbackPeriods) + { + // check parameter arguments + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + double? prevFi = null; + double? sumRawFi = 0; + double k = 2d / (lookbackPeriods + 1); + + // skip first period + if (length > 0) + { + results.Add(new(source[0].Timestamp)); + } + + // roll through source values + for (int i = 1; i < length; i++) + { + QuoteD q = source[i]; + double? fi = null; + + // raw Force Index + double? rawFi = q.Volume * (q.Close - source[i - 1].Close); + + // calculate EMA + if (i > lookbackPeriods) + { + fi = prevFi + (k * (rawFi - prevFi)); + } + + // initialization period + // TODO: update healing, without requiring specific indexing + else + { + sumRawFi += rawFi; + + // first EMA value + if (i == lookbackPeriods) + { + fi = sumRawFi / lookbackPeriods; + } + } + + results.Add(new ForceIndexResult( + Timestamp: q.Timestamp, + ForceIndex: fi)); + + prevFi = fi; + } + + return results; + } +} diff --git a/src/e-k/ForceIndex/ForceIndex.Utilities.cs b/src/e-k/ForceIndex/ForceIndex.Utilities.cs index b5143c529..025c244ca 100644 --- a/src/e-k/ForceIndex/ForceIndex.Utilities.cs +++ b/src/e-k/ForceIndex/ForceIndex.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// FORCE INDEX (UTILITIES) + +public static partial class ForceIndex { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +15,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(n + 100); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Force Index."); + } + } } diff --git a/src/e-k/Fractal/Fractal.Api.cs b/src/e-k/Fractal/Fractal.Api.cs deleted file mode 100644 index 169e06146..000000000 --- a/src/e-k/Fractal/Fractal.Api.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Skender.Stock.Indicators; - -// WILLIAMS FRACTAL (API) -public static partial class Indicator -{ - /// - /// - public static IEnumerable GetFractal( - this IEnumerable quotes, - int windowSpan = 2, - EndType endType = EndType.HighLow) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcFractal(windowSpan, windowSpan, endType); - - // more configurable version (undocumented) - /// - /// - public static IEnumerable GetFractal( - this IEnumerable quotes, - int leftSpan, - int rightSpan, - EndType endType = EndType.HighLow) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcFractal(leftSpan, rightSpan, endType); -} diff --git a/src/e-k/Fractal/Fractal.Models.cs b/src/e-k/Fractal/Fractal.Models.cs index 406feb5e6..23fb6e239 100644 --- a/src/e-k/Fractal/Fractal.Models.cs +++ b/src/e-k/Fractal/Fractal.Models.cs @@ -1,13 +1,9 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class FractalResult : ResultBase -{ - public FractalResult(DateTime date) - { - Date = date; - } - - public decimal? FractalBear { get; set; } - public decimal? FractalBull { get; set; } -} +public record FractalResult +( + DateTime Timestamp, + decimal? FractalBear, + decimal? FractalBull +) : ISeries; diff --git a/src/e-k/Fractal/Fractal.Series.cs b/src/e-k/Fractal/Fractal.Series.cs deleted file mode 100644 index 66abf1e3c..000000000 --- a/src/e-k/Fractal/Fractal.Series.cs +++ /dev/null @@ -1,95 +0,0 @@ -namespace Skender.Stock.Indicators; - -// WILLIAMS FRACTAL (SERIES) -public static partial class Indicator -{ - internal static List CalcFractal( - this List quotesList, - int leftSpan, - int rightSpan, - EndType endType) - where TQuote : IQuote - { - // check parameter arguments - ValidateFractal(Math.Min(leftSpan, rightSpan)); - - // initialize - List results = new(quotesList.Count); - - // roll through quotes - for (int i = 0; i < quotesList.Count; i++) - { - TQuote q = quotesList[i]; - - FractalResult r = new(q.Date); - results.Add(r); - - if (i + 1 > leftSpan && i + 1 <= quotesList.Count - rightSpan) - { - bool isHigh = true; - bool isLow = true; - - decimal evalHigh = (endType == EndType.Close) ? - q.Close : q.High; - - decimal evalLow = (endType == EndType.Close) ? - q.Close : q.Low; - - // compare today with wings - for (int p = i - leftSpan; p <= i + rightSpan; p++) - { - // skip center eval period - if (p == i) - { - continue; - } - - // evaluate wing periods - TQuote wing = quotesList[p]; - - decimal wingHigh = (endType == EndType.Close) ? - wing.Close : wing.High; - - decimal wingLow = (endType == EndType.Close) ? - wing.Close : wing.Low; - - if (evalHigh <= wingHigh) - { - isHigh = false; - } - - if (evalLow >= wingLow) - { - isLow = false; - } - } - - // bearish signal - if (isHigh) - { - r.FractalBear = evalHigh; - } - - // bullish signal - if (isLow) - { - r.FractalBull = evalLow; - } - } - } - - return results; - } - - // parameter validation - private static void ValidateFractal( - int windowSpan) - { - // check parameter arguments - if (windowSpan < 2) - { - throw new ArgumentOutOfRangeException(nameof(windowSpan), windowSpan, - "Window span must be at least 2 for Fractal."); - } - } -} diff --git a/src/e-k/Fractal/Fractal.StaticSeries.cs b/src/e-k/Fractal/Fractal.StaticSeries.cs new file mode 100644 index 000000000..2dd3b1615 --- /dev/null +++ b/src/e-k/Fractal/Fractal.StaticSeries.cs @@ -0,0 +1,97 @@ +namespace Skender.Stock.Indicators; + +// WILLIAMS FRACTAL (SERIES) + +public static partial class Fractal +{ + public static IReadOnlyList ToFractal( + this IReadOnlyList quotes, + int windowSpan = 2, + EndType endType = EndType.HighLow) + where TQuote : IQuote => quotes + .ToFractal(windowSpan, windowSpan, endType); + + public static IReadOnlyList ToFractal( + this IReadOnlyList quotes, + int leftSpan, + int rightSpan, + EndType endType = EndType.HighLow) + where TQuote : IQuote + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(quotes); + Validate(Math.Min(leftSpan, rightSpan)); + + // initialize + int length = quotes.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + TQuote q = quotes[i]; + decimal? fractalBear = null; + decimal? fractalBull = null; + + if (i + 1 > leftSpan && i + 1 <= length - rightSpan) + { + bool isHigh = true; + bool isLow = true; + + decimal evalHigh = endType == EndType.Close ? + q.Close : q.High; + + decimal evalLow = endType == EndType.Close ? + q.Close : q.Low; + + // compare today with wings + for (int p = i - leftSpan; p <= i + rightSpan; p++) + { + // skip center eval period + if (p == i) + { + continue; + } + + // evaluate wing periods + TQuote wing = quotes[p]; + + decimal wingHigh = endType == EndType.Close ? + wing.Close : wing.High; + + decimal wingLow = endType == EndType.Close ? + wing.Close : wing.Low; + + if (evalHigh <= wingHigh) + { + isHigh = false; + } + + if (evalLow >= wingLow) + { + isLow = false; + } + } + + // bearish signal + if (isHigh) + { + fractalBear = evalHigh; + } + + // bullish signal + if (isLow) + { + fractalBull = evalLow; + } + } + + results.Add(new( + Timestamp: q.Timestamp, + FractalBear: fractalBear, + FractalBull: fractalBull)); + } + + return results; + } +} diff --git a/src/e-k/Fractal/Fractal.Utilities.cs b/src/e-k/Fractal/Fractal.Utilities.cs index b7146d215..9e36b3105 100644 --- a/src/e-k/Fractal/Fractal.Utilities.cs +++ b/src/e-k/Fractal/Fractal.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// WILLIAMS FRACTAL (UTILITIES) + +public static partial class Fractal { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -17,4 +18,16 @@ public static IEnumerable Condense( return resultsList.ToSortedList(); } + + // parameter validation + internal static void Validate( + int windowSpan) + { + // check parameter arguments + if (windowSpan < 2) + { + throw new ArgumentOutOfRangeException(nameof(windowSpan), windowSpan, + "Window span must be at least 2 for Fractal."); + } + } } diff --git a/src/e-k/Gator/Gator.Api.cs b/src/e-k/Gator/Gator.Api.cs deleted file mode 100644 index a45991cdc..000000000 --- a/src/e-k/Gator/Gator.Api.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Skender.Stock.Indicators; - -// GATOR OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetGator( - this IEnumerable quotes) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.HL2) - .GetAlligator() - .ToList() - .CalcGator(); - - // SERIES, from [custom] Alligator - public static IEnumerable GetGator( - this IEnumerable alligator) => alligator - .ToList() - .CalcGator(); - - // SERIES, from CHAIN - public static IEnumerable GetGator( - this IEnumerable results) => results - .ToTuple() - .GetAlligator() - .ToList() - .CalcGator() - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetGator( - this IEnumerable<(DateTime, double)> priceTuples) => priceTuples - .ToSortedList() - .GetAlligator() - .ToList() - .CalcGator(); -} diff --git a/src/e-k/Gator/Gator.Models.cs b/src/e-k/Gator/Gator.Models.cs index 3cf27e8eb..8e7311cf3 100644 --- a/src/e-k/Gator/Gator.Models.cs +++ b/src/e-k/Gator/Gator.Models.cs @@ -1,16 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public class GatorResult : ResultBase -{ - public GatorResult(DateTime date) - { - Date = date; - } - - public double? Upper { get; set; } - public double? Lower { get; set; } - - public bool? UpperIsExpanding { get; set; } - public bool? LowerIsExpanding { get; set; } -} +public record GatorResult +( + DateTime Timestamp, + double? Upper = null, + double? Lower = null, + bool? UpperIsExpanding = null, + bool? LowerIsExpanding = null +) : ISeries; diff --git a/src/e-k/Gator/Gator.Series.cs b/src/e-k/Gator/Gator.Series.cs deleted file mode 100644 index 2c308f322..000000000 --- a/src/e-k/Gator/Gator.Series.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Skender.Stock.Indicators; - -// GATOR OSCILLATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcGator( - this List alligator) - { - List results = alligator - .Select(x => new GatorResult(x.Date) { - Upper = NullMath.Abs(x.Jaw - x.Teeth), - Lower = -NullMath.Abs(x.Teeth - x.Lips) - }) - .ToList(); - - // roll through quotes - for (int i = 1; i < results.Count; i++) - { - GatorResult r = results[i]; - GatorResult p = results[i - 1]; - - // directional information - r.UpperIsExpanding = p.Upper is not null ? (r.Upper > p.Upper) : null; - r.LowerIsExpanding = p.Lower is not null ? (r.Lower < p.Lower) : null; - } - - return results; - } -} diff --git a/src/e-k/Gator/Gator.StaticSeries.cs b/src/e-k/Gator/Gator.StaticSeries.cs new file mode 100644 index 000000000..66fe5353b --- /dev/null +++ b/src/e-k/Gator/Gator.StaticSeries.cs @@ -0,0 +1,68 @@ +namespace Skender.Stock.Indicators; + +// GATOR OSCILLATOR (SERIES) + +public static partial class Gator +{ + /// + /// Gator Oscillator is an expanded view of Williams Alligator. + /// + /// + /// + /// + /// T must be or type + /// + /// Time-series values to transform. + /// Time series of Gator values. + public static IReadOnlyList ToGator( + this IReadOnlyList source) + where T : IReusable + => source + .ToAlligator() + .ToGator(); + + // from [custom] Alligator + public static IReadOnlyList ToGator( + this IReadOnlyList alligator) + { + ArgumentNullException.ThrowIfNull(alligator); + + // initialize + int length = alligator.Count; + List results = []; + + if (length > 0) + { + results.Add(new(alligator[0].Timestamp)); + } + + // roll through source values + for (int i = 1; i < length; i++) + { + AlligatorResult a = alligator[i]; + GatorResult p = results[i - 1]; + + double? upper = (a.Jaw - a.Teeth).Abs(); + double? lower = -(a.Teeth - a.Lips).Abs(); + + results.Add(new GatorResult( + + Timestamp: a.Timestamp, + + // gator + Upper: upper, + Lower: lower, + + // directional information + UpperIsExpanding: p.Upper is not null + ? upper > p.Upper + : null, + + LowerIsExpanding: p.Lower is not null + ? lower < p.Lower + : null)); + } + + return results; + } +} diff --git a/src/e-k/Gator/Gator.Utilities.cs b/src/e-k/Gator/Gator.Utilities.cs index a9ea87295..a9b97545a 100644 --- a/src/e-k/Gator/Gator.Utilities.cs +++ b/src/e-k/Gator/Gator.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// GATOR OSCILLATOR (UTILITIES) + +public static partial class Gator { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -19,8 +20,7 @@ public static IEnumerable Condense( } // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) => results.Remove(150); + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) => results.Remove(150); } diff --git a/src/e-k/Gator/info.xml b/src/e-k/Gator/info.xml deleted file mode 100644 index 19f3416d9..000000000 --- a/src/e-k/Gator/info.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Gator Oscillator is an expanded view of Williams Alligator. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - Time series of Gator values. - \ No newline at end of file diff --git a/src/e-k/HeikinAshi/HeikinAshi.Api.cs b/src/e-k/HeikinAshi/HeikinAshi.Api.cs deleted file mode 100644 index 8a0efd725..000000000 --- a/src/e-k/HeikinAshi/HeikinAshi.Api.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Skender.Stock.Indicators; - -// HEIKIN-ASHI (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetHeikinAshi( - this IEnumerable quotes) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcHeikinAshi(); -} diff --git a/src/e-k/HeikinAshi/HeikinAshi.Models.cs b/src/e-k/HeikinAshi/HeikinAshi.Models.cs index 4cb917ac5..13e1d047a 100644 --- a/src/e-k/HeikinAshi/HeikinAshi.Models.cs +++ b/src/e-k/HeikinAshi/HeikinAshi.Models.cs @@ -1,16 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class HeikinAshiResult : ResultBase, IQuote -{ - public HeikinAshiResult(DateTime date) - { - Date = date; - } - - 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 record HeikinAshiResult( + DateTime Timestamp, + decimal Open, + decimal High, + decimal Low, + decimal Close, + decimal Volume +) : Quote(Timestamp, Open, High, Low, Close, Volume); diff --git a/src/e-k/HeikinAshi/HeikinAshi.Series.cs b/src/e-k/HeikinAshi/HeikinAshi.StaticSeries.cs similarity index 62% rename from src/e-k/HeikinAshi/HeikinAshi.Series.cs rename to src/e-k/HeikinAshi/HeikinAshi.StaticSeries.cs index ecc83d4eb..946cb94eb 100644 --- a/src/e-k/HeikinAshi/HeikinAshi.Series.cs +++ b/src/e-k/HeikinAshi/HeikinAshi.StaticSeries.cs @@ -1,14 +1,17 @@ namespace Skender.Stock.Indicators; // HEIKIN-ASHI (SERIES) -public static partial class Indicator + +public static class HeikinAshi { - internal static List CalcHeikinAshi( - this List quotesList) + public static IReadOnlyList ToHeikinAshi( + this IReadOnlyList quotes) where TQuote : IQuote { + ArgumentNullException.ThrowIfNull(quotes); + // initialize - int length = quotesList.Count; + int length = quotes.Count; List results = new(length); decimal prevOpen = decimal.MinValue; @@ -16,15 +19,15 @@ internal static List CalcHeikinAshi( if (length > 0) { - TQuote q = quotesList[0]; + TQuote q = quotes[0]; prevOpen = q.Open; prevClose = q.Close; } - // roll through quotes + // roll through source values for (int i = 0; i < length; i++) { - TQuote q = quotesList[i]; + TQuote q = quotes[i]; // close decimal close = (q.Open + q.High + q.Low + q.Close) / 4; @@ -40,14 +43,13 @@ internal static List CalcHeikinAshi( decimal[] arrL = [q.Low, open, close]; decimal low = arrL.Min(); - HeikinAshiResult r = new(q.Date) { - Open = open, - High = high, - Low = low, - Close = close, - Volume = q.Volume - }; - results.Add(r); + results.Add(new HeikinAshiResult( + Timestamp: q.Timestamp, + Open: open, + High: high, + Low: low, + Close: close, + Volume: q.Volume)); // save for next iteration prevOpen = open; diff --git a/src/e-k/HeikinAshi/HeikinAshi.Utilities.cs b/src/e-k/HeikinAshi/HeikinAshi.Utilities.cs deleted file mode 100644 index 096e44982..000000000 --- a/src/e-k/HeikinAshi/HeikinAshi.Utilities.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Skender.Stock.Indicators; - -public static partial class Indicator -{ - // convert results to quotes - public static IEnumerable ToQuotes( - this IEnumerable results) - => results - .Select(x => new Quote { - Date = x.Date, - Open = x.Open, - High = x.High, - Low = x.Low, - Close = x.Close, - Volume = x.Volume - }) - .OrderBy(x => x.Date) - .ToList(); -} diff --git a/src/e-k/Hma/Hma.Api.cs b/src/e-k/Hma/Hma.Api.cs deleted file mode 100644 index c60107ee4..000000000 --- a/src/e-k/Hma/Hma.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// HULL MOVING AVERAGE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetHma( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcHma(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetHma( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcHma(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetHma( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcHma(lookbackPeriods); -} diff --git a/src/e-k/Hma/Hma.Models.cs b/src/e-k/Hma/Hma.Models.cs index f63a82743..0bc754fa6 100644 --- a/src/e-k/Hma/Hma.Models.cs +++ b/src/e-k/Hma/Hma.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class HmaResult : ResultBase, IReusableResult +public record HmaResult +( + DateTime Timestamp, + double? Hma = null +) : IReusable { - public HmaResult(DateTime date) - { - Date = date; - } - - public double? Hma { get; set; } - - double? IReusableResult.Value => Hma; + public double Value => Hma.Null2NaN(); } diff --git a/src/e-k/Hma/Hma.Series.cs b/src/e-k/Hma/Hma.Series.cs deleted file mode 100644 index a895f64e3..000000000 --- a/src/e-k/Hma/Hma.Series.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace Skender.Stock.Indicators; - -// HULL MOVING AVERAGE (SERIES) -public static partial class Indicator -{ - // calculate series - internal static List CalcHma( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateHma(lookbackPeriods); - - // initialize - int shiftQty = lookbackPeriods - 1; - List<(DateTime, double)> synthHistory = []; - - List wmaN1 = tpList.GetWma(lookbackPeriods).ToList(); - List wmaN2 = tpList.GetWma(lookbackPeriods / 2).ToList(); - - // roll through quotes, to get interim synthetic quotes - for (int i = 0; i < tpList.Count; i++) - { - (DateTime date, double _) = tpList[i]; - - WmaResult w1 = wmaN1[i]; - WmaResult w2 = wmaN2[i]; - - if (i >= shiftQty) - { - (DateTime, double) sh - = new(date, (w2.Wma.Null2NaN() * 2d) - w1.Wma.Null2NaN()); - - synthHistory.Add(sh); - } - } - - // add back truncated null results - int sqN = (int)Math.Sqrt(lookbackPeriods); - - List results = tpList - .Take(shiftQty) - .Select(x => new HmaResult(x.Item1)) - .ToList(); - - // calculate final HMA = WMA with period SQRT(n) - List hmaResults = synthHistory.CalcWma(sqN) - .Select(x => new HmaResult(x.Date) { - Hma = x.Wma - }) - .ToList(); - - // add WMA to results - results.AddRange(hmaResults); - - return results.ToSortedList(); - } - - // parameter validation - private static void ValidateHma( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for HMA."); - } - } -} diff --git a/src/e-k/Hma/Hma.StaticSeries.cs b/src/e-k/Hma/Hma.StaticSeries.cs new file mode 100644 index 000000000..a4c02ba66 --- /dev/null +++ b/src/e-k/Hma/Hma.StaticSeries.cs @@ -0,0 +1,68 @@ +namespace Skender.Stock.Indicators; + +// HULL MOVING AVERAGE (SERIES) + +public static partial class Hma +{ + public static IReadOnlyList ToHma( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + int shiftQty = lookbackPeriods - 1; + List synthHistory = []; + + IReadOnlyList wmaN1 + = source.ToWma(lookbackPeriods); + + IReadOnlyList wmaN2 + = source.ToWma(lookbackPeriods / 2); + + // roll through source values, to get interim synthetic quotes + for (int i = 0; i < length; i++) + { + T s = source[i]; + + WmaResult w1 = wmaN1[i]; + WmaResult w2 = wmaN2[i]; + + if (i < shiftQty) + { + continue; + } + + QuotePart sh = new( + s.Timestamp, + (w2.Wma.Null2NaN() * 2d) - w1.Wma.Null2NaN()); + + synthHistory.Add(sh); + } + + // add back truncated null results + int sqN = (int)Math.Sqrt(lookbackPeriods); + + List results = source + .Take(shiftQty) + .Select(x => new HmaResult(x.Timestamp)) + .ToList(); + + // calculate final HMA = WMA with period SQRT(n) + List hmaResults = synthHistory.ToWma(sqN) + .Select(x => new HmaResult( + Timestamp: x.Timestamp, + Hma: x.Wma + )) + .ToList(); + + // add WMA to results + results.AddRange(hmaResults); + + return results.ToSortedList(); + } +} diff --git a/src/e-k/Hma/Hma.Utilities.cs b/src/e-k/Hma/Hma.Utilities.cs index fc5e0be5b..2cb778625 100644 --- a/src/e-k/Hma/Hma.Utilities.cs +++ b/src/e-k/Hma/Hma.Utilities.cs @@ -1,17 +1,16 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +public static partial class Hma { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Hma != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for HMA."); + } } } diff --git a/src/e-k/HtTrendline/HtTrendline.Api.cs b/src/e-k/HtTrendline/HtTrendline.Api.cs deleted file mode 100644 index 2ca410491..000000000 --- a/src/e-k/HtTrendline/HtTrendline.Api.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Skender.Stock.Indicators; - -// HILBERT TRANSFORM - INSTANTANEOUS TRENDLINE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetHtTrendline( - this IEnumerable quotes) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.HL2) - .CalcHtTrendline(); - - // SERIES, from CHAIN - public static IEnumerable GetHtTrendline( - this IEnumerable results) => results - .ToTuple() - .CalcHtTrendline() - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetHtTrendline( - this IEnumerable<(DateTime, double)> priceTuples) => priceTuples - .ToSortedList() - .CalcHtTrendline(); -} diff --git a/src/e-k/HtTrendline/HtTrendline.Models.cs b/src/e-k/HtTrendline/HtTrendline.Models.cs index a4e9913c1..0c73dd30d 100644 --- a/src/e-k/HtTrendline/HtTrendline.Models.cs +++ b/src/e-k/HtTrendline/HtTrendline.Models.cs @@ -1,16 +1,13 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class HtlResult : ResultBase, IReusableResult +public record HtlResult +( + DateTime Timestamp, + int? DcPeriods, + double? Trendline, + double? SmoothPrice +) : IReusable { - public HtlResult(DateTime date) - { - Date = date; - } - - public int? DcPeriods { get; set; } - public double? Trendline { get; set; } - public double? SmoothPrice { get; set; } - - double? IReusableResult.Value => Trendline; + public double Value => Trendline.Null2NaN(); } diff --git a/src/e-k/HtTrendline/HtTrendline.Series.cs b/src/e-k/HtTrendline/HtTrendline.StaticSeries.cs similarity index 63% rename from src/e-k/HtTrendline/HtTrendline.Series.cs rename to src/e-k/HtTrendline/HtTrendline.StaticSeries.cs index fc1c4e4e6..0788b5181 100644 --- a/src/e-k/HtTrendline/HtTrendline.Series.cs +++ b/src/e-k/HtTrendline/HtTrendline.StaticSeries.cs @@ -1,13 +1,29 @@ namespace Skender.Stock.Indicators; // HILBERT TRANSFORM - INSTANTANEOUS TRENDLINE (SERIES) -public static partial class Indicator + +public static partial class HtTrendline { - internal static List CalcHtTrendline( - this List<(DateTime, double)> tpList) + // SERIES, from CHAIN + /// + /// Hilbert Transform Instantaneous Trendline(HTL) is a 5-period trendline + /// of high/low price that uses signal processing to reduce noise. + /// + /// + /// T must be type + /// + /// Time-series values to transform. + /// Time series of HTL values and smoothed price. + public static IReadOnlyList ToHtTrendline( + this IReadOnlyList source) + where T : IReusable { + // prefer HL2 when IQuote + IReadOnlyList values + = source.ToPreferredList(CandlePart.HL2); + // initialize - int length = tpList.Count; + int length = values.Count; List results = new(length); double[] pr = new double[length]; // price @@ -18,9 +34,6 @@ internal static List CalcHtTrendline( double[] q1 = new double[length]; // quadrature double[] i1 = new double[length]; // in-phase - double jI; - double jQ; - double[] q2 = new double[length]; // adj. quadrature double[] i2 = new double[length]; // adj. in-phase @@ -30,14 +43,11 @@ internal static List CalcHtTrendline( double[] sd = new double[length]; // smooth period double[] it = new double[length]; // instantaneous trend (raw) - // roll through quotes + // roll through source values for (int i = 0; i < length; i++) { - (DateTime date, double value) = tpList[i]; - pr[i] = value; - - HtlResult r = new(date); - results.Add(r); + IReusable s = values[i]; + pr[i] = s.Value; if (i > 5) { @@ -52,8 +62,8 @@ internal static List CalcHtTrendline( i1[i] = dt[i - 3]; // advance the phases by 90 degrees - jI = ((0.0962 * i1[i]) + (0.5769 * i1[i - 2]) - (0.5769 * i1[i - 4]) - (0.0962 * i1[i - 6])) * adj; - jQ = ((0.0962 * q1[i]) + (0.5769 * q1[i - 2]) - (0.5769 * q1[i - 4]) - (0.0962 * q1[i - 6])) * adj; + double jI = ((0.0962 * i1[i]) + (0.5769 * i1[i - 2]) - (0.5769 * i1[i - 4]) - (0.0962 * i1[i - 6])) * adj; + double jQ = ((0.0962 * q1[i]) + (0.5769 * q1[i - 2]) - (0.5769 * q1[i - 4]) - (0.0962 * q1[i - 6])) * adj; // phasor addition for 3-bar averaging i2[i] = i1[i] - jQ; @@ -75,10 +85,10 @@ internal static List CalcHtTrendline( : 0d; // adjust period to thresholds - pd[i] = (pd[i] > 1.5 * pd[i - 1]) ? 1.5 * pd[i - 1] : pd[i]; - pd[i] = (pd[i] < 0.67 * pd[i - 1]) ? 0.67 * pd[i - 1] : pd[i]; - pd[i] = (pd[i] < 6d) ? 6d : pd[i]; - pd[i] = (pd[i] > 50d) ? 50d : pd[i]; + pd[i] = pd[i] > 1.5 * pd[i - 1] ? 1.5 * pd[i - 1] : pd[i]; + pd[i] = pd[i] < 0.67 * pd[i - 1] ? 0.67 * pd[i - 1] : pd[i]; + pd[i] = pd[i] < 6d ? 6d : pd[i]; + pd[i] = pd[i] > 50d ? 50d : pd[i]; // smooth the period pd[i] = (0.2 * pd[i]) + (0.8 * pd[i - 1]); @@ -103,21 +113,27 @@ internal static List CalcHtTrendline( it[i] = dcPeriods > 0 ? sumPr / dcPeriods : pr[i]; - r.DcPeriods = dcPeriods > 0 ? dcPeriods : null; - // final indicators - r.Trendline = i >= 11 // 12th bar - ? (((4 * it[i]) + (3 * it[i - 1]) + (2 * it[i - 2]) + it[i - 3]) / 10d).NaN2Null() - : pr[i].NaN2Null(); + results.Add(new( + + Timestamp: s.Timestamp, + DcPeriods: dcPeriods > 0 ? dcPeriods : null, + + Trendline: i >= 11 // 12th bar + ? (((4 * it[i]) + (3 * it[i - 1]) + (2 * it[i - 2]) + it[i - 3]) / 10d).NaN2Null() + : pr[i].NaN2Null(), - r.SmoothPrice = (((4 * pr[i]) + (3 * pr[i - 1]) + (2 * pr[i - 2]) + pr[i - 3]) / 10d).NaN2Null(); + SmoothPrice: (((4 * pr[i]) + (3 * pr[i - 1]) + (2 * pr[i - 2]) + pr[i - 3]) / 10d).NaN2Null())); } // initialization period else { - r.Trendline = pr[i].NaN2Null(); - r.SmoothPrice = null; + results.Add(new( + Timestamp: s.Timestamp, + DcPeriods: null, + Trendline: pr[i].NaN2Null(), + SmoothPrice: null)); pd[i] = 0; sp[i] = 0; diff --git a/src/e-k/HtTrendline/HtTrendline.Utilities.cs b/src/e-k/HtTrendline/HtTrendline.Utilities.cs index 698505a1b..6ca015ee2 100644 --- a/src/e-k/HtTrendline/HtTrendline.Utilities.cs +++ b/src/e-k/HtTrendline/HtTrendline.Utilities.cs @@ -1,10 +1,12 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// HILBERT TRANSFORM - INSTANTANEOUS TRENDLINE (UTILITIES) + +public static partial class HtTrendline { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) => results.Remove(100); + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) + => results.Remove(100); } diff --git a/src/e-k/HtTrendline/info.xml b/src/e-k/HtTrendline/info.xml deleted file mode 100644 index 465a32f5a..000000000 --- a/src/e-k/HtTrendline/info.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Hilbert Transform Instantaneous Trendline (HTL) is a 5-period trendline of high/low price that uses signal processing to reduce noise. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - Time series of HTL values and smoothed price. - \ No newline at end of file diff --git a/src/e-k/Hurst/Hurst.Api.cs b/src/e-k/Hurst/Hurst.Api.cs deleted file mode 100644 index a8b33a1b6..000000000 --- a/src/e-k/Hurst/Hurst.Api.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Skender.Stock.Indicators; - -// HURST EXPONENT (API) -public static partial class Indicator -{ - /// - /// - public static IEnumerable GetHurst( - this IEnumerable quotes, - int lookbackPeriods = 100) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcHurst(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetHurst( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcHurst(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetHurst( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcHurst(lookbackPeriods); -} diff --git a/src/e-k/Hurst/Hurst.Models.cs b/src/e-k/Hurst/Hurst.Models.cs index 3c6b82b15..6b283a542 100644 --- a/src/e-k/Hurst/Hurst.Models.cs +++ b/src/e-k/Hurst/Hurst.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class HurstResult : ResultBase, IReusableResult +public record HurstResult +( + DateTime Timestamp, + double? HurstExponent +) : IReusable { - public HurstResult(DateTime date) - { - Date = date; - } - - public double? HurstExponent { get; set; } - - double? IReusableResult.Value => HurstExponent; + public double Value => HurstExponent.Null2NaN(); } diff --git a/src/e-k/Hurst/Hurst.Series.cs b/src/e-k/Hurst/Hurst.StaticSeries.cs similarity index 71% rename from src/e-k/Hurst/Hurst.Series.cs rename to src/e-k/Hurst/Hurst.StaticSeries.cs index d7685bb5d..3b2ff650d 100644 --- a/src/e-k/Hurst/Hurst.Series.cs +++ b/src/e-k/Hurst/Hurst.StaticSeries.cs @@ -1,26 +1,27 @@ namespace Skender.Stock.Indicators; // HURST EXPONENT (SERIES) -public static partial class Indicator + +public static partial class Hurst { - internal static List CalcHurst( - this List<(DateTime, double)> tpList, - int lookbackPeriods) + public static IReadOnlyList ToHurst( + this IReadOnlyList source, + int lookbackPeriods = 100) + where T : IReusable { // check parameter arguments - ValidateHurst(lookbackPeriods); + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); // initialize - int length = tpList.Count; + int length = source.Count; List results = new(length); - // roll through quotes + // roll through source values for (int i = 0; i < length; i++) { - (DateTime date, double _) = tpList[i]; - - HurstResult r = new(date); - results.Add(r); + T s = source[i]; + double? h = null; if (i + 1 > lookbackPeriods) { @@ -28,22 +29,26 @@ internal static List CalcHurst( double[] values = new double[lookbackPeriods]; int x = 0; - double l = tpList[i - lookbackPeriods].Item2; + double l = source[i - lookbackPeriods].Value; for (int p = i + 1 - lookbackPeriods; p <= i; p++) { - (DateTime _, double c) = tpList[p]; + T ps = source[p]; // return values - values[x] = l != 0 ? (c / l) - 1 : double.NaN; + values[x] = l != 0 ? ps.Value / l - 1 : double.NaN; - l = c; + l = ps.Value; x++; } // calculate hurst exponent - r.HurstExponent = CalcHurstWindow(values).NaN2Null(); + h = CalcHurstWindow(values).NaN2Null(); } + + results.Add(new( + Timestamp: s.Timestamp, + HurstExponent: h)); } return results; @@ -85,7 +90,7 @@ private static double CalcHurstWindow(double[] values) // starting index position used to skip // observations to enforce same-sized chunks - int startIndex = totalSize - (chunkSize * chunkQty); + int startIndex = totalSize - chunkSize * chunkQty; // analyze chunks in set for (int chunkNum = 1; chunkNum <= chunkQty; chunkNum++) @@ -108,8 +113,8 @@ private static double CalcHurstWindow(double[] values) { double y = values[i] - chunkMean; sumY += y; - minY = (sumY < minY) ? sumY : minY; - maxY = (sumY > maxY) ? sumY : maxY; + minY = sumY < minY ? sumY : minY; + maxY = sumY > maxY ? sumY : maxY; sumSq += y * y; } @@ -135,18 +140,6 @@ private static double CalcHurstWindow(double[] values) // hurst exponent // TODO: apply Anis-Lloyd corrected R/S Hurst? - return Numerix.Slope(logSize, logRs); - } - - // parameter validation - private static void ValidateHurst( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods < 20) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be at least 20 for Hurst Exponent."); - } + return Numerical.Slope(logSize, logRs); } } diff --git a/src/e-k/Hurst/Hurst.Utilities.cs b/src/e-k/Hurst/Hurst.Utilities.cs index 53f79595e..92fc16415 100644 --- a/src/e-k/Hurst/Hurst.Utilities.cs +++ b/src/e-k/Hurst/Hurst.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// HURST EXPONENT (UTILITIES) + +public static partial class Hurst { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -14,4 +15,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods < 20) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be at least 20 for Hurst Exponent."); + } + } } diff --git a/src/e-k/Ichimoku/Ichimoku.Api.cs b/src/e-k/Ichimoku/Ichimoku.Api.cs deleted file mode 100644 index 34184d4ed..000000000 --- a/src/e-k/Ichimoku/Ichimoku.Api.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ICHIMOKU CLOUD (API) -public static partial class Indicator -{ - /// - /// - public static IEnumerable GetIchimoku( - this IEnumerable quotes, - int tenkanPeriods = 9, - int kijunPeriods = 26, - int senkouBPeriods = 52) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcIchimoku( - tenkanPeriods, - kijunPeriods, - senkouBPeriods, - kijunPeriods, - kijunPeriods); - - /// - /// - public static IEnumerable GetIchimoku( - this IEnumerable quotes, - int tenkanPeriods, - int kijunPeriods, - int senkouBPeriods, - int offsetPeriods) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcIchimoku( - tenkanPeriods, - kijunPeriods, - senkouBPeriods, - offsetPeriods, - offsetPeriods); - - /// - /// - public static IEnumerable GetIchimoku( - this IEnumerable quotes, - int tenkanPeriods, - int kijunPeriods, - int senkouBPeriods, - int senkouOffset, - int chikouOffset) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcIchimoku( - tenkanPeriods, - kijunPeriods, - senkouBPeriods, - senkouOffset, - chikouOffset); -} diff --git a/src/e-k/Ichimoku/Ichimoku.Models.cs b/src/e-k/Ichimoku/Ichimoku.Models.cs index 5fb1692c3..512c5690d 100644 --- a/src/e-k/Ichimoku/Ichimoku.Models.cs +++ b/src/e-k/Ichimoku/Ichimoku.Models.cs @@ -1,16 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class IchimokuResult : ResultBase -{ - public IchimokuResult(DateTime date) - { - Date = date; - } - - public decimal? TenkanSen { get; set; } // conversion line - public decimal? KijunSen { get; set; } // base line - public decimal? SenkouSpanA { get; set; } // leading span A - public decimal? SenkouSpanB { get; set; } // leading span B - public decimal? ChikouSpan { get; set; } // lagging span -} +public record IchimokuResult +( + DateTime Timestamp, + decimal? TenkanSen, // conversion line + decimal? KijunSen, // base line + decimal? SenkouSpanA, // leading span A + decimal? SenkouSpanB, // leading span B + decimal? ChikouSpan // lagging span +) : ISeries; diff --git a/src/e-k/Ichimoku/Ichimoku.Series.cs b/src/e-k/Ichimoku/Ichimoku.Series.cs deleted file mode 100644 index ab276f947..000000000 --- a/src/e-k/Ichimoku/Ichimoku.Series.cs +++ /dev/null @@ -1,192 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ICHIMOKU CLOUD (SERIES) -public static partial class Indicator -{ - internal static List CalcIchimoku( - this List quotesList, - int tenkanPeriods, - int kijunPeriods, - int senkouBPeriods, - int senkouOffset, - int chikouOffset) - where TQuote : IQuote - { - // check parameter arguments - ValidateIchimoku( - tenkanPeriods, - kijunPeriods, - senkouBPeriods, - senkouOffset, - chikouOffset); - - // initialize - int length = quotesList.Count; - List results = new(length); - int senkouStartPeriod = Math.Max( - 2 * senkouOffset, - Math.Max(tenkanPeriods, kijunPeriods)) - 1; - - // roll through quotes - for (int i = 0; i < length; i++) - { - TQuote q = quotesList[i]; - - IchimokuResult r = new(q.Date); - results.Add(r); - - // tenkan-sen conversion line - CalcIchimokuTenkanSen(i, quotesList, r, tenkanPeriods); - - // kijun-sen base line - CalcIchimokuKijunSen(i, quotesList, r, kijunPeriods); - - // senkou span A - if (i >= senkouStartPeriod) - { - IchimokuResult skq = results[i - senkouOffset]; - - if (skq != null && skq.TenkanSen != null && skq.KijunSen != null) - { - r.SenkouSpanA = (skq.TenkanSen + skq.KijunSen) / 2; - } - } - - // senkou span B - CalcIchimokuSenkouB(i, quotesList, r, senkouOffset, senkouBPeriods); - - // chikou line - if (i + chikouOffset < quotesList.Count) - { - r.ChikouSpan = quotesList[i + chikouOffset].Close; - } - } - - return results; - } - - private static void CalcIchimokuTenkanSen( - int i, List quotesList, IchimokuResult result, int tenkanPeriods) - where TQuote : IQuote - { - if (i >= tenkanPeriods - 1) - { - decimal max = 0; - decimal min = decimal.MaxValue; - - for (int p = i - tenkanPeriods + 1; p <= i; p++) - { - TQuote d = quotesList[p]; - - if (d.High > max) - { - max = d.High; - } - - if (d.Low < min) - { - min = d.Low; - } - } - - result.TenkanSen = (min == decimal.MaxValue) ? null : (min + max) / 2; - } - } - - private static void CalcIchimokuKijunSen( - int i, - List quotesList, - IchimokuResult result, - int kijunPeriods) - where TQuote : IQuote - { - if (i >= kijunPeriods - 1) - { - decimal max = 0; - decimal min = decimal.MaxValue; - - for (int p = i - kijunPeriods + 1; p <= i; p++) - { - TQuote d = quotesList[p]; - - if (d.High > max) - { - max = d.High; - } - - if (d.Low < min) - { - min = d.Low; - } - } - - result.KijunSen = (min == decimal.MaxValue) ? null : (min + max) / 2; - } - } - - private static void CalcIchimokuSenkouB( - int i, - List quotesList, - IchimokuResult result, - int senkouOffset, - int senkouBPeriods) - where TQuote : IQuote - { - if (i >= senkouOffset + senkouBPeriods - 1) - { - decimal max = 0; - decimal min = decimal.MaxValue; - - for (int p = i - senkouOffset - senkouBPeriods + 1; - p <= i - senkouOffset; p++) - { - TQuote d = quotesList[p]; - - if (d.High > max) - { - max = d.High; - } - - if (d.Low < min) - { - min = d.Low; - } - } - - result.SenkouSpanB = (min == decimal.MaxValue) ? null : (min + max) / 2; - } - } - - private static void ValidateIchimoku( - int tenkanPeriods, - int kijunPeriods, - int senkouBPeriods, - int senkouOffset, - int chikouOffset) - { - // check parameter arguments - if (tenkanPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(tenkanPeriods), tenkanPeriods, - "Tenkan periods must be greater than 0 for Ichimoku Cloud."); - } - - if (kijunPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(kijunPeriods), kijunPeriods, - "Kijun periods must be greater than 0 for Ichimoku Cloud."); - } - - if (senkouBPeriods <= kijunPeriods) - { - throw new ArgumentOutOfRangeException(nameof(senkouBPeriods), senkouBPeriods, - "Senkou B periods must be greater than Kijun periods for Ichimoku Cloud."); - } - - if (senkouOffset < 0 || chikouOffset < 0) - { - throw new ArgumentOutOfRangeException(nameof(senkouOffset), senkouOffset, - "Senkou and Chikou offset periods must be non-negative for Ichimoku Cloud."); - } - } -} diff --git a/src/e-k/Ichimoku/Ichimoku.StaticSeries.cs b/src/e-k/Ichimoku/Ichimoku.StaticSeries.cs new file mode 100644 index 000000000..f370ff6a2 --- /dev/null +++ b/src/e-k/Ichimoku/Ichimoku.StaticSeries.cs @@ -0,0 +1,226 @@ +namespace Skender.Stock.Indicators; + +// ICHIMOKU CLOUD (SERIES) + +public static partial class Ichimoku +{ + public static IReadOnlyList ToIchimoku( + this IReadOnlyList quotes, + int tenkanPeriods = 9, + int kijunPeriods = 26, + int senkouBPeriods = 52) + where TQuote : IQuote => quotes + .ToSortedList() + .CalcIchimoku( + tenkanPeriods, + kijunPeriods, + senkouBPeriods, + kijunPeriods, + kijunPeriods); + + public static IReadOnlyList GetIchimoku( + this IReadOnlyList quotes, + int tenkanPeriods, + int kijunPeriods, + int senkouBPeriods, + int offsetPeriods) + where TQuote : IQuote => quotes + .ToSortedList() + .CalcIchimoku( + tenkanPeriods, + kijunPeriods, + senkouBPeriods, + offsetPeriods, + offsetPeriods); + + public static IReadOnlyList GetIchimoku( + this IReadOnlyList quotes, + int tenkanPeriods, + int kijunPeriods, + int senkouBPeriods, + int senkouOffset, + int chikouOffset) + where TQuote : IQuote => quotes + .ToSortedList() + .CalcIchimoku( + tenkanPeriods, + kijunPeriods, + senkouBPeriods, + senkouOffset, + chikouOffset); + + private static List CalcIchimoku( + this IReadOnlyList quotes, + int tenkanPeriods, + int kijunPeriods, + int senkouBPeriods, + int senkouOffset, + int chikouOffset) + where TQuote : IQuote + { + // check parameter arguments + Validate( + tenkanPeriods, + kijunPeriods, + senkouBPeriods, + senkouOffset, + chikouOffset); + + // initialize + int length = quotes.Count; + List results = new(length); + + int senkouStartPeriod = Math.Max( + 2 * senkouOffset, + Math.Max(tenkanPeriods, kijunPeriods)) - 1; + + // roll through source values + for (int i = 0; i < length; i++) + { + TQuote q = quotes[i]; + + // tenkan-sen conversion line + decimal? tenkanSen = CalcIchimokuTenkanSen( + i, quotes, tenkanPeriods); + + // kijun-sen base line + decimal? kijunSen = CalcIchimokuKijunSen( + i, quotes, kijunPeriods); + + // senkou span A + decimal? senkouSpanA = null; + + if (i >= senkouStartPeriod) + { + if (senkouOffset == 0) + { + senkouSpanA = (tenkanSen + kijunSen) / 2; + } + else + { + IchimokuResult skq = results[i - senkouOffset]; + senkouSpanA = (skq.TenkanSen + skq.KijunSen) / 2; + } + } + + // senkou span B + decimal? senkouSpanB = CalcIchimokuSenkouB( + i, quotes, senkouOffset, senkouBPeriods); + + // chikou line + decimal? chikouSpan = null; + + if (i + chikouOffset < quotes.Count) + { + chikouSpan = quotes[i + chikouOffset].Close; + } + + results.Add(new( + Timestamp: q.Timestamp, + TenkanSen: tenkanSen, + KijunSen: kijunSen, + SenkouSpanA: senkouSpanA, + SenkouSpanB: senkouSpanB, + ChikouSpan: chikouSpan)); + } + + return results; + } + + private static decimal? CalcIchimokuTenkanSen( + int i, IReadOnlyList quotes, int tenkanPeriods) + where TQuote : IQuote + { + if (i < tenkanPeriods - 1) + { + return null; + } + + decimal max = 0; + decimal min = decimal.MaxValue; + + for (int p = i - tenkanPeriods + 1; p <= i; p++) + { + TQuote d = quotes[p]; + + if (d.High > max) + { + max = d.High; + } + + if (d.Low < min) + { + min = d.Low; + } + } + + return min == decimal.MaxValue ? null : (min + max) / 2; + + } + + private static decimal? CalcIchimokuKijunSen( + int i, + IReadOnlyList quotes, + int kijunPeriods) + where TQuote : IQuote + { + if (i < kijunPeriods - 1) + { + return null; + } + + decimal max = 0; + decimal min = decimal.MaxValue; + + for (int p = i - kijunPeriods + 1; p <= i; p++) + { + TQuote d = quotes[p]; + + if (d.High > max) + { + max = d.High; + } + + if (d.Low < min) + { + min = d.Low; + } + } + + return min == decimal.MaxValue ? null : (min + max) / 2; + } + + private static decimal? CalcIchimokuSenkouB( + int i, + IReadOnlyList quotes, + int senkouOffset, + int senkouBPeriods) + where TQuote : IQuote + { + if (i < senkouOffset + senkouBPeriods - 1) + { + return null; + } + + decimal max = 0; + decimal min = decimal.MaxValue; + + for (int p = i - senkouOffset - senkouBPeriods + 1; + p <= i - senkouOffset; p++) + { + TQuote d = quotes[p]; + + if (d.High > max) + { + max = d.High; + } + + if (d.Low < min) + { + min = d.Low; + } + } + + return min == decimal.MaxValue ? null : (min + max) / 2; + } +} diff --git a/src/e-k/Ichimoku/Ichimoku.Utilities.cs b/src/e-k/Ichimoku/Ichimoku.Utilities.cs index de63ef8a6..5a09101db 100644 --- a/src/e-k/Ichimoku/Ichimoku.Utilities.cs +++ b/src/e-k/Ichimoku/Ichimoku.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// ICHIMOKU CLOUD (UTILITIES) + +public static partial class Ichimoku { - // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + // remove null results + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -21,4 +22,37 @@ x.TenkanSen is null return resultsList.ToSortedList(); } + + // validate parameters + internal static void Validate( + int tenkanPeriods, + int kijunPeriods, + int senkouBPeriods, + int senkouOffset, + int chikouOffset) + { + if (tenkanPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(tenkanPeriods), tenkanPeriods, + "Tenkan periods must be greater than 0 for Ichimoku Cloud."); + } + + if (kijunPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(kijunPeriods), kijunPeriods, + "Kijun periods must be greater than 0 for Ichimoku Cloud."); + } + + if (senkouBPeriods <= kijunPeriods) + { + throw new ArgumentOutOfRangeException(nameof(senkouBPeriods), senkouBPeriods, + "Senkou B periods must be greater than Kijun periods for Ichimoku Cloud."); + } + + if (senkouOffset < 0 || chikouOffset < 0) + { + throw new ArgumentOutOfRangeException(nameof(senkouOffset), senkouOffset, + "Senkou and Chikou offset periods must be non-negative for Ichimoku Cloud."); + } + } } diff --git a/src/e-k/Kama/Kama.Api.cs b/src/e-k/Kama/Kama.Api.cs deleted file mode 100644 index 01a308a1c..000000000 --- a/src/e-k/Kama/Kama.Api.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Skender.Stock.Indicators; - -// KAUFMAN's ADAPTIVE MOVING AVERAGE (API) -public static partial class Indicator -{ - /// - /// - public static IEnumerable GetKama( - this IEnumerable quotes, - int erPeriods = 10, - int fastPeriods = 2, - int slowPeriods = 30) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcKama(erPeriods, fastPeriods, slowPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetKama( - this IEnumerable results, - int erPeriods = 10, - int fastPeriods = 2, - int slowPeriods = 30) => results - .ToTuple() - .CalcKama(erPeriods, fastPeriods, slowPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetKama( - this IEnumerable<(DateTime, double)> priceTuples, - int erPeriods = 10, - int fastPeriods = 2, - int slowPeriods = 30) => priceTuples - .ToSortedList() - .CalcKama(erPeriods, fastPeriods, slowPeriods); -} diff --git a/src/e-k/Kama/Kama.Models.cs b/src/e-k/Kama/Kama.Models.cs index e5e3e0e23..27b62844c 100644 --- a/src/e-k/Kama/Kama.Models.cs +++ b/src/e-k/Kama/Kama.Models.cs @@ -1,15 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class KamaResult : ResultBase, IReusableResult +public record KamaResult +( + DateTime Timestamp, + double? Er = null, + double? Kama = null +) : IReusable { - public KamaResult(DateTime date) - { - Date = date; - } - - public double? ER { get; set; } - public double? Kama { get; set; } - - double? IReusableResult.Value => Kama; + public double Value => Kama.Null2NaN(); } diff --git a/src/e-k/Kama/Kama.Series.cs b/src/e-k/Kama/Kama.Series.cs deleted file mode 100644 index d5b342b8a..000000000 --- a/src/e-k/Kama/Kama.Series.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace Skender.Stock.Indicators; - -// KAUFMAN's ADAPTIVE MOVING AVERAGE (SERIES) -public static partial class Indicator -{ - internal static List CalcKama( - this List<(DateTime, double)> tpList, - int erPeriods, - int fastPeriods, - int slowPeriods) - { - // check parameter arguments - ValidateKama(erPeriods, fastPeriods, slowPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - double scFast = 2d / (fastPeriods + 1); - double scSlow = 2d / (slowPeriods + 1); - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - KamaResult r = new(date); - results.Add(r); - - if (i + 1 > erPeriods) - { - // ER period change - double change = Math.Abs(value - tpList[i - erPeriods].Item2); - - // volatility - double sumPV = 0; - for (int p = i - erPeriods + 1; p <= i; p++) - { - sumPV += Math.Abs(tpList[p].Item2 - tpList[p - 1].Item2); - } - - if (sumPV != 0) - { - // efficiency ratio - double er = change / sumPV; - r.ER = er.NaN2Null(); - - // smoothing constant - double sc = (er * (scFast - scSlow)) + scSlow; // squared later - - // kama calculation - double? pk = results[i - 1].Kama; // prior KAMA - r.Kama = (pk + (sc * sc * (value - pk))).NaN2Null(); - } - - // handle flatline case - else - { - r.ER = 0; - r.Kama = value.NaN2Null(); - } - } - - // initial value - else if (i + 1 == erPeriods) - { - r.Kama = value.NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidateKama( - int erPeriods, - int fastPeriods, - int slowPeriods) - { - // check parameter arguments - if (erPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(erPeriods), erPeriods, - "Efficiency Ratio periods must be greater than 0 for KAMA."); - } - - if (fastPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, - "Fast EMA periods must be greater than 0 for KAMA."); - } - - if (slowPeriods <= fastPeriods) - { - throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, - "Slow EMA periods must be greater than Fast EMA period for KAMA."); - } - } -} diff --git a/src/e-k/Kama/Kama.StaticSeries.cs b/src/e-k/Kama/Kama.StaticSeries.cs new file mode 100644 index 000000000..b321d4cbf --- /dev/null +++ b/src/e-k/Kama/Kama.StaticSeries.cs @@ -0,0 +1,91 @@ +namespace Skender.Stock.Indicators; + +// KAUFMAN's ADAPTIVE MOVING AVERAGE (SERIES) + +public static partial class Kama +{ + public static IReadOnlyList ToKama( + this IReadOnlyList source, + int erPeriods = 10, + int fastPeriods = 2, + int slowPeriods = 30) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(erPeriods, fastPeriods, slowPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double scFast = 2d / (fastPeriods + 1); + double scSlow = 2d / (slowPeriods + 1); + + double prevKama = double.NaN; + + // roll through source values + for (int i = 0; i < length; i++) + { + // skip incalculable periods + if (i < erPeriods - 1) + { + results.Add(new(source[i].Timestamp)); + continue; + } + + double er; + double kama; + + if (results[i - 1].Kama is not null) + { + double newVal = source[i].Value; + + // ER period change + double change = Math.Abs(newVal - source[i - erPeriods].Value); + + // volatility + double sumPv = 0; + for (int p = i - erPeriods + 1; p <= i; p++) + { + sumPv += Math.Abs(source[p].Value - source[p - 1].Value); + } + + if (sumPv != 0) + { + // efficiency ratio + er = change / sumPv; + + // smoothing constant + double sc = (er * (scFast - scSlow)) + scSlow; // squared later + + // kama calculation + kama = prevKama + (sc * sc * (newVal - prevKama)); + } + + // handle flatline case + else + { + er = 0; + kama = source[i].Value; + } + } + + // re/initialize + else + { + er = double.NaN; + kama = source[i].Value; + } + + results.Add(new KamaResult( + Timestamp: source[i].Timestamp, + Er: er.NaN2Null(), + Kama: kama.NaN2Null())); + + prevKama = kama; + } + + return results; + } +} diff --git a/src/e-k/Kama/Kama.Utilities.cs b/src/e-k/Kama/Kama.Utilities.cs index 2e3493944..93af9a74a 100644 --- a/src/e-k/Kama/Kama.Utilities.cs +++ b/src/e-k/Kama/Kama.Utilities.cs @@ -1,17 +1,44 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// KAUFMAN's ADAPTIVE MOVING AVERAGE (UTILITIES) + +public static partial class Kama { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int erPeriods = results .ToList() - .FindIndex(x => x.ER != null); + .FindIndex(x => x.Er != null); return results.Remove(Math.Max(erPeriods + 100, 10 * erPeriods)); } + + // parameter validation + internal static void Validate( + int erPeriods, + int fastPeriods, + int slowPeriods) + { + // check parameter arguments + if (erPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(erPeriods), erPeriods, + "Efficiency Ratio periods must be greater than 0 for KAMA."); + } + + if (fastPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, + "Fast EMA periods must be greater than 0 for KAMA."); + } + + if (slowPeriods <= fastPeriods) + { + throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, + "Slow EMA periods must be greater than Fast EMA period for KAMA."); + } + } } diff --git a/src/e-k/Keltner/Keltner.Api.cs b/src/e-k/Keltner/Keltner.Api.cs deleted file mode 100644 index 9df400951..000000000 --- a/src/e-k/Keltner/Keltner.Api.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Skender.Stock.Indicators; - -// KELTNER CHANNELS (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetKeltner( - this IEnumerable quotes, - int emaPeriods = 20, - double multiplier = 2, - int atrPeriods = 10) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcKeltner(emaPeriods, multiplier, atrPeriods); -} diff --git a/src/e-k/Keltner/Keltner.Models.cs b/src/e-k/Keltner/Keltner.Models.cs index ba9d79fbe..7acc1a4b6 100644 --- a/src/e-k/Keltner/Keltner.Models.cs +++ b/src/e-k/Keltner/Keltner.Models.cs @@ -1,15 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class KeltnerResult : ResultBase -{ - public KeltnerResult(DateTime date) - { - Date = date; - } - - public double? UpperBand { get; set; } - public double? Centerline { get; set; } - public double? LowerBand { get; set; } - public double? Width { get; set; } -} +public record KeltnerResult +( + DateTime Timestamp, + double? UpperBand = null, + double? Centerline = null, + double? LowerBand = null, + double? Width = null +) : ISeries; diff --git a/src/e-k/Keltner/Keltner.Series.cs b/src/e-k/Keltner/Keltner.Series.cs deleted file mode 100644 index b6dfcde83..000000000 --- a/src/e-k/Keltner/Keltner.Series.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace Skender.Stock.Indicators; - -// KELTNER CHANNELS (SERIES) -public static partial class Indicator -{ - internal static List CalcKeltner( - this List qdList, - int emaPeriods, - double multiplier, - int atrPeriods) - { - // check parameter arguments - ValidateKeltner(emaPeriods, multiplier, atrPeriods); - - // initialize - int length = qdList.Count; - List results = new(length); - - List emaResults = qdList - .ToTuple(CandlePart.Close) - .CalcEma(emaPeriods) - .ToList(); - - List atrResults = qdList - .CalcAtr(atrPeriods) - .ToList(); - - int lookbackPeriods = Math.Max(emaPeriods, atrPeriods); - - // roll through quotes - for (int i = 0; i < length; i++) - { - QuoteD q = qdList[i]; - - KeltnerResult r = new(q.Date); - results.Add(r); - - if (i + 1 >= lookbackPeriods) - { - EmaResult ema = emaResults[i]; - AtrResult atr = atrResults[i]; - double? atrSpan = atr.Atr * multiplier; - - r.UpperBand = ema.Ema + atrSpan; - r.LowerBand = ema.Ema - atrSpan; - r.Centerline = ema.Ema; - r.Width = (r.Centerline == 0) ? null - : (r.UpperBand - r.LowerBand) / r.Centerline; - } - } - - return results; - } - - // parameter validation - private static void ValidateKeltner( - int emaPeriods, - double multiplier, - int atrPeriods) - { - // check parameter arguments - if (emaPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(emaPeriods), emaPeriods, - "EMA periods must be greater than 1 for Keltner Channel."); - } - - if (atrPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(atrPeriods), atrPeriods, - "ATR periods must be greater than 1 for Keltner Channel."); - } - - if (multiplier <= 0) - { - throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, - "Multiplier must be greater than 0 for Keltner Channel."); - } - } -} diff --git a/src/e-k/Keltner/Keltner.StaticSeries.cs b/src/e-k/Keltner/Keltner.StaticSeries.cs new file mode 100644 index 000000000..baa1ba676 --- /dev/null +++ b/src/e-k/Keltner/Keltner.StaticSeries.cs @@ -0,0 +1,63 @@ +namespace Skender.Stock.Indicators; + +// KELTNER CHANNELS (SERIES) + +public static partial class Keltner +{ + public static IReadOnlyList ToKeltner( + this IReadOnlyList quotes, + int emaPeriods = 20, + double multiplier = 2, + int atrPeriods = 10) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcKeltner(emaPeriods, multiplier, atrPeriods); + + private static List CalcKeltner( + this IReadOnlyList source, + int emaPeriods, + double multiplier, + int atrPeriods) + { + // check parameter arguments + Validate(emaPeriods, multiplier, atrPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + IReadOnlyList emaResults + = source.ToEma(emaPeriods); + + IReadOnlyList atrResults + = source.CalcAtr(atrPeriods); + + int lookbackPeriods = Math.Max(emaPeriods, atrPeriods); + + // roll through source values + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + + if (i >= lookbackPeriods - 1) + { + EmaResult ema = emaResults[i]; + AtrResult atr = atrResults[i]; + double? atrSpan = atr.Atr * multiplier; + + results.Add(new KeltnerResult( + Timestamp: q.Timestamp, + UpperBand: ema.Ema + atrSpan, + LowerBand: ema.Ema - atrSpan, + Centerline: ema.Ema, + Width: ema.Ema == 0 ? null : 2 * atrSpan / ema.Ema)); + } + else + { + results.Add(new(q.Timestamp)); + } + } + + return results; + } +} diff --git a/src/e-k/Keltner/Keltner.Utilities.cs b/src/e-k/Keltner/Keltner.Utilities.cs index 9223b3a24..992bf4f5e 100644 --- a/src/e-k/Keltner/Keltner.Utilities.cs +++ b/src/e-k/Keltner/Keltner.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// KELTNER CHANNELS (UTILITIES) + +public static partial class Keltner { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -19,10 +20,9 @@ public static IEnumerable Condense( } // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -30,4 +30,30 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(Math.Max(2 * n, n + 100)); } + + // parameter validation + internal static void Validate( + int emaPeriods, + double multiplier, + int atrPeriods) + { + // check parameter arguments + if (emaPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(emaPeriods), emaPeriods, + "EMA periods must be greater than 1 for Keltner Channel."); + } + + if (atrPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(atrPeriods), atrPeriods, + "ATR periods must be greater than 1 for Keltner Channel."); + } + + if (multiplier <= 0) + { + throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, + "Multiplier must be greater than 0 for Keltner Channel."); + } + } } diff --git a/src/e-k/Kvo/Kvo.Api.cs b/src/e-k/Kvo/Kvo.Api.cs deleted file mode 100644 index 03076766d..000000000 --- a/src/e-k/Kvo/Kvo.Api.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Skender.Stock.Indicators; - -// KLINGER VOLUME OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetKvo( - this IEnumerable quotes, - int fastPeriods = 34, - int slowPeriods = 55, - int signalPeriods = 13) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcKvo(fastPeriods, slowPeriods, signalPeriods); -} diff --git a/src/e-k/Kvo/Kvo.Models.cs b/src/e-k/Kvo/Kvo.Models.cs index 030bd1a31..9588ad1c8 100644 --- a/src/e-k/Kvo/Kvo.Models.cs +++ b/src/e-k/Kvo/Kvo.Models.cs @@ -1,15 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class KvoResult : ResultBase, IReusableResult +public record KvoResult +( + DateTime Timestamp, + double? Oscillator = null, + double? Signal = null +) : IReusable { - internal KvoResult(DateTime date) - { - Date = date; - } - - public double? Oscillator { get; set; } - public double? Signal { get; set; } - - double? IReusableResult.Value => Oscillator; + public double Value => Oscillator.Null2NaN(); } diff --git a/src/e-k/Kvo/Kvo.Series.cs b/src/e-k/Kvo/Kvo.StaticSeries.cs similarity index 60% rename from src/e-k/Kvo/Kvo.Series.cs rename to src/e-k/Kvo/Kvo.StaticSeries.cs index b27ae2b49..1b735fe2d 100644 --- a/src/e-k/Kvo/Kvo.Series.cs +++ b/src/e-k/Kvo/Kvo.StaticSeries.cs @@ -1,19 +1,29 @@ namespace Skender.Stock.Indicators; // KLINGER VOLUME OSCILLATOR (SERIES) -public static partial class Indicator + +public static partial class Kvo { - internal static List CalcKvo( - this List qdList, + public static IReadOnlyList ToKvo( + this IReadOnlyList quotes, + int fastPeriods = 34, + int slowPeriods = 55, + int signalPeriods = 13) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcKvo(fastPeriods, slowPeriods, signalPeriods); + + private static List CalcKvo( + this IReadOnlyList source, int fastPeriods, int slowPeriods, int signalPeriods) { // check parameter arguments - ValidateKlinger(fastPeriods, slowPeriods, signalPeriods); + Validate(fastPeriods, slowPeriods, signalPeriods); // initialize - int length = qdList.Count; + int length = source.Count; List results = new(length); double[] t = new double[length]; // trend direction @@ -29,13 +39,13 @@ internal static List CalcKvo( double kSlow = 2d / (slowPeriods + 1); double kSignal = 2d / (signalPeriods + 1); - // roll through quotes + // roll through source values for (int i = 0; i < length; i++) { - QuoteD q = qdList[i]; + QuoteD q = source[i]; - KvoResult r = new(q.Date); - results.Add(r); + double? kvo = null; + double? sig = null; // trend basis comparator hlc[i] = q.High + q.Low + q.Close; @@ -45,26 +55,28 @@ internal static List CalcKvo( if (i <= 0) { + results.Add(new(q.Timestamp)); continue; } // trend direction - t[i] = (hlc[i] > hlc[i - 1]) ? 1 : -1; + t[i] = hlc[i] > hlc[i - 1] ? 1 : -1; if (i <= 1) { cm[i] = 0; + results.Add(new(q.Timestamp)); continue; } // cumulative measurement - cm[i] = (t[i] == t[i - 1]) ? - (cm[i - 1] + dm[i]) : (dm[i - 1] + dm[i]); + cm[i] = t[i] == t[i - 1] ? + cm[i - 1] + dm[i] : dm[i - 1] + dm[i]; // volume force (VF) - vf[i] = (dm[i] == cm[i] || q.Volume == 0) ? 0 - : (dm[i] == 0) ? q.Volume * 2d * t[i] * 100d - : (cm[i] != 0) ? q.Volume * Math.Abs(2d * ((dm[i] / cm[i]) - 1)) * t[i] * 100d + vf[i] = dm[i] == cm[i] || q.Volume == 0 ? 0 + : dm[i] == 0 ? q.Volume * 2d * t[i] * 100d + : cm[i] != 0 ? q.Volume * Math.Abs(2d * ((dm[i] / cm[i]) - 1)) * t[i] * 100d : vf[i - 1]; // fast-period EMA of VF @@ -72,6 +84,8 @@ internal static List CalcKvo( { vfFastEma[i] = (vf[i] * kFast) + (vfFastEma[i - 1] * (1 - kFast)); } + + // TODO: update healing, without requiring specific indexing else if (i == fastPeriods + 1) { double sum = 0; @@ -88,6 +102,8 @@ internal static List CalcKvo( { vfSlowEma[i] = (vf[i] * kSlow) + (vfSlowEma[i - 1] * (1 - kSlow)); } + + // TODO: update healing, without requiring specific indexing else if (i == slowPeriods + 1) { double sum = 0; @@ -102,53 +118,34 @@ internal static List CalcKvo( // Klinger Oscillator if (i >= slowPeriods + 1) { - r.Oscillator = vfFastEma[i] - vfSlowEma[i]; + kvo = vfFastEma[i] - vfSlowEma[i]; // Signal if (i > slowPeriods + signalPeriods) { - r.Signal = (r.Oscillator * kSignal) + sig = (kvo * kSignal) + (results[i - 1].Signal * (1 - kSignal)); } + + // TODO: update healing, without requiring specific indexing else if (i == slowPeriods + signalPeriods) { - double? sum = 0; - for (int p = slowPeriods + 1; p <= i; p++) + double? sum = kvo; + for (int p = slowPeriods + 1; p < i; p++) { sum += results[p].Oscillator; } - r.Signal = sum / signalPeriods; + sig = sum / signalPeriods; } } - } - - return results; - } - - // parameter validation - private static void ValidateKlinger( - int fastPeriods, - int slowPeriods, - int signalPeriods) - { - // check parameter arguments - if (fastPeriods <= 2) - { - throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, - "Fast (short) Periods must be greater than 2 for Klinger Oscillator."); - } - if (slowPeriods <= fastPeriods) - { - throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, - "Slow (long) Periods must be greater than Fast Periods for Klinger Oscillator."); + results.Add(new KvoResult( + Timestamp: q.Timestamp, + Oscillator: kvo, + Signal: sig)); } - if (signalPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, - "Signal Periods must be greater than 0 for Klinger Oscillator."); - } + return results; } } diff --git a/src/e-k/Kvo/Kvo.Utilities.cs b/src/e-k/Kvo/Kvo.Utilities.cs index fcc18e34d..022893924 100644 --- a/src/e-k/Kvo/Kvo.Utilities.cs +++ b/src/e-k/Kvo/Kvo.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// KLINGER VOLUME OSCILLATOR (UTILIITES) + +public static partial class Kvo { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int l = results .ToList() @@ -14,4 +15,30 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(l + 150); } + + // parameter validation + internal static void Validate( + int fastPeriods, + int slowPeriods, + int signalPeriods) + { + // check parameter arguments + if (fastPeriods <= 2) + { + throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, + "Fast (short) Periods must be greater than 2 for Klinger Oscillator."); + } + + if (slowPeriods <= fastPeriods) + { + throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, + "Slow (long) Periods must be greater than Fast Periods for Klinger Oscillator."); + } + + if (signalPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, + "Signal Periods must be greater than 0 for Klinger Oscillator."); + } + } } diff --git a/src/m-r/MaEnvelopes/MaEnvelopes.Api.cs b/src/m-r/MaEnvelopes/MaEnvelopes.Api.cs deleted file mode 100644 index 4d2035b5f..000000000 --- a/src/m-r/MaEnvelopes/MaEnvelopes.Api.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Skender.Stock.Indicators; - -// MOVING AVERAGE ENVELOPES (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetMaEnvelopes( - this IEnumerable quotes, - int lookbackPeriods, - double percentOffset = 2.5, - MaType movingAverageType = MaType.SMA) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcMaEnvelopes(lookbackPeriods, percentOffset, movingAverageType); - - // SERIES, from CHAIN - public static IEnumerable GetMaEnvelopes( - this IEnumerable results, - int lookbackPeriods, - double percentOffset = 2.5, - MaType movingAverageType = MaType.SMA) => results - .ToTuple() - .CalcMaEnvelopes(lookbackPeriods, percentOffset, movingAverageType) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetMaEnvelopes( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods, - double percentOffset = 2.5, - MaType movingAverageType = MaType.SMA) => priceTuples - .ToSortedList() - .CalcMaEnvelopes(lookbackPeriods, percentOffset, movingAverageType); -} diff --git a/src/m-r/MaEnvelopes/MaEnvelopes.Models.cs b/src/m-r/MaEnvelopes/MaEnvelopes.Models.cs index 399d39c12..c485dfe50 100644 --- a/src/m-r/MaEnvelopes/MaEnvelopes.Models.cs +++ b/src/m-r/MaEnvelopes/MaEnvelopes.Models.cs @@ -1,14 +1,10 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class MaEnvelopeResult : ResultBase -{ - public MaEnvelopeResult(DateTime date) - { - Date = date; - } - - public double? Centerline { get; set; } - public double? UpperEnvelope { get; set; } - public double? LowerEnvelope { get; set; } -} +public record MaEnvelopeResult +( + DateTime Timestamp, + double? Centerline, + double? UpperEnvelope, + double? LowerEnvelope +) : ISeries; diff --git a/src/m-r/MaEnvelopes/MaEnvelopes.Series.cs b/src/m-r/MaEnvelopes/MaEnvelopes.Series.cs deleted file mode 100644 index ce8776b33..000000000 --- a/src/m-r/MaEnvelopes/MaEnvelopes.Series.cs +++ /dev/null @@ -1,151 +0,0 @@ -namespace Skender.Stock.Indicators; - -// MOVING AVERAGE ENVELOPES (SERIES) -public static partial class Indicator -{ - // calculate series - internal static IEnumerable CalcMaEnvelopes( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double percentOffset, - MaType movingAverageType) - { - // check parameter arguments - // note: most validations are done in variant methods - ValidateMaEnvelopes(percentOffset); - - // initialize - double offsetRatio = percentOffset / 100d; - - // get envelopes variant - return movingAverageType switch { - MaType.ALMA => tpList.MaEnvAlma(lookbackPeriods, offsetRatio), - MaType.DEMA => tpList.MaEnvDema(lookbackPeriods, offsetRatio), - MaType.EMA => tpList.MaEnvEma(lookbackPeriods, offsetRatio), - MaType.EPMA => tpList.MaEnvEpma(lookbackPeriods, offsetRatio), - MaType.HMA => tpList.MaEnvHma(lookbackPeriods, offsetRatio), - MaType.SMA => tpList.MaEnvSma(lookbackPeriods, offsetRatio), - MaType.SMMA => tpList.MaEnvSmma(lookbackPeriods, offsetRatio), - MaType.TEMA => tpList.MaEnvTema(lookbackPeriods, offsetRatio), - MaType.WMA => tpList.MaEnvWma(lookbackPeriods, offsetRatio), - - _ => throw new ArgumentOutOfRangeException( - nameof(movingAverageType), movingAverageType, - string.Format( - invCulture, - "Moving Average Envelopes does not support {0}.", - Enum.GetName(typeof(MaType), movingAverageType))) - }; - } - - private static IEnumerable MaEnvAlma( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double offsetRatio) - => tpList.GetAlma(lookbackPeriods) - .Select(x => new MaEnvelopeResult(x.Date) { - Centerline = x.Alma, - UpperEnvelope = x.Alma + (x.Alma * offsetRatio), - LowerEnvelope = x.Alma - (x.Alma * offsetRatio) - }); - - private static IEnumerable MaEnvDema( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double offsetRatio) - => tpList.GetDema(lookbackPeriods) - .Select(x => new MaEnvelopeResult(x.Date) { - Centerline = x.Dema, - UpperEnvelope = x.Dema + (x.Dema * offsetRatio), - LowerEnvelope = x.Dema - (x.Dema * offsetRatio) - }); - - private static IEnumerable MaEnvEma( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double offsetRatio) - => tpList.GetEma(lookbackPeriods) - .Select(x => new MaEnvelopeResult(x.Date) { - Centerline = x.Ema, - UpperEnvelope = x.Ema + (x.Ema * offsetRatio), - LowerEnvelope = x.Ema - (x.Ema * offsetRatio) - }); - - private static IEnumerable MaEnvEpma( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double offsetRatio) - => tpList.GetEpma(lookbackPeriods) - .Select(x => new MaEnvelopeResult(x.Date) { - Centerline = x.Epma, - UpperEnvelope = x.Epma + (x.Epma * offsetRatio), - LowerEnvelope = x.Epma - (x.Epma * offsetRatio) - }); - - private static IEnumerable MaEnvHma( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double offsetRatio) - => tpList.GetHma(lookbackPeriods) - .Select(x => new MaEnvelopeResult(x.Date) { - Centerline = x.Hma, - UpperEnvelope = x.Hma + (x.Hma * offsetRatio), - LowerEnvelope = x.Hma - (x.Hma * offsetRatio) - }); - - private static IEnumerable MaEnvSma( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double offsetRatio) - => tpList.GetSma(lookbackPeriods) - .Select(x => new MaEnvelopeResult(x.Date) { - Centerline = x.Sma, - UpperEnvelope = x.Sma + (x.Sma * offsetRatio), - LowerEnvelope = x.Sma - (x.Sma * offsetRatio) - }); - - private static IEnumerable MaEnvSmma( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double offsetRatio) - => tpList.GetSmma(lookbackPeriods) - .Select(x => new MaEnvelopeResult(x.Date) { - Centerline = x.Smma, - UpperEnvelope = x.Smma + (x.Smma * offsetRatio), - LowerEnvelope = x.Smma - (x.Smma * offsetRatio) - }); - - private static IEnumerable MaEnvTema( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double offsetRatio) - => tpList.GetTema(lookbackPeriods) - .Select(x => new MaEnvelopeResult(x.Date) { - Centerline = x.Tema, - UpperEnvelope = x.Tema + (x.Tema * offsetRatio), - LowerEnvelope = x.Tema - (x.Tema * offsetRatio) - }); - - private static IEnumerable MaEnvWma( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double offsetRatio) - => tpList.GetWma(lookbackPeriods) - .Select(x => new MaEnvelopeResult(x.Date) { - Centerline = x.Wma, - UpperEnvelope = x.Wma + (x.Wma * offsetRatio), - LowerEnvelope = x.Wma - (x.Wma * offsetRatio) - }); - - // parameter validation - private static void ValidateMaEnvelopes( - double percentOffset) - { - // check parameter arguments - if (percentOffset <= 0) - { - throw new ArgumentOutOfRangeException(nameof(percentOffset), percentOffset, - "Percent Offset must be greater than 0 for Moving Average Envelopes."); - } - } -} diff --git a/src/m-r/MaEnvelopes/MaEnvelopes.StaticSeries.cs b/src/m-r/MaEnvelopes/MaEnvelopes.StaticSeries.cs new file mode 100644 index 000000000..52942bbbd --- /dev/null +++ b/src/m-r/MaEnvelopes/MaEnvelopes.StaticSeries.cs @@ -0,0 +1,155 @@ +using System.Globalization; + +namespace Skender.Stock.Indicators; + +// MOVING AVERAGE ENVELOPES (SERIES) + +public static partial class MaEnvelopes +{ + // calculate series + public static IReadOnlyList ToMaEnvelopes( + this IReadOnlyList source, + int lookbackPeriods, + double percentOffset = 2.5, + MaType movingAverageType = MaType.SMA) + where T : IReusable + { + // check parameter arguments + // note: most validations are done in variant methods + Validate(percentOffset); + + // initialize + double offsetRatio = percentOffset / 100d; + + // get envelopes variant + IEnumerable results = movingAverageType + switch { + MaType.ALMA => source.MaEnvAlma(lookbackPeriods, offsetRatio), + MaType.DEMA => source.MaEnvDema(lookbackPeriods, offsetRatio), + MaType.EMA => source.MaEnvEma(lookbackPeriods, offsetRatio), + MaType.EPMA => source.MaEnvEpma(lookbackPeriods, offsetRatio), + MaType.HMA => source.MaEnvHma(lookbackPeriods, offsetRatio), + MaType.SMA => source.MaEnvSma(lookbackPeriods, offsetRatio), + MaType.SMMA => source.MaEnvSmma(lookbackPeriods, offsetRatio), + MaType.TEMA => source.MaEnvTema(lookbackPeriods, offsetRatio), + MaType.WMA => source.MaEnvWma(lookbackPeriods, offsetRatio), + + _ => throw new ArgumentOutOfRangeException( + nameof(movingAverageType), movingAverageType, + string.Format( + CultureInfo.InvariantCulture, + "Moving Average Envelopes does not support {0}.", + Enum.GetName(typeof(MaType), movingAverageType))) + }; + + return results.ToList(); + } + + private static IEnumerable MaEnvAlma( + this IReadOnlyList source, + int lookbackPeriods, + double offsetRatio) + where T : IReusable + => source.ToAlma(lookbackPeriods, offset: 0.85, sigma: 6) + .Select(x => new MaEnvelopeResult( + Timestamp: x.Timestamp, + Centerline: x.Alma, + UpperEnvelope: x.Alma + (x.Alma * offsetRatio), + LowerEnvelope: x.Alma - (x.Alma * offsetRatio))); + + private static IEnumerable MaEnvDema( + this IReadOnlyList source, + int lookbackPeriods, + double offsetRatio) + where T : IReusable + => source.ToDema(lookbackPeriods) + .Select(x => new MaEnvelopeResult( + Timestamp: x.Timestamp, + Centerline: x.Dema, + UpperEnvelope: x.Dema + (x.Dema * offsetRatio), + LowerEnvelope: x.Dema - (x.Dema * offsetRatio))); + + private static IEnumerable MaEnvEma( + this IReadOnlyList source, + int lookbackPeriods, + double offsetRatio) + where T : IReusable + => source.ToEma(lookbackPeriods) + .Select(x => new MaEnvelopeResult( + Timestamp: x.Timestamp, + Centerline: x.Ema, + UpperEnvelope: x.Ema + (x.Ema * offsetRatio), + LowerEnvelope: x.Ema - (x.Ema * offsetRatio))); + + private static IEnumerable MaEnvEpma( + this IReadOnlyList source, + int lookbackPeriods, + double offsetRatio) + where T : IReusable + => source.ToEpma(lookbackPeriods) + .Select(x => new MaEnvelopeResult( + Timestamp: x.Timestamp, + Centerline: x.Epma, + UpperEnvelope: x.Epma + (x.Epma * offsetRatio), + LowerEnvelope: x.Epma - (x.Epma * offsetRatio))); + + private static IEnumerable MaEnvHma( + this IReadOnlyList source, + int lookbackPeriods, + double offsetRatio) + where T : IReusable + => source.ToHma(lookbackPeriods) + .Select(x => new MaEnvelopeResult( + Timestamp: x.Timestamp, + Centerline: x.Hma, + UpperEnvelope: x.Hma + (x.Hma * offsetRatio), + LowerEnvelope: x.Hma - (x.Hma * offsetRatio))); + + private static IEnumerable MaEnvSma( + this IReadOnlyList source, + int lookbackPeriods, + double offsetRatio) + where T : IReusable + => source.ToSma(lookbackPeriods) + .Select(x => new MaEnvelopeResult( + Timestamp: x.Timestamp, + Centerline: x.Sma, + UpperEnvelope: x.Sma + (x.Sma * offsetRatio), + LowerEnvelope: x.Sma - (x.Sma * offsetRatio))); + + private static IEnumerable MaEnvSmma( + this IReadOnlyList source, + int lookbackPeriods, + double offsetRatio) + where T : IReusable + => source.ToSmma(lookbackPeriods) + .Select(x => new MaEnvelopeResult( + Timestamp: x.Timestamp, + Centerline: x.Smma, + UpperEnvelope: x.Smma + (x.Smma * offsetRatio), + LowerEnvelope: x.Smma - (x.Smma * offsetRatio))); + + private static IEnumerable MaEnvTema( + this IReadOnlyList source, + int lookbackPeriods, + double offsetRatio) + where T : IReusable + => source.ToTema(lookbackPeriods) + .Select(x => new MaEnvelopeResult( + Timestamp: x.Timestamp, + Centerline: x.Tema, + UpperEnvelope: x.Tema + (x.Tema * offsetRatio), + LowerEnvelope: x.Tema - (x.Tema * offsetRatio))); + + private static IEnumerable MaEnvWma( + this IReadOnlyList source, + int lookbackPeriods, + double offsetRatio) + where T : IReusable + => source.ToWma(lookbackPeriods) + .Select(x => new MaEnvelopeResult( + Timestamp: x.Timestamp, + Centerline: x.Wma, + UpperEnvelope: x.Wma + (x.Wma * offsetRatio), + LowerEnvelope: x.Wma - (x.Wma * offsetRatio))); +} diff --git a/src/m-r/MaEnvelopes/MaEnvelopes.Utilities.cs b/src/m-r/MaEnvelopes/MaEnvelopes.Utilities.cs index 9f2f3ed4b..07b314c46 100644 --- a/src/m-r/MaEnvelopes/MaEnvelopes.Utilities.cs +++ b/src/m-r/MaEnvelopes/MaEnvelopes.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// MOVING AVERAGE ENVELOPES (UTILITIES) + +public static partial class MaEnvelopes { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -17,4 +18,16 @@ public static IEnumerable Condense( return resultsList.ToSortedList(); } + + // parameter validation + internal static void Validate( + double percentOffset) + { + // check parameter arguments + if (percentOffset <= 0) + { + throw new ArgumentOutOfRangeException(nameof(percentOffset), percentOffset, + "Percent Offset must be greater than 0 for Moving Average Envelopes."); + } + } } diff --git a/src/m-r/Macd/Macd.Models.cs b/src/m-r/Macd/Macd.Models.cs index e989b1a5e..5af8fb0df 100644 --- a/src/m-r/Macd/Macd.Models.cs +++ b/src/m-r/Macd/Macd.Models.cs @@ -1,20 +1,18 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class MacdResult : ResultBase, IReusableResult -{ - public MacdResult(DateTime date) - { - Date = date; - } - - public double? Macd { get; set; } - public double? Signal { get; set; } - public double? Histogram { get; set; } +public record MacdResult +( + DateTime Timestamp, + double? Macd, + double? Signal, + double? Histogram, - // extra interim data - public double? FastEma { get; set; } - public double? SlowEma { get; set; } + // extra/interim data + double? FastEma, + double? SlowEma - double? IReusableResult.Value => Macd; +) : IReusable +{ + public double Value => Macd.Null2NaN(); } diff --git a/src/m-r/Macd/Macd.Series.cs b/src/m-r/Macd/Macd.Series.cs deleted file mode 100644 index d5660d9c3..000000000 --- a/src/m-r/Macd/Macd.Series.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace Skender.Stock.Indicators; - -// MOVING AVERAGE CONVERGENCE/DIVERGENCE (MACD) OSCILLATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcMacd( - this List<(DateTime, double)> tpList, - int fastPeriods, - int slowPeriods, - int signalPeriods) - { - // check parameter arguments - ValidateMacd(fastPeriods, slowPeriods, signalPeriods); - - // initialize - List emaFast = tpList.CalcEma(fastPeriods); - List emaSlow = tpList.CalcEma(slowPeriods); - - int length = tpList.Count; - List<(DateTime, double)> emaDiff = []; - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double _) = tpList[i]; - EmaResult df = emaFast[i]; - EmaResult ds = emaSlow[i]; - - MacdResult r = new(date) { - FastEma = df.Ema, - SlowEma = ds.Ema - }; - results.Add(r); - - if (i >= slowPeriods - 1) - { - double macd = (df.Ema - ds.Ema).Null2NaN(); - r.Macd = macd.NaN2Null(); - - // temp data for interim EMA of macd - (DateTime, double) diff = (date, macd); - - emaDiff.Add(diff); - } - } - - // add signal and histogram to result - List emaSignal = CalcEma(emaDiff, signalPeriods); - - for (int d = slowPeriods - 1; d < length; d++) - { - MacdResult r = results[d]; - EmaResult ds = emaSignal[d + 1 - slowPeriods]; - - r.Signal = ds.Ema.NaN2Null(); - r.Histogram = (r.Macd - r.Signal).NaN2Null(); - } - - return results; - } - - // parameter validation - private static void ValidateMacd( - int fastPeriods, - int slowPeriods, - int signalPeriods) - { - // check parameter arguments - if (fastPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, - "Fast periods must be greater than 0 for MACD."); - } - - if (signalPeriods < 0) - { - throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, - "Signal periods must be greater than or equal to 0 for MACD."); - } - - if (slowPeriods <= fastPeriods) - { - throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, - "Slow periods must be greater than the fast period for MACD."); - } - } -} diff --git a/src/m-r/Macd/Macd.StaticSeries.cs b/src/m-r/Macd/Macd.StaticSeries.cs new file mode 100644 index 000000000..45c4628d6 --- /dev/null +++ b/src/m-r/Macd/Macd.StaticSeries.cs @@ -0,0 +1,82 @@ +namespace Skender.Stock.Indicators; + +// MOVING AVERAGE CONVERGENCE/DIVERGENCE (MACD) OSCILLATOR (SERIES) + +public static partial class Macd +{ + public static IReadOnlyList ToMacd( + this IReadOnlyList source, + int fastPeriods = 12, + int slowPeriods = 26, + int signalPeriods = 9) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(fastPeriods, slowPeriods, signalPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double lastEmaFast = double.NaN; + double lastEmaSlow = double.NaN; + double lastEmaMacd = double.NaN; + + double kFast = 2d / (fastPeriods + 1); + double kSlow = 2d / (slowPeriods + 1); + double kMacd = 2d / (signalPeriods + 1); + + // roll through source values + for (int i = 0; i < length; i++) + { + // Fast EMA + double emaFast + = i >= fastPeriods - 1 && results[i - 1].FastEma is null + ? Sma.Increment(source, fastPeriods, i) + : Ema.Increment(kFast, lastEmaFast, source[i].Value); + + // Slow EMA + double emaSlow + = i >= slowPeriods - 1 && results[i - 1].SlowEma is null + ? Sma.Increment(source, slowPeriods, i) + : Ema.Increment(kSlow, lastEmaSlow, source[i].Value); + + // MACD + double macd = emaFast - emaSlow; + + // Signal + double signal; + + if (i >= signalPeriods + slowPeriods - 2 && results[i - 1].Signal is null) + { + double sum = macd; + for (int p = i - signalPeriods + 1; p < i; p++) + { + sum += results[p].Value; + } + + signal = sum / signalPeriods; + } + else + { + signal = Ema.Increment(kMacd, lastEmaMacd, macd); + } + + // results + results.Add(new MacdResult( + Timestamp: source[i].Timestamp, + Macd: macd.NaN2Null(), + Signal: signal.NaN2Null(), + Histogram: (macd - signal).NaN2Null(), + FastEma: emaFast.NaN2Null(), + SlowEma: emaSlow.NaN2Null())); + + lastEmaMacd = signal; + lastEmaFast = emaFast; + lastEmaSlow = emaSlow; + } + + return results; + } +} diff --git a/src/m-r/Macd/Macd.Utilities.cs b/src/m-r/Macd/Macd.Utilities.cs index a09ff5480..4326aac2d 100644 --- a/src/m-r/Macd/Macd.Utilities.cs +++ b/src/m-r/Macd/Macd.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// MOVING AVERAGE CONVERGENCE/DIVERGENCE (MACD) OSCILLATOR (UTILITIES) + +public static partial class Macd { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +15,30 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(n + 250); } + + // parameter validation + internal static void Validate( + int fastPeriods, + int slowPeriods, + int signalPeriods) + { + // check parameter arguments + if (fastPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, + "Fast periods must be greater than 0 for MACD."); + } + + if (signalPeriods < 0) + { + throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, + "Signal periods must be greater than or equal to 0 for MACD."); + } + + if (slowPeriods <= fastPeriods) + { + throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, + "Slow periods must be greater than the fast period for MACD."); + } + } } diff --git a/src/m-r/Macd/MacdApi.cs b/src/m-r/Macd/MacdApi.cs deleted file mode 100644 index eeefb3827..000000000 --- a/src/m-r/Macd/MacdApi.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Skender.Stock.Indicators; - -// MOVING AVERAGE CONVERGENCE/DIVERGENCE (MACD) OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetMacd( - this IEnumerable quotes, - int fastPeriods = 12, - int slowPeriods = 26, - int signalPeriods = 9) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcMacd(fastPeriods, slowPeriods, signalPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetMacd( - this IEnumerable results, - int fastPeriods = 12, - int slowPeriods = 26, - int signalPeriods = 9) => results - .ToTuple() - .CalcMacd(fastPeriods, slowPeriods, signalPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetMacd( - this IEnumerable<(DateTime, double)> priceTuples, - int fastPeriods = 12, - int slowPeriods = 26, - int signalPeriods = 9) => priceTuples - .ToSortedList() - .CalcMacd(fastPeriods, slowPeriods, signalPeriods); -} diff --git a/src/m-r/Mama/Mama.Api.cs b/src/m-r/Mama/Mama.Api.cs deleted file mode 100644 index 8d30b7fab..000000000 --- a/src/m-r/Mama/Mama.Api.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Skender.Stock.Indicators; - -// MOTHER of ADAPTIVE MOVING AVERAGES - MAMA (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetMama( - this IEnumerable quotes, - double fastLimit = 0.5, - double slowLimit = 0.05) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.HL2) - .CalcMama(fastLimit, slowLimit); - - // SERIES, from CHAIN - public static IEnumerable GetMama( - this IEnumerable results, - double fastLimit = 0.5, - double slowLimit = 0.05) => results - .ToTuple() - .CalcMama(fastLimit, slowLimit) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetMama( - this IEnumerable<(DateTime, double)> priceTuples, - double fastLimit = 0.5, - double slowLimit = 0.05) => priceTuples - .ToSortedList() - .CalcMama(fastLimit, slowLimit); -} diff --git a/src/m-r/Mama/Mama.Models.cs b/src/m-r/Mama/Mama.Models.cs index 7b18f3b78..4a3f0f949 100644 --- a/src/m-r/Mama/Mama.Models.cs +++ b/src/m-r/Mama/Mama.Models.cs @@ -1,15 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class MamaResult : ResultBase, IReusableResult +public record MamaResult +( + DateTime Timestamp, + double? Mama = null, + double? Fama = null +) : IReusable { - public MamaResult(DateTime date) - { - Date = date; - } - - public double? Mama { get; set; } - public double? Fama { get; set; } - - double? IReusableResult.Value => Mama; + public double Value => Mama.Null2NaN(); } diff --git a/src/m-r/Mama/Mama.Series.cs b/src/m-r/Mama/Mama.StaticSeries.cs similarity index 51% rename from src/m-r/Mama/Mama.Series.cs rename to src/m-r/Mama/Mama.StaticSeries.cs index ebbeaac73..3110c5702 100644 --- a/src/m-r/Mama/Mama.Series.cs +++ b/src/m-r/Mama/Mama.StaticSeries.cs @@ -1,21 +1,28 @@ namespace Skender.Stock.Indicators; -// MOTHER of ADAPTIVE MOVING AVERAGES - MAMA (SERIES) -public static partial class Indicator +// MOTHER of ADAPTIVE MOVING AVERAGES (SERIES) + +public static partial class Mama { - internal static List CalcMama( - this List<(DateTime, double)> tpList, - double fastLimit, - double slowLimit) + public static IReadOnlyList ToMama( + this IReadOnlyList source, + double fastLimit = 0.5, + double slowLimit = 0.05) + where T : IReusable { // check parameter arguments - ValidateMama(fastLimit, slowLimit); + Validate(fastLimit, slowLimit); + + // prefer HL2 when IQuote + IReadOnlyList values + = source.ToPreferredList(CandlePart.HL2); // initialize - int length = tpList.Count; + int length = values.Count; List results = new(length); - double sumPr = 0d; + double prevMama = double.NaN; + double prevFama = double.NaN; double[] pr = new double[length]; // price double[] sm = new double[length]; // smooth @@ -25,9 +32,6 @@ internal static List CalcMama( double[] q1 = new double[length]; // quadrature double[] i1 = new double[length]; // in-phase - double jI; - double jQ; - double[] q2 = new double[length]; // adj. quadrature double[] i2 = new double[length]; // adj. in-phase @@ -36,16 +40,50 @@ internal static List CalcMama( double[] ph = new double[length]; // phase - // roll through quotes + // roll through source values for (int i = 0; i < length; i++) { - (DateTime date, double value) = tpList[i]; - pr[i] = value; + IReusable s = values[i]; + pr[i] = s.Value; + + // skip incalculable periods + if (i < 5) + { + results.Add(new(s.Timestamp)); + continue; + } + + double mama; + double fama; + + // initialization + if (double.IsNaN(prevMama)) + { + double sum = 0; + for (int p = i - 5; p <= i; p++) + { + pd[p] = 0; + sm[p] = 0; + dt[p] = 0; + + i1[p] = 0; + q1[p] = 0; + i2[p] = 0; + q2[p] = 0; + + re[p] = 0; + im[p] = 0; - MamaResult r = new(date); - results.Add(r); + ph[p] = 0; + + sum += pr[p]; + } - if (i > 5) + mama = fama = sum / 6; + } + + // normal MAMA + else { double adj = (0.075 * pd[i - 1]) + 0.54; @@ -58,8 +96,8 @@ internal static List CalcMama( i1[i] = dt[i - 3]; // advance the phases by 90 degrees - jI = ((0.0962 * i1[i]) + (0.5769 * i1[i - 2]) - (0.5769 * i1[i - 4]) - (0.0962 * i1[i - 6])) * adj; - jQ = ((0.0962 * q1[i]) + (0.5769 * q1[i - 2]) - (0.5769 * q1[i - 4]) - (0.0962 * q1[i - 6])) * adj; + double jI = ((0.0962 * i1[i]) + (0.5769 * i1[i - 2]) - (0.5769 * i1[i - 4]) - (0.0962 * i1[i - 6])) * adj; + double jQ = ((0.0962 * q1[i]) + (0.5769 * q1[i - 2]) - (0.5769 * q1[i - 4]) - (0.0962 * q1[i - 6])) * adj; // phasor addition for 3-bar averaging i2[i] = i1[i] - jQ; @@ -78,77 +116,40 @@ internal static List CalcMama( // calculate period pd[i] = im[i] != 0 && re[i] != 0 ? 2 * Math.PI / Math.Atan(im[i] / re[i]) - : 0d; + : 0; // adjust period to thresholds - pd[i] = (pd[i] > 1.5 * pd[i - 1]) ? 1.5 * pd[i - 1] : pd[i]; - pd[i] = (pd[i] < 0.67 * pd[i - 1]) ? 0.67 * pd[i - 1] : pd[i]; - pd[i] = (pd[i] < 6d) ? 6d : pd[i]; - pd[i] = (pd[i] > 50d) ? 50d : pd[i]; + pd[i] = pd[i] > 1.5 * pd[i - 1] ? 1.5 * pd[i - 1] : pd[i]; + pd[i] = pd[i] < 0.67 * pd[i - 1] ? 0.67 * pd[i - 1] : pd[i]; + pd[i] = pd[i] < 6 ? 6 : pd[i]; + pd[i] = pd[i] > 50 ? 50 : pd[i]; // smooth the period pd[i] = (0.2 * pd[i]) + (0.8 * pd[i - 1]); // determine phase position - ph[i] = (i1[i] != 0) ? Math.Atan(q1[i] / i1[i]) * 180 / Math.PI : 0; + ph[i] = i1[i] != 0 ? Math.Atan(q1[i] / i1[i]) * 180 / Math.PI : 0; // change in phase - double delta = Math.Max(ph[i - 1] - ph[i], 1d); + double delta = Math.Max(ph[i - 1] - ph[i], 1); // adaptive alpha value double alpha = Math.Max(fastLimit / delta, slowLimit); // final indicators - r.Mama = ((alpha * pr[i]) + ((1d - alpha) * results[i - 1].Mama)).NaN2Null(); - r.Fama = ((0.5d * alpha * r.Mama) + ((1d - (0.5d * alpha)) * results[i - 1].Fama)).NaN2Null(); + mama = (alpha * pr[i]) + ((1d - alpha) * prevMama); + fama = (0.5 * alpha * mama) + ((1d - (0.5 * alpha)) * prevFama); } - // initialization period - else - { - sumPr += pr[i]; - - if (i == 5) - { - r.Mama = (sumPr / 6d).NaN2Null(); - r.Fama = r.Mama; - } - - pd[i] = 0; - sm[i] = 0; - dt[i] = 0; + results.Add(new MamaResult( + Timestamp: s.Timestamp, + Mama: mama.NaN2Null(), + Fama: fama.NaN2Null())); - i1[i] = 0; - q1[i] = 0; - i2[i] = 0; - q2[i] = 0; - - re[i] = 0; - im[i] = 0; - - ph[i] = 0; - } + prevMama = mama; + prevFama = fama; } return results; } - - // parameter validation - private static void ValidateMama( - double fastLimit, - double slowLimit) - { - // check parameter arguments - if (fastLimit <= slowLimit || fastLimit >= 1) - { - throw new ArgumentOutOfRangeException(nameof(fastLimit), fastLimit, - "Fast Limit must be greater than Slow Limit and less than 1 for MAMA."); - } - - if (slowLimit <= 0) - { - throw new ArgumentOutOfRangeException(nameof(slowLimit), slowLimit, - "Slow Limit must be greater than 0 for MAMA."); - } - } } diff --git a/src/m-r/Mama/Mama.Utilities.cs b/src/m-r/Mama/Mama.Utilities.cs index adae6c5ec..2b9a21126 100644 --- a/src/m-r/Mama/Mama.Utilities.cs +++ b/src/m-r/Mama/Mama.Utilities.cs @@ -1,10 +1,30 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// MOTHER of ADAPTIVE MOVING AVERAGES (UTILITIES) + +public static partial class Mama { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) => results.Remove(50); + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) => results.Remove(50); + + // parameter validation + internal static void Validate( + double fastLimit, + double slowLimit) + { + // check parameter arguments + if (fastLimit <= slowLimit || fastLimit >= 1) + { + throw new ArgumentOutOfRangeException(nameof(fastLimit), fastLimit, + "Fast Limit must be greater than Slow Limit and less than 1 for MAMA."); + } + + if (slowLimit <= 0) + { + throw new ArgumentOutOfRangeException(nameof(slowLimit), slowLimit, + "Slow Limit must be greater than 0 for MAMA."); + } + } } diff --git a/src/m-r/Marubozu/Marubozu.Api.cs b/src/m-r/Marubozu/Marubozu.Api.cs deleted file mode 100644 index bec3d47da..000000000 --- a/src/m-r/Marubozu/Marubozu.Api.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Skender.Stock.Indicators; - -// MARUBOZU (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetMarubozu( - this IEnumerable quotes, - double minBodyPercent = 95) - where TQuote : IQuote => quotes - .CalcMarubozu(minBodyPercent); -} diff --git a/src/m-r/Marubozu/Marubozu.Series.cs b/src/m-r/Marubozu/Marubozu.Series.cs deleted file mode 100644 index 6126d2010..000000000 --- a/src/m-r/Marubozu/Marubozu.Series.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace Skender.Stock.Indicators; - -// MARUBOZU (SERIES) -public static partial class Indicator -{ - /// - /// - internal static List CalcMarubozu( - this IEnumerable quotes, - double minBodyPercent) - where TQuote : IQuote - { - // check parameter arguments - ValidateMarubozu(minBodyPercent); - - // initialize - List results = quotes.ToCandleResults(); - minBodyPercent /= 100; - int length = results.Count; - - // roll through candles - for (int i = 0; i < length; i++) - { - CandleResult r = results[i]; - - // check for current signal - if (r.Candle.BodyPct >= minBodyPercent) - { - r.Price = r.Candle.Close; - r.Match = r.Candle.IsBullish ? Match.BullSignal : Match.BearSignal; - } - } - - return results; - } - - // parameter validation - private static void ValidateMarubozu( - double minBodyPercent) - { - // check parameter arguments - if (minBodyPercent > 100) - { - throw new ArgumentOutOfRangeException(nameof(minBodyPercent), minBodyPercent, - "Minimum Body Percent must be less than 100 for Marubozu (<=100%)."); - } - - if (minBodyPercent < 80) - { - throw new ArgumentOutOfRangeException(nameof(minBodyPercent), minBodyPercent, - "Minimum Body Percent must at least 80 (80%) for Marubozu and is usually greater than 90 (90%)."); - } - } -} diff --git a/src/m-r/Marubozu/Marubozu.StaticSeries.cs b/src/m-r/Marubozu/Marubozu.StaticSeries.cs new file mode 100644 index 000000000..6cf34e122 --- /dev/null +++ b/src/m-r/Marubozu/Marubozu.StaticSeries.cs @@ -0,0 +1,46 @@ +namespace Skender.Stock.Indicators; + +// MARUBOZU (SERIES) + +public static partial class Marubozu +{ + public static IReadOnlyList ToMarubozu( + this IReadOnlyList quotes, + double minBodyPercent = 95) + where TQuote : IQuote + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(quotes); + Validate(minBodyPercent); + + // initialize + int length = quotes.Count; + List results = new(length); + + minBodyPercent /= 100; + + // roll through candles + for (int i = 0; i < length; i++) + { + TQuote q = quotes[i]; + decimal? matchPrice = null; + Match matchType = Match.None; + CandleProperties candle = q.ToCandle(); + + // check for current signal + if (candle.BodyPct >= minBodyPercent) + { + matchPrice = q.Close; + matchType = candle.IsBullish ? Match.BullSignal : Match.BearSignal; + } + + results.Add(new( + timestamp: q.Timestamp, + candle: candle, + match: matchType, + price: matchPrice)); + } + + return results; + } +} diff --git a/src/m-r/Marubozu/Marubozu.Utilities.cs b/src/m-r/Marubozu/Marubozu.Utilities.cs new file mode 100644 index 000000000..7d859c14c --- /dev/null +++ b/src/m-r/Marubozu/Marubozu.Utilities.cs @@ -0,0 +1,28 @@ +namespace Skender.Stock.Indicators; + +// MARUBOZU (UTILITIES) + +public static partial class Marubozu +{ + // parameter validation + internal static void Validate( + double minBodyPercent) + { + // check parameter arguments + if (minBodyPercent > 100) + { + throw new ArgumentOutOfRangeException( + nameof(minBodyPercent), minBodyPercent, + "Minimum Body Percent must be less than 100 " + + "for Marubozu (<=100%)."); + } + + if (minBodyPercent < 80) + { + throw new ArgumentOutOfRangeException( + nameof(minBodyPercent), minBodyPercent, + "Minimum Body Percent must at least 80 (80%) " + + "for Marubozu and is usually greater than 90 (90%)."); + } + } +} diff --git a/src/m-r/Marubozu/info.xml b/src/m-r/Marubozu/info.xml index 531da5f7d..5a7e7a20c 100644 --- a/src/m-r/Marubozu/info.xml +++ b/src/m-r/Marubozu/info.xml @@ -5,7 +5,7 @@ Marubozu is a single candlestick pattern that has no wicks, representing consistent directional movement. See - documentation + documentation for more information. @@ -14,4 +14,4 @@ Optional. Minimum candle body size as percentage. Time series of Marubozu values. Invalid parameter value provided. - \ No newline at end of file + diff --git a/src/m-r/Mfi/Mfi.Api.cs b/src/m-r/Mfi/Mfi.Api.cs deleted file mode 100644 index db59c6350..000000000 --- a/src/m-r/Mfi/Mfi.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// MONEY FLOW INDEX (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetMfi( - this IEnumerable quotes, - int lookbackPeriods = 14) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcMfi(lookbackPeriods); -} diff --git a/src/m-r/Mfi/Mfi.Models.cs b/src/m-r/Mfi/Mfi.Models.cs index e4d133454..0a0c8f29b 100644 --- a/src/m-r/Mfi/Mfi.Models.cs +++ b/src/m-r/Mfi/Mfi.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class MfiResult : ResultBase, IReusableResult +public record MfiResult +( + DateTime Timestamp, + double? Mfi +) : IReusable { - public MfiResult(DateTime date) - { - Date = date; - } - - public double? Mfi { get; set; } - - double? IReusableResult.Value => Mfi; + public double Value => Mfi.Null2NaN(); } diff --git a/src/m-r/Mfi/Mfi.Series.cs b/src/m-r/Mfi/Mfi.Series.cs deleted file mode 100644 index f47115438..000000000 --- a/src/m-r/Mfi/Mfi.Series.cs +++ /dev/null @@ -1,101 +0,0 @@ -namespace Skender.Stock.Indicators; - -// MONEY FLOW INDEX (SERIES) -public static partial class Indicator -{ - internal static List CalcMfi( - this List qdList, - int lookbackPeriods) - { - // check parameter arguments - ValidateMfi(lookbackPeriods); - - // initialize - int length = qdList.Count; - List results = new(length); - double[] tp = new double[length]; // true price - double[] mf = new double[length]; // raw MF value - int[] direction = new int[length]; // direction - - double? prevTP = null; - - // roll through quotes, to get preliminary data - for (int i = 0; i < qdList.Count; i++) - { - QuoteD q = qdList[i]; - - MfiResult r = new(q.Date); - results.Add(r); - - // true price - tp[i] = (q.High + q.Low + q.Close) / 3; - - // raw money flow - mf[i] = tp[i] * q.Volume; - - // direction - if (prevTP == null || tp[i] == prevTP) - { - direction[i] = 0; - } - else if (tp[i] > prevTP) - { - direction[i] = 1; - } - else if (tp[i] < prevTP) - { - direction[i] = -1; - } - - prevTP = tp[i]; - } - - // add money flow index - for (int i = lookbackPeriods; i < results.Count; i++) - { - MfiResult r = results[i]; - - double sumPosMFs = 0; - double sumNegMFs = 0; - - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - if (direction[p] == 1) - { - sumPosMFs += mf[p]; - } - else if (direction[p] == -1) - { - sumNegMFs += mf[p]; - } - } - - // calculate MFI normally - if (sumNegMFs != 0) - { - double? mfRatio = sumPosMFs / sumNegMFs; - r.Mfi = 100 - (100 / (1 + mfRatio)); - } - - // handle no negative case - else - { - r.Mfi = 100; - } - } - - return results; - } - - // parameter validation - private static void ValidateMfi( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for MFI."); - } - } -} diff --git a/src/m-r/Mfi/Mfi.StaticSeries.cs b/src/m-r/Mfi/Mfi.StaticSeries.cs new file mode 100644 index 000000000..2d9043a7c --- /dev/null +++ b/src/m-r/Mfi/Mfi.StaticSeries.cs @@ -0,0 +1,102 @@ +namespace Skender.Stock.Indicators; + +// MONEY FLOW INDEX (SERIES) + +public static partial class Mfi +{ + public static IReadOnlyList ToMfi( + this IReadOnlyList quotes, + int lookbackPeriods = 14) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcMfi(lookbackPeriods); + + private static List CalcMfi( + this IReadOnlyList source, + int lookbackPeriods) + { + // check parameter arguments + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double[] tp = new double[length]; // true price + double[] mf = new double[length]; // raw MF value + int[] direction = new int[length]; // direction + + double? prevTp = null; + + // roll through source values, to get preliminary data + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + double mfi; + + // true price + tp[i] = (q.High + q.Low + q.Close) / 3; + + // raw money flow + mf[i] = tp[i] * q.Volume; + + // direction + if (prevTp == null || tp[i] == prevTp) + { + direction[i] = 0; + } + else if (tp[i] > prevTp) + { + direction[i] = 1; + } + else if (tp[i] < prevTp) + { + direction[i] = -1; + } + + // add money flow index + if (i >= lookbackPeriods) + { + double sumPosMFs = 0; + double sumNegMFs = 0; + + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + if (direction[p] == 1) + { + sumPosMFs += mf[p]; + } + else if (direction[p] == -1) + { + sumNegMFs += mf[p]; + } + } + + // calculate MFI normally + if (sumNegMFs != 0) + { + double mfRatio = sumPosMFs / sumNegMFs; + mfi = 100 - 100 / (1 + mfRatio); + } + + // handle no negative case + else + { + mfi = 100; + } + } + else + { + mfi = double.NaN; + } + + results.Add(new( + Timestamp: q.Timestamp, + Mfi: mfi.NaN2Null())); + + prevTp = tp[i]; + } + + return results; + } +} diff --git a/src/m-r/Mfi/Mfi.Utilities.cs b/src/m-r/Mfi/Mfi.Utilities.cs index 8177422d0..e4768b450 100644 --- a/src/m-r/Mfi/Mfi.Utilities.cs +++ b/src/m-r/Mfi/Mfi.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// MONEY FLOW INDEX (UTILITIES) + +public static partial class Mfi { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -14,4 +15,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for MFI."); + } + } } diff --git a/src/m-r/Obv/Obv.Api.cs b/src/m-r/Obv/Obv.Api.cs deleted file mode 100644 index 099233100..000000000 --- a/src/m-r/Obv/Obv.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ON-BALANCE VOLUME (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetObv( - this IEnumerable quotes, - int? smaPeriods = null) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcObv(smaPeriods); -} diff --git a/src/m-r/Obv/Obv.Models.cs b/src/m-r/Obv/Obv.Models.cs index 2f315ae4a..458caa43a 100644 --- a/src/m-r/Obv/Obv.Models.cs +++ b/src/m-r/Obv/Obv.Models.cs @@ -1,15 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class ObvResult : ResultBase, IReusableResult +public record ObvResult +( + DateTime Timestamp, + double Obv +) : IReusable { - public ObvResult(DateTime date) - { - Date = date; - } - - public double Obv { get; set; } - public double? ObvSma { get; set; } - - double? IReusableResult.Value => Obv; + public double Value => Obv; } diff --git a/src/m-r/Obv/Obv.Series.cs b/src/m-r/Obv/Obv.Series.cs deleted file mode 100644 index d50ba1007..000000000 --- a/src/m-r/Obv/Obv.Series.cs +++ /dev/null @@ -1,71 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ON-BALANCE VOLUME (SERIES) -public static partial class Indicator -{ - internal static List CalcObv( - this List qdList, - int? smaPeriods) - { - // check parameter arguments - ValidateObv(smaPeriods); - - // initialize - List results = new(qdList.Count); - - double prevClose = double.NaN; - double obv = 0; - - // roll through quotes - for (int i = 0; i < qdList.Count; i++) - { - QuoteD q = qdList[i]; - - if (double.IsNaN(prevClose) || q.Close == prevClose) - { - // no change to OBV - } - else if (q.Close > prevClose) - { - obv += q.Volume; - } - else if (q.Close < prevClose) - { - obv -= q.Volume; - } - - ObvResult r = new(q.Date) { - Obv = obv - }; - results.Add(r); - - prevClose = q.Close; - - // optional SMA - if (smaPeriods != null && i + 1 > smaPeriods) - { - double? sumSma = 0; - for (int p = i + 1 - (int)smaPeriods; p <= i; p++) - { - sumSma += results[p].Obv; - } - - r.ObvSma = sumSma / smaPeriods; - } - } - - return results; - } - - // parameter validation - private static void ValidateObv( - int? smaPeriods) - { - // check parameter arguments - if (smaPeriods is not null and <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for OBV."); - } - } -} diff --git a/src/m-r/Obv/Obv.StaticSeries.cs b/src/m-r/Obv/Obv.StaticSeries.cs new file mode 100644 index 000000000..48456b11d --- /dev/null +++ b/src/m-r/Obv/Obv.StaticSeries.cs @@ -0,0 +1,50 @@ +namespace Skender.Stock.Indicators; + +// ON-BALANCE VOLUME (SERIES) + +public static partial class Obv +{ + public static IReadOnlyList ToObv( + this IReadOnlyList quotes) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcObv(); + + private static List CalcObv( + this IReadOnlyList source) + { + // initialize + int length = source.Count; + List results = new(length); + + double prevClose = double.NaN; + double obv = 0; + + // roll through source values + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + + if (double.IsNaN(prevClose) || q.Close == prevClose) + { + // no change to OBV + } + else if (q.Close > prevClose) + { + obv += q.Volume; + } + else if (q.Close < prevClose) + { + obv -= q.Volume; + } + + results.Add(new ObvResult( + Timestamp: q.Timestamp, + Obv: obv)); + + prevClose = q.Close; + } + + return results; + } +} diff --git a/src/m-r/Obv/info.xml b/src/m-r/Obv/info.xml index 7ce02375d..378ee0b7f 100644 --- a/src/m-r/Obv/info.xml +++ b/src/m-r/Obv/info.xml @@ -11,7 +11,6 @@ Configurable Quote type. See Guide for more information. Historical price quotes. - Optional. Number of periods for an SMA of the OBV line. Time series of OBV values. Invalid parameter value provided. \ No newline at end of file diff --git a/src/m-r/ParabolicSar/ParabolicSar.Api.cs b/src/m-r/ParabolicSar/ParabolicSar.Api.cs deleted file mode 100644 index 6bec07060..000000000 --- a/src/m-r/ParabolicSar/ParabolicSar.Api.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PARABOLIC SAR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetParabolicSar( - this IEnumerable quotes, - double accelerationStep = 0.02, - double maxAccelerationFactor = 0.2) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcParabolicSar( - accelerationStep, - maxAccelerationFactor, - accelerationStep); - - // SERIES, from TQuote (alt) - /// - /// - public static IEnumerable GetParabolicSar( - this IEnumerable quotes, - double accelerationStep, - double maxAccelerationFactor, - double initialFactor) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcParabolicSar( - accelerationStep, - maxAccelerationFactor, - initialFactor); -} diff --git a/src/m-r/ParabolicSar/ParabolicSar.Models.cs b/src/m-r/ParabolicSar/ParabolicSar.Models.cs index 6b70d76b7..8fad331a8 100644 --- a/src/m-r/ParabolicSar/ParabolicSar.Models.cs +++ b/src/m-r/ParabolicSar/ParabolicSar.Models.cs @@ -1,15 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class ParabolicSarResult : ResultBase, IReusableResult +public record ParabolicSarResult +( + DateTime Timestamp, + double? Sar = null, + bool? IsReversal = null +) : IReusable { - public ParabolicSarResult(DateTime date) - { - Date = date; - } - - public double? Sar { get; set; } - public bool? IsReversal { get; set; } - - double? IReusableResult.Value => Sar; + public double Value => Sar.Null2NaN(); } diff --git a/src/m-r/ParabolicSar/ParabolicSar.Series.cs b/src/m-r/ParabolicSar/ParabolicSar.StaticSeries.cs similarity index 55% rename from src/m-r/ParabolicSar/ParabolicSar.Series.cs rename to src/m-r/ParabolicSar/ParabolicSar.StaticSeries.cs index 67e7cf10c..53eca2d91 100644 --- a/src/m-r/ParabolicSar/ParabolicSar.Series.cs +++ b/src/m-r/ParabolicSar/ParabolicSar.StaticSeries.cs @@ -1,48 +1,70 @@ namespace Skender.Stock.Indicators; // PARABOLIC SAR (SERIES) -public static partial class Indicator + +public static partial class ParabolicSar { - internal static List CalcParabolicSar( - this List qdList, + public static IReadOnlyList ToParabolicSar( + this IReadOnlyList quotes, + double accelerationStep = 0.02, + double maxAccelerationFactor = 0.2) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcParabolicSar( + accelerationStep, + maxAccelerationFactor, + accelerationStep); + + public static IReadOnlyList GetParabolicSar( + this IReadOnlyList quotes, + double accelerationStep, + double maxAccelerationFactor, + double initialFactor) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcParabolicSar( + accelerationStep, + maxAccelerationFactor, + initialFactor); + + private static List CalcParabolicSar( + this IReadOnlyList source, double accelerationStep, double maxAccelerationFactor, double initialFactor) { // check parameter arguments - ValidateParabolicSar( + Validate( accelerationStep, maxAccelerationFactor, initialFactor); // initialize - int length = qdList.Count; + int length = source.Count; List results = new(length); - QuoteD q0; if (length == 0) { return results; } - else - { - q0 = qdList[0]; - } + + QuoteD q0 = source[0]; double accelerationFactor = initialFactor; double extremePoint = q0.High; double priorSar = q0.Low; bool isRising = true; // initial guess - // roll through quotes + // roll through source values for (int i = 0; i < length; i++) { - QuoteD q = qdList[i]; + QuoteD q = source[i]; - ParabolicSarResult r = new(q.Date); - results.Add(r); + bool? isReversal; + double psar; // skip first one if (i == 0) { + results.Add(new(q.Timestamp)); continue; } @@ -57,8 +79,8 @@ internal static List CalcParabolicSar( { double minLastTwo = Math.Min( - qdList[i - 1].Low, - qdList[i - 2].Low); + source[i - 1].Low, + source[i - 2].Low); sar = Math.Min(sar, minLastTwo); } @@ -66,8 +88,8 @@ internal static List CalcParabolicSar( // turn down if (q.Low < sar) { - r.IsReversal = true; - r.Sar = extremePoint; + isReversal = true; + psar = extremePoint; isRising = false; accelerationFactor = initialFactor; @@ -77,8 +99,8 @@ internal static List CalcParabolicSar( // continue rising else { - r.IsReversal = false; - r.Sar = sar; + isReversal = false; + psar = sar; // new high extreme point if (q.High > extremePoint) @@ -102,8 +124,8 @@ double sar if (i >= 2) { double maxLastTwo = Math.Max( - qdList[i - 1].High, - qdList[i - 2].High); + source[i - 1].High, + source[i - 2].High); sar = Math.Max(sar, maxLastTwo); } @@ -111,8 +133,8 @@ double sar // turn up if (q.High > sar) { - r.IsReversal = true; - r.Sar = extremePoint; + isReversal = true; + psar = extremePoint; isRising = true; accelerationFactor = initialFactor; @@ -122,8 +144,8 @@ double sar // continue falling else { - r.IsReversal = false; - r.Sar = sar; + isReversal = false; + psar = sar; // new low extreme point if (q.Low < extremePoint) @@ -137,62 +159,29 @@ double sar } } - priorSar = (double)r.Sar; + results.Add(new ParabolicSarResult( + Timestamp: q.Timestamp, + Sar: psar.NaN2Null(), + IsReversal: isReversal)); + + priorSar = psar; } // remove first trendline since it is an invalid guess - ParabolicSarResult? firstReversal = results - .Where(x => x.IsReversal == true) - .OrderBy(x => x.Date) - .FirstOrDefault(); + int cutIndex = results.FindIndex(x => x.IsReversal ?? false); - int cutIndex = (firstReversal != null) - ? results.IndexOf(firstReversal) - : length - 1; + cutIndex = cutIndex < 0 ? length - 1 : cutIndex; for (int d = 0; d <= cutIndex; d++) { - ParabolicSarResult r = results[d]; - r.Sar = null; - r.IsReversal = null; - } - - return results; - } - - // parameter validation - private static void ValidateParabolicSar( - double accelerationStep, - double maxAccelerationFactor, - double initialFactor) - { - // check parameter arguments - if (accelerationStep <= 0) - { - throw new ArgumentOutOfRangeException(nameof(accelerationStep), accelerationStep, - "Acceleration Step must be greater than 0 for Parabolic SAR."); - } - - if (maxAccelerationFactor <= 0) - { - throw new ArgumentOutOfRangeException(nameof(maxAccelerationFactor), maxAccelerationFactor, - "Max Acceleration Factor must be greater than 0 for Parabolic SAR."); - } - - if (accelerationStep > maxAccelerationFactor) - { - string message = string.Format( - invCulture, - "Acceleration Step cannot be larger than the Max Acceleration Factor ({0}) for Parabolic SAR.", - maxAccelerationFactor); + ParabolicSarResult r = results[d] with { + Sar = null, + IsReversal = null + }; - throw new ArgumentOutOfRangeException(nameof(accelerationStep), accelerationStep, message); + results[d] = r; } - if (initialFactor <= 0 || initialFactor > maxAccelerationFactor) - { - throw new ArgumentOutOfRangeException(nameof(initialFactor), initialFactor, - "Initial Factor must be greater than 0 and not larger than Max Acceleration Factor for Parabolic SAR."); - } + return results; } } diff --git a/src/m-r/ParabolicSar/ParabolicSar.Utilities.cs b/src/m-r/ParabolicSar/ParabolicSar.Utilities.cs index 850844880..afa732021 100644 --- a/src/m-r/ParabolicSar/ParabolicSar.Utilities.cs +++ b/src/m-r/ParabolicSar/ParabolicSar.Utilities.cs @@ -1,17 +1,46 @@ +using System.Globalization; + namespace Skender.Stock.Indicators; -public static partial class Indicator +// PARABOLIC SAR (UTILITIES) + +public static partial class ParabolicSar { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + private static readonly CultureInfo invariantCulture = CultureInfo.InvariantCulture; + + // parameter validation + internal static void Validate( + double accelerationStep, + double maxAccelerationFactor, + double initialFactor) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Sar != null); + // check parameter arguments + if (accelerationStep <= 0) + { + throw new ArgumentOutOfRangeException(nameof(accelerationStep), accelerationStep, + "Acceleration Step must be greater than 0 for Parabolic SAR."); + } + + if (maxAccelerationFactor <= 0) + { + throw new ArgumentOutOfRangeException(nameof(maxAccelerationFactor), maxAccelerationFactor, + "Max Acceleration Factor must be greater than 0 for Parabolic SAR."); + } + + if (accelerationStep > maxAccelerationFactor) + { + string message = string.Format( + invariantCulture, + "Acceleration Step cannot be larger than the Max Acceleration Factor ({0}) for Parabolic SAR.", + maxAccelerationFactor); + + throw new ArgumentOutOfRangeException(nameof(accelerationStep), accelerationStep, message); + } - return results.Remove(removePeriods); + if (initialFactor <= 0 || initialFactor > maxAccelerationFactor) + { + throw new ArgumentOutOfRangeException(nameof(initialFactor), initialFactor, + "Initial Factor must be greater than 0 and not larger than Max Acceleration Factor for Parabolic SAR."); + } } } diff --git a/src/m-r/PivotPoints/PivotPoints.Api.cs b/src/m-r/PivotPoints/PivotPoints.Api.cs deleted file mode 100644 index 95e7d0fe9..000000000 --- a/src/m-r/PivotPoints/PivotPoints.Api.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PIVOT POINTS (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetPivotPoints( - this IEnumerable quotes, - PeriodSize windowSize, - PivotPointType pointType = PivotPointType.Standard) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcPivotPoints(windowSize, pointType); -} diff --git a/src/m-r/PivotPoints/PivotPoints.Models.cs b/src/m-r/PivotPoints/PivotPoints.Models.cs index 5a8a6de8f..f14c31cdc 100644 --- a/src/m-r/PivotPoints/PivotPoints.Models.cs +++ b/src/m-r/PivotPoints/PivotPoints.Models.cs @@ -2,31 +2,51 @@ namespace Skender.Stock.Indicators; internal interface IPivotPoint { - public decimal? R4 { get; set; } - public decimal? R3 { get; set; } - public decimal? R2 { get; set; } - public decimal? R1 { get; set; } - public decimal? PP { get; set; } - public decimal? S1 { get; set; } - public decimal? S2 { get; set; } - public decimal? S3 { get; set; } - public decimal? S4 { get; set; } + decimal? R4 { get; } + decimal? R3 { get; } + decimal? R2 { get; } + decimal? R1 { get; } + decimal? PP { get; } + decimal? S1 { get; } + decimal? S2 { get; } + decimal? S3 { get; } + decimal? S4 { get; } } [Serializable] -public sealed class PivotPointsResult : ResultBase, IPivotPoint +public record PivotPointsResult : ISeries, IPivotPoint { - public decimal? R4 { get; set; } - public decimal? R3 { get; set; } - public decimal? R2 { get; set; } - public decimal? R1 { get; set; } - public decimal? PP { get; set; } - public decimal? S1 { get; set; } - public decimal? S2 { get; set; } - public decimal? S3 { get; set; } - public decimal? S4 { get; set; } + public DateTime Timestamp { get; init; } + + public decimal? PP { get; init; } + + public decimal? S1 { get; init; } + public decimal? S2 { get; init; } + public decimal? S3 { get; init; } + public decimal? S4 { get; init; } + + public decimal? R1 { get; init; } + public decimal? R2 { get; init; } + public decimal? R3 { get; init; } + public decimal? R4 { get; init; } } +internal record WindowPoint : IPivotPoint +{ + public decimal? PP { get; init; } + + public decimal? S1 { get; init; } + public decimal? S2 { get; init; } + public decimal? S3 { get; init; } + public decimal? S4 { get; init; } + + public decimal? R1 { get; init; } + public decimal? R2 { get; init; } + public decimal? R3 { get; init; } + public decimal? R4 { get; init; } +} + + public enum PivotPointType { // do not modify numbers, diff --git a/src/m-r/PivotPoints/PivotPoints.Series.cs b/src/m-r/PivotPoints/PivotPoints.StaticSeries.cs similarity index 52% rename from src/m-r/PivotPoints/PivotPoints.Series.cs rename to src/m-r/PivotPoints/PivotPoints.StaticSeries.cs index 98b8f5e85..7b540b12f 100644 --- a/src/m-r/PivotPoints/PivotPoints.Series.cs +++ b/src/m-r/PivotPoints/PivotPoints.StaticSeries.cs @@ -1,32 +1,32 @@ namespace Skender.Stock.Indicators; // PIVOT POINTS (SERIES) -public static partial class Indicator + +public static partial class PivotPoints { - internal static List CalcPivotPoints( - this List quotesList, + public static IReadOnlyList ToPivotPoints( + this IReadOnlyList quotes, PeriodSize windowSize, - PivotPointType pointType) + PivotPointType pointType = PivotPointType.Standard) where TQuote : IQuote { + ArgumentNullException.ThrowIfNull(quotes); + // initialize - int length = quotesList.Count; + int length = quotes.Count; List results = new(length); - PivotPointsResult? windowPoint = new(); - TQuote h0; + + WindowPoint windowPoint = new(); if (length == 0) { return results; } - else - { - h0 = quotesList[0]; - } - int windowId = GetWindowNumber(h0.Date, windowSize); + TQuote h0 = quotes[0]; + + int windowId = GetWindowNumber(h0.Timestamp, windowSize); - int windowEval; bool firstWindow = true; decimal windowHigh = h0.High; @@ -34,17 +34,13 @@ internal static List CalcPivotPoints( decimal windowOpen = h0.Open; decimal windowClose = h0.Close; - // roll through quotes + // roll through source values for (int i = 0; i < length; i++) { - TQuote q = quotesList[i]; - - PivotPointsResult r = new() { - Date = q.Date - }; + TQuote q = quotes[i]; // new window evaluation - windowEval = GetWindowNumber(q.Date, windowSize); + int windowEval = GetWindowNumber(q.Timestamp, windowSize); if (windowEval != windowId) { @@ -57,7 +53,7 @@ internal static List CalcPivotPoints( windowOpen = q.Open; } - windowPoint = GetPivotPoint( + windowPoint = GetPivotPoint( pointType, windowOpen, windowHigh, windowLow, windowClose); // reset window min/max thresholds @@ -67,43 +63,85 @@ internal static List CalcPivotPoints( } // add levels - if (!firstWindow) - { - // pivot point - r.PP = windowPoint?.PP; - - // support - r.S1 = windowPoint?.S1; - r.S2 = windowPoint?.S2; - r.S3 = windowPoint?.S3; - r.S4 = windowPoint?.S4; - - // resistance - r.R1 = windowPoint?.R1; - r.R2 = windowPoint?.R2; - r.R3 = windowPoint?.R3; - r.R4 = windowPoint?.R4; - } + PivotPointsResult r + = !firstWindow + ? new() { + + Timestamp = q.Timestamp, + + // pivot point + PP = windowPoint.PP, + + // support + S1 = windowPoint.S1, + S2 = windowPoint.S2, + S3 = windowPoint.S3, + S4 = windowPoint.S4, + + // resistance + R1 = windowPoint.R1, + R2 = windowPoint.R2, + R3 = windowPoint.R3, + R4 = windowPoint.R4 + } + : new PivotPointsResult { + Timestamp = q.Timestamp + }; results.Add(r); // capture window threholds (for next iteration) - windowHigh = (q.High > windowHigh) ? q.High : windowHigh; - windowLow = (q.Low < windowLow) ? q.Low : windowLow; + windowHigh = q.High > windowHigh ? q.High : windowHigh; + windowLow = q.Low < windowLow ? q.Low : windowLow; windowClose = q.Close; } return results; } - // internals - internal static TPivotPoint GetPivotPointStandard( + // pivot point lookup + internal static WindowPoint GetPivotPoint( + PivotPointType pointType, decimal open, decimal high, decimal low, decimal close) + => pointType switch { + + PivotPointType.Standard => GetPivotPointStandard(high, low, close), + PivotPointType.Camarilla => GetPivotPointCamarilla(high, low, close), + PivotPointType.Demark => GetPivotPointDemark(open, high, low, close), + PivotPointType.Fibonacci => GetPivotPointFibonacci(high, low, close), + PivotPointType.Woodie => GetPivotPointWoodie(open, high, low), + + _ => throw new ArgumentOutOfRangeException( + nameof(pointType), pointType, "Invalid pointType provided.") + }; + + // window size lookup + private static int GetWindowNumber(DateTime d, PeriodSize windowSize) + => windowSize switch { + + PeriodSize.Month => d.Month, + + PeriodSize.Week => calendar.GetWeekOfYear( + d, calendarWeekRule, firstDayOfWeek), + + PeriodSize.Day => d.Day, + PeriodSize.OneHour => d.Hour, + + _ => throw new ArgumentOutOfRangeException( + nameof(windowSize), windowSize, + string.Format( + invariantCulture, + "Pivot Points does not support PeriodSize of {0}. " + + "See documentation for valid options.", + Enum.GetName(typeof(PeriodSize), windowSize))) + }; + + // pivot point variants + private static WindowPoint GetPivotPointStandard( decimal high, decimal low, decimal close) - where TPivotPoint : IPivotPoint, new() { decimal pp = (high + low + close) / 3; - return new TPivotPoint { + return new() { PP = pp, S1 = (pp * 2) - high, S2 = pp - (high - low), @@ -114,9 +152,8 @@ internal static TPivotPoint GetPivotPointStandard( }; } - internal static TPivotPoint GetPivotPointCamarilla( + private static WindowPoint GetPivotPointCamarilla( decimal high, decimal low, decimal close) - where TPivotPoint : IPivotPoint, new() => new() { PP = close, S1 = close - (1.1m / 12 * (high - low)), @@ -129,39 +166,28 @@ internal static TPivotPoint GetPivotPointCamarilla( R4 = close + (1.1m / 2 * (high - low)) }; - internal static TPivotPoint GetPivotPointDemark( + internal static WindowPoint GetPivotPointDemark( decimal open, decimal high, decimal low, decimal close) - where TPivotPoint : IPivotPoint, new() { - decimal? x; + decimal x = close < open + ? high + (2 * low) + close + : close > open + ? (2 * high) + low + close + : high + low + (2 * close); - if (close < open) - { - x = high + (2 * low) + close; - } - else if (close > open) - { - x = (2 * high) + low + close; - } - else // close == open - { - x = high + low + (2 * close); - } - - return new TPivotPoint { + return new() { PP = x / 4, S1 = (x / 2) - high, R1 = (x / 2) - low }; } - internal static TPivotPoint GetPivotPointFibonacci( + private static WindowPoint GetPivotPointFibonacci( decimal high, decimal low, decimal close) - where TPivotPoint : IPivotPoint, new() { decimal pp = (high + low + close) / 3; - return new TPivotPoint { + return new() { PP = pp, S1 = pp - (0.382m * (high - low)), S2 = pp - (0.618m * (high - low)), @@ -172,47 +198,19 @@ internal static TPivotPoint GetPivotPointFibonacci( }; } - internal static TPivotPoint GetPivotPointWoodie( + private static WindowPoint GetPivotPointWoodie( decimal currentOpen, decimal high, decimal low) - where TPivotPoint : IPivotPoint, new() { decimal pp = (high + low + (2 * currentOpen)) / 4; - return new TPivotPoint { + return new() { PP = pp, S1 = (pp * 2) - high, S2 = pp - high + low, S3 = low - (2 * (high - pp)), R1 = (pp * 2) - low, R2 = pp + high - low, - R3 = high + (2 * (pp - low)), + R3 = high + (2 * (pp - low)) }; } - - // pivot type lookup - internal static TPivotPoint GetPivotPoint( - PivotPointType pointType, decimal open, decimal high, decimal low, decimal close) - where TPivotPoint : IPivotPoint, new() - => pointType switch { - PivotPointType.Standard => GetPivotPointStandard(high, low, close), - PivotPointType.Camarilla => GetPivotPointCamarilla(high, low, close), - PivotPointType.Demark => GetPivotPointDemark(open, high, low, close), - PivotPointType.Fibonacci => GetPivotPointFibonacci(high, low, close), - PivotPointType.Woodie => GetPivotPointWoodie(open, high, low), - _ => throw new ArgumentOutOfRangeException(nameof(pointType), pointType, "Invalid pointType provided.") - }; - - // window size lookup - private static int GetWindowNumber(DateTime d, PeriodSize windowSize) - => windowSize switch { - PeriodSize.Month => d.Month, - PeriodSize.Week => invCalendar.GetWeekOfYear(d, invCalendarWeekRule, invFirstDayOfWeek), - PeriodSize.Day => d.Day, - PeriodSize.OneHour => d.Hour, - _ => throw new ArgumentOutOfRangeException(nameof(windowSize), windowSize, - string.Format( - invCulture, - "Pivot Points does not support PeriodSize of {0}. See documentation for valid options.", - Enum.GetName(typeof(PeriodSize), windowSize))) - }; } diff --git a/src/m-r/PivotPoints/PivotPoints.Utilities.cs b/src/m-r/PivotPoints/PivotPoints.Utilities.cs index bbd81b5c2..c562b0da9 100644 --- a/src/m-r/PivotPoints/PivotPoints.Utilities.cs +++ b/src/m-r/PivotPoints/PivotPoints.Utilities.cs @@ -1,16 +1,29 @@ +using System.Globalization; + namespace Skender.Stock.Indicators; // PIVOT POINTS (UTILITIES) -public static partial class Indicator + +public static partial class PivotPoints { + private static readonly CultureInfo invariantCulture = CultureInfo.InvariantCulture; + + private static readonly Calendar calendar = invariantCulture.Calendar; + + // Gets the DTFI properties required by GetWeekOfYear. + private static readonly CalendarWeekRule calendarWeekRule + = invariantCulture.DateTimeFormat.CalendarWeekRule; + + private static readonly DayOfWeek firstDayOfWeek + = invariantCulture.DateTimeFormat.FirstDayOfWeek; + // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results - .ToList() + .ToList() // TODO: is there a no-copy way to do this? Many places. .FindIndex(x => x.PP != null); return results.Remove(removePeriods); diff --git a/src/m-r/Pivots/Pivots.Api.cs b/src/m-r/Pivots/Pivots.Api.cs deleted file mode 100644 index 6dc75d34e..000000000 --- a/src/m-r/Pivots/Pivots.Api.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PIVOTS (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetPivots( - this IEnumerable quotes, - int leftSpan = 2, - int rightSpan = 2, - int maxTrendPeriods = 20, - EndType endType = EndType.HighLow) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcPivots(leftSpan, rightSpan, maxTrendPeriods, endType); -} diff --git a/src/m-r/Pivots/Pivots.Models.cs b/src/m-r/Pivots/Pivots.Models.cs index d8231c267..8561851c7 100644 --- a/src/m-r/Pivots/Pivots.Models.cs +++ b/src/m-r/Pivots/Pivots.Models.cs @@ -1,25 +1,21 @@ namespace Skender.Stock.Indicators; [Serializable] -public class PivotsResult : ResultBase -{ - public PivotsResult(DateTime date) - { - Date = date; - } - - public decimal? HighPoint { get; set; } - public decimal? LowPoint { get; set; } - public decimal? HighLine { get; set; } - public decimal? LowLine { get; set; } - public PivotTrend? HighTrend { get; set; } - public PivotTrend? LowTrend { get; set; } -} +public record struct PivotsResult +( + DateTime Timestamp, + decimal? HighPoint, + decimal? LowPoint, + decimal? HighLine, + decimal? LowLine, + PivotTrend? HighTrend, + PivotTrend? LowTrend +) : ISeries; public enum PivotTrend { - HH, // higher high - LH, // lower high - HL, // higher low - LL // lower low + Hh, // higher high + Lh, // lower high + Hl, // higher low + Ll // lower low } diff --git a/src/m-r/Pivots/Pivots.Series.cs b/src/m-r/Pivots/Pivots.Series.cs deleted file mode 100644 index b19f7e0f3..000000000 --- a/src/m-r/Pivots/Pivots.Series.cs +++ /dev/null @@ -1,135 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PIVOTS (SERIES) -public static partial class Indicator -{ - internal static List CalcPivots( - this List quotesList, - int leftSpan, - int rightSpan, - int maxTrendPeriods, - EndType endType) - where TQuote : IQuote - { - // check parameter arguments - ValidatePivots(leftSpan, rightSpan, maxTrendPeriods); - - // initialize - - List results - = quotesList - .CalcFractal(leftSpan, rightSpan, endType) - .Select(x => new PivotsResult(x.Date) { - HighPoint = x.FractalBear, - LowPoint = x.FractalBull - }) - .ToList(); - - int? lastHighIndex = null; - decimal? lastHighValue = null; - int? lastLowIndex = null; - decimal? lastLowValue = null; - - // roll through results - for (int i = leftSpan; i <= results.Count - rightSpan; i++) - { - PivotsResult r = results[i]; - - // reset expired indexes - if (lastHighIndex < i - maxTrendPeriods) - { - lastHighIndex = null; - lastHighValue = null; - } - - if (lastLowIndex < i - maxTrendPeriods) - { - lastLowIndex = null; - lastLowValue = null; - } - - // evaluate high trend - if (r.HighPoint != null) - { - // repaint trend - if (lastHighIndex != null && r.HighPoint != lastHighValue) - { - PivotTrend trend = (r.HighPoint > lastHighValue) - ? PivotTrend.HH - : PivotTrend.LH; - - results[(int)lastHighIndex].HighLine = lastHighValue; - - decimal? incr = (r.HighPoint - lastHighValue) - / (i - lastHighIndex); - - for (int t = (int)lastHighIndex + 1; t <= i; t++) - { - results[t].HighTrend = trend; - results[t].HighLine = r.HighPoint + (incr * (t - i)); - } - } - - // reset starting position - lastHighIndex = i; - lastHighValue = r.HighPoint; - } - - // evaluate low trend - if (r.LowPoint != null) - { - // repaint trend - if (lastLowIndex != null && r.LowPoint != lastLowValue) - { - PivotTrend trend = (r.LowPoint > lastLowValue) - ? PivotTrend.HL - : PivotTrend.LL; - - results[(int)lastLowIndex].LowLine = lastLowValue; - - decimal? incr = (r.LowPoint - lastLowValue) - / (i - lastLowIndex); - - for (int t = (int)lastLowIndex + 1; t <= i; t++) - { - results[t].LowTrend = trend; - results[t].LowLine = r.LowPoint + (incr * (t - i)); - } - } - - // reset starting position - lastLowIndex = i; - lastLowValue = r.LowPoint; - } - } - - return results; - } - - // parameter validation - internal static void ValidatePivots( - int leftSpan, - int rightSpan, - int maxTrendPeriods, - string caller = "Pivots") - { - // check parameter arguments - if (rightSpan < 2) - { - throw new ArgumentOutOfRangeException(nameof(rightSpan), rightSpan, - $"Right span must be at least 2 for {caller}."); - } - - if (leftSpan < 2) - { - throw new ArgumentOutOfRangeException(nameof(leftSpan), leftSpan, - $"Left span must be at least 2 for {caller}."); - } - - if (maxTrendPeriods <= leftSpan) - { - throw new ArgumentOutOfRangeException(nameof(leftSpan), leftSpan, - $"Lookback periods must be greater than the Left window span for {caller}."); - } - } -} diff --git a/src/m-r/Pivots/Pivots.StaticSeries.cs b/src/m-r/Pivots/Pivots.StaticSeries.cs new file mode 100644 index 000000000..00a833b10 --- /dev/null +++ b/src/m-r/Pivots/Pivots.StaticSeries.cs @@ -0,0 +1,141 @@ +namespace Skender.Stock.Indicators; + +// PIVOTS (SERIES) + +public static partial class Pivots +{ + public static IReadOnlyList ToPivots( + this IReadOnlyList quotes, + int leftSpan = 2, + int rightSpan = 2, + int maxTrendPeriods = 20, + EndType endType = EndType.HighLow) + where TQuote : IQuote + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(quotes); + Validate(leftSpan, rightSpan, maxTrendPeriods); + + // initialize + int length = quotes.Count; + + decimal?[] highLine = new decimal?[length]; + PivotTrend?[] highTrend = new PivotTrend?[length]; + + decimal?[] lowLine = new decimal?[length]; + PivotTrend?[] lowTrend = new PivotTrend?[length]; + + IReadOnlyList<(decimal? highPoint, decimal? lowPoint)> fractals + = quotes + .ToFractal(leftSpan, rightSpan, endType) + .Select(f => (f.FractalBear, f.FractalBull)) + .ToList(); + + int? lastHighIndex = null; + decimal? lastHighValue = null; + int? lastLowIndex = null; + decimal? lastLowValue = null; + + // roll through results + for (int i = leftSpan; i <= length - rightSpan; i++) + { + (decimal? highPoint, decimal? lowPoint) = fractals[i]; + + // reset expired indexes + if (lastHighIndex < i - maxTrendPeriods) + { + lastHighIndex = null; + lastHighValue = null; + } + + if (lastLowIndex < i - maxTrendPeriods) + { + lastLowIndex = null; + lastLowValue = null; + } + + // evaluate high trend + if (highPoint != null) + { + // repaint trend + if (lastHighIndex != null && highPoint != lastHighValue) + { + PivotTrend trend = highPoint > lastHighValue + ? PivotTrend.Hh + : PivotTrend.Lh; + + highLine[(int)lastHighIndex] = lastHighValue; + + decimal? incr = (highPoint - lastHighValue) + / (i - lastHighIndex); + + for (int t = (int)lastHighIndex + 1; t <= i; t++) + { + highTrend[t] = trend; + highLine[t] = highPoint + incr * (t - i); + } + } + + // reset starting position + lastHighIndex = i; + lastHighValue = highPoint; + } + + // evaluate low trend + if (lowPoint != null) + { + // repaint trend + if (lastLowIndex != null && lowPoint != lastLowValue) + { + PivotTrend trend = lowPoint > lastLowValue + ? PivotTrend.Hl + : PivotTrend.Ll; + + lowLine[(int)lastLowIndex] = lastLowValue; + + decimal? incr = (lowPoint - lastLowValue) + / (i - lastLowIndex); + + for (int t = (int)lastLowIndex + 1; t <= i; t++) + { + lowTrend[t] = trend; + lowLine[t] = lowPoint + incr * (t - i); + } + } + + // reset starting position + lastLowIndex = i; + lastLowValue = lowPoint; + } + } + + // write results + + // TODO: this may need to be re-writes (with) for streaming + // or even here, since it still may be better than 2 full passes + + List results = new(length); + + for (int i = 0; i < length; i++) + { + TQuote q = quotes[i]; + (decimal? highPoint, decimal? lowPoint) = fractals[i]; + + decimal? hl = highLine[i]; + decimal? ll = lowLine[i]; + PivotTrend? ht = highTrend[i]; + PivotTrend? lt = lowTrend[i]; + + results.Add(new( + Timestamp: q.Timestamp, + HighPoint: highPoint, + LowPoint: lowPoint, + HighLine: hl, + LowLine: ll, + HighTrend: ht, + LowTrend: lt)); + } + + return results; + } +} diff --git a/src/m-r/Pivots/Pivots.Utilities.cs b/src/m-r/Pivots/Pivots.Utilities.cs index d791b91ee..4030ac0a0 100644 --- a/src/m-r/Pivots/Pivots.Utilities.cs +++ b/src/m-r/Pivots/Pivots.Utilities.cs @@ -1,12 +1,12 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// PIVOTS (UTILITIES) + +public static partial class Pivots { - // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -17,4 +17,31 @@ public static IEnumerable Condense( return resultsList.ToSortedList(); } + + // parameter validation + internal static void Validate( + int leftSpan, + int rightSpan, + int maxTrendPeriods, + string caller = "Pivots") + { + // check parameter arguments + if (rightSpan < 2) + { + throw new ArgumentOutOfRangeException(nameof(rightSpan), rightSpan, + $"Right span must be at least 2 for {caller}."); + } + + if (leftSpan < 2) + { + throw new ArgumentOutOfRangeException(nameof(leftSpan), leftSpan, + $"Left span must be at least 2 for {caller}."); + } + + if (maxTrendPeriods <= leftSpan) + { + throw new ArgumentOutOfRangeException(nameof(leftSpan), leftSpan, + $"Lookback periods must be greater than the Left window span for {caller}."); + } + } } diff --git a/src/m-r/Pmo/Pmo.Api.cs b/src/m-r/Pmo/Pmo.Api.cs deleted file mode 100644 index f4b990ba4..000000000 --- a/src/m-r/Pmo/Pmo.Api.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PRICE MOMENTUM OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetPmo( - this IEnumerable quotes, - int timePeriods = 35, - int smoothPeriods = 20, - int signalPeriods = 10) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcPmo(timePeriods, smoothPeriods, signalPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetPmo( - this IEnumerable results, - int timePeriods = 35, - int smoothPeriods = 20, - int signalPeriods = 10) => results - .ToTuple() - .CalcPmo(timePeriods, smoothPeriods, signalPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetPmo( - this IEnumerable<(DateTime, double)> priceTuples, - int timePeriods = 35, - int smoothPeriods = 20, - int signalPeriods = 10) => priceTuples - .ToSortedList() - .CalcPmo(timePeriods, smoothPeriods, signalPeriods); -} diff --git a/src/m-r/Pmo/Pmo.Models.cs b/src/m-r/Pmo/Pmo.Models.cs index c46c40236..422add37b 100644 --- a/src/m-r/Pmo/Pmo.Models.cs +++ b/src/m-r/Pmo/Pmo.Models.cs @@ -1,18 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class PmoResult : ResultBase, IReusableResult +public record PmoResult +( + DateTime Timestamp, + double? Pmo, + double? Signal +) : IReusable { - public PmoResult(DateTime date) - { - Date = date; - } - - public double? Pmo { get; set; } - public double? Signal { get; set; } - - // internal use only - internal double? RocEma { get; set; } - - double? IReusableResult.Value => Pmo; + public double Value => Pmo.Null2NaN(); } diff --git a/src/m-r/Pmo/Pmo.Series.cs b/src/m-r/Pmo/Pmo.Series.cs deleted file mode 100644 index f2fdc94bf..000000000 --- a/src/m-r/Pmo/Pmo.Series.cs +++ /dev/null @@ -1,155 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PRICE MOMENTUM OSCILLATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcPmo( - this List<(DateTime, double)> tpList, - int timePeriods, - int smoothPeriods, - int signalPeriods) - { - // check parameter arguments - ValidatePmo(timePeriods, smoothPeriods, signalPeriods); - - // initialize - List results = tpList.CalcPmoRocEma(timePeriods); - double smoothingConstant = 2d / smoothPeriods; - double? lastPmo = null; - - // calculate PMO - int startIndex = timePeriods + smoothPeriods; - - for (int i = startIndex - 1; i < results.Count; i++) - { - PmoResult pr = results[i]; - - if (i + 1 > startIndex) - { - pr.Pmo = ((pr.RocEma - lastPmo) * smoothingConstant) + lastPmo; - } - else if (i + 1 == startIndex) - { - double? sumRocEma = 0; - for (int p = i + 1 - smoothPeriods; p <= i; p++) - { - PmoResult d = results[p]; - sumRocEma += d.RocEma; - } - - pr.Pmo = sumRocEma / smoothPeriods; - } - - lastPmo = pr.Pmo; - } - - // add Signal - CalcPmoSignal(results, timePeriods, smoothPeriods, signalPeriods); - - return results; - } - - // internals - private static List CalcPmoRocEma( - this List<(DateTime, double)> tpList, - int timePeriods) - { - // initialize - double smoothingMultiplier = 2d / timePeriods; - double? lastRocEma = null; - List roc = tpList.CalcRoc(1, null).ToList(); - List results = []; - - int startIndex = timePeriods + 1; - - for (int i = 0; i < roc.Count; i++) - { - RocResult rocResult = roc[i]; - - PmoResult r = new(rocResult.Date); - results.Add(r); - - if (i + 1 > startIndex) - { - r.RocEma = (rocResult.Roc * smoothingMultiplier) + (lastRocEma * (1 - smoothingMultiplier)); - } - else if (i + 1 == startIndex) - { - double? sumRoc = 0; - for (int p = i + 1 - timePeriods; p <= i; p++) - { - RocResult d = roc[p]; - sumRoc += d.Roc; - } - - r.RocEma = sumRoc / timePeriods; - } - - lastRocEma = r.RocEma; - r.RocEma *= 10; - } - - return results; - } - - private static void CalcPmoSignal( - List results, - int timePeriods, - int smoothPeriods, - int signalPeriods) - { - double signalConstant = 2d / (signalPeriods + 1); - double? lastSignal = null; - - int startIndex = timePeriods + smoothPeriods + signalPeriods - 1; - - for (int i = startIndex - 1; i < results.Count; i++) - { - PmoResult pr = results[i]; - - if (i + 1 > startIndex) - { - pr.Signal = ((pr.Pmo - lastSignal) * signalConstant) + lastSignal; - } - else if (i + 1 == startIndex) - { - double? sumPmo = 0; - for (int p = i + 1 - signalPeriods; p <= i; p++) - { - PmoResult d = results[p]; - sumPmo += d.Pmo; - } - - pr.Signal = sumPmo / signalPeriods; - } - - lastSignal = pr.Signal; - } - } - - // parameter validation - private static void ValidatePmo( - int timePeriods, - int smoothPeriods, - int signalPeriods) - { - // check parameter arguments - if (timePeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(timePeriods), timePeriods, - "Time periods must be greater than 1 for PMO."); - } - - if (smoothPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smoothPeriods), smoothPeriods, - "Smoothing periods must be greater than 0 for PMO."); - } - - if (signalPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, - "Signal periods must be greater than 0 for PMO."); - } - } -} diff --git a/src/m-r/Pmo/Pmo.StaticSeries.cs b/src/m-r/Pmo/Pmo.StaticSeries.cs new file mode 100644 index 000000000..feaea3a64 --- /dev/null +++ b/src/m-r/Pmo/Pmo.StaticSeries.cs @@ -0,0 +1,112 @@ +namespace Skender.Stock.Indicators; + +// PRICE MOMENTUM OSCILLATOR (SERIES) + +public static partial class Pmo +{ + public static IReadOnlyList ToPmo( + this IReadOnlyList source, + int timePeriods = 35, + int smoothPeriods = 20, + int signalPeriods = 10) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(timePeriods, smoothPeriods, signalPeriods); + + // initialize + int length = source.Count; + List results = new(length); + double smoothingConstant1 = 2d / smoothPeriods; + double smoothingConstant2 = 2d / timePeriods; + double smoothingConstant3 = 2d / (signalPeriods + 1); + + double prevPrice = double.NaN; + double prevPmo = double.NaN; + double prevRocEma = double.NaN; + double prevSignal = double.NaN; + + double[] rc = new double[length]; // roc + double[] re = new double[length]; // roc ema + double[] pm = new double[length]; // pmo + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + // rate of change (ROC) + rc[i] = prevPrice == 0 ? double.NaN : 100 * ((s.Value / prevPrice) - 1); + prevPrice = s.Value; + + // ROC smoothed moving average + double rocEma; + + if (double.IsNaN(prevRocEma) && i >= timePeriods) + { + double sum = 0; + for (int p = i - timePeriods + 1; p <= i; p++) + { + sum += rc[p]; + } + rocEma = sum / timePeriods; + } + else + { + rocEma = prevRocEma + (smoothingConstant2 * (rc[i] - prevRocEma)); + } + + re[i] = rocEma * 10; + prevRocEma = rocEma; + + // price momentum oscillator + double pmo; + + if (double.IsNaN(prevPmo) && i >= smoothPeriods) + { + double sum = 0; + for (int p = i - smoothPeriods + 1; p <= i; p++) + { + sum += re[p]; + } + pmo = sum / smoothPeriods; + } + else + { + pmo = prevPmo + (smoothingConstant1 * (re[i] - prevPmo)); + } + + prevPmo = pm[i] = pmo; + + // add signal (EMA of PMO) + double signal; + + if (double.IsNaN(prevSignal) && i >= signalPeriods) + { + double sum = 0; + for (int p = i - signalPeriods + 1; p <= i; p++) + { + sum += pm[p]; + } + + signal = sum / signalPeriods; + } + else + { + signal = Ema.Increment(smoothingConstant3, prevSignal, pm[i]); + } + + PmoResult r = new( + Timestamp: s.Timestamp, + Pmo: pmo.NaN2Null(), + Signal: signal.NaN2Null()); + + results.Add(r); + + prevSignal = signal; + } + + return results; + } +} diff --git a/src/m-r/Pmo/Pmo.Utilities.cs b/src/m-r/Pmo/Pmo.Utilities.cs index ac67f96b7..3ca068f94 100644 --- a/src/m-r/Pmo/Pmo.Utilities.cs +++ b/src/m-r/Pmo/Pmo.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// PRICE MOMENTUM OSCILLATOR (UTILITIES) + +public static partial class Pmo { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int ts = results .ToList() @@ -14,4 +15,30 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(ts + 250); } + + // parameter validation + internal static void Validate( + int timePeriods, + int smoothPeriods, + int signalPeriods) + { + // check parameter arguments + if (timePeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(timePeriods), timePeriods, + "Time periods must be greater than 1 for PMO."); + } + + if (smoothPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(smoothPeriods), smoothPeriods, + "Smoothing periods must be greater than 0 for PMO."); + } + + if (signalPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, + "Signal periods must be greater than 0 for PMO."); + } + } } diff --git a/src/m-r/Prs/Prs.Api.cs b/src/m-r/Prs/Prs.Api.cs deleted file mode 100644 index 16bd36901..000000000 --- a/src/m-r/Prs/Prs.Api.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PRICE RELATIVE STRENGTH (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetPrs( - this IEnumerable quotesEval, - IEnumerable quotesBase, - int? lookbackPeriods = null, - int? smaPeriods = null) - where TQuote : IQuote - { - List<(DateTime, double)> tpListBase = quotesBase - .ToTuple(CandlePart.Close); - List<(DateTime, double)> tpListEval = quotesEval - .ToTuple(CandlePart.Close); - - return CalcPrs(tpListEval, tpListBase, lookbackPeriods, smaPeriods); - } - - // SERIES, from CHAINS (both inputs reusable) - public static IEnumerable GetPrs( - this IEnumerable quotesEval, - IEnumerable quotesBase, - int? lookbackPeriods = null, - int? smaPeriods = null) - { - List<(DateTime Date, double Value)> tpListEval - = quotesEval.ToTuple(); - - List<(DateTime Date, double Value)> tpListBase - = quotesBase.ToTuple(); - - return CalcPrs(tpListEval, tpListBase, lookbackPeriods, smaPeriods) - .SyncIndex(quotesEval, SyncType.Prepend); - } - - // SERIES, from TUPLE - public static IEnumerable GetPrs( - this IEnumerable<(DateTime, double)> tupleEval, - IEnumerable<(DateTime, double)> tupleBase, - int? lookbackPeriods = null, - int? smaPeriods = null) - { - List<(DateTime, double)> tpListBase = tupleBase.ToSortedList(); - List<(DateTime, double)> tpListEval = tupleEval.ToSortedList(); - - return CalcPrs(tpListEval, tpListBase, lookbackPeriods, smaPeriods); - } -} diff --git a/src/m-r/Prs/Prs.Models.cs b/src/m-r/Prs/Prs.Models.cs index 20c884fef..e565d7e56 100644 --- a/src/m-r/Prs/Prs.Models.cs +++ b/src/m-r/Prs/Prs.Models.cs @@ -1,16 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class PrsResult : ResultBase, IReusableResult +public record PrsResult +( + DateTime Timestamp, + double? Prs, + double? PrsPercent +) : IReusable { - public PrsResult(DateTime date) - { - Date = date; - } - - public double? Prs { get; set; } - public double? PrsSma { get; set; } - public double? PrsPercent { get; set; } - - double? IReusableResult.Value => Prs; + public double Value => Prs.Null2NaN(); } diff --git a/src/m-r/Prs/Prs.Series.cs b/src/m-r/Prs/Prs.Series.cs deleted file mode 100644 index c5aa072cc..000000000 --- a/src/m-r/Prs/Prs.Series.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PRICE RELATIVE STRENGTH (SERIES) -public static partial class Indicator -{ - internal static List CalcPrs( - List<(DateTime, double)> tpListEval, - List<(DateTime, double)> tpListBase, - int? lookbackPeriods = null, - int? smaPeriods = null) - { - // check parameter arguments - ValidatePriceRelative(tpListEval, tpListBase, lookbackPeriods, smaPeriods); - - // initialize - List results = new(tpListEval.Count); - - // roll through quotes - for (int i = 0; i < tpListEval.Count; i++) - { - (DateTime bDate, double bValue) = tpListBase[i]; - (DateTime eDate, double eValue) = tpListEval[i]; - - if (eDate != bDate) - { - throw new InvalidQuotesException(nameof(tpListEval), eDate, - "Date sequence does not match. Price Relative requires matching dates in provided histories."); - } - - PrsResult r = new(eDate) { - Prs = (bValue == 0) ? null : (eValue / bValue).NaN2Null() // relative strength ratio - }; - results.Add(r); - - if (lookbackPeriods != null && i + 1 > lookbackPeriods) - { - (DateTime _, double boValue) = tpListBase[i - (int)lookbackPeriods]; - (DateTime _, double eoValue) = tpListEval[i - (int)lookbackPeriods]; - - if (boValue != 0 && eoValue != 0) - { - double? pctB = (bValue - boValue) / boValue; - double? pctE = (eValue - eoValue) / eoValue; - - r.PrsPercent = (pctE - pctB).NaN2Null(); - } - } - - // optional moving average of PRS - if (smaPeriods != null && i + 1 >= smaPeriods) - { - double? sumRs = 0; - for (int p = i + 1 - (int)smaPeriods; p <= i; p++) - { - PrsResult d = results[p]; - sumRs += d.Prs; - } - - r.PrsSma = (sumRs / smaPeriods).NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidatePriceRelative( - List<(DateTime, double)> quotesEval, - List<(DateTime, double)> quotesBase, - int? lookbackPeriods, - int? smaPeriods) - { - // check parameter arguments - if (lookbackPeriods is not null and <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Price Relative Strength."); - } - - if (smaPeriods is not null and <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for Price Relative Strength."); - } - - // check quotes - int qtyHistoryEval = quotesEval.Count; - int qtyHistoryBase = quotesBase.Count; - - int? minHistory = lookbackPeriods; - if (minHistory != null && qtyHistoryEval < minHistory) - { - string message = "Insufficient quotes provided for Price Relative Strength. " + - string.Format( - invCulture, - "You provided {0} periods of quotes when at least {1} are required.", - qtyHistoryEval, minHistory); - - throw new InvalidQuotesException(nameof(quotesEval), message); - } - - if (qtyHistoryBase != qtyHistoryEval) - { - throw new InvalidQuotesException( - nameof(quotesBase), - "Base quotes should have at least as many records as Eval quotes for PRS."); - } - } -} diff --git a/src/m-r/Prs/Prs.StaticSeries.cs b/src/m-r/Prs/Prs.StaticSeries.cs new file mode 100644 index 000000000..40a6ab70d --- /dev/null +++ b/src/m-r/Prs/Prs.StaticSeries.cs @@ -0,0 +1,66 @@ +namespace Skender.Stock.Indicators; + +// PRICE RELATIVE STRENGTH (SERIES) + +public static partial class Prs +{ + public static IReadOnlyList ToPrs( + this IReadOnlyList sourceEval, + IReadOnlyList sourceBase, + int? lookbackPeriods = null) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(sourceEval); + ArgumentNullException.ThrowIfNull(sourceBase); + Validate(sourceEval, sourceBase, lookbackPeriods); + + // initialize + int length = sourceEval.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + T b = sourceBase[i]; + T e = sourceEval[i]; + + if (e.Timestamp != b.Timestamp) + { + throw new InvalidQuotesException( + nameof(sourceEval), e.Timestamp, + "Timestamp sequence does not match. " + + "Price Relative requires matching dates in provided histories."); + } + + double? prsPercent = null; + + if (lookbackPeriods is not null && i > lookbackPeriods - 1) + { + T bo = sourceBase[i - (int)lookbackPeriods]; + T eo = sourceEval[i - (int)lookbackPeriods]; + + if (bo.Value != 0 && eo.Value != 0) + { + double? pctB = (b.Value - bo.Value) / bo.Value; + double? pctE = (e.Value - eo.Value) / eo.Value; + + prsPercent = (pctE - pctB).NaN2Null(); + } + } + + PrsResult r = new( + Timestamp: e.Timestamp, + + Prs: b.Value == 0 + ? null + : (e.Value / b.Value).NaN2Null(), // relative strength ratio + + PrsPercent: prsPercent); + + results.Add(r); + } + + return results; + } +} diff --git a/src/m-r/Prs/Prs.Utilities.cs b/src/m-r/Prs/Prs.Utilities.cs new file mode 100644 index 000000000..497f2e649 --- /dev/null +++ b/src/m-r/Prs/Prs.Utilities.cs @@ -0,0 +1,49 @@ +using System.Globalization; + +namespace Skender.Stock.Indicators; + +// PRICE RELATIVE STRENGTH (UTILITIES) + +public static partial class Prs +{ + private static readonly CultureInfo invariantCulture = CultureInfo.InvariantCulture; + + // parameter validation + internal static void Validate( + IReadOnlyList quotesEval, + IReadOnlyList quotesBase, + int? lookbackPeriods) + where T : IReusable + { + // check parameter arguments + if (lookbackPeriods is <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Price Relative Strength."); + } + + // check quotes + int qtyHistoryEval = quotesEval.Count; + int qtyHistoryBase = quotesBase.Count; + + if (qtyHistoryEval < lookbackPeriods) + { + string message = "Insufficient quotes provided for Price Relative Strength. " + + string.Format( + invariantCulture, + "You provided {0} periods of quotes when at least {1} are required.", + qtyHistoryEval, lookbackPeriods); + + throw new InvalidQuotesException(nameof(quotesEval), message); + } + + if (qtyHistoryBase != qtyHistoryEval) + { + throw new InvalidQuotesException( + nameof(quotesBase), + "Base quotes should have at least as many records as Eval quotes for PRS."); + } + } + +} diff --git a/src/m-r/Prs/info.xml b/src/m-r/Prs/info.xml index e83fc1f16..e9a4e9e7c 100644 --- a/src/m-r/Prs/info.xml +++ b/src/m-r/Prs/info.xml @@ -16,7 +16,6 @@ Historical price quotes for evaluation. This is usually market index data, but could be any baseline data that you might use for comparison. Optional. Number of periods for % difference. - Optional. Number of periods for a PRS SMA signal line. Time series of PRS values. Invalid parameter value provided. Invalid quotes provided. diff --git a/src/m-r/Pvo/Pvo.Api.cs b/src/m-r/Pvo/Pvo.Api.cs deleted file mode 100644 index 158e82376..000000000 --- a/src/m-r/Pvo/Pvo.Api.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PRICE VOLUME OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetPvo( - this IEnumerable quotes, - int fastPeriods = 12, - int slowPeriods = 26, - int signalPeriods = 9) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Volume) - .CalcPvo(fastPeriods, slowPeriods, signalPeriods); - - // given that this is volume-based, other chaining is moot -} diff --git a/src/m-r/Pvo/Pvo.Models.cs b/src/m-r/Pvo/Pvo.Models.cs index 98e6966a8..39fb9966b 100644 --- a/src/m-r/Pvo/Pvo.Models.cs +++ b/src/m-r/Pvo/Pvo.Models.cs @@ -1,16 +1,13 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class PvoResult : ResultBase, IReusableResult +public record PvoResult +( + DateTime Timestamp, + double? Pvo, + double? Signal, + double? Histogram +) : IReusable { - public PvoResult(DateTime date) - { - Date = date; - } - - public double? Pvo { get; set; } - public double? Signal { get; set; } - public double? Histogram { get; set; } - - double? IReusableResult.Value => Pvo; + public double Value => Pvo.Null2NaN(); } diff --git a/src/m-r/Pvo/Pvo.Series.cs b/src/m-r/Pvo/Pvo.Series.cs deleted file mode 100644 index 29f831dce..000000000 --- a/src/m-r/Pvo/Pvo.Series.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PRICE VOLUME OSCILLATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcPvo( - this List<(DateTime, double)> tpList, - int fastPeriods, - int slowPeriods, - int signalPeriods) - { - // check parameter arguments - ValidatePvo(fastPeriods, slowPeriods, signalPeriods); - - // initialize - List emaFast = tpList.CalcEma(fastPeriods); - List emaSlow = tpList.CalcEma(slowPeriods); - - int length = tpList.Count; - List<(DateTime, double)> emaDiff = []; - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double _) = tpList[i]; - EmaResult df = emaFast[i]; - EmaResult ds = emaSlow[i]; - - PvoResult r = new(date); - results.Add(r); - - if (i >= slowPeriods - 1) - { - double? pvo = (ds.Ema != 0) ? - 100 * ((df.Ema - ds.Ema) / ds.Ema) : null; - - r.Pvo = pvo; - - // temp data for interim EMA of PVO - (DateTime, double) diff = (date, (pvo == null) ? 0 : (double)pvo); - - emaDiff.Add(diff); - } - } - - // add signal and histogram to result - List emaSignal = CalcEma(emaDiff, signalPeriods); - - for (int d = slowPeriods - 1; d < length; d++) - { - PvoResult r = results[d]; - EmaResult ds = emaSignal[d + 1 - slowPeriods]; - - r.Signal = ds.Ema; - r.Histogram = r.Pvo - r.Signal; - } - - return results; - } - - // parameter validation - private static void ValidatePvo( - int fastPeriods, - int slowPeriods, - int signalPeriods) - { - // check parameter arguments - if (fastPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, - "Fast periods must be greater than 0 for PVO."); - } - - if (signalPeriods < 0) - { - throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, - "Signal periods must be greater than or equal to 0 for PVO."); - } - - if (slowPeriods <= fastPeriods) - { - throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, - "Slow periods must be greater than the fast period for PVO."); - } - } -} diff --git a/src/m-r/Pvo/Pvo.StaticSeries.cs b/src/m-r/Pvo/Pvo.StaticSeries.cs new file mode 100644 index 000000000..4f02630e7 --- /dev/null +++ b/src/m-r/Pvo/Pvo.StaticSeries.cs @@ -0,0 +1,120 @@ +namespace Skender.Stock.Indicators; + +// PRICE VOLUME OSCILLATOR (SERIES) + +public static partial class Pvo +{ + public static IReadOnlyList ToPvo( + this IReadOnlyList quotes, + int fastPeriods = 12, + int slowPeriods = 26, + int signalPeriods = 9) + where TQuote : IQuote => quotes + .Use(CandlePart.Volume) + .CalcPvo(fastPeriods, slowPeriods, signalPeriods); + + private static List CalcPvo( + this IReadOnlyList source, // volume + int fastPeriods, + int slowPeriods, + int signalPeriods) + where T : IReusable + { + // check parameter arguments + Validate(fastPeriods, slowPeriods, signalPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double lastEmaFast = double.NaN; + double lastEmaSlow = double.NaN; + double lastEmaPvo = double.NaN; + + double kFast = 2d / (fastPeriods + 1); + double kSlow = 2d / (slowPeriods + 1); + double kPvo = 2d / (signalPeriods + 1); + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + // re-initialize Fast EMA + double emaFast; + + if (double.IsNaN(lastEmaFast) && i >= fastPeriods - 1) + { + double sum = 0; + for (int p = i - fastPeriods + 1; p <= i; p++) + { + T ps = source[p]; + sum += ps.Value; + } + + emaFast = sum / fastPeriods; + } + else + { + emaFast = Ema.Increment(kFast, lastEmaFast, s.Value); + } + + // re-initialize Slow EMA + double emaSlow; + + if (double.IsNaN(lastEmaSlow) && i >= slowPeriods - 1) + { + double sum = 0; + for (int p = i - slowPeriods + 1; p <= i; p++) + { + T ps = source[p]; + sum += ps.Value; + } + + emaSlow = sum / slowPeriods; + } + else + { + emaSlow = Ema.Increment(kSlow, lastEmaSlow, s.Value); + } + + double pvo = emaSlow != 0 ? + 100 * ((emaFast - emaSlow) / emaSlow) : double.NaN; + + // re-initialize Signal EMA + double signal; + + if (double.IsNaN(lastEmaPvo) && i >= signalPeriods + slowPeriods - 2) + { + double sum = pvo; + for (int p = i - signalPeriods + 1; p < i; p++) + { + sum += ((IReusable)results[p]).Value; + } + + signal = sum / signalPeriods; + } + else + { + signal = Ema.Increment(kPvo, lastEmaPvo, pvo); + } + + // write results + results.Add(new( + Timestamp: s.Timestamp, + Pvo: pvo.NaN2Null(), + Signal: signal.NaN2Null(), + Histogram: (pvo - signal).NaN2Null())); + + lastEmaPvo = signal; + lastEmaFast = emaFast; + lastEmaSlow = emaSlow; + } + + return results; + } + + /* DESIGN NOTE: this is exactly like MACD, except for: + * a) it uses Volume instead of Price (see API) + * b) the PVO calculation slightly different */ +} diff --git a/src/m-r/Pvo/Pvo.Utilities.cs b/src/m-r/Pvo/Pvo.Utilities.cs index 639d42d90..3731ccb5d 100644 --- a/src/m-r/Pvo/Pvo.Utilities.cs +++ b/src/m-r/Pvo/Pvo.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// PRICE VOLUME OSCILLATOR (UTILITIES) + +public static partial class Pvo { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +15,30 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(n + 250); } + + // parameter validation + internal static void Validate( + int fastPeriods, + int slowPeriods, + int signalPeriods) + { + // check parameter arguments + if (fastPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, + "Fast periods must be greater than 0 for PVO."); + } + + if (signalPeriods < 0) + { + throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, + "Signal periods must be greater than or equal to 0 for PVO."); + } + + if (slowPeriods <= fastPeriods) + { + throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, + "Slow periods must be greater than the fast period for PVO."); + } + } } diff --git a/src/m-r/Renko/Renko.Api.cs b/src/m-r/Renko/Renko.Api.cs deleted file mode 100644 index 07ea95f0c..000000000 --- a/src/m-r/Renko/Renko.Api.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RENKO CHART - STANDARD (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetRenko( - this IEnumerable quotes, - decimal brickSize, - EndType endType = EndType.Close) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcRenko(brickSize, endType); -} diff --git a/src/m-r/Renko/Renko.Models.cs b/src/m-r/Renko/Renko.Models.cs index a85e45c78..d8fe874d3 100644 --- a/src/m-r/Renko/Renko.Models.cs +++ b/src/m-r/Renko/Renko.Models.cs @@ -1,17 +1,14 @@ namespace Skender.Stock.Indicators; +/// [Serializable] -public sealed class RenkoResult : ResultBase, IQuote -{ - public RenkoResult(DateTime date) - { - Date = date; - } - - 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 bool IsUp { get; set; } -} +public record RenkoResult +( + DateTime Timestamp, + decimal Open, + decimal High, + decimal Low, + decimal Close, + decimal Volume, + bool IsUp +) : Quote(Timestamp, Open, High, Low, Close, Volume); diff --git a/src/m-r/Renko/Renko.Series.cs b/src/m-r/Renko/Renko.Series.cs deleted file mode 100644 index 0d27477b8..000000000 --- a/src/m-r/Renko/Renko.Series.cs +++ /dev/null @@ -1,156 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RENKO CHART - STANDARD (SERIES) -public static partial class Indicator -{ - internal static List CalcRenko( - this List quotesList, - decimal brickSize, - EndType endType) - where TQuote : IQuote - { - // check parameter arguments - ValidateRenko(brickSize); - - // initialize - int length = quotesList.Count; - List results = new(length); - TQuote q0; - - if (length == 0) - { - return results; - } - else - { - q0 = quotesList[0]; - } - - bool resetHLV = true; - int decimals = brickSize.GetDecimalPlaces(); - decimal baseline = Math.Round(q0.Close, Math.Max(decimals - 1, 0)); - - decimal h = decimal.MinValue; - decimal l = decimal.MaxValue; - decimal v = 0; - - RenkoResult lastBrick = new(q0.Date) { - Open = baseline, - Close = baseline - }; - - // roll through quotes - for (int i = 1; i < length; i++) - { - TQuote q = quotesList[i]; - - // accumulate brick info - if (resetHLV) - { - // reset - h = q.High; - l = q.Low; - v = q.Volume; - } - else - { - h = q.High > h ? q.High : h; - l = q.Low < l ? q.Low : l; - v += q.Volume; - } - - // determine if new brick threshold is met - int newBrickQty = GetNewBricks(endType, q, lastBrick, brickSize); - int absQty = Math.Abs(newBrickQty); - - // add new brick(s) - // can add more than one brick! - for (int b = 0; b < absQty; b++) - { - decimal c; - bool isUp = newBrickQty >= 0; - - if (newBrickQty > 0) - { - baseline = Math.Max(lastBrick.Open, lastBrick.Close); - c = baseline + brickSize; - } - else - { - baseline = Math.Min(lastBrick.Open, lastBrick.Close); - c = baseline - brickSize; - } - - RenkoResult r = new(q.Date) { - Open = baseline, - High = h, - Low = l, - Close = c, - Volume = v / absQty, - IsUp = isUp - }; - results.Add(r); - lastBrick = r; - } - - // init next brick(s) - resetHLV = absQty != 0; - } - - return results; - } - - // calculate brick size - private static int GetNewBricks( - EndType endType, - TQuote q, - RenkoResult lastBrick, - decimal brickSize) - where TQuote : IQuote - { - int bricks; - decimal upper = Math.Max(lastBrick.Open, lastBrick.Close); - decimal lower = Math.Min(lastBrick.Open, lastBrick.Close); - - switch (endType) - { - case EndType.Close: - - bricks = q.Close > upper - ? (int)((q.Close - upper) / brickSize) - : q.Close < lower - ? (int)((q.Close - lower) / brickSize) - : 0; - - break; - - case EndType.HighLow: - - // high/low assumption: absolute greater diff wins - // --> does not consider close direction - - decimal hQty = (q.High - upper) / brickSize; - decimal lQty = (lower - q.Low) / brickSize; - - bricks = (int)((hQty >= lQty) ? hQty : -lQty); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(endType)); - } - - return bricks; - } - - // parameter validation - private static void ValidateRenko( - decimal brickSize) - { - // check parameter arguments - if (brickSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(brickSize), brickSize, - "Brick size must be greater than 0 for Renko Charts."); - } - } -} diff --git a/src/m-r/Renko/Renko.StaticSeries.cs b/src/m-r/Renko/Renko.StaticSeries.cs new file mode 100644 index 000000000..6aaa45bc5 --- /dev/null +++ b/src/m-r/Renko/Renko.StaticSeries.cs @@ -0,0 +1,94 @@ +namespace Skender.Stock.Indicators; + +// RENKO CHART (SERIES) + +public static partial class Renko +{ + public static IReadOnlyList ToRenko( + this IReadOnlyList quotes, + decimal brickSize, + EndType endType = EndType.Close) + where TQuote : IQuote + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(quotes); + Validate(brickSize); + + // initialize + int length = quotes.Count; + List results = new(length); + + if (length == 0) + { + return results; + } + + // first brick baseline + TQuote q0 = quotes[0]; + + int decimals = brickSize.GetDecimalPlaces(); + decimal baseline = Math.Round(q0.Close, Math.Max(decimals - 1, 0)); + + RenkoResult lastBrick = new( + q0.Timestamp, + Open: baseline, 0, 0, + Close: baseline, 0, false); + + // initialize high/low/volume tracking + decimal h = decimal.MinValue; + decimal l = decimal.MaxValue; + decimal sumV = 0; // cumulative + + // roll through source values + for (int i = 1; i < length; i++) + { + TQuote q = quotes[i]; + + // track high/low/volume between bricks + h = Math.Max(h, q.High); + l = Math.Min(l, q.Low); + sumV += q.Volume; + + // determine new brick quantity + int newBrickQty = GetNewBrickQuantity(q, lastBrick, brickSize, endType); + int absBrickQty = Math.Abs(newBrickQty); + bool isUp = newBrickQty >= 0; + + // add new brick(s) + // can add more than one brick! + for (int b = 0; b < absBrickQty; b++) + { + decimal o; + decimal c; + decimal v = sumV / absBrickQty; + + if (isUp) + { + o = Math.Max(lastBrick.Open, lastBrick.Close); + c = o + brickSize; + } + else + { + o = Math.Min(lastBrick.Open, lastBrick.Close); + c = o - brickSize; + } + + RenkoResult r + = new(q.Timestamp, o, h, l, c, v, isUp); + + results.Add(r); + lastBrick = r; + } + + // reset high/low/volume tracking + if (absBrickQty != 0) + { + h = decimal.MinValue; + l = decimal.MaxValue; + sumV = 0; + } + } + + return results; + } +} diff --git a/src/m-r/Renko/Renko.StreamHub.cs b/src/m-r/Renko/Renko.StreamHub.cs new file mode 100644 index 000000000..8486bf51f --- /dev/null +++ b/src/m-r/Renko/Renko.StreamHub.cs @@ -0,0 +1,200 @@ +namespace Skender.Stock.Indicators; + +// RENKO CHART (STREAM HUB) + +#region hub interface and initializer + +public interface IRenkoHub +{ + decimal BrickSize { get; } + EndType EndType { get; } +} + +public static partial class Renko +{ + public static RenkoHub ToRenko( + this IQuoteProvider quoteProvider, + decimal brickSize, + EndType endType = EndType.Close) + where TIn : IQuote + => new(quoteProvider, brickSize, endType); +} +#endregion + +public class RenkoHub + : QuoteProvider, IRenkoHub + where TIn : IQuote +{ + #region constructors + + private readonly string hubName; + + private RenkoResult lastBrick + = new(default, default, default, + default, default, default, default); + + internal RenkoHub( + IQuoteProvider provider, + decimal brickSize, + EndType endType) : base(provider) + { + Renko.Validate(brickSize); + BrickSize = brickSize; + EndType = endType; + hubName = $"RENKO({brickSize},{endType.ToString().ToUpperInvariant()})"; + + Reinitialize(); + } + #endregion + + /// + /// Renko hub settings. Since it can produce 0 or many bricks per quote, + /// the default 1:1 in/out is not used and must be skipped to prevent + /// same-date triggerred rebuilds when caching. + /// + public override BinarySettings Properties { get; init; } = new(0b00000010); // custom + + /// + /// Standard brick size for Renko chart. + /// + public decimal BrickSize { get; } + + /// + /// Close or High/Low price used to determine when threshold + /// is met to generate new bricks. + /// + public EndType EndType { get; } + + // METHODS + + public override string ToString() => hubName; + + public override void OnAdd(TIn item, bool notify, int? indexHint) + => ToIndicator(item, notify, indexHint); + + protected override (RenkoResult result, int index) + ToIndicator(TIn item, int? indexHint) + => throw new InvalidOperationException(); // not used + + // TODO: see if returning array of results is possible ^^ + // for all indicators, so we don't have to do this goofy override + + /// + /// Restore last brick marker. + /// + /// + protected override void RollbackState(DateTime timestamp) + { + // restore last brick marker + if (Cache.Count != 0) + { + lastBrick = Cache + .Last(c => c.Timestamp <= timestamp); + + return; + } + + // skip first quote + if (ProviderCache.Count <= 1) + { + return; + } + + SetBaselineBrick(); + } + + // re/initialize last brick marker + private void SetBaselineBrick() + { + int decimals = BrickSize.GetDecimalPlaces(); + + TIn q0 = ProviderCache[0]; + + decimal baseline + = Math.Round(q0.Close, + Math.Max(decimals - 1, 0)); + + lastBrick = new( + q0.Timestamp, + Open: baseline, + High: 0, + Low: 0, + Close: baseline, + Volume: 0, + IsUp: false); + } + + // custom: build 0 to many bricks per quote + private void ToIndicator(TIn item, bool notify, int? indexHint) + { + int providerIndex = indexHint + ?? throw new InvalidOperationException($"{nameof(indexHint)} cannot be empty"); + + // nothing to do + if (providerIndex <= 0) + { + return; + } + + // establish baseline brick + if (providerIndex == 1) + { + SetBaselineBrick(); + } + + // determine new brick quantity + int newBrickQty + = Renko.GetNewBrickQuantity( + item, lastBrick, BrickSize, EndType); + + int absBrickQty = Math.Abs(newBrickQty); + bool isUp = newBrickQty >= 0; + + // add new brick(s) ... can add more than one! + if (absBrickQty > 0) + { + // get high/low/volume between bricks + decimal h = decimal.MinValue; + decimal l = decimal.MaxValue; + decimal sumV = 0; // cumulative + + // by aggregating provider cache range + int lastBrickIndex = ProviderCache.GetIndex(lastBrick.Timestamp, true); + + for (int w = lastBrickIndex + 1; w <= providerIndex; w++) + { + TIn pq = ProviderCache[w]; + + h = Math.Max(h, pq.High); + l = Math.Min(l, pq.Low); + sumV += pq.Volume; + } + + decimal v = sumV / absBrickQty; + + for (int b = 0; b < absBrickQty; b++) + { + decimal o = isUp + ? Math.Max(lastBrick.Open, lastBrick.Close) + : Math.Min(lastBrick.Open, lastBrick.Close); + + decimal c = isUp + ? o + BrickSize + : o - BrickSize; + + // candidate result + RenkoResult r + = new(item.Timestamp, o, h, l, c, v, isUp); + + lastBrick = r; + + // save and send + AppendCache(r, notify); + + // note: bypass rebuild bit set in Properties to allow + // sequential bricks with duplicate dates that would + // normally trigger rebuild, causing stack overflow. + } + } + } +} diff --git a/src/m-r/Renko/Renko.Utilities.cs b/src/m-r/Renko/Renko.Utilities.cs new file mode 100644 index 000000000..d8463910b --- /dev/null +++ b/src/m-r/Renko/Renko.Utilities.cs @@ -0,0 +1,63 @@ +namespace Skender.Stock.Indicators; + +// RENKO CHART (UTILITIES) + +public static partial class Renko +{ + // calculate brick size + internal static int GetNewBrickQuantity( + TQuote q, + RenkoResult lastBrick, + decimal brickSize, + EndType endType) + where TQuote : IQuote + { + int brickQuantity; + decimal upper = Math.Max(lastBrick.Open, lastBrick.Close); + decimal lower = Math.Min(lastBrick.Open, lastBrick.Close); + + switch (endType) + { + case EndType.Close: + + brickQuantity + = q.Close > upper + ? (int)((q.Close - upper) / brickSize) + : q.Close < lower + ? (int)((q.Close - lower) / brickSize) + : 0; + + break; + + case EndType.HighLow: + + // high/low assumption: absolute greater diff wins + // --> does not consider close direction + + decimal hQty = (q.High - upper) / brickSize; + decimal lQty = (lower - q.Low) / brickSize; + + brickQuantity = (int)(hQty >= lQty ? hQty : -lQty); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(endType)); + } + + return brickQuantity; + } + + // parameter validation + internal static void Validate( + decimal brickSize) + { + // check parameter arguments + if (brickSize <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(brickSize), brickSize, + "Brick size must be greater than 0 for Renko Charts."); + } + } + +} diff --git a/src/m-r/Renko/RenkoAtr.Api.cs b/src/m-r/Renko/RenkoAtr.Api.cs deleted file mode 100644 index 38f9e894e..000000000 --- a/src/m-r/Renko/RenkoAtr.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RENKO CHART - ATR (API) -public static partial class Indicator -{ - /// - /// - public static IEnumerable GetRenkoAtr( - this IEnumerable quotes, - int atrPeriods, - EndType endType = EndType.Close) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcRenkoAtr(atrPeriods, endType); -} diff --git a/src/m-r/Renko/RenkoAtr.Series.cs b/src/m-r/Renko/RenkoAtr.Series.cs deleted file mode 100644 index 73323db2f..000000000 --- a/src/m-r/Renko/RenkoAtr.Series.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Skender.Stock.Indicators; - -public static partial class Indicator -{ - // RENKO CHART - ATR (SERIES) - internal static List CalcRenkoAtr( - this List quotesList, - int atrPeriods, - EndType endType = EndType.Close) - where TQuote : IQuote - { - // initialize - List atrResults = quotesList - .ToQuoteD() - .CalcAtr(atrPeriods); - - double? atr = atrResults.LastOrDefault()?.Atr; - decimal brickSize = (atr == null) ? 0 : (decimal)atr; - - return brickSize is 0 ? - [] - : quotesList.CalcRenko(brickSize, endType); - } -} diff --git a/src/m-r/RenkoAtr/RenkoAtr.StaticSeries.cs b/src/m-r/RenkoAtr/RenkoAtr.StaticSeries.cs new file mode 100644 index 000000000..479278122 --- /dev/null +++ b/src/m-r/RenkoAtr/RenkoAtr.StaticSeries.cs @@ -0,0 +1,25 @@ +namespace Skender.Stock.Indicators; + +// RENKO CHART - ATR (SERIES) + +public static partial class Renko +{ + public static IReadOnlyList GetRenkoAtr( + this IReadOnlyList quotes, + int atrPeriods, + EndType endType = EndType.Close) + where TQuote : IQuote + { + // initialize + List atrResults = quotes + .ToQuoteDList() + .CalcAtr(atrPeriods); + + AtrResult? last = atrResults.LastOrDefault(); + decimal brickSize = (decimal?)last?.Atr ?? 0; + + return brickSize == 0 + ? [] + : quotes.ToRenko(brickSize, endType); + } +} diff --git a/src/m-r/Roc/Roc.Api.cs b/src/m-r/Roc/Roc.Api.cs deleted file mode 100644 index 86bdd081f..000000000 --- a/src/m-r/Roc/Roc.Api.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RATE OF CHANGE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetRoc( - this IEnumerable quotes, - int lookbackPeriods, - int? smaPeriods = null) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcRoc(lookbackPeriods, smaPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetRoc( - this IEnumerable results, - int lookbackPeriods, - int? smaPeriods = null) => results - .ToTuple() - .CalcRoc(lookbackPeriods, smaPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetRoc( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods, - int? smaPeriods = null) => priceTuples - .ToSortedList() - .CalcRoc(lookbackPeriods, smaPeriods); -} diff --git a/src/m-r/Roc/Roc.Models.cs b/src/m-r/Roc/Roc.Models.cs index 8a060ff3a..c7040df8d 100644 --- a/src/m-r/Roc/Roc.Models.cs +++ b/src/m-r/Roc/Roc.Models.cs @@ -1,16 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class RocResult : ResultBase, IReusableResult +public record RocResult +( + DateTime Timestamp, + double? Momentum, + double? Roc +) : IReusable { - public RocResult(DateTime date) - { - Date = date; - } - - public double? Momentum { get; set; } - public double? Roc { get; set; } - public double? RocSma { get; set; } - - double? IReusableResult.Value => Roc; + public double Value => Roc.Null2NaN(); } diff --git a/src/m-r/Roc/Roc.Series.cs b/src/m-r/Roc/Roc.Series.cs deleted file mode 100644 index d6781bacd..000000000 --- a/src/m-r/Roc/Roc.Series.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RATE OF CHANGE (SERIES) -public static partial class Indicator -{ - internal static List CalcRoc( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - int? smaPeriods) - { - // check parameter arguments - ValidateRoc(lookbackPeriods, smaPeriods); - - // initialize - List results = new(tpList.Count); - - // roll through quotes - for (int i = 0; i < tpList.Count; i++) - { - (DateTime date, double value) = tpList[i]; - - RocResult r = new(date); - results.Add(r); - - if (i + 1 > lookbackPeriods) - { - (DateTime _, double backValue) = tpList[i - lookbackPeriods]; - - r.Momentum = (value - backValue).NaN2Null(); - r.Roc = (backValue == 0) ? null - : (100d * r.Momentum / backValue).NaN2Null(); - } - - // optional SMA - if (smaPeriods != null && i >= lookbackPeriods + smaPeriods - 1) - { - double? sumSma = 0; - for (int p = i + 1 - (int)smaPeriods; p <= i; p++) - { - sumSma += results[p].Roc; - } - - r.RocSma = sumSma / smaPeriods; - } - } - - return results; - } - - // parameter validation - private static void ValidateRoc( - int lookbackPeriods, - int? smaPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for ROC."); - } - - if (smaPeriods is not null and <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for ROC."); - } - } -} diff --git a/src/m-r/Roc/Roc.StaticSeries.cs b/src/m-r/Roc/Roc.StaticSeries.cs new file mode 100644 index 000000000..aacfbf368 --- /dev/null +++ b/src/m-r/Roc/Roc.StaticSeries.cs @@ -0,0 +1,54 @@ +namespace Skender.Stock.Indicators; + +// RATE OF CHANGE (SERIES) + +public static partial class Roc +{ + public static IReadOnlyList ToRoc( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + double roc; + double momentum; + + if (i + 1 > lookbackPeriods) + { + T back = source[i - lookbackPeriods]; + + momentum = s.Value - back.Value; + + roc = back.Value == 0 + ? double.NaN + : 100d * momentum / back.Value; + } + else + { + momentum = double.NaN; + roc = double.NaN; + } + + RocResult r = new( + Timestamp: s.Timestamp, + Momentum: momentum.NaN2Null(), + Roc: roc.NaN2Null()); + + results.Add(r); + } + + return results; + } +} diff --git a/src/m-r/Roc/Roc.Utilities.cs b/src/m-r/Roc/Roc.Utilities.cs index 473586d9d..2256c42ec 100644 --- a/src/m-r/Roc/Roc.Utilities.cs +++ b/src/m-r/Roc/Roc.Utilities.cs @@ -1,17 +1,18 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// RATE OF CHANGE (UTILITIES) + +public static partial class Roc { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Roc != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for ROC."); + } } } diff --git a/src/m-r/Roc/info.xml b/src/m-r/Roc/info.xml index b9a05cf9f..c1021b2dd 100644 --- a/src/m-r/Roc/info.xml +++ b/src/m-r/Roc/info.xml @@ -14,7 +14,6 @@ Configurable Quote type. See Guide for more information. Historical price quotes. Number of periods in the lookback window. - Optional. Number of periods for an ROC SMA signal line. Time series of ROC values. Invalid parameter value provided. diff --git a/src/m-r/RocWb/Roc.Models.cs b/src/m-r/RocWb/Roc.Models.cs deleted file mode 100644 index 3b46dd0b5..000000000 --- a/src/m-r/RocWb/Roc.Models.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Skender.Stock.Indicators; - -[Serializable] -public sealed class RocWbResult : ResultBase, IReusableResult -{ - public RocWbResult(DateTime date) - { - Date = date; - } - - public double? Roc { get; set; } - public double? RocEma { get; set; } - public double? UpperBand { get; set; } - public double? LowerBand { get; set; } - - double? IReusableResult.Value => Roc; -} diff --git a/src/m-r/RocWb/RocWb.Api.cs b/src/m-r/RocWb/RocWb.Api.cs deleted file mode 100644 index 5b95ea1b3..000000000 --- a/src/m-r/RocWb/RocWb.Api.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RATE OF CHANGE (ROC) WITH BANDS (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetRocWb( - this IEnumerable quotes, - int lookbackPeriods, - int emaPeriods, - int stdDevPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcRocWb(lookbackPeriods, emaPeriods, stdDevPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetRocWb( - this IEnumerable results, - int lookbackPeriods, - int emaPeriods, - int stdDevPeriods) => results - .ToTuple() - .CalcRocWb(lookbackPeriods, emaPeriods, stdDevPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetRocWb( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods, - int emaPeriods, - int stdDevPeriods) => priceTuples - .ToSortedList() - .CalcRocWb(lookbackPeriods, emaPeriods, stdDevPeriods); -} diff --git a/src/m-r/RocWb/RocWb.Models.cs b/src/m-r/RocWb/RocWb.Models.cs new file mode 100644 index 000000000..6ed030dbb --- /dev/null +++ b/src/m-r/RocWb/RocWb.Models.cs @@ -0,0 +1,14 @@ +namespace Skender.Stock.Indicators; + +[Serializable] +public record RocWbResult +( + DateTime Timestamp, + double? Roc, + double? RocEma, + double? UpperBand, + double? LowerBand +) : IReusable +{ + public double Value => Roc.Null2NaN(); +} diff --git a/src/m-r/RocWb/RocWb.Series.cs b/src/m-r/RocWb/RocWb.Series.cs deleted file mode 100644 index 864c7bf8b..000000000 --- a/src/m-r/RocWb/RocWb.Series.cs +++ /dev/null @@ -1,107 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RATE OF CHANGE (ROC) WITH BANDS (SERIES) -public static partial class Indicator -{ - internal static List CalcRocWb( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - int emaPeriods, - int stdDevPeriods) - { - // check parameter arguments - ValidateRocWb(lookbackPeriods, emaPeriods, stdDevPeriods); - - // initialize - List results = tpList - .CalcRoc(lookbackPeriods, null) - .Select(x => new RocWbResult(x.Date) { - Roc = x.Roc - }) - .ToList(); - - double k = 2d / (emaPeriods + 1); - double? lastEma = 0; - - int length = results.Count; - - if (length > lookbackPeriods) - { - int initPeriods = Math.Min(lookbackPeriods + emaPeriods, length); - - for (int i = lookbackPeriods; i < initPeriods; i++) - { - lastEma += results[i].Roc; - } - - lastEma /= emaPeriods; - } - - double?[] rocSq = results - .Select(x => x.Roc * x.Roc) - .ToArray(); - - // roll through quotes - for (int i = lookbackPeriods; i < length; i++) - { - RocWbResult r = results[i]; - - // exponential moving average - if (i + 1 > lookbackPeriods + emaPeriods) - { - r.RocEma = lastEma + (k * (r.Roc - lastEma)); - lastEma = r.RocEma; - } - else if (i + 1 == lookbackPeriods + emaPeriods) - { - r.RocEma = lastEma; - } - - // ROC deviation - if (i + 1 >= lookbackPeriods + stdDevPeriods) - { - double? sumSq = 0; - for (int p = i - stdDevPeriods + 1; p <= i; p++) - { - sumSq += rocSq[p]; - } - - if (sumSq is not null) - { - double? rocDev = Math.Sqrt((double)sumSq / stdDevPeriods); - - r.UpperBand = rocDev; - r.LowerBand = -rocDev; - } - } - } - - return results; - } - - // parameter validation - private static void ValidateRocWb( - int lookbackPeriods, - int emaPeriods, - int stdDevPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for ROC with Bands."); - } - - if (emaPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(emaPeriods), emaPeriods, - "EMA periods must be greater than 0 for ROC."); - } - - if (stdDevPeriods <= 0 || stdDevPeriods > lookbackPeriods) - { - throw new ArgumentOutOfRangeException(nameof(stdDevPeriods), stdDevPeriods, - "Standard Deviation periods must be greater than 0 and less than lookback period for ROC with Bands."); - } - } -} diff --git a/src/m-r/RocWb/RocWb.StaticSeries.cs b/src/m-r/RocWb/RocWb.StaticSeries.cs new file mode 100644 index 000000000..5fa46b88f --- /dev/null +++ b/src/m-r/RocWb/RocWb.StaticSeries.cs @@ -0,0 +1,83 @@ +namespace Skender.Stock.Indicators; + +// RATE OF CHANGE (ROC) WITH BANDS (SERIES) + +public static partial class RocWb +{ + public static IReadOnlyList ToRocWb( + this IReadOnlyList source, + int lookbackPeriods, + int emaPeriods, + int stdDevPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods, emaPeriods, stdDevPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double k = 2d / (emaPeriods + 1); + double prevEma = double.NaN; + + IReadOnlyList ogRoc = source + .ToRoc(lookbackPeriods); + + double[] rocSq = ogRoc + .Select(x => x.Value * x.Value) + .ToArray(); + + double[] ema = new double[length]; + + // roll through results + for (int i = 0; i < length; i++) + { + IReusable roc = ogRoc[i]; + + // exponential moving average + if (double.IsNaN(prevEma) && i >= emaPeriods) + { + double sum = 0; + for (int p = i - emaPeriods + 1; p <= i; p++) + { + sum += ogRoc[p].Value; + } + + ema[i] = sum / emaPeriods; + } + + // normal EMA + else + { + ema[i] = Ema.Increment(k, prevEma, roc.Value); + } + + prevEma = ema[i]; + + // ROC deviation + double? rocDev = null; + + if (i >= stdDevPeriods) + { + double sum = 0; + for (int p = i - stdDevPeriods + 1; p <= i; p++) + { + sum += rocSq[p]; + } + + rocDev = Math.Sqrt(sum / stdDevPeriods).NaN2Null(); + } + + results.Add(new( + Timestamp: roc.Timestamp, + Roc: roc.Value.NaN2Null(), + RocEma: ema[i].NaN2Null(), + UpperBand: rocDev, + LowerBand: -rocDev)); + } + + return results; + } +} diff --git a/src/m-r/RocWb/RocWb.Utilities.cs b/src/m-r/RocWb/RocWb.Utilities.cs index be2323824..3b94d7257 100644 --- a/src/m-r/RocWb/RocWb.Utilities.cs +++ b/src/m-r/RocWb/RocWb.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// RATE OF CHANGE (ROC) WITH BANDS (UTILITIES) + +public static partial class RocWb { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +15,30 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(n + 100); } + + // parameter validation + internal static void Validate( + int lookbackPeriods, + int emaPeriods, + int stdDevPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for ROC with Bands."); + } + + if (emaPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(emaPeriods), emaPeriods, + "EMA periods must be greater than 0 for ROC."); + } + + if (stdDevPeriods <= 0 || stdDevPeriods > lookbackPeriods) + { + throw new ArgumentOutOfRangeException(nameof(stdDevPeriods), stdDevPeriods, + "Standard Deviation periods must be greater than 0 and less than lookback period for ROC with Bands."); + } + } } diff --git a/src/m-r/RollingPivots/RollingPivots.Api.cs b/src/m-r/RollingPivots/RollingPivots.Api.cs deleted file mode 100644 index 0c0a4807f..000000000 --- a/src/m-r/RollingPivots/RollingPivots.Api.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ROLLING PIVOT POINTS (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetRollingPivots( - this IEnumerable quotes, - int windowPeriods, - int offsetPeriods, - PivotPointType pointType = PivotPointType.Standard) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcRollingPivots(windowPeriods, offsetPeriods, pointType); -} diff --git a/src/m-r/RollingPivots/RollingPivots.Models.cs b/src/m-r/RollingPivots/RollingPivots.Models.cs index b1b49434c..c5782f236 100644 --- a/src/m-r/RollingPivots/RollingPivots.Models.cs +++ b/src/m-r/RollingPivots/RollingPivots.Models.cs @@ -1,15 +1,19 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class RollingPivotsResult : ResultBase, IPivotPoint +public record RollingPivotsResult : ISeries, IPivotPoint { - public decimal? R4 { get; set; } - public decimal? R3 { get; set; } - public decimal? R2 { get; set; } - public decimal? R1 { get; set; } - public decimal? PP { get; set; } - public decimal? S1 { get; set; } - public decimal? S2 { get; set; } - public decimal? S3 { get; set; } - public decimal? S4 { get; set; } + public DateTime Timestamp { get; init; } + + public decimal? PP { get; init; } + + public decimal? S1 { get; init; } + public decimal? S2 { get; init; } + public decimal? S3 { get; init; } + public decimal? S4 { get; init; } + + public decimal? R1 { get; init; } + public decimal? R2 { get; init; } + public decimal? R3 { get; init; } + public decimal? R4 { get; init; } } diff --git a/src/m-r/RollingPivots/RollingPivots.Series.cs b/src/m-r/RollingPivots/RollingPivots.Series.cs deleted file mode 100644 index 2b3b7d138..000000000 --- a/src/m-r/RollingPivots/RollingPivots.Series.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PIVOT POINTS (SERIES) -public static partial class Indicator -{ - internal static List CalcRollingPivots( - this List quotesList, - int windowPeriods, - int offsetPeriods, - PivotPointType pointType) - where TQuote : IQuote - { - // check parameter arguments - ValidateRollingPivots(windowPeriods, offsetPeriods); - - // initialize - int length = quotesList.Count; - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - TQuote q = quotesList[i]; - - RollingPivotsResult r = new() { - Date = q.Date - }; - - if (i >= windowPeriods + offsetPeriods) - { - // window values - int s = i - windowPeriods - offsetPeriods; - TQuote hi = quotesList[s]; - - decimal windowHigh = hi.High; - decimal windowLow = hi.Low; - decimal windowClose = quotesList[i - offsetPeriods - 1].Close; - - for (int p = s; p <= i - offsetPeriods - 1; p++) - { - TQuote d = quotesList[p]; - windowHigh = (d.High > windowHigh) ? d.High : windowHigh; - windowLow = (d.Low < windowLow) ? d.Low : windowLow; - } - - // pivot points - RollingPivotsResult wp = GetPivotPoint( - pointType, q.Open, windowHigh, windowLow, windowClose); - - r.PP = wp.PP; - r.S1 = wp.S1; - r.S2 = wp.S2; - r.S3 = wp.S3; - r.S4 = wp.S4; - r.R1 = wp.R1; - r.R2 = wp.R2; - r.R3 = wp.R3; - r.R4 = wp.R4; - } - - results.Add(r); - } - - return results; - } - - // parameter validation - private static void ValidateRollingPivots( - int windowPeriods, - int offsetPeriods) - { - // check parameter arguments - if (windowPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(windowPeriods), windowPeriods, - "Window periods must be greater than 0 for Rolling Pivot Points."); - } - - if (offsetPeriods < 0) - { - throw new ArgumentOutOfRangeException(nameof(offsetPeriods), offsetPeriods, - "Offset periods must be greater than or equal to 0 for Rolling Pivot Points."); - } - } -} diff --git a/src/m-r/RollingPivots/RollingPivots.StaticSeries.cs b/src/m-r/RollingPivots/RollingPivots.StaticSeries.cs new file mode 100644 index 000000000..efbc07fde --- /dev/null +++ b/src/m-r/RollingPivots/RollingPivots.StaticSeries.cs @@ -0,0 +1,80 @@ +namespace Skender.Stock.Indicators; + +// ROLLING PIVOT POINTS (SERIES) + +public static partial class RollingPivots +{ + public static IReadOnlyList ToRollingPivots( + this IReadOnlyList quotes, + int windowPeriods, + int offsetPeriods, + PivotPointType pointType = PivotPointType.Standard) + where TQuote : IQuote + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(quotes); + Validate(windowPeriods, offsetPeriods); + + // initialize + int length = quotes.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + TQuote q = quotes[i]; + + RollingPivotsResult r; + + if (i >= windowPeriods + offsetPeriods) + { + // window values + int s = i - windowPeriods - offsetPeriods; + TQuote hi = quotes[s]; + + decimal windowHigh = hi.High; + decimal windowLow = hi.Low; + decimal windowClose = quotes[i - offsetPeriods - 1].Close; + + for (int p = s; p <= i - offsetPeriods - 1; p++) + { + TQuote d = quotes[p]; + windowHigh = d.High > windowHigh ? d.High : windowHigh; + windowLow = d.Low < windowLow ? d.Low : windowLow; + } + + // pivot points + WindowPoint wp = PivotPoints.GetPivotPoint( + pointType, q.Open, windowHigh, windowLow, windowClose); + + r = new() { + + Timestamp = q.Timestamp, + + // pivot point + PP = wp.PP, + + // support + S1 = wp.S1, + S2 = wp.S2, + S3 = wp.S3, + S4 = wp.S4, + + // resistance + R1 = wp.R1, + R2 = wp.R2, + R3 = wp.R3, + R4 = wp.R4 + }; + } + else + { + r = new() { Timestamp = q.Timestamp }; + } + + results.Add(r); + } + + return results; + } +} diff --git a/src/m-r/RollingPivots/RollingPivots.Utilities.cs b/src/m-r/RollingPivots/RollingPivots.Utilities.cs index 6c897032f..95a88b44b 100644 --- a/src/m-r/RollingPivots/RollingPivots.Utilities.cs +++ b/src/m-r/RollingPivots/RollingPivots.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// ROLLING PIVOT POINTS (UTILITIES) + +public static partial class RollingPivots { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -14,4 +15,23 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int windowPeriods, + int offsetPeriods) + { + // check parameter arguments + if (windowPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(windowPeriods), windowPeriods, + "Window periods must be greater than 0 for Rolling Pivot Points."); + } + + if (offsetPeriods < 0) + { + throw new ArgumentOutOfRangeException(nameof(offsetPeriods), offsetPeriods, + "Offset periods must be greater than or equal to 0 for Rolling Pivot Points."); + } + } } diff --git a/src/m-r/Rsi/Rsi.Api.cs b/src/m-r/Rsi/Rsi.Api.cs deleted file mode 100644 index 31900a55a..000000000 --- a/src/m-r/Rsi/Rsi.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RELATIVE STRENGTH INDEX (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetRsi( - this IEnumerable quotes, - int lookbackPeriods = 14) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcRsi(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetRsi( - this IEnumerable results, - int lookbackPeriods = 14) => results - .ToTuple() - .CalcRsi(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetRsi( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods = 14) => priceTuples - .ToSortedList() - .CalcRsi(lookbackPeriods); -} diff --git a/src/m-r/Rsi/Rsi.Models.cs b/src/m-r/Rsi/Rsi.Models.cs index d0d2268e6..2689789e5 100644 --- a/src/m-r/Rsi/Rsi.Models.cs +++ b/src/m-r/Rsi/Rsi.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class RsiResult : ResultBase, IReusableResult +public record RsiResult +( + DateTime Timestamp, + double? Rsi = null +) : IReusable { - public RsiResult(DateTime date) - { - Date = date; - } - - public double? Rsi { get; set; } - - double? IReusableResult.Value => Rsi; + public double Value => Rsi.Null2NaN(); } diff --git a/src/m-r/Rsi/Rsi.Series.cs b/src/m-r/Rsi/Rsi.Series.cs deleted file mode 100644 index 41b1dafb2..000000000 --- a/src/m-r/Rsi/Rsi.Series.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace Skender.Stock.Indicators; - -// RELATIVE STRENGTH INDEX (SERIES) -public static partial class Indicator -{ - internal static List CalcRsi( - this List<(DateTime Date, double Value)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateRsi(lookbackPeriods); - - // initialize - int length = tpList.Count; - double avgGain = 0; - double avgLoss = 0; - - List results = new(length); - double[] gain = new double[length]; // gain - double[] loss = new double[length]; // loss - double lastValue; - - if (length == 0) - { - return results; - } - else - { - lastValue = tpList[0].Value; - } - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - RsiResult r = new(date); - results.Add(r); - - gain[i] = (value > lastValue) ? value - lastValue : 0; - loss[i] = (value < lastValue) ? lastValue - value : 0; - lastValue = value; - - // calculate RSI - if (i > lookbackPeriods) - { - avgGain = ((avgGain * (lookbackPeriods - 1)) + gain[i]) / lookbackPeriods; - avgLoss = ((avgLoss * (lookbackPeriods - 1)) + loss[i]) / lookbackPeriods; - - if (avgLoss > 0) - { - double rs = avgGain / avgLoss; - r.Rsi = 100 - (100 / (1 + rs)); - } - else - { - r.Rsi = 100; - } - } - - // initialize average gain - else if (i == lookbackPeriods) - { - double sumGain = 0; - double sumLoss = 0; - - for (int p = 1; p <= lookbackPeriods; p++) - { - sumGain += gain[p]; - sumLoss += loss[p]; - } - - avgGain = sumGain / lookbackPeriods; - avgLoss = sumLoss / lookbackPeriods; - - r.Rsi = (avgLoss > 0) ? 100 - (100 / (1 + (avgGain / avgLoss))) : 100; - } - } - - return results; - } - - // parameter validation - private static void ValidateRsi( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods < 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for RSI."); - } - } -} diff --git a/src/m-r/Rsi/Rsi.StaticSeries.cs b/src/m-r/Rsi/Rsi.StaticSeries.cs new file mode 100644 index 000000000..fe0163cdf --- /dev/null +++ b/src/m-r/Rsi/Rsi.StaticSeries.cs @@ -0,0 +1,96 @@ +namespace Skender.Stock.Indicators; + +// RELATIVE STRENGTH INDEX (SERIES) + +public static partial class Rsi +{ + public static IReadOnlyList ToRsi( + this IReadOnlyList source, + int lookbackPeriods = 14) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + double avgGain = double.NaN; + double avgLoss = double.NaN; + + List results = new(length); + double[] gain = new double[length]; // gain + double[] loss = new double[length]; // loss + + if (length == 0) + { + return results; + } + + double prevValue = source[0].Value; + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + if (double.IsNaN(s.Value) || double.IsNaN(prevValue)) + { + gain[i] = loss[i] = double.NaN; + } + else + { + gain[i] = s.Value > prevValue ? s.Value - prevValue : 0; + loss[i] = s.Value < prevValue ? prevValue - s.Value : 0; + } + + double? rsi = null; + prevValue = s.Value; + + // re/initialize average gain + if (i >= lookbackPeriods && (double.IsNaN(avgGain) || double.IsNaN(avgLoss))) + { + double sumGain = 0; + double sumLoss = 0; + + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + sumGain += gain[p]; + sumLoss += loss[p]; + } + + avgGain = sumGain / lookbackPeriods; + avgLoss = sumLoss / lookbackPeriods; + + rsi = !double.IsNaN(avgGain / avgLoss) + ? avgLoss > 0 ? 100 - 100 / (1 + avgGain / avgLoss) : 100 + : null; + } + + // calculate RSI normally + else if (i > lookbackPeriods) + { + avgGain = (avgGain * (lookbackPeriods - 1) + gain[i]) / lookbackPeriods; + avgLoss = (avgLoss * (lookbackPeriods - 1) + loss[i]) / lookbackPeriods; + + if (avgLoss > 0) + { + double rs = avgGain / avgLoss; + rsi = 100 - 100 / (1 + rs); + } + else + { + rsi = 100; + } + } + + RsiResult r = new( + Timestamp: s.Timestamp, + Rsi: rsi); + + results.Add(r); + } + + return results; + } +} diff --git a/src/m-r/Rsi/Rsi.Utilities.cs b/src/m-r/Rsi/Rsi.Utilities.cs index a6112933e..c82bab854 100644 --- a/src/m-r/Rsi/Rsi.Utilities.cs +++ b/src/m-r/Rsi/Rsi.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// RELATIVE STRENGTH INDEX (UTILITIES) + +public static partial class Rsi { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +15,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(10 * n); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods < 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for RSI."); + } + } } diff --git a/src/s-z/Slope/Slope.Api.cs b/src/s-z/Slope/Slope.Api.cs deleted file mode 100644 index c915e9e8b..000000000 --- a/src/s-z/Slope/Slope.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SLOPE AND LINEAR REGRESSION (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetSlope( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcSlope(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetSlope( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcSlope(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetSlope( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcSlope(lookbackPeriods); -} diff --git a/src/s-z/Slope/Slope.Models.cs b/src/s-z/Slope/Slope.Models.cs index 202dd32db..a9488d2c7 100644 --- a/src/s-z/Slope/Slope.Models.cs +++ b/src/s-z/Slope/Slope.Models.cs @@ -1,18 +1,15 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class SlopeResult : ResultBase, IReusableResult +public record SlopeResult +( + DateTime Timestamp, + double? Slope = null, + double? Intercept = null, + double? StdDev = null, + double? RSquared = null, + decimal? Line = null // last line segment only +) : IReusable { - public SlopeResult(DateTime date) - { - Date = date; - } - - public double? Slope { get; set; } - public double? Intercept { get; set; } - public double? StdDev { get; set; } - public double? RSquared { get; set; } - public decimal? Line { get; set; } // last line segment only - - double? IReusableResult.Value => Slope; + public double Value => Slope.Null2NaN(); } diff --git a/src/s-z/Slope/Slope.Series.cs b/src/s-z/Slope/Slope.Series.cs deleted file mode 100644 index 52553f7ca..000000000 --- a/src/s-z/Slope/Slope.Series.cs +++ /dev/null @@ -1,104 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SLOPE AND LINEAR REGRESSION (SERIES) -public static partial class Indicator -{ - // calculate series - internal static List CalcSlope( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateSlope(lookbackPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double _) = tpList[i]; - - SlopeResult r = new(date); - results.Add(r); - - // skip initialization period - if (i + 1 < lookbackPeriods) - { - continue; - } - - // get averages for period - double sumX = 0; - double sumY = 0; - - for (int p = i - lookbackPeriods + 1; p <= i; p++) - { - (DateTime _, double pValue) = tpList[p]; - - sumX += p + 1d; - sumY += pValue; - } - - double avgX = sumX / lookbackPeriods; - double avgY = sumY / lookbackPeriods; - - // least squares method - double sumSqX = 0; - double sumSqY = 0; - double sumSqXY = 0; - - for (int p = i - lookbackPeriods + 1; p <= i; p++) - { - (DateTime _, double pValue) = tpList[p]; - - double devX = p + 1d - avgX; - double devY = pValue - avgY; - - sumSqX += devX * devX; - sumSqY += devY * devY; - sumSqXY += devX * devY; - } - - r.Slope = (sumSqXY / sumSqX).NaN2Null(); - r.Intercept = (avgY - (r.Slope * avgX)).NaN2Null(); - - // calculate Standard Deviation and R-Squared - double stdDevX = Math.Sqrt(sumSqX / lookbackPeriods); - double stdDevY = Math.Sqrt(sumSqY / lookbackPeriods); - r.StdDev = stdDevY.NaN2Null(); - - if (stdDevX * stdDevY != 0) - { - double arrr = sumSqXY / (stdDevX * stdDevY) / lookbackPeriods; - r.RSquared = (arrr * arrr).NaN2Null(); - } - } - - // add last Line (y = mx + b) - if (length >= lookbackPeriods) - { - SlopeResult? last = results.LastOrDefault(); - for (int p = length - lookbackPeriods; p < length; p++) - { - SlopeResult d = results[p]; - d.Line = (decimal?)((last?.Slope * (p + 1)) + last?.Intercept).NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidateSlope( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for Slope/Linear Regression."); - } - } -} diff --git a/src/s-z/Slope/Slope.StaticSeries.cs b/src/s-z/Slope/Slope.StaticSeries.cs new file mode 100644 index 000000000..6082405ed --- /dev/null +++ b/src/s-z/Slope/Slope.StaticSeries.cs @@ -0,0 +1,111 @@ +namespace Skender.Stock.Indicators; + +// SLOPE AND LINEAR REGRESSION (SERIES) + +public static partial class Slope +{ + public static IReadOnlyList ToSlope( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + // skip initialization period + if (i < lookbackPeriods - 1) + { + results.Add(new(s.Timestamp)); + continue; + } + + // get averages for period + double sumX = 0; + double sumY = 0; + + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + T ps = source[p]; + + sumX += p + 1d; + sumY += ps.Value; + } + + double avgX = sumX / lookbackPeriods; + double avgY = sumY / lookbackPeriods; + + // least squares method + double sumSqX = 0; + double sumSqY = 0; + double sumSqXy = 0; + + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + T ps = source[p]; + + double devX = p + 1d - avgX; + double devY = ps.Value - avgY; + + sumSqX += devX * devX; + sumSqY += devY * devY; + sumSqXy += devX * devY; + } + + double? slope = (sumSqXy / sumSqX).NaN2Null(); + double? intercept = (avgY - (slope * avgX)).NaN2Null(); + + // calculate Standard Deviation and R-Squared + double stdDevX = Math.Sqrt(sumSqX / lookbackPeriods); + double stdDevY = Math.Sqrt(sumSqY / lookbackPeriods); + + double? rSquared = null; + + if (stdDevX * stdDevY != 0) + { + double arrr = sumSqXy / (stdDevX * stdDevY) / lookbackPeriods; + rSquared = (arrr * arrr).NaN2Null(); + } + + // write results + SlopeResult r = new( + Timestamp: s.Timestamp, + Slope: slope, + Intercept: intercept, + StdDev: stdDevY.NaN2Null(), + RSquared: rSquared, + Line: null); // re-written below + + results.Add(r); + } + + // insufficient length for last line + if (length < lookbackPeriods) + { + return results; + } + + // add last Line (y = mx + b) + SlopeResult last = results.Last(); + + for (int p = length - lookbackPeriods; p < length; p++) + { + SlopeResult d = results[p]; + + results[p] = d with { + Line = (decimal?)((last.Slope * (p + 1)) + last.Intercept).NaN2Null() + }; + } + + return results; + } +} diff --git a/src/s-z/Slope/Slope.Utilities.cs b/src/s-z/Slope/Slope.Utilities.cs index c41549edb..f04ed9b5e 100644 --- a/src/s-z/Slope/Slope.Utilities.cs +++ b/src/s-z/Slope/Slope.Utilities.cs @@ -1,17 +1,18 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// SLOPE AND LINEAR REGRESSION (UTILITIES) + +public static partial class Slope { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Slope != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for Slope/Linear Regression."); + } } } diff --git a/src/s-z/Sma/Sma.Analysis.cs b/src/s-z/Sma/Sma.Analysis.cs deleted file mode 100644 index a4cddf4ee..000000000 --- a/src/s-z/Sma/Sma.Analysis.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SIMPLE MOVING AVERAGE (ANALYSIS) -public static partial class Indicator -{ - internal static IEnumerable CalcSmaAnalysis( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // initialize - List results = tpList - .CalcSma(lookbackPeriods) - .Select(x => new SmaAnalysis(x.Date) { Sma = x.Sma }) - .ToList(); - - // roll through quotes - for (int i = lookbackPeriods - 1; i < results.Count; i++) - { - SmaAnalysis r = results[i]; - double sma = (r.Sma == null) ? double.NaN : (double)r.Sma; - - double sumMad = 0; - double sumMse = 0; - double sumMape = 0; - - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - (DateTime _, double value) = tpList[p]; - - sumMad += Math.Abs(value - sma); - sumMse += (value - sma) * (value - sma); - - sumMape += (value == 0) ? double.NaN - : Math.Abs(value - sma) / value; - } - - // mean absolute deviation - r.Mad = (sumMad / lookbackPeriods).NaN2Null(); - - // mean squared error - r.Mse = (sumMse / lookbackPeriods).NaN2Null(); - - // mean absolute percent error - r.Mape = (sumMape / lookbackPeriods).NaN2Null(); - } - - return results; - } -} diff --git a/src/s-z/Sma/Sma.Api.cs b/src/s-z/Sma/Sma.Api.cs deleted file mode 100644 index 5e21293dd..000000000 --- a/src/s-z/Sma/Sma.Api.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SIMPLE MOVING AVERAGE (API) - -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetSma( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcSma(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetSma( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcSma(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetSma( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcSma(lookbackPeriods); - - // OBSERVER, from Quote Provider - /// - /// - public static SmaObserver GetSma( - this QuoteProvider provider, - int lookbackPeriods) - { - UseObserver useObserver = provider - .Use(CandlePart.Close); - - return new(useObserver, lookbackPeriods); - } - - // OBSERVER, from Chain Provider - /// - /// - public static SmaObserver GetSma( - this TupleProvider tupleProvider, - int lookbackPeriods) - => new(tupleProvider, lookbackPeriods); - - /// - /// - // ANALYSIS, from TQuote - public static IEnumerable GetSmaAnalysis( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcSmaAnalysis(lookbackPeriods); - - // ANALYSIS, from CHAIN - public static IEnumerable GetSmaAnalysis( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcSmaAnalysis(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // ANALYSIS, from TUPLE - public static IEnumerable GetSmaAnalysis( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcSmaAnalysis(lookbackPeriods); -} diff --git a/src/s-z/Sma/Sma.Models.cs b/src/s-z/Sma/Sma.Models.cs index 04f6371f5..f0dfcd857 100644 --- a/src/s-z/Sma/Sma.Models.cs +++ b/src/s-z/Sma/Sma.Models.cs @@ -1,30 +1,10 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class SmaResult : ResultBase, IReusableResult +public record SmaResult( + DateTime Timestamp, + double? Sma +) : IReusable { - public SmaResult(DateTime date) - { - Date = date; - } - - public double? Sma { get; set; } - - double? IReusableResult.Value => Sma; -} - -[Serializable] -public sealed class SmaAnalysis : ResultBase, IReusableResult -{ - public SmaAnalysis(DateTime date) - { - Date = date; - } - - public double? Sma { get; set; } // simple moving average - public double? Mad { get; set; } // mean absolute deviation - public double? Mse { get; set; } // mean square error - public double? Mape { get; set; } // mean absolute percentage error - - double? IReusableResult.Value => Sma; + public double Value => Sma.Null2NaN(); } diff --git a/src/s-z/Sma/Sma.Observer.cs b/src/s-z/Sma/Sma.Observer.cs deleted file mode 100644 index f0227f4cd..000000000 --- a/src/s-z/Sma/Sma.Observer.cs +++ /dev/null @@ -1,139 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SIMPLE MOVING AVERAGE (STREAMING) - -public class SmaObserver : ChainProvider -{ - public SmaObserver( - TupleProvider provider, - int lookbackPeriods) - { - Supplier = provider; - ProtectedResults = []; - - LookbackPeriods = lookbackPeriods; - - Initialize(); - } - - // PROPERTIES - - public IEnumerable Results => ProtectedResults; - internal List ProtectedResults { get; set; } - - private int LookbackPeriods { get; set; } - - // STATIC METHODS - - // parameter validation - internal static void Validate( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for SMA."); - } - } - - // incremental calculation - internal static double Increment( - List<(DateTime Date, double Value)> values, - int index, - int lookbackPeriods) - { - if (index < lookbackPeriods - 1) - { - return double.NaN; - } - - double sum = 0; - for (int i = index - lookbackPeriods + 1; i <= index; i++) - { - sum += values[i].Value; - } - - return sum / lookbackPeriods; - } - - // NON-STATIC METHODS - - // handle quote arrival - public override void OnNext((DateTime Date, double Value) value) => Add(value); - - // add new tuple quote - internal void Add((DateTime Date, double Value) tp) - { - // candidate result - SmaResult r = new(tp.Date); - - // initialize - int lengthRes = ProtectedResults.Count; - int lengthSrc = Supplier!.ProtectedTuples.Count; // merge fix, okay to replace - - // handle first value - if (lengthRes == 0) - { - ProtectedResults.Add(r); - SendToChain(r); - return; - } - - SmaResult lastResult = ProtectedResults[lengthRes - 1]; - (DateTime lastSrcDate, double _) = Supplier.ProtectedTuples[lengthSrc - 1]; - - if (r.Date == lastSrcDate) - { - r.Sma = Increment( - Supplier.ProtectedTuples, - lengthSrc - 1, - LookbackPeriods) - .NaN2Null(); - } - - // add new - if (r.Date > lastResult.Date) - { - ProtectedResults.Add(r); - SendToChain(r); - } - - // update last - else if (r.Date == lastResult.Date) - { - lastResult.Sma = r.Sma; - SendToChain(lastResult); - } - - // late arrival - else - { - // heal - throw new NotImplementedException(); - - // existing and index in sync? - - // new and index otherwise in sync? - - // all other scenarios: unsubscribe from provider and end transmission to others? - } - } - - // calculate with provider cache - private void Initialize() - { - if (Supplier != null) - { - List<(DateTime, double)> tuples = Supplier - .ProtectedTuples; - - for (int i = 0; i < tuples.Count; i++) - { - Add(tuples[i]); - } - - Subscribe(); - } - } -} diff --git a/src/s-z/Sma/Sma.Series.cs b/src/s-z/Sma/Sma.Series.cs deleted file mode 100644 index 2541de516..000000000 --- a/src/s-z/Sma/Sma.Series.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SIMPLE MOVING AVERAGE (SERIES) -public static partial class Indicator -{ - internal static List CalcSma( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - SmaObserver.Validate(lookbackPeriods); - - // initialize - List results = new(tpList.Count); - - // roll through quotes - for (int i = 0; i < tpList.Count; i++) - { - (DateTime date, double _) = tpList[i]; - - SmaResult result = new(date); - results.Add(result); - - result.Sma = SmaObserver - .Increment(tpList, i, lookbackPeriods) - .NaN2Null(); - } - - return results; - } -} diff --git a/src/s-z/Sma/Sma.StaticSeries.cs b/src/s-z/Sma/Sma.StaticSeries.cs new file mode 100644 index 000000000..a0b7a5c39 --- /dev/null +++ b/src/s-z/Sma/Sma.StaticSeries.cs @@ -0,0 +1,58 @@ +namespace Skender.Stock.Indicators; + +// SIMPLE MOVING AVERAGE (SERIES) + +public static partial class Sma +{ + public static IReadOnlyList ToSma( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + SmaResult[] results = new SmaResult[length]; + double[] values = new double[length]; + + for (int i = 0; i < length; i++) + { + values[i] = source[i].Value; + } + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + double sma; + + if (i >= lookbackPeriods - 1) + { + double sum = 0; + int end = i + 1; + int start = end - lookbackPeriods; + + for (int p = start; p < end; p++) + { + sum += source[p].Value; + } + + sma = sum / lookbackPeriods; + } + else + { + sma = double.NaN; + } + + results[i] = new SmaResult( + Timestamp: s.Timestamp, + Sma: sma.NaN2Null()); + } + + return new List(results); + } +} diff --git a/src/s-z/Sma/Sma.StreamHub.cs b/src/s-z/Sma/Sma.StreamHub.cs new file mode 100644 index 000000000..150609bb2 --- /dev/null +++ b/src/s-z/Sma/Sma.StreamHub.cs @@ -0,0 +1,60 @@ +namespace Skender.Stock.Indicators; + +// SIMPLE MOVING AVERAGE (STREAM HUB) + +#region hub interface and initializer + +public interface ISmaHub +{ + int LookbackPeriods { get; } +} + +public static partial class Sma +{ + public static SmaHub ToSma( + this IChainProvider chainProvider, + int lookbackPeriods) + where TIn : IReusable + => new(chainProvider, lookbackPeriods); +} +#endregion + +public class SmaHub + : ChainProvider, ISmaHub + where TIn : IReusable +{ + #region constructors + + private readonly string hubName; + + internal SmaHub( + IChainProvider provider, + int lookbackPeriods) : base(provider) + { + Sma.Validate(lookbackPeriods); + LookbackPeriods = lookbackPeriods; + hubName = $"SMA({lookbackPeriods})"; + + Reinitialize(); + } + #endregion + + public int LookbackPeriods { get; init; } + + // METHODS + + public override string ToString() => hubName; + + protected override (SmaResult result, int index) + ToIndicator(TIn item, int? indexHint) + { + int i = indexHint ?? ProviderCache.GetIndex(item, true); + + // candidate result + SmaResult r = new( + Timestamp: item.Timestamp, + Sma: Sma.Increment(ProviderCache, LookbackPeriods, i).NaN2Null()); + + return (r, i); + } +} diff --git a/src/s-z/Sma/Sma.Utilities.cs b/src/s-z/Sma/Sma.Utilities.cs index 4a14403eb..f8700d185 100644 --- a/src/s-z/Sma/Sma.Utilities.cs +++ b/src/s-z/Sma/Sma.Utilities.cs @@ -1,30 +1,116 @@ +using System.Numerics; + namespace Skender.Stock.Indicators; -public static partial class Indicator +// SIMPLE MOVING AVERAGE (UTILITIES) + +public static partial class Sma { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + /// Simple moving average calculation + /// + /// List of chainable values + /// + /// Window to evaluate, prior to 'endIndex' + /// + /// + /// Index position to evaluate or last position when . + /// + /// IReusable (chainable) type + /// + /// Simple moving average or + /// if incalculable + /// values are in range. + /// + internal static double? Average( + this IReadOnlyList values, + int lookbackPeriods, + int? endIndex = null) + where T : IReusable + + // TODO: unused SMA utility, either make public or remove + + => Increment( + values, + lookbackPeriods, + endIndex ?? values.Count - 1) + .NaN2Null(); + + /// + /// Simple moving average calculation + /// + /// List of chainable values + /// Window to evaluate, prior to 'endIndex' + /// Index position to evaluate. + /// IReusable (chainable) type + /// + /// Simple moving average or + /// when incalculable. + /// + internal static double Increment( + IReadOnlyList source, + int lookbackPeriods, + int endIndex) + where T : IReusable { - int removePeriods = results - .ToList() - .FindIndex(x => x.Sma != null); + if (endIndex < lookbackPeriods - 1 || endIndex >= source.Count) + { + return double.NaN; + } + + double sum = 0; + for (int i = endIndex - lookbackPeriods + 1; i <= endIndex; i++) + { + sum += source[i].Value; + } - return results.Remove(removePeriods); + return sum / lookbackPeriods; + + // TODO: apply this SMA increment method more widely in other indicators (see EMA example) } - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + internal static double[] Increment(this double[] prices, int period) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Sma != null); + // TODO: is this used (probably just an experiment, has rounding errors) + + int count = prices.Length - period + 1; + double[] sma = new double[count]; - return results.Remove(removePeriods); + int simdWidth = Vector.Count; + for (int i = 0; i < count; i++) + { + Vector sumVector = Vector.Zero; + + int j; + for (j = 0; j <= period - simdWidth; j += simdWidth) + { + Vector priceVector = new(prices, i + j); + sumVector += priceVector; + } + + double sum = 0; + for (; j < period; j++) // remainder loop + { + sum += prices[i + j]; + } + sum += Vector.Dot(sumVector, Vector.One); + + sma[i] = sum / period; + } + + return sma; + } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for SMA."); + } } } diff --git a/src/s-z/Sma/info.xml b/src/s-z/Sma/info.xml index 386418d41..2e257ef90 100644 --- a/src/s-z/Sma/info.xml +++ b/src/s-z/Sma/info.xml @@ -2,7 +2,7 @@ - + Simple Moving Average (SMA) of the price. @@ -17,35 +17,8 @@ Time series of SMA values. Invalid parameter value provided. - - - Establish an observable streaming Exponential Moving Average (EMA). - - See - documentation - for more information. - - - Observable quote provider. - Number of periods in the lookback window. - Observable EMA instance. - Invalid parameter value provided. - - - - Chain from an observable streaming Simple Moving Average (SMA). - - See - documentation - for more information. - - - Observable from chained indicator. - Number of periods in the lookback window. - Observable SMA instance. - Invalid parameter value provided. - - + + Simple Moving Average (SMA) is the average of price over a lookback window. This extended variant includes mean absolute deviation (MAD), mean square error (MSE), and mean absolute percentage error (MAPE). @@ -61,4 +34,4 @@ Invalid parameter value provided. - \ No newline at end of file + diff --git a/src/s-z/SmaAnalysis/SmaAnalysis.Models.cs b/src/s-z/SmaAnalysis/SmaAnalysis.Models.cs new file mode 100644 index 000000000..055f5c004 --- /dev/null +++ b/src/s-z/SmaAnalysis/SmaAnalysis.Models.cs @@ -0,0 +1,19 @@ +namespace Skender.Stock.Indicators; + +/// +/// SMA with extended analysis. +/// +/// Timestamp +/// Simple moving average +/// Mean absolute deviation +/// Mean square error +/// Mean absolute percentage error +[Serializable] +public record SmaAnalysis +( + DateTime Timestamp, + double? Sma = null, + double? Mad = null, + double? Mse = null, + double? Mape = null +) : SmaResult(Timestamp, Sma); diff --git a/src/s-z/SmaAnalysis/SmaAnalysis.StaticSeries.cs b/src/s-z/SmaAnalysis/SmaAnalysis.StaticSeries.cs new file mode 100644 index 000000000..d43770795 --- /dev/null +++ b/src/s-z/SmaAnalysis/SmaAnalysis.StaticSeries.cs @@ -0,0 +1,54 @@ +namespace Skender.Stock.Indicators; + +// SIMPLE MOVING AVERAGE (ANALYSIS) + +public static partial class Sma +{ + public static IReadOnlyList ToSmaAnalysis( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // initialize + List results = source + .ToSma(lookbackPeriods) + .Select(s => new SmaAnalysis(s.Timestamp, s.Sma)) + .ToList(); + + // roll through source values + for (int i = lookbackPeriods - 1; i < results.Count; i++) + { + SmaAnalysis r = results[i]; + double sma = r.Sma ?? double.NaN; + + double sumMad = 0; + double sumMse = 0; + double sumMape = 0; + + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + T s = source[p]; + + sumMad += Math.Abs(s.Value - sma); + sumMse += (s.Value - sma) * (s.Value - sma); + + sumMape += s.Value == 0 ? double.NaN + : Math.Abs(s.Value - sma) / s.Value; + } + + results[i] = r with { + + // mean absolute deviation + Mad = (sumMad / lookbackPeriods).NaN2Null(), + + // mean squared error + Mse = (sumMse / lookbackPeriods).NaN2Null(), + + // mean absolute percent error + Mape = (sumMape / lookbackPeriods).NaN2Null() + }; + } + + return results; + } +} diff --git a/src/s-z/Smi/Smi.Api.cs b/src/s-z/Smi/Smi.Api.cs deleted file mode 100644 index 924ab5c84..000000000 --- a/src/s-z/Smi/Smi.Api.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STOCHASTIC MOMENTUM INDEX (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetSmi( - this IEnumerable quotes, - int lookbackPeriods = 13, - int firstSmoothPeriods = 25, - int secondSmoothPeriods = 2, - int signalPeriods = 3) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcSmi( - lookbackPeriods, - firstSmoothPeriods, - secondSmoothPeriods, - signalPeriods); -} diff --git a/src/s-z/Smi/Smi.Models.cs b/src/s-z/Smi/Smi.Models.cs index d0fc2018e..739c082fc 100644 --- a/src/s-z/Smi/Smi.Models.cs +++ b/src/s-z/Smi/Smi.Models.cs @@ -1,15 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class SmiResult : ResultBase, IReusableResult +public record SmiResult +( + DateTime Timestamp, + double? Smi, + double? Signal +) : IReusable { - public SmiResult(DateTime date) - { - Date = date; - } - - public double? Smi { get; set; } - public double? Signal { get; set; } - - double? IReusableResult.Value => Smi; + public double Value => Smi.Null2NaN(); } diff --git a/src/s-z/Smi/Smi.Series.cs b/src/s-z/Smi/Smi.Series.cs deleted file mode 100644 index 7eb6f3148..000000000 --- a/src/s-z/Smi/Smi.Series.cs +++ /dev/null @@ -1,140 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STOCHASTIC MOMENTUM INDEX (SERIES) -public static partial class Indicator -{ - internal static List CalcSmi( - this List qdList, - int lookbackPeriods, - int firstSmoothPeriods, - int secondSmoothPeriods, - int signalPeriods) - { - // check parameter arguments - ValidateSmi( - lookbackPeriods, - firstSmoothPeriods, - secondSmoothPeriods, - signalPeriods); - - // initialize - int length = qdList.Count; - List results = new(length); - - double k1 = 2d / (firstSmoothPeriods + 1); - double k2 = 2d / (secondSmoothPeriods + 1); - double kS = 2d / (signalPeriods + 1); - - double lastSmEma1 = 0; - double lastSmEma2 = 0; - double lastHlEma1 = 0; - double lastHlEma2 = 0; - double lastSignal = 0; - - // roll through quotes - for (int i = 0; i < length; i++) - { - QuoteD q = qdList[i]; - - SmiResult r = new(q.Date); - results.Add(r); - - if (i + 1 >= lookbackPeriods) - { - double hH = double.MinValue; - double lL = double.MaxValue; - - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - QuoteD x = qdList[p]; - - if (x.High > hH) - { - hH = x.High; - } - - if (x.Low < lL) - { - lL = x.Low; - } - } - - double sm = q.Close - (0.5d * (hH + lL)); - double hl = hH - lL; - - // initialize last EMA values - if (i + 1 == lookbackPeriods) - { - lastSmEma1 = sm; - lastSmEma2 = lastSmEma1; - lastHlEma1 = hl; - lastHlEma2 = lastHlEma1; - } - - // first smoothing - double smEma1 = lastSmEma1 + (k1 * (sm - lastSmEma1)); - double hlEma1 = lastHlEma1 + (k1 * (hl - lastHlEma1)); - - // second smoothing - double smEma2 = lastSmEma2 + (k2 * (smEma1 - lastSmEma2)); - double hlEma2 = lastHlEma2 + (k2 * (hlEma1 - lastHlEma2)); - - // stochastic momentum index - double smi = 100 * (smEma2 / (0.5 * hlEma2)); - r.Smi = smi; - - // initialize signal line - if (i + 1 == lookbackPeriods) - { - lastSignal = smi; - } - - // signal line - double signal = lastSignal + (kS * (smi - lastSignal)); - r.Signal = signal; - - // carryover values - lastSmEma1 = smEma1; - lastSmEma2 = smEma2; - lastHlEma1 = hlEma1; - lastHlEma2 = hlEma2; - lastSignal = signal; - } - } - - return results; - } - - // parameter validation - private static void ValidateSmi( - int lookbackPeriods, - int firstSmoothPeriods, - int secondSmoothPeriods, - int signalPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for SMI."); - } - - if (firstSmoothPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(firstSmoothPeriods), firstSmoothPeriods, - "Smoothing periods must be greater than 0 for SMI."); - } - - if (secondSmoothPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(secondSmoothPeriods), secondSmoothPeriods, - "Smoothing periods must be greater than 0 for SMI."); - } - - if (signalPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, - "Signal periods must be greater than 0 for SMI."); - } - } -} diff --git a/src/s-z/Smi/Smi.StaticSeries.cs b/src/s-z/Smi/Smi.StaticSeries.cs new file mode 100644 index 000000000..9d80f0be2 --- /dev/null +++ b/src/s-z/Smi/Smi.StaticSeries.cs @@ -0,0 +1,132 @@ +namespace Skender.Stock.Indicators; + +// STOCHASTIC MOMENTUM INDEX (SERIES) + +public static partial class Smi +{ + public static IReadOnlyList ToSmi( + this IReadOnlyList quotes, + int lookbackPeriods = 13, + int firstSmoothPeriods = 25, + int secondSmoothPeriods = 2, + int signalPeriods = 3) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcSmi( + lookbackPeriods, + firstSmoothPeriods, + secondSmoothPeriods, + signalPeriods); + + private static List CalcSmi( + this IReadOnlyList source, + int lookbackPeriods, + int firstSmoothPeriods, + int secondSmoothPeriods, + int signalPeriods) + { + // check parameter arguments + Validate( + lookbackPeriods, + firstSmoothPeriods, + secondSmoothPeriods, + signalPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double k1 = 2d / (firstSmoothPeriods + 1); + double k2 = 2d / (secondSmoothPeriods + 1); + double kS = 2d / (signalPeriods + 1); + + double lastSmEma1 = 0; + double lastSmEma2 = 0; + double lastHlEma1 = 0; + double lastHlEma2 = 0; + double lastSignal = 0; + + // roll through source values + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + + double smi; + double signal; + + if (i >= lookbackPeriods - 1) + { + double hH = double.MinValue; + double lL = double.MaxValue; + + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + QuoteD x = source[p]; + + if (x.High > hH) + { + hH = x.High; + } + + if (x.Low < lL) + { + lL = x.Low; + } + } + + double sm = q.Close - 0.5d * (hH + lL); + double hl = hH - lL; + + // initialize last EMA values + // TODO: update healing, without requiring specific indexing + if (i == lookbackPeriods - 1) + { + lastSmEma1 = sm; + lastSmEma2 = lastSmEma1; + lastHlEma1 = hl; + lastHlEma2 = lastHlEma1; + } + + // first smoothing + double smEma1 = lastSmEma1 + k1 * (sm - lastSmEma1); + double hlEma1 = lastHlEma1 + k1 * (hl - lastHlEma1); + + // second smoothing + double smEma2 = lastSmEma2 + k2 * (smEma1 - lastSmEma2); + double hlEma2 = lastHlEma2 + k2 * (hlEma1 - lastHlEma2); + + // stochastic momentum index + smi = 100 * (smEma2 / (0.5 * hlEma2)); + + // initialize signal line + // TODO: update healing, without requiring specific indexing + if (i == lookbackPeriods - 1) + { + lastSignal = smi; + } + + // signal line + signal = lastSignal + kS * (smi - lastSignal); + + // carryover values + lastSmEma1 = smEma1; + lastSmEma2 = smEma2; + lastHlEma1 = hlEma1; + lastHlEma2 = hlEma2; + lastSignal = signal; + } + else + { + smi = double.NaN; + signal = double.NaN; + } + + results.Add(new( + Timestamp: q.Timestamp, + Smi: smi.NaN2Null(), + Signal: signal.NaN2Null())); + } + + return results; + } +} diff --git a/src/s-z/Smi/Smi.Utilities.cs b/src/s-z/Smi/Smi.Utilities.cs index 5aca22aef..b22632495 100644 --- a/src/s-z/Smi/Smi.Utilities.cs +++ b/src/s-z/Smi/Smi.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// STOCHASTIC MOMENTUM INDEX (UTILITIES) + +public static partial class Smi { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -14,4 +15,37 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods + 2 + 100); } + + // parameter validation + internal static void Validate( + int lookbackPeriods, + int firstSmoothPeriods, + int secondSmoothPeriods, + int signalPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for SMI."); + } + + if (firstSmoothPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(firstSmoothPeriods), firstSmoothPeriods, + "Smoothing periods must be greater than 0 for SMI."); + } + + if (secondSmoothPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(secondSmoothPeriods), secondSmoothPeriods, + "Smoothing periods must be greater than 0 for SMI."); + } + + if (signalPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, + "Signal periods must be greater than 0 for SMI."); + } + } } diff --git a/src/s-z/Smma/Smma.Api.cs b/src/s-z/Smma/Smma.Api.cs deleted file mode 100644 index ada941da1..000000000 --- a/src/s-z/Smma/Smma.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SMOOTHED MOVING AVERAGE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetSmma( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcSmma(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetSmma( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcSmma(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetSmma( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcSmma(lookbackPeriods); -} diff --git a/src/s-z/Smma/Smma.Models.cs b/src/s-z/Smma/Smma.Models.cs index d685792b4..3ea1c2e02 100644 --- a/src/s-z/Smma/Smma.Models.cs +++ b/src/s-z/Smma/Smma.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class SmmaResult : ResultBase, IReusableResult +public record SmmaResult +( + DateTime Timestamp, + double? Smma = null +) : IReusable { - public SmmaResult(DateTime date) - { - Date = date; - } - - public double? Smma { get; set; } - - double? IReusableResult.Value => Smma; + public double Value => Smma.Null2NaN(); } diff --git a/src/s-z/Smma/Smma.Series.cs b/src/s-z/Smma/Smma.Series.cs deleted file mode 100644 index 3181ebb33..000000000 --- a/src/s-z/Smma/Smma.Series.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SMOOTHED MOVING AVERAGE (SERIES) -public static partial class Indicator -{ - // calculate series - internal static List CalcSmma( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateSmma(lookbackPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - double prevValue = double.NaN; - - // roll through quotes - for (int i = 0; i < length; i++) - { - double smma = double.NaN; - (DateTime date, double value) = tpList[i]; - - SmmaResult r = new(date); - results.Add(r); - - // calculate SMMA - if (i + 1 > lookbackPeriods) - { - smma = ((prevValue * (lookbackPeriods - 1)) + value) / lookbackPeriods; - r.Smma = smma.NaN2Null(); - } - - // first SMMA calculated as simple SMA - else if (i + 1 == lookbackPeriods) - { - double sumClose = 0; - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - (DateTime _, double pValue) = tpList[p]; - sumClose += pValue; - } - - smma = sumClose / lookbackPeriods; - r.Smma = smma.NaN2Null(); - } - - prevValue = smma; - } - - return results; - } - - // parameter validation - private static void ValidateSmma( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for SMMA."); - } - } -} diff --git a/src/s-z/Smma/Smma.StaticSeries.cs b/src/s-z/Smma/Smma.StaticSeries.cs new file mode 100644 index 000000000..e2503ef6c --- /dev/null +++ b/src/s-z/Smma/Smma.StaticSeries.cs @@ -0,0 +1,63 @@ +namespace Skender.Stock.Indicators; + +// SMOOTHED MOVING AVERAGE (SERIES) + +public static partial class Smma +{ + public static IReadOnlyList ToSmma( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + double prevSmma = double.NaN; + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + // skip incalculable periods + if (i < lookbackPeriods - 1) + { + results.Add(new(s.Timestamp)); + continue; + } + + double smma; + + // when no prior SMMA, reset as SMA + if (double.IsNaN(prevSmma)) + { + double sum = 0; + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + T ps = source[p]; + sum += ps.Value; + } + + smma = sum / lookbackPeriods; + } + + // normal SMMA + else + { + smma = ((prevSmma * (lookbackPeriods - 1)) + s.Value) / lookbackPeriods; + } + + results.Add(new SmmaResult( + Timestamp: s.Timestamp, + Smma: smma.NaN2Null())); + + prevSmma = smma; + } + + return results; + } +} diff --git a/src/s-z/Smma/Smma.Utilities.cs b/src/s-z/Smma/Smma.Utilities.cs index 8a3ea18dc..5b5336023 100644 --- a/src/s-z/Smma/Smma.Utilities.cs +++ b/src/s-z/Smma/Smma.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// SMOOTHED MOVING AVERAGE (UTILITIES) + +public static partial class Smma { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +15,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(n + 100); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for SMMA."); + } + } } diff --git a/src/s-z/StarcBands/StarcBands.Api.cs b/src/s-z/StarcBands/StarcBands.Api.cs deleted file mode 100644 index ece5d9af0..000000000 --- a/src/s-z/StarcBands/StarcBands.Api.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STARC BANDS (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetStarcBands( - this IEnumerable quotes, - int smaPeriods, - double multiplier = 2, - int atrPeriods = 10) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcStarcBands(smaPeriods, multiplier, atrPeriods); -} diff --git a/src/s-z/StarcBands/StarcBands.Models.cs b/src/s-z/StarcBands/StarcBands.Models.cs index 09c275abf..aafb95c00 100644 --- a/src/s-z/StarcBands/StarcBands.Models.cs +++ b/src/s-z/StarcBands/StarcBands.Models.cs @@ -1,14 +1,10 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class StarcBandsResult : ResultBase -{ - public StarcBandsResult(DateTime date) - { - Date = date; - } - - public double? UpperBand { get; set; } - public double? Centerline { get; set; } - public double? LowerBand { get; set; } -} +public record StarcBandsResult +( + DateTime Timestamp, + double? UpperBand, + double? Centerline, + double? LowerBand +) : ISeries; diff --git a/src/s-z/StarcBands/StarcBands.Series.cs b/src/s-z/StarcBands/StarcBands.Series.cs deleted file mode 100644 index 5d1318abd..000000000 --- a/src/s-z/StarcBands/StarcBands.Series.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STARC BANDS (SERIES) -public static partial class Indicator -{ - internal static List CalcStarcBands( - this List qdList, - int smaPeriods, - double multiplier, - int atrPeriods) - { - // check parameter arguments - ValidateStarcBands(smaPeriods, multiplier, atrPeriods); - - // initialize - List atrResults = qdList.CalcAtr(atrPeriods); - - List results = qdList - .ToTuple(CandlePart.Close) - .CalcSma(smaPeriods) - .Select(x => new StarcBandsResult(x.Date) { - Centerline = x.Sma - }) - .ToList(); - - int lookbackPeriods = Math.Max(smaPeriods, atrPeriods); - - // roll through quotes - for (int i = lookbackPeriods - 1; i < results.Count; i++) - { - StarcBandsResult r = results[i]; - - AtrResult a = atrResults[i]; - - r.UpperBand = r.Centerline + (multiplier * a.Atr); - r.LowerBand = r.Centerline - (multiplier * a.Atr); - } - - return results; - } - - // parameter validation - private static void ValidateStarcBands( - int smaPeriods, - double multiplier, - int atrPeriods) - { - // check parameter arguments - if (smaPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "EMA periods must be greater than 1 for STARC Bands."); - } - - if (atrPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(atrPeriods), atrPeriods, - "ATR periods must be greater than 1 for STARC Bands."); - } - - if (multiplier <= 0) - { - throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, - "Multiplier must be greater than 0 for STARC Bands."); - } - } -} diff --git a/src/s-z/StarcBands/StarcBands.StaticSeries.cs b/src/s-z/StarcBands/StarcBands.StaticSeries.cs new file mode 100644 index 000000000..3a28837e4 --- /dev/null +++ b/src/s-z/StarcBands/StarcBands.StaticSeries.cs @@ -0,0 +1,46 @@ +namespace Skender.Stock.Indicators; + +// STARC BANDS (SERIES) + +public static partial class StarcBands +{ + public static IReadOnlyList ToStarcBands( + this IReadOnlyList quotes, + int smaPeriods, + double multiplier = 2, + int atrPeriods = 10) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcStarcBands(smaPeriods, multiplier, atrPeriods); + + private static List CalcStarcBands( + this IReadOnlyList source, + int smaPeriods, + double multiplier, + int atrPeriods) + { + // check parameter arguments + Validate(smaPeriods, multiplier, atrPeriods); + + // initialize + int length = source.Count; + List results = new(length); + List atrResults = source.CalcAtr(atrPeriods); + IReadOnlyList smaResults = source.ToSma(smaPeriods); + + // roll through source values + for (int i = 0; i < length; i++) + { + SmaResult s = smaResults[i]; + AtrResult a = atrResults[i]; + + results.Add(new( + Timestamp: s.Timestamp, + Centerline: s.Sma, + UpperBand: s.Sma + (multiplier * a.Atr), + LowerBand: s.Sma - (multiplier * a.Atr))); + } + + return results; + } +} diff --git a/src/s-z/StarcBands/StarcBands.Utilities.cs b/src/s-z/StarcBands/StarcBands.Utilities.cs index 382fb23f8..c30e4d801 100644 --- a/src/s-z/StarcBands/StarcBands.Utilities.cs +++ b/src/s-z/StarcBands/StarcBands.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// STARC BANDS (UTILITIES) + +public static partial class StarcBands { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -19,10 +20,9 @@ public static IEnumerable Condense( } // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -30,4 +30,30 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(n + 150); } + + // parameter validation + internal static void Validate( + int smaPeriods, + double multiplier, + int atrPeriods) + { + // check parameter arguments + if (smaPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, + "EMA periods must be greater than 1 for STARC Bands."); + } + + if (atrPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(atrPeriods), atrPeriods, + "ATR periods must be greater than 1 for STARC Bands."); + } + + if (multiplier <= 0) + { + throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, + "Multiplier must be greater than 0 for STARC Bands."); + } + } } diff --git a/src/s-z/Stc/Stc.Api.cs b/src/s-z/Stc/Stc.Api.cs deleted file mode 100644 index aad669cfc..000000000 --- a/src/s-z/Stc/Stc.Api.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SCHAFF TREND CYCLE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetStc( - this IEnumerable quotes, - int cyclePeriods = 10, - int fastPeriods = 23, - int slowPeriods = 50) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcStc(cyclePeriods, fastPeriods, slowPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetStc( - this IEnumerable results, - int cyclePeriods = 10, - int fastPeriods = 23, - int slowPeriods = 50) => results - .ToTuple() - .CalcStc(cyclePeriods, fastPeriods, slowPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetStc( - this IEnumerable<(DateTime, double)> priceTuples, - int cyclePeriods = 10, - int fastPeriods = 23, - int slowPeriods = 50) => priceTuples - .ToSortedList() - .CalcStc(cyclePeriods, fastPeriods, slowPeriods); -} diff --git a/src/s-z/Stc/Stc.Models.cs b/src/s-z/Stc/Stc.Models.cs index 51a781e9c..c60915501 100644 --- a/src/s-z/Stc/Stc.Models.cs +++ b/src/s-z/Stc/Stc.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class StcResult : ResultBase, IReusableResult +public record StcResult +( + DateTime Timestamp, + double? Stc +) : IReusable { - public StcResult(DateTime date) - { - Date = date; - } - - public double? Stc { get; set; } - - double? IReusableResult.Value => Stc; + public double Value => Stc.Null2NaN(); } diff --git a/src/s-z/Stc/Stc.Series.cs b/src/s-z/Stc/Stc.Series.cs deleted file mode 100644 index 8fc170176..000000000 --- a/src/s-z/Stc/Stc.Series.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SCHAFF TREND CYCLE (SERIES) -public static partial class Indicator -{ - internal static List CalcStc( - this List<(DateTime, double)> tpList, - int cyclePeriods, - int fastPeriods, - int slowPeriods) - { - // check parameter arguments - ValidateStc(cyclePeriods, fastPeriods, slowPeriods); - - // initialize results - int length = tpList.Count; - int initPeriods = Math.Min(slowPeriods - 1, length); - List results = new(length); - - // add back auto-pruned results - for (int i = 0; i < initPeriods; i++) - { - (DateTime date, double _) = tpList[i]; - results.Add(new StcResult(date)); - } - - // get stochastic of macd - List stochMacd = tpList - .CalcMacd(fastPeriods, slowPeriods, 1) - .Remove(initPeriods) - .Select(x => new QuoteD { - Date = x.Date, - High = x.Macd.Null2NaN(), - Low = x.Macd.Null2NaN(), - Close = x.Macd.Null2NaN() - }) - .ToList() - .CalcStoch(cyclePeriods, 1, 3, 3, 2, MaType.SMA); - - // add stoch results - for (int i = 0; i < stochMacd.Count; i++) - { - StochResult r = stochMacd[i]; - results.Add(new StcResult(r.Date) { Stc = r.Oscillator }); - } - - return results; - } - - // parameter validation - private static void ValidateStc( - int cyclePeriods, - int fastPeriods, - int slowPeriods) - { - // check parameter arguments - if (cyclePeriods < 0) - { - throw new ArgumentOutOfRangeException(nameof(cyclePeriods), cyclePeriods, - "Trend Cycle periods must be greater than or equal to 0 for STC."); - } - - if (fastPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, - "Fast periods must be greater than 0 for STC."); - } - - if (slowPeriods <= fastPeriods) - { - throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, - "Slow periods must be greater than the fast period for STC."); - } - } -} diff --git a/src/s-z/Stc/Stc.StaticSeries.cs b/src/s-z/Stc/Stc.StaticSeries.cs new file mode 100644 index 000000000..bb103d5bb --- /dev/null +++ b/src/s-z/Stc/Stc.StaticSeries.cs @@ -0,0 +1,45 @@ +namespace Skender.Stock.Indicators; + +// SCHAFF TREND CYCLE (SERIES) + +public static partial class Stc +{ + public static IReadOnlyList ToStc( + this IReadOnlyList source, + int cyclePeriods = 10, + int fastPeriods = 23, + int slowPeriods = 50) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(cyclePeriods, fastPeriods, slowPeriods); + + // initialize results + int length = source.Count; + List results = new(length); + + // get stochastic of macd + IReadOnlyList stochMacd = source + .ToMacd(fastPeriods, slowPeriods, 1) + .Select(x => new QuoteD( + x.Timestamp, 0, + x.Macd.Null2NaN(), + x.Macd.Null2NaN(), + x.Macd.Null2NaN(), 0)) + .ToList() + .CalcStoch(cyclePeriods, 1, 3, 3, 2, MaType.SMA); + + // add stoch results + for (int i = 0; i < length; i++) + { + StochResult r = stochMacd[i]; + + results.Add(new StcResult( + Timestamp: r.Timestamp, + Stc: r.Oscillator)); + } + + return results; + } +} diff --git a/src/s-z/Stc/Stc.Utilities.cs b/src/s-z/Stc/Stc.Utilities.cs index c2de9b594..ecc8a0004 100644 --- a/src/s-z/Stc/Stc.Utilities.cs +++ b/src/s-z/Stc/Stc.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// SCHAFF TREND CYCLE (UTILITIES) + +public static partial class Stc { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +15,30 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(n + 250); } + + // parameter validation + internal static void Validate( + int cyclePeriods, + int fastPeriods, + int slowPeriods) + { + // check parameter arguments + if (cyclePeriods < 0) + { + throw new ArgumentOutOfRangeException(nameof(cyclePeriods), cyclePeriods, + "Trend Cycle periods must be greater than or equal to 0 for STC."); + } + + if (fastPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(fastPeriods), fastPeriods, + "Fast periods must be greater than 0 for STC."); + } + + if (slowPeriods <= fastPeriods) + { + throw new ArgumentOutOfRangeException(nameof(slowPeriods), slowPeriods, + "Slow periods must be greater than the fast period for STC."); + } + } } diff --git a/src/s-z/StdDev/StdDev.Api.cs b/src/s-z/StdDev/StdDev.Api.cs deleted file mode 100644 index a4dc29e37..000000000 --- a/src/s-z/StdDev/StdDev.Api.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STANDARD DEVIATION (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetStdDev( - this IEnumerable quotes, - int lookbackPeriods, - int? smaPeriods = null) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcStdDev(lookbackPeriods, smaPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetStdDev( - this IEnumerable results, - int lookbackPeriods, - int? smaPeriods = null) => results - .ToTuple() - .CalcStdDev(lookbackPeriods, smaPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetStdDev( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods, - int? smaPeriods = null) => priceTuples - .ToSortedList() - .CalcStdDev(lookbackPeriods, smaPeriods); -} diff --git a/src/s-z/StdDev/StdDev.Models.cs b/src/s-z/StdDev/StdDev.Models.cs index 78768abfd..5d4dbb6ee 100644 --- a/src/s-z/StdDev/StdDev.Models.cs +++ b/src/s-z/StdDev/StdDev.Models.cs @@ -1,17 +1,13 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class StdDevResult : ResultBase, IReusableResult +public record StdDevResult +( + DateTime Timestamp, + double? StdDev, + double? Mean, + double? ZScore +) : IReusable { - public StdDevResult(DateTime date) - { - Date = date; - } - - public double? StdDev { get; set; } - public double? Mean { get; set; } - public double? ZScore { get; set; } - public double? StdDevSma { get; set; } - - double? IReusableResult.Value => StdDev; + public double Value => StdDev.Null2NaN(); } diff --git a/src/s-z/StdDev/StdDev.Series.cs b/src/s-z/StdDev/StdDev.Series.cs deleted file mode 100644 index b3d2c1264..000000000 --- a/src/s-z/StdDev/StdDev.Series.cs +++ /dev/null @@ -1,83 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STANDARD DEVIATION (SERIES) -public static partial class Indicator -{ - internal static List CalcStdDev( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - int? smaPeriods) - { - // check parameter arguments - ValidateStdDev(lookbackPeriods, smaPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - StdDevResult r = new(date); - results.Add(r); - - if (i + 1 >= lookbackPeriods) - { - double[] values = new double[lookbackPeriods]; - double sum = 0; - int n = 0; - - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - (DateTime _, double v) = tpList[p]; - values[n] = v; - sum += v; - n++; - } - - double avg = sum / lookbackPeriods; - - r.StdDev = values.StdDev().NaN2Null(); - r.Mean = avg.NaN2Null(); - - r.ZScore = (r.StdDev == 0) ? null - : (value - avg) / r.StdDev; - } - - // optional SMA - if (smaPeriods != null && i >= lookbackPeriods + smaPeriods - 2) - { - double? sumSma = 0; - for (int p = i + 1 - (int)smaPeriods; p <= i; p++) - { - sumSma += results[p].StdDev; - } - - r.StdDevSma = (sumSma / smaPeriods).NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidateStdDev( - int lookbackPeriods, - int? smaPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for Standard Deviation."); - } - - if (smaPeriods is not null and <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for Standard Deviation."); - } - } -} diff --git a/src/s-z/StdDev/StdDev.StaticSeries.cs b/src/s-z/StdDev/StdDev.StaticSeries.cs new file mode 100644 index 000000000..f1a9aba47 --- /dev/null +++ b/src/s-z/StdDev/StdDev.StaticSeries.cs @@ -0,0 +1,68 @@ +namespace Skender.Stock.Indicators; + +// STANDARD DEVIATION (SERIES) + +public static partial class StdDev +{ + public static IReadOnlyList ToStdDev( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + double mean; + double stdDev; + double zScore; + + if (i >= lookbackPeriods - 1) + { + double[] values = new double[lookbackPeriods]; + double sum = 0; + int n = 0; + + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + T ps = source[p]; + values[n] = ps.Value; + sum += ps.Value; + n++; + } + + mean = sum / lookbackPeriods; + + stdDev = values.StdDev(); + + zScore = stdDev == 0 ? double.NaN + : (s.Value - mean) / stdDev; + } + else + { + mean = double.NaN; + stdDev = double.NaN; + zScore = double.NaN; + } + + StdDevResult r = new( + Timestamp: s.Timestamp, + StdDev: stdDev.NaN2Null(), + Mean: mean.NaN2Null(), + ZScore: zScore.NaN2Null()); + + results.Add(r); + } + + return results; + } +} diff --git a/src/s-z/StdDev/StdDev.Utilities.cs b/src/s-z/StdDev/StdDev.Utilities.cs index 2d9e9f9aa..bd42630e0 100644 --- a/src/s-z/StdDev/StdDev.Utilities.cs +++ b/src/s-z/StdDev/StdDev.Utilities.cs @@ -1,17 +1,18 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// STANDARD DEVIATION (UTILITIES) + +public static partial class StdDev { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.StdDev != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for Standard Deviation."); + } } } diff --git a/src/s-z/StdDev/info.xml b/src/s-z/StdDev/info.xml index 6bab67d38..71c861cf5 100644 --- a/src/s-z/StdDev/info.xml +++ b/src/s-z/StdDev/info.xml @@ -12,7 +12,6 @@ Configurable Quote type. See Guide for more information. Historical price quotes. Number of periods in the lookback window. - Optional. Number of periods in the Standard Deviation SMA signal line. Time series of Standard Deviations values. Invalid parameter value provided. \ No newline at end of file diff --git a/src/s-z/StdDevChannels/StdDevChannels.Api.cs b/src/s-z/StdDevChannels/StdDevChannels.Api.cs deleted file mode 100644 index 5d7818128..000000000 --- a/src/s-z/StdDevChannels/StdDevChannels.Api.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STANDARD DEVIATION CHANNELS (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetStdDevChannels( - this IEnumerable quotes, - int? lookbackPeriods = 20, - double stdDeviations = 2) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcStdDevChannels(lookbackPeriods, stdDeviations); - - // SERIES, from CHAIN - public static IEnumerable GetStdDevChannels( - this IEnumerable results, - int? lookbackPeriods = 20, - double stdDeviations = 2) => results - .ToTuple() - .CalcStdDevChannels(lookbackPeriods, stdDeviations) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetStdDevChannels( - this IEnumerable<(DateTime, double)> priceTuples, - int? lookbackPeriods = 20, - double stdDeviations = 2) => priceTuples - .ToSortedList() - .CalcStdDevChannels(lookbackPeriods, stdDeviations); -} diff --git a/src/s-z/StdDevChannels/StdDevChannels.Models.cs b/src/s-z/StdDevChannels/StdDevChannels.Models.cs index 00da6a900..34be76c47 100644 --- a/src/s-z/StdDevChannels/StdDevChannels.Models.cs +++ b/src/s-z/StdDevChannels/StdDevChannels.Models.cs @@ -1,15 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class StdDevChannelsResult : ResultBase -{ - public StdDevChannelsResult(DateTime date) - { - Date = date; - } - - public double? Centerline { get; set; } - public double? UpperChannel { get; set; } - public double? LowerChannel { get; set; } - public bool BreakPoint { get; set; } -} +public record StdDevChannelsResult +( + DateTime Timestamp, + double? Centerline = null, + double? UpperChannel = null, + double? LowerChannel = null, + bool BreakPoint = false +) : ISeries; diff --git a/src/s-z/StdDevChannels/StdDevChannels.Series.cs b/src/s-z/StdDevChannels/StdDevChannels.Series.cs deleted file mode 100644 index 85fe19a16..000000000 --- a/src/s-z/StdDevChannels/StdDevChannels.Series.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STANDARD DEVIATION CHANNELS -public static partial class Indicator -{ - internal static List CalcStdDevChannels( - this List<(DateTime, double)> tpList, - int? lookbackPeriods, - double stdDeviations) - { - // assume whole quotes when lookback is null - lookbackPeriods ??= tpList.Count; - - // check parameter arguments - ValidateStdDevChannels(lookbackPeriods, stdDeviations); - - // initialize - List slopeResults = tpList - .CalcSlope((int)lookbackPeriods); - - int length = slopeResults.Count; - List results = slopeResults - .Select(x => new StdDevChannelsResult(x.Date)) - .ToList(); - - // roll through quotes in reverse - for (int w = length - 1; w >= lookbackPeriods - 1; w -= (int)lookbackPeriods) - { - SlopeResult s = slopeResults[w]; - double? width = stdDeviations * s.StdDev; - - // add regression line (y = mx + b) and channels - for (int p = w - (int)lookbackPeriods + 1; p <= w; p++) - { - if (p >= 0) - { - StdDevChannelsResult d = results[p]; - d.Centerline = (s.Slope * (p + 1)) + s.Intercept; - d.UpperChannel = d.Centerline + width; - d.LowerChannel = d.Centerline - width; - - d.BreakPoint = p == w - lookbackPeriods + 1; - } - } - } - - return results; - } - - // parameter validation - private static void ValidateStdDevChannels( - int? lookbackPeriods, - double stdDeviations) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for Standard Deviation Channels."); - } - - if (stdDeviations <= 0) - { - throw new ArgumentOutOfRangeException(nameof(stdDeviations), stdDeviations, - "Standard Deviations must be greater than 0 for Standard Deviation Channels."); - } - } -} diff --git a/src/s-z/StdDevChannels/StdDevChannels.StaticSeries.cs b/src/s-z/StdDevChannels/StdDevChannels.StaticSeries.cs new file mode 100644 index 000000000..067c32127 --- /dev/null +++ b/src/s-z/StdDevChannels/StdDevChannels.StaticSeries.cs @@ -0,0 +1,58 @@ +namespace Skender.Stock.Indicators; + +// STANDARD DEVIATION CHANNELS (SERIES) + +public static partial class StdDevChannels +{ + public static IReadOnlyList ToStdDevChannels( + this IReadOnlyList source, + int? lookbackPeriods = 20, + double stdDeviations = 2) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods, stdDeviations); + + // initialize + lookbackPeriods ??= source.Count; // assume whole quotes when null + int length = source.Count; + + IReadOnlyList slopeResults = source + .ToSlope((int)lookbackPeriods); + + List results = slopeResults + .Select(x => new StdDevChannelsResult(x.Timestamp)) + .ToList(); + + // roll through source values in reverse + for (int i = length - 1; i >= lookbackPeriods - 1; i -= (int)lookbackPeriods) + { + SlopeResult s = slopeResults[i]; + double? width = stdDeviations * s.StdDev; + + // add regression line (y = mx + b) and channels + for (int p = i - (int)lookbackPeriods + 1; p <= i; p++) + { + if (p < 0) + { + continue; + } + + StdDevChannelsResult d = results[p]; + + double? c = (s.Slope * (p + 1)) + s.Intercept; + + // re-write record + results[p] = d with { + Centerline = c, + UpperChannel = c + width, + LowerChannel = c - width, + BreakPoint = p == i - lookbackPeriods + 1 + }; + } + } + + return results; + } +} diff --git a/src/s-z/StdDevChannels/StdDevChannels.Utilities.cs b/src/s-z/StdDevChannels/StdDevChannels.Utilities.cs index f00a074c1..70d382c48 100644 --- a/src/s-z/StdDevChannels/StdDevChannels.Utilities.cs +++ b/src/s-z/StdDevChannels/StdDevChannels.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// STANDARD DEVIATION CHANNELS (UTILITIES) + +public static partial class StdDevChannels { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -16,16 +17,15 @@ public static IEnumerable Condense( x.UpperChannel is null && x.LowerChannel is null && x.Centerline is null - && x.BreakPoint is false); + && !x.BreakPoint); return resultsList.ToSortedList(); } // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -33,4 +33,23 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int? lookbackPeriods, + double stdDeviations) + { + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for Standard Deviation Channels."); + } + + if (stdDeviations <= 0) + { + throw new ArgumentOutOfRangeException(nameof(stdDeviations), stdDeviations, + "Standard Deviations must be greater than 0 for Standard Deviation Channels."); + } + } } diff --git a/src/s-z/Stoch/Stoch.Api.cs b/src/s-z/Stoch/Stoch.Api.cs deleted file mode 100644 index 0119bad4b..000000000 --- a/src/s-z/Stoch/Stoch.Api.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STOCHASTIC OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote (standard) - /// - /// - public static IEnumerable GetStoch( - this IEnumerable quotes, - int lookbackPeriods = 14, - int signalPeriods = 3, - int smoothPeriods = 3) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcStoch( - lookbackPeriods, - signalPeriods, - smoothPeriods, 3, 2, MaType.SMA); - - // SERIES, from TQuote (extended) - /// - /// - public static IEnumerable GetStoch( - this IEnumerable quotes, - int lookbackPeriods, - int signalPeriods, - int smoothPeriods, - double kFactor, - double dFactor, - MaType movingAverageType) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcStoch( - lookbackPeriods, - signalPeriods, - smoothPeriods, - kFactor, - dFactor, - movingAverageType); -} diff --git a/src/s-z/Stoch/Stoch.Models.cs b/src/s-z/Stoch/Stoch.Models.cs index c1099e1df..ee9c63f99 100644 --- a/src/s-z/Stoch/Stoch.Models.cs +++ b/src/s-z/Stoch/Stoch.Models.cs @@ -1,23 +1,18 @@ namespace Skender.Stock.Indicators; -/// -/// [Serializable] -public sealed class StochResult : ResultBase, IReusableResult +public record StochResult +( + DateTime Timestamp, + double? Oscillator, + double? Signal, + double? PercentJ +) : IReusable { - public StochResult(DateTime date) - { - Date = date; - } - - public double? Oscillator { get; set; } - public double? Signal { get; set; } - public double? PercentJ { get; set; } + public double Value => Oscillator.Null2NaN(); // aliases public double? K => Oscillator; public double? D => Signal; public double? J => PercentJ; - - double? IReusableResult.Value => Oscillator; } diff --git a/src/s-z/Stoch/Stoch.Series.cs b/src/s-z/Stoch/Stoch.Series.cs deleted file mode 100644 index 3f0aaa22e..000000000 --- a/src/s-z/Stoch/Stoch.Series.cs +++ /dev/null @@ -1,213 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STOCHASTIC OSCILLATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcStoch( - this List qdList, - int lookbackPeriods, - int signalPeriods, - int smoothPeriods, - double kFactor, - double dFactor, - MaType movingAverageType) - { - // check parameter arguments - ValidateStoch( - lookbackPeriods, signalPeriods, smoothPeriods, - kFactor, dFactor, movingAverageType); - - // initialize - int length = qdList.Count; - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - QuoteD q = qdList[i]; - - StochResult r = new(q.Date); - results.Add(r); - - if (i + 1 >= lookbackPeriods) - { - double highHigh = double.MinValue; - double lowLow = double.MaxValue; - - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - QuoteD x = qdList[p]; - - if (x.High > highHigh) - { - highHigh = x.High; - } - - if (x.Low < lowLow) - { - lowLow = x.Low; - } - } - - r.Oscillator = lowLow != highHigh - ? 100 * (q.Close - lowLow) / (highHigh - lowLow) - : 0; - - // reclaim nulls from NaNs - r.Oscillator = r.Oscillator.NaN2Null(); - } - } - - // smooth the oscillator - if (smoothPeriods > 1) - { - results = SmoothOscillator( - results, length, lookbackPeriods, smoothPeriods, movingAverageType); - } - - // handle insufficient length - if (length < lookbackPeriods - 1) - { - return results; - } - - // signal (%D) and %J - int signalIndex = lookbackPeriods + smoothPeriods + signalPeriods - 2; - double? s = null; - - for (int i = lookbackPeriods - 1; i < length; i++) - { - StochResult r = results[i]; - - // add signal - - if (signalPeriods <= 1) - { - r.Signal = r.Oscillator; - } - - // SMA case - else if (i + 1 >= signalIndex && movingAverageType is MaType.SMA) - { - double? sumOsc = 0; - for (int p = i + 1 - signalPeriods; p <= i; p++) - { - StochResult x = results[p]; - sumOsc += x.Oscillator; - } - - r.Signal = sumOsc / signalPeriods; - } - - // SMMA case - else if (i >= lookbackPeriods - 1 && movingAverageType is MaType.SMMA) - { - s ??= results[i].Oscillator; // set initial or reset if null - - s = ((s * (signalPeriods - 1)) + results[i].Oscillator) / signalPeriods; - r.Signal = s; - } - - // %J - r.PercentJ = (kFactor * r.Oscillator) - (dFactor * r.Signal); - } - - return results; - } - - // internals - private static List SmoothOscillator( - List results, - int length, - int lookbackPeriods, - int smoothPeriods, - MaType movingAverageType) - { - // temporarily store interim smoothed oscillator - double?[] smooth = new double?[length]; // smoothed value - - if (movingAverageType is MaType.SMA) - { - int smoothIndex = lookbackPeriods + smoothPeriods - 2; - - for (int i = smoothIndex; i < length; i++) - { - double? sumOsc = 0; - for (int p = i + 1 - smoothPeriods; p <= i; p++) - { - sumOsc += results[p].Oscillator; - } - - smooth[i] = sumOsc / smoothPeriods; - } - } - else if (movingAverageType is MaType.SMMA) - { - // initialize with unsmoothed value - double? k = results[lookbackPeriods - 1].Oscillator; - - for (int i = lookbackPeriods - 1; i < length; i++) - { - k ??= results[i].Oscillator; // reset if null - - k = ((k * (smoothPeriods - 1)) + results[i].Oscillator) / smoothPeriods; - smooth[i] = k; - } - } - - // replace oscillator - for (int i = 0; i < length; i++) - { - results[i].Oscillator = smooth[i]; - } - - return results; - } - - // parameter validation - private static void ValidateStoch( - int lookbackPeriods, - int signalPeriods, - int smoothPeriods, - double kFactor, - double dFactor, - MaType movingAverageType) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Stochastic."); - } - - if (signalPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, - "Signal periods must be greater than 0 for Stochastic."); - } - - if (smoothPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smoothPeriods), smoothPeriods, - "Smooth periods must be greater than 0 for Stochastic."); - } - - if (kFactor <= 0) - { - throw new ArgumentOutOfRangeException(nameof(kFactor), kFactor, - "kFactor must be greater than 0 for Stochastic."); - } - - if (dFactor <= 0) - { - throw new ArgumentOutOfRangeException(nameof(dFactor), dFactor, - "dFactor must be greater than 0 for Stochastic."); - } - - if (movingAverageType is not MaType.SMA and not MaType.SMMA) - { - throw new ArgumentOutOfRangeException(nameof(dFactor), dFactor, - "Stochastic only supports SMA and SMMA moving average types."); - } - } -} diff --git a/src/s-z/Stoch/Stoch.StaticSeries.cs b/src/s-z/Stoch/Stoch.StaticSeries.cs new file mode 100644 index 000000000..b272dfb4a --- /dev/null +++ b/src/s-z/Stoch/Stoch.StaticSeries.cs @@ -0,0 +1,216 @@ +namespace Skender.Stock.Indicators; + +// STOCHASTIC OSCILLATOR (SERIES) + +public static partial class Stoch +{ + public static IReadOnlyList ToStoch( + this IReadOnlyList quotes, + int lookbackPeriods = 14, + int signalPeriods = 3, + int smoothPeriods = 3) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcStoch( + lookbackPeriods, + signalPeriods, + smoothPeriods, 3, 2, MaType.SMA); + + public static IReadOnlyList ToStoch( + this IReadOnlyList quotes, + int lookbackPeriods, + int signalPeriods, + int smoothPeriods, + double kFactor, + double dFactor, + MaType movingAverageType) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcStoch( + lookbackPeriods, + signalPeriods, + smoothPeriods, + kFactor, + dFactor, + movingAverageType); + + internal static List CalcStoch( + this IReadOnlyList source, + int lookbackPeriods, + int signalPeriods, + int smoothPeriods, + double kFactor, + double dFactor, + MaType movingAverageType) + { + // check parameter arguments + Validate( + lookbackPeriods, signalPeriods, smoothPeriods, + kFactor, dFactor, movingAverageType); + + // initialize + int length = source.Count; + List results = new(length); + + double[] o = new double[length]; // %K oscillator (initial) + double[] k = new double[length]; // %K oscillator (final) + + double prevK = double.NaN; + double prevD = double.NaN; + + // roll through source values + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + + // initial %K oscillator + if (i >= lookbackPeriods - 1) + { + double highHigh = double.MinValue; + double lowLow = double.MaxValue; + bool isViable = true; + + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + QuoteD x = source[p]; + + if (double.IsNaN(x.High) + || double.IsNaN(x.Low) + || double.IsNaN(x.Close)) + { + isViable = false; + break; + } + + if (x.High > highHigh) + { + highHigh = x.High; + } + + if (x.Low < lowLow) + { + lowLow = x.Low; + } + } + + o[i] = !isViable + ? double.NaN + : highHigh - lowLow != 0 + ? 100 * (q.Close - lowLow) / (highHigh - lowLow) + : 0; + } + else + { + o[i] = double.NaN; + } + + // final %K oscillator, keep original + if (smoothPeriods <= 1) + { + k[i] = o[i]; + } + + // final %K oscillator, if smoothed + else if (i >= smoothPeriods) + { + k[i] = double.NaN; + + switch (movingAverageType) + { + // SMA case + case MaType.SMA: + { + double sum = 0; + for (int p = i - smoothPeriods + 1; p <= i; p++) + { + sum += o[p]; + } + + k[i] = sum / smoothPeriods; + break; + } + + // SMMA case + case MaType.SMMA: + { + // re/initialize + if (double.IsNaN(prevK)) + { + prevK = o[i]; + } + + k[i] = ((prevK * (smoothPeriods - 1)) + o[i]) / smoothPeriods; + prevK = k[i]; + break; + } + + default: + throw new InvalidOperationException( + "Invalid Stochastic moving average type."); + } + } + else + { + k[i] = double.NaN; + } + + double oscillator = k[i]; + double signal; + + + // %D signal line + if (signalPeriods <= 1) + { + signal = oscillator; + } + else if (i >= signalPeriods) + { + switch (movingAverageType) + { + // SMA case + // TODO: || double.IsNaN(prevD) to re/initialize SMMA? + case MaType.SMA: + { + double sum = 0; + for (int p = i - signalPeriods + 1; p <= i; p++) + { + sum += k[p]; + } + + signal = sum / signalPeriods; + break; + } + + // SMMA case + case MaType.SMMA: + { + // re/initialize + if (double.IsNaN(prevD)) + { + prevD = k[i]; + } + + double d = ((prevD * (signalPeriods - 1)) + k[i]) / signalPeriods; + signal = d; + prevD = d; + break; + } + + default: + throw new InvalidOperationException("Invalid Stochastic moving average type."); + } + } + else + { + signal = double.NaN; + } + + results.Add(new( + Timestamp: q.Timestamp, + Oscillator: oscillator.NaN2Null(), + Signal: signal.NaN2Null(), + PercentJ: ((kFactor * oscillator) - (dFactor * signal)).NaN2Null())); + } + return results; + } +} diff --git a/src/s-z/Stoch/Stoch.Utilities.cs b/src/s-z/Stoch/Stoch.Utilities.cs index feaf9867f..bb1fcc708 100644 --- a/src/s-z/Stoch/Stoch.Utilities.cs +++ b/src/s-z/Stoch/Stoch.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// STOCHASTIC OSCILLATOR (UTILITIES) + +public static partial class Stoch { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -14,4 +15,51 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int lookbackPeriods, + int signalPeriods, + int smoothPeriods, + double kFactor, + double dFactor, + MaType movingAverageType) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Stochastic."); + } + + if (signalPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, + "Signal periods must be greater than 0 for Stochastic."); + } + + if (smoothPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(smoothPeriods), smoothPeriods, + "Smooth periods must be greater than 0 for Stochastic."); + } + + if (kFactor <= 0) + { + throw new ArgumentOutOfRangeException(nameof(kFactor), kFactor, + "kFactor must be greater than 0 for Stochastic."); + } + + if (dFactor <= 0) + { + throw new ArgumentOutOfRangeException(nameof(dFactor), dFactor, + "dFactor must be greater than 0 for Stochastic."); + } + + if (movingAverageType is not MaType.SMA and not MaType.SMMA) + { + throw new ArgumentOutOfRangeException(nameof(dFactor), dFactor, + "Stochastic only supports SMA and SMMA moving average types."); + } + } } diff --git a/src/s-z/StochRsi/StochRsi.Api.cs b/src/s-z/StochRsi/StochRsi.Api.cs deleted file mode 100644 index 1cf710fc1..000000000 --- a/src/s-z/StochRsi/StochRsi.Api.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STOCHASTIC RSI (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetStochRsi( - this IEnumerable quotes, - int rsiPeriods, - int stochPeriods, - int signalPeriods, - int smoothPeriods = 1) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcStochRsi( - rsiPeriods, - stochPeriods, - signalPeriods, - smoothPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetStochRsi( - this IEnumerable results, - int rsiPeriods, - int stochPeriods, - int signalPeriods, - int smoothPeriods) => results - .ToTuple() - .CalcStochRsi( - rsiPeriods, - stochPeriods, - signalPeriods, - smoothPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetStochRsi( - this IEnumerable<(DateTime, double)> priceTuples, - int rsiPeriods, - int stochPeriods, - int signalPeriods, - int smoothPeriods) => priceTuples - .ToSortedList() - .CalcStochRsi( - rsiPeriods, - stochPeriods, - signalPeriods, - smoothPeriods); -} diff --git a/src/s-z/StochRsi/StochRsi.Models.cs b/src/s-z/StochRsi/StochRsi.Models.cs index 96c2b4f04..f365f5748 100644 --- a/src/s-z/StochRsi/StochRsi.Models.cs +++ b/src/s-z/StochRsi/StochRsi.Models.cs @@ -1,15 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class StochRsiResult : ResultBase, IReusableResult +public record StochRsiResult +( + DateTime Timestamp, + double? StochRsi = null, + double? Signal = null +) : IReusable { - public StochRsiResult(DateTime date) - { - Date = date; - } - - public double? StochRsi { get; set; } - public double? Signal { get; set; } - - double? IReusableResult.Value => StochRsi; + public double Value => StochRsi.Null2NaN(); } diff --git a/src/s-z/StochRsi/StochRsi.Series.cs b/src/s-z/StochRsi/StochRsi.Series.cs deleted file mode 100644 index 087099ee0..000000000 --- a/src/s-z/StochRsi/StochRsi.Series.cs +++ /dev/null @@ -1,91 +0,0 @@ -namespace Skender.Stock.Indicators; - -// STOCHASTIC RSI (SERIES) -public static partial class Indicator -{ - internal static List CalcStochRsi( - this List<(DateTime, double)> tpList, - int rsiPeriods, - int stochPeriods, - int signalPeriods, - int smoothPeriods) - { - // check parameter arguments - ValidateStochRsi(rsiPeriods, stochPeriods, signalPeriods, smoothPeriods); - - // initialize results - int length = tpList.Count; - int initPeriods = Math.Min(rsiPeriods + stochPeriods - 1, length); - List results = new(length); - - // add back auto-pruned results - for (int i = 0; i < initPeriods; i++) - { - (DateTime date, double _) = tpList[i]; - results.Add(new StochRsiResult(date)); - } - - // get Stochastic of RSI - List stoResults = - tpList - .CalcRsi(rsiPeriods) - .Remove(Math.Min(rsiPeriods, length)) - .Select(x => new QuoteD { - Date = x.Date, - High = x.Rsi.Null2NaN(), - Low = x.Rsi.Null2NaN(), - Close = x.Rsi.Null2NaN() - }) - .ToList() - .CalcStoch( - stochPeriods, - signalPeriods, - smoothPeriods, 3, 2, MaType.SMA) - .ToList(); - - // add stoch results - for (int i = rsiPeriods + stochPeriods - 1; i < length; i++) - { - StochResult r = stoResults[i - rsiPeriods]; - results.Add(new StochRsiResult(r.Date) { - StochRsi = r.Oscillator, - Signal = r.Signal - }); - } - - return results; - } - - // parameter validation - private static void ValidateStochRsi( - int rsiPeriods, - int stochPeriods, - int signalPeriods, - int smoothPeriods) - { - // check parameter arguments - if (rsiPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(rsiPeriods), rsiPeriods, - "RSI periods must be greater than 0 for Stochastic RSI."); - } - - if (stochPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(stochPeriods), stochPeriods, - "STOCH periods must be greater than 0 for Stochastic RSI."); - } - - if (signalPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, - "Signal periods must be greater than 0 for Stochastic RSI."); - } - - if (smoothPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smoothPeriods), smoothPeriods, - "Smooth periods must be greater than 0 for Stochastic RSI."); - } - } -} diff --git a/src/s-z/StochRsi/StochRsi.StaticSeries.cs b/src/s-z/StochRsi/StochRsi.StaticSeries.cs new file mode 100644 index 000000000..85837f5bf --- /dev/null +++ b/src/s-z/StochRsi/StochRsi.StaticSeries.cs @@ -0,0 +1,64 @@ +namespace Skender.Stock.Indicators; + +// STOCHASTIC RSI (SERIES) + +public static partial class StochRsi +{ + public static IReadOnlyList ToStochRsi( + this IReadOnlyList source, + int rsiPeriods, + int stochPeriods, + int signalPeriods, + int smoothPeriods = 1) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(rsiPeriods, stochPeriods, signalPeriods, smoothPeriods); + + // initialize results + int length = source.Count; + int initPeriods = Math.Min(rsiPeriods + stochPeriods - 1, length); + List results = new(length); + + // add back auto-pruned results + for (int i = 0; i < initPeriods; i++) + { + T s = source[i]; + results.Add(new(s.Timestamp)); + } + + // get Stochastic of RSI + List stoResults = + source + .ToRsi(rsiPeriods) + .Remove(Math.Min(rsiPeriods, length)) // TODO: still need to Remove() here, or auto-healing? + .Select(x => new QuoteD( + Timestamp: x.Timestamp, + Open: 0, + High: x.Rsi.Null2NaN(), + Low: x.Rsi.Null2NaN(), + Close: x.Rsi.Null2NaN(), + Volume: 0 + )) + .ToList() + .CalcStoch( + stochPeriods, + signalPeriods, + smoothPeriods, 3, 2, MaType.SMA) + .ToList(); + + // add stoch results + for (int i = rsiPeriods + stochPeriods - 1; i < length; i++) + { + StochResult r = stoResults[i - rsiPeriods]; + + results.Add(new StochRsiResult( + Timestamp: r.Timestamp, + StochRsi: r.Oscillator, + Signal: r.Signal)); + } + + return results; + } +} diff --git a/src/s-z/StochRsi/StochRsi.Utilities.cs b/src/s-z/StochRsi/StochRsi.Utilities.cs index 52170655f..d7d71e061 100644 --- a/src/s-z/StochRsi/StochRsi.Utilities.cs +++ b/src/s-z/StochRsi/StochRsi.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// STOCHASTIC RSI (UTILITIES) + +public static partial class StochRsi { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() @@ -14,4 +15,37 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(n + 100); } + + // parameter validation + internal static void Validate( + int rsiPeriods, + int stochPeriods, + int signalPeriods, + int smoothPeriods) + { + // check parameter arguments + if (rsiPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(rsiPeriods), rsiPeriods, + "RSI periods must be greater than 0 for Stochastic RSI."); + } + + if (stochPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(stochPeriods), stochPeriods, + "STOCH periods must be greater than 0 for Stochastic RSI."); + } + + if (signalPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, + "Signal periods must be greater than 0 for Stochastic RSI."); + } + + if (smoothPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(smoothPeriods), smoothPeriods, + "Smooth periods must be greater than 0 for Stochastic RSI."); + } + } } diff --git a/src/s-z/SuperTrend/SuperTrend.Api.cs b/src/s-z/SuperTrend/SuperTrend.Api.cs deleted file mode 100644 index d36ad4638..000000000 --- a/src/s-z/SuperTrend/SuperTrend.Api.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SUPERTREND (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetSuperTrend( - this IEnumerable quotes, - int lookbackPeriods = 10, - double multiplier = 3) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcSuperTrend(lookbackPeriods, multiplier); -} diff --git a/src/s-z/SuperTrend/SuperTrend.Models.cs b/src/s-z/SuperTrend/SuperTrend.Models.cs index cd8063612..f50d8dceb 100644 --- a/src/s-z/SuperTrend/SuperTrend.Models.cs +++ b/src/s-z/SuperTrend/SuperTrend.Models.cs @@ -1,14 +1,10 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class SuperTrendResult : ResultBase -{ - public SuperTrendResult(DateTime date) - { - Date = date; - } - - public decimal? SuperTrend { get; set; } - public decimal? UpperBand { get; set; } - public decimal? LowerBand { get; set; } -} +public record SuperTrendResult +( + DateTime Timestamp, + decimal? SuperTrend, + decimal? UpperBand, + decimal? LowerBand +) : ISeries; diff --git a/src/s-z/SuperTrend/SuperTrend.Series.cs b/src/s-z/SuperTrend/SuperTrend.Series.cs deleted file mode 100644 index 99b2bd9c9..000000000 --- a/src/s-z/SuperTrend/SuperTrend.Series.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace Skender.Stock.Indicators; - -// SUPERTREND (SERIES) -public static partial class Indicator -{ - internal static List CalcSuperTrend( - this List qdList, - int lookbackPeriods, - double multiplier) - { - // check parameter arguments - ValidateSuperTrend(lookbackPeriods, multiplier); - - // initialize - List results = new(qdList.Count); - List atrResults = qdList.CalcAtr(lookbackPeriods); - - bool isBullish = true; - double? upperBand = null; - double? lowerBand = null; - - // roll through quotes - for (int i = 0; i < qdList.Count; i++) - { - QuoteD q = qdList[i]; - - SuperTrendResult r = new(q.Date); - results.Add(r); - - if (i >= lookbackPeriods) - { - double? mid = (q.High + q.Low) / 2; - double? atr = atrResults[i].Atr; - double? prevClose = qdList[i - 1].Close; - - // potential bands - double? upperEval = mid + (multiplier * atr); - double? lowerEval = mid - (multiplier * atr); - - // initial values - if (i == lookbackPeriods) - { - isBullish = q.Close >= mid; - - upperBand = upperEval; - lowerBand = lowerEval; - } - - // new upper band - if (upperEval < upperBand || prevClose > upperBand) - { - upperBand = upperEval; - } - - // new lower band - if (lowerEval > lowerBand || prevClose < lowerBand) - { - lowerBand = lowerEval; - } - - // supertrend - if (q.Close <= (isBullish ? lowerBand : upperBand)) - { - r.SuperTrend = (decimal?)upperBand; - r.UpperBand = (decimal?)upperBand; - isBullish = false; - } - else - { - r.SuperTrend = (decimal?)lowerBand; - r.LowerBand = (decimal?)lowerBand; - isBullish = true; - } - } - } - - return results; - } - - // parameter validation - private static void ValidateSuperTrend( - int lookbackPeriods, - double multiplier) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for SuperTrend."); - } - - if (multiplier <= 0) - { - throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, - "Multiplier must be greater than 0 for SuperTrend."); - } - } -} diff --git a/src/s-z/SuperTrend/SuperTrend.StaticSeries.cs b/src/s-z/SuperTrend/SuperTrend.StaticSeries.cs new file mode 100644 index 000000000..98e6fbee6 --- /dev/null +++ b/src/s-z/SuperTrend/SuperTrend.StaticSeries.cs @@ -0,0 +1,105 @@ +namespace Skender.Stock.Indicators; + +// SUPERTREND (SERIES) + +public static partial class SuperTrend +{ + public static IReadOnlyList ToSuperTrend( + this IReadOnlyList quotes, + int lookbackPeriods = 10, + double multiplier = 3) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcSuperTrend(lookbackPeriods, multiplier); + + private static List CalcSuperTrend( + this IReadOnlyList source, + int lookbackPeriods, + double multiplier) + { + // check parameter arguments + Validate(lookbackPeriods, multiplier); + + // initialize + int length = source.Count; + List results = new(length); + List atrResults = source.CalcAtr(lookbackPeriods); + + bool isBullish = true; + double? upperBand = null; + double? lowerBand = null; + + // roll through source values + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + + double? superTrend; + double? upperOnly; + double? lowerOnly; + + if (i >= lookbackPeriods) + { + double? mid = (q.High + q.Low) / 2; + double? atr = atrResults[i].Atr; + double? prevClose = source[i - 1].Close; + + // potential bands + double? upperEval = mid + multiplier * atr; + double? lowerEval = mid - multiplier * atr; + + // initial values + // TODO: update healing, without requiring specific indexing + if (i == lookbackPeriods) + { + isBullish = q.Close >= mid; + + upperBand = upperEval; + lowerBand = lowerEval; + } + + // new upper band + if (upperEval < upperBand || prevClose > upperBand) + { + upperBand = upperEval; + } + + // new lower band + if (lowerEval > lowerBand || prevClose < lowerBand) + { + lowerBand = lowerEval; + } + + // supertrend + if (q.Close <= (isBullish ? lowerBand : upperBand)) + { + superTrend = upperBand; + upperOnly = upperBand; + lowerOnly = null; + isBullish = false; + } + else + { + superTrend = lowerBand; + lowerOnly = lowerBand; + upperOnly = null; + isBullish = true; + } + } + else + { + superTrend = null; + upperOnly = null; + lowerOnly = null; + } + + results.Add(new( + Timestamp: q.Timestamp, + SuperTrend: (decimal?)superTrend, + UpperBand: (decimal?)upperOnly, + LowerBand: (decimal?)lowerOnly)); + } + + return results; + } +} diff --git a/src/s-z/SuperTrend/SuperTrend.Utilities.cs b/src/s-z/SuperTrend/SuperTrend.Utilities.cs index 99efe4d80..052d097b9 100644 --- a/src/s-z/SuperTrend/SuperTrend.Utilities.cs +++ b/src/s-z/SuperTrend/SuperTrend.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// SUPERTREND (UTILITIES) + +public static partial class SuperTrend { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -19,10 +20,9 @@ public static IEnumerable Condense( } // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -30,4 +30,23 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int lookbackPeriods, + double multiplier) + { + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for SuperTrend."); + } + + if (multiplier <= 0) + { + throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, + "Multiplier must be greater than 0 for SuperTrend."); + } + } } diff --git a/src/s-z/T3/T3.Api.cs b/src/s-z/T3/T3.Api.cs deleted file mode 100644 index f1c5e5eaf..000000000 --- a/src/s-z/T3/T3.Api.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TILLSON T3 MOVING AVERAGE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetT3( - this IEnumerable quotes, - int lookbackPeriods = 5, - double volumeFactor = 0.7) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcT3(lookbackPeriods, volumeFactor); - - // SERIES, from CHAIN - public static IEnumerable GetT3( - this IEnumerable results, - int lookbackPeriods = 5, - double volumeFactor = 0.7) => results - .ToTuple() - .CalcT3(lookbackPeriods, volumeFactor) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetT3( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods = 5, - double volumeFactor = 0.7) => priceTuples - .ToSortedList() - .CalcT3(lookbackPeriods, volumeFactor); -} diff --git a/src/s-z/T3/T3.Models.cs b/src/s-z/T3/T3.Models.cs index be3814a92..de026e889 100644 --- a/src/s-z/T3/T3.Models.cs +++ b/src/s-z/T3/T3.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class T3Result : ResultBase, IReusableResult +public record T3Result +( + DateTime Timestamp, + double? T3 +) : IReusable { - public T3Result(DateTime date) - { - Date = date; - } - - public double? T3 { get; set; } - - double? IReusableResult.Value => T3; + public double Value => T3.Null2NaN(); } diff --git a/src/s-z/T3/T3.Series.cs b/src/s-z/T3/T3.Series.cs deleted file mode 100644 index 11d0ad331..000000000 --- a/src/s-z/T3/T3.Series.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TILLSON T3 MOVING AVERAGE (SERIES) -public static partial class Indicator -{ - internal static List CalcT3( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - double volumeFactor) - { - // check parameter arguments - ValidateT3(lookbackPeriods, volumeFactor); - - // initialize - int length = tpList.Count; - List results = new(length); - - if (length == 0) - { - return results; - } - - double k = 2d / (lookbackPeriods + 1); - double a = volumeFactor; - double c1 = -a * a * a; - double c2 = (3 * a * a) + (3 * a * a * a); - double c3 = (-6 * a * a) - (3 * a) - (3 * a * a * a); - double c4 = 1 + (3 * a) + (a * a * a) + (3 * a * a); - - double? e1; - double? e2; - double? e3; - double? e4; - double? e5; - double? e6; - - // add initial value - (DateTime date, double value) r0 = tpList[0]; - e1 = e2 = e3 = e4 = e5 = e6 = r0.value; - results.Add(new T3Result(r0.date) { T3 = r0.value }); - - // roll through quotes - for (int i = 1; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - T3Result r = new(date); - results.Add(r); - - // first smoothing - e1 += k * (value - e1); - e2 += k * (e1 - e2); - e3 += k * (e2 - e3); - e4 += k * (e3 - e4); - e5 += k * (e4 - e5); - e6 += k * (e5 - e6); - - // T3 moving average - r.T3 = ((c1 * e6) + (c2 * e5) + (c3 * e4) + (c4 * e3)).NaN2Null(); - } - - return results; - } - - // parameter validation - private static void ValidateT3( - int lookbackPeriods, - double volumeFactor) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for T3."); - } - - if (volumeFactor <= 0) - { - throw new ArgumentOutOfRangeException(nameof(volumeFactor), volumeFactor, - "Volume Factor must be greater than 0 for T3."); - } - } -} diff --git a/src/s-z/T3/T3.StaticSeries.cs b/src/s-z/T3/T3.StaticSeries.cs new file mode 100644 index 000000000..a85bf64ac --- /dev/null +++ b/src/s-z/T3/T3.StaticSeries.cs @@ -0,0 +1,63 @@ +namespace Skender.Stock.Indicators; + +// TILLSON T3 MOVING AVERAGE (SERIES) + +public static partial class T3 +{ + public static IReadOnlyList ToT3( + this IReadOnlyList source, + int lookbackPeriods = 5, + double volumeFactor = 0.7) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods, volumeFactor); + + // initialize + int length = source.Count; + List results = new(length); + + double k = 2d / (lookbackPeriods + 1); + double a = volumeFactor; + + double c1 = -a * a * a; + double c2 = 3 * a * a + 3 * a * a * a; + double c3 = -6 * a * a - 3 * a - 3 * a * a * a; + double c4 = 1 + 3 * a + a * a * a + 3 * a * a; + + double e1 = double.NaN; + double e2 = double.NaN; + double e3 = double.NaN; + double e4 = double.NaN; + double e5 = double.NaN; + double e6 = double.NaN; + + // roll through remaining quotes + for (int i = 0; i < length; i++) + { + T s = source[i]; + + // re/seed values + if (double.IsNaN(e6)) + { + e1 = e2 = e3 = e4 = e5 = e6 = s.Value; + } + + // first smoothing + e1 += k * (s.Value - e1); + e2 += k * (e1 - e2); + e3 += k * (e2 - e3); + e4 += k * (e3 - e4); + e5 += k * (e4 - e5); + e6 += k * (e5 - e6); + + // T3 moving average + results.Add(new( + Timestamp: s.Timestamp, + T3: (c1 * e6 + c2 * e5 + c3 * e4 + c4 * e3).NaN2Null())); + } + + return results; + } +} diff --git a/src/s-z/T3/T3.Utilities.cs b/src/s-z/T3/T3.Utilities.cs new file mode 100644 index 000000000..70c4264aa --- /dev/null +++ b/src/s-z/T3/T3.Utilities.cs @@ -0,0 +1,25 @@ +namespace Skender.Stock.Indicators; + +// TILLSON T3 MOVING AVERAGE (UTILITIES) + +public static partial class T3 +{ + // parameter validation + internal static void Validate( + int lookbackPeriods, + double volumeFactor) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for T3."); + } + + if (volumeFactor <= 0) + { + throw new ArgumentOutOfRangeException(nameof(volumeFactor), volumeFactor, + "Volume Factor must be greater than 0 for T3."); + } + } +} diff --git a/src/s-z/Tema/Tema.Api.cs b/src/s-z/Tema/Tema.Api.cs deleted file mode 100644 index 1955004f3..000000000 --- a/src/s-z/Tema/Tema.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TRIPLE EXPONENTIAL MOVING AVERAGE - TEMA (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetTema( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcTema(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetTema( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcTema(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetTema( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcTema(lookbackPeriods); -} diff --git a/src/s-z/Tema/Tema.Models.cs b/src/s-z/Tema/Tema.Models.cs index 145a7548a..825eecffe 100644 --- a/src/s-z/Tema/Tema.Models.cs +++ b/src/s-z/Tema/Tema.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class TemaResult : ResultBase, IReusableResult +public record TemaResult +( + DateTime Timestamp, + double? Tema = null +) : IReusable { - public TemaResult(DateTime date) - { - Date = date; - } - - public double? Tema { get; set; } - - double? IReusableResult.Value => Tema; + public double Value => Tema.Null2NaN(); } diff --git a/src/s-z/Tema/Tema.Series.cs b/src/s-z/Tema/Tema.Series.cs deleted file mode 100644 index 2a564f2c5..000000000 --- a/src/s-z/Tema/Tema.Series.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TRIPLE EXPONENTIAL MOVING AVERAGE - TEMA (SERIES) -public static partial class Indicator -{ - // calculate series - internal static List CalcTema( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateTema(lookbackPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - - double k = 2d / (lookbackPeriods + 1); - double? lastEma1 = 0; - double? lastEma2; - double? lastEma3; - int initPeriods = Math.Min(lookbackPeriods, length); - - for (int i = 0; i < initPeriods; i++) - { - (DateTime _, double value) = tpList[i]; - lastEma1 += value; - } - - lastEma1 /= lookbackPeriods; - lastEma2 = lastEma3 = lastEma1; - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - TemaResult r = new(date); - results.Add(r); - - if (i > lookbackPeriods - 1) - { - double? ema1 = lastEma1 + (k * (value - lastEma1)); - double? ema2 = lastEma2 + (k * (ema1 - lastEma2)); - double? ema3 = lastEma3 + (k * (ema2 - lastEma3)); - - r.Tema = ((3 * ema1) - (3 * ema2) + ema3).NaN2Null(); - - lastEma1 = ema1; - lastEma2 = ema2; - lastEma3 = ema3; - } - else if (i == lookbackPeriods - 1) - { - r.Tema = ((3 * lastEma1) - (3 * lastEma2) + lastEma3).NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidateTema( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for TEMA."); - } - } -} diff --git a/src/s-z/Tema/Tema.StaticSeries.cs b/src/s-z/Tema/Tema.StaticSeries.cs new file mode 100644 index 000000000..f14a28512 --- /dev/null +++ b/src/s-z/Tema/Tema.StaticSeries.cs @@ -0,0 +1,74 @@ +namespace Skender.Stock.Indicators; + +// TRIPLE EXPONENTIAL MOVING AVERAGE (SERIES) + +public static partial class Tema +{ + // calculate series + public static IReadOnlyList ToTema( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double k = 2d / (lookbackPeriods + 1); + double lastEma1 = double.NaN; + double lastEma2 = double.NaN; + double lastEma3 = double.NaN; + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + // skip incalculable periods + if (i < lookbackPeriods - 1) + { + results.Add(new(s.Timestamp)); + continue; + } + + double ema1; + double ema2; + double ema3; + + // when no prior EMA, reset as SMA + if (double.IsNaN(lastEma3)) + { + double sum = 0; + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + T ps = source[p]; + sum += ps.Value; + } + + ema1 = ema2 = ema3 = sum / lookbackPeriods; + } + + // normal TEMA + else + { + ema1 = lastEma1 + (k * (s.Value - lastEma1)); + ema2 = lastEma2 + (k * (ema1 - lastEma2)); + ema3 = lastEma3 + (k * (ema2 - lastEma3)); + } + + results.Add(new TemaResult( + Timestamp: s.Timestamp, + Tema: ((3 * ema1) - (3 * ema2) + ema3).NaN2Null())); + + lastEma1 = ema1; + lastEma2 = ema2; + lastEma3 = ema3; + } + + return results; + } +} diff --git a/src/s-z/Tema/Tema.Utilities.cs b/src/s-z/Tema/Tema.Utilities.cs index ad438bec5..963479cb1 100644 --- a/src/s-z/Tema/Tema.Utilities.cs +++ b/src/s-z/Tema/Tema.Utilities.cs @@ -1,17 +1,30 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// TRIPLE EXPONENTIAL MOVING AVERAGE (UTILITIES) + +public static partial class Tema { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() .FindIndex(x => x.Tema != null) + 1; - return results.Remove((3 * n) + 100); + return results.Remove(3 * n + 100); + } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for TEMA."); + } } } diff --git a/src/s-z/Tr/Tr.Api.cs b/src/s-z/Tr/Tr.Api.cs deleted file mode 100644 index d3cf0ac23..000000000 --- a/src/s-z/Tr/Tr.Api.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TRUE RANGE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetTr( - this IEnumerable quotes) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcTr(); -} diff --git a/src/s-z/Tr/Tr.Models.cs b/src/s-z/Tr/Tr.Models.cs index 2cb02a6aa..49dff2582 100644 --- a/src/s-z/Tr/Tr.Models.cs +++ b/src/s-z/Tr/Tr.Models.cs @@ -1,14 +1,10 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class TrResult : ResultBase, IReusableResult +public record TrResult( + DateTime Timestamp, + double? Tr +) : IReusable { - public TrResult(DateTime date) - { - Date = date; - } - - public double? Tr { get; set; } - - double? IReusableResult.Value => Tr; + public double Value => Tr.Null2NaN(); } diff --git a/src/s-z/Tr/Tr.Series.cs b/src/s-z/Tr/Tr.Series.cs deleted file mode 100644 index 617928bfe..000000000 --- a/src/s-z/Tr/Tr.Series.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TRUE RANGE (SERIES) -public static partial class Indicator -{ - // calculate series - internal static List CalcTr( - this List qdList) - { - // initialize - List results = new(qdList.Count); - double prevClose = double.NaN; - - // roll through quotes - for (int i = 0; i < qdList.Count; i++) - { - QuoteD q = qdList[i]; - - TrResult r = new(q.Date); - results.Add(r); - - if (i is 0) - { - prevClose = q.Close; - continue; - } - - double hmpc = Math.Abs(q.High - prevClose); - double lmpc = Math.Abs(q.Low - prevClose); - - r.Tr = Math.Max(q.High - q.Low, Math.Max(hmpc, lmpc)); - - prevClose = q.Close; - } - - return results; - } -} diff --git a/src/s-z/Tr/Tr.StaticSeries.cs b/src/s-z/Tr/Tr.StaticSeries.cs new file mode 100644 index 000000000..79fa0dbaf --- /dev/null +++ b/src/s-z/Tr/Tr.StaticSeries.cs @@ -0,0 +1,38 @@ +namespace Skender.Stock.Indicators; + +// TRUE RANGE (SERIES) + +public static partial class Tr +{ + public static IReadOnlyList ToTr( + this IReadOnlyList quotes) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcTr(); + + private static List CalcTr( + this IReadOnlyList source) + { + // initialize + int length = source.Count; + TrResult[] results = new TrResult[length]; + + // skip first period + if (length > 0) + { + results[0] = new TrResult(source[0].Timestamp, null); + } + + // roll through source values + for (int i = 1; i < length; i++) + { + QuoteD q = source[i]; + + results[i] = new TrResult( + Timestamp: q.Timestamp, + Tr: Increment(q.High, q.Low, source[i - 1].Close)); + } + + return new List(results); + } +} diff --git a/src/s-z/Tr/Tr.StreamHub.cs b/src/s-z/Tr/Tr.StreamHub.cs new file mode 100644 index 000000000..ea0803e9b --- /dev/null +++ b/src/s-z/Tr/Tr.StreamHub.cs @@ -0,0 +1,58 @@ +namespace Skender.Stock.Indicators; + +// TRUE RANGE (STREAM HUB) + +#region initializer + +public static partial class Tr +{ + public static TrHub ToTr( + this IQuoteProvider quoteProvider) + where TIn : IQuote + => new(quoteProvider); +} +#endregion + +public class TrHub + : ChainProvider + where TIn : IQuote +{ + #region constructors + + private const string hubName = "TRUE RANGE"; + + internal TrHub(IQuoteProvider provider) + : base(provider) + { + Reinitialize(); + } + #endregion + + // METHODS + + public override string ToString() => hubName; + + protected override (TrResult result, int index) + ToIndicator(TIn item, int? indexHint) + { + int i = indexHint ?? ProviderCache.GetIndex(item, true); + + // skip first period + if (i == 0) + { + return (new TrResult(item.Timestamp, null), i); + } + + TIn prev = ProviderCache[i - 1]; + + // candidate result + TrResult r = new( + item.Timestamp, + Tr.Increment( + (double)item.High, + (double)item.Low, + (double)prev.Close)); + + return (r, i); + } +} diff --git a/src/s-z/Tr/Tr.Utilities.cs b/src/s-z/Tr/Tr.Utilities.cs new file mode 100644 index 000000000..d6f9f4d09 --- /dev/null +++ b/src/s-z/Tr/Tr.Utilities.cs @@ -0,0 +1,17 @@ +namespace Skender.Stock.Indicators; + +// TRUE RANGE (UTILITIES) + +public static partial class Tr +{ + public static double Increment( + double high, + double low, + double prevClose) + { + double hmpc = Math.Abs(high - prevClose); + double lmpc = Math.Abs(low - prevClose); + + return Math.Max(high - low, Math.Max(hmpc, lmpc)); + } +} diff --git a/src/s-z/Tr/info.xml b/src/s-z/Tr/info.xml index 407b2b550..cacf17b58 100644 --- a/src/s-z/Tr/info.xml +++ b/src/s-z/Tr/info.xml @@ -1,16 +1,30 @@ - - True Range (TR) is a measure of volatility that captures gaps and limits between periods. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - Time series of True Range (TR) values. - Invalid parameter value provided. + + + True Range (TR) is a measure of volatility that captures gaps and limits between periods. + + See + documentation + for more information. + + + Configurable Quote type. See Guide for more information. + Historical price quotes. + Time series of True Range (TR) values. + Invalid parameter value provided. + + + Get the next incremental True Range (TR) value. + + See + documentation + for more information. + + Last Close price, from prior period. + High price, current period. + Low price, current period. + New TR value. + \ No newline at end of file diff --git a/src/s-z/Trix/Trix.Api.cs b/src/s-z/Trix/Trix.Api.cs deleted file mode 100644 index 88914cd69..000000000 --- a/src/s-z/Trix/Trix.Api.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TRIPLE EMA OSCILLATOR - TRIX (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetTrix( - this IEnumerable quotes, - int lookbackPeriods, - int? signalPeriods = null) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcTrix(lookbackPeriods, signalPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetTrix( - this IEnumerable results, - int lookbackPeriods, - int? signalPeriods = null) => results - .ToTuple() - .CalcTrix(lookbackPeriods, signalPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetTrix( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods, - int? signalPeriods = null) => priceTuples - .ToSortedList() - .CalcTrix(lookbackPeriods, signalPeriods); -} diff --git a/src/s-z/Trix/Trix.Models.cs b/src/s-z/Trix/Trix.Models.cs index de69555be..ad12f3937 100644 --- a/src/s-z/Trix/Trix.Models.cs +++ b/src/s-z/Trix/Trix.Models.cs @@ -1,16 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class TrixResult : ResultBase, IReusableResult +public record TrixResult +( + DateTime Timestamp, + double? Ema3 = null, + double? Trix = null +) : IReusable { - public TrixResult(DateTime date) - { - Date = date; - } - - public double? Ema3 { get; set; } - public double? Trix { get; set; } - public double? Signal { get; set; } - - double? IReusableResult.Value => Trix; + public double Value => Trix.Null2NaN(); } diff --git a/src/s-z/Trix/Trix.Series.cs b/src/s-z/Trix/Trix.Series.cs deleted file mode 100644 index 14ec09e59..000000000 --- a/src/s-z/Trix/Trix.Series.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TRIPLE EMA OSCILLATOR - TRIX (SERIES) -public static partial class Indicator -{ - internal static List CalcTrix( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - int? signalPeriods) - { - // check parameter arguments - ValidateTrix(lookbackPeriods); - - // initialize - int length = tpList.Count; - List results = new(length); - - double k = 2d / (lookbackPeriods + 1); - double? lastEma1 = 0; - double? lastEma2; - double? lastEma3; - int initPeriods = Math.Min(lookbackPeriods, length); - - for (int i = 0; i < initPeriods; i++) - { - lastEma1 += tpList[i].Item2; - } - - lastEma1 /= initPeriods; - lastEma2 = lastEma3 = lastEma1; - - // compose final results - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - TrixResult r = new(date); - results.Add(r); - - if (i >= lookbackPeriods) - { - double? ema1 = lastEma1 + (k * (value - lastEma1)); - double? ema2 = lastEma2 + (k * (ema1 - lastEma2)); - double? ema3 = lastEma3 + (k * (ema2 - lastEma3)); - - r.Ema3 = ema3.NaN2Null(); - r.Trix = (100d * (ema3 - lastEma3) / lastEma3).NaN2Null(); - - lastEma1 = ema1; - lastEma2 = ema2; - lastEma3 = ema3; - } - - // optional SMA signal - CalcTrixSignal(signalPeriods, i, lookbackPeriods, results); - } - - return results; - } - - // internals - private static void CalcTrixSignal( - int? signalPeriods, int i, int lookbackPeriods, List results) - { - if (signalPeriods != null && i >= (lookbackPeriods + signalPeriods - 1)) - { - double? sumSma = 0; - for (int p = i + 1 - (int)signalPeriods; p <= i; p++) - { - sumSma += results[p].Trix; - } - - results[i].Signal = sumSma / signalPeriods; - } - } - - // parameter validation - private static void ValidateTrix( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for TRIX."); - } - } -} diff --git a/src/s-z/Trix/Trix.StaticSeries.cs b/src/s-z/Trix/Trix.StaticSeries.cs new file mode 100644 index 000000000..a94818133 --- /dev/null +++ b/src/s-z/Trix/Trix.StaticSeries.cs @@ -0,0 +1,78 @@ +namespace Skender.Stock.Indicators; + +// TRIPLE EMA OSCILLATOR - TRIX (SERIES) + +public static partial class Trix +{ + public static IReadOnlyList ToTrix( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double k = 2d / (lookbackPeriods + 1); + double lastEma1 = double.NaN; + double lastEma2 = double.NaN; + double lastEma3 = double.NaN; + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + // skip incalculable periods + if (i < lookbackPeriods - 1) + { + results.Add(new(s.Timestamp)); + continue; + } + + double ema1; + double ema2; + double ema3; + + // when no prior EMA, reset as SMA + if (double.IsNaN(lastEma3)) + { + double sum = 0; + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + T ps = source[p]; + sum += ps.Value; + } + + ema1 = ema2 = ema3 = sum / lookbackPeriods; + + results.Add(new(s.Timestamp)); + } + + // normal TRIX + else + { + ema1 = lastEma1 + (k * (s.Value - lastEma1)); + ema2 = lastEma2 + (k * (ema1 - lastEma2)); + ema3 = lastEma3 + (k * (ema2 - lastEma3)); + + double trix = 100 * (ema3 - lastEma3) / lastEma3; + + results.Add(new TrixResult( + Timestamp: s.Timestamp, + Ema3: ema3.NaN2Null(), + Trix: trix.NaN2Null())); + } + + lastEma1 = ema1; + lastEma2 = ema2; + lastEma3 = ema3; + } + + return results; + } +} diff --git a/src/s-z/Trix/Trix.Utilities.cs b/src/s-z/Trix/Trix.Utilities.cs index 2849feb50..ce869af82 100644 --- a/src/s-z/Trix/Trix.Utilities.cs +++ b/src/s-z/Trix/Trix.Utilities.cs @@ -1,17 +1,30 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// TRIPLE EMA OSCILLATOR - TRIX (UTILITIES) + +public static partial class Trix { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int n = results .ToList() .FindIndex(x => x.Trix != null); - return results.Remove((3 * n) + 100); + return results.Remove(3 * n + 100); + } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for TRIX."); + } } } diff --git a/src/s-z/Trix/info.xml b/src/s-z/Trix/info.xml index 67c8c5884..00f9697cb 100644 --- a/src/s-z/Trix/info.xml +++ b/src/s-z/Trix/info.xml @@ -12,7 +12,6 @@ Configurable Quote type. See Guide for more information. Historical price quotes. Number of periods in the lookback window. - Optional. Number of periods for a TRIX SMA signal line. Time series of TRIX values. Invalid parameter value provided. \ No newline at end of file diff --git a/src/s-z/Tsi/Tsi.Api.cs b/src/s-z/Tsi/Tsi.Api.cs deleted file mode 100644 index 9cbb03e3d..000000000 --- a/src/s-z/Tsi/Tsi.Api.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TRUE STRENGTH INDEX (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetTsi( - this IEnumerable quotes, - int lookbackPeriods = 25, - int smoothPeriods = 13, - int signalPeriods = 7) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcTsi(lookbackPeriods, smoothPeriods, signalPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetTsi( - this IEnumerable results, - int lookbackPeriods = 25, - int smoothPeriods = 13, - int signalPeriods = 7) => results - .ToTuple() - .CalcTsi(lookbackPeriods, smoothPeriods, signalPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetTsi( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods = 25, - int smoothPeriods = 13, - int signalPeriods = 7) => priceTuples - .ToSortedList() - .CalcTsi(lookbackPeriods, smoothPeriods, signalPeriods); -} diff --git a/src/s-z/Tsi/Tsi.Models.cs b/src/s-z/Tsi/Tsi.Models.cs index c01f12784..35a2caf2c 100644 --- a/src/s-z/Tsi/Tsi.Models.cs +++ b/src/s-z/Tsi/Tsi.Models.cs @@ -1,15 +1,12 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class TsiResult : ResultBase, IReusableResult +public record TsiResult +( + DateTime Timestamp, + double? Tsi = null, + double? Signal = null +) : IReusable { - public TsiResult(DateTime date) - { - Date = date; - } - - public double? Tsi { get; set; } - public double? Signal { get; set; } - - double? IReusableResult.Value => Tsi; + public double Value => Tsi.Null2NaN(); } diff --git a/src/s-z/Tsi/Tsi.Series.cs b/src/s-z/Tsi/Tsi.Series.cs deleted file mode 100644 index c89f62891..000000000 --- a/src/s-z/Tsi/Tsi.Series.cs +++ /dev/null @@ -1,161 +0,0 @@ -namespace Skender.Stock.Indicators; - -// TRUE STRENGTH INDEX (SERIES) -public static partial class Indicator -{ - internal static List CalcTsi( - this List<(DateTime, double)> tpList, - int lookbackPeriods, - int smoothPeriods, - int signalPeriods) - { - // check parameter arguments - ValidateTsi(lookbackPeriods, smoothPeriods, signalPeriods); - - // initialize - int length = tpList.Count; - double mult1 = 2d / (lookbackPeriods + 1); - double mult2 = 2d / (smoothPeriods + 1); - double multS = 2d / (signalPeriods + 1); - double sumS = 0; - - List results = new(length); - - double[] c = new double[length]; // price change - double[] cs1 = new double[length]; // smooth 1 - double[] cs2 = new double[length]; // smooth 2 - double sumC = 0; - double sumC1 = 0; - - double[] a = new double[length]; // abs of price change - double[] as1 = new double[length]; // smooth 1 - double[] as2 = new double[length]; // smooth 2 - double sumA = 0; - double sumA1 = 0; - - // roll through quotes - for (int i = 0; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - TsiResult r = new(date); - results.Add(r); - - // skip first period - if (i == 0) - { - continue; - } - - // price change - c[i] = value - tpList[i - 1].Item2; - a[i] = Math.Abs(c[i]); - - // smoothing - if (i > lookbackPeriods) - { - // first smoothing - cs1[i] = ((c[i] - cs1[i - 1]) * mult1) + cs1[i - 1]; - as1[i] = ((a[i] - as1[i - 1]) * mult1) + as1[i - 1]; - - // second smoothing - if (i + 1 > lookbackPeriods + smoothPeriods) - { - cs2[i] = ((cs1[i] - cs2[i - 1]) * mult2) + cs2[i - 1]; - as2[i] = ((as1[i] - as2[i - 1]) * mult2) + as2[i - 1]; - - double tsi = (as2[i] != 0) ? 100d * (cs2[i] / as2[i]) : double.NaN; - r.Tsi = tsi.NaN2Null(); - - // signal line - if (signalPeriods > 0) - { - int startSignal = lookbackPeriods + smoothPeriods + signalPeriods - 1; - - if (i >= startSignal) - { - r.Signal = ((tsi - results[i - 1].Signal) * multS).NaN2Null() - + results[i - 1].Signal; - } - - // initialize signal - else if (i == startSignal - 1) - { - sumS += tsi; - r.Signal = sumS / signalPeriods; - } - - // warmup signal - else - { - sumS += tsi; - } - } - } - - // prepare second smoothing - else - { - sumC1 += cs1[i]; - sumA1 += as1[i]; - - // inialize second smoothing - if (i + 1 == lookbackPeriods + smoothPeriods) - { - cs2[i] = sumC1 / smoothPeriods; - as2[i] = sumA1 / smoothPeriods; - - double tsi = (as2[i] != 0) ? 100 * cs2[i] / as2[i] : double.NaN; - r.Tsi = tsi; - sumS = tsi; - } - } - } - - // prepare first smoothing - else - { - sumC += c[i]; - sumA += a[i]; - - // initialize first smoothing - if (i == lookbackPeriods) - { - cs1[i] = sumC / lookbackPeriods; - as1[i] = sumA / lookbackPeriods; - - sumC1 = cs1[i]; - sumA1 = as1[i]; - } - } - } - - return results; - } - - // parameter validation - private static void ValidateTsi( - int lookbackPeriods, - int smoothPeriods, - int signalPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for TSI."); - } - - if (smoothPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smoothPeriods), smoothPeriods, - "Smoothing periods must be greater than 0 for TSI."); - } - - if (signalPeriods < 0) - { - throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, - "Signal periods must be greater than or equal to 0 for TSI."); - } - } -} diff --git a/src/s-z/Tsi/Tsi.StaticSeries.cs b/src/s-z/Tsi/Tsi.StaticSeries.cs new file mode 100644 index 000000000..578f6ab88 --- /dev/null +++ b/src/s-z/Tsi/Tsi.StaticSeries.cs @@ -0,0 +1,149 @@ +namespace Skender.Stock.Indicators; + +// TRUE STRENGTH INDEX (SERIES) + +public static partial class Tsi +{ + public static IReadOnlyList ToTsi( + this IReadOnlyList source, + int lookbackPeriods = 25, + int smoothPeriods = 13, + int signalPeriods = 7) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods, smoothPeriods, signalPeriods); + + // initialize + int length = source.Count; + double mult1 = 2d / (lookbackPeriods + 1); + double mult2 = 2d / (smoothPeriods + 1); + double multS = 2d / (signalPeriods + 1); + List results = new(length); + + double[] c = new double[length]; // price change + double[] cs1 = new double[length]; // smooth 1 + double[] cs2 = new double[length]; // smooth 2 + + double[] a = new double[length]; // abs of price change + double[] as1 = new double[length]; // smooth 1 + double[] as2 = new double[length]; // smooth 2 + + double prevSignal = double.NaN; + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + // skip first period + if (i == 0) + { + a[i] = double.NaN; + c[i] = double.NaN; + cs1[i] = double.NaN; + as1[i] = double.NaN; + cs2[i] = double.NaN; + as2[i] = double.NaN; + + results.Add(new(s.Timestamp)); + continue; + } + + // price change + c[i] = s.Value - source[i - 1].Value; + a[i] = Math.Abs(c[i]); + + // re/initialize first smoothing + if (double.IsNaN(cs1[i - 1]) && i >= lookbackPeriods) + { + double sumC = 0; + double sumA = 0; + + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + sumC += c[p]; + sumA += a[p]; + } + + cs1[i] = sumC / lookbackPeriods; + as1[i] = sumA / lookbackPeriods; + } + + // normal first smoothing + else + { + cs1[i] = ((c[i] - cs1[i - 1]) * mult1) + cs1[i - 1]; + as1[i] = ((a[i] - as1[i - 1]) * mult1) + as1[i - 1]; + } + + // re/initialize second smoothing + if (double.IsNaN(cs2[i - 1]) && i >= smoothPeriods) + { + double sumCs = 0; + double sumAs = 0; + + for (int p = i - smoothPeriods + 1; p <= i; p++) + { + sumCs += cs1[p]; + sumAs += as1[p]; + } + + cs2[i] = sumCs / smoothPeriods; + as2[i] = sumAs / smoothPeriods; + } + + // normal second smoothing + else + { + cs2[i] = ((cs1[i] - cs2[i - 1]) * mult2) + cs2[i - 1]; + as2[i] = ((as1[i] - as2[i - 1]) * mult2) + as2[i - 1]; + } + + // true strength index + double tsi = as2[i] != 0 + ? 100d * (cs2[i] / as2[i]) + : double.NaN; + + // signal line + double signal; + + if (signalPeriods > 1) + { + // re/initialize signal + if (double.IsNaN(prevSignal) && i > signalPeriods) + { + double sum = tsi; + for (int p = i - signalPeriods + 1; p < i; p++) + { + sum += results[p].Tsi.Null2NaN(); + } + + signal = sum / signalPeriods; + } + + // normal signal + else + { + signal = ((tsi - prevSignal) * multS) + prevSignal; + } + } + else + { + signal = signalPeriods == 1 + ? tsi + : double.NaN; + } + + results.Add(new TsiResult( + Timestamp: s.Timestamp, + Tsi: tsi.NaN2Null(), + Signal: signal.NaN2Null())); + + prevSignal = signal; + } + + return results; + } +} diff --git a/src/s-z/Tsi/Tsi.Utilities.cs b/src/s-z/Tsi/Tsi.Utilities.cs index cc5efd837..16b08c8ef 100644 --- a/src/s-z/Tsi/Tsi.Utilities.cs +++ b/src/s-z/Tsi/Tsi.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// TRUE STRENGTH INDEX (UTILITIES) + +public static partial class Tsi { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int nm = results .ToList() @@ -14,4 +15,30 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(nm + 250); } + + // parameter validation + internal static void Validate( + int lookbackPeriods, + int smoothPeriods, + int signalPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for TSI."); + } + + if (smoothPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(smoothPeriods), smoothPeriods, + "Smoothing periods must be greater than 0 for TSI."); + } + + if (signalPeriods < 0) + { + throw new ArgumentOutOfRangeException(nameof(signalPeriods), signalPeriods, + "Signal periods must be greater than or equal to 0 for TSI."); + } + } } diff --git a/src/s-z/UlcerIndex/UlcerIndex.Api.cs b/src/s-z/UlcerIndex/UlcerIndex.Api.cs deleted file mode 100644 index 2ca2b6828..000000000 --- a/src/s-z/UlcerIndex/UlcerIndex.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ULCER INDEX (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetUlcerIndex( - this IEnumerable quotes, - int lookbackPeriods = 14) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcUlcerIndex(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetUlcerIndex( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcUlcerIndex(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetUlcerIndex( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcUlcerIndex(lookbackPeriods); -} diff --git a/src/s-z/UlcerIndex/UlcerIndex.Models.cs b/src/s-z/UlcerIndex/UlcerIndex.Models.cs index 81064ae3b..72332f6a0 100644 --- a/src/s-z/UlcerIndex/UlcerIndex.Models.cs +++ b/src/s-z/UlcerIndex/UlcerIndex.Models.cs @@ -1,14 +1,14 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class UlcerIndexResult : ResultBase, IReusableResult +public record UlcerIndexResult +( + DateTime Timestamp, + double? UlcerIndex +) : IReusable { - public UlcerIndexResult(DateTime date) - { - Date = date; - } + public double Value => UlcerIndex.Null2NaN(); - public double? UI { get; set; } // ulcer index - - double? IReusableResult.Value => UI; + [Obsolete("Rename UI to UlcerIndex")] // v3.0.0 + public double? UI => UlcerIndex; } diff --git a/src/s-z/UlcerIndex/UlcerIndex.Series.cs b/src/s-z/UlcerIndex/UlcerIndex.Series.cs deleted file mode 100644 index 9d217a036..000000000 --- a/src/s-z/UlcerIndex/UlcerIndex.Series.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ULCER INDEX (SERIES) -public static partial class Indicator -{ - internal static List CalcUlcerIndex( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateUlcer(lookbackPeriods); - - // initialize - List results = new(tpList.Count); - - // roll through quotes - for (int i = 0; i < tpList.Count; i++) - { - (DateTime date, double _) = tpList[i]; - - UlcerIndexResult r = new(date); - results.Add(r); - - if (i + 1 >= lookbackPeriods) - { - double sumSquared = 0; - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - (DateTime _, double pValue) = tpList[p]; - int dIndex = p + 1; - - double maxClose = 0; - for (int s = i + 1 - lookbackPeriods; s < dIndex; s++) - { - (DateTime _, double sValue) = tpList[s]; - if (sValue > maxClose) - { - maxClose = sValue; - } - } - - double percentDrawdown = (maxClose == 0) ? double.NaN - : 100 * ((pValue - maxClose) / maxClose); - - sumSquared += percentDrawdown * percentDrawdown; - } - - r.UI = Math.Sqrt(sumSquared / lookbackPeriods).NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidateUlcer( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Ulcer Index."); - } - } -} diff --git a/src/s-z/UlcerIndex/UlcerIndex.StaticSeries.cs b/src/s-z/UlcerIndex/UlcerIndex.StaticSeries.cs new file mode 100644 index 000000000..29f7a3bc3 --- /dev/null +++ b/src/s-z/UlcerIndex/UlcerIndex.StaticSeries.cs @@ -0,0 +1,66 @@ +namespace Skender.Stock.Indicators; + +// ULCER INDEX (SERIES) + +public static partial class UlcerIndex +{ + public static IReadOnlyList ToUlcerIndex( + this IReadOnlyList source, + int lookbackPeriods = 14) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + double? ui; + + if (i + 1 >= lookbackPeriods) + { + double sumSquared = 0; + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + T ps = source[p]; + int dIndex = p + 1; + + double maxClose = 0; + for (int z = i + 1 - lookbackPeriods; z < dIndex; z++) + { + T zs = source[z]; + if (zs.Value > maxClose) + { + maxClose = zs.Value; + } + } + + double percentDrawdown = maxClose == 0 ? double.NaN + : 100 * ((ps.Value - maxClose) / maxClose); + + sumSquared += percentDrawdown * percentDrawdown; + } + + ui = Math.Sqrt(sumSquared / lookbackPeriods).NaN2Null(); + } + else + { + ui = null; + } + + UlcerIndexResult r = new( + Timestamp: s.Timestamp, + UlcerIndex: ui); + results.Add(r); + } + + return results; + } +} diff --git a/src/s-z/UlcerIndex/UlcerIndex.Utilities.cs b/src/s-z/UlcerIndex/UlcerIndex.Utilities.cs index 57dff98a9..26679c7a6 100644 --- a/src/s-z/UlcerIndex/UlcerIndex.Utilities.cs +++ b/src/s-z/UlcerIndex/UlcerIndex.Utilities.cs @@ -1,17 +1,18 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// ULCER INDEX (UTILITIES) + +public static partial class UlcerIndex { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.UI != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Ulcer Index."); + } } } diff --git a/src/s-z/Ultimate/Ultimate.Api.cs b/src/s-z/Ultimate/Ultimate.Api.cs deleted file mode 100644 index 702908f00..000000000 --- a/src/s-z/Ultimate/Ultimate.Api.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ULTIMATE OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetUltimate( - this IEnumerable quotes, - int shortPeriods = 7, - int middlePeriods = 14, - int longPeriods = 28) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcUltimate(shortPeriods, middlePeriods, longPeriods); -} diff --git a/src/s-z/Ultimate/Ultimate.Models.cs b/src/s-z/Ultimate/Ultimate.Models.cs index 4238d731a..b8b82ba3f 100644 --- a/src/s-z/Ultimate/Ultimate.Models.cs +++ b/src/s-z/Ultimate/Ultimate.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class UltimateResult : ResultBase, IReusableResult +public record UltimateResult +( + DateTime Timestamp, + double? Ultimate +) : IReusable { - public UltimateResult(DateTime date) - { - Date = date; - } - - public double? Ultimate { get; set; } - - double? IReusableResult.Value => Ultimate; + public double Value => Ultimate.Null2NaN(); } diff --git a/src/s-z/Ultimate/Ultimate.Series.cs b/src/s-z/Ultimate/Ultimate.Series.cs deleted file mode 100644 index 4fc3bcba3..000000000 --- a/src/s-z/Ultimate/Ultimate.Series.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ULTIMATE OSCILLATOR (SERIES) -public static partial class Indicator -{ - internal static List CalcUltimate( - this List qdList, - int shortPeriods, - int middlePeriods, - int longPeriods) - { - // check parameter arguments - ValidateUltimate(shortPeriods, middlePeriods, longPeriods); - - // initialize - int length = qdList.Count; - List results = new(length); - double[] bp = new double[length]; // buying pressure - double[] tr = new double[length]; // true range - - double priorClose = 0; - - // roll through quotes - for (int i = 0; i < qdList.Count; i++) - { - QuoteD q = qdList[i]; - - UltimateResult r = new(q.Date); - results.Add(r); - - if (i > 0) - { - bp[i] = q.Close - Math.Min(q.Low, priorClose); - tr[i] = Math.Max(q.High, priorClose) - Math.Min(q.Low, priorClose); - } - - if (i >= longPeriods) - { - double sumBP1 = 0; - double sumBP2 = 0; - double sumBP3 = 0; - - double sumTR1 = 0; - double sumTR2 = 0; - double sumTR3 = 0; - - for (int p = i + 1 - longPeriods; p <= i; p++) - { - int pIndex = p + 1; - - // short aggregate - if (pIndex > i + 1 - shortPeriods) - { - sumBP1 += bp[p]; - sumTR1 += tr[p]; - } - - // middle aggregate - if (pIndex > i + 1 - middlePeriods) - { - sumBP2 += bp[p]; - sumTR2 += tr[p]; - } - - // long aggregate - sumBP3 += bp[p]; - sumTR3 += tr[p]; - } - - double avg1 = (sumTR1 == 0) ? double.NaN : sumBP1 / sumTR1; - double avg2 = (sumTR2 == 0) ? double.NaN : sumBP2 / sumTR2; - double avg3 = (sumTR3 == 0) ? double.NaN : sumBP3 / sumTR3; - - r.Ultimate = (100d * ((4d * avg1) + (2d * avg2) + avg3) / 7d).NaN2Null(); - } - - priorClose = q.Close; - } - - return results; - } - - // parameter validation - private static void ValidateUltimate( - int shortPeriods, - int middleAverage, - int longPeriods) - { - // check parameter arguments - if (shortPeriods <= 0 || middleAverage <= 0 || longPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(longPeriods), longPeriods, - "Average periods must be greater than 0 for Ultimate Oscillator."); - } - - if (shortPeriods >= middleAverage || middleAverage >= longPeriods) - { - throw new ArgumentOutOfRangeException(nameof(middleAverage), middleAverage, - "Average periods must be increasingly larger than each other for Ultimate Oscillator."); - } - } -} diff --git a/src/s-z/Ultimate/Ultimate.StaticSeries.cs b/src/s-z/Ultimate/Ultimate.StaticSeries.cs new file mode 100644 index 000000000..a4978492b --- /dev/null +++ b/src/s-z/Ultimate/Ultimate.StaticSeries.cs @@ -0,0 +1,98 @@ +namespace Skender.Stock.Indicators; + +// ULTIMATE OSCILLATOR (SERIES) + +public static partial class Ultimate +{ + public static IReadOnlyList ToUltimate( + this IReadOnlyList quotes, + int shortPeriods = 7, + int middlePeriods = 14, + int longPeriods = 28) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcUltimate(shortPeriods, middlePeriods, longPeriods); + + private static List CalcUltimate( + this IReadOnlyList source, + int shortPeriods, + int middlePeriods, + int longPeriods) + { + // check parameter arguments + Validate(shortPeriods, middlePeriods, longPeriods); + + // initialize + int length = source.Count; + List results = new(length); + double[] bp = new double[length]; // buying pressure + double[] tr = new double[length]; // true range + + double priorClose = 0; + + // roll through source values + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + double? ultimate; + + if (i > 0) + { + bp[i] = q.Close - Math.Min(q.Low, priorClose); + tr[i] = Math.Max(q.High, priorClose) - Math.Min(q.Low, priorClose); + } + + if (i >= longPeriods) + { + double sumBp1 = 0; + double sumBp2 = 0; + double sumBp3 = 0; + + double sumTr1 = 0; + double sumTr2 = 0; + double sumTr3 = 0; + + for (int p = i + 1 - longPeriods; p <= i; p++) + { + int pIndex = p + 1; + + // short aggregate + if (pIndex > i + 1 - shortPeriods) + { + sumBp1 += bp[p]; + sumTr1 += tr[p]; + } + + // middle aggregate + if (pIndex > i + 1 - middlePeriods) + { + sumBp2 += bp[p]; + sumTr2 += tr[p]; + } + + // long aggregate + sumBp3 += bp[p]; + sumTr3 += tr[p]; + } + + double avg1 = sumTr1 == 0 ? double.NaN : sumBp1 / sumTr1; + double avg2 = sumTr2 == 0 ? double.NaN : sumBp2 / sumTr2; + double avg3 = sumTr3 == 0 ? double.NaN : sumBp3 / sumTr3; + + ultimate = (100d * (4d * avg1 + 2d * avg2 + avg3) / 7d).NaN2Null(); + } + else + { + ultimate = null; + } + + results.Add(new( + Timestamp: q.Timestamp, + Ultimate: ultimate)); + + priorClose = q.Close; + } + + return results; + } +} diff --git a/src/s-z/Ultimate/Ultimate.Utilities.cs b/src/s-z/Ultimate/Ultimate.Utilities.cs index 728d4967a..63c6da0ff 100644 --- a/src/s-z/Ultimate/Ultimate.Utilities.cs +++ b/src/s-z/Ultimate/Ultimate.Utilities.cs @@ -1,17 +1,26 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// ULTIMATE OSCILLATOR (UTILITIES) + +public static partial class Ultimate { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int shortPeriods, + int middleAverage, + int longPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Ultimate != null); + // check parameter arguments + if (shortPeriods <= 0 || middleAverage <= 0 || longPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(longPeriods), longPeriods, + "Average periods must be greater than 0 for Ultimate Oscillator."); + } - return results.Remove(removePeriods); + if (shortPeriods >= middleAverage || middleAverage >= longPeriods) + { + throw new ArgumentOutOfRangeException(nameof(middleAverage), middleAverage, + "Average periods must be increasingly larger than each other for Ultimate Oscillator."); + } } } diff --git a/src/s-z/VolatilityStop/VolatilityStop.Api.cs b/src/s-z/VolatilityStop/VolatilityStop.Api.cs deleted file mode 100644 index 751f3b215..000000000 --- a/src/s-z/VolatilityStop/VolatilityStop.Api.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Skender.Stock.Indicators; - -// VOLATILITY SYSTEM/STOP (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetVolatilityStop( - this IEnumerable quotes, - int lookbackPeriods = 7, - double multiplier = 3) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcVolatilityStop(lookbackPeriods, multiplier); -} diff --git a/src/s-z/VolatilityStop/VolatilityStop.Models.cs b/src/s-z/VolatilityStop/VolatilityStop.Models.cs index 1382b24e4..2c729a914 100644 --- a/src/s-z/VolatilityStop/VolatilityStop.Models.cs +++ b/src/s-z/VolatilityStop/VolatilityStop.Models.cs @@ -1,19 +1,17 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class VolatilityStopResult : ResultBase, IReusableResult -{ - public VolatilityStopResult(DateTime date) - { - Date = date; - } - - public double? Sar { get; set; } - public bool? IsStop { get; set; } +public record VolatilityStopResult +( + DateTime Timestamp, + double? Sar = null, + bool? IsStop = null, // SAR values as long/short stop bands - public double? UpperBand { get; set; } - public double? LowerBand { get; set; } + double? UpperBand = null, + double? LowerBand = null - double? IReusableResult.Value => Sar; +) : IReusable +{ + public double Value => Sar.Null2NaN(); } diff --git a/src/s-z/VolatilityStop/VolatilityStop.Series.cs b/src/s-z/VolatilityStop/VolatilityStop.Series.cs deleted file mode 100644 index b49881305..000000000 --- a/src/s-z/VolatilityStop/VolatilityStop.Series.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace Skender.Stock.Indicators; - -// VOLATILITY SYSTEM/STOP (SERIES) -public static partial class Indicator -{ - internal static List CalcVolatilityStop( - this List qdList, - int lookbackPeriods, - double multiplier) - { - // convert quotes - List<(DateTime, double)> tpList = qdList - .ToTuple(CandlePart.Close); - - // check parameter arguments - ValidateVolatilityStop(lookbackPeriods, multiplier); - - // initialize - int length = tpList.Count; - List results = new(length); - - if (length == 0) - { - return results; - } - - List atrList = qdList.CalcAtr(lookbackPeriods); - - // initial trend (guess) - int initPeriods = Math.Min(length, lookbackPeriods); - double sic = tpList[0].Item2; - bool isLong = tpList[initPeriods - 1].Item2 > sic; - - for (int i = 0; i < initPeriods; i++) - { - (DateTime date, double value) = tpList[i]; - sic = isLong ? Math.Max(sic, value) : Math.Min(sic, value); - results.Add(new VolatilityStopResult(date)); - } - - // roll through quotes - for (int i = lookbackPeriods; i < length; i++) - { - (DateTime date, double value) = tpList[i]; - - // average true range × multiplier constant - double? arc = atrList[i - 1].Atr * multiplier; - - VolatilityStopResult r = new(date) { - // stop and reverse threshold - Sar = isLong ? sic - arc : sic + arc - }; - results.Add(r); - - // add SAR as separate bands - if (isLong) - { - r.LowerBand = r.Sar; - } - else - { - r.UpperBand = r.Sar; - } - - // evaluate stop and reverse - if ((isLong && value < r.Sar) - || (!isLong && value > r.Sar)) - { - r.IsStop = true; - sic = value; - isLong = !isLong; - } - else - { - r.IsStop = false; - - // significant close adjustment - // extreme favorable close (value) while in trade - sic = isLong ? Math.Max(sic, value) : Math.Min(sic, value); - } - } - - // remove first trend to stop, since it is a guess - VolatilityStopResult? firstStop = results - .Where(x => x.IsStop == true) - .OrderBy(x => x.Date) - .FirstOrDefault(); - - if (firstStop != null) - { - int cutIndex = results.IndexOf(firstStop); - - for (int d = 0; d <= cutIndex; d++) - { - VolatilityStopResult r = results[d]; - r.Sar = null; - r.UpperBand = null; - r.LowerBand = null; - r.IsStop = null; - } - } - - return results; - } - - // parameter validation - private static void ValidateVolatilityStop( - int lookbackPeriods, - double multiplier) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for Volatility Stop."); - } - - if (multiplier <= 0) - { - throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, - "ATR Multiplier must be greater than 0 for Volatility Stop."); - } - } -} diff --git a/src/s-z/VolatilityStop/VolatilityStop.StaticSeries.cs b/src/s-z/VolatilityStop/VolatilityStop.StaticSeries.cs new file mode 100644 index 000000000..6693ca1a4 --- /dev/null +++ b/src/s-z/VolatilityStop/VolatilityStop.StaticSeries.cs @@ -0,0 +1,119 @@ +namespace Skender.Stock.Indicators; + +// VOLATILITY SYSTEM/STOP (SERIES) + +public static partial class VolatilityStop +{ + public static IReadOnlyList ToVolatilityStop( + this IReadOnlyList quotes, + int lookbackPeriods = 7, + double multiplier = 3) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcVolatilityStop(lookbackPeriods, multiplier); + + private static List CalcVolatilityStop( + this IReadOnlyList source, + int lookbackPeriods, + double multiplier) + { + //convert quotes + List reList = source + .Cast() + .ToList(); + + // check parameter arguments + Validate(lookbackPeriods, multiplier); + + // initialize + int length = source.Count; + List results = new(length); + + if (length == 0) + { + return results; + } + + List atrList = source.CalcAtr(lookbackPeriods); + + // initial trend (guess) + int initPeriods = Math.Min(length, lookbackPeriods); + double sic = reList[0].Value; + bool isLong = reList[initPeriods - 1].Value > sic; + + for (int i = 0; i < initPeriods; i++) + { + IReusable init = reList[i]; + sic = isLong ? Math.Max(sic, init.Value) : Math.Min(sic, init.Value); + results.Add(new(init.Timestamp)); + } + + // roll through source values + for (int i = lookbackPeriods; i < length; i++) + { + IReusable s = reList[i]; + + // average true range × multiplier constant + double? arc = atrList[i - 1].Atr * multiplier; + + // stop and reverse threshold + double? sar = isLong ? sic - arc : sic + arc; + + // add SAR as separate bands + double? lowerBand = null; + double? upperBand = null; + + if (isLong) + { + lowerBand = sar; + } + else + { + upperBand = sar; + } + + // evaluate stop and reverse + bool? isStop; + + if ((isLong && s.Value < sar) + || (!isLong && s.Value > sar)) + { + isStop = true; + sic = s.Value; + isLong = !isLong; + } + else + { + isStop = false; + + // significant close adjustment + // extreme favorable close (value) while in trade + sic = isLong ? Math.Max(sic, s.Value) : Math.Min(sic, s.Value); + } + + results.Add(new VolatilityStopResult( + Timestamp: s.Timestamp, + Sar: sar, + IsStop: isStop, + UpperBand: upperBand, + LowerBand: lowerBand)); + } + + // remove trend to first stop, since it is a guess + int cutIndex = results.FindIndex(x => x.IsStop ?? false); + + for (int d = 0; d <= cutIndex; d++) + { + VolatilityStopResult r = results[d]; + + results[d] = r with { + Sar = null, + UpperBand = null, + LowerBand = null, + IsStop = null + }; + } + + return results; + } +} diff --git a/src/s-z/VolatilityStop/VolatilityStop.Utilities.cs b/src/s-z/VolatilityStop/VolatilityStop.Utilities.cs index cb0fb830d..ec65d0404 100644 --- a/src/s-z/VolatilityStop/VolatilityStop.Utilities.cs +++ b/src/s-z/VolatilityStop/VolatilityStop.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// VOLATILITY SYSTEM/STOP (UTILITIES) + +public static partial class VolatilityStop { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -16,4 +17,23 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int lookbackPeriods, + double multiplier) + { + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for Volatility Stop."); + } + + if (multiplier <= 0) + { + throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier, + "ATR Multiplier must be greater than 0 for Volatility Stop."); + } + } } diff --git a/src/s-z/Vortex/Vortex.Api.cs b/src/s-z/Vortex/Vortex.Api.cs deleted file mode 100644 index d6e30a2b3..000000000 --- a/src/s-z/Vortex/Vortex.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// VORTEX INDICATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetVortex( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcVortex(lookbackPeriods); -} diff --git a/src/s-z/Vortex/Vortex.Models.cs b/src/s-z/Vortex/Vortex.Models.cs index 1d46cce2b..29fa3cf0a 100644 --- a/src/s-z/Vortex/Vortex.Models.cs +++ b/src/s-z/Vortex/Vortex.Models.cs @@ -1,13 +1,9 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class VortexResult : ResultBase -{ - public VortexResult(DateTime date) - { - Date = date; - } - - public double? Pvi { get; set; } - public double? Nvi { get; set; } -} +public record VortexResult +( + DateTime Timestamp, + double? Pvi = null, + double? Nvi = null +) : ISeries; diff --git a/src/s-z/Vortex/Vortex.Series.cs b/src/s-z/Vortex/Vortex.StaticSeries.cs similarity index 66% rename from src/s-z/Vortex/Vortex.Series.cs rename to src/s-z/Vortex/Vortex.StaticSeries.cs index c2eab9e30..2d6ce4549 100644 --- a/src/s-z/Vortex/Vortex.Series.cs +++ b/src/s-z/Vortex/Vortex.StaticSeries.cs @@ -1,17 +1,25 @@ namespace Skender.Stock.Indicators; // VORTEX INDICATOR (SERIES) -public static partial class Indicator + +public static partial class Vortex { - internal static List CalcVortex( - this List qdList, + public static IReadOnlyList ToVortex( + this IReadOnlyList quotes, + int lookbackPeriods) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcVortex(lookbackPeriods); + + private static List CalcVortex( + this IReadOnlyList source, int lookbackPeriods) { // check parameter arguments - ValidateVortex(lookbackPeriods); + Validate(lookbackPeriods); // initialize - int length = qdList.Count; + int length = source.Count; List results = new(length); double[] tr = new double[length]; @@ -22,13 +30,10 @@ internal static List CalcVortex( double prevLow = 0; double prevClose = 0; - // roll through quotes + // roll through source values for (int i = 0; i < length; i++) { - QuoteD q = qdList[i]; - - VortexResult r = new(q.Date); - results.Add(r); + QuoteD q = source[i]; // skip first period if (i == 0) @@ -36,6 +41,8 @@ internal static List CalcVortex( prevHigh = q.High; prevLow = q.Low; prevClose = q.Close; + + results.Add(new(q.Timestamp)); continue; } @@ -51,6 +58,9 @@ internal static List CalcVortex( prevLow = q.Low; prevClose = q.Close; + double pvi = double.NaN; + double nvi = double.NaN; + // vortex indicator if (i + 1 > lookbackPeriods) { @@ -67,24 +77,17 @@ internal static List CalcVortex( if (sumTr is not 0) { - r.Pvi = sumPvm / sumTr; - r.Nvi = sumNvm / sumTr; + pvi = sumPvm / sumTr; + nvi = sumNvm / sumTr; } } + + results.Add(new VortexResult( + Timestamp: q.Timestamp, + Pvi: pvi.NaN2Null(), + Nvi: nvi.NaN2Null())); } return results; } - - // parameter validation - private static void ValidateVortex( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 1) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 1 for VI."); - } - } } diff --git a/src/s-z/Vortex/Vortex.Utilities.cs b/src/s-z/Vortex/Vortex.Utilities.cs index 91c9feac6..22e449b50 100644 --- a/src/s-z/Vortex/Vortex.Utilities.cs +++ b/src/s-z/Vortex/Vortex.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// VORTEX INDICATOR (UTILITIES) + +public static partial class Vortex { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -19,10 +20,9 @@ public static IEnumerable Condense( } // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -30,4 +30,16 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 1) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 1 for VI."); + } + } } diff --git a/src/s-z/Vwap/Vwap.Api.cs b/src/s-z/Vwap/Vwap.Api.cs deleted file mode 100644 index e45502dd6..000000000 --- a/src/s-z/Vwap/Vwap.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// VOLUME WEIGHTED AVERAGE PRICE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetVwap( - this IEnumerable quotes, - DateTime? startDate = null) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcVwap(startDate); -} diff --git a/src/s-z/Vwap/Vwap.Models.cs b/src/s-z/Vwap/Vwap.Models.cs index b6df0b221..5b4400319 100644 --- a/src/s-z/Vwap/Vwap.Models.cs +++ b/src/s-z/Vwap/Vwap.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class VwapResult : ResultBase, IReusableResult +public record VwapResult +( + DateTime Timestamp, + double? Vwap +) : IReusable { - public VwapResult(DateTime date) - { - Date = date; - } - - public double? Vwap { get; set; } - - double? IReusableResult.Value => Vwap; + public double Value => Vwap.Null2NaN(); } diff --git a/src/s-z/Vwap/Vwap.Series.cs b/src/s-z/Vwap/Vwap.Series.cs deleted file mode 100644 index c54de6b86..000000000 --- a/src/s-z/Vwap/Vwap.Series.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Skender.Stock.Indicators; - -// VOLUME WEIGHTED AVERAGE PRICE (SERIES) -public static partial class Indicator -{ - internal static List CalcVwap( - this List qdList, - DateTime? startDate = null) - { - // check parameter arguments - ValidateVwap(qdList, startDate); - - // initialize - int length = qdList.Count; - List results = new(length); - - if (length == 0) - { - return results; - } - - startDate ??= qdList[0].Date; - - double? cumVolume = 0; - double? cumVolumeTP = 0; - - // roll through quotes - for (int i = 0; i < length; i++) - { - QuoteD q = qdList[i]; - double? v = q.Volume; - double? h = q.High; - double? l = q.Low; - double? c = q.Close; - - VwapResult r = new(q.Date); - results.Add(r); - - if (q.Date >= startDate) - { - cumVolume += v; - cumVolumeTP += v * (h + l + c) / 3; - - r.Vwap = (cumVolume != 0) ? (cumVolumeTP / cumVolume) : null; - } - } - - return results; - } - - // parameter validation - private static void ValidateVwap( - List quotesList, - DateTime? startDate) - { - // nothing to do for 0 length - if (quotesList.Count == 0) - { - return; - } - - // check parameter arguments (intentionally after quotes check) - if (startDate < quotesList[0].Date) - { - throw new ArgumentOutOfRangeException(nameof(startDate), startDate, - "Start Date must be within the quotes range for VWAP."); - } - } -} diff --git a/src/s-z/Vwap/Vwap.StaticSeries.cs b/src/s-z/Vwap/Vwap.StaticSeries.cs new file mode 100644 index 000000000..0f31b1505 --- /dev/null +++ b/src/s-z/Vwap/Vwap.StaticSeries.cs @@ -0,0 +1,66 @@ +namespace Skender.Stock.Indicators; + +// VOLUME WEIGHTED AVERAGE PRICE (SERIES) + +public static partial class Vwap +{ + public static IReadOnlyList ToVwap( + this IReadOnlyList quotes, + DateTime? startDate = null) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcVwap(startDate); + + private static List CalcVwap( + this IReadOnlyList source, + DateTime? startDate = null) + { + // check parameter arguments + Validate(source, startDate); + + // initialize + int length = source.Count; + List results = new(length); + + if (length == 0) + { + return results; + } + + startDate ??= source[0].Timestamp; + + double? cumVolume = 0; + double? cumVolumeTp = 0; + + // roll through source values + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + + double? v = q.Volume; + double? h = q.High; + double? l = q.Low; + double? c = q.Close; + + double? vwap; + + if (q.Timestamp >= startDate) + { + cumVolume += v; + cumVolumeTp += v * (h + l + c) / 3; + + vwap = cumVolume != 0 ? cumVolumeTp / cumVolume : null; + } + else + { + vwap = null; + } + + results.Add(new( + Timestamp: q.Timestamp, + Vwap: vwap)); + } + + return results; + } +} diff --git a/src/s-z/Vwap/Vwap.Utilities.cs b/src/s-z/Vwap/Vwap.Utilities.cs index 66ec8a0bc..bf24c9fea 100644 --- a/src/s-z/Vwap/Vwap.Utilities.cs +++ b/src/s-z/Vwap/Vwap.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// VOLUME WEIGHTED AVERAGE PRICE (UTILITIES) + +public static partial class Vwap { // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + /// + public static IReadOnlyList RemoveWarmupPeriods( + this IReadOnlyList results) { int removePeriods = results .ToList() @@ -14,4 +15,23 @@ public static IEnumerable RemoveWarmupPeriods( return results.Remove(removePeriods); } + + // parameter validation + internal static void Validate( + IReadOnlyList quotes, + DateTime? startDate) + { + // nothing to do for 0 length + if (quotes.Count == 0) + { + return; + } + + // check parameter arguments (intentionally after quotes check) + if (startDate < quotes[0].Timestamp) + { + throw new ArgumentOutOfRangeException(nameof(startDate), startDate, + "Start Timestamp must be within the quotes range for VWAP."); + } + } } diff --git a/src/s-z/Vwma/Vwma.Api.cs b/src/s-z/Vwma/Vwma.Api.cs deleted file mode 100644 index 603d2bbf6..000000000 --- a/src/s-z/Vwma/Vwma.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// VOLUME WEIGHTED MOVING AVERAGE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetVwma( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcVwma(lookbackPeriods); -} diff --git a/src/s-z/Vwma/Vwma.Models.cs b/src/s-z/Vwma/Vwma.Models.cs index 45774b6c4..647136fbe 100644 --- a/src/s-z/Vwma/Vwma.Models.cs +++ b/src/s-z/Vwma/Vwma.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class VwmaResult : ResultBase, IReusableResult +public record VwmaResult +( + DateTime Timestamp, + double? Vwma +) : IReusable { - public VwmaResult(DateTime date) - { - Date = date; - } - - public double? Vwma { get; set; } - - double? IReusableResult.Value => Vwma; + public double Value => Vwma.Null2NaN(); } diff --git a/src/s-z/Vwma/Vwma.Series.cs b/src/s-z/Vwma/Vwma.Series.cs deleted file mode 100644 index 632c13098..000000000 --- a/src/s-z/Vwma/Vwma.Series.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace Skender.Stock.Indicators; - -// VOLUME WEIGHTED MOVING AVERAGE (SERIES) -public static partial class Indicator -{ - internal static List CalcVwma( - this List qdList, - int lookbackPeriods) - { - // check parameter arguments - ValidateVwma(lookbackPeriods); - - // initialize - int length = qdList.Count; - List results = new(length); - - // roll through quotes - for (int i = 0; i < length; i++) - { - QuoteD q = qdList[i]; - - VwmaResult r = new(q.Date); - results.Add(r); - - if (i + 1 >= lookbackPeriods) - { - double? sumCl = 0; - double? sumVl = 0; - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - QuoteD d = qdList[p]; - double? c = d.Close; - double? v = d.Volume; - - sumCl += c * v; - sumVl += v; - } - - r.Vwma = sumVl != 0 ? (sumCl / sumVl) : null; - } - } - - return results; - } - - // parameter validation - private static void ValidateVwma( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for Vwma."); - } - } -} diff --git a/src/s-z/Vwma/Vwma.StaticSeries.cs b/src/s-z/Vwma/Vwma.StaticSeries.cs new file mode 100644 index 000000000..20c4c689e --- /dev/null +++ b/src/s-z/Vwma/Vwma.StaticSeries.cs @@ -0,0 +1,60 @@ +namespace Skender.Stock.Indicators; + +// VOLUME WEIGHTED MOVING AVERAGE (SERIES) + +public static partial class Vwma +{ + public static IReadOnlyList ToVwma( + this IReadOnlyList quotes, + int lookbackPeriods) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcVwma(lookbackPeriods); + + private static List CalcVwma( + this IReadOnlyList source, + int lookbackPeriods) + { + // check parameter arguments + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + QuoteD q = source[i]; + + double vwma; + + if (i + 1 >= lookbackPeriods) + { + double sumCl = 0; + double sumVl = 0; + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + QuoteD d = source[p]; + double c = d.Close; + double v = d.Volume; + + sumCl += c * v; + sumVl += v; + } + + vwma = sumVl != 0 ? sumCl / sumVl : double.NaN; + } + else + { + vwma = double.NaN; + } + + results.Add(new( + Timestamp: q.Timestamp, + Vwma: vwma.NaN2Null())); + } + + return results; + } +} diff --git a/src/s-z/Vwma/Vwma.Utilities.cs b/src/s-z/Vwma/Vwma.Utilities.cs index f29154581..dd43909b3 100644 --- a/src/s-z/Vwma/Vwma.Utilities.cs +++ b/src/s-z/Vwma/Vwma.Utilities.cs @@ -1,17 +1,18 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// VOLUME WEIGHTED MOVING AVERAGE (UTILITIES) + +public static partial class Vwma { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Vwma != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for Vwma."); + } } } diff --git a/src/s-z/WilliamsR/WilliamsR.Api.cs b/src/s-z/WilliamsR/WilliamsR.Api.cs deleted file mode 100644 index c5b196bd8..000000000 --- a/src/s-z/WilliamsR/WilliamsR.Api.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Skender.Stock.Indicators; - -// WILLIAM %R OSCILLATOR (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetWilliamsR( - this IEnumerable quotes, - int lookbackPeriods = 14) - where TQuote : IQuote => quotes - .ToQuoteD() - .CalcWilliamsR(lookbackPeriods); -} diff --git a/src/s-z/WilliamsR/WilliamsR.Models.cs b/src/s-z/WilliamsR/WilliamsR.Models.cs index a00b47754..0fb8b05aa 100644 --- a/src/s-z/WilliamsR/WilliamsR.Models.cs +++ b/src/s-z/WilliamsR/WilliamsR.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class WilliamsResult : ResultBase, IReusableResult +public record WilliamsResult +( + DateTime Timestamp, + double? WilliamsR +) : IReusable { - public WilliamsResult(DateTime date) - { - Date = date; - } - - public double? WilliamsR { get; set; } - - double? IReusableResult.Value => WilliamsR; + public double Value => WilliamsR.Null2NaN(); } diff --git a/src/s-z/WilliamsR/WilliamsR.Series.cs b/src/s-z/WilliamsR/WilliamsR.Series.cs deleted file mode 100644 index 025bc2e90..000000000 --- a/src/s-z/WilliamsR/WilliamsR.Series.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Skender.Stock.Indicators; - -// WILLIAM %R OSCILLATOR (SERIES -public static partial class Indicator -{ - internal static List CalcWilliamsR( - this List qdList, - int lookbackPeriods) - { - // check parameter arguments - ValidateWilliam(lookbackPeriods); - - // convert Fast Stochastic to William %R - return qdList.CalcStoch(lookbackPeriods, 1, 1, 3, 2, MaType.SMA) - .Select(s => new WilliamsResult(s.Date) { - WilliamsR = s.Oscillator - 100 - }) - .ToList(); - } - - // parameter validation - private static void ValidateWilliam( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for William %R."); - } - } -} diff --git a/src/s-z/WilliamsR/WilliamsR.StaticSeries.cs b/src/s-z/WilliamsR/WilliamsR.StaticSeries.cs new file mode 100644 index 000000000..23becb293 --- /dev/null +++ b/src/s-z/WilliamsR/WilliamsR.StaticSeries.cs @@ -0,0 +1,29 @@ +namespace Skender.Stock.Indicators; + +// WILLIAM %R OSCILLATOR (SERIES) + +public static partial class WilliamsR +{ + public static IReadOnlyList ToWilliamsR( + this IReadOnlyList quotes, + int lookbackPeriods = 14) + where TQuote : IQuote => quotes + .ToQuoteDList() + .CalcWilliamsR(lookbackPeriods); + + private static List CalcWilliamsR( + this IReadOnlyList source, + int lookbackPeriods) + { + // check parameter arguments + Validate(lookbackPeriods); + + // convert Fast Stochastic to William %R + return source.CalcStoch(lookbackPeriods, 1, 1, 3, 2, MaType.SMA) + .Select(s => new WilliamsResult( + Timestamp: s.Timestamp, + WilliamsR: s.Oscillator - 100 + )) + .ToList(); + } +} diff --git a/src/s-z/WilliamsR/WilliamsR.Utilities.cs b/src/s-z/WilliamsR/WilliamsR.Utilities.cs index 7f5563189..49f8d8af1 100644 --- a/src/s-z/WilliamsR/WilliamsR.Utilities.cs +++ b/src/s-z/WilliamsR/WilliamsR.Utilities.cs @@ -1,17 +1,18 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// WILLIAM %R OSCILLATOR (UTILITIES) + +public static partial class WilliamsR { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.WilliamsR != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for William %R."); + } } } diff --git a/src/s-z/Wma/Wma.Api.cs b/src/s-z/Wma/Wma.Api.cs deleted file mode 100644 index a29f543a7..000000000 --- a/src/s-z/Wma/Wma.Api.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Skender.Stock.Indicators; - -// WEIGHTED MOVING AVERAGE (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetWma( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcWma(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetWma( - this IEnumerable results, - int lookbackPeriods) => results - .ToTuple() - .CalcWma(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetWma( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcWma(lookbackPeriods); -} diff --git a/src/s-z/Wma/Wma.Models.cs b/src/s-z/Wma/Wma.Models.cs index 551798d04..41b2e6638 100644 --- a/src/s-z/Wma/Wma.Models.cs +++ b/src/s-z/Wma/Wma.Models.cs @@ -1,14 +1,11 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class WmaResult : ResultBase, IReusableResult +public record WmaResult +( + DateTime Timestamp, + double? Wma +) : IReusable { - public WmaResult(DateTime date) - { - Date = date; - } - - public double? Wma { get; set; } - - double? IReusableResult.Value => Wma; + public double Value => Wma.Null2NaN(); } diff --git a/src/s-z/Wma/Wma.Series.cs b/src/s-z/Wma/Wma.Series.cs deleted file mode 100644 index 15c46f0b3..000000000 --- a/src/s-z/Wma/Wma.Series.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace Skender.Stock.Indicators; - -// WEIGHTED MOVING AVERAGE (SERIES) -public static partial class Indicator -{ - internal static List CalcWma( - this List<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - ValidateWma(lookbackPeriods); - - // initialize - List results = new(tpList.Count); - double divisor = (double)lookbackPeriods * (lookbackPeriods + 1) / 2d; - - // roll through quotes - for (int i = 0; i < tpList.Count; i++) - { - (DateTime date, double _) = tpList[i]; - - WmaResult r = new(date); - results.Add(r); - - if (i + 1 >= lookbackPeriods) - { - double wma = 0; - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - (DateTime _, double pValue) = tpList[p]; - wma += pValue * (lookbackPeriods - (i + 1 - p - 1)) / divisor; - } - - r.Wma = wma.NaN2Null(); - } - } - - return results; - } - - // parameter validation - private static void ValidateWma( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for WMA."); - } - } -} diff --git a/src/s-z/Wma/Wma.StaticSeries.cs b/src/s-z/Wma/Wma.StaticSeries.cs new file mode 100644 index 000000000..95fcc8aac --- /dev/null +++ b/src/s-z/Wma/Wma.StaticSeries.cs @@ -0,0 +1,50 @@ +namespace Skender.Stock.Indicators; + +// WEIGHTED MOVING AVERAGE (SERIES) + +public static partial class Wma +{ + public static IReadOnlyList ToWma( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + ArgumentNullException.ThrowIfNull(source); + Validate(lookbackPeriods); + + // initialize + int length = source.Count; + List results = new(length); + + double divisor = (double)lookbackPeriods * (lookbackPeriods + 1) / 2d; + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + double wma; + + if (i >= lookbackPeriods - 1) + { + wma = 0; + for (int p = i + 1 - lookbackPeriods; p <= i; p++) + { + T ps = source[p]; + wma += ps.Value * (lookbackPeriods - (i + 1 - p - 1)) / divisor; + } + } + else + { + wma = double.NaN; + } + + results.Add(new( + Timestamp: s.Timestamp, + Wma: wma.NaN2Null())); + } + + return results; + } +} diff --git a/src/s-z/Wma/Wma.Utilities.cs b/src/s-z/Wma/Wma.Utilities.cs index f6e9034e3..cd7b65c13 100644 --- a/src/s-z/Wma/Wma.Utilities.cs +++ b/src/s-z/Wma/Wma.Utilities.cs @@ -1,17 +1,18 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// WEIGHTED MOVING AVERAGE (UTILITIES) + +public static partial class Wma { - // remove recommended periods - /// - /// - public static IEnumerable RemoveWarmupPeriods( - this IEnumerable results) + // parameter validation + internal static void Validate( + int lookbackPeriods) { - int removePeriods = results - .ToList() - .FindIndex(x => x.Wma != null); - - return results.Remove(removePeriods); + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for WMA."); + } } } diff --git a/src/s-z/ZigZag/ZigZag.Api.cs b/src/s-z/ZigZag/ZigZag.Api.cs deleted file mode 100644 index f5afcaf5c..000000000 --- a/src/s-z/ZigZag/ZigZag.Api.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Skender.Stock.Indicators; - -// ZIG ZAG (API) -public static partial class Indicator -{ - // SERIES, from TQuote - /// - /// - public static IEnumerable GetZigZag( - this IEnumerable quotes, - EndType endType = EndType.Close, - decimal percentChange = 5) - where TQuote : IQuote => quotes - .ToSortedList() - .CalcZigZag(endType, percentChange); -} diff --git a/src/s-z/ZigZag/ZigZag.Models.cs b/src/s-z/ZigZag/ZigZag.Models.cs index 339b67d79..6e0a410a5 100644 --- a/src/s-z/ZigZag/ZigZag.Models.cs +++ b/src/s-z/ZigZag/ZigZag.Models.cs @@ -1,24 +1,21 @@ namespace Skender.Stock.Indicators; [Serializable] -public sealed class ZigZagResult : ResultBase, IReusableResult +public record ZigZagResult +( + DateTime Timestamp, + decimal? ZigZag = null, // zig zag line + string? PointType = null, // indicates a specific point and type e.g. H or L + decimal? RetraceHigh = null, // zig zag retrace high line + decimal? RetraceLow = null // zig zag retrace low line +) : IReusable { - public ZigZagResult(DateTime date) - { - Date = date; - } - - public decimal? ZigZag { get; set; } // zig zag line - public string? PointType { get; set; } // indicates a specific point and type e.g. H or L - public decimal? RetraceHigh { get; set; } // zig zag retrace high line - public decimal? RetraceLow { get; set; } // zig zag retrace low line - - double? IReusableResult.Value => (double?)ZigZag; + public double Value => ZigZag.Null2NaN(); } internal class ZigZagEval { - internal int Index { get; set; } + internal int Index { get; init; } internal decimal? High { get; set; } internal decimal? Low { get; set; } } diff --git a/src/s-z/ZigZag/ZigZag.Series.cs b/src/s-z/ZigZag/ZigZag.StaticSeries.cs similarity index 62% rename from src/s-z/ZigZag/ZigZag.Series.cs rename to src/s-z/ZigZag/ZigZag.StaticSeries.cs index 29d42485e..8d249e916 100644 --- a/src/s-z/ZigZag/ZigZag.Series.cs +++ b/src/s-z/ZigZag/ZigZag.StaticSeries.cs @@ -1,30 +1,29 @@ namespace Skender.Stock.Indicators; // ZIG ZAG (SERIES) -public static partial class Indicator + +public static partial class ZigZag { - internal static List CalcZigZag( - this List quotesList, + public static IReadOnlyList ToZigZag( + this IReadOnlyList quotes, EndType endType = EndType.Close, decimal percentChange = 5) where TQuote : IQuote { // check parameter arguments - ValidateZigZag(percentChange); + ArgumentNullException.ThrowIfNull(quotes); + Validate(percentChange); // initialize - int length = quotesList.Count; + int length = quotes.Count; List results = new(length); - TQuote q0; if (length == 0) { return results; } - else - { - q0 = quotesList[0]; - } + + TQuote q0 = quotes[0]; ZigZagEval eval = GetZigZagEval(endType, 1, q0); decimal changeThreshold = percentChange / 100m; @@ -47,21 +46,23 @@ internal static List CalcZigZag( PointType = "L" }; - int finalPointIndex = length; - - // roll through quotes, to find initial trend + // roll through source values, to find initial trend for (int i = 0; i < length; i++) { - TQuote q = quotesList[i]; + TQuote q = quotes[i]; int index = i + 1; eval = GetZigZagEval(endType, index, q); - decimal? changeUp = (lastLowPoint.Value == 0) ? null - : (eval.High - lastLowPoint.Value) / lastLowPoint.Value; + decimal? changeUp = lastLowPoint.Value == 0 + ? null + : (eval.High - lastLowPoint.Value) + / lastLowPoint.Value; - decimal? changeDn = (lastHighPoint.Value == 0) ? null - : (lastHighPoint.Value - eval.Low) / lastHighPoint.Value; + decimal? changeDn = lastHighPoint.Value == 0 + ? null + : (lastHighPoint.Value - eval.Low) + / lastHighPoint.Value; if (changeUp >= changeThreshold && changeUp > changeDn) { @@ -81,20 +82,23 @@ internal static List CalcZigZag( } // add first point to results - ZigZagResult firstResult = new(q0.Date); + ZigZagResult firstResult = new(q0.Timestamp); results.Add(firstResult); // find and draw lines - while (lastPoint.Index < finalPointIndex) + while (lastPoint.Index < length) { - ZigZagPoint nextPoint = EvaluateNextPoint(quotesList, endType, changeThreshold, lastPoint); + ZigZagPoint nextPoint = EvaluateNextPoint( + quotes, endType, changeThreshold, lastPoint); + string lastDirection = lastPoint.PointType; // draw line (and reset last point) - DrawZigZagLine(results, quotesList, lastPoint, nextPoint); + DrawZigZagLine(results, quotes, lastPoint, nextPoint); // draw retrace line (and reset last high/low point) - DrawRetraceLine(results, lastDirection, lastLowPoint, lastHighPoint, nextPoint); + DrawRetraceLine(results, lastDirection, lastLowPoint, + lastHighPoint, nextPoint); } return results; @@ -102,7 +106,7 @@ internal static List CalcZigZag( // internals private static ZigZagPoint EvaluateNextPoint( - List quotesList, + IReadOnlyList quotesList, EndType endType, decimal changeThreshold, ZigZagPoint lastPoint) @@ -138,8 +142,10 @@ private static ZigZagPoint EvaluateNextPoint( } else { - change = (extremePoint.Value == 0) ? null - : (extremePoint.Value - eval.Low) / extremePoint.Value; + change = extremePoint.Value == 0 + ? null + : (extremePoint.Value - eval.Low) + / extremePoint.Value; } } else @@ -152,8 +158,10 @@ private static ZigZagPoint EvaluateNextPoint( } else { - change = (extremePoint.Value == 0) ? null - : (eval.High - extremePoint.Value) / extremePoint.Value; + change = extremePoint.Value == 0 + ? null + : (eval.High - extremePoint.Value) + / extremePoint.Value; } } @@ -175,25 +183,31 @@ private static ZigZagPoint EvaluateNextPoint( return extremePoint; } - private static void DrawZigZagLine(List results, List quotesList, + private static void DrawZigZagLine( + List results, IReadOnlyList quotes, ZigZagPoint lastPoint, ZigZagPoint nextPoint) where TQuote : IQuote { if (nextPoint.Index != lastPoint.Index) { - decimal? increment = (nextPoint.Value - lastPoint.Value) / (nextPoint.Index - lastPoint.Index); + decimal? increment + = (nextPoint.Value - lastPoint.Value) + / (nextPoint.Index - lastPoint.Index); // add new line segment for (int i = lastPoint.Index; i < nextPoint.Index; i++) { - TQuote q = quotesList[i]; + TQuote q = quotes[i]; int index = i + 1; - ZigZagResult result = new(q.Date) { - ZigZag = (lastPoint.Index != 1 || index == nextPoint.Index) ? - lastPoint.Value + (increment * (index - lastPoint.Index)) : null, - PointType = (index == nextPoint.Index) ? nextPoint.PointType : null - }; + ZigZagResult result = new( + Timestamp: q.Timestamp, + ZigZag: lastPoint.Index != 1 || index == nextPoint.Index + ? lastPoint.Value + (increment * (index - lastPoint.Index)) + : null, + PointType: index == nextPoint.Index + ? nextPoint.PointType + : null); results.Add(result); } @@ -214,24 +228,27 @@ private static void DrawRetraceLine( { ZigZagPoint priorPoint = new(); - // handle type and reset last point - if (lastDirection == "L") + switch (lastDirection) { - priorPoint.Index = lastHighPoint.Index; - priorPoint.Value = lastHighPoint.Value; + // handle type and reset last point + case "L": + priorPoint.Index = lastHighPoint.Index; + priorPoint.Value = lastHighPoint.Value; - lastHighPoint.Index = nextPoint.Index; - lastHighPoint.Value = nextPoint.Value; - } + lastHighPoint.Index = nextPoint.Index; + lastHighPoint.Value = nextPoint.Value; + break; - // low line - else if (lastDirection == "H") - { - priorPoint.Index = lastLowPoint.Index; - priorPoint.Value = lastLowPoint.Value; + // low line + case "H": + priorPoint.Index = lastLowPoint.Index; + priorPoint.Value = lastLowPoint.Value; - lastLowPoint.Index = nextPoint.Index; - lastLowPoint.Value = nextPoint.Value; + lastLowPoint.Index = nextPoint.Index; + lastLowPoint.Value = nextPoint.Value; + break; + + default: break; // do nothing } // nothing to draw cases @@ -244,7 +261,9 @@ private static void DrawRetraceLine( } // narrow to period - decimal? increment = (nextPoint.Value - priorPoint.Value) / (nextPoint.Index - priorPoint.Index); + decimal? increment + = (nextPoint.Value - priorPoint.Value) + / (nextPoint.Index - priorPoint.Index); // add new line segment for (int i = priorPoint.Index - 1; i < nextPoint.Index; i++) @@ -252,16 +271,29 @@ private static void DrawRetraceLine( ZigZagResult r = results[i]; int index = i + 1; - // high line - if (lastDirection == "L") + switch (lastDirection) { - r.RetraceHigh = priorPoint.Value + (increment * (index - priorPoint.Index)); - } + // high line + case "L": - // low line - else if (lastDirection == "H") - { - r.RetraceLow = priorPoint.Value + (increment * (index - priorPoint.Index)); + decimal? retraceHigh + = priorPoint.Value + + (increment * (index - priorPoint.Index)); + + results[i] = r with { RetraceHigh = retraceHigh }; + break; + + // low line + case "H": + + decimal? retraceLow + = priorPoint.Value + + (increment * (index - priorPoint.Index)); + + results[i] = r with { RetraceLow = retraceLow }; + break; + + default: break; // do nothing } } } @@ -297,16 +329,4 @@ private static ZigZagEval GetZigZagEval( return eval; } - - // parameter validation - private static void ValidateZigZag( - decimal percentChange) - { - // check parameter arguments - if (percentChange <= 0) - { - throw new ArgumentOutOfRangeException(nameof(percentChange), percentChange, - "Percent change must be greater than 0 for ZIGZAG."); - } - } } diff --git a/src/s-z/ZigZag/ZigZag.Utilities.cs b/src/s-z/ZigZag/ZigZag.Utilities.cs index 6971abed2..b3af14695 100644 --- a/src/s-z/ZigZag/ZigZag.Utilities.cs +++ b/src/s-z/ZigZag/ZigZag.Utilities.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public static partial class Indicator +// ZIG ZAG (UTILITIES) + +public static partial class ZigZag { // CONDENSE (REMOVE null results) - /// - /// - public static IEnumerable Condense( - this IEnumerable results) + /// + public static IReadOnlyList Condense( + this IReadOnlyList results) { List resultsList = results .ToList(); @@ -17,4 +18,16 @@ public static IEnumerable Condense( return resultsList.ToSortedList(); } + + // parameter validation + internal static void Validate( + decimal percentChange) + { + // check parameter arguments + if (percentChange <= 0) + { + throw new ArgumentOutOfRangeException(nameof(percentChange), percentChange, + "Percent change must be greater than 0 for ZIGZAG."); + } + } } diff --git a/tests/application/GlobalUsings.cs b/tests/application/GlobalUsings.cs new file mode 100644 index 000000000..f72ae9eb8 --- /dev/null +++ b/tests/application/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using Skender.Stock.Indicators; +global using Test.Data; diff --git a/tests/application/Program.cs b/tests/application/Program.cs new file mode 100644 index 000000000..620267b46 --- /dev/null +++ b/tests/application/Program.cs @@ -0,0 +1,216 @@ +namespace Test.Application; + +internal class Program +{ + private static void Main(string[] args) + { + if (args.Length != 0) + { + Console.WriteLine(args); + } + + string scenario = "C"; + + switch (scenario) + { + case "A": Do.QuoteHub(); break; + case "B": Do.EmaHub(); break; + case "C": Do.MultipleSubscribers(); break; + } + } +} + +public class Do +{ + private static readonly bool verbose = true; // turn this off when profiling + + private static readonly QuoteHub provider = new(); + + private static readonly IReadOnlyList quotesList = Data.Data.GetDefault(); + + private static readonly int quotesLength = quotesList.Count; + + internal Do() + { + if (!verbose) + { + Prefill(); + } + } + + private static void Prefill() + { + // prefill quotes to provider + for (int i = 0; i < quotesLength; i++) + { + provider.Add(quotesList[i]); + } + } + + internal static void QuoteHub() + { + EmaHub emaHub = provider.ToEma(14); + + if (!verbose) + { + return; + } + + // initialize console display + Console.WriteLine(""" + Date Close price + -------------------- + """); + + // add quotes to provider + for (int i = 0; i < quotesLength; i++) + { + Quote q = quotesList[i]; + provider.Add(q); + + // wait for next quote + Timewarp(); + + // send to console + SendToConsole(q); + } + } + + private static void SendToConsole(Quote q) + { + string m = $"{q.Timestamp:yyyy-MM-dd} ${q.Close:N2}"; + Console.WriteLine(m); + } + + + internal static void EmaHub() + { + EmaHub emaHub = provider.ToEma(14); + + if (!verbose) + { + return; + } + + // initialize console display + Console.WriteLine(""" + Date Close price EMA(14) + ------------------------------ + """); + + // add quotes to provider + for (int i = 0; i < quotesLength; i++) + { + Quote q = quotesList[i]; + provider.Add(q); + + // wait for next quote + Timewarp(); + + // send to console + SendToConsole(q, emaHub); + } + } + + private static void SendToConsole(Quote q, EmaHub emaHub) + where T : IReusable + { + string m = $"{q.Timestamp:yyyy-MM-dd} ${q.Close:N2}"; + + EmaResult e = emaHub.Results[^1]; + + if (e.Ema is not null) + { + m += $"{e.Ema,10:N3}"; + } + else + { + m += $"{"[null]",10}"; + } + + Console.WriteLine(m); + } + + internal static void MultipleSubscribers() + { + SmaHub smaHub = provider.ToSma(3); + EmaHub emaHub = provider.ToEma(5); + EmaHub useChain = provider.ToQuotePart(CandlePart.HL2).ToEma(7); + EmaHub emaChain = provider.ToSma(4).ToEma(4); + + if (!verbose) + { + return; + } + + // initialize console display + Console.WriteLine(""" + Date Close price SMA(3) EMA(5) EMA(7,HL2) SMA/EMA(8) + ------------------------------------------------------------- + """); + + // add quotes to provider + for (int i = 0; i < quotesLength; i++) + { + Quote q = quotesList[i]; + provider.Add(q); + + // wait for next quote + Timewarp(); + + // send to console + SendToConsole(q, smaHub, emaHub, useChain, emaChain); + } + } + + private static void SendToConsole( + Quote q, + SmaHub smaHub, + EmaHub emaHub, + EmaHub useChain, + EmaHub emaChain) + { + string m = $"{q.Timestamp:yyyy-MM-dd} ${q.Close:N2}"; + + SmaResult s = smaHub.Results[^1]; + EmaResult e = emaHub.Results[^1]; + EmaResult u = useChain.Results[^1]; + EmaResult c = emaChain.Results[^1]; + + if (s.Sma is not null) + { + m += $"{s.Sma,8:N1}"; + } + + if (e.Ema is not null) + { + m += $"{e.Ema,8:N1}"; + } + + if (u.Ema is not null) + { + m += $"{u.Ema,12:N1}"; + } + + if (c.Ema is not null) + { + m += $"{c.Ema,12:N1}"; + } + + Console.WriteLine(m); + } + + /// + /// Emulate quote arrival rate. + /// Use '0' to disable. + /// + private static void Timewarp(int quotesPerMinute = 0) + { + if (quotesPerMinute == 0) + { + return; + } + + Thread.Sleep(60000 / quotesPerMinute); + } +} diff --git a/tests/application/README.md b/tests/application/README.md new file mode 100644 index 000000000..9f30b7217 --- /dev/null +++ b/tests/application/README.md @@ -0,0 +1,3 @@ +# Test application + +This is only used for performance profiling and testing. diff --git a/tests/application/Test.Application.csproj b/tests/application/Test.Application.csproj new file mode 100644 index 000000000..a727a0290 --- /dev/null +++ b/tests/application/Test.Application.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + + + + + + + + + Always + + + + diff --git a/tests/application/_testdata/TestData.Getter.cs b/tests/application/_testdata/TestData.Getter.cs new file mode 100644 index 000000000..dcc0a79bc --- /dev/null +++ b/tests/application/_testdata/TestData.Getter.cs @@ -0,0 +1,52 @@ +namespace Test.Data; + +// TEST QUOTE GETTERs + +internal static class Data +{ + // sorted by filename + + // DEFAULT: S&P 500 ~2 years of daily data + internal static IReadOnlyList GetDefault(int days = 502) + => File.ReadAllLines("_testdata/data/default.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderBy(x => x.Timestamp) + .Take(days) + .ToList(); + + // COMPARE DATA ~2 years of TSLA data (matches default time) + internal static IReadOnlyList GetCompare(int days = 502) + => File.ReadAllLines("_testdata/data/compare.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderBy(x => x.Timestamp) + .Take(days) + .ToList(); + + // INTRADAY DATA + internal static IReadOnlyList GetIntraday(int days = 1564) + => File.ReadAllLines("_testdata/data/intraday.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderBy(x => x.Timestamp) + .Take(days) + .ToList(); + + // LONGEST DATA ~62 years of S&P 500 daily data + internal static IReadOnlyList GetLongest() + => File.ReadAllLines("_testdata/data/longest.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderBy(x => x.Timestamp) + .ToList(); + + // LONGISH DATA ~20 years of S&P 500 daily data + internal static IReadOnlyList GetLongish(int days = 5285) + => File.ReadAllLines("_testdata/data/longish.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderBy(x => x.Timestamp) + .Take(days) + .ToList(); +} diff --git a/tests/application/_testdata/TestData.Imports.cs b/tests/application/_testdata/TestData.Imports.cs new file mode 100644 index 000000000..222ccd511 --- /dev/null +++ b/tests/application/_testdata/TestData.Imports.cs @@ -0,0 +1,46 @@ +using System.Globalization; + +namespace Test.Data; + +// TEST DATA IMPORT UTILITIES + +internal static class Imports +{ + private static readonly CultureInfo EnglishCulture = new("en-US", false); + + // importer / parser + internal static Quote QuoteFromCsv(string csvLine) + { + if (string.IsNullOrEmpty(csvLine)) + { + throw new InvalidDataException("CSV line was empty"); + } + + string[] csv = csvLine.Split(','); + + Quote quote = new( + Timestamp: DateTime.TryParse(csv[0], EnglishCulture, out DateTime d) ? d : default, + Open: csv[1].ToDecimalDefault(), + High: csv[2].ToDecimalDefault(), + Low: csv[3].ToDecimalDefault(), + Close: csv[4].ToDecimalDefault(), + Volume: csv[5].ToDecimalDefault() + ); + + return quote; + } + + internal static decimal ToDecimal(this string value) + => decimal.TryParse(value, out decimal d) ? d + : throw new NotFiniteNumberException( + $"Cannot convert `{value}`, it is not a number."); + + internal static decimal ToDecimalDefault(this string value) + => decimal.TryParse(value, out decimal d) ? d : default; + + internal static decimal? ToDecimalNull(this string value) + => decimal.TryParse(value, out decimal d) ? d : null; + + internal static double? ToDoubleNull(this string value) + => double.TryParse(value, out double d) ? d : null; +} diff --git a/tests/indicators/_common/data/compare.csv b/tests/application/_testdata/data/compare.csv similarity index 100% rename from tests/indicators/_common/data/compare.csv rename to tests/application/_testdata/data/compare.csv diff --git a/tests/indicators/_common/data/default.csv b/tests/application/_testdata/data/default.csv similarity index 100% rename from tests/indicators/_common/data/default.csv rename to tests/application/_testdata/data/default.csv diff --git a/tests/indicators/_common/data/intraday.csv b/tests/application/_testdata/data/intraday.csv similarity index 100% rename from tests/indicators/_common/data/intraday.csv rename to tests/application/_testdata/data/intraday.csv diff --git a/tests/indicators/_common/data/longest.csv b/tests/application/_testdata/data/longest.csv similarity index 100% rename from tests/indicators/_common/data/longest.csv rename to tests/application/_testdata/data/longest.csv diff --git a/tests/indicators/_common/data/longish.csv b/tests/application/_testdata/data/longish.csv similarity index 100% rename from tests/indicators/_common/data/longish.csv rename to tests/application/_testdata/data/longish.csv diff --git a/tests/indicators/GlobalSuppressions.cs b/tests/indicators/GlobalSuppressions.cs index b63914909..400f328ca 100644 --- a/tests/indicators/GlobalSuppressions.cs +++ b/tests/indicators/GlobalSuppressions.cs @@ -1,31 +1,6 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage( - "StyleCop.CSharp.NamingRules", - "SA1304:Non-private readonly fields should begin with upper-case letter", - Justification = "Acceptable for test project.")] - -[assembly: SuppressMessage( - "StyleCop.CSharp.NamingRules", - "SA1307:Accessible fields should begin with upper-case letter", - Justification = "Acceptable for test project.")] - -[assembly: SuppressMessage( - "StyleCop.CSharp.NamingRules", - "SA1311:Static readonly fields should begin with upper-case letter", - Justification = "Acceptable for test project.")] - [assembly: SuppressMessage( "Security", "CA5394:Do not use insecure randomness", Justification = "Okay for test rig, non-production code.")] - -[assembly: SuppressMessage( - "StyleCop.CSharp.SpacingRules", - "SA1010:Opening square brackets should be spaced correctly", - Justification = "Invalid for new C# 12 [ collection ] syntax.")] diff --git a/tests/indicators/GlobalUsings.cs b/tests/indicators/GlobalUsings.cs index bafd87a1b..fc2afe48a 100644 --- a/tests/indicators/GlobalUsings.cs +++ b/tests/indicators/GlobalUsings.cs @@ -1,4 +1,4 @@ global using FluentAssertions; global using Microsoft.VisualStudio.TestTools.UnitTesting; global using Skender.Stock.Indicators; -global using Tests.Common; +global using Test.Data; diff --git a/tests/indicators/TestBase.cs b/tests/indicators/TestBase.cs new file mode 100644 index 000000000..5486c5f0e --- /dev/null +++ b/tests/indicators/TestBase.cs @@ -0,0 +1,81 @@ +using System.Globalization; +using System.Runtime.CompilerServices; + +// GLOBALS & INITIALIZATION OF TEST DATA + +[assembly: CLSCompliant(true)] +[assembly: InternalsVisibleTo("Tests.Other")] // these use test data +[assembly: InternalsVisibleTo("Tests.Performance")] +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] + +namespace Test.Data; + +public abstract class TestBase // base for all tests +{ + internal static readonly CultureInfo invariantCulture = CultureInfo.InvariantCulture; + + internal static readonly IReadOnlyList Quotes = Data.GetDefault(); + internal static readonly IReadOnlyList OtherQuotes = Data.GetCompare(); + internal static readonly IReadOnlyList BadQuotes = Data.GetBad(); + internal static readonly IReadOnlyList BigQuotes = Data.GetTooBig(); + internal static readonly IReadOnlyList LongishQuotes = Data.GetLongish(); + internal static readonly IReadOnlyList LongestQuotes = Data.GetLongest(); + internal static readonly IReadOnlyList MismatchQuotes = Data.GetMismatch(); + internal static readonly IReadOnlyList Noquotes = []; + internal static readonly IReadOnlyList Onequote = Data.GetDefault(1); + internal static readonly IReadOnlyList RandomQuotes = Data.GetRandom(1000); + internal static readonly IReadOnlyList ZeroesQuotes = Data.GetZeros(); + + protected static readonly double DoublePrecision = 1E-13; + + protected static readonly DateTime EvalDate + = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", invariantCulture); +} + +/// +/// Base tests that all series indicators should have. +/// +public abstract class StaticSeriesTestBase : TestBase +{ + public abstract void Standard(); + + public abstract void BadData(); + + public abstract void NoQuotes(); +} + +/// +/// Base tests that all static indicators (series) should have. +/// +public abstract class IncrementsTestBase : TestBase +{ + public abstract void FromQuote(); + + public abstract void FromQuoteBatch(); +} + +/// +/// Base tests that all streamed indicators should have. +/// +public abstract class StreamHubTestBase : TestBase // default: quote observer +{ + public abstract void QuoteObserver(); + + public abstract void CustomToString(); +} + +/// +/// Add this to stream chainee indicator tests. +/// +public interface ITestChainObserver +{ + void ChainObserver(); +} + +/// +/// Add this to all stream chainor indicator tests. +/// +public interface ITestChainProvider +{ + void ChainProvider(); +} diff --git a/tests/indicators/Tests.Indicators.csproj b/tests/indicators/Tests.Indicators.csproj index b4d8d0d07..7f6a08c48 100644 --- a/tests/indicators/Tests.Indicators.csproj +++ b/tests/indicators/Tests.Indicators.csproj @@ -18,6 +18,10 @@ + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,7 +33,8 @@ - + + Always diff --git a/tests/indicators/_Initialize.cs b/tests/indicators/_Initialize.cs deleted file mode 100644 index 8fdecf061..000000000 --- a/tests/indicators/_Initialize.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Globalization; -using System.Runtime.CompilerServices; - -// GLOBALS & INITIALIZATION OF TEST DATA - -[assembly: CLSCompliant(true)] -[assembly: InternalsVisibleTo("Tests.Other")] -[assembly: InternalsVisibleTo("Tests.Performance")] -[assembly: InternalsVisibleTo("Observe.Streaming")] -namespace Tests.Common; - -[TestClass] -public abstract class TestBase -{ - internal static readonly CultureInfo EnglishCulture = new("en-US", false); - - internal static readonly IEnumerable quotes = TestData.GetDefault(); - internal static readonly IEnumerable otherQuotes = TestData.GetCompare(); - internal static readonly IEnumerable badQuotes = TestData.GetBad(); - internal static readonly IEnumerable bigQuotes = TestData.GetTooBig(); - internal static readonly IEnumerable maxQuotes = TestData.GetMax(); - internal static readonly IEnumerable longishQuotes = TestData.GetLongish(); - internal static readonly IEnumerable longestQuotes = TestData.GetLongest(); - internal static readonly IEnumerable mismatchQuotes = TestData.GetMismatch(); - internal static readonly IEnumerable noquotes = new List(); - internal static readonly IEnumerable onequote = TestData.GetDefault(1); - internal static readonly IEnumerable randomQuotes = TestData.GetRandom(1000); - internal static readonly IEnumerable zeroesQuotes = TestData.GetZeros(); - internal static readonly IEnumerable<(DateTime, double)> tupleNanny = TestData.GetTupleNaN(); -} diff --git a/tests/indicators/_common/Candles/Candles.Tests.cs b/tests/indicators/_common/Candles/Candles.Tests.cs index 2f5dbf94e..a23c94202 100644 --- a/tests/indicators/_common/Candles/Candles.Tests.cs +++ b/tests/indicators/_common/Candles/Candles.Tests.cs @@ -1,4 +1,4 @@ -namespace Tests.Common; +namespace Utilities; [TestClass] public class Candles : TestBase @@ -6,73 +6,77 @@ public class Candles : TestBase [TestMethod] public void SortCandles() { - IEnumerable quotes = TestData.GetMismatch(); + IReadOnlyList quotes = Data.GetMismatch(); // sort - List candles = quotes.ToCandleResults(); + IReadOnlyList candles = quotes + .ToCandles(); // not sorted // proper quantities Assert.AreEqual(502, candles.Count); // sample values - DateTime firstDate = DateTime.ParseExact("01/18/2016", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(firstDate, candles[0].Date); + DateTime firstDate = DateTime.ParseExact("01/18/2016", "MM/dd/yyyy", invariantCulture); + Assert.AreEqual(firstDate, candles[0].Timestamp); - DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(lastDate, candles.LastOrDefault().Date); + DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", invariantCulture); + Assert.AreEqual(lastDate, candles[^1].Timestamp); - DateTime spotDate = DateTime.ParseExact("03/16/2017", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(spotDate, candles[50].Date); + DateTime spotDate = DateTime.ParseExact("03/16/2017", "MM/dd/yyyy", invariantCulture); + Assert.AreEqual(spotDate, candles[50].Timestamp); } [TestMethod] public void CandleValues() { - List candles = quotes.ToCandleResults(); + IReadOnlyList candles = Quotes + .ToCandles(); // proper quantities Assert.AreEqual(502, candles.Count); // sample values - CandleResult r0 = candles[0]; - Assert.AreEqual(212.8m, r0.Candle.Close); - Assert.AreEqual(1.83m, r0.Candle.Size); - Assert.AreEqual(0.19m, r0.Candle.Body); - Assert.AreEqual(0.55m, r0.Candle.UpperWick); - Assert.AreEqual(1.09m, r0.Candle.LowerWick); - Assert.AreEqual(0.10383, r0.Candle.BodyPct.Round(5)); - Assert.AreEqual(0.30055, r0.Candle.UpperWickPct.Round(5)); - Assert.AreEqual(0.59563, r0.Candle.LowerWickPct.Round(5)); - Assert.IsTrue(r0.Candle.IsBullish); - Assert.IsFalse(r0.Candle.IsBearish); + CandleProperties r0 = candles[0]; + Assert.AreEqual(212.8m, r0.Close); + Assert.AreEqual(1.83m, r0.Size); + Assert.AreEqual(0.19m, r0.Body); + Assert.AreEqual(0.55m, r0.UpperWick); + Assert.AreEqual(1.09m, r0.LowerWick); + Assert.AreEqual(0.10383, r0.BodyPct.Round(5)); + Assert.AreEqual(0.30055, r0.UpperWickPct.Round(5)); + Assert.AreEqual(0.59563, r0.LowerWickPct.Round(5)); + Assert.IsTrue(r0.IsBullish); + Assert.IsFalse(r0.IsBearish); - CandleResult r351 = candles[351]; - Assert.AreEqual(1.24m, r351.Candle.Size); - Assert.AreEqual(0m, r351.Candle.Body); - Assert.AreEqual(0.69m, r351.Candle.UpperWick); - Assert.AreEqual(0.55m, r351.Candle.LowerWick); - Assert.AreEqual(0, r351.Candle.BodyPct.Round(5)); - Assert.AreEqual(0.55645, r351.Candle.UpperWickPct.Round(5)); - Assert.AreEqual(0.44355, r351.Candle.LowerWickPct.Round(5)); - Assert.IsFalse(r351.Candle.IsBullish); - Assert.IsFalse(r351.Candle.IsBearish); + CandleProperties r351 = candles[351]; + Assert.AreEqual(1.24m, r351.Size); + Assert.AreEqual(0m, r351.Body); + Assert.AreEqual(0.69m, r351.UpperWick); + Assert.AreEqual(0.55m, r351.LowerWick); + Assert.AreEqual(0, r351.BodyPct.Round(5)); + Assert.AreEqual(0.55645, r351.UpperWickPct.Round(5)); + Assert.AreEqual(0.44355, r351.LowerWickPct.Round(5)); + Assert.IsFalse(r351.IsBullish); + Assert.IsFalse(r351.IsBearish); - CandleResult r501 = candles[501]; - Assert.AreEqual(2.67m, r501.Candle.Size); - Assert.AreEqual(0.36m, r501.Candle.Body); - Assert.AreEqual(0.26m, r501.Candle.UpperWick); - Assert.AreEqual(2.05m, r501.Candle.LowerWick); - Assert.AreEqual(0.13483, r501.Candle.BodyPct.Round(5)); - Assert.AreEqual(0.09738, r501.Candle.UpperWickPct.Round(5)); - Assert.AreEqual(0.76779, r501.Candle.LowerWickPct.Round(5)); - Assert.IsTrue(r501.Candle.IsBullish); - Assert.IsFalse(r501.Candle.IsBearish); + CandleProperties r501 = candles[501]; + Assert.AreEqual(2.67m, r501.Size); + Assert.AreEqual(0.36m, r501.Body); + Assert.AreEqual(0.26m, r501.UpperWick); + Assert.AreEqual(2.05m, r501.LowerWick); + Assert.AreEqual(0.13483, r501.BodyPct.Round(5)); + Assert.AreEqual(0.09738, r501.UpperWickPct.Round(5)); + Assert.AreEqual(0.76779, r501.LowerWickPct.Round(5)); + Assert.IsTrue(r501.IsBullish); + Assert.IsFalse(r501.IsBearish); } [TestMethod] public void ToCandles() { - IEnumerable candles = quotes.ToCandles(); - Assert.AreEqual(quotes.Count(), candles.Count()); + IReadOnlyList candles + = Quotes.ToCandles(); + + Assert.AreEqual(Quotes.Count, candles.Count); } } diff --git a/tests/indicators/_common/Generics/BinarySettingsTests.cs b/tests/indicators/_common/Generics/BinarySettingsTests.cs new file mode 100644 index 000000000..aa2b01b78 --- /dev/null +++ b/tests/indicators/_common/Generics/BinarySettingsTests.cs @@ -0,0 +1,73 @@ +namespace Utilities; + +[TestClass] +public class BinarySettingsTests : TestBase +{ + // see Renko Hub tests for inheritance + + [TestMethod] + public void InitializationDefault() + { + BinarySettings sut = new(); + sut.Settings.Should().Be(0); + sut.Mask.Should().Be(0b11111111); + } + + [TestMethod] + public void InitializationPartial() + { + BinarySettings sut = new(0); + sut.Settings.Should().Be(0); + sut.Mask.Should().Be(0b11111111); + } + + [TestMethod] + public void InitializationCustom() + { + BinarySettings sut = new(0b10101010, 0b11001100); + sut.Settings.Should().Be(0b10101010); + sut.Mask.Should().Be(0b11001100); + } + + [TestMethod] + public void AccessBit() + { + BinarySettings sut = new(0b00010001); + + // positions: 76543210 + sut[0].Should().BeTrue(); + sut[1].Should().BeFalse(); + sut[2].Should().BeFalse(); + sut[3].Should().BeFalse(); + sut[4].Should().BeTrue(); + sut[5].Should().BeFalse(); + sut[6].Should().BeFalse(); + sut[7].Should().BeFalse(); + } + + [TestMethod] + public void CombineDefaultMask() + { + BinarySettings srcSettings = new(0b01101001); + BinarySettings defSettings = new(0b00000010); + BinarySettings newSettings = defSettings.Combine(srcSettings); + newSettings.Settings.Should().Be(0b01101011); + } + + [TestMethod] + public void CombineCustomMask() + { + BinarySettings srcSettings = new(0b01101001, 0b11111110); + BinarySettings defSettings = new(0b00000010); + BinarySettings newSettings = defSettings.Combine(srcSettings); + newSettings.Settings.Should().Be(0b01101010); + } + + [TestMethod] + public void Equality() + { + BinarySettings sut = new(); + Assert.AreEqual(0b00000000, sut.Settings); + Assert.AreEqual(0b11111111, sut.Mask); + } +} diff --git a/tests/indicators/_common/Generics/Pruning.Tests.cs b/tests/indicators/_common/Generics/Pruning.Tests.cs deleted file mode 100644 index 5edd0265c..000000000 --- a/tests/indicators/_common/Generics/Pruning.Tests.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Tests.Common; - -[TestClass] -public class Pruning : TestBase -{ - [TestMethod] - public void Remove() - { - // specific periods - IEnumerable results = - quotes.GetHeikinAshi() - .RemoveWarmupPeriods(102); - - Assert.AreEqual(400, results.Count()); - - // bad remove period - Assert.ThrowsException(() - => quotes.GetAdx(14).RemoveWarmupPeriods(-1)); - } - - [TestMethod] - public void RemoveTooMany() - { - // more than available - IEnumerable results = - quotes.GetHeikinAshi() - .RemoveWarmupPeriods(600); - - Assert.AreEqual(0, results.Count()); - } -} diff --git a/tests/indicators/_common/Generics/RemoveWarmup.Tests.cs b/tests/indicators/_common/Generics/RemoveWarmup.Tests.cs new file mode 100644 index 000000000..99abb398b --- /dev/null +++ b/tests/indicators/_common/Generics/RemoveWarmup.Tests.cs @@ -0,0 +1,31 @@ +namespace Utilities; + +[TestClass] +public class RemoveWarmup : TestBase +{ + [TestMethod] + public void Standard() + { + // specific periods + IReadOnlyList results = Quotes + .ToHeikinAshi() + .RemoveWarmupPeriods(102); + + Assert.AreEqual(400, results.Count); + + // bad remove period + Assert.ThrowsException(() + => Quotes.ToAdx().RemoveWarmupPeriods(-1)); + } + + [TestMethod] + public void TooMany() + { + // more than available + IReadOnlyList results = Quotes + .ToHeikinAshi() + .RemoveWarmupPeriods(600); + + Assert.AreEqual(0, results.Count); + } +} diff --git a/tests/indicators/_common/Generics/Seek.Tests.cs b/tests/indicators/_common/Generics/Seek.Tests.cs index cb312d597..bcd1a673e 100644 --- a/tests/indicators/_common/Generics/Seek.Tests.cs +++ b/tests/indicators/_common/Generics/Seek.Tests.cs @@ -4,42 +4,14 @@ namespace Tests.Common; public class Seeking : TestBase { [TestMethod] - public void FindSeries() + public void Find() { - IEnumerable quotes = TestData.GetDefault(); - IEnumerable emaResults = quotes.GetEma(20); + var emaResults = Quotes.ToEma(20); // find specific date - DateTime findDate = DateTime.ParseExact("2018-12-31", "yyyy-MM-dd", EnglishCulture); + DateTime findDate = DateTime.ParseExact("2018-12-31", "yyyy-MM-dd", invariantCulture); EmaResult r = emaResults.Find(findDate); Assert.AreEqual(249.3519, r.Ema.Round(4)); } - - [TestMethod] - public void FindSeriesNone() - { - IEnumerable quotes = TestData.GetDefault(); - IEnumerable emaResults = quotes.GetEma(20); - - // find specific date - DateTime findDate = DateTime.ParseExact("1928-10-29", "yyyy-MM-dd", EnglishCulture); - - EmaResult r = emaResults.Find(findDate); - Assert.IsNull(r); - } - - [TestMethod] - public void FindSeriesIndex() - { - List quotes = TestData - .GetDefault() - .ToSortedList(); - - // find specific date - DateTime findDate = DateTime.ParseExact("2018-12-31", "yyyy-MM-dd", EnglishCulture); - - int i = quotes.FindIndex(findDate); - Assert.AreEqual(501, i); - } } diff --git a/tests/indicators/_common/Generics/Sort.Tests.cs b/tests/indicators/_common/Generics/Sort.Tests.cs deleted file mode 100644 index 9445c0426..000000000 --- a/tests/indicators/_common/Generics/Sort.Tests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Tests.Common; - -[TestClass] -public class Sorting : TestBase -{ - [TestMethod] - public void ToSortedCollection() - { - // baseline for comparison - List baseline = - [ - new SmaResult(DateTime.Parse("1/1/2000", EnglishCulture)) { Sma = null }, - new SmaResult(DateTime.Parse("1/2/2000", EnglishCulture)) { Sma = null }, - new SmaResult(DateTime.Parse("1/9/2000", EnglishCulture)) { Sma = null }, - new SmaResult(DateTime.Parse("1/3/2000", EnglishCulture)) { Sma = 3 }, - new SmaResult(DateTime.Parse("1/4/2000", EnglishCulture)) { Sma = 4 }, - new SmaResult(DateTime.Parse("1/5/2000", EnglishCulture)) { Sma = 5 }, - new SmaResult(DateTime.Parse("1/6/2000", EnglishCulture)) { Sma = 6 }, - new SmaResult(DateTime.Parse("1/7/2000", EnglishCulture)) { Sma = 7 }, - new SmaResult(DateTime.Parse("1/8/2000", EnglishCulture)) { Sma = double.NaN }, - ]; - - // PUBLIC VARIANT, generic sorted Collection - Collection sortResults = baseline - .ToSortedCollection(); - - Assert.AreEqual(5, sortResults[4].Sma); - Assert.AreEqual(DateTime.Parse("1/9/2000", EnglishCulture), sortResults.LastOrDefault().Date); - } -} diff --git a/tests/indicators/_common/Generics/Sorting.Tests.cs b/tests/indicators/_common/Generics/Sorting.Tests.cs new file mode 100644 index 000000000..4bb16536b --- /dev/null +++ b/tests/indicators/_common/Generics/Sorting.Tests.cs @@ -0,0 +1,32 @@ +using System.Collections.ObjectModel; + +namespace Utilities; + +[TestClass] +public class Sorting : TestBase +{ + [TestMethod] + public void ToSortedList() + { + // baseline for comparison + IReadOnlyList baseline = + [ + new(Timestamp: DateTime.Parse("1/1/2000", invariantCulture), Sma: null), + new(Timestamp: DateTime.Parse("1/2/2000", invariantCulture), Sma: null), + new(Timestamp: DateTime.Parse("1/9/2000", invariantCulture), Sma: null), + new(Timestamp: DateTime.Parse("1/3/2000", invariantCulture), Sma: 3), + new(Timestamp: DateTime.Parse("1/4/2000", invariantCulture), Sma: 4), + new(Timestamp: DateTime.Parse("1/5/2000", invariantCulture), Sma: 5), + new(Timestamp: DateTime.Parse("1/6/2000", invariantCulture), Sma: 6), + new(Timestamp: DateTime.Parse("1/7/2000", invariantCulture), Sma: 7), + new(Timestamp: DateTime.Parse("1/8/2000", invariantCulture), Sma: double.NaN) + ]; + + // PUBLIC VARIANT, generic sorted list + IReadOnlyList sortResults = baseline + .ToSortedList(); + + Assert.AreEqual(5, sortResults[4].Sma); + Assert.AreEqual(DateTime.Parse("1/9/2000", invariantCulture), sortResults[^1].Timestamp); + } +} diff --git a/tests/indicators/_common/Generics/Transforms.Tests.cs b/tests/indicators/_common/Generics/Transforms.Tests.cs index 8c398518f..60a8da66b 100644 --- a/tests/indicators/_common/Generics/Transforms.Tests.cs +++ b/tests/indicators/_common/Generics/Transforms.Tests.cs @@ -1,27 +1,27 @@ using System.Collections.ObjectModel; -namespace Tests.Common; +namespace Utilities; [TestClass] -public class TransformTests : TestBase +public class Transforms : TestBase { [TestMethod] public void ToCollection() { - Collection collection = quotes + Collection collection = Quotes .ToSortedList() .ToCollection(); Assert.IsNotNull(collection); Assert.AreEqual(502, collection.Count); - Assert.AreEqual(collection.LastOrDefault().Close, 245.28m); + Assert.AreEqual(245.28m, collection.LastOrDefault().Close); } // null ToCollection [TestMethod] - public void Exceptions() + public void ToCollectionNullExceptions() { - List nullQuotes = null; + IReadOnlyList nullQuotes = null; Assert.ThrowsException(() => nullQuotes.ToCollection()); diff --git a/tests/indicators/_common/Helper.Getter.cs b/tests/indicators/_common/Helper.Getter.cs deleted file mode 100644 index 7a2daa11a..000000000 --- a/tests/indicators/_common/Helper.Getter.cs +++ /dev/null @@ -1,212 +0,0 @@ -namespace Tests.Common; - -// IMPORT TEST DATA -internal static class TestData -{ - // DEFAULT: S&P 500 ~2 years of daily data - internal static IEnumerable GetDefault(int days = 502) - => File.ReadAllLines("_common/data/default.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // RANDOM: gaussian brownaian motion - internal static IEnumerable GetRandom(int days = 502) - => new RandomGbm(bars: days); - - // ZEROS (200) - internal static IEnumerable GetZeros(int days = 200) - => File.ReadAllLines("_common/data/zeros.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // BAD DATA - internal static IEnumerable GetBad(int days = 502) - => File.ReadAllLines("_common/data/bad.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // TOO BIG DATA - internal static IEnumerable GetTooBig(int days = 1246) - => File.ReadAllLines("_common/data/toobig.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // MAX SIZE DATA - internal static IEnumerable GetMax(int days = 502) - => File.ReadAllLines("_common/data/toobig.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // BITCOIN DATA - internal static IEnumerable GetBitcoin(int days = 1246) - => File.ReadAllLines("_common/data/bitcoin.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // COMPARE DATA ~2 years of TSLA data (matches default time) - internal static IEnumerable GetCompare(int days = 502) - => File.ReadAllLines("_common/data/compare.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // INTRADAY DATA - internal static IEnumerable GetIntraday(int days = 1564) - => File.ReadAllLines("_common/data/intraday.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // LONGISH DATA ~20 years of S&P 500 daily data - internal static IEnumerable GetLongish(int days = 5285) - => File.ReadAllLines("_common/data/longish.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // LONGEST DATA ~62 years of S&P 500 daily data - internal static IEnumerable GetLongest() - => File.ReadAllLines("_common/data/longest.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .ToList(); - - // PENNY DATA - internal static IEnumerable GetPenny() - => File.ReadAllLines("_common/data/penny.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .ToList(); - - // MISMATCH DATA is in incorrect sequence - internal static IEnumerable GetMismatch() - => File.ReadAllLines("_common/data/mismatch.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .ToList(); - - // SPX, 30 years, daily - internal static IEnumerable GetSpx(int days = 8111) - => File.ReadAllLines("_common/data/spx.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // MSFT, 30 years, daily - internal static IEnumerable GetMsft(int days = 8111) - => File.ReadAllLines("_common/data/msft.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // BTCUSD, 69288 records, 15-minute bars - internal static IEnumerable GetBtcUsdNan(int bars = 69288) - => File.ReadAllLines("_common/data/btcusd15x69k.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(bars) - .ToList(); - - // TUPLE with NaNs - internal static IEnumerable<(DateTime, double)> GetTupleNaN() - { - List<(DateTime, double)> tpList = new(200); - double timeFactor = 10000000d; - - DateTime date = DateTime.UtcNow; - - // sequential - for (int i = 0; i < 25; i++) - { - date = date.AddDays(1); - double value = date.ToFileTime() / timeFactor; - - tpList.Add(new(date, value)); - } - - // sequential negative - for (int i = 0; i < 25; i++) - { - date = date.AddDays(1); - double value = -date.ToFileTime() / timeFactor; - - tpList.Add(new(date, value)); - } - - // sequential 0 - for (int i = 0; i < 25; i++) - { - date = date.AddDays(1); - double value = 0; - - tpList.Add(new(date, value)); - } - - // sequential -10 - for (int i = 0; i < 25; i++) - { - date = date.AddDays(1); - double value = -10; - - tpList.Add(new(date, value)); - } - - // sequential 10 - for (int i = 0; i < 25; i++) - { - date = date.AddDays(1); - double value = 10; - - tpList.Add(new(date, value)); - } - - // some NaNs - for (int i = 0; i < 25; i++) - { - date = date.AddDays(1); - double value = double.NaN; - - tpList.Add(new(date, value)); - } - - // more sequential - for (int i = 0; i < 50; i++) - { - date = date.AddDays(1); - double value = date.ToFileTime() / timeFactor; - - tpList.Add(new(date, value)); - } - - return tpList; - } -} diff --git a/tests/indicators/_common/Helper.Importer.cs b/tests/indicators/_common/Helper.Importer.cs deleted file mode 100644 index b88006ca4..000000000 --- a/tests/indicators/_common/Helper.Importer.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Globalization; - -namespace Tests.Common; - -// TEST QUOTE IMPORTER -internal static class Importer -{ - private static readonly CultureInfo EnglishCulture = new("en-US", false); - - // importer / parser - internal static Quote QuoteFromCsv(string csvLine) - { - if (string.IsNullOrEmpty(csvLine)) - { - return new Quote(); - } - - string[] values = csvLine.Split(','); - Quote quote = new(); - - HandleOHLCV(quote, "D", values[0]); - HandleOHLCV(quote, "O", values[1]); - HandleOHLCV(quote, "H", values[2]); - HandleOHLCV(quote, "L", values[3]); - HandleOHLCV(quote, "C", values[4]); - HandleOHLCV(quote, "V", values[5]); - - return quote; - } - - internal static decimal ToDecimal(this string value) - => decimal.TryParse(value, out decimal d) ? d - : throw new NotFiniteNumberException( - $"Cannot convert `{value}`, it is not a number."); - - internal static decimal? ToDecimalNull(this string value) - => decimal.TryParse(value, out decimal d) ? d : null; - - internal static double? ToDoubleNull(this string value) - => double.TryParse(value, out double d) ? d : null; - - private static void HandleOHLCV(Quote quote, string position, string value) - { - if (string.IsNullOrEmpty(value)) - { - return; - } - - switch (position) - { - case "D": - quote.Date = Convert.ToDateTime(value, EnglishCulture); - break; - case "O": - quote.Open = Convert.ToDecimal(value, EnglishCulture); - break; - case "H": - quote.High = Convert.ToDecimal(value, EnglishCulture); - break; - case "L": - quote.Low = Convert.ToDecimal(value, EnglishCulture); - break; - case "C": - quote.Close = Convert.ToDecimal(value, EnglishCulture); - break; - case "V": - quote.Volume = Convert.ToDecimal(value, EnglishCulture); - break; - default: - throw new ArgumentOutOfRangeException(nameof(position)); - } - } -} diff --git a/tests/indicators/_common/Helper.Random.cs b/tests/indicators/_common/Helper.Random.cs deleted file mode 100644 index cc11b90b8..000000000 --- a/tests/indicators/_common/Helper.Random.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace Tests.Common; -/** - -Geometric Brownian Motion (GMB) is a random simulator of market movement. -GBM can be used for testing indicators, validation and Monte Carlo simulations of strategies. - -Sample usage: -RandomGbm data = new(); // generates 1 year (252) list of bars -RandomGbm data = new(Bars: 1000); // generates 1,000 bars -RandomGbm data = new(Bars: 252, Volatility: 0.05, Drift: 0.0005, Seed: 100.0) - -Parameters -Bars: number of bars (quotes) requested -Volatility: how dymamic/volatile the series should be; default is 1 -Drift: incremental drift due to annual interest rate; default is 5% -Seed: starting value of the random series; should not be 0. - -**/ -internal class RandomGbm : List -{ - private readonly double volatility; - private readonly double drift; - private double seed; - - public RandomGbm( - int bars = 250, - double volatility = 1.0, - double drift = 0.01, - double seed = 1000.0) - { - this.seed = seed; - this.volatility = volatility * 0.01; - this.drift = drift * 0.001; - for (int i = 0; i < bars; i++) - { - DateTime date = DateTime.Today.AddMinutes(i - bars); - Add(date); - } - } - - public void Add(DateTime timestamp) - { - double open = Price(seed, volatility * volatility, drift); - double close = Price(open, volatility, drift); - - double ocMax = Math.Max(open, close); - double high = Price(seed, volatility * 0.5, 0); - high = (high < ocMax) ? (2 * ocMax) - high : high; - - double ocMin = Math.Min(open, close); - double low = Price(seed, volatility * 0.5, 0); - low = (low > ocMin) ? (2 * ocMin) - low : low; - - double volume = Price(seed * 10, volatility * 2, drift: 0); - - Quote quote = new() { - Date = timestamp, - Open = (decimal)open, - High = (decimal)high, - Low = (decimal)low, - Close = (decimal)close, - Volume = (decimal)volume - }; - - Add(quote); - seed = close; - } - - private static double Price(double seed, double volatility, double drift) - { - Random rnd = new((int)DateTime.UtcNow.Ticks); - double u1 = 1.0 - rnd.NextDouble(); - double u2 = 1.0 - rnd.NextDouble(); - double z = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2); - return seed * Math.Exp(drift - (volatility * volatility * 0.5) + (volatility * z)); - } -} diff --git a/tests/indicators/_common/Math/Numerix.Tests.cs b/tests/indicators/_common/Math/Numerical.Tests.cs similarity index 67% rename from tests/indicators/_common/Math/Numerix.Tests.cs rename to tests/indicators/_common/Math/Numerical.Tests.cs index a8cc81498..e116fe11f 100644 --- a/tests/indicators/_common/Math/Numerix.Tests.cs +++ b/tests/indicators/_common/Math/Numerical.Tests.cs @@ -1,55 +1,63 @@ -namespace Tests.Common; +namespace Utilities; [TestClass] -public class NumerixTests : TestBase +public class Numericals : TestBase { - private readonly double[] closePrice = longishQuotes + private readonly double[] _closePrice = LongishQuotes .Select(x => (double)x.Close) .ToArray(); - private readonly double[] x = [1, 2, 3, 4, 5]; - private readonly double[] y = [0, 0, 0, 0]; + private readonly double[] _x = { 1, 2, 3, 4, 5 }; + private readonly double[] _y = { 0, 0, 0, 0 }; [TestMethod] public void StdDev() { - double sd = closePrice.StdDev(); + double sd = _closePrice.StdDev(); Assert.AreEqual(633.932098287, Math.Round(sd, 9)); } [TestMethod] - [ExpectedException(typeof(ArgumentNullException), "Null parameter.")] - public void StdDevNull() => Numerix.StdDev(null); + public void StdDevNull() + { + Assert.ThrowsException(() => Numerical.StdDev(null)); + } [TestMethod] public void Slope() { - double s = Numerix.Slope(x, x); + double s = Numerical.Slope(_x, _x); Assert.AreEqual(1d, s); } [TestMethod] - [ExpectedException(typeof(ArgumentNullException), "Null X parameter.")] - public void SlopeXnull() => Numerix.Slope(null, x); + public void SlopeXnull() + { + Assert.ThrowsException(() => Numerical.Slope(null, _x)); + } [TestMethod] - [ExpectedException(typeof(ArgumentNullException), "Null Y parameter.")] - public void SlopeYnull() => Numerix.Slope(x, null); + public void SlopeYnull() + { + Assert.ThrowsException(() => Numerical.Slope(_x, null)); + } [TestMethod] - [ExpectedException(typeof(ArgumentException), "X and Y different lengths.")] - public void SlopeMismatch() => Numerix.Slope(x, y); + public void SlopeMismatch() + { + Assert.ThrowsException(() => Numerical.Slope(_x, _y)); + } [TestMethod] public void RoundDownDate() { TimeSpan interval = PeriodSize.OneHour.ToTimeSpan(); - DateTime evDate = DateTime.Parse("2020-12-15 09:35:45", EnglishCulture); + DateTime evDate = DateTime.Parse("2020-12-15 09:35:45", invariantCulture); DateTime rnDate = evDate.RoundDown(interval); - DateTime exDate = DateTime.Parse("2020-12-15 09:00:00", EnglishCulture); + DateTime exDate = DateTime.Parse("2020-12-15 09:00:00", invariantCulture); Assert.AreEqual(exDate, rnDate); } diff --git a/tests/indicators/_common/Observables/StreamHub.CacheMgmt.Tests.cs b/tests/indicators/_common/Observables/StreamHub.CacheMgmt.Tests.cs new file mode 100644 index 000000000..2ebd45acf --- /dev/null +++ b/tests/indicators/_common/Observables/StreamHub.CacheMgmt.Tests.cs @@ -0,0 +1,159 @@ +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() + { + QuoteHub provider = new(); + SmaHub observer = provider.ToSma(20); + provider.Add(Quotes.Take(21)); + + observer.Results[19].Sma.Should().BeApproximately(214.5250, precision: DoublePrecision); + + provider.Remove(Quotes[14]); + provider.EndTransmission(); + + 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 + { + int length = Quotes.Count; + + // add base quotes + QuoteHub provider = new(); + + QuotePartHub observer = provider + .ToQuotePart(CandlePart.Close); + + // emulate incremental quotes + for (int i = 0; i < length; i++) + { + // skip one + if (i == 100) + { + continue; + } + + Quote q = Quotes[i]; + provider.Add(q); + } + + // add late + provider.Insert(Quotes[100]); + + // assert same as original + for (int i = 0; i < length; i++) + { + Quote q = Quotes[i]; + QuotePart r = observer.Cache[i]; + + // compare quote to result cache + r.Timestamp.Should().Be(q.Timestamp); + r.Value.Should().Be(q.Value); + } + + // close observations + provider.EndTransmission(); + } + + [TestMethod] + public void Overflowing() + { + // initialize + QuoteHub provider = new(); + + Quote dup = new( + Timestamp: DateTime.Now, + Open: 1.00m, + High: 2.00m, + Low: 0.50m, + Close: 1.75m, + Volume: 1000); + + QuotePartHub observer = provider + .ToQuotePart(CandlePart.Close); + + // overflowing, under threshold + for (int i = 0; i <= 100; i++) + { + provider.Add(dup); + } + + // assert: no fault, no overflow (yet) + + provider.Quotes.Should().HaveCount(1); + observer.Results.Should().HaveCount(1); + provider.IsFaulted.Should().BeFalse(); + provider.OverflowCount.Should().Be(100); + provider.HasObservers.Should().BeTrue(); + + provider.EndTransmission(); + } + + [TestMethod] + public void OverflowedAndReset() + { + // initialize + QuoteHub provider = new(); + + Quote dup = new( + Timestamp: DateTime.Now, + Open: 1.00m, + High: 2.00m, + Low: 0.50m, + Close: 1.75m, + Volume: 1000); + + QuotePartHub observer = provider + .ToQuotePart(CandlePart.Close); + + // overflowed, over threshold + Assert.ThrowsException(() => { + + for (int i = 0; i <= 101; i++) + { + provider.Add(dup); + } + }); + + // assert: faulted + + provider.Quotes.Should().HaveCount(1); + observer.Results.Should().HaveCount(1); + provider.IsFaulted.Should().BeTrue(); + provider.OverflowCount.Should().Be(101); + provider.HasObservers.Should().BeTrue(); + + // act: reset + + provider.ResetFault(); + + for (int i = 0; i < 100; i++) + { + provider.Add(dup); + } + + // assert: no fault, no overflow (yet) + + provider.Quotes.Should().HaveCount(1); + observer.Results.Should().HaveCount(1); + provider.IsFaulted.Should().BeFalse(); + provider.OverflowCount.Should().Be(100); + provider.HasObservers.Should().BeTrue(); // not lost + + provider.EndTransmission(); + } +} diff --git a/tests/indicators/_common/Observables/StreamHub.Observable.Tests.cs b/tests/indicators/_common/Observables/StreamHub.Observable.Tests.cs new file mode 100644 index 000000000..8dcb6f73c --- /dev/null +++ b/tests/indicators/_common/Observables/StreamHub.Observable.Tests.cs @@ -0,0 +1,112 @@ +namespace Observables; + +[TestClass] +public class StreamObservables : TestBase, ITestChainProvider +{ + [TestMethod] + public void Prefill() + { + IReadOnlyList quotesList = Quotes + .Take(50) + .ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider + provider.Add(quotesList); + + // initialize observer + QuotePartHub observer = provider + .ToQuotePart(CandlePart.Close); + + // assert: prefilled + provider.Cache.Should().HaveCount(50); + observer.Cache.Should().HaveCount(50); + + // assert: same dates + for (int i = 0; i < 50; i++) + { + IReusable r = observer.Cache[i]; + IReusable q = provider.Cache[i]; + + r.Timestamp.Should().Be(q.Timestamp); + } + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void Subscription() + { + // setup quote provider, observer + QuoteHub provider = new(); + + QuotePartHub observer + = provider.ToQuotePart(CandlePart.OHLC4); + + // assert: subscribed + provider.ObserverCount.Should().Be(1); + provider.HasObservers.Should().BeTrue(); + observer.IsSubscribed.Should().BeTrue(); + + // act: unsubscribe + observer.Unsubscribe(); + + // assert: not subscribed + provider.ObserverCount.Should().Be(0); + provider.HasObservers.Should().BeFalse(); + observer.IsSubscribed.Should().BeFalse(); + + // act: resubscribe + provider.Subscribe(observer); + + // assert: subscribed + provider.ObserverCount.Should().Be(1); + provider.HasObservers.Should().BeTrue(); + observer.IsSubscribed.Should().BeTrue(); + + // act: end all subscriptions + provider.EndTransmission(); + + // assert: not subscribed + provider.ObserverCount.Should().Be(0); + provider.HasObservers.Should().BeFalse(); + observer.IsSubscribed.Should().BeFalse(); + } + + [TestMethod] + public void ChainProvider() + { + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + EmaHub observer = provider + .ToQuotePart(CandlePart.HL2) + .ToEma(11); + + // emulate adding quotes to provider + provider.Add(Quotes); + provider.EndTransmission(); + + // stream results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList = Quotes + .Use(CandlePart.HL2) + .ToEma(11); + + // assert, should equal series + streamList.Should().HaveCount(Quotes.Count); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } +} diff --git a/tests/indicators/_common/Observables/StreamHub.Observer.Tests.cs b/tests/indicators/_common/Observables/StreamHub.Observer.Tests.cs new file mode 100644 index 000000000..794749b3b --- /dev/null +++ b/tests/indicators/_common/Observables/StreamHub.Observer.Tests.cs @@ -0,0 +1,62 @@ +namespace Observables; + +[TestClass] +public class StreamObservers : TestBase +{ + [TestMethod] + public void RebuildCache() + { + int qtyQuotes = 5000; + + // setup: many random quotes (massive) + IReadOnlyList quotesList + = Data.GetRandom(qtyQuotes).ToList(); + + int length = quotesList.Count; + + length.Should().Be(qtyQuotes); // check rando + + QuoteHub provider = new(); + + QuotePartHub observer = provider + .ToQuotePart(CandlePart.Close); + + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + // original results + IReadOnlyList original = observer.Results.ToList(); + + // quotes to replace + Quote q1000original = quotesList[1000] with { /* copy */ }; + QuotePart r1000original = observer.Cache[1000] with { /* copy */ }; + + // modify results (keeping provider intact) + Quote q1000modified = quotesList[1000] with { Close = 12345m }; + QuotePart r1000modified = q1000modified.ToQuotePart(CandlePart.Close); + + observer.Cache.Insert(1000, r1000modified); // add directly to cache + + IReadOnlyList modified = observer.Results.ToList(); + + // precondition: prefilled, modified + provider.Cache.Should().HaveCount(length); + observer.Cache.Should().HaveCount(length + 1); + + observer.Cache[1000].Value.Should().Be(12345); + observer.Cache.Should().NotBeEquivalentTo(original); + observer.Cache.Should().BeEquivalentTo(modified); + + // act: Rebuild() + observer.Rebuild(); + + // assert: restored to original + observer.Results.Should().HaveCount(length); + observer.Results.Should().BeEquivalentTo(original); + + observer.Cache[1000].Value.Should().NotBe(12345); + observer.Cache[1000].Value.Should().Be((double)quotesList[1000].Close); + } +} diff --git a/tests/indicators/_common/Observables/StreamHub.Stackoverflow.Tests.cs b/tests/indicators/_common/Observables/StreamHub.Stackoverflow.Tests.cs new file mode 100644 index 000000000..0e61b8fcc --- /dev/null +++ b/tests/indicators/_common/Observables/StreamHub.Stackoverflow.Tests.cs @@ -0,0 +1,260 @@ +namespace Observables; + +[TestClass] +public class Stackoverflow : TestBase +{ + [TestMethod] + public void FatLongStack() + { + // goal: about ~10 subscribers, with really long + // quote history, checking for stack overflow + + int qtyQuotes = 20000; + + // setup: many random quotes (massive) + IReadOnlyList quotesList = Data.GetRandom(qtyQuotes); + + QuoteHub provider = new(); + + // setup: define ~10 subscribers (flat) + List<(string label, IReadOnlyList results, bool irregular)> subscribers = new() + { + HubRef(provider.ToAdl()), + HubRef(provider.ToEma(14)) + }; + + // all USEs + foreach (CandlePart candlePart in Enum.GetValues()) + { + subscribers.Add(HubRef(provider.ToQuotePart(candlePart))); + } + + // act: add quotes + for (int i = 0; i < qtyQuotes; i++) + { + provider.Add(quotesList[i]); + } + + subscribers.Insert(0, new(provider.ToString(), provider.Quotes, false)); + + // assert: this just has to not fail, really + + Console.WriteLine($"Subscribers: {subscribers.Count}"); + Console.WriteLine("--------------------"); + + // assert: all non-irregular subscribers have the same count + foreach ((string label, IReadOnlyList results, bool irregular) in subscribers) + { + int resultQty = results.Count; + Console.WriteLine($"Hub: {resultQty} - {label}"); + if (irregular) { continue; } + resultQty.Should().Be(qtyQuotes); + } + + // assert: [last subscriber] has the same dates + IReadOnlyList lastSubscriber = subscribers[^1].results.ToList(); + for (int i = 0; i < qtyQuotes; i++) + { + Quote q = quotesList[i]; + ISeries r = lastSubscriber[i]; + r.Timestamp.Should().Be(q.Timestamp); + } + + // act: clear provider cache (cascades to subscribers) + int cutoff = qtyQuotes / 2; + provider.RemoveRange(cutoff, notify: true); + + provider.Quotes.Count.Should().Be(cutoff); + + Console.WriteLine("--------------------"); + + // assert: all have same count + foreach ((string label, IReadOnlyList results, bool irregular) in subscribers) + { + int resultQty = results.Count; + Console.WriteLine($"Cut: {resultQty} - {label}"); + if (irregular) { continue; } + resultQty.Should().Be(cutoff); + } + } + + [TestMethod] + public void ManyChainDepths() + { + // goal: test that a massive chain where each new subscriber + // subscribes to the next creating a really long chain + // of observers, without stack overflow + + int qtyQuotes = 10000; + int chainDepth = 500; + + // setup: many random quotes (massive) + IReadOnlyList quotesList = Data.GetRandom(qtyQuotes); + + QuoteHub provider = new(); + + // setup: subscribe a large chain depth + List<(string label, IReadOnlyList results, bool irregular)> subscribers = new(chainDepth + 2); + + SmaHub init = provider.ToSma(1); + SmaHub sma = init.ToSma(2); + + subscribers.Add(HubRef(init)); + subscribers.Add(HubRef(sma)); + + int lookbackPeriods = 1; + + // recursive providers + for (int i = 1; i <= chainDepth; i++) + { + sma = sma.ToSma(lookbackPeriods); + subscribers.Add(HubRef(sma)); + + lookbackPeriods = lookbackPeriods is 2 ? 1 : 2; + } + + // act: add quotes + for (int i = 0; i < qtyQuotes; i++) + { + provider.Add(quotesList[i]); + } + + subscribers.Insert(0, new(provider.ToString(), provider.Quotes, false)); + + Console.WriteLine($"Subscribers: {subscribers.Count}"); + Console.WriteLine("--------------------"); + + // assert: this just has to not fail, really + + // assert: all non-irregular subscribers have the same count + foreach ((string label, IReadOnlyList results, bool irregular) in subscribers) + { + int resultQty = results.Count; + Console.WriteLine($"Hub: {resultQty} - {label}"); + if (irregular) { continue; } + resultQty.Should().Be(qtyQuotes); + } + + // assert: [last subscriber] has the same dates + IReadOnlyList lastSubscriber = subscribers[^1].results.ToList(); + for (int i = 0; i < qtyQuotes; i++) + { + Quote q = quotesList[i]; + ISeries r = lastSubscriber[i]; + r.Timestamp.Should().Be(q.Timestamp); + } + + // act: clear provider cache (cascades to subscribers) + int cutoff = qtyQuotes / 2; + provider.RemoveRange(cutoff, notify: true); + + provider.Quotes.Count.Should().Be(cutoff); + + // assert: all have same count + foreach ((string label, IReadOnlyList results, bool irregular) in subscribers) + { + int resultQty = results.Count; + Console.WriteLine($"Cut: {resultQty} - {label}"); + if (irregular) { continue; } + resultQty.Should().Be(cutoff); + } + } + + [TestMethod] + public void ManySubscribers() + { + // goal: test that many indictors (all at once) + // can subscribe to the same quote provider + // without stack overflow; ~350 subscribers + + int qtyQuotes = 5000; + + // setup: many random quotes + IReadOnlyList quotesList = Data.GetRandom(qtyQuotes); + + QuoteHub provider = new(); + + // setup: define all possible subscribers + // TODO: add to this as more Hubs come online + List<(string label, IReadOnlyList results, bool irregular)> subscribers = new() + { + HubRef(provider.ToAdl()), + HubRef(provider.ToAlligator()), + HubRef(provider.ToEma(14)), + //HubRef(provider.ToRenko(2.1m), irregular: true), + HubRef(provider.ToQuote()) + }; + + // all QuoteParts + foreach (CandlePart candlePart in Enum.GetValues()) + { + subscribers.Add(HubRef(provider.ToQuotePart(candlePart))); + } + + // many SMAs + for (int i = 1; i <= 300; i++) + { + subscribers.Add(HubRef(provider.ToSma(i))); + } + + // act: add quotes + for (int i = 0; i < qtyQuotes; i++) + { + provider.Add(quotesList[i]); + } + + subscribers.Insert(0, new(provider.ToString(), provider.Quotes, false)); + + // assert: this just has to not fail, really + + Console.WriteLine($"Subscribers: {subscribers.Count}"); + Console.WriteLine("--------------------"); + + // assert: all non-irregular subscribers have the same count + foreach ((string label, IReadOnlyList results, bool irregular) in subscribers) + { + int resultQty = results.Count; + Console.WriteLine($"Hub: {resultQty} - {label}"); + if (irregular) { continue; } + resultQty.Should().Be(qtyQuotes); + } + + // assert: [last subscriber] has the same dates + IReadOnlyList lastSubscriber = subscribers[^1].results.ToList(); + for (int i = 0; i < qtyQuotes; i++) + { + Quote q = quotesList[i]; + ISeries r = lastSubscriber[i]; + r.Timestamp.Should().Be(q.Timestamp); + } + + // act: clear provider cache (cascades to subscribers) + int cutoff = qtyQuotes / 2; + provider.RemoveRange(cutoff, notify: true); + + provider.Quotes.Count.Should().Be(cutoff); + + Console.WriteLine("--------------------"); + + // assert: all have same count + foreach ((string label, IReadOnlyList results, bool irregular) in subscribers) + { + int resultQty = results.Count; + Console.WriteLine($"Cut: {resultQty} - {label}"); + if (irregular) { continue; } + resultQty.Should().Be(cutoff); + } + } + + /// + /// Utility to get references to a hub's results. + /// + private static (string, IReadOnlyList, bool) HubRef( + StreamHub hub, bool irregular = false) + where TIn : ISeries + where TOut : ISeries + { + IReadOnlyList results = hub.Cache; + return (hub.ToString(), results, irregular); + } +} diff --git a/tests/indicators/_common/Observables/StreamHub.Utilities.StaticSeries.Tests.cs b/tests/indicators/_common/Observables/StreamHub.Utilities.StaticSeries.Tests.cs new file mode 100644 index 000000000..17e87a1dd --- /dev/null +++ b/tests/indicators/_common/Observables/StreamHub.Utilities.StaticSeries.Tests.cs @@ -0,0 +1,189 @@ +namespace Observables; + +[TestClass] +public class CacheUtilities : TestBase +{ + [TestMethod] + public void ClearCacheByTimestamp() + { + + // setup quote provider + + IReadOnlyList quotesList = Quotes + .Take(10) + .ToList(); + + int length = quotesList.Count; + + QuoteHub provider = new(); + + QuotePartHub observer = provider + .ToQuotePart(CandlePart.Close); + + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + Quote q3 = quotesList[3]; + + // act: clear cache + observer.RemoveRange(q3.Timestamp, notify: false); + + // assert: cache is empty + observer.Cache.Should().HaveCount(3); + provider.Cache.Should().HaveCount(10); + + List cacheOver + = observer.Results + .Where(c => c.Timestamp >= q3.Timestamp).ToList(); + + List cacheUndr + = observer.Results + .Where(c => c.Timestamp <= q3.Timestamp).ToList(); + + cacheOver.Should().BeEmpty(); + cacheUndr.Should().HaveCount(3); + } + + [TestMethod] + public void ClearCacheByIndex() + { + // setup quote provider + + List quotesList = Quotes + .ToSortedList() + .Take(10) + .ToList(); + + int length = quotesList.Count; + + QuoteHub provider = new(); + + QuotePartHub observer = provider + .ToQuotePart(CandlePart.Close); + + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + Quote q3 = quotesList[3]; + + // act: clear cache + observer.RemoveRange(3, notify: true); + + // assert: cache is empty + observer.Cache.Should().HaveCount(3); + provider.Cache.Should().HaveCount(10); + + List cacheOver + = observer.Results + .Where(c => c.Timestamp >= q3.Timestamp).ToList(); + + List cacheUndr + = observer.Results + .Where(c => c.Timestamp <= q3.Timestamp).ToList(); + + cacheOver.Should().BeEmpty(); + cacheUndr.Should().HaveCount(3); + } + + [TestMethod] + public void GetIndex() + { + // setup quote provider + + IReadOnlyList quotesList = Quotes + .Take(10) + .ToList(); + + int length = quotesList.Count; + + QuoteHub provider = new(); + + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + // find position of quote + Quote q = quotesList[4]; + + int itemIndexEx = provider.Cache.GetIndex(q, true); + int timeIndexEx = provider.Cache.GetIndex(q.Timestamp, true); + + // assert: same index + itemIndexEx.Should().Be(4); + timeIndexEx.Should().Be(4); + + // out of range (exceptions) + Quote o = Quotes[10]; + + Assert.ThrowsException(() => { + provider.Cache.GetIndex(o, true); + }); + + Assert.ThrowsException(() => { + provider.Cache.GetIndex(o.Timestamp, true); + }); + + // out of range (no exceptions) + int itemIndexNo = provider.Cache.GetIndex(o, false); + int timeIndexNo = provider.Cache.GetIndex(o.Timestamp, false); + + itemIndexNo.Should().Be(-1); + timeIndexNo.Should().Be(-1); + + int timeInsertOut = provider.Cache.GetIndexGte(o.Timestamp); + int timeInsertIn = provider.Cache.GetIndexGte(quotesList[2].Timestamp); + + timeInsertOut.Should().Be(-1); + timeInsertIn.Should().Be(2); + } + + [TestMethod] + public void TryFindIndex() + { + + // setup quote provider + + List quotesList = Quotes + .ToSortedList() + .Take(10) + .ToList(); + + int length = quotesList.Count; + + QuoteHub provider = new(); + + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + Quote q = quotesList[4]; + + // act: find index of quote + + // assert: correct index + if (provider.Cache.TryFindIndex(q.Timestamp, out int goodIndex)) + { + goodIndex.Should().Be(4); + } + else + { + Assert.Fail("index not found"); + } + + // assert: out of range + if (provider.Cache.TryFindIndex(DateTime.MaxValue, out int badIndex)) + { + Assert.Fail("unexpected index found"); + } + else + { + badIndex.Should().Be(-1); + } + } +} diff --git a/tests/indicators/_common/Quotes/Quote.Aggregates.Tests.cs b/tests/indicators/_common/Quotes/Quote.Aggregates.Tests.cs index 44a79bd47..8fa697629 100644 --- a/tests/indicators/_common/Quotes/Quote.Aggregates.Tests.cs +++ b/tests/indicators/_common/Quotes/Quote.Aggregates.Tests.cs @@ -1,24 +1,24 @@ -namespace Tests.Common; +namespace Utilities; -[TestClass] -public class QuoteAggregateTests : TestBase +// quote aggregates + +public partial class Quotes : TestBase { [TestMethod] public void Aggregate() { - IEnumerable quotes = TestData.GetIntraday(); + IReadOnlyList quotes = Data.GetIntraday(); // aggregate - List results = quotes - .Aggregate(PeriodSize.FifteenMinutes) - .ToList(); + IReadOnlyList results = quotes + .Aggregate(PeriodSize.FifteenMinutes); // proper quantities Assert.AreEqual(108, results.Count); // sample values Quote r0 = results[0]; - Assert.AreEqual(DateTime.Parse("2020-12-15 09:30", EnglishCulture), r0.Date); + Assert.AreEqual(DateTime.Parse("2020-12-15 09:30", TestBase.invariantCulture), r0.Timestamp); Assert.AreEqual(367.40m, r0.Open); Assert.AreEqual(367.775m, r0.High); Assert.AreEqual(367.02m, r0.Low); @@ -26,7 +26,7 @@ public void Aggregate() Assert.AreEqual(2401786m, r0.Volume); Quote r1 = results[1]; - Assert.AreEqual(DateTime.Parse("2020-12-15 09:45", EnglishCulture), r1.Date); + Assert.AreEqual(DateTime.Parse("2020-12-15 09:45", TestBase.invariantCulture), r1.Timestamp); Assert.AreEqual(367.25m, r1.Open); Assert.AreEqual(367.44m, r1.High); Assert.AreEqual(366.69m, r1.Low); @@ -34,7 +34,7 @@ public void Aggregate() Assert.AreEqual(1669983m, r1.Volume); Quote r2 = results[2]; - Assert.AreEqual(DateTime.Parse("2020-12-15 10:00", EnglishCulture), r2.Date); + Assert.AreEqual(DateTime.Parse("2020-12-15 10:00", TestBase.invariantCulture), r2.Timestamp); Assert.AreEqual(366.85m, r2.Open); Assert.AreEqual(367.17m, r2.High); Assert.AreEqual(366.57m, r2.Low); @@ -42,27 +42,26 @@ public void Aggregate() Assert.AreEqual(1396993m, r2.Volume); // no history scenario - List noQuotes = []; - IEnumerable noResults = noQuotes.Aggregate(PeriodSize.Day); + IReadOnlyList noQuotes = []; + IReadOnlyList noResults = noQuotes.Aggregate(PeriodSize.Day); Assert.IsFalse(noResults.Any()); } [TestMethod] public void AggregateTimeSpan() { - IEnumerable quotes = TestData.GetIntraday(); + IReadOnlyList quotes = Data.GetIntraday(); // aggregate - List results = quotes - .Aggregate(TimeSpan.FromMinutes(15)) - .ToList(); + IReadOnlyList results = quotes + .Aggregate(TimeSpan.FromMinutes(15)); // proper quantities Assert.AreEqual(108, results.Count); // sample values Quote r0 = results[0]; - Assert.AreEqual(DateTime.Parse("2020-12-15 09:30", EnglishCulture), r0.Date); + Assert.AreEqual(DateTime.Parse("2020-12-15 09:30", TestBase.invariantCulture), r0.Timestamp); Assert.AreEqual(367.40m, r0.Open); Assert.AreEqual(367.775m, r0.High); Assert.AreEqual(367.02m, r0.Low); @@ -70,7 +69,7 @@ public void AggregateTimeSpan() Assert.AreEqual(2401786m, r0.Volume); Quote r1 = results[1]; - Assert.AreEqual(DateTime.Parse("2020-12-15 09:45", EnglishCulture), r1.Date); + Assert.AreEqual(DateTime.Parse("2020-12-15 09:45", TestBase.invariantCulture), r1.Timestamp); Assert.AreEqual(367.25m, r1.Open); Assert.AreEqual(367.44m, r1.High); Assert.AreEqual(366.69m, r1.Low); @@ -78,7 +77,7 @@ public void AggregateTimeSpan() Assert.AreEqual(1669983m, r1.Volume); Quote r2 = results[2]; - Assert.AreEqual(DateTime.Parse("2020-12-15 10:00", EnglishCulture), r2.Date); + Assert.AreEqual(DateTime.Parse("2020-12-15 10:00", TestBase.invariantCulture), r2.Timestamp); Assert.AreEqual(366.85m, r2.Open); Assert.AreEqual(367.17m, r2.High); Assert.AreEqual(366.57m, r2.Low); @@ -86,8 +85,8 @@ public void AggregateTimeSpan() Assert.AreEqual(1396993m, r2.Volume); // no history scenario - List noQuotes = []; - IEnumerable noResults = noQuotes.Aggregate(TimeSpan.FromDays(1)); + IReadOnlyList noQuotes = []; + IReadOnlyList noResults = noQuotes.Aggregate(TimeSpan.FromDays(1)); Assert.IsFalse(noResults.Any()); } @@ -95,16 +94,15 @@ public void AggregateTimeSpan() public void AggregateMonth() { // aggregate - List results = quotes - .Aggregate(PeriodSize.Month) - .ToList(); + IReadOnlyList results = Quotes + .Aggregate(PeriodSize.Month); // proper quantities Assert.AreEqual(24, results.Count); // sample values Quote r0 = results[0]; - Assert.AreEqual(DateTime.Parse("2017-01-01", EnglishCulture), r0.Date); + Assert.AreEqual(DateTime.Parse("2017-01-01", TestBase.invariantCulture), r0.Timestamp); Assert.AreEqual(212.61m, r0.Open); Assert.AreEqual(217.02m, r0.High); Assert.AreEqual(211.52m, r0.Low); @@ -112,7 +110,7 @@ public void AggregateMonth() Assert.AreEqual(1569087580m, r0.Volume); Quote r1 = results[1]; - Assert.AreEqual(DateTime.Parse("2017-02-01", EnglishCulture), r1.Date); + Assert.AreEqual(DateTime.Parse("2017-02-01", TestBase.invariantCulture), r1.Timestamp); Assert.AreEqual(215.65m, r1.Open); Assert.AreEqual(224.20m, r1.High); Assert.AreEqual(214.29m, r1.Low); @@ -120,7 +118,7 @@ public void AggregateMonth() Assert.AreEqual(1444958340m, r1.Volume); Quote r23 = results[23]; - Assert.AreEqual(DateTime.Parse("2018-12-01", EnglishCulture), r23.Date); + Assert.AreEqual(DateTime.Parse("2018-12-01", TestBase.invariantCulture), r23.Timestamp); Assert.AreEqual(273.47m, r23.Open); Assert.AreEqual(273.59m, r23.High); Assert.AreEqual(229.42m, r23.Low); @@ -128,10 +126,8 @@ public void AggregateMonth() Assert.AreEqual(3173255968m, r23.Volume); } - [TestMethod] - [ExpectedException(typeof(ArgumentOutOfRangeException), "Bad aggregation size.")] - public void BadAggregationSize() => - - // bad period size - quotes.Aggregate(TimeSpan.Zero); + [TestMethod] // bad period size + public void AggregateBadSize() + => Assert.ThrowsException(() + => Quotes.Aggregate(TimeSpan.Zero)); } diff --git a/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs b/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs index e0cb88630..8d837fe98 100644 --- a/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs +++ b/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs @@ -1,265 +1,32 @@ using System.Collections.ObjectModel; -namespace Tests.Common; +// quote list converters + +namespace Utilities; [TestClass] -public class QuoteUtilityTests : TestBase +public partial class Quotes : TestBase { [TestMethod] - public void QuoteToSortedCollection() - { - IEnumerable quotes = TestData.GetMismatch(); - - Collection h = quotes.ToSortedCollection(); - - // proper quantities - Assert.AreEqual(502, h.Count); - - // check first date - DateTime firstDate = DateTime.ParseExact("01/18/2016", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(firstDate, h[0].Date); - - // check last date - DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(lastDate, h.LastOrDefault().Date); - - // spot check an out of sequence date - DateTime spotDate = DateTime.ParseExact("03/16/2017", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(spotDate, h[50].Date); - } - - [TestMethod] - public void QuoteToSortedList() + public void ToSortedList() { - IEnumerable quotes = TestData.GetMismatch(); + IReadOnlyList quotes = Data.GetMismatch(); - List h = quotes.ToSortedList(); + IReadOnlyList h = quotes.ToSortedList(); // proper quantities Assert.AreEqual(502, h.Count); // check first date - DateTime firstDate = DateTime.ParseExact("01/18/2016", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(firstDate, h[0].Date); + DateTime firstDate = DateTime.ParseExact("01/18/2016", "MM/dd/yyyy", TestBase.invariantCulture); + Assert.AreEqual(firstDate, h[0].Timestamp); // check last date - DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(lastDate, h.LastOrDefault().Date); + DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", TestBase.invariantCulture); + Assert.AreEqual(lastDate, h[^1].Timestamp); // spot check an out of sequence date - DateTime spotDate = DateTime.ParseExact("03/16/2017", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(spotDate, h[50].Date); - } - - [TestMethod] - public void QuoteToTuple() - { - DateTime d = DateTime.Parse("5/5/2055", EnglishCulture); - - decimal l = 111111111111111m; - decimal o = 222222222222222m; - decimal c = 333333333333333m; - decimal h = 444444444444444m; - decimal v = 555555555555555m; - decimal hl2 = (h + l) / 2m; - decimal hlc3 = (h + l + c) / 3m; - decimal oc2 = (o + c) / 2m; - decimal ohl3 = (o + h + l) / 3m; - decimal ohlc4 = (o + h + l + c) / 4m; - - Quote q = new() { - Date = d, - Open = o, - High = h, - Low = l, - Close = c, - Volume = v - }; - - Assert.AreEqual( - NullMath.Round((double)o, 10), - NullMath.Round(q.ToTuple(CandlePart.Open).value, 10)); - Assert.AreEqual( - NullMath.Round((double)h, 10), - NullMath.Round(q.ToTuple(CandlePart.High).value, 10)); - Assert.AreEqual( - NullMath.Round((double)l, 10), - NullMath.Round(q.ToTuple(CandlePart.Low).value, 10)); - Assert.AreEqual( - NullMath.Round((double)c, 10), - NullMath.Round(q.ToTuple(CandlePart.Close).value, 10)); - Assert.AreEqual( - NullMath.Round((double)v, 10), - NullMath.Round(q.ToTuple(CandlePart.Volume).value, 10)); - Assert.AreEqual( - NullMath.Round((double)hl2, 10), - NullMath.Round(q.ToTuple(CandlePart.HL2).value, 10)); - Assert.AreEqual( - NullMath.Round((double)hlc3, 10), - NullMath.Round(q.ToTuple(CandlePart.HLC3).value, 10)); - Assert.AreEqual( - NullMath.Round((double)oc2, 10), - NullMath.Round(q.ToTuple(CandlePart.OC2).value, 10)); - Assert.AreEqual( - NullMath.Round((double)ohl3, 10), - NullMath.Round(q.ToTuple(CandlePart.OHL3).value, 10)); - Assert.AreEqual( - NullMath.Round((double)ohlc4, 10), - NullMath.Round(q.ToTuple(CandlePart.OHLC4).value, 10)); - - // bad argument - Assert.ThrowsException(() - => q.ToTuple((CandlePart)999)); - - // bad argument - Assert.ThrowsException(() - => q.ToBasicData((CandlePart)999)); - } - - [TestMethod] - public void ToTupleCollection() - { - Collection<(DateTime, double)> collection = quotes - .OrderBy(x => x.Date) - .ToTupleCollection(CandlePart.Close); - - Assert.IsNotNull(collection); - Assert.AreEqual(502, collection.Count); - Assert.AreEqual(collection.LastOrDefault().Item2, 245.28d); - } - - [TestMethod] - public void ToSortedList() - { - Collection<(DateTime, double)> collection = quotes - .OrderBy(x => x.Date) - .ToTuple(CandlePart.Close) - .ToSortedCollection(); - - Assert.IsNotNull(collection); - Assert.AreEqual(502, collection.Count); - Assert.AreEqual(collection.LastOrDefault().Item2, 245.28d); - } - - [TestMethod] - public void QuoteToBasicData() - { - DateTime d = DateTime.Parse("5/5/2055", EnglishCulture); - - decimal l = 111111111111111m; - decimal o = 222222222222222m; - decimal c = 333333333333333m; - decimal h = 444444444444444m; - decimal v = 555555555555555m; - decimal hl2 = (h + l) / 2m; - decimal hlc3 = (h + l + c) / 3m; - decimal oc2 = (o + c) / 2m; - decimal ohl3 = (o + h + l) / 3m; - decimal ohlc4 = (o + h + l + c) / 4m; - - Quote q = new() { - Date = d, - Open = o, - High = h, - Low = l, - Close = c, - Volume = v - }; - - Assert.AreEqual( - NullMath.Round((double)o, 10), - NullMath.Round(q.ToBasicData(CandlePart.Open).Value, 10)); - Assert.AreEqual( - NullMath.Round((double)h, 10), - NullMath.Round(q.ToBasicData(CandlePart.High).Value, 10)); - Assert.AreEqual( - NullMath.Round((double)l, 10), - NullMath.Round(q.ToBasicData(CandlePart.Low).Value, 10)); - Assert.AreEqual( - NullMath.Round((double)c, 10), - NullMath.Round(q.ToBasicData(CandlePart.Close).Value, 10)); - Assert.AreEqual( - NullMath.Round((double)v, 10), - NullMath.Round(q.ToBasicData(CandlePart.Volume).Value, 10)); - Assert.AreEqual( - NullMath.Round((double)hl2, 10), - NullMath.Round(q.ToBasicData(CandlePart.HL2).Value, 10)); - Assert.AreEqual( - NullMath.Round((double)hlc3, 10), - NullMath.Round(q.ToBasicData(CandlePart.HLC3).Value, 10)); - Assert.AreEqual( - NullMath.Round((double)oc2, 10), - NullMath.Round(q.ToBasicData(CandlePart.OC2).Value, 10)); - Assert.AreEqual( - NullMath.Round((double)ohl3, 10), - NullMath.Round(q.ToBasicData(CandlePart.OHL3).Value, 10)); - Assert.AreEqual( - NullMath.Round((double)ohlc4, 10), - NullMath.Round(q.ToBasicData(CandlePart.OHLC4).Value, 10)); - - // bad argument - Assert.ThrowsException(() - => q.ToBasicData((CandlePart)999)); - } - - [TestMethod] - public void QuoteDToTuple() - { - DateTime d = DateTime.Parse("5/5/2055", EnglishCulture); - - double l = 111111111111111; - double o = 222222222222222; - double c = 333333333333333; - double h = 444444444444444; - double v = 555555555555555; - double hl2 = (h + l) / 2; - double hlc3 = (h + l + c) / 3; - double oc2 = (o + c) / 2; - double ohl3 = (o + h + l) / 3; - double ohlc4 = (o + h + l + c) / 4; - - QuoteD q = new() { - Date = d, - Open = o, - High = h, - Low = l, - Close = c, - Volume = v - }; - - Assert.AreEqual( - NullMath.Round((double)o, 10), - NullMath.Round(q.ToTuple(CandlePart.Open).Item2, 10)); - Assert.AreEqual( - NullMath.Round((double)h, 10), - NullMath.Round(q.ToTuple(CandlePart.High).Item2, 10)); - Assert.AreEqual( - NullMath.Round((double)l, 10), - NullMath.Round(q.ToTuple(CandlePart.Low).Item2, 10)); - Assert.AreEqual( - NullMath.Round((double)c, 10), - NullMath.Round(q.ToTuple(CandlePart.Close).Item2, 10)); - Assert.AreEqual( - NullMath.Round((double)v, 10), - NullMath.Round(q.ToTuple(CandlePart.Volume).Item2, 10)); - Assert.AreEqual( - NullMath.Round((double)hl2, 10), - NullMath.Round(q.ToTuple(CandlePart.HL2).Item2, 10)); - Assert.AreEqual( - NullMath.Round((double)hlc3, 10), - NullMath.Round(q.ToTuple(CandlePart.HLC3).Item2, 10)); - Assert.AreEqual( - NullMath.Round((double)oc2, 10), - NullMath.Round(q.ToTuple(CandlePart.OC2).Item2, 10)); - Assert.AreEqual( - NullMath.Round((double)ohl3, 10), - NullMath.Round(q.ToTuple(CandlePart.OHL3).Item2, 10)); - Assert.AreEqual( - NullMath.Round((double)ohlc4, 10), - NullMath.Round(q.ToTuple(CandlePart.OHLC4).Item2, 10)); - - // bad argument - Assert.ThrowsException(() - => q.ToTuple((CandlePart)999)); + DateTime spotDate = DateTime.ParseExact("03/16/2017", "MM/dd/yyyy", TestBase.invariantCulture); + Assert.AreEqual(spotDate, h[50].Timestamp); } } diff --git a/tests/indicators/_common/Quotes/Quote.Equality.Tests.cs b/tests/indicators/_common/Quotes/Quote.Equality.Tests.cs new file mode 100644 index 000000000..0ebbe8621 --- /dev/null +++ b/tests/indicators/_common/Quotes/Quote.Equality.Tests.cs @@ -0,0 +1,26 @@ +namespace Utilities; + +// quotes equality + +public partial class Quotes : TestBase +{ + [TestMethod] + public void Equality() + { + Quote q1 = new(EvalDate, 1m, 1m, 1m, 1m, 100); + Quote q2 = new(EvalDate, 1m, 1m, 1m, 1m, 100); + Quote q3 = new(EvalDate, 1m, 1m, 1m, 2m, 99); + + Assert.IsTrue(Equals(q1, q2)); + Assert.IsFalse(Equals(q1, q3)); + + Assert.IsTrue(q1.Equals(q2)); + Assert.IsFalse(q1.Equals(q3)); + + Assert.IsTrue(q1 == q2); + Assert.IsFalse(q1 == q3); + + Assert.IsFalse(q1 != q2); + Assert.IsTrue(q1 != q3); + } +} diff --git a/tests/indicators/_common/Quotes/Quote.Exceptions.Tests.cs b/tests/indicators/_common/Quotes/Quote.Exceptions.Tests.cs index 5a546b482..48ba52573 100644 --- a/tests/indicators/_common/Quotes/Quote.Exceptions.Tests.cs +++ b/tests/indicators/_common/Quotes/Quote.Exceptions.Tests.cs @@ -1,21 +1,21 @@ -namespace Tests.Common; +namespace Utilities; -[TestClass] -public class ExceptionTests : TestBase +// invalid quotes exceptions + +public partial class Quotes : TestBase { - // bad quotes exceptions [TestMethod] - [ExpectedException(typeof(InvalidQuotesException), "Bad quotes without message.")] - public void BadHistory() - => throw new InvalidQuotesException(); + public void InvalidQuotesExceptionThrow() + => Assert.ThrowsException(() + => throw new InvalidQuotesException()); [TestMethod] - [ExpectedException(typeof(InvalidQuotesException), "Bad quotes with message.")] - public void BadHistoryWithMessage() - => throw new InvalidQuotesException("This is a quotes exception."); + public void InvalidQuotesExceptionThrowWithMessage() + => Assert.ThrowsException(() + => throw new InvalidQuotesException("This is a quotes exception.")); [TestMethod] - [ExpectedException(typeof(InvalidQuotesException), "Bad quotes with inner Exception.")] - public void BadHistoryWithInner() - => throw new InvalidQuotesException("This has an inner Exception.", new ArgumentException()); + public void InvalidQuotesExceptionThrowWithInner() + => Assert.ThrowsException(() + => throw new InvalidQuotesException("This has an inner Exception.", new ArgumentException())); } diff --git a/tests/indicators/_common/Quotes/Quote.StreamHub.Tests.cs b/tests/indicators/_common/Quotes/Quote.StreamHub.Tests.cs new file mode 100644 index 000000000..babf4c1c3 --- /dev/null +++ b/tests/indicators/_common/Quotes/Quote.StreamHub.Tests.cs @@ -0,0 +1,134 @@ +namespace StreamHub; + +// QUOTEHUB + +[TestClass] +public class QuoteHub : StreamHubTestBase, ITestChainProvider +{ + [TestMethod] + public override void QuoteObserver() + { + // tests quote redistribution + + List quotesList = Quotes.ToList(); + + int length = Quotes.Count; + + // add base quotes (batch) + QuoteHub provider = new(); + + provider.Add(quotesList.Take(200)); + + // add incremental quotes + for (int i = 200; i < length; i++) + { + Quote q = quotesList[i]; + provider.Add(q); + } + + QuoteHub observer + = provider.ToQuote(); + + // close observations + provider.EndTransmission(); + + // assert same as original + observer.Cache.Should().HaveCount(length); + observer.Cache.Should().BeEquivalentTo(provider.Cache); + } + + [TestMethod] + public void ChainProvider() + { + int smaPeriods = 8; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + SmaHub observer + = provider + .ToSma(smaPeriods); + + // emulate quote stream + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + // delete + provider.RemoveAt(400); + quotesList.RemoveAt(400); + + // final results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList + .ToSma(smaPeriods); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + // cleanup + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public override void CustomToString() + { + QuoteHub hub = new(); + + hub.ToString().Should().Be("QUOTES: 0 items"); + + hub.Add(Quotes[0]); + hub.Add(Quotes[1]); + + hub.ToString().Should().Be("QUOTES: 2 items"); + } + + [TestMethod] + public void AddQuote() + { + // covers both single and batch add + + List quotesList = Quotes.ToList(); + + int length = Quotes.Count; + + // add base quotes (batch) + QuoteHub provider = new(); + + provider.Add(quotesList.Take(200)); + + // add incremental quotes + for (int i = 200; i < length; i++) + { + Quote q = quotesList[i]; + provider.Add(q); + } + + // assert same as original + for (int i = 0; i < length; i++) + { + Quote o = quotesList[i]; + Quote q = provider.Cache[i]; + + Assert.AreEqual(o, q); // same ref + } + + // confirm public interfaces + Assert.AreEqual(provider.Cache.Count, provider.Quotes.Count); + + // close observations + provider.EndTransmission(); + } +} diff --git a/tests/indicators/_common/Quotes/Quote.Validation.Tests.cs b/tests/indicators/_common/Quotes/Quote.Validation.Tests.cs index 78d66cace..c1c54cac1 100644 --- a/tests/indicators/_common/Quotes/Quote.Validation.Tests.cs +++ b/tests/indicators/_common/Quotes/Quote.Validation.Tests.cs @@ -1,37 +1,40 @@ -namespace Tests.Common; +using System.Globalization; -[TestClass] -public class QuoteValidationTests : TestBase +namespace Utilities; + +// quote validation + +public partial class Quotes : TestBase { [TestMethod] public void Validate() { - IEnumerable quotes = TestData.GetDefault(); + IReadOnlyList quotes = Data.GetDefault(); - List h = quotes.Validate().ToList(); + IReadOnlyList h = quotes.Validate(); // proper quantities Assert.AreEqual(502, h.Count); // sample values - DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(lastDate, h[501].Date); + DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", invariantCulture); + Assert.AreEqual(lastDate, h[501].Timestamp); - DateTime spotDate = DateTime.ParseExact("02/01/2017", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(spotDate, h[20].Date); + DateTime spotDate = DateTime.ParseExact("02/01/2017", "MM/dd/yyyy", invariantCulture); + Assert.AreEqual(spotDate, h[20].Timestamp); } [TestMethod] public void ValidateLong() { - List h = longishQuotes.Validate().ToList(); + IReadOnlyList h = LongishQuotes.Validate(); // proper quantities Assert.AreEqual(5285, h.Count); // sample values - DateTime lastDate = DateTime.ParseExact("09/04/2020", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(lastDate, h[5284].Date); + DateTime lastDate = DateTime.ParseExact("09/04/2020", "MM/dd/yyyy", invariantCulture); + Assert.AreEqual(lastDate, h[5284].Timestamp); } [TestMethod] @@ -39,28 +42,29 @@ public void ValidateCut() { // if quotes post-cleaning, is cut down in size it should not corrupt the results - IEnumerable quotes = TestData.GetDefault(200); - List h = quotes.Validate().ToList(); + IReadOnlyList quotes = Data.GetDefault(200); + + IReadOnlyList h = quotes.Validate(); // should be 200 periods, initially Assert.AreEqual(200, h.Count); // should be 20 results and no index corruption - List r1 = Indicator.GetSma(h.TakeLast(20), 14).ToList(); + IReadOnlyList r1 = h.TakeLast(20).ToList().ToSma(14).ToList(); Assert.AreEqual(20, r1.Count); for (int i = 1; i < r1.Count; i++) { - Assert.IsTrue(r1[i].Date >= r1[i - 1].Date); + Assert.IsTrue(r1[i].Timestamp >= r1[i - 1].Timestamp); } // should be 50 results and no index corruption - List r2 = Indicator.GetSma(h.TakeLast(50), 14).ToList(); + IReadOnlyList r2 = h.TakeLast(50).ToList().ToSma(14).ToList(); Assert.AreEqual(50, r2.Count); for (int i = 1; i < r2.Count; i++) { - Assert.IsTrue(r2[i].Date >= r2[i - 1].Date); + Assert.IsTrue(r2[i].Timestamp >= r2[i - 1].Timestamp); } // should be original 200 periods and no index corruption, after temp mods @@ -68,26 +72,46 @@ public void ValidateCut() for (int i = 1; i < h.Count; i++) { - Assert.IsTrue(h[i].Date >= h[i - 1].Date); + Assert.IsTrue(h[i].Timestamp >= h[i - 1].Timestamp); } } [TestMethod] - public void DuplicateHistory() + public void ValidateDuplicates() { - List badHistory = - [ - new Quote { Date = DateTime.ParseExact("2017-01-03", "yyyy-MM-dd", EnglishCulture), Open = 214.86m, High = 220.33m, Low = 210.96m, Close = 216.99m, Volume = 5923254 }, - new Quote { Date = DateTime.ParseExact("2017-01-04", "yyyy-MM-dd", EnglishCulture), Open = 214.75m, High = 228.00m, Low = 214.31m, Close = 226.99m, Volume = 11213471 }, - new Quote { Date = DateTime.ParseExact("2017-01-05", "yyyy-MM-dd", EnglishCulture), Open = 226.42m, High = 227.48m, Low = 221.95m, Close = 226.75m, Volume = 5911695 }, - new Quote { Date = DateTime.ParseExact("2017-01-06", "yyyy-MM-dd", EnglishCulture), Open = 226.93m, High = 230.31m, Low = 225.45m, Close = 229.01m, Volume = 5527893 }, - new Quote { Date = DateTime.ParseExact("2017-01-06", "yyyy-MM-dd", EnglishCulture), Open = 228.97m, High = 231.92m, Low = 228.00m, Close = 231.28m, Volume = 3979484 } - ]; - - InvalidQuotesException ex = - Assert.ThrowsException(() - => badHistory.Validate()); - - ex.Message.Should().Contain("Duplicate date found on 2017-01-06T00:00:00.0000000."); + IReadOnlyList dupQuotes = new List + { + new(Timestamp: DateTime.ParseExact("2017-01-03", "yyyy-MM-dd", invariantCulture), Open: 214.86m, High: 220.33m, Low: 210.96m, Close: 216.99m, Volume: 5923254), + new(Timestamp: DateTime.ParseExact("2017-01-04", "yyyy-MM-dd", invariantCulture), Open: 214.75m, High: 228.00m, Low: 214.31m, Close: 226.99m, Volume: 11213471), + new(Timestamp: DateTime.ParseExact("2017-01-05", "yyyy-MM-dd", invariantCulture), Open: 226.42m, High: 227.48m, Low: 221.95m, Close: 226.75m, Volume: 5911695), + new(Timestamp: DateTime.ParseExact("2017-01-06", "yyyy-MM-dd", invariantCulture), Open: 226.93m, High: 230.31m, Low: 225.45m, Close: 229.01m, Volume: 5527893), + new(Timestamp: DateTime.ParseExact("2017-01-06", "yyyy-MM-dd", invariantCulture), Open: 228.97m, High: 231.92m, Low: 228.00m, Close: 231.28m, Volume: 3979484) + }; + + InvalidQuotesException dx + = Assert.ThrowsException( + () => dupQuotes.Validate()); + + dx.Message.Should().Contain("Duplicate date found on 2017-01-06T00:00:00.0000000."); + } + + [TestMethod] + public void ValidateOutOfSequence() + { + IReadOnlyList unorderedQuotes = new List + { + new(Timestamp: DateTime.ParseExact("2017-01-03", "yyyy-MM-dd", invariantCulture), Open: 214.86m, High: 220.33m, Low: 210.96m, Close: 216.99m, Volume: 5923254), + new(Timestamp: DateTime.ParseExact("2017-01-04", "yyyy-MM-dd", invariantCulture), Open: 214.75m, High: 228.00m, Low: 214.31m, Close: 226.99m, Volume: 11213471), + new(Timestamp: DateTime.ParseExact("2017-01-06", "yyyy-MM-dd", invariantCulture), Open: 228.97m, High: 231.92m, Low: 228.00m, Close: 231.28m, Volume: 3979484), + new(Timestamp: DateTime.ParseExact("2017-01-05", "yyyy-MM-dd", invariantCulture), Open: 226.42m, High: 227.48m, Low: 221.95m, Close: 226.75m, Volume: 5911695), + new(Timestamp: DateTime.ParseExact("2017-01-06", "yyyy-MM-dd", invariantCulture), Open: 226.93m, High: 230.31m, Low: 225.45m, Close: 229.01m, Volume: 5527893) + }; + + InvalidQuotesException dx + = Assert.ThrowsException( + () => unorderedQuotes.Validate()); + + dx.Message.Should() + .Contain("Quotes are out of sequence on 2017-01-05T00:00:00.0000000."); } } diff --git a/tests/indicators/_common/Quotes/Test.Quote.Provider.cs b/tests/indicators/_common/Quotes/Test.Quote.Provider.cs deleted file mode 100644 index 64d0a8a4d..000000000 --- a/tests/indicators/_common/Quotes/Test.Quote.Provider.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Skender.Stock.Indicators; - -namespace Tests.Common; - -[TestClass] -public class QuoteSourceTests : TestBase -{ - [TestMethod] - public void Standard() - { - List quotesList = quotes - .ToSortedList(); - - int length = quotes.Count(); - - // add base quotes - QuoteProvider provider = new(); - provider.Add(quotesList.Take(200)); - - // emulate incremental quotes - for (int i = 200; i < length; i++) - { - Quote q = quotesList[i]; - provider.Add(q); - } - - // assert same as original - for (int i = 0; i < length; i++) - { - Quote o = quotesList[i]; - Quote q = provider.ProtectedQuotes[i]; - - Assert.AreEqual(o, q); - } - - provider.EndTransmission(); - } - - [TestMethod] - public void Exceptions() - { - // null quote added - QuoteProvider provider = new(); - - Assert.ThrowsException(() => - { - Quote quote = new() - { - Date = DateTime.Now - }; - - for (int i = 0; i <= 101; i++) - { - provider.Add(quote); - } - }); - - provider.EndTransmission(); - } -} diff --git a/tests/indicators/_common/Quotes/Test.Use.Observer.cs b/tests/indicators/_common/Quotes/Test.Use.Observer.cs deleted file mode 100644 index bf52f9217..000000000 --- a/tests/indicators/_common/Quotes/Test.Use.Observer.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Skender.Stock.Indicators; -using Tests.Common; - -namespace Tests.Indicators; - -[TestClass] -public class UseStreamTests : TestBase -{ - [TestMethod] - public void Standard() - { - List quotesList = quotes - .ToSortedList(); - - int length = quotesList.Count; - - // time-series, for comparison - List<(DateTime Date, double Value)> seriesList = quotes - .ToTuple(CandlePart.Close); - - // setup quote provider - QuoteProvider provider = new(); - - // initialize EMA observer - UseObserver observer = provider - .Use(CandlePart.Close); - - // fetch initial results - IEnumerable<(DateTime Date, double Value)> results - = observer.Results; - - // emulate adding quotes to provider - for (int i = 0; i < length; i++) - { - Quote q = quotesList[i]; - provider.Add(q); - } - - // final results - List<(DateTime Date, double Value)> resultsList - = results.ToList(); - - // assert, should equal series - for (int i = 0; i < seriesList.Count; i++) - { - (DateTime sDate, double sValue) = seriesList[i]; - (DateTime rDate, double rValue) = resultsList[i]; - - Assert.AreEqual(sDate, rDate); - Assert.AreEqual(sValue, rValue); - } - - observer.Unsubscribe(); - provider.EndTransmission(); - } - - [TestMethod] - public void Chainor() - { - List quotesList = quotes - .ToSortedList(); - - int length = quotesList.Count; - - // time-series, for comparison - List staticEma = quotes - .Use(CandlePart.HL2) - .GetEma(11) - .ToList(); - - // setup quote provider - QuoteProvider provider = new(); - - // initialize EMA observer - List streamEma = provider - .Use(CandlePart.HL2) - .GetEma(11) - .ProtectedResults; - - // emulate adding quotes to provider - for (int i = 0; i < length; i++) - { - provider.Add(quotesList[i]); - } - - provider.EndTransmission(); - - // assert, should equal series - for (int i = 0; i < length; i++) - { - EmaResult t = staticEma[i]; - EmaResult s = streamEma[i]; - - Assert.AreEqual(t.Date, s.Date); - Assert.AreEqual(t.Ema, s.Ema); - } - } -} diff --git a/tests/indicators/_common/Results/Result.Syncing.Tests.cs b/tests/indicators/_common/Results/Result.Syncing.Tests.cs deleted file mode 100644 index e22906cdb..000000000 --- a/tests/indicators/_common/Results/Result.Syncing.Tests.cs +++ /dev/null @@ -1,101 +0,0 @@ -namespace Tests.Common; - -[TestClass] -public class Syncing : TestBase -{ - [TestMethod] - public void SyncIndex() - { - // baseline for comparison - List baseline = - [ - new SmaResult(DateTime.Parse("1/1/2000", EnglishCulture)) { Sma = null }, - new SmaResult(DateTime.Parse("1/2/2000", EnglishCulture)) { Sma = null }, - new SmaResult(DateTime.Parse("1/3/2000", EnglishCulture)) { Sma = 3 }, - new SmaResult(DateTime.Parse("1/4/2000", EnglishCulture)) { Sma = 4 }, - new SmaResult(DateTime.Parse("1/5/2000", EnglishCulture)) { Sma = 5 }, - new SmaResult(DateTime.Parse("1/6/2000", EnglishCulture)) { Sma = 6 }, - new SmaResult(DateTime.Parse("1/7/2000", EnglishCulture)) { Sma = 7 }, - new SmaResult(DateTime.Parse("1/8/2000", EnglishCulture)) { Sma = double.NaN }, - new SmaResult(DateTime.Parse("1/9/2000", EnglishCulture)) { Sma = null }, - ]; - - // to be synced - List eval = - [ - new EmaResult(DateTime.Parse("1/3/2000", EnglishCulture)) { Ema = 3 }, - new EmaResult(DateTime.Parse("1/4/2000", EnglishCulture)) { Ema = 4 }, - new EmaResult(DateTime.Parse("1/5/2000", EnglishCulture)) { Ema = 5 }, - new EmaResult(DateTime.Parse("1/6/2000", EnglishCulture)) { Ema = 6 }, - new EmaResult(DateTime.Parse("1/7/2000", EnglishCulture)) { Ema = 7 }, - new EmaResult(DateTime.Parse("1/9/2000", EnglishCulture)) { Ema = double.NaN }, - new EmaResult(DateTime.Parse("1/10/2000", EnglishCulture)) { Ema = null }, - ]; - - // prepend option - List prepend = eval.SyncIndex(baseline, SyncType.Prepend).ToList(); - - Assert.AreEqual(9, prepend.Count); - Assert.AreEqual(3, prepend.Count(x => x.Ema is null)); - - for (int i = 0; i < 6; i++) - { - SmaResult b = baseline[i]; - EmaResult r = prepend[i]; - - Assert.AreEqual(b.Date, r.Date); - } - - // append option - List append = eval.SyncIndex(baseline, SyncType.AppendOnly).ToList(); - - Assert.AreEqual(10, append.Count); - Assert.AreEqual(4, append.Count(x => x.Ema is null)); - - for (int i = 0; i < 8; i++) - { - SmaResult b = baseline[i]; - EmaResult r = append[i]; - - Assert.AreEqual(b.Date, r.Date); - } - - // remove option - List remove = eval.SyncIndex(baseline, SyncType.RemoveOnly).ToList(); - - Assert.AreEqual(6, remove.Count); - Assert.AreEqual(0, remove.Count(x => x.Ema is null)); - Assert.AreEqual(0, remove.Count(x => - x.Date == DateTime.Parse("1/10/2000", EnglishCulture))); - - // full option - List fullmatch = eval.SyncIndex(baseline, SyncType.FullMatch).ToList(); - - Assert.AreEqual(9, fullmatch.Count); - Assert.AreEqual(3, fullmatch.Count(x => x.Ema is null)); - Assert.AreEqual(0, fullmatch.Count(x => - x.Date == DateTime.Parse("1/10/2000", EnglishCulture))); - - for (int i = 0; i < baseline.Count; i++) - { - SmaResult b = baseline[i]; - EmaResult r = fullmatch[i]; - - Assert.AreEqual(b.Date, r.Date); - } - - // no results - List noBaseline = []; - List noEval = []; - - IEnumerable noBaseResults = eval.SyncIndex(noBaseline); - IEnumerable noEvalResults = noEval.SyncIndex(baseline); - - Assert.IsFalse(noBaseResults.Any()); - Assert.IsFalse(noEvalResults.Any()); - - // bad sync type - Assert.ThrowsException(() - => eval.SyncIndex(baseline, (SyncType)int.MaxValue)); - } -} diff --git a/tests/indicators/_common/Results/Result.Utilities.Tests.cs b/tests/indicators/_common/Results/Result.Utilities.Tests.cs deleted file mode 100644 index 555386720..000000000 --- a/tests/indicators/_common/Results/Result.Utilities.Tests.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Tests.Common; - -[TestClass] -public class Results : TestBase -{ - [TestMethod] - public void Condense() - { - List x = quotes - .GetAdx(14) - .ToList(); - - // make a few more in the middle null and NaN - x[249].Adx = null; - x[345].Adx = double.NaN; - - List r = x.Condense().ToList(); - - // proper quantities - Assert.AreEqual(473, r.Count); - - // sample values - AdxResult last = r.LastOrDefault(); - Assert.AreEqual(17.7565, last.Pdi.Round(4)); - Assert.AreEqual(31.1510, last.Mdi.Round(4)); - Assert.AreEqual(34.2987, last.Adx.Round(4)); - } - - [TestMethod] - public void ToTuple() - { - // baseline for comparison - List baseline = - [ - new SmaResult(DateTime.Parse("1/1/2000", EnglishCulture)) { Sma = null }, - new SmaResult(DateTime.Parse("1/2/2000", EnglishCulture)) { Sma = null }, - new SmaResult(DateTime.Parse("1/3/2000", EnglishCulture)) { Sma = 3 }, - new SmaResult(DateTime.Parse("1/4/2000", EnglishCulture)) { Sma = 4 }, - new SmaResult(DateTime.Parse("1/5/2000", EnglishCulture)) { Sma = 5 }, - new SmaResult(DateTime.Parse("1/6/2000", EnglishCulture)) { Sma = 6 }, - new SmaResult(DateTime.Parse("1/7/2000", EnglishCulture)) { Sma = 7 }, - new SmaResult(DateTime.Parse("1/8/2000", EnglishCulture)) { Sma = double.NaN }, - new SmaResult(DateTime.Parse("1/9/2000", EnglishCulture)) { Sma = null }, - ]; - - // default chainable NaN with pruning (internal) - List<(DateTime Date, double Value)> chainableTuple = baseline - .ToTuple(); - - Assert.AreEqual(5, chainableTuple.Count(x => !double.IsNaN(x.Value))); - Assert.AreEqual(2, chainableTuple.Count(x => double.IsNaN(x.Value))); - - // PUBLIC VARIANT - - // default chainable NaN with pruning - Collection<(DateTime Date, double Value)> cnaNresults = baseline - .ToTupleChainable(); - - Assert.AreEqual(5, cnaNresults.Count(x => !double.IsNaN(x.Value))); - Assert.AreEqual(2, cnaNresults.Count(x => double.IsNaN(x.Value))); - - // with NaN option, no pruning - Collection<(DateTime Date, double Value)> nanResults = baseline - .ToTupleNaN(); - - Assert.AreEqual(4, nanResults.Count(x => x.Value is double.NaN)); - Assert.AreEqual(9, nanResults.Count); - } -} diff --git a/tests/indicators/_common/Reusable/Reusable.Utilities.Tests.cs b/tests/indicators/_common/Reusable/Reusable.Utilities.Tests.cs new file mode 100644 index 000000000..f8bba07d7 --- /dev/null +++ b/tests/indicators/_common/Reusable/Reusable.Utilities.Tests.cs @@ -0,0 +1,98 @@ +namespace Utilities; + +[TestClass] +public class Reusable : TestBase +{ + [TestMethod] + public void Condense() + { + List original = Quotes + .ToAdx() + .ToList(); + + // make a few more in the middle null and NaN + original[249] = original[249] with { Adx = null }; + original[345] = original[345] with { Adx = double.NaN }; + + IReadOnlyList results + = original.Condense(); + + // proper quantities + Assert.AreEqual(473, results.Count); + + // sample values + AdxResult last = results[^1]; + Assert.AreEqual(17.7565, last.Pdi.Round(4)); + Assert.AreEqual(31.1510, last.Mdi.Round(4)); + Assert.AreEqual(34.2987, last.Adx.Round(4)); + } + + [TestMethod] + public void ToReusableList() + { + IReadOnlyList reusableList = Quotes + .ToReusableList(CandlePart.Close); + + Assert.IsNotNull(reusableList); + Assert.AreEqual(502, reusableList.Count); + Assert.AreEqual(245.28d, reusableList[^1].Value); + } + + [TestMethod] + public void QuoteToReusable() + { + DateTime t = DateTime.Parse("5/5/2055", invariantCulture); + + decimal l = 111111111111111m; + decimal o = 222222222222222m; + decimal c = 333333333333333m; + decimal h = 444444444444444m; + decimal v = 555555555555555m; + decimal hl2 = (h + l) / 2m; + decimal hlc3 = (h + l + c) / 3m; + decimal oc2 = (o + c) / 2m; + decimal ohl3 = (o + h + l) / 3m; + decimal ohlc4 = (o + h + l + c) / 4m; + + Quote q = new(t, o, h, l, c, v); + + Assert.AreEqual( + ((double)o).Round(10), + q.ToReusable(CandlePart.Open).Value.Round(10)); + Assert.AreEqual( + ((double)h).Round(10), + q.ToReusable(CandlePart.High).Value.Round(10)); + Assert.AreEqual( + ((double)l).Round(10), + q.ToReusable(CandlePart.Low).Value.Round(10)); + Assert.AreEqual( + ((double)c).Round(10), + q.ToReusable(CandlePart.Close).Value.Round(10)); + Assert.AreEqual( + ((double)v).Round(10), + q.ToReusable(CandlePart.Volume).Value.Round(10)); + Assert.AreEqual( + ((double)hl2).Round(10), + q.ToReusable(CandlePart.HL2).Value.Round(10)); + Assert.AreEqual( + ((double)hlc3).Round(10), + q.ToReusable(CandlePart.HLC3).Value.Round(10)); + Assert.AreEqual( + ((double)oc2).Round(10), + q.ToReusable(CandlePart.OC2).Value.Round(10)); + Assert.AreEqual( + ((double)ohl3).Round(10), + q.ToReusable(CandlePart.OHL3).Value.Round(10)); + Assert.AreEqual( + ((double)ohlc4).Round(10), + q.ToReusable(CandlePart.OHLC4).Value.Round(10)); + + // bad argument + Assert.ThrowsException(() + => q.ToReusable((CandlePart)999)); + + // bad argument + Assert.ThrowsException(() + => q.ToReusable((CandlePart)999)); + } +} diff --git a/tests/indicators/_common/Use (QuotePart)/QuotePart.StaticSeries.Tests.cs b/tests/indicators/_common/Use (QuotePart)/QuotePart.StaticSeries.Tests.cs new file mode 100644 index 000000000..72c57b1a2 --- /dev/null +++ b/tests/indicators/_common/Use (QuotePart)/QuotePart.StaticSeries.Tests.cs @@ -0,0 +1,88 @@ +namespace StaticSeries; + +[TestClass] +public class QuoteParts : StaticSeriesTestBase +{ + [TestMethod] + public override void Standard() + { + // compose data + IReadOnlyList o = Quotes.Use(CandlePart.Open); + IReadOnlyList h = Quotes.Use(CandlePart.High); + IReadOnlyList l = Quotes.Use(CandlePart.Low); + IReadOnlyList c = Quotes.Use(CandlePart.Close); + IReadOnlyList v = Quotes.Use(CandlePart.Volume); + IReadOnlyList hl = Quotes.Use(CandlePart.HL2); + IReadOnlyList hlc = Quotes.Use(CandlePart.HLC3); + IReadOnlyList oc = Quotes.Use(CandlePart.OC2); + IReadOnlyList ohl = Quotes.Use(CandlePart.OHL3); + IReadOnlyList ohlc = Quotes.Use(CandlePart.OHLC4); + + // proper quantities + Assert.AreEqual(502, c.Count); + + // samples + QuotePart ro = o[501]; + QuotePart rh = h[501]; + QuotePart rl = l[501]; + QuotePart rc = c[501]; + QuotePart rv = v[501]; + QuotePart rhl = hl[501]; + QuotePart rhlc = hlc[501]; + QuotePart roc = oc[501]; + QuotePart rohl = ohl[501]; + QuotePart rohlc = ohlc[501]; + + // proper last date + DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", invariantCulture); + Assert.AreEqual(lastDate, rc.Timestamp); + + // last values should be correct + Assert.AreEqual(245.28, rc.Value); + Assert.AreEqual(244.92, ro.Value); + Assert.AreEqual(245.54, rh.Value); + Assert.AreEqual(242.87, rl.Value); + Assert.AreEqual(245.28, rc.Value); + Assert.AreEqual(147031456, rv.Value); + Assert.AreEqual(244.205, rhl.Value); + Assert.AreEqual(244.5633, rhlc.Value.Round(4)); + Assert.AreEqual(245.1, roc.Value); + Assert.AreEqual(244.4433, rohl.Value.Round(4)); + Assert.AreEqual(244.6525, rohlc.Value); + } + + [TestMethod] + public void Chainor() + { + IReadOnlyList results = Quotes + .Use(CandlePart.Close) + .ToSma(10); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(493, results.Count(x => x.Sma != null)); + } + + [TestMethod] + public override void BadData() + { + IReadOnlyList r = BadQuotes + .Use(CandlePart.Close); + + Assert.AreEqual(502, r.Count); + Assert.AreEqual(0, r.Count(x => x.Value is double.NaN)); + } + + [TestMethod] + public override void NoQuotes() + { + IReadOnlyList r0 = Noquotes + .Use(CandlePart.Close); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = Onequote + .Use(CandlePart.Close); + + Assert.AreEqual(1, r1.Count); + } +} diff --git a/tests/indicators/_common/Use (QuotePart)/QuotePart.StreamHub.Tests.cs b/tests/indicators/_common/Use (QuotePart)/QuotePart.StreamHub.Tests.cs new file mode 100644 index 000000000..9a5bbb284 --- /dev/null +++ b/tests/indicators/_common/Use (QuotePart)/QuotePart.StreamHub.Tests.cs @@ -0,0 +1,142 @@ +namespace StreamHub; + +[TestClass] +public class QuotePartHub : StreamHubTestBase, ITestChainProvider +{ + [TestMethod] + public override void QuoteObserver() + { + CandlePart candlePart = CandlePart.HLC3; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider + for (int i = 0; i < 20; i++) + { + provider.Add(quotesList[i]); + } + + // initialize observer + QuotePartHub observer = provider + .ToQuotePart(candlePart); + + // fetch initial results (early) + IReadOnlyList streamList + = observer.Results; + + // emulate adding quotes to provider + for (int i = 20; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotesList[80]); + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList + .Use(candlePart); + + // assert, should equal series + for (int i = 0; i < length - 1; i++) + { + Quote q = quotesList[i]; + QuotePart s = seriesList[i]; + QuotePart r = streamList[i]; + + r.Timestamp.Should().Be(q.Timestamp); + r.Timestamp.Should().Be(s.Timestamp); + r.Value.Should().Be(s.Value); + r.Should().Be(s); + } + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void ChainProvider() + { + int smaPeriods = 8; + CandlePart candlePart = CandlePart.OHLC4; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + SmaHub observer + = provider + .ToQuotePart(candlePart) + .ToSma(smaPeriods); + + // emulate quote stream + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // final results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList + .Use(candlePart) + .ToSma(smaPeriods); + + // assert, should equal series + for (int i = 0; i < length - 1; i++) + { + Quote q = quotesList[i]; + SmaResult s = seriesList[i]; + SmaResult r = streamList[i]; + + r.Timestamp.Should().Be(q.Timestamp); + r.Timestamp.Should().Be(s.Timestamp); + r.Sma.Should().Be(s.Sma); + r.Should().Be(s); + } + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public override void CustomToString() + { + QuotePartHub hub = new(new QuoteHub(), CandlePart.Close); + hub.ToString().Should().Be("QUOTE-PART(CLOSE)"); + } +} diff --git a/tests/indicators/_common/Use (QuotePart)/QuotePart.Utilities.Tests.cs b/tests/indicators/_common/Use (QuotePart)/QuotePart.Utilities.Tests.cs new file mode 100644 index 000000000..bbf643f4b --- /dev/null +++ b/tests/indicators/_common/Use (QuotePart)/QuotePart.Utilities.Tests.cs @@ -0,0 +1,89 @@ +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 ConvertQuote() + { + // compose basic data + QuotePart o = Quotes[501].ToQuotePart(CandlePart.Open); + QuotePart h = Quotes[501].ToQuotePart(CandlePart.High); + QuotePart l = Quotes[501].ToQuotePart(CandlePart.Low); + QuotePart c = Quotes[501].ToQuotePart(CandlePart.Close); + QuotePart v = Quotes[501].ToQuotePart(CandlePart.Volume); + QuotePart hl = Quotes[501].ToQuotePart(CandlePart.HL2); + QuotePart hlc = Quotes[501].ToQuotePart(CandlePart.HLC3); + QuotePart oc = Quotes[501].ToQuotePart(CandlePart.OC2); + QuotePart ohl = Quotes[501].ToQuotePart(CandlePart.OHL3); + QuotePart ohlc = Quotes[501].ToQuotePart(CandlePart.OHLC4); + + // proper last date + DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", invariantCulture); + Assert.AreEqual(lastDate, c.Timestamp); + + // last values should be correct + Assert.AreEqual(245.28, c.Value); + Assert.AreEqual(244.92, o.Value); + Assert.AreEqual(245.54, h.Value); + Assert.AreEqual(242.87, l.Value); + Assert.AreEqual(245.28, c.Value); + Assert.AreEqual(147031456, v.Value); + Assert.AreEqual(244.205, hl.Value); + Assert.AreEqual(244.5633, hlc.Value.Round(4)); + Assert.AreEqual(245.1, oc.Value); + Assert.AreEqual(244.4433, ohl.Value.Round(4)); + Assert.AreEqual(244.6525, ohlc.Value); + } + + [TestMethod] + public void ConvertList() + { + // compose data + IReadOnlyList o = Quotes.ToQuotePart(CandlePart.Open); + IReadOnlyList h = Quotes.ToQuotePart(CandlePart.High); + IReadOnlyList l = Quotes.ToQuotePart(CandlePart.Low); + IReadOnlyList c = Quotes.ToQuotePart(CandlePart.Close); + IReadOnlyList v = Quotes.ToQuotePart(CandlePart.Volume); + IReadOnlyList hl = Quotes.ToQuotePart(CandlePart.HL2); + IReadOnlyList hlc = Quotes.ToQuotePart(CandlePart.HLC3); + IReadOnlyList oc = Quotes.ToQuotePart(CandlePart.OC2); + IReadOnlyList ohl = Quotes.ToQuotePart(CandlePart.OHL3); + IReadOnlyList ohlc = Quotes.ToQuotePart(CandlePart.OHLC4); + + // proper quantities + Assert.AreEqual(502, c.Count); + + // samples + QuotePart ro = o[501]; + QuotePart rh = h[501]; + QuotePart rl = l[501]; + QuotePart rc = c[501]; + QuotePart rv = v[501]; + QuotePart rhl = hl[501]; + QuotePart rhlc = hlc[501]; + QuotePart roc = oc[501]; + QuotePart rohl = ohl[501]; + QuotePart rohlc = ohlc[501]; + + // proper last date + DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", invariantCulture); + Assert.AreEqual(lastDate, rc.Timestamp); + + // last values should be correct + Assert.AreEqual(245.28, rc.Value); + Assert.AreEqual(244.92, ro.Value); + Assert.AreEqual(245.54, rh.Value); + Assert.AreEqual(242.87, rl.Value); + Assert.AreEqual(245.28, rc.Value); + Assert.AreEqual(147031456, rv.Value); + Assert.AreEqual(244.205, rhl.Value); + Assert.AreEqual(244.5633, rhlc.Value.Round(4)); + Assert.AreEqual(245.1, roc.Value); + Assert.AreEqual(244.4433, rohl.Value.Round(4)); + Assert.AreEqual(244.6525, rohlc.Value); + } +} diff --git a/tests/indicators/a-d/BasicQuote/BasicQuote.Calc.xlsx b/tests/indicators/_common/Use (QuotePart)/Use.Calc.xlsx similarity index 100% rename from tests/indicators/a-d/BasicQuote/BasicQuote.Calc.xlsx rename to tests/indicators/_common/Use (QuotePart)/Use.Calc.xlsx diff --git a/tests/indicators/_common/Data.Aggregate.xlsx b/tests/indicators/_testdata/Data.Aggregate.xlsx similarity index 100% rename from tests/indicators/_common/Data.Aggregate.xlsx rename to tests/indicators/_testdata/Data.Aggregate.xlsx diff --git a/tests/indicators/_common/Data.Candles.xlsx b/tests/indicators/_testdata/Data.Candles.xlsx similarity index 100% rename from tests/indicators/_common/Data.Candles.xlsx rename to tests/indicators/_testdata/Data.Candles.xlsx diff --git a/tests/indicators/_common/Data.Quotes.xlsx b/tests/indicators/_testdata/Data.Quotes.xlsx similarity index 100% rename from tests/indicators/_common/Data.Quotes.xlsx rename to tests/indicators/_testdata/Data.Quotes.xlsx diff --git a/tests/indicators/_testdata/TestData.Getter.cs b/tests/indicators/_testdata/TestData.Getter.cs new file mode 100644 index 000000000..69d763678 --- /dev/null +++ b/tests/indicators/_testdata/TestData.Getter.cs @@ -0,0 +1,141 @@ +namespace Test.Data; + +// TEST QUOTE GETTERs + +internal static class Data +{ + // DEFAULT: S&P 500 ~2 years of daily data + internal static IReadOnlyList GetDefault(int days = 502) + => File.ReadAllLines("_testdata/data/default.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); + + // RANDOM: gaussian brownaian motion + internal static IReadOnlyList GetRandom(int days = 502) + => new RandomGbm(bars: days); + + // sorted by filename + + // BAD DATA + internal static IReadOnlyList GetBad(int days = 502) + => File.ReadAllLines("_testdata/data/bad.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); + + // BITCOIN DATA + internal static IReadOnlyList GetBitcoin(int days = 1246) + => File.ReadAllLines("_testdata/data/bitcoin.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); + + // BTCUSD, 69288 records, 15-minute bars + internal static IReadOnlyList GetBtcUsdNan(int bars = 69288) + => File.ReadAllLines("_testdata/data/btcusd15x69k.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(bars) + .ToSortedList(); + + // COMPARE DATA ~2 years of TSLA data (matches default time) + internal static IReadOnlyList GetCompare(int days = 502) + => File.ReadAllLines("_testdata/data/compare.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); + + // INTRADAY DATA + internal static IReadOnlyList GetIntraday(int days = 1564) + => File.ReadAllLines("_testdata/data/intraday.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); + + // LONGEST DATA ~62 years of S&P 500 daily data (15,821) + internal static IReadOnlyList GetLongest() + => File.ReadAllLines("_testdata/data/longest.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .ToSortedList(); + + // LONGISH DATA ~20 years of S&P 500 daily data + internal static IReadOnlyList GetLongish(int days = 5285) + => File.ReadAllLines("_testdata/data/longish.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); + + // MISMATCH DATA is in incorrect sequence + internal static IReadOnlyList GetMismatch() + => File.ReadAllLines("_testdata/data/mismatch.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .ToList(); // not sorted + + // MSFT, 30 years, daily + internal static IReadOnlyList GetMsft(int days = 8111) + => File.ReadAllLines("_testdata/data/msft.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); + + // PENNY DATA + internal static IReadOnlyList GetPenny() + => File.ReadAllLines("_testdata/data/penny.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .ToSortedList(); + + // SPX, 30 years, daily + internal static IReadOnlyList GetSpx(int days = 8111) + => File.ReadAllLines("_testdata/data/spx.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); + + // TOO BIG DATA + internal static IReadOnlyList GetTooBig(int days = 1246) + => File.ReadAllLines("_testdata/data/toobig.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); + + // MAX SIZE DATA + internal static IReadOnlyList GetMax(int days = 502) + => File.ReadAllLines("_testdata/data/toobig.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); + + // ZEROS (200) + internal static IReadOnlyList GetZeros(int days = 200) + => File.ReadAllLines("_testdata/data/zeros.csv") + .Skip(1) + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .Take(days) + .ToSortedList(); +} diff --git a/tests/indicators/_testdata/TestData.Imports.cs b/tests/indicators/_testdata/TestData.Imports.cs new file mode 100644 index 000000000..222ccd511 --- /dev/null +++ b/tests/indicators/_testdata/TestData.Imports.cs @@ -0,0 +1,46 @@ +using System.Globalization; + +namespace Test.Data; + +// TEST DATA IMPORT UTILITIES + +internal static class Imports +{ + private static readonly CultureInfo EnglishCulture = new("en-US", false); + + // importer / parser + internal static Quote QuoteFromCsv(string csvLine) + { + if (string.IsNullOrEmpty(csvLine)) + { + throw new InvalidDataException("CSV line was empty"); + } + + string[] csv = csvLine.Split(','); + + Quote quote = new( + Timestamp: DateTime.TryParse(csv[0], EnglishCulture, out DateTime d) ? d : default, + Open: csv[1].ToDecimalDefault(), + High: csv[2].ToDecimalDefault(), + Low: csv[3].ToDecimalDefault(), + Close: csv[4].ToDecimalDefault(), + Volume: csv[5].ToDecimalDefault() + ); + + return quote; + } + + internal static decimal ToDecimal(this string value) + => decimal.TryParse(value, out decimal d) ? d + : throw new NotFiniteNumberException( + $"Cannot convert `{value}`, it is not a number."); + + internal static decimal ToDecimalDefault(this string value) + => decimal.TryParse(value, out decimal d) ? d : default; + + internal static decimal? ToDecimalNull(this string value) + => decimal.TryParse(value, out decimal d) ? d : null; + + internal static double? ToDoubleNull(this string value) + => double.TryParse(value, out double d) ? d : null; +} diff --git a/tests/indicators/_testdata/TestData.Random.cs b/tests/indicators/_testdata/TestData.Random.cs new file mode 100644 index 000000000..7a8d1cb5f --- /dev/null +++ b/tests/indicators/_testdata/TestData.Random.cs @@ -0,0 +1,79 @@ +namespace Test.Data; + +/// +/// Geometric Brownian Motion (GMB) is a random simulator of market movement. +/// GBM can be used for testing indicators, validation and Monte Carlo simulations of strategies. +/// +/// +/// Sample usage: +/// +/// RandomGbm data = new(); // generates 1 year (252) list of bars +/// RandomGbm data = new(Bars: 1000); // generates 1,000 bars +/// RandomGbm data = new(Bars: 252, Volatility: 0.05, Drift: 0.0005, Seed: 100.0) +/// +/// Parameters: +/// +/// Bars: number of bars (quotes) requested +/// Volatility: how dymamic/volatile the series should be; default is 1 +/// Drift: incremental drift due to annual interest rate; default is 5% +/// Seed: starting value of the random series; should not be 0. +/// + +internal class RandomGbm : List +{ + private readonly double _volatility; + private readonly double _drift; + private double _seed; + + public RandomGbm( + int bars = 250, + double volatility = 1.0, + double drift = 0.01, + double seed = 1000.0) + { + _seed = seed; + _volatility = volatility * 0.01; + _drift = drift * 0.001; + for (int i = 0; i < bars; i++) + { + DateTime date = DateTime.Today.AddMinutes(i - bars); + Add(date); + } + } + + public void Add(DateTime timestamp) + { + double open = Price(_seed, _volatility * _volatility, _drift); + double close = Price(open, _volatility, _drift); + + double ocMax = Math.Max(open, close); + double high = Price(_seed, _volatility * 0.5, 0); + high = high < ocMax ? (2 * ocMax) - high : high; + + double ocMin = Math.Min(open, close); + double low = Price(_seed, _volatility * 0.5, 0); + low = low > ocMin ? (2 * ocMin) - low : low; + + double volume = Price(_seed * 10, _volatility * 2, drift: 0); + + Quote quote = new( + Timestamp: timestamp, + Open: (decimal)open, + High: (decimal)high, + Low: (decimal)low, + Close: (decimal)close, + Volume: (decimal)volume); + + Add(quote); + _seed = close; + } + + private static double Price(double seed, double volatility, double drift) + { + Random rnd = new((int)DateTime.UtcNow.Ticks); + double u1 = 1.0 - rnd.NextDouble(); + double u2 = 1.0 - rnd.NextDouble(); + double z = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2); + return seed * Math.Exp(drift - (volatility * volatility * 0.5) + (volatility * z)); + } +} diff --git a/tests/indicators/_testdata/TestData.Tests.cs b/tests/indicators/_testdata/TestData.Tests.cs new file mode 100644 index 000000000..7fcf9406f --- /dev/null +++ b/tests/indicators/_testdata/TestData.Tests.cs @@ -0,0 +1,77 @@ +namespace TestOfTests; + +[TestClass] +public class TestData : TestBase +{ + // Test the test data to + // ensure it meets the expected format + + [TestMethod] + public void QuotesIsValid() + { + Quotes.Should().HaveCount(502); + Quotes.Validate(); + } + + [TestMethod] + public void OtherQuotesIsValid() + { + OtherQuotes.Should().HaveCount(502); + OtherQuotes.Validate(); + } + + [TestMethod] + public void BadQuotesIsInvalid() + { + BadQuotes.Should().HaveCount(502); + + // duplicates + Assert.ThrowsException( + () => BadQuotes.Validate()); + } + + [TestMethod] + public void BigQuotesIsValid() + { + BigQuotes.Should().HaveCount(1246); + BigQuotes.Validate(); + } + + [TestMethod] + public void LongishQuotesIsValid() + { + LongishQuotes.Should().HaveCount(5285); + LongishQuotes.Validate(); + } + + [TestMethod] + public void LongestQuotesIsValid() + { + LongestQuotes.Should().HaveCount(15821); + LongestQuotes.Validate(); + } + + [TestMethod] + public void MismatchQuotesIsValid() + { + MismatchQuotes.Should().HaveCount(502); + + // out of sequence + Assert.ThrowsException( + () => MismatchQuotes.Validate()); + } + + [TestMethod] + public void RandomQuotesIsValid() + { + RandomQuotes.Should().HaveCount(1000); + RandomQuotes.Validate(); + } + + [TestMethod] + public void ZeroesQuotesIsValid() + { + ZeroesQuotes.Should().HaveCount(200); + ZeroesQuotes.Validate(); + } +} diff --git a/tests/indicators/_common/data/bad.csv b/tests/indicators/_testdata/data/bad.csv similarity index 100% rename from tests/indicators/_common/data/bad.csv rename to tests/indicators/_testdata/data/bad.csv diff --git a/tests/indicators/_common/data/bitcoin.csv b/tests/indicators/_testdata/data/bitcoin.csv similarity index 100% rename from tests/indicators/_common/data/bitcoin.csv rename to tests/indicators/_testdata/data/bitcoin.csv diff --git a/tests/indicators/_common/data/btcusd15x69k.csv b/tests/indicators/_testdata/data/btcusd15x69k.csv similarity index 100% rename from tests/indicators/_common/data/btcusd15x69k.csv rename to tests/indicators/_testdata/data/btcusd15x69k.csv diff --git a/tests/performance/helpers/data/compare.csv b/tests/indicators/_testdata/data/compare.csv similarity index 100% rename from tests/performance/helpers/data/compare.csv rename to tests/indicators/_testdata/data/compare.csv diff --git a/tests/performance/helpers/data/default.csv b/tests/indicators/_testdata/data/default.csv similarity index 100% rename from tests/performance/helpers/data/default.csv rename to tests/indicators/_testdata/data/default.csv diff --git a/tests/performance/helpers/data/intraday.csv b/tests/indicators/_testdata/data/intraday.csv similarity index 100% rename from tests/performance/helpers/data/intraday.csv rename to tests/indicators/_testdata/data/intraday.csv diff --git a/tests/performance/helpers/data/longest.csv b/tests/indicators/_testdata/data/longest.csv similarity index 99% rename from tests/performance/helpers/data/longest.csv rename to tests/indicators/_testdata/data/longest.csv index b753dcc33..5dd865443 100644 --- a/tests/performance/helpers/data/longest.csv +++ b/tests/indicators/_testdata/data/longest.csv @@ -1,4 +1,4 @@ -Date,Open,High,Low,Close,Volume +Timestamp,Open,High,Low,Close,Volume 2012-11-15,1355.41,1360.62,1348.05,1353.33,3928870000 2012-11-14,1374.64,1380.13,1352.5,1355.49,4109510000 2012-11-13,1380.03,1388.81,1371.39,1374.53,3455550000 diff --git a/tests/performance/helpers/data/longish.csv b/tests/indicators/_testdata/data/longish.csv similarity index 100% rename from tests/performance/helpers/data/longish.csv rename to tests/indicators/_testdata/data/longish.csv diff --git a/tests/indicators/_common/data/max.csv b/tests/indicators/_testdata/data/max.csv similarity index 100% rename from tests/indicators/_common/data/max.csv rename to tests/indicators/_testdata/data/max.csv diff --git a/tests/indicators/_common/data/mismatch.csv b/tests/indicators/_testdata/data/mismatch.csv similarity index 99% rename from tests/indicators/_common/data/mismatch.csv rename to tests/indicators/_testdata/data/mismatch.csv index 564ae3e48..cbcd48f8d 100644 --- a/tests/indicators/_common/data/mismatch.csv +++ b/tests/indicators/_testdata/data/mismatch.csv @@ -1,4 +1,4 @@ -Date,Open,High,Low,Close,Volume +Timestamp,Open,High,Low,Close,Volume 1/3/2017,212.61,213.35,211.52,212.8,96708880 1/4/2017,213.16,214.22,213.15,214.06,83348752 1/5/2017,213.77,214.06,213.02,213.89,82961968 diff --git a/tests/indicators/_common/data/msft.csv b/tests/indicators/_testdata/data/msft.csv similarity index 100% rename from tests/indicators/_common/data/msft.csv rename to tests/indicators/_testdata/data/msft.csv diff --git a/tests/indicators/_common/data/penny.csv b/tests/indicators/_testdata/data/penny.csv similarity index 100% rename from tests/indicators/_common/data/penny.csv rename to tests/indicators/_testdata/data/penny.csv diff --git a/tests/indicators/_common/data/spx.csv b/tests/indicators/_testdata/data/spx.csv similarity index 100% rename from tests/indicators/_common/data/spx.csv rename to tests/indicators/_testdata/data/spx.csv diff --git a/tests/indicators/_common/data/toobig.csv b/tests/indicators/_testdata/data/toobig.csv similarity index 100% rename from tests/indicators/_common/data/toobig.csv rename to tests/indicators/_testdata/data/toobig.csv diff --git a/tests/indicators/_common/data/zeros.csv b/tests/indicators/_testdata/data/zeros.csv similarity index 100% rename from tests/indicators/_common/data/zeros.csv rename to tests/indicators/_testdata/data/zeros.csv diff --git a/tests/indicators/a-d/Adl/Adl.StaticSeries.Tests.cs b/tests/indicators/a-d/Adl/Adl.StaticSeries.Tests.cs new file mode 100644 index 000000000..1c98e391d --- /dev/null +++ b/tests/indicators/a-d/Adl/Adl.StaticSeries.Tests.cs @@ -0,0 +1,82 @@ +namespace StaticSeries; + +[TestClass] +public class Adl : StaticSeriesTestBase +{ + [TestMethod] + public override void Standard() + { + IReadOnlyList results = Quotes + .ToAdl(); + + // proper quantities + Assert.AreEqual(502, results.Count); + + // sample values + AdlResult r1 = results[249]; + Assert.AreEqual(0.7778, r1.MoneyFlowMultiplier.Round(4)); + Assert.AreEqual(36433792.89, r1.MoneyFlowVolume.Round(2)); + Assert.AreEqual(3266400865.74, r1.Adl.Round(2)); + + AdlResult r2 = results[501]; + Assert.AreEqual(0.8052, r2.MoneyFlowMultiplier.Round(4)); + Assert.AreEqual(118396116.25, r2.MoneyFlowVolume.Round(2)); + Assert.AreEqual(3439986548.42, r2.Adl.Round(2)); + } + + [TestMethod] + public void Chainor() + { + IReadOnlyList results = Quotes + .ToAdl() + .ToSma(10); + + // assertions + + // proper quantities + Assert.AreEqual(502, results.Count); + Assert.AreEqual(493, results.Count(x => x.Sma != null)); + } + + [TestMethod] + public override void BadData() + { + IReadOnlyList r = BadQuotes + .ToAdl(); + + Assert.AreEqual(502, r.Count); + Assert.AreEqual(0, r.Count(x => double.IsNaN(x.Adl))); + } + + [TestMethod] + public void BigData() + { + IReadOnlyList r = BigQuotes + .ToAdl(); + + Assert.AreEqual(1246, r.Count); + } + + [TestMethod] + public void RandomData() + { + IReadOnlyList r = RandomQuotes + .ToAdl(); + + Assert.AreEqual(1000, r.Count); + } + + [TestMethod] + public override void NoQuotes() + { + IReadOnlyList r0 = Noquotes + .ToAdl(); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = Onequote + .ToAdl(); + + Assert.AreEqual(1, r1.Count); + } +} diff --git a/tests/indicators/a-d/Adl/Adl.StreamHub.Tests.cs b/tests/indicators/a-d/Adl/Adl.StreamHub.Tests.cs new file mode 100644 index 000000000..0b2d3e66b --- /dev/null +++ b/tests/indicators/a-d/Adl/Adl.StreamHub.Tests.cs @@ -0,0 +1,128 @@ +namespace StreamHub; + +[TestClass] +public class AdlHub : StreamHubTestBase, ITestChainProvider +{ + [TestMethod] + public override void QuoteObserver() + { + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider + for (int i = 0; i < 20; i++) + { + provider.Add(quotesList[i]); + } + + // initialize observer + StreamHub observer = provider + .ToAdl(); + + // fetch initial results (early) + IReadOnlyList streamList + = observer.Results; + + // emulate adding quotes to provider + for (int i = 20; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotesList[80]); + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // time-series, for comparison + IReadOnlyList seriesList = quotesList + .ToAdl(); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void ChainProvider() + { + const int smaPeriods = 8; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + AdlHub adlHub = provider + .ToAdl(); + + SmaHub observer = adlHub + .ToSma(smaPeriods); + + // emulate quote stream + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // final results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList = quotesList + .ToAdl() + .ToSma(smaPeriods); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public override void CustomToString() + { + QuoteHub provider = new(); + + AdlHub hub = new(provider); + hub.ToString().Should().Be("ADL"); + + provider.Add(Quotes[0]); + provider.Add(Quotes[1]); + + string s = $"ADL({Quotes[0].Timestamp:d})"; + hub.ToString().Should().Be(s); + } +} diff --git a/tests/indicators/a-d/Adl/Adl.Tests.cs b/tests/indicators/a-d/Adl/Adl.Tests.cs deleted file mode 100644 index 3130641c5..000000000 --- a/tests/indicators/a-d/Adl/Adl.Tests.cs +++ /dev/null @@ -1,117 +0,0 @@ -namespace Tests.Indicators; - -[TestClass] -public class AdlTests : TestBase -{ - [TestMethod] - public void Standard() - { - List results = quotes - .GetAdl() - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(502, results.Count(x => x.AdlSma == null)); - - // sample values - AdlResult r1 = results[249]; - Assert.AreEqual(0.7778, r1.MoneyFlowMultiplier.Round(4)); - Assert.AreEqual(36433792.89, r1.MoneyFlowVolume.Round(2)); - Assert.AreEqual(3266400865.74, r1.Adl.Round(2)); - Assert.AreEqual(null, r1.AdlSma); - - AdlResult r2 = results[501]; - Assert.AreEqual(0.8052, r2.MoneyFlowMultiplier.Round(4)); - Assert.AreEqual(118396116.25, r2.MoneyFlowVolume.Round(2)); - Assert.AreEqual(3439986548.42, r2.Adl.Round(2)); - Assert.AreEqual(null, r2.AdlSma); - } - - [TestMethod] - public void WithSma() - { - List results = quotes - .GetAdl(20) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(483, results.Count(x => x.AdlSma != null)); - - // sample value - AdlResult r = results[501]; - Assert.AreEqual(0.8052, r.MoneyFlowMultiplier.Round(4)); - Assert.AreEqual(118396116.25, r.MoneyFlowVolume.Round(2)); - Assert.AreEqual(3439986548.42, r.Adl.Round(2)); - Assert.AreEqual(3595352721.16, r.AdlSma.Round(2)); - } - - [TestMethod] - public void Chainor() - { - List results = quotes - .GetAdl() - .GetSma(10) - .ToList(); - - // assertions - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(493, results.Count(x => x.Sma != null)); - } - - [TestMethod] - public void BadData() - { - List r = badQuotes - .GetAdl() - .ToList(); - - Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => double.IsNaN(x.Adl))); - } - - [TestMethod] - public void BigData() - { - List r = bigQuotes - .GetAdl() - .ToList(); - - Assert.AreEqual(1246, r.Count); - } - - [TestMethod] - public void RandomData() - { - List r = randomQuotes - .GetAdl() - .ToList(); - - Assert.AreEqual(1000, r.Count); - } - - [TestMethod] - public void NoQuotes() - { - List r0 = noquotes - .GetAdl() - .ToList(); - - Assert.AreEqual(0, r0.Count); - - List r1 = onequote - .GetAdl() - .ToList(); - - Assert.AreEqual(1, r1.Count); - } - - // bad SMA period - [TestMethod] - public void Exceptions() - => Assert.ThrowsException(() - => quotes.GetAdl(0)); -} diff --git a/tests/indicators/a-d/Adx/Adx.Tests.cs b/tests/indicators/a-d/Adx/Adx.StaticSeries.Tests.cs similarity index 62% rename from tests/indicators/a-d/Adx/Adx.Tests.cs rename to tests/indicators/a-d/Adx/Adx.StaticSeries.Tests.cs index 81a4c4805..ca0a5c213 100644 --- a/tests/indicators/a-d/Adx/Adx.Tests.cs +++ b/tests/indicators/a-d/Adx/Adx.StaticSeries.Tests.cs @@ -1,14 +1,12 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class AdxTests : TestBase +public class Adx : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetAdx(14) - .ToList(); + IReadOnlyList results = Quotes.ToAdx(); // proper quantities Assert.AreEqual(502, results.Count); @@ -47,48 +45,39 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetAdx(14) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToAdx() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(466, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetAdx(20) - .ToList(); + IReadOnlyList r = BadQuotes.ToAdx(20); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Adx is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Adx is double.NaN)); } [TestMethod] public void BigData() { - List r = bigQuotes - .GetAdx(200) - .ToList(); + IReadOnlyList r = BigQuotes.ToAdx(200); Assert.AreEqual(1246, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetAdx(5) - .ToList(); + IReadOnlyList r0 = Noquotes.ToAdx(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetAdx(5) - .ToList(); + IReadOnlyList r1 = Onequote.ToAdx(5); Assert.AreEqual(1, r1.Count); } @@ -96,37 +85,38 @@ public void NoQuotes() [TestMethod] public void Issue859() { - IOrderedEnumerable test859 = File.ReadAllLines("a-d/Adx/issue859quotes.csv") + List test859 = File.ReadAllLines("a-d/Adx/issue859quotes.csv") .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date); + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp) + .ToList(); - List r = test859.GetAdx(14).ToList(); + IReadOnlyList r = test859.ToAdx(); - Assert.AreEqual(0, r.Count(x => x.Adx is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Adx is double.NaN)); Assert.AreEqual(595, r.Count); } [TestMethod] public void Zeroes() { - List r = zeroesQuotes.GetAdx(14).ToList(); + IReadOnlyList r = ZeroesQuotes.ToAdx(); - Assert.AreEqual(0, r.Count(x => x.Adx is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Adx is double.NaN)); Assert.AreEqual(200, r.Count); } [TestMethod] public void Removed() { - List r = quotes.GetAdx(14) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToAdx() + .RemoveWarmupPeriods(); // assertions - Assert.AreEqual(502 - ((2 * 14) + 100), r.Count); + Assert.AreEqual(502 - ((2 * 14) + 100), results.Count); - AdxResult last = r.LastOrDefault(); + AdxResult last = results[^1]; Assert.AreEqual(17.7565, last.Pdi.Round(4)); Assert.AreEqual(31.1510, last.Mdi.Round(4)); Assert.AreEqual(34.2987, last.Adx.Round(4)); @@ -137,5 +127,5 @@ public void Exceptions() => // bad lookback period Assert.ThrowsException(() => - quotes.GetAdx(1)); + Quotes.ToAdx(1)); } diff --git a/tests/indicators/a-d/Adx/issue859quotes.csv b/tests/indicators/a-d/Adx/issue859quotes.csv index b5fe10488..e6fe253a4 100644 --- a/tests/indicators/a-d/Adx/issue859quotes.csv +++ b/tests/indicators/a-d/Adx/issue859quotes.csv @@ -1,4 +1,4 @@ -Date,Open,High,Low,Close,Volume +Timestamp,Open,High,Low,Close,Volume 2022-07-25T09:15:00+05:30,498.075,502.1,494.05,498.075,50 2022-07-25T09:18:00+05:30,498.075,498.075,494.05,494.05,0 2022-07-25T09:21:00+05:30,496.0625,496.0625,494.05,494.05,0 diff --git a/tests/indicators/a-d/Alligator/Alligator.Tests.cs b/tests/indicators/a-d/Alligator/Alligator.StaticSeries.Tests.cs similarity index 55% rename from tests/indicators/a-d/Alligator/Alligator.Tests.cs rename to tests/indicators/a-d/Alligator/Alligator.StaticSeries.Tests.cs index f74aaec15..e8c682b40 100644 --- a/tests/indicators/a-d/Alligator/Alligator.Tests.cs +++ b/tests/indicators/a-d/Alligator/Alligator.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class AlligatorTests : TestBase +public class Alligator : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetAlligator() - .ToList(); + IReadOnlyList results = Quotes + .ToAlligator(); // proper quantities Assert.AreEqual(502, results.Count); @@ -43,79 +42,37 @@ public void Standard() Assert.AreEqual(244.29591, results[501].Lips.Round(5)); } - [TestMethod] - public void UseTuple() - { - List results = quotes - .Use(CandlePart.HL2) - .GetAlligator() - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(482, results.Count(x => x.Jaw != null)); - - AlligatorResult last = results.LastOrDefault(); - Assert.AreEqual(244.29591, last.Lips.Round(5)); - } - - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetAlligator() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Lips is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetAlligator() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToAlligator(); Assert.AreEqual(502, results.Count); Assert.AreEqual(481, results.Count(x => x.Jaw != null)); } [TestMethod] - public void Sync() - { - List results = quotes - .GetSma(3) - .GetAlligator() - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(480, results.Count(x => x.Jaw != null)); - } - - [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetAlligator(3, 3, 2, 1, 1, 1) - .ToList(); + IReadOnlyList r = BadQuotes + .ToAlligator(3, 3, 2, 1, 1, 1); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Jaw is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Jaw is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetAlligator() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToAlligator(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetAlligator() - .ToList(); + IReadOnlyList r1 = Onequote + .ToAlligator(); Assert.AreEqual(1, r1.Count); } @@ -123,14 +80,13 @@ public void NoQuotes() [TestMethod] public void Condense() { - List r = quotes - .GetAlligator() - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToAlligator() + .Condense(); - Assert.AreEqual(495, r.Count); + Assert.AreEqual(495, results.Count); - AlligatorResult last = r.LastOrDefault(); + AlligatorResult last = results[^1]; Assert.AreEqual(260.98953, last.Jaw.Round(5)); Assert.AreEqual(253.53576, last.Teeth.Round(5)); Assert.AreEqual(244.29591, last.Lips.Round(5)); @@ -139,52 +95,62 @@ public void Condense() [TestMethod] public void Removed() { - List r = quotes - .GetAlligator(13, 8) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToAlligator() + .RemoveWarmupPeriods(); - Assert.AreEqual(502 - 21 - 250, r.Count); + Assert.AreEqual(502 - 21 - 250, results.Count); - AlligatorResult last = r.LastOrDefault(); + AlligatorResult last = results[^1]; Assert.AreEqual(260.98953, last.Jaw.Round(5)); Assert.AreEqual(253.53576, last.Teeth.Round(5)); Assert.AreEqual(244.29591, last.Lips.Round(5)); } + [TestMethod] + public void Equality() + { + AlligatorResult r1 = new(EvalDate, 1d, null, null); + AlligatorResult r2 = new(EvalDate, 1d, null, null); + AlligatorResult r3 = new(EvalDate, 2d, null, null); // abberent + + Assert.IsTrue(Equals(r1, r2)); + Assert.IsFalse(Equals(r1, r3)); + } + [TestMethod] public void Exceptions() { // bad jaw lookback periods Assert.ThrowsException(() => - quotes.GetAlligator(13, 8, 13, 5, 5, 3)); + Quotes.ToAlligator(13, 8, 13)); // bad teeth lookback periods Assert.ThrowsException(() => - quotes.GetAlligator(13, 8, 8, 5, 8, 3)); + Quotes.ToAlligator(13, 8, 8, 5, 8)); // bad lips lookback periods Assert.ThrowsException(() => - quotes.GetAlligator(13, 8, 8, 5, 0, 3)); + Quotes.ToAlligator(13, 8, 8, 5, 0)); // bad jaw offset periods Assert.ThrowsException(() => - quotes.GetAlligator(13, 0, 8, 5, 5, 3)); + Quotes.ToAlligator(13, 0)); // bad teeth offset periods Assert.ThrowsException(() => - quotes.GetAlligator(13, 8, 8, 0, 5, 3)); + Quotes.ToAlligator(13, 8, 8, 0)); // bad lips offset periods Assert.ThrowsException(() => - quotes.GetAlligator(13, 8, 8, 5, 5, 0)); + Quotes.ToAlligator(13, 8, 8, 5, 5, 0)); // bad jaw + offset periods Assert.ThrowsException(() => - quotes.GetAlligator(13, 8, 12, 11, 5, 3)); + Quotes.ToAlligator(13, 8, 12, 11)); // bad teeth + offset periods Assert.ThrowsException(() => - quotes.GetAlligator(13, 8, 8, 5, 7, 7)); + Quotes.ToAlligator(13, 8, 8, 5, 7, 7)); } } diff --git a/tests/indicators/a-d/Alligator/Alligator.StreamHub.Tests.cs b/tests/indicators/a-d/Alligator/Alligator.StreamHub.Tests.cs new file mode 100644 index 000000000..f6db7e617 --- /dev/null +++ b/tests/indicators/a-d/Alligator/Alligator.StreamHub.Tests.cs @@ -0,0 +1,135 @@ +namespace StreamHub; + +[TestClass] +public class AlligatorHub : StreamHubTestBase, ITestChainObserver +{ + [TestMethod] + public override void QuoteObserver() + { + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider + for (int i = 0; i < 20; i++) + { + provider.Add(quotesList[i]); + } + + // initialize observer + AlligatorHub observer = provider + .ToAlligator(); + + // fetch initial results (early) + IReadOnlyList streamList + = observer.Results; + + // emulate adding quotes to provider + for (int i = 20; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotesList[80]); + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList + .ToAlligator(); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void ChainObserver() + { + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + AlligatorHub observer = provider + .ToSma(10) + .ToAlligator(); + + // emulate adding quotes out of order + // note: this works when graceful order + for (int i = 0; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotesList[80]); + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // final results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList + .ToSma(10) + .ToAlligator(); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public override void CustomToString() + { + AlligatorHub hub = new(new QuoteHub(), 13, 8, 7, 5, 4, 3); + hub.ToString().Should().Be("ALLIGATOR(13,8,7,5,4,3)"); + } +} diff --git a/tests/indicators/a-d/Alma/Alma.Tests.cs b/tests/indicators/a-d/Alma/Alma.StaticSeries.Tests.cs similarity index 53% rename from tests/indicators/a-d/Alma/Alma.Tests.cs rename to tests/indicators/a-d/Alma/Alma.StaticSeries.Tests.cs index e855c5b6f..d9ccb1884 100644 --- a/tests/indicators/a-d/Alma/Alma.Tests.cs +++ b/tests/indicators/a-d/Alma/Alma.StaticSeries.Tests.cs @@ -1,18 +1,17 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class Alma : TestBase +public class Alma : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int lookbackPeriods = 10; double offset = 0.85; double sigma = 6; - List results = quotes - .GetAlma(lookbackPeriods, offset, sigma) - .ToList(); + IReadOnlyList results = Quotes + .ToAlma(lookbackPeriods, offset, sigma); // proper quantities Assert.AreEqual(502, results.Count); @@ -39,38 +38,25 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetAlma(10, 0.85, 6) - .ToList(); + .ToAlma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(493, results.Count(x => x.Alma != null)); - AlmaResult last = results.LastOrDefault(); + AlmaResult last = results[^1]; Assert.AreEqual(242.1871, last.Alma.Round(4)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetAlma() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Alma is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetAlma(10, 0.85, 6) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToAlma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(492, results.Count(x => x.Alma != null)); @@ -83,10 +69,9 @@ public void Chainor() double offset = 0.85; double sigma = 6; - List results = quotes - .GetAlma(lookbackPeriods, offset, sigma) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToAlma(lookbackPeriods, offset, sigma) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(484, results.Count(x => x.Sma != null)); @@ -95,38 +80,34 @@ public void Chainor() [TestMethod] public void NaN() { - List r1 = TestData.GetBtcUsdNan().GetAlma(9, 0.85, 6).ToList(); + IReadOnlyList r1 + = Data.GetBtcUsdNan().ToAlma(); - Assert.AreEqual(0, r1.Count(x => x.Alma is double and double.NaN)); + Assert.AreEqual(0, r1.Count(x => x.Alma is double.NaN)); - List r2 = TestData.GetBtcUsdNan().GetAlma(20, 0.85, 6).ToList(); + IReadOnlyList r2 + = Data.GetBtcUsdNan().ToAlma(20); - Assert.AreEqual(0, r2.Count(x => x.Alma is double and double.NaN)); + Assert.AreEqual(0, r2.Count(x => x.Alma is double.NaN)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetAlma(14, 0.5, 3) - .ToList(); + IReadOnlyList r = BadQuotes.ToAlma(14, 0.5, 3); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Alma is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Alma is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetAlma() - .ToList(); + IReadOnlyList r0 = Noquotes.ToAlma(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetAlma() - .ToList(); + IReadOnlyList r1 = Onequote.ToAlma(); Assert.AreEqual(1, r1.Count); } @@ -134,15 +115,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetAlma(10, 0.85, 6) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToAlma(10) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 9, results.Count); - AlmaResult last = results.LastOrDefault(); + AlmaResult last = results[^1]; Assert.AreEqual(242.1871, last.Alma.Round(4)); } @@ -151,14 +131,14 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetAlma(0, 1, 5)); + Quotes.ToAlma(0, 1, 5)); // bad offset Assert.ThrowsException(() => - quotes.GetAlma(15, 1.1, 3)); + Quotes.ToAlma(15, 1.1, 3)); // bad sigma Assert.ThrowsException(() => - quotes.GetAlma(10, 0.5, 0)); + Quotes.ToAlma(10, 0.5, 0)); } } diff --git a/tests/indicators/a-d/Aroon/Aroon.Tests.cs b/tests/indicators/a-d/Aroon/Aroon.StaticSeries.Tests.cs similarity index 68% rename from tests/indicators/a-d/Aroon/Aroon.Tests.cs rename to tests/indicators/a-d/Aroon/Aroon.StaticSeries.Tests.cs index 50fe8f8e5..db6dcba18 100644 --- a/tests/indicators/a-d/Aroon/Aroon.Tests.cs +++ b/tests/indicators/a-d/Aroon/Aroon.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class Aroon : TestBase +public class Aroon : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetAroon(25) - .ToList(); + IReadOnlyList results = Quotes + .ToAroon(); // proper quantities Assert.AreEqual(502, results.Count); @@ -46,38 +45,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetAroon(25) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToAroon() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(468, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetAroon(20) - .ToList(); + IReadOnlyList r = BadQuotes + .ToAroon(20); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Oscillator is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Oscillator is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetAroon() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToAroon(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetAroon() - .ToList(); + IReadOnlyList r1 = Onequote + .ToAroon(); Assert.AreEqual(1, r1.Count); } @@ -85,15 +80,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetAroon(25) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToAroon() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 25, results.Count); - AroonResult last = results.LastOrDefault(); + AroonResult last = results[^1]; Assert.AreEqual(28, last.AroonUp); Assert.AreEqual(88, last.AroonDown); Assert.AreEqual(-60, last.Oscillator); @@ -103,5 +97,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetAroon(0)); + => Quotes.ToAroon(0)); } diff --git a/tests/indicators/a-d/Atr/Atr.Tests.cs b/tests/indicators/a-d/Atr/Atr.StaticSeries.Tests.cs similarity index 59% rename from tests/indicators/a-d/Atr/Atr.Tests.cs rename to tests/indicators/a-d/Atr/Atr.StaticSeries.Tests.cs index 23267d3ea..0682f0fc3 100644 --- a/tests/indicators/a-d/Atr/Atr.Tests.cs +++ b/tests/indicators/a-d/Atr/Atr.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class AtrTests : TestBase +public class Atr : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetAtr(14) - .ToList(); + IReadOnlyList results = Quotes + .ToAtr(); // proper quantities Assert.AreEqual(502, results.Count); @@ -41,41 +40,58 @@ public void Standard() Assert.AreEqual(2.5072, r501.Atrp.Round(4)); } + [TestMethod] + public void MatchingTrueRange() + { + IReadOnlyList resultsAtr = Quotes + .ToAtr(14); + + IReadOnlyList resultsTr = Quotes + .ToTr(); + + for (int i = 0; i < Quotes.Count; i++) + { + Quote q = Quotes[i]; + TrResult t = resultsTr[i]; + AtrResult r = resultsAtr[i]; + + r.Timestamp.Should().Be(q.Timestamp); + r.Timestamp.Should().Be(t.Timestamp); + r.Tr.Should().Be(t.Tr); + } + } + [TestMethod] public void Chainor() { - List results = quotes - .GetAtr(10) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToAtr(10) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(502 - 19, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetAtr(20) - .ToList(); + IReadOnlyList r = BadQuotes + .ToAtr(20); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Atr is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Atr is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetAtr() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToAtr(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetAtr() - .ToList(); + IReadOnlyList r1 = Onequote + .ToAtr(); Assert.AreEqual(1, r1.Count); } @@ -83,15 +99,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetAtr(14) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToAtr() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 14, results.Count); - AtrResult last = results.LastOrDefault(); + AtrResult last = results[^1]; Assert.AreEqual(2.67, last.Tr.Round(8)); Assert.AreEqual(6.1497, last.Atr.Round(4)); Assert.AreEqual(2.5072, last.Atrp.Round(4)); @@ -101,5 +116,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() => - quotes.GetAtr(1)); + Quotes.ToAtr(1)); } diff --git a/tests/indicators/a-d/Atr/Atr.StreamHub.Tests.cs b/tests/indicators/a-d/Atr/Atr.StreamHub.Tests.cs new file mode 100644 index 000000000..0f3e5e419 --- /dev/null +++ b/tests/indicators/a-d/Atr/Atr.StreamHub.Tests.cs @@ -0,0 +1,120 @@ +namespace StreamHub; + +[TestClass] +public class AtrHub : StreamHubTestBase, ITestChainProvider +{ + [TestMethod] + public override void QuoteObserver() + { + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider + for (int i = 0; i < 20; i++) + { + provider.Add(quotesList[i]); + } + + // initialize observer + StreamHub observer = provider + .ToAtr(14); + + // fetch initial results (early) + IReadOnlyList streamList + = observer.Results; + + // emulate adding quotes to provider + for (int i = 20; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotesList[80]); + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // time-series, for comparison + IReadOnlyList seriesList = quotesList + .ToAtr(14); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void ChainProvider() + { + int smaPeriods = 8; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + IChainProvider adlHub = provider + .ToAtr(14); + + SmaHub observer = adlHub + .ToSma(smaPeriods); + + // emulate quote stream + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // final results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList = quotesList + .ToAtr(14) + .ToSma(smaPeriods); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public override void CustomToString() + { + AtrHub hub = new(new QuoteHub(), 20); + hub.ToString().Should().Be("ATR(20)"); + } +} diff --git a/tests/indicators/a-d/AtrStop/AtrStop.Tests.cs b/tests/indicators/a-d/AtrStop/AtrStop.StaticSeries.Tests.cs similarity index 63% rename from tests/indicators/a-d/AtrStop/AtrStop.Tests.cs rename to tests/indicators/a-d/AtrStop/AtrStop.StaticSeries.Tests.cs index 7d53fe6c5..091b89486 100644 --- a/tests/indicators/a-d/AtrStop/AtrStop.Tests.cs +++ b/tests/indicators/a-d/AtrStop/AtrStop.StaticSeries.Tests.cs @@ -1,17 +1,16 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class AtrStopTests : TestBase +public class AtrStop : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int lookbackPeriods = 21; double multiplier = 3; - List results = quotes - .GetAtrStop(lookbackPeriods, multiplier, EndType.Close) - .ToList(); + IReadOnlyList results = Quotes + .ToAtrStop(lookbackPeriods, multiplier); // proper quantities Assert.AreEqual(502, results.Count); @@ -24,27 +23,27 @@ public void Standard() Assert.AreEqual(null, r20.SellStop); AtrStopResult r21 = results[21]; - Assert.AreEqual(211.13m, r21.AtrStop.Round(4)); + Assert.AreEqual(211.13, r21.AtrStop.Round(4)); Assert.AreEqual(null, r21.BuyStop); Assert.AreEqual(r21.AtrStop, r21.SellStop); AtrStopResult r151 = results[151]; - Assert.AreEqual(232.7861m, r151.AtrStop.Round(4)); + Assert.AreEqual(232.7861, r151.AtrStop.Round(4)); Assert.AreEqual(null, r151.BuyStop); Assert.AreEqual(r151.AtrStop, r151.SellStop); AtrStopResult r152 = results[152]; - Assert.AreEqual(236.3913m, r152.AtrStop.Round(4)); + Assert.AreEqual(236.3913, r152.AtrStop.Round(4)); Assert.AreEqual(r152.AtrStop, r152.BuyStop); Assert.AreEqual(null, r152.SellStop); AtrStopResult r249 = results[249]; - Assert.AreEqual(253.8863m, r249.AtrStop.Round(4)); + Assert.AreEqual(253.8863, r249.AtrStop.Round(4)); Assert.AreEqual(null, r249.BuyStop); Assert.AreEqual(r249.AtrStop, r249.SellStop); AtrStopResult r501 = results[501]; - Assert.AreEqual(246.3232m, r501.AtrStop.Round(4)); + Assert.AreEqual(246.3232, r501.AtrStop.Round(4)); Assert.AreEqual(r501.AtrStop, r501.BuyStop); Assert.AreEqual(null, r501.SellStop); } @@ -55,9 +54,8 @@ public void HighLow() int lookbackPeriods = 21; double multiplier = 3; - List results = quotes - .GetAtrStop(lookbackPeriods, multiplier, EndType.HighLow) - .ToList(); + IReadOnlyList results = Quotes + .ToAtrStop(lookbackPeriods, multiplier, EndType.HighLow); // proper quantities Assert.AreEqual(502, results.Count); @@ -70,53 +68,50 @@ public void HighLow() Assert.AreEqual(null, r20.SellStop); AtrStopResult r21 = results[21]; - Assert.AreEqual(210.23m, r21.AtrStop.Round(4)); + Assert.AreEqual(210.23, r21.AtrStop.Round(4)); Assert.AreEqual(null, r21.BuyStop); Assert.AreEqual(r21.AtrStop, r21.SellStop); AtrStopResult r69 = results[69]; - Assert.AreEqual(221.0594m, r69.AtrStop.Round(4)); + Assert.AreEqual(221.0594, r69.AtrStop.Round(4)); Assert.AreEqual(null, r69.BuyStop); Assert.AreEqual(r69.AtrStop, r69.SellStop); AtrStopResult r70 = results[70]; - Assert.AreEqual(226.4624m, r70.AtrStop.Round(4)); + Assert.AreEqual(226.4624, r70.AtrStop.Round(4)); Assert.AreEqual(r70.AtrStop, r70.BuyStop); Assert.AreEqual(null, r70.SellStop); AtrStopResult r249 = results[249]; - Assert.AreEqual(253.4863m, r249.AtrStop.Round(4)); + Assert.AreEqual(253.4863, r249.AtrStop.Round(4)); Assert.AreEqual(null, r249.BuyStop); Assert.AreEqual(r249.AtrStop, r249.SellStop); AtrStopResult r501 = results[501]; - Assert.AreEqual(252.6932m, r501.AtrStop.Round(4)); + Assert.AreEqual(252.6932, r501.AtrStop.Round(4)); Assert.AreEqual(r501.AtrStop, r501.BuyStop); Assert.AreEqual(null, r501.SellStop); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetAtrStop(7) - .ToList(); + IReadOnlyList r = BadQuotes + .ToAtrStop(7); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetAtrStop() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToAtrStop(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetAtrStop() - .ToList(); + IReadOnlyList r1 = Onequote + .ToAtrStop(); Assert.AreEqual(1, r1.Count); } @@ -127,16 +122,15 @@ public void Condense() int lookbackPeriods = 21; double multiplier = 3; - List results = - quotes.GetAtrStop(lookbackPeriods, multiplier) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToAtrStop(lookbackPeriods, multiplier) + .Condense(); // assertions Assert.AreEqual(481, results.Count); - AtrStopResult last = results.LastOrDefault(); - Assert.AreEqual(246.3232m, last.AtrStop.Round(4)); + AtrStopResult last = results[^1]; + Assert.AreEqual(246.3232, last.AtrStop.Round(4)); Assert.AreEqual(last.AtrStop, last.BuyStop); Assert.AreEqual(null, last.SellStop); } @@ -147,16 +141,15 @@ public void Removed() int lookbackPeriods = 21; double multiplier = 3; - List results = - quotes.GetAtrStop(lookbackPeriods, multiplier) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToAtrStop(lookbackPeriods, multiplier) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(481, results.Count); - AtrStopResult last = results.LastOrDefault(); - Assert.AreEqual(246.3232m, last.AtrStop.Round(4)); + AtrStopResult last = results[^1]; + Assert.AreEqual(246.3232, last.AtrStop.Round(4)); Assert.AreEqual(last.AtrStop, last.BuyStop); Assert.AreEqual(null, last.SellStop); } @@ -166,10 +159,10 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() - => quotes.GetAtrStop(1)); + => Quotes.ToAtrStop(1)); // bad multiplier Assert.ThrowsException(() - => quotes.GetAtrStop(7, 0)); + => Quotes.ToAtrStop(7, 0)); } } diff --git a/tests/indicators/a-d/AtrStop/AtrStop.StreamHub.Tests.cs b/tests/indicators/a-d/AtrStop/AtrStop.StreamHub.Tests.cs new file mode 100644 index 000000000..539fc18ec --- /dev/null +++ b/tests/indicators/a-d/AtrStop/AtrStop.StreamHub.Tests.cs @@ -0,0 +1,107 @@ +namespace StreamHub; + +[TestClass] +public class AtrStop : StreamHubTestBase +{ + [TestMethod] + public override void QuoteObserver() + { + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider (batch) + provider.Add(quotesList.Take(20)); + + // initialize observer + AtrStopHub observer = provider + .ToAtrStop(); + + observer.Results.Should().HaveCount(20); + + // fetch initial results (early) + IReadOnlyList streamList + = observer.Results; + + // emulate adding quotes to provider + for (int i = 20; i < length; i++) + { + // skip one (add later) + if (i is 30 or 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrivals + provider.Insert(quotesList[30]); // rebuilds complete series + provider.Insert(quotesList[80]); // rebuilds from last reversal + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList + .ToAtrStop(); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void QuoteObserverHighLow() + { + // simple test, just to check High/Low variant + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + AtrStopHub observer = provider + .ToAtrStop(endType: EndType.HighLow); + + // add quotes to provider + provider.Add(Quotes); + + // stream results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList + = Quotes + .ToAtrStop(endType: EndType.HighLow); + + // assert, should equal series + streamList.Should().HaveCount(Quotes.Count); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public override void CustomToString() + { + AtrStopHub hub = new(new QuoteHub(), 14, 3, EndType.Close); + hub.ToString().Should().Be("ATR-STOP(14,3,CLOSE)"); + } +} diff --git a/tests/indicators/a-d/Awesome/Awesome.Tests.cs b/tests/indicators/a-d/Awesome/Awesome.StaticSeries.Tests.cs similarity index 57% rename from tests/indicators/a-d/Awesome/Awesome.Tests.cs rename to tests/indicators/a-d/Awesome/Awesome.StaticSeries.Tests.cs index eab128bcc..92982f51e 100644 --- a/tests/indicators/a-d/Awesome/Awesome.Tests.cs +++ b/tests/indicators/a-d/Awesome/Awesome.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class AwesomeTests : TestBase +public class Awesome : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetAwesome(5, 34) - .ToList(); + IReadOnlyList results = Quotes + .ToAwesome(); // proper quantities Assert.AreEqual(502, results.Count); @@ -33,35 +32,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetAwesome() - .ToList(); + .ToAwesome(); Assert.AreEqual(502, results.Count); Assert.AreEqual(469, results.Count(x => x.Oscillator != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetAwesome() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Oscillator is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetAwesome() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToAwesome(); Assert.AreEqual(502, results.Count); Assert.AreEqual(468, results.Count(x => x.Oscillator != null)); @@ -70,38 +56,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetAwesome() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToAwesome() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(460, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetAwesome() - .ToList(); + IReadOnlyList r = BadQuotes + .ToAwesome(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Oscillator is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Oscillator is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetAwesome() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToAwesome(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetAwesome() - .ToList(); + IReadOnlyList r1 = Onequote + .ToAwesome(); Assert.AreEqual(1, r1.Count); } @@ -109,15 +91,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetAwesome(5, 34) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToAwesome() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 33, results.Count); - AwesomeResult last = results.LastOrDefault(); + AwesomeResult last = results[^1]; Assert.AreEqual(-17.7692, last.Oscillator.Round(4)); Assert.AreEqual(-7.2763, last.Normalized.Round(4)); } @@ -127,10 +108,10 @@ public void Exceptions() { // bad fast period Assert.ThrowsException(() => - quotes.GetAwesome(0, 34)); + Quotes.ToAwesome(0)); // bad slow period Assert.ThrowsException(() => - quotes.GetAwesome(25, 25)); + Quotes.ToAwesome(25, 25)); } } diff --git a/tests/indicators/a-d/BasicQuote/BasicQuote.Tests.cs b/tests/indicators/a-d/BasicQuote/BasicQuote.Tests.cs deleted file mode 100644 index 45a8472a1..000000000 --- a/tests/indicators/a-d/BasicQuote/BasicQuote.Tests.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace Tests.Indicators; - -[TestClass] -public class BaseQuoteTests : TestBase -{ - [TestMethod] - public void Standard() - { - // compose basic data - List o = quotes.GetBaseQuote(CandlePart.Open).ToList(); - List h = quotes.GetBaseQuote(CandlePart.High).ToList(); - List l = quotes.GetBaseQuote(CandlePart.Low).ToList(); - List c = quotes.GetBaseQuote(CandlePart.Close).ToList(); - List v = quotes.GetBaseQuote(CandlePart.Volume).ToList(); - List hl = quotes.GetBaseQuote(CandlePart.HL2).ToList(); - List hlc = quotes.GetBaseQuote(CandlePart.HLC3).ToList(); - List oc = quotes.GetBaseQuote(CandlePart.OC2).ToList(); - List ohl = quotes.GetBaseQuote(CandlePart.OHL3).ToList(); - List ohlc = quotes.GetBaseQuote(CandlePart.OHLC4).ToList(); - - // proper quantities - Assert.AreEqual(502, c.Count); - - // samples - BasicData ro = o[501]; - BasicData rh = h[501]; - BasicData rl = l[501]; - BasicData rc = c[501]; - BasicData rv = v[501]; - BasicData rhl = hl[501]; - BasicData rhlc = hlc[501]; - BasicData roc = oc[501]; - BasicData rohl = ohl[501]; - BasicData rohlc = ohlc[501]; - - // proper last date - DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(lastDate, rc.Date); - - // last values should be correct - Assert.AreEqual(244.92, ro.Value); - Assert.AreEqual(245.54, rh.Value); - Assert.AreEqual(242.87, rl.Value); - Assert.AreEqual(245.28, rc.Value); - Assert.AreEqual(147031456, rv.Value); - Assert.AreEqual(244.205, rhl.Value); - Assert.AreEqual(244.5633, rhlc.Value.Round(4)); - Assert.AreEqual(245.1, roc.Value); - Assert.AreEqual(244.4433, rohl.Value.Round(4)); - Assert.AreEqual(244.6525, rohlc.Value); - } - - [TestMethod] - public void Use() - { - List<(DateTime Date, double Value)> results = quotes - .Use(CandlePart.Close) - .ToList(); - - Assert.AreEqual(502, results.Count); - } - - [TestMethod] - public void Chainor() - { - List results = quotes - .GetBaseQuote(CandlePart.Close) - .GetSma(10) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(493, results.Count(x => x.Sma != null)); - } -} diff --git a/tests/indicators/a-d/Beta/Beta.Tests.cs b/tests/indicators/a-d/Beta/Beta.StaticSeries.Tests.cs similarity index 50% rename from tests/indicators/a-d/Beta/Beta.Tests.cs rename to tests/indicators/a-d/Beta/Beta.StaticSeries.Tests.cs index 67229ba06..c8214d024 100644 --- a/tests/indicators/a-d/Beta/Beta.Tests.cs +++ b/tests/indicators/a-d/Beta/Beta.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class BetaTests : TestBase +public class Beta : StaticSeriesTestBase { [TestMethod] public void All() { - List results = otherQuotes - .GetBeta(quotes, 20, BetaType.All) - .ToList(); + IReadOnlyList results = OtherQuotes + .ToBeta(Quotes, 20, BetaType.All); // proper quantities Assert.AreEqual(502, results.Count); @@ -49,11 +48,10 @@ public void All() } [TestMethod] - public void Standard() + public override void Standard() { - List results = Indicator - .GetBeta(otherQuotes, quotes, 20, BetaType.Standard) - .ToList(); + IReadOnlyList results = OtherQuotes + .ToBeta(Quotes, 20); // proper quantities Assert.AreEqual(502, results.Count); @@ -67,9 +65,8 @@ public void Standard() [TestMethod] public void Up() { - List results = otherQuotes - .GetBeta(quotes, 20, BetaType.Up) - .ToList(); + IReadOnlyList results = OtherQuotes + .ToBeta(Quotes, 20, BetaType.Up); // proper quantities Assert.AreEqual(502, results.Count); @@ -83,9 +80,8 @@ public void Up() [TestMethod] public void Down() { - List results = otherQuotes - .GetBeta(quotes, 20, BetaType.Down) - .ToList(); + IReadOnlyList results = OtherQuotes + .ToBeta(Quotes, 20, BetaType.Down); // proper quantities Assert.AreEqual(502, results.Count); @@ -97,35 +93,22 @@ public void Down() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = otherQuotes + IReadOnlyList results = OtherQuotes .Use(CandlePart.Close) - .GetBeta(quotes.Use(CandlePart.Close), 20) - .ToList(); + .ToBeta(Quotes.Use(CandlePart.Close), 20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Beta != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetBeta(tupleNanny, 6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Beta is double and double.NaN)); - } - [TestMethod] public void Chainor() { - List results = otherQuotes - .GetBeta(quotes, 20) - .GetSma(10) - .ToList(); + IReadOnlyList results = OtherQuotes + .ToBeta(Quotes, 20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(473, results.Count(x => x.Sma != null)); @@ -134,47 +117,42 @@ public void Chainor() [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetBeta(otherQuotes.GetSma(2), 20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToBeta(OtherQuotes.ToSma(2), 20); Assert.AreEqual(502, results.Count); Assert.AreEqual(481, results.Count(x => x.Beta != null)); - Assert.AreEqual(0, results.Count(x => x.Beta is double and double.NaN)); + Assert.AreEqual(0, results.Count(x => x.Beta is double.NaN)); } [TestMethod] - public void BadData() + public override void BadData() { - List r1 = badQuotes - .GetBeta(badQuotes, 15, BetaType.Standard) - .ToList(); + IReadOnlyList r1 = BadQuotes + .ToBeta(BadQuotes, 15); Assert.AreEqual(502, r1.Count); - Assert.AreEqual(0, r1.Count(x => x.Beta is double and double.NaN)); + Assert.AreEqual(0, r1.Count(x => x.Beta is double.NaN)); - List r2 = badQuotes - .GetBeta(badQuotes, 15, BetaType.Up) - .ToList(); + IReadOnlyList r2 = BadQuotes + .ToBeta(BadQuotes, 15, BetaType.Up); Assert.AreEqual(502, r2.Count); - Assert.AreEqual(0, r2.Count(x => x.BetaUp is double and double.NaN)); + Assert.AreEqual(0, r2.Count(x => x.BetaUp is double.NaN)); - List r3 = badQuotes - .GetBeta(badQuotes, 15, BetaType.Down) - .ToList(); + IReadOnlyList r3 = BadQuotes + .ToBeta(BadQuotes, 15, BetaType.Down); Assert.AreEqual(502, r3.Count); - Assert.AreEqual(0, r3.Count(x => x.BetaDown is double and double.NaN)); + Assert.AreEqual(0, r3.Count(x => x.BetaDown is double.NaN)); } [TestMethod] public void BigData() { - List r = bigQuotes - .GetBeta(bigQuotes, 150, BetaType.All) - .ToList(); + IReadOnlyList r = BigQuotes + .ToBeta(BigQuotes, 150, BetaType.All); Assert.AreEqual(1246, r.Count); } @@ -190,15 +168,12 @@ public void BetaMsft() https://www.nasdaq.com/market-activity/stocks/msft */ - List evalQuotes = TestData.GetMsft().ToList(); - List mktQuotes = TestData.GetSpx().ToList(); + IReadOnlyList evalQuotes = Data.GetMsft(); + IReadOnlyList mktQuotes = Data.GetSpx(); - List results = Indicator - .GetBeta( - evalQuotes.Aggregate(PeriodSize.Month), - mktQuotes.Aggregate(PeriodSize.Month), - 60, BetaType.Standard) - .ToList(); + IReadOnlyList results = evalQuotes + .Aggregate(PeriodSize.Month) + .ToBeta(mktQuotes.Aggregate(PeriodSize.Month), 60); Assert.AreEqual(0.91, results[385].Beta.Round(2)); } @@ -206,15 +181,14 @@ public void BetaMsft() [TestMethod] public void Removed() { - List results = otherQuotes - .GetBeta(quotes, 20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = OtherQuotes + .ToBeta(Quotes, 20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 20, results.Count); - BetaResult last = results.LastOrDefault(); + BetaResult last = results[^1]; Assert.AreEqual(1.5123, last.Beta.Round(4)); } @@ -222,9 +196,8 @@ public void Removed() public void SameSame() { // Beta should be 1 if evaluating against self - List results = quotes - .GetBeta(quotes, 20) - .ToList(); + IReadOnlyList results = Quotes + .ToBeta(Quotes, 20); // proper quantities Assert.AreEqual(502, results.Count); @@ -236,49 +209,50 @@ public void SameSame() } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetBeta(noquotes, 5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToBeta(Noquotes, 5); Assert.AreEqual(0, r0.Count); - List r1 = onequote.GetBeta(onequote, 5).ToList(); + IReadOnlyList r1 = Onequote + .ToBeta(Onequote, 5); + Assert.AreEqual(1, r1.Count); } [TestMethod] public void NoMatch() { - List quoteA = + IReadOnlyList quoteA = [ - new Quote { Date = DateTime.Parse("1/1/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/2/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/3/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/4/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/5/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/6/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/7/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/8/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/9/2020", EnglishCulture), Close = 1234 } + new(DateTime.Parse("1/1/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/2/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/3/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/4/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/5/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/6/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/7/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/8/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/9/2020", invariantCulture), 0, 0, 0, 1234, 0) ]; - List quoteB = + IReadOnlyList quoteB = [ - new Quote { Date = DateTime.Parse("1/1/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/2/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/3/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("2/4/2020", EnglishCulture), Close = 1234 }, // abberrant - new Quote { Date = DateTime.Parse("1/5/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/6/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/7/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/8/2020", EnglishCulture), Close = 1234 }, - new Quote { Date = DateTime.Parse("1/9/2020", EnglishCulture), Close = 1234 } + new(DateTime.Parse("1/1/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/2/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/3/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("2/4/2020", invariantCulture), 0, 0, 0, 1234, 0), // abberrant + new(DateTime.Parse("1/5/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/6/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/7/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/8/2020", invariantCulture), 0, 0, 0, 1234, 0), + new(DateTime.Parse("1/9/2020", invariantCulture), 0, 0, 0, 1234, 0) ]; Assert.ThrowsException(() - => quoteA.GetBeta(quoteB, 3)); + => quoteA.ToBeta(quoteB, 3)); } [TestMethod] @@ -286,12 +260,12 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() - => quotes.GetBeta(otherQuotes, 0)); + => Quotes.ToBeta(OtherQuotes, 0)); // bad evaluation quotes - List eval = TestData.GetCompare(300).ToList(); + IReadOnlyList eval = Data.GetCompare(300).ToList(); Assert.ThrowsException(() - => quotes.GetBeta(eval, 30)); + => Quotes.ToBeta(eval, 30)); } } diff --git a/tests/indicators/a-d/BollingerBands/BollingerBands.Tests.cs b/tests/indicators/a-d/BollingerBands/BollingerBands.StaticSeries.Tests.cs similarity index 62% rename from tests/indicators/a-d/BollingerBands/BollingerBands.Tests.cs rename to tests/indicators/a-d/BollingerBands/BollingerBands.StaticSeries.Tests.cs index abd4cecea..a0fa51e21 100644 --- a/tests/indicators/a-d/BollingerBands/BollingerBands.Tests.cs +++ b/tests/indicators/a-d/BollingerBands/BollingerBands.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class BollingerBandsTests : TestBase +public class BollingerBands : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = - quotes.GetBollingerBands(20, 2) - .ToList(); + IReadOnlyList results = + Quotes.ToBollingerBands(); // proper quantities Assert.AreEqual(502, results.Count); @@ -38,35 +37,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetBollingerBands() - .ToList(); + .ToBollingerBands(); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Sma != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetBollingerBands() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.UpperBand is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetBollingerBands() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToBollingerBands(); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.UpperBand != null)); @@ -75,38 +61,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetBollingerBands() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToBollingerBands() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetBollingerBands(15, 3) - .ToList(); + IReadOnlyList r = BadQuotes + .ToBollingerBands(15, 3); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.UpperBand is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.UpperBand is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetBollingerBands() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToBollingerBands(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetBollingerBands() - .ToList(); + IReadOnlyList r1 = Onequote + .ToBollingerBands(); Assert.AreEqual(1, r1.Count); } @@ -114,15 +96,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = - quotes.GetBollingerBands(20, 2) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToBollingerBands() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 19, results.Count); - BollingerBandsResult last = results.LastOrDefault(); + BollingerBandsResult last = results[^1]; Assert.AreEqual(251.8600, last.Sma.Round(4)); Assert.AreEqual(273.7004, last.UpperBand.Round(4)); Assert.AreEqual(230.0196, last.LowerBand.Round(4)); @@ -136,10 +117,10 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetBollingerBands(1)); + Quotes.ToBollingerBands(1)); // bad standard deviation Assert.ThrowsException(() => - quotes.GetBollingerBands(2, 0)); + Quotes.ToBollingerBands(2, 0)); } } diff --git a/tests/indicators/a-d/Bop/Bop.Tests.cs b/tests/indicators/a-d/Bop/Bop.StaticSeries.Tests.cs similarity index 57% rename from tests/indicators/a-d/Bop/Bop.Tests.cs rename to tests/indicators/a-d/Bop/Bop.StaticSeries.Tests.cs index 3f459108a..7169a2238 100644 --- a/tests/indicators/a-d/Bop/Bop.Tests.cs +++ b/tests/indicators/a-d/Bop/Bop.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class BopTests : TestBase +public class Bop : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetBop(14) - .ToList(); + IReadOnlyList results = Quotes + .ToBop(); // proper quantities Assert.AreEqual(502, results.Count); @@ -34,10 +33,9 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetBop(14) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToBop() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(480, results.Count(x => x.Sma != null)); @@ -46,49 +44,45 @@ public void Chainor() [TestMethod] public void NaN() { - IEnumerable r = TestData.GetBtcUsdNan() - .GetBop(50); + IReadOnlyList r = Data.GetBtcUsdNan() + .ToBop(50); - Assert.AreEqual(0, r.Count(x => x.Bop is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Bop is double.NaN)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetBop() - .ToList(); + IReadOnlyList r = BadQuotes + .ToBop(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Bop is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Bop is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetBop() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToBop(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetBop() - .ToList(); + IReadOnlyList r1 = Onequote + .ToBop(); Assert.AreEqual(1, r1.Count); } [TestMethod] public void Removed() { - List results = quotes - .GetBop(14) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToBop() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 13, results.Count); - BopResult last = results.LastOrDefault(); + BopResult last = results[^1]; Assert.AreEqual(-0.292788, last.Bop.Round(6)); } @@ -96,5 +90,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetBop(0)); + => Quotes.ToBop(0)); } diff --git a/tests/indicators/a-d/Cci/Cci.Tests.cs b/tests/indicators/a-d/Cci/Cci.StaticSeries.Tests.cs similarity index 52% rename from tests/indicators/a-d/Cci/Cci.Tests.cs rename to tests/indicators/a-d/Cci/Cci.StaticSeries.Tests.cs index 1154e6fb6..c4550ed6a 100644 --- a/tests/indicators/a-d/Cci/Cci.Tests.cs +++ b/tests/indicators/a-d/Cci/Cci.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class CciTests : TestBase +public class Cci : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetCci(20) - .ToList(); + IReadOnlyList results = Quotes + .ToCci(); // proper quantities Assert.AreEqual(502, results.Count); @@ -22,38 +21,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetCci(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToCci() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetCci(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToCci(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Cci is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Cci is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetCci() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToCci(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetCci() - .ToList(); + IReadOnlyList r1 = Onequote + .ToCci(); Assert.AreEqual(1, r1.Count); } @@ -61,15 +56,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetCci(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToCci() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 19, results.Count); - CciResult last = results.LastOrDefault(); + CciResult last = results[^1]; Assert.AreEqual(-52.9946, last.Cci.Round(4)); } @@ -77,5 +71,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetCci(0)); + => Quotes.ToCci(0)); } diff --git a/tests/indicators/a-d/ChaikinOsc/ChaikinOsc.Tests.cs b/tests/indicators/a-d/ChaikinOsc/ChaikinOsc.StaticSeries.Tests.cs similarity index 60% rename from tests/indicators/a-d/ChaikinOsc/ChaikinOsc.Tests.cs rename to tests/indicators/a-d/ChaikinOsc/ChaikinOsc.StaticSeries.Tests.cs index 89faa7093..613c34cb6 100644 --- a/tests/indicators/a-d/ChaikinOsc/ChaikinOsc.Tests.cs +++ b/tests/indicators/a-d/ChaikinOsc/ChaikinOsc.StaticSeries.Tests.cs @@ -1,17 +1,16 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class ChaikinOscTests : TestBase +public class ChaikinOsc : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int fastPeriods = 3; int slowPeriods = 10; - List results = quotes - .GetChaikinOsc(fastPeriods, slowPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToChaikinOsc(fastPeriods, slowPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -28,38 +27,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetChaikinOsc(3, 10) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToChaikinOsc() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(484, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetChaikinOsc(5, 15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToChaikinOsc(5, 15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Oscillator is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Oscillator is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetChaikinOsc() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToChaikinOsc(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetChaikinOsc() - .ToList(); + IReadOnlyList r1 = Onequote + .ToChaikinOsc(); Assert.AreEqual(1, r1.Count); } @@ -70,15 +65,14 @@ public void Removed() int fastPeriods = 3; int slowPeriods = 10; - List results = quotes - .GetChaikinOsc(fastPeriods, slowPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToChaikinOsc(fastPeriods, slowPeriods) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (slowPeriods + 100), results.Count); - ChaikinOscResult last = results.LastOrDefault(); + ChaikinOscResult last = results[^1]; Assert.AreEqual(3439986548.42, last.Adl.Round(2)); Assert.AreEqual(0.8052, last.MoneyFlowMultiplier.Round(4)); Assert.AreEqual(118396116.25, last.MoneyFlowVolume.Round(2)); @@ -90,10 +84,10 @@ public void Exceptions() { // bad fast lookback Assert.ThrowsException(() => - quotes.GetChaikinOsc(0)); + Quotes.ToChaikinOsc(0)); // bad slow lookback Assert.ThrowsException(() => - quotes.GetChaikinOsc(10, 5)); + Quotes.ToChaikinOsc(10, 5)); } } diff --git a/tests/indicators/a-d/Chandelier/Chandelier.Tests.cs b/tests/indicators/a-d/Chandelier/Chandelier.StaticSeries.Tests.cs similarity index 54% rename from tests/indicators/a-d/Chandelier/Chandelier.Tests.cs rename to tests/indicators/a-d/Chandelier/Chandelier.StaticSeries.Tests.cs index f100e2a7e..92b8663d0 100644 --- a/tests/indicators/a-d/Chandelier/Chandelier.Tests.cs +++ b/tests/indicators/a-d/Chandelier/Chandelier.StaticSeries.Tests.cs @@ -1,16 +1,15 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class ChandelierTests : TestBase +public class Chandelier : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int lookbackPeriods = 22; - List longResult = - quotes.GetChandelier(lookbackPeriods, 3) - .ToList(); + IReadOnlyList longResult = + Quotes.ToChandelier(lookbackPeriods); // proper quantities Assert.AreEqual(502, longResult.Count); @@ -24,9 +23,8 @@ public void Standard() Assert.AreEqual(259.0480, b.ChandelierExit.Round(4)); // short - List shortResult = - quotes.GetChandelier(lookbackPeriods, 3, ChandelierType.Short) - .ToList(); + IReadOnlyList shortResult = + Quotes.ToChandelier(lookbackPeriods, 3, ChandelierType.Short); ChandelierResult c = shortResult[501]; Assert.AreEqual(246.4240, c.ChandelierExit.Round(4)); @@ -35,38 +33,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetChandelier(22) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToChandelier() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(471, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetChandelier(15, 2) - .ToList(); + IReadOnlyList r = BadQuotes + .ToChandelier(15, 2); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.ChandelierExit is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.ChandelierExit is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetChandelier() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToChandelier(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetChandelier() - .ToList(); + IReadOnlyList r1 = Onequote + .ToChandelier(); Assert.AreEqual(1, r1.Count); } @@ -74,15 +68,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List longResult = quotes - .GetChandelier(22, 3) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToChandelier() + .RemoveWarmupPeriods(); // assertions - Assert.AreEqual(502 - 22, longResult.Count); + Assert.AreEqual(502 - 22, results.Count); - ChandelierResult last = longResult.LastOrDefault(); + ChandelierResult last = results[^1]; Assert.AreEqual(256.5860, last.ChandelierExit.Round(4)); } @@ -91,14 +84,14 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetChandelier(0)); + Quotes.ToChandelier(0)); // bad multiplier Assert.ThrowsException(() => - quotes.GetChandelier(25, 0)); + Quotes.ToChandelier(25, 0)); // bad type Assert.ThrowsException(() => - quotes.GetChandelier(25, 2, (ChandelierType)int.MaxValue)); + Quotes.ToChandelier(25, 2, (ChandelierType)int.MaxValue)); } } diff --git a/tests/indicators/a-d/Chop/Chop.Tests.cs b/tests/indicators/a-d/Chop/Chop.StaticSeries.Tests.cs similarity index 59% rename from tests/indicators/a-d/Chop/Chop.Tests.cs rename to tests/indicators/a-d/Chop/Chop.StaticSeries.Tests.cs index cb316bf39..0563157d0 100644 --- a/tests/indicators/a-d/Chop/Chop.Tests.cs +++ b/tests/indicators/a-d/Chop/Chop.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class ChopTests : TestBase +public class Chop : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetChop(14) - .ToList(); + IReadOnlyList results = Quotes + .ToChop(); // proper quantities Assert.AreEqual(502, results.Count); @@ -31,10 +30,9 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetChop(14) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToChop() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(479, results.Count(x => x.Sma != null)); @@ -44,9 +42,8 @@ public void Chainor() public void SmallLookback() { int lookbackPeriods = 2; - List results = quotes - .GetChop(lookbackPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToChop(lookbackPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -54,28 +51,25 @@ public void SmallLookback() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetChop(20) - .ToList(); + IReadOnlyList r = BadQuotes + .ToChop(20); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Chop is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Chop is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetChop() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToChop(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetChop() - .ToList(); + IReadOnlyList r1 = Onequote + .ToChop(); Assert.AreEqual(1, r1.Count); } @@ -83,15 +77,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetChop(14) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToChop() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 14, results.Count); - ChopResult last = results.LastOrDefault(); + ChopResult last = results[^1]; Assert.AreEqual(38.6526, last.Chop.Round(4)); } @@ -99,5 +92,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetChop(1)); + => Quotes.ToChop(1)); } diff --git a/tests/indicators/a-d/Cmf/Cmf.Tests.cs b/tests/indicators/a-d/Cmf/Cmf.StaticSeries.Tests.cs similarity index 65% rename from tests/indicators/a-d/Cmf/Cmf.Tests.cs rename to tests/indicators/a-d/Cmf/Cmf.StaticSeries.Tests.cs index 53d1e23fd..f6c5814c0 100644 --- a/tests/indicators/a-d/Cmf/Cmf.Tests.cs +++ b/tests/indicators/a-d/Cmf/Cmf.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class CmfTests : TestBase +public class Cmf : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetCmf(20) - .ToList(); + IReadOnlyList results = Quotes + .ToCmf(); // proper quantities Assert.AreEqual(502, results.Count); @@ -34,48 +33,43 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetCmf(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToCmf() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetCmf(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToCmf(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Cmf is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Cmf is double.NaN)); } [TestMethod] public void BigData() { - List r = bigQuotes - .GetCmf(150) - .ToList(); + IReadOnlyList r = BigQuotes + .ToCmf(150); Assert.AreEqual(1246, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetCmf() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToCmf(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetCmf() - .ToList(); + IReadOnlyList r1 = Onequote + .ToCmf(); Assert.AreEqual(1, r1.Count); } @@ -83,15 +77,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetCmf(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToCmf() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 19, results.Count); - CmfResult last = results.LastOrDefault(); + CmfResult last = results[^1]; Assert.AreEqual(0.8052, last.MoneyFlowMultiplier.Round(4)); Assert.AreEqual(118396116.25, last.MoneyFlowVolume.Round(2)); Assert.AreEqual(-0.123754, last.Cmf.Round(6)); @@ -101,5 +94,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetCmf(0)); + => Quotes.ToCmf(0)); } diff --git a/tests/indicators/a-d/Cmo/Cmo.Tests.cs b/tests/indicators/a-d/Cmo/Cmo.StaticSeries.Tests.cs similarity index 51% rename from tests/indicators/a-d/Cmo/Cmo.Tests.cs rename to tests/indicators/a-d/Cmo/Cmo.StaticSeries.Tests.cs index 52b1b82bc..866f2b762 100644 --- a/tests/indicators/a-d/Cmo/Cmo.Tests.cs +++ b/tests/indicators/a-d/Cmo/Cmo.StaticSeries.Tests.cs @@ -1,19 +1,16 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class CmoTests : TestBase +public class Cmo : StaticSeriesTestBase { + // TODO: test for CMO isUp works as expected + // when there’s no price change + [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetCmo(14) - .ToList(); - - foreach (CmoResult r in results) - { - Console.WriteLine($"{r.Date:d},{r.Cmo:N4}"); - } + IReadOnlyList results = Quotes + .ToCmo(14); // proper quantities Assert.AreEqual(502, results.Count); @@ -34,35 +31,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetCmo(14) - .ToList(); + .ToCmo(14); Assert.AreEqual(502, results.Count); Assert.AreEqual(488, results.Count(x => x.Cmo != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetCmo(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Cmo is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetCmo(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToCmo(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(481, results.Count(x => x.Cmo != null)); @@ -71,38 +55,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetCmo(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToCmo(20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(473, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetCmo(35) - .ToList(); + IReadOnlyList r = BadQuotes + .ToCmo(35); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Cmo is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Cmo is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetCmo(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToCmo(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetCmo(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToCmo(5); Assert.AreEqual(1, r1.Count); } @@ -110,15 +90,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetCmo(14) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToCmo(14) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(488, results.Count); - CmoResult last = results.LastOrDefault(); + CmoResult last = results[^1]; Assert.AreEqual(-26.7502, last.Cmo.Round(4)); } @@ -126,5 +105,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetCmo(0)); + => Quotes.ToCmo(0)); } diff --git a/tests/indicators/a-d/ConnorsRsi/ConnorsRsi.Tests.cs b/tests/indicators/a-d/ConnorsRsi/ConnorsRsi.StaticSeries.Tests.cs similarity index 60% rename from tests/indicators/a-d/ConnorsRsi/ConnorsRsi.Tests.cs rename to tests/indicators/a-d/ConnorsRsi/ConnorsRsi.StaticSeries.Tests.cs index e28b2b9c2..3e1c6e07c 100644 --- a/tests/indicators/a-d/ConnorsRsi/ConnorsRsi.Tests.cs +++ b/tests/indicators/a-d/ConnorsRsi/ConnorsRsi.StaticSeries.Tests.cs @@ -1,19 +1,18 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class ConnorsRsiTests : TestBase +public class ConnorsRsi : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int rsiPeriods = 3; int streakPeriods = 2; int rankPeriods = 100; int startPeriod = Math.Max(rsiPeriods, Math.Max(streakPeriods, rankPeriods)) + 2; - List results1 = - quotes.GetConnorsRsi(rsiPeriods, streakPeriods, rankPeriods) - .ToList(); + IReadOnlyList results1 = Quotes + .ToConnorsRsi(rsiPeriods, streakPeriods, rankPeriods); // proper quantities Assert.AreEqual(502, results1.Count); @@ -27,7 +26,7 @@ public void Standard() Assert.AreEqual(74.7662, r1.ConnorsRsi.Round(4)); // different parameters - List results2 = quotes.GetConnorsRsi(14, 20, 10).ToList(); + IReadOnlyList results2 = Quotes.ToConnorsRsi(14, 20, 10).ToList(); ConnorsRsiResult r2 = results2[501]; Assert.AreEqual(42.0773, r2.Rsi.Round(4)); Assert.AreEqual(52.7386, r2.RsiStreak.Round(4)); @@ -36,35 +35,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetConnorsRsi() - .ToList(); + .ToConnorsRsi(); Assert.AreEqual(502, results.Count); Assert.AreEqual(401, results.Count(x => x.ConnorsRsi != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetConnorsRsi() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.ConnorsRsi is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetConnorsRsi() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToConnorsRsi(); Assert.AreEqual(502, results.Count); Assert.AreEqual(400, results.Count(x => x.ConnorsRsi != null)); @@ -73,38 +59,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetConnorsRsi() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToConnorsRsi() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(392, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetConnorsRsi(4, 3, 25) - .ToList(); + IReadOnlyList r = BadQuotes + .ToConnorsRsi(4, 3, 25); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Rsi is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Rsi is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetConnorsRsi() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToConnorsRsi(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetConnorsRsi() - .ToList(); + IReadOnlyList r1 = Onequote + .ToConnorsRsi(); Assert.AreEqual(1, r1.Count); } @@ -119,15 +101,14 @@ public void Removed() // TODO: I don't think this is right, inconsistent int removePeriods = Math.Max(rsiPeriods, Math.Max(streakPeriods, rankPeriods)) + 2; - List results = - quotes.GetConnorsRsi(rsiPeriods, streakPeriods, rankPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToConnorsRsi(rsiPeriods, streakPeriods, rankPeriods) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - removePeriods + 1, results.Count); - ConnorsRsiResult last = results.LastOrDefault(); + ConnorsRsiResult last = results[^1]; Assert.AreEqual(68.8087, last.Rsi.Round(4)); Assert.AreEqual(67.4899, last.RsiStreak.Round(4)); Assert.AreEqual(88.0000, last.PercentRank.Round(4)); @@ -139,14 +120,14 @@ public void Exceptions() { // bad RSI period Assert.ThrowsException(() => - quotes.GetConnorsRsi(1, 2, 100)); + Quotes.ToConnorsRsi(1)); // bad Streak period Assert.ThrowsException(() => - quotes.GetConnorsRsi(3, 1, 100)); + Quotes.ToConnorsRsi(3, 1)); // bad Rank period Assert.ThrowsException(() => - quotes.GetConnorsRsi(3, 2, 1)); + Quotes.ToConnorsRsi(3, 2, 1)); } } diff --git a/tests/indicators/a-d/Correlation/Correlation.Tests.cs b/tests/indicators/a-d/Correlation/Correlation.StaticSeries.Tests.cs similarity index 54% rename from tests/indicators/a-d/Correlation/Correlation.Tests.cs rename to tests/indicators/a-d/Correlation/Correlation.StaticSeries.Tests.cs index 53c60c0a3..5502f29db 100644 --- a/tests/indicators/a-d/Correlation/Correlation.Tests.cs +++ b/tests/indicators/a-d/Correlation/Correlation.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class CorrelationTests : TestBase +public class Correlation : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = - quotes.GetCorrelation(otherQuotes, 20) - .ToList(); + IReadOnlyList results = Quotes + .ToCorrelation(OtherQuotes, 20); // proper quantities // should always be the same number of results as there is quotes @@ -34,35 +33,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetCorrelation(otherQuotes.Use(CandlePart.Close), 20) - .ToList(); + .ToCorrelation(OtherQuotes.Use(CandlePart.Close), 20); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Correlation != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetCorrelation(tupleNanny, 6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Correlation is double and double.NaN)); - } - [TestMethod] public void Chainor() { - List results = quotes - .GetCorrelation(otherQuotes, 20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToCorrelation(OtherQuotes, 20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); @@ -71,49 +57,44 @@ public void Chainor() [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetCorrelation(otherQuotes.GetSma(2), 20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToCorrelation(OtherQuotes.ToSma(2), 20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Correlation != null)); - Assert.AreEqual(0, results.Count(x => x.Correlation is double and double.NaN)); + Assert.AreEqual(0, results.Count(x => x.Correlation is double.NaN)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetCorrelation(badQuotes, 15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToCorrelation(BadQuotes, 15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Correlation is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Correlation is double.NaN)); } [TestMethod] public void BigData() { - List r = bigQuotes - .GetCorrelation(bigQuotes, 150) - .ToList(); + IReadOnlyList r = BigQuotes + .ToCorrelation(BigQuotes, 150); Assert.AreEqual(1246, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetCorrelation(noquotes, 10) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToCorrelation(Noquotes, 10); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetCorrelation(onequote, 10) - .ToList(); + IReadOnlyList r1 = Onequote + .ToCorrelation(Onequote, 10); Assert.AreEqual(1, r1.Count); } @@ -121,15 +102,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetCorrelation(otherQuotes, 20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToCorrelation(OtherQuotes, 20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 19, results.Count); - CorrResult last = results.LastOrDefault(); + CorrResult last = results[^1]; Assert.AreEqual(0.8460, last.Correlation.Round(4)); Assert.AreEqual(0.7157, last.RSquared.Round(4)); } @@ -139,15 +119,15 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetCorrelation(otherQuotes, 0)); + Quotes.ToCorrelation(OtherQuotes, 0)); // bad eval quotes - IEnumerable eval = TestData.GetCompare(300); + IReadOnlyList eval = Data.GetCompare(300); Assert.ThrowsException(() => - quotes.GetCorrelation(eval, 30)); + Quotes.ToCorrelation(eval, 30)); // mismatched quotes Assert.ThrowsException(() => - mismatchQuotes.GetCorrelation(otherQuotes, 20)); + MismatchQuotes.ToCorrelation(OtherQuotes, 20)); } } diff --git a/tests/indicators/a-d/Dema/Dema.Tests.cs b/tests/indicators/a-d/Dema/Dema.StaticSeries.Tests.cs similarity index 53% rename from tests/indicators/a-d/Dema/Dema.Tests.cs rename to tests/indicators/a-d/Dema/Dema.StaticSeries.Tests.cs index b0a040b7c..b98a3b5f7 100644 --- a/tests/indicators/a-d/Dema/Dema.Tests.cs +++ b/tests/indicators/a-d/Dema/Dema.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class DemaTests : TestBase +public class Dema : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetDema(20) - .ToList(); + IReadOnlyList results = Quotes + .ToDema(20); // proper quantities Assert.AreEqual(502, results.Count); @@ -29,35 +28,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetDema(20) - .ToList(); + .ToDema(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Dema != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetDema(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Dema is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetDema(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToDema(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Dema != null)); @@ -66,38 +52,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetDema(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToDema(20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetDema(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToDema(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Dema is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Dema is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetDema(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToDema(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetDema(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToDema(5); Assert.AreEqual(1, r1.Count); } @@ -105,15 +87,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetDema(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToDema(20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (40 + 100), results.Count); - DemaResult last = results.LastOrDefault(); + DemaResult last = results[^1]; Assert.AreEqual(241.1677, last.Dema.Round(4)); } @@ -121,5 +102,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetDema(0)); + => Quotes.ToDema(0)); } diff --git a/tests/indicators/a-d/Doji/Doji.Tests.cs b/tests/indicators/a-d/Doji/Doji.StaticSeries.Tests.cs similarity index 67% rename from tests/indicators/a-d/Doji/Doji.Tests.cs rename to tests/indicators/a-d/Doji/Doji.StaticSeries.Tests.cs index 4e43d65b2..6b0622535 100644 --- a/tests/indicators/a-d/Doji/Doji.Tests.cs +++ b/tests/indicators/a-d/Doji/Doji.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class DojiTests : TestBase +public class Doji : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetDoji(0.1) - .ToList(); + IReadOnlyList results = Quotes + .ToDoji(); // proper quantities Assert.AreEqual(502, results.Count); @@ -41,27 +40,24 @@ public void Standard() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetDoji() - .ToList(); + IReadOnlyList r = BadQuotes + .ToDoji(); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetDoji() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToDoji(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetDoji() - .ToList(); + IReadOnlyList r1 = Onequote + .ToDoji(); Assert.AreEqual(1, r1.Count); } @@ -69,12 +65,11 @@ public void NoQuotes() [TestMethod] public void Condense() { - List r = quotes - .GetDoji(0.1) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToDoji() + .Condense(); - Assert.AreEqual(112, r.Count); + Assert.AreEqual(112, results.Count); } [TestMethod] @@ -82,9 +77,9 @@ public void Exceptions() { // bad maximum change value Assert.ThrowsException(() => - quotes.GetDoji(-0.00001)); + Quotes.ToDoji(-0.00001)); Assert.ThrowsException(() => - quotes.GetDoji(0.50001)); + Quotes.ToDoji(0.50001)); } } diff --git a/tests/indicators/a-d/Donchian/Donchian.Tests.cs b/tests/indicators/a-d/Donchian/Donchian.StaticSeries.Tests.cs similarity index 74% rename from tests/indicators/a-d/Donchian/Donchian.Tests.cs rename to tests/indicators/a-d/Donchian/Donchian.StaticSeries.Tests.cs index d41056dad..c16cee7b4 100644 --- a/tests/indicators/a-d/Donchian/Donchian.Tests.cs +++ b/tests/indicators/a-d/Donchian/Donchian.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class DonchianTests : TestBase +public class Donchian : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetDonchian(20) - .ToList(); + IReadOnlyList results = Quotes + .ToDonchian(); // proper quantities Assert.AreEqual(502, results.Count); @@ -50,27 +49,24 @@ public void Standard() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetDonchian(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToDonchian(15); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetDonchian() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToDonchian(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetDonchian() - .ToList(); + IReadOnlyList r1 = Onequote + .ToDonchian(); Assert.AreEqual(1, r1.Count); } @@ -78,15 +74,14 @@ public void NoQuotes() [TestMethod] public void Condense() { - List r = quotes - .GetDonchian(20) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToDonchian() + .Condense(); // assertions - Assert.AreEqual(502 - 20, r.Count); + Assert.AreEqual(502 - 20, results.Count); - DonchianResult last = r.LastOrDefault(); + DonchianResult last = results[^1]; Assert.AreEqual(251.5050m, last.Centerline.Round(4)); Assert.AreEqual(273.5900m, last.UpperBand.Round(4)); Assert.AreEqual(229.4200m, last.LowerBand.Round(4)); @@ -96,15 +91,14 @@ public void Condense() [TestMethod] public void Removed() { - List results = quotes - .GetDonchian(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToDonchian() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 20, results.Count); - DonchianResult last = results.LastOrDefault(); + DonchianResult last = results[^1]; Assert.AreEqual(251.5050m, last.Centerline.Round(4)); Assert.AreEqual(273.5900m, last.UpperBand.Round(4)); Assert.AreEqual(229.4200m, last.LowerBand.Round(4)); @@ -115,5 +109,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetDonchian(0)); + => Quotes.ToDonchian(0)); } diff --git a/tests/indicators/a-d/Dpo/Dpo.Tests.cs b/tests/indicators/a-d/Dpo/Dpo.StaticSeries.Tests.cs similarity index 51% rename from tests/indicators/a-d/Dpo/Dpo.Tests.cs rename to tests/indicators/a-d/Dpo/Dpo.StaticSeries.Tests.cs index 324b2ec69..08628d406 100644 --- a/tests/indicators/a-d/Dpo/Dpo.Tests.cs +++ b/tests/indicators/a-d/Dpo/Dpo.StaticSeries.Tests.cs @@ -1,10 +1,10 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class DpoTests : TestBase +public class Dpo : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { // get expected data List qot = []; @@ -17,22 +17,14 @@ public void Standard() for (int i = 0; i < csvData.Count; i++) { string[] csv = csvData[i].Split(","); - DateTime date = Convert.ToDateTime(csv[1], EnglishCulture); + DateTime date = Convert.ToDateTime(csv[1], invariantCulture); - qot.Add(new Quote { - Date = date, - Close = csv[5].ToDecimal() - }); - - exp.Add(new DpoResult(date) { - Sma = csv[6].ToDoubleNull(), - Dpo = csv[7].ToDoubleNull() - }); + qot.Add(new Quote(date, 0, 0, 0, Close: csv[5].ToDecimal(), 0)); + exp.Add(new(date, csv[7].ToDoubleNull(), csv[6].ToDoubleNull())); } // calculate actual data - List act = qot.GetDpo(14) - .ToList(); + IReadOnlyList act = qot.ToDpo(14); // assertions Assert.AreEqual(exp.Count, act.Count); @@ -43,42 +35,29 @@ public void Standard() DpoResult e = exp[i]; DpoResult a = act[i]; - Assert.AreEqual(e.Date, a.Date); + Assert.AreEqual(e.Timestamp, a.Timestamp); Assert.AreEqual(e.Sma, a.Sma.Round(5), $"at index {i}"); Assert.AreEqual(e.Dpo, a.Dpo.Round(5), $"at index {i}"); } } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetDpo(14) - .ToList(); + .ToDpo(14); Assert.AreEqual(502, results.Count); Assert.AreEqual(489, results.Count(x => x.Dpo != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetDpo(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Dpo is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetDpo(14) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToDpo(14); Assert.AreEqual(502, results.Count); Assert.AreEqual(488, results.Count(x => x.Dpo != null)); @@ -87,38 +66,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetDpo(14) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToDpo(14) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(480, results.Count(x => x.Sma is not null and not double.NaN)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetDpo(5) - .ToList(); + IReadOnlyList r = BadQuotes + .ToDpo(5); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Dpo is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Dpo is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetDpo(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToDpo(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetDpo(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToDpo(5); Assert.AreEqual(1, r1.Count); } @@ -127,5 +102,5 @@ public void NoQuotes() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetDpo(0)); + => Quotes.ToDpo(0)); } diff --git a/tests/indicators/a-d/Dynamic/Dynamic.Tests.cs b/tests/indicators/a-d/Dynamic/Dynamic.StaticSeries.Tests.cs similarity index 54% rename from tests/indicators/a-d/Dynamic/Dynamic.Tests.cs rename to tests/indicators/a-d/Dynamic/Dynamic.StaticSeries.Tests.cs index a3fb16f0f..6f2a47247 100644 --- a/tests/indicators/a-d/Dynamic/Dynamic.Tests.cs +++ b/tests/indicators/a-d/Dynamic/Dynamic.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class McGinleyDynamicTests : TestBase +public class McGinleyDynamic : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetDynamic(14) - .ToList(); + IReadOnlyList results = Quotes + .ToDynamic(14); // assertions Assert.AreEqual(502, results.Count); @@ -29,36 +28,23 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetDynamic(20) - .ToList(); + .ToDynamic(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(501, results.Count(x => x.Dynamic != null)); - Assert.AreEqual(0, results.Count(x => x.Dynamic is double and double.NaN)); - } - - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetDynamic(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Dynamic is double and double.NaN)); + Assert.AreEqual(0, results.Count(x => x.Dynamic is double.NaN)); } [TestMethod] public void Chainee() { - List results = quotes - .GetSma(10) - .GetDynamic(14) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(10) + .ToDynamic(14); Assert.AreEqual(502, results.Count); Assert.AreEqual(492, results.Count(x => x.Dynamic != null)); @@ -67,38 +53,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetDynamic(14) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToDynamic(14) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(492, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetDynamic(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToDynamic(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Dynamic is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Dynamic is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetDynamic(14) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToDynamic(14); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetDynamic(14) - .ToList(); + IReadOnlyList r1 = Onequote + .ToDynamic(14); Assert.AreEqual(1, r1.Count); } @@ -108,10 +90,10 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() - => quotes.GetDynamic(0)); + => Quotes.ToDynamic(0)); // bad k-factor Assert.ThrowsException(() - => quotes.GetDynamic(14, 0)); + => Quotes.ToDynamic(14, 0)); } } diff --git a/tests/indicators/e-k/ElderRay/ElderRay.Tests.cs b/tests/indicators/e-k/ElderRay/ElderRay.StaticSeries.Tests.cs similarity index 70% rename from tests/indicators/e-k/ElderRay/ElderRay.Tests.cs rename to tests/indicators/e-k/ElderRay/ElderRay.StaticSeries.Tests.cs index 34af7b32b..f857c4df9 100644 --- a/tests/indicators/e-k/ElderRay/ElderRay.Tests.cs +++ b/tests/indicators/e-k/ElderRay/ElderRay.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class ElderRayTests : TestBase +public class ElderRay : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetElderRay(13) - .ToList(); + IReadOnlyList results = Quotes + .ToElderRay(); // proper quantities Assert.AreEqual(502, results.Count); @@ -50,38 +49,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetElderRay(13) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToElderRay() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(481, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetElderRay() - .ToList(); + IReadOnlyList r = BadQuotes + .ToElderRay(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.BullPower is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.BullPower is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetElderRay() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToElderRay(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetElderRay() - .ToList(); + IReadOnlyList r1 = Onequote + .ToElderRay(); Assert.AreEqual(1, r1.Count); } @@ -89,15 +84,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetElderRay(13) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToElderRay() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (100 + 13), results.Count); - ElderRayResult last = results.LastOrDefault(); + ElderRayResult last = results[^1]; Assert.AreEqual(246.0129, last.Ema.Round(4)); Assert.AreEqual(-0.4729, last.BullPower.Round(4)); Assert.AreEqual(-3.1429, last.BearPower.Round(4)); @@ -107,5 +101,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetElderRay(0)); + => Quotes.ToElderRay(0)); } diff --git a/tests/indicators/e-k/Ema/Ema.Increments.Tests.cs b/tests/indicators/e-k/Ema/Ema.Increments.Tests.cs new file mode 100644 index 000000000..b19ce4c69 --- /dev/null +++ b/tests/indicators/e-k/Ema/Ema.Increments.Tests.cs @@ -0,0 +1,72 @@ +namespace Increments; + +[TestClass] +public class Ema : IncrementsTestBase +{ + private const int lookbackPeriods = 14; + + private static readonly IReadOnlyList reusables + = Quotes + .Cast() + .ToList(); + + private static readonly IReadOnlyList series + = Quotes.ToEma(lookbackPeriods); + + [TestMethod] + public void FromReusableSplit() + { + EmaList sut = new(lookbackPeriods); + + foreach (IReusable item in reusables) + { + sut.Add(item.Timestamp, item.Value); + } + + sut.Should().HaveCount(Quotes.Count); + sut.Should().BeEquivalentTo(series); + } + + [TestMethod] + public void FromReusableItem() + { + EmaList sut = new(lookbackPeriods); + + foreach (IReusable item in reusables) { sut.Add(item); } + + sut.Should().HaveCount(Quotes.Count); + sut.Should().BeEquivalentTo(series); + } + + [TestMethod] + public void FromReusableBatch() + { + EmaList sut = new(lookbackPeriods) { reusables }; + + sut.Should().HaveCount(Quotes.Count); + sut.Should().BeEquivalentTo(series); + } + + [TestMethod] + public override void FromQuote() + { + EmaList sut = new(lookbackPeriods); + + foreach (Quote q in Quotes) { sut.Add(q); } + + sut.Should().HaveCount(Quotes.Count); + sut.Should().BeEquivalentTo(series); + } + + [TestMethod] + public override void FromQuoteBatch() + { + EmaList sut = new(lookbackPeriods) { Quotes }; + + IReadOnlyList series + = Quotes.ToEma(lookbackPeriods); + + sut.Should().HaveCount(Quotes.Count); + sut.Should().BeEquivalentTo(series); + } +} diff --git a/tests/indicators/e-k/Ema/Ema.Obs.Tests.cs b/tests/indicators/e-k/Ema/Ema.Obs.Tests.cs deleted file mode 100644 index a62556b1e..000000000 --- a/tests/indicators/e-k/Ema/Ema.Obs.Tests.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Skender.Stock.Indicators; -using Tests.Common; - -namespace Tests.Indicators; - -[TestClass] -public class EmaStreamTests : TestBase -{ - [TestMethod] - public void Standard() - { - List quotesList = quotes - .ToSortedList(); - - int length = quotesList.Count; - - // time-series, for comparison - List seriesList = quotes - .GetEma(20) - .ToList(); - - // setup quote provider - QuoteProvider provider = new(); - - // initialize EMA observer - EmaObserver observer = provider - .GetEma(20); - - // fetch initial results - IEnumerable results - = observer.Results; - - // emulate adding quotes to provider - for (int i = 0; i < length; i++) - { - Quote q = quotesList[i]; - provider.Add(q); - } - - // final results - List resultsList - = results.ToList(); - - // assert, should equal series - for (int i = 0; i < seriesList.Count; i++) - { - EmaResult s = seriesList[i]; - EmaResult r = resultsList[i]; - - Assert.AreEqual(s.Date, r.Date); - Assert.AreEqual(s.Ema, r.Ema); - } - - observer.Unsubscribe(); - provider.EndTransmission(); - } - - [TestMethod] - public void Usee() - { - List quotesList = quotes - .ToSortedList(); - - int length = quotesList.Count; - - // time-series, for comparison - List staticEma = quotes - .Use(CandlePart.OC2) - .GetEma(11) - .ToList(); - - // setup quote provider - QuoteProvider provider = new(); - - // initialize EMA observer - List streamEma = provider - .Use(CandlePart.OC2) - .GetEma(11) - .ProtectedResults; - - // emulate adding quotes to provider - for (int i = 0; i < length; i++) - { - provider.Add(quotesList[i]); - } - - provider.EndTransmission(); - - // assert, should equal series - for (int i = 0; i < length; i++) - { - EmaResult t = staticEma[i]; - EmaResult s = streamEma[i]; - - Assert.AreEqual(t.Date, s.Date); - Assert.AreEqual(t.Ema, s.Ema); - } - } -} diff --git a/tests/indicators/e-k/Ema/Ema.Static.Tests.cs b/tests/indicators/e-k/Ema/Ema.StaticSeries.Tests.cs similarity index 55% rename from tests/indicators/e-k/Ema/Ema.Static.Tests.cs rename to tests/indicators/e-k/Ema/Ema.StaticSeries.Tests.cs index 7987bae6d..39a670a7f 100644 --- a/tests/indicators/e-k/Ema/Ema.Static.Tests.cs +++ b/tests/indicators/e-k/Ema/Ema.StaticSeries.Tests.cs @@ -1,14 +1,20 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class EmaStaticTests : TestBase +public class EmaTests : StaticSeriesTestBase { [TestMethod] - public void Standard() + public void Increment() { - List results = quotes - .GetEma(20) - .ToList(); + double ema = Ema.Increment(20, 217.5693, 222.10); + + Assert.AreEqual(218.0008, ema.Round(4)); + } + + [TestMethod] + public override void Standard() + { + IReadOnlyList results = Quotes.ToEma(20); // proper quantities Assert.AreEqual(502, results.Count); @@ -28,10 +34,9 @@ public void Standard() [TestMethod] public void UsePart() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Open) - .GetEma(20) - .ToList(); + .ToEma(20); // assertions @@ -42,77 +47,62 @@ public void UsePart() // sample values EmaResult r29 = results[29]; - Assert.AreEqual(216.2643, NullMath.Round(r29.Ema, 4)); + Assert.AreEqual(216.2643, r29.Ema.Round(4)); EmaResult r249 = results[249]; - Assert.AreEqual(255.4875, NullMath.Round(r249.Ema, 4)); + Assert.AreEqual(255.4875, r249.Ema.Round(4)); EmaResult r501 = results[501]; - Assert.AreEqual(249.9157, NullMath.Round(r501.Ema, 4)); + Assert.AreEqual(249.9157, r501.Ema.Round(4)); } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetEma(20) - .ToList(); + .ToEma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Ema != null)); - Assert.AreEqual(0, results.Count(x => x.Ema is double and double.NaN)); - } - - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetEma(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Ema is double and double.NaN)); + Assert.AreEqual(0, results.Count(x => x.Ema is double.NaN)); } [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetEma(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToEma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Ema != null)); - Assert.AreEqual(0, results.Count(x => x.Ema is double and double.NaN)); + Assert.AreEqual(0, results.Count(x => x.Ema is double.NaN)); } [TestMethod] public void Chainor() { - List results = quotes - .GetEma(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToEma(20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); - Assert.AreEqual(0, results.Count(x => x.Sma is double and double.NaN)); + Assert.AreEqual(0, results.Count(x => x.Sma is double.NaN)); } [TestMethod] public void ChaineeMore() { - List results = quotes - .GetRsi(14) - .GetEma(20) - .ToList(); + IReadOnlyList results = Quotes + .ToRsi() + .ToEma(20); // assertions Assert.AreEqual(502, results.Count); Assert.AreEqual(469, results.Count(x => x.Ema != null)); - Assert.AreEqual(0, results.Count(x => x.Ema is double and double.NaN)); + Assert.AreEqual(0, results.Count(x => x.Ema is double.NaN)); // sample values EmaResult r32 = results[32]; @@ -129,28 +119,22 @@ public void ChaineeMore() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetEma(15) - .ToList(); + IReadOnlyList r = BadQuotes.ToEma(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Ema is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Ema is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetEma(10) - .ToList(); + IReadOnlyList r0 = Noquotes.ToEma(10); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetEma(10) - .ToList(); + IReadOnlyList r1 = Onequote.ToEma(10); Assert.AreEqual(1, r1.Count); } @@ -158,15 +142,13 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetEma(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes.ToEma(20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (20 + 100), results.Count); - EmaResult last = results.LastOrDefault(); + EmaResult last = results[^1]; Assert.AreEqual(249.3519, last.Ema.Round(4)); } @@ -174,5 +156,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetEma(0)); + => Quotes.ToEma(0)); } diff --git a/tests/indicators/e-k/Ema/Ema.StreamHub.Tests.cs b/tests/indicators/e-k/Ema/Ema.StreamHub.Tests.cs new file mode 100644 index 000000000..60354ebfb --- /dev/null +++ b/tests/indicators/e-k/Ema/Ema.StreamHub.Tests.cs @@ -0,0 +1,176 @@ +namespace StreamHub; + +[TestClass] +public class EmaHub : StreamHubTestBase, ITestChainObserver, ITestChainProvider +{ + [TestMethod] + public override void QuoteObserver() + { + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider + for (int i = 0; i < 20; i++) + { + provider.Add(quotesList[i]); + } + + // initialize observer + EmaHub observer = provider + .ToEma(5); + + // fetch initial results (early) + IReadOnlyList streamList + = observer.Results; + + // emulate adding quotes to provider + for (int i = 20; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotesList[80]); + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // time-series, for comparison + IReadOnlyList seriesList = quotesList.ToEma(5); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void ChainObserver() + { + int emaPeriods = 12; + int smaPeriods = 8; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + EmaHub observer = provider + .ToSma(smaPeriods) + .ToEma(emaPeriods); + + // emulate quote stream + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + // final results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList + .ToSma(smaPeriods) + .ToEma(emaPeriods); + + // assert, should equal series + streamList.Should().HaveCount(length); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void ChainProvider() + { + int emaPeriods = 20; + int smaPeriods = 10; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + SmaHub observer = provider + .ToEma(emaPeriods) + .ToSma(smaPeriods); + + // emulate adding quotes to provider + for (int i = 0; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotesList[80]); + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // final results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList.ToEma(emaPeriods) + .ToSma(smaPeriods); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public override void CustomToString() + { + EmaHub hub = new(new QuoteHub(), 14); + hub.ToString().Should().Be("EMA(14)"); + } +} diff --git a/tests/indicators/e-k/Epma/Epma.Tests.cs b/tests/indicators/e-k/Epma/Epma.StaticSeries.Tests.cs similarity index 54% rename from tests/indicators/e-k/Epma/Epma.Tests.cs rename to tests/indicators/e-k/Epma/Epma.StaticSeries.Tests.cs index 5db2e5478..6756a5159 100644 --- a/tests/indicators/e-k/Epma/Epma.Tests.cs +++ b/tests/indicators/e-k/Epma/Epma.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class EpmaTests : TestBase +public class Epma : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetEpma(20) - .ToList(); + IReadOnlyList results = Quotes + .ToEpma(20); // proper quantities Assert.AreEqual(502, results.Count); @@ -32,35 +31,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetEpma(20) - .ToList(); + .ToEpma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Epma != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetEpma(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Epma is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetEpma(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToEpma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Epma != null)); @@ -69,38 +55,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetEpma(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToEpma(20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetEpma(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToEpma(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Epma is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Epma is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetEpma(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToEpma(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetEpma(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToEpma(5); Assert.AreEqual(1, r1.Count); } @@ -108,15 +90,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetEpma(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToEpma(20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 19, results.Count); - EpmaResult last = results.LastOrDefault(); + EpmaResult last = results[^1]; Assert.AreEqual(235.8131, last.Epma.Round(4)); } @@ -124,5 +105,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetEpma(0)); + => Quotes.ToEpma(0)); } diff --git a/tests/indicators/e-k/Fcb/Fcb.Tests.cs b/tests/indicators/e-k/Fcb/Fcb.StaticSeries.Tests.cs similarity index 68% rename from tests/indicators/e-k/Fcb/Fcb.Tests.cs rename to tests/indicators/e-k/Fcb/Fcb.StaticSeries.Tests.cs index ed51d3f43..e0742cacd 100644 --- a/tests/indicators/e-k/Fcb/Fcb.Tests.cs +++ b/tests/indicators/e-k/Fcb/Fcb.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class FcbTests : TestBase +public class Fcb : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetFcb(2) - .ToList(); + IReadOnlyList results = Quotes + .ToFcb(); // proper quantities Assert.AreEqual(502, results.Count); @@ -42,27 +41,24 @@ public void Standard() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetFcb() - .ToList(); + IReadOnlyList r = BadQuotes + .ToFcb(); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetFcb() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToFcb(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetFcb() - .ToList(); + IReadOnlyList r1 = Onequote + .ToFcb(); Assert.AreEqual(1, r1.Count); } @@ -70,15 +66,14 @@ public void NoQuotes() [TestMethod] public void Condense() { - List results = quotes - .GetFcb(2) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToFcb() + .Condense(); // assertions Assert.AreEqual(502 - 5, results.Count); - FcbResult last = results.LastOrDefault(); + FcbResult last = results[^1]; Assert.AreEqual(262.47m, last.UpperBand); Assert.AreEqual(229.42m, last.LowerBand); } @@ -86,15 +81,14 @@ public void Condense() [TestMethod] public void Removed() { - List results = quotes - .GetFcb(2) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToFcb() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 5, results.Count); - FcbResult last = results.LastOrDefault(); + FcbResult last = results[^1]; Assert.AreEqual(262.47m, last.UpperBand); Assert.AreEqual(229.42m, last.LowerBand); } @@ -103,5 +97,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetFcb(1)); + => Quotes.ToFcb(1)); } diff --git a/tests/indicators/e-k/FisherTransform/FisherTransform.Tests.cs b/tests/indicators/e-k/FisherTransform/FisherTransform.StaticSeries.Tests.cs similarity index 63% rename from tests/indicators/e-k/FisherTransform/FisherTransform.Tests.cs rename to tests/indicators/e-k/FisherTransform/FisherTransform.StaticSeries.Tests.cs index 5ea407a37..e826b2b5d 100644 --- a/tests/indicators/e-k/FisherTransform/FisherTransform.Tests.cs +++ b/tests/indicators/e-k/FisherTransform/FisherTransform.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class FisherTransformTests : TestBase +public class FisherTransform : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetFisherTransform(10) - .ToList(); + IReadOnlyList results = Quotes + .ToFisherTransform(); // proper quantities Assert.AreEqual(502, results.Count); @@ -47,35 +46,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetFisherTransform(10) - .ToList(); + .ToFisherTransform(); Assert.AreEqual(502, results.Count); Assert.AreEqual(501, results.Count(x => x.Fisher != 0)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetFisherTransform(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Fisher is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetFisherTransform(10) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToFisherTransform(); Assert.AreEqual(502, results.Count); Assert.AreEqual(501, results.Count(x => x.Fisher != 0)); @@ -84,38 +70,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetFisherTransform(10) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToFisherTransform() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(493, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetFisherTransform(9) - .ToList(); + IReadOnlyList r = BadQuotes + .ToFisherTransform(9); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Fisher is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Fisher is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetFisherTransform() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToFisherTransform(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetFisherTransform() - .ToList(); + IReadOnlyList r1 = Onequote + .ToFisherTransform(); Assert.AreEqual(1, r1.Count); } @@ -124,5 +106,5 @@ public void NoQuotes() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetFisherTransform(0)); + => Quotes.ToFisherTransform(0)); } diff --git a/tests/indicators/e-k/ForceIndex/ForceIndex.Tests.cs b/tests/indicators/e-k/ForceIndex/ForceIndex.StaticSeries.Tests.cs similarity index 58% rename from tests/indicators/e-k/ForceIndex/ForceIndex.Tests.cs rename to tests/indicators/e-k/ForceIndex/ForceIndex.StaticSeries.Tests.cs index 0c6c37ea1..0e9d54eed 100644 --- a/tests/indicators/e-k/ForceIndex/ForceIndex.Tests.cs +++ b/tests/indicators/e-k/ForceIndex/ForceIndex.StaticSeries.Tests.cs @@ -1,12 +1,12 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class ForceIndexTests : TestBase +public class ForceIndex : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List r = quotes.GetForceIndex(13).ToList(); + IReadOnlyList r = Quotes.ToForceIndex(13).ToList(); // proper quantities Assert.AreEqual(502, r.Count); @@ -25,38 +25,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetForceIndex(13) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToForceIndex(13) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(480, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetForceIndex(2) - .ToList(); + IReadOnlyList r = BadQuotes + .ToForceIndex(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.ForceIndex is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.ForceIndex is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetForceIndex(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToForceIndex(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetForceIndex(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToForceIndex(5); Assert.AreEqual(1, r1.Count); } @@ -64,15 +60,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetForceIndex(13) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToForceIndex(13) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (13 + 100), results.Count); - ForceIndexResult last = results.LastOrDefault(); + ForceIndexResult last = results[^1]; Assert.AreEqual(-16824018.428, Math.Round(last.ForceIndex.Value, 3)); } @@ -80,5 +75,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetForceIndex(0)); + => Quotes.ToForceIndex(0)); } diff --git a/tests/indicators/e-k/Fractal/Fractal.Tests.cs b/tests/indicators/e-k/Fractal/Fractal.StaticSeries.Tests.cs similarity index 75% rename from tests/indicators/e-k/Fractal/Fractal.Tests.cs rename to tests/indicators/e-k/Fractal/Fractal.StaticSeries.Tests.cs index fcc7685f9..6085d67b7 100644 --- a/tests/indicators/e-k/Fractal/Fractal.Tests.cs +++ b/tests/indicators/e-k/Fractal/Fractal.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class FractalTests : TestBase +public class Fractal : StaticSeriesTestBase { [TestMethod] - public void StandardSpan2() + public override void Standard() // Span 2 { - List results = quotes - .GetFractal(2, EndType.HighLow) - .ToList(); + IReadOnlyList results = Quotes + .ToFractal(); // proper quantities Assert.AreEqual(502, results.Count); @@ -44,9 +43,8 @@ public void StandardSpan2() [TestMethod] public void StandardSpan4() { - List results = quotes - .GetFractal(4, 4, EndType.HighLow) - .ToList(); + IReadOnlyList results = Quotes + .ToFractal(4, 4); // proper quantities Assert.AreEqual(502, results.Count); @@ -80,27 +78,24 @@ public void StandardSpan4() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetFractal() - .ToList(); + IReadOnlyList r = BadQuotes + .ToFractal(); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetFractal() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToFractal(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetFractal() - .ToList(); + IReadOnlyList r1 = Onequote + .ToFractal(); Assert.AreEqual(1, r1.Count); } @@ -108,17 +103,16 @@ public void NoQuotes() [TestMethod] public void Condense() { - List r = quotes - .GetFractal() - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToFractal() + .Condense(); - Assert.AreEqual(129, r.Count); + Assert.AreEqual(129, results.Count); } // bad window span [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetFractal(1)); + => Quotes.ToFractal(1)); } diff --git a/tests/indicators/e-k/Gator/Gator.Tests.cs b/tests/indicators/e-k/Gator/Gator.StaticSeries.Tests.cs similarity index 81% rename from tests/indicators/e-k/Gator/Gator.Tests.cs rename to tests/indicators/e-k/Gator/Gator.StaticSeries.Tests.cs index bb7f44816..3c448b971 100644 --- a/tests/indicators/e-k/Gator/Gator.Tests.cs +++ b/tests/indicators/e-k/Gator/Gator.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class GatorTests : TestBase +public class Gator : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetGator() - .ToList(); + IReadOnlyList results = Quotes + .ToGator(); // proper quantities Assert.AreEqual(502, results.Count); @@ -76,10 +75,9 @@ public void Standard() [TestMethod] public void FromAlligator() { - List results = quotes - .GetAlligator() - .GetGator() - .ToList(); + IReadOnlyList results = Quotes + .ToAlligator() + .ToGator(); // proper quantities Assert.AreEqual(502, results.Count); @@ -145,63 +143,47 @@ public void FromAlligator() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetGator() - .ToList(); + .ToGator(); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Upper != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetGator() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Upper is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetGator() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToGator(); Assert.AreEqual(502, results.Count); Assert.AreEqual(481, results.Count(x => x.Upper != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetGator() - .ToList(); + IReadOnlyList r = BadQuotes + .ToGator(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Upper is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Upper is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetGator() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToGator(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetGator() - .ToList(); + IReadOnlyList r1 = Onequote + .ToGator(); Assert.AreEqual(1, r1.Count); } @@ -209,15 +191,14 @@ public void NoQuotes() [TestMethod] public void Condense() { - List results = quotes - .GetGator() - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToGator() + .Condense(); // assertions Assert.AreEqual(490, results.Count); - GatorResult last = results.LastOrDefault(); + GatorResult last = results[^1]; Assert.AreEqual(7.4538, Math.Round(last.Upper.Value, 4)); Assert.AreEqual(-9.2399, Math.Round(last.Lower.Value, 4)); Assert.IsTrue(last.UpperIsExpanding); @@ -227,15 +208,14 @@ public void Condense() [TestMethod] public void Removed() { - List results = quotes - .GetGator() - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToGator() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 150, results.Count); - GatorResult last = results.LastOrDefault(); + GatorResult last = results[^1]; Assert.AreEqual(7.4538, Math.Round(last.Upper.Value, 4)); Assert.AreEqual(-9.2399, Math.Round(last.Lower.Value, 4)); Assert.IsTrue(last.UpperIsExpanding); diff --git a/tests/indicators/e-k/HeikinAshi/HeikinAshi.StaticSeries.Tests.cs b/tests/indicators/e-k/HeikinAshi/HeikinAshi.StaticSeries.Tests.cs new file mode 100644 index 000000000..6d452026e --- /dev/null +++ b/tests/indicators/e-k/HeikinAshi/HeikinAshi.StaticSeries.Tests.cs @@ -0,0 +1,54 @@ +namespace StaticSeries; + +[TestClass] +public class HeikinAshi : StaticSeriesTestBase +{ + [TestMethod] + public override void Standard() + { + IReadOnlyList results = Quotes + .ToHeikinAshi(); + + // proper quantities + Assert.AreEqual(502, results.Count); + + // sample value + HeikinAshiResult r = results[501]; + Assert.AreEqual(241.3018m, r.Open.Round(4)); + Assert.AreEqual(245.54m, r.High.Round(4)); + Assert.AreEqual(241.3018m, r.Low.Round(4)); + Assert.AreEqual(244.6525m, r.Close.Round(4)); + Assert.AreEqual(147031456m, r.Volume); + } + + [TestMethod] + public void UseAsQuotes() + { + IReadOnlyList haQuotes = Quotes.ToHeikinAshi(); + IReadOnlyList haSma = haQuotes.ToSma(5); + Assert.AreEqual(498, haSma.Count(x => x.Sma != null)); + } + + [TestMethod] + public override void BadData() + { + IReadOnlyList r = BadQuotes + .ToHeikinAshi(); + + Assert.AreEqual(502, r.Count); + } + + [TestMethod] + public override void NoQuotes() + { + IReadOnlyList r0 = Noquotes + .ToHeikinAshi(); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = Onequote + .ToHeikinAshi(); + + Assert.AreEqual(1, r1.Count); + } +} diff --git a/tests/indicators/e-k/HeikinAshi/HeikinAshi.Tests.cs b/tests/indicators/e-k/HeikinAshi/HeikinAshi.Tests.cs deleted file mode 100644 index cfff18c7a..000000000 --- a/tests/indicators/e-k/HeikinAshi/HeikinAshi.Tests.cs +++ /dev/null @@ -1,83 +0,0 @@ -namespace Tests.Indicators; - -[TestClass] -public class HeikinAshiTests : TestBase -{ - [TestMethod] - public void Standard() - { - List results = quotes - .GetHeikinAshi() - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - - // sample value - HeikinAshiResult r = results[501]; - Assert.AreEqual(241.3018m, r.Open.Round(4)); - Assert.AreEqual(245.54m, r.High.Round(4)); - Assert.AreEqual(241.3018m, r.Low.Round(4)); - Assert.AreEqual(244.6525m, r.Close.Round(4)); - Assert.AreEqual(147031456m, r.Volume); - } - - [TestMethod] - public void UseAsQuotes() - { - IEnumerable haQuotes = quotes.GetHeikinAshi(); - IEnumerable haSma = haQuotes.GetSma(5); - Assert.AreEqual(498, haSma.Count(x => x.Sma != null)); - } - - [TestMethod] - public void ToQuotes() - { - List results = quotes - .GetHeikinAshi() - .ToList(); - - List haQuotes = results - .ToQuotes() - .ToList(); - - for (int i = 0; i < results.Count; i++) - { - HeikinAshiResult r = results[i]; - Quote q = haQuotes[i]; - - Assert.AreEqual(r.Date, q.Date); - Assert.AreEqual(r.Open, q.Open); - Assert.AreEqual(r.High, q.High); - Assert.AreEqual(r.Low, q.Low); - Assert.AreEqual(r.Close, q.Close); - Assert.AreEqual(r.Volume, q.Volume); - } - } - - [TestMethod] - public void BadData() - { - List r = badQuotes - .GetHeikinAshi() - .ToList(); - - Assert.AreEqual(502, r.Count); - } - - [TestMethod] - public void NoQuotes() - { - List r0 = noquotes - .GetHeikinAshi() - .ToList(); - - Assert.AreEqual(0, r0.Count); - - List r1 = onequote - .GetHeikinAshi() - .ToList(); - - Assert.AreEqual(1, r1.Count); - } -} diff --git a/tests/indicators/e-k/Hma/Hma.Tests.cs b/tests/indicators/e-k/Hma/Hma.StaticSeries.Tests.cs similarity index 50% rename from tests/indicators/e-k/Hma/Hma.Tests.cs rename to tests/indicators/e-k/Hma/Hma.StaticSeries.Tests.cs index d773a8e69..a0bfafe45 100644 --- a/tests/indicators/e-k/Hma/Hma.Tests.cs +++ b/tests/indicators/e-k/Hma/Hma.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class HmaTests : TestBase +public class Hma : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetHma(20) - .ToList(); + IReadOnlyList results = Quotes + .ToHma(20); // proper quantities Assert.AreEqual(502, results.Count); @@ -23,35 +22,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetHma(20) - .ToList(); + .ToHma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(480, results.Count(x => x.Hma != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetHma(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Hma is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetHma(19) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToHma(19); Assert.AreEqual(502, results.Count); Assert.AreEqual(480, results.Count(x => x.Hma != null)); @@ -60,38 +46,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetHma(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToHma(20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(471, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetHma(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToHma(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Hma is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Hma is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetHma(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToHma(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetHma(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToHma(5); Assert.AreEqual(1, r1.Count); } @@ -99,15 +81,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetHma(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToHma(20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(480, results.Count); - HmaResult last = results.LastOrDefault(); + HmaResult last = results[^1]; Assert.AreEqual(235.6972, last.Hma.Round(4)); } @@ -115,5 +96,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetHma(1)); + => Quotes.ToHma(1)); } diff --git a/tests/indicators/e-k/HtTrendline/HtTrendline.Tests.cs b/tests/indicators/e-k/HtTrendline/HtTrendline.StaticSeries.Tests.cs similarity index 65% rename from tests/indicators/e-k/HtTrendline/HtTrendline.Tests.cs rename to tests/indicators/e-k/HtTrendline/HtTrendline.StaticSeries.Tests.cs index c20807ea3..e15b2268c 100644 --- a/tests/indicators/e-k/HtTrendline/HtTrendline.Tests.cs +++ b/tests/indicators/e-k/HtTrendline/HtTrendline.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class HtTrendlineTests : TestBase +public class HtTrendline : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetHtTrendline() - .ToList(); + IReadOnlyList results = Quotes + .ToHtTrendline(); // proper quantities // should always be the same number of results as there is quotes @@ -58,35 +57,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetHtTrendline() - .ToList(); + .ToHtTrendline(); Assert.AreEqual(502, results.Count); Assert.AreEqual(502, results.Count(x => x.Trendline != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetHtTrendline() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Trendline is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetHtTrendline() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToHtTrendline(); Assert.AreEqual(502, results.Count); Assert.AreEqual(501, results.Count(x => x.Trendline != null)); @@ -95,38 +81,35 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetHtTrendline() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToHtTrendline() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(493, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetHtTrendline() - .ToList(); + IReadOnlyList r = BadQuotes + .ToHtTrendline(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Trendline is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Trendline is double.NaN)); } [TestMethod] public void Removed() { - List results = quotes - .GetHtTrendline() - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToHtTrendline() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 100, results.Count); - HtlResult last = results.LastOrDefault(); + HtlResult last = results[^1]; Assert.AreEqual(252.2172, last.Trendline.Round(4)); Assert.AreEqual(242.3435, last.SmoothPrice.Round(4)); } @@ -134,27 +117,24 @@ public void Removed() [TestMethod] public void PennyData() { - IEnumerable penny = TestData.GetPenny(); + IReadOnlyList penny = Data.GetPenny(); - List r = penny - .GetHtTrendline() - .ToList(); + IReadOnlyList r = penny + .ToHtTrendline(); Assert.AreEqual(533, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetHtTrendline() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToHtTrendline(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetHtTrendline() - .ToList(); + IReadOnlyList r1 = Onequote + .ToHtTrendline(); Assert.AreEqual(1, r1.Count); } diff --git a/tests/indicators/e-k/Hurst/Hurst.Tests.cs b/tests/indicators/e-k/Hurst/Hurst.StaticSeries.Tests.cs similarity index 50% rename from tests/indicators/e-k/Hurst/Hurst.Tests.cs rename to tests/indicators/e-k/Hurst/Hurst.StaticSeries.Tests.cs index 6cb517461..f596df5c5 100644 --- a/tests/indicators/e-k/Hurst/Hurst.Tests.cs +++ b/tests/indicators/e-k/Hurst/Hurst.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class HurstTests : TestBase +public class Hurst : StaticSeriesTestBase { [TestMethod] - public void StandardLong() + public override void Standard() { - List results = longestQuotes - .GetHurst(longestQuotes.Count() - 1) - .ToList(); + IReadOnlyList results = LongestQuotes + .ToHurst(LongestQuotes.Count - 1); // assertions @@ -22,35 +21,22 @@ public void StandardLong() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetHurst(100) - .ToList(); + .ToHurst(); Assert.AreEqual(502, results.Count); Assert.AreEqual(402, results.Count(x => x.HurstExponent != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetHurst(100) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.HurstExponent is double and double.NaN)); - } - [TestMethod] public void Chainor() { - List results = quotes - .GetHurst(100) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToHurst() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(393, results.Count(x => x.Sma != null)); @@ -59,38 +45,34 @@ public void Chainor() [TestMethod] public void Chainee() { - List results = quotes - .GetSma(10) - .GetHurst(100) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(10) + .ToHurst(); Assert.AreEqual(502, results.Count); Assert.AreEqual(393, results.Count(x => x.HurstExponent != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetHurst(150) - .ToList(); + IReadOnlyList r = BadQuotes + .ToHurst(150); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.HurstExponent is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.HurstExponent is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetHurst() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToHurst(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetHurst() - .ToList(); + IReadOnlyList r1 = Onequote + .ToHurst(); Assert.AreEqual(1, r1.Count); } @@ -98,14 +80,13 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = longestQuotes.GetHurst(longestQuotes.Count() - 1) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = LongestQuotes.ToHurst(LongestQuotes.Count - 1) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(1, results.Count); - HurstResult last = results.LastOrDefault(); + HurstResult last = results[^1]; Assert.AreEqual(0.483563, last.HurstExponent.Round(6)); } @@ -113,5 +94,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetHurst(19)); + => Quotes.ToHurst(19)); } diff --git a/tests/indicators/e-k/Ichimoku/Ichimoku.Tests.cs b/tests/indicators/e-k/Ichimoku/Ichimoku.StaticSeries.Tests.cs similarity index 69% rename from tests/indicators/e-k/Ichimoku/Ichimoku.Tests.cs rename to tests/indicators/e-k/Ichimoku/Ichimoku.StaticSeries.Tests.cs index 6974c481e..fd077faa0 100644 --- a/tests/indicators/e-k/Ichimoku/Ichimoku.Tests.cs +++ b/tests/indicators/e-k/Ichimoku/Ichimoku.StaticSeries.Tests.cs @@ -1,18 +1,17 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class IchimokuTests : TestBase +public class Ichimoku : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int tenkanPeriods = 9; int kijunPeriods = 26; int senkouBPeriods = 52; - List results = quotes - .GetIchimoku(tenkanPeriods, kijunPeriods, senkouBPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToIchimoku(tenkanPeriods, kijunPeriods, senkouBPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -55,35 +54,31 @@ public void Standard() [TestMethod] public void Extended() { - List r = quotes - .GetIchimoku(3, 13, 40, 0, 0) - .ToList(); + IReadOnlyList results = Quotes + .GetIchimoku(3, 13, 40, 0, 0); - Assert.AreEqual(502, r.Count); + Assert.AreEqual(502, results.Count); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetIchimoku() - .ToList(); + IReadOnlyList r = BadQuotes + .ToIchimoku(); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetIchimoku() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToIchimoku(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetIchimoku() - .ToList(); + IReadOnlyList r1 = Onequote + .ToIchimoku(); Assert.AreEqual(1, r1.Count); } @@ -91,12 +86,11 @@ public void NoQuotes() [TestMethod] public void Condense() { - List r = quotes - .GetIchimoku() - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToIchimoku() + .Condense(); - Assert.AreEqual(502, r.Count); + Assert.AreEqual(502, results.Count); } [TestMethod] @@ -104,24 +98,24 @@ public void Exceptions() { // bad signal period Assert.ThrowsException(() => - quotes.GetIchimoku(0)); + Quotes.ToIchimoku(0)); // bad short span period Assert.ThrowsException(() => - quotes.GetIchimoku(9, 0, 52)); + Quotes.ToIchimoku(9, 0)); // bad long span period Assert.ThrowsException(() => - quotes.GetIchimoku(9, 26, 26)); + Quotes.ToIchimoku(9, 26, 26)); // invalid offsets Assert.ThrowsException(() => - quotes.GetIchimoku(9, 26, 52, -1)); + Quotes.GetIchimoku(9, 26, 52, -1)); Assert.ThrowsException(() => - quotes.GetIchimoku(9, 26, 52, -1, 12)); + Quotes.GetIchimoku(9, 26, 52, -1, 12)); Assert.ThrowsException(() => - quotes.GetIchimoku(9, 26, 52, 12, -1)); + Quotes.GetIchimoku(9, 26, 52, 12, -1)); } } diff --git a/tests/indicators/e-k/Kama/Kama.Tests.cs b/tests/indicators/e-k/Kama/Kama.StaticSeries.Tests.cs similarity index 53% rename from tests/indicators/e-k/Kama/Kama.Tests.cs rename to tests/indicators/e-k/Kama/Kama.StaticSeries.Tests.cs index 3375f0fb4..bbd19115c 100644 --- a/tests/indicators/e-k/Kama/Kama.Tests.cs +++ b/tests/indicators/e-k/Kama/Kama.StaticSeries.Tests.cs @@ -1,84 +1,70 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class KamaTests : TestBase +public class Kama : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int erPeriods = 10; int fastPeriods = 2; int slowPeriods = 30; - List results = quotes - .GetKama(erPeriods, fastPeriods, slowPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToKama(erPeriods, fastPeriods, slowPeriods); // proper quantities Assert.AreEqual(502, results.Count); - Assert.AreEqual(492, results.Count(x => x.ER != null)); + Assert.AreEqual(492, results.Count(x => x.Er != null)); Assert.AreEqual(493, results.Count(x => x.Kama != null)); // sample values KamaResult r1 = results[8]; - Assert.AreEqual(null, r1.ER); + Assert.AreEqual(null, r1.Er); Assert.AreEqual(null, r1.Kama); KamaResult r2 = results[9]; - Assert.AreEqual(null, r2.ER); + Assert.AreEqual(null, r2.Er); Assert.AreEqual(213.7500, r2.Kama.Round(4)); KamaResult r3 = results[10]; - Assert.AreEqual(0.2465, r3.ER.Round(4)); + Assert.AreEqual(0.2465, r3.Er.Round(4)); Assert.AreEqual(213.7713, r3.Kama.Round(4)); KamaResult r4 = results[24]; - Assert.AreEqual(0.2136, r4.ER.Round(4)); + Assert.AreEqual(0.2136, r4.Er.Round(4)); Assert.AreEqual(214.7423, r4.Kama.Round(4)); KamaResult r5 = results[149]; - Assert.AreEqual(0.3165, r5.ER.Round(4)); + Assert.AreEqual(0.3165, r5.Er.Round(4)); Assert.AreEqual(235.5510, r5.Kama.Round(4)); KamaResult r6 = results[249]; - Assert.AreEqual(0.3182, r6.ER.Round(4)); + Assert.AreEqual(0.3182, r6.Er.Round(4)); Assert.AreEqual(256.0898, r6.Kama.Round(4)); KamaResult r7 = results[501]; - Assert.AreEqual(0.2214, r7.ER.Round(4)); + Assert.AreEqual(0.2214, r7.Er.Round(4)); Assert.AreEqual(240.1138, r7.Kama.Round(4)); } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetKama() - .ToList(); + .ToKama(); Assert.AreEqual(502, results.Count); Assert.AreEqual(493, results.Count(x => x.Kama != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetKama() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Kama is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetKama() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToKama(); Assert.AreEqual(502, results.Count); Assert.AreEqual(492, results.Count(x => x.Kama != null)); @@ -87,38 +73,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetKama() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToKama() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(484, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetKama() - .ToList(); + IReadOnlyList r = BadQuotes + .ToKama(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Kama is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Kama is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetKama() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToKama(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetKama() - .ToList(); + IReadOnlyList r1 = Onequote + .ToKama(); Assert.AreEqual(1, r1.Count); } @@ -130,16 +112,15 @@ public void Removed() int fastPeriods = 2; int slowPeriods = 30; - List results = quotes - .GetKama(erPeriods, fastPeriods, slowPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToKama(erPeriods, fastPeriods, slowPeriods) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - Math.Max(erPeriods + 100, erPeriods * 10), results.Count); - KamaResult last = results.LastOrDefault(); - Assert.AreEqual(0.2214, last.ER.Round(4)); + KamaResult last = results[^1]; + Assert.AreEqual(0.2214, last.Er.Round(4)); Assert.AreEqual(240.1138, last.Kama.Round(4)); } @@ -148,14 +129,14 @@ public void Exceptions() { // bad ER period Assert.ThrowsException(() => - quotes.GetKama(0, 2, 30)); + Quotes.ToKama(0)); // bad fast period Assert.ThrowsException(() => - quotes.GetKama(10, 0, 30)); + Quotes.ToKama(10, 0)); // bad slow period Assert.ThrowsException(() => - quotes.GetKama(10, 5, 5)); + Quotes.ToKama(10, 5, 5)); } } diff --git a/tests/indicators/e-k/Keltner/Keltner.Tests.cs b/tests/indicators/e-k/Keltner/Keltner.StaticSeries.Tests.cs similarity index 69% rename from tests/indicators/e-k/Keltner/Keltner.Tests.cs rename to tests/indicators/e-k/Keltner/Keltner.StaticSeries.Tests.cs index 05c6e88e4..ce3dc3c2f 100644 --- a/tests/indicators/e-k/Keltner/Keltner.Tests.cs +++ b/tests/indicators/e-k/Keltner/Keltner.StaticSeries.Tests.cs @@ -1,18 +1,17 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class KeltnerTests : TestBase +public class Keltner : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int emaPeriods = 20; int multiplier = 2; int atrPeriods = 10; - List results = quotes - .GetKeltner(emaPeriods, multiplier, atrPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToKeltner(emaPeriods, multiplier, atrPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -38,28 +37,25 @@ public void Standard() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetKeltner(10, 3, 15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToKeltner(10, 3, 15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.UpperBand is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.UpperBand is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetKeltner() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToKeltner(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetKeltner() - .ToList(); + IReadOnlyList r1 = Onequote + .ToKeltner(); Assert.AreEqual(1, r1.Count); } @@ -71,15 +67,14 @@ public void Condense() int multiplier = 2; int atrPeriods = 10; - List results = quotes - .GetKeltner(emaPeriods, multiplier, atrPeriods) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToKeltner(emaPeriods, multiplier, atrPeriods) + .Condense(); // assertions Assert.AreEqual(483, results.Count); - KeltnerResult last = results.LastOrDefault(); + KeltnerResult last = results[^1]; Assert.AreEqual(262.1873, last.UpperBand.Round(4)); Assert.AreEqual(249.3519, last.Centerline.Round(4)); Assert.AreEqual(236.5165, last.LowerBand.Round(4)); @@ -94,15 +89,14 @@ public void Removed() int atrPeriods = 10; int n = Math.Max(emaPeriods, atrPeriods); - List results = quotes - .GetKeltner(emaPeriods, multiplier, atrPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToKeltner(emaPeriods, multiplier, atrPeriods) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - Math.Max(2 * n, n + 100), results.Count); - KeltnerResult last = results.LastOrDefault(); + KeltnerResult last = results[^1]; Assert.AreEqual(262.1873, last.UpperBand.Round(4)); Assert.AreEqual(249.3519, last.Centerline.Round(4)); Assert.AreEqual(236.5165, last.LowerBand.Round(4)); @@ -114,14 +108,14 @@ public void Exceptions() { // bad EMA period Assert.ThrowsException(() => - quotes.GetKeltner(1, 2, 10)); + Quotes.ToKeltner(1)); // bad ATR period Assert.ThrowsException(() => - quotes.GetKeltner(20, 2, 1)); + Quotes.ToKeltner(20, 2, 1)); // bad multiplier Assert.ThrowsException(() => - quotes.GetKeltner(20, 0, 10)); + Quotes.ToKeltner(20, 0)); } } diff --git a/tests/indicators/e-k/Kvo/Kvo.Tests.cs b/tests/indicators/e-k/Kvo/Kvo.StaticSeries.Tests.cs similarity index 71% rename from tests/indicators/e-k/Kvo/Kvo.Tests.cs rename to tests/indicators/e-k/Kvo/Kvo.StaticSeries.Tests.cs index 80e1c7869..34dccb79f 100644 --- a/tests/indicators/e-k/Kvo/Kvo.Tests.cs +++ b/tests/indicators/e-k/Kvo/Kvo.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class KlingerTests : TestBase +public class Klinger : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = - quotes.GetKvo(34, 55, 13) - .ToList(); + IReadOnlyList results = + Quotes.ToKvo(); // proper quantities Assert.AreEqual(502, results.Count); @@ -48,38 +47,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetKvo() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToKvo() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(437, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetKvo() - .ToList(); + IReadOnlyList r = BadQuotes + .ToKvo(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Oscillator is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Oscillator is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetKvo() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToKvo(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetKvo() - .ToList(); + IReadOnlyList r1 = Onequote + .ToKvo(); Assert.AreEqual(1, r1.Count); } @@ -87,15 +82,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetKvo(34, 55, 13) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToKvo() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (55 + 150), results.Count); - KvoResult last = results.LastOrDefault(); + KvoResult last = results[^1]; Assert.AreEqual(-539224047, Math.Round(last.Oscillator.Value, 0)); Assert.AreEqual(-1548306127, Math.Round(last.Signal.Value, 0)); } @@ -105,14 +99,14 @@ public void Exceptions() { // bad fast period Assert.ThrowsException(() => - quotes.GetKvo(2)); + Quotes.ToKvo(2)); // bad slow period Assert.ThrowsException(() => - quotes.GetKvo(20, 20)); + Quotes.ToKvo(20, 20)); // bad signal period Assert.ThrowsException(() => - quotes.GetKvo(34, 55, 0)); + Quotes.ToKvo(34, 55, 0)); } } diff --git a/tests/indicators/m-r/MaEnvelopes/MaEnvelopes.Tests.cs b/tests/indicators/m-r/MaEnvelopes/MaEnvelopes.StaticSeries.Tests.cs similarity index 74% rename from tests/indicators/m-r/MaEnvelopes/MaEnvelopes.Tests.cs rename to tests/indicators/m-r/MaEnvelopes/MaEnvelopes.StaticSeries.Tests.cs index d1da84130..13aa3ddd7 100644 --- a/tests/indicators/m-r/MaEnvelopes/MaEnvelopes.Tests.cs +++ b/tests/indicators/m-r/MaEnvelopes/MaEnvelopes.StaticSeries.Tests.cs @@ -1,14 +1,40 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class MaEnvelopesTests : TestBase +public class MaEnvelopes : StaticSeriesTestBase { + [TestMethod] + public override void Standard() // SMA + { + IReadOnlyList results = + Quotes.ToMaEnvelopes(20); + + // proper quantities + Assert.AreEqual(502, results.Count); + Assert.AreEqual(483, results.Count(x => x.Centerline != null)); + + // sample values + MaEnvelopeResult r1 = results[24]; + Assert.AreEqual(215.0310, r1.Centerline.Round(4)); + Assert.AreEqual(220.4068, r1.UpperEnvelope.Round(4)); + Assert.AreEqual(209.6552, r1.LowerEnvelope.Round(4)); + + MaEnvelopeResult r2 = results[249]; + Assert.AreEqual(255.5500, r2.Centerline.Round(4)); + Assert.AreEqual(261.9388, r2.UpperEnvelope.Round(4)); + Assert.AreEqual(249.16125, r2.LowerEnvelope.Round(5)); + + MaEnvelopeResult r3 = results[501]; + Assert.AreEqual(251.8600, r3.Centerline.Round(4)); + Assert.AreEqual(258.1565, r3.UpperEnvelope.Round(4)); + Assert.AreEqual(245.5635, r3.LowerEnvelope.Round(4)); + } + [TestMethod] public void Alma() { - List results = - quotes.GetMaEnvelopes(10, 2.5, MaType.ALMA) - .ToList(); + IReadOnlyList results = + Quotes.ToMaEnvelopes(10, 2.5, MaType.ALMA); // proper quantities Assert.AreEqual(502, results.Count); @@ -34,9 +60,8 @@ public void Alma() [TestMethod] public void Dema() { - List results = - quotes.GetMaEnvelopes(20, 2.5, MaType.DEMA) - .ToList(); + IReadOnlyList results = + Quotes.ToMaEnvelopes(20, 2.5, MaType.DEMA); // proper quantities Assert.AreEqual(502, results.Count); @@ -62,9 +87,8 @@ public void Dema() [TestMethod] public void Epma() { - List results = - quotes.GetMaEnvelopes(20, 2.5, MaType.EPMA) - .ToList(); + IReadOnlyList results = + Quotes.ToMaEnvelopes(20, 2.5, MaType.EPMA); // proper quantities Assert.AreEqual(502, results.Count); @@ -90,9 +114,8 @@ public void Epma() [TestMethod] public void Ema() { - List results = - quotes.GetMaEnvelopes(20, 2.5, MaType.EMA) - .ToList(); + IReadOnlyList results = + Quotes.ToMaEnvelopes(20, 2.5, MaType.EMA); // proper quantities Assert.AreEqual(502, results.Count); @@ -118,9 +141,8 @@ public void Ema() [TestMethod] public void Hma() { - List results = - quotes.GetMaEnvelopes(20, 2.5, MaType.HMA) - .ToList(); + IReadOnlyList results = + Quotes.ToMaEnvelopes(20, 2.5, MaType.HMA); // proper quantities Assert.AreEqual(502, results.Count); @@ -138,40 +160,11 @@ public void Hma() Assert.AreEqual(229.8048, r3.LowerEnvelope.Round(4)); } - [TestMethod] - public void Sma() - { - List results = - quotes.GetMaEnvelopes(20, 2.5, MaType.SMA) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(483, results.Count(x => x.Centerline != null)); - - // sample values - MaEnvelopeResult r1 = results[24]; - Assert.AreEqual(215.0310, r1.Centerline.Round(4)); - Assert.AreEqual(220.4068, r1.UpperEnvelope.Round(4)); - Assert.AreEqual(209.6552, r1.LowerEnvelope.Round(4)); - - MaEnvelopeResult r2 = results[249]; - Assert.AreEqual(255.5500, r2.Centerline.Round(4)); - Assert.AreEqual(261.9388, r2.UpperEnvelope.Round(4)); - Assert.AreEqual(249.16125, r2.LowerEnvelope.Round(5)); - - MaEnvelopeResult r3 = results[501]; - Assert.AreEqual(251.8600, r3.Centerline.Round(4)); - Assert.AreEqual(258.1565, r3.UpperEnvelope.Round(4)); - Assert.AreEqual(245.5635, r3.LowerEnvelope.Round(4)); - } - [TestMethod] public void Smma() { - List results = - quotes.GetMaEnvelopes(20, 2.5, MaType.SMMA) - .ToList(); + IReadOnlyList results = + Quotes.ToMaEnvelopes(20, 2.5, MaType.SMMA); // proper quantities Assert.AreEqual(502, results.Count); @@ -197,9 +190,8 @@ public void Smma() [TestMethod] public void Tema() { - List results = - quotes.GetMaEnvelopes(20, 2.5, MaType.TEMA) - .ToList(); + IReadOnlyList results = + Quotes.ToMaEnvelopes(20, 2.5, MaType.TEMA); // proper quantities Assert.AreEqual(502, results.Count); @@ -225,9 +217,8 @@ public void Tema() [TestMethod] public void Wma() { - List results = - quotes.GetMaEnvelopes(20, 2.5, MaType.WMA) - .ToList(); + IReadOnlyList results = + Quotes.ToMaEnvelopes(20, 2.5, MaType.WMA); // proper quantities Assert.AreEqual(502, results.Count); @@ -246,101 +237,93 @@ public void Wma() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetMaEnvelopes(10, 2.5, MaType.SMA) - .ToList(); + .ToMaEnvelopes(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(493, results.Count(x => x.Centerline != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetMaEnvelopes(8, 2.5, MaType.ALMA) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.UpperEnvelope is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetMaEnvelopes(10, 2.5, MaType.SMA) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToMaEnvelopes(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(492, results.Count(x => x.Centerline != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List a = badQuotes - .GetMaEnvelopes(5, 2.5, MaType.ALMA) - .ToList(); + IReadOnlyList a = BadQuotes + .ToMaEnvelopes(5, 2.5, MaType.ALMA); Assert.AreEqual(502, a.Count); - List d = badQuotes - .GetMaEnvelopes(5, 2.5, MaType.DEMA) - .ToList(); + IReadOnlyList d = BadQuotes + .ToMaEnvelopes(5, 2.5, MaType.DEMA); Assert.AreEqual(502, d.Count); - List p = badQuotes - .GetMaEnvelopes(5, 2.5, MaType.EPMA) - .ToList(); + IReadOnlyList p = BadQuotes + .ToMaEnvelopes(5, 2.5, MaType.EPMA); Assert.AreEqual(502, p.Count); - List e = badQuotes - .GetMaEnvelopes(5, 2.5, MaType.EMA) - .ToList(); + IReadOnlyList e = BadQuotes + .ToMaEnvelopes(5, 2.5, MaType.EMA); Assert.AreEqual(502, e.Count); - List h = badQuotes - .GetMaEnvelopes(5, 2.5, MaType.HMA) - .ToList(); + IReadOnlyList h = BadQuotes + .ToMaEnvelopes(5, 2.5, MaType.HMA); Assert.AreEqual(502, h.Count); - List s = badQuotes - .GetMaEnvelopes(5, 2.5, MaType.SMA) - .ToList(); + IReadOnlyList s = BadQuotes + .ToMaEnvelopes(5); Assert.AreEqual(502, s.Count); - List t = badQuotes - .GetMaEnvelopes(5, 2.5, MaType.TEMA) - .ToList(); + IReadOnlyList t = BadQuotes + .ToMaEnvelopes(5, 2.5, MaType.TEMA); Assert.AreEqual(502, t.Count); - List w = badQuotes - .GetMaEnvelopes(5, 2.5, MaType.WMA) - .ToList(); + IReadOnlyList w = BadQuotes + .ToMaEnvelopes(5, 2.5, MaType.WMA); Assert.AreEqual(502, w.Count); } + [TestMethod] + public override void NoQuotes() + { + IReadOnlyList r0 = Noquotes + .ToMaEnvelopes(10); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = Onequote + .ToMaEnvelopes(10); + + Assert.AreEqual(1, r1.Count); + } + [TestMethod] public void Condense() { - List r = quotes - .GetMaEnvelopes(20, 2.5, MaType.SMA) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToMaEnvelopes(20) + .Condense(); - Assert.AreEqual(483, r.Count); + Assert.AreEqual(483, results.Count); } [TestMethod] @@ -348,11 +331,11 @@ public void Exceptions() { // bad offset period Assert.ThrowsException(() => - quotes.GetMaEnvelopes(14, 0)); + Quotes.ToMaEnvelopes(14, 0)); // bad MA period Assert.ThrowsException(() => - quotes.GetMaEnvelopes(14, 5, MaType.KAMA)); + Quotes.ToMaEnvelopes(14, 5, MaType.KAMA)); // note: insufficient quotes is tested elsewhere } diff --git a/tests/indicators/m-r/Macd/Macd.Tests.cs b/tests/indicators/m-r/Macd/Macd.StaticSeries.Tests.cs similarity index 65% rename from tests/indicators/m-r/Macd/Macd.Tests.cs rename to tests/indicators/m-r/Macd/Macd.StaticSeries.Tests.cs index c4b135464..63f78539e 100644 --- a/tests/indicators/m-r/Macd/Macd.Tests.cs +++ b/tests/indicators/m-r/Macd/Macd.StaticSeries.Tests.cs @@ -1,18 +1,17 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class MacdTests : TestBase +public class Macd : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int fastPeriods = 12; int slowPeriods = 26; int signalPeriods = 9; - List results = - quotes.GetMacd(fastPeriods, slowPeriods, signalPeriods) - .ToList(); + IReadOnlyList results = + Quotes.ToMacd(fastPeriods, slowPeriods, signalPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -44,35 +43,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetMacd() - .ToList(); + .ToMacd(); Assert.AreEqual(502, results.Count); Assert.AreEqual(477, results.Count(x => x.Macd != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetMacd() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Macd is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetMacd() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToMacd(); Assert.AreEqual(502, results.Count); Assert.AreEqual(476, results.Count(x => x.Macd != null)); @@ -81,38 +67,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetMacd() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToMacd() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(468, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetMacd(10, 20, 5) - .ToList(); + IReadOnlyList r = BadQuotes + .ToMacd(10, 20, 5); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Macd is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Macd is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetMacd() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToMacd(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetMacd() - .ToList(); + IReadOnlyList r1 = Onequote + .ToMacd(); Assert.AreEqual(1, r1.Count); } @@ -124,15 +106,14 @@ public void Removed() int slowPeriods = 26; int signalPeriods = 9; - List results = quotes - .GetMacd(fastPeriods, slowPeriods, signalPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToMacd(fastPeriods, slowPeriods, signalPeriods) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (slowPeriods + signalPeriods + 250), results.Count); - MacdResult last = results.LastOrDefault(); + MacdResult last = results[^1]; Assert.AreEqual(-6.2198, last.Macd.Round(4)); Assert.AreEqual(-5.8569, last.Signal.Round(4)); Assert.AreEqual(-0.3629, last.Histogram.Round(4)); @@ -143,14 +124,14 @@ public void Exceptions() { // bad fast period Assert.ThrowsException(() => - quotes.GetMacd(0, 26, 9)); + Quotes.ToMacd(0)); // bad slow periods must be larger than faster period Assert.ThrowsException(() => - quotes.GetMacd(12, 12, 9)); + Quotes.ToMacd(12, 12)); // bad signal period Assert.ThrowsException(() => - quotes.GetMacd(12, 26, -1)); + Quotes.ToMacd(12, 26, -1)); } } diff --git a/tests/indicators/m-r/Mama/Mama.Tests.cs b/tests/indicators/m-r/Mama/Mama.StaticSeries.Tests.cs similarity index 63% rename from tests/indicators/m-r/Mama/Mama.Tests.cs rename to tests/indicators/m-r/Mama/Mama.StaticSeries.Tests.cs index 114426172..bdb748645 100644 --- a/tests/indicators/m-r/Mama/Mama.Tests.cs +++ b/tests/indicators/m-r/Mama/Mama.StaticSeries.Tests.cs @@ -1,17 +1,16 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class MamaTests : TestBase +public class Mama : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { double fastLimit = 0.5; double slowLimit = 0.05; - List results = quotes - .GetMama(fastLimit, slowLimit) - .ToList(); + IReadOnlyList results = Quotes + .ToMama(fastLimit, slowLimit); // proper quantities Assert.AreEqual(502, results.Count); @@ -48,35 +47,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetMama() - .ToList(); + .ToMama(); Assert.AreEqual(502, results.Count); Assert.AreEqual(497, results.Count(x => x.Mama != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetMama() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Mama is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetMama() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToMama(); Assert.AreEqual(502, results.Count); Assert.AreEqual(496, results.Count(x => x.Mama != null)); @@ -85,38 +71,32 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetMama() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToMama() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(488, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetMama() - .ToList(); + IReadOnlyList r = BadQuotes + .ToMama(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Mama is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Mama is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetMama() - .ToList(); + IReadOnlyList r0 = Noquotes.ToMama(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetMama() - .ToList(); + IReadOnlyList r1 = Onequote.ToMama(); Assert.AreEqual(1, r1.Count); } @@ -127,15 +107,14 @@ public void Removed() double fastLimit = 0.5; double slowLimit = 0.05; - List results = quotes - .GetMama(fastLimit, slowLimit) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToMama(fastLimit, slowLimit) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 50, results.Count); - MamaResult last = results.LastOrDefault(); + MamaResult last = results[^1]; Assert.AreEqual(244.1092, last.Mama.Round(4)); Assert.AreEqual(252.6139, last.Fama.Round(4)); } @@ -145,14 +124,14 @@ public void Exceptions() { // bad fast period (same as slow period) Assert.ThrowsException(() => - quotes.GetMama(0.5, 0.5)); + Quotes.ToMama(0.5, 0.5)); // bad fast period (cannot be 1 or more) Assert.ThrowsException(() => - quotes.GetMama(1, 0.5)); + Quotes.ToMama(1, 0.5)); // bad slow period Assert.ThrowsException(() => - quotes.GetMama(0.5, 0)); + Quotes.ToMama(0.5, 0)); } } diff --git a/tests/indicators/m-r/Marubozu/Marubozu.Tests.cs b/tests/indicators/m-r/Marubozu/Marubozu.StaticSeries.Tests.cs similarity index 66% rename from tests/indicators/m-r/Marubozu/Marubozu.Tests.cs rename to tests/indicators/m-r/Marubozu/Marubozu.StaticSeries.Tests.cs index bd3f17a0c..66ba37050 100644 --- a/tests/indicators/m-r/Marubozu/Marubozu.Tests.cs +++ b/tests/indicators/m-r/Marubozu/Marubozu.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class MarubozuTests : TestBase +public class Marubozu : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetMarubozu(95) - .ToList(); + IReadOnlyList results = Quotes + .ToMarubozu(); // proper quantities Assert.AreEqual(502, results.Count); @@ -41,27 +40,24 @@ public void Standard() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetMarubozu() - .ToList(); + IReadOnlyList r = BadQuotes + .ToMarubozu(); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetMarubozu() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToMarubozu(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetMarubozu() - .ToList(); + IReadOnlyList r1 = Onequote + .ToMarubozu(); Assert.AreEqual(1, r1.Count); } @@ -69,12 +65,11 @@ public void NoQuotes() [TestMethod] public void Condense() { - List r = quotes - .GetMarubozu(95) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToMarubozu() + .Condense(); - Assert.AreEqual(6, r.Count); + Assert.AreEqual(6, results.Count); } [TestMethod] @@ -82,9 +77,9 @@ public void Exceptions() { // bad minimum body percent values Assert.ThrowsException(() => - quotes.GetMarubozu(79.9)); + Quotes.ToMarubozu(79.9)); Assert.ThrowsException(() => - quotes.GetMarubozu(100.1)); + Quotes.ToMarubozu(100.1)); } } diff --git a/tests/indicators/m-r/Mfi/Mfi.Tests.cs b/tests/indicators/m-r/Mfi/Mfi.StaticSeries.Tests.cs similarity index 60% rename from tests/indicators/m-r/Mfi/Mfi.Tests.cs rename to tests/indicators/m-r/Mfi/Mfi.StaticSeries.Tests.cs index 85ecf769a..bd5be8d19 100644 --- a/tests/indicators/m-r/Mfi/Mfi.Tests.cs +++ b/tests/indicators/m-r/Mfi/Mfi.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class MfiTests : TestBase +public class Mfi : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetMfi(14) - .ToList(); + IReadOnlyList results = Quotes + .ToMfi(); // proper quantities Assert.AreEqual(502, results.Count); @@ -25,10 +24,9 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetMfi() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToMfi() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(479, results.Count(x => x.Sma != null)); @@ -39,9 +37,8 @@ public void SmallLookback() { int lookbackPeriods = 4; - List results = quotes - .GetMfi(lookbackPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToMfi(lookbackPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -56,28 +53,25 @@ public void SmallLookback() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetMfi(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToMfi(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Mfi is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Mfi is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetMfi() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToMfi(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetMfi() - .ToList(); + IReadOnlyList r1 = Onequote + .ToMfi(); Assert.AreEqual(1, r1.Count); } @@ -87,15 +81,14 @@ public void Removed() { int lookbackPeriods = 14; - List results = quotes - .GetMfi(lookbackPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToMfi(lookbackPeriods) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 14, results.Count); - MfiResult last = results.LastOrDefault(); + MfiResult last = results[^1]; Assert.AreEqual(39.9494, last.Mfi.Round(4)); } @@ -103,5 +96,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetMfi(1)); + => Quotes.ToMfi(1)); } diff --git a/tests/indicators/m-r/Obv/Obv.Calc.xlsx b/tests/indicators/m-r/Obv/Obv.Calc.xlsx index bae142741..f2df72246 100644 Binary files a/tests/indicators/m-r/Obv/Obv.Calc.xlsx and b/tests/indicators/m-r/Obv/Obv.Calc.xlsx differ diff --git a/tests/indicators/m-r/Obv/Obv.StaticSeries.Tests.cs b/tests/indicators/m-r/Obv/Obv.StaticSeries.Tests.cs new file mode 100644 index 000000000..a0127f678 --- /dev/null +++ b/tests/indicators/m-r/Obv/Obv.StaticSeries.Tests.cs @@ -0,0 +1,66 @@ +namespace StaticSeries; + +[TestClass] +public class Obv : StaticSeriesTestBase +{ + [TestMethod] + public override void Standard() + { + IReadOnlyList results = Quotes + .ToObv(); + + // proper quantities + Assert.AreEqual(502, results.Count); + + // sample values + ObvResult r1 = results[249]; + Assert.AreEqual(1780918888, r1.Obv); + + ObvResult r2 = results[501]; + Assert.AreEqual(539843504, r2.Obv); + } + + [TestMethod] + public void Chainor() + { + IReadOnlyList results = Quotes + .ToObv() + .ToSma(10); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(493, results.Count(x => x.Sma != null)); + } + + [TestMethod] + public override void BadData() + { + IReadOnlyList r = BadQuotes + .ToObv(); + + Assert.AreEqual(502, r.Count); + Assert.AreEqual(0, r.Count(x => double.IsNaN(x.Obv))); + } + + [TestMethod] + public void BigData() + { + IReadOnlyList r = BigQuotes + .ToObv(); + + Assert.AreEqual(1246, r.Count); + } + + [TestMethod] + public override void NoQuotes() + { + IReadOnlyList r0 = Noquotes + .ToObv(); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = Onequote + .ToObv(); + + Assert.AreEqual(1, r1.Count); + } +} diff --git a/tests/indicators/m-r/Obv/Obv.Tests.cs b/tests/indicators/m-r/Obv/Obv.Tests.cs deleted file mode 100644 index fe332d810..000000000 --- a/tests/indicators/m-r/Obv/Obv.Tests.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace Tests.Indicators; - -[TestClass] -public class ObvTests : TestBase -{ - [TestMethod] - public void Standard() - { - List results = quotes - .GetObv() - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(502, results.Count(x => x.ObvSma == null)); - - // sample values - ObvResult r1 = results[249]; - Assert.AreEqual(1780918888, r1.Obv); - Assert.AreEqual(null, r1.ObvSma); - - ObvResult r2 = results[501]; - Assert.AreEqual(539843504, r2.Obv); - Assert.AreEqual(null, r2.ObvSma); - } - - [TestMethod] - public void WithSma() - { - List results = quotes - .GetObv(20) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(482, results.Count(x => x.ObvSma != null)); - - // sample values - ObvResult r1 = results[501]; - Assert.AreEqual(539843504, r1.Obv); - Assert.AreEqual(1016208844.40, r1.ObvSma); - } - - [TestMethod] - public void Chainor() - { - List results = quotes - .GetObv() - .GetSma(10) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(493, results.Count(x => x.Sma != null)); - } - - [TestMethod] - public void BadData() - { - List r = badQuotes - .GetObv() - .ToList(); - - Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => double.IsNaN(x.Obv))); - } - - [TestMethod] - public void BigData() - { - List r = bigQuotes - .GetObv() - .ToList(); - - Assert.AreEqual(1246, r.Count); - } - - [TestMethod] - public void NoQuotes() - { - List r0 = noquotes - .GetObv() - .ToList(); - - Assert.AreEqual(0, r0.Count); - - List r1 = onequote - .GetObv() - .ToList(); - - Assert.AreEqual(1, r1.Count); - } - - // bad SMA period - [TestMethod] - public void Exceptions() - => Assert.ThrowsException(() - => quotes.GetObv(0)); -} diff --git a/tests/indicators/m-r/ParabolicSar/ParabolicSar.Tests.cs b/tests/indicators/m-r/ParabolicSar/ParabolicSar.StaticSeries.Tests.cs similarity index 71% rename from tests/indicators/m-r/ParabolicSar/ParabolicSar.Tests.cs rename to tests/indicators/m-r/ParabolicSar/ParabolicSar.StaticSeries.Tests.cs index 36494f1d5..e8eea6afa 100644 --- a/tests/indicators/m-r/ParabolicSar/ParabolicSar.Tests.cs +++ b/tests/indicators/m-r/ParabolicSar/ParabolicSar.StaticSeries.Tests.cs @@ -1,16 +1,16 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class ParabolicSarTests : TestBase +public class ParabolicSar : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { double acclerationStep = 0.02; double maxAccelerationFactor = 0.2; List results = - quotes.GetParabolicSar(acclerationStep, maxAccelerationFactor) + Quotes.ToParabolicSar(acclerationStep, maxAccelerationFactor) .ToList(); // proper quantities @@ -43,7 +43,7 @@ public void Extended() double initialStep = 0.01; List results = - quotes.GetParabolicSar( + Quotes.GetParabolicSar( acclerationStep, maxAccelerationFactor, initialStep) .ToList(); @@ -76,10 +76,9 @@ public void Extended() [TestMethod] public void Chainor() { - List results = quotes - .GetParabolicSar() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToParabolicSar() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(479, results.Count(x => x.Sma != null)); @@ -91,13 +90,14 @@ public void InsufficientQuotes() double acclerationStep = 0.02; double maxAccelerationFactor = 0.2; - IEnumerable insufficientQuotes = TestData.GetDefault() - .OrderBy(x => x.Date) - .Take(10); + List insufficientQuotes = Data.GetDefault() + .OrderBy(x => x.Timestamp) + .Take(10) + .ToList(); - List results = - insufficientQuotes.GetParabolicSar(acclerationStep, maxAccelerationFactor) - .ToList(); + IReadOnlyList results = + insufficientQuotes + .ToParabolicSar(acclerationStep, maxAccelerationFactor); // assertions @@ -107,28 +107,25 @@ public void InsufficientQuotes() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetParabolicSar(0.2, 0.2, 0.2) - .ToList(); + IReadOnlyList r = BadQuotes + .GetParabolicSar(0.2, 0.2, 0.2); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Sar is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Sar is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetParabolicSar() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToParabolicSar(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetParabolicSar() - .ToList(); + IReadOnlyList r1 = Onequote + .ToParabolicSar(); Assert.AreEqual(1, r1.Count); } @@ -139,15 +136,14 @@ public void Removed() double acclerationStep = 0.02; double maxAccelerationFactor = 0.2; - List results = quotes - .GetParabolicSar(acclerationStep, maxAccelerationFactor) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToParabolicSar(acclerationStep, maxAccelerationFactor) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(488, results.Count); - ParabolicSarResult last = results.LastOrDefault(); + ParabolicSarResult last = results[^1]; Assert.AreEqual(229.7662, last.Sar.Round(4)); Assert.AreEqual(false, last.IsReversal); } @@ -157,18 +153,18 @@ public void Exceptions() { // bad acceleration step Assert.ThrowsException(() => - quotes.GetParabolicSar(0, 1)); + Quotes.ToParabolicSar(0, 1)); // insufficient acceleration step Assert.ThrowsException(() => - quotes.GetParabolicSar(0.02, 0)); + Quotes.ToParabolicSar(0.02, 0)); // step larger than factor Assert.ThrowsException(() => - quotes.GetParabolicSar(6, 2)); + Quotes.ToParabolicSar(6, 2)); // insufficient initial factor Assert.ThrowsException(() => - quotes.GetParabolicSar(0.02, 0.5, 0)); + Quotes.GetParabolicSar(0.02, 0.5, 0)); } } diff --git a/tests/indicators/m-r/PivotPoints/PivotPoints.Tests.cs b/tests/indicators/m-r/PivotPoints/PivotPoints.StaticSeries.Tests.cs similarity index 89% rename from tests/indicators/m-r/PivotPoints/PivotPoints.Tests.cs rename to tests/indicators/m-r/PivotPoints/PivotPoints.StaticSeries.Tests.cs index aa8e93910..56ad3e87d 100644 --- a/tests/indicators/m-r/PivotPoints/PivotPoints.Tests.cs +++ b/tests/indicators/m-r/PivotPoints/PivotPoints.StaticSeries.Tests.cs @@ -1,17 +1,16 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class PivotPointsTests : TestBase +public class PivotPointz : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { PeriodSize periodSize = PeriodSize.Month; PivotPointType pointType = PivotPointType.Standard; - List results = quotes - .GetPivotPoints(periodSize, pointType) - .ToList(); + IReadOnlyList results = Quotes + .ToPivotPoints(periodSize, pointType); // proper quantities Assert.AreEqual(502, results.Count); @@ -91,9 +90,9 @@ public void Camarilla() PeriodSize periodSize = PeriodSize.Week; PivotPointType pointType = PivotPointType.Camarilla; - IEnumerable h = TestData.GetDefault(38); - List results = h.GetPivotPoints(periodSize, pointType) - .ToList(); + IReadOnlyList h = Data.GetDefault(38); + IReadOnlyList results + = h.ToPivotPoints(periodSize, pointType); // proper quantities Assert.AreEqual(38, results.Count); @@ -162,9 +161,8 @@ public void Demark() PeriodSize periodSize = PeriodSize.Month; PivotPointType pointType = PivotPointType.Demark; - List results = quotes - .GetPivotPoints(periodSize, pointType) - .ToList(); + IReadOnlyList results = Quotes + .ToPivotPoints(periodSize, pointType); // proper quantities Assert.AreEqual(502, results.Count); @@ -238,7 +236,7 @@ public void Demark() Assert.AreEqual(null, r6.S4); // special Demark case: test close = open - PivotPointsResult d1 = Indicator.GetPivotPointDemark(125, 200, 100, 125); + WindowPoint d1 = PivotPoints.GetPivotPointDemark(125, 200, 100, 125); Assert.AreEqual(550m / 4, d1.PP); } @@ -248,9 +246,9 @@ public void Fibonacci() PeriodSize periodSize = PeriodSize.OneHour; PivotPointType pointType = PivotPointType.Fibonacci; - IEnumerable h = TestData.GetIntraday(300); - List results = h.GetPivotPoints(periodSize, pointType) - .ToList(); + IReadOnlyList h = Data.GetIntraday(300); + IReadOnlyList results + = h.ToPivotPoints(periodSize, pointType); // proper quantities Assert.AreEqual(300, results.Count); @@ -320,9 +318,9 @@ public void Woodie() PeriodSize periodSize = PeriodSize.Day; PivotPointType pointType = PivotPointType.Woodie; - IEnumerable h = TestData.GetIntraday(); - List results = h.GetPivotPoints(periodSize, pointType) - .ToList(); + IReadOnlyList h = Data.GetIntraday(); + IReadOnlyList results + = h.ToPivotPoints(periodSize, pointType); // proper quantities Assert.AreEqual(1564, results.Count); @@ -378,27 +376,24 @@ public void Woodie() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetPivotPoints(PeriodSize.Week) - .ToList(); + IReadOnlyList r = BadQuotes + .ToPivotPoints(PeriodSize.Week); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetPivotPoints(PeriodSize.Week) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToPivotPoints(PeriodSize.Week); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetPivotPoints(PeriodSize.Week) - .ToList(); + IReadOnlyList r1 = Onequote + .ToPivotPoints(PeriodSize.Week); Assert.AreEqual(1, r1.Count); } @@ -409,15 +404,14 @@ public void Removed() PeriodSize periodSize = PeriodSize.Month; PivotPointType pointType = PivotPointType.Standard; - List results = quotes - .GetPivotPoints(periodSize, pointType) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToPivotPoints(periodSize, pointType) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(482, results.Count); - PivotPointsResult last = results.LastOrDefault(); + PivotPointsResult last = results[^1]; Assert.AreEqual(266.6767m, last.PP.Round(4)); Assert.AreEqual(258.9633m, last.S1.Round(4)); Assert.AreEqual(248.9667m, last.S2.Round(4)); @@ -434,12 +428,12 @@ public void Exceptions() { // bad pointtype size Assert.ThrowsException(() - => quotes - .GetPivotPoints(PeriodSize.Week, (PivotPointType)999)); + => Quotes + .ToPivotPoints(PeriodSize.Week, (PivotPointType)999)); // bad window size Assert.ThrowsException(() - => quotes - .GetPivotPoints(PeriodSize.ThreeMinutes)); + => Quotes + .ToPivotPoints(PeriodSize.ThreeMinutes)); } } diff --git a/tests/indicators/m-r/Pivots/Pivots.Tests.cs b/tests/indicators/m-r/Pivots/Pivots.StaticSeries.Tests.cs similarity index 74% rename from tests/indicators/m-r/Pivots/Pivots.Tests.cs rename to tests/indicators/m-r/Pivots/Pivots.StaticSeries.Tests.cs index cc53cb23b..ef3c9385f 100644 --- a/tests/indicators/m-r/Pivots/Pivots.Tests.cs +++ b/tests/indicators/m-r/Pivots/Pivots.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class PivotsTests : TestBase +public class Pivots : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetPivots(4, 4, 20, EndType.HighLow) - .ToList(); + IReadOnlyList results = Quotes + .ToPivots(4, 4); // proper quantities Assert.AreEqual(502, results.Count); @@ -38,18 +37,18 @@ public void Standard() PivotsResult r120 = results[120]; Assert.AreEqual(233.02m, r120.HighPoint); - Assert.AreEqual(PivotTrend.LH, r120.HighTrend); + Assert.AreEqual(PivotTrend.Lh, r120.HighTrend); Assert.AreEqual(233.02m, r120.HighLine); Assert.AreEqual(null, r120.LowPoint); - Assert.AreEqual(PivotTrend.LL, r120.LowTrend); + Assert.AreEqual(PivotTrend.Ll, r120.LowTrend); Assert.AreEqual(228.9671m, r120.LowLine.Round(4)); PivotsResult r180 = results[180]; Assert.AreEqual(239.74m, r180.HighPoint); - Assert.AreEqual(PivotTrend.HH, r180.HighTrend); + Assert.AreEqual(PivotTrend.Hh, r180.HighTrend); Assert.AreEqual(239.74m, r180.HighLine); Assert.AreEqual(null, r180.LowPoint); - Assert.AreEqual(PivotTrend.HL, r180.LowTrend); + Assert.AreEqual(PivotTrend.Hl, r180.LowTrend); Assert.AreEqual(236.7050m, r180.LowLine.Round(4)); PivotsResult r250 = results[250]; @@ -62,10 +61,10 @@ public void Standard() PivotsResult r472 = results[472]; Assert.AreEqual(null, r472.HighPoint); - Assert.AreEqual(PivotTrend.LH, r472.HighTrend); + Assert.AreEqual(PivotTrend.Lh, r472.HighTrend); Assert.AreEqual(274.14m, r472.HighLine); Assert.AreEqual(null, r472.LowPoint); - Assert.AreEqual(PivotTrend.HL, r472.LowTrend); + Assert.AreEqual(PivotTrend.Hl, r472.LowTrend); Assert.AreEqual(255.8078m, r472.LowLine.Round(4)); PivotsResult r497 = results[497]; @@ -86,27 +85,24 @@ public void Standard() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetPivots() - .ToList(); + IReadOnlyList r = BadQuotes + .ToPivots(); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetPivots() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToPivots(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetPivots() - .ToList(); + IReadOnlyList r1 = Onequote + .ToPivots(); Assert.AreEqual(1, r1.Count); } @@ -114,12 +110,11 @@ public void NoQuotes() [TestMethod] public void Condense() { - List r = quotes - .GetPivots(4, 4, 20, EndType.HighLow) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToPivots(4, 4) + .Condense(); - Assert.AreEqual(67, r.Count); + Assert.AreEqual(67, results.Count); } [TestMethod] @@ -127,14 +122,14 @@ public void Exceptions() { // bad left span Assert.ThrowsException(() => - quotes.GetPivots(1)); + Quotes.ToPivots(1)); // bad right span Assert.ThrowsException(() => - quotes.GetPivots(2, 1)); + Quotes.ToPivots(2, 1)); // bad lookback window Assert.ThrowsException(() => - quotes.GetPivots(20, 10, 20, EndType.Close)); + Quotes.ToPivots(20, 10, 20, EndType.Close)); } } diff --git a/tests/indicators/m-r/Pmo/Pmo.Calc.xlsx b/tests/indicators/m-r/Pmo/Pmo.Calc.xlsx index 4c77c998b..ba84b07a7 100644 Binary files a/tests/indicators/m-r/Pmo/Pmo.Calc.xlsx and b/tests/indicators/m-r/Pmo/Pmo.Calc.xlsx differ diff --git a/tests/indicators/m-r/Pmo/Pmo.Tests.cs b/tests/indicators/m-r/Pmo/Pmo.StaticSeries.Tests.cs similarity index 56% rename from tests/indicators/m-r/Pmo/Pmo.Tests.cs rename to tests/indicators/m-r/Pmo/Pmo.StaticSeries.Tests.cs index bc6110442..97f15b910 100644 --- a/tests/indicators/m-r/Pmo/Pmo.Tests.cs +++ b/tests/indicators/m-r/Pmo/Pmo.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class PmoTests : TestBase +public class Pmo : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetPmo(35, 20, 10) - .ToList(); + IReadOnlyList results = Quotes + .ToPmo(); // proper quantities Assert.AreEqual(502, results.Count); @@ -26,35 +25,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetPmo() - .ToList(); + .ToPmo(); Assert.AreEqual(502, results.Count); Assert.AreEqual(448, results.Count(x => x.Pmo != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetPmo() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Pmo is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetPmo() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToPmo(); Assert.AreEqual(502, results.Count); Assert.AreEqual(447, results.Count(x => x.Pmo != null)); @@ -63,38 +49,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetPmo() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToPmo() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(439, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetPmo(25, 15, 5) - .ToList(); + IReadOnlyList r = BadQuotes + .ToPmo(25, 15, 5); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Pmo is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Pmo is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetPmo() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToPmo(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetPmo() - .ToList(); + IReadOnlyList r1 = Onequote + .ToPmo(); Assert.AreEqual(1, r1.Count); } @@ -102,15 +84,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetPmo(35, 20, 10) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToPmo() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (35 + 20 + 250), results.Count); - PmoResult last = results.LastOrDefault(); + PmoResult last = results[^1]; Assert.AreEqual(-2.7016, last.Pmo.Round(4)); Assert.AreEqual(-2.3117, last.Signal.Round(4)); } @@ -120,14 +101,14 @@ public void Exceptions() { // bad time period Assert.ThrowsException(() => - quotes.GetPmo(1)); + Quotes.ToPmo(1)); // bad smoothing period Assert.ThrowsException(() => - quotes.GetPmo(5, 0)); + Quotes.ToPmo(5, 0)); // bad signal period Assert.ThrowsException(() => - quotes.GetPmo(5, 5, 0)); + Quotes.ToPmo(5, 5, 0)); } } diff --git a/tests/indicators/m-r/Prs/Prs.StaticSeries.Tests.cs b/tests/indicators/m-r/Prs/Prs.StaticSeries.Tests.cs new file mode 100644 index 000000000..debcc99da --- /dev/null +++ b/tests/indicators/m-r/Prs/Prs.StaticSeries.Tests.cs @@ -0,0 +1,109 @@ +namespace StaticSeries; + +[TestClass] +public class Prs : StaticSeriesTestBase +{ + [TestMethod] + public override void Standard() + { + int lookbackPeriods = 30; + + IReadOnlyList results = OtherQuotes + .ToPrs(Quotes, lookbackPeriods); + + // proper quantities + Assert.AreEqual(502, results.Count); + Assert.AreEqual(502, results.Count(x => x.Prs != null)); + + // sample values + PrsResult r1 = results[8]; + Assert.AreEqual(1.108340, r1.Prs.Round(6)); + Assert.AreEqual(null, r1.PrsPercent); + + PrsResult r2 = results[249]; + Assert.AreEqual(1.222373, r2.Prs.Round(6)); + Assert.AreEqual(-0.023089, r2.PrsPercent.Round(6)); + + PrsResult r3 = results[501]; + Assert.AreEqual(1.356817, r3.Prs.Round(6)); + Assert.AreEqual(0.037082, r3.PrsPercent.Round(6)); + } + + [TestMethod] + public void UseReusable() + { + IReadOnlyList results = OtherQuotes + .Use(CandlePart.Close) + .ToPrs(Quotes.Use(CandlePart.Close), 20); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(502, results.Count(x => x.Prs != null)); + } + + [TestMethod] + public void Chainor() + { + IReadOnlyList results = OtherQuotes + .ToPrs(Quotes, 20) + .ToSma(10); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(493, results.Count(x => x.Sma != null)); + } + + [TestMethod] + public void Chainee() + { + IReadOnlyList results = Quotes + .ToSma(2) + .ToPrs(OtherQuotes.ToSma(2), 20); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(501, results.Count(x => x.Prs != null)); + Assert.AreEqual(0, results.Count(x => x.Prs is double.NaN)); + } + + [TestMethod] + public override void BadData() + { + IReadOnlyList r = BadQuotes + .ToPrs(BadQuotes, 15); + + Assert.AreEqual(502, r.Count); + Assert.AreEqual(0, r.Count(x => x.Prs is double.NaN)); + } + + [TestMethod] + public override void NoQuotes() + { + IReadOnlyList r0 = Noquotes + .ToPrs(Noquotes); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = Onequote + .ToPrs(Onequote); + + Assert.AreEqual(1, r1.Count); + } + + [TestMethod] + public void Exceptions() + { + // bad lookback period + Assert.ThrowsException(() => + OtherQuotes.ToPrs(Quotes, 0)); + + // insufficient quotes + Assert.ThrowsException(() => + Data.GetCompare(13).ToPrs(Quotes, 14)); + + // insufficient eval quotes + Assert.ThrowsException(() => + Data.GetCompare(300).ToPrs(Quotes, 14)); + + // mismatch quotes + Assert.ThrowsException(() => + OtherQuotes.ToPrs(MismatchQuotes, 14)); + } +} diff --git a/tests/indicators/m-r/Prs/Prs.Tests.cs b/tests/indicators/m-r/Prs/Prs.Tests.cs deleted file mode 100644 index e3bf0536c..000000000 --- a/tests/indicators/m-r/Prs/Prs.Tests.cs +++ /dev/null @@ -1,136 +0,0 @@ -namespace Tests.Indicators; - -[TestClass] -public class PrsTests : TestBase -{ - [TestMethod] - public void Standard() - { - int lookbackPeriods = 30; - int smaPeriods = 10; - - List results = - otherQuotes.GetPrs(quotes, lookbackPeriods, smaPeriods) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(502, results.Count(x => x.Prs != null)); - Assert.AreEqual(493, results.Count(x => x.PrsSma != null)); - - // sample values - PrsResult r1 = results[8]; - Assert.AreEqual(1.108340, r1.Prs.Round(6)); - Assert.AreEqual(null, r1.PrsSma); - Assert.AreEqual(null, r1.PrsPercent); - - PrsResult r2 = results[249]; - Assert.AreEqual(1.222373, r2.Prs.Round(6)); - Assert.AreEqual(1.275808, r2.PrsSma.Round(6)); - Assert.AreEqual(-0.023089, r2.PrsPercent.Round(6)); - - PrsResult r3 = results[501]; - Assert.AreEqual(1.356817, r3.Prs.Round(6)); - Assert.AreEqual(1.343445, r3.PrsSma.Round(6)); - Assert.AreEqual(0.037082, r3.PrsPercent.Round(6)); - } - - [TestMethod] - public void UseTuple() - { - List results = otherQuotes - .Use(CandlePart.Close) - .GetPrs(quotes.Use(CandlePart.Close), 20) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(502, results.Count(x => x.Prs != null)); - } - - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetPrs(tupleNanny, 6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Prs is double and double.NaN)); - } - - [TestMethod] - public void Chainor() - { - List results = otherQuotes - .GetPrs(quotes, 20) - .GetSma(10) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(493, results.Count(x => x.Sma != null)); - } - - [TestMethod] - public void Chainee() - { - List results = quotes - .GetSma(2) - .GetPrs(otherQuotes.GetSma(2), 20) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(501, results.Count(x => x.Prs != null)); - Assert.AreEqual(0, results.Count(x => x.Prs is double and double.NaN)); - } - - [TestMethod] - public void BadData() - { - List r = badQuotes - .GetPrs(badQuotes, 15, 4) - .ToList(); - - Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Prs is double and double.NaN)); - } - - [TestMethod] - public void NoQuotes() - { - List r0 = noquotes - .GetPrs(noquotes) - .ToList(); - - Assert.AreEqual(0, r0.Count); - - List r1 = onequote - .GetPrs(onequote) - .ToList(); - - Assert.AreEqual(1, r1.Count); - } - - [TestMethod] - public void Exceptions() - { - // bad lookback period - Assert.ThrowsException(() => - otherQuotes.GetPrs(quotes, 0)); - - // bad SMA period - Assert.ThrowsException(() => - otherQuotes.GetPrs(quotes, 14, 0)); - - // insufficient quotes - Assert.ThrowsException(() => - TestData.GetCompare(13).GetPrs(quotes, 14)); - - // insufficient eval quotes - Assert.ThrowsException(() => - TestData.GetCompare(300).GetPrs(quotes, 14)); - - // mismatch quotes - Assert.ThrowsException(() => - otherQuotes.GetPrs(mismatchQuotes, 14)); - } -} diff --git a/tests/indicators/m-r/Pvo/Pvo.Tests.cs b/tests/indicators/m-r/Pvo/Pvo.StaticSeries.Tests.cs similarity index 71% rename from tests/indicators/m-r/Pvo/Pvo.Tests.cs rename to tests/indicators/m-r/Pvo/Pvo.StaticSeries.Tests.cs index 0c82c340b..6b5e8123d 100644 --- a/tests/indicators/m-r/Pvo/Pvo.Tests.cs +++ b/tests/indicators/m-r/Pvo/Pvo.StaticSeries.Tests.cs @@ -1,18 +1,17 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class PvoTests : TestBase +public class Pvo : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int fastPeriods = 12; int slowPeriods = 26; int signalPeriods = 9; - List results = - quotes.GetPvo(fastPeriods, slowPeriods, signalPeriods) - .ToList(); + IReadOnlyList results = + Quotes.ToPvo(fastPeriods, slowPeriods, signalPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -50,38 +49,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetPvo() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToPvo() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(468, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetPvo(10, 20, 5) - .ToList(); + IReadOnlyList r = BadQuotes + .ToPvo(10, 20, 5); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Pvo is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Pvo is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetPvo() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToPvo(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetPvo() - .ToList(); + IReadOnlyList r1 = Onequote + .ToPvo(); Assert.AreEqual(1, r1.Count); } @@ -93,15 +88,14 @@ public void Removed() int slowPeriods = 26; int signalPeriods = 9; - List results = quotes - .GetPvo(fastPeriods, slowPeriods, signalPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToPvo(fastPeriods, slowPeriods, signalPeriods) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (slowPeriods + signalPeriods + 250), results.Count); - PvoResult last = results.LastOrDefault(); + PvoResult last = results[^1]; Assert.AreEqual(10.4395, last.Pvo.Round(4)); Assert.AreEqual(12.2681, last.Signal.Round(4)); Assert.AreEqual(-1.8286, last.Histogram.Round(4)); @@ -112,14 +106,14 @@ public void Exceptions() { // bad fast period Assert.ThrowsException(() => - quotes.GetPvo(0, 26, 9)); + Quotes.ToPvo(0)); // bad slow periods must be larger than faster period Assert.ThrowsException(() => - quotes.GetPvo(12, 12, 9)); + Quotes.ToPvo(12, 12)); // bad signal period Assert.ThrowsException(() => - quotes.GetPvo(12, 26, -1)); + Quotes.ToPvo(12, 26, -1)); } } diff --git a/tests/indicators/m-r/Renko/Renko.Calc.xlsx b/tests/indicators/m-r/Renko/Renko.Calc.xlsx index 97f03fddb..b32259adb 100644 Binary files a/tests/indicators/m-r/Renko/Renko.Calc.xlsx and b/tests/indicators/m-r/Renko/Renko.Calc.xlsx differ diff --git a/tests/indicators/m-r/Renko/Renko.Tests.cs b/tests/indicators/m-r/Renko/Renko.StaticSeries.Tests.cs similarity index 76% rename from tests/indicators/m-r/Renko/Renko.Tests.cs rename to tests/indicators/m-r/Renko/Renko.StaticSeries.Tests.cs index 9e94bfaa5..1a2ed70ac 100644 --- a/tests/indicators/m-r/Renko/Renko.Tests.cs +++ b/tests/indicators/m-r/Renko/Renko.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class RenkoTests : TestBase +public class Renko : StaticSeriesTestBase { [TestMethod] - public void StandardClose() + public override void Standard() // close { - List results = quotes - .GetRenko(2.5m, EndType.Close) - .ToList(); + IReadOnlyList results = Quotes + .ToRenko(2.5m); // assertions @@ -33,7 +32,7 @@ public void StandardClose() Assert.AreEqual(4192959240m, r5.Volume); Assert.IsTrue(r5.IsUp); - RenkoResult last = results.LastOrDefault(); + RenkoResult last = results[^1]; Assert.AreEqual(240.5m, last.Open); Assert.AreEqual(243.68m, last.High); Assert.AreEqual(234.52m, last.Low); @@ -45,9 +44,8 @@ public void StandardClose() [TestMethod] public void StandardHighLow() { - List results = quotes - .GetRenko(2.5m, EndType.HighLow) - .ToList(); + IReadOnlyList results = Quotes + .ToRenko(2.5m, EndType.HighLow); // assertions @@ -70,7 +68,7 @@ public void StandardHighLow() Assert.AreEqual(100801672m, r25.Volume.Round(0)); Assert.IsTrue(r25.IsUp); - RenkoResult last = results.LastOrDefault(); + RenkoResult last = results[^1]; Assert.AreEqual(243m, last.Open); Assert.AreEqual(246.73m, last.High); Assert.AreEqual(241.87m, last.Low); @@ -82,9 +80,8 @@ public void StandardHighLow() [TestMethod] public void Atr() { - List results = quotes - .GetRenkoAtr(14, EndType.Close) - .ToList(); + IReadOnlyList results = Quotes + .GetRenkoAtr(14); // proper quantities Assert.AreEqual(29, results.Count); @@ -98,7 +95,7 @@ public void Atr() Assert.AreEqual(2090292272m, r0.Volume.Round(0)); Assert.IsTrue(r0.IsUp); - RenkoResult last = results.LastOrDefault(); + RenkoResult last = results[^1]; Assert.AreEqual(237.3990m, last.Open.Round(4)); Assert.AreEqual(246.73m, last.High.Round(4)); Assert.AreEqual(229.42m, last.Low.Round(4)); @@ -110,27 +107,25 @@ public void Atr() [TestMethod] public void UseAsQuotes() { - IEnumerable renkoQuotes = quotes.GetRenko(2.5m); - IEnumerable renkoSma = renkoQuotes.GetSma(5); + IReadOnlyList renkoQuotes = Quotes.ToRenko(2.5m); + IReadOnlyList renkoSma = renkoQuotes.ToSma(5); Assert.AreEqual(108, renkoSma.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetRenko(100m) - .ToList(); + IReadOnlyList r = BadQuotes + .ToRenko(100m); Assert.AreNotEqual(0, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetRenko(0.01m) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToRenko(0.01m); Assert.AreEqual(0, r0.Count); } @@ -140,10 +135,10 @@ public void Exceptions() { // bad arguments Assert.ThrowsException(() - => quotes.GetRenko(0)); + => Quotes.ToRenko(0)); // bad end type Assert.ThrowsException(() - => quotes.GetRenko(2, (EndType)int.MaxValue)); + => Quotes.ToRenko(2, (EndType)int.MaxValue)); } } diff --git a/tests/indicators/m-r/Renko/Renko.StreamHub.Tests.cs b/tests/indicators/m-r/Renko/Renko.StreamHub.Tests.cs new file mode 100644 index 000000000..9e859a39d --- /dev/null +++ b/tests/indicators/m-r/Renko/Renko.StreamHub.Tests.cs @@ -0,0 +1,147 @@ +namespace StreamHub; + +[TestClass] +public class RenkoHub : StreamHubTestBase, ITestChainProvider +{ + [TestMethod] + public override void QuoteObserver() + { + decimal brickSize = 2.5m; + EndType endType = EndType.HighLow; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider + for (int i = 0; i < 50; i++) + { + provider.Add(quotesList[i]); + } + + // initialize observer + RenkoHub observer = provider + .ToRenko(brickSize, endType); + + // fetch initial results (early) + IReadOnlyList streamList + = observer.Results; + + // emulate adding quotes to provider + for (int i = 50; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotesList[80]); + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // time-series, for comparison + IReadOnlyList seriesList = quotesList + .ToRenko(brickSize, endType); + + // assert, should equal series + streamList.Should().BeEquivalentTo(seriesList); + streamList.Should().HaveCount(159); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void ChainProvider() + { + decimal brickSize = 2.5m; + EndType endType = EndType.Close; + int smaPeriods = 50; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + SmaHub observer = provider + .ToRenko(brickSize, endType) + .ToSma(smaPeriods); + + // emulate quote stream + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // final results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList = quotesList + .ToRenko(brickSize, endType) + .ToSma(smaPeriods); + + // assert, should equal series + streamList.Should().BeEquivalentTo(seriesList); + streamList.Should().HaveCount(112); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public override void CustomToString() + { + RenkoHub hub = new(new QuoteHub(), 2.5m, EndType.Close); + hub.ToString().Should().Be("RENKO(2.5,CLOSE)"); + } + + [TestMethod] + public void SettingsInheritance() + { + // setup quote hub (1st level) + QuoteHub quoteHub = new(); + + // setup renko hub (2nd level) + RenkoHub renkoHub = quoteHub + .ToRenko(brickSize: 2.5m, endType: EndType.Close); + + // setup child hub (3rd level) + SmaHub childHub = renkoHub + .ToSma(lookbackPeriods: 5); + + // note: dispite `quoteHub` being parentless, + // it has default properties; it should not + // inherit its own empty provider settings + + // assert + quoteHub.Properties.Settings.Should().Be(0b00000000, "is has default settings, not inherited"); + renkoHub.Properties.Settings.Should().Be(0b00000010, "it has custom Renko properties"); + childHub.Properties.Settings.Should().Be(0b00000010, "it inherits Renko properties"); + } +} diff --git a/tests/indicators/m-r/Roc/Roc.StaticSeries.Tests.cs b/tests/indicators/m-r/Roc/Roc.StaticSeries.Tests.cs new file mode 100644 index 000000000..1042bca94 --- /dev/null +++ b/tests/indicators/m-r/Roc/Roc.StaticSeries.Tests.cs @@ -0,0 +1,107 @@ +namespace StaticSeries; + +[TestClass] +public class Roc : StaticSeriesTestBase +{ + [TestMethod] + public override void Standard() + { + IReadOnlyList results = Quotes + .ToRoc(20); + + // proper quantities + Assert.AreEqual(502, results.Count); + Assert.AreEqual(482, results.Count(x => x.Momentum != null)); + Assert.AreEqual(482, results.Count(x => x.Roc != null)); + + // sample values + RocResult r49 = results[49]; + Assert.AreEqual(4.96, r49.Momentum.Round(4)); + Assert.AreEqual(2.2465, r49.Roc.Round(4)); + + RocResult r249 = results[249]; + Assert.AreEqual(6.25, r249.Momentum.Round(4)); + Assert.AreEqual(2.4827, r249.Roc.Round(4)); + + RocResult r501 = results[501]; + Assert.AreEqual(-22.05, r501.Momentum.Round(4)); + Assert.AreEqual(-8.2482, r501.Roc.Round(4)); + } + + [TestMethod] + public void UseReusable() + { + IReadOnlyList results = Quotes + .Use(CandlePart.Close) + .ToRoc(20); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(482, results.Count(x => x.Roc != null)); + } + + [TestMethod] + public void Chainee() + { + IReadOnlyList results = Quotes + .ToSma(2) + .ToRoc(20); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(481, results.Count(x => x.Roc != null)); + } + + [TestMethod] + public void Chainor() + { + IReadOnlyList results = Quotes + .ToRoc(20) + .ToSma(10); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(473, results.Count(x => x.Sma != null)); + } + + [TestMethod] + public override void BadData() + { + IReadOnlyList r = BadQuotes + .ToRoc(35); + + Assert.AreEqual(502, r.Count); + Assert.AreEqual(0, r.Count(x => x.Roc is double.NaN)); + } + + [TestMethod] + public override void NoQuotes() + { + IReadOnlyList r0 = Noquotes + .ToRoc(5); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = Onequote + .ToRoc(5); + + Assert.AreEqual(1, r1.Count); + } + + [TestMethod] + public void Removed() + { + IReadOnlyList results = Quotes + .ToRoc(20) + .RemoveWarmupPeriods(); + + // assertions + Assert.AreEqual(502 - 20, results.Count); + + RocResult last = results[^1]; + Assert.AreEqual(-8.2482, last.Roc.Round(4)); + } + + [TestMethod] + public void Exceptions() => + // bad lookback period + Assert.ThrowsException(() => + Quotes.ToRoc(0)); +} diff --git a/tests/indicators/m-r/Roc/Roc.Tests.cs b/tests/indicators/m-r/Roc/Roc.Tests.cs deleted file mode 100644 index 9933afca6..000000000 --- a/tests/indicators/m-r/Roc/Roc.Tests.cs +++ /dev/null @@ -1,162 +0,0 @@ -namespace Tests.Indicators; - -[TestClass] -public class RocTests : TestBase -{ - [TestMethod] - public void Standard() - { - List results = quotes - .GetRoc(20) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(482, results.Count(x => x.Momentum != null)); - Assert.AreEqual(482, results.Count(x => x.Roc != null)); - Assert.AreEqual(false, results.Any(x => x.RocSma != null)); - - // sample values - RocResult r49 = results[49]; - Assert.AreEqual(4.96, r49.Momentum.Round(4)); - Assert.AreEqual(2.2465, r49.Roc.Round(4)); - Assert.AreEqual(null, r49.RocSma); - - RocResult r249 = results[249]; - Assert.AreEqual(6.25, r249.Momentum.Round(4)); - Assert.AreEqual(2.4827, r249.Roc.Round(4)); - Assert.AreEqual(null, r249.RocSma); - - RocResult r501 = results[501]; - Assert.AreEqual(-22.05, r501.Momentum.Round(4)); - Assert.AreEqual(-8.2482, r501.Roc.Round(4)); - Assert.AreEqual(null, r501.RocSma); - } - - [TestMethod] - public void WithSma() - { - int lookbackPeriods = 20; - int smaPeriods = 5; - - List results = quotes - .GetRoc(lookbackPeriods, smaPeriods) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(482, results.Count(x => x.Roc != null)); - Assert.AreEqual(478, results.Count(x => x.RocSma != null)); - - // sample values - RocResult r1 = results[29]; - Assert.AreEqual(3.2936, r1.Roc.Round(4)); - Assert.AreEqual(2.1558, r1.RocSma.Round(4)); - - RocResult r2 = results[501]; - Assert.AreEqual(-8.2482, r2.Roc.Round(4)); - Assert.AreEqual(-8.4828, r2.RocSma.Round(4)); - } - - [TestMethod] - public void UseTuple() - { - List results = quotes - .Use(CandlePart.Close) - .GetRoc(20) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(482, results.Count(x => x.Roc != null)); - } - - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetRoc(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Roc is double and double.NaN)); - } - - [TestMethod] - public void Chainee() - { - List results = quotes - .GetSma(2) - .GetRoc(20) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(481, results.Count(x => x.Roc != null)); - } - - [TestMethod] - public void Chainor() - { - List results = quotes - .GetRoc(20) - .GetSma(10) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(473, results.Count(x => x.Sma != null)); - } - - [TestMethod] - public void BadData() - { - List r = badQuotes - .GetRoc(35, 2) - .ToList(); - - Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Roc is double and double.NaN)); - } - - [TestMethod] - public void NoQuotes() - { - List r0 = noquotes - .GetRoc(5) - .ToList(); - - Assert.AreEqual(0, r0.Count); - - List r1 = onequote - .GetRoc(5) - .ToList(); - - Assert.AreEqual(1, r1.Count); - } - - [TestMethod] - public void Removed() - { - List results = quotes - .GetRoc(20) - .RemoveWarmupPeriods() - .ToList(); - - // assertions - Assert.AreEqual(502 - 20, results.Count); - - RocResult last = results.LastOrDefault(); - Assert.AreEqual(-8.2482, last.Roc.Round(4)); - Assert.AreEqual(null, last.RocSma); - } - - [TestMethod] - public void Exceptions() - { - // bad lookback period - Assert.ThrowsException(() => - quotes.GetRoc(0)); - - // bad SMA period - Assert.ThrowsException(() => - quotes.GetRoc(14, 0)); - } -} diff --git a/tests/indicators/m-r/RocWb/RocWb.Tests.cs b/tests/indicators/m-r/RocWb/RocWb.StaticSeries.Tests.cs similarity index 70% rename from tests/indicators/m-r/RocWb/RocWb.Tests.cs rename to tests/indicators/m-r/RocWb/RocWb.StaticSeries.Tests.cs index 2e5b95412..65bd3dd2e 100644 --- a/tests/indicators/m-r/RocWb/RocWb.Tests.cs +++ b/tests/indicators/m-r/RocWb/RocWb.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class RocWbTests : TestBase +public class RocWb : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetRocWb(20, 3, 20) - .ToList(); + IReadOnlyList results = Quotes + .ToRocWb(20, 3, 20); // proper quantities Assert.AreEqual(502, results.Count); @@ -68,35 +67,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetRocWb(20, 3, 20) - .ToList(); + .ToRocWb(20, 3, 20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Roc != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetRocWb(6, 7, 5) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.UpperBand is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetRocWb(20, 3, 20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToRocWb(20, 3, 20); Assert.AreEqual(502, results.Count); Assert.AreEqual(481, results.Count(x => x.Roc != null)); @@ -105,38 +91,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetRocWb(20, 3, 20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToRocWb(20, 3, 20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(473, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetRocWb(35, 3, 35) - .ToList(); + IReadOnlyList r = BadQuotes + .ToRocWb(35, 3, 35); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Roc is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Roc is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetRocWb(5, 3, 2) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToRocWb(5, 3, 2); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetRocWb(5, 3, 2) - .ToList(); + IReadOnlyList r1 = Onequote + .ToRocWb(5, 3, 2); Assert.AreEqual(1, r1.Count); } @@ -144,15 +126,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetRocWb(20, 3, 20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToRocWb(20, 3, 20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (20 + 3 + 100), results.Count); - RocWbResult last = results.LastOrDefault(); + RocWbResult last = results[^1]; Assert.AreEqual(-8.2482, Math.Round(last.Roc.Value, 4)); Assert.AreEqual(-8.3390, Math.Round(last.RocEma.Value, 4)); Assert.AreEqual(6.1294, Math.Round(last.UpperBand.Value, 4)); @@ -164,14 +145,14 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetRocWb(0, 3, 12)); + Quotes.ToRocWb(0, 3, 12)); // bad EMA period Assert.ThrowsException(() => - quotes.GetRocWb(14, 0, 14)); + Quotes.ToRocWb(14, 0, 14)); // bad STDDEV period Assert.ThrowsException(() => - quotes.GetRocWb(15, 3, 16)); + Quotes.ToRocWb(15, 3, 16)); } } diff --git a/tests/indicators/m-r/RollingPivots/RollingPivots.Tests.cs b/tests/indicators/m-r/RollingPivots/RollingPivots.StaticSeries.Tests.cs similarity index 89% rename from tests/indicators/m-r/RollingPivots/RollingPivots.Tests.cs rename to tests/indicators/m-r/RollingPivots/RollingPivots.StaticSeries.Tests.cs index 2e422d274..7874751ca 100644 --- a/tests/indicators/m-r/RollingPivots/RollingPivots.Tests.cs +++ b/tests/indicators/m-r/RollingPivots/RollingPivots.StaticSeries.Tests.cs @@ -1,18 +1,17 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class RollingPivotsTests : TestBase +public class RollingPivots : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int windowPeriods = 11; int offsetPeriods = 9; PivotPointType pointType = PivotPointType.Standard; - List results = - quotes.GetRollingPivots(windowPeriods, offsetPeriods, pointType) - .ToList(); + IReadOnlyList results = + Quotes.ToRollingPivots(windowPeriods, offsetPeriods, pointType); // proper quantities Assert.AreEqual(502, results.Count); @@ -82,11 +81,10 @@ public void Camarilla() int offsetPeriods = 0; PivotPointType pointType = PivotPointType.Camarilla; - IEnumerable h = TestData.GetDefault(38); + IReadOnlyList h = Data.GetDefault(38); - List results = h - .GetRollingPivots(windowPeriods, offsetPeriods, pointType) - .ToList(); + IReadOnlyList results = h + .ToRollingPivots(windowPeriods, offsetPeriods, pointType); // proper quantities Assert.AreEqual(38, results.Count); @@ -156,9 +154,8 @@ public void Demark() int offsetPeriods = 10; PivotPointType pointType = PivotPointType.Demark; - List results = quotes - .GetRollingPivots(windowPeriods, offsetPeriods, pointType) - .ToList(); + IReadOnlyList results = Quotes + .ToRollingPivots(windowPeriods, offsetPeriods, pointType); // proper quantities Assert.AreEqual(502, results.Count); @@ -239,11 +236,10 @@ public void Fibonacci() int offsetPeriods = 15; PivotPointType pointType = PivotPointType.Fibonacci; - IEnumerable h = TestData.GetIntraday(300); + IReadOnlyList h = Data.GetIntraday(300); - List results = - h.GetRollingPivots(windowPeriods, offsetPeriods, pointType) - .ToList(); + IReadOnlyList results = + h.ToRollingPivots(windowPeriods, offsetPeriods, pointType); // proper quantities Assert.AreEqual(300, results.Count); @@ -314,11 +310,10 @@ public void Woodie() int offsetPeriods = 16; PivotPointType pointType = PivotPointType.Woodie; - IEnumerable h = TestData.GetIntraday(1564); + IReadOnlyList h = Data.GetIntraday(); - List results = h - .GetRollingPivots(windowPeriods, offsetPeriods, pointType) - .ToList(); + IReadOnlyList results = h + .ToRollingPivots(windowPeriods, offsetPeriods, pointType); // proper quantities Assert.AreEqual(1564, results.Count); @@ -374,27 +369,24 @@ public void Woodie() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetRollingPivots(5, 5) - .ToList(); + IReadOnlyList r = BadQuotes + .ToRollingPivots(5, 5); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetRollingPivots(5, 2) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToRollingPivots(5, 2); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetRollingPivots(5, 2) - .ToList(); + IReadOnlyList r1 = Onequote + .ToRollingPivots(5, 2); Assert.AreEqual(1, r1.Count); } @@ -406,15 +398,14 @@ public void Removed() int offsetPeriods = 9; PivotPointType pointType = PivotPointType.Standard; - List results = quotes - .GetRollingPivots(windowPeriods, offsetPeriods, pointType) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToRollingPivots(windowPeriods, offsetPeriods, pointType) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (windowPeriods + offsetPeriods), results.Count); - RollingPivotsResult last = results.LastOrDefault(); + RollingPivotsResult last = results[^1]; Assert.AreEqual(260.0267m, last.PP.Round(4)); Assert.AreEqual(246.4633m, last.S1.Round(4)); Assert.AreEqual(238.7767m, last.S2.Round(4)); @@ -431,10 +422,10 @@ public void Exceptions() { // bad window period Assert.ThrowsException(() => - quotes.GetRollingPivots(0, 10)); + Quotes.ToRollingPivots(0, 10)); // bad offset period Assert.ThrowsException(() => - quotes.GetRollingPivots(10, -1)); + Quotes.ToRollingPivots(10, -1)); } } diff --git a/tests/indicators/m-r/Rsi/Rsi.Tests.cs b/tests/indicators/m-r/Rsi/Rsi.StaticSeries.Tests.cs similarity index 54% rename from tests/indicators/m-r/Rsi/Rsi.Tests.cs rename to tests/indicators/m-r/Rsi/Rsi.StaticSeries.Tests.cs index 2ef69eefe..f70a73693 100644 --- a/tests/indicators/m-r/Rsi/Rsi.Tests.cs +++ b/tests/indicators/m-r/Rsi/Rsi.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class RsiTests : TestBase +public class Rsi : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetRsi(14) - .ToList(); + IReadOnlyList results = Quotes + .ToRsi(); // proper quantities Assert.AreEqual(502, results.Count); @@ -32,9 +31,8 @@ public void Standard() public void SmallLookback() { int lookbackPeriods = 1; - List results = quotes - .GetRsi(lookbackPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToRsi(lookbackPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -51,45 +49,31 @@ public void SmallLookback() [TestMethod] public void CryptoData() { - IEnumerable btc = TestData.GetBitcoin(); + IReadOnlyList btc = Data.GetBitcoin(); - List r = btc - .GetRsi(1) - .ToList(); + IReadOnlyList r = btc + .ToRsi(1); Assert.AreEqual(1246, r.Count); } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetRsi(14) - .ToList(); + .ToRsi(); Assert.AreEqual(502, results.Count); Assert.AreEqual(488, results.Count(x => x.Rsi != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetRsi(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Rsi is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetRsi(14) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToRsi(); Assert.AreEqual(502, results.Count); Assert.AreEqual(487, results.Count(x => x.Rsi != null)); @@ -98,10 +82,9 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetRsi(14) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToRsi() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(479, results.Count(x => x.Sma != null)); @@ -110,35 +93,32 @@ public void Chainor() [TestMethod] public void NaN() { - IEnumerable r = TestData.GetBtcUsdNan() - .GetRsi(14); + IReadOnlyList r = Data.GetBtcUsdNan() + .ToRsi(); - Assert.AreEqual(0, r.Count(x => x.Rsi is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Rsi is double.NaN)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetRsi(20) - .ToList(); + IReadOnlyList r = BadQuotes + .ToRsi(20); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Rsi is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Rsi is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetRsi() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToRsi(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetRsi() - .ToList(); + IReadOnlyList r1 = Onequote + .ToRsi(); Assert.AreEqual(1, r1.Count); } @@ -146,15 +126,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetRsi(14) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToRsi() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (10 * 14), results.Count); - RsiResult last = results.LastOrDefault(); + RsiResult last = results[^1]; Assert.AreEqual(42.0773, last.Rsi.Round(4)); } @@ -162,5 +141,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetRsi(0)); + => Quotes.ToRsi(0)); } diff --git a/tests/indicators/s-z/Slope/Slope.Tests.cs b/tests/indicators/s-z/Slope/Slope.StaticSeries.Tests.cs similarity index 63% rename from tests/indicators/s-z/Slope/Slope.Tests.cs rename to tests/indicators/s-z/Slope/Slope.StaticSeries.Tests.cs index d6a51eab2..9fc6af0f4 100644 --- a/tests/indicators/s-z/Slope/Slope.Tests.cs +++ b/tests/indicators/s-z/Slope/Slope.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class SlopeTests : TestBase +public class Slope : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetSlope(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSlope(20); // proper quantities Assert.AreEqual(502, results.Count); @@ -40,35 +39,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetSlope(20) - .ToList(); + .ToSlope(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Slope != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetSlope(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Slope is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetSlope(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToSlope(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Slope != null)); @@ -77,48 +63,43 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetSlope(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToSlope(20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetSlope(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToSlope(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Slope is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Slope is double.NaN)); } [TestMethod] public void BigData() { - List r = bigQuotes - .GetSlope(250) - .ToList(); + IReadOnlyList r = BigQuotes + .ToSlope(250); Assert.AreEqual(1246, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetSlope(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToSlope(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetSlope(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToSlope(5); Assert.AreEqual(1, r1.Count); } @@ -126,15 +107,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetSlope(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToSlope(20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 19, results.Count); - SlopeResult last = results.LastOrDefault(); + SlopeResult last = results[^1]; Assert.AreEqual(-1.689143, last.Slope.Round(6)); Assert.AreEqual(1083.7629, last.Intercept.Round(4)); Assert.AreEqual(0.7955, last.RSquared.Round(4)); @@ -146,5 +126,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetSlope(1)); + => Quotes.ToSlope(1)); } diff --git a/tests/indicators/s-z/Sma/Sma.Analysis.Tests.cs b/tests/indicators/s-z/Sma/Sma.Analysis.Tests.cs deleted file mode 100644 index 790bbc920..000000000 --- a/tests/indicators/s-z/Sma/Sma.Analysis.Tests.cs +++ /dev/null @@ -1,117 +0,0 @@ -namespace Tests.Indicators; - -[TestClass] -public class SmaExtendedTests : TestBase -{ - [TestMethod] - public void Analysis() - { - List results = quotes - .GetSmaAnalysis(20) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(483, results.Count(x => x.Sma != null)); - - // sample value - SmaAnalysis r = results[501]; - Assert.AreEqual(251.86, r.Sma.Round(6)); - Assert.AreEqual(9.450000, r.Mad.Round(6)); - Assert.AreEqual(119.25102, r.Mse.Round(6)); - Assert.AreEqual(0.037637, r.Mape.Round(6)); - } - - [TestMethod] - public void UseTuple() - { - List results = quotes - .Use(CandlePart.Close) - .GetSmaAnalysis(20) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(483, results.Count(x => x.Sma != null)); - } - - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetSmaAnalysis(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Mse is double and double.NaN)); - } - - [TestMethod] - public void Chainee() - { - List results = quotes - .GetSma(2) - .GetSmaAnalysis(20) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(482, results.Count(x => x.Sma != null)); - } - - [TestMethod] - public void Chainor() - { - List results = quotes - .GetSmaAnalysis(10) - .GetEma(10) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(484, results.Count(x => x.Ema != null)); - } - - [TestMethod] - public void BadData() - { - List r = badQuotes - .GetSmaAnalysis(15) - .ToList(); - - Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Mape is double and double.NaN)); - } - - [TestMethod] - public void NoQuotes() - { - List r0 = noquotes - .GetSmaAnalysis(6) - .ToList(); - - Assert.AreEqual(0, r0.Count); - - List r1 = onequote - .GetSmaAnalysis(6) - .ToList(); - - Assert.AreEqual(1, r1.Count); - } - - [TestMethod] - public void Removed() - { - List results = quotes - .GetSmaAnalysis(20) - .RemoveWarmupPeriods() - .ToList(); - - // assertions - Assert.AreEqual(502 - 19, results.Count); - Assert.AreEqual(251.8600, Math.Round(results.LastOrDefault().Sma.Value, 4)); - } - - // bad lookback period - [TestMethod] - public void Exceptions() - => Assert.ThrowsException(() - => quotes.GetSmaAnalysis(0)); -} diff --git a/tests/indicators/s-z/Sma/Sma.Obs.Tests.cs b/tests/indicators/s-z/Sma/Sma.Obs.Tests.cs deleted file mode 100644 index bca4caab6..000000000 --- a/tests/indicators/s-z/Sma/Sma.Obs.Tests.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Skender.Stock.Indicators; -using Tests.Common; - -namespace Tests.Indicators; - -[TestClass] -public class SmaStreamTests : TestBase -{ - [TestMethod] - public void Standard() - { - List quotesList = quotes - .ToSortedList(); - - int length = quotesList.Count; - - // time-series, for comparison - List seriesList = quotes - .GetSma(20) - .ToList(); - - // setup quote provider - QuoteProvider provider = new(); - - // initialize EMA observer - SmaObserver observer = provider - .GetSma(20); - - // fetch initial results - IEnumerable results - = observer.Results; - - // emulate adding quotes to provider - for (int i = 0; i < length; i++) - { - Quote q = quotesList[i]; - provider.Add(q); - } - - // final results - List resultsList - = results.ToList(); - - // assert, should equal series - for (int i = 0; i < seriesList.Count; i++) - { - SmaResult s = seriesList[i]; - SmaResult r = resultsList[i]; - - Assert.AreEqual(s.Date, r.Date); - Assert.AreEqual(s.Sma, r.Sma); - } - - observer.Unsubscribe(); - provider.EndTransmission(); - } - - [TestMethod] - public void Increment() - { - // baseline for comparison - List<(DateTime Date, double Value)> tpList = new() - { - new (DateTime.Parse("1/1/2000", EnglishCulture), 1d), - new (DateTime.Parse("1/2/2000", EnglishCulture), 2d), - new (DateTime.Parse("1/3/2000", EnglishCulture), 3d), - new (DateTime.Parse("1/4/2000", EnglishCulture), 4d), - new (DateTime.Parse("1/5/2000", EnglishCulture), 5d), - new (DateTime.Parse("1/6/2000", EnglishCulture), 6d), - new (DateTime.Parse("1/7/2000", EnglishCulture), 7d), - new (DateTime.Parse("1/8/2000", EnglishCulture), 8d), - new (DateTime.Parse("1/9/2000", EnglishCulture), 9d), - }; - - double sma; - - sma = SmaObserver.Increment(tpList, tpList.Count - 1, 9); - Assert.AreEqual(5d, sma); - - sma = SmaObserver.Increment(tpList, tpList.Count - 1, 10); - Assert.AreEqual(double.NaN, sma); - } - - [TestMethod] - public void Usee() - { - List quotesList = quotes - .ToSortedList(); - - int length = quotesList.Count; - - // time-series, for comparison - List staticSma = quotes - .Use(CandlePart.OC2) - .GetSma(11) - .ToList(); - - // setup quote provider - QuoteProvider provider = new(); - - // initialize EMA observer - List streamSma = provider - .Use(CandlePart.OC2) - .GetSma(11) - .ProtectedResults; - - // emulate adding quotes to provider - for (int i = 0; i < length; i++) - { - provider.Add(quotesList[i]); - } - - provider.EndTransmission(); - - // assert, should equal series - for (int i = 0; i < length; i++) - { - SmaResult t = staticSma[i]; - SmaResult s = streamSma[i]; - - Assert.AreEqual(t.Date, s.Date); - Assert.AreEqual(t.Sma, s.Sma); - } - } -} diff --git a/tests/indicators/s-z/Sma/Sma.Static.Tests.cs b/tests/indicators/s-z/Sma/Sma.StaticSeries.Tests.cs similarity index 59% rename from tests/indicators/s-z/Sma/Sma.Static.Tests.cs rename to tests/indicators/s-z/Sma/Sma.StaticSeries.Tests.cs index e6defb30a..d66fe61ff 100644 --- a/tests/indicators/s-z/Sma/Sma.Static.Tests.cs +++ b/tests/indicators/s-z/Sma/Sma.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class SmaTests : TestBase +public class Sma : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetSma(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(20); // proper quantities Assert.AreEqual(502, results.Count); @@ -26,10 +25,9 @@ public void Standard() [TestMethod] public void CandlePartOpen() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Open) - .GetSma(20) - .ToList(); + .ToSma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Sma != null)); @@ -46,10 +44,9 @@ public void CandlePartOpen() [TestMethod] public void CandlePartVolume() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Volume) - .GetSma(20) - .ToList(); + .ToSma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Sma != null)); @@ -62,66 +59,50 @@ public void CandlePartVolume() Assert.AreEqual(157958070.8, r290.Sma); SmaResult r501 = results[501]; - Assert.AreEqual(DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", EnglishCulture), r501.Date); + Assert.AreEqual(DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", invariantCulture), r501.Timestamp); Assert.AreEqual(163695200, r501.Sma); } [TestMethod] public void Chainor() { - List results = quotes - .GetSma(10) - .GetEma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(10) + .ToEma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(484, results.Count(x => x.Ema != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetSma(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Sma is double and double.NaN)); - } - [TestMethod] public void NaN() { - List r = TestData.GetBtcUsdNan() - .GetSma(50) - .ToList(); + IReadOnlyList r = Data.GetBtcUsdNan() + .ToSma(50); - Assert.AreEqual(0, r.Count(x => x.Sma is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Sma is double.NaN)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetSma(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToSma(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Sma is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Sma is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetSma(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToSma(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetSma(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToSma(5); Assert.AreEqual(1, r1.Count); } @@ -129,19 +110,40 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetSma(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 19, results.Count); - Assert.AreEqual(251.8600, results.LastOrDefault().Sma.Round(4)); + Assert.AreEqual(251.8600, results[^1].Sma.Round(4)); + } + + [TestMethod] + public void Equality() + { + SmaResult r1 = new(Timestamp: EvalDate, Sma: 1d); + + SmaResult r2 = new(Timestamp: EvalDate, Sma: 1d); + + SmaResult r3 = new(Timestamp: EvalDate, Sma: 2d); + + Assert.IsTrue(Equals(r1, r2)); + Assert.IsFalse(Equals(r1, r3)); + + Assert.IsTrue(r1.Equals(r2)); + Assert.IsFalse(r1.Equals(r3)); + + Assert.IsTrue(r1 == r2); + Assert.IsFalse(r1 == r3); + + Assert.IsFalse(r1 != r2); + Assert.IsTrue(r1 != r3); } // bad lookback period [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetSma(0)); + => Quotes.ToSma(0)); } diff --git a/tests/indicators/s-z/Sma/Sma.StreamHub.Tests.cs b/tests/indicators/s-z/Sma/Sma.StreamHub.Tests.cs new file mode 100644 index 000000000..c09661948 --- /dev/null +++ b/tests/indicators/s-z/Sma/Sma.StreamHub.Tests.cs @@ -0,0 +1,166 @@ +namespace StreamHub; + +[TestClass] +public class SmaHub : StreamHubTestBase, ITestChainObserver, ITestChainProvider +{ + [TestMethod] + public override void QuoteObserver() + { + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider + for (int i = 0; i < 20; i++) + { + provider.Add(quotesList[i]); + } + + // initialize observer + SmaHub observer = provider + .ToSma(5); + + // fetch initial results (early) + IReadOnlyList streamList + = observer.Results; + + // emulate adding quotes to provider + for (int i = 20; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotesList[80]); + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList + .ToSma(5); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void ChainObserver() + { + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider + for (int i = 0; i < 50; i++) + { + provider.Add(quotesList[i]); + } + + // initialize observer + SmaHub observer = provider + .ToQuotePart(CandlePart.OC2) + .ToSma(11); + + // emulate quote stream + for (int i = 50; i < length; i++) + { + provider.Add(quotesList[i]); + } + + IReadOnlyList streamList = + observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList + .Use(CandlePart.OC2) + .ToSma(11); + + // assert, should equal series + streamList.Should().HaveCount(length); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void ChainProvider() + { + int emaPeriods = 12; + int smaPeriods = 8; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + EmaHub observer + = provider + .ToSma(smaPeriods) + .ToEma(emaPeriods); + + // emulate quote stream + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // final results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList + = quotesList + .ToSma(smaPeriods) + .ToEma(emaPeriods); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public override void CustomToString() + { + SmaHub hub = new(new QuoteHub(), 5); + hub.ToString().Should().Be("SMA(5)"); + } +} diff --git a/tests/indicators/s-z/SmaAnalysis/SmaAnalysis.StaticSeries.Tests.cs b/tests/indicators/s-z/SmaAnalysis/SmaAnalysis.StaticSeries.Tests.cs new file mode 100644 index 000000000..1b41ece3d --- /dev/null +++ b/tests/indicators/s-z/SmaAnalysis/SmaAnalysis.StaticSeries.Tests.cs @@ -0,0 +1,98 @@ +namespace StaticSeries; + +[TestClass] +public class SmaAnalyses : StaticSeriesTestBase +{ + [TestMethod] + public override void Standard() + { + IReadOnlyList results = Quotes + .ToSmaAnalysis(20); + + // proper quantities + Assert.AreEqual(502, results.Count); + Assert.AreEqual(483, results.Count(x => x.Sma != null)); + + // sample value + SmaAnalysis r = results[501]; + Assert.AreEqual(251.86, r.Sma.Round(6)); + Assert.AreEqual(9.450000, r.Mad.Round(6)); + Assert.AreEqual(119.25102, r.Mse.Round(6)); + Assert.AreEqual(0.037637, r.Mape.Round(6)); + } + + [TestMethod] + public void UseReusable() + { + IReadOnlyList results = Quotes + .Use(CandlePart.Close) + .ToSmaAnalysis(20); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(483, results.Count(x => x.Sma != null)); + } + + [TestMethod] + public void Chainee() + { + IReadOnlyList results = Quotes + .ToSma(2) + .ToSmaAnalysis(20); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(482, results.Count(x => x.Sma != null)); + } + + [TestMethod] + public void Chainor() + { + IReadOnlyList results = Quotes + .ToSmaAnalysis(10) + .ToEma(10); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(484, results.Count(x => x.Ema != null)); + } + + [TestMethod] + public override void BadData() + { + IReadOnlyList r = BadQuotes + .ToSmaAnalysis(15); + + Assert.AreEqual(502, r.Count); + Assert.AreEqual(0, r.Count(x => x.Mape is double.NaN)); + } + + [TestMethod] + public override void NoQuotes() + { + IReadOnlyList r0 = Noquotes + .ToSmaAnalysis(6); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = Onequote + .ToSmaAnalysis(6); + + Assert.AreEqual(1, r1.Count); + } + + [TestMethod] + public void Removed() + { + IReadOnlyList results = Quotes + .ToSmaAnalysis(20) + .RemoveWarmupPeriods(); + + // assertions + Assert.AreEqual(502 - 19, results.Count); + Assert.AreEqual(251.8600, Math.Round(results[^1].Sma.Value, 4)); + } + + // bad lookback period + [TestMethod] + public void Exceptions() + => Assert.ThrowsException(() + => Quotes.ToSmaAnalysis(0)); +} diff --git a/tests/indicators/s-z/Smi/Smi.Tests.cs b/tests/indicators/s-z/Smi/Smi.StaticSeries.Tests.cs similarity index 71% rename from tests/indicators/s-z/Smi/Smi.Tests.cs rename to tests/indicators/s-z/Smi/Smi.StaticSeries.Tests.cs index 1d0ecba39..5dc184c73 100644 --- a/tests/indicators/s-z/Smi/Smi.Tests.cs +++ b/tests/indicators/s-z/Smi/Smi.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class SmiTests : TestBase +public class Smi : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetSmi(14, 20, 5, 3) - .ToList(); + IReadOnlyList results = Quotes + .ToSmi(14, 20, 5); // proper quantities Assert.AreEqual(502, results.Count); @@ -48,10 +47,9 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetSmi(14, 20, 5, 3) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToSmi(14, 20, 5) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(480, results.Count(x => x.Sma != null)); @@ -60,9 +58,8 @@ public void Chainor() [TestMethod] public void NoSignal() { - List results = quotes - .GetSmi(5, 20, 20, 1) - .ToList(); + IReadOnlyList results = Quotes + .ToSmi(5, 20, 20, 1); // signal equals oscillator SmiResult r1 = results[487]; @@ -75,9 +72,8 @@ public void NoSignal() [TestMethod] public void SmallPeriods() { - List results = quotes - .GetSmi(1, 1, 1, 5) - .ToList(); + IReadOnlyList results = Quotes + .ToSmi(1, 1, 1, 5); // sample values SmiResult r51 = results[51]; @@ -94,28 +90,25 @@ public void SmallPeriods() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetSmi(5, 5, 1, 5) - .ToList(); + IReadOnlyList r = BadQuotes + .ToSmi(5, 5, 1, 5); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Smi is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Smi is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetSmi(5, 5, 2) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToSmi(5, 5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetSmi(5, 3, 3) - .ToList(); + IReadOnlyList r1 = Onequote + .ToSmi(5, 3, 3); Assert.AreEqual(1, r1.Count); } @@ -123,15 +116,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetSmi(14, 20, 5, 3) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToSmi(14, 20, 5) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(501 - (14 + 100), results.Count); - SmiResult last = results.LastOrDefault(); + SmiResult last = results[^1]; Assert.AreEqual(-52.6560, last.Smi.Round(4)); Assert.AreEqual(-54.1903, last.Signal.Round(4)); } @@ -141,18 +133,18 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetSmi(0, 5, 5, 5)); + Quotes.ToSmi(0, 5, 5, 5)); // bad first smooth period Assert.ThrowsException(() => - quotes.GetSmi(14, 0, 5, 5)); + Quotes.ToSmi(14, 0, 5, 5)); // bad second smooth period Assert.ThrowsException(() => - quotes.GetSmi(14, 3, 0, 5)); + Quotes.ToSmi(14, 3, 0, 5)); // bad signal Assert.ThrowsException(() => - quotes.GetSmi(9, 3, 1, 0)); + Quotes.ToSmi(9, 3, 1, 0)); } } diff --git a/tests/indicators/s-z/Smma/Smma.Tests.cs b/tests/indicators/s-z/Smma/Smma.StaticSeries.Tests.cs similarity index 54% rename from tests/indicators/s-z/Smma/Smma.Tests.cs rename to tests/indicators/s-z/Smma/Smma.StaticSeries.Tests.cs index 27750da1a..b2370be85 100644 --- a/tests/indicators/s-z/Smma/Smma.Tests.cs +++ b/tests/indicators/s-z/Smma/Smma.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class SmmaTests : TestBase +public class Smma : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetSmma(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSmma(20); // proper quantities Assert.AreEqual(502, results.Count); @@ -27,35 +26,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetSmma(20) - .ToList(); + .ToSmma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Smma != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetSmma(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Smma is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetSmma(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToSmma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Smma != null)); @@ -64,38 +50,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetSmma(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToSmma(20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetSmma(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToSmma(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Smma is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Smma is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetSmma(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToSmma(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetSmma(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToSmma(5); Assert.AreEqual(1, r1.Count); } @@ -103,19 +85,18 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetSmma(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToSmma(20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (20 + 100), results.Count); - Assert.AreEqual(255.67462, Math.Round(results.LastOrDefault().Smma.Value, 5)); + Assert.AreEqual(255.67462, Math.Round(results[^1].Smma.Value, 5)); } // bad lookback period [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetSmma(0)); + => Quotes.ToSmma(0)); } diff --git a/tests/indicators/s-z/StarcBands/StarcBands.Tests.cs b/tests/indicators/s-z/StarcBands/StarcBands.StaticSeries.Tests.cs similarity index 70% rename from tests/indicators/s-z/StarcBands/StarcBands.Tests.cs rename to tests/indicators/s-z/StarcBands/StarcBands.StaticSeries.Tests.cs index 08a7a9ce4..86030fed3 100644 --- a/tests/indicators/s-z/StarcBands/StarcBands.Tests.cs +++ b/tests/indicators/s-z/StarcBands/StarcBands.StaticSeries.Tests.cs @@ -1,18 +1,17 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class StarcBandsTests : TestBase +public class StarcBands : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int smaPeriods = 20; int multiplier = 2; int atrPeriods = 14; - List results = quotes - .GetStarcBands(smaPeriods, multiplier, atrPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToStarcBands(smaPeriods, multiplier, atrPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -48,28 +47,25 @@ public void Standard() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetStarcBands(10, 3, 15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToStarcBands(10, 3, 15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.UpperBand is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.UpperBand is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetStarcBands(10) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToStarcBands(10); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetStarcBands(10) - .ToList(); + IReadOnlyList r1 = Onequote + .ToStarcBands(10); Assert.AreEqual(1, r1.Count); } @@ -82,15 +78,14 @@ public void Condense() int atrPeriods = 14; int lookbackPeriods = Math.Max(smaPeriods, atrPeriods); - List results = quotes - .GetStarcBands(smaPeriods, multiplier, atrPeriods) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToStarcBands(smaPeriods, multiplier, atrPeriods) + .Condense(); // assertions Assert.AreEqual(502 - lookbackPeriods + 1, results.Count); - StarcBandsResult last = results.LastOrDefault(); + StarcBandsResult last = results[^1]; Assert.AreEqual(251.8600, last.Centerline.Round(4)); Assert.AreEqual(264.1595, last.UpperBand.Round(4)); Assert.AreEqual(239.5605, last.LowerBand.Round(4)); @@ -104,15 +99,14 @@ public void Removed() int atrPeriods = 14; int lookbackPeriods = Math.Max(smaPeriods, atrPeriods); - List results = quotes - .GetStarcBands(smaPeriods, multiplier, atrPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToStarcBands(smaPeriods, multiplier, atrPeriods) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (lookbackPeriods + 150), results.Count); - StarcBandsResult last = results.LastOrDefault(); + StarcBandsResult last = results[^1]; Assert.AreEqual(251.8600, last.Centerline.Round(4)); Assert.AreEqual(264.1595, last.UpperBand.Round(4)); Assert.AreEqual(239.5605, last.LowerBand.Round(4)); @@ -123,14 +117,14 @@ public void Exceptions() { // bad EMA period Assert.ThrowsException(() => - quotes.GetStarcBands(1, 2, 10)); + Quotes.ToStarcBands(1)); // bad ATR period Assert.ThrowsException(() => - quotes.GetStarcBands(20, 2, 1)); + Quotes.ToStarcBands(20, 2, 1)); // bad multiplier Assert.ThrowsException(() => - quotes.GetStarcBands(20, 0, 10)); + Quotes.ToStarcBands(20, 0)); } } diff --git a/tests/indicators/s-z/Stc/Stc.Tests.cs b/tests/indicators/s-z/Stc/Stc.StaticSeries.Tests.cs similarity index 55% rename from tests/indicators/s-z/Stc/Stc.Tests.cs rename to tests/indicators/s-z/Stc/Stc.StaticSeries.Tests.cs index fda82df9b..9cf0fbb6b 100644 --- a/tests/indicators/s-z/Stc/Stc.Tests.cs +++ b/tests/indicators/s-z/Stc/Stc.StaticSeries.Tests.cs @@ -1,23 +1,17 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class StcTests : TestBase +public class Stc : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int cyclePeriods = 9; int fastPeriods = 12; int slowPeriods = 26; - List results = - quotes.GetStc(cyclePeriods, fastPeriods, slowPeriods) - .ToList(); - - foreach (StcResult r in results) - { - Console.WriteLine($"{r.Date:d},{r.Stc:N4}"); - } + IReadOnlyList results = Quotes + .ToStc(cyclePeriods, fastPeriods, slowPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -36,40 +30,27 @@ public void Standard() StcResult r249 = results[249]; Assert.AreEqual(27.7340, r249.Stc.Round(4)); - StcResult last = results.LastOrDefault(); + StcResult last = results[^1]; Assert.AreEqual(19.2544, last.Stc.Round(4)); } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetStc(9, 12, 26) - .ToList(); + .ToStc(9, 12, 26); Assert.AreEqual(502, results.Count); Assert.AreEqual(467, results.Count(x => x.Stc != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetStc() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Stc is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetStc(9, 12, 26) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToStc(9, 12, 26); Assert.AreEqual(502, results.Count); Assert.AreEqual(466, results.Count(x => x.Stc != null)); @@ -78,38 +59,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetStc(9, 12, 26) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToStc(9, 12, 26) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(458, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetStc(10, 23, 50) - .ToList(); + IReadOnlyList r = BadQuotes + .ToStc(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Stc is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Stc is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetStc() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToStc(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetStc() - .ToList(); + IReadOnlyList r1 = Onequote + .ToStc(); Assert.AreEqual(1, r1.Count); } @@ -121,9 +98,8 @@ public void Issue1107() RandomGbm quotes = new(58); - List results = quotes - .GetStc(10, 23, 50) - .ToList(); + IReadOnlyList results = quotes + .ToStc(); Assert.AreEqual(58, results.Count); } @@ -135,15 +111,14 @@ public void Removed() int fastPeriods = 12; int slowPeriods = 26; - List results = quotes - .GetStc(cyclePeriods, fastPeriods, slowPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToStc(cyclePeriods, fastPeriods, slowPeriods) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (slowPeriods + cyclePeriods + 250), results.Count); - StcResult last = results.LastOrDefault(); + StcResult last = results[^1]; Assert.AreEqual(19.2544, last.Stc.Round(4)); } @@ -152,14 +127,14 @@ public void Exceptions() { // bad fast period Assert.ThrowsException(() => - quotes.GetStc(9, 0, 26)); + Quotes.ToStc(9, 0, 26)); // bad slow periods must be larger than faster period Assert.ThrowsException(() => - quotes.GetStc(9, 12, 12)); + Quotes.ToStc(9, 12, 12)); // bad signal period Assert.ThrowsException(() => - quotes.GetStc(-1, 12, 26)); + Quotes.ToStc(-1, 12, 26)); } } diff --git a/tests/indicators/s-z/StdDev/StdDev.StaticSeries.Tests.cs b/tests/indicators/s-z/StdDev/StdDev.StaticSeries.Tests.cs new file mode 100644 index 000000000..c4fbc30b0 --- /dev/null +++ b/tests/indicators/s-z/StdDev/StdDev.StaticSeries.Tests.cs @@ -0,0 +1,127 @@ +namespace StaticSeries; + +[TestClass] +public class StdDev : StaticSeriesTestBase +{ + [TestMethod] + public override void Standard() + { + IReadOnlyList results = Quotes + .ToStdDev(10); + + // proper quantities + Assert.AreEqual(502, results.Count); + Assert.AreEqual(493, results.Count(x => x.StdDev != null)); + Assert.AreEqual(493, results.Count(x => x.ZScore != null)); + + // sample values + StdDevResult r1 = results[8]; + Assert.AreEqual(null, r1.StdDev); + Assert.AreEqual(null, r1.Mean); + Assert.AreEqual(null, r1.ZScore); + + StdDevResult r2 = results[9]; + Assert.AreEqual(0.5020, r2.StdDev.Round(4)); + Assert.AreEqual(214.0140, r2.Mean.Round(4)); + Assert.AreEqual(-0.525917, r2.ZScore.Round(6)); + + StdDevResult r3 = results[249]; + Assert.AreEqual(0.9827, r3.StdDev.Round(4)); + Assert.AreEqual(257.2200, r3.Mean.Round(4)); + Assert.AreEqual(0.783563, r3.ZScore.Round(6)); + + StdDevResult r4 = results[501]; + Assert.AreEqual(5.4738, r4.StdDev.Round(4)); + Assert.AreEqual(242.4100, r4.Mean.Round(4)); + Assert.AreEqual(0.524312, r4.ZScore.Round(6)); + } + + [TestMethod] + public void UseReusable() + { + IReadOnlyList results = Quotes + .Use(CandlePart.Close) + .ToStdDev(10); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(493, results.Count(x => x.StdDev != null)); + } + + [TestMethod] + public void Chainee() + { + IReadOnlyList results = Quotes + .ToSma(2) + .ToStdDev(10); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(492, results.Count(x => x.StdDev != null)); + } + + [TestMethod] + public void Chainor() + { + IReadOnlyList results = Quotes + .ToStdDev(10) + .ToSma(10); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(484, results.Count(x => x.Sma != null)); + } + + [TestMethod] + public override void BadData() + { + IReadOnlyList r = BadQuotes + .ToStdDev(15); + + Assert.AreEqual(502, r.Count); + Assert.AreEqual(0, r.Count(x => x.StdDev is double.NaN)); + } + + [TestMethod] + public void BigData() + { + IReadOnlyList r = BigQuotes + .ToStdDev(200); + + Assert.AreEqual(1246, r.Count); + } + + [TestMethod] + public override void NoQuotes() + { + IReadOnlyList r0 = Noquotes + .ToStdDev(10); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = Onequote + .ToStdDev(10); + + Assert.AreEqual(1, r1.Count); + } + + [TestMethod] + public void Removed() + { + IReadOnlyList results = Quotes + .ToStdDev(10) + .RemoveWarmupPeriods(); + + // assertions + Assert.AreEqual(502 - 9, results.Count); + + StdDevResult last = results[^1]; + Assert.AreEqual(5.4738, last.StdDev.Round(4)); + Assert.AreEqual(242.4100, last.Mean.Round(4)); + Assert.AreEqual(0.524312, last.ZScore.Round(6)); + } + + [TestMethod] + public void Exceptions() => + + // bad lookback period + Assert.ThrowsException(() => + Quotes.ToStdDev(1)); +} diff --git a/tests/indicators/s-z/StdDev/StdDev.Tests.cs b/tests/indicators/s-z/StdDev/StdDev.Tests.cs deleted file mode 100644 index 54a51fc51..000000000 --- a/tests/indicators/s-z/StdDev/StdDev.Tests.cs +++ /dev/null @@ -1,185 +0,0 @@ -namespace Tests.Indicators; - -[TestClass] -public class StdDevTests : TestBase -{ - [TestMethod] - public void Standard() - { - List results = quotes - .GetStdDev(10) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(493, results.Count(x => x.StdDev != null)); - Assert.AreEqual(493, results.Count(x => x.ZScore != null)); - Assert.AreEqual(false, results.Any(x => x.StdDevSma != null)); - - // sample values - StdDevResult r1 = results[8]; - Assert.AreEqual(null, r1.StdDev); - Assert.AreEqual(null, r1.Mean); - Assert.AreEqual(null, r1.ZScore); - Assert.AreEqual(null, r1.StdDevSma); - - StdDevResult r2 = results[9]; - Assert.AreEqual(0.5020, r2.StdDev.Round(4)); - Assert.AreEqual(214.0140, r2.Mean.Round(4)); - Assert.AreEqual(-0.525917, r2.ZScore.Round(6)); - Assert.AreEqual(null, r2.StdDevSma); - - StdDevResult r3 = results[249]; - Assert.AreEqual(0.9827, r3.StdDev.Round(4)); - Assert.AreEqual(257.2200, r3.Mean.Round(4)); - Assert.AreEqual(0.783563, r3.ZScore.Round(6)); - Assert.AreEqual(null, r3.StdDevSma); - - StdDevResult r4 = results[501]; - Assert.AreEqual(5.4738, r4.StdDev.Round(4)); - Assert.AreEqual(242.4100, r4.Mean.Round(4)); - Assert.AreEqual(0.524312, r4.ZScore.Round(6)); - Assert.AreEqual(null, r4.StdDevSma); - } - - [TestMethod] - public void UseTuple() - { - List results = quotes - .Use(CandlePart.Close) - .GetStdDev(10) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(493, results.Count(x => x.StdDev != null)); - } - - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetStdDev(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.StdDev is double and double.NaN)); - } - - [TestMethod] - public void Chainee() - { - List results = quotes - .GetSma(2) - .GetStdDev(10) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(492, results.Count(x => x.StdDev != null)); - } - - [TestMethod] - public void Chainor() - { - List results = quotes - .GetStdDev(10) - .GetSma(10) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(484, results.Count(x => x.Sma != null)); - } - - [TestMethod] - public void WithSma() - { - int lookbackPeriods = 10; - int smaPeriods = 5; - List results = quotes - .GetStdDev(lookbackPeriods, smaPeriods) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(493, results.Count(x => x.StdDev != null)); - Assert.AreEqual(493, results.Count(x => x.ZScore != null)); - Assert.AreEqual(489, results.Count(x => x.StdDevSma != null)); - - // sample values - StdDevResult r1 = results[19]; - Assert.AreEqual(1.1642, r1.StdDev.Round(4)); - Assert.AreEqual(-0.065282, r1.ZScore.Round(6)); - Assert.AreEqual(1.1422, r1.StdDevSma.Round(4)); - - StdDevResult r2 = results[501]; - Assert.AreEqual(5.4738, r2.StdDev.Round(4)); - Assert.AreEqual(0.524312, r2.ZScore.Round(6)); - Assert.AreEqual(7.6886, r2.StdDevSma.Round(4)); - } - - [TestMethod] - public void BadData() - { - List r = badQuotes - .GetStdDev(15, 3) - .ToList(); - - Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.StdDev is double and double.NaN)); - } - - [TestMethod] - public void BigData() - { - List r = bigQuotes - .GetStdDev(200, 3) - .ToList(); - - Assert.AreEqual(1246, r.Count); - } - - [TestMethod] - public void NoQuotes() - { - List r0 = noquotes - .GetStdDev(10) - .ToList(); - - Assert.AreEqual(0, r0.Count); - - List r1 = onequote - .GetStdDev(10) - .ToList(); - - Assert.AreEqual(1, r1.Count); - } - - [TestMethod] - public void Removed() - { - List results = quotes - .GetStdDev(10) - .RemoveWarmupPeriods() - .ToList(); - - // assertions - Assert.AreEqual(502 - 9, results.Count); - - StdDevResult last = results.LastOrDefault(); - Assert.AreEqual(5.4738, last.StdDev.Round(4)); - Assert.AreEqual(242.4100, last.Mean.Round(4)); - Assert.AreEqual(0.524312, last.ZScore.Round(6)); - Assert.AreEqual(null, last.StdDevSma); - } - - [TestMethod] - public void Exceptions() - { - // bad lookback period - Assert.ThrowsException(() => - quotes.GetStdDev(1)); - - // bad SMA period - Assert.ThrowsException(() => - quotes.GetStdDev(14, 0)); - } -} diff --git a/tests/indicators/s-z/StdDevChannels/StdDevChannels.Tests.cs b/tests/indicators/s-z/StdDevChannels/StdDevChannels.StaticSeries.Tests.cs similarity index 73% rename from tests/indicators/s-z/StdDevChannels/StdDevChannels.Tests.cs rename to tests/indicators/s-z/StdDevChannels/StdDevChannels.StaticSeries.Tests.cs index ad6abad22..dc145f82b 100644 --- a/tests/indicators/s-z/StdDevChannels/StdDevChannels.Tests.cs +++ b/tests/indicators/s-z/StdDevChannels/StdDevChannels.StaticSeries.Tests.cs @@ -1,17 +1,16 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class StdDevChannelsTests : TestBase +public class StdDevChannels : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { int lookbackPeriods = 20; double standardDeviations = 2; - List results = - quotes.GetStdDevChannels(lookbackPeriods, standardDeviations) - .ToList(); + IReadOnlyList results = + Quotes.ToStdDevChannels(lookbackPeriods, standardDeviations); // proper quantities Assert.AreEqual(502, results.Count); @@ -68,9 +67,8 @@ public void FullHistory() { // null provided for lookback period - List results = - quotes.GetStdDevChannels(null, 2) - .ToList(); + IReadOnlyList results = + Quotes.ToStdDevChannels(null); // proper quantities Assert.AreEqual(502, results.Count); @@ -98,63 +96,47 @@ public void FullHistory() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetStdDevChannels(20, 2) - .ToList(); + .ToStdDevChannels(); Assert.AreEqual(502, results.Count); Assert.AreEqual(500, results.Count(x => x.Centerline != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetStdDevChannels(6, 1.1) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.UpperChannel is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetStdDevChannels(20, 2) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToStdDevChannels(); Assert.AreEqual(502, results.Count); Assert.AreEqual(500, results.Count(x => x.Centerline != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetStdDevChannels() - .ToList(); + IReadOnlyList r = BadQuotes + .ToStdDevChannels(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.UpperChannel is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.UpperChannel is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetStdDevChannels() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToStdDevChannels(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetStdDevChannels() - .ToList(); + IReadOnlyList r1 = Onequote + .ToStdDevChannels(); Assert.AreEqual(1, r1.Count); } @@ -165,14 +147,13 @@ public void Condense() int lookbackPeriods = 20; double standardDeviations = 2; - List results = quotes - .GetStdDevChannels(lookbackPeriods, standardDeviations) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToStdDevChannels(lookbackPeriods, standardDeviations) + .Condense(); // assertions Assert.AreEqual(500, results.Count); - StdDevChannelsResult last = results.LastOrDefault(); + StdDevChannelsResult last = results[^1]; Assert.AreEqual(235.8131, last.Centerline.Round(4)); Assert.AreEqual(257.6536, last.UpperChannel.Round(4)); Assert.AreEqual(213.9727, last.LowerChannel.Round(4)); @@ -185,14 +166,13 @@ public void Removed() int lookbackPeriods = 20; double standardDeviations = 2; - List results = quotes - .GetStdDevChannels(lookbackPeriods, standardDeviations) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToStdDevChannels(lookbackPeriods, standardDeviations) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(500, results.Count); - StdDevChannelsResult last = results.LastOrDefault(); + StdDevChannelsResult last = results[^1]; Assert.AreEqual(235.8131, last.Centerline.Round(4)); Assert.AreEqual(257.6536, last.UpperChannel.Round(4)); Assert.AreEqual(213.9727, last.LowerChannel.Round(4)); @@ -204,10 +184,10 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetStdDevChannels(0)); + Quotes.ToStdDevChannels(0)); // bad standard deviations Assert.ThrowsException(() => - quotes.GetStdDevChannels(20, 0)); + Quotes.ToStdDevChannels(20, 0)); } } diff --git a/tests/indicators/s-z/Stoch/Stoch.Tests.cs b/tests/indicators/s-z/Stoch/Stoch.StaticSeries.Tests.cs similarity index 75% rename from tests/indicators/s-z/Stoch/Stoch.Tests.cs rename to tests/indicators/s-z/Stoch/Stoch.StaticSeries.Tests.cs index 9abee8370..6e8fe7a1b 100644 --- a/tests/indicators/s-z/Stoch/Stoch.Tests.cs +++ b/tests/indicators/s-z/Stoch/Stoch.StaticSeries.Tests.cs @@ -1,18 +1,17 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class StochTests : TestBase +public class Stoch : StaticSeriesTestBase { [TestMethod] - public void Standard() // Slow + public override void Standard() // Slow { int lookbackPeriods = 14; int signalPeriods = 3; int smoothPeriods = 3; - List results = quotes - .GetStoch(lookbackPeriods, signalPeriods, smoothPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToStoch(lookbackPeriods, signalPeriods, smoothPeriods); // proper quantities Assert.AreEqual(502, results.Count); @@ -47,10 +46,8 @@ public void Standard() // Slow // test boundary condition - for (int i = 0; i < results.Count; i++) + foreach (StochResult r in results) { - StochResult r = results[i]; - if (r.Oscillator is not null) { Assert.IsTrue(r.Oscillator >= 0); @@ -72,11 +69,10 @@ public void Standard() // Slow } [TestMethod] - public void Extended() // with extra parameteres + public void Extended() // with extra parameters { - List results = - quotes.GetStoch(9, 3, 3, 5, 4, MaType.SMMA) - .ToList(); + IReadOnlyList results = + Quotes.ToStoch(9, 3, 3, 5, 4, MaType.SMMA); // proper quantities Assert.AreEqual(502, results.Count); @@ -118,10 +114,9 @@ public void Extended() // with extra parameteres [TestMethod] public void Chainor() { - List results = quotes - .GetStoch() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToStoch() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(478, results.Count(x => x.Sma != null)); @@ -130,13 +125,12 @@ public void Chainor() [TestMethod] public void NoSignal() { - int lookbackPeriods = 5; - int signalPeriods = 1; - int smoothPeriods = 3; + const int lookbackPeriods = 5; + const int signalPeriods = 1; + const int smoothPeriods = 3; - List results = quotes - .GetStoch(lookbackPeriods, signalPeriods, smoothPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToStoch(lookbackPeriods, signalPeriods, smoothPeriods); // signal equals oscillator StochResult r1 = results[487]; @@ -153,9 +147,8 @@ public void Fast() int signalPeriods = 10; int smoothPeriods = 1; - List results = quotes - .GetStoch(lookbackPeriods, signalPeriods, smoothPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToStoch(lookbackPeriods, signalPeriods, smoothPeriods); // sample values StochResult r1 = results[487]; @@ -174,9 +167,8 @@ public void FastSmall() int signalPeriods = 10; int smoothPeriods = 1; - List results = quotes - .GetStoch(lookbackPeriods, signalPeriods, smoothPeriods) - .ToList(); + IReadOnlyList results = Quotes + .ToStoch(lookbackPeriods, signalPeriods, smoothPeriods); // sample values StochResult r1 = results[70]; @@ -187,28 +179,25 @@ public void FastSmall() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetStoch(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToStoch(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Oscillator is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Oscillator is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetStoch() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToStoch(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetStoch() - .ToList(); + IReadOnlyList r1 = Onequote + .ToStoch(); Assert.AreEqual(1, r1.Count); } @@ -220,15 +209,14 @@ public void Removed() int signalPeriods = 3; int smoothPeriods = 3; - List results = quotes - .GetStoch(lookbackPeriods, signalPeriods, smoothPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToStoch(lookbackPeriods, signalPeriods, smoothPeriods) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (lookbackPeriods + smoothPeriods - 2), results.Count); - StochResult last = results.LastOrDefault(); + StochResult last = results[^1]; Assert.AreEqual(43.1353, last.Oscillator.Round(4)); Assert.AreEqual(35.5674, last.Signal.Round(4)); Assert.AreEqual(58.2712, last.PercentJ.Round(4)); @@ -241,10 +229,9 @@ public void Boundary() int signalPeriods = 3; int smoothPeriods = 3; - List results = TestData + IReadOnlyList results = Data .GetRandom(2500) - .GetStoch(lookbackPeriods, signalPeriods, smoothPeriods) - .ToList(); + .ToStoch(lookbackPeriods, signalPeriods, smoothPeriods); // test boundary condition @@ -277,26 +264,26 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetStoch(0)); + Quotes.ToStoch(0)); // bad signal period Assert.ThrowsException(() => - quotes.GetStoch(14, 0)); + Quotes.ToStoch(14, 0)); // bad smoothing period Assert.ThrowsException(() => - quotes.GetStoch(14, 3, 0)); + Quotes.ToStoch(14, 3, 0)); // bad kFactor Assert.ThrowsException(() => - quotes.GetStoch(9, 3, 1, 0, 2, MaType.SMA)); + Quotes.ToStoch(9, 3, 1, 0, 2, MaType.SMA)); // bad dFactor Assert.ThrowsException(() => - quotes.GetStoch(9, 3, 1, 3, 0, MaType.SMA)); + Quotes.ToStoch(9, 3, 1, 3, 0, MaType.SMA)); // bad MA type Assert.ThrowsException(() => - quotes.GetStoch(9, 3, 3, 3, 2, MaType.ALMA)); + Quotes.ToStoch(9, 3, 3, 3, 2, MaType.ALMA)); } } diff --git a/tests/indicators/s-z/StochRsi/StochRsi.Tests.cs b/tests/indicators/s-z/StochRsi/StochRsi.StaticSeries.Tests.cs similarity index 65% rename from tests/indicators/s-z/StochRsi/StochRsi.Tests.cs rename to tests/indicators/s-z/StochRsi/StochRsi.StaticSeries.Tests.cs index 966e9b40f..2efbd0983 100644 --- a/tests/indicators/s-z/StochRsi/StochRsi.Tests.cs +++ b/tests/indicators/s-z/StochRsi/StochRsi.StaticSeries.Tests.cs @@ -1,19 +1,18 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class StochRsiTests : TestBase +public class StochRsi : StaticSeriesTestBase { [TestMethod] - public void FastRsi() + public override void Standard() // Fast RSI { int rsiPeriods = 14; int stochPeriods = 14; int signalPeriods = 3; int smoothPeriods = 1; - List results = - quotes.GetStochRsi(rsiPeriods, stochPeriods, signalPeriods, smoothPeriods) - .ToList(); + IReadOnlyList results = + Quotes.ToStochRsi(rsiPeriods, stochPeriods, signalPeriods, smoothPeriods); // assertions @@ -48,9 +47,8 @@ public void SlowRsi() int signalPeriods = 3; int smoothPeriods = 3; - List results = - quotes.GetStochRsi(rsiPeriods, stochPeriods, signalPeriods, smoothPeriods) - .ToList(); + IReadOnlyList results = + Quotes.ToStochRsi(rsiPeriods, stochPeriods, signalPeriods, smoothPeriods); // assertions @@ -78,36 +76,23 @@ public void SlowRsi() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetStochRsi(14, 14, 3, 1) - .ToList(); + .ToStochRsi(14, 14, 3); Assert.AreEqual(502, results.Count); Assert.AreEqual(475, results.Count(x => x.StochRsi != null)); - Assert.AreEqual(0, results.Count(x => x.StochRsi is double and double.NaN)); - } - - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetStochRsi(14, 14, 3, 1) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.StochRsi is double and double.NaN)); + Assert.AreEqual(0, results.Count(x => x.StochRsi is double.NaN)); } [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetStochRsi(14, 14, 3, 1) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToStochRsi(14, 14, 3); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.StochRsi != null)); @@ -116,38 +101,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetStochRsi(14, 14, 3, 3) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToStochRsi(14, 14, 3, 3) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(464, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetStochRsi(15, 20, 3, 2) - .ToList(); + IReadOnlyList r = BadQuotes + .ToStochRsi(15, 20, 3, 2); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.StochRsi is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.StochRsi is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetStochRsi(10, 20, 3) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToStochRsi(10, 20, 3); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetStochRsi(8, 13, 2) - .ToList(); + IReadOnlyList r1 = Onequote + .ToStochRsi(8, 13, 2); Assert.AreEqual(1, r1.Count); } @@ -160,16 +141,15 @@ public void Removed() int signalPeriods = 3; int smoothPeriods = 3; - List results = quotes - .GetStochRsi(rsiPeriods, stochPeriods, signalPeriods, smoothPeriods) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToStochRsi(rsiPeriods, stochPeriods, signalPeriods, smoothPeriods) + .RemoveWarmupPeriods(); // assertions int removeQty = rsiPeriods + stochPeriods + smoothPeriods + 100; Assert.AreEqual(502 - removeQty, results.Count); - StochRsiResult last = results.LastOrDefault(); + StochRsiResult last = results[^1]; Assert.AreEqual(89.8385, last.StochRsi.Round(4)); Assert.AreEqual(73.4176, last.Signal.Round(4)); } @@ -179,18 +159,18 @@ public void Exceptions() { // bad RSI period Assert.ThrowsException(() => - quotes.GetStochRsi(0, 14, 3, 1)); + Quotes.ToStochRsi(0, 14, 3)); // bad STO period Assert.ThrowsException(() => - quotes.GetStochRsi(14, 0, 3, 3)); + Quotes.ToStochRsi(14, 0, 3, 3)); // bad STO signal period Assert.ThrowsException(() => - quotes.GetStochRsi(14, 14, 0)); + Quotes.ToStochRsi(14, 14, 0)); // bad STO smoothing period Assert.ThrowsException(() => - quotes.GetStochRsi(14, 14, 3, 0)); + Quotes.ToStochRsi(14, 14, 3, 0)); } } diff --git a/tests/indicators/s-z/SuperTrend/SuperTrend.Tests.cs b/tests/indicators/s-z/SuperTrend/SuperTrend.StaticSeries.Tests.cs similarity index 68% rename from tests/indicators/s-z/SuperTrend/SuperTrend.Tests.cs rename to tests/indicators/s-z/SuperTrend/SuperTrend.StaticSeries.Tests.cs index 47232f3fd..3b652b756 100644 --- a/tests/indicators/s-z/SuperTrend/SuperTrend.Tests.cs +++ b/tests/indicators/s-z/SuperTrend/SuperTrend.StaticSeries.Tests.cs @@ -1,17 +1,16 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class SuperTrendTests : TestBase +public class SuperTrend : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - int lookbackPeriods = 14; - double multiplier = 3; + const int lookbackPeriods = 14; + const double multiplier = 3; - List results = quotes - .GetSuperTrend(lookbackPeriods, multiplier) - .ToList(); + IReadOnlyList results = Quotes + .ToSuperTrend(lookbackPeriods, multiplier); // proper quantities Assert.AreEqual(502, results.Count); @@ -52,11 +51,10 @@ public void Standard() [TestMethod] public void Bitcoin() { - IEnumerable h = TestData.GetBitcoin(); + IReadOnlyList h = Data.GetBitcoin(); - List results = h - .GetSuperTrend(10, 3) - .ToList(); + IReadOnlyList results = h + .ToSuperTrend(); Assert.AreEqual(1246, results.Count); @@ -65,27 +63,24 @@ public void Bitcoin() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetSuperTrend(7) - .ToList(); + IReadOnlyList r = BadQuotes + .ToSuperTrend(7); Assert.AreEqual(502, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetSuperTrend() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToSuperTrend(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetSuperTrend() - .ToList(); + IReadOnlyList r1 = Onequote + .ToSuperTrend(); Assert.AreEqual(1, r1.Count); } @@ -96,15 +91,14 @@ public void Condense() int lookbackPeriods = 14; double multiplier = 3; - List results = quotes - .GetSuperTrend(lookbackPeriods, multiplier) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToSuperTrend(lookbackPeriods, multiplier) + .Condense(); // assertions Assert.AreEqual(488, results.Count); - SuperTrendResult last = results.LastOrDefault(); + SuperTrendResult last = results[^1]; Assert.AreEqual(250.7954m, last.SuperTrend.Round(4)); Assert.AreEqual(last.SuperTrend, last.UpperBand); Assert.AreEqual(null, last.LowerBand); @@ -116,15 +110,14 @@ public void Removed() int lookbackPeriods = 14; double multiplier = 3; - List results = quotes - .GetSuperTrend(lookbackPeriods, multiplier) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToSuperTrend(lookbackPeriods, multiplier) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(488, results.Count); - SuperTrendResult last = results.LastOrDefault(); + SuperTrendResult last = results[^1]; Assert.AreEqual(250.7954m, last.SuperTrend.Round(4)); Assert.AreEqual(last.SuperTrend, last.UpperBand); Assert.AreEqual(null, last.LowerBand); @@ -135,10 +128,10 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetSuperTrend(1)); + Quotes.ToSuperTrend(1)); // bad multiplier Assert.ThrowsException(() => - quotes.GetSuperTrend(7, 0)); + Quotes.ToSuperTrend(7, 0)); } } diff --git a/tests/indicators/s-z/T3/T3.Tests.cs b/tests/indicators/s-z/T3/T3.StaticSeries.Tests.cs similarity index 57% rename from tests/indicators/s-z/T3/T3.Tests.cs rename to tests/indicators/s-z/T3/T3.StaticSeries.Tests.cs index 145a773e2..5cb40775b 100644 --- a/tests/indicators/s-z/T3/T3.Tests.cs +++ b/tests/indicators/s-z/T3/T3.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class T3Tests : TestBase +public class T3 : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetT3(5, 0.7) - .ToList(); + IReadOnlyList results = Quotes + .ToT3(); // proper quantities Assert.AreEqual(502, results.Count); @@ -35,35 +34,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetT3() - .ToList(); + .ToT3(); Assert.AreEqual(502, results.Count); Assert.AreEqual(502, results.Count(x => x.T3 != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetT3() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.T3 is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetT3() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToT3(); Assert.AreEqual(502, results.Count); Assert.AreEqual(501, results.Count(x => x.T3 != null)); @@ -72,37 +58,33 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetT3() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToT3() + .ToSma(10); Assert.AreEqual(502, results.Count); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetT3() - .ToList(); + IReadOnlyList r = BadQuotes + .ToT3(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.T3 is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.T3 is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetT3() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToT3(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetT3() - .ToList(); + IReadOnlyList r1 = Onequote + .ToT3(); Assert.AreEqual(1, r1.Count); } @@ -112,10 +94,10 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetT3(0)); + Quotes.ToT3(0)); // bad volume factor Assert.ThrowsException(() => - quotes.GetT3(25, 0)); + Quotes.ToT3(25, 0)); } } diff --git a/tests/indicators/s-z/Tema/Tema.Tests.cs b/tests/indicators/s-z/Tema/Tema.StaticSeries.Tests.cs similarity index 51% rename from tests/indicators/s-z/Tema/Tema.Tests.cs rename to tests/indicators/s-z/Tema/Tema.StaticSeries.Tests.cs index 7970b480e..23e39b123 100644 --- a/tests/indicators/s-z/Tema/Tema.Tests.cs +++ b/tests/indicators/s-z/Tema/Tema.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class TemaTests : TestBase +public class Tema : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetTema(20) - .ToList(); + IReadOnlyList results = Quotes + .ToTema(20); // proper quantities Assert.AreEqual(502, results.Count); @@ -29,35 +28,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetTema(20) - .ToList(); + .ToTema(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Tema != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetTema(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Tema is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetTema(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToTema(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Tema != null)); @@ -66,38 +52,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetTema(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToTema(20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetTema(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToTema(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Tema is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Tema is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetTema(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToTema(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetTema(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToTema(5); Assert.AreEqual(1, r1.Count); } @@ -105,15 +87,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetTema(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToTema(20) + .RemoveWarmupPeriods(); // assertions - Assert.AreEqual(502 - ((3 * 20) + 100), results.Count); + Assert.AreEqual(502 - (3 * 20 + 100), results.Count); - TemaResult last = results.LastOrDefault(); + TemaResult last = results[^1]; Assert.AreEqual(238.7690, last.Tema.Round(4)); } @@ -121,5 +102,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetTema(0)); + => Quotes.ToTema(0)); } diff --git a/tests/indicators/s-z/Tr/Tr.Tests.cs b/tests/indicators/s-z/Tr/Tr.StaticSeries.Tests.cs similarity index 61% rename from tests/indicators/s-z/Tr/Tr.Tests.cs rename to tests/indicators/s-z/Tr/Tr.StaticSeries.Tests.cs index c7fbebe54..e44f90c03 100644 --- a/tests/indicators/s-z/Tr/Tr.Tests.cs +++ b/tests/indicators/s-z/Tr/Tr.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class TrTests : TestBase +public class Tr : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetTr() - .ToList(); + IReadOnlyList results = Quotes + .ToTr(); // proper quantities Assert.AreEqual(502, results.Count); @@ -41,48 +40,43 @@ public void Standard() public void Chainor() { // same as ATR - List results = quotes - .GetTr() - .GetSmma(14) - .ToList(); + IReadOnlyList results = Quotes + .ToTr() + .ToSmma(14); - List atrResults = quotes - .GetAtr(14) - .ToList(); + IReadOnlyList atrResults = Quotes + .ToAtr(); for (int i = 0; i < results.Count; i++) { SmmaResult r = results[i]; AtrResult a = atrResults[i]; - Assert.AreEqual(a.Date, r.Date); + Assert.AreEqual(a.Timestamp, r.Timestamp); Assert.AreEqual(a.Atr, r.Smma); } } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetTr() - .ToList(); + IReadOnlyList r = BadQuotes + .ToTr(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Tr is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Tr is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetTr() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToTr(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetTr() - .ToList(); + IReadOnlyList r1 = Onequote + .ToTr(); Assert.AreEqual(1, r1.Count); } diff --git a/tests/indicators/s-z/Tr/Tr.StreamHub.Tests.cs b/tests/indicators/s-z/Tr/Tr.StreamHub.Tests.cs new file mode 100644 index 000000000..b67bcc866 --- /dev/null +++ b/tests/indicators/s-z/Tr/Tr.StreamHub.Tests.cs @@ -0,0 +1,120 @@ +namespace StreamHub; + +[TestClass] +public class TrHub : StreamHubTestBase, ITestChainProvider +{ + [TestMethod] + public override void QuoteObserver() + { + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // prefill quotes to provider + for (int i = 0; i < 20; i++) + { + provider.Add(quotesList[i]); + } + + // initialize observer + StreamHub observer = provider + .ToTr(); + + // fetch initial results (early) + IReadOnlyList streamList + = observer.Results; + + // emulate adding quotes to provider + for (int i = 20; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotesList[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotesList[80]); + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // time-series, for comparison + IReadOnlyList seriesList = quotesList + .ToTr(); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void ChainProvider() + { + int smaPeriods = 8; + + List quotesList = Quotes.ToList(); + + int length = quotesList.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observer + IChainProvider adlHub = provider + .ToTr(); + + SmaHub observer = adlHub + .ToSma(smaPeriods); + + // emulate quote stream + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + // delete + provider.Remove(quotesList[400]); + quotesList.RemoveAt(400); + + // final results + IReadOnlyList streamList + = observer.Results; + + // time-series, for comparison + IReadOnlyList seriesList = quotesList + .ToTr() + .ToSma(smaPeriods); + + // assert, should equal series + streamList.Should().HaveCount(length - 1); + streamList.Should().BeEquivalentTo(seriesList); + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public override void CustomToString() + { + TrHub hub = new(new QuoteHub()); + hub.ToString().Should().Be("TRUE RANGE"); + } +} diff --git a/tests/indicators/s-z/Trix/Trix.Tests.cs b/tests/indicators/s-z/Trix/Trix.StaticSeries.Tests.cs similarity index 51% rename from tests/indicators/s-z/Trix/Trix.Tests.cs rename to tests/indicators/s-z/Trix/Trix.StaticSeries.Tests.cs index cd3d52d92..626d544b5 100644 --- a/tests/indicators/s-z/Trix/Trix.Tests.cs +++ b/tests/indicators/s-z/Trix/Trix.StaticSeries.Tests.cs @@ -1,73 +1,54 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class TrixTests : TestBase +public class Trix : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetTrix(20, 5) - .ToList(); + IReadOnlyList results = Quotes + .ToTrix(20); // proper quantities Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Ema3 != null)); Assert.AreEqual(482, results.Count(x => x.Trix != null)); - Assert.AreEqual(478, results.Count(x => x.Signal != null)); // sample values TrixResult r24 = results[24]; Assert.AreEqual(214.5486, r24.Ema3.Round(4)); Assert.AreEqual(0.005047, r24.Trix.Round(6)); - Assert.AreEqual(0.002196, r24.Signal.Round(6)); TrixResult r67 = results[67]; Assert.AreEqual(221.7837, r67.Ema3.Round(4)); Assert.AreEqual(0.050030, r67.Trix.Round(6)); - Assert.AreEqual(0.057064, r67.Signal.Round(6)); TrixResult r249 = results[249]; Assert.AreEqual(249.4469, r249.Ema3.Round(4)); Assert.AreEqual(0.121781, r249.Trix.Round(6)); - Assert.AreEqual(0.119769, r249.Signal.Round(6)); TrixResult r501 = results[501]; Assert.AreEqual(263.3216, r501.Ema3.Round(4)); Assert.AreEqual(-0.230742, r501.Trix.Round(6)); - Assert.AreEqual(-0.204536, r501.Signal.Round(6)); } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetTrix(20, 5) - .ToList(); + .ToTrix(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Trix != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetTrix(6, 2) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Trix is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetTrix(20, 5) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToTrix(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(481, results.Count(x => x.Trix != null)); @@ -76,38 +57,34 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetTrix(20, 5) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToTrix(20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(473, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetTrix(15, 2) - .ToList(); + IReadOnlyList r = BadQuotes + .ToTrix(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Trix is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Trix is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetTrix(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToTrix(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetTrix(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToTrix(5); Assert.AreEqual(1, r1.Count); } @@ -115,23 +92,21 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetTrix(20, 5) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToTrix(20) + .RemoveWarmupPeriods(); // assertions - Assert.AreEqual(502 - ((3 * 20) + 100), results.Count); + Assert.AreEqual(502 - (3 * 20 + 100), results.Count); - TrixResult last = results.LastOrDefault(); + TrixResult last = results[^1]; Assert.AreEqual(263.3216, last.Ema3.Round(4)); Assert.AreEqual(-0.230742, last.Trix.Round(6)); - Assert.AreEqual(-0.204536, last.Signal.Round(6)); } // bad lookback period [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetTrix(0)); + => Quotes.ToTrix(0)); } diff --git a/tests/indicators/s-z/Tsi/Tsi.Tests.cs b/tests/indicators/s-z/Tsi/Tsi.StaticSeries.Tests.cs similarity index 55% rename from tests/indicators/s-z/Tsi/Tsi.Tests.cs rename to tests/indicators/s-z/Tsi/Tsi.StaticSeries.Tests.cs index 84b8d7776..ec96ba655 100644 --- a/tests/indicators/s-z/Tsi/Tsi.Tests.cs +++ b/tests/indicators/s-z/Tsi/Tsi.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class TsiTests : TestBase +public class Tsi : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetTsi(25, 13, 7) - .ToList(); + IReadOnlyList results = Quotes + .ToTsi(); // proper quantities Assert.AreEqual(502, results.Count); @@ -20,13 +19,13 @@ public void Standard() Assert.AreEqual(53.1204, r2.Tsi.Round(4)); Assert.AreEqual(null, r2.Signal); - TsiResult r3a = results[43]; - Assert.AreEqual(46.0960, r3a.Tsi.Round(4)); - Assert.AreEqual(51.6916, r3a.Signal.Round(4)); + TsiResult r3A = results[43]; + Assert.AreEqual(46.0960, r3A.Tsi.Round(4)); + Assert.AreEqual(51.6916, r3A.Signal.Round(4)); - TsiResult r3b = results[44]; - Assert.AreEqual(42.5121, r3b.Tsi.Round(4)); - Assert.AreEqual(49.3967, r3b.Signal.Round(4)); + TsiResult r3B = results[44]; + Assert.AreEqual(42.5121, r3B.Tsi.Round(4)); + Assert.AreEqual(49.3967, r3B.Signal.Round(4)); TsiResult r4 = results[149]; Assert.AreEqual(29.0936, r4.Tsi.Round(4)); @@ -42,35 +41,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetTsi() - .ToList(); + .ToTsi(); Assert.AreEqual(502, results.Count); Assert.AreEqual(465, results.Count(x => x.Tsi != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetTsi() - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Tsi is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetTsi() - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToTsi(); Assert.AreEqual(502, results.Count); Assert.AreEqual(464, results.Count(x => x.Tsi != null)); @@ -79,48 +65,43 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetTsi() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToTsi() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(456, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetTsi() - .ToList(); + IReadOnlyList r = BadQuotes + .ToTsi(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Tsi is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Tsi is double.NaN)); } [TestMethod] public void BigData() { - List r = bigQuotes - .GetTsi() - .ToList(); + IReadOnlyList r = BigQuotes + .ToTsi(); Assert.AreEqual(1246, r.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetTsi() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToTsi(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetTsi() - .ToList(); + IReadOnlyList r1 = Onequote + .ToTsi(); Assert.AreEqual(1, r1.Count); } @@ -128,15 +109,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetTsi(25, 13, 7) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToTsi() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - (25 + 13 + 250), results.Count); - TsiResult last = results.LastOrDefault(); + TsiResult last = results[^1]; Assert.AreEqual(-28.3513, last.Tsi.Round(4)); Assert.AreEqual(-29.3597, last.Signal.Round(4)); } @@ -146,14 +126,14 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetTsi(0)); + Quotes.ToTsi(0)); // bad smoothing period Assert.ThrowsException(() => - quotes.GetTsi(25, 0)); + Quotes.ToTsi(25, 0)); // bad signal period Assert.ThrowsException(() => - quotes.GetTsi(25, 13, -1)); + Quotes.ToTsi(25, 13, -1)); } } diff --git a/tests/indicators/s-z/UlcerIndex/UlcerIndex.StaticSeries.Tests.cs b/tests/indicators/s-z/UlcerIndex/UlcerIndex.StaticSeries.Tests.cs new file mode 100644 index 000000000..8e08fa5f7 --- /dev/null +++ b/tests/indicators/s-z/UlcerIndex/UlcerIndex.StaticSeries.Tests.cs @@ -0,0 +1,97 @@ +namespace StaticSeries; + +[TestClass] +public class UlcerIndex : StaticSeriesTestBase +{ + [TestMethod] + public override void Standard() + { + IReadOnlyList results = Quotes + .ToUlcerIndex(); + + // proper quantities + Assert.AreEqual(502, results.Count); + Assert.AreEqual(489, results.Count(x => x.UlcerIndex != null)); + + // sample value + UlcerIndexResult r = results[501]; + Assert.AreEqual(5.7255, r.UlcerIndex.Round(4)); + } + + [TestMethod] + public void UseReusable() + { + IReadOnlyList results = Quotes + .Use(CandlePart.Close) + .ToUlcerIndex(); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(489, results.Count(x => x.UlcerIndex != null)); + } + + [TestMethod] + public void Chainee() + { + IReadOnlyList results = Quotes + .ToSma(2) + .ToUlcerIndex(); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(488, results.Count(x => x.UlcerIndex != null)); + } + + [TestMethod] + public void Chainor() + { + IReadOnlyList results = Quotes + .ToUlcerIndex() + .ToSma(10); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(480, results.Count(x => x.Sma != null)); + } + + [TestMethod] + public override void BadData() + { + IReadOnlyList r = BadQuotes + .ToUlcerIndex(15); + + Assert.AreEqual(502, r.Count); + Assert.AreEqual(0, r.Count(x => x.UlcerIndex is double.NaN)); + } + + [TestMethod] + public override void NoQuotes() + { + IReadOnlyList r0 = Noquotes + .ToUlcerIndex(); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = Onequote + .ToUlcerIndex(); + + Assert.AreEqual(1, r1.Count); + } + + [TestMethod] + public void Removed() + { + IReadOnlyList results = Quotes + .ToUlcerIndex() + .RemoveWarmupPeriods(); + + // assertions + Assert.AreEqual(502 - 13, results.Count); + + UlcerIndexResult last = results[^1]; + Assert.AreEqual(5.7255, last.UlcerIndex.Round(4)); + } + + // bad lookback period + [TestMethod] + public void Exceptions() + => Assert.ThrowsException(() + => Quotes.ToUlcerIndex(0)); +} diff --git a/tests/indicators/s-z/UlcerIndex/UlcerIndex.Tests.cs b/tests/indicators/s-z/UlcerIndex/UlcerIndex.Tests.cs deleted file mode 100644 index f94d4e3f3..000000000 --- a/tests/indicators/s-z/UlcerIndex/UlcerIndex.Tests.cs +++ /dev/null @@ -1,116 +0,0 @@ -namespace Tests.Indicators; - -[TestClass] -public class UlcerIndexTests : TestBase -{ - [TestMethod] - public void Standard() - { - List results = quotes - .GetUlcerIndex(14) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(489, results.Count(x => x.UI != null)); - - // sample value - UlcerIndexResult r = results[501]; - Assert.AreEqual(5.7255, r.UI.Round(4)); - } - - [TestMethod] - public void UseTuple() - { - List results = quotes - .Use(CandlePart.Close) - .GetUlcerIndex(14) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(489, results.Count(x => x.UI != null)); - } - - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetUlcerIndex(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.UI is double and double.NaN)); - } - - [TestMethod] - public void Chainee() - { - List results = quotes - .GetSma(2) - .GetUlcerIndex(14) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(488, results.Count(x => x.UI != null)); - } - - [TestMethod] - public void Chainor() - { - List results = quotes - .GetUlcerIndex(14) - .GetSma(10) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(480, results.Count(x => x.Sma != null)); - } - - [TestMethod] - public void BadData() - { - List r = badQuotes - .GetUlcerIndex(15) - .ToList(); - - Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.UI is double and double.NaN)); - } - - [TestMethod] - public void NoQuotes() - { - List r0 = noquotes - .GetUlcerIndex() - .ToList(); - - Assert.AreEqual(0, r0.Count); - - List r1 = onequote - .GetUlcerIndex() - .ToList(); - - Assert.AreEqual(1, r1.Count); - } - - [TestMethod] - public void Removed() - { - List results = quotes - .GetUlcerIndex(14) - .RemoveWarmupPeriods() - .ToList(); - - // assertions - Assert.AreEqual(502 - 13, results.Count); - - UlcerIndexResult last = results.LastOrDefault(); - Assert.AreEqual(5.7255, last.UI.Round(4)); - } - - // bad lookback period - [TestMethod] - public void Exceptions() - => Assert.ThrowsException(() - => quotes.GetUlcerIndex(0)); -} diff --git a/tests/indicators/s-z/Ultimate/Ultimate.Tests.cs b/tests/indicators/s-z/Ultimate/Ultimate.StaticSeries.Tests.cs similarity index 57% rename from tests/indicators/s-z/Ultimate/Ultimate.Tests.cs rename to tests/indicators/s-z/Ultimate/Ultimate.StaticSeries.Tests.cs index 0ee1e7361..8571195da 100644 --- a/tests/indicators/s-z/Ultimate/Ultimate.Tests.cs +++ b/tests/indicators/s-z/Ultimate/Ultimate.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class UltimateTests : TestBase +public class Ultimate : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetUltimate(7, 14, 28) - .ToList(); + IReadOnlyList results = Quotes + .ToUltimate(); // proper quantities Assert.AreEqual(502, results.Count); @@ -28,38 +27,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetUltimate() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToUltimate() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(465, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetUltimate(1, 2, 3) - .ToList(); + IReadOnlyList r = BadQuotes + .ToUltimate(1, 2, 3); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Ultimate is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Ultimate is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetUltimate() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToUltimate(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetUltimate() - .ToList(); + IReadOnlyList r1 = Onequote + .ToUltimate(); Assert.AreEqual(1, r1.Count); } @@ -67,15 +62,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetUltimate(7, 14, 28) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToUltimate() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 28, results.Count); - UltimateResult last = results.LastOrDefault(); + UltimateResult last = results[^1]; Assert.AreEqual(49.5257, last.Ultimate.Round(4)); } @@ -84,14 +78,14 @@ public void Exceptions() { // bad short period Assert.ThrowsException(() => - quotes.GetUltimate(0)); + Quotes.ToUltimate(0)); // bad middle period Assert.ThrowsException(() => - quotes.GetUltimate(7, 6)); + Quotes.ToUltimate(7, 6)); // bad long period Assert.ThrowsException(() => - quotes.GetUltimate(7, 14, 11)); + Quotes.ToUltimate(7, 14, 11)); } } diff --git a/tests/indicators/s-z/VolatilityStop/VolatilityStop.Tests.cs b/tests/indicators/s-z/VolatilityStop/VolatilityStop.StaticSeries.Tests.cs similarity index 70% rename from tests/indicators/s-z/VolatilityStop/VolatilityStop.Tests.cs rename to tests/indicators/s-z/VolatilityStop/VolatilityStop.StaticSeries.Tests.cs index 6dbb3e82c..66687b868 100644 --- a/tests/indicators/s-z/VolatilityStop/VolatilityStop.Tests.cs +++ b/tests/indicators/s-z/VolatilityStop/VolatilityStop.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class VolatilityStopTests : TestBase +public class VolatilityStop : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = - quotes.GetVolatilityStop(14, 3) - .ToList(); + IReadOnlyList results = + Quotes.ToVolatilityStop(14); // proper quantities Assert.AreEqual(502, results.Count); @@ -54,7 +53,7 @@ public void Standard() Assert.AreEqual(249.7460, r284.LowerBand.Round(4)); Assert.IsNull(r284.UpperBand); - VolatilityStopResult last = results.LastOrDefault(); + VolatilityStopResult last = results[^1]; Assert.AreEqual(249.2423, last.Sar.Round(4)); Assert.AreEqual(false, last.IsStop); Assert.AreEqual(249.2423, last.UpperBand.Round(4)); @@ -64,38 +63,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetVolatilityStop() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToVolatilityStop() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(439, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetVolatilityStop() - .ToList(); + IReadOnlyList r = BadQuotes + .ToVolatilityStop(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Sar is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Sar is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetVolatilityStop() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToVolatilityStop(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetVolatilityStop() - .ToList(); + IReadOnlyList r1 = Onequote + .ToVolatilityStop(); Assert.AreEqual(1, r1.Count); } @@ -103,15 +98,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetVolatilityStop(14, 3) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToVolatilityStop(14) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(402, results.Count); - VolatilityStopResult last = results.LastOrDefault(); + VolatilityStopResult last = results[^1]; Assert.AreEqual(249.2423, last.Sar.Round(4)); Assert.AreEqual(false, last.IsStop); } @@ -121,10 +115,10 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() => - quotes.GetVolatilityStop(1)); + Quotes.ToVolatilityStop(1)); // bad multiplier Assert.ThrowsException(() => - quotes.GetVolatilityStop(20, 0)); + Quotes.ToVolatilityStop(20, 0)); } } diff --git a/tests/indicators/s-z/Vortex/Vortex.Tests.cs b/tests/indicators/s-z/Vortex/Vortex.StaticSeries.Tests.cs similarity index 63% rename from tests/indicators/s-z/Vortex/Vortex.Tests.cs rename to tests/indicators/s-z/Vortex/Vortex.StaticSeries.Tests.cs index 7d183471b..389863567 100644 --- a/tests/indicators/s-z/Vortex/Vortex.Tests.cs +++ b/tests/indicators/s-z/Vortex/Vortex.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class VortexTests : TestBase +public class Vortex : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetVortex(14) - .ToList(); + IReadOnlyList results = Quotes + .ToVortex(14); // proper quantities Assert.AreEqual(502, results.Count); @@ -37,28 +36,25 @@ public void Standard() } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetVortex(20) - .ToList(); + IReadOnlyList r = BadQuotes + .ToVortex(20); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Pvi is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Pvi is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetVortex(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToVortex(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetVortex(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToVortex(5); Assert.AreEqual(1, r1.Count); } @@ -66,15 +62,14 @@ public void NoQuotes() [TestMethod] public void Condense() { - List results = quotes - .GetVortex(14) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToVortex(14) + .Condense(); // assertions Assert.AreEqual(502 - 14, results.Count); - VortexResult last = results.LastOrDefault(); + VortexResult last = results[^1]; Assert.AreEqual(0.8712, last.Pvi.Round(4)); Assert.AreEqual(1.1163, last.Nvi.Round(4)); } @@ -82,15 +77,14 @@ public void Condense() [TestMethod] public void Removed() { - List results = quotes - .GetVortex(14) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToVortex(14) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 14, results.Count); - VortexResult last = results.LastOrDefault(); + VortexResult last = results[^1]; Assert.AreEqual(0.8712, last.Pvi.Round(4)); Assert.AreEqual(1.1163, last.Nvi.Round(4)); } @@ -99,5 +93,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetVortex(1)); + => Quotes.ToVortex(1)); } diff --git a/tests/indicators/s-z/Vwap/Vwap.Tests.cs b/tests/indicators/s-z/Vwap/Vwap.StaticSeries.Tests.cs similarity index 61% rename from tests/indicators/s-z/Vwap/Vwap.Tests.cs rename to tests/indicators/s-z/Vwap/Vwap.StaticSeries.Tests.cs index 11143e3a7..cb07abc66 100644 --- a/tests/indicators/s-z/Vwap/Vwap.Tests.cs +++ b/tests/indicators/s-z/Vwap/Vwap.StaticSeries.Tests.cs @@ -1,17 +1,17 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class VwapTests : TestBase +public class Vwap : StaticSeriesTestBase { - private readonly IEnumerable intraday = TestData.GetIntraday() - .OrderBy(x => x.Date) - .Take(391); + private static readonly IReadOnlyList intraday = Data.GetIntraday() + .OrderBy(x => x.Timestamp) + .Take(391) + .ToList(); [TestMethod] - public void Standard() + public override void Standard() { - List results = intraday.GetVwap() - .ToList(); + IReadOnlyList results = intraday.ToVwap(); // proper quantities Assert.AreEqual(391, results.Count); @@ -35,11 +35,10 @@ public void Standard() public void WithStartDate() { DateTime startDate = - DateTime.ParseExact("2020-12-15 10:00", "yyyy-MM-dd h:mm", EnglishCulture); + DateTime.ParseExact("2020-12-15 10:00", "yyyy-MM-dd h:mm", invariantCulture); - List results = intraday - .GetVwap(startDate) - .ToList(); + IReadOnlyList results = intraday + .ToVwap(startDate); // proper quantities Assert.AreEqual(391, results.Count); @@ -62,38 +61,34 @@ public void WithStartDate() [TestMethod] public void Chainor() { - List results = quotes - .GetVwap() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToVwap() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(493, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetVwap() - .ToList(); + IReadOnlyList r = BadQuotes + .ToVwap(); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Vwap is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Vwap is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetVwap() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToVwap(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetVwap() - .ToList(); + IReadOnlyList r1 = Onequote + .ToVwap(); Assert.AreEqual(1, r1.Count); } @@ -102,30 +97,28 @@ public void NoQuotes() public void Removed() { // no start date - List results = intraday - .GetVwap() - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = intraday + .ToVwap() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(391, results.Count); - VwapResult last = results.LastOrDefault(); + VwapResult last = results[^1]; Assert.AreEqual(368.1804, last.Vwap.Round(4)); // with start date DateTime startDate = - DateTime.ParseExact("2020-12-15 10:00", "yyyy-MM-dd h:mm", EnglishCulture); + DateTime.ParseExact("2020-12-15 10:00", "yyyy-MM-dd h:mm", invariantCulture); - List sdResults = intraday - .GetVwap(startDate) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList sdResults = intraday + .ToVwap(startDate) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(361, sdResults.Count); - VwapResult sdLast = sdResults.LastOrDefault(); + VwapResult sdLast = sdResults[^1]; Assert.AreEqual(368.2908, sdLast.Vwap.Round(4)); } @@ -134,9 +127,9 @@ public void Exceptions() { // bad SMA period DateTime startDate = - DateTime.ParseExact("2000-12-15", "yyyy-MM-dd", EnglishCulture); + DateTime.ParseExact("2000-12-15", "yyyy-MM-dd", invariantCulture); Assert.ThrowsException(() => - quotes.GetVwap(startDate)); + Quotes.ToVwap(startDate)); } } diff --git a/tests/indicators/s-z/Vwma/Vwma.Tests.cs b/tests/indicators/s-z/Vwma/Vwma.StaticSeries.Tests.cs similarity index 58% rename from tests/indicators/s-z/Vwma/Vwma.Tests.cs rename to tests/indicators/s-z/Vwma/Vwma.StaticSeries.Tests.cs index f722d9d29..8cfc11cc0 100644 --- a/tests/indicators/s-z/Vwma/Vwma.Tests.cs +++ b/tests/indicators/s-z/Vwma/Vwma.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class VwmaTests : TestBase +public class Vwma : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetVwma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToVwma(10); // proper quantities Assert.AreEqual(502, results.Count); @@ -28,38 +27,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetVwma(10) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToVwma(10) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(484, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetVwma(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToVwma(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Vwma is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Vwma is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetVwma(4) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToVwma(4); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetVwma(4) - .ToList(); + IReadOnlyList r1 = Onequote + .ToVwma(4); Assert.AreEqual(1, r1.Count); } @@ -67,15 +62,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetVwma(10) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToVwma(10) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 9, results.Count); - VwmaResult last = results.LastOrDefault(); + VwmaResult last = results[^1]; Assert.AreEqual(242.101548, last.Vwma.Round(6)); } @@ -83,5 +77,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetVwma(0)); + => Quotes.ToVwma(0)); } diff --git a/tests/indicators/s-z/WilliamsR/WilliamsR.Tests.cs b/tests/indicators/s-z/WilliamsR/WilliamsR.StaticSeries.Tests.cs similarity index 64% rename from tests/indicators/s-z/WilliamsR/WilliamsR.Tests.cs rename to tests/indicators/s-z/WilliamsR/WilliamsR.StaticSeries.Tests.cs index 99f9455a1..c0af8b3e2 100644 --- a/tests/indicators/s-z/WilliamsR/WilliamsR.Tests.cs +++ b/tests/indicators/s-z/WilliamsR/WilliamsR.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class WilliamsRTests : TestBase +public class WilliamsR : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetWilliamsR(14) - .ToList(); + IReadOnlyList results = Quotes + .ToWilliamsR(); // proper quantities Assert.AreEqual(502, results.Count); @@ -37,41 +36,34 @@ public void Standard() [TestMethod] public void Chainor() { - List results = quotes - .GetWilliamsR() - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToWilliamsR() + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(480, results.Count(x => x.Sma != null)); } [TestMethod] - public void BadData() + public override void BadData() { - List quotes = badQuotes - .ToSortedList(); - - List results = badQuotes - .GetWilliamsR(20) - .ToList(); + IReadOnlyList results = BadQuotes + .ToWilliamsR(20); Assert.AreEqual(502, results.Count); - Assert.AreEqual(0, results.Count(x => x.WilliamsR is double and double.NaN)); + Assert.AreEqual(0, results.Count(x => x.WilliamsR is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetWilliamsR() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToWilliamsR(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetWilliamsR() - .ToList(); + IReadOnlyList r1 = Onequote + .ToWilliamsR(); Assert.AreEqual(1, r1.Count); } @@ -79,25 +71,23 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetWilliamsR(14) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToWilliamsR() + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 13, results.Count); - WilliamsResult last = results.LastOrDefault(); + WilliamsResult last = results[^1]; Assert.AreEqual(-52.0121, last.WilliamsR.Round(4)); } [TestMethod] public void Boundary() { - List results = TestData + IReadOnlyList results = Data .GetRandom(2500) - .GetWilliamsR(14) - .ToList(); + .ToWilliamsR(); // analyze boundary for (int i = 0; i < results.Count; i++) @@ -118,16 +108,15 @@ public void Issue1127() // initialize IOrderedEnumerable test1127 = File.ReadAllLines("s-z/WilliamsR/issue1127quotes.csv") .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date); + .Select(Imports.QuoteFromCsv) + .OrderByDescending(x => x.Timestamp); - List quotesList = test1127.ToList(); + IReadOnlyList quotesList = test1127.ToList(); int length = quotesList.Count; // get indicators - List resultsList = test1127 - .GetWilliamsR(14) - .ToList(); + IReadOnlyList resultsList = quotesList + .ToWilliamsR(); Console.WriteLine($"%R from {length} quotes."); @@ -137,7 +126,7 @@ public void Issue1127() Quote q = quotesList[i]; WilliamsResult r = resultsList[i]; - Console.WriteLine($"{q.Date:s} {r.WilliamsR}"); + Console.WriteLine($"{q.Timestamp:s} {r.WilliamsR}"); if (r.WilliamsR is not null) { @@ -151,5 +140,5 @@ public void Issue1127() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetWilliamsR(0)); + => Quotes.ToWilliamsR(0)); } diff --git a/tests/indicators/s-z/WilliamsR/issue1127quotes.csv b/tests/indicators/s-z/WilliamsR/issue1127quotes.csv index c3c727ad9..a091ab242 100644 --- a/tests/indicators/s-z/WilliamsR/issue1127quotes.csv +++ b/tests/indicators/s-z/WilliamsR/issue1127quotes.csv @@ -1,4 +1,4 @@ -Date,Open,High,Low,Close,Volume +Timestamp,Open,High,Low,Close,Volume 2021-01-29,121.21,121.685,119.28,120.17,2103083 2021-02-01,120.69,122.09,120.18,120.83,1451881 2021-02-02,122.5,125.85,122.415,123.19,1675502 diff --git a/tests/indicators/s-z/Wma/Wma.Tests.cs b/tests/indicators/s-z/Wma/Wma.StaticSeries.Tests.cs similarity index 51% rename from tests/indicators/s-z/Wma/Wma.Tests.cs rename to tests/indicators/s-z/Wma/Wma.StaticSeries.Tests.cs index e114970de..842b28399 100644 --- a/tests/indicators/s-z/Wma/Wma.Tests.cs +++ b/tests/indicators/s-z/Wma/Wma.StaticSeries.Tests.cs @@ -1,14 +1,13 @@ -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class WmaTests : TestBase +public class Wma : StaticSeriesTestBase { [TestMethod] - public void Standard() + public override void Standard() { - List results = quotes - .GetWma(20) - .ToList(); + IReadOnlyList results = Quotes + .ToWma(20); // proper quantities Assert.AreEqual(502, results.Count); @@ -23,35 +22,22 @@ public void Standard() } [TestMethod] - public void UseTuple() + public void UseReusable() { - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetWma(20) - .ToList(); + .ToWma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(483, results.Count(x => x.Wma != null)); } - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetWma(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Wma is double and double.NaN)); - } - [TestMethod] public void Chainee() { - List results = quotes - .GetSma(2) - .GetWma(20) - .ToList(); + IReadOnlyList results = Quotes + .ToSma(2) + .ToWma(20); Assert.AreEqual(502, results.Count); Assert.AreEqual(482, results.Count(x => x.Wma != null)); @@ -60,10 +46,9 @@ public void Chainee() [TestMethod] public void Chainor() { - List results = quotes - .GetWma(20) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToWma(20) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(474, results.Count(x => x.Sma != null)); @@ -72,14 +57,12 @@ public void Chainor() [TestMethod] public void Chaining() { - List standard = quotes - .GetWma(17) - .ToList(); + IReadOnlyList standard = Quotes + .ToWma(17); - List results = quotes + IReadOnlyList results = Quotes .Use(CandlePart.Close) - .GetWma(17) - .ToList(); + .ToWma(17); // assertions for (int i = 0; i < results.Count; i++) @@ -87,34 +70,31 @@ public void Chaining() WmaResult s = standard[i]; WmaResult c = results[i]; - Assert.AreEqual(s.Date, c.Date); + Assert.AreEqual(s.Timestamp, c.Timestamp); Assert.AreEqual(s.Wma, c.Wma); } } [TestMethod] - public void BadData() + public override void BadData() { - List r = badQuotes - .GetWma(15) - .ToList(); + IReadOnlyList r = BadQuotes + .ToWma(15); Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Wma is double and double.NaN)); + Assert.AreEqual(0, r.Count(x => x.Wma is double.NaN)); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetWma(5) - .ToList(); + IReadOnlyList r0 = Noquotes + .ToWma(5); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetWma(5) - .ToList(); + IReadOnlyList r1 = Onequote + .ToWma(5); Assert.AreEqual(1, r1.Count); } @@ -122,15 +102,14 @@ public void NoQuotes() [TestMethod] public void Removed() { - List results = quotes - .GetWma(20) - .RemoveWarmupPeriods() - .ToList(); + IReadOnlyList results = Quotes + .ToWma(20) + .RemoveWarmupPeriods(); // assertions Assert.AreEqual(502 - 19, results.Count); - WmaResult last = results.LastOrDefault(); + WmaResult last = results[^1]; Assert.AreEqual(246.5110, last.Wma.Round(4)); } @@ -138,5 +117,5 @@ public void Removed() [TestMethod] public void Exceptions() => Assert.ThrowsException(() - => quotes.GetWma(0)); + => Quotes.ToWma(0)); } diff --git a/tests/indicators/s-z/ZigZag/ZigZag.Tests.cs b/tests/indicators/s-z/ZigZag/ZigZag.StaticSeries.Tests.cs similarity index 77% rename from tests/indicators/s-z/ZigZag/ZigZag.Tests.cs rename to tests/indicators/s-z/ZigZag/ZigZag.StaticSeries.Tests.cs index 869bae9bb..30c5e0300 100644 --- a/tests/indicators/s-z/ZigZag/ZigZag.Tests.cs +++ b/tests/indicators/s-z/ZigZag/ZigZag.StaticSeries.Tests.cs @@ -1,16 +1,15 @@ using Newtonsoft.Json; -namespace Tests.Indicators; +namespace StaticSeries; [TestClass] -public class ZigZagTests : TestBase +public class ZigZag : StaticSeriesTestBase { [TestMethod] - public void StandardClose() + public override void Standard() // on Close { - List results = - quotes.GetZigZag(EndType.Close, 3) - .ToList(); + IReadOnlyList results = + Quotes.ToZigZag(EndType.Close, 3); // proper quantities Assert.AreEqual(502, results.Count); @@ -60,9 +59,8 @@ public void StandardClose() [TestMethod] public void StandardHighLow() { - List results = - quotes.GetZigZag(EndType.HighLow, 3) - .ToList(); + IReadOnlyList results = + Quotes.ToZigZag(EndType.HighLow, 3); // proper quantities Assert.AreEqual(502, results.Count); @@ -112,10 +110,9 @@ public void StandardHighLow() [TestMethod] public void Chainor() { - List results = quotes - .GetZigZag(EndType.Close, 3) - .GetSma(10) - .ToList(); + IReadOnlyList results = Quotes + .ToZigZag(EndType.Close, 3) + .ToSma(10); Assert.AreEqual(502, results.Count); Assert.AreEqual(225, results.Count(x => x.Sma != null)); @@ -127,13 +124,13 @@ public void NoEntry() // thresholds are never met string json = File.ReadAllText("./s-z/ZigZag/data.ethusdt.json"); - IReadOnlyCollection quotes = JsonConvert - .DeserializeObject>(json); - - List results = quotes - .GetZigZag(EndType.Close, 5m) + IReadOnlyList quotes = JsonConvert + .DeserializeObject>(json) .ToList(); + IReadOnlyList results = quotes + .ToZigZag(); + Assert.AreEqual(0, results.Count(x => x.PointType != null)); } @@ -143,45 +140,40 @@ public void Issue632() // thresholds are never met string json = File.ReadAllText("./s-z/ZigZag/data.issue632.json"); - List quotesList = JsonConvert + IReadOnlyList quotesList = JsonConvert .DeserializeObject>(json) .ToList(); - List resultsList = quotesList - .GetZigZag(EndType.Close, 5m) - .ToList(); + IReadOnlyList resultsList = quotesList + .ToZigZag(); Assert.AreEqual(17, resultsList.Count); } [TestMethod] - public void BadData() + public override void BadData() { - List r1 = badQuotes - .GetZigZag(EndType.Close) - .ToList(); + IReadOnlyList r1 = BadQuotes + .ToZigZag(); Assert.AreEqual(502, r1.Count); - List r2 = badQuotes - .GetZigZag(EndType.HighLow) - .ToList(); + IReadOnlyList r2 = BadQuotes + .ToZigZag(EndType.HighLow); Assert.AreEqual(502, r2.Count); } [TestMethod] - public void NoQuotes() + public override void NoQuotes() { - List r0 = noquotes - .GetZigZag() - .ToList(); + IReadOnlyList r0 = Noquotes + .ToZigZag(); Assert.AreEqual(0, r0.Count); - List r1 = onequote - .GetZigZag() - .ToList(); + IReadOnlyList r1 = Onequote + .ToZigZag(); Assert.AreEqual(1, r1.Count); } @@ -189,10 +181,9 @@ public void NoQuotes() [TestMethod] public void Condense() { - List results = quotes - .GetZigZag(EndType.Close, 3) - .Condense() - .ToList(); + IReadOnlyList results = Quotes + .ToZigZag(EndType.Close, 3) + .Condense(); // assertions Assert.AreEqual(14, results.Count); @@ -203,17 +194,17 @@ public void SchrodingerScenario() { string json = File.ReadAllText("./s-z/ZigZag/data.schrodinger.json"); - List h = JsonConvert + IReadOnlyList h = JsonConvert .DeserializeObject>(json) - .OrderBy(x => x.Date) + .OrderBy(x => x.Timestamp) .ToList(); - List r1 = h.GetZigZag(EndType.Close, 0.25m).ToList(); + IReadOnlyList r1 = h.ToZigZag(EndType.Close, 0.25m).ToList(); Assert.AreEqual(342, r1.Count); // first period has High/Low that exceeds threhold // where it is both a H and L pivot simultaenously - List r2 = h.GetZigZag(EndType.HighLow, 3).ToList(); + IReadOnlyList r2 = h.ToZigZag(EndType.HighLow, 3).ToList(); Assert.AreEqual(342, r2.Count); } @@ -222,10 +213,10 @@ public void Exceptions() { // bad lookback period Assert.ThrowsException(() - => quotes.GetZigZag(EndType.Close, 0)); + => Quotes.ToZigZag(EndType.Close, 0)); // bad end type Assert.ThrowsException(() - => quotes.GetZigZag((EndType)int.MaxValue, 2)); + => Quotes.ToZigZag((EndType)int.MaxValue, 2)); } } diff --git a/tests/indicators/s-z/ZigZag/data.ethusdt.json b/tests/indicators/s-z/ZigZag/data.ethusdt.json index 5a628f1dc..8ea90a20e 100644 --- a/tests/indicators/s-z/ZigZag/data.ethusdt.json +++ b/tests/indicators/s-z/ZigZag/data.ethusdt.json @@ -2,7 +2,7 @@ { "TimeOfCandle": "2021-10-27T09:55:00+03:30", "Amount": 163057.4955050465, - "Date": "2021-10-27T09:55:00+03:30", + "Timestamp": "2021-10-27T09:55:00+03:30", "Open": 4247.84, "High": 4256.64, "Low": 4246.00, @@ -12,7 +12,7 @@ { "TimeOfCandle": "2021-10-27T10:00:00+03:30", "Amount": 34654.0711221263, - "Date": "2021-10-27T10:00:00+03:30", + "Timestamp": "2021-10-27T10:00:00+03:30", "Open": 4256.49, "High": 4263.43, "Low": 4256.41, @@ -22,7 +22,7 @@ { "TimeOfCandle": "2021-10-27T10:05:00+03:30", "Amount": 119438.8924996978, - "Date": "2021-10-27T10:05:00+03:30", + "Timestamp": "2021-10-27T10:05:00+03:30", "Open": 4257.78, "High": 4257.78, "Low": 4250.64, @@ -32,7 +32,7 @@ { "TimeOfCandle": "2021-10-27T10:10:00+03:30", "Amount": 79710.1383965050, - "Date": "2021-10-27T10:10:00+03:30", + "Timestamp": "2021-10-27T10:10:00+03:30", "Open": 4251.66, "High": 4255.66, "Low": 4238.02, @@ -42,7 +42,7 @@ { "TimeOfCandle": "2021-10-27T10:15:00+03:30", "Amount": 35916.8475715381, - "Date": "2021-10-27T10:15:00+03:30", + "Timestamp": "2021-10-27T10:15:00+03:30", "Open": 4238.03, "High": 4244.00, "Low": 4236.20, @@ -52,7 +52,7 @@ { "TimeOfCandle": "2021-10-27T10:20:00+03:30", "Amount": 31735.4679234844, - "Date": "2021-10-27T10:20:00+03:30", + "Timestamp": "2021-10-27T10:20:00+03:30", "Open": 4238.01, "High": 4247.43, "Low": 4236.70, @@ -62,7 +62,7 @@ { "TimeOfCandle": "2021-10-27T10:25:00+03:30", "Amount": 34824.0746251221, - "Date": "2021-10-27T10:25:00+03:30", + "Timestamp": "2021-10-27T10:25:00+03:30", "Open": 4241.03, "High": 4244.64, "Low": 4239.55, @@ -72,7 +72,7 @@ { "TimeOfCandle": "2021-10-27T10:30:00+03:30", "Amount": 43817.2577893878, - "Date": "2021-10-27T10:30:00+03:30", + "Timestamp": "2021-10-27T10:30:00+03:30", "Open": 4239.91, "High": 4239.91, "Low": 4226.97, @@ -82,7 +82,7 @@ { "TimeOfCandle": "2021-10-27T10:35:00+03:30", "Amount": 37631.6618704417, - "Date": "2021-10-27T10:35:00+03:30", + "Timestamp": "2021-10-27T10:35:00+03:30", "Open": 4225.00, "High": 4246.19, "Low": 4224.88, @@ -92,7 +92,7 @@ { "TimeOfCandle": "2021-10-27T10:40:00+03:30", "Amount": 29871.7942738019, - "Date": "2021-10-27T10:40:00+03:30", + "Timestamp": "2021-10-27T10:40:00+03:30", "Open": 4233.36, "High": 4240.73, "Low": 4228.21, @@ -102,7 +102,7 @@ { "TimeOfCandle": "2021-10-27T10:45:00+03:30", "Amount": 13541.9867665602, - "Date": "2021-10-27T10:45:00+03:30", + "Timestamp": "2021-10-27T10:45:00+03:30", "Open": 4227.89, "High": 4229.75, "Low": 4221.03, @@ -112,7 +112,7 @@ { "TimeOfCandle": "2021-10-27T10:50:00+03:30", "Amount": 68016.9641253717, - "Date": "2021-10-27T10:50:00+03:30", + "Timestamp": "2021-10-27T10:50:00+03:30", "Open": 4224.68, "High": 4234.19, "Low": 4215.01, @@ -122,7 +122,7 @@ { "TimeOfCandle": "2021-10-27T10:55:00+03:30", "Amount": 104286.9385587473, - "Date": "2021-10-27T10:55:00+03:30", + "Timestamp": "2021-10-27T10:55:00+03:30", "Open": 4221.97, "High": 4221.97, "Low": 4200.71, @@ -132,7 +132,7 @@ { "TimeOfCandle": "2021-10-27T11:00:00+03:30", "Amount": 123321.2347351709, - "Date": "2021-10-27T11:00:00+03:30", + "Timestamp": "2021-10-27T11:00:00+03:30", "Open": 4210.00, "High": 4212.00, "Low": 4193.45, @@ -142,7 +142,7 @@ { "TimeOfCandle": "2021-10-27T11:05:00+03:30", "Amount": 71808.7749004083, - "Date": "2021-10-27T11:05:00+03:30", + "Timestamp": "2021-10-27T11:05:00+03:30", "Open": 4196.06, "High": 4196.06, "Low": 4166.13, @@ -152,7 +152,7 @@ { "TimeOfCandle": "2021-10-27T11:10:00+03:30", "Amount": 316937.3768832286, - "Date": "2021-10-27T11:10:00+03:30", + "Timestamp": "2021-10-27T11:10:00+03:30", "Open": 4171.97, "High": 4179.03, "Low": 4160.00, @@ -162,7 +162,7 @@ { "TimeOfCandle": "2021-10-27T11:15:00+03:30", "Amount": 150355.5828374267, - "Date": "2021-10-27T11:15:00+03:30", + "Timestamp": "2021-10-27T11:15:00+03:30", "Open": 4162.32, "High": 4173.10, "Low": 4146.00, @@ -172,7 +172,7 @@ { "TimeOfCandle": "2021-10-27T11:20:00+03:30", "Amount": 120265.5829372087, - "Date": "2021-10-27T11:20:00+03:30", + "Timestamp": "2021-10-27T11:20:00+03:30", "Open": 4150.00, "High": 4161.61, "Low": 4140.00, @@ -182,7 +182,7 @@ { "TimeOfCandle": "2021-10-27T11:25:00+03:30", "Amount": 69818.4970617914, - "Date": "2021-10-27T11:25:00+03:30", + "Timestamp": "2021-10-27T11:25:00+03:30", "Open": 4143.30, "High": 4152.12, "Low": 4135.00, @@ -192,7 +192,7 @@ { "TimeOfCandle": "2021-10-27T11:30:00+03:30", "Amount": 535494.0913993873, - "Date": "2021-10-27T11:30:00+03:30", + "Timestamp": "2021-10-27T11:30:00+03:30", "Open": 4135.61, "High": 4136.85, "Low": 4034.81, @@ -202,7 +202,7 @@ { "TimeOfCandle": "2021-10-27T11:35:00+03:30", "Amount": 584292.7598844911, - "Date": "2021-10-27T11:35:00+03:30", + "Timestamp": "2021-10-27T11:35:00+03:30", "Open": 4036.21, "High": 4059.58, "Low": 3943.00, @@ -212,7 +212,7 @@ { "TimeOfCandle": "2021-10-27T11:40:00+03:30", "Amount": 1104359.0885250876, - "Date": "2021-10-27T11:40:00+03:30", + "Timestamp": "2021-10-27T11:40:00+03:30", "Open": 3942.00, "High": 4040.00, "Low": 3930.00, @@ -222,7 +222,7 @@ { "TimeOfCandle": "2021-10-27T11:45:00+03:30", "Amount": 498093.6682223480, - "Date": "2021-10-27T11:45:00+03:30", + "Timestamp": "2021-10-27T11:45:00+03:30", "Open": 4035.44, "High": 4047.02, "Low": 4013.36, @@ -232,7 +232,7 @@ { "TimeOfCandle": "2021-10-27T11:50:00+03:30", "Amount": 393538.5112353333, - "Date": "2021-10-27T11:50:00+03:30", + "Timestamp": "2021-10-27T11:50:00+03:30", "Open": 4009.20, "High": 4028.57, "Low": 3985.01, @@ -242,7 +242,7 @@ { "TimeOfCandle": "2021-10-27T11:55:00+03:30", "Amount": 272563.4694278689, - "Date": "2021-10-27T11:55:00+03:30", + "Timestamp": "2021-10-27T11:55:00+03:30", "Open": 4029.00, "High": 4047.96, "Low": 4017.84, @@ -252,7 +252,7 @@ { "TimeOfCandle": "2021-10-27T12:00:00+03:30", "Amount": 189129.5282852959, - "Date": "2021-10-27T12:00:00+03:30", + "Timestamp": "2021-10-27T12:00:00+03:30", "Open": 4041.01, "High": 4051.49, "Low": 4025.01, @@ -262,7 +262,7 @@ { "TimeOfCandle": "2021-10-27T12:05:00+03:30", "Amount": 205552.1358770712, - "Date": "2021-10-27T12:05:00+03:30", + "Timestamp": "2021-10-27T12:05:00+03:30", "Open": 4054.51, "High": 4066.64, "Low": 4047.52, @@ -272,7 +272,7 @@ { "TimeOfCandle": "2021-10-27T12:10:00+03:30", "Amount": 152693.0842788783, - "Date": "2021-10-27T12:10:00+03:30", + "Timestamp": "2021-10-27T12:10:00+03:30", "Open": 4057.14, "High": 4057.14, "Low": 4019.91, @@ -282,7 +282,7 @@ { "TimeOfCandle": "2021-10-27T12:15:00+03:30", "Amount": 98541.8880402825, - "Date": "2021-10-27T12:15:00+03:30", + "Timestamp": "2021-10-27T12:15:00+03:30", "Open": 4030.05, "High": 4037.21, "Low": 4015.80, @@ -292,7 +292,7 @@ { "TimeOfCandle": "2021-10-27T12:20:00+03:30", "Amount": 53318.3827931539, - "Date": "2021-10-27T12:20:00+03:30", + "Timestamp": "2021-10-27T12:20:00+03:30", "Open": 4023.80, "High": 4041.68, "Low": 4020.00, @@ -302,7 +302,7 @@ { "TimeOfCandle": "2021-10-27T12:25:00+03:30", "Amount": 41183.5512783411, - "Date": "2021-10-27T12:25:00+03:30", + "Timestamp": "2021-10-27T12:25:00+03:30", "Open": 4037.21, "High": 4051.47, "Low": 4036.55, @@ -312,7 +312,7 @@ { "TimeOfCandle": "2021-10-27T12:30:00+03:30", "Amount": 92519.0718745724, - "Date": "2021-10-27T12:30:00+03:30", + "Timestamp": "2021-10-27T12:30:00+03:30", "Open": 4045.45, "High": 4045.89, "Low": 4027.83, @@ -322,7 +322,7 @@ { "TimeOfCandle": "2021-10-27T12:35:00+03:30", "Amount": 109053.6992621962, - "Date": "2021-10-27T12:35:00+03:30", + "Timestamp": "2021-10-27T12:35:00+03:30", "Open": 4041.27, "High": 4041.71, "Low": 4017.46, @@ -332,7 +332,7 @@ { "TimeOfCandle": "2021-10-27T12:40:00+03:30", "Amount": 125007.8261205876, - "Date": "2021-10-27T12:40:00+03:30", + "Timestamp": "2021-10-27T12:40:00+03:30", "Open": 4026.12, "High": 4031.00, "Low": 4004.06, @@ -342,7 +342,7 @@ { "TimeOfCandle": "2021-10-27T12:45:00+03:30", "Amount": 147232.5785344835, - "Date": "2021-10-27T12:45:00+03:30", + "Timestamp": "2021-10-27T12:45:00+03:30", "Open": 4014.00, "High": 4016.01, "Low": 3998.23, @@ -352,7 +352,7 @@ { "TimeOfCandle": "2021-10-27T12:50:00+03:30", "Amount": 102310.8955090087, - "Date": "2021-10-27T12:50:00+03:30", + "Timestamp": "2021-10-27T12:50:00+03:30", "Open": 4013.20, "High": 4017.07, "Low": 3988.11, @@ -362,7 +362,7 @@ { "TimeOfCandle": "2021-10-27T12:55:00+03:30", "Amount": 132902.3966462262, - "Date": "2021-10-27T12:55:00+03:30", + "Timestamp": "2021-10-27T12:55:00+03:30", "Open": 3991.04, "High": 4009.77, "Low": 3987.20, @@ -372,7 +372,7 @@ { "TimeOfCandle": "2021-10-27T13:00:00+03:30", "Amount": 73266.9402888383, - "Date": "2021-10-27T13:00:00+03:30", + "Timestamp": "2021-10-27T13:00:00+03:30", "Open": 3991.21, "High": 4009.00, "Low": 3991.21, @@ -382,7 +382,7 @@ { "TimeOfCandle": "2021-10-27T13:05:00+03:30", "Amount": 158835.2367543159, - "Date": "2021-10-27T13:05:00+03:30", + "Timestamp": "2021-10-27T13:05:00+03:30", "Open": 3996.90, "High": 4019.99, "Low": 3995.75, @@ -392,7 +392,7 @@ { "TimeOfCandle": "2021-10-27T13:10:00+03:30", "Amount": 44859.2898833674, - "Date": "2021-10-27T13:10:00+03:30", + "Timestamp": "2021-10-27T13:10:00+03:30", "Open": 4019.00, "High": 4027.00, "Low": 4017.12, @@ -402,7 +402,7 @@ { "TimeOfCandle": "2021-10-27T13:15:00+03:30", "Amount": 88053.4843285457, - "Date": "2021-10-27T13:15:00+03:30", + "Timestamp": "2021-10-27T13:15:00+03:30", "Open": 4017.03, "High": 4032.05, "Low": 4015.43, @@ -412,7 +412,7 @@ { "TimeOfCandle": "2021-10-27T13:20:00+03:30", "Amount": 52333.4460034385, - "Date": "2021-10-27T13:20:00+03:30", + "Timestamp": "2021-10-27T13:20:00+03:30", "Open": 4019.25, "High": 4020.96, "Low": 4009.15, @@ -422,7 +422,7 @@ { "TimeOfCandle": "2021-10-27T13:25:00+03:30", "Amount": 98046.1700274930, - "Date": "2021-10-27T13:25:00+03:30", + "Timestamp": "2021-10-27T13:25:00+03:30", "Open": 4017.50, "High": 4018.43, "Low": 4005.09, @@ -432,7 +432,7 @@ { "TimeOfCandle": "2021-10-27T13:30:00+03:30", "Amount": 46053.3690618458, - "Date": "2021-10-27T13:30:00+03:30", + "Timestamp": "2021-10-27T13:30:00+03:30", "Open": 4016.07, "High": 4016.07, "Low": 4000.01, @@ -442,7 +442,7 @@ { "TimeOfCandle": "2021-10-27T13:35:00+03:30", "Amount": 90834.5443393879, - "Date": "2021-10-27T13:35:00+03:30", + "Timestamp": "2021-10-27T13:35:00+03:30", "Open": 4000.80, "High": 4013.30, "Low": 3997.00, @@ -452,7 +452,7 @@ { "TimeOfCandle": "2021-10-27T13:40:00+03:30", "Amount": 82155.6342514178, - "Date": "2021-10-27T13:40:00+03:30", + "Timestamp": "2021-10-27T13:40:00+03:30", "Open": 3999.43, "High": 4010.57, "Low": 3996.37, @@ -462,7 +462,7 @@ { "TimeOfCandle": "2021-10-27T13:45:00+03:30", "Amount": 98227.7457658266, - "Date": "2021-10-27T13:45:00+03:30", + "Timestamp": "2021-10-27T13:45:00+03:30", "Open": 4005.90, "High": 4009.39, "Low": 3988.54, @@ -472,7 +472,7 @@ { "TimeOfCandle": "2021-10-27T13:50:00+03:30", "Amount": 2124956.7030016861, - "Date": "2021-10-27T13:50:00+03:30", + "Timestamp": "2021-10-27T13:50:00+03:30", "Open": 4002.54, "High": 4013.05, "Low": 3990.38, @@ -482,7 +482,7 @@ { "TimeOfCandle": "2021-10-27T13:55:00+03:30", "Amount": 1970623.1467266278, - "Date": "2021-10-27T13:55:00+03:30", + "Timestamp": "2021-10-27T13:55:00+03:30", "Open": 3994.88, "High": 4001.29, "Low": 3986.07, @@ -492,7 +492,7 @@ { "TimeOfCandle": "2021-10-27T14:00:00+03:30", "Amount": 290220.8725276126, - "Date": "2021-10-27T14:00:00+03:30", + "Timestamp": "2021-10-27T14:00:00+03:30", "Open": 3988.00, "High": 4008.37, "Low": 3972.00, @@ -502,7 +502,7 @@ { "TimeOfCandle": "2021-10-27T14:05:00+03:30", "Amount": 101606.6291843867, - "Date": "2021-10-27T14:05:00+03:30", + "Timestamp": "2021-10-27T14:05:00+03:30", "Open": 4000.23, "High": 4004.55, "Low": 3986.84, @@ -512,7 +512,7 @@ { "TimeOfCandle": "2021-10-27T14:10:00+03:30", "Amount": 83358.9949078253, - "Date": "2021-10-27T14:10:00+03:30", + "Timestamp": "2021-10-27T14:10:00+03:30", "Open": 4003.73, "High": 4014.77, "Low": 4000.04, @@ -522,7 +522,7 @@ { "TimeOfCandle": "2021-10-27T14:15:00+03:30", "Amount": 52820.2686269136, - "Date": "2021-10-27T14:15:00+03:30", + "Timestamp": "2021-10-27T14:15:00+03:30", "Open": 4014.51, "High": 4014.51, "Low": 4004.31, @@ -532,7 +532,7 @@ { "TimeOfCandle": "2021-10-27T14:20:00+03:30", "Amount": 62143.4357792429, - "Date": "2021-10-27T14:20:00+03:30", + "Timestamp": "2021-10-27T14:20:00+03:30", "Open": 4014.36, "High": 4031.17, "Low": 4014.36, @@ -542,7 +542,7 @@ { "TimeOfCandle": "2021-10-27T14:25:00+03:30", "Amount": 116474.0283805076, - "Date": "2021-10-27T14:25:00+03:30", + "Timestamp": "2021-10-27T14:25:00+03:30", "Open": 4020.97, "High": 4026.27, "Low": 4014.46, @@ -552,7 +552,7 @@ { "TimeOfCandle": "2021-10-27T14:30:00+03:30", "Amount": 73118.9082466528, - "Date": "2021-10-27T14:30:00+03:30", + "Timestamp": "2021-10-27T14:30:00+03:30", "Open": 4021.46, "High": 4030.01, "Low": 4017.30, @@ -562,7 +562,7 @@ { "TimeOfCandle": "2021-10-27T14:35:00+03:30", "Amount": 88706.0117936053, - "Date": "2021-10-27T14:35:00+03:30", + "Timestamp": "2021-10-27T14:35:00+03:30", "Open": 4028.61, "High": 4035.75, "Low": 4028.61, @@ -572,7 +572,7 @@ { "TimeOfCandle": "2021-10-27T14:40:00+03:30", "Amount": 107147.4920464002, - "Date": "2021-10-27T14:40:00+03:30", + "Timestamp": "2021-10-27T14:40:00+03:30", "Open": 4034.00, "High": 4046.41, "Low": 4031.23, @@ -582,7 +582,7 @@ { "TimeOfCandle": "2021-10-27T14:45:00+03:30", "Amount": 45140.2381872377, - "Date": "2021-10-27T14:45:00+03:30", + "Timestamp": "2021-10-27T14:45:00+03:30", "Open": 4045.31, "High": 4045.31, "Low": 4030.24, @@ -592,7 +592,7 @@ { "TimeOfCandle": "2021-10-27T14:50:00+03:30", "Amount": 148529.2821350580, - "Date": "2021-10-27T14:50:00+03:30", + "Timestamp": "2021-10-27T14:50:00+03:30", "Open": 4033.96, "High": 4037.22, "Low": 4022.02, @@ -602,7 +602,7 @@ { "TimeOfCandle": "2021-10-27T14:55:00+03:30", "Amount": 66960.0661849583, - "Date": "2021-10-27T14:55:00+03:30", + "Timestamp": "2021-10-27T14:55:00+03:30", "Open": 4024.03, "High": 4025.00, "Low": 4014.61, @@ -612,7 +612,7 @@ { "TimeOfCandle": "2021-10-27T15:00:00+03:30", "Amount": 116472.1985497611, - "Date": "2021-10-27T15:00:00+03:30", + "Timestamp": "2021-10-27T15:00:00+03:30", "Open": 4017.49, "High": 4017.49, "Low": 4004.12, @@ -622,7 +622,7 @@ { "TimeOfCandle": "2021-10-27T15:05:00+03:30", "Amount": 62059.9082265501, - "Date": "2021-10-27T15:05:00+03:30", + "Timestamp": "2021-10-27T15:05:00+03:30", "Open": 4008.99, "High": 4017.03, "Low": 4000.00, @@ -632,7 +632,7 @@ { "TimeOfCandle": "2021-10-27T15:10:00+03:30", "Amount": 78430.2838209792, - "Date": "2021-10-27T15:10:00+03:30", + "Timestamp": "2021-10-27T15:10:00+03:30", "Open": 4003.97, "High": 4008.36, "Low": 3997.28, @@ -642,7 +642,7 @@ { "TimeOfCandle": "2021-10-27T15:15:00+03:30", "Amount": 74775.0361081990, - "Date": "2021-10-27T15:15:00+03:30", + "Timestamp": "2021-10-27T15:15:00+03:30", "Open": 4005.15, "High": 4010.99, "Low": 3996.08, @@ -652,7 +652,7 @@ { "TimeOfCandle": "2021-10-27T15:20:00+03:30", "Amount": 69304.8337077704, - "Date": "2021-10-27T15:20:00+03:30", + "Timestamp": "2021-10-27T15:20:00+03:30", "Open": 3997.64, "High": 4005.67, "Low": 3988.17, @@ -662,7 +662,7 @@ { "TimeOfCandle": "2021-10-27T15:25:00+03:30", "Amount": 63705.7338575308, - "Date": "2021-10-27T15:25:00+03:30", + "Timestamp": "2021-10-27T15:25:00+03:30", "Open": 3988.11, "High": 3998.40, "Low": 3987.00, @@ -672,7 +672,7 @@ { "TimeOfCandle": "2021-10-27T15:30:00+03:30", "Amount": 221828.1794979553, - "Date": "2021-10-27T15:30:00+03:30", + "Timestamp": "2021-10-27T15:30:00+03:30", "Open": 3990.29, "High": 3997.33, "Low": 3963.00, @@ -682,7 +682,7 @@ { "TimeOfCandle": "2021-10-27T15:35:00+03:30", "Amount": 116058.4370126909, - "Date": "2021-10-27T15:35:00+03:30", + "Timestamp": "2021-10-27T15:35:00+03:30", "Open": 3995.28, "High": 4002.90, "Low": 3985.12, @@ -692,7 +692,7 @@ { "TimeOfCandle": "2021-10-27T15:40:00+03:30", "Amount": 53330.7365431283, - "Date": "2021-10-27T15:40:00+03:30", + "Timestamp": "2021-10-27T15:40:00+03:30", "Open": 3996.00, "High": 4001.89, "Low": 3985.20, @@ -702,7 +702,7 @@ { "TimeOfCandle": "2021-10-27T15:45:00+03:30", "Amount": 21997.5887486905, - "Date": "2021-10-27T15:45:00+03:30", + "Timestamp": "2021-10-27T15:45:00+03:30", "Open": 3993.86, "High": 4002.79, "Low": 3992.04, @@ -712,7 +712,7 @@ { "TimeOfCandle": "2021-10-27T15:50:00+03:30", "Amount": 48520.8751434566, - "Date": "2021-10-27T15:50:00+03:30", + "Timestamp": "2021-10-27T15:50:00+03:30", "Open": 3997.60, "High": 4013.52, "Low": 3997.14, @@ -722,7 +722,7 @@ { "TimeOfCandle": "2021-10-27T15:55:00+03:30", "Amount": 910637.5813854319, - "Date": "2021-10-27T15:55:00+03:30", + "Timestamp": "2021-10-27T15:55:00+03:30", "Open": 4010.15, "High": 4015.01, "Low": 4008.82, @@ -732,7 +732,7 @@ { "TimeOfCandle": "2021-10-27T16:00:00+03:30", "Amount": 35055.9461030449, - "Date": "2021-10-27T16:00:00+03:30", + "Timestamp": "2021-10-27T16:00:00+03:30", "Open": 4015.40, "High": 4020.00, "Low": 4010.94, @@ -742,7 +742,7 @@ { "TimeOfCandle": "2021-10-27T16:05:00+03:30", "Amount": 103567.0506405367, - "Date": "2021-10-27T16:05:00+03:30", + "Timestamp": "2021-10-27T16:05:00+03:30", "Open": 4018.00, "High": 4023.46, "Low": 4018.00, @@ -752,7 +752,7 @@ { "TimeOfCandle": "2021-10-27T16:10:00+03:30", "Amount": 1490851.0331273030, - "Date": "2021-10-27T16:10:00+03:30", + "Timestamp": "2021-10-27T16:10:00+03:30", "Open": 4022.39, "High": 4022.39, "Low": 4010.00, @@ -762,7 +762,7 @@ { "TimeOfCandle": "2021-10-27T16:15:00+03:30", "Amount": 2236162.5600715379, - "Date": "2021-10-27T16:15:00+03:30", + "Timestamp": "2021-10-27T16:15:00+03:30", "Open": 4010.00, "High": 4010.01, "Low": 3992.76, @@ -772,7 +772,7 @@ { "TimeOfCandle": "2021-10-27T16:20:00+03:30", "Amount": 64652.6701311666, - "Date": "2021-10-27T16:20:00+03:30", + "Timestamp": "2021-10-27T16:20:00+03:30", "Open": 3998.41, "High": 4016.44, "Low": 3998.01, @@ -782,7 +782,7 @@ { "TimeOfCandle": "2021-10-27T16:25:00+03:30", "Amount": 20619.8462768749, - "Date": "2021-10-27T16:25:00+03:30", + "Timestamp": "2021-10-27T16:25:00+03:30", "Open": 4008.38, "High": 4012.00, "Low": 4001.20, @@ -792,7 +792,7 @@ { "TimeOfCandle": "2021-10-27T16:30:00+03:30", "Amount": 319402.1248540172, - "Date": "2021-10-27T16:30:00+03:30", + "Timestamp": "2021-10-27T16:30:00+03:30", "Open": 4012.00, "High": 4014.78, "Low": 4000.00, @@ -802,7 +802,7 @@ { "TimeOfCandle": "2021-10-27T16:35:00+03:30", "Amount": 20982.3739803013, - "Date": "2021-10-27T16:35:00+03:30", + "Timestamp": "2021-10-27T16:35:00+03:30", "Open": 4014.77, "High": 4025.00, "Low": 4014.77, @@ -812,7 +812,7 @@ { "TimeOfCandle": "2021-10-27T16:40:00+03:30", "Amount": 48746.0780131629, - "Date": "2021-10-27T16:40:00+03:30", + "Timestamp": "2021-10-27T16:40:00+03:30", "Open": 4018.87, "High": 4026.00, "Low": 4016.89, @@ -822,7 +822,7 @@ { "TimeOfCandle": "2021-10-27T16:45:00+03:30", "Amount": 85098.1610378864, - "Date": "2021-10-27T16:45:00+03:30", + "Timestamp": "2021-10-27T16:45:00+03:30", "Open": 4026.00, "High": 4026.00, "Low": 4008.56, @@ -832,7 +832,7 @@ { "TimeOfCandle": "2021-10-27T16:50:00+03:30", "Amount": 41730.0008734617, - "Date": "2021-10-27T16:50:00+03:30", + "Timestamp": "2021-10-27T16:50:00+03:30", "Open": 4016.99, "High": 4019.37, "Low": 4007.00, @@ -842,7 +842,7 @@ { "TimeOfCandle": "2021-10-27T16:55:00+03:30", "Amount": 430881.4056792271, - "Date": "2021-10-27T16:55:00+03:30", + "Timestamp": "2021-10-27T16:55:00+03:30", "Open": 4007.14, "High": 4008.00, "Low": 3990.00, @@ -852,7 +852,7 @@ { "TimeOfCandle": "2021-10-27T17:00:00+03:30", "Amount": 35444.2488422846, - "Date": "2021-10-27T17:00:00+03:30", + "Timestamp": "2021-10-27T17:00:00+03:30", "Open": 3997.04, "High": 4002.32, "Low": 3990.00, @@ -862,7 +862,7 @@ { "TimeOfCandle": "2021-10-27T17:05:00+03:30", "Amount": 46657.3033344308, - "Date": "2021-10-27T17:05:00+03:30", + "Timestamp": "2021-10-27T17:05:00+03:30", "Open": 3997.00, "High": 4014.91, "Low": 3995.34, @@ -872,7 +872,7 @@ { "TimeOfCandle": "2021-10-27T17:10:00+03:30", "Amount": 37359.4277336326, - "Date": "2021-10-27T17:10:00+03:30", + "Timestamp": "2021-10-27T17:10:00+03:30", "Open": 4014.00, "High": 4021.00, "Low": 4009.81, @@ -882,7 +882,7 @@ { "TimeOfCandle": "2021-10-27T17:15:00+03:30", "Amount": 78110.4230670868, - "Date": "2021-10-27T17:15:00+03:30", + "Timestamp": "2021-10-27T17:15:00+03:30", "Open": 4022.94, "High": 4027.84, "Low": 4001.91, @@ -892,7 +892,7 @@ { "TimeOfCandle": "2021-10-27T17:20:00+03:30", "Amount": 26460.0944223798, - "Date": "2021-10-27T17:20:00+03:30", + "Timestamp": "2021-10-27T17:20:00+03:30", "Open": 4002.17, "High": 4006.14, "Low": 3996.16, @@ -902,7 +902,7 @@ { "TimeOfCandle": "2021-10-27T17:25:00+03:30", "Amount": 56156.7217013364, - "Date": "2021-10-27T17:25:00+03:30", + "Timestamp": "2021-10-27T17:25:00+03:30", "Open": 3998.24, "High": 4004.64, "Low": 3990.74, @@ -912,7 +912,7 @@ { "TimeOfCandle": "2021-10-27T17:30:00+03:30", "Amount": 14243.4526834259, - "Date": "2021-10-27T17:30:00+03:30", + "Timestamp": "2021-10-27T17:30:00+03:30", "Open": 3994.64, "High": 4004.64, "Low": 3991.04, @@ -922,7 +922,7 @@ { "TimeOfCandle": "2021-10-27T17:35:00+03:30", "Amount": 23717.8848672741, - "Date": "2021-10-27T17:35:00+03:30", + "Timestamp": "2021-10-27T17:35:00+03:30", "Open": 3990.24, "High": 3996.01, "Low": 3990.24, @@ -932,7 +932,7 @@ { "TimeOfCandle": "2021-10-27T17:40:00+03:30", "Amount": 110546.9260679716, - "Date": "2021-10-27T17:40:00+03:30", + "Timestamp": "2021-10-27T17:40:00+03:30", "Open": 3993.03, "High": 3996.31, "Low": 3977.90, @@ -942,7 +942,7 @@ { "TimeOfCandle": "2021-10-27T17:45:00+03:30", "Amount": 59327.4535829998, - "Date": "2021-10-27T17:45:00+03:30", + "Timestamp": "2021-10-27T17:45:00+03:30", "Open": 3982.02, "High": 3982.10, "Low": 3976.13, @@ -952,7 +952,7 @@ { "TimeOfCandle": "2021-10-27T17:50:00+03:30", "Amount": 115547.2986699034, - "Date": "2021-10-27T17:50:00+03:30", + "Timestamp": "2021-10-27T17:50:00+03:30", "Open": 3976.19, "High": 4007.17, "Low": 3966.68, @@ -962,7 +962,7 @@ { "TimeOfCandle": "2021-10-27T17:55:00+03:30", "Amount": 37807.3212559758, - "Date": "2021-10-27T17:55:00+03:30", + "Timestamp": "2021-10-27T17:55:00+03:30", "Open": 4003.03, "High": 4006.99, "Low": 3986.77, @@ -972,7 +972,7 @@ { "TimeOfCandle": "2021-10-27T18:00:00+03:30", "Amount": 40589.7326571969, - "Date": "2021-10-27T18:00:00+03:30", + "Timestamp": "2021-10-27T18:00:00+03:30", "Open": 3988.00, "High": 4005.00, "Low": 3988.00, @@ -982,7 +982,7 @@ { "TimeOfCandle": "2021-10-27T18:05:00+03:30", "Amount": 70403.5437790836, - "Date": "2021-10-27T18:05:00+03:30", + "Timestamp": "2021-10-27T18:05:00+03:30", "Open": 4003.00, "High": 4022.40, "Low": 3996.00, @@ -992,7 +992,7 @@ { "TimeOfCandle": "2021-10-27T18:10:00+03:30", "Amount": 94298.9659173314, - "Date": "2021-10-27T18:10:00+03:30", + "Timestamp": "2021-10-27T18:10:00+03:30", "Open": 4020.28, "High": 4026.00, "Low": 4014.18, @@ -1002,7 +1002,7 @@ { "TimeOfCandle": "2021-10-27T18:15:00+03:30", "Amount": 30485.1910075324, - "Date": "2021-10-27T18:15:00+03:30", + "Timestamp": "2021-10-27T18:15:00+03:30", "Open": 4016.03, "High": 4039.52, "Low": 4016.01, @@ -1012,7 +1012,7 @@ { "TimeOfCandle": "2021-10-27T18:20:00+03:30", "Amount": 76416.9726550779, - "Date": "2021-10-27T18:20:00+03:30", + "Timestamp": "2021-10-27T18:20:00+03:30", "Open": 4036.41, "High": 4038.70, "Low": 4018.00, @@ -1022,7 +1022,7 @@ { "TimeOfCandle": "2021-10-27T18:25:00+03:30", "Amount": 47503.5825096199, - "Date": "2021-10-27T18:25:00+03:30", + "Timestamp": "2021-10-27T18:25:00+03:30", "Open": 4020.00, "High": 4025.93, "Low": 4011.28, @@ -1032,7 +1032,7 @@ { "TimeOfCandle": "2021-10-27T18:30:00+03:30", "Amount": 105518.3959404762, - "Date": "2021-10-27T18:30:00+03:30", + "Timestamp": "2021-10-27T18:30:00+03:30", "Open": 4013.09, "High": 4020.00, "Low": 4008.08, @@ -1042,7 +1042,7 @@ { "TimeOfCandle": "2021-10-27T18:35:00+03:30", "Amount": 65027.5467606292, - "Date": "2021-10-27T18:35:00+03:30", + "Timestamp": "2021-10-27T18:35:00+03:30", "Open": 4009.30, "High": 4025.78, "Low": 4007.42, @@ -1052,7 +1052,7 @@ { "TimeOfCandle": "2021-10-27T18:40:00+03:30", "Amount": 43492.3142140926, - "Date": "2021-10-27T18:40:00+03:30", + "Timestamp": "2021-10-27T18:40:00+03:30", "Open": 4025.58, "High": 4034.82, "Low": 4019.18, @@ -1062,7 +1062,7 @@ { "TimeOfCandle": "2021-10-27T18:45:00+03:30", "Amount": 41467.5231430141, - "Date": "2021-10-27T18:45:00+03:30", + "Timestamp": "2021-10-27T18:45:00+03:30", "Open": 4034.35, "High": 4035.00, "Low": 4022.31, @@ -1072,7 +1072,7 @@ { "TimeOfCandle": "2021-10-27T18:50:00+03:30", "Amount": 43326.9804764149, - "Date": "2021-10-27T18:50:00+03:30", + "Timestamp": "2021-10-27T18:50:00+03:30", "Open": 4021.15, "High": 4021.15, "Low": 4005.03, @@ -1082,7 +1082,7 @@ { "TimeOfCandle": "2021-10-27T18:55:00+03:30", "Amount": 29033.0895006258, - "Date": "2021-10-27T18:55:00+03:30", + "Timestamp": "2021-10-27T18:55:00+03:30", "Open": 4008.01, "High": 4008.01, "Low": 3995.37, @@ -1092,7 +1092,7 @@ { "TimeOfCandle": "2021-10-27T19:00:00+03:30", "Amount": 27447.6735284852, - "Date": "2021-10-27T19:00:00+03:30", + "Timestamp": "2021-10-27T19:00:00+03:30", "Open": 3997.32, "High": 4007.42, "Low": 3994.40, @@ -1102,7 +1102,7 @@ { "TimeOfCandle": "2021-10-27T19:05:00+03:30", "Amount": 60780.3299001833, - "Date": "2021-10-27T19:05:00+03:30", + "Timestamp": "2021-10-27T19:05:00+03:30", "Open": 4009.91, "High": 4011.74, "Low": 3995.18, @@ -1112,7 +1112,7 @@ { "TimeOfCandle": "2021-10-27T19:10:00+03:30", "Amount": 102196.4306436680, - "Date": "2021-10-27T19:10:00+03:30", + "Timestamp": "2021-10-27T19:10:00+03:30", "Open": 3996.14, "High": 3999.10, "Low": 3984.01, @@ -1122,7 +1122,7 @@ { "TimeOfCandle": "2021-10-27T19:15:00+03:30", "Amount": 14416.7893995620, - "Date": "2021-10-27T19:15:00+03:30", + "Timestamp": "2021-10-27T19:15:00+03:30", "Open": 3987.01, "High": 3992.70, "Low": 3985.00, @@ -1132,7 +1132,7 @@ { "TimeOfCandle": "2021-10-27T19:20:00+03:30", "Amount": 39581.0880037031, - "Date": "2021-10-27T19:20:00+03:30", + "Timestamp": "2021-10-27T19:20:00+03:30", "Open": 3988.03, "High": 3999.05, "Low": 3985.03, @@ -1142,7 +1142,7 @@ { "TimeOfCandle": "2021-10-27T19:25:00+03:30", "Amount": 66857.2459465237, - "Date": "2021-10-27T19:25:00+03:30", + "Timestamp": "2021-10-27T19:25:00+03:30", "Open": 3988.05, "High": 3996.82, "Low": 3985.08, @@ -1152,7 +1152,7 @@ { "TimeOfCandle": "2021-10-27T19:30:00+03:30", "Amount": 33569.1745906142, - "Date": "2021-10-27T19:30:00+03:30", + "Timestamp": "2021-10-27T19:30:00+03:30", "Open": 3993.89, "High": 4004.94, "Low": 3990.75, @@ -1162,7 +1162,7 @@ { "TimeOfCandle": "2021-10-27T19:35:00+03:30", "Amount": 81290.5461734230, - "Date": "2021-10-27T19:35:00+03:30", + "Timestamp": "2021-10-27T19:35:00+03:30", "Open": 3996.64, "High": 4009.50, "Low": 3981.01, @@ -1172,7 +1172,7 @@ { "TimeOfCandle": "2021-10-27T19:40:00+03:30", "Amount": 57540.2307408276, - "Date": "2021-10-27T19:40:00+03:30", + "Timestamp": "2021-10-27T19:40:00+03:30", "Open": 3985.76, "High": 3991.79, "Low": 3974.00, @@ -1182,7 +1182,7 @@ { "TimeOfCandle": "2021-10-27T19:45:00+03:30", "Amount": 22353.9525734641, - "Date": "2021-10-27T19:45:00+03:30", + "Timestamp": "2021-10-27T19:45:00+03:30", "Open": 3977.45, "High": 3986.84, "Low": 3975.92, @@ -1192,7 +1192,7 @@ { "TimeOfCandle": "2021-10-27T19:50:00+03:30", "Amount": 166332.5234942742, - "Date": "2021-10-27T19:50:00+03:30", + "Timestamp": "2021-10-27T19:50:00+03:30", "Open": 3982.72, "High": 3988.68, "Low": 3966.01, @@ -1202,7 +1202,7 @@ { "TimeOfCandle": "2021-10-27T19:55:00+03:30", "Amount": 77740.6864855915, - "Date": "2021-10-27T19:55:00+03:30", + "Timestamp": "2021-10-27T19:55:00+03:30", "Open": 3970.50, "High": 3976.00, "Low": 3951.58, @@ -1212,7 +1212,7 @@ { "TimeOfCandle": "2021-10-27T20:00:00+03:30", "Amount": 150124.1088885074, - "Date": "2021-10-27T20:00:00+03:30", + "Timestamp": "2021-10-27T20:00:00+03:30", "Open": 3953.39, "High": 3971.17, "Low": 3950.70, @@ -1222,7 +1222,7 @@ { "TimeOfCandle": "2021-10-27T20:05:00+03:30", "Amount": 49443.4854979036, - "Date": "2021-10-27T20:05:00+03:30", + "Timestamp": "2021-10-27T20:05:00+03:30", "Open": 3969.27, "High": 3991.00, "Low": 3969.27, @@ -1232,7 +1232,7 @@ { "TimeOfCandle": "2021-10-27T20:10:00+03:30", "Amount": 46514.2790878015, - "Date": "2021-10-27T20:10:00+03:30", + "Timestamp": "2021-10-27T20:10:00+03:30", "Open": 3974.99, "High": 3989.00, "Low": 3969.57, @@ -1242,7 +1242,7 @@ { "TimeOfCandle": "2021-10-27T20:15:00+03:30", "Amount": 46794.7610396896, - "Date": "2021-10-27T20:15:00+03:30", + "Timestamp": "2021-10-27T20:15:00+03:30", "Open": 3991.20, "High": 3997.83, "Low": 3979.55, @@ -1252,7 +1252,7 @@ { "TimeOfCandle": "2021-10-27T20:20:00+03:30", "Amount": 31727.9092260733, - "Date": "2021-10-27T20:20:00+03:30", + "Timestamp": "2021-10-27T20:20:00+03:30", "Open": 3982.50, "High": 3992.88, "Low": 3974.13, @@ -1262,7 +1262,7 @@ { "TimeOfCandle": "2021-10-27T20:25:00+03:30", "Amount": 78922.4312739531, - "Date": "2021-10-27T20:25:00+03:30", + "Timestamp": "2021-10-27T20:25:00+03:30", "Open": 3991.22, "High": 3995.17, "Low": 3977.15, @@ -1272,7 +1272,7 @@ { "TimeOfCandle": "2021-10-27T20:30:00+03:30", "Amount": 39017.5323945848, - "Date": "2021-10-27T20:30:00+03:30", + "Timestamp": "2021-10-27T20:30:00+03:30", "Open": 3977.01, "High": 3987.04, "Low": 3977.01, @@ -1282,7 +1282,7 @@ { "TimeOfCandle": "2021-10-27T20:35:00+03:30", "Amount": 133448.2078679945, - "Date": "2021-10-27T20:35:00+03:30", + "Timestamp": "2021-10-27T20:35:00+03:30", "Open": 3986.20, "High": 3996.77, "Low": 3986.20, @@ -1292,7 +1292,7 @@ { "TimeOfCandle": "2021-10-27T20:40:00+03:30", "Amount": 21394.6901842039, - "Date": "2021-10-27T20:40:00+03:30", + "Timestamp": "2021-10-27T20:40:00+03:30", "Open": 3996.72, "High": 4000.06, "Low": 3994.08, @@ -1302,7 +1302,7 @@ { "TimeOfCandle": "2021-10-27T20:45:00+03:30", "Amount": 17995.6504669334, - "Date": "2021-10-27T20:45:00+03:30", + "Timestamp": "2021-10-27T20:45:00+03:30", "Open": 3995.47, "High": 3999.54, "Low": 3993.69, @@ -1312,7 +1312,7 @@ { "TimeOfCandle": "2021-10-27T20:50:00+03:30", "Amount": 51214.2288879049, - "Date": "2021-10-27T20:50:00+03:30", + "Timestamp": "2021-10-27T20:50:00+03:30", "Open": 3995.49, "High": 4004.66, "Low": 3991.93, @@ -1322,7 +1322,7 @@ { "TimeOfCandle": "2021-10-27T20:55:00+03:30", "Amount": 28253.4401483474, - "Date": "2021-10-27T20:55:00+03:30", + "Timestamp": "2021-10-27T20:55:00+03:30", "Open": 3998.10, "High": 4001.99, "Low": 3995.11, @@ -1332,7 +1332,7 @@ { "TimeOfCandle": "2021-10-27T21:00:00+03:30", "Amount": 58895.6822952612, - "Date": "2021-10-27T21:00:00+03:30", + "Timestamp": "2021-10-27T21:00:00+03:30", "Open": 3995.12, "High": 4004.65, "Low": 3994.48, @@ -1342,7 +1342,7 @@ { "TimeOfCandle": "2021-10-27T21:05:00+03:30", "Amount": 51273.3983814235, - "Date": "2021-10-27T21:05:00+03:30", + "Timestamp": "2021-10-27T21:05:00+03:30", "Open": 4000.39, "High": 4000.39, "Low": 3989.20, @@ -1352,7 +1352,7 @@ { "TimeOfCandle": "2021-10-27T21:10:00+03:30", "Amount": 30710.2070649218, - "Date": "2021-10-27T21:10:00+03:30", + "Timestamp": "2021-10-27T21:10:00+03:30", "Open": 3996.41, "High": 3997.52, "Low": 3983.00, @@ -1362,7 +1362,7 @@ { "TimeOfCandle": "2021-10-27T21:15:00+03:30", "Amount": 88986.9621437308, - "Date": "2021-10-27T21:15:00+03:30", + "Timestamp": "2021-10-27T21:15:00+03:30", "Open": 3985.65, "High": 3989.99, "Low": 3980.00, @@ -1372,7 +1372,7 @@ { "TimeOfCandle": "2021-10-27T21:20:00+03:30", "Amount": 137937.2976391142, - "Date": "2021-10-27T21:20:00+03:30", + "Timestamp": "2021-10-27T21:20:00+03:30", "Open": 3982.88, "High": 3988.88, "Low": 3980.00, @@ -1382,7 +1382,7 @@ { "TimeOfCandle": "2021-10-27T21:25:00+03:30", "Amount": 75699.0285909942, - "Date": "2021-10-27T21:25:00+03:30", + "Timestamp": "2021-10-27T21:25:00+03:30", "Open": 3984.61, "High": 3990.00, "Low": 3983.99, @@ -1392,7 +1392,7 @@ { "TimeOfCandle": "2021-10-27T21:30:00+03:30", "Amount": 30744.6813743739, - "Date": "2021-10-27T21:30:00+03:30", + "Timestamp": "2021-10-27T21:30:00+03:30", "Open": 3987.23, "High": 4000.00, "Low": 3987.23, @@ -1402,7 +1402,7 @@ { "TimeOfCandle": "2021-10-27T21:35:00+03:30", "Amount": 159389.8402236414, - "Date": "2021-10-27T21:35:00+03:30", + "Timestamp": "2021-10-27T21:35:00+03:30", "Open": 4000.32, "High": 4001.00, "Low": 3991.44, @@ -1412,7 +1412,7 @@ { "TimeOfCandle": "2021-10-27T21:40:00+03:30", "Amount": 70684.9116768428, - "Date": "2021-10-27T21:40:00+03:30", + "Timestamp": "2021-10-27T21:40:00+03:30", "Open": 3995.55, "High": 3996.92, "Low": 3986.26, @@ -1422,7 +1422,7 @@ { "TimeOfCandle": "2021-10-27T21:45:00+03:30", "Amount": 114116.0910697675, - "Date": "2021-10-27T21:45:00+03:30", + "Timestamp": "2021-10-27T21:45:00+03:30", "Open": 3986.26, "High": 3995.05, "Low": 3983.02, @@ -1432,7 +1432,7 @@ { "TimeOfCandle": "2021-10-27T21:50:00+03:30", "Amount": 112671.5869541956, - "Date": "2021-10-27T21:50:00+03:30", + "Timestamp": "2021-10-27T21:50:00+03:30", "Open": 3996.80, "High": 4000.00, "Low": 3994.33, @@ -1442,7 +1442,7 @@ { "TimeOfCandle": "2021-10-27T21:55:00+03:30", "Amount": 39700.5545955455, - "Date": "2021-10-27T21:55:00+03:30", + "Timestamp": "2021-10-27T21:55:00+03:30", "Open": 4000.00, "High": 4002.01, "Low": 3995.82, @@ -1452,7 +1452,7 @@ { "TimeOfCandle": "2021-10-27T22:00:00+03:30", "Amount": 64357.7482437342, - "Date": "2021-10-27T22:00:00+03:30", + "Timestamp": "2021-10-27T22:00:00+03:30", "Open": 3997.12, "High": 4001.00, "Low": 3996.56, @@ -1462,7 +1462,7 @@ { "TimeOfCandle": "2021-10-27T22:05:00+03:30", "Amount": 149177.2301050255, - "Date": "2021-10-27T22:05:00+03:30", + "Timestamp": "2021-10-27T22:05:00+03:30", "Open": 4002.70, "High": 4006.05, "Low": 3986.75, @@ -1472,7 +1472,7 @@ { "TimeOfCandle": "2021-10-27T22:10:00+03:30", "Amount": 14613.8462000497, - "Date": "2021-10-27T22:10:00+03:30", + "Timestamp": "2021-10-27T22:10:00+03:30", "Open": 3993.10, "High": 3993.10, "Low": 3983.26, @@ -1482,7 +1482,7 @@ { "TimeOfCandle": "2021-10-27T22:15:00+03:30", "Amount": 37524.6073212686, - "Date": "2021-10-27T22:15:00+03:30", + "Timestamp": "2021-10-27T22:15:00+03:30", "Open": 3985.18, "High": 3985.19, "Low": 3975.61, @@ -1492,7 +1492,7 @@ { "TimeOfCandle": "2021-10-27T22:20:00+03:30", "Amount": 39998.1036182549, - "Date": "2021-10-27T22:20:00+03:30", + "Timestamp": "2021-10-27T22:20:00+03:30", "Open": 3976.86, "High": 3985.21, "Low": 3976.86, @@ -1502,7 +1502,7 @@ { "TimeOfCandle": "2021-10-27T22:25:00+03:30", "Amount": 303615.7030023482, - "Date": "2021-10-27T22:25:00+03:30", + "Timestamp": "2021-10-27T22:25:00+03:30", "Open": 3981.71, "High": 3982.99, "Low": 3966.00, @@ -1512,7 +1512,7 @@ { "TimeOfCandle": "2021-10-27T22:30:00+03:30", "Amount": 94867.3177641075, - "Date": "2021-10-27T22:30:00+03:30", + "Timestamp": "2021-10-27T22:30:00+03:30", "Open": 3968.16, "High": 3975.43, "Low": 3955.00, @@ -1522,7 +1522,7 @@ { "TimeOfCandle": "2021-10-27T22:35:00+03:30", "Amount": 69408.1348228205, - "Date": "2021-10-27T22:35:00+03:30", + "Timestamp": "2021-10-27T22:35:00+03:30", "Open": 3958.32, "High": 3964.46, "Low": 3951.65, @@ -1532,7 +1532,7 @@ { "TimeOfCandle": "2021-10-27T22:40:00+03:30", "Amount": 80717.8313752461, - "Date": "2021-10-27T22:40:00+03:30", + "Timestamp": "2021-10-27T22:40:00+03:30", "Open": 3957.04, "High": 3957.04, "Low": 3949.00, @@ -1542,7 +1542,7 @@ { "TimeOfCandle": "2021-10-27T22:45:00+03:30", "Amount": 242708.8666580094, - "Date": "2021-10-27T22:45:00+03:30", + "Timestamp": "2021-10-27T22:45:00+03:30", "Open": 3949.70, "High": 3974.40, "Low": 3949.70, @@ -1552,7 +1552,7 @@ { "TimeOfCandle": "2021-10-27T22:50:00+03:30", "Amount": 227192.0887373993, - "Date": "2021-10-27T22:50:00+03:30", + "Timestamp": "2021-10-27T22:50:00+03:30", "Open": 3951.01, "High": 3971.00, "Low": 3945.00, @@ -1562,7 +1562,7 @@ { "TimeOfCandle": "2021-10-27T22:55:00+03:30", "Amount": 144539.4606930547, - "Date": "2021-10-27T22:55:00+03:30", + "Timestamp": "2021-10-27T22:55:00+03:30", "Open": 3970.63, "High": 3977.00, "Low": 3957.08, @@ -1572,7 +1572,7 @@ { "TimeOfCandle": "2021-10-27T23:00:00+03:30", "Amount": 186346.2273317000, - "Date": "2021-10-27T23:00:00+03:30", + "Timestamp": "2021-10-27T23:00:00+03:30", "Open": 3960.01, "High": 3960.01, "Low": 3939.99, @@ -1582,7 +1582,7 @@ { "TimeOfCandle": "2021-10-27T23:05:00+03:30", "Amount": 74035.7146876510, - "Date": "2021-10-27T23:05:00+03:30", + "Timestamp": "2021-10-27T23:05:00+03:30", "Open": 3960.00, "High": 3978.00, "Low": 3955.68, @@ -1592,7 +1592,7 @@ { "TimeOfCandle": "2021-10-27T23:10:00+03:30", "Amount": 327917.1808982129, - "Date": "2021-10-27T23:10:00+03:30", + "Timestamp": "2021-10-27T23:10:00+03:30", "Open": 3966.79, "High": 3968.00, "Low": 3954.62, @@ -1602,7 +1602,7 @@ { "TimeOfCandle": "2021-10-27T23:15:00+03:30", "Amount": 100023.7304644088, - "Date": "2021-10-27T23:15:00+03:30", + "Timestamp": "2021-10-27T23:15:00+03:30", "Open": 3956.51, "High": 3971.78, "Low": 3956.00, @@ -1612,7 +1612,7 @@ { "TimeOfCandle": "2021-10-27T23:20:00+03:30", "Amount": 81712.5185117450, - "Date": "2021-10-27T23:20:00+03:30", + "Timestamp": "2021-10-27T23:20:00+03:30", "Open": 3971.78, "High": 3989.99, "Low": 3970.50, @@ -1622,7 +1622,7 @@ { "TimeOfCandle": "2021-10-27T23:25:00+03:30", "Amount": 82774.0681344909, - "Date": "2021-10-27T23:25:00+03:30", + "Timestamp": "2021-10-27T23:25:00+03:30", "Open": 3989.98, "High": 4004.00, "Low": 3989.13, @@ -1632,7 +1632,7 @@ { "TimeOfCandle": "2021-10-27T23:30:00+03:30", "Amount": 106105.0303093647, - "Date": "2021-10-27T23:30:00+03:30", + "Timestamp": "2021-10-27T23:30:00+03:30", "Open": 3997.69, "High": 4022.73, "Low": 3994.01, @@ -1642,7 +1642,7 @@ { "TimeOfCandle": "2021-10-27T23:35:00+03:30", "Amount": 89768.7369124281, - "Date": "2021-10-27T23:35:00+03:30", + "Timestamp": "2021-10-27T23:35:00+03:30", "Open": 4022.52, "High": 4022.52, "Low": 4006.82, @@ -1652,7 +1652,7 @@ { "TimeOfCandle": "2021-10-27T23:40:00+03:30", "Amount": 232493.5492491686, - "Date": "2021-10-27T23:40:00+03:30", + "Timestamp": "2021-10-27T23:40:00+03:30", "Open": 4007.01, "High": 4009.00, "Low": 3977.64, @@ -1662,7 +1662,7 @@ { "TimeOfCandle": "2021-10-27T23:45:00+03:30", "Amount": 129847.5874973421, - "Date": "2021-10-27T23:45:00+03:30", + "Timestamp": "2021-10-27T23:45:00+03:30", "Open": 3978.77, "High": 3991.34, "Low": 3977.63, @@ -1672,7 +1672,7 @@ { "TimeOfCandle": "2021-10-27T23:50:00+03:30", "Amount": 67983.2700277651, - "Date": "2021-10-27T23:50:00+03:30", + "Timestamp": "2021-10-27T23:50:00+03:30", "Open": 3981.60, "High": 3986.64, "Low": 3977.81, @@ -1682,7 +1682,7 @@ { "TimeOfCandle": "2021-10-27T23:55:00+03:30", "Amount": 16043.0685293261, - "Date": "2021-10-27T23:55:00+03:30", + "Timestamp": "2021-10-27T23:55:00+03:30", "Open": 3979.39, "High": 3979.39, "Low": 3974.31, @@ -1692,7 +1692,7 @@ { "TimeOfCandle": "2021-10-28T00:00:00+03:30", "Amount": 34836.9960663127, - "Date": "2021-10-28T00:00:00+03:30", + "Timestamp": "2021-10-28T00:00:00+03:30", "Open": 3975.01, "High": 3985.00, "Low": 3968.49, @@ -1702,7 +1702,7 @@ { "TimeOfCandle": "2021-10-28T00:05:00+03:30", "Amount": 49114.9030808019, - "Date": "2021-10-28T00:05:00+03:30", + "Timestamp": "2021-10-28T00:05:00+03:30", "Open": 3968.48, "High": 3976.00, "Low": 3967.00, @@ -1712,7 +1712,7 @@ { "TimeOfCandle": "2021-10-28T00:10:00+03:30", "Amount": 37188.1367738455, - "Date": "2021-10-28T00:10:00+03:30", + "Timestamp": "2021-10-28T00:10:00+03:30", "Open": 3968.19, "High": 3968.19, "Low": 3958.77, @@ -1722,7 +1722,7 @@ { "TimeOfCandle": "2021-10-28T00:15:00+03:30", "Amount": 46604.1712072566, - "Date": "2021-10-28T00:15:00+03:30", + "Timestamp": "2021-10-28T00:15:00+03:30", "Open": 3961.03, "High": 3977.00, "Low": 3960.66, @@ -1732,7 +1732,7 @@ { "TimeOfCandle": "2021-10-28T00:20:00+03:30", "Amount": 48573.0487806496, - "Date": "2021-10-28T00:20:00+03:30", + "Timestamp": "2021-10-28T00:20:00+03:30", "Open": 3974.92, "High": 3980.97, "Low": 3966.22, @@ -1742,7 +1742,7 @@ { "TimeOfCandle": "2021-10-28T00:25:00+03:30", "Amount": 65288.0958001717, - "Date": "2021-10-28T00:25:00+03:30", + "Timestamp": "2021-10-28T00:25:00+03:30", "Open": 3978.44, "High": 3983.99, "Low": 3975.41, @@ -1752,7 +1752,7 @@ { "TimeOfCandle": "2021-10-28T00:30:00+03:30", "Amount": 106878.1389167607, - "Date": "2021-10-28T00:30:00+03:30", + "Timestamp": "2021-10-28T00:30:00+03:30", "Open": 3983.00, "High": 3990.00, "Low": 3972.55, @@ -1762,7 +1762,7 @@ { "TimeOfCandle": "2021-10-28T00:35:00+03:30", "Amount": 15221.3173230813, - "Date": "2021-10-28T00:35:00+03:30", + "Timestamp": "2021-10-28T00:35:00+03:30", "Open": 3987.37, "High": 3987.37, "Low": 3981.16, @@ -1772,7 +1772,7 @@ { "TimeOfCandle": "2021-10-28T00:40:00+03:30", "Amount": 19901.4094844077, - "Date": "2021-10-28T00:40:00+03:30", + "Timestamp": "2021-10-28T00:40:00+03:30", "Open": 3985.13, "High": 3991.00, "Low": 3985.13, @@ -1782,7 +1782,7 @@ { "TimeOfCandle": "2021-10-28T00:45:00+03:30", "Amount": 8190.7067113876, - "Date": "2021-10-28T00:45:00+03:30", + "Timestamp": "2021-10-28T00:45:00+03:30", "Open": 3991.00, "High": 3994.58, "Low": 3989.23, @@ -1792,7 +1792,7 @@ { "TimeOfCandle": "2021-10-28T00:50:00+03:30", "Amount": 487558.2178707675, - "Date": "2021-10-28T00:50:00+03:30", + "Timestamp": "2021-10-28T00:50:00+03:30", "Open": 3996.75, "High": 3996.75, "Low": 3987.70, @@ -1802,7 +1802,7 @@ { "TimeOfCandle": "2021-10-28T00:55:00+03:30", "Amount": 14335.0927518033, - "Date": "2021-10-28T00:55:00+03:30", + "Timestamp": "2021-10-28T00:55:00+03:30", "Open": 3989.73, "High": 3989.73, "Low": 3985.78, @@ -1812,7 +1812,7 @@ { "TimeOfCandle": "2021-10-28T01:00:00+03:30", "Amount": 12354.9197489652, - "Date": "2021-10-28T01:00:00+03:30", + "Timestamp": "2021-10-28T01:00:00+03:30", "Open": 3986.90, "High": 3997.15, "Low": 3985.78, @@ -1822,7 +1822,7 @@ { "TimeOfCandle": "2021-10-28T01:05:00+03:30", "Amount": 40364.0646764932, - "Date": "2021-10-28T01:05:00+03:30", + "Timestamp": "2021-10-28T01:05:00+03:30", "Open": 3997.08, "High": 3998.20, "Low": 3993.02, @@ -1832,7 +1832,7 @@ { "TimeOfCandle": "2021-10-28T01:10:00+03:30", "Amount": 229209.3429570671, - "Date": "2021-10-28T01:10:00+03:30", + "Timestamp": "2021-10-28T01:10:00+03:30", "Open": 3996.99, "High": 4004.37, "Low": 3996.99, @@ -1842,7 +1842,7 @@ { "TimeOfCandle": "2021-10-28T01:15:00+03:30", "Amount": 31346.6619300750, - "Date": "2021-10-28T01:15:00+03:30", + "Timestamp": "2021-10-28T01:15:00+03:30", "Open": 4002.59, "High": 4010.00, "Low": 4001.58, @@ -1852,7 +1852,7 @@ { "TimeOfCandle": "2021-10-28T01:20:00+03:30", "Amount": 57976.4738653399, - "Date": "2021-10-28T01:20:00+03:30", + "Timestamp": "2021-10-28T01:20:00+03:30", "Open": 4001.30, "High": 4004.76, "Low": 3994.84, @@ -1862,7 +1862,7 @@ { "TimeOfCandle": "2021-10-28T01:25:00+03:30", "Amount": 65667.2820190157, - "Date": "2021-10-28T01:25:00+03:30", + "Timestamp": "2021-10-28T01:25:00+03:30", "Open": 4002.62, "High": 4002.62, "Low": 3993.57, @@ -1872,7 +1872,7 @@ { "TimeOfCandle": "2021-10-28T01:30:00+03:30", "Amount": 41969.6892109929, - "Date": "2021-10-28T01:30:00+03:30", + "Timestamp": "2021-10-28T01:30:00+03:30", "Open": 3993.57, "High": 3998.71, "Low": 3985.90, @@ -1882,7 +1882,7 @@ { "TimeOfCandle": "2021-10-28T01:35:00+03:30", "Amount": 22620.1554278169, - "Date": "2021-10-28T01:35:00+03:30", + "Timestamp": "2021-10-28T01:35:00+03:30", "Open": 3988.08, "High": 3990.90, "Low": 3985.67, @@ -1892,7 +1892,7 @@ { "TimeOfCandle": "2021-10-28T01:40:00+03:30", "Amount": 127048.3484521555, - "Date": "2021-10-28T01:40:00+03:30", + "Timestamp": "2021-10-28T01:40:00+03:30", "Open": 3988.00, "High": 3988.00, "Low": 3974.74, @@ -1902,7 +1902,7 @@ { "TimeOfCandle": "2021-10-28T01:45:00+03:30", "Amount": 83169.8303165688, - "Date": "2021-10-28T01:45:00+03:30", + "Timestamp": "2021-10-28T01:45:00+03:30", "Open": 3975.80, "High": 3982.45, "Low": 3975.80, @@ -1912,7 +1912,7 @@ { "TimeOfCandle": "2021-10-28T01:50:00+03:30", "Amount": 39434.4127253306, - "Date": "2021-10-28T01:50:00+03:30", + "Timestamp": "2021-10-28T01:50:00+03:30", "Open": 3975.79, "High": 3975.79, "Low": 3970.48, @@ -1922,7 +1922,7 @@ { "TimeOfCandle": "2021-10-28T01:55:00+03:30", "Amount": 52445.5821880096, - "Date": "2021-10-28T01:55:00+03:30", + "Timestamp": "2021-10-28T01:55:00+03:30", "Open": 3970.49, "High": 3985.61, "Low": 3970.01, @@ -1932,7 +1932,7 @@ { "TimeOfCandle": "2021-10-28T02:00:00+03:30", "Amount": 21106.8787935945, - "Date": "2021-10-28T02:00:00+03:30", + "Timestamp": "2021-10-28T02:00:00+03:30", "Open": 3983.00, "High": 3985.31, "Low": 3977.38, @@ -1942,7 +1942,7 @@ { "TimeOfCandle": "2021-10-28T02:05:00+03:30", "Amount": 40052.2333173531, - "Date": "2021-10-28T02:05:00+03:30", + "Timestamp": "2021-10-28T02:05:00+03:30", "Open": 3987.99, "High": 3990.15, "Low": 3975.92, @@ -1952,7 +1952,7 @@ { "TimeOfCandle": "2021-10-28T02:10:00+03:30", "Amount": 214487.7497857716, - "Date": "2021-10-28T02:10:00+03:30", + "Timestamp": "2021-10-28T02:10:00+03:30", "Open": 3975.94, "High": 3975.94, "Low": 3970.00, @@ -1962,7 +1962,7 @@ { "TimeOfCandle": "2021-10-28T02:15:00+03:30", "Amount": 32175.6777478184, - "Date": "2021-10-28T02:15:00+03:30", + "Timestamp": "2021-10-28T02:15:00+03:30", "Open": 3968.14, "High": 3971.38, "Low": 3960.00, @@ -1972,7 +1972,7 @@ { "TimeOfCandle": "2021-10-28T02:20:00+03:30", "Amount": 19627.3625307061, - "Date": "2021-10-28T02:20:00+03:30", + "Timestamp": "2021-10-28T02:20:00+03:30", "Open": 3960.21, "High": 3965.78, "Low": 3958.01, @@ -1982,7 +1982,7 @@ { "TimeOfCandle": "2021-10-28T02:25:00+03:30", "Amount": 6893.4923853207, - "Date": "2021-10-28T02:25:00+03:30", + "Timestamp": "2021-10-28T02:25:00+03:30", "Open": 3965.39, "High": 3975.14, "Low": 3965.39, @@ -1992,7 +1992,7 @@ { "TimeOfCandle": "2021-10-28T02:30:00+03:30", "Amount": 28637.4926543907, - "Date": "2021-10-28T02:30:00+03:30", + "Timestamp": "2021-10-28T02:30:00+03:30", "Open": 3974.51, "High": 3982.64, "Low": 3974.01, @@ -2002,7 +2002,7 @@ { "TimeOfCandle": "2021-10-28T02:35:00+03:30", "Amount": 39542.6510080857, - "Date": "2021-10-28T02:35:00+03:30", + "Timestamp": "2021-10-28T02:35:00+03:30", "Open": 3976.78, "High": 3981.33, "Low": 3970.41, @@ -2012,7 +2012,7 @@ { "TimeOfCandle": "2021-10-28T02:40:00+03:30", "Amount": 42491.3188601221, - "Date": "2021-10-28T02:40:00+03:30", + "Timestamp": "2021-10-28T02:40:00+03:30", "Open": 3979.77, "High": 3979.77, "Low": 3969.01, @@ -2022,7 +2022,7 @@ { "TimeOfCandle": "2021-10-28T02:45:00+03:30", "Amount": 4649.3478574620, - "Date": "2021-10-28T02:45:00+03:30", + "Timestamp": "2021-10-28T02:45:00+03:30", "Open": 3971.79, "High": 3971.79, "Low": 3961.79, @@ -2032,7 +2032,7 @@ { "TimeOfCandle": "2021-10-28T02:50:00+03:30", "Amount": 23515.6423111153, - "Date": "2021-10-28T02:50:00+03:30", + "Timestamp": "2021-10-28T02:50:00+03:30", "Open": 3961.81, "High": 3961.81, "Low": 3952.42, @@ -2042,7 +2042,7 @@ { "TimeOfCandle": "2021-10-28T02:55:00+03:30", "Amount": 29357.1486591352, - "Date": "2021-10-28T02:55:00+03:30", + "Timestamp": "2021-10-28T02:55:00+03:30", "Open": 3951.54, "High": 3960.62, "Low": 3950.00, @@ -2052,7 +2052,7 @@ { "TimeOfCandle": "2021-10-28T03:00:00+03:30", "Amount": 23042.5607715281, - "Date": "2021-10-28T03:00:00+03:30", + "Timestamp": "2021-10-28T03:00:00+03:30", "Open": 3952.51, "High": 3963.99, "Low": 3952.51, @@ -2062,7 +2062,7 @@ { "TimeOfCandle": "2021-10-28T03:05:00+03:30", "Amount": 42530.4085163106, - "Date": "2021-10-28T03:05:00+03:30", + "Timestamp": "2021-10-28T03:05:00+03:30", "Open": 3956.10, "High": 3956.10, "Low": 3943.26, @@ -2072,7 +2072,7 @@ { "TimeOfCandle": "2021-10-28T03:10:00+03:30", "Amount": 39556.1025329786, - "Date": "2021-10-28T03:10:00+03:30", + "Timestamp": "2021-10-28T03:10:00+03:30", "Open": 3943.20, "High": 3948.69, "Low": 3942.01, @@ -2082,7 +2082,7 @@ { "TimeOfCandle": "2021-10-28T03:15:00+03:30", "Amount": 178762.7658904020, - "Date": "2021-10-28T03:15:00+03:30", + "Timestamp": "2021-10-28T03:15:00+03:30", "Open": 3945.38, "High": 3952.45, "Low": 3912.18, @@ -2092,7 +2092,7 @@ { "TimeOfCandle": "2021-10-28T03:20:00+03:30", "Amount": 72632.1365462373, - "Date": "2021-10-28T03:20:00+03:30", + "Timestamp": "2021-10-28T03:20:00+03:30", "Open": 3930.03, "High": 3948.13, "Low": 3923.49, @@ -2102,7 +2102,7 @@ { "TimeOfCandle": "2021-10-28T03:25:00+03:30", "Amount": 62098.1811618796, - "Date": "2021-10-28T03:25:00+03:30", + "Timestamp": "2021-10-28T03:25:00+03:30", "Open": 3948.84, "High": 3949.70, "Low": 3920.88, @@ -2112,7 +2112,7 @@ { "TimeOfCandle": "2021-10-28T03:30:00+03:30", "Amount": 31756.3007380927, - "Date": "2021-10-28T03:30:00+03:30", + "Timestamp": "2021-10-28T03:30:00+03:30", "Open": 3922.00, "High": 3939.59, "Low": 3922.00, @@ -2122,7 +2122,7 @@ { "TimeOfCandle": "2021-10-28T03:35:00+03:30", "Amount": 48851.7663252967, - "Date": "2021-10-28T03:35:00+03:30", + "Timestamp": "2021-10-28T03:35:00+03:30", "Open": 3936.99, "High": 3949.83, "Low": 3926.83, @@ -2132,7 +2132,7 @@ { "TimeOfCandle": "2021-10-28T03:40:00+03:30", "Amount": 45433.3652010404, - "Date": "2021-10-28T03:40:00+03:30", + "Timestamp": "2021-10-28T03:40:00+03:30", "Open": 3926.78, "High": 3956.03, "Low": 3926.78, @@ -2142,7 +2142,7 @@ { "TimeOfCandle": "2021-10-28T03:45:00+03:30", "Amount": 60588.5254922610, - "Date": "2021-10-28T03:45:00+03:30", + "Timestamp": "2021-10-28T03:45:00+03:30", "Open": 3958.00, "High": 3967.24, "Low": 3955.51, @@ -2152,7 +2152,7 @@ { "TimeOfCandle": "2021-10-28T03:50:00+03:30", "Amount": 55195.7613779615, - "Date": "2021-10-28T03:50:00+03:30", + "Timestamp": "2021-10-28T03:50:00+03:30", "Open": 3963.88, "High": 3965.91, "Low": 3955.14, @@ -2162,7 +2162,7 @@ { "TimeOfCandle": "2021-10-28T03:55:00+03:30", "Amount": 2856.5374718826, - "Date": "2021-10-28T03:55:00+03:30", + "Timestamp": "2021-10-28T03:55:00+03:30", "Open": 3956.99, "High": 3956.99, "Low": 3945.96, @@ -2172,7 +2172,7 @@ { "TimeOfCandle": "2021-10-28T04:00:00+03:30", "Amount": 35308.7610833563, - "Date": "2021-10-28T04:00:00+03:30", + "Timestamp": "2021-10-28T04:00:00+03:30", "Open": 3947.13, "High": 3948.44, "Low": 3929.76, @@ -2182,7 +2182,7 @@ { "TimeOfCandle": "2021-10-28T04:05:00+03:30", "Amount": 146879.0690564311, - "Date": "2021-10-28T04:05:00+03:30", + "Timestamp": "2021-10-28T04:05:00+03:30", "Open": 3931.00, "High": 3950.08, "Low": 3930.68, @@ -2192,7 +2192,7 @@ { "TimeOfCandle": "2021-10-28T04:10:00+03:30", "Amount": 111037.5786578993, - "Date": "2021-10-28T04:10:00+03:30", + "Timestamp": "2021-10-28T04:10:00+03:30", "Open": 3943.76, "High": 3943.76, "Low": 3933.44, @@ -2202,7 +2202,7 @@ { "TimeOfCandle": "2021-10-28T04:15:00+03:30", "Amount": 59594.7710248393, - "Date": "2021-10-28T04:15:00+03:30", + "Timestamp": "2021-10-28T04:15:00+03:30", "Open": 3941.01, "High": 3948.92, "Low": 3930.04, @@ -2212,7 +2212,7 @@ { "TimeOfCandle": "2021-10-28T04:20:00+03:30", "Amount": 61073.9412918996, - "Date": "2021-10-28T04:20:00+03:30", + "Timestamp": "2021-10-28T04:20:00+03:30", "Open": 3930.86, "High": 3932.91, "Low": 3920.23, @@ -2222,7 +2222,7 @@ { "TimeOfCandle": "2021-10-28T04:25:00+03:30", "Amount": 16457.5220338643, - "Date": "2021-10-28T04:25:00+03:30", + "Timestamp": "2021-10-28T04:25:00+03:30", "Open": 3929.00, "High": 3929.00, "Low": 3915.00, @@ -2232,7 +2232,7 @@ { "TimeOfCandle": "2021-10-28T04:30:00+03:30", "Amount": 261686.4374838773, - "Date": "2021-10-28T04:30:00+03:30", + "Timestamp": "2021-10-28T04:30:00+03:30", "Open": 3912.25, "High": 3930.02, "Low": 3890.00, @@ -2242,7 +2242,7 @@ { "TimeOfCandle": "2021-10-28T04:35:00+03:30", "Amount": 35550.2363406301, - "Date": "2021-10-28T04:35:00+03:30", + "Timestamp": "2021-10-28T04:35:00+03:30", "Open": 3932.03, "High": 3942.50, "Low": 3929.63, @@ -2252,7 +2252,7 @@ { "TimeOfCandle": "2021-10-28T04:40:00+03:30", "Amount": 61593.6344219828, - "Date": "2021-10-28T04:40:00+03:30", + "Timestamp": "2021-10-28T04:40:00+03:30", "Open": 3941.34, "High": 3952.40, "Low": 3940.15, @@ -2262,7 +2262,7 @@ { "TimeOfCandle": "2021-10-28T04:45:00+03:30", "Amount": 28698.6822369537, - "Date": "2021-10-28T04:45:00+03:30", + "Timestamp": "2021-10-28T04:45:00+03:30", "Open": 3948.04, "High": 3964.28, "Low": 3943.70, @@ -2272,7 +2272,7 @@ { "TimeOfCandle": "2021-10-28T04:50:00+03:30", "Amount": 35214.5840803390, - "Date": "2021-10-28T04:50:00+03:30", + "Timestamp": "2021-10-28T04:50:00+03:30", "Open": 3958.39, "High": 3971.33, "Low": 3947.02, @@ -2282,7 +2282,7 @@ { "TimeOfCandle": "2021-10-28T04:55:00+03:30", "Amount": 22680.0582273844, - "Date": "2021-10-28T04:55:00+03:30", + "Timestamp": "2021-10-28T04:55:00+03:30", "Open": 3949.04, "High": 3966.13, "Low": 3949.04, @@ -2292,7 +2292,7 @@ { "TimeOfCandle": "2021-10-28T05:00:00+03:30", "Amount": 12510.2603304619, - "Date": "2021-10-28T05:00:00+03:30", + "Timestamp": "2021-10-28T05:00:00+03:30", "Open": 3966.48, "High": 3968.58, "Low": 3956.14, @@ -2302,7 +2302,7 @@ { "TimeOfCandle": "2021-10-28T05:05:00+03:30", "Amount": 4157.8630732236, - "Date": "2021-10-28T05:05:00+03:30", + "Timestamp": "2021-10-28T05:05:00+03:30", "Open": 3953.75, "High": 3961.31, "Low": 3953.75, @@ -2312,7 +2312,7 @@ { "TimeOfCandle": "2021-10-28T05:10:00+03:30", "Amount": 55071.1071877642, - "Date": "2021-10-28T05:10:00+03:30", + "Timestamp": "2021-10-28T05:10:00+03:30", "Open": 3963.98, "High": 3989.79, "Low": 3963.98, @@ -2322,7 +2322,7 @@ { "TimeOfCandle": "2021-10-28T05:15:00+03:30", "Amount": 27931.7230543210, - "Date": "2021-10-28T05:15:00+03:30", + "Timestamp": "2021-10-28T05:15:00+03:30", "Open": 3984.00, "High": 3986.71, "Low": 3973.01, @@ -2332,7 +2332,7 @@ { "TimeOfCandle": "2021-10-28T05:20:00+03:30", "Amount": 20140.0700601502, - "Date": "2021-10-28T05:20:00+03:30", + "Timestamp": "2021-10-28T05:20:00+03:30", "Open": 3972.43, "High": 3988.00, "Low": 3971.40, @@ -2342,7 +2342,7 @@ { "TimeOfCandle": "2021-10-28T05:25:00+03:30", "Amount": 27940.2730614710, - "Date": "2021-10-28T05:25:00+03:30", + "Timestamp": "2021-10-28T05:25:00+03:30", "Open": 3984.68, "High": 3994.15, "Low": 3982.10, @@ -2352,7 +2352,7 @@ { "TimeOfCandle": "2021-10-28T05:30:00+03:30", "Amount": 377296.6298401995, - "Date": "2021-10-28T05:30:00+03:30", + "Timestamp": "2021-10-28T05:30:00+03:30", "Open": 3994.48, "High": 3994.48, "Low": 3978.80, @@ -2362,7 +2362,7 @@ { "TimeOfCandle": "2021-10-28T05:35:00+03:30", "Amount": 61752.0229436478, - "Date": "2021-10-28T05:35:00+03:30", + "Timestamp": "2021-10-28T05:35:00+03:30", "Open": 3983.46, "High": 3992.54, "Low": 3980.07, @@ -2372,7 +2372,7 @@ { "TimeOfCandle": "2021-10-28T05:40:00+03:30", "Amount": 28619.8133087463, - "Date": "2021-10-28T05:40:00+03:30", + "Timestamp": "2021-10-28T05:40:00+03:30", "Open": 3994.46, "High": 3994.46, "Low": 3979.46, @@ -2382,7 +2382,7 @@ { "TimeOfCandle": "2021-10-28T05:45:00+03:30", "Amount": 3253.3199269062, - "Date": "2021-10-28T05:45:00+03:30", + "Timestamp": "2021-10-28T05:45:00+03:30", "Open": 3979.64, "High": 3982.15, "Low": 3977.41, @@ -2392,7 +2392,7 @@ { "TimeOfCandle": "2021-10-28T05:50:00+03:30", "Amount": 14832.3673962559, - "Date": "2021-10-28T05:50:00+03:30", + "Timestamp": "2021-10-28T05:50:00+03:30", "Open": 3975.96, "High": 3976.64, "Low": 3970.00, @@ -2402,7 +2402,7 @@ { "TimeOfCandle": "2021-10-28T05:55:00+03:30", "Amount": 107206.4616606490, - "Date": "2021-10-28T05:55:00+03:30", + "Timestamp": "2021-10-28T05:55:00+03:30", "Open": 3970.04, "High": 3979.03, "Low": 3970.04, @@ -2412,7 +2412,7 @@ { "TimeOfCandle": "2021-10-28T06:00:00+03:30", "Amount": 33323.9111571420, - "Date": "2021-10-28T06:00:00+03:30", + "Timestamp": "2021-10-28T06:00:00+03:30", "Open": 3974.06, "High": 3984.99, "Low": 3974.00, @@ -2422,7 +2422,7 @@ { "TimeOfCandle": "2021-10-28T06:05:00+03:30", "Amount": 86242.5545243392, - "Date": "2021-10-28T06:05:00+03:30", + "Timestamp": "2021-10-28T06:05:00+03:30", "Open": 3980.46, "High": 3986.89, "Low": 3980.46, @@ -2432,7 +2432,7 @@ { "TimeOfCandle": "2021-10-28T06:10:00+03:30", "Amount": 29914.3522593907, - "Date": "2021-10-28T06:10:00+03:30", + "Timestamp": "2021-10-28T06:10:00+03:30", "Open": 3982.66, "High": 3986.47, "Low": 3980.74, @@ -2442,7 +2442,7 @@ { "TimeOfCandle": "2021-10-28T06:15:00+03:30", "Amount": 23767.7803766722, - "Date": "2021-10-28T06:15:00+03:30", + "Timestamp": "2021-10-28T06:15:00+03:30", "Open": 3984.14, "High": 3992.99, "Low": 3983.50, @@ -2452,7 +2452,7 @@ { "TimeOfCandle": "2021-10-28T06:20:00+03:30", "Amount": 33204.2712460854, - "Date": "2021-10-28T06:20:00+03:30", + "Timestamp": "2021-10-28T06:20:00+03:30", "Open": 3989.02, "High": 3989.02, "Low": 3978.49, @@ -2462,7 +2462,7 @@ { "TimeOfCandle": "2021-10-28T06:25:00+03:30", "Amount": 67822.6708667253, - "Date": "2021-10-28T06:25:00+03:30", + "Timestamp": "2021-10-28T06:25:00+03:30", "Open": 3986.00, "High": 3997.00, "Low": 3984.73, @@ -2472,7 +2472,7 @@ { "TimeOfCandle": "2021-10-28T06:30:00+03:30", "Amount": 66611.3175330311, - "Date": "2021-10-28T06:30:00+03:30", + "Timestamp": "2021-10-28T06:30:00+03:30", "Open": 3997.99, "High": 4019.16, "Low": 3997.45, @@ -2482,7 +2482,7 @@ { "TimeOfCandle": "2021-10-28T06:35:00+03:30", "Amount": 176207.8822032963, - "Date": "2021-10-28T06:35:00+03:30", + "Timestamp": "2021-10-28T06:35:00+03:30", "Open": 4018.85, "High": 4020.00, "Low": 4003.28, @@ -2492,7 +2492,7 @@ { "TimeOfCandle": "2021-10-28T06:40:00+03:30", "Amount": 24921.7508786178, - "Date": "2021-10-28T06:40:00+03:30", + "Timestamp": "2021-10-28T06:40:00+03:30", "Open": 4003.26, "High": 4011.01, "Low": 4001.57, @@ -2502,7 +2502,7 @@ { "TimeOfCandle": "2021-10-28T06:45:00+03:30", "Amount": 115028.2245376136, - "Date": "2021-10-28T06:45:00+03:30", + "Timestamp": "2021-10-28T06:45:00+03:30", "Open": 4004.66, "High": 4004.66, "Low": 3987.98, @@ -2512,7 +2512,7 @@ { "TimeOfCandle": "2021-10-28T06:50:00+03:30", "Amount": 28514.5788980357, - "Date": "2021-10-28T06:50:00+03:30", + "Timestamp": "2021-10-28T06:50:00+03:30", "Open": 3987.44, "High": 3990.61, "Low": 3979.01, @@ -2522,7 +2522,7 @@ { "TimeOfCandle": "2021-10-28T06:55:00+03:30", "Amount": 11977.9046242508, - "Date": "2021-10-28T06:55:00+03:30", + "Timestamp": "2021-10-28T06:55:00+03:30", "Open": 3983.30, "High": 3984.21, "Low": 3976.00, @@ -2532,7 +2532,7 @@ { "TimeOfCandle": "2021-10-28T07:00:00+03:30", "Amount": 15257.1653014095, - "Date": "2021-10-28T07:00:00+03:30", + "Timestamp": "2021-10-28T07:00:00+03:30", "Open": 3983.00, "High": 3993.76, "Low": 3983.00, @@ -2542,7 +2542,7 @@ { "TimeOfCandle": "2021-10-28T07:05:00+03:30", "Amount": 8790.8925250374, - "Date": "2021-10-28T07:05:00+03:30", + "Timestamp": "2021-10-28T07:05:00+03:30", "Open": 3993.23, "High": 3996.32, "Low": 3987.25, @@ -2552,7 +2552,7 @@ { "TimeOfCandle": "2021-10-28T07:10:00+03:30", "Amount": 23316.0996030819, - "Date": "2021-10-28T07:10:00+03:30", + "Timestamp": "2021-10-28T07:10:00+03:30", "Open": 3988.60, "High": 3992.75, "Low": 3986.00, @@ -2562,7 +2562,7 @@ { "TimeOfCandle": "2021-10-28T07:15:00+03:30", "Amount": 178519.9664682986, - "Date": "2021-10-28T07:15:00+03:30", + "Timestamp": "2021-10-28T07:15:00+03:30", "Open": 3986.01, "High": 3997.99, "Low": 3985.19, @@ -2572,7 +2572,7 @@ { "TimeOfCandle": "2021-10-28T07:20:00+03:30", "Amount": 4270.3175749069, - "Date": "2021-10-28T07:20:00+03:30", + "Timestamp": "2021-10-28T07:20:00+03:30", "Open": 3996.34, "High": 3996.93, "Low": 3990.00, @@ -2582,7 +2582,7 @@ { "TimeOfCandle": "2021-10-28T07:25:00+03:30", "Amount": 29999.1433082533, - "Date": "2021-10-28T07:25:00+03:30", + "Timestamp": "2021-10-28T07:25:00+03:30", "Open": 3997.99, "High": 4001.73, "Low": 3993.00, @@ -2592,7 +2592,7 @@ { "TimeOfCandle": "2021-10-28T07:30:00+03:30", "Amount": 71830.2989172508, - "Date": "2021-10-28T07:30:00+03:30", + "Timestamp": "2021-10-28T07:30:00+03:30", "Open": 3995.20, "High": 4011.00, "Low": 3995.20, @@ -2602,7 +2602,7 @@ { "TimeOfCandle": "2021-10-28T07:35:00+03:30", "Amount": 44696.4380463335, - "Date": "2021-10-28T07:35:00+03:30", + "Timestamp": "2021-10-28T07:35:00+03:30", "Open": 4002.64, "High": 4002.71, "Low": 3998.57, @@ -2612,7 +2612,7 @@ { "TimeOfCandle": "2021-10-28T07:40:00+03:30", "Amount": 90436.5260374202, - "Date": "2021-10-28T07:40:00+03:30", + "Timestamp": "2021-10-28T07:40:00+03:30", "Open": 3999.99, "High": 4019.00, "Low": 3999.99, @@ -2622,7 +2622,7 @@ { "TimeOfCandle": "2021-10-28T07:45:00+03:30", "Amount": 40713.0396065300, - "Date": "2021-10-28T07:45:00+03:30", + "Timestamp": "2021-10-28T07:45:00+03:30", "Open": 4014.97, "High": 4016.44, "Low": 4001.64, @@ -2632,7 +2632,7 @@ { "TimeOfCandle": "2021-10-28T07:50:00+03:30", "Amount": 36715.0971062257, - "Date": "2021-10-28T07:50:00+03:30", + "Timestamp": "2021-10-28T07:50:00+03:30", "Open": 4005.00, "High": 4005.00, "Low": 3996.76, @@ -2642,7 +2642,7 @@ { "TimeOfCandle": "2021-10-28T07:55:00+03:30", "Amount": 23058.1884856409, - "Date": "2021-10-28T07:55:00+03:30", + "Timestamp": "2021-10-28T07:55:00+03:30", "Open": 4000.10, "High": 4003.99, "Low": 3997.01, @@ -2652,7 +2652,7 @@ { "TimeOfCandle": "2021-10-28T08:00:00+03:30", "Amount": 22167.5891585791, - "Date": "2021-10-28T08:00:00+03:30", + "Timestamp": "2021-10-28T08:00:00+03:30", "Open": 3997.28, "High": 3999.95, "Low": 3991.41, @@ -2662,7 +2662,7 @@ { "TimeOfCandle": "2021-10-28T08:05:00+03:30", "Amount": 40659.2361277657, - "Date": "2021-10-28T08:05:00+03:30", + "Timestamp": "2021-10-28T08:05:00+03:30", "Open": 3991.94, "High": 3996.32, "Low": 3988.69, @@ -2672,7 +2672,7 @@ { "TimeOfCandle": "2021-10-28T08:10:00+03:30", "Amount": 62058.8328728053, - "Date": "2021-10-28T08:10:00+03:30", + "Timestamp": "2021-10-28T08:10:00+03:30", "Open": 3993.77, "High": 4008.00, "Low": 3993.36, @@ -2682,7 +2682,7 @@ { "TimeOfCandle": "2021-10-28T08:15:00+03:30", "Amount": 11128.7966533162, - "Date": "2021-10-28T08:15:00+03:30", + "Timestamp": "2021-10-28T08:15:00+03:30", "Open": 4006.00, "High": 4007.00, "Low": 3991.01, @@ -2692,7 +2692,7 @@ { "TimeOfCandle": "2021-10-28T08:20:00+03:30", "Amount": 5858.2039898135, - "Date": "2021-10-28T08:20:00+03:30", + "Timestamp": "2021-10-28T08:20:00+03:30", "Open": 3994.68, "High": 3997.00, "Low": 3986.52, @@ -2702,7 +2702,7 @@ { "TimeOfCandle": "2021-10-28T08:25:00+03:30", "Amount": 77289.1383465509, - "Date": "2021-10-28T08:25:00+03:30", + "Timestamp": "2021-10-28T08:25:00+03:30", "Open": 3986.47, "High": 4000.00, "Low": 3986.06, @@ -2712,7 +2712,7 @@ { "TimeOfCandle": "2021-10-28T08:30:00+03:30", "Amount": 42635.1897146719, - "Date": "2021-10-28T08:30:00+03:30", + "Timestamp": "2021-10-28T08:30:00+03:30", "Open": 3994.61, "High": 4006.00, "Low": 3994.61, @@ -2722,7 +2722,7 @@ { "TimeOfCandle": "2021-10-28T08:35:00+03:30", "Amount": 14503.8375733987, - "Date": "2021-10-28T08:35:00+03:30", + "Timestamp": "2021-10-28T08:35:00+03:30", "Open": 4005.99, "High": 4014.36, "Low": 4005.03, @@ -2732,7 +2732,7 @@ { "TimeOfCandle": "2021-10-28T08:40:00+03:30", "Amount": 33348.6125536275, - "Date": "2021-10-28T08:40:00+03:30", + "Timestamp": "2021-10-28T08:40:00+03:30", "Open": 4010.44, "High": 4013.00, "Low": 4006.97, @@ -2742,7 +2742,7 @@ { "TimeOfCandle": "2021-10-28T08:45:00+03:30", "Amount": 33285.4127162129, - "Date": "2021-10-28T08:45:00+03:30", + "Timestamp": "2021-10-28T08:45:00+03:30", "Open": 4010.00, "High": 4017.50, "Low": 4003.36, @@ -2752,7 +2752,7 @@ { "TimeOfCandle": "2021-10-28T08:50:00+03:30", "Amount": 29632.1558832761, - "Date": "2021-10-28T08:50:00+03:30", + "Timestamp": "2021-10-28T08:50:00+03:30", "Open": 4005.03, "High": 4006.95, "Low": 3997.51, @@ -2762,7 +2762,7 @@ { "TimeOfCandle": "2021-10-28T08:55:00+03:30", "Amount": 20273.8551947189, - "Date": "2021-10-28T08:55:00+03:30", + "Timestamp": "2021-10-28T08:55:00+03:30", "Open": 4000.28, "High": 4007.00, "Low": 3998.00, @@ -2772,7 +2772,7 @@ { "TimeOfCandle": "2021-10-28T09:00:00+03:30", "Amount": 50376.0214964975, - "Date": "2021-10-28T09:00:00+03:30", + "Timestamp": "2021-10-28T09:00:00+03:30", "Open": 4009.10, "High": 4009.10, "Low": 3991.00, @@ -2782,7 +2782,7 @@ { "TimeOfCandle": "2021-10-28T09:05:00+03:30", "Amount": 30198.8728909118, - "Date": "2021-10-28T09:05:00+03:30", + "Timestamp": "2021-10-28T09:05:00+03:30", "Open": 4003.01, "High": 4013.90, "Low": 3998.00, @@ -2792,7 +2792,7 @@ { "TimeOfCandle": "2021-10-28T09:10:00+03:30", "Amount": 43756.7826465745, - "Date": "2021-10-28T09:10:00+03:30", + "Timestamp": "2021-10-28T09:10:00+03:30", "Open": 4014.01, "High": 4017.00, "Low": 4011.22, @@ -2802,7 +2802,7 @@ { "TimeOfCandle": "2021-10-28T09:15:00+03:30", "Amount": 23511.2437216130, - "Date": "2021-10-28T09:15:00+03:30", + "Timestamp": "2021-10-28T09:15:00+03:30", "Open": 4014.23, "High": 4017.39, "Low": 4007.53, @@ -2812,7 +2812,7 @@ { "TimeOfCandle": "2021-10-28T09:20:00+03:30", "Amount": 14040.4748413780, - "Date": "2021-10-28T09:20:00+03:30", + "Timestamp": "2021-10-28T09:20:00+03:30", "Open": 4007.29, "High": 4013.00, "Low": 4007.29, @@ -2822,7 +2822,7 @@ { "TimeOfCandle": "2021-10-28T09:25:00+03:30", "Amount": 32784.2049970370, - "Date": "2021-10-28T09:25:00+03:30", + "Timestamp": "2021-10-28T09:25:00+03:30", "Open": 4010.10, "High": 4021.00, "Low": 4007.78, @@ -2832,7 +2832,7 @@ { "TimeOfCandle": "2021-10-28T09:30:00+03:30", "Amount": 135151.0443850006, - "Date": "2021-10-28T09:30:00+03:30", + "Timestamp": "2021-10-28T09:30:00+03:30", "Open": 4021.25, "High": 4030.00, "Low": 3998.68, @@ -2842,7 +2842,7 @@ { "TimeOfCandle": "2021-10-28T09:35:00+03:30", "Amount": 353945.0049915913, - "Date": "2021-10-28T09:35:00+03:30", + "Timestamp": "2021-10-28T09:35:00+03:30", "Open": 4002.00, "High": 4007.05, "Low": 3993.00, @@ -2852,7 +2852,7 @@ { "TimeOfCandle": "2021-10-28T09:40:00+03:30", "Amount": 143843.8618766398, - "Date": "2021-10-28T09:40:00+03:30", + "Timestamp": "2021-10-28T09:40:00+03:30", "Open": 3995.00, "High": 4004.00, "Low": 3982.80, @@ -2862,7 +2862,7 @@ { "TimeOfCandle": "2021-10-28T09:45:00+03:30", "Amount": 178253.2209346618, - "Date": "2021-10-28T09:45:00+03:30", + "Timestamp": "2021-10-28T09:45:00+03:30", "Open": 4004.55, "High": 4007.00, "Low": 3987.46, @@ -2872,7 +2872,7 @@ { "TimeOfCandle": "2021-10-28T09:50:00+03:30", "Amount": 242622.9380993191, - "Date": "2021-10-28T09:50:00+03:30", + "Timestamp": "2021-10-28T09:50:00+03:30", "Open": 4003.04, "High": 4017.53, "Low": 3996.69, @@ -2882,7 +2882,7 @@ { "TimeOfCandle": "2021-10-28T09:55:00+03:30", "Amount": 78878.8866204291, - "Date": "2021-10-28T09:55:00+03:30", + "Timestamp": "2021-10-28T09:55:00+03:30", "Open": 4016.42, "High": 4027.00, "Low": 4011.62, @@ -2892,7 +2892,7 @@ { "TimeOfCandle": "2021-10-28T10:00:00+03:30", "Amount": 371175.5092334881, - "Date": "2021-10-28T10:00:00+03:30", + "Timestamp": "2021-10-28T10:00:00+03:30", "Open": 4020.76, "High": 4021.41, "Low": 4011.14, @@ -2902,7 +2902,7 @@ { "TimeOfCandle": "2021-10-28T10:05:00+03:30", "Amount": 88168.4615095025, - "Date": "2021-10-28T10:05:00+03:30", + "Timestamp": "2021-10-28T10:05:00+03:30", "Open": 4022.99, "High": 4024.02, "Low": 4013.36, @@ -2912,7 +2912,7 @@ { "TimeOfCandle": "2021-10-28T10:10:00+03:30", "Amount": 94346.7704570256, - "Date": "2021-10-28T10:10:00+03:30", + "Timestamp": "2021-10-28T10:10:00+03:30", "Open": 4025.00, "High": 4028.70, "Low": 4015.01, @@ -2922,7 +2922,7 @@ { "TimeOfCandle": "2021-10-28T10:15:00+03:30", "Amount": 42233.7585981885, - "Date": "2021-10-28T10:15:00+03:30", + "Timestamp": "2021-10-28T10:15:00+03:30", "Open": 4018.07, "High": 4028.98, "Low": 4016.96, @@ -2932,7 +2932,7 @@ { "TimeOfCandle": "2021-10-28T10:20:00+03:30", "Amount": 74681.1523884324, - "Date": "2021-10-28T10:20:00+03:30", + "Timestamp": "2021-10-28T10:20:00+03:30", "Open": 4028.43, "High": 4036.30, "Low": 4023.40, @@ -2942,7 +2942,7 @@ { "TimeOfCandle": "2021-10-28T10:25:00+03:30", "Amount": 110575.7778364137, - "Date": "2021-10-28T10:25:00+03:30", + "Timestamp": "2021-10-28T10:25:00+03:30", "Open": 4029.00, "High": 4029.31, "Low": 4018.78, @@ -2952,7 +2952,7 @@ { "TimeOfCandle": "2021-10-28T10:30:00+03:30", "Amount": 193815.0065813983, - "Date": "2021-10-28T10:30:00+03:30", + "Timestamp": "2021-10-28T10:30:00+03:30", "Open": 4023.52, "High": 4041.00, "Low": 4023.52, @@ -2962,7 +2962,7 @@ { "TimeOfCandle": "2021-10-28T10:35:00+03:30", "Amount": 35844.0412768128, - "Date": "2021-10-28T10:35:00+03:30", + "Timestamp": "2021-10-28T10:35:00+03:30", "Open": 4024.98, "High": 4029.11, "Low": 4022.38, @@ -2972,7 +2972,7 @@ { "TimeOfCandle": "2021-10-28T10:40:00+03:30", "Amount": 165107.3654087503, - "Date": "2021-10-28T10:40:00+03:30", + "Timestamp": "2021-10-28T10:40:00+03:30", "Open": 4025.26, "High": 4025.26, "Low": 4015.00, @@ -2982,7 +2982,7 @@ { "TimeOfCandle": "2021-10-28T10:45:00+03:30", "Amount": 37197.2751947274, - "Date": "2021-10-28T10:45:00+03:30", + "Timestamp": "2021-10-28T10:45:00+03:30", "Open": 4016.73, "High": 4019.68, "Low": 4010.00, @@ -2992,7 +2992,7 @@ { "TimeOfCandle": "2021-10-28T10:50:00+03:30", "Amount": 11167.1724041085, - "Date": "2021-10-28T10:50:00+03:30", + "Timestamp": "2021-10-28T10:50:00+03:30", "Open": 4013.09, "High": 4016.05, "Low": 4010.00, diff --git a/tests/indicators/s-z/ZigZag/data.issue632.json b/tests/indicators/s-z/ZigZag/data.issue632.json index 39f8994d3..18eba9c48 100644 --- a/tests/indicators/s-z/ZigZag/data.issue632.json +++ b/tests/indicators/s-z/ZigZag/data.issue632.json @@ -1,6 +1,6 @@ [ { - "Date": "2021-10-20T00:00:00", + "Timestamp": "2021-10-20T00:00:00", "Open": 23.59800, "High": 24.42100, "Low": 23.57500, @@ -8,7 +8,7 @@ "Volume": 91.0 }, { - "Date": "2021-10-21T00:00:00", + "Timestamp": "2021-10-21T00:00:00", "Open": 24.29400, "High": 24.48700, "Low": 24.00800, @@ -16,7 +16,7 @@ "Volume": 89.0 }, { - "Date": "2021-10-22T00:00:00", + "Timestamp": "2021-10-22T00:00:00", "Open": 24.15200, "High": 24.82600, "Low": 24.12300, @@ -24,7 +24,7 @@ "Volume": 84.0 }, { - "Date": "2021-10-25T00:00:00", + "Timestamp": "2021-10-25T00:00:00", "Open": 24.39800, "High": 24.61800, "Low": 24.30600, @@ -32,7 +32,7 @@ "Volume": 92.0 }, { - "Date": "2021-10-26T00:00:00", + "Timestamp": "2021-10-26T00:00:00", "Open": 24.55600, "High": 24.55800, "Low": 23.87900, @@ -40,7 +40,7 @@ "Volume": 92.0 }, { - "Date": "2021-10-27T00:00:00", + "Timestamp": "2021-10-27T00:00:00", "Open": 24.14500, "High": 24.25400, "Low": 23.83700, @@ -48,7 +48,7 @@ "Volume": 92.0 }, { - "Date": "2021-10-28T00:00:00", + "Timestamp": "2021-10-28T00:00:00", "Open": 24.03800, "High": 24.24600, "Low": 23.96300, @@ -56,7 +56,7 @@ "Volume": 92.0 }, { - "Date": "2021-10-29T00:00:00", + "Timestamp": "2021-10-29T00:00:00", "Open": 24.06600, "High": 24.08100, "Low": 23.66100, @@ -64,7 +64,7 @@ "Volume": 84.0 }, { - "Date": "2021-11-01T00:00:00", + "Timestamp": "2021-11-01T00:00:00", "Open": 23.77900, "High": 24.10200, "Low": 23.74900, @@ -72,7 +72,7 @@ "Volume": 92.0 }, { - "Date": "2021-11-02T00:00:00", + "Timestamp": "2021-11-02T00:00:00", "Open": 24.02300, "High": 24.06700, "Low": 23.39200, @@ -80,7 +80,7 @@ "Volume": 92.0 }, { - "Date": "2021-11-03T00:00:00", + "Timestamp": "2021-11-03T00:00:00", "Open": 23.52700, "High": 23.72400, "Low": 23.02000, @@ -88,7 +88,7 @@ "Volume": 92.0 }, { - "Date": "2021-11-04T00:00:00", + "Timestamp": "2021-11-04T00:00:00", "Open": 23.72100, "High": 24.04100, "Low": 23.44300, @@ -96,7 +96,7 @@ "Volume": 92.0 }, { - "Date": "2021-11-05T00:00:00", + "Timestamp": "2021-11-05T00:00:00", "Open": 23.77300, "High": 24.17800, "Low": 23.62700, @@ -104,7 +104,7 @@ "Volume": 84.0 }, { - "Date": "2021-11-08T00:00:00", + "Timestamp": "2021-11-08T00:00:00", "Open": 24.13100, "High": 24.51300, "Low": 24.05800, @@ -112,7 +112,7 @@ "Volume": 92.0 }, { - "Date": "2021-11-09T00:00:00", + "Timestamp": "2021-11-09T00:00:00", "Open": 24.44700, "High": 24.47700, "Low": 24.02900, @@ -120,7 +120,7 @@ "Volume": 92.0 }, { - "Date": "2021-11-10T00:00:00", + "Timestamp": "2021-11-10T00:00:00", "Open": 24.30800, "High": 25.13000, "Low": 24.06500, @@ -128,7 +128,7 @@ "Volume": 92.0 }, { - "Date": "2021-11-11T00:00:00", + "Timestamp": "2021-11-11T00:00:00", "Open": 24.64500, "High": 25.01200, "Low": 24.59000, diff --git a/tests/indicators/s-z/ZigZag/data.schrodinger.json b/tests/indicators/s-z/ZigZag/data.schrodinger.json index 3bbad1348..655aee761 100644 --- a/tests/indicators/s-z/ZigZag/data.schrodinger.json +++ b/tests/indicators/s-z/ZigZag/data.schrodinger.json @@ -1,6 +1,6 @@ [ { - "Date": "2019-10-29 15:00", + "Timestamp": "2019-10-29 15:00", "Open": 8.424630165, "High": 10.11337376, "Low": 8.424630165, @@ -8,7 +8,7 @@ "Volume": 4059400321 }, { - "Date": "2019-10-30 15:00", + "Timestamp": "2019-10-30 15:00", "Open": 8.396008492, "High": 8.396008492, "Low": 8.033452988, @@ -16,7 +16,7 @@ "Volume": 2320616294 }, { - "Date": "2019-10-31 15:00", + "Timestamp": "2019-10-31 15:00", "Open": 7.489620686, "High": 8.081157684, "Low": 7.308342934, @@ -24,7 +24,7 @@ "Volume": 2286077392 }, { - "Date": "2019-11-01 15:00", + "Timestamp": "2019-11-01 15:00", "Open": 7.508702278, "High": 7.699520588, "Low": 7.441916466, @@ -32,7 +32,7 @@ "Volume": 1336825882 }, { - "Date": "2019-11-04 15:00", + "Timestamp": "2019-11-04 15:00", "Open": 7.451457024, "High": 7.556407452, "Low": 7.308342934, @@ -40,7 +40,7 @@ "Volume": 1266693767 }, { - "Date": "2019-11-05 15:00", + "Timestamp": "2019-11-05 15:00", "Open": 7.336966038, "High": 7.422834396, "Low": 7.165229797, @@ -48,7 +48,7 @@ "Volume": 1184345773 }, { - "Date": "2019-11-06 15:00", + "Timestamp": "2019-11-06 15:00", "Open": 7.279720783, "High": 7.279720783, "Low": 7.107983589, @@ -56,7 +56,7 @@ "Volume": 934589619 }, { - "Date": "2019-11-07 15:00", + "Timestamp": "2019-11-07 15:00", "Open": 7.117525101, "High": 7.251097679, "Low": 7.117525101, @@ -64,7 +64,7 @@ "Volume": 634838541 }, { - "Date": "2019-11-08 15:00", + "Timestamp": "2019-11-08 15:00", "Open": 7.212934017, "High": 7.212934017, "Low": 7.088901997, @@ -72,7 +72,7 @@ "Volume": 573972846 }, { - "Date": "2019-11-11 15:00", + "Timestamp": "2019-11-11 15:00", "Open": 7.060279369, "High": 7.060279369, "Low": 6.85037899, @@ -80,7 +80,7 @@ "Volume": 700986893 }, { - "Date": "2019-11-12 15:00", + "Timestamp": "2019-11-12 15:00", "Open": 6.85037899, "High": 6.888542652, "Low": 6.697724342, @@ -88,7 +88,7 @@ "Volume": 557485894 }, { - "Date": "2019-11-13 15:00", + "Timestamp": "2019-11-13 15:00", "Open": 6.745429039, "High": 6.754970074, "Low": 6.611855984, @@ -96,7 +96,7 @@ "Volume": 556249904 }, { - "Date": "2019-11-14 15:00", + "Timestamp": "2019-11-14 15:00", "Open": 6.583233356, "High": 6.936247349, "Low": 6.516447067, @@ -104,7 +104,7 @@ "Volume": 933603542 }, { - "Date": "2019-11-15 15:00", + "Timestamp": "2019-11-15 15:00", "Open": 6.726347446, "High": 6.812215328, "Low": 6.611855984, @@ -112,7 +112,7 @@ "Volume": 529075401 }, { - "Date": "2019-11-18 15:00", + "Timestamp": "2019-11-18 15:00", "Open": 6.602315426, "High": 6.697724342, "Low": 6.564151764, @@ -120,7 +120,7 @@ "Volume": 363056995 }, { - "Date": "2019-11-19 15:00", + "Timestamp": "2019-11-19 15:00", "Open": 6.621397018, "High": 6.735888004, "Low": 6.602315426, @@ -128,7 +128,7 @@ "Volume": 425813619 }, { - "Date": "2019-11-20 15:00", + "Timestamp": "2019-11-20 15:00", "Open": 6.697724342, "High": 6.716806412, "Low": 6.611855984, @@ -136,7 +136,7 @@ "Volume": 371981297 }, { - "Date": "2019-11-21 15:00", + "Timestamp": "2019-11-21 15:00", "Open": 6.602315426, "High": 6.640479088, "Low": 6.583233356, @@ -144,7 +144,7 @@ "Volume": 265244733 }, { - "Date": "2019-11-22 15:00", + "Timestamp": "2019-11-22 15:00", "Open": 6.611855984, "High": 6.650019646, "Low": 6.554610729, @@ -152,7 +152,7 @@ "Volume": 305350578 }, { - "Date": "2019-11-25 15:00", + "Timestamp": "2019-11-25 15:00", "Open": 6.583233356, "High": 6.697724342, "Low": 6.564151764, @@ -160,7 +160,7 @@ "Volume": 406964233 }, { - "Date": "2019-11-26 15:00", + "Timestamp": "2019-11-26 15:00", "Open": 6.621397018, "High": 6.697724342, "Low": 6.611855984, @@ -168,7 +168,7 @@ "Volume": 407453677 }, { - "Date": "2019-11-27 15:00", + "Timestamp": "2019-11-27 15:00", "Open": 6.592774391, "High": 6.592774391, "Low": 6.449660778, @@ -176,7 +176,7 @@ "Volume": 535419253 }, { - "Date": "2019-11-28 15:00", + "Timestamp": "2019-11-28 15:00", "Open": 6.449660778, "High": 6.478283405, "Low": 6.363792419, @@ -184,7 +184,7 @@ "Volume": 364736312 }, { - "Date": "2019-11-29 15:00", + "Timestamp": "2019-11-29 15:00", "Open": 6.363792419, "High": 6.411496639, "Low": 6.325628757, @@ -192,7 +192,7 @@ "Volume": 255866984 }, { - "Date": "2019-12-02 15:00", + "Timestamp": "2019-12-02 15:00", "Open": 6.344710827, "High": 6.363792419, "Low": 6.297005653, @@ -200,7 +200,7 @@ "Volume": 258548232 }, { - "Date": "2019-12-03 15:00", + "Timestamp": "2019-12-03 15:00", "Open": 6.287465096, "High": 6.287465096, "Low": 6.230219841, @@ -208,7 +208,7 @@ "Volume": 230697680 }, { - "Date": "2019-12-04 15:00", + "Timestamp": "2019-12-04 15:00", "Open": 6.249301434, "High": 6.316087723, "Low": 6.230219841, @@ -216,7 +216,7 @@ "Volume": 181464531 }, { - "Date": "2019-12-05 15:00", + "Timestamp": "2019-12-05 15:00", "Open": 6.277924061, "High": 6.363792419, "Low": 6.258841991, @@ -224,7 +224,7 @@ "Volume": 296670173 }, { - "Date": "2019-12-06 15:00", + "Timestamp": "2019-12-06 15:00", "Open": 6.316087723, "High": 6.325628757, "Low": 6.277924061, @@ -232,7 +232,7 @@ "Volume": 217770930 }, { - "Date": "2019-12-09 15:00", + "Timestamp": "2019-12-09 15:00", "Open": 6.316087723, "High": 6.344710827, "Low": 6.297005653, @@ -240,7 +240,7 @@ "Volume": 241221190 }, { - "Date": "2019-12-10 15:00", + "Timestamp": "2019-12-10 15:00", "Open": 6.306547165, "High": 6.316087723, "Low": 6.258841991, @@ -248,7 +248,7 @@ "Volume": 305170005 }, { - "Date": "2019-12-11 15:00", + "Timestamp": "2019-12-11 15:00", "Open": 6.258841991, "High": 6.306547165, "Low": 6.249301434, @@ -256,7 +256,7 @@ "Volume": 237129090 }, { - "Date": "2019-12-12 15:00", + "Timestamp": "2019-12-12 15:00", "Open": 6.268383503, "High": 6.268383503, "Low": 6.182515144, @@ -264,7 +264,7 @@ "Volume": 328011417 }, { - "Date": "2019-12-13 15:00", + "Timestamp": "2019-12-13 15:00", "Open": 6.211137772, "High": 6.277924061, "Low": 6.192055702, @@ -272,7 +272,7 @@ "Volume": 414155025 }, { - "Date": "2019-12-16 15:00", + "Timestamp": "2019-12-16 15:00", "Open": 6.258841991, "High": 6.268383503, "Low": 6.220678806, @@ -280,7 +280,7 @@ "Volume": 319083760 }, { - "Date": "2019-12-17 15:00", + "Timestamp": "2019-12-17 15:00", "Open": 6.277924061, "High": 6.745429039, "Low": 6.258841991, @@ -288,7 +288,7 @@ "Volume": 1437600223 }, { - "Date": "2019-12-18 15:00", + "Timestamp": "2019-12-18 15:00", "Open": 6.53552866, "High": 6.611855984, "Low": 6.468742847, @@ -296,7 +296,7 @@ "Volume": 791161760 }, { - "Date": "2019-12-19 15:00", + "Timestamp": "2019-12-19 15:00", "Open": 6.506906509, "High": 6.65956068, "Low": 6.497364998, @@ -304,7 +304,7 @@ "Volume": 579540342 }, { - "Date": "2019-12-20 15:00", + "Timestamp": "2019-12-20 15:00", "Open": 6.53552866, "High": 6.707265854, "Low": 6.516447067, @@ -312,7 +312,7 @@ "Volume": 743212796 }, { - "Date": "2019-12-23 15:00", + "Timestamp": "2019-12-23 15:00", "Open": 6.573692322, "High": 6.602315426, "Low": 6.401956081, @@ -320,7 +320,7 @@ "Volume": 495364395 }, { - "Date": "2019-12-24 15:00", + "Timestamp": "2019-12-24 15:00", "Open": 6.449660778, "High": 6.468742847, "Low": 6.373332977, @@ -328,7 +328,7 @@ "Volume": 270327101 }, { - "Date": "2019-12-25 15:00", + "Timestamp": "2019-12-25 15:00", "Open": 6.421037674, "High": 6.430578709, "Low": 6.373332977, @@ -336,7 +336,7 @@ "Volume": 257601111 }, { - "Date": "2019-12-26 15:00", + "Timestamp": "2019-12-26 15:00", "Open": 6.392415047, "High": 6.421037674, "Low": 6.363792419, @@ -344,7 +344,7 @@ "Volume": 255462864 }, { - "Date": "2019-12-27 15:00", + "Timestamp": "2019-12-27 15:00", "Open": 6.411496639, "High": 6.525988102, "Low": 6.392415047, @@ -352,7 +352,7 @@ "Volume": 415148112 }, { - "Date": "2019-12-30 15:00", + "Timestamp": "2019-12-30 15:00", "Open": 6.354251385, "High": 6.401956081, "Low": 6.258841991, @@ -360,7 +360,7 @@ "Volume": 389765020 }, { - "Date": "2019-12-31 15:00", + "Timestamp": "2019-12-31 15:00", "Open": 6.382874489, "High": 6.440119743, "Low": 6.354251385, @@ -368,7 +368,7 @@ "Volume": 228308972 }, { - "Date": "2020-01-02 15:00", + "Timestamp": "2020-01-02 15:00", "Open": 6.497364998, "High": 6.545070171, "Low": 6.449660778, @@ -376,7 +376,7 @@ "Volume": 519608791 }, { - "Date": "2020-01-03 15:00", + "Timestamp": "2020-01-03 15:00", "Open": 6.478283405, "High": 6.478283405, "Low": 6.430578709, @@ -384,7 +384,7 @@ "Volume": 297443419 }, { - "Date": "2020-01-06 15:00", + "Timestamp": "2020-01-06 15:00", "Open": 6.440119743, "High": 6.48782444, "Low": 6.401956081, @@ -392,7 +392,7 @@ "Volume": 326749047 }, { - "Date": "2020-01-07 15:00", + "Timestamp": "2020-01-07 15:00", "Open": 6.449660778, "High": 6.48782444, "Low": 6.430578709, @@ -400,7 +400,7 @@ "Volume": 341134545 }, { - "Date": "2020-01-08 15:00", + "Timestamp": "2020-01-08 15:00", "Open": 6.468742847, "High": 6.468742847, "Low": 6.373332977, @@ -408,7 +408,7 @@ "Volume": 364122847 }, { - "Date": "2020-01-09 15:00", + "Timestamp": "2020-01-09 15:00", "Open": 6.411496639, "High": 6.440119743, "Low": 6.401956081, @@ -416,7 +416,7 @@ "Volume": 215216477 }, { - "Date": "2020-01-10 15:00", + "Timestamp": "2020-01-10 15:00", "Open": 6.430578709, "High": 6.440119743, "Low": 6.382874489, @@ -424,7 +424,7 @@ "Volume": 168831418 }, { - "Date": "2020-01-13 15:00", + "Timestamp": "2020-01-13 15:00", "Open": 6.392415047, "High": 6.401956081, "Low": 6.354251385, @@ -432,7 +432,7 @@ "Volume": 179832623 }, { - "Date": "2020-01-14 15:00", + "Timestamp": "2020-01-14 15:00", "Open": 6.401956081, "High": 6.411496639, "Low": 6.382874489, @@ -440,7 +440,7 @@ "Volume": 160677776 }, { - "Date": "2020-01-15 15:00", + "Timestamp": "2020-01-15 15:00", "Open": 6.382874489, "High": 6.382874489, "Low": 6.335169315, @@ -448,7 +448,7 @@ "Volume": 172642834 }, { - "Date": "2020-01-16 15:00", + "Timestamp": "2020-01-16 15:00", "Open": 6.354251385, "High": 6.354251385, "Low": 6.297005653, @@ -456,7 +456,7 @@ "Volume": 160226870 }, { - "Date": "2020-01-17 15:00", + "Timestamp": "2020-01-17 15:00", "Open": 6.297005653, "High": 6.325628757, "Low": 6.297005653, @@ -464,7 +464,7 @@ "Volume": 118202341 }, { - "Date": "2020-01-20 15:00", + "Timestamp": "2020-01-20 15:00", "Open": 6.297005653, "High": 6.316087723, "Low": 6.277924061, @@ -472,7 +472,7 @@ "Volume": 137543797 }, { - "Date": "2020-01-21 15:00", + "Timestamp": "2020-01-21 15:00", "Open": 6.325628757, "High": 6.325628757, "Low": 6.249301434, @@ -480,7 +480,7 @@ "Volume": 176235026 }, { - "Date": "2020-01-22 15:00", + "Timestamp": "2020-01-22 15:00", "Open": 6.249301434, "High": 6.258841991, "Low": 6.211137772, @@ -488,7 +488,7 @@ "Volume": 168290099 }, { - "Date": "2020-01-23 15:00", + "Timestamp": "2020-01-23 15:00", "Open": 6.220678806, "High": 6.230219841, "Low": 6.039401054, @@ -496,7 +496,7 @@ "Volume": 285122377 }, { - "Date": "2020-02-03 15:00", + "Timestamp": "2020-02-03 15:00", "Open": 5.486027718, "High": 5.52419138, "Low": 5.486027718, @@ -504,7 +504,7 @@ "Volume": 268612320 }, { - "Date": "2020-02-04 15:00", + "Timestamp": "2020-02-04 15:00", "Open": 5.333373547, "High": 5.543273449, "Low": 5.333373547, @@ -512,7 +512,7 @@ "Volume": 274029620 }, { - "Date": "2020-02-05 15:00", + "Timestamp": "2020-02-05 15:00", "Open": 5.438323021, "High": 5.552814484, "Low": 5.409700871, @@ -520,7 +520,7 @@ "Volume": 287029249 }, { - "Date": "2020-02-06 15:00", + "Timestamp": "2020-02-06 15:00", "Open": 5.486027718, "High": 5.571896553, "Low": 5.447864056, @@ -528,7 +528,7 @@ "Volume": 292072223 }, { - "Date": "2020-02-07 15:00", + "Timestamp": "2020-02-07 15:00", "Open": 5.505109787, "High": 5.505109787, "Low": 5.447864056, @@ -536,7 +536,7 @@ "Volume": 218100591 }, { - "Date": "2020-02-10 15:00", + "Timestamp": "2020-02-10 15:00", "Open": 5.466946125, "High": 5.52419138, "Low": 5.438323021, @@ -544,7 +544,7 @@ "Volume": 196809450 }, { - "Date": "2020-02-11 15:00", + "Timestamp": "2020-02-11 15:00", "Open": 5.52419138, "High": 5.6482234, "Low": 5.505109787, @@ -552,7 +552,7 @@ "Volume": 304820437 }, { - "Date": "2020-02-12 15:00", + "Timestamp": "2020-02-12 15:00", "Open": 5.552814484, "High": 5.600518703, "Low": 5.533732891, @@ -560,7 +560,7 @@ "Volume": 196059746 }, { - "Date": "2020-02-13 15:00", + "Timestamp": "2020-02-13 15:00", "Open": 5.581437111, "High": 5.600518703, "Low": 5.505109787, @@ -568,7 +568,7 @@ "Volume": 208902228 }, { - "Date": "2020-02-14 15:00", + "Timestamp": "2020-02-14 15:00", "Open": 5.486027718, "High": 5.552814484, "Low": 5.486027718, @@ -576,7 +576,7 @@ "Volume": 153275933 }, { - "Date": "2020-02-17 15:00", + "Timestamp": "2020-02-17 15:00", "Open": 5.533732891, "High": 5.705469131, "Low": 5.52419138, @@ -584,7 +584,7 @@ "Volume": 401712899 }, { - "Date": "2020-02-18 15:00", + "Timestamp": "2020-02-18 15:00", "Open": 5.657764435, "High": 5.686387062, "Low": 5.629141808, @@ -592,7 +592,7 @@ "Volume": 262387253 }, { - "Date": "2020-02-19 15:00", + "Timestamp": "2020-02-19 15:00", "Open": 5.6482234, "High": 5.695928097, "Low": 5.629141808, @@ -600,7 +600,7 @@ "Volume": 222794958 }, { - "Date": "2020-02-20 15:00", + "Timestamp": "2020-02-20 15:00", "Open": 5.66730547, "High": 5.819960117, "Low": 5.638682365, @@ -608,7 +608,7 @@ "Volume": 459717561 }, { - "Date": "2020-02-21 15:00", + "Timestamp": "2020-02-21 15:00", "Open": 5.753173828, "High": 5.819960117, "Low": 5.743632793, @@ -616,7 +616,7 @@ "Volume": 317394574 }, { - "Date": "2020-02-24 15:00", + "Timestamp": "2020-02-24 15:00", "Open": 5.753173828, "High": 5.762714386, "Low": 5.695928097, @@ -624,7 +624,7 @@ "Volume": 250149645 }, { - "Date": "2020-02-25 15:00", + "Timestamp": "2020-02-25 15:00", "Open": 5.629141808, "High": 5.629141808, "Low": 5.505109787, @@ -632,7 +632,7 @@ "Volume": 332851339 }, { - "Date": "2020-02-26 15:00", + "Timestamp": "2020-02-26 15:00", "Open": 5.533732891, "High": 5.734092236, "Low": 5.495569229, @@ -640,7 +640,7 @@ "Volume": 354230119 }, { - "Date": "2020-02-27 15:00", + "Timestamp": "2020-02-27 15:00", "Open": 5.657764435, "High": 5.715009689, "Low": 5.619600773, @@ -648,7 +648,7 @@ "Volume": 260972510 }, { - "Date": "2020-02-28 15:00", + "Timestamp": "2020-02-28 15:00", "Open": 5.600518703, "High": 5.629141808, "Low": 5.476486683, @@ -656,7 +656,7 @@ "Volume": 327142262 }, { - "Date": "2020-03-02 15:00", + "Timestamp": "2020-03-02 15:00", "Open": 5.495569229, "High": 5.600518703, "Low": 5.486027718, @@ -664,7 +664,7 @@ "Volume": 242493530 }, { - "Date": "2020-03-03 15:00", + "Timestamp": "2020-03-03 15:00", "Open": 5.619600773, "High": 5.6482234, "Low": 5.581437111, @@ -672,7 +672,7 @@ "Volume": 237301914 }, { - "Date": "2020-03-04 15:00", + "Timestamp": "2020-03-04 15:00", "Open": 5.571896553, "High": 5.610059738, "Low": 5.552814484, @@ -680,7 +680,7 @@ "Volume": 170073889 }, { - "Date": "2020-03-05 15:00", + "Timestamp": "2020-03-05 15:00", "Open": 5.638682365, "High": 5.877205372, "Low": 5.619600773, @@ -688,7 +688,7 @@ "Volume": 799500914 }, { - "Date": "2020-03-06 15:00", + "Timestamp": "2020-03-06 15:00", "Open": 5.762714386, "High": 5.79133749, "Low": 5.724550724, @@ -696,7 +696,7 @@ "Volume": 392612912 }, { - "Date": "2020-03-09 15:00", + "Timestamp": "2020-03-09 15:00", "Open": 5.657764435, "High": 5.66730547, "Low": 5.562355042, @@ -704,7 +704,7 @@ "Volume": 294122353 }, { - "Date": "2020-03-10 15:00", + "Timestamp": "2020-03-10 15:00", "Open": 5.505109787, "High": 5.610059738, "Low": 5.495569229, @@ -712,7 +712,7 @@ "Volume": 270879498 }, { - "Date": "2020-03-11 15:00", + "Timestamp": "2020-03-11 15:00", "Open": 5.600518703, "High": 5.638682365, "Low": 5.562355042, @@ -720,7 +720,7 @@ "Volume": 181957781 }, { - "Date": "2020-03-12 15:00", + "Timestamp": "2020-03-12 15:00", "Open": 5.552814484, "High": 5.629141808, "Low": 5.514650822, @@ -728,7 +728,7 @@ "Volume": 207296655 }, { - "Date": "2020-03-13 15:00", + "Timestamp": "2020-03-13 15:00", "Open": 5.361995697, "High": 5.543273449, "Low": 5.304750443, @@ -736,7 +736,7 @@ "Volume": 263231569 }, { - "Date": "2020-03-16 15:00", + "Timestamp": "2020-03-16 15:00", "Open": 5.543273449, "High": 5.543273449, "Low": 5.390618801, @@ -744,7 +744,7 @@ "Volume": 218431971 }, { - "Date": "2020-03-17 15:00", + "Timestamp": "2020-03-17 15:00", "Open": 5.400159359, "High": 5.438323021, "Low": 5.285668373, @@ -752,7 +752,7 @@ "Volume": 169382232 }, { - "Date": "2020-03-18 15:00", + "Timestamp": "2020-03-18 15:00", "Open": 5.371537209, "High": 5.390618801, "Low": 5.247505188, @@ -760,7 +760,7 @@ "Volume": 152820214 }, { - "Date": "2020-03-19 15:00", + "Timestamp": "2020-03-19 15:00", "Open": 5.247505188, "High": 5.257046223, "Low": 5.104391098, @@ -768,7 +768,7 @@ "Volume": 181264768 }, { - "Date": "2020-03-20 15:00", + "Timestamp": "2020-03-20 15:00", "Open": 5.190259457, "High": 5.209341526, "Low": 5.152095795, @@ -776,7 +776,7 @@ "Volume": 114235724 }, { - "Date": "2020-03-23 15:00", + "Timestamp": "2020-03-23 15:00", "Open": 5.113932133, "High": 5.113932133, "Low": 5.008982182, @@ -784,7 +784,7 @@ "Volume": 127052162 }, { - "Date": "2020-03-24 15:00", + "Timestamp": "2020-03-24 15:00", "Open": 5.075768471, "High": 5.104391098, "Low": 5.028063774, @@ -792,7 +792,7 @@ "Volume": 88650349 }, { - "Date": "2020-03-25 15:00", + "Timestamp": "2020-03-25 15:00", "Open": 5.152095795, "High": 5.190259457, "Low": 5.123472691, @@ -800,7 +800,7 @@ "Volume": 147656317 }, { - "Date": "2020-03-26 15:00", + "Timestamp": "2020-03-26 15:00", "Open": 5.152095795, "High": 5.218882084, "Low": 5.133014202, @@ -808,7 +808,7 @@ "Volume": 128997735 }, { - "Date": "2020-03-27 15:00", + "Timestamp": "2020-03-27 15:00", "Open": 5.190259457, "High": 5.218882084, "Low": 5.14255476, @@ -816,7 +816,7 @@ "Volume": 121873505 }, { - "Date": "2020-03-30 15:00", + "Timestamp": "2020-03-30 15:00", "Open": 5.085309505, "High": 5.085309505, "Low": 5.028063774, @@ -824,7 +824,7 @@ "Volume": 106167125 }, { - "Date": "2020-03-31 15:00", + "Timestamp": "2020-03-31 15:00", "Open": 5.085309505, "High": 5.09485054, "Low": 5.037604809, @@ -832,7 +832,7 @@ "Volume": 66378042 }, { - "Date": "2020-04-01 15:00", + "Timestamp": "2020-04-01 15:00", "Open": 5.056686878, "High": 5.104391098, "Low": 5.037604809, @@ -840,7 +840,7 @@ "Volume": 79057850 }, { - "Date": "2020-04-02 15:00", + "Timestamp": "2020-04-02 15:00", "Open": 5.018523216, "High": 5.085309505, "Low": 5.008982182, @@ -848,7 +848,7 @@ "Volume": 71676279 }, { - "Date": "2020-04-03 15:00", + "Timestamp": "2020-04-03 15:00", "Open": 5.075768471, "High": 5.09485054, "Low": 5.047145844, @@ -856,7 +856,7 @@ "Volume": 65745365 }, { - "Date": "2020-04-07 15:00", + "Timestamp": "2020-04-07 15:00", "Open": 5.133014202, "High": 5.14255476, "Low": 5.09485054, @@ -864,7 +864,7 @@ "Volume": 98229462 }, { - "Date": "2020-04-08 15:00", + "Timestamp": "2020-04-08 15:00", "Open": 5.085309505, "High": 5.113932133, "Low": 5.085309505, @@ -872,7 +872,7 @@ "Volume": 53963630 }, { - "Date": "2020-04-09 15:00", + "Timestamp": "2020-04-09 15:00", "Open": 5.123472691, "High": 5.123472691, "Low": 5.104391098, @@ -880,7 +880,7 @@ "Volume": 57992533 }, { - "Date": "2020-04-10 15:00", + "Timestamp": "2020-04-10 15:00", "Open": 5.113932133, "High": 5.113932133, "Low": 5.047145844, @@ -888,7 +888,7 @@ "Volume": 83259057 }, { - "Date": "2020-04-13 15:00", + "Timestamp": "2020-04-13 15:00", "Open": 5.037604809, "High": 5.047145844, "Low": 5.018523216, @@ -896,7 +896,7 @@ "Volume": 54171844 }, { - "Date": "2020-04-14 15:00", + "Timestamp": "2020-04-14 15:00", "Open": 5.018523216, "High": 5.056686878, "Low": 5.018523216, @@ -904,7 +904,7 @@ "Volume": 48392879 }, { - "Date": "2020-04-15 15:00", + "Timestamp": "2020-04-15 15:00", "Open": 5.047145844, "High": 5.066227436, "Low": 5.028063774, @@ -912,7 +912,7 @@ "Volume": 53872919 }, { - "Date": "2020-04-16 15:00", + "Timestamp": "2020-04-16 15:00", "Open": 5.018523216, "High": 5.047145844, "Low": 5.008982182, @@ -920,7 +920,7 @@ "Volume": 56733163 }, { - "Date": "2020-04-17 15:00", + "Timestamp": "2020-04-17 15:00", "Open": 5.047145844, "High": 5.056686878, "Low": 5.028063774, @@ -928,7 +928,7 @@ "Volume": 59468552 }, { - "Date": "2020-04-20 15:00", + "Timestamp": "2020-04-20 15:00", "Open": 5.037604809, "High": 5.047145844, "Low": 5.018523216, @@ -936,7 +936,7 @@ "Volume": 41532302 }, { - "Date": "2020-04-21 15:00", + "Timestamp": "2020-04-21 15:00", "Open": 5.028063774, "High": 5.028063774, "Low": 4.980359077, @@ -944,7 +944,7 @@ "Volume": 82288866 }, { - "Date": "2020-04-22 15:00", + "Timestamp": "2020-04-22 15:00", "Open": 4.980359077, "High": 4.989900112, "Low": 4.961277008, @@ -952,7 +952,7 @@ "Volume": 54268019 }, { - "Date": "2020-04-23 15:00", + "Timestamp": "2020-04-23 15:00", "Open": 4.980359077, "High": 4.989900112, "Low": 4.97081852, @@ -960,7 +960,7 @@ "Volume": 47618500 }, { - "Date": "2020-04-24 15:00", + "Timestamp": "2020-04-24 15:00", "Open": 4.961277008, "High": 4.980359077, "Low": 4.904031754, @@ -968,7 +968,7 @@ "Volume": 80555276 }, { - "Date": "2020-04-27 15:00", + "Timestamp": "2020-04-27 15:00", "Open": 4.894491196, "High": 5.047145844, "Low": 4.894491196, @@ -976,7 +976,7 @@ "Volume": 150645009 }, { - "Date": "2020-04-28 15:00", + "Timestamp": "2020-04-28 15:00", "Open": 4.97081852, "High": 4.989900112, "Low": 4.837245464, @@ -984,7 +984,7 @@ "Volume": 101539030 }, { - "Date": "2020-04-29 15:00", + "Timestamp": "2020-04-29 15:00", "Open": 4.884950161, "High": 4.961277008, "Low": 4.875409126, @@ -992,7 +992,7 @@ "Volume": 77926459 }, { - "Date": "2020-04-30 15:00", + "Timestamp": "2020-04-30 15:00", "Open": 4.751377106, "High": 4.799081802, "Low": 4.675049782, @@ -1000,7 +1000,7 @@ "Volume": 682889816 }, { - "Date": "2020-05-06 15:00", + "Timestamp": "2020-05-06 15:00", "Open": 4.741836071, "High": 4.76091814, "Low": 4.713213444, @@ -1008,7 +1008,7 @@ "Volume": 238092495 }, { - "Date": "2020-05-07 15:00", + "Timestamp": "2020-05-07 15:00", "Open": 4.751377106, "High": 4.751377106, "Low": 4.722754478, @@ -1016,7 +1016,7 @@ "Volume": 131296840 }, { - "Date": "2020-05-08 15:00", + "Timestamp": "2020-05-08 15:00", "Open": 4.741836071, "High": 4.76091814, "Low": 4.722754478, @@ -1024,7 +1024,7 @@ "Volume": 162045680 }, { - "Date": "2020-05-11 15:00", + "Timestamp": "2020-05-11 15:00", "Open": 4.741836071, "High": 4.76091814, "Low": 4.732295513, @@ -1032,7 +1032,7 @@ "Volume": 120217831 }, { - "Date": "2020-05-12 15:00", + "Timestamp": "2020-05-12 15:00", "Open": 4.732295513, "High": 4.741836071, "Low": 4.703672409, @@ -1040,7 +1040,7 @@ "Volume": 125054914 }, { - "Date": "2020-05-13 15:00", + "Timestamp": "2020-05-13 15:00", "Open": 4.694131851, "High": 4.713213444, "Low": 4.684590816, @@ -1048,7 +1048,7 @@ "Volume": 99711909 }, { - "Date": "2020-05-14 15:00", + "Timestamp": "2020-05-14 15:00", "Open": 4.694131851, "High": 4.713213444, "Low": 4.675049782, @@ -1056,7 +1056,7 @@ "Volume": 109620513 }, { - "Date": "2020-05-15 15:00", + "Timestamp": "2020-05-15 15:00", "Open": 4.703672409, "High": 4.713213444, "Low": 4.675049782, @@ -1064,7 +1064,7 @@ "Volume": 90973657 }, { - "Date": "2020-05-18 15:00", + "Timestamp": "2020-05-18 15:00", "Open": 4.694131851, "High": 4.694131851, "Low": 4.655968189, @@ -1072,7 +1072,7 @@ "Volume": 93449775 }, { - "Date": "2020-05-19 15:00", + "Timestamp": "2020-05-19 15:00", "Open": 4.694131851, "High": 4.703672409, "Low": 4.675049782, @@ -1080,7 +1080,7 @@ "Volume": 83965436 }, { - "Date": "2020-05-20 15:00", + "Timestamp": "2020-05-20 15:00", "Open": 4.675049782, "High": 4.751377106, "Low": 4.665508747, @@ -1088,7 +1088,7 @@ "Volume": 216752922 }, { - "Date": "2020-05-21 15:00", + "Timestamp": "2020-05-21 15:00", "Open": 4.694131851, "High": 4.713213444, "Low": 4.665508747, @@ -1096,7 +1096,7 @@ "Volume": 102522007 }, { - "Date": "2020-05-22 15:00", + "Timestamp": "2020-05-22 15:00", "Open": 4.665508747, "High": 4.665508747, "Low": 4.589181423, @@ -1104,7 +1104,7 @@ "Volume": 139951436 }, { - "Date": "2020-05-25 15:00", + "Timestamp": "2020-05-25 15:00", "Open": 4.608263493, "High": 4.655968189, "Low": 4.598722458, @@ -1112,7 +1112,7 @@ "Volume": 108551422 }, { - "Date": "2020-05-26 15:00", + "Timestamp": "2020-05-26 15:00", "Open": 4.598722458, "High": 4.63688612, "Low": 4.598722458, @@ -1120,7 +1120,7 @@ "Volume": 81845831 }, { - "Date": "2020-05-27 15:00", + "Timestamp": "2020-05-27 15:00", "Open": 4.627345085, "High": 4.655968189, "Low": 4.608263493, @@ -1128,7 +1128,7 @@ "Volume": 92758595 }, { - "Date": "2020-05-28 15:00", + "Timestamp": "2020-05-28 15:00", "Open": 4.63688612, "High": 4.694131851, "Low": 4.627345085, @@ -1136,7 +1136,7 @@ "Volume": 119546499 }, { - "Date": "2020-05-29 15:00", + "Timestamp": "2020-05-29 15:00", "Open": 4.627345085, "High": 4.63688612, "Low": 4.608263493, @@ -1144,7 +1144,7 @@ "Volume": 96768908 }, { - "Date": "2020-06-01 15:00", + "Timestamp": "2020-06-01 15:00", "Open": 4.63688612, "High": 4.684590816, "Low": 4.617804527, @@ -1152,7 +1152,7 @@ "Volume": 135039814 }, { - "Date": "2020-06-02 15:00", + "Timestamp": "2020-06-02 15:00", "Open": 4.675049782, "High": 4.732295513, "Low": 4.665508747, @@ -1160,7 +1160,7 @@ "Volume": 202329736 }, { - "Date": "2020-06-03 15:00", + "Timestamp": "2020-06-03 15:00", "Open": 4.722754478, "High": 4.78000021, "Low": 4.703672409, @@ -1168,7 +1168,7 @@ "Volume": 164585588 }, { - "Date": "2020-06-04 15:00", + "Timestamp": "2020-06-04 15:00", "Open": 4.722754478, "High": 4.732295513, "Low": 4.684590816, @@ -1176,7 +1176,7 @@ "Volume": 102598710 }, { - "Date": "2020-06-05 15:00", + "Timestamp": "2020-06-05 15:00", "Open": 4.694131851, "High": 4.713213444, "Low": 4.675049782, @@ -1184,7 +1184,7 @@ "Volume": 77107447 }, { - "Date": "2020-06-08 15:00", + "Timestamp": "2020-06-08 15:00", "Open": 4.703672409, "High": 4.732295513, "Low": 4.703672409, @@ -1192,7 +1192,7 @@ "Volume": 94208437 }, { - "Date": "2020-06-09 15:00", + "Timestamp": "2020-06-09 15:00", "Open": 4.722754478, "High": 4.741836071, "Low": 4.713213444, @@ -1200,7 +1200,7 @@ "Volume": 77383403 }, { - "Date": "2020-06-10 15:00", + "Timestamp": "2020-06-10 15:00", "Open": 4.722754478, "High": 4.722754478, "Low": 4.694131851, @@ -1208,7 +1208,7 @@ "Volume": 80669428 }, { - "Date": "2020-06-11 15:00", + "Timestamp": "2020-06-11 15:00", "Open": 4.722754478, "High": 4.751377106, "Low": 4.703672409, @@ -1216,7 +1216,7 @@ "Volume": 179211762 }, { - "Date": "2020-06-12 15:00", + "Timestamp": "2020-06-12 15:00", "Open": 4.694131851, "High": 4.741836071, "Low": 4.675049782, @@ -1224,7 +1224,7 @@ "Volume": 178743165 }, { - "Date": "2020-06-15 15:00", + "Timestamp": "2020-06-15 15:00", "Open": 4.703672409, "High": 4.741836071, "Low": 4.684590816, @@ -1232,7 +1232,7 @@ "Volume": 114689957 }, { - "Date": "2020-06-16 15:00", + "Timestamp": "2020-06-16 15:00", "Open": 4.732295513, "High": 4.741836071, "Low": 4.703672409, @@ -1240,7 +1240,7 @@ "Volume": 91245129 }, { - "Date": "2020-06-17 15:00", + "Timestamp": "2020-06-17 15:00", "Open": 4.741836071, "High": 4.770459175, "Low": 4.732295513, @@ -1248,7 +1248,7 @@ "Volume": 121430348 }, { - "Date": "2020-06-18 15:00", + "Timestamp": "2020-06-18 15:00", "Open": 4.770459175, "High": 4.789540768, "Low": 4.741836071, @@ -1256,7 +1256,7 @@ "Volume": 161660437 }, { - "Date": "2020-06-19 15:00", + "Timestamp": "2020-06-19 15:00", "Open": 4.800000191, "High": 4.800000191, "Low": 4.739999771, @@ -1264,7 +1264,7 @@ "Volume": 220829811 }, { - "Date": "2020-06-22 15:00", + "Timestamp": "2020-06-22 15:00", "Open": 4.75, "High": 4.769999981, "Low": 4.71999979, @@ -1272,7 +1272,7 @@ "Volume": 146421030 }, { - "Date": "2020-06-23 15:00", + "Timestamp": "2020-06-23 15:00", "Open": 4.730000019, "High": 4.730000019, "Low": 4.679999828, @@ -1280,7 +1280,7 @@ "Volume": 99974108 }, { - "Date": "2020-06-24 15:00", + "Timestamp": "2020-06-24 15:00", "Open": 4.699999809, "High": 4.789999962, "Low": 4.679999828, @@ -1288,7 +1288,7 @@ "Volume": 136540853 }, { - "Date": "2020-06-29 15:00", + "Timestamp": "2020-06-29 15:00", "Open": 4.75, "High": 4.760000229, "Low": 4.690000057, @@ -1296,7 +1296,7 @@ "Volume": 95327177 }, { - "Date": "2020-06-30 15:00", + "Timestamp": "2020-06-30 15:00", "Open": 4.699999809, "High": 4.739999771, "Low": 4.690000057, @@ -1304,7 +1304,7 @@ "Volume": 88925761 }, { - "Date": "2020-07-01 15:00", + "Timestamp": "2020-07-01 15:00", "Open": 4.71999979, "High": 4.769999981, "Low": 4.690000057, @@ -1312,7 +1312,7 @@ "Volume": 163310116 }, { - "Date": "2020-07-02 15:00", + "Timestamp": "2020-07-02 15:00", "Open": 4.760000229, "High": 4.940000057, "Low": 4.75, @@ -1320,7 +1320,7 @@ "Volume": 445781574 }, { - "Date": "2020-07-03 15:00", + "Timestamp": "2020-07-03 15:00", "Open": 4.949999809, "High": 5.289999962, "Low": 4.929999828, @@ -1328,7 +1328,7 @@ "Volume": 817401435 }, { - "Date": "2020-07-06 15:00", + "Timestamp": "2020-07-06 15:00", "Open": 5.300000191, "High": 5.710000038, "Low": 5.300000191, @@ -1336,7 +1336,7 @@ "Volume": 1345110540 }, { - "Date": "2020-07-07 15:00", + "Timestamp": "2020-07-07 15:00", "Open": 6.28000021, "High": 6.28000021, "Low": 5.96999979, @@ -1344,7 +1344,7 @@ "Volume": 2589682228 }, { - "Date": "2020-07-08 15:00", + "Timestamp": "2020-07-08 15:00", "Open": 5.71999979, "High": 6.119999886, "Low": 5.610000134, @@ -1352,7 +1352,7 @@ "Volume": 1797593800 }, { - "Date": "2020-07-09 15:00", + "Timestamp": "2020-07-09 15:00", "Open": 5.829999924, "High": 5.96999979, "Low": 5.75, @@ -1360,7 +1360,7 @@ "Volume": 1246966813 }, { - "Date": "2020-07-10 15:00", + "Timestamp": "2020-07-10 15:00", "Open": 5.760000229, "High": 5.78000021, "Low": 5.53000021, @@ -1368,7 +1368,7 @@ "Volume": 880319408 }, { - "Date": "2020-07-13 15:00", + "Timestamp": "2020-07-13 15:00", "Open": 5.489999771, "High": 5.710000038, "Low": 5.460000038, @@ -1376,7 +1376,7 @@ "Volume": 589179313 }, { - "Date": "2020-07-14 15:00", + "Timestamp": "2020-07-14 15:00", "Open": 5.619999886, "High": 5.639999866, "Low": 5.5, @@ -1384,7 +1384,7 @@ "Volume": 430760990 }, { - "Date": "2020-07-15 15:00", + "Timestamp": "2020-07-15 15:00", "Open": 5.559999943, "High": 5.599999905, "Low": 5.400000095, @@ -1392,7 +1392,7 @@ "Volume": 390929289 }, { - "Date": "2020-07-16 15:00", + "Timestamp": "2020-07-16 15:00", "Open": 5.400000095, "High": 5.570000172, "Low": 5.369999886, @@ -1400,7 +1400,7 @@ "Volume": 473236597 }, { - "Date": "2020-07-17 15:00", + "Timestamp": "2020-07-17 15:00", "Open": 5.380000114, "High": 5.449999809, "Low": 5.25, @@ -1408,7 +1408,7 @@ "Volume": 306479042 }, { - "Date": "2020-07-20 15:00", + "Timestamp": "2020-07-20 15:00", "Open": 5.340000153, "High": 5.460000038, "Low": 5.289999962, @@ -1416,7 +1416,7 @@ "Volume": 321410629 }, { - "Date": "2020-07-21 15:00", + "Timestamp": "2020-07-21 15:00", "Open": 5.429999828, "High": 5.460000038, "Low": 5.360000134, @@ -1424,7 +1424,7 @@ "Volume": 208745107 }, { - "Date": "2020-07-22 15:00", + "Timestamp": "2020-07-22 15:00", "Open": 5.369999886, "High": 5.46999979, "Low": 5.340000153, @@ -1432,7 +1432,7 @@ "Volume": 270127340 }, { - "Date": "2020-07-23 15:00", + "Timestamp": "2020-07-23 15:00", "Open": 5.349999905, "High": 5.349999905, "Low": 5.210000038, @@ -1440,7 +1440,7 @@ "Volume": 263988476 }, { - "Date": "2020-07-24 15:00", + "Timestamp": "2020-07-24 15:00", "Open": 5.260000229, "High": 5.289999962, "Low": 5.110000134, @@ -1448,7 +1448,7 @@ "Volume": 281719996 }, { - "Date": "2020-07-27 15:00", + "Timestamp": "2020-07-27 15:00", "Open": 5.110000134, "High": 5.150000095, "Low": 5.059999943, @@ -1456,7 +1456,7 @@ "Volume": 113996849 }, { - "Date": "2020-07-28 15:00", + "Timestamp": "2020-07-28 15:00", "Open": 5.110000134, "High": 5.139999866, "Low": 5.090000153, @@ -1464,7 +1464,7 @@ "Volume": 93843641 }, { - "Date": "2020-07-29 15:00", + "Timestamp": "2020-07-29 15:00", "Open": 5.099999905, "High": 5.199999809, "Low": 5.090000153, @@ -1472,7 +1472,7 @@ "Volume": 170719515 }, { - "Date": "2020-07-30 15:00", + "Timestamp": "2020-07-30 15:00", "Open": 5.179999828, "High": 5.199999809, "Low": 5.130000114, @@ -1480,7 +1480,7 @@ "Volume": 157100671 }, { - "Date": "2020-07-31 15:00", + "Timestamp": "2020-07-31 15:00", "Open": 5.130000114, "High": 5.199999809, "Low": 5.119999886, @@ -1488,7 +1488,7 @@ "Volume": 142827024 }, { - "Date": "2020-08-03 15:00", + "Timestamp": "2020-08-03 15:00", "Open": 5.179999828, "High": 5.260000229, "Low": 5.150000095, @@ -1496,7 +1496,7 @@ "Volume": 277216353 }, { - "Date": "2020-08-04 15:00", + "Timestamp": "2020-08-04 15:00", "Open": 5.28000021, "High": 5.440000057, "Low": 5.21999979, @@ -1504,7 +1504,7 @@ "Volume": 437510900 }, { - "Date": "2020-08-05 15:00", + "Timestamp": "2020-08-05 15:00", "Open": 5.289999962, "High": 5.289999962, "Low": 5.210000038, @@ -1512,7 +1512,7 @@ "Volume": 220489374 }, { - "Date": "2020-08-06 15:00", + "Timestamp": "2020-08-06 15:00", "Open": 5.25, "High": 5.28000021, "Low": 5.199999809, @@ -1520,7 +1520,7 @@ "Volume": 146723495 }, { - "Date": "2020-08-07 15:00", + "Timestamp": "2020-08-07 15:00", "Open": 5.230000019, "High": 5.320000172, "Low": 5.210000038, @@ -1528,7 +1528,7 @@ "Volume": 205343220 }, { - "Date": "2020-08-10 15:00", + "Timestamp": "2020-08-10 15:00", "Open": 5.239999771, "High": 5.300000191, "Low": 5.199999809, @@ -1536,7 +1536,7 @@ "Volume": 161078305 }, { - "Date": "2020-08-11 15:00", + "Timestamp": "2020-08-11 15:00", "Open": 5.260000229, "High": 5.400000095, "Low": 5.25, @@ -1544,7 +1544,7 @@ "Volume": 317081873 }, { - "Date": "2020-08-12 15:00", + "Timestamp": "2020-08-12 15:00", "Open": 5.21999979, "High": 5.300000191, "Low": 5.199999809, @@ -1552,7 +1552,7 @@ "Volume": 188144748 }, { - "Date": "2020-08-13 15:00", + "Timestamp": "2020-08-13 15:00", "Open": 5.25, "High": 5.269999981, "Low": 5.230000019, @@ -1560,7 +1560,7 @@ "Volume": 106954175 }, { - "Date": "2020-08-14 15:00", + "Timestamp": "2020-08-14 15:00", "Open": 5.230000019, "High": 5.329999924, "Low": 5.210000038, @@ -1568,7 +1568,7 @@ "Volume": 209282026 }, { - "Date": "2020-08-17 15:00", + "Timestamp": "2020-08-17 15:00", "Open": 5.329999924, "High": 5.590000153, "Low": 5.329999924, @@ -1576,7 +1576,7 @@ "Volume": 627526355 }, { - "Date": "2020-08-18 15:00", + "Timestamp": "2020-08-18 15:00", "Open": 5.489999771, "High": 5.489999771, "Low": 5.389999866, @@ -1584,7 +1584,7 @@ "Volume": 337782466 }, { - "Date": "2020-08-19 15:00", + "Timestamp": "2020-08-19 15:00", "Open": 5.400000095, "High": 5.429999828, "Low": 5.360000134, @@ -1592,7 +1592,7 @@ "Volume": 178051523 }, { - "Date": "2020-08-20 15:00", + "Timestamp": "2020-08-20 15:00", "Open": 5.340000153, "High": 5.369999886, "Low": 5.300000191, @@ -1600,7 +1600,7 @@ "Volume": 133022101 }, { - "Date": "2020-08-21 15:00", + "Timestamp": "2020-08-21 15:00", "Open": 5.349999905, "High": 5.360000134, "Low": 5.28000021, @@ -1608,7 +1608,7 @@ "Volume": 117089980 }, { - "Date": "2020-08-24 15:00", + "Timestamp": "2020-08-24 15:00", "Open": 5.300000191, "High": 5.320000172, "Low": 5.269999981, @@ -1616,7 +1616,7 @@ "Volume": 87444583 }, { - "Date": "2020-08-25 15:00", + "Timestamp": "2020-08-25 15:00", "Open": 5.300000191, "High": 5.329999924, "Low": 5.260000229, @@ -1624,7 +1624,7 @@ "Volume": 89329710 }, { - "Date": "2020-08-26 15:00", + "Timestamp": "2020-08-26 15:00", "Open": 5.260000229, "High": 5.28000021, "Low": 5.179999828, @@ -1632,7 +1632,7 @@ "Volume": 118942032 }, { - "Date": "2020-08-27 15:00", + "Timestamp": "2020-08-27 15:00", "Open": 5.190000057, "High": 5.21999979, "Low": 5.170000076, @@ -1640,7 +1640,7 @@ "Volume": 83194697 }, { - "Date": "2020-08-28 15:00", + "Timestamp": "2020-08-28 15:00", "Open": 5.210000038, "High": 5.269999981, "Low": 5.190000057, @@ -1648,7 +1648,7 @@ "Volume": 121289550 }, { - "Date": "2020-08-31 15:00", + "Timestamp": "2020-08-31 15:00", "Open": 5.269999981, "High": 5.300000191, "Low": 5.210000038, @@ -1656,7 +1656,7 @@ "Volume": 131156840 }, { - "Date": "2020-09-01 15:00", + "Timestamp": "2020-09-01 15:00", "Open": 5.199999809, "High": 5.230000019, "Low": 5.179999828, @@ -1664,7 +1664,7 @@ "Volume": 94861950 }, { - "Date": "2020-09-02 15:00", + "Timestamp": "2020-09-02 15:00", "Open": 5.210000038, "High": 5.210000038, "Low": 5.170000076, @@ -1672,7 +1672,7 @@ "Volume": 95333658 }, { - "Date": "2020-09-03 15:00", + "Timestamp": "2020-09-03 15:00", "Open": 5.170000076, "High": 5.199999809, "Low": 5.150000095, @@ -1680,7 +1680,7 @@ "Volume": 83132609 }, { - "Date": "2020-09-04 15:00", + "Timestamp": "2020-09-04 15:00", "Open": 5.119999886, "High": 5.159999847, "Low": 5.110000134, @@ -1688,7 +1688,7 @@ "Volume": 78193398 }, { - "Date": "2020-09-07 15:00", + "Timestamp": "2020-09-07 15:00", "Open": 5.139999866, "High": 5.170000076, "Low": 5.099999905, @@ -1696,7 +1696,7 @@ "Volume": 87042221 }, { - "Date": "2020-09-08 15:00", + "Timestamp": "2020-09-08 15:00", "Open": 5.110000134, "High": 5.179999828, "Low": 5.110000134, @@ -1704,7 +1704,7 @@ "Volume": 90201450 }, { - "Date": "2020-09-09 15:00", + "Timestamp": "2020-09-09 15:00", "Open": 5.139999866, "High": 5.159999847, "Low": 5.119999886, @@ -1712,7 +1712,7 @@ "Volume": 87860782 }, { - "Date": "2020-09-10 15:00", + "Timestamp": "2020-09-10 15:00", "Open": 5.159999847, "High": 5.159999847, "Low": 5.059999943, @@ -1720,7 +1720,7 @@ "Volume": 96375360 }, { - "Date": "2020-09-11 15:00", + "Timestamp": "2020-09-11 15:00", "Open": 5.059999943, "High": 5.070000172, "Low": 5.03000021, @@ -1728,7 +1728,7 @@ "Volume": 68229573 }, { - "Date": "2020-09-14 15:00", + "Timestamp": "2020-09-14 15:00", "Open": 5.059999943, "High": 5.099999905, "Low": 5.039999962, @@ -1736,7 +1736,7 @@ "Volume": 67006885 }, { - "Date": "2020-09-15 15:00", + "Timestamp": "2020-09-15 15:00", "Open": 5.070000172, "High": 5.079999924, "Low": 5.050000191, @@ -1744,7 +1744,7 @@ "Volume": 45045921 }, { - "Date": "2020-09-16 15:00", + "Timestamp": "2020-09-16 15:00", "Open": 5.059999943, "High": 5.090000153, "Low": 5.050000191, @@ -1752,7 +1752,7 @@ "Volume": 44274686 }, { - "Date": "2020-09-17 15:00", + "Timestamp": "2020-09-17 15:00", "Open": 5.050000191, "High": 5.059999943, "Low": 5.03000021, @@ -1760,7 +1760,7 @@ "Volume": 50017793 }, { - "Date": "2020-09-18 15:00", + "Timestamp": "2020-09-18 15:00", "Open": 5.039999962, "High": 5.150000095, "Low": 5.03000021, @@ -1768,7 +1768,7 @@ "Volume": 135338733 }, { - "Date": "2020-09-21 15:00", + "Timestamp": "2020-09-21 15:00", "Open": 5.159999847, "High": 5.170000076, "Low": 5.099999905, @@ -1776,7 +1776,7 @@ "Volume": 71215691 }, { - "Date": "2020-09-22 15:00", + "Timestamp": "2020-09-22 15:00", "Open": 5.079999924, "High": 5.230000019, "Low": 5.059999943, @@ -1784,7 +1784,7 @@ "Volume": 98780698 }, { - "Date": "2020-09-23 15:00", + "Timestamp": "2020-09-23 15:00", "Open": 5.110000134, "High": 5.110000134, "Low": 5.079999924, @@ -1792,7 +1792,7 @@ "Volume": 38418433 }, { - "Date": "2020-09-24 15:00", + "Timestamp": "2020-09-24 15:00", "Open": 5.079999924, "High": 5.090000153, "Low": 5.019999981, @@ -1800,7 +1800,7 @@ "Volume": 63226540 }, { - "Date": "2020-09-25 15:00", + "Timestamp": "2020-09-25 15:00", "Open": 5.03000021, "High": 5.050000191, "Low": 5.010000229, @@ -1808,7 +1808,7 @@ "Volume": 40838063 }, { - "Date": "2020-09-28 15:00", + "Timestamp": "2020-09-28 15:00", "Open": 5.019999981, "High": 5.039999962, "Low": 5, @@ -1816,7 +1816,7 @@ "Volume": 37235104 }, { - "Date": "2020-09-29 15:00", + "Timestamp": "2020-09-29 15:00", "Open": 5.019999981, "High": 5.03000021, "Low": 5, @@ -1824,7 +1824,7 @@ "Volume": 36330271 }, { - "Date": "2020-09-30 15:00", + "Timestamp": "2020-09-30 15:00", "Open": 5, "High": 5.03000021, "Low": 4.980000019, @@ -1832,7 +1832,7 @@ "Volume": 44230595 }, { - "Date": "2020-10-09 15:00", + "Timestamp": "2020-10-09 15:00", "Open": 5, "High": 5.039999962, "Low": 5, @@ -1840,7 +1840,7 @@ "Volume": 69641535 }, { - "Date": "2020-10-12 15:00", + "Timestamp": "2020-10-12 15:00", "Open": 5.03000021, "High": 5.150000095, "Low": 5.010000229, @@ -1848,7 +1848,7 @@ "Volume": 130031246 }, { - "Date": "2020-10-13 15:00", + "Timestamp": "2020-10-13 15:00", "Open": 5.099999905, "High": 5.110000134, "Low": 5.070000172, @@ -1856,7 +1856,7 @@ "Volume": 49437559 }, { - "Date": "2020-10-14 15:00", + "Timestamp": "2020-10-14 15:00", "Open": 5.090000153, "High": 5.099999905, "Low": 5.019999981, @@ -1864,7 +1864,7 @@ "Volume": 58099320 }, { - "Date": "2020-10-15 15:00", + "Timestamp": "2020-10-15 15:00", "Open": 5.050000191, "High": 5.119999886, "Low": 5.03000021, @@ -1872,7 +1872,7 @@ "Volume": 79762659 }, { - "Date": "2020-10-16 15:00", + "Timestamp": "2020-10-16 15:00", "Open": 5.059999943, "High": 5.130000114, "Low": 5.03000021, @@ -1880,7 +1880,7 @@ "Volume": 153036072 }, { - "Date": "2020-10-19 15:00", + "Timestamp": "2020-10-19 15:00", "Open": 5.130000114, "High": 5.190000057, "Low": 5.070000172, @@ -1888,7 +1888,7 @@ "Volume": 136985180 }, { - "Date": "2020-10-20 15:00", + "Timestamp": "2020-10-20 15:00", "Open": 5.059999943, "High": 5.070000172, "Low": 5.019999981, @@ -1896,7 +1896,7 @@ "Volume": 85103625 }, { - "Date": "2020-10-21 15:00", + "Timestamp": "2020-10-21 15:00", "Open": 5.039999962, "High": 5.090000153, "Low": 5.019999981, @@ -1904,7 +1904,7 @@ "Volume": 88404624 }, { - "Date": "2020-10-22 15:00", + "Timestamp": "2020-10-22 15:00", "Open": 5.070000172, "High": 5.079999924, "Low": 5.03000021, @@ -1912,7 +1912,7 @@ "Volume": 75363592 }, { - "Date": "2020-10-23 15:00", + "Timestamp": "2020-10-23 15:00", "Open": 5.039999962, "High": 5.099999905, "Low": 5.03000021, @@ -1920,7 +1920,7 @@ "Volume": 108124617 }, { - "Date": "2020-10-26 15:00", + "Timestamp": "2020-10-26 15:00", "Open": 5.050000191, "High": 5.050000191, "Low": 4.980000019, @@ -1928,7 +1928,7 @@ "Volume": 104402119 }, { - "Date": "2020-10-27 15:00", + "Timestamp": "2020-10-27 15:00", "Open": 4.980000019, "High": 4.980000019, "Low": 4.909999847, @@ -1936,7 +1936,7 @@ "Volume": 79701713 }, { - "Date": "2020-10-28 15:00", + "Timestamp": "2020-10-28 15:00", "Open": 4.940000057, "High": 4.940000057, "Low": 4.78000021, @@ -1944,7 +1944,7 @@ "Volume": 146061526 }, { - "Date": "2020-10-29 15:00", + "Timestamp": "2020-10-29 15:00", "Open": 4.760000229, "High": 4.78000021, "Low": 4.71999979, @@ -1952,7 +1952,7 @@ "Volume": 88982782 }, { - "Date": "2020-10-30 15:00", + "Timestamp": "2020-10-30 15:00", "Open": 4.510000229, "High": 4.559999943, "Low": 4.429999828, @@ -1960,7 +1960,7 @@ "Volume": 739564040 }, { - "Date": "2020-11-02 15:00", + "Timestamp": "2020-11-02 15:00", "Open": 4.440000057, "High": 4.460000038, "Low": 4.380000114, @@ -1968,7 +1968,7 @@ "Volume": 291674752 }, { - "Date": "2020-11-03 15:00", + "Timestamp": "2020-11-03 15:00", "Open": 4.400000095, "High": 4.460000038, "Low": 4.389999866, @@ -1976,7 +1976,7 @@ "Volume": 177273320 }, { - "Date": "2020-11-04 15:00", + "Timestamp": "2020-11-04 15:00", "Open": 4.460000038, "High": 4.480000019, "Low": 4.420000076, @@ -1984,7 +1984,7 @@ "Volume": 162728036 }, { - "Date": "2020-11-05 15:00", + "Timestamp": "2020-11-05 15:00", "Open": 4.449999809, "High": 4.46999979, "Low": 4.429999828, @@ -1992,7 +1992,7 @@ "Volume": 150917855 }, { - "Date": "2020-11-06 15:00", + "Timestamp": "2020-11-06 15:00", "Open": 4.460000038, "High": 4.460000038, "Low": 4.429999828, @@ -2000,7 +2000,7 @@ "Volume": 130816487 }, { - "Date": "2020-11-09 15:00", + "Timestamp": "2020-11-09 15:00", "Open": 4.449999809, "High": 4.5, "Low": 4.449999809, @@ -2008,7 +2008,7 @@ "Volume": 291472862 }, { - "Date": "2020-11-10 15:00", + "Timestamp": "2020-11-10 15:00", "Open": 4.489999771, "High": 4.53000021, "Low": 4.449999809, @@ -2016,7 +2016,7 @@ "Volume": 255342765 }, { - "Date": "2020-11-11 15:00", + "Timestamp": "2020-11-11 15:00", "Open": 4.449999809, "High": 4.480000019, "Low": 4.429999828, @@ -2024,7 +2024,7 @@ "Volume": 237066062 }, { - "Date": "2020-11-12 15:00", + "Timestamp": "2020-11-12 15:00", "Open": 4.449999809, "High": 4.460000038, "Low": 4.429999828, @@ -2032,7 +2032,7 @@ "Volume": 143700968 }, { - "Date": "2020-11-13 15:00", + "Timestamp": "2020-11-13 15:00", "Open": 4.429999828, "High": 4.440000057, "Low": 4.400000095, @@ -2040,7 +2040,7 @@ "Volume": 138486840 }, { - "Date": "2020-11-16 15:00", + "Timestamp": "2020-11-16 15:00", "Open": 4.429999828, "High": 4.460000038, "Low": 4.420000076, @@ -2048,7 +2048,7 @@ "Volume": 130619948 }, { - "Date": "2020-11-17 15:00", + "Timestamp": "2020-11-17 15:00", "Open": 4.449999809, "High": 4.480000019, "Low": 4.440000057, @@ -2056,7 +2056,7 @@ "Volume": 171720546 }, { - "Date": "2020-11-18 15:00", + "Timestamp": "2020-11-18 15:00", "Open": 4.46999979, "High": 4.53000021, "Low": 4.460000038, @@ -2064,7 +2064,7 @@ "Volume": 245552039 }, { - "Date": "2020-11-19 15:00", + "Timestamp": "2020-11-19 15:00", "Open": 4.5, "High": 4.519999981, "Low": 4.489999771, @@ -2072,7 +2072,7 @@ "Volume": 178069173 }, { - "Date": "2020-11-20 15:00", + "Timestamp": "2020-11-20 15:00", "Open": 4.489999771, "High": 4.510000229, "Low": 4.480000019, @@ -2080,7 +2080,7 @@ "Volume": 133448004 }, { - "Date": "2020-11-23 15:00", + "Timestamp": "2020-11-23 15:00", "Open": 4.489999771, "High": 4.570000172, "Low": 4.489999771, @@ -2088,7 +2088,7 @@ "Volume": 252056078 }, { - "Date": "2020-11-24 15:00", + "Timestamp": "2020-11-24 15:00", "Open": 4.539999962, "High": 4.570000172, "Low": 4.519999981, @@ -2096,7 +2096,7 @@ "Volume": 163739479 }, { - "Date": "2020-11-25 15:00", + "Timestamp": "2020-11-25 15:00", "Open": 4.550000191, "High": 4.579999924, "Low": 4.5, @@ -2104,7 +2104,7 @@ "Volume": 214558614 }, { - "Date": "2020-11-26 15:00", + "Timestamp": "2020-11-26 15:00", "Open": 4.5, "High": 4.53000021, "Low": 4.489999771, @@ -2112,7 +2112,7 @@ "Volume": 139644749 }, { - "Date": "2020-11-27 15:00", + "Timestamp": "2020-11-27 15:00", "Open": 4.519999981, "High": 4.610000134, "Low": 4.510000229, @@ -2120,7 +2120,7 @@ "Volume": 346593716 }, { - "Date": "2020-11-30 15:00", + "Timestamp": "2020-11-30 15:00", "Open": 4.639999866, "High": 4.809999943, "Low": 4.610000134, @@ -2128,7 +2128,7 @@ "Volume": 623885277 }, { - "Date": "2020-12-01 15:00", + "Timestamp": "2020-12-01 15:00", "Open": 4.610000134, "High": 4.820000172, "Low": 4.590000153, @@ -2136,7 +2136,7 @@ "Volume": 628148306 }, { - "Date": "2020-12-02 15:00", + "Timestamp": "2020-12-02 15:00", "Open": 4.769999981, "High": 4.78000021, "Low": 4.710000038, @@ -2144,7 +2144,7 @@ "Volume": 399448879 }, { - "Date": "2020-12-03 15:00", + "Timestamp": "2020-12-03 15:00", "Open": 4.730000019, "High": 4.869999886, "Low": 4.699999809, @@ -2152,7 +2152,7 @@ "Volume": 568819755 }, { - "Date": "2020-12-04 15:00", + "Timestamp": "2020-12-04 15:00", "Open": 4.769999981, "High": 4.769999981, "Low": 4.690000057, @@ -2160,7 +2160,7 @@ "Volume": 370847881 }, { - "Date": "2020-12-07 15:00", + "Timestamp": "2020-12-07 15:00", "Open": 4.699999809, "High": 4.71999979, "Low": 4.630000114, @@ -2168,7 +2168,7 @@ "Volume": 212967349 }, { - "Date": "2020-12-08 15:00", + "Timestamp": "2020-12-08 15:00", "Open": 4.639999866, "High": 4.670000076, "Low": 4.590000153, @@ -2176,7 +2176,7 @@ "Volume": 141026711 }, { - "Date": "2020-12-09 15:00", + "Timestamp": "2020-12-09 15:00", "Open": 4.610000134, "High": 4.699999809, "Low": 4.610000134, @@ -2184,7 +2184,7 @@ "Volume": 230571559 }, { - "Date": "2020-12-10 15:00", + "Timestamp": "2020-12-10 15:00", "Open": 4.590000153, "High": 4.619999886, "Low": 4.559999943, @@ -2192,7 +2192,7 @@ "Volume": 164670529 }, { - "Date": "2020-12-11 15:00", + "Timestamp": "2020-12-11 15:00", "Open": 4.599999905, "High": 4.630000114, "Low": 4.559999943, @@ -2200,7 +2200,7 @@ "Volume": 155700296 }, { - "Date": "2020-12-14 15:00", + "Timestamp": "2020-12-14 15:00", "Open": 4.579999924, "High": 4.590000153, "Low": 4.539999962, @@ -2208,7 +2208,7 @@ "Volume": 119271066 }, { - "Date": "2020-12-15 15:00", + "Timestamp": "2020-12-15 15:00", "Open": 4.539999962, "High": 4.539999962, "Low": 4.480000019, @@ -2216,7 +2216,7 @@ "Volume": 115664623 }, { - "Date": "2020-12-16 15:00", + "Timestamp": "2020-12-16 15:00", "Open": 4.510000229, "High": 4.539999962, "Low": 4.46999979, @@ -2224,7 +2224,7 @@ "Volume": 105325134 }, { - "Date": "2020-12-17 15:00", + "Timestamp": "2020-12-17 15:00", "Open": 4.480000019, "High": 4.53000021, "Low": 4.440000057, @@ -2232,7 +2232,7 @@ "Volume": 162318881 }, { - "Date": "2020-12-18 15:00", + "Timestamp": "2020-12-18 15:00", "Open": 4.510000229, "High": 4.53000021, "Low": 4.480000019, @@ -2240,7 +2240,7 @@ "Volume": 102932684 }, { - "Date": "2020-12-21 15:00", + "Timestamp": "2020-12-21 15:00", "Open": 4.489999771, "High": 4.519999981, "Low": 4.46999979, @@ -2248,7 +2248,7 @@ "Volume": 92663190 }, { - "Date": "2020-12-22 15:00", + "Timestamp": "2020-12-22 15:00", "Open": 4.489999771, "High": 4.5, "Low": 4.409999847, @@ -2256,7 +2256,7 @@ "Volume": 148952288 }, { - "Date": "2020-12-23 15:00", + "Timestamp": "2020-12-23 15:00", "Open": 4.420000076, "High": 4.449999809, "Low": 4.409999847, @@ -2264,7 +2264,7 @@ "Volume": 88930202 }, { - "Date": "2020-12-24 15:00", + "Timestamp": "2020-12-24 15:00", "Open": 4.429999828, "High": 4.46999979, "Low": 4.409999847, @@ -2272,7 +2272,7 @@ "Volume": 95734205 }, { - "Date": "2020-12-25 15:00", + "Timestamp": "2020-12-25 15:00", "Open": 4.409999847, "High": 4.440000057, "Low": 4.400000095, @@ -2280,7 +2280,7 @@ "Volume": 101063413 }, { - "Date": "2020-12-28 15:00", + "Timestamp": "2020-12-28 15:00", "Open": 4.420000076, "High": 4.449999809, "Low": 4.389999866, @@ -2288,7 +2288,7 @@ "Volume": 106086864 }, { - "Date": "2020-12-29 15:00", + "Timestamp": "2020-12-29 15:00", "Open": 4.420000076, "High": 4.460000038, "Low": 4.409999847, @@ -2296,7 +2296,7 @@ "Volume": 134490694 }, { - "Date": "2020-12-30 15:00", + "Timestamp": "2020-12-30 15:00", "Open": 4.420000076, "High": 4.449999809, "Low": 4.409999847, @@ -2304,7 +2304,7 @@ "Volume": 112040625 }, { - "Date": "2020-12-31 15:00", + "Timestamp": "2020-12-31 15:00", "Open": 4.440000057, "High": 4.550000191, "Low": 4.429999828, @@ -2312,7 +2312,7 @@ "Volume": 216125896 }, { - "Date": "2021-01-04 15:00", + "Timestamp": "2021-01-04 15:00", "Open": 4.489999771, "High": 4.489999771, "Low": 4.420000076, @@ -2320,7 +2320,7 @@ "Volume": 251881884 }, { - "Date": "2021-01-05 15:00", + "Timestamp": "2021-01-05 15:00", "Open": 4.429999828, "High": 4.440000057, "Low": 4.369999886, @@ -2328,7 +2328,7 @@ "Volume": 265717296 }, { - "Date": "2021-01-06 15:00", + "Timestamp": "2021-01-06 15:00", "Open": 4.400000095, "High": 4.409999847, "Low": 4.380000114, @@ -2336,7 +2336,7 @@ "Volume": 247874438 }, { - "Date": "2021-01-07 15:00", + "Timestamp": "2021-01-07 15:00", "Open": 4.400000095, "High": 4.400000095, "Low": 4.289999962, @@ -2344,7 +2344,7 @@ "Volume": 322600805 }, { - "Date": "2021-01-08 15:00", + "Timestamp": "2021-01-08 15:00", "Open": 4.300000191, "High": 4.349999905, "Low": 4.25, @@ -2352,7 +2352,7 @@ "Volume": 155169899 }, { - "Date": "2021-01-11 15:00", + "Timestamp": "2021-01-11 15:00", "Open": 4.329999924, "High": 4.349999905, "Low": 4.260000229, @@ -2360,7 +2360,7 @@ "Volume": 153473953 }, { - "Date": "2021-01-12 15:00", + "Timestamp": "2021-01-12 15:00", "Open": 4.260000229, "High": 4.329999924, "Low": 4.260000229, @@ -2368,7 +2368,7 @@ "Volume": 138909043 }, { - "Date": "2021-01-13 15:00", + "Timestamp": "2021-01-13 15:00", "Open": 4.320000172, "High": 4.329999924, "Low": 4.260000229, @@ -2376,7 +2376,7 @@ "Volume": 126967274 }, { - "Date": "2021-01-14 15:00", + "Timestamp": "2021-01-14 15:00", "Open": 4.289999962, "High": 4.380000114, "Low": 4.289999962, @@ -2384,7 +2384,7 @@ "Volume": 135689641 }, { - "Date": "2021-01-15 15:00", + "Timestamp": "2021-01-15 15:00", "Open": 4.360000134, "High": 4.460000038, "Low": 4.349999905, @@ -2392,7 +2392,7 @@ "Volume": 351355466 }, { - "Date": "2021-01-18 15:00", + "Timestamp": "2021-01-18 15:00", "Open": 4.389999866, "High": 4.420000076, "Low": 4.369999886, @@ -2400,7 +2400,7 @@ "Volume": 180705429 }, { - "Date": "2021-01-19 15:00", + "Timestamp": "2021-01-19 15:00", "Open": 4.389999866, "High": 4.460000038, "Low": 4.369999886, @@ -2408,7 +2408,7 @@ "Volume": 211825785 }, { - "Date": "2021-01-20 15:00", + "Timestamp": "2021-01-20 15:00", "Open": 4.429999828, "High": 4.449999809, "Low": 4.380000114, @@ -2416,7 +2416,7 @@ "Volume": 138801836 }, { - "Date": "2021-01-21 15:00", + "Timestamp": "2021-01-21 15:00", "Open": 4.389999866, "High": 4.429999828, "Low": 4.389999866, @@ -2424,7 +2424,7 @@ "Volume": 139520057 }, { - "Date": "2021-01-22 15:00", + "Timestamp": "2021-01-22 15:00", "Open": 4.400000095, "High": 4.400000095, "Low": 4.320000172, @@ -2432,7 +2432,7 @@ "Volume": 134402252 }, { - "Date": "2021-01-25 15:00", + "Timestamp": "2021-01-25 15:00", "Open": 4.320000172, "High": 4.329999924, "Low": 4.28000021, @@ -2440,7 +2440,7 @@ "Volume": 105412237 }, { - "Date": "2021-01-26 15:00", + "Timestamp": "2021-01-26 15:00", "Open": 4.289999962, "High": 4.329999924, "Low": 4.28000021, @@ -2448,7 +2448,7 @@ "Volume": 108292551 }, { - "Date": "2021-01-27 15:00", + "Timestamp": "2021-01-27 15:00", "Open": 4.289999962, "High": 4.369999886, "Low": 4.289999962, @@ -2456,7 +2456,7 @@ "Volume": 121845089 }, { - "Date": "2021-01-28 15:00", + "Timestamp": "2021-01-28 15:00", "Open": 4.289999962, "High": 4.300000191, "Low": 4.190000057, @@ -2464,7 +2464,7 @@ "Volume": 337418602 }, { - "Date": "2021-01-29 15:00", + "Timestamp": "2021-01-29 15:00", "Open": 4.21999979, "High": 4.21999979, "Low": 4.130000114, @@ -2472,7 +2472,7 @@ "Volume": 258409212 }, { - "Date": "2021-02-01 15:00", + "Timestamp": "2021-02-01 15:00", "Open": 4.150000095, "High": 4.170000076, "Low": 4.110000134, @@ -2480,7 +2480,7 @@ "Volume": 238873654 }, { - "Date": "2021-02-02 15:00", + "Timestamp": "2021-02-02 15:00", "Open": 4.119999886, "High": 4.150000095, "Low": 4.099999905, @@ -2488,7 +2488,7 @@ "Volume": 209954937 }, { - "Date": "2021-02-03 15:00", + "Timestamp": "2021-02-03 15:00", "Open": 4.099999905, "High": 4.110000134, "Low": 4.039999962, @@ -2496,7 +2496,7 @@ "Volume": 192374742 }, { - "Date": "2021-02-04 15:00", + "Timestamp": "2021-02-04 15:00", "Open": 4.070000172, "High": 4.150000095, "Low": 4.039999962, @@ -2504,7 +2504,7 @@ "Volume": 255367422 }, { - "Date": "2021-02-05 15:00", + "Timestamp": "2021-02-05 15:00", "Open": 4.070000172, "High": 4.190000057, "Low": 4.050000191, @@ -2512,7 +2512,7 @@ "Volume": 636100362 }, { - "Date": "2021-02-08 15:00", + "Timestamp": "2021-02-08 15:00", "Open": 4.150000095, "High": 4.150000095, "Low": 4.059999943, @@ -2520,7 +2520,7 @@ "Volume": 364780069 }, { - "Date": "2021-02-09 15:00", + "Timestamp": "2021-02-09 15:00", "Open": 4.079999924, "High": 4.119999886, "Low": 4.059999943, @@ -2528,7 +2528,7 @@ "Volume": 252588058 }, { - "Date": "2021-02-10 15:00", + "Timestamp": "2021-02-10 15:00", "Open": 4.099999905, "High": 4.159999847, "Low": 4.079999924, @@ -2536,7 +2536,7 @@ "Volume": 248939365 }, { - "Date": "2021-02-18 15:00", + "Timestamp": "2021-02-18 15:00", "Open": 4.170000076, "High": 4.21999979, "Low": 4.150000095, @@ -2544,7 +2544,7 @@ "Volume": 314925622 }, { - "Date": "2021-02-19 15:00", + "Timestamp": "2021-02-19 15:00", "Open": 4.179999828, "High": 4.260000229, "Low": 4.170000076, @@ -2552,7 +2552,7 @@ "Volume": 302803021 }, { - "Date": "2021-02-22 15:00", + "Timestamp": "2021-02-22 15:00", "Open": 4.260000229, "High": 4.329999924, "Low": 4.239999771, @@ -2560,7 +2560,7 @@ "Volume": 354856473 }, { - "Date": "2021-02-23 15:00", + "Timestamp": "2021-02-23 15:00", "Open": 4.269999981, "High": 4.320000172, "Low": 4.25, @@ -2568,7 +2568,7 @@ "Volume": 291327120 }, { - "Date": "2021-02-24 15:00", + "Timestamp": "2021-02-24 15:00", "Open": 4.260000229, "High": 4.300000191, "Low": 4.210000038, @@ -2576,7 +2576,7 @@ "Volume": 272271388 }, { - "Date": "2021-02-25 15:00", + "Timestamp": "2021-02-25 15:00", "Open": 4.260000229, "High": 4.329999924, "Low": 4.230000019, @@ -2584,7 +2584,7 @@ "Volume": 353270608 }, { - "Date": "2021-02-26 15:00", + "Timestamp": "2021-02-26 15:00", "Open": 4.260000229, "High": 4.289999962, "Low": 4.21999979, @@ -2592,7 +2592,7 @@ "Volume": 303420663 }, { - "Date": "2021-03-01 15:00", + "Timestamp": "2021-03-01 15:00", "Open": 4.25, "High": 4.260000229, "Low": 4.21999979, @@ -2600,7 +2600,7 @@ "Volume": 171659937 }, { - "Date": "2021-03-02 15:00", + "Timestamp": "2021-03-02 15:00", "Open": 4.25, "High": 4.269999981, "Low": 4.199999809, @@ -2608,7 +2608,7 @@ "Volume": 204390011 }, { - "Date": "2021-03-03 15:00", + "Timestamp": "2021-03-03 15:00", "Open": 4.21999979, "High": 4.309999943, "Low": 4.210000038, @@ -2616,7 +2616,7 @@ "Volume": 324998052 }, { - "Date": "2021-03-04 15:00", + "Timestamp": "2021-03-04 15:00", "Open": 4.28000021, "High": 4.320000172, "Low": 4.260000229, @@ -2624,7 +2624,7 @@ "Volume": 302154537 }, { - "Date": "2021-03-05 15:00", + "Timestamp": "2021-03-05 15:00", "Open": 4.289999962, "High": 4.329999924, "Low": 4.260000229, @@ -2632,7 +2632,7 @@ "Volume": 280188294 }, { - "Date": "2021-03-08 15:00", + "Timestamp": "2021-03-08 15:00", "Open": 4.329999924, "High": 4.369999886, "Low": 4.309999943, @@ -2640,7 +2640,7 @@ "Volume": 279702805 }, { - "Date": "2021-03-09 15:00", + "Timestamp": "2021-03-09 15:00", "Open": 4.329999924, "High": 4.329999924, "Low": 4.239999771, @@ -2648,7 +2648,7 @@ "Volume": 235903429 }, { - "Date": "2021-03-10 15:00", + "Timestamp": "2021-03-10 15:00", "Open": 4.269999981, "High": 4.289999962, "Low": 4.239999771, @@ -2656,7 +2656,7 @@ "Volume": 169045868 }, { - "Date": "2021-03-11 15:00", + "Timestamp": "2021-03-11 15:00", "Open": 4.260000229, "High": 4.309999943, "Low": 4.21999979, @@ -2664,7 +2664,7 @@ "Volume": 243095346 }, { - "Date": "2021-03-12 15:00", + "Timestamp": "2021-03-12 15:00", "Open": 4.289999962, "High": 4.349999905, "Low": 4.269999981, @@ -2672,7 +2672,7 @@ "Volume": 300471167 }, { - "Date": "2021-03-15 15:00", + "Timestamp": "2021-03-15 15:00", "Open": 4.340000153, "High": 4.409999847, "Low": 4.329999924, @@ -2680,7 +2680,7 @@ "Volume": 314736665 }, { - "Date": "2021-03-16 15:00", + "Timestamp": "2021-03-16 15:00", "Open": 4.380000114, "High": 4.409999847, "Low": 4.369999886, @@ -2688,7 +2688,7 @@ "Volume": 212190818 }, { - "Date": "2021-03-17 15:00", + "Timestamp": "2021-03-17 15:00", "Open": 4.380000114, "High": 4.400000095, "Low": 4.340000153, @@ -2696,7 +2696,7 @@ "Volume": 180738804 }, { - "Date": "2021-03-18 15:00", + "Timestamp": "2021-03-18 15:00", "Open": 4.360000134, "High": 4.389999866, "Low": 4.349999905, @@ -2704,7 +2704,7 @@ "Volume": 122488225 }, { - "Date": "2021-03-19 15:00", + "Timestamp": "2021-03-19 15:00", "Open": 4.349999905, "High": 4.360000134, "Low": 4.300000191, @@ -2712,7 +2712,7 @@ "Volume": 173591254 }, { - "Date": "2021-03-22 15:00", + "Timestamp": "2021-03-22 15:00", "Open": 4.320000172, "High": 4.369999886, "Low": 4.309999943, @@ -2720,7 +2720,7 @@ "Volume": 156250662 }, { - "Date": "2021-03-23 15:00", + "Timestamp": "2021-03-23 15:00", "Open": 4.369999886, "High": 4.369999886, "Low": 4.329999924, @@ -2728,7 +2728,7 @@ "Volume": 119699501 }, { - "Date": "2021-03-24 15:00", + "Timestamp": "2021-03-24 15:00", "Open": 4.340000153, "High": 4.349999905, "Low": 4.320000172, diff --git a/tests/observe/Program.cs b/tests/observe/Program.cs deleted file mode 100644 index f7c7d0511..000000000 --- a/tests/observe/Program.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Alpaca.Markets; -using Skender.Stock.Indicators; - -namespace ObserveStreaming; - -internal class Program -{ - private static async Task Main(string[] args) - { - if (args.Length != 0) - { - Console.WriteLine(args); - } - - QuoteStream quoteStream = new(); - await quoteStream.SubscribeToQuotes("BTC/USD"); - } -} - -public class QuoteStream -{ - private readonly string alpacaApiKey = Environment.GetEnvironmentVariable("ALPACA_KEY"); - private readonly string alpacaSecret = Environment.GetEnvironmentVariable("ALPACA_SECRET"); - - internal QuoteStream() - { - if (string.IsNullOrEmpty(alpacaApiKey)) - { - throw new ArgumentNullException( - alpacaApiKey, - $"API KEY missing, use `setx ALPACA_KEY \"MY_ALPACA_KEY\"` to set."); - } - - if (string.IsNullOrEmpty(alpacaSecret)) - { - throw new ArgumentNullException( - alpacaSecret, - $"API SECRET missing, use `setx ALPACA_SECRET \"MY_ALPACA_SECRET\"` to set."); - } - } - - public async Task SubscribeToQuotes(string symbol) - { - Console.WriteLine("Press any key to exit the process..."); - Console.WriteLine("PLEASE WAIT. QUOTES ARRIVE EVERY MINUTE."); - - if (string.IsNullOrEmpty(alpacaApiKey)) - { - throw new ArgumentNullException(alpacaApiKey); - } - - if (string.IsNullOrEmpty(alpacaSecret)) - { - throw new ArgumentNullException(alpacaSecret); - } - - // initialize our quote provider and a few subscribers - QuoteProvider provider = new(); - - EmaObserver ema = provider.GetEma(14); - SmaObserver sma = provider.GetSma(5); - EmaObserver emaChain = provider - .Use(CandlePart.HL2) - .GetEma(10); - - // connect to Alpaca websocket - SecretKey secretKey = new(alpacaApiKey, alpacaSecret); - - IAlpacaCryptoStreamingClient client - = Environments - .Paper - .GetAlpacaCryptoStreamingClient(secretKey); - - await client.ConnectAndAuthenticateAsync(); - - // TODO: is this needed? - AutoResetEvent[] waitObjects = [new AutoResetEvent(false)]; - - IAlpacaDataSubscription quoteSubscription - = client.GetMinuteBarSubscription(symbol); - - await client.SubscribeAsync(quoteSubscription); - - // console display header - Console.WriteLine("A new quote will be shown when they arrive every minute."); - Console.WriteLine("PLEASE WAIT > 8 MINUTES BEFORE EXITING TO SEE ALL 4 INDICATORS CALCULATED."); - Console.WriteLine("Press any key to EXIT the process and to see results."); - Console.WriteLine(); - - Console.WriteLine("Date Close price SMA(3) EMA(5) EMA(7,HL2) SMA/EMA(8)"); - Console.WriteLine("----------------------------------------------------------------------------------"); - - // handle new quotes - quoteSubscription.Received += (q) => - { - // add to our provider - provider.Add(new Quote - { - Date = q.TimeUtc, - Open = q.Open, - High = q.High, - Low = q.Low, - Close = q.Close, - Volume = q.Volume - }); - - Console.WriteLine($"{q.Symbol} {q.TimeUtc:s} ${q.Close:N2} | {q.TradeCount} trades"); - }; - - await client.SubscribeAsync(quoteSubscription); - - // to stop watching on key press - Console.ReadKey(); - - // end observation - provider.EndTransmission(); - - // close WebSocket - await client.UnsubscribeAsync(quoteSubscription); - await client.DisconnectAsync(); - } -} diff --git a/tests/observe/README.md b/tests/observe/README.md deleted file mode 100644 index f53a331ac..000000000 --- a/tests/observe/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Prerequisite steps - -To get connected to the WebSocket in this console test project, you have to add your own Alpaca key and secret information to your local environment variables. - -Run the following command line items to set, after replacing the secret and key values. - -```bash -setx AlpacaApiKey "ALPACA_API_KEY" -setx AlpacaSecret "ALPACA_SECRET" -``` diff --git a/tests/other/Convergence.Tests.cs b/tests/other/Convergence.Tests.cs index cace80bc0..541bb7cca 100644 --- a/tests/other/Convergence.Tests.cs +++ b/tests/other/Convergence.Tests.cs @@ -1,7 +1,7 @@ -namespace Tests.Convergence; +namespace Behavioral; [TestClass] -public class ConvergenceTests : TestBase +public class Convergence : TestBase { private static readonly int[] QuotesQuantities = [5, 14, 28, 40, 50, 75, 100, 110, 120, 130, 140, 150, 160, 175, 200, 250, 350, 500, 600, 700, 800, 900, 1000]; @@ -11,13 +11,11 @@ public void Adx() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetAdx(14); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToAdx(); - AdxResult l = r.LastOrDefault(); - Console.WriteLine( - "ADX(14) on {0:d} with {1,4} historical quotes: {2:N8}", - l.Date, quotes.Count(), l.Adx); + AdxResult l = r[^1]; + Console.WriteLine($"ADX(14) on {l.Timestamp:d} with {qts.Count,4} historical qts: {l.Adx:N8}"); } } @@ -26,13 +24,13 @@ public void Alligator() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetAlligator(); + IReadOnlyList quotes = Data.GetLongish(qty); + IReadOnlyList r = quotes.ToAlligator(); - AlligatorResult l = r.LastOrDefault(); + AlligatorResult l = r[^1]; Console.WriteLine( "ALLIGATOR(13,8,5) on {0:d} with {1,4} periods: Jaw {2:N8}", - l.Date, quotes.Count(), l.Jaw); + l.Timestamp, quotes.Count, l.Jaw); } } @@ -41,13 +39,11 @@ public void Atr() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetAtr(14); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToAtr(); - AtrResult l = r.LastOrDefault(); - Console.WriteLine( - "ATR(14) on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Atr); + AtrResult l = r[^1]; + Console.WriteLine($"ATR(14) on {l.Timestamp:d} with {qts.Count,4} periods: {l.Atr:N8}"); } } @@ -56,13 +52,11 @@ public void ChaikinOsc() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetChaikinOsc(); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToChaikinOsc(); - ChaikinOscResult l = r.LastOrDefault(); - Console.WriteLine( - "CHAIKIN OSC on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Oscillator); + ChaikinOscResult l = r[^1]; + Console.WriteLine($"CHAIKIN OSC on {l.Timestamp:d} with {qts.Count,4} periods: {l.Oscillator:N8}"); } } @@ -71,13 +65,11 @@ public void ConnorsRsi() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetConnorsRsi(3, 2, 10); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToConnorsRsi(3, 2, 10); - ConnorsRsiResult l = r.LastOrDefault(); - Console.WriteLine( - "CRSI on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.ConnorsRsi); + ConnorsRsiResult l = r[^1]; + Console.WriteLine($"CRSI on {l.Timestamp:d} with {qts.Count,4} periods: {l.ConnorsRsi:N8}"); } } @@ -86,13 +78,11 @@ public void Dema() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetDema(15); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToDema(15); - DemaResult l = r.LastOrDefault(); - Console.WriteLine( - "DEMA(15) on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Dema); + DemaResult l = r[^1]; + Console.WriteLine($"DEMA(15) on {l.Timestamp:d} with {qts.Count,4} periods: {l.Dema:N8}"); } } @@ -101,13 +91,11 @@ public void Dynamic() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetDynamic(100); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToDynamic(100); - DynamicResult l = r.LastOrDefault(); - Console.WriteLine( - "DYNAMIC(15) on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Dynamic); + DynamicResult l = r[^1]; + Console.WriteLine($"DYNAMIC(15) on {l.Timestamp:d} with {qts.Count,4} periods: {l.Dynamic:N8}"); } } @@ -116,13 +104,11 @@ public void Ema() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetEma(15); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToEma(15); - EmaResult l = r.LastOrDefault(); - Console.WriteLine( - "EMA(15) on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Ema); + EmaResult l = r[^1]; + Console.WriteLine($"EMA(15) on {l.Timestamp:d} with {qts.Count,4} periods: {l.Ema:N8}"); } } @@ -131,13 +117,11 @@ public void FisherTransform() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetFisherTransform(10); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToFisherTransform(); - FisherTransformResult l = r.LastOrDefault(); - Console.WriteLine( - "FT(10) on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Fisher); + FisherTransformResult l = r[^1]; + Console.WriteLine($"FT(10) on {l.Timestamp:d} with {qts.Count,4} periods: {l.Fisher:N8}"); } } @@ -146,13 +130,13 @@ public void Gator() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetGator(); + IReadOnlyList quotes = Data.GetLongish(qty); + IReadOnlyList r = quotes.ToGator(); - GatorResult l = r.LastOrDefault(); + GatorResult l = r[^1]; Console.WriteLine( "GATOR() on {0:d} with {1,4} periods: Upper {2:N8} Lower {3:N8}", - l.Date, quotes.Count(), l.Upper, l.Lower); + l.Timestamp, quotes.Count, l.Upper, l.Lower); } } @@ -161,13 +145,11 @@ public void HtTrendline() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetHtTrendline(); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToHtTrendline(); - HtlResult l = r.LastOrDefault(); - Console.WriteLine( - "HTL on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Trendline); + HtlResult l = r[^1]; + Console.WriteLine($"HTL on {l.Timestamp:d} with {qts.Count,4} periods: {l.Trendline:N8}"); } } @@ -176,13 +158,11 @@ public void Kama() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetKama(10); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToKama(); - KamaResult l = r.LastOrDefault(); - Console.WriteLine( - "KAMA(10) on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Kama); + KamaResult l = r[^1]; + Console.WriteLine($"KAMA(10) on {l.Timestamp:d} with {qts.Count,4} periods: {l.Kama:N8}"); } } @@ -191,13 +171,11 @@ public void Keltner() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetKeltner(100); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToKeltner(100); - KeltnerResult l = r.LastOrDefault(); - Console.WriteLine( - "KC-UP on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.UpperBand); + KeltnerResult l = r[^1]; + Console.WriteLine($"KC-UP on {l.Timestamp:d} with {qts.Count,4} periods: {l.UpperBand:N8}"); } } @@ -206,13 +184,11 @@ public void Macd() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(15 + qty); - IEnumerable r = quotes.GetMacd(); + IReadOnlyList qts = Data.GetLongish(15 + qty); + IReadOnlyList r = qts.ToMacd(); - MacdResult l = r.LastOrDefault(); - Console.WriteLine( - "MACD on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Macd); + MacdResult l = r[^1]; + Console.WriteLine($"MACD on {l.Timestamp:d} with {qts.Count,4} periods: {l.Macd:N8}"); } } @@ -221,13 +197,11 @@ public void Mama() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetMama(); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToMama(); - MamaResult l = r.LastOrDefault(); - Console.WriteLine( - "MAMA on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Mama); + MamaResult l = r[^1]; + Console.WriteLine($"MAMA on {l.Timestamp:d} with {qts.Count,4} periods: {l.Mama:N8}"); } } @@ -236,13 +210,11 @@ public void Pmo() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetPmo(); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToPmo(); - PmoResult l = r.LastOrDefault(); - Console.WriteLine( - "PMO on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Pmo); + PmoResult l = r[^1]; + Console.WriteLine($"PMO on {l.Timestamp:d} with {qts.Count,4} periods: {l.Pmo:N8}"); } } @@ -251,13 +223,11 @@ public void Pvo() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetPvo(); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToPvo(); - PvoResult l = r.LastOrDefault(); - Console.WriteLine( - "PVO on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Pvo); + PvoResult l = r[^1]; + Console.WriteLine($"PVO on {l.Timestamp:d} with {qts.Count,4} periods: {l.Pvo:N8}"); } } @@ -266,13 +236,11 @@ public void Rsi() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetRsi(14); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToRsi(); - RsiResult l = r.LastOrDefault(); - Console.WriteLine( - "RSI(14) on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Rsi); + RsiResult l = r[^1]; + Console.WriteLine($"RSI(14) on {l.Timestamp:d} with {qts.Count,4} periods: {l.Rsi:N8}"); } } @@ -281,13 +249,11 @@ public void Smi() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetDefault(qty); - IEnumerable r = quotes.GetSmi(14, 20, 5, 3); + IReadOnlyList qts = Data.GetDefault(qty); + IReadOnlyList r = qts.ToSmi(14, 20, 5); - SmiResult l = r.LastOrDefault(); - Console.WriteLine( - "SMI on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Smi); + SmiResult l = r[^1]; + Console.WriteLine($"SMI on {l.Timestamp:d} with {qts.Count,4} periods: {l.Smi:N8}"); } } @@ -296,13 +262,11 @@ public void Smma() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetSmma(15); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToSmma(15); - SmmaResult l = r.LastOrDefault(); - Console.WriteLine( - "SMMA(15) on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Smma); + SmmaResult l = r[^1]; + Console.WriteLine($"SMMA(15) on {l.Timestamp:d} with {qts.Count,4} periods: {l.Smma:N8}"); } } @@ -311,13 +275,11 @@ public void StarcBands() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetStarcBands(20); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToStarcBands(20); - StarcBandsResult l = r.LastOrDefault(); - Console.WriteLine( - "STARC UPPER on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.UpperBand); + StarcBandsResult l = r[^1]; + Console.WriteLine($"STARC UPPER on {l.Timestamp:d} with {qts.Count,4} periods: {l.UpperBand:N8}"); } } @@ -326,13 +288,11 @@ public void StochRsi() { foreach (int qty in QuotesQuantities.Where(x => x <= 502)) { - IEnumerable quotes = TestData.GetDefault(qty); - IEnumerable r = quotes.GetStochRsi(14, 14, 3, 1); + IReadOnlyList qts = Data.GetDefault(qty); + IReadOnlyList r = qts.ToStochRsi(14, 14, 3); - StochRsiResult l = r.LastOrDefault(); - Console.WriteLine( - "SRSI on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.StochRsi); + StochRsiResult l = r[^1]; + Console.WriteLine($"SRSI on {l.Timestamp:d} with {qts.Count,4} periods: {l.StochRsi:N8}"); } } @@ -341,13 +301,11 @@ public void T3() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetT3(20); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToT3(20); - T3Result l = r.LastOrDefault(); - Console.WriteLine( - "T3 on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.T3); + T3Result l = r[^1]; + Console.WriteLine($"T3 on {l.Timestamp:d} with {qts.Count,4} periods: {l.T3:N8}"); } } @@ -356,13 +314,11 @@ public void Tema() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetTema(15); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToTema(15); - TemaResult l = r.LastOrDefault(); - Console.WriteLine( - "TEMA on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Tema); + TemaResult l = r[^1]; + Console.WriteLine($"TEMA on {l.Timestamp:d} with {qts.Count,4} periods: {l.Tema:N8}"); } } @@ -371,13 +327,11 @@ public void Trix() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetTrix(15); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToTrix(15); - TrixResult l = r.LastOrDefault(); - Console.WriteLine( - "TRIX on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Trix); + TrixResult l = r[^1]; + Console.WriteLine($"TRIX on {l.Timestamp:d} with {qts.Count,4} periods: {l.Trix:N8}"); } } @@ -386,13 +340,11 @@ public void Tsi() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(20 + qty); - IEnumerable r = quotes.GetTsi(); + IReadOnlyList qts = Data.GetLongish(20 + qty); + IReadOnlyList r = qts.ToTsi(); - TsiResult l = r.LastOrDefault(); - Console.WriteLine( - "TSI on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Tsi); + TsiResult l = r[^1]; + Console.WriteLine($"TSI on {l.Timestamp:d} with {qts.Count,4} periods: {l.Tsi:N8}"); } } @@ -401,13 +353,11 @@ public void Vortex() { foreach (int qty in QuotesQuantities) { - IEnumerable quotes = TestData.GetLongish(qty); - IEnumerable r = quotes.GetVortex(14); + IReadOnlyList qts = Data.GetLongish(qty); + IReadOnlyList r = qts.ToVortex(14); - VortexResult l = r.LastOrDefault(); - Console.WriteLine( - "VI+ on {0:d} with {1,4} periods: {2:N8}", - l.Date, quotes.Count(), l.Pvi); + VortexResult l = r[^1]; + Console.WriteLine($"VI+ on {l.Timestamp:d} with {qts.Count,4} periods: {l.Pvi:N8}"); } } } diff --git a/tests/other/Custom.Indicator.Tests.cs b/tests/other/Custom.Indicator.Tests.cs new file mode 100644 index 000000000..133d4d488 --- /dev/null +++ b/tests/other/Custom.Indicator.Tests.cs @@ -0,0 +1,162 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using Sut; + +namespace Customization; + +// CUSTOM INDICATORS + +[TestClass] +public class CustomIndicators +{ + private static readonly CultureInfo EnglishCulture = new("en-US", false); + + private static readonly IReadOnlyList quotes = Data.GetDefault(); + private static readonly IReadOnlyList badQuotes = Data.GetBad(); + private static readonly IReadOnlyList noquotes = []; + private static readonly IReadOnlyList onequote = Data.GetDefault(1); + + [TestMethod] + public void Standard() + { + IReadOnlyList results = quotes + .GetIndicator(20); + + // proper quantities + Assert.AreEqual(502, results.Count); + Assert.AreEqual(483, results.Count(x => x.Sma != null)); + + // sample values + Assert.IsNull(results[18].Sma); + Assert.AreEqual(214.5250, Math.Round(results[19].Sma.Value, 4)); + Assert.AreEqual(215.0310, Math.Round(results[24].Sma.Value, 4)); + Assert.AreEqual(234.9350, Math.Round(results[149].Sma.Value, 4)); + Assert.AreEqual(255.5500, Math.Round(results[249].Sma.Value, 4)); + Assert.AreEqual(251.8600, Math.Round(results[501].Sma.Value, 4)); + } + + [TestMethod] + public void CandlePartOpen() + { + IReadOnlyList results = quotes + .Use(CandlePart.Open) + .GetIndicator(20); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(483, results.Count(x => x.Sma != null)); + + // sample values + Assert.IsNull(results[18].Sma); + Assert.AreEqual(214.3795, Math.Round(results[19].Sma.Value, 4)); + Assert.AreEqual(214.9535, Math.Round(results[24].Sma.Value, 4)); + Assert.AreEqual(234.8280, Math.Round(results[149].Sma.Value, 4)); + Assert.AreEqual(255.6915, Math.Round(results[249].Sma.Value, 4)); + Assert.AreEqual(253.1725, Math.Round(results[501].Sma.Value, 4)); + } + + [TestMethod] + public void CandlePartVolume() + { + IReadOnlyList results = quotes + .Use(CandlePart.Volume) + .GetIndicator(20); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(483, results.Count(x => x.Sma != null)); + + // sample values + CustomReusable r24 = results[24]; + Assert.AreEqual(77293768.2, r24.Sma); + + CustomReusable r290 = results[290]; + Assert.AreEqual(157958070.8, r290.Sma); + + CustomReusable r501 = results[501]; + Assert.AreEqual(DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", EnglishCulture), r501.Timestamp); + Assert.AreEqual(163695200, r501.Sma); + } + + [TestMethod] + public void Chainor() + { + IReadOnlyList results = quotes + .GetIndicator(10) + .ToEma(10); + + Assert.AreEqual(502, results.Count); + Assert.AreEqual(484, results.Count(x => x.Ema != null)); + } + + [TestMethod] + public void QuoteToSortedList() + { + IReadOnlyList mismatch = Data.GetMismatch(); + + IReadOnlyList h = mismatch.ToSortedList(); + + // proper quantities + Assert.AreEqual(502, h.Count); + + // check first date + DateTime firstDate = DateTime.ParseExact("01/18/2016", "MM/dd/yyyy", EnglishCulture); + Assert.AreEqual(firstDate, h[0].Timestamp); + + // check last date + DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", EnglishCulture); + Assert.AreEqual(lastDate, h[^1].Timestamp); + + // spot check an out of sequence date + DateTime spotDate = DateTime.ParseExact("03/16/2017", "MM/dd/yyyy", EnglishCulture); + Assert.AreEqual(spotDate, h[50].Timestamp); + } + + [TestMethod] + public void NaN() + { + IReadOnlyList r = Data.GetBtcUsdNan() + .GetIndicator(50); + + Assert.AreEqual(0, r.Count(x => x.Sma is double.NaN)); + } + + [TestMethod] + public void BadData() + { + IReadOnlyList r = badQuotes + .GetIndicator(15); + + Assert.AreEqual(502, r.Count); + Assert.AreEqual(0, r.Count(x => x.Sma is double.NaN)); + } + + [TestMethod] + public void NoQuotesExist() + { + IReadOnlyList r0 = noquotes + .GetIndicator(5); + + Assert.AreEqual(0, r0.Count); + + IReadOnlyList r1 = onequote + .GetIndicator(5); + + Assert.AreEqual(1, r1.Count); + } + + [TestMethod] + public void Removed() + { + IReadOnlyList results = quotes + .GetIndicator(20) + .RemoveWarmupPeriods(19); + + Assert.AreEqual(502 - 19, results.Count); + Assert.AreEqual(251.8600, Math.Round(results[^1].Sma.Value, 4)); + } + + // bad lookback period + [TestMethod] + public void Exceptions() + => Assert.ThrowsException(() + => quotes.GetIndicator(0)); +} diff --git a/tests/other/Custom.Quotes.Tests.cs b/tests/other/Custom.Quotes.Tests.cs new file mode 100644 index 000000000..3b50b542d --- /dev/null +++ b/tests/other/Custom.Quotes.Tests.cs @@ -0,0 +1,178 @@ +using System.Globalization; +using Sut; + +namespace Customization; + +// CUSTOM QUOTES + +[TestClass] +public class CustomQuotes +{ + private static readonly CultureInfo EnglishCulture + = new("en-US", false); + + private static readonly DateTime EvalDate + = DateTime.ParseExact( + "12/31/2018", "MM/dd/yyyy", EnglishCulture); + + private static readonly IReadOnlyList quotes = Data.GetDefault(); + private static readonly IReadOnlyList intraday = Data.GetIntraday(); + + [TestMethod] + public void CustomQuoteSeries() + { + List myGenericHistory = quotes + .Select(x => new CustomQuote { + CloseDate = x.Timestamp, + Open = x.Open, + High = x.High, + Low = x.Low, + CloseValue = x.Close, + Volume = x.Volume, + MyOtherProperty = 123456 + }) + .ToList(); + + IReadOnlyList results = Ema.ToEma(myGenericHistory, 20); + + // proper quantities + Assert.AreEqual(502, results.Count); + Assert.AreEqual(483, results.Count(x => x.Ema != null)); + + // sample values + EmaResult r1 = results[501]; + Assert.AreEqual(249.3519m, Math.Round((decimal)r1.Ema, 4)); + + EmaResult r2 = results[249]; + Assert.AreEqual(255.3873m, Math.Round((decimal)r2.Ema, 4)); + + EmaResult r3 = results[29]; + Assert.AreEqual(216.6228m, Math.Round((decimal)r3.Ema, 4)); + } + + [TestMethod] + public void CustomQuoteEquality() + { + CustomQuote q1 = new() { + CloseDate = EvalDate, + Open = 1m, + High = 1m, + Low = 1m, + CloseValue = 1m, + Volume = 100 + }; + + CustomQuote q2 = new() { + CloseDate = EvalDate, + Open = 1m, + High = 1m, + Low = 1m, + CloseValue = 1m, + Volume = 100 + }; + + CustomQuote q3 = new() { + CloseDate = EvalDate, + Open = 1m, + High = 1m, + Low = 1m, + CloseValue = 2m, + Volume = 99 + }; + + Assert.IsTrue(Equals(q1, q2)); + Assert.IsFalse(Equals(q1, q3)); + + Assert.IsTrue(q1.Equals(q2)); + Assert.IsFalse(q1.Equals(q3)); + + Assert.IsTrue(q1 == q2, "== operator"); + Assert.IsFalse(q1 == q3, "== operator"); + + Assert.IsFalse(q1 != q2, "!= operator"); + Assert.IsTrue(q1 != q3, "!= operator"); + } + + [TestMethod] + public void CustomQuoteAggregate() + { + List myGenericHistory = intraday + .Select(x => new CustomQuote { + CloseDate = x.Timestamp, + Open = x.Open, + High = x.High, + Low = x.Low, + CloseValue = x.Close, + Volume = x.Volume, + MyOtherProperty = 123456 + }) + .ToList(); + + IReadOnlyList quotesList = myGenericHistory + .Aggregate(PeriodSize.TwoHours); + + // proper quantities + Assert.AreEqual(20, quotesList.Count); + + // sample values + Quote r19 = quotesList[19]; + Assert.AreEqual(369.04m, r19.Low); + } + + [TestMethod] + public void CustomQuoteAggregateTimeSpan() + { + List myGenericHistory = intraday + .Select(x => new CustomQuote { + CloseDate = x.Timestamp, + Open = x.Open, + High = x.High, + Low = x.Low, + CloseValue = x.Close, + Volume = x.Volume, + MyOtherProperty = 123456 + }) + .ToList(); + + IReadOnlyList quotesList = myGenericHistory + .Aggregate(TimeSpan.FromHours(2)); + + // proper quantities + Assert.AreEqual(20, quotesList.Count); + + // sample values + Quote r19 = quotesList[19]; + Assert.AreEqual(369.04m, r19.Low); + } + + [TestMethod] + public void CustomQuoteInheritedSeries() + { + List myGenericHistory = quotes + .Select(x => new CustomQuoteInherited( + CloseDate: x.Timestamp, + Open: x.Open, + High: x.High, + Low: x.Low, + Close: x.Close, + Volume: x.Volume, + MyOtherProperty: 123456)) + .ToList(); + + IReadOnlyList results = myGenericHistory.ToEma(20); + + // proper quantities + Assert.AreEqual(502, results.Count); + Assert.AreEqual(483, results.Count(x => x.Ema != null)); + + // sample values + EmaResult r1 = results[501]; + Assert.AreEqual(249.3519m, Math.Round((decimal)r1.Ema, 4)); + + EmaResult r2 = results[249]; + Assert.AreEqual(255.3873m, Math.Round((decimal)r2.Ema, 4)); + + EmaResult r3 = results[29]; + Assert.AreEqual(216.6228m, Math.Round((decimal)r3.Ema, 4)); + } +} diff --git a/tests/other/Custom.Results.Tests.cs b/tests/other/Custom.Results.Tests.cs new file mode 100644 index 000000000..d7b4c7f60 --- /dev/null +++ b/tests/other/Custom.Results.Tests.cs @@ -0,0 +1,86 @@ +using System.Globalization; +using Sut; + +namespace Customization; + +// CUSTOM RESULTS + +[TestClass] +public class CustomResults +{ + private static readonly CultureInfo EnglishCulture + = new("en-US", false); + + private static readonly IReadOnlyList quotes = Data.GetDefault(); + + [TestMethod] + public void CustomSeriesClass() + { + // can use a derive Indicator class + CustomSeries myIndicator = new() { + Timestamp = DateTime.Now, + Ema = 123.456, + MyProperty = false + }; + + Assert.AreEqual(false, myIndicator.MyProperty); + } + + [TestMethod] + public void CustomSeriesClassLinq() + { + IReadOnlyList emaResults = quotes.ToEma(14); + + // can use a derive Indicator class using Linq + + IEnumerable myIndicatorResults = emaResults + .Where(x => x.Ema != null) + .Select(x => new CustomSeries { + Timestamp = x.Timestamp, + Ema = x.Ema, + MyProperty = false + }); + + Assert.IsTrue(myIndicatorResults.Any()); + } + + [TestMethod] + public void CustomSeriesClassFind() + { + List emaResults + = quotes.ToEma(20).ToList(); + + // can use a derive Indicator class using Linq + + List myIndicatorResults = emaResults + .Where(x => x.Ema != null) + .Select(x => new CustomSeries { + Id = 12345, + Timestamp = x.Timestamp, + Ema = x.Ema, + MyProperty = false + }) + .ToList(); + + Assert.IsTrue(myIndicatorResults.Count > 0); + + // find specific date + DateTime findDate = DateTime.ParseExact( + "2018-12-31", "yyyy-MM-dd", EnglishCulture); + + CustomSeries i = myIndicatorResults.Find(x => x.Timestamp == findDate); + Assert.AreEqual(12345, i.Id); + + 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/CustomIndicator.Tests.cs b/tests/other/CustomIndicator.Tests.cs deleted file mode 100644 index 328909e2f..000000000 --- a/tests/other/CustomIndicator.Tests.cs +++ /dev/null @@ -1,264 +0,0 @@ -using System.Collections.ObjectModel; -using System.Globalization; - -namespace Tests.CustomIndicators; - -public sealed class MyResult : ResultBase, IReusableResult -{ - public MyResult(DateTime date) - { - Date = date; - } - - public double? Sma { get; set; } - - double? IReusableResult.Value => Sma; -} - -public static class CustomIndicator -{ - // SERIES, from TQuote - public static IEnumerable GetIndicator( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTupleCollection(CandlePart.Close) - .CalcIndicator(lookbackPeriods); - - // SERIES, from CHAIN - public static IEnumerable GetIndicator( - this IEnumerable results, - int lookbackPeriods) => results - .ToTupleChainable() - .CalcIndicator(lookbackPeriods) - .SyncIndex(results, SyncType.Prepend); - - // SERIES, from TUPLE - public static IEnumerable GetIndicator( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedCollection() - .CalcIndicator(lookbackPeriods); - - internal static List CalcIndicator( - this Collection<(DateTime, double)> tpList, - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for SMA."); - } - - // initialize - List results = new(tpList.Count); - - // roll through quotes - for (int i = 0; i < tpList.Count; i++) - { - (DateTime date, double _) = tpList[i]; - - MyResult result = new(date); - results.Add(result); - - if (i + 1 >= lookbackPeriods) - { - double sumSma = 0; - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - (DateTime _, double pValue) = tpList[p]; - sumSma += pValue; - } - - result.Sma = (sumSma / lookbackPeriods).NaN2Null(); - } - } - - return results; - } -} - -[TestClass] -public class CustomIndicatorTests -{ - private static readonly CultureInfo EnglishCulture = new("en-US", false); - - internal static readonly IEnumerable quotes = TestData.GetDefault(); - internal static readonly IEnumerable otherQuotes = TestData.GetCompare(); - internal static readonly IEnumerable badQuotes = TestData.GetBad(); - internal static readonly IEnumerable bigQuotes = TestData.GetTooBig(); - internal static readonly IEnumerable maxQuotes = TestData.GetMax(); - internal static readonly IEnumerable longishQuotes = TestData.GetLongish(); - internal static readonly IEnumerable longestQuotes = TestData.GetLongest(); - internal static readonly IEnumerable mismatchQuotes = TestData.GetMismatch(); - internal static readonly IEnumerable noquotes = new List(); - internal static readonly IEnumerable onequote = TestData.GetDefault(1); - internal static readonly IEnumerable randomQuotes = TestData.GetRandom(1000); - internal static readonly IEnumerable zeroesQuotes = TestData.GetZeros(); - internal static readonly IEnumerable<(DateTime, double)> tupleNanny = TestData.GetTupleNaN(); - - [TestMethod] - public void Standard() - { - List results = quotes - .GetIndicator(20) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(483, results.Count(x => x.Sma != null)); - - // sample values - Assert.IsNull(results[18].Sma); - Assert.AreEqual(214.5250, Math.Round(results[19].Sma.Value, 4)); - Assert.AreEqual(215.0310, Math.Round(results[24].Sma.Value, 4)); - Assert.AreEqual(234.9350, Math.Round(results[149].Sma.Value, 4)); - Assert.AreEqual(255.5500, Math.Round(results[249].Sma.Value, 4)); - Assert.AreEqual(251.8600, Math.Round(results[501].Sma.Value, 4)); - } - - [TestMethod] - public void CandlePartOpen() - { - List results = quotes - .Use(CandlePart.Open) - .GetIndicator(20) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(483, results.Count(x => x.Sma != null)); - - // sample values - Assert.IsNull(results[18].Sma); - Assert.AreEqual(214.3795, Math.Round(results[19].Sma.Value, 4)); - Assert.AreEqual(214.9535, Math.Round(results[24].Sma.Value, 4)); - Assert.AreEqual(234.8280, Math.Round(results[149].Sma.Value, 4)); - Assert.AreEqual(255.6915, Math.Round(results[249].Sma.Value, 4)); - Assert.AreEqual(253.1725, Math.Round(results[501].Sma.Value, 4)); - } - - [TestMethod] - public void CandlePartVolume() - { - List results = quotes - .Use(CandlePart.Volume) - .GetIndicator(20) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(483, results.Count(x => x.Sma != null)); - - // sample values - MyResult r24 = results[24]; - Assert.AreEqual(77293768.2, r24.Sma); - - MyResult r290 = results[290]; - Assert.AreEqual(157958070.8, r290.Sma); - - MyResult r501 = results[501]; - Assert.AreEqual(DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", EnglishCulture), r501.Date); - Assert.AreEqual(163695200, r501.Sma); - } - - [TestMethod] - public void Chainor() - { - List results = quotes - .GetIndicator(10) - .GetEma(10) - .ToList(); - - Assert.AreEqual(502, results.Count); - Assert.AreEqual(484, results.Count(x => x.Ema != null)); - } - - [TestMethod] - public void QuoteToSortedList() - { - IEnumerable mismatch = TestData.GetMismatch(); - - Collection h = mismatch.ToSortedCollection(); - - // proper quantities - Assert.AreEqual(502, h.Count); - - // check first date - DateTime firstDate = DateTime.ParseExact("01/18/2016", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(firstDate, h[0].Date); - - // check last date - DateTime lastDate = DateTime.ParseExact("12/31/2018", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(lastDate, h.LastOrDefault().Date); - - // spot check an out of sequence date - DateTime spotDate = DateTime.ParseExact("03/16/2017", "MM/dd/yyyy", EnglishCulture); - Assert.AreEqual(spotDate, h[50].Date); - } - - [TestMethod] - public void TupleNaN() - { - List r = tupleNanny - .GetIndicator(6) - .ToList(); - - Assert.AreEqual(200, r.Count); - Assert.AreEqual(0, r.Count(x => x.Sma is not null and double.NaN)); - } - - [TestMethod] - public void NaN() - { - List r = TestData.GetBtcUsdNan() - .GetIndicator(50) - .ToList(); - - Assert.AreEqual(0, r.Count(x => x.Sma is not null and double.NaN)); - } - - [TestMethod] - public void BadData() - { - List r = badQuotes - .GetIndicator(15) - .ToList(); - - Assert.AreEqual(502, r.Count); - Assert.AreEqual(0, r.Count(x => x.Sma is not null and double.NaN)); - } - - [TestMethod] - public void NoQuotesExist() - { - List r0 = noquotes - .GetIndicator(5) - .ToList(); - - Assert.AreEqual(0, r0.Count); - - List r1 = onequote - .GetIndicator(5) - .ToList(); - - Assert.AreEqual(1, r1.Count); - } - - [TestMethod] - public void Removed() - { - List results = quotes - .GetIndicator(20) - .RemoveWarmupPeriods(19) - .ToList(); - - Assert.AreEqual(502 - 19, results.Count); - Assert.AreEqual(251.8600, Math.Round(results.LastOrDefault().Sma.Value, 4)); - } - - // bad lookback period - [TestMethod] - public void Exceptions() - => Assert.ThrowsException(() - => quotes.GetIndicator(0)); -} diff --git a/tests/other/GlobalUsings.cs b/tests/other/GlobalUsings.cs index 211ba4da6..fc2afe48a 100644 --- a/tests/other/GlobalUsings.cs +++ b/tests/other/GlobalUsings.cs @@ -1,3 +1,4 @@ +global using FluentAssertions; global using Microsoft.VisualStudio.TestTools.UnitTesting; global using Skender.Stock.Indicators; -global using Tests.Common; +global using Test.Data; diff --git a/tests/other/PublicApi.Interface.Tests.cs b/tests/other/PublicApi.Interface.Tests.cs new file mode 100644 index 000000000..9dad62942 --- /dev/null +++ b/tests/other/PublicApi.Interface.Tests.cs @@ -0,0 +1,147 @@ +[assembly: CLSCompliant(true)] +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] + +namespace PublicApi; + +// PUBLIC API (INTERFACES) + +[TestClass] +public class UserInterface +{ + private static readonly IReadOnlyList quotes = Data.GetDefault(); + private static readonly IReadOnlyList quotesBad = Data.GetBad(); + + [TestMethod] + public void QuoteValidation() + { + IReadOnlyList clean = quotes; + + clean.Validate(); + clean.ToSma(6); + clean.ToEma(5); + + IReadOnlyList reverse = quotes + .OrderByDescending(x => x.Timestamp) + .ToList(); + + // has duplicates + InvalidQuotesException dx + = Assert.ThrowsException( + () => quotesBad.Validate()); + + dx.Message.Should().Contain("Duplicate date found"); + + // out of order + InvalidQuotesException sx + = Assert.ThrowsException( + () => reverse.Validate()); + + sx.Message.Should().Contain("Quotes are out of sequence"); + } + + [TestMethod] + public void QuoteValidationReturn() + { + IReadOnlyList h = quotes.Validate(); + + Quote f = h[0]; + Console.WriteLine($"Quote:{f}"); + } + + [TestMethod] + public void StreamMany() // from quote provider + { + /****************************************************** + * Attaches many stream observers to one Quote provider + * for a full sprectrum stream collective. + * + * Currently, it does not include any [direct] chains. + * + * This test covers most of the unusual test cases, like: + * + * - out of order quotes (late arrivals) + * - duplicates, but not to an overflow situation + * + ******************************************************/ + + int length = quotes.Count; + + // setup quote provider + QuoteHub provider = new(); + + // initialize observers + AdlHub adlHub = provider.ToAdl(); + AlligatorHub alligatorHub = provider.ToAlligator(); + AtrHub atrHub = provider.ToAtr(); + AtrStopHub atrStopHub = provider.ToAtrStop(); + EmaHub emaHub = provider.ToEma(20); + QuotePartHub quotePartHub = provider.ToQuotePart(CandlePart.OHL3); + SmaHub smaHub = provider.ToSma(20); + TrHub trHub = provider.ToTr(); + + // emulate adding quotes to provider + for (int i = 0; i < length; i++) + { + // skip one (add later) + if (i == 80) + { + continue; + } + + Quote q = quotes[i]; + provider.Add(q); + + // resend duplicate quotes + if (i is > 100 and < 105) + { + provider.Add(q); + } + } + + // late arrival + provider.Insert(quotes[80]); + + // end all observations + provider.EndTransmission(); + + // get static equivalents for comparison + IReadOnlyList staticAdl = quotes.ToAdl(); + IReadOnlyList staticAtr = quotes.ToAtr(); + IReadOnlyList staticAtrStop = quotes.ToAtrStop(); + IReadOnlyList staticAlligator = quotes.ToAlligator(); + IReadOnlyList staticEma = quotes.ToEma(20); + IReadOnlyList staticQuotePart = quotes.Use(CandlePart.OHL3); + IReadOnlyList staticSma = quotes.ToSma(20); + IReadOnlyList staticTr = quotes.ToTr(); + + // final results should persist in scope + IReadOnlyList streamAdl = adlHub.Results; + IReadOnlyList streamAtr = atrHub.Results; + IReadOnlyList streamAtrStop = atrStopHub.Results; + IReadOnlyList streamAlligator = alligatorHub.Results; + IReadOnlyList streamEma = emaHub.Results; + IReadOnlyList streamQuotePart = quotePartHub.Results; + IReadOnlyList streamSma = smaHub.Results; + IReadOnlyList streamTr = trHub.Results; + + // assert, should be correct length + streamAdl.Should().HaveCount(length); + streamAlligator.Should().HaveCount(length); + streamAtr.Should().HaveCount(length); + streamAtrStop.Should().HaveCount(length); + streamEma.Should().HaveCount(length); + streamQuotePart.Should().HaveCount(length); + streamSma.Should().HaveCount(length); + streamTr.Should().HaveCount(length); + + // assert, should equal static series + streamAdl.Should().BeEquivalentTo(staticAdl); + streamAtr.Should().BeEquivalentTo(staticAtr); + streamAtrStop.Should().BeEquivalentTo(staticAtrStop); + streamAlligator.Should().BeEquivalentTo(staticAlligator); + streamEma.Should().BeEquivalentTo(staticEma); + streamQuotePart.Should().BeEquivalentTo(staticQuotePart); + streamSma.Should().BeEquivalentTo(staticSma); + streamTr.Should().BeEquivalentTo(staticTr); + } +} diff --git a/tests/other/PublicApi.Tests.cs b/tests/other/PublicApi.Tests.cs deleted file mode 100644 index 7ffe5f564..000000000 --- a/tests/other/PublicApi.Tests.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System.Globalization; - -[assembly: CLSCompliant(true)] -namespace Tests.PublicApi; - -internal sealed class MyQuote : Quote -{ - public bool MyProperty { get; set; } - public decimal? MyClose { get; set; } -} - -internal sealed class MyEma : ResultBase -{ - public int Id { get; set; } - public bool MyProperty { get; set; } - public double? Ema { get; set; } -} - -internal sealed class MyGenericQuote : IQuote -{ - // required base properties - DateTime ISeries.Date => CloseDate; - public decimal Open { get; set; } - public decimal High { get; set; } - public decimal Low { get; set; } - decimal IQuote.Close => CloseValue; - public decimal Volume { get; set; } - - // custom properties - public int MyOtherProperty { get; set; } - public DateTime CloseDate { get; set; } - public decimal CloseValue { get; set; } -} - -[TestClass] -public class PublicClassTests -{ - internal static readonly CultureInfo EnglishCulture = new("en-US", false); - - [TestMethod] - public void ValidateHistory() - { - IEnumerable quotes = TestData.GetDefault(); - - quotes.Validate(); - quotes.GetSma(6); - quotes.GetSma(5); - } - - [TestMethod] - public void ReadQuoteClass() - { - IEnumerable quotes = TestData.GetDefault(); - IEnumerable h = quotes.Validate(); - - Quote f = h.FirstOrDefault(); - Console.WriteLine("Date:{0},Close:{1}", f.Date, f.Close); - } - - [TestMethod] - public void DerivedQuoteClass() - { - // can use a derive Quote class - MyQuote myQuote = new() { - Date = DateTime.Now, - MyProperty = true - }; - - Assert.AreEqual(true, myQuote.MyProperty); - } - - [TestMethod] - public void DerivedQuoteClassLinq() - { - IEnumerable quotes = TestData.GetDefault(); - quotes = quotes.Validate(); - - // can use a derive Quote class using Linq - - IEnumerable myHistory = quotes - .Select(x => new MyQuote { - Date = x.Date, - MyClose = x.Close, - MyProperty = false - }); - - Assert.IsTrue(myHistory.Any()); - } - - [TestMethod] - public void CustomQuoteClass() - { - List myGenericHistory = TestData.GetDefault() - .Select(x => new MyGenericQuote { - CloseDate = x.Date, - Open = x.Open, - High = x.High, - Low = x.Low, - CloseValue = x.Close, - Volume = x.Volume, - MyOtherProperty = 123456 - }) - .ToList(); - - List results = myGenericHistory.GetEma(20) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(483, results.Count(x => x.Ema != null)); - - // sample values - EmaResult r1 = results[501]; - Assert.AreEqual(249.3519m, Math.Round((decimal)r1.Ema, 4)); - - EmaResult r2 = results[249]; - Assert.AreEqual(255.3873m, Math.Round((decimal)r2.Ema, 4)); - - EmaResult r3 = results[29]; - Assert.AreEqual(216.6228m, Math.Round((decimal)r3.Ema, 4)); - } - - [TestMethod] - public void CustomQuoteAggregate() - { - List myGenericHistory = TestData.GetIntraday() - .Select(x => new MyGenericQuote { - CloseDate = x.Date, - Open = x.Open, - High = x.High, - Low = x.Low, - CloseValue = x.Close, - Volume = x.Volume, - MyOtherProperty = 123456 - }) - .ToList(); - - List quotesList = myGenericHistory - .Aggregate(PeriodSize.TwoHours) - .ToList(); - - // proper quantities - Assert.AreEqual(20, quotesList.Count); - - // sample values - Quote r19 = quotesList[19]; - Assert.AreEqual(369.04m, r19.Low); - } - - [TestMethod] - public void CustomQuoteAggregateTimeSpan() - { - List myGenericHistory = TestData.GetIntraday() - .Select(x => new MyGenericQuote { - CloseDate = x.Date, - Open = x.Open, - High = x.High, - Low = x.Low, - CloseValue = x.Close, - Volume = x.Volume, - MyOtherProperty = 123456 - }) - .ToList(); - - List quotesList = myGenericHistory - .Aggregate(TimeSpan.FromHours(2)) - .ToList(); - - // proper quantities - Assert.AreEqual(20, quotesList.Count); - - // sample values - Quote r19 = quotesList[19]; - Assert.AreEqual(369.04m, r19.Low); - } - - [TestMethod] - public void CustomIndicatorClass() - { - // can use a derive Indicator class - MyEma myIndicator = new() { - Date = DateTime.Now, - Ema = 123.456, - MyProperty = false - }; - - Assert.AreEqual(false, myIndicator.MyProperty); - } - - [TestMethod] - public void CustomIndicatorClassLinq() - { - IEnumerable quotes = TestData.GetDefault(); - IEnumerable emaResults = quotes.GetEma(14); - - // can use a derive Indicator class using Linq - - IEnumerable myIndicatorResults = emaResults - .Where(x => x.Ema != null) - .Select(x => new MyEma { - Date = x.Date, - Ema = x.Ema, - MyProperty = false - }); - - Assert.IsTrue(myIndicatorResults.Any()); - } - - [TestMethod] - public void CustomIndicatorClassFind() - { - IEnumerable quotes = TestData.GetDefault(); - IEnumerable emaResults = Indicator.GetEma(quotes, 20); - - // can use a derive Indicator class using Linq - - IEnumerable myIndicatorResults = emaResults - .Where(x => x.Ema != null) - .Select(x => new MyEma { - Id = 12345, - Date = x.Date, - Ema = x.Ema, - MyProperty = false - }); - - Assert.IsTrue(myIndicatorResults.Any()); - - // find specific date - DateTime findDate = DateTime.ParseExact("2018-12-31", "yyyy-MM-dd", EnglishCulture); - - MyEma i = myIndicatorResults.Find(findDate); - Assert.AreEqual(12345, i.Id); - - EmaResult r = emaResults.Find(findDate); - Assert.AreEqual(249.3519m, Math.Round((decimal)r.Ema, 4)); - } -} diff --git a/tests/other/Sut.CustomItems.cs b/tests/other/Sut.CustomItems.cs new file mode 100644 index 000000000..2715bb860 --- /dev/null +++ b/tests/other/Sut.CustomItems.cs @@ -0,0 +1,138 @@ +using System.Collections.ObjectModel; + +namespace Sut; + +// SUBJECT UNDER TEST (SUT) + +public sealed record CustomQuote : IQuote +{ + // override, redirect required properties + DateTime ISeries.Timestamp + => CloseDate; + + decimal IQuote.Close + => CloseValue; + + // custom properties + public int MyOtherProperty { get; set; } + public DateTime CloseDate { get; init; } + public decimal CloseValue { get; init; } + + // required base properties + public decimal Open { get; init; } + public decimal High { get; init; } + public decimal Low { get; init; } + public decimal Volume { get; init; } + + double IReusable.Value + => (double)CloseValue; +} + +[Serializable] +public record CustomQuoteInherited +( + DateTime CloseDate, + decimal Open, + decimal High, + decimal Low, + decimal Close, + decimal Volume, + int MyOtherProperty +) : Quote(CloseDate, Open, High, Low, Close, Volume); + +public sealed record CustomReusable : IReusable +{ + public DateTime Timestamp { get; init; } + public double? Sma { get; init; } + + double IReusable.Value + => Sma.Null2NaN(); +} + +public sealed record CustomReusableInherited( + DateTime Timestamp, + double? Sma + ) : IReusable +{ + public double Value + => Sma.Null2NaN(); +} + +public sealed class CustomSeries : ISeries +{ + public DateTime Timestamp { get; init; } + public int Id { get; init; } + public bool MyProperty { get; init; } + public double? Ema { get; set; } +} + +public record class CustomInheritEma : EmaResult +{ + // classic constructor + public CustomInheritEma(DateTime Timestamp) + : base(Timestamp) { } + + // custom properties (has defaults) + public int Id { get; set; } + public bool MyProperty { get; set; } +} + +public static class CustomIndicator +{ + // SERIES, from CHAIN + public static IReadOnlyList GetIndicator( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + => source + .ToSortedList() + .CalcIndicator(lookbackPeriods); + + private static List CalcIndicator( + this IReadOnlyList source, + int lookbackPeriods) + where T : IReusable + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for SMA."); + } + + // initialize + int length = source.Count; + List results = new(length); + + // roll through source values + for (int i = 0; i < length; i++) + { + T s = source[i]; + + double? sma; + + if (i >= lookbackPeriods - 1) + { + double sum = 0; + for (int p = i - lookbackPeriods + 1; p <= i; p++) + { + T ps = source[p]; + sum += ps.Value; + } + + sma = (sum / lookbackPeriods).NaN2Null(); + } + else + { + sma = null; + } + + results.Add(new() { + Timestamp = s.Timestamp, + Sma = sma + }); + } + + return results; + } +} diff --git a/tests/other/Tests.Other.csproj b/tests/other/Tests.Other.csproj index 4163be127..7d31c15e1 100644 --- a/tests/other/Tests.Other.csproj +++ b/tests/other/Tests.Other.csproj @@ -14,7 +14,12 @@ + + + all + runtime; build; native; contentfiles; analyzers + diff --git a/tests/performance/GlobalSuppressions.cs b/tests/performance/GlobalSuppressions.cs index 0d913b5dc..76695c9cc 100644 --- a/tests/performance/GlobalSuppressions.cs +++ b/tests/performance/GlobalSuppressions.cs @@ -1,24 +1,11 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage( - "Performance", - "CA1822:Mark members as static", + "Design", + "CA1051:Do not declare visible instance fields", Justification = "Required for BenchmarkDotNet")] [assembly: SuppressMessage( - "StyleCop.CSharp.MaintainabilityRules", - "SA1401:Fields should be private", - Justification = "Required for BenchmarkDotNet", - Scope = "member", - Target = "~F:Tests.Performance.InternalsPerformance.Periods")] - -[assembly: SuppressMessage("Design", - "CA1051:Do not declare visible instance fields", - Justification = "Required for BenchmarkDotNet", - Scope = "member", - Target = "~F:Tests.Performance.InternalsPerformance.Periods")] + "Performance", + "CA1822:Mark members as static", + Justification = "Required for BenchmarkDotNet")] diff --git a/tests/performance/GlobalUsings.cs b/tests/performance/GlobalUsings.cs index 373a066f6..9293b0a2d 100644 --- a/tests/performance/GlobalUsings.cs +++ b/tests/performance/GlobalUsings.cs @@ -1,3 +1,3 @@ global using BenchmarkDotNet.Attributes; global using Skender.Stock.Indicators; -global using Tests.Common; +global using Test.Data; diff --git a/tests/performance/Perf.Helpers.cs b/tests/performance/Perf.Helpers.cs deleted file mode 100644 index 8b5b4ed54..000000000 --- a/tests/performance/Perf.Helpers.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Tests.Performance; - -// HELPERS, both public and private - -[ShortRunJob] -public class HelperPerformance -{ - private static IEnumerable h; - private static IEnumerable i; - - [GlobalSetup] - public static void Setup() => h = TestData.GetDefault(); - - [GlobalSetup(Targets = [nameof(Aggregate)])] - public static void SetupIntraday() => i = TestData.GetIntraday(); - - [Benchmark] - public object ToSortedList() => h.ToSortedList(); - - [Benchmark] - public object ToSortedCollection() => h.ToSortedCollection(); - - [Benchmark] - public object ToListQuoteD() => h.ToQuoteD(); - - [Benchmark] - public object ToTupleClose() => h.ToTuple(CandlePart.Close); - - [Benchmark] - public object ToTupleOHLC4() => h.ToTuple(CandlePart.OHLC4); - - [Benchmark] - public object ToCandleResults() => h.ToCandleResults(); - - [Benchmark] - public object Validate() => h.Validate(); - - [Benchmark] - public object Aggregate() => i.Aggregate(PeriodSize.FifteenMinutes); -} diff --git a/tests/performance/Perf.Increments.cs b/tests/performance/Perf.Increments.cs new file mode 100644 index 000000000..cb4ffdef1 --- /dev/null +++ b/tests/performance/Perf.Increments.cs @@ -0,0 +1,95 @@ +namespace Performance; + +// TIME-SERIES INDICATORS + +[ShortRunJob] +public class Incrementals +{ + private static readonly IReadOnlyList quotes + = Data.GetDefault(); + + private static readonly IReadOnlyList reusables + = quotes + .Cast() + .ToList(); + + private readonly QuoteHub provider = new(); + + [GlobalSetup] + public void Setup() => provider.Add(quotes); + + [GlobalCleanup] + public void Cleanup() + { + provider.EndTransmission(); + provider.Cache.Clear(); + } + + [Benchmark] + public object EmaIncRusBatch() + { + EmaList sut = new(14) { reusables }; + return sut; + } + + [Benchmark] + public object EmaIncRusItem() + { + EmaList sut = new(14); + + for (int i = 0; i < reusables.Count; i++) + { + sut.Add(reusables[i]); + } + + return sut; + } + + [Benchmark] + public object EmaIncRusSplit() + { + EmaList sut = new(14); + + for (int i = 0; i < reusables.Count; i++) + { + sut.Add(reusables[i].Timestamp, reusables[i].Value); + } + + return sut; + } + + [Benchmark] + public object EmaIncQotBatch() + { + EmaList sut = new(14) { quotes }; + return sut; + } + + [Benchmark] + public object EmaIncQot() + { + EmaList sut = new(14); + + for (int i = 0; i < quotes.Count; i++) + { + sut.Add(quotes[i]); + } + + return sut; + } + + // TIME-SERIES EQUIVALENTS + + [Benchmark] + public object EmaSeries() => quotes.ToEma(14); + + [Benchmark] + public object EmaIncrem() + { + EmaList ema = new(14) { quotes }; + return ema; + } + + [Benchmark] + public object EmaStream() => provider.ToEma(14).Results; +} diff --git a/tests/performance/Perf.Indicators.Static.cs b/tests/performance/Perf.Indicators.Static.cs deleted file mode 100644 index 36ebfd01a..000000000 --- a/tests/performance/Perf.Indicators.Static.cs +++ /dev/null @@ -1,324 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Skender.Stock.Indicators; -using Tests.Common; - -namespace Tests.Performance; - -[ShortRunJob] -public class IndicatorsStatic -{ - private static IEnumerable q; - private static IEnumerable o; - private static List ql; - private static List ll; - - // SETUP - - [GlobalSetup] - public static void Setup() - { - q = TestData.GetDefault(); - ql = q.ToList(); - ll = TestData.GetLongest().ToList(); - } - - [GlobalSetup(Targets = new[] - { - nameof(GetBeta), - nameof(GetBetaUp), - nameof(GetBetaDown), - nameof(GetBetaAll), - nameof(GetCorrelation), - nameof(GetPrs), - nameof(GetPrsWithSma) - })] - public static void SetupCompare() - { - q = TestData.GetDefault(); - o = TestData.GetCompare(); - } - - // BENCHMARKS - - [Benchmark] - public object GetAdl() => q.GetAdl(); - - [Benchmark] - public object GetAdx() => q.GetAdx(); - - [Benchmark] - public object GetAlligator() => q.GetAlligator(); - - [Benchmark] - public object GetAlma() => q.GetAlma(); - - [Benchmark] - public object GetAroon() => q.GetAroon(); - - [Benchmark] - public object GetAtr() => q.GetAtr(); - - [Benchmark] - public object GetAtrStop() => q.GetAtrStop(); - - [Benchmark] - public object GetAwesome() => q.GetAwesome(); - - [Benchmark] - public object GetBeta() => Indicator.GetBeta(q, o, 20, BetaType.Standard); - - [Benchmark] - public object GetBetaUp() => Indicator.GetBeta(q, o, 20, BetaType.Up); - - [Benchmark] - public object GetBetaDown() => Indicator.GetBeta(q, o, 20, BetaType.Down); - - [Benchmark] - public object GetBetaAll() => Indicator.GetBeta(q, o, 20, BetaType.All); - - [Benchmark] - public object GetBollingerBands() => q.GetBollingerBands(); - - [Benchmark] - public object GetBop() => q.GetBop(); - - [Benchmark] - public object GetCci() => q.GetCci(); - - [Benchmark] - public object GetChaikinOsc() => q.GetChaikinOsc(); - - [Benchmark] - public object GetChandelier() => q.GetChandelier(); - - [Benchmark] - public object GetChop() => q.GetChop(); - - [Benchmark] - public object GetCmf() => q.GetCmf(); - - [Benchmark] - public object GetCmo() => q.GetCmo(14); - - [Benchmark] - public object GetConnorsRsi() => q.GetConnorsRsi(); - - [Benchmark] - public object GetCorrelation() => q.GetCorrelation(o, 20); - - [Benchmark] - public object GetDema() => q.GetDema(14); - - [Benchmark] - public object GetDoji() => q.GetDoji(); - - [Benchmark] - public object GetDonchian() => q.GetDonchian(); - - [Benchmark] - public object GetDpo() => q.GetDpo(14); - - [Benchmark] - public object GetDynamic() => q.GetDynamic(20); - - [Benchmark] - public object GetElderRay() => q.GetElderRay(); - - [Benchmark] - public object GetEma() => q.GetEma(14); - - [Benchmark] - public object GetEpma() => q.GetEpma(14); - - [Benchmark] - public object GetFcb() => q.GetFcb(14); - - [Benchmark] - public object GetFisherTransform() => q.GetFisherTransform(10); - - [Benchmark] - public object GetForceIndex() => q.GetForceIndex(13); - - [Benchmark] - public object GetFractal() => q.GetFractal(); - - [Benchmark] - public object GetGator() => q.GetGator(); - - [Benchmark] - public object GetHeikinAshi() => q.GetHeikinAshi(); - - [Benchmark] - public object GetHma() => q.GetHma(14); - - [Benchmark] - public object GetHtTrendline() => q.GetHtTrendline(); - - [Benchmark] - public object GetHurst() => q.GetHurst(); - - [Benchmark] - public object GetIchimoku() => q.GetIchimoku(); - - [Benchmark] - public object GetKama() => q.GetKama(); - - [Benchmark] - public object GetKlinger() => q.GetKvo(); - - [Benchmark] - public object GetKeltner() => q.GetKeltner(); - - [Benchmark] - public object GetKvo() => q.GetKvo(); - - [Benchmark] - public object GetMacd() => q.GetMacd(); - - [Benchmark] - public object GetMaEnvelopes() => q.GetMaEnvelopes(20, 2.5, MaType.SMA); - - [Benchmark] - public object GetMama() => q.GetMama(); - - [Benchmark] - public object GetMarubozu() => q.GetMarubozu(); - - [Benchmark] - public object GetMfi() => q.GetMfi(); - - [Benchmark] - public object GetObv() => q.GetObv(); - - [Benchmark] - public object GetObvWithSma() => q.GetObv(14); - - [Benchmark] - public object GetParabolicSar() => q.GetParabolicSar(); - - [Benchmark] - public object GetPivotPoints() => q.GetPivotPoints(PeriodSize.Month, PivotPointType.Standard); - - [Benchmark] - public object GetPivots() => q.GetPivots(); - - [Benchmark] - public object GetPmo() => q.GetPmo(); - - [Benchmark] - public object GetPrs() => q.GetPrs(o); - - [Benchmark] - public object GetPrsWithSma() => q.GetPrs(o, null, 5); - - [Benchmark] - public object GetPvo() => q.GetPvo(); - - [Benchmark] - public object GetRenko() => q.GetRenko(2.5m); - - [Benchmark] - public object GetRenkoAtr() => q.GetRenko(14); - - [Benchmark] - public object GetRoc() => q.GetRoc(20); - - [Benchmark] - public object GetRocWb() => q.GetRocWb(12, 3, 12); - - [Benchmark] - public object GetRocWithSma() => q.GetRoc(20, 14); - - [Benchmark] - public object GetRollingPivots() => q.GetRollingPivots(14, 1); - - [Benchmark] - public object GetRsi() => q.GetRsi(); - - [Benchmark] - public object GetSlope() => q.GetSlope(20); - - [Benchmark] - public object GetSma() => q.GetSma(10); - - [Benchmark] - public object GetSmaAnalysis() => q.GetSmaAnalysis(10); - - [Benchmark] - public object GetSmi() => q.GetSmi(5, 20, 5, 3); - - [Benchmark] - public object GetSmma() => q.GetSmma(10); - - [Benchmark] - public object GetStarcBands() => q.GetStarcBands(10); - - [Benchmark] - public object GetStc() => q.GetStc(); - - [Benchmark] - public object GetStdDev() => q.GetStdDev(20); - - [Benchmark] - public object GetStdDevWithSma() => q.GetStdDev(20, 14); - - [Benchmark] - public object GetStdDevChannels() => q.GetStdDevChannels(); - - [Benchmark] - public object GetStoch() => q.GetStoch(); - - [Benchmark] - public object GetStochSMMA() => q.GetStoch(9, 3, 3, 3, 2, MaType.SMMA); - - [Benchmark] - public object GetStochRsi() => q.GetStochRsi(14, 14, 3); - - [Benchmark] - public object GetSuperTrend() => q.GetSuperTrend(); - - [Benchmark] - public object GetT3() => q.GetT3(); - - [Benchmark] - public object GetTema() => q.GetTema(14); - - [Benchmark] - public object GetTr() => q.GetTr(); - - [Benchmark] - public object GetTrix() => q.GetTrix(14); - - [Benchmark] - public object GetTrixWithSma() => q.GetTrix(14, 5); - - [Benchmark] - public object GetTsi() => q.GetTsi(); - - [Benchmark] - public object GetUlcerIndex() => q.GetUlcerIndex(); - - [Benchmark] - public object GetUltimate() => q.GetUltimate(); - - [Benchmark] - public object GetVolatilityStop() => q.GetVolatilityStop(); - - [Benchmark] - public object GetVortex() => q.GetVortex(14); - - [Benchmark] - public object GetVwap() => q.GetVwap(); - - [Benchmark] - public object GetVwma() => q.GetVwma(14); - - [Benchmark] - public object GetWilliamsR() => q.GetWilliamsR(); - - [Benchmark] - public object GetWma() => q.GetWma(14); - - [Benchmark] - public object GetZigZag() => q.GetZigZag(); -} diff --git a/tests/performance/Perf.Indicators.Stream.cs b/tests/performance/Perf.Indicators.Stream.cs deleted file mode 100644 index 485ffc8dd..000000000 --- a/tests/performance/Perf.Indicators.Stream.cs +++ /dev/null @@ -1,62 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Skender.Stock.Indicators; -using Tests.Common; - -namespace Tests.Performance; - -public class IndicatorsStreaming -{ - private static IEnumerable q; - private static List ql; - - // SETUP - - [GlobalSetup] - public void Setup() - { - q = TestData.GetDefault(); - ql = q.ToSortedList(); - } - - // BENCHMARKS - - [Benchmark] - public object GetEma() => q.GetEma(14); - - [Benchmark] - public object GetEmaStream() - { - // todo: refactor to exclude provider - QuoteProvider provider = new(); - EmaObserver observer = provider.GetEma(14); - - for (int i = 0; i < ql.Count; i++) - { - provider.Add(ql[i]); - } - - provider.EndTransmission(); - - return observer.Results; - } - - [Benchmark] - public object GetSma() => q.GetSma(10); - - [Benchmark] - public object GetSmaStream() - { - // todo: refactor to exclude provider - QuoteProvider provider = new(); - SmaObserver observer = provider.GetSma(10); - - for (int i = 0; i < ql.Count; i++) - { - provider.Add(ql[i]); - } - - provider.EndTransmission(); - - return observer.Results; - } -} diff --git a/tests/performance/Perf.StaticSeries.cs b/tests/performance/Perf.StaticSeries.cs new file mode 100644 index 000000000..6892ef6e4 --- /dev/null +++ b/tests/performance/Perf.StaticSeries.cs @@ -0,0 +1,277 @@ +namespace Performance; + +// TIME-SERIES INDICATORS + +[ShortRunJob] +public class SeriesIndicators +{ + private static readonly IReadOnlyList q = Data.GetDefault(); + private static readonly IReadOnlyList o = Data.GetCompare(); + + [Benchmark] + public object ToAdl() => q.ToAdl(); + + [Benchmark] + public object ToAdx() => q.ToAdx(); + + [Benchmark] + public object ToAlligator() => q.ToAlligator(); + + [Benchmark] + public object ToAlma() => q.ToAlma(); + + [Benchmark] + public object ToAroon() => q.ToAroon(); + + [Benchmark] + public object ToAtr() => q.ToAtr(); + + [Benchmark] + public object ToAtrStop() => q.ToAtrStop(); + + [Benchmark] + public object ToAwesome() => q.ToAwesome(); + + [Benchmark] + public object ToBeta() => Beta.ToBeta(q, o, 20, BetaType.Standard); + + [Benchmark] + public object ToBetaUp() => Beta.ToBeta(q, o, 20, BetaType.Up); + + [Benchmark] + public object ToBetaDown() => Beta.ToBeta(q, o, 20, BetaType.Down); + + [Benchmark] + public object ToBetaAll() => Beta.ToBeta(q, o, 20, BetaType.All); + + [Benchmark] + public object ToBollingerBands() => q.ToBollingerBands(); + + [Benchmark] + public object ToBop() => q.ToBop(); + + [Benchmark] + public object ToCci() => q.ToCci(); + + [Benchmark] + public object ToChaikinOsc() => q.ToChaikinOsc(); + + [Benchmark] + public object ToChandelier() => q.ToChandelier(); + + [Benchmark] + public object ToChop() => q.ToChop(); + + [Benchmark] + public object ToCmf() => q.ToCmf(); + + [Benchmark] + public object ToCmo() => q.ToCmo(14); + + [Benchmark] + public object ToConnorsRsi() => q.ToConnorsRsi(); + + [Benchmark] + public object ToCorrelation() => q.ToCorrelation(o, 20); + + [Benchmark] + public object ToDema() => q.ToDema(14); + + [Benchmark] + public object ToDoji() => q.ToDoji(); + + [Benchmark] + public object ToDonchian() => q.ToDonchian(); + + [Benchmark] + public object ToDpo() => q.ToDpo(14); + + [Benchmark] + public object ToDynamic() => q.ToDynamic(20); + + [Benchmark] + public object ToElderRay() => q.ToElderRay(); + + [Benchmark] + public object ToEma() => q.ToEma(14); + + [Benchmark] + public object ToEpma() => q.ToEpma(14); + + [Benchmark] + public object ToFcb() => q.ToFcb(14); + + [Benchmark] + public object ToFisherTransform() => q.ToFisherTransform(10); + + [Benchmark] + public object ToForceIndex() => q.ToForceIndex(13); + + [Benchmark] + public object ToFractal() => q.ToFractal(); + + [Benchmark] + public object ToGator() => q.ToGator(); + + [Benchmark] + public object ToHeikinAshi() => q.ToHeikinAshi(); + + [Benchmark] + public object ToHma() => q.ToHma(14); + + [Benchmark] + public object ToHtTrendline() => q.ToHtTrendline(); + + [Benchmark] + public object ToHurst() => q.ToHurst(); + + [Benchmark] + public object ToIchimoku() => q.ToIchimoku(); + + [Benchmark] + public object ToKama() => q.ToKama(); + + [Benchmark] + public object ToKlinger() => q.ToKvo(); + + [Benchmark] + public object ToKeltner() => q.ToKeltner(); + + [Benchmark] + public object ToKvo() => q.ToKvo(); + + [Benchmark] + public object ToMacd() => q.ToMacd(); + + [Benchmark] + public object ToMaEnvelopes() => q.ToMaEnvelopes(20, 2.5, MaType.SMA); + + [Benchmark] + public object ToMama() => q.ToMama(); + + [Benchmark] + public object ToMarubozu() => q.ToMarubozu(); + + [Benchmark] + public object ToMfi() => q.ToMfi(); + + [Benchmark] + public object ToObv() => q.ToObv(); + + [Benchmark] + public object ToParabolicSar() => q.ToParabolicSar(); + + [Benchmark] + public object ToPivotPoints() => q.ToPivotPoints(PeriodSize.Month, PivotPointType.Standard); + + [Benchmark] + public object ToPivots() => q.ToPivots(); + + [Benchmark] + public object ToPmo() => q.ToPmo(); + + [Benchmark] + public object ToPrs() => q.ToPrs(o); + + [Benchmark] + public object ToPvo() => q.ToPvo(); + + [Benchmark] + public object ToRenko() => q.ToRenko(2.5m); + + [Benchmark] + public object ToRenkoAtr() => q.ToRenko(14); + + [Benchmark] + public object ToRoc() => q.ToRoc(20); + + [Benchmark] + public object ToRocWb() => q.ToRocWb(12, 3, 12); + + [Benchmark] + public object ToRollingPivots() => q.ToRollingPivots(14, 1); + + [Benchmark] + public object ToRsi() => q.ToRsi(); + + [Benchmark] + public object ToSlope() => q.ToSlope(20); + + [Benchmark] + public object ToSma() => q.ToSma(10); + + [Benchmark] + public object ToSmaAnalysis() => q.ToSmaAnalysis(10); + + [Benchmark] + public object ToSmi() => q.ToSmi(5, 20, 5, 3); + + [Benchmark] + public object ToSmma() => q.ToSmma(10); + + [Benchmark] + public object ToStarcBands() => q.ToStarcBands(10); + + [Benchmark] + public object ToStc() => q.ToStc(); + + [Benchmark] + public object ToStdDev() => q.ToStdDev(20); + + [Benchmark] + public object ToStdDevChannels() => q.ToStdDevChannels(); + + [Benchmark] + public object ToStoch() => q.ToStoch(); + + [Benchmark] + public object ToStochSMMA() => q.ToStoch(9, 3, 3, 3, 2, MaType.SMMA); + + [Benchmark] + public object ToStochRsi() => q.ToStochRsi(14, 14, 3); + + [Benchmark] + public object ToSuperTrend() => q.ToSuperTrend(); + + [Benchmark] + public object ToT3() => q.ToT3(); + + [Benchmark] + public object ToTema() => q.ToTema(14); + + [Benchmark] + public object ToTr() => q.ToTr(); + + [Benchmark] + public object ToTrix() => q.ToTrix(14); + + [Benchmark] + public object ToTsi() => q.ToTsi(); + + [Benchmark] + public object ToUlcerIndex() => q.ToUlcerIndex(); + + [Benchmark] + public object ToUltimate() => q.ToUltimate(); + + [Benchmark] + public object ToVolatilityStop() => q.ToVolatilityStop(); + + [Benchmark] + public object ToVortex() => q.ToVortex(14); + + [Benchmark] + public object ToVwap() => q.ToVwap(); + + [Benchmark] + public object ToVwma() => q.ToVwma(14); + + [Benchmark] + public object ToWilliamsR() => q.ToWilliamsR(); + + [Benchmark] + public object ToWma() => q.ToWma(14); + + [Benchmark] + public object ToZigZag() => q.ToZigZag(); +} diff --git a/tests/performance/Perf.StreamHub.Externals.cs b/tests/performance/Perf.StreamHub.Externals.cs new file mode 100644 index 000000000..3a2dd4eb0 --- /dev/null +++ b/tests/performance/Perf.StreamHub.Externals.cs @@ -0,0 +1,42 @@ +namespace Performance; + +// STREAMING INDICATOR HUBS (EXTERNAL CACHE) + +[ShortRunJob] +public class StreamExternal +{ + private static readonly IReadOnlyList quotes + = Data.GetDefault(); + + private readonly QuoteHub provider = new(); + + /* SETUP/CLEANUP - runs before and after each. + * + * This Setup implies that each benchmark + * will start with a prepopulated observable + * QuoteHub provider. + * + * We do this because we want to measure + * the performance of observer methods + * without the overhead of the provider. */ + + [GlobalSetup] + public void Setup() => provider.Add(quotes); + + [GlobalCleanup] + public void Cleanup() + { + provider.EndTransmission(); + provider.Cache.Clear(); + } + + // BENCHMARKS + + // TODO: replace with external data cache model, when available + + [Benchmark(Baseline = true)] + public object EmaSeries() => quotes.ToEma(14); + + [Benchmark] + public object EmaStream() => provider.ToEma(14).Results; +} diff --git a/tests/performance/Perf.StreamHub.cs b/tests/performance/Perf.StreamHub.cs new file mode 100644 index 000000000..6ad618641 --- /dev/null +++ b/tests/performance/Perf.StreamHub.cs @@ -0,0 +1,78 @@ +namespace Performance; + +// STREAMING INDICATOR HUBS + +[ShortRunJob] +public class StreamIndicators +{ + private static readonly IReadOnlyList quotes + = Data.GetDefault(); + + private readonly QuoteHub provider = new(); // prepopulated + private readonly QuoteHub supplier = new(); // empty + + /* SETUP/CLEANUP - runs before and after each. + * + * This Setup implies that each benchmark + * will start with a prepopulated observable + * QuoteHub provider. + * + * We do this because we want to measure + * the performance of observer methods + * without the overhead of the provider. */ + + [GlobalSetup] + public void Setup() => provider.Add(quotes); + + [GlobalCleanup] + public void Cleanup() + { + provider.EndTransmission(); + provider.Cache.Clear(); + } + + // BENCHMARKS + + [Benchmark] + public object AdlHub() => provider.ToAdl().Results; + + [Benchmark] + public object AtrHub() => provider.ToAtr(14).Results; + + [Benchmark] + public object AtrStopHub() => provider.ToAtrStop().Results; + + [Benchmark] + public object AlligatorHub() => provider.ToAlligator().Results; + + [Benchmark] + public object EmaHub() => provider.ToEma(14).Results; + + [Benchmark] + public object EmaHub2() + { + EmaHub observer = supplier.ToEma(14); + + for (int i = 0; i < quotes.Count; i++) + { + observer.OnAdd(quotes[i], notify: false, i); + } + + return observer.Results; + } + + [Benchmark] + public object QuoteHub() => provider.ToQuote().Results; + + [Benchmark] + public object QuotePartHub() => provider.ToQuotePart(CandlePart.OHL3).Results; + + [Benchmark] + public object RenkoHub() => provider.ToRenko(2.5m).Results; + + [Benchmark] + public object SmaHub() => provider.ToSma(10).Results; + + [Benchmark] + public object TrHub() => provider.ToTr().Results; +} diff --git a/tests/performance/Perf.Internals.cs b/tests/performance/Perf.Utility.Maths.cs similarity index 55% rename from tests/performance/Perf.Internals.cs rename to tests/performance/Perf.Utility.Maths.cs index 461dbab29..a8f4acab7 100644 --- a/tests/performance/Perf.Internals.cs +++ b/tests/performance/Perf.Utility.Maths.cs @@ -1,22 +1,22 @@ -namespace Tests.Performance; +namespace Performance; -// INTERNAL FUNCTIONS +// INTERNAL UTILITIES [ShortRunJob] -public class InternalsPerformance +public class UtilityMaths { [Params(20, 50, 250, 1000)] public int Periods; - private double[] values; + private double[] _values; // standard deviation [GlobalSetup(Targets = [nameof(StdDev)])] public void Setup() - => values = TestData.GetLongish(Periods) + => _values = Data.GetLongish(Periods) .Select(x => (double)x.Close) .ToArray(); [Benchmark] - public object StdDev() => values.StdDev(); + public object StdDev() => _values.StdDev(); } diff --git a/tests/performance/Perf.Utility.cs b/tests/performance/Perf.Utility.cs new file mode 100644 index 000000000..eb1401be1 --- /dev/null +++ b/tests/performance/Perf.Utility.cs @@ -0,0 +1,31 @@ +namespace Performance; + +// INTERNAL UTILITIES + +[ShortRunJob] +public class Utility +{ + private static readonly IReadOnlyList q = Data.GetDefault(); + private static readonly IReadOnlyList i = Data.GetIntraday(); + + [Benchmark] + public object ToSortedList() => q.ToSortedList(); + + [Benchmark] + public object ToListQuoteD() => q.ToQuoteDList(); + + [Benchmark] + public object ToReusableClose() => q.ToReusableList(CandlePart.Close); + + [Benchmark] + public object ToReusableOhlc4() => q.ToReusableList(CandlePart.OHLC4); + + [Benchmark] + public object ToCandleResults() => q.ToCandles(); + + [Benchmark] + public object Validate() => q.Validate(); + + [Benchmark] + public object Aggregate() => i.Aggregate(PeriodSize.FifteenMinutes); +} diff --git a/tests/performance/Program.cs b/tests/performance/Program.cs index 0c6b90d6b..068dd6fe8 100644 --- a/tests/performance/Program.cs +++ b/tests/performance/Program.cs @@ -2,7 +2,7 @@ [assembly: CLSCompliant(false)] -namespace Tests.Performance; +namespace Performance; public static class Program { @@ -21,4 +21,13 @@ public static void Main(string[] args) BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); } } + + /* USAGE + * + * dotnet build -c Release + * + * Examples, to run cohorts: + * dotnet run -c Release -filter *Stream* + * dotnet run -c Release -filter *External.EmaHub* + */ } diff --git a/tests/performance/Tests.Performance.csproj b/tests/performance/Tests.Performance.csproj index e2f0d910d..43bc26589 100644 --- a/tests/performance/Tests.Performance.csproj +++ b/tests/performance/Tests.Performance.csproj @@ -2,8 +2,14 @@ net8.0 - enable Exe + Performance.Program + + false + None + true + + enable true latest @@ -19,24 +25,7 @@ - - - - - Always - - - Always - - - Always - - - Always - - - Always - + diff --git a/tests/performance/helpers/Helper.Getter.cs b/tests/performance/helpers/Helper.Getter.cs deleted file mode 100644 index c82c64dcd..000000000 --- a/tests/performance/helpers/Helper.Getter.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace Tests.Common; - -// IMPORT TEST DATA -internal static class TestData -{ - // DEFAULT: S&P 500 ~2 years of daily data - internal static IEnumerable GetDefault(int days = 502) - => File.ReadAllLines("helpers/data/default.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // COMPARE DATA ~2 years of TSLA data (matches default time) - internal static IEnumerable GetCompare(int days = 502) - => File.ReadAllLines("helpers/data/compare.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // INTRADAY DATA - internal static IEnumerable GetIntraday(int days = 1564) - => File.ReadAllLines("helpers/data/intraday.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); - - // LONGEST DATA ~62 years of S&P 500 daily data - internal static IEnumerable GetLongest() - => File.ReadAllLines("helpers/data/longest.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .ToList(); - - // LONGISH DATA ~20 years of S&P 500 daily data - internal static IEnumerable GetLongish(int days = 5285) - => File.ReadAllLines("helpers/data/longish.csv") - .Skip(1) - .Select(Importer.QuoteFromCsv) - .OrderByDescending(x => x.Date) - .Take(days) - .ToList(); -} diff --git a/tests/performance/helpers/Helper.Importer.cs b/tests/performance/helpers/Helper.Importer.cs deleted file mode 100644 index b88006ca4..000000000 --- a/tests/performance/helpers/Helper.Importer.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Globalization; - -namespace Tests.Common; - -// TEST QUOTE IMPORTER -internal static class Importer -{ - private static readonly CultureInfo EnglishCulture = new("en-US", false); - - // importer / parser - internal static Quote QuoteFromCsv(string csvLine) - { - if (string.IsNullOrEmpty(csvLine)) - { - return new Quote(); - } - - string[] values = csvLine.Split(','); - Quote quote = new(); - - HandleOHLCV(quote, "D", values[0]); - HandleOHLCV(quote, "O", values[1]); - HandleOHLCV(quote, "H", values[2]); - HandleOHLCV(quote, "L", values[3]); - HandleOHLCV(quote, "C", values[4]); - HandleOHLCV(quote, "V", values[5]); - - return quote; - } - - internal static decimal ToDecimal(this string value) - => decimal.TryParse(value, out decimal d) ? d - : throw new NotFiniteNumberException( - $"Cannot convert `{value}`, it is not a number."); - - internal static decimal? ToDecimalNull(this string value) - => decimal.TryParse(value, out decimal d) ? d : null; - - internal static double? ToDoubleNull(this string value) - => double.TryParse(value, out double d) ? d : null; - - private static void HandleOHLCV(Quote quote, string position, string value) - { - if (string.IsNullOrEmpty(value)) - { - return; - } - - switch (position) - { - case "D": - quote.Date = Convert.ToDateTime(value, EnglishCulture); - break; - case "O": - quote.Open = Convert.ToDecimal(value, EnglishCulture); - break; - case "H": - quote.High = Convert.ToDecimal(value, EnglishCulture); - break; - case "L": - quote.Low = Convert.ToDecimal(value, EnglishCulture); - break; - case "C": - quote.Close = Convert.ToDecimal(value, EnglishCulture); - break; - case "V": - quote.Volume = Convert.ToDecimal(value, EnglishCulture); - break; - default: - throw new ArgumentOutOfRangeException(nameof(position)); - } - } -} diff --git a/tests/simulate/Program.cs b/tests/simulate/Program.cs new file mode 100644 index 000000000..38e5a2c32 --- /dev/null +++ b/tests/simulate/Program.cs @@ -0,0 +1,37 @@ +using Skender.Stock.Indicators; +using Utilities; + +// define simulated quotes, arrival rate +int quotesPerMinute = 600; +int quantityToStream = 75; + +List quotes + = Util.Setup(quantityToStream, quotesPerMinute); + +// initialize quote provider + +QuoteHub provider = new(); + +// subscribe indicator hubs (SMA, EMA, etc.) + +SmaHub smaHub = provider.ToSma(3); +EmaHub emaHub = provider.ToEma(5); +EmaHub useChain = provider.ToQuotePart(CandlePart.HL2).ToEma(7); +EmaHub emaChain = provider.ToSma(4).ToEma(4); // chainable + +/* normally, you'd plugin your WebSocket here + * and use `provider.Add(q);` to connect the streams */ + +// simulate streaming quotes + +for (int i = 0; i < quantityToStream; i++) +{ + Quote quote = quotes[i]; + provider.Add(quote); // on arrival from external WebSocket + + // govern simulation rate + Thread.Sleep(60000 / quotesPerMinute); + + // send output to console + Util.PrintData(quote, smaHub, emaHub, useChain, emaChain); +} diff --git a/tests/observe/Observe.Streaming.csproj b/tests/simulate/Test.Simulation.csproj similarity index 75% rename from tests/observe/Observe.Streaming.csproj rename to tests/simulate/Test.Simulation.csproj index a6edaccf3..914048054 100644 --- a/tests/observe/Observe.Streaming.csproj +++ b/tests/simulate/Test.Simulation.csproj @@ -4,12 +4,9 @@ Exe net8.0 enable + enable - - - - diff --git a/tests/simulate/Utilities.cs b/tests/simulate/Utilities.cs new file mode 100644 index 000000000..941f5a659 --- /dev/null +++ b/tests/simulate/Utilities.cs @@ -0,0 +1,140 @@ +using Skender.Stock.Indicators; + +namespace Utilities; + +internal static class Util +{ + internal static List Setup(int quantityToStream, int quotesPerMinute) + { + Console.WriteLine($"Simulating {quotesPerMinute:N0} quotes per minute"); + PrintHeader(); + + return new RandomGbm(bars: quantityToStream); + } + + internal static void PrintHeader() + { + // dislay header + Console.WriteLine(); + Console.WriteLine(""" + Date Close price SMA(3) EMA(5) EMA(7,HL2) SMA/EMA(8) + ------------------------------------------------------------------------------ + """); + } + + internal static void PrintData( + Quote q, + SmaHub smaHub, + EmaHub emaHub, + EmaHub useChain, + EmaHub emaChain) + { + // send output to console + string m = $"{q.Timestamp:yyyy-MM-dd HH:mm} {q.Close,11:N2}"; + + SmaResult s = smaHub.Results[^1]; + EmaResult e = emaHub.Results[^1]; + EmaResult u = useChain.Results[^1]; + EmaResult c = emaChain.Results[^1]; + + if (s.Sma is not null) + { + m += $"{s.Sma,12:N1}"; + } + + if (e.Ema is not null) + { + m += $"{e.Ema,12:N1}"; + } + + if (u.Ema is not null) + { + m += $"{u.Ema,12:N1}"; + } + + if (c.Ema is not null) + { + m += $"{c.Ema,12:N1}"; + } + + Console.WriteLine(m); + } +} + +/// +/// Geometric Brownian Motion (GMB) is a random simulator of market movement. +/// GBM can be used for testing indicators, validation and Monte Carlo simulations of strategies. +/// +/// +/// Sample usage: +/// +/// RandomGbm data = new(); // generates 1 year (252) list of bars +/// RandomGbm data = new(Bars: 1000); // generates 1,000 bars +/// RandomGbm data = new(Bars: 252, Volatility: 0.05, Drift: 0.0005, Seed: 100.0) +/// +/// Parameters: +/// +/// Bars: number of bars (quotes) requested +/// Volatility: how dymamic/volatile the series should be; default is 1 +/// Drift: incremental drift due to annual interest rate; default is 5% +/// Seed: starting value of the random series; should not be 0. +/// + +internal class RandomGbm : List +{ + private readonly double _volatility; + private readonly double _drift; + private double _seed; + + internal RandomGbm( + int bars = 250, + double volatility = 1.0, + double drift = 0.01, + double seed = 1000.0) + { + _seed = seed; + _volatility = volatility * 0.01; + _drift = drift * 0.001; + for (int i = 0; i < bars; i++) + { + DateTime date = DateTime.Today.AddMinutes(i - bars); + Add(date); + } + } + + public void Add(DateTime timestamp) + { + double open = Price(_seed, _volatility * _volatility, _drift); + double close = Price(open, _volatility, _drift); + + double ocMax = Math.Max(open, close); + double high = Price(_seed, _volatility * 0.5, 0); + high = high < ocMax ? (2 * ocMax) - high : high; + + double ocMin = Math.Min(open, close); + double low = Price(_seed, _volatility * 0.5, 0); + low = low > ocMin ? (2 * ocMin) - low : low; + + double volume = Price(_seed * 10, _volatility * 2, drift: 0); + + Quote quote = new( + Timestamp: timestamp, + Open: (decimal)open, + High: (decimal)high, + Low: (decimal)low, + Close: (decimal)close, + Volume: (decimal)volume); + + Add(quote); + _seed = close; + } + + private static double Price(double seed, double volatility, double drift) + { + Random rnd = new((int)DateTime.UtcNow.Ticks); + double u1 = 1.0 - rnd.NextDouble(); + double u2 = 1.0 - rnd.NextDouble(); + double z = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2); + return seed * Math.Exp(drift - (volatility * volatility * 0.5) + (volatility * z)); + } +}