Skip to content

Commit 44adc0c

Browse files
authored
Kraken symbol refactor (#449)
* Start work on kraken symbol overhaul * Fix Kraken exchange to global market symbol * Kraken global to market conversion working, except for .d pairs * Simplify Kraken caching * Better name * Remove redundant code
1 parent 133fdde commit 44adc0c

File tree

3 files changed

+115
-160
lines changed

3 files changed

+115
-160
lines changed

ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs

Lines changed: 110 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,6 @@ public sealed partial class ExchangeKrakenAPI : ExchangeAPI
2727
{
2828
public override string BaseUrl { get; set; } = "https://api.kraken.com";
2929
public override string BaseUrlWebSocket { get; set; } = "wss://ws.kraken.com";
30-
static ExchangeKrakenAPI()
31-
{
32-
Dictionary<string, string> d = normalizedSymbolToExchangeSymbol as Dictionary<string, string>;
33-
foreach (KeyValuePair<string, string> kv in exchangeSymbolToNormalizedSymbol)
34-
{
35-
if (!d.ContainsKey(kv.Value))
36-
{
37-
d.Add(kv.Value, kv.Key);
38-
}
39-
}
40-
}
4130

4231
public ExchangeKrakenAPI()
4332
{
@@ -47,164 +36,126 @@ public ExchangeKrakenAPI()
4736
NonceStyle = NonceStyle.UnixMilliseconds;
4837
}
4938

50-
public override async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol)
39+
private IReadOnlyDictionary<string, string> exchangeCurrencyToNormalizedCurrency = new Dictionary<string, string>();
40+
private IReadOnlyDictionary<string, string> normalizedCurrencyToExchangeCurrency = new Dictionary<string, string>();
41+
private IReadOnlyDictionary<string, string> exchangeSymbolToNormalizedSymbol = new Dictionary<string, string>();
42+
private IReadOnlyDictionary<string, string> normalizedSymbolToExchangeSymbol = new Dictionary<string, string>();
43+
private IReadOnlyDictionary<string, string> exchangeCurrenciesToMarketSymbol = new Dictionary<string, string>();
44+
45+
static ExchangeKrakenAPI()
5146
{
52-
if (exchangeSymbolToNormalizedSymbol.TryGetValue(marketSymbol, out string normalizedSymbol))
47+
ExchangeGlobalCurrencyReplacements[typeof(ExchangeKrakenAPI)] = new KeyValuePair<string, string>[]
5348
{
54-
int pos;
55-
if (marketSymbol.StartsWith("WAVES"))
49+
new KeyValuePair<string, string>("XBT", "BTC"),
50+
new KeyValuePair<string, string>("XDG", "DOGE")
51+
};
52+
}
53+
54+
/// <summary>
55+
/// Populate dictionaries to deal with Kraken weirdness in currency and market names, will use cache if it exists
56+
/// </summary>
57+
/// <returns>Task</returns>
58+
private async Task PopulateLookupTables()
59+
{
60+
await Cache.Get<object>(nameof(PopulateLookupTables), async () =>
61+
{
62+
IReadOnlyDictionary<string, ExchangeCurrency> currencies = await GetCurrenciesAsync();
63+
ExchangeMarket[] markets = (await GetMarketSymbolsMetadataAsync())?.ToArray();
64+
if (markets == null || markets.Length == 0)
5665
{
57-
pos = 5;
66+
return new CachedItem<object>();
5867
}
59-
else
68+
69+
Dictionary<string, string> exchangeCurrencyToNormalizedCurrencyNew = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
70+
Dictionary<string, string> normalizedCurrencyToExchangeCurrencyNew = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
71+
Dictionary<string, string> exchangeSymbolToNormalizedSymbolNew = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
72+
Dictionary<string, string> normalizedSymbolToExchangeSymbolNew = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
73+
Dictionary<string, string> exchangeCurrenciesToMarketSymbolNew = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
74+
75+
foreach (KeyValuePair<string, ExchangeCurrency> kv in currencies)
6076
{
61-
pos = (normalizedSymbol.Length == 6 ? 3 : (normalizedSymbol.Length == 7 ? 4 : throw new InvalidOperationException("Cannot normalize symbol " + normalizedSymbol)));
77+
string altName = kv.Value.AltName;
78+
switch (altName.ToLowerInvariant())
79+
{
80+
// wtf kraken...
81+
case "xbt": altName = "BTC"; break;
82+
case "xdg": altName = "DOGE"; break;
83+
}
84+
exchangeCurrencyToNormalizedCurrencyNew[kv.Value.Name] = altName;
85+
normalizedCurrencyToExchangeCurrencyNew[altName] = kv.Value.Name;
6286
}
63-
return await base.ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync(normalizedSymbol.Substring(0, pos) + GlobalMarketSymbolSeparator + normalizedSymbol.Substring(pos), GlobalMarketSymbolSeparator);
64-
}
6587

66-
# region if the initial fails, we try to use another method
67-
var symbols = (await GetMarketSymbolsMetadataAsync()).ToList();
68-
var symbol = symbols.FirstOrDefault(a => a.MarketSymbol.Replace("/", "").Equals(marketSymbol));
69-
string _marketSymbol = symbol.BaseCurrency + symbol.QuoteCurrency;
70-
if (exchangeSymbolToNormalizedSymbol.TryGetValue(_marketSymbol, out normalizedSymbol))
71-
{
72-
int pos = (normalizedSymbol.Length == 6 ? 3 : (normalizedSymbol.Length == 7 ? 4 : throw new InvalidOperationException("Cannot normalize symbol " + normalizedSymbol)));
73-
return await base.ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync(normalizedSymbol.Substring(0, pos) + GlobalMarketSymbolSeparator + normalizedSymbol.Substring(pos), GlobalMarketSymbolSeparator);
74-
}
75-
#endregion
88+
foreach (ExchangeMarket market in markets.Where(m => !m.MarketSymbol.Contains(".d")))
89+
{
90+
string baseSymbol = market.BaseCurrency;
91+
string quoteSymbol = market.QuoteCurrency;
92+
string baseNorm = exchangeCurrencyToNormalizedCurrencyNew[market.BaseCurrency];
93+
string quoteNorm = exchangeCurrencyToNormalizedCurrencyNew[market.QuoteCurrency];
94+
string marketSymbolNorm = baseNorm + quoteNorm;
95+
string marketSymbol = market.MarketSymbol;
96+
exchangeSymbolToNormalizedSymbolNew[marketSymbol] = marketSymbolNorm;
97+
normalizedSymbolToExchangeSymbolNew[marketSymbolNorm] = marketSymbol;
98+
exchangeCurrenciesToMarketSymbolNew[baseSymbol + quoteSymbol] = marketSymbol;
99+
}
76100

77-
throw new ArgumentException($"Symbol {marketSymbol} not found in Kraken lookup table");
101+
exchangeCurrencyToNormalizedCurrency = exchangeCurrencyToNormalizedCurrencyNew;
102+
normalizedCurrencyToExchangeCurrency = normalizedCurrencyToExchangeCurrencyNew;
103+
exchangeSymbolToNormalizedSymbol = exchangeSymbolToNormalizedSymbolNew;
104+
normalizedSymbolToExchangeSymbol = normalizedSymbolToExchangeSymbolNew;
105+
exchangeCurrenciesToMarketSymbol = exchangeCurrenciesToMarketSymbolNew;
106+
107+
return new CachedItem<object>(new object(), CryptoUtility.UtcNow.AddHours(4.0));
108+
});
78109
}
79110

