diff --git a/Algorithm.CSharp/OrderSurfaceShortcutsRegressionAlgorithm.cs b/Algorithm.CSharp/OrderSurfaceShortcutsRegressionAlgorithm.cs
new file mode 100644
index 000000000000..d9eb7c9eeea7
--- /dev/null
+++ b/Algorithm.CSharp/OrderSurfaceShortcutsRegressionAlgorithm.cs
@@ -0,0 +1,249 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using QuantConnect.Data;
+using QuantConnect.Interfaces;
+using QuantConnect.Orders;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Regression algorithm asserting the flat order surface shortcuts: the numeric OrderFee
+ /// surface (OrderFee.Amount, arithmetic and comparison operators, OrderEvent.OrderFeeAmount),
+ /// the flat combo group ids (Order.GroupOrderManagerId, OrderEvent.GroupId, ComboOrderTicket),
+ /// the tag-argument tolerance of MarketOrder/Liquidate and OrderTargetNotional
+ ///
+ public class OrderSurfaceShortcutsRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ private Symbol _equitySymbol;
+ private Symbol _optionSymbol;
+ private OrderTicket _taggedTicket;
+ private OrderTicket _notionalTicket;
+ private ComboOrderTicket _comboTicket;
+ private readonly HashSet _comboFillGroupIds = new();
+ private int _comboFillEventsCount;
+
+ public override void Initialize()
+ {
+ SetStartDate(2015, 12, 24);
+ SetEndDate(2015, 12, 24);
+ SetCash(200000);
+
+ var equity = AddEquity("GOOG", leverage: 4, fillForward: true);
+ _equitySymbol = equity.Symbol;
+ var option = AddOption(equity.Symbol, fillForward: true);
+ _optionSymbol = option.Symbol;
+
+ option.SetFilter(u => u.StandardsOnly().Strikes(-2, +2).Expiration(0, 180));
+ }
+
+ public override void OnData(Slice slice)
+ {
+ if (_taggedTicket == null && IsMarketOpen(_equitySymbol))
+ {
+ // tag in the tag argument slot; the Python version passes it in the third positional slot
+ _taggedTicket = MarketOrder(_equitySymbol, 1, tag: "tagged entry");
+
+ // target an absolute notional value instead of a portfolio percentage
+ _notionalTicket = OrderTargetNotional(_equitySymbol, 10000);
+ if (_notionalTicket == null)
+ {
+ throw new RegressionTestException("OrderTargetNotional was expected to place an order");
+ }
+
+ // a tag slipped into the symbol slot must fail pointing to the tag parameter
+ var liquidateFailed = false;
+ try
+ {
+ Liquidate("EOD close");
+ }
+ catch (ArgumentException exception)
+ {
+ liquidateFailed = true;
+ if (!exception.Message.Contains("tag"))
+ {
+ throw new RegressionTestException(
+ $"Liquidate() with an unknown ticker was expected to point to the tag parameter but the error was: {exception.Message}");
+ }
+ }
+ if (!liquidateFailed)
+ {
+ throw new RegressionTestException("Liquidate() with an unknown ticker was expected to fail");
+ }
+ }
+
+ if (_comboTicket == null && IsMarketOpen(_optionSymbol) && slice.OptionChains.TryGetValue(_optionSymbol, out var chain))
+ {
+ var callContracts = chain.Where(contract => contract.Right == OptionRight.Call)
+ .GroupBy(x => x.Expiry)
+ .OrderBy(grouping => grouping.Key)
+ .First()
+ .OrderBy(x => x.Strike)
+ .ToList();
+ if (callContracts.Count < 3)
+ {
+ return;
+ }
+
+ var legs = new List()
+ {
+ Leg.Create(callContracts[0].Symbol, 1),
+ Leg.Create(callContracts[1].Symbol, -2),
+ Leg.Create(callContracts[2].Symbol, 1)
+ };
+ _comboTicket = ComboMarketOrder(legs, 10);
+
+ if (_comboTicket.Count != legs.Count || _comboTicket.Tickets.Count != legs.Count)
+ {
+ throw new RegressionTestException($"Expected {legs.Count} leg tickets, found {_comboTicket.Count}");
+ }
+ if (_comboTicket.GroupOrderManagerId == null)
+ {
+ throw new RegressionTestException("The combo order ticket was expected to have a group order manager id");
+ }
+ }
+ }
+
+ public override void OnOrderEvent(OrderEvent orderEvent)
+ {
+ if (orderEvent.Status != OrderStatus.Filled)
+ {
+ return;
+ }
+
+ var order = Transactions.GetOrderById(orderEvent.OrderId);
+
+ // the fee amount shortcuts and operators must match the two-level Value.Amount
+ var feeAmount = orderEvent.OrderFee.Value.Amount;
+ if (orderEvent.OrderFeeAmount != feeAmount || orderEvent.OrderFee.Amount != feeAmount)
+ {
+ throw new RegressionTestException($"Order fee amount shortcuts do not match the fee amount {feeAmount}");
+ }
+ if (orderEvent.OrderFee + orderEvent.OrderFee != 2 * feeAmount || (feeAmount != 0 && !(orderEvent.OrderFee > 0)))
+ {
+ throw new RegressionTestException($"Order fee operators do not match the fee amount {feeAmount}");
+ }
+
+ if (order.Type == OrderType.ComboMarket)
+ {
+ // Note: these fill events are received while the synchronous ComboMarketOrder() call is still
+ // in flight, so the combo ticket is checked against them in OnEndOfAlgorithm
+ _comboFillEventsCount++;
+ if (order.GroupOrderManagerId == null)
+ {
+ throw new RegressionTestException("Combo orders were expected to have a group order manager id");
+ }
+ if (orderEvent.GroupId != order.GroupOrderManagerId)
+ {
+ throw new RegressionTestException($"Expected order event group id {order.GroupOrderManagerId}, found {orderEvent.GroupId}");
+ }
+ _comboFillGroupIds.Add(order.GroupOrderManagerId);
+ }
+ else if (order.GroupOrderManagerId != null || orderEvent.GroupId != null)
+ {
+ throw new RegressionTestException("Non-combo orders were expected to have null group ids");
+ }
+ }
+
+ public override void OnEndOfAlgorithm()
+ {
+ if (_taggedTicket == null || _taggedTicket.Tag != "tagged entry")
+ {
+ throw new RegressionTestException("The market order tag was not set from the tag argument");
+ }
+ if (_notionalTicket.Status != OrderStatus.Filled)
+ {
+ throw new RegressionTestException("The notional target order was expected to be filled");
+ }
+ if (_comboTicket == null || _comboFillEventsCount != _comboTicket.Count)
+ {
+ throw new RegressionTestException("The combo order was expected to be placed and filled");
+ }
+ if (!_comboTicket.Filled)
+ {
+ throw new RegressionTestException("The combo order ticket was expected to aggregate the leg fills");
+ }
+ if (_comboFillGroupIds.Single() != _comboTicket.GroupOrderManagerId)
+ {
+ throw new RegressionTestException($"Expected all combo fills to have group id {_comboTicket.GroupOrderManagerId}, " +
+ $"found {string.Join(", ", _comboFillGroupIds)}");
+ }
+ }
+
+ ///
+ /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
+ ///
+ public bool CanRunLocally => true;
+
+ ///
+ /// This is used by the regression test system to indicate which languages this algorithm is written in.
+ ///
+ public List Languages { get; } = new() { Language.CSharp, Language.Python };
+
+ ///
+ /// Data Points count of all timeslices of algorithm
+ ///
+ public long DataPoints => 15023;
+
+ ///
+ /// Data Points count of the algorithm history
+ ///
+ public int AlgorithmHistoryDataPoints => 0;
+
+ ///
+ /// Final status of the algorithm
+ ///
+ public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
+
+ ///
+ /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
+ ///
+ public Dictionary ExpectedStatistics => new Dictionary
+ {
+ {"Total Orders", "5"},
+ {"Average Win", "0%"},
+ {"Average Loss", "0%"},
+ {"Compounding Annual Return", "0%"},
+ {"Drawdown", "0%"},
+ {"Expectancy", "0"},
+ {"Start Equity", "200000"},
+ {"End Equity", "198005.36"},
+ {"Net Profit", "0%"},
+ {"Sharpe Ratio", "0"},
+ {"Sortino Ratio", "0"},
+ {"Probabilistic Sharpe Ratio", "0%"},
+ {"Loss Rate", "0%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "0"},
+ {"Beta", "0"},
+ {"Annual Standard Deviation", "0"},
+ {"Annual Variance", "0"},
+ {"Information Ratio", "0"},
+ {"Tracking Error", "0"},
+ {"Treynor Ratio", "0"},
+ {"Total Fees", "$28.00"},
+ {"Estimated Strategy Capacity", "$80000.00"},
+ {"Lowest Capacity Asset", "GOOCV W78ZERHAT67A|GOOCV VP83T1ZUHROL"},
+ {"Portfolio Turnover", "35.27%"},
+ {"Drawdown Recovery", "0"},
+ {"OrderListHash", "94d4e9ad7a0c13884b49d68165ce6766"}
+ };
+ }
+}
diff --git a/Algorithm.Python/OrderSurfaceShortcutsRegressionAlgorithm.py b/Algorithm.Python/OrderSurfaceShortcutsRegressionAlgorithm.py
new file mode 100644
index 000000000000..920005d040ea
--- /dev/null
+++ b/Algorithm.Python/OrderSurfaceShortcutsRegressionAlgorithm.py
@@ -0,0 +1,127 @@
+# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from AlgorithmImports import *
+
+###
+### Regression algorithm asserting the flat order surface shortcuts: the numeric OrderFee
+### surface (order_fee.amount, arithmetic and comparison operators, order_event.order_fee_amount),
+### the flat combo group ids (order.group_order_manager_id, order_event.group_id, ComboOrderTicket),
+### the tag-argument tolerance of market_order/liquidate and order_target_notional
+###
+class OrderSurfaceShortcutsRegressionAlgorithm(QCAlgorithm):
+
+ def initialize(self):
+ self.set_start_date(2015, 12, 24)
+ self.set_end_date(2015, 12, 24)
+ self.set_cash(200000)
+
+ equity = self.add_equity("GOOG", leverage=4, fill_forward=True)
+ self._equity_symbol = equity.symbol
+ option = self.add_option(equity.symbol, fill_forward=True)
+ self._option_symbol = option.symbol
+
+ option.set_filter(lambda u: u.standards_only().strikes(-2, 2).expiration(0, 180))
+
+ self._tagged_ticket = None
+ self._notional_ticket = None
+ self._combo_ticket = None
+ self._combo_fill_group_ids = set()
+ self._combo_fill_events_count = 0
+
+ def on_data(self, slice):
+ if self._tagged_ticket is None and self.is_market_open(self._equity_symbol):
+ # the tag in the third positional slot must be accepted as the tag argument
+ self._tagged_ticket = self.market_order(self._equity_symbol, 1, "tagged entry")
+
+ # target an absolute notional value instead of a portfolio percentage
+ self._notional_ticket = self.order_target_notional(self._equity_symbol, 10000)
+ if self._notional_ticket is None:
+ raise AssertionError("order_target_notional was expected to place an order")
+
+ # a tag slipped into the symbol slot must fail pointing to the tag parameter
+ liquidate_failed = False
+ try:
+ self.liquidate("EOD close")
+ except Exception as exception:
+ liquidate_failed = True
+ if "tag" not in str(exception):
+ raise AssertionError("liquidate() with an unknown ticker was expected to point to the "
+ f"tag parameter but the error was: {exception}")
+ if not liquidate_failed:
+ raise AssertionError("liquidate() with an unknown ticker was expected to fail")
+
+ if self._combo_ticket is None and self.is_market_open(self._option_symbol):
+ chain = slice.option_chains.get(self._option_symbol)
+ if chain is None:
+ return
+ call_contracts = [contract for contract in chain if contract.right == OptionRight.CALL]
+ if not call_contracts:
+ return
+ first_expiry = min(contract.expiry for contract in call_contracts)
+ call_contracts = sorted((contract for contract in call_contracts if contract.expiry == first_expiry),
+ key=lambda contract: contract.strike)
+ if len(call_contracts) < 3:
+ return
+
+ legs = [
+ Leg.create(call_contracts[0].symbol, 1),
+ Leg.create(call_contracts[1].symbol, -2),
+ Leg.create(call_contracts[2].symbol, 1),
+ ]
+ self._combo_ticket = self.combo_market_order(legs, 10)
+
+ if len(self._combo_ticket) != len(legs) or len(self._combo_ticket.tickets) != len(legs):
+ raise AssertionError(f"Expected {len(legs)} leg tickets, found {len(self._combo_ticket)}")
+ if self._combo_ticket.group_order_manager_id is None:
+ raise AssertionError("The combo order ticket was expected to have a group order manager id")
+
+ def on_order_event(self, order_event):
+ if order_event.status != OrderStatus.FILLED:
+ return
+
+ order = self.transactions.get_order_by_id(order_event.order_id)
+
+ # the fee amount shortcuts and operators must match the two-level value.amount
+ fee_amount = order_event.order_fee.value.amount
+ if order_event.order_fee_amount != fee_amount or order_event.order_fee.amount != fee_amount:
+ raise AssertionError(f"Order fee amount shortcuts do not match the fee amount {fee_amount}")
+ fee = order_event.order_fee
+ if fee + fee != 2 * fee_amount or sum([fee, fee], 0) != 2 * fee_amount or (fee_amount != 0 and not fee > 0):
+ raise AssertionError(f"Order fee operators do not match the fee amount {fee_amount}")
+
+ if order.type == OrderType.COMBO_MARKET:
+ # Note: these fill events are received while the synchronous combo_market_order() call is still
+ # in flight, so the combo ticket is checked against them in on_end_of_algorithm
+ self._combo_fill_events_count += 1
+ if order.group_order_manager_id is None:
+ raise AssertionError("Combo orders were expected to have a group order manager id")
+ if order_event.group_id != order.group_order_manager_id:
+ raise AssertionError(f"Expected order event group id {order.group_order_manager_id}, "
+ f"found {order_event.group_id}")
+ self._combo_fill_group_ids.add(order.group_order_manager_id)
+ elif order.group_order_manager_id is not None or order_event.group_id is not None:
+ raise AssertionError("Non-combo orders were expected to have null group ids")
+
+ def on_end_of_algorithm(self):
+ if self._tagged_ticket is None or self._tagged_ticket.tag != "tagged entry":
+ raise AssertionError("The market order tag was not set from the tag argument")
+ if self._notional_ticket.status != OrderStatus.FILLED:
+ raise AssertionError("The notional target order was expected to be filled")
+ if self._combo_ticket is None or self._combo_fill_events_count != len(self._combo_ticket):
+ raise AssertionError("The combo order was expected to be placed and filled")
+ if not self._combo_ticket.filled:
+ raise AssertionError("The combo order ticket was expected to aggregate the leg fills")
+ if self._combo_fill_group_ids != {self._combo_ticket.group_order_manager_id}:
+ raise AssertionError(f"Expected all combo fills to have group id {self._combo_ticket.group_order_manager_id}, "
+ f"found {self._combo_fill_group_ids}")
diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs
index 9859cbc4094b..ca8122007820 100644
--- a/Algorithm/QCAlgorithm.Python.cs
+++ b/Algorithm/QCAlgorithm.Python.cs
@@ -1897,6 +1897,23 @@ public IndicatorHistory IndicatorHistory(PyObject indicator, IEnumerable
}
}
+ ///
+ /// Market order implementation accepting the tag in the third position: Send a market order and wait for it to be filled.
+ /// Absorbs the common 'market_order(symbol, quantity, tag)' call shape from Python, where the tag string
+ /// would otherwise bind to the 'asynchronous' flag and fail overload resolution
+ ///
+ /// Symbol of the MarketType Required.
+ /// Number of shares to request.
+ /// Place a custom order property or tag (e.g. indicator data).
+ /// The order properties to use. Defaults to
+ /// The order ticket instance.
+ [DocumentationAttribute(TradingAndOrders)]
+ public OrderTicket MarketOrder(PyObject symbol, decimal quantity, string tag, IOrderProperties orderProperties = null)
+ {
+ return MarketOrder(symbol.ConvertToSymbolEnumerable().Single(), quantity, asynchronous: false, tag: tag,
+ orderProperties: orderProperties);
+ }
+
///
/// Liquidate your portfolio holdings
///
diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs
index ddba02f0249c..4cbe2c712a5f 100644
--- a/Algorithm/QCAlgorithm.Trading.cs
+++ b/Algorithm/QCAlgorithm.Trading.cs
@@ -870,11 +870,11 @@ public List Order(OptionStrategy strategy, int quantity, bool async
/// Send the order asynchronously (false). Otherwise we'll block until it fills
/// String tag for the order (optional)
/// The order properties to use. Defaults to
- /// Sequence of order tickets, one for each leg
+ /// The combo order ticket, a list of order tickets, one for each leg
[DocumentationAttribute(TradingAndOrders)]
- public List ComboMarketOrder(List legs, int quantity, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null)
+ public ComboOrderTicket ComboMarketOrder(List legs, int quantity, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null)
{
- return SubmitComboOrder(legs, quantity, 0, asynchronous, tag, orderProperties);
+ return new ComboOrderTicket(SubmitComboOrder(legs, quantity, 0, asynchronous, tag, orderProperties));
}
///
@@ -885,10 +885,10 @@ public List ComboMarketOrder(List legs, int quantity, bool asy
/// Send the order asynchronously (false). Otherwise we'll block until it is fully submitted
/// String tag for the order (optional)
/// The order properties to use. Defaults to
- /// Sequence of order tickets, one for each leg
+ /// The combo order ticket, a list of order tickets, one for each leg
/// If not every leg has a defined limit price
[DocumentationAttribute(TradingAndOrders)]
- public List ComboLegLimitOrder(List legs, int quantity, bool asynchronous = false,
+ public ComboOrderTicket ComboLegLimitOrder(List legs, int quantity, bool asynchronous = false,
string tag = "", IOrderProperties orderProperties = null)
{
if (legs.Any(x => x.OrderPrice == null || x.OrderPrice == 0))
@@ -896,7 +896,7 @@ public List ComboLegLimitOrder(List legs, int quantity, bool a
throw new ArgumentException("ComboLegLimitOrder requires a limit price for each leg");
}
- return SubmitComboOrder(legs, quantity, 0, asynchronous, tag, orderProperties);
+ return new ComboOrderTicket(SubmitComboOrder(legs, quantity, 0, asynchronous, tag, orderProperties));
}
///
@@ -909,10 +909,10 @@ public List ComboLegLimitOrder(List legs, int quantity, bool a
/// Send the order asynchronously (false). Otherwise we'll block until it is fully submitted
/// String tag for the order (optional)
/// The order properties to use. Defaults to
- /// Sequence of order tickets, one for each leg
+ /// The combo order ticket, a list of order tickets, one for each leg
/// If the order type is neither ComboMarket, ComboLimit nor ComboLegLimit
[DocumentationAttribute(TradingAndOrders)]
- public List ComboLimitOrder(List legs, int quantity, decimal limitPrice,
+ public ComboOrderTicket ComboLimitOrder(List legs, int quantity, decimal limitPrice,
bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null)
{
if (limitPrice == 0)
@@ -925,7 +925,7 @@ public List ComboLimitOrder(List legs, int quantity, decimal l
throw new ArgumentException("ComboLimitOrder does not support limit prices for individual legs");
}
- return SubmitComboOrder(legs, quantity, limitPrice, asynchronous, tag, orderProperties);
+ return new ComboOrderTicket(SubmitComboOrder(legs, quantity, limitPrice, asynchronous, tag, orderProperties));
}
private List GenerateOptionStrategyOrders(OptionStrategy strategy, int strategyQuantity, bool asynchronous, string tag, IOrderProperties orderProperties)
@@ -1418,6 +1418,28 @@ public List Liquidate(IEnumerable symbols, bool asynchronou
return orderTickets;
}
+ ///
+ /// Liquidate the holdings of the security referenced by the given ticker
+ ///
+ /// If the ticker is not a known symbol this fails pointing to the tag parameter, absorbing the
+ /// common 'Liquidate(tag)' call shape where the tag would otherwise silently bind to the symbol argument
+ /// The ticker of the asset to liquidate
+ /// Flag to indicate if the symbols should be liquidated asynchronously
+ /// Custom tag to know who is calling this
+ /// Order properties to use
+ [DocumentationAttribute(TradingAndOrders)]
+ public List Liquidate(string ticker, bool asynchronous = false, string tag = null, IOrderProperties orderProperties = null)
+ {
+ if (!SymbolCache.TryGetSymbol(ticker, out var symbol))
+ {
+ throw new ArgumentException($"Liquidate(): '{ticker}' is not a recognized symbol. " +
+ "The first argument must be a Symbol, a ticker or a list of them. " +
+ $"To pass a custom tag use Liquidate(tag: \"{ticker}\")");
+ }
+
+ return Liquidate(symbol, asynchronous, tag, orderProperties);
+ }
+
///
/// Liquidate all holdings and cancel open orders. Called at the end of day for tick-strategies.
///
@@ -1431,6 +1453,44 @@ public List Liquidate(Symbol symbolToLiquidate, string tag)
return Liquidate(symbol: symbolToLiquidate, tag: tag).Select(x => x.OrderId).ToList();
}
+ ///
+ /// Sends a market order to adjust the holdings of the given symbol to the target notional value
+ /// in units of the account currency, rounding the target quantity down to the security's lot size.
+ /// Unlike , which targets a
+ /// percentage of the total portfolio value, this targets an absolute position value: for derivatives, the
+ /// contract multiplier is included, so the target quantity is targetNotional / (price x multiplier)
+ ///
+ /// Symbol of the asset to trade
+ /// The target notional value of the holdings, in units of the account currency
+ /// Send the order asynchronously (false). Otherwise we'll block until it fills
+ /// Place a custom order property or tag (e.g. indicator data).
+ /// The order properties to use. Defaults to
+ /// The order ticket instance, or null if the current holdings already match the target
+ [DocumentationAttribute(TradingAndOrders)]
+ public OrderTicket OrderTargetNotional(Symbol symbol, decimal targetNotional, bool asynchronous = false, string tag = "",
+ IOrderProperties orderProperties = null)
+ {
+ var security = GetSecurityForOrder(symbol);
+
+ // the current notional value of a single unit, including the contract multiplier and currency conversion
+ var unitValue = security.Holdings.GetQuantityValue(1).InAccountCurrency;
+ if (unitValue == 0)
+ {
+ throw new InvalidOperationException($"OrderTargetNotional(): {symbol.Value}: unable to compute an order quantity for " +
+ $"the target notional {targetNotional}: the security has no market price yet. Warm up the algorithm or wait for " +
+ "data before placing the order.");
+ }
+
+ var targetQuantity = OrderSizing.AdjustByLotSize(security, targetNotional / unitValue);
+ var quantity = targetQuantity - security.Holdings.Quantity;
+ if (quantity == 0)
+ {
+ return null;
+ }
+
+ return MarketOrder(symbol, quantity, asynchronous, tag, orderProperties);
+ }
+
///
/// Maximum number of orders for the algorithm
///
diff --git a/Common/Orders/ComboOrderTicket.cs b/Common/Orders/ComboOrderTicket.cs
new file mode 100644
index 000000000000..024cb6832ec7
--- /dev/null
+++ b/Common/Orders/ComboOrderTicket.cs
@@ -0,0 +1,62 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System.Collections.Generic;
+using System.Linq;
+
+namespace QuantConnect.Orders
+{
+ ///
+ /// The collection of leg order tickets resulting from a combo order submission,
+ /// with helpers to track the combo as a single unit instead of reassembling the
+ /// legs by group order manager id
+ ///
+ /// Deliberately a non-generic subclass: pythonnet converts
+ /// generic list instances into plain Python lists, which would strip these properties.
+ /// A non-generic subclass reaches Python as an object that still supports len(),
+ /// indexing and iteration
+ public class ComboOrderTicket : List
+ {
+ ///
+ /// The order tickets of the combo order legs
+ ///
+ public IReadOnlyList Tickets => this;
+
+ ///
+ /// The unique id of the group of orders this combo order consists of, null if empty
+ ///
+ public int? GroupOrderManagerId => Count > 0 ? this[0].SubmitRequest?.GroupOrderManager?.Id : null;
+
+ ///
+ /// True if every leg of the combo order has been completely filled
+ ///
+ public bool Filled => Count > 0 && this.All(ticket => ticket.Status == OrderStatus.Filled);
+
+ ///
+ /// Creates a new empty instance
+ ///
+ public ComboOrderTicket()
+ {
+ }
+
+ ///
+ /// Creates a new instance holding the given leg order tickets
+ ///
+ /// The order tickets of the combo order legs
+ public ComboOrderTicket(IEnumerable tickets) : base(tickets)
+ {
+ }
+ }
+}
diff --git a/Common/Orders/Fees/OrderFee.cs b/Common/Orders/Fees/OrderFee.cs
index 0d3d63203029..d707bba2db21 100644
--- a/Common/Orders/Fees/OrderFee.cs
+++ b/Common/Orders/Fees/OrderFee.cs
@@ -13,6 +13,7 @@
* limitations under the License.
*/
+using Newtonsoft.Json;
using ProtoBuf;
using QuantConnect.Securities;
@@ -30,6 +31,19 @@ public class OrderFee
[ProtoMember(1)]
public CashAmount Value { get; set; }
+ ///
+ /// Gets the order fee amount, shortcut for the of .
+ /// The two-level 'Value.Amount' is hard to discover, especially from Python ('order_fee.value.amount')
+ ///
+ [JsonIgnore]
+ public decimal Amount => Value.Amount;
+
+ ///
+ /// Gets the order fee currency, shortcut for the of
+ ///
+ [JsonIgnore]
+ public string Currency => Value.Currency;
+
///
/// Initializes a new instance of the class
///
@@ -67,6 +81,58 @@ public static implicit operator decimal(OrderFee m)
return m.Value.Amount;
}
+ // Numeric operators delegating to the fee amount. In C# these mirror what the implicit
+ // decimal conversion above already allowed, so semantics are unchanged. Their real purpose
+ // is Python: pythonnet maps C# operators to __add__/__radd__/__gt__/... so summing or
+ // comparing fees works instead of raising TypeError. Note float(fee) is still not supported
+ // (pythonnet does not wire the nb_float slot for CLR types), use the 'Amount' property instead.
+
+ /// Adds two order fee amounts
+ public static decimal operator +(OrderFee a, OrderFee b) => a.Value.Amount + b.Value.Amount;
+ /// Adds a value to the order fee amount
+ public static decimal operator +(OrderFee fee, decimal value) => fee.Value.Amount + value;
+ /// Adds the order fee amount to a value
+ public static decimal operator +(decimal value, OrderFee fee) => value + fee.Value.Amount;
+
+ /// Subtracts two order fee amounts
+ public static decimal operator -(OrderFee a, OrderFee b) => a.Value.Amount - b.Value.Amount;
+ /// Subtracts a value from the order fee amount
+ public static decimal operator -(OrderFee fee, decimal value) => fee.Value.Amount - value;
+ /// Subtracts the order fee amount from a value
+ public static decimal operator -(decimal value, OrderFee fee) => value - fee.Value.Amount;
+
+ /// Multiplies two order fee amounts
+ public static decimal operator *(OrderFee a, OrderFee b) => a.Value.Amount * b.Value.Amount;
+ /// Multiplies the order fee amount by a value
+ public static decimal operator *(OrderFee fee, decimal value) => fee.Value.Amount * value;
+ /// Multiplies a value by the order fee amount
+ public static decimal operator *(decimal value, OrderFee fee) => value * fee.Value.Amount;
+
+ /// Divides two order fee amounts
+ public static decimal operator /(OrderFee a, OrderFee b) => a.Value.Amount / b.Value.Amount;
+ /// Divides the order fee amount by a value
+ public static decimal operator /(OrderFee fee, decimal value) => fee.Value.Amount / value;
+ /// Divides a value by the order fee amount
+ public static decimal operator /(decimal value, OrderFee fee) => value / fee.Value.Amount;
+
+ /// Determines whether one order fee amount is less than another
+ public static bool operator <(OrderFee a, OrderFee b) => a.Value.Amount < b.Value.Amount;
+ /// Determines whether one order fee amount is greater than another
+ public static bool operator >(OrderFee a, OrderFee b) => a.Value.Amount > b.Value.Amount;
+ /// Determines whether one order fee amount is less than or equal to another
+ public static bool operator <=(OrderFee a, OrderFee b) => a.Value.Amount <= b.Value.Amount;
+ /// Determines whether one order fee amount is greater than or equal to another
+ public static bool operator >=(OrderFee a, OrderFee b) => a.Value.Amount >= b.Value.Amount;
+
+ /// Determines whether the order fee amount is less than the given value
+ public static bool operator <(OrderFee fee, decimal value) => fee.Value.Amount < value;
+ /// Determines whether the order fee amount is greater than the given value
+ public static bool operator >(OrderFee fee, decimal value) => fee.Value.Amount > value;
+ /// Determines whether the order fee amount is less than or equal to the given value
+ public static bool operator <=(OrderFee fee, decimal value) => fee.Value.Amount <= value;
+ /// Determines whether the order fee amount is greater than or equal to the given value
+ public static bool operator >=(OrderFee fee, decimal value) => fee.Value.Amount >= value;
+
///
/// Gets an instance of that represents zero.
///
diff --git a/Common/Orders/Order.cs b/Common/Orders/Order.cs
index dfddcba0655c..90dd9f2f830f 100644
--- a/Common/Orders/Order.cs
+++ b/Common/Orders/Order.cs
@@ -230,6 +230,13 @@ public bool IsMarketable
[JsonProperty(PropertyName = "groupOrderManager", DefaultValueHandling = DefaultValueHandling.Ignore)]
public GroupOrderManager GroupOrderManager { get; set; }
+ ///
+ /// The unique id of the group of orders this order belongs to, if this is a combo order, null otherwise.
+ /// Shortcut for .Id
+ ///
+ [JsonIgnore]
+ public int? GroupOrderManagerId => GroupOrderManager?.Id;
+
///
/// The adjustment mode used on the order fill price
///
diff --git a/Common/Orders/OrderEvent.cs b/Common/Orders/OrderEvent.cs
index 32bc2e2c7fc2..fce41eead2d9 100644
--- a/Common/Orders/OrderEvent.cs
+++ b/Common/Orders/OrderEvent.cs
@@ -74,6 +74,13 @@ public class OrderEvent
[ProtoMember(6)]
public OrderFee OrderFee { get; set; }
+ ///
+ /// The fee amount associated with the order, shortcut for .Value.Amount.
+ /// The two-level 'OrderFee.Value.Amount' is hard to discover, especially from Python
+ ///
+ [JsonIgnore]
+ public decimal OrderFeeAmount => OrderFee?.Value.Amount ?? 0m;
+
///
/// Fill price information about the order
///
@@ -233,6 +240,12 @@ public bool? TrailingAsPercentage
[JsonIgnore]
public OrderTicket Ticket { get; set; }
+ ///
+ /// The unique id of the order group this event's order belongs to, if it is a combo order leg, null otherwise
+ ///
+ [JsonIgnore]
+ public int? GroupId => Ticket?.SubmitRequest?.GroupOrderManager?.Id;
+
///
/// Order Event empty constructor required for json converter
///
diff --git a/Tests/Algorithm/AlgorithmTradingTests.cs b/Tests/Algorithm/AlgorithmTradingTests.cs
index 831a9702e59a..0c452173bb6b 100644
--- a/Tests/Algorithm/AlgorithmTradingTests.cs
+++ b/Tests/Algorithm/AlgorithmTradingTests.cs
@@ -1591,6 +1591,86 @@ public void LiquidateIgnoresSymbolsNotAddedToTheAlgorithm(Language language, boo
Assert.IsEmpty(liquidatedTickets);
}
+ [Test]
+ public void LiquidateWithKnownTickerResolvesTheSymbol()
+ {
+ var algo = GetAlgorithm(out _, 1, 0);
+
+ // MSFT was registered in the symbol cache when added to the algorithm
+ List liquidatedTickets = null;
+ Assert.DoesNotThrow(() => liquidatedTickets = algo.Liquidate("MSFT"));
+
+ // no holdings, so nothing to liquidate
+ Assert.IsEmpty(liquidatedTickets);
+ }
+
+ [Test]
+ public void LiquidateWithUnknownTickerFailsPointingToTheTagParameter()
+ {
+ var algo = GetAlgorithm(out _, 1, 0);
+
+ // the common slip: the tag passed as the first positional argument
+ var exception = Assert.Throws(() => algo.Liquidate("EOD close"));
+ Assert.IsTrue(exception.Message.Contains("EOD close", StringComparison.InvariantCulture));
+ Assert.IsTrue(exception.Message.Contains("tag", StringComparison.InvariantCulture));
+ }
+
+ [TestCase(10000, 0, 400)]
+ [TestCase(10000, 100, 300)]
+ [TestCase(-10000, 0, -400)]
+ [TestCase(-10000, 100, -500)]
+ [TestCase(10010, 0, 400, Description = "Rounds the target quantity down to the lot size")]
+ [TestCase(0, 100, -100, Description = "Zero notional closes the position")]
+ public void OrderTargetNotionalComputesTheQuantityFromTheNotionalValue(decimal targetNotional, decimal initialHoldings,
+ decimal expectedQuantity)
+ {
+ var algo = GetAlgorithm(out var msft, 1, 0);
+ msft.Exchange.SetMarketHours(new List() { MarketHoursSegment.OpenAllDay() });
+ Update(msft, 25);
+ msft.Holdings.SetHoldings(25, initialHoldings);
+
+ var ticket = algo.OrderTargetNotional(Symbols.MSFT, targetNotional);
+
+ Assert.IsNotNull(ticket);
+ Assert.AreEqual(expectedQuantity, ticket.Quantity);
+ }
+
+ [Test]
+ public void OrderTargetNotionalIncludesTheContractMultiplier()
+ {
+ var algo = GetAlgorithm(out _, 1, 0);
+ var es20h20 = algo.AddFutureContract(
+ QuantConnect.Symbol.CreateFuture(Futures.Indices.SP500EMini, Market.CME, new DateTime(2020, 3, 20)),
+ Resolution.Minute);
+ Update(es20h20, 2000);
+
+ // ES has a contract multiplier of 50: one contract is worth 2000 * 50 = 100k
+ var ticket = algo.OrderTargetNotional(es20h20.Symbol, 500000);
+
+ Assert.IsNotNull(ticket);
+ Assert.AreEqual(5, ticket.Quantity);
+ }
+
+ [Test]
+ public void OrderTargetNotionalReturnsNullWhenHoldingsAlreadyMatchTheTarget()
+ {
+ var algo = GetAlgorithm(out var msft, 1, 0);
+ msft.Exchange.SetMarketHours(new List() { MarketHoursSegment.OpenAllDay() });
+ Update(msft, 25);
+ msft.Holdings.SetHoldings(25, 400);
+
+ Assert.IsNull(algo.OrderTargetNotional(Symbols.MSFT, 10000));
+ }
+
+ [Test]
+ public void OrderTargetNotionalFailsWithoutMarketPrice()
+ {
+ var algo = GetAlgorithm(out _, 1, 0);
+
+ var exception = Assert.Throws(() => algo.OrderTargetNotional(Symbols.MSFT, 10000));
+ Assert.IsTrue(exception.Message.Contains("no market price", StringComparison.InvariantCulture));
+ }
+
[Test]
public void MarketOrdersAreSupportedForFuturesOnExtendedMarketHours()
{
diff --git a/Tests/Common/Orders/ComboOrderTicketTests.cs b/Tests/Common/Orders/ComboOrderTicketTests.cs
new file mode 100644
index 000000000000..0fb94b312738
--- /dev/null
+++ b/Tests/Common/Orders/ComboOrderTicketTests.cs
@@ -0,0 +1,83 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System;
+using NUnit.Framework;
+using QuantConnect.Orders;
+
+namespace QuantConnect.Tests.Common.Orders
+{
+ [TestFixture]
+ public class ComboOrderTicketTests
+ {
+ private int _orderId;
+
+ [Test]
+ public void ExposesTheGroupOrderManagerIdOfItsLegs()
+ {
+ var groupOrderManager = new GroupOrderManager(33, 2, 10);
+ var comboTicket = new ComboOrderTicket(new[]
+ {
+ CreateLegTicket(groupOrderManager, Symbols.SPY_C_192_Feb19_2016, 10, OrderStatus.Submitted),
+ CreateLegTicket(groupOrderManager, Symbols.SPY_P_192_Feb19_2016, -10, OrderStatus.Submitted)
+ });
+
+ Assert.AreEqual(33, comboTicket.GroupOrderManagerId);
+ Assert.AreEqual(2, comboTicket.Tickets.Count);
+ Assert.AreSame(comboTicket, comboTicket.Tickets);
+ }
+
+ [Test]
+ public void FilledOnlyWhenEveryLegIsFilled()
+ {
+ var groupOrderManager = new GroupOrderManager(1, 2, 10);
+ var firstLeg = CreateLegTicket(groupOrderManager, Symbols.SPY_C_192_Feb19_2016, 10, OrderStatus.Filled);
+ var secondLeg = CreateLegTicket(groupOrderManager, Symbols.SPY_P_192_Feb19_2016, -10, OrderStatus.PartiallyFilled);
+
+ var comboTicket = new ComboOrderTicket(new[] { firstLeg, secondLeg });
+ Assert.IsFalse(comboTicket.Filled);
+
+ comboTicket = new ComboOrderTicket(new[]
+ {
+ CreateLegTicket(groupOrderManager, Symbols.SPY_C_192_Feb19_2016, 10, OrderStatus.Filled),
+ CreateLegTicket(groupOrderManager, Symbols.SPY_P_192_Feb19_2016, -10, OrderStatus.Filled)
+ });
+ Assert.IsTrue(comboTicket.Filled);
+ }
+
+ [Test]
+ public void EmptyTicketHasNoGroupIdAndIsNotFilled()
+ {
+ var comboTicket = new ComboOrderTicket();
+
+ Assert.IsNull(comboTicket.GroupOrderManagerId);
+ Assert.IsFalse(comboTicket.Filled);
+ }
+
+ private OrderTicket CreateLegTicket(GroupOrderManager groupOrderManager, Symbol symbol, decimal quantity, OrderStatus status)
+ {
+ var request = new SubmitOrderRequest(OrderType.ComboMarket, symbol.SecurityType, symbol, quantity, 0, 0,
+ new DateTime(2016, 2, 16, 11, 53, 30), "", groupOrderManager: groupOrderManager);
+ request.SetOrderId(++_orderId);
+
+ var ticket = new OrderTicket(null, request);
+ var order = Order.CreateOrder(request);
+ order.Status = status;
+ ticket.SetOrder(order);
+
+ return ticket;
+ }
+ }
+}
diff --git a/Tests/Common/Orders/Fees/OrderFeeTests.cs b/Tests/Common/Orders/Fees/OrderFeeTests.cs
new file mode 100644
index 000000000000..e0fdf8b6ee35
--- /dev/null
+++ b/Tests/Common/Orders/Fees/OrderFeeTests.cs
@@ -0,0 +1,93 @@
+/*
+ * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
+ * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using QuantConnect.Orders.Fees;
+using QuantConnect.Securities;
+
+namespace QuantConnect.Tests.Common.Orders.Fees
+{
+ [TestFixture]
+ public class OrderFeeTests
+ {
+ [Test]
+ public void FlatAmountAndCurrencyShortcuts()
+ {
+ var fee = new OrderFee(new CashAmount(12.34m, Currencies.EUR));
+
+ Assert.AreEqual(12.34m, fee.Amount);
+ Assert.AreEqual(Currencies.EUR, fee.Currency);
+ }
+
+ [Test]
+ public void ArithmeticOperatorsDelegateToTheFeeAmount()
+ {
+ var fee = new OrderFee(new CashAmount(2m, Currencies.USD));
+ var otherFee = new OrderFee(new CashAmount(3m, Currencies.USD));
+
+ Assert.AreEqual(5m, fee + otherFee);
+ Assert.AreEqual(3m, fee + 1m);
+ Assert.AreEqual(3m, 1m + fee);
+
+ Assert.AreEqual(-1m, fee - otherFee);
+ Assert.AreEqual(1m, fee - 1m);
+ Assert.AreEqual(8m, 10m - fee);
+
+ Assert.AreEqual(6m, fee * otherFee);
+ Assert.AreEqual(4m, fee * 2m);
+ Assert.AreEqual(4m, 2m * fee);
+
+ Assert.AreEqual(1.5m, otherFee / fee);
+ Assert.AreEqual(1m, fee / 2m);
+ Assert.AreEqual(5m, 10m / fee);
+ }
+
+ [Test]
+ public void ComparisonOperatorsDelegateToTheFeeAmount()
+ {
+ var fee = new OrderFee(new CashAmount(2m, Currencies.USD));
+ var otherFee = new OrderFee(new CashAmount(3m, Currencies.USD));
+
+ Assert.IsTrue(fee < otherFee);
+ Assert.IsFalse(fee > otherFee);
+ Assert.IsTrue(fee <= otherFee);
+ Assert.IsFalse(fee >= otherFee);
+
+ Assert.IsTrue(fee > 0m);
+ Assert.IsFalse(fee < 2m);
+ Assert.IsTrue(fee <= 2m);
+ Assert.IsTrue(fee >= 2m);
+ }
+
+ [Test]
+ public void SerializationShapeIsUnchangedByTheShortcutProperties()
+ {
+ var fee = new OrderFee(new CashAmount(12.34m, Currencies.EUR));
+
+ var json = JsonConvert.SerializeObject(fee);
+ var jObject = JObject.Parse(json);
+
+ // the flat Amount/Currency shortcuts are json-ignored, only 'Value' is serialized
+ CollectionAssert.AreEqual(new[] { "Value" }, jObject.Properties().Select(property => property.Name));
+
+ var deserialized = JsonConvert.DeserializeObject(json);
+ Assert.AreEqual(fee.Value.Amount, deserialized.Value.Amount);
+ Assert.AreEqual(fee.Value.Currency, deserialized.Value.Currency);
+ }
+ }
+}
diff --git a/Tests/Common/Orders/OrderEventTests.cs b/Tests/Common/Orders/OrderEventTests.cs
index 170c6bf3081c..78163c0287fd 100644
--- a/Tests/Common/Orders/OrderEventTests.cs
+++ b/Tests/Common/Orders/OrderEventTests.cs
@@ -50,6 +50,35 @@ public void JsonIgnores()
Assert.IsTrue(json.Contains("LimitPrice", StringComparison.InvariantCulture));
Assert.IsTrue(json.Contains("StopPrice", StringComparison.InvariantCulture));
Assert.IsTrue(json.Contains(value: "IsInTheMoney", StringComparison.InvariantCulture));
+
+ // the flat shortcut properties are not serialized
+ Assert.IsFalse(json.Contains("OrderFeeAmount", StringComparison.InvariantCulture));
+ Assert.IsFalse(json.Contains("GroupId", StringComparison.InvariantCulture));
+ }
+
+ [Test]
+ public void OrderFeeAmountShortcut()
+ {
+ var order = new MarketOrder(Symbols.BTCUSD, 0.123m, DateTime.UtcNow);
+ var orderEvent = new OrderEvent(order, DateTime.UtcNow, new OrderFee(new CashAmount(88, Currencies.USD)));
+
+ Assert.AreEqual(88m, orderEvent.OrderFeeAmount);
+ Assert.AreEqual(0m, new OrderEvent().OrderFeeAmount);
+ }
+
+ [Test]
+ public void GroupIdComesFromTheTicketGroupOrderManager()
+ {
+ var groupOrderManager = new GroupOrderManager(11, 2, 10);
+ var request = new SubmitOrderRequest(OrderType.ComboMarket, SecurityType.Option, Symbols.SPY_C_192_Feb19_2016,
+ 10, 0, 0, DateTime.UtcNow, "", groupOrderManager: groupOrderManager);
+ var order = new ComboMarketOrder(Symbols.SPY_C_192_Feb19_2016, 10, DateTime.UtcNow, groupOrderManager);
+
+ var orderEvent = new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero);
+ Assert.IsNull(orderEvent.GroupId);
+
+ orderEvent.Ticket = new OrderTicket(null, request);
+ Assert.AreEqual(11, orderEvent.GroupId);
}
[Test]
diff --git a/Tests/Common/Orders/OrderTests.cs b/Tests/Common/Orders/OrderTests.cs
index 4d5d65d81079..d4cd8a768fdd 100644
--- a/Tests/Common/Orders/OrderTests.cs
+++ b/Tests/Common/Orders/OrderTests.cs
@@ -44,6 +44,17 @@ public void GetValueTest(ValueTestParameters parameters)
Assert.AreEqual(parameters.ExpectedValue, value);
}
+ [Test]
+ public void GroupOrderManagerIdShortcut()
+ {
+ var groupOrderManager = new GroupOrderManager(7, 2, 10);
+ var comboOrder = new ComboMarketOrder(Symbols.SPY_C_192_Feb19_2016, 10, DateTime.UtcNow, groupOrderManager);
+ Assert.AreEqual(7, comboOrder.GroupOrderManagerId);
+
+ var marketOrder = new MarketOrder(Symbols.SPY, 10, DateTime.UtcNow);
+ Assert.IsNull(marketOrder.GroupOrderManagerId);
+ }
+
[TestCase(OrderDirection.Sell, 300, 0.1, true, 270)]
[TestCase(OrderDirection.Sell, 300, 30, false, 270)]
[TestCase(OrderDirection.Buy, 300, 0.1, true, 330)]