Skip to content

Commit 0c77be0

Browse files
authored
Allow closed orders tag update (#7016)
* Allow closed orders tag update * Minor changes
1 parent 47bfdae commit 0c77be0

5 files changed

Lines changed: 376 additions & 8 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
20+
using QuantConnect.Data;
21+
using QuantConnect.Interfaces;
22+
using QuantConnect.Orders;
23+
24+
namespace QuantConnect.Algorithm.CSharp
25+
{
26+
/// <summary>
27+
/// Algorithm asserting that closed orders can be updated with a new tag
28+
/// </summary>
29+
public class CompleteOrderTagUpdateAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
30+
{
31+
private static string TagAfterFill = "This is the tag set after order was filled.";
32+
private static string TagAfterCanceled = "This is the tag set after order was canceled.";
33+
34+
private OrderTicket _marketOrderTicket;
35+
private OrderTicket _limitOrderTicket;
36+
37+
private int _quantity = 100;
38+
39+
private Symbol _spy;
40+
41+
public override void Initialize()
42+
{
43+
SetStartDate(2013, 10, 07);
44+
SetEndDate(2013, 10, 11);
45+
SetCash(100000);
46+
47+
_spy = AddEquity("SPY", Resolution.Minute).Symbol;
48+
}
49+
50+
public override void OnData(Slice data)
51+
{
52+
if (!Portfolio.Invested)
53+
{
54+
// a limit order to test the tag update after order was canceled
55+
if (_limitOrderTicket == null)
56+
{
57+
// low price, we don't want it to fill since we are canceling it
58+
_limitOrderTicket = LimitOrder(_spy, 100, Securities[_spy].Price * 0.1m);
59+
_limitOrderTicket.Cancel();
60+
}
61+
// a market order to test the tag update after order was filled
62+
else
63+
{
64+
Buy(_spy, _quantity);
65+
}
66+
}
67+
}
68+
69+
public override void OnOrderEvent(OrderEvent orderEvent)
70+
{
71+
if (orderEvent.Status == OrderStatus.Canceled)
72+
{
73+
if (orderEvent.OrderId != _limitOrderTicket.OrderId)
74+
{
75+
throw new Exception("The only canceled order should have been the limit order.");
76+
}
77+
78+
// update canceled order tag
79+
UpdateOrderTag(_limitOrderTicket, TagAfterCanceled, "Error updating order tag after canceled");
80+
}
81+
else if (orderEvent.Status == OrderStatus.Filled)
82+
{
83+
_marketOrderTicket = Transactions.GetOrderTickets(x => x.OrderType == OrderType.Market).Single();
84+
if (orderEvent.OrderId != _marketOrderTicket.OrderId)
85+
{
86+
throw new Exception("The only filled order should have been the market order.");
87+
}
88+
89+
// try to update a field other than the tag
90+
var updateFields = new UpdateOrderFields();
91+
updateFields.Quantity = 50;
92+
var response = _marketOrderTicket.Update(updateFields);
93+
if (response.IsSuccess)
94+
{
95+
throw new Exception("The market order quantity should not have been updated.");
96+
}
97+
98+
// update filled order tag
99+
UpdateOrderTag(_marketOrderTicket, TagAfterFill, "Error updating order tag after fill");
100+
}
101+
}
102+
103+
public override void OnEndOfAlgorithm()
104+
{
105+
// check the filled order
106+
AssertOrderTagUpdate(_marketOrderTicket, TagAfterFill, "filled");
107+
if (_marketOrderTicket.Quantity != _quantity || _marketOrderTicket.QuantityFilled != _quantity)
108+
{
109+
throw new Exception("The market order quantity should not have been updated.");
110+
}
111+
112+
// check the canceled order
113+
AssertOrderTagUpdate(_limitOrderTicket, TagAfterCanceled, "canceled");
114+
}
115+
116+
private void AssertOrderTagUpdate(OrderTicket ticket, string expectedTag, string orderAction)
117+
{
118+
if (ticket == null)
119+
{
120+
throw new Exception($"The order ticket was not set for the {orderAction} order");
121+
}
122+
123+
if (ticket.Tag != expectedTag)
124+
{
125+
throw new Exception($"Order ticket tag was not updated after order was {orderAction}");
126+
}
127+
128+
var order = Transactions.GetOrderById(ticket.OrderId);
129+
if (order.Tag != expectedTag)
130+
{
131+
throw new Exception($"Order tag was not updated after order was {orderAction}");
132+
}
133+
}
134+
135+
private static void UpdateOrderTag(OrderTicket ticket, string tag, string errorMessagePrefix)
136+
{
137+
var updateFields = new UpdateOrderFields();
138+
updateFields.Tag = tag;
139+
var response = ticket.Update(updateFields);
140+
141+
if (response.IsError)
142+
{
143+
throw new Exception($"{errorMessagePrefix}: {response.ErrorMessage}");
144+
}
145+
}
146+
147+
/// <summary>
148+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
149+
/// </summary>
150+
public bool CanRunLocally { get; } = true;
151+
152+
/// <summary>
153+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
154+
/// </summary>
155+
public Language[] Languages { get; } = { Language.CSharp, Language.Python };
156+
157+
/// <summary>
158+
/// Data Points count of all timeslices of algorithm
159+
/// </summary>
160+
public long DataPoints => 3943;
161+
162+
/// <summary>
163+
/// Data Points count of the algorithm history
164+
/// </summary>
165+
public int AlgorithmHistoryDataPoints => 0;
166+
167+
/// <summary>
168+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
169+
/// </summary>
170+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
171+
{
172+
{"Total Trades", "1"},
173+
{"Average Win", "0%"},
174+
{"Average Loss", "0%"},
175+
{"Compounding Annual Return", "21.706%"},
176+
{"Drawdown", "0.300%"},
177+
{"Expectancy", "0"},
178+
{"Net Profit", "0.251%"},
179+
{"Sharpe Ratio", "5.312"},
180+
{"Probabilistic Sharpe Ratio", "67.483%"},
181+
{"Loss Rate", "0%"},
182+
{"Win Rate", "0%"},
183+
{"Profit-Loss Ratio", "0"},
184+
{"Alpha", "-0.115"},
185+
{"Beta", "0.144"},
186+
{"Annual Standard Deviation", "0.032"},
187+
{"Annual Variance", "0.001"},
188+
{"Information Ratio", "-9.515"},
189+
{"Tracking Error", "0.191"},
190+
{"Treynor Ratio", "1.182"},
191+
{"Total Fees", "$1.00"},
192+
{"Estimated Strategy Capacity", "$210000000.00"},
193+
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
194+
{"Fitness Score", "0.036"},
195+
{"Kelly Criterion Estimate", "0"},
196+
{"Kelly Criterion Probability Value", "0"},
197+
{"Sortino Ratio", "79228162514264337593543950335"},
198+
{"Return Over Maximum Drawdown", "72.372"},
199+
{"Portfolio Turnover", "0.036"},
200+
{"Total Insights Generated", "0"},
201+
{"Total Insights Closed", "0"},
202+
{"Total Insights Analysis Completed", "0"},
203+
{"Long Insight Count", "0"},
204+
{"Short Insight Count", "0"},
205+
{"Long/Short Ratio", "100%"},
206+
{"Estimated Monthly Alpha Value", "$0"},
207+
{"Total Accumulated Estimated Alpha Value", "$0"},
208+
{"Mean Population Estimated Insight Value", "$0"},
209+
{"Mean Population Direction", "0%"},
210+
{"Mean Population Magnitude", "0%"},
211+
{"Rolling Averaged Population Direction", "0%"},
212+
{"Rolling Averaged Population Magnitude", "0%"},
213+
{"OrderListHash", "cbf413671bbd919d0113b4f2a6a9608f"}
214+
};
215+
}
216+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
2+
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
from AlgorithmImports import *
15+
16+
### <summary>
17+
### Algorithm asserting that closed orders can be updated with a new tag
18+
### </summary>
19+
class CompleteOrderTagUpdateAlgorithm(QCAlgorithm):
20+
21+
TagAfterFill = "This is the tag set after order was filled.";
22+
TagAfterCanceled = "This is the tag set after order was canceled.";
23+
24+
def Initialize(self) -> None:
25+
self.SetStartDate(2013,10, 7)
26+
self.SetEndDate(2013,10,11)
27+
self.SetCash(100000)
28+
29+
self._spy = self.AddEquity("SPY", Resolution.Minute).Symbol
30+
31+
self._marketOrderTicket = None
32+
self._limitOrderTicket = None
33+
34+
self._quantity = 100
35+
36+
def OnData(self, data: Slice) -> None:
37+
if not self.Portfolio.Invested:
38+
if self._limitOrderTicket is None:
39+
# a limit order to test the tag update after order was canceled.
40+
41+
# low price, we don't want it to fill since we are canceling it
42+
self._limitOrderTicket = self.LimitOrder(self._spy, 100, self.Securities[self._spy].Price * 0.1)
43+
self._limitOrderTicket.Cancel()
44+
else:
45+
# a market order to test the tag update after order was filled.
46+
self.Buy(self._spy, self._quantity)
47+
48+
def OnOrderEvent(self, orderEvent: OrderEvent) -> None:
49+
if orderEvent.Status == OrderStatus.Canceled:
50+
if orderEvent.OrderId != self._limitOrderTicket.OrderId:
51+
raise Exception("The only canceled order should have been the limit order.")
52+
53+
# update canceled order tag
54+
self.UpdateOrderTag(self._limitOrderTicket, self.TagAfterCanceled, "Error updating order tag after canceled")
55+
elif orderEvent.Status == OrderStatus.Filled:
56+
self._marketOrderTicket = list(self.Transactions.GetOrderTickets(lambda x: x.OrderType == OrderType.Market))[0]
57+
if orderEvent.OrderId != self._marketOrderTicket.OrderId:
58+
raise Exception("The only filled order should have been the market order.")
59+
60+
# update filled order tag
61+
self.UpdateOrderTag(self._marketOrderTicket, self.TagAfterFill, "Error updating order tag after fill")
62+
63+
def OnEndOfAlgorithm(self) -> None:
64+
# check the filled order
65+
self.AssertOrderTagUpdate(self._marketOrderTicket, self.TagAfterFill, "filled")
66+
if self._marketOrderTicket.Quantity != self._quantity or self._marketOrderTicket.QuantityFilled != self._quantity:
67+
raise Exception("The market order quantity should not have been updated.")
68+
69+
# check the canceled order
70+
self.AssertOrderTagUpdate(self._limitOrderTicket, self.TagAfterCanceled, "canceled")
71+
72+
def AssertOrderTagUpdate(self, ticket: OrderTicket, expectedTag: str, orderAction: str) -> None:
73+
if ticket is None:
74+
raise Exception(f"The order ticket was not set for the {orderAction} order")
75+
76+
if ticket.Tag != expectedTag:
77+
raise Exception(f"Order ticket tag was not updated after order was {orderAction}")
78+
79+
order = self.Transactions.GetOrderById(ticket.OrderId)
80+
if order.Tag != expectedTag:
81+
raise Exception(f"Order tag was not updated after order was {orderAction}")
82+
83+
def UpdateOrderTag(self, ticket: OrderTicket, tag: str, errorMessagePrefix: str) -> None:
84+
updateFields = UpdateOrderFields()
85+
updateFields.Tag = tag
86+
response = ticket.Update(updateFields)
87+
88+
if response.IsError:
89+
raise Exception(f"{errorMessagePrefix}: {response.ErrorMessage}")

‎Common/Orders/UpdateOrderRequest.cs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,15 @@ public override string ToString()
7676
{
7777
return Messages.UpdateOrderRequest.ToString(this);
7878
}
79+
80+
/// <summary>
81+
/// Checks whether the update request is allowed for a closed order.
82+
/// Only tag updates are allowed on closed orders.
83+
/// </summary>
84+
/// <returns>True if the update request is allowed for a closed order</returns>
85+
public bool IsAllowedForClosedOrder()
86+
{
87+
return !Quantity.HasValue && !LimitPrice.HasValue && !StopPrice.HasValue && !TriggerPrice.HasValue;
88+
}
7989
}
8090
}

