/*
* 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.Linq;
using NUnit.Framework;
using System.Threading;
using QuantConnect.Util;
using QuantConnect.Orders;
using QuantConnect.Logging;
using System.Threading.Tasks;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using QuantConnect.Brokerages;
using QuantConnect.Orders.Fees;
using System.Collections.Generic;
using QuantConnect.Brokerages.CrossZero;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Brokerages
{
[TestFixture]
public class OrderCrossingBrokerageTests
{
///
/// Provides a collection of test case data for order scenarios.
///
///
/// This property generates test case data for various order statuses, specifically
/// for a Stop Market Order on the AAPL symbol.
///
///
/// An containing test cases with a stop market order and an array of order statuses.
///
private static IEnumerable OrderParameters
{
get
{
var expectedOrderStatusChangedOrdering = new[] { OrderStatus.Submitted, OrderStatus.PartiallyFilled, OrderStatus.Filled };
yield return new TestCaseData(new MarketOrder(Symbols.AAPL, -15, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new LimitOrder(Symbols.AAPL, -15, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new StopMarketOrder(Symbols.AAPL, -20, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new StopLimitOrder(Symbols.AAPL, -15, 180m, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
}
}
///
/// Tests placing an order and updating it, verifying the sequence of order status changes.
///
/// The order to be placed and updated.
/// The expected sequence of order status changes.
[Test, TestCaseSource(nameof(OrderParameters))]
public void PlaceCrossOrder(Order leanOrder, OrderStatus[] expectedOrderStatusChangedOrdering)
{
var actualCrossZeroOrderStatusOrdering = new Queue();
using var autoResetEventPartialFilledStatus = new AutoResetEvent(false);
using var autoResetEventFilledStatus = new AutoResetEvent(false);
using var brokerage = InitializeBrokerage((leanOrder?.Symbol.Value, 180m, 10));
brokerage.OrdersStatusChanged += (_, orderEvents) =>
{
var orderEventStatus = orderEvents[0].Status;
actualCrossZeroOrderStatusOrdering.Enqueue(orderEventStatus);
Log.Trace($"{nameof(PlaceCrossOrder)}.OrdersStatusChangedEvent.Status: {orderEventStatus}");
if (orderEventStatus == OrderStatus.PartiallyFilled)
{
autoResetEventPartialFilledStatus.Set();
}
if (orderEventStatus == OrderStatus.Filled)
{
autoResetEventFilledStatus.Set();
}
};
var response = brokerage.PlaceOrder(leanOrder);
Assert.IsTrue(response);
AssertComingOrderStatusByEvent(autoResetEventPartialFilledStatus, brokerage, OrderStatus.PartiallyFilled);
AssertComingOrderStatusByEvent(autoResetEventFilledStatus, brokerage, OrderStatus.Filled);
CollectionAssert.AreEquivalent(expectedOrderStatusChangedOrdering, actualCrossZeroOrderStatusOrdering);
Assert.AreEqual(0, brokerage.GetLeanOrderByZeroCrossBrokerageOrderIdCount());
}
///
/// Provides a collection of test case data for order update scenarios.
///
///
/// This property generates test case data for various order statuses, specifically
/// for a Stop Market Order on the AAPL symbol.
///
///
/// An containing test cases with a stop market order and an array of order statuses.
///
private static IEnumerable OrderUpdateParameters
{
get
{
var expectedOrderStatusChangedOrdering = new[] { OrderStatus.Submitted, OrderStatus.PartiallyFilled, OrderStatus.UpdateSubmitted, OrderStatus.Filled };
yield return new TestCaseData(new MarketOrder(Symbols.AAPL, -15, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new LimitOrder(Symbols.AAPL, -15, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new StopMarketOrder(Symbols.AAPL, -20, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new StopLimitOrder(Symbols.AAPL, -15, 180m, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
}
}
///
/// Tests placing an order and updating it, verifying the sequence of order status changes.
///
/// The order to be placed and updated.
/// The expected sequence of order status changes.
[Test, TestCaseSource(nameof(OrderUpdateParameters))]
public void PlaceCrossOrderAndUpdate(Order leanOrder, OrderStatus[] expectedOrderStatusChangedOrdering)
{
var actualCrossZeroOrderStatusOrdering = new Queue();
using var autoResetEventPartialFilledStatus = new AutoResetEvent(false);
using var autoResetEventUpdateSubmittedStatus = new AutoResetEvent(false);
using var autoResetEventFilledStatus = new AutoResetEvent(false);
using var brokerage = InitializeBrokerage((leanOrder?.Symbol.Value, 180m, 10));
brokerage.OrdersStatusChanged += (_, orderEvents) =>
{
var orderEventStatus = orderEvents[0].Status;
actualCrossZeroOrderStatusOrdering.Enqueue(orderEventStatus);
Log.Trace($"{nameof(PlaceCrossOrder)}.OrdersStatusChangedEvent.Status: {orderEventStatus}");
if (orderEventStatus == OrderStatus.PartiallyFilled)
{
autoResetEventPartialFilledStatus.Set();
}
if (orderEventStatus == OrderStatus.UpdateSubmitted)
{
autoResetEventUpdateSubmittedStatus.Set();
}
if (orderEventStatus == OrderStatus.Filled)
{
autoResetEventFilledStatus.Set();
}
};
var response = brokerage.PlaceOrder(leanOrder);
Assert.IsTrue(response);
AssertComingOrderStatusByEvent(autoResetEventPartialFilledStatus, brokerage, OrderStatus.PartiallyFilled);
var updateResponse = brokerage.UpdateOrder(leanOrder);
Assert.IsTrue(updateResponse);
AssertComingOrderStatusByEvent(autoResetEventUpdateSubmittedStatus, brokerage, OrderStatus.UpdateSubmitted);
AssertComingOrderStatusByEvent(autoResetEventFilledStatus, brokerage, OrderStatus.Filled);
CollectionAssert.AreEquivalent(expectedOrderStatusChangedOrdering, actualCrossZeroOrderStatusOrdering);
Assert.AreEqual(0, brokerage.GetLeanOrderByZeroCrossBrokerageOrderIdCount());
}
private static IEnumerable CrossZeroInvalidFirstPartParameters
{
get
{
var expectedOrderStatusChangedOrdering = new[] { OrderStatus.Submitted, OrderStatus.Invalid };
yield return new TestCaseData(new MarketOrder(Symbols.AAPL, -15, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new LimitOrder(Symbols.AAPL, -15, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new StopMarketOrder(Symbols.AAPL, -20, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new StopLimitOrder(Symbols.AAPL, -15, 180m, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
}
}
[Test, TestCaseSource(nameof(CrossZeroInvalidFirstPartParameters))]
public void PlaceCrossOrderInvalid(Order leanOrder, OrderStatus[] expectedOrderStatusChangedOrdering)
{
var actualCrossZeroOrderStatusOrdering = new Queue();
using var autoResetEventInvalidStatus = new AutoResetEvent(false);
using var brokerage = InitializeBrokerage((leanOrder?.Symbol.Value, 180m, 10));
brokerage.OrdersStatusChanged += (_, orderEvents) =>
{
var orderEventStatus = orderEvents[0].Status;
actualCrossZeroOrderStatusOrdering.Enqueue(orderEventStatus);
Log.Trace($"{nameof(PlaceCrossOrder)}.OrdersStatusChangedEvent.Status: {orderEventStatus}");
if (orderEventStatus == OrderStatus.Invalid)
{
autoResetEventInvalidStatus.Set();
}
};
brokerage.IsPlaceOrderPhonyBrokerageFirstPartSuccessfully = false;
brokerage.IsPlaceOrderPhonyBrokerageSecondPartSuccessfully = false;
var response = brokerage.PlaceOrder(leanOrder);
Assert.IsFalse(response);
AssertComingOrderStatusByEvent(autoResetEventInvalidStatus, brokerage, OrderStatus.Invalid);
CollectionAssert.AreEquivalent(expectedOrderStatusChangedOrdering, actualCrossZeroOrderStatusOrdering);
Assert.AreEqual(0, brokerage.GetLeanOrderByZeroCrossBrokerageOrderIdCount());
}
private static IEnumerable OrderCrossZeroSecondPartParameters
{
get
{
var expectedOrderStatusChangedOrdering = new[] { OrderStatus.Submitted, OrderStatus.PartiallyFilled, OrderStatus.Canceled };
yield return new TestCaseData(new MarketOrder(Symbols.AAPL, -15, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new LimitOrder(Symbols.AAPL, -15, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new StopMarketOrder(Symbols.AAPL, -20, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
yield return new TestCaseData(new StopLimitOrder(Symbols.AAPL, -15, 180m, 180m, new DateTime(2024, 6, 10)), expectedOrderStatusChangedOrdering);
}
}
[Test, TestCaseSource(nameof(OrderCrossZeroSecondPartParameters))]
public void PlaceCrossZeroSecondPartInvalid(Order leanOrder, OrderStatus[] expectedOrderStatusChangedOrdering)
{
var actualCrossZeroOrderStatusOrdering = new Queue();
using var autoResetEventInvalidStatus = new AutoResetEvent(false);
using var brokerage = InitializeBrokerage((leanOrder?.Symbol.Value, 180m, 10));
var skipFirstFilledEvent = default(bool);
brokerage.OrdersStatusChanged += (_, orderEvents) =>
{
var orderEventStatus = orderEvents[0].Status;
// Skip processing the first occurrence of the Filled event, The First Part of CrossZeroOrder was filled.
if (!skipFirstFilledEvent && orderEventStatus == OrderStatus.Filled)
{
skipFirstFilledEvent = true;
return;
}
actualCrossZeroOrderStatusOrdering.Enqueue(orderEventStatus);
Log.Trace($"{nameof(PlaceCrossOrder)}.OrdersStatusChangedEvent.Status: {orderEventStatus}");
if (orderEventStatus == OrderStatus.Canceled)
{
autoResetEventInvalidStatus.Set();
}
};
brokerage.IsPlaceOrderPhonyBrokerageFirstPartSuccessfully = true;
brokerage.IsPlaceOrderPhonyBrokerageSecondPartSuccessfully = false;
var response = brokerage.PlaceOrder(leanOrder);
Assert.IsTrue(response);
AssertComingOrderStatusByEvent(autoResetEventInvalidStatus, brokerage, OrderStatus.Canceled);
CollectionAssert.AreEquivalent(expectedOrderStatusChangedOrdering, actualCrossZeroOrderStatusOrdering);
Assert.AreEqual(0, brokerage.GetLeanOrderByZeroCrossBrokerageOrderIdCount());
}
///
/// Create instance of Phony brokerage.
///
/// ("AAPL", 190m, 10)
/// The instance of Phony Brokerage
private static PhonyBrokerage InitializeBrokerage(params (string ticker, decimal averagePrice, decimal quantity)[] equityQuantity)
{
var algorithm = new AlgorithmStub();
foreach (var (symbol, averagePrice, quantity) in equityQuantity)
{
algorithm.AddEquity(symbol).Holdings.SetHoldings(averagePrice, quantity);
}
var brokerage = new PhonyBrokerage("Phony", algorithm);
AssertThatHoldingIsNotEmpty(brokerage);
return brokerage;
}
///
/// Asserts that the brokerage account holdings are not empty and that the first holding has a positive quantity.
///
/// The brokerage instance to check the account holdings.
///
/// Thrown if the account holdings are empty or the first holding has a non-positive quantity.
///
private static void AssertThatHoldingIsNotEmpty(IBrokerage brokerage)
{
var holdings = brokerage.GetAccountHoldings();
Assert.Greater(holdings.Count, 0);
Assert.Greater(holdings[0].Quantity, 0);
}
///
/// Asserts that an order with the specified status has arrived by waiting for an event signal.
///
/// The event to wait on, which signals that an order status update has occurred.
/// The phony brokerage instance used to check the order status.
/// The expected status of the coming order to assert.
///
/// Thrown if there is not exactly one order with the specified status.
///
private static void AssertComingOrderStatusByEvent(AutoResetEvent resetEvent, PhonyBrokerage brokerage, OrderStatus comingOrderStatus)
{
Assert.True(resetEvent.WaitOne(TimeSpan.FromSeconds(5)));
var partialFilledOrder = brokerage.GetAllOrders(o => o.Status == comingOrderStatus).Single();
Assert.IsNotNull(partialFilledOrder);
}
private class PhonyBrokerage : Brokerage
{
///
private readonly IAlgorithm _algorithm;
///
private readonly ISecurityProvider _securityProvider;
///
private readonly CustomOrderProvider _orderProvider;
///
private readonly CancellationTokenSource _cancellationTokenSource = new();
///
/// Temporarily stores the IDs of brokerage orders for testing purposes.
///
private List _tempBrokerageOrderIds = new();
///
/// This field indicates whether the order has been placed successfully with a phony brokerage during testing.
///
///
/// The default value is true. This is used specifically for testing purposes to simulate the successful placement of an order.
///
public bool IsPlaceOrderPhonyBrokerageFirstPartSuccessfully { get; set; } = true;
///
/// This field indicates whether the second part of the order has been placed successfully with a phony brokerage during testing.
///
///
/// The default value is true. This is used specifically for testing purposes to simulate the successful placement of the second part of an order.
///
public bool IsPlaceOrderPhonyBrokerageSecondPartSuccessfully { get; set; } = true;
public override bool IsConnected => true;
public PhonyBrokerage(string name, IAlgorithm algorithm) : base(name)
{
_algorithm = algorithm;
_orderProvider = new CustomOrderProvider();
_securityProvider = algorithm.Portfolio;
OrdersStatusChanged += OrdersStatusChangedEventHandler;
ImitationBrokerageOrderUpdates();
}
private void OrdersStatusChangedEventHandler(object _, List orderEvents)
{
var orderEvent = orderEvents[0];
var brokerageOrderId = _tempBrokerageOrderIds.Last();
if (!TryGetOrRemoveCrossZeroOrder(brokerageOrderId, orderEvent.Status, out var leanOrder))
{
leanOrder = _orderProvider.GetOrderById(orderEvent.OrderId);
}
if (!TryHandleRemainingCrossZeroOrder(leanOrder, orderEvent))
{
_orderProvider.UpdateOrderStatusById(orderEvent.OrderId, orderEvent.Status);
}
}
public IEnumerable GetAllOrders(Func filter)
{
return _orderProvider.GetOrders(filter);
}
public override bool CancelOrder(Order order)
{
OnOrderEvent(new OrderEvent(order, new DateTime(2024, 6, 10), OrderFee.Zero, "CancelOrder") { Status = OrderStatus.Canceled });
return true;
}
public override void Connect()
{
throw new NotImplementedException();
}
public override void Disconnect()
{
throw new NotImplementedException();
}
public override List GetAccountHoldings()
{
return base.GetAccountHoldings(null, _algorithm.Securities.Values);
}
public override List GetCashBalance()
{
throw new NotImplementedException();
}
public override List GetOpenOrders()
{
throw new NotImplementedException();
}
///
/// Gets the count of Lean orders indexed by ZeroCross brokerage order ID.
///
///
/// The number of Lean orders that are indexed by ZeroCross brokerage order ID.
///
public int GetLeanOrderByZeroCrossBrokerageOrderIdCount()
{
return LeanOrderByZeroCrossBrokerageOrderId.Count;
}
public override bool PlaceOrder(Order order)
{
// For testing purposes only: Adds the specified order to the order provider.
_orderProvider.Add(order);
var holdingQuantity = _securityProvider.GetHoldingsQuantity(order.Symbol);
var isPlaceCrossOrder = TryCrossZeroPositionOrder(order, holdingQuantity);
// Alert: This test covers only CrossZeroOrdering scenarios.
// If isPlaceCrossOrder is null, it indicates failure to place a cross order.
// Please ensure your account has sufficient securities and try again.
if (isPlaceCrossOrder == null)
{
Assert.Fail("Unable to place a cross order. Please ensure your account holds the necessary securities and try again.");
}
return isPlaceCrossOrder.Value;
}
///
/// Places a cross-zero order with the PhonyBrokerage.
///
/// The cross-zero order request.
/// Flag indicating whether to place the order without Lean event.
///
/// A containing the result of placing the order.
///
protected override CrossZeroOrderResponse PlaceCrossZeroOrder(CrossZeroFirstOrderRequest crossZeroOrderRequest, bool isPlaceOrderWithoutLeanEvent)
{
Log.Trace($"{nameof(PhonyBrokerage)}.{nameof(PlaceCrossZeroOrder)}");
// Step 1: Create order request under the hood of any brokerage
var brokeragePhonyParameterRequest = new PhonyPlaceOrderRequest(crossZeroOrderRequest.LeanOrder.Symbol.Value, crossZeroOrderRequest.OrderQuantity,
crossZeroOrderRequest.LeanOrder.Direction, 0m, crossZeroOrderRequest.OrderType);
// Step 2: Place the order request, paying attention to the flag 'isPlaceOrderWithoutLeanEvent'
var response = PlaceOrderPhonyBrokerage(crossZeroOrderRequest.LeanOrder, isPlaceOrderWithoutLeanEvent, brokeragePhonyParameterRequest);
// Step 3: Return the result of placing the order
return new CrossZeroOrderResponse(response.OrderId, response.IsOrderPlacedSuccessfully, response.Message);
}
///
/// Places an order with the PhonyBrokerage.
///
/// The original Lean order.
/// Flag indicating whether to trigger the order submitted event.
/// The order request parameters.
///
/// A containing the result of placing the order.
///
private PhonyPlaceOrderResponse PlaceOrderPhonyBrokerage(Order originalLeanOrder, bool isSubmittedEvent = true, PhonyPlaceOrderRequest orderRequest = default)
{
var newOrderId = Guid.NewGuid().ToString();
_tempBrokerageOrderIds.Add(newOrderId);
if (isSubmittedEvent)
{
OnOrderEvent(new OrderEvent(originalLeanOrder, new DateTime(2024, 6, 10), OrderFee.Zero) { Status = OrderStatus.Submitted });
}
if (IsPlaceOrderPhonyBrokerageFirstPartSuccessfully)
{
IsPlaceOrderPhonyBrokerageFirstPartSuccessfully = false;
return new PhonyPlaceOrderResponse(newOrderId, true);
}
if (IsPlaceOrderPhonyBrokerageSecondPartSuccessfully)
{
return new PhonyPlaceOrderResponse(newOrderId, true);
}
return new PhonyPlaceOrderResponse(newOrderId, false, "Something was wrong");
}
public override void Dispose()
{
_cancellationTokenSource.Dispose();
base.Dispose();
}
public override bool UpdateOrder(Order order)
{
OnOrderEvent(new OrderEvent(order, new DateTime(2024, 6, 10), OrderFee.Zero, $"{nameof(PhonyBrokerage)} Order Event")
{
Status = OrderStatus.UpdateSubmitted
});
return true;
}
///
/// Simulates a brokerage sending order update events at regular intervals.
///
///
/// This method starts a new long-running task that periodically checks for open orders
/// and updates their status. Specifically, it transitions orders with statuses
/// or
/// to after a fixed delay.
///
///
///
/// // Example usage
/// var brokerage = new Brokerage();
/// brokerage.ImitationBrokerageOrderUpdates();
///
///
///
/// Thrown if the operation is canceled via the cancellation token.
///
private void ImitationBrokerageOrderUpdates()
{
Task.Factory.StartNew(() =>
{
while (!_cancellationTokenSource.IsCancellationRequested)
{
_cancellationTokenSource.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(3));
var orders = _orderProvider.GetOpenOrders();
foreach (var order in orders)
{
if (order.Status == OrderStatus.Submitted || order.Status == OrderStatus.PartiallyFilled || order.Status == OrderStatus.UpdateSubmitted)
{
var orderEvent = new OrderEvent(order, new DateTime(2024, 6, 10), OrderFee.Zero) { Status = OrderStatus.Filled };
if (!TryHandleRemainingCrossZeroOrder(order, orderEvent))
{
OnOrderEvent(orderEvent);
}
}
}
}
}, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
///
/// Represents a request to place an order in the Phony Brokerage system.
///
private protected readonly struct PhonyPlaceOrderRequest
{
///
/// Gets the symbol for the order.
///
public string Symbol { get; }
///
/// Gets the quantity for the order.
///
public decimal Quantity { get; }
///
/// Gets the direction of the order.
///
public OrderPosition Direction { get; }
///
/// Gets the custom brokerage order type.
///
public OrderType CustomBrokerageOrderType { get; }
///
/// Initializes a new instance of the struct.
///
/// The symbol for the order.
/// The quantity for the order.
/// The direction of the order.
/// The quantity currently held.
/// The type of the order.
public PhonyPlaceOrderRequest(string symbol, decimal quantity, OrderDirection orderDirection, decimal holdingQuantity, OrderType leanOrderType)
{
Symbol = symbol;
Quantity = quantity;
Direction = GetOrderPosition(orderDirection, holdingQuantity);
CustomBrokerageOrderType = leanOrderType;
}
}
///
/// Represents a response from placing an order in the Phony Brokerage system.
///
private protected readonly struct PhonyPlaceOrderResponse
{
///
/// Gets the unique identifier for the placed order.
///
public string OrderId { get; }
///
/// Gets a value indicating whether the order was placed successfully.
///
public bool IsOrderPlacedSuccessfully { get; }
///
/// Gets the message associated with the order response.
///
public string Message { get; }
///
/// Initializes a new instance of the struct.
///
/// The unique identifier for the placed order.
/// A value indicating whether the order was placed successfully.
/// The message associated with the order response. This parameter is optional and defaults to null.
public PhonyPlaceOrderResponse(string orderId, bool isOrderPlacedSuccessfully, string message = null)
{
OrderId = orderId;
IsOrderPlacedSuccessfully = isOrderPlacedSuccessfully;
Message = message;
}
}
}
///
private protected class CustomOrderProvider : OrderProvider
{
public void UpdateOrderStatusById(int orderId, OrderStatus newOrderStatus)
{
var order = _orders.First(x => x.Id == orderId);
order.Status = newOrderStatus;
}
}
}
}