Skip to content

Commit 02b009d

Browse files
nh43devslee
authored andcommitted
fix Base/Quote currency ordering in global symbol conversion (#515)
* Test and fix base ExhchangeAPI symbol functionality * Fix bitthumb and Kraken APIs
1 parent 04eef85 commit 02b009d

File tree

5 files changed

+116
-88
lines changed

5 files changed

+116
-88
lines changed

src/ExchangeSharp/API/Exchanges/Bithumb/ExchangeBithumbAPI.cs

Lines changed: 5 additions & 3 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
@@ -40,12 +40,14 @@ public override string NormalizeMarketSymbol(string marketSymbol)
4040

4141
public override Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol)
4242
{
43-
return Task.FromResult("KRW" + GlobalMarketSymbolSeparator + marketSymbol);
43+
return Task.FromResult(marketSymbol + GlobalMarketSymbolSeparator + "KRW"); //e.g. 1 btc worth 9.7m KRW
4444
}
4545

4646
public override Task<string> GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol)
4747
{
48-
return Task.FromResult(marketSymbol.Substring(marketSymbol.IndexOf(GlobalMarketSymbolSeparator) + 1));
48+
var values = marketSymbol.Split(GlobalMarketSymbolSeparator); //for Bitthumb, e.g. "BTC-KRW", 1 btc worth about 9.7m won. Market symbol is BTC.
49+
50+
return Task.FromResult(values[0]);
4951
}
5052

5153
private string StatusToError(string status)

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,6 @@ public override async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync
131131
{
132132
quoteCurrencyNormalized = quoteCurrency;
133133
}
134-
if (quoteCurrencyNormalized == "BTC")
135-
{
136-
// prefer BTC in front
137-
return quoteCurrencyNormalized + GlobalMarketSymbolSeparatorString + baseCurrencyNormalized;
138-
}
139134
return baseCurrencyNormalized + GlobalMarketSymbolSeparatorString + quoteCurrencyNormalized;
140135
}
141136

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

Lines changed: 84 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -155,36 +155,46 @@ protected async Task<decimal> ClampOrderQuantity(string marketSymbol, decimal ou
155155
return market == null ? outputQuantity : CryptoUtility.ClampDecimal(market.MinTradeSize, market.MaxTradeSize, market.QuantityStepSize, outputQuantity);
156156
}
157157

158-
/// <summary>
159-
/// Convert an exchange symbol into a global symbol, which will be the same for all exchanges.
160-
/// Global symbols are always uppercase and separate the currency pair with a hyphen (-).
161-
/// Global symbols list the base currency first (i.e. BTC) and conversion currency
162-
/// second (i.e. ETH). Example BTC-ETH, read as x BTC is worth y ETH.
163-
/// BTC is always first, then ETH, etc. Fiat pair is always first in global symbol too.
164-
/// </summary>
165-
/// <param name="marketSymbol">Exchange market symbol</param>
166-
/// <param name="separator">Separator</param>
167-
/// <returns>Global symbol</returns>
168-
protected async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync(string marketSymbol, char separator = GlobalMarketSymbolSeparator)
169-
{
170-
if (string.IsNullOrEmpty(marketSymbol))
171-
{
172-
throw new ArgumentException("Symbol must be non null and non empty");
173-
}
174-
string[] pieces = marketSymbol.Split(separator);
175-
if (MarketSymbolIsReversed)
176-
{
177-
return (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[0])).ToUpperInvariant() + GlobalMarketSymbolSeparator + (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[1])).ToUpperInvariant();
178-
}
179-
return (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[1])).ToUpperInvariant() + GlobalMarketSymbolSeparator + (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[0])).ToUpperInvariant();
180-
}
158+
/// <summary>
159+
/// Convert an exchange symbol into a global symbol, which will be the same for all exchanges.
160+
/// Global symbols are always uppercase and separate the currency pair with a hyphen (-).
161+
/// Global symbols list the base currency first (i.e. BTC) and quote/conversion currency
162+
/// second (i.e. USD). Global symbols are of the form BASE-QUOTE. BASE-QUOTE is read as
163+
/// 1 BASE is worth y QUOTE.
164+
///
165+
/// Examples:
166+
/// On 1/25/2020,
167+
/// - BTC-USD: $8,371; 1 BTC (base) is worth $8,371 USD (quote)
168+
/// - ETH-BTC: 0.01934; 1 ETH is worth 0.01934 BTC
169+
/// - EUR-USD: 1.2; 1 EUR worth 1.2 USD
170+
///
171+
/// A value greater than 1 means one unit of base currency is more valuable than one unit of
172+
/// quote currency.
173+
///
174+
/// </summary>
175+
/// <param name="marketSymbol">Exchange market symbol</param>
176+
/// <param name="separator">Separator</param>
177+
/// <returns>Global symbol</returns>
178+
protected async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync(string marketSymbol, char separator = GlobalMarketSymbolSeparator)
179+
{
180+
if (string.IsNullOrEmpty(marketSymbol))
181+
{
182+
throw new ArgumentException("Symbol must be non null and non empty");
183+
}
184+
string[] pieces = marketSymbol.Split(separator);
185+
if (MarketSymbolIsReversed == false) //if reversed then put quote currency first
186+
{
187+
return (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[0])).ToUpperInvariant() + GlobalMarketSymbolSeparator + (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[1])).ToUpperInvariant();
188+
}
189+
return (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[1])).ToUpperInvariant() + GlobalMarketSymbolSeparator + (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[0])).ToUpperInvariant();
190+
}
181191

