Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions TriggerEngine/TriggerEngineService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion TriggerEngine/View/AddAPISetting.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
14 changes: 11 additions & 3 deletions TriggerEngine/View/TriggerSettingAddOrUpdate.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,18 @@ TextOptions.TextRenderingMode="Auto"
<LineBreak />
<Run> • </Run>
<Run FontWeight="Bold">Body Template:</Run>
<Run> JSON payload with dynamic placeholders like </Run>
<Run> JSON payload with dynamic placeholders: </Run>
<Run FontStyle="Italic">{{rulename}}</Run><Run>, </Run>
<Run FontStyle="Italic">{{metric}}</Run>
<Run> (the metric that fired), </Run>
<Run FontStyle="Italic">{{condition}}</Run><Run>, </Run>
<Run FontStyle="Italic">{{threshold}}</Run><Run>, </Run>
<Run FontStyle="Italic">{{value}}</Run>
<Run> and </Run>
<Run FontStyle="Italic">{{timestamp}}</Run>
<Run>. </Run>
<Run FontStyle="Italic">{{plugin}}</Run>
<Run> or </Run>
<Run FontStyle="Italic">{{condition}} etc</Run><Run>.</Run>
<Run> is kept for older templates and receives the same metric name.</Run>
<LineBreak />
<Run> • </Run>
<Run FontWeight="Bold">Headers:</Run>
Expand Down
29 changes: 24 additions & 5 deletions VisualHFT.Plugins/MarketConnectors.Binance/BinancePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -662,6 +664,20 @@ private void deltaSubscription_Exception(Exception obj)
#endregion

// ✅ FIX: Thread-safe UpdateOrderBook with TryGetValue
/// <summary>
/// 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.
/// </summary>
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
Expand All @@ -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;
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);
}

Expand Down
35 changes: 26 additions & 9 deletions VisualHFT.Plugins/MarketConnectors.Bitfinex/BitfinexPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class BitfinexPlugin : BasePluginDataRetriever, IDataRetrieverTestable
private BitfinexSocketClient _socketClient;
private BitfinexRestClient _restClient;
private Dictionary<string, VisualHFT.Model.OrderBook> _localOrderBooks = new Dictionary<string, VisualHFT.Model.OrderBook>();
private Dictionary<string, HelperCustomQueue<Tuple<DateTime, string, BitfinexOrderBookEntry>>> _eventBuffers = new();
private Dictionary<string, HelperCustomQueue<Tuple<DateTime?, string, BitfinexOrderBookEntry>>> _eventBuffers = new();
private Dictionary<string, HelperCustomQueue<Tuple<string, BitfinexTradeSimple>>> _tradesBuffers = new();
private readonly object _buffersLock = new object(); // ✅ ADD: Thread-safe buffer access

Expand Down Expand Up @@ -115,7 +115,7 @@ private async Task InternalStartAsync()
// Initialize event buffer for each symbol
foreach (var symbol in GetAllNormalizedSymbols())
{
_eventBuffers.Add(symbol, new HelperCustomQueue<Tuple<DateTime, string, BitfinexOrderBookEntry>>($"<Tuple<DateTime, string, BitfinexOrderBookEntry>>_{this.Name.Replace(" Plugin", "")}", eventBuffers_onReadAction, eventBuffers_onErrorAction));
_eventBuffers.Add(symbol, new HelperCustomQueue<Tuple<DateTime?, string, BitfinexOrderBookEntry>>($"<Tuple<DateTime?, string, BitfinexOrderBookEntry>>_{this.Name.Replace(" Plugin", "")}", eventBuffers_onReadAction, eventBuffers_onErrorAction));
_tradesBuffers.Add(symbol, new HelperCustomQueue<Tuple<string, BitfinexTradeSimple>>($"<Tuple<DateTime, string, BitfinexTradeSimple>>_{this.Name.Replace(" Plugin", "")}", tradesBuffers_onReadAction, tradesBuffers_onErrorAction));
}

