Skip to content

fix Base/Quote currency ordering in global symbol conversion #515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/ExchangeSharp/API/Exchanges/Bithumb/ExchangeBithumbAPI.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
MIT LICENSE

Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com
Expand Down Expand Up @@ -40,12 +40,14 @@ public override string NormalizeMarketSymbol(string marketSymbol)

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

public override Task<string> GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol)
{
return Task.FromResult(marketSymbol.Substring(marketSymbol.IndexOf(GlobalMarketSymbolSeparator) + 1));
var values = marketSymbol.Split(GlobalMarketSymbolSeparator); //for Bitthumb, e.g. "BTC-KRW", 1 btc worth about 9.7m won. Market symbol is BTC.

return Task.FromResult(values[0]);
}

private string StatusToError(string status)
Expand Down
5 changes: 0 additions & 5 deletions src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,6 @@ public override async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync
{
quoteCurrencyNormalized = quoteCurrency;
}
if (quoteCurrencyNormalized == "BTC")
{
// prefer BTC in front
return quoteCurrencyNormalized + GlobalMarketSymbolSeparatorString + baseCurrencyNormalized;
}
return baseCurrencyNormalized + GlobalMarketSymbolSeparatorString + quoteCurrencyNormalized;
}

Expand Down
149 changes: 84 additions & 65 deletions src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,36 +155,46 @@ protected async Task<decimal> ClampOrderQuantity(string marketSymbol, decimal ou
return market == null ? outputQuantity : CryptoUtility.ClampDecimal(market.MinTradeSize, market.MaxTradeSize, market.QuantityStepSize, outputQuantity);
}

/// <summary>
/// Convert an exchange symbol into a global symbol, which will be the same for all exchanges.
/// Global symbols are always uppercase and separate the currency pair with a hyphen (-).
/// Global symbols list the base currency first (i.e. BTC) and conversion currency
/// second (i.e. ETH). Example BTC-ETH, read as x BTC is worth y ETH.
/// BTC is always first, then ETH, etc. Fiat pair is always first in global symbol too.
/// </summary>
/// <param name="marketSymbol">Exchange market symbol</param>
/// <param name="separator">Separator</param>
/// <returns>Global symbol</returns>
protected async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync(string marketSymbol, char separator = GlobalMarketSymbolSeparator)
{
if (string.IsNullOrEmpty(marketSymbol))
{
throw new ArgumentException("Symbol must be non null and non empty");
}
string[] pieces = marketSymbol.Split(separator);
if (MarketSymbolIsReversed)
{
return (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[0])).ToUpperInvariant() + GlobalMarketSymbolSeparator + (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[1])).ToUpperInvariant();
}
return (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[1])).ToUpperInvariant() + GlobalMarketSymbolSeparator + (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[0])).ToUpperInvariant();
}
/// <summary>
/// Convert an exchange symbol into a global symbol, which will be the same for all exchanges.
/// Global symbols are always uppercase and separate the currency pair with a hyphen (-).
/// Global symbols list the base currency first (i.e. BTC) and quote/conversion currency
/// second (i.e. USD). Global symbols are of the form BASE-QUOTE. BASE-QUOTE is read as
/// 1 BASE is worth y QUOTE.
///
/// Examples:
/// On 1/25/2020,
/// - BTC-USD: $8,371; 1 BTC (base) is worth $8,371 USD (quote)
/// - ETH-BTC: 0.01934; 1 ETH is worth 0.01934 BTC
/// - EUR-USD: 1.2; 1 EUR worth 1.2 USD
///
/// A value greater than 1 means one unit of base currency is more valuable than one unit of
/// quote currency.
///
/// </summary>
/// <param name="marketSymbol">Exchange market symbol</param>
/// <param name="separator">Separator</param>
/// <returns>Global symbol</returns>
protected async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync(string marketSymbol, char separator = GlobalMarketSymbolSeparator)
{
if (string.IsNullOrEmpty(marketSymbol))
{
throw new ArgumentException("Symbol must be non null and non empty");
}
string[] pieces = marketSymbol.Split(separator);
if (MarketSymbolIsReversed == false) //if reversed then put quote currency first
{
return (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[0])).ToUpperInvariant() + GlobalMarketSymbolSeparator + (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[1])).ToUpperInvariant();
}
return (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[1])).ToUpperInvariant() + GlobalMarketSymbolSeparator + (await ExchangeCurrencyToGlobalCurrencyAsync(pieces[0])).ToUpperInvariant();
}

