diff --git a/Common/Securities/Future/FutureMarginModel.cs b/Common/Securities/Future/FutureMarginModel.cs index d6b662585405..44170124a979 100644 --- a/Common/Securities/Future/FutureMarginModel.cs +++ b/Common/Securities/Future/FutureMarginModel.cs @@ -111,7 +111,8 @@ public override void SetLeverage(Security security, decimal leverage) public override GetMaximumOrderQuantityResult GetMaximumOrderQuantityForTargetBuyingPower( GetMaximumOrderQuantityForTargetBuyingPowerParameters parameters) { - if (Math.Abs(parameters.TargetBuyingPower) > 1) + if (Math.Abs(parameters.TargetBuyingPower) > 1 + && !IsReducingExistingExposure(parameters)) { throw new InvalidOperationException( "Futures do not allow specifying a leveraged target, since they are traded using margin which already is leveraged. " + @@ -120,6 +121,31 @@ public override GetMaximumOrderQuantityResult GetMaximumOrderQuantityForTargetBu return base.GetMaximumOrderQuantityForTargetBuyingPower(parameters); } + /// + /// Futures can temporarily exceed 100% target buying power during margin call liquidation requests. + /// In these cases we allow leveraged targets only when they reduce existing exposure. + /// + private bool IsReducingExistingExposure(GetMaximumOrderQuantityForTargetBuyingPowerParameters parameters) + { + var totalPortfolioValue = parameters.Portfolio.TotalPortfolioValue; + if (totalPortfolioValue == 0) + { + return false; + } + + var currentMargin = this.GetInitialMarginRequirement( + parameters.Security, parameters.Security.Holdings.Quantity + ); + if (currentMargin == 0) + { + return false; + } + + var targetMargin = parameters.TargetBuyingPower * totalPortfolioValue; + return Math.Sign(targetMargin) == Math.Sign(currentMargin) + && Math.Abs(targetMargin) <= Math.Abs(currentMargin); + } + /// /// Gets the total margin required to execute the specified order in units of the account currency including fees /// diff --git a/Tests/Common/Securities/FutureMarginBuyingPowerModelTests.cs b/Tests/Common/Securities/FutureMarginBuyingPowerModelTests.cs index 67ae203500e8..19694de12d0f 100644 --- a/Tests/Common/Securities/FutureMarginBuyingPowerModelTests.cs +++ b/Tests/Common/Securities/FutureMarginBuyingPowerModelTests.cs @@ -396,6 +396,43 @@ public void GetMaximumOrderQuantityForTargetBuyingPower_ThrowsForInvalidTarget(d 0))); } + [Test] + public void GetMaximumOrderQuantityForDeltaBuyingPower_AllowsReducingLeveragedTarget() + { + var algorithm = new QCAlgorithm(); + algorithm.SetFinishedWarmingUp(); + algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm)); + + var ticker = QuantConnect.Securities.Futures.Financials.EuroDollar; + var futureSecurity = algorithm.AddFuture(ticker); + Update(futureSecurity, 100, algorithm); + + algorithm.Portfolio.SetCash(1000); + futureSecurity.Holdings.SetHoldings(100, 10); + algorithm.Portfolio.InvalidateTotalPortfolioValue(); + + var usedBuyingPower = futureSecurity.BuyingPowerModel.GetReservedBuyingPowerForPosition( + new ReservedBuyingPowerForPositionParameters(futureSecurity)).AbsoluteUsedBuyingPower; + var totalPortfolioValue = algorithm.Portfolio.TotalPortfolioValue; + var targetBuyingPower = 1.1m * totalPortfolioValue; + var deltaBuyingPower = targetBuyingPower - usedBuyingPower; + + Assert.Greater(Math.Abs(usedBuyingPower / totalPortfolioValue), 1m); + Assert.Less(deltaBuyingPower, 0m); + + Assert.DoesNotThrow(() => + { + var result = futureSecurity.BuyingPowerModel.GetMaximumOrderQuantityForDeltaBuyingPower( + new GetMaximumOrderQuantityForDeltaBuyingPowerParameters( + algorithm.Portfolio, + futureSecurity, + deltaBuyingPower, + 0 + )); + Assert.Less(result.Quantity, 0m); + }); + } + [TestCase(1)] [TestCase(0.5)] [TestCase(-1)]