Lean  $LEAN_TAG$
PositionGroupBuyingPowerModel.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using QuantConnect.Orders;
18 using QuantConnect.Util;
19 using System;
20 using System.Collections.Generic;
21 using System.Linq;
22 using static QuantConnect.StringExtensions;
23 
25 {
26  /// <summary>
27  /// Provides a base class for implementations of <see cref="IPositionGroupBuyingPowerModel"/>
28  /// </summary>
30  {
31  /// <summary>
32  /// Gets the percentage of portfolio buying power to leave as a buffer
33  /// </summary>
34  protected decimal RequiredFreeBuyingPowerPercent { get; }
35 
36  /// <summary>
37  /// Initializes a new instance of the <see cref="PositionGroupBuyingPowerModel"/> class
38  /// </summary>
39  /// <param name="requiredFreeBuyingPowerPercent">The percentage of portfolio buying power to leave as a buffer</param>
40  protected PositionGroupBuyingPowerModel(decimal requiredFreeBuyingPowerPercent = 0m)
41  {
42  RequiredFreeBuyingPowerPercent = requiredFreeBuyingPowerPercent;
43  }
44 
45  /// <summary>
46  /// Gets the margin currently allocated to the specified holding
47  /// </summary>
48  /// <param name="parameters">An object containing the security</param>
49  /// <returns>The maintenance margin required for the </returns>
51 
52  /// <summary>
53  /// The margin that must be held in order to increase the position by the provided quantity
54  /// </summary>
55  /// <param name="parameters">An object containing the security and quantity</param>
57 
58  /// <summary>
59  /// Gets the total margin required to execute the specified order in units of the account currency including fees
60  /// </summary>
61  /// <param name="parameters">An object containing the portfolio, the security and the order</param>
62  /// <returns>The total margin in terms of the currency quoted in the order</returns>
64 
65  /// <summary>
66  /// Computes the impact on the portfolio's buying power from adding the position group to the portfolio. This is
67  /// a 'what if' analysis to determine what the state of the portfolio would be if these changes were applied. The
68  /// delta (before - after) is the margin requirement for adding the positions and if the margin used after the changes
69  /// are applied is less than the total portfolio value, this indicates sufficient capital.
70  /// </summary>
71  /// <param name="parameters">An object containing the portfolio and a position group containing the contemplated
72  /// changes to the portfolio</param>
73  /// <returns>Returns the portfolio's total portfolio value and margin used before and after the position changes are applied</returns>
75  {
76  // This process aims to avoid having to compute buying power on the entire portfolio and instead determines
77  // the set of groups that can be impacted by the changes being contemplated. The only real way to determine
78  // the change in maintenance margin is to determine what groups we'll have after the changes and compute the
79  // margin based on that.
80  // 1. Determine impacted groups (depends on IPositionGroupResolver.GetImpactedGroups)
81  // 2. Compute the currently reserved buying power of impacted groups
82  // 3. Create position collection using impacted groups and apply contemplated changes
83  // 4. Resolve new position groups using position collection with applied contemplated changes
84  // 5. Compute the contemplated reserved buying power on these newly resolved groups
85 
86  // 1. Determine impacted groups
87  var positionManager = parameters.Portfolio.Positions;
88 
89  // 2. Compute current reserved buying power
90  var current = 0m;
91  var impactedGroups = new List<IPositionGroup>();
92 
93  // 3. Determine set of impacted positions to be grouped
94  var positions = parameters.Orders.Select(o => o.CreatePositions(parameters.Portfolio.Securities)).SelectMany(p => p).ToList();
95 
96  var impactedPositions = positions.ToDictionary(p => p.Symbol);
97 
98  foreach (var impactedGroup in positionManager.GetImpactedGroups(positions))
99  {
100  impactedGroups.Add(impactedGroup);
101  current += impactedGroup.BuyingPowerModel.GetReservedBuyingPowerForPositionGroup(
102  parameters.Portfolio, impactedGroup
103  );
104 
105  foreach (var position in impactedGroup)
106  {
107  IPosition existing;
108  if (impactedPositions.TryGetValue(position.Symbol, out existing))
109  {
110  // if it already exists then combine it with the existing
111  impactedPositions[position.Symbol] = existing.Combine(position);
112  }
113  else
114  {
115  impactedPositions[position.Symbol] = position;
116  }
117  }
118  }
119 
120  // 4. Resolve new position groups
121  var contemplatedGroups = positionManager.ResolvePositionGroups(new PositionCollection(impactedPositions.Values));
122 
123  // 5. Compute contemplated margin
124  var contemplated = GetContemplatedGroupsInitialMargin(parameters.Portfolio, contemplatedGroups, positions);
125 
126  return new ReservedBuyingPowerImpact(current, contemplated, impactedGroups, parameters.ContemplatedChanges, contemplatedGroups);
127  }
128 
129  /// <summary>
130  /// Gets the initial margin required for the specified contemplated position group.
131  /// Used by <see cref="GetReservedBuyingPowerImpact"/> to get the contemplated groups margin.
132  /// </summary>
133  protected virtual decimal GetContemplatedGroupsInitialMargin(SecurityPortfolioManager portfolio, PositionGroupCollection contemplatedGroups,
134  List<IPosition> ordersPositions)
135  {
136  var contemplatedMargin = 0m;
137  foreach (var contemplatedGroup in contemplatedGroups)
138  {
139  // We use the initial margin requirement as the contemplated groups margin in order to ensure
140  // the available buying power is enough to execute the order.
141  contemplatedMargin += contemplatedGroup.BuyingPowerModel.GetInitialMarginRequirement(portfolio, contemplatedGroup);
142  }
143 
144  return contemplatedMargin;
145  }
146 
147  /// <summary>
148  /// Check if there is sufficient buying power for the position group to execute this order.
149  /// </summary>
150  /// <param name="parameters">An object containing the portfolio, the position group and the order</param>
151  /// <returns>Returns buying power information for an order against a position group</returns>
154  )
155  {
156  // The addition of position groups requires that we not only check initial margin requirements, but also
157  // that we confirm that after the changes have been applied and the new groups resolved our maintenance
158  // margin is still in a valid range (less than TPV). For this model, we use the security's sufficient buying
159  // power impl to confirm initial margin requirements and lean heavily on GetReservedBuyingPowerImpact for
160  // help with confirming that our expected maintenance margin is still less than TPV.
161  // 1. Confirm we have sufficient buying power to execute the trade using security's BP model
162  // 2. Confirm we pass position group specific checks
163  // 3. Confirm we haven't exceeded maintenance margin limits via GetReservedBuyingPowerImpact's delta
164 
165  // 1. Confirm we meet initial margin requirements, accounting for buffer
166  var deltaBuyingPowerArgs = new ReservedBuyingPowerImpactParameters(parameters.Portfolio, parameters.PositionGroup, parameters.Orders);
167  var deltaBuyingPower = GetReservedBuyingPowerImpact(deltaBuyingPowerArgs).Delta;
168 
169  // When order only reduces or closes a security position, capital is always sufficient
170  if (deltaBuyingPower < 0)
171  {
172  return parameters.Sufficient();
173  }
174 
175  var availableBuyingPower = parameters.Portfolio.MarginRemaining;
176 
177  // 2. Confirm we pass position group specific checks
178  var result = PassesPositionGroupSpecificBuyingPowerForOrderChecks(parameters, availableBuyingPower);
179  if (result?.IsSufficient == false)
180  {
181  return result;
182  }
183 
184  // 3. Confirm that the new groupings arising from the change doesn't make maintenance margin exceed TPV
185  // We can just compare the delta to the available buying power because the delta how much the maintenance margin will increase by
186  // if the order is executed, so it needs to stay below the available buying power
187  if (deltaBuyingPower <= availableBuyingPower)
188  {
189  return parameters.Sufficient();
190  }
191 
192  return parameters.Insufficient(Invariant($@"Id: {string.Join(",", parameters.Orders.Select(o => o.Id))}, Maintenance Margin Delta: {
193  deltaBuyingPower.Normalize()}, Free Margin: {availableBuyingPower.Normalize()}"
194  ));
195  }
196 
197  /// <summary>
198  /// Provides a mechanism for derived types to add their own buying power for order checks without needing to
199  /// recompute the available buying power. Implementations should return null if all checks pass and should
200  /// return an instance of <see cref="HasSufficientBuyingPowerForOrderResult"/> with IsSufficient=false if it
201  /// fails.
202  /// </summary>
205  decimal availableBuyingPower
206  )
207  {
208  return null;
209  }
210 
211  /// <summary>
212  /// Computes the amount of buying power reserved by the provided position group
213  /// </summary>
216  )
217  {
218  return this.GetMaintenanceMargin(parameters.Portfolio, parameters.PositionGroup);
219  }
220 
221  /// <summary>
222  /// Get the maximum position group order quantity to obtain a position with a given buying power
223  /// percentage. Will not take into account free buying power.
224  /// </summary>
225  /// <param name="parameters">An object containing the portfolio, the position group and the target
226  /// signed buying power percentage</param>
227  /// <returns>
228  /// Returns the maximum allowed market order quantity and if zero, also the reason.
229  ///
230  /// Since there is no sense of "short" or "long" on position groups with multiple positions,
231  /// the sign of the returned quantity will indicate the direction of the order regarding the
232  /// reference position group passed in the parameters:
233  /// - quantity &gt; 0: the order should be placed in the same direction as the reference position group to increase it,
234  /// without changing the positions' signs.
235  /// - quantity &lt; 0: the order should be placed in the opposite direction as the reference position group to reduce it,
236  /// using each position's opposite sign.
237  /// </returns>
240  )
241  {
242  // In order to determine maximum order quantity for a particular amount of buying power, we must resolve
243  // the group's 'unit' as this will be the quantity step size. If we don't step according to these units
244  // then we could be left with a different type of group with vastly different margin requirements, so we
245  // must keep the ratios between all of the position quantities the same. First we'll determine the target
246  // buying power, taking into account RequiredFreeBuyingPowerPercent to ensure a buffer. Then we'll evaluate
247  // the initial margin requirement using the provided position group position quantities. From this value,
248  // we can determine if we need to add more quantity or remove quantity by looking at the delta from the target
249  // to the computed initial margin requirement. We can also compute, assuming linearity, the change in initial
250  // margin requirements for each 'unit' of the position group added. The final value we need before starting to
251  // iterate to solve for quantity is the minimum quantities. This is the 'unit' of the position group, and any
252  // quantities less than the unit's quantity would yield an entirely different group w/ different margin calcs.
253  // Now that we've resolved our target, our group unit and the unit's initial margin requirement, we can iterate
254  // increasing/decreasing quantities in multiples of the unit's quantities until we're within a unit's amount of
255  // initial margin to the target buying power.
256  // NOTE: The first estimate MUST be greater than the target and iteration will successively decrease quantity estimates.
257  // 1. Determine current holdings of position group
258  // 2. Determine target buying power, taking into account RequiredFreeBuyingPowerPercent
259  // 2a. If targeting zero, simply return the negative of the quantity
260  // 3. Determine current used margin [we're using initial here to match BuyingPowerModel]
261  // 4. Check that the change of margin is above our models minimum percentage change
262  // 5. Resolve the group's 'unit' quantities, this is our step size
263  // 5a. Compute the initial margin requirement for a single unit
264  // 6. Begin iterating until the allocated holdings margin (after order fees are applied) less or equal to the expected target margin
265  // 6a. Calculate the amount to order to get the target margin
266  // 6b. Apply order fees to the allocated holdings margin and compare to the target margin to end loop.
267 
268  var portfolio = parameters.Portfolio;
269 
270  // 1. Determine current holdings of position group
271  var currentPositionGroup = portfolio.Positions[parameters.PositionGroup.Key];
272 
273  var inverted = false;
274  var targetBuyingPower = parameters.TargetBuyingPower;
275  // The reference position group is not necessarily in the same side as the position group in the portfolio, it could be the inverted.
276  // So the consumer needs the result relative to that position group instead of the one being held.
277  if (parameters.PositionGroup.IsInvertedOf(currentPositionGroup))
278  {
279  inverted = true;
280  targetBuyingPower = -targetBuyingPower;
281  }
282 
283  // 2. Determine target buying power, taking into account RequiredFreeBuyingPowerPercent
284  var bufferFactor = 1 - RequiredFreeBuyingPowerPercent;
285  var targetBufferFactor = bufferFactor * targetBuyingPower;
286  var totalPortfolioValue = portfolio.TotalPortfolioValue;
287  var targetFinalMargin = targetBufferFactor * totalPortfolioValue;
288 
289  // 2a. If targeting zero, simply return the negative of the quantity
290  if (targetFinalMargin == 0)
291  {
292  var quantity = -Math.Abs(currentPositionGroup.Quantity);
293  return parameters.Result(inverted ? -quantity : quantity);
294  }
295 
296  // 3. Determine initial margin requirement for current holdings
297  var currentUsedMargin = 0m;
298  if (currentPositionGroup.Quantity != 0)
299  {
300  currentUsedMargin = Math.Abs(currentPositionGroup.BuyingPowerModel.GetInitialMarginRequirement(portfolio, currentPositionGroup));
301  }
302 
303  // 4. Check that the change of margin is above our models minimum percentage change
304  var absDifferenceOfMargin = Math.Abs(targetFinalMargin - currentUsedMargin);
306  parameters.MinimumOrderMarginPortfolioPercentage, absDifferenceOfMargin))
307  {
308  string reason = null;
309  if (!parameters.SilenceNonErrorReasons)
310  {
311  var minimumValue = totalPortfolioValue * parameters.MinimumOrderMarginPortfolioPercentage;
312  reason = Messages.BuyingPowerModel.TargetOrderMarginNotAboveMinimum(absDifferenceOfMargin, minimumValue);
313  }
314  return new GetMaximumLotsResult(0, reason, false);
315  }
316 
317  // 5. Resolve 'unit' group -- this is our step size
318  var groupUnit = currentPositionGroup.CreateUnitGroup(parameters.Portfolio.Positions);
319 
320  // 5a. Compute initial margin requirement for a single unit
321  var unitMargin = Math.Abs(groupUnit.BuyingPowerModel.GetInitialMarginRequirement(portfolio, groupUnit));
322  if (unitMargin == 0m)
323  {
324  // likely due to missing price data
325  var zeroPricedPosition = parameters.PositionGroup.FirstOrDefault(
326  p => portfolio.Securities.GetValueOrDefault(p.Symbol)?.Price == 0m
327  );
328  return parameters.Error(zeroPricedPosition?.Symbol.GetZeroPriceMessage()
330  }
331 
332  // 6. Begin iterating
333  var lastPositionGroupOrderQuantity = 0m; // For safety check
334  decimal orderFees;
335  decimal targetHoldingsMargin;
336  decimal positionGroupQuantity;
337  do
338  {
339  // 6a.Calculate the amount to order to get the target margin
340  positionGroupQuantity = GetPositionGroupOrderQuantity(portfolio, currentPositionGroup, currentUsedMargin, targetFinalMargin,
341  groupUnit, unitMargin, out targetHoldingsMargin);
342  if (positionGroupQuantity == 0)
343  {
344  string reason = null;
345  if (!parameters.SilenceNonErrorReasons)
346  {
347  reason = Messages.PositionGroupBuyingPowerModel.PositionGroupQuantityRoundedToZero(targetFinalMargin - currentUsedMargin);
348  }
349 
350  return new GetMaximumLotsResult(0, reason, false);
351  }
352 
353  // 6b.Apply order fees to the allocated holdings margin
354  orderFees = GetOrderFeeInAccountCurrency(portfolio, currentPositionGroup.WithQuantity(positionGroupQuantity, portfolio.Positions));
355 
356  // Update our target portfolio margin allocated when considering fees, then calculate the new FinalOrderMargin
357  targetFinalMargin = (totalPortfolioValue - orderFees) * targetBufferFactor;
358 
359  // Start safe check after first loop, stops endless recursion
360  if (lastPositionGroupOrderQuantity == positionGroupQuantity)
361  {
362  throw new ArgumentException(Messages.PositionGroupBuyingPowerModel.FailedToConvergeOnTargetMargin(targetFinalMargin,
363  positionGroupQuantity, orderFees, parameters));
364  }
365 
366  lastPositionGroupOrderQuantity = positionGroupQuantity;
367 
368  }
369  // Ensure that our target holdings margin will be less than or equal to our target allocated margin
370  while (Math.Abs(targetHoldingsMargin) > Math.Abs(targetFinalMargin));
371 
372  return parameters.Result(inverted ? -positionGroupQuantity : positionGroupQuantity);
373  }
374 
375  /// <summary>
376  /// Get the maximum market position group order quantity to obtain a delta in the buying power used by a position group.
377  /// The deltas sign defines the position side to apply it to, positive long, negative short.
378  /// </summary>
379  /// <param name="parameters">An object containing the portfolio, the position group and the delta buying power</param>
380  /// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns>
381  /// <remarks>Used by the margin call model to reduce the position by a delta percent.</remarks>
384  )
385  {
386  // we convert this delta request into a target buying power request through projection
387  // by determining the currently used (reserved) buying power and adding the delta to
388  // arrive at a target buying power percentage
389 
390  var currentPositionGroup = parameters.Portfolio.Positions[parameters.PositionGroup.Key];
392  parameters.Portfolio, currentPositionGroup
393  );
394 
395  var targetBuyingPower = usedBuyingPower + parameters.DeltaBuyingPower;
396  // The reference position group is not necessarily in the same side as the position group in the portfolio, it could be the inverted.
397  // So the consumer needs the result relative to that position group instead of the one being held.
398  if (parameters.PositionGroup.IsInvertedOf(currentPositionGroup))
399  {
400  targetBuyingPower = parameters.DeltaBuyingPower - usedBuyingPower;
401  }
402 
403  var targetBuyingPowerPercent = parameters.Portfolio.TotalPortfolioValue != 0
404  ? targetBuyingPower / parameters.Portfolio.TotalPortfolioValue
405  : 0;
406 
408  parameters.Portfolio, parameters.PositionGroup, targetBuyingPowerPercent, parameters.MinimumOrderMarginPortfolioPercentage
409  ));
410  }
411 
412  /// <summary>
413  /// Gets the buying power available for a position group trade
414  /// </summary>
415  /// <param name="parameters">A parameters object containing the algorithm's portfolio, security, and order direction</param>
416  /// <returns>The buying power available for the trade</returns>
418  {
419  // SecurityPositionGroupBuyingPowerModel models buying power the same as non-grouped, so we can simply delegate
420  // to the security's model. For posterity, however, I'll lay out the process for computing the available buying
421  // power for a position group trade. There's two separate cases, one where we're increasing the position and one
422  // where we're decreasing the position and potentially crossing over zero. When decreasing the position we have
423  // to account for the reserved buying power that the position currently holds and add that to any free buying power
424  // in the portfolio.
425  // 1. Get portfolio's MarginRemaining (free buying power)
426  // 2. Determine if closing position
427  // 2a. Add reserved buying power freed up by closing the position
428  // 2b. Rebate initial buying power required for current position [to match current behavior, might not be possible]
429 
430  // 1. Get MarginRemaining
431  var buyingPower = parameters.Portfolio.MarginRemaining;
432 
433  // 2. Determine if closing position
434  IPositionGroup existing;
435  if (parameters.Portfolio.Positions.Groups.TryGetGroup(parameters.PositionGroup.Key, out existing))
436  {
437  var isInverted = parameters.PositionGroup.IsInvertedOf(existing);
438  if (isInverted && parameters.Direction == OrderDirection.Buy || !isInverted && parameters.Direction == OrderDirection.Sell)
439  {
440  // 2a. Add reserved buying power of current position
441  // Using the existing position group's buying power model to compute its reserved buying power and initial margin requirement.
442  // This is necessary because the margin calculations depend on the option strategy underneath the position group's BPM.
443  buyingPower += existing.Key.BuyingPowerModel.GetReservedBuyingPowerForPositionGroup(parameters.Portfolio, existing);
444 
445  // 2b. Rebate the initial margin equivalent of current position
446  // this interface doesn't have a concept of initial margin as it's an impl detail of the BuyingPowerModel base class
447  buyingPower += Math.Abs(existing.Key.BuyingPowerModel.GetInitialMarginRequirement(parameters.Portfolio, existing));
448  }
449  }
450 
451  return buyingPower;
452  }
453 
454  /// <summary>
455  /// Helper function to convert a <see cref="CashAmount"/> to the account currency
456  /// </summary>
457  protected virtual decimal ToAccountCurrency(SecurityPortfolioManager portfolio, CashAmount cash)
458  {
459  return portfolio.CashBook.ConvertToAccountCurrency(cash).Amount;
460  }
461 
462  /// <summary>
463  /// Helper function to compute the order fees associated with executing market orders for the specified <paramref name="positionGroup"/>
464  /// </summary>
465  protected virtual decimal GetOrderFeeInAccountCurrency(SecurityPortfolioManager portfolio, IPositionGroup positionGroup)
466  {
467  // TODO : Add Order parameter to support Combo order type, pulling the orders per position
468 
469  var orderFee = 0m;
470  var utcTime = portfolio.Securities.UtcTime;
471 
472  foreach (var position in positionGroup)
473  {
474  var security = portfolio.Securities[position.Symbol];
475  var order = new MarketOrder(position.Symbol, position.Quantity, utcTime);
476  var positionOrderFee = security.FeeModel.GetOrderFee(new OrderFeeParameters(security, order)).Value;
477  orderFee += ToAccountCurrency(portfolio, positionOrderFee);
478  }
479 
480  return orderFee;
481  }
482 
483  /// <summary>
484  /// Checks if the margin difference is not growing in final margin calculation, just making sure we don't end up in an infinite loop.
485  /// This function was split out to support derived types using the same error message as well as removing the added noise of the check
486  /// and message creation.
487  /// </summary>
488  protected static bool UnableToConverge(decimal currentMarginDifference, decimal lastMarginDifference, IPositionGroup groupUnit,
489  SecurityPortfolioManager portfolio, decimal positionGroupQuantity, decimal targetMargin, decimal currentMargin,
490  decimal absUnitMargin, out ArgumentException error)
491  {
492  // determine if we're unable to converge by seeing if quantity estimate hasn't changed
493  if (Math.Abs(currentMarginDifference) > Math.Abs(lastMarginDifference) &&
494  Math.Sign(currentMarginDifference) == Math.Sign(lastMarginDifference)
495  || currentMarginDifference == lastMarginDifference)
496  {
497  string message;
498  if (groupUnit.Count == 1)
499  {
500  // single security group
501  var security = portfolio.Securities[groupUnit.Single().Symbol];
502  message = "GetMaximumPositionGroupOrderQuantityForTargetBuyingPower failed to converge to target margin " +
503  Invariant($"{targetMargin}. Current margin is {currentMargin}. Position group quantity {positionGroupQuantity}. ") +
504  Invariant($"Lot size is {security.SymbolProperties.LotSize}.Security symbol ") +
505  Invariant($"{security.Symbol}. Margin unit {absUnitMargin}.");
506  }
507  else
508  {
509  message = "GetMaximumPositionGroupOrderQuantityForTargetBuyingPower failed to converge to target margin " +
510  Invariant($"{targetMargin}. Current margin is {currentMargin}. Position group quantity {positionGroupQuantity}. ") +
511  Invariant($"Position Group Unit is {groupUnit.Key}. Position Group Name ") +
512  Invariant($"{groupUnit.GetUserFriendlyName()}. Margin unit {absUnitMargin}.");
513  }
514 
515  error = new ArgumentException(message);
516  return true;
517  }
518 
519  error = null;
520  return false;
521  }
522 
523  /// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
524  /// <param name="other">An object to compare with this object.</param>
525  /// <returns>true if the current object is equal to the <paramref name="other" /> parameter; otherwise, false.</returns>
526  public virtual bool Equals(IPositionGroupBuyingPowerModel other)
527  {
528  if (ReferenceEquals(null, other))
529  {
530  return false;
531  }
532 
533  if (ReferenceEquals(this, other))
534  {
535  return true;
536  }
537 
538  return GetType() == other.GetType();
539  }
540 
541  /// <summary>Determines whether the specified object is equal to the current object.</summary>
542  /// <param name="obj">The object to compare with the current object. </param>
543  /// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
544  public override bool Equals(object obj)
545  {
546  if (ReferenceEquals(null, obj))
547  {
548  return false;
549  }
550 
551  if (ReferenceEquals(this, obj))
552  {
553  return true;
554  }
555 
556  if (obj.GetType() != GetType())
557  {
558  return false;
559  }
560 
562  }
563 
564  /// <summary>Serves as the default hash function. </summary>
565  /// <returns>A hash code for the current object.</returns>
566  public override int GetHashCode()
567  {
568  return GetType().GetHashCode();
569  }
570 
571  /// <summary>
572  /// Helper method that determines the amount to order to get to a given target safely.
573  /// Meaning it will either be at or just below target always.
574  /// </summary>
575  /// <param name="portfolio">Current portfolio</param>
576  /// <param name="currentPositionGroup">Current position group</param>
577  /// <param name="currentUsedMargin">Current margin reserved for the position</param>
578  /// <param name="targetFinalMargin">The target margin</param>
579  /// <param name="groupUnit">Unit position group corresponding to the <paramref name="currentPositionGroup"/></param>
580  /// <param name="unitMargin">Margin required for the <paramref name="groupUnit"/></param>
581  /// <param name="finalMargin">Output the final margin allocated for the position group</param>
582  /// <returns>The size of the order to get safely to our target</returns>
583  public decimal GetPositionGroupOrderQuantity(SecurityPortfolioManager portfolio, IPositionGroup currentPositionGroup,
584  decimal currentUsedMargin, decimal targetFinalMargin, IPositionGroup groupUnit, decimal unitMargin,
585  out decimal finalMargin)
586  {
587  // Determine the direction to go towards when updating the estimate: +1 to increase, -1 to decrease.
588  var quantityStep = targetFinalMargin > currentUsedMargin ? +1 : -1;
589 
590  // Compute initial position group quantity estimate -- group quantities are whole numbers [number of lots/unit quantities].
591  // - If going to the opposite side (target margin < 0), move towards said side from 0 since we need to completely close the position.
592  // - Else, just start with a unit step towards the determined direction.
593  var currentGroupAbsQuantity = Math.Abs(currentPositionGroup.Quantity);
594  var positionGroupQuantity = targetFinalMargin < 0 ? -currentGroupAbsQuantity + quantityStep : quantityStep;
595 
596  // Calculate the initial value for the wanted final margin after the delta is applied.
597  var finalPositionGroup = currentPositionGroup.WithQuantity(currentGroupAbsQuantity + positionGroupQuantity, portfolio.Positions);
598  finalMargin = Math.Abs(finalPositionGroup.BuyingPowerModel.GetInitialMarginRequirement(portfolio, finalPositionGroup));
599 
600  // Keep the previous calculated final margin we would get after the delta is applied.
601  // This is useful for the cases were the final group gets us with final margin greater than the target.
602  var prevFinalMargin = finalMargin;
603 
604  // Begin iterating until the final margin is equal or greater than the target margin.
605  var absTargetFinalMargin = Math.Abs(targetFinalMargin);
606  var getMarginDifference = (decimal currentFinalMargin) =>
607  targetFinalMargin < 0 ? absTargetFinalMargin - currentFinalMargin : currentFinalMargin - absTargetFinalMargin;
608 
609  var marginDifference = getMarginDifference(finalMargin);
610  while ((quantityStep < 0 && marginDifference > 0) || (quantityStep > 0 && marginDifference < 0))
611  {
612  positionGroupQuantity += quantityStep;
613  finalPositionGroup = currentPositionGroup.WithQuantity(currentGroupAbsQuantity + positionGroupQuantity, portfolio.Positions);
614  finalMargin = Math.Abs(finalPositionGroup.BuyingPowerModel.GetInitialMarginRequirement(portfolio, finalPositionGroup));
615 
616  var newMarginDifference = getMarginDifference(finalMargin);
617  if (UnableToConverge(newMarginDifference, marginDifference, groupUnit, portfolio, positionGroupQuantity,
618  targetFinalMargin, currentUsedMargin, unitMargin, out var error))
619  {
620  throw error;
621  }
622 
623  marginDifference = newMarginDifference;
624  }
625 
626  // If the final margin is greater than the target, the result is the previous quantity,
627  // which is the maximum allowed to be within the target margin.
628  if (finalMargin > absTargetFinalMargin)
629  {
630  finalMargin = prevFinalMargin;
631  return positionGroupQuantity - quantityStep;
632  }
633 
634  return positionGroupQuantity;
635  }
636  }
637 }