Files
quantconnect--lean/Tests/Engine/BrokerageTransactionHandlerTests/BacktestingTransactionHandlerTests.cs
T
2026-07-13 13:02:50 +08:00

459 lines
21 KiB
C#

/*
* 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<int, Order>();
}
[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<IOrderProcessor>();
orderProcessorMock.Setup(m => m.GetOrderTicket(It.IsAny<int>())).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<IOrderProcessor>();
orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is<int>(i => i == 1))).Returns(new OrderTicket(_algorithm.Transactions, orderRequest));
orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is<int>(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<IOrderProcessor>();
orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is<int>(i => i == 1))).Returns(new OrderTicket(_algorithm.Transactions, orderRequest));
orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is<int>(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<IOrderProcessor>();
orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is<int>(i => i == 1))).Returns(new OrderTicket(_algorithm.Transactions, orderRequest));
orderProcessorMock.Setup(m => m.GetOrderTicket(It.Is<int>(i => i == 2))).Returns(new OrderTicket(_algorithm.Transactions, orderRequest2));
_algorithm.Transactions.SetOrderProcessor(orderProcessorMock.Object);
var allOrderEvents = new List<OrderEvent>();
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<string> ProcessingThreadNames = new();
public ConcurrentBag<OrderRequest> 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<int, Order> 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
});
}
}
}
}