18 using Newtonsoft.Json;
19 using System.Threading;
23 using System.Threading.Tasks;
27 using System.Collections.Generic;
28 using System.Collections.Concurrent;
39 private static readonly TimeSpan LiveBrokerageCashSyncTime =
new TimeSpan(7, 45, 0);
41 private readonly
object _performCashSyncReentranceGuard =
new object();
42 private bool _syncedLiveBrokerageCashToday =
true;
43 private long _lastSyncTimeTicks = DateTime.UtcNow.Ticks;
91 public event EventHandler<BrokerageMessageEvent>
Message;
96 public string Name {
get; }
136 public abstract void Connect();
161 catch (Exception err)
186 catch (Exception err)
202 catch (Exception err)
216 Log.
Debug(
"Brokerage.OptionPositionAssigned(): " + e);
220 catch (Exception err)
234 Log.
Debug(
"Brokerage.OnOptionNotification(): " + e);
238 catch (Exception err)
252 Log.
Debug(
"Brokerage.OnNewBrokerageOrderNotification(): " + e);
256 catch (Exception err)
270 Log.
Debug(
"Brokerage.OnDelistingNotification(): " + e);
274 catch (Exception err)
288 Log.
Trace($
"Brokerage.OnAccountChanged(): {e}");
292 catch (Exception err)
308 Log.
Error(
"Brokerage.OnMessage(): " + e);
312 Log.
Trace(
"Brokerage.OnMessage(): " + e);
317 catch (Exception err)
328 protected virtual List<Holding>
GetAccountHoldings(Dictionary<string, string> brokerageData, IEnumerable<Security> securities)
332 Log.
Debug(
"Brokerage.GetAccountHoldings(): starting...");
335 if (brokerageData !=
null && brokerageData.Remove(
"live-holdings", out var value) && !
string.IsNullOrEmpty(value))
339 Log.
Debug($
"Brokerage.GetAccountHoldings(): raw value: {value}");
343 var result = JsonConvert.DeserializeObject<List<Holding>>(value);
346 return new List<Holding>();
348 Log.
Trace($
"Brokerage.GetAccountHoldings(): sourcing holdings from provided brokerage data, found {result.Count} entries");
352 return securities?.Where(security => security.Holdings.AbsoluteQuantity > 0)
353 .OrderBy(security => security.Symbol)
354 .Select(security =>
new Holding(security)).ToList() ??
new List<Holding>();
366 Log.
Debug(
"Brokerage.GetCashBalance(): starting...");
369 if (brokerageData !=
null && brokerageData.Remove(
"live-cash-balance", out var value) && !
string.IsNullOrEmpty(value))
372 var result = JsonConvert.DeserializeObject<List<CashAmount>>(value);
375 return new List<CashAmount>();
377 Log.
Trace($
"Brokerage.GetCashBalance(): sourcing cash balance from provided brokerage data, found {result.Count} entries");
381 return cashBook?.Select(x =>
new CashAmount(x.Value.Amount, x.Value.Symbol)).ToList() ??
new List<CashAmount>();
420 return Enumerable.Empty<
BaseData>();
433 return orderDirection
switch
437 _ =>
throw new ArgumentOutOfRangeException(nameof(orderDirection), orderDirection,
"Invalid order direction")
441 #region IBrokerageCashSynchronizer implementation
462 if (_syncedLiveBrokerageCashToday && currentTimeNewYork.Date !=
LastSyncDate)
464 _syncedLiveBrokerageCashToday =
false;
467 return !_syncedLiveBrokerageCashToday && currentTimeNewYork.TimeOfDay >= LiveBrokerageCashSyncTime;
482 if (!Monitor.TryEnter(_performCashSyncReentranceGuard))
484 Log.
Trace(
"Brokerage.PerformCashSync(): Reentrant call, cash sync not performed");
488 Log.
Trace(
"Brokerage.PerformCashSync(): Sync cash balance");
490 List<CashAmount> balances =
null;
495 catch (Exception err)
497 Log.
Error(err,
"Error in GetCashBalance:");
501 if (balances ==
null)
503 Log.
Trace(
"Brokerage.PerformCashSync(): No cash balances available, cash sync not performed");
508 foreach (var balance
in balances)
512 Log.
Trace($
"Brokerage.PerformCashSync(): Unexpected cash found {balance.Currency} {balance.Amount}",
true);
521 var cash = kvp.Value;
524 var balanceCash = balances.Find(balance => balance.Currency == cash.Symbol);
528 var delta = cash.Amount - balanceCash.Amount;
532 Log.
Trace($
"Brokerage.PerformCashSync(): {balanceCash.Currency} Delta: {delta:0.00}",
true);
539 Log.
Trace($
"Brokerage.PerformCashSync(): {cash.Symbol} was not found in brokerage cash balance, setting the amount to 0",
true);
543 _syncedLiveBrokerageCashToday =
true;
544 _lastSyncTimeTicks = currentTimeUtc.Ticks;
548 Monitor.Exit(_performCashSyncReentranceGuard);
553 Task.Delay(TimeSpan.FromSeconds(10)).ContinueWith(_ =>
556 if (getTimeSinceLastFill() <= TimeSpan.FromSeconds(20))
560 _syncedLiveBrokerageCashToday = false;
562 Log.Trace(
"Brokerage.PerformCashSync(): Unverified cash sync - resync required.");
566 Log.Trace(
"Brokerage.PerformCashSync(): Verified cash sync.");
568 algorithm.Portfolio.LogMarginInformation();
577 #region CrossZeroOrder implementation
582 private readonly ConcurrentDictionary<int, CrossZeroSecondOrderRequest> _leanOrderByBrokerageCrossingOrders =
new();
588 private object _lockCrossZeroObject =
new();
617 throw new NotImplementedException($
"{nameof(PlaceCrossZeroOrder)} method should be overridden in the derived class to handle brokerage-specific logic.");
639 throw new ArgumentNullException(nameof(order),
"The order parameter cannot be null.");
647 var (firstOrderQuantity, secondOrderQuantity) = GetQuantityOnCrossPosition(holdingQuantity, order.
Quantity);
659 _leanOrderByBrokerageCrossingOrders.AddOrUpdate(order.
Id, secondOrderPartRequest);
662 lock (_lockCrossZeroObject)
669 if (!order.
BrokerId.Contains(orderId))
680 Status = OrderStatus.Invalid
684 _leanOrderByBrokerageCrossingOrders.TryRemove(order.
Id, out _);
704 if (leanOrder ==
null)
706 throw new ArgumentNullException(nameof(leanOrder),
"The provided leanOrder cannot be null.");
710 if (_leanOrderByBrokerageCrossingOrders.TryGetValue(leanOrder.
Id, out var crossZeroOrderRequest))
713 quantity = crossZeroOrderRequest.FirstPartCrossZeroOrder.OrderQuantity;
715 if (crossZeroOrderRequest.LeanOrder.Quantity != leanOrder.
Quantity)
748 lock (_lockCrossZeroObject)
752 switch (leanOrderStatus)
774 if (leanOrder !=
null && orderEvent !=
null && _leanOrderByBrokerageCrossingOrders.TryGetValue(leanOrder.
Id, out var brokerageOrder))
776 switch (orderEvent.
Status)
782 _leanOrderByBrokerageCrossingOrders.Remove(leanOrder.
Id, out var _);
786 _leanOrderByBrokerageCrossingOrders.Remove(leanOrder.
Id, out var _);
796 #pragma warning disable CA1031 // Do not catch general exception types
800 lock (_lockCrossZeroObject)
802 Log.
Trace($
"{nameof(Brokerage)}.{nameof(TryHandleRemainingCrossZeroOrder)}: Submit the second part of cross order by Id:{leanOrder.Id}");
805 if (response.IsOrderPlacedSuccessfully)
808 var orderId = response.BrokerageOrderId;
809 if (!leanOrder.
BrokerId.Contains(orderId))
820 if (!response.IsOrderPlacedSuccessfully)
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}";
832 catch (Exception err)
838 #pragma warning restore CA1031 // Do not catch general exception types
861 private static (decimal closePostionQunatity, decimal newPositionQuantity) GetQuantityOnCrossPosition(decimal holdingQuantity, decimal orderQuantity)
864 var firstOrderQuantity = -holdingQuantity;
865 var secondOrderQuantity = orderQuantity - firstOrderQuantity;
867 return (firstOrderQuantity, secondOrderQuantity);