‎Engine/TransactionHandlers/BrokerageTransactionHandler.cs‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ public OrderTicket UpdateOrder(UpdateOrderRequest request)
355355
Log.Error("BrokerageTransactionHandler.Update(): Cannot update a pending submit order with status " + order.Status);
356356
request.SetResponse(OrderResponse.InvalidNewStatus(request, order));
357357
}
358-
else if (order.Status.IsClosed())
358+
else if (order.Status.IsClosed() && !request.IsAllowedForClosedOrder())
359359
{
360360
// can't update a completed order
361361
Log.Error("BrokerageTransactionHandler.Update(): Cannot update closed order with status " + order.Status);
@@ -867,9 +867,16 @@ private OrderResponse HandleUpdateOrderRequest(UpdateOrderRequest request)
867867
return OrderResponse.InvalidNewStatus(request, order);
868868
}
869869

870+
var isClosedOrderUpdate = false;
871+
870872
if (order.Status.IsClosed())
871873
{
872-
return OrderResponse.InvalidStatus(request, order);
874+
if (!request.IsAllowedForClosedOrder())
875+
{
876+
return OrderResponse.InvalidStatus(request, order);
877+
}
878+
879+
isClosedOrderUpdate = true;
873880
}
874881

875882
// rounds off the order towards 0 to the nearest multiple of lot size
@@ -899,14 +906,21 @@ private OrderResponse HandleUpdateOrderRequest(UpdateOrderRequest request)
899906
ticket.SetOrder(order);
900907

901908
bool orderUpdated;
902-
try
909+
if (isClosedOrderUpdate)
903910
{
904-
orderUpdated = _brokerage.UpdateOrder(order);
911+
orderUpdated = true;
905912
}
906-
catch (Exception err)
913+
else
907914
{
908-
Log.Error(err);
909-
orderUpdated = false;
915+
try
916+
{
917+
orderUpdated = _brokerage.UpdateOrder(order);
918+
}
919+
catch (Exception err)
920+
{
921+
Log.Error(err);
922+
orderUpdated = false;
923+
}
910924
}
911925

912926
if (!orderUpdated)

0 commit comments

Comments
 (0)