/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using Moq; using NUnit.Framework; using QuantConnect.Brokerages; using QuantConnect.Brokerages.Backtesting; using QuantConnect.Brokerages.Paper; using QuantConnect.Data.Market; using QuantConnect.Lean.Engine.Results; using QuantConnect.Lean.Engine.TransactionHandlers; using QuantConnect.Logging; using QuantConnect.Orders; using QuantConnect.Orders.Fees; using QuantConnect.Orders.Fills; using QuantConnect.Securities; using QuantConnect.Tests.Engine.DataFeeds; namespace QuantConnect.Tests.Engine.BrokerageTransactionHandlerTests { [TestFixture] public class BacktestingTransactionHandlerTests { private const string Ticker = "EURUSD"; private BrokerageTransactionHandlerTests.TestAlgorithm _algorithm; [SetUp] public void Initialize() { _algorithm = new BrokerageTransactionHandlerTests.TestAlgorithm { HistoryProvider = new BrokerageTransactionHandlerTests.EmptyHistoryProvider() }; _algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(_algorithm)); _algorithm.SetBrokerageModel(BrokerageName.FxcmBrokerage); _algorithm.SetCash(100000); _algorithm.AddSecurity(SecurityType.Forex, Ticker); _algorithm.SetFinishedWarmingUp(); TestPartialFilledModel.FilledOrders = new Dictionary(); } [Test] public void InvalidOrderRequestWontSetTicketAsProcessed() { //Initializes the transaction handler var transactionHandler = new BacktestingTransactionHandler(); using var backtestingBrokerage = new BacktestingBrokerage(_algorithm); transactionHandler.Initialize(_algorithm, backtestingBrokerage, new BacktestingResultHandler()); try { // Creates the order var security = _algorithm.Securities[Ticker]; var orderRequest = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, 600, 0, 0, 0, DateTime.Now, ""); // Mock the the order processor var orderProcessorMock = new Mock(); orderProcessorMock.Setup(m => m.GetOrderTicket(It.IsAny())).Returns(new OrderTicket(_algorithm.Transactions, orderRequest)); _algorithm.Transactions.SetOrderProcessor(orderProcessorMock.Object); var ticket = transactionHandler.AddOrder(orderRequest); var ticket2 = transactionHandler.AddOrder(orderRequest); // 600 after round off becomes 0 -> order is not placed Assert.IsTrue(orderRequest.Response.IsProcessed); Assert.IsTrue(orderRequest.Response.IsError); Assert.IsTrue(orderRequest.Response.ErrorMessage .Contains("Cannot process submit request because order with id {0} already exists")); } finally { transactionHandler.Exit(); } } [Test] public void SendingNewOrderFromOnOrderEvent() { //Initializes the transaction handler var transactionHandler = new BacktestingTransactionHandler(); using var brokerage = new BacktestingBrokerage(_algorithm); transactionHandler.Initialize(_algorithm, brokerage, new BacktestingResultHandler()); try { // Creates a market order var security = _algorithm.Securities[Ticker]; var price = 1.12m; // Use a recent price so the data is not stale relative to the order submission time, otherwise the // market orders would wait for fresh data instead of filling. var reference = new DateTime(2026, 06, 26, 12, 0, 0); security.SetMarketPrice(new Tick(reference.AddMinutes(-1), security.Symbol, price, price, price)); var orderRequest = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, 1000, 0, 0, 0, reference, ""); var orderRequest2 = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, -1000, 0, 0, 0, reference, ""); orderRequest.SetOrderId(1); orderRequest2.SetOrderId(2); // Mock the the order processor var orderProcessorMock = new Mock(); orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is(i => i == 1))).Returns(new OrderTicket(_algorithm.Transactions, orderRequest)); orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is(i => i == 2))).Returns(new OrderTicket(_algorithm.Transactions, orderRequest2)); _algorithm.Transactions.SetOrderProcessor(orderProcessorMock.Object); var orderEventCalls = 0; brokerage.OrdersStatusChanged += (sender, orderEvents) => { orderEventCalls++; var orderEvent = orderEvents[0]; switch (orderEventCalls) { case 1: Assert.AreEqual(1, orderEvent.OrderId); Assert.AreEqual(OrderStatus.Submitted, orderEvent.Status); // we send a new order request var ticket2 = transactionHandler.Process(orderRequest2); break; case 2: Assert.AreEqual(2, orderEvent.OrderId); Assert.AreEqual(OrderStatus.Submitted, orderEvent.Status); break; case 3: Assert.AreEqual(1, orderEvent.OrderId); Assert.AreEqual(OrderStatus.Filled, orderEvent.Status); break; case 4: Assert.AreEqual(2, orderEvent.OrderId); Assert.AreEqual(OrderStatus.Filled, orderEvent.Status); break; } Log.Trace($"{orderEvent}"); }; var ticket = transactionHandler.Process(orderRequest); Assert.IsTrue(orderRequest.Response.IsProcessed); Assert.IsTrue(orderRequest.Response.IsSuccess); Assert.AreEqual(OrderRequestStatus.Processed, orderRequest.Status); Assert.IsTrue(orderRequest2.Response.IsProcessed); Assert.IsTrue(orderRequest2.Response.IsSuccess); Assert.AreEqual(OrderRequestStatus.Processed, orderRequest2.Status); var order1 = transactionHandler.GetOrderById(1); Assert.AreEqual(OrderStatus.Filled, order1.Status); var order2 = transactionHandler.GetOrderById(2); Assert.AreEqual(OrderStatus.Filled, order2.Status); // 2 submitted and 2 filled Assert.AreEqual(4, orderEventCalls); } finally { transactionHandler.Exit(); } } [Test] public void SendingNewOrderFromPartiallyFilledOnOrderEvent() { //Initializes the transaction handler var transactionHandler = new BacktestingTransactionHandler(); using var brokerage = new BacktestingBrokerage(_algorithm); transactionHandler.Initialize(_algorithm, brokerage, new BacktestingResultHandler()); try { // Creates a market order var security = _algorithm.Securities[Ticker]; security.FillModel = new TestPartialFilledModel(); var price = 1.12m; security.SetMarketPrice(new Tick(DateTime.UtcNow.AddDays(-1), security.Symbol, price, price, price)); var orderRequest = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, 2000, 0, 0, 9, DateTime.UtcNow, ""); var orderRequest2 = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, -2000, 0, 0, 9, DateTime.UtcNow, ""); orderRequest.SetOrderId(1); orderRequest2.SetOrderId(2); // Mock the the order processor var orderProcessorMock = new Mock(); orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is(i => i == 1))).Returns(new OrderTicket(_algorithm.Transactions, orderRequest)); orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is(i => i == 2))).Returns(new OrderTicket(_algorithm.Transactions, orderRequest2)); _algorithm.Transactions.SetOrderProcessor(orderProcessorMock.Object); var orderEventCalls = 0; brokerage.OrdersStatusChanged += (sender, orderEvents) => { orderEventCalls++; var orderEvent = orderEvents[0]; switch (orderEventCalls) { case 1: Assert.AreEqual(1, orderEvent.OrderId); Assert.AreEqual(OrderStatus.Submitted, orderEvent.Status); // we send a new order request var ticket2 = transactionHandler.Process(orderRequest2); break; case 2: Assert.AreEqual(2, orderEvent.OrderId); Assert.AreEqual(OrderStatus.Submitted, orderEvent.Status); break; case 3: Assert.AreEqual(1, orderEvent.OrderId); Assert.AreEqual(OrderStatus.PartiallyFilled, orderEvent.Status); break; case 4: Assert.AreEqual(2, orderEvent.OrderId); Assert.AreEqual(OrderStatus.PartiallyFilled, orderEvent.Status); break; case 5: Assert.AreEqual(1, orderEvent.OrderId); Assert.AreEqual(OrderStatus.Filled, orderEvent.Status); break; case 6: Assert.AreEqual(2, orderEvent.OrderId); Assert.AreEqual(OrderStatus.Filled, orderEvent.Status); break; } Log.Trace($"{orderEvent}"); }; var ticket = transactionHandler.Process(orderRequest); Assert.IsTrue(orderRequest.Response.IsProcessed); Assert.IsTrue(orderRequest.Response.IsSuccess); Assert.AreEqual(OrderRequestStatus.Processed, orderRequest.Status); Assert.IsTrue(orderRequest2.Response.IsProcessed); Assert.IsTrue(orderRequest2.Response.IsSuccess); Assert.AreEqual(OrderRequestStatus.Processed, orderRequest2.Status); var order1 = transactionHandler.GetOrderById(1); Assert.AreEqual(OrderStatus.Filled, order1.Status); var order2 = transactionHandler.GetOrderById(2); Assert.AreEqual(OrderStatus.Filled, order2.Status); // 2 submitted and 2 PartiallyFilled and 2 Filled Assert.AreEqual(6, orderEventCalls); } finally { transactionHandler.Exit(); } } [Test] public void ProcessesOrdersInLivePaperTrading() { //Initializes the transaction handler var transactionHandler = new BacktestingTransactionHandler(); using var brokerage = new PaperBrokerage(_algorithm, null); _algorithm.SetLiveMode(true); transactionHandler.Initialize(_algorithm, brokerage, new BacktestingResultHandler()); try { // Creates a market order var security = _algorithm.Securities[Ticker]; var price = 1.12m; security.SetMarketPrice(new Tick(DateTime.UtcNow.AddDays(-1), security.Symbol, price, price, price)); var reference = new DateTime(2025, 07, 03, 10, 0, 0); var orderRequest = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, 1000, 0, 0, 0, reference, ""); var orderRequest2 = new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, -1000, 0, 0, 0, reference.AddSeconds(1), ""); orderRequest.SetOrderId(1); orderRequest2.SetOrderId(2); // Mock the the order processor var orderProcessorMock = new Mock(); orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is(i => i == 1))).Returns(new OrderTicket(_algorithm.Transactions, orderRequest)); orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is(i => i == 2))).Returns(new OrderTicket(_algorithm.Transactions, orderRequest2)); _algorithm.Transactions.SetOrderProcessor(orderProcessorMock.Object); var allOrderEvents = new List(); using var eventsReceived = new AutoResetEvent(false); brokerage.OrdersStatusChanged += (sender, orderEvents) => { var orderEvent = orderEvents[0]; lock (allOrderEvents) { allOrderEvents.Add(orderEvent); if (allOrderEvents.Count == 4) { eventsReceived.Set(); } } // Let's place another order before this one is filled if (orderEvent.OrderId == 1 && orderEvent.Status == OrderStatus.Submitted) { var ticket2 = transactionHandler.Process(orderRequest2); } Log.Debug($"{orderEvent}"); }; var ticket = transactionHandler.Process(orderRequest); if (!eventsReceived.WaitOne(10000)) { Assert.Fail($"Did not receive all order events, received {allOrderEvents.Count} order events: {string.Join(", ", allOrderEvents)}"); } Assert.IsTrue(orderRequest.Response.IsProcessed); Assert.IsTrue(orderRequest.Response.IsSuccess); Assert.AreEqual(OrderRequestStatus.Processed, orderRequest.Status); Assert.IsTrue(orderRequest2.Response.IsProcessed); Assert.IsTrue(orderRequest2.Response.IsSuccess); Assert.AreEqual(OrderRequestStatus.Processed, orderRequest2.Status); var order1 = transactionHandler.GetOrderById(1); Assert.AreEqual(OrderStatus.Filled, order1.Status); var order2 = transactionHandler.GetOrderById(2); Assert.AreEqual(OrderStatus.Filled, order2.Status); // 2 submitted and 2 filled Assert.AreEqual(4, allOrderEvents.Count); var firstOrderSubmittedEvent = allOrderEvents.FirstOrDefault(x => x.OrderId == 1 && x.Status == OrderStatus.Submitted); Assert.IsNotNull(firstOrderSubmittedEvent); var firstOrderFilledEvent = allOrderEvents.FirstOrDefault(x => x.OrderId == 1 && x.Status == OrderStatus.Filled); Assert.IsNotNull(firstOrderFilledEvent); var secondOrderSubmittedEvent = allOrderEvents.FirstOrDefault(x => x.OrderId == 2 && x.Status == OrderStatus.Submitted); Assert.IsNotNull(secondOrderSubmittedEvent); var secondOrderFilledEvent = allOrderEvents.FirstOrDefault(x => x.OrderId == 2 && x.Status == OrderStatus.Filled); Assert.IsNotNull(secondOrderFilledEvent); } finally { transactionHandler.Exit(); } } [Test] public void ProcessesOrdersConcurrentlyInLivePaperTrading() { _algorithm.SetLiveMode(true); using var brokerage = new PaperBrokerage(_algorithm, null); const int expectedOrdersCount = 20; using var finishedEvent = new ManualResetEventSlim(false); var transactionHandler = new TestablePaperBrokerageTransactionHandler(expectedOrdersCount, finishedEvent); transactionHandler.Initialize(_algorithm, brokerage, new BacktestingResultHandler()); try { _algorithm.Transactions.SetOrderProcessor(transactionHandler); var security = (Security)_algorithm.AddEquity("SPY"); _algorithm.SetFinishedWarmingUp(); // Set up security var reference = new DateTime(2025, 07, 03, 10, 0, 0); security.SetMarketPrice(new Tick(reference, security.Symbol, 300, 300)); // Creates the order var orderRequests = Enumerable.Range(0, expectedOrdersCount) .Select(_ => new SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, 1000, 0, 0, 0, reference, "")) .ToList(); // Act for (var i = 0; i < orderRequests.Count; i++) { var orderRequest = orderRequests[i]; orderRequest.SetOrderId(i + 1); transactionHandler.Process(orderRequest); } // Wait for all orders to be processed Assert.IsTrue(finishedEvent.Wait(10000)); Assert.Greater(transactionHandler.ProcessingThreadNames.Count, 1); CollectionAssert.AreEquivalent(orderRequests.Select(x => x.ToString()), transactionHandler.ProcessedRequests.Select(x => x.ToString())); } finally { transactionHandler.Exit(); } } private class TestablePaperBrokerageTransactionHandler : BacktestingTransactionHandler { private readonly int _expectedOrdersCount; private readonly ManualResetEventSlim _finishedEvent; private int _currentOrdersCount; public HashSet ProcessingThreadNames = new(); public ConcurrentBag ProcessedRequests = new(); public TestablePaperBrokerageTransactionHandler(int expectedOrdersCount, ManualResetEventSlim finishedEvent) { _expectedOrdersCount = expectedOrdersCount; _finishedEvent = finishedEvent; } public override void HandleOrderRequest(OrderRequest request) { base.HandleOrderRequest(request); // Capture the thread name for debugging purposes lock (ProcessingThreadNames) { ProcessingThreadNames.Add(Thread.CurrentThread.Name ?? Environment.CurrentManagedThreadId.ToStringInvariant()); } ProcessedRequests.Add(request); if (Interlocked.Increment(ref _currentOrdersCount) >= _expectedOrdersCount) { // Signal that we have processed the expected number of orders _finishedEvent.Set(); } } } internal class TestPartialFilledModel : IFillModel { public static Dictionary FilledOrders; public Fill Fill(FillModelParameters parameters) { var order = parameters.Order; var status = OrderStatus.PartiallyFilled; if (FilledOrders.ContainsKey(order.Id)) { status = OrderStatus.Filled; } FilledOrders[order.Id] = order; return new Fill(new OrderEvent(order, DateTime.UtcNow, OrderFee.Zero) { FillPrice = parameters.Security.Price, FillQuantity = order.Quantity / 2, Status = status }); } } } }