182-
/// <summary>
183-
/// Split a market symbol into currencies. For weird exchanges like Bitthumb, they can override and hard-code the other pair
184-
/// </summary>
185-
/// <param name="marketSymbol">Market symbol</param>
186-
/// <returns>Base and quote currency</returns>
187-
protected virtual (string baseCurrency, string quoteCurrency) OnSplitMarketSymbolToCurrencies(string marketSymbol)
192+
/// <summary>
193+
/// Split a market symbol into currencies. For weird exchanges like Bitthumb, they can override and hard-code the other pair
194+
/// </summary>
195+
/// <param name="marketSymbol">Market symbol</param>
196+
/// <returns>Base and quote currency</returns>
197+
protected virtual (string baseCurrency, string quoteCurrency) OnSplitMarketSymbolToCurrencies(string marketSymbol)
188198
{
189199
var pieces = marketSymbol.Split(MarketSymbolSeparator[0]);
190200
if (pieces.Length < 2)
@@ -348,32 +358,32 @@ public static IExchangeAPI[] GetExchangeAPIs()
348358
}
349359
}
350360

351-
/// <summary>
352-
/// Convert an exchange currency to a global currency. For example, on Binance,
353-
/// BCH (Bitcoin Cash) is BCC but in most other exchanges it is BCH, hence
354-
/// the global symbol is BCH.
355-
/// </summary>
356-
/// <param name="currency">Exchange currency</param>
357-
/// <returns>Global currency</returns>
358-
public Task<string> ExchangeCurrencyToGlobalCurrencyAsync(string currency)
359-
{
360-
currency = (currency ?? string.Empty);
361-
foreach (KeyValuePair<string, string> kv in ExchangeGlobalCurrencyReplacements[GetType()])
362-
{
363-
currency = currency.Replace(kv.Key, kv.Value);
364-
}
365-
return Task.FromResult(currency.ToUpperInvariant());
366-
}
361+
/// <summary>
362+
/// Convert an exchange currency to a global currency. For example, on Binance,
363+
/// BCH (Bitcoin Cash) is BCC but in most other exchanges it is BCH, hence
364+
/// the global symbol is BCH.
365+
/// </summary>
366+
/// <param name="currency">Exchange currency</param>
367+
/// <returns>Global currency</returns>
368+
public Task<string> ExchangeCurrencyToGlobalCurrencyAsync(string currency)
369+
{
370+
currency = (currency ?? string.Empty);
371+
foreach (KeyValuePair<string, string> kv in ExchangeGlobalCurrencyReplacements[GetType()])
372+
{
373+
currency = currency.Replace(kv.Key, kv.Value);
374+
}
375+
return Task.FromResult(currency.ToUpperInvariant());
376+
}
367377

