Skip to content
Open
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
69 changes: 65 additions & 4 deletions Engine/TransactionHandlers/BrokerageTransactionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -993,9 +993,7 @@ private OrderResponse HandleUpdateOrderRequest(UpdateOrderRequest request)
isClosedOrderUpdate = true;
}

// rounds off the order towards 0 to the nearest multiple of lot size
security ??= _algorithm.Securities[order.Symbol];
order.Quantity = RoundOffOrder(order, security);

// verify that our current brokerage can actually update the order
BrokerageMessageEvent message;
Expand All @@ -1011,10 +1009,39 @@ private OrderResponse HandleUpdateOrderRequest(UpdateOrderRequest request)
return response;
}

// If the order is not part of a ComboLegLimit update, validate sufficient buying power
if (order.GroupOrderManager == null)
// Validate the proposed order without changing the order held by the transaction handler. For combo orders,
// the buying power model must see every leg with the proposed group-level update applied.
if (order.GroupOrderManager != null)
{
if (!order.TryGetGroupOrders(GetComboOrderLeg, out var comboOrders))
{
var errorMessage = $"Unable to update order with id {request.OrderId} because its combo group is incomplete.";
_algorithm.Error(errorMessage);
return OrderResponse.Error(request, OrderResponseErrorCode.ProcessingError, errorMessage);
}

var updatedOrders = CloneGroupOrders(comboOrders);
var updatedOrder = updatedOrders.Single(o => o.Id == order.Id);
// Round the candidate rather than the live order so a rejected update has no side effects.
updatedOrder.Quantity = RoundOffOrder(updatedOrder, security);
updatedOrder.ApplyUpdateOrderRequest(request);
if (!updatedOrders.TryGetGroupOrdersSecurities(_algorithm.Portfolio, out var securities))
{
var errorMessage = $"Unable to update order with id {request.OrderId} because a combo leg security is missing.";
_algorithm.Error(errorMessage);
return OrderResponse.Error(request, OrderResponseErrorCode.ProcessingError, errorMessage);
}

if (!HasSufficientBuyingPowerForOrders(updatedOrder, request, out var validationResult, updatedOrders, securities))
{
return validationResult;
}
}
else
{
var updatedOrder = order.Clone();
// Round the candidate rather than the live order so a rejected update has no side effects.
updatedOrder.Quantity = RoundOffOrder(updatedOrder, security);
updatedOrder.ApplyUpdateOrderRequest(request);
if (!HasSufficientBuyingPowerForOrders(updatedOrder, request, out var validationResult))
{
Expand All @@ -1023,6 +1050,8 @@ private OrderResponse HandleUpdateOrderRequest(UpdateOrderRequest request)
}

// modify the values of the order object
// rounds off the order towards 0 to the nearest multiple of lot size
order.Quantity = RoundOffOrder(order, security);
order.ApplyUpdateOrderRequest(request);

// rounds the order prices
Expand Down Expand Up @@ -1917,6 +1946,38 @@ private Order GetComboOrderLeg(int orderId)
return order;
}

/// <summary>
/// Creates isolated copies of all orders in a combo for a what-if buying power check.
/// Combo orders share their group manager, so the manager must be copied as well before applying an update.
/// </summary>
private static List<Order> CloneGroupOrders(List<Order> orders)
{
var groupOrderManager = orders[0].GroupOrderManager;
var clonedGroupOrderManager = new GroupOrderManager(
groupOrderManager.Id,
groupOrderManager.Count,
groupOrderManager.Quantity,
groupOrderManager.LimitPrice);

lock (groupOrderManager.OrderIds)
{
foreach (var orderId in groupOrderManager.OrderIds)
{
clonedGroupOrderManager.OrderIds.Add(orderId);
}
}

var clonedOrders = new List<Order>(orders.Count);
foreach (var order in orders)
{
var clonedOrder = order.Clone();
clonedOrder.GroupOrderManager = clonedGroupOrderManager;
clonedOrders.Add(clonedOrder);
}

return clonedOrders;
}

