770 lines
40 KiB
C#
770 lines
40 KiB
C#
/*
|
|
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
|
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using Newtonsoft.Json;
|
|
using NodaTime;
|
|
using NUnit.Framework;
|
|
using QuantConnect.Brokerages;
|
|
using QuantConnect.Data;
|
|
using QuantConnect.Data.Market;
|
|
using QuantConnect.Data.UniverseSelection;
|
|
using QuantConnect.Securities;
|
|
using QuantConnect.Securities.CurrencyConversion;
|
|
using QuantConnect.Tests.Common.Data.UniverseSelection;
|
|
using QuantConnect.Tests.Engine.DataFeeds;
|
|
|
|
namespace QuantConnect.Tests.Common.Securities
|
|
{
|
|
[TestFixture]
|
|
public class CashTests
|
|
{
|
|
private static readonly DateTimeZone TimeZone = TimeZones.NewYork;
|
|
private static readonly SecurityExchangeHours SecurityExchangeHours = SecurityExchangeHours.AlwaysOpen(TimeZone);
|
|
private static readonly IReadOnlyDictionary<SecurityType, string> MarketMap = DefaultBrokerageModel.DefaultMarketMap;
|
|
|
|
[Test]
|
|
public void ConstructorCapitalizedSymbol()
|
|
{
|
|
var cash = new Cash("low", 0, 0);
|
|
Assert.AreEqual("LOW", cash.Symbol);
|
|
}
|
|
|
|
[TestCase(null)]
|
|
[TestCase("")]
|
|
public void ConstructorThrowsOnEmptySymbol(string currency)
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
{
|
|
var cash = new Cash(currency, 0, 0);
|
|
}, "Cash symbols cannot be null or empty.");
|
|
}
|
|
|
|
[Test]
|
|
[TestCase("too long")]
|
|
[TestCase("s")]
|
|
public void ConstructorOnCustomSymbolLength(string currency)
|
|
{
|
|
var cash = new Cash(currency, 0, 0);
|
|
Assert.AreEqual(currency.ToUpper(CultureInfo.InvariantCulture), cash.Symbol);
|
|
}
|
|
|
|
[Test]
|
|
public void ConstructorSetsProperties()
|
|
{
|
|
const string symbol = "JPY";
|
|
const int quantity = 1;
|
|
const decimal conversionRate = 1.2m;
|
|
var cash = new Cash(symbol, quantity, conversionRate);
|
|
Assert.AreEqual(symbol, cash.Symbol);
|
|
Assert.AreEqual(quantity, cash.Amount);
|
|
Assert.AreEqual(conversionRate, cash.ConversionRate);
|
|
}
|
|
|
|
[Test]
|
|
public void ComputesValueInBaseCurrency()
|
|
{
|
|
const int quantity = 100;
|
|
const decimal conversionRate = 1 / 100m;
|
|
var cash = new Cash("JPY", quantity, conversionRate);
|
|
Assert.AreEqual(quantity * conversionRate, cash.ValueInAccountCurrency);
|
|
}
|
|
|
|
[Test]
|
|
public void EnsureCurrencyDataFeedAddsSubscription()
|
|
{
|
|
const int quantity = 100;
|
|
const decimal conversionRate = 1 / 100m;
|
|
var cash = new Cash("JPY", quantity, conversionRate);
|
|
var cashBook = new CashBook();
|
|
cashBook.Add("JPY", cash);
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var abcConfig = subscriptions.Add(Symbols.SPY, Resolution.Minute, TimeZone, TimeZone);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
|
|
securities.Add(
|
|
Symbols.SPY,
|
|
new Security(
|
|
SecurityExchangeHours,
|
|
abcConfig,
|
|
new Cash(Currencies.USD, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
cashBook,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()));
|
|
cash.EnsureCurrencyDataFeed(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService, cashBook.AccountCurrency);
|
|
|
|
Assert.AreEqual(1, subscriptions.SubscriptionDataConfigService.GetSubscriptionDataConfigs(Symbols.USDJPY, includeInternalConfigs:true).Count);
|
|
Assert.AreEqual(1, securities.Values.Count(x => x.Symbol == Symbols.USDJPY));
|
|
}
|
|
|
|
[Test]
|
|
public void EnsureCurrencyDataFeedChecksSecurityChangesForSecurity()
|
|
{
|
|
const int quantity = 100;
|
|
const decimal conversionRate = 1 / 100m;
|
|
var cash = new Cash("JPY", quantity, conversionRate);
|
|
var cashBook = new CashBook();
|
|
cashBook.Add("JPY", cash);
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var abcConfig = subscriptions.Add(Symbols.SPY, Resolution.Minute, TimeZone, TimeZone);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
securities.Add(
|
|
Symbols.SPY,
|
|
new Security(
|
|
SecurityExchangeHours,
|
|
abcConfig,
|
|
new Cash(cashBook.AccountCurrency, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
)
|
|
);
|
|
var usdjpy = new Security(Symbols.USDJPY, SecurityExchangeHours, new Cash("JPY", 0, 0), SymbolProperties.GetDefault("JPY"), ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null, new SecurityCache());
|
|
var changes = SecurityChangesTests.CreateNonInternal(new[] { usdjpy }, Enumerable.Empty<Security>());
|
|
var addedSecurities = cash.EnsureCurrencyDataFeed(securities, subscriptions, MarketMap, changes, dataManager.SecurityService, cashBook.AccountCurrency);
|
|
|
|
// the security exists in SecurityChanges so it is NOT added to the security manager or subscriptions
|
|
// this security will be added by the algorithm manager
|
|
Assert.True(addedSecurities == null || addedSecurities.Count == 0);
|
|
}
|
|
|
|
[Test]
|
|
public void EnsureCurrencyDataFeedsAddsSubscriptionAtMinimumResolution()
|
|
{
|
|
const int quantity = 100;
|
|
const decimal conversionRate = 1 / 100m;
|
|
const Resolution minimumResolution = Resolution.Second;
|
|
var cash = new Cash("JPY", quantity, conversionRate);
|
|
var cashBook = new CashBook();
|
|
cashBook.Add("JPY", cash);
|
|
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
securities.Add(
|
|
Symbols.SPY,
|
|
new Security(
|
|
SecurityExchangeHours,
|
|
subscriptions.Add(Symbols.SPY, Resolution.Minute, TimeZone, TimeZone),
|
|
new Cash(cashBook.AccountCurrency, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
)
|
|
);
|
|
securities.Add(
|
|
Symbols.EURUSD,
|
|
new Security(
|
|
SecurityExchangeHours,
|
|
subscriptions.Add(Symbols.EURUSD, minimumResolution, TimeZone, TimeZone),
|
|
new Cash(cashBook.AccountCurrency, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
)
|
|
);
|
|
|
|
cash.EnsureCurrencyDataFeed(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService, cashBook.AccountCurrency);
|
|
Assert.AreEqual(minimumResolution, subscriptions.SubscriptionDataConfigService.GetSubscriptionDataConfigs(Symbols.USDJPY, includeInternalConfigs: true).Single().Resolution);
|
|
}
|
|
|
|
[Test]
|
|
public void EnsureCurrencyDataFeedMarksIsCurrencyDataFeedForNewSubscriptions()
|
|
{
|
|
const int quantity = 100;
|
|
const decimal conversionRate = 1 / 100m;
|
|
var cash = new Cash("JPY", quantity, conversionRate);
|
|
var cashBook = new CashBook();
|
|
cashBook.Add("JPY", cash);
|
|
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
securities.Add(
|
|
Symbols.EURUSD,
|
|
new Security(
|
|
SecurityExchangeHours,
|
|
subscriptions.Add(Symbols.EURUSD, Resolution.Minute, TimeZone, TimeZone),
|
|
new Cash(cashBook.AccountCurrency, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
)
|
|
);
|
|
|
|
cash.EnsureCurrencyDataFeed(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService, cashBook.AccountCurrency);
|
|
var config = subscriptions.SubscriptionDataConfigService.GetSubscriptionDataConfigs(Symbols.USDJPY, includeInternalConfigs: true).Single();
|
|
Assert.IsTrue(config.IsInternalFeed);
|
|
}
|
|
|
|
[Test]
|
|
public void EnsureCurrencyDataFeedDoesNotMarkIsCurrencyDataFeedForExistantSubscriptions()
|
|
{
|
|
const int quantity = 100;
|
|
const decimal conversionRate = 1 / 100m;
|
|
var cash = new Cash("JPY", quantity, conversionRate);
|
|
var cashBook = new CashBook();
|
|
cashBook.Add("JPY", cash);
|
|
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
securities.Add(
|
|
Symbols.USDJPY,
|
|
new Security(
|
|
SecurityExchangeHours,
|
|
subscriptions.Add(Symbols.USDJPY, Resolution.Minute, TimeZone, TimeZone),
|
|
new Cash(cashBook.AccountCurrency, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
)
|
|
);
|
|
|
|
cash.EnsureCurrencyDataFeed(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService, cashBook.AccountCurrency);
|
|
var config = subscriptions.Subscriptions.Single(x => x.Symbol == Symbols.USDJPY);
|
|
Assert.IsFalse(config.IsInternalFeed);
|
|
}
|
|
|
|
[TestCase("USD", "GBP", "JPY", "GBPUSD", "USDJPY", SecurityType.Forex, Market.FXCM),
|
|
TestCase("EUR", "GBP", "JPY", "EURGBP", "EURJPY", SecurityType.Forex, Market.FXCM),
|
|
TestCase("AUD", "GBP", "USD", "GBPAUD", "AUDUSD", SecurityType.Forex, Market.FXCM),
|
|
TestCase("AUD", "JPY", "EUR", "AUDJPY", "EURAUD", SecurityType.Forex, Market.FXCM),
|
|
TestCase("CHF", "JPY", "EUR", "CHFJPY", "EURCHF", SecurityType.Forex, Market.FXCM),
|
|
TestCase("SGD", "JPY", "EUR", "SGDJPY", "EURSGD", SecurityType.Forex, Market.Oanda),
|
|
TestCase("BTC", "USD", "EUR", "BTCUSD", "BTCEUR", SecurityType.Crypto, Market.Bitfinex),
|
|
TestCase("EUR", "BTC", "ETH", "BTCEUR", "ETHEUR", SecurityType.Crypto, Market.Bitfinex),
|
|
TestCase("USD", "BTC", "ETH", "BTCUSD", "ETHUSD", SecurityType.Crypto, Market.Bitfinex),
|
|
TestCase("ETH", "USD", "BTC", "ETHUSD", "ETHBTC", SecurityType.Crypto, Market.Bitfinex),
|
|
TestCase("LTC", "USD", "BTC", "LTCUSD", "LTCBTC", SecurityType.Crypto, Market.Bitfinex),
|
|
TestCase("ETH", "BTC", "EOS", "ETHBTC", "EOSETH", SecurityType.Crypto, Market.Bitfinex)]
|
|
public void NonUsdAccountCurrencyCurrencyDataFeedsGetAdded(string accountCurrency,
|
|
string quoteCurrency,
|
|
string baseCurrency,
|
|
string quoteCurrencySymbol,
|
|
string baseCurrencySymbol,
|
|
SecurityType securityType,
|
|
string market)
|
|
{
|
|
var quoteCash = new Cash(quoteCurrency, 100, 1);
|
|
var baseCash = new Cash(baseCurrency, 100, 1);
|
|
var cashBook = new CashBook {{quoteCurrency, quoteCash},
|
|
{ baseCurrency, baseCash}};
|
|
|
|
var symbol = Symbol.Create(baseCurrency + quoteCurrency,
|
|
securityType,
|
|
market);
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper)
|
|
{
|
|
{
|
|
symbol, new Security(
|
|
SecurityExchangeHours,
|
|
subscriptions.Add(symbol, Resolution.Minute, TimeZone, TimeZone),
|
|
new Cash(cashBook.AccountCurrency, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
)
|
|
}
|
|
};
|
|
|
|
var configs1 = quoteCash.EnsureCurrencyDataFeed(securities,
|
|
subscriptions,
|
|
MarketMap,
|
|
SecurityChanges.None,
|
|
dataManager.SecurityService,
|
|
accountCurrency);
|
|
Assert.AreEqual(1, configs1.Count);
|
|
|
|
var config1 = configs1[0];
|
|
Assert.IsNotNull(config1);
|
|
Assert.AreEqual(quoteCurrencySymbol, config1.Symbol.Value);
|
|
|
|
var configs2 = baseCash.EnsureCurrencyDataFeed(securities,
|
|
subscriptions,
|
|
MarketMap,
|
|
SecurityChanges.None,
|
|
dataManager.SecurityService,
|
|
accountCurrency);
|
|
Assert.AreEqual(1, configs2.Count);
|
|
|
|
var config2 = configs2[0];
|
|
Assert.IsNotNull(config2);
|
|
Assert.AreEqual(baseCurrencySymbol, config2.Symbol.Value);
|
|
}
|
|
|
|
public void EnsureInternalCurrencyDataFeedsForNonUsdQuoteCurrencyGetAdded()
|
|
{
|
|
const int quantity = 100;
|
|
const decimal conversionRate = 1 / 100m;
|
|
var cashJPY = new Cash("JPY", quantity, conversionRate);
|
|
var cashGBP = new Cash("GBP", quantity, conversionRate);
|
|
var cashBook = new CashBook();
|
|
cashBook.Add("JPY", cashJPY);
|
|
cashBook.Add("GBP", cashGBP);
|
|
|
|
var symbol = Symbol.Create("GBPJPY", SecurityType.Forex, Market.FXCM);
|
|
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
securities.Add(
|
|
symbol,
|
|
new Security(
|
|
SecurityExchangeHours,
|
|
subscriptions.Add(symbol, Resolution.Minute, TimeZone, TimeZone),
|
|
new Cash(cashBook.AccountCurrency, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
)
|
|
);
|
|
|
|
cashJPY.EnsureCurrencyDataFeed(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService, cashBook.AccountCurrency);
|
|
var config1 = subscriptions.Subscriptions.Single(x => x.Symbol == Symbols.USDJPY);
|
|
Assert.IsTrue(config1.IsInternalFeed);
|
|
|
|
cashGBP.EnsureCurrencyDataFeed(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService, cashBook.AccountCurrency);
|
|
var config2 = subscriptions.Subscriptions.Single(x => x.Symbol == Symbols.GBPUSD);
|
|
Assert.IsTrue(config2.IsInternalFeed);
|
|
}
|
|
|
|
[Test]
|
|
public void EnsureCurrencyDataFeedsForNonUsdQuoteCurrencyDoNotGetAddedToSymbolCache()
|
|
{
|
|
SymbolCache.Clear();
|
|
const int quantity = 100;
|
|
const decimal conversionRate = 1 / 100m;
|
|
var cashJPY = new Cash("JPY", quantity, conversionRate);
|
|
var cashGBP = new Cash("GBP", quantity, conversionRate);
|
|
var cashBook = new CashBook();
|
|
cashBook.Add("JPY", cashJPY);
|
|
cashBook.Add("GBP", cashGBP);
|
|
|
|
var symbol = Symbol.Create("GBPJPY", SecurityType.Forex, Market.FXCM);
|
|
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
securities.Add(
|
|
symbol,
|
|
new Security(
|
|
SecurityExchangeHours,
|
|
subscriptions.Add(symbol, Resolution.Minute, TimeZone, TimeZone),
|
|
new Cash(cashBook.AccountCurrency, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
)
|
|
);
|
|
|
|
|
|
Assert.IsNotNull(
|
|
cashGBP.EnsureCurrencyDataFeed(
|
|
securities,
|
|
subscriptions,
|
|
MarketMap,
|
|
SecurityChanges.None,
|
|
dataManager.SecurityService,
|
|
cashBook.AccountCurrency));
|
|
Assert.IsNotNull(
|
|
cashJPY.EnsureCurrencyDataFeed(securities,
|
|
subscriptions,
|
|
MarketMap,
|
|
SecurityChanges.None,
|
|
dataManager.SecurityService,
|
|
cashBook.AccountCurrency));
|
|
Assert.IsFalse(SymbolCache.TryGetSymbol("USDJPY", out symbol));
|
|
Assert.IsFalse(SymbolCache.TryGetSymbol("GBPUSD", out symbol));
|
|
}
|
|
|
|
[Test]
|
|
public void EnsureCurrencyDataFeedForCryptoCurrency()
|
|
{
|
|
var book = new CashBook
|
|
{
|
|
{Currencies.USD, new Cash(Currencies.USD, 100, 1) },
|
|
{"BTC", new Cash("BTC", 100, 6000) },
|
|
{"LTC", new Cash("LTC", 100, 55) },
|
|
{"ETH", new Cash("ETH", 100, 290) },
|
|
{"EUR", new Cash("EUR", 100, 1.2m) },
|
|
{"JPY", new Cash("JPY", 100, 0.0088m) },
|
|
{"XAG", new Cash("XAG", 100, 1275) },
|
|
{"XAU", new Cash("XAU", 100, 17) }
|
|
};
|
|
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
|
|
book.EnsureCurrencyDataFeeds(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService);
|
|
|
|
var symbols = dataManager.SubscriptionManagerSubscriptions.Select(sdc => sdc.Symbol).ToHashSet();
|
|
|
|
Assert.IsTrue(symbols.Contains(Symbols.BTCUSD));
|
|
Assert.IsTrue(symbols.Contains(Symbols.LTCUSD));
|
|
Assert.IsTrue(symbols.Contains(Symbols.ETHUSD));
|
|
Assert.IsTrue(symbols.Contains(Symbols.EURUSD));
|
|
Assert.IsTrue(symbols.Contains(Symbols.XAGUSD));
|
|
Assert.IsTrue(symbols.Contains(Symbols.XAUUSD));
|
|
|
|
foreach (var subscription in subscriptions.Subscriptions)
|
|
{
|
|
Assert.AreEqual(
|
|
subscription.Symbol.SecurityType == SecurityType.Crypto ? TickType.Trade : TickType.Quote,
|
|
subscription.TickType);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void EnsureCurrencyDataFeedForCryptoCurrency_CryptoFuturesFirst_When_dYdX()
|
|
{
|
|
var book = new CashBook
|
|
{
|
|
{Currencies.USD, new Cash(Currencies.USD, 100, 1) },
|
|
{"BTC", new Cash("BTC", 100, 6000) },
|
|
{"ETH", new Cash("ETH", 100, 290) }
|
|
};
|
|
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
|
|
var marketMapWithdYdX = MarketMap.ToDictionary();
|
|
marketMapWithdYdX[SecurityType.CryptoFuture] = Market.DYDX;
|
|
|
|
book.EnsureCurrencyDataFeeds(securities, subscriptions, marketMapWithdYdX, SecurityChanges.None, dataManager.SecurityService);
|
|
|
|
var symbols = dataManager.SubscriptionManagerSubscriptions.Select(sdc => sdc.Symbol).ToHashSet();
|
|
|
|
Assert.IsTrue(symbols.Contains(Symbol.Create("BTCUSD", SecurityType.CryptoFuture, Market.DYDX)));
|
|
Assert.IsTrue(symbols.Contains(Symbol.Create("ETHUSD", SecurityType.CryptoFuture, Market.DYDX)));
|
|
|
|
foreach (var subscription in subscriptions.Subscriptions)
|
|
{
|
|
Assert.AreEqual(subscription.Symbol.SecurityType, SecurityType.CryptoFuture);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void UpdateModifiesConversionRateAsInvertedValue()
|
|
{
|
|
const int quantity = 100;
|
|
const decimal conversionRate = 1 / 100m;
|
|
var cash = new Cash("JPY", quantity, conversionRate);
|
|
var cashBook = new CashBook();
|
|
cashBook.Add("JPY", cash);
|
|
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
var security = new Security(
|
|
SecurityExchangeHours,
|
|
subscriptions.Add(Symbols.USDJPY, Resolution.Minute, TimeZone, TimeZone),
|
|
new Cash(cashBook.AccountCurrency, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
);
|
|
securities.Add(Symbols.USDJPY, security);
|
|
|
|
// we need to get subscription index
|
|
cash.EnsureCurrencyDataFeed(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService, cashBook.AccountCurrency);
|
|
|
|
var last = 120m;
|
|
security.SetMarketPrice(new Tick(DateTime.Now, Symbols.USDJPY, last, 119.95m, 120.05m));
|
|
cash.Update();
|
|
|
|
// jpy is inverted, so compare on the inverse
|
|
Assert.AreEqual(1 / last, cash.ConversionRate);
|
|
}
|
|
|
|
[Test]
|
|
public void UpdateModifiesConversionRate()
|
|
{
|
|
const int quantity = 100;
|
|
const decimal conversionRate = 1 / 100m;
|
|
var cash = new Cash("GBP", quantity, conversionRate);
|
|
var cashBook = new CashBook();
|
|
cashBook.Add("GBP", cash);
|
|
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
var security = new Security(
|
|
SecurityExchangeHours,
|
|
subscriptions.Add(Symbols.GBPUSD, Resolution.Minute, TimeZone, TimeZone),
|
|
new Cash(cashBook.AccountCurrency, 0, 1m),
|
|
SymbolProperties.GetDefault(cashBook.AccountCurrency),
|
|
ErrorCurrencyConverter.Instance,
|
|
RegisteredSecurityDataTypesProvider.Null,
|
|
new SecurityCache()
|
|
);
|
|
securities.Add(Symbols.GBPUSD, security);
|
|
|
|
// we need to get subscription index
|
|
cash.EnsureCurrencyDataFeed(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService, cashBook.AccountCurrency);
|
|
|
|
var last = 1.5m;
|
|
security.SetMarketPrice(new Tick(DateTime.Now, Symbols.GBPUSD, last, last * 1.009m, last * 0.009m));
|
|
cash.Update();
|
|
|
|
// jpy is inverted, so compare on the inverse
|
|
Assert.AreEqual(last, cash.ConversionRate);
|
|
}
|
|
|
|
[TestCase("USD", "$")]
|
|
[TestCase("EUR", "€")]
|
|
[TestCase("GBP", "₤")]
|
|
[TestCase("BTC", "₿")]
|
|
public void CashHasCorrectCurrencySymbol(string symbol, string currencySymbol)
|
|
{
|
|
var cash = new Cash(symbol, 1, 1);
|
|
Assert.AreEqual(currencySymbol, cash.CurrencySymbol);
|
|
}
|
|
|
|
[Test]
|
|
public void UpdateEventCalledForSetAmountMethod()
|
|
{
|
|
var called = false;
|
|
var cash = new Cash(Currencies.USD, 1, 1);
|
|
cash.Updated += (sender, args) =>
|
|
{
|
|
called = true;
|
|
};
|
|
cash.SetAmount(10m);
|
|
Assert.IsTrue(called);
|
|
}
|
|
|
|
[Test]
|
|
public void UpdateEventCalledForAddAmountMethod()
|
|
{
|
|
var called = false;
|
|
var cash = new Cash(Currencies.USD, 1, 1);
|
|
cash.Updated += (sender, args) =>
|
|
{
|
|
called = true;
|
|
};
|
|
cash.AddAmount(10m);
|
|
Assert.IsTrue(called);
|
|
}
|
|
|
|
[Test]
|
|
public void CashBookWithUsdCanBeSerializedAfterEnsureCurrencyDataFeed()
|
|
{
|
|
var book = new CashBook
|
|
{
|
|
{Currencies.USD, new Cash(Currencies.USD, 100, 1) },
|
|
{"EUR", new Cash("EUR", 100, 1.2m) }
|
|
};
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
|
|
book.EnsureCurrencyDataFeeds(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService);
|
|
|
|
Assert.DoesNotThrow(() => JsonConvert.SerializeObject(book, Formatting.Indented));
|
|
}
|
|
|
|
[Test]
|
|
public void EnsureCurrencyDataFeedDoesNothingWithUnsupportedCurrency()
|
|
{
|
|
var book = new CashBook
|
|
{
|
|
{Currencies.USD, new Cash(Currencies.USD, 100, 1) },
|
|
{"ILS", new Cash("ILS", 0, 0.3m) }
|
|
};
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
var dataManager = new DataManagerStub(TimeKeeper);
|
|
subscriptions.SetDataManager(dataManager);
|
|
var securities = new SecurityManager(TimeKeeper);
|
|
|
|
var added = book.EnsureCurrencyDataFeeds(securities, subscriptions, MarketMap, SecurityChanges.None, dataManager.SecurityService);
|
|
Assert.IsEmpty(added);
|
|
}
|
|
|
|
[TestCaseSource(nameof(cryptoBrokerageStableCoinCases))]
|
|
public void CryptoStableCoinMappingIsCorrect(IBrokerageModel brokerageModel, string accountCurrency, string stableCoin, bool shouldThrow, Symbol[] expectedConversionSymbols)
|
|
{
|
|
var cashBook = new CashBook() {AccountCurrency = accountCurrency};
|
|
var cash = new Cash(stableCoin, 10m, 0);
|
|
cashBook.Add(cash.Symbol, cash);
|
|
|
|
var subscriptions = new SubscriptionManager(NullTimeKeeper.Instance);
|
|
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 (expectedConversionSymbols == null)
|
|
{
|
|
Assert.IsInstanceOf(typeof(ConstantCurrencyConversion), cash.CurrencyConversion);
|
|
Assert.AreEqual(accountCurrency, cash.CurrencyConversion.SourceCurrency);
|
|
Assert.AreEqual(stableCoin, cash.CurrencyConversion.DestinationCurrency);
|
|
Assert.AreEqual(1m, cash.ConversionRate);
|
|
CollectionAssert.IsEmpty(cash.CurrencyConversion.ConversionRateSecurities);
|
|
}
|
|
else
|
|
{
|
|
Assert.IsInstanceOf(typeof(SecurityCurrencyConversion), cash.CurrencyConversion);
|
|
var actualConversionSymbols = cash.CurrencyConversion
|
|
.ConversionRateSecurities
|
|
.Select(x => x.Symbol)
|
|
.ToArray();
|
|
|
|
Assert.AreEqual(expectedConversionSymbols, actualConversionSymbols);
|
|
}
|
|
}
|
|
|
|
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 an (indirect) conversion for our tests output
|
|
private static object[] cryptoBrokerageStableCoinCases =
|
|
{
|
|
// *** Bitfinex ***
|
|
// Trades USDC, EURS, USDT y XCHF
|
|
// USDC Cases
|
|
new object[] { new BitfinexBrokerageModel(), Currencies.USD, "USDC", false, new[] { Symbol.Create("USDCUSD", SecurityType.Crypto, Market.Bitfinex) } },
|
|
new object[] { new BitfinexBrokerageModel(), Currencies.EUR, "USDC", false, new[] { Symbol.Create("USDCUSD", SecurityType.Crypto, Market.Bitfinex), Symbol.Create("EURUSD", SecurityType.Forex, Market.Oanda) } }, // No USDCEUR, but indirect conversion exists
|
|
new object[] { new BitfinexBrokerageModel(), Currencies.GBP, "USDC", false, new[] { Symbol.Create("USDCUSD", SecurityType.Crypto, Market.Bitfinex), Symbol.Create("GBPUSD", SecurityType.Forex, Market.Oanda) } }, // No USDCGBP, but indirect conversion exists
|
|
|
|
// EURS Cases
|
|
new object[] { new BitfinexBrokerageModel(), Currencies.USD, "EURS", false, new[] { 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", false, new[] { Symbol.Create("EURSUSD", SecurityType.Crypto, Market.Bitfinex), Symbol.Create("GBPUSD", SecurityType.Forex, Market.Oanda) } }, // No EURSGBP, but indirect conversion exists
|
|
|
|
// USDT (Tether) Cases
|
|
new object[] { new BitfinexBrokerageModel(), Currencies.CNH, "USDT", false, new[] { Symbol.Create("USDTCNHT", SecurityType.Crypto, Market.Bitfinex), Symbol.Create("CNHCNHT", SecurityType.Crypto, Market.Bitfinex) } }, // No USDTCNH, but indirect conversion exists
|
|
new object[] { new BitfinexBrokerageModel(), Currencies.USD, "USDT", false, new[] { Symbol.Create("USDTUSD", SecurityType.Crypto, Market.Bitfinex) } },
|
|
new object[] { new BitfinexBrokerageModel(), Currencies.EUR, "USDT", false, new[] { Symbol.Create("EURUSDT", SecurityType.Crypto, Market.Bitfinex) } },
|
|
new object[] { new BitfinexBrokerageModel(), Currencies.GBP, "USDT", false, new[] { Symbol.Create("GBPUSDT", SecurityType.Crypto, Market.Bitfinex) } },
|
|
|
|
// XCHF Cases
|
|
new object[] { new BitfinexBrokerageModel(), "CHF", "XCHF", false, null }, // No XCHFCHF, but does not throw! Conversion 1-1
|
|
new object[] { new BitfinexBrokerageModel(), Currencies.EUR, "XCHF", false, new[] { Symbol.Create("BTCXCHF", SecurityType.Crypto, Market.Bitfinex), Symbol.Create("BTCEUR", SecurityType.Crypto, Market.Bitfinex) } }, // No XCHFEUR, but indirect conversion exists
|
|
new object[] { new BitfinexBrokerageModel(), Currencies.GBP, "XCHF", false, new[] { Symbol.Create("BTCXCHF", SecurityType.Crypto, Market.Bitfinex), Symbol.Create("BTCGBP", SecurityType.Crypto, Market.Bitfinex) } }, // No XCHFGBP, but indirect conversion exists
|
|
|
|
// *** Coinbase ***
|
|
// Trades USDC and USDT* (*Not yet trading live, but expected soon)
|
|
// USDC Cases
|
|
new object[] { new CoinbaseBrokerageModel(), Currencies.USD, "USDC", false, null }, // No USDCUSD, but does not throw! Conversion 1-1
|
|
new object[] { new CoinbaseBrokerageModel(), Currencies.EUR, "USDC", false, new[] { Symbol.Create("USDCEUR", SecurityType.Crypto, Market.Coinbase) } },
|
|
new object[] { new CoinbaseBrokerageModel(), Currencies.GBP, "USDC", false, new[] { Symbol.Create("USDCGBP", SecurityType.Crypto, Market.Coinbase) } },
|
|
|
|
// *** Binance ***
|
|
// USDC Cases
|
|
new object[] { new BinanceBrokerageModel(), "USDT", "BNFCR", false, null },
|
|
new object[] { new BinanceBrokerageModel(), "USDC", "BNFCR", false, null },
|
|
new object[] { new BinanceBrokerageModel(), Currencies.USD, "BNFCR", false, null },
|
|
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", false, new[] { Symbol.Create("EURUSDC", SecurityType.Crypto, Market.Binance) } },
|
|
new object[] { new BinanceBrokerageModel(), Currencies.GBP, "USDC", false, new[] { Symbol.Create("ADAUSDC", SecurityType.Crypto, Market.Binance), Symbol.Create("ADAGBP", SecurityType.Crypto, Market.Binance) } }, // No USDCGBP, but indirect conversion exists
|
|
|
|
// USDT Cases
|
|
new object[] { new BinanceBrokerageModel(), Currencies.USD, "USDT", false, null }, // No USDTUSD, but does not throw! Conversion 1-1
|
|
new object[] { new BinanceBrokerageModel(), "VIA", "USDT", false, new[] { Symbol.Create("BNBUSDT", SecurityType.Crypto, Market.Binance), Symbol.Create("VIABNB", SecurityType.Crypto, Market.Binance) } }, // No USDTVIA, but indirect conversion exists
|
|
|
|
// USDP Cases
|
|
new object[] { new BinanceBrokerageModel(), Currencies.USD, "USDP", false, null }, // No USDPUSD, but does not throw! Conversion 1-1
|
|
new object[] { new BinanceBrokerageModel(), "VAI", "USDP", false, new[] { Symbol.Create("BTCUSDP", SecurityType.Crypto, Market.Binance), Symbol.Create("BTCVAI", SecurityType.Crypto, Market.Binance) } }, // No USDPVAI, but indirect conversion exists
|
|
|
|
// BUSD Cases
|
|
new object[] { new BinanceBrokerageModel(), Currencies.USD, "BUSD", false, null }, // No BUSDUSD, but does not throw! Conversion 1-1
|
|
new object[] { new BinanceBrokerageModel(), Currencies.EUR, "BUSD", false, new[] { Symbol.Create("EURBUSD", SecurityType.Crypto, Market.Binance) } },
|
|
new object[] { new BinanceBrokerageModel(), Currencies.GBP, "BUSD", false, new[] { Symbol.Create("GBPBUSD", SecurityType.Crypto, Market.Binance) } },
|
|
|
|
// UST Cases
|
|
new object[] { new BinanceBrokerageModel(), Currencies.USD, "UST", false, null }, // No USTUSD, but does not throw! Conversion 1-1
|
|
new object[] { new BinanceBrokerageModel(), "VAI", "UST", false, new[] { Symbol.Create("BTCUST", SecurityType.Crypto, Market.Binance), Symbol.Create("BTCVAI", SecurityType.Crypto, Market.Binance) } }, // No USTVAI, but indirect conversion exists
|
|
|
|
// TUSD Cases
|
|
new object[] { new BinanceBrokerageModel(), Currencies.USD, "TUSD", false, null }, // No TUSDUSD, but does not throw! Conversion 1-1
|
|
new object[] { new BinanceBrokerageModel(), "VAI", "TUSD", false, new[] { Symbol.Create("BTCTUSD", SecurityType.Crypto, Market.Binance), Symbol.Create("BTCVAI", SecurityType.Crypto, Market.Binance) } }, // No TUSDVAI, but indirect conversion exists
|
|
|
|
// DAI Cases
|
|
new object[] { new BinanceBrokerageModel(), Currencies.USD, "DAI", false, null }, // No DAIUSD, but does not throw! Conversion 1-1
|
|
new object[] { new BinanceBrokerageModel(), Currencies.EUR, "DAI", false, new[] { Symbol.Create("BNBDAI", SecurityType.Crypto, Market.Binance), Symbol.Create("BNBEUR", SecurityType.Crypto, Market.Binance) } }, // No DAIEUR, but indirect conversion exists
|
|
new object[] { new BinanceBrokerageModel(), Currencies.GBP, "DAI", false, new[] { Symbol.Create("BNBDAI", SecurityType.Crypto, Market.Binance), Symbol.Create("BNBGBP", SecurityType.Crypto, Market.Binance) } }, // No DAIGBP, but indirect conversion exists
|
|
|
|
// USDS Cases
|
|
new object[] { new BinanceBrokerageModel(), Currencies.USD, "SUSD", false, null }, // No SUSDUSD, but does not throw! Conversion 1-1
|
|
new object[] { new BinanceBrokerageModel(), Currencies.EUR, "SUSD", false, new[] { Symbol.Create("SUSDBTC", SecurityType.Crypto, Market.Binance), Symbol.Create("BTCEUR", SecurityType.Crypto, Market.Binance) } }, // No SUSDEUR, but indirect conversion exists
|
|
new object[] { new BinanceBrokerageModel(), Currencies.GBP, "SUSD", false, new[] { Symbol.Create("SUSDBTC", SecurityType.Crypto, Market.Binance), Symbol.Create("BTCGBP", SecurityType.Crypto, Market.Binance) } }, // No SUSDGBP, but indirect conversion exists
|
|
|
|
// IDRT Cases
|
|
new object[] { new BinanceBrokerageModel(), "IDR", "IDRT", false, null }, // No IDRTIDR, but does not throw! Conversion 1-1
|
|
new object[] { new BinanceBrokerageModel(), Currencies.EUR, "IDRT", false, new[] { Symbol.Create("BNBIDRT", SecurityType.Crypto, Market.Binance), Symbol.Create("BNBEUR", SecurityType.Crypto, Market.Binance) } }, // No IDRTEUR, but indirect conversion exists
|
|
new object[] { new BinanceBrokerageModel(), Currencies.GBP, "IDRT", false, new[] { Symbol.Create("BNBIDRT", SecurityType.Crypto, Market.Binance), Symbol.Create("BNBGBP", SecurityType.Crypto, Market.Binance) } }, // No IDRTGBP, but indirect conversion exists
|
|
|
|
new object[] { new OandaBrokerageModel(), Currencies.EUR, "INR", false, new[] { Symbol.Create("USDINR", SecurityType.Forex, Market.Oanda), Symbol.Create("EURUSD", SecurityType.Forex, Market.Oanda) } }, // No INREUR, but indirect conversion exists
|
|
|
|
// FDUSD Cases
|
|
new object[] { new BinanceBrokerageModel(), Currencies.USD, "FDUSD", false, null }, // No FDUSDUSD, but does not throw! Conversion 1-1
|
|
new object[] { new BinanceBrokerageModel(), "VAI", "FDUSD", false, new[] { Symbol.Create("BTCFDUSD", SecurityType.Crypto, Market.Binance), Symbol.Create("BTCVAI", SecurityType.Crypto, Market.Binance) } }, // No FDUSDVAI, but indirect conversion exists
|
|
};
|
|
}
|
|
}
|