Skip to content

Commit d3e5a86

Browse files
committed
Gemini ticker web socket and symbol metadata fix
1 parent df06927 commit d3e5a86

File tree

8 files changed

+156
-78
lines changed

8 files changed

+156
-78
lines changed

src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs

Lines changed: 134 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ The above copyright notice and this permission notice shall be included in all c
1111
*/
1212

1313
using System;
14+
using System.Collections.Concurrent;
1415
using System.Collections.Generic;
1516
using System.Linq;
1617
using System.Net;
1718
using System.Text;
1819
using System.Threading.Tasks;
20+
using System.Web;
1921

2022
using Newtonsoft.Json;
2123
using 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"; }

src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ static ExchangeKrakenAPI()
5757
/// <returns>Task</returns>
5858
private async Task PopulateLookupTables()
5959
{
60-
await Cache.Get<object>(nameof(PopulateLookupTables), async () =>
60+
await Cache.GetOrCreate<object>(nameof(PopulateLookupTables), async () =>
6161
{
6262
IReadOnlyDictionary<string, ExchangeCurrency> currencies = await GetCurrenciesAsync();
6363
ExchangeMarket[] markets = (await GetMarketSymbolsMetadataAsync())?.ToArray();

src/ExchangeSharp/API/Exchanges/ZBcom/ExchangeZBcomAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>>
122122

123123
var data = await MakeRequestZBcomAsync(null, "/allTicker", BaseUrl);
124124
List<KeyValuePair<string, ExchangeTicker>> tickers = new List<KeyValuePair<string, ExchangeTicker>>();
125-
var symbolLookup = await Cache.Get<Dictionary<string, string>>(nameof(GetMarketSymbolsAsync) + "_Set", async () =>
125+
var symbolLookup = await Cache.GetOrCreate<Dictionary<string, string>>(nameof(GetMarketSymbolsAsync) + "_Set", async () =>
126126
{
127127
// create lookup dictionary of symbol string without separator to symbol string with separator
128128
IEnumerable<string> symbols = await GetMarketSymbolsAsync();

src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ public ExchangeAPI()
252252
{
253253
MethodCachePolicy.Add(nameof(GetCurrenciesAsync), TimeSpan.FromHours(1.0));
254254
MethodCachePolicy.Add(nameof(GetMarketSymbolsAsync), TimeSpan.FromHours(1.0));
255-
MethodCachePolicy.Add(nameof(GetMarketSymbolsMetadataAsync), TimeSpan.FromHours(4.0));
255+
MethodCachePolicy.Add(nameof(GetMarketSymbolsMetadataAsync), TimeSpan.FromHours(6.0));
256256
MethodCachePolicy.Add(nameof(GetTickerAsync), TimeSpan.FromSeconds(10.0));
257257
MethodCachePolicy.Add(nameof(GetTickersAsync), TimeSpan.FromSeconds(10.0));
258258
MethodCachePolicy.Add(nameof(GetOrderBookAsync), TimeSpan.FromSeconds(10.0));

src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ async Task innerCallback(ExchangeOrderBook newOrderBook)
203203
public static async Task<Dictionary<string, ExchangeMarket>> GetExchangeMarketDictionaryFromCacheAsync(this ExchangeAPI api)
204204
{
205205
await new SynchronizationContextRemover();
206-
CachedItem<Dictionary<string, ExchangeMarket>> cacheResult = await api.Cache.Get<Dictionary<string, ExchangeMarket>>(nameof(GetExchangeMarketDictionaryFromCacheAsync), async () =>
206+
CachedItem<Dictionary<string, ExchangeMarket>> cacheResult = await api.Cache.GetOrCreate<Dictionary<string, ExchangeMarket>>(nameof(GetExchangeMarketDictionaryFromCacheAsync), async () =>
207207
{
208208
try
209209
{

src/ExchangeSharp/Model/ExchangeTicker.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
MIT LICENSE
33
44
Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com
@@ -62,7 +62,7 @@ public sealed class ExchangeTicker
6262
/// <returns>String</returns>
6363
public override string ToString()
6464
{
65-
return string.Format("Bid: {0}, Ask: {1}, Last: {2}", Bid, Ask, Last);
65+
return string.Format("Bid: {0}, Ask: {1}, Last: {2}, Vol: {3}", Bid, Ask, Last, Volume);
6666
}
6767

6868
/// <summary>
@@ -125,11 +125,17 @@ public sealed class ExchangeVolume
125125
/// </summary>
126126
public decimal BaseCurrencyVolume { get; set; }
127127

128-
/// <summary>
129-
/// Write to a binary writer
130-
/// </summary>
131-
/// <param name="writer">Binary writer</param>
132-
public void ToBinary(BinaryWriter writer)
128+
/// <inheritdoc />
129+
public override string ToString()
130+
{
131+
return $"{BaseCurrencyVolume:0.0000}/{QuoteCurrencyVolume:0.0000}";
132+
}
133+
134+
/// <summary>
135+
/// Write to a binary writer
136+
/// </summary>
137+
/// <param name="writer">Binary writer</param>
138+
public void ToBinary(BinaryWriter writer)
133139
{
134140
writer.Write(Timestamp.ToUniversalTime().Ticks);
135141
writer.Write(QuoteCurrency);

src/ExchangeSharp/Utility/CryptoUtility.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1401,7 +1401,7 @@ public static async Task<T> CacheMethod<T>(this ICache cache, Dictionary<string,
14011401
}
14021402
if (methodCachePolicy.TryGetValue(methodName, out TimeSpan cacheTime))
14031403
{
1404-
return (await cache.Get<T>(cacheKey, async () =>
1404+
return (await cache.GetOrCreate<T>(cacheKey, async () =>
14051405
{
14061406
T innerResult = await method();
14071407
return new CachedItem<T>(innerResult, CryptoUtility.UtcNow.Add(cacheTime));

src/ExchangeSharp/Utility/MemoryCache.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
MIT LICENSE
33
44
Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com
@@ -64,8 +64,8 @@ public interface ICache : IDisposable
6464
/// <typeparam name="T">Type to read</typeparam>
6565
/// <param name="key">Key</param>
6666
/// <param name="value">Value</param>
67-
/// <param name="notFound">Create T if not found, null to not do this. Item1 = value, Item2 = expiration.</param>
68-
Task<CachedItem<T>> Get<T>(string key, Func<Task<CachedItem<T>>> notFound) where T : class;
67+
/// <param name="factory">Create T if not found, null to not do this. Item1 = value, Item2 = expiration.</param>
68+
Task<CachedItem<T>> GetOrCreate<T>(string key, Func<Task<CachedItem<T>>> factory) where T : class;
6969

7070
/// <summary>
7171
/// Remove a key from the cache immediately
@@ -176,7 +176,7 @@ public void Dispose()
176176
/// <param name="key">Key</param>
177177
/// <param name="value">Value</param>
178178
/// <param name="notFound">Create T if not found, null to not do this. Item1 = value, Item2 = expiration.</param>
179-
public async Task<CachedItem<T>> Get<T>(string key, Func<Task<CachedItem<T>>> notFound) where T : class
179+
public async Task<CachedItem<T>> GetOrCreate<T>(string key, Func<Task<CachedItem<T>>> notFound) where T : class
180180
{
181181
CachedItem<T> newItem = default;
182182
cacheTimerLock.EnterReadLock();

0 commit comments

Comments
 (0)