368-
/// <summary>
369-
/// Convert a global currency to exchange currency. For example, on Binance,
370-
/// BCH (Bitcoin Cash) is BCC but in most other exchanges it is BCH, hence
371-
/// the global symbol BCH would convert to BCC for Binance, but stay BCH
372-
/// for most other exchanges.
373-
/// </summary>
374-
/// <param name="currency">Global currency</param>
375-
/// <returns>Exchange currency</returns>
376-
public string GlobalCurrencyToExchangeCurrency(string currency)
378+
/// <summary>
379+
/// Convert a global currency to exchange currency. For example, on Binance,
380+
/// BCH (Bitcoin Cash) is BCC but in most other exchanges it is BCH, hence
381+
/// the global symbol BCH would convert to BCC for Binance, but stay BCH
382+
/// for most other exchanges.
383+
/// </summary>
384+
/// <param name="currency">Global currency</param>
385+
/// <returns>Exchange currency</returns>
386+
public string GlobalCurrencyToExchangeCurrency(string currency)
377387
{
378388
currency = (currency ?? string.Empty);
379389
foreach (KeyValuePair<string, string> kv in ExchangeGlobalCurrencyReplacements[GetType()])
@@ -404,16 +414,25 @@ public virtual string NormalizeMarketSymbol(string? marketSymbol)
404414
return marketSymbol.ToLowerInvariant();
405415
}
406416

407-
/// <summary>
408-
/// Convert an exchange symbol into a global symbol, which will be the same for all exchanges.
409-
/// Global symbols are always uppercase and separate the currency pair with a hyphen (-).
410-
/// Global symbols list the base currency first (i.e. BTC) and conversion currency
411-
/// second (i.e. ETH). Example BTC-ETH, read as x BTC is worth y ETH.
412-
/// BTC is always first, then ETH, etc. Fiat pair is always first in global symbol too.
413-
/// </summary>
414-
/// <param name="marketSymbol">Exchange symbol</param>
415-
/// <returns>Global symbol</returns>
416-
public virtual async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol)
417+
/// <summary>
418+
/// Convert an exchange symbol into a global symbol, which will be the same for all exchanges.
419+
/// Global symbols are always uppercase and separate the currency pair with a hyphen (-).
420+
/// Global symbols list the base currency first (i.e. BTC) and quote/conversion currency
421+
/// second (i.e. USD). Global symbols are of the form BASE-QUOTE. BASE-QUOTE is read as
422+
/// 1 BASE is worth y QUOTE.
423+
///
424+
/// Examples:
425+
/// On 1/25/2020,
426+
/// - BTC-USD: $8,371; 1 BTC (base) is worth $8,371 USD (quote)
427+
/// - ETH-BTC: 0.01934; 1 ETH is worth 0.01934 BTC
428+
/// - EUR-USD: 1.2; 1 EUR worth 1.2 USD
429+
///
430+
/// A value greater than 1 means one unit of base currency is more valuable than one unit of
431+
/// quote currency.
432+
/// </summary>
433+
/// <param name="marketSymbol">Exchange symbol</param>
434+
/// <returns>Global symbol</returns>
435+
public virtual async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol)
417436
{
418437
string modifiedMarketSymbol = marketSymbol;
419438
char separator;
@@ -490,7 +509,7 @@ public virtual Task<string> GlobalMarketSymbolToExchangeMarketSymbolAsync(string
490509
{
491510
throw new ArgumentException($"Market symbol {marketSymbol} is missing the global symbol separator '{GlobalMarketSymbolSeparator}'");
492511
}
493-
if (MarketSymbolIsReversed)
512+
if (MarketSymbolIsReversed == false)
494513
{
495514
marketSymbol = GlobalCurrencyToExchangeCurrency(marketSymbol.Substring(0, pos)) + MarketSymbolSeparator + GlobalCurrencyToExchangeCurrency(marketSymbol.Substring(pos + 1));
496515
}

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,32 @@ public interface IExchangeAPI : IDisposable, IBaseAPI, IOrderBookProvider
2525
#region Utility Methods
2626

2727
/// <summary>
28-
/// Normalize a symbol for use on this exchange
28+
/// Normalize a symbol for use on this exchange.
2929
/// </summary>
3030
/// <param name="marketSymbol">Symbol</param>
3131
/// <returns>Normalized symbol</returns>
3232
string NormalizeMarketSymbol(string marketSymbol);
3333

34-
/// <summary>
35-
/// Convert an exchange symbol into a global symbol, which will be the same for all exchanges.
36-
/// Global symbols are always uppercase and separate the currency pair with a hyphen (-).
37-
/// Global symbols list the base currency first (i.e. BTC) and conversion currency
38-
/// second (i.e. USD). Example BTC-USD, read as x BTC is worth y USD.
39-
/// </summary>
40-
/// <param name="marketSymbol">Exchange symbol</param>
41-
/// <returns>Global symbol</returns>
42-
Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol);
34+
/// <summary>
35+
/// Convert an exchange symbol into a global symbol, which will be the same for all exchanges.
36+
/// Global symbols are always uppercase and separate the currency pair with a hyphen (-).
37+
/// Global symbols list the base currency first (i.e. BTC) and quote/conversion currency
38+
/// second (i.e. USD). Global symbols are of the form BASE-QUOTE. BASE-QUOTE is read as
39+
/// 1 BASE is worth y QUOTE.
40+
///
41+
/// Examples:
42+
/// On 1/25/2020,
43+
/// - BTC-USD: $8,371; 1 BTC (base) is worth $8,371 USD (quote)
44+
/// - ETH-BTC: 0.01934; 1 ETH is worth 0.01934 BTC
45+
/// - EUR-USD: 1.2; 1 EUR worth 1.2 USD
46+
///
47+
/// A value greater than 1 means one unit of base currency is more valuable than one unit of
48+
/// quote currency.
49+
///
50+
/// </summary>
51+
/// <param name="marketSymbol">Exchange symbol</param>
52+
/// <returns>Global symbol</returns>
53+
Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol);
4354

