diff --git a/Algorithm.CSharp/CustomBrokerageMessageHandlerAlgorithm.cs b/Algorithm.CSharp/CustomBrokerageMessageHandlerAlgorithm.cs
index 6c2dafbcc1f2..c122b3f2479b 100644
--- a/Algorithm.CSharp/CustomBrokerageMessageHandlerAlgorithm.cs
+++ b/Algorithm.CSharp/CustomBrokerageMessageHandlerAlgorithm.cs
@@ -1,4 +1,4 @@
-/*
+/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
@@ -59,11 +59,21 @@ public class CustomBrokerageMessageHandler : IBrokerageMessageHandler
/// Process the brokerage message event. Trigger any actions in the algorithm or notifications system required.
///
/// Message object
- public void Handle(BrokerageMessageEvent message)
+ public void HandleMessage(BrokerageMessageEvent message)
{
var toLog = $"{_algo.Time.ToStringInvariant("o")} Event: {message.Message}";
_algo.Debug(toLog);
_algo.Log(toLog);
}
+
+ ///
+ /// Handles a new order placed manually in the brokerage side
+ ///
+ /// The new order event
+ /// Whether the order should be added to the transaction handler
+ public bool HandleOrder(NewBrokerageOrderNotificationEventArgs eventArgs)
+ {
+ return true;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Algorithm.CSharp/CustomBrokerageSideOrderHandlingRegressionAlgorithm.cs b/Algorithm.CSharp/CustomBrokerageSideOrderHandlingRegressionAlgorithm.cs
new file mode 100644
index 000000000000..ecbdda318dff
--- /dev/null
+++ b/Algorithm.CSharp/CustomBrokerageSideOrderHandlingRegressionAlgorithm.cs
@@ -0,0 +1,93 @@
+/*
+ * 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.Globalization;
+using QuantConnect.Brokerages;
+using QuantConnect.Interfaces;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Algorithm demonstrating the usage of custom brokerage message handler and the new brokerage-side order handling/filtering.
+ /// This test is supposed to be ran by the CustomBrokerageMessageHandlerTests unit test fixture.
+ ///
+ /// All orders are sent from the brokerage, none of them will be placed by the algorithm.
+ ///
+ public class CustomBrokerageSideOrderHandlingRegressionAlgorithm : QCAlgorithm
+ {
+ private Symbol _spy = QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA);
+
+ public override void Initialize()
+ {
+ SetStartDate(2013, 10, 07);
+ SetEndDate(2013, 10, 11);
+ SetCash(100000);
+
+ SetBrokerageMessageHandler(new CustomBrokerageMessageHandler(this));
+ }
+
+ public override void OnEndOfAlgorithm()
+ {
+ // The security should have been added
+ if (!Securities.ContainsKey(_spy))
+ {
+ throw new Exception("Expected security to have been added");
+ }
+
+ if (Transactions.OrdersCount == 0)
+ {
+ throw new Exception("Expected orders to be added from brokerage side");
+ }
+
+ if (Portfolio.Positions.Groups.Count != 1)
+ {
+ throw new Exception("Expected only one position");
+ }
+ }
+
+ public class CustomBrokerageMessageHandler : IBrokerageMessageHandler
+ {
+ private readonly IAlgorithm _algorithm;
+ public CustomBrokerageMessageHandler(IAlgorithm algo) { _algorithm = algo; }
+
+ ///
+ /// Process the brokerage message event. Trigger any actions in the algorithm or notifications system required.
+ ///
+ /// Message object
+ public void HandleMessage(BrokerageMessageEvent message)
+ {
+ _algorithm.Debug($"{_algorithm.Time.ToStringInvariant("o")} Event: {message.Message}");
+ }
+
+ ///
+ /// Handles a new order placed manually in the brokerage side
+ ///
+ /// The new order event
+ /// Whether the order should be added to the transaction handler
+ public bool HandleOrder(NewBrokerageOrderNotificationEventArgs eventArgs)
+ {
+ var order = eventArgs.Order;
+ if (string.IsNullOrEmpty(order.Tag) || !int.TryParse(order.Tag, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ {
+ throw new Exception("Expected all new brokerage-side orders to have a valid tag");
+ }
+
+ // We will only process orders with even tags
+ return value % 2 == 0;
+ }
+ }
+ }
+}
diff --git a/Algorithm.Python/CustomBrokerageSideOrderHandlingRegressionAlgorithm.py b/Algorithm.Python/CustomBrokerageSideOrderHandlingRegressionAlgorithm.py
new file mode 100644
index 000000000000..9ec6df9aae27
--- /dev/null
+++ b/Algorithm.Python/CustomBrokerageSideOrderHandlingRegressionAlgorithm.py
@@ -0,0 +1,63 @@
+# 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 *
+
+###
+### Algorithm demonstrating the usage of custom brokerage message handler and the new brokerage-side order handling/filtering.
+### This test is supposed to be ran by the CustomBrokerageMessageHandlerTests unit test fixture.
+###
+### All orders are sent from the brokerage, none of them will be placed by the algorithm.
+###
+class CustomBrokerageSideOrderHandlingRegressionAlgorithm(QCAlgorithm):
+ '''Algorithm demonstrating the usage of custom brokerage message handler and the new brokerage-side order handling/filtering.
+ This test is supposed to be ran by the CustomBrokerageMessageHandlerTests unit test fixture.
+
+ All orders are sent from the brokerage, none of them will be placed by the algorithm.'''
+
+ def Initialize(self):
+ self.SetStartDate(2013, 10, 7)
+ self.SetEndDate(2013, 10, 11)
+ self.SetCash(100000)
+
+ self.SetBrokerageMessageHandler(CustomBrokerageMessageHandler(self))
+
+ self._spy = Symbol.Create("SPY", SecurityType.Equity, Market.USA)
+
+ def OnEndOfAlgorithm(self):
+ # The security should have been added
+ if not self.Securities.ContainsKey(self._spy):
+ raise Exception("Expected security to have been added")
+
+ if self.Transactions.OrdersCount == 0:
+ raise Exception("Expected orders to be added from brokerage side")
+
+ if len(list(self.Portfolio.Positions.Groups)) != 1:
+ raise Exception("Expected only one position")
+
+class CustomBrokerageMessageHandler(IBrokerageMessageHandler):
+ __namespace__ = "CustomBrokerageSideOrderHandlingRegressionAlgorithm"
+
+ def __init__(self, algorithm):
+ self._algorithm = algorithm
+
+ def HandleMessage(self, message):
+ self._algorithm.Debug(f"{self._algorithm.Time} Event: {message.Message}")
+
+ def HandleOrder(self, eventArgs):
+ order = eventArgs.Order
+ if order.Tag is None or not order.Tag.isdigit():
+ raise Exception("Expected all new brokerage-side orders to have a valid tag")
+
+ # We will only process orders with even tags
+ return int(order.Tag) % 2 == 0
diff --git a/Common/Brokerages/DefaultBrokerageMessageHandler.cs b/Common/Brokerages/DefaultBrokerageMessageHandler.cs
index 7e6cc75e84e1..87c9c22e69e2 100644
--- a/Common/Brokerages/DefaultBrokerageMessageHandler.cs
+++ b/Common/Brokerages/DefaultBrokerageMessageHandler.cs
@@ -68,7 +68,7 @@ public DefaultBrokerageMessageHandler(IAlgorithm algorithm, AlgorithmNodePacket
/// Handles the message
///
/// The message to be handled
- public void Handle(BrokerageMessageEvent message)
+ public void HandleMessage(BrokerageMessageEvent message)
{
// based on message type dispatch to result handler
switch (message.Type)
@@ -159,6 +159,16 @@ where exchange.IsOpenDuringBar(
}
}
+ ///
+ /// Handles a new order placed manually in the brokerage side
+ ///
+ /// The new order event
+ /// Whether the order should be added to the transaction handler
+ public bool HandleOrder(NewBrokerageOrderNotificationEventArgs eventArgs)
+ {
+ return true;
+ }
+
private void StartCheckReconnected(TimeSpan delay, BrokerageMessageEvent message)
{
_cancellationTokenSource.DisposeSafely();
diff --git a/Common/Brokerages/DowngradeErrorCodeToWarningBrokerageMessageHandler.cs b/Common/Brokerages/DowngradeErrorCodeToWarningBrokerageMessageHandler.cs
index 5632dd0cfc3e..0878eb0cfc3b 100644
--- a/Common/Brokerages/DowngradeErrorCodeToWarningBrokerageMessageHandler.cs
+++ b/Common/Brokerages/DowngradeErrorCodeToWarningBrokerageMessageHandler.cs
@@ -1,4 +1,4 @@
-/*
+/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
@@ -41,7 +41,7 @@ public DowngradeErrorCodeToWarningBrokerageMessageHandler(IBrokerageMessageHandl
/// Handles the message
///
/// The message to be handled
- public void Handle(BrokerageMessageEvent message)
+ public void HandleMessage(BrokerageMessageEvent message)
{
if (message.Type == BrokerageMessageType.Error && _errorCodesToIgnore.Contains(message.Code))
{
@@ -49,7 +49,17 @@ public void Handle(BrokerageMessageEvent message)
message = new BrokerageMessageEvent(BrokerageMessageType.Warning, message.Code, message.Message);
}
- _brokerageMessageHandler.Handle(message);
+ _brokerageMessageHandler.HandleMessage(message);
+ }
+
+ ///
+ /// Handles a new order placed manually in the brokerage side
+ ///
+ /// The new order event
+ /// Whether the order should be added to the transaction handler
+ public bool HandleOrder(NewBrokerageOrderNotificationEventArgs eventArgs)
+ {
+ return _brokerageMessageHandler.HandleOrder(eventArgs);
}
}
-}
\ No newline at end of file
+}
diff --git a/Common/Brokerages/IBrokerageMessageHandler.cs b/Common/Brokerages/IBrokerageMessageHandler.cs
index 4b938426f01f..73eeacaf412a 100644
--- a/Common/Brokerages/IBrokerageMessageHandler.cs
+++ b/Common/Brokerages/IBrokerageMessageHandler.cs
@@ -1,4 +1,4 @@
-/*
+/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
@@ -25,6 +25,13 @@ public interface IBrokerageMessageHandler
/// Handles the message
///
/// The message to be handled
- void Handle(BrokerageMessageEvent message);
+ void HandleMessage(BrokerageMessageEvent message);
+
+ ///
+ /// Handles a new order placed manually in the brokerage side
+ ///
+ /// The new order event
+ /// Whether the order should be added to the transaction handler
+ bool HandleOrder(NewBrokerageOrderNotificationEventArgs eventArgs);
}
}
diff --git a/Common/Extensions.cs b/Common/Extensions.cs
index 58482dde0250..f4291edac2d4 100644
--- a/Common/Extensions.cs
+++ b/Common/Extensions.cs
@@ -3630,6 +3630,82 @@ private static IEnumerable CreateFutureChain(this IAlgorithm algorithm
return algorithm.UniverseManager.Values.Where(universe => universe.Configuration.Symbol == symbol.Canonical || ContinuousContractUniverse.CreateSymbol(symbol.Canonical) == universe.Configuration.Symbol);
}
+ private static bool _notifiedUniverseSettingsUsed;
+ private static readonly HashSet _supportedSecurityTypes = new()
+ {
+ SecurityType.Equity,
+ SecurityType.Forex,
+ SecurityType.Cfd,
+ SecurityType.Option,
+ SecurityType.Future,
+ SecurityType.FutureOption,
+ SecurityType.IndexOption,
+ SecurityType.Crypto,
+ SecurityType.CryptoFuture
+ };
+
+ ///
+ /// Gets the security for the specified symbol from the algorithm's securities collection.
+ /// In case the security is not found, it will be created using the
+ /// and a best effort configuration setup.
+ ///
+ /// The algorithm instance
+ /// The symbol which security is being looked up
+ /// The found or added security instance
+ /// Callback to invoke in case of unsupported security type
+ /// True if the security was found or added
+ public static bool GetOrAddUnrequestedSecurity(this IAlgorithm algorithm, Symbol symbol, out Security security,
+ Action> onError = null)
+ {
+ if (!algorithm.Securities.TryGetValue(symbol, out security))
+ {
+ if (!_supportedSecurityTypes.Contains(symbol.SecurityType))
+ {
+ Log.Error("GetOrAddUnrequestedSecurity(): Unsupported security type: " + symbol.SecurityType + "-" + symbol.Value);
+ onError?.Invoke(_supportedSecurityTypes);
+ return false;
+ }
+
+ var resolution = algorithm.UniverseSettings.Resolution;
+ var fillForward = algorithm.UniverseSettings.FillForward;
+ var leverage = algorithm.UniverseSettings.Leverage;
+ var extendedHours = algorithm.UniverseSettings.ExtendedMarketHours;
+
+ if (!_notifiedUniverseSettingsUsed)
+ {
+ // let's just send the message once
+ _notifiedUniverseSettingsUsed = true;
+
+ var leverageMsg = $" Leverage = {leverage};";
+ if (leverage == Security.NullLeverage)
+ {
+ leverageMsg = $" Leverage = default;";
+ }
+ algorithm.Debug($"Will use UniverseSettings for automatically added securities for open orders and holdings. UniverseSettings:" +
+ $" Resolution = {resolution};{leverageMsg} FillForward = {fillForward}; ExtendedHours = {extendedHours}");
+ }
+
+ Log.Trace("GetOrAddUnrequestedSecurity(): Adding unrequested security: " + symbol.Value);
+
+ if (symbol.SecurityType.IsOption())
+ {
+ // add current option contract to the system
+ security = algorithm.AddOptionContract(symbol, resolution, fillForward, leverage, extendedHours);
+ }
+ else if (symbol.SecurityType == SecurityType.Future)
+ {
+ // add current future contract to the system
+ security = algorithm.AddFutureContract(symbol, resolution, fillForward, leverage, extendedHours);
+ }
+ else
+ {
+ // for items not directly requested set leverage to 1 and at the min resolution
+ security = algorithm.AddSecurity(symbol.SecurityType, symbol.Value, resolution, symbol.ID.Market, fillForward, leverage, extendedHours);
+ }
+ }
+ return true;
+ }
+
///
/// Inverts the specified
///
diff --git a/Common/Python/BrokerageMessageHandlerPythonWrapper.cs b/Common/Python/BrokerageMessageHandlerPythonWrapper.cs
index f0205a005e70..68084b36a383 100644
--- a/Common/Python/BrokerageMessageHandlerPythonWrapper.cs
+++ b/Common/Python/BrokerageMessageHandlerPythonWrapper.cs
@@ -1,4 +1,4 @@
-/*
+/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
@@ -38,11 +38,24 @@ public BrokerageMessageHandlerPythonWrapper(PyObject model)
/// Handles the message
///
/// The message to be handled
- public void Handle(BrokerageMessageEvent message)
+ public void HandleMessage(BrokerageMessageEvent message)
{
using (Py.GIL())
{
- _model.Handle(message);
+ _model.HandleMessage(message);
+ }
+ }
+
+ ///
+ /// Handles a new order placed manually in the brokerage side
+ ///
+ /// The new order event
+ /// Whether the order should be added to the transaction handler
+ public bool HandleOrder(NewBrokerageOrderNotificationEventArgs eventArgs)
+ {
+ using (Py.GIL())
+ {
+ return _model.HandleOrder(eventArgs);
}
}
}
diff --git a/Engine/Engine.cs b/Engine/Engine.cs
index 9ba2b7e2a426..fdd1b8083bc5 100644
--- a/Engine/Engine.cs
+++ b/Engine/Engine.cs
@@ -300,7 +300,7 @@ public void Run(AlgorithmNodePacket job, AlgorithmManager manager, string assemb
// wire up the brokerage message handler
brokerage.Message += (sender, message) =>
{
- algorithm.BrokerageMessageHandler.Handle(message);
+ algorithm.BrokerageMessageHandler.HandleMessage(message);
// fire brokerage message events
algorithm.OnBrokerageMessage(message);
diff --git a/Engine/Setup/BacktestingSetupHandler.cs b/Engine/Setup/BacktestingSetupHandler.cs
index 53a1ed0910e6..7499169fd77c 100644
--- a/Engine/Setup/BacktestingSetupHandler.cs
+++ b/Engine/Setup/BacktestingSetupHandler.cs
@@ -112,7 +112,7 @@ public virtual IAlgorithm CreateAlgorithmInstance(AlgorithmNodePacket algorithmN
/// The algorithm instance before Initialize has been called
/// The brokerage factory
/// The brokerage instance, or throws if error creating instance
- public IBrokerage CreateBrokerage(AlgorithmNodePacket algorithmNodePacket, IAlgorithm uninitializedAlgorithm, out IBrokerageFactory factory)
+ public virtual IBrokerage CreateBrokerage(AlgorithmNodePacket algorithmNodePacket, IAlgorithm uninitializedAlgorithm, out IBrokerageFactory factory)
{
factory = new BacktestingBrokerageFactory();
return new BacktestingBrokerage(uninitializedAlgorithm);
@@ -209,7 +209,7 @@ public bool Setup(SetupHandlerParameters parameters)
BaseSetupHandler.LoadBacktestJobCashAmount(algorithm, job);
// after algorithm was initialized, should set trading days per year for our great portfolio statistics
- BaseSetupHandler.SetBrokerageTradingDayPerYear(algorithm);
+ BaseSetupHandler.SetBrokerageTradingDayPerYear(algorithm);
// finalize initialization
algorithm.PostInitialize();
diff --git a/Engine/Setup/BrokerageSetupHandler.cs b/Engine/Setup/BrokerageSetupHandler.cs
index 2f4ea0fd603b..9efd2b81c58d 100644
--- a/Engine/Setup/BrokerageSetupHandler.cs
+++ b/Engine/Setup/BrokerageSetupHandler.cs
@@ -39,8 +39,6 @@ namespace QuantConnect.Lean.Engine.Setup
///
public class BrokerageSetupHandler : ISetupHandler
{
- private bool _notifiedUniverseSettingsUsed;
-
///
/// Max allocation limit configuration variable name
///
@@ -79,18 +77,6 @@ public class BrokerageSetupHandler : ISetupHandler
// saves ref to algo so we can call quit if runtime error encountered
private IBrokerageFactory _factory;
private IBrokerage _dataQueueHandlerBrokerage;
- protected virtual HashSet SupportedSecurityTypes => new()
- {
- SecurityType.Equity,
- SecurityType.Forex,
- SecurityType.Cfd,
- SecurityType.Option,
- SecurityType.Future,
- SecurityType.FutureOption,
- SecurityType.IndexOption,
- SecurityType.Crypto,
- SecurityType.CryptoFuture
- };
///
/// Initializes a new BrokerageSetupHandler
@@ -464,55 +450,10 @@ protected bool LoadExistingHoldingsAndOrders(IBrokerage brokerage, IAlgorithm al
private bool GetOrAddUnrequestedSecurity(IAlgorithm algorithm, Symbol symbol, SecurityType securityType, out Security security)
{
- if (!algorithm.Securities.TryGetValue(symbol, out security))
- {
- if (!SupportedSecurityTypes.Contains((SecurityType)securityType))
- {
- Log.Error("BrokerageSetupHandler.Setup(): Unsupported security type: " + securityType + "-" + symbol.Value);
- AddInitializationError("Found unsupported security type in existing brokerage holdings: " + securityType + ". " +
- "QuantConnect currently supports the following security types: " + string.Join(",", SupportedSecurityTypes));
- security = null;
- return false;
- }
-
- var resolution = algorithm.UniverseSettings.Resolution;
- var fillForward = algorithm.UniverseSettings.FillForward;
- var leverage = algorithm.UniverseSettings.Leverage;
- var extendedHours = algorithm.UniverseSettings.ExtendedMarketHours;
-
- if (!_notifiedUniverseSettingsUsed)
- {
- // let's just send the message once
- _notifiedUniverseSettingsUsed = true;
-
- var leverageMsg = $" Leverage = {leverage};";
- if (leverage == Security.NullLeverage)
- {
- leverageMsg = $" Leverage = default;";
- }
- algorithm.Debug($"Will use UniverseSettings for automatically added securities for open orders and holdings. UniverseSettings:" +
- $" Resolution = {resolution};{leverageMsg} FillForward = {fillForward}; ExtendedHours = {extendedHours}");
- }
-
- Log.Trace("BrokerageSetupHandler.Setup(): Adding unrequested security: " + symbol.Value);
-
- if (symbol.SecurityType.IsOption())
- {
- // add current option contract to the system
- security = algorithm.AddOptionContract(symbol, resolution, fillForward, leverage, extendedHours);
- }
- else if (symbol.SecurityType == SecurityType.Future)
- {
- // add current future contract to the system
- security = algorithm.AddFutureContract(symbol, resolution, fillForward, leverage, extendedHours);
- }
- else
- {
- // for items not directly requested set leverage to 1 and at the min resolution
- security = algorithm.AddSecurity(symbol.SecurityType, symbol.Value, resolution, symbol.ID.Market, fillForward, leverage, extendedHours);
- }
- }
- return true;
+ return algorithm.GetOrAddUnrequestedSecurity(symbol, out security,
+ onError: (supportedSecurityTypes) => AddInitializationError(
+ "Found unsupported security type in existing brokerage holdings: " + securityType + ". " +
+ "QuantConnect currently supports the following security types: " + string.Join(",", supportedSecurityTypes)));
}
///
diff --git a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs
index a0c5d10d01cb..375d745ec476 100644
--- a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs
+++ b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs
@@ -180,7 +180,7 @@ public virtual void Initialize(IAlgorithm algorithm, IBrokerage brokerage, IResu
_brokerage.NewBrokerageOrderNotification += (sender, e) =>
{
- AddOpenOrder(e.Order, _algorithm);
+ HandleNewBrokerageSideOrder(e);
};
_brokerage.DelistingNotification += (sender, e) =>
@@ -1521,6 +1521,22 @@ private void HandleOptionNotification(OptionNotificationEventArgs e)
}
}
+ ///
+ /// New brokerage-side order event handler
+ ///
+ private void HandleNewBrokerageSideOrder(NewBrokerageOrderNotificationEventArgs e)
+ {
+ void onError(IReadOnlyCollection supportedSecurityTypes) =>
+ _algorithm.Debug($"Warning: New brokerage-side order could not be processed due to " +
+ $"it's security not being supported. Supported security types are {string.Join(", ", supportedSecurityTypes)}");
+
+ if (_algorithm.BrokerageMessageHandler.HandleOrder(e) &&
+ _algorithm.GetOrAddUnrequestedSecurity(e.Order.Symbol, out _, onError))
+ {
+ AddOpenOrder(e.Order, _algorithm);
+ }
+ }
+
private OptionExerciseOrder GenerateOptionExerciseOrder(Security security, decimal quantity)
{
// generate new exercise order and ticket for the option
diff --git a/Tests/Brokerages/DowngradeErrorCodeToWarningBrokerageMessageHandlerTests.cs b/Tests/Brokerages/DowngradeErrorCodeToWarningBrokerageMessageHandlerTests.cs
index c80712b00cb4..e905f0b6a5ef 100644
--- a/Tests/Brokerages/DowngradeErrorCodeToWarningBrokerageMessageHandlerTests.cs
+++ b/Tests/Brokerages/DowngradeErrorCodeToWarningBrokerageMessageHandlerTests.cs
@@ -30,41 +30,41 @@ public class DowngradeErrorCodeToWarningBrokerageMessageHandlerTests
public void PatchesNonErrorMessagesToWrappedImplementation(BrokerageMessageType type)
{
var wrapped = new Mock();
- wrapped.Setup(bmh => bmh.Handle(It.IsAny())).Verifiable();
+ wrapped.Setup(bmh => bmh.HandleMessage(It.IsAny())).Verifiable();
var downgrader = new DowngradeErrorCodeToWarningBrokerageMessageHandler(wrapped.Object, new[] { "code" });
var message = new BrokerageMessageEvent(type, "code", "message");
- downgrader.Handle(message);
+ downgrader.HandleMessage(message);
- wrapped.Verify(bmh => bmh.Handle(message), Times.Once);
+ wrapped.Verify(bmh => bmh.HandleMessage(message), Times.Once);
}
[Test]
public void PatchesErrorMessageNotMatchingCodeToWrappedImplementation()
{
var wrapped = new Mock();
- wrapped.Setup(bmh => bmh.Handle(It.IsAny())).Verifiable();
+ wrapped.Setup(bmh => bmh.HandleMessage(It.IsAny())).Verifiable();
var downgrader = new DowngradeErrorCodeToWarningBrokerageMessageHandler(wrapped.Object, new[] { "code" });
var message = new BrokerageMessageEvent(BrokerageMessageType.Error, "not-code", "message");
- downgrader.Handle(message);
+ downgrader.HandleMessage(message);
- wrapped.Verify(bmh => bmh.Handle(message), Times.Once);
+ wrapped.Verify(bmh => bmh.HandleMessage(message), Times.Once);
}
[Test]
public void RewritesErrorMessageMatchingCodeAsWarning()
{
var wrapped = new Mock();
- wrapped.Setup(bmh => bmh.Handle(It.IsAny())).Verifiable();
+ wrapped.Setup(bmh => bmh.HandleMessage(It.IsAny())).Verifiable();
var downgrader = new DowngradeErrorCodeToWarningBrokerageMessageHandler(wrapped.Object, new[] { "code" });
var message = new BrokerageMessageEvent(BrokerageMessageType.Error, "code", "message");
- downgrader.Handle(message);
+ downgrader.HandleMessage(message);
// verify we converter the message to a warning message w/ the same message and code
wrapped.Verify(
- bmh => bmh.Handle(
+ bmh => bmh.HandleMessage(
It.Is(
e => e.Type == BrokerageMessageType.Warning
&& e.Message == message.Message
diff --git a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
index c7de9de790ba..cdc30ac2a09c 100644
--- a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
+++ b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
@@ -2093,6 +2093,98 @@ public void OrderPriceAdjustmentModeIsSetWhenAddingOpenOrder(DataNormalizationMo
Assert.AreEqual(expectedNormalizationMode, transactionHandler.GetOrderById(order.Id).PriceAdjustmentMode);
}
+ private static TestCaseData[] BrokerageSideOrdersTestCases => new[]
+ {
+ new TestCaseData(OrderType.Limit, false),
+ new TestCaseData(OrderType.StopMarket, false),
+ new TestCaseData(OrderType.StopLimit, false),
+ new TestCaseData(OrderType.MarketOnOpen, false),
+ new TestCaseData(OrderType.MarketOnClose, false),
+ new TestCaseData(OrderType.LimitIfTouched, false),
+ new TestCaseData(OrderType.ComboMarket, false),
+ new TestCaseData(OrderType.ComboLimit, false),
+ new TestCaseData(OrderType.ComboLegLimit, false),
+ new TestCaseData(OrderType.TrailingStop, false),
+ // Only market orders are supported for this test
+ new TestCaseData(OrderType.Market, true),
+ };
+
+ private static Order GetOrder(OrderType type, Symbol symbol)
+ {
+ switch (type)
+ {
+ case OrderType.Market:
+ return new MarketOrder(symbol, 100, new DateTime(2024, 01, 19, 12, 0, 0));
+ case OrderType.Limit:
+ return new LimitOrder(symbol, 100, 100m, new DateTime(2024, 01, 19, 12, 0, 0));
+ case OrderType.StopMarket:
+ return new StopMarketOrder(symbol, 100, 100m, new DateTime(2024, 01, 19, 12, 0, 0));
+ case OrderType.StopLimit:
+ return new StopLimitOrder(symbol, 100, 100m, 100m, new DateTime(2024, 01, 19, 12, 0, 0));
+ case OrderType.MarketOnOpen:
+ return new MarketOnOpenOrder(symbol, 100, new DateTime(2024, 01, 19, 12, 0, 0));
+ case OrderType.MarketOnClose:
+ return new MarketOnCloseOrder(symbol, 100, new DateTime(2024, 01, 19, 12, 0, 0));
+ case OrderType.LimitIfTouched:
+ return new LimitIfTouchedOrder(symbol, 100, 100m, 100m, new DateTime(2024, 01, 19, 12, 0, 0));
+ case OrderType.ComboMarket:
+ return new ComboMarketOrder(symbol, 100, new DateTime(2024, 01, 19, 12, 0, 0), new GroupOrderManager(1, 1, 10));
+ case OrderType.ComboLimit:
+ return new ComboLimitOrder(symbol, 100, 100m, new DateTime(2024, 01, 19, 12, 0, 0), new GroupOrderManager(1, 1, 10, 100));
+ case OrderType.ComboLegLimit:
+ return new ComboLegLimitOrder(symbol, 100, 100m, new DateTime(2024, 01, 19, 12, 0, 0), new GroupOrderManager(1, 1, 10));
+ case OrderType.TrailingStop:
+ return new TrailingStopOrder(symbol, 100, 100m, 100m, false, new DateTime(2024, 01, 19, 12, 0, 0));
+ default:
+ throw new ArgumentOutOfRangeException(nameof(type), type, null);
+ }
+ }
+
+ [TestCaseSource(nameof(BrokerageSideOrdersTestCases))]
+ public void NewBrokerageOrdersAreFiltered(OrderType orderType, bool accepted)
+ {
+ //Initialize the transaction handler
+ var transactionHandler = new TestBrokerageTransactionHandler();
+ using var brokerage = new TestingBrokerage();
+ transactionHandler.Initialize(_algorithm, brokerage, new BacktestingResultHandler());
+
+ _algorithm.SetBrokerageModel(new DefaultBrokerageModel());
+ var brokerageMessageHandler = new TestBrokerageMessageHandler();
+ _algorithm.SetBrokerageMessageHandler(brokerageMessageHandler);
+
+ var symbol = _algorithm.AddEquity("SPY").Symbol;
+
+ var order = GetOrder(orderType, symbol);
+ Assert.AreEqual(orderType, order.Type);
+ brokerage.OnNewBrokerageOrder(new NewBrokerageOrderNotificationEventArgs(order));
+ Assert.AreEqual(accepted, brokerageMessageHandler.LastHandleOrderResult);
+ Assert.AreEqual(accepted ? 1 : 0, transactionHandler.OrdersCount);
+ }
+
+ [Test]
+ public void UnrequestedSecuritiesAreAddedForNewBrokerageSideOrders()
+ {
+ //Initialize the transaction handler
+ var transactionHandler = new TestBrokerageTransactionHandler();
+ using var brokerage = new TestingBrokerage();
+ transactionHandler.Initialize(_algorithm, brokerage, new BacktestingResultHandler());
+
+ _algorithm.SetBrokerageModel(new DefaultBrokerageModel());
+ var brokerageMessageHandler = new TestBrokerageMessageHandler();
+ _algorithm.SetBrokerageMessageHandler(brokerageMessageHandler);
+
+ var symbol = Symbols.SPY;
+ Assert.IsFalse(_algorithm.Securities.ContainsKey(symbol));
+
+ var order = GetOrder(OrderType.Market, symbol);
+ brokerage.OnNewBrokerageOrder(new NewBrokerageOrderNotificationEventArgs(order));
+ Assert.IsTrue(brokerageMessageHandler.LastHandleOrderResult);
+ Assert.AreEqual(1, transactionHandler.OrdersCount);
+
+ Assert.IsTrue(_algorithm.Securities.TryGetValue(symbol, out var security));
+ Assert.AreEqual(symbol, security.Symbol);
+ }
+
internal class TestIncrementalOrderIdAlgorithm : OrderTicketDemoAlgorithm
{
public static readonly Dictionary OrderEventIds = new Dictionary();
@@ -2257,5 +2349,29 @@ public override IShortableProvider GetShortableProvider(Security security)
return new TestNonShortableProvider();
}
}
+
+ private class TestBrokerageMessageHandler : IBrokerageMessageHandler
+ {
+ public bool LastHandleOrderResult { get; private set; }
+
+ public void HandleMessage(BrokerageMessageEvent messageEvent)
+ {
+ }
+
+ public bool HandleOrder(NewBrokerageOrderNotificationEventArgs eventArgs)
+ {
+ // For testing purposes, only market orders are handled
+ return LastHandleOrderResult = eventArgs.Order.Type == OrderType.Market;
+
+ }
+ }
+
+ private class TestingBrokerage : TestBrokerage
+ {
+ public void OnNewBrokerageOrder(NewBrokerageOrderNotificationEventArgs e)
+ {
+ OnNewBrokerageOrderNotification(e);
+ }
+ }
}
}
diff --git a/Tests/Engine/CustomBrokerageMessageHandlerTests.cs b/Tests/Engine/CustomBrokerageMessageHandlerTests.cs
new file mode 100644
index 000000000000..17901e97eb09
--- /dev/null
+++ b/Tests/Engine/CustomBrokerageMessageHandlerTests.cs
@@ -0,0 +1,128 @@
+/*
+ * 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.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using QuantConnect.Brokerages;
+using QuantConnect.Brokerages.Backtesting;
+using QuantConnect.Interfaces;
+using QuantConnect.Lean.Engine.Setup;
+using QuantConnect.Orders;
+using QuantConnect.Packets;
+
+namespace QuantConnect.Tests.Engine
+{
+ [TestFixture]
+ public class CustomBrokerageMessageHandlerTests
+ {
+ [Test]
+ public void RunRegressionAlgorithm([Values(Language.CSharp, Language.Python)] Language language)
+ {
+ // We expect only half of the orders to be processed
+ var expectedOrdersCount = CustomBacktestingBrokerage.MaxOrderCount / 2;
+
+ var parameter = new RegressionTests.AlgorithmStatisticsTestParameters("CustomBrokerageSideOrderHandlingRegressionAlgorithm",
+ new Dictionary {
+ {"Total Trades", expectedOrdersCount.ToStringInvariant()},
+ {"Average Win", "0%"},
+ {"Average Loss", "0%"},
+ {"Compounding Annual Return", "-10.771%"},
+ {"Drawdown", "0.200%"},
+ {"Expectancy", "0"},
+ {"Net Profit", "-0.146%"},
+ {"Sharpe Ratio", "-5.186"},
+ {"Sortino Ratio", "-6.53"},
+ {"Probabilistic Sharpe Ratio", "24.692%"},
+ {"Loss Rate", "0%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "0.059"},
+ {"Beta", "-0.072"},
+ {"Annual Standard Deviation", "0.016"},
+ {"Annual Variance", "0"},
+ {"Information Ratio", "-8.629"},
+ {"Tracking Error", "0.239"},
+ {"Treynor Ratio", "1.154"},
+ {"Total Fees", "$50.00"},
+ {"Estimated Strategy Capacity", "$17000000.00"},
+ {"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
+ {"Portfolio Turnover", "1.45%"}
+ },
+ language,
+ AlgorithmStatus.Completed);
+
+ AlgorithmRunner.RunLocalBacktest(parameter.Algorithm,
+ parameter.Statistics,
+ parameter.Language,
+ parameter.ExpectedFinalStatus,
+ setupHandler: nameof(CustomBacktestingSetupHandler));
+ }
+
+ public class CustomBacktestingBrokerage : BacktestingBrokerage
+ {
+ public static readonly int MaxOrderCount = 100;
+
+ private OrderDirection _direction = OrderDirection.Buy;
+
+ private int _orderCount;
+
+ public CustomBacktestingBrokerage(IAlgorithm algorithm) : base(algorithm)
+ {
+ }
+
+ public override void Scan()
+ {
+ if (_orderCount <= MaxOrderCount)
+ {
+ var quantity = 0m;
+ // Only orders with even numbers in the tags will be processed
+ if (_orderCount % 2 == 0)
+ {
+ quantity = _direction == OrderDirection.Buy ? 1 : -1;
+ // Switch direction
+ _direction = OrderDirection.Sell;
+ }
+
+ var marketOrder = new MarketOrder(Symbols.SPY, quantity, Algorithm.UtcTime, tag: _orderCount.ToStringInvariant());
+ marketOrder.Status = OrderStatus.New;
+ OnNewBrokerageOrderNotification(new NewBrokerageOrderNotificationEventArgs(marketOrder));
+ _orderCount++;
+ }
+
+ base.Scan();
+ }
+ }
+
+ public class CustomBacktestingSetupHandler : BacktestingSetupHandler
+ {
+ public override IBrokerage CreateBrokerage(AlgorithmNodePacket algorithmNodePacket, IAlgorithm uninitializedAlgorithm, out IBrokerageFactory factory)
+ {
+ factory = new BacktestingBrokerageFactory();
+ var brokerage = new CustomBacktestingBrokerage(uninitializedAlgorithm);
+ brokerage.NewBrokerageOrderNotification += (sender, e) =>
+ {
+ if (uninitializedAlgorithm.BrokerageMessageHandler.HandleOrder(e) &&
+ uninitializedAlgorithm.GetOrAddUnrequestedSecurity(e.Order.Symbol, out _))
+ {
+ brokerage.PlaceOrder(e.Order);
+ }
+ };
+
+ return brokerage;
+ }
+ }
+ }
+}
diff --git a/Tests/Engine/DefaultBrokerageMessageHandler.cs b/Tests/Engine/DefaultBrokerageMessageHandlerTests.cs
similarity index 93%
rename from Tests/Engine/DefaultBrokerageMessageHandler.cs
rename to Tests/Engine/DefaultBrokerageMessageHandlerTests.cs
index 89dfae5b035b..cea20940d172 100644
--- a/Tests/Engine/DefaultBrokerageMessageHandler.cs
+++ b/Tests/Engine/DefaultBrokerageMessageHandlerTests.cs
@@ -44,7 +44,7 @@ public void DoesNotSetAlgorithmRunTimeErrorOnDisconnectIfAllSecuritiesClosed()
Assert.IsNull(algorithm.RunTimeError);
- handler.Handle(BrokerageMessageEvent.Disconnected("Disconnection!"));
+ handler.HandleMessage(BrokerageMessageEvent.Disconnected("Disconnection!"));
Assert.IsNull(algorithm.RunTimeError);
@@ -71,11 +71,11 @@ public void DoesNotSetRunTimeErrorWhenReconnectMessageComesThrough()
Assert.IsNull(algorithm.RunTimeError);
- handler.Handle(BrokerageMessageEvent.Disconnected("Disconnection!"));
+ handler.HandleMessage(BrokerageMessageEvent.Disconnected("Disconnection!"));
Thread.Sleep(100);
- handler.Handle(BrokerageMessageEvent.Reconnected("Reconnected!"));
+ handler.HandleMessage(BrokerageMessageEvent.Reconnected("Reconnected!"));
Thread.Sleep(500);
diff --git a/Tests/Engine/Setup/BrokerageSetupHandlerTests.cs b/Tests/Engine/Setup/BrokerageSetupHandlerTests.cs
index a4704c597c63..4c31cb0f0906 100644
--- a/Tests/Engine/Setup/BrokerageSetupHandlerTests.cs
+++ b/Tests/Engine/Setup/BrokerageSetupHandlerTests.cs
@@ -652,7 +652,7 @@ public static IEnumerable GetExistingHoldingsAndOrdersTestCaseData
yield return new TestCaseData(
new Func>(() => new List()),
new Func>(() => new List()), true);
-
+
yield return new TestCaseData(
new Func>(() => new List
{
@@ -662,7 +662,7 @@ public static IEnumerable GetExistingHoldingsAndOrdersTestCaseData
{
new LimitOrder(Symbols.SPY, 1, 1, DateTime.UtcNow)
}), true);
-
+
yield return new TestCaseData(
new Func>(() => new List
{
@@ -672,7 +672,7 @@ public static IEnumerable GetExistingHoldingsAndOrdersTestCaseData
{
new LimitOrder(Symbols.SPY_C_192_Feb19_2016, 1, 1, DateTime.UtcNow)
}), true);
-
+
yield return new TestCaseData(
new Func>(() => new List
{
@@ -684,7 +684,7 @@ public static IEnumerable GetExistingHoldingsAndOrdersTestCaseData
new LimitOrder(Symbols.SPY, 1, 1, DateTime.UtcNow),
new LimitOrder(Symbols.SPY_C_192_Feb19_2016, 1, 1, DateTime.UtcNow)
}), true);
-
+
yield return new TestCaseData(
new Func>(() => new List
{
@@ -696,7 +696,7 @@ public static IEnumerable GetExistingHoldingsAndOrdersTestCaseData
new LimitOrder(Symbols.SPY_C_192_Feb19_2016, 1, 1, DateTime.UtcNow),
new LimitOrder(Symbols.SPY, 1, 1, DateTime.UtcNow)
}), true);
-
+
yield return new TestCaseData(
new Func>(() => new List
{
@@ -706,7 +706,7 @@ public static IEnumerable GetExistingHoldingsAndOrdersTestCaseData
{
new LimitOrder(Symbols.SPY, 1, 1, DateTime.UtcNow),
}), true);
-
+
yield return new TestCaseData(
new Func>(() => new List
{
@@ -716,7 +716,7 @@ public static IEnumerable GetExistingHoldingsAndOrdersTestCaseData
{
new LimitOrder(Symbols.EURUSD, 1, 1, DateTime.UtcNow)
}), true);
-
+
yield return new TestCaseData(
new Func>(() => new List
{
@@ -726,7 +726,7 @@ public static IEnumerable GetExistingHoldingsAndOrdersTestCaseData
{
new LimitOrder(Symbols.BTCUSD, 1, 1, DateTime.UtcNow)
}), true);
-
+
yield return new TestCaseData(
new Func>(() => new List
{
@@ -736,7 +736,7 @@ public static IEnumerable GetExistingHoldingsAndOrdersTestCaseData
{
new LimitOrder(Symbols.Fut_SPY_Feb19_2016, 1, 1, DateTime.UtcNow)
}), true);
-
+
yield return new TestCaseData(
new Func>(() => new List
{
@@ -760,7 +760,7 @@ public static IEnumerable GetExistingHoldingsAndOrdersTestCaseData
yield return new TestCaseData(
new Func>(() => { throw new Exception(); }),
new Func>(() => new List()), false);
-
+
yield return new TestCaseData(
new Func>(() => new List()),
new Func>(() => { throw new Exception(); }), false);
@@ -818,11 +818,6 @@ public override void DebugMessage(string message)
private class TestableBrokerageSetupHandler : BrokerageSetupHandler
{
- protected override HashSet SupportedSecurityTypes => new HashSet
- {
- SecurityType.Equity, SecurityType.Forex, SecurityType.Cfd, SecurityType.Option, SecurityType.Future, SecurityType.Crypto
- };
-
public void PublicGetOpenOrders(IAlgorithm algorithm, IResultHandler resultHandler, ITransactionHandler transactionHandler, IBrokerage brokerage)
{
GetOpenOrders(algorithm, resultHandler, transactionHandler, brokerage);