diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..b864276
Binary files /dev/null and b/.DS_Store differ
diff --git a/Dockerfile b/Dockerfile
index b8de174..b188160 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,9 +4,9 @@ RUN mkdir /intrinio
COPY . /intrinio
-WORKDIR /intrinio/IntrinioRealTimeSDK
+WORKDIR /intrinio/SampleApp
-RUN dotnet build IntrinioRealTimeSDK.csproj
+RUN dotnet build SampleApp.csproj
-CMD dotnet run IntrinioRealTimeSDK.csproj
+CMD dotnet run SampleApp.csproj
diff --git a/Intrinio.Realtime/CandleStick.cs b/Intrinio.Realtime/CandleStick.cs
new file mode 100644
index 0000000..681d492
--- /dev/null
+++ b/Intrinio.Realtime/CandleStick.cs
@@ -0,0 +1,106 @@
+using System.Globalization;
+
+namespace Intrinio.Realtime;
+
+using System;
+
+public abstract class CandleStick
+{
+ private readonly string _contract;
+ private readonly double _openTimestamp;
+ private readonly double _closeTimestamp;
+ private readonly IntervalType _interval;
+
+ public UInt32 Volume { get; set; }
+ public double High { get; set; }
+ public double Low { get; set; }
+ public double Close { get; set; }
+ public double Open { get; set; }
+ public double OpenTimestamp
+ {
+ get { return _openTimestamp; }
+ }
+ public double CloseTimestamp
+ {
+ get { return _closeTimestamp; }
+ }
+ public double FirstTimestamp { get; set; }
+ public double LastTimestamp { get; set; }
+ public bool Complete { get; set; }
+ public double Average { get; set; }
+ public double Change { get; set; }
+ public IntervalType Interval
+ {
+ get { return _interval; }
+ }
+
+ public CandleStick(UInt32 volume, double price, double openTimestamp, double closeTimestamp, IntervalType interval, double eventTime)
+ {
+ Volume = volume;
+ High = price;
+ Low = price;
+ Close = price;
+ Open = price;
+ _openTimestamp = openTimestamp;
+ _closeTimestamp = closeTimestamp;
+ FirstTimestamp = eventTime;
+ LastTimestamp = eventTime;
+ Complete = false;
+ Average = price;
+ Change = 0.0;
+ _interval = interval;
+ }
+
+ public CandleStick(UInt32 volume, double high, double low, double closePrice, double openPrice, double openTimestamp, double closeTimestamp, double firstTimestamp, double lastTimestamp, bool complete, double average, double change, IntervalType interval)
+ {
+ Volume = volume;
+ High = high;
+ Low = low;
+ Close = closePrice;
+ Open = openPrice;
+ _openTimestamp = openTimestamp;
+ _closeTimestamp = closeTimestamp;
+ FirstTimestamp = firstTimestamp;
+ LastTimestamp = lastTimestamp;
+ Complete = complete;
+ Average = average;
+ Change = change;
+ _interval = interval;
+ }
+
+ public void Merge(CandleStick candle)
+ {
+ Average = ((Convert.ToDouble(Volume) * Average) + (Convert.ToDouble(candle.Volume) * candle.Average)) / (Convert.ToDouble(Volume + candle.Volume));
+ Volume += candle.Volume;
+ High = High > candle.High ? High : candle.High;
+ Low = Low < candle.Low ? Low : candle.Low;
+ Close = LastTimestamp > candle.LastTimestamp ? Close : candle.Close;
+ Open = FirstTimestamp < candle.FirstTimestamp ? Open : candle.Open;
+ FirstTimestamp = candle.FirstTimestamp < FirstTimestamp ? candle.FirstTimestamp : FirstTimestamp;
+ LastTimestamp = candle.LastTimestamp > LastTimestamp ? candle.LastTimestamp : LastTimestamp;
+ Change = (Close - Open) / Open;
+ }
+
+ internal void Update(UInt32 volume, double price, double time)
+ {
+ Average = ((Convert.ToDouble(Volume) * Average) + (Convert.ToDouble(volume) * price)) / (Convert.ToDouble(Volume + volume));
+ Volume += volume;
+ High = price > High ? price : High;
+ Low = price < Low ? price : Low;
+ Close = time > LastTimestamp ? price : Close;
+ Open = time < FirstTimestamp ? price : Open;
+ FirstTimestamp = time < FirstTimestamp ? time : FirstTimestamp;
+ LastTimestamp = time > LastTimestamp ? time : LastTimestamp;
+ Change = (Close - Open) / Open;
+ }
+
+ internal void MarkComplete()
+ {
+ Complete = true;
+ }
+
+ internal void MarkIncomplete()
+ {
+ Complete = false;
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/ClientStats.cs b/Intrinio.Realtime/ClientStats.cs
new file mode 100644
index 0000000..c093386
--- /dev/null
+++ b/Intrinio.Realtime/ClientStats.cs
@@ -0,0 +1,47 @@
+namespace Intrinio.Realtime;
+
+using System;
+
+public class ClientStats
+{
+ private readonly UInt64 _socketDataMessages;
+ private readonly UInt64 _socketTextMessages;
+ private readonly int _queueDepth;
+ private readonly UInt64 _eventCount;
+ private readonly int _queueCapacity;
+ private readonly int _overflowQueueDepth;
+ private readonly int _overflowQueueCapacity;
+ private readonly int _droppedCount;
+ private readonly int _overflowCount;
+
+ public ClientStats(UInt64 socketDataMessages, UInt64 socketTextMessages, int queueDepth, UInt64 eventCount, int queueCapacity, int overflowQueueDepth, int overflowQueueCapacity, int droppedCount, int overflowCount)
+ {
+ _socketDataMessages = socketDataMessages;
+ _socketTextMessages = socketTextMessages;
+ _queueDepth = queueDepth;
+ _eventCount = eventCount;
+ _queueCapacity = queueCapacity;
+ _overflowQueueDepth = overflowQueueDepth;
+ _overflowQueueCapacity = overflowQueueCapacity;
+ _droppedCount = droppedCount;
+ _overflowCount = overflowCount;
+ }
+
+ public UInt64 SocketDataMessages { get { return _socketDataMessages; } }
+
+ public UInt64 SocketTextMessages { get { return _socketTextMessages; } }
+
+ public int QueueDepth { get { return _queueDepth; } }
+
+ public int QueueCapacity { get { return _queueCapacity; } }
+
+ public int OverflowQueueDepth { get { return _overflowQueueDepth; } }
+
+ public int OverflowQueueCapacity { get { return _overflowQueueCapacity; } }
+
+ public UInt64 EventCount { get { return _eventCount; } }
+
+ public int DroppedCount { get { return _droppedCount; } }
+
+ public int OverflowCount { get { return _overflowCount; } }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Composite/Delegates.cs b/Intrinio.Realtime/Composite/Delegates.cs
new file mode 100644
index 0000000..6c4ae4c
--- /dev/null
+++ b/Intrinio.Realtime/Composite/Delegates.cs
@@ -0,0 +1,20 @@
+namespace Intrinio.Realtime.Composite;
+
+using System;
+using System.Threading.Tasks;
+
+public delegate Task OnSupplementalDatumUpdated(String key, double datum, IDataCache dataCache);
+public delegate Task OnSecuritySupplementalDatumUpdated(String key, double datum, ISecurityData securityData, IDataCache dataCache);
+public delegate Task OnOptionsContractSupplementalDatumUpdated(String key, double datum, IOptionsContractData optionsContractData, ISecurityData securityData, IDataCache dataCache);
+
+public delegate Task OnEquitiesTradeUpdated(ISecurityData securityData, IDataCache dataCache);
+public delegate Task OnEquitiesQuoteUpdated(ISecurityData SecurityData, IDataCache DataCache);
+public delegate Task OnEquitiesTradeCandleStickUpdated(ISecurityData securityData, IDataCache dataCache);
+public delegate Task OnEquitiesQuoteCandleStickUpdated(ISecurityData SecurityData, IDataCache DataCache);
+
+public delegate Task OnOptionsTradeUpdated(IOptionsContractData optionsContractData, IDataCache dataCache, ISecurityData securityData);
+public delegate Task OnOptionsQuoteUpdated(IOptionsContractData optionsContractData, IDataCache dataCache, ISecurityData securityData);
+public delegate Task OnOptionsRefreshUpdated(IOptionsContractData optionsContractData, IDataCache dataCache, ISecurityData securityData);
+public delegate Task OnOptionsUnusualActivityUpdated(IOptionsContractData optionsContractData, IDataCache dataCache, ISecurityData securityData);
+public delegate Task OnOptionsTradeCandleStickUpdated(IOptionsContractData optionsContractData, IDataCache dataCache, ISecurityData securityData);
+public delegate Task OnOptionsQuoteCandleStickUpdated(IOptionsContractData optionsContractData, IDataCache dataCache, ISecurityData securityData);
\ No newline at end of file
diff --git a/Intrinio.Realtime/Composite/IDataCache.cs b/Intrinio.Realtime/Composite/IDataCache.cs
new file mode 100644
index 0000000..1c2b0c8
--- /dev/null
+++ b/Intrinio.Realtime/Composite/IDataCache.cs
@@ -0,0 +1,76 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Intrinio.Realtime.Composite;
+
+using System;
+
+///
+/// Not for Use yet. Subject to change.
+///
+public interface IDataCache
+{
+ double? GetsupplementaryDatum(string key);
+ Task SetsupplementaryDatum(string key, double? datum);
+ IReadOnlyDictionary AllSupplementaryData { get; }
+
+ ISecurityData GetSecurityData(string tickerSymbol);
+ IReadOnlyDictionary AllSecurityData { get; }
+
+ Intrinio.Realtime.Equities.Trade? GetLatestEquityTrade(string tickerSymbol);
+ Task SetEquityTrade(Intrinio.Realtime.Equities.Trade trade);
+
+ Intrinio.Realtime.Equities.Quote? GetLatestEquityQuote(string tickerSymbol);
+ Task SetEquityQuote(Intrinio.Realtime.Equities.Quote quote);
+
+ Intrinio.Realtime.Equities.TradeCandleStick? GetLatestEquityTradeCandleStick(string tickerSymbol);
+ Task SetEquityTradeCandleStick(Intrinio.Realtime.Equities.TradeCandleStick tradeCandleStick);
+
+ Intrinio.Realtime.Equities.QuoteCandleStick? GetLatestEquityAskQuoteCandleStick(string tickerSymbol);
+ Intrinio.Realtime.Equities.QuoteCandleStick? GetLatestEquityBidQuoteCandleStick(string tickerSymbol);
+ Task SetEquityQuoteCandleStick(Intrinio.Realtime.Equities.QuoteCandleStick quoteCandleStick);
+
+ IOptionsContractData GetOptionsContractData(string tickerSymbol, string contract);
+
+ Intrinio.Realtime.Options.Trade? GetLatestOptionsTrade(string tickerSymbol, string contract);
+ Task SetOptionsTrade(Intrinio.Realtime.Options.Trade trade);
+
+ Intrinio.Realtime.Options.Quote? GetLatestOptionsQuote(string tickerSymbol, string contract);
+ Task SetOptionsQuote(Intrinio.Realtime.Options.Quote quote);
+
+ Intrinio.Realtime.Options.Refresh? GetLatestOptionsRefresh(string tickerSymbol, string contract);
+ Task SetOptionsRefresh(Intrinio.Realtime.Options.Refresh refresh);
+
+ Intrinio.Realtime.Options.UnusualActivity? GetLatestOptionsUnusualActivity(string tickerSymbol, string contract);
+ Task SetOptionsUnusualActivity(Intrinio.Realtime.Options.UnusualActivity unusualActivity);
+
+ Intrinio.Realtime.Options.TradeCandleStick? GetLatestOptionsTradeCandleStick(string tickerSymbol, string contract);
+ Task SetOptionsTradeCandleStick(Intrinio.Realtime.Options.TradeCandleStick tradeCandleStick);
+
+ Intrinio.Realtime.Options.QuoteCandleStick? GetOptionsAskQuoteCandleStick(string tickerSymbol);
+ Intrinio.Realtime.Options.QuoteCandleStick? GetOptionsBidQuoteCandleStick(string tickerSymbol);
+ Task SetOptionsQuoteCandleStick(Intrinio.Realtime.Options.QuoteCandleStick quoteCandleStick);
+
+ double? GetSecuritySupplementalDatum(string tickerSymbol, string key);
+ Task SetSecuritySupplementalDatum(string tickerSymbol, string key, double? datum);
+
+ double? GetOptionsContractSupplementalDatum(string tickerSymbol, string contract, string key);
+ Task SetOptionSupplementalDatum(string tickerSymbol, string contract, string key, double? datum);
+
+ void SetOnSupplementalDatumUpdated(OnSupplementalDatumUpdated onSupplementalDatumUpdated);
+ void SetOnSecuritySupplementalDatumUpdated(OnSecuritySupplementalDatumUpdated onSecuritySupplementalDatumUpdated);
+ void SetOnOptionSupplementalDatumUpdated(OnOptionsContractSupplementalDatumUpdated onOptionsContractSupplementalDatumUpdated);
+
+ void SetOnEquitiesTradeUpdated(OnEquitiesTradeUpdated onEquitiesTradeUpdated);
+ void SetOnEquitiesQuoteUpdated(OnEquitiesQuoteUpdated onEquitiesQuoteUpdated);
+ void SetOnEquitiesTradeCandleStickUpdated(OnEquitiesTradeCandleStickUpdated onEquitiesTradeCandleStickUpdated);
+ void SetOnEquitiesQuoteCandleStickUpdated(OnEquitiesQuoteCandleStickUpdated onEquitiesQuoteCandleStickUpdated);
+
+ void SetOnOptionsTradeUpdated(OnOptionsTradeUpdated onOptionsTradeUpdated);
+ void SetOnOptionsQuoteUpdated(OnOptionsQuoteUpdated onOptionsQuoteUpdated);
+ void SetOnOptionsRefreshUpdated(OnOptionsRefreshUpdated onOptionsRefreshUpdated);
+ void SetOnOptionsUnusualActivityUpdated(OnOptionsUnusualActivityUpdated onOptionsUnusualActivityUpdated);
+ void SetOnOptionsTradeCandleStickUpdated(OnOptionsTradeCandleStickUpdated onOptionsTradeCandleStickUpdated);
+ void SetOnOptionsQuoteCandleStickUpdated(OnOptionsQuoteCandleStickUpdated onOptionsQuoteCandleStickUpdated);
+}
diff --git a/Intrinio.Realtime/Composite/IOptionsContractData.cs b/Intrinio.Realtime/Composite/IOptionsContractData.cs
new file mode 100644
index 0000000..416a8d8
--- /dev/null
+++ b/Intrinio.Realtime/Composite/IOptionsContractData.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Intrinio.Realtime.Composite;
+
+///
+/// Not for Use yet. Subject to change.
+///
+public interface IOptionsContractData {
+ string Contract { get; }
+
+ Intrinio.Realtime.Options.Trade? LatestTrade { get; }
+ Intrinio.Realtime.Options.Quote? LatestQuote { get; }
+ Intrinio.Realtime.Options.Refresh? LatestRefresh { get; }
+ Intrinio.Realtime.Options.UnusualActivity? LatestUnusualActivity { get; }
+ Intrinio.Realtime.Options.TradeCandleStick? LatestTradeCandleStick { get; }
+ Intrinio.Realtime.Options.QuoteCandleStick? LatestAskQuoteCandleStick { get; }
+ Intrinio.Realtime.Options.QuoteCandleStick? LatestBidQuoteCandleStick { get; }
+
+ Task SetTrade(Intrinio.Realtime.Options.Trade trade);
+ Task SetQuote(Intrinio.Realtime.Options.Quote quote);
+ Task SetRefresh(Intrinio.Realtime.Options.Refresh refresh);
+ Task SetUnusualActivity(Intrinio.Realtime.Options.UnusualActivity unusualActivity);
+ Task SetTradeCandleStick(Intrinio.Realtime.Options.TradeCandleStick tradeCandleStick);
+ Task SetQuoteCandleStick(Intrinio.Realtime.Options.QuoteCandleStick quoteCandleStick);
+
+ double? GetSupplementaryDatum(string key);
+ Task SetSupplementaryDatum(string key, double? datum);
+ IReadOnlyDictionary AllSupplementaryData { get; }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Composite/ISecurityData.cs b/Intrinio.Realtime/Composite/ISecurityData.cs
new file mode 100644
index 0000000..bb65a54
--- /dev/null
+++ b/Intrinio.Realtime/Composite/ISecurityData.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Intrinio.Realtime.Composite;
+
+///
+/// Not for Use yet. Subject to change.
+///
+public interface ISecurityData {
+ string TickerSymbol { get; }
+
+ double? GetSupplementaryDatum(string key);
+ Task SetSupplementaryDatum(string key, double? datum);
+ IReadOnlyDictionary AllSupplementaryData { get; }
+
+ Intrinio.Realtime.Equities.Trade? LatestEquitiesTrade { get; }
+ Intrinio.Realtime.Equities.Quote? LatestEquitiesQuote { get; }
+
+ Intrinio.Realtime.Equities.TradeCandleStick? LatestEquitiesTradeCandleStick { get; }
+ Intrinio.Realtime.Equities.QuoteCandleStick? LatestEquitiesAskQuoteCandleStick { get; }
+ Intrinio.Realtime.Equities.QuoteCandleStick? LatestEquitiesBidQuoteCandleStick { get; }
+
+ IOptionsContractData GetOptionsContractData(string contract);
+ IReadOnlyDictionary AllOptionsContractData { get; }
+ List GetContractNames(string ticker);
+
+ Task SetEquitiesTrade(Intrinio.Realtime.Equities.Trade trade);
+ Task SetEquitiesQuote(Intrinio.Realtime.Equities.Quote quote);
+
+ Task SetEquitiesTradeCandleStick(Intrinio.Realtime.Equities.TradeCandleStick tradeCandleStick);
+ Task SetEquitiesQuoteCandleStick(Intrinio.Realtime.Equities.QuoteCandleStick quoteCandleStick);
+
+ Intrinio.Realtime.Options.Trade? GetLatestOptionsContractTrade(string contract);
+ Task SetOptionsContractTrade(Intrinio.Realtime.Options.Trade trade);
+
+ Intrinio.Realtime.Options.Quote? GetLatestOptionsContractQuote(string contract);
+ Task SetOptionsContractQuote(Intrinio.Realtime.Options.Quote quote);
+
+ Intrinio.Realtime.Options.Refresh? GetLatestOptionsContractRefresh(string contract);
+ Task SetOptionsContractRefresh(Intrinio.Realtime.Options.Refresh refresh);
+
+ Intrinio.Realtime.Options.UnusualActivity? GetLatestOptionsContractUnusualActivity(string contract);
+ Task SetOptionsContractUnusualActivity(Intrinio.Realtime.Options.UnusualActivity unusualActivity);
+
+ double? GetOptionsContractSupplementalDatum(string contract, string key);
+ Task SetOptionsContractSupplementalDatum(string contract, string key, double? datum);
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Composite/OptionsContractData.cs b/Intrinio.Realtime/Composite/OptionsContractData.cs
new file mode 100644
index 0000000..57bcb4b
--- /dev/null
+++ b/Intrinio.Realtime/Composite/OptionsContractData.cs
@@ -0,0 +1,263 @@
+using System;
+using System.Threading.Tasks;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Intrinio.Realtime.Options;
+
+namespace Intrinio.Realtime.Composite;
+
+///
+/// Not for Use yet. Subject to change.
+///
+public class OptionsContractData : IOptionsContractData
+{
+ private readonly String contract;
+ private Intrinio.Realtime.Options.Trade? _latestTrade;
+ private Intrinio.Realtime.Options.Quote? _latestQuote;
+ private Intrinio.Realtime.Options.Refresh? _latestRefresh;
+ private Intrinio.Realtime.Options.UnusualActivity? _latestUnusualActivity;
+ private Intrinio.Realtime.Options.TradeCandleStick? _latestTradeCandleStick;
+ private Intrinio.Realtime.Options.QuoteCandleStick? _latestAskQuoteCandleStick;
+ private Intrinio.Realtime.Options.QuoteCandleStick? _latestBidQuoteCandleStick;
+ private readonly ConcurrentDictionary _supplementaryData;
+ private readonly IReadOnlyDictionary _readonlySupplementaryData;
+
+ public OptionsContractData( String contract,
+ Intrinio.Realtime.Options.Trade latestTrade,
+ Intrinio.Realtime.Options.Quote latestQuote,
+ Intrinio.Realtime.Options.Refresh latestRefresh,
+ Intrinio.Realtime.Options.UnusualActivity latestUnusualActivity,
+ Intrinio.Realtime.Options.TradeCandleStick latestTradeCandleStick,
+ Intrinio.Realtime.Options.QuoteCandleStick latestAskQuoteCandleStick,
+ Intrinio.Realtime.Options.QuoteCandleStick latestBidQuoteCandleStick)
+ {
+ this.contract = contract;
+ this._latestTrade = latestTrade;
+ this._latestQuote = latestQuote;
+ this._latestRefresh = latestRefresh;
+ this._latestUnusualActivity = latestUnusualActivity;
+ this._latestTradeCandleStick = latestTradeCandleStick;
+ this._latestAskQuoteCandleStick = latestAskQuoteCandleStick;
+ this._latestBidQuoteCandleStick = latestBidQuoteCandleStick;
+ this._supplementaryData = new ConcurrentDictionary();
+ this._readonlySupplementaryData = new ReadOnlyDictionary(_supplementaryData);
+ }
+
+ public String Contract { get { return this.contract; } }
+
+ public Intrinio.Realtime.Options.Trade? LatestTrade { get { return this._latestTrade; } }
+
+ public Intrinio.Realtime.Options.Quote? LatestQuote { get { return this._latestQuote; } }
+
+ public Intrinio.Realtime.Options.Refresh? LatestRefresh { get { return this._latestRefresh; } }
+
+ public Intrinio.Realtime.Options.UnusualActivity? LatestUnusualActivity { get { return this._latestUnusualActivity; } }
+
+ public Intrinio.Realtime.Options.TradeCandleStick? LatestTradeCandleStick { get { return this._latestTradeCandleStick; } }
+
+ public Intrinio.Realtime.Options.QuoteCandleStick? LatestAskQuoteCandleStick { get { return this._latestAskQuoteCandleStick; } }
+
+ public Intrinio.Realtime.Options.QuoteCandleStick? LatestBidQuoteCandleStick { get { return this._latestBidQuoteCandleStick; } }
+
+ public Task SetTrade(Intrinio.Realtime.Options.Trade trade)
+ {
+ //dirty set
+ if ((!_latestTrade.HasValue) || (trade.Timestamp > _latestTrade.Value.Timestamp))
+ {
+ _latestTrade = trade;
+ return Task.FromResult(true);
+ }
+ return Task.FromResult(false);
+ }
+
+ internal async Task SetTrade(Intrinio.Realtime.Options.Trade trade, OnOptionsTradeUpdated onOptionsTradeUpdated, ISecurityData securityData, IDataCache dataCache)
+ {
+ bool isSet = await SetTrade(trade);
+ if (isSet && onOptionsTradeUpdated != null)
+ {
+ try
+ {
+ await onOptionsTradeUpdated(this, dataCache, securityData);
+ }
+ catch (Exception e)
+ {
+ Logging.Log(LogLevel.ERROR, "Error in OnOptionsTradeUpdated Callback: {0}", e.Message);
+ }
+ }
+ return isSet;
+ }
+
+ public Task SetQuote(Intrinio.Realtime.Options.Quote quote)
+ {
+ //dirty set
+ if ((!_latestQuote.HasValue) || (quote.Timestamp > _latestQuote.Value.Timestamp))
+ {
+ _latestQuote = quote;
+ return Task.FromResult(true);
+ }
+ return Task.FromResult(false);
+ }
+
+ internal async Task SetQuote(Intrinio.Realtime.Options.Quote quote, OnOptionsQuoteUpdated onOptionsQuoteUpdated, ISecurityData securityData, IDataCache dataCache)
+ {
+ bool isSet = await this.SetQuote(quote);
+ if (isSet && onOptionsQuoteUpdated != null)
+ {
+ try
+ {
+ await onOptionsQuoteUpdated(this, dataCache, securityData);
+ }
+ catch (Exception e)
+ {
+ Logging.Log(LogLevel.ERROR, "Error in onOptionsQuoteUpdated Callback: {0}", e.Message);
+ }
+ }
+ return isSet;
+ }
+
+ public Task SetRefresh(Intrinio.Realtime.Options.Refresh refresh)
+ {
+ _latestRefresh = refresh;
+ return Task.FromResult(true);
+ }
+
+ internal async Task SetRefresh(Intrinio.Realtime.Options.Refresh refresh, OnOptionsRefreshUpdated onOptionsRefreshUpdated, ISecurityData securityData, IDataCache dataCache)
+ {
+ bool isSet = await this.SetRefresh(refresh);
+ if (isSet && onOptionsRefreshUpdated != null)
+ {
+ try
+ {
+ await onOptionsRefreshUpdated(this, dataCache, securityData);
+ }
+ catch (Exception e)
+ {
+ Logging.Log(LogLevel.ERROR, "Error in onOptionsRefreshUpdated Callback: {0}", e.Message);
+ }
+ }
+ return isSet;
+ }
+
+ public Task SetUnusualActivity(Intrinio.Realtime.Options.UnusualActivity unusualActivity)
+ {
+ _latestUnusualActivity = unusualActivity;
+ return Task.FromResult(true);
+ }
+
+ internal async Task SetUnusualActivity(Intrinio.Realtime.Options.UnusualActivity unusualActivity, OnOptionsUnusualActivityUpdated onOptionsUnusualActivityUpdated, ISecurityData securityData, IDataCache dataCache)
+ {
+ bool isSet = await this.SetUnusualActivity(unusualActivity);
+ if (isSet && onOptionsUnusualActivityUpdated != null)
+ {
+ try
+ {
+ await onOptionsUnusualActivityUpdated(this, dataCache, securityData);
+ }
+ catch (Exception e)
+ {
+ Logging.Log(LogLevel.ERROR, "Error in onOptionsUnusualActivityUpdated Callback: {0}", e.Message);
+ }
+ }
+ return isSet;
+ }
+
+ public Task SetTradeCandleStick(Intrinio.Realtime.Options.TradeCandleStick tradeCandleStick)
+ {
+ //dirty set
+ if ((_latestTradeCandleStick == null) || (tradeCandleStick.OpenTimestamp > _latestTradeCandleStick.OpenTimestamp) || (tradeCandleStick.LastTimestamp > _latestTradeCandleStick.LastTimestamp))
+ {
+ _latestTradeCandleStick = tradeCandleStick;
+ return Task.FromResult(true);
+ }
+ return Task.FromResult(false);
+ }
+
+ internal async Task SetTradeCandleStick(Intrinio.Realtime.Options.TradeCandleStick tradeCandleStick, OnOptionsTradeCandleStickUpdated onOptionsTradeCandleStickUpdated, ISecurityData securityData, IDataCache dataCache)
+ {
+ bool isSet = await SetTradeCandleStick(tradeCandleStick);
+ if (isSet && onOptionsTradeCandleStickUpdated != null)
+ {
+ try
+ {
+ await onOptionsTradeCandleStickUpdated(this, dataCache, securityData);
+ }
+ catch (Exception e)
+ {
+ Logging.Log(LogLevel.ERROR, "Error in OnOptionsTradeCandleStickUpdated Callback: {0}", e.Message);
+ }
+ }
+ return isSet;
+ }
+
+ public Task SetQuoteCandleStick(Intrinio.Realtime.Options.QuoteCandleStick quoteCandleStick)
+ {
+ switch (quoteCandleStick.QuoteType)
+ {
+ case QuoteType.Ask:
+ //dirty set
+ if ((_latestAskQuoteCandleStick == null) || (quoteCandleStick.OpenTimestamp > _latestAskQuoteCandleStick.OpenTimestamp) || (quoteCandleStick.LastTimestamp > _latestAskQuoteCandleStick.LastTimestamp))
+ {
+ _latestAskQuoteCandleStick = quoteCandleStick;
+ return Task.FromResult(true);
+ }
+ return Task.FromResult(false);
+ case QuoteType.Bid:
+ //dirty set
+ if ((_latestBidQuoteCandleStick == null) || (quoteCandleStick.OpenTimestamp > _latestBidQuoteCandleStick.OpenTimestamp) || (quoteCandleStick.LastTimestamp > _latestBidQuoteCandleStick.LastTimestamp))
+ {
+ _latestBidQuoteCandleStick = quoteCandleStick;
+ return Task.FromResult(true);
+ }
+ return Task.FromResult(false);
+ default:
+ return Task.FromResult(false);
+ }
+ }
+
+ internal async Task SetQuoteCandleStick(Intrinio.Realtime.Options.QuoteCandleStick quoteCandleStick, OnOptionsQuoteCandleStickUpdated onOptionsQuoteCandleStickUpdated, ISecurityData securityData, IDataCache dataCache)
+ {
+ bool isSet = await this.SetQuoteCandleStick(quoteCandleStick);
+ if (isSet && onOptionsQuoteCandleStickUpdated != null)
+ {
+ try
+ {
+ await onOptionsQuoteCandleStickUpdated(this, dataCache, securityData);
+ }
+ catch (Exception e)
+ {
+ Logging.Log(LogLevel.ERROR, "Error in onOptionsQuoteCandleStickUpdated Callback: {0}", e.Message);
+ }
+ }
+ return isSet;
+ }
+
+ public double? GetSupplementaryDatum(String key)
+ {
+ return _supplementaryData.GetValueOrDefault(key, null);
+ }
+
+ public Task SetSupplementaryDatum(String key, double? datum)
+ {
+ return Task.FromResult(datum == _supplementaryData.AddOrUpdate(key, datum, (key, oldValue) => datum));
+ }
+
+ internal async Task SetSupplementaryDatum(String key, double datum, OnOptionsContractSupplementalDatumUpdated onOptionsContractSupplementalDatumUpdated, ISecurityData securityData, IDataCache dataCache)
+ {
+ bool result = await SetSupplementaryDatum(key, datum);
+ if (result && onOptionsContractSupplementalDatumUpdated != null)
+ {
+ try
+ {
+ await onOptionsContractSupplementalDatumUpdated(key, datum, this, securityData, dataCache);
+ }
+ catch (Exception e)
+ {
+ Logging.Log(LogLevel.ERROR, "Error in onOptionsContractSupplementalDatumUpdated Callback: {0}", e.Message);
+ }
+ }
+ return result;
+ }
+
+ public IReadOnlyDictionary AllSupplementaryData{ get { return _readonlySupplementaryData; } }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/DropOldestRingBuffer.cs b/Intrinio.Realtime/DropOldestRingBuffer.cs
new file mode 100644
index 0000000..038f263
--- /dev/null
+++ b/Intrinio.Realtime/DropOldestRingBuffer.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace Intrinio.Realtime;
+
+internal class DropOldestRingBuffer
+{
+ #region Data Members
+ private readonly byte[] _data;
+ private uint _blockNextReadIndex;
+ private uint _blockNextWriteIndex;
+ private readonly object _readLock;
+ private readonly object _writeLock;
+ private ulong _count;
+ private readonly uint _blockSize;
+ private readonly uint _blockCapacity;
+ private ulong _dropCount;
+
+ public ulong Count { get { return Interlocked.Read(ref _count); } }
+ public uint BlockSize { get { return _blockSize; } }
+ public uint BlockCapacity { get { return _blockCapacity; } }
+ public ulong DropCount { get { return Interlocked.Read(ref _dropCount); } }
+
+ public bool IsEmpty
+ {
+ get
+ {
+ return IsEmptyNoLock();
+ }
+ }
+
+ public bool IsFull
+ {
+ get
+ {
+ return IsFullNoLock();
+ }
+ }
+ #endregion //Data Members
+
+ #region Constructors
+
+ public DropOldestRingBuffer(uint blockSize, uint blockCapacity)
+ {
+ _blockSize = blockSize;
+ _blockCapacity = blockCapacity;
+ _blockNextReadIndex = 0u;
+ _blockNextWriteIndex = 0u;
+ _count = 0u;
+ _dropCount = 0UL;
+ _readLock = new object();
+ _writeLock = new object();
+ _data = new byte[blockSize * blockCapacity];
+ }
+
+ #endregion //Constructors
+
+ ///
+ /// blockToWrite MUST be of length BlockSize!
+ ///
+ ///
+ public void Enqueue(ReadOnlySpan blockToWrite)
+ {
+ lock (_writeLock)
+ {
+ if (IsFullNoLock())
+ {
+ lock (_readLock)
+ {
+ if (IsFullNoLock())
+ {
+ _blockNextReadIndex = (++_blockNextReadIndex) % BlockCapacity;
+ Interlocked.Increment(ref _dropCount);
+ }
+ }
+ }
+
+ Span target = new Span(_data, Convert.ToInt32(_blockNextWriteIndex * BlockSize), Convert.ToInt32(BlockSize));
+ blockToWrite.CopyTo(target);
+
+ _blockNextWriteIndex = (++_blockNextWriteIndex) % BlockCapacity;
+ Interlocked.Increment(ref _count);
+ }
+ }
+
+ ///
+ /// blockBuffer MUST be of length BlockSize!
+ ///
+ ///
+ public bool TryDequeue(Span blockBuffer)
+ {
+ lock (_readLock)
+ {
+ if (IsEmptyNoLock())
+ return false;
+
+ Span target = new Span(_data, Convert.ToInt32(_blockNextReadIndex * BlockSize), Convert.ToInt32(BlockSize));
+ target.CopyTo(blockBuffer);
+
+ _blockNextReadIndex = (++_blockNextReadIndex) % BlockCapacity;
+ Interlocked.Decrement(ref _count);
+ return true;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool IsFullNoLock()
+ {
+ return Interlocked.Read(ref _count) == _blockCapacity;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool IsEmptyNoLock()
+ {
+ return Interlocked.Read(ref _count) == 0UL;
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/CandleStickClient.cs b/Intrinio.Realtime/Equities/CandleStickClient.cs
new file mode 100644
index 0000000..f6d707f
--- /dev/null
+++ b/Intrinio.Realtime/Equities/CandleStickClient.cs
@@ -0,0 +1,720 @@
+using System.Threading.Tasks;
+
+namespace Intrinio.Realtime.Equities;
+
+using Intrinio;
+using Serilog;
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Runtime.CompilerServices;
+
+public delegate TradeCandleStick FetchHistoricalTradeCandleStick(string symbol, double openTimestamp, double closeTimestamp, IntervalType interval);
+public delegate QuoteCandleStick FetchHistoricalQuoteCandleStick(string symbol, double openTimestamp, double closeTimestamp, QuoteType quoteType, IntervalType interval);
+
+public class CandleStickClient
+{
+ #region Data Members
+ private readonly IntervalType _interval;
+ private readonly bool _broadcastPartialCandles;
+ private readonly double _sourceDelaySeconds;
+ private readonly bool _useTradeFiltering;
+ private readonly CancellationTokenSource _ctSource;
+ private const int InitialDictionarySize = 31_387;//3_601_579; //a close prime number greater than 2x the max expected size. There are usually around 1.5m option contracts.
+ private readonly object _symbolBucketsLock;
+ private readonly object _lostAndFoundLock;
+ private readonly Dictionary _symbolBuckets;
+ private readonly Dictionary _lostAndFound;
+ private const double FlushBufferSeconds = 30.0;
+ private readonly Thread _lostAndFoundThread;
+ private readonly Thread _flushThread;
+
+ private bool UseOnTradeCandleStick { get { return !ReferenceEquals(OnTradeCandleStick, null); } }
+ private bool UseOnQuoteCandleStick { get { return !ReferenceEquals(OnQuoteCandleStick, null); } }
+ private bool UseGetHistoricalTradeCandleStick { get { return !ReferenceEquals(GetHistoricalTradeCandleStick,null); } }
+ private bool UseGetHistoricalQuoteCandleStick { get { return !ReferenceEquals(GetHistoricalQuoteCandleStick,null); } }
+
+ ///
+ /// The callback used for broadcasting trade candles.
+ ///
+ public Action OnTradeCandleStick { get; set; }
+
+ ///
+ /// The callback used for broadcasting quote candles.
+ ///
+ private Action OnQuoteCandleStick { get; set; }
+
+ ///
+ /// Fetch a previously broadcasted trade candlestick from the given unique parameters.
+ ///
+ public FetchHistoricalTradeCandleStick GetHistoricalTradeCandleStick { get; set; }
+
+ ///
+ /// Fetch a previously broadcasted quote candlestick from the given unique parameters.
+ ///
+ public FetchHistoricalQuoteCandleStick GetHistoricalQuoteCandleStick { get; set; }
+ #endregion //Data Members
+
+ #region Constructors
+
+ ///
+ /// Creates an equities CandleStickClient that creates trade and quote candlesticks from a stream of trades and quotes.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public CandleStickClient(
+ Action onTradeCandleStick,
+ Action onQuoteCandleStick,
+ IntervalType interval,
+ bool broadcastPartialCandles,
+ FetchHistoricalTradeCandleStick getHistoricalTradeCandleStick,
+ FetchHistoricalQuoteCandleStick getHistoricalQuoteCandleStick,
+ double sourceDelaySeconds,
+ bool useTradeFiltering)
+ {
+ this.OnTradeCandleStick = onTradeCandleStick;
+ this.OnQuoteCandleStick = onQuoteCandleStick;
+ this._interval = interval;
+ this._broadcastPartialCandles = broadcastPartialCandles;
+ this.GetHistoricalTradeCandleStick = getHistoricalTradeCandleStick;
+ this.GetHistoricalQuoteCandleStick = getHistoricalQuoteCandleStick;
+ this._sourceDelaySeconds = sourceDelaySeconds;
+ this._useTradeFiltering = useTradeFiltering;
+ _ctSource = new CancellationTokenSource();
+ _symbolBucketsLock = new object();
+ _lostAndFoundLock = new object();
+ _symbolBuckets = new Dictionary(InitialDictionarySize);
+ _lostAndFound = new Dictionary(InitialDictionarySize);
+ _lostAndFoundThread = new Thread(new ThreadStart(LostAndFoundFn));
+ _flushThread = new Thread(new ThreadStart(FlushFn));
+ }
+ #endregion //Constructors
+
+ #region Public Methods
+
+ public void OnTrade(Trade trade)
+ {
+ try
+ {
+ if (UseOnTradeCandleStick && (!ShouldFilterTrade(trade, _useTradeFiltering)))
+ {
+ SymbolBucket bucket = GetSlot(trade.Symbol, _symbolBuckets, _symbolBucketsLock);
+
+ lock (bucket.Locker)
+ {
+ double ts = ConvertToTimestamp(trade.Timestamp);
+
+ if (bucket.TradeCandleStick != null)
+ {
+ if (bucket.TradeCandleStick.CloseTimestamp < ts)
+ {
+ bucket.TradeCandleStick.MarkComplete();
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ bucket.TradeCandleStick = CreateNewTradeCandle(trade, ts);
+ }
+ else if (bucket.TradeCandleStick.OpenTimestamp <= ts)
+ {
+ bucket.TradeCandleStick.Update(trade.Size, trade.Price, ts);
+ if (_broadcastPartialCandles)
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ }
+ else //This is a late trade. We already shipped the candle, so add to lost and found
+ {
+ AddTradeToLostAndFound(trade);
+ }
+ }
+ else
+ {
+ bucket.TradeCandleStick = CreateNewTradeCandle(trade, ts);
+ if (_broadcastPartialCandles)
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Warning("Error on handling trade in CandleStick Client: {0}", e.Message);
+ }
+ }
+
+ public void OnQuote(Quote quote)
+ {
+ try
+ {
+ if (UseOnQuoteCandleStick && (!(ShouldFilterQuote(quote, _useTradeFiltering))))
+ {
+ SymbolBucket bucket = GetSlot(quote.Symbol, _symbolBuckets, _symbolBucketsLock);
+
+ lock (bucket.Locker)
+ {
+ if (quote.Type == QuoteType.Ask)
+ {
+ OnAsk(quote, bucket);
+ }
+
+ if (quote.Type == QuoteType.Bid)
+ {
+ OnBid(quote, bucket);
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Warning("Error on handling trade in CandleStick Client: {0}", e.Message);
+ }
+ }
+
+ public void Start()
+ {
+ if (!_flushThread.IsAlive)
+ {
+ _flushThread.Start();
+ }
+
+ if (!_lostAndFoundThread.IsAlive)
+ {
+ _lostAndFoundThread.Start();
+ }
+ }
+
+ public void Stop()
+ {
+ _ctSource.Cancel();
+ }
+ #endregion //Public Methods
+
+ #region Private Methods
+
+ private TradeCandleStick CreateNewTradeCandle(Trade trade, double timestamp)
+ {
+ double start = GetNearestModInterval(timestamp, _interval);
+ TradeCandleStick freshCandle = new TradeCandleStick(trade.Symbol, trade.Size, trade.Price, start, (start + System.Convert.ToDouble((int)_interval)), _interval, timestamp);
+
+ if (UseGetHistoricalTradeCandleStick && UseOnTradeCandleStick)
+ {
+ try
+ {
+ TradeCandleStick historical = GetHistoricalTradeCandleStick(freshCandle.Symbol, freshCandle.OpenTimestamp, freshCandle.CloseTimestamp, freshCandle.Interval);
+ if (ReferenceEquals(historical,null))
+ return freshCandle;
+ historical.MarkIncomplete();
+ return MergeTradeCandles(historical, freshCandle);
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical TradeCandleStick: {0}; trade: {1}", e.Message, trade);
+ return freshCandle;
+ }
+ }
+ else
+ {
+ return freshCandle;
+ }
+ }
+
+ private QuoteCandleStick CreateNewQuoteCandle(Quote quote, double timestamp)
+ {
+ double start = GetNearestModInterval(timestamp, _interval);
+ QuoteCandleStick freshCandle = new QuoteCandleStick(quote.Symbol, quote.Size, quote.Price, quote.Type, start, (start + System.Convert.ToDouble((int)_interval)), _interval, timestamp);
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick)
+ {
+ try
+ {
+ QuoteCandleStick historical = GetHistoricalQuoteCandleStick.Invoke(freshCandle.Symbol, freshCandle.OpenTimestamp, freshCandle.CloseTimestamp, freshCandle.QuoteType, freshCandle.Interval);
+ if (ReferenceEquals(historical,null))
+ return freshCandle;
+ historical.MarkIncomplete();
+ return MergeQuoteCandles(historical, freshCandle);
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical QuoteCandleStick: {0}; quote: {1}", e.Message, quote);
+ return freshCandle;
+ }
+ }
+ else
+ {
+ return freshCandle;
+ }
+ }
+
+ private void AddAskToLostAndFound(Quote ask)
+ {
+ double ts = ConvertToTimestamp(ask.Timestamp);
+ string key = String.Format("{0}|{1}|{2}", ask.Symbol, GetNearestModInterval(ts, _interval), _interval);
+ SymbolBucket bucket = GetSlot(key, _lostAndFound, _lostAndFoundLock);
+ try
+ {
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick)
+ {
+ lock (bucket.Locker)
+ {
+ if (bucket.AskCandleStick != null)
+ {
+ bucket.AskCandleStick.Update(ask.Size, ask.Price, ts);
+ }
+ else
+ {
+ double start = GetNearestModInterval(ts, _interval);
+ bucket.AskCandleStick = new QuoteCandleStick(ask.Symbol, ask.Size, ask.Price, QuoteType.Ask, start, (start + System.Convert.ToDouble((int)_interval)), _interval, ts);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning("Error on handling late ask in CandleStick Client: {0}", ex.Message);
+ }
+ }
+
+ private void AddBidToLostAndFound(Quote bid)
+ {
+ double ts = ConvertToTimestamp(bid.Timestamp);
+ string key = String.Format("{0}|{1}|{2}", bid.Symbol, GetNearestModInterval(ts, _interval), _interval);
+ SymbolBucket bucket = GetSlot(key, _lostAndFound, _lostAndFoundLock);
+ try
+ {
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick)
+ {
+ lock (bucket.Locker)
+ {
+ if (bucket.BidCandleStick != null)
+ {
+ bucket.BidCandleStick.Update(bid.Size, bid.Price, ts);
+ }
+ else
+ {
+ double start = GetNearestModInterval(ts, _interval);
+ bucket.BidCandleStick = new QuoteCandleStick(bid.Symbol, bid.Size, bid.Price, QuoteType.Bid, start, (start + System.Convert.ToDouble((int) _interval)), _interval, ts);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning("Error on handling late bid in CandleStick Client: {0}", ex.Message);
+ }
+ }
+
+ private void AddTradeToLostAndFound(Trade trade)
+ {
+ double ts = ConvertToTimestamp(trade.Timestamp);
+ string key = String.Format("{0}|{1}|{2}", trade.Symbol, GetNearestModInterval(ts, _interval), _interval);
+ SymbolBucket bucket = GetSlot(key, _lostAndFound, _lostAndFoundLock);
+ try
+ {
+ if (UseGetHistoricalTradeCandleStick && UseOnTradeCandleStick)
+ {
+ lock (bucket.Locker)
+ {
+ if (bucket.TradeCandleStick != null)
+ {
+ bucket.TradeCandleStick.Update(trade.Size, trade.Price, ts);
+ }
+ else
+ {
+ double start = GetNearestModInterval(ts, _interval);
+ bucket.TradeCandleStick = new TradeCandleStick(trade.Symbol, trade.Size, trade.Price, start, (start + System.Convert.ToDouble((int)_interval)), _interval, ts);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning("Error on handling late trade in CandleStick Client: {0}", ex.Message);
+ }
+ }
+
+ private void OnAsk(Quote quote, SymbolBucket bucket)
+ {
+ double ts = ConvertToTimestamp(quote.Timestamp);
+
+ if (bucket.AskCandleStick != null && !Double.IsNaN(quote.Price))
+ {
+ if (bucket.AskCandleStick.CloseTimestamp < ts)
+ {
+ bucket.AskCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ bucket.AskCandleStick = CreateNewQuoteCandle(quote, ts);
+ }
+ else if (bucket.AskCandleStick.OpenTimestamp <= ts)
+ {
+ bucket.AskCandleStick.Update(quote.Size, quote.Price, ts);
+ if (_broadcastPartialCandles)
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ }
+ else //This is a late event. We already shipped the candle, so add to lost and found
+ {
+ AddAskToLostAndFound(quote);
+ }
+ }
+ else if (bucket.AskCandleStick == null && !Double.IsNaN(quote.Price))
+ {
+ bucket.AskCandleStick = CreateNewQuoteCandle(quote, ts);
+ if (_broadcastPartialCandles)
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ }
+ }
+
+ private void OnBid(Quote quote, SymbolBucket bucket)
+ {
+ double ts = ConvertToTimestamp(quote.Timestamp);
+
+ if (bucket.BidCandleStick != null && !Double.IsNaN(quote.Price))
+ {
+ if (bucket.BidCandleStick.CloseTimestamp < ts)
+ {
+ bucket.BidCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ bucket.BidCandleStick = CreateNewQuoteCandle(quote, ts);
+ }
+ else if(bucket.BidCandleStick.OpenTimestamp <= ts)
+ {
+ bucket.BidCandleStick.Update(quote.Size, quote.Price, ts);
+ if (_broadcastPartialCandles)
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ }
+ else //This is a late event. We already shipped the candle, so add to lost and found
+ {
+ AddBidToLostAndFound(quote);
+ }
+ }
+ else if (bucket.BidCandleStick == null && !Double.IsNaN(quote.Price))
+ {
+ bucket.BidCandleStick = CreateNewQuoteCandle(quote, ts);
+ if (_broadcastPartialCandles)
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ }
+ }
+
+ private void FlushFn()
+ {
+ Log.Information("Starting candlestick expiration watcher...");
+ CancellationToken ct = _ctSource.Token;
+ System.Threading.Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
+ List keys = new List();
+
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ lock (_symbolBucketsLock)
+ {
+ foreach (string key in _symbolBuckets.Keys)
+ keys.Add(key);
+ }
+
+ foreach (string key in keys)
+ {
+ SymbolBucket bucket = GetSlot(key, _symbolBuckets, _symbolBucketsLock);
+ double flushThresholdTime = GetCurrentTimestamp(_sourceDelaySeconds) - FlushBufferSeconds;
+
+ lock (bucket.Locker)
+ {
+ if (UseOnTradeCandleStick && bucket.TradeCandleStick != null && (bucket.TradeCandleStick.CloseTimestamp < flushThresholdTime))
+ {
+ bucket.TradeCandleStick.MarkComplete();
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ bucket.TradeCandleStick = null;
+ }
+
+ if (UseOnQuoteCandleStick && bucket.AskCandleStick != null && (bucket.AskCandleStick.CloseTimestamp < flushThresholdTime))
+ {
+ bucket.AskCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ bucket.AskCandleStick = null;
+ }
+
+ if (UseOnQuoteCandleStick && bucket.BidCandleStick != null && (bucket.BidCandleStick.CloseTimestamp < flushThresholdTime))
+ {
+ bucket.BidCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ bucket.BidCandleStick = null;
+ }
+ }
+ }
+ keys.Clear();
+
+ if (!(ct.IsCancellationRequested))
+ Thread.Sleep(1000);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ }
+
+ Log.Information("Stopping candlestick expiration watcher...");
+ }
+
+ private async void LostAndFoundFn()
+ {
+ Log.Information("Starting candlestick late event watcher...");
+ CancellationToken ct = _ctSource.Token;
+ System.Threading.Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
+ List keys = new List();
+
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ lock (_lostAndFoundLock)
+ {
+ foreach (string key in _lostAndFound.Keys)
+ keys.Add(key);
+ }
+
+ foreach (string key in keys)
+ {
+ SymbolBucket bucket = GetSlot(key, _lostAndFound, _lostAndFoundLock);
+
+ lock (bucket.Locker)
+ {
+ if (UseGetHistoricalTradeCandleStick && UseOnTradeCandleStick && bucket.TradeCandleStick != null)
+ {
+ try
+ {
+ TradeCandleStick historical = GetHistoricalTradeCandleStick.Invoke(bucket.TradeCandleStick.Symbol, bucket.TradeCandleStick.OpenTimestamp, bucket.TradeCandleStick.CloseTimestamp, bucket.TradeCandleStick.Interval);
+ if (ReferenceEquals(historical,null))
+ {
+ bucket.TradeCandleStick.MarkComplete();
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ bucket.TradeCandleStick = null;
+ }
+ else
+ {
+ bucket.TradeCandleStick = MergeTradeCandles(historical, bucket.TradeCandleStick);
+ bucket.TradeCandleStick.MarkComplete();
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ bucket.TradeCandleStick = null;
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical TradeCandleStick: {0}", e.Message);
+ bucket.TradeCandleStick.MarkComplete();
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ bucket.TradeCandleStick = null;
+ }
+ }
+ else
+ {
+ bucket.TradeCandleStick = null;
+ }
+
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick && bucket.AskCandleStick != null)
+ {
+ try
+ {
+ QuoteCandleStick historical = GetHistoricalQuoteCandleStick.Invoke(bucket.AskCandleStick.Symbol, bucket.AskCandleStick.OpenTimestamp, bucket.AskCandleStick.CloseTimestamp, bucket.AskCandleStick.QuoteType, bucket.AskCandleStick.Interval);
+ if (ReferenceEquals(historical,null))
+ {
+ bucket.AskCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ bucket.AskCandleStick = null;
+ }
+ else
+ {
+ bucket.AskCandleStick = MergeQuoteCandles(historical, bucket.AskCandleStick);
+ bucket.AskCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ bucket.AskCandleStick = null;
+ }
+
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical QuoteCandleStick: {0}", e.Message);
+ bucket.AskCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ bucket.AskCandleStick = null;
+ }
+ }
+ else
+ {
+ bucket.AskCandleStick = null;
+ }
+
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick && bucket.BidCandleStick != null)
+ {
+ try
+ {
+ QuoteCandleStick historical = GetHistoricalQuoteCandleStick.Invoke(bucket.BidCandleStick.Symbol, bucket.BidCandleStick.OpenTimestamp, bucket.BidCandleStick.CloseTimestamp, bucket.BidCandleStick.QuoteType, bucket.BidCandleStick.Interval);
+ if (ReferenceEquals(historical,null))
+ {
+ bucket.BidCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ bucket.BidCandleStick = null;
+ }
+ else
+ {
+ bucket.BidCandleStick = MergeQuoteCandles(historical, bucket.BidCandleStick);
+ bucket.BidCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ bucket.BidCandleStick = null;
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical QuoteCandleStick: {0}", e.Message);
+ bucket.BidCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ bucket.BidCandleStick = null;
+ }
+ }
+ else
+ {
+ bucket.BidCandleStick = null;
+ }
+
+ if (bucket.TradeCandleStick == null && bucket.AskCandleStick == null && bucket.BidCandleStick == null)
+ RemoveSlot(key, _lostAndFound, _lostAndFoundLock);
+ }
+ }
+ keys.Clear();
+
+ if (!ct.IsCancellationRequested)
+ Thread.Sleep(1000);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ }
+
+ Log.Information("Stopping candlestick late event watcher...");
+ }
+
+ #endregion //Private Methods
+
+ private class SymbolBucket
+ {
+ public TradeCandleStick TradeCandleStick;
+ public QuoteCandleStick AskCandleStick;
+ public QuoteCandleStick BidCandleStick;
+ public object Locker;
+
+ public SymbolBucket(TradeCandleStick tradeCandleStick, QuoteCandleStick askCandleStick, QuoteCandleStick bidCandleStick)
+ {
+ TradeCandleStick = tradeCandleStick;
+ AskCandleStick = askCandleStick;
+ BidCandleStick = bidCandleStick;
+ Locker = new object();
+ }
+ }
+
+ #region Private Static Methods
+ // [SkipLocalsInit]
+ // [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ // private static Span StackAlloc(int length) where T : unmanaged
+ // {
+ // unsafe
+ // {
+ // Span p = stackalloc T[length];
+ // return p;
+ // }
+ // }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static double GetCurrentTimestamp(double delay)
+ {
+ return (DateTime.UtcNow - DateTime.UnixEpoch.ToUniversalTime()).TotalSeconds - delay;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static double GetNearestModInterval(double timestamp, IntervalType interval)
+ {
+ return Convert.ToDouble(Convert.ToUInt64(timestamp) / Convert.ToUInt64((int)interval)) * Convert.ToDouble(((int)interval));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static TradeCandleStick MergeTradeCandles(TradeCandleStick a, TradeCandleStick b)
+ {
+ a.Merge(b);
+ return a;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static QuoteCandleStick MergeQuoteCandles(QuoteCandleStick a, QuoteCandleStick b)
+ {
+ a.Merge(b);
+ return a;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsAnomalous(char marketCenter, string condition)
+ {
+ return marketCenter.Equals('L') && (condition.Equals("@ Zo", StringComparison.InvariantCultureIgnoreCase) ||
+ condition.Equals("@ To", StringComparison.InvariantCultureIgnoreCase) ||
+ condition.Equals("@ TW", StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsDarkPoolMarketCenter(char marketCenter)
+ {
+ return marketCenter.Equals((char)0) || Char.IsWhiteSpace(marketCenter) || marketCenter.Equals('D') || marketCenter.Equals('E');
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool ShouldFilterTrade(Trade incomingTrade, bool useFiltering)
+ {
+ return useFiltering && (IsDarkPoolMarketCenter(incomingTrade.MarketCenter) || IsAnomalous(incomingTrade.MarketCenter, incomingTrade.Condition));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool ShouldFilterQuote(Quote incomingQuote, bool useFiltering)
+ {
+ return useFiltering && IsDarkPoolMarketCenter(incomingQuote.MarketCenter);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static double ConvertToTimestamp(DateTime input)
+ {
+ return (input.ToUniversalTime() - DateTime.UnixEpoch.ToUniversalTime()).TotalSeconds;
+ }
+
+ private static SymbolBucket GetSlot(string key, Dictionary dict, object locker)
+ {
+ SymbolBucket value;
+ if (dict.TryGetValue(key, out value))
+ {
+ return value;
+ }
+
+ lock (locker)
+ {
+ if (dict.TryGetValue(key, out value))
+ {
+ return value;
+ }
+
+ SymbolBucket bucket = new SymbolBucket(null, null, null);
+ dict.Add(key, bucket);
+ return bucket;
+ }
+ }
+
+ private static void RemoveSlot(string key, Dictionary dict, object locker)
+ {
+ if (dict.ContainsKey(key))
+ {
+ lock (locker)
+ {
+ if (dict.ContainsKey(key))
+ {
+ dict.Remove(key);
+ }
+ }
+ }
+ }
+ #endregion //Private Static Methods
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/Config.cs b/Intrinio.Realtime/Equities/Config.cs
new file mode 100644
index 0000000..1b0a031
--- /dev/null
+++ b/Intrinio.Realtime/Equities/Config.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+
+namespace Intrinio.Realtime.Equities;
+
+using System;
+using Serilog;
+using System.IO;
+using Microsoft.Extensions.Configuration;
+
+public class Config
+{
+ public string ApiKey { get; set; }
+
+ public Provider Provider { get; set; }
+
+ public string IPAddress { get; set; }
+
+ public string[] Symbols { get; set; }
+
+ public bool TradesOnly { get; set; }
+
+ public int NumThreads { get; set; }
+
+ public int BufferSize { get; set; }
+
+ public int OverflowBufferSize { get; set; }
+
+ ///
+ /// The configuration for The Equities Websocket Client.
+ ///
+ public Config()
+ {
+ ApiKey = String.Empty;
+ Provider = Provider.NONE;
+ IPAddress = String.Empty;
+ Symbols = Array.Empty();
+ TradesOnly = false;
+ NumThreads = 2;
+ BufferSize = 2048;
+ OverflowBufferSize = 2048;
+ }
+
+ public void Validate()
+ {
+ if (String.IsNullOrWhiteSpace(ApiKey))
+ {
+ throw new ArgumentException("You must provide a valid API key");
+ }
+
+ if (Provider == Provider.NONE)
+ {
+ throw new ArgumentException("You must specify a valid 'provider'");
+ }
+
+ if ((Provider == Provider.MANUAL) && (String.IsNullOrWhiteSpace(IPAddress)))
+ {
+ throw new ArgumentException("You must specify an IP address for manual configuration");
+ }
+
+ if (NumThreads <= 0)
+ {
+ throw new ArgumentException("You must specify a valid 'NumThreads'");
+ }
+
+ if (BufferSize < 2048)
+ {
+ throw new ArgumentException("'BufferSize' must be greater than or equal to 2048.");
+ }
+
+ if (OverflowBufferSize < 2048)
+ {
+ throw new ArgumentException("'OverflowBufferSize' must be greater than or equal to 2048.");
+ }
+ }
+
+ public static Config LoadConfig()
+ {
+ Log.Information("Loading application configuration");
+ var rawConfig = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("config.json").Build();
+ Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(rawConfig).CreateLogger();
+ Config config = new Config();
+
+ foreach (KeyValuePair kvp in rawConfig.AsEnumerable())
+ {
+ Log.Debug("Key: {0}, Value:{1}", kvp.Key, kvp.Value);
+ }
+
+ rawConfig.Bind("Config", config);
+ config.Validate();
+ return config;
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/EquitiesWebSocketClient.cs b/Intrinio.Realtime/Equities/EquitiesWebSocketClient.cs
new file mode 100644
index 0000000..9617c98
--- /dev/null
+++ b/Intrinio.Realtime/Equities/EquitiesWebSocketClient.cs
@@ -0,0 +1,367 @@
+using System;
+using System.Text;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Intrinio.Realtime.Equities;
+
+public class EquitiesWebSocketClient : WebSocketClient, IEquitiesWebSocketClient
+{
+ #region Data Members
+
+ private const string LobbyName = "lobby";
+ private bool _useOnTrade;
+ private bool _useOnQuote;
+ private Action _onTrade;
+
+ ///
+ /// The callback for when a trade event occurs.
+ ///
+ public Action OnTrade
+ {
+ set
+ {
+ _useOnTrade = !ReferenceEquals(value, null);
+ _onTrade = value;
+ }
+ }
+
+ private Action _onQuote;
+
+ ///
+ /// The callback for when a quote event occurs.
+ ///
+ public Action OnQuote
+ {
+ set
+ {
+ _useOnQuote = !ReferenceEquals(value, null);
+ _onQuote = value;
+ }
+ }
+
+ private readonly Config _config;
+ private UInt64 _dataTradeCount = 0UL;
+ private UInt64 _dataQuoteCount = 0UL;
+ public UInt64 TradeCount { get { return Interlocked.Read(ref _dataTradeCount); } }
+ public UInt64 QuoteCount { get { return Interlocked.Read(ref _dataQuoteCount); } }
+
+ private readonly string _logPrefix;
+ private const string MessageVersionHeaderKey = "UseNewEquitiesFormat";
+ private const string MessageVersionHeaderValue = "v2";
+ private const uint MaxMessageSize = 64u;
+ private const string ChannelFormat = "{0}|TradesOnly|{1}";
+ #endregion //Data Members
+
+ #region Constuctors
+ ///
+ /// Create a new Equities websocket client.
+ ///
+ ///
+ ///
+ ///
+ public EquitiesWebSocketClient(Action onTrade, Action onQuote, Config config)
+ : base(Convert.ToUInt32(config.NumThreads), Convert.ToUInt32(config.BufferSize), Convert.ToUInt32(config.OverflowBufferSize), MaxMessageSize)
+ {
+ OnTrade = onTrade;
+ OnQuote = onQuote;
+ _config = config;
+
+ if (ReferenceEquals(null, _config))
+ throw new ArgumentException("Config may not be null.");
+ _config.Validate();
+ _logPrefix = String.Format("{0}: ", _config?.Provider.ToString());
+ }
+
+ ///
+ /// Create a new Equities websocket client.
+ ///
+ ///
+ public EquitiesWebSocketClient(Action onTrade) : this(onTrade, null, Config.LoadConfig())
+ {
+ }
+
+ ///
+ /// Create a new Equities websocket client.
+ ///
+ ///
+ public EquitiesWebSocketClient(Action onQuote) : this(null, onQuote, Config.LoadConfig())
+ {
+ }
+
+ ///
+ /// Create a new Equities websocket client.
+ ///
+ ///
+ ///
+ public EquitiesWebSocketClient(Action onTrade, Action onQuote) : this(onTrade, onQuote, Config.LoadConfig())
+ {
+ }
+ #endregion //Constructors
+
+ #region Public Methods
+ public async Task Join()
+ {
+ while (!IsReady())
+ await Task.Delay(1000);
+ HashSet channelsToAdd = _config.Symbols.Select(s => GetChannel(s, _config.TradesOnly)).ToHashSet();
+ channelsToAdd.ExceptWith(Channels);
+ foreach (string channel in channelsToAdd)
+ await JoinImpl(channel);
+ }
+
+ public async Task Join(string symbol, bool? tradesOnly)
+ {
+ bool t = tradesOnly.HasValue ? tradesOnly.Value || _config.TradesOnly : false || _config.TradesOnly;
+ while (!IsReady())
+ await Task.Delay(1000);
+ if (!Channels.Contains(GetChannel(symbol, t)))
+ await JoinImpl(GetChannel(symbol, t));
+ }
+
+ public async Task JoinLobby(bool? tradesOnly)
+ {
+ await Join(LobbyName, tradesOnly);
+ }
+
+ public async Task Join(string[] symbols, bool? tradesOnly)
+ {
+ bool t = tradesOnly.HasValue ? tradesOnly.Value || _config.TradesOnly : false || _config.TradesOnly;
+ while (!IsReady())
+ await Task.Delay(1000);
+ HashSet symbolsToAdd = symbols.Select(s => GetChannel(s, t)).ToHashSet();
+ symbolsToAdd.ExceptWith(Channels);
+ foreach (string channel in symbolsToAdd)
+ await JoinImpl(channel);
+ }
+
+ public async Task Leave()
+ {
+ await LeaveImpl();
+ }
+
+ public async Task Leave(string symbol)
+ {
+ foreach (string channel in Channels.Where(c => symbol == GetSymbolFromChannel(c)))
+ await LeaveImpl(channel);
+ }
+
+ public async Task LeaveLobby()
+ {
+ await Leave(LobbyName);
+ }
+
+ public async Task Leave(string[] symbols)
+ {
+ HashSet hashSymbols = new HashSet(symbols);
+ foreach (string channel in Channels.Where(c => hashSymbols.Contains(GetSymbolFromChannel(c))))
+ await LeaveImpl(channel);
+ }
+ #endregion //Public Methods
+
+ #region Private Methods
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private string GetChannel(string symbol, bool tradesOnly)
+ {
+ return String.Format(ChannelFormat, symbol, _config.TradesOnly.ToString());
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private string GetSymbolFromChannel(string channel)
+ {
+ return channel.Split('|')[0];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool GetTradesOnlyFromChannel(string channel)
+ {
+ return Boolean.Parse(channel.Split('|')[2]);
+ }
+
+ protected override string GetLogPrefix()
+ {
+ return _logPrefix;
+ }
+
+ protected override string GetAuthUrl()
+ {
+ switch (_config.Provider)
+ {
+ case Provider.REALTIME:
+ return $"https://realtime-mx.intrinio.com/auth?api_key={_config.ApiKey}";
+ break;
+ case Provider.DELAYED_SIP:
+ return $"https://realtime-delayed-sip.intrinio.com/auth?api_key={_config.ApiKey}";
+ break;
+ case Provider.NASDAQ_BASIC:
+ return $"https://realtime-nasdaq-basic.intrinio.com/auth?api_key={_config.ApiKey}";
+ break;
+ case Provider.MANUAL:
+ return $"http://{_config.IPAddress}/auth?api_key={_config.ApiKey}";
+ break;
+ default:
+ throw new ArgumentException("Provider not specified!");
+ break;
+ }
+ }
+
+ protected override string GetWebSocketUrl(string token)
+ {
+ switch (_config.Provider)
+ {
+ case Provider.REALTIME:
+ return $"wss://realtime-mx.intrinio.com/socket/websocket?vsn=1.0.0&token={token}";
+ break;
+ case Provider.DELAYED_SIP:
+ return $"wss://realtime-delayed-sip.intrinio.com/socket/websocket?vsn=1.0.0&token={token}";
+ break;
+ case Provider.NASDAQ_BASIC:
+ return $"wss://realtime-nasdaq-basic.intrinio.com/socket/websocket?vsn=1.0.0&token={token}";
+ break;
+ case Provider.MANUAL:
+ return $"ws://{_config.IPAddress}/socket/websocket?vsn=1.0.0&token={token}";
+ break;
+ default:
+ throw new ArgumentException("Provider not specified!");
+ break;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override int GetNextChunkLength(ReadOnlySpan bytes)
+ {
+ return Convert.ToInt32(bytes[1]);
+ }
+
+ protected override List> GetCustomSocketHeaders()
+ {
+ List> headers = new List>();
+ headers.Add(new KeyValuePair(MessageVersionHeaderKey, MessageVersionHeaderValue));
+ return headers;
+ }
+
+ private Trade ParseTrade(ReadOnlySpan bytes)
+ {
+ int symbolLength = Convert.ToInt32(bytes[2]);
+ int conditionLength = Convert.ToInt32(bytes[26 + symbolLength]);
+ string symbol = Encoding.ASCII.GetString(bytes.Slice(3, symbolLength));
+ double price = Convert.ToDouble(BitConverter.ToSingle(bytes.Slice(6 + symbolLength, 4)));
+ UInt32 size = BitConverter.ToUInt32(bytes.Slice(10 + symbolLength, 4));
+ DateTime timestamp = DateTime.UnixEpoch + TimeSpan.FromTicks(Convert.ToInt64(BitConverter.ToUInt64(bytes.Slice(14 + symbolLength, 8)) / 100UL));
+ SubProvider subProvider = (SubProvider)((int)bytes[3 + symbolLength]);
+ char marketCenter = BitConverter.ToChar(bytes.Slice(4 + symbolLength, 2));
+ string condition = conditionLength > 0 ? Encoding.ASCII.GetString(bytes.Slice(27 + symbolLength, conditionLength)) : String.Empty;
+ UInt64 totalVolume = Convert.ToUInt64(BitConverter.ToUInt32(bytes.Slice(22 + symbolLength, 4)));
+
+ return new Trade(symbol, price, size, totalVolume, timestamp, subProvider, marketCenter, condition);
+ }
+
+ private Quote ParseQuote(ReadOnlySpan bytes)
+ {
+ int symbolLength = Convert.ToInt32(bytes[2]);
+ int conditionLength = Convert.ToInt32(bytes[22 + symbolLength]);
+ QuoteType type = (QuoteType)((int)(bytes[0]));
+ string symbol = Encoding.ASCII.GetString(bytes.Slice(3, symbolLength));
+ double price = Convert.ToDouble(BitConverter.ToSingle(bytes.Slice(6 + symbolLength, 4)));
+ UInt32 size = BitConverter.ToUInt32(bytes.Slice(10 + symbolLength, 4));
+ DateTime timestamp = DateTime.UnixEpoch + TimeSpan.FromTicks(Convert.ToInt64(BitConverter.ToUInt64(bytes.Slice(14 + symbolLength, 8)) / 100UL));
+ SubProvider subProvider = (SubProvider)((int)(bytes[3 + symbolLength]));
+ char marketCenter = BitConverter.ToChar(bytes.Slice(4 + symbolLength, 2));
+ string condition = (conditionLength > 0) ? Encoding.ASCII.GetString(bytes.Slice(23 + symbolLength, conditionLength)) : String.Empty;
+
+ return new Quote(type, symbol, price, size, timestamp, subProvider, marketCenter, condition);
+ }
+
+ protected override void HandleMessage(ReadOnlySpan bytes)
+ {
+ MessageType msgType = (MessageType)Convert.ToInt32(bytes[0]);
+ switch (msgType)
+ {
+ case MessageType.Trade:
+ {
+ if (_useOnTrade)
+ {
+ Trade trade = ParseTrade(bytes);
+ Interlocked.Increment(ref _dataTradeCount);
+ try { _onTrade.Invoke(trade); }
+ catch (Exception e)
+ {
+ LogMessage(LogLevel.ERROR, "Error while invoking user supplied OnTrade: {0}; {1}", new object[]{e.Message, e.StackTrace});
+ }
+ }
+ break;
+ }
+ case MessageType.Ask:
+ case MessageType.Bid:
+ {
+ if (_useOnQuote)
+ {
+ Quote quote = ParseQuote(bytes);
+ Interlocked.Increment(ref _dataQuoteCount);
+ try { _onQuote.Invoke(quote); }
+ catch (Exception e)
+ {
+ LogMessage(LogLevel.ERROR, "Error while invoking user supplied OnQuote: {0}; {1}", new object[]{e.Message, e.StackTrace});
+ }
+ }
+ break;
+ }
+ default:
+ LogMessage(LogLevel.WARNING, "Invalid MessageType: {0}", new object[] {Convert.ToInt32(bytes[0])});
+ break;
+ }
+ }
+
+ protected override byte[] MakeJoinMessage(string channel)
+ {
+ string symbol = GetSymbolFromChannel(channel);
+ bool tradesOnly = GetTradesOnlyFromChannel(channel);
+ switch (symbol)
+ {
+ case LobbyName:
+ {
+ byte[] message = new byte[11]; //1 + 1 + 9
+ message[0] = Convert.ToByte(74); //type: join (74uy) or leave (76uy)
+ message[1] = tradesOnly ? Convert.ToByte(1) : Convert.ToByte(0);
+ Encoding.ASCII.GetBytes("$FIREHOSE").CopyTo(message, 2);
+ return message;
+ }
+ default:
+ {
+ byte[] message = new byte[2 + symbol.Length]; //1 + 1 + symbol.Length
+ message[0] = Convert.ToByte(74); //type: join (74uy) or leave (76uy)
+ message[1] = tradesOnly ? Convert.ToByte(1) : Convert.ToByte(0);
+ Encoding.ASCII.GetBytes(symbol).CopyTo(message, 2);
+ return message;
+ }
+ }
+ }
+
+ protected override byte[] MakeLeaveMessage(string channel)
+ {
+ string symbol = GetSymbolFromChannel(channel);
+ bool tradesOnly = GetTradesOnlyFromChannel(channel);
+ switch (symbol)
+ {
+ case LobbyName:
+ {
+ byte[] message = new byte[10]; // 1 (type = join) + 9 (symbol = $FIREHOSE)
+ message[0] = Convert.ToByte(76); //type: join (74uy) or leave (76uy)
+ Encoding.ASCII.GetBytes("$FIREHOSE").CopyTo(message, 1);
+ return message;
+ }
+ default:
+ {
+ byte[] message = new byte[1 + symbol.Length]; //1 + symbol.Length
+ message[0] = Convert.ToByte(76); //type: join (74uy) or leave (76uy)
+ Encoding.ASCII.GetBytes(symbol).CopyTo(message, 1);
+ return message;
+ }
+ }
+ }
+ #endregion //Private Methods
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/IEquitiesWebSocketClient.cs b/Intrinio.Realtime/Equities/IEquitiesWebSocketClient.cs
new file mode 100644
index 0000000..7d9cb2f
--- /dev/null
+++ b/Intrinio.Realtime/Equities/IEquitiesWebSocketClient.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Intrinio.Realtime.Equities;
+
+public interface IEquitiesWebSocketClient
+{
+ public Action OnTrade { set; }
+ public Action OnQuote { set; }
+ public Task Join();
+ public Task Join(string channel, bool? tradesOnly);
+ public Task JoinLobby(bool? tradesOnly);
+ public Task Join(string[] channels, bool? tradesOnly);
+ public Task Leave();
+ public Task Leave(string channel);
+ public Task LeaveLobby();
+ public Task Leave(string[] channels);
+ public Task Stop();
+ public Task Start();
+ public ClientStats GetStats();
+ public UInt64 TradeCount { get; }
+ public UInt64 QuoteCount { get; }
+ public void LogMessage(LogLevel logLevel, string messageTemplate, params object[] propertyValues);
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/MessageType.cs b/Intrinio.Realtime/Equities/MessageType.cs
new file mode 100644
index 0000000..e331724
--- /dev/null
+++ b/Intrinio.Realtime/Equities/MessageType.cs
@@ -0,0 +1,8 @@
+namespace Intrinio.Realtime.Equities;
+
+public enum MessageType
+{
+ Trade = 0,
+ Ask = 1,
+ Bid = 2
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/Provider.cs b/Intrinio.Realtime/Equities/Provider.cs
new file mode 100644
index 0000000..428f414
--- /dev/null
+++ b/Intrinio.Realtime/Equities/Provider.cs
@@ -0,0 +1,10 @@
+namespace Intrinio.Realtime.Equities;
+
+public enum Provider
+{
+ NONE = 0,
+ REALTIME = 1,
+ MANUAL = 2,
+ DELAYED_SIP = 3,
+ NASDAQ_BASIC = 4
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/Quote.cs b/Intrinio.Realtime/Equities/Quote.cs
new file mode 100644
index 0000000..5a86f2f
--- /dev/null
+++ b/Intrinio.Realtime/Equities/Quote.cs
@@ -0,0 +1,38 @@
+namespace Intrinio.Realtime.Equities;
+
+using System;
+
+public struct Quote
+{
+ public readonly QuoteType Type;
+ public readonly string Symbol;
+ public readonly double Price;
+ public readonly UInt32 Size;
+ public readonly DateTime Timestamp;
+ public readonly SubProvider SubProvider;
+ public readonly char MarketCenter;
+ public readonly string Condition;
+
+ /// Type: the type of the quote (can be 'ask' or 'bid')
+ /// Symbol: the 'ticker' symbol
+ /// Price: the dollar price of the quote
+ /// Size: the number of shares that were offered as part of the quote
+ /// Timestamp: the time that the quote was placed (a unix timestamp representing the number of milliseconds (or better) since the unix epoch)
+ /// SubProvider: the specific provider this trade came from under the parent provider grouping.
+ public Quote(QuoteType type, string symbol, double price, UInt32 size, DateTime timestamp, SubProvider subProvider, char marketCenter, string condition)
+ {
+ Type = type;
+ Symbol = symbol;
+ Price = price;
+ Size = size;
+ Timestamp = timestamp;
+ SubProvider = subProvider;
+ MarketCenter = marketCenter;
+ Condition = condition;
+ }
+
+ public override string ToString()
+ {
+ return $"Quote (Type: {Type}, Symbol: {Symbol}, Price: {Price}, Size: {Size}, Timestamp: {Timestamp}, SubProvider: {SubProvider}, MarketCenter: {MarketCenter}, Condition: {Condition})";
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/QuoteCandleStick.cs b/Intrinio.Realtime/Equities/QuoteCandleStick.cs
new file mode 100644
index 0000000..856e71a
--- /dev/null
+++ b/Intrinio.Realtime/Equities/QuoteCandleStick.cs
@@ -0,0 +1,129 @@
+namespace Intrinio.Realtime.Equities;
+
+using System;
+
+public class QuoteCandleStick : CandleStick, IEquatable, IComparable, IComparable
+{
+ private readonly string _symbol;
+ private readonly QuoteType _quoteType;
+
+ public string Symbol
+ {
+ get { return _symbol; }
+ }
+ public QuoteType QuoteType
+ {
+ get { return _quoteType; }
+ }
+
+ public QuoteCandleStick(string symbol, UInt32 volume, double price, QuoteType quoteType, double openTimestamp, double closeTimestamp, IntervalType interval, double quoteTime)
+ : base(volume, price, openTimestamp, closeTimestamp, interval, quoteTime)
+ {
+ _symbol = symbol;
+ _quoteType = quoteType;
+ }
+
+ public QuoteCandleStick(string symbol, UInt32 volume, double high, double low, double closePrice, double openPrice, QuoteType quoteType, double openTimestamp, double closeTimestamp, double firstTimestamp, double lastTimestamp, bool complete, double average, double change, IntervalType interval)
+ : base(volume, high, low, closePrice, openPrice, openTimestamp, closeTimestamp, firstTimestamp, lastTimestamp, complete, average, change, interval)
+ {
+ _symbol = symbol;
+ _quoteType = quoteType;
+ }
+
+ public override bool Equals(object other)
+ {
+ return ((!(ReferenceEquals(other, null))) && ReferenceEquals(this, other))
+ || (
+ (!(ReferenceEquals(other, null)))
+ && (!(ReferenceEquals(this, other)))
+ && (other is QuoteCandleStick)
+ && (Symbol.Equals(((QuoteCandleStick)other).Symbol))
+ && (Interval.Equals(((QuoteCandleStick)other).Interval))
+ && (QuoteType.Equals(((QuoteCandleStick)other).QuoteType))
+ && (OpenTimestamp.Equals(((QuoteCandleStick)other).OpenTimestamp))
+ );
+ }
+
+ public override int GetHashCode()
+ {
+ return Symbol.GetHashCode() ^ Interval.GetHashCode() ^ OpenTimestamp.GetHashCode() ^ QuoteType.GetHashCode();
+ }
+
+ public bool Equals(QuoteCandleStick other)
+ {
+ return ((!(ReferenceEquals(other, null))) && ReferenceEquals(this, other))
+ || (
+ (!(ReferenceEquals(other, null)))
+ && (!(ReferenceEquals(this, other)))
+ && (Symbol.Equals(other.Symbol))
+ && (Interval.Equals(other.Interval))
+ && (QuoteType.Equals(other.QuoteType))
+ && (OpenTimestamp.Equals(other.OpenTimestamp))
+ );
+ }
+
+ public int CompareTo(object other)
+ {
+ return Equals(other) switch
+ {
+ true => 0,
+ false => ReferenceEquals(other, null) switch
+ {
+ true => 1,
+ false => (other is QuoteCandleStick) switch
+ {
+ true => Symbol.CompareTo(((QuoteCandleStick)other).Symbol) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => Interval.CompareTo(((QuoteCandleStick)other).Interval) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => QuoteType.CompareTo(((QuoteCandleStick)other).QuoteType) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => OpenTimestamp.CompareTo(((QuoteCandleStick)other).OpenTimestamp)
+ }
+ }
+ },
+ false => 1
+ }
+ }
+ };
+ }
+
+ public int CompareTo(QuoteCandleStick other)
+ {
+ return Equals(other) switch
+ {
+ true => 0,
+ false => Object.ReferenceEquals(other, null) switch
+ {
+ true => 1,
+ false => Symbol.CompareTo(other.Symbol) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => Interval.CompareTo(other.Interval) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => QuoteType.CompareTo(other.QuoteType) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => OpenTimestamp.CompareTo(other.OpenTimestamp)
+ }
+ }
+ }
+ }
+ };
+ }
+
+ public override string ToString()
+ {
+ return $"QuoteCandleStick (Symbol: {Symbol}, QuoteType: {QuoteType.ToString()}, High: {High.ToString("f3")}, Low: {Low.ToString("f3")}, Close: {Close.ToString("f3")}, Open: {Open.ToString("f3")}, OpenTimestamp: {OpenTimestamp.ToString("f6")}, CloseTimestamp: {CloseTimestamp.ToString("f6")}, Change: {Change.ToString("f6")}, Complete: {Complete.ToString()})";
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/QuoteType.cs b/Intrinio.Realtime/Equities/QuoteType.cs
new file mode 100644
index 0000000..2697287
--- /dev/null
+++ b/Intrinio.Realtime/Equities/QuoteType.cs
@@ -0,0 +1,7 @@
+namespace Intrinio.Realtime.Equities;
+
+public enum QuoteType
+{
+ Ask = 1,
+ Bid = 2
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/ReplayClient.cs b/Intrinio.Realtime/Equities/ReplayClient.cs
new file mode 100644
index 0000000..a255b29
--- /dev/null
+++ b/Intrinio.Realtime/Equities/ReplayClient.cs
@@ -0,0 +1,693 @@
+using System.Linq;
+
+namespace Intrinio.Realtime.Equities;
+
+using Intrinio.SDK.Model;
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Text;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+public class ReplayClient : IEquitiesWebSocketClient
+{
+ #region Data Members
+ private const string LobbyName = "lobby";
+ public Action OnTrade { get; set; }
+ public Action OnQuote { get; set; }
+ private readonly Config _config;
+ private readonly DateTime _date;
+ private readonly bool _withSimulatedDelay;
+ private readonly bool _deleteFileWhenDone;
+ private readonly bool _writeToCsv;
+ private readonly string _csvFilePath;
+ private ulong _dataMsgCount;
+ private ulong _dataEventCount;
+ private ulong _dataTradeCount;
+ private ulong _dataQuoteCount;
+ private ulong _textMsgCount;
+ private readonly HashSet _channels;
+ private readonly CancellationTokenSource _ctSource;
+ private readonly ConcurrentQueue _data;
+ private bool _useOnTrade { get {return !(ReferenceEquals(OnTrade, null));} }
+ private bool _useOnQuote { get {return !(ReferenceEquals(OnQuote, null));} }
+
+ private readonly string _logPrefix;
+ private readonly object _csvLock;
+ private readonly Thread[] _threads;
+ private readonly Thread _replayThread;
+ public UInt64 TradeCount { get { return Interlocked.Read(ref _dataTradeCount); } }
+ public UInt64 QuoteCount { get { return Interlocked.Read(ref _dataQuoteCount); } }
+ #endregion //Data Members
+
+ #region Constructors
+ public ReplayClient(Action onTrade, Action onQuote, Config config, DateTime date, bool withSimulatedDelay, bool deleteFileWhenDone, bool writeToCsv, string csvFilePath)
+ {
+ this.OnTrade = onTrade;
+ this.OnQuote = onQuote;
+ this._config = config;
+ this._date = date;
+ this._withSimulatedDelay = withSimulatedDelay;
+ this._deleteFileWhenDone = deleteFileWhenDone;
+ this._writeToCsv = writeToCsv;
+ this._csvFilePath = csvFilePath;
+
+ _dataMsgCount = 0UL;
+ _dataEventCount = 0UL;
+ _dataTradeCount = 0UL;
+ _dataQuoteCount = 0UL;
+ _textMsgCount = 0UL;
+ _channels = new HashSet();
+ _ctSource = new CancellationTokenSource();
+ _data = new ConcurrentQueue();
+
+ _logPrefix = _logPrefix = String.Format("{0}: ", config.Provider.ToString());
+ _csvLock = new Object();
+ _threads = new Thread[config.NumThreads];
+ for (int i = 0; i < _threads.Length; i++)
+ _threads[i] = new Thread(ThreadFn);
+ _replayThread = new Thread(ReplayThreadFn);
+
+ config.Validate();
+ }
+
+ public ReplayClient(Action onTrade, DateTime date, bool withSimulatedDelay, bool deleteFileWhenDone, bool writeToCsv, string csvFilePath) : this(onTrade, null, Config.LoadConfig(), date, withSimulatedDelay, deleteFileWhenDone, writeToCsv, csvFilePath)
+ {
+
+ }
+
+ public ReplayClient(Action onQuote, DateTime date, bool withSimulatedDelay, bool deleteFileWhenDone, bool writeToCsv, string csvFilePath) : this(null, onQuote, Config.LoadConfig(), date, withSimulatedDelay, deleteFileWhenDone, writeToCsv, csvFilePath)
+ {
+
+ }
+
+ public ReplayClient(Action onTrade, Action onQuote, DateTime date, bool withSimulatedDelay, bool deleteFileWhenDone, bool writeToCsv, string csvFilePath) : this(onTrade, onQuote, Config.LoadConfig(), date, withSimulatedDelay, deleteFileWhenDone, writeToCsv, csvFilePath)
+ {
+
+ }
+ #endregion //Constructors
+
+ #region Public Methods
+
+ public Task Join()
+ {
+ HashSet symbolsToAdd = _config.Symbols.Select(s => new Channel(s, _config.TradesOnly)).ToHashSet();
+ symbolsToAdd.ExceptWith(_channels);
+
+ foreach (Channel channel in symbolsToAdd)
+ Join(channel.ticker, channel.tradesOnly);
+
+ return Task.CompletedTask;
+ }
+
+ public Task Join(string symbol, bool? tradesOnly)
+ {
+ bool t = tradesOnly.HasValue
+ ? tradesOnly.Value || _config.TradesOnly
+ : _config.TradesOnly;
+ if (!_channels.Contains(new Channel(symbol, t)))
+ Join(symbol, t);
+
+ return Task.CompletedTask;
+ }
+
+ public async Task JoinLobby(bool? tradesOnly)
+ {
+ await Join(LobbyName, tradesOnly);
+ }
+
+ public Task Join(string[] symbols, bool? tradesOnly)
+ {
+ bool t = tradesOnly.HasValue
+ ? tradesOnly.Value || _config.TradesOnly
+ : _config.TradesOnly;
+ HashSet symbolsToAdd = symbols.Select(s => new Channel(s, t)).ToHashSet();
+ symbolsToAdd.ExceptWith(_channels);
+ foreach (Channel channel in symbolsToAdd)
+ Join(channel.ticker, channel.tradesOnly);
+ return Task.CompletedTask;
+ }
+
+ public Task Leave()
+ {
+ foreach (Channel channel in _channels)
+ Leave(channel.ticker, channel.tradesOnly);
+ return Task.CompletedTask;
+ }
+
+ public Task Leave(string symbol)
+ {
+ IEnumerable matchingChannels = _channels.Where(c => c.ticker == symbol);
+ foreach (Channel channel in matchingChannels)
+ Leave(channel.ticker, channel.tradesOnly);
+ return Task.CompletedTask;
+ }
+
+ public async Task LeaveLobby()
+ {
+ await Leave(LobbyName);
+ }
+
+ public Task Leave(string[] symbols)
+ {
+ HashSet _symbols = new HashSet(symbols);
+ IEnumerable matchingChannels = _channels.Where(c => _symbols.Contains(c.ticker));
+ foreach (Channel channel in matchingChannels)
+ Leave(channel.ticker, channel.tradesOnly);
+ return Task.CompletedTask;
+ }
+
+ public Task Start()
+ {
+ foreach (Thread thread in _threads)
+ thread.Start();
+ if (_writeToCsv)
+ WriteHeaderRow();
+ _replayThread.Start();
+
+ return Task.CompletedTask;
+ }
+
+ public Task Stop()
+ {
+ foreach (Channel channel in _channels)
+ Leave(channel.ticker, channel.tradesOnly);
+
+ _ctSource.Cancel();
+ LogMessage(LogLevel.INFORMATION, "Websocket - Closing...");
+
+ foreach (Thread thread in _threads)
+ thread.Join();
+
+ _replayThread.Join();
+
+ LogMessage(LogLevel.INFORMATION, "Stopped");
+ return Task.CompletedTask;
+ }
+
+ public ClientStats GetStats()
+ {
+ return new ClientStats(
+ Interlocked.Read(ref _dataMsgCount),
+ Interlocked.Read(ref _textMsgCount),
+ _data.Count,
+ Interlocked.Read(ref _dataEventCount),
+ Int32.MaxValue,
+ 0,
+ Int32.MaxValue,
+ 0,
+ 0
+ );
+ }
+
+ [Serilog.Core.MessageTemplateFormatMethod("messageTemplate")]
+ public void LogMessage(LogLevel logLevel, string messageTemplate, params object[] propertyValues)
+ {
+ switch (logLevel)
+ {
+ case LogLevel.DEBUG:
+ Serilog.Log.Debug(_logPrefix + messageTemplate, propertyValues);
+ break;
+ case LogLevel.INFORMATION:
+ Serilog.Log.Information(_logPrefix + messageTemplate, propertyValues);
+ break;
+ case LogLevel.WARNING:
+ Serilog.Log.Warning(_logPrefix + messageTemplate, propertyValues);
+ break;
+ case LogLevel.ERROR:
+ Serilog.Log.Error(_logPrefix + messageTemplate, propertyValues);
+ break;
+ default:
+ throw new ArgumentException("LogLevel not specified!");
+ break;
+ }
+ }
+ #endregion //Public Methods
+
+ #region Private Methods
+ private DateTime ParseTimeReceived(ReadOnlySpan bytes)
+ {
+ return DateTime.UnixEpoch + TimeSpan.FromTicks(Convert.ToInt64(BitConverter.ToUInt64(bytes) / 100UL));
+ }
+
+ private Trade ParseTrade(ReadOnlySpan bytes)
+ {
+ int symbolLength = Convert.ToInt32(bytes[2]);
+ int conditionLength = Convert.ToInt32(bytes[26 + symbolLength]);
+ Trade trade = new Trade(
+ Encoding.ASCII.GetString(bytes.Slice(3, symbolLength)),
+ Convert.ToDouble(BitConverter.ToSingle(bytes.Slice(6 + symbolLength, 4))),
+ BitConverter.ToUInt32(bytes.Slice(10 + symbolLength, 4)),
+ BitConverter.ToUInt32(bytes.Slice(22 + symbolLength, 4)),
+ DateTime.UnixEpoch + TimeSpan.FromTicks(Convert.ToInt64(BitConverter.ToUInt64(bytes.Slice(14 + symbolLength, 8)) / 100UL)),
+ (SubProvider)Convert.ToInt32(bytes[3 + symbolLength]),
+ BitConverter.ToChar(bytes.Slice(4 + symbolLength, 2)),
+ conditionLength > 0 ? Encoding.ASCII.GetString(bytes.Slice(27 + symbolLength, conditionLength)) : String.Empty
+ );
+
+ return trade;
+ }
+
+ private Quote ParseQuote(ReadOnlySpan bytes)
+ {
+ int symbolLength = Convert.ToInt32(bytes[2]);
+ int conditionLength = Convert.ToInt32(bytes[22 + symbolLength]);
+
+ Quote quote = new Quote(
+ (QuoteType)(Convert.ToInt32(bytes[0])),
+ Encoding.ASCII.GetString(bytes.Slice(3, symbolLength)),
+ (Convert.ToDouble(BitConverter.ToSingle(bytes.Slice(6 + symbolLength, 4)))),
+ BitConverter.ToUInt32(bytes.Slice(10 + symbolLength, 4)),
+ DateTime.UnixEpoch + TimeSpan.FromTicks(Convert.ToInt64(BitConverter.ToUInt64(bytes.Slice(14 + symbolLength, 8)) / 100UL)),
+ (SubProvider)(Convert.ToInt32(bytes[3 + symbolLength])),
+ BitConverter.ToChar(bytes.Slice(4 + symbolLength, 2)),
+ conditionLength > 0 ? Encoding.ASCII.GetString(bytes.Slice(23 + symbolLength, conditionLength)) : String.Empty
+ );
+
+ return quote;
+ }
+
+ private void WriteRowToOpenCsvWithoutLock(IEnumerable row)
+ {
+ bool first = true;
+ using (FileStream fs = new FileStream(_csvFilePath, FileMode.Append))
+ using (TextWriter tw = new StreamWriter(fs))
+ {
+ foreach (string s in row)
+ {
+ if (!first)
+ tw.Write(",");
+ else
+ first = false;
+ tw.Write($"\"{s}\"");
+ }
+
+ tw.WriteLine();
+ }
+ }
+
+ private void WriteRowToOpenCsvWithLock(IEnumerable row)
+ {
+ lock (_csvLock)
+ {
+ WriteRowToOpenCsvWithoutLock(row);
+ }
+ }
+
+ private string DoubleRoundSecRule612(double value)
+ {
+ if (value >= 1.0D)
+ return value.ToString("0.00");
+
+ return value.ToString("0.0000");
+ }
+
+ private IEnumerable MapTradeToRow(Trade trade)
+ {
+ yield return MessageType.Trade.ToString();
+ yield return trade.Symbol;
+ yield return DoubleRoundSecRule612(trade.Price);
+ yield return trade.Size.ToString();
+ yield return trade.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK");
+ yield return trade.SubProvider.ToString();
+ yield return trade.MarketCenter.ToString();
+ yield return trade.Condition;
+ yield return trade.TotalVolume.ToString();
+ }
+
+ private void WriteTradeToCsv(Trade trade)
+ {
+ WriteRowToOpenCsvWithLock(MapTradeToRow(trade));
+ }
+
+ private IEnumerable MapQuoteToRow(Quote quote)
+ {
+ yield return quote.Type.ToString();
+ yield return quote.Symbol;
+ yield return DoubleRoundSecRule612(quote.Price);
+ yield return quote.Size.ToString();
+ yield return quote.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK");
+ yield return quote.SubProvider.ToString();
+ yield return quote.MarketCenter.ToString();
+ yield return quote.Condition;
+ }
+
+ private void WriteQuoteToCsv(Quote quote)
+ {
+ WriteRowToOpenCsvWithLock(MapQuoteToRow(quote));
+ }
+
+ private void WriteHeaderRow()
+ {
+ WriteRowToOpenCsvWithLock(new string[]{"Type", "Symbol", "Price", "Size", "Timestamp", "SubProvider", "MarketCenter", "Condition", "TotalVolume"});
+ }
+
+ private void ThreadFn()
+ {
+ CancellationToken ct = _ctSource.Token;
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ if (_data.TryDequeue(out Tick datum))
+ {
+ if (datum.IsTrade())
+ {
+ if (_useOnTrade)
+ {
+ Interlocked.Increment(ref _dataTradeCount);
+ OnTrade.Invoke(datum.Trade);
+ }
+ }
+ else
+ {
+ if (_useOnQuote)
+ {
+ Interlocked.Increment(ref _dataQuoteCount);
+ OnQuote.Invoke(datum.Quote);
+ }
+ }
+ }
+ else
+ Thread.Sleep(1);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception exn)
+ {
+ LogMessage(LogLevel.ERROR, "Error parsing message: {0}; {1}", exn.Message, exn.StackTrace);
+ }
+ }
+ }
+
+ ///
+ /// The results of this should be streamed and not ToList-ed.
+ ///
+ ///
+ ///
+ ///
+ private IEnumerable ReplayTickFileWithoutDelay(string fullFilePath, int byteBufferSize, CancellationToken ct)
+ {
+ if (File.Exists(fullFilePath))
+ {
+ using (FileStream fRead = new FileStream(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.None))
+ {
+ if (fRead.CanRead)
+ {
+ int readResult = fRead.ReadByte(); //This is message type
+
+ while (readResult != -1)
+ {
+ if (!ct.IsCancellationRequested)
+ {
+ byte[] eventBuffer = new byte[byteBufferSize];
+ byte[] timeReceivedBuffer = new byte[8];
+ ReadOnlySpan eventSpanBuffer = new ReadOnlySpan(eventBuffer);
+ ReadOnlySpan timeReceivedSpanBuffer = new ReadOnlySpan(timeReceivedBuffer);
+ eventBuffer[0] = (byte)readResult; //This is message type
+ eventBuffer[1] = (byte)(fRead.ReadByte()); //This is message length, including this and the previous byte.
+ int bytesRead = fRead.Read(eventBuffer, 2, (System.Convert.ToInt32(eventBuffer[1]) - 2)); //read the rest of the message
+ int timeBytesRead = fRead.Read(timeReceivedBuffer, 0, 8); //get the time received
+ DateTime timeReceived = ParseTimeReceived(timeReceivedSpanBuffer);
+
+ switch ((MessageType)(Convert.ToInt32(eventBuffer[0])))
+ {
+ case MessageType.Trade:
+ Trade trade = ParseTrade(eventSpanBuffer);
+ if (_channels.Contains(new Channel(LobbyName, true))
+ || _channels.Contains(new Channel(LobbyName, false))
+ || _channels.Contains(new Channel(trade.Symbol, true))
+ || _channels.Contains(new Channel(trade.Symbol, false)))
+ {
+ if (_writeToCsv)
+ WriteTradeToCsv(trade);
+ yield return new Tick(timeReceived, trade, null);
+ }
+ break;
+ case MessageType.Ask:
+ case MessageType.Bid:
+ Quote quote = ParseQuote(eventSpanBuffer);
+ if (_channels.Contains (new Channel(LobbyName, false)) || _channels.Contains (new Channel(quote.Symbol, false)))
+ {
+ if (_writeToCsv)
+ WriteQuoteToCsv(quote);
+ yield return new Tick(timeReceived, null, quote);
+ }
+ break;
+ default:
+ LogMessage(LogLevel.ERROR, "Invalid MessageType: {0}", eventBuffer[0]);
+ break;
+ }
+
+ //Set up the next iteration
+ readResult = fRead.ReadByte();
+ }
+ else
+ readResult = -1;
+ }
+ }
+ else
+ throw new FileLoadException("Unable to read replay file.");
+ }
+ }
+ else
+ {
+ yield break;
+ }
+ }
+
+ ///
+ /// The results of this should be streamed and not ToList-ed.
+ ///
+ ///
+ ///
+ /// returns
+ private IEnumerable ReplayTickFileWithDelay(string fullFilePath, int byteBufferSize, CancellationToken ct)
+ {
+ long start = DateTime.UtcNow.Ticks;
+ long offset = 0L;
+ foreach (Tick tick in ReplayTickFileWithoutDelay(fullFilePath, byteBufferSize, ct))
+ {
+ if (offset == 0L)
+ offset = start - tick.TimeReceived().Ticks;
+
+ if (!ct.IsCancellationRequested)
+ {
+ SpinWait.SpinUntil(() => (tick.TimeReceived().Ticks + offset) <= DateTime.UtcNow.Ticks);
+ yield return tick;
+ }
+ }
+ }
+
+ private string MapSubProviderToApiValue(SubProvider subProvider)
+ {
+ switch (subProvider)
+ {
+ case SubProvider.IEX: return "iex";
+ case SubProvider.UTP: return "utp_delayed";
+ case SubProvider.CTA_A: return "cta_a_delayed";
+ case SubProvider.CTA_B: return "cta_b_delayed";
+ case SubProvider.OTC: return "otc_delayed";
+ case SubProvider.NASDAQ_BASIC: return "nasdaq_basic";
+ default: return "iex";
+ }
+ }
+
+ private SubProvider[] MapProviderToSubProviders(Intrinio.Realtime.Equities.Provider provider)
+ {
+ switch (provider)
+ {
+ case Provider.NONE: return Array.Empty();
+ case Provider.MANUAL: return Array.Empty();
+ case Provider.REALTIME: return new SubProvider[]{SubProvider.IEX};
+ case Provider.DELAYED_SIP: return new SubProvider[]{SubProvider.UTP, SubProvider.CTA_A, SubProvider.CTA_B, SubProvider.OTC};
+ case Provider.NASDAQ_BASIC: return new SubProvider[]{SubProvider.NASDAQ_BASIC};
+ default: return new SubProvider[0];
+ }
+ }
+
+ private string FetchReplayFile(SubProvider subProvider)
+ {
+ Intrinio.SDK.Api.SecurityApi api = new Intrinio.SDK.Api.SecurityApi();
+
+ if (!api.Configuration.ApiKey.ContainsKey("api_key"))
+ api.Configuration.ApiKey.Add("api_key", _config.ApiKey);
+
+ try
+ {
+ SecurityReplayFileResult result = api.GetSecurityReplayFile(MapSubProviderToApiValue(subProvider), _date);
+ string decodedUrl = result.Url.Replace(@"\u0026", "&");
+ string tempDir = System.IO.Path.GetTempPath();
+ string fileName = Path.Combine(tempDir, result.Name);
+
+ using (FileStream outputFile = new FileStream(fileName,System.IO.FileMode.Create))
+ using (HttpClient httpClient = new HttpClient())
+ {
+ httpClient.Timeout = TimeSpan.FromHours(1);
+ httpClient.BaseAddress = new Uri(decodedUrl);
+ using (HttpResponseMessage response = httpClient.GetAsync(decodedUrl, HttpCompletionOption.ResponseHeadersRead).Result)
+ using (Stream streamToReadFrom = response.Content.ReadAsStreamAsync().Result)
+ {
+ streamToReadFrom.CopyTo(outputFile);
+ }
+ }
+
+ return fileName;
+ }
+ catch (Exception e)
+ {
+ LogMessage(LogLevel.ERROR, "Error while fetching {0} file: {1}", subProvider.ToString(), e.Message);
+ return null;
+ }
+ }
+
+ private void FillNextTicks(IEnumerator[] enumerators, Tick[] nextTicks)
+ {
+ for (int i = 0; i < nextTicks.Length; i++)
+ if (nextTicks[i] == null && enumerators[i].MoveNext())
+ nextTicks[i] = enumerators[i].Current;
+ }
+
+ private Tick PullNextTick(Tick[] nextTicks)
+ {
+ int pullIndex = 0;
+ DateTime t = DateTime.MaxValue;
+ for (int i = 0; i < nextTicks.Length; i++)
+ {
+ if (nextTicks[i] != null && nextTicks[i].TimeReceived() < t)
+ {
+ pullIndex = i;
+ t = nextTicks[i].TimeReceived();
+ }
+ }
+
+ Tick pulledTick = nextTicks[pullIndex];
+ nextTicks[pullIndex] = null;
+ return pulledTick;
+ }
+
+ private bool HasAnyValue(Tick[] nextTicks)
+ {
+ bool hasValue = false;
+
+ for (int i = 0; i < nextTicks.Length; i++)
+ if (nextTicks[i] != null)
+ hasValue = true;
+
+ return hasValue;
+ }
+
+ private IEnumerable ReplayFileGroupWithoutDelay(IEnumerable[] tickGroup, CancellationToken ct)
+ {
+ Tick[] nextTicks = new Tick[tickGroup.Length];
+ IEnumerator[] enumerators = new IEnumerator[tickGroup.Length];
+ for (int i = 0; i < tickGroup.Length; i++)
+ {
+ enumerators[i] = tickGroup[i].GetEnumerator();
+ }
+
+ FillNextTicks(enumerators, nextTicks);
+ while (HasAnyValue(nextTicks))
+ {
+ Tick nextTick = PullNextTick(nextTicks);
+ if (nextTick != null)
+ yield return nextTick;
+
+ FillNextTicks(enumerators, nextTicks);
+ }
+ }
+
+ private IEnumerable ReplayFileGroupWithDelay(IEnumerable[] tickGroup, CancellationToken ct)
+ {
+ Int64 start = DateTime.UtcNow.Ticks;
+ Int64 offset = 0L;
+
+ foreach (Tick tick in ReplayFileGroupWithoutDelay(tickGroup, ct))
+ {
+ if (offset == 0L)
+ {
+ offset = start - tick.TimeReceived().Ticks;
+ }
+
+ if (!ct.IsCancellationRequested)
+ {
+ System.Threading.SpinWait.SpinUntil(() => (tick.TimeReceived().Ticks + offset) <= DateTime.UtcNow.Ticks);
+ yield return tick;
+ }
+ }
+ }
+
+ private void ReplayThreadFn()
+ {
+ CancellationToken ct = _ctSource.Token;
+ SubProvider[] subProviders = MapProviderToSubProviders(_config.Provider);
+ string[] replayFiles = new string[subProviders.Length];
+ IEnumerable[] allTicks = new IEnumerable[subProviders.Length];
+
+ try
+ {
+ for (int i = 0; i < subProviders.Length; i++)
+ {
+ LogMessage(LogLevel.INFORMATION, "Downloading Replay file for {0} on {1}...", subProviders[i].ToString(), _date.Date.ToString());
+ replayFiles[i] = FetchReplayFile(subProviders[i]);
+ LogMessage(LogLevel.INFORMATION, "Downloaded Replay file to: {0}", replayFiles[i]);
+ allTicks[i] = ReplayTickFileWithoutDelay(replayFiles[i], 100, ct);
+ }
+
+ IEnumerable aggregatedTicks = _withSimulatedDelay
+ ? ReplayFileGroupWithDelay(allTicks, ct)
+ : ReplayFileGroupWithoutDelay(allTicks, ct);
+
+ foreach (Tick tick in aggregatedTicks)
+ {
+ if (!ct.IsCancellationRequested)
+ {
+ Interlocked.Increment(ref _dataEventCount);
+ Interlocked.Increment(ref _dataMsgCount);
+ _data.Enqueue(tick);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ LogMessage(LogLevel.ERROR, "Error while replaying file: {0}", e.Message);
+ }
+
+ if (_deleteFileWhenDone)
+ {
+ foreach (string deleteFilePath in replayFiles)
+ {
+ if (File.Exists(deleteFilePath))
+ {
+ LogMessage(LogLevel.INFORMATION, "Deleting Replay file: {0}", deleteFilePath);
+ File.Delete(deleteFilePath);
+ }
+ }
+ }
+ }
+
+ private void Join(string symbol, bool tradesOnly)
+ {
+ string lastOnly = tradesOnly ? "true" : "false";
+ if (_channels.Add(new (symbol, tradesOnly)))
+ {
+ LogMessage(LogLevel.INFORMATION, "Websocket - Joining channel: {0} (trades only = {1})", symbol, lastOnly);
+ }
+ }
+
+ private void Leave(string symbol, bool tradesOnly)
+ {
+ string lastOnly = tradesOnly ? "true" : "false";
+ if (_channels.Remove(new (symbol, tradesOnly)))
+ {
+ LogMessage(LogLevel.INFORMATION, "Websocket - Leaving channel: {0} (trades only = {1})", symbol, lastOnly);
+ }
+ }
+ #endregion //Private Methods
+
+ private record Channel(string ticker, bool tradesOnly);
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/SubProvider.cs b/Intrinio.Realtime/Equities/SubProvider.cs
new file mode 100644
index 0000000..8033299
--- /dev/null
+++ b/Intrinio.Realtime/Equities/SubProvider.cs
@@ -0,0 +1,12 @@
+namespace Intrinio.Realtime.Equities;
+
+public enum SubProvider
+{
+ NONE = 0,
+ CTA_A = 1,
+ CTA_B = 2,
+ UTP = 3,
+ OTC = 4,
+ NASDAQ_BASIC = 5,
+ IEX = 6
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/Tick.cs b/Intrinio.Realtime/Equities/Tick.cs
new file mode 100644
index 0000000..0b96060
--- /dev/null
+++ b/Intrinio.Realtime/Equities/Tick.cs
@@ -0,0 +1,131 @@
+namespace Intrinio.Realtime.Equities;
+
+using System;
+using System.Text;
+
+internal class Tick
+{
+ private readonly DateTime _timeReceived;
+ private readonly Trade? _trade;
+ private readonly Quote? _quote;
+
+ public Tick(DateTime timeReceived, Trade? trade, Quote? quote)
+ {
+ _timeReceived = timeReceived;
+ _trade = trade;
+ _quote = quote;
+ }
+
+ public byte[] getTradeBytes(Trade trade)
+ {
+ byte[] symbolBytes = Encoding.ASCII.GetBytes(trade.Symbol);
+ byte symbolLength = Convert.ToByte(symbolBytes.Length);
+ int symbolLengthInt32 = Convert.ToInt32(symbolLength);
+ byte[] marketCenterBytes = BitConverter.GetBytes(trade.MarketCenter);
+ byte[] tradePrice = BitConverter.GetBytes(Convert.ToSingle(trade.Price));
+ byte[] tradeSize = BitConverter.GetBytes(trade.Size);
+ byte[] timeStamp = BitConverter.GetBytes(Convert.ToUInt64((trade.Timestamp - DateTime.UnixEpoch).Ticks) * 100UL);
+ byte[] tradeTotalVolume = BitConverter.GetBytes(trade.TotalVolume);
+ byte[] condition = Encoding.ASCII.GetBytes(trade.Condition);
+ byte conditionLength = Convert.ToByte(condition.Length);
+ byte messageLength = Convert.ToByte(symbolLength + conditionLength + 27);
+
+ byte[] bytes = GC.AllocateUninitializedArray(System.Convert.ToInt32(messageLength));
+ bytes[0] = System.Convert.ToByte((int)(MessageType.Trade));
+ bytes[1] = messageLength;
+ bytes[2] = symbolLength;
+ Array.Copy(symbolBytes, 0, bytes, 3, symbolLengthInt32);
+ bytes[3 + symbolLengthInt32] = Convert.ToByte((int)(trade.SubProvider));
+ Array.Copy(marketCenterBytes, 0, bytes, 4 + symbolLengthInt32, marketCenterBytes.Length);
+ Array.Copy(tradePrice, 0, bytes, 6 + symbolLengthInt32, tradePrice.Length);
+ Array.Copy(tradeSize, 0, bytes, 10 + symbolLengthInt32, tradeSize.Length);
+ Array.Copy(timeStamp, 0, bytes, 14 + symbolLengthInt32, timeStamp.Length);
+ Array.Copy(tradeTotalVolume, 0, bytes, 22 + symbolLengthInt32, tradeTotalVolume.Length);
+ bytes[26 + symbolLengthInt32] = conditionLength;
+ Array.Copy(condition, 0, bytes, 27 + symbolLengthInt32, System.Convert.ToInt32(conditionLength));
+
+ // byte 0: message type (hasn't changed)
+ // byte 1: message length (in bytes, including bytes 0 and 1)
+ // byte 2: symbol length (in bytes)
+ // bytes[3...]: symbol string (ascii)
+ // next byte: source
+ // next 2 bytes: market center (as 1 char)
+ // next 4 bytes: trade price (float)
+ // next 4 bytes: trade size (uint)
+ // next 8 bytes: timestamp (uint64)
+ // next 4 bytes: trade total volume ((uint)
+ // next byte: condition len
+ // next bytes: condition string (ascii)
+
+ return bytes;
+ }
+
+ public byte[] getQuoteBytes(Quote quote)
+ {
+ byte[] symbolBytes = Encoding.ASCII.GetBytes(quote.Symbol);
+ byte symbolLength = Convert.ToByte(symbolBytes.Length);
+ int symbolLengthInt32 = Convert.ToInt32(symbolLength);
+ byte[] marketCenterBytes = BitConverter.GetBytes(quote.MarketCenter);
+ byte[] tradePrice = BitConverter.GetBytes(Convert.ToSingle(quote.Price));
+ byte[] tradeSize = BitConverter.GetBytes(quote.Size);
+ byte[] timeStamp = BitConverter.GetBytes(Convert.ToUInt64((quote.Timestamp - DateTime.UnixEpoch).Ticks) * 100UL);
+ byte[] condition = Encoding.ASCII.GetBytes(quote.Condition);
+ byte conditionLength = Convert.ToByte(condition.Length);
+ byte messageLength = Convert.ToByte(23 + symbolLength + conditionLength);
+
+ byte[] bytes = System.GC.AllocateUninitializedArray(System.Convert.ToInt32(messageLength));
+ bytes[0] = System.Convert.ToByte((int)quote.Type);
+ bytes[1] = messageLength;
+ bytes[2] = symbolLength;
+ Array.Copy(symbolBytes, 0, bytes, 3, symbolLengthInt32);
+ bytes[3 + symbolLengthInt32] = System.Convert.ToByte((int)(quote.SubProvider));
+ Array.Copy(marketCenterBytes, 0, bytes, 4 + symbolLengthInt32, marketCenterBytes.Length);
+ Array.Copy(tradePrice, 0, bytes, 6 + symbolLengthInt32, tradePrice.Length);
+ Array.Copy(tradeSize, 0, bytes, 10 + symbolLengthInt32, tradeSize.Length);
+ Array.Copy(timeStamp, 0, bytes, 14 + symbolLengthInt32, timeStamp.Length);
+ bytes[22 + symbolLengthInt32] = conditionLength;
+ Array.Copy(condition, 0, bytes, 23 + symbolLengthInt32, System.Convert.ToInt32(conditionLength));
+
+ // byte 0: message type (hasn't changed)
+ // byte 1: message length (in bytes, including bytes 0 and 1)
+ // byte 2: symbol length (in bytes)
+ // bytes[3...]: symbol string (ascii)
+ // next byte: source
+ // next 2 bytes: market center (as 1 char)
+ // next 4 bytes: ask/bid price (float)
+ // next 4 bytes: ask/bid size (uint)
+ // next 8 bytes: timestamp (uint64)
+ // next byte: condition len
+ // next bytes: condition string (ascii)
+
+ return bytes;
+ }
+
+ public DateTime TimeReceived()
+ {
+ return _timeReceived;
+ }
+
+ public bool IsTrade()
+ {
+ return _trade.HasValue;
+ }
+
+ public Trade Trade { get {return _trade ?? default;} }
+
+ public Quote Quote { get { return _quote ?? default; } }
+
+ public byte[] GetTimeReceivedBytes()
+ {
+ return BitConverter.GetBytes(Convert.ToUInt64((_timeReceived - DateTime.UnixEpoch).Ticks) * 100UL);
+ }
+
+ public byte[] GetEventBytes()
+ {
+ return _trade.HasValue
+ ? getTradeBytes(_trade.Value)
+ : _quote.HasValue
+ ? getQuoteBytes(_quote.Value)
+ : Array.Empty();
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/Trade.cs b/Intrinio.Realtime/Equities/Trade.cs
new file mode 100644
index 0000000..404a326
--- /dev/null
+++ b/Intrinio.Realtime/Equities/Trade.cs
@@ -0,0 +1,43 @@
+namespace Intrinio.Realtime.Equities;
+
+using System;
+
+public readonly struct Trade
+{
+ public readonly string Symbol;
+ public readonly double Price;
+ public readonly UInt32 Size;
+ public readonly DateTime Timestamp;
+ public readonly SubProvider SubProvider;
+ public readonly char MarketCenter;
+ public readonly string Condition;
+ public readonly UInt64 TotalVolume;
+
+ /// Symbol: the 'ticker' symbol
+ /// Price: the dollar price of the last trade
+ /// Size: the number of shares that were exchanged in the last trade
+ /// TotalVolume: the total number of shares that have been traded since market open
+ /// Timestamp: the time that the trade was executed (a unix timestamp representing the number of milliseconds (or better) since the unix epoch)
+ /// SubProvider: the specific provider this trade came from under the parent provider grouping.
+ public Trade(string symbol, double price, UInt32 size, UInt64 totalVolume, DateTime timestamp, SubProvider subProvider, char marketCenter, string condition)
+ {
+ Symbol = symbol;
+ Price = price;
+ Size = size;
+ Timestamp = timestamp;
+ SubProvider = subProvider;
+ MarketCenter = marketCenter;
+ Condition = condition;
+ TotalVolume = totalVolume;
+ }
+
+ public override string ToString()
+ {
+ return $"Trade (Symbol: {Symbol}, Price: {Price}, Size: {Size}, TotalVolume: {TotalVolume}, Timestamp: {Timestamp}, SubProvider: {SubProvider}, MarketCenter: {MarketCenter}, Condition: {Condition})";
+ }
+
+ public bool IsDarkpool()
+ {
+ return MarketCenter.Equals((char)0) || MarketCenter.Equals('D') || MarketCenter.Equals('E') || Char.IsWhiteSpace(MarketCenter);
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Equities/TradeCandleStick.cs b/Intrinio.Realtime/Equities/TradeCandleStick.cs
new file mode 100644
index 0000000..ecc89b7
--- /dev/null
+++ b/Intrinio.Realtime/Equities/TradeCandleStick.cs
@@ -0,0 +1,110 @@
+namespace Intrinio.Realtime.Equities;
+
+using System;
+
+public class TradeCandleStick : CandleStick, IEquatable, IComparable, IComparable
+{
+ private readonly string _symbol;
+
+ public string Symbol
+ {
+ get { return _symbol; }
+ }
+
+ public TradeCandleStick(string symbol, UInt32 volume, double price, double openTimestamp, double closeTimestamp, IntervalType interval, double tradeTime)
+ : base(volume, price, openTimestamp, closeTimestamp, interval, tradeTime)
+ {
+ _symbol = symbol;
+ }
+
+ public TradeCandleStick(string symbol, UInt32 volume, double high, double low, double closePrice, double openPrice, double openTimestamp, double closeTimestamp, double firstTimestamp, double lastTimestamp, bool complete, double average, double change, IntervalType interval)
+ : base(volume, high, low, closePrice, openPrice, openTimestamp, closeTimestamp, firstTimestamp, lastTimestamp, complete, average, change, interval)
+ {
+ _symbol = symbol;
+ }
+
+ public override bool Equals(object other)
+ {
+ return ((!(ReferenceEquals(other, null))) && ReferenceEquals(this, other))
+ || (
+ (!(ReferenceEquals(other, null)))
+ && (!(ReferenceEquals(this, other)))
+ && (other is TradeCandleStick)
+ && (Symbol.Equals(((TradeCandleStick)other).Symbol))
+ && (Interval.Equals(((TradeCandleStick)other).Interval))
+ && (OpenTimestamp.Equals(((TradeCandleStick)other).OpenTimestamp))
+ );
+ }
+
+ public override int GetHashCode()
+ {
+ return Symbol.GetHashCode() ^ Interval.GetHashCode() ^ OpenTimestamp.GetHashCode();
+ }
+
+ public bool Equals(TradeCandleStick other)
+ {
+ return ((!(ReferenceEquals(other, null))) && ReferenceEquals(this, other))
+ || (
+ (!(ReferenceEquals(other, null)))
+ && (!(ReferenceEquals(this, other)))
+ && (Symbol.Equals(other.Symbol))
+ && (Interval.Equals(other.Interval))
+ && (OpenTimestamp.Equals(other.OpenTimestamp))
+ );
+ }
+
+ public int CompareTo(object other)
+ {
+ return Equals(other) switch
+ {
+ true => 0,
+ false => ReferenceEquals(other, null) switch
+ {
+ true => 1,
+ false => (other is TradeCandleStick) switch
+ {
+ true => Symbol.CompareTo(((TradeCandleStick)other).Symbol) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => Interval.CompareTo(((TradeCandleStick)other).Interval) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => this.OpenTimestamp.CompareTo(((TradeCandleStick)other).OpenTimestamp)
+ }
+ },
+ false => 1
+ }
+ }
+ };
+ }
+
+ public int CompareTo(TradeCandleStick other)
+ {
+ return Equals(other) switch
+ {
+ true => 0,
+ false => Object.ReferenceEquals(other, null) switch
+ {
+ true => 1,
+ false => this.Symbol.CompareTo(other.Symbol) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => this.Interval.CompareTo(other.Interval) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => this.OpenTimestamp.CompareTo(other.OpenTimestamp)
+ }
+ }
+ }
+ };
+ }
+
+ public override string ToString()
+ {
+ return $"TradeCandleStick (Symbol: {Symbol}, Volume: {Volume.ToString()}, High: {High.ToString("f3")}, Low: {Low.ToString("f3")}, Close: {Close.ToString("f3")}, Open: {Open.ToString("f3")}, OpenTimestamp: {OpenTimestamp.ToString("f6")}, CloseTimestamp: {CloseTimestamp.ToString("f6")}, AveragePrice: {Average.ToString("f3")}, Change: {Change.ToString("f6")}, Complete: {Complete.ToString()})";
+ }
+}
\ No newline at end of file
diff --git a/IntrinioRealtimeMultiExchange/config.json b/Intrinio.Realtime/Equities/config.json
similarity index 71%
rename from IntrinioRealtimeMultiExchange/config.json
rename to Intrinio.Realtime/Equities/config.json
index abec0c4..5851eeb 100644
--- a/IntrinioRealtimeMultiExchange/config.json
+++ b/Intrinio.Realtime/Equities/config.json
@@ -1,14 +1,16 @@
{
"Config": {
- "ApiKey": "",
- "NumThreads": 2,
+ "ApiKey": "API_KEY_HERE",
+ "NumThreads": 4,
"Provider": "REALTIME",
//"Provider": "DELAYED_SIP",
//"Provider": "NASDAQ_BASIC",
//"Provider": "MANUAL",
- "Symbols": [ "AAPL", "MSFT", "GOOG" ]
+ //"IPAddress": "1.2.3.4",
+ "BufferSize": 2048,
+ "OverflowBufferSize": 2048,
+ "Symbols": [ "AAPL", "MSFT", "TSLA" ]
//"Symbols": [ "lobby" ]
- //"IPAddress": "1.2.3.4"
},
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
diff --git a/Intrinio.Realtime/IntervalType.cs b/Intrinio.Realtime/IntervalType.cs
new file mode 100644
index 0000000..ea7fc3c
--- /dev/null
+++ b/Intrinio.Realtime/IntervalType.cs
@@ -0,0 +1,14 @@
+namespace Intrinio.Realtime;
+
+public enum IntervalType
+{
+ OneMinute = 60,
+ TwoMinute = 120,
+ ThreeMinute = 180,
+ FourMinute = 240,
+ FiveMinute = 300,
+ TenMinute = 600,
+ FifteenMinute = 900,
+ ThirtyMinute = 1800,
+ SixtyMinute = 3600
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Intrinio.Realtime.csproj b/Intrinio.Realtime/Intrinio.Realtime.csproj
new file mode 100644
index 0000000..84f8182
--- /dev/null
+++ b/Intrinio.Realtime/Intrinio.Realtime.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0
+ disable
+ enable
+ true
+ true
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Intrinio.Realtime/LogLevel.cs b/Intrinio.Realtime/LogLevel.cs
new file mode 100644
index 0000000..01dc904
--- /dev/null
+++ b/Intrinio.Realtime/LogLevel.cs
@@ -0,0 +1,9 @@
+namespace Intrinio.Realtime;
+
+public enum LogLevel
+{
+ DEBUG = 0,
+ INFORMATION = 1,
+ WARNING = 2,
+ ERROR = 3
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Logging.cs b/Intrinio.Realtime/Logging.cs
new file mode 100644
index 0000000..48ce8d9
--- /dev/null
+++ b/Intrinio.Realtime/Logging.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Intrinio.Realtime;
+
+public static class Logging
+{
+ [Serilog.Core.MessageTemplateFormatMethod("messageTemplate")]
+ public static void Log(LogLevel logLevel, string messageTemplate, params object[] propertyValues)
+ {
+ switch (logLevel)
+ {
+ case LogLevel.DEBUG:
+ Serilog.Log.Debug(messageTemplate, propertyValues);
+ break;
+ case LogLevel.INFORMATION:
+ Serilog.Log.Information(messageTemplate, propertyValues);
+ break;
+ case LogLevel.WARNING:
+ Serilog.Log.Warning(messageTemplate, propertyValues);
+ break;
+ case LogLevel.ERROR:
+ Serilog.Log.Error(messageTemplate, propertyValues);
+ break;
+ default:
+ throw new ArgumentException("LogLevel not specified!");
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/CandleStickClient.cs b/Intrinio.Realtime/Options/CandleStickClient.cs
new file mode 100644
index 0000000..05e4e1b
--- /dev/null
+++ b/Intrinio.Realtime/Options/CandleStickClient.cs
@@ -0,0 +1,711 @@
+using System.Threading.Tasks;
+
+namespace Intrinio.Realtime.Options;
+
+using Intrinio;
+using Serilog;
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Runtime.CompilerServices;
+
+public delegate TradeCandleStick FetchHistoricalTradeCandleStick(string contract, double openTimestamp, double closeTimestamp, IntervalType interval);
+public delegate QuoteCandleStick FetchHistoricalQuoteCandleStick(string contract, double openTimestamp, double closeTimestamp, QuoteType quoteType, IntervalType interval);
+
+public class CandleStickClient
+{
+ #region Data Members
+ private readonly IntervalType _interval;
+ private readonly bool _broadcastPartialCandles;
+ private readonly double _sourceDelaySeconds;
+ private readonly bool _useTradeFiltering;
+ private readonly CancellationTokenSource _ctSource;
+ private const int InitialDictionarySize = 3_601_579; //a close prime number greater than 2x the max expected size. There are usually around 1.5m option contracts.
+ private readonly object _contractBucketsLock;
+ private readonly object _lostAndFoundLock;
+ private readonly Dictionary _contractBuckets;
+ private readonly Dictionary _lostAndFound;
+ private const double FlushBufferSeconds = 30.0;
+ private readonly Thread _lostAndFoundThread;
+ private readonly Thread _flushThread;
+
+ private bool UseOnTradeCandleStick { get { return !ReferenceEquals(OnTradeCandleStick, null); } }
+ private bool UseOnQuoteCandleStick { get { return !ReferenceEquals(OnQuoteCandleStick, null); } }
+ private bool UseGetHistoricalTradeCandleStick { get { return !ReferenceEquals(GetHistoricalTradeCandleStick,null); } }
+ private bool UseGetHistoricalQuoteCandleStick { get { return !ReferenceEquals(GetHistoricalQuoteCandleStick,null); } }
+
+ ///
+ /// The callback used for broadcasting trade candles.
+ ///
+ public Action OnTradeCandleStick { get; set; }
+
+ ///
+ /// The callback used for broadcasting quote candles.
+ ///
+ private Action OnQuoteCandleStick { get; set; }
+
+ ///
+ /// Fetch a previously broadcasted trade candlestick from the given unique parameters.
+ ///
+ public FetchHistoricalTradeCandleStick GetHistoricalTradeCandleStick { get; set; }
+
+ ///
+ /// Fetch a previously broadcasted quote candlestick from the given unique parameters.
+ ///
+ public FetchHistoricalQuoteCandleStick GetHistoricalQuoteCandleStick { get; set; }
+ #endregion //Data Members
+
+ #region Constructors
+
+ ///
+ /// Creates an equities CandleStickClient that creates trade and quote candlesticks from a stream of trades and quotes.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public CandleStickClient(
+ Action onTradeCandleStick,
+ Action onQuoteCandleStick,
+ IntervalType interval,
+ bool broadcastPartialCandles,
+ FetchHistoricalTradeCandleStick getHistoricalTradeCandleStick,
+ FetchHistoricalQuoteCandleStick getHistoricalQuoteCandleStick,
+ double sourceDelaySeconds)
+ {
+ this.OnTradeCandleStick = onTradeCandleStick;
+ this.OnQuoteCandleStick = onQuoteCandleStick;
+ this._interval = interval;
+ this._broadcastPartialCandles = broadcastPartialCandles;
+ this.GetHistoricalTradeCandleStick = getHistoricalTradeCandleStick;
+ this.GetHistoricalQuoteCandleStick = getHistoricalQuoteCandleStick;
+ this._sourceDelaySeconds = sourceDelaySeconds;
+ _ctSource = new CancellationTokenSource();
+ _contractBucketsLock = new object();
+ _lostAndFoundLock = new object();
+ _contractBuckets = new Dictionary(InitialDictionarySize);
+ _lostAndFound = new Dictionary(InitialDictionarySize);
+ _lostAndFoundThread = new Thread(new ThreadStart(LostAndFoundFn));
+ _flushThread = new Thread(new ThreadStart(FlushFn));
+ }
+ #endregion //Constructors
+
+ #region Public Methods
+
+ public void OnTrade(Trade trade)
+ {
+ try
+ {
+ if (UseOnTradeCandleStick)
+ {
+ ContractBucket bucket = GetSlot(trade.Contract, _contractBuckets, _contractBucketsLock);
+
+ lock (bucket.Locker)
+ {
+ double ts = ConvertToUnixTimestamp(trade.Timestamp);
+
+ if (bucket.TradeCandleStick != null)
+ {
+ if (bucket.TradeCandleStick.CloseTimestamp < ts)
+ {
+ bucket.TradeCandleStick.MarkComplete();
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ bucket.TradeCandleStick = CreateNewTradeCandle(trade, ts);
+ }
+ else if (bucket.TradeCandleStick.OpenTimestamp <= ts)
+ {
+ bucket.TradeCandleStick.Update(trade.Size, trade.Price, ts);
+ if (_broadcastPartialCandles)
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ }
+ else //This is a late trade. We already shipped the candle, so add to lost and found
+ {
+ AddTradeToLostAndFound(trade);
+ }
+ }
+ else
+ {
+ bucket.TradeCandleStick = CreateNewTradeCandle(trade, ts);
+ if (_broadcastPartialCandles)
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Warning("Error on handling trade in CandleStick Client: {0}", e.Message);
+ }
+ }
+
+ public void OnQuote(Quote quote)
+ {
+ try
+ {
+ if (UseOnQuoteCandleStick)
+ {
+ ContractBucket bucket = GetSlot(quote.Contract, _contractBuckets, _contractBucketsLock);
+
+ lock (bucket.Locker)
+ {
+ OnAsk(quote, bucket);
+ OnBid(quote, bucket);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Warning("Error on handling trade in CandleStick Client: {0}", e.Message);
+ }
+ }
+
+ public void Start()
+ {
+ if (!_flushThread.IsAlive)
+ {
+ _flushThread.Start();
+ }
+
+ if (!_lostAndFoundThread.IsAlive)
+ {
+ _lostAndFoundThread.Start();
+ }
+ }
+
+ public void Stop()
+ {
+ _ctSource.Cancel();
+ }
+ #endregion //Public Methods
+
+ #region Private Methods
+
+ private TradeCandleStick CreateNewTradeCandle(Trade trade, double timestamp)
+ {
+ double start = GetNearestModInterval(timestamp, _interval);
+ TradeCandleStick freshCandle = new TradeCandleStick(trade.Contract, trade.Size, trade.Price, start, (start + System.Convert.ToDouble((int)_interval)), _interval, timestamp);
+
+ if (UseGetHistoricalTradeCandleStick && UseOnTradeCandleStick)
+ {
+ try
+ {
+ TradeCandleStick historical = GetHistoricalTradeCandleStick(freshCandle.Contract, freshCandle.OpenTimestamp, freshCandle.CloseTimestamp, freshCandle.Interval);
+ if (ReferenceEquals(historical,null))
+ return freshCandle;
+ historical.MarkIncomplete();
+ return MergeTradeCandles(historical, freshCandle);
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical TradeCandleStick: {0}; trade: {1}", e.Message, trade);
+ return freshCandle;
+ }
+ }
+ else
+ {
+ return freshCandle;
+ }
+ }
+
+ private QuoteCandleStick CreateNewAskCandle(Quote quote, double timestamp)
+ {
+ double start = GetNearestModInterval(timestamp, _interval);
+ QuoteCandleStick freshCandle = new QuoteCandleStick(quote.Contract, quote.AskSize, quote.AskPrice, QuoteType.Ask, start, (start + System.Convert.ToDouble((int)_interval)), _interval, timestamp);
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick)
+ {
+ try
+ {
+ QuoteCandleStick historical = GetHistoricalQuoteCandleStick.Invoke(freshCandle.Contract, freshCandle.OpenTimestamp, freshCandle.CloseTimestamp, freshCandle.QuoteType, freshCandle.Interval);
+ if (ReferenceEquals(historical,null))
+ return freshCandle;
+ historical.MarkIncomplete();
+ return MergeQuoteCandles(historical, freshCandle);
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical QuoteCandleStick: {0}; quote: {1}", e.Message, quote);
+ return freshCandle;
+ }
+ }
+ else
+ {
+ return freshCandle;
+ }
+ }
+
+ private QuoteCandleStick CreateNewBidCandle(Quote quote, double timestamp)
+ {
+ double start = GetNearestModInterval(timestamp, _interval);
+ QuoteCandleStick freshCandle = new QuoteCandleStick(quote.Contract, quote.BidSize, quote.BidPrice, QuoteType.Bid, start, (start + System.Convert.ToDouble((int)_interval)), _interval, timestamp);
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick)
+ {
+ try
+ {
+ QuoteCandleStick historical = GetHistoricalQuoteCandleStick.Invoke(freshCandle.Contract, freshCandle.OpenTimestamp, freshCandle.CloseTimestamp, freshCandle.QuoteType, freshCandle.Interval);
+ if (ReferenceEquals(historical,null))
+ return freshCandle;
+ historical.MarkIncomplete();
+ return MergeQuoteCandles(historical, freshCandle);
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical QuoteCandleStick: {0}; quote: {1}", e.Message, quote);
+ return freshCandle;
+ }
+ }
+ else
+ {
+ return freshCandle;
+ }
+ }
+
+ private void AddAskToLostAndFound(Quote ask)
+ {
+ double ts = ConvertToUnixTimestamp(ask.Timestamp);
+ string key = String.Format("{0}|{1}|{2}", ask.Contract, GetNearestModInterval(ts, _interval), _interval);
+ ContractBucket bucket = GetSlot(key, _lostAndFound, _lostAndFoundLock);
+ try
+ {
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick)
+ {
+ lock (bucket.Locker)
+ {
+ if (bucket.AskCandleStick != null)
+ {
+ bucket.AskCandleStick.Update(ask.AskSize, ask.AskPrice, ts);
+ }
+ else
+ {
+ double start = GetNearestModInterval(ts, _interval);
+ bucket.AskCandleStick = new QuoteCandleStick(ask.Contract, ask.AskSize, ask.AskPrice, QuoteType.Ask, start, (start + System.Convert.ToDouble((int)_interval)), _interval, ts);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning("Error on handling late ask in CandleStick Client: {0}", ex.Message);
+ }
+ }
+
+ private void AddBidToLostAndFound(Quote bid)
+ {
+ double ts = ConvertToUnixTimestamp(bid.Timestamp);
+ string key = String.Format("{0}|{1}|{2}", bid.Contract, GetNearestModInterval(ts, _interval), _interval);
+ ContractBucket bucket = GetSlot(key, _lostAndFound, _lostAndFoundLock);
+ try
+ {
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick)
+ {
+ lock (bucket.Locker)
+ {
+ if (bucket.BidCandleStick != null)
+ {
+ bucket.BidCandleStick.Update(bid.BidSize, bid.AskPrice, ts);
+ }
+ else
+ {
+ double start = GetNearestModInterval(ts, _interval);
+ bucket.BidCandleStick = new QuoteCandleStick(bid.Contract, bid.BidSize, bid.AskPrice, QuoteType.Bid, start, (start + System.Convert.ToDouble((int) _interval)), _interval, ts);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning("Error on handling late bid in CandleStick Client: {0}", ex.Message);
+ }
+ }
+
+ private void AddTradeToLostAndFound(Trade trade)
+ {
+ double ts = ConvertToUnixTimestamp(trade.Timestamp);
+ string key = String.Format("{0}|{1}|{2}", trade.Contract, GetNearestModInterval(ts, _interval), _interval);
+ ContractBucket bucket = GetSlot(key, _lostAndFound, _lostAndFoundLock);
+ try
+ {
+ if (UseGetHistoricalTradeCandleStick && UseOnTradeCandleStick)
+ {
+ lock (bucket.Locker)
+ {
+ if (bucket.TradeCandleStick != null)
+ {
+ bucket.TradeCandleStick.Update(trade.Size, trade.Price, ts);
+ }
+ else
+ {
+ double start = GetNearestModInterval(ts, _interval);
+ bucket.TradeCandleStick = new TradeCandleStick(trade.Contract, trade.Size, trade.Price, start, (start + System.Convert.ToDouble((int)_interval)), _interval, ts);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning("Error on handling late trade in CandleStick Client: {0}", ex.Message);
+ }
+ }
+
+ private void OnAsk(Quote quote, ContractBucket bucket)
+ {
+ double ts = ConvertToUnixTimestamp(quote.Timestamp);
+
+ if (bucket.AskCandleStick != null && !Double.IsNaN(quote.AskPrice))
+ {
+ if (bucket.AskCandleStick.CloseTimestamp < ts)
+ {
+ bucket.AskCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ bucket.AskCandleStick = CreateNewAskCandle(quote, ts);
+ }
+ else if (bucket.AskCandleStick.OpenTimestamp <= ts)
+ {
+ bucket.AskCandleStick.Update(quote.AskSize, quote.AskPrice, ts);
+ if (_broadcastPartialCandles)
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ }
+ else //This is a late event. We already shipped the candle, so add to lost and found
+ {
+ AddAskToLostAndFound(quote);
+ }
+ }
+ else if (bucket.AskCandleStick == null && !Double.IsNaN(quote.AskPrice))
+ {
+ bucket.AskCandleStick = CreateNewAskCandle(quote, ts);
+ if (_broadcastPartialCandles)
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ }
+ }
+
+ private void OnBid(Quote quote, ContractBucket bucket)
+ {
+ double ts = ConvertToUnixTimestamp(quote.Timestamp);
+
+ if (bucket.BidCandleStick != null && !Double.IsNaN(quote.AskPrice))
+ {
+ if (bucket.BidCandleStick.CloseTimestamp < ts)
+ {
+ bucket.BidCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ bucket.BidCandleStick = CreateNewBidCandle(quote, ts);
+ }
+ else if(bucket.BidCandleStick.OpenTimestamp <= ts)
+ {
+ bucket.BidCandleStick.Update(quote.BidSize, quote.AskPrice, ts);
+ if (_broadcastPartialCandles)
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ }
+ else //This is a late event. We already shipped the candle, so add to lost and found
+ {
+ AddBidToLostAndFound(quote);
+ }
+ }
+ else if (bucket.BidCandleStick == null && !Double.IsNaN(quote.AskPrice))
+ {
+ bucket.BidCandleStick = CreateNewBidCandle(quote, ts);
+ if (_broadcastPartialCandles)
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ }
+ }
+
+ private void FlushFn()
+ {
+ Log.Information("Starting candlestick expiration watcher...");
+ CancellationToken ct = _ctSource.Token;
+ System.Threading.Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
+ List keys = new List();
+
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ lock (_contractBucketsLock)
+ {
+ foreach (string key in _contractBuckets.Keys)
+ keys.Add(key);
+ }
+
+ foreach (string key in keys)
+ {
+ ContractBucket bucket = GetSlot(key, _contractBuckets, _contractBucketsLock);
+ double flushThresholdTime = GetCurrentTimestamp(_sourceDelaySeconds) - FlushBufferSeconds;
+
+ lock (bucket.Locker)
+ {
+ if (UseOnTradeCandleStick && bucket.TradeCandleStick != null && (bucket.TradeCandleStick.CloseTimestamp < flushThresholdTime))
+ {
+ bucket.TradeCandleStick.MarkComplete();
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ bucket.TradeCandleStick = null;
+ }
+
+ if (UseOnQuoteCandleStick && bucket.AskCandleStick != null && (bucket.AskCandleStick.CloseTimestamp < flushThresholdTime))
+ {
+ bucket.AskCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ bucket.AskCandleStick = null;
+ }
+
+ if (UseOnQuoteCandleStick && bucket.BidCandleStick != null && (bucket.BidCandleStick.CloseTimestamp < flushThresholdTime))
+ {
+ bucket.BidCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ bucket.BidCandleStick = null;
+ }
+ }
+ }
+ keys.Clear();
+
+ if (!(ct.IsCancellationRequested))
+ Thread.Sleep(1000);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ }
+
+ Log.Information("Stopping candlestick expiration watcher...");
+ }
+
+ private async void LostAndFoundFn()
+ {
+ Log.Information("Starting candlestick late event watcher...");
+ CancellationToken ct = _ctSource.Token;
+ System.Threading.Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
+ List keys = new List();
+
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ lock (_lostAndFoundLock)
+ {
+ foreach (string key in _lostAndFound.Keys)
+ keys.Add(key);
+ }
+
+ foreach (string key in keys)
+ {
+ ContractBucket bucket = GetSlot(key, _lostAndFound, _lostAndFoundLock);
+
+ lock (bucket.Locker)
+ {
+ if (UseGetHistoricalTradeCandleStick && UseOnTradeCandleStick && bucket.TradeCandleStick != null)
+ {
+ try
+ {
+ TradeCandleStick historical = GetHistoricalTradeCandleStick.Invoke(bucket.TradeCandleStick.Contract, bucket.TradeCandleStick.OpenTimestamp, bucket.TradeCandleStick.CloseTimestamp, bucket.TradeCandleStick.Interval);
+ if (ReferenceEquals(historical,null))
+ {
+ bucket.TradeCandleStick.MarkComplete();
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ bucket.TradeCandleStick = null;
+ }
+ else
+ {
+ bucket.TradeCandleStick = MergeTradeCandles(historical, bucket.TradeCandleStick);
+ bucket.TradeCandleStick.MarkComplete();
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ bucket.TradeCandleStick = null;
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical TradeCandleStick: {0}", e.Message);
+ bucket.TradeCandleStick.MarkComplete();
+ OnTradeCandleStick.Invoke(bucket.TradeCandleStick);
+ bucket.TradeCandleStick = null;
+ }
+ }
+ else
+ {
+ bucket.TradeCandleStick = null;
+ }
+
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick && bucket.AskCandleStick != null)
+ {
+ try
+ {
+ QuoteCandleStick historical = GetHistoricalQuoteCandleStick.Invoke(bucket.AskCandleStick.Contract, bucket.AskCandleStick.OpenTimestamp, bucket.AskCandleStick.CloseTimestamp, bucket.AskCandleStick.QuoteType, bucket.AskCandleStick.Interval);
+ if (ReferenceEquals(historical,null))
+ {
+ bucket.AskCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ bucket.AskCandleStick = null;
+ }
+ else
+ {
+ bucket.AskCandleStick = MergeQuoteCandles(historical, bucket.AskCandleStick);
+ bucket.AskCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ bucket.AskCandleStick = null;
+ }
+
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical QuoteCandleStick: {0}", e.Message);
+ bucket.AskCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.AskCandleStick);
+ bucket.AskCandleStick = null;
+ }
+ }
+ else
+ {
+ bucket.AskCandleStick = null;
+ }
+
+ if (UseGetHistoricalQuoteCandleStick && UseOnQuoteCandleStick && bucket.BidCandleStick != null)
+ {
+ try
+ {
+ QuoteCandleStick historical = GetHistoricalQuoteCandleStick.Invoke(bucket.BidCandleStick.Contract, bucket.BidCandleStick.OpenTimestamp, bucket.BidCandleStick.CloseTimestamp, bucket.BidCandleStick.QuoteType, bucket.BidCandleStick.Interval);
+ if (ReferenceEquals(historical,null))
+ {
+ bucket.BidCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ bucket.BidCandleStick = null;
+ }
+ else
+ {
+ bucket.BidCandleStick = MergeQuoteCandles(historical, bucket.BidCandleStick);
+ bucket.BidCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ bucket.BidCandleStick = null;
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Error("Error retrieving historical QuoteCandleStick: {0}", e.Message);
+ bucket.BidCandleStick.MarkComplete();
+ OnQuoteCandleStick.Invoke(bucket.BidCandleStick);
+ bucket.BidCandleStick = null;
+ }
+ }
+ else
+ {
+ bucket.BidCandleStick = null;
+ }
+
+ if (bucket.TradeCandleStick == null && bucket.AskCandleStick == null && bucket.BidCandleStick == null)
+ RemoveSlot(key, _lostAndFound, _lostAndFoundLock);
+ }
+ }
+ keys.Clear();
+
+ if (!ct.IsCancellationRequested)
+ Thread.Sleep(1000);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ }
+
+ Log.Information("Stopping candlestick late event watcher...");
+ }
+
+ #endregion //Private Methods
+
+ private class ContractBucket
+ {
+ public TradeCandleStick TradeCandleStick;
+ public QuoteCandleStick AskCandleStick;
+ public QuoteCandleStick BidCandleStick;
+ public object Locker;
+
+ public ContractBucket(TradeCandleStick tradeCandleStick, QuoteCandleStick askCandleStick, QuoteCandleStick bidCandleStick)
+ {
+ TradeCandleStick = tradeCandleStick;
+ AskCandleStick = askCandleStick;
+ BidCandleStick = bidCandleStick;
+ Locker = new object();
+ }
+ }
+
+ #region Private Static Methods
+ // [SkipLocalsInit]
+ // [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ // private static Span StackAlloc(int length) where T : unmanaged
+ // {
+ // unsafe
+ // {
+ // Span p = stackalloc T[length];
+ // return p;
+ // }
+ // }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static double GetCurrentTimestamp(double delay)
+ {
+ return (DateTime.UtcNow - DateTime.UnixEpoch.ToUniversalTime()).TotalSeconds - delay;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static double GetNearestModInterval(double timestamp, IntervalType interval)
+ {
+ return Convert.ToDouble(Convert.ToUInt64(timestamp) / Convert.ToUInt64((int)interval)) * Convert.ToDouble(((int)interval));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static TradeCandleStick MergeTradeCandles(TradeCandleStick a, TradeCandleStick b)
+ {
+ a.Merge(b);
+ return a;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static QuoteCandleStick MergeQuoteCandles(QuoteCandleStick a, QuoteCandleStick b)
+ {
+ a.Merge(b);
+ return a;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static double ConvertToUnixTimestamp(double input)
+ {
+ return input;
+ }
+
+ private static ContractBucket GetSlot(string key, Dictionary dict, object locker)
+ {
+ ContractBucket value;
+ if (dict.TryGetValue(key, out value))
+ {
+ return value;
+ }
+
+ lock (locker)
+ {
+ if (dict.TryGetValue(key, out value))
+ {
+ return value;
+ }
+
+ ContractBucket bucket = new ContractBucket(null, null, null);
+ dict.Add(key, bucket);
+ return bucket;
+ }
+ }
+
+ private static void RemoveSlot(string key, Dictionary dict, object locker)
+ {
+ if (dict.ContainsKey(key))
+ {
+ lock (locker)
+ {
+ if (dict.ContainsKey(key))
+ {
+ dict.Remove(key);
+ }
+ }
+ }
+ }
+ #endregion //Private Static Methods
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/Conditions.cs b/Intrinio.Realtime/Options/Conditions.cs
new file mode 100644
index 0000000..cf1bc22
--- /dev/null
+++ b/Intrinio.Realtime/Options/Conditions.cs
@@ -0,0 +1,9 @@
+using System.Runtime.CompilerServices;
+
+namespace Intrinio.Realtime.Options;
+
+[InlineArray(4)]
+public struct Conditions
+{
+ public byte _a;
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/Config.cs b/Intrinio.Realtime/Options/Config.cs
new file mode 100644
index 0000000..4f6c094
--- /dev/null
+++ b/Intrinio.Realtime/Options/Config.cs
@@ -0,0 +1,127 @@
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace Intrinio.Realtime.Options;
+
+using System;
+using Serilog;
+using System.IO;
+using Microsoft.Extensions.Configuration;
+
+public class Config
+{
+ public string ApiKey { get; set; }
+
+ public Provider Provider { get; set; }
+
+ public string IPAddress { get; set; }
+
+ public string[] Symbols { get; set; }
+
+ public bool TradesOnly { get; set; }
+
+ public int NumThreads { get; set; }
+
+ public int BufferSize { get; set; }
+
+ public int OverflowBufferSize { get; set; }
+
+ public bool Delayed { get; set; }
+
+ ///
+ /// The configuration for The Options Websocket Client.
+ ///
+ public Config()
+ {
+ ApiKey = String.Empty;
+ Provider = Provider.NONE;
+ IPAddress = String.Empty;
+ Symbols = Array.Empty();
+ TradesOnly = false;
+ NumThreads = 2;
+ BufferSize = 2048;
+ OverflowBufferSize = 2048;
+ Delayed = false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static string TranslateContract(string contract)
+ {
+ if ((contract.Length <= 9) || (contract.IndexOf(".")>=9))
+ {
+ return contract;
+ }
+
+ //this is of the old format and we need to translate it to what the server understands. input: AAPL__220101C00140000, TSLA__221111P00195000
+ string symbol = contract.Substring(0, 6).TrimEnd('_');
+ string date = contract.Substring(6, 6);
+ char callPut = contract[12];
+ string wholePrice = contract.Substring(13, 5).TrimStart('0');
+ if (wholePrice == String.Empty)
+ {
+ wholePrice = "0";
+ }
+
+ string decimalPrice = contract.Substring(18);
+
+ if (decimalPrice[2] == '0')
+ decimalPrice = decimalPrice.Substring(0, 2);
+
+ return String.Format($"{symbol}_{date}{callPut}{wholePrice}.{decimalPrice}");
+ }
+
+ public void Validate()
+ {
+ if (String.IsNullOrWhiteSpace(ApiKey))
+ {
+ throw new ArgumentException("You must provide a valid API key");
+ }
+
+ if (Provider == Provider.NONE)
+ {
+ throw new ArgumentException("You must specify a valid 'provider'");
+ }
+
+ if ((Provider == Provider.MANUAL) && (String.IsNullOrWhiteSpace(IPAddress)))
+ {
+ throw new ArgumentException("You must specify an IP address for manual configuration");
+ }
+
+ if (NumThreads <= 0)
+ {
+ throw new ArgumentException("You must specify a valid 'NumThreads'");
+ }
+
+ if (BufferSize < 2048)
+ {
+ throw new ArgumentException("'BufferSize' must be greater than or equal to 2048.");
+ }
+
+ if (OverflowBufferSize < 2048)
+ {
+ throw new ArgumentException("'OverflowBufferSize' must be greater than or equal to 2048.");
+ }
+
+ for (int i = 0; i < Symbols.Length; i++)
+ {
+ Symbols[i] = TranslateContract(Symbols[i]);
+ }
+ }
+
+ public static Config LoadConfig()
+ {
+ Log.Information("Loading application configuration");
+ var rawConfig = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("config.json").Build();
+ Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(rawConfig).CreateLogger();
+ Config config = new Config();
+
+ foreach (KeyValuePair kvp in rawConfig.AsEnumerable())
+ {
+ Log.Debug("Key: {0}, Value:{1}", kvp.Key, kvp.Value);
+ }
+
+ rawConfig.Bind("Config", config);
+ config.Validate();
+ return config;
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/Exchange.cs b/Intrinio.Realtime/Options/Exchange.cs
new file mode 100644
index 0000000..53bcac5
--- /dev/null
+++ b/Intrinio.Realtime/Options/Exchange.cs
@@ -0,0 +1,24 @@
+namespace Intrinio.Realtime.Options;
+
+public enum Exchange
+{
+ UNKNOWN = '?',
+ NYSE_AMERICAN = 'A',
+ BOSTON = 'B',
+ CBOE = 'C',
+ MIAMI_EMERALD = 'D',
+ BATS_EDGX = 'E',
+ ISE_GEMINI = 'H',
+ ISE = 'I',
+ MERCURY = 'J',
+ MIAMI = 'M',
+ NYSE_ARCA = 'N',
+ MIAMI_PEARL = 'P',
+ NASDAQ = 'Q',
+ MIAX_SAPPHIRE = 'S',
+ NASDAQ_BX = 'T',
+ MEMX = 'U',
+ CBOE_C2 = 'W',
+ PHLX = 'X',
+ BATS_BZX = 'Z'
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/Helpers.cs b/Intrinio.Realtime/Options/Helpers.cs
new file mode 100644
index 0000000..e323bd2
--- /dev/null
+++ b/Intrinio.Realtime/Options/Helpers.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Intrinio.Realtime.Options;
+
+internal static class Helpers
+{
+ internal static readonly double[] _priceTypeDivisorTable = new double[]
+ {
+ 1,
+ 10,
+ 100,
+ 1000,
+ 10000,
+ 100000,
+ 1000000,
+ 10000000,
+ 100000000,
+ 1000000000,
+ 512,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ Double.NaN
+ };
+
+ // []
+ // let inline internal stackalloc<'a when 'a: unmanaged> (length: int): Span<'a> =
+ // let p = NativePtr.stackalloc<'a> length |> NativePtr.toVoidPtr
+ // Span<'a>(p, length)
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static double ScaleUInt64Price(UInt64 price, byte priceType)
+ {
+ return ((double)price) / _priceTypeDivisorTable[(int)priceType];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static double ScaleInt32Price(int price, byte priceType)
+ {
+ return ((double)price) / _priceTypeDivisorTable[(int)priceType];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static double ScaleTimestampToSeconds(UInt64 nanoseconds)
+ {
+ return ((double) nanoseconds) / 1_000_000_000.0;
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/IOptionsWebSocketClient.cs b/Intrinio.Realtime/Options/IOptionsWebSocketClient.cs
new file mode 100644
index 0000000..0d451a3
--- /dev/null
+++ b/Intrinio.Realtime/Options/IOptionsWebSocketClient.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Intrinio.Realtime.Options;
+
+public interface IOptionsWebSocketClient
+{
+ public Action OnTrade { set; }
+ public Action OnQuote { set; }
+ public Action OnRefresh { set; }
+ public Action OnUnusualActivity { set; }
+ public Task Join();
+ public Task Join(string channel, bool? tradesOnly);
+ public Task JoinLobby(bool? tradesOnly);
+ public Task Join(string[] channels, bool? tradesOnly);
+ public Task Leave();
+ public Task Leave(string channel);
+ public Task LeaveLobby();
+ public Task Leave(string[] channels);
+ public Task Stop();
+ public Task Start();
+ public ClientStats GetStats();
+ public UInt64 TradeCount { get; }
+ public UInt64 QuoteCount { get; }
+ public UInt64 RefreshCount { get; }
+ public UInt64 UnusualActivityCount { get; }
+ public void LogMessage(LogLevel logLevel, string messageTemplate, params object[] propertyValues);
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/MessageType.cs b/Intrinio.Realtime/Options/MessageType.cs
new file mode 100644
index 0000000..f5787da
--- /dev/null
+++ b/Intrinio.Realtime/Options/MessageType.cs
@@ -0,0 +1,9 @@
+namespace Intrinio.Realtime.Options;
+
+public enum MessageType
+{
+ Trade = 0,
+ Quote = 1,
+ Refresh = 2,
+ UnusualActivity = 3
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/OptionsWebSocketClient.cs b/Intrinio.Realtime/Options/OptionsWebSocketClient.cs
new file mode 100644
index 0000000..d27b4f3
--- /dev/null
+++ b/Intrinio.Realtime/Options/OptionsWebSocketClient.cs
@@ -0,0 +1,582 @@
+using System;
+using System.Text;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Intrinio.Realtime.Options;
+
+public class OptionsWebSocketClient : WebSocketClient, IOptionsWebSocketClient
+{
+ #region Data Members
+
+ private const string LobbyName = "lobby";
+ private const uint MaxMessageSize = 75u;
+ private const int MessageTypeIndex = 22;
+ private const int TradeMessageSize = 72;
+ private const int QuoteMessageSize = 52;
+ private const int RefreshMessageSize = 52;
+ private const int UnusualActivityMessageSize = 74;
+ private bool _useOnTrade;
+ private bool _useOnQuote;
+ private bool _useOnRefresh;
+ private bool _useOnUnusualActivity;
+ private Action _onTrade;
+
+ ///
+ /// The callback for when a trade event occurs.
+ ///
+ public Action OnTrade
+ {
+ set
+ {
+ _useOnTrade = !ReferenceEquals(value, null);
+ _onTrade = value;
+ }
+ }
+
+ private Action _onQuote;
+
+ ///
+ /// The callback for when a quote event occurs.
+ ///
+ public Action OnQuote
+ {
+ set
+ {
+ _useOnQuote = !ReferenceEquals(value, null);
+ _onQuote = value;
+ }
+ }
+
+ private Action _onRefresh;
+
+ ///
+ /// The callback for when a refresh event occurs.
+ ///
+ public Action OnRefresh
+ {
+ set
+ {
+ _useOnRefresh = !ReferenceEquals(value, null);
+ _onRefresh = value;
+ }
+ }
+
+ private Action _onUnusualActivity;
+
+ ///
+ /// The callback for when an unusual activity event occurs.
+ ///
+ public Action OnUnusualActivity
+ {
+ set
+ {
+ _useOnUnusualActivity = !ReferenceEquals(value, null);
+ _onUnusualActivity = value;
+ }
+ }
+
+ private readonly Config _config;
+ private UInt64 _dataTradeCount = 0UL;
+ private UInt64 _dataQuoteCount = 0UL;
+ private UInt64 _dataRefreshCount = 0UL;
+ private UInt64 _dataUnusualActivityCount = 0UL;
+ public UInt64 TradeCount { get { return Interlocked.Read(ref _dataTradeCount); } }
+ public UInt64 QuoteCount { get { return Interlocked.Read(ref _dataQuoteCount); } }
+ public UInt64 RefreshCount { get { return Interlocked.Read(ref _dataRefreshCount); } }
+ public UInt64 UnusualActivityCount { get { return Interlocked.Read(ref _dataUnusualActivityCount); } }
+
+ private readonly string _logPrefix;
+ private const string MessageVersionHeaderKey = "UseNewOptionsFormat";
+ private const string MessageVersionHeaderValue = "v2";
+ private const string DelayHeaderKey = "delay";
+ private const string DelayHeaderValue = "true";
+ private const string ChannelFormat = "{0}|TradesOnly|{1}";
+ #endregion //Data Members
+
+ #region Constuctors
+ ///
+ /// Create a new Options websocket client.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public OptionsWebSocketClient(Action onTrade, Action onQuote, Action onRefresh, Action onUnusualActivity, Config config)
+ : base(Convert.ToUInt32(config.NumThreads), Convert.ToUInt32(config.BufferSize), Convert.ToUInt32(config.OverflowBufferSize), MaxMessageSize)
+ {
+ OnTrade = onTrade;
+ OnQuote = onQuote;
+ OnRefresh = onRefresh;
+ OnUnusualActivity = onUnusualActivity;
+ _config = config;
+
+ if (ReferenceEquals(null, _config))
+ throw new ArgumentException("Config may not be null.");
+ _config.Validate();
+ _logPrefix = String.Format("{0}: ", _config?.Provider.ToString());
+ }
+
+ ///
+ /// Create a new Options websocket client.
+ ///
+ ///
+ public OptionsWebSocketClient(Action onTrade) : this(onTrade, null, null, null, Config.LoadConfig())
+ {
+ }
+
+ ///
+ /// Create a new Options websocket client.
+ ///
+ ///
+ public OptionsWebSocketClient(Action onQuote) : this(null, onQuote, null, null, Config.LoadConfig())
+ {
+ }
+
+ ///
+ /// Create a new Options websocket client.
+ ///
+ ///
+ ///
+ public OptionsWebSocketClient(Action onTrade, Action onQuote) : this(onTrade, onQuote, null, null, Config.LoadConfig())
+ {
+ }
+
+ ///
+ /// Create a new Options websocket client.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public OptionsWebSocketClient(Action onTrade, Action onQuote, Action onRefresh, Action onUnusualActivity) : this(onTrade, onQuote, onRefresh, onUnusualActivity, Config.LoadConfig())
+ {
+ }
+ #endregion //Constructors
+
+ #region Public Methods
+ public async Task Join()
+ {
+ while (!IsReady())
+ await Task.Delay(1000);
+ HashSet channelsToAdd = _config.Symbols.Select(s => GetChannel(s, _config.TradesOnly)).ToHashSet();
+ channelsToAdd.ExceptWith(Channels);
+ foreach (string channel in channelsToAdd)
+ await JoinImpl(channel);
+ }
+
+ public async Task Join(string symbol, bool? tradesOnly)
+ {
+ bool t = tradesOnly.HasValue ? tradesOnly.Value || _config.TradesOnly : false || _config.TradesOnly;
+ while (!IsReady())
+ await Task.Delay(1000);
+ if (!Channels.Contains(GetChannel(symbol, t)))
+ await JoinImpl(GetChannel(symbol, t));
+ }
+
+ public async Task JoinLobby(bool? tradesOnly)
+ {
+ await Join(LobbyName, tradesOnly);
+ }
+
+ public async Task Join(string[] symbols, bool? tradesOnly)
+ {
+ bool t = tradesOnly.HasValue ? tradesOnly.Value || _config.TradesOnly : false || _config.TradesOnly;
+ while (!IsReady())
+ await Task.Delay(1000);
+ HashSet symbolsToAdd = symbols.Select(s => GetChannel(s, t)).ToHashSet();
+ symbolsToAdd.ExceptWith(Channels);
+ foreach (string channel in symbolsToAdd)
+ await JoinImpl(channel);
+ }
+
+ public async Task Leave()
+ {
+ await LeaveImpl();
+ }
+
+ public async Task Leave(string symbol)
+ {
+ foreach (string channel in Channels.Where(c => symbol == GetSymbolFromChannel(c)))
+ await LeaveImpl(channel);
+ }
+
+ public async Task LeaveLobby()
+ {
+ await Leave(LobbyName);
+ }
+
+ public async Task Leave(string[] symbols)
+ {
+ HashSet hashSymbols = new HashSet(symbols);
+ foreach (string channel in Channels.Where(c => hashSymbols.Contains(GetSymbolFromChannel(c))))
+ await LeaveImpl(channel);
+ }
+ #endregion //Public Methods
+
+ #region Private Methods
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private string GetChannel(string symbol, bool tradesOnly)
+ {
+ return String.Format(ChannelFormat, symbol, _config.TradesOnly.ToString());
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private string GetSymbolFromChannel(string channel)
+ {
+ return channel.Split('|')[0];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool GetTradesOnlyFromChannel(string channel)
+ {
+ return Boolean.Parse(channel.Split('|')[2]);
+ }
+
+ protected override string GetLogPrefix()
+ {
+ return _logPrefix;
+ }
+
+ protected override string GetAuthUrl()
+ {
+ switch (_config.Provider)
+ {
+ case Provider.OPRA:
+ return $"https://realtime-options.intrinio.com/auth?api_key={_config.ApiKey}";
+ break;
+ case Provider.MANUAL:
+ return $"http://{_config.IPAddress}/auth?api_key={_config.ApiKey}";
+ break;
+ default:
+ throw new ArgumentException("Provider not specified!");
+ break;
+ }
+ }
+
+ protected override string GetWebSocketUrl(string token)
+ {
+ string delayedPart = _config.Delayed ? "&delayed=true" : String.Empty;
+ switch (_config.Provider)
+ {
+ case Provider.OPRA:
+ return $"wss://realtime-options.intrinio.com/socket/websocket?vsn=1.0.0&token={token}{delayedPart}";
+ break;
+ case Provider.MANUAL:
+ return $"ws://{_config.IPAddress}/socket/websocket?vsn=1.0.0&token={token}{delayedPart}";
+ break;
+ default:
+ throw new ArgumentException("Provider not specified!");
+ break;
+ }
+ }
+
+ protected override List> GetCustomSocketHeaders()
+ {
+ List> headers = new List>();
+ headers.Add(new KeyValuePair(MessageVersionHeaderKey, MessageVersionHeaderValue));
+ if (_config.Delayed)
+ headers.Add(new KeyValuePair(DelayHeaderKey, DelayHeaderValue));
+ return headers;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override int GetNextChunkLength(ReadOnlySpan bytes)
+ {
+ byte msgType = bytes[MessageTypeIndex]; //in the bytes, symbol length is first, then symbol, then msg type.
+
+ //using if-else vs switch for hotpathing
+ if (msgType == 1u)
+ return QuoteMessageSize;
+ if (msgType == 0u)
+ return TradeMessageSize;
+ if (msgType == 2u)
+ return RefreshMessageSize;
+ return UnusualActivityMessageSize;
+ }
+
+ private string FormatContract(ReadOnlySpan alternateFormattedChars)
+ {
+ //Transform from server format to normal format
+ //From this: AAPL_201016C100.00 or ABC_201016C100.003
+ //Patch: some upstream contracts now have 4 decimals. We are truncating the last decimal for now to fit in this format.
+ //To this: AAPL__201016C00100000 or ABC___201016C00100003
+ // strike: 5 whole digits, 3 decimal digits
+
+ Span contractChars = stackalloc byte[21];
+ contractChars[0] = (byte)'_';
+ contractChars[1] = (byte)'_';
+ contractChars[2] = (byte)'_';
+ contractChars[3] = (byte)'_';
+ contractChars[4] = (byte)'_';
+ contractChars[5] = (byte)'_';
+ contractChars[6] = (byte)'2';
+ contractChars[7] = (byte)'2';
+ contractChars[8] = (byte)'0';
+ contractChars[9] = (byte)'1';
+ contractChars[10] = (byte)'0';
+ contractChars[11] = (byte)'1';
+ contractChars[12] = (byte)'C';
+ contractChars[13] = (byte)'0';
+ contractChars[14] = (byte)'0';
+ contractChars[15] = (byte)'0';
+ contractChars[16] = (byte)'0';
+ contractChars[17] = (byte)'0';
+ contractChars[18] = (byte)'0';
+ contractChars[19] = (byte)'0';
+ contractChars[20] = (byte)'0';
+
+ int underscoreIndex = alternateFormattedChars.IndexOf((byte)'_');
+ int decimalIndex = alternateFormattedChars.Slice(9).IndexOf((byte)'.') + 9; //ignore decimals in tickersymbol
+
+ alternateFormattedChars.Slice(0, underscoreIndex).CopyTo(contractChars); //copy symbol
+ alternateFormattedChars.Slice(underscoreIndex + 1, 6).CopyTo(contractChars.Slice(6)); //copy date
+ alternateFormattedChars.Slice(underscoreIndex + 7, 1).CopyTo(contractChars.Slice(12)); //copy put/call
+ alternateFormattedChars.Slice(underscoreIndex + 8, decimalIndex - underscoreIndex - 8).CopyTo(contractChars.Slice(18 - (decimalIndex - underscoreIndex - 8))); //whole number copy
+ alternateFormattedChars.Slice(decimalIndex + 1, Math.Min(3, alternateFormattedChars.Length - decimalIndex - 1)).CopyTo(contractChars.Slice(18)); //decimal number copy. Truncate decimals over 3 digits for now.
+
+ return Encoding.ASCII.GetString(contractChars);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private Exchange ParseExchange(char c)
+ {
+ switch (c)
+ {
+ case 'A':
+ case 'a':
+ return Exchange.NYSE_AMERICAN;
+ case 'B':
+ case 'b':
+ return Exchange.BOSTON;
+ case 'C':
+ case 'c':
+ return Exchange.CBOE;
+ case 'D':
+ case 'd':
+ return Exchange.MIAMI_EMERALD;
+ case 'E':
+ case 'e':
+ return Exchange.BATS_EDGX;
+ case 'H':
+ case 'h':
+ return Exchange.ISE_GEMINI;
+ case 'I':
+ case 'i':
+ return Exchange.ISE;
+ case 'J':
+ case 'j':
+ return Exchange.MERCURY;
+ case 'M':
+ case 'm':
+ return Exchange.MIAMI;
+ case 'N':
+ case 'n':
+ case 'P':
+ case 'p':
+ return Exchange.NYSE_ARCA;
+ case 'O':
+ case 'o':
+ return Exchange.MIAMI_PEARL;
+ case 'Q':
+ case 'q':
+ return Exchange.NASDAQ;
+ case 'S':
+ case 's':
+ return Exchange.MIAX_SAPPHIRE;
+ case 'T':
+ case 't':
+ return Exchange.NASDAQ_BX;
+ case 'U':
+ case 'u':
+ return Exchange.MEMX;
+ case 'W':
+ case 'w':
+ return Exchange.CBOE_C2;
+ case 'X':
+ case 'x':
+ return Exchange.PHLX;
+ case 'Z':
+ case 'z':
+ return Exchange.BATS_BZX;
+ default:
+ return Exchange.UNKNOWN;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private Trade ParseTrade(ReadOnlySpan bytes)
+ {
+ Conditions conditions = new Conditions();
+ bytes.Slice(61, 4).CopyTo(conditions);
+
+ return new Trade(FormatContract(bytes.Slice(1, (int)bytes[0])),
+ ParseExchange((char)bytes[65]),
+ bytes[23],
+ bytes[24],
+ BitConverter.ToInt32(bytes.Slice(25, 4)),
+ BitConverter.ToUInt32(bytes.Slice(29, 4)),
+ BitConverter.ToUInt64(bytes.Slice(33, 8)),
+ BitConverter.ToUInt64(bytes.Slice(41, 8)),
+ conditions,
+ BitConverter.ToInt32(bytes.Slice(49, 4)),
+ BitConverter.ToInt32(bytes.Slice(53, 4)),
+ BitConverter.ToInt32(bytes.Slice(57, 4))
+ );
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private Quote ParseQuote(ReadOnlySpan bytes)
+ {
+ return new Quote(FormatContract(bytes.Slice(1, (int)bytes[0])),
+ bytes[23],
+ BitConverter.ToInt32(bytes.Slice(24, 4)),
+ BitConverter.ToUInt32(bytes.Slice(28, 4)),
+ BitConverter.ToInt32(bytes.Slice(32, 4)),
+ BitConverter.ToUInt32(bytes.Slice(36, 4)),
+ BitConverter.ToUInt64(bytes.Slice(40, 8))
+ );
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private Refresh ParseRefresh(ReadOnlySpan bytes)
+ {
+ return new Refresh(FormatContract(bytes.Slice(1, (int)bytes[0])),
+ bytes[23],
+ BitConverter.ToUInt32(bytes.Slice(24, 4)),
+ BitConverter.ToInt32(bytes.Slice(28, 4)),
+ BitConverter.ToInt32(bytes.Slice(32, 4)),
+ BitConverter.ToInt32(bytes.Slice(36, 4)),
+ BitConverter.ToInt32(bytes.Slice(40, 4))
+ );
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private UnusualActivity ParseUnusualActivity(ReadOnlySpan bytes)
+ {
+ return new UnusualActivity(FormatContract(bytes.Slice(1, (int)bytes[0])),
+ (UAType)((int)bytes[22]),
+ (UASentiment)((int)bytes[23]),
+ bytes[24],
+ bytes[25],
+ BitConverter.ToUInt64(bytes.Slice(26, 8)),
+ BitConverter.ToUInt32(bytes.Slice(34, 4)),
+ BitConverter.ToInt32(bytes.Slice(38, 4)),
+ BitConverter.ToInt32(bytes.Slice(42, 4)),
+ BitConverter.ToInt32(bytes.Slice(46, 4)),
+ BitConverter.ToInt32(bytes.Slice(50, 4)),
+ BitConverter.ToUInt64(bytes.Slice(54, 8))
+ );
+ }
+
+ protected override void HandleMessage(ReadOnlySpan bytes)
+ {
+ byte msgType = bytes[MessageTypeIndex];
+
+ if (msgType == 1u && _useOnQuote)
+ {
+ Quote quote = ParseQuote(bytes);
+ Interlocked.Increment(ref _dataQuoteCount);
+ try { _onQuote.Invoke(quote); }
+ catch (Exception e)
+ {
+ LogMessage(LogLevel.ERROR, "Error while invoking user supplied OnQuote: {0}; {1}", new object[]{e.Message, e.StackTrace});
+ }
+ }
+ if (msgType == 0u && _useOnTrade)
+ {
+ Trade trade = ParseTrade(bytes);
+ Interlocked.Increment(ref _dataTradeCount);
+ try { _onTrade.Invoke(trade); }
+ catch (Exception e)
+ {
+ LogMessage(LogLevel.ERROR, "Error while invoking user supplied OnTrade: {0}; {1}", new object[]{e.Message, e.StackTrace});
+ }
+ }
+ if (msgType == 2u && _useOnRefresh)
+ {
+ Refresh refresh = ParseRefresh(bytes);
+ Interlocked.Increment(ref _dataRefreshCount);
+ try { _onRefresh.Invoke(refresh); }
+ catch (Exception e)
+ {
+ LogMessage(LogLevel.ERROR, "Error while invoking user supplied OnRefresh: {0}; {1}", new object[]{e.Message, e.StackTrace});
+ }
+ }
+ if (msgType > 2u && _useOnUnusualActivity)
+ {
+ UnusualActivity unusualActivity = ParseUnusualActivity(bytes);
+ Interlocked.Increment(ref _dataUnusualActivityCount);
+ try { _onUnusualActivity.Invoke(unusualActivity); }
+ catch (Exception e)
+ {
+ LogMessage(LogLevel.ERROR, "Error while invoking user supplied OnUnusualActivity: {0}; {1}", new object[]{e.Message, e.StackTrace});
+ }
+ }
+ }
+
+ protected override byte[] MakeJoinMessage(string channel)
+ {
+ string symbol = GetSymbolFromChannel(channel);
+ bool tradesOnly = GetTradesOnlyFromChannel(channel);
+ byte mask = 0;
+ if (_useOnTrade) SetUsesTrade(ref mask);
+ if (_useOnQuote && !tradesOnly) SetUsesQuote(ref mask);
+ if (_useOnRefresh) SetUsesRefresh(ref mask);
+ if (_useOnUnusualActivity) SetUsesUA(ref mask);
+ switch (symbol)
+ {
+ case LobbyName:
+ {
+ byte[] message = new byte[11]; //1 + 1 + 9
+ message[0] = Convert.ToByte(74); //type: join (74uy) or leave (76uy)
+ message[1] = mask;
+ Encoding.ASCII.GetBytes("$FIREHOSE").CopyTo(message, 2);
+ return message;
+ }
+ default:
+ {
+ string translatedSymbol = Config.TranslateContract(symbol);
+ byte[] message = new byte[2 + translatedSymbol.Length]; //1 + 1 + symbol.Length
+ message[0] = Convert.ToByte(74); //type: join (74uy) or leave (76uy)
+ message[1] = mask;
+ Encoding.ASCII.GetBytes(translatedSymbol).CopyTo(message, 2);
+ return message;
+ }
+ }
+ }
+
+ protected override byte[] MakeLeaveMessage(string channel)
+ {
+ string symbol = GetSymbolFromChannel(channel);
+ bool tradesOnly = GetTradesOnlyFromChannel(channel);
+ switch (symbol)
+ {
+ case LobbyName:
+ {
+ byte[] message = new byte[10]; // 1 (type = join) + 9 (symbol = $FIREHOSE)
+ message[0] = Convert.ToByte(76); //type: join (74uy) or leave (76uy)
+ Encoding.ASCII.GetBytes("$FIREHOSE").CopyTo(message, 1);
+ return message;
+ }
+ default:
+ {
+ string translatedSymbol = Config.TranslateContract(symbol);
+ byte[] message = new byte[2 + translatedSymbol.Length]; //1 + symbol.Length
+ message[0] = Convert.ToByte(76); //type: join (74uy) or leave (76uy)
+ Encoding.ASCII.GetBytes(translatedSymbol).CopyTo(message, 2);
+ return message;
+ }
+ }
+ }
+
+ private static void SetUsesTrade(ref byte bitMask) { bitMask = (byte)(bitMask | 0b1); }
+ private static void SetUsesQuote(ref byte bitMask) { bitMask = (byte)(bitMask | 0b10); }
+ private static void SetUsesRefresh(ref byte bitMask) { bitMask = (byte)(bitMask | 0b100); }
+ private static void SetUsesUA(ref byte bitMask) { bitMask = (byte)(bitMask | 0b1000); }
+ #endregion //Private Methods
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/Provider.cs b/Intrinio.Realtime/Options/Provider.cs
new file mode 100644
index 0000000..a813b77
--- /dev/null
+++ b/Intrinio.Realtime/Options/Provider.cs
@@ -0,0 +1,8 @@
+namespace Intrinio.Realtime.Options;
+
+public enum Provider
+{
+ NONE = 0,
+ OPRA = 1,
+ MANUAL = 2
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/Quote.cs b/Intrinio.Realtime/Options/Quote.cs
new file mode 100644
index 0000000..5f9f7cd
--- /dev/null
+++ b/Intrinio.Realtime/Options/Quote.cs
@@ -0,0 +1,95 @@
+using System.Globalization;
+
+namespace Intrinio.Realtime.Options;
+
+using System;
+
+public struct Quote
+{
+ private readonly string _contract;
+ private readonly byte _priceType;
+ private readonly Int32 _askPrice;
+ private readonly UInt32 _askSize;
+ private readonly Int32 _bidPrice;
+ private readonly UInt32 _bidSize;
+ private readonly UInt64 _timeStamp;
+
+ public string Contract { get { return _contract;} }
+ public double AskPrice { get { return (_askPrice == Int32.MaxValue) || (_askPrice == Int32.MinValue) ? Double.NaN : Helpers.ScaleInt32Price(_askPrice, _priceType);} }
+ public UInt32 AskSize { get { return _askSize;} }
+ public double BidPrice { get { return (_bidPrice == Int32.MaxValue) || (_bidPrice == Int32.MinValue) ? Double.NaN : Helpers.ScaleInt32Price(_bidPrice, _priceType);} }
+ public UInt32 BidSize { get { return _bidSize;} }
+ public double Timestamp { get { return Helpers.ScaleTimestampToSeconds(_timeStamp);} }
+
+ ///
+ /// A 'Quote' is a unit of data representing a conflated market bid and/or ask event.
+ ///
+ /// The id of the option contract (e.g. AAPL__201016C00100000).
+ /// The scalar for the prices.
+ /// The dollar price of the last ask.
+ /// The number of contacts for the ask.
+ /// The dollars price of the last bid.
+ /// The number of contacts for the bid.
+ /// The time that the Quote was made (a unix timestamp representing the number of seconds (or better) since the unix epoch).
+ public Quote(string contract, byte priceType, Int32 askPrice, UInt32 askSize, Int32 bidPrice, UInt32 bidSize, UInt64 timeStamp)
+ {
+ _contract = contract;
+ _priceType = priceType;
+ _askPrice = askPrice;
+ _askSize = askSize;
+ _bidPrice = bidPrice;
+ _bidSize = bidSize;
+ _timeStamp = timeStamp;
+ }
+
+ public override string ToString()
+ {
+ return $"Quote (Contract: {Contract}, AskPrice: {AskPrice.ToString("f3")}, AskSize: {AskSize.ToString()}, BidPrice: {BidPrice.ToString("f3")}, BidSize: {BidSize.ToString()}, Timestamp: {Timestamp.ToString("f6")})";
+ }
+
+ public string GetUnderlyingSymbol()
+ {
+ return Contract.Substring(0, 6).TrimEnd('_');
+ }
+
+ public DateTime GetExpirationDate()
+ {
+ return DateTime.ParseExact(Contract.Substring(6, 6), "yyMMdd", CultureInfo.InvariantCulture);
+ }
+
+ public bool IsCall()
+ {
+ return Contract[12] == 'C';
+ }
+
+ public bool IsPut()
+ {
+ return Contract[12] == 'P';
+ }
+
+ public double GetStrikePrice()
+ {
+ const UInt32 zeroChar = (UInt32)'0';
+
+ UInt32 whole = ((UInt32)Contract[13] - zeroChar) * 10_000u
+ + ((UInt32)Contract[14] - zeroChar) * 1_000u
+ + ((UInt32)Contract[15] - zeroChar) * 100u
+ + ((UInt32)Contract[16] - zeroChar) * 10u
+ + ((UInt32)Contract[17] - zeroChar) * 1u;
+
+ double part = Convert.ToDouble((UInt32) Contract[18] - zeroChar) * 0.1D
+ + Convert.ToDouble((UInt32) Contract[19] - zeroChar) * 0.01D
+ + Convert.ToDouble((UInt32) Contract[20] - zeroChar) * 0.001D;
+
+ return Convert.ToDouble(whole) + part;
+ }
+
+ public static Quote CreateUnitTestObject(string contract, double askPrice, UInt32 askSize, double bidPrice, UInt32 bidSize, UInt64 nanoSecondsSinceUnixEpoch)
+ {
+ byte priceType = (byte)4;
+ int unscaledAskPrice = Convert.ToInt32(askPrice * 10000.0);
+ int unscaledBidPrice = Convert.ToInt32(bidPrice * 10000.0);
+ Quote quote = new Quote(contract, priceType, unscaledAskPrice, askSize, unscaledBidPrice, bidSize, nanoSecondsSinceUnixEpoch);
+ return quote;
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/QuoteCandleStick.cs b/Intrinio.Realtime/Options/QuoteCandleStick.cs
new file mode 100644
index 0000000..5b5362b
--- /dev/null
+++ b/Intrinio.Realtime/Options/QuoteCandleStick.cs
@@ -0,0 +1,167 @@
+using System.Globalization;
+
+namespace Intrinio.Realtime.Options;
+
+using System;
+
+public class QuoteCandleStick : CandleStick, IEquatable, IComparable, IComparable
+{
+ private readonly string _contract;
+ private readonly QuoteType _quoteType;
+ public string Contract
+ {
+ get { return _contract; }
+ }
+ public QuoteType QuoteType
+ {
+ get { return _quoteType; }
+ }
+
+ public QuoteCandleStick(string contract, UInt32 volume, double price, QuoteType quoteType, double openTimestamp, double closeTimestamp, IntervalType interval, double quoteTime)
+ :base(volume, price, openTimestamp, closeTimestamp, interval, quoteTime)
+ {
+ _contract = contract;
+ _quoteType = quoteType;
+ }
+
+ public QuoteCandleStick(string contract, UInt32 volume, double high, double low, double closePrice, double openPrice, QuoteType quoteType, double openTimestamp, double closeTimestamp, double firstTimestamp, double lastTimestamp, bool complete, double average, double change, IntervalType interval)
+ : base(volume, high, low, closePrice, openPrice, openTimestamp, closeTimestamp, firstTimestamp, lastTimestamp, complete, average, change, interval)
+ {
+ _contract = contract;
+ _quoteType = quoteType;
+ }
+
+ public override bool Equals(object other)
+ {
+ return ((!(ReferenceEquals(other, null))) && ReferenceEquals(this, other))
+ || (
+ (!(ReferenceEquals(other, null)))
+ && (!(ReferenceEquals(this, other)))
+ && (other is QuoteCandleStick)
+ && (Contract.Equals(((QuoteCandleStick)other).Contract))
+ && (Interval.Equals(((QuoteCandleStick)other).Interval))
+ && (QuoteType.Equals(((QuoteCandleStick)other).QuoteType))
+ && (OpenTimestamp.Equals(((QuoteCandleStick)other).OpenTimestamp))
+ );
+ }
+
+ public override int GetHashCode()
+ {
+ return Contract.GetHashCode() ^ Interval.GetHashCode() ^ OpenTimestamp.GetHashCode() ^ QuoteType.GetHashCode();
+ }
+
+ public bool Equals(QuoteCandleStick other)
+ {
+ return ((!(ReferenceEquals(other, null))) && ReferenceEquals(this, other))
+ || (
+ (!(ReferenceEquals(other, null)))
+ && (!(ReferenceEquals(this, other)))
+ && (Contract.Equals(other.Contract))
+ && (Interval.Equals(other.Interval))
+ && (QuoteType.Equals(other.QuoteType))
+ && (OpenTimestamp.Equals(other.OpenTimestamp))
+ );
+ }
+
+ public int CompareTo(object other)
+ {
+ return Equals(other) switch
+ {
+ true => 0,
+ false => ReferenceEquals(other, null) switch
+ {
+ true => 1,
+ false => (other is QuoteCandleStick) switch
+ {
+ true => Contract.CompareTo(((QuoteCandleStick)other).Contract) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => Interval.CompareTo(((QuoteCandleStick)other).Interval) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => QuoteType.CompareTo(((QuoteCandleStick)other).QuoteType) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => OpenTimestamp.CompareTo(((QuoteCandleStick)other).OpenTimestamp)
+ }
+ }
+ },
+ false => 1
+ }
+ }
+ };
+ }
+
+ public int CompareTo(QuoteCandleStick other)
+ {
+ return Equals(other) switch
+ {
+ true => 0,
+ false => Object.ReferenceEquals(other, null) switch
+ {
+ true => 1,
+ false => Contract.CompareTo(other.Contract) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => Interval.CompareTo(other.Interval) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => QuoteType.CompareTo(other.QuoteType) switch
+ {
+ < 0 => -1,
+ > 0 => 1,
+ 0 => OpenTimestamp.CompareTo(other.OpenTimestamp)
+ }
+ }
+ }
+ }
+ };
+ }
+
+ public override string ToString()
+ {
+ return $"QuoteCandleStick (Contract: {Contract}, QuoteType: {QuoteType.ToString()}, High: {High.ToString("f3")}, Low: {Low.ToString("f3")}, Close: {Close.ToString("f3")}, Open: {Open.ToString("f3")}, OpenTimestamp: {OpenTimestamp.ToString("f6")}, CloseTimestamp: {CloseTimestamp.ToString("f6")}, Change: {Change.ToString("f6")}, Complete: {Complete.ToString()})";
+ }
+
+ public string GetUnderlyingSymbol()
+ {
+ return Contract.Substring(0, 6).TrimEnd('_');
+ }
+
+ public DateTime GetExpirationDate()
+ {
+ return DateTime.ParseExact(Contract.Substring(6, 6), "yyMMdd", CultureInfo.InvariantCulture);
+ }
+
+ public bool IsCall()
+ {
+ return Contract[12] == 'C';
+ }
+
+ public bool IsPut()
+ {
+ return Contract[12] == 'P';
+ }
+
+ public double GetStrikePrice()
+ {
+ const UInt32 zeroChar = (UInt32)'0';
+
+ UInt32 whole = ((UInt32)Contract[13] - zeroChar) * 10_000u
+ + ((UInt32)Contract[14] - zeroChar) * 1_000u
+ + ((UInt32)Contract[15] - zeroChar) * 100u
+ + ((UInt32)Contract[16] - zeroChar) * 10u
+ + ((UInt32)Contract[17] - zeroChar) * 1u;
+
+ double part = Convert.ToDouble((UInt32) Contract[18] - zeroChar) * 0.1D
+ + Convert.ToDouble((UInt32) Contract[19] - zeroChar) * 0.01D
+ + Convert.ToDouble((UInt32) Contract[20] - zeroChar) * 0.001D;
+
+ return Convert.ToDouble(whole) + part;
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/QuoteType.cs b/Intrinio.Realtime/Options/QuoteType.cs
new file mode 100644
index 0000000..f626773
--- /dev/null
+++ b/Intrinio.Realtime/Options/QuoteType.cs
@@ -0,0 +1,7 @@
+namespace Intrinio.Realtime.Options;
+
+public enum QuoteType
+{
+ Ask = 0,
+ Bid = 1
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/Refresh.cs b/Intrinio.Realtime/Options/Refresh.cs
new file mode 100644
index 0000000..8eb0d98
--- /dev/null
+++ b/Intrinio.Realtime/Options/Refresh.cs
@@ -0,0 +1,98 @@
+using System.Globalization;
+
+namespace Intrinio.Realtime.Options;
+
+using System;
+
+public struct Refresh
+{
+ private readonly string _contract;
+ private readonly byte _priceType;
+ private readonly UInt32 _openInterest;
+ private readonly int _openPrice;
+ private readonly int _closePrice;
+ private readonly int _highPrice;
+ private readonly int _lowPrice;
+
+ public string Contract { get { return _contract; } }
+ public UInt32 OpenInterest { get { return _openInterest; } }
+ public double OpenPrice { get { return (_openPrice == Int32.MaxValue) || (_openPrice == Int32.MinValue) ? Double.NaN : Helpers.ScaleInt32Price(_openPrice, _priceType); } }
+ public double ClosePrice { get { return (_closePrice == Int32.MaxValue) || (_closePrice == Int32.MinValue) ? Double.NaN : Helpers.ScaleInt32Price(_closePrice, _priceType); } }
+ public double HighPrice { get { return (_highPrice == Int32.MaxValue) || (_highPrice == Int32.MinValue) ? Double.NaN : Helpers.ScaleInt32Price(_highPrice, _priceType); } }
+ public double LowPrice { get { return (_lowPrice == Int32.MaxValue) || (_lowPrice == Int32.MinValue) ? Double.NaN : Helpers.ScaleInt32Price(_lowPrice, _priceType); } }
+
+ ///
+ /// A 'Refresh' is an event that periodically sends updated values for open interest and high/low/open/close.
+ ///
+ /// The id of the option contract (e.g. AAPL__201016C00100000).
+ /// The scalar for the price.
+ /// Number of total active contracts for this contract.
+ /// The opening price for this contract for the day.
+ /// The closing price for this contract for the day.
+ /// The running high price for this contract today.
+ /// The running low price for this contract today.
+ public Refresh(string contract, byte priceType, UInt32 openInterest, int openPrice, int closePrice, int highPrice, int lowPrice)
+ {
+ _contract = contract;
+ _priceType = priceType;
+ _openInterest = openInterest;
+ _openPrice = openPrice;
+ _closePrice = closePrice;
+ _highPrice = highPrice;
+ _lowPrice = lowPrice;
+ }
+
+ public override string ToString()
+ {
+ return $"Refresh (Contract: {Contract}, OpenInterest: {OpenInterest.ToString()}, OpenPrice: {OpenPrice.ToString("f3")}, ClosePrice: {ClosePrice.ToString("f3")}, HighPrice: {HighPrice.ToString("f3")}, LowPrice: {LowPrice.ToString("f3")})";
+ }
+
+ public string GetUnderlyingSymbol()
+ {
+ return Contract.Substring(0, 6).TrimEnd('_');
+ }
+
+ public DateTime GetExpirationDate()
+ {
+ return DateTime.ParseExact(Contract.Substring(6, 6), "yyMMdd", CultureInfo.InvariantCulture);
+ }
+
+ public bool IsCall()
+ {
+ return Contract[12] == 'C';
+ }
+
+ public bool IsPut()
+ {
+ return Contract[12] == 'P';
+ }
+
+ public double GetStrikePrice()
+ {
+ const UInt32 zeroChar = (UInt32)'0';
+
+ UInt32 whole = ((UInt32)Contract[13] - zeroChar) * 10_000u
+ + ((UInt32)Contract[14] - zeroChar) * 1_000u
+ + ((UInt32)Contract[15] - zeroChar) * 100u
+ + ((UInt32)Contract[16] - zeroChar) * 10u
+ + ((UInt32)Contract[17] - zeroChar) * 1u;
+
+ double part = Convert.ToDouble((UInt32) Contract[18] - zeroChar) * 0.1D
+ + Convert.ToDouble((UInt32) Contract[19] - zeroChar) * 0.01D
+ + Convert.ToDouble((UInt32) Contract[20] - zeroChar) * 0.001D;
+
+ return Convert.ToDouble(whole) + part;
+ }
+
+ public static Refresh CreateUnitTestObject(string contract, UInt32 openInterest, double openPrice, double closePrice, double highPrice, double lowPrice)
+ {
+ byte priceType = (byte)4;
+ int unscaledOpenPrice = Convert.ToInt32(openPrice * 10000.0);
+ int unscaledClosePrice = Convert.ToInt32(closePrice * 10000.0);
+ int unscaledHighPrice = Convert.ToInt32(highPrice * 10000.0);
+ int unscaledLowPrice = Convert.ToInt32(lowPrice * 10000.0);
+
+ Refresh refresh = new Refresh(contract, priceType, openInterest, unscaledOpenPrice, unscaledClosePrice, unscaledHighPrice, unscaledLowPrice);
+ return refresh;
+ }
+}
\ No newline at end of file
diff --git a/Intrinio.Realtime/Options/ReplayClient.cs b/Intrinio.Realtime/Options/ReplayClient.cs
new file mode 100644
index 0000000..dc1a5b7
--- /dev/null
+++ b/Intrinio.Realtime/Options/ReplayClient.cs
@@ -0,0 +1,705 @@
+// using System.Linq;
+//
+// namespace Intrinio.Realtime.Options;
+//
+// using Intrinio.SDK.Model;
+// using System;
+// using System.IO;
+// using System.Net.Http;
+// using System.Text;
+// using System.Collections.Concurrent;
+// using System.Collections.Generic;
+// using System.Threading;
+// using System.Threading.Tasks;
+//
+// public class ReplayClient : IOptionsWebSocketClient
+// {
+// #region Data Members
+// private const string LobbyName = "lobby";
+// public Action OnTrade { get; set; }
+// public Action OnQuote { get; set; }
+// public Action OnRefresh { get; set; }
+// public Action OnUnusualActivity { get; set; }
+// private readonly Config _config;
+// private readonly DateTime _date;
+// private readonly bool _withSimulatedDelay;
+// private readonly bool _deleteFileWhenDone;
+// private readonly bool _writeToCsv;
+// private readonly string _csvFilePath;
+// private ulong _dataMsgCount;
+// private ulong _dataEventCount;
+// private ulong _dataTradeCount;
+// private ulong _dataQuoteCount;
+// private ulong _dataRefreshCount;
+// private ulong _dataUnusualActivityCount;
+// private ulong _textMsgCount;
+// private readonly HashSet _channels;
+// private readonly CancellationTokenSource _ctSource;
+// private readonly ConcurrentQueue _data;
+// private bool _useOnTrade { get {return !(ReferenceEquals(OnTrade, null));} }
+// private bool _useOnQuote { get {return !(ReferenceEquals(OnQuote, null));} }
+// private bool _useOnRefresh { get {return !(ReferenceEquals(OnRefresh, null));} }
+// private bool _useOnUnusualActivity { get {return !(ReferenceEquals(OnUnusualActivity, null));} }
+//
+// private readonly string _logPrefix;
+// private readonly object _csvLock;
+// private readonly Thread[] _threads;
+// private readonly Thread _replayThread;
+// public UInt64 TradeCount { get { return Interlocked.Read(ref _dataTradeCount); } }
+// public UInt64 QuoteCount { get { return Interlocked.Read(ref _dataQuoteCount); } }
+// public UInt64 RefreshCount { get { return Interlocked.Read(ref _dataRefreshCount); } }
+// public UInt64 UnusualActivityCount { get { return Interlocked.Read(ref _dataUnusualActivityCount); } }
+// #endregion //Data Members
+//
+// #region Constructors
+// public ReplayClient(Action onTrade, Action onQuote, Config config, DateTime date, bool withSimulatedDelay, bool deleteFileWhenDone, bool writeToCsv, string csvFilePath)
+// {
+// this.OnTrade = onTrade;
+// this.OnQuote = onQuote;
+// this._config = config;
+// this._date = date;
+// this._withSimulatedDelay = withSimulatedDelay;
+// this._deleteFileWhenDone = deleteFileWhenDone;
+// this._writeToCsv = writeToCsv;
+// this._csvFilePath = csvFilePath;
+//
+// _dataMsgCount = 0UL;
+// _dataEventCount = 0UL;
+// _dataTradeCount = 0UL;
+// _dataQuoteCount = 0UL;
+// _textMsgCount = 0UL;
+// _channels = new HashSet();
+// _ctSource = new CancellationTokenSource();
+// _data = new ConcurrentQueue();
+//
+// _logPrefix = _logPrefix = String.Format("{0}: ", config.Provider.ToString());
+// _csvLock = new Object();
+// _threads = new Thread[config.NumThreads];
+// for (int i = 0; i < _threads.Length; i++)
+// _threads[i] = new Thread(ThreadFn);
+// // _replayThread = new Thread(ReplayThreadFn);
+//
+// config.Validate();
+// }
+//
+// public ReplayClient(Action onTrade, DateTime date, bool withSimulatedDelay, bool deleteFileWhenDone, bool writeToCsv, string csvFilePath) : this(onTrade, null, Config.LoadConfig(), date, withSimulatedDelay, deleteFileWhenDone, writeToCsv, csvFilePath)
+// {
+//
+// }
+//
+// public ReplayClient(Action onQuote, DateTime date, bool withSimulatedDelay, bool deleteFileWhenDone, bool writeToCsv, string csvFilePath) : this(null, onQuote, Config.LoadConfig(), date, withSimulatedDelay, deleteFileWhenDone, writeToCsv, csvFilePath)
+// {
+//
+// }
+//
+// public ReplayClient(Action onTrade, Action onQuote, DateTime date, bool withSimulatedDelay, bool deleteFileWhenDone, bool writeToCsv, string csvFilePath) : this(onTrade, onQuote, Config.LoadConfig(), date, withSimulatedDelay, deleteFileWhenDone, writeToCsv, csvFilePath)
+// {
+//
+// }
+// #endregion //Constructors
+//
+// #region Public Methods
+//
+// public Task Join()
+// {
+// HashSet symbolsToAdd = _config.Symbols.Select(s => new Channel(s, _config.TradesOnly)).ToHashSet();
+// symbolsToAdd.ExceptWith(_channels);
+//
+// foreach (Channel channel in symbolsToAdd)
+// Join(channel.ticker, channel.tradesOnly);
+//
+// return Task.CompletedTask;
+// }
+//
+// public Task Join(string symbol, bool? tradesOnly)
+// {
+// bool t = tradesOnly.HasValue
+// ? tradesOnly.Value || _config.TradesOnly
+// : _config.TradesOnly;
+// if (!_channels.Contains(new Channel(symbol, t)))
+// Join(symbol, t);
+//
+// return Task.CompletedTask;
+// }
+//
+// public async Task JoinLobby(bool? tradesOnly)
+// {
+// await Join(LobbyName, tradesOnly);
+// }
+//
+// public Task Join(string[] symbols, bool? tradesOnly)
+// {
+// bool t = tradesOnly.HasValue
+// ? tradesOnly.Value || _config.TradesOnly
+// : _config.TradesOnly;
+// HashSet symbolsToAdd = symbols.Select(s => new Channel(s, t)).ToHashSet();
+// symbolsToAdd.ExceptWith(_channels);
+// foreach (Channel channel in symbolsToAdd)
+// Join(channel.ticker, channel.tradesOnly);
+// return Task.CompletedTask;
+// }
+//
+// public Task Leave()
+// {
+// foreach (Channel channel in _channels)
+// Leave(channel.ticker, channel.tradesOnly);
+// return Task.CompletedTask;
+// }
+//
+// public Task Leave(string symbol)
+// {
+// IEnumerable matchingChannels = _channels.Where(c => c.ticker == symbol);
+// foreach (Channel channel in matchingChannels)
+// Leave(channel.ticker, channel.tradesOnly);
+// return Task.CompletedTask;
+// }
+//
+// public async Task LeaveLobby()
+// {
+// await Leave(LobbyName);
+// }
+//
+// public Task Leave(string[] symbols)
+// {
+// HashSet _symbols = new HashSet(symbols);
+// IEnumerable matchingChannels = _channels.Where(c => _symbols.Contains(c.ticker));
+// foreach (Channel channel in matchingChannels)
+// Leave(channel.ticker, channel.tradesOnly);
+// return Task.CompletedTask;
+// }
+//
+// public Task Start()
+// {
+// foreach (Thread thread in _threads)
+// thread.Start();
+// if (_writeToCsv)
+// WriteHeaderRow();
+// _replayThread.Start();
+//
+// return Task.CompletedTask;
+// }
+//
+// public Task Stop()
+// {
+// foreach (Channel channel in _channels)
+// Leave(channel.ticker, channel.tradesOnly);
+//
+// _ctSource.Cancel();
+// LogMessage(LogLevel.INFORMATION, "Websocket - Closing...");
+//
+// foreach (Thread thread in _threads)
+// thread.Join();
+//
+// _replayThread.Join();
+//
+// LogMessage(LogLevel.INFORMATION, "Stopped");
+// return Task.CompletedTask;
+// }
+//
+// public ClientStats GetStats()
+// {
+// return new ClientStats(
+// Interlocked.Read(ref _dataMsgCount),
+// Interlocked.Read(ref _textMsgCount),
+// _data.Count,
+// Interlocked.Read(ref _dataEventCount),
+// Int32.MaxValue,
+// 0,
+// Int32.MaxValue,
+// 0,
+// 0
+// );
+// }
+//
+// [Serilog.Core.MessageTemplateFormatMethod("messageTemplate")]
+// public void LogMessage(LogLevel logLevel, string messageTemplate, params object[] propertyValues)
+// {
+// switch (logLevel)
+// {
+// case LogLevel.DEBUG:
+// Serilog.Log.Debug(_logPrefix + messageTemplate, propertyValues);
+// break;
+// case LogLevel.INFORMATION:
+// Serilog.Log.Information(_logPrefix + messageTemplate, propertyValues);
+// break;
+// case LogLevel.WARNING:
+// Serilog.Log.Warning(_logPrefix + messageTemplate, propertyValues);
+// break;
+// case LogLevel.ERROR:
+// Serilog.Log.Error(_logPrefix + messageTemplate, propertyValues);
+// break;
+// default:
+// throw new ArgumentException("LogLevel not specified!");
+// break;
+// }
+// }
+// #endregion //Public Methods
+//
+// #region Private Methods
+// private DateTime ParseTimeReceived(ReadOnlySpan bytes)
+// {
+// return DateTime.UnixEpoch + TimeSpan.FromTicks(Convert.ToInt64(BitConverter.ToUInt64(bytes) / 100UL));
+// }
+//
+// private Trade ParseTrade(ReadOnlySpan bytes)
+// {
+// throw new NotImplementedException();
+// // int symbolLength = Convert.ToInt32(bytes[2]);
+// // int conditionLength = Convert.ToInt32(bytes[26 + symbolLength]);
+// // Trade trade = new Trade(
+// // Encoding.ASCII.GetString(bytes.Slice(3, symbolLength)),
+// // Convert.ToDouble(BitConverter.ToSingle(bytes.Slice(6 + symbolLength, 4))),
+// // BitConverter.ToUInt32(bytes.Slice(10 + symbolLength, 4)),
+// // BitConverter.ToUInt32(bytes.Slice(22 + symbolLength, 4)),
+// // DateTime.UnixEpoch + TimeSpan.FromTicks(Convert.ToInt64(BitConverter.ToUInt64(bytes.Slice(14 + symbolLength, 8)) / 100UL)),
+// // (SubProvider)Convert.ToInt32(bytes[3 + symbolLength]),
+// // BitConverter.ToChar(bytes.Slice(4 + symbolLength, 2)),
+// // conditionLength > 0 ? Encoding.ASCII.GetString(bytes.Slice(27 + symbolLength, conditionLength)) : String.Empty
+// // );
+// //
+// // return trade;
+// }
+//
+// private Quote ParseQuote(ReadOnlySpan bytes)
+// {
+// throw new NotImplementedException();
+// // int symbolLength = Convert.ToInt32(bytes[2]);
+// // int conditionLength = Convert.ToInt32(bytes[22 + symbolLength]);
+// //
+// // Quote quote = new Quote(
+// // (QuoteType)(Convert.ToInt32(bytes[0])),
+// // Encoding.ASCII.GetString(bytes.Slice(3, symbolLength)),
+// // (Convert.ToDouble(BitConverter.ToSingle(bytes.Slice(6 + symbolLength, 4)))),
+// // BitConverter.ToUInt32(bytes.Slice(10 + symbolLength, 4)),
+// // DateTime.UnixEpoch + TimeSpan.FromTicks(Convert.ToInt64(BitConverter.ToUInt64(bytes.Slice(14 + symbolLength, 8)) / 100UL)),
+// // (SubProvider)(Convert.ToInt32(bytes[3 + symbolLength])),
+// // BitConverter.ToChar(bytes.Slice(4 + symbolLength, 2)),
+// // conditionLength > 0 ? Encoding.ASCII.GetString(bytes.Slice(23 + symbolLength, conditionLength)) : String.Empty
+// // );
+// //
+// // return quote;
+// }
+//
+// private void WriteRowToOpenCsvWithoutLock(IEnumerable row)
+// {
+// bool first = true;
+// using (FileStream fs = new FileStream(_csvFilePath, FileMode.Append))
+// using (TextWriter tw = new StreamWriter(fs))
+// {
+// foreach (string s in row)
+// {
+// if (!first)
+// tw.Write(",");
+// else
+// first = false;
+// tw.Write($"\"{s}\"");
+// }
+//
+// tw.WriteLine();
+// }
+// }
+//
+// private void WriteRowToOpenCsvWithLock(IEnumerable row)
+// {
+// lock (_csvLock)
+// {
+// WriteRowToOpenCsvWithoutLock(row);
+// }
+// }
+//
+// private string DoubleRoundSecRule612(double value)
+// {
+// if (value >= 1.0D)
+// return value.ToString("0.00");
+//
+// return value.ToString("0.0000");
+// }
+//
+// private IEnumerable MapTradeToRow(Trade trade)
+// {
+// throw new NotImplementedException();
+// // yield return MessageType.Trade.ToString();
+// // yield return trade.Symbol;
+// // yield return DoubleRoundSecRule612(trade.Price);
+// // yield return trade.Size.ToString();
+// // yield return trade.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK");
+// // yield return trade.SubProvider.ToString();
+// // yield return trade.MarketCenter.ToString();
+// // yield return trade.Condition;
+// // yield return trade.TotalVolume.ToString();
+// }
+//
+// private void WriteTradeToCsv(Trade trade)
+// {
+// WriteRowToOpenCsvWithLock(MapTradeToRow(trade));
+// }
+//
+// private IEnumerable MapQuoteToRow(Quote quote)
+// {
+// throw new NotImplementedException();
+// // yield return quote.Type.ToString();
+// // yield return quote.Symbol;
+// // yield return DoubleRoundSecRule612(quote.Price);
+// // yield return quote.Size.ToString();
+// // yield return quote.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK");
+// // yield return quote.SubProvider.ToString();
+// // yield return quote.MarketCenter.ToString();
+// // yield return quote.Condition;
+// }
+//
+// private void WriteQuoteToCsv(Quote quote)
+// {
+// WriteRowToOpenCsvWithLock(MapQuoteToRow(quote));
+// }
+//
+// private void WriteHeaderRow()
+// {
+// WriteRowToOpenCsvWithLock(new string[]{"Type", "Symbol", "Price", "Size", "Timestamp", "SubProvider", "MarketCenter", "Condition", "TotalVolume"});
+// }
+//
+// private void ThreadFn()
+// {
+// CancellationToken ct = _ctSource.Token;
+// while (!ct.IsCancellationRequested)
+// {
+// try
+// {
+// if (_data.TryDequeue(out Tick datum))
+// {
+// if (datum.IsTrade())
+// {
+// if (_useOnTrade)
+// {
+// Interlocked.Increment(ref _dataTradeCount);
+// OnTrade.Invoke(datum.Trade);
+// }
+// }
+// else
+// {
+// if (_useOnQuote)
+// {
+// Interlocked.Increment(ref _dataQuoteCount);
+// OnQuote.Invoke(datum.Quote);
+// }
+// }
+// }
+// else
+// Thread.Sleep(1);
+// }
+// catch (OperationCanceledException)
+// {
+// }
+// catch (Exception exn)
+// {
+// LogMessage(LogLevel.ERROR, "Error parsing message: {0}; {1}", exn.Message, exn.StackTrace);
+// }
+// }
+// }
+//
+// // ///
+// // /// The results of this should be streamed and not ToList-ed.
+// // ///
+// // ///
+// // ///
+// // ///
+// // private IEnumerable ReplayTickFileWithoutDelay(string fullFilePath, int byteBufferSize, CancellationToken ct)
+// // {
+// // if (File.Exists(fullFilePath))
+// // {
+// // using (FileStream fRead = new FileStream(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.None))
+// // {
+// // if (fRead.CanRead)
+// // {
+// // int readResult = fRead.ReadByte(); //This is message type
+// //
+// // while (readResult != -1)
+// // {
+// // if (!ct.IsCancellationRequested)
+// // {
+// // byte[] eventBuffer = new byte[byteBufferSize];
+// // byte[] timeReceivedBuffer = new byte[8];
+// // ReadOnlySpan eventSpanBuffer = new ReadOnlySpan(eventBuffer);
+// // ReadOnlySpan timeReceivedSpanBuffer = new ReadOnlySpan(timeReceivedBuffer);
+// // eventBuffer[0] = (byte)readResult; //This is message type
+// // eventBuffer[1] = (byte)(fRead.ReadByte()); //This is message length, including this and the previous byte.
+// // int bytesRead = fRead.Read(eventBuffer, 2, (System.Convert.ToInt32(eventBuffer[1]) - 2)); //read the rest of the message
+// // int timeBytesRead = fRead.Read(timeReceivedBuffer, 0, 8); //get the time received
+// // DateTime timeReceived = ParseTimeReceived(timeReceivedSpanBuffer);
+// //
+// // switch ((MessageType)(Convert.ToInt32(eventBuffer[0])))
+// // {
+// // case MessageType.Trade:
+// // Trade trade = ParseTrade(eventSpanBuffer);
+// // if (_channels.Contains(new Channel(LobbyName, true))
+// // || _channels.Contains(new Channel(LobbyName, false))
+// // || _channels.Contains(new Channel(trade.Contract, true))
+// // || _channels.Contains(new Channel(trade.Contract, false)))
+// // {
+// // if (_writeToCsv)
+// // WriteTradeToCsv(trade);
+// // yield return new Tick(timeReceived, trade, null);
+// // }
+// // break;
+// // case MessageType.Ask:
+// // case MessageType.Bid:
+// // Quote quote = ParseQuote(eventSpanBuffer);
+// // if (_channels.Contains (new Channel(LobbyName, false)) || _channels.Contains (new Channel(quote.Contract, false)))
+// // {
+// // if (_writeToCsv)
+// // WriteQuoteToCsv(quote);
+// // yield return new Tick(timeReceived, null, quote);
+// // }
+// // break;
+// // default:
+// // LogMessage(LogLevel.ERROR, "Invalid MessageType: {0}", eventBuffer[0]);
+// // break;
+// // }
+// //
+// // //Set up the next iteration
+// // readResult = fRead.ReadByte();
+// // }
+// // else
+// // readResult = -1;
+// // }
+// // }
+// // else
+// // throw new FileLoadException("Unable to read replay file.");
+// // }
+// // }
+// // else
+// // {
+// // yield break;
+// // }
+// // }
+//
+// // ///
+// // /// The results of this should be streamed and not ToList-ed.
+// // ///
+// // ///
+// // ///
+// // /// returns
+// // private IEnumerable ReplayTickFileWithDelay(string fullFilePath, int byteBufferSize, CancellationToken ct)
+// // {
+// // long start = DateTime.UtcNow.Ticks;
+// // long offset = 0L;
+// // foreach (Tick tick in ReplayTickFileWithoutDelay(fullFilePath, byteBufferSize, ct))
+// // {
+// // if (offset == 0L)
+// // offset = start - tick.TimeReceived().Ticks;
+// //
+// // if (!ct.IsCancellationRequested)
+// // {
+// // SpinWait.SpinUntil(() => (tick.TimeReceived().Ticks + offset) <= DateTime.UtcNow.Ticks);
+// // yield return tick;
+// // }
+// // }
+// // }
+//
+// // private string MapSubProviderToApiValue(SubProvider subProvider)
+// // {
+// // switch (subProvider)
+// // {
+// // case SubProvider.IEX: return "iex";
+// // case SubProvider.UTP: return "utp_delayed";
+// // case SubProvider.CTA_A: return "cta_a_delayed";
+// // case SubProvider.CTA_B: return "cta_b_delayed";
+// // case SubProvider.OTC: return "otc_delayed";
+// // case SubProvider.NASDAQ_BASIC: return "nasdaq_basic";
+// // default: return "iex";
+// // }
+// // }
+//
+// // private SubProvider[] MapProviderToSubProviders(Intrinio.Realtime.Equities.Provider provider)
+// // {
+// // switch (provider)
+// // {
+// // case Provider.NONE: return Array.Empty();
+// // case Provider.MANUAL: return Array.Empty();
+// // case Provider.REALTIME: return new SubProvider[]{SubProvider.IEX};
+// // case Provider.DELAYED_SIP: return new SubProvider[]{SubProvider.UTP, SubProvider.CTA_A, SubProvider.CTA_B, SubProvider.OTC};
+// // case Provider.NASDAQ_BASIC: return new SubProvider[]{SubProvider.NASDAQ_BASIC};
+// // default: return new SubProvider[0];
+// // }
+// // }
+//
+// // private string FetchReplayFile(SubProvider subProvider)
+// // {
+// // Intrinio.SDK.Api.SecurityApi api = new Intrinio.SDK.Api.SecurityApi();
+// //
+// // if (!api.Configuration.ApiKey.ContainsKey("api_key"))
+// // api.Configuration.ApiKey.Add("api_key", _config.ApiKey);
+// //
+// // try
+// // {
+// // SecurityReplayFileResult result = api.GetSecurityReplayFile(MapSubProviderToApiValue(subProvider), _date);
+// // string decodedUrl = result.Url.Replace(@"\u0026", "&");
+// // string tempDir = System.IO.Path.GetTempPath();
+// // string fileName = Path.Combine(tempDir, result.Name);
+// //
+// // using (FileStream outputFile = new FileStream(fileName,System.IO.FileMode.Create))
+// // using (HttpClient httpClient = new HttpClient())
+// // {
+// // httpClient.Timeout = TimeSpan.FromHours(1);
+// // httpClient.BaseAddress = new Uri(decodedUrl);
+// // using (HttpResponseMessage response = httpClient.GetAsync(decodedUrl, HttpCompletionOption.ResponseHeadersRead).Result)
+// // using (Stream streamToReadFrom = response.Content.ReadAsStreamAsync().Result)
+// // {
+// // streamToReadFrom.CopyTo(outputFile);
+// // }
+// // }
+// //
+// // return fileName;
+// // }
+// // catch (Exception e)
+// // {
+// // LogMessage(LogLevel.ERROR, "Error while fetching {0} file: {1}", subProvider.ToString(), e.Message);
+// // return null;
+// // }
+// // }
+//
+// private void FillNextTicks(IEnumerator[] enumerators, Tick[] nextTicks)
+// {
+// for (int i = 0; i < nextTicks.Length; i++)
+// if (nextTicks[i] == null && enumerators[i].MoveNext())
+// nextTicks[i] = enumerators[i].Current;
+// }
+//
+// private Tick PullNextTick(Tick[] nextTicks)
+// {
+// int pullIndex = 0;
+// DateTime t = DateTime.MaxValue;
+// for (int i = 0; i < nextTicks.Length; i++)
+// {
+// if (nextTicks[i] != null && nextTicks[i].TimeReceived() < t)
+// {
+// pullIndex = i;
+// t = nextTicks[i].TimeReceived();
+// }
+// }
+//
+// Tick pulledTick = nextTicks[pullIndex];
+// nextTicks[pullIndex] = null;
+// return pulledTick;
+// }
+//
+// private bool HasAnyValue(Tick[] nextTicks)
+// {
+// bool hasValue = false;
+//
+// for (int i = 0; i < nextTicks.Length; i++)
+// if (nextTicks[i] != null)
+// hasValue = true;
+//
+// return hasValue;
+// }
+//
+// private IEnumerable ReplayFileGroupWithoutDelay(IEnumerable