Skip to content

Commit

Permalink
Address Missing StableCoin Pairs in Crypto Exchanges (QuantConnect#5488)
Browse files Browse the repository at this point in the history
* Allow USDC in cashbook without USDC-USD pair

* Cover more stablecoin cases unique to our crypto brokers

* Add unit test

* Cleanup and expand test cases

* Add USDCEUR and USDCGBP to GDAX Symbols

* Add missing tickers to SPDB

* Cleanup test
  • Loading branch information
C-SELLERS authored Apr 28, 2021
1 parent 845e874 commit c082f0d
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 0 deletions.
20 changes: 20 additions & 0 deletions Common/Currencies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ public static class Currencies
{"USDT", "USDT"}
};

/// <summary>
/// Define some StableCoins that don't have direct pairs for base currencies in our SPDB
/// This is because some CryptoExchanges do not define direct pairs with the stablecoins they offer.
///
/// We use this to allow setting cash amounts for these stablecoins without needing a conversion
/// security.
/// </summary>
public static HashSet<Symbol> StableCoinsWithoutPairs = new HashSet<Symbol>
{
// Binance StableCoins Missing 1-1 Pairs
Symbol.Create("USDCUSD", SecurityType.Crypto, Market.Binance), // USD -> USDC
Symbol.Create("BGBPGBP", SecurityType.Crypto, Market.Binance), // GBP -> BGBP

// Coinbase StableCoins Missing 1-1 Pairs
Symbol.Create("USDCUSD", SecurityType.Crypto, Market.GDAX), // USD -> USDC

// Bitfinex StableCoins Missing 1-1 Pairs
Symbol.Create("EURSEUR", SecurityType.Crypto, Market.Bitfinex), // EUR -> EURS
};

/// <summary>
/// Gets the currency symbol for the specified currency code
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions Common/Securities/Cash.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,16 @@ public SubscriptionDataConfig EnsureCurrencyDataFeed(SecurityManager securities,
}
}

// Special case for crypto markets without direct pairs (They wont be found by the above)
// This allows us to add cash for "StableCoins" that are 1-1 with our account currency without needing a conversion security.
// Check out the StableCoinsWithoutPairs static var for those that are missing their 1-1 conversion pairs
if (Currencies.StableCoinsWithoutPairs.Contains(QuantConnect.Symbol.Create(normal, SecurityType.Crypto, marketMap[SecurityType.Crypto])))
{
ConversionRateSecurity = null;
ConversionRate = 1.0m;
return null;
}

// if this still hasn't been set then it's an error condition
throw new ArgumentException($"In order to maintain cash in {Symbol} you are required to add a " +
$"subscription for Forex pair {Symbol}{accountCurrency} or {accountCurrency}{Symbol}"
Expand Down
2 changes: 2 additions & 0 deletions Data/symbol-properties/symbol-properties-database.csv
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ gdax,UMAEUR,crypto,UMA-Euro,EUR,1,0.001,0.001,UMA-EUR
gdax,UMAGBP,crypto,UMA-British Pound,GBP,1,0.001,0.001,UMA-GBP
gdax,UMAUSD,crypto,UMA-United States Dollar,USD,1,0.001,0.001,UMA-USD
gdax,UNIUSD,crypto,Uniswap-United States Dollar,USD,1,0.0001,0.000001,UNI-USD
gdax,USDCEUR,crypto,USDC-Eur,EUR,1,0.001,0.01,USDC-EUR
gdax,USDCGBP,crypto,USDC-GBP,GBP,1,0.001,0.01,USDC-GBP
gdax,WBTCBTC,crypto,Wrapped Bitcoin-Bitcoin,BTC,1,0.0001,0.00000001,WBTC-BTC
gdax,WBTCUSD,crypto,Wrapped Bitcoin-United States Dollar,USD,1,0.01,0.00000001,WBTC-USD
gdax,XLMBTC,crypto,Stellar-Bitcoin,BTC,1,0.00000001,1,XLM-BTC
Expand Down
80 changes: 80 additions & 0 deletions Tests/Common/Securities/CashTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -601,9 +601,89 @@ public void EnsureCurrencyDataFeedDoesNothingWithUnsupportedCurrency()
Assert.IsEmpty(added);
}

