/* * 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.Threading; using NUnit.Framework; using QuantConnect.Algorithm; using QuantConnect.Brokerages; using QuantConnect.Brokerages.Backtesting; using QuantConnect.Data; using QuantConnect.Data.Market; using QuantConnect.Interfaces; using QuantConnect.Lean.Engine.Results; using QuantConnect.Lean.Engine.TransactionHandlers; using QuantConnect.Orders; using QuantConnect.Orders.Fees; using QuantConnect.Securities; using QuantConnect.Securities.Crypto; using QuantConnect.Securities.Positions; using QuantConnect.Tests.Engine; using QuantConnect.Tests.Engine.DataFeeds; namespace QuantConnect.Tests.Common.Securities { [TestFixture, Parallelizable(ParallelScope.Fixtures)] public class CashBuyingPowerModelTests { private Crypto _btcusd; private Crypto _btceur; private Crypto _ethusd; private Crypto _ethbtc; private SecurityPortfolioManager _portfolio; private BacktestingTransactionHandler _transactionHandler; private BacktestingBrokerage _brokerage; private IBuyingPowerModel _buyingPowerModel; private QCAlgorithm _algorithm; private LocalTimeKeeper _timeKeeper; private ITimeKeeper _globalTimeKeeper; private IResultHandler _resultHandler; [SetUp] public void Initialize() { _algorithm = new QCAlgorithm(); _algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(_algorithm)); _portfolio = _algorithm.Portfolio; _portfolio.CashBook.Add("EUR", 0, 1.20m); _portfolio.CashBook.Add("BTC", 0, 15000m); _portfolio.CashBook.Add("ETH", 0, 1000m); _algorithm.SetBrokerageModel(BrokerageName.Coinbase, AccountType.Cash); _transactionHandler = new BacktestingTransactionHandler(); _brokerage = new BacktestingBrokerage(_algorithm); _resultHandler = new TestResultHandler(); _transactionHandler.Initialize(_algorithm, _brokerage, _resultHandler); _algorithm.Transactions.SetOrderProcessor(_transactionHandler); var tz = TimeZones.NewYork; _btcusd = new Crypto( SecurityExchangeHours.AlwaysOpen(tz), _portfolio.CashBook[Currencies.USD], _portfolio.CashBook["BTC"], new SubscriptionDataConfig(typeof(TradeBar), Symbols.BTCUSD, Resolution.Minute, tz, tz, true, false, false), new SymbolProperties("BTCUSD", Currencies.USD, 1, 0.01m, 0.00000001m, string.Empty), ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null ); _ethusd = new Crypto( SecurityExchangeHours.AlwaysOpen(tz), _portfolio.CashBook[Currencies.USD], _portfolio.CashBook["ETH"], new SubscriptionDataConfig(typeof(TradeBar), Symbols.ETHUSD, Resolution.Minute, tz, tz, true, false, false), new SymbolProperties("ETHUSD", Currencies.USD, 1, 0.01m, 0.00000001m, string.Empty), ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null ); _btceur = new Crypto( SecurityExchangeHours.AlwaysOpen(tz), _portfolio.CashBook["EUR"], _portfolio.CashBook["BTC"], new SubscriptionDataConfig(typeof(TradeBar), Symbols.BTCEUR, Resolution.Minute, tz, tz, true, false, false), new SymbolProperties("BTCEUR", "EUR", 1, 0.01m, 0.00000001m, string.Empty), ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null ); _ethbtc = new Crypto( SecurityExchangeHours.AlwaysOpen(tz), _portfolio.CashBook["BTC"], _portfolio.CashBook["ETH"], new SubscriptionDataConfig(typeof(TradeBar), Symbols.ETHBTC, Resolution.Minute, tz, tz, true, false, false), new SymbolProperties("ETHBTC", "BTC", 1, 0.00001m, 0.00000001m, string.Empty), ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null ); _globalTimeKeeper = new TimeKeeper(new DateTime(2019, 11, 7)); _timeKeeper = _globalTimeKeeper.GetLocalTimeKeeper(tz); _buyingPowerModel = new BuyingPowerModelComparator( new CashBuyingPowerModel(), new SecurityPositionGroupBuyingPowerModel(), _portfolio, _globalTimeKeeper ); _btcusd.SetLocalTimeKeeper(_timeKeeper); _ethusd.SetLocalTimeKeeper(_timeKeeper); _btceur.SetLocalTimeKeeper(_timeKeeper); _ethbtc.SetLocalTimeKeeper(_timeKeeper); } [TearDown] public void TearDown() { _transactionHandler.Exit(); _resultHandler.Exit(); } [Test] public void InitializesCorrectly() { Assert.AreEqual(1m, _buyingPowerModel.GetLeverage(_btcusd)); Assert.AreEqual(0m, _buyingPowerModel.GetReservedBuyingPowerForPosition(_btcusd)); } [Test] public void SetLeverageDoesNotUpdateLeverage() { Assert.Throws(() => _buyingPowerModel.SetLeverage(_btcusd, 50m)); } [Test] public void LimitBuyBtcWithUsdRequiresUsdInPortfolio() { _portfolio.SetCash(20000); // Available cash = 20000 USD, can buy 2 BTC at 10000 var order = new LimitOrder(_btcusd.Symbol, 2m, 10000m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // Available cash = 20000 USD, cannot buy 2.1 BTC at 10000, need 21000 order = new LimitOrder(_btcusd.Symbol, 2.1m, 10000m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // Available cash = 20000 USD, cannot buy 2 BTC at 11000, need 22000 order = new LimitOrder(_btcusd.Symbol, 2m, 11000m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void LimitBuyBtcWithEurRequiresEurInPortfolio() { _portfolio.SetCash("EUR", 20000m, 1.20m); // Available cash = 20000 EUR, can buy 2 BTC at 10000 var order = new LimitOrder(_btceur.Symbol, 2m, 10000m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btceur, order).IsSufficient); // Available cash = 20000 EUR, cannot buy 2.1 BTC at 10000, need 21000 order = new LimitOrder(_btceur.Symbol, 2.1m, 10000m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btceur, order).IsSufficient); // Available cash = 20000 EUR, cannot buy 2 BTC at 11000, need 22000 order = new LimitOrder(_btceur.Symbol, 2m, 11000m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btceur, order).IsSufficient); } [Test] public void LimitSellOrderRequiresBaseCurrencyInPortfolio() { _portfolio.SetCash(0); _portfolio.CashBook["BTC"].SetAmount(0.5m); // 0.5 BTC in portfolio, can sell 0.5 BTC at any price var order = new LimitOrder(_btcusd.Symbol, -0.5m, 10000m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // 0.5 BTC in portfolio, cannot sell 0.6 BTC at any price order = new LimitOrder(_btcusd.Symbol, -0.6m, 10000m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void LimitBuyOrderChecksOpenOrders() { _portfolio.SetCash(5000); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 15000m }); _ethusd = _algorithm.AddCrypto("ETHUSD"); _ethusd.SetMarketPrice(new Tick { Value = 1000m }); _algorithm.SetFinishedWarmingUp(); // BTCUSD buy order decreases available USD (5000 - 1500 = 3500 USD) SubmitLimitOrder(_btcusd.Symbol, 0.1m, 15000m); // ETHUSD buy order decreases available USD (3500 - 3000 = 500 USD) SubmitLimitOrder(_ethusd.Symbol, 3m, 1000m); // 500 USD available, can buy 0.048 BTC at 10000 var order = new LimitOrder(_btcusd.Symbol, 0.048m, 10000m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // 500 USD available, cannot buy 0.06 BTC at 10000 order = new LimitOrder(_btcusd.Symbol, 0.06m, 10000m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void LimitSellOrderChecksOpenOrders() { _portfolio.SetCash(5000); _portfolio.CashBook["BTC"].SetAmount(1m); _portfolio.CashBook["ETH"].SetAmount(3m); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 15000m }); _ethusd = _algorithm.AddCrypto("ETHUSD"); _ethusd.SetMarketPrice(new Tick { Value = 1000m }); _ethbtc = _algorithm.AddCrypto("ETHBTC"); _ethbtc.SetMarketPrice(new Tick { Value = 0.1m }); _algorithm.SetFinishedWarmingUp(); // BTCUSD sell limit order decreases available BTC (1 - 0.1 = 0.9 BTC) SubmitLimitOrder(_btcusd.Symbol, -0.1m, 15000m); // ETHUSD sell limit order decreases available ETH (3 - 1 = 2 ETH) SubmitLimitOrder(_ethusd.Symbol, -1m, 1000m); // ETHBTC buy limit order decreases available BTC (0.9 - 0.1 = 0.8 BTC) SubmitLimitOrder(_ethbtc.Symbol, 1m, 0.1m); // BTCUSD sell stop order decreases available BTC (0.8 - 0.1 = 0.7 BTC) SubmitStopMarketOrder(_btcusd.Symbol, -0.1m, 5000m); // 0.7 BTC available, can sell 0.7 BTC at any price var order = new LimitOrder(_btcusd.Symbol, -0.7m, 10000m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // 0.7 BTC available, cannot sell 0.8 BTC at any price order = new LimitOrder(_btcusd.Symbol, -0.8m, 10000m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // 2 ETH available, can sell 2 ETH at any price order = new LimitOrder(_ethusd.Symbol, -2m, 1200m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _ethusd, order).IsSufficient); // 2 ETH available, cannot sell 2.1 ETH at any price order = new LimitOrder(_ethusd.Symbol, -2.1m, 2000m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _ethusd, order).IsSufficient); // 0.7 BTC available, can sell stop 0.7 BTC at any price var stopOrder = new StopMarketOrder(_btcusd.Symbol, -0.7m, 5000m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, stopOrder).IsSufficient); // 0.7 BTC available, cannot sell stop 0.8 BTC at any price stopOrder = new StopMarketOrder(_btcusd.Symbol, -0.8m, 5000m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, stopOrder).IsSufficient); } [Test] public void MarketBuyBtcWithUsdRequiresUsdInPortfolioPlusFees() { _portfolio.SetCash(20000); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m }); // Available cash = 20000 USD, cannot buy 2 BTC at 10000 (fees are excluded) var order = new MarketOrder(_btcusd.Symbol, 2m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // Maximum we can market buy with 20000 USD is 1.98412698 BTC Assert.AreEqual(1.98412698m, _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 1, 0).Quantity); _btcusd.SetMarketPrice(new Tick { Value = 9900m }); // Available cash = 20000 USD, can buy 2 BTC at 9900 (plus fees) order = new MarketOrder(_btcusd.Symbol, 2m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void MarketBuyBtcWithEurRequiresEurInPortfolioPlusFees() { _portfolio.SetCash("EUR", 20000m, 1.20m); _btceur.SetLocalTimeKeeper(_timeKeeper); _btceur.SetMarketPrice(new Tick { Value = 10000m }); // Available cash = 20000 EUR, cannot buy 2 BTC at 10000 (fees are excluded) var order = new MarketOrder(_btceur.Symbol, 2m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btceur, order).IsSufficient); // Maximum we can market buy with 20000 EUR is 1.98412698 BTC var targetValue = 20000m * _portfolio.CashBook["EUR"].ConversionRate / _portfolio.TotalPortfolioValue; Assert.AreEqual(1.98412698m, _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btceur, targetValue, 0).Quantity); _btceur.SetMarketPrice(new Tick { Value = 9900m }); // Available cash = 20000 EUR, can buy 2 BTC at 9900 (plus fees) order = new MarketOrder(_btceur.Symbol, 2m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btceur, order).IsSufficient); } [Test] public void MarketSellOrderRequiresBaseCurrencyInPortfolioPlusFees() { _portfolio.SetCash(0); _btcusd.SetMarketPrice(new Tick { Value = 10000m }); _portfolio.SetCash("BTC", 0.5m, 10000m); // 0.5 BTC in portfolio, can sell 0.5 BTC var order = new MarketOrder(_btcusd.Symbol, -0.5m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // 0.5 BTC in portfolio, cannot sell 0.51 BTC order = new MarketOrder(_btcusd.Symbol, -0.51m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // Maximum we can market sell with 0.5 BTC is 0.5 BTC Assert.AreEqual(-0.5m, _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 0, 0).Quantity); } [Test] public void MarketBuyOrderChecksOpenOrders() { _portfolio.SetCash(5000); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 15000m }); _ethusd = _algorithm.AddCrypto("ETHUSD"); _ethusd.SetMarketPrice(new Tick { Value = 1000m }); _algorithm.SetFinishedWarmingUp(); // BTCUSD buy order decreases available USD (5000 - 1500 = 3500 USD) SubmitLimitOrder(_btcusd.Symbol, 0.1m, 15000m); // ETHUSD buy order decreases available USD (3500 - 3000 = 500 USD) SubmitLimitOrder(_ethusd.Symbol, 3m, 1000m); // Maximum we can market buy with 500 USD is 0.03306878 BTC var quantity = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 500 / _portfolio.TotalPortfolioValue, 0).Quantity; Assert.AreEqual(0.03306878m, quantity); // 500 USD available, can buy `quantity` BTC at 15000 var order = new MarketOrder(_btcusd.Symbol, quantity, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // 500 USD available, cannot buy `quantity` + _btcusd.SymbolProperties.LotSize BTC at 15000 order = new MarketOrder(_btcusd.Symbol, quantity + _btcusd.SymbolProperties.LotSize, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void MarketSellOrderChecksOpenOrders() { _portfolio.SetCash(5000); _portfolio.CashBook["BTC"].SetAmount(1m); _portfolio.CashBook["ETH"].SetAmount(3m); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 15000m }); _ethusd = _algorithm.AddCrypto("ETHUSD"); _ethusd.SetMarketPrice(new Tick { Value = 1000m }); _ethbtc = _algorithm.AddCrypto("ETHBTC"); _ethbtc.SetMarketPrice(new Tick { Value = 0.1m }); _algorithm.SetFinishedWarmingUp(); // BTCUSD sell order decreases available BTC (1 - 0.1 = 0.9 BTC) SubmitLimitOrder(_btcusd.Symbol, -0.1m, 15000m); // ETHBTC buy order decreases available BTC (0.9 - 0.1 = 0.8 BTC) SubmitLimitOrder(_ethbtc.Symbol, 1m, 0.1m); // Maximum we can market sell with 0.8 BTC is -0.79365079 BTC (for a target position of 0.2 BTC) // target value = (1 - 0.8) * price Assert.AreEqual(-0.79365079m, _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 0.2m * 15000 / _portfolio.TotalPortfolioValue, 0).Quantity); // 0.8 BTC available, can sell 0.80 BTC at market var order = new MarketOrder(_btcusd.Symbol, -0.80m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // 0.8 BTC available, cannot sell 0.81 BTC at market order = new MarketOrder(_btcusd.Symbol, -0.81m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void LimitBuyOrderIncludesFees() { _portfolio.SetCash(20000); _btcusd.FeeModel = new ConstantFeeModel(50); // Available cash = 20000, cannot buy 2 BTC at 10000 because of order fee var order = new LimitOrder(_btcusd.Symbol, 2m, 10000m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // deposit another 50 USD _portfolio.CashBook[Currencies.USD].AddAmount(50); // now the order is allowed Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void CalculatesMaximumOrderQuantityCorrectly() { _portfolio.SetCash(10000); _portfolio.SetCash("EUR", 10000m, 1.20m); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 15000m }); _ethusd = _algorithm.AddCrypto("ETHUSD"); _ethusd.SetLocalTimeKeeper(_timeKeeper); _ethusd.SetMarketPrice(new Tick { Value = 1000m }); _ethbtc = _algorithm.AddCrypto("ETHBTC"); _ethbtc.SetLocalTimeKeeper(_timeKeeper); _ethbtc.SetMarketPrice(new Tick { Value = 0.1m }); _btceur = _algorithm.AddCrypto("BTCEUR"); _btceur.SetLocalTimeKeeper(_timeKeeper); _btceur.SetMarketPrice(new Tick { Value = 12000m }); _algorithm.SetFinishedWarmingUp(); // 0.66137566 * 15000 + fees + price buffer <= 10000 USD var targetValue = 10000 / _portfolio.TotalPortfolioValue; var getMaximumOrderQuantityForTargetValueResult = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, targetValue, 0).Quantity; Assert.AreEqual(0.66137566m, getMaximumOrderQuantityForTargetValueResult); var order = new MarketOrder(_btcusd.Symbol, getMaximumOrderQuantityForTargetValueResult, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); // 9.92063492 * 1000 + fees <= 10000 USD getMaximumOrderQuantityForTargetValueResult = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _ethusd, targetValue, 0).Quantity; Assert.AreEqual(9.92063492m, getMaximumOrderQuantityForTargetValueResult); order = new MarketOrder(_ethusd.Symbol, getMaximumOrderQuantityForTargetValueResult, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _ethusd, order).IsSufficient); // no BTC in portfolio, but GetMaximumOrderQuantityForTargetBuyingPower does not care var quantity = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _ethbtc, 1, 0).Quantity; Assert.AreNotEqual(0m, quantity); // HasSufficientBuyingPowerForOrder does check margin requirements order = new MarketOrder(_ethbtc.Symbol, quantity, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _ethbtc, order).IsSufficient); // 0.82671957 * 12000 + fees <= 10000 EUR targetValue = 10000m * _portfolio.CashBook["EUR"].ConversionRate / _portfolio.TotalPortfolioValue; getMaximumOrderQuantityForTargetValueResult = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btceur, targetValue, 0).Quantity; Assert.AreEqual(0.82671957m, getMaximumOrderQuantityForTargetValueResult); order = new MarketOrder(_btceur.Symbol, getMaximumOrderQuantityForTargetValueResult, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btceur, order).IsSufficient); } [Test] public void CalculatesMaximumOrderQuantityCorrectlySmallerTarget() { _portfolio.SetCash(10000); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 15000m }); _algorithm.SetFinishedWarmingUp(); var getMaximumOrderQuantityForTargetValueResult = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 0.1m, 0); // Quantity * 15000m + fees + price buffer <= 1000 USD Target Assert.AreEqual(0.06613756m, getMaximumOrderQuantityForTargetValueResult.Quantity); var order = new MarketOrder(_btcusd.Symbol, getMaximumOrderQuantityForTargetValueResult.Quantity, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void CalculatesMaximumOrderQuantityCorrectlyBiggerTarget() { _portfolio.SetCash(10000); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 15000m }); _algorithm.SetFinishedWarmingUp(); var getMaximumOrderQuantityForTargetValueResult = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 10m, 0); // Quantity * 15000m + fees + price buffer <= 100000 USD Target Assert.AreEqual(6.61375661m, getMaximumOrderQuantityForTargetValueResult.Quantity); var order = new MarketOrder(_btcusd.Symbol, getMaximumOrderQuantityForTargetValueResult.Quantity, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void CalculatesMaximumOrderQuantityCorrectlyForAlmostNoCashRemaining() { _portfolio.SetCash(0.00000000001m); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 15000m }); _algorithm.SetFinishedWarmingUp(); var getMaximumOrderQuantityForTargetValueResult = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 10000 / _portfolio.TotalPortfolioValue, 0); // We don't have enough cash, but GetMaximumOrderQuantityForTargetValue does not care about this :) Assert.AreEqual(0.66137566m, getMaximumOrderQuantityForTargetValueResult.Quantity); var order = new MarketOrder(_btcusd.Symbol, getMaximumOrderQuantityForTargetValueResult.Quantity, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void CalculatesMaximumOrderQuantityCorrectlyForNoOrderFee() { _portfolio.SetCash(100000m); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _algorithm.Securities[_btcusd.Symbol].SetFeeModel(new ConstantFeeModel(0)); _btcusd.SetMarketPrice(new Tick { Value = 15000m }); _algorithm.SetFinishedWarmingUp(); var getMaximumOrderQuantityForTargetValueResult = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 0.1m, 0); // Quantity * 15000m + fees (0) + price buffer <= 10000 USD Target Assert.AreEqual(0.66666666m, getMaximumOrderQuantityForTargetValueResult.Quantity); var order = new MarketOrder(_btcusd.Symbol, getMaximumOrderQuantityForTargetValueResult.Quantity, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void MarketBuyOrderChecksExistingHoldings() { _portfolio.SetCash(8000); _portfolio.CashBook.Add("BTC", 0.2m, 10000m); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m }); _algorithm.SetFinishedWarmingUp(); Assert.AreEqual(10000m, _portfolio.TotalPortfolioValue); // Maximum we can market buy for (10000-2000) = 8000 USD is 0.79365079 BTC var quantity = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 10000m / _portfolio.TotalPortfolioValue, 0).Quantity; Assert.AreEqual(0.79365079m, quantity); // the maximum order quantity can be executed var order = new MarketOrder(_btcusd.Symbol, quantity, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void MarketBuyBtcWithEurCalculatesBuyingPowerProperlyWithExistingHoldings() { _portfolio.SetCash("EUR", 20000m, 1.20m); _portfolio.CashBook.Add("BTC", 1m, 12000m); _btceur.SetLocalTimeKeeper(_timeKeeper); _btceur.SetMarketPrice(new Tick { Value = 10000m }); // Maximum we can market buy with 20000 EUR is 1.98412698 BTC // target value = 30000 EUR = 20000 EUR in cash + 10000 EUR in BTC var targetValue = 30000m * _portfolio.CashBook["EUR"].ConversionRate / _portfolio.TotalPortfolioValue; Assert.AreEqual(1.98412698m, _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btceur, targetValue, 0).Quantity); // Available cash = 20000 EUR, can buy 1.98 BTC at 10000 (plus fees) var order = new MarketOrder(_btceur.Symbol, 1.98m, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btceur, order).IsSufficient); // Available cash = 20000 EUR, cannot buy 2 BTC at 10000 (plus fees) order = new MarketOrder(_btceur.Symbol, 2m, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btceur, order).IsSufficient); } [Test] public void MarketBuyOrderUsesAskPriceIfAvailable() { _portfolio.SetCash(10000); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m, BidPrice = 9950, AskPrice = 10050, TickType = TickType.Quote }); _algorithm.SetFinishedWarmingUp(); Assert.AreEqual(10000m, _portfolio.TotalPortfolioValue); // Maximum we can market buy at ask price with 10000 USD is 0.98712785 BTC var quantity = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 1m, 0).Quantity; Assert.AreEqual(0.98712785m, quantity); // the maximum order quantity can be executed var order = new MarketOrder(_btcusd.Symbol, quantity, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void ZeroTargetWithZeroHoldingsIsNotAnError() { _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); var result = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_algorithm.Portfolio, _btcusd, 0, 0); var order = new MarketOrder(_btcusd.Symbol, result.Quantity, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); Assert.AreEqual(0, result.Quantity); Assert.AreEqual(string.Empty, result.Reason); Assert.AreEqual(false, result.IsError); } [Test] public void ZeroTargetWithNonZeroHoldingsReturnsNegativeOfQuantity() { _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _portfolio.CashBook.Add("BTC", 1m, 12000m); var result = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_algorithm.Portfolio, _btcusd, 0, 0); var order = new MarketOrder(_btcusd.Symbol, result.Quantity, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); Assert.AreEqual(-1, result.Quantity); Assert.AreEqual(string.Empty, result.Reason); Assert.AreEqual(false, result.IsError); } [Test] public void NonAccountCurrencyFees() { _portfolio.SetCash(10000); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m, BidPrice = 9950, AskPrice = 10050, TickType = TickType.Quote }); _algorithm.SetFinishedWarmingUp(); _btcusd.FeeModel = new NonAccountCurrencyCustomFeeModel(); Assert.AreEqual(10000m, _portfolio.TotalPortfolioValue); // 0.24875621 * 100050 (ask price) + 0.5 (fee) * 15000 (conversion rate, because its BTC) = 9999.9999105 var quantity = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 1m, 0).Quantity; Assert.AreEqual(0.24875621m, quantity); // the maximum order quantity can be executed var order = new MarketOrder(_btcusd.Symbol, quantity, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void NonAccountCurrency_NoQuoteCurrencyCash() { _algorithm.Portfolio.CashBook.Clear(); _algorithm.Portfolio.SetAccountCurrency("EUR"); _algorithm.Portfolio.SetCash(10000); Assert.AreEqual(10000m, _portfolio.TotalPortfolioValue); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m, BidPrice = 9950, AskPrice = 10050, TickType = TickType.Quote }); _algorithm.SetFinishedWarmingUp(); _algorithm.Portfolio.CashBook[Currencies.USD].ConversionRate = 0.88m; // we don't have any USD ! cash model shouldn't let us trade var quantity = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 1m, 0).Quantity; Assert.AreNotEqual(0m, quantity); // HasSufficientBuyingPowerForOrder does check margin requirements var order = new MarketOrder(_btcusd.Symbol, quantity, DateTime.UtcNow); var result = _buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order); Assert.IsFalse(result.IsSufficient); Assert.IsTrue(result.Reason.Contains("only a total value of 0 USD is available.")); } [TestCase("EUR")] [TestCase("ARG")] public void ZeroNonAccountCurrency_GetMaximumOrderQuantityForTargetValue(string accountCurrency) { _algorithm.Portfolio.CashBook.Clear(); _algorithm.Portfolio.SetAccountCurrency(accountCurrency); _algorithm.Portfolio.SetCash(0); _algorithm.Portfolio.SetCash(Currencies.USD, 10000, 0.88m); Assert.AreEqual(8800m, _portfolio.TotalPortfolioValue); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m, BidPrice = 9950, AskPrice = 10050, TickType = TickType.Quote }); _algorithm.SetFinishedWarmingUp(); // Maximum we can market buy at ask price with 10000 USD is 0.98712785 BTC => Account currency should not matter var quantity = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 1m, 0).Quantity; Assert.AreEqual(0.98712785m, quantity); var order = new MarketOrder(_btcusd.Symbol, quantity, DateTime.UtcNow); var fee = _btcusd.FeeModel.GetOrderFee(new OrderFeeParameters(_btcusd, order)); var feeAsAccountCurrency = _algorithm.Portfolio.CashBook.ConvertToAccountCurrency(fee.Value); var expectedQuantity = (8800 - feeAsAccountCurrency.Amount) / (_btcusd.AskPrice * 0.88m); expectedQuantity -= expectedQuantity % _btcusd.SymbolProperties.LotSize; Assert.AreEqual(expectedQuantity, quantity); // the maximum order quantity can be executed Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [Test] public void ZeroNonAccountCurrency_GetBuyingPower() { _algorithm.Portfolio.CashBook.Clear(); _algorithm.Portfolio.SetAccountCurrency("EUR"); _algorithm.Portfolio.SetCash(0); _algorithm.Portfolio.SetCash(Currencies.USD, 10000, 0.88m); Assert.AreEqual(8800m, _portfolio.TotalPortfolioValue); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m, BidPrice = 9950, AskPrice = 10050, TickType = TickType.Quote }); _algorithm.SetFinishedWarmingUp(); var quantity = _buyingPowerModel.GetBuyingPower(new BuyingPowerParameters(_portfolio, _btcusd, OrderDirection.Buy)).Value; Assert.AreEqual(1m, quantity); } [Test] public void ZeroNonAccountCurrency_GetReservedBuyingPowerForPosition() { _algorithm.Portfolio.CashBook.Clear(); _algorithm.Portfolio.SetAccountCurrency("EUR"); _algorithm.Portfolio.SetCash(0); _algorithm.Portfolio.SetCash(Currencies.USD, 10000, 0.88m); Assert.AreEqual(8800m, _portfolio.TotalPortfolioValue); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m, BidPrice = 9950, AskPrice = 10050, TickType = TickType.Quote }); _algorithm.SetFinishedWarmingUp(); _btcusd.Holdings.SetHoldings(_btcusd.Price, 100); var res = _buyingPowerModel.GetReservedBuyingPowerForPosition( new ReservedBuyingPowerForPositionParameters(_btcusd)); // Always returns 0. Since we're purchasing currencies outright, the position doesn't consume buying power Assert.AreEqual(0m, res.AbsoluteUsedBuyingPower); } [Test] public void NonAccountCurrency_GetBuyingPower() { _algorithm.Portfolio.CashBook.Clear(); _algorithm.Portfolio.SetAccountCurrency("EUR"); _algorithm.Portfolio.SetCash(10000); _algorithm.Portfolio.SetCash(Currencies.USD, 10000, 0.88m); Assert.AreEqual(18800m, _portfolio.TotalPortfolioValue); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m, BidPrice = 9950, AskPrice = 10050, TickType = TickType.Quote }); _algorithm.SetFinishedWarmingUp(); var quantity = _buyingPowerModel.GetBuyingPower(new BuyingPowerParameters(_portfolio, _btcusd, OrderDirection.Buy)).Value; Assert.AreEqual(1m, quantity); } [Test] public void NonAccountCurrency_ZeroQuoteCurrency_GetBuyingPower() { _algorithm.Portfolio.CashBook.Clear(); _algorithm.Portfolio.SetAccountCurrency("EUR"); _algorithm.Portfolio.SetCash(10000); _algorithm.Portfolio.SetCash(Currencies.USD, 0, 0.88m); Assert.AreEqual(10000, _portfolio.TotalPortfolioValue); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m, BidPrice = 9950, AskPrice = 10050, TickType = TickType.Quote }); _algorithm.SetFinishedWarmingUp(); var quantity = _buyingPowerModel.GetBuyingPower(new BuyingPowerParameters(_portfolio, _btcusd, OrderDirection.Buy)).Value; Assert.AreEqual(0m, quantity); } [TestCase("EUR")] [TestCase("ARG")] public void NonZeroNonAccountCurrency_UnReachableTarget(string accountCurrency) { _algorithm.Portfolio.CashBook.Clear(); _algorithm.Portfolio.SetAccountCurrency("EUR"); _algorithm.Portfolio.SetCash(10000); _algorithm.Portfolio.SetCash(Currencies.USD, 10000, 0.88m); Assert.AreEqual(18800m, _portfolio.TotalPortfolioValue); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m, BidPrice = 9950, AskPrice = 10050, TickType = TickType.Quote }); _algorithm.SetFinishedWarmingUp(); // Maximum we can market buy at ask price with 10000 USD + (10000 EUR / 0.88 rate) is 2.10886404 BTC var quantity = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, 1m, 0).Quantity; Assert.AreEqual(2.10886404m, quantity); // the maximum order quantity can be executed var order = new MarketOrder(_btcusd.Symbol, quantity, DateTime.UtcNow); Assert.IsFalse(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } [TestCase("EUR")] [TestCase("ARG")] public void NonZeroNonAccountCurrency_ReachableTarget(string accountCurrency) { _algorithm.Portfolio.CashBook.Clear(); _algorithm.Portfolio.SetAccountCurrency(accountCurrency); _algorithm.Portfolio.SetCash(10000); _algorithm.Portfolio.SetCash(Currencies.USD, 10000, 0.88m); Assert.AreEqual(18800m, _portfolio.TotalPortfolioValue); _btcusd = _algorithm.AddCrypto("BTCUSD"); _btcusd.SetLocalTimeKeeper(_timeKeeper); _btcusd.SetMarketPrice(new Tick { Value = 10000m, BidPrice = 9950, AskPrice = 10050, TickType = TickType.Quote }); _algorithm.SetFinishedWarmingUp(); // only use the USD for determining the target var reachableTarget = 8800m / 18800m; // Maximum we can market buy at ask price with 10000 USD is 0.98712785 BTC => Account currency should not matter var quantity = _buyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(_portfolio, _btcusd, reachableTarget, 0).Quantity; Assert.AreEqual(0.98712785m, quantity); // the maximum order quantity can be executed var order = new MarketOrder(_btcusd.Symbol, quantity, DateTime.UtcNow); Assert.IsTrue(_buyingPowerModel.HasSufficientBuyingPowerForOrder(_portfolio, _btcusd, order).IsSufficient); } private void SubmitLimitOrder(Symbol symbol, decimal quantity, decimal limitPrice) { using (var resetEvent = new ManualResetEvent(false)) { EventHandler> handler = (s, e) => { resetEvent.Set(); }; _brokerage.OrdersStatusChanged += handler; _algorithm.LimitOrder(symbol, quantity, limitPrice); if (!resetEvent.WaitOne(5000)) { throw new TimeoutException("SubmitLimitOrder"); } _brokerage.OrdersStatusChanged -= handler; } } private void SubmitStopMarketOrder(Symbol symbol, decimal quantity, decimal stopPrice) { using (var resetEvent = new ManualResetEvent(false)) { EventHandler> handler = (s, e) => { resetEvent.Set(); }; _brokerage.OrdersStatusChanged += handler; _algorithm.StopMarketOrder(symbol, quantity, stopPrice); if (!resetEvent.WaitOne(5000)) { throw new TimeoutException("SubmitStopMarketOrder"); } _brokerage.OrdersStatusChanged -= handler; } } internal class NonAccountCurrencyCustomFeeModel : FeeModel { public string FeeCurrency = "BTC"; public decimal FeeAmount = 0.5m; public override OrderFee GetOrderFee(OrderFeeParameters parameters) { return new OrderFee(new CashAmount(FeeAmount, FeeCurrency)); } } } }