Skip to content

Commit 077b6e4

Browse files
authored
Updating Orders should fail if it causes insufficient margin (#8553)
* Initial solution * Update regression test * Update assertions * Resolve PR comments * Add more order types to regression test * Update regression algorithm name * Update old regression algorithm * Refactor validation to check buying power only for non-ComboLeg update orders * Update ValidateSufficientBuyingPowerForOrders * Update regression algorithm * Resolve review comments * Use try out pattern
1 parent 37d0c42 commit 077b6e4

File tree

2 files changed

+258
-22
lines changed

2 files changed

+258
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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.Collections.Generic;
17+
using System.Linq;
18+
using QuantConnect.Data;
19+
using QuantConnect.Interfaces;
20+
using QuantConnect.Orders;
21+
22+
namespace QuantConnect.Algorithm.CSharp
23+
{
24+
/// <summary>
25+
/// This algorithm tests order updates with margin constraints to ensure that orders become invalid when exceeding margin requirements.
26+
/// </summary>
27+
public class InsufficientMarginOrderUpdateRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
28+
{
29+
private OrderTicket _stopOrderTicket;
30+
private OrderTicket _limitOrderTicket;
31+
private OrderTicket _trailingStopOrderTicket;
32+
private bool _updatesReady;
33+
private bool _updatesInProgress;
34+
private int _updateEventsCount;
35+
36+
public override void Initialize()
37+
{
38+
SetStartDate(2018, 4, 3);
39+
SetEndDate(2018, 4, 4);
40+
AddForex("EURUSD", Resolution.Minute);
41+
_updatesInProgress = true;
42+
_updateEventsCount = 0;
43+
}
44+
45+
public override void OnData(Slice data)
46+
{
47+
48+
if (!Portfolio.Invested)
49+
{
50+
var qty = CalculateOrderQuantity("EURUSD", 50m);
51+
52+
MarketOrder("EURUSD", qty);
53+
54+
// Place stop market, limit, and trailing stop orders with half the quantity
55+
_stopOrderTicket = StopMarketOrder("EURUSD", -qty / 2, Securities["EURUSD"].Price - 0.003m);
56+
_limitOrderTicket = LimitOrder("EURUSD", -qty / 2, Securities["EURUSD"].Price - 0.003m);
57+
_trailingStopOrderTicket = TrailingStopOrder("EURUSD", -qty / 2, Securities["EURUSD"].Price - 0.003m, 0.01m, true);
58+
59+
// Update the stop order
60+
var updateStopOrderSettings = new UpdateOrderFields
61+
{
62+
// Attempt to increase the order quantity significantly
63+
Quantity = -qty * 100,
64+
StopPrice = Securities["EURUSD"].Price - 0.003m
65+
};
66+
_stopOrderTicket.Update(updateStopOrderSettings);
67+
68+
// Update limit order
69+
var updateLimitOrderSettings = new UpdateOrderFields
70+
{
71+
// Attempt to increase the order quantity significantly
72+
Quantity = -qty * 100,
73+
LimitPrice = Securities["EURUSD"].Price - 0.003m
74+
};
75+
_limitOrderTicket.Update(updateLimitOrderSettings);
76+
77+
// Update trailing stop order
78+
var updateTrailingStopOrderSettings = new UpdateOrderFields
79+
{
80+
// Attempt to increase the order quantity significantly
81+
Quantity = -qty * 100,
82+
StopPrice = Securities["EURUSD"].Price - 0.003m,
83+
TrailingAmount = 0.01m,
84+
};
85+
_trailingStopOrderTicket.Update(updateTrailingStopOrderSettings);
86+
_updatesReady = true;
87+
}
88+
}
89+
90+
public override void OnOrderEvent(OrderEvent orderEvent)
91+
{
92+
if (_updatesReady && _updatesInProgress)
93+
{
94+
if (orderEvent.Status != OrderStatus.Submitted)
95+
{
96+
throw new RegressionTestException($"Unexpected order event status {orderEvent.Status} received. Expected Submitted.");
97+
}
98+
// All updates have been enqueued and should be rejected one by one
99+
if (orderEvent.OrderId == _stopOrderTicket.OrderId && !orderEvent.Message.Contains("Brokerage failed to update order"))
100+
{
101+
throw new RegressionTestException($"The stop order update should have been rejected due to insufficient margin");
102+
}
103+
104+
if (orderEvent.Id == _limitOrderTicket.OrderId && !orderEvent.Message.Contains("Brokerage failed to update order"))
105+
{
106+
throw new RegressionTestException($"The limit order update should have been rejected due to insufficient margin");
107+
}
108+
109+
if (orderEvent.Id == _trailingStopOrderTicket.OrderId && !orderEvent.Message.Contains("Brokerage failed to update order"))
110+
{
111+
throw new RegressionTestException($"The trailing stop order update should have been rejected due to insufficient margin");
112+
}
113+
_updateEventsCount++;
114+
}
115+
if (_updateEventsCount >= 3)
116+
{
117+
_updatesInProgress = false;
118+
}
119+
120+
}
121+
122+
public override void OnEndOfAlgorithm()
123+
{
124+
// Updates were rejected, so all orders should be in Filled status
125+
var orders = Transactions.GetOrders().ToList();
126+
foreach (var order in orders)
127+
{
128+
if (order.Status != OrderStatus.Filled)
129+
{
130+
throw new RegressionTestException($"Order {order.Id} with symbol {order.Symbol} should have been filled, but its current status is {order.Status}.");
131+
}
132+
}
133+
if (!_updatesReady)
134+
{
135+
throw new RegressionTestException("Update Orders should be ready!");
136+
}
137+
}
138+
139+
/// <summary>
140+
/// Final status of the algorithm
141+
/// </summary>
142+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
143+
144+
/// <summary>
145+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
146+
/// </summary>
147+
public bool CanRunLocally { get; } = true;
148+
149+
/// <summary>
150+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
151+
/// </summary>
152+
public virtual List<Language> Languages { get; } = new() { Language.CSharp };
153+
154+
/// <summary>
155+
/// Data Points count of all timeslices of algorithm
156+
/// </summary>
157+
public long DataPoints => 2893;
158+
159+
/// <summary>
160+
/// Data Points count of the algorithm history
161+
/// </summary>
162+
public int AlgorithmHistoryDataPoints => 60;
163+
164+
/// <summary>
165+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
166+
/// </summary>
167+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
168+
{
169+
{"Total Orders", "4"},
170+
{"Average Win", "0%"},
171+
{"Average Loss", "0%"},
172+
{"Compounding Annual Return", "0%"},
173+
{"Drawdown", "0%"},
174+
{"Expectancy", "0"},
175+
{"Start Equity", "100000.00"},
176+
{"End Equity", "90809.64"},
177+
{"Net Profit", "0%"},
178+
{"Sharpe Ratio", "0"},
179+
{"Sortino Ratio", "0"},
180+
{"Probabilistic Sharpe Ratio", "0%"},
181+
{"Loss Rate", "0%"},
182+
{"Win Rate", "0%"},
183+
{"Profit-Loss Ratio", "0"},
184+
{"Alpha", "0"},
185+
{"Beta", "0"},
186+
{"Annual Standard Deviation", "0"},
187+
{"Annual Variance", "0"},
188+
{"Information Ratio", "0"},
189+
{"Tracking Error", "0"},
190+
{"Treynor Ratio", "0"},
191+
{"Total Fees", "$0.00"},
192+
{"Estimated Strategy Capacity", "$99000.00"},
193+
{"Lowest Capacity Asset", "EURUSD 8G"},
194+
{"Portfolio Turnover", "6777.62%"},
195+
{"OrderListHash", "505feaf1ae70ead2d7ab78ea257d7342"}
196+
};
197+
}
198+
}

Engine/TransactionHandlers/BrokerageTransactionHandler.cs

+60-22
Original file line numberDiff line numberDiff line change
@@ -844,29 +844,9 @@ private OrderResponse HandleSubmitOrderRequest(SubmitOrderRequest request)
844844
}
845845

