Skip to content

Commit

Permalink
Fix updating StopLimitOrder.StopTriggered flag globally (#7407)
Browse files Browse the repository at this point in the history
  • Loading branch information
jhonabreul authored Aug 8, 2023
1 parent 6e9df75 commit 76b1916
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 11 deletions.
183 changes: 183 additions & 0 deletions Algorithm.CSharp/StopLimitOrderRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Basic algorithm demonstrating how to place stop limit orders.
/// </summary>
/// <meta name="tag" content="trading and orders" />
/// <meta name="tag" content="placing orders" />
/// <meta name="tag" content="stop limit order"/>
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); } }

/// <summary>
/// Initialize the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
/// </summary>
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);
}

/// <summary>
/// OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
/// </summary>
/// <param name="data">Slice object keyed by symbol containing the stock data</param>
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.");
}
}

/// <summary>
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
/// </summary>
public bool CanRunLocally { get; } = true;

/// <summary>
/// This is used by the regression test system to indicate which languages this algorithm is written in.
/// </summary>
public Language[] Languages { get; } = { Language.CSharp, Language.Python };

/// <summary>
/// Data Points count of all timeslices of algorithm
/// </summary>
public long DataPoints => 8062;

/// <summary>
/// Data Points count of the algorithm history
/// </summary>
public int AlgorithmHistoryDataPoints => 0;

/// <summary>
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"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"}
};
}
}
77 changes: 77 additions & 0 deletions Algorithm.Python/StopLimitOrderRegressionAlgorithm.py
Original file line number Diff line number Diff line change
@@ -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 *

### <summary>
### Basic algorithm demonstrating how to place stop limit orders.
### </summary>
### <meta name="tag" content="trading and orders" />
### <meta name="tag" content="placing orders" />
### <meta name="tag" content="stop limit order"/>
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)
11 changes: 8 additions & 3 deletions Brokerages/Backtesting/BacktestingBrokerage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -435,10 +435,15 @@ public virtual void Scan()
/// </summary>
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;
}
}

Expand Down
12 changes: 10 additions & 2 deletions Common/Orders/Fills/EquityFillModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions Common/Orders/Fills/FillModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions Common/Orders/OrderUpdateEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,10 @@ public class OrderUpdateEvent
/// The updated stop price for a <see cref="TrailingStopOrder"/>
/// </summary>
public decimal TrailingStopPrice { get; set; }

/// <summary>
/// Flag indicating whether stop has been triggered for a <see cref="StopLimitOrder"/>
/// </summary>
public bool StopTriggered { get; set; }
}
}
2 changes: 1 addition & 1 deletion Common/Orders/StopLimitOrder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public override string ToString()
/// <returns>A copy of this order</returns>
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;
}
Expand Down
11 changes: 8 additions & 3 deletions Engine/TransactionHandlers/BrokerageTransactionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down

0 comments on commit 76b1916

Please sign in to comment.