diff --git a/Common/Orders/Fees/OrderFee.cs b/Common/Orders/Fees/OrderFee.cs
index 0d3d63203029..a05fd0bc32a2 100644
--- a/Common/Orders/Fees/OrderFee.cs
+++ b/Common/Orders/Fees/OrderFee.cs
@@ -13,6 +13,7 @@
* limitations under the License.
*/
+using Newtonsoft.Json;
using ProtoBuf;
using QuantConnect.Securities;
@@ -30,6 +31,18 @@ public class OrderFee
[ProtoMember(1)]
public CashAmount Value { get; set; }
+ ///
+ /// Gets the order fee amount. Synonym of .
+ ///
+ [JsonIgnore]
+ public decimal Amount => Value.Amount;
+
+ ///
+ /// Gets the order fee currency. Synonym of .
+ ///
+ [JsonIgnore]
+ public string Currency => Value.Currency;
+
///
/// Initializes a new instance of the class
///
diff --git a/Common/Orders/GroupOrderExtensions.cs b/Common/Orders/GroupOrderExtensions.cs
index 21848d8c62b1..a0e1deb13c58 100644
--- a/Common/Orders/GroupOrderExtensions.cs
+++ b/Common/Orders/GroupOrderExtensions.cs
@@ -156,5 +156,35 @@ public static decimal GetGroupQuantityByEachLegQuantity(IEnumerable leg
_ => throw new ArgumentException($"Unsupported {nameof(OrderDirection)} value: '{orderDirection}'.", nameof(orderDirection))
};
}
+
+ ///
+ /// Gets the id of the group of orders the given combo order leg tickets belong to, null if there are no tickets
+ ///
+ /// The combo order leg tickets
+ /// The group order manager id of the first ticket
+ public static int? GetGroupId(this IEnumerable tickets)
+ {
+ return tickets.FirstOrDefault()?.SubmitRequest?.GroupOrderManager?.Id;
+ }
+
+ ///
+ /// Determines whether every one of the given combo order leg tickets is filled
+ ///
+ /// The combo order leg tickets
+ /// True if there is at least one ticket and all of them are filled
+ public static bool IsFilled(this IEnumerable tickets)
+ {
+ var any = false;
+ foreach (var ticket in tickets)
+ {
+ any = true;
+ if (ticket.Status != OrderStatus.Filled)
+ {
+ return false;
+ }
+ }
+
+ return any;
+ }
}
}
diff --git a/Common/Orders/OptionExercise/DefaultExerciseModel.cs b/Common/Orders/OptionExercise/DefaultExerciseModel.cs
index cb15f9a2925b..5e91bccfee09 100644
--- a/Common/Orders/OptionExercise/DefaultExerciseModel.cs
+++ b/Common/Orders/OptionExercise/DefaultExerciseModel.cs
@@ -52,7 +52,8 @@ public virtual IEnumerable OptionExercise(Option option, OptionExerc
)
{
IsAssignment = isAssignment,
- IsInTheMoney = inTheMoney
+ IsInTheMoney = inTheMoney,
+ OrderType = order.Type
};
// TODO : Support Manual Exercise of OTM contracts [ inTheMoney = false ]
@@ -70,7 +71,7 @@ public virtual IEnumerable OptionExercise(Option option, OptionExerc
exerciseQuantity,
OrderFee.Zero,
isAssignment ? Messages.DefaultExerciseModel.OptionAssignment : Messages.DefaultExerciseModel.OptionExercise
- ) { IsInTheMoney = true };
+ ) { IsInTheMoney = true, OrderType = order.Type };
}
}
}
diff --git a/Common/Orders/Order.cs b/Common/Orders/Order.cs
index b98c26895eb5..8b48ee7d9a0e 100644
--- a/Common/Orders/Order.cs
+++ b/Common/Orders/Order.cs
@@ -232,6 +232,13 @@ public bool IsMarketable
[JsonProperty(PropertyName = "groupOrderManager", DefaultValueHandling = DefaultValueHandling.Ignore)]
public GroupOrderManager GroupOrderManager { get; set; }
+ ///
+ /// The id of the group of orders this order belongs to if it is a combo order leg, null otherwise.
+ /// Synonym of .
+ ///
+ [JsonIgnore]
+ public int? GroupId => GroupOrderManager?.Id;
+
///
/// The contingency of this order, if any: the set of contingent orders it belongs to (OCO, OTO, OUO, brackets)
/// and the links defining how it relates to the rest of the orders in the set
diff --git a/Common/Orders/OrderEvent.cs b/Common/Orders/OrderEvent.cs
index 32bc2e2c7fc2..ba9794666aa4 100644
--- a/Common/Orders/OrderEvent.cs
+++ b/Common/Orders/OrderEvent.cs
@@ -62,6 +62,16 @@ public class OrderEvent
[ProtoMember(4)]
public DateTime UtcTime { get; set; }
+ ///
+ /// The date and time of this event (UTC). Synonym of
+ ///
+ [JsonIgnore]
+ public DateTime Time
+ {
+ get { return UtcTime; }
+ set { UtcTime = value; }
+ }
+
///
/// Status message of the order.
///
@@ -112,6 +122,21 @@ public decimal FillQuantity
[ProtoMember(10)]
public OrderDirection Direction { get; set; }
+ ///
+ /// The type of the order this event comes from. Null for events built from the order id alone
+ /// until the transaction handler stamps it, and for payloads that predate this property
+ ///
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [ProtoMember(20)]
+ public OrderType? OrderType { get; set; }
+
+ ///
+ /// The id of the group of orders the order belongs to if it is a combo order leg, null otherwise
+ ///
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [ProtoMember(21)]
+ public int? GroupId { get; set; }
+
///
/// Any message from the exchange.
///
@@ -119,6 +144,13 @@ public decimal FillQuantity
[ProtoMember(11)]
public string Message { get; set; }
+ ///
+ /// The tag of the order this event comes from. Null for events built from the order id alone
+ /// until the transaction handler stamps it, and for deserialized events
+ ///
+ [JsonIgnore]
+ public string Tag { get; internal set; }
+
///
/// True if the order event is an assignment
///
@@ -290,6 +322,9 @@ public OrderEvent(Order order, DateTime utcTime, OrderFee orderFee, string messa
Status = order.Status;
Quantity = order.Quantity;
Direction = order.Direction;
+ OrderType = order.Type;
+ GroupId = order.GroupId;
+ Tag = order.Tag;
//Initialize to zero, manually set fill quantity
FillQuantity = 0;
@@ -348,7 +383,7 @@ public static OrderEvent FromSerialized(SerializedOrderEvent serializedOrderEven
var orderEvent = new OrderEvent(serializedOrderEvent.OrderId,
symbol,
- DateTime.SpecifyKind(Time.UnixTimeStampToDateTime(serializedOrderEvent.Time), DateTimeKind.Utc),
+ DateTime.SpecifyKind(QuantConnect.Time.UnixTimeStampToDateTime(serializedOrderEvent.Time), DateTimeKind.Utc),
serializedOrderEvent.Status,
serializedOrderEvent.Direction,
serializedOrderEvent.FillPrice,
@@ -358,6 +393,8 @@ public static OrderEvent FromSerialized(SerializedOrderEvent serializedOrderEven
{
IsAssignment = serializedOrderEvent.IsAssignment,
IsInTheMoney = serializedOrderEvent.IsInTheMoney,
+ OrderType = serializedOrderEvent.OrderType,
+ GroupId = serializedOrderEvent.GroupId,
LimitPrice = serializedOrderEvent.LimitPrice,
StopPrice = serializedOrderEvent.StopPrice,
FillPriceCurrency = serializedOrderEvent.FillPriceCurrency,
diff --git a/Common/Orders/Serialization/SerializedOrderEvent.cs b/Common/Orders/Serialization/SerializedOrderEvent.cs
index f1df7d9e60b9..297edb0640b4 100644
--- a/Common/Orders/Serialization/SerializedOrderEvent.cs
+++ b/Common/Orders/Serialization/SerializedOrderEvent.cs
@@ -114,6 +114,18 @@ public class SerializedOrderEvent
[JsonProperty("direction"), JsonConverter(typeof(StringEnumConverter), true)]
public OrderDirection Direction { get; set; }
+ ///
+ /// The type of the order this event comes from. Null for payloads that predate this property
+ ///
+ [JsonProperty("orderType", DefaultValueHandling = DefaultValueHandling.Ignore), JsonConverter(typeof(StringEnumConverter), true)]
+ public OrderType? OrderType { get; set; }
+
+ ///
+ /// The id of the group of orders the order belongs to if it is a combo order leg, null otherwise
+ ///
+ [JsonProperty("groupId", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ public int? GroupId { get; set; }
+
///
/// Any message from the exchange.
///
@@ -179,6 +191,8 @@ public SerializedOrderEvent(OrderEvent orderEvent, string algorithmId)
FillPriceCurrency = orderEvent.FillPriceCurrency;
FillQuantity = orderEvent.FillQuantity;
Direction = orderEvent.Direction;
+ OrderType = orderEvent.OrderType;
+ GroupId = orderEvent.GroupId;
Message = orderEvent.Message;
IsAssignment = orderEvent.IsAssignment;
IsInTheMoney = orderEvent.IsInTheMoney;
diff --git a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs
index 7cb7a0b378f9..4e993c860492 100644
--- a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs
+++ b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs
@@ -1296,6 +1296,9 @@ private void HandleOrderEvents(List orderEvents)
// but the order event symbol is the underlying security, not the option contract.
orders.Add(new OpenOrderState(order, ticket, security != null && security.Symbol == orderEvent.Symbol ? security : _algorithm.Securities[orderEvent.Symbol]));
orderEvent.Ticket = ticket;
+ orderEvent.OrderType = order.Type;
+ orderEvent.GroupId = order.GroupId;
+ orderEvent.Tag = order.Tag;
}
var fillsToProcess = new List(orderEvents.Count);
diff --git a/Tests/Common/Orders/Fees/OrderFeesTests.cs b/Tests/Common/Orders/Fees/OrderFeesTests.cs
index a0babded5bdb..82848b56c30d 100644
--- a/Tests/Common/Orders/Fees/OrderFeesTests.cs
+++ b/Tests/Common/Orders/Fees/OrderFeesTests.cs
@@ -15,6 +15,7 @@
*/
using System;
+using Newtonsoft.Json;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Orders;
@@ -48,6 +49,39 @@ public void OrderFeeZeroCanBeConvertedToAccountCurrency()
Assert.AreEqual(0, result3);
}
+ [Test]
+ public void AmountAndCurrencyAreSynonymsOfTheValueAmountAndCurrency()
+ {
+ var orderFee = new OrderFee(new CashAmount(12.5m, Currencies.USD));
+
+ Assert.AreEqual(12.5m, orderFee.Amount);
+ Assert.AreEqual(orderFee.Value.Amount, orderFee.Amount);
+ Assert.AreEqual(Currencies.USD, orderFee.Currency);
+ Assert.AreEqual(orderFee.Value.Currency, orderFee.Currency);
+ // The synonym is not serialized, only Value is
+ Assert.AreEqual("{\"Value\":{\"Amount\":12.5,\"Currency\":\"USD\"}}", JsonConvert.SerializeObject(orderFee));
+ }
+
+ [Test]
+ public void AmountIsAccessibleFromPython()
+ {
+ var orderFee = new OrderFee(new CashAmount(12.5m, Currencies.USD));
+
+ using (Py.GIL())
+ {
+ var module = PyModule.FromString("testModule",
+ @"
+from AlgorithmImports import *
+
+def get_amount(order_fee: OrderFee):
+ return order_fee.amount
+");
+
+ dynamic getAmount = module.GetAttr("get_amount");
+ Assert.AreEqual(12.5m, (decimal)getAmount(orderFee));
+ }
+ }
+
[Test]
public void OrderFeeIsNumericFromPython()
{
diff --git a/Tests/Common/Orders/GroupOrderExtensionsTests.cs b/Tests/Common/Orders/GroupOrderExtensionsTests.cs
index 94ddbf01edde..8c8f4e8b0de2 100644
--- a/Tests/Common/Orders/GroupOrderExtensionsTests.cs
+++ b/Tests/Common/Orders/GroupOrderExtensionsTests.cs
@@ -50,5 +50,50 @@ public void GetGroupQuantityByEachLegQuantityShouldReturnExpectedGCD(Leg[] legs,
}
private static Leg[] CreateLegs(params int[] quantities) => [.. quantities.Select(q => Leg.Create(null, q))];
+
+ [Test]
+ public void GetGroupIdOfTheLegTickets()
+ {
+ var groupOrderManager = new GroupOrderManager(33, 2, 10);
+ var tickets = new List
+ {
+ CreateLegTicket(groupOrderManager, Symbols.SPY_C_192_Feb19_2016, 10, OrderStatus.Submitted, 1),
+ CreateLegTicket(groupOrderManager, Symbols.SPY_P_192_Feb19_2016, -10, OrderStatus.Submitted, 2)
+ };
+
+ Assert.AreEqual(33, tickets.GetGroupId());
+ Assert.IsNull(new List().GetGroupId());
+ }
+
+ [Test]
+ public void IsFilledOnlyWhenEveryLegTicketIsFilled()
+ {
+ var groupOrderManager = new GroupOrderManager(33, 2, 10);
+ var tickets = new List
+ {
+ CreateLegTicket(groupOrderManager, Symbols.SPY_C_192_Feb19_2016, 10, OrderStatus.Filled, 1),
+ CreateLegTicket(groupOrderManager, Symbols.SPY_P_192_Feb19_2016, -10, OrderStatus.PartiallyFilled, 2)
+ };
+ Assert.IsFalse(tickets.IsFilled());
+
+ tickets[1] = CreateLegTicket(groupOrderManager, Symbols.SPY_P_192_Feb19_2016, -10, OrderStatus.Filled, 2);
+ Assert.IsTrue(tickets.IsFilled());
+
+ Assert.IsFalse(new List().IsFilled());
+ }
+
+ private static OrderTicket CreateLegTicket(GroupOrderManager groupOrderManager, Symbol symbol, decimal quantity, OrderStatus status, int orderId)
+ {
+ var request = new SubmitOrderRequest(OrderType.ComboMarket, symbol.SecurityType, symbol, quantity, 0, 0,
+ new DateTime(2016, 2, 16, 11, 53, 30), "", groupOrderManager: groupOrderManager);
+ request.SetOrderId(orderId);
+
+ var ticket = new OrderTicket(null, request);
+ var order = Order.CreateOrder(request);
+ order.Status = status;
+ ticket.SetOrder(order);
+
+ return ticket;
+ }
}
}
diff --git a/Tests/Common/Orders/OrderEventTests.cs b/Tests/Common/Orders/OrderEventTests.cs
index b0ec564f98e0..b482a46c3df4 100644
--- a/Tests/Common/Orders/OrderEventTests.cs
+++ b/Tests/Common/Orders/OrderEventTests.cs
@@ -30,7 +30,7 @@ public class OrderEventTests
[Test]
public void JsonIgnores()
{
- var order = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.UtcNow);
+ var order = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.UtcNow, "entry");
var json = JsonConvert.SerializeObject(new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero),
new OrderEventJsonConverter("id"));
@@ -38,6 +38,7 @@ public void JsonIgnores()
Assert.IsFalse(json.Contains("LimitPrice", StringComparison.InvariantCulture));
Assert.IsFalse(json.Contains("StopPrice", StringComparison.InvariantCulture));
Assert.IsFalse(json.Contains(value: "IsInTheMoney", StringComparison.InvariantCulture));
+ Assert.IsFalse(json.Contains("Tag", StringComparison.InvariantCulture));
json = JsonConvert.SerializeObject(new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero, "This is a message")
{
@@ -51,16 +52,98 @@ public void JsonIgnores()
Assert.IsTrue(json.Contains("LimitPrice", StringComparison.InvariantCulture));
Assert.IsTrue(json.Contains("StopPrice", StringComparison.InvariantCulture));
Assert.IsTrue(json.Contains(value: "IsInTheMoney", StringComparison.InvariantCulture));
+ // The order tag and the Time synonym are not serialized, only Message and UtcTime are
+ Assert.IsFalse(json.Contains("\"Tag\"", StringComparison.InvariantCulture));
+ Assert.IsFalse(json.Contains("entry", StringComparison.InvariantCulture));
+ Assert.IsFalse(json.Contains("\"Time\"", StringComparison.InvariantCulture));
}
[Test]
- public void RoundTripSerialization([Values] bool isInTheMoney)
+ public void TagIsTheOrderTagAndNotTheMessage()
+ {
+ var order = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.UtcNow, "entry");
+ var orderEvent = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero, "Filled at stale price");
+
+ Assert.AreEqual("entry", orderEvent.Tag);
+ Assert.AreEqual("Filled at stale price", orderEvent.Message);
+
+ // events built from the order id alone do not know the order
+ var brokerageEvent = new OrderEvent(order.Id, order.Symbol, DateTime.UtcNow, OrderStatus.Filled, order.Direction, 1, 1,
+ OrderFee.Zero, "Filled at stale price");
+ Assert.IsNull(brokerageEvent.Tag);
+ Assert.AreEqual("Filled at stale price", brokerageEvent.Message);
+ }
+
+ [Test]
+ public void TimeIsASynonymOfUtcTime()
{
var order = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.UtcNow);
+ var utcTime = new DateTime(2024, 3, 27, 18, 45, 19, DateTimeKind.Utc);
+ var orderEvent = new OrderEvent(order, utcTime, OrderFee.Zero);
+
+ Assert.AreEqual(utcTime, orderEvent.Time);
+
+ orderEvent.Time = utcTime.AddHours(1);
+
+ Assert.AreEqual(utcTime.AddHours(1), orderEvent.UtcTime);
+ }
+
+ [Test]
+ public void TagAndTimeAreAccessibleFromPython()
+ {
+ var order = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.UtcNow, "entry");
+ var utcTime = new DateTime(2024, 3, 27, 18, 45, 19, DateTimeKind.Utc);
+ var orderEvent = new OrderEvent(order, utcTime, OrderFee.Zero, "Filled at stale price");
+
+ using (Py.GIL())
+ {
+ var module = PyModule.FromString("testModule",
+ @"
+from AlgorithmImports import *
+
+def get_tag_message_and_time(order_event: OrderEvent):
+ return order_event.tag, order_event.message, order_event.time == order_event.utc_time
+");
+
+ dynamic getTagMessageAndTime = module.GetAttr("get_tag_message_and_time");
+ var result = getTagMessageAndTime(orderEvent);
+
+ Assert.AreEqual("entry", (string)result[0]);
+ Assert.AreEqual("Filled at stale price", (string)result[1]);
+ Assert.IsTrue((bool)result[2]);
+ }
+ }
+
+ [Test]
+ public void OrderTypeIsTakenFromTheOrder()
+ {
+ var order = new StopLimitOrder(Symbols.BTCUSD, 0.123m, 100m, 101m, DateTime.UtcNow);
+ var orderEvent = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero);
+
+ Assert.AreEqual(OrderType.StopLimit, orderEvent.OrderType);
+ Assert.AreEqual(order.Type, orderEvent.OrderType);
+ }
+
+ [Test]
+ public void GroupIdIsTakenFromTheOrder()
+ {
+ var order = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.UtcNow);
+ Assert.IsNull(new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero).GroupId);
+
+ var comboOrder = new ComboMarketOrder(Symbols.BTCUSD, 0.123m, DateTime.UtcNow, new GroupOrderManager(33, 2, 10));
+ Assert.AreEqual(33, new OrderEvent(comboOrder, DateTime.UtcNow, OrderFee.Zero).GroupId);
+ }
+
+ [Test]
+ public void RoundTripSerialization([Values] bool isInTheMoney)
+ {
+ var order = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.UtcNow, "entry");
var orderEvent = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero)
{
Message = "Pepe",
Status = OrderStatus.PartiallyFilled,
+ OrderType = OrderType.StopLimit,
+ GroupId = 7,
StopPrice = 1,
LimitPrice = 2,
FillPrice = 11,
@@ -79,8 +162,15 @@ public void RoundTripSerialization([Values] bool isInTheMoney)
Assert.IsFalse(serializeObject.Contains("order-fee-amount", StringComparison.InvariantCulture));
Assert.IsFalse(serializeObject.Contains("order-fee-currency", StringComparison.InvariantCulture));
Assert.AreEqual(isInTheMoney, serializeObject.Contains("isInTheMoney", StringComparison.InvariantCulture));
+ Assert.IsTrue(serializeObject.Contains("\"orderType\":\"stopLimit\"", StringComparison.InvariantCulture));
+ Assert.IsTrue(serializeObject.Contains("\"groupId\":7", StringComparison.InvariantCulture));
var deserializeObject = JsonConvert.DeserializeObject(serializeObject, converter);
+ Assert.AreEqual(orderEvent.OrderType, deserializeObject.OrderType);
+ Assert.AreEqual(orderEvent.GroupId, deserializeObject.GroupId);
+ // the order tag is not part of the payload, it comes from the order
+ Assert.AreEqual("entry", orderEvent.Tag);
+ Assert.IsNull(deserializeObject.Tag);
Assert.AreEqual(orderEvent.Symbol, deserializeObject.Symbol);
Assert.AreEqual(orderEvent.StopPrice, deserializeObject.StopPrice);
@@ -169,6 +259,24 @@ public void BackwardsCompatibleDeserialization()
Assert.AreEqual(orderEvent.OrderFee.Value.Amount, deserializeObject.OrderFee.Value.Amount);
Assert.AreEqual(orderEvent.OrderFee.Value.Currency, deserializeObject.OrderFee.Value.Currency);
+ // payloads that predate the order type must not be read back as market orders
+ Assert.IsNull(deserializeObject.OrderType);
+ Assert.IsNull(deserializeObject.GroupId);
+ }
+
+ [Test]
+ public void NullOrderTypeAndGroupIdAreNotSerialized()
+ {
+ var order = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.UtcNow);
+ var orderEvent = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero) { OrderType = null };
+
+ var json = JsonConvert.SerializeObject(orderEvent, new OrderEventJsonConverter("id"));
+ Assert.IsFalse(json.Contains("orderType", StringComparison.InvariantCulture));
+ Assert.IsFalse(json.Contains("groupId", StringComparison.InvariantCulture));
+
+ json = JsonConvert.SerializeObject(orderEvent);
+ Assert.IsFalse(json.Contains("OrderType", StringComparison.InvariantCulture));
+ Assert.IsFalse(json.Contains("GroupId", StringComparison.InvariantCulture));
}
[Test]
diff --git a/Tests/Common/Orders/OrderTests.cs b/Tests/Common/Orders/OrderTests.cs
index 585fdf48ee6d..617720c6e22c 100644
--- a/Tests/Common/Orders/OrderTests.cs
+++ b/Tests/Common/Orders/OrderTests.cs
@@ -238,6 +238,16 @@ private static TestCaseData[] GetValueTestParameters()
}.Select(x => new TestCaseData(x).SetName(x.Name)).ToArray();
}
+ [Test]
+ public void GroupIdComesFromTheGroupOrderManager()
+ {
+ var order = new MarketOrder(Symbols.SPY, 10, DateTime.UtcNow);
+ Assert.IsNull(order.GroupId);
+
+ var comboOrder = new ComboMarketOrder(Symbols.SPY, 10, DateTime.UtcNow, new GroupOrderManager(33, 2, 10));
+ Assert.AreEqual(33, comboOrder.GroupId);
+ }
+
public class ValueTestParameters
{
public string Name { get; init; }
diff --git a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
index c15dc19feff8..160dcf92d271 100644
--- a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
+++ b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
@@ -1465,6 +1465,83 @@ public void SlowOnOrderEventHandlerWarnsOnlyOnce()
Assert.AreEqual(1, _algorithm.DebugMessages.Count(message => message.Contains("OnOrderEvent")));
}
+ [Test]
+ public void BrokerageOrderEventsCarryTheOrderTypeAndTag()
+ {
+ _transactionHandler = new TestBrokerageTransactionHandler();
+ using var brokerage = new NoSubmitTestBrokerage(_algorithm);
+ _transactionHandler.Initialize(_algorithm, brokerage, new BacktestingResultHandler());
+
+ var security = _algorithm.Securities[_symbol];
+ var price = 1.12m;
+ security.SetMarketPrice(new Tick(DateTime.Now, security.Symbol, price, price, price));
+ var orderRequest = new SubmitOrderRequest(OrderType.Limit, security.Type, security.Symbol, 1000, 0, 1.11m, DateTime.Now, "entry");
+ _algorithm.Transactions.SetOrderProcessor(_transactionHandler);
+ _transactionHandler.Process(orderRequest);
+ _transactionHandler.PumpPendingRequests();
+
+ // brokerages building events from the order id alone do not know the order type or tag
+ var order = _algorithm.Transactions.GetOpenOrders().Single();
+ brokerage.PublishOrderEvent(new OrderEvent(order.Id, order.Symbol, _algorithm.UtcTime, OrderStatus.Submitted,
+ order.Direction, 0, 0, OrderFee.Zero, "Brokerage message"));
+
+ var orderEvent = _algorithm.OrderEvents.Single(orderEvent => orderEvent.Status == OrderStatus.Submitted);
+ Assert.AreEqual(OrderType.Limit, orderEvent.OrderType);
+ Assert.AreEqual(order.Type, orderEvent.OrderType);
+ Assert.IsNull(orderEvent.GroupId);
+ Assert.AreEqual("entry", orderEvent.Tag);
+ Assert.AreEqual("Brokerage message", orderEvent.Message);
+
+ // events after a tag update carry the new tag
+ _transactionHandler.Process(new UpdateOrderRequest(DateTime.Now, order.Id, new UpdateOrderFields { Tag = "exit" }));
+ _transactionHandler.PumpPendingRequests();
+ brokerage.PublishOrderEvent(new OrderEvent(order.Id, order.Symbol, _algorithm.UtcTime, OrderStatus.Filled,
+ order.Direction, 1.11m, 1000, OrderFee.Zero));
+
+ orderEvent = _algorithm.OrderEvents.Single(orderEvent => orderEvent.Status == OrderStatus.Filled);
+ Assert.AreEqual("exit", orderEvent.Tag);
+ }
+
+ [Test]
+ public void BrokerageOrderEventsCarryTheGroupId()
+ {
+ var algorithm = new TestAlgorithm { HistoryProvider = new EmptyHistoryProvider() };
+ algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
+ algorithm.SetBrokerageModel(BrokerageName.Default);
+ algorithm.SetCash(1000000);
+ var index = algorithm.AddIndex("SPX").Symbol;
+ var option = Symbol.CreateOption(index, Market.USA, OptionStyle.European, OptionRight.Put, 300m, new DateTime(2024, 05, 16));
+ algorithm.AddIndexOptionContract(option);
+ algorithm.SetFinishedWarmingUp();
+
+ _transactionHandler = new TestBrokerageTransactionHandler();
+ using var brokerage = new NoSubmitTestBrokerage(algorithm);
+ _transactionHandler.Initialize(algorithm, brokerage, new BacktestingResultHandler());
+ algorithm.Transactions.SetOrderProcessor(_transactionHandler);
+
+ var dateTime = new DateTime(2024, 05, 14, 12, 0, 0);
+ var groupOrderManager = new GroupOrderManager(1, 2, 10);
+ foreach (var (symbol, price) in new[] { (index, 1.12m), (option, 330.12m) })
+ {
+ var security = algorithm.Securities[symbol];
+ security.SetMarketPrice(new Tick(dateTime, symbol, price, price, price));
+ algorithm.Transactions.AddOrder(new SubmitOrderRequest(OrderType.ComboMarket, security.Type, symbol, 10, 0, 0, dateTime, "",
+ groupOrderManager: groupOrderManager));
+ }
+ _transactionHandler.PumpPendingRequests();
+
+ var order = algorithm.Transactions.GetOpenOrders().Single(openOrder => openOrder.Symbol == option);
+ Assert.AreEqual(1, order.GroupOrderManager.Id);
+
+ // brokerages building events from the order id alone do not know the order group
+ brokerage.PublishOrderEvent(new OrderEvent(order.Id, order.Symbol, algorithm.UtcTime, OrderStatus.PartiallyFilled,
+ order.Direction, 330.12m, 1, OrderFee.Zero));
+
+ var orderEvent = algorithm.OrderEvents.Single(orderEvent => orderEvent.Status == OrderStatus.PartiallyFilled);
+ Assert.AreEqual(OrderType.ComboMarket, orderEvent.OrderType);
+ Assert.AreEqual(order.GroupOrderManager.Id, orderEvent.GroupId);
+ }
+
[Test]
public void CancelOrderRequestShouldFailForFilledOrder()
{