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();
-}