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

900 lines
43 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 NUnit.Framework;
using QuantConnect.Data;
using QuantConnect.Util;
using QuantConnect.Orders;
using QuantConnect.Algorithm;
using QuantConnect.Brokerages;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using QuantConnect.Data.Market;
using QuantConnect.Orders.Fees;
using QuantConnect.Securities.Option;
using QuantConnect.Tests.Engine.DataFeeds;
using Option = QuantConnect.Securities.Option.Option;
namespace QuantConnect.Tests.Common.Securities
{
[TestFixture]
public class SecurityMarginModelTests
{
private static Symbol _symbol;
private static FakeOrderProcessor _fakeOrderProcessor;
[TestCase(1)]
[TestCase(2)]
[TestCase(50)]
public void MarginRemainingForLeverage(decimal leverage)
{
var algorithm = GetAlgorithm();
algorithm.SetCash(1000);
var spy = InitAndGetSecurity(algorithm, 0);
spy.Holdings.SetHoldings(25, 100);
spy.SetLeverage(leverage);
var spyMarginAvailable = spy.Holdings.HoldingsValue - spy.Holdings.HoldingsValue * (1 / leverage);
var marginRemaining = algorithm.Portfolio.MarginRemaining;
Assert.AreEqual(1000 + spyMarginAvailable, marginRemaining);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(50)]
public void MarginUsedForPositionWhenPriceDrops(decimal leverage)
{
var algorithm = GetAlgorithm();
// (1000 * 20) = 20k
// Initial and maintenance margin = (1000 * 20) / leverage = X
var spy = InitAndGetSecurity(algorithm, 0);
spy.Holdings.SetHoldings(20, 1000);
spy.SetLeverage(leverage);
// Drop 40% price from $20 to $12
// 1000 * 12 = 12k
Update(spy, 12);
var marginForPosition = spy.BuyingPowerModel.GetReservedBuyingPowerForPosition(
new ReservedBuyingPowerForPositionParameters(spy)).AbsoluteUsedBuyingPower;
Assert.AreEqual(1000 * 12 / leverage, marginForPosition);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(50)]
public void MarginUsedForPositionWhenPriceIncreases(decimal leverage)
{
var algorithm = GetAlgorithm();
algorithm.SetCash(1000);
// (1000 * 20) = 20k
// Initial and maintenance margin = (1000 * 20) / leverage = X
var spy = InitAndGetSecurity(algorithm, 0);
spy.Holdings.SetHoldings(25, 1000);
spy.SetLeverage(leverage);
// Increase from $20 to $40
// 1000 * 40 = 400000
Update(spy, 40);
var marginForPosition = spy.BuyingPowerModel.GetReservedBuyingPowerForPosition(
new ReservedBuyingPowerForPositionParameters(spy)).AbsoluteUsedBuyingPower;
Assert.AreEqual(1000 * 40 / leverage, marginForPosition);
}
[Test]
public void ZeroTargetWithZeroHoldingsIsNotAnError()
{
var algorithm = GetAlgorithm();
var security = InitAndGetSecurity(algorithm, 0);
var model = new SecurityMarginModel();
var result = model.GetMaximumOrderQuantityForTargetBuyingPower(algorithm.Portfolio, security, 0, 0);
Assert.AreEqual(0, result.Quantity);
Assert.IsTrue(result.Reason.IsNullOrEmpty());
Assert.IsFalse(result.IsError);
}
[TestCase(0)]
[TestCase(1)]
[TestCase(-1)]
public void ReturnsMinimumOrderValueReason(decimal holdings)
{
var algorithm = GetAlgorithm();
var security = InitAndGetSecurity(algorithm, 0);
var model = new SecurityMarginModel();
security.Holdings.SetHoldings(security.Price, holdings);
var currentSignedUsedMargin = model.GetInitialMarginRequirement(security, security.Holdings.Quantity);
var totalPortfolioValue = algorithm.Portfolio.TotalPortfolioValue;
var sign = Math.Sign(security.Holdings.Quantity) == 0 ? 1 : Math.Sign(security.Holdings.Quantity);
// we increase it slightly, should not trigger a new order because it's increasing final margin usage, rounds down
var newTarget = currentSignedUsedMargin / (totalPortfolioValue) + 0.00001m * sign;
var result = model.GetMaximumOrderQuantityForTargetBuyingPower(algorithm.Portfolio, security, newTarget, 0);
Assert.AreEqual(0m, result.Quantity);
Assert.IsFalse(result.IsError);
Assert.IsTrue(result.Reason.Contains("The order quantity is less than the lot size of", StringComparison.InvariantCultureIgnoreCase));
}
[TestCase(1)]
[TestCase(-1)]
public void ReducesPositionWhenMarginAboveTargetWhenNegativeFreeMargin(decimal holdings)
{
var algorithm = GetAlgorithm();
var security = InitAndGetSecurity(algorithm, 0);
var model = new SecurityMarginModel();
security.Holdings.SetHoldings(security.Price, holdings);
var security2 = InitAndGetSecurity(algorithm, 0, symbol: "AAPL");
// eat up all our TPV
security2.Holdings.SetHoldings(security.Price, (algorithm.Portfolio.TotalPortfolioValue / security.Price) * 2);
var currentSignedUsedMargin = model.GetInitialMarginRequirement(security, security.Holdings.Quantity);
var totalPortfolioValue = algorithm.Portfolio.TotalPortfolioValue;
var sign = Math.Sign(security.Holdings.Quantity) == 0 ? 1 : Math.Sign(security.Holdings.Quantity);
// we inverse the sign here so that new target is less than current, we expect a reduction
var newTarget = currentSignedUsedMargin / (totalPortfolioValue) + 0.00001m * sign * -1;
Assert.IsTrue(0 > algorithm.Portfolio.MarginRemaining);
var result = model.GetMaximumOrderQuantityForTargetBuyingPower(algorithm.Portfolio, security, newTarget, 0);
// Reproduces GH issue #5763 a small Reduction in the target should reduce the position
Assert.AreEqual(1m * sign * -1, result.Quantity);
Assert.IsFalse(result.IsError);
}
[TestCase(1, 0)]
[TestCase(-1, 0)]
[TestCase(1, 0.001d)]
[TestCase(-1, 0.001d)]
public void ReducesPositionWhenMarginAboveTargetBasedOnSetting(decimal holdings, decimal minimumOrderMarginPortfolioPercentage)
{
var algorithm = GetAlgorithm();
var security = InitAndGetSecurity(algorithm, 0);
var model = new SecurityMarginModel();
security.Holdings.SetHoldings(security.Price, holdings);
var currentSignedUsedMargin = model.GetInitialMarginRequirement(security, security.Holdings.Quantity);
var totalPortfolioValue = algorithm.Portfolio.TotalPortfolioValue;
var sign = Math.Sign(security.Holdings.Quantity) == 0 ? 1 : Math.Sign(security.Holdings.Quantity);
// we inverse the sign here so that new target is less than current, we expect a reduction
var newTarget = currentSignedUsedMargin / (totalPortfolioValue) + 0.00001m * sign * -1;
var result = model.GetMaximumOrderQuantityForTargetBuyingPower(algorithm.Portfolio, security, newTarget, minimumOrderMarginPortfolioPercentage);
if (minimumOrderMarginPortfolioPercentage == 0)
{
// Reproduces GH issue #5763 a small Reduction in the target should reduce the position
Assert.AreEqual(1m * sign * -1, result.Quantity);
Assert.IsFalse(result.IsError);
}
else
{
Assert.AreEqual(0, result.Quantity);
Assert.IsFalse(result.IsError);
}
}
[Test]
public void ZeroTargetWithNonZeroHoldingsReturnsNegativeOfQuantity()
{
var algorithm = GetAlgorithm();
var security = InitAndGetSecurity(algorithm, 0);
security.Holdings.SetHoldings(200, 10);
var model = new SecurityMarginModel();
var result = model.GetMaximumOrderQuantityForTargetBuyingPower(algorithm.Portfolio, security, 0, 0);
Assert.AreEqual(-10, result.Quantity);
Assert.IsTrue(result.Reason.IsNullOrEmpty());
Assert.IsFalse(result.IsError);
}
[Test]
public void SetHoldings_ZeroToFullLong()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 5);
var actual = algo.CalculateOrderQuantity(_symbol, 1m * security.BuyingPowerModel.GetLeverage(security));
// (100000 * 2 * 0.9975 setHoldingsBuffer) / 25 - fee ~=7979m
Assert.AreEqual(7979m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
}
[Test]
public void SetHoldings_ZeroToFullLong_NonAccountCurrency_ZeroQuoteCurrency()
{
var algorithm = GetAlgorithm();
algorithm.Portfolio.CashBook.Clear();
algorithm.Portfolio.SetAccountCurrency("EUR");
algorithm.Portfolio.SetCash(10000);
// We don't have quote currency - we will get a "loan"
algorithm.Portfolio.SetCash(Currencies.USD, 0, 0.88m);
var security = InitAndGetSecurity(algorithm, 5);
algorithm.Settings.FreePortfolioValue =
algorithm.Portfolio.TotalPortfolioValue * algorithm.Settings.FreePortfolioValuePercentage;
var actual = algorithm.CalculateOrderQuantity(_symbol, 1m * security.BuyingPowerModel.GetLeverage(security));
// (10000 * 2 * 0.9975 setHoldingsBuffer) / 25 * 0.88 conversion rate - 5 USD fee * 0.88 conversion rate ~=906m
Assert.AreEqual(906m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algorithm));
}
[TestCase("Long")]
[TestCase("Short")]
public void GetReservedBuyingPowerForPosition_NonAccountCurrency_ZeroQuoteCurrency(string position)
{
var algorithm = GetAlgorithm();
algorithm.Portfolio.CashBook.Clear();
algorithm.Portfolio.SetAccountCurrency("EUR");
algorithm.Portfolio.SetCash(10000);
algorithm.Portfolio.SetCash(Currencies.USD, 0, 0.88m);
var security = InitAndGetSecurity(algorithm, 5);
security.Holdings.SetHoldings(security.Price,
(position == "Long" ? 1 : -1) * 100);
var actual = security.BuyingPowerModel.GetReservedBuyingPowerForPosition(new ReservedBuyingPowerForPositionParameters(security));
// 100quantity * 25price * 0.88rate * 0.5 MaintenanceMarginRequirement = 1100
Assert.AreEqual(1100, actual.AbsoluteUsedBuyingPower);
}
[Test]
public void SetHoldings_ZeroToFullLong_NonAccountCurrency()
{
var algorithm = GetAlgorithm();
algorithm.Portfolio.CashBook.Clear();
algorithm.Portfolio.SetAccountCurrency("EUR");
algorithm.Portfolio.SetCash(10000);
// We have 1000 USD too
algorithm.Portfolio.SetCash(Currencies.USD, 1000, 0.88m);
var security = InitAndGetSecurity(algorithm, 5);
algorithm.Settings.FreePortfolioValue =
algorithm.Portfolio.TotalPortfolioValue * algorithm.Settings.FreePortfolioValuePercentage;
var actual = algorithm.CalculateOrderQuantity(_symbol, 1m * security.BuyingPowerModel.GetLeverage(security));
// ((10000 + 1000 USD * 0.88 rate) * 2 * 0.9975 setHoldingsBuffer) / 25 * 0.88 rate - 5 USD fee * 0.88 rate ~=986m
Assert.AreEqual(986m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algorithm));
}
[Test]
public void SetHoldings_Long_TooBigOfATarget()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 5);
var actual = algo.CalculateOrderQuantity(_symbol, 1m * security.BuyingPowerModel.GetLeverage(security) + 0.1m);
// (100000 * 2.1* 0.9975 setHoldingsBuffer) / 25 - fee ~=8378m
Assert.AreEqual(8378m, actual);
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual, security, algo));
}
[Test]
public void SetHoldings_Long_TooBigOfATarget_NonAccountCurrency()
{
var algorithm = GetAlgorithm();
algorithm.Portfolio.CashBook.Clear();
algorithm.Portfolio.SetAccountCurrency("EUR");
algorithm.Portfolio.SetCash(10000);
// We don't have quote currency - we will get a "loan"
algorithm.Portfolio.SetCash(Currencies.USD, 0, 0.88m);
var security = InitAndGetSecurity(algorithm, 5);
algorithm.Settings.FreePortfolioValue =
algorithm.Portfolio.TotalPortfolioValue * algorithm.Settings.FreePortfolioValuePercentage;
var actual = algorithm.CalculateOrderQuantity(_symbol, 1m * security.BuyingPowerModel.GetLeverage(security) + 0.1m);
// (10000 * 2.1 * 0.9975 setHoldingsBuffer) / 25 * 0.88 conversion rate - 5 USD fee * 0.88 conversion rate ~=951m
Assert.AreEqual(951m, actual);
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual, security, algorithm));
}
[Test]
public void SetHoldings_ZeroToFullShort()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 5);
var actual = algo.CalculateOrderQuantity(_symbol, -1m * security.BuyingPowerModel.GetLeverage(security));
// (100000 * 2 * 0.9975 setHoldingsBuffer) / 25 - fee~=-7979m
Assert.AreEqual(-7979m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
}
[Test]
public void SetHoldings_ZeroToFullShort_NonAccountCurrency_ZeroQuoteCurrency()
{
var algorithm = GetAlgorithm();
algorithm.Portfolio.CashBook.Clear();
algorithm.Portfolio.SetAccountCurrency("EUR");
algorithm.Portfolio.SetCash(10000);
algorithm.Portfolio.SetCash(Currencies.USD, 0, 0.88m);
var security = InitAndGetSecurity(algorithm, 5);
algorithm.Settings.FreePortfolioValue =
algorithm.Portfolio.TotalPortfolioValue * algorithm.Settings.FreePortfolioValuePercentage;
var actual = algorithm.CalculateOrderQuantity(_symbol, -1m * security.BuyingPowerModel.GetLeverage(security));
// (10000 * - 2 * 0.9975 setHoldingsBuffer) / 25 * 0.88 conversion rate - 5 USD fee * 0.88 conversion rate ~=906m
Assert.AreEqual(-906m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algorithm));
}
[Test]
public void SetHoldings_ZeroToFullShort_NonAccountCurrency()
{
var algorithm = GetAlgorithm();
algorithm.Portfolio.CashBook.Clear();
algorithm.Portfolio.SetAccountCurrency("EUR");
algorithm.Portfolio.SetCash(10000);
algorithm.Portfolio.SetCash(Currencies.USD, 1000, 0.88m);
var security = InitAndGetSecurity(algorithm, 5);
algorithm.Settings.FreePortfolioValue =
algorithm.Portfolio.TotalPortfolioValue * algorithm.Settings.FreePortfolioValuePercentage;
var actual = algorithm.CalculateOrderQuantity(_symbol, -1m * security.BuyingPowerModel.GetLeverage(security));
// ((10000 + 1000 * 0.88)* - 2 * 0.9975 setHoldingsBuffer) / 25 * 0.88 conversion rate - 5 USD fee * 0.88 conversion rate ~=986m
Assert.AreEqual(-986m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algorithm));
}
[Test]
public void SetHoldings_Short_TooBigOfATarget()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 5);
var actual = algo.CalculateOrderQuantity(_symbol, -1m * security.BuyingPowerModel.GetLeverage(security) - 0.1m);
// (100000 * - 2.1m * 0.9975 setHoldingsBuffer) / 25 - fee~=-8378m
Assert.AreEqual(-8378m, actual);
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual, security, algo));
}
[Test]
public void SetHoldings_Short_TooBigOfATarget_NonAccountCurrency()
{
var algorithm = GetAlgorithm();
algorithm.Portfolio.CashBook.Clear();
algorithm.Portfolio.SetAccountCurrency("EUR");
algorithm.Portfolio.SetCash(10000);
algorithm.Portfolio.SetCash(Currencies.USD, 0, 0.88m);
var security = InitAndGetSecurity(algorithm, 5);
algorithm.Settings.FreePortfolioValue =
algorithm.Portfolio.TotalPortfolioValue * algorithm.Settings.FreePortfolioValuePercentage;
var actual = algorithm.CalculateOrderQuantity(_symbol, -1m * security.BuyingPowerModel.GetLeverage(security) - 0.1m);
// (10000 * - 2.1 * 0.9975 setHoldingsBuffer) / 25 * 0.88 conversion rate - 5 USD fee * 0.88 conversion rate ~=951m
Assert.AreEqual(-951m, actual);
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual, security, algorithm));
}
[Test]
public void SetHoldings_ZeroToFullLong_NoFee()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 0);
var actual = algo.CalculateOrderQuantity(_symbol, 1m * security.BuyingPowerModel.GetLeverage(security));
// (100000 * 2 * 0.9975 setHoldingsBuffer) / 25 =7980m
Assert.AreEqual(7980m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
}
[Test]
public void SetHoldings_Long_TooBigOfATarget_NoFee()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 0);
var actual = algo.CalculateOrderQuantity(_symbol, 1m * security.BuyingPowerModel.GetLeverage(security) + 0.1m);
// (100000 * 2.1m* 0.9975 setHoldingsBuffer) / 25 = 8379m
Assert.AreEqual(8379m, actual);
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual, security, algo));
}
[Test]
public void SetHoldings_ZeroToFullShort_NoFee()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 0);
var actual = algo.CalculateOrderQuantity(_symbol, -1m * security.BuyingPowerModel.GetLeverage(security));
var order = new MarketOrder(_symbol, actual, DateTime.UtcNow);
// (100000 * 2 * 0.9975 setHoldingsBuffer) / 25 = -7980m
Assert.AreEqual(-7980m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
}
[Test]
public void SetHoldings_Short_TooBigOfATarget_NoFee()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 0);
var actual = algo.CalculateOrderQuantity(_symbol, -1m * security.BuyingPowerModel.GetLeverage(security) - 0.1m);
// (100000 * -2.1 * 0.9975 setHoldingsBuffer) / 25 = -8379m
Assert.AreEqual(-8379m, actual);
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual, security, algo));
}
[Test]
public void FreeBuyingPowerPercentDefault_Equity()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 5, SecurityType.Equity);
var model = security.BuyingPowerModel;
var actual = algo.CalculateOrderQuantity(_symbol, 1m * model.GetLeverage(security));
// (100000 * 2 * 0.9975) / 25 - 1 order due to fees
Assert.AreEqual(7979m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
Assert.AreEqual(algo.Portfolio.Cash, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
}
[Test]
public void FreeBuyingPowerPercentAppliesForCashAccount_Equity()
{
var algo = GetAlgorithm();
algo.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash);
var security = InitAndGetSecurity(algo, 5, SecurityType.Equity);
var requiredFreeBuyingPowerPercent = 0.05m;
var model = security.BuyingPowerModel = new SecurityMarginModel(1, requiredFreeBuyingPowerPercent);
var actual = algo.CalculateOrderQuantity(_symbol, 1m * model.GetLeverage(security));
// (100000 * 1 * 0.95 * 0.9975) / 25 - 1 order due to fees
Assert.AreEqual(3790m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual * 1.0025m, security, algo));
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual * 1.0025m + security.SymbolProperties.LotSize + 9, security, algo));
var expectedBuyingPower = algo.Portfolio.Cash * (1 - requiredFreeBuyingPowerPercent);
Assert.AreEqual(expectedBuyingPower, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
}
[Test]
public void FreeBuyingPowerPercentAppliesForMarginAccount_Equity()
{
var algo = GetAlgorithm();
algo.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
var security = InitAndGetSecurity(algo, 5, SecurityType.Equity);
var requiredFreeBuyingPowerPercent = 0.05m;
var model = security.BuyingPowerModel = new SecurityMarginModel(2, requiredFreeBuyingPowerPercent);
var actual = algo.CalculateOrderQuantity(_symbol, 1m * model.GetLeverage(security));
// (100000 * 2 * 0.95 * 0.9975) / 25 - 1 order due to fees
Assert.AreEqual(7580m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual * 1.0025m, security, algo));
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual * 1.0025m + security.SymbolProperties.LotSize + 9, security, algo));
var expectedBuyingPower = algo.Portfolio.Cash * (1 - requiredFreeBuyingPowerPercent);
Assert.AreEqual(expectedBuyingPower, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
}
[Test]
public void FreeBuyingPowerPercentCashAccountWithLongHoldings_Equity()
{
var algo = GetAlgorithm();
algo.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash);
var security = InitAndGetSecurity(algo, 5, SecurityType.Equity);
var requiredFreeBuyingPowerPercent = 0.05m;
var model = security.BuyingPowerModel = new SecurityMarginModel(1, requiredFreeBuyingPowerPercent);
security.Holdings.SetHoldings(25, 2000);
security.SettlementModel.ApplyFunds(new ApplyFundsSettlementModelParameters(algo.Portfolio, security, DateTime.UtcNow.AddDays(-10), new CashAmount(-2000 * 25, Currencies.USD), null));
// Margin remaining 50k + used 50k + initial margin 50k - 5k free buying power percent (5% of 100k)
Assert.AreEqual(145000, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Sell));
// Margin remaining 50k - 5k free buying power percent (5% of 100k)
Assert.AreEqual(45000, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
var actual = algo.CalculateOrderQuantity(_symbol, -1m * model.GetLeverage(security));
// ((100k - 5) * -1 * 0.95 * 0.9975 - (50k holdings)) / 25 - 1 order due to fees
Assert.AreEqual(-5790m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual * 1.0025m, security, algo));
}
[Test]
public void FreeBuyingPowerPercentMarginAccountWithLongHoldings_Equity()
{
var algo = GetAlgorithm();
algo.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
var security = InitAndGetSecurity(algo, 5, SecurityType.Equity);
var requiredFreeBuyingPowerPercent = 0.05m;
var model = security.BuyingPowerModel = new SecurityMarginModel(2, requiredFreeBuyingPowerPercent);
security.Holdings.SetHoldings(25, 2000);
security.SettlementModel.ApplyFunds(new ApplyFundsSettlementModelParameters(algo.Portfolio, security, DateTime.UtcNow.AddDays(-10), new CashAmount(-2000 * 25, Currencies.USD), null));
// Margin remaining 75k + used 25k + initial margin 25k - 5k free buying power percent (5% of 100k)
Assert.AreEqual(120000, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Sell));
// Margin remaining 75k - 5k free buying power percent
Assert.AreEqual(70000, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
var actual = algo.CalculateOrderQuantity(_symbol, -1m * model.GetLeverage(security));
// ((100k - 5) * -2 * 0.95 * 0.9975 - (50k holdings)) / 25 - 1 order due to fees
Assert.AreEqual(-9580m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual * 1.0025m, security, algo));
}
[Test]
public void FreeBuyingPowerPercentMarginAccountWithShortHoldings_Equity()
{
var algo = GetAlgorithm();
algo.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
var security = InitAndGetSecurity(algo, 5, SecurityType.Equity);
var requiredFreeBuyingPowerPercent = 0.05m;
var model = security.BuyingPowerModel = new SecurityMarginModel(2, requiredFreeBuyingPowerPercent);
security.Holdings.SetHoldings(25, -2000);
security.SettlementModel.ApplyFunds(new ApplyFundsSettlementModelParameters(algo.Portfolio, security, DateTime.UtcNow.AddDays(-10), new CashAmount(2000 * 25, Currencies.USD), null));
// Margin remaining 75k + used 25k + initial margin 25k - 5k free buying power percent (5% of 100k)
Assert.AreEqual(120000, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
// Margin remaining 75k - 5k free buying power percent
Assert.AreEqual(70000, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Sell));
var actual = algo.CalculateOrderQuantity(_symbol, 1m * model.GetLeverage(security));
// ((100k - 5) * 2 * 0.95 * 0.9975 - (-50k holdings)) / 25 - 1 order due to fees
Assert.AreEqual(9580m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
Assert.IsFalse(HasSufficientBuyingPowerForOrder(actual * 1.0025m, security, algo));
}
[Test]
public void FreeBuyingPowerPercentDefault_Option()
{
const decimal price = 25m;
const decimal underlyingPrice = 25m;
var tz = TimeZones.NewYork;
var equity = new QuantConnect.Securities.Equity.Equity(
SecurityExchangeHours.AlwaysOpen(tz),
new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Minute, tz, tz, true, false, false),
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
);
equity.SetMarketPrice(new Tick { Value = underlyingPrice });
var optionPutSymbol = Symbol.CreateOption(Symbols.SPY, Market.USA, OptionStyle.American, OptionRight.Put, 207m, new DateTime(2015, 02, 27));
var security = new Option(
SecurityExchangeHours.AlwaysOpen(tz),
new SubscriptionDataConfig(typeof(TradeBar), optionPutSymbol, Resolution.Minute, tz, tz, true, false, false),
new Cash(Currencies.USD, 0, 1m),
new OptionSymbolProperties("", Currencies.USD, 100, 0.01m, 1),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null
);
security.SetMarketPrice(new Tick { Value = price });
security.Underlying = equity;
var algo = GetAlgorithm();
security.SetLocalTimeKeeper(algo.TimeKeeper.GetLocalTimeKeeper(tz));
var actual = security.BuyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(
new GetMaximumOrderQuantityForTargetBuyingPowerParameters(algo.Portfolio, security, 1, 0)).Quantity;
// (100000 * 1) / (25 * 100 contract multiplier) - 1 order due to fees
Assert.AreEqual(39m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
Assert.AreEqual(algo.Portfolio.Cash, security.BuyingPowerModel.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
}
[Test]
public void FreeBuyingPowerPercentAppliesForCashAccount_Option()
{
var algo = GetAlgorithm();
algo.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash);
var security = InitAndGetSecurity(algo, 5, SecurityType.Option);
var requiredFreeBuyingPowerPercent = 0.05m;
var model = security.BuyingPowerModel = new SecurityMarginModel(1, requiredFreeBuyingPowerPercent);
var actual = algo.CalculateOrderQuantity(_symbol, 1m * model.GetLeverage(security));
// (100000 * 1 * 0.95) / (25 * 100 contract multiplier) - 1 order due to fees
Assert.AreEqual(37m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
var expectedBuyingPower = algo.Portfolio.Cash * (1 - requiredFreeBuyingPowerPercent);
Assert.AreEqual(expectedBuyingPower, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
}
[Test]
public void FreeBuyingPowerPercentAppliesForMarginAccount_Option()
{
var algo = GetAlgorithm();
algo.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
var security = InitAndGetSecurity(algo, 5, SecurityType.Option);
var requiredFreeBuyingPowerPercent = 0.05m;
var model = security.BuyingPowerModel = new SecurityMarginModel(2, requiredFreeBuyingPowerPercent);
var actual = algo.CalculateOrderQuantity(_symbol, 1m * model.GetLeverage(security));
// (100000 * 2 * 0.95) / (25 * 100 contract multiplier) - 1 order due to fees
Assert.AreEqual(75m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
var expectedBuyingPower = algo.Portfolio.Cash * (1 - requiredFreeBuyingPowerPercent);
Assert.AreEqual(expectedBuyingPower, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
}
[Test]
public void FreeBuyingPowerPercentDefault_Future()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 5, SecurityType.Future, "ES", time: new DateTime(2020, 1, 27));
var model = security.BuyingPowerModel;
var actual = algo.CalculateOrderQuantity(_symbol, 1m * model.GetLeverage(security));
// (100000 * 1 * 0.9975 ) / 6600 - 1 order due to fees
Assert.AreEqual(13m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
Assert.AreEqual(algo.Portfolio.Cash, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
}
[Test]
public void FreeBuyingPowerPercentAppliesForCashAccount_Future()
{
var algo = GetAlgorithm();
algo.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash);
var security = InitAndGetSecurity(algo, 5, SecurityType.Future);
var requiredFreeBuyingPowerPercent = 0.05m;
var model = security.BuyingPowerModel = new SecurityMarginModel(1, requiredFreeBuyingPowerPercent);
var actual = algo.CalculateOrderQuantity(_symbol, 1m * model.GetLeverage(security));
// ((100000 - 5) * 1 * 0.95 * 0.9975 / (25 * 50)
Assert.AreEqual(75m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
var expectedBuyingPower = algo.Portfolio.Cash * (1 - requiredFreeBuyingPowerPercent);
Assert.AreEqual(expectedBuyingPower, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
}
[Test]
public void FreeBuyingPowerPercentAppliesForMarginAccount_Future()
{
var algo = GetAlgorithm();
algo.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
var security = InitAndGetSecurity(algo, 5, SecurityType.Future);
var requiredFreeBuyingPowerPercent = 0.05m;
var model = security.BuyingPowerModel = new SecurityMarginModel(2, requiredFreeBuyingPowerPercent);
var actual = algo.CalculateOrderQuantity(_symbol, 1m * model.GetLeverage(security));
// ((100000 - 5) * 2 * 0.95 * 0.9975 / (25 * 50)
Assert.AreEqual(151m, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
var expectedBuyingPower = algo.Portfolio.Cash * (1 - requiredFreeBuyingPowerPercent);
Assert.AreEqual(expectedBuyingPower, model.GetBuyingPower(algo.Portfolio, security, OrderDirection.Buy));
}
[TestCase(0)]
[TestCase(10000)]
public void NonAccountCurrency_GetBuyingPower(decimal nonAccountCurrencyCash)
{
var algo = GetAlgorithm();
algo.Portfolio.CashBook.Clear();
algo.Portfolio.SetAccountCurrency("EUR");
algo.Portfolio.SetCash(10000);
algo.Portfolio.SetCash(Currencies.USD, nonAccountCurrencyCash, 0.88m);
var security = InitAndGetSecurity(algo, 0);
Assert.AreEqual(10000m + algo.Portfolio.CashBook[Currencies.USD].ValueInAccountCurrency,
algo.Portfolio.TotalPortfolioValue);
var quantity = security.BuyingPowerModel.GetBuyingPower(
new BuyingPowerParameters(algo.Portfolio, security, OrderDirection.Buy)).Value;
Assert.AreEqual(10000m + algo.Portfolio.CashBook[Currencies.USD].ValueInAccountCurrency,
quantity);
}
[Test]
public void NonAccountCurrencyFees()
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 0);
algo.SetCash("EUR", 0, 100);
security.FeeModel = new NonAccountCurrencyCustomFeeModel();
// ((100000 - 100 * 100) * 2 * 0.9975 / (25)
var actual = algo.CalculateOrderQuantity(_symbol, 1m * security.BuyingPowerModel.GetLeverage(security));
Assert.AreEqual(7182m, actual);
// ((100000 - 100 * 100) * 2 / (25)
var quantity = security.BuyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(
algo.Portfolio, security, 1m, 0).Quantity;
Assert.AreEqual(7200m, quantity);
// the maximum order quantity can be executed
Assert.IsTrue(HasSufficientBuyingPowerForOrder(quantity, security, algo)); ;
}
[TestCase(1)]
[TestCase(-1)]
public void GetMaximumOrderQuantityForTargetDeltaBuyingPower_NoHoldings(int side)
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 5);
// we use our entire buying power
var buyingPower = algo.Portfolio.MarginRemaining * side;
var actual = security.BuyingPowerModel.GetMaximumOrderQuantityForDeltaBuyingPower(
new GetMaximumOrderQuantityForDeltaBuyingPowerParameters(algo.Portfolio,
security,
buyingPower,
0)).Quantity;
// (100000 * 2 ) / 25 =8k - 1 fees
Assert.AreEqual(7999 * side, actual);
}
[TestCase(100, 510, false)]
[TestCase(-100, 510, false)]
[TestCase(-100, 50000, true)]
[TestCase(100, -510, false)]
[TestCase(-100, -510, false)]
[TestCase(100, -50000, true)]
public void GetMaximumOrderQuantityForTargetDeltaBuyingPower_WithHoldings(decimal quantity, decimal buyingPowerDelta, bool invertsSide)
{
// TPV = 100k
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 0);
// SPY @ $25 * Quantity Shares = Holdings
// Quantity = 100 -> 2500; TPV = 102500
// Quantity = -100 -> -2500; TPV = 97500
security.Holdings.SetHoldings(security.Price, quantity);
// Used Buying Power = Holdings / Leverage
// Target BP = Used BP + buyingPowerDelta
// Target Holdings = Target BP / Unit
// Max Order For Delta BP = Target Holdings - Current Holdings
// EX. -100 Quantity, 510 BP Delta.
// Used BP = -2500 / 2 = -1250
// Target BP = -1250 + 510 = -740
// Target Holdings = -740 / 12.5 = -59.2 -> ~-59
// Max Order = -59 - (-100) = 41
var actual = security.BuyingPowerModel.GetMaximumOrderQuantityForDeltaBuyingPower(
new GetMaximumOrderQuantityForDeltaBuyingPowerParameters(algo.Portfolio,
security,
buyingPowerDelta,
0)).Quantity;
// Calculate expected using logic above
var targetBuyingPower = ((quantity * (security.Price / security.Leverage)) + buyingPowerDelta);
var targetHoldings = (targetBuyingPower / (security.Price / security.Leverage));
targetHoldings -= (targetHoldings % security.SymbolProperties.LotSize);
var expectedQuantity = targetHoldings - quantity;
Assert.AreEqual(expectedQuantity, actual);
Assert.IsTrue(HasSufficientBuyingPowerForOrder(actual, security, algo));
if (invertsSide)
{
Assert.AreNotEqual(Math.Sign(quantity), Math.Sign(actual));
}
}
[TestCase(true, 1, 1)]
[TestCase(true, 1, -1)]
[TestCase(true, -1, -1)]
[TestCase(true, -1, 1)]
// reducing the position to target 0 is valid
[TestCase(false, 0, -1)]
[TestCase(false, 0, 1)]
public void NegativeMarginRemaining(bool isError, int target, int side)
{
var algo = GetAlgorithm();
var security = InitAndGetSecurity(algo, 5);
security.Holdings.SetHoldings(security.Price, 1000 * side);
algo.Portfolio.CashBook.Add(algo.AccountCurrency, -100000, 1);
var fakeOrderProcessor = new FakeOrderProcessor();
algo.Transactions.SetOrderProcessor(fakeOrderProcessor);
Assert.IsTrue(algo.Portfolio.MarginRemaining < 0);
var quantity = security.BuyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(
new GetMaximumOrderQuantityForTargetBuyingPowerParameters(algo.Portfolio,
security,
target * side,
0)).Quantity;
if (!isError)
{
Assert.AreEqual(1000 * side * -1, quantity);
}
else
{
// even if we don't have margin 'GetMaximumOrderQuantityForTargetBuyingPower' doesn't care
Assert.AreNotEqual(0, quantity);
}
var order = new MarketOrder(security.Symbol, quantity, new DateTime(2020, 1, 1));
fakeOrderProcessor.AddTicket(order.ToOrderTicket(algo.Transactions));
var actual = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(
new HasSufficientBuyingPowerForOrderParameters(algo.Portfolio,
security,
order));
Assert.AreEqual(!isError, actual.IsSufficient);
}
private static QCAlgorithm GetAlgorithm()
{
SymbolCache.Clear();
// Initialize algorithm
var algo = new QCAlgorithm();
algo.SetFinishedWarmingUp();
_fakeOrderProcessor = new FakeOrderProcessor();
algo.Transactions.SetOrderProcessor(_fakeOrderProcessor);
return algo;
}
private static Security InitAndGetSecurity(QCAlgorithm algo, decimal fee, SecurityType securityType = SecurityType.Equity, string symbol = "SPY", DateTime? time = null)
{
algo.SubscriptionManager.SetDataManager(new DataManagerStub(algo));
Security security;
if (securityType == SecurityType.Equity)
{
security = algo.AddEquity(symbol);
_symbol = security.Symbol;
}
else if (securityType == SecurityType.Option)
{
security = algo.AddOption(symbol);
_symbol = security.Symbol;
}
else if (securityType == SecurityType.Future)
{
security = algo.AddFuture(symbol == "SPY" ? "ES" : symbol);
_symbol = security.Symbol;
}
else
{
throw new Exception("SecurityType not implemented");
}
security.FeeModel = new ConstantFeeModel(fee);
Update(security, 25, time);
return security;
}
private static void Update(Security security, decimal close, DateTime? time = null)
{
security.SetMarketPrice(new TradeBar
{
Time = time ?? DateTime.Now,
Symbol = security.Symbol,
Open = close,
High = close,
Low = close,
Close = close
});
}
private bool HasSufficientBuyingPowerForOrder(decimal orderQuantity, Security security, IAlgorithm algo)
{
var order = new MarketOrder(security.Symbol, orderQuantity, DateTime.UtcNow);
_fakeOrderProcessor.AddTicket(order.ToOrderTicket(algo.Transactions));
var hashSufficientBuyingPower = security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(algo.Portfolio,
security, new MarketOrder(security.Symbol, orderQuantity, DateTime.UtcNow));
return hashSufficientBuyingPower.IsSufficient;
}
internal class NonAccountCurrencyCustomFeeModel : FeeModel
{
public string FeeCurrency = "EUR";
public decimal FeeAmount = 100m;
public override OrderFee GetOrderFee(OrderFeeParameters parameters)
{
return new OrderFee(new CashAmount(FeeAmount, FeeCurrency));
}
}
}
}