From 0fcbb453c620bceee73f6ab88d926d19e4db387b Mon Sep 17 00:00:00 2001 From: Roman Yavnikov <45608740+Romazes@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:25:14 +0300 Subject: [PATCH] Feature: support MarketOnOpen and MarketOnClose (#19) * feat: support Open/Close-MarketOrder test:feat: Open/Close-MarketOrder feat: update config support of new Order Types * refactor: add HashSet of unsupported TimeInForce in GetOpenOrders --- .../AlpacaBrokerageTests.cs | 68 +++++++++++++++++++ .../AlpacaBrokerage.cs | 32 +++++++-- .../AlpacaBrokerageExtensions.cs | 46 ++++++++++++- alpaca.json | 2 +- 4 files changed, 140 insertions(+), 8 deletions(-) diff --git a/QuantConnect.AlpacaBrokerage.Tests/AlpacaBrokerageTests.cs b/QuantConnect.AlpacaBrokerage.Tests/AlpacaBrokerageTests.cs index 5e7cd26..b6076f7 100644 --- a/QuantConnect.AlpacaBrokerage.Tests/AlpacaBrokerageTests.cs +++ b/QuantConnect.AlpacaBrokerage.Tests/AlpacaBrokerageTests.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using static QuantConnect.Brokerages.Alpaca.Tests.AlpacaBrokerageAdditionalTests; namespace QuantConnect.Brokerages.Alpaca.Tests @@ -232,5 +233,72 @@ public void LongUpdateOrderCrypto(OrderTestParameters parameters) ModifyOrderUntilFilled(order, parameters); } } + + private static IEnumerable MarketOpenCloseOrderTypeParameters + { + get + { + var symbol = Symbols.AAPL; + yield return new TestCaseData(new MarketOnOpenOrder(symbol, 1m, DateTime.UtcNow), !symbol.IsMarketOpen(DateTime.UtcNow, false)); + yield return new TestCaseData(new MarketOnCloseOrder(symbol, 1m, DateTime.UtcNow), symbol.IsMarketOpen(DateTime.UtcNow, false)); + } + } + + [TestCaseSource(nameof(MarketOpenCloseOrderTypeParameters))] + public void PlaceMarketOpenCloseOrder(Order order, bool marketIsOpen) + { + Log.Trace($"PLACE {order.Type} ORDER TEST"); + + var submittedResetEvent = new AutoResetEvent(false); + var invalidResetEvent = new AutoResetEvent(false); + + OrderProvider.Add(order); + + Brokerage.OrdersStatusChanged += (_, orderEvents) => + { + var orderEvent = orderEvents[0]; + + Log.Trace(""); + Log.Trace($"{nameof(PlaceMarketOpenCloseOrder)}.OrderEvent.Status: {orderEvent.Status}"); + Log.Trace(""); + + if (orderEvent.Status == OrderStatus.Submitted) + { + submittedResetEvent.Set(); + } + else if (orderEvent.Status == OrderStatus.Invalid) + { + invalidResetEvent.Set(); + } + }; + + + + if (marketIsOpen) + { + Assert.IsTrue(Brokerage.PlaceOrder(order)); + + if (!submittedResetEvent.WaitOne(TimeSpan.FromSeconds(5))) + { + Assert.Fail($"{nameof(PlaceMarketOpenCloseOrder)}: the brokerage doesn't return {OrderStatus.Submitted}"); + } + + var openOrders = Brokerage.GetOpenOrders(); + + Assert.IsNotEmpty(openOrders); + Assert.That(openOrders.Count, Is.EqualTo(1)); + Assert.That(openOrders[0].Type, Is.EqualTo(order.Type)); + Assert.IsTrue(Brokerage.CancelOrder(order)); + } + else + { + Assert.IsFalse(Brokerage.PlaceOrder(order)); + + if (!invalidResetEvent.WaitOne(TimeSpan.FromSeconds(5))) + { + Assert.Fail($"{nameof(PlaceMarketOpenCloseOrder)}: the brokerage doesn't return {OrderStatus.Invalid}"); + } + } + } } } \ No newline at end of file diff --git a/QuantConnect.AlpacaBrokerage/AlpacaBrokerage.cs b/QuantConnect.AlpacaBrokerage/AlpacaBrokerage.cs index 0a61106..b7b0341 100644 --- a/QuantConnect.AlpacaBrokerage/AlpacaBrokerage.cs +++ b/QuantConnect.AlpacaBrokerage/AlpacaBrokerage.cs @@ -252,29 +252,51 @@ public override List GetOpenOrders() var orders = _tradingClient.ListOrdersAsync(new ListOrdersRequest() { OrderStatusFilter = OrderStatusFilter.Open }).SynchronouslyAwaitTaskResult(); var leanOrders = new List(); + var unsupportedTimeInForce = new HashSet(); foreach (var brokerageOrder in orders) { + var orderProperties = new AlpacaOrderProperties(); + if (!orderProperties.TryGetLeanTimeInForceByAlpacaTimeInForce(brokerageOrder.TimeInForce)) + { + if (unsupportedTimeInForce.Add(brokerageOrder.TimeInForce)) + { + OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, -1, $"Detected unsupported Lean TimeInForce of '{brokerageOrder.TimeInForce}', ignoring. Using default: TimeInForce.GoodTilCanceled")); + } + } + var leanSymbol = _symbolMapper.GetLeanSymbol(brokerageOrder.AssetClass, brokerageOrder.Symbol); var quantity = (brokerageOrder.OrderSide == OrderSide.Buy ? brokerageOrder.Quantity : decimal.Negate(brokerageOrder.Quantity.Value)).Value; var leanOrder = default(Order); switch (brokerageOrder.OrderType) { case AlpacaMarket.OrderType.Market: - leanOrder = new Orders.MarketOrder(leanSymbol, quantity, brokerageOrder.SubmittedAtUtc.Value); + + switch (brokerageOrder.TimeInForce) + { + case AlpacaMarket.TimeInForce.Opg: + leanOrder = new MarketOnOpenOrder(leanSymbol, quantity, brokerageOrder.SubmittedAtUtc.Value, properties: orderProperties); + break; + case AlpacaMarket.TimeInForce.Cls: + leanOrder = new MarketOnCloseOrder(leanSymbol, quantity, brokerageOrder.SubmittedAtUtc.Value, properties: orderProperties); + break; + default: + leanOrder = new Orders.MarketOrder(leanSymbol, quantity, brokerageOrder.SubmittedAtUtc.Value, properties: orderProperties); + break; + } break; case AlpacaMarket.OrderType.Limit: - leanOrder = new Orders.LimitOrder(leanSymbol, quantity, brokerageOrder.LimitPrice.Value, brokerageOrder.SubmittedAtUtc.Value); + leanOrder = new Orders.LimitOrder(leanSymbol, quantity, brokerageOrder.LimitPrice.Value, brokerageOrder.SubmittedAtUtc.Value, properties: orderProperties); break; case AlpacaMarket.OrderType.Stop: - leanOrder = new StopMarketOrder(leanSymbol, quantity, brokerageOrder.StopPrice.Value, brokerageOrder.SubmittedAtUtc.Value); + leanOrder = new StopMarketOrder(leanSymbol, quantity, brokerageOrder.StopPrice.Value, brokerageOrder.SubmittedAtUtc.Value, properties: orderProperties); break; case AlpacaMarket.OrderType.StopLimit: - leanOrder = new Orders.StopLimitOrder(leanSymbol, quantity, brokerageOrder.StopPrice.Value, brokerageOrder.LimitPrice.Value, brokerageOrder.SubmittedAtUtc.Value); + leanOrder = new Orders.StopLimitOrder(leanSymbol, quantity, brokerageOrder.StopPrice.Value, brokerageOrder.LimitPrice.Value, brokerageOrder.SubmittedAtUtc.Value, properties: orderProperties); break; case AlpacaMarket.OrderType.TrailingStop: var trailingAsPercent = brokerageOrder.TrailOffsetInPercent.HasValue ? true : false; var trailingAmount = brokerageOrder.TrailOffsetInPercent.HasValue ? brokerageOrder.TrailOffsetInPercent.Value / 100m : brokerageOrder.TrailOffsetInDollars.Value; - leanOrder = new Orders.TrailingStopOrder(leanSymbol, quantity, brokerageOrder.StopPrice.Value, trailingAmount, trailingAsPercent, brokerageOrder.SubmittedAtUtc.Value); + leanOrder = new Orders.TrailingStopOrder(leanSymbol, quantity, brokerageOrder.StopPrice.Value, trailingAmount, trailingAsPercent, brokerageOrder.SubmittedAtUtc.Value, properties: orderProperties); break; default: throw new NotSupportedException($"{nameof(AlpacaBrokerage)}.{nameof(GetOpenOrders)}: Order type '{brokerageOrder.OrderType}' is not supported."); diff --git a/QuantConnect.AlpacaBrokerage/AlpacaBrokerageExtensions.cs b/QuantConnect.AlpacaBrokerage/AlpacaBrokerageExtensions.cs index 8ceca76..be1ea6b 100644 --- a/QuantConnect.AlpacaBrokerage/AlpacaBrokerageExtensions.cs +++ b/QuantConnect.AlpacaBrokerage/AlpacaBrokerageExtensions.cs @@ -48,11 +48,39 @@ public static AlpacaMarket.OrderBase CreateAlpacaOrder(this Order order, decimal { throw new InvalidOperationException($"Can't create order for direction {order.Direction}"); } - var alpacaTimeInForce = order.TimeInForce.ConvertLeanTimeInForceToBrokerage(order.SecurityType); + var alpacaTimeInForce = order.TimeInForce.ConvertLeanTimeInForceToBrokerage(order.SecurityType, order.Type); AlpacaMarket.OrderBaseExtensions.WithDuration(orderRequest, alpacaTimeInForce); return orderRequest; } + /// + /// Try to Convert Alpaca to Lean + /// + /// The instance of Alpaca Order Properties. + /// The Alpaca Time In Force duration of order. + /// + /// true - if it was converted successfully. + /// false - if Alpaca Time In Force was not provided. + /// + public static bool TryGetLeanTimeInForceByAlpacaTimeInForce(this AlpacaOrderProperties orderProperties, AlpacaMarket.TimeInForce timeInForce) + { + switch (timeInForce) + { + case AlpacaMarket.TimeInForce.Day: + orderProperties.TimeInForce = TimeInForce.Day; + return true; + case AlpacaMarket.TimeInForce.Gtc: + orderProperties.TimeInForce = TimeInForce.GoodTilCanceled; + return true; + case AlpacaMarket.TimeInForce.Opg: + case AlpacaMarket.TimeInForce.Cls: + orderProperties.TimeInForce = TimeInForce.GoodTilCanceled; + return true; + default: + return false; + } + } + /// /// Creates an Alpaca sell order based on the provided Lean order type. /// @@ -66,6 +94,8 @@ private static AlpacaMarket.OrderBase CreateAlpacaSellOrder(this Order order, st switch (orderType) { case OrderType.Market: + case OrderType.MarketOnOpen: + case OrderType.MarketOnClose: return AlpacaMarket.MarketOrder.Sell(brokerageSymbol, quantity); case OrderType.TrailingStop: var tso = (TrailingStopOrder)order; @@ -105,6 +135,8 @@ private static AlpacaMarket.OrderBase CreateAlpacaBuyOrder(this Order order, str switch (orderType) { case OrderType.Market: + case OrderType.MarketOnOpen: + case OrderType.MarketOnClose: return AlpacaMarket.MarketOrder.Buy(brokerageSymbol, quantity); case OrderType.TrailingStop: var tso = (TrailingStopOrder)order; @@ -135,9 +167,11 @@ private static AlpacaMarket.OrderBase CreateAlpacaBuyOrder(this Order order, str /// Converts Lean TimeInForce to Alpaca brokerage TimeInForce. /// /// The Lean TimeInForce object to be converted. + /// The SecurityType of tradable security. + /// The Lean order type. /// Returns the corresponding AlpacaMarket.TimeInForce value. /// Thrown when the provided TimeInForce type is not supported. - private static AlpacaMarket.TimeInForce ConvertLeanTimeInForceToBrokerage(this TimeInForce timeInForce, SecurityType securityType) + private static AlpacaMarket.TimeInForce ConvertLeanTimeInForceToBrokerage(this TimeInForce timeInForce, SecurityType securityType, OrderType leanOrderType) { if (securityType == SecurityType.Option && timeInForce is not DayTimeInForce) { @@ -145,6 +179,14 @@ private static AlpacaMarket.TimeInForce ConvertLeanTimeInForceToBrokerage(this T return AlpacaMarket.TimeInForce.Day; } + switch (leanOrderType) + { + case OrderType.MarketOnOpen: + return AlpacaMarket.TimeInForce.Opg; + case OrderType.MarketOnClose: + return AlpacaMarket.TimeInForce.Cls; + } + return timeInForce switch { DayTimeInForce => AlpacaMarket.TimeInForce.Day, diff --git a/alpaca.json b/alpaca.json index 509fef3..9fb1bd3 100644 --- a/alpaca.json +++ b/alpaca.json @@ -9,7 +9,7 @@ "Live Trading": [0,0,0] } ], - "order-types": ["Market", "Limit", "Stop Market", "Stop Limit", "Trailing"], + "order-types": ["Market", "Limit", "Stop Market", "Stop Limit", "Trailing", "Market On Open", "Market On Close"], "assets": ["Equity", "Equity Options", "Crypto"], "brokerage-url": "https://alpaca.markets/", "documentation": "/docs/v2/cloud-platform/live-trading/brokerages/alpaca",