private void InvalidateOrders(List<Order> orders, string message)
{
for (var i = 0; i < orders.Count; i++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,96 @@ public void UpdateOrderRequestShouldWork()
Assert.IsTrue(_algorithm.OrderEvents[1].Status == OrderStatus.UpdateSubmitted);
}

[Test]
public void ComboOrderUpdateShouldRejectInsufficientBuyingPowerWithoutMutatingTheGroup()
{
_transactionHandler = new TestBrokerageTransactionHandler();
using var brokerage = new NoSubmitTestBrokerage(_algorithm);
_transactionHandler.Initialize(_algorithm, brokerage, new BacktestingResultHandler());

var security1 = (Security)_algorithm.AddEquity("SPY");
var security2 = (Security)_algorithm.AddEquity("AAPL");
security1.SetMarketPrice(new Tick(DateTime.UtcNow, security1.Symbol, 100, 100));
security2.SetMarketPrice(new Tick(DateTime.UtcNow, security2.Symbol, 100, 100));

var firstBuyingPowerModel = new Mock<IBuyingPowerModel>();
firstBuyingPowerModel
.Setup(model => model.HasSufficientBuyingPowerForOrder(It.IsAny<HasSufficientBuyingPowerForOrderParameters>()))
.Returns(new HasSufficientBuyingPowerForOrderResult(true));
security1.SetBuyingPowerModel(firstBuyingPowerModel.Object);

var secondBuyingPowerModel = new Mock<IBuyingPowerModel>();
secondBuyingPowerModel
.Setup(model => model.HasSufficientBuyingPowerForOrder(It.IsAny<HasSufficientBuyingPowerForOrderParameters>()))
.Returns((HasSufficientBuyingPowerForOrderParameters parameters) =>
new HasSufficientBuyingPowerForOrderResult(parameters.Order.Quantity <= 1, "insufficient combo buying power"));
security2.SetBuyingPowerModel(secondBuyingPowerModel.Object);

var groupOrderManager = new GroupOrderManager(1, 2, 1);
var firstOrder = new ComboMarketOrder(security1.Symbol, 1, DateTime.UtcNow, groupOrderManager);
var secondOrder = new ComboMarketOrder(security2.Symbol, 1, DateTime.UtcNow, groupOrderManager);
_transactionHandler.AddOpenOrder(firstOrder, _algorithm);
_transactionHandler.AddOpenOrder(secondOrder, _algorithm);

var updateRequest = new UpdateOrderRequest(DateTime.UtcNow, firstOrder.Id, new UpdateOrderFields { Quantity = 2 });
_transactionHandler.Process(updateRequest);
_transactionHandler.HandleOrderRequest(updateRequest);

Assert.IsTrue(updateRequest.Response.IsError);
Assert.AreEqual(OrderResponseErrorCode.BrokerageFailedToUpdateOrder, updateRequest.Response.ErrorCode);
Assert.AreEqual(1, groupOrderManager.Quantity);
Assert.AreEqual(1, firstOrder.Quantity);
Assert.AreEqual(1, secondOrder.Quantity);
secondBuyingPowerModel.Verify(model => model.HasSufficientBuyingPowerForOrder(
It.Is<HasSufficientBuyingPowerForOrderParameters>(parameters => parameters.Order.Quantity == 2)), Times.Once);
Assert.IsEmpty(brokerage.UpdatedOrders);
}

[Test]
public void ComboOrderUpdateShouldValidateTheCompleteGroupAndUpdateBrokerage()
{
_transactionHandler = new TestBrokerageTransactionHandler();
using var brokerage = new NoSubmitTestBrokerage(_algorithm);
_transactionHandler.Initialize(_algorithm, brokerage, new BacktestingResultHandler());

var security1 = (Security)_algorithm.AddEquity("SPY");
var security2 = (Security)_algorithm.AddEquity("AAPL");
security1.SetMarketPrice(new Tick(DateTime.UtcNow, security1.Symbol, 100, 100));
security2.SetMarketPrice(new Tick(DateTime.UtcNow, security2.Symbol, 100, 100));

var firstBuyingPowerModel = new Mock<IBuyingPowerModel>();
firstBuyingPowerModel
.Setup(model => model.HasSufficientBuyingPowerForOrder(It.IsAny<HasSufficientBuyingPowerForOrderParameters>()))
.Returns(new HasSufficientBuyingPowerForOrderResult(true));
security1.SetBuyingPowerModel(firstBuyingPowerModel.Object);

var secondBuyingPowerModel = new Mock<IBuyingPowerModel>();
secondBuyingPowerModel
.Setup(model => model.HasSufficientBuyingPowerForOrder(It.IsAny<HasSufficientBuyingPowerForOrderParameters>()))
.Returns(new HasSufficientBuyingPowerForOrderResult(true));
security2.SetBuyingPowerModel(secondBuyingPowerModel.Object);

var groupOrderManager = new GroupOrderManager(1, 2, 1);
var firstOrder = new ComboMarketOrder(security1.Symbol, 1, DateTime.UtcNow, groupOrderManager);
var secondOrder = new ComboMarketOrder(security2.Symbol, 1, DateTime.UtcNow, groupOrderManager);
_transactionHandler.AddOpenOrder(firstOrder, _algorithm);
_transactionHandler.AddOpenOrder(secondOrder, _algorithm);

var updateRequest = new UpdateOrderRequest(DateTime.UtcNow, firstOrder.Id, new UpdateOrderFields { Quantity = 2 });
_transactionHandler.Process(updateRequest);
_transactionHandler.HandleOrderRequest(updateRequest);

Assert.IsTrue(updateRequest.Response.IsSuccess);
Assert.AreEqual(2, groupOrderManager.Quantity);
Assert.AreEqual(2, firstOrder.Quantity);
Assert.AreEqual(2, secondOrder.Quantity);
secondBuyingPowerModel.Verify(model => model.HasSufficientBuyingPowerForOrder(
It.Is<HasSufficientBuyingPowerForOrderParameters>(parameters => parameters.Order.Quantity == 2)), Times.Once);
Assert.That(brokerage.UpdatedOrders, Has.Count.EqualTo(1));
Assert.AreEqual(firstOrder.Id, brokerage.UpdatedOrders[0].Id);
Assert.AreEqual(2, brokerage.UpdatedOrders[0].Quantity);
}

[Test]
public void UpdatePartiallyFilledOrderRequestShouldWork()
{
Expand Down Expand Up @@ -3039,6 +3129,8 @@ internal class NoSubmitTestBrokerage : Brokerage
{
private BacktestingBrokerage _underlyingBrokerage;

public List<Order> UpdatedOrders { get; } = new();

public override bool IsConnected => _underlyingBrokerage.IsConnected;

public NoSubmitTestBrokerage(IAlgorithm algorithm) : base("NoSubmitTestBrokerage")
Expand All @@ -3051,6 +3143,7 @@ public override bool PlaceOrder(Order order)
}
public override bool UpdateOrder(Order order)
{
UpdatedOrders.Add(order);
return true;
}
public void PublishOrderEvent(OrderEvent orderEvent)
Expand Down