846846
// check to see if we have enough money to place the order
847-
HasSufficientBuyingPowerForOrderResult hasSufficientBuyingPowerResult;
848-
try
847+
if (!HasSufficientBuyingPowerForOrders(order, request, out var validationResult, orders, securities))
849848
{
850-
hasSufficientBuyingPowerResult = _algorithm.Portfolio.HasSufficientBuyingPowerForOrder(orders);
851-
}
852-
catch (Exception err)
853-
{
854-
Log.Error(err);
855-
_algorithm.Error($"Order Error: id: {order.Id.ToStringInvariant()}, Error executing margin models: {err.Message}");
856-
HandleOrderEvent(new OrderEvent(order,
857-
_algorithm.UtcTime,
858-
OrderFee.Zero,
859-
"Error executing margin models"));
860-
return OrderResponse.Error(request, OrderResponseErrorCode.ProcessingError, "Error in GetSufficientCapitalForOrder");
861-
}
862-
863-
if (!hasSufficientBuyingPowerResult.IsSufficient)
864-
{
865-
var errorMessage = securities.GetErrorMessage(hasSufficientBuyingPowerResult);
866-
_algorithm.Error(errorMessage);
867-
868-
InvalidateOrders(orders, errorMessage);
869-
return OrderResponse.Error(request, OrderResponseErrorCode.InsufficientBuyingPower, errorMessage);
849+
return validationResult;
870850
}
871851

