diff --git a/Algorithm.CSharp/StopLimitOrderRegressionAlgorithm.cs b/Algorithm.CSharp/StopLimitOrderRegressionAlgorithm.cs new file mode 100644 index 000000000000..1bd7f7195785 --- /dev/null +++ b/Algorithm.CSharp/StopLimitOrderRegressionAlgorithm.cs @@ -0,0 +1,183 @@ + +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; + +using QuantConnect.Data; +using QuantConnect.Indicators; +using QuantConnect.Interfaces; +using QuantConnect.Orders; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Basic algorithm demonstrating how to place stop limit orders. + /// + /// + /// + /// + public class StopLimitOrderRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private Symbol _symbol; + private OrderTicket _buyOrderTicket; + private OrderTicket _sellOrderTicket; + + private const decimal _tolerance = 0.001m; + private const int _fastPeriod = 30; + private const int _slowPeriod = 60; + + 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 (!IsReady) + { + return; + } + + var security = Securities[_symbol]; + if (_buyOrderTicket == null && TrendIsUp) + { + _buyOrderTicket = StopLimitOrder(_symbol, 100, stopPrice: security.High * 1.10m, limitPrice: security.High * 1.11m); + } + else if (_buyOrderTicket.Status == OrderStatus.Filled && _sellOrderTicket == null && TrendIsDown) + { + _sellOrderTicket = StopLimitOrder(_symbol, -100, stopPrice: security.Low * 0.99m, limitPrice: security.Low * 0.98m); + } + } + + public override void OnOrderEvent(OrderEvent orderEvent) + { + if (orderEvent.Status == OrderStatus.Filled) + { + var order = Transactions.GetOrderById(orderEvent.OrderId); + if (!((StopLimitOrder)order).StopTriggered) + { + throw new Exception("StopLimitOrder StopTriggered should haven been set if the order filled."); + } + + if (orderEvent.Direction == OrderDirection.Buy) + { + var limitPrice = _buyOrderTicket.Get(OrderField.LimitPrice); + if (orderEvent.FillPrice > limitPrice) + { + throw new Exception($@"Buy 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 Exception($@"Sell 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 Exception("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 Exception("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 Language[] Languages { get; } = { Language.CSharp, Language.Python }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 8062; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 0; + + /// + /// 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 Trades", "2"}, + {"Average Win", "1.44%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0.359%"}, + {"Drawdown", "1.500%"}, + {"Expectancy", "0"}, + {"Net Profit", "1.445%"}, + {"Sharpe Ratio", "0.332"}, + {"Probabilistic Sharpe Ratio", "5.635%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "100%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "-0.001"}, + {"Beta", "0.03"}, + {"Annual Standard Deviation", "0.008"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-0.96"}, + {"Tracking Error", "0.104"}, + {"Treynor Ratio", "0.083"}, + {"Total Fees", "$2.00"}, + {"Estimated Strategy Capacity", "$2700000000.00"}, + {"Lowest Capacity Asset", "SPY R735QTJ8XC9X"}, + {"Portfolio Turnover", "0.02%"}, + {"OrderListHash", "4269e401ce8ef69539bedb6b8f8a6499"} + }; + } +} diff --git a/Algorithm.Python/StopLimitOrderRegressionAlgorithm.py b/Algorithm.Python/StopLimitOrderRegressionAlgorithm.py new file mode 100644 index 000000000000..b4b92558645b --- /dev/null +++ b/Algorithm.Python/StopLimitOrderRegressionAlgorithm.py @@ -0,0 +1,77 @@ +# 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 stop limit orders. +### +### +### +### +class StopLimitOrderRegressionAlgorithm(QCAlgorithm): + '''Basic algorithm demonstrating how to place stop limit orders.''' + + Tolerance = 0.001 + FastPeriod = 30 + SlowPeriod = 60 + + def Initialize(self): + self.SetStartDate(2013, 1, 1) + self.SetEndDate(2017, 1, 1) + self.SetCash(100000) + + self._symbol = self.AddEquity("SPY", Resolution.Daily).Symbol + + self._fast = self.EMA(self._symbol, self.FastPeriod, Resolution.Daily) + self._slow = self.EMA(self._symbol, self.SlowPeriod, Resolution.Daily) + + self._buyOrderTicket: OrderTicket = None + self._sellOrderTicket: OrderTicket = None + self._previousSlice: Slice = None + + def OnData(self, slice: Slice): + if not self.IsReady(): + return + + security = self.Securities[self._symbol] + if self._buyOrderTicket is None and self.TrendIsUp(): + self._buyOrderTicket = self.StopLimitOrder(self._symbol, 100, stopPrice=security.High * 1.10, limitPrice=security.High * 1.11) + elif self._buyOrderTicket.Status == OrderStatus.Filled and self._sellOrderTicket is None and self.TrendIsDown(): + self._sellOrderTicket = self.StopLimitOrder(self._symbol, -100, stopPrice=security.Low * 0.99, limitPrice=security.Low * 0.98) + + def OnOrderEvent(self, orderEvent: OrderEvent): + if orderEvent.Status == OrderStatus.Filled: + order: StopLimitOrder = self.Transactions.GetOrderById(orderEvent.OrderId) + if not order.StopTriggered: + raise Exception("StopLimitOrder StopTriggered should haven been set if the order filled.") + + if orderEvent.Direction == OrderDirection.Buy: + limitPrice = self._buyOrderTicket.Get(OrderField.LimitPrice) + if orderEvent.FillPrice > limitPrice: + raise Exception(f"Buy stop limit order should have filled with price less than or equal to the limit price {limitPrice}. " + f"Fill price: {orderEvent.FillPrice}") + else: + limitPrice = self._sellOrderTicket.Get(OrderField.LimitPrice) + if orderEvent.FillPrice < limitPrice: + raise Exception(f"Sell stop limit order should have filled with price greater than or equal to the limit price {limitPrice}. " + f"Fill price: {orderEvent.FillPrice}") + + def IsReady(self): + return self._fast.IsReady and self._slow.IsReady + + def TrendIsUp(self): + return self.IsReady() and self._fast.Current.Value > self._slow.Current.Value * (1 + self.Tolerance) + + def TrendIsDown(self): + return self.IsReady() and self._fast.Current.Value < self._slow.Current.Value * (1 + self.Tolerance) diff --git a/Brokerages/Backtesting/BacktestingBrokerage.cs b/Brokerages/Backtesting/BacktestingBrokerage.cs index 3f333d215de0..f7d48e84779c 100644 --- a/Brokerages/Backtesting/BacktestingBrokerage.cs +++ b/Brokerages/Backtesting/BacktestingBrokerage.cs @@ -435,10 +435,15 @@ public virtual void Scan() /// private void OnOrderUpdated(Order order) { - // Only trailing stop orders updates are supported for now - if (order.Type == OrderType.TrailingStop) + switch (order.Type) { - OnOrderUpdated(new OrderUpdateEvent { OrderId = order.Id, TrailingStopPrice = ((TrailingStopOrder)order).StopPrice }); + case OrderType.TrailingStop: + OnOrderUpdated(new OrderUpdateEvent { OrderId = order.Id, TrailingStopPrice = ((TrailingStopOrder)order).StopPrice }); + break; + + case OrderType.StopLimit: + OnOrderUpdated(new OrderUpdateEvent { OrderId = order.Id, StopTriggered = ((StopLimitOrder)order).StopTriggered }); + break; } } diff --git a/Common/Orders/Fills/EquityFillModel.cs b/Common/Orders/Fills/EquityFillModel.cs index 3de47da98c46..19cfbbc1884f 100644 --- a/Common/Orders/Fills/EquityFillModel.cs +++ b/Common/Orders/Fills/EquityFillModel.cs @@ -288,7 +288,11 @@ public override OrderEvent StopLimitFill(Security asset, StopLimitOrder order) //-> 1.2 Buy Stop: If Price Above Setpoint, Buy: if (prices.High > order.StopPrice || order.StopTriggered) { - order.StopTriggered = true; + 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. @@ -306,7 +310,11 @@ public override OrderEvent StopLimitFill(Security asset, StopLimitOrder order) //-> 1.1 Sell Stop: If Price below setpoint, Sell: if (prices.Low < order.StopPrice || order.StopTriggered) { - order.StopTriggered = true; + 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. diff --git a/Common/Orders/Fills/FillModel.cs b/Common/Orders/Fills/FillModel.cs index 769192430aa8..eccd04bbb6d8 100644 --- a/Common/Orders/Fills/FillModel.cs +++ b/Common/Orders/Fills/FillModel.cs @@ -491,7 +491,11 @@ public virtual OrderEvent StopLimitFill(Security asset, StopLimitOrder order) //-> 1.2 Buy Stop: If Price Above Setpoint, Buy: if (prices.High > order.StopPrice || order.StopTriggered) { - order.StopTriggered = true; + 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. @@ -509,7 +513,11 @@ public virtual OrderEvent StopLimitFill(Security asset, StopLimitOrder order) //-> 1.1 Sell Stop: If Price below setpoint, Sell: if (prices.Low < order.StopPrice || order.StopTriggered) { - order.StopTriggered = true; + 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. diff --git a/Common/Orders/OrderUpdateEvent.cs b/Common/Orders/OrderUpdateEvent.cs index f1062032bf0d..04724891c35a 100644 --- a/Common/Orders/OrderUpdateEvent.cs +++ b/Common/Orders/OrderUpdateEvent.cs @@ -30,5 +30,10 @@ public class OrderUpdateEvent /// The updated stop price for a /// public decimal TrailingStopPrice { get; set; } + + /// + /// Flag indicating whether stop has been triggered for a + /// + public bool StopTriggered { get; set; } } } diff --git a/Common/Orders/StopLimitOrder.cs b/Common/Orders/StopLimitOrder.cs index be499921fd0f..2a43eb145a1b 100644 --- a/Common/Orders/StopLimitOrder.cs +++ b/Common/Orders/StopLimitOrder.cs @@ -133,7 +133,7 @@ public override string ToString() /// A copy of this order public override Order Clone() { - var order = new StopLimitOrder {StopPrice = StopPrice, LimitPrice = LimitPrice}; + var order = new StopLimitOrder { StopPrice = StopPrice, LimitPrice = LimitPrice, StopTriggered = StopTriggered }; CopyTo(order); return order; } diff --git a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs index dfd2a2cefbe0..8e0e0b25a51c 100644 --- a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs +++ b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs @@ -1252,10 +1252,15 @@ private void HandleOrderUpdated(OrderUpdateEvent e) return; } - // Only trailing stop orders updates are supported for now - if (order.Type == OrderType.TrailingStop) + switch (order.Type) { - ((TrailingStopOrder)order).StopPrice = e.TrailingStopPrice; + case OrderType.TrailingStop: + ((TrailingStopOrder)order).StopPrice = e.TrailingStopPrice; + break; + + case OrderType.StopLimit: + ((StopLimitOrder)order).StopTriggered = e.StopTriggered; + break; } }