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;
}
}