From 26b2dd8153f18ff8a8720e5e27d74729931b7744 Mon Sep 17 00:00:00 2001 From: mike Date: Wed, 19 Nov 2025 06:29:22 -0500 Subject: [PATCH 1/2] Added Exchange and Symbol to trade messages, and ClientOrderId and Status to Position messages. Depth of book now also includes the exchange name. --- .../BinanceGroup/BinanceGroupCommon.cs | 30 ++-- .../Exchanges/BitBank/ExchangeBitBankAPI.cs | 1 + .../Exchanges/Bitfinex/ExchangeBitfinexAPI.cs | 1 + .../API/Exchanges/Gemini/ExchangeGeminiAPI.cs | 140 +----------------- .../API/Exchanges/Kraken/ExchangeKrakenAPI.cs | 6 +- .../API/Exchanges/_Base/ExchangeAPI.cs | 2 +- .../Exchanges/_Base/ExchangeAPIExtensions.cs | 8 + src/ExchangeSharp/Model/ExchangePosition.cs | 10 ++ src/ExchangeSharp/Model/ExchangeTrade.cs | 10 ++ 9 files changed, 61 insertions(+), 147 deletions(-) diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs index 483065d6..6b2beb0b 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs @@ -438,20 +438,24 @@ params string[] marketSymbols name.Substring(0, name.IndexOf('@')) ); - // buy=0 -> m = true (The buyer is maker, while the seller is taker). - // buy=1 -> m = false(The seller is maker, while the buyer is taker). - await callback( + ExchangeTrade trade = + token.ParseTradeBinance( + amountKey: "q", + priceKey: "p", + typeKey: "m", + timestampKey: "T", // use trade time (T) instead of event time (E) + timestampType: TimestampType.UnixMilliseconds, + idKey: "a", + typeKeyIsBuyValue: "false"); + trade.Exchange = Name; + trade.Symbol = marketSymbol; + + // buy=0 -> m = true (The buyer is maker, while the seller is taker). + // buy=1 -> m = false(The seller is maker, while the buyer is taker). + await callback( new KeyValuePair( marketSymbol, - token.ParseTradeBinance( - amountKey: "q", - priceKey: "p", - typeKey: "m", - timestampKey: "T", // use trade time (T) instead of event time (E) - timestampType: TimestampType.UnixMilliseconds, - idKey: "a", - typeKeyIsBuyValue: "false" - ) + trade ) ); } @@ -525,7 +529,7 @@ protected override async Task OnGetOrderBookAsync( $"/depth?symbol={marketSymbol}&limit={maxCount}", BaseUrlApi ); - return obj.ParseOrderBookFromJTokenArrays(sequence: "lastUpdateId"); + return obj.ParseOrderBookFromJTokenArrays(exchange: Name, sequence: "lastUpdateId"); } protected override async Task OnGetHistoricalTradesAsync( diff --git a/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs b/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs index eaf966ed..f772ed27 100644 --- a/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs @@ -88,6 +88,7 @@ protected override async Task OnGetOrderBookAsync( } result.MarketSymbol = NormalizeMarketSymbol(marketSymbol); } + result.ExchangeName = Name; return result; } diff --git a/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs b/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs index 38eb3989..595e9327 100644 --- a/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs @@ -434,6 +434,7 @@ protected override async Task OnGetOrderBookAsync( }; } } + orders.ExchangeName = Name; return orders; } diff --git a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs index cb220fc0..f7cae991 100644 --- a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs @@ -10,17 +10,14 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Net; -using System.Text; using System.Threading.Tasks; using System.Web; -using System.Xml; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -35,7 +32,7 @@ private ExchangeGeminiAPI() MarketSymbolIsUppercase = false; MarketSymbolSeparator = string.Empty; WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; - RateLimit = new RateGate(1, TimeSpan.FromSeconds(0.5)); + RateLimit = new RateGate(600, TimeSpan.FromSeconds(60)); } private async Task ParseVolumeAsync(JToken token, string symbol) @@ -126,130 +123,6 @@ protected internal override async Task< > OnGetMarketSymbolsMetadataAsync() { List markets = new List(); - - try - { - string html = ( - await RequestMaker.MakeRequestAsync("/rest-api", "https://docs.gemini.com") - ).Response; - int startPos = html.IndexOf( - "

Symbols and minimums