Expand Down Expand Up @@ -356,18 +356,21 @@ private async Task InitializeDeltasAsync()
else
{
// ✅ FIX: Thread-safe buffer access
HelperCustomQueue<Tuple<DateTime, string, BitfinexOrderBookEntry>> buffer;
HelperCustomQueue<Tuple<DateTime?, string, BitfinexOrderBookEntry>> 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<DateTime, string, BitfinexOrderBookEntry>(
data.ReceiveTime.ToLocalTime(), normalizedSymbol, item));
new Tuple<DateTime?, string, BitfinexOrderBookEntry>(
exchangeTs, normalizedSymbol, item));
}
}
}
Expand Down Expand Up @@ -436,7 +439,7 @@ private async Task InitializePingTimerAsync()
_timerPing.Enabled = true; // Start the timer
}

private void eventBuffers_onReadAction(Tuple<DateTime, string, BitfinexOrderBookEntry> eventData)
private void eventBuffers_onReadAction(Tuple<DateTime?, string, BitfinexOrderBookEntry> eventData)
{
UpdateOrderBook(eventData.Item3, eventData.Item2, eventData.Item1);
}
Expand Down Expand Up @@ -659,7 +662,21 @@ private void UpdateOrderBookSnapshot(IEnumerable<BitfinexOrderBookEntry> data, s
});
});
}
private void UpdateOrderBook(BitfinexOrderBookEntry lob_update, string symbol, DateTime ts)
/// <summary>
/// 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.
/// </summary>
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;
Expand All @@ -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,
};
Expand All @@ -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,
};
Expand Down
55 changes: 44 additions & 11 deletions VisualHFT.Plugins/MarketConnectors.Coinbase/CoinbasePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public class CoinbasePlugin : BasePluginDataRetriever
private readonly ConcurrentDictionary<string, VisualHFT.Model.OrderBook> _localOrderBooks =
new ConcurrentDictionary<string, VisualHFT.Model.OrderBook>();

private readonly ConcurrentDictionary<string, HelperCustomQueue<Tuple<DateTime, string, CoinbaseOrderBookUpdate>>> _eventBuffers =
new ConcurrentDictionary<string, HelperCustomQueue<Tuple<DateTime, string, CoinbaseOrderBookUpdate>>>();
private readonly ConcurrentDictionary<string, HelperCustomQueue<Tuple<DateTime?, string, CoinbaseOrderBookUpdate>>> _eventBuffers =
new ConcurrentDictionary<string, HelperCustomQueue<Tuple<DateTime?, string, CoinbaseOrderBookUpdate>>>();

private readonly ConcurrentDictionary<string, HelperCustomQueue<Tuple<string, CoinbaseTrade>>> _tradesBuffers =
new ConcurrentDictionary<string, HelperCustomQueue<Tuple<string, CoinbaseTrade>>>();
Expand Down Expand Up @@ -139,7 +139,7 @@ private async Task InternalStartAsync()
foreach (var symbol in GetAllNormalizedSymbols())
{
_eventBuffers.TryAdd(symbol,
new HelperCustomQueue<Tuple<DateTime, string, CoinbaseOrderBookUpdate>>(
new HelperCustomQueue<Tuple<DateTime?, string, CoinbaseOrderBookUpdate>>(
$"<Tuple<DateTime, string, CoinbaseStreamOrderBookChanged>>_{this.Name.Replace(" Plugin", "")}",
eventBuffers_onReadAction, eventBuffers_onErrorAction));
Comment on lines +142 to 144
_tradesBuffers.TryAdd(symbol,
Expand Down Expand Up @@ -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<DateTime, string, CoinbaseOrderBookUpdate>(
data.ReceiveTime.ToLocalTime(), normalizedSymbol, data.Data));
new Tuple<DateTime?, string, CoinbaseOrderBookUpdate>(
ResolveBookTimestamp(data.Data), normalizedSymbol, data.Data));
}
}
catch (Exception ex)
Expand Down Expand Up @@ -417,7 +420,7 @@ private async Task InitializePingTimerAsync()
_timerPing.Enabled = true; // Start the timer
}

private void eventBuffers_onReadAction(Tuple<DateTime, string, CoinbaseOrderBookUpdate> eventData)
private void eventBuffers_onReadAction(Tuple<DateTime?, string, CoinbaseOrderBookUpdate> eventData)
{
UpdateOrderBook(eventData.Item3, eventData.Item2, eventData.Item1);
}
Expand Down Expand Up @@ -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)
/// <summary>
/// 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.
/// </summary>
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;
Expand Down Expand Up @@ -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
});
Comment on lines 593 to 597
}
Expand All @@ -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
});
}
Expand All @@ -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
});
}
Expand All @@ -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
});
}
Expand Down
Loading