/* * 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 QuantConnect.Orders; using QuantConnect.Securities; using System.Collections.Generic; namespace QuantConnect.Brokerages { /// /// Provides extension methods for handling brokerage operations. /// public static class BrokerageExtensions { /// /// The default set of order types that are not allowed to cross zero holdings. /// This is used by when no custom set is provided. /// private static readonly IReadOnlySet DefaultNotSupportedCrossZeroOrderTypes = new HashSet { OrderType.MarketOnOpen, OrderType.MarketOnClose }; /// /// Determines if executing the specified order will cross the zero holdings threshold. /// /// The current quantity of holdings. /// The quantity of the order to be evaluated. /// /// true if the order will change the holdings from positive to negative or vice versa; otherwise, false. /// /// /// This method checks if the order will result in a position change from positive to negative holdings or from negative to positive holdings. /// public static bool OrderCrossesZero(decimal holdingQuantity, decimal orderQuantity) { //We're reducing position or flipping: if (holdingQuantity > 0 && orderQuantity < 0) { if ((holdingQuantity + orderQuantity) < 0) { //We don't have enough holdings so will cross through zero: return true; } } else if (holdingQuantity < 0 && orderQuantity > 0) { if ((holdingQuantity + orderQuantity) > 0) { //Crossed zero: need to split into 2 orders: return true; } } return false; } /// /// Determines whether an order that crosses zero holdings is permitted /// for the specified brokerage model and order type. /// /// The brokerage model performing the validation. /// The security associated with the order. /// The order to validate. /// The set of order types that cannot cross zero holdings. /// /// When the method returns false, contains a /// explaining why the order is not supported; otherwise null. /// /// /// true if the order is valid to submit; false if crossing zero is not supported /// for the given order type. /// public static bool ValidateCrossZeroOrder( IBrokerageModel brokerageModel, Security security, Order order, out BrokerageMessageEvent message, IReadOnlySet notSupportedTypes = null) { message = null; notSupportedTypes ??= DefaultNotSupportedCrossZeroOrderTypes; if (OrderCrossesZero(security.Holdings.Quantity, order.Quantity) && notSupportedTypes.Contains(order.Type)) { message = new BrokerageMessageEvent( BrokerageMessageType.Warning, "NotSupported", Messages.DefaultBrokerageModel.UnsupportedCrossZeroByOrderType(brokerageModel, order.Type) ); return false; } return true; } /// /// Validates whether a order. /// /// The security associated with the order. /// The order to validate. /// /// A delegate that takes a and returns the allowed /// Market-on-Open submission window as a tuple (start, end). /// /// The set of values allowed for orders. /// /// An output containing the reason /// the order is invalid if the check fails; otherwise null. /// /// true if the order may be submitted within the given window; otherwise false. public static bool ValidateMarketOnOpenOrder( Security security, Order order, Func getMarketOnOpenAllowedWindow, IReadOnlySet supportedSecurityTypes, out BrokerageMessageEvent message) { message = null; if (order.Type != OrderType.MarketOnOpen) { return true; } if (!supportedSecurityTypes.Contains(security.Type)) { message = new BrokerageMessageEvent(BrokerageMessageType.Warning, $"UnsupportedSecurityType", $"The broker does not support Market-on-Open orders for security type {security.Type}"); return false; } var targetTime = TimeOnly.FromDateTime(security.LocalTime); var regularHours = security.Exchange.Hours.GetMarketHours(security.LocalTime).Segments.FirstOrDefault(x => x.State == MarketHoursState.Market); var (windowStart, windowEnd) = (TimeOnly.MinValue, TimeOnly.MaxValue); if (regularHours != null) { (windowStart, windowEnd) = getMarketOnOpenAllowedWindow(regularHours); } if (!targetTime.IsBetween(windowStart, windowEnd)) { message = new BrokerageMessageEvent( BrokerageMessageType.Warning, "NotSupported", Messages.DefaultBrokerageModel.UnsupportedMarketOnOpenOrderTime(windowStart, windowEnd) ); return false; } return true; } /// /// Gets the position that might result given the specified order direction and the current holdings quantity. /// This is useful for brokerages that require more specific direction information than provided by the OrderDirection enum /// (e.g. Tradier differentiates Buy/Sell and BuyToOpen/BuyToCover/SellShort/SellToClose) /// /// The order direction /// The current holdings quantity /// The order position public static OrderPosition GetOrderPosition(OrderDirection orderDirection, decimal holdingsQuantity) { return orderDirection switch { OrderDirection.Buy => holdingsQuantity >= 0 ? OrderPosition.BuyToOpen : OrderPosition.BuyToClose, OrderDirection.Sell => holdingsQuantity <= 0 ? OrderPosition.SellToOpen : OrderPosition.SellToClose, _ => throw new ArgumentOutOfRangeException(nameof(orderDirection), orderDirection, "Invalid order direction") }; } } }