Lean  $LEAN_TAG$
EquityFillModel.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Collections.Generic;
18 using System.Linq;
19 
20 using QuantConnect.Data;
26 using QuantConnect.Util;
27 
29 {
30  /// <summary>
31  /// Represents the fill model used to simulate order fills for equities
32  /// </summary>
33  public class EquityFillModel : FillModel
34  {
35  /// <summary>
36  /// Default limit if touched fill model implementation in base class security.
37  /// </summary>
38  /// <param name="asset">Security asset we're filling</param>
39  /// <param name="order">Order packet to model</param>
40  /// <returns>Order fill information detailing the average price and quantity filled.</returns>
41  /// <remarks>
42  /// There is no good way to model limit orders with OHLC because we never know whether the market has
43  /// gapped past our fill price. We have to make the assumption of a fluid, high volume market.
44  ///
45  /// With Limit if Touched orders, whether or not a trigger is surpassed is determined by the high (low)
46  /// of the previous tradebar when making a sell (buy) request. Following the behaviour of
47  /// <see cref="StopLimitFill"/>, current quote information is used when determining fill parameters
48  /// (e.g., price, quantity) as the tradebar containing the incoming data is not yet consolidated.
49  /// This conservative approach, however, can lead to trades not occuring as would be expected when
50  /// compared to future consolidated data.
51  /// </remarks>
53  {
54  //Default order event to return.
55  var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
56  var fill = new OrderEvent(order, utcTime, OrderFee.Zero);
57 
58  //If its cancelled don't need anymore checks:
59  if (order.Status == OrderStatus.Canceled) return fill;
60 
61  // Fill only if open or extended
62  if (!IsExchangeOpen(asset,
64  .GetSubscriptionDataConfigs(asset.Symbol)
65  .IsExtendedMarketHours()))
66  {
67  return fill;
68  }
69 
70  // Get the trade bar that closes after the order time
71  var tradeBar = GetBestEffortTradeBar(asset, order.Time);
72 
73  // Do not fill on stale data
74  if (tradeBar == null) return fill;
75 
76  //Check if the limit if touched order was filled:
77  switch (order.Direction)
78  {
79  case OrderDirection.Buy:
80  //-> 1.2 Buy: If Price below Trigger, Buy:
81  if (tradeBar.Low <= order.TriggerPrice || order.TriggerTouched)
82  {
83  order.TriggerTouched = true;
84  var askCurrent = GetBestEffortAskPrice(asset, order.Time, out var fillMessage);
85 
86  if (askCurrent <= order.LimitPrice)
87  {
88  fill.Status = OrderStatus.Filled;
89  fill.FillPrice = Math.Min(askCurrent, order.LimitPrice);
90  fill.FillQuantity = order.Quantity;
91  fill.Message = fillMessage;
92  }
93  }
94 
95  break;
96 
97  case OrderDirection.Sell:
98  //-> 1.2 Sell: If Price above Trigger, Sell:
99  if (tradeBar.High >= order.TriggerPrice || order.TriggerTouched)
100  {
101  order.TriggerTouched = true;
102  var bidCurrent = GetBestEffortBidPrice(asset, order.Time, out var fillMessage);
103 
104  if (bidCurrent >= order.LimitPrice)
105  {
106  fill.Status = OrderStatus.Filled;
107  fill.FillPrice = Math.Max(bidCurrent, order.LimitPrice);
108  fill.FillQuantity = order.Quantity;
109  fill.Message = fillMessage;
110  }
111  }
112 
113  break;
114  }
115  return fill;
116  }
117 
118  /// <summary>
119  /// Default market fill model for the base security class. Fills at the last traded price.
120  /// </summary>
121  /// <param name="asset">Security asset we're filling</param>
122  /// <param name="order">Order packet to model</param>
123  /// <returns>Order fill information detailing the average price and quantity filled.</returns>
124  public override OrderEvent MarketFill(Security asset, MarketOrder order)
125  {
126  //Default order event to return.
127  var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
128  var fill = new OrderEvent(order, utcTime, OrderFee.Zero);
129 
130  if (order.Status == OrderStatus.Canceled) return fill;
131 
132  // Make sure the exchange is open/normal market hours before filling
133  if (!IsExchangeOpen(asset, false)) return fill;
134 
135  // Calculate the model slippage: e.g. 0.01c
136  var slip = asset.SlippageModel.GetSlippageApproximation(asset, order);
137 
138  var fillMessage = string.Empty;
139 
140  switch (order.Direction)
141  {
142  case OrderDirection.Buy:
143  //Order [fill]price for a buy market order model is the current security ask price
144  fill.FillPrice = GetBestEffortAskPrice(asset, order.Time, out fillMessage) + slip;
145  break;
146  case OrderDirection.Sell:
147  //Order [fill]price for a buy market order model is the current security bid price
148  fill.FillPrice = GetBestEffortBidPrice(asset, order.Time, out fillMessage) - slip;
149  break;
150  }
151 
152  // assume the order completely filled
153  fill.FillQuantity = order.Quantity;
154  fill.Message = fillMessage;
155  fill.Status = OrderStatus.Filled;
156  return fill;
157  }
158 
159  /// <summary>
160  /// Stop fill model implementation for Equity.
161  /// </summary>
162  /// <param name="asset">Security asset we're filling</param>
163  /// <param name="order">Order packet to model</param>
164  /// <returns>Order fill information detailing the average price and quantity filled.</returns>
165  /// <remarks>
166  /// A Stop order is an instruction to submit a buy or sell market order
167  /// if and when the user-specified stop trigger price is attained or penetrated.
168  ///
169  /// A Sell Stop order is always placed below the current market price.
170  /// We assume a fluid/continuous, high volume market. Therefore, it is filled at the stop trigger price
171  /// if the current low price of trades is less than or equal to this price.
172  ///
173  /// A Buy Stop order is always placed above the current market price.
174  /// We assume a fluid, high volume market. Therefore, it is filled at the stop trigger price
175  /// if the current high price of trades is greater or equal than this price.
176  ///
177  /// The continuous market assumption is not valid if the market opens with an unfavorable gap.
178  /// In this case, a new bar opens below/above the stop trigger price, and the order is filled with the opening price.
179  /// <seealso cref="MarketFill(Security, MarketOrder)"/>
180  public override OrderEvent StopMarketFill(Security asset, StopMarketOrder order)
181  {
182  // Default order event to return.
183  var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
184  var fill = new OrderEvent(order, utcTime, OrderFee.Zero);
185 
186  // If cancelled, don't need anymore checks:
187  if (order.Status == OrderStatus.Canceled) return fill;
188 
189  // Make sure the exchange is open/normal market hours before filling
190  if (!IsExchangeOpen(asset, false)) return fill;
191 
192  // Get the trade bar that closes after the order time
193  var tradeBar = GetBestEffortTradeBar(asset, order.Time);
194 
195  // Do not fill on stale data
196  if (tradeBar == null) return fill;
197 
198  switch (order.Direction)
199  {
200  case OrderDirection.Sell:
201  if (tradeBar.Low <= order.StopPrice)
202  {
203  fill.Status = OrderStatus.Filled;
204  fill.FillQuantity = order.Quantity;
205 
206  var slip = asset.SlippageModel.GetSlippageApproximation(asset, order);
207 
208  // Unfavorable gap case: if the bar opens below the stop price, fill at open price
209  if (tradeBar.Open <= order.StopPrice)
210  {
211  fill.FillPrice = tradeBar.Open - slip;
212  fill.Message = Messages.EquityFillModel.FilledWithOpenDueToUnfavorableGap(asset, tradeBar);
213  return fill;
214  }
215 
216  fill.FillPrice = order.StopPrice - slip;
217  }
218  break;
219 
220  case OrderDirection.Buy:
221  if (tradeBar.High >= order.StopPrice)
222  {
223  fill.Status = OrderStatus.Filled;
224  fill.FillQuantity = order.Quantity;
225 
226  var slip = asset.SlippageModel.GetSlippageApproximation(asset, order);
227 
228  // Unfavorable gap case: if the bar opens above the stop price, fill at open price
229  if (tradeBar.Open >= order.StopPrice)
230  {
231  fill.FillPrice = tradeBar.Open + slip;
232  fill.Message = Messages.EquityFillModel.FilledWithOpenDueToUnfavorableGap(asset, tradeBar);
233  return fill;
234  }
235 
236  fill.FillPrice = order.StopPrice + slip;
237  }
238  break;
239  }
240 
241  return fill;
242  }
243 
244  /// <summary>
245  /// Default stop limit fill model implementation in base class security. (Stop Limit Order Type)
246  /// </summary>
247  /// <param name="asset">Security asset we're filling</param>
248  /// <param name="order">Order packet to model</param>
249  /// <returns>Order fill information detailing the average price and quantity filled.</returns>
250  /// <seealso cref="StopMarketFill(Security, StopMarketOrder)"/>
251  /// <remarks>
252  /// There is no good way to model limit orders with OHLC because we never know whether the market has
253  /// gapped past our fill price. We have to make the assumption of a fluid, high volume market.
254  ///
255  /// Stop limit orders we also can't be sure of the order of the H - L values for the limit fill. The assumption
256  /// was made the limit fill will be done with closing price of the bar after the stop has been triggered..
257  /// </remarks>
258  public override OrderEvent StopLimitFill(Security asset, StopLimitOrder order)
259  {
260  //Default order event to return.
261  var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
262  var fill = new OrderEvent(order, utcTime, OrderFee.Zero);
263 
264  //If its cancelled don't need anymore checks:
265  if (order.Status == OrderStatus.Canceled) return fill;
266 
267  // make sure the exchange is open before filling -- allow pre/post market fills to occur
268  if (!IsExchangeOpen(
269  asset,
271  .GetSubscriptionDataConfigs(asset.Symbol)
272  .IsExtendedMarketHours()))
273  {
274  return fill;
275  }
276 
277  //Get the range of prices in the last bar:
278  var prices = GetPricesCheckingPythonWrapper(asset, order.Direction);
279  var pricesEndTime = prices.EndTime.ConvertToUtc(asset.Exchange.TimeZone);
280 
281  // do not fill on stale data
282  if (pricesEndTime <= order.Time) return fill;
283 
284  //Check if the Stop Order was filled: opposite to a limit order
285  switch (order.Direction)
286  {
287  case OrderDirection.Buy:
288  //-> 1.2 Buy Stop: If Price Above Setpoint, Buy:
289  if (prices.High > order.StopPrice || order.StopTriggered)
290  {
291  if (!order.StopTriggered)
292  {
293  order.StopTriggered = true;
294  Parameters.OnOrderUpdated(order);
295  }
296 
297  // Fill the limit order, using closing price of bar:
298  // Note > Can't use minimum price, because no way to be sure minimum wasn't before the stop triggered.
299  if (prices.Current < order.LimitPrice)
300  {
301  fill.Status = OrderStatus.Filled;
302  fill.FillPrice = Math.Min(prices.High, order.LimitPrice);
303  // assume the order completely filled
304  fill.FillQuantity = order.Quantity;
305  }
306  }
307  break;
308 
309  case OrderDirection.Sell:
310  //-> 1.1 Sell Stop: If Price below setpoint, Sell:
311  if (prices.Low < order.StopPrice || order.StopTriggered)
312  {
313  if (!order.StopTriggered)
314  {
315  order.StopTriggered = true;
316  Parameters.OnOrderUpdated(order);
317  }
318 
319  // Fill the limit order, using minimum price of the bar
320  // Note > Can't use minimum price, because no way to be sure minimum wasn't before the stop triggered.
321  if (prices.Current > order.LimitPrice)
322  {
323  fill.Status = OrderStatus.Filled;
324  fill.FillPrice = Math.Max(prices.Low, order.LimitPrice);
325  // assume the order completely filled
326  fill.FillQuantity = order.Quantity;
327  }
328  }
329  break;
330  }
331 
332  return fill;
333  }
334 
335  /// <summary>
336  /// Limit fill model implementation for Equity.
337  /// </summary>
338  /// <param name="asset">Security asset we're filling</param>
339  /// <param name="order">Order packet to model</param>
340  /// <returns>Order fill information detailing the average price and quantity filled.</returns>
341  /// <remarks>
342  /// A Limit order is an order to buy or sell at a specified price or better.
343  /// The Limit order ensures that if the order fills, it will not fill at a price less favorable than your limit price,
344  /// but it does not guarantee a fill.
345  ///
346  /// A Buy Limit order is always placed above the current market price.
347  /// We assume a fluid/continuous, high volume market. Therefore, it is filled at the limit price
348  /// if the current low price of trades is less than this price.
349  ///
350  /// A Sell Limit order is always placed below the current market price.
351  /// We assume a fluid, high volume market. Therefore, it is filled at the limit price
352  /// if the current high price of trades is greater than this price.
353  ///
354  /// This model does not trigger the limit order when the limit is attained (equals to).
355  /// Since the order may not be filled in reality if it is not the top of the order book
356  /// (first come, first served), we assume our order is the last in the book with its limit price,
357  /// thus it will be filled when the limit price is penetrated.
358  ///
359  /// The continuous market assumption is not valid if the market opens with a favorable gap.
360  /// If the buy/sell limit order is placed below/above the current market price,
361  /// the order is filled with the opening price.
362  /// <seealso cref="StopMarketFill(Security, StopMarketOrder)"/>
363  /// <seealso cref="MarketFill(Security, MarketOrder)"/>
364  public override OrderEvent LimitFill(Security asset, LimitOrder order)
365  {
366  //Initialise;
367  var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
368  var fill = new OrderEvent(order, utcTime, OrderFee.Zero);
369 
370  //If its cancelled don't need anymore checks:
371  if (order.Status == OrderStatus.Canceled) return fill;
372 
373  // make sure the exchange is open before filling -- allow pre/post market fills to occur
374  if (!IsExchangeOpen(asset,
376  .GetSubscriptionDataConfigs(asset.Symbol)
377  .IsExtendedMarketHours()))
378  {
379  return fill;
380  }
381 
382  // Get the trade bar that closes after the order time
383  var tradeBar = GetBestEffortTradeBar(asset, order.Time);
384 
385  // Do not fill on stale data
386  if (tradeBar == null) return fill;
387 
388  //-> Valid Live/Model Order:
389  switch (order.Direction)
390  {
391  case OrderDirection.Buy:
392  if (tradeBar.Low < order.LimitPrice)
393  {
394  // assume the order completely filled
395  // TODO: Add separate DepthLimited fill partial order quantities based on tick quantity / bar.Volume available.
396  fill.FillQuantity = order.Quantity;
397  fill.Status = OrderStatus.Filled;
398 
399  fill.FillPrice = order.LimitPrice;
400 
401  // Favorable gap case: if the bar opens below the limit price, fill at open price
402  if (tradeBar.Open < order.LimitPrice)
403  {
404  fill.FillPrice = tradeBar.Open;
405  fill.Message = Messages.EquityFillModel.FilledWithOpenDueToFavorableGap(asset, tradeBar);
406  return fill;
407  }
408  }
409  break;
410  case OrderDirection.Sell:
411  if (tradeBar.High > order.LimitPrice)
412  {
413  // Assume the order completely filled
414  // TODO: Add separate DepthLimited fill partial order quantities based on tick quantity / bar.Volume available.
415  fill.FillQuantity = order.Quantity;
416  fill.Status = OrderStatus.Filled;
417 
418  fill.FillPrice = order.LimitPrice;
419 
420  // Favorable gap case: if the bar opens above the limit price, fill at open price
421  if (tradeBar.Open > order.LimitPrice)
422  {
423  fill.FillPrice = tradeBar.Open;
424  fill.Message = Messages.EquityFillModel.FilledWithOpenDueToFavorableGap(asset, tradeBar);
425  return fill;
426  }
427  }
428  break;
429  }
430 
431  return fill;
432  }
433 
434  /// <summary>
435  /// Market on Open Fill Model. Return an order event with the fill details
436  /// </summary>
437  /// <param name="asset">Asset we're trading with this order</param>
438  /// <param name="order">Order to be filled</param>
439  /// <returns>Order fill information detailing the average price and quantity filled.</returns>
441  {
442  var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
443  var fill = new OrderEvent(order, utcTime, OrderFee.Zero);
444 
445  if (order.Status == OrderStatus.Canceled) return fill;
446 
447  // MOO should never fill on the same bar or on stale data
448  // Imagine the case where we have a thinly traded equity, ASUR, and another liquid
449  // equity, say SPY, SPY gets data every minute but ASUR, if not on fill forward, maybe
450  // have large gaps, in which case the currentBar.EndTime will be in the past
451  // ASUR | | | [order] | | | | | | |
452  // SPY | | | | | | | | | | | | | | | | | | | |
453  var localOrderTime = order.Time.ConvertFromUtc(asset.Exchange.TimeZone);
454  var endTime = DateTime.MinValue;
455 
456  var subscribedTypes = GetSubscribedTypes(asset);
457 
458  if (subscribedTypes.Contains(typeof(Tick)))
459  {
460  var primaryExchangeCode = ((Equity)asset).PrimaryExchange.Code;
461  var openTradeTickFlags = (uint)(TradeConditionFlags.OfficialOpen | TradeConditionFlags.OpeningPrints);
462 
463  var trades = asset.Cache.GetAll<Tick>()
464  .Where(x => x.TickType == TickType.Trade && asset.Exchange.DateTimeIsOpen(x.Time))
465  .OrderBy(x => x.EndTime).ToList();
466 
467  // Get the first valid (non-zero) tick of trade type from an open market
468  var tick = trades
469  .FirstOrDefault(x =>
470  !string.IsNullOrWhiteSpace(x.SaleCondition) &&
471  x.ExchangeCode == primaryExchangeCode &&
472  (x.ParsedSaleCondition & openTradeTickFlags) != 0 &&
473  asset.Exchange.DateTimeIsOpen(x.Time));
474 
475  // If there is no OfficialOpen or OpeningPrints in the current list of trades,
476  // we will wait for the next up to 1 minute before accepting the last trade without flags
477  // We will give priority to trade then use quote to get the timestamp
478  // If there are only quotes, we will need to test for the tick type before we assign the fill price
479  if (tick == null)
480  {
481  var previousOpen = asset.Exchange.Hours
482  .GetMarketHours(asset.LocalTime)
483  .GetMarketOpen(TimeSpan.Zero, false);
484 
485  fill.Message = Messages.EquityFillModel.MarketOnOpenFillNoOfficialOpenOrOpeningPrintsWithinOneMinute;
486 
487  tick = trades.LastOrDefault() ?? asset.Cache.GetAll<Tick>().LastOrDefault();
488  if ((tick?.EndTime.TimeOfDay - previousOpen)?.TotalMinutes < 1)
489  {
490  return fill;
491  }
492 
493  fill.Message += " " + Messages.EquityFillModel.FilledWithLastTickTypeData(tick);
494  }
495 
496  endTime = tick?.EndTime ?? endTime;
497 
498  if (tick?.TickType == TickType.Trade)
499  {
500  fill.FillPrice = tick.Price;
501  }
502  }
503  else if (subscribedTypes.Contains(typeof(TradeBar)))
504  {
505  var tradeBar = asset.Cache.GetData<TradeBar>();
506  if (tradeBar != null)
507  {
508  // If the order was placed during the bar aggregation, we cannot use its open price
509  if (tradeBar.Time < localOrderTime) return fill;
510 
511  // We need to verify whether the trade data is from the open market.
512  if (tradeBar.Period < Resolution.Hour.ToTimeSpan() && !asset.Exchange.DateTimeIsOpen(tradeBar.Time))
513  {
514  return fill;
515  }
516 
517  endTime = tradeBar.EndTime;
518  fill.FillPrice = tradeBar.Open;
519  }
520  }
521  else
522  {
523  fill.Message = Messages.EquityFillModel.FilledWithQuoteData(asset);
524  }
525 
526  if (localOrderTime >= endTime) return fill;
527 
528  // if the MOO was submitted during market the previous day, wait for a day to turn over
529  // The date of the order and the trade data end time cannot be the same.
530  // Note that the security local time can be ahead of the data end time.
531  if (asset.Exchange.DateTimeIsOpen(localOrderTime) && localOrderTime.Date == endTime.Date)
532  {
533  return fill;
534  }
535 
536  // wait until market open
537  // make sure the exchange is open/normal market hours before filling
538  if (!IsExchangeOpen(asset, false)) return fill;
539 
540  // assume the order completely filled
541  fill.FillQuantity = order.Quantity;
542  fill.Status = OrderStatus.Filled;
543 
544  //Calculate the model slippage: e.g. 0.01c
545  var slip = asset.SlippageModel.GetSlippageApproximation(asset, order);
546 
547  var bestEffortMessage = "";
548 
549  // If there is no trade information, get the bid or ask, then apply the slippage
550  switch (order.Direction)
551  {
552  case OrderDirection.Buy:
553  if (fill.FillPrice == 0)
554  {
555  fill.FillPrice = GetBestEffortAskPrice(asset, order.Time, out bestEffortMessage);
556  fill.Message += bestEffortMessage;
557  }
558 
559  fill.FillPrice += slip;
560  break;
561  case OrderDirection.Sell:
562  if (fill.FillPrice == 0)
563  {
564  fill.FillPrice = GetBestEffortBidPrice(asset, order.Time, out bestEffortMessage);
565  fill.Message += bestEffortMessage;
566  }
567 
568  fill.FillPrice -= slip;
569  break;
570  }
571 
572  return fill;
573  }
574 
575  /// <summary>
576  /// Market on Close Fill Model. Return an order event with the fill details
577  /// </summary>
578  /// <param name="asset">Asset we're trading with this order</param>
579  /// <param name="order">Order to be filled</param>
580  /// <returns>Order fill information detailing the average price and quantity filled.</returns>
582  {
583  var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
584  var fill = new OrderEvent(order, utcTime, OrderFee.Zero);
585 
586  if (order.Status == OrderStatus.Canceled) return fill;
587 
588  var localOrderTime = order.Time.ConvertFromUtc(asset.Exchange.TimeZone);
589  var nextMarketClose = asset.Exchange.Hours.GetNextMarketClose(localOrderTime, false);
590 
591  // wait until market closes after the order time
592  if (asset.LocalTime < nextMarketClose)
593  {
594  return fill;
595  }
596 
597  var subscribedTypes = GetSubscribedTypes(asset);
598 
599  if (subscribedTypes.Contains(typeof(Tick)))
600  {
601  var primaryExchangeCode = ((Equity)asset).PrimaryExchange.Code;
602  var closeTradeTickFlags = (uint)(TradeConditionFlags.OfficialClose | TradeConditionFlags.ClosingPrints);
603 
604  var trades = asset.Cache.GetAll<Tick>()
605  .Where(x => x.TickType == TickType.Trade)
606  .OrderBy(x => x.EndTime).ToList();
607 
608  // Get the last valid (non-zero) tick of trade type from an close market
609  var tick = trades
610  .LastOrDefault(x =>
611  !string.IsNullOrWhiteSpace(x.SaleCondition) &&
612  x.ExchangeCode == primaryExchangeCode
613  && (x.ParsedSaleCondition & closeTradeTickFlags) != 0);
614 
615  // If there is no OfficialClose or ClosingPrints in the current list of trades,
616  // we will wait for the next up to 1 minute before accepting the last tick without flags
617  // We will give priority to trade then use quote to get the timestamp
618  // If there are only quotes, we will need to test for the tick type before we assign the fill price
619  if (tick == null)
620  {
621  tick = trades.LastOrDefault() ?? asset.Cache.GetAll<Tick>().LastOrDefault();
622  if (Parameters.ConfigProvider.GetSubscriptionDataConfigs(asset.Symbol).IsExtendedMarketHours())
623  {
624  fill.Message = Messages.EquityFillModel.MarketOnCloseFillNoOfficialCloseOrClosingPrintsWithinOneMinute;
625 
626  if ((tick?.EndTime - nextMarketClose)?.TotalMinutes < 1)
627  {
628  return fill;
629  }
630  }
631  else
632  {
633  fill.Message = Messages.EquityFillModel.MarketOnCloseFillNoOfficialCloseOrClosingPrintsWithoutExtendedMarketHours;
634  }
635 
636  fill.Message += " " + Messages.EquityFillModel.FilledWithLastTickTypeData(tick);
637  }
638 
639  if (tick?.TickType == TickType.Trade)
640  {
641  fill.FillPrice = tick.Price;
642  }
643  }
644  // make sure the exchange is open/normal market hours before filling
645  // It will return true if the last bar opens before the market closes
646  else if (!IsExchangeOpen(asset, false))
647  {
648  return fill;
649  }
650  else if (subscribedTypes.Contains(typeof(TradeBar)))
651  {
652  fill.FillPrice = asset.Cache.GetData<TradeBar>()?.Close ?? 0;
653  }
654  else
655  {
656  fill.Message = Messages.EquityFillModel.FilledWithQuoteData(asset);
657  }
658 
659  // Calculate the model slippage: e.g. 0.01c
660  var slip = asset.SlippageModel.GetSlippageApproximation(asset, order);
661 
662  var bestEffortMessage = "";
663 
664  // If there is no trade information, get the bid or ask, then apply the slippage
665  switch (order.Direction)
666  {
667  case OrderDirection.Buy:
668  if (fill.FillPrice == 0)
669  {
670  fill.FillPrice = GetBestEffortAskPrice(asset, order.Time, out bestEffortMessage);
671  fill.Message += bestEffortMessage;
672  }
673 
674  fill.FillPrice += slip;
675  break;
676  case OrderDirection.Sell:
677  if (fill.FillPrice == 0)
678  {
679  fill.FillPrice = GetBestEffortBidPrice(asset, order.Time, out bestEffortMessage);
680  fill.Message += bestEffortMessage;
681  }
682 
683  fill.FillPrice -= slip;
684  break;
685  }
686 
687  // assume the order completely filled
688  fill.FillQuantity = order.Quantity;
689  fill.Status = OrderStatus.Filled;
690 
691  return fill;
692  }
693 
694  /// <summary>
695  /// Get data types the Security is subscribed to
696  /// </summary>
697  /// <param name="asset">Security which has subscribed data types</param>
698  protected override HashSet<Type> GetSubscribedTypes(Security asset)
699  {
700  var subscribedTypes = Parameters
702  .GetSubscriptionDataConfigs(asset.Symbol)
703  .ToHashSet(x => x.Type);
704 
705  if (subscribedTypes.Count == 0)
706  {
707  throw new InvalidOperationException($"Cannot perform fill for {asset.Symbol} because no data subscription were found.");
708  }
709 
710  return subscribedTypes;
711  }
712 
713  /// <summary>
714  /// Get current ask price for subscribed data
715  /// This method will try to get the most recent ask price data, so it will try to get tick quote first, then quote bar.
716  /// If no quote, tick or bar, is available (e.g. hourly data), use trade data with preference to tick data.
717  /// </summary>
718  /// <param name="asset">Security which has subscribed data types</param>
719  /// <param name="orderTime">Time the order was submitted</param>
720  /// <param name="message">Information about the best effort, whether prices are stale or need to use trade information</param>
721  private decimal GetBestEffortAskPrice(Security asset, DateTime orderTime, out string message)
722  {
723  message = string.Empty;
724  BaseData baseData = null;
725  var bestEffortAskPrice = 0m;
726 
727  // Define the cut off time to get the best effort bid or ask and whether the price is stale
728  var localOrderTime = orderTime.ConvertFromUtc(asset.Exchange.TimeZone);
729  var cutOffTime = localOrderTime.Add(-Parameters.StalePriceTimeSpan);
730 
731  var subscribedTypes = GetSubscribedTypes(asset);
732 
733  List<Tick> ticks = null;
734  var isTickSubscribed = subscribedTypes.Contains(typeof(Tick));
735 
736  if (isTickSubscribed)
737  {
738  ticks = asset.Cache.GetAll<Tick>().ToList();
739 
740  var quote = ticks.LastOrDefault(x => x.TickType == TickType.Quote && x.AskPrice > 0);
741  if (quote != null)
742  {
743  if (quote.EndTime >= cutOffTime)
744  {
745  return quote.AskPrice;
746  }
747 
748  baseData = quote;
749  bestEffortAskPrice = quote.AskPrice;
750  message = Messages.EquityFillModel.FilledWithQuoteTickData(asset, quote);
751  }
752  }
753 
754  if (subscribedTypes.Contains(typeof(QuoteBar)))
755  {
756  var quoteBar = asset.Cache.GetData<QuoteBar>();
757  if (quoteBar != null && (baseData == null || quoteBar.EndTime > baseData.EndTime))
758  {
759  if (quoteBar.EndTime >= cutOffTime)
760  {
761  return quoteBar.Ask?.Close ?? quoteBar.Close;
762  }
763 
764  baseData = quoteBar;
765  bestEffortAskPrice = quoteBar.Ask?.Close ?? quoteBar.Close;
766  message = Messages.EquityFillModel.FilledWithQuoteBarData(asset, quoteBar);
767  }
768  }
769 
770  if (isTickSubscribed)
771  {
772  var trade = ticks.LastOrDefault(x => x.TickType == TickType.Trade);
773  if (trade != null && (baseData == null || trade.EndTime > baseData.EndTime))
774  {
775  message = Messages.EquityFillModel.FilledWithTradeTickData(asset, trade);
776 
777  if (trade.EndTime >= cutOffTime)
778  {
779  return trade.Price;
780  }
781 
782  baseData = trade;
783  bestEffortAskPrice = trade.Price;
784  }
785  }
786 
787  if (subscribedTypes.Contains(typeof(TradeBar)))
788  {
789  var tradeBar = asset.Cache.GetData<TradeBar>();
790  if (tradeBar != null && (baseData == null || tradeBar.EndTime > baseData.EndTime))
791  {
792  message = Messages.EquityFillModel.FilledWithTradeBarData(asset, tradeBar);
793 
794  if (tradeBar.EndTime >= cutOffTime)
795  {
796  return tradeBar.Close;
797  }
798 
799  baseData = tradeBar;
800  bestEffortAskPrice = tradeBar.Close;
801  }
802  }
803 
804  if (baseData != null)
805  {
806  return bestEffortAskPrice;
807  }
808 
809  throw new InvalidOperationException(Messages.FillModel.NoMarketDataToGetAskPriceForFilling(asset, subscribedTypes));
810  }
811 
812  /// <summary>
813  /// Get current bid price for subscribed data
814  /// This method will try to get the most recent bid price data, so it will try to get tick quote first, then quote bar.
815  /// If no quote, tick or bar, is available (e.g. hourly data), use trade data with preference to tick data.
816  /// </summary>
817  /// <param name="asset">Security which has subscribed data types</param>
818  /// <param name="orderTime">Time the order was submitted</param>
819  /// <param name="message">Information about the best effort, whether prices are stale or need to use trade information</param>
820  private decimal GetBestEffortBidPrice(Security asset, DateTime orderTime, out string message)
821  {
822  message = string.Empty;
823  BaseData baseData = null;
824  var bestEffortBidPrice = 0m;
825 
826  // Define the cut off time to get the best effort bid or ask and whether the price is stale
827  var localOrderTime = orderTime.ConvertFromUtc(asset.Exchange.TimeZone);
828  var cutOffTime = localOrderTime.Add(-Parameters.StalePriceTimeSpan);
829 
830  var subscribedTypes = GetSubscribedTypes(asset);
831 
832  List<Tick> ticks = null;
833  var isTickSubscribed = subscribedTypes.Contains(typeof(Tick));
834 
835  if (isTickSubscribed)
836  {
837  ticks = asset.Cache.GetAll<Tick>().ToList();
838 
839  var quote = ticks.LastOrDefault(x => x.TickType == TickType.Quote && x.BidPrice > 0);
840  if (quote != null)
841  {
842  if (quote.EndTime >= cutOffTime)
843  {
844  return quote.BidPrice;
845  }
846 
847  baseData = quote;
848  bestEffortBidPrice = quote.BidPrice;
849  message = Messages.EquityFillModel.FilledWithQuoteTickData(asset, quote);
850  }
851  }
852 
853  if (subscribedTypes.Contains(typeof(QuoteBar)))
854  {
855  var quoteBar = asset.Cache.GetData<QuoteBar>();
856  if (quoteBar != null && (baseData == null || quoteBar.EndTime > baseData.EndTime))
857  {
858  if (quoteBar.EndTime >= cutOffTime)
859  {
860  return quoteBar.Bid?.Close ?? quoteBar.Close;
861  }
862 
863  baseData = quoteBar;
864  bestEffortBidPrice = quoteBar.Bid?.Close ?? quoteBar.Close;
865  message = Messages.EquityFillModel.FilledWithQuoteBarData(asset, quoteBar);
866  }
867  }
868 
869  if (isTickSubscribed)
870  {
871  var trade = ticks.LastOrDefault(x => x.TickType == TickType.Trade);
872  if (trade != null && (baseData == null || trade.EndTime > baseData.EndTime))
873  {
874  message = Messages.EquityFillModel.FilledWithTradeTickData(asset, trade);
875 
876  if (trade.EndTime >= cutOffTime)
877  {
878  return trade.Price;
879  }
880 
881  baseData = trade;
882  bestEffortBidPrice = trade.Price;
883  }
884  }
885 
886  if (subscribedTypes.Contains(typeof(TradeBar)))
887  {
888  var tradeBar = asset.Cache.GetData<TradeBar>();
889  if (tradeBar != null && (baseData == null || tradeBar.EndTime > baseData.EndTime))
890  {
891  message = Messages.EquityFillModel.FilledWithTradeBarData(asset, tradeBar);
892 
893  if (tradeBar.EndTime >= cutOffTime)
894  {
895  return tradeBar.Close;
896  }
897 
898  baseData = tradeBar;
899  bestEffortBidPrice = tradeBar.Close;
900  }
901  }
902 
903  if (baseData != null)
904  {
905  return bestEffortBidPrice;
906  }
907 
908  throw new InvalidOperationException(Messages.FillModel.NoMarketDataToGetBidPriceForFilling(asset, subscribedTypes));
909  }
910 
911  /// <summary>
912  /// Get current trade bar for subscribed data
913  /// This method will try to get the most recent trade bar after the order time,
914  /// so it will try to get tick trades first to create a trade bar, then trade bar.
915  /// </summary>
916  /// <param name="asset">Security which has subscribed data types</param>
917  /// <param name="orderTime">Time the order was submitted</param>
918  /// <returns>
919  /// A TradeBar object with the most recent trade information after the order close.
920  /// If there is no trade information or it is older than the order, returns null.
921  /// </returns>
922  private TradeBar GetBestEffortTradeBar(Security asset, DateTime orderTime)
923  {
924  TradeBar bestEffortTradeBar = null;
925 
926  var subscribedTypes = GetSubscribedTypes(asset);
927 
928  if (subscribedTypes.Contains(typeof(Tick)))
929  {
930  var tradeOpen = 0m;
931  var tradeHigh = decimal.MinValue;
932  var tradeLow = decimal.MaxValue;
933  var tradeClose = 0m;
934  var tradeVolume = 0m;
935  var startTimeUtc = DateTime.MinValue;
936  var endTimeUtc = DateTime.MinValue;
937 
938  var trades = asset.Cache.GetAll<Tick>().Where(x => x.TickType == TickType.Trade).ToList();
939  if (trades.Any())
940  {
941  foreach (var trade in trades)
942  {
943  if (tradeOpen == 0)
944  {
945  tradeOpen = trade.Price;
946  startTimeUtc = trade.Time;
947  }
948 
949  tradeHigh = Math.Max(tradeHigh, trade.Price);
950  tradeLow = Math.Min(tradeLow, trade.Price);
951  tradeClose = trade.Price;
952  tradeVolume += trade.Quantity;
953  endTimeUtc = trade.EndTime;
954  }
955 
956  bestEffortTradeBar = new TradeBar(startTimeUtc, asset.Symbol,
957  tradeOpen, tradeHigh, tradeLow, tradeClose, tradeVolume, endTimeUtc - startTimeUtc);
958  }
959  }
960  else if (subscribedTypes.Contains(typeof(TradeBar)))
961  {
962  bestEffortTradeBar = asset.Cache.GetData<TradeBar>();
963  }
964 
965  // Do not accept trade information older than the order
966  if (bestEffortTradeBar == null ||
967  bestEffortTradeBar.EndTime.ConvertToUtc(asset.Exchange.TimeZone) <= orderTime)
968  {
969  return null;
970  }
971 
972  return bestEffortTradeBar;
973  }
974 
975  /// <summary>
976  /// This is required due to a limitation in PythonNet to resolved
977  /// overriden methods. <see cref="GetPrices"/>
978  /// </summary>
979  protected override Prices GetPricesCheckingPythonWrapper(Security asset, OrderDirection direction)
980  {
981  if (PythonWrapper != null)
982  {
983  var prices = PythonWrapper.GetPricesInternal(asset, direction);
984  return new Prices(prices.EndTime, prices.Current, prices.Open, prices.High, prices.Low, prices.Close);
985  }
986  return GetPrices(asset, direction);
987  }
988 
989  /// <summary>
990  /// Get the minimum and maximum price for this security in the last bar:
991  /// </summary>
992  /// <param name="asset">Security asset we're checking</param>
993  /// <param name="direction">The order direction, decides whether to pick bid or ask</param>
994  protected override Prices GetPrices(Security asset, OrderDirection direction)
995  {
996  var low = asset.Low;
997  var high = asset.High;
998  var open = asset.Open;
999  var close = asset.Close;
1000  var current = asset.Price;
1001  var endTime = asset.Cache.GetData()?.EndTime ?? DateTime.MinValue;
1002 
1003  if (direction == OrderDirection.Hold)
1004  {
1005  return new Prices(endTime, current, open, high, low, close);
1006  }
1007 
1008  // Only fill with data types we are subscribed to
1009  var subscriptionTypes = Parameters.ConfigProvider
1010  .GetSubscriptionDataConfigs(asset.Symbol)
1011  .Select(x => x.Type).ToList();
1012  // Tick
1013  var tick = asset.Cache.GetData<Tick>();
1014  if (subscriptionTypes.Contains(typeof(Tick)) && tick != null)
1015  {
1016  var price = direction == OrderDirection.Sell ? tick.BidPrice : tick.AskPrice;
1017  if (price != 0m)
1018  {
1019  return new Prices(tick.EndTime, price, 0, 0, 0, 0);
1020  }
1021 
1022  // If the ask/bid spreads are not available for ticks, try the price
1023  price = tick.Price;
1024  if (price != 0m)
1025  {
1026  return new Prices(tick.EndTime, price, 0, 0, 0, 0);
1027  }
1028  }
1029 
1030  // Quote
1031  var quoteBar = asset.Cache.GetData<QuoteBar>();
1032  if (subscriptionTypes.Contains(typeof(QuoteBar)) && quoteBar != null)
1033  {
1034  var bar = direction == OrderDirection.Sell ? quoteBar.Bid : quoteBar.Ask;
1035  if (bar != null)
1036  {
1037  return new Prices(quoteBar.EndTime, bar);
1038  }
1039  }
1040 
1041  // Trade
1042  var tradeBar = asset.Cache.GetData<TradeBar>();
1043  if (subscriptionTypes.Contains(typeof(TradeBar)) && tradeBar != null)
1044  {
1045  return new Prices(tradeBar);
1046  }
1047 
1048  return new Prices(endTime, current, open, high, low, close);
1049  }
1050  }
1051 }