" - ); - if (startPos < 0) - { - throw new ApplicationException( - "Gemini html for symbol metadata is missing expected h1 tag and id" - ); - } - - startPos = html.IndexOf("", startPos); - if (startPos < 0) - { - throw new ApplicationException( - "Gemini html for symbol metadata is missing start tbody tag" - ); - } - - int endPos = html.IndexOf("", startPos); - if (endPos < 0) - { - throw new ApplicationException( - "Gemini html for symbol metadata is missing ending tbody tag" - ); - } - - string table = html.Substring(startPos, endPos - startPos + "".Length); - string xml = "\n" + table; - XmlDocument doc = new XmlDocument(); - doc.LoadXml(xml); - if (doc.ChildNodes.Count < 2) - { - throw new ApplicationException( - "Gemini html for symbol metadata does not have the expected number of nodes" - ); - } - - XmlNode root = doc.ChildNodes.Item(1); - foreach (XmlNode tr in root.ChildNodes) - { - // - // Symbol - // Minimum Order Size - // Tick Size - // Quote Currency Price Increment - - // btcusd - // 0.00001 BTC (1e-5) - // 0.00000001 BTC (1e-8) - // 0.01 USD - // - - if (tr.ChildNodes.Count != 4) - { - throw new ApplicationException( - "Gemini html for symbol metadata does not have 4 rows per entry anymore" - ); - } - - ExchangeMarket market = new ExchangeMarket { IsActive = true }; - XmlNode symbolNode = tr.ChildNodes.Item(0); - XmlNode minOrderSizeNode = tr.ChildNodes.Item(1); - XmlNode tickSizeNode = tr.ChildNodes.Item(2); - XmlNode incrementNode = tr.ChildNodes.Item(3); - string symbol = symbolNode.InnerText; - int minOrderSizePos = minOrderSizeNode.InnerText.IndexOf(' '); - if (minOrderSizePos < 0) - { - throw new ArgumentException( - "Min order size text does not have a space after the number" - ); - } - decimal minOrderSize = minOrderSizeNode.InnerText - .Substring(0, minOrderSizePos) - .ConvertInvariant(); - int tickSizePos = tickSizeNode.InnerText.IndexOf(' '); - if (tickSizePos < 0) - { - throw new ArgumentException( - "Tick size text does not have a space after the number" - ); - } - decimal tickSize = tickSizeNode.InnerText - .Substring(0, tickSizePos) - .ConvertInvariant(); - int incrementSizePos = incrementNode.InnerText.IndexOf(' '); - if (incrementSizePos < 0) - { - throw new ArgumentException( - "Increment size text does not have a space after the number" - ); - } - decimal incrementSize = incrementNode.InnerText - .Substring(0, incrementSizePos) - .ConvertInvariant(); - market.MarketSymbol = symbol; - market.AltMarketSymbol = symbol.ToUpper(); - market.BaseCurrency = symbol.Substring(0, symbol.Length - 3); - market.QuoteCurrency = symbol.Substring(symbol.Length - 3); - market.MinTradeSize = minOrderSize; - market.QuantityStepSize = tickSize; - market.PriceStepSize = incrementSize; - markets.Add(market); - } - return markets; - } - catch (Exception ex) - { - markets.Clear(); - Logger.Error( - ex, - "Failed to parse gemini symbol metadata web page, falling back to per symbol query..." - ); - } - - // slow way, fetch each symbol one by one, gemini api epic fail - Logger.Warn("Fetching gemini symbol metadata per symbol, this may take a minute..."); - string[] symbols = (await GetMarketSymbolsAsync()).ToArray(); List tasks = new List(); foreach (string symbol in symbols) @@ -323,7 +196,7 @@ protected override async Task OnGetOrderBookAsync( JToken obj = await MakeJsonRequestAsync( "/book/" + marketSymbol + "?limit_bids=" + maxCount + "&limit_asks=" + maxCount ); - return obj.ParseOrderBookFromJTokenDictionaries(); + return obj.ParseOrderBookFromJTokenDictionaries(exchange: Name); } protected override async Task OnGetHistoricalTradesAsync( @@ -489,7 +362,7 @@ protected override async Task> OnGetOpenOrderDe { foreach (JToken token in array) { - if (marketSymbol == null || token["symbol"].ToStringInvariant() == marketSymbol) + if (string.IsNullOrEmpty(marketSymbol) || token["symbol"].ToStringInvariant() == marketSymbol) { orders.Add(ParseOrder(token)); } @@ -802,6 +675,8 @@ params string[] marketSymbols foreach (var tradeToken in tradesToken) { var trade = ParseWebSocketTrade(tradeToken); + trade.Exchange = Name; + trade.Symbol = marketSymbol; trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; await callback( new KeyValuePair(marketSymbol, trade) @@ -812,6 +687,7 @@ await callback( { string marketSymbol = token["symbol"].ToStringInvariant(); var trade = ParseWebSocketTrade(token); + trade.Exchange = Name; await callback( new KeyValuePair(marketSymbol, trade) ); diff --git a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs index 6a2c8576..e59bc5f9 100644 --- a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs @@ -770,7 +770,7 @@ protected override async Task OnGetOrderBookAsync( JToken obj = await MakeJsonRequestAsync( "/0/public/Depth?pair=" + marketSymbol + "&count=" + maxCount ); - return obj[marketSymbol].ParseOrderBookFromJTokenArrays(); + return obj[marketSymbol].ParseOrderBookFromJTokenArrays(exchange: Name); } protected override async Task> OnGetRecentTradesAsync( @@ -1154,6 +1154,8 @@ private ExchangePosition ParsePosition(JToken token) { if (kvp.Value["descr"] is JObject descr) { + result.ClientOrderId = kvp.Value["cl_ord_id"].ToObject(); + result.Status = kvp.Value["status"].ToObject(); decimal epochMilliseconds = kvp.Value["opentm"].ToObject(); // Preserve Kraken timestamp decimal precision by converting seconds to milliseconds. epochMilliseconds = epochMilliseconds * 1000; @@ -1399,6 +1401,8 @@ params string[] marketSymbols idKey: null, typeKeyIsBuyValue: "b" ); + trade.Exchange = Name; + trade.Symbol = marketSymbol; await callback( new KeyValuePair(marketSymbol, trade) ); diff --git a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs index fe07e57b..6d73b1ca 100644 --- a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs @@ -996,7 +996,7 @@ string marketSymbol // not sure if this is needed, but adding it just in case await new SynchronizationContextRemover(); var lookup = await this.GetExchangeMarketDictionaryFromCacheAsync(); - + if (lookup == null) return null; foreach (var kvp in lookup) { if ( diff --git a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs index 11016076..1abf7aaa 100644 --- a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs +++ b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs @@ -44,6 +44,7 @@ public static class ExchangeAPIExtensions /// Web socket, call Dispose to close public static async Task GetFullOrderBookWebSocketAsync( this IOrderBookProvider api, + string exchange, Action callback, int maxCount = 100, params string[] symbols @@ -192,6 +193,7 @@ out partialOrderBookQueue fullOrderBook.LastUpdatedUtc = CryptoUtility.UtcNow; trimFullOrderBook(fullOrderBook); + fullOrderBook.ExchangeName = exchange; callback(fullOrderBook); } @@ -469,6 +471,7 @@ internal static ExchangeOrderBook ParseOrderBookFromJTokenArray( /// Order book internal static ExchangeOrderBook ParseOrderBookFromJTokenArrays( this JToken token, + string exchange = "unknown", string asks = "asks", string bids = "bids", string sequence = "ts" @@ -518,6 +521,8 @@ internal static ExchangeOrderBook ParseOrderBookFromJTokenArrays( Logger.Error($"No bids in {nameof(ParseOrderBookFromJTokenArrays)}"); } + book.ExchangeName = exchange; + return book; } @@ -532,6 +537,7 @@ internal static ExchangeOrderBook ParseOrderBookFromJTokenArrays( /// Order book internal static ExchangeOrderBook ParseOrderBookFromJTokenDictionaries( this JToken token, + string exchange = "unknown", string asks = "asks", string bids = "bids", string price = "price", @@ -563,6 +569,8 @@ internal static ExchangeOrderBook ParseOrderBookFromJTokenDictionaries( book.Bids[depth.Price] = depth; } + book.ExchangeName = exchange; + return book; } diff --git a/src/ExchangeSharp/Model/ExchangePosition.cs b/src/ExchangeSharp/Model/ExchangePosition.cs index 42c26f26..08e4e18b 100644 --- a/src/ExchangeSharp/Model/ExchangePosition.cs +++ b/src/ExchangeSharp/Model/ExchangePosition.cs @@ -19,6 +19,16 @@ namespace ExchangeSharp /// public class ExchangePosition { + /// + /// Client Order ID + /// + public string ClientOrderId { get; set; } + + /// + /// Order Status + /// + public string Status { get; set; } + /// /// Market Symbol /// diff --git a/src/ExchangeSharp/Model/ExchangeTrade.cs b/src/ExchangeSharp/Model/ExchangeTrade.cs index 86cc3bab..ad9746ff 100644 --- a/src/ExchangeSharp/Model/ExchangeTrade.cs +++ b/src/ExchangeSharp/Model/ExchangeTrade.cs @@ -24,6 +24,16 @@ namespace ExchangeSharp /// public class ExchangeTrade { + /// + /// Exchange + /// + public string Exchange { get; set; } + + /// + /// Symbol + /// + public string Symbol { get; set; } + /// /// Timestamp /// From f1332a1739c74599b2beffd816b186204c62c7d6 Mon Sep 17 00:00:00 2001 From: mike Date: Wed, 19 Nov 2025 06:34:26 -0500 Subject: [PATCH 2/2] Reverting OnGetMarketSymbolsMetadataAsync (unintentionally changed.) --- .../API/Exchanges/Gemini/ExchangeGeminiAPI.cs | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs index f7cae991..54e3e68c 100644 --- a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs @@ -18,6 +18,7 @@ The above copyright notice and this permission notice shall be included in all c using System.Linq; using System.Threading.Tasks; using System.Web; +using System.Xml; namespace ExchangeSharp { @@ -123,6 +124,130 @@ protected internal override async Task< > OnGetMarketSymbolsMetadataAsync() { List markets = new List(); + + try + { + string html = ( + await RequestMaker.MakeRequestAsync("/rest-api", "https://docs.gemini.com") + ).Response; + int startPos = html.IndexOf( + "

