Skip to content

Commit

Permalink
Feature: support MarketOnOpen and MarketOnClose (#19)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Romazes authored Sep 25, 2024
1 parent ed10124 commit 0fcbb45
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 8 deletions.
68 changes: 68 additions & 0 deletions QuantConnect.AlpacaBrokerage.Tests/AlpacaBrokerageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -232,5 +233,72 @@ public void LongUpdateOrderCrypto(OrderTestParameters parameters)
ModifyOrderUntilFilled(order, parameters);
}
}

private static IEnumerable<TestCaseData> 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}");
}
}
}
}
}
32 changes: 27 additions & 5 deletions QuantConnect.AlpacaBrokerage/AlpacaBrokerage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,29 +252,51 @@ public override List<Order> GetOpenOrders()
var orders = _tradingClient.ListOrdersAsync(new ListOrdersRequest() { OrderStatusFilter = OrderStatusFilter.Open }).SynchronouslyAwaitTaskResult();

var leanOrders = new List<Order>();
var unsupportedTimeInForce = new HashSet<AlpacaMarket.TimeInForce>();
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.");
Expand Down
46 changes: 44 additions & 2 deletions QuantConnect.AlpacaBrokerage/AlpacaBrokerageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>
/// Try to Convert Alpaca <see cref="AlpacaMarket.TimeInForce"/> to Lean <see cref="TimeInForce"/>
/// </summary>
/// <param name="orderProperties">The instance of Alpaca Order Properties.</param>
/// <param name="timeInForce">The Alpaca Time In Force duration of order.</param>
/// <returns>
/// true - if it was converted successfully.
/// false - if Alpaca Time In Force was not provided.
/// </returns>
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;
}
}

/// <summary>
/// Creates an Alpaca sell order based on the provided Lean order type.
/// </summary>
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -135,16 +167,26 @@ private static AlpacaMarket.OrderBase CreateAlpacaBuyOrder(this Order order, str
/// Converts Lean TimeInForce to Alpaca brokerage TimeInForce.
/// </summary>
/// <param name="timeInForce">The Lean TimeInForce object to be converted.</param>
/// <param name="securityType">The SecurityType of tradable security.</param>
/// <param name="leanOrderType">The Lean order type.</param>
/// <returns>Returns the corresponding AlpacaMarket.TimeInForce value.</returns>
/// <exception cref="NotSupportedException">Thrown when the provided TimeInForce type is not supported.</exception>
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)
{
Log.Error($"{nameof(AlpacaBrokerageExtensions)}.{nameof(ConvertLeanTimeInForceToBrokerage)}: Invalid TimeInForce '{timeInForce.GetType().Name}' for Option security type. Only 'DayTimeInForce' is supported for options.");
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,
Expand Down
2 changes: 1 addition & 1 deletion alpaca.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 0fcbb45

Please sign in to comment.