17 using System.Collections.Concurrent;
18 using System.Collections.Generic;
20 using System.Runtime.CompilerServices;
21 using System.Threading;
46 private bool _brokerageIsBacktesting;
47 private bool _loggedFeeAdjustmentWarning;
50 private int _totalOrderCount;
53 private bool _firstRoundOffMessage =
false;
56 private long _lastFillTimeTicks;
58 private const int MaxCashSyncAttempts = 5;
59 private int _failedCashSyncAttempts;
67 private Thread _processingThread;
68 private readonly CancellationTokenSource _cancellationTokenSource =
new CancellationTokenSource();
70 private readonly ConcurrentQueue<OrderEvent> _orderEvents =
new ConcurrentQueue<OrderEvent>();
76 private readonly ConcurrentDictionary<int, Order> _completeOrders =
new ConcurrentDictionary<int, Order>();
82 private readonly ConcurrentDictionary<int, Order> _openOrders =
new ConcurrentDictionary<int, Order>();
89 private readonly ConcurrentDictionary<int, OrderTicket> _openOrderTickets =
new ConcurrentDictionary<int, OrderTicket>();
96 private readonly ConcurrentDictionary<int, OrderTicket> _completeOrderTickets =
new ConcurrentDictionary<int, OrderTicket>();
101 private readonly Dictionary<Symbol, DataNormalizationMode> _priceAdjustmentModes =
new Dictionary<Symbol, DataNormalizationMode>();
110 private readonly
object _lockHandleOrderEvent =
new object();
120 public ConcurrentDictionary<int, Order>
Orders
124 return _completeOrders;
136 public ConcurrentDictionary<int, OrderTicket>
OrderTickets
140 return _completeOrderTickets;
157 if (brokerage ==
null)
159 throw new ArgumentNullException(nameof(brokerage));
164 _resultHandler = resultHandler;
166 _brokerage = brokerage;
171 HandleOrderEvents(orderEvents);
176 HandleAccountChanged(account);
181 HandlePositionAssigned(fill);
186 HandleOptionNotification(e);
191 HandleNewBrokerageSideOrder(e);
196 HandleDelistingNotification(e);
201 HandlerBrokerageOrderIdChangedEvent(e);
206 HandleOrderUpdated(e);
211 _algorithm = algorithm;
227 _processingThread =
new Thread(
Run) { IsBackground =
true, Name =
"Transaction Thread" };
228 _processingThread.Start();
237 #region Order Request Processing
247 Log.
Trace(
"BrokerageTransactionHandler.Process(): " + request);
264 throw new ArgumentOutOfRangeException();
279 var shortable =
true;
287 var message = GetShortableErrorMessage(request.
Symbol, request.
Quantity);
291 _algorithm.
Debug($
"Warning: {message}");
302 Interlocked.Increment(ref _totalOrderCount);
304 if (response.IsSuccess)
306 _openOrderTickets.TryAdd(ticket.OrderId, ticket);
307 _completeOrderTickets.TryAdd(ticket.OrderId, ticket);
320 ?
"Algorithm warming up."
321 : response.ErrorMessage;
324 var security = _algorithm.
Securities[order.Symbol];
325 order.PriceCurrency = security.SymbolProperties.QuoteCurrency;
328 order.Tag = orderTag;
329 ticket.SetOrder(order);
330 _completeOrderTickets.TryAdd(ticket.OrderId, ticket);
331 _completeOrders.TryAdd(order.Id, order);
348 if (!ticket.
OrderSet.WaitOne(orderSetTimeout))
350 Log.
Error(
"BrokerageTransactionHandler.WaitForOrderSubmission(): " +
351 $
"The order request (Id={ticket.OrderId}) was not submitted within {orderSetTimeout.TotalSeconds} second(s).");
363 if (!_completeOrderTickets.TryGetValue(request.
OrderId, out ticket))
368 ticket.AddUpdateRequest(request);
373 var order = GetOrderByIdInternal(request.
OrderId);
374 var orderQuantity = request.
Quantity ?? ticket.Quantity;
376 var shortable =
true;
379 shortable = _algorithm.
Shortable(ticket.Symbol, orderQuantity, order.Id);
381 if (_algorithm.
LiveMode && !shortable)
386 _algorithm.
Debug($
"Warning: {GetShortableErrorMessage(ticket.Symbol, ticket.Quantity)}");
393 Log.
Error(
"BrokerageTransactionHandler.Update(): Cannot update a null order");
399 Log.
Error(
"BrokerageTransactionHandler.Update(): Cannot update a pending submit order with status " + order.Status);
405 Log.
Error(
"BrokerageTransactionHandler.Update(): Cannot update closed order with status " + order.Status);
419 GetShortableErrorMessage(ticket.Symbol, ticket.Quantity));
429 catch (Exception err)
445 if (!_completeOrderTickets.TryGetValue(request.
OrderId, out ticket))
447 Log.
Error(
"BrokerageTransactionHandler.CancelOrder(): Unable to locate ticket for order.");
455 if (!ticket.TrySetCancelRequest(request))
463 var order = GetOrderByIdInternal(request.
OrderId);
464 if (order !=
null && request.
Tag !=
null)
466 order.Tag = request.
Tag;
470 Log.
Error(
"BrokerageTransactionHandler.CancelOrder(): Cannot find this id.");
475 Log.
Error(
"BrokerageTransactionHandler.CancelOrder(): Cannot cancel order with status: " + order.Status);
478 else if (order.Status.IsClosed())
480 Log.
Error(
"BrokerageTransactionHandler.CancelOrder(): Cannot cancel order already " + order.Status);
503 catch (Exception err)
517 public IEnumerable<OrderTicket>
GetOrderTickets(Func<OrderTicket, bool> filter =
null)
519 return _completeOrderTickets.Select(x => x.Value).Where(filter ?? (x =>
true));
529 return _openOrderTickets.Select(x => x.Value).Where(filter ?? (x =>
true));
540 _completeOrderTickets.TryGetValue(orderId, out ticket);
553 Order order = GetOrderByIdInternal(orderId);
554 return order?.
Clone();
557 private Order GetOrderByIdInternal(
int orderId)
560 return _completeOrders.TryGetValue(orderId, out order) ? order :
null;
572 if (openOrders.Count > 0
574 && (openOrders[0].GroupOrderManager ==
null || openOrders[0].GroupOrderManager.Count == openOrders.Count))
582 private static List<Order>
GetOrdersByBrokerageId(
string brokerageId, ConcurrentDictionary<int, Order> orders)
585 .Where(x => x.Value.BrokerId.Contains(brokerageId))
586 .Select(kvp => kvp.Value.Clone())
596 public IEnumerable<Order>
GetOrders(Func<Order, bool> filter =
null)
601 return _completeOrders.Select(x => x.Value).Where(filter).Select(x => x.Clone());
603 return _completeOrders.Select(x => x.Value).Select(x => x.Clone());
616 return _openOrders.Select(x => x.Value).Where(filter).Select(x => x.Clone()).ToList();
618 return _openOrders.Select(x => x.Value).Select(x => x.Clone()).ToList();
628 foreach (var request
in _orderRequestQueue.GetConsumingEnumerable(_cancellationTokenSource.Token))
634 catch (Exception err)
637 _algorithm.SetRuntimeError(err,
"HandleOrderRequest");
640 if (_processingThread !=
null)
642 Log.
Trace(
"BrokerageTransactionHandler.Run(): Ending Thread...");
667 Log.
Error(
"BrokerageTransactionHandler.ProcessSynchronousEvents(): Timed out waiting for request queue to finish processing.");
682 if (++_failedCashSyncAttempts >= MaxCashSyncAttempts)
684 throw new Exception(
"The maximum number of attempts for brokerage cash sync has been reached.");
691 const int maxOrdersToKeep = 10000;
692 if (_completeOrders.Count < maxOrdersToKeep + 1)
697 Log.
Debug(
"BrokerageTransactionHandler.ProcessSynchronousEvents(): Start removing old orders...");
698 var max = _completeOrders.Max(x => x.Key);
699 var lowestOrderIdToKeep = max - maxOrdersToKeep;
700 foreach (var item
in _completeOrders.Where(x => x.Key <= lowestOrderIdToKeep))
704 _completeOrders.TryRemove(item.Key, out value);
705 _completeOrderTickets.TryRemove(item.Key, out ticket);
708 Log.
Debug($
"BrokerageTransactionHandler.ProcessSynchronousEvents(): New order count {_completeOrders.Count}. Exit");
729 var orderTicket = order.ToOrderTicket(algorithm.
Transactions);
731 SetPriceAdjustmentMode(order, algorithm);
733 _openOrders.AddOrUpdate(order.
Id, order, (i, o) => order);
734 _completeOrders.AddOrUpdate(order.
Id, order, (i, o) => order);
735 _openOrderTickets.AddOrUpdate(order.
Id, orderTicket);
736 _completeOrderTickets.AddOrUpdate(order.
Id, orderTicket);
738 Interlocked.Increment(ref _totalOrderCount);
747 var timeout = TimeSpan.FromSeconds(60);
748 if (_processingThread !=
null)
753 Log.
Error(
"BrokerageTransactionHandler.Exit(): Exceed timeout: " + (
int)(timeout.TotalSeconds) +
" seconds.");
757 _processingThread?.StopSafely(timeout, _cancellationTokenSource);
759 _cancellationTokenSource.DisposeSafely();
782 throw new ArgumentOutOfRangeException();
798 var security = _algorithm.
Securities[order.Symbol];
799 order.PriceCurrency = security.SymbolProperties.QuoteCurrency;
800 if (
string.IsNullOrEmpty(order.Tag))
802 order.Tag = order.GetDefaultTag();
808 if (!_openOrders.TryAdd(order.Id, order) || !_completeOrders.TryAdd(order.Id, order))
810 Log.
Error(
"BrokerageTransactionHandler.HandleSubmitOrderRequest(): Unable to add new order, order not processed.");
813 if (!_completeOrderTickets.TryGetValue(order.Id, out ticket))
815 Log.
Error(
"BrokerageTransactionHandler.HandleSubmitOrderRequest(): Unable to retrieve order ticket, order not processed.");
819 var comboIsReady = order.TryGetGroupOrders(TryGetOrder, out var orders);
820 var comboSecuritiesFound = orders.TryGetGroupOrdersSecurities(_algorithm.
Portfolio, out var securities);
826 order.OrderSubmissionData =
new OrderSubmissionData(security.BidPrice, security.AskPrice, security.Close);
829 SetPriceAdjustmentMode(order, _algorithm);
832 ticket.SetOrder(order);
840 if (orders.Any(o => o.Quantity == 0))
843 _algorithm.
Error(response.ErrorMessage);
845 InvalidateOrders(orders, response.ErrorMessage);
849 if (!comboSecuritiesFound)
852 _algorithm.
Error(response.ErrorMessage);
854 InvalidateOrders(orders, response.ErrorMessage);
859 if (!HasSufficientBuyingPowerForOrders(order, request, out var validationResult, orders, securities))
861 return validationResult;
865 foreach (var kvp
in securities)
869 var errorMessage = $
"BrokerageModel declared unable to submit order: [{string.Join(",
", orders.Select(o => o.Id))}]";
875 InvalidateOrders(orders, response.ErrorMessage);
876 _algorithm.
Error(response.ErrorMessage);
885 orderPlaced = orders.All(o => _brokerage.
PlaceOrder(o));
887 catch (Exception err)
896 var errorMessage = $
"Brokerage failed to place orders: [{string.Join(",
", orders.Select(o => o.Id))}]";
898 InvalidateOrders(orders, errorMessage);
899 _algorithm.
Error(errorMessage);
913 if (!_completeOrders.TryGetValue(request.
OrderId, out order) || !_completeOrderTickets.TryGetValue(request.
OrderId, out ticket))
915 Log.
Error(
"BrokerageTransactionHandler.HandleUpdateOrderRequest(): Unable to update order with ID " + request.
OrderId);
924 var isClosedOrderUpdate =
false;
926 if (order.Status.IsClosed())
933 isClosedOrderUpdate =
true;
937 var security = _algorithm.
Securities[order.Symbol];
946 _algorithm.
Error(response.ErrorMessage);
950 "BrokerageModel declared unable to update order"));
955 if (order.GroupOrderManager ==
null)
957 var updatedOrder = order.Clone();
958 updatedOrder.ApplyUpdateOrderRequest(request);
959 if (!HasSufficientBuyingPowerForOrders(updatedOrder, request, out var validationResult))
961 return validationResult;
966 order.ApplyUpdateOrderRequest(request);
971 ticket.SetOrder(order);
974 if (isClosedOrderUpdate)
984 catch (Exception err)
987 orderUpdated =
false;
994 var errorMessage =
"Brokerage failed to update order with id " + request.
OrderId;
995 _algorithm.
Error(errorMessage);
999 "Brokerage failed to update order"));
1013 if (!_completeOrders.TryGetValue(request.
OrderId, out order) || !_completeOrderTickets.TryGetValue(request.
OrderId, out ticket))
1015 Log.
Error(
"BrokerageTransactionHandler.HandleCancelOrderRequest(): Unable to cancel order with ID " + request.
OrderId +
".");
1026 if (order.Status.IsClosed())
1032 ticket.SetOrder(order);
1039 catch (Exception err)
1042 orderCanceled =
false;
1048 var message =
"Brokerage failed to cancel order with id " + order.Id;
1049 _algorithm.
Error(message);
1054 if (request.
Tag !=
null)
1057 order.Tag = request.
Tag;
1068 private bool HasSufficientBuyingPowerForOrders(
Order order,
OrderRequest request, out
OrderResponse response, List<Order> orders =
null, Dictionary<Order, Security> securities =
null)
1076 catch (Exception err)
1079 _algorithm.
Error($
"Order Error: id: {order.Id.ToStringInvariant()}, Error executing margin models: {err.Message}");
1088 var errorMessage = securities !=
null
1089 ? securities.GetErrorMessage(hasSufficientBuyingPowerResult)
1090 : $
"Brokerage failed to update order with id: {order.Id.ToStringInvariant()}, Symbol: {order.Symbol.Value}, Insufficient buying power to complete order, Reason: {hasSufficientBuyingPowerResult.Reason}.";
1092 _algorithm.
Error(errorMessage);
1101 InvalidateOrders(orders, errorMessage);
1110 private void HandleOrderEvents(List<OrderEvent> orderEvents)
1112 lock (_lockHandleOrderEvent)
1115 var orders =
new List<Order>(orderEvents.Count);
1117 for (var i = 0; i < orderEvents.Count; i++)
1119 var orderEvent = orderEvents[i];
1121 if (orderEvent.Status.IsClosed() && _openOrders.TryRemove(orderEvent.OrderId, out var order))
1123 _completeOrders[orderEvent.OrderId] = order;
1125 else if (!_completeOrders.TryGetValue(orderEvent.OrderId, out order))
1127 Log.
Error(
"BrokerageTransactionHandler.HandleOrderEvents(): Unable to locate open Combo Order with id " + orderEvent.OrderId);
1128 LogOrderEvent(orderEvent);
1133 if (orderEvent.Status.IsClosed() && _openOrderTickets.TryRemove(orderEvent.OrderId, out var ticket))
1135 _completeOrderTickets[orderEvent.OrderId] = ticket;
1137 else if (!_completeOrderTickets.TryGetValue(orderEvent.OrderId, out ticket))
1139 Log.
Error(
"BrokerageTransactionHandler.HandleOrderEvents(): Unable to resolve open ticket: " + orderEvent.OrderId);
1140 LogOrderEvent(orderEvent);
1143 orderEvent.Ticket = ticket;
1146 var fillsToProcess =
new List<OrderEvent>(orderEvents.Count);
1149 for (var i = 0; i < orderEvents.Count; i++)
1151 var orderEvent = orderEvents[i];
1152 var order = orders[i];
1153 var ticket = orderEvent.Ticket;
1161 order.Status = orderEvent.Status;
1164 orderEvent.Id = order.GetNewId();
1167 switch (orderEvent.Status)
1170 order.CanceledTime = orderEvent.UtcTime;
1175 order.LastFillTime = orderEvent.UtcTime;
1181 if (ticket.UpdateRequests.Count > 0)
1183 order.LastUpdateTime = orderEvent.UtcTime;
1190 orderEvent.Quantity = order.Quantity;
1199 orderEvent.
LimitPrice = legLimitOrder.LimitPrice;
1203 orderEvent.
StopPrice = marketOrder.StopPrice;
1207 orderEvent.
LimitPrice = stopLimitOrder.LimitPrice;
1208 orderEvent.StopPrice = stopLimitOrder.StopPrice;
1212 orderEvent.
StopPrice = trailingStopOrder.StopPrice;
1213 orderEvent.TrailingAmount = trailingStopOrder.TrailingAmount;
1217 orderEvent.
LimitPrice = limitIfTouchedOrder.LimitPrice;
1218 orderEvent.TriggerPrice = limitIfTouchedOrder.TriggerPrice;
1225 fillsToProcess.Add(orderEvent);
1226 Interlocked.Exchange(ref _lastFillTimeTicks,
CurrentTimeUtc.Ticks);
1228 var security = _algorithm.
Securities[orderEvent.Symbol];
1230 if (orderEvent.Symbol.SecurityType ==
SecurityType.Crypto
1233 && orderEvent.OrderFee.Value.Currency == baseCurrency)
1237 orderEvent.FillQuantity -= orderEvent.OrderFee.Value.Amount;
1238 orderEvent.OrderFee =
new ModifiedFillQuantityOrderFee(orderEvent.OrderFee.Value, quoteCurrency, security.SymbolProperties.ContractMultiplier);
1240 if (!_loggedFeeAdjustmentWarning)
1242 _loggedFeeAdjustmentWarning =
true;
1243 const string message =
"When buying currency pairs, using Cash account types, fees in base currency" +
1244 " will be deducted from the filled quantity so virtual positions reflect actual holdings.";
1245 Log.
Trace($
"BrokerageTransactionHandler.HandleOrderEvent(): {message}");
1246 _algorithm.
Debug(message);
1257 catch (Exception err)
1260 _algorithm.
Error($
"Fill error: error in TradeBuilder.ProcessFill: {err.Message}");
1264 for (var i = 0; i < orderEvents.Count; i++)
1266 var orderEvent = orderEvents[i];
1270 var security = _algorithm.
Securities[orderEvent.Symbol];
1272 var multiplier = security.SymbolProperties.ContractMultiplier;
1273 var securityConversionRate = security.QuoteCurrency.ConversionRate;
1281 securityConversionRate,
1282 feeInAccountCurrency,
1285 catch (Exception err)
1292 orderEvent.Ticket.AddOrderEvent(orderEvent);
1297 for (var i = 0; i < orderEvents.Count; i++)
1299 var orderEvent = orderEvents[i];
1303 _orderEvents.Enqueue(orderEvent);
1315 catch (Exception err)
1318 _algorithm.SetRuntimeError(err,
"Order Event Handler");
1322 LogOrderEvent(orderEvent);
1326 private void HandleOrderEvent(
OrderEvent orderEvent)
1328 HandleOrderEvents(
new List<OrderEvent> { orderEvent });
1333 if (!_completeOrders.TryGetValue(e.
OrderId, out var order))
1335 Log.
Error(
"BrokerageTransactionHandler.HandleOrderUpdated(): Unable to locate open order with id " + e.
OrderId);
1354 private void SetPriceAdjustmentMode(
Order order,
IAlgorithm algorithm)
1363 if (!_priceAdjustmentModes.TryGetValue(order.
Symbol, out var mode))
1366 .GetSubscriptionDataConfigs(order.
Symbol, includeInternalConfigs:
true);
1367 if (configs.Count == 0)
1369 throw new InvalidOperationException($
"Unable to locate subscription data config for {order.Symbol}");
1372 mode = configs[0].DataNormalizationMode;
1373 _priceAdjustmentModes[order.
Symbol] = mode;
1383 private static void LogOrderEvent(
OrderEvent e)
1387 Log.
Debug(
"BrokerageTransactionHandler.LogOrderEvent(): " + e);
1396 private void HandleAccountChanged(
AccountEvent account)
1402 Log.
Trace($
"BrokerageTransactionHandler.HandleAccountChanged(): {account.CurrencySymbol} Cash Lean: {existingCashBalance} Brokerage: {account.CashBalance}. Will update: {_brokerage.AccountInstantlyUpdated}");
1418 var originalOrder = GetOrderByIdInternal(brokerageOrderIdChangedEvent.
OrderId);
1420 if (originalOrder ==
null)
1423 Log.
Error($
"BrokerageTransactionHandler.HandlerBrokerageOrderIdChangedEvent(): Lean order id {brokerageOrderIdChangedEvent.OrderId} not found");
1428 originalOrder.BrokerId = brokerageOrderIdChangedEvent.
BrokerId;
1434 private void HandlePositionAssigned(
OrderEvent fill)
1445 if (_algorithm.
LiveMode || security.Holdings.Quantity != 0)
1448 $
"BrokerageTransactionHandler.HandleDelistingNotification(): UtcTime: {CurrentTimeUtc} clearing position for delisted holding: " +
1449 $
"Symbol: {e.Symbol.Value}, " +
1450 $
"Quantity: {security.Holdings.Quantity}");
1454 var quantity = -security.Holdings.Quantity;
1457 var tag =
"Liquidate from delisting";
1466 FillPrice = security.Price,
1468 FillQuantity = order.Quantity
1472 HandleOrderEvent(fill);
1487 lock (_lockHandleOrderEvent)
1494 if (_algorithm.
LiveMode || security.Holdings.Quantity != 0)
1497 $
"BrokerageTransactionHandler.HandleOptionNotification(): UtcTime: {CurrentTimeUtc} clearing position for expired option holding: " +
1498 $
"Symbol: {e.Symbol.Value}, " +
1499 $
"Holdings: {security.Holdings.Quantity}");
1502 var quantity = -security.Holdings.Quantity;
1507 var exerciseOrder = GenerateOptionExerciseOrder(security, quantity, e.
Tag);
1509 EmitOptionNotificationEvents(security, exerciseOrder);
1514 Log.
Error(
"BrokerageTransactionHandler.HandleOptionNotification(): " +
1515 $
"unexpected position ({e.Position} instead of zero) " +
1516 $
"for expired option contract: {e.Symbol.Value}");
1522 if (Math.Abs(e.
Position) < security.Holdings.AbsoluteQuantity)
1524 Log.
Trace(
"BrokerageTransactionHandler.HandleOptionNotification(): " +
1525 $
"Symbol {e.Symbol.Value} EventQuantity {e.Position} Holdings {security.Holdings.Quantity}");
1528 if (security.Holdings.IsLong)
1537 EmitOptionNotificationEvents(security, exerciseOrder);
1542 else if (security.Holdings.IsShort)
1558 const int orderWindowSeconds = 10;
1562 if (_brokerageIsBacktesting ||
1565 && (x.Status.IsOpen() || x.Status.IsFill() &&
1566 (Math.Abs((x.Time - nowUtc).TotalSeconds) < orderWindowSeconds
1567 || (x.LastUpdateTime.HasValue && Math.Abs((x.LastUpdateTime.Value - nowUtc).TotalSeconds) < orderWindowSeconds)
1568 || (x.LastFillTime.HasValue && Math.Abs((x.LastFillTime.Value - nowUtc).TotalSeconds) < orderWindowSeconds)))).Any())
1570 var quantity = e.
Position - security.Holdings.Quantity;
1572 var exerciseOrder = GenerateOptionExerciseOrder(security, quantity, e.
Tag);
1574 EmitOptionNotificationEvents(security, exerciseOrder);
1588 void onError(IReadOnlyCollection<SecurityType> supportedSecurityTypes) =>
1589 _algorithm.
Debug($
"Warning: New brokerage-side order could not be processed due to " +
1590 $
"it's security not being supported. Supported security types are {string.Join(",
", supportedSecurityTypes)}");
1593 _algorithm.GetOrAddUnrequestedSecurity(e.
Order.
Symbol, out _, onError))
1615 var option = (
Option)security;
1616 var orderEvents = option.OptionExerciseModel.OptionExercise(option, order);
1618 foreach (var orderEvent
in orderEvents)
1620 HandleOrderEvent(orderEvent);
1622 if (orderEvent.IsAssignment)
1624 orderEvent.Message = order.
Tag;
1625 HandlePositionAssigned(orderEvent);
1634 CurrentTimeUtc -
new DateTime(Interlocked.Read(ref _lastFillTimeTicks));
1648 if (orderLotMod != 0)
1652 if (!_firstRoundOffMessage)
1654 _algorithm.
Error(
"Warning: Due to brokerage limitations, orders will be rounded to " +
1655 $
"the nearest lot size of {security.SymbolProperties.LotSize.ToStringInvariant()}"
1657 _firstRoundOffMessage =
true;
1675 var comboIsReady = order.TryGetGroupOrders(TryGetOrder, out var orders);
1676 orders.TryGetGroupOrdersSecurities(_algorithm.
Portfolio, out var securities);
1694 RoundOrderPrice(security, limitOrder.LimitPrice,
"LimitPrice", (roundedPrice) => limitOrder.LimitPrice = roundedPrice);
1701 RoundOrderPrice(security, stopMarketOrder.StopPrice,
"StopPrice", (roundedPrice) => stopMarketOrder.StopPrice = roundedPrice);
1708 RoundOrderPrice(security, stopLimitOrder.LimitPrice,
"LimitPrice", (roundedPrice) => stopLimitOrder.LimitPrice = roundedPrice);
1709 RoundOrderPrice(security, stopLimitOrder.StopPrice,
"StopPrice", (roundedPrice) => stopLimitOrder.StopPrice = roundedPrice);
1716 RoundOrderPrice(security, trailingStopOrder.StopPrice,
"StopPrice",
1717 (roundedPrice) => trailingStopOrder.StopPrice = roundedPrice);
1719 if (!trailingStopOrder.TrailingAsPercentage)
1721 RoundOrderPrice(security, trailingStopOrder.TrailingAmount,
"TrailingAmount",
1722 (roundedAmount) => trailingStopOrder.TrailingAmount = roundedAmount);
1730 RoundOrderPrice(security, limitIfTouchedOrder.LimitPrice,
"LimitPrice",
1731 (roundedPrice) => limitIfTouchedOrder.LimitPrice = roundedPrice);
1732 RoundOrderPrice(security, limitIfTouchedOrder.TriggerPrice,
"TriggerPrice",
1733 (roundedPrice) => limitIfTouchedOrder.TriggerPrice = roundedPrice);
1740 RoundOrderPrice(security, comboLegOrder.LimitPrice,
"LimitPrice",
1741 (roundedPrice) => comboLegOrder.LimitPrice = roundedPrice);
1754 foreach (var (legOrder, legSecurity) in orders)
1756 var legIncrement = legSecurity.PriceVariationModel.GetMinimumPriceVariation(
1758 if (legIncrement > 0 && (increment == 0 || legIncrement < increment))
1760 increment = legIncrement;
1764 RoundOrderPrice(groupOrderManager.LimitPrice, increment,
"LimitPrice",
1765 (roundedPrice) => groupOrderManager.LimitPrice = roundedPrice);
1773 private void RoundOrderPrice(
Security security, decimal price,
string priceType, Action<decimal> setPrice)
1776 RoundOrderPrice(price, increment, priceType, setPrice);
1779 [MethodImpl(MethodImplOptions.AggressiveInlining)]
1780 private void RoundOrderPrice(decimal price, decimal increment,
string priceType, Action<decimal> setPrice)
1784 var roundedPrice = Math.Round(price / increment) * increment;
1785 setPrice(roundedPrice);
1786 SendWarningOnPriceChange(priceType, roundedPrice, price);
1790 private Order TryGetOrder(
int orderId)
1792 _completeOrders.TryGetValue(orderId, out var order);
1796 private void InvalidateOrders(List<Order> orders,
string message)
1798 for (var i = 0; i < orders.Count; i++)
1800 var orderInGroup = orders[i];
1801 if (!orderInGroup.Status.IsClosed())
1809 private void SendWarningOnPriceChange(
string priceType, decimal priceRound, decimal priceOriginal)
1811 if (!priceOriginal.Equals(priceRound))
1814 $
"Warning: To meet brokerage precision requirements, order {priceType.ToStringInvariant()} was rounded to {priceRound.ToStringInvariant()} from {priceOriginal.ToStringInvariant()}"
1819 private string GetShortableErrorMessage(Symbol symbol, decimal quantity)
1822 return $
"Order exceeds shortable quantity {shortableQuantity} for Symbol {symbol} requested {quantity})";