80-
public override Task<string> GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol)
111+
public override async Task<(string baseCurrency, string quoteCurrency)> ExchangeMarketSymbolToCurrenciesAsync(string marketSymbol)
81112
{
82-
if (normalizedSymbolToExchangeSymbol.TryGetValue(marketSymbol.Replace(GlobalMarketSymbolSeparator.ToString(), string.Empty), out string exchangeSymbol))
113+
ExchangeMarket market = await GetExchangeMarketFromCacheAsync(marketSymbol);
114+
if (market == null)
83115
{
84-
return Task.FromResult(exchangeSymbol);
116+
throw new ArgumentException("Unable to get currencies for market symbol " + marketSymbol);
85117
}
118+
return (market.BaseCurrency, market.QuoteCurrency);
119+
}
86120

87-
// not found, reverse the pair
88-
int idx = marketSymbol.IndexOf(GlobalMarketSymbolSeparator);
89-
marketSymbol = marketSymbol.Substring(idx + 1) + marketSymbol.Substring(0, idx);
90-
if (normalizedSymbolToExchangeSymbol.TryGetValue(marketSymbol.Replace(GlobalMarketSymbolSeparator.ToString(), string.Empty), out exchangeSymbol))
121+
public override async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol)
122+
{
123+
await PopulateLookupTables();
124+
var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(marketSymbol);
125+
if (!exchangeCurrencyToNormalizedCurrency.TryGetValue(baseCurrency, out string baseCurrencyNormalized))
91126
{
92-
return Task.FromResult(exchangeSymbol);
127+
baseCurrencyNormalized = baseCurrency;
93128
}
94-
95-
throw new ArgumentException($"Symbol {marketSymbol} not found in Kraken lookup table");
129+
if (!exchangeCurrencyToNormalizedCurrency.TryGetValue(quoteCurrency, out string quoteCurrencyNormalized))
130+
{
131+
quoteCurrencyNormalized = quoteCurrency;
132+
}
133+
return baseCurrencyNormalized + GlobalMarketSymbolSeparatorString + quoteCurrencyNormalized;
96134
}
97135

