Example #1
0
        /// <summary>
        /// Get the maximum market order quantity to obtain a position with a given buying power percentage.
        /// Will not take into account free buying power.
        /// </summary>
        /// <param name="parameters">An object containing the portfolio, the security and the target signed buying power percentage</param>
        /// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns>
        /// <remarks>This implementation ensures that our resulting holdings is less than the target, but it does not necessarily
        /// maximize the holdings to meet the target. To do that we need a minimizing algorithm that reduces the difference between
        /// the target final margin value and the target holdings margin.</remarks>
        public virtual GetMaximumOrderQuantityResult GetMaximumOrderQuantityForTargetBuyingPower(GetMaximumOrderQuantityForTargetBuyingPowerParameters parameters)
        {
            // this is expensive so lets fetch it once
            var totalPortfolioValue = parameters.Portfolio.TotalPortfolioValue;

            // adjust target buying power to comply with required Free Buying Power Percent
            var signedTargetFinalMarginValue =
                parameters.TargetBuyingPower * (totalPortfolioValue - totalPortfolioValue * RequiredFreeBuyingPowerPercent);

            // if targeting zero, simply return the negative of the quantity
            if (signedTargetFinalMarginValue == 0)
            {
                return(new GetMaximumOrderQuantityResult(-parameters.Security.Holdings.Quantity, string.Empty, false));
            }

            // we use initial margin requirement here to avoid the duplicate PortfolioTarget.Percent situation:
            // PortfolioTarget.Percent(1) -> fills -> PortfolioTarget.Percent(1) _could_ detect free buying power if we use Maintenance requirement here
            var signedCurrentUsedMargin = this.GetInitialMarginRequirement(parameters.Security, parameters.Security.Holdings.Quantity);

            // determine the unit price in terms of the account currency
            var utcTime = parameters.Security.LocalTime.ConvertToUtc(parameters.Security.Exchange.TimeZone);

            // determine the margin required for 1 unit
            var absUnitMargin = this.GetInitialMarginRequirement(parameters.Security, 1);

            if (absUnitMargin == 0)
            {
                return(new GetMaximumOrderQuantityResult(0, parameters.Security.Symbol.GetZeroPriceMessage()));
            }

            // Check that the change of margin is above our models minimum percentage change
            var absDifferenceOfMargin = Math.Abs(signedTargetFinalMarginValue - signedCurrentUsedMargin);

            if (!BuyingPowerModelExtensions.AboveMinimumOrderMarginPortfolioPercentage(parameters.Portfolio,
                                                                                       parameters.MinimumOrderMarginPortfolioPercentage, absDifferenceOfMargin))
            {
                string reason = null;
                if (!parameters.SilenceNonErrorReasons)
                {
                    var minimumValue = totalPortfolioValue * parameters.MinimumOrderMarginPortfolioPercentage;
                    reason = $"The target order margin {absDifferenceOfMargin} is less than the minimum {minimumValue}.";
                }
                return(new GetMaximumOrderQuantityResult(0, reason, false));
            }

            // Use the following loop to converge on a value that places us under our target allocation when adjusted for fees
            var     lastOrderQuantity = 0m; // For safety check
            decimal orderFees         = 0m;
            decimal signedTargetHoldingsMargin;
            decimal orderQuantity;

            do
            {
                // Calculate our order quantity
                orderQuantity = GetAmountToOrder(parameters.Security, signedTargetFinalMarginValue, absUnitMargin, out signedTargetHoldingsMargin);
                if (orderQuantity == 0)
                {
                    string reason = null;
                    if (!parameters.SilenceNonErrorReasons)
                    {
                        reason = Invariant($"The order quantity is less than the lot size of {parameters.Security.SymbolProperties.LotSize} ") +
                                 Invariant($"and has been rounded to zero. Target order margin {signedTargetFinalMarginValue - signedCurrentUsedMargin}. ");
                    }

                    return(new GetMaximumOrderQuantityResult(0, reason, false));
                }

                // generate the order
                var order = new MarketOrder(parameters.Security.Symbol, orderQuantity, utcTime);
                var fees  = parameters.Security.FeeModel.GetOrderFee(
                    new OrderFeeParameters(parameters.Security,
                                           order)).Value;
                orderFees = parameters.Portfolio.CashBook.ConvertToAccountCurrency(fees).Amount;

                // Update our target portfolio margin allocated when considering fees, then calculate the new FinalOrderMargin
                signedTargetFinalMarginValue = (totalPortfolioValue - orderFees - totalPortfolioValue * RequiredFreeBuyingPowerPercent) * parameters.TargetBuyingPower;

                // Start safe check after first loop, stops endless recursion
                if (lastOrderQuantity == orderQuantity)
                {
                    var message =
                        Invariant($"GetMaximumOrderQuantityForTargetBuyingPower failed to converge on the target margin: {signedTargetFinalMarginValue}; ") +
                        Invariant($"the following information can be used to reproduce the issue. Total Portfolio Cash: {parameters.Portfolio.Cash}; Security : {parameters.Security.Symbol.ID}; ") +
                        Invariant($"Price : {parameters.Security.Close}; Leverage: {parameters.Security.Leverage}; Order Fee: {orderFees}; Lot Size: {parameters.Security.SymbolProperties.LotSize}; ") +
                        Invariant($"Current Holdings: {parameters.Security.Holdings.Quantity} @ {parameters.Security.Holdings.AveragePrice}; Target Percentage: %{parameters.TargetBuyingPower * 100};");

                    // Need to add underlying value to message to reproduce with options
                    if (parameters.Security is Option.Option option && option.Underlying != null)
                    {
                        var underlying = option.Underlying;
                        message += Invariant($" Underlying Security: {underlying.Symbol.ID}; Underlying Price: {underlying.Close}; Underlying Holdings: {underlying.Holdings.Quantity} @ {underlying.Holdings.AveragePrice};");
                    }

                    throw new ArgumentException(message);
                }
                lastOrderQuantity = orderQuantity;
            }
            // Ensure that our target holdings margin will be less than or equal to our target allocated margin
            while (Math.Abs(signedTargetHoldingsMargin) > Math.Abs(signedTargetFinalMarginValue));

            // add directionality back in
            return(new GetMaximumOrderQuantityResult(orderQuantity));
        }
