Skip to content
13 changes: 13 additions & 0 deletions Common/Orders/Fees/OrderFee.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* limitations under the License.
*/

using Newtonsoft.Json;
using ProtoBuf;
using QuantConnect.Securities;

Expand All @@ -30,6 +31,18 @@ public class OrderFee
[ProtoMember(1)]
public CashAmount Value { get; set; }

/// <summary>
/// Gets the order fee amount. Synonym of <see cref="Value"/>.<see cref="CashAmount.Amount"/>
/// </summary>
[JsonIgnore]
public decimal Amount => Value.Amount;

/// <summary>
/// Gets the order fee currency. Synonym of <see cref="Value"/>.<see cref="CashAmount.Currency"/>
/// </summary>
[JsonIgnore]
public string Currency => Value.Currency;

/// <summary>
/// Initializes a new instance of the <see cref="OrderFee"/> class
/// </summary>
Expand Down
30 changes: 30 additions & 0 deletions Common/Orders/GroupOrderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,35 @@ public static decimal GetGroupQuantityByEachLegQuantity(IEnumerable<decimal> leg
_ => throw new ArgumentException($"Unsupported {nameof(OrderDirection)} value: '{orderDirection}'.", nameof(orderDirection))
};
}

/// <summary>
/// Gets the id of the group of orders the given combo order leg tickets belong to, null if there are no tickets
/// </summary>
/// <param name="tickets">The combo order leg tickets</param>
/// <returns>The group order manager id of the first ticket</returns>
public static int? GetGroupId(this IEnumerable<OrderTicket> tickets)
Comment thread
jhonabreul marked this conversation as resolved.
Resolved
{
return tickets.FirstOrDefault()?.SubmitRequest?.GroupOrderManager?.Id;
}

/// <summary>
/// Determines whether every one of the given combo order leg tickets is filled
/// </summary>
/// <param name="tickets">The combo order leg tickets</param>
/// <returns>True if there is at least one ticket and all of them are filled</returns>
public static bool IsFilled(this IEnumerable<OrderTicket> tickets)
{
var any = false;
foreach (var ticket in tickets)
{
any = true;
if (ticket.Status != OrderStatus.Filled)
{
return false;
}
}

return any;
}
}
}
5 changes: 3 additions & 2 deletions Common/Orders/OptionExercise/DefaultExerciseModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public virtual IEnumerable<OrderEvent> OptionExercise(Option option, OptionExerc
)
{
IsAssignment = isAssignment,
IsInTheMoney = inTheMoney
IsInTheMoney = inTheMoney,
OrderType = order.Type
};

// TODO : Support Manual Exercise of OTM contracts [ inTheMoney = false ]
Expand All @@ -70,7 +71,7 @@ public virtual IEnumerable<OrderEvent> OptionExercise(Option option, OptionExerc
exerciseQuantity,
OrderFee.Zero,
isAssignment ? Messages.DefaultExerciseModel.OptionAssignment : Messages.DefaultExerciseModel.OptionExercise
) { IsInTheMoney = true };
) { IsInTheMoney = true, OrderType = order.Type };
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions Common/Orders/Order.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,13 @@ public bool IsMarketable
[JsonProperty(PropertyName = "groupOrderManager", DefaultValueHandling = DefaultValueHandling.Ignore)]
public GroupOrderManager GroupOrderManager { get; set; }

/// <summary>
/// The id of the group of orders this order belongs to if it is a combo order leg, null otherwise.
/// Synonym of <see cref="GroupOrderManager"/>.<see cref="Orders.GroupOrderManager.Id"/>
/// </summary>
[JsonIgnore]
public int? GroupId => GroupOrderManager?.Id;

/// <summary>
/// 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
Expand Down
39 changes: 38 additions & 1 deletion Common/Orders/OrderEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ public class OrderEvent
[ProtoMember(4)]
public DateTime UtcTime { get; set; }

/// <summary>
/// The date and time of this event (UTC). Synonym of <see cref="UtcTime"/>
/// </summary>
[JsonIgnore]
public DateTime Time
{
get { return UtcTime; }
set { UtcTime = value; }
}

/// <summary>
/// Status message of the order.
/// </summary>
Expand Down Expand Up @@ -112,13 +122,35 @@ public decimal FillQuantity
[ProtoMember(10)]
public OrderDirection Direction { get; set; }

/// <summary>
/// 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
/// </summary>
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
[ProtoMember(20)]
public OrderType? OrderType { get; set; }

/// <summary>
/// The id of the group of orders the order belongs to if it is a combo order leg, null otherwise
/// </summary>
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
[ProtoMember(21)]
public int? GroupId { get; set; }

/// <summary>
/// Any message from the exchange.
/// </summary>
[DefaultValue(""), JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
[ProtoMember(11)]
public string Message { get; set; }

/// <summary>
/// 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
/// </summary>
[JsonIgnore]
public string Tag { get; internal set; }

/// <summary>
/// True if the order event is an assignment
/// </summary>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions Common/Orders/Serialization/SerializedOrderEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ public class SerializedOrderEvent
[JsonProperty("direction"), JsonConverter(typeof(StringEnumConverter), true)]
public OrderDirection Direction { get; set; }

/// <summary>
/// The type of the order this event comes from. Null for payloads that predate this property
/// </summary>
[JsonProperty("orderType", DefaultValueHandling = DefaultValueHandling.Ignore), JsonConverter(typeof(StringEnumConverter), true)]
public OrderType? OrderType { get; set; }

/// <summary>
/// The id of the group of orders the order belongs to if it is a combo order leg, null otherwise
/// </summary>
[JsonProperty("groupId", DefaultValueHandling = DefaultValueHandling.Ignore)]
public int? GroupId { get; set; }

/// <summary>
/// Any message from the exchange.
/// </summary>
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions Engine/TransactionHandlers/BrokerageTransactionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,9 @@ private void HandleOrderEvents(List<OrderEvent> 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<OrderEvent>(orderEvents.Count);
Expand Down
34 changes: 34 additions & 0 deletions Tests/Common/Orders/Fees/OrderFeesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

using System;
using Newtonsoft.Json;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Orders;
Expand Down Expand Up @@ -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()
{
Expand Down
45 changes: 45 additions & 0 deletions Tests/Common/Orders/GroupOrderExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrderTicket>
{
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<OrderTicket>().GetGroupId());
}

[Test]
public void IsFilledOnlyWhenEveryLegTicketIsFilled()
{
var groupOrderManager = new GroupOrderManager(33, 2, 10);
var tickets = new List<OrderTicket>
{
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<OrderTicket>().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;
}
}
}
Loading
Loading