98-
/// <summary>
99-
/// Change Kraken symbols to more common sense symbols
100-
/// </summary>
101-
private static readonly IReadOnlyDictionary<string, string> exchangeSymbolToNormalizedSymbol = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
136+
public override async Task<string> GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol)
102137
{
103-
{ "ADACAD" , "adacad" },
104-
{ "ADAETH" , "adaeth" },
105-
{ "ADAEUR" , "adaeur" },
106-
{ "ADAUSD" , "adausd" },
107-
{ "ADAXBT" , "adabtc" },
108-
{ "ATOMCAD", "atomcad" },
109-
{ "ATOMETH", "atometh" },
110-
{ "ATOMEUR", "atomeur" },
111-
{ "ATOMUSD", "atomusd" },
112-
{ "ATOMXBT", "atombtc" },
113-
{ "BATETH", "bateth" },
114-
{ "BATEUR", "bateur" },
115-
{ "BATUSD", "batusd" },
116-
{ "BATXBT", "batbtc" },
117-
{ "BCHEUR" , "bcheur" },
118-
{ "BCHUSD" , "bchusd" },
119-
{ "BCHXBT" , "bchbtc" },
120-
{ "BSVEUR" , "bsveur" },
121-
{ "BSVUSD" , "bsvusd" },
122-
{ "BSVXBT" , "bsvbtc" },
123-
{ "DASHEUR" , "dasheur" },
124-
{ "DASHUSD" , "dashusd" },
125-
{ "DASHXBT" , "dashbtc" },
126-
{ "EOSETH" , "eoseth" },
127-
{ "EOSEUR" , "eoseur" },
128-
{ "EOSUSD" , "eosusd" },
129-
{ "EOSXBT" , "eosbtc" },
130-
{ "ETCETH" , "etceth" },
131-
{ "ETCEUR" , "etceur" },
132-
{ "ETCUSD" , "etcusd" },
133-
{ "ETCXBT" , "etcbtc" },
134-
{ "ETHCAD" , "ethcad" },
135-
{ "GNOETH" , "gnoeth" },
136-
{ "GNOEUR" , "gnoeur" },
137-
{ "GNOUSD" , "gnousd" },
138-
{ "GNOXBT" , "gnobtc" },
139-
{ "QTUMCAD" , "qtumcad" },
140-
{ "QTUMETH" , "qtumeth" },
141-
{ "QTUMEUR" , "qtumeur" },
142-
{ "QTUMUSD" , "qtumusd" },
143-
{ "QTUMXBT" , "qtumbtc" },
144-
{ "USDTZUSD" , "usdtusd" },
145-
{ "WAVESETH" , "waveseth" },
146-
{ "WAVESEUR" , "waveseur" },
147-
{ "WAVESUSD" , "wavesusd" },
148-
{ "WAVESXBT" , "wavesbtc" },
149-
{ "XETCXETH" , "etceth" },
150-
{ "XETCXXBT" , "etcbtc" },
151-
{ "XETCZEUR" , "etceur" },
152-
{ "XETCZUSD" , "etcusd" },
153-
{ "XETHXXBT" , "ethbtc" },
154-
{ "XETHXXBT.d" , "ethbtcd" },
155-
{ "XETHZCAD" , "ethcad" },
156-
{ "XETHZCAD.d" , "ethcadd" },
157-
{ "XETHZEUR" , "etheur" },
158-
{ "XETHZEUR.d" , "etheurd" },
159-
{ "XETHZGBP" , "ethgbp" },
160-
{ "XETHZGBP.d" , "ethgbpd" },
161-
{ "XETHZJPY" , "ethjpy" },
162-
{ "XETHZJPY.d" , "ethjpyd" },
163-
{ "XETHZUSD" , "ethusd" },
164-
{ "XETHZUSD.d" , "ethusdd" },
165-
{ "XLTCXXBT" , "ltcbtc" },
166-
{ "XLTCZEUR" , "ltceur" },
167-
{ "XLTCZUSD" , "ltcusd" },
168-
{ "XMLNXETH" , "mlneth" },
169-
{ "XMLNXXBT" , "mlnbtc" },
170-
{ "XREPXETH" , "repeth" },
171-
{ "XREPXXBT" , "repbtc" },
172-
{ "XREPZEUR" , "repeur" },
173-
{ "XREPZUSD" , "repusd" },
174-
{ "XTZCAD" , "xtzcad" },
175-
{ "XTZETH" , "xtzeth" },
176-
{ "XTZEUR" , "xtzeur" },
177-
{ "XTZUSD" , "xtzusd" },
178-
{ "XTZXBT" , "xtzbtc" },
179-
{ "XXBTZCAD" , "btccad" },
180-
{ "XXBTZCAD.d" , "btccadd" },
181-
{ "XXBTZEUR" , "btceur" },
182-
{ "XXBTZEUR.d" , "btceurd" },
183-
{ "XXBTZGBP" , "btcgbp" },
184-
{ "XXBTZGBP.d" , "btcgbpd" },
185-
{ "XXBTZJPY" , "btcjpy" },
186-
{ "XXBTZJPY.d" , "btcjpyd" },
187-
{ "XXBTZUSD" , "btcusd" },
188-
{ "XXBTZUSD.d" , "btcusdd" },
189-
{ "XXDGXXBT" , "xdgbtc" },
190-
{ "XXLMXXBT" , "xlmbtc" },
191-
{ "XXLMZEUR" , "xlmeur" },
192-
{ "XXLMZUSD" , "xlmusd" },
193-
{ "XXMRXXBT" , "xmrbtc" },
194-
{ "XXMRZEUR" , "xmreur" },
195-
{ "XXMRZUSD" , "xmrusd" },
196-
{ "XXRPXXBT" , "xrpbtc" },
197-
{ "XXRPZCAD" , "xrpcad" },
198-
{ "XXRPZEUR" , "xrpeur" },
199-
{ "XXRPZJPY" , "xrpjpy" },
200-
{ "XXRPZUSD" , "xrpusd" },
201-
{ "XZECXXBT" , "zecbtc" },
202-
{ "XZECZEUR" , "zeceur" },
203-
{ "XZECZJPY" , "zecjpy" },
204-
{ "XZECZUSD" , "zecusd" }
205-
};
206-
207-
private static readonly IReadOnlyDictionary<string, string> normalizedSymbolToExchangeSymbol = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
138+
await PopulateLookupTables();
139+
string[] pieces = marketSymbol.Split('-');
140+
if (pieces.Length < 2)
141+
{
142+
throw new ArgumentException("Market symbol must be at least two pieces");
143+
}
144+
string marketSymbol2 = pieces[0] + pieces[1];
145+
if (!normalizedCurrencyToExchangeCurrency.TryGetValue(pieces[0], out string baseCurrencyExchange))
146+
{
147+
baseCurrencyExchange = pieces[0];
148+
}
149+
if (!normalizedCurrencyToExchangeCurrency.TryGetValue(pieces[1], out string quoteCurrencyExchange))
150+
{
151+
quoteCurrencyExchange = pieces[1];
152+
}
153+
if (!exchangeCurrenciesToMarketSymbol.TryGetValue(baseCurrencyExchange + quoteCurrencyExchange, out string exchangeMarketSymbol))
154+
{
155+
throw new ArgumentException("Unable to find exchange market for global market symbol " + marketSymbol);
156+
}
157+
return exchangeMarketSymbol;
158+
}
208159