/// <summary>
/// Split a market symbol into currencies. For weird exchanges like Bitthumb, they can override and hard-code the other pair
/// </summary>
/// <param name="marketSymbol">Market symbol</param>
/// <returns>Base and quote currency</returns>
protected virtual (string baseCurrency, string quoteCurrency) OnSplitMarketSymbolToCurrencies(string marketSymbol)
/// <summary>
/// Split a market symbol into currencies. For weird exchanges like Bitthumb, they can override and hard-code the other pair
/// </summary>
/// <param name="marketSymbol">Market symbol</param>
/// <returns>Base and quote currency</returns>
protected virtual (string baseCurrency, string quoteCurrency) OnSplitMarketSymbolToCurrencies(string marketSymbol)
{
var pieces = marketSymbol.Split(MarketSymbolSeparator[0]);
if (pieces.Length < 2)
Expand Down Expand Up @@ -348,32 +358,32 @@ public static IExchangeAPI[] GetExchangeAPIs()
}
}

/// <summary>
/// Convert an exchange currency to a global currency. For example, on Binance,
/// BCH (Bitcoin Cash) is BCC but in most other exchanges it is BCH, hence
/// the global symbol is BCH.
/// </summary>
/// <param name="currency">Exchange currency</param>
/// <returns>Global currency</returns>
public Task<string> ExchangeCurrencyToGlobalCurrencyAsync(string currency)
{
currency = (currency ?? string.Empty);
foreach (KeyValuePair<string, string> kv in ExchangeGlobalCurrencyReplacements[GetType()])
{
currency = currency.Replace(kv.Key, kv.Value);
}
return Task.FromResult(currency.ToUpperInvariant());
}
/// <summary>
/// Convert an exchange currency to a global currency. For example, on Binance,
/// BCH (Bitcoin Cash) is BCC but in most other exchanges it is BCH, hence
/// the global symbol is BCH.
/// </summary>
/// <param name="currency">Exchange currency</param>
/// <returns>Global currency</returns>
public Task<string> ExchangeCurrencyToGlobalCurrencyAsync(string currency)
{
currency = (currency ?? string.Empty);
foreach (KeyValuePair<string, string> kv in ExchangeGlobalCurrencyReplacements[GetType()])
{
currency = currency.Replace(kv.Key, kv.Value);
}
return Task.FromResult(currency.ToUpperInvariant());
}

/// <summary>
/// Convert a global currency to exchange currency. For example, on Binance,
/// BCH (Bitcoin Cash) is BCC but in most other exchanges it is BCH, hence
/// the global symbol BCH would convert to BCC for Binance, but stay BCH
/// for most other exchanges.
/// </summary>
/// <param name="currency">Global currency</param>
/// <returns>Exchange currency</returns>
public string GlobalCurrencyToExchangeCurrency(string currency)
/// <summary>
/// Convert a global currency to exchange currency. For example, on Binance,
/// BCH (Bitcoin Cash) is BCC but in most other exchanges it is BCH, hence
/// the global symbol BCH would convert to BCC for Binance, but stay BCH
/// for most other exchanges.
/// </summary>
/// <param name="currency">Global currency</param>
/// <returns>Exchange currency</returns>
public string GlobalCurrencyToExchangeCurrency(string currency)
{
currency = (currency ?? string.Empty);
foreach (KeyValuePair<string, string> kv in ExchangeGlobalCurrencyReplacements[GetType()])
Expand Down Expand Up @@ -404,16 +414,25 @@ public virtual string NormalizeMarketSymbol(string? marketSymbol)
return marketSymbol.ToLowerInvariant();
}

