/*
* 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 QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Util;
using System;
using System.Collections.Generic;
using System.Linq;
using static QuantConnect.StringExtensions;
namespace QuantConnect.Securities.Positions
{
///
/// Provides a base class for implementations of
///
public abstract class PositionGroupBuyingPowerModel : IPositionGroupBuyingPowerModel
{
///
/// Gets the percentage of portfolio buying power to leave as a buffer
///
protected decimal RequiredFreeBuyingPowerPercent { get; }
///
/// Initializes a new instance of the class
///
/// The percentage of portfolio buying power to leave as a buffer
protected PositionGroupBuyingPowerModel(decimal requiredFreeBuyingPowerPercent = 0m)
{
RequiredFreeBuyingPowerPercent = requiredFreeBuyingPowerPercent;
}
///
/// Gets the margin currently allocated to the specified holding
///
/// An object containing the security
/// The maintenance margin required for the
public abstract MaintenanceMargin GetMaintenanceMargin(PositionGroupMaintenanceMarginParameters parameters);
///
/// The margin that must be held in order to increase the position by the provided quantity
///
/// An object containing the security and quantity
public abstract InitialMargin GetInitialMarginRequirement(PositionGroupInitialMarginParameters parameters);
///
/// Gets the total margin required to execute the specified order in units of the account currency including fees
///
/// An object containing the portfolio, the security and the order
/// The total margin in terms of the currency quoted in the order
public abstract InitialMargin GetInitialMarginRequiredForOrder(PositionGroupInitialMarginForOrderParameters parameters);
///
/// Computes the impact on the portfolio's buying power from adding the position group to the portfolio. This is
/// a 'what if' analysis to determine what the state of the portfolio would be if these changes were applied. The
/// delta (before - after) is the margin requirement for adding the positions and if the margin used after the changes
/// are applied is less than the total portfolio value, this indicates sufficient capital.
///
/// An object containing the portfolio and a position group containing the contemplated
/// changes to the portfolio
/// Returns the portfolio's total portfolio value and margin used before and after the position changes are applied
public virtual ReservedBuyingPowerImpact GetReservedBuyingPowerImpact(ReservedBuyingPowerImpactParameters parameters)
{
// This process aims to avoid having to compute buying power on the entire portfolio and instead determines
// the set of groups that can be impacted by the changes being contemplated. The only real way to determine
// the change in maintenance margin is to determine what groups we'll have after the changes and compute the
// margin based on that.
// 1. Determine impacted groups (depends on IPositionGroupResolver.GetImpactedGroups)
// 2. Compute the currently reserved buying power of impacted groups
// 3. Create position collection using impacted groups and apply contemplated changes
// 4. Resolve new position groups using position collection with applied contemplated changes
// 5. Compute the contemplated reserved buying power on these newly resolved groups
// 1. Determine impacted groups
var positionManager = parameters.Portfolio.Positions;
// 2. Compute current reserved buying power
var current = 0m;
var impactedGroups = new List();
// 3. Determine set of impacted positions to be grouped
var positions = parameters.Orders.Select(o => o.CreatePositions(parameters.Portfolio.Securities)).SelectMany(p => p).ToList();
var impactedPositions = positions.ToDictionary(p => p.Symbol);
foreach (var impactedGroup in positionManager.GetImpactedGroups(positions))
{
impactedGroups.Add(impactedGroup);
current += impactedGroup.BuyingPowerModel.GetReservedBuyingPowerForPositionGroup(
parameters.Portfolio, impactedGroup
);
foreach (var position in impactedGroup)
{
IPosition existing;
if (impactedPositions.TryGetValue(position.Symbol, out existing))
{
// if it already exists then combine it with the existing
impactedPositions[position.Symbol] = existing.Combine(position);
}
else
{
impactedPositions[position.Symbol] = position;
}
}
}
// 4. Resolve new position groups
var contemplatedGroups = positionManager.ResolvePositionGroups(new PositionCollection(impactedPositions.Values));
// 5. Compute contemplated margin
var contemplated = GetContemplatedGroupsInitialMargin(parameters.Portfolio, contemplatedGroups, positions);
return new ReservedBuyingPowerImpact(current, contemplated, impactedGroups, parameters.ContemplatedChanges, contemplatedGroups);
}
///
/// Gets the initial margin required for the specified contemplated position group.
/// Used by to get the contemplated groups margin.
///
protected virtual decimal GetContemplatedGroupsInitialMargin(SecurityPortfolioManager portfolio, PositionGroupCollection contemplatedGroups,
List ordersPositions)
{
var contemplatedMargin = 0m;
foreach (var contemplatedGroup in contemplatedGroups)
{
// We use the initial margin requirement as the contemplated groups margin in order to ensure
// the available buying power is enough to execute the order.
contemplatedMargin += contemplatedGroup.BuyingPowerModel.GetInitialMarginRequirement(portfolio, contemplatedGroup);
}
return contemplatedMargin;
}
///
/// Check if there is sufficient buying power for the position group to execute this order.
///
/// An object containing the portfolio, the position group and the order
/// Returns buying power information for an order against a position group
public virtual HasSufficientBuyingPowerForOrderResult HasSufficientBuyingPowerForOrder(
HasSufficientPositionGroupBuyingPowerForOrderParameters parameters
)
{
// The addition of position groups requires that we not only check initial margin requirements, but also
// that we confirm that after the changes have been applied and the new groups resolved our maintenance
// margin is still in a valid range (less than TPV). For this model, we use the security's sufficient buying
// power impl to confirm initial margin requirements and lean heavily on GetReservedBuyingPowerImpact for
// help with confirming that our expected maintenance margin is still less than TPV.
// 1. Confirm we have sufficient buying power to execute the trade using security's BP model
// 2. Confirm we pass position group specific checks
// 3. Confirm we haven't exceeded maintenance margin limits via GetReservedBuyingPowerImpact's delta
// 1. Confirm we meet initial margin requirements, accounting for buffer
var deltaBuyingPowerArgs = new ReservedBuyingPowerImpactParameters(parameters.Portfolio, parameters.PositionGroup, parameters.Orders);
var deltaBuyingPower = GetReservedBuyingPowerImpact(deltaBuyingPowerArgs).Delta;
// When order only reduces or closes a security position, capital is always sufficient
if (deltaBuyingPower < 0)
{
return parameters.Sufficient();
}
var availableBuyingPower = parameters.Portfolio.MarginRemaining;
// 2. Confirm we pass position group specific checks
var result = PassesPositionGroupSpecificBuyingPowerForOrderChecks(parameters, availableBuyingPower);
if (result?.IsSufficient == false)
{
return result;
}
// 3. Confirm that the new groupings arising from the change doesn't make maintenance margin exceed TPV
// We can just compare the delta to the available buying power because the delta how much the maintenance margin will increase by
// if the order is executed, so it needs to stay below the available buying power
if (deltaBuyingPower <= availableBuyingPower)
{
return parameters.Sufficient();
}
return parameters.Insufficient(Invariant($@"Id: {string.Join(",", parameters.Orders.Select(o => o.Id))}, Maintenance Margin Delta: {
deltaBuyingPower.Normalize()}, Free Margin: {availableBuyingPower.Normalize()}"
));
}
///
/// Provides a mechanism for derived types to add their own buying power for order checks without needing to
/// recompute the available buying power. Implementations should return null if all checks pass and should
/// return an instance of with IsSufficient=false if it
/// fails.
///
protected virtual HasSufficientBuyingPowerForOrderResult PassesPositionGroupSpecificBuyingPowerForOrderChecks(
HasSufficientPositionGroupBuyingPowerForOrderParameters parameters,
decimal availableBuyingPower
)
{
return null;
}
///
/// Computes the amount of buying power reserved by the provided position group
///
public virtual ReservedBuyingPowerForPositionGroup GetReservedBuyingPowerForPositionGroup(
ReservedBuyingPowerForPositionGroupParameters parameters
)
{
return this.GetMaintenanceMargin(parameters.Portfolio, parameters.PositionGroup);
}
///
/// 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.
///
/// An object containing the portfolio, the position group and the target
/// signed buying power percentage
///
/// Returns the maximum allowed market order quantity and if zero, also the reason.
///
/// Since there is no sense of "short" or "long" on position groups with multiple positions,
/// the sign of the returned quantity will indicate the direction of the order regarding the
/// reference position group passed in the parameters:
/// - quantity > 0: the order should be placed in the same direction as the reference position group to increase it,
/// without changing the positions' signs.
/// - quantity < 0: the order should be placed in the opposite direction as the reference position group to reduce it,
/// using each position's opposite sign.
///
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. Determine target buying power, taking into account RequiredFreeBuyingPowerPercent
// 2a. If targeting zero, simply return the negative of the quantity
// 3. Determine current used margin [we're using initial here to match BuyingPowerModel]
// 4. Check that the change of margin is above our models minimum percentage change
// 5. Resolve the group's 'unit' quantities, this is our step size
// 5a. Compute the initial margin requirement for a single unit
// 6. Begin iterating until the allocated holdings margin (after order fees are applied) less or equal to the expected target margin
// 6a. Calculate the amount to order to get the target margin
// 6b. Apply order fees to the allocated holdings margin and compare to the target margin to end loop.
var portfolio = parameters.Portfolio;
// 1. Determine current holdings of position group
var currentPositionGroup = portfolio.Positions[parameters.PositionGroup.Key];
var inverted = false;
var targetBuyingPower = parameters.TargetBuyingPower;
// The reference position group is not necessarily in the same side as the position group in the portfolio, it could be the inverted.
// So the consumer needs the result relative to that position group instead of the one being held.
if (parameters.PositionGroup.IsInvertedOf(currentPositionGroup))
{
inverted = true;
targetBuyingPower = -targetBuyingPower;
}
// 2. Determine target buying power, taking into account RequiredFreeBuyingPowerPercent
var bufferFactor = 1 - RequiredFreeBuyingPowerPercent;
var targetBufferFactor = bufferFactor * targetBuyingPower;
var totalPortfolioValue = portfolio.TotalPortfolioValue;
var targetFinalMargin = targetBufferFactor * totalPortfolioValue;
// 2a. If targeting zero, simply return the negative of the quantity
if (targetFinalMargin == 0)
{
var quantity = -Math.Abs(currentPositionGroup.Quantity);
return parameters.Result(inverted ? -quantity : quantity);
}
// 3. Determine initial margin requirement for current holdings
var currentUsedMargin = 0m;
if (currentPositionGroup.Quantity != 0)
{
currentUsedMargin = Math.Abs(currentPositionGroup.BuyingPowerModel.GetInitialMarginRequirement(portfolio, currentPositionGroup));
}
// 4. Check that the change of margin is above our models minimum percentage change
var absDifferenceOfMargin = Math.Abs(targetFinalMargin - currentUsedMargin);
if (!BuyingPowerModelExtensions.AboveMinimumOrderMarginPortfolioPercentage(parameters.Portfolio,
parameters.MinimumOrderMarginPortfolioPercentage, absDifferenceOfMargin))
{
string reason = null;
if (!parameters.SilenceNonErrorReasons)
{
var minimumValue = totalPortfolioValue * parameters.MinimumOrderMarginPortfolioPercentage;
reason = Messages.BuyingPowerModel.TargetOrderMarginNotAboveMinimum(absDifferenceOfMargin, minimumValue);
}
return new GetMaximumLotsResult(0, reason, false);
}
// 5. Resolve 'unit' group -- this is our step size
var groupUnit = currentPositionGroup.CreateUnitGroup(parameters.Portfolio.Positions);
// 5a. Compute initial margin requirement for a single unit
var unitMargin = Math.Abs(groupUnit.BuyingPowerModel.GetInitialMarginRequirement(portfolio, groupUnit));
if (unitMargin == 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()
?? Messages.PositionGroupBuyingPowerModel.ComputedZeroInitialMargin(parameters.PositionGroup));
}
// 6. Begin iterating
var lastPositionGroupOrderQuantity = 0m; // For safety check
decimal orderFees;
decimal targetHoldingsMargin;
decimal positionGroupQuantity;
do
{
// 6a.Calculate the amount to order to get the target margin
positionGroupQuantity = GetPositionGroupOrderQuantity(portfolio, currentPositionGroup, currentUsedMargin, targetFinalMargin,
groupUnit, unitMargin, out targetHoldingsMargin);
if (positionGroupQuantity == 0)
{
string reason = null;
if (!parameters.SilenceNonErrorReasons)
{
reason = Messages.PositionGroupBuyingPowerModel.PositionGroupQuantityRoundedToZero(targetFinalMargin - currentUsedMargin);
}
return new GetMaximumLotsResult(0, reason, false);
}
// 6b.Apply order fees to the allocated holdings margin
orderFees = GetOrderFeeInAccountCurrency(portfolio, currentPositionGroup.WithQuantity(positionGroupQuantity, portfolio.Positions));
// Update our target portfolio margin allocated when considering fees, then calculate the new FinalOrderMargin
targetFinalMargin = (totalPortfolioValue - orderFees) * targetBufferFactor;
// Start safe check after first loop, stops endless recursion
if (lastPositionGroupOrderQuantity == positionGroupQuantity)
{
throw new ArgumentException(Messages.PositionGroupBuyingPowerModel.FailedToConvergeOnTargetMargin(targetFinalMargin,
positionGroupQuantity, orderFees, parameters));
}
lastPositionGroupOrderQuantity = positionGroupQuantity;
}
// Ensure that our target holdings margin will be less than or equal to our target allocated margin
while (Math.Abs(targetHoldingsMargin) > Math.Abs(targetFinalMargin));
return parameters.Result(inverted ? -positionGroupQuantity : positionGroupQuantity);
}
///
/// Get the maximum market position group order quantity to obtain a delta in the buying power used by a position group.
/// The deltas sign defines the position side to apply it to, positive long, negative short.
///
/// An object containing the portfolio, the position group and the delta buying power
/// Returns the maximum allowed market order quantity and if zero, also the reason
/// Used by the margin call model to reduce the position by a delta percent.
public virtual GetMaximumLotsResult GetMaximumLotsForDeltaBuyingPower(
GetMaximumLotsForDeltaBuyingPowerParameters parameters
)
{
// we convert this delta request into a target buying power request through projection
// by determining the currently used (reserved) buying power and adding the delta to
// arrive at a target buying power percentage
var currentPositionGroup = parameters.Portfolio.Positions[parameters.PositionGroup.Key];
var usedBuyingPower = parameters.PositionGroup.BuyingPowerModel.GetReservedBuyingPowerForPositionGroup(
parameters.Portfolio, currentPositionGroup
);
var targetBuyingPower = usedBuyingPower + parameters.DeltaBuyingPower;
// The reference position group is not necessarily in the same side as the position group in the portfolio, it could be the inverted.
// So the consumer needs the result relative to that position group instead of the one being held.
if (parameters.PositionGroup.IsInvertedOf(currentPositionGroup))
{
targetBuyingPower = parameters.DeltaBuyingPower - usedBuyingPower;
}
var targetBuyingPowerPercent = parameters.Portfolio.TotalPortfolioValue != 0
? targetBuyingPower / parameters.Portfolio.TotalPortfolioValue
: 0;
return GetMaximumLotsForTargetBuyingPower(new GetMaximumLotsForTargetBuyingPowerParameters(
parameters.Portfolio, parameters.PositionGroup, targetBuyingPowerPercent, parameters.MinimumOrderMarginPortfolioPercentage
));
}
///
/// Gets the buying power available for a position group trade
///
/// A parameters object containing the algorithm's portfolio, security, and order direction
/// The buying power available for the trade
public PositionGroupBuyingPower GetPositionGroupBuyingPower(PositionGroupBuyingPowerParameters parameters)
{
// SecurityPositionGroupBuyingPowerModel models buying power the same as non-grouped, so we can simply delegate
// to the security's model. For posterity, however, I'll lay out the process for computing the available buying
// power for a position group trade. There's two separate cases, one where we're increasing the position and one
// where we're decreasing the position and potentially crossing over zero. When decreasing the position we have
// to account for the reserved buying power that the position currently holds and add that to any free buying power
// in the portfolio.
// 1. Get portfolio's MarginRemaining (free buying power)
// 2. Determine if closing position
// 2a. Add reserved buying power freed up by closing the position
// 2b. Rebate initial buying power required for current position [to match current behavior, might not be possible]
// 1. Get MarginRemaining
var buyingPower = parameters.Portfolio.MarginRemaining;
// 2. Determine if closing position
IPositionGroup existing;
if (parameters.Portfolio.Positions.Groups.TryGetGroup(parameters.PositionGroup.Key, out existing))
{
var isInverted = parameters.PositionGroup.IsInvertedOf(existing);
if (isInverted && parameters.Direction == OrderDirection.Buy || !isInverted && parameters.Direction == OrderDirection.Sell)
{
// 2a. Add reserved buying power of current position
// Using the existing position group's buying power model to compute its reserved buying power and initial margin requirement.
// This is necessary because the margin calculations depend on the option strategy underneath the position group's BPM.
buyingPower += existing.Key.BuyingPowerModel.GetReservedBuyingPowerForPositionGroup(parameters.Portfolio, existing);
// 2b. Rebate the initial margin equivalent of current position
// this interface doesn't have a concept of initial margin as it's an impl detail of the BuyingPowerModel base class
buyingPower += Math.Abs(existing.Key.BuyingPowerModel.GetInitialMarginRequirement(parameters.Portfolio, existing));
}
}
return buyingPower;
}
///
/// Helper function to convert a to the account currency
///
protected virtual decimal ToAccountCurrency(SecurityPortfolioManager portfolio, CashAmount cash)
{
return portfolio.CashBook.ConvertToAccountCurrency(cash).Amount;
}
///
/// Helper function to compute the order fees associated with executing market orders for the specified
///
protected virtual decimal GetOrderFeeInAccountCurrency(SecurityPortfolioManager portfolio, IPositionGroup positionGroup)
{
// TODO : Add Order parameter to support Combo order type, pulling the orders per position
var orderFee = 0m;
var utcTime = portfolio.Securities.UtcTime;
foreach (var position in positionGroup)
{
var security = portfolio.Securities[position.Symbol];
var order = new MarketOrder(position.Symbol, position.Quantity, utcTime);
var positionOrderFee = security.FeeModel.GetOrderFee(new OrderFeeParameters(security, order)).Value;
orderFee += ToAccountCurrency(portfolio, positionOrderFee);
}
return orderFee;
}
///
/// Checks if the margin difference is not growing in final margin calculation, just making sure we don't end up in an infinite loop.
/// This function was split out to support derived types using the same error message as well as removing the added noise of the check
/// and message creation.
///
protected static bool UnableToConverge(decimal currentMarginDifference, decimal lastMarginDifference, IPositionGroup groupUnit,
SecurityPortfolioManager portfolio, decimal positionGroupQuantity, decimal targetMargin, decimal currentMargin,
decimal absUnitMargin, out ArgumentException error)
{
// determine if we're unable to converge by seeing if quantity estimate hasn't changed
if (Math.Abs(currentMarginDifference) > Math.Abs(lastMarginDifference) &&
Math.Sign(currentMarginDifference) == Math.Sign(lastMarginDifference)
|| currentMarginDifference == lastMarginDifference)
{
string message;
if (groupUnit.Count == 1)
{
// single security group
var security = portfolio.Securities[groupUnit.Single().Symbol];
message = "GetMaximumPositionGroupOrderQuantityForTargetBuyingPower failed to converge to target margin " +
Invariant($"{targetMargin}. Current margin is {currentMargin}. Position group quantity {positionGroupQuantity}. ") +
Invariant($"Lot size is {security.SymbolProperties.LotSize}.Security symbol ") +
Invariant($"{security.Symbol}. Margin unit {absUnitMargin}.");
}
else
{
message = "GetMaximumPositionGroupOrderQuantityForTargetBuyingPower failed to converge to target margin " +
Invariant($"{targetMargin}. Current margin is {currentMargin}. Position group quantity {positionGroupQuantity}. ") +
Invariant($"Position Group Unit is {groupUnit.Key}. Position Group Name ") +
Invariant($"{groupUnit.GetUserFriendlyName()}. Margin unit {absUnitMargin}.");
}
error = new ArgumentException(message);
return true;
}
error = null;
return false;
}
/// Indicates whether the current object is equal to another object of the same type.
/// An object to compare with this object.
/// true if the current object is equal to the parameter; otherwise, false.
public virtual bool Equals(IPositionGroupBuyingPowerModel other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return GetType() == other.GetType();
}
/// Determines whether the specified object is equal to the current object.
/// The object to compare with the current object.
/// true if the specified object is equal to the current object; otherwise, false.
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((IPositionGroupBuyingPowerModel) obj);
}
/// Serves as the default hash function.
/// A hash code for the current object.
public override int GetHashCode()
{
return GetType().GetHashCode();
}
///
/// Helper method that determines the amount to order to get to a given target safely.
/// Meaning it will either be at or just below target always.
///
/// Current portfolio
/// Current position group
/// Current margin reserved for the position
/// The target margin
/// Unit position group corresponding to the
/// Margin required for the
/// Output the final margin allocated for the position group
/// The size of the order to get safely to our target
public decimal GetPositionGroupOrderQuantity(SecurityPortfolioManager portfolio, IPositionGroup currentPositionGroup,
decimal currentUsedMargin, decimal targetFinalMargin, IPositionGroup groupUnit, decimal unitMargin,
out decimal finalMargin)
{
// Determine the direction to go towards when updating the estimate: +1 to increase, -1 to decrease.
var quantityStep = targetFinalMargin > currentUsedMargin ? +1 : -1;
// Compute initial position group quantity estimate -- group quantities are whole numbers [number of lots/unit quantities].
// - If going to the opposite side (target margin < 0), move towards said side from 0 since we need to completely close the position.
// - Else, just start with a unit step towards the determined direction.
var currentGroupAbsQuantity = Math.Abs(currentPositionGroup.Quantity);
var positionGroupQuantity = targetFinalMargin < 0 ? -currentGroupAbsQuantity + quantityStep : quantityStep;
// Calculate the initial value for the wanted final margin after the delta is applied.
var finalPositionGroup = currentPositionGroup.WithQuantity(currentGroupAbsQuantity + positionGroupQuantity, portfolio.Positions);
finalMargin = Math.Abs(finalPositionGroup.BuyingPowerModel.GetInitialMarginRequirement(portfolio, finalPositionGroup));
// Keep the previous calculated final margin we would get after the delta is applied.
// This is useful for the cases were the final group gets us with final margin greater than the target.
var prevFinalMargin = finalMargin;
// Begin iterating until the final margin is equal or greater than the target margin.
var absTargetFinalMargin = Math.Abs(targetFinalMargin);
var getMarginDifference = (decimal currentFinalMargin) =>
targetFinalMargin < 0 ? absTargetFinalMargin - currentFinalMargin : currentFinalMargin - absTargetFinalMargin;
var marginDifference = getMarginDifference(finalMargin);
while ((quantityStep < 0 && marginDifference > 0) || (quantityStep > 0 && marginDifference < 0))
{
positionGroupQuantity += quantityStep;
finalPositionGroup = currentPositionGroup.WithQuantity(currentGroupAbsQuantity + positionGroupQuantity, portfolio.Positions);
finalMargin = Math.Abs(finalPositionGroup.BuyingPowerModel.GetInitialMarginRequirement(portfolio, finalPositionGroup));
var newMarginDifference = getMarginDifference(finalMargin);
if (UnableToConverge(newMarginDifference, marginDifference, groupUnit, portfolio, positionGroupQuantity,
targetFinalMargin, currentUsedMargin, unitMargin, out var error))
{
throw error;
}
marginDifference = newMarginDifference;
}
// If the final margin is greater than the target, the result is the previous quantity,
// which is the maximum allowed to be within the target margin.
if (finalMargin > absTargetFinalMargin)
{
finalMargin = prevFinalMargin;
return positionGroupQuantity - quantityStep;
}
return positionGroupQuantity;
}
}
}