209160
protected override JToken CheckJsonResponse(JToken json)
210161
{
@@ -280,6 +231,7 @@ private async Task<ExchangeOrderResult> ParseHistoryOrder(string orderId, JToken
280231

281232
private async Task<IEnumerable<ExchangeOrderResult>> QueryOrdersAsync(string symbol, string path)
282233
{
234+
await PopulateLookupTables();
283235
List<ExchangeOrderResult> orders = new List<ExchangeOrderResult>();
284236
JToken result = await MakeJsonRequestAsync<JToken>(path, null, await GetNoncePayloadAsync());
285237
result = result["open"];
@@ -299,6 +251,7 @@ private async Task<IEnumerable<ExchangeOrderResult>> QueryOrdersAsync(string sym
299251

300252
private async Task<IEnumerable<ExchangeOrderResult>> QueryClosedOrdersAsync(string symbol, string path)
301253
{
254+
await PopulateLookupTables();
302255
List<ExchangeOrderResult> orders = new List<ExchangeOrderResult>();
303256
JToken result = await MakeJsonRequestAsync<JToken>(path, null, await GetNoncePayloadAsync());
304257
result = result["closed"];
@@ -325,6 +278,7 @@ private async Task<IEnumerable<ExchangeOrderResult>> QueryClosedOrdersAsync(stri
325278

326279
private async Task<IEnumerable<ExchangeOrderResult>> QueryHistoryOrdersAsync(string symbol, string path)
327280
{
281+
await PopulateLookupTables();
328282
List<ExchangeOrderResult> orders = new List<ExchangeOrderResult>();
329283
JToken result = await MakeJsonRequestAsync<JToken>(path, null, await GetNoncePayloadAsync());
330284
result = result["trades"];
@@ -421,7 +375,8 @@ protected override async Task<IReadOnlyDictionary<string, ExchangeCurrency>> OnG
421375
{
422376
CoinType = token.Value["aclass"].ToStringInvariant(),
423377
Name = token.Name,
424-
FullName = token.Value["altname"].ToStringInvariant()
378+
FullName = token.Name,
379+
AltName = token.Value["altname"].ToStringInvariant()
425380
};
426381

427382
currencies[coin.Name] = coin;
@@ -433,9 +388,7 @@ protected override async Task<IReadOnlyDictionary<string, ExchangeCurrency>> OnG
433388
protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
434389
{
435390
JToken result = await MakeJsonRequestAsync<JToken>("/0/public/AssetPairs");
436-
return (from prop in result.Children<JProperty>()
437-
where !prop.Name.Contains(".d")
438-
select prop.Value["wsname"].ToStringInvariant()).ToArray();
391+
return result.Children<JProperty>().Where(p => !p.Name.Contains(".d")).Select(p => p.Name).ToArray();
439392
}
440393

441394
protected override async Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMetadataAsync()
@@ -536,15 +489,14 @@ protected override async Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMet
536489
JToken allPairs = await MakeJsonRequestAsync<JToken>("/0/public/AssetPairs");
537490
var res = (from prop in allPairs.Children<JProperty>() select prop).ToArray();
538491

539-
foreach (JProperty prop in res)
492+
foreach (JProperty prop in res.Where(p => !p.Name.EndsWith(".d")))
540493
{
541494
JToken pair = prop.Value;
542495
var quantityStepSize = Math.Pow(0.1, pair["lot_decimals"].ConvertInvariant<int>()).ConvertInvariant<decimal>();
543496
var market = new ExchangeMarket
544497
{
545-
IsActive = !prop.Name.Contains(".d"),
546-
MarketSymbol = (pair["wsname"].ToStringInvariant() != "" ?
547-
pair["wsname"].ToStringInvariant() : pair["altname"].ToStringInvariant()).Replace("/", string.Empty),
498+
IsActive = true,
499+
MarketSymbol = prop.Name,
548500
MinTradeSize = quantityStepSize,
549501
MarginEnabled = pair["leverage_buy"].Children().Any() || pair["leverage_sell"].Children().Any(),
550502
BaseCurrency = pair["base"].ToStringInvariant(),

ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,10 +429,10 @@ public virtual async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(
429429
/// <param name="baseCurrency">Base currency</param>
430430
/// <param name="quoteCurrency">Quote currency</param>
431431
/// <returns>Exchange market symbol</returns>
432-
public virtual string CurrenciesToExchangeMarketSymbol(string baseCurrency, string quoteCurrency)
432+
public virtual Task<string> CurrenciesToExchangeMarketSymbol(string baseCurrency, string quoteCurrency)
433433
{
434434
string symbol = (MarketSymbolIsReversed ? $"{quoteCurrency}{MarketSymbolSeparator}{baseCurrency}" : $"{baseCurrency}{MarketSymbolSeparator}{quoteCurrency}");
435-
return (MarketSymbolIsUppercase ? symbol.ToUpperInvariant() : symbol);
435+
return Task.FromResult(MarketSymbolIsUppercase ? symbol.ToUpperInvariant() : symbol);
436436
}
437437

438438
/// <summary>

ExchangeSharp/Model/ExchangeCurrency.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public sealed class ExchangeCurrency
2020
/// <summary>Full name of the currency. Eg. Ethereum</summary>
2121
public string FullName { get; set; }
2222

23+
/// <summary>Alternate name, null if none</summary>
24+
public string AltName { get; set; }
25+
2326
/// <summary>The transaction fee.</summary>
2427
public decimal TxFee { get; set; }
2528

0 commit comments

Comments
 (0)