diff --git a/Algorithm.CSharp/TimeInForceAlgorithm.cs b/Algorithm.CSharp/TimeInForceAlgorithm.cs index 054caefefe4a..e8a9510bc785 100644 --- a/Algorithm.CSharp/TimeInForceAlgorithm.cs +++ b/Algorithm.CSharp/TimeInForceAlgorithm.cs @@ -13,6 +13,8 @@ * limitations under the License. */ +using System; +using System.Collections.Generic; using QuantConnect.Data; using QuantConnect.Orders; @@ -27,8 +29,10 @@ namespace QuantConnect.Algorithm.CSharp public class TimeInForceAlgorithm : QCAlgorithm { private Symbol _symbol; - private OrderTicket _gtcOrderTicket; - private OrderTicket _dayOrderTicket; + private OrderTicket _gtcOrderTicket1, _gtcOrderTicket2; + private OrderTicket _dayOrderTicket1, _dayOrderTicket2; + private OrderTicket _gtdOrderTicket1, _gtdOrderTicket2; + private readonly Dictionary _expectedOrderStatuses = new Dictionary(); /// /// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized. @@ -41,7 +45,7 @@ public override void Initialize() // The default time in force setting for all orders is GoodTilCancelled (GTC), // uncomment this line to set a different time in force. - // We currently only support GTC and DAY. + // We currently only support GTC, DAY, GTD. // DefaultOrderProperties.TimeInForce = TimeInForce.Day; _symbol = AddEquity("SPY", Resolution.Minute).Symbol; @@ -53,22 +57,51 @@ public override void Initialize() /// Slice object keyed by symbol containing the stock data public override void OnData(Slice data) { - if (_gtcOrderTicket == null) + if (_gtcOrderTicket1 == null) { - // This order has a default time in force of GoodTilCanceled, - // it will never expire and will not be canceled automatically. + // These GTC orders will never expire and will not be canceled automatically. DefaultOrderProperties.TimeInForce = TimeInForce.GoodTilCanceled; - _gtcOrderTicket = LimitOrder(_symbol, 10, 160m); + + // this order will not be filled before the end of the backtest + _gtcOrderTicket1 = LimitOrder(_symbol, 10, 100m); + _expectedOrderStatuses.Add(_gtcOrderTicket1.OrderId, OrderStatus.Submitted); + + // this order will be filled before the end of the backtest + _gtcOrderTicket2 = LimitOrder(_symbol, 10, 160m); + _expectedOrderStatuses.Add(_gtcOrderTicket2.OrderId, OrderStatus.Filled); } - if (_dayOrderTicket == null) + if (_dayOrderTicket1 == null) { - // This order will expire at market close, - // if not filled by then it will be canceled automatically. + // These DAY orders will expire at market close, + // if not filled by then they will be canceled automatically. DefaultOrderProperties.TimeInForce = TimeInForce.Day; - _dayOrderTicket = LimitOrder(_symbol, 10, 160m); + + // this order will not be filled before market close and will be canceled + _dayOrderTicket1 = LimitOrder(_symbol, 10, 160m); + _expectedOrderStatuses.Add(_dayOrderTicket1.OrderId, OrderStatus.Canceled); + + // this order will be filled before market close + _dayOrderTicket2 = LimitOrder(_symbol, 10, 180m); + _expectedOrderStatuses.Add(_dayOrderTicket2.OrderId, OrderStatus.Filled); + } + + if (_gtdOrderTicket1 == null) + { + // These GTD orders will expire on October 10th at market close, + // if not filled by then they will be canceled automatically. + + DefaultOrderProperties.TimeInForce = TimeInForce.GoodTilDate(new DateTime(2013, 10, 10)); + + // this order will not be filled before expiry and will be canceled + _gtdOrderTicket1 = LimitOrder(_symbol, 10, 100m); + _expectedOrderStatuses.Add(_gtdOrderTicket1.OrderId, OrderStatus.Canceled); + + // this order will be filled before expiry + _gtdOrderTicket2 = LimitOrder(_symbol, 10, 160m); + _expectedOrderStatuses.Add(_gtdOrderTicket2.OrderId, OrderStatus.Filled); } } @@ -82,5 +115,23 @@ public override void OnOrderEvent(OrderEvent orderEvent) Debug($"{Time} {orderEvent}"); } + /// + /// End of algorithm run event handler. This method is called at the end of a backtest or live trading operation. + /// + public override void OnEndOfAlgorithm() + { + foreach (var kvp in _expectedOrderStatuses) + { + var orderId = kvp.Key; + var expectedStatus = kvp.Value; + + var order = Transactions.GetOrderById(orderId); + + if (order.Status != expectedStatus) + { + throw new Exception($"Invalid status for order {orderId} - Expected: {expectedStatus}, actual: {order.Status}"); + } + } + } } } diff --git a/Algorithm.Python/TimeInForceAlgorithm.py b/Algorithm.Python/TimeInForceAlgorithm.py index 389bac13ef1e..5f7d14aae2fa 100644 --- a/Algorithm.Python/TimeInForceAlgorithm.py +++ b/Algorithm.Python/TimeInForceAlgorithm.py @@ -20,6 +20,8 @@ from QuantConnect import * from QuantConnect.Algorithm import * from QuantConnect.Orders import * +from QuantConnect.Orders.TimeInForces import * +from datetime import datetime ### ### Demonstration algorithm of time in force order settings. @@ -41,31 +43,70 @@ def Initialize(self): # We currently only support GTC and DAY. # self.DefaultOrderProperties.TimeInForce = TimeInForce.Day; - self.symbol = self.AddEquity("SPY", Resolution.Second).Symbol + self.symbol = self.AddEquity("SPY", Resolution.Minute).Symbol - self.gtcOrderTicket = None - self.dayOrderTicket = None + self.gtcOrderTicket1 = None + self.gtcOrderTicket2 = None + self.dayOrderTicket1 = None + self.dayOrderTicket2 = None + self.gtdOrderTicket1 = None + self.gtdOrderTicket2 = None + self.expectedOrderStatuses = {} # OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. # Arguments: # data: Slice object keyed by symbol containing the stock data def OnData(self, data): - if self.gtcOrderTicket is None: - # This order has a default time in force of GoodTilCanceled, - # it will never expire and will not be canceled automatically. + if self.gtcOrderTicket1 is None: + # These GTC orders will never expire and will not be canceled automatically. self.DefaultOrderProperties.TimeInForce = TimeInForce.GoodTilCanceled - self.gtcOrderTicket = self.LimitOrder(self.symbol, 10, 160) - if self.dayOrderTicket is None: - # This order will expire at market close, - # if not filled by then it will be canceled automatically. + # this order will not be filled before the end of the backtest + self.gtcOrderTicket1 = self.LimitOrder(self.symbol, 10, 100) + self.expectedOrderStatuses[self.gtcOrderTicket1.OrderId] = OrderStatus.Submitted + + # this order will be filled before the end of the backtest + self.gtcOrderTicket2 = self.LimitOrder(self.symbol, 10, 160) + self.expectedOrderStatuses[self.gtcOrderTicket2.OrderId] = OrderStatus.Filled + + if self.dayOrderTicket1 is None: + # These DAY orders will expire at market close, + # if not filled by then they will be canceled automatically. self.DefaultOrderProperties.TimeInForce = TimeInForce.Day - self.dayOrderTicket = self.LimitOrder(self.symbol, 10, 160) + + # this order will not be filled before market close and will be canceled + self.dayOrderTicket1 = self.LimitOrder(self.symbol, 10, 160) + self.expectedOrderStatuses[self.dayOrderTicket1.OrderId] = OrderStatus.Canceled + + # this order will be filled before market close + self.dayOrderTicket2 = self.LimitOrder(self.symbol, 10, 180) + self.expectedOrderStatuses[self.dayOrderTicket2.OrderId] = OrderStatus.Filled + + if self.gtdOrderTicket1 is None: + # These GTD orders will expire on October 10th at market close, + # if not filled by then they will be canceled automatically. + + self.DefaultOrderProperties.TimeInForce = TimeInForce.GoodTilDate(datetime(2013, 10, 10)) + + # this order will not be filled before expiry and will be canceled + self.gtdOrderTicket1 = self.LimitOrder(self.symbol, 10, 100) + self.expectedOrderStatuses[self.gtdOrderTicket1.OrderId] = OrderStatus.Canceled + + # this order will be filled before expiry + self.gtdOrderTicket2 = self.LimitOrder(self.symbol, 10, 160) + self.expectedOrderStatuses[self.gtdOrderTicket2.OrderId] = OrderStatus.Filled # Order event handler. This handler will be called for all order events, including submissions, fills, cancellations. # This method can be called asynchronously, ensure you use proper locks on thread-unsafe objects def OnOrderEvent(self, orderEvent): self.Debug(f"{self.Time} {orderEvent}") + + # End of algorithm run event handler. This method is called at the end of a backtest or live trading operation. + def OnEndOfAlgorithm(self): + for orderId, expectedStatus in self.expectedOrderStatuses.items(): + order = self.Transactions.GetOrderById(orderId) + if order.Status != expectedStatus: + raise Exception(f"Invalid status for order {orderId} - Expected: {expectedStatus}, actual: {order.Status}") \ No newline at end of file diff --git a/Brokerages/Backtesting/BacktestingBrokerage.cs b/Brokerages/Backtesting/BacktestingBrokerage.cs index 1bbbe5d305c4..71dc94457a45 100644 --- a/Brokerages/Backtesting/BacktestingBrokerage.cs +++ b/Brokerages/Backtesting/BacktestingBrokerage.cs @@ -20,7 +20,6 @@ using QuantConnect.Interfaces; using QuantConnect.Logging; using QuantConnect.Orders; -using QuantConnect.Orders.TimeInForces; using QuantConnect.Securities; using QuantConnect.Securities.Option; @@ -40,14 +39,6 @@ public class BacktestingBrokerage : Brokerage private readonly object _needsScanLock = new object(); private readonly HashSet _pendingOptionAssignments = new HashSet(); - private readonly Dictionary _timeInForceHandlers = new Dictionary - { - { TimeInForce.GoodTilCanceled, new GoodTilCanceledTimeInForceHandler() }, - { TimeInForce.Day, new DayTimeInForceHandler() }, - // Custom time in force will be renamed to GTD soon and will have its own new handler - { TimeInForce.Custom, new GoodTilCanceledTimeInForceHandler() } - }; - /// /// This is the algorithm under test /// @@ -286,10 +277,8 @@ public void Scan() continue; } - var timeInForceHandler = _timeInForceHandlers[order.TimeInForce]; - // check if the time in force handler allows fills - if (timeInForceHandler.IsOrderExpired(security, order)) + if (order.TimeInForce.IsOrderExpired(security, order)) { OnOrderEvent(new OrderEvent(order, Algorithm.UtcTime, 0m) { @@ -386,7 +375,7 @@ public void Scan() foreach (var fill in fills) { // check if the fill should be emitted - if (!timeInForceHandler.IsFillValid(security, order, fill)) + if (!order.TimeInForce.IsFillValid(security, order, fill)) { break; } diff --git a/Brokerages/InteractiveBrokers/InteractiveBrokersBrokerage.cs b/Brokerages/InteractiveBrokers/InteractiveBrokersBrokerage.cs index 46fc3a824a8a..a9e15abca22f 100644 --- a/Brokerages/InteractiveBrokers/InteractiveBrokersBrokerage.cs +++ b/Brokerages/InteractiveBrokers/InteractiveBrokersBrokerage.cs @@ -35,6 +35,7 @@ using IB = QuantConnect.Brokerages.InteractiveBrokers.Client; using IBApi; using NodaTime; +using QuantConnect.Orders.TimeInForces; using Bar = QuantConnect.Data.Market.Bar; using HistoryRequest = QuantConnect.Data.HistoryRequest; @@ -1626,6 +1627,30 @@ private IBApi.Order ConvertOrder(Order order, Contract contract, int ibOrderId) Rule80A = _agentDescription }; + var gtdTimeInForce = order.TimeInForce as GoodTilDateTimeInForce; + if (gtdTimeInForce != null) + { + DateTime expiryUtc; + if (order.SecurityType == SecurityType.Forex) + { + expiryUtc = gtdTimeInForce.GetForexOrderExpiryDateTime(order); + } + else + { + var exchangeHours = MarketHoursDatabase.FromDataFolder() + .GetExchangeHours(order.Symbol.ID.Market, order.Symbol, order.SecurityType); + + var expiry = exchangeHours.GetNextMarketClose(gtdTimeInForce.Expiry.Date, false); + expiryUtc = expiry.ConvertToUtc(exchangeHours.TimeZone); + } + + // The IB format for the GoodTillDate order property is "yyyymmdd hh:mm:ss xxx" where yyyymmdd and xxx are optional. + // E.g.: 20031126 15:59:00 EST + // If no date is specified, current date is assumed. If no time-zone is specified, local time-zone is assumed. + + ibOrder.GoodTillDate = expiryUtc.ToString("yyyyMMdd HH:mm:ss UTC", CultureInfo.InvariantCulture); + } + var limitOrder = order as LimitOrder; var stopMarketOrder = order as StopMarketOrder; var stopLimitOrder = order as StopLimitOrder; @@ -1750,7 +1775,7 @@ private Order ConvertOrder(IBApi.Order ibOrder, Contract contract) order.BrokerId.Add(ibOrder.OrderId.ToString()); - order.Properties.TimeInForce = ConvertTimeInForce(ibOrder.Tif); + order.Properties.TimeInForce = ConvertTimeInForce(ibOrder.Tif, ibOrder.GoodTillDate); return order; } @@ -1894,15 +1919,15 @@ private static OrderType ConvertOrderType(IBApi.Order order) /// /// Maps TimeInForce from IB to LEAN /// - private static TimeInForce ConvertTimeInForce(string timeInForce) + private static TimeInForce ConvertTimeInForce(string timeInForce, string expiryDateTime) { switch (timeInForce) { case IB.TimeInForce.Day: return TimeInForce.Day; - //case IB.TimeInForce.GoodTillDate: - // return TimeInForce.GoodTilDate; + case IB.TimeInForce.GoodTillDate: + return TimeInForce.GoodTilDate(ParseExpiryDateTime(expiryDateTime)); //case IB.TimeInForce.FillOrKill: // return TimeInForce.FillOrKill; @@ -1917,6 +1942,30 @@ private static TimeInForce ConvertTimeInForce(string timeInForce) } } + private static DateTime ParseExpiryDateTime(string expiryDateTime) + { + // NOTE: we currently ignore the time zone in this method for a couple of reasons: + // - TZ abbreviations are ambiguous and unparsable to a unique time zone + // see this article for more info: + // https://codeblog.jonskeet.uk/2015/05/05/common-mistakes-in-datetime-formatting-and-parsing/ + // - IB seems to also have issues with Daylight Saving Time zones + // Example: an order submitted from Europe with GoodTillDate property set to "20180524 21:00:00 UTC" + // when reading the open orders, the same property will have this value: "20180524 23:00:00 CET" + // which is incorrect: should be CEST (UTC+2) instead of CET (UTC+1) + + // We can ignore this issue, because the method is only called by GetOpenOrders, + // we only call GetOpenOrders during live trading, which means we won't be simulating time in force + // and instead will rely on brokerages to apply TIF properly. + + var parts = expiryDateTime.Split(' '); + if (parts.Length == 3) + { + expiryDateTime = expiryDateTime.Substring(0, expiryDateTime.LastIndexOf(" ", StringComparison.Ordinal)); + } + + return DateTime.ParseExact(expiryDateTime, "yyyyMMdd HH:mm:ss", CultureInfo.InvariantCulture).Date; + } + /// /// Maps TimeInForce from LEAN to IB /// @@ -1931,24 +1980,27 @@ private static string ConvertTimeInForce(Order order) return IB.TimeInForce.Day; } - switch (order.TimeInForce) + if (order.TimeInForce is DayTimeInForce) { - case TimeInForce.Day: - return IB.TimeInForce.Day; + return IB.TimeInForce.Day; + } - //case TimeInForce.GoodTilDate: - // return IB.TimeInForce.GoodTillDate; + if (order.TimeInForce is GoodTilDateTimeInForce) + { + return IB.TimeInForce.GoodTillDate; + } - //case TimeInForce.FillOrKill: - // return IB.TimeInForce.FillOrKill; + //if (order.TimeInForce is FillOrKillTimeInForce) + //{ + // return IB.TimeInForce.FillOrKill; + //} - //case TimeInForce.ImmediateOrCancel: - // return IB.TimeInForce.ImmediateOrCancel; + //if (order.TimeInForce is ImmediateOrCancelTimeInForce) + //{ + // return IB.TimeInForce.ImmediateOrCancel; + //} - case TimeInForce.GoodTilCanceled: - default: - return IB.TimeInForce.GoodTillCancel; - } + return IB.TimeInForce.GoodTillCancel; } /// diff --git a/Brokerages/Oanda/OandaRestApiV1.cs b/Brokerages/Oanda/OandaRestApiV1.cs index 6d6e5f342ebb..60fde57fab31 100644 --- a/Brokerages/Oanda/OandaRestApiV1.cs +++ b/Brokerages/Oanda/OandaRestApiV1.cs @@ -1,11 +1,11 @@ /* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); + * + * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -71,7 +71,7 @@ public override List GetInstrumentList() } /// - /// Gets all open orders on the account. + /// Gets all open orders on the account. /// NOTE: The order objects returned do not have QC order IDs. /// /// The open orders returned from Oanda @@ -770,8 +770,8 @@ private Order ConvertOrder(RestV1.DataType.Order order) qcOrder.Id = orderByBrokerageId.Id; } - qcOrder.Properties.TimeInForce = TimeInForce.Custom; - qcOrder.DurationValue = XmlConvert.ToDateTime(order.expiry, XmlDateTimeSerializationMode.Utc); + var expiry = XmlConvert.ToDateTime(order.expiry, XmlDateTimeSerializationMode.Utc); + qcOrder.Properties.TimeInForce = TimeInForce.GoodTilDate(expiry); qcOrder.Time = XmlConvert.ToDateTime(order.time, XmlDateTimeSerializationMode.Utc); return qcOrder; @@ -842,13 +842,13 @@ private static void PopulateOrderRequestParameters(Order order, Dictionary /// Converts the qc order duration into a tradier order duration /// - protected static TradierOrderDuration GetOrderDuration(TimeInForce duration) + protected static TradierOrderDuration GetOrderDuration(TimeInForce timeInForce) { - switch (duration) + if (timeInForce is GoodTilCanceledTimeInForce) { - case TimeInForce.GoodTilCanceled: - return TradierOrderDuration.GTC; - default: - throw new ArgumentOutOfRangeException(); + return TradierOrderDuration.GTC; } + + if (timeInForce is DayTimeInForce) + { + return TradierOrderDuration.Day; + } + + throw new ArgumentOutOfRangeException(); } /// diff --git a/Common/Brokerages/FxcmBrokerageModel.cs b/Common/Brokerages/FxcmBrokerageModel.cs index 8a737556aa2d..b0ea94b40320 100644 --- a/Common/Brokerages/FxcmBrokerageModel.cs +++ b/Common/Brokerages/FxcmBrokerageModel.cs @@ -126,7 +126,7 @@ public override bool CanSubmitOrder(Security security, Order order, out Brokerag if (order.TimeInForce != TimeInForce.GoodTilCanceled) { message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", - "This model does not support " + order.TimeInForce + " time in force." + "This model does not support " + order.TimeInForce.GetType().Name + " time in force." ); return false; diff --git a/Common/Brokerages/GDAXBrokerageModel.cs b/Common/Brokerages/GDAXBrokerageModel.cs index d1955d02ca9c..bce223bdd6e9 100644 --- a/Common/Brokerages/GDAXBrokerageModel.cs +++ b/Common/Brokerages/GDAXBrokerageModel.cs @@ -148,7 +148,7 @@ public override bool CanSubmitOrder(Security security, Order order, out Brokerag if (order.TimeInForce != TimeInForce.GoodTilCanceled) { message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", - "This model does not support " + order.TimeInForce + " time in force." + "This model does not support " + order.TimeInForce.GetType().Name + " time in force." ); return false; diff --git a/Common/Brokerages/InteractiveBrokersBrokerageModel.cs b/Common/Brokerages/InteractiveBrokersBrokerageModel.cs index 3f712c95611d..bc7d0b7c81b7 100644 --- a/Common/Brokerages/InteractiveBrokersBrokerageModel.cs +++ b/Common/Brokerages/InteractiveBrokersBrokerageModel.cs @@ -15,8 +15,10 @@ using System; using System.Collections.Generic; +using System.Linq; using QuantConnect.Orders; using QuantConnect.Orders.Fees; +using QuantConnect.Orders.TimeInForces; using QuantConnect.Securities; using QuantConnect.Securities.Forex; @@ -27,11 +29,18 @@ namespace QuantConnect.Brokerages /// public class InteractiveBrokersBrokerageModel : DefaultBrokerageModel { + private readonly Type[] _supportedTimeInForces = + { + typeof(GoodTilCanceledTimeInForce), + typeof(DayTimeInForce), + typeof(GoodTilDateTimeInForce) + }; + /// /// Initializes a new instance of the class /// /// The type of account to be modelled, defaults to - /// + /// public InteractiveBrokersBrokerageModel(AccountType accountType = AccountType.Margin) : base(accountType) { @@ -84,10 +93,10 @@ public override bool CanSubmitOrder(Security security, Order order, out Brokerag } // validate time in force - if (order.TimeInForce != TimeInForce.GoodTilCanceled && order.TimeInForce != TimeInForce.Day) + if (!_supportedTimeInForces.Contains(order.TimeInForce.GetType())) { message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", - "This model does not support " + order.TimeInForce + " time in force." + "This model does not support " + order.TimeInForce.GetType().Name + " time in force." ); return false; @@ -166,7 +175,6 @@ CNH China Renminbi (offshore) 160,000 40,000,000 string baseCurrency, quoteCurrency; Forex.DecomposeCurrencyPair(currencyPair, out baseCurrency, out quoteCurrency); - decimal max; ForexCurrencyLimits.TryGetValue(baseCurrency, out max); diff --git a/Common/Brokerages/OandaBrokerageModel.cs b/Common/Brokerages/OandaBrokerageModel.cs index 62e04a08972d..df913b64517e 100644 --- a/Common/Brokerages/OandaBrokerageModel.cs +++ b/Common/Brokerages/OandaBrokerageModel.cs @@ -13,7 +13,6 @@ * limitations under the License. */ -using System; using System.Collections.Generic; using QuantConnect.Orders; using QuantConnect.Orders.Fees; @@ -98,7 +97,7 @@ public override bool CanSubmitOrder(Security security, Order order, out Brokerag if (order.TimeInForce != TimeInForce.GoodTilCanceled) { message = new BrokerageMessageEvent(BrokerageMessageType.Warning, "NotSupported", - "This model does not support " + order.TimeInForce + " time in force." + "This model does not support " + order.TimeInForce.GetType().Name + " time in force." ); return false; @@ -107,21 +106,6 @@ public override bool CanSubmitOrder(Security security, Order order, out Brokerag return true; } - /// - /// Returns true if the brokerage would be able to execute this order at this time assuming - /// market prices are sufficient for the fill to take place. This is used to emulate the - /// brokerage fills in backtesting and paper trading. For example some brokerages may not perform - /// executions during extended market hours. This is not intended to be checking whether or not - /// the exchange is open, that is handled in the Security.Exchange property. - /// - /// The security being traded - /// The order to test for execution - /// True if the brokerage would be able to perform the execution, false otherwise - public override bool CanExecuteOrder(Security security, Order order) - { - return order.DurationValue == DateTime.MaxValue || order.DurationValue <= order.Time.AddMonths(3); - } - /// /// Gets a new fill model that represents this brokerage's fill behavior /// diff --git a/Common/Orders/Order.cs b/Common/Orders/Order.cs index 8b19cad21d7a..a8f36fdff815 100644 --- a/Common/Orders/Order.cs +++ b/Common/Orders/Order.cs @@ -26,6 +26,9 @@ namespace QuantConnect.Orders /// public abstract class Order { + private decimal _quantity; + private decimal _price; + /// /// Order ID. /// @@ -51,8 +54,8 @@ public abstract class Order /// public decimal Price { - get { return price; } - internal set { price = value.Normalize(); } + get { return _price; } + internal set { _price = value.Normalize(); } } /// @@ -90,8 +93,8 @@ public decimal Price /// public decimal Quantity { - get { return quantity; } - internal set { quantity = value.Normalize(); } + get { return _quantity; } + internal set { _quantity = value.Normalize(); } } /// @@ -105,7 +108,7 @@ public decimal Quantity public OrderStatus Status { get; internal set; } /// - /// Order Time In Force - GTC or Day. Day not supported in backtests. + /// Order Time In Force /// public TimeInForce TimeInForce => Properties.TimeInForce; @@ -193,7 +196,6 @@ protected Order() Tag = ""; BrokerId = new List(); ContingentId = 0; - DurationValue = DateTime.MaxValue; Properties = new OrderProperties(); } @@ -216,7 +218,6 @@ protected Order(Symbol symbol, decimal quantity, DateTime time, string tag = "", Tag = tag; BrokerId = new List(); ContingentId = 0; - DurationValue = DateTime.MaxValue; Properties = properties ?? new OrderProperties(); } @@ -269,7 +270,7 @@ public virtual void ApplyUpdateOrderRequest(UpdateOrderRequest request) /// 2 public override string ToString() { - return string.Format("OrderId: {0} {1} {2} order for {3} unit{4} of {5}", Id, Status, Type, Quantity, Quantity == 1 ? "" : "s", Symbol); + return $"OrderId: {Id} {Status} {Type} order for {Quantity} unit{(Quantity == 1 ? "" : "s")} of {Symbol}"; } /// @@ -314,24 +315,31 @@ public static Order CreateOrder(SubmitOrderRequest request) case OrderType.Market: order = new MarketOrder(request.Symbol, request.Quantity, request.Time, request.Tag, request.OrderProperties); break; + case OrderType.Limit: order = new LimitOrder(request.Symbol, request.Quantity, request.LimitPrice, request.Time, request.Tag, request.OrderProperties); break; + case OrderType.StopMarket: order = new StopMarketOrder(request.Symbol, request.Quantity, request.StopPrice, request.Time, request.Tag, request.OrderProperties); break; + case OrderType.StopLimit: order = new StopLimitOrder(request.Symbol, request.Quantity, request.StopPrice, request.LimitPrice, request.Time, request.Tag, request.OrderProperties); break; + case OrderType.MarketOnOpen: order = new MarketOnOpenOrder(request.Symbol, request.Quantity, request.Time, request.Tag, request.OrderProperties); break; + case OrderType.MarketOnClose: order = new MarketOnCloseOrder(request.Symbol, request.Quantity, request.Time, request.Tag, request.OrderProperties); break; + case OrderType.OptionExercise: order = new OptionExerciseOrder(request.Symbol, request.Quantity, request.Time, request.Tag, request.OrderProperties); break; + default: throw new ArgumentOutOfRangeException(); } @@ -343,13 +351,5 @@ public static Order CreateOrder(SubmitOrderRequest request) } return order; } - - /// - /// Order Expiry on a specific UTC time. - /// - public DateTime DurationValue; - - private decimal quantity; - private decimal price; } } diff --git a/Common/Orders/OrderJsonConverter.cs b/Common/Orders/OrderJsonConverter.cs index c249c3faff7b..d1336cfbf938 100644 --- a/Common/Orders/OrderJsonConverter.cs +++ b/Common/Orders/OrderJsonConverter.cs @@ -106,10 +106,9 @@ public static Order CreateOrderFromJObject(JObject jObject) order.ContingentId = jObject["ContingentId"].Value(); var timeInForce = jObject["TimeInForce"] ?? jObject["Duration"]; - if (timeInForce != null) - { - order.Properties.TimeInForce = (TimeInForce)timeInForce.Value(); - } + order.Properties.TimeInForce = timeInForce != null + ? CreateTimeInForce(timeInForce, jObject) + : TimeInForce.GoodTilCanceled; string market = Market.USA; @@ -194,5 +193,36 @@ private static Order CreateOrder(OrderType orderType, JObject jObject) } return order; } + + /// + /// Creates a Time In Force of the correct type + /// + private static TimeInForce CreateTimeInForce(JToken timeInForce, JObject jObject) + { + // for backward-compatibility support deserialization of old JSON format + if (timeInForce is JValue) + { + var value = timeInForce.Value(); + + switch (value) + { + case 0: + return TimeInForce.GoodTilCanceled; + + case 1: + return TimeInForce.Day; + + case 2: + var expiry = jObject["DurationValue"].Value(); + return TimeInForce.GoodTilDate(expiry); + + default: + throw new Exception($"Unknown time in force value: {value}"); + } + } + + // convert with TimeInForceJsonConverter + return timeInForce.ToObject(); + } } } diff --git a/Common/Orders/OrderTypes.cs b/Common/Orders/OrderTypes.cs index 23b69302d155..378ab771ea48 100644 --- a/Common/Orders/OrderTypes.cs +++ b/Common/Orders/OrderTypes.cs @@ -56,40 +56,11 @@ public enum OrderType OptionExercise } - - /// - /// Time In Force - defines the length of time over which an order will continue working before it is canceled - /// - public enum TimeInForce - { - /// - /// Order active until it is filled or canceled (same as GTC). - /// - GoodTilCanceled, - - /// - /// Order active until it is filled or canceled (same as GoodTilCanceled). - /// - GTC = GoodTilCanceled, - - /// - /// Order valid only for the current day (DAY). - /// The order will be cancelled if not executed before the market close. - /// - Day, - - /// - /// Order valid until a custom set date time value. - /// - Custom - } - - /// /// Direction of the order /// - public enum OrderDirection { - + public enum OrderDirection + { /// /// Buy Order /// @@ -110,12 +81,11 @@ public enum OrderDirection { Hold } - /// /// Fill status of the order class. /// - public enum OrderStatus { - + public enum OrderStatus + { /// /// New order pre-submission to the order processor. /// @@ -156,5 +126,4 @@ public enum OrderStatus { /// CancelPending = 8 } - -} // End QC Namespace: +} diff --git a/Common/Orders/TimeInForce.cs b/Common/Orders/TimeInForce.cs new file mode 100644 index 000000000000..3f4704597002 --- /dev/null +++ b/Common/Orders/TimeInForce.cs @@ -0,0 +1,65 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using Newtonsoft.Json; +using QuantConnect.Interfaces; +using QuantConnect.Orders.TimeInForces; +using QuantConnect.Securities; + +namespace QuantConnect.Orders +{ + /// + /// Time In Force - defines the length of time over which an order will continue working before it is canceled + /// + [JsonConverter(typeof(TimeInForceJsonConverter))] + public abstract class TimeInForce : ITimeInForceHandler + { + /// + /// Gets a instance + /// + public static readonly TimeInForce GoodTilCanceled = new GoodTilCanceledTimeInForce(); + + /// + /// Gets a instance + /// + public static readonly TimeInForce Day = new DayTimeInForce(); + + /// + /// Gets a instance + /// + public static TimeInForce GoodTilDate(DateTime expiry) + { + return new GoodTilDateTimeInForce(expiry); + } + + /// + /// Checks if an order is expired + /// + /// The security matching the order + /// The order to be checked + /// Returns true if the order has expired, false otherwise + public abstract bool IsOrderExpired(Security security, Order order); + + /// + /// Checks if an order fill is valid + /// + /// The security matching the order + /// The order to be checked + /// The order fill to be checked + /// Returns true if the order fill can be emitted, false otherwise + public abstract bool IsFillValid(Security security, Order order, OrderEvent fill); + } +} diff --git a/Common/Orders/TimeInForceJsonConverter.cs b/Common/Orders/TimeInForceJsonConverter.cs new file mode 100644 index 000000000000..db52df276b35 --- /dev/null +++ b/Common/Orders/TimeInForceJsonConverter.cs @@ -0,0 +1,115 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace QuantConnect.Orders +{ + /// + /// Provides an implementation of that can deserialize TimeInForce objects + /// + public class TimeInForceJsonConverter : JsonConverter + { + /// + /// Gets a value indicating whether this can write JSON. + /// + /// + /// true if this can write JSON; otherwise, false. + /// + public override bool CanWrite => true; + + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) + { + return typeof(TimeInForce).IsAssignableFrom(objectType); + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to.The value.The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var timeInForce = value as TimeInForce; + if (ReferenceEquals(timeInForce, null)) return; + + var jo = new JObject(); + + var type = value.GetType(); + jo.Add("$type", type.FullName); + + foreach (var property in type.GetProperties()) + { + if (property.CanRead) + { + var propertyValue = property.GetValue(value, null); + if (propertyValue != null) + { + jo.Add(property.Name, JToken.FromObject(propertyValue, serializer)); + } + } + } + + jo.WriteTo(writer); + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from.Type of the object.The existing value of object being read.The calling serializer. + /// + /// The object value. + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jObject = JObject.Load(reader); + + var typeName = jObject["$type"].ToString(); + var type = Type.GetType(typeName); + if (type == null) + { + throw new Exception($"Unable to find the type: {typeName}"); + } + + var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[0], null); + if (constructor == null) + { + throw new Exception($"Unable to find a constructor for type: {typeName}"); + } + + var timeInForce = constructor.Invoke(null); + + foreach (var property in timeInForce.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + var value = jObject[property.Name]; + if (value != null) + { + property.SetValue(timeInForce, value.ToObject(property.PropertyType)); + } + } + + return timeInForce; + } + } +} diff --git a/Common/Orders/TimeInForces/DayTimeInForceHandler.cs b/Common/Orders/TimeInForces/DayTimeInForce.cs similarity index 92% rename from Common/Orders/TimeInForces/DayTimeInForceHandler.cs rename to Common/Orders/TimeInForces/DayTimeInForce.cs index 7780c3344a4a..bff520744a29 100644 --- a/Common/Orders/TimeInForces/DayTimeInForceHandler.cs +++ b/Common/Orders/TimeInForces/DayTimeInForce.cs @@ -14,15 +14,14 @@ */ using System; -using QuantConnect.Interfaces; using QuantConnect.Securities; namespace QuantConnect.Orders.TimeInForces { /// - /// Handles the Day time in force for an order (DAY) + /// Day Time In Force - order expires at market close /// - public class DayTimeInForceHandler : ITimeInForceHandler + public class DayTimeInForce : TimeInForce { /// /// Checks if an order is expired @@ -30,7 +29,7 @@ public class DayTimeInForceHandler : ITimeInForceHandler /// The security matching the order /// The order to be checked /// Returns true if the order has expired, false otherwise - public bool IsOrderExpired(Security security, Order order) + public override bool IsOrderExpired(Security security, Order order) { var exchangeHours = security.Exchange.Hours; @@ -84,7 +83,7 @@ public bool IsOrderExpired(Security security, Order order) /// The order to be checked /// The order fill to be checked /// Returns true if the order fill can be emitted, false otherwise - public bool IsFillValid(Security security, Order order, OrderEvent fill) + public override bool IsFillValid(Security security, Order order, OrderEvent fill) { return true; } diff --git a/Common/Orders/TimeInForces/GoodTilCanceledTimeInForceHandler.cs b/Common/Orders/TimeInForces/GoodTilCanceledTimeInForce.cs similarity index 83% rename from Common/Orders/TimeInForces/GoodTilCanceledTimeInForceHandler.cs rename to Common/Orders/TimeInForces/GoodTilCanceledTimeInForce.cs index a9cf2cd2f37c..429a6a075262 100644 --- a/Common/Orders/TimeInForces/GoodTilCanceledTimeInForceHandler.cs +++ b/Common/Orders/TimeInForces/GoodTilCanceledTimeInForce.cs @@ -13,15 +13,14 @@ * limitations under the License. */ -using QuantConnect.Interfaces; using QuantConnect.Securities; namespace QuantConnect.Orders.TimeInForces { /// - /// Handles the Good-Til-Canceled time in force for an order (GTC) + /// Good Til Canceled Time In Force - order does never expires /// - public class GoodTilCanceledTimeInForceHandler : ITimeInForceHandler + public class GoodTilCanceledTimeInForce : TimeInForce { /// /// Checks if an order is expired @@ -29,7 +28,7 @@ public class GoodTilCanceledTimeInForceHandler : ITimeInForceHandler /// The security matching the order /// The order to be checked /// Returns true if the order has expired, false otherwise - public bool IsOrderExpired(Security security, Order order) + public override bool IsOrderExpired(Security security, Order order) { return false; } @@ -41,7 +40,7 @@ public bool IsOrderExpired(Security security, Order order) /// The order to be checked /// The order fill to be checked /// Returns true if the order fill can be emitted, false otherwise - public bool IsFillValid(Security security, Order order, OrderEvent fill) + public override bool IsFillValid(Security security, Order order, OrderEvent fill) { return true; } diff --git a/Common/Orders/TimeInForces/GoodTilDateTimeInForce.cs b/Common/Orders/TimeInForces/GoodTilDateTimeInForce.cs new file mode 100644 index 000000000000..ef98e81e2212 --- /dev/null +++ b/Common/Orders/TimeInForces/GoodTilDateTimeInForce.cs @@ -0,0 +1,123 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using QuantConnect.Securities; + +namespace QuantConnect.Orders.TimeInForces +{ + /// + /// Good Til Date Time In Force - order expires and will be cancelled on a fixed date/time + /// + public class GoodTilDateTimeInForce : TimeInForce + { + /// + /// The date/time on which the order will expire and will be cancelled + /// + /// The private set is required for JSON deserialization + public DateTime Expiry { get; private set; } + + /// + /// Initializes a new instance of the class + /// + /// This constructor is required for JSON deserialization + private GoodTilDateTimeInForce() + { + } + + /// + /// Initializes a new instance of the class + /// + public GoodTilDateTimeInForce(DateTime expiry) + { + Expiry = expiry; + } + + /// + /// Checks if an order is expired + /// + /// The security matching the order + /// The order to be checked + /// Returns true if the order has expired, false otherwise + public override bool IsOrderExpired(Security security, Order order) + { + var exchangeHours = security.Exchange.Hours; + + var time = security.LocalTime; + + bool expired; + switch (order.SecurityType) + { + case SecurityType.Forex: + case SecurityType.Cfd: + // With real brokerages (IB, Oanda, FXCM have been verified) FX orders expire at 5 PM NewYork time. + // For now we use this fixed cut-off time, in future we might get this value from brokerage models, + // to support custom brokerage implementations. + expired = time.ConvertToUtc(exchangeHours.TimeZone) >= GetForexOrderExpiryDateTime(order); + break; + + case SecurityType.Crypto: + // expires at midnight after expiry date + expired = time.Date > Expiry.Date; + break; + + case SecurityType.Equity: + case SecurityType.Option: + case SecurityType.Future: + default: + // expires at market close of expiry date + expired = time >= exchangeHours.GetNextMarketClose(Expiry.Date, false); + break; + } + + return expired; + } + + /// + /// Checks if an order fill is valid + /// + /// The security matching the order + /// The order to be checked + /// The order fill to be checked + /// Returns true if the order fill can be emitted, false otherwise + public override bool IsFillValid(Security security, Order order, OrderEvent fill) + { + return true; + } + + /// + /// Returns the expiry date and time (UTC) for a Forex order + /// + public DateTime GetForexOrderExpiryDateTime(Order order) + { + var cutOffTimeZone = TimeZones.NewYork; + var cutOffTimeSpan = TimeSpan.FromHours(17); + + var expiryTime = Expiry.Date.Add(cutOffTimeSpan); + if (order.Time.Date == Expiry.Date) + { + // expiry date same as order date + var orderTime = order.Time.ConvertFromUtc(cutOffTimeZone); + if (orderTime.TimeOfDay > cutOffTimeSpan) + { + // order submitted after 5 PM, expiry on next date + expiryTime = expiryTime.AddDays(1); + } + } + + return expiryTime.ConvertToUtc(cutOffTimeZone); + } + } +} diff --git a/Common/QuantConnect.csproj b/Common/QuantConnect.csproj index ce1985c26ff4..8f6bb3644a59 100644 --- a/Common/QuantConnect.csproj +++ b/Common/QuantConnect.csproj @@ -191,12 +191,15 @@ + + + + - - + diff --git a/Tests/Common/Orders/OrderJsonConverterTests.cs b/Tests/Common/Orders/OrderJsonConverterTests.cs index 9ff4267ef82f..f6ce2cfa00b6 100644 --- a/Tests/Common/Orders/OrderJsonConverterTests.cs +++ b/Tests/Common/Orders/OrderJsonConverterTests.cs @@ -19,6 +19,7 @@ using Newtonsoft.Json; using NUnit.Framework; using QuantConnect.Orders; +using QuantConnect.Orders.TimeInForces; namespace QuantConnect.Tests.Common.Orders { @@ -201,7 +202,7 @@ public void WorksWithJsonConvert() var order = JsonConvert.DeserializeObject(json); Assert.IsInstanceOf(order); Assert.AreEqual(Market.USA, order.Symbol.ID.Market); - Assert.AreEqual(1, (int)order.TimeInForce); + Assert.IsTrue(order.TimeInForce is DayTimeInForce); } [Test] @@ -234,7 +235,44 @@ public void DeserializesOldDurationProperty() var order = JsonConvert.DeserializeObject(json); Assert.IsInstanceOf(order); Assert.AreEqual(Market.USA, order.Symbol.ID.Market); - Assert.AreEqual(1, (int)order.TimeInForce); + Assert.IsTrue(order.TimeInForce is DayTimeInForce); + } + + [Test] + public void DeserializesOldDurationValueProperty() + { + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + Converters = { new OrderJsonConverter() } + }; + + // The DurationValue property has been moved to GoodTilDateTimeInforce.Expiry, + // we still want to deserialize old JSON files containing Duration. + const string json = @"{'Type':0, +'Value':99986.827413672, +'Id':1, +'ContingentId':0, +'BrokerId':[1], +'Symbol':{'Value':'SPY', +'Permtick':'SPY'}, +'Price':100.086914328, +'Time':'2010-03-04T14:31:00Z', +'Quantity':999, +'Status':3, +'Duration':2, +'DurationValue':'2010-04-04T14:31:00Z', +'Tag':'', +'SecurityType':1, +'Direction':0, +'AbsoluteQuantity':999}"; + + var order = JsonConvert.DeserializeObject(json); + Assert.IsInstanceOf(order); + Assert.AreEqual(Market.USA, order.Symbol.ID.Market); + Assert.IsTrue(order.TimeInForce is GoodTilDateTimeInForce); + + var timeInForce = (GoodTilDateTimeInForce)order.TimeInForce; + Assert.AreEqual(new DateTime(2010, 4, 4, 14, 31, 0), timeInForce.Expiry); } [Test] @@ -244,6 +282,37 @@ public void DeserializesDecimalizedQuantity() TestOrderType(expected); } + [Test] + public void DeserializesOrderGoodTilCanceledTimeInForce() + { + var orderProperties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled }; + var expected = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.Today, "", orderProperties); + TestOrderType(expected); + } + + [Test] + public void DeserializesOrderDayTimeInForce() + { + var orderProperties = new OrderProperties { TimeInForce = TimeInForce.Day }; + var expected = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.Today, "", orderProperties); + TestOrderType(expected); + } + + [Test] + public void DeserializesOrderGoodTilDateTimeInForce() + { + var expiry = new DateTime(2018, 5, 26); + var orderProperties = new OrderProperties { TimeInForce = TimeInForce.GoodTilDate(expiry) }; + var expected = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.Today, "", orderProperties); + TestOrderType(expected); + + var json = JsonConvert.SerializeObject(expected); + var actual = DeserializeOrder(json); + + var gtd = (GoodTilDateTimeInForce)actual.Properties.TimeInForce; + Assert.AreEqual(expiry, gtd.Expiry); + } + private static T TestOrderType(T expected) where T : Order { @@ -256,8 +325,7 @@ private static T TestOrderType(T expected) CollectionAssert.AreEqual(expected.BrokerId, actual.BrokerId); Assert.AreEqual(expected.ContingentId, actual.ContingentId); Assert.AreEqual(expected.Direction, actual.Direction); - Assert.AreEqual(expected.TimeInForce, actual.TimeInForce); - Assert.AreEqual(expected.DurationValue, actual.DurationValue); + Assert.AreEqual(expected.TimeInForce.GetType(), actual.TimeInForce.GetType()); Assert.AreEqual(expected.Id, actual.Id); Assert.AreEqual(expected.Price, actual.Price); Assert.AreEqual(expected.SecurityType, actual.SecurityType); diff --git a/Tests/Common/Orders/TimeInForces/TimeInForceHandlerTests.cs b/Tests/Common/Orders/TimeInForces/TimeInForceHandlerTests.cs deleted file mode 100644 index c61fe7cbabfd..000000000000 --- a/Tests/Common/Orders/TimeInForces/TimeInForceHandlerTests.cs +++ /dev/null @@ -1,205 +0,0 @@ -/* - * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. - * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -using System; -using NUnit.Framework; -using QuantConnect.Data; -using QuantConnect.Data.Market; -using QuantConnect.Orders; -using QuantConnect.Orders.TimeInForces; -using QuantConnect.Securities; -using QuantConnect.Securities.Crypto; -using QuantConnect.Securities.Equity; -using QuantConnect.Securities.Forex; -using QuantConnect.Tests.Common.Securities; - -namespace QuantConnect.Tests.Common.Orders.TimeInForces -{ - [TestFixture] - public class TimeInForceHandlerTests - { - [Test] - public void GtcTimeInForceOrderDoesNotExpire() - { - var handler = new GoodTilCanceledTimeInForceHandler(); - - var security = new Equity( - SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), - new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), - new Cash(CashBook.AccountCurrency, 0, 1m), - SymbolProperties.GetDefault(CashBook.AccountCurrency)); - - var order = new LimitOrder(Symbols.SPY, 10, 100, DateTime.UtcNow); - - Assert.IsFalse(handler.IsOrderExpired(security, order)); - - var fill1 = new OrderEvent(order.Id, order.Symbol, DateTime.UtcNow, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); - Assert.IsTrue(handler.IsFillValid(security, order, fill1)); - - var fill2 = new OrderEvent(order.Id, order.Symbol, DateTime.UtcNow, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); - Assert.IsTrue(handler.IsFillValid(security, order, fill2)); - } - - [Test] - public void DayTimeInForceEquityOrderExpiresAtMarketClose() - { - var utcTime = new DateTime(2018, 4, 27, 10, 0, 0).ConvertToUtc(TimeZones.NewYork); - var handler = new DayTimeInForceHandler(); - - var security = new Equity( - SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), - new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), - new Cash(CashBook.AccountCurrency, 0, 1m), - SymbolProperties.GetDefault(CashBook.AccountCurrency)); - var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); - security.SetLocalTimeKeeper(localTimeKeeper); - - var orderProperties = new OrderProperties { TimeInForce = TimeInForce.Day }; - var order = new LimitOrder(Symbols.SPY, 10, 100, utcTime, "", orderProperties); - - Assert.IsFalse(handler.IsOrderExpired(security, order)); - - var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); - Assert.IsTrue(handler.IsFillValid(security, order, fill1)); - - var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); - Assert.IsTrue(handler.IsFillValid(security, order, fill2)); - - localTimeKeeper.UpdateTime(utcTime.AddHours(6).AddSeconds(-1)); - Assert.IsFalse(handler.IsOrderExpired(security, order)); - - localTimeKeeper.UpdateTime(utcTime.AddHours(6)); - Assert.IsTrue(handler.IsOrderExpired(security, order)); - - Assert.IsTrue(handler.IsFillValid(security, order, fill1)); - Assert.IsTrue(handler.IsFillValid(security, order, fill2)); - } - - [Test] - public void DayTimeInForceForexOrderBefore5PMExpiresAt5PM() - { - // set time to 10:00:00 AM (NY time) - var utcTime = new DateTime(2018, 4, 25, 10, 0, 0).ConvertToUtc(TimeZones.NewYork); - var handler = new DayTimeInForceHandler(); - - var security = new Forex( - SecurityExchangeHoursTests.CreateForexSecurityExchangeHours(), - new Cash(CashBook.AccountCurrency, 0, 1m), - new SubscriptionDataConfig(typeof(QuoteBar), Symbols.EURUSD, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), - SymbolProperties.GetDefault(CashBook.AccountCurrency)); - var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); - security.SetLocalTimeKeeper(localTimeKeeper); - - var orderProperties = new OrderProperties { TimeInForce = TimeInForce.Day }; - var order = new LimitOrder(Symbols.EURUSD, 10, 100, utcTime, "", orderProperties); - - Assert.IsFalse(handler.IsOrderExpired(security, order)); - - var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); - Assert.IsTrue(handler.IsFillValid(security, order, fill1)); - - var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); - Assert.IsTrue(handler.IsFillValid(security, order, fill2)); - - // set time to 4:59:59 PM (NY time) - localTimeKeeper.UpdateTime(utcTime.AddHours(7).AddSeconds(-1)); - Assert.IsFalse(handler.IsOrderExpired(security, order)); - - // set time to 5:00:00 PM (NY time) - localTimeKeeper.UpdateTime(utcTime.AddHours(7)); - Assert.IsTrue(handler.IsOrderExpired(security, order)); - - Assert.IsTrue(handler.IsFillValid(security, order, fill1)); - Assert.IsTrue(handler.IsFillValid(security, order, fill2)); - } - - [Test] - public void DayTimeInForceForexOrderAfter5PMExpiresAt5PMNextDay() - { - // set time to 6:00:00 PM (NY time) - var utcTime = new DateTime(2018, 4, 25, 18, 0, 0).ConvertToUtc(TimeZones.NewYork); - var handler = new DayTimeInForceHandler(); - - var security = new Forex( - SecurityExchangeHoursTests.CreateForexSecurityExchangeHours(), - new Cash(CashBook.AccountCurrency, 0, 1m), - new SubscriptionDataConfig(typeof(QuoteBar), Symbols.EURUSD, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), - SymbolProperties.GetDefault(CashBook.AccountCurrency)); - var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); - security.SetLocalTimeKeeper(localTimeKeeper); - - var orderProperties = new OrderProperties { TimeInForce = TimeInForce.Day }; - var order = new LimitOrder(Symbols.EURUSD, 10, 100, utcTime, "", orderProperties); - - Assert.IsFalse(handler.IsOrderExpired(security, order)); - - var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); - Assert.IsTrue(handler.IsFillValid(security, order, fill1)); - - var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); - Assert.IsTrue(handler.IsFillValid(security, order, fill2)); - - // set time to midnight (NY time) - localTimeKeeper.UpdateTime(utcTime.AddHours(6)); - Assert.IsFalse(handler.IsOrderExpired(security, order)); - - // set time to 4:59:59 PM next day (NY time) - localTimeKeeper.UpdateTime(utcTime.AddHours(23).AddSeconds(-1)); - Assert.IsFalse(handler.IsOrderExpired(security, order)); - - // set time to 5:00:00 PM next day (NY time) - localTimeKeeper.UpdateTime(utcTime.AddHours(23)); - Assert.IsTrue(handler.IsOrderExpired(security, order)); - - Assert.IsTrue(handler.IsFillValid(security, order, fill1)); - Assert.IsTrue(handler.IsFillValid(security, order, fill2)); - } - - [Test] - public void DayTimeInForceCryptoOrderExpiresAtMidnight() - { - var utcTime = new DateTime(2018, 4, 27, 10, 0, 0); - var handler = new DayTimeInForceHandler(); - - var security = new Crypto( - SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), - new Cash(CashBook.AccountCurrency, 0, 1m), - new SubscriptionDataConfig(typeof(QuoteBar), Symbols.BTCUSD, Resolution.Minute, TimeZones.Utc, TimeZones.Utc, true, true, true), - SymbolProperties.GetDefault(CashBook.AccountCurrency)); - var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.Utc); - security.SetLocalTimeKeeper(localTimeKeeper); - - var orderProperties = new OrderProperties { TimeInForce = TimeInForce.Day }; - var order = new LimitOrder(Symbols.BTCUSD, 10, 100, utcTime, "", orderProperties); - - Assert.IsFalse(handler.IsOrderExpired(security, order)); - - var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); - Assert.IsTrue(handler.IsFillValid(security, order, fill1)); - - var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); - Assert.IsTrue(handler.IsFillValid(security, order, fill2)); - - localTimeKeeper.UpdateTime(utcTime.AddHours(14).AddSeconds(-1)); - Assert.IsFalse(handler.IsOrderExpired(security, order)); - - localTimeKeeper.UpdateTime(utcTime.AddHours(14)); - Assert.IsTrue(handler.IsOrderExpired(security, order)); - - Assert.IsTrue(handler.IsFillValid(security, order, fill1)); - Assert.IsTrue(handler.IsFillValid(security, order, fill2)); - } - } -} diff --git a/Tests/Common/Orders/TimeInForces/TimeInForceTests.cs b/Tests/Common/Orders/TimeInForces/TimeInForceTests.cs new file mode 100644 index 000000000000..a6de4558e93b --- /dev/null +++ b/Tests/Common/Orders/TimeInForces/TimeInForceTests.cs @@ -0,0 +1,502 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using NUnit.Framework; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using QuantConnect.Orders; +using QuantConnect.Orders.TimeInForces; +using QuantConnect.Securities; +using QuantConnect.Securities.Crypto; +using QuantConnect.Securities.Equity; +using QuantConnect.Securities.Forex; +using QuantConnect.Tests.Common.Securities; + +namespace QuantConnect.Tests.Common.Orders.TimeInForces +{ + [TestFixture] + public class TimeInForceTests + { + [Test] + public void GtcTimeInForceOrderDoesNotExpire() + { + var security = new Equity( + SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), + new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), + new Cash(CashBook.AccountCurrency, 0, 1m), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + + var timeInForce = new GoodTilCanceledTimeInForce(); + var order = new LimitOrder(Symbols.SPY, 10, 100, DateTime.UtcNow); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, DateTime.UtcNow, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, DateTime.UtcNow, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void DayTimeInForceEquityOrderExpiresAtMarketClose() + { + var utcTime = new DateTime(2018, 4, 27, 10, 0, 0).ConvertToUtc(TimeZones.NewYork); + + var security = new Equity( + SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), + new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), + new Cash(CashBook.AccountCurrency, 0, 1m), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.Day; + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.SPY, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + localTimeKeeper.UpdateTime(utcTime.AddHours(6).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + localTimeKeeper.UpdateTime(utcTime.AddHours(6)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void DayTimeInForceForexOrderBefore5PMExpiresAt5PM() + { + // set time to 10:00:00 AM (NY time) + var utcTime = new DateTime(2018, 4, 25, 10, 0, 0).ConvertToUtc(TimeZones.NewYork); + + var security = new Forex( + SecurityExchangeHoursTests.CreateForexSecurityExchangeHours(), + new Cash(CashBook.AccountCurrency, 0, 1m), + new SubscriptionDataConfig(typeof(QuoteBar), Symbols.EURUSD, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.Day; + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.EURUSD, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + // set time to 4:59:59 PM (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(7).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // set time to 5:00:00 PM (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(7)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void DayTimeInForceForexOrderAfter5PMExpiresAt5PMNextDay() + { + // set time to 6:00:00 PM (NY time) + var utcTime = new DateTime(2018, 4, 25, 18, 0, 0).ConvertToUtc(TimeZones.NewYork); + + var security = new Forex( + SecurityExchangeHoursTests.CreateForexSecurityExchangeHours(), + new Cash(CashBook.AccountCurrency, 0, 1m), + new SubscriptionDataConfig(typeof(QuoteBar), Symbols.EURUSD, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.Day; + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.EURUSD, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + // set time to midnight (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(6)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // set time to 4:59:59 PM next day (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(23).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // set time to 5:00:00 PM next day (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(23)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void DayTimeInForceCryptoOrderExpiresAtMidnightUtc() + { + var utcTime = new DateTime(2018, 4, 27, 10, 0, 0); + + var security = new Crypto( + SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + new Cash(CashBook.AccountCurrency, 0, 1m), + new SubscriptionDataConfig(typeof(QuoteBar), Symbols.BTCUSD, Resolution.Minute, TimeZones.Utc, TimeZones.Utc, true, true, true), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.Utc); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.Day; + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.BTCUSD, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + localTimeKeeper.UpdateTime(utcTime.AddHours(14).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + localTimeKeeper.UpdateTime(utcTime.AddHours(14)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void GtdTimeInForceEquityOrderExpiresAtMarketCloseOnExpiryDate() + { + var utcTime = new DateTime(2018, 4, 27, 10, 0, 0).ConvertToUtc(TimeZones.NewYork); + + var security = new Equity( + SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), + new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), + new Cash(CashBook.AccountCurrency, 0, 1m), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.GoodTilDate(new DateTime(2018, 5, 1)); + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.SPY, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + // April 27th before market close + localTimeKeeper.UpdateTime(utcTime.AddHours(6).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // April 27th at market close + localTimeKeeper.UpdateTime(utcTime.AddHours(6)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // May 1st at 10 AM + localTimeKeeper.UpdateTime(utcTime.AddDays(4)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // May 1st before market close + localTimeKeeper.UpdateTime(utcTime.AddDays(4).AddHours(6).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // May 1st at market close + localTimeKeeper.UpdateTime(utcTime.AddDays(4).AddHours(6)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void GtdTimeInForceForexOrderBeforeExpiresAt5PMOnExpiryDate() + { + // set time to 10:00:00 AM (NY time) + var utcTime = new DateTime(2018, 4, 27, 10, 0, 0).ConvertToUtc(TimeZones.NewYork); + + var security = new Forex( + SecurityExchangeHoursTests.CreateForexSecurityExchangeHours(), + new Cash(CashBook.AccountCurrency, 0, 1m), + new SubscriptionDataConfig(typeof(QuoteBar), Symbols.EURUSD, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.GoodTilDate(new DateTime(2018, 5, 1)); + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.EURUSD, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + // April 27th 4:59:59 PM (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(7).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // April 27th 5:00:00 PM (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(7)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // May 1st at 10 AM + localTimeKeeper.UpdateTime(utcTime.AddDays(4)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // May 1st 4:59:59 PM (NY time) + localTimeKeeper.UpdateTime(utcTime.AddDays(4).AddHours(7).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // May 1st 5:00:00 PM (NY time) + localTimeKeeper.UpdateTime(utcTime.AddDays(4).AddHours(7)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void GtdTimeInForceCryptoOrderExpiresAtMidnightUtcAfterExpiryDate() + { + var utcTime = new DateTime(2018, 4, 27, 10, 0, 0); + + var security = new Crypto( + SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + new Cash(CashBook.AccountCurrency, 0, 1m), + new SubscriptionDataConfig(typeof(QuoteBar), Symbols.BTCUSD, Resolution.Minute, TimeZones.Utc, TimeZones.Utc, true, true, true), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.Utc); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.GoodTilDate(new DateTime(2018, 5, 1)); + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.BTCUSD, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + // April 27th before midnight + localTimeKeeper.UpdateTime(utcTime.AddHours(14).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // April 28th at midnight + localTimeKeeper.UpdateTime(utcTime.AddHours(14)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // May 1st at 10 AM + localTimeKeeper.UpdateTime(utcTime.AddDays(4)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // May 1st before midnight + localTimeKeeper.UpdateTime(utcTime.AddDays(4).AddHours(14).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // May 2nd at midnight + localTimeKeeper.UpdateTime(utcTime.AddDays(4).AddHours(14)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void GtdSameDayTimeInForceEquityOrderExpiresAtMarketClose() + { + var utcTime = new DateTime(2018, 4, 27, 10, 0, 0).ConvertToUtc(TimeZones.NewYork); + + var security = new Equity( + SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), + new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), + new Cash(CashBook.AccountCurrency, 0, 1m), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.GoodTilDate(new DateTime(2018, 4, 27)); + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.SPY, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + localTimeKeeper.UpdateTime(utcTime.AddHours(6).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + localTimeKeeper.UpdateTime(utcTime.AddHours(6)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void GtdSameDayTimeInForceForexOrderBefore5PMExpiresAt5PM() + { + // set time to 10:00:00 AM (NY time) + var utcTime = new DateTime(2018, 4, 27, 10, 0, 0).ConvertToUtc(TimeZones.NewYork); + + var security = new Forex( + SecurityExchangeHoursTests.CreateForexSecurityExchangeHours(), + new Cash(CashBook.AccountCurrency, 0, 1m), + new SubscriptionDataConfig(typeof(QuoteBar), Symbols.EURUSD, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.GoodTilDate(new DateTime(2018, 4, 27)); + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.EURUSD, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + // set time to 4:59:59 PM (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(7).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // set time to 5:00:00 PM (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(7)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void GtdSameDayTimeInForceForexOrderAfter5PMExpiresAt5PMNextDay() + { + // set time to 6:00:00 PM (NY time) + var utcTime = new DateTime(2018, 4, 27, 18, 0, 0).ConvertToUtc(TimeZones.NewYork); + + var security = new Forex( + SecurityExchangeHoursTests.CreateForexSecurityExchangeHours(), + new Cash(CashBook.AccountCurrency, 0, 1m), + new SubscriptionDataConfig(typeof(QuoteBar), Symbols.EURUSD, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, true), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.NewYork); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.GoodTilDate(new DateTime(2018, 4, 27)); + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.EURUSD, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + // set time to midnight (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(6)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // set time to 4:59:59 PM next day (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(23).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + // set time to 5:00:00 PM next day (NY time) + localTimeKeeper.UpdateTime(utcTime.AddHours(23)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + + [Test] + public void GtdSameDayTimeInForceCryptoOrderExpiresAtMidnightUtc() + { + var utcTime = new DateTime(2018, 4, 27, 10, 0, 0); + + var security = new Crypto( + SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), + new Cash(CashBook.AccountCurrency, 0, 1m), + new SubscriptionDataConfig(typeof(QuoteBar), Symbols.BTCUSD, Resolution.Minute, TimeZones.Utc, TimeZones.Utc, true, true, true), + SymbolProperties.GetDefault(CashBook.AccountCurrency)); + var localTimeKeeper = new LocalTimeKeeper(utcTime, TimeZones.Utc); + security.SetLocalTimeKeeper(localTimeKeeper); + + var timeInForce = TimeInForce.GoodTilDate(new DateTime(2018, 4, 27)); + var orderProperties = new OrderProperties { TimeInForce = timeInForce }; + var order = new LimitOrder(Symbols.BTCUSD, 10, 100, utcTime, "", orderProperties); + + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + var fill1 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.PartiallyFilled, OrderDirection.Buy, order.LimitPrice, 3, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + + var fill2 = new OrderEvent(order.Id, order.Symbol, utcTime, OrderStatus.Filled, OrderDirection.Buy, order.LimitPrice, 7, 0); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + + localTimeKeeper.UpdateTime(utcTime.AddHours(14).AddSeconds(-1)); + Assert.IsFalse(timeInForce.IsOrderExpired(security, order)); + + localTimeKeeper.UpdateTime(utcTime.AddHours(14)); + Assert.IsTrue(timeInForce.IsOrderExpired(security, order)); + + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill1)); + Assert.IsTrue(timeInForce.IsFillValid(security, order, fill2)); + } + } +} diff --git a/Tests/QuantConnect.Tests.csproj b/Tests/QuantConnect.Tests.csproj index edfae986625b..5b1f415e0f9e 100644 --- a/Tests/QuantConnect.Tests.csproj +++ b/Tests/QuantConnect.Tests.csproj @@ -172,7 +172,7 @@ - + diff --git a/Tests/RegressionTests.cs b/Tests/RegressionTests.cs index a07d025197b2..8e20e7dfbae1 100644 --- a/Tests/RegressionTests.cs +++ b/Tests/RegressionTests.cs @@ -1019,25 +1019,25 @@ private static TestCaseData[] GetRegressionTestParameters() var timeInForceAlgorithmStatistics = new Dictionary { - {"Total Trades", "1"}, + {"Total Trades", "3"}, {"Average Win", "0%"}, {"Average Loss", "0%"}, - {"Compounding Annual Return", "3.502%"}, - {"Drawdown", "0.000%"}, + {"Compounding Annual Return", "9.319%"}, + {"Drawdown", "0.100%"}, {"Expectancy", "0"}, - {"Net Profit", "0.044%"}, - {"Sharpe Ratio", "9.199"}, + {"Net Profit", "0.114%"}, + {"Sharpe Ratio", "7.351"}, {"Loss Rate", "0%"}, {"Win Rate", "0%"}, {"Profit-Loss Ratio", "0"}, - {"Alpha", "0"}, - {"Beta", "2.009"}, - {"Annual Standard Deviation", "0.002"}, + {"Alpha", "-0.003"}, + {"Beta", "5.432"}, + {"Annual Standard Deviation", "0.008"}, {"Annual Variance", "0"}, - {"Information Ratio", "4.813"}, - {"Tracking Error", "0.002"}, + {"Information Ratio", "6.013"}, + {"Tracking Error", "0.008"}, {"Treynor Ratio", "0.011"}, - {"Total Fees", "$1.00"} + {"Total Fees", "$3.00"} }; var delistingEventsAlgorithm = new Dictionary