[TestCaseSource(nameof(cryptoBrokerageStableCoinCases))]
public void CryptoStableCoinMappingIsCorrect(IBrokerageModel brokerageModel, string accountCurrency, string stableCoin, bool shouldThrow, Symbol expectedConversionSymbol)
{
var cashBook = new CashBook() {AccountCurrency = accountCurrency};
var cash = new Cash(stableCoin, 10m, 1m);
cashBook.Add(cash.Symbol, cash);

var subscriptions = new SubscriptionManager();
var dataManager = new DataManagerStub(TimeKeeper);
subscriptions.SetDataManager(dataManager);
var securities = new SecurityManager(TimeKeeper);

// Verify the behavior throws or doesn't throw depending on the case
if (shouldThrow)
{
Assert.Throws<ArgumentException>(() =>
{
cash.EnsureCurrencyDataFeed(securities, subscriptions, brokerageModel.DefaultMarkets, SecurityChanges.None, dataManager.SecurityService, cashBook.AccountCurrency);
});
}
else
{
Assert.DoesNotThrow(() =>
{
cash.EnsureCurrencyDataFeed(securities, subscriptions, brokerageModel.DefaultMarkets, SecurityChanges.None, dataManager.SecurityService, cashBook.AccountCurrency);
});
}

// Verify the conversion symbol is correct
if (expectedConversionSymbol == null)
{
Assert.IsNull(cash.ConversionRateSecurity);
}
else
{
Assert.AreEqual(expectedConversionSymbol, cash.ConversionRateSecurity.Symbol);
}
}

private static TimeKeeper TimeKeeper
{
get { return new TimeKeeper(DateTime.Now, new[] { TimeZone }); }
}

// Crypto brokerage model stable coin and account currency cases
// The last var is expectedConversionSymbol, and is null when we expect there
// not to be a conversion security for our tests output
private static object[] cryptoBrokerageStableCoinCases =
{
// *** Bitfinex ***
// Trades USDC, EURS, and USDT
// USDC Cases
new object[] { new BitfinexBrokerageModel(), Currencies.USD, "USDC", false, Symbol.Create("USDCUSD", SecurityType.Crypto, Market.Bitfinex) },
new object[] { new BitfinexBrokerageModel(), Currencies.EUR, "USDC", true, null }, // No USDCEUR, does throw!
new object[] { new BitfinexBrokerageModel(), Currencies.GBP, "USDC", true, null }, // No USDCGBP, does throw!

// EURS Cases
new object[] { new BitfinexBrokerageModel(), Currencies.USD, "EURS", false, Symbol.Create("EURSUSD", SecurityType.Crypto, Market.Bitfinex) },
new object[] { new BitfinexBrokerageModel(), Currencies.EUR, "EURS", false, null }, // No EURSEUR, but does not throw! Conversion 1-1
new object[] { new BitfinexBrokerageModel(), Currencies.GBP, "EURS", true, null }, // No EURSGBP, does throw!

// USDT (Tether) Cases
new object[] { new BitfinexBrokerageModel(), Currencies.USD, "USDT", false, Symbol.Create("USDTUSD", SecurityType.Crypto, Market.Bitfinex) },
new object[] { new BitfinexBrokerageModel(), Currencies.EUR, "USDT", true, null }, // No USDTEUR, does throw!
new object[] { new BitfinexBrokerageModel(), Currencies.GBP, "USDT", true, null }, // No USDTGBP, does throw!

// *** GDAX ***
// Trades USDC and USDT* (*Not yet trading live, but expected soon)
// USDC Cases
new object[] { new GDAXBrokerageModel(), Currencies.USD, "USDC", false, null }, // No USDCUSD, but does not throw! Conversion 1-1
new object[] { new GDAXBrokerageModel(), Currencies.EUR, "USDC", false, Symbol.Create("USDCEUR", SecurityType.Crypto, Market.GDAX) },
new object[] { new GDAXBrokerageModel(), Currencies.GBP, "USDC", false, Symbol.Create("USDCGBP", SecurityType.Crypto, Market.GDAX) },

// *** Binance ***
// USDC Cases
new object[] { new BinanceBrokerageModel(), Currencies.USD, "USDC", false, null }, // No USDCUSD, but does not throw! Conversion 1-1
new object[] { new BinanceBrokerageModel(), Currencies.EUR, "USDC", true, null }, // No USDCEUR, does throw!
new object[] { new BinanceBrokerageModel(), Currencies.GBP, "USDC", true, null }, // No USDCGBP, does throw!

// BGBP Cases
new object[] { new BinanceBrokerageModel(), Currencies.USD, "BGBP", true, null }, // No BGBPUSD, does throw!
new object[] { new BinanceBrokerageModel(), Currencies.EUR, "BGBP", true, null }, // No BGBPEUR, does throw!
new object[] { new BinanceBrokerageModel(), Currencies.GBP, "BGBP", false, null }, // No BGBPGBP, but does not throw! Conversion 1-1
};
}
}

0 comments on commit c082f0d

Please sign in to comment.