Lean  $LEAN_TAG$
Brokerage.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.Linq;
18 using Newtonsoft.Json;
19 using System.Threading;
20 using QuantConnect.Data;
21 using QuantConnect.Orders;
22 using QuantConnect.Logging;
23 using System.Threading.Tasks;
27 using System.Collections.Generic;
28 using System.Collections.Concurrent;
30 
32 {
33  /// <summary>
34  /// Represents the base Brokerage implementation. This provides logging on brokerage events.
35  /// </summary>
36  public abstract class Brokerage : IBrokerage
37  {
38  // 7:45 AM (New York time zone)
39  private static readonly TimeSpan LiveBrokerageCashSyncTime = new TimeSpan(7, 45, 0);
40 
41  private readonly object _performCashSyncReentranceGuard = new object();
42  private bool _syncedLiveBrokerageCashToday = true;
43  private long _lastSyncTimeTicks = DateTime.UtcNow.Ticks;
44 
45  /// <summary>
46  /// Event that fires each time the brokerage order id changes
47  /// </summary>
48  public event EventHandler<BrokerageOrderIdChangedEvent> OrderIdChanged;
49 
50  /// <summary>
51  /// Event that fires each time the status for a list of orders change
52  /// </summary>
53  public event EventHandler<List<OrderEvent>> OrdersStatusChanged;
54 
55  /// <summary>
56  /// Event that fires each time an order is updated in the brokerage side
57  /// </summary>
58  /// <remarks>
59  /// These are not status changes but mainly price changes, like the stop price of a trailing stop order
60  /// </remarks>
61  public event EventHandler<OrderUpdateEvent> OrderUpdated;
62 
63  /// <summary>
64  /// Event that fires each time a short option position is assigned
65  /// </summary>
66  public event EventHandler<OrderEvent> OptionPositionAssigned;
67 
68  /// <summary>
69  /// Event that fires each time an option position has changed
70  /// </summary>
71  public event EventHandler<OptionNotificationEventArgs> OptionNotification;
72 
73  /// <summary>
74  /// Event that fires each time there's a brokerage side generated order
75  /// </summary>
76  public event EventHandler<NewBrokerageOrderNotificationEventArgs> NewBrokerageOrderNotification;
77 
78  /// <summary>
79  /// Event that fires each time a delisting occurs
80  /// </summary>
81  public event EventHandler<DelistingNotificationEventArgs> DelistingNotification;
82 
83  /// <summary>
84  /// Event that fires each time a user's brokerage account is changed
85  /// </summary>
86  public event EventHandler<AccountEvent> AccountChanged;
87 
88  /// <summary>
89  /// Event that fires when an error is encountered in the brokerage
90  /// </summary>
91  public event EventHandler<BrokerageMessageEvent> Message;
92 
93  /// <summary>
94  /// Gets the name of the brokerage
95  /// </summary>
96  public string Name { get; }
97 
98  /// <summary>
99  /// Returns true if we're currently connected to the broker
100  /// </summary>
101  public abstract bool IsConnected { get; }
102 
103  /// <summary>
104  /// Creates a new Brokerage instance with the specified name
105  /// </summary>
106  /// <param name="name">The name of the brokerage</param>
107  protected Brokerage(string name)
108  {
109  Name = name;
110  }
111 
112  /// <summary>
113  /// Places a new order and assigns a new broker ID to the order
114  /// </summary>
115  /// <param name="order">The order to be placed</param>
116  /// <returns>True if the request for a new order has been placed, false otherwise</returns>
117  public abstract bool PlaceOrder(Order order);
118 
119  /// <summary>
120  /// Updates the order with the same id
121  /// </summary>
122  /// <param name="order">The new order information</param>
123  /// <returns>True if the request was made for the order to be updated, false otherwise</returns>
124  public abstract bool UpdateOrder(Order order);
125 
126  /// <summary>
127  /// Cancels the order with the specified ID
128  /// </summary>
129  /// <param name="order">The order to cancel</param>
130  /// <returns>True if the request was made for the order to be canceled, false otherwise</returns>
131  public abstract bool CancelOrder(Order order);
132 
133  /// <summary>
134  /// Connects the client to the broker's remote servers
135  /// </summary>
136  public abstract void Connect();
137 
138  /// <summary>
139  /// Disconnects the client from the broker's remote servers
140  /// </summary>
141  public abstract void Disconnect();
142 
143  /// <summary>
144  /// Dispose of the brokerage instance
145  /// </summary>
146  public virtual void Dispose()
147  {
148  // NOP
149  }
150 
151  /// <summary>
152  /// Event invocator for the OrderFilled event
153  /// </summary>
154  /// <param name="orderEvents">The list of order events</param>
155  protected virtual void OnOrderEvents(List<OrderEvent> orderEvents)
156  {
157  try
158  {
159  OrdersStatusChanged?.Invoke(this, orderEvents);
160  }
161  catch (Exception err)
162  {
163  Log.Error(err);
164  }
165  }
166 
167  /// <summary>
168  /// Event invocator for the OrderFilled event
169  /// </summary>
170  /// <param name="e">The order event</param>
171  protected virtual void OnOrderEvent(OrderEvent e)
172  {
173  OnOrderEvents(new List<OrderEvent> { e });
174  }
175 
176  /// <summary>
177  /// Event invocator for the OrderUpdated event
178  /// </summary>
179  /// <param name="e">The update event</param>
180  protected virtual void OnOrderUpdated(OrderUpdateEvent e)
181  {
182  try
183  {
184  OrderUpdated?.Invoke(this, e);
185  }
186  catch (Exception err)
187  {
188  Log.Error(err);
189  }
190  }
191 
192  /// <summary>
193  /// Event invocator for the OrderIdChanged event
194  /// </summary>
195  /// <param name="e">The BrokerageOrderIdChangedEvent</param>
197  {
198  try
199  {
200  OrderIdChanged?.Invoke(this, e);
201  }
202  catch (Exception err)
203  {
204  Log.Error(err);
205  }
206  }
207 
208  /// <summary>
209  /// Event invocator for the OptionPositionAssigned event
210  /// </summary>
211  /// <param name="e">The OrderEvent</param>
212  protected virtual void OnOptionPositionAssigned(OrderEvent e)
213  {
214  try
215  {
216  Log.Debug("Brokerage.OptionPositionAssigned(): " + e);
217 
218  OptionPositionAssigned?.Invoke(this, e);
219  }
220  catch (Exception err)
221  {
222  Log.Error(err);
223  }
224  }
225 
226  /// <summary>
227  /// Event invocator for the OptionNotification event
228  /// </summary>
229  /// <param name="e">The OptionNotification event arguments</param>
231  {
232  try
233  {
234  Log.Debug("Brokerage.OnOptionNotification(): " + e);
235 
236  OptionNotification?.Invoke(this, e);
237  }
238  catch (Exception err)
239  {
240  Log.Error(err);
241  }
242  }
243 
244  /// <summary>
245  /// Event invocator for the NewBrokerageOrderNotification event
246  /// </summary>
247  /// <param name="e">The NewBrokerageOrderNotification event arguments</param>
249  {
250  try
251  {
252  Log.Debug("Brokerage.OnNewBrokerageOrderNotification(): " + e);
253 
254  NewBrokerageOrderNotification?.Invoke(this, e);
255  }
256  catch (Exception err)
257  {
258  Log.Error(err);
259  }
260  }
261 
262  /// <summary>
263  /// Event invocator for the DelistingNotification event
264  /// </summary>
265  /// <param name="e">The DelistingNotification event arguments</param>
267  {
268  try
269  {
270  Log.Debug("Brokerage.OnDelistingNotification(): " + e);
271 
272  DelistingNotification?.Invoke(this, e);
273  }
274  catch (Exception err)
275  {
276  Log.Error(err);
277  }
278  }
279 
280  /// <summary>
281  /// Event invocator for the AccountChanged event
282  /// </summary>
283  /// <param name="e">The AccountEvent</param>
284  protected virtual void OnAccountChanged(AccountEvent e)
285  {
286  try
287  {
288  Log.Trace($"Brokerage.OnAccountChanged(): {e}");
289 
290  AccountChanged?.Invoke(this, e);
291  }
292  catch (Exception err)
293  {
294  Log.Error(err);
295  }
296  }
297 
298  /// <summary>
299  /// Event invocator for the Message event
300  /// </summary>
301  /// <param name="e">The error</param>
302  protected virtual void OnMessage(BrokerageMessageEvent e)
303  {
304  try
305  {
306  if (e.Type == BrokerageMessageType.Error)
307  {
308  Log.Error("Brokerage.OnMessage(): " + e);
309  }
310  else
311  {
312  Log.Trace("Brokerage.OnMessage(): " + e);
313  }
314 
315  Message?.Invoke(this, e);
316  }
317  catch (Exception err)
318  {
319  Log.Error(err);
320  }
321  }
322 
323  /// <summary>
324  /// Helper method that will try to get the live holdings from the provided brokerage data collection else will default to the algorithm state
325  /// </summary>
326  /// <remarks>Holdings will removed from the provided collection on the first call, since this method is expected to be called only
327  /// once on initialize, after which the algorithm should use Lean accounting</remarks>
328  protected virtual List<Holding> GetAccountHoldings(Dictionary<string, string> brokerageData, IEnumerable<Security> securities)
329  {
330  if (Log.DebuggingEnabled)
331  {
332  Log.Debug("Brokerage.GetAccountHoldings(): starting...");
333  }
334 
335  if (brokerageData != null && brokerageData.Remove("live-holdings", out var value) && !string.IsNullOrEmpty(value))
336  {
337  if (Log.DebuggingEnabled)
338  {
339  Log.Debug($"Brokerage.GetAccountHoldings(): raw value: {value}");
340  }
341 
342  // remove the key, we really only want to return the cached value on the first request
343  var result = JsonConvert.DeserializeObject<List<Holding>>(value);
344  if (result == null)
345  {
346  return new List<Holding>();
347  }
348  Log.Trace($"Brokerage.GetAccountHoldings(): sourcing holdings from provided brokerage data, found {result.Count} entries");
349  return result;
350  }
351 
352  return securities?.Where(security => security.Holdings.AbsoluteQuantity > 0)
353  .OrderBy(security => security.Symbol)
354  .Select(security => new Holding(security)).ToList() ?? new List<Holding>();
355  }
356 
357  /// <summary>
358  /// Helper method that will try to get the live cash balance from the provided brokerage data collection else will default to the algorithm state
359  /// </summary>
360  /// <remarks>Cash balance will removed from the provided collection on the first call, since this method is expected to be called only
361  /// once on initialize, after which the algorithm should use Lean accounting</remarks>
362  protected virtual List<CashAmount> GetCashBalance(Dictionary<string, string> brokerageData, CashBook cashBook)
363  {
364  if (Log.DebuggingEnabled)
365  {
366  Log.Debug("Brokerage.GetCashBalance(): starting...");
367  }
368 
369  if (brokerageData != null && brokerageData.Remove("live-cash-balance", out var value) && !string.IsNullOrEmpty(value))
370  {
371  // remove the key, we really only want to return the cached value on the first request
372  var result = JsonConvert.DeserializeObject<List<CashAmount>>(value);
373  if (result == null)
374  {
375  return new List<CashAmount>();
376  }
377  Log.Trace($"Brokerage.GetCashBalance(): sourcing cash balance from provided brokerage data, found {result.Count} entries");
378  return result;
379  }
380 
381  return cashBook?.Select(x => new CashAmount(x.Value.Amount, x.Value.Symbol)).ToList() ?? new List<CashAmount>();
382  }
383 
384  /// <summary>
385  /// Gets all open orders on the account.
386  /// NOTE: The order objects returned do not have QC order IDs.
387  /// </summary>
388  /// <returns>The open orders returned from IB</returns>
389  public abstract List<Order> GetOpenOrders();
390 
391  /// <summary>
392  /// Gets all holdings for the account
393  /// </summary>
394  /// <returns>The current holdings from the account</returns>
395  public abstract List<Holding> GetAccountHoldings();
396 
397  /// <summary>
398  /// Gets the current cash balance for each currency held in the brokerage account
399  /// </summary>
400  /// <returns>The current cash balance for each currency available for trading</returns>
401  public abstract List<CashAmount> GetCashBalance();
402 
403  /// <summary>
404  /// Specifies whether the brokerage will instantly update account balances
405  /// </summary>
406  public virtual bool AccountInstantlyUpdated => false;
407 
408  /// <summary>
409  /// Returns the brokerage account's base currency
410  /// </summary>
411  public virtual string AccountBaseCurrency { get; protected set; }
412 
413  /// <summary>
414  /// Gets the history for the requested security
415  /// </summary>
416  /// <param name="request">The historical data request</param>
417  /// <returns>An enumerable of bars covering the span specified in the request</returns>
418  public virtual IEnumerable<BaseData> GetHistory(HistoryRequest request)
419  {
420  return Enumerable.Empty<BaseData>();
421  }
422 
423  /// <summary>
424  /// Gets the position that might result given the specified order direction and the current holdings quantity.
425  /// This is useful for brokerages that require more specific direction information than provided by the OrderDirection enum
426  /// (e.g. Tradier differentiates Buy/Sell and BuyToOpen/BuyToCover/SellShort/SellToClose)
427  /// </summary>
428  /// <param name="orderDirection">The order direction</param>
429  /// <param name="holdingsQuantity">The current holdings quantity</param>
430  /// <returns>The order position</returns>
431  protected static OrderPosition GetOrderPosition(OrderDirection orderDirection, decimal holdingsQuantity)
432  {
433  return orderDirection switch
434  {
435  OrderDirection.Buy => holdingsQuantity >= 0 ? OrderPosition.BuyToOpen : OrderPosition.BuyToClose,
436  OrderDirection.Sell => holdingsQuantity <= 0 ? OrderPosition.SellToOpen : OrderPosition.SellToClose,
437  _ => throw new ArgumentOutOfRangeException(nameof(orderDirection), orderDirection, "Invalid order direction")
438  };
439  }
440 
441  #region IBrokerageCashSynchronizer implementation
442 
443  /// <summary>
444  /// Gets the date of the last sync (New York time zone)
445  /// </summary>
446  protected DateTime LastSyncDate => LastSyncDateTimeUtc.ConvertFromUtc(TimeZones.NewYork).Date;
447 
448  /// <summary>
449  /// Gets the datetime of the last sync (UTC)
450  /// </summary>
451  public DateTime LastSyncDateTimeUtc => new DateTime(Interlocked.Read(ref _lastSyncTimeTicks));
452 
453  /// <summary>
454  /// Returns whether the brokerage should perform the cash synchronization
455  /// </summary>
456  /// <param name="currentTimeUtc">The current time (UTC)</param>
457  /// <returns>True if the cash sync should be performed</returns>
458  public virtual bool ShouldPerformCashSync(DateTime currentTimeUtc)
459  {
460  // every morning flip this switch back
461  var currentTimeNewYork = currentTimeUtc.ConvertFromUtc(TimeZones.NewYork);
462  if (_syncedLiveBrokerageCashToday && currentTimeNewYork.Date != LastSyncDate)
463  {
464  _syncedLiveBrokerageCashToday = false;
465  }
466 
467  return !_syncedLiveBrokerageCashToday && currentTimeNewYork.TimeOfDay >= LiveBrokerageCashSyncTime;
468  }
469 
470  /// <summary>
471  /// Synchronizes the cashbook with the brokerage account
472  /// </summary>
473  /// <param name="algorithm">The algorithm instance</param>
474  /// <param name="currentTimeUtc">The current time (UTC)</param>
475  /// <param name="getTimeSinceLastFill">A function which returns the time elapsed since the last fill</param>
476  /// <returns>True if the cash sync was performed successfully</returns>
477  public virtual bool PerformCashSync(IAlgorithm algorithm, DateTime currentTimeUtc, Func<TimeSpan> getTimeSinceLastFill)
478  {
479  try
480  {
481  // prevent reentrance in this method
482  if (!Monitor.TryEnter(_performCashSyncReentranceGuard))
483  {
484  Log.Trace("Brokerage.PerformCashSync(): Reentrant call, cash sync not performed");
485  return false;
486  }
487 
488  Log.Trace("Brokerage.PerformCashSync(): Sync cash balance");
489 
490  List<CashAmount> balances = null;
491  try
492  {
493  balances = GetCashBalance();
494  }
495  catch (Exception err)
496  {
497  Log.Error(err, "Error in GetCashBalance:");
498  }
499 
500  // empty cash balance is valid, if there was No error/exception
501  if (balances == null)
502  {
503  Log.Trace("Brokerage.PerformCashSync(): No cash balances available, cash sync not performed");
504  return false;
505  }
506 
507  // Adds currency to the cashbook that the user might have deposited
508  foreach (var balance in balances)
509  {
510  if (!algorithm.Portfolio.CashBook.ContainsKey(balance.Currency))
511  {
512  Log.Trace($"Brokerage.PerformCashSync(): Unexpected cash found {balance.Currency} {balance.Amount}", true);
513  algorithm.Portfolio.SetCash(balance.Currency, balance.Amount, 0);
514  }
515  }
516 
517  var totalPorfolioValueThreshold = algorithm.Portfolio.TotalPortfolioValue * 0.02m;
518  // if we were returned our balances, update everything and flip our flag as having performed sync today
519  foreach (var kvp in algorithm.Portfolio.CashBook)
520  {
521  var cash = kvp.Value;
522 
523  //update the cash if the entry if found in the balances
524  var balanceCash = balances.Find(balance => balance.Currency == cash.Symbol);
525  if (balanceCash != default(CashAmount))
526  {
527  // compare in account currency
528  var delta = cash.Amount - balanceCash.Amount;
529  if (Math.Abs(algorithm.Portfolio.CashBook.ConvertToAccountCurrency(delta, cash.Symbol)) > totalPorfolioValueThreshold)
530  {
531  // log the delta between
532  Log.Trace($"Brokerage.PerformCashSync(): {balanceCash.Currency} Delta: {delta:0.00}", true);
533  }
534  algorithm.Portfolio.CashBook[cash.Symbol].SetAmount(balanceCash.Amount);
535  }
536  else
537  {
538  //Set the cash amount to zero if cash entry not found in the balances
539  Log.Trace($"Brokerage.PerformCashSync(): {cash.Symbol} was not found in brokerage cash balance, setting the amount to 0", true);
540  algorithm.Portfolio.CashBook[cash.Symbol].SetAmount(0);
541  }
542  }
543  _syncedLiveBrokerageCashToday = true;
544  _lastSyncTimeTicks = currentTimeUtc.Ticks;
545  }
546  finally
547  {
548  Monitor.Exit(_performCashSyncReentranceGuard);
549  }
550 
551  // fire off this task to check if we've had recent fills, if we have then we'll invalidate the cash sync
552  // and do it again until we're confident in it
553  Task.Delay(TimeSpan.FromSeconds(10)).ContinueWith(_ =>
554  {
555  // we want to make sure this is a good value, so check for any recent fills
556  if (getTimeSinceLastFill() <= TimeSpan.FromSeconds(20))
557  {
558  // this will cause us to come back in and reset cash again until we
559  // haven't processed a fill for +- 10 seconds of the set cash time
560  _syncedLiveBrokerageCashToday = false;
561  //_failedCashSyncAttempts = 0;
562  Log.Trace("Brokerage.PerformCashSync(): Unverified cash sync - resync required.");
563  }
564  else
565  {
566  Log.Trace("Brokerage.PerformCashSync(): Verified cash sync.");
567 
568  algorithm.Portfolio.LogMarginInformation();
569  }
570  });
571 
572  return true;
573  }
574 
575  #endregion
576 
577  #region CrossZeroOrder implementation
578 
579  /// <summary>
580  /// A dictionary to store the relationship between brokerage crossing orders and Lean orer id.
581  /// </summary>
582  private readonly ConcurrentDictionary<int, CrossZeroSecondOrderRequest> _leanOrderByBrokerageCrossingOrders = new();
583 
584  /// <summary>
585  /// An object used to lock the critical section in the <see cref="TryGetOrRemoveCrossZeroOrder"/> method,
586  /// ensuring thread safety when accessing the order collection.
587  /// </summary>
588  private object _lockCrossZeroObject = new();
589 
590  /// <summary>
591  /// A thread-safe dictionary that maps brokerage order IDs to their corresponding Order objects.
592  /// </summary>
593  /// <remarks>
594  /// This ConcurrentDictionary is used to maintain a mapping between Zero Cross brokerage order IDs and Lean Order objects.
595  /// The dictionary is protected and read-only, ensuring that it can only be modified by the class that declares it and cannot
596  /// be assigned a new instance after initialization.
597  /// </remarks>
598  protected ConcurrentDictionary<string, Order> LeanOrderByZeroCrossBrokerageOrderId { get; } = new();
599 
600  /// <summary>
601  /// Places an order that crosses zero (transitions from a short position to a long position or vice versa) and returns the response.
602  /// This method should be overridden in a derived class to implement brokerage-specific logic for placing such orders.
603  /// </summary>
604  /// <param name="crossZeroOrderRequest">The request object containing details of the cross zero order to be placed.</param>
605  /// <param name="isPlaceOrderWithLeanEvent">
606  /// A boolean indicating whether the order should be placed with triggering a Lean event.
607  /// Default is <c>true</c>, meaning Lean events will be triggered.
608  /// </param>
609  /// <returns>
610  /// A <see cref="CrossZeroOrderResponse"/> object indicating the result of the order placement.
611  /// </returns>
612  /// <exception cref="NotImplementedException">
613  /// Thrown if the method is not overridden in a derived class.
614  /// </exception>
615  protected virtual CrossZeroOrderResponse PlaceCrossZeroOrder(CrossZeroFirstOrderRequest crossZeroOrderRequest, bool isPlaceOrderWithLeanEvent = true)
616  {
617  throw new NotImplementedException($"{nameof(PlaceCrossZeroOrder)} method should be overridden in the derived class to handle brokerage-specific logic.");
618  }
619 
620  /// <summary>
621  /// Attempts to place an order that may cross the zero position.
622  /// If the order needs to be split into two parts due to crossing zero,
623  /// this method handles the split and placement accordingly.
624  /// </summary>
625  /// <param name="order">The order to be placed. Must not be <c>null</c>.</param>
626  /// <param name="holdingQuantity">The current holding quantity of the order's symbol.</param>
627  /// <returns>
628  /// <para><c>true</c> if the order crosses zero and the first part was successfully placed;</para>
629  /// <para><c>false</c> if the first part of the order could not be placed;</para>
630  /// <para><c>null</c> if the order does not cross zero.</para>
631  /// </returns>
632  /// <exception cref="ArgumentNullException">
633  /// Thrown if <paramref name="order"/> is <c>null</c>.
634  /// </exception>
635  protected bool? TryCrossZeroPositionOrder(Order order, decimal holdingQuantity)
636  {
637  if (order == null)
638  {
639  throw new ArgumentNullException(nameof(order), "The order parameter cannot be null.");
640  }
641 
642  // do we need to split the order into two pieces?
643  var crossesZero = BrokerageExtensions.OrderCrossesZero(holdingQuantity, order.Quantity);
644  if (crossesZero)
645  {
646  // first we need an order to close out the current position
647  var (firstOrderQuantity, secondOrderQuantity) = GetQuantityOnCrossPosition(holdingQuantity, order.Quantity);
648 
649  // Note: original quantity - already sell
650  var firstOrderPartRequest = new CrossZeroFirstOrderRequest(order, order.Type, firstOrderQuantity, holdingQuantity,
651  GetOrderPosition(order.Direction, holdingQuantity));
652 
653  // we actually can't place this order until the closingOrder is filled
654  // create another order for the rest, but we'll convert the order type to not be a stop
655  // but a market or a limit order
656  var secondOrderPartRequest = new CrossZeroSecondOrderRequest(order, order.Type, secondOrderQuantity, 0m,
657  GetOrderPosition(order.Direction, 0m), firstOrderPartRequest);
658 
659  _leanOrderByBrokerageCrossingOrders.AddOrUpdate(order.Id, secondOrderPartRequest);
660 
661  CrossZeroOrderResponse response;
662  lock (_lockCrossZeroObject)
663  {
664  // issue the first order to close the position
665  response = PlaceCrossZeroOrder(firstOrderPartRequest);
666  if (response.IsOrderPlacedSuccessfully)
667  {
668  var orderId = response.BrokerageOrderId;
669  if (!order.BrokerId.Contains(orderId))
670  {
671  order.BrokerId.Add(orderId);
672  }
673  }
674  }
675 
676  if (!response.IsOrderPlacedSuccessfully)
677  {
678  OnOrderEvent(new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero, $"{nameof(Brokerage)}: {response.Message}")
679  {
680  Status = OrderStatus.Invalid
681  });
682  // remove the contingent order if we weren't successful in placing the first
683  //ContingentOrderQueue contingent;
684  _leanOrderByBrokerageCrossingOrders.TryRemove(order.Id, out _);
685  return false;
686  }
687  return true;
688  }
689 
690  return null;
691  }
692 
693  /// <summary>
694  /// Determines whether the given Lean order crosses zero quantity based on the initial order quantity.
695  /// </summary>
696  /// <param name="leanOrder">The Lean order to check.</param>
697  /// <param name="quantity">The quantity to be updated based on whether the order crosses zero.</param>
698  /// <returns>
699  /// <c>true</c> if the Lean order does not cross zero quantity; otherwise, <c>false</c>.
700  /// </returns>
701  /// <exception cref="ArgumentNullException">Thrown when the <paramref name="leanOrder"/> is null.</exception>
702  protected bool TryGetUpdateCrossZeroOrderQuantity(Order leanOrder, out decimal quantity)
703  {
704  if (leanOrder == null)
705  {
706  throw new ArgumentNullException(nameof(leanOrder), "The provided leanOrder cannot be null.");
707  }
708 
709  // Check if the order is a CrossZeroOrder.
710  if (_leanOrderByBrokerageCrossingOrders.TryGetValue(leanOrder.Id, out var crossZeroOrderRequest))
711  {
712  // If it is a CrossZeroOrder, use the first part of the quantity for the update.
713  quantity = crossZeroOrderRequest.FirstPartCrossZeroOrder.OrderQuantity;
714  // If the quantities of the LeanOrder do not match, return false. Don't support.
715  if (crossZeroOrderRequest.LeanOrder.Quantity != leanOrder.Quantity)
716  {
717  return false;
718  }
719  }
720  else
721  {
722  // If it is not a CrossZeroOrder, use the original order quantity.
723  quantity = leanOrder.Quantity;
724  }
725  return true;
726  }
727 
728  /// <summary>
729  /// Attempts to retrieve or remove a cross-zero order based on the brokerage order ID and its filled status.
730  /// </summary>
731  /// <param name="brokerageOrderId">The unique identifier of the brokerage order.</param>
732  /// <param name="leanOrderStatus">The updated status of the order received from the brokerage</param>
733  /// <param name="leanOrder">
734  /// When this method returns, contains the <see cref="Order"/> object associated with the given brokerage order ID,
735  /// if the operation was successful; otherwise, null.
736  /// This parameter is passed uninitialized.
737  /// </param>
738  /// <returns>
739  /// <c>true</c> if the method successfully retrieves or removes the order; otherwise, <c>false</c>.
740  /// </returns>
741  /// <remarks>
742  /// The method locks on a private object to ensure thread safety while accessing the collection of orders.
743  /// If the order is filled, it is removed from the collection. If the order is partially filled,
744  /// it is retrieved but not removed. If the order is not found, the method returns <c>false</c>.
745  /// </remarks>
746  protected bool TryGetOrRemoveCrossZeroOrder(string brokerageOrderId, OrderStatus leanOrderStatus, out Order leanOrder)
747  {
748  lock (_lockCrossZeroObject)
749  {
750  if (LeanOrderByZeroCrossBrokerageOrderId.TryGetValue(brokerageOrderId, out leanOrder))
751  {
752  switch (leanOrderStatus)
753  {
754  case OrderStatus.Filled:
755  case OrderStatus.Canceled:
756  case OrderStatus.Invalid:
757  LeanOrderByZeroCrossBrokerageOrderId.TryRemove(brokerageOrderId, out var _);
758  break;
759  };
760  return true;
761  }
762  // Return false if the brokerage order ID does not correspond to a cross-zero order
763  return false;
764  }
765  }
766 
767  /// <summary>
768  /// Attempts to handle any remaining orders that cross the zero boundary.
769  /// </summary>
770  /// <param name="leanOrder">The order object that needs to be processed.</param>
771  /// <param name="orderEvent">The event object containing order event details.</param>
772  protected bool TryHandleRemainingCrossZeroOrder(Order leanOrder, OrderEvent orderEvent)
773  {
774  if (leanOrder != null && orderEvent != null && _leanOrderByBrokerageCrossingOrders.TryGetValue(leanOrder.Id, out var brokerageOrder))
775  {
776  switch (orderEvent.Status)
777  {
778  case OrderStatus.Filled:
779  // if we have a contingent that needs to be submitted then we can't respect the 'Filled' state from the order
780  // because the Lean order hasn't been technically filled yet, so mark it as 'PartiallyFilled'
781  orderEvent.Status = OrderStatus.PartiallyFilled;
782  _leanOrderByBrokerageCrossingOrders.Remove(leanOrder.Id, out var _);
783  break;
784  case OrderStatus.Canceled:
785  case OrderStatus.Invalid:
786  _leanOrderByBrokerageCrossingOrders.Remove(leanOrder.Id, out var _);
787  return false;
788  default:
789  return false;
790  };
791 
792  OnOrderEvent(orderEvent);
793 
794  Task.Run(() =>
795  {
796 #pragma warning disable CA1031 // Do not catch general exception types
797  try
798  {
799  var response = default(CrossZeroOrderResponse);
800  lock (_lockCrossZeroObject)
801  {
802  Log.Trace($"{nameof(Brokerage)}.{nameof(TryHandleRemainingCrossZeroOrder)}: Submit the second part of cross order by Id:{leanOrder.Id}");
803  response = PlaceCrossZeroOrder(brokerageOrder, false);
804 
805  if (response.IsOrderPlacedSuccessfully)
806  {
807  // add the new brokerage id for retrieval later
808  var orderId = response.BrokerageOrderId;
809  if (!leanOrder.BrokerId.Contains(orderId))
810  {
811  leanOrder.BrokerId.Add(orderId);
812  }
813 
814  // leanOrder is a clone, here we can add the new brokerage order Id for the second part of the cross zero
815  OnOrderIdChangedEvent(new BrokerageOrderIdChangedEvent { OrderId = leanOrder.Id, BrokerId = leanOrder.BrokerId });
816  LeanOrderByZeroCrossBrokerageOrderId.AddOrUpdate(orderId, leanOrder);
817  }
818  }
819 
820  if (!response.IsOrderPlacedSuccessfully)
821  {
822  // if we failed to place this order I don't know what to do, we've filled the first part
823  // and failed to place the second... strange. Should we invalidate the rest of the order??
824  Log.Error($"{nameof(Brokerage)}.{nameof(TryHandleRemainingCrossZeroOrder)}: Failed to submit contingent order.");
825  var message = $"{leanOrder.Symbol} Failed submitting the second part of cross order for " +
826  $"LeanOrderId: {leanOrder.Id.ToStringInvariant()} Filled - BrokerageOrderId: {response.BrokerageOrderId}. " +
827  $"{response.Message}";
828  OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "CrossZeroFailed", message));
829  OnOrderEvent(new OrderEvent(leanOrder, DateTime.UtcNow, OrderFee.Zero) { Status = OrderStatus.Canceled });
830  }
831  }
832  catch (Exception err)
833  {
834  Log.Error(err);
835  OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "CrossZeroOrderError", "Error occurred submitting cross zero order: " + err.Message));
836  OnOrderEvent(new OrderEvent(leanOrder, DateTime.UtcNow, OrderFee.Zero) { Status = OrderStatus.Canceled });
837  }
838 #pragma warning restore CA1031 // Do not catch general exception types
839  });
840  return true;
841  }
842  return false;
843  }
844 
845  /// <summary>
846  /// Calculates the quantities needed to close the current position and establish a new position based on the provided order.
847  /// </summary>
848  /// <param name="holdingQuantity">The quantity currently held in the position that needs to be closed.</param>
849  /// <param name="orderQuantity">The quantity defined in the new order to be established.</param>
850  /// <returns>
851  /// A tuple containing:
852  /// <list type="bullet">
853  /// <item>
854  /// <description>The quantity needed to close the current position (negative value).</description>
855  /// </item>
856  /// <item>
857  /// <description>The quantity needed to establish the new position.</description>
858  /// </item>
859  /// </list>
860  /// </returns>
861  private static (decimal closePostionQunatity, decimal newPositionQuantity) GetQuantityOnCrossPosition(decimal holdingQuantity, decimal orderQuantity)
862  {
863  // first we need an order to close out the current position
864  var firstOrderQuantity = -holdingQuantity;
865  var secondOrderQuantity = orderQuantity - firstOrderQuantity;
866 
867  return (firstOrderQuantity, secondOrderQuantity);
868  }
869 
870  #endregion
871  }
872 }