From 6c8b30c94d9eca2b4ad57aa4aa9685e81bff5a31 Mon Sep 17 00:00:00 2001 From: Dave Skender <8432125+DaveSkender@users.noreply.github.com> Date: Fri, 21 Jun 2024 20:48:13 -0400 Subject: [PATCH] refactor: abstract stream types, use record struct types (incomplete) --- .editorconfig | 16 +- docs/pages/utilities.md | 2 + src/_common/Candles/Candles.Models.cs | 43 +- src/_common/Candles/Candles.cs | 34 +- src/_common/Enums.cs | 19 + .../Generics/{Series.Model.cs => ISeries.cs} | 0 src/_common/Generics/Seek.cs | 21 +- src/_common/Generics/info.xml | 14 +- src/_common/Observables/AbstractCache.cs | 374 +++++++++++++++ .../Observables/AbstractChainInChainOut.cs | 85 ++++ .../Observables/AbstractChainInResultOut.cs | 85 ++++ src/_common/Observables/AbstractProvider.cs | 149 ++++++ .../Observables/AbstractQuoteInChainOut.cs | 85 ++++ .../Observables/AbstractQuoteInQuoteOut.cs | 85 ++++ src/_common/Observables/Cache.cs | 444 ------------------ src/_common/Observables/ChainObserver.cs | 79 ---- src/_common/Observables/ChainProvider.cs | 89 ---- src/_common/Observables/IStreamCache.cs | 51 ++ src/_common/Observables/IStreamObserver.cs | 64 +++ src/_common/Observables/IStreamProvider.cs | 74 +++ src/_common/Observables/Interfaces.cs | 117 ----- src/_common/Observables/QuoteObserver.cs | 85 ---- src/_common/Observables/QuoteProvider.cs | 160 ++----- src/_common/ObsoleteV3.cs | 183 ++------ src/_common/ObsoleteV3.md | 18 +- src/_common/Quotes/Quote.Converters.cs | 66 +-- src/_common/Quotes/Quote.Models.cs | 115 +---- .../Results/{Result.Models.cs => IResult.cs} | 0 src/_common/Use (quote converter)/Use.Api.cs | 10 +- .../Use (quote converter)/Use.Models.cs | 11 +- .../Use (quote converter)/Use.Stream.cs | 41 +- src/_common/Use (quote converter)/info.xml | 21 - src/a-d/Adl/Adl.Api.cs | 9 +- src/a-d/Adl/Adl.Common.cs | 30 +- src/a-d/Adl/Adl.Models.cs | 21 +- src/a-d/Adl/Adl.Series.cs | 5 +- src/a-d/Adl/Adl.Stream.cs | 25 + src/a-d/Adl/info.xml | 29 +- src/a-d/Adx/Adx.Models.cs | 17 +- src/a-d/Adx/Adx.Series.cs | 28 +- src/a-d/Alligator/Alligator.Api.cs | 97 ++-- src/a-d/Alligator/Alligator.Common.cs | 6 +- src/a-d/Alligator/Alligator.Models.cs | 5 +- src/a-d/Alligator/Alligator.Stream.cs | 65 +-- src/a-d/Alligator/info.xml | 24 - src/a-d/Alma/Alma.Api.cs | 20 +- src/a-d/Alma/Alma.Models.cs | 5 +- src/a-d/Aroon/Aroon.Models.cs | 5 +- src/a-d/Atr/Atr.Models.cs | 5 +- src/a-d/AtrStop/AtrStop.Models.cs | 2 +- src/a-d/Awesome/Awesome.Api.cs | 19 +- src/a-d/Awesome/Awesome.Models.cs | 5 +- src/a-d/BasicQuote/BasicQuote.Api.cs | 23 - src/a-d/BasicQuote/BasicQuote.Common.cs | 16 - src/a-d/BasicQuote/BasicQuote.Models.cs | 7 - src/a-d/BasicQuote/info.xml | 35 -- src/a-d/Beta/Beta.Api.cs | 27 +- src/a-d/Beta/Beta.Models.cs | 5 +- src/a-d/BollingerBands/BollingerBands.Api.cs | 19 +- .../BollingerBands/BollingerBands.Models.cs | 5 +- src/a-d/Bop/Bop.Models.cs | 5 +- src/a-d/Cci/Cci.Models.cs | 5 +- src/a-d/ChaikinOsc/ChaikinOsc.Models.cs | 5 +- src/a-d/Chandelier/Chandelier.Models.cs | 5 +- src/a-d/Chop/Chop.Models.cs | 5 +- src/a-d/Cmf/Cmf.Models.cs | 5 +- src/a-d/Cmo/Cmo.Api.cs | 18 +- src/a-d/Cmo/Cmo.Models.cs | 5 +- src/a-d/ConnorsRsi/ConnorsRsi.Api.cs | 20 +- src/a-d/ConnorsRsi/ConnorsRsi.Models.cs | 6 +- src/a-d/Correlation/Correlation.Api.cs | 25 +- src/a-d/Correlation/Correlation.Models.cs | 5 +- src/a-d/Dema/Dema.Api.cs | 18 +- src/a-d/Dema/Dema.Models.cs | 5 +- src/a-d/Donchian/Donchian.Models.cs | 2 +- src/a-d/Dpo/Dpo.Api.cs | 25 +- src/a-d/Dpo/Dpo.Models.cs | 13 +- src/a-d/Dpo/Dpo.Series.cs | 22 +- src/a-d/Dynamic/Dynamic.Api.cs | 19 +- src/a-d/Dynamic/Dynamic.Models.cs | 5 +- src/e-k/ElderRay/ElderRay.Models.cs | 5 +- src/e-k/Ema/Ema.Api.cs | 46 +- src/e-k/Ema/Ema.Common.cs | 6 +- src/e-k/Ema/Ema.Models.cs | 15 +- src/e-k/Ema/Ema.Series.cs | 12 +- src/e-k/Ema/Ema.Stream.cs | 63 +-- src/e-k/Ema/info.xml | 31 +- src/e-k/Epma/Epma.Api.cs | 18 +- src/e-k/Epma/Epma.Models.cs | 5 +- src/e-k/Fcb/Fcb.Models.cs | 2 +- .../FisherTransform/FisherTransform.Api.cs | 18 +- .../FisherTransform/FisherTransform.Models.cs | 5 +- src/e-k/ForceIndex/ForceIndex.Models.cs | 5 +- src/e-k/Fractal/Fractal.Models.cs | 2 +- src/e-k/Gator/Gator.Api.cs | 37 +- src/e-k/Gator/info.xml | 15 - src/e-k/HeikinAshi/HeikinAshi.Models.cs | 6 +- src/e-k/Hma/Hma.Api.cs | 18 +- src/e-k/Hma/Hma.Models.cs | 5 +- src/e-k/HtTrendline/HtTrendline.Api.cs | 25 +- src/e-k/HtTrendline/HtTrendline.Models.cs | 5 +- src/e-k/HtTrendline/info.xml | 15 - src/e-k/Hurst/Hurst.Api.cs | 17 +- src/e-k/Hurst/Hurst.Models.cs | 5 +- src/e-k/Ichimoku/Ichimoku.Models.cs | 2 +- src/e-k/Ichimoku/Ichimoku.Series.cs | 2 +- src/e-k/Kama/Kama.Api.cs | 19 +- src/e-k/Kama/Kama.Models.cs | 5 +- src/e-k/Keltner/Keltner.Models.cs | 2 +- src/e-k/Kvo/Kvo.Models.cs | 5 +- src/m-r/MaEnvelopes/MaEnvelopes.Api.cs | 29 +- src/m-r/MaEnvelopes/MaEnvelopes.Models.cs | 2 +- src/m-r/MaEnvelopes/MaEnvelopes.Series.cs | 55 ++- src/m-r/Macd/Macd.Models.cs | 5 +- src/m-r/Macd/MacdApi.cs | 20 +- src/m-r/Mama/Mama.Api.cs | 19 +- src/m-r/Mama/Mama.Models.cs | 5 +- src/m-r/Mfi/Mfi.Models.cs | 5 +- src/m-r/Obv/Obv.Models.cs | 7 +- src/m-r/ParabolicSar/ParabolicSar.Models.cs | 5 +- src/m-r/ParabolicSar/ParabolicSar.Series.cs | 9 +- src/m-r/PivotPoints/PivotPoints.Models.cs | 2 +- src/m-r/PivotPoints/PivotPoints.Series.cs | 20 +- src/m-r/Pmo/Pmo.Api.cs | 20 +- src/m-r/Pmo/Pmo.Models.cs | 5 +- src/m-r/Pmo/Pmo.Series.cs | 2 +- src/m-r/Prs/Prs.Api.cs | 24 +- src/m-r/Prs/Prs.Models.cs | 8 +- src/m-r/Pvo/Pvo.Models.cs | 5 +- src/m-r/Renko/Renko.Models.cs | 6 +- src/m-r/Renko/RenkoAtr.Series.cs | 8 +- src/m-r/Roc/Roc.Api.cs | 18 +- src/m-r/Roc/Roc.Models.cs | 8 +- src/m-r/RocWb/RocWb.Api.cs | 20 +- src/m-r/RocWb/RocWb.Models.cs | 5 +- src/m-r/RocWb/RocWb.Series.cs | 2 +- src/m-r/RollingPivots/RollingPivots.Models.cs | 2 +- src/m-r/Rsi/Rsi.Api.cs | 18 +- src/m-r/Rsi/Rsi.Models.cs | 5 +- src/s-z/Slope/Slope.Api.cs | 26 +- src/s-z/Slope/Slope.Models.cs | 5 +- src/s-z/Slope/Slope.Series.cs | 4 +- src/s-z/Sma/Sma.Analysis.cs | 16 +- src/s-z/Sma/Sma.Api.cs | 75 +-- src/s-z/Sma/Sma.Common.cs | 6 +- src/s-z/Sma/Sma.Models.cs | 40 +- src/s-z/Sma/Sma.Series.cs | 20 +- src/s-z/Sma/Sma.Stream.cs | 60 +-- src/s-z/Sma/info.xml | 32 +- src/s-z/Smi/Smi.Models.cs | 5 +- src/s-z/Smma/Smma.Api.cs | 18 +- src/s-z/Smma/Smma.Models.cs | 5 +- src/s-z/StarcBands/StarcBands.Models.cs | 2 +- src/s-z/Stc/Stc.Api.cs | 20 +- src/s-z/Stc/Stc.Models.cs | 5 +- src/s-z/StdDev/StdDev.Api.cs | 18 +- src/s-z/StdDev/StdDev.Models.cs | 8 +- src/s-z/StdDevChannels/StdDevChannels.Api.cs | 19 +- .../StdDevChannels/StdDevChannels.Models.cs | 2 +- src/s-z/Stoch/Stoch.Models.cs | 11 +- src/s-z/StochRsi/StochRsi.Api.cs | 25 +- src/s-z/StochRsi/StochRsi.Models.cs | 5 +- src/s-z/SuperTrend/SuperTrend.Models.cs | 2 +- src/s-z/T3/T3.Api.cs | 19 +- src/s-z/T3/T3.Models.cs | 5 +- src/s-z/Tema/Tema.Api.cs | 18 +- src/s-z/Tema/Tema.Models.cs | 5 +- src/s-z/Tr/Tr.Models.cs | 5 +- src/s-z/Trix/Trix.Api.cs | 18 +- src/s-z/Trix/Trix.Models.cs | 8 +- src/s-z/Tsi/Tsi.Api.cs | 20 +- src/s-z/Tsi/Tsi.Models.cs | 5 +- src/s-z/UlcerIndex/UlcerIndex.Api.cs | 18 +- src/s-z/UlcerIndex/UlcerIndex.Models.cs | 7 +- src/s-z/Ultimate/Ultimate.Models.cs | 5 +- .../VolatilityStop/VolatilityStop.Models.cs | 5 +- .../VolatilityStop/VolatilityStop.Series.cs | 22 +- src/s-z/Vortex/Vortex.Models.cs | 2 +- src/s-z/Vwap/Vwap.Models.cs | 5 +- src/s-z/Vwma/Vwma.Models.cs | 5 +- src/s-z/WilliamsR/WilliamsR.Models.cs | 5 +- src/s-z/Wma/Wma.Api.cs | 18 +- src/s-z/Wma/Wma.Models.cs | 5 +- src/s-z/ZigZag/ZigZag.Models.cs | 5 +- tests/indicators/TestBase.cs | 84 ++-- .../indicators/_common/Generics/Seek.Tests.cs | 13 +- tests/indicators/_common/Helper.Importer.cs | 52 +- .../Observables/ChainProvider.Tests.cs | 35 +- .../Observables/QuoteProvider.Tests.cs | 10 +- .../_common/Quotes/Quote.Converters.Tests.cs | 24 +- .../_common/Results/Result.Utilities.Tests.cs | 8 +- .../Use/Use.Calc.xlsx} | Bin .../Use/Use.Tests.cs} | 46 +- .../a-d/Alligator/Alligator.Series.Tests.cs | 24 +- .../a-d/Alligator/Alligator.Stream.Tests.cs | 10 +- tests/indicators/a-d/Dpo/Dpo.Tests.cs | 11 - tests/indicators/e-k/Ema/Ema.Series.Tests.cs | 13 +- tests/indicators/e-k/Ema/Ema.Stream.Tests.cs | 24 +- tests/indicators/e-k/Gator/Gator.Tests.cs | 11 - .../m-r/MaEnvelopes/MaEnvelopes.Tests.cs | 11 - .../indicators/s-z/Sma/Sma.Analysis.Tests.cs | 11 - tests/indicators/s-z/Sma/Sma.Series.Tests.cs | 11 - tests/indicators/s-z/Sma/Sma.Stream.Tests.cs | 26 +- tests/observe/Program.cs | 22 +- tests/other/CustomIndicator.Tests.cs | 39 +- tests/other/PublicApi.Tests.cs | 54 +-- .../Perf.Indicators.Stream.External.cs | 2 +- tests/performance/Perf.Indicators.Stream.cs | 4 +- 208 files changed, 2257 insertions(+), 3094 deletions(-) rename src/_common/Generics/{Series.Model.cs => ISeries.cs} (100%) create mode 100644 src/_common/Observables/AbstractCache.cs create mode 100644 src/_common/Observables/AbstractChainInChainOut.cs create mode 100644 src/_common/Observables/AbstractChainInResultOut.cs create mode 100644 src/_common/Observables/AbstractProvider.cs create mode 100644 src/_common/Observables/AbstractQuoteInChainOut.cs create mode 100644 src/_common/Observables/AbstractQuoteInQuoteOut.cs delete mode 100644 src/_common/Observables/Cache.cs delete mode 100644 src/_common/Observables/ChainObserver.cs delete mode 100644 src/_common/Observables/ChainProvider.cs create mode 100644 src/_common/Observables/IStreamCache.cs create mode 100644 src/_common/Observables/IStreamObserver.cs create mode 100644 src/_common/Observables/IStreamProvider.cs delete mode 100644 src/_common/Observables/Interfaces.cs delete mode 100644 src/_common/Observables/QuoteObserver.cs rename src/_common/Results/{Result.Models.cs => IResult.cs} (100%) delete mode 100644 src/_common/Use (quote converter)/info.xml create mode 100644 src/a-d/Adl/Adl.Stream.cs delete mode 100644 src/a-d/Alligator/info.xml delete mode 100644 src/a-d/BasicQuote/BasicQuote.Api.cs delete mode 100644 src/a-d/BasicQuote/BasicQuote.Common.cs delete mode 100644 src/a-d/BasicQuote/BasicQuote.Models.cs delete mode 100644 src/a-d/BasicQuote/info.xml delete mode 100644 src/e-k/Gator/info.xml delete mode 100644 src/e-k/HtTrendline/info.xml rename tests/indicators/{a-d/BasicQuote/BasicQuote.Calc.xlsx => _common/Use/Use.Calc.xlsx} (100%) rename tests/indicators/{a-d/BasicQuote/BasicQuote.Tests.cs => _common/Use/Use.Tests.cs} (50%) 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/docs/pages/utilities.md b/docs/pages/utilities.md index fed3e87bc..40459183c 100644 --- a/docs/pages/utilities.md +++ b/docs/pages/utilities.md @@ -125,6 +125,8 @@ IEnumerable 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 diff --git a/src/_common/Candles/Candles.Models.cs b/src/_common/Candles/Candles.Models.cs index 9dff2904b..b870fcaf5 100644 --- a/src/_common/Candles/Candles.Models.cs +++ b/src/_common/Candles/Candles.Models.cs @@ -2,34 +2,39 @@ namespace Skender.Stock.Indicators; // CANDLESTICK MODELS -public record CandleProperties : Quote +public record struct CandleProperties : IQuote, IReusableResult { + // base quote properties + public DateTime Timestamp { get; set; } + public decimal Open { get; set; } + public decimal High { get; set; } + public decimal Low { get; set; } + public decimal Close { get; set; } + public decimal Volume { get; set; } + + readonly double IReusableResult.Value + => (double)Close; + // 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; + public readonly decimal? Size => High - Low; + public readonly decimal? Body => (Open > Close) ? (Open - Close) : (Close - Open); + public readonly decimal? UpperWick => High - (Open > Close ? Open : Close); + public readonly 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; + public readonly double? BodyPct => (Size != 0) ? (double?)(Body / Size) : 1; + public readonly double? UpperWickPct => (Size != 0) ? (double?)(UpperWick / Size) : 1; + public readonly double? LowerWickPct => (Size != 0) ? (double?)(LowerWick / Size) : 1; // directional info - public bool IsBullish => Close > Open; - public bool IsBearish => Close < Open; + public readonly bool IsBullish => Close > Open; + public readonly bool IsBearish => Close < Open; } -public record class CandleResult : IResult +public record struct CandleResult : IResult { - public CandleResult(DateTime date, Match match) - { - Timestamp = date; - Match = match; - } - - public DateTime Timestamp { get; private set; } + public DateTime Timestamp { get; set; } public decimal? Price { get; set; } public Match Match { get; set; } - public CandleProperties Candle { get; set; } = new CandleProperties(); + public CandleProperties Candle { get; set; } } diff --git a/src/_common/Candles/Candles.cs b/src/_common/Candles/Candles.cs index ea7a58f32..aab413d36 100644 --- a/src/_common/Candles/Candles.cs +++ b/src/_common/Candles/Candles.cs @@ -21,36 +21,20 @@ public static CandleProperties ToCandle( // convert/sort quotes into candles list public static IEnumerable ToCandles( this IEnumerable quotes) - where TQuote : IQuote - { - List candlesList = - [ - .. quotes + where TQuote : IQuote => quotes .Select(x => x.ToCandle()) .OrderBy(x => x.Timestamp) - ]; - - // validate - return candlesList; - } + .ToList(); // convert/sort quotes into candle results internal static List ToCandleResults( this IEnumerable quotes) - where TQuote : IQuote - { - List candlesList = - [ - .. quotes - .Select(x => new CandleResult( - x.Timestamp, - Match.None) { - Candle = x.ToCandle() + where TQuote : IQuote => quotes + .Select(q => new CandleResult() { + Timestamp = q.Timestamp, + Match = Match.None, + Candle = q.ToCandle() }) - .OrderBy(x => x.Timestamp) - ]; - - // validate - return candlesList; - } + .OrderBy(c => c.Timestamp) + .ToList(); } diff --git a/src/_common/Enums.cs b/src/_common/Enums.cs index 08bc12a9b..d966e5254 100644 --- a/src/_common/Enums.cs +++ b/src/_common/Enums.cs @@ -3,6 +3,9 @@ namespace Skender.Stock.Indicators; // SHARED ENUMERATIONS // note: indicator unique ENUMS specified in indicator models +/// +/// Part or value of a quote candle +/// public enum CandlePart { Open, @@ -17,12 +20,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 +44,9 @@ public enum Match BearConfirmed = -200 } +/// +/// Moving average type +/// public enum MaType { ALMA, @@ -50,6 +62,10 @@ public enum MaType WMA } +/// +/// Period size. Usually referring to the +/// time period represented in a quote candle. +/// public enum PeriodSize { Month, @@ -66,6 +82,9 @@ public enum PeriodSize OneMinute } +/// +/// The action taken on the cache (outcome) +/// public enum Act { AddNew, diff --git a/src/_common/Generics/Series.Model.cs b/src/_common/Generics/ISeries.cs similarity index 100% rename from src/_common/Generics/Series.Model.cs rename to src/_common/Generics/ISeries.cs diff --git a/src/_common/Generics/Seek.cs b/src/_common/Generics/Seek.cs index 4c7f86aea..57a37f6dc 100644 --- a/src/_common/Generics/Seek.cs +++ b/src/_common/Generics/Seek.cs @@ -5,17 +5,29 @@ namespace Skender.Stock.Indicators; public static class Seeking { // FIND SERIES by DATE - /// - /// + /// 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. + /// + /// Sequence contains no matching element + /// public static TSeries? Find( this IEnumerable series, DateTime lookupDate) where TSeries : ISeries => series - .FirstOrDefault(x => x.Timestamp == lookupDate); + .First(x => x.Timestamp == lookupDate); + + // TODO: add TryFind(), like TryParse() since struct won't allow null return types. + // May just use this (above) with a try/catch and `bool` primary return type. // FIND INDEX by DATE /// - /// public static int FindIndex( this List series, DateTime lookupDate) @@ -25,7 +37,6 @@ public static int FindIndex( // FIND INDEX by DATE /// - /// public static int FindIndex( this List<(DateTime Timestamp, double Value)> tuple, DateTime lookupDate) diff --git a/src/_common/Generics/info.xml b/src/_common/Generics/info.xml index bdd7a6e2f..b5b0c4ac0 100644 --- a/src/_common/Generics/info.xml +++ b/src/_common/Generics/info.xml @@ -2,18 +2,6 @@ - - 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. @@ -53,4 +41,4 @@ Invalid parameter value provided. - \ No newline at end of file + diff --git a/src/_common/Observables/AbstractCache.cs b/src/_common/Observables/AbstractCache.cs new file mode 100644 index 000000000..de9ec99c4 --- /dev/null +++ b/src/_common/Observables/AbstractCache.cs @@ -0,0 +1,374 @@ +namespace Skender.Stock.Indicators; + +// CACHE PROVIDER + +/// +/// Base cache and management utilities +/// +public abstract class AbstractCache : IStreamCache + where TSeries : struct, ISeries +{ + // CONSTRUCTORS + + /// + /// Default. Use internal cache. + /// + internal AbstractCache() + { + Cache = []; + } + + /// + /// Optional. Use externally provided cache. + /// + /// + /// + /// DO NOT USE. This is for future consideration only, + /// to allow users to provide their own cache storage location. + /// + internal AbstractCache( + List externalCache) + { + Cache = externalCache; + throw new NotImplementedException(); + } + + // PROPERTIES + + public IReadOnlyList Results => Cache; + + public bool IsFaulted { get; internal set; } + + internal List Cache { get; set; } + + internal TSeries LastArrival { get; set; } = new(); + + internal int OverflowCount { get; set; } + + + // METHODS + + // get a segment of the cache + /// + public IReadOnlyList GetRange(int index, int count) + => Cache.GetRange(index, count); + + // get the cache index based on a timestamp + /// + public int FindIndex(DateTime timeStamp) + => Cache.FindIndex(x => x.Timestamp == timeStamp); + + // clear entire cache without restore + /// + public void ClearCache() + { + // nothing to do + if (Cache.Count == 0) + { + Cache = []; + return; + } + + // reset all (with handling) + ClearCache(0); + } + + /// + /// + /// `fromTimestamp` not found in cache + /// + public void ClearCache(DateTime fromTimestamp) + { + int s = Cache.FindIndex(fromTimestamp); // start of range + + if (s == -1) + { + throw new InvalidOperationException( + "Cache clear starting target not found" + + " for provided timestamp."); + } + + ClearCache(s); + } + + /// + /// Deletes all cache entries after `fromIndex` (inclusive) + /// + /// From index, inclusive + internal void ClearCache(int fromIndex) + => ClearCache(fromIndex, toIndex: Math.Max(0, Cache.Count - 1)); + + /// + /// Deletes cache entries between index range values. + /// + /// + /// This is usually overridden/implemented in inheriting + /// classes due to unique requirements to notify subscribers. + /// + /// First element to delete + /// Last element to delete + internal virtual void ClearCache(int fromIndex, int toIndex) + { + for (int i = toIndex; i >= fromIndex; i--) + { + TSeries r = Cache[i]; + ModifyCache(Act.Delete, r); + } + } + + /// + /// Analyze new arrival to determine caching instruction; + /// then follow-on with caching action. + /// + /// + /// Fully formed cacheable time-series object. + /// + /// Action taken (outcome) + /// + /// + internal Act CacheWithAnalysis(TSeries item) + { + // Currently, only inbound Quote is accepted as an + // external chain entry point and is the only type + // using this method. -- DS 12/4/2023 + + // TODO: consider moving analysis to QuoteProvider, + // if it's the only user. + + // check format and overflow + if (CheckOverflow(item) is Act.DoNothing) + { + return Act.DoNothing; + }; + + + // DETERMINE ACTion INSTRUCTION + + Act act; + List cache = Cache; + int length = cache.Count; + + // first + if (length == 0) + { + act = Act.AddNew; + return ModifyCache(act, item); + } + + TSeries last = cache[length - 1]; + + // newer + if (item.Timestamp > last.Timestamp) + { + act = Act.AddNew; + } + + // repeat or late arrival + else + { + // seek duplicate + int foundIndex = cache + .FindIndex(x => x.Timestamp == item.Timestamp); + + // replace duplicate + act = foundIndex == -1 ? Act.AddOld : Act.Update; + } + + // perform actual update, return final action + return ModifyCache(act, item); + } + + /// + /// Analyze and DELETE new arrivals from cache, + /// after validating best instruction. + /// + /// + /// Fully formed cacheable time-series object. + /// + /// Action taken (outcome) + /// + /// + internal Act PurgeWithAnalysis(TSeries item) + { + // check format and overflow + if (CheckOverflow(item) is Act.DoNothing) + { + return Act.DoNothing; + }; + + // determine if record exists + int foundIndex = Cache + .FindIndex(x => x.Timestamp == item.Timestamp); + + // not found + if (foundIndex == -1) + { + return Act.DoNothing; + } + + TSeries t = Cache[foundIndex]; + + // delete if full match + return t.Equals(item) + ? ModifyCache(Act.Delete, t) + : Act.DoNothing; + } + + /// + /// Update cache, per "act" instruction. + /// + /// Caching instruction + /// + /// Fully formed cacheable time-series object. + /// + /// Action taken (outcome) + /// + internal Act ModifyCache(Act act, TSeries item) + { + // execute action + switch (act) + { + case Act.AddNew: + + Cache.Add(item); + + break; + + case Act.AddOld: + + // find + int ao = Cache.FindIndex(x => x.Timestamp > item.Timestamp); + + // insert + if (ao != -1) + { + Cache.Insert(ao, item); + } + + // failure to find should never happen + else + { + throw new InvalidOperationException( + "Cache insert target not found."); + } + + break; + + case Act.Update: + + // find + int uo = Cache.FindIndex(item.Timestamp); + + // replace + Cache[uo] = uo != -1 + ? item + : throw new InvalidOperationException( + "Cache update target not found."); + + break; + + case Act.Delete: + + // find + int d = Cache.FindIndex(item.Timestamp); + + // delete + if (d != -1) + { + Cache.RemoveAt(d); + } + + // failure to find should never happen + else + { + throw new InvalidOperationException( + "Cache delete target not found."); + } + + break; + + case Act.DoNothing: + + break; + + // should never get here + default: + + throw new InvalidOperationException( + "Undefined cache action."); + } + + IsFaulted = false; + return act; + } + + /// + /// Validate inbound item and compare to prior arrivals + /// to manage and prevent overflow conditions. + /// + /// Overflow can occur for many reasons; + /// the most aggregious being a circular subscription, + /// set by an external user. + /// + /// + /// + /// Fully formed cacheable time-series object. + /// + /// + /// An "do nothing" act instruction if duplicate or 'null' + /// + /// + /// + private Act? CheckOverflow(TSeries item) + { + Act? act = null; + + // check for overflow condition + if (item.Timestamp == LastArrival.Timestamp) + { + // note: we have a better IsEqual() comparison method below, + // but it is too expensive as an initial quick evaluation. + + OverflowCount++; + + if (OverflowCount > 100) + { + 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; + + throw new OverflowException(msg); + + // note: overflow exception is also further handled by providers, + // where it will EndTransmission(); and then throw error to user. + } + + // aggressive property value comparison + // TODO: not handling add-back after delete, registers as dup + if (item.Equals(LastArrival)) + { + // to prevent propogation + // of identical cache entry + act = Act.DoNothing; + } + + // same date with different values + // continues as an update + else + { + LastArrival = item; + } + } + else + { + OverflowCount = 0; + LastArrival = item; + } + + return act; + } +} diff --git a/src/_common/Observables/AbstractChainInChainOut.cs b/src/_common/Observables/AbstractChainInChainOut.cs new file mode 100644 index 000000000..341dd4a29 --- /dev/null +++ b/src/_common/Observables/AbstractChainInChainOut.cs @@ -0,0 +1,85 @@ +namespace Skender.Stock.Indicators; + +public abstract class AbstractChainInChainOut + : AbstractChainProvider, IChainObserver + where TIn : struct, IReusableResult + where TOut : struct, IReusableResult +{ + internal AbstractChainInChainOut( + IChainProvider provider) + { + Provider = provider; + } + + // reminder: these are the subscriber members only + + // PROPERTIES + + public IChainProvider Provider { get; internal set; } + + public bool IsSubscribed => Subscription is not null; + + internal IDisposable? Subscription; + + protected IReadOnlyList ProviderCache => Provider.Results; + + + // METHODS + + public void OnNext((Act, TIn) value) + => OnNextArrival(value.Item1, value.Item2); + + internal abstract void OnNextArrival(Act act, IReusableResult inbound); + + public void OnError(Exception error) => throw error; + + public void OnCompleted() => Unsubscribe(); + + public void Unsubscribe() => Subscription?.Dispose(); + + // clear and resubscribe + public void Reinitialize(bool withRebuild = true) + { + Unsubscribe(); + Subscription = Provider.Subscribe(this); + + if (withRebuild) + { + RebuildCache(); + } + else + { + ClearCache(); + } + } + + // rebuild cache + public void RebuildCache() => RebuildCache(0); + + // rebuild cache from date + public void RebuildCache(DateTime fromTimestamp) + => RebuildCache(fromTimestamp, 0); + + // rebuild cache from timestamp + private void RebuildCache( + DateTime fromTimestamp, int offset = 0) + { + int fromIndex = Cache + .FindIndex(fromTimestamp); + + if (fromIndex == -1) + { + throw new InvalidOperationException( + "Cache rebuild starting date not found."); + } + + RebuildCache(fromIndex, offset); + } + + // rebuild cache from index + private void RebuildCache(int fromIndex, int offset = 0) + { + ClearCache(fromIndex, offset); + Provider.Resend(fromIndex, this); + } +} diff --git a/src/_common/Observables/AbstractChainInResultOut.cs b/src/_common/Observables/AbstractChainInResultOut.cs new file mode 100644 index 000000000..038239189 --- /dev/null +++ b/src/_common/Observables/AbstractChainInResultOut.cs @@ -0,0 +1,85 @@ +namespace Skender.Stock.Indicators; + +public abstract class AbstractChainInResultOut + : AbstractResultProvider, IChainObserver + where TIn : struct, IReusableResult + where TOut : struct, IResult +{ + internal AbstractChainInResultOut( + IChainProvider provider) + { + Provider = provider; + } + + // reminder: these are the subscriber members only + + // PROPERTIES + + public IChainProvider Provider { get; internal set; } + + public bool IsSubscribed => Subscription is not null; + + internal IDisposable? Subscription; + + protected IReadOnlyList ProviderCache => Provider.Results; + + + // METHODS + + public void OnNext((Act, TIn) value) + => OnNextArrival(value.Item1, value.Item2); + + internal abstract void OnNextArrival(Act act, IReusableResult inbound); + + public void OnError(Exception error) => throw error; + + public void OnCompleted() => Unsubscribe(); + + public void Unsubscribe() => Subscription?.Dispose(); + + // clear and resubscribe + public void Reinitialize(bool withRebuild = true) + { + Unsubscribe(); + Subscription = Provider.Subscribe(this); + + if (withRebuild) + { + RebuildCache(); + } + else + { + ClearCache(); + } + } + + // rebuild cache + public void RebuildCache() => RebuildCache(0); + + // rebuild cache from date + public void RebuildCache(DateTime fromTimestamp) + => RebuildCache(fromTimestamp, 0); + + // rebuild cache from timestamp + private void RebuildCache( + DateTime fromTimestamp, int offset = 0) + { + int fromIndex = Cache + .FindIndex(fromTimestamp); + + if (fromIndex == -1) + { + throw new InvalidOperationException( + "Cache rebuild starting date not found."); + } + + RebuildCache(fromIndex, offset); + } + + // rebuild cache from index + private void RebuildCache(int fromIndex, int offset = 0) + { + ClearCache(fromIndex, offset); + Provider.Resend(fromIndex, this); + } +} diff --git a/src/_common/Observables/AbstractProvider.cs b/src/_common/Observables/AbstractProvider.cs new file mode 100644 index 000000000..0bb4cc5c6 --- /dev/null +++ b/src/_common/Observables/AbstractProvider.cs @@ -0,0 +1,149 @@ +namespace Skender.Stock.Indicators; + +// STREAM PROVIDERS (BASE) + +#region interface variants +/// +/// Quote provider (abstract base) +/// +public abstract class AbstractQuoteProvider + : AbstractProvider, IQuoteProvider, IChainProvider + where TQuote : struct, IQuote, IReusableResult +{ + // string label + public override string ToString() + => $"{Cache.Count} quotes (type: {nameof(TQuote)})"; +} + +/// +/// Chainable result provider (abstract base) +/// +public abstract class AbstractChainProvider + : AbstractProvider, IChainProvider + where TReusableResult : struct, IReusableResult; + +/// +/// Non-chainable result provider (abstract base) +/// +public abstract class AbstractResultProvider + : AbstractProvider, IResultProvider + where TResult : struct, IResult; +#endregion + +/// +/// Streaming provider (abstract base) +/// +public abstract class AbstractProvider + : AbstractCache, IStreamProvider + where TSeries : struct, ISeries +{ + // reminder: these are the provider members only + + // fields + private readonly HashSet> Observers = new(); + + // PROPERTIES + + public bool HasSubscribers => Observers.Count > 0; + + public int SubscriberCount => Observers.Count; + + // METHODS + + // string label + public abstract override string ToString(); + + // subscribe observer + public IDisposable Subscribe(IObserver<(Act, TSeries)> observer) + { + Observers.Add(observer); + return new Subscription(Observers, observer); + } + + // unsubscribe all observers + public void EndTransmission() + { + foreach (IObserver<(Act, TSeries)> obs in Observers.ToArray()) + { + if (Observers.Contains(obs)) + { + obs.OnCompleted(); + } + } + + Observers.Clear(); + } + + // resend to an observer + /// + public void Resend( + int fromIndex, + IObserver<(Act, TSeries)> toObserver) + { + if (toObserver is not null && Observers.Contains(toObserver)) + { + for (int i = fromIndex; i < Cache.Count; i++) + { + toObserver.OnNext((Act.AddOld, Cache[i])); + } + } + + throw new NotImplementedException("unsure if needed"); + } + + // clears cache segment + /// + internal override void ClearCache(int fromIndex, int toIndex) + { + // delete and deliver instruction in reverse + // order to prevent recursive recompositions + + for (int i = toIndex; i >= fromIndex; i--) + { + TSeries r = Cache[i]; + Act act = ModifyCache(Act.Delete, r); + NotifyObservers(act, r); + } + } + + /// + /// Sends new item to all subscribers + /// + /// + /// + protected void NotifyObservers(Act act, TSeries item) + { + // do not propogate "do nothing" acts + if (act == Act.DoNothing) + { + return; + } + + // send to subscribers + List> obsList = [.. Observers]; + + for (int i = 0; i < obsList.Count; i++) + { + IObserver<(Act, TSeries)> obs = obsList[i]; + obs.OnNext((act, item)); + } + } + + /// + /// A disposable subscription to the stream provider. + /// Unsubscribed with + /// + /// + /// Registry of all subscribers (by ref) + /// + /// + /// Your unique subscription as provided. + /// + private class Subscription( + ISet> observers, + IObserver<(Act, TSeries)> observer) : IDisposable + { + // remove single observer + public void Dispose() => observers.Remove(observer); + } +} diff --git a/src/_common/Observables/AbstractQuoteInChainOut.cs b/src/_common/Observables/AbstractQuoteInChainOut.cs new file mode 100644 index 000000000..2a4965f78 --- /dev/null +++ b/src/_common/Observables/AbstractQuoteInChainOut.cs @@ -0,0 +1,85 @@ +namespace Skender.Stock.Indicators; + +public abstract class AbstractQuoteInChainOut + : AbstractChainProvider, IQuoteObserver + where TIn : struct, IQuote + where TOut : struct, IReusableResult +{ + internal AbstractQuoteInChainOut( + IQuoteProvider provider) + { + Provider = provider; + } + + // reminder: these are the subscriber members only + + // PROPERTIES + + public IQuoteProvider Provider { get; internal set; } + + public bool IsSubscribed => Subscription is not null; + + internal IDisposable? Subscription; + + protected IReadOnlyList ProviderCache => Provider.Results; + + + // METHODS + + public void OnNext((Act, TIn) value) + => OnNextArrival(value.Item1, value.Item2); + + internal abstract void OnNextArrival(Act act, IQuote inbound); + + public void OnError(Exception error) => throw error; + + public void OnCompleted() => Unsubscribe(); + + public void Unsubscribe() => Subscription?.Dispose(); + + // clear and resubscribe + public void Reinitialize(bool withRebuild = true) + { + Unsubscribe(); + Subscription = Provider.Subscribe(this); + + if (withRebuild) + { + RebuildCache(); + } + else + { + ClearCache(); + } + } + + // rebuild cache + public void RebuildCache() => RebuildCache(0); + + // rebuild cache from date + public void RebuildCache(DateTime fromTimestamp) + => RebuildCache(fromTimestamp, 0); + + // rebuild cache from timestamp + private void RebuildCache( + DateTime fromTimestamp, int offset = 0) + { + int fromIndex = Cache + .FindIndex(fromTimestamp); + + if (fromIndex == -1) + { + throw new InvalidOperationException( + "Cache rebuild starting date not found."); + } + + RebuildCache(fromIndex, offset); + } + + // rebuild cache from index + private void RebuildCache(int fromIndex, int offset = 0) + { + ClearCache(fromIndex, offset); + Provider.Resend(fromIndex, this); + } +} diff --git a/src/_common/Observables/AbstractQuoteInQuoteOut.cs b/src/_common/Observables/AbstractQuoteInQuoteOut.cs new file mode 100644 index 000000000..7ab34a91e --- /dev/null +++ b/src/_common/Observables/AbstractQuoteInQuoteOut.cs @@ -0,0 +1,85 @@ +namespace Skender.Stock.Indicators; + +public abstract class AbstractQuoteInQuoteOut + : AbstractQuoteProvider, IQuoteObserver + where TIn : struct, IQuote, IReusableResult + where TOut : struct, IQuote, IReusableResult +{ + internal AbstractQuoteInQuoteOut( + IQuoteProvider provider) + { + Provider = provider; + } + + // reminder: these are the observer members only + + // PROPERTIES + + public IQuoteProvider Provider { get; internal set; } + + public bool IsSubscribed => Subscription is not null; + + internal IDisposable? Subscription; + + protected IReadOnlyList ProviderCache => Provider.Results; + + + // METHODS + + public void OnNext((Act, TIn) value) + => OnNextArrival(value.Item1, value.Item2); + + internal abstract void OnNextArrival(Act act, IQuote inbound); + + public void OnError(Exception error) => throw error; + + public void OnCompleted() => Unsubscribe(); + + public void Unsubscribe() => Subscription?.Dispose(); + + // clear and resubscribe + public void Reinitialize(bool withRebuild = true) + { + Unsubscribe(); + Subscription = Provider.Subscribe(this); + + if (withRebuild) + { + RebuildCache(); + } + else + { + ClearCache(); + } + } + + // rebuild cache + public void RebuildCache() => RebuildCache(0); + + // rebuild cache from date + public void RebuildCache(DateTime fromTimestamp) + => RebuildCache(fromTimestamp, 0); + + // rebuild cache from timestamp + private void RebuildCache( + DateTime fromTimestamp, int offset = 0) + { + int fromIndex = Cache + .FindIndex(fromTimestamp); + + if (fromIndex == -1) + { + throw new InvalidOperationException( + "Cache rebuild starting date not found."); + } + + RebuildCache(fromIndex, offset); + } + + // rebuild cache from index + private void RebuildCache(int fromIndex, int offset = 0) + { + ClearCache(fromIndex, offset); + Provider.Resend(fromIndex, this); + } +} diff --git a/src/_common/Observables/Cache.cs b/src/_common/Observables/Cache.cs deleted file mode 100644 index 80fa1edba..000000000 --- a/src/_common/Observables/Cache.cs +++ /dev/null @@ -1,444 +0,0 @@ -namespace Skender.Stock.Indicators; - -// base cache for Quotes -public abstract class QuoteCache - : StreamCache - where TQuote : IQuote, new() -{ - internal QuoteCache() - : base(isChainor: false) { } - - public IEnumerable Quotes => Cache; -} - -// base cache for Indicator results -public abstract class ResultCache - : StreamCache - where TResult : IResult, new() -{ - internal ResultCache(bool isChainor) - : base(isChainor) { } - - public IEnumerable Results => Cache; -} - -// base result or series cache -/// -public abstract class StreamCache - : ChainProvider, IStreamCache - where TSeries : ISeries, new() -{ - // fields - private readonly bool isChainor; - - // constructor - protected internal StreamCache(bool isChainor) - { - Cache = []; - LastArrival = new(); - OverflowCount = 0; - this.isChainor = isChainor; - } - - // PROPERTIES - - internal List Cache { get; set; } - - internal TSeries LastArrival { get; set; } - - internal int OverflowCount { get; set; } - - // METHODS - - /// - public abstract override string ToString(); - - /// - /// Deletes all cache and chain records, gracefully - /// - public void ClearCache() - { - // nothing to do - if (Cache.Count == 0) - { - Cache = []; - Chain = []; - return; - } - - // reset all - ClearCache(0); - } - - /// - /// Deletes all cache entries after `fromDate` (inclusive) - /// - /// From date, inclusive - /// - /// `fromDate` not found - /// - public void ClearCache(DateTime fromTimestamp) - { - int s = Cache.FindIndex(fromTimestamp); // start of range - - if (s == -1) - { - throw new InvalidOperationException( - "Cache clear starting target not found."); - } - - ClearCache(s); - } - - /// - /// Deletes all cache entries after `fromIndex` (inclusive) - /// - /// From index, inclusive - internal void ClearCache(int fromIndex) => ClearCache(fromIndex, toIndex: Cache.Count - 1); - - /// - /// Deletes cache and chain entries between index range values, inclusively. - /// It is implemented in inheriting classes due to unique requirements. - /// - /// First element to delete - /// Last element to delete - internal abstract void ClearCache(int fromIndex, int toIndex); - - /// - /// Replay from supplier cache start date, inclusive - /// - /// First element to rebuild - /// Offset start index - /// - internal abstract void RebuildCache(DateTime fromTimestamp, int offset = 0); - - /// - /// Replay from supplier cache index, inclusive - /// - /// First element to rebuild - /// Offset start index - internal abstract void RebuildCache(int fromIndex, int offset = 0); - - /// - /// Overload for non-chainable cachors that do not store chain values. - /// - /// - /// Action taken - internal Act CacheWithAnalysis(TSeries r) => CacheWithAnalysis(r, double.NaN); - - /// - /// Analyze and ADD new arrival to cache, after determining best instruction. - /// - /// - /// Fully formed cacheable time-series object. - /// - /// Meaningful chain observable value. Unused if not a Chainor. - /// Action taken - /// - /// - internal Act CacheWithAnalysis(TSeries r, double value) - { - /* ANALYZE NEW VALUE (then act) - * - * Analyze new indicator value against the cache for proper handling. - * Note the "value" is optional as only some indicators are chainable - * and would need to compile the Chain cache. Only the main Cache is - * evaluated as both are expected to be synchronized automatically in - * the following AddPerAction method. The IsChainor boolean is set - * to simplify the determination of whether TSeries is a chainable type. - * - * Currently, only inbound Quote is accepted as an external chain entry - * point and is the only type using this method. -- DS 12/4/2023 - * - * TODO: consider moving analysis to QuoteProvider, if it's the only user. - */ - - // null TSeries is not expected - if (r == null) - { - throw new ArgumentNullException( - nameof(r), - "Unexpected null TSeries in Cache add analyzer."); - } - - // REPEAT AND OVERFLOW PROTECTION - - if (r.Timestamp == LastArrival.Timestamp) - { - // note: we have a better IsEqual() comparison method below, - // but it is too expensive as an initial quick evaluation. - - OverflowCount++; - - if (OverflowCount > 100) - { - string msg = "A repeated stream update exceeded the 100 attempt threshold. " - + "Check and remove circular chains or check your stream provider." - + "Provider terminated."; - - // note: if provider, catch overflow exception in parent observable, - // where it will EndTransmission(); and then throw to user. - - throw new OverflowException(msg); - } - - // aggressive property value comparison - // TODO: not handling add-back after delete, registers as dup - if (r.Equals(LastArrival)) - { - // to prevent propogation - // of identical cache entry - return Act.DoNothing; - } - - // same date with different values continues as an update, - // but still counts towards overflow threshold - else - { - LastArrival = r; - } - } - else - { - OverflowCount = 0; - LastArrival = r; - } - - // DETERMINE ACTion INSTRUCTION - - Act act; - List cache = Cache; - int length = cache.Count; - - // first - if (length == 0) - { - act = Act.AddNew; - return ModifyPerAction(act, r, value); - } - - ISeries last = cache[length - 1]; - - // newer - if (r.Timestamp > last.Timestamp) - { - act = Act.AddNew; - } - - // repeat or late arrival - else - { - // seek duplicate - int foundIndex = cache - .FindIndex(x => x.Timestamp == r.Timestamp); - - // replace duplicate - act = foundIndex == -1 ? Act.AddOld : Act.Update; - } - - // perform actual update, return final action - return ModifyPerAction(act, r, value); - } - - /// - /// Analyze and DELETE new arrivals from cache, after determining best instruction. - /// - /// - /// Fully formed cacheable time-series object. - /// - /// Action taken - /// - /// - internal Act PurgeWithAnalysis(TSeries r) - { - // null TSeries is not expected - if (r == null) - { - throw new ArgumentNullException( - nameof(r), - "Unexpected null TSeries in Cache purge analyzer."); - } - - // REPEAT AND OVERFLOW PROTECTION - - if (r.Timestamp == LastArrival.Timestamp) - { - // note: we have a better IsEqual() comparison method below, - // but it is too expensive as an initial quick evaluation. - - OverflowCount++; - - if (OverflowCount > 100) - { - string msg = "A repeated stream update exceeded the 100 attempt threshold. " - + "Check and remove circular chains or check your stream provider." - + "Provider terminated."; - - // note: if provider, catch overflow exception in parent observable, - // where it will EndTransmission(); and then throw to user. - - throw new OverflowException(msg); - } - - // note: aggressive property value comparison is often - // not possible for deletes due to an inability to re-calculate prior values - // TODO: not handling add-back after delete, registers as dup - } - else - { - OverflowCount = 0; - LastArrival = r; - } - - // determine if record exists - int foundIndex = Cache - .FindIndex(x => x.Timestamp == r.Timestamp); - - // not found - if (foundIndex == -1) - { - return Act.DoNothing; - } - - TSeries t = Cache[foundIndex]; - - // delete if full match - return t.Equals(r) - ? ModifyPerAction(Act.Delete, t, double.NaN) - : Act.DoNothing; - } - - // overload for chainable cachors - internal Act CacheChainorPerAction(Act act, TSeries r, double value) - => ModifyPerAction(act, r, value); - - // overload for non-chainable cachors - internal Act CacheResultPerAction(Act act, TSeries r) - => ModifyPerAction(act, r, double.NaN); - - // update cache, per instruction act - private Act ModifyPerAction(Act act, TSeries r, double value) - { - /* MODIFY THE CACHE - * - * This performs the actual modification to the cache. - * If the indicator is also a chain provider, also add to the - * standard observable tuple format. Timestamp and value parity - * must be maintained between these two cache and chain repos. - * - * Historically, we've had trouble simply reusing the cache - * as the source for re-building the observer caches due to - * the use of ChainProvider as the handler, a separate entity. - * There is potential to remove this redundant Chain cache, - * however, I could not find a satisfactory and performant - * alternative to this separation. -- DS 12/4/2023 - */ - - List cache = Cache; - List<(DateTime Timestamp, double Value)> chain = Chain; - - (DateTime Timestamp, double) t = (r.Timestamp, value); - - // execute action - switch (act) - { - case Act.AddNew: - - cache.Add(r); - - if (isChainor) - { - chain.Add(t); - } - - break; - - case Act.AddOld: - - // find - int ao = Cache.FindIndex(x => x.Timestamp > r.Timestamp); - - // insert - if (ao != -1) - { - cache.Insert(ao, r); - - if (isChainor) - { - chain.Insert(ao, t); - } - } - - // failure to find should never happen - else - { - throw new InvalidOperationException( - "Cache insert target not found."); - } - - break; - - case Act.Update: - - // find - int uo = Cache.FindIndex(r.Timestamp); - - // replace - if (uo != -1) - { - cache[uo] = r; - - if (isChainor) - { - chain[uo] = t; - } - } - - // failure to find should never happen - else - { - throw new InvalidOperationException( - "Cache update target not found."); - } - - break; - - case Act.Delete: - - // find - int d = cache.FindIndex(r.Timestamp); - - // delete - if (d != -1) - { - cache.RemoveAt(d); - - if (isChainor) - { - chain.RemoveAt(d); - } - } - - // failure to find should never happen - else - { - throw new InvalidOperationException( - "Cache delete target not found."); - } - - break; - - case Act.DoNothing: - - break; - - // should never get here - default: - - throw new InvalidOperationException( - "Undefined cache action."); - } - - return act; - } -} diff --git a/src/_common/Observables/ChainObserver.cs b/src/_common/Observables/ChainObserver.cs deleted file mode 100644 index 68949977c..000000000 --- a/src/_common/Observables/ChainObserver.cs +++ /dev/null @@ -1,79 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHAIN OBSERVER - -public abstract class ChainObserver - : ResultCache, IChainObserver - where TResult : IResult, new() -{ - internal IDisposable? unsubscriber; - - private protected ChainObserver( - ChainProvider provider, - bool isChainor) : base(isChainor) - { - ChainSupplier = provider; - } - - // PROPERTIES - - internal ChainProvider ChainSupplier { get; } - - // METHODS - - public abstract void OnNext((Act act, DateTime date, double price) value); - - public void OnError(Exception error) => throw error; - - public void OnCompleted() => Unsubscribe(); - - public void Unsubscribe() => unsubscriber?.Dispose(); - - // re/initialize my cache, from provider cache - public void RebuildCache() - { - // nothing to do - if (ChainSupplier.Chain.Count == 0) - { - return; - } - - // rebuild - RebuildCache(0); - } - - // replay from supplier cache, from date - public void RebuildCache(DateTime fromTimestamp) - => RebuildCache(fromTimestamp, 0); - - internal override void RebuildCache( - DateTime fromTimestamp, int offset = 0) - { - int fromIndex = ChainSupplier.Chain.FindIndex(fromTimestamp); - - if (fromIndex == -1) - { - throw new InvalidOperationException( - "Cache rebuild starting target not found."); - } - - RebuildCache(fromIndex, offset); - } - - // replay from supplier cache, from index - internal override void RebuildCache( - int fromIndex, int offset = 0) - { - int firstIndex = fromIndex + offset; - - // clear forward values - ClearCache(firstIndex); - - // replay from source - for (int i = firstIndex; i < ChainSupplier.Chain.Count; i++) - { - (DateTime date, double value) = ChainSupplier.Chain[i]; - OnNext((Act.AddNew, date, value)); - } - } -} diff --git a/src/_common/Observables/ChainProvider.cs b/src/_common/Observables/ChainProvider.cs deleted file mode 100644 index 41fa8746d..000000000 --- a/src/_common/Observables/ChainProvider.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace Skender.Stock.Indicators; - -// CHAIN PROVIDER - -public abstract class ChainProvider : IChainProvider -{ - // fields - private readonly List> observers; - - // constructor - private protected ChainProvider() - { - observers = []; - Chain = []; - } - - // PROPERTIES - - internal List<(DateTime Timestamp, double Value)> Chain; - - // METHODS - - // subscribe observer - public IDisposable Subscribe(IObserver<(Act, DateTime, double)> observer) - { - if (!observers.Contains(observer)) - { - observers.Add(observer); - } - - return new Unsubscriber(observers, observer); - } - - // unsubscribe all observers - public virtual void EndTransmission() - { - foreach (IObserver<(Act, DateTime, double)> obs in observers.ToArray()) - { - if (observers.Contains(obs)) - { - obs.OnCompleted(); - } - } - - observers.Clear(); - } - - // notify observers - internal void NotifyObservers((Act act, DateTime, double) chainMessage) - { - // do not propogate "do nothing" acts - if (chainMessage.act == Act.DoNothing) - { - return; - } - - // send to subscribers - List> obsList = [.. observers]; - - for (int i = 0; i < obsList.Count; i++) - { - IObserver<(Act, DateTime, double)> obs = obsList[i]; - obs.OnNext(chainMessage); - } - } - - // notify observers (helper, for IReusableResult) - internal void NotifyObservers(Act act, IReusableResult r) - => NotifyObservers((act, r.Timestamp, r.Value)); - - // unsubscriber - private class Unsubscriber( - List> observers, - IObserver<(Act, DateTime, double)> observer) : IDisposable - { - // can't mutate and iterate on same list, make copy - private readonly List> observers = observers; - private readonly IObserver<(Act, DateTime, double)> observer = observer; - - // remove single observer - public void Dispose() - { - if (observer != null && observers.Contains(observer)) - { - observers.Remove(observer); - } - } - } -} diff --git a/src/_common/Observables/IStreamCache.cs b/src/_common/Observables/IStreamCache.cs new file mode 100644 index 000000000..d3f16f1dd --- /dev/null +++ b/src/_common/Observables/IStreamCache.cs @@ -0,0 +1,51 @@ +namespace Skender.Stock.Indicators; + +// STREAM CACHE INTERFACES + +/// +/// Cache of stored values and related management +/// +public interface IStreamCache + where TSeries : struct, ISeries +{ + /// + /// Read-only cache of results + /// + IReadOnlyList Results { get; } + + /// + /// An error caused this provider to stop + /// and terminated all subscriptions. />. + /// + bool IsFaulted { get; } + + /// + /// Get a segment (window) of cached values. + /// + /// + IReadOnlyList GetRange(int index, int count); + + /// + /// Finds the index position of the provided timestamp + /// + /// + /// Index value or -1 when not found + int FindIndex(DateTime timeStamp); + + /// + /// Deletes all cached time-series records, + /// without restore. When applicable, + /// it will cascade delete commands to subscribers. + /// + void ClearCache(); + + /// + /// Deletes newer cached time-series records from point in time, + /// without restore. When applicable, it will cascade delete + /// commands to subscribers. + /// + /// + /// All periods (inclusive) after this DateTime will be removed. + /// + void ClearCache(DateTime fromTimestamp); +} diff --git a/src/_common/Observables/IStreamObserver.cs b/src/_common/Observables/IStreamObserver.cs new file mode 100644 index 000000000..02ab14cc0 --- /dev/null +++ b/src/_common/Observables/IStreamObserver.cs @@ -0,0 +1,64 @@ +namespace Skender.Stock.Indicators; + +// OBSERVER INTERFACES + +/// +/// Observer of a streamed quote source +/// +/// +public interface IQuoteObserver : IStreamObserver, IObserver<(Act act, TQuote quote)> + where TQuote : struct, IQuote; + +/// +/// Observer of a streamed chain source +/// +/// +public interface IChainObserver : IStreamObserver, IObserver<(Act act, TResult result)> + where TResult : struct, IReusableResult; + +/// +/// Observer of a unchainable result source +/// +/// +public interface IResultObserver : IStreamObserver, IObserver<(Act act, TResult result)> + where TResult : struct, IResult; + +/// +/// Observer of streamed chain or quote sources +/// +public interface IStreamObserver +{ + /// + /// Current state of subscription to provider. + /// + bool IsSubscribed { get; } + + /// + /// Unsubscribe from the data provider. + /// + void Unsubscribe(); + + /// + /// Reinitialize the cache to erase all stored values, + /// and resubscribe to provider. + /// + void Reinitialize(bool withRebuild = true); + + /// + /// Reset the entire results cache + /// and rebuild it from provider sources, + /// with cascading updates to subscribers. + /// + void RebuildCache(); + + /// + /// Reset the entire results cache from a known point in time + /// and rebuild it from provider sources, + /// with cascading updates to subscribers. + /// + /// + /// All periods (inclusive) after this DateTime will + /// be removed and recalculated. + /// + void RebuildCache(DateTime fromTimestamp); +} diff --git a/src/_common/Observables/IStreamProvider.cs b/src/_common/Observables/IStreamProvider.cs new file mode 100644 index 000000000..b1a905899 --- /dev/null +++ b/src/_common/Observables/IStreamProvider.cs @@ -0,0 +1,74 @@ +namespace Skender.Stock.Indicators; + +// PROVIDER INTERFACES (OBSERVABLES) + +/// +/// Quote provider interface (observable) +/// +/// +public interface IQuoteProvider : IStreamProvider + where TQuote : struct, IQuote; + +/// +/// Chainable result provider interface (observable) +/// +/// +public interface IChainProvider : IStreamProvider + where TResult : struct, IReusableResult; + +/// +/// Non-chainable result provider interface (observable) +/// +/// +public interface IResultProvider : IStreamProvider + where TResult : struct, IResult; + +/// +/// Streaming provider interface (observable) +/// +/// +public interface IStreamProvider : IObservable<(Act, TSeries)> + where TSeries : struct, ISeries +{ + /// + /// Currently has subscribers + /// + bool HasSubscribers { get; } + + /// + /// Current number of subscribers + /// + int SubscriberCount { get; } + + /// + /// Read-only cache of historical provider cache values + /// + IReadOnlyList Results { get; } + + /// + /// Returns a short formatted label + /// with parameter values, e.g. EMA(10) + /// + /// Indicator or quote label + string ToString(); + + /// + /// Unsubscribe all observers (subscribers) + /// + void EndTransmission(); + + /// + /// Resends historical cached values to a requesting observer, + /// starting at an index position. + /// + /// + /// + void Resend(int fromIndex, IObserver<(Act, TSeries)> toObserver); + + /// + /// Finds the index position of the provided timestamp + /// + /// + /// Index value or -1 when not found + int FindIndex(DateTime timeStamp); +} diff --git a/src/_common/Observables/Interfaces.cs b/src/_common/Observables/Interfaces.cs deleted file mode 100644 index 028bfef14..000000000 --- a/src/_common/Observables/Interfaces.cs +++ /dev/null @@ -1,117 +0,0 @@ -namespace Skender.Stock.Indicators; - -// PUBLIC INTERFACES ONLY ***** -// Reminder: do not add non-public elements here for internal templating. -// Conversly, non-public members should be defined as internal or private. - -/// -/// Observable provider incremental quotes from external feed sources. -/// -public interface IQuoteProvider - : IObservable<(Act act, IQuote quote)> -{ - /// - /// Terminates all subscriber connections gracefully. - /// - void EndTransmission(); -} - -/// -/// Observable provider of incremental chain value changes. -/// -public interface IChainProvider - : IObservable<(Act act, DateTime date, double price)> -{ - /// - /// Terminates all subscriber connections gracefully. - /// - void EndTransmission(); -} - -/// -/// Observer of incremental quotes from external feed sources. -/// -public interface IQuoteObserver - : IStreamCache, IObserver<(Act act, IQuote quote)> - where TResult : IResult -{ - /// - /// Unsubscribe from the data provider - /// - void Unsubscribe(); - - /// - /// Reset the entire results cache and rebuild it from quote provider sources. - /// Consider using RebuildCache(fromTimestapmp) from a known point in time instead. - /// - void RebuildCache(); - - /// - /// Reset and rebuild the results cache from a point in time. - /// Use RebuildCache() without arguments to reset the entire cache. - /// - /// - /// All periods (inclusive) after this DateTime will - /// be removed and recalculated. - /// - void RebuildCache(DateTime fromTimestamp); -} - -/// -/// Observer of indicator chain value changes. -/// -/// -public interface IChainObserver - : IStreamCache, IObserver<(Act act, DateTime date, double price)> - where TResult : IResult -{ - /// - /// Unsubscribe from the data provider - /// - void Unsubscribe(); - - /// - /// Reset the entire results cache and rebuild it from provider sources. - /// Consider using RebuildCache(fromTimestapmp) from a known point in time instead. - /// - void RebuildCache(); - - /// - /// Reset and rebuild the results cache from a point in time. - /// Use RebuildCache() without arguments to reset the entire cache. - /// - /// - /// All periods (inclusive) after this DateTime will - /// be removed and recalculated. - /// - void RebuildCache(DateTime fromTimestamp); -} - -/// -/// Read-only quote or indicator values. They are automatically updated. -/// -/// -public interface IStreamCache - where TSeries : ISeries -{ - /// - /// Returns a short formatted label with parameter values, e.g. EMA(10) - /// - /// Indicator or quote label - string ToString(); - - /// - /// Deletes all cached time-series records, without restore. - /// Subscribed indicators' caches will also be deleted. - /// - void ClearCache(); - - /// - /// Deletes cached time-series records from point in time, without restore. - /// Subscribed indicators' caches will also be deleted accordingly. - /// - /// - /// All periods (inclusive) after this DateTime will be removed. - /// - void ClearCache(DateTime fromTimestamp); -} diff --git a/src/_common/Observables/QuoteObserver.cs b/src/_common/Observables/QuoteObserver.cs deleted file mode 100644 index ffc60377a..000000000 --- a/src/_common/Observables/QuoteObserver.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace Skender.Stock.Indicators; - -// QUOTE OBSERVER - -public abstract class QuoteObserver - : ResultCache, IQuoteObserver - where TQuote : IQuote, new() - where TResult : IResult, new() -{ - internal IDisposable? unsubscriber; - - internal QuoteObserver( - QuoteProvider provider, - bool isChainor) : base(isChainor) - { - QuoteSupplier = provider; - } - - // PROPERTIES - - internal QuoteProvider QuoteSupplier { get; } - - // METHODS - - // standard observer properties - - public abstract void OnNext((Act act, IQuote quote) value); - - public void OnError(Exception error) => throw error; - - public void OnCompleted() => Unsubscribe(); - - public void Unsubscribe() => unsubscriber?.Dispose(); - - // re/initialize my cache, from provider cache - public void RebuildCache() - { - // nothing to do - if (QuoteSupplier.Cache.Count == 0) - { - return; - } - - // rebuild - RebuildCache(0); - } - - // replay from supplier cache, from date - public void RebuildCache(DateTime fromTimestamp) - => RebuildCache(fromTimestamp, 0); - - internal override void RebuildCache( - DateTime fromTimestamp, int offset = 0) - { - int fromIndex = QuoteSupplier.Cache.FindIndex(fromTimestamp); - - if (fromIndex == -1) - { - throw new InvalidOperationException( - "Cache rebuild starting target not found."); - } - - RebuildCache(fromIndex, offset); - } - - // replay from supplier cache, from index - internal override void RebuildCache( - int fromIndex, int offset = 0) - { - int firstIndex = fromIndex + offset; - - // clear forward values - ClearCache(firstIndex); - - // replay from source - for (int i = firstIndex; i < QuoteSupplier.Cache.Count; i++) - { - TQuote quote = QuoteSupplier.Cache[i]; - OnNext((Act.AddNew, quote)); - } - } - - // delete cache range values (implemented in inheritor) - internal abstract override void ClearCache(int fromIndex, int toIndex); -} diff --git a/src/_common/Observables/QuoteProvider.cs b/src/_common/Observables/QuoteProvider.cs index 7725adbda..96aae009d 100644 --- a/src/_common/Observables/QuoteProvider.cs +++ b/src/_common/Observables/QuoteProvider.cs @@ -1,56 +1,46 @@ namespace Skender.Stock.Indicators; -// QUOTE PROVIDER - -public class QuoteProvider - : QuoteCache, IQuoteProvider - where TQuote : IQuote, new() +/// +/// Quote provider, using generic IQuote interface type. +/// +/// +/// OHLCV price quote with value-based equality comparer +/// +public class QuoteProvider : AbstractQuoteProvider + where TQuote : struct, IQuote, IReusableResult { - // fields - private readonly List> observers; - - // constructor - public QuoteProvider() - { - observers = []; - Cache = []; - - Initialize(); - } - - // METHODS - - // string label - public override string ToString() - => $"Quote Provider ({Cache.Count} items)"; - /// - /// Add a single quote. We'll determine if it's new or an update. + /// Add a single quote. + /// We'll determine if it's new or an update. /// - /// Quote to add or update + /// + /// Quote to add or update + /// + /// Action taken (outcome) public Act Add(TQuote quote) { try { Act act = CacheWithAnalysis(quote); - NotifyObservers((act, quote)); + NotifyObservers(act, quote); return act; } - catch (OverflowException ox) + catch (OverflowException) { EndTransmission(); - - string msg = "A repeated Quote update exceeded the 100 attempt threshold. " - + "Check and remove circular chains or check your Quote provider." - + "Provider terminated."; - - throw new OverflowException(msg, ox); + throw; } } - // add many + /// + /// Add a batch of quotes. + /// We'll determine if they're new or updated. + /// + /// + /// Batch of quotes to add or update + /// public void Add(IEnumerable quotes) { List added = quotes @@ -67,113 +57,19 @@ public void Add(IEnumerable quotes) /// cache before propogating the event to subscribers. /// /// Quote to delete + /// Action taken (outcome) public Act Delete(TQuote quote) { try { Act act = PurgeWithAnalysis(quote); - NotifyObservers((act, quote)); + NotifyObservers(act, quote); return act; } - catch (OverflowException ox) + catch (OverflowException) { EndTransmission(); - - string msg = "A repeated Quote delete exceeded the 100 attempt threshold. " - + "Check and remove circular chains or check your Quote provider." - + "Provider terminated."; - - throw new OverflowException(msg, ox); - } - - } - - // re/initialize is graceful erase only for quote provider - public void Initialize() => ClearCache(); - - // subscribe observer - public IDisposable Subscribe(IObserver<(Act, IQuote)> observer) - { - if (!observers.Contains(observer)) - { - observers.Add(observer); - } - - return new Unsubscriber(observers, observer); - } - - // unsubscribe all observers - public override void EndTransmission() - { - foreach (IObserver<(Act, IQuote)> obs in observers.ToArray()) - { - if (observers.Contains(obs)) - { - obs.OnCompleted(); - } - } - - observers.Clear(); - } - - // delete cache, gracefully - internal override void ClearCache(int fromIndex, int toIndex) - { - // delete and deliver instruction, - // in reverse order to prevent recompositions - for (int i = Cache.Count - 1; i > 0; i--) - { - TQuote q = Cache[i]; - Act act = CacheResultPerAction(Act.Delete, q); - NotifyObservers((act, q)); - } - - // note: there is no auto-rebuild option since the - // quote provider is a top level external entry point. - // The using system will need to handle resupply with Add(). - } - - internal override void RebuildCache(DateTime fromDate, int offset) - => throw new InvalidOperationException(); - - internal override void RebuildCache(int fromIndex, int offset) - => throw new InvalidOperationException(); - - // notify observers - private void NotifyObservers((Act act, IQuote quote) quoteMessage) - { - // do not propogate "do nothing" acts - if (quoteMessage.act == Act.DoNothing) - { - return; - } - - // send to subscribers - List> obsList = [.. observers]; - - for (int i = 0; i < obsList.Count; i++) - { - IObserver<(Act, IQuote)> obs = obsList[i]; - obs.OnNext(quoteMessage); - } - } - - // unsubscriber - private class Unsubscriber( - List> observers, - IObserver<(Act, IQuote)> observer) : IDisposable - { - // can't mutate and iterate on same list, make copy - private readonly List> observers = observers; - private readonly IObserver<(Act, IQuote)> observer = observer; - - // remove single observer - public void Dispose() - { - if (observer != null && observers.Contains(observer)) - { - observers.Remove(observer); - } + throw; } } } diff --git a/src/_common/ObsoleteV3.cs b/src/_common/ObsoleteV3.cs index 9fae331ef..41995e9f8 100644 --- a/src/_common/ObsoleteV3.cs +++ b/src/_common/ObsoleteV3.cs @@ -5,194 +5,69 @@ namespace Skender.Stock.Indicators; // OBSOLETE IN v3 public static partial class Indicator { + // 3.0.0 + [ExcludeFromCodeCoverage] + [Obsolete("Use alternate 'GetX' variant.", false)] + 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); + // 3.0.0 [ExcludeFromCodeCoverage] [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average.", false)] public static IEnumerable GetAdl( - this IEnumerable quotes, - int smaPeriods) + this IEnumerable quotes, int smaPeriods) where TQuote : IQuote - { - // check parameter arguments - if (smaPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for ADL."); - } - - // add SMA - List results = quotes - .ToQuoteD() - .CalcAdl(); - - List sma = results - .GetSma(smaPeriods) - .ToList(); - - for (int i = 0; i < results.Count; i++) - { - results[i].AdlSma = sma[i].Sma; - } - - return results; - } + => quotes.ToQuoteD().CalcAdl(); + // 3.0.0 [ExcludeFromCodeCoverage] [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average.", false)] public static IEnumerable GetObv( - this IEnumerable quotes, - int smaPeriods) + this IEnumerable quotes, int smaPeriods) where TQuote : IQuote - { - // check parameter arguments - if (smaPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for OBV."); - } - - List results = quotes - .ToQuoteD() - .CalcObv(); - - List sma = results - .GetSma(smaPeriods) - .ToList(); - - for (int i = 0; i < results.Count; i++) - { - results[i].ObvSma = sma[i].Sma; - } - - return results; - } + => quotes.ToQuoteD().CalcObv(); // 3.0.0 [ExcludeFromCodeCoverage] [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average.", false)] public static IEnumerable GetPrs( - this IEnumerable quotesEval, - IEnumerable quotesBase, - int lookbackPeriods, - int smaPeriods) + this IEnumerable quotesEval, IEnumerable quotesBase, int lookbackPeriods, int smaPeriods) where TQuote : IQuote - { - if (smaPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for PRS."); - } - - List<(DateTime, double)> tpListBase = quotesBase - .ToTuple(CandlePart.Close); - List<(DateTime, double)> tpListEval = quotesEval - .ToTuple(CandlePart.Close); - - List results = [.. CalcPrs(tpListEval, tpListBase, lookbackPeriods)]; - List sma = results.GetSma(smaPeriods).ToList(); - - for (int i = 0; i < results.Count; i++) - { - results[i].PrsSma = sma[i].Sma; - } - - return results; - } + => quotesEval.ToTuple(CandlePart.Close).GetPrs(quotesBase.ToTuple(CandlePart.Close), lookbackPeriods); // 3.0.0 [ExcludeFromCodeCoverage] [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average.", false)] public static IEnumerable GetRoc( - this IEnumerable quotes, - int lookbackPeriods, - int smaPeriods) + this IEnumerable quotes, int lookbackPeriods, int smaPeriods) where TQuote : IQuote - { - if (smaPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for ROC."); - } - - List results = quotes - .ToTuple(CandlePart.Close) - .CalcRoc(lookbackPeriods); - - List sma = results - .GetSma(smaPeriods) - .ToList(); - - for (int i = 0; i < results.Count; i++) - { - results[i].RocSma = sma[i].Sma; - } - - return results; - } + => quotes.ToTuple(CandlePart.Close).GetRoc(lookbackPeriods); // 3.0.0 [ExcludeFromCodeCoverage] [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average.", false)] public static IEnumerable GetStdDev( - this IEnumerable quotes, - int lookbackPeriods, - int smaPeriods) + this IEnumerable quotes, int lookbackPeriods, int smaPeriods) where TQuote : IQuote - { - if (smaPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for Standard Deviation."); - } - - List results = quotes - .ToTuple(CandlePart.Close) - .CalcStdDev(lookbackPeriods); - - List sma = results - .GetSma(smaPeriods) - .ToList(); - - for (int i = 0; i < results.Count; i++) - { - results[i].StdDevSma = sma[i].Sma; - } - - return results; - } + => quotes.ToTuple(CandlePart.Close).CalcStdDev(lookbackPeriods); // 3.0.0 [ExcludeFromCodeCoverage] [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average.", false)] public static IEnumerable GetTrix( - this IEnumerable quotes, - int lookbackPeriods, - int smaPeriods) + this IEnumerable quotes, int lookbackPeriods, int smaPeriods) where TQuote : IQuote - { - // check parameter arguments - if (smaPeriods is <= 0) - { - throw new ArgumentOutOfRangeException(nameof(smaPeriods), smaPeriods, - "SMA periods must be greater than 0 for TRIX."); - } - - // add SMA - List results = quotes - .ToTuple(CandlePart.Close) - .CalcTrix(lookbackPeriods); - - List sma = results - .GetSma(smaPeriods) - .ToList(); - - for (int i = 0; i < results.Count; i++) - { - results[i].Signal = sma[i].Sma; - } - - return results; - } + => quotes.ToTuple(CandlePart.Close).CalcTrix(lookbackPeriods); // v3.0.0 [ExcludeFromCodeCoverage] diff --git a/src/_common/ObsoleteV3.md b/src/_common/ObsoleteV3.md index 3b324a298..ff228c939 100644 --- a/src/_common/ObsoleteV3.md +++ b/src/_common/ObsoleteV3.md @@ -14,9 +14,13 @@ In addition there are [breaking changes](#breaking-changes) that will require yo See your compiler `Warning` to identify these in your code. - `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; @@ -40,24 +44,20 @@ See your compiler `Warning` to identify these in your code. 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 backwards compatible v1 accommodations removed. +- all v1 backwards compatibility accommodations were removed. - no longer supporting .NET Standard 2.0 for older .NET Framework compatibility. -- 🚩 `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. +- 🚩 `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. - Result classes were changes to `record` class types. This will only impact rare cases where result classes are used for base inheritance. - Quote class (built-in) was changed to `record` class type. -- `Date` property was widely renamed to `Timestamp` to avoid conflict with C# reserved name. + - `Date` property was widely renamed to `Timestamp` to avoid conflict with C# reserved name. -- `IQuote` customization now has to be `IEquatable` type to support streaming operations. See [the Guide](/guide) for more information. +- `TQuote` custom quote types now have to be a `struct` type and implement the `IReusableResult` interface, to support streaming operations. The simplest way to fix is to change your `TQuote` from a regular `class` to a `record class`. See [the Guide](/guide) for more information. - `BasicData` class was renamed to `BasicResult` for consistency with other return types. -- `SyncSeries()` utility function and related `SyncType` enum were removed. These were primarily for internal - utility, but were part of the public API since they were useful for 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. +- `SyncSeries()` utility function and related `SyncType` enum were removed. These were primarily for internal utility, but were part of the public API since they were useful for 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. diff --git a/src/_common/Quotes/Quote.Converters.cs b/src/_common/Quotes/Quote.Converters.cs index 4b3ebe04e..acd6813ea 100644 --- a/src/_common/Quotes/Quote.Converters.cs +++ b/src/_common/Quotes/Quote.Converters.cs @@ -20,6 +20,15 @@ public static partial class QuoteUtility .ToTuple(candlePart) .ToCollection(); + internal static Quote ToQuote(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); + internal static List<(DateTime, double)> ToTuple( this IEnumerable quotes, CandlePart candlePart) @@ -44,27 +53,26 @@ public static partial class QuoteUtility // convert to quotes in double precision internal static QuoteD ToQuoteD( this TQuote quote) - where TQuote : IQuote => new() { - Timestamp = quote.Timestamp, - Open = (double)quote.Open, - High = (double)quote.High, - Low = (double)quote.Low, - Close = (double)quote.Close, - Volume = (double)quote.Volume - }; + where TQuote : IQuote => new( + Timestamp: quote.Timestamp, + Open: (double)quote.Open, + High: (double)quote.High, + Low: (double)quote.Low, + Close: (double)quote.Close, + Volume: (double)quote.Volume); internal static List ToQuoteD( this IEnumerable quotes) - where TQuote : IQuote => [.. quotes - .Select(x => new QuoteD { - Timestamp = x.Timestamp, - Open = (double)x.Open, - High = (double)x.High, - Low = (double)x.Low, - Close = (double)x.Close, - Volume = (double)x.Volume - }) - .OrderBy(x => x.Timestamp)]; + where TQuote : IQuote => quotes + .Select(x => new QuoteD( + Timestamp: x.Timestamp, + Open: (double)x.Open, + High: (double)x.High, + Low: (double)x.Low, + Close: (double)x.Close, + Volume: (double)x.Volume)) + .OrderBy(x => x.Timestamp) + .ToList(); // convert quoteD list to tuples internal static List<(DateTime, double)> ToTuple( @@ -95,20 +103,20 @@ internal static (DateTime date, double value) ToTuple( }; // convert TQuote element to basic double class - internal static BasicResult ToBasicData( + internal static QuotePart ToQuotePart( this TQuote q, CandlePart candlePart) where TQuote : IQuote => candlePart switch { - CandlePart.Open => new BasicResult { Timestamp = q.Timestamp, Value = (double)q.Open }, - CandlePart.High => new BasicResult { Timestamp = q.Timestamp, Value = (double)q.High }, - CandlePart.Low => new BasicResult { Timestamp = q.Timestamp, Value = (double)q.Low }, - CandlePart.Close => new BasicResult { Timestamp = q.Timestamp, Value = (double)q.Close }, - CandlePart.Volume => new BasicResult { Timestamp = q.Timestamp, Value = (double)q.Volume }, - CandlePart.HL2 => new BasicResult { Timestamp = q.Timestamp, Value = (double)(q.High + q.Low) / 2 }, - CandlePart.HLC3 => new BasicResult { Timestamp = q.Timestamp, Value = (double)(q.High + q.Low + q.Close) / 3 }, - CandlePart.OC2 => new BasicResult { Timestamp = q.Timestamp, Value = (double)(q.Open + q.Close) / 2 }, - CandlePart.OHL3 => new BasicResult { Timestamp = q.Timestamp, Value = (double)(q.Open + q.High + q.Low) / 3 }, - CandlePart.OHLC4 => new BasicResult { Timestamp = q.Timestamp, Value = (double)(q.Open + q.High + q.Low + q.Close) / 4 }, + CandlePart.Open => new QuotePart { Timestamp = q.Timestamp, Value = (double)q.Open }, + CandlePart.High => new QuotePart { Timestamp = q.Timestamp, Value = (double)q.High }, + CandlePart.Low => new QuotePart { Timestamp = q.Timestamp, Value = (double)q.Low }, + CandlePart.Close => new QuotePart { Timestamp = q.Timestamp, Value = (double)q.Close }, + CandlePart.Volume => new QuotePart { Timestamp = q.Timestamp, Value = (double)q.Volume }, + CandlePart.HL2 => new QuotePart { Timestamp = q.Timestamp, Value = (double)(q.High + q.Low) / 2 }, + CandlePart.HLC3 => new QuotePart { Timestamp = q.Timestamp, Value = (double)(q.High + q.Low + q.Close) / 3 }, + CandlePart.OC2 => new QuotePart { Timestamp = q.Timestamp, Value = (double)(q.Open + q.Close) / 2 }, + CandlePart.OHL3 => new QuotePart { Timestamp = q.Timestamp, Value = (double)(q.Open + q.High + q.Low) / 3 }, + CandlePart.OHLC4 => new QuotePart { Timestamp = q.Timestamp, Value = (double)(q.Open + q.High + q.Low + q.Close) / 4 }, _ => throw new ArgumentOutOfRangeException(nameof(candlePart), candlePart, "Invalid candlePart provided."), }; diff --git a/src/_common/Quotes/Quote.Models.cs b/src/_common/Quotes/Quote.Models.cs index 504183e10..dfcd61452 100644 --- a/src/_common/Quotes/Quote.Models.cs +++ b/src/_common/Quotes/Quote.Models.cs @@ -2,113 +2,36 @@ namespace Skender.Stock.Indicators; // QUOTE MODELS -public interface IQuote : ISeries, IEquatable +public interface IQuote : IReusableResult { decimal Open { get; } decimal High { get; } decimal Low { get; } decimal Close { get; } decimal Volume { get; } - - // CS0567 C# Interfaces cannot contain conversion, - // equality, or inequality operators (i.e. == or !=) - // and cannot be inforced here } /// /// Built-in Quote type. +/// Custom IQuote types are also supported. /// -public record class Quote : IQuote +public record struct Quote( + DateTime Timestamp, + decimal Open, + decimal High, + decimal Low, + decimal Close, + decimal Volume) + : IQuote { - public DateTime Timestamp { get; set; } - public decimal Open { get; set; } - public decimal High { get; set; } - public decimal Low { get; set; } - public decimal Close { get; set; } - public decimal Volume { get; set; } - - // this is only an appropriate - // implementation for record types - public bool Equals(IQuote? other) - => base.Equals(other); + readonly double IReusableResult.Value + => (double)Close; } -public abstract class EquatableQuote : IQuote -{ - public virtual DateTime Timestamp { get; set; } - public virtual decimal Open { get; set; } - public virtual decimal High { get; set; } - public virtual decimal Low { get; set; } - public virtual decimal Close { get; set; } - public virtual decimal Volume { get; set; } - - public override bool Equals(object? obj) - => Equals(obj as IQuote); - - public bool Equals(IQuote? other) - { - if (other is null) - { - return false; - } - - // same object reference - if (ReferenceEquals(this, other)) - { - return true; - } - - // mismatch object types - if (GetType() != other.GetType()) - { - return false; - } - - // deep compare - return Timestamp == other.Timestamp - && Open == other.Open - && High == other.High - && Low == other.Low - && Close == other.Close - && Volume == other.Volume; - } - - public static bool operator - ==(EquatableQuote lhs, EquatableQuote rhs) - { - if (lhs is null) - { - if (rhs is null) - { - // null == null = true - return true; - } - - // left side is null - return false; - } - - // null on right side also handled - return lhs.Equals(rhs); - } - - public static bool operator - !=(EquatableQuote lhs, EquatableQuote rhs) - { - return !(lhs == rhs); - } - - public override int GetHashCode() - => HashCode.Combine( - Timestamp, Open, High, Low, Close, Volume); -} - -internal class QuoteD -{ - internal DateTime Timestamp { 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; } -} +internal record struct QuoteD( + DateTime Timestamp, + double Open, + double High, + double Low, + double Close, + double Volume); diff --git a/src/_common/Results/Result.Models.cs b/src/_common/Results/IResult.cs similarity index 100% rename from src/_common/Results/Result.Models.cs rename to src/_common/Results/IResult.cs diff --git a/src/_common/Use (quote converter)/Use.Api.cs b/src/_common/Use (quote converter)/Use.Api.cs index 337bd9007..c61089cc2 100644 --- a/src/_common/Use (quote converter)/Use.Api.cs +++ b/src/_common/Use (quote converter)/Use.Api.cs @@ -5,18 +5,16 @@ namespace Skender.Stock.Indicators; public static partial class Indicator { // SERIES, from Quotes - /// - /// - public static IEnumerable<(DateTime Timestamp, double Value)> Use( + public static IEnumerable Use( this IEnumerable quotes, CandlePart candlePart) where TQuote : IQuote - => quotes.Select(x => x.ToTuple(candlePart)); + => quotes.Select(q => q.ToQuotePart(candlePart)); // OBSERVER, from Quote Provider public static Use Use( - this QuoteProvider quoteProvider, + this IQuoteProvider quoteProvider, CandlePart candlePart) - where TQuote : IQuote, new() + where TQuote : struct, IQuote => new(quoteProvider, candlePart); } diff --git a/src/_common/Use (quote converter)/Use.Models.cs b/src/_common/Use (quote converter)/Use.Models.cs index c8aae46a3..0f5a67d21 100644 --- a/src/_common/Use (quote converter)/Use.Models.cs +++ b/src/_common/Use (quote converter)/Use.Models.cs @@ -1,8 +1,11 @@ namespace Skender.Stock.Indicators; -// TODO: this is redundant to "BasicResult", but it has a funny name -public sealed record class UseResult : IReusableResult +public record struct QuotePart( + DateTime Timestamp, + double Value +) : IReusableResult; + +public interface IUse : IStreamObserver { - public DateTime Timestamp { get; set; } - public double Value { get; set; } + CandlePart CandlePartSelection { get; } } diff --git a/src/_common/Use (quote converter)/Use.Stream.cs b/src/_common/Use (quote converter)/Use.Stream.cs index e9b2e12a6..19736c93d 100644 --- a/src/_common/Use (quote converter)/Use.Stream.cs +++ b/src/_common/Use (quote converter)/Use.Stream.cs @@ -2,28 +2,28 @@ namespace Skender.Stock.Indicators; // USE (STREAMING) -public class Use : QuoteObserver - where TQuote : IQuote, new() +public class Use + : AbstractQuoteInChainOut, IUse + where TQuote : struct, IQuote { // constructor public Use( - QuoteProvider provider, - CandlePart candlePart) : - base(provider, isChainor: true) + IQuoteProvider provider, + CandlePart candlePart) : base(provider) { CandlePartSelection = candlePart; RebuildCache(); // subscribe to quote provider - unsubscriber = provider is null + Subscription = provider is null ? throw new ArgumentNullException(nameof(provider)) : provider.Subscribe(this); } // PROPERTIES - private CandlePart CandlePartSelection { get; set; } + public CandlePart CandlePartSelection { get; private set; } // METHODS @@ -33,31 +33,18 @@ public override string ToString() => $"USE({Enum.GetName(typeof(CandlePart), CandlePartSelection)})"; // handle quote arrival - public override void OnNext((Act act, IQuote quote) value) + internal override void OnNextArrival(Act act, IQuote quote) { // candidate result - (DateTime d, double v) = value.quote.ToTuple(CandlePartSelection); - UseResult r = new() { Timestamp = d, Value = v }; + (DateTime d, double v) + = quote.ToTuple(CandlePartSelection); + + QuotePart result = new() { Timestamp = d, Value = v }; // save to cache - CacheChainorPerAction(value.act, r, v); + ModifyCache(act, result); // send to observers - NotifyObservers(value.act, r); - } - - // delete cache between index values - // usually called from inherited ClearCache(fromDate) - internal override void ClearCache(int fromIndex, int toIndex) - { - // delete and deliver instruction, - // in reverse order to prevent recompositions - - for (int i = toIndex; i >= fromIndex; i--) - { - UseResult r = Cache[i]; - Act act = CacheChainorPerAction(Act.Delete, r, double.NaN); - NotifyObservers(act, r); - } + NotifyObservers(act, result); } } diff --git a/src/_common/Use (quote converter)/info.xml b/src/_common/Use (quote converter)/info.xml deleted file mode 100644 index 7f532580d..000000000 --- a/src/_common/Use (quote converter)/info.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - 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. - - - \ No newline at end of file diff --git a/src/a-d/Adl/Adl.Api.cs b/src/a-d/Adl/Adl.Api.cs index e8b661612..8eaea2d37 100644 --- a/src/a-d/Adl/Adl.Api.cs +++ b/src/a-d/Adl/Adl.Api.cs @@ -9,7 +9,14 @@ public static partial class Indicator /// public static IEnumerable GetAdl( this IEnumerable quotes) - where TQuote : IQuote => quotes + where TQuote : IQuote + => quotes .ToQuoteD() .CalcAdl(); + + // OBSERVER, from Quote Provider + public static Adl ToAdl( + this IQuoteProvider quoteProvider) + where TQuote : struct, IQuote + => new(quoteProvider); } diff --git a/src/a-d/Adl/Adl.Common.cs b/src/a-d/Adl/Adl.Common.cs index ffe235ecc..a3d7c0521 100644 --- a/src/a-d/Adl/Adl.Common.cs +++ b/src/a-d/Adl/Adl.Common.cs @@ -5,12 +5,25 @@ namespace Skender.Stock.Indicators; /// See the /// Stock Indicators for .NET online guide for more information. -public static class Adl +public static partial class Adl { // increment calculation - /// - /// + /// Get the next incremental Accumulation/Distribution Line(ADL) value. + /// + /// See + /// documentation + /// for more information. + /// + /// + /// Timestamp + /// Last ADL value, from prior period. + /// High price, current period. + /// Low price, current period. + /// Close price, current period. + /// Volume, current period. + /// New ADL result value. public static AdlResult Increment( + DateTime timestamp, double prevAdl, double high, double low, @@ -21,11 +34,10 @@ public static AdlResult Increment( double mfv = mfm * volume; double adl = mfv + prevAdl; - return new AdlResult { - Timestamp = DateTime.MinValue, - MoneyFlowMultiplier = mfm, - MoneyFlowVolume = mfv, - Adl = adl - }; + return new AdlResult( + Timestamp: timestamp, + Adl: adl, + MoneyFlowMultiplier: mfm, + MoneyFlowVolume: mfv); } } diff --git a/src/a-d/Adl/Adl.Models.cs b/src/a-d/Adl/Adl.Models.cs index f68cfb02e..4fcffeb0b 100644 --- a/src/a-d/Adl/Adl.Models.cs +++ b/src/a-d/Adl/Adl.Models.cs @@ -1,14 +1,17 @@ namespace Skender.Stock.Indicators; -public sealed record class AdlResult : IReusableResult +public record struct AdlResult +( + DateTime Timestamp, + double Adl, + double? MoneyFlowMultiplier = default, + double? MoneyFlowVolume = default) + : IReusableResult { - public DateTime Timestamp { get; set; } - public double? MoneyFlowMultiplier { get; set; } - public double? MoneyFlowVolume { get; set; } - public double Adl { get; set; } - - [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average.", false)] - public double? AdlSma { get; set; } + readonly double IReusableResult.Value => Adl; +} - double IReusableResult.Value => Adl; +public interface IAdl : IStreamObserver +{ + // no public parameters } diff --git a/src/a-d/Adl/Adl.Series.cs b/src/a-d/Adl/Adl.Series.cs index 2864f44d1..7a14579d2 100644 --- a/src/a-d/Adl/Adl.Series.cs +++ b/src/a-d/Adl/Adl.Series.cs @@ -16,8 +16,9 @@ internal static List CalcAdl( { QuoteD q = qdList[i]; - AdlResult r = Adl.Increment(prevAdl, q.High, q.Low, q.Close, q.Volume); - r.Timestamp = q.Timestamp; + AdlResult r = Adl.Increment( + q.Timestamp, prevAdl, q.High, q.Low, q.Close, q.Volume); + results.Add(r); prevAdl = r.Adl; diff --git a/src/a-d/Adl/Adl.Stream.cs b/src/a-d/Adl/Adl.Stream.cs new file mode 100644 index 000000000..1589e63b0 --- /dev/null +++ b/src/a-d/Adl/Adl.Stream.cs @@ -0,0 +1,25 @@ +namespace Skender.Stock.Indicators; + +// ACCUMULATION/DISTRIBUTION LINE (STREAM) + +public partial class Adl : AbstractQuoteInChainOut, IAdl + where TQuote: struct, IQuote +{ + public Adl(IQuoteProvider provider) + : base(provider) { + + RebuildCache(); + + // subscribe to quote provider + Subscription = provider is null + ? throw new ArgumentNullException(nameof(provider)) + : provider.Subscribe(this); + } + + // string label + public override string ToString() + => Cache.Count == 0 ? "ADL" : $"ADL({Cache[0].Timestamp:d})"; + + internal override void OnNextArrival(Act act, IQuote quote) + => throw new NotImplementedException(); +} diff --git a/src/a-d/Adl/info.xml b/src/a-d/Adl/info.xml index c16dd6842..cb124315a 100644 --- a/src/a-d/Adl/info.xml +++ b/src/a-d/Adl/info.xml @@ -14,31 +14,4 @@ Historical price quotes. Time series of ADL values. - - Get the next incremental Accumulation/Distribution Line (ADL) value. - - See - documentation - for more information. - - Last ADL value, from prior period. - High price, current period. - Low price, current period. - Close price, current period. - Volume, current period. - New ADL value. - - - - Get the next incremental Accumulation/Distribution Line (ADL) result. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quote. - New AdlResult value. - - \ No newline at end of file + diff --git a/src/a-d/Adx/Adx.Models.cs b/src/a-d/Adx/Adx.Models.cs index 34dd43532..0d85f26ca 100644 --- a/src/a-d/Adx/Adx.Models.cs +++ b/src/a-d/Adx/Adx.Models.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public sealed record class AdxResult : IReusableResult +public record struct AdxResult( + DateTime Timestamp, + double? Pdi = default, + double? Mdi = default, + double? Adx = default, + double? Adxr = default) +: IReusableResult { - public DateTime Timestamp { get; set; } - public double? Pdi { get; set; } - public double? Mdi { get; set; } - public double? Adx { get; set; } - public double? Adxr { get; set; } - - double IReusableResult.Value => Adx.Null2NaN(); + readonly double IReusableResult.Value + => Adx.Null2NaN(); } diff --git a/src/a-d/Adx/Adx.Series.cs b/src/a-d/Adx/Adx.Series.cs index 2d4954264..02fe473f8 100644 --- a/src/a-d/Adx/Adx.Series.cs +++ b/src/a-d/Adx/Adx.Series.cs @@ -33,15 +33,14 @@ internal static List CalcAdx( { QuoteD q = qdList[i]; - AdxResult r = new() { Timestamp = q.Timestamp }; - results.Add(r); - // skip first period if (i == 0) { prevHigh = q.High; prevLow = q.Low; prevClose = q.Close; + + results.Add(new AdxResult(Timestamp: q.Timestamp)); continue; } @@ -70,6 +69,7 @@ internal static List CalcAdx( // skip DM initialization period if (i < lookbackPeriods) { + results.Add(new AdxResult(Timestamp: q.Timestamp)); continue; } @@ -98,6 +98,7 @@ internal static List CalcAdx( if (trs is 0) { + results.Add(new AdxResult(Timestamp: q.Timestamp)); continue; } @@ -105,9 +106,6 @@ 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) ? 0 @@ -115,16 +113,16 @@ internal static List CalcAdx( ? 100 * Math.Abs(pdi - mdi) / (pdi + mdi) : double.NaN; - double adx; + double adx = double.NaN; + double adxr = double.NaN; if (i > (2 * lookbackPeriods) - 1) { adx = ((prevAdx * (lookbackPeriods - 1)) + dx) / lookbackPeriods; - r.Adx = adx.NaN2Null(); - 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; } @@ -133,7 +131,6 @@ internal static List CalcAdx( { sumDx += dx; adx = sumDx / lookbackPeriods; - r.Adx = adx.NaN2Null(); prevAdx = adx; } @@ -143,6 +140,15 @@ internal static List CalcAdx( { sumDx += dx; } + + AdxResult r = new( + Timestamp: q.Timestamp, + Pdi: pdi, + Mdi: mdi, + Adx: adx.NaN2Null(), + Adxr: adxr.NaN2Null()); + + results.Add(r); } return results; diff --git a/src/a-d/Alligator/Alligator.Api.cs b/src/a-d/Alligator/Alligator.Api.cs index 65f4f6ad1..a53fa7b17 100644 --- a/src/a-d/Alligator/Alligator.Api.cs +++ b/src/a-d/Alligator/Alligator.Api.cs @@ -3,36 +3,37 @@ 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, + /// + /// 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. + 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 + int lipsOffset = 3) + where T : IReusableResult + => results .ToTupleResult() .CalcAlligator( jawPeriods, @@ -42,58 +43,16 @@ public static IEnumerable GetAlligator( lipsPeriods, lipsOffset); - // SERIES, from TUPLE - // TODO: is this variant still needed, or just an extra option (all indicators) - 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); - - // OBSERVER, from Quote Provider - public static Alligator AttachAlligator( - this QuoteProvider quoteProvider, - int jawPeriods, - int jawOffset, - int teethPeriods, - int teethOffset, - int lipsPeriods, - int lipsOffset) - where TQuote : IQuote, new() - { - Use chainProvider = quoteProvider - .Use(CandlePart.HL2); - - return new( - chainProvider, - jawPeriods, - jawOffset, - teethPeriods, - teethOffset, - lipsPeriods, - lipsOffset); - } - // OBSERVER, from Chain Provider - public static Alligator AttachAlligator( - this ChainProvider chainProvider, + public static Alligator ToAlligator( + this IChainProvider chainProvider, int jawPeriods, int jawOffset, int teethPeriods, int teethOffset, int lipsPeriods, int lipsOffset) + where TIn : struct, IReusableResult => new( chainProvider, jawPeriods, diff --git a/src/a-d/Alligator/Alligator.Common.cs b/src/a-d/Alligator/Alligator.Common.cs index bc826b9ea..fcb9201cd 100644 --- a/src/a-d/Alligator/Alligator.Common.cs +++ b/src/a-d/Alligator/Alligator.Common.cs @@ -2,12 +2,8 @@ namespace Skender.Stock.Indicators; // WILLIAMS ALLIGATOR (COMMON) -public partial class Alligator +public static class Alligator { - // string label - public override string ToString() - => $"ALLIGATOR({JawPeriods},{JawOffset},{TeethPeriods},{TeethOffset},{LipsPeriods},{LipsOffset})"; - // parameter validation internal static void Validate( int jawPeriods, diff --git a/src/a-d/Alligator/Alligator.Models.cs b/src/a-d/Alligator/Alligator.Models.cs index 968bff177..5253ad8dd 100644 --- a/src/a-d/Alligator/Alligator.Models.cs +++ b/src/a-d/Alligator/Alligator.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class AlligatorResult : IResult +public record struct AlligatorResult : IResult { public DateTime Timestamp { get; set; } public double? Jaw { get; set; } @@ -8,8 +8,7 @@ public sealed record class AlligatorResult : IResult public double? Lips { get; set; } } -public interface IAlligator : - IChainObserver +public interface IAlligator : IStreamObserver { int JawPeriods { get; } int JawOffset { get; } diff --git a/src/a-d/Alligator/Alligator.Stream.cs b/src/a-d/Alligator/Alligator.Stream.cs index dc79e5e60..f84afd487 100644 --- a/src/a-d/Alligator/Alligator.Stream.cs +++ b/src/a-d/Alligator/Alligator.Stream.cs @@ -1,20 +1,20 @@ namespace Skender.Stock.Indicators; -public partial class Alligator : ChainObserver, IAlligator +public class Alligator : AbstractChainInResultOut, IAlligator + where TIn : struct, IReusableResult { // constructor public Alligator( - ChainProvider provider, + IChainProvider provider, int jawPeriods, int jawOffset, int teethPeriods, int teethOffset, int lipsPeriods, int lipsOffset) - : base(provider, - isChainor: false) + : base(provider) { - Validate( + Alligator.Validate( jawPeriods, jawOffset, teethPeriods, @@ -32,7 +32,7 @@ public Alligator( RebuildCache(); // subscribe to provider - unsubscriber = provider != null + Subscription = provider != null ? provider.Subscribe(this) : throw new ArgumentNullException(nameof(provider)); } @@ -48,20 +48,22 @@ public Alligator( // METHODS + // string label + public override string ToString() + => $"ALLIGATOR({JawPeriods},{JawOffset},{TeethPeriods},{TeethOffset},{LipsPeriods},{LipsOffset})"; + // handle chain arrival - public override void OnNext((Act act, DateTime date, double price) value) + internal override void OnNextArrival(Act act, IReusableResult inbound) { int i; double jaw = double.NaN; double lips = double.NaN; double teeth = double.NaN; - List<(DateTime _, double value)> supplier = ChainSupplier.Chain; - // handle deletes - if (value.act == Act.Delete) + if (act == Act.Delete) { - i = Cache.FindIndex(value.date); + i = Cache.FindIndex(inbound.Timestamp); AlligatorResult alligator = Cache[i]; jaw = alligator.Jaw.Null2NaN(); lips = alligator.Lips.Null2NaN(); @@ -72,7 +74,7 @@ public override void OnNext((Act act, DateTime date, double price) value) else { - i = ChainSupplier.Chain.FindIndex(value.date); + i = Provider.FindIndex(inbound.Timestamp); // source unexpectedly not found if (i == -1) @@ -92,7 +94,7 @@ public override void OnNext((Act act, DateTime date, double price) value) double sum = 0; for (int p = i - JawPeriods - JawOffset + 1; p <= i - JawOffset; p++) { - sum += ChainSupplier.Chain[p].Value; + sum += ProviderCache[p].Value; } jaw = sum / JawPeriods; @@ -101,7 +103,7 @@ public override void OnNext((Act act, DateTime date, double price) value) // remaining values: SMMA else { - double newVal = ChainSupplier.Chain[i - JawOffset].Value; + double newVal = ProviderCache[i - JawOffset].Value; jaw = ((prevJaw * (JawPeriods - 1)) + newVal) / JawPeriods; } } @@ -118,7 +120,7 @@ public override void OnNext((Act act, DateTime date, double price) value) for (int p = i - TeethPeriods - TeethOffset + 1; p <= i - TeethOffset; p++) { - sum += ChainSupplier.Chain[p].Value; + sum += ProviderCache[p].Value; } teeth = sum / TeethPeriods; @@ -127,7 +129,7 @@ public override void OnNext((Act act, DateTime date, double price) value) // remaining values: SMMA else { - double newVal = ChainSupplier.Chain[i - TeethOffset].Value; + double newVal = ProviderCache[i - TeethOffset].Value; teeth = ((prevTeeth * (TeethPeriods - 1)) + newVal) / TeethPeriods; } } @@ -143,7 +145,7 @@ public override void OnNext((Act act, DateTime date, double price) value) double sum = 0; for (int p = i - LipsPeriods - LipsOffset + 1; p <= i - LipsOffset; p++) { - sum += ChainSupplier.Chain[p].Value; + sum += ProviderCache[p].Value; } lips = sum / LipsPeriods; @@ -152,7 +154,7 @@ public override void OnNext((Act act, DateTime date, double price) value) // remaining values: SMMA else { - double newVal = ChainSupplier.Chain[i - LipsOffset].Value; + double newVal = ProviderCache[i - LipsOffset].Value; lips = ((prevLips * (LipsPeriods - 1)) + newVal) / LipsPeriods; } } @@ -160,38 +162,23 @@ public override void OnNext((Act act, DateTime date, double price) value) // candidate result AlligatorResult r = new() { - Timestamp = value.date, + Timestamp = inbound.Timestamp, Jaw = jaw.NaN2Null(), Lips = lips.NaN2Null(), Teeth = teeth.NaN2Null() }; // save to cache - Act act = CacheResultPerAction(value.act, r); + act = ModifyCache(act, r); // note: this indicator is not observable (no notification) - // update forward values - if (act != Act.AddNew && i < supplier.Count - 1) + // cascade update forward values (recursively) + if (act != Act.AddNew && i < ProviderCache.Count - 1) { - // cascade updates gracefully int next = act == Act.Delete ? i : i + 1; - (DateTime d, double v) = supplier[next]; - OnNext((Act.Update, d, v)); - } - } - - // delete cache between index values - // usually called from inherited ClearCache(fromDate) - internal override void ClearCache(int fromIndex, int toIndex) - { - // delete and deliver instruction, - // in reverse order to prevent recompositions - - for (int i = toIndex; i >= fromIndex; i--) - { - AlligatorResult r = Cache[i]; - Act act = CacheResultPerAction(Act.Delete, r); + TIn value = ProviderCache[next]; + OnNextArrival(Act.Update, value); } } } 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 index 87ed290bb..32a8a7cc1 100644 --- a/src/a-d/Alma/Alma.Api.cs +++ b/src/a-d/Alma/Alma.Api.cs @@ -3,24 +3,14 @@ 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, + public static IEnumerable GetAlma( + this IEnumerable results, int lookbackPeriods = 9, double offset = 0.85, - double sigma = 6) => results + double sigma = 6) + where T: IReusableResult + => results .ToTupleResult() .CalcAlma(lookbackPeriods, offset, sigma); diff --git a/src/a-d/Alma/Alma.Models.cs b/src/a-d/Alma/Alma.Models.cs index d6fd955df..d5eb090b4 100644 --- a/src/a-d/Alma/Alma.Models.cs +++ b/src/a-d/Alma/Alma.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class AlmaResult : IReusableResult +public record struct AlmaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Alma { get; set; } - double IReusableResult.Value => Alma.Null2NaN(); + readonly double IReusableResult.Value + => Alma.Null2NaN(); } diff --git a/src/a-d/Aroon/Aroon.Models.cs b/src/a-d/Aroon/Aroon.Models.cs index 3c97f5d8f..2d1b5d641 100644 --- a/src/a-d/Aroon/Aroon.Models.cs +++ b/src/a-d/Aroon/Aroon.Models.cs @@ -1,11 +1,12 @@ namespace Skender.Stock.Indicators; -public sealed record class AroonResult : IReusableResult +public record struct AroonResult : IReusableResult { public DateTime Timestamp { get; set; } public double? AroonUp { get; set; } public double? AroonDown { get; set; } public double? Oscillator { get; set; } - double IReusableResult.Value => Oscillator.Null2NaN(); + readonly double IReusableResult.Value + => Oscillator.Null2NaN(); } diff --git a/src/a-d/Atr/Atr.Models.cs b/src/a-d/Atr/Atr.Models.cs index 111d2b4c4..9c3028916 100644 --- a/src/a-d/Atr/Atr.Models.cs +++ b/src/a-d/Atr/Atr.Models.cs @@ -1,11 +1,12 @@ namespace Skender.Stock.Indicators; -public sealed record class AtrResult : IReusableResult +public record struct AtrResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Tr { get; set; } public double? Atr { get; set; } public double? Atrp { get; set; } - double IReusableResult.Value => Atrp.Null2NaN(); + readonly double IReusableResult.Value + => Atrp.Null2NaN(); } diff --git a/src/a-d/AtrStop/AtrStop.Models.cs b/src/a-d/AtrStop/AtrStop.Models.cs index 04552c5f5..2b864ebb3 100644 --- a/src/a-d/AtrStop/AtrStop.Models.cs +++ b/src/a-d/AtrStop/AtrStop.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class AtrStopResult : IResult +public record struct AtrStopResult : IResult { public DateTime Timestamp { get; set; } public decimal? AtrStop { get; set; } diff --git a/src/a-d/Awesome/Awesome.Api.cs b/src/a-d/Awesome/Awesome.Api.cs index a87c945a4..2843c6916 100644 --- a/src/a-d/Awesome/Awesome.Api.cs +++ b/src/a-d/Awesome/Awesome.Api.cs @@ -3,22 +3,13 @@ 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, + public static IEnumerable GetAwesome( + this IEnumerable results, int fastPeriods = 5, - int slowPeriods = 34) => results + int slowPeriods = 34) + where T : IReusableResult + => results .ToTupleResult() .CalcAwesome(fastPeriods, slowPeriods); diff --git a/src/a-d/Awesome/Awesome.Models.cs b/src/a-d/Awesome/Awesome.Models.cs index 344ea39e4..393df1f0a 100644 --- a/src/a-d/Awesome/Awesome.Models.cs +++ b/src/a-d/Awesome/Awesome.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class AwesomeResult : IReusableResult +public record struct AwesomeResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Oscillator { get; set; } public double? Normalized { get; set; } - double IReusableResult.Value => Oscillator.Null2NaN(); + readonly double IReusableResult.Value + => Oscillator.Null2NaN(); } diff --git a/src/a-d/BasicQuote/BasicQuote.Api.cs b/src/a-d/BasicQuote/BasicQuote.Api.cs deleted file mode 100644 index 0cbc3cd51..000000000 --- a/src/a-d/BasicQuote/BasicQuote.Api.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Skender.Stock.Indicators; - -public static partial class Indicator -{ - // BASE QUOTE (specified candle part) - /// - /// - public static IEnumerable GetBaseQuote( - this IEnumerable quotes, - CandlePart candlePart) - where TQuote : IQuote => quotes - .Select(q => q.ToBasicData(candlePart)) - .OrderBy(x => x.Timestamp); - - // BASE QUOTE (default to Close) - /// - /// - public static IEnumerable GetBaseQuote( - this IEnumerable quotes) - where TQuote : IQuote => quotes - .Select(q => q.ToBasicData(CandlePart.Close)) - .OrderBy(x => x.Timestamp); -} diff --git a/src/a-d/BasicQuote/BasicQuote.Common.cs b/src/a-d/BasicQuote/BasicQuote.Common.cs deleted file mode 100644 index 37231710e..000000000 --- a/src/a-d/BasicQuote/BasicQuote.Common.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Skender.Stock.Indicators; - -// BASE QUOTE (COMMON) - -public static class BasicQuote -{ - // parameter validation - internal static void Validate(Quote quote) - { - if (quote is null) - { - throw new ArgumentNullException(nameof(quote), "Quote cannot be null for BasicQuotes"); - } - } - -} diff --git a/src/a-d/BasicQuote/BasicQuote.Models.cs b/src/a-d/BasicQuote/BasicQuote.Models.cs deleted file mode 100644 index a64d4b546..000000000 --- a/src/a-d/BasicQuote/BasicQuote.Models.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Skender.Stock.Indicators; - -public sealed record class BasicResult : IReusableResult -{ - public DateTime Timestamp { get; set; } - public double Value { get; set; } -} diff --git a/src/a-d/BasicQuote/info.xml b/src/a-d/BasicQuote/info.xml deleted file mode 100644 index e6c0b5ec7..000000000 --- a/src/a-d/BasicQuote/info.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - A simple transform of Quote into a generic date/value class where the specified Candle Part is the value. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - The OHLCV element or simply calculated value type to return. - Time series of Basic Quote values. - Invalid candle part provided. - - - - - A simple transform of Quote into a generic date/value class where Close is the value. - - See - documentation - for more information. - - - Configurable Quote type. See Guide for more information. - Historical price quotes. - Time series of Close values. - - - \ No newline at end of file diff --git a/src/a-d/Beta/Beta.Api.cs b/src/a-d/Beta/Beta.Api.cs index 20c94d851..8a890d589 100644 --- a/src/a-d/Beta/Beta.Api.cs +++ b/src/a-d/Beta/Beta.Api.cs @@ -3,32 +3,13 @@ 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, + public static IEnumerable GetBeta( + this IEnumerable evalResults, + IEnumerable mrktResults, int lookbackPeriods, BetaType type = BetaType.Standard) + where T : IReusableResult { List<(DateTime Timestamp, double Value)> tpListEval = evalResults.ToTupleResult(); diff --git a/src/a-d/Beta/Beta.Models.cs b/src/a-d/Beta/Beta.Models.cs index 091f2e050..0e405d2de 100644 --- a/src/a-d/Beta/Beta.Models.cs +++ b/src/a-d/Beta/Beta.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class BetaResult : IReusableResult +public record struct BetaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Beta { get; set; } @@ -11,7 +11,8 @@ public sealed record class BetaResult : IReusableResult public double? ReturnsEval { get; set; } public double? ReturnsMrkt { get; set; } - double IReusableResult.Value => Beta.Null2NaN(); + readonly double IReusableResult.Value + => Beta.Null2NaN(); } public enum BetaType diff --git a/src/a-d/BollingerBands/BollingerBands.Api.cs b/src/a-d/BollingerBands/BollingerBands.Api.cs index 8dd282f39..bce7b6bea 100644 --- a/src/a-d/BollingerBands/BollingerBands.Api.cs +++ b/src/a-d/BollingerBands/BollingerBands.Api.cs @@ -3,22 +3,13 @@ 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, + public static IEnumerable GetBollingerBands( + this IEnumerable results, int lookbackPeriods = 20, - double standardDeviations = 2) => results + double standardDeviations = 2) + where T : IReusableResult + => results .ToTupleResult() .CalcBollingerBands(lookbackPeriods, standardDeviations); diff --git a/src/a-d/BollingerBands/BollingerBands.Models.cs b/src/a-d/BollingerBands/BollingerBands.Models.cs index 85e5f31aa..11ca582e6 100644 --- a/src/a-d/BollingerBands/BollingerBands.Models.cs +++ b/src/a-d/BollingerBands/BollingerBands.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class BollingerBandsResult : IReusableResult +public record struct BollingerBandsResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Sma { get; set; } @@ -11,5 +11,6 @@ public sealed record class BollingerBandsResult : IReusableResult public double? ZScore { get; set; } public double? Width { get; set; } - double IReusableResult.Value => PercentB.Null2NaN(); + readonly double IReusableResult.Value + => PercentB.Null2NaN(); } diff --git a/src/a-d/Bop/Bop.Models.cs b/src/a-d/Bop/Bop.Models.cs index fa0669e78..d7357192b 100644 --- a/src/a-d/Bop/Bop.Models.cs +++ b/src/a-d/Bop/Bop.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class BopResult : IReusableResult +public record struct BopResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Bop { get; set; } - double IReusableResult.Value => Bop.Null2NaN(); + readonly double IReusableResult.Value + => Bop.Null2NaN(); } diff --git a/src/a-d/Cci/Cci.Models.cs b/src/a-d/Cci/Cci.Models.cs index cc0266c8f..1ee8860f7 100644 --- a/src/a-d/Cci/Cci.Models.cs +++ b/src/a-d/Cci/Cci.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class CciResult : IReusableResult +public record struct CciResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Cci { get; set; } - double IReusableResult.Value => Cci.Null2NaN(); + readonly double IReusableResult.Value + => Cci.Null2NaN(); } diff --git a/src/a-d/ChaikinOsc/ChaikinOsc.Models.cs b/src/a-d/ChaikinOsc/ChaikinOsc.Models.cs index f16d2d522..373d4c1be 100644 --- a/src/a-d/ChaikinOsc/ChaikinOsc.Models.cs +++ b/src/a-d/ChaikinOsc/ChaikinOsc.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class ChaikinOscResult : IReusableResult +public record struct ChaikinOscResult : IReusableResult { public DateTime Timestamp { get; set; } public double? MoneyFlowMultiplier { get; set; } @@ -8,5 +8,6 @@ public sealed record class ChaikinOscResult : IReusableResult public double? Adl { get; set; } public double? Oscillator { get; set; } - double IReusableResult.Value => Oscillator.Null2NaN(); + readonly double IReusableResult.Value + => Oscillator.Null2NaN(); } diff --git a/src/a-d/Chandelier/Chandelier.Models.cs b/src/a-d/Chandelier/Chandelier.Models.cs index 5a15a412c..b39e94d8b 100644 --- a/src/a-d/Chandelier/Chandelier.Models.cs +++ b/src/a-d/Chandelier/Chandelier.Models.cs @@ -1,11 +1,12 @@ namespace Skender.Stock.Indicators; -public sealed record class ChandelierResult : IReusableResult +public record struct ChandelierResult : IReusableResult { public DateTime Timestamp { get; set; } public double? ChandelierExit { get; set; } - double IReusableResult.Value => ChandelierExit.Null2NaN(); + readonly double IReusableResult.Value + => ChandelierExit.Null2NaN(); } public enum ChandelierType diff --git a/src/a-d/Chop/Chop.Models.cs b/src/a-d/Chop/Chop.Models.cs index c63db3abd..fee40cd55 100644 --- a/src/a-d/Chop/Chop.Models.cs +++ b/src/a-d/Chop/Chop.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class ChopResult : IReusableResult +public record struct ChopResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Chop { get; set; } - double IReusableResult.Value => Chop.Null2NaN(); + readonly double IReusableResult.Value + => Chop.Null2NaN(); } diff --git a/src/a-d/Cmf/Cmf.Models.cs b/src/a-d/Cmf/Cmf.Models.cs index 6e239ccd4..f18a61328 100644 --- a/src/a-d/Cmf/Cmf.Models.cs +++ b/src/a-d/Cmf/Cmf.Models.cs @@ -1,11 +1,12 @@ namespace Skender.Stock.Indicators; -public sealed record class CmfResult : IReusableResult +public record struct CmfResult : IReusableResult { public DateTime Timestamp { get; set; } public double? MoneyFlowMultiplier { get; set; } public double? MoneyFlowVolume { get; set; } public double? Cmf { get; set; } - double IReusableResult.Value => Cmf.Null2NaN(); + readonly double IReusableResult.Value + => Cmf.Null2NaN(); } diff --git a/src/a-d/Cmo/Cmo.Api.cs b/src/a-d/Cmo/Cmo.Api.cs index 60ecdee74..adeb818de 100644 --- a/src/a-d/Cmo/Cmo.Api.cs +++ b/src/a-d/Cmo/Cmo.Api.cs @@ -3,20 +3,12 @@ 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 + public static IEnumerable GetCmo( + this IEnumerable results, + int lookbackPeriods) + where T : IReusableResult + => results .ToTupleResult() .CalcCmo(lookbackPeriods); diff --git a/src/a-d/Cmo/Cmo.Models.cs b/src/a-d/Cmo/Cmo.Models.cs index 4785e5b76..424074b89 100644 --- a/src/a-d/Cmo/Cmo.Models.cs +++ b/src/a-d/Cmo/Cmo.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class CmoResult : IReusableResult +public record struct CmoResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Cmo { get; set; } - double IReusableResult.Value => Cmo.Null2NaN(); + readonly double IReusableResult.Value + => Cmo.Null2NaN(); } diff --git a/src/a-d/ConnorsRsi/ConnorsRsi.Api.cs b/src/a-d/ConnorsRsi/ConnorsRsi.Api.cs index cd75d7714..ffb6662d3 100644 --- a/src/a-d/ConnorsRsi/ConnorsRsi.Api.cs +++ b/src/a-d/ConnorsRsi/ConnorsRsi.Api.cs @@ -3,24 +3,14 @@ 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, + public static IEnumerable GetConnorsRsi( + this IEnumerable results, int rsiPeriods = 3, int streakPeriods = 2, - int rankPeriods = 100) => results + int rankPeriods = 100) + where T: IReusableResult + => results .ToTupleResult() .CalcConnorsRsi(rsiPeriods, streakPeriods, rankPeriods); diff --git a/src/a-d/ConnorsRsi/ConnorsRsi.Models.cs b/src/a-d/ConnorsRsi/ConnorsRsi.Models.cs index e80fe3348..3d67bc2c1 100644 --- a/src/a-d/ConnorsRsi/ConnorsRsi.Models.cs +++ b/src/a-d/ConnorsRsi/ConnorsRsi.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class ConnorsRsiResult : IReusableResult +public record struct ConnorsRsiResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Rsi { get; set; } @@ -10,5 +10,7 @@ public sealed record class ConnorsRsiResult : IReusableResult // internal use only internal double Streak { get; set; } - double IReusableResult.Value => ConnorsRsi.Null2NaN(); + + readonly double IReusableResult.Value + => ConnorsRsi.Null2NaN(); } diff --git a/src/a-d/Correlation/Correlation.Api.cs b/src/a-d/Correlation/Correlation.Api.cs index f38de9f1f..7733066c6 100644 --- a/src/a-d/Correlation/Correlation.Api.cs +++ b/src/a-d/Correlation/Correlation.Api.cs @@ -3,29 +3,12 @@ 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, + public static IEnumerable GetCorrelation( + this IEnumerable quotesA, + IEnumerable quotesB, int lookbackPeriods) + where T : IReusableResult { List<(DateTime Timestamp, double Value)> tpListA = quotesA.ToTupleResult(); diff --git a/src/a-d/Correlation/Correlation.Models.cs b/src/a-d/Correlation/Correlation.Models.cs index ccf28f3f4..5803274dd 100644 --- a/src/a-d/Correlation/Correlation.Models.cs +++ b/src/a-d/Correlation/Correlation.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class CorrResult : IReusableResult +public record struct CorrResult : IReusableResult { public DateTime Timestamp { get; set; } public double? VarianceA { get; set; } @@ -9,5 +9,6 @@ public sealed record class CorrResult : IReusableResult public double? Correlation { get; set; } public double? RSquared { get; set; } - double IReusableResult.Value => Correlation.Null2NaN(); + readonly double IReusableResult.Value + => Correlation.Null2NaN(); } diff --git a/src/a-d/Dema/Dema.Api.cs b/src/a-d/Dema/Dema.Api.cs index 812bc04e4..3e648e1d4 100644 --- a/src/a-d/Dema/Dema.Api.cs +++ b/src/a-d/Dema/Dema.Api.cs @@ -3,20 +3,12 @@ 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 + public static IEnumerable GetDema( + this IEnumerable results, + int lookbackPeriods) + where T:IReusableResult + => results .ToTupleResult() .CalcDema(lookbackPeriods); diff --git a/src/a-d/Dema/Dema.Models.cs b/src/a-d/Dema/Dema.Models.cs index 5dc3d8ed9..99ac510ff 100644 --- a/src/a-d/Dema/Dema.Models.cs +++ b/src/a-d/Dema/Dema.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class DemaResult : IReusableResult +public record struct DemaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Dema { get; set; } - double IReusableResult.Value => Dema.Null2NaN(); + readonly double IReusableResult.Value + => Dema.Null2NaN(); } diff --git a/src/a-d/Donchian/Donchian.Models.cs b/src/a-d/Donchian/Donchian.Models.cs index d7ec449f1..054e4a762 100644 --- a/src/a-d/Donchian/Donchian.Models.cs +++ b/src/a-d/Donchian/Donchian.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class DonchianResult : IResult +public record struct DonchianResult : IResult { public DateTime Timestamp { get; set; } public decimal? UpperBand { get; set; } diff --git a/src/a-d/Dpo/Dpo.Api.cs b/src/a-d/Dpo/Dpo.Api.cs index 32032a9ea..b9eab1ce7 100644 --- a/src/a-d/Dpo/Dpo.Api.cs +++ b/src/a-d/Dpo/Dpo.Api.cs @@ -3,27 +3,12 @@ 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 - .ToTupleResult() - .CalcDpo(lookbackPeriods); - - // SERIES, from TUPLE - public static IEnumerable GetDpo( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples + public static IEnumerable GetDpo( + this IEnumerable results, + int lookbackPeriods) + where T : IReusableResult + => results .ToSortedList() .CalcDpo(lookbackPeriods); } diff --git a/src/a-d/Dpo/Dpo.Models.cs b/src/a-d/Dpo/Dpo.Models.cs index 78d0cc063..0d4f8195f 100644 --- a/src/a-d/Dpo/Dpo.Models.cs +++ b/src/a-d/Dpo/Dpo.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class DpoResult : IReusableResult +public record struct DpoResult( + DateTime Timestamp, + double? Dpo = default, + double? Sma = default) + : IReusableResult { - public DateTime Timestamp { get; set; } - public double? Sma { get; set; } - public double? Dpo { get; set; } - - double IReusableResult.Value => Dpo.Null2NaN(); + readonly double IReusableResult.Value + => Dpo.Null2NaN(); } diff --git a/src/a-d/Dpo/Dpo.Series.cs b/src/a-d/Dpo/Dpo.Series.cs index 617a36343..936dfc74f 100644 --- a/src/a-d/Dpo/Dpo.Series.cs +++ b/src/a-d/Dpo/Dpo.Series.cs @@ -5,9 +5,10 @@ namespace Skender.Stock.Indicators; public static partial class Indicator { // calculate series - internal static List CalcDpo( - this List<(DateTime, double)> tpList, + internal static List CalcDpo( + this List tpList, int lookbackPeriods) + where T : IReusableResult { // check parameter arguments Dpo.Validate(lookbackPeriods); @@ -21,17 +22,24 @@ internal static List CalcDpo( // roll through quotes for (int i = 0; i < length; i++) { - (DateTime date, double value) = tpList[i]; + T src = tpList[i]; - DpoResult r = new() { Timestamp = date }; - results.Add(r); + double? dpoSma = default; + double? dpoVal = default; 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(); + 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/Dynamic/Dynamic.Api.cs b/src/a-d/Dynamic/Dynamic.Api.cs index fec4a73e9..45407109e 100644 --- a/src/a-d/Dynamic/Dynamic.Api.cs +++ b/src/a-d/Dynamic/Dynamic.Api.cs @@ -3,22 +3,13 @@ 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, + public static IEnumerable GetDynamic( + this IEnumerable results, int lookbackPeriods, - double kFactor = 0.6) => results + double kFactor = 0.6) + where T: IReusableResult + => results .ToTupleResult() .CalcDynamic(lookbackPeriods, kFactor); diff --git a/src/a-d/Dynamic/Dynamic.Models.cs b/src/a-d/Dynamic/Dynamic.Models.cs index 2f5b480de..8f3eed5b9 100644 --- a/src/a-d/Dynamic/Dynamic.Models.cs +++ b/src/a-d/Dynamic/Dynamic.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class DynamicResult : IReusableResult +public record struct DynamicResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Dynamic { get; set; } - double IReusableResult.Value => Dynamic.Null2NaN(); + readonly double IReusableResult.Value + => Dynamic.Null2NaN(); } diff --git a/src/e-k/ElderRay/ElderRay.Models.cs b/src/e-k/ElderRay/ElderRay.Models.cs index 67f5fc53b..c9c3e0e4e 100644 --- a/src/e-k/ElderRay/ElderRay.Models.cs +++ b/src/e-k/ElderRay/ElderRay.Models.cs @@ -1,11 +1,12 @@ namespace Skender.Stock.Indicators; -public sealed record class ElderRayResult : IReusableResult +public record struct ElderRayResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Ema { get; set; } public double? BullPower { get; set; } public double? BearPower { get; set; } - double IReusableResult.Value => (BullPower + BearPower).Null2NaN(); + readonly double IReusableResult.Value + => (BullPower + BearPower).Null2NaN(); } diff --git a/src/e-k/Ema/Ema.Api.cs b/src/e-k/Ema/Ema.Api.cs index bd5876f81..29a65d5ca 100644 --- a/src/e-k/Ema/Ema.Api.cs +++ b/src/e-k/Ema/Ema.Api.cs @@ -4,49 +4,19 @@ namespace Skender.Stock.Indicators; 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 + public static IEnumerable GetEma( + this IEnumerable results, + int lookbackPeriods) + where T: IReusableResult + => results .ToTupleResult() .CalcEma(lookbackPeriods); - // SERIES, from TUPLE - public static IEnumerable GetEma( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcEma(lookbackPeriods); - - // OBSERVER, from Quote Provider - /// - /// - public static Ema AttachEma( - this QuoteProvider quoteProvider, - int lookbackPeriods) - where TQuote : IQuote, new() - { - Use chainProvider = quoteProvider - .Use(CandlePart.Close); - - return new(chainProvider, lookbackPeriods); - } - // OBSERVER, from Chain Provider - /// - /// - public static Ema AttachEma( - this ChainProvider chainProvider, + public static Ema ToEma( + this IChainProvider chainProvider, int lookbackPeriods) + where TIn : struct, IReusableResult => new(chainProvider, lookbackPeriods); } diff --git a/src/e-k/Ema/Ema.Common.cs b/src/e-k/Ema/Ema.Common.cs index 7bc692b94..cf290a4b5 100644 --- a/src/e-k/Ema/Ema.Common.cs +++ b/src/e-k/Ema/Ema.Common.cs @@ -5,12 +5,8 @@ namespace Skender.Stock.Indicators; /// See the /// Stock Indicators for .NET online guide for more information. -public partial class Ema +public static class EmaUtilities { - // string label - public override string ToString() - => $"EMA({LookbackPeriods})"; - // INCREMENT CALCULATIONS /// diff --git a/src/e-k/Ema/Ema.Models.cs b/src/e-k/Ema/Ema.Models.cs index 5b4c0d3f5..e9c39b67d 100644 --- a/src/e-k/Ema/Ema.Models.cs +++ b/src/e-k/Ema/Ema.Models.cs @@ -1,16 +1,15 @@ namespace Skender.Stock.Indicators; -public sealed record class EmaResult : IReusableResult +public record struct EmaResult( + DateTime Timestamp, + double? Ema = default) + : IReusableResult { - public DateTime Timestamp { get; set; } - public double? Ema { get; internal set; } - - double IReusableResult.Value => Ema.Null2NaN(); + readonly double IReusableResult.Value + => Ema.Null2NaN(); } -public interface IEma : - IChainObserver, - IChainProvider +public interface IEma : IStreamObserver { int LookbackPeriods { get; } double K { get; } diff --git a/src/e-k/Ema/Ema.Series.cs b/src/e-k/Ema/Ema.Series.cs index d64a93cdd..ad8e867a0 100644 --- a/src/e-k/Ema/Ema.Series.cs +++ b/src/e-k/Ema/Ema.Series.cs @@ -9,7 +9,7 @@ internal static List CalcEma( int lookbackPeriods) { // check parameter arguments - Ema.Validate(lookbackPeriods); + EmaUtilities.Validate(lookbackPeriods); // initialize int length = tpList.Count; @@ -23,12 +23,10 @@ internal static List CalcEma( { (DateTime date, double value) = tpList[i]; - EmaResult r = new() { Timestamp = date }; - results.Add(r); - // skip incalculable periods if (i < lookbackPeriods - 1) { + results.Add(new EmaResult(Timestamp: date)); continue; } @@ -50,10 +48,12 @@ internal static List CalcEma( // normal EMA else { - ema = Ema.Increment(k, lastEma, value); + ema = EmaUtilities.Increment(k, lastEma, value); } - r.Ema = ema.NaN2Null(); + EmaResult r = new(Timestamp: date, Ema: ema.NaN2Null()); + results.Add(r); + lastEma = ema; } diff --git a/src/e-k/Ema/Ema.Stream.cs b/src/e-k/Ema/Ema.Stream.cs index 87236adc0..4e657a1e5 100644 --- a/src/e-k/Ema/Ema.Stream.cs +++ b/src/e-k/Ema/Ema.Stream.cs @@ -2,15 +2,17 @@ namespace Skender.Stock.Indicators; // EXPONENTIAL MOVING AVERAGE (STREAMING) -public partial class Ema : ChainObserver, IEma +public class Ema + : AbstractChainInChainOut, IEma + where TIn : struct, IReusableResult { // constructor public Ema( - ChainProvider provider, + IChainProvider provider, int lookbackPeriods) - : base(provider, isChainor: true) + : base(provider) { - Validate(lookbackPeriods); + EmaUtilities.Validate(lookbackPeriods); LookbackPeriods = lookbackPeriods; K = 2d / (lookbackPeriods + 1); @@ -18,7 +20,7 @@ public Ema( RebuildCache(); // subscribe to provider - unsubscriber = provider != null + Subscription = provider != null ? provider.Subscribe(this) : throw new ArgumentNullException(nameof(provider)); } @@ -30,25 +32,27 @@ public Ema( // METHODS + // string label + public override string ToString() + => $"EMA({LookbackPeriods})"; + // handle chain arrival - public override void OnNext((Act act, DateTime date, double price) value) + internal override void OnNextArrival(Act act, IReusableResult inbound) { int i; double ema; - List<(DateTime _, double value)> supplier = ChainSupplier.Chain; - // handle deletes - if (value.act == Act.Delete) + if (act is Act.Delete) { - i = Cache.FindIndex(value.date); + i = Cache.FindIndex(inbound.Timestamp); ema = Cache[i].Ema.Null2NaN(); } // handle new values else { - i = supplier.FindIndex(value.date); + i = Provider.FindIndex(inbound.Timestamp); // source unexpectedly not found if (i == -1) @@ -65,7 +69,7 @@ public override void OnNext((Act act, DateTime date, double price) value) // normal if (!double.IsNaN(last.Value)) { - ema = Increment(K, last.Value, value.price); + ema = EmaUtilities.Increment(K, last.Value, inbound.Value); } // set first value (normal) or reset @@ -75,7 +79,7 @@ public override void OnNext((Act act, DateTime date, double price) value) double sum = 0; for (int w = i - LookbackPeriods + 1; w <= i; w++) { - sum += supplier[w].value; + sum += Provider.Results[w].Value; } ema = sum / LookbackPeriods; @@ -90,39 +94,22 @@ public override void OnNext((Act act, DateTime date, double price) value) } // candidate result - EmaResult r = new() { - Timestamp = value.date, - Ema = ema.NaN2Null() - }; + EmaResult r = new( + Timestamp: inbound.Timestamp, + Ema: ema.NaN2Null()); // save to cache - Act act = CacheChainorPerAction(value.act, r, ema); + act = ModifyCache(act, r); // send to observers NotifyObservers(act, r); - // update forward values - if (act != Act.AddNew && i < supplier.Count - 1) + // cascade update forward values (recursively) + if (act != Act.AddNew && i < ProviderCache.Count - 1) { - // cascade updates gracefully int next = act == Act.Delete ? i : i + 1; - (DateTime d, double v) = supplier[next]; - OnNext((Act.Update, d, v)); - } - } - - // delete cache between index values - // usually called from inherited ClearCache(fromDate) - internal override void ClearCache(int fromIndex, int toIndex) - { - // delete and deliver instruction, - // in reverse order to prevent recompositions - - for (int i = toIndex; i >= fromIndex; i--) - { - EmaResult r = Cache[i]; - Act act = CacheChainorPerAction(Act.Delete, r, double.NaN); - NotifyObservers(act, r); + TIn value = ProviderCache[next]; + OnNextArrival(Act.Update, value); } } } diff --git a/src/e-k/Ema/info.xml b/src/e-k/Ema/info.xml index e7ebc807f..cb90b4738 100644 --- a/src/e-k/Ema/info.xml +++ b/src/e-k/Ema/info.xml @@ -57,33 +57,4 @@ Historical price quote. New EmaResult value. - - - - Establish an observable streaming Exponential Moving Average (EMA) from a Quote provider. - See - documentation - for more information. - - - Observable quote provider. - Number of periods in the lookback window. - Observable EMA instance. - Invalid parameter value provided. - - - - - Establish an observable streaming Exponential Moving Average (EMA) from a Chain provider. - - See - documentation - for more information. - - - Observable source 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 index bc3fbb035..5459547c7 100644 --- a/src/e-k/Epma/Epma.Api.cs +++ b/src/e-k/Epma/Epma.Api.cs @@ -3,20 +3,12 @@ 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 + public static IEnumerable GetEpma( + this IEnumerable results, + int lookbackPeriods) + where T: IReusableResult + => results .ToTupleResult() .CalcEpma(lookbackPeriods); diff --git a/src/e-k/Epma/Epma.Models.cs b/src/e-k/Epma/Epma.Models.cs index 8357be0cf..331c6d29d 100644 --- a/src/e-k/Epma/Epma.Models.cs +++ b/src/e-k/Epma/Epma.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class EpmaResult : IReusableResult +public record struct EpmaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Epma { get; set; } - double IReusableResult.Value => Epma.Null2NaN(); + readonly double IReusableResult.Value + => Epma.Null2NaN(); } diff --git a/src/e-k/Fcb/Fcb.Models.cs b/src/e-k/Fcb/Fcb.Models.cs index c62dfcace..6f2c718a6 100644 --- a/src/e-k/Fcb/Fcb.Models.cs +++ b/src/e-k/Fcb/Fcb.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class FcbResult : IResult +public record struct FcbResult : IResult { public DateTime Timestamp { get; set; } public decimal? UpperBand { get; set; } diff --git a/src/e-k/FisherTransform/FisherTransform.Api.cs b/src/e-k/FisherTransform/FisherTransform.Api.cs index 84bfe6a41..587d99c30 100644 --- a/src/e-k/FisherTransform/FisherTransform.Api.cs +++ b/src/e-k/FisherTransform/FisherTransform.Api.cs @@ -3,20 +3,12 @@ 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 + public static IEnumerable GetFisherTransform( + this IEnumerable results, + int lookbackPeriods = 10) + where T: IReusableResult + => results .ToTupleResult() .CalcFisherTransform(lookbackPeriods); diff --git a/src/e-k/FisherTransform/FisherTransform.Models.cs b/src/e-k/FisherTransform/FisherTransform.Models.cs index f139511ee..d44840b12 100644 --- a/src/e-k/FisherTransform/FisherTransform.Models.cs +++ b/src/e-k/FisherTransform/FisherTransform.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class FisherTransformResult : IReusableResult +public record struct FisherTransformResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Fisher { get; set; } public double? Trigger { get; set; } - double IReusableResult.Value => Fisher.Null2NaN(); + readonly double IReusableResult.Value + => Fisher.Null2NaN(); } diff --git a/src/e-k/ForceIndex/ForceIndex.Models.cs b/src/e-k/ForceIndex/ForceIndex.Models.cs index b5c0b53bb..de2931e1b 100644 --- a/src/e-k/ForceIndex/ForceIndex.Models.cs +++ b/src/e-k/ForceIndex/ForceIndex.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class ForceIndexResult : IReusableResult +public record struct ForceIndexResult : IReusableResult { public DateTime Timestamp { get; set; } public double? ForceIndex { get; set; } - double IReusableResult.Value => ForceIndex.Null2NaN(); + readonly double IReusableResult.Value + => ForceIndex.Null2NaN(); } diff --git a/src/e-k/Fractal/Fractal.Models.cs b/src/e-k/Fractal/Fractal.Models.cs index ce2c0bc9e..bdcd9321a 100644 --- a/src/e-k/Fractal/Fractal.Models.cs +++ b/src/e-k/Fractal/Fractal.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class FractalResult : IResult +public record struct FractalResult : IResult { public DateTime Timestamp { get; set; } public decimal? FractalBear { get; set; } diff --git a/src/e-k/Gator/Gator.Api.cs b/src/e-k/Gator/Gator.Api.cs index 086df0bb8..67ad66071 100644 --- a/src/e-k/Gator/Gator.Api.cs +++ b/src/e-k/Gator/Gator.Api.cs @@ -4,12 +4,24 @@ namespace Skender.Stock.Indicators; public static partial class Indicator { // SERIES, from TQuote - /// - /// + /// + /// 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. public static IEnumerable GetGator( - this IEnumerable quotes) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.HL2) + this IEnumerable quotes, + CandlePart candlePart = CandlePart.HL2) + where TQuote : IQuote + => quotes + .Use(candlePart) .GetAlligator() .ToList() .CalcGator(); @@ -21,17 +33,10 @@ public static IEnumerable GetGator( .CalcGator(); // SERIES, from CHAIN - public static IEnumerable GetGator( - this IEnumerable results) => results - .ToTupleResult() - .GetAlligator() - .ToList() - .CalcGator(); - - // SERIES, from TUPLE - public static IEnumerable GetGator( - this IEnumerable<(DateTime, double)> priceTuples) => priceTuples - .ToSortedList() + public static IEnumerable GetGator( + this IEnumerable results) + where T : IReusableResult + => results .GetAlligator() .ToList() .CalcGator(); 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.Models.cs b/src/e-k/HeikinAshi/HeikinAshi.Models.cs index d15553272..7b7e58afe 100644 --- a/src/e-k/HeikinAshi/HeikinAshi.Models.cs +++ b/src/e-k/HeikinAshi/HeikinAshi.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class HeikinAshiResult : IResult, IQuote +public record struct HeikinAshiResult : IReusableResult, IQuote { public DateTime Timestamp { get; set; } public decimal Open { get; set; } @@ -9,6 +9,6 @@ public sealed record class HeikinAshiResult : IResult, IQuote public decimal Close { get; set; } public decimal Volume { get; set; } - public bool Equals(IQuote? other) - => base.Equals(other); + readonly double IReusableResult.Value + => (double)Close; } diff --git a/src/e-k/Hma/Hma.Api.cs b/src/e-k/Hma/Hma.Api.cs index 8d254a837..4b1f89596 100644 --- a/src/e-k/Hma/Hma.Api.cs +++ b/src/e-k/Hma/Hma.Api.cs @@ -3,20 +3,12 @@ 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 + public static IEnumerable GetHma( + this IEnumerable results, + int lookbackPeriods) + where T: IReusableResult + => results .ToTupleResult() .CalcHma(lookbackPeriods); diff --git a/src/e-k/Hma/Hma.Models.cs b/src/e-k/Hma/Hma.Models.cs index b2240b6d3..34897d0f0 100644 --- a/src/e-k/Hma/Hma.Models.cs +++ b/src/e-k/Hma/Hma.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class HmaResult : IReusableResult +public record struct HmaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Hma { get; set; } - double IReusableResult.Value => Hma.Null2NaN(); + readonly double IReusableResult.Value + => Hma.Null2NaN(); } diff --git a/src/e-k/HtTrendline/HtTrendline.Api.cs b/src/e-k/HtTrendline/HtTrendline.Api.cs index 95018b6d0..7c52b2031 100644 --- a/src/e-k/HtTrendline/HtTrendline.Api.cs +++ b/src/e-k/HtTrendline/HtTrendline.Api.cs @@ -4,17 +4,30 @@ namespace Skender.Stock.Indicators; public static partial class Indicator { // SERIES, from TQuote - /// - /// + /// + /// 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. + /// Optional. Default is HL2. + /// Time series of HTL values and smoothed price. public static IEnumerable GetHtTrendline( - this IEnumerable quotes) + this IEnumerable quotes, + CandlePart candlePart = CandlePart.HL2) where TQuote : IQuote => quotes - .ToTuple(CandlePart.HL2) + .ToTuple(candlePart) .CalcHtTrendline(); // SERIES, from CHAIN - public static IEnumerable GetHtTrendline( - this IEnumerable results) => results + public static IEnumerable GetHtTrendline( + this IEnumerable results) + where T : IReusableResult + => results .ToTupleResult() .CalcHtTrendline(); diff --git a/src/e-k/HtTrendline/HtTrendline.Models.cs b/src/e-k/HtTrendline/HtTrendline.Models.cs index cc0167828..028f3f3c1 100644 --- a/src/e-k/HtTrendline/HtTrendline.Models.cs +++ b/src/e-k/HtTrendline/HtTrendline.Models.cs @@ -1,11 +1,12 @@ namespace Skender.Stock.Indicators; -public sealed record class HtlResult : IReusableResult +public record struct HtlResult : IReusableResult { public DateTime Timestamp { get; set; } public int? DcPeriods { get; set; } public double? Trendline { get; set; } public double? SmoothPrice { get; set; } - double IReusableResult.Value => Trendline.Null2NaN(); + readonly double IReusableResult.Value + => Trendline.Null2NaN(); } 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 index 344b318ad..4ef2e2897 100644 --- a/src/e-k/Hurst/Hurst.Api.cs +++ b/src/e-k/Hurst/Hurst.Api.cs @@ -3,19 +3,12 @@ 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 + public static IEnumerable GetHurst( + this IEnumerable results, + int lookbackPeriods = 100) + where T: IReusableResult + => results .ToTupleResult() .CalcHurst(lookbackPeriods); diff --git a/src/e-k/Hurst/Hurst.Models.cs b/src/e-k/Hurst/Hurst.Models.cs index f6b442e28..b4f34b793 100644 --- a/src/e-k/Hurst/Hurst.Models.cs +++ b/src/e-k/Hurst/Hurst.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class HurstResult : IReusableResult +public record struct HurstResult : IReusableResult { public DateTime Timestamp { get; set; } public double? HurstExponent { get; set; } - double IReusableResult.Value => HurstExponent.Null2NaN(); + readonly double IReusableResult.Value + => HurstExponent.Null2NaN(); } diff --git a/src/e-k/Ichimoku/Ichimoku.Models.cs b/src/e-k/Ichimoku/Ichimoku.Models.cs index 207c74d95..5705d041e 100644 --- a/src/e-k/Ichimoku/Ichimoku.Models.cs +++ b/src/e-k/Ichimoku/Ichimoku.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class IchimokuResult : IResult +public record struct IchimokuResult : IResult { public DateTime Timestamp { get; set; } public decimal? TenkanSen { get; set; } // conversion line diff --git a/src/e-k/Ichimoku/Ichimoku.Series.cs b/src/e-k/Ichimoku/Ichimoku.Series.cs index 5cba9afac..63e62c286 100644 --- a/src/e-k/Ichimoku/Ichimoku.Series.cs +++ b/src/e-k/Ichimoku/Ichimoku.Series.cs @@ -47,7 +47,7 @@ internal static List CalcIchimoku( { IchimokuResult skq = results[i - senkouOffset]; - if (skq != null && skq.TenkanSen != null && skq.KijunSen != null) + if (skq.TenkanSen != null && skq.KijunSen != null) { r.SenkouSpanA = (skq.TenkanSen + skq.KijunSen) / 2; } diff --git a/src/e-k/Kama/Kama.Api.cs b/src/e-k/Kama/Kama.Api.cs index b515f38c2..870864bca 100644 --- a/src/e-k/Kama/Kama.Api.cs +++ b/src/e-k/Kama/Kama.Api.cs @@ -3,23 +3,14 @@ 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, + public static IEnumerable GetKama( + this IEnumerable results, int erPeriods = 10, int fastPeriods = 2, - int slowPeriods = 30) => results + int slowPeriods = 30) + where T: IReusableResult + => results .ToTupleResult() .CalcKama(erPeriods, fastPeriods, slowPeriods); diff --git a/src/e-k/Kama/Kama.Models.cs b/src/e-k/Kama/Kama.Models.cs index 3c11b08ff..44707cdcb 100644 --- a/src/e-k/Kama/Kama.Models.cs +++ b/src/e-k/Kama/Kama.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class KamaResult : IReusableResult +public record struct KamaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? ER { get; set; } public double? Kama { get; set; } - double IReusableResult.Value => Kama.Null2NaN(); + readonly double IReusableResult.Value + => Kama.Null2NaN(); } diff --git a/src/e-k/Keltner/Keltner.Models.cs b/src/e-k/Keltner/Keltner.Models.cs index af84e7b6a..cf7751259 100644 --- a/src/e-k/Keltner/Keltner.Models.cs +++ b/src/e-k/Keltner/Keltner.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class KeltnerResult : IResult +public record struct KeltnerResult : IResult { public DateTime Timestamp { get; set; } public double? UpperBand { get; set; } diff --git a/src/e-k/Kvo/Kvo.Models.cs b/src/e-k/Kvo/Kvo.Models.cs index b7e9462bb..60ee36131 100644 --- a/src/e-k/Kvo/Kvo.Models.cs +++ b/src/e-k/Kvo/Kvo.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class KvoResult : IReusableResult +public record struct KvoResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Oscillator { get; set; } public double? Signal { get; set; } - double IReusableResult.Value => Oscillator.Null2NaN(); + readonly double IReusableResult.Value + => Oscillator.Null2NaN(); } diff --git a/src/m-r/MaEnvelopes/MaEnvelopes.Api.cs b/src/m-r/MaEnvelopes/MaEnvelopes.Api.cs index 279a8b14d..654c13598 100644 --- a/src/m-r/MaEnvelopes/MaEnvelopes.Api.cs +++ b/src/m-r/MaEnvelopes/MaEnvelopes.Api.cs @@ -3,33 +3,14 @@ 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, + public static IEnumerable GetMaEnvelopes( + this IEnumerable results, int lookbackPeriods, double percentOffset = 2.5, - MaType movingAverageType = MaType.SMA) => results - .ToTupleResult() - .CalcMaEnvelopes(lookbackPeriods, percentOffset, movingAverageType); - - // SERIES, from TUPLE - public static IEnumerable GetMaEnvelopes( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods, - double percentOffset = 2.5, - MaType movingAverageType = MaType.SMA) => priceTuples + MaType movingAverageType = MaType.SMA) + where T : IReusableResult + => results .ToSortedList() .CalcMaEnvelopes(lookbackPeriods, percentOffset, movingAverageType); } diff --git a/src/m-r/MaEnvelopes/MaEnvelopes.Models.cs b/src/m-r/MaEnvelopes/MaEnvelopes.Models.cs index 2fb690084..683f559f0 100644 --- a/src/m-r/MaEnvelopes/MaEnvelopes.Models.cs +++ b/src/m-r/MaEnvelopes/MaEnvelopes.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class MaEnvelopeResult : IResult +public record struct MaEnvelopeResult : IResult { public DateTime Timestamp { get; set; } public double? Centerline { get; set; } diff --git a/src/m-r/MaEnvelopes/MaEnvelopes.Series.cs b/src/m-r/MaEnvelopes/MaEnvelopes.Series.cs index d8b93af94..c4c90f0f1 100644 --- a/src/m-r/MaEnvelopes/MaEnvelopes.Series.cs +++ b/src/m-r/MaEnvelopes/MaEnvelopes.Series.cs @@ -5,11 +5,12 @@ namespace Skender.Stock.Indicators; public static partial class Indicator { // calculate series - internal static IEnumerable CalcMaEnvelopes( - this List<(DateTime, double)> tpList, + internal static IEnumerable CalcMaEnvelopes( + this List tpList, int lookbackPeriods, double percentOffset, MaType movingAverageType) + where T: IReusableResult { // check parameter arguments // note: most validations are done in variant methods @@ -39,10 +40,11 @@ internal static IEnumerable CalcMaEnvelopes( }; } - private static IEnumerable MaEnvAlma( - this List<(DateTime, double)> tpList, + private static IEnumerable MaEnvAlma( + this List tpList, int lookbackPeriods, double offsetRatio) + where T : IReusableResult => tpList.GetAlma(lookbackPeriods) .Select(x => new MaEnvelopeResult { Timestamp = x.Timestamp, @@ -51,10 +53,11 @@ private static IEnumerable MaEnvAlma( LowerEnvelope = x.Alma - (x.Alma * offsetRatio) }); - private static IEnumerable MaEnvDema( - this List<(DateTime, double)> tpList, + private static IEnumerable MaEnvDema( + this List tpList, int lookbackPeriods, double offsetRatio) + where T : IReusableResult => tpList.GetDema(lookbackPeriods) .Select(x => new MaEnvelopeResult { Timestamp = x.Timestamp, @@ -63,11 +66,13 @@ private static IEnumerable MaEnvDema( LowerEnvelope = x.Dema - (x.Dema * offsetRatio) }); - private static IEnumerable MaEnvEma( - this List<(DateTime, double)> tpList, + private static IEnumerable MaEnvEma( + this List tpList, int lookbackPeriods, double offsetRatio) - => tpList.GetEma(lookbackPeriods) + where T : IReusableResult + => tpList + .GetEma(lookbackPeriods) .Select(x => new MaEnvelopeResult { Timestamp = x.Timestamp, Centerline = x.Ema, @@ -75,10 +80,11 @@ private static IEnumerable MaEnvEma( LowerEnvelope = x.Ema - (x.Ema * offsetRatio) }); - private static IEnumerable MaEnvEpma( - this List<(DateTime, double)> tpList, + private static IEnumerable MaEnvEpma( + this List tpList, int lookbackPeriods, double offsetRatio) + where T : IReusableResult => tpList.GetEpma(lookbackPeriods) .Select(x => new MaEnvelopeResult { Timestamp = x.Timestamp, @@ -87,10 +93,11 @@ private static IEnumerable MaEnvEpma( LowerEnvelope = x.Epma - (x.Epma * offsetRatio) }); - private static IEnumerable MaEnvHma( - this List<(DateTime, double)> tpList, + private static IEnumerable MaEnvHma( + this List tpList, int lookbackPeriods, double offsetRatio) + where T : IReusableResult => tpList.GetHma(lookbackPeriods) .Select(x => new MaEnvelopeResult { Timestamp = x.Timestamp, @@ -99,11 +106,12 @@ private static IEnumerable MaEnvHma( LowerEnvelope = x.Hma - (x.Hma * offsetRatio) }); - private static IEnumerable MaEnvSma( - this List<(DateTime, double)> tpList, + private static IEnumerable MaEnvSma( + this List tpList, int lookbackPeriods, double offsetRatio) - => tpList.GetSma(lookbackPeriods) + where T : IReusableResult + => tpList.GetSma(lookbackPeriods) .Select(x => new MaEnvelopeResult { Timestamp = x.Timestamp, Centerline = x.Sma, @@ -111,10 +119,11 @@ private static IEnumerable MaEnvSma( LowerEnvelope = x.Sma - (x.Sma * offsetRatio) }); - private static IEnumerable MaEnvSmma( - this List<(DateTime, double)> tpList, + private static IEnumerable MaEnvSmma( + this List tpList, int lookbackPeriods, double offsetRatio) + where T : IReusableResult => tpList.GetSmma(lookbackPeriods) .Select(x => new MaEnvelopeResult { Timestamp = x.Timestamp, @@ -123,10 +132,11 @@ private static IEnumerable MaEnvSmma( LowerEnvelope = x.Smma - (x.Smma * offsetRatio) }); - private static IEnumerable MaEnvTema( - this List<(DateTime, double)> tpList, + private static IEnumerable MaEnvTema( + this List tpList, int lookbackPeriods, double offsetRatio) + where T : IReusableResult => tpList.GetTema(lookbackPeriods) .Select(x => new MaEnvelopeResult { Timestamp = x.Timestamp, @@ -135,10 +145,11 @@ private static IEnumerable MaEnvTema( LowerEnvelope = x.Tema - (x.Tema * offsetRatio) }); - private static IEnumerable MaEnvWma( - this List<(DateTime, double)> tpList, + private static IEnumerable MaEnvWma( + this List tpList, int lookbackPeriods, double offsetRatio) + where T : IReusableResult => tpList.GetWma(lookbackPeriods) .Select(x => new MaEnvelopeResult { Timestamp = x.Timestamp, diff --git a/src/m-r/Macd/Macd.Models.cs b/src/m-r/Macd/Macd.Models.cs index f92e32365..0935b623c 100644 --- a/src/m-r/Macd/Macd.Models.cs +++ b/src/m-r/Macd/Macd.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class MacdResult : IReusableResult +public record struct MacdResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Macd { get; set; } @@ -11,5 +11,6 @@ public sealed record class MacdResult : IReusableResult public double? FastEma { get; set; } public double? SlowEma { get; set; } - double IReusableResult.Value => Macd.Null2NaN(); + readonly double IReusableResult.Value + => Macd.Null2NaN(); } diff --git a/src/m-r/Macd/MacdApi.cs b/src/m-r/Macd/MacdApi.cs index 7d24fcdb9..8cf7c2bde 100644 --- a/src/m-r/Macd/MacdApi.cs +++ b/src/m-r/Macd/MacdApi.cs @@ -3,24 +3,14 @@ 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, + public static IEnumerable GetMacd( + this IEnumerable results, int fastPeriods = 12, int slowPeriods = 26, - int signalPeriods = 9) => results + int signalPeriods = 9) + where T : IReusableResult + => results .ToTupleResult() .CalcMacd(fastPeriods, slowPeriods, signalPeriods); diff --git a/src/m-r/Mama/Mama.Api.cs b/src/m-r/Mama/Mama.Api.cs index bf22e113f..f1f042f98 100644 --- a/src/m-r/Mama/Mama.Api.cs +++ b/src/m-r/Mama/Mama.Api.cs @@ -3,22 +3,13 @@ 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, + public static IEnumerable GetMama( + this IEnumerable results, double fastLimit = 0.5, - double slowLimit = 0.05) => results + double slowLimit = 0.05) + where T : IReusableResult + => results .ToTupleResult() .CalcMama(fastLimit, slowLimit); diff --git a/src/m-r/Mama/Mama.Models.cs b/src/m-r/Mama/Mama.Models.cs index 6a853cd48..d93b501a3 100644 --- a/src/m-r/Mama/Mama.Models.cs +++ b/src/m-r/Mama/Mama.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class MamaResult : IReusableResult +public record struct MamaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Mama { get; set; } public double? Fama { get; set; } - double IReusableResult.Value => Mama.Null2NaN(); + readonly double IReusableResult.Value + => Mama.Null2NaN(); } diff --git a/src/m-r/Mfi/Mfi.Models.cs b/src/m-r/Mfi/Mfi.Models.cs index 6197e9a55..07debc3c9 100644 --- a/src/m-r/Mfi/Mfi.Models.cs +++ b/src/m-r/Mfi/Mfi.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class MfiResult : IReusableResult +public record struct MfiResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Mfi { get; set; } - double IReusableResult.Value => Mfi.Null2NaN(); + readonly double IReusableResult.Value + => Mfi.Null2NaN(); } diff --git a/src/m-r/Obv/Obv.Models.cs b/src/m-r/Obv/Obv.Models.cs index 92d8219bd..812dddcc2 100644 --- a/src/m-r/Obv/Obv.Models.cs +++ b/src/m-r/Obv/Obv.Models.cs @@ -1,12 +1,9 @@ namespace Skender.Stock.Indicators; -public sealed record class ObvResult : IReusableResult +public record struct ObvResult : IReusableResult { public DateTime Timestamp { get; set; } public double Obv { get; set; } - [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average.", false)] - public double? ObvSma { get; set; } - - double IReusableResult.Value => Obv; + readonly double IReusableResult.Value => Obv; } diff --git a/src/m-r/ParabolicSar/ParabolicSar.Models.cs b/src/m-r/ParabolicSar/ParabolicSar.Models.cs index 78dd2f1c1..c97b8597e 100644 --- a/src/m-r/ParabolicSar/ParabolicSar.Models.cs +++ b/src/m-r/ParabolicSar/ParabolicSar.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class ParabolicSarResult : IReusableResult +public record struct ParabolicSarResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Sar { get; set; } public bool? IsReversal { get; set; } - double IReusableResult.Value => Sar.Null2NaN(); + readonly double IReusableResult.Value + => Sar.Null2NaN(); } diff --git a/src/m-r/ParabolicSar/ParabolicSar.Series.cs b/src/m-r/ParabolicSar/ParabolicSar.Series.cs index 115df4378..0da12b9a4 100644 --- a/src/m-r/ParabolicSar/ParabolicSar.Series.cs +++ b/src/m-r/ParabolicSar/ParabolicSar.Series.cs @@ -142,14 +142,7 @@ double sar } // remove first trendline since it is an invalid guess - ParabolicSarResult? firstReversal = results - .Where(x => x.IsReversal == true) - .OrderBy(x => x.Timestamp) - .FirstOrDefault(); - - int cutIndex = (firstReversal != null) - ? results.IndexOf(firstReversal) - : length - 1; + int cutIndex = results.FindIndex(x => x.IsReversal is true); for (int d = 0; d <= cutIndex; d++) { diff --git a/src/m-r/PivotPoints/PivotPoints.Models.cs b/src/m-r/PivotPoints/PivotPoints.Models.cs index be5ce7c37..9bc96498d 100644 --- a/src/m-r/PivotPoints/PivotPoints.Models.cs +++ b/src/m-r/PivotPoints/PivotPoints.Models.cs @@ -13,7 +13,7 @@ internal interface IPivotPoint public decimal? S4 { get; set; } } -public sealed record class PivotPointsResult : IResult, IPivotPoint +public record struct PivotPointsResult : IResult, IPivotPoint { public DateTime Timestamp { get; set; } public decimal? R4 { get; set; } diff --git a/src/m-r/PivotPoints/PivotPoints.Series.cs b/src/m-r/PivotPoints/PivotPoints.Series.cs index 404dca086..9e07dbf2b 100644 --- a/src/m-r/PivotPoints/PivotPoints.Series.cs +++ b/src/m-r/PivotPoints/PivotPoints.Series.cs @@ -13,7 +13,7 @@ internal static List CalcPivotPoints( // initialize int length = quotesList.Count; List results = new(length); - PivotPointsResult? windowPoint = new(); + PivotPointsResult windowPoint = new(); TQuote h0; if (length == 0) @@ -71,19 +71,19 @@ internal static List CalcPivotPoints( if (!firstWindow) { // pivot point - r.PP = windowPoint?.PP; + r.PP = windowPoint.PP; // support - r.S1 = windowPoint?.S1; - r.S2 = windowPoint?.S2; - r.S3 = windowPoint?.S3; - r.S4 = windowPoint?.S4; + 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; + r.R1 = windowPoint.R1; + r.R2 = windowPoint.R2; + r.R3 = windowPoint.R3; + r.R4 = windowPoint.R4; } results.Add(r); diff --git a/src/m-r/Pmo/Pmo.Api.cs b/src/m-r/Pmo/Pmo.Api.cs index 8f7ef56a8..cb55bc10c 100644 --- a/src/m-r/Pmo/Pmo.Api.cs +++ b/src/m-r/Pmo/Pmo.Api.cs @@ -3,24 +3,14 @@ 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, + public static IEnumerable GetPmo( + this IEnumerable results, int timePeriods = 35, int smoothPeriods = 20, - int signalPeriods = 10) => results + int signalPeriods = 10) + where T : IReusableResult + => results .ToTupleResult() .CalcPmo(timePeriods, smoothPeriods, signalPeriods); diff --git a/src/m-r/Pmo/Pmo.Models.cs b/src/m-r/Pmo/Pmo.Models.cs index 538542838..f7faf1b28 100644 --- a/src/m-r/Pmo/Pmo.Models.cs +++ b/src/m-r/Pmo/Pmo.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class PmoResult : IReusableResult +public record struct PmoResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Pmo { get; set; } public double? Signal { get; set; } - double IReusableResult.Value => Pmo.Null2NaN(); + readonly double IReusableResult.Value + => Pmo.Null2NaN(); } diff --git a/src/m-r/Pmo/Pmo.Series.cs b/src/m-r/Pmo/Pmo.Series.cs index 57a645157..c686a9ad8 100644 --- a/src/m-r/Pmo/Pmo.Series.cs +++ b/src/m-r/Pmo/Pmo.Series.cs @@ -95,7 +95,7 @@ internal static List CalcPmo( } else { - signal = Ema.Increment(smoothingConstant3, prevSignal, pm[i]); + signal = EmaUtilities.Increment(smoothingConstant3, prevSignal, pm[i]); } prevSignal = signal; diff --git a/src/m-r/Prs/Prs.Api.cs b/src/m-r/Prs/Prs.Api.cs index 1c2edf960..b71671d99 100644 --- a/src/m-r/Prs/Prs.Api.cs +++ b/src/m-r/Prs/Prs.Api.cs @@ -3,28 +3,12 @@ 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) - where TQuote : IQuote - { - List<(DateTime, double)> tpListBase = quotesBase - .ToTuple(CandlePart.Close); - List<(DateTime, double)> tpListEval = quotesEval - .ToTuple(CandlePart.Close); - - return CalcPrs(tpListEval, tpListBase, lookbackPeriods); - } - // SERIES, from CHAINS (both inputs reusable) - public static IEnumerable GetPrs( - this IEnumerable quotesEval, - IEnumerable quotesBase, + public static IEnumerable GetPrs( + this IEnumerable quotesEval, + IEnumerable quotesBase, int? lookbackPeriods = null) + where T : IReusableResult { List<(DateTime Timestamp, double Value)> tpListEval = quotesEval.ToTupleResult(); diff --git a/src/m-r/Prs/Prs.Models.cs b/src/m-r/Prs/Prs.Models.cs index a56cff843..a7bdd0edc 100644 --- a/src/m-r/Prs/Prs.Models.cs +++ b/src/m-r/Prs/Prs.Models.cs @@ -1,13 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class PrsResult : IReusableResult +public record struct PrsResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Prs { get; set; } public double? PrsPercent { get; set; } - [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average signal.", false)] - public double? PrsSma { get; set; } - - double IReusableResult.Value => Prs.Null2NaN(); + readonly double IReusableResult.Value + => Prs.Null2NaN(); } diff --git a/src/m-r/Pvo/Pvo.Models.cs b/src/m-r/Pvo/Pvo.Models.cs index 4fe54754f..74f133262 100644 --- a/src/m-r/Pvo/Pvo.Models.cs +++ b/src/m-r/Pvo/Pvo.Models.cs @@ -1,11 +1,12 @@ namespace Skender.Stock.Indicators; -public sealed record class PvoResult : IReusableResult +public record struct PvoResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Pvo { get; set; } public double? Signal { get; set; } public double? Histogram { get; set; } - double IReusableResult.Value => Pvo.Null2NaN(); + readonly double IReusableResult.Value + => Pvo.Null2NaN(); } diff --git a/src/m-r/Renko/Renko.Models.cs b/src/m-r/Renko/Renko.Models.cs index 51c7fb1c9..8eb736891 100644 --- a/src/m-r/Renko/Renko.Models.cs +++ b/src/m-r/Renko/Renko.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class RenkoResult : IResult, IQuote +public record struct RenkoResult : IQuote, IReusableResult { public DateTime Timestamp { get; set; } public decimal Open { get; set; } @@ -11,6 +11,6 @@ public sealed record class RenkoResult : IResult, IQuote public bool IsUp { get; set; } - public bool Equals(IQuote? other) - => base.Equals(other); + readonly double IReusableResult.Value + => (double)Close; } diff --git a/src/m-r/Renko/RenkoAtr.Series.cs b/src/m-r/Renko/RenkoAtr.Series.cs index ff57c9ea8..245e57444 100644 --- a/src/m-r/Renko/RenkoAtr.Series.cs +++ b/src/m-r/Renko/RenkoAtr.Series.cs @@ -15,11 +15,11 @@ internal static List CalcRenkoAtr( .ToQuoteD() .CalcAtr(atrPeriods); - double? atr = atrResults.LastOrDefault()?.Atr; - decimal brickSize = (atr == null) ? 0 : (decimal)atr; + AtrResult last = atrResults.LastOrDefault(); + decimal brickSize = (decimal?)last.Atr ?? 0; - return brickSize is 0 ? - [] + return brickSize is 0 + ? [] : quotesList.CalcRenko(brickSize, endType); } } diff --git a/src/m-r/Roc/Roc.Api.cs b/src/m-r/Roc/Roc.Api.cs index 62a40e147..c5e0cf7a5 100644 --- a/src/m-r/Roc/Roc.Api.cs +++ b/src/m-r/Roc/Roc.Api.cs @@ -3,20 +3,12 @@ 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) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcRoc(lookbackPeriods); - // SERIES, from CHAIN - public static IEnumerable GetRoc( - this IEnumerable results, - int lookbackPeriods) => results + public static IEnumerable GetRoc( + this IEnumerable results, + int lookbackPeriods) + where T : IReusableResult + => results .ToTupleResult() .CalcRoc(lookbackPeriods); diff --git a/src/m-r/Roc/Roc.Models.cs b/src/m-r/Roc/Roc.Models.cs index 241d2d3f1..126df94a4 100644 --- a/src/m-r/Roc/Roc.Models.cs +++ b/src/m-r/Roc/Roc.Models.cs @@ -1,13 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class RocResult : IReusableResult +public record struct RocResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Momentum { get; set; } public double? Roc { get; set; } - [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average signal.", false)] - public double? RocSma { get; set; } - - double IReusableResult.Value => Roc.Null2NaN(); + readonly double IReusableResult.Value + => Roc.Null2NaN(); } diff --git a/src/m-r/RocWb/RocWb.Api.cs b/src/m-r/RocWb/RocWb.Api.cs index 1e0a85bcd..ac8f4bc76 100644 --- a/src/m-r/RocWb/RocWb.Api.cs +++ b/src/m-r/RocWb/RocWb.Api.cs @@ -3,24 +3,14 @@ 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, + public static IEnumerable GetRocWb( + this IEnumerable results, int lookbackPeriods, int emaPeriods, - int stdDevPeriods) => results + int stdDevPeriods) + where T : IReusableResult + => results .ToTupleResult() .CalcRocWb(lookbackPeriods, emaPeriods, stdDevPeriods); diff --git a/src/m-r/RocWb/RocWb.Models.cs b/src/m-r/RocWb/RocWb.Models.cs index 153335050..a93c9c67b 100644 --- a/src/m-r/RocWb/RocWb.Models.cs +++ b/src/m-r/RocWb/RocWb.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class RocWbResult : IReusableResult +public record struct RocWbResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Roc { get; set; } @@ -8,5 +8,6 @@ public sealed record class RocWbResult : IReusableResult public double? UpperBand { get; set; } public double? LowerBand { get; set; } - double IReusableResult.Value => Roc.Null2NaN(); + readonly double IReusableResult.Value + => Roc.Null2NaN(); } diff --git a/src/m-r/RocWb/RocWb.Series.cs b/src/m-r/RocWb/RocWb.Series.cs index fd21c5df7..7b3d703d6 100644 --- a/src/m-r/RocWb/RocWb.Series.cs +++ b/src/m-r/RocWb/RocWb.Series.cs @@ -54,7 +54,7 @@ internal static List CalcRocWb( // normal EMA else { - ema[i] = Ema.Increment(k, prevEma, roc); + ema[i] = EmaUtilities.Increment(k, prevEma, roc); } r.RocEma = ema[i].NaN2Null(); diff --git a/src/m-r/RollingPivots/RollingPivots.Models.cs b/src/m-r/RollingPivots/RollingPivots.Models.cs index 44a472e6c..8bf65e44a 100644 --- a/src/m-r/RollingPivots/RollingPivots.Models.cs +++ b/src/m-r/RollingPivots/RollingPivots.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class RollingPivotsResult : IResult, IPivotPoint +public record struct RollingPivotsResult : IResult, IPivotPoint { public DateTime Timestamp { get; set; } public decimal? R4 { get; set; } diff --git a/src/m-r/Rsi/Rsi.Api.cs b/src/m-r/Rsi/Rsi.Api.cs index 96825d4c8..4289e8fab 100644 --- a/src/m-r/Rsi/Rsi.Api.cs +++ b/src/m-r/Rsi/Rsi.Api.cs @@ -3,20 +3,12 @@ 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 + public static IEnumerable GetRsi( + this IEnumerable results, + int lookbackPeriods = 14) + where T : IReusableResult + => results .ToTupleResult() .CalcRsi(lookbackPeriods); diff --git a/src/m-r/Rsi/Rsi.Models.cs b/src/m-r/Rsi/Rsi.Models.cs index e8f50f4fd..95e874945 100644 --- a/src/m-r/Rsi/Rsi.Models.cs +++ b/src/m-r/Rsi/Rsi.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class RsiResult : IReusableResult +public record struct RsiResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Rsi { get; set; } - double IReusableResult.Value => Rsi.Null2NaN(); + readonly double IReusableResult.Value + => Rsi.Null2NaN(); } diff --git a/src/s-z/Slope/Slope.Api.cs b/src/s-z/Slope/Slope.Api.cs index e47a8e748..e82e0eea8 100644 --- a/src/s-z/Slope/Slope.Api.cs +++ b/src/s-z/Slope/Slope.Api.cs @@ -3,20 +3,22 @@ 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 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 + public static IEnumerable GetSlope( + this IEnumerable results, + int lookbackPeriods) + where T : IReusableResult + => results .ToTupleResult() .CalcSlope(lookbackPeriods); diff --git a/src/s-z/Slope/Slope.Models.cs b/src/s-z/Slope/Slope.Models.cs index 265c31bf3..3db6eacc7 100644 --- a/src/s-z/Slope/Slope.Models.cs +++ b/src/s-z/Slope/Slope.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class SlopeResult : IReusableResult +public record struct SlopeResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Slope { get; set; } @@ -9,5 +9,6 @@ public sealed record class SlopeResult : IReusableResult public double? RSquared { get; set; } public decimal? Line { get; set; } // last line segment only - double IReusableResult.Value => Slope.Null2NaN(); + readonly double IReusableResult.Value + => Slope.Null2NaN(); } diff --git a/src/s-z/Slope/Slope.Series.cs b/src/s-z/Slope/Slope.Series.cs index ded64f04a..037ccfe8b 100644 --- a/src/s-z/Slope/Slope.Series.cs +++ b/src/s-z/Slope/Slope.Series.cs @@ -80,11 +80,11 @@ internal static List CalcSlope( // add last Line (y = mx + b) if (length >= lookbackPeriods) { - SlopeResult? last = results.LastOrDefault(); + 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(); + d.Line = (decimal?)((last.Slope * (p + 1)) + last.Intercept).NaN2Null(); } } diff --git a/src/s-z/Sma/Sma.Analysis.cs b/src/s-z/Sma/Sma.Analysis.cs index d21491801..5a313f761 100644 --- a/src/s-z/Sma/Sma.Analysis.cs +++ b/src/s-z/Sma/Sma.Analysis.cs @@ -37,14 +37,18 @@ internal static IEnumerable CalcSmaAnalysis( : Math.Abs(value - sma) / value; } - // mean absolute deviation - r.Mad = (sumMad / lookbackPeriods).NaN2Null(); + results[i] = r with { - // mean squared error - r.Mse = (sumMse / lookbackPeriods).NaN2Null(); + // mean absolute deviation + Mad = (sumMad / lookbackPeriods).NaN2Null(), - // mean absolute percent error - r.Mape = (sumMape / 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/Sma/Sma.Api.cs b/src/s-z/Sma/Sma.Api.cs index f88758cdb..989be88a6 100644 --- a/src/s-z/Sma/Sma.Api.cs +++ b/src/s-z/Sma/Sma.Api.cs @@ -4,73 +4,28 @@ namespace Skender.Stock.Indicators; 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 + public static IEnumerable GetSma( + this IEnumerable results, + int lookbackPeriods) + where T : IReusableResult + => results .ToTupleResult() .CalcSma(lookbackPeriods); - // SERIES, from TUPLE - public static IEnumerable GetSma( - this IEnumerable<(DateTime, double)> priceTuples, - int lookbackPeriods) => priceTuples - .ToSortedList() - .CalcSma(lookbackPeriods); - - // OBSERVER, from Quote Provider - /// - /// - public static Sma AttachSma( - this QuoteProvider quoteProvider, + // ANALYSIS, from CHAIN + public static IEnumerable GetSmaAnalysis( + this IEnumerable results, int lookbackPeriods) - where TQuote : IQuote, new() - { - Use chainProvider = quoteProvider - .Use(CandlePart.Close); - - return new(chainProvider, lookbackPeriods); - } + where T : IReusableResult + => results + .ToTupleResult() + .CalcSmaAnalysis(lookbackPeriods); // OBSERVER, from Chain Provider - /// - /// - public static Sma AttachSma( - this ChainProvider chainProvider, + public static Sma ToSma( + this IChainProvider chainProvider, int lookbackPeriods) + where TIn : struct, IReusableResult => new(chainProvider, 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 - .ToTupleResult() - .CalcSmaAnalysis(lookbackPeriods); - - // 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.Common.cs b/src/s-z/Sma/Sma.Common.cs index 39c359152..53a536e0a 100644 --- a/src/s-z/Sma/Sma.Common.cs +++ b/src/s-z/Sma/Sma.Common.cs @@ -5,12 +5,8 @@ namespace Skender.Stock.Indicators; /// See the /// Stock Indicators for .NET online guide for more information. -public partial class Sma +public static class SmaUtilities { - // string label - public override string ToString() - => $"SMA({LookbackPeriods})"; - // parameter validation internal static void Validate( int lookbackPeriods) diff --git a/src/s-z/Sma/Sma.Models.cs b/src/s-z/Sma/Sma.Models.cs index 6db6208f6..ceb6cad07 100644 --- a/src/s-z/Sma/Sma.Models.cs +++ b/src/s-z/Sma/Sma.Models.cs @@ -1,27 +1,35 @@ namespace Skender.Stock.Indicators; -public sealed record class SmaResult : IReusableResult +public record struct SmaResult( + DateTime Timestamp, + double? Sma = default) + : IReusableResult { - public DateTime Timestamp { get; set; } - public double? Sma { get; set; } - - double IReusableResult.Value => Sma.Null2NaN(); + readonly double IReusableResult.Value + => Sma.Null2NaN(); } -public sealed record class SmaAnalysis : IReusableResult +/// +/// SMA with extended analysis. +/// +/// Timestamp +/// Simple moving average +/// Mean absolute deviation +/// Mean square error +/// Mean absolute percentage error +public record struct SmaAnalysis( + DateTime Timestamp, + double? Sma, + double? Mad, + double? Mse, + double? Mape) + : IReusableResult { - public DateTime Timestamp { get; set; } - 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.Null2NaN(); + readonly double IReusableResult.Value + => Sma.Null2NaN(); } -public interface ISma : - IChainObserver, - IChainProvider +public interface ISma : IStreamObserver { int LookbackPeriods { get; } } diff --git a/src/s-z/Sma/Sma.Series.cs b/src/s-z/Sma/Sma.Series.cs index 55ef4a417..bb1c044ff 100644 --- a/src/s-z/Sma/Sma.Series.cs +++ b/src/s-z/Sma/Sma.Series.cs @@ -4,12 +4,15 @@ namespace Skender.Stock.Indicators; public static partial class Indicator { + // TODO: discontinue converting to Tuple, + // everywhere. It's unneeded overhead; use IReusableResult. + internal static List CalcSma( this List<(DateTime, double)> tpList, int lookbackPeriods) { // check parameter arguments - Sma.Validate(lookbackPeriods); + SmaUtilities.Validate(lookbackPeriods); // initialize List results = new(tpList.Count); @@ -19,8 +22,7 @@ internal static List CalcSma( { (DateTime date, double _) = tpList[i]; - SmaResult result = new() { Timestamp = date }; - results.Add(result); + double sma; if (i >= lookbackPeriods - 1) { @@ -31,8 +33,18 @@ internal static List CalcSma( sumSma += pValue; } - result.Sma = (sumSma / lookbackPeriods).NaN2Null(); + sma = sumSma / lookbackPeriods; + } + else + { + sma = double.NaN; } + + SmaResult result = new( + Timestamp: date, + Sma: sma.NaN2Null()); + + results.Add(result); } return results; diff --git a/src/s-z/Sma/Sma.Stream.cs b/src/s-z/Sma/Sma.Stream.cs index 9808d9f38..57e001c5c 100644 --- a/src/s-z/Sma/Sma.Stream.cs +++ b/src/s-z/Sma/Sma.Stream.cs @@ -2,22 +2,23 @@ namespace Skender.Stock.Indicators; // SIMPLE MOVING AVERAGE (STREAMING) -public partial class Sma : ChainObserver, ISma +public partial class Sma : AbstractChainInChainOut, ISma + where TIn : struct, IReusableResult { // constructor public Sma( - ChainProvider provider, + IChainProvider provider, int lookbackPeriods) - : base(provider, isChainor: true) + : base(provider) { - Validate(lookbackPeriods); + SmaUtilities.Validate(lookbackPeriods); LookbackPeriods = lookbackPeriods; RebuildCache(); // subscribe to chain provider - unsubscriber = provider != null + Subscription = provider != null ? provider.Subscribe(this) : throw new ArgumentNullException(nameof(provider)); } @@ -28,18 +29,20 @@ public Sma( // METHODS + // string label + public override string ToString() + => $"SMA({LookbackPeriods})"; + // handle chain arrival - public override void OnNext((Act act, DateTime date, double price) value) + internal override void OnNextArrival(Act act, IReusableResult inbound) { int i; double sma; - List<(DateTime _, double value)> supplier = ChainSupplier.Chain; - // handle deletes - if (value.act == Act.Delete) + if (act == Act.Delete) { - i = Cache.FindIndex(value.date); + i = Cache.FindIndex(inbound.Timestamp); sma = Cache[i].Sma.Null2NaN(); } @@ -47,7 +50,7 @@ public override void OnNext((Act act, DateTime date, double price) value) else { // calculate incremental value - i = supplier.FindIndex(value.date); + i = Provider.FindIndex(inbound.Timestamp); // source unexpectedly not found if (i == -1) @@ -62,7 +65,7 @@ public override void OnNext((Act act, DateTime date, double price) value) double sum = 0; for (int w = i - LookbackPeriods + 1; w <= i; w++) { - sum += supplier[w].value; + sum += ProviderCache[w].Value; } sma = sum / LookbackPeriods; @@ -76,39 +79,22 @@ public override void OnNext((Act act, DateTime date, double price) value) } // candidate result - SmaResult r = new() { - Timestamp = value.date, - Sma = sma.NaN2Null() - }; + SmaResult r = new( + Timestamp: inbound.Timestamp, + Sma: sma.NaN2Null()); // save to cache - Act act = CacheChainorPerAction(value.act, r, sma); + act = ModifyCache(act, r); // send to observers NotifyObservers(act, r); - // update forward values - if (act != Act.AddNew && i < supplier.Count - 1) + // cascade update forward values (recursively) + if (act != Act.AddNew && i < ProviderCache.Count - 1) { - // cascade updates gracefully int next = act == Act.Delete ? i : i + 1; - (DateTime d, double v) = supplier[next]; - OnNext((Act.Update, d, v)); - } - } - - // delete cache between index values - // usually called from inherited ClearCache(fromDate) - internal override void ClearCache(int fromIndex, int toIndex) - { - // delete and deliver instruction, - // in reverse order to prevent recompositions - - for (int i = toIndex; i >= fromIndex; i--) - { - SmaResult r = Cache[i]; - Act act = CacheChainorPerAction(Act.Delete, r, double.NaN); - NotifyObservers(act, r); + TIn value = ProviderCache[next]; + OnNextArrival(Act.Update, value); } } } diff --git a/src/s-z/Sma/info.xml b/src/s-z/Sma/info.xml index cc6182444..2e257ef90 100644 --- a/src/s-z/Sma/info.xml +++ b/src/s-z/Sma/info.xml @@ -18,36 +18,6 @@ Invalid parameter value provided. - - - Establish an observable streaming Simple Moving Average (SMA) from a Quote provider. - - See - documentation - for more information. - - - Observable quote provider. - Number of periods in the lookback window. - Observable SMA instance. - Invalid parameter value provided. - - - - - Establish an observable streaming Simple Moving Average (SMA) from a Chain provider. - - See - documentation - for more information. - - - Observable source 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). @@ -64,4 +34,4 @@ Invalid parameter value provided. - \ No newline at end of file + diff --git a/src/s-z/Smi/Smi.Models.cs b/src/s-z/Smi/Smi.Models.cs index 668abee06..b8f86fbfa 100644 --- a/src/s-z/Smi/Smi.Models.cs +++ b/src/s-z/Smi/Smi.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class SmiResult : IReusableResult +public record struct SmiResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Smi { get; set; } public double? Signal { get; set; } - double IReusableResult.Value => Smi.Null2NaN(); + readonly double IReusableResult.Value + => Smi.Null2NaN(); } diff --git a/src/s-z/Smma/Smma.Api.cs b/src/s-z/Smma/Smma.Api.cs index 5d95491a2..328bc8220 100644 --- a/src/s-z/Smma/Smma.Api.cs +++ b/src/s-z/Smma/Smma.Api.cs @@ -3,20 +3,12 @@ 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 + public static IEnumerable GetSmma( + this IEnumerable results, + int lookbackPeriods) + where T : IReusableResult + => results .ToTupleResult() .CalcSmma(lookbackPeriods); diff --git a/src/s-z/Smma/Smma.Models.cs b/src/s-z/Smma/Smma.Models.cs index c70601329..2feb80411 100644 --- a/src/s-z/Smma/Smma.Models.cs +++ b/src/s-z/Smma/Smma.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class SmmaResult : IReusableResult +public record struct SmmaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Smma { get; set; } - double IReusableResult.Value => Smma.Null2NaN(); + readonly double IReusableResult.Value + => Smma.Null2NaN(); } diff --git a/src/s-z/StarcBands/StarcBands.Models.cs b/src/s-z/StarcBands/StarcBands.Models.cs index 515eb211a..b8bc2e285 100644 --- a/src/s-z/StarcBands/StarcBands.Models.cs +++ b/src/s-z/StarcBands/StarcBands.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class StarcBandsResult : IResult +public record struct StarcBandsResult : IResult { public DateTime Timestamp { get; set; } public double? UpperBand { get; set; } diff --git a/src/s-z/Stc/Stc.Api.cs b/src/s-z/Stc/Stc.Api.cs index 0932fcaa9..b8b5a9c46 100644 --- a/src/s-z/Stc/Stc.Api.cs +++ b/src/s-z/Stc/Stc.Api.cs @@ -3,24 +3,14 @@ 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, + public static IEnumerable GetStc( + this IEnumerable results, int cyclePeriods = 10, int fastPeriods = 23, - int slowPeriods = 50) => results + int slowPeriods = 50) + where T: IReusableResult + => results .ToTupleResult() .CalcStc(cyclePeriods, fastPeriods, slowPeriods); diff --git a/src/s-z/Stc/Stc.Models.cs b/src/s-z/Stc/Stc.Models.cs index fea317b5c..bf8b56f29 100644 --- a/src/s-z/Stc/Stc.Models.cs +++ b/src/s-z/Stc/Stc.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class StcResult : IReusableResult +public record struct StcResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Stc { get; set; } - double IReusableResult.Value => Stc.Null2NaN(); + readonly double IReusableResult.Value + => Stc.Null2NaN(); } diff --git a/src/s-z/StdDev/StdDev.Api.cs b/src/s-z/StdDev/StdDev.Api.cs index b73ffc4cc..08f619e1d 100644 --- a/src/s-z/StdDev/StdDev.Api.cs +++ b/src/s-z/StdDev/StdDev.Api.cs @@ -3,20 +3,12 @@ namespace Skender.Stock.Indicators; // STANDARD DEVIATION (API) public static partial class Indicator { - // SERIES, from TQuote - /// - /// - public static IEnumerable GetStdDev( - this IEnumerable quotes, - int lookbackPeriods) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcStdDev(lookbackPeriods); - // SERIES, from CHAIN - public static IEnumerable GetStdDev( - this IEnumerable results, - int lookbackPeriods) => results + public static IEnumerable GetStdDev( + this IEnumerable results, + int lookbackPeriods) + where T: IReusableResult + => results .ToTupleResult() .CalcStdDev(lookbackPeriods); diff --git a/src/s-z/StdDev/StdDev.Models.cs b/src/s-z/StdDev/StdDev.Models.cs index a38d03687..2b9d9abc1 100644 --- a/src/s-z/StdDev/StdDev.Models.cs +++ b/src/s-z/StdDev/StdDev.Models.cs @@ -1,14 +1,12 @@ namespace Skender.Stock.Indicators; -public sealed record class StdDevResult : IReusableResult +public record struct StdDevResult : IReusableResult { public DateTime Timestamp { get; set; } public double? StdDev { get; set; } public double? Mean { get; set; } public double? ZScore { get; set; } - [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average signal.", false)] - public double? StdDevSma { get; set; } - - double IReusableResult.Value => StdDev.Null2NaN(); + readonly double IReusableResult.Value + => StdDev.Null2NaN(); } diff --git a/src/s-z/StdDevChannels/StdDevChannels.Api.cs b/src/s-z/StdDevChannels/StdDevChannels.Api.cs index d780c2885..9249f39e2 100644 --- a/src/s-z/StdDevChannels/StdDevChannels.Api.cs +++ b/src/s-z/StdDevChannels/StdDevChannels.Api.cs @@ -3,22 +3,13 @@ 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, + public static IEnumerable GetStdDevChannels( + this IEnumerable results, int? lookbackPeriods = 20, - double stdDeviations = 2) => results + double stdDeviations = 2) + where T: IReusableResult + => results .ToTupleResult() .CalcStdDevChannels(lookbackPeriods, stdDeviations); diff --git a/src/s-z/StdDevChannels/StdDevChannels.Models.cs b/src/s-z/StdDevChannels/StdDevChannels.Models.cs index 5acf3bb5b..5e6c9c5dd 100644 --- a/src/s-z/StdDevChannels/StdDevChannels.Models.cs +++ b/src/s-z/StdDevChannels/StdDevChannels.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class StdDevChannelsResult : IResult +public record struct StdDevChannelsResult : IResult { public DateTime Timestamp { get; set; } public double? Centerline { get; set; } diff --git a/src/s-z/Stoch/Stoch.Models.cs b/src/s-z/Stoch/Stoch.Models.cs index 9590b3f0a..f51d389de 100644 --- a/src/s-z/Stoch/Stoch.Models.cs +++ b/src/s-z/Stoch/Stoch.Models.cs @@ -2,7 +2,7 @@ namespace Skender.Stock.Indicators; /// /// -public sealed record class StochResult : IReusableResult +public record struct StochResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Oscillator { get; set; } @@ -10,9 +10,10 @@ public sealed record class StochResult : IReusableResult public double? PercentJ { get; set; } // aliases - public double? K => Oscillator; - public double? D => Signal; - public double? J => PercentJ; + public readonly double? K => Oscillator; + public readonly double? D => Signal; + public readonly double? J => PercentJ; - double IReusableResult.Value => Oscillator.Null2NaN(); + readonly double IReusableResult.Value + => Oscillator.Null2NaN(); } diff --git a/src/s-z/StochRsi/StochRsi.Api.cs b/src/s-z/StochRsi/StochRsi.Api.cs index 0cac816cf..02c09bcec 100644 --- a/src/s-z/StochRsi/StochRsi.Api.cs +++ b/src/s-z/StochRsi/StochRsi.Api.cs @@ -3,30 +3,15 @@ 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, + public static IEnumerable GetStochRsi( + this IEnumerable results, int rsiPeriods, int stochPeriods, int signalPeriods, - int smoothPeriods) => results + int smoothPeriods = 1) + where T:IReusableResult + => results .ToTupleResult() .CalcStochRsi( rsiPeriods, diff --git a/src/s-z/StochRsi/StochRsi.Models.cs b/src/s-z/StochRsi/StochRsi.Models.cs index cb45145fd..ca44d1d4e 100644 --- a/src/s-z/StochRsi/StochRsi.Models.cs +++ b/src/s-z/StochRsi/StochRsi.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class StochRsiResult : IReusableResult +public record struct StochRsiResult : IReusableResult { public DateTime Timestamp { get; set; } public double? StochRsi { get; set; } public double? Signal { get; set; } - double IReusableResult.Value => StochRsi.Null2NaN(); + readonly double IReusableResult.Value + => StochRsi.Null2NaN(); } diff --git a/src/s-z/SuperTrend/SuperTrend.Models.cs b/src/s-z/SuperTrend/SuperTrend.Models.cs index df95ad323..fa44437e0 100644 --- a/src/s-z/SuperTrend/SuperTrend.Models.cs +++ b/src/s-z/SuperTrend/SuperTrend.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class SuperTrendResult : IResult +public record struct SuperTrendResult : IResult { public DateTime Timestamp { get; set; } public decimal? SuperTrend { get; set; } diff --git a/src/s-z/T3/T3.Api.cs b/src/s-z/T3/T3.Api.cs index cc57b1bee..5f3f107a7 100644 --- a/src/s-z/T3/T3.Api.cs +++ b/src/s-z/T3/T3.Api.cs @@ -3,22 +3,13 @@ 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, + public static IEnumerable GetT3( + this IEnumerable results, int lookbackPeriods = 5, - double volumeFactor = 0.7) => results + double volumeFactor = 0.7) + where T:IReusableResult + => results .ToTupleResult() .CalcT3(lookbackPeriods, volumeFactor); diff --git a/src/s-z/T3/T3.Models.cs b/src/s-z/T3/T3.Models.cs index dfc2e9d1f..548fafec9 100644 --- a/src/s-z/T3/T3.Models.cs +++ b/src/s-z/T3/T3.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class T3Result : IReusableResult +public record struct T3Result : IReusableResult { public DateTime Timestamp { get; set; } public double? T3 { get; set; } - double IReusableResult.Value => T3.Null2NaN(); + readonly double IReusableResult.Value + => T3.Null2NaN(); } diff --git a/src/s-z/Tema/Tema.Api.cs b/src/s-z/Tema/Tema.Api.cs index b4972d3e4..e93b63a06 100644 --- a/src/s-z/Tema/Tema.Api.cs +++ b/src/s-z/Tema/Tema.Api.cs @@ -3,20 +3,12 @@ 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 + public static IEnumerable GetTema( + this IEnumerable results, + int lookbackPeriods) + where T: IReusableResult + => results .ToTupleResult() .CalcTema(lookbackPeriods); diff --git a/src/s-z/Tema/Tema.Models.cs b/src/s-z/Tema/Tema.Models.cs index a27355877..38113ee04 100644 --- a/src/s-z/Tema/Tema.Models.cs +++ b/src/s-z/Tema/Tema.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class TemaResult : IReusableResult +public record struct TemaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Tema { get; set; } - double IReusableResult.Value => Tema.Null2NaN(); + readonly double IReusableResult.Value + => Tema.Null2NaN(); } diff --git a/src/s-z/Tr/Tr.Models.cs b/src/s-z/Tr/Tr.Models.cs index 6ba75f67e..874fe8ae2 100644 --- a/src/s-z/Tr/Tr.Models.cs +++ b/src/s-z/Tr/Tr.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class TrResult : IReusableResult +public record struct TrResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Tr { get; set; } - double IReusableResult.Value => Tr.Null2NaN(); + readonly double IReusableResult.Value + => Tr.Null2NaN(); } diff --git a/src/s-z/Trix/Trix.Api.cs b/src/s-z/Trix/Trix.Api.cs index 898bec0df..31aa1a043 100644 --- a/src/s-z/Trix/Trix.Api.cs +++ b/src/s-z/Trix/Trix.Api.cs @@ -3,20 +3,12 @@ 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) - where TQuote : IQuote => quotes - .ToTuple(CandlePart.Close) - .CalcTrix(lookbackPeriods); - // SERIES, from CHAIN - public static IEnumerable GetTrix( - this IEnumerable results, - int lookbackPeriods) => results + public static IEnumerable GetTrix( + this IEnumerable results, + int lookbackPeriods) + where T: IReusableResult + => results .ToTupleResult() .CalcTrix(lookbackPeriods); // SERIES, from TUPLE diff --git a/src/s-z/Trix/Trix.Models.cs b/src/s-z/Trix/Trix.Models.cs index 662301d4e..f0dbd988d 100644 --- a/src/s-z/Trix/Trix.Models.cs +++ b/src/s-z/Trix/Trix.Models.cs @@ -1,13 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class TrixResult : IReusableResult +public record struct TrixResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Ema3 { get; set; } public double? Trix { get; set; } - [Obsolete("Use a chained `results.GetSma(smaPeriods)` to generate a moving average signal.", false)] - public double? Signal { get; set; } - - double IReusableResult.Value => Trix.Null2NaN(); + readonly double IReusableResult.Value + => Trix.Null2NaN(); } diff --git a/src/s-z/Tsi/Tsi.Api.cs b/src/s-z/Tsi/Tsi.Api.cs index 584f59dac..4d0fc3b72 100644 --- a/src/s-z/Tsi/Tsi.Api.cs +++ b/src/s-z/Tsi/Tsi.Api.cs @@ -3,24 +3,14 @@ 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, + public static IEnumerable GetTsi( + this IEnumerable results, int lookbackPeriods = 25, int smoothPeriods = 13, - int signalPeriods = 7) => results + int signalPeriods = 7) + where T : IReusableResult + => results .ToTupleResult() .CalcTsi(lookbackPeriods, smoothPeriods, signalPeriods); diff --git a/src/s-z/Tsi/Tsi.Models.cs b/src/s-z/Tsi/Tsi.Models.cs index 0d02cc4b9..b16eb19ad 100644 --- a/src/s-z/Tsi/Tsi.Models.cs +++ b/src/s-z/Tsi/Tsi.Models.cs @@ -1,10 +1,11 @@ namespace Skender.Stock.Indicators; -public sealed record class TsiResult : IReusableResult +public record struct TsiResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Tsi { get; set; } public double? Signal { get; set; } - double IReusableResult.Value => Tsi.Null2NaN(); + readonly double IReusableResult.Value + => Tsi.Null2NaN(); } diff --git a/src/s-z/UlcerIndex/UlcerIndex.Api.cs b/src/s-z/UlcerIndex/UlcerIndex.Api.cs index 88f04a5f0..dacef1605 100644 --- a/src/s-z/UlcerIndex/UlcerIndex.Api.cs +++ b/src/s-z/UlcerIndex/UlcerIndex.Api.cs @@ -3,20 +3,12 @@ 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 + public static IEnumerable GetUlcerIndex( + this IEnumerable results, + int lookbackPeriods = 14) + where T: IReusableResult + => results .ToTupleResult() .CalcUlcerIndex(lookbackPeriods); diff --git a/src/s-z/UlcerIndex/UlcerIndex.Models.cs b/src/s-z/UlcerIndex/UlcerIndex.Models.cs index 27a04997a..5794d55a4 100644 --- a/src/s-z/UlcerIndex/UlcerIndex.Models.cs +++ b/src/s-z/UlcerIndex/UlcerIndex.Models.cs @@ -1,12 +1,13 @@ namespace Skender.Stock.Indicators; -public sealed record class UlcerIndexResult : IReusableResult +public record struct UlcerIndexResult : IReusableResult { public DateTime Timestamp { get; set; } public double? UlcerIndex { get; set; } // ulcer index - double IReusableResult.Value => UlcerIndex.Null2NaN(); + readonly double IReusableResult.Value + => UlcerIndex.Null2NaN(); [Obsolete("Rename UI to UlcerIndex")] - public double? UI => UlcerIndex; + public readonly double? UI => UlcerIndex; } diff --git a/src/s-z/Ultimate/Ultimate.Models.cs b/src/s-z/Ultimate/Ultimate.Models.cs index cee70c029..ab55f2c1b 100644 --- a/src/s-z/Ultimate/Ultimate.Models.cs +++ b/src/s-z/Ultimate/Ultimate.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class UltimateResult : IReusableResult +public record struct UltimateResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Ultimate { get; set; } - double IReusableResult.Value => Ultimate.Null2NaN(); + readonly double IReusableResult.Value + => Ultimate.Null2NaN(); } diff --git a/src/s-z/VolatilityStop/VolatilityStop.Models.cs b/src/s-z/VolatilityStop/VolatilityStop.Models.cs index aa7001cf7..5cc05706a 100644 --- a/src/s-z/VolatilityStop/VolatilityStop.Models.cs +++ b/src/s-z/VolatilityStop/VolatilityStop.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class VolatilityStopResult : IReusableResult +public record struct VolatilityStopResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Sar { get; set; } @@ -10,5 +10,6 @@ public sealed record class VolatilityStopResult : IReusableResult public double? UpperBand { get; set; } public double? LowerBand { get; set; } - double IReusableResult.Value => Sar.Null2NaN(); + readonly double IReusableResult.Value + => Sar.Null2NaN(); } diff --git a/src/s-z/VolatilityStop/VolatilityStop.Series.cs b/src/s-z/VolatilityStop/VolatilityStop.Series.cs index c125e21b7..ae2d321d7 100644 --- a/src/s-z/VolatilityStop/VolatilityStop.Series.cs +++ b/src/s-z/VolatilityStop/VolatilityStop.Series.cs @@ -82,23 +82,15 @@ internal static List CalcVolatilityStop( } // remove first trend to stop, since it is a guess - VolatilityStopResult? firstStop = results - .Where(x => x.IsStop == true) - .OrderBy(x => x.Timestamp) - .FirstOrDefault(); + int cutIndex = results.FindIndex(x => x.IsStop is true); - if (firstStop != null) + for (int d = 0; d <= cutIndex; d++) { - 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; - } + VolatilityStopResult r = results[d]; + r.Sar = null; + r.UpperBand = null; + r.LowerBand = null; + r.IsStop = null; } return results; diff --git a/src/s-z/Vortex/Vortex.Models.cs b/src/s-z/Vortex/Vortex.Models.cs index 0375f34b4..fbfe1b34c 100644 --- a/src/s-z/Vortex/Vortex.Models.cs +++ b/src/s-z/Vortex/Vortex.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class VortexResult : IResult +public record struct VortexResult : IResult { public DateTime Timestamp { get; set; } public double? Pvi { get; set; } diff --git a/src/s-z/Vwap/Vwap.Models.cs b/src/s-z/Vwap/Vwap.Models.cs index 0e960248f..c9f8ad9f2 100644 --- a/src/s-z/Vwap/Vwap.Models.cs +++ b/src/s-z/Vwap/Vwap.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class VwapResult : IReusableResult +public record struct VwapResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Vwap { get; set; } - double IReusableResult.Value => Vwap.Null2NaN(); + readonly double IReusableResult.Value + => Vwap.Null2NaN(); } diff --git a/src/s-z/Vwma/Vwma.Models.cs b/src/s-z/Vwma/Vwma.Models.cs index 7070c2aa5..bcfb57f51 100644 --- a/src/s-z/Vwma/Vwma.Models.cs +++ b/src/s-z/Vwma/Vwma.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class VwmaResult : IReusableResult +public record struct VwmaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Vwma { get; set; } - double IReusableResult.Value => Vwma.Null2NaN(); + readonly double IReusableResult.Value + => Vwma.Null2NaN(); } diff --git a/src/s-z/WilliamsR/WilliamsR.Models.cs b/src/s-z/WilliamsR/WilliamsR.Models.cs index 5fd3b3698..70fd55b7b 100644 --- a/src/s-z/WilliamsR/WilliamsR.Models.cs +++ b/src/s-z/WilliamsR/WilliamsR.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class WilliamsResult : IReusableResult +public record struct WilliamsResult : IReusableResult { public DateTime Timestamp { get; set; } public double? WilliamsR { get; set; } - double IReusableResult.Value => WilliamsR.Null2NaN(); + readonly double IReusableResult.Value + => WilliamsR.Null2NaN(); } diff --git a/src/s-z/Wma/Wma.Api.cs b/src/s-z/Wma/Wma.Api.cs index 0daae09e9..90a1fbbd3 100644 --- a/src/s-z/Wma/Wma.Api.cs +++ b/src/s-z/Wma/Wma.Api.cs @@ -3,20 +3,12 @@ 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 + public static IEnumerable GetWma( + this IEnumerable results, + int lookbackPeriods) + where T : IReusableResult + => results .ToTupleResult() .CalcWma(lookbackPeriods); diff --git a/src/s-z/Wma/Wma.Models.cs b/src/s-z/Wma/Wma.Models.cs index 99485500a..59ef46ea8 100644 --- a/src/s-z/Wma/Wma.Models.cs +++ b/src/s-z/Wma/Wma.Models.cs @@ -1,9 +1,10 @@ namespace Skender.Stock.Indicators; -public sealed record class WmaResult : IReusableResult +public record struct WmaResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Wma { get; set; } - double IReusableResult.Value => Wma.Null2NaN(); + readonly double IReusableResult.Value + => Wma.Null2NaN(); } diff --git a/src/s-z/ZigZag/ZigZag.Models.cs b/src/s-z/ZigZag/ZigZag.Models.cs index aac094d99..a988ed74a 100644 --- a/src/s-z/ZigZag/ZigZag.Models.cs +++ b/src/s-z/ZigZag/ZigZag.Models.cs @@ -1,6 +1,6 @@ namespace Skender.Stock.Indicators; -public sealed record class ZigZagResult : IReusableResult +public record struct ZigZagResult : IReusableResult { public DateTime Timestamp { get; set; } public decimal? ZigZag { get; set; } // zig zag line @@ -8,7 +8,8 @@ public sealed record class ZigZagResult : IReusableResult public decimal? RetraceHigh { get; set; } // zig zag retrace high line public decimal? RetraceLow { get; set; } // zig zag retrace low line - double IReusableResult.Value => ZigZag.Null2NaN(); + readonly double IReusableResult.Value + => ZigZag.Null2NaN(); } internal class ZigZagEval diff --git a/tests/indicators/TestBase.cs b/tests/indicators/TestBase.cs index 098e0f257..1b360cbb6 100644 --- a/tests/indicators/TestBase.cs +++ b/tests/indicators/TestBase.cs @@ -14,23 +14,50 @@ public abstract class TestQuoteBase { 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(); + internal static IEnumerable quotes = []; + internal static IEnumerable otherQuotes = []; + internal static IEnumerable badQuotes = []; + internal static IEnumerable bigQuotes = []; + internal static IEnumerable maxQuotes = []; + internal static IEnumerable longishQuotes = []; + internal static IEnumerable longestQuotes = []; + internal static IEnumerable mismatchQuotes = []; + internal static IEnumerable noquotes = []; + internal static IEnumerable onequote = []; + internal static IEnumerable randomQuotes = []; + internal static IEnumerable zeroesQuotes = []; + internal static IEnumerable<(DateTime, double)> tupleNanny = []; + + internal TestQuoteBase() + { + try + { + quotes = TestData.GetDefault(); + otherQuotes = TestData.GetCompare(); + badQuotes = TestData.GetBad(); + bigQuotes = TestData.GetTooBig(); + maxQuotes = TestData.GetMax(); + longishQuotes = TestData.GetLongish(); + longestQuotes = TestData.GetLongest(); + mismatchQuotes = TestData.GetMismatch(); + noquotes = new List(); + onequote = TestData.GetDefault(1); + randomQuotes = TestData.GetRandom(1000); + zeroesQuotes = TestData.GetZeros(); + tupleNanny = TestData.GetTupleNaN(); + } + catch (Exception ex) + { + Console.WriteLine("Test data failed to load."); + Console.WriteLine(ex); + throw; + } + } } /// /// Base tests that all static indicators (series) should have. +/// You'll get a placeholder result where not implemented. /// [TestClass] public abstract class SeriesTestBase : TestQuoteBase @@ -42,50 +69,40 @@ internal readonly DateTime evalDate [TestMethod] public virtual void Standard() - { - Assert.Inconclusive(); - } + => Assert.Inconclusive("Test not implemented"); [TestMethod] public virtual void BadData() - { - Assert.Inconclusive(); - } + => Assert.Inconclusive("Test not implemented"); [TestMethod] public virtual void NoQuotes() - { - Assert.Inconclusive(); - } + => Assert.Inconclusive("Test not implemented"); [TestMethod] public virtual void Equality() - { - Assert.Inconclusive(); - } + => Assert.Inconclusive("Test not implemented"); } /// /// Base tests that all streamed indicators should have. +/// You'll get a placeholder result where not implemented. /// [TestClass] public abstract class StreamTestBase : TestQuoteBase { [TestMethod] public virtual void QuoteObserver() - { - Assert.Inconclusive(); - } + => Assert.Inconclusive("Test not implemented"); [TestMethod] public virtual void Duplicates() - { - Assert.Inconclusive(); - } + => Assert.Inconclusive("Test not implemented"); } /// -/// Additional tests all stream chainee indicators should have. +/// Add this to stream chainee indicator tests. +/// You'll get a placeholder result where not implemented. /// public interface ITestChainObserver { @@ -94,7 +111,8 @@ public interface ITestChainObserver } /// -/// Additional tests all stream chainor indicators should have. +/// Add this to all stream chainor indicator tests. +/// You'll get a placeholder result where not implemented. /// public interface ITestChainProvider { diff --git a/tests/indicators/_common/Generics/Seek.Tests.cs b/tests/indicators/_common/Generics/Seek.Tests.cs index aff41e334..73d2fd3fd 100644 --- a/tests/indicators/_common/Generics/Seek.Tests.cs +++ b/tests/indicators/_common/Generics/Seek.Tests.cs @@ -10,7 +10,8 @@ public void FindSeries() IEnumerable emaResults = quotes.GetEma(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", EnglishCulture); EmaResult r = emaResults.Find(findDate); Assert.AreEqual(249.3519, r.Ema.Round(4)); @@ -23,10 +24,11 @@ public void FindSeriesNone() IEnumerable emaResults = quotes.GetEma(20); // find specific date - DateTime findDate = DateTime.ParseExact("1928-10-29", "yyyy-MM-dd", EnglishCulture); + DateTime findDate + = DateTime.ParseExact("1928-10-29", "yyyy-MM-dd", EnglishCulture); - EmaResult r = emaResults.Find(findDate); - Assert.IsNull(r); + Assert.ThrowsException( + () => emaResults.Find(findDate)); } [TestMethod] @@ -37,7 +39,8 @@ public void FindSeriesIndex() .ToSortedList(); // find specific date - DateTime findDate = DateTime.ParseExact("2018-12-31", "yyyy-MM-dd", EnglishCulture); + 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/Helper.Importer.cs b/tests/indicators/_common/Helper.Importer.cs index 8c975bb74..0f115bfcc 100644 --- a/tests/indicators/_common/Helper.Importer.cs +++ b/tests/indicators/_common/Helper.Importer.cs @@ -15,15 +15,16 @@ internal static Quote QuoteFromCsv(string csvLine) return new Quote(); } - string[] values = csvLine.Split(','); - Quote quote = new(); + string[] csv = csvLine.Split(','); - 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]); + 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; } @@ -33,41 +34,12 @@ internal static decimal ToDecimal(this string value) : 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; - - private static void HandleOHLCV(Quote quote, string position, string value) - { - if (string.IsNullOrEmpty(value)) - { - return; - } - - switch (position) - { - case "D": - quote.Timestamp = 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/Observables/ChainProvider.Tests.cs b/tests/indicators/_common/Observables/ChainProvider.Tests.cs index c80cba732..dd8fad1b3 100644 --- a/tests/indicators/_common/Observables/ChainProvider.Tests.cs +++ b/tests/indicators/_common/Observables/ChainProvider.Tests.cs @@ -25,7 +25,7 @@ public override void QuoteObserver() .Use(CandlePart.Close); // fetch initial results - IEnumerable results = observer.Results; + IEnumerable results = observer.Results; // emulate adding quotes to provider for (int i = 50; i < length; i++) @@ -35,7 +35,7 @@ public override void QuoteObserver() } // final results - List resultsList + List resultsList = results.ToList(); // time-series, for comparison @@ -46,17 +46,17 @@ List resultsList for (int i = 0; i < seriesList.Count; i++) { (DateTime date, double value) = seriesList[i]; - UseResult r = resultsList[i]; + QuotePart r = resultsList[i]; Assert.AreEqual(date, r.Timestamp); Assert.AreEqual(value, r.Value); } // confirm public interface - Assert.AreEqual(observer.Cache.Count, observer.Results.Count()); + Assert.AreEqual(observer.Cache.Count, observer.Results.Count); - // confirm chain cache length (more tests in Chainor) - Assert.AreEqual(observer.Cache.Count, observer.Chain.Count); + // confirm same length as provider cache + Assert.AreEqual(observer.Cache.Count, provider.Results.Count); observer.Unsubscribe(); provider.EndTransmission(); @@ -74,9 +74,9 @@ public void Chainor() QuoteProvider provider = new(); // initialize observer - Ema ema = provider + Ema ema = provider .Use(CandlePart.HL2) - .AttachEma(11); + .ToEma(11); // emulate adding quotes to provider for (int i = 0; i < length; i++) @@ -93,7 +93,7 @@ public void Chainor() // time-series, for comparison List staticEma = quotes - .Use(CandlePart.HL2) + .Use(CandlePart.HL2) .GetEma(11) .ToList(); @@ -102,15 +102,11 @@ public void Chainor() { EmaResult s = staticEma[i]; EmaResult r = streamEma[i]; - (DateTime date, double value) = ema.Chain[i]; + QuotePart e = ema.Provider.Results[i]; // compare series Assert.AreEqual(s.Timestamp, r.Timestamp); Assert.AreEqual(s.Ema, r.Ema); - - // compare chain cache - Assert.AreEqual(r.Timestamp, date); - Assert.AreEqual(r.Ema.Null2NaN(), value); } } @@ -148,16 +144,11 @@ public void LateArrival() for (int i = 0; i < length; i++) { Quote q = quotesList[i]; - UseResult r = observer.Cache[i]; - (DateTime Timestamp, double Value) = observer.Chain[i]; + QuotePart r = observer.Cache[i]; // compare quote to result cache Assert.AreEqual(q.Timestamp, r.Timestamp); Assert.AreEqual((double)q.Close, r.Value); - - // compare result to chain cache - Assert.AreEqual(r.Timestamp, Timestamp); - Assert.AreEqual(r.Value, Value); } // close observations @@ -183,7 +174,7 @@ public void Overflow() } }); - Assert.AreEqual(1, chainProvider.Results.Count()); - Assert.AreEqual(1, chainProvider.Chain.Count); + Assert.AreEqual(1, provider.Results.Count); + Assert.AreEqual(1, chainProvider.Results.Count); } } diff --git a/tests/indicators/_common/Observables/QuoteProvider.Tests.cs b/tests/indicators/_common/Observables/QuoteProvider.Tests.cs index dbfaf5b6f..7063ceeec 100644 --- a/tests/indicators/_common/Observables/QuoteProvider.Tests.cs +++ b/tests/indicators/_common/Observables/QuoteProvider.Tests.cs @@ -32,7 +32,7 @@ public override void QuoteObserver() } // confirm public interfaces - Assert.AreEqual(provider.Cache.Count, provider.Quotes.Count()); + Assert.AreEqual(provider.Cache.Count, provider.Results.Count); // close observations provider.EndTransmission(); @@ -103,7 +103,7 @@ public void Overflow() } }); - Assert.AreEqual(1, provider.Quotes.Count()); + Assert.AreEqual(1, provider.Results.Count); provider.EndTransmission(); } @@ -124,12 +124,6 @@ public void Exceptions() } }); - // null quote - Assert.ThrowsException(() => { - Quote quote = null; - provider.Add(quote); - }); - // close observations provider.EndTransmission(); } diff --git a/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs b/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs index 2fc147509..bb9fc2f69 100644 --- a/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs +++ b/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs @@ -113,7 +113,7 @@ public void QuoteToTuple() // bad argument Assert.ThrowsException(() - => q.ToBasicData((CandlePart)999)); + => q.ToQuotePart((CandlePart)999)); } [TestMethod] @@ -168,38 +168,38 @@ public void QuoteToBasicData() Assert.AreEqual( NullMath.Round((double)o, 10), - NullMath.Round(q.ToBasicData(CandlePart.Open).Value, 10)); + NullMath.Round(q.ToQuotePart(CandlePart.Open).Value, 10)); Assert.AreEqual( NullMath.Round((double)h, 10), - NullMath.Round(q.ToBasicData(CandlePart.High).Value, 10)); + NullMath.Round(q.ToQuotePart(CandlePart.High).Value, 10)); Assert.AreEqual( NullMath.Round((double)l, 10), - NullMath.Round(q.ToBasicData(CandlePart.Low).Value, 10)); + NullMath.Round(q.ToQuotePart(CandlePart.Low).Value, 10)); Assert.AreEqual( NullMath.Round((double)c, 10), - NullMath.Round(q.ToBasicData(CandlePart.Close).Value, 10)); + NullMath.Round(q.ToQuotePart(CandlePart.Close).Value, 10)); Assert.AreEqual( NullMath.Round((double)v, 10), - NullMath.Round(q.ToBasicData(CandlePart.Volume).Value, 10)); + NullMath.Round(q.ToQuotePart(CandlePart.Volume).Value, 10)); Assert.AreEqual( NullMath.Round((double)hl2, 10), - NullMath.Round(q.ToBasicData(CandlePart.HL2).Value, 10)); + NullMath.Round(q.ToQuotePart(CandlePart.HL2).Value, 10)); Assert.AreEqual( NullMath.Round((double)hlc3, 10), - NullMath.Round(q.ToBasicData(CandlePart.HLC3).Value, 10)); + NullMath.Round(q.ToQuotePart(CandlePart.HLC3).Value, 10)); Assert.AreEqual( NullMath.Round((double)oc2, 10), - NullMath.Round(q.ToBasicData(CandlePart.OC2).Value, 10)); + NullMath.Round(q.ToQuotePart(CandlePart.OC2).Value, 10)); Assert.AreEqual( NullMath.Round((double)ohl3, 10), - NullMath.Round(q.ToBasicData(CandlePart.OHL3).Value, 10)); + NullMath.Round(q.ToQuotePart(CandlePart.OHL3).Value, 10)); Assert.AreEqual( NullMath.Round((double)ohlc4, 10), - NullMath.Round(q.ToBasicData(CandlePart.OHLC4).Value, 10)); + NullMath.Round(q.ToQuotePart(CandlePart.OHLC4).Value, 10)); // bad argument Assert.ThrowsException(() - => q.ToBasicData((CandlePart)999)); + => q.ToQuotePart((CandlePart)999)); } [TestMethod] diff --git a/tests/indicators/_common/Results/Result.Utilities.Tests.cs b/tests/indicators/_common/Results/Result.Utilities.Tests.cs index 58c63ded0..4de7aba83 100644 --- a/tests/indicators/_common/Results/Result.Utilities.Tests.cs +++ b/tests/indicators/_common/Results/Result.Utilities.Tests.cs @@ -8,15 +8,15 @@ public class Results : SeriesTestBase [TestMethod] public void Condense() { - List x = quotes + List results = quotes .GetAdx(14) .ToList(); // make a few more in the middle null and NaN - x[249].Adx = null; - x[345].Adx = double.NaN; + results[249] = results[249] with { Adx = null }; + results[345] = results[345] with { Adx = double.NaN }; - List r = x.Condense().ToList(); + List r = results.Condense().ToList(); // proper quantities Assert.AreEqual(473, r.Count); diff --git a/tests/indicators/a-d/BasicQuote/BasicQuote.Calc.xlsx b/tests/indicators/_common/Use/Use.Calc.xlsx similarity index 100% rename from tests/indicators/a-d/BasicQuote/BasicQuote.Calc.xlsx rename to tests/indicators/_common/Use/Use.Calc.xlsx diff --git a/tests/indicators/a-d/BasicQuote/BasicQuote.Tests.cs b/tests/indicators/_common/Use/Use.Tests.cs similarity index 50% rename from tests/indicators/a-d/BasicQuote/BasicQuote.Tests.cs rename to tests/indicators/_common/Use/Use.Tests.cs index 9a1f2a124..0fcdfc1ec 100644 --- a/tests/indicators/a-d/BasicQuote/BasicQuote.Tests.cs +++ b/tests/indicators/_common/Use/Use.Tests.cs @@ -1,37 +1,37 @@ namespace Tests.Indicators; [TestClass] -public class BaseQuoteTests : SeriesTestBase +public class UseTests : SeriesTestBase { [TestMethod] public override 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(); + List o = quotes.Use(CandlePart.Open).ToList(); + List h = quotes.Use(CandlePart.High).ToList(); + List l = quotes.Use(CandlePart.Low).ToList(); + List c = quotes.Use(CandlePart.Close).ToList(); + List v = quotes.Use(CandlePart.Volume).ToList(); + List hl = quotes.Use(CandlePart.HL2).ToList(); + List hlc = quotes.Use(CandlePart.HLC3).ToList(); + List oc = quotes.Use(CandlePart.OC2).ToList(); + List ohl = quotes.Use(CandlePart.OHL3).ToList(); + List ohlc = quotes.Use(CandlePart.OHLC4).ToList(); // proper quantities Assert.AreEqual(502, c.Count); // samples - BasicResult ro = o[501]; - BasicResult rh = h[501]; - BasicResult rl = l[501]; - BasicResult rc = c[501]; - BasicResult rv = v[501]; - BasicResult rhl = hl[501]; - BasicResult rhlc = hlc[501]; - BasicResult roc = oc[501]; - BasicResult rohl = ohl[501]; - BasicResult rohlc = ohlc[501]; + 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", EnglishCulture); @@ -53,7 +53,7 @@ public override void Standard() [TestMethod] public void Use() { - List<(DateTime Timestamp, double Value)> results = quotes + List results = quotes .Use(CandlePart.Close) .ToList(); @@ -64,7 +64,7 @@ public void Use() public void Chainor() { List results = quotes - .GetBaseQuote(CandlePart.Close) + .Use(CandlePart.Close) .GetSma(10) .ToList(); diff --git a/tests/indicators/a-d/Alligator/Alligator.Series.Tests.cs b/tests/indicators/a-d/Alligator/Alligator.Series.Tests.cs index 3ab44b809..887782208 100644 --- a/tests/indicators/a-d/Alligator/Alligator.Series.Tests.cs +++ b/tests/indicators/a-d/Alligator/Alligator.Series.Tests.cs @@ -43,26 +43,18 @@ public override 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() + .CalcAlligator( + jawPeriods: 13, + jawOffset: 8, + teethPeriods: 8, + teethOffset: 5, + lipsPeriods: 5, + lipsOffset: 3) .ToList(); Assert.AreEqual(200, r.Count); diff --git a/tests/indicators/a-d/Alligator/Alligator.Stream.Tests.cs b/tests/indicators/a-d/Alligator/Alligator.Stream.Tests.cs index 9f472f9a2..3be2275e9 100644 --- a/tests/indicators/a-d/Alligator/Alligator.Stream.Tests.cs +++ b/tests/indicators/a-d/Alligator/Alligator.Stream.Tests.cs @@ -15,8 +15,8 @@ public override void QuoteObserver() QuoteProvider provider = new(); // initialize observer - Alligator observer = provider - .AttachAlligator(13, 8, 8, 5, 5, 3); + var observer = provider + .ToAlligator(13, 8, 8, 5, 5, 3); // fetch initial results (early) IEnumerable results @@ -85,9 +85,9 @@ public void Chainee() QuoteProvider provider = new(); // initialize observer - Alligator observer = provider - .AttachSma(10) - .AttachAlligator(13, 8, 8, 5, 5, 3); + var observer = provider + .ToSma(10) + .ToAlligator(13, 8, 8, 5, 5, 3); // emulate adding quotes out of order // note: this works when graceful order diff --git a/tests/indicators/a-d/Dpo/Dpo.Tests.cs b/tests/indicators/a-d/Dpo/Dpo.Tests.cs index 6c04b6b26..22a258b48 100644 --- a/tests/indicators/a-d/Dpo/Dpo.Tests.cs +++ b/tests/indicators/a-d/Dpo/Dpo.Tests.cs @@ -62,17 +62,6 @@ public void UseTuple() 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() { diff --git a/tests/indicators/e-k/Ema/Ema.Series.Tests.cs b/tests/indicators/e-k/Ema/Ema.Series.Tests.cs index a1303d5f9..654f4a555 100644 --- a/tests/indicators/e-k/Ema/Ema.Series.Tests.cs +++ b/tests/indicators/e-k/Ema/Ema.Series.Tests.cs @@ -6,7 +6,7 @@ public class EmaSeriesTests : SeriesTestBase [TestMethod] public void Increment() { - double ema = Ema.Increment(20, 217.5693, 222.10); + double ema = EmaUtilities.Increment(20, 217.5693, 222.10); Assert.AreEqual(218.0008, ema.Round(4)); } @@ -72,17 +72,6 @@ public void UseTuple() 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)); - } - [TestMethod] public void Chainee() { diff --git a/tests/indicators/e-k/Ema/Ema.Stream.Tests.cs b/tests/indicators/e-k/Ema/Ema.Stream.Tests.cs index b15015eef..8807d3d5f 100644 --- a/tests/indicators/e-k/Ema/Ema.Stream.Tests.cs +++ b/tests/indicators/e-k/Ema/Ema.Stream.Tests.cs @@ -15,8 +15,8 @@ public override void QuoteObserver() QuoteProvider provider = new(); // initialize observer - Ema observer = provider - .AttachEma(20); + Ema observer = provider + .ToEma(20); // fetch initial results (early) IEnumerable results @@ -86,9 +86,9 @@ public void Chainor() QuoteProvider provider = new(); // initialize observer - Sma observer = provider - .AttachEma(emaPeriods) - .AttachSma(smaPeriods); + Sma observer = provider + .ToEma(emaPeriods) + .ToSma(smaPeriods); // emulate adding quotes to provider for (int i = 0; i < length; i++) @@ -155,9 +155,9 @@ public void Chainee() QuoteProvider provider = new(); // initialize observer - Ema observer = provider - .AttachSma(smaPeriods) - .AttachEma(emaPeriods); + Ema observer = provider + .ToSma(smaPeriods) + .ToEma(emaPeriods); // emulate quote stream for (int i = 0; i < length; i++) @@ -196,8 +196,8 @@ public override void Duplicates() QuoteProvider provider = new(); // initialize observer - Ema observer = provider - .AttachEma(10); + Ema observer = provider + .ToEma(10); // add duplicate to cover warmup Quote quote = quotes.Last(); @@ -210,7 +210,7 @@ public override void Duplicates() observer.Unsubscribe(); provider.EndTransmission(); - Assert.AreEqual(1, observer.Results.Count()); - Assert.AreEqual(1, observer.Chain.Count); + Assert.AreEqual(1, observer.Results.Count); + Assert.AreEqual(1, provider.Results.Count); } } diff --git a/tests/indicators/e-k/Gator/Gator.Tests.cs b/tests/indicators/e-k/Gator/Gator.Tests.cs index 97ef89c46..3340eb706 100644 --- a/tests/indicators/e-k/Gator/Gator.Tests.cs +++ b/tests/indicators/e-k/Gator/Gator.Tests.cs @@ -156,17 +156,6 @@ public void UseTuple() 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() { diff --git a/tests/indicators/m-r/MaEnvelopes/MaEnvelopes.Tests.cs b/tests/indicators/m-r/MaEnvelopes/MaEnvelopes.Tests.cs index 8bc8d0cad..ae1a80963 100644 --- a/tests/indicators/m-r/MaEnvelopes/MaEnvelopes.Tests.cs +++ b/tests/indicators/m-r/MaEnvelopes/MaEnvelopes.Tests.cs @@ -257,17 +257,6 @@ public void UseTuple() 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() { diff --git a/tests/indicators/s-z/Sma/Sma.Analysis.Tests.cs b/tests/indicators/s-z/Sma/Sma.Analysis.Tests.cs index 84f362eca..cd11d76ec 100644 --- a/tests/indicators/s-z/Sma/Sma.Analysis.Tests.cs +++ b/tests/indicators/s-z/Sma/Sma.Analysis.Tests.cs @@ -34,17 +34,6 @@ public void UseTuple() 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() { diff --git a/tests/indicators/s-z/Sma/Sma.Series.Tests.cs b/tests/indicators/s-z/Sma/Sma.Series.Tests.cs index 36a67bb2f..8293d94b1 100644 --- a/tests/indicators/s-z/Sma/Sma.Series.Tests.cs +++ b/tests/indicators/s-z/Sma/Sma.Series.Tests.cs @@ -78,17 +78,6 @@ public void Chainor() 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() { diff --git a/tests/indicators/s-z/Sma/Sma.Stream.Tests.cs b/tests/indicators/s-z/Sma/Sma.Stream.Tests.cs index 2388dbcee..9367d0b78 100644 --- a/tests/indicators/s-z/Sma/Sma.Stream.Tests.cs +++ b/tests/indicators/s-z/Sma/Sma.Stream.Tests.cs @@ -15,8 +15,8 @@ public override void QuoteObserver() QuoteProvider provider = new(); // initialize observer - Sma observer = provider - .AttachSma(20); + Sma observer = provider + .ToSma(20); // fetch initial results (early) IEnumerable results @@ -53,8 +53,8 @@ List streamList = results.ToList(); // time-series, for comparison - List seriesList = quotesList - .GetSma(20) + var seriesList = quotesList + .GetSma(20) .ToList(); // assert, should equal series @@ -86,9 +86,9 @@ public void Chainor() QuoteProvider provider = new(); // initialize observer - Ema observer = provider - .AttachSma(smaPeriods) - .AttachEma(emaPeriods); + Ema observer = provider + .ToSma(smaPeriods) + .ToEma(emaPeriods); // emulate quote stream for (int i = 0; i < length; i++) @@ -142,9 +142,9 @@ public void Chainee() } // initialize observer - Sma observer = provider + Sma observer = provider .Use(CandlePart.OC2) - .AttachSma(11); + .ToSma(11); // emulate adding quotes to provider for (int i = 50; i < length; i++) @@ -181,8 +181,8 @@ public override void Duplicates() QuoteProvider provider = new(); // initialize observer - Sma observer = provider - .AttachSma(10); + Sma observer = provider + .ToSma(10); // add duplicate to cover warmup Quote quote = quotes.Last(); @@ -195,7 +195,7 @@ public override void Duplicates() observer.Unsubscribe(); provider.EndTransmission(); - Assert.AreEqual(1, observer.Results.Count()); - Assert.AreEqual(1, observer.Chain.Count); + Assert.AreEqual(1, observer.Results.Count); + Assert.AreEqual(1, provider.Results.Count); } } diff --git a/tests/observe/Program.cs b/tests/observe/Program.cs index 153b4bd14..3c6060183 100644 --- a/tests/observe/Program.cs +++ b/tests/observe/Program.cs @@ -57,14 +57,14 @@ public async Task SubscribeToQuotes(string symbol) // initialize our quote provider and a few subscribers QuoteProvider provider = new(); - Sma sma = provider.AttachSma(3); - Ema ema = provider.AttachEma(5); - Ema useChain = provider + Sma sma = provider.ToSma(3); + Ema ema = provider.ToEma(5); + Ema useChain = provider .Use(CandlePart.HL2) - .AttachEma(7); - Ema emaChain = provider - .AttachSma(4) - .AttachEma(4); + .ToEma(7); + Ema emaChain = provider + .ToSma(4) + .ToEma(4); // connect to Alpaca websocket SecretKey secretKey = new(ALPACA_KEY, ALPACA_SECRET); @@ -108,10 +108,10 @@ IAlpacaDataSubscription quoteSubscription // display live results string liveMessage = $"{q.TimeUtc:u} ${q.Close:N2}"; - SmaResult s = sma.Results.Last(); - EmaResult e = ema.Results.Last(); - EmaResult u = useChain.Results.Last(); - EmaResult c = emaChain.Results.Last(); + SmaResult s = sma.Results[^1]; + EmaResult e = ema.Results[^1]; + EmaResult u = useChain.Results[^1]; + EmaResult c = emaChain.Results[^1]; if (s.Sma is not null) { diff --git a/tests/other/CustomIndicator.Tests.cs b/tests/other/CustomIndicator.Tests.cs index 63206fd79..546d81db3 100644 --- a/tests/other/CustomIndicator.Tests.cs +++ b/tests/other/CustomIndicator.Tests.cs @@ -3,38 +3,26 @@ namespace Tests.CustomIndicators; -public sealed class MyResult : IReusableResult +public record struct MyResult : IReusableResult { public DateTime Timestamp { get; set; } public double? Sma { get; set; } - double IReusableResult.Value => Sma.Null2NaN(); + readonly double IReusableResult.Value + => Sma.Null2NaN(); } 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 + public static IEnumerable GetIndicator( + this IEnumerable results, + int lookbackPeriods) + where T : IReusableResult + => results .ToTupleChainable() .CalcIndicator(lookbackPeriods); - // 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) @@ -191,17 +179,6 @@ public void QuoteToSortedList() Assert.AreEqual(spotDate, h[50].Timestamp); } - [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() { diff --git a/tests/other/PublicApi.Tests.cs b/tests/other/PublicApi.Tests.cs index bdc7c9e25..8778b8104 100644 --- a/tests/other/PublicApi.Tests.cs +++ b/tests/other/PublicApi.Tests.cs @@ -3,12 +3,6 @@ [assembly: CLSCompliant(true)] namespace Tests.PublicApi; -internal sealed record MyExtendedQuote : Quote -{ - public bool MyProperty { get; set; } - public decimal? MyClose { get; set; } -} - internal sealed class MyEma : IResult { public DateTime Timestamp { get; set; } @@ -17,17 +11,23 @@ internal sealed class MyEma : IResult public double? Ema { get; set; } } -internal sealed class MyCustomQuote - : EquatableQuote, IQuote +internal sealed class MyCustomQuote : IQuote { // override, redirect required properties - public override DateTime Timestamp => CloseDate; - public override decimal Close => CloseValue; + DateTime ISeries.Timestamp => CloseDate; + decimal IQuote.Close => CloseValue; // custom properties public int MyOtherProperty { get; set; } public DateTime CloseDate { get; set; } public decimal CloseValue { get; set; } + + // required base properties + public decimal Open { get; set; } + public decimal High { get; set; } + public decimal Low { get; set; } + public decimal Volume { get; set; } + public double Value { get; set; } } [TestClass] @@ -57,36 +57,6 @@ public void ReadQuoteClass() Console.WriteLine($"Date:{f.Timestamp},Close:{f.Close}"); } - [TestMethod] - public void DerivedQuoteClass() - { - // can use a derive Quote class - MyExtendedQuote myQuote = new() { - Timestamp = 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 MyExtendedQuote { - Timestamp = x.Timestamp, - MyClose = x.Close, - MyProperty = false - }); - - Assert.IsTrue(myHistory.Any()); - } - [TestMethod] public void CustomQuoteClass() { @@ -306,10 +276,10 @@ public void StreamAll() // from quote provider QuoteProvider provider = new(); // initialize observers, get static results for comparison (later) - Ema observeEma = provider.AttachEma(20); + Ema observeEma = provider.ToEma(20); List staticEma = quotesList.GetEma(20).ToList(); - Sma observeSma = provider.AttachSma(20); + Sma observeSma = provider.ToSma(20); List staticSma = quotesList.GetSma(20).ToList(); // emulate adding quotes to provider diff --git a/tests/performance/Perf.Indicators.Stream.External.cs b/tests/performance/Perf.Indicators.Stream.External.cs index c410b0df8..1ef908cec 100644 --- a/tests/performance/Perf.Indicators.Stream.External.cs +++ b/tests/performance/Perf.Indicators.Stream.External.cs @@ -29,7 +29,7 @@ public void Setup() public object GetFoo() { QuoteProvider provider = new(); - Ema ema = provider.AttachEma(14); + Ema ema = provider.ToEma(14); for (int i = 0; i < ql.Count; i++) { diff --git a/tests/performance/Perf.Indicators.Stream.cs b/tests/performance/Perf.Indicators.Stream.cs index e66c04855..2b3a997e9 100644 --- a/tests/performance/Perf.Indicators.Stream.cs +++ b/tests/performance/Perf.Indicators.Stream.cs @@ -28,7 +28,7 @@ public void Setup() public object GetEma() { QuoteProvider provider = new(); - Ema ema = provider.AttachEma(14); + Ema ema = provider.ToEma(14); for (int i = 0; i < ql.Count; i++) { @@ -43,7 +43,7 @@ public object GetEma() public object GetSma() { QuoteProvider provider = new(); - Sma sma = provider.AttachSma(10); + Sma sma = provider.ToSma(10); for (int i = 0; i < ql.Count; i++) {