diff --git a/.github/build.main.yml b/.github/build.main.yml index f82a1585d..2eb715e33 100644 --- a/.github/build.main.yml +++ b/.github/build.main.yml @@ -1,8 +1,10 @@ trigger: - main + - v3 pr: - main + - v3 pool: name: Azure Pipelines diff --git a/Stock.Indicators.sln b/Stock.Indicators.sln index 77aa0aea6..9ebe65bf0 100644 --- a/Stock.Indicators.sln +++ b/Stock.Indicators.sln @@ -21,9 +21,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Performance", "tests\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{3A4158F9-4165-4823-9526-0CFAACCF1ACC}" ProjectSection(SolutionItems) = preProject - .lgtm.yml = .lgtm.yml .github\build.main.yml = .github\build.main.yml .github\dependabot.yml = .github\dependabot.yml + gitversion.yml = gitversion.yml + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Observe.Streaming", "tests\observe\Observe.Streaming.csproj", "{14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}" + ProjectSection(ProjectDependencies) = postProject + {11CD6C7E-871F-4903-AEAD-58E034C6521D} = {11CD6C7E-871F-4903-AEAD-58E034C6521D} + {8D0F1781-EDA3-4C51-B05D-D33FF1156E49} = {8D0F1781-EDA3-4C51-B05D-D33FF1156E49} EndProjectSection EndProject Global @@ -48,6 +54,10 @@ Global {3BD4837B-D197-41FD-A286-A3256D0770E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {3BD4837B-D197-41FD-A286-A3256D0770E1}.Release|Any CPU.ActiveCfg = Release|Any CPU {3BD4837B-D197-41FD-A286-A3256D0770E1}.Release|Any CPU.Build.0 = Release|Any CPU + {14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14DEC3AF-9AF2-4A66-8BEE-C342C6CC4307}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/gitversion.yml b/gitversion.yml index 4cb24ba14..3c3ed762a 100644 --- a/gitversion.yml +++ b/gitversion.yml @@ -1,4 +1,5 @@ mode: ContinuousDelivery +next-version: 3.0.0 commit-message-incrementing: Enabled major-version-bump-message: '\+semver:\s?(breaking|major)' diff --git a/src/GlobalSuppressions.cs b/src/GlobalSuppressions.cs index c085362d6..454263ce2 100644 --- a/src/GlobalSuppressions.cs +++ b/src/GlobalSuppressions.cs @@ -39,3 +39,23 @@ Justification = "Not compatible with `or` statement (Microsoft bug)", Scope = "member", Target = "~M:Skender.Stock.Indicators.ResultUtility.Condense``1(System.Collections.Generic.IEnumerable{``0})~System.Collections.Generic.IEnumerable{``0}")] + +[assembly: SuppressMessage( + "Naming", + "CA1725:Parameter names should match base declaration", + Justification = "The microsoft OnError implementation uses reserved word Error", + Scope = "member", + Target = "~M:Skender.Stock.Indicators.QuoteObserver.OnError(System.Exception)")] + +[assembly: SuppressMessage( + "Naming", + "CA1716:Identifiers should not match keywords", + Justification = "The microsoft OnError implementation uses reserved word Error", + Scope = "member", + Target = "~M:Skender.Stock.Indicators.QuoteObserver.OnError(System.Exception)")] + +[assembly: SuppressMessage( + "Naming", + "CA1716:Identifiers should not match keywords", + Justification = "The microsoft OnError implementation uses reserved word Error", + Scope = "member", Target = "~M:Skender.Stock.Indicators.TupleObserver.OnError(System.Exception)")] diff --git a/src/_common/Generics/Seek.cs b/src/_common/Generics/Seek.cs index c2fdd5b0a..0b0387b71 100644 --- a/src/_common/Generics/Seek.cs +++ b/src/_common/Generics/Seek.cs @@ -4,12 +4,22 @@ namespace Skender.Stock.Indicators; public static class Seeking { - // FIND by DATE - /// + // FIND SERIES by DATE + /// /// public static TSeries? Find( this IEnumerable series, DateTime lookupDate) where TSeries : ISeries => series .FirstOrDefault(x => x.Date == lookupDate); + + // FIND INDEX by DATE + /// + /// + public static int FindIndex( + this List series, + DateTime lookupDate) + where TSeries : ISeries => series == null + ? -1 + : series.FindIndex(x => x.Date == lookupDate); } diff --git a/src/_common/Generics/info.xml b/src/_common/Generics/info.xml index e50729fba..3d2e60d7a 100644 --- a/src/_common/Generics/info.xml +++ b/src/_common/Generics/info.xml @@ -1,7 +1,7 @@ - + Finds time series values on a specific date. See documentation for more information. @@ -10,8 +10,21 @@ Any series type. Time series to evaluate. Exact date to lookup. - First - record in the series on the date specified. + First record in the series on the date specified. + + + + Finds time series index on a specific date. + + See documentation for more information. + + + Any series type. + Time series to evaluate. + Exact date to lookup. + + First index in the series of the date specified or -1 if not found. + Removes a specific quantity from the beginning of the time series list. diff --git a/src/_common/Observables/ChainProvider.cs b/src/_common/Observables/ChainProvider.cs new file mode 100644 index 000000000..de73cfdbc --- /dev/null +++ b/src/_common/Observables/ChainProvider.cs @@ -0,0 +1,194 @@ +namespace Skender.Stock.Indicators; + +// TUPLE OBSERVER and TUPLE PROVIDER (CHAIN STREAM) + +public abstract class ChainProvider + : TupleObserver, IObservable<(DateTime Date, double Value)> +{ + // fields + private readonly List> observers; + + // initialize + protected ChainProvider() + { + observers = new(); + ProtectedChain = new(); + Warmup = true; + } + + // properties + internal IEnumerable<(DateTime Date, double Value)> Output => ProtectedChain; + + internal List<(DateTime Date, double Value)> ProtectedChain { get; set; } + + private int OverflowCount { get; set; } + + private bool Warmup { get; set; } + + // METHODS + + // subscribe observer + public IDisposable Subscribe(IObserver<(DateTime Date, double Value)> observer) + { + if (!observers.Contains(observer)) + { + observers.Add(observer); + } + + return new Unsubscriber(observers, observer); + } + + // close all observations + public void EndTransmission() + { + foreach (IObserver<(DateTime Date, double Value)> observer in observers.ToArray()) + { + if (observers.Contains(observer)) + { + observer.OnCompleted(); + } + } + + observers.Clear(); + } + + // add one + internal void SendToChain(TResult result) + where TResult : IReusableResult + { + // candidate result + (DateTime Date, double Value) r = new(result.Date, result.Value.Null2NaN()); + + int length = ProtectedChain.Count; + + // initialize + if (length == 0 && result.Value != null) + { + // add new tuple + ProtectedChain.Add(r); + Warmup = false; + + // notify observers + NotifyObservers(r); + return; + } + + // do not proceed until first non-null Value recieved + if (Warmup && result.Value == null) + { + return; + } + else + { + Warmup = false; + } + + (DateTime lastDate, _) = ProtectedChain[length - 1]; + + // add tuple + if (r.Date > lastDate) + { + // add new tuple + ProtectedChain.Add(r); + + // notify observers + NotifyObservers(r); + } + + // same date or tuple recieved + else if (r.Date <= lastDate) + { + // check for overflow condition + // where same tuple continues (possible circular condition) + if (r.Date == lastDate) + { + OverflowCount++; + + if (OverflowCount > 100) + { + string msg = "A repeated Chain update exceeded the 100 attempt threshold. " + + "Check and remove circular chains or check your Chain provider."; + + EndTransmission(); + + throw new OverflowException(msg); + } + } + else + { + OverflowCount = 0; + } + + // seek old tuple + int foundIndex = ProtectedChain + .FindIndex(x => x.Date == r.Date); + + // found + if (foundIndex >= 0) + { + ProtectedChain[foundIndex] = r; + } + + // add missing tuple + else + { + ProtectedChain.Add(r); + + // re-sort cache + ProtectedChain = ProtectedChain + .ToSortedList(); + } + + // let observer handle old + duplicates + NotifyObservers(r); + } + } + + // add many + internal void SendToChain(IEnumerable results) + where TResult : IReusableResult + { + List added = results + .ToSortedList(); + + for (int i = 0; i < added.Count; i++) + { + SendToChain(added[i]); + } + } + + // notify observers + private void NotifyObservers((DateTime Date, double Value) tuple) + { + List> obsList = observers.ToList(); + + for (int i = 0; i < obsList.Count; i++) + { + IObserver<(DateTime Date, double Value)> obs = obsList[i]; + obs.OnNext(tuple); + } + } + + // unsubscriber + private class Unsubscriber : IDisposable + { + private readonly List> observers; + private readonly IObserver<(DateTime Date, double Value)> observer; + + // identify and save observer + public Unsubscriber(List> observers, IObserver<(DateTime Date, double Value)> observer) + { + this.observers = observers; + this.observer = observer; + } + + // remove single observer + public void Dispose() + { + if (observer != null && observers.Contains(observer)) + { + observers.Remove(observer); + } + } + } +} diff --git a/src/_common/Observables/QuoteObserver.cs b/src/_common/Observables/QuoteObserver.cs new file mode 100644 index 000000000..628364594 --- /dev/null +++ b/src/_common/Observables/QuoteObserver.cs @@ -0,0 +1,29 @@ +namespace Skender.Stock.Indicators; + +// OBSERVER of QUOTES (BOILERPLATE) + +public abstract class QuoteObserver : IObserver +{ + // fields + private IDisposable? unsubscriber; + + // properites + internal QuoteProvider? Supplier { get; set; } + + // methods + public virtual void Subscribe() + => unsubscriber = Supplier != null + ? Supplier.Subscribe(this) + : throw new ArgumentNullException(nameof(Supplier)); + + public virtual void OnCompleted() => Unsubscribe(); + + public virtual void OnError(Exception error) => throw error; + + public virtual void OnNext(Quote value) + { + // » handle new quote with override in observer + } + + public virtual void Unsubscribe() => unsubscriber?.Dispose(); +} diff --git a/src/_common/Observables/QuoteProvider.cs b/src/_common/Observables/QuoteProvider.cs new file mode 100644 index 000000000..6f9d4c01c --- /dev/null +++ b/src/_common/Observables/QuoteProvider.cs @@ -0,0 +1,186 @@ +namespace Skender.Stock.Indicators; + +// QUOTES as PROVIDER + +public class QuoteProvider : IObservable +{ + // fields + private readonly List> observers; + + // initialize + public QuoteProvider() + { + observers = new(); + ProtectedQuotes = new(); + } + + // properties + public IEnumerable Quotes => ProtectedQuotes; + + internal List ProtectedQuotes { get; private set; } + + private int OverflowCount { get; set; } + + // METHODS + + // add one + public void Add(Quote quote) + { + // validate quote + if (quote == null) + { + throw new ArgumentNullException(nameof(quote), "Quote cannot be null."); + } + + int length = ProtectedQuotes.Count; + + if (length == 0) + { + // add new quote + ProtectedQuotes.Add(quote); + + // notify observers + NotifyObservers(quote); + + return; + } + + Quote last = ProtectedQuotes[length - 1]; + + // add quote + if (quote.Date > last.Date) + { + // add new quote + ProtectedQuotes.Add(quote); + + // notify observers + NotifyObservers(quote); + } + + // same date or quote recieved + else if (quote.Date <= last.Date) + { + // check for overflow condition + // where same quote continues (possible circular condition) + if (quote.Date == last.Date) + { + OverflowCount++; + + if (OverflowCount > 100) + { + string msg = "A repeated Quote update exceeded the 100 attempt threshold. " + + "Check and remove circular chains or check your Quote provider."; + + EndTransmission(); + + throw new OverflowException(msg); + } + } + else + { + OverflowCount = 0; + } + + // seek old quote + int foundIndex = ProtectedQuotes + .FindIndex(x => x.Date == quote.Date); + + // found + if (foundIndex >= 0) + { + Quote old = ProtectedQuotes[foundIndex]; + + old.Open = quote.Open; + old.High = quote.High; + old.Low = quote.Low; + old.Close = quote.Close; + old.Volume = quote.Volume; + } + + // add missing quote + else + { + ProtectedQuotes.Add(quote); + + // re-sort cache + ProtectedQuotes = ProtectedQuotes + .ToSortedList(); + } + + // let observer handle old + duplicates + NotifyObservers(quote); + } + } + + // add many + public void Add(IEnumerable quotes) + { + List added = quotes + .ToSortedList(); + + for (int i = 0; i < added.Count; i++) + { + Add(added[i]); + } + } + + // subscribe observer + public IDisposable Subscribe(IObserver observer) + { + if (!observers.Contains(observer)) + { + observers.Add(observer); + } + + return new Unsubscriber(observers, observer); + } + + // close all observations + public void EndTransmission() + { + foreach (IObserver observer in observers.ToArray()) + { + if (observers.Contains(observer)) + { + observer.OnCompleted(); + } + } + + observers.Clear(); + } + + // notify observers + private void NotifyObservers(Quote quote) + { + List> obsList = observers.ToList(); + + for (int i = 0; i < obsList.Count; i++) + { + IObserver obs = obsList[i]; + obs.OnNext(quote); + } + } + + // unsubscriber + private class Unsubscriber : IDisposable + { + private readonly List> observers; + private readonly IObserver observer; + + // identify and save observer + public Unsubscriber(List> observers, IObserver observer) + { + this.observers = observers; + this.observer = observer; + } + + // remove single observer + public void Dispose() + { + if (observer != null && observers.Contains(observer)) + { + observers.Remove(observer); + } + } + } +} diff --git a/src/_common/Observables/TupleObserver.cs b/src/_common/Observables/TupleObserver.cs new file mode 100644 index 000000000..ae6809f85 --- /dev/null +++ b/src/_common/Observables/TupleObserver.cs @@ -0,0 +1,29 @@ +namespace Skender.Stock.Indicators; + +// OBSERVER of TUPLES (BOILERPLATE) + +public abstract class TupleObserver : IObserver<(DateTime Date, double Value)> +{ + // fields + private IDisposable? unsubscriber; + + // properites + internal TupleProvider? Supplier { get; set; } + + // methods + public virtual void Subscribe() + => unsubscriber = Supplier != null + ? Supplier.Subscribe(this) + : throw new ArgumentNullException(nameof(Supplier)); + + public virtual void OnCompleted() => Unsubscribe(); + + public virtual void OnError(Exception error) => throw error; + + public virtual void OnNext((DateTime Date, double Value) value) + { + // » handle new quote with override in observer + } + + public virtual void Unsubscribe() => unsubscriber?.Dispose(); +} diff --git a/src/_common/Observables/TupleProvider.cs b/src/_common/Observables/TupleProvider.cs new file mode 100644 index 000000000..38edc7ab7 --- /dev/null +++ b/src/_common/Observables/TupleProvider.cs @@ -0,0 +1,174 @@ +namespace Skender.Stock.Indicators; + +// QUOTE OBSERVER and TUPLE PROVIDER + +public abstract class TupleProvider + : QuoteObserver, IObservable<(DateTime Date, double Value)> +{ + // fields + private readonly List> observers; + + // initialize + protected TupleProvider() + { + observers = new(); + ProtectedTuples = new(); + } + + // properties + internal IEnumerable<(DateTime Date, double Value)> Output => ProtectedTuples; + + internal List<(DateTime Date, double Value)> ProtectedTuples { get; set; } + + private int OverflowCount { get; set; } + + // METHODS + + // subscribe observer + public IDisposable Subscribe(IObserver<(DateTime Date, double Value)> observer) + { + if (!observers.Contains(observer)) + { + observers.Add(observer); + } + + return new Unsubscriber(observers, observer); + } + + // close all observations + public void EndTransmission() + { + foreach (IObserver<(DateTime Date, double Value)> observer in observers.ToArray()) + { + if (observers.Contains(observer)) + { + observer.OnCompleted(); + } + } + + observers.Clear(); + } + + // add one + internal void AddSend((DateTime Date, double Value) tuple) + { + int length = ProtectedTuples.Count; + + if (length == 0) + { + // add new tuple + ProtectedTuples.Add(tuple); + + // notify observers + NotifyObservers(tuple); + return; + } + + (DateTime lastDate, _) = ProtectedTuples[length - 1]; + + // add tuple + if (tuple.Date > lastDate) + { + // add new tuple + ProtectedTuples.Add(tuple); + + // notify observers + NotifyObservers(tuple); + } + + // same date or tuple recieved + else if (tuple.Date <= lastDate) + { + // check for overflow condition + // where same tuple continues (possible circular condition) + if (tuple.Date == lastDate) + { + OverflowCount++; + + if (OverflowCount > 100) + { + string msg = "A repeated Tuple update exceeded the 100 attempt threshold. " + + "Check and remove circular chains or check your Tuple provider."; + + EndTransmission(); + + throw new OverflowException(msg); + } + } + else + { + OverflowCount = 0; + } + + // seek old tuple + int foundIndex = ProtectedTuples + .FindIndex(x => x.Date == tuple.Date); + + // found + if (foundIndex >= 0) + { + ProtectedTuples[foundIndex] = tuple; + } + + // add missing tuple + else + { + ProtectedTuples.Add(tuple); + + // re-sort cache + ProtectedTuples = ProtectedTuples + .ToSortedList(); + } + + // let observer handle old + duplicates + NotifyObservers(tuple); + } + } + + // add many + internal void AddSend(IEnumerable<(DateTime Date, double Value)> tuples) + { + List<(DateTime Date, double Value)> added = tuples + .ToSortedList(); + + for (int i = 0; i < added.Count; i++) + { + AddSend(added[i]); + } + } + + // notify observers + private void NotifyObservers((DateTime Date, double Value) tuple) + { + List> obsList = observers.ToList(); + + for (int i = 0; i < obsList.Count; i++) + { + IObserver<(DateTime Date, double Value)> obs = obsList[i]; + obs.OnNext(tuple); + } + } + + // unsubscriber + private class Unsubscriber : IDisposable + { + private readonly List> observers; + private readonly IObserver<(DateTime Date, double Value)> observer; + + // identify and save observer + public Unsubscriber(List> observers, IObserver<(DateTime Date, double Value)> observer) + { + this.observers = observers; + this.observer = observer; + } + + // remove single observer + public void Dispose() + { + if (observer != null && observers.Contains(observer)) + { + observers.Remove(observer); + } + } + } +} diff --git a/src/_common/Quotes/Quote.Converters.cs b/src/_common/Quotes/Quote.Converters.cs index b73c22b9b..07f45c7bf 100644 --- a/src/_common/Quotes/Quote.Converters.cs +++ b/src/_common/Quotes/Quote.Converters.cs @@ -9,17 +9,6 @@ public static partial class QuoteUtility { private static readonly CultureInfo NativeCulture = Thread.CurrentThread.CurrentUICulture; - /* STANDARD DECIMAL QUOTES */ - - // convert TQuotes to basic double tuple list - /// - /// - public static IEnumerable<(DateTime Date, double Value)> Use( - this IEnumerable quotes, - CandlePart candlePart = CandlePart.Close) - where TQuote : IQuote => quotes - .Select(x => x.ToTuple(candlePart)); - // TUPLE QUOTES // convert quotes to tuple list diff --git a/src/_common/Quotes/Use.Api.cs b/src/_common/Quotes/Use.Api.cs new file mode 100644 index 000000000..f9a3f2a45 --- /dev/null +++ b/src/_common/Quotes/Use.Api.cs @@ -0,0 +1,21 @@ +namespace Skender.Stock.Indicators; + +// USE (API) + +public static partial class QuoteUtility +{ + // convert TQuotes to basic double tuple list + /// + /// + public static IEnumerable<(DateTime Date, double Value)> Use( + this IEnumerable quotes, + CandlePart candlePart = CandlePart.Close) + where TQuote : IQuote => quotes + .Select(x => x.ToTuple(candlePart)); + + // OBSERVER, from Quote Provider + public static UseObserver Use( + this QuoteProvider provider, + CandlePart candlePart = CandlePart.Close) + => new(provider, candlePart); +} diff --git a/src/_common/Quotes/Use.Observer.cs b/src/_common/Quotes/Use.Observer.cs new file mode 100644 index 000000000..935b27753 --- /dev/null +++ b/src/_common/Quotes/Use.Observer.cs @@ -0,0 +1,76 @@ +namespace Skender.Stock.Indicators; + +// USE (STREAMING) +public class UseObserver : TupleProvider +{ + public UseObserver( + QuoteProvider? provider, + CandlePart candlePart) + { + Supplier = provider; + CandlePartSelection = candlePart; + Initialize(); + } + + // PROPERTIES + + public IEnumerable<(DateTime Date, double Value)> Results => ProtectedTuples; + + private CandlePart CandlePartSelection { get; set; } + + // NON-STATIC METHODS + + // handle quote arrival + public override void OnNext(Quote value) => HandleArrival(value); + + // add new quote + internal void HandleArrival(Quote quote) + { + // candidate result + (DateTime date, double value) r = quote.ToTuple(CandlePartSelection); + + // initialize + int length = ProtectedTuples.Count; + + if (length == 0) + { + AddSend(r); + return; + } + + // check against last entry + (DateTime lastDate, _) = ProtectedTuples[length - 1]; + + // add new + if (r.date > lastDate) + { + AddSend(r); + } + + // update last + else if (r.date == lastDate) + { + ProtectedTuples[length - 1] = r; + } + + // late arrival + else + { + AddSend(r); + throw new NotImplementedException(); + } + } + + // calculate initial cache of quotes + private void Initialize() + { + if (Supplier != null) + { + ProtectedTuples = Supplier + .ProtectedQuotes + .ToTuple(CandlePartSelection); + } + + Subscribe(); + } +} diff --git a/src/_common/Results/Result.Utilities.cs b/src/_common/Results/Result.Utilities.cs index 06fd4e49c..a7a45748c 100644 --- a/src/_common/Results/Result.Utilities.cs +++ b/src/_common/Results/Result.Utilities.cs @@ -26,17 +26,19 @@ public static IEnumerable Condense( // CONVERT TO TUPLE (default with pruning) /// /// - public static Collection<(DateTime Date, double Value)> ToTupleChainable( - this IEnumerable reusable) + public static Collection<(DateTime Date, double Value)> ToTupleChainable( + this IEnumerable reusable) + where TResult : IReusableResult => reusable .ToTuple() .ToCollection(); - internal static List<(DateTime Date, double Value)> ToTuple( - this IEnumerable reusable) + internal static List<(DateTime Date, double Value)> ToTuple( + this IEnumerable reusable) + where TResult : IReusableResult { List<(DateTime date, double value)> prices = new(); - List reList = reusable.ToList(); + List reList = reusable.ToList(); // find first non-nulled int first = reList.FindIndex(x => x.Value != null); @@ -53,10 +55,11 @@ public static IEnumerable Condense( // CONVERT TO TUPLE with non-nullable NaN value option and no pruning /// /// - public static Collection<(DateTime Date, double Value)> ToTupleNaN( - this IEnumerable reusable) + public static Collection<(DateTime Date, double Value)> ToTupleNaN( + this IEnumerable reusable) + where TResult : IReusableResult { - List reList = reusable.ToSortedList(); + List reList = reusable.ToSortedList(); int length = reList.Count; Collection<(DateTime Date, double Value)> results = new(); diff --git a/src/_common/Results/info.xml b/src/_common/Results/info.xml index 38ca78960..e78cd58a0 100644 --- a/src/_common/Results/info.xml +++ b/src/_common/Results/info.xml @@ -2,11 +2,13 @@ - Converts results into a reusable tuple with warmup periods removed and nulls converted - to NaN. See Converts results into a reusable tuple with warmup periods removed and nulls converted to NaN. + See - documentation for more information. + documentation for more information. + + Any reusable result type. Indicator results to evaluate. Collection of non-nullable tuple time series of results, without null warmup periods. @@ -16,6 +18,7 @@ href="https://dotnet.StockIndicators.dev/utilities/#using-tuple-results?utm_source=library&utm_medium=inline-help&utm_campaign=embedded"> documentation for more information. + Any reusable result type. Indicator results to evaluate. Collection of tuple time series of results with specified handline of nulls, without pruning. @@ -46,8 +49,7 @@ href="https://dotnet.StockIndicators.dev/utilities/#condense?utm_source=library&utm_medium=inline-help&utm_campaign=embedded"> documentation for more information. - Any result - type. + Any result type. Indicator results to evaluate. Time series of indicator results, condensed. diff --git a/src/e-k/Ema/Ema.Api.cs b/src/e-k/Ema/Ema.Api.cs index e57464f92..765314080 100644 --- a/src/e-k/Ema/Ema.Api.cs +++ b/src/e-k/Ema/Ema.Api.cs @@ -1,6 +1,7 @@ namespace Skender.Stock.Indicators; // EXPONENTIAL MOVING AVERAGE (API) + public static partial class Indicator { // SERIES, from TQuote @@ -28,30 +29,24 @@ public static IEnumerable GetEma( .ToSortedList() .CalcEma(lookbackPeriods); - // STREAM INITIALIZATION, from TQuote - /// + // OBSERVER, from Quote Provider + /// /// - internal static EmaBase InitEma( - this IEnumerable quotes, + public static EmaObserver GetEma( + this QuoteProvider provider, int lookbackPeriods) - where TQuote : IQuote { - // convert quotes - List<(DateTime, double)> tpList - = quotes.ToTuple(CandlePart.Close); + UseObserver useObserver = provider + .Use(CandlePart.Close); - return new EmaBase(tpList, lookbackPeriods); + return new(useObserver, lookbackPeriods); } - // STREAM INITIALIZATION, from CHAIN - internal static EmaBase InitEma( - this IEnumerable results, + // OBSERVER, from Chain Provider + /// + /// + public static EmaObserver GetEma( + this TupleProvider tupleProvider, int lookbackPeriods) - { - // convert results - List<(DateTime, double)> tpList - = results.ToTuple(); - - return new EmaBase(tpList, lookbackPeriods); - } + => new(tupleProvider, lookbackPeriods); } diff --git a/src/e-k/Ema/Ema.Observer.cs b/src/e-k/Ema/Ema.Observer.cs new file mode 100644 index 000000000..0ad2e7ba3 --- /dev/null +++ b/src/e-k/Ema/Ema.Observer.cs @@ -0,0 +1,144 @@ +namespace Skender.Stock.Indicators; + +// EXPONENTIAL MOVING AVERAGE (STREAMING) + +public class EmaObserver : ChainProvider +{ + public EmaObserver( + TupleProvider provider, + int lookbackPeriods) + { + Supplier = provider; + ProtectedResults = new(); + + LookbackPeriods = lookbackPeriods; + K = 2d / (lookbackPeriods + 1); + + Initialize(); + } + + // PROPERTIES + + public IEnumerable Results => ProtectedResults; + internal List ProtectedResults { get; set; } + + private double WarmupValue { get; set; } + private int LookbackPeriods { get; set; } + private double K { get; set; } + + // STATIC METHODS + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for EMA."); + } + } + + // incremental calculation + internal static double Increment(double newValue, double lastEma, double k) + => lastEma + (k * (newValue - lastEma)); + + // NON-STATIC METHODS + + // handle quote arrival + public override void OnNext((DateTime Date, double Value) value) => Add(value); + + // add new tuple quote + internal void Add((DateTime Date, double Value) tuple) + { + // candidate result (empty) + EmaResult r = new(tuple.Date); + + // initialize + int length = ProtectedResults.Count; + + if (length == 0) + { + ProtectedResults.Add(r); + WarmupValue += tuple.Value; + SendToChain(r); + return; + } + + // check against last entry + EmaResult last = ProtectedResults[length - 1]; + + // initialization periods + if (length < LookbackPeriods - 1) + { + // add if not duplicate + if (last.Date != r.Date) + { + ProtectedResults.Add(r); + WarmupValue += tuple.Value; + } + + return; + } + + // initialize with SMA + if (length == LookbackPeriods - 1) + { + WarmupValue += tuple.Value; + r.Ema = (WarmupValue / LookbackPeriods).NaN2Null(); + ProtectedResults.Add(r); + SendToChain(r); + return; + } + + // add new + if (r.Date > last.Date) + { + // calculate incremental value + double lastEma = (last.Ema == null) ? double.NaN : (double)last.Ema; + double newEma = Increment(tuple.Value, lastEma, K); + + r.Ema = newEma.NaN2Null(); + ProtectedResults.Add(r); + SendToChain(r); + return; + } + + // update last + else if (r.Date == last.Date) + { + // get prior last EMA + EmaResult prior = ProtectedResults[length - 2]; + + double priorEma = (prior.Ema == null) ? double.NaN : (double)prior.Ema; + last.Ema = Increment(tuple.Value, priorEma, K); + SendToChain(last); + return; + } + + // late arrival + else + { + // heal + throw new NotImplementedException(); + } + } + + // calculate with provider cache + private void Initialize() + { + if (Supplier != null) + { + List<(DateTime, double)> tuples = Supplier + .ProtectedTuples; + + for (int i = 0; i < tuples.Count; i++) + { + Add(tuples[i]); + } + + Subscribe(); + } + } +} diff --git a/src/e-k/Ema/Ema.Series.cs b/src/e-k/Ema/Ema.Series.cs index b54f3e79e..77952750d 100644 --- a/src/e-k/Ema/Ema.Series.cs +++ b/src/e-k/Ema/Ema.Series.cs @@ -8,7 +8,7 @@ internal static List CalcEma( int lookbackPeriods) { // check parameter arguments - EmaBase.Validate(lookbackPeriods); + EmaObserver.Validate(lookbackPeriods); // initialize int length = tpList.Count; @@ -35,7 +35,7 @@ internal static List CalcEma( if (i + 1 > lookbackPeriods) { - double ema = EmaBase.Increment(value, lastEma, k); + double ema = EmaObserver.Increment(value, lastEma, k); r.Ema = ema.NaN2Null(); lastEma = ema; } diff --git a/src/e-k/Ema/Ema.Stream.cs b/src/e-k/Ema/Ema.Stream.cs deleted file mode 100644 index fad65dc99..000000000 --- a/src/e-k/Ema/Ema.Stream.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace Skender.Stock.Indicators; - -// EXPONENTIAL MOVING AVERAGE (STREAMING) -public class EmaBase -{ - // initialize base - internal EmaBase(IEnumerable<(DateTime, double)> tpQuotes, int lookbackPeriods) - { - K = 2d / (lookbackPeriods + 1); - - ProtectedResults = tpQuotes - .ToSortedList() - .CalcEma(lookbackPeriods); - } - - // properties - internal double K { get; set; } - internal List ProtectedResults { get; set; } - - public IEnumerable Results => ProtectedResults; - - // methods - public IEnumerable Add( - Quote quote, - CandlePart candlePart = CandlePart.Close) - { - if (quote == null) - { - throw new InvalidQuotesException(nameof(quote), quote, "No quote provided."); - } - - (DateTime Date, double Value) tuple = quote.ToTuple(candlePart); - return Add(tuple); - } - - public IEnumerable Add( - (DateTime Date, double Value) tuple) - { - // get last value - int lastIndex = ProtectedResults.Count - 1; - EmaResult last = ProtectedResults[lastIndex]; - - // update bar - if (tuple.Date == last.Date) - { - // get prior last EMA - EmaResult prior = ProtectedResults[lastIndex - 1]; - - double priorEma = (prior.Ema == null) ? double.NaN : (double)prior.Ema; - last.Ema = Increment(tuple.Value, priorEma, K); - } - - // add new bar - else if (tuple.Date > last.Date) - { - // calculate incremental value - double lastEma = (last.Ema == null) ? double.NaN : (double)last.Ema; - double newEma = Increment(tuple.Value, lastEma, K); - - EmaResult r = new(tuple.Date) { Ema = newEma }; - ProtectedResults.Add(r); - } - - return Results; - } - - // incremental calculation - internal static double Increment(double newValue, double lastEma, double k) - => lastEma + (k * (newValue - lastEma)); - - // parameter validation - internal static void Validate( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for EMA."); - } - } -} diff --git a/src/e-k/Ema/info.xml b/src/e-k/Ema/info.xml index a9e87b466..5e3e3be73 100644 --- a/src/e-k/Ema/info.xml +++ b/src/e-k/Ema/info.xml @@ -16,19 +16,32 @@ Time series of EMA values. Invalid parameter value provided. - + - Extablish a streaming base for Exponential Moving Average (EMA). + Establish an observable streaming Exponential Moving Average (EMA). See documentation for more information. - Configurable Quote type. See Guide for more information. - Historical price quotes. + Observable quote provider. + Number of periods in the lookback window. + Observable EMA instance. + Invalid parameter value provided. + + + + Chain from an observable streaming Exponential Moving Average (EMA). + + See + documentation + for more information. + + + Observable from chained indicator. Number of periods in the lookback window. - EMA base that you can add Quotes to with the .Add(quote) method. + Observable EMA instance. Invalid parameter value provided. \ No newline at end of file diff --git a/src/s-z/Sma/Sma.Api.cs b/src/s-z/Sma/Sma.Api.cs index c0f3d40b5..5e21293dd 100644 --- a/src/s-z/Sma/Sma.Api.cs +++ b/src/s-z/Sma/Sma.Api.cs @@ -1,6 +1,7 @@ namespace Skender.Stock.Indicators; // SIMPLE MOVING AVERAGE (API) + public static partial class Indicator { // SERIES, from TQuote @@ -28,6 +29,27 @@ public static IEnumerable GetSma( .ToSortedList() .CalcSma(lookbackPeriods); + // OBSERVER, from Quote Provider + /// + /// + public static SmaObserver GetSma( + this QuoteProvider provider, + int lookbackPeriods) + { + UseObserver useObserver = provider + .Use(CandlePart.Close); + + return new(useObserver, lookbackPeriods); + } + + // OBSERVER, from Chain Provider + /// + /// + public static SmaObserver GetSma( + this TupleProvider tupleProvider, + int lookbackPeriods) + => new(tupleProvider, lookbackPeriods); + /// /// // ANALYSIS, from TQuote diff --git a/src/s-z/Sma/Sma.Observer.cs b/src/s-z/Sma/Sma.Observer.cs new file mode 100644 index 000000000..cc496dd92 --- /dev/null +++ b/src/s-z/Sma/Sma.Observer.cs @@ -0,0 +1,144 @@ +namespace Skender.Stock.Indicators; + +// SIMPLE MOVING AVERAGE (STREAMING) + +public class SmaObserver : ChainProvider +{ + public SmaObserver( + TupleProvider provider, + int lookbackPeriods) + { + Supplier = provider; + ProtectedResults = new(); + + LookbackPeriods = lookbackPeriods; + + Initialize(); + } + + // PROPERTIES + + public IEnumerable Results => ProtectedResults; + internal List ProtectedResults { get; set; } + + private int LookbackPeriods { get; set; } + + // STATIC METHODS + + // parameter validation + internal static void Validate( + int lookbackPeriods) + { + // check parameter arguments + if (lookbackPeriods <= 0) + { + throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, + "Lookback periods must be greater than 0 for SMA."); + } + } + + // incremental calculation + internal static double Increment( + List<(DateTime Date, double Value)> values, + int index, + int lookbackPeriods) + { + if (index < lookbackPeriods - 1) + { + return double.NaN; + } + + double sum = 0; + for (int i = index - lookbackPeriods + 1; i <= index; i++) + { + sum += values[i].Value; + } + + return sum / lookbackPeriods; + } + + // NON-STATIC METHODS + + // handle quote arrival + public override void OnNext((DateTime Date, double Value) value) => Add(value); + + // add new tuple quote + internal void Add((DateTime Date, double Value) tp) + { + if (Supplier == null) + { + throw new ArgumentNullException(nameof(Supplier), "Could not find data source."); + } + + // candidate result + SmaResult r = new(tp.Date); + + // initialize + int lengthRes = ProtectedResults.Count; + int lengthSrc = Supplier.ProtectedTuples.Count; + + // handle first value + if (lengthRes == 0) + { + ProtectedResults.Add(r); + SendToChain(r); + return; + } + + SmaResult lastResult = ProtectedResults[lengthRes - 1]; + (DateTime lastSrcDate, double _) = Supplier.ProtectedTuples[lengthSrc - 1]; + + if (r.Date == lastSrcDate) + { + r.Sma = Increment( + Supplier.ProtectedTuples, + lengthSrc - 1, + LookbackPeriods) + .NaN2Null(); + } + + // add new + if (r.Date > lastResult.Date) + { + ProtectedResults.Add(r); + SendToChain(r); + } + + // update last + else if (r.Date == lastResult.Date) + { + lastResult.Sma = r.Sma; + SendToChain(lastResult); + } + + // late arrival + else + { + // heal + throw new NotImplementedException(); + + // existing and index in sync? + + // new and index otherwise in sync? + + // all other scenarios: unsubscribe from provider and end transmission to others? + } + } + + // calculate with provider cache + private void Initialize() + { + if (Supplier != null) + { + List<(DateTime, double)> tuples = Supplier + .ProtectedTuples; + + for (int i = 0; i < tuples.Count; i++) + { + Add(tuples[i]); + } + + Subscribe(); + } + } +} diff --git a/src/s-z/Sma/Sma.Series.cs b/src/s-z/Sma/Sma.Series.cs index 0662b9e5d..2541de516 100644 --- a/src/s-z/Sma/Sma.Series.cs +++ b/src/s-z/Sma/Sma.Series.cs @@ -8,7 +8,7 @@ internal static List CalcSma( int lookbackPeriods) { // check parameter arguments - ValidateSma(lookbackPeriods); + SmaObserver.Validate(lookbackPeriods); // initialize List results = new(tpList.Count); @@ -21,31 +21,11 @@ internal static List CalcSma( SmaResult result = new(date); results.Add(result); - if (i + 1 >= lookbackPeriods) - { - double sumSma = 0; - for (int p = i + 1 - lookbackPeriods; p <= i; p++) - { - (DateTime _, double pValue) = tpList[p]; - sumSma += pValue; - } - - result.Sma = (sumSma / lookbackPeriods).NaN2Null(); - } + result.Sma = SmaObserver + .Increment(tpList, i, lookbackPeriods) + .NaN2Null(); } return results; } - - // parameter validation - private static void ValidateSma( - int lookbackPeriods) - { - // check parameter arguments - if (lookbackPeriods <= 0) - { - throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods, - "Lookback periods must be greater than 0 for SMA."); - } - } } diff --git a/src/s-z/Sma/info.xml b/src/s-z/Sma/info.xml index fb6d7271a..386418d41 100644 --- a/src/s-z/Sma/info.xml +++ b/src/s-z/Sma/info.xml @@ -17,7 +17,34 @@ Time series of SMA values. Invalid parameter value provided. - + + + Establish an observable streaming Exponential Moving Average (EMA). + + See + documentation + for more information. + + + Observable quote provider. + Number of periods in the lookback window. + Observable EMA instance. + Invalid parameter value provided. + + + + Chain from an observable streaming Simple Moving Average (SMA). + + See + documentation + for more information. + + + Observable from chained indicator. + Number of periods in the lookback window. + Observable SMA instance. + Invalid parameter value provided. + Simple Moving Average (SMA) is the average of price over a lookback window. This extended variant includes mean absolute deviation (MAD), mean square error (MSE), and mean absolute percentage error (MAPE). diff --git a/tests/indicators/_Initialize.cs b/tests/indicators/_Initialize.cs index d8e149490..13531ce50 100644 --- a/tests/indicators/_Initialize.cs +++ b/tests/indicators/_Initialize.cs @@ -8,6 +8,7 @@ [assembly: CLSCompliant(true)] [assembly: InternalsVisibleTo("Tests.Other")] [assembly: InternalsVisibleTo("Tests.Performance")] +[assembly: InternalsVisibleTo("Observe.Streaming")] namespace Tests.Common; [TestClass] diff --git a/tests/indicators/_common/Generics/Seek.Tests.cs b/tests/indicators/_common/Generics/Seek.Tests.cs index f2b16fabd..5c0b5e7f1 100644 --- a/tests/indicators/_common/Generics/Seek.Tests.cs +++ b/tests/indicators/_common/Generics/Seek.Tests.cs @@ -7,7 +7,7 @@ namespace Tests.Common; public class Seeking : TestBase { [TestMethod] - public void Find() + public void FindSeries() { IEnumerable quotes = TestData.GetDefault(); IEnumerable emaResults = quotes.GetEma(20); @@ -18,4 +18,31 @@ public void Find() EmaResult r = emaResults.Find(findDate); Assert.AreEqual(249.3519, r.Ema.Round(4)); } + + [TestMethod] + public void FindSeriesNone() + { + IEnumerable quotes = TestData.GetDefault(); + IEnumerable emaResults = quotes.GetEma(20); + + // find specific date + DateTime findDate = DateTime.ParseExact("1928-10-29", "yyyy-MM-dd", EnglishCulture); + + EmaResult r = emaResults.Find(findDate); + Assert.IsNull(r); + } + + [TestMethod] + public void FindSeriesIndex() + { + List quotes = TestData + .GetDefault() + .ToSortedList(); + + // find specific date + DateTime findDate = DateTime.ParseExact("2018-12-31", "yyyy-MM-dd", EnglishCulture); + + int i = quotes.FindIndex(findDate); + Assert.AreEqual(501, i); + } } diff --git a/tests/indicators/_common/Helper.Random.cs b/tests/indicators/_common/Helper.Random.cs index e2ef4cbe1..d2d08b533 100644 --- a/tests/indicators/_common/Helper.Random.cs +++ b/tests/indicators/_common/Helper.Random.cs @@ -27,15 +27,15 @@ internal class RandomGbm : List public RandomGbm( int bars = 250, double volatility = 1.0, - double drift = 0.05, - double seed = 10000000.0) + double drift = 0.01, + double seed = 1000.0) { this.seed = seed; this.volatility = volatility * 0.01; - this.drift = drift * 0.01; + this.drift = drift * 0.001; for (int i = 0; i < bars; i++) { - DateTime date = DateTime.Today.AddDays(i - bars); + DateTime date = DateTime.Today.AddMinutes(i - bars); Add(date); } } diff --git a/tests/indicators/_common/Quotes/Quote.Aggregates.Tests.cs b/tests/indicators/_common/Quotes/Quote.Aggregates.Tests.cs index 34b23a1ed..281b4ca17 100644 --- a/tests/indicators/_common/Quotes/Quote.Aggregates.Tests.cs +++ b/tests/indicators/_common/Quotes/Quote.Aggregates.Tests.cs @@ -4,7 +4,7 @@ namespace Tests.Common; [TestClass] -public class QuoteHistory : TestBase +public class QuoteAggregateTests : TestBase { [TestMethod] public void Aggregate() diff --git a/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs b/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs index c7ee494e0..37f7db72f 100644 --- a/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs +++ b/tests/indicators/_common/Quotes/Quote.Converters.Tests.cs @@ -5,7 +5,7 @@ namespace Tests.Common; [TestClass] -public class QuoteUtility : TestBase +public class QuoteUtilityTests : TestBase { [TestMethod] public void QuoteToSortedCollection() diff --git a/tests/indicators/_common/Quotes/Quote.Validation.Tests.cs b/tests/indicators/_common/Quotes/Quote.Validation.Tests.cs index 35d088693..7aa0abece 100644 --- a/tests/indicators/_common/Quotes/Quote.Validation.Tests.cs +++ b/tests/indicators/_common/Quotes/Quote.Validation.Tests.cs @@ -4,7 +4,7 @@ namespace Tests.Common; [TestClass] -public class QuoteValidation : TestBase +public class QuoteValidationTests : TestBase { [TestMethod] public void Validate() diff --git a/tests/indicators/_common/Quotes/Test.Quote.Provider.cs b/tests/indicators/_common/Quotes/Test.Quote.Provider.cs new file mode 100644 index 000000000..64d0a8a4d --- /dev/null +++ b/tests/indicators/_common/Quotes/Test.Quote.Provider.cs @@ -0,0 +1,61 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Skender.Stock.Indicators; + +namespace Tests.Common; + +[TestClass] +public class QuoteSourceTests : TestBase +{ + [TestMethod] + public void Standard() + { + List quotesList = quotes + .ToSortedList(); + + int length = quotes.Count(); + + // add base quotes + QuoteProvider provider = new(); + provider.Add(quotesList.Take(200)); + + // emulate incremental quotes + for (int i = 200; i < length; i++) + { + Quote q = quotesList[i]; + provider.Add(q); + } + + // assert same as original + for (int i = 0; i < length; i++) + { + Quote o = quotesList[i]; + Quote q = provider.ProtectedQuotes[i]; + + Assert.AreEqual(o, q); + } + + provider.EndTransmission(); + } + + [TestMethod] + public void Exceptions() + { + // null quote added + QuoteProvider provider = new(); + + Assert.ThrowsException(() => + { + Quote quote = new() + { + Date = DateTime.Now + }; + + for (int i = 0; i <= 101; i++) + { + provider.Add(quote); + } + }); + + provider.EndTransmission(); + } +} diff --git a/tests/indicators/_common/Quotes/Test.Use.Observer.cs b/tests/indicators/_common/Quotes/Test.Use.Observer.cs new file mode 100644 index 000000000..bf52f9217 --- /dev/null +++ b/tests/indicators/_common/Quotes/Test.Use.Observer.cs @@ -0,0 +1,99 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Skender.Stock.Indicators; +using Tests.Common; + +namespace Tests.Indicators; + +[TestClass] +public class UseStreamTests : TestBase +{ + [TestMethod] + public void Standard() + { + List quotesList = quotes + .ToSortedList(); + + int length = quotesList.Count; + + // time-series, for comparison + List<(DateTime Date, double Value)> seriesList = quotes + .ToTuple(CandlePart.Close); + + // setup quote provider + QuoteProvider provider = new(); + + // initialize EMA observer + UseObserver observer = provider + .Use(CandlePart.Close); + + // fetch initial results + IEnumerable<(DateTime Date, double Value)> results + = observer.Results; + + // emulate adding quotes to provider + for (int i = 0; i < length; i++) + { + Quote q = quotesList[i]; + provider.Add(q); + } + + // final results + List<(DateTime Date, double Value)> resultsList + = results.ToList(); + + // assert, should equal series + for (int i = 0; i < seriesList.Count; i++) + { + (DateTime sDate, double sValue) = seriesList[i]; + (DateTime rDate, double rValue) = resultsList[i]; + + Assert.AreEqual(sDate, rDate); + Assert.AreEqual(sValue, rValue); + } + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void Chainor() + { + List quotesList = quotes + .ToSortedList(); + + int length = quotesList.Count; + + // time-series, for comparison + List staticEma = quotes + .Use(CandlePart.HL2) + .GetEma(11) + .ToList(); + + // setup quote provider + QuoteProvider provider = new(); + + // initialize EMA observer + List streamEma = provider + .Use(CandlePart.HL2) + .GetEma(11) + .ProtectedResults; + + // emulate adding quotes to provider + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + provider.EndTransmission(); + + // assert, should equal series + for (int i = 0; i < length; i++) + { + EmaResult t = staticEma[i]; + EmaResult s = streamEma[i]; + + Assert.AreEqual(t.Date, s.Date); + Assert.AreEqual(t.Ema, s.Ema); + } + } +} diff --git a/tests/indicators/e-k/Ema/Ema.Obs.Tests.cs b/tests/indicators/e-k/Ema/Ema.Obs.Tests.cs new file mode 100644 index 000000000..a62556b1e --- /dev/null +++ b/tests/indicators/e-k/Ema/Ema.Obs.Tests.cs @@ -0,0 +1,100 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Skender.Stock.Indicators; +using Tests.Common; + +namespace Tests.Indicators; + +[TestClass] +public class EmaStreamTests : TestBase +{ + [TestMethod] + public void Standard() + { + List quotesList = quotes + .ToSortedList(); + + int length = quotesList.Count; + + // time-series, for comparison + List seriesList = quotes + .GetEma(20) + .ToList(); + + // setup quote provider + QuoteProvider provider = new(); + + // initialize EMA observer + EmaObserver observer = provider + .GetEma(20); + + // fetch initial results + IEnumerable results + = observer.Results; + + // emulate adding quotes to provider + for (int i = 0; i < length; i++) + { + Quote q = quotesList[i]; + provider.Add(q); + } + + // final results + List resultsList + = results.ToList(); + + // assert, should equal series + for (int i = 0; i < seriesList.Count; i++) + { + EmaResult s = seriesList[i]; + EmaResult r = resultsList[i]; + + Assert.AreEqual(s.Date, r.Date); + Assert.AreEqual(s.Ema, r.Ema); + } + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void Usee() + { + List quotesList = quotes + .ToSortedList(); + + int length = quotesList.Count; + + // time-series, for comparison + List staticEma = quotes + .Use(CandlePart.OC2) + .GetEma(11) + .ToList(); + + // setup quote provider + QuoteProvider provider = new(); + + // initialize EMA observer + List streamEma = provider + .Use(CandlePart.OC2) + .GetEma(11) + .ProtectedResults; + + // emulate adding quotes to provider + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + provider.EndTransmission(); + + // assert, should equal series + for (int i = 0; i < length; i++) + { + EmaResult t = staticEma[i]; + EmaResult s = streamEma[i]; + + Assert.AreEqual(t.Date, s.Date); + Assert.AreEqual(t.Ema, s.Ema); + } + } +} diff --git a/tests/indicators/e-k/Ema/Ema.Tests.cs b/tests/indicators/e-k/Ema/Ema.Static.Tests.cs similarity index 75% rename from tests/indicators/e-k/Ema/Ema.Tests.cs rename to tests/indicators/e-k/Ema/Ema.Static.Tests.cs index 41c67e1cc..bdc4e628e 100644 --- a/tests/indicators/e-k/Ema/Ema.Tests.cs +++ b/tests/indicators/e-k/Ema/Ema.Static.Tests.cs @@ -5,7 +5,7 @@ namespace Tests.Indicators; [TestClass] -public class EmaTests : TestBase +public class EmaStaticTests : TestBase { [TestMethod] public void Standard() @@ -29,6 +29,32 @@ public void Standard() Assert.AreEqual(249.3519, r501.Ema.Round(4)); } + [TestMethod] + public void UsePart() + { + List results = quotes + .Use(CandlePart.Open) + .GetEma(20) + .ToList(); + + // assertions + + // proper quantities + // should always be the same number of results as there is quotes + Assert.AreEqual(502, results.Count); + Assert.AreEqual(483, results.Count(x => x.Ema != null)); + + // sample values + EmaResult r29 = results[29]; + Assert.AreEqual(216.2643, NullMath.Round(r29.Ema, 4)); + + EmaResult r249 = results[249]; + Assert.AreEqual(255.4875, NullMath.Round(r249.Ema, 4)); + + EmaResult r501 = results[501]; + Assert.AreEqual(249.9157, NullMath.Round(r501.Ema, 4)); + } + [TestMethod] public void UseTuple() { @@ -80,40 +106,7 @@ public void Chainor() } [TestMethod] - public void Stream() - { - List quotesList = quotes - .OrderBy(x => x.Date) - .ToList(); - - // time-series - List series = quotesList.GetEma(20).ToList(); - - // stream simulation - EmaBase emaBase = quotesList.Take(25).InitEma(20); - - for (int i = 25; i < series.Count; i++) - { - Quote q = quotesList[i]; - emaBase.Add(q); - emaBase.Add(q); // redundant - } - - List stream = emaBase.Results.ToList(); - - // assertions - for (int i = 0; i < series.Count; i++) - { - EmaResult t = series[i]; - EmaResult s = stream[i]; - - Assert.AreEqual(t.Date, s.Date); - Assert.AreEqual(t.Ema, s.Ema); - } - } - - [TestMethod] - public void Chaining() + public void ChaineeMore() { List results = quotes .GetRsi(14) @@ -139,29 +132,6 @@ public void Chaining() Assert.AreEqual(37.0728, r501.Ema.Round(4)); } - [TestMethod] - public void Custom() - { - List results = quotes - .Use(CandlePart.Open) - .GetEma(20) - .ToList(); - - // proper quantities - Assert.AreEqual(502, results.Count); - Assert.AreEqual(483, results.Count(x => x.Ema != null)); - - // sample values - EmaResult r29 = results[29]; - Assert.AreEqual(216.2643, r29.Ema.Round(4)); - - EmaResult r249 = results[249]; - Assert.AreEqual(255.4875, r249.Ema.Round(4)); - - EmaResult r501 = results[501]; - Assert.AreEqual(249.9157, r501.Ema.Round(4)); - } - [TestMethod] public void BadData() { @@ -204,16 +174,9 @@ public void Removed() Assert.AreEqual(249.3519, last.Ema.Round(4)); } + // bad lookback period [TestMethod] public void Exceptions() - { - // bad lookback period - Assert.ThrowsException(() => - quotes.GetEma(0)); - - // null quote added - EmaBase emaBase = quotes.InitEma(14); - Assert.ThrowsException(() => - emaBase.Add(null)); - } + => Assert.ThrowsException(() + => quotes.GetEma(0)); } diff --git a/tests/indicators/s-z/Sma/Sma.Obs.Tests.cs b/tests/indicators/s-z/Sma/Sma.Obs.Tests.cs new file mode 100644 index 000000000..bca4caab6 --- /dev/null +++ b/tests/indicators/s-z/Sma/Sma.Obs.Tests.cs @@ -0,0 +1,126 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Skender.Stock.Indicators; +using Tests.Common; + +namespace Tests.Indicators; + +[TestClass] +public class SmaStreamTests : TestBase +{ + [TestMethod] + public void Standard() + { + List quotesList = quotes + .ToSortedList(); + + int length = quotesList.Count; + + // time-series, for comparison + List seriesList = quotes + .GetSma(20) + .ToList(); + + // setup quote provider + QuoteProvider provider = new(); + + // initialize EMA observer + SmaObserver observer = provider + .GetSma(20); + + // fetch initial results + IEnumerable results + = observer.Results; + + // emulate adding quotes to provider + for (int i = 0; i < length; i++) + { + Quote q = quotesList[i]; + provider.Add(q); + } + + // final results + List resultsList + = results.ToList(); + + // assert, should equal series + for (int i = 0; i < seriesList.Count; i++) + { + SmaResult s = seriesList[i]; + SmaResult r = resultsList[i]; + + Assert.AreEqual(s.Date, r.Date); + Assert.AreEqual(s.Sma, r.Sma); + } + + observer.Unsubscribe(); + provider.EndTransmission(); + } + + [TestMethod] + public void Increment() + { + // baseline for comparison + List<(DateTime Date, double Value)> tpList = new() + { + new (DateTime.Parse("1/1/2000", EnglishCulture), 1d), + new (DateTime.Parse("1/2/2000", EnglishCulture), 2d), + new (DateTime.Parse("1/3/2000", EnglishCulture), 3d), + new (DateTime.Parse("1/4/2000", EnglishCulture), 4d), + new (DateTime.Parse("1/5/2000", EnglishCulture), 5d), + new (DateTime.Parse("1/6/2000", EnglishCulture), 6d), + new (DateTime.Parse("1/7/2000", EnglishCulture), 7d), + new (DateTime.Parse("1/8/2000", EnglishCulture), 8d), + new (DateTime.Parse("1/9/2000", EnglishCulture), 9d), + }; + + double sma; + + sma = SmaObserver.Increment(tpList, tpList.Count - 1, 9); + Assert.AreEqual(5d, sma); + + sma = SmaObserver.Increment(tpList, tpList.Count - 1, 10); + Assert.AreEqual(double.NaN, sma); + } + + [TestMethod] + public void Usee() + { + List quotesList = quotes + .ToSortedList(); + + int length = quotesList.Count; + + // time-series, for comparison + List staticSma = quotes + .Use(CandlePart.OC2) + .GetSma(11) + .ToList(); + + // setup quote provider + QuoteProvider provider = new(); + + // initialize EMA observer + List streamSma = provider + .Use(CandlePart.OC2) + .GetSma(11) + .ProtectedResults; + + // emulate adding quotes to provider + for (int i = 0; i < length; i++) + { + provider.Add(quotesList[i]); + } + + provider.EndTransmission(); + + // assert, should equal series + for (int i = 0; i < length; i++) + { + SmaResult t = staticSma[i]; + SmaResult s = streamSma[i]; + + Assert.AreEqual(t.Date, s.Date); + Assert.AreEqual(t.Sma, s.Sma); + } + } +} diff --git a/tests/indicators/s-z/Sma/Sma.Tests.cs b/tests/indicators/s-z/Sma/Sma.Static.Tests.cs similarity index 80% rename from tests/indicators/s-z/Sma/Sma.Tests.cs rename to tests/indicators/s-z/Sma/Sma.Static.Tests.cs index da1fcded7..f48f11c9b 100644 --- a/tests/indicators/s-z/Sma/Sma.Tests.cs +++ b/tests/indicators/s-z/Sma/Sma.Static.Tests.cs @@ -20,11 +20,11 @@ public void Standard() // sample values Assert.IsNull(results[18].Sma); - Assert.AreEqual(214.5250, Math.Round(results[19].Sma.Value, 4)); - Assert.AreEqual(215.0310, Math.Round(results[24].Sma.Value, 4)); - Assert.AreEqual(234.9350, Math.Round(results[149].Sma.Value, 4)); - Assert.AreEqual(255.5500, Math.Round(results[249].Sma.Value, 4)); - Assert.AreEqual(251.8600, Math.Round(results[501].Sma.Value, 4)); + Assert.AreEqual(214.5250, results[19].Sma.Round(4)); + Assert.AreEqual(215.0310, results[24].Sma.Round(4)); + Assert.AreEqual(234.9350, results[149].Sma.Round(4)); + Assert.AreEqual(255.5500, results[249].Sma.Round(4)); + Assert.AreEqual(251.8600, results[501].Sma.Round(4)); } [TestMethod] @@ -40,11 +40,11 @@ public void CandlePartOpen() // sample values Assert.IsNull(results[18].Sma); - Assert.AreEqual(214.3795, Math.Round(results[19].Sma.Value, 4)); - Assert.AreEqual(214.9535, Math.Round(results[24].Sma.Value, 4)); - Assert.AreEqual(234.8280, Math.Round(results[149].Sma.Value, 4)); - Assert.AreEqual(255.6915, Math.Round(results[249].Sma.Value, 4)); - Assert.AreEqual(253.1725, Math.Round(results[501].Sma.Value, 4)); + Assert.AreEqual(214.3795, results[19].Sma.Round(4)); + Assert.AreEqual(214.9535, results[24].Sma.Round(4)); + Assert.AreEqual(234.8280, results[149].Sma.Round(4)); + Assert.AreEqual(255.6915, results[249].Sma.Round(4)); + Assert.AreEqual(253.1725, results[501].Sma.Round(4)); } [TestMethod] @@ -140,7 +140,7 @@ public void Removed() // assertions Assert.AreEqual(502 - 19, results.Count); - Assert.AreEqual(251.8600, Math.Round(results.LastOrDefault().Sma.Value, 4)); + Assert.AreEqual(251.8600, results.LastOrDefault().Sma.Round(4)); } // bad lookback period diff --git a/tests/observe/Observe.Streaming.csproj b/tests/observe/Observe.Streaming.csproj new file mode 100644 index 000000000..e3d530cc2 --- /dev/null +++ b/tests/observe/Observe.Streaming.csproj @@ -0,0 +1,19 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/tests/observe/Program.cs b/tests/observe/Program.cs new file mode 100644 index 000000000..2df87d5e8 --- /dev/null +++ b/tests/observe/Program.cs @@ -0,0 +1,117 @@ +using Alpaca.Markets; +using Skender.Stock.Indicators; + +namespace ObserveAlpaca; + +internal class Program +{ + private static async Task Main(string[] args) + { + if (args.Any()) + { + Console.WriteLine(args); + } + + QuoteStream quoteStream = new(); + await quoteStream.SubscribeToQuotes("BTC/USD"); + } +} + +public class QuoteStream +{ + private readonly string? alpacaApiKey = Environment.GetEnvironmentVariable("AlpacaApiKey"); + private readonly string? alpacaSecret = Environment.GetEnvironmentVariable("AlpacaSecret"); + + public async Task SubscribeToQuotes(string symbol) + { + Console.WriteLine("PLEASE WAIT. QUOTES ARRIVE EVERY MINUTE."); + Console.WriteLine("Press any key to exit the process..."); + + if (alpacaApiKey == null) + { + throw new ArgumentNullException(alpacaApiKey); + } + + if (alpacaSecret == null) + { + throw new ArgumentNullException(alpacaSecret); + } + + // initialize our quote provider and a few subscribers + QuoteProvider provider = new(); + + EmaObserver ema = provider.GetEma(14); + SmaObserver sma = provider.GetSma(5); + EmaObserver emaChain = provider + .Use(CandlePart.HL2) + .GetEma(10); + + // connect to Alpaca websocket + SecretKey secretKey = new(alpacaApiKey, alpacaSecret); + + IAlpacaCryptoStreamingClient client + = Environments + .Paper + .GetAlpacaCryptoStreamingClient(secretKey); + + await client.ConnectAndAuthenticateAsync(); + + AutoResetEvent[] waitObjects = new[] // todo: is this needed? + { + new AutoResetEvent(false) + }; + + IAlpacaDataSubscription quoteSubscription + = client.GetMinuteBarSubscription(symbol); + + quoteSubscription.Received += (q) => + { + // add to our provider + provider.Add(new Quote + { + Date = q.TimeUtc, + Open = q.Open, + High = q.High, + Low = q.Low, + Close = q.Close, + Volume = q.Volume + }); + + Console.WriteLine($"{q.Symbol} {q.TimeUtc:s} ${q.Close:N2} | {q.TradeCount} trades"); + }; + + await client.SubscribeAsync(quoteSubscription); + + // to stop watching on key press + Console.ReadKey(); + + provider.EndTransmission(); + await client.UnsubscribeAsync(quoteSubscription); + await client.DisconnectAsync(); + + Console.WriteLine("-- QUOTES STORED (last 10 only) --"); + foreach (Quote? pt in provider.Quotes.TakeLast(10)) + { + Console.WriteLine($"{symbol} {pt.Date:s} ${pt.Close:N2}"); + } + + // show last 3 results for indicator results + Console.WriteLine("-- EMA(14,CLOSE) RESULTS (last 3 only) --"); + foreach (EmaResult? e in ema.Results.TakeLast(3)) + { + Console.WriteLine($"{symbol} {e.Date:s} ${e.Ema:N2}"); + } + + Console.WriteLine("-- EMA(10,HL2) CHAINED (last 3 only) --"); + foreach (EmaResult? e in emaChain.Results.TakeLast(3)) + { + Console.WriteLine($"{symbol} {e.Date:s} ${e.Ema:N2}"); + } + + Console.WriteLine("-- SMA(5) RESULTS (last 3 only) --"); + foreach (SmaResult? s in sma.Results.TakeLast(3)) + { + Console.WriteLine($"{symbol} {s.Date:s} ${s.Sma:N2}"); + } + } +} \ No newline at end of file diff --git a/tests/observe/README.md b/tests/observe/README.md new file mode 100644 index 000000000..f53a331ac --- /dev/null +++ b/tests/observe/README.md @@ -0,0 +1,10 @@ +# Prerequisite steps + +To get connected to the WebSocket in this console test project, you have to add your own Alpaca key and secret information to your local environment variables. + +Run the following command line items to set, after replacing the secret and key values. + +```bash +setx AlpacaApiKey "ALPACA_API_KEY" +setx AlpacaSecret "ALPACA_SECRET" +``` diff --git a/tests/performance/Perf.Helpers.cs b/tests/performance/Perf.Helpers.cs index 877787aaa..f1f794aed 100644 --- a/tests/performance/Perf.Helpers.cs +++ b/tests/performance/Perf.Helpers.cs @@ -27,14 +27,17 @@ public class HelperPerformance public object ToListQuoteD() => h.ToQuoteD(); [Benchmark] - public object Validate() => h.Validate(); + public object ToTupleClose() => h.ToTuple(CandlePart.Close); [Benchmark] - public object Aggregate() => i.Aggregate(PeriodSize.FifteenMinutes); + public object ToTupleOHLC4() => h.ToTuple(CandlePart.OHLC4); [Benchmark] - public object ToTuple() => h.ToTuple(CandlePart.Close); + public object ToCandleResults() => h.ToCandleResults(); [Benchmark] - public object ToCandleResults() => h.ToCandleResults(); + public object Validate() => h.Validate(); + + [Benchmark] + public object Aggregate() => i.Aggregate(PeriodSize.FifteenMinutes); } diff --git a/tests/performance/Perf.Indicators.Static.cs b/tests/performance/Perf.Indicators.Static.cs new file mode 100644 index 000000000..e4bb92aaa --- /dev/null +++ b/tests/performance/Perf.Indicators.Static.cs @@ -0,0 +1,323 @@ +using BenchmarkDotNet.Attributes; +using Skender.Stock.Indicators; +using Tests.Common; + +namespace Tests.Performance; + +public class IndicatorsStatic +{ + private static IEnumerable q; + private static IEnumerable o; + private static List ql; + private static List ll; + + // SETUP + + [GlobalSetup] + public static void Setup() + { + q = TestData.GetDefault(); + ql = q.ToList(); + ll = TestData.GetLongest().ToList(); + } + + [GlobalSetup(Targets = new[] + { + nameof(GetBeta), + nameof(GetBetaUp), + nameof(GetBetaDown), + nameof(GetBetaAll), + nameof(GetCorrelation), + nameof(GetPrs), + nameof(GetPrsWithSma) + })] + public static void SetupCompare() + { + q = TestData.GetDefault(); + o = TestData.GetCompare(); + } + + // BENCHMARKS + + [Benchmark] + public object GetAdl() => q.GetAdl(); + + [Benchmark] + public object GetAdlWithSma() => q.GetAdl(14); + + [Benchmark] + public object GetAdx() => q.GetAdx(); + + [Benchmark] + public object GetAlligator() => q.GetAlligator(); + + [Benchmark] + public object GetAlma() => q.GetAlma(); + + [Benchmark] + public object GetAroon() => q.GetAroon(); + + [Benchmark] + public object GetAtr() => q.GetAtr(); + + [Benchmark] + public object GetAtrStop() => q.GetAtrStop(); + + [Benchmark] + public object GetAwesome() => q.GetAwesome(); + + [Benchmark] + public object GetBeta() => Indicator.GetBeta(q, o, 20, BetaType.Standard); + + [Benchmark] + public object GetBetaUp() => Indicator.GetBeta(q, o, 20, BetaType.Up); + + [Benchmark] + public object GetBetaDown() => Indicator.GetBeta(q, o, 20, BetaType.Down); + + [Benchmark] + public object GetBetaAll() => Indicator.GetBeta(q, o, 20, BetaType.All); + + [Benchmark] + public object GetBollingerBands() => q.GetBollingerBands(); + + [Benchmark] + public object GetBop() => q.GetBop(); + + [Benchmark] + public object GetCci() => q.GetCci(); + + [Benchmark] + public object GetChaikinOsc() => q.GetChaikinOsc(); + + [Benchmark] + public object GetChandelier() => q.GetChandelier(); + + [Benchmark] + public object GetChop() => q.GetChop(); + + [Benchmark] + public object GetCmf() => q.GetCmf(); + + [Benchmark] + public object GetCmo() => q.GetCmo(14); + + [Benchmark] + public object GetConnorsRsi() => q.GetConnorsRsi(); + + [Benchmark] + public object GetCorrelation() => q.GetCorrelation(o, 20); + + [Benchmark] + public object GetDema() => q.GetDema(14); + + [Benchmark] + public object GetDoji() => q.GetDoji(); + + [Benchmark] + public object GetDonchian() => q.GetDonchian(); + + [Benchmark] + public object GetDpo() => q.GetDpo(14); + + [Benchmark] + public object GetElderRay() => q.GetElderRay(); + + [Benchmark] + public object GetEma() => q.GetEma(14); + + [Benchmark] + public object GetEpma() => q.GetEpma(14); + + [Benchmark] + public object GetFcb() => q.GetFcb(14); + + [Benchmark] + public object GetFisherTransform() => q.GetFisherTransform(10); + + [Benchmark] + public object GetForceIndex() => q.GetForceIndex(13); + + [Benchmark] + public object GetFractal() => q.GetFractal(); + + [Benchmark] + public object GetGator() => q.GetGator(); + + [Benchmark] + public object GetHeikinAshi() => q.GetHeikinAshi(); + + [Benchmark] + public object GetHma() => q.GetHma(14); + + [Benchmark] + public object GetHtTrendline() => q.GetHtTrendline(); + + [Benchmark] + public object GetHurst() => q.GetHurst(); + + [Benchmark] + public object GetIchimoku() => q.GetIchimoku(); + + [Benchmark] + public object GetKama() => q.GetKama(); + + [Benchmark] + public object GetKlinger() => q.GetKvo(); + + [Benchmark] + public object GetKeltner() => q.GetKeltner(); + + [Benchmark] + public object GetKvo() => q.GetKvo(); + + [Benchmark] + public object GetMacd() => q.GetMacd(); + + [Benchmark] + public object GetMaEnvelopes() => q.GetMaEnvelopes(20, 2.5, MaType.SMA); + + [Benchmark] + public object GetMama() => q.GetMama(); + + [Benchmark] + public object GetMarubozu() => q.GetMarubozu(); + + [Benchmark] + public object GetMfi() => q.GetMfi(); + + [Benchmark] + public object GetObv() => q.GetObv(); + + [Benchmark] + public object GetObvWithSma() => q.GetObv(14); + + [Benchmark] + public object GetParabolicSar() => q.GetParabolicSar(); + + [Benchmark] + public object GetPivotPoints() => q.GetPivotPoints(PeriodSize.Month, PivotPointType.Standard); + + [Benchmark] + public object GetPivots() => q.GetPivots(); + + [Benchmark] + public object GetPmo() => q.GetPmo(); + + [Benchmark] + public object GetPrs() => q.GetPrs(o); + + [Benchmark] + public object GetPrsWithSma() => q.GetPrs(o, null, 5); + + [Benchmark] + public object GetPvo() => q.GetPvo(); + + [Benchmark] + public object GetRenko() => q.GetRenko(2.5m); + + [Benchmark] + public object GetRenkoAtr() => q.GetRenko(14); + + [Benchmark] + public object GetRoc() => q.GetRoc(20); + + [Benchmark] + public object GetRocWb() => q.GetRocWb(12, 3, 12); + + [Benchmark] + public object GetRocWithSma() => q.GetRoc(20, 14); + + [Benchmark] + public object GetRollingPivots() => q.GetRollingPivots(14, 1); + + [Benchmark] + public object GetRsi() => q.GetRsi(); + + [Benchmark] + public object GetSlope() => q.GetSlope(20); + + [Benchmark] + public object GetSma() => q.GetSma(10); + + [Benchmark] + public object GetSmaAnalysis() => q.GetSmaAnalysis(10); + + [Benchmark] + public object GetSmi() => q.GetSmi(5, 20, 5, 3); + + [Benchmark] + public object GetSmma() => q.GetSmma(10); + + [Benchmark] + public object GetStarcBands() => q.GetStarcBands(); + + [Benchmark] + public object GetStc() => q.GetStc(); + + [Benchmark] + public object GetStdDev() => q.GetStdDev(20); + + [Benchmark] + public object GetStdDevWithSma() => q.GetStdDev(20, 14); + + [Benchmark] + public object GetStdDevChannels() => q.GetStdDevChannels(); + + [Benchmark] + public object GetStoch() => q.GetStoch(); + + [Benchmark] + public object GetStochSMMA() => q.GetStoch(9, 3, 3, 3, 2, MaType.SMMA); + + [Benchmark] + public object GetStochRsi() => q.GetStochRsi(14, 14, 3); + + [Benchmark] + public object GetSuperTrend() => q.GetSuperTrend(); + + [Benchmark] + public object GetT3() => q.GetT3(); + + [Benchmark] + public object GetTema() => q.GetTema(14); + + [Benchmark] + public object GetTr() => q.GetTr(); + + [Benchmark] + public object GetTrix() => q.GetTrix(14); + + [Benchmark] + public object GetTrixWithSma() => q.GetTrix(14, 5); + + [Benchmark] + public object GetTsi() => q.GetTsi(); + + [Benchmark] + public object GetUlcerIndex() => q.GetUlcerIndex(); + + [Benchmark] + public object GetUltimate() => q.GetUltimate(); + + [Benchmark] + public object GetVolatilityStop() => q.GetVolatilityStop(); + + [Benchmark] + public object GetVortex() => q.GetVortex(14); + + [Benchmark] + public object GetVwap() => q.GetVwap(); + + [Benchmark] + public object GetVwma() => q.GetVwma(14); + + [Benchmark] + public object GetWilliamsR() => q.GetWilliamsR(); + + [Benchmark] + public object GetWma() => q.GetWma(14); + + [Benchmark] + public object GetZigZag() => q.GetZigZag(); +} diff --git a/tests/performance/Perf.Indicators.Stream.cs b/tests/performance/Perf.Indicators.Stream.cs new file mode 100644 index 000000000..485ffc8dd --- /dev/null +++ b/tests/performance/Perf.Indicators.Stream.cs @@ -0,0 +1,62 @@ +using BenchmarkDotNet.Attributes; +using Skender.Stock.Indicators; +using Tests.Common; + +namespace Tests.Performance; + +public class IndicatorsStreaming +{ + private static IEnumerable q; + private static List ql; + + // SETUP + + [GlobalSetup] + public void Setup() + { + q = TestData.GetDefault(); + ql = q.ToSortedList(); + } + + // BENCHMARKS + + [Benchmark] + public object GetEma() => q.GetEma(14); + + [Benchmark] + public object GetEmaStream() + { + // todo: refactor to exclude provider + QuoteProvider provider = new(); + EmaObserver observer = provider.GetEma(14); + + for (int i = 0; i < ql.Count; i++) + { + provider.Add(ql[i]); + } + + provider.EndTransmission(); + + return observer.Results; + } + + [Benchmark] + public object GetSma() => q.GetSma(10); + + [Benchmark] + public object GetSmaStream() + { + // todo: refactor to exclude provider + QuoteProvider provider = new(); + SmaObserver observer = provider.GetSma(10); + + for (int i = 0; i < ql.Count; i++) + { + provider.Add(ql[i]); + } + + provider.EndTransmission(); + + return observer.Results; + } +} diff --git a/tests/performance/Perf.Indicators.cs b/tests/performance/Perf.Indicators.cs deleted file mode 100644 index e482e21ec..000000000 --- a/tests/performance/Perf.Indicators.cs +++ /dev/null @@ -1,335 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Skender.Stock.Indicators; -using Tests.Common; - -namespace Tests.Performance; - -public class IndicatorPerformance -{ - private static IEnumerable h; - private static IEnumerable ho; - private static List hList; - - // SETUP - - [GlobalSetup] - public static void Setup() - { - h = TestData.GetDefault(); - hList = h.ToList(); - } - - [GlobalSetup(Targets = new[] - { - nameof(GetBeta), - nameof(GetBetaUp), - nameof(GetBetaDown), - nameof(GetBetaAll), - nameof(GetCorrelation), - nameof(GetPrs), - nameof(GetPrsWithSma) - })] - public static void SetupCompare() - { - h = TestData.GetDefault(); - ho = TestData.GetCompare(); - } - - // BENCHMARKS - - [Benchmark] - public object GetAdl() => h.GetAdl(); - - [Benchmark] - public object GetAdlWithSma() => h.GetAdl(14); - - [Benchmark] - public object GetAdx() => h.GetAdx(); - - [Benchmark] - public object GetAlligator() => h.GetAlligator(); - - [Benchmark] - public object GetAlma() => h.GetAlma(); - - [Benchmark] - public object GetAroon() => h.GetAroon(); - - [Benchmark] - public object GetAtr() => h.GetAtr(); - - [Benchmark] - public object GetAtrStop() => h.GetAtrStop(); - - [Benchmark] - public object GetAwesome() => h.GetAwesome(); - - [Benchmark] - public object GetBeta() => Indicator.GetBeta(h, ho, 20, BetaType.Standard); - - [Benchmark] - public object GetBetaUp() => Indicator.GetBeta(h, ho, 20, BetaType.Up); - - [Benchmark] - public object GetBetaDown() => Indicator.GetBeta(h, ho, 20, BetaType.Down); - - [Benchmark] - public object GetBetaAll() => Indicator.GetBeta(h, ho, 20, BetaType.All); - - [Benchmark] - public object GetBollingerBands() => h.GetBollingerBands(); - - [Benchmark] - public object GetBop() => h.GetBop(); - - [Benchmark] - public object GetCci() => h.GetCci(); - - [Benchmark] - public object GetChaikinOsc() => h.GetChaikinOsc(); - - [Benchmark] - public object GetChandelier() => h.GetChandelier(); - - [Benchmark] - public object GetChop() => h.GetChop(); - - [Benchmark] - public object GetCmf() => h.GetCmf(); - - [Benchmark] - public object GetCmo() => h.GetCmo(14); - - [Benchmark] - public object GetConnorsRsi() => h.GetConnorsRsi(); - - [Benchmark] - public object GetCorrelation() => h.GetCorrelation(ho, 20); - - [Benchmark] - public object GetDema() => h.GetDema(14); - - [Benchmark] - public object GetDoji() => h.GetDoji(); - - [Benchmark] - public object GetDonchian() => h.GetDonchian(); - - [Benchmark] - public object GetDpo() => h.GetDpo(14); - - [Benchmark] - public object GetElderRay() => h.GetElderRay(); - - [Benchmark] - public object GetEma() => h.GetEma(14); - - [Benchmark] - public object GetEmaStream() - { - EmaBase emaBase = hList.Take(15).InitEma(14); - - for (int i = 15; i < hList.Count; i++) - { - Quote q = hList[i]; - _ = emaBase.Add(q); - } - - return emaBase.Results; - } - - [Benchmark] - public object GetEpma() => h.GetEpma(14); - - [Benchmark] - public object GetFcb() => h.GetFcb(14); - - [Benchmark] - public object GetFisherTransform() => h.GetFisherTransform(10); - - [Benchmark] - public object GetForceIndex() => h.GetForceIndex(13); - - [Benchmark] - public object GetFractal() => h.GetFractal(); - - [Benchmark] - public object GetGator() => h.GetGator(); - - [Benchmark] - public object GetHeikinAshi() => h.GetHeikinAshi(); - - [Benchmark] - public object GetHma() => h.GetHma(14); - - [Benchmark] - public object GetHtTrendline() => h.GetHtTrendline(); - - [Benchmark] - public object GetHurst() => h.GetHurst(); - - [Benchmark] - public object GetIchimoku() => h.GetIchimoku(); - - [Benchmark] - public object GetKama() => h.GetKama(); - - [Benchmark] - public object GetKlinger() => h.GetKvo(); - - [Benchmark] - public object GetKeltner() => h.GetKeltner(); - - [Benchmark] - public object GetKvo() => h.GetKvo(); - - [Benchmark] - public object GetMacd() => h.GetMacd(); - - [Benchmark] - public object GetMaEnvelopes() => h.GetMaEnvelopes(20, 2.5, MaType.SMA); - - [Benchmark] - public object GetMama() => h.GetMama(); - - [Benchmark] - public object GetMarubozu() => h.GetMarubozu(); - - [Benchmark] - public object GetMfi() => h.GetMfi(); - - [Benchmark] - public object GetObv() => h.GetObv(); - - [Benchmark] - public object GetObvWithSma() => h.GetObv(14); - - [Benchmark] - public object GetParabolicSar() => h.GetParabolicSar(); - - [Benchmark] - public object GetPivotPoints() => h.GetPivotPoints(PeriodSize.Month, PivotPointType.Standard); - - [Benchmark] - public object GetPivots() => h.GetPivots(); - - [Benchmark] - public object GetPmo() => h.GetPmo(); - - [Benchmark] - public object GetPrs() => h.GetPrs(ho); - - [Benchmark] - public object GetPrsWithSma() => h.GetPrs(ho, null, 5); - - [Benchmark] - public object GetPvo() => h.GetPvo(); - - [Benchmark] - public object GetRenko() => h.GetRenko(2.5m); - - [Benchmark] - public object GetRenkoAtr() => h.GetRenko(14); - - [Benchmark] - public object GetRoc() => h.GetRoc(20); - - [Benchmark] - public object GetRocWb() => h.GetRocWb(12, 3, 12); - - [Benchmark] - public object GetRocWithSma() => h.GetRoc(20, 14); - - [Benchmark] - public object GetRollingPivots() => h.GetRollingPivots(14, 1); - - [Benchmark] - public object GetRsi() => h.GetRsi(); - - [Benchmark] - public object GetSlope() => h.GetSlope(20); - - [Benchmark] - public object GetSma() => h.GetSma(10); - - [Benchmark] - public object GetSmaAnalysis() => h.GetSmaAnalysis(10); - - [Benchmark] - public object GetSmi() => h.GetSmi(5, 20, 5, 3); - - [Benchmark] - public object GetSmma() => h.GetSmma(10); - - [Benchmark] - public object GetStarcBands() => h.GetStarcBands(); - - [Benchmark] - public object GetStc() => h.GetStc(); - - [Benchmark] - public object GetStdDev() => h.GetStdDev(20); - - [Benchmark] - public object GetStdDevWithSma() => h.GetStdDev(20, 14); - - [Benchmark] - public object GetStdDevChannels() => h.GetStdDevChannels(); - - [Benchmark] - public object GetStoch() => h.GetStoch(); - - [Benchmark] - public object GetStochSMMA() => h.GetStoch(9, 3, 3, 3, 2, MaType.SMMA); - - [Benchmark] - public object GetStochRsi() => h.GetStochRsi(14, 14, 3); - - [Benchmark] - public object GetSuperTrend() => h.GetSuperTrend(); - - [Benchmark] - public object GetT3() => h.GetT3(); - - [Benchmark] - public object GetTema() => h.GetTema(14); - - [Benchmark] - public object GetTr() => h.GetTr(); - - [Benchmark] - public object GetTrix() => h.GetTrix(14); - - [Benchmark] - public object GetTrixWithSma() => h.GetTrix(14, 5); - - [Benchmark] - public object GetTsi() => h.GetTsi(); - - [Benchmark] - public object GetUlcerIndex() => h.GetUlcerIndex(); - - [Benchmark] - public object GetUltimate() => h.GetUltimate(); - - [Benchmark] - public object GetVolatilityStop() => h.GetVolatilityStop(); - - [Benchmark] - public object GetVortex() => h.GetVortex(14); - - [Benchmark] - public object GetVwap() => h.GetVwap(); - - [Benchmark] - public object GetVwma() => h.GetVwma(14); - - [Benchmark] - public object GetWilliamsR() => h.GetWilliamsR(); - - [Benchmark] - public object GetWma() => h.GetWma(14); - - [Benchmark] - public object GetZigZag() => h.GetZigZag(); -}