/// <summary>
        /// Get the maximum position group 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 position group and the target
        ///     signed buying power percentage</param>
        /// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns>
        public override GetMaximumLotsResult GetMaximumLotsForTargetBuyingPower(
            GetMaximumLotsForTargetBuyingPowerParameters parameters
            )
        {
            if (parameters.PositionGroup.Count != 1)
            {
                return(parameters.Error(
                           $"{nameof(SecurityPositionGroupBuyingPowerModel)} only supports position groups containing exactly one position."
                           ));
            }

            var position = parameters.PositionGroup.Single();
            var security = parameters.Portfolio.Securities[position.Symbol];
            var result   = security.BuyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower(
                parameters.Portfolio, security, parameters.TargetBuyingPower, parameters.MinimumOrderMarginPortfolioPercentage
                );

            var quantity = result.Quantity / security.SymbolProperties.LotSize;

            return(new GetMaximumLotsResult(quantity, result.Reason, result.IsError));
        }
Beispiel #2
0
        /// <summary>
        /// Get the maximum position group 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 position group and the target
        ///     signed buying power percentage</param>
        /// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns>
        public virtual GetMaximumLotsResult GetMaximumLotsForTargetBuyingPower(
            GetMaximumLotsForTargetBuyingPowerParameters parameters
            )
        {
            // In order to determine maximum order quantity for a particular amount of buying power, we must resolve
            // the group's 'unit' as this will be the quantity step size. If we don't step according to these units
            // then we could be left with a different type of group with vastly different margin requirements, so we
            // must keep the ratios between all of the position quantities the same. First we'll determine the target
            // buying power, taking into account RequiredFreeBuyingPowerPercent to ensure a buffer. Then we'll evaluate
            // the initial margin requirement using the provided position group position quantities. From this value,
            // we can determine if we need to add more quantity or remove quantity by looking at the delta from the target
            // to the computed initial margin requirement. We can also compute, assuming linearity, the change in initial
            // margin requirements for each 'unit' of the position group added. The final value we need before starting to
            // iterate to solve for quantity is the minimum quantities. This is the 'unit' of the position group, and any
            // quantities less than the unit's quantity would yield an entirely different group w/ different margin calcs.
            // Now that we've resolved our target, our group unit and the unit's initial margin requirement, we can iterate
            // increasing/decreasing quantities in multiples of the unit's quantities until we're within a unit's amount of
            // initial margin to the target buying power.
            // NOTE: The first estimate MUST be greater than the target and iteration will successively decrease quantity estimates.
            //   1. Determine current holdings of position group
            //   2. If targeting zero, we can short circuit and return the negative of existing position quantities
            //   3. Determine target buying power, taking into account RequiredFreeBuyingPowerPercent
            //   4. Determine current used margin [we're using initial here to match BuyingPowerModel]
            //   5. Determine if we need to buy or sell to reach the target and convert to absolutes
            //   6. Resolve the group's 'unit' quantities, this is our step size
            //   7. Compute the initial margin requirement for a single unit
            //   7a. Compute and add order fees into the unit initial margin requirement
            //   8. Verify the target is greater than 1 unit's initial margin, otherwise exit w/ zero
            //   8a. Define minimum bounds on the target as target - (unit order margin + fees)
            //   9. Assuming linearity, compute estimate of absolute order quantity to reach target
            //  10. Begin iterating
            //  11. For each quantity estimate, compute initial margin requirement
            //  12. Compute order fees and add to the initial margin requirement
            //  13. Check to see if current estimate yields is w/in one unit's margin of target (must be less than)
            //  14. Compute a new quantity estimate
            //  15. After 13 results in ending iteration, return result w/ direction from #5

            var portfolio = parameters.Portfolio;

            // 1. Determine current holdings of position group
            var currentPositionGroup = portfolio.Positions[parameters.PositionGroup.Key];

            // 2. If targeting zero, short circuit and return the negative of existing quantities
            if (parameters.TargetBuyingPower == 0m)
            {
                return(parameters.Result(currentPositionGroup.Quantity));
            }

            // 3. Determine target buying power, taking into account RequiredFreeBuyingPowerPercent
            var bufferFactor       = 1 - RequiredFreeBuyingPowerPercent;
            var targetBufferFactor = bufferFactor * parameters.TargetBuyingPower;

            var totalPortfolioValue = portfolio.TotalPortfolioValue;

            // 4. Determine initial margin requirement for current holdings
            var currentSignedUsedMargin = 0m;

            if (currentPositionGroup.Quantity != 0)
            {
                currentSignedUsedMargin = this.GetInitialMarginRequirement(portfolio, currentPositionGroup);
            }

            // 5. Determine if we need to buy or sell to reach target, we'll work in the land of absolutes after this
            var signedTarget = targetBufferFactor * totalPortfolioValue - currentSignedUsedMargin;
            var globalTarget = Math.Abs(signedTarget);
            var direction    = Math.Sign(signedTarget);

            // 6. Resolve 'unit' -- this defines our step size
            var groupUnit = parameters.PositionGroup.Key.CreateUnitGroup();

            // 7. Compute initial margin requirement for a single unit
            var absUnitMargin = this.GetInitialMarginRequirement(portfolio, groupUnit);

            if (absUnitMargin == 0m)
            {
                // likely due to missing price data
                var zeroPricedPosition = parameters.PositionGroup.FirstOrDefault(
                    p => portfolio.Securities.GetValueOrDefault(p.Symbol)?.Price == 0m
                    );
                return(parameters.Error(zeroPricedPosition?.Symbol.GetZeroPriceMessage()
                                        ?? $"Computed zero initial margin requirement for {parameters.PositionGroup.GetUserFriendlyName()}."
                                        ));
            }

            // 7a. Compute fees for a single unit - if fees and price are linear, we'll be able to solve exactly w/out iteration
            var contemplatedOrderFees = GetOrderFeeInAccountCurrency(portfolio, groupUnit);

            absUnitMargin += contemplatedOrderFees;

            // 8. Verify target is more that the unit margin -- for groups, minimum is same as unit margin
            if (absUnitMargin > globalTarget)
            {
                return(parameters.SilenceNonErrorReasons
                    ? parameters.Zero()
                    : parameters.Zero(
                           $"The target order margin {globalTarget} is less than the minimum initial margin: {absUnitMargin}"
                           ));
            }

            // 9. Compute initial position group quantity estimate -- group quantities are whole numbers [number of lots/unit quantities]
            var positionGroupQuantity = Math.Floor(globalTarget / absUnitMargin);
            var positionGroup         = groupUnit.WithQuantity(positionGroupQuantity);

            // 9a. Compute fees for initial estimate and adjust targets. We take fees out of the target to decouple it from the quantity root finding

            // 10. Begin iterating until order quantity is within target absFinalOrderMargin bounds (coming from above)
            var       loopCount    = 0;
            const int maxLoopCount = 5;

            contemplatedOrderFees = GetOrderFeeInAccountCurrency(portfolio, positionGroup);
            var orderMarginWithoutFees = this.GetInitialMarginRequirement(portfolio, positionGroup);
            var orderMargin            = orderMarginWithoutFees + contemplatedOrderFees;

            // 8a. Define lower bounds on target, we seek an order that is >= targetMinimum and <= target
            var target        = Math.Abs(targetBufferFactor * (totalPortfolioValue - contemplatedOrderFees) - currentSignedUsedMargin);
            var targetMinimum = target - absUnitMargin;

            var lastOrderQuantity = 0m;

            while (orderMarginWithoutFees > target || orderMarginWithoutFees < targetMinimum)
            {
                // Evaluate delta target and compute new quantity estimate
                var deltaTarget   = target - orderMarginWithoutFees;
                var marginPerUnit = orderMarginWithoutFees / positionGroupQuantity;
                var deltaQuantity = (int)Math.Floor(deltaTarget / marginPerUnit);
                if (deltaQuantity == 0)
                {
                    deltaQuantity = orderMarginWithoutFees > target ? -1 : 1;
                }

                positionGroupQuantity += deltaQuantity;

                if (positionGroupQuantity <= 0)
                {
                    return(parameters.Zero(
                               $"The target order margin {target} is less than the minimum {absUnitMargin}"
                               ));
                }

                ArgumentException error;
                if (UnableToConverge(lastOrderQuantity, positionGroupQuantity, groupUnit, portfolio, target, orderMargin, absUnitMargin, contemplatedOrderFees, out error))
                {
                    throw error;
                }

                if (loopCount >= maxLoopCount)
                {
                    break;
                }

                // 12. Update order margin with new quantity estimate
                positionGroup          = positionGroup.WithQuantity(positionGroupQuantity);
                contemplatedOrderFees  = GetOrderFeeInAccountCurrency(portfolio, positionGroup);
                orderMarginWithoutFees = this.GetInitialMarginRequirement(portfolio, positionGroup);
                orderMargin            = orderMarginWithoutFees + contemplatedOrderFees;

                loopCount++;
                lastOrderQuantity = positionGroupQuantity;

                // Update margin per unit so we can refine our targets
                marginPerUnit = orderMarginWithoutFees / positionGroupQuantity;

                // if we're iterating, it's possible that we have an exotic/non-linear fee or price structure
                // to remove a potential source of non-linearity, we deduct the fees from our target to focus on quantity root finding
                target        = Math.Abs(targetBufferFactor * (totalPortfolioValue - contemplatedOrderFees) - currentSignedUsedMargin);
                targetMinimum = target - marginPerUnit;
            }

            // 15. Incorporate direction back into the result
            return(parameters.Result(direction * positionGroupQuantity));
        }