From 3dab2e42eb4c34b7221db58b1698dd915007e7d6 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 23 Jan 2024 12:01:03 -0400 Subject: [PATCH] Handle/filter new brokerage-side orders (#7706) * Handle/filter brokerage side orders * Add unit tests * Minor fix * Add unrequested security for new brokerage-side orders * Add unit test for algorithms overriding the brokerage message handler * Add unit test for python algorithm overriding the brokerage message handler * Minor change * Address peer review --- .../CustomBrokerageMessageHandlerAlgorithm.cs | 16 ++- ...ageSideOrderHandlingRegressionAlgorithm.cs | 93 +++++++++++++ ...ageSideOrderHandlingRegressionAlgorithm.py | 63 +++++++++ .../DefaultBrokerageMessageHandler.cs | 12 +- ...rorCodeToWarningBrokerageMessageHandler.cs | 18 ++- Common/Brokerages/IBrokerageMessageHandler.cs | 11 +- Common/Extensions.cs | 76 +++++++++++ .../BrokerageMessageHandlerPythonWrapper.cs | 19 ++- Engine/Engine.cs | 2 +- Engine/Setup/BacktestingSetupHandler.cs | 4 +- Engine/Setup/BrokerageSetupHandler.cs | 67 +-------- .../BrokerageTransactionHandler.cs | 18 ++- ...deToWarningBrokerageMessageHandlerTests.cs | 18 +-- .../BrokerageTransactionHandlerTests.cs | 116 ++++++++++++++++ .../CustomBrokerageMessageHandlerTests.cs | 128 ++++++++++++++++++ ...=> DefaultBrokerageMessageHandlerTests.cs} | 6 +- .../Setup/BrokerageSetupHandlerTests.cs | 25 ++-- 17 files changed, 585 insertions(+), 107 deletions(-) create mode 100644 Algorithm.CSharp/CustomBrokerageSideOrderHandlingRegressionAlgorithm.cs create mode 100644 Algorithm.Python/CustomBrokerageSideOrderHandlingRegressionAlgorithm.py create mode 100644 Tests/Engine/CustomBrokerageMessageHandlerTests.cs rename Tests/Engine/{DefaultBrokerageMessageHandler.cs => DefaultBrokerageMessageHandlerTests.cs} (93%) 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);