872852
// verify that our current brokerage can actually take the order
@@ -959,6 +939,17 @@ private OrderResponse HandleUpdateOrderRequest(UpdateOrderRequest request)
959939
return response;
960940
}
961941

942+
// If the order is not part of a ComboLegLimit update, validate sufficient buying power
943+
if (order.GroupOrderManager == null)
944+
{
945+
var updatedOrder = order.Clone();
946+
updatedOrder.ApplyUpdateOrderRequest(request);
947+
if (!HasSufficientBuyingPowerForOrders(updatedOrder, request, out var validationResult))
948+
{
949+
return validationResult;
950+
}
951+
}
952+
962953
// modify the values of the order object
963954
order.ApplyUpdateOrderRequest(request);
964955

@@ -1057,6 +1048,53 @@ private OrderResponse HandleCancelOrderRequest(CancelOrderRequest request)
10571048
return OrderResponse.Success(request);
10581049
}
10591050

1051+
/// <summary>
1052+
/// Validates if there is sufficient buying power for the given order(s).
1053+
/// Returns an error response if validation fails or an exception occurs.
1054+
/// Returns null if validation passes.
1055+
/// </summary>
1056+
private bool HasSufficientBuyingPowerForOrders(Order order, OrderRequest request, out OrderResponse response, List<Order> orders = null, Dictionary<Order, Security> securities = null)
1057+
{
1058+
response = null;
1059+
HasSufficientBuyingPowerForOrderResult hasSufficientBuyingPowerResult;
1060+
try
1061+
{
1062+
hasSufficientBuyingPowerResult = _algorithm.Portfolio.HasSufficientBuyingPowerForOrder(orders ?? [order]);
1063+
}
1064+
catch (Exception err)
1065+
{
1066+
Log.Error(err);
1067+
_algorithm.Error($"Order Error: id: {order.Id.ToStringInvariant()}, Error executing margin models: {err.Message}");
1068+
HandleOrderEvent(new OrderEvent(order, _algorithm.UtcTime, OrderFee.Zero, "Error executing margin models"));
1069+
1070+
response = OrderResponse.Error(request, OrderResponseErrorCode.ProcessingError, "An error occurred while checking sufficient buying power for the orders.");
1071+
return false;
1072+
}
1073+
1074+
if (!hasSufficientBuyingPowerResult.IsSufficient)
1075+
{
1076+
var errorMessage = securities != null
1077+
? securities.GetErrorMessage(hasSufficientBuyingPowerResult)
1078+
: $"Brokerage failed to update order with id: {order.Id.ToStringInvariant()}, Symbol: {order.Symbol.Value}, Insufficient buying power to complete order, Reason: {hasSufficientBuyingPowerResult.Reason}.";
1079+
1080+
_algorithm.Error(errorMessage);
1081+
1082+
if (request is UpdateOrderRequest)
1083+
{
1084+
HandleOrderEvent(new OrderEvent(order, _algorithm.UtcTime, OrderFee.Zero, errorMessage));
1085+
response = OrderResponse.Error(request, OrderResponseErrorCode.BrokerageFailedToUpdateOrder, errorMessage);
1086+
}
1087+
else
1088+
{
1089+
InvalidateOrders(orders, errorMessage);
1090+
response = OrderResponse.Error(request, OrderResponseErrorCode.InsufficientBuyingPower, errorMessage);
1091+
}
1092+
return false;
1093+
}
1094+
1095+
return true;
1096+
}
1097+
10601098
private void HandleOrderEvents(List<OrderEvent> orderEvents)
10611099
{
10621100
lock (_lockHandleOrderEvent)

0 commit comments

Comments
 (0)