diff --git a/Algorithm.CSharp/OrderTicketDemoAlgorithm.cs b/Algorithm.CSharp/OrderTicketDemoAlgorithm.cs index bc12330e213f..f1d09b682f2a 100644 --- a/Algorithm.CSharp/OrderTicketDemoAlgorithm.cs +++ b/Algorithm.CSharp/OrderTicketDemoAlgorithm.cs @@ -40,6 +40,7 @@ public class OrderTicketDemoAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinit private readonly List _openStopMarketOrders = new List(); private readonly List _openStopLimitOrders = new List(); private readonly List _openTrailingStopOrders = new List(); + private readonly List _openTrailingStopLimitOrders = new List(); /// /// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized. @@ -86,6 +87,10 @@ public override void OnData(Slice slice) // MARKET ON CLOSE ORDERS MarketOnCloseOrders(); + + // TRAILING STOP LIMIT ORDERS + + TrailingStopLimitOrders(); } /// @@ -418,6 +423,84 @@ private void TrailingStopOrders() } } + /// + /// TrailingStopLimitOrders work the same way as StopLimitOrders, except + /// their stop price is adjusted to a certain amount, keeping it a certain + /// fixed distance from/to the market price, depending on the order direction. + /// The limit price adjusts based on a limit offset compared to the stop price. + /// You can submit requests to update or cancel the StopLimitOrder at any time. + /// The stop price, trailing amount, limit price and limit offset for an order + /// can be retrieved from the ticket using the OrderTicket.Get(OrderField) method, for example: + /// + /// var currentStopPrice = orderTicket.Get(OrderField.StopPrice); + /// var trailingAmount = orderTicket.Get(OrderField.TrailingAmount); + /// var currentLimitPrice = orderTicket.Get(OrderField.LimitPrice); + /// var limitOffset = orderTicket.Get(OrderField.LimitOffset); + /// + /// + private void TrailingStopLimitOrders() + { + if (TimeIs(7, 12, 0)) + { + Log("Submitting TrailingStopLimitOrder"); + + // a long stop is triggered when the price rises above the value + // so we'll set a long stop .25% above the current bar's close + + var close = Securities[symbol].Close; + var stopPrice = close * 1.0025m; + var limitPrice = stopPrice + 0.1m; + var newTicket = TrailingStopLimitOrder(symbol, 10, stopPrice, limitPrice, trailingAmount: 0.0025m, trailingAsPercentage: true, 0.1m); + _openTrailingStopLimitOrders.Add(newTicket); + + // a short stop is triggered when the price falls below the value + // so we'll set a short stop .25% below the current bar's close + + stopPrice = close * .9975m; + limitPrice = stopPrice - 0.1m; + newTicket = TrailingStopLimitOrder(symbol, -10, stopPrice, limitPrice, trailingAmount: 0.0025m, trailingAsPercentage: true, 0.1m); + _openTrailingStopLimitOrders.Add(newTicket); + } + + // when we submitted new trailing stop limit orders we placed them into this list, + // so while there's two entries they're still open and need processing + else if (_openTrailingStopLimitOrders.Count == 2) + { + // check if either is filled and cancel the other + var longOrder = _openTrailingStopLimitOrders[0]; + var shortOrder = _openTrailingStopLimitOrders[1]; + if (CheckPairOrdersForFills(longOrder, shortOrder)) + { + _openTrailingStopLimitOrders.Clear(); + return; + } + + // if neither order has filled in the last 5 minutes, bring in the trailing percentage by 0.01% + if ((UtcTime - longOrder.Time).TotalMinutes % 5 != 0) + { + return; + } + var longTrailingPercentage = longOrder.Get(OrderField.TrailingAmount); + var newLongTrailingPercentage = Math.Max(longTrailingPercentage - 0.0001m, 0.0001m); + var shortTrailingPercentage = shortOrder.Get(OrderField.TrailingAmount); + var newShortTrailingPercentage = Math.Max(shortTrailingPercentage - 0.0001m, 0.0001m); + Log($"Updating trailing percentages - Long: {newLongTrailingPercentage.ToStringInvariant("0.000")} Short: {newShortTrailingPercentage.ToStringInvariant("0.000")}"); + + longOrder.Update(new UpdateOrderFields + { + // we could change the quantity, but need to specify it + //Quantity = + TrailingAmount = newLongTrailingPercentage, + Tag = "Update #" + (longOrder.UpdateRequests.Count + 1) + }); + shortOrder.Update(new UpdateOrderFields + { + TrailingAmount = newShortTrailingPercentage, + Tag = "Update #" + (shortOrder.UpdateRequests.Count + 1) + }); + } + } + /// /// MarketOnCloseOrders are always executed at the next market's closing /// price. The only properties that can be updated are the quantity and @@ -503,7 +586,6 @@ private void MarketOnOpenOrders() Tag = "Update #" + (ticket.UpdateRequests.Count + 1) }); } - } public override void OnOrderEvent(OrderEvent orderEvent) @@ -572,9 +654,9 @@ public override void OnEndOfAlgorithm() var openOrderTickets = Transactions.GetOpenOrderTickets(basicOrderTicketFilter); var remainingOpenOrders = Transactions.GetOpenOrdersRemainingQuantity(basicOrderTicketFilter); - if (filledOrders.Count() != 9 || orderTickets.Count() != 12) + if (filledOrders.Count() != 10 || orderTickets.Count() != 14) { - throw new RegressionTestException($"There were expected 9 filled orders and 12 order tickets"); + throw new RegressionTestException($"There were expected 10 filled orders and 14 order tickets"); } if (openOrders.Count != 0 || openOrderTickets.Any()) { @@ -604,9 +686,9 @@ public override void OnEndOfAlgorithm() var defaultOpenOrderTickets = Transactions.GetOpenOrderTickets(); var defaultOpenOrdersRemaining = Transactions.GetOpenOrdersRemainingQuantity(); - if (defaultOrders.Count() != 12 || defaultOrderTickets.Count() != 12) + if (defaultOrders.Count() != 14 || defaultOrderTickets.Count() != 14) { - throw new RegressionTestException($"There were expected 12 orders and 12 order tickets"); + throw new RegressionTestException($"There were expected 14 orders and 14 order tickets"); } if (defaultOpenOrders.Count != 0 || defaultOpenOrderTickets.Any()) { @@ -648,33 +730,33 @@ public override void OnEndOfAlgorithm() /// public Dictionary ExpectedStatistics => new Dictionary { - {"Total Orders", "12"}, + {"Total Orders", "14"}, {"Average Win", "0%"}, - {"Average Loss", "-0.01%"}, - {"Compounding Annual Return", "77.184%"}, + {"Average Loss", "0.00%"}, + {"Compounding Annual Return", "63.380%"}, {"Drawdown", "0.100%"}, {"Expectancy", "-1"}, {"Start Equity", "100000"}, - {"End Equity", "100734.03"}, - {"Net Profit", "0.734%"}, - {"Sharpe Ratio", "12.597"}, - {"Sortino Ratio", "464.862"}, - {"Probabilistic Sharpe Ratio", "99.521%"}, + {"End Equity", "100629.62"}, + {"Net Profit", "0.630%"}, + {"Sharpe Ratio", "12.445"}, + {"Sortino Ratio", "680.042"}, + {"Probabilistic Sharpe Ratio", "99.827%"}, {"Loss Rate", "100%"}, {"Win Rate", "0%"}, {"Profit-Loss Ratio", "0"}, - {"Alpha", "0.2"}, - {"Beta", "0.195"}, - {"Annual Standard Deviation", "0.047"}, + {"Alpha", "0.165"}, + {"Beta", "0.161"}, + {"Annual Standard Deviation", "0.039"}, {"Annual Variance", "0.002"}, - {"Information Ratio", "-7.724"}, - {"Tracking Error", "0.18"}, - {"Treynor Ratio", "3.002"}, - {"Total Fees", "$9.00"}, - {"Estimated Strategy Capacity", "$49000000.00"}, + {"Information Ratio", "-7.97"}, + {"Tracking Error", "0.187"}, + {"Treynor Ratio", "2.998"}, + {"Total Fees", "$10.00"}, + {"Estimated Strategy Capacity", "$51000000.00"}, {"Lowest Capacity Asset", "SPY R735QTJ8XC9X"}, - {"Portfolio Turnover", "7.18%"}, - {"OrderListHash", "d1ed6571d5895f4c951d287b2903f561"} + {"Portfolio Turnover", "6.90%"}, + {"OrderListHash", "4a84e8f5608a8a32ff95d0004a35a822"} }; } } diff --git a/Algorithm.CSharp/SplitEquityRegressionAlgorithm.cs b/Algorithm.CSharp/SplitEquityRegressionAlgorithm.cs index 6149a41c9a9e..4883c4d1cb72 100644 --- a/Algorithm.CSharp/SplitEquityRegressionAlgorithm.cs +++ b/Algorithm.CSharp/SplitEquityRegressionAlgorithm.cs @@ -59,6 +59,8 @@ public override void OnData(Slice slice) _tickets.Add(StopLimitOrder(_aapl, 10, 15, 15)); _tickets.Add(TrailingStopOrder(_aapl, 10, 1000, 60m, trailingAsPercentage: false)); _tickets.Add(TrailingStopOrder(_aapl, 10, 1000, 0.1m, trailingAsPercentage: true)); + _tickets.Add(TrailingStopLimitOrder(_aapl, 10, 1000m, 1005m, 60m, trailingAsPercentage: false, 5m)); + _tickets.Add(TrailingStopLimitOrder(_aapl, 10, 1000m, 1005m, 0.1m, trailingAsPercentage: true, 5m)); } } @@ -131,6 +133,56 @@ public override void OnEndOfAlgorithm() } } break; + + case OrderType.TrailingStopLimit: + stopPrice = ticket.Get(OrderField.StopPrice); + trailingAmount = ticket.Get(OrderField.TrailingAmount); + + if (ticket.Get(OrderField.TrailingAsPercentage)) + { + // We only expect one stop price update in this algorithm + if (Math.Abs(stopPrice - _marketPriceAtLatestSplit) > 0.1m * stopPrice) + { + throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Stop Price equal to 2.14, but was {stopPrice}"); + } + + // Trailing amount unchanged since it's a percentage + if (trailingAmount != 0.1m) + { + throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Trailing Amount equal to 0.214m, but was {trailingAmount}"); + } + } + else + { + // We only expect one stop price update in this algorithm + if (Math.Abs(stopPrice - _marketPriceAtLatestSplit) > 60m * _splitFactor) + { + throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Stop Price equal to 2.14, but was {ticket.Get(OrderField.StopPrice)}"); + } + + if (trailingAmount != 8.57m) + { + throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Trailing Amount equal to 8.57m, but was {trailingAmount}"); + } + } + + // Limit offset should be updated after split + var limitOffset = ticket.Get(OrderField.LimitOffset); + var limitPrice = ticket.Get(OrderField.LimitPrice); + var expectedLimitOffsetAfterSplit = 0.7143m; + var expectedLimitPriceAfterSplit = stopPrice + expectedLimitOffsetAfterSplit; + + if (limitOffset != expectedLimitOffsetAfterSplit) + { + throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Limit Offset equal to 0.714m, but was {limitOffset}"); + } + + if (limitPrice != expectedLimitPriceAfterSplit) + { + throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Limit Price equal to {expectedLimitPriceAfterSplit}, but was {limitPrice}"); + } + + break; } } } @@ -165,7 +217,7 @@ public override void OnEndOfAlgorithm() /// public Dictionary ExpectedStatistics => new Dictionary { - {"Total Orders", "5"}, + {"Total Orders", "7"}, {"Average Win", "0%"}, {"Average Loss", "0%"}, {"Compounding Annual Return", "0%"}, @@ -191,7 +243,7 @@ public override void OnEndOfAlgorithm() {"Estimated Strategy Capacity", "$0"}, {"Lowest Capacity Asset", ""}, {"Portfolio Turnover", "0%"}, - {"OrderListHash", "1433d839e97cd82fc9b051cfd98f166f"} + {"OrderListHash", "db1b4cf6b2280f09a854a785d3c61cbf"} }; } } diff --git a/Algorithm.CSharp/TrailingStopLimitOrderRegressionAlgorithm.cs b/Algorithm.CSharp/TrailingStopLimitOrderRegressionAlgorithm.cs new file mode 100644 index 000000000000..e97feba8c605 --- /dev/null +++ b/Algorithm.CSharp/TrailingStopLimitOrderRegressionAlgorithm.cs @@ -0,0 +1,251 @@ +/* + * 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 QuantConnect.Data; +using QuantConnect.Indicators; +using QuantConnect.Interfaces; +using QuantConnect.Orders; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Basic algorithm demonstrating how to place trailing stop limit orders. + /// + /// + /// + /// + public class TrailingStopLimitOrderRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private Symbol _symbol; + private OrderTicket _buyOrderTicket; + private OrderTicket _sellOrderTicket; + private Slice _previousSlice; + + private const decimal Tolerance = 0.001m; + private const int FastPeriod = 30; + private const int SlowPeriod = 60; + private const decimal TrailingAmount = 5m; + private const decimal LimitOffset = 1m; + + private ExponentialMovingAverage _fast; + private ExponentialMovingAverage _slow; + + public bool IsReady { get { return _fast.IsReady && _slow.IsReady; } } + public bool TrendIsUp { get { return IsReady && _fast > _slow * (1 + Tolerance); } } + public bool TrendIsDown { get { return IsReady && _fast < _slow * (1 + Tolerance); } } + + /// + /// Initialize the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized. + /// + public override void Initialize() + { + SetStartDate(2013, 01, 01); + SetEndDate(2017, 01, 01); + SetCash(100000); + + _symbol = AddEquity("SPY", Resolution.Daily).Symbol; + + _fast = EMA(_symbol, FastPeriod, Resolution.Daily); + _slow = EMA(_symbol, SlowPeriod, Resolution.Daily); + } + + /// + /// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. + /// + /// Slice object keyed by symbol containing the stock data + public override void OnData(Slice slice) + { + if (!slice.ContainsKey(_symbol)) + { + return; + } + + if (!IsReady) + { + return; + } + + var security = Securities[_symbol]; + + if (_buyOrderTicket == null) + { + if (TrendIsUp) + { + _buyOrderTicket = TrailingStopLimitOrder(_symbol, 100, security.Price * 1.10m, + (security.Price * 1.10m) + LimitOffset, TrailingAmount, false, LimitOffset); + } + } + else if (_buyOrderTicket.Status != OrderStatus.Filled) + { + var stopPrice = _buyOrderTicket.Get(OrderField.StopPrice); + var limitPrice = _buyOrderTicket.Get(OrderField.LimitPrice); + + // Get the previous bar to compare to the stop and limit prices, + // because stop and limit price update attempt with the current slice data happens after OnData. + var low = _previousSlice.QuoteBars.TryGetValue(_symbol, out var quoteBar) + ? quoteBar.Ask.Low + : _previousSlice.Bars[_symbol].Low; + + var stopPriceToMarketPriceDistance = stopPrice - low; + if (stopPriceToMarketPriceDistance > TrailingAmount) + { + throw new RegressionTestException($"StopPrice {stopPrice} should be within {TrailingAmount} of the previous low price {low} at all times."); + } + + var stopPriceToLimitPriceDistance = limitPrice - stopPrice; + if (stopPriceToLimitPriceDistance != LimitOffset) + { + throw new RegressionTestException($"LimitPrice {limitPrice} should be {LimitOffset} from the stop price {stopPrice} at all times."); + } + } + + else if (_sellOrderTicket == null) + { + if (TrendIsDown) + { + _sellOrderTicket = TrailingStopLimitOrder(_symbol, -100, security.Price * 0.99m, (security.Price * 0.99m) - LimitOffset, + TrailingAmount, false, LimitOffset); + } + } + else if (_sellOrderTicket.Status != OrderStatus.Filled) + { + var stopPrice = _sellOrderTicket.Get(OrderField.StopPrice); + var limitPrice = _sellOrderTicket.Get(OrderField.LimitPrice); + + // Get the previous bar to compare to the stop and limit prices, + // because stop and limit price update attempt with the current slice data happens after OnData. + var high = _previousSlice.QuoteBars.TryGetValue(_symbol, out var quoteBar) + ? quoteBar.Bid.High + : _previousSlice.Bars[_symbol].High; + + var stopPriceToMarketPriceDistance = high - stopPrice; + if (stopPriceToMarketPriceDistance > TrailingAmount) + { + throw new RegressionTestException($"StopPrice {stopPrice} should be within {TrailingAmount} of the previous high price {high} at all times."); + } + + var stopPriceToLimitPriceDistance = stopPrice - limitPrice; + if (stopPriceToLimitPriceDistance != LimitOffset) + { + throw new RegressionTestException($"LimitPrice {limitPrice} should be {LimitOffset} from the stop price {stopPrice} at all times."); + } + } + + _previousSlice = slice; + } + + public override void OnOrderEvent(OrderEvent orderEvent) + { + if (orderEvent.Status == OrderStatus.Filled) + { + var order = Transactions.GetOrderById(orderEvent.OrderId); + if (!((TrailingStopLimitOrder)order).StopTriggered) + { + throw new RegressionTestException("TrailingStopLimitOrder StopTriggered should have been set if the order filled."); + } + + if (orderEvent.Direction == OrderDirection.Buy) + { + var limitPrice = _buyOrderTicket.Get(OrderField.LimitPrice); + if (orderEvent.FillPrice > limitPrice) + { + throw new RegressionTestException($@"Buy trailing stop limit order should have filled with price less than or equal to the limit price {limitPrice}. Fill price: {orderEvent.FillPrice}"); + } + } + else + { + var limitPrice = _sellOrderTicket.Get(OrderField.LimitPrice); + if (orderEvent.FillPrice < limitPrice) + { + throw new RegressionTestException($@"Sell trailing stop limit order should have filled with price greater than or equal to the limit price {limitPrice}. Fill price: {orderEvent.FillPrice}"); + } + } + } + } + + public override void OnEndOfAlgorithm() + { + if (_buyOrderTicket == null || _sellOrderTicket == null) + { + throw new RegressionTestException("Expected two orders (buy and sell) to have been filled at the end of the algorithm."); + } + + if (_buyOrderTicket.Status != OrderStatus.Filled || _sellOrderTicket.Status != OrderStatus.Filled) + { + throw new RegressionTestException("Expected the two orders (buy and sell) to have been filled at the end of the algorithm."); + } + } + + /// + /// 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 { get; } = 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 => 8061; + + /// + /// 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", "2"}, + {"Average Win", "2.59%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0.641%"}, + {"Drawdown", "1.400%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "102587.73"}, + {"Net Profit", "2.588%"}, + {"Sharpe Ratio", "-0.424"}, + {"Sortino Ratio", "-0.281"}, + {"Probabilistic Sharpe Ratio", "12.205%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "100%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "-0.008"}, + {"Beta", "0.044"}, + {"Annual Standard Deviation", "0.009"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-0.954"}, + {"Tracking Error", "0.103"}, + {"Treynor Ratio", "-0.085"}, + {"Total Fees", "$2.00"}, + {"Estimated Strategy Capacity", "$3800000000.00"}, + {"Lowest Capacity Asset", "SPY R735QTJ8XC9X"}, + {"Portfolio Turnover", "0.02%"}, + {"OrderListHash", "977baf60d0a4640106bd9a0f57e73a3a"} + }; + } +} diff --git a/Algorithm.CSharp/UpdateOrderRegressionAlgorithm.cs b/Algorithm.CSharp/UpdateOrderRegressionAlgorithm.cs index b720d54dab5c..425d031876f9 100644 --- a/Algorithm.CSharp/UpdateOrderRegressionAlgorithm.cs +++ b/Algorithm.CSharp/UpdateOrderRegressionAlgorithm.cs @@ -123,8 +123,10 @@ public override void OnData(Slice slice) Log("TICKET:: " + ticket); ticket.Update(new UpdateOrderFields { - LimitPrice = Security.Price*(1 - Math.Sign(ticket.Quantity)*LimitPercentageDelta), - StopPrice = ticket.OrderType != OrderType.TrailingStop + LimitPrice = ticket.OrderType != OrderType.TrailingStopLimit + ? Security.Price*(1 - Math.Sign(ticket.Quantity)*LimitPercentageDelta) + : null, + StopPrice = (ticket.OrderType != OrderType.TrailingStop && ticket.OrderType != OrderType.TrailingStopLimit) ? Security.Price*(1 + Math.Sign(ticket.Quantity)*StopPercentageDelta) : null, Tag = "Change prices: " + Time.Day @@ -215,31 +217,31 @@ public override void OnOrderEvent(OrderEvent orderEvent) { {"Total Orders", "24"}, {"Average Win", "0%"}, - {"Average Loss", "-2.00%"}, - {"Compounding Annual Return", "-15.280%"}, - {"Drawdown", "30.100%"}, + {"Average Loss", "-2.17%"}, + {"Compounding Annual Return", "-14.133%"}, + {"Drawdown", "28.500%"}, {"Expectancy", "-1"}, {"Start Equity", "100000"}, - {"End Equity", "71786.23"}, - {"Net Profit", "-28.214%"}, - {"Sharpe Ratio", "-1.107"}, - {"Sortino Ratio", "-1.357"}, - {"Probabilistic Sharpe Ratio", "0.024%"}, + {"End Equity", "73741.52"}, + {"Net Profit", "-26.258%"}, + {"Sharpe Ratio", "-1.072"}, + {"Sortino Ratio", "-1.232"}, + {"Probabilistic Sharpe Ratio", "0.027%"}, {"Loss Rate", "100%"}, {"Win Rate", "0%"}, {"Profit-Loss Ratio", "0"}, - {"Alpha", "0.03"}, - {"Beta", "-0.952"}, - {"Annual Standard Deviation", "0.1"}, - {"Annual Variance", "0.01"}, - {"Information Ratio", "-1.375"}, - {"Tracking Error", "0.189"}, - {"Treynor Ratio", "0.117"}, - {"Total Fees", "$20.00"}, - {"Estimated Strategy Capacity", "$1000000000.00"}, + {"Alpha", "0.031"}, + {"Beta", "-0.906"}, + {"Annual Standard Deviation", "0.096"}, + {"Annual Variance", "0.009"}, + {"Information Ratio", "-1.364"}, + {"Tracking Error", "0.184"}, + {"Treynor Ratio", "0.114"}, + {"Total Fees", "$21.00"}, + {"Estimated Strategy Capacity", "$750000000.00"}, {"Lowest Capacity Asset", "SPY R735QTJ8XC9X"}, - {"Portfolio Turnover", "0.50%"}, - {"OrderListHash", "a6482ce8abd669338eaced3104226c1b"} + {"Portfolio Turnover", "0.52%"}, + {"OrderListHash", "f2371f5962b956c9d102b95263702242"} }; } } diff --git a/Algorithm.Python/OrderTicketDemoAlgorithm.py b/Algorithm.Python/OrderTicketDemoAlgorithm.py index 6c75314e86f9..89c7860bd7d7 100644 --- a/Algorithm.Python/OrderTicketDemoAlgorithm.py +++ b/Algorithm.Python/OrderTicketDemoAlgorithm.py @@ -39,7 +39,7 @@ def initialize(self): self.__open_stop_market_orders = [] self.__open_stop_limit_orders = [] self.__open_trailing_stop_orders = [] - + self.__open_trailing_stop_limit_orders = [] def on_data(self, data): '''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.''' @@ -64,6 +64,8 @@ def on_data(self, data): # MARKET ON CLOSE ORDERS self.market_on_close_orders() + # TRAILING STOP LIMIT ORDERS + self.trailing_stop_limit_orders() def market_orders(self): ''' MarketOrders are the only orders that are processed synchronously by default, so @@ -320,6 +322,75 @@ def trailing_stop_orders(self): update_order_fields.tag = "Update #{0}".format(len(short_order.update_requests) + 1) short_order.update(update_order_fields) + def trailing_stop_limit_orders(self): + '''TrailingStopLimitOrders work the same way as StopLimitOrders, except + their stop price is adjusted to a certain amount, keeping it a certain + fixed distance from/to the market price, depending on the order direction. + The limit price adjusts based on a limit offset compared to the stop price. + You can submit requests to update or cancel the StopLimitOrder at any time. + The stop price, trailing amount, limit price and limit offset for an order + can be retrieved from the ticket using the OrderTicket.Get(OrderField) method, for example: + Code: + current_stop_price = order_ticket.get(OrderField.STOP_PRICE); + trailing_amount = order_ticket.get(OrderField.TRAILING_AMOUNT); + current_limit_price = order_ticket.get(OrderField.LIMIT_PRICE); + limit_offset = order_ticket.get(OrderField.LIMIT_OFFSET)''' + if self.time_is(7, 12, 0): + self.log("Submitting TrailingStopLimitOrder") + + # a long stop is triggered when the price rises above the value + # so we'll set a long stop .25% above the current bar's close + + close = self.securities[self.spy.value].close + stop_price = close * 1.0025 + limit_price = stop_price + 0.1 + new_ticket = self.trailing_stop_limit_order(self.spy.value, 10, stop_price, limit_price, + trailing_amount=0.0025, trailing_as_percentage=True, limit_offset=0.1) + self.__open_trailing_stop_limit_orders.append(new_ticket) + + + # a short stop is triggered when the price falls below the value + # so we'll set a short stop .25% below the current bar's close + + stop_price = close * 0.9975; + limit_price = stop_price - 0.1; + new_ticket = self.trailing_stop_limit_order(self.spy.value, -10, stop_price, limit_price, + trailing_amount=0.0025, trailing_as_percentage=True, limit_offset=0.1); + self.__open_trailing_stop_limit_orders.append(new_ticket) + + # when we submitted new trailing stop limit orders we placed them into this list, + # so while there's two entries they're still open and need processing + elif len(self.__open_trailing_stop_limit_orders) == 2: + + # check if either is filled and cancel the other + long_order = self.__open_trailing_stop_limit_orders[0] + short_order = self.__open_trailing_stop_limit_orders[1] + if self.check_pair_orders_for_fills(long_order, short_order): + self.__open_trailing_stop_limit_orders = [] + return + + # if neither order has filled in the last 5 minutes, bring in the trailing percentage by 0.01% + if ((self.utc_time - long_order.time).total_seconds() / 60) % 5 != 0: + return + + long_trailing_percentage = long_order.get(OrderField.TRAILING_AMOUNT) + new_long_trailing_percentage = max(long_trailing_percentage - 0.0001, 0.0001) + short_trailing_percentage = short_order.get(OrderField.TRAILING_AMOUNT) + new_short_trailing_percentage = max(short_trailing_percentage - 0.0001, 0.0001) + self.log(self.log("Updating trailing percentages - Long: {0:.3f} Short: {1:.3f}".format(new_long_trailing_percentage, new_short_trailing_percentage))) + + update_order_fields = UpdateOrderFields() + # we could change the quantity, but need to specify it + #Quantity = + update_order_fields.trailing_amount = new_long_trailing_percentage + update_order_fields.tag = "Update #{0}".format(len(long_order.update_requests) + 1) + long_order.update(update_order_fields) + + update_order_fields = UpdateOrderFields() + update_order_fields.trailing_amount = new_short_trailing_percentage + update_order_fields.tag = "Update #{0}".format(len(short_order.update_requests) + 1) + short_order.update(update_order_fields) + def market_on_close_orders(self): '''MarketOnCloseOrders are always executed at the next market's closing price. @@ -439,7 +510,7 @@ def on_end_of_algorithm(self): order_tickets_size = sum(1 for ticket in order_tickets) open_order_tickets_size = sum(1 for ticket in open_order_tickets) - assert(filled_orders_size == 9 and order_tickets_size == 12), "There were expected 9 filled orders and 12 order tickets" + assert(filled_orders_size == 10 and order_tickets_size == 14), "There were expected 10 filled orders and 14 order tickets" assert(not (len(open_orders) or open_order_tickets_size)), "No open orders or tickets were expected" assert(not remaining_open_orders), "No remaining quantity to be filled from open orders was expected" @@ -461,6 +532,6 @@ def on_end_of_algorithm(self): default_order_tickets_size = sum(1 for ticket in default_order_tickets) default_open_order_tickets_size = sum(1 for ticket in default_open_order_tickets) - assert(default_orders_size == 12 and default_order_tickets_size == 12), "There were expected 12 orders and 12 order tickets" + assert(default_orders_size == 14 and default_order_tickets_size == 14), "There were expected 14 orders and 14 order tickets" assert(not (len(default_open_orders) or default_open_order_tickets_size)), "No open orders or tickets were expected" assert(not default_open_orders_remaining), "No remaining quantity to be filled from open orders was expected" diff --git a/Algorithm.Python/TrailingStopLimitOrderRegressionAlgorithm.py b/Algorithm.Python/TrailingStopLimitOrderRegressionAlgorithm.py new file mode 100644 index 000000000000..05148b3c664e --- /dev/null +++ b/Algorithm.Python/TrailingStopLimitOrderRegressionAlgorithm.py @@ -0,0 +1,122 @@ +# 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 * + +### +### Basic algorithm demonstrating how to place trailing stop limit orders. +### +### +### +### +class TrailingStopLimitOrderRegressionAlgorithm(QCAlgorithm): + '''Basic algorithm demonstrating how to place trailing stop limit orders.''' + + tolerance = 0.001 + fast_period = 30 + slow_period = 60 + trailing_amount = 5 + limit_offset = 1 + + def initialize(self): + self.set_start_date(2013, 1, 1) + self.set_end_date(2017, 1, 1) + self.set_cash(100000) + + self._symbol = self.add_equity("SPY", Resolution.DAILY).symbol + + self._fast = self.ema(self._symbol, self.fast_period, Resolution.DAILY) + self._slow = self.ema(self._symbol, self.slow_period, Resolution.DAILY) + + self._buy_order_ticket: OrderTicket = None + self._sell_order_ticket: OrderTicket = None + self._previous_slice: Slice = None + + def on_data(self, slice: Slice): + if not slice.contains_key(self._symbol): + return + + if not self.is_ready(): + return + + security = self.securities[self._symbol] + + if self._buy_order_ticket is None: + if self.trend_is_up(): + self._buy_order_ticket = self.trailing_stop_limit_order(self._symbol, 100, security.price * 1.10, (security.price * 1.10) + self.limit_offset, + self.trailing_amount, False, self.limit_offset) + elif self._buy_order_ticket.status != OrderStatus.FILLED: + stop_price = self._buy_order_ticket.get(OrderField.STOP_PRICE) + limit_price = self._buy_order_ticket.get(OrderField.LIMIT_PRICE) + + # Get the previous bar to compare to the stop and limit prices, + # because stop and limit price update attempt with the current slice data happens after OnData. + low = self._previous_slice.quote_bars[self._symbol].ask.low if self._previous_slice.quote_bars.contains_key(self._symbol) \ + else self._previous_slice.bars[self._symbol].low + + stop_price_to_market_price_distance = stop_price - low + if stop_price_to_market_price_distance > self.trailing_amount: + raise Exception(f"StopPrice {stop_price} should be within {self.trailing_amount} of the previous low price {low} at all times.") + + stop_price_to_limit_price_distance = limit_price - stop_price + if stop_price_to_limit_price_distance != self.limit_offset: + raise Exception(f"LimitPrice {limit_price} should be {self.limit_offset} from the stop price {stop_price} at all times.") + + elif self._sell_order_ticket is None: + if self.trend_is_down(): + self._sell_order_ticket = self.trailing_stop_limit_order(self._symbol, -100, security.price * 0.99, (security.price * 0.99) - self.limit_offset, + self.trailing_amount, False, self.limit_offset) + elif self._sell_order_ticket.status != OrderStatus.FILLED: + stop_price = self._sell_order_ticket.get(OrderField.STOP_PRICE) + limit_price = self._sell_order_ticket.get(OrderField.LIMIT_PRICE) + + # Get the previous bar to compare to the stop and limit prices, + # because stop and limit price update attempt with the current slice data happens after OnData. + high = self._previous_slice.quote_bars[self._symbol].bid.high if self._previous_slice.quote_bars.contains_key(self._symbol) \ + else self._previous_slice.bars[self._symbol].high + + stop_price_to_market_price_distance = high - stop_price + if stop_price_to_market_price_distance > self.trailing_amount: + raise Exception(f"StopPrice {stop_price} should be within {self.sell_trailing_amount} of the previous high price {high} at all times.") + + stop_price_to_limit_price_distance = stop_price - limit_price + if stop_price_to_limit_price_distance != self.limit_offset: + raise Exception(f"LimitPrice {limit_price} should be {self.limit_offset} from the stop price {stop_price} at all times.") + + self._previous_slice = slice + + def on_order_event(self, order_event: OrderEvent): + if order_event.status == OrderStatus.FILLED: + order: TrailingStopLimitOrder = self.transactions.get_order_by_id(order_event.order_id) + if not order.stop_triggered: + raise Exception("TrailingStopLimitOrder StopTriggered should haven been set if the order filled.") + + if order_event.direction == OrderDirection.BUY: + limit_price = self._buy_order_ticket.get(OrderField.LIMIT_PRICE) + if order_event.fill_price > limit_price: + raise Exception(f"Buy stop limit order should have filled with price less than or equal to the limit price {limit_price}. " + f"Fill price: {order_event.fill_price}") + else: + limit_price = self._sell_order_ticket.get(OrderField.LIMIT_PRICE) + if order_event.fill_price < limit_price: + raise Exception(f"Sell stop limit order should have filled with price greater than or equal to the limit price {limit_price}. " + f"Fill price: {order_event.fill_price}") + + def is_ready(self): + return self._fast.is_ready and self._slow.is_ready + + def trend_is_up(self): + return self.is_ready() and self._fast.current.value > self._slow.current.value * (1 + self.tolerance) + + def trend_is_down(self): + return self.is_ready() and self._fast.current.value < self._slow.current.value * (1 + self.tolerance) diff --git a/Algorithm.Python/UpdateOrderRegressionAlgorithm.py b/Algorithm.Python/UpdateOrderRegressionAlgorithm.py index 7092c1f216e0..b2829edb178a 100644 --- a/Algorithm.Python/UpdateOrderRegressionAlgorithm.py +++ b/Algorithm.Python/UpdateOrderRegressionAlgorithm.py @@ -39,7 +39,9 @@ def initialize(self): self.limit_percentage = 0.025 self.limit_percentage_delta = 0.005 - order_type_enum = [OrderType.MARKET, OrderType.LIMIT, OrderType.STOP_MARKET, OrderType.STOP_LIMIT, OrderType.MARKET_ON_OPEN, OrderType.MARKET_ON_CLOSE, OrderType.TRAILING_STOP] + order_type_enum = [ + OrderType.MARKET, OrderType.LIMIT, OrderType.STOP_MARKET, OrderType.STOP_LIMIT, OrderType.MARKET_ON_OPEN, + OrderType.MARKET_ON_CLOSE, OrderType.TRAILING_STOP, OrderType.TRAILING_STOP_LIMIT] self.order_types_queue = CircularQueue[OrderType](order_type_enum) self.order_types_queue.circle_completed += self.on_circle_completed self.tickets = [] @@ -79,7 +81,7 @@ def on_data(self, data): ticket = self.tickets[-1] if self.time.day > 8 and self.time.day < 14: - if len(ticket.update_requests) == 0 and ticket.status is not OrderStatus.FILLED: + if len(ticket.update_requests) == 0 and self.is_open(ticket): self.log("TICKET:: {0}".format(ticket)) update_order_fields = UpdateOrderFields() update_order_fields.quantity = ticket.quantity + copysign(self.delta_quantity, self.quantity) @@ -87,15 +89,17 @@ def on_data(self, data): ticket.update(update_order_fields) elif self.time.day > 13 and self.time.day < 20: - if len(ticket.update_requests) == 1 and ticket.status is not OrderStatus.FILLED: + if len(ticket.update_requests) == 1 and self.is_open(ticket): self.log("TICKET:: {0}".format(ticket)) update_order_fields = UpdateOrderFields() - update_order_fields.limit_price = self.security.price*(1 - copysign(self.limit_percentage_delta, ticket.quantity)) - update_order_fields.stop_price = self.security.price*(1 + copysign(self.stop_percentage_delta, ticket.quantity)) if ticket.order_type != OrderType.TRAILING_STOP else None + update_order_fields.limit_price = self.security.price*(1 - copysign(self.limit_percentage_delta, ticket.quantity)) \ + if ticket.order_type != OrderType.TRAILING_STOP_LIMIT else None + update_order_fields.stop_price = self.security.price*(1 + copysign(self.stop_percentage_delta, ticket.quantity)) \ + if ticket.order_type not in (OrderType.TRAILING_STOP, OrderType.TRAILING_STOP_LIMIT) else None update_order_fields.tag = "Change prices: {0}".format(self.time.day) ticket.update(update_order_fields) else: - if len(ticket.update_requests) == 2 and ticket.status is not OrderStatus.FILLED: + if len(ticket.update_requests) == 2 and self.is_open(ticket): self.log("TICKET:: {0}".format(ticket)) ticket.cancel("{0} and is still open!".format(self.time.day)) self.log("CANCELLED:: {0}".format(ticket.cancel_request)) @@ -122,3 +126,6 @@ def on_order_event(self, orderEvent): else: self.log(orderEvent.to_string()) self.log("TICKET:: {0}".format(ticket)) + + def is_open(self, order_ticket): + return order_ticket.status not in (OrderStatus.FILLED, OrderStatus.CANCELED, OrderStatus.INVALID) diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index 1ef0ba2c53e5..9c31b74a79f2 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -649,6 +649,157 @@ public OrderTicket StopLimitOrder(Symbol symbol, decimal quantity, decimal stopP return SubmitOrderRequest(request); } + /// + /// Send a trailing stop limit order to the transaction handler. The newly created order id will be negative if the order is invalid. + /// It will calculate the stop price and limit price using the trailing amount, the limit offset and the current market price. + /// + /// Trading asset symbol + /// Quantity to be traded + /// The trailing amount to be used to update the stop price + /// Whether the is a percentage or an absolute currency value + /// The offset to be used to update the limit price + /// Optional string data tag for the order + /// The order properties to use. Defaults to + /// The order ticket instance. + [DocumentationAttribute(TradingAndOrders)] + public OrderTicket TrailingStopLimitOrder(Symbol symbol, int quantity, decimal trailingAmount, bool trailingAsPercentage, + decimal limitOffset, string tag = "", IOrderProperties orderProperties = null) + { + return TrailingStopLimitOrder(symbol, (decimal)quantity, trailingAmount, trailingAsPercentage, + limitOffset, tag, orderProperties); + } + + /// + /// Send a trailing stop limit order to the transaction handler. The newly created order id will be negative if the order is invalid. + /// It will calculate the stop price and limit price using the trailing amount, the limit offset and the current market price. + /// + /// Trading asset symbol + /// Quantity to be traded + /// The trailing amount to be used to update the stop price + /// Whether the is a percentage or an absolute currency value + /// The offset to be used to update the limit price + /// Optional string data tag for the order + /// The order properties to use. Defaults to + /// The order ticket instance. + [DocumentationAttribute(TradingAndOrders)] + public OrderTicket TrailingStopLimitOrder(Symbol symbol, double quantity, decimal trailingAmount, bool trailingAsPercentage, + decimal limitOffset, string tag = "", IOrderProperties orderProperties = null) + { + return TrailingStopLimitOrder(symbol, quantity.SafeDecimalCast(), trailingAmount, trailingAsPercentage, + limitOffset, tag, orderProperties); + } + + /// + /// Send a trailing stop limit order to the transaction handler. The newly created order id will be negative if the order is invalid. + /// It will calculate the stop price and limit price using the trailing amount, the limit offset and the current market price. + /// + /// Trading asset symbol + /// Quantity to be traded + /// The trailing amount to be used to update the stop price + /// Whether the is a percentage or an absolute currency value + /// The offset to be used to update the limit price + /// Optional string data tag for the order + /// The order properties to use. Defaults to + /// The order ticket instance. + [DocumentationAttribute(TradingAndOrders)] + public OrderTicket TrailingStopLimitOrder(Symbol symbol, decimal quantity, decimal trailingAmount, bool trailingAsPercentage, + decimal limitOffset, string tag = "", IOrderProperties orderProperties = null) + { + var security = Securities[symbol]; + var direction = quantity > 0 ? OrderDirection.Buy : OrderDirection.Sell; + var stopPrice = Orders.TrailingStopLimitOrder.CalculateStopPrice(security.Price, trailingAmount, trailingAsPercentage, direction); + var limitPrice = Orders.TrailingStopLimitOrder.CalculateLimitPrice(stopPrice, limitOffset, direction); + + return TrailingStopLimitOrder(symbol, quantity, stopPrice, limitPrice, trailingAmount, trailingAsPercentage, + limitOffset, tag, orderProperties); + } + + /// + /// Send a trailing stop limit order to the transaction handler. The newly created order id will be negative if the order is invalid. + /// + /// Trading asset symbol + /// Quantity to be traded + /// + /// + /// The trailing amount to be used to update the stop price + /// Whether the is a percentage or an absolute currency value + /// The offset to be used to update the limit price + /// Optional string data tag for the order + /// The order properties to use. Defaults to + /// The order ticket instance. + [DocumentationAttribute(TradingAndOrders)] + public OrderTicket TrailingStopLimitOrder(Symbol symbol, int quantity, decimal stopPrice, decimal limitPrice, decimal trailingAmount, + bool trailingAsPercentage, decimal limitOffset, string tag = "", IOrderProperties orderProperties = null) + { + return TrailingStopLimitOrder(symbol, (decimal)quantity, stopPrice, limitPrice, trailingAmount, trailingAsPercentage, limitOffset, + tag, orderProperties); + } + + /// + /// Send a trailing stop limit order to the transaction handler. The newly created order id will be negative if the order is invalid. + /// + /// Trading asset symbol + /// Quantity to be traded + /// + /// + /// The trailing amount to be used to update the stop price + /// Whether the is a percentage or an absolute currency value + /// The offset to be used to update the limit price + /// Optional string data tag for the order + /// The order properties to use. Defaults to + /// The order ticket instance. + [DocumentationAttribute(TradingAndOrders)] + public OrderTicket TrailingStopLimitOrder(Symbol symbol, double quantity, decimal stopPrice, decimal limitPrice, decimal trailingAmount, + bool trailingAsPercentage, decimal limitOffset, string tag = "", IOrderProperties orderProperties = null) + { + return TrailingStopLimitOrder(symbol, quantity.SafeDecimalCast(), stopPrice, limitPrice, trailingAmount, trailingAsPercentage, + limitOffset, tag, orderProperties); + } + + /// + /// Send a trailing stop limit order to the transaction handler. The newly created order id will be negative if the order is invalid. + /// + /// Trading asset symbol + /// Quantity to be traded + /// + /// + /// The trailing amount to be used to update the stop price + /// Whether the is a percentage or an absolute currency value + /// The offset to be used to update the limit price + /// Optional string data tag for the order + /// The order properties to use. Defaults to + /// The order ticket instance. + [DocumentationAttribute(TradingAndOrders)] + public OrderTicket TrailingStopLimitOrder(Symbol symbol, decimal quantity, decimal stopPrice, decimal limitPrice, decimal trailingAmount, + bool trailingAsPercentage, decimal limitOffset, string tag = "", IOrderProperties orderProperties = null) + { + var security = Securities[symbol]; + var request = CreateSubmitOrderRequest( + orderType: OrderType.TrailingStopLimit, + security: security, + quantity: quantity, + tag: tag, + properties: orderProperties, + stopPrice: stopPrice, + limitPrice: limitPrice, + trailingAmount: trailingAmount, + trailingAsPercentage: trailingAsPercentage, + limitOffset: limitOffset); + + return SubmitOrderRequest(request); + } + + + + + + + + + + + + /// /// Send a limit if touched order to the transaction handler: /// @@ -1565,10 +1716,10 @@ public bool IsMarketOpen(Symbol symbol) private SubmitOrderRequest CreateSubmitOrderRequest(OrderType orderType, Security security, decimal quantity, string tag, IOrderProperties properties, decimal stopPrice = 0m, decimal limitPrice = 0m, decimal triggerPrice = 0m, decimal trailingAmount = 0m, - bool trailingAsPercentage = false, GroupOrderManager groupOrderManager = null) + bool trailingAsPercentage = false, decimal limitOffset = 0m, GroupOrderManager groupOrderManager = null) { return new SubmitOrderRequest(orderType, security.Type, security.Symbol, quantity, stopPrice, limitPrice, triggerPrice, trailingAmount, - trailingAsPercentage, UtcTime, tag, properties, groupOrderManager); + trailingAsPercentage, limitOffset, UtcTime, tag, properties, groupOrderManager); } private static void CheckComboOrderSizing(List legs, decimal quantity) diff --git a/Brokerages/Backtesting/BacktestingBrokerage.cs b/Brokerages/Backtesting/BacktestingBrokerage.cs index 105a9f68050b..ab53ddcdf7d4 100644 --- a/Brokerages/Backtesting/BacktestingBrokerage.cs +++ b/Brokerages/Backtesting/BacktestingBrokerage.cs @@ -446,6 +446,16 @@ private void OnOrderUpdated(Order order) case OrderType.StopLimit: OnOrderUpdated(new OrderUpdateEvent { OrderId = order.Id, StopTriggered = ((StopLimitOrder)order).StopTriggered }); break; + + case OrderType.TrailingStopLimit: + OnOrderUpdated(new OrderUpdateEvent + { + OrderId = order.Id, + TrailingStopPrice = ((TrailingStopLimitOrder)order).StopPrice, + StopTriggered = ((TrailingStopLimitOrder)order).StopTriggered, + LimitPrice = ((TrailingStopLimitOrder)order).LimitPrice, + }); + break; } } diff --git a/Common/Brokerages/DefaultBrokerageModel.cs b/Common/Brokerages/DefaultBrokerageModel.cs index f4a5b893f822..a308e769e700 100644 --- a/Common/Brokerages/DefaultBrokerageModel.cs +++ b/Common/Brokerages/DefaultBrokerageModel.cs @@ -160,7 +160,12 @@ public virtual void ApplySplit(List tickets, Split split) LimitPrice = ticket.OrderType.IsLimitOrder() ? ticket.Get(OrderField.LimitPrice)*splitFactor : (decimal?) null, StopPrice = ticket.OrderType.IsStopOrder() ? ticket.Get(OrderField.StopPrice)*splitFactor : (decimal?) null, TriggerPrice = ticket.OrderType == OrderType.LimitIfTouched ? ticket.Get(OrderField.TriggerPrice) * splitFactor : (decimal?) null, - TrailingAmount = ticket.OrderType == OrderType.TrailingStop && !ticket.Get(OrderField.TrailingAsPercentage) ? ticket.Get(OrderField.TrailingAmount) * splitFactor : (decimal?) null + TrailingAmount = + (ticket.OrderType == OrderType.TrailingStop || ticket.OrderType == OrderType.TrailingStopLimit) + && !ticket.Get(OrderField.TrailingAsPercentage) + ? ticket.Get(OrderField.TrailingAmount) * splitFactor + : (decimal?) null, + LimitOffset = ticket.OrderType == OrderType.TrailingStopLimit ? ticket.Get(OrderField.LimitOffset) * splitFactor : (decimal?) null, })); } diff --git a/Common/Brokerages/InteractiveBrokersBrokerageModel.cs b/Common/Brokerages/InteractiveBrokersBrokerageModel.cs index e92586801578..b5bda2402d43 100644 --- a/Common/Brokerages/InteractiveBrokersBrokerageModel.cs +++ b/Common/Brokerages/InteractiveBrokersBrokerageModel.cs @@ -68,7 +68,8 @@ public class InteractiveBrokersBrokerageModel : DefaultBrokerageModel OrderType.ComboMarket, OrderType.ComboLimit, OrderType.ComboLegLimit, - OrderType.OptionExercise + OrderType.OptionExercise, + OrderType.TrailingStopLimit }; /// diff --git a/Common/Extensions.cs b/Common/Extensions.cs index 8a2f2cd443c1..4508eb24bdff 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -535,6 +535,14 @@ public static string GetHash(this IDictionary orders) { trailingStop.TrailingAmount = trailingStop.TrailingAmount.SmartRounding(); } + var trailingStopLimit = order as TrailingStopLimitOrder; + if (trailingStopLimit != null) + { + trailingStopLimit.StopPrice = trailingStopLimit.StopPrice.SmartRounding(); + trailingStopLimit.LimitPrice = trailingStopLimit.LimitPrice.SmartRounding(); + trailingStopLimit.TrailingAmount = trailingStopLimit.TrailingAmount.SmartRounding(); + trailingStopLimit.LimitOffset = trailingStopLimit.LimitOffset.SmartRounding(); + } var stopMarket = order as StopMarketOrder; if (stopMarket != null) { @@ -2617,6 +2625,7 @@ public static OrderTicket ToOrderTicket(this Order order, SecurityTransactionMan var triggerPrice = 0m; var trailingAmount = 0m; var trailingAsPercentage = false; + var limitOffset = 0m; switch (order.Type) { @@ -2639,6 +2648,14 @@ public static OrderTicket ToOrderTicket(this Order order, SecurityTransactionMan trailingAmount = trailingStopOrder.TrailingAmount; trailingAsPercentage = trailingStopOrder.TrailingAsPercentage; break; + case OrderType.TrailingStopLimit: + var trailingStopLimitOrder = order as TrailingStopLimitOrder; + stopPrice = trailingStopLimitOrder.StopPrice; + limitPrice = trailingStopLimitOrder.LimitPrice; + trailingAmount = trailingStopLimitOrder.TrailingAmount; + trailingAsPercentage = trailingStopLimitOrder.TrailingAsPercentage; + limitOffset = trailingStopLimitOrder.LimitOffset; + break; case OrderType.LimitIfTouched: var limitIfTouched = order as LimitIfTouchedOrder; triggerPrice = limitIfTouched.TriggerPrice; @@ -2672,6 +2689,7 @@ public static OrderTicket ToOrderTicket(this Order order, SecurityTransactionMan triggerPrice, trailingAmount, trailingAsPercentage, + limitOffset, order.Time, order.Tag, order.Properties, diff --git a/Common/Messages/Messages.Orders.cs b/Common/Messages/Messages.Orders.cs index 1562593b46ce..171894ee9e70 100644 --- a/Common/Messages/Messages.Orders.cs +++ b/Common/Messages/Messages.Orders.cs @@ -173,6 +173,12 @@ public static string ToString(Orders.OrderEvent orderEvent) message += $" TrailingAmount: {trailingAmountString}"; } + if (orderEvent.LimitOffset.HasValue) + { + var limitOffsetString = TrailingStopLimitOrder.LimitOffset(orderEvent.LimitOffset.Value, currencySymbol); + message += $" LimitOffset: {limitOffsetString}"; + } + if (orderEvent.TriggerPrice.HasValue) { message += Invariant($" TriggerPrice: {currencySymbol}{orderEvent.TriggerPrice.Value.SmartRounding()}"); @@ -229,6 +235,12 @@ public static string ShortToString(Orders.OrderEvent orderEvent) message += $" TA: {trailingAmountString}"; } + if (orderEvent.LimitOffset.HasValue) + { + var limitOffsetString = TrailingStopLimitOrder.LimitOffset(orderEvent.LimitOffset.Value, currencySymbol); + message += $" LimitOffset: {limitOffsetString}"; + } + if (orderEvent.TriggerPrice.HasValue) { message += Invariant($" TP:{currencySymbol}{orderEvent.TriggerPrice.Value.SmartRounding()}"); @@ -514,6 +526,89 @@ private static string TrailingAmountImpl(Orders.TrailingStopOrder order, string } } + /// + /// Provides user-facing messages for the class and its consumers or related classes + /// + public static class TrailingStopLimitOrder + { + /// + /// Returns a tag message for the given TrailingStopLimitOrder + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Tag(Orders.TrailingStopLimitOrder order) + { + return Invariant($"Trailing Amount: {TrailingAmount(order)}. Limit Offset: {LimitOffset(order)}."); + } + + /// + /// Parses a TrailingStopLimitOrder into a string + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ToString(Orders.TrailingStopLimitOrder order) + { + var currencySymbol = QuantConnect.Currencies.GetCurrencySymbol(order.PriceCurrency); + return Invariant($@"{Order.ToString(order)} at stop {currencySymbol}{order.StopPrice.SmartRounding()} and limit { + currencySymbol}{order.LimitPrice.SmartRounding()}. Trailing amount: { + TrailingAmount(order, currencySymbol)}. Limit offset: {LimitOffset(order, currencySymbol)}."); + } + + /// + /// Returns a TrailingAmount string representation for the given TrailingStopLimitOrder + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string TrailingAmount(Orders.TrailingStopLimitOrder order) + { + var currencySymbol = QuantConnect.Currencies.GetCurrencySymbol(order.PriceCurrency); + return TrailingAmount(order.TrailingAmount, order.TrailingAsPercentage, currencySymbol); + } + + /// + /// Returns a TrailingAmount string representation for the given TrailingStopLimitOrder + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string TrailingAmount(Orders.TrailingStopLimitOrder order, string priceCurrency) + { + return TrailingAmount(order.TrailingAmount, order.TrailingAsPercentage, priceCurrency); + } + + /// + /// Returns a message for the given TrailingAmount and PriceCurrency values taking into account if the trailing is as percentage + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string TrailingAmount(decimal trailingAmount, bool trailingAsPercentage, string priceCurrency) + { + return trailingAsPercentage ? Invariant($"{trailingAmount * 100}%") : Invariant($"{priceCurrency}{trailingAmount}"); + } + + /// + /// Returns a LimitOffset string representation for the given TrailingStopLimitOrder + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string LimitOffset(Orders.TrailingStopLimitOrder order) + { + var currencySymbol = QuantConnect.Currencies.GetCurrencySymbol(order.PriceCurrency); + return LimitOffset(order, currencySymbol); + } + + /// + /// Returns a LimitOffset string representation for the given TrailingStopLimitOrder + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string LimitOffset(Orders.TrailingStopLimitOrder order, string priceCurrency) + { + return LimitOffset(order.LimitOffset, priceCurrency); + } + + /// + /// Returns a LimitOffset string representation for the given TrailingStopLimitOrder + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string LimitOffset(decimal limitOffset, string priceCurrency) + { + return Invariant($"{priceCurrency}{limitOffset}"); + } + } + /// /// Provides user-facing messages for the class and its consumers or related classes /// @@ -530,7 +625,7 @@ public static string ToString(Orders.SubmitOrderRequest request) return Invariant($"{request.Time} UTC: Submit Order: ({request.OrderId}) - {proxy} {request.Tag} Status: {request.Status}"); } } - + /// /// Provides user-facing messages for the class and its consumers or related classes /// @@ -563,6 +658,10 @@ public static string ToString(Orders.UpdateOrderRequest request) { updates.Add(Invariant($"TriggerPrice: {request.TriggerPrice.Value.SmartRounding()}")); } + if (request.LimitOffset.HasValue) + { + updates.Add(Invariant($"LimitOffset: {request.LimitOffset.Value.SmartRounding()}")); + } return Invariant($@"{request.Time} UTC: Update Order: ({request.OrderId}) - {string.Join(", ", updates)} { request.Tag} Status: {request.Status}"); diff --git a/Common/Orders/Fills/EquityFillModel.cs b/Common/Orders/Fills/EquityFillModel.cs index 0315159ccde2..29db1d2f79a0 100644 --- a/Common/Orders/Fills/EquityFillModel.cs +++ b/Common/Orders/Fills/EquityFillModel.cs @@ -16,7 +16,6 @@ using System; using System.Collections.Generic; using System.Linq; - using QuantConnect.Data; using QuantConnect.Data.Auxiliary; using QuantConnect.Data.Market; @@ -332,6 +331,112 @@ public override OrderEvent StopLimitFill(Security asset, StopLimitOrder order) return fill; } + /// + /// Trailing stop limit fill model implementation for equity. (Trailing Stop Limit Order Type) + /// + /// Security asset we're filling + /// Order packet to model + /// Order fill information detailing the average price and quantity filled. + /// + /// There is no good way to model limit orders with OHLC because we never know whether the market has + /// gapped past our fill price. We have to make the assumption of a fluid, high volume market. + /// + /// Stop limit orders we also can't be sure of the order of the H - L values for the limit fill. The assumption + /// was made the limit fill will be done with closing price of the bar after the stop has been triggered.. + /// + public override OrderEvent TrailingStopLimitFill(Security asset, TrailingStopLimitOrder order) + { + //Default order event to return. + var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone); + var fill = new OrderEvent(order, utcTime, OrderFee.Zero); + + //If its cancelled don't need anymore checks: + if (order.Status == OrderStatus.Canceled) return fill; + + // make sure the exchange is open before filling -- allow pre/post market fills to occur + if (!IsExchangeOpen( + asset, + Parameters.ConfigProvider + .GetSubscriptionDataConfigs(asset.Symbol) + .IsExtendedMarketHours())) + { + return fill; + } + + //Get the range of prices in the last bar: + var prices = GetPricesCheckingPythonWrapper(asset, order.Direction); + var pricesEndTime = prices.EndTime.ConvertToUtc(asset.Exchange.TimeZone); + + // do not fill on stale data + if (pricesEndTime <= order.Time) return fill; + + //Check if the Stop Order was filled: opposite to a limit order + switch (order.Direction) + { + case OrderDirection.Buy: + //-> 1.2 Buy Stop: If Price Above Setpoint, Buy: + if (prices.High > order.StopPrice || order.StopTriggered) + { + if (!order.StopTriggered) + { + order.StopTriggered = true; + Parameters.OnOrderUpdated(order); + } + + // Fill the limit order, using high price of bar: + // Note > Can't use minimum price, because no way to be sure minimum wasn't before the stop triggered. + if (prices.Current < order.LimitPrice) + { + fill.Status = OrderStatus.Filled; + fill.FillPrice = Math.Min(prices.High, order.LimitPrice); + // assume the order completely filled + fill.FillQuantity = order.Quantity; + } + } + break; + + case OrderDirection.Sell: + //-> 1.1 Sell Stop: If Price below setpoint, Sell: + if (prices.Low < order.StopPrice || order.StopTriggered) + { + if (!order.StopTriggered) + { + order.StopTriggered = true; + Parameters.OnOrderUpdated(order); + } + + // Fill the limit order, using low price of the bar + // Note > Can't use maximum price, because no way to be sure maximum wasn't before the stop triggered. + if (prices.Current > order.LimitPrice) + { + fill.Status = OrderStatus.Filled; + fill.FillPrice = Math.Max(prices.Low, order.LimitPrice); + // assume the order completely filled + fill.FillQuantity = order.Quantity; + } + } + break; + } + + // Update the stop and limit prices: + // NOTE: Doing this after attempting to fill the order in the following cases: + // - Sell: if low < stop price, order is filled. If we were to update the stop price before and it is moved towards the high price + // placing the stop price above the low price, it will not trigger a fill. + // - Buy: if high > stop price, order is filled. If we were to update the stop price before and it is moved towards the low price + // placing the stop price below the high price, it will not trigger a fill. + if (fill.Status != OrderStatus.Filled && + TrailingStopLimitOrder.TryUpdateStopAndLimitPrices(order.Direction == OrderDirection.Sell ? prices.High : prices.Low, order.StopPrice, + order.TrailingAmount, order.TrailingAsPercentage, order.LimitOffset, order.Direction, out var updatedStopPrice, out var updatedLimitPrice)) + { + order.StopPrice = updatedStopPrice; + order.LimitPrice = updatedLimitPrice; + Parameters.OnOrderUpdated(order); + } + + return fill; + } + + /// /// Limit fill model implementation for Equity. /// diff --git a/Common/Orders/Fills/FillModel.cs b/Common/Orders/Fills/FillModel.cs index 7abe1acbac19..262ae1c97b75 100644 --- a/Common/Orders/Fills/FillModel.cs +++ b/Common/Orders/Fills/FillModel.cs @@ -93,6 +93,11 @@ public virtual Fill Fill(FillModelParameters parameters) ? PythonWrapper.StopLimitFill(parameters.Security, parameters.Order as StopLimitOrder) : StopLimitFill(parameters.Security, parameters.Order as StopLimitOrder)); break; + case OrderType.TrailingStopLimit: + orderEvents.Add(PythonWrapper != null + ? PythonWrapper.TrailingStopLimitFill(parameters.Security, parameters.Order as TrailingStopLimitOrder) + : TrailingStopLimitFill(parameters.Security, parameters.Order as TrailingStopLimitOrder)); + break; case OrderType.MarketOnOpen: orderEvents.Add(PythonWrapper != null ? PythonWrapper.MarketOnOpenFill(parameters.Security, parameters.Order as MarketOnOpenOrder) @@ -541,6 +546,107 @@ public virtual OrderEvent StopLimitFill(Security asset, StopLimitOrder order) return fill; } + /// + /// Default trailing stop limit fill model implementation in base class security. (Trailing Stop Limit Order Type) + /// + /// Security asset we're filling + /// Order packet to model + /// Order fill information detailing the average price and quantity filled. + /// + /// There is no good way to model limit orders with OHLC because we never know whether the market has + /// gapped past our fill price. We have to make the assumption of a fluid, high volume market. + /// + /// Stop limit orders we also can't be sure of the order of the H - L values for the limit fill. The assumption + /// was made the limit fill will be done with closing price of the bar after the stop has been triggered.. + /// + public virtual OrderEvent TrailingStopLimitFill(Security asset, TrailingStopLimitOrder order) + { + //Default order event to return. + var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone); + var fill = new OrderEvent(order, utcTime, OrderFee.Zero); + + //If its cancelled don't need anymore checks: + if (order.Status == OrderStatus.Canceled) return fill; + + // make sure the exchange is open before filling -- allow pre/post market fills to occur + if (!IsExchangeOpen(asset)) + { + return fill; + } + + //Get the range of prices in the last bar: + var prices = GetPricesCheckingPythonWrapper(asset, order.Direction); + var pricesEndTime = prices.EndTime.ConvertToUtc(asset.Exchange.TimeZone); + + // do not fill on stale data + if (pricesEndTime <= order.Time) return fill; + + //Check if the Stop Order was filled: opposite to a limit order + switch (order.Direction) + { + case OrderDirection.Buy: + //-> 1.2 Buy Stop: If Price Above Setpoint, Buy: + if (prices.High > order.StopPrice || order.StopTriggered) + { + if (!order.StopTriggered) + { + order.StopTriggered = true; + Parameters.OnOrderUpdated(order); + } + + // Fill the limit order, using closing price of bar: + // Note > Can't use minimum price, because no way to be sure minimum wasn't before the stop triggered. + if (prices.Current < order.LimitPrice) + { + fill.Status = OrderStatus.Filled; + fill.FillPrice = Math.Min(prices.High, order.LimitPrice); + // assume the order completely filled + fill.FillQuantity = order.Quantity; + } + } + break; + + case OrderDirection.Sell: + //-> 1.1 Sell Stop: If Price below setpoint, Sell: + if (prices.Low < order.StopPrice || order.StopTriggered) + { + if (!order.StopTriggered) + { + order.StopTriggered = true; + Parameters.OnOrderUpdated(order); + } + + // Fill the limit order, using minimum price of the bar + // Note > Can't use minimum price, because no way to be sure minimum wasn't before the stop triggered. + if (prices.Current > order.LimitPrice) + { + fill.Status = OrderStatus.Filled; + fill.FillPrice = Math.Max(prices.Low, order.LimitPrice); + // assume the order completely filled + fill.FillQuantity = order.Quantity; + } + } + break; + } + + // Update the stop and limit prices: + // NOTE: Doing this after attempting to fill the order in the following cases: + // - Sell: if low < stop price, order is filled. If we were to update the stop price before and it is moved towards the high price + // placing the stop price above the low price, it will not trigger a fill. + // - Buy: if high > stop price, order is filled. If we were to update the stop price before and it is moved towards the low price + // placing the stop price below the high price, it will not trigger a fill. + if (fill.Status != OrderStatus.Filled && + TrailingStopLimitOrder.TryUpdateStopAndLimitPrices(order.Direction == OrderDirection.Sell ? prices.High : prices.Low, order.StopPrice, + order.TrailingAmount, order.TrailingAsPercentage, order.LimitOffset, order.Direction, out var updatedStopPrice, out var updatedLimitPrice)) + { + order.StopPrice = updatedStopPrice; + order.LimitPrice = updatedLimitPrice; + Parameters.OnOrderUpdated(order); + } + + return fill; + } + /// /// Default limit if touched fill model implementation in base class security. (Limit If Touched Order Type) /// diff --git a/Common/Orders/Order.cs b/Common/Orders/Order.cs index dfddcba0655c..815677eaadab 100644 --- a/Common/Orders/Order.cs +++ b/Common/Orders/Order.cs @@ -414,12 +414,12 @@ public static Order CreateOrder(SubmitOrderRequest request) { return CreateOrder(request.OrderId, request.OrderType, request.Symbol, request.Quantity, request.Time, request.Tag, request.OrderProperties, request.LimitPrice, request.StopPrice, request.TriggerPrice, request.TrailingAmount, - request.TrailingAsPercentage, request.GroupOrderManager); + request.TrailingAsPercentage, request.LimitOffset, request.GroupOrderManager); } private static Order CreateOrder(int orderId, OrderType type, Symbol symbol, decimal quantity, DateTime time, string tag, IOrderProperties properties, decimal limitPrice, decimal stopPrice, decimal triggerPrice, decimal trailingAmount, - bool trailingAsPercentage, GroupOrderManager groupOrderManager) + bool trailingAsPercentage, decimal limitOffset, GroupOrderManager groupOrderManager) { Order order; switch (type) @@ -444,6 +444,11 @@ private static Order CreateOrder(int orderId, OrderType type, Symbol symbol, dec order = new TrailingStopOrder(symbol, quantity, stopPrice, trailingAmount, trailingAsPercentage, time, tag, properties); break; + case OrderType.TrailingStopLimit: + order = new TrailingStopLimitOrder(symbol, quantity, stopPrice, limitPrice, trailingAmount, trailingAsPercentage, + limitOffset, time, tag, properties); + break; + case OrderType.LimitIfTouched: order = new LimitIfTouchedOrder(symbol, quantity, triggerPrice, limitPrice, time, tag, properties); break; diff --git a/Common/Orders/OrderEvent.cs b/Common/Orders/OrderEvent.cs index 32bc2e2c7fc2..2c14aba562e7 100644 --- a/Common/Orders/OrderEvent.cs +++ b/Common/Orders/OrderEvent.cs @@ -37,6 +37,7 @@ public class OrderEvent private decimal? _stopPrice; private decimal? _trailingAmount; private bool? _trailingAsPercentage; + private decimal? _limitOffset; /// /// Id of the order this event comes from. @@ -227,6 +228,23 @@ public bool? TrailingAsPercentage } } + /// + /// The limit offset + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + [ProtoMember(20)] + public decimal? LimitOffset + { + get { return _limitOffset; } + set + { + if (value.HasValue) + { + _limitOffset = value.Value.Normalize(); + } + } + } + /// /// The order ticket associated to the order /// diff --git a/Common/Orders/OrderExtensions.cs b/Common/Orders/OrderExtensions.cs index c52a34058d0c..54cd26d9db7b 100644 --- a/Common/Orders/OrderExtensions.cs +++ b/Common/Orders/OrderExtensions.cs @@ -64,7 +64,8 @@ public static bool IsLimitOrder(this OrderType orderType) { return orderType == OrderType.Limit || orderType == OrderType.StopLimit - || orderType == OrderType.LimitIfTouched; + || orderType == OrderType.LimitIfTouched + || orderType == OrderType.TrailingStopLimit; } /// @@ -74,7 +75,10 @@ public static bool IsLimitOrder(this OrderType orderType) /// True if the order is a stop order, false otherwise public static bool IsStopOrder(this OrderType orderType) { - return orderType == OrderType.StopMarket || orderType == OrderType.StopLimit || orderType == OrderType.TrailingStop; + return orderType == OrderType.StopMarket + || orderType == OrderType.StopLimit + || orderType == OrderType.TrailingStop + || orderType == OrderType.TrailingStopLimit; } } } diff --git a/Common/Orders/OrderField.cs b/Common/Orders/OrderField.cs index 268b79998ea5..85039e79f2c0 100644 --- a/Common/Orders/OrderField.cs +++ b/Common/Orders/OrderField.cs @@ -43,6 +43,11 @@ public enum OrderField /// /// Whether the trailing amount for a is a percentage or an absolute currency value (4) /// - TrailingAsPercentage + TrailingAsPercentage, + + /// + /// The limit offset amount for a (5) + /// + LimitOffset } } diff --git a/Common/Orders/OrderJsonConverter.cs b/Common/Orders/OrderJsonConverter.cs index 96b64eb2a5ad..00aa88076a94 100644 --- a/Common/Orders/OrderJsonConverter.cs +++ b/Common/Orders/OrderJsonConverter.cs @@ -283,6 +283,17 @@ private static Order CreateOrder(OrderType orderType, JObject jObject) }; break; + case OrderType.TrailingStopLimit: + order = new TrailingStopLimitOrder + { + StopPrice = jObject["StopPrice"]?.Value() ?? jObject["stopPrice"]?.Value() ?? default(decimal), + TrailingAmount = jObject["TrailingAmount"]?.Value() ?? jObject["trailingAmount"]?.Value() ?? default(decimal), + TrailingAsPercentage = jObject["TrailingAsPercentage"]?.Value() ?? jObject["trailingAsPercentage"]?.Value() ?? default(bool), + LimitPrice = jObject["LimitPrice"]?.Value() ?? jObject["limitPrice"]?.Value() ?? default(decimal), + LimitOffset = jObject["LimitOffset"]?.Value() ?? jObject["limitOffset"]?.Value() ?? default(decimal), + }; + break; + case OrderType.LimitIfTouched: order = new LimitIfTouchedOrder { diff --git a/Common/Orders/OrderTicket.cs b/Common/Orders/OrderTicket.cs index 02b30c493abb..a0e62bce58d7 100644 --- a/Common/Orders/OrderTicket.cs +++ b/Common/Orders/OrderTicket.cs @@ -269,6 +269,10 @@ public T Get(OrderField field) { fieldValue = AccessOrder(this, field, o => o.LimitPrice, r => r.LimitPrice); } + else if (_submitRequest.OrderType == OrderType.TrailingStopLimit) + { + fieldValue = AccessOrder(this, field, o => o.LimitPrice, r => r.LimitPrice); + } break; case OrderField.StopPrice: @@ -284,6 +288,10 @@ public T Get(OrderField field) { fieldValue = AccessOrder(this, field, o => o.StopPrice, r => r.StopPrice); } + else if (_submitRequest.OrderType == OrderType.TrailingStopLimit) + { + fieldValue = AccessOrder(this, field, o => o.StopPrice, r => r.StopPrice); + } break; case OrderField.TriggerPrice: @@ -291,11 +299,29 @@ public T Get(OrderField field) break; case OrderField.TrailingAmount: - fieldValue = AccessOrder(this, field, o => o.TrailingAmount, r => r.TrailingAmount); + if (_submitRequest.OrderType == OrderType.TrailingStop) + { + fieldValue = AccessOrder(this, field, o => o.TrailingAmount, r => r.TrailingAmount); + } + else if (_submitRequest.OrderType == OrderType.TrailingStopLimit) + { + fieldValue = AccessOrder(this, field, o => o.TrailingAmount, r => r.TrailingAmount); + } break; case OrderField.TrailingAsPercentage: - fieldValue = AccessOrder(this, field, o => o.TrailingAsPercentage, r => r.TrailingAsPercentage); + if (_submitRequest.OrderType == OrderType.TrailingStop) + { + fieldValue = AccessOrder(this, field, o => o.TrailingAsPercentage, r => r.TrailingAsPercentage); + } + else if (_submitRequest.OrderType == OrderType.TrailingStopLimit) + { + fieldValue = AccessOrder(this, field, o => o.TrailingAsPercentage, r => r.TrailingAsPercentage); + } + break; + + case OrderField.LimitOffset: + fieldValue = AccessOrder(this, field, o => o.LimitOffset, r => r.LimitOffset); break; default: diff --git a/Common/Orders/OrderTypes.cs b/Common/Orders/OrderTypes.cs index d37be01542ef..c56826bc119c 100644 --- a/Common/Orders/OrderTypes.cs +++ b/Common/Orders/OrderTypes.cs @@ -78,7 +78,12 @@ public enum OrderType /// /// Trailing Stop Order Type - (11) /// - TrailingStop + TrailingStop, + + /// + /// Trailing Stop Limit Order Type - (12) + /// + TrailingStopLimit } /// diff --git a/Common/Orders/OrderUpdateEvent.cs b/Common/Orders/OrderUpdateEvent.cs index 04724891c35a..ea99d278c597 100644 --- a/Common/Orders/OrderUpdateEvent.cs +++ b/Common/Orders/OrderUpdateEvent.cs @@ -35,5 +35,10 @@ public class OrderUpdateEvent /// Flag indicating whether stop has been triggered for a /// public bool StopTriggered { get; set; } + + /// + /// The updated limit price for a + /// + public decimal LimitPrice { get; set; } } } diff --git a/Common/Orders/SubmitOrderRequest.cs b/Common/Orders/SubmitOrderRequest.cs index 6123b3c99aac..0f5dcd23a8d4 100644 --- a/Common/Orders/SubmitOrderRequest.cs +++ b/Common/Orders/SubmitOrderRequest.cs @@ -103,6 +103,14 @@ public bool TrailingAsPercentage get; private set; } + /// + /// Limit offset amount for a trailing stop limit order + /// + public decimal LimitOffset + { + get; private set; + } + /// /// Gets the order properties for this request /// @@ -132,6 +140,7 @@ public GroupOrderManager GroupOrderManager /// The trigger price for limit if touched orders, for non-limit if touched orders this value is ignored /// The trailing amount to be used to update the stop price /// Whether the is a percentage or an absolute currency value + /// The offset to be used to update the limit price /// The time this request was created /// A custom tag for this request /// The order properties for this request @@ -146,6 +155,7 @@ public SubmitOrderRequest( decimal triggerPrice, decimal trailingAmount, bool trailingAsPercentage, + decimal limitOffset, DateTime time, string tag, IOrderProperties properties = null, @@ -163,9 +173,47 @@ public SubmitOrderRequest( TriggerPrice = triggerPrice; TrailingAmount = trailingAmount; TrailingAsPercentage = trailingAsPercentage; + LimitOffset = limitOffset; OrderProperties = properties; } + /// + /// Initializes a new instance of the class. + /// The will default to + /// + /// The order type to be submitted + /// The symbol's + /// The symbol to be traded + /// The number of units to be ordered + /// The stop price for stop orders, non-stop orders this value is ignored + /// The limit price for limit orders, non-limit orders this value is ignored + /// The trigger price for limit if touched orders, for non-limit if touched orders this value is ignored + /// The trailing amount to be used to update the stop price + /// Whether the is a percentage or an absolute currency value + /// The time this request was created + /// A custom tag for this request + /// The order properties for this request + /// The manager for this combo order + public SubmitOrderRequest( + OrderType orderType, + SecurityType securityType, + Symbol symbol, + decimal quantity, + decimal stopPrice, + decimal limitPrice, + decimal triggerPrice, + decimal trailingAmount, + bool trailingAsPercentage, + DateTime time, + string tag, + IOrderProperties properties = null, + GroupOrderManager groupOrderManager = null + ) + : this(orderType, securityType, symbol, quantity, stopPrice, limitPrice, triggerPrice, trailingAmount, trailingAsPercentage, + 0, time, tag, properties, groupOrderManager) + { + } + /// /// Initializes a new instance of the class. /// The will default to diff --git a/Common/Orders/TrailingStopLimitOrder.cs b/Common/Orders/TrailingStopLimitOrder.cs new file mode 100644 index 000000000000..93ad9dfc88b9 --- /dev/null +++ b/Common/Orders/TrailingStopLimitOrder.cs @@ -0,0 +1,214 @@ +/* + * 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 Newtonsoft.Json; +using QuantConnect.Interfaces; +using System; + +namespace QuantConnect.Orders +{ + /// + /// Trailing Stop Limit Order Type Definition + /// + public class TrailingStopLimitOrder : StopLimitOrder + { + /// + /// Trailing amount for this trailing stop limit order + /// + [JsonProperty(PropertyName = "trailingAmount")] + public decimal TrailingAmount { get; internal set; } + + /// + /// Determines whether the is a percentage or an absolute currency value + /// + [JsonProperty(PropertyName = "trailingAsPercentage")] + public bool TrailingAsPercentage { get; internal set; } + + /// + /// Limit offset amount for this trailing stop limit order + /// + [JsonProperty(PropertyName = "limitOffset")] + public decimal LimitOffset { get; internal set; } + + /// + /// TrailingStopLimit Order Type + /// + public override OrderType Type + { + get { return OrderType.TrailingStopLimit; } + } + + /// + /// Default constructor for JSON Deserialization: + /// + public TrailingStopLimitOrder() + { + } + + /// + /// New Trailing Stop Limit Order constructor + /// + /// Symbol of the asset being traded + /// Quantity of the asset being traded + /// Initial stop price at which the order should be triggered + /// Price the order should be filled at if triggered + /// The trailing amount to be used to update the stop price + /// Whether the is a percentage or an absolute currency value + /// The limit offset amount used to update the limit price + /// Time the order was placed + /// User defined data tag for this order + /// The properties for this order + public TrailingStopLimitOrder(Symbol symbol, decimal quantity, decimal stopPrice, decimal limitPrice, decimal trailingAmount, bool trailingAsPercentage, + decimal limitOffset, DateTime time, string tag = "", IOrderProperties properties = null) + : base(symbol, quantity, stopPrice, limitPrice, time, tag, properties) + { + TrailingAmount = trailingAmount; + TrailingAsPercentage = trailingAsPercentage; + LimitOffset = limitOffset; + } + + /// + /// Gets the default tag for this order + /// + /// The default tag + public override string GetDefaultTag() + { + return Messages.TrailingStopLimitOrder.Tag(this); + } + + /// + /// Modifies the state of this order to match the update request + /// + /// The request to update this order object + public override void ApplyUpdateOrderRequest(UpdateOrderRequest request) + { + base.ApplyUpdateOrderRequest(request); + if (request.TrailingAmount.HasValue) + { + TrailingAmount = request.TrailingAmount.Value; + } + if (request.LimitOffset.HasValue) + { + LimitOffset = request.LimitOffset.Value; + } + } + + /// + /// Returns a string that represents the current object. + /// + /// + /// A string that represents the current object. + /// + /// 2 + public override string ToString() + { + return Messages.TrailingStopLimitOrder.ToString(this); + } + + /// + /// Creates a deep-copy clone of this order + /// + /// A copy of this order + public override Order Clone() + { + var order = new TrailingStopLimitOrder + { + StopPrice = StopPrice, + TrailingAmount = TrailingAmount, + TrailingAsPercentage = TrailingAsPercentage, + LimitPrice = LimitPrice, + LimitOffset = LimitOffset, + StopTriggered = StopTriggered + }; + CopyTo(order); + return order; + } + + /// + /// Tries to update the stop price for a trailing stop order given the current market price + /// + /// The current market price + /// The current trailing stop order stop price + /// The trailing amount to be used to update the stop price + /// Whether the is a percentage or an absolute currency value + /// The limit offset amount used to update the limit price + /// The order direction + /// The updated stop price + /// The updated limit price + /// + /// Whether the stop price was updated. + /// This only happens when the distance between the current stop price and the current market price is greater than the trailing amount, + /// which will happen when the market price raises/falls for sell/buy orders respectively. + /// + public static bool TryUpdateStopAndLimitPrices(decimal currentMarketPrice, decimal currentStopPrice, decimal trailingAmount, + bool trailingAsPercentage, decimal limitOffset, OrderDirection direction, out decimal updatedStopPrice, out decimal updatedLimitPrice) + { + updatedStopPrice = 0m; + updatedLimitPrice = 0m; + + var distanceToMarketPrice = direction == OrderDirection.Sell + ? currentMarketPrice - currentStopPrice + : currentStopPrice - currentMarketPrice; + var stopReference = trailingAsPercentage ? currentMarketPrice * trailingAmount : trailingAmount; + + if (distanceToMarketPrice <= stopReference) + { + return false; + } + + updatedStopPrice = CalculateStopPrice(currentMarketPrice, trailingAmount, trailingAsPercentage, direction); + updatedLimitPrice = CalculateLimitPrice(updatedStopPrice, limitOffset, direction); + return true; + } + + /// + /// Calculates the stop price for a trailing stop limit order given the current market price + /// + /// The current market price + /// The trailing amount to be used to update the stop price + /// Whether the is a percentage or an absolute currency value + /// The order direction + /// The stop price for the order given the current market price + public static decimal CalculateStopPrice(decimal currentMarketPrice, decimal trailingAmount, bool trailingAsPercentage, + OrderDirection direction) + { + if (trailingAsPercentage) + { + return direction == OrderDirection.Buy + ? currentMarketPrice * (1 + trailingAmount) + : currentMarketPrice * (1 - trailingAmount); + } + + return direction == OrderDirection.Buy + ? currentMarketPrice + trailingAmount + : currentMarketPrice - trailingAmount; + } + + /// + /// Calculates the limit price for a trailing stop limit order given the stop price and current market price + /// + /// The current stop price of the trailing stop limit order + /// The limit offset amount used to update the limit price + /// The order direction + /// The stop price for the order given the current market price + public static decimal CalculateLimitPrice(decimal currentStopPrice, decimal limitOffset, OrderDirection direction) + { + return direction == OrderDirection.Buy + ? currentStopPrice + limitOffset + : currentStopPrice - limitOffset; + } + } +} + diff --git a/Common/Orders/UpdateOrderFields.cs b/Common/Orders/UpdateOrderFields.cs index ef4298001d0a..e09f35f6f887 100644 --- a/Common/Orders/UpdateOrderFields.cs +++ b/Common/Orders/UpdateOrderFields.cs @@ -45,6 +45,11 @@ public class UpdateOrderFields /// public decimal? TrailingAmount { get; set; } + /// + /// The trailing stop limit order limit offset + /// + public decimal? LimitOffset { get; set; } + /// /// Specify to update the order's tag /// diff --git a/Common/Orders/UpdateOrderRequest.cs b/Common/Orders/UpdateOrderRequest.cs index 1ba6ecdfe7a1..a201b50bb525 100644 --- a/Common/Orders/UpdateOrderRequest.cs +++ b/Common/Orders/UpdateOrderRequest.cs @@ -55,6 +55,11 @@ public override OrderRequestType OrderRequestType /// public decimal? TrailingAmount { get; private set; } + /// + /// The trailing stop limit order limit offset + /// + public decimal? LimitOffset { get; private set; } + /// /// Initializes a new instance of the class /// @@ -69,6 +74,7 @@ public UpdateOrderRequest(DateTime time, int orderId, UpdateOrderFields fields) StopPrice = fields.StopPrice; TriggerPrice = fields.TriggerPrice; TrailingAmount = fields.TrailingAmount; + LimitOffset = fields.LimitOffset; } /// @@ -90,7 +96,8 @@ public override string ToString() /// True if the update request is allowed for a closed order public bool IsAllowedForClosedOrder() { - return !Quantity.HasValue && !LimitPrice.HasValue && !StopPrice.HasValue && !TriggerPrice.HasValue & !TrailingAmount.HasValue; + return !Quantity.HasValue && !LimitPrice.HasValue && !StopPrice.HasValue && + !TriggerPrice.HasValue && !TrailingAmount.HasValue && !LimitOffset.HasValue; } } } diff --git a/Common/Python/FillModelPythonWrapper.cs b/Common/Python/FillModelPythonWrapper.cs index 0c4ba6c5b08c..0c7b7eb9aa4c 100644 --- a/Common/Python/FillModelPythonWrapper.cs +++ b/Common/Python/FillModelPythonWrapper.cs @@ -140,6 +140,17 @@ public override OrderEvent TrailingStopFill(Security asset, TrailingStopOrder or return _model.InvokeMethod(nameof(TrailingStopFill), asset, order); } + /// + /// Trailing Stop Limit Fill Model. Return an order event with the fill details. + /// + /// Asset we're trading this order + /// Trailing Stop Limit Order to Check, return filled if true + /// Order fill information detailing the average price and quantity filled. + public override OrderEvent TrailingStopLimitFill(Security asset, TrailingStopLimitOrder order) + { + return _model.InvokeMethod(nameof(TrailingStopLimitFill), asset, order); + } + /// /// Default combo market fill model for the base security class. Fills at the last traded price for each leg. /// diff --git a/Common/Securities/CashBuyingPowerModel.cs b/Common/Securities/CashBuyingPowerModel.cs index 92c44eca0eec..6b665ee9d1ac 100644 --- a/Common/Securities/CashBuyingPowerModel.cs +++ b/Common/Securities/CashBuyingPowerModel.cs @@ -392,6 +392,10 @@ private static decimal GetOrderPrice(Security security, Order order) case OrderType.TrailingStop: orderPrice = ((TrailingStopOrder)order).StopPrice; break; + + case OrderType.TrailingStopLimit: + orderPrice = ((TrailingStopLimitOrder)order).LimitPrice; + break; } return orderPrice; diff --git a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs index 41ce41bc13ac..42b465ebcbe8 100644 --- a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs +++ b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs @@ -1175,6 +1175,13 @@ private void HandleOrderEvents(List orderEvents) orderEvent.StopPrice = trailingStopOrder.StopPrice; orderEvent.TrailingAmount = trailingStopOrder.TrailingAmount; break; + case OrderType.TrailingStopLimit: + var trailingStopLimitOrder = order as TrailingStopLimitOrder; + orderEvent.StopPrice = trailingStopLimitOrder.StopPrice; + orderEvent.TrailingAmount = trailingStopLimitOrder.TrailingAmount; + orderEvent.LimitPrice = trailingStopLimitOrder.LimitPrice; + orderEvent.LimitOffset = trailingStopLimitOrder.LimitOffset; + break; case OrderType.LimitIfTouched: var limitIfTouchedOrder = order as LimitIfTouchedOrder; orderEvent.LimitPrice = limitIfTouchedOrder.LimitPrice; @@ -1308,6 +1315,12 @@ private void HandleOrderUpdated(OrderUpdateEvent e) case OrderType.StopLimit: ((StopLimitOrder)order).StopTriggered = e.StopTriggered; break; + + case OrderType.TrailingStopLimit: + ((TrailingStopLimitOrder)order).StopPrice = e.TrailingStopPrice; + ((TrailingStopLimitOrder)order).StopTriggered = e.StopTriggered; + ((TrailingStopLimitOrder)order).LimitPrice = e.LimitPrice; + break; } } @@ -1687,6 +1700,26 @@ protected void RoundOrderPrices(Order order, Security security, bool comboIsRead } break; + case OrderType.TrailingStopLimit: + { + var trailingStopLimitOrder = (TrailingStopLimitOrder)order; + RoundOrderPrice(security, trailingStopLimitOrder.StopPrice, "StopPrice", + (roundedPrice) => trailingStopLimitOrder.StopPrice = roundedPrice); + + if (!trailingStopLimitOrder.TrailingAsPercentage) + { + RoundOrderPrice(security, trailingStopLimitOrder.TrailingAmount, "TrailingAmount", + (roundedAmount) => trailingStopLimitOrder.TrailingAmount = roundedAmount); + } + + RoundOrderPrice(security, trailingStopLimitOrder.LimitPrice, "LimitPrice", + (roundedPrice) => trailingStopLimitOrder.LimitPrice = roundedPrice); + + RoundOrderPrice(security, trailingStopLimitOrder.LimitOffset, "LimitOffset", + (roundedPrice) => trailingStopLimitOrder.LimitOffset = roundedPrice); + } + break; + case OrderType.LimitIfTouched: { var limitIfTouchedOrder = (LimitIfTouchedOrder)order; diff --git a/Tests/Algorithm/AlgorithmTradingTests.cs b/Tests/Algorithm/AlgorithmTradingTests.cs index 1a3732a20327..6294eb9d7cbb 100644 --- a/Tests/Algorithm/AlgorithmTradingTests.cs +++ b/Tests/Algorithm/AlgorithmTradingTests.cs @@ -1388,12 +1388,19 @@ public void OrderQuantityConversionTest() algo.LimitIfTouchedOrder(Symbols.MSFT, 1.0, 1, 2); algo.LimitIfTouchedOrder(Symbols.MSFT, 1.0m, 1, 2); + algo.TrailingStopLimitOrder(Symbols.MSFT, 1, 1, true, 1); + algo.TrailingStopLimitOrder(Symbols.MSFT, 1.0, 1, true, 1); + algo.TrailingStopLimitOrder(Symbols.MSFT, 1.0m, 1, true, 1); + algo.TrailingStopLimitOrder(Symbols.MSFT, 1, 10m, 11m, 0.01m, false, 1m); + algo.TrailingStopLimitOrder(Symbols.MSFT, 1.0, 10m, 11m, 0.01m, false, 1m); + algo.TrailingStopLimitOrder(Symbols.MSFT, 1.0m, 10m, 11m, 0.01m, false, 1m); + algo.SetHoldings(Symbols.MSFT, 1); algo.SetHoldings(Symbols.MSFT, 1.0); algo.SetHoldings(Symbols.MSFT, 1.0m); algo.SetHoldings(Symbols.MSFT, 1.0f); - const int expected = 44; + const int expected = 50; Assert.AreEqual(expected, algo.Transactions.LastOrderId); } diff --git a/Tests/Common/Brokerages/DefaultBrokerageModelTests.cs b/Tests/Common/Brokerages/DefaultBrokerageModelTests.cs index 09c0f1c431a2..e97cf8a541b7 100644 --- a/Tests/Common/Brokerages/DefaultBrokerageModelTests.cs +++ b/Tests/Common/Brokerages/DefaultBrokerageModelTests.cs @@ -87,7 +87,8 @@ public void ApplySplitWorksAsExpected() OrderType.Limit, OrderType.StopLimit, OrderType.LimitIfTouched, - OrderType.TrailingStop + OrderType.TrailingStop, + OrderType.TrailingStopLimit }; var algorithm = new BrokerageTransactionHandlerTests.TestAlgorithm @@ -122,6 +123,10 @@ public void ApplySplitWorksAsExpected() orderRequest = new SubmitOrderRequest(OrderType.TrailingStop, SecurityType.Equity, Symbols.IBM, 100, stopPrice: 10, 0, 0, trailingAmount: 0.5m, trailingAsPercentage: false, DateTime.UtcNow, ""); break; + case OrderType.TrailingStopLimit: + orderRequest = new SubmitOrderRequest(OrderType.TrailingStopLimit, SecurityType.Equity, Symbols.IBM, 100, stopPrice: 10, + limitPrice: 11, 0, trailingAmount: 2, trailingAsPercentage: false, limitOffset: 1, DateTime.UtcNow, ""); + break; } algorithm.Transactions.AddOrder(orderRequest); var ticket = new OrderTicket(algorithm.Transactions, orderRequest); @@ -151,11 +156,16 @@ public void ApplySplitWorksAsExpected() Assert.AreEqual(5, order.GetPropertyValue("StopPrice")); Assert.AreEqual(0.25m, order.GetPropertyValue("TrailingAmount")); break; + case OrderType.TrailingStopLimit: + Assert.AreEqual(5, order.GetPropertyValue("StopPrice")); + Assert.AreEqual(1, order.GetPropertyValue("TrailingAmount")); + Assert.AreEqual(5.5m, order.GetPropertyValue("LimitPrice")); + Assert.AreEqual(0.5m, order.GetPropertyValue("LimitOffset")); + break; } } } - [Test] public void AppliesSplitOnlyWhenTrailingStopOrderTrailingAmountIsNotPercentage([Values] bool trailingAsPercentage) { @@ -172,6 +182,7 @@ public void AppliesSplitOnlyWhenTrailingStopOrderTrailingAmountIsNotPercentage([ var tickets = new List(); var orderTime = new DateTime(2023, 07, 21, 12, 0, 0); + var orderRequest = new SubmitOrderRequest(OrderType.TrailingStop, SecurityType.Equity, Symbols.IBM, 100, stopPrice: 10, 0, 0, trailingAmount: 0.1m, trailingAsPercentage, orderTime, ""); algorithm.Transactions.AddOrder(orderRequest); @@ -188,6 +199,38 @@ public void AppliesSplitOnlyWhenTrailingStopOrderTrailingAmountIsNotPercentage([ Assert.AreEqual(trailingAsPercentage ? 0.1m : 0.05m, order.GetPropertyValue("TrailingAmount")); } + [Test] + public void AppliesSplitOnlyWhenTrailingStopLimitOrderTrailingAmountIsNotPercentage([Values] bool trailingAsPercentage) + { + var algorithm = new BrokerageTransactionHandlerTests.TestAlgorithm + { + HistoryProvider = new BrokerageTransactionHandlerTests.EmptyHistoryProvider() + }; + var transactionHandler = new BacktestingTransactionHandler(); + using var backtestingBrokerage = new BacktestingBrokerage(algorithm); + transactionHandler.Initialize(algorithm, backtestingBrokerage, new TestResultHandler(Console.WriteLine)); + + algorithm.Transactions.SetOrderProcessor(transactionHandler); + algorithm.AddEquity("IBM"); + + var tickets = new List(); + var orderTime = new DateTime(2023, 07, 21, 12, 0, 0); + + var orderRequest = new SubmitOrderRequest(OrderType.TrailingStopLimit, SecurityType.Equity, Symbols.IBM, 100, stopPrice: 10, + limitPrice: 11, 0, trailingAmount: 0.25m, trailingAsPercentage, limitOffset: 1, DateTime.UtcNow, ""); + algorithm.Transactions.AddOrder(orderRequest); + var ticket = new OrderTicket(algorithm.Transactions, orderRequest); + tickets.Add(ticket); + + var split = new Split(Symbols.IBM, orderTime, 1, 0.5m, SplitType.SplitOccurred); + _defaultBrokerageModel.ApplySplit(tickets, split); + transactionHandler.ProcessSynchronousEvents(); + + var order = algorithm.Transactions.GetOrders().Single(); + + Assert.AreEqual(5, order.GetPropertyValue("StopPrice", Flags.Instance | Flags.Public)); + Assert.AreEqual(trailingAsPercentage ? 0.25m : 0.125m, order.GetPropertyValue("TrailingAmount")); + } private static Order GetMarketOnOpenOrder() { diff --git a/Tests/Common/Orders/Fills/EquityFillModelTests.TrailingStopLimitFill.cs b/Tests/Common/Orders/Fills/EquityFillModelTests.TrailingStopLimitFill.cs new file mode 100644 index 000000000000..ee919ce18a85 --- /dev/null +++ b/Tests/Common/Orders/Fills/EquityFillModelTests.TrailingStopLimitFill.cs @@ -0,0 +1,424 @@ +/* + * 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 NUnit.Framework; +using QuantConnect.Data.Market; +using QuantConnect.Indicators; +using QuantConnect.Orders; +using QuantConnect.Orders.Fills; +using QuantConnect.Securities; +using QuantConnect.Tests.Common.Data; +using QuantConnect.Tests.Common.Securities; +using System; +using System.Linq; + +namespace QuantConnect.Tests.Common.Orders.Fills +{ + [TestFixture] + public partial class EquityFillModelTests + { + [Test] + public void PerformsTrailingStopLimitImmediateFillBuy([Values] bool trailingAsPercentage) + { + var model = new EquityFillModel(); + // Assume market price is $100: + var order = trailingAsPercentage + // a trailing amount of 5%, stop price $105. limit offset of $5, limit price $110 + ? new TrailingStopLimitOrder(Symbols.SPY, 100, 105m, 110m, 0.05m, true, 5m, Noon) + // a trailing amount of $5 set the stop price to $105. limit offset of $5, limit price $110 + : new TrailingStopLimitOrder(Symbols.SPY, 100, 105m, 110m, 5m, false, 5m, Noon); + + var config = CreateTradeBarConfig(Symbols.SPY); + var security = new Security( + SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), + config, + new Cash(Currencies.USD, 0, 1m), + SymbolProperties.GetDefault(Currencies.USD), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache()); + security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + + // Security price rises above stop price immediately + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, trailingAsPercentage ? + 100m * (1 + 0.075m) : 100m + 7.5m)); + + var fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // High is below limit price so fills at the high + Assert.AreEqual(order.Quantity, fill.FillQuantity); + Assert.AreEqual(security.High, fill.FillPrice); + Assert.AreEqual(OrderStatus.Filled, fill.Status); + } + + [Test] + public void PerformsTrailingStopLimitFillBuy([Values] bool trailingAsPercentage) + { + var model = new ImmediateFillModel(); + // Assume market price is $100: + var order = trailingAsPercentage + // a trailing amount of 5%, stop price $105. limit offset of $1, limit price $106 + ? new TrailingStopLimitOrder(Symbols.SPY, 100, 105m, 106m, 0.05m, true, 1m, Noon) + // a trailing amount of $5 set the stop price to $105. limit offset of $1, limit price $106 + : new TrailingStopLimitOrder(Symbols.SPY, 100, 105m, 106m, 5m, false, 1m, Noon); + + var config = CreateTradeBarConfig(Symbols.SPY); + var security = new Security( + SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), + config, + new Cash(Currencies.USD, 0, 1m), + SymbolProperties.GetDefault(Currencies.USD), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache()); + security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + + var initialTrailingStopPrice = order.StopPrice; + var prevMarketPrice = 100m; + // Market price hasn't moved + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, prevMarketPrice)); + + var fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a rising security price, but not enough to trigger the stop + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 102.5m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a falling security price, but still above the lowest market price + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a falling security price, which triggers a stop price update + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 99m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have been updated to: + // --> (market price + trailing amount) if trailing amount is not a percentage + // --> (market price * (1 + trailing amount)) if trailing amount is a percentage + // Limit price should have been updated to (new stop price + limit offset) + Assert.AreNotEqual(initialTrailingStopPrice, order.StopPrice); + var expectedUpdatedStopPrice = trailingAsPercentage ? security.Price * (1 + 0.05m) : security.Price + 5m; + var expectedUpdatedLimitPrice = expectedUpdatedStopPrice + 1m; + Assert.AreEqual(expectedUpdatedStopPrice, order.StopPrice); + Assert.AreEqual(expectedUpdatedLimitPrice, order.LimitPrice); + + // Simulate a rising security price, enough to trigger the stop + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 110m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // Stop price should have not been updated + Assert.AreEqual(expectedUpdatedStopPrice, order.StopPrice); + + // Market price is above limit price + AssertUnfilled(fill); + + // Market price moves below limit price and order can fill + security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 110m, 110m, 102m, 103m, 100)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // Assumes worst case fill price -> limit price + AssertFilled(fill, order.Quantity, Math.Max(security.Price, order.LimitPrice)); + } + + [Test] + public void PerformsTrailingStopLimitImmediateFillSell([Values] bool trailingAsPercentage) + { + var model = new ImmediateFillModel(); + // Assume market price is $100: + var order = trailingAsPercentage + // a trailing amount of 5%, stop price $95. limit offset of $5, limit price $90 + ? new TrailingStopLimitOrder(Symbols.SPY, -100, 95m, 90m, 0.05m, true, 5m, Noon) + // a trailing amount of $5 set the stop price to $95. limit offset of $5, limit price $195 + : new TrailingStopLimitOrder(Symbols.SPY, -100, 95m, 90m, 5m, false, 5m, Noon); + + var config = CreateTradeBarConfig(Symbols.SPY); + var security = new Security( + SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), + config, + new Cash(Currencies.USD, 0, 1m), + SymbolProperties.GetDefault(Currencies.USD), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache()); + security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + + // Security price falls below stop price immediately + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, trailingAsPercentage ? + 100m * (1 - 0.075m) : 100m - 7.5m)); + + var fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // Low is above limit price so fills at the low + AssertFilled(fill, order.Quantity, security.Low); + } + + [Test] + public void PerformsTrailingStopLimitFillSell([Values] bool trailingAsPercentage) + { + var model = new ImmediateFillModel(); + // Assume market price is $100: + var order = trailingAsPercentage + // a trailing amount of 5%, stop price $95. limit offset of $1, limit price $94 + ? new TrailingStopLimitOrder(Symbols.SPY, -100, 95m, 94m, 0.05m, true, 1m, Noon) + // a trailing amount of $5 set the stop price to $95. limit offset of $1, limit price $94 + : new TrailingStopLimitOrder(Symbols.SPY, -100, 95m, 94m, 5m, false, 1m, Noon); + + var config = CreateTradeBarConfig(Symbols.SPY); + var security = new Security( + SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), + config, + new Cash(Currencies.USD, 0, 1m), + SymbolProperties.GetDefault(Currencies.USD), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache()); + security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + + var initialTrailingStopPrice = order.StopPrice; + var prevMarketPrice = 100m; + // Market price hasn't moved + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, prevMarketPrice)); + + var fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a falling security price, but not enough to trigger the stop + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 97.5m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a rising security price, but still below the highest market price + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 99m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a rising security price, which triggers a stop price update + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have been updated to: + // --> (market price - trailing amount) if trailing amount is not a percentage + // --> (market price * (1 - trailing amount)) if trailing amount is a percentage + // Limit price should have been updated to (new stop price - limit offset) + Assert.AreNotEqual(initialTrailingStopPrice, order.StopPrice); + var expectedUpdatedStopPrice = trailingAsPercentage ? security.Price * (1 - 0.05m) : security.Price - 5m; + var expectedUpdatedLimitPrice = expectedUpdatedStopPrice - 1m; + Assert.AreEqual(expectedUpdatedStopPrice, order.StopPrice); + Assert.AreEqual(expectedUpdatedLimitPrice, order.LimitPrice); + + // Simulate a falling security price, enough to trigger the stop + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 92m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // Stop price should have not been updated + Assert.AreEqual(expectedUpdatedStopPrice, order.StopPrice); + + // Market price is below limit price + AssertUnfilled(fill); + + // Market price moves above limit price and order can fill + security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 93m, 97m, 93m, 95.5m, 100)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // Assumes worst case fill price -> limit price + AssertFilled(fill, order.Quantity, Math.Min(security.Price, order.LimitPrice)); + } + + [TestCase(100, 101, 102)] + [TestCase(-100, 99, 98)] + public void TrailingStopLimitDoesNotFillUsingDataBeforeSubmitTime(decimal orderQuantity, decimal stopPrice, + decimal limitPrice) + { + var time = new DateTime(2018, 9, 24, 9, 30, 0); + + var symbol = Symbols.SPY; + var config = CreateTradeBarConfig(symbol); + var security = new Security( + SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(), + config, + new Cash(Currencies.USD, 0, 1m), + SymbolProperties.GetDefault(Currencies.USD), + ErrorCurrencyConverter.Instance, + RegisteredSecurityDataTypesProvider.Null, + new SecurityCache()); + var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork); + security.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + + // The new prices are enough to trigger the stop and fill for the orders + var tradeBar = new TradeBar(time, symbol, 100m, 102m, 98.5m, 100m, 12345); + security.SetMarketPrice(tradeBar); + + time += TimeSpan.FromMinutes(1); + timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork)); + + var fillForwardBar = (TradeBar)tradeBar.Clone(true); + security.SetMarketPrice(fillForwardBar); + + var fillModel = new ImmediateFillModel(); + var order = new TrailingStopLimitOrder(symbol, orderQuantity, stopPrice, limitPrice, 0.1m, true, 1m, + time.ConvertToUtc(TimeZones.NewYork)); + + var fill = fillModel.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + Assert.False(order.StopTriggered); + + time += TimeSpan.FromMinutes(1); + timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork)); + + tradeBar = new TradeBar(time, symbol, 100m, 102m, 98.5m, 100m, 12345); + security.SetMarketPrice(tradeBar); + + fill = fillModel.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertFilled(fill, orderQuantity, orderQuantity < 0 ? Math.Max(security.Low, limitPrice) : Math.Min(security.High, limitPrice)); + } + private static void AssertUnfilled(OrderEvent fill) + { + Assert.AreEqual(OrderStatus.None, fill.Status); + Assert.AreEqual(0, fill.FillQuantity); + Assert.AreEqual(0, fill.FillPrice); + } + + private static void AssertFilled(OrderEvent fill, decimal expectedFillQuantity, decimal expectedFillPrice) + { + Assert.AreEqual(OrderStatus.Filled, fill.Status); + Assert.AreEqual(expectedFillQuantity, fill.FillQuantity); + Assert.AreEqual(expectedFillPrice, fill.FillPrice); + } + } +} + diff --git a/Tests/Common/Orders/Fills/ImmediateFillModelTests.cs b/Tests/Common/Orders/Fills/ImmediateFillModelTests.cs index e1870648fda4..2beb5d9b7237 100644 --- a/Tests/Common/Orders/Fills/ImmediateFillModelTests.cs +++ b/Tests/Common/Orders/Fills/ImmediateFillModelTests.cs @@ -666,6 +666,349 @@ public void TrailingStopOrderDoesNotFillUsingDataBeforeSubmitTime(decimal orderQ AssertFilled(fill, orderQuantity, orderQuantity < 0 ? Math.Min(security.Price, stopPrice) : Math.Max(security.Price, stopPrice)); } + [Test] + public void PerformsTrailingStopLimitImmediateFillBuy([Values] bool isInternal, [Values] bool trailingAsPercentage) + { + var model = new ImmediateFillModel(); + // Assume market price is $100: + var order = trailingAsPercentage + // a trailing amount of 5%, stop price $105. limit offset of $5, limit price $110 + ? new TrailingStopLimitOrder(Symbols.SPY, 100, 105m, 110m, 0.05m, true, 5m, Noon) + // a trailing amount of $5 set the stop price to $105. limit offset of $5, limit price $110 + : new TrailingStopLimitOrder(Symbols.SPY, 100, 105m, 110m, 5m, false, 5m, Noon); + + var config = CreateTradeBarConfig(Symbols.SPY, isInternal); + var security = GetSecurity(config); + security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + + // Security price rises above stop price immediately + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, trailingAsPercentage ? + 100m * (1 + 0.075m) : 100m + 7.5m)); + + var fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // High is below limit price so fills at the high + AssertFilled(fill, order.Quantity, security.High); + } + + [Test] + public void PerformsTrailingStopLimitFillBuy([Values] bool isInternal, [Values] bool trailingAsPercentage) + { + var model = new ImmediateFillModel(); + // Assume market price is $100: + var order = trailingAsPercentage + // a trailing amount of 5%, stop price $105. limit offset of $1, limit price $106 + ? new TrailingStopLimitOrder(Symbols.SPY, 100, 105m, 106m, 0.05m, true, 1m, Noon) + // a trailing amount of $5 set the stop price to $105. limit offset of $1, limit price $106 + : new TrailingStopLimitOrder(Symbols.SPY, 100, 105m, 106m, 5m, false, 1m, Noon); + + var config = CreateTradeBarConfig(Symbols.SPY, isInternal); + var security = GetSecurity(config); + security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + + var initialTrailingStopPrice = order.StopPrice; + var prevMarketPrice = 100m; + // Market price hasn't moved + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, prevMarketPrice)); + + var fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a rising security price, but not enough to trigger the stop + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 102.5m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a falling security price, but still above the lowest market price + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a falling security price, which triggers a stop price update + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 99m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have been updated to: + // --> (market price + trailing amount) if trailing amount is not a percentage + // --> (market price * (1 + trailing amount)) if trailing amount is a percentage + // Limit price should have been updated to (new stop price + limit offset) + Assert.AreNotEqual(initialTrailingStopPrice, order.StopPrice); + var expectedUpdatedStopPrice = trailingAsPercentage ? security.Price * (1 + 0.05m) : security.Price + 5m; + var expectedUpdatedLimitPrice = expectedUpdatedStopPrice + 1m; + Assert.AreEqual(expectedUpdatedStopPrice, order.StopPrice); + Assert.AreEqual(expectedUpdatedLimitPrice, order.LimitPrice); + + // Simulate a rising security price, enough to trigger the stop + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 110m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // Stop price should have not been updated + Assert.AreEqual(expectedUpdatedStopPrice, order.StopPrice); + + // Market price is above limit price + AssertUnfilled(fill); + + // Market price moves below limit price and order can fill + security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 110m, 110m, 102m, 103m, 100)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // Assumes worst case fill price -> limit price + AssertFilled(fill, order.Quantity, Math.Max(security.Price, order.LimitPrice)); + } + + [Test] + public void PerformsTrailingStopLimitImmediateFillSell([Values] bool isInternal, [Values] bool trailingAsPercentage) + { + var model = new ImmediateFillModel(); + // Assume market price is $100: + var order = trailingAsPercentage + // a trailing amount of 5%, stop price $95. limit offset of $5, limit price $90 + ? new TrailingStopLimitOrder(Symbols.SPY, -100, 95m, 90m, 0.05m, true, 5m, Noon) + // a trailing amount of $5 set the stop price to $95. limit offset of $5, limit price $195 + : new TrailingStopLimitOrder(Symbols.SPY, -100, 95m, 90m, 5m, false, 5m, Noon); + + var config = CreateTradeBarConfig(Symbols.SPY, isInternal); + var security = GetSecurity(config); + security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + + // Security price falls below stop price immediately + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, trailingAsPercentage ? + 100m * (1 - 0.075m) : 100m - 7.5m)); + + var fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // Low is above limit price so fills at the low + AssertFilled(fill, order.Quantity, security.Low); + } + + [Test] + public void PerformsTrailingStopLimitFillSell([Values] bool isInternal, [Values] bool trailingAsPercentage) + { + var model = new ImmediateFillModel(); + // Assume market price is $100: + var order = trailingAsPercentage + // a trailing amount of 5%, stop price $95. limit offset of $1, limit price $94 + ? new TrailingStopLimitOrder(Symbols.SPY, -100, 95m, 94m, 0.05m, true, 1m, Noon) + // a trailing amount of $5 set the stop price to $95. limit offset of $1, limit price $94 + : new TrailingStopLimitOrder(Symbols.SPY, -100, 95m, 94m, 5m, false, 1m, Noon); + + var config = CreateTradeBarConfig(Symbols.SPY, isInternal); + var security = GetSecurity(config); + security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + + var initialTrailingStopPrice = order.StopPrice; + var prevMarketPrice = 100m; + // Market price hasn't moved + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, prevMarketPrice)); + + var fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a falling security price, but not enough to trigger the stop + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 97.5m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a rising security price, but still below the highest market price + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 99m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have not been updated + Assert.AreEqual(initialTrailingStopPrice, order.StopPrice); + + // Simulate a rising security price, which triggers a stop price update + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + + // Stop price should have been updated to: + // --> (market price - trailing amount) if trailing amount is not a percentage + // --> (market price * (1 - trailing amount)) if trailing amount is a percentage + // Limit price should have been updated to (new stop price - limit offset) + Assert.AreNotEqual(initialTrailingStopPrice, order.StopPrice); + var expectedUpdatedStopPrice = trailingAsPercentage ? security.Price * (1 - 0.05m) : security.Price - 5m; + var expectedUpdatedLimitPrice = expectedUpdatedStopPrice - 1m; + Assert.AreEqual(expectedUpdatedStopPrice, order.StopPrice); + Assert.AreEqual(expectedUpdatedLimitPrice, order.LimitPrice); + + // Simulate a falling security price, enough to trigger the stop + security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 92m)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // Stop price should have not been updated + Assert.AreEqual(expectedUpdatedStopPrice, order.StopPrice); + + // Market price is below limit price + AssertUnfilled(fill); + + // Market price moves above limit price and order can fill + security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 93m, 97m, 93m, 95.5m, 100)); + + fill = model.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + // Assumes worst case fill price -> limit price + AssertFilled(fill, order.Quantity, Math.Min(security.Price, order.LimitPrice)); + } + + [TestCase(100, 101, 102, true)] + [TestCase(100, 101, 102, false)] + [TestCase(-100, 99, 98, true)] + [TestCase(-100, 99, 98, false)] + public void TrailingStopLimitDoesNotFillUsingDataBeforeSubmitTime(decimal orderQuantity, decimal stopPrice, + decimal limitPrice, bool isInternal) + { + var time = new DateTime(2018, 9, 24, 9, 30, 0); + + var symbol = Symbols.SPY; + var config = CreateTradeBarConfig(symbol, isInternal); + var security = GetSecurity(config); + var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork); + security.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + + // The new prices are enough to trigger the stop and fill for the orders + var tradeBar = new TradeBar(time, symbol, 100m, 102m, 98.5m, 100m, 12345); + security.SetMarketPrice(tradeBar); + + time += TimeSpan.FromMinutes(1); + timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork)); + + var fillForwardBar = (TradeBar)tradeBar.Clone(true); + security.SetMarketPrice(fillForwardBar); + + var fillModel = new ImmediateFillModel(); + var order = new TrailingStopLimitOrder(symbol, orderQuantity, stopPrice, limitPrice, 0.1m, true, 1m, + time.ConvertToUtc(TimeZones.NewYork)); + + var fill = fillModel.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertUnfilled(fill); + Assert.False(order.StopTriggered); + + time += TimeSpan.FromMinutes(1); + timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork)); + + tradeBar = new TradeBar(time, symbol, 100m, 102m, 98.5m, 100m, 12345); + security.SetMarketPrice(tradeBar); + + fill = fillModel.Fill(new FillModelParameters( + security, + order, + new MockSubscriptionDataConfigProvider(config), + Time.OneHour, + null)).Single(); + + AssertFilled(fill, orderQuantity, orderQuantity < 0 ? Math.Max(security.Low, limitPrice) : Math.Min(security.High, limitPrice)); + } + [TestCase(true)] [TestCase(false)] public void PerformsLimitIfTouchedFillBuy(bool isInternal) diff --git a/Tests/Common/Orders/OrderJsonConverterTests.cs b/Tests/Common/Orders/OrderJsonConverterTests.cs index 8304b4d21cf1..5a1eda561e73 100644 --- a/Tests/Common/Orders/OrderJsonConverterTests.cs +++ b/Tests/Common/Orders/OrderJsonConverterTests.cs @@ -193,6 +193,28 @@ public void DeserializesTrailingStopOrder(Symbols.SymbolsKey key) Assert.AreEqual(expected.TrailingAsPercentage, actual.TrailingAsPercentage); } + [TestCase(Symbols.SymbolsKey.SPY)] + [TestCase(Symbols.SymbolsKey.EURUSD)] + [TestCase(Symbols.SymbolsKey.BTCUSD)] + public void DeserializesTrailingStopLimitOrder(Symbols.SymbolsKey key) + { + var expected = new TrailingStopLimitOrder(Symbols.Lookup(key), 100, 210.10m, 211.10m, 0.1m, true, 1m, new DateTime(2015, 11, 23, 17, 15, 37), "now") + { + Id = 12345, + Price = 209.03m, + ContingentId = 123456, + BrokerId = new List { "727", "54970" } + }; + + var actual = TestOrderType(expected); + + Assert.AreEqual(expected.StopPrice, actual.StopPrice); + Assert.AreEqual(expected.TrailingAmount, actual.TrailingAmount); + Assert.AreEqual(expected.TrailingAsPercentage, actual.TrailingAsPercentage); + Assert.AreEqual(expected.LimitPrice, actual.LimitPrice); + Assert.AreEqual(expected.LimitOffset, actual.LimitOffset); + } + [TestCase(Symbols.SymbolsKey.SPY)] [TestCase(Symbols.SymbolsKey.EURUSD)] [TestCase(Symbols.SymbolsKey.BTCUSD)] diff --git a/Tests/Common/Orders/OrderTests.cs b/Tests/Common/Orders/OrderTests.cs index 4d5d65d81079..33b7173995f5 100644 --- a/Tests/Common/Orders/OrderTests.cs +++ b/Tests/Common/Orders/OrderTests.cs @@ -80,6 +80,52 @@ public void TrailingStopOrder_UpdatesStopPriceIfNecessary(OrderDirection directi } } + [TestCase(OrderDirection.Sell, 300, 0.1, true, 270)] + [TestCase(OrderDirection.Sell, 300, 30, false, 270)] + [TestCase(OrderDirection.Buy, 300, 0.1, true, 330)] + [TestCase(OrderDirection.Buy, 300, 30, false, 330)] + public void TrailingStopLimitorder_CalculatesStopPrice(OrderDirection direction, decimal marketPrice, decimal trailingAmount, + bool trailingAsPercentage, decimal expectedStopPrice) + { + var stopPrice = TrailingStopLimitOrder.CalculateStopPrice(marketPrice, trailingAmount, trailingAsPercentage, direction); + Assert.AreEqual(expectedStopPrice, stopPrice); + } + + [TestCase(OrderDirection.Sell, 300, 10, 290)] + [TestCase(OrderDirection.Buy, 300, 10, 310)] + public void TrailingStopLimitOrder_CalculatesLimitPrice(OrderDirection direction, decimal stopPrice, decimal limitOffset, + decimal expectedLimitPrice) + { + var limitPrice = TrailingStopLimitOrder.CalculateLimitPrice(stopPrice, limitOffset, direction); + Assert.AreEqual(expectedLimitPrice, limitPrice); + } + + [TestCase(OrderDirection.Sell, 269, 300, 0.1, true, 10, 270, 260)] + [TestCase(OrderDirection.Sell, 270, 300, 0.1, true, 10, null, null)] + [TestCase(OrderDirection.Sell, 269, 300, 30, false, 10, 270, 260)] + [TestCase(OrderDirection.Sell, 270, 300, 30, false, 10, null, null)] + [TestCase(OrderDirection.Buy, 331, 300, 0.1, true, 10, 330, 340)] + [TestCase(OrderDirection.Buy, 330, 300, 0.1, true, 10, null, null)] + [TestCase(OrderDirection.Buy, 331, 300, 30, false, 10, 330, 340)] + [TestCase(OrderDirection.Buy, 330, 300, 30, false, 10, null, null)] + public void TrailingStopLimitOrder_UpdatesStopAndLimitPricesIfNecessary(OrderDirection direction, decimal currentStopPrice, decimal marketPrice, + decimal trailingAmount, bool trailingAsPercentage, decimal limitOffset, decimal? expectedStopPrice, decimal? expectedLimitPrice) + { + var updated = TrailingStopLimitOrder.TryUpdateStopAndLimitPrices(marketPrice, currentStopPrice, trailingAmount, trailingAsPercentage, + limitOffset, direction, out var updatedStopPrice, out var updatedLimitPrice); + + if (expectedStopPrice.HasValue) + { + Assert.IsTrue(updated); + Assert.AreEqual(expectedStopPrice, updatedStopPrice); + Assert.AreEqual(expectedLimitPrice, updatedLimitPrice); + } + else + { + Assert.IsFalse(updated); + } + } + private static TestCaseData[] GetValueTestParameters() { const decimal delta = 1m; @@ -165,6 +211,10 @@ private static TestCaseData[] GetValueTestParameters() new ValueTestParameters("EquityShortTrailingStopOrderPriceMinusDelta", equity, new TrailingStopOrder(Symbols.SPY, -quantity, priceMinusDelta, 0.1m, true, time), -quantity*price), new ValueTestParameters("EquityLongLimitIfTouchedOrder", equity, new LimitIfTouchedOrder(Symbols.SPY, quantity, 1.5m*pricePlusDelta, priceMinusDelta, time), quantity*priceMinusDelta), new ValueTestParameters("EquityShortLimitIfTouchedOrder", equity, new LimitIfTouchedOrder(Symbols.SPY, -quantity, .5m*priceMinusDelta, pricePlusDelta, time), -quantity*pricePlusDelta), + new ValueTestParameters("EquityLongTrailingStopLimitOrderPriceMinusDelta", equity, new TrailingStopLimitOrder(Symbols.SPY, quantity, .5m*priceMinusDelta, priceMinusDelta, 0.1m, true, 0.1m, time), quantity*priceMinusDelta), + new ValueTestParameters("EquityLongTrailingStopLimitOrderPricePlusDelta", equity, new TrailingStopLimitOrder(Symbols.SPY, quantity, .5m*pricePlusDelta, pricePlusDelta, 0.1m, true, 0.1m, time), quantity*price), + new ValueTestParameters("EquityShortTrailingStopLimitOrderPricePlusDelta", equity, new TrailingStopLimitOrder(Symbols.SPY, -quantity, 1.5m*pricePlusDelta, pricePlusDelta, 0.1m, true, 0.1m, time), -quantity*pricePlusDelta), + new ValueTestParameters("EquityShortTrailingStopLimitOrderPriceMinusDelta", equity, new TrailingStopLimitOrder(Symbols.SPY, -quantity, 1.5m*priceMinusDelta, priceMinusDelta, 0.1m, true, 0.1m, time), -quantity*price), // forex orders new ValueTestParameters("ForexLongMarketOrder", forex, new MarketOrder(Symbols.EURGBP, quantity, time), quantity*price*forex.QuoteCurrency.ConversionRate), @@ -183,6 +233,10 @@ private static TestCaseData[] GetValueTestParameters() new ValueTestParameters("ForexShortTrailingStopOrderPriceMinusDelta", forex, new TrailingStopOrder(Symbols.EURGBP, -quantity, priceMinusDelta, 0.1m, true, time), -quantity*price*forex.QuoteCurrency.ConversionRate), new ValueTestParameters("ForexLongLimitIfTouchedOrder", forex, new LimitIfTouchedOrder(Symbols.EURGBP, quantity,1.5m*priceMinusDelta, priceMinusDelta, time), quantity*priceMinusDelta*forex.QuoteCurrency.ConversionRate), new ValueTestParameters("ForexShortLimitIfTouchedOrder", forex, new LimitIfTouchedOrder(Symbols.EURGBP, -quantity, .5m*pricePlusDelta, pricePlusDelta, time), -quantity*pricePlusDelta*forex.QuoteCurrency.ConversionRate), + new ValueTestParameters("ForexLongTrailingStopLimitOrderPriceMinusDelta", forex, new TrailingStopLimitOrder(Symbols.EURGBP, quantity, .5m*priceMinusDelta, priceMinusDelta, 0.1m, true, 0.1m, time), quantity*priceMinusDelta*forex.QuoteCurrency.ConversionRate), + new ValueTestParameters("ForexLongTrailingStopLimitOrderPricePlusDelta", forex, new TrailingStopLimitOrder(Symbols.EURGBP, quantity, .5m*pricePlusDelta, pricePlusDelta, 0.1m, true, 0.1m, time), quantity*price*forex.QuoteCurrency.ConversionRate), + new ValueTestParameters("ForexShortTrailingStopLimitOrderPricePlusDelta", forex, new TrailingStopLimitOrder(Symbols.EURGBP, -quantity, 1.5m*pricePlusDelta, pricePlusDelta, 0.1m, true, 0.1m, time), -quantity*pricePlusDelta*forex.QuoteCurrency.ConversionRate), + new ValueTestParameters("ForexShortTrailingStopLimitOrderPriceMinusDelta", forex, new TrailingStopLimitOrder(Symbols.EURGBP, -quantity, 1.5m*priceMinusDelta, priceMinusDelta, 0.1m, true, 0.1m, time), -quantity*price*forex.QuoteCurrency.ConversionRate), // cfd orders new ValueTestParameters("CfdLongMarketOrder", cfd, new MarketOrder(Symbols.DE10YBEUR, quantity, time), quantity*price*multiplierTimesConversionRate), @@ -201,7 +255,10 @@ private static TestCaseData[] GetValueTestParameters() new ValueTestParameters("CfdShortTrailingStopOrderPriceMinusDelta", cfd, new TrailingStopOrder(Symbols.DE10YBEUR, -quantity, priceMinusDelta, 0.1m, true, time), -quantity*price*multiplierTimesConversionRate), new ValueTestParameters("CfdShortLimitIfTouchedOrder", cfd, new LimitIfTouchedOrder(Symbols.DE10YBEUR, -quantity, 1.5m*pricePlusDelta, pricePlusDelta, time), -quantity*pricePlusDelta*multiplierTimesConversionRate), new ValueTestParameters("CfdLongLimitIfTouchedOrder", cfd, new LimitIfTouchedOrder(Symbols.DE10YBEUR, quantity,.5m*priceMinusDelta, priceMinusDelta, time), quantity*priceMinusDelta*multiplierTimesConversionRate), - + new ValueTestParameters("CfdLongTrailingStopLimitOrderPriceMinusDelta", cfd, new TrailingStopLimitOrder(Symbols.SPY, quantity, .5m*priceMinusDelta, priceMinusDelta, 0.1m, true, 0.1m, time), quantity*priceMinusDelta*multiplierTimesConversionRate), + new ValueTestParameters("CfdLongTrailingStopLimitOrderPricePlusDelta", cfd, new TrailingStopLimitOrder(Symbols.SPY, quantity, .5m*pricePlusDelta, pricePlusDelta, 0.1m, true, 0.1m, time), quantity*price*multiplierTimesConversionRate), + new ValueTestParameters("CfdShortTrailingStopLimitOrderPricePlusDelta", cfd, new TrailingStopLimitOrder(Symbols.SPY, -quantity, 1.5m*pricePlusDelta, pricePlusDelta, 0.1m, true, 0.1m, time), -quantity*pricePlusDelta*multiplierTimesConversionRate), + new ValueTestParameters("CfdShortTrailingStopLimitOrderPriceMinusDelta", cfd, new TrailingStopLimitOrder(Symbols.SPY, -quantity, 1.5m*priceMinusDelta, priceMinusDelta, 0.1m, true, 0.1m, time), -quantity*price*multiplierTimesConversionRate), // equity/index option orders new ValueTestParameters("OptionLongMarketOrder", option, new MarketOrder(Symbols.SPY_P_192_Feb19_2016, quantity, time), quantity*price), @@ -220,6 +277,10 @@ private static TestCaseData[] GetValueTestParameters() new ValueTestParameters("OptionShortTrailingStopOrderPriceMinusDelta", option, new TrailingStopOrder(Symbols.SPY_P_192_Feb19_2016, -quantity, priceMinusDelta, 0.1m, true, time), -quantity*price), new ValueTestParameters("OptionShortLimitIfTouchedOrder", option, new LimitIfTouchedOrder(Symbols.SPY_P_192_Feb19_2016, -quantity, 1.5m*pricePlusDelta, pricePlusDelta, time), -quantity*pricePlusDelta), new ValueTestParameters("OptionLongLimitIfTouchedOrder", option, new LimitIfTouchedOrder(Symbols.SPY_P_192_Feb19_2016, quantity,.5m*priceMinusDelta, priceMinusDelta, time), quantity*priceMinusDelta), + new ValueTestParameters("OptionLongTrailingStopLimitOrderPriceMinusDelta", option, new TrailingStopLimitOrder(Symbols.SPY_P_192_Feb19_2016, quantity, .5m*priceMinusDelta, priceMinusDelta, 0.1m, true, 0.1m, time), quantity*priceMinusDelta), + new ValueTestParameters("OptionLongTrailingStopLimitOrderPricePlusDelta", option, new TrailingStopLimitOrder(Symbols.SPY_P_192_Feb19_2016, quantity, .5m*pricePlusDelta, pricePlusDelta, 0.1m, true, 0.1m, time), quantity*price), + new ValueTestParameters("OptionShortTrailingStopLimitOrderPricePlusDelta", option, new TrailingStopLimitOrder(Symbols.SPY_P_192_Feb19_2016, -quantity, 1.5m*pricePlusDelta, pricePlusDelta, 0.1m, true, 0.1m, time), -quantity*pricePlusDelta), + new ValueTestParameters("OptionShortTrailingStopLimitOrderPriceMinusDelta", option, new TrailingStopLimitOrder(Symbols.SPY_P_192_Feb19_2016, -quantity, 1.5m*priceMinusDelta, priceMinusDelta, 0.1m, true, 0.1m, time), -quantity*price), new ValueTestParameters("OptionExerciseOrderPut", option, new OptionExerciseOrder(Symbols.SPY_P_192_Feb19_2016, quantity, time), quantity*option.Symbol.ID.StrikePrice), new ValueTestParameters("OptionAssignmentOrderPut", option, new OptionExerciseOrder(Symbols.SPY_P_192_Feb19_2016, -quantity, time), -quantity*option.Symbol.ID.StrikePrice), diff --git a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs index 4bf3e01187de..3861d77fbcbe 100644 --- a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs +++ b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs @@ -81,6 +81,7 @@ private static SubmitOrderRequest MakeOrderRequest(Security security, OrderType OrderType.ComboLimit => new SubmitOrderRequest(OrderType.ComboLimit, security.Type, security.Symbol, 1, 295, 0, date, "", groupOrderManager: groupOrderManager), OrderType.ComboLegLimit => new SubmitOrderRequest(OrderType.ComboLegLimit, security.Type, security.Symbol, 1, 295, 0, date, "", groupOrderManager: groupOrderManager), OrderType.TrailingStop => new SubmitOrderRequest(OrderType.TrailingStop, security.Type, security.Symbol, 1, 305, 0, 305, date, ""), + OrderType.TrailingStopLimit => new SubmitOrderRequest(OrderType.TrailingStopLimit, security.Type, security.Symbol, 1, 305, 306, date, ""), _ => throw new ArgumentOutOfRangeException(nameof(orderType), orderType, null) }; } @@ -508,6 +509,47 @@ public void TrailingStopOrderPriceIsRounded([Values] bool trailingAsPercentage) Assert.AreEqual(trailingAsPercentage ? 20.12121212m : 20.12, orderTicket.Get(OrderField.TrailingAmount)); } + [Test] + public void TrailingStopLimitOrderPriceIsRounded([Values] bool trailingAsPercentage) + { + //Initialize the transaction handler + var transactionHandler = new TestBrokerageTransactionHandler(); + using var brokerage = new BacktestingBrokerage(_algorithm); + transactionHandler.Initialize(_algorithm, brokerage, new BacktestingResultHandler()); + + // Create the order + _algorithm.SetBrokerageModel(new DefaultBrokerageModel()); + var security = _algorithm.AddEquity("SPY"); + security.PriceVariationModel = new EquityPriceVariationModel(); + var price = 330.12129m; + security.SetMarketPrice(new Tick(DateTime.Now, security.Symbol, price, price, price)); + var orderRequest = new SubmitOrderRequest(OrderType.TrailingStopLimit, security.Type, security.Symbol, 100, stopPrice: 350.12121212m, + limitPrice: 351.12121212m, 0, trailingAmount: 20.12121212m, trailingAsPercentage, limitOffset: 1.12121212m, DateTime.Now, ""); + + // Mock the order processor + var orderProcessorMock = new Mock(); + orderProcessorMock.Setup(m => m.GetOrderTicket(It.IsAny())).Returns(new OrderTicket(_algorithm.Transactions, orderRequest)); + _algorithm.Transactions.SetOrderProcessor(orderProcessorMock.Object); + + // Act + var orderTicket = transactionHandler.Process(orderRequest); + Assert.IsTrue(orderTicket.Status == OrderStatus.New); + transactionHandler.HandleOrderRequest(orderRequest); + + // Assert + Assert.IsTrue(orderRequest.Response.IsProcessed); + Assert.IsTrue(orderRequest.Response.IsSuccess); + Assert.IsTrue(orderTicket.Status == OrderStatus.Submitted); + // 350.12121212 after round becomes 300.12 + Assert.AreEqual(350.12m, orderTicket.Get(OrderField.StopPrice)); + // 351.12121212 after round becomes 301.12 + Assert.AreEqual(351.12m, orderTicket.Get(OrderField.LimitPrice)); + // If trailing amount is not a price, it's not rounded + Assert.AreEqual(trailingAsPercentage ? 20.12121212m : 20.12, orderTicket.Get(OrderField.TrailingAmount)); + // 1.12121212 after round becomes 1.12 + Assert.AreEqual(1.12m, orderTicket.Get(OrderField.LimitOffset)); + } + // 331.12121212m after round becomes 331.12m, the smallest price variation is 0.01 - index. For index options it is 0.1 [TestCase(OrderType.ComboLimit, 300.12121212, 0, 0, 300.12, 300.12)] [TestCase(OrderType.ComboLegLimit, 0, 1.12121212, 300.13131313, 1.12, 300.1)] @@ -2315,6 +2357,7 @@ public void OrderPriceAdjustmentModeIsSetWhenAddingOpenOrder(DataNormalizationMo new TestCaseData(OrderType.ComboLimit, false), new TestCaseData(OrderType.ComboLegLimit, false), new TestCaseData(OrderType.TrailingStop, false), + new TestCaseData(OrderType.TrailingStopLimit, false), // Only market orders are supported for this test new TestCaseData(OrderType.Market, true), }; @@ -2345,6 +2388,8 @@ private static Order GetOrder(OrderType type, Symbol symbol) return new ComboLegLimitOrder(symbol, 100, 100m, new DateTime(2024, 01, 19, 12, 0, 0), new GroupOrderManager(1, 1, 10)); case OrderType.TrailingStop: return new TrailingStopOrder(symbol, 100, 100m, 100m, false, new DateTime(2024, 01, 19, 12, 0, 0)); + case OrderType.TrailingStopLimit: + return new TrailingStopLimitOrder(symbol, 100, 100m, 101m, 1m, false, 1m, new DateTime(2024, 01, 19, 12, 0, 0)); default: throw new ArgumentOutOfRangeException(nameof(type), type, null); }