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))
338 var result = JsonConvert.DeserializeObject<List<Holding>>(value);
341 return new List<Holding>();
343 Log.
Trace($
"Brokerage.GetAccountHoldings(): sourcing holdings from provided brokerage data, found {result.Count} entries");
347 return securities?.Where(security => security.Holdings.AbsoluteQuantity > 0)
348 .OrderBy(security => security.Symbol)
349 .Select(security =>
new Holding(security)).ToList() ??
new List<Holding>();
361 Log.
Debug(
"Brokerage.GetCashBalance(): starting...");
364 if (brokerageData !=
null && brokerageData.Remove(
"live-cash-balance", out var value) && !
string.IsNullOrEmpty(value))
367 var result = JsonConvert.DeserializeObject<List<CashAmount>>(value);
370 return new List<CashAmount>();
372 Log.
Trace($
"Brokerage.GetCashBalance(): sourcing cash balance from provided brokerage data, found {result.Count} entries");
376 return cashBook?.Select(x =>
new CashAmount(x.Value.Amount, x.Value.Symbol)).ToList() ??
new List<CashAmount>();
415 return Enumerable.Empty<
BaseData>();
428 return orderDirection
switch
432 _ =>
throw new ArgumentOutOfRangeException(nameof(orderDirection), orderDirection,
"Invalid order direction")
436 #region IBrokerageCashSynchronizer implementation
457 if (_syncedLiveBrokerageCashToday && currentTimeNewYork.Date !=
LastSyncDate)
459 _syncedLiveBrokerageCashToday =
false;
462 return !_syncedLiveBrokerageCashToday && currentTimeNewYork.TimeOfDay >= LiveBrokerageCashSyncTime;
477 if (!Monitor.TryEnter(_performCashSyncReentranceGuard))
479 Log.
Trace(
"Brokerage.PerformCashSync(): Reentrant call, cash sync not performed");
483 Log.
Trace(
"Brokerage.PerformCashSync(): Sync cash balance");
485 List<CashAmount> balances =
null;
490 catch (Exception err)
492 Log.
Error(err,
"Error in GetCashBalance:");
496 if (balances ==
null)
498 Log.
Trace(
"Brokerage.PerformCashSync(): No cash balances available, cash sync not performed");
503 foreach (var balance
in balances)
507 Log.
Trace($
"Brokerage.PerformCashSync(): Unexpected cash found {balance.Currency} {balance.Amount}",
true);
516 var cash = kvp.Value;
519 var balanceCash = balances.Find(balance => balance.Currency == cash.Symbol);
523 var delta = cash.Amount - balanceCash.Amount;
527 Log.
Trace($
"Brokerage.PerformCashSync(): {balanceCash.Currency} Delta: {delta:0.00}",
true);
534 Log.
Trace($
"Brokerage.PerformCashSync(): {cash.Symbol} was not found in brokerage cash balance, setting the amount to 0",
true);
538 _syncedLiveBrokerageCashToday =
true;
539 _lastSyncTimeTicks = currentTimeUtc.Ticks;
543 Monitor.Exit(_performCashSyncReentranceGuard);
548 Task.Delay(TimeSpan.FromSeconds(10)).ContinueWith(_ =>
551 if (getTimeSinceLastFill() <= TimeSpan.FromSeconds(20))
555 _syncedLiveBrokerageCashToday = false;
557 Log.Trace(
"Brokerage.PerformCashSync(): Unverified cash sync - resync required.");
561 Log.Trace(
"Brokerage.PerformCashSync(): Verified cash sync.");
563 algorithm.Portfolio.LogMarginInformation();
572 #region CrossZeroOrder implementation
577 private readonly ConcurrentDictionary<int, CrossZeroSecondOrderRequest> _leanOrderByBrokerageCrossingOrders =
new();
583 private object _lockCrossZeroObject =
new();
612 throw new NotImplementedException($
"{nameof(PlaceCrossZeroOrder)} method should be overridden in the derived class to handle brokerage-specific logic.");
634 throw new ArgumentNullException(nameof(order),
"The order parameter cannot be null.");
642 var (firstOrderQuantity, secondOrderQuantity) = GetQuantityOnCrossPosition(holdingQuantity, order.
Quantity);
654 _leanOrderByBrokerageCrossingOrders.AddOrUpdate(order.
Id, secondOrderPartRequest);
657 lock (_lockCrossZeroObject)
664 if (!order.
BrokerId.Contains(orderId))
675 Status = OrderStatus.Invalid
679 _leanOrderByBrokerageCrossingOrders.TryRemove(order.
Id, out _);
699 if (leanOrder ==
null)
701 throw new ArgumentNullException(nameof(leanOrder),
"The provided leanOrder cannot be null.");
705 if (_leanOrderByBrokerageCrossingOrders.TryGetValue(leanOrder.
Id, out var crossZeroOrderRequest))
708 quantity = crossZeroOrderRequest.FirstPartCrossZeroOrder.OrderQuantity;
710 if (crossZeroOrderRequest.LeanOrder.Quantity != leanOrder.
Quantity)
743 lock (_lockCrossZeroObject)
747 switch (leanOrderStatus)
769 if (leanOrder !=
null && orderEvent !=
null && _leanOrderByBrokerageCrossingOrders.TryGetValue(leanOrder.
Id, out var brokerageOrder))
771 switch (orderEvent.
Status)
777 _leanOrderByBrokerageCrossingOrders.Remove(leanOrder.
Id, out var _);
781 _leanOrderByBrokerageCrossingOrders.Remove(leanOrder.
Id, out var _);
791 #pragma warning disable CA1031 // Do not catch general exception types
795 lock (_lockCrossZeroObject)
797 Log.
Trace($
"{nameof(Brokerage)}.{nameof(TryHandleRemainingCrossZeroOrder)}: Submit the second part of cross order by Id:{leanOrder.Id}");
800 if (response.IsOrderPlacedSuccessfully)
803 var orderId = response.BrokerageOrderId;
804 if (!leanOrder.
BrokerId.Contains(orderId))
815 if (!response.IsOrderPlacedSuccessfully)
819 Log.
Error($
"{nameof(Brokerage)}.{nameof(TryHandleRemainingCrossZeroOrder)}: Failed to submit contingent order.");
820 var message = $
"{leanOrder.Symbol} Failed submitting the second part of cross order for " +
821 $
"LeanOrderId: {leanOrder.Id.ToStringInvariant()} Filled - BrokerageOrderId: {response.BrokerageOrderId}. " +
822 $
"{response.Message}";
827 catch (Exception err)
833 #pragma warning restore CA1031 // Do not catch general exception types
856 private static (decimal closePostionQunatity, decimal newPositionQuantity) GetQuantityOnCrossPosition(decimal holdingQuantity, decimal orderQuantity)
859 var firstOrderQuantity = -holdingQuantity;
860 var secondOrderQuantity = orderQuantity - firstOrderQuantity;
862 return (firstOrderQuantity, secondOrderQuantity);