17 using System.Collections.Concurrent;
18 using System.Collections.Generic;
20 using System.Runtime.CompilerServices;
21 using System.Threading;
42 private bool _brokerageIsBacktesting;
43 private bool _loggedFeeAdjustmentWarning;
46 private int _totalOrderCount;
49 private bool _firstRoundOffMessage =
false;
52 private long _lastFillTimeTicks;
54 private const int MaxCashSyncAttempts = 5;
55 private int _failedCashSyncAttempts;
63 private Thread _processingThread;
64 private readonly CancellationTokenSource _cancellationTokenSource =
new CancellationTokenSource();
66 private readonly ConcurrentQueue<OrderEvent> _orderEvents =
new ConcurrentQueue<OrderEvent>();
72 private readonly ConcurrentDictionary<int, Order> _completeOrders =
new ConcurrentDictionary<int, Order>();
78 private readonly ConcurrentDictionary<int, Order> _openOrders =
new ConcurrentDictionary<int, Order>();
85 private readonly ConcurrentDictionary<int, OrderTicket> _openOrderTickets =
new ConcurrentDictionary<int, OrderTicket>();
92 private readonly ConcurrentDictionary<int, OrderTicket> _completeOrderTickets =
new ConcurrentDictionary<int, OrderTicket>();
97 private readonly Dictionary<Symbol, DataNormalizationMode> _priceAdjustmentModes =
new Dictionary<Symbol, DataNormalizationMode>();
106 private readonly
object _lockHandleOrderEvent =
new object();
116 public ConcurrentDictionary<int, Order>
Orders
120 return _completeOrders;
132 public ConcurrentDictionary<int, OrderTicket>
OrderTickets
136 return _completeOrderTickets;
153 if (brokerage ==
null)
155 throw new ArgumentNullException(nameof(brokerage));
160 _resultHandler = resultHandler;
162 _brokerage = brokerage;
167 HandleOrderEvents(orderEvents);
172 HandleAccountChanged(account);
177 HandlePositionAssigned(fill);
182 HandleOptionNotification(e);
187 HandleNewBrokerageSideOrder(e);
192 HandleDelistingNotification(e);
197 HandlerBrokerageOrderIdChangedEvent(e);
202 HandleOrderUpdated(e);
207 _algorithm = algorithm;
217 _processingThread =
new Thread(
Run) { IsBackground =
true, Name =
"Transaction Thread" };
218 _processingThread.Start();
227 #region Order Request Processing
237 Log.
Trace(
"BrokerageTransactionHandler.Process(): " + request);
254 throw new ArgumentOutOfRangeException();
269 var shortable =
true;
277 var message = GetShortableErrorMessage(request.
Symbol, request.
Quantity);
281 _algorithm.
Debug($
"Warning: {message}");
292 Interlocked.Increment(ref _totalOrderCount);
294 if (response.IsSuccess)
296 _openOrderTickets.TryAdd(ticket.OrderId, ticket);
297 _completeOrderTickets.TryAdd(ticket.OrderId, ticket);
310 ?
"Algorithm warming up."
311 : response.ErrorMessage;
314 var security = _algorithm.
Securities[order.Symbol];
315 order.PriceCurrency = security.SymbolProperties.QuoteCurrency;
318 order.Tag = orderTag;
319 ticket.SetOrder(order);
320 _completeOrderTickets.TryAdd(ticket.OrderId, ticket);
321 _completeOrders.TryAdd(order.Id, order);
338 if (!ticket.
OrderSet.WaitOne(orderSetTimeout))
340 Log.
Error(
"BrokerageTransactionHandler.WaitForOrderSubmission(): " +
341 $
"The order request (Id={ticket.OrderId}) was not submitted within {orderSetTimeout.TotalSeconds} second(s).");
353 if (!_completeOrderTickets.TryGetValue(request.
OrderId, out ticket))
358 ticket.AddUpdateRequest(request);
363 var order = GetOrderByIdInternal(request.
OrderId);
364 var orderQuantity = request.
Quantity ?? ticket.Quantity;
366 var shortable =
true;
369 shortable = _algorithm.
Shortable(ticket.Symbol, orderQuantity, order.Id);
371 if (_algorithm.
LiveMode && !shortable)
376 _algorithm.
Debug($
"Warning: {GetShortableErrorMessage(ticket.Symbol, ticket.Quantity)}");
383 Log.
Error(
"BrokerageTransactionHandler.Update(): Cannot update a null order");
389 Log.
Error(
"BrokerageTransactionHandler.Update(): Cannot update a pending submit order with status " + order.Status);
395 Log.
Error(
"BrokerageTransactionHandler.Update(): Cannot update closed order with status " + order.Status);
409 GetShortableErrorMessage(ticket.Symbol, ticket.Quantity));
419 catch (Exception err)
435 if (!_completeOrderTickets.TryGetValue(request.
OrderId, out ticket))
437 Log.
Error(
"BrokerageTransactionHandler.CancelOrder(): Unable to locate ticket for order.");
445 if (!ticket.TrySetCancelRequest(request))
453 var order = GetOrderByIdInternal(request.
OrderId);
454 if (order !=
null && request.
Tag !=
null)
456 order.Tag = request.
Tag;
460 Log.
Error(
"BrokerageTransactionHandler.CancelOrder(): Cannot find this id.");
465 Log.
Error(
"BrokerageTransactionHandler.CancelOrder(): Cannot cancel order with status: " + order.Status);
468 else if (order.Status.IsClosed())
470 Log.
Error(
"BrokerageTransactionHandler.CancelOrder(): Cannot cancel order already " + order.Status);
493 catch (Exception err)
507 public IEnumerable<OrderTicket>
GetOrderTickets(Func<OrderTicket, bool> filter =
null)
509 return _completeOrderTickets.Select(x => x.Value).Where(filter ?? (x =>
true));
519 return _openOrderTickets.Select(x => x.Value).Where(filter ?? (x =>
true));
530 _completeOrderTickets.TryGetValue(orderId, out ticket);
543 Order order = GetOrderByIdInternal(orderId);
544 return order?.
Clone();
547 private Order GetOrderByIdInternal(
int orderId)
550 return _completeOrders.TryGetValue(orderId, out order) ? order :
null;
562 if (openOrders.Count > 0)
570 private static List<Order>
GetOrdersByBrokerageId(
string brokerageId, ConcurrentDictionary<int, Order> orders)
573 .Where(x => x.Value.BrokerId.Contains(brokerageId))
574 .Select(kvp => kvp.Value.Clone())
584 public IEnumerable<Order>
GetOrders(Func<Order, bool> filter =
null)
589 return _completeOrders.Select(x => x.Value).Where(filter).Select(x => x.Clone());
591 return _completeOrders.Select(x => x.Value).Select(x => x.Clone());
604 return _openOrders.Select(x => x.Value).Where(filter).Select(x => x.Clone()).ToList();
606 return _openOrders.Select(x => x.Value).Select(x => x.Clone()).ToList();
622 catch (Exception err)
625 _algorithm.SetRuntimeError(err,
"HandleOrderRequest");
628 if (_processingThread !=
null)
630 Log.
Trace(
"BrokerageTransactionHandler.Run(): Ending Thread...");
655 Log.
Error(
"BrokerageTransactionHandler.ProcessSynchronousEvents(): Timed out waiting for request queue to finish processing.");
668 if (++_failedCashSyncAttempts >= MaxCashSyncAttempts)
670 throw new Exception(
"The maximum number of attempts for brokerage cash sync has been reached.");
677 const int maxOrdersToKeep = 10000;
678 if (_completeOrders.Count < maxOrdersToKeep + 1)
683 Log.
Debug(
"BrokerageTransactionHandler.ProcessSynchronousEvents(): Start removing old orders...");
684 var max = _completeOrders.Max(x => x.Key);
685 var lowestOrderIdToKeep = max - maxOrdersToKeep;
686 foreach (var item
in _completeOrders.Where(x => x.Key <= lowestOrderIdToKeep))
690 _completeOrders.TryRemove(item.Key, out value);
691 _completeOrderTickets.TryRemove(item.Key, out ticket);
694 Log.
Debug($
"BrokerageTransactionHandler.ProcessSynchronousEvents(): New order count {_completeOrders.Count}. Exit");
710 var orderTicket = order.ToOrderTicket(algorithm.
Transactions);
712 SetPriceAdjustmentMode(order, algorithm);
714 _openOrders.AddOrUpdate(order.
Id, order, (i, o) => order);
715 _completeOrders.AddOrUpdate(order.
Id, order, (i, o) => order);
716 _openOrderTickets.AddOrUpdate(order.
Id, orderTicket);
717 _completeOrderTickets.AddOrUpdate(order.
Id, orderTicket);
719 Interlocked.Increment(ref _totalOrderCount);
728 var timeout = TimeSpan.FromSeconds(60);
729 if (_processingThread !=
null)
734 Log.
Error(
"BrokerageTransactionHandler.Exit(): Exceed timeout: " + (
int)(timeout.TotalSeconds) +
" seconds.");
738 _processingThread?.StopSafely(timeout, _cancellationTokenSource);
740 _cancellationTokenSource.DisposeSafely();
763 throw new ArgumentOutOfRangeException();
779 var security = _algorithm.
Securities[order.Symbol];
780 order.PriceCurrency = security.SymbolProperties.QuoteCurrency;
781 if (
string.IsNullOrEmpty(order.Tag))
783 order.Tag = order.GetDefaultTag();
789 if (!_openOrders.TryAdd(order.Id, order) || !_completeOrders.TryAdd(order.Id, order))
791 Log.
Error(
"BrokerageTransactionHandler.HandleSubmitOrderRequest(): Unable to add new order, order not processed.");
794 if (!_completeOrderTickets.TryGetValue(order.Id, out ticket))
796 Log.
Error(
"BrokerageTransactionHandler.HandleSubmitOrderRequest(): Unable to retrieve order ticket, order not processed.");
800 var comboIsReady = order.TryGetGroupOrders(TryGetOrder, out var orders);
801 var comboSecuritiesFound = orders.TryGetGroupOrdersSecurities(_algorithm.
Portfolio, out var securities);
807 order.OrderSubmissionData =
new OrderSubmissionData(security.BidPrice, security.AskPrice, security.Close);
810 SetPriceAdjustmentMode(order, _algorithm);
813 ticket.SetOrder(order);
821 if (orders.Any(o => o.Quantity == 0))
824 _algorithm.
Error(response.ErrorMessage);
826 InvalidateOrders(orders, response.ErrorMessage);
830 if (!comboSecuritiesFound)
833 _algorithm.
Error(response.ErrorMessage);
835 InvalidateOrders(orders, response.ErrorMessage);
845 catch (Exception err)
848 _algorithm.
Error($
"Order Error: id: {order.Id.ToStringInvariant()}, Error executing margin models: {err.Message}");
852 "Error executing margin models"));
858 var errorMessage = securities.GetErrorMessage(hasSufficientBuyingPowerResult);
859 _algorithm.
Error(errorMessage);
861 InvalidateOrders(orders, errorMessage);
866 foreach (var kvp
in securities)
870 var errorMessage = $
"BrokerageModel declared unable to submit order: [{string.Join(",
", orders.Select(o => o.Id))}]";
876 InvalidateOrders(orders, response.ErrorMessage);
877 _algorithm.
Error(response.ErrorMessage);
886 orderPlaced = orders.All(o => _brokerage.
PlaceOrder(o));
888 catch (Exception err)
897 var errorMessage = $
"Brokerage failed to place orders: [{string.Join(",
", orders.Select(o => o.Id))}]";
899 InvalidateOrders(orders, errorMessage);
900 _algorithm.
Error(errorMessage);
914 if (!_completeOrders.TryGetValue(request.
OrderId, out order) || !_completeOrderTickets.TryGetValue(request.
OrderId, out ticket))
916 Log.
Error(
"BrokerageTransactionHandler.HandleUpdateOrderRequest(): Unable to update order with ID " + request.
OrderId);
925 var isClosedOrderUpdate =
false;
927 if (order.Status.IsClosed())
934 isClosedOrderUpdate =
true;
938 var security = _algorithm.
Securities[order.Symbol];
947 _algorithm.
Error(response.ErrorMessage);
951 "BrokerageModel declared unable to update order"));
956 order.ApplyUpdateOrderRequest(request);
961 ticket.SetOrder(order);
964 if (isClosedOrderUpdate)
974 catch (Exception err)
977 orderUpdated =
false;
984 var errorMessage =
"Brokerage failed to update order with id " + request.
OrderId;
985 _algorithm.
Error(errorMessage);
989 "Brokerage failed to update order"));
1003 if (!_completeOrders.TryGetValue(request.
OrderId, out order) || !_completeOrderTickets.TryGetValue(request.
OrderId, out ticket))
1005 Log.
Error(
"BrokerageTransactionHandler.HandleCancelOrderRequest(): Unable to cancel order with ID " + request.
OrderId +
".");
1016 if (order.Status.IsClosed())
1022 ticket.SetOrder(order);
1029 catch (Exception err)
1032 orderCanceled =
false;
1038 var message =
"Brokerage failed to cancel order with id " + order.Id;
1039 _algorithm.
Error(message);
1044 if (request.
Tag !=
null)
1047 order.Tag = request.
Tag;
1053 private void HandleOrderEvents(List<OrderEvent> orderEvents)
1055 lock (_lockHandleOrderEvent)
1058 var orders =
new List<Order>(orderEvents.Count);
1060 for (var i = 0; i < orderEvents.Count; i++)
1062 var orderEvent = orderEvents[i];
1064 if (orderEvent.Status.IsClosed() && _openOrders.TryRemove(orderEvent.OrderId, out var order))
1066 _completeOrders[orderEvent.OrderId] = order;
1068 else if (!_completeOrders.TryGetValue(orderEvent.OrderId, out order))
1070 Log.
Error(
"BrokerageTransactionHandler.HandleOrderEvents(): Unable to locate open Combo Order with id " + orderEvent.OrderId);
1071 LogOrderEvent(orderEvent);
1076 if (orderEvent.Status.IsClosed() && _openOrderTickets.TryRemove(orderEvent.OrderId, out var ticket))
1078 _completeOrderTickets[orderEvent.OrderId] = ticket;
1080 else if (!_completeOrderTickets.TryGetValue(orderEvent.OrderId, out ticket))
1082 Log.
Error(
"BrokerageTransactionHandler.HandleOrderEvents(): Unable to resolve open ticket: " + orderEvent.OrderId);
1083 LogOrderEvent(orderEvent);
1086 orderEvent.Ticket = ticket;
1089 var fillsToProcess =
new List<OrderEvent>(orderEvents.Count);
1092 for (var i = 0; i < orderEvents.Count; i++)
1094 var orderEvent = orderEvents[i];
1095 var order = orders[i];
1096 var ticket = orderEvent.Ticket;
1104 order.Status = orderEvent.Status;
1107 orderEvent.Id = order.GetNewId();
1110 switch (orderEvent.Status)
1113 order.CanceledTime = orderEvent.UtcTime;
1118 order.LastFillTime = orderEvent.UtcTime;
1121 if (orderEvent.Status ==
OrderStatus.Filled && !
string.IsNullOrWhiteSpace(orderEvent.Message))
1123 if (
string.IsNullOrWhiteSpace(order.Tag))
1125 order.Tag = orderEvent.Message;
1129 order.Tag +=
" - " + orderEvent.Message;
1137 if (ticket.UpdateRequests.Count > 0)
1139 order.LastUpdateTime = orderEvent.UtcTime;
1146 orderEvent.Quantity = order.Quantity;
1155 orderEvent.
LimitPrice = legLimitOrder.LimitPrice;
1159 orderEvent.
StopPrice = marketOrder.StopPrice;
1163 orderEvent.
LimitPrice = stopLimitOrder.LimitPrice;
1164 orderEvent.StopPrice = stopLimitOrder.StopPrice;
1168 orderEvent.
StopPrice = trailingStopOrder.StopPrice;
1169 orderEvent.TrailingAmount = trailingStopOrder.TrailingAmount;
1173 orderEvent.
LimitPrice = limitIfTouchedOrder.LimitPrice;
1174 orderEvent.TriggerPrice = limitIfTouchedOrder.TriggerPrice;
1181 fillsToProcess.Add(orderEvent);
1182 Interlocked.Exchange(ref _lastFillTimeTicks,
CurrentTimeUtc.Ticks);
1184 var security = _algorithm.
Securities[orderEvent.Symbol];
1186 if (orderEvent.Symbol.SecurityType ==
SecurityType.Crypto
1189 && orderEvent.OrderFee.Value.Currency == baseCurrency)
1193 orderEvent.FillQuantity -= orderEvent.OrderFee.Value.Amount;
1194 orderEvent.OrderFee =
new ModifiedFillQuantityOrderFee(orderEvent.OrderFee.Value, quoteCurrency, security.SymbolProperties.ContractMultiplier);
1196 if (!_loggedFeeAdjustmentWarning)
1198 _loggedFeeAdjustmentWarning =
true;
1199 const string message =
"When buying currency pairs, using Cash account types, fees in base currency" +
1200 " will be deducted from the filled quantity so virtual positions reflect actual holdings.";
1201 Log.
Trace($
"BrokerageTransactionHandler.HandleOrderEvent(): {message}");
1202 _algorithm.
Debug(message);
1213 catch (Exception err)
1216 _algorithm.
Error($
"Fill error: error in TradeBuilder.ProcessFill: {err.Message}");
1220 for (var i = 0; i < orderEvents.Count; i++)
1222 var orderEvent = orderEvents[i];
1226 var security = _algorithm.
Securities[orderEvent.Symbol];
1228 var multiplier = security.SymbolProperties.ContractMultiplier;
1229 var securityConversionRate = security.QuoteCurrency.ConversionRate;
1237 securityConversionRate,
1238 feeInAccountCurrency,
1241 catch (Exception err)
1248 orderEvent.Ticket.AddOrderEvent(orderEvent);
1253 for (var i = 0; i < orderEvents.Count; i++)
1255 var orderEvent = orderEvents[i];
1259 _orderEvents.Enqueue(orderEvent);
1271 catch (Exception err)
1274 _algorithm.SetRuntimeError(err,
"Order Event Handler");
1278 LogOrderEvent(orderEvent);
1282 private void HandleOrderEvent(
OrderEvent orderEvent)
1284 HandleOrderEvents(
new List<OrderEvent> { orderEvent });
1289 if (!_completeOrders.TryGetValue(e.
OrderId, out var order))
1291 Log.
Error(
"BrokerageTransactionHandler.HandleOrderUpdated(): Unable to locate open order with id " + e.
OrderId);
1310 private void SetPriceAdjustmentMode(
Order order,
IAlgorithm algorithm)
1319 if (!_priceAdjustmentModes.TryGetValue(order.
Symbol, out var mode))
1322 .GetSubscriptionDataConfigs(order.
Symbol, includeInternalConfigs:
true);
1323 if (configs.Count == 0)
1325 throw new InvalidOperationException($
"Unable to locate subscription data config for {order.Symbol}");
1328 mode = configs[0].DataNormalizationMode;
1329 _priceAdjustmentModes[order.
Symbol] = mode;
1339 private static void LogOrderEvent(
OrderEvent e)
1343 Log.
Debug(
"BrokerageTransactionHandler.LogOrderEvent(): " + e);
1352 private void HandleAccountChanged(
AccountEvent account)
1358 Log.
Trace($
"BrokerageTransactionHandler.HandleAccountChanged(): {account.CurrencySymbol} Cash Lean: {existingCashBalance} Brokerage: {account.CashBalance}. Will update: {_brokerage.AccountInstantlyUpdated}");
1374 var originalOrder = GetOrderByIdInternal(brokerageOrderIdChangedEvent.
OrderId);
1376 if (originalOrder ==
null)
1379 Log.
Error($
"BrokerageTransactionHandler.HandlerBrokerageOrderIdChangedEvent(): Lean order id {brokerageOrderIdChangedEvent.OrderId} not found");
1384 originalOrder.BrokerId = brokerageOrderIdChangedEvent.
BrokerId;
1390 private void HandlePositionAssigned(
OrderEvent fill)
1401 if (_algorithm.
LiveMode || security.Holdings.Quantity != 0)
1404 $
"BrokerageTransactionHandler.HandleDelistingNotification(): UtcTime: {CurrentTimeUtc} clearing position for delisted holding: " +
1405 $
"Symbol: {e.Symbol.Value}, " +
1406 $
"Quantity: {security.Holdings.Quantity}");
1410 var quantity = -security.Holdings.Quantity;
1413 var tag =
"Liquidate from delisting";
1422 FillPrice = security.Price,
1424 FillQuantity = order.Quantity
1428 HandleOrderEvent(fill);
1443 lock (_lockHandleOrderEvent)
1450 if (_algorithm.
LiveMode || security.Holdings.Quantity != 0)
1453 $
"BrokerageTransactionHandler.HandleOptionNotification(): UtcTime: {CurrentTimeUtc} clearing position for expired option holding: " +
1454 $
"Symbol: {e.Symbol.Value}, " +
1455 $
"Holdings: {security.Holdings.Quantity}");
1458 var quantity = -security.Holdings.Quantity;
1463 var exerciseOrder = GenerateOptionExerciseOrder(security, quantity, e.
Tag);
1465 EmitOptionNotificationEvents(security, exerciseOrder);
1470 Log.
Error(
"BrokerageTransactionHandler.HandleOptionNotification(): " +
1471 $
"unexpected position ({e.Position} instead of zero) " +
1472 $
"for expired option contract: {e.Symbol.Value}");
1478 if (Math.Abs(e.
Position) < security.Holdings.AbsoluteQuantity)
1480 Log.
Trace(
"BrokerageTransactionHandler.HandleOptionNotification(): " +
1481 $
"Symbol {e.Symbol.Value} EventQuantity {e.Position} Holdings {security.Holdings.Quantity}");
1484 if (security.Holdings.IsLong)
1493 EmitOptionNotificationEvents(security, exerciseOrder);
1498 else if (security.Holdings.IsShort)
1514 const int orderWindowSeconds = 10;
1518 if (_brokerageIsBacktesting ||
1521 && (x.Status.IsOpen() || x.Status.IsFill() &&
1522 (Math.Abs((x.Time - nowUtc).TotalSeconds) < orderWindowSeconds
1523 || (x.LastUpdateTime.HasValue && Math.Abs((x.LastUpdateTime.Value - nowUtc).TotalSeconds) < orderWindowSeconds)
1524 || (x.LastFillTime.HasValue && Math.Abs((x.LastFillTime.Value - nowUtc).TotalSeconds) < orderWindowSeconds)))).Any())
1526 var quantity = e.
Position - security.Holdings.Quantity;
1528 var exerciseOrder = GenerateOptionExerciseOrder(security, quantity, e.
Tag);
1530 EmitOptionNotificationEvents(security, exerciseOrder);
1544 void onError(IReadOnlyCollection<SecurityType> supportedSecurityTypes) =>
1545 _algorithm.
Debug($
"Warning: New brokerage-side order could not be processed due to " +
1546 $
"it's security not being supported. Supported security types are {string.Join(",
", supportedSecurityTypes)}");
1549 _algorithm.GetOrAddUnrequestedSecurity(e.
Order.
Symbol, out _, onError))
1571 var option = (
Option)security;
1572 var orderEvents = option.OptionExerciseModel.OptionExercise(option, order);
1574 foreach (var orderEvent
in orderEvents)
1576 HandleOrderEvent(orderEvent);
1578 if (orderEvent.IsAssignment)
1580 orderEvent.Message = order.
Tag;
1581 HandlePositionAssigned(orderEvent);
1590 CurrentTimeUtc -
new DateTime(Interlocked.Read(ref _lastFillTimeTicks));
1604 if (orderLotMod != 0)
1608 if (!_firstRoundOffMessage)
1610 _algorithm.
Error(
"Warning: Due to brokerage limitations, orders will be rounded to " +
1611 $
"the nearest lot size of {security.SymbolProperties.LotSize.ToStringInvariant()}"
1613 _firstRoundOffMessage =
true;
1631 var comboIsReady = order.TryGetGroupOrders(TryGetOrder, out var orders);
1632 orders.TryGetGroupOrdersSecurities(_algorithm.
Portfolio, out var securities);
1650 RoundOrderPrice(security, limitOrder.LimitPrice,
"LimitPrice", (roundedPrice) => limitOrder.LimitPrice = roundedPrice);
1657 RoundOrderPrice(security, stopMarketOrder.StopPrice,
"StopPrice", (roundedPrice) => stopMarketOrder.StopPrice = roundedPrice);
1664 RoundOrderPrice(security, stopLimitOrder.LimitPrice,
"LimitPrice", (roundedPrice) => stopLimitOrder.LimitPrice = roundedPrice);
1665 RoundOrderPrice(security, stopLimitOrder.StopPrice,
"StopPrice", (roundedPrice) => stopLimitOrder.StopPrice = roundedPrice);
1672 RoundOrderPrice(security, trailingStopOrder.StopPrice,
"StopPrice",
1673 (roundedPrice) => trailingStopOrder.StopPrice = roundedPrice);
1675 if (!trailingStopOrder.TrailingAsPercentage)
1677 RoundOrderPrice(security, trailingStopOrder.TrailingAmount,
"TrailingAmount",
1678 (roundedAmount) => trailingStopOrder.TrailingAmount = roundedAmount);
1686 RoundOrderPrice(security, limitIfTouchedOrder.LimitPrice,
"LimitPrice",
1687 (roundedPrice) => limitIfTouchedOrder.LimitPrice = roundedPrice);
1688 RoundOrderPrice(security, limitIfTouchedOrder.TriggerPrice,
"TriggerPrice",
1689 (roundedPrice) => limitIfTouchedOrder.TriggerPrice = roundedPrice);
1696 RoundOrderPrice(security, comboLegOrder.LimitPrice,
"LimitPrice",
1697 (roundedPrice) => comboLegOrder.LimitPrice = roundedPrice);
1710 foreach (var (legOrder, legSecurity) in orders)
1712 var legIncrement = legSecurity.PriceVariationModel.GetMinimumPriceVariation(
1714 if (legIncrement > 0 && (increment == 0 || legIncrement < increment))
1716 increment = legIncrement;
1720 RoundOrderPrice(groupOrderManager.LimitPrice, increment,
"LimitPrice",
1721 (roundedPrice) => groupOrderManager.LimitPrice = roundedPrice);
1729 private void RoundOrderPrice(
Security security, decimal price,
string priceType, Action<decimal> setPrice)
1732 RoundOrderPrice(price, increment, priceType, setPrice);
1735 [MethodImpl(MethodImplOptions.AggressiveInlining)]
1736 private void RoundOrderPrice(decimal price, decimal increment,
string priceType, Action<decimal> setPrice)
1740 var roundedPrice = Math.Round(price / increment) * increment;
1741 setPrice(roundedPrice);
1742 SendWarningOnPriceChange(priceType, roundedPrice, price);
1746 private Order TryGetOrder(
int orderId)
1748 _completeOrders.TryGetValue(orderId, out var order);
1752 private void InvalidateOrders(List<Order> orders,
string message)
1754 for (var i = 0; i < orders.Count; i++)
1756 var orderInGroup = orders[i];
1757 if (!orderInGroup.Status.IsClosed())
1765 private void SendWarningOnPriceChange(
string priceType, decimal priceRound, decimal priceOriginal)
1767 if (!priceOriginal.Equals(priceRound))
1770 $
"Warning: To meet brokerage precision requirements, order {priceType.ToStringInvariant()} was rounded to {priceRound.ToStringInvariant()} from {priceOriginal.ToStringInvariant()}"
1775 private string GetShortableErrorMessage(Symbol symbol, decimal quantity)
1778 return $
"Order exceeds shortable quantity {shortableQuantity} for Symbol {symbol} requested {quantity})";