Lean  $LEAN_TAG$
BacktestingBrokerage.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.Concurrent;
18 using System.Collections.Generic;
19 using System.Linq;
23 using QuantConnect.Logging;
24 using QuantConnect.Orders;
29 using QuantConnect.Util;
30 
32 {
33  /// <summary>
34  /// Represents a brokerage to be used during backtesting. This is intended to be only be used with the BacktestingTransactionHandler
35  /// </summary>
38  {
39  // flag used to indicate whether or not we need to scan for
40  // fills, this is purely a performance concern is ConcurrentDictionary.IsEmpty
41  // is not exactly the fastest operation and Scan gets called at least twice per
42  // time loop
43  private bool _needsScan;
44  private DateTime _nextOptionAssignmentTime;
45  private readonly ConcurrentDictionary<int, Order> _pending;
46  private readonly object _needsScanLock = new object();
47  private readonly HashSet<Symbol> _pendingOptionAssignments = new HashSet<Symbol>();
48 
49  /// <summary>
50  /// This is the algorithm under test
51  /// </summary>
52  protected IAlgorithm Algorithm { get; init; }
53 
54  /// <summary>
55  /// Creates a new BacktestingBrokerage for the specified algorithm
56  /// </summary>
57  /// <param name="algorithm">The algorithm instance</param>
58  public BacktestingBrokerage(IAlgorithm algorithm)
59  : this(algorithm, "Backtesting Brokerage")
60  {
61  }
62 
63  /// <summary>
64  /// Creates a new BacktestingBrokerage for the specified algorithm
65  /// </summary>
66  /// <param name="algorithm">The algorithm instance</param>
67  /// <param name="name">The name of the brokerage</param>
68  protected BacktestingBrokerage(IAlgorithm algorithm, string name)
69  : base(name)
70  {
71  Algorithm = algorithm;
72  _pending = new ConcurrentDictionary<int, Order>();
73  }
74 
75  /// <summary>
76  /// Gets the connection status
77  /// </summary>
78  /// <remarks>
79  /// The BacktestingBrokerage is always connected
80  /// </remarks>
81  public override bool IsConnected => true;
82 
83  /// <summary>
84  /// Gets all open orders on the account
85  /// </summary>
86  /// <returns>The open orders returned from IB</returns>
87  public override List<Order> GetOpenOrders()
88  {
89  return Algorithm.Transactions.GetOpenOrders().ToList();
90  }
91 
92  /// <summary>
93  /// Gets all holdings for the account
94  /// </summary>
95  /// <returns>The current holdings from the account</returns>
96  public override List<Holding> GetAccountHoldings()
97  {
98  // grab everything from the portfolio with a non-zero absolute quantity
99  return (from kvp in Algorithm.Portfolio.Securities.OrderBy(x => x.Value.Symbol)
100  where kvp.Value.Holdings.AbsoluteQuantity > 0
101  select new Holding(kvp.Value)).ToList();
102  }
103 
104  /// <summary>
105  /// Gets the current cash balance for each currency held in the brokerage account
106  /// </summary>
107  /// <returns>The current cash balance for each currency available for trading</returns>
108  public override List<CashAmount> GetCashBalance()
109  {
110  return Algorithm.Portfolio.CashBook.Select(x => new CashAmount(x.Value.Amount, x.Value.Symbol)).ToList();
111  }
112 
113  /// <summary>
114  /// Places a new order and assigns a new broker ID to the order
115  /// </summary>
116  /// <param name="order">The order to be placed</param>
117  /// <returns>True if the request for a new order has been placed, false otherwise</returns>
118  public override bool PlaceOrder(Order order)
119  {
120  if (Algorithm.LiveMode)
121  {
122  Log.Trace("BacktestingBrokerage.PlaceOrder(): Type: " + order.Type + " Symbol: " + order.Symbol.Value + " Quantity: " + order.Quantity);
123  }
124 
125  if (order.Status == OrderStatus.New)
126  {
127  lock (_needsScanLock)
128  {
129  _needsScan = true;
130  SetPendingOrder(order);
131  }
132 
133  AddBrokerageOrderId(order);
134 
135  // fire off the event that says this order has been submitted
136  var submitted = new OrderEvent(order,
138  OrderFee.Zero)
139  { Status = OrderStatus.Submitted };
140  OnOrderEvent(submitted);
141 
142  return true;
143  }
144  return false;
145  }
146 
147  /// <summary>
148  /// Updates the order with the same ID
149  /// </summary>
150  /// <param name="order">The new order information</param>
151  /// <returns>True if the request was made for the order to be updated, false otherwise</returns>
152  public override bool UpdateOrder(Order order)
153  {
154  if (Algorithm.LiveMode)
155  {
156  Log.Trace("BacktestingBrokerage.UpdateOrder(): Symbol: " + order.Symbol.Value + " Quantity: " + order.Quantity + " Status: " + order.Status);
157  }
158 
159  lock (_needsScanLock)
160  {
161  Order pending;
162  if (!_pending.TryGetValue(order.Id, out pending))
163  {
164  // can't update something that isn't there
165  return false;
166  }
167 
168  _needsScan = true;
169  SetPendingOrder(order);
170  }
171 
172  AddBrokerageOrderId(order);
173 
174  // fire off the event that says this order has been updated
175  var updated = new OrderEvent(order,
177  OrderFee.Zero)
178  {
179  Status = OrderStatus.UpdateSubmitted
180  };
181  OnOrderEvent(updated);
182 
183  return true;
184  }
185 
186  /// <summary>
187  /// Cancels the order with the specified ID
188  /// </summary>
189  /// <param name="order">The order to cancel</param>
190  /// <returns>True if the request was made for the order to be canceled, false otherwise</returns>
191  public override bool CancelOrder(Order order)
192  {
193  if (Algorithm.LiveMode)
194  {
195  Log.Trace("BacktestingBrokerage.CancelOrder(): Symbol: " + order.Symbol.Value + " Quantity: " + order.Quantity);
196  }
197 
198  if (!order.TryGetGroupOrders(TryGetOrder, out var orders))
199  {
200  return false;
201  }
202 
203  var result = true;
204  foreach (var orderInGroup in orders)
205  {
206  lock (_needsScanLock)
207  {
208  if (!_pending.TryRemove(orderInGroup.Id, out var _))
209  {
210  // can't cancel something that isn't there,
211  // let's continue just in case some other order of the group has to be cancelled
212  result = false;
213  }
214  }
215 
216  AddBrokerageOrderId(orderInGroup);
217 
218  // fire off the event that says this order has been canceled
219  var canceled = new OrderEvent(orderInGroup,
221  OrderFee.Zero)
222  { Status = OrderStatus.Canceled };
223  OnOrderEvent(canceled);
224  }
225 
226  return result;
227  }
228 
229  /// <summary>
230  /// Scans all the outstanding orders and applies the algorithm model fills to generate the order events
231  /// </summary>
232  public virtual void Scan()
233  {
234  ProcessAssignmentOrders();
235 
236  lock (_needsScanLock)
237  {
238  // there's usually nothing in here
239  if (!_needsScan)
240  {
241  return;
242  }
243 
244  var stillNeedsScan = false;
245 
246  // process each pending order to produce fills/fire events
247  foreach (var kvp in _pending.OrderBySafe(x => x.Key))
248  {
249  var order = kvp.Value;
250  if (order == null)
251  {
252  Log.Error("BacktestingBrokerage.Scan(): Null pending order found: " + kvp.Key);
253  _pending.TryRemove(kvp.Key, out order);
254  continue;
255  }
256 
257  if (order.Status.IsClosed())
258  {
259  // this should never actually happen as we always remove closed orders as they happen
260  _pending.TryRemove(order.Id, out var _);
261  continue;
262  }
263 
264  // all order fills are processed on the next bar (except for market orders)
265  if (order.Time == Algorithm.UtcTime && order.Type != OrderType.Market && order.Type != OrderType.ComboMarket && order.Type != OrderType.OptionExercise)
266  {
267  stillNeedsScan = true;
268  continue;
269  }
270 
271  if (!order.TryGetGroupOrders(TryGetOrder, out var orders))
272  {
273  // an Order of the group is missing
274  stillNeedsScan = true;
275  continue;
276  }
277 
278  if(!orders.TryGetGroupOrdersSecurities(Algorithm.Portfolio, out var securities))
279  {
280  Log.Error($"BacktestingBrokerage.Scan(): Unable to process orders: [{string.Join(",", orders.Select(o => o.Id))}] The security no longer exists. UtcTime: {Algorithm.UtcTime}");
281  // invalidate the order in the algorithm before removing
282  RemoveOrders(orders, OrderStatus.Invalid);
283  continue;
284  }
285 
286  if (!TryOrderPreChecks(securities, out stillNeedsScan))
287  {
288  continue;
289  }
290 
291  // verify sure we have enough cash to perform the fill
292  HasSufficientBuyingPowerForOrderResult hasSufficientBuyingPowerResult;
293  try
294  {
295  hasSufficientBuyingPowerResult = Algorithm.Portfolio.HasSufficientBuyingPowerForOrder(orders);
296  }
297  catch (Exception err)
298  {
299  // if we threw an error just mark it as invalid and remove the order from our pending list
300  RemoveOrders(orders, OrderStatus.Invalid, err.Message);
301 
302  Log.Error(err);
303  Algorithm.Error($"Order Error: ids: [{string.Join(",", orders.Select(o => o.Id))}], Error executing margin models: {err.Message}");
304  continue;
305  }
306 
307  var fills = new List<OrderEvent>();
308  //Before we check this queued order make sure we have buying power:
309  if (hasSufficientBuyingPowerResult.IsSufficient)
310  {
311  //Model:
312  var security = securities[order];
313  var model = security.FillModel;
314 
315  //Based on the order type: refresh its model to get fill price and quantity
316  try
317  {
318  if (order.Type == OrderType.OptionExercise)
319  {
320  var option = (Option)security;
321  fills.AddRange(option.OptionExerciseModel.OptionExercise(option, order as OptionExerciseOrder));
322  }
323  else
324  {
325  var context = new FillModelParameters(
326  security,
327  order,
328  Algorithm.SubscriptionManager.SubscriptionDataConfigService,
330  securities,
331  OnOrderUpdated);
332 
333  // check if the fill should be emitted
334  var fill = model.Fill(context);
335  if (fill.All(x => order.TimeInForce.IsFillValid(security, order, x)))
336  {
337  fills.AddRange(fill);
338  }
339  }
340 
341  // invoke fee models for completely filled order events
342  foreach (var fill in fills)
343  {
344  if (fill.Status == OrderStatus.Filled)
345  {
346  // this check is provided for backwards compatibility of older user-defined fill models
347  // that may be performing fee computation inside the fill model w/out invoking the fee model
348  // TODO : This check can be removed in April, 2019 -- a 6-month window to upgrade (also, suspect small % of users, if any are impacted)
349  if (fill.OrderFee.Value.Amount == 0m)
350  {
351  // It could be the case the order is a combo order, then it contains legs with different quantities and security types.
352  // Therefore, we need to compute the fees based on the specific leg order and security
353  var legKVP = securities.Where(x => x.Key.Id == fill.OrderId).Single();
354  fill.OrderFee = legKVP.Value.FeeModel.GetOrderFee(new OrderFeeParameters(legKVP.Value, legKVP.Key));
355  }
356  }
357  }
358  }
359  catch (Exception err)
360  {
361  Log.Error(err);
362  Algorithm.Error($"Order Error: id: {order.Id}, Transaction model failed to fill for order type: {order.Type} with error: {err.Message}");
363  }
364  }
365  else if (order.Status == OrderStatus.CancelPending)
366  {
367  // the pending CancelOrderRequest will be handled during the next transaction handler run
368  continue;
369  }
370  else
371  {
372  // invalidate the order in the algorithm before removing
373  var message = securities.GetErrorMessage(hasSufficientBuyingPowerResult);
374  RemoveOrders(orders, OrderStatus.Invalid, message);
375 
376  Algorithm.Error(message);
377  continue;
378  }
379 
380  if (fills.Count == 0)
381  {
382  continue;
383  }
384 
385  List<OrderEvent> fillEvents = new(orders.Count);
386  List<Tuple<Order, OrderEvent>> positionAssignments = new(orders.Count);
387  foreach (var targetOrder in orders)
388  {
389  var orderFills = fills.Where(f => f.OrderId == targetOrder.Id);
390  foreach (var fill in orderFills)
391  {
392  // change in status or a new fill
393  if (targetOrder.Status != fill.Status || fill.FillQuantity != 0)
394  {
395  // we update the order status so we do not re process it if we re enter
396  // because of the call to OnOrderEvent.
397  // Note: this is done by the transaction handler but we have a clone of the order
398  targetOrder.Status = fill.Status;
399  fillEvents.Add(fill);
400  }
401 
402  if (fill.IsAssignment)
403  {
404  positionAssignments.Add(Tuple.Create(targetOrder, fill));
405  }
406  }
407  }
408 
409  OnOrderEvents(fillEvents);
410  foreach (var assignment in positionAssignments)
411  {
412  assignment.Item2.Message = assignment.Item1.Tag;
413  OnOptionPositionAssigned(assignment.Item2);
414  }
415 
416  if (fills.All(x => x.Status.IsClosed()))
417  {
418  foreach (var o in orders)
419  {
420  _pending.TryRemove(o.Id, out var _);
421  }
422  }
423  else
424  {
425  stillNeedsScan = true;
426  }
427  }
428 
429  // if we didn't fill then we need to continue to scan or
430  // if there are still pending orders
431  _needsScan = stillNeedsScan || !_pending.IsEmpty;
432  }
433  }
434 
435  /// <summary>
436  /// Invokes the <see cref="Brokerage.OnOrderUpdated(OrderUpdateEvent)" /> event with the given order updates.
437  /// </summary>
438  private void OnOrderUpdated(Order order)
439  {
440  switch (order.Type)
441  {
442  case OrderType.TrailingStop:
443  OnOrderUpdated(new OrderUpdateEvent { OrderId = order.Id, TrailingStopPrice = ((TrailingStopOrder)order).StopPrice });
444  break;
445 
446  case OrderType.StopLimit:
447  OnOrderUpdated(new OrderUpdateEvent { OrderId = order.Id, StopTriggered = ((StopLimitOrder)order).StopTriggered });
448  break;
449  }
450  }
451 
452  /// <summary>
453  /// Helper method to drive option assignment models
454  /// </summary>
455  private void ProcessAssignmentOrders()
456  {
457  if (Algorithm.UtcTime >= _nextOptionAssignmentTime)
458  {
459  _nextOptionAssignmentTime = Algorithm.UtcTime.RoundDown(Time.OneHour) + Time.OneHour;
460 
461  foreach (var security in Algorithm.Securities.Values
462  .Where(security => security.Symbol.SecurityType.IsOption() && security.Holdings.IsShort)
463  .OrderBy(security => security.Symbol.ID.Symbol))
464  {
465  var option = (Option)security;
466  var result = option.OptionAssignmentModel.GetAssignment(new OptionAssignmentParameters(option));
467  if (result != null && result.Quantity != 0)
468  {
469  if (!_pendingOptionAssignments.Add(option.Symbol))
470  {
471  throw new InvalidOperationException($"Duplicate option exercise order request for symbol {option.Symbol}. Please contact support");
472  }
473 
474  OnOptionNotification(new OptionNotificationEventArgs(option.Symbol, 0, result.Tag));
475  }
476  }
477  }
478  }
479 
480  /// <summary>
481  /// Event invocator for the OrderFilled event
482  /// </summary>
483  /// <param name="orderEvents">The list of order events</param>
484  protected override void OnOrderEvents(List<OrderEvent> orderEvents)
485  {
486  for (int i = 0; i < orderEvents.Count; i++)
487  {
488  _pendingOptionAssignments.Remove(orderEvents[i].Symbol);
489  }
490  base.OnOrderEvents(orderEvents);
491  }
492 
493  /// <summary>
494  /// The BacktestingBrokerage is always connected. This is a no-op.
495  /// </summary>
496  public override void Connect()
497  {
498  //NOP
499  }
500 
501  /// <summary>
502  /// The BacktestingBrokerage is always connected. This is a no-op.
503  /// </summary>
504  public override void Disconnect()
505  {
506  //NOP
507  }
508 
509  /// <summary>
510  /// Sets the pending order as a clone to prevent object reference nastiness
511  /// </summary>
512  /// <param name="order">The order to be added to the pending orders dictionary</param>
513  /// <returns></returns>
514  private void SetPendingOrder(Order order)
515  {
516  _pending[order.Id] = order;
517  }
518 
519  /// <summary>
520  /// Process delistings
521  /// </summary>
522  /// <param name="delistings">Delistings to process</param>
523  public void ProcessDelistings(Delistings delistings)
524  {
525  // Process our delistings, important to do options first because of possibility of having future options contracts
526  // and underlying future delisting at the same time.
527  foreach (var delisting in delistings?.Values.OrderBy(x => !x.Symbol.SecurityType.IsOption()))
528  {
529  Log.Debug($"BacktestingBrokerage.ProcessDelistings(): Delisting {delisting.Type}: {delisting.Symbol.Value}, UtcTime: {Algorithm.UtcTime}, DelistingTime: {delisting.Time}");
530  if (delisting.Type == DelistingType.Warning)
531  {
532  // We do nothing with warnings
533  continue;
534  }
535 
536  var security = Algorithm.Securities[delisting.Symbol];
537 
538  if (security.Symbol.SecurityType.IsOption())
539  {
540  // Process the option delisting
541  OnOptionNotification(new OptionNotificationEventArgs(delisting.Symbol, 0));
542  }
543  else
544  {
545  // Any other type of delisting
547  }
548 
549  // the subscription are getting removed from the data feed because they end
550  // remove security from all universes
551  foreach (var ukvp in Algorithm.UniverseManager)
552  {
553  var universe = ukvp.Value;
554  if (universe.ContainsMember(security.Symbol))
555  {
556  var userUniverse = universe as UserDefinedUniverse;
557  if (userUniverse != null)
558  {
559  userUniverse.Remove(security.Symbol);
560  }
561  else
562  {
563  universe.RemoveMember(Algorithm.UtcTime, security);
564  }
565  }
566  }
567 
568  if (!Algorithm.IsWarmingUp)
569  {
570  // Cancel any other orders
571  var cancelledOrders = Algorithm.Transactions.CancelOpenOrders(delisting.Symbol);
572  foreach (var cancelledOrder in cancelledOrders)
573  {
574  Log.Trace("AlgorithmManager.Run(): " + cancelledOrder);
575  }
576  }
577  }
578  }
579 
580  private void RemoveOrders(List<Order> orders, OrderStatus orderStatus, string message = "")
581  {
582  var orderEvents = new List<OrderEvent>(orders.Count);
583  for (var i = 0; i < orders.Count; i++)
584  {
585  var order = orders[i];
586  orderEvents.Add(new OrderEvent(order, Algorithm.UtcTime, OrderFee.Zero, message) { Status = orderStatus });
587  _pending.TryRemove(order.Id, out var _);
588  }
589 
590  OnOrderEvents(orderEvents);
591  }
592 
593  private bool TryOrderPreChecks(Dictionary<Order, Security> ordersSecurities, out bool stillNeedsScan)
594  {
595  var result = true;
596  stillNeedsScan = false;
597 
598  var removedOrdersIds = new HashSet<int>();
599 
600  foreach (var kvp in ordersSecurities)
601  {
602  var order = kvp.Key;
603  var security = kvp.Value;
604 
605  if (order.Type == OrderType.MarketOnOpen)
606  {
607  // This is a performance improvement:
608  // Since MOO should never fill on the same bar or on stale data (see FillModel)
609  // the order can remain unfilled for multiple 'scans', so we want to avoid
610  // margin and portfolio calculations since they are expensive
611  var currentBar = security.GetLastData();
612  var localOrderTime = order.Time.ConvertFromUtc(security.Exchange.TimeZone);
613  if (currentBar == null || localOrderTime >= currentBar.EndTime)
614  {
615  stillNeedsScan = true;
616  result = false;
617  break;
618  }
619  }
620 
621  // check if the time in force handler allows fills
622  if (order.TimeInForce.IsOrderExpired(security, order))
623  {
624  // We remove all orders in the combo
625  RemoveOrders(ordersSecurities.Select(kvp => kvp.Key).ToList(), OrderStatus.Canceled, "The order has expired.");
626  result = false;
627  break;
628  }
629 
630  // check if we would actually be able to fill this
631  if (!Algorithm.BrokerageModel.CanExecuteOrder(security, order))
632  {
633  result = false;
634  break;
635  }
636  }
637 
638  return result;
639  }
640 
641  private Order TryGetOrder(int orderId)
642  {
643  _pending.TryGetValue(orderId, out var order);
644  return order;
645  }
646 
647  private static void AddBrokerageOrderId(Order order)
648  {
649  var orderId = order.Id.ToStringInvariant();
650  if (!order.BrokerId.Contains(orderId))
651  {
652  order.BrokerId.Add(orderId);
653  }
654  }
655  }
656 }