/*
* 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")
};
}
}
}