4455
/// <summary>
4556
/// Convert a global symbol into an exchange symbol, which will potentially be different from other exchanges.

tests/ExchangeSharpTests/ExchangeTests.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ public async Task GlobalSymbolTest()
6868
// if tests fail, uncomment this and it will save a new test file
6969
// string allSymbolsJson = GetAllSymbolsJsonAsync().Sync(); System.IO.File.WriteAllText("TestData/AllSymbols.json", allSymbolsJson);
7070

71-
string globalMarketSymbol = "BTC-ETH";
72-
string globalMarketSymbolAlt = "KRW-BTC"; // WTF Bitthumb...
71+
string globalMarketSymbol = "ETH-BTC"; //1 ETH is worth 0.0192 BTC...
72+
string globalMarketSymbolAlt = "BTC-KRW"; // WTF Bitthumb... //1 BTC worth 9,783,000 won
7373
Dictionary<string, string[]> allSymbols = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(System.IO.File.ReadAllText("TestData/AllSymbols.json"));
7474

7575
// sanity test that all exchanges return the same global symbol when converted back and forth
@@ -88,9 +88,10 @@ api is ExchangeOKCoinAPI || api is ExchangeDigifinexAPI || api is ExchangeNDAXAP
8888
bool isBithumb = (api.Name == ExchangeName.Bithumb);
8989
string exchangeMarketSymbol = await api.GlobalMarketSymbolToExchangeMarketSymbolAsync(isBithumb ? globalMarketSymbolAlt : globalMarketSymbol);
9090
string globalMarketSymbol2 = await api.ExchangeMarketSymbolToGlobalMarketSymbolAsync(exchangeMarketSymbol);
91-
if ((!isBithumb && globalMarketSymbol2.EndsWith("-BTC")) ||
92-
globalMarketSymbol2.EndsWith("-USD") ||
93-
globalMarketSymbol2.EndsWith("-USDT"))
91+
92+
if ((!isBithumb && globalMarketSymbol2.StartsWith("BTC-")) ||
93+
globalMarketSymbol2.StartsWith("USD-") ||
94+
globalMarketSymbol2.StartsWith("USDT-"))
9495
{
9596
Assert.Fail($"Exchange {api.Name} has wrong SymbolIsReversed parameter");
9697
}

0 commit comments

Comments
 (0)