Example #2
0
        /// <summary>
        /// Get the maximum market order quantity to obtain a position with a given buying power percentage.
        /// Will not take into account free buying power.
        /// </summary>
        /// <param name="parameters">An object containing the portfolio, the security and the target signed buying power percentage</param>
        /// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns>
        /// <remarks>This implementation ensures that our resulting holdings is less than the target, but it does not necessarily
        /// maximize the holdings to meet the target. To do that we need a minimizing algorithm that reduces the difference between
        /// the target final margin value and the target holdings margin.</remarks>
        public virtual GetMaximumOrderQuantityResult GetMaximumOrderQuantityForTargetBuyingPower(GetMaximumOrderQuantityForTargetBuyingPowerParameters parameters)
        {
            // this is expensive so lets fetch it once
            var totalPortfolioValue = parameters.Portfolio.TotalPortfolioValue;

            // adjust target buying power to comply with required Free Buying Power Percent
            var signedTargetFinalMarginValue =
                parameters.TargetBuyingPower * (totalPortfolioValue - totalPortfolioValue * RequiredFreeBuyingPowerPercent);

            // if targeting zero, simply return the negative of the quantity
            if (signedTargetFinalMarginValue == 0)
            {
                return(new GetMaximumOrderQuantityResult(-parameters.Security.Holdings.Quantity, string.Empty, false));
            }

            // we use initial margin requirement here to avoid the duplicate PortfolioTarget.Percent situation:
            // PortfolioTarget.Percent(1) -> fills -> PortfolioTarget.Percent(1) _could_ detect free buying power if we use Maintenance requirement here
            var currentSignedUsedMargin = this.GetInitialMarginRequirement(parameters.Security, parameters.Security.Holdings.Quantity);

            // remove directionality, we'll work in the land of absolutes
            var absFinalOrderMargin = Math.Abs(signedTargetFinalMarginValue - currentSignedUsedMargin);
            var direction           = signedTargetFinalMarginValue > currentSignedUsedMargin ? OrderDirection.Buy : OrderDirection.Sell;

            // determine the unit price in terms of the account currency
            var utcTime = parameters.Security.LocalTime.ConvertToUtc(parameters.Security.Exchange.TimeZone);
            // determine the margin required for 1 unit, positive since we are working with absolutes
            var absUnitMargin = this.GetInitialMarginRequirement(parameters.Security, 1);

            if (absUnitMargin == 0)
            {
                return(new GetMaximumOrderQuantityResult(0, parameters.Security.Symbol.GetZeroPriceMessage()));
            }

            // compute the initial order quantity
            var absOrderQuantity = Math.Abs(GetAmountToOrder(currentSignedUsedMargin, signedTargetFinalMarginValue, absUnitMargin,
                                                             parameters.Security.SymbolProperties.LotSize));

            if (absOrderQuantity == 0)
            {
                string reason = null;
                if (!parameters.SilenceNonErrorReasons)
                {
                    reason = $"The order quantity is less than the lot size of {parameters.Security.SymbolProperties.LotSize} " +
                             "and has been rounded to zero.";
                }
                return(new GetMaximumOrderQuantityResult(0, reason, false));
            }

            if (!BuyingPowerModelExtensions.AboveMinimumOrderMarginPortfolioPercentage(parameters.Portfolio,
                                                                                       parameters.MinimumOrderMarginPortfolioPercentage, absFinalOrderMargin))
            {
                var    minimumValue = totalPortfolioValue * parameters.MinimumOrderMarginPortfolioPercentage;
                string reason       = null;
                if (!parameters.SilenceNonErrorReasons)
                {
                    reason = $"The target order margin {absFinalOrderMargin} is less than the minimum {minimumValue}.";
                }
                return(new GetMaximumOrderQuantityResult(0, reason, false));
            }

            // Use the following loop to converge on a value that places us under our target allocation when adjusted for fees
            var lastOrderQuantity = 0m;     // For safety check

            var     signedTargetHoldingsMargin = ((direction == OrderDirection.Sell ? -1 : 1) * absOrderQuantity + parameters.Security.Holdings.Quantity) * absUnitMargin;
            decimal orderFees = 0;

            do
            {
                // If our order target holdings is larger than our target margin allocated we need to recalculate our order size
                if (Math.Abs(signedTargetHoldingsMargin) > Math.Abs(signedTargetFinalMarginValue))
                {
                    absOrderQuantity = Math.Abs(GetAmountToOrder(currentSignedUsedMargin, signedTargetFinalMarginValue, absUnitMargin,
                                                                 parameters.Security.SymbolProperties.LotSize, absOrderQuantity * (direction == OrderDirection.Sell ? -1 : 1)));
                }

                if (absOrderQuantity <= 0)
                {
                    var sign = direction == OrderDirection.Buy ? 1 : -1;
                    return(new GetMaximumOrderQuantityResult(0,
                                                             Invariant($"The order quantity is less than the lot size of {parameters.Security.SymbolProperties.LotSize} ") +
                                                             Invariant($"and has been rounded to zero.Target order margin {absFinalOrderMargin * sign}. Order fees ") +
                                                             Invariant($"{orderFees}. Order quantity {absOrderQuantity * sign}. Margin unit {absUnitMargin}."),
                                                             false
                                                             ));
                }

                // generate the order
                var order = new MarketOrder(parameters.Security.Symbol, absOrderQuantity, utcTime);
                var fees  = parameters.Security.FeeModel.GetOrderFee(
                    new OrderFeeParameters(parameters.Security,
                                           order)).Value;
                orderFees = parameters.Portfolio.CashBook.ConvertToAccountCurrency(fees).Amount;

                // Update our target portfolio margin allocated when considering fees, then calculate the new FinalOrderMargin
                signedTargetFinalMarginValue = (totalPortfolioValue - orderFees - totalPortfolioValue * RequiredFreeBuyingPowerPercent) * parameters.TargetBuyingPower;
                absFinalOrderMargin          = Math.Abs(signedTargetFinalMarginValue - currentSignedUsedMargin);

                // Start safe check after first loop
                if (lastOrderQuantity == absOrderQuantity)
                {
                    var sign    = direction == OrderDirection.Buy ? 1 : -1;
                    var message =
                        Invariant($"GetMaximumOrderQuantityForTargetBuyingPower failed to converge on the target margin: {signedTargetFinalMarginValue}; ") +
                        Invariant($"the following information can be used to reproduce the issue. Total Portfolio Cash: {parameters.Portfolio.Cash}; ") +
                        Invariant($"Leverage: {parameters.Security.Leverage}; Order Fee: {orderFees}; Lot Size: {parameters.Security.SymbolProperties.LotSize}; ") +
                        Invariant($"Per Unit Margin: {absUnitMargin}; Current Holdings: {parameters.Security.Holdings}; Target Percentage: %{parameters.TargetBuyingPower * 100}; ") +
                        Invariant($"Current Order Target Margin: {absFinalOrderMargin * sign}; Current Order Margin: {absOrderQuantity * absUnitMargin * sign}");
                    throw new ArgumentException(message);
                }
                lastOrderQuantity = absOrderQuantity;

                // Update our target holdings margin
                signedTargetHoldingsMargin = ((direction == OrderDirection.Sell ? -1 : 1) * absOrderQuantity + parameters.Security.Holdings.Quantity) * absUnitMargin;
            }
            // Ensure that our target holdings margin will be less than or equal to our target allocated margin
            while (Math.Abs(signedTargetHoldingsMargin) > Math.Abs(signedTargetFinalMarginValue));

            // add directionality back in
            return(new GetMaximumOrderQuantityResult((direction == OrderDirection.Sell ? -1 : 1) * absOrderQuantity));
        }