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