/// <summary>
/// Convert an exchange symbol into a global symbol, which will be the same for all exchanges.
/// Global symbols are always uppercase and separate the currency pair with a hyphen (-).
/// Global symbols list the base currency first (i.e. BTC) and conversion currency
/// second (i.e. ETH). Example BTC-ETH, read as x BTC is worth y ETH.
/// BTC is always first, then ETH, etc. Fiat pair is always first in global symbol too.
/// </summary>
/// <param name="marketSymbol">Exchange symbol</param>
/// <returns>Global symbol</returns>
public virtual async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol)
/// <summary>
/// Convert an exchange symbol into a global symbol, which will be the same for all exchanges.
/// Global symbols are always uppercase and separate the currency pair with a hyphen (-).
/// Global symbols list the base currency first (i.e. BTC) and quote/conversion currency
/// second (i.e. USD). Global symbols are of the form BASE-QUOTE. BASE-QUOTE is read as
/// 1 BASE is worth y QUOTE.
///
/// Examples:
/// On 1/25/2020,
/// - BTC-USD: $8,371; 1 BTC (base) is worth $8,371 USD (quote)
/// - ETH-BTC: 0.01934; 1 ETH is worth 0.01934 BTC
/// - EUR-USD: 1.2; 1 EUR worth 1.2 USD
///
/// A value greater than 1 means one unit of base currency is more valuable than one unit of
/// quote currency.
/// </summary>
/// <param name="marketSymbol">Exchange symbol</param>
/// <returns>Global symbol</returns>
public virtual async Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol)
{
string modifiedMarketSymbol = marketSymbol;
char separator;
Expand Down Expand Up @@ -490,7 +509,7 @@ public virtual Task<string> GlobalMarketSymbolToExchangeMarketSymbolAsync(string
{
throw new ArgumentException($"Market symbol {marketSymbol} is missing the global symbol separator '{GlobalMarketSymbolSeparator}'");
}
if (MarketSymbolIsReversed)
if (MarketSymbolIsReversed == false)
{
marketSymbol = GlobalCurrencyToExchangeCurrency(marketSymbol.Substring(0, pos)) + MarketSymbolSeparator + GlobalCurrencyToExchangeCurrency(marketSymbol.Substring(pos + 1));
}
Expand Down
31 changes: 21 additions & 10 deletions src/ExchangeSharp/API/Exchanges/_Base/IExchangeAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,32 @@ public interface IExchangeAPI : IDisposable, IBaseAPI, IOrderBookProvider
#region Utility Methods

/// <summary>
/// Normalize a symbol for use on this exchange
/// Normalize a symbol for use on this exchange.
/// </summary>
/// <param name="marketSymbol">Symbol</param>
/// <returns>Normalized symbol</returns>
string NormalizeMarketSymbol(string marketSymbol);

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

/// <summary>
/// Convert a global symbol into an exchange symbol, which will potentially be different from other exchanges.
Expand Down
11 changes: 6 additions & 5 deletions tests/ExchangeSharpTests/ExchangeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ public async Task GlobalSymbolTest()
// if tests fail, uncomment this and it will save a new test file
// string allSymbolsJson = GetAllSymbolsJsonAsync().Sync(); System.IO.File.WriteAllText("TestData/AllSymbols.json", allSymbolsJson);

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

// sanity test that all exchanges return the same global symbol when converted back and forth
Expand All @@ -88,9 +88,10 @@ api is ExchangeOKCoinAPI || api is ExchangeDigifinexAPI || api is ExchangeNDAXAP
bool isBithumb = (api.Name == ExchangeName.Bithumb);
string exchangeMarketSymbol = await api.GlobalMarketSymbolToExchangeMarketSymbolAsync(isBithumb ? globalMarketSymbolAlt : globalMarketSymbol);
string globalMarketSymbol2 = await api.ExchangeMarketSymbolToGlobalMarketSymbolAsync(exchangeMarketSymbol);
if ((!isBithumb && globalMarketSymbol2.EndsWith("-BTC")) ||
globalMarketSymbol2.EndsWith("-USD") ||
globalMarketSymbol2.EndsWith("-USDT"))

if ((!isBithumb && globalMarketSymbol2.StartsWith("BTC-")) ||
globalMarketSymbol2.StartsWith("USD-") ||
globalMarketSymbol2.StartsWith("USDT-"))
{
Assert.Fail($"Exchange {api.Name} has wrong SymbolIsReversed parameter");
}
Expand Down