diff --git a/TriggerEngine/TriggerEngineService.cs b/TriggerEngine/TriggerEngineService.cs index 25af06f0..2a73708f 100644 --- a/TriggerEngine/TriggerEngineService.cs +++ b/TriggerEngine/TriggerEngineService.cs @@ -448,7 +448,12 @@ private static Task ExecuteActionAsync(string ruleName, TriggerCondition conditi { var body = action.RestApi.BodyTemplate .Replace("{{rulename}}", ruleName) + // {{plugin}} historically receives the METRIC name (the display name does + // not exist at fire time); kept as-is so existing saved templates keep + // their current output. {{metric}} is the documented, correctly-named + // placeholder for the same value. .Replace("{{plugin}}", metric) + .Replace("{{metric}}", metric) .Replace("{{condition}}", condition.Operator.ToString()) .Replace("{{threshold}}", condition.Threshold.ToString()) .Replace("{{value}}", value.ToString()) diff --git a/TriggerEngine/View/AddAPISetting.xaml.cs b/TriggerEngine/View/AddAPISetting.xaml.cs index c826afa9..87d095cf 100644 --- a/TriggerEngine/View/AddAPISetting.xaml.cs +++ b/TriggerEngine/View/AddAPISetting.xaml.cs @@ -43,7 +43,7 @@ public AddAPISetting(RestApiActionViewModel _rule) } else { - restApiAction.BodyTemplate = "{\r\n\r\n\"plugin\":\"{{plugin}}\",\r\n\"value\":\"{{value}}\",\r\n\"timestamp\":\"{{timestamp}}\", \r\n}"; + restApiAction.BodyTemplate = "{\r\n\r\n\"metric\":\"{{metric}}\",\r\n\"value\":\"{{value}}\",\r\n\"timestamp\":\"{{timestamp}}\", \r\n}"; } this.DataContext = restApiAction; } diff --git a/TriggerEngine/View/TriggerSettingAddOrUpdate.xaml b/TriggerEngine/View/TriggerSettingAddOrUpdate.xaml index d31b9f2c..81168234 100644 --- a/TriggerEngine/View/TriggerSettingAddOrUpdate.xaml +++ b/TriggerEngine/View/TriggerSettingAddOrUpdate.xaml @@ -266,10 +266,18 @@ TextOptions.TextRenderingMode="Auto" Body Template: - JSON payload with dynamic placeholders like + JSON payload with dynamic placeholders: + {{rulename}}, + {{metric}} + (the metric that fired), + {{condition}}, + {{threshold}}, + {{value}} + and + {{timestamp}} + . {{plugin}} - or - {{condition}} etc. + is kept for older templates and receives the same metric name. Headers: diff --git a/VisualHFT.Plugins/MarketConnectors.Binance/BinancePlugin.cs b/VisualHFT.Plugins/MarketConnectors.Binance/BinancePlugin.cs index bd448fab..cbaa99b7 100644 --- a/VisualHFT.Plugins/MarketConnectors.Binance/BinancePlugin.cs +++ b/VisualHFT.Plugins/MarketConnectors.Binance/BinancePlugin.cs @@ -288,10 +288,12 @@ private async Task InitializeDeltasAsync() { try { - data.Data.EventTime = data.ReceiveTime; - if (Math.Abs(DateTime.Now.Subtract(data.Data.EventTime.ToLocalTime()).TotalSeconds) > 1) + // EventTime is the venue's own event timestamp ("E") — never overwrite it. + // The freshness warn keys on RECEIVE time: it measures in-process dispatch + // delay, and must not fire on venue/local clock offset. + if (Math.Abs(DateTime.Now.Subtract(data.ReceiveTime.ToLocalTime()).TotalSeconds) > 1) { - var _msg = $"Rates are coming late at {Math.Abs(DateTime.Now.Subtract(data.Data.EventTime.ToLocalTime()).TotalSeconds)} seconds."; + var _msg = $"Rates are coming late at {Math.Abs(DateTime.Now.Subtract(data.ReceiveTime.ToLocalTime()).TotalSeconds)} seconds."; log.Warn(_msg); HelperNotificationManager.Instance.AddNotification(this.Name, _msg, HelprNorificationManagerTypes.WARNING, HelprNorificationManagerCategories.PLUGINS); } @@ -662,6 +664,20 @@ private void deltaSubscription_Exception(Exception obj) #endregion // ✅ FIX: Thread-safe UpdateOrderBook with TryGetValue + /// + /// The single decision point for the venue's book timestamp. Binance's diff-depth + /// stream carries the exchange event time (wire field "E"); return it in local + /// kind. A frame without it returns null — a default(DateTime) stamp would + /// fabricate a colossal latency spike, and receive time must never masquerade + /// as exchange time. + /// + public static DateTime? ResolveBookTimestamp(IBinanceEventOrderBook lob_update) + { + if (lob_update == null || lob_update.EventTime == default) + return null; + return lob_update.EventTime.ToLocalTime(); + } + private void UpdateOrderBook(IBinanceEventOrderBook lob_update, string normalizedSymbol) { // ✅ Use TryGetValue for thread safety @@ -674,7 +690,7 @@ private void UpdateOrderBook(IBinanceEventOrderBook lob_update, string normalize return; } - DateTime ts = lob_update.EventTime.ToLocalTime(); + DateTime? exchangeTs = ResolveBookTimestamp(lob_update); if (lob_update.LastUpdateId <= local_lob.Sequence) return; @@ -685,6 +701,7 @@ private void UpdateOrderBook(IBinanceEventOrderBook lob_update, string normalize // ✅ Cache DateTime.Now once var now = DateTime.Now; + DateTime ts = exchangeTs ?? now; foreach (var item in lob_update.Bids) { @@ -741,7 +758,9 @@ private void UpdateOrderBook(IBinanceEventOrderBook lob_update, string normalize } local_lob.Sequence = lob_update.LastUpdateId; - local_lob.LastUpdated = ts; + // Venue event time when the frame carries one; null otherwise (never + // receive time — Mkt Lat measures the venue clock, not our decode delay). + local_lob.LastUpdated = exchangeTs; RaiseOnDataReceived(local_lob); } diff --git a/VisualHFT.Plugins/MarketConnectors.Bitfinex/BitfinexPlugin.cs b/VisualHFT.Plugins/MarketConnectors.Bitfinex/BitfinexPlugin.cs index 17e0db34..5134c309 100644 --- a/VisualHFT.Plugins/MarketConnectors.Bitfinex/BitfinexPlugin.cs +++ b/VisualHFT.Plugins/MarketConnectors.Bitfinex/BitfinexPlugin.cs @@ -39,7 +39,7 @@ public class BitfinexPlugin : BasePluginDataRetriever, IDataRetrieverTestable private BitfinexSocketClient _socketClient; private BitfinexRestClient _restClient; private Dictionary _localOrderBooks = new Dictionary(); - private Dictionary>> _eventBuffers = new(); + private Dictionary>> _eventBuffers = new(); private Dictionary>> _tradesBuffers = new(); private readonly object _buffersLock = new object(); // ✅ ADD: Thread-safe buffer access @@ -115,7 +115,7 @@ private async Task InternalStartAsync() // Initialize event buffer for each symbol foreach (var symbol in GetAllNormalizedSymbols()) { - _eventBuffers.Add(symbol, new HelperCustomQueue>($">_{this.Name.Replace(" Plugin", "")}", eventBuffers_onReadAction, eventBuffers_onErrorAction)); + _eventBuffers.Add(symbol, new HelperCustomQueue>($">_{this.Name.Replace(" Plugin", "")}", eventBuffers_onReadAction, eventBuffers_onErrorAction)); _tradesBuffers.Add(symbol, new HelperCustomQueue>($">_{this.Name.Replace(" Plugin", "")}", tradesBuffers_onReadAction, tradesBuffers_onErrorAction)); } @@ -356,18 +356,21 @@ private async Task InitializeDeltasAsync() else { // ✅ FIX: Thread-safe buffer access - HelperCustomQueue> buffer; + HelperCustomQueue> buffer; lock (_buffersLock) { if (!_eventBuffers.TryGetValue(normalizedSymbol, out buffer)) return; // Buffer was cleared during reconnection } + // The BOOK stamp is the venue's server timestamp carried on the + // wrapper (DataTime, null when absent) — never local receive time. + var exchangeTs = ResolveBookTimestamp(data.DataTime); foreach (var item in data.Data) { buffer.Add( - new Tuple( - data.ReceiveTime.ToLocalTime(), normalizedSymbol, item)); + new Tuple( + exchangeTs, normalizedSymbol, item)); } } } @@ -436,7 +439,7 @@ private async Task InitializePingTimerAsync() _timerPing.Enabled = true; // Start the timer } - private void eventBuffers_onReadAction(Tuple eventData) + private void eventBuffers_onReadAction(Tuple eventData) { UpdateOrderBook(eventData.Item3, eventData.Item2, eventData.Item1); } @@ -659,7 +662,21 @@ private void UpdateOrderBookSnapshot(IEnumerable data, s }); }); } - private void UpdateOrderBook(BitfinexOrderBookEntry lob_update, string symbol, DateTime ts) + /// + /// The single decision point for the venue's book timestamp. Bitfinex book + /// entries carry no timestamp, but the socket library enables the exchange's + /// TIMESTAMP conf flag and delivers the server's event time on + /// DataEvent.DataTime for every book frame; return it in local kind. A frame + /// without it returns null — receive time must never masquerade as exchange time. + /// + public static DateTime? ResolveBookTimestamp(DateTime? dataTime) + { + if (!dataTime.HasValue || dataTime.Value == default) + return null; + return dataTime.Value.ToLocalTime(); + } + + private void UpdateOrderBook(BitfinexOrderBookEntry lob_update, string symbol, DateTime? ts) { if (!_localOrderBooks.ContainsKey(symbol)) return; @@ -680,7 +697,7 @@ private void UpdateOrderBook(BitfinexOrderBookEntry lob_update, string symbol, D Size = (double)Math.Abs(lob_update.Quantity), IsBid = isBid, LocalTimeStamp = DateTime.Now, - ServerTimeStamp = ts, + ServerTimeStamp = ts ?? DateTime.Now, Symbol = local_lob.Symbol, MDUpdateAction = eMDUpdateAction.Delete, }; @@ -694,7 +711,7 @@ private void UpdateOrderBook(BitfinexOrderBookEntry lob_update, string symbol, D Size = (double)Math.Abs(lob_update.Quantity), IsBid = isBid, LocalTimeStamp = DateTime.Now, - ServerTimeStamp = ts, + ServerTimeStamp = ts ?? DateTime.Now, Symbol = local_lob.Symbol, MDUpdateAction = eMDUpdateAction.Change, }; diff --git a/VisualHFT.Plugins/MarketConnectors.Coinbase/CoinbasePlugin.cs b/VisualHFT.Plugins/MarketConnectors.Coinbase/CoinbasePlugin.cs index a3111b7f..631dad83 100644 --- a/VisualHFT.Plugins/MarketConnectors.Coinbase/CoinbasePlugin.cs +++ b/VisualHFT.Plugins/MarketConnectors.Coinbase/CoinbasePlugin.cs @@ -35,8 +35,8 @@ public class CoinbasePlugin : BasePluginDataRetriever private readonly ConcurrentDictionary _localOrderBooks = new ConcurrentDictionary(); - private readonly ConcurrentDictionary>> _eventBuffers = - new ConcurrentDictionary>>(); + private readonly ConcurrentDictionary>> _eventBuffers = + new ConcurrentDictionary>>(); private readonly ConcurrentDictionary>> _tradesBuffers = new ConcurrentDictionary>>(); @@ -139,7 +139,7 @@ private async Task InternalStartAsync() foreach (var symbol in GetAllNormalizedSymbols()) { _eventBuffers.TryAdd(symbol, - new HelperCustomQueue>( + new HelperCustomQueue>( $">_{this.Name.Replace(" Plugin", "")}", eventBuffers_onReadAction, eventBuffers_onErrorAction)); _tradesBuffers.TryAdd(symbol, @@ -335,9 +335,12 @@ private async Task InitializeDeltasAsync() HelprNorificationManagerCategories.PLUGINS); } + // The BOOK stamp is the venue's own event time (newest entry + // "event_time", null when absent) — ReceiveTime stays the + // freshness-warn input only, never the book stamp. _eventBuffers[normalizedSymbol].Add( - new Tuple( - data.ReceiveTime.ToLocalTime(), normalizedSymbol, data.Data)); + new Tuple( + ResolveBookTimestamp(data.Data), normalizedSymbol, data.Data)); } } catch (Exception ex) @@ -417,7 +420,7 @@ private async Task InitializePingTimerAsync() _timerPing.Enabled = true; // Start the timer } - private void eventBuffers_onReadAction(Tuple eventData) + private void eventBuffers_onReadAction(Tuple eventData) { UpdateOrderBook(eventData.Item3, eventData.Item2, eventData.Item1); } @@ -530,7 +533,37 @@ private void deltaSubscription_Exception(Exception obj) #endregion // ✅ FIX: Thread-safe UpdateOrderBook - private void UpdateOrderBook(CoinbaseOrderBookUpdate lob_update, string symbol, DateTime ts) + /// + /// The single decision point for the venue's book timestamp. Coinbase's level2 + /// stream carries the exchange event time PER ENTRY (wire field "event_time"); + /// the frame timestamp is the newest entry event time across bids and asks, + /// returned in local kind. Entries without it are ignored; a frame with no + /// usable entry timestamps returns null — receive time must never masquerade + /// as exchange time. + /// + public static DateTime? ResolveBookTimestamp(CoinbaseOrderBookUpdate lob_update) + { + if (lob_update == null) + return null; + var newest = default(DateTime); + if (lob_update.Bids != null) + { + foreach (var item in lob_update.Bids) + { + if (item.EventTime > newest) newest = item.EventTime; + } + } + if (lob_update.Asks != null) + { + foreach (var item in lob_update.Asks) + { + if (item.EventTime > newest) newest = item.EventTime; + } + } + return newest == default ? null : newest.ToLocalTime(); + } + + private void UpdateOrderBook(CoinbaseOrderBookUpdate lob_update, string symbol, DateTime? ts) { if (lob_update == null) return; @@ -559,7 +592,7 @@ private void UpdateOrderBook(CoinbaseOrderBookUpdate lob_update, string symbol, Size = (double)item.Quantity, IsBid = true, LocalTimeStamp = now, - ServerTimeStamp = ts, + ServerTimeStamp = ts ?? DateTime.Now, Symbol = symbol }); } @@ -571,7 +604,7 @@ private void UpdateOrderBook(CoinbaseOrderBookUpdate lob_update, string symbol, Price = (double)item.Price, IsBid = true, LocalTimeStamp = now, - ServerTimeStamp = ts, + ServerTimeStamp = ts ?? DateTime.Now, Symbol = symbol }); } @@ -588,7 +621,7 @@ private void UpdateOrderBook(CoinbaseOrderBookUpdate lob_update, string symbol, Size = (double)item.Quantity, IsBid = false, LocalTimeStamp = now, - ServerTimeStamp = ts, + ServerTimeStamp = ts ?? DateTime.Now, Symbol = symbol }); } @@ -600,7 +633,7 @@ private void UpdateOrderBook(CoinbaseOrderBookUpdate lob_update, string symbol, Price = (double)item.Price, IsBid = false, LocalTimeStamp = now, - ServerTimeStamp = ts, + ServerTimeStamp = ts ?? DateTime.Now, Symbol = symbol }); } diff --git a/VisualHFT.Plugins/MarketConnectors.Kraken/KrakenPlugin.cs b/VisualHFT.Plugins/MarketConnectors.Kraken/KrakenPlugin.cs index 9ea06ab8..878d5448 100644 --- a/VisualHFT.Plugins/MarketConnectors.Kraken/KrakenPlugin.cs +++ b/VisualHFT.Plugins/MarketConnectors.Kraken/KrakenPlugin.cs @@ -58,7 +58,7 @@ private enum ChecksumValidationMode { Off, LogOnly, Enforce } private ChecksumValidationMode _checksumMode = ChecksumValidationMode.Enforce; // PERF-5: value-tuple payloads avoid a per-frame heap Tuple allocation. The isSnapshot flag routes // snapshots through the SAME single-consumer queue as deltas (ACC-4/CONC-4: ordered, race-free book build). - private Dictionary> _eventBuffers = new(); + private Dictionary> _eventBuffers = new(); private Dictionary> _tradesBuffers = new(); private readonly object _buffersLock = new object(); // ✅ ADD: Thread-safe buffer access @@ -141,7 +141,7 @@ private async Task InternalStartAsync() // Initialize event buffer for each symbol foreach (var symbol in GetAllNormalizedSymbols()) { - _eventBuffers.Add(symbol, new HelperCustomQueue<(DateTime ts, string symbol, KrakenBookUpdate data, bool isSnapshot)>($"_{this.Name.Replace(" Plugin", "")}", eventBuffers_onReadAction, eventBuffers_onErrorAction)); + _eventBuffers.Add(symbol, new HelperCustomQueue<(DateTime? ts, string symbol, KrakenBookUpdate data, bool isSnapshot)>($"_{this.Name.Replace(" Plugin", "")}", eventBuffers_onReadAction, eventBuffers_onErrorAction)); _tradesBuffers.Add(symbol, new HelperCustomQueue<(string symbol, KrakenTradeUpdate trade)>($"_{this.Name.Replace(" Plugin", "")}", tradesBuffers_onReadAction, tradesBuffers_onErrorAction)); } @@ -435,14 +435,18 @@ private async Task InitializeDeltasAsync() if (!isSnapshot) CheckFrameFreshnessAndWarn(receiveLocal); - HelperCustomQueue<(DateTime ts, string symbol, KrakenBookUpdate data, bool isSnapshot)> buffer; + // The BOOK stamp is the venue's own frame timestamp (null when + // absent) — receiveLocal stays the freshness-guard input only. + var exchangeTs = ResolveBookTimestamp(data.Data); + + HelperCustomQueue<(DateTime? ts, string symbol, KrakenBookUpdate data, bool isSnapshot)> buffer; lock (_buffersLock) { if (!_eventBuffers.TryGetValue(normalizedSymbol, out buffer)) return; // Buffer was cleared during reconnection } - buffer.Add((receiveLocal, normalizedSymbol, data.Data, isSnapshot)); + buffer.Add((exchangeTs, normalizedSymbol, data.Data, isSnapshot)); } catch (Exception ex) { @@ -511,7 +515,7 @@ private async Task InitializePingTimerAsync() _timerPing.Enabled = true; // Start the timer } - private void eventBuffers_onReadAction((DateTime ts, string symbol, KrakenBookUpdate data, bool isSnapshot) e) + private void eventBuffers_onReadAction((DateTime? ts, string symbol, KrakenBookUpdate data, bool isSnapshot) e) { if (e.isSnapshot) ApplySnapshot(e.data, e.symbol, e.ts); @@ -754,7 +758,7 @@ private void UpdateOrderBookSnapshot(KrakenBookUpdate data, string symbol) } // Live snapshot handler. Runs on the single book-consumer thread, strictly in order with deltas // (ACC-4/CONC-4), so the display book and the decimal ladder are always built before any delta. - private void ApplySnapshot(KrakenBookUpdate data, string symbol, DateTime ts) + private void ApplySnapshot(KrakenBookUpdate data, string symbol, DateTime? ts) { var lob = ToOrderBookModel(data, symbol); lob.LastUpdated = ts; @@ -771,7 +775,21 @@ private void ApplySnapshot(KrakenBookUpdate data, string symbol, DateTime ts) RaiseOnDataReceived(lob); } - private void UpdateOrderBook(KrakenBookUpdate lob_update, string symbol, DateTime ts) + /// + /// The single decision point for the venue's book timestamp. Kraken book-v2 + /// carries a frame-level exchange timestamp (wire field "timestamp"); return it + /// in local kind. A frame without it returns null — a default(DateTime) stamp + /// would fabricate a colossal latency spike, and receive time must never + /// masquerade as exchange time (same principle as the ACC-3 trades rule). + /// + public static DateTime? ResolveBookTimestamp(KrakenBookUpdate lob_update) + { + if (lob_update == null || lob_update.Timestamp == default) + return null; + return lob_update.Timestamp.ToLocalTime(); + } + + private void UpdateOrderBook(KrakenBookUpdate lob_update, string symbol, DateTime? ts) { if (!_localOrderBooks.TryGetValue(symbol, out VisualHFT.Model.OrderBook? local_lob) || local_lob == null) return; @@ -794,7 +812,7 @@ private void UpdateOrderBook(KrakenBookUpdate lob_update, string symbol, DateTim continue; // PERF-1: allocation-free primitive overloads (no per-level DeltaBookItem), mirroring KuCoin. if (item.Quantity != 0) - local_lob.AddOrUpdateLevel(true, string.Empty, (double)item.Price, (double)item.Quantity, now, ts); + local_lob.AddOrUpdateLevel(true, string.Empty, (double)item.Price, (double)item.Quantity, now, ts ?? now); else local_lob.DeleteLevel(true, string.Empty, (double)item.Price, (double)item.Quantity); if (validate) @@ -805,7 +823,7 @@ private void UpdateOrderBook(KrakenBookUpdate lob_update, string symbol, DateTim if (item.Quantity == 0 && item.Price == 0 || item.Quantity < 0) continue; if (item.Quantity != 0) - local_lob.AddOrUpdateLevel(false, string.Empty, (double)item.Price, (double)item.Quantity, now, ts); + local_lob.AddOrUpdateLevel(false, string.Empty, (double)item.Price, (double)item.Quantity, now, ts ?? now); else local_lob.DeleteLevel(false, string.Empty, (double)item.Price, (double)item.Quantity); if (validate) diff --git a/VisualHFT.Plugins/MarketConnectors.KuCoin/KuCoinPlugin.cs b/VisualHFT.Plugins/MarketConnectors.KuCoin/KuCoinPlugin.cs index 49d9f197..37ff71b8 100644 --- a/VisualHFT.Plugins/MarketConnectors.KuCoin/KuCoinPlugin.cs +++ b/VisualHFT.Plugins/MarketConnectors.KuCoin/KuCoinPlugin.cs @@ -45,8 +45,8 @@ public class KuCoinPlugin : BasePluginDataRetriever, IDataRetrieverTestable private readonly ReaderWriterLockSlim _orderBooksLock = new ReaderWriterLockSlim(); private readonly object _buffersLock = new object(); - private Dictionary>> _eventBuffers = - new Dictionary>>(); + private Dictionary>> _eventBuffers = + new Dictionary>>(); private Dictionary>> _tradesBuffers = new Dictionary>>(); @@ -172,7 +172,7 @@ private async Task InternalStartAsync() foreach (var symbol in GetAllNormalizedSymbols()) { _eventBuffers.Add(symbol, - new HelperCustomQueue>( + new HelperCustomQueue>( $">_{this.Name.Replace(" Plugin", "")}", eventBuffers_onReadAction, eventBuffers_onErrorAction)); _tradesBuffers.Add(symbol, @@ -449,16 +449,19 @@ private async Task InitializeDeltasAsync() { // Per-frame freshness guard (shared + virtual-clock — see BasePluginDataRetriever). CheckFrameFreshnessAndWarn(data.ReceiveTime.ToLocalTime()); - HelperCustomQueue> buffer; + HelperCustomQueue> buffer; lock (_buffersLock) // ✅ Protect access { if (!_eventBuffers.TryGetValue(normalizedSymbol, out buffer)) return; // Buffer was cleared } + // The BOOK stamp is the venue's own frame timestamp ("time", + // null when absent) — ReceiveTime stays the freshness-guard + // input only, never the book stamp. buffer.Add( - new Tuple( - data.ReceiveTime.ToLocalTime(), normalizedSymbol, data.Data)); + new Tuple( + ResolveBookTimestamp(data.Data), normalizedSymbol, data.Data)); } } catch (Exception ex) @@ -573,7 +576,7 @@ private async Task InitializePingTimerAsync() _timerPing.Enabled = true; // Start the timer } - private void eventBuffers_onReadAction(Tuple eventData) + private void eventBuffers_onReadAction(Tuple eventData) { UpdateOrderBook(eventData.Item3, eventData.Item2, eventData.Item1); } @@ -704,7 +707,21 @@ private void deltaSubscription_Exception(Exception obj) #endregion - private void UpdateOrderBook(KucoinStreamOrderBook lob_update, string symbol, DateTime ts) + /// + /// The single decision point for the venue's book timestamp. KuCoin's aggregated + /// level-2 stream carries a frame-level exchange timestamp (wire field "time"); + /// return it in local kind. A frame without it returns null — a default(DateTime) + /// stamp would fabricate a colossal latency spike, and receive time must never + /// masquerade as exchange time. + /// + public static DateTime? ResolveBookTimestamp(KucoinStreamOrderBook lob_update) + { + if (lob_update == null || lob_update.Timestamp == default) + return null; + return lob_update.Timestamp.ToLocalTime(); + } + + private void UpdateOrderBook(KucoinStreamOrderBook lob_update, string symbol, DateTime? ts) { _orderBooksLock.EnterWriteLock(); @@ -747,7 +764,7 @@ private void UpdateOrderBook(KucoinStreamOrderBook lob_update, string symbol, Da if (item.Quantity == 0) local_lob.DeleteLevel(true, string.Empty, (double)item.Price, (double)item.Quantity); else - local_lob.AddOrUpdateLevel(true, string.Empty, (double)item.Price, (double)item.Quantity, DateTime.Now, ts); + local_lob.AddOrUpdateLevel(true, string.Empty, (double)item.Price, (double)item.Quantity, DateTime.Now, ts ?? DateTime.Now); } foreach (var item in lob_update.Changes.Asks) @@ -758,7 +775,7 @@ private void UpdateOrderBook(KucoinStreamOrderBook lob_update, string symbol, Da if (item.Quantity == 0) local_lob.DeleteLevel(false, string.Empty, (double)item.Price, (double)item.Quantity); else - local_lob.AddOrUpdateLevel(false, string.Empty, (double)item.Price, (double)item.Quantity, DateTime.Now, ts); + local_lob.AddOrUpdateLevel(false, string.Empty, (double)item.Price, (double)item.Quantity, DateTime.Now, ts ?? DateTime.Now); } local_lob.Sequence = lob_update.SequenceEnd;