Symbols and minimums

" + ); + if (startPos < 0) + { + throw new ApplicationException( + "Gemini html for symbol metadata is missing expected h1 tag and id" + ); + } + + startPos = html.IndexOf("", startPos); + if (startPos < 0) + { + throw new ApplicationException( + "Gemini html for symbol metadata is missing start tbody tag" + ); + } + + int endPos = html.IndexOf("", startPos); + if (endPos < 0) + { + throw new ApplicationException( + "Gemini html for symbol metadata is missing ending tbody tag" + ); + } + + string table = html.Substring(startPos, endPos - startPos + "".Length); + string xml = "\n" + table; + XmlDocument doc = new XmlDocument(); + doc.LoadXml(xml); + if (doc.ChildNodes.Count < 2) + { + throw new ApplicationException( + "Gemini html for symbol metadata does not have the expected number of nodes" + ); + } + + XmlNode root = doc.ChildNodes.Item(1); + foreach (XmlNode tr in root.ChildNodes) + { + // + // Symbol + // Minimum Order Size + // Tick Size + // Quote Currency Price Increment + + // btcusd + // 0.00001 BTC (1e-5) + // 0.00000001 BTC (1e-8) + // 0.01 USD + // + + if (tr.ChildNodes.Count != 4) + { + throw new ApplicationException( + "Gemini html for symbol metadata does not have 4 rows per entry anymore" + ); + } + + ExchangeMarket market = new ExchangeMarket { IsActive = true }; + XmlNode symbolNode = tr.ChildNodes.Item(0); + XmlNode minOrderSizeNode = tr.ChildNodes.Item(1); + XmlNode tickSizeNode = tr.ChildNodes.Item(2); + XmlNode incrementNode = tr.ChildNodes.Item(3); + string symbol = symbolNode.InnerText; + int minOrderSizePos = minOrderSizeNode.InnerText.IndexOf(' '); + if (minOrderSizePos < 0) + { + throw new ArgumentException( + "Min order size text does not have a space after the number" + ); + } + decimal minOrderSize = minOrderSizeNode.InnerText + .Substring(0, minOrderSizePos) + .ConvertInvariant(); + int tickSizePos = tickSizeNode.InnerText.IndexOf(' '); + if (tickSizePos < 0) + { + throw new ArgumentException( + "Tick size text does not have a space after the number" + ); + } + decimal tickSize = tickSizeNode.InnerText + .Substring(0, tickSizePos) + .ConvertInvariant(); + int incrementSizePos = incrementNode.InnerText.IndexOf(' '); + if (incrementSizePos < 0) + { + throw new ArgumentException( + "Increment size text does not have a space after the number" + ); + } + decimal incrementSize = incrementNode.InnerText + .Substring(0, incrementSizePos) + .ConvertInvariant(); + market.MarketSymbol = symbol; + market.AltMarketSymbol = symbol.ToUpper(); + market.BaseCurrency = symbol.Substring(0, symbol.Length - 3); + market.QuoteCurrency = symbol.Substring(symbol.Length - 3); + market.MinTradeSize = minOrderSize; + market.QuantityStepSize = tickSize; + market.PriceStepSize = incrementSize; + markets.Add(market); + } + return markets; + } + catch (Exception ex) + { + markets.Clear(); + Logger.Error( + ex, + "Failed to parse gemini symbol metadata web page, falling back to per symbol query..." + ); + } + + // slow way, fetch each symbol one by one, gemini api epic fail + Logger.Warn("Fetching gemini symbol metadata per symbol, this may take a minute..."); + string[] symbols = (await GetMarketSymbolsAsync()).ToArray(); List tasks = new List(); foreach (string symbol in symbols)