@@ -11,11 +11,13 @@ The above copyright notice and this permission notice shall be included in all c
1111*/
1212
1313using System ;
14+ using System . Collections . Concurrent ;
1415using System . Collections . Generic ;
1516using System . Linq ;
1617using System . Net ;
1718using System . Text ;
1819using System . Threading . Tasks ;
20+ using System . Web ;
1921
2022using Newtonsoft . Json ;
2123using Newtonsoft . Json . Linq ;
@@ -30,6 +32,7 @@ public ExchangeGeminiAPI()
3032 {
3133 MarketSymbolIsUppercase = false ;
3234 MarketSymbolSeparator = string . Empty ;
35+ RateLimit = new RateGate ( 1 , TimeSpan . FromSeconds ( 0.5 ) ) ;
3336 }
3437
3538 private async Task < ExchangeVolume > ParseVolumeAsync ( JToken token , string symbol )
@@ -93,63 +96,38 @@ protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
9396
9497 protected internal override async Task < IEnumerable < ExchangeMarket > > OnGetMarketSymbolsMetadataAsync ( )
9598 {
96- List < ExchangeMarket > hardcodedSymbols = new List < ExchangeMarket > ( )
99+ Logger . Warn ( "Fetching gemini symbol metadata, this may take a minute..." ) ;
100+
101+ string [ ] symbols = ( await GetMarketSymbolsAsync ( ) ) . ToArray ( ) ;
102+ List < ExchangeMarket > markets = new List < ExchangeMarket > ( ) ;
103+ List < Task > tasks = new List < Task > ( ) ;
104+ foreach ( string symbol in symbols )
97105 {
98- new ExchangeMarket ( ) { IsActive = true ,
99- MarketSymbol = "btcusd" , BaseCurrency = "BTC" , QuoteCurrency = "USD" ,
100- MinTradeSize = 0.00001M , QuantityStepSize = 0.00000001M , PriceStepSize = 0.01M } ,
101- new ExchangeMarket ( ) { IsActive = true ,
102- MarketSymbol = "ethusd" , BaseCurrency = "ETH" , QuoteCurrency = "USD" ,
103- MinTradeSize = 0.001M , QuantityStepSize = 0.000001M , PriceStepSize = 0.01M } ,
104- new ExchangeMarket ( ) { IsActive = true ,
105- MarketSymbol = "ethbtc" , BaseCurrency = "ETH" , QuoteCurrency = "BTC" ,
106- MinTradeSize = 0.001M , QuantityStepSize = 0.000001M , PriceStepSize = 0.00001M } ,
107- new ExchangeMarket ( ) { IsActive = true ,
108- MarketSymbol = "zecusd" , BaseCurrency = "ZEC" , QuoteCurrency = "USD" ,
109- MinTradeSize = 0.001M , QuantityStepSize = 0.000001M , PriceStepSize = 0.01M } ,
110- new ExchangeMarket ( ) { IsActive = true ,
111- MarketSymbol = "zecbtc" , BaseCurrency = "ZEC" , QuoteCurrency = "BTC" ,
112- MinTradeSize = 0.001M , QuantityStepSize = 0.000001M , PriceStepSize = 0.00001M } ,
113- new ExchangeMarket ( ) { IsActive = true ,
114- MarketSymbol = "zeceth" , BaseCurrency = "ZEC" , QuoteCurrency = "ETH" ,
115- MinTradeSize = 0.001M , QuantityStepSize = 0.000001M , PriceStepSize = 0.0001M } ,
116- new ExchangeMarket ( ) { IsActive = true ,
117- MarketSymbol = "zecbch" , BaseCurrency = "ZEC" , QuoteCurrency = "BCH" ,
118- MinTradeSize = 0.001M , QuantityStepSize = 0.000001M , PriceStepSize = 0.0001M } ,
119- new ExchangeMarket ( ) { IsActive = true ,
120- MarketSymbol = "zecltc" , BaseCurrency = "ZEC" , QuoteCurrency = "LTC" ,
121- MinTradeSize = 0.001M , QuantityStepSize = 0.000001M , PriceStepSize = 0.001M } ,
122- new ExchangeMarket ( ) { IsActive = true ,
123- MarketSymbol = "bchusd" , BaseCurrency = "BCH" , QuoteCurrency = "USD" ,
124- MinTradeSize = 0.001M , QuantityStepSize = 0.000001M , PriceStepSize = 0.01M } ,
125- new ExchangeMarket ( ) { IsActive = true ,
126- MarketSymbol = "bchbtc" , BaseCurrency = "BCH" , QuoteCurrency = "BTC" ,
127- MinTradeSize = 0.001M , QuantityStepSize = 0.000001M , PriceStepSize = 0.00001M } ,
128- new ExchangeMarket ( ) { IsActive = true ,
129- MarketSymbol = "bcheth" , BaseCurrency = "BCH" , QuoteCurrency = "ETH" ,
130- MinTradeSize = 0.001M , QuantityStepSize = 0.000001M , PriceStepSize = 0.0001M } ,
131- new ExchangeMarket ( ) { IsActive = true ,
132- MarketSymbol = "ltcusd" , BaseCurrency = "LTC" , QuoteCurrency = "USD" ,
133- MinTradeSize = 0.01M , QuantityStepSize = 0.00001M , PriceStepSize = 0.01M } ,
134- new ExchangeMarket ( ) { IsActive = true ,
135- MarketSymbol = "ltcbtc" , BaseCurrency = "LTC" , QuoteCurrency = "BTC" ,
136- MinTradeSize = 0.01M , QuantityStepSize = 0.00001M , PriceStepSize = 0.00001M } ,
137- new ExchangeMarket ( ) { IsActive = true ,
138- MarketSymbol = "ltceth" , BaseCurrency = "LTC" , QuoteCurrency = "ETH" ,
139- MinTradeSize = 0.01M , QuantityStepSize = 0.00001M , PriceStepSize = 0.0001M } ,
140- new ExchangeMarket ( ) { IsActive = true ,
141- MarketSymbol = "ltcbch" , BaseCurrency = "LTC" , QuoteCurrency = "BCH" ,
142- MinTradeSize = 0.01M , QuantityStepSize = 0.00001M , PriceStepSize = 0.0001M } ,
143- } ;
144- // + check to make sure no symbols are missing
145- var apiSymbols = await GetMarketSymbolsAsync ( ) ;
146- foreach ( var apiSymbol in apiSymbols )
147- if ( ! hardcodedSymbols . Select ( m => m . MarketSymbol ) . Contains ( apiSymbol ) )
148- throw new Exception ( "hardcoded symbols out of date, please send a PR on GitHub to update." ) ;
149- foreach ( var hardcodedSymbol in hardcodedSymbols )
150- if ( ! apiSymbols . Contains ( hardcodedSymbol . MarketSymbol ) )
151- throw new Exception ( "hardcoded symbols out of date, please send a PR on GitHub to update." ) ;
152- return hardcodedSymbols ;
106+ tasks . Add ( Task . Run ( async ( ) =>
107+ {
108+ JToken token = await MakeJsonRequestAsync < JToken > ( "/symbols/details/" + HttpUtility . UrlEncode ( symbol ) ) ;
109+
110+ // {"symbol":"BTCUSD","base_currency":"BTC","quote_currency":"USD","tick_size":1E-8,"quote_increment":0.01,"min_order_size":"0.00001","status":"open"}
111+ lock ( markets )
112+ {
113+ markets . Add ( new ExchangeMarket
114+ {
115+ BaseCurrency = token [ "base_currency" ] . ToStringInvariant ( ) ,
116+ IsActive = token [ "status" ] . ToStringInvariant ( ) . Equals ( "open" , StringComparison . OrdinalIgnoreCase ) ,
117+ MarketSymbol = token [ "symbol" ] . ToStringInvariant ( ) ,
118+ MinTradeSize = token [ "min_order_Size" ] . ConvertInvariant < decimal > ( ) ,
119+ QuantityStepSize = token [ "tick_size" ] . ConvertInvariant < decimal > ( ) ,
120+ QuoteCurrency = token [ "quote_currency" ] . ToStringInvariant ( ) ,
121+ PriceStepSize = token [ "quote_increment" ] . ConvertInvariant < decimal > ( )
122+ } ) ;
123+ }
124+ } ) ) ;
125+ }
126+ await Task . WhenAll ( tasks ) ;
127+
128+ Logger . Warn ( "Gemini symbol metadata fetched and cached for several hours." ) ;
129+
130+ return markets ;
153131 }
154132
155133 protected override async Task < ExchangeTicker > OnGetTickerAsync ( string marketSymbol )
@@ -284,6 +262,98 @@ protected override async Task OnCancelOrderAsync(string orderId, string marketSy
284262 await MakeJsonRequestAsync < JToken > ( "/order/cancel" , null , new Dictionary < string , object > { { "nonce" , nonce } , { "order_id" , orderId } } ) ;
285263 }
286264
265+ protected override async Task < IWebSocket > OnGetTickersWebSocketAsync ( Action < IReadOnlyCollection < KeyValuePair < string , ExchangeTicker > > > tickers , params string [ ] marketSymbols )
266+ {
267+ if ( marketSymbols == null || marketSymbols . Length == 0 )
268+ {
269+ marketSymbols = ( await GetMarketSymbolsAsync ( ) ) . ToArray ( ) ;
270+ }
271+ ConcurrentDictionary < string , decimal > volumeDict = new ConcurrentDictionary < string , decimal > ( ) ;
272+ ConcurrentDictionary < string , ExchangeTicker > tickerDict = new ConcurrentDictionary < string , ExchangeTicker > ( ) ;
273+ return await ConnectWebSocketAsync ( null , messageCallback : async ( _socket , msg ) =>
274+ {
275+ JToken token = JToken . Parse ( msg . ToStringFromUTF8 ( ) ) ;
276+ if ( token [ "result" ] . ToStringInvariant ( ) == "error" )
277+ {
278+ // {{ "result": "error", "reason": "InvalidJson"}}
279+ Logger . Info ( token [ "reason" ] . ToStringInvariant ( ) ) ;
280+ }
281+ else if ( token [ "type" ] . ToStringInvariant ( ) == "candles_1d_updates" )
282+ {
283+ JToken changesToken = token [ "changes" ] ;
284+ string marketSymbol = token [ "symbol" ] . ToStringInvariant ( ) ;
285+ if ( changesToken != null )
286+ {
287+ if ( changesToken . FirstOrDefault ( ) is JArray candleArray )
288+ {
289+ decimal volume = candleArray [ 5 ] . ConvertInvariant < decimal > ( ) ;
290+ volumeDict [ marketSymbol ] = volume ;
291+ }
292+ }
293+ }
294+ else if ( token [ "type" ] . ToStringInvariant ( ) == "l2_updates" )
295+ {
296+ // fetch the active ticker metadata for this symbol
297+ string marketSymbol = token [ "symbol" ] . ToStringInvariant ( ) ;
298+ ExchangeTicker ticker = tickerDict . GetOrAdd ( marketSymbol , ( _marketSymbol ) =>
299+ {
300+ ( string baseCurrency , string quoteCurrency ) = ExchangeMarketSymbolToCurrenciesAsync ( marketSymbol ) . Sync ( ) ;
301+ return new ExchangeTicker
302+ {
303+ MarketSymbol = _marketSymbol ,
304+ Volume = new ExchangeVolume
305+ {
306+ BaseCurrency = baseCurrency ,
307+ QuoteCurrency = quoteCurrency
308+ }
309+ } ;
310+ } ) ;
311+
312+ // fetch the last bid/ask/last prices
313+ if ( token [ "changes" ] is JArray changesToken )
314+ {
315+ if ( changesToken . FirstOrDefault ( t => t [ 0 ] . ToStringInvariant ( ) . Equals ( "buy" , StringComparison . OrdinalIgnoreCase ) ) is JArray buyToken )
316+ {
317+ decimal bidPrice = buyToken [ 1 ] . ConvertInvariant < decimal > ( ) ;
318+ ticker . Bid = bidPrice ;
319+ }
320+
321+ if ( changesToken . FirstOrDefault ( t => t [ 0 ] . ToStringInvariant ( ) . Equals ( "sell" , StringComparison . OrdinalIgnoreCase ) ) is JArray sellToken )
322+ {
323+ decimal askPrice = sellToken [ 1 ] . ConvertInvariant < decimal > ( ) ;
324+ ticker . Ask = askPrice ;
325+ }
326+
327+ if ( token [ "trades" ] is JArray tradesToken )
328+ {
329+ JToken lastTrade = tradesToken . FirstOrDefault ( ) ;
330+ if ( lastTrade != null )
331+ {
332+ decimal lastTradePrice = lastTrade [ "price" ] . ConvertInvariant < decimal > ( ) ;
333+ ticker . Last = lastTradePrice ;
334+ }
335+ }
336+
337+ // see if we have volume yet
338+ if ( volumeDict . TryGetValue ( marketSymbol , out decimal tickerVolume ) )
339+ {
340+ ticker . Volume . BaseCurrencyVolume = tickerVolume ;
341+ ticker . Volume . QuoteCurrencyVolume = tickerVolume * ticker . Last ;
342+ var kv = new KeyValuePair < string , ExchangeTicker > ( marketSymbol , ticker ) ;
343+ tickers ( new KeyValuePair < string , ExchangeTicker > [ ] { kv } ) ;
344+ }
345+ }
346+ }
347+ } , connectCallback : async ( _socket ) =>
348+ {
349+ await _socket . SendMessageAsync ( new
350+ {
351+ type = "subscribe" ,
352+ subscriptions = new [ ] { new { name = "candles_1d" , symbols = marketSymbols } , new { name = "l2" , symbols = marketSymbols } }
353+ } ) ;
354+ } ) ;
355+ }
356+
287357 protected override async Task < IWebSocket > OnGetTradesWebSocketAsync ( Func < KeyValuePair < string , ExchangeTrade > , Task > callback , params string [ ] marketSymbols )
288358 {
289359 //{
@@ -358,6 +428,7 @@ protected override async Task<IWebSocket> OnGetTradesWebSocketAsync(Func<KeyValu
358428 {
359429 marketSymbols = ( await GetMarketSymbolsAsync ( ) ) . ToArray ( ) ;
360430 }
431+
361432 return await ConnectWebSocketAsync ( BaseUrlWebSocket , messageCallback : async ( _socket , msg ) =>
362433 {
363434 JToken token = JToken . Parse ( msg . ToStringFromUTF8 ( ) ) ;
@@ -371,15 +442,15 @@ protected override async Task<IWebSocket> OnGetTradesWebSocketAsync(Func<KeyValu
371442 var tradesToken = token [ "trades" ] ;
372443 if ( tradesToken != null ) foreach ( var tradeToken in tradesToken )
373444 {
374- var trade = parseTrade ( tradeToken ) ;
445+ var trade = ParseWebSocketTrade ( tradeToken ) ;
375446 trade . Flags |= ExchangeTradeFlags . IsFromSnapshot ;
376447 await callback ( new KeyValuePair < string , ExchangeTrade > ( marketSymbol , trade ) ) ;
377448 }
378449 }
379450 else if ( token [ "type" ] . ToStringInvariant ( ) == "trade" )
380451 {
381452 string marketSymbol = token [ "symbol" ] . ToStringInvariant ( ) ;
382- var trade = parseTrade ( token ) ;
453+ var trade = ParseWebSocketTrade ( token ) ;
383454 await callback ( new KeyValuePair < string , ExchangeTrade > ( marketSymbol , trade ) ) ;
384455 }
385456 } , connectCallback : async ( _socket ) =>
@@ -388,11 +459,12 @@ protected override async Task<IWebSocket> OnGetTradesWebSocketAsync(Func<KeyValu
388459 await _socket . SendMessageAsync ( new {
389460 type = "subscribe" , subscriptions = new [ ] { new { name = "l2" , symbols = marketSymbols } } } ) ;
390461 } ) ;
391- ExchangeTrade parseTrade ( JToken token ) => token . ParseTrade (
392- amountKey : "quantity" , priceKey : "price" ,
393- typeKey : "side" , timestampKey : "timestamp" ,
394- TimestampType . UnixMilliseconds , idKey : "event_id" ) ;
395462 }
463+
464+ private static ExchangeTrade ParseWebSocketTrade ( JToken token ) => token . ParseTrade (
465+ amountKey : "quantity" , priceKey : "price" ,
466+ typeKey : "side" , timestampKey : "timestamp" ,
467+ TimestampType . UnixMilliseconds , idKey : "event_id" ) ;
396468 }
397469
398470 public partial class ExchangeName { public const string Gemini = "Gemini" ; }
0 commit comments