Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion Common/Securities/Future/FutureMarginModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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. " +
Expand All @@ -120,6 +121,31 @@ public override GetMaximumOrderQuantityResult GetMaximumOrderQuantityForTargetBu
return base.GetMaximumOrderQuantityForTargetBuyingPower(parameters);
}

/// <summary>
/// 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.
/// </summary>
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);
}

/// <summary>
/// Gets the total margin required to execute the specified order in units of the account currency including fees
/// </summary>
Expand Down
37 changes: 37 additions & 0 deletions Tests/Common/Securities/FutureMarginBuyingPowerModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down