Files
2026-07-13 13:02:50 +08:00

917 lines
44 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.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<InvalidOperationException>(() => _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<List<OrderEvent>> 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<List<OrderEvent>> 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));
}
}
}
}