chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
@@ -0,0 +1,160 @@
/*
* 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 System;
using System.Linq;
using System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Provides an implementation of <see cref="IPositionGroupResolver"/> that invokes multiple wrapped implementations
/// in succession. Each successive call to <see cref="IPositionGroupResolver.Resolve"/> will receive
/// the remaining positions that have yet to be grouped. Any non-grouped positions are placed into identity groups.
/// </summary>
public class CompositePositionGroupResolver : IPositionGroupResolver
{
/// <summary>
/// Gets the count of registered resolvers
/// </summary>
public int Count => _resolvers.Count;
private readonly List<IPositionGroupResolver> _resolvers;
/// <summary>
/// Initializes a new instance of the <see cref="CompositePositionGroupResolver"/> class
/// </summary>
/// <param name="resolvers">The position group resolvers to be invoked in order</param>
public CompositePositionGroupResolver(params IPositionGroupResolver[] resolvers)
: this((IEnumerable<IPositionGroupResolver>)resolvers)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CompositePositionGroupResolver"/> class
/// </summary>
/// <param name="resolvers">The position group resolvers to be invoked in order</param>
public CompositePositionGroupResolver(IEnumerable<IPositionGroupResolver> resolvers)
{
_resolvers = resolvers.ToList();
}
/// <summary>
/// Adds the specified <paramref name="resolver"/> to the end of the list of resolvers. This resolver will run last.
/// </summary>
/// <param name="resolver">The resolver to be added</param>
public void Add(IPositionGroupResolver resolver)
{
_resolvers.Add(resolver);
}
/// <summary>
/// Inserts the specified <paramref name="resolver"/> into the list of resolvers at the specified index.
/// </summary>
/// <param name="resolver">The resolver to be inserted</param>
/// <param name="index">The zero based index indicating where to insert the resolver, zero inserts to the beginning
/// of the list making this resolver un first and <see cref="Count"/> inserts the resolver to the end of the list
/// making this resolver run last</param>
public void Add(IPositionGroupResolver resolver, int index)
{
// insert handles bounds checking
_resolvers.Insert(index, resolver);
}
/// <summary>
/// Removes the specified <paramref name="resolver"/> from the list of resolvers
/// </summary>
/// <param name="resolver">The resolver to be removed</param>
/// <returns>True if the resolver was removed, false if it wasn't found in the list</returns>
public bool Remove(IPositionGroupResolver resolver)
{
return _resolvers.Remove(resolver);
}
/// <summary>
/// Resolves the optimal set of <see cref="IPositionGroup"/> from the provided <paramref name="positions"/>.
/// Implementations are required to deduct grouped positions from the <paramref name="positions"/> collection.
/// </summary>
public PositionGroupCollection Resolve(PositionCollection positions)
{
// we start with no groups, each resolver's result will get merged in
var groups = PositionGroupCollection.Empty;
// each call to ResolvePositionGroups is expected to deduct grouped positions from the PositionCollection
foreach (var resolver in _resolvers)
{
var resolved = resolver.Resolve(positions);
groups = groups.CombineWith(resolved);
}
if (positions.Count > 0)
{
throw new InvalidOperationException("All positions must be resolved into groups.");
}
return groups;
}
/// <summary>
/// Attempts to group the specified positions into a new <see cref="IPositionGroup"/> using an
/// appropriate <see cref="IPositionGroupBuyingPowerModel"/> for position groups created via this
/// resolver.
/// </summary>
/// <param name="newPositions">The positions to be grouped</param>
/// <param name="currentPositions">The currently grouped positions</param>
/// <param name="group">The grouped positions when this resolver is able to, otherwise null</param>
/// <returns>True if this resolver can group the specified positions, otherwise false</returns>
public bool TryGroup(IReadOnlyCollection<IPosition> newPositions, PositionGroupCollection currentPositions, out IPositionGroup group)
{
foreach (var resolver in _resolvers)
{
if (resolver.TryGroup(newPositions, currentPositions, out group))
{
return true;
}
}
group = null;
return false;
}
/// <summary>
/// Determines the position groups that would be evaluated for grouping of the specified
/// positions were passed into the <see cref="Resolve"/> method.
/// </summary>
/// <remarks>
/// This function allows us to determine a set of impacted groups and run the resolver on just
/// those groups in order to support what-if analysis
/// </remarks>
/// <param name="groups">The existing position groups</param>
/// <param name="positions">The positions being changed</param>
/// <returns>An enumerable containing the position groups that could be impacted by the specified position changes</returns>
public IEnumerable<IPositionGroup> GetImpactedGroups(PositionGroupCollection groups, IReadOnlyCollection<IPosition> positions)
{
// we keep track of yielded groups for all resolvers
var seen = new HashSet<PositionGroupKey>();
foreach (var resolver in _resolvers)
{
foreach (var group in resolver.GetImpactedGroups(groups, positions))
{
if (seen.Add(group.Key))
{
yield return group;
}
}
}
}
}
}
@@ -0,0 +1,107 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines the parameters for <see cref="IPositionGroupBuyingPowerModel.GetMaximumLotsForDeltaBuyingPower"/>
/// </summary>
public class GetMaximumLotsForDeltaBuyingPowerParameters
{
/// <summary>
/// Gets the algorithm's portfolio manager
/// </summary>
public SecurityPortfolioManager Portfolio { get; }
/// <summary>
/// Gets the position group
/// </summary>
public IPositionGroup PositionGroup { get; }
/// <summary>
/// The delta buying power.
/// </summary>
/// <remarks>Sign defines the position side to apply the delta, positive long, negative short side.</remarks>
public decimal DeltaBuyingPower { get; }
/// <summary>
/// True enables the <see cref="IBuyingPowerModel"/> to skip setting <see cref="GetMaximumLotsResult.Reason"/>
/// for non error situations, for performance
/// </summary>
public bool SilenceNonErrorReasons { get; }
/// <summary>
/// Configurable minimum order margin portfolio percentage to ignore bad orders, orders with unrealistic small sizes
/// </summary>
/// <remarks>Default value is 0. This setting is useful to avoid small trading noise when using SetHoldings</remarks>
public decimal MinimumOrderMarginPortfolioPercentage { get; }
/// <summary>
/// Initializes a new instance of the <see cref="GetMaximumLotsForDeltaBuyingPowerParameters"/> class
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="positionGroup">The position group</param>
/// <param name="deltaBuyingPower">The delta buying power to apply. Sign defines the position side to apply the delta</param>
/// <param name="minimumOrderMarginPortfolioPercentage">Configurable minimum order margin portfolio percentage to ignore orders with unrealistic small sizes</param>
/// <param name="silenceNonErrorReasons">True will not return <see cref="GetMaximumLotsResult.Reason"/>
/// set for non error situation, this is for performance</param>
public GetMaximumLotsForDeltaBuyingPowerParameters(
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup,
decimal deltaBuyingPower,
decimal minimumOrderMarginPortfolioPercentage,
bool silenceNonErrorReasons = false
)
{
Portfolio = portfolio;
PositionGroup = positionGroup;
DeltaBuyingPower = deltaBuyingPower;
SilenceNonErrorReasons = silenceNonErrorReasons;
MinimumOrderMarginPortfolioPercentage = minimumOrderMarginPortfolioPercentage;
}
/// <summary>
/// Creates a new <see cref="GetMaximumLotsResult"/> with zero quantity and an error message.
/// </summary>
public GetMaximumLotsResult Error(string reason)
{
return new GetMaximumLotsResult(0, reason, true);
}
/// <summary>
/// Creates a new <see cref="GetMaximumLotsResult"/> with zero quantity and no message.
/// </summary>
public GetMaximumLotsResult Zero()
{
return new GetMaximumLotsResult(0, string.Empty, false);
}
/// <summary>
/// Creates a new <see cref="GetMaximumLotsResult"/> with zero quantity and an info message.
/// </summary>
public GetMaximumLotsResult Zero(string reason)
{
return new GetMaximumLotsResult(0, reason, false);
}
/// <summary>
/// Creates a new <see cref="GetMaximumLotsResult"/> for the specified quantity and no message.
/// </summary>
public GetMaximumLotsResult Result(decimal quantity)
{
return new GetMaximumLotsResult(quantity, string.Empty, false);
}
}
}
@@ -0,0 +1,107 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines the parameters for <see cref="IPositionGroupBuyingPowerModel.GetMaximumLotsForTargetBuyingPower"/>
/// </summary>
public class GetMaximumLotsForTargetBuyingPowerParameters
{
/// <summary>
/// Gets the algorithm's portfolio manager
/// </summary>
public SecurityPortfolioManager Portfolio { get; }
/// <summary>
/// Gets the position group
/// </summary>
public IPositionGroup PositionGroup { get; }
/// <summary>
/// The target buying power.
/// </summary>
/// <remarks>Sign defines the position side, positive long, negative short side.</remarks>
public decimal TargetBuyingPower { get; }
/// <summary>
/// True enables the <see cref="IBuyingPowerModel"/> to skip setting <see cref="GetMaximumLotsResult.Reason"/>
/// for non error situations, for performance
/// </summary>
public bool SilenceNonErrorReasons { get; }
/// <summary>
/// Configurable minimum order margin portfolio percentage to ignore bad orders, orders with unrealistic small sizes
/// </summary>
/// <remarks>Default value is 0. This setting is useful to avoid small trading noise when using SetHoldings</remarks>
public decimal MinimumOrderMarginPortfolioPercentage { get; }
/// <summary>
/// Initializes a new instance of the <see cref="GetMaximumLotsForTargetBuyingPowerParameters"/> class
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="positionGroup">The position group</param>
/// <param name="targetBuyingPower">The target buying power</param>
/// <param name="minimumOrderMarginPortfolioPercentage">Configurable minimum order margin portfolio percentage to ignore orders with unrealistic small sizes</param>
/// <param name="silenceNonErrorReasons">True will not return <see cref="GetMaximumLotsResult.Reason"/>
/// set for non error situation, this is for performance</param>
public GetMaximumLotsForTargetBuyingPowerParameters(
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup,
decimal targetBuyingPower,
decimal minimumOrderMarginPortfolioPercentage,
bool silenceNonErrorReasons = false
)
{
Portfolio = portfolio;
PositionGroup = positionGroup;
TargetBuyingPower = targetBuyingPower;
SilenceNonErrorReasons = silenceNonErrorReasons;
MinimumOrderMarginPortfolioPercentage = minimumOrderMarginPortfolioPercentage;
}
/// <summary>
/// Creates a new <see cref="GetMaximumLotsResult"/> with zero quantity and an error message.
/// </summary>
public GetMaximumLotsResult Error(string reason)
{
return new GetMaximumLotsResult(0, reason, true);
}
/// <summary>
/// Creates a new <see cref="GetMaximumLotsResult"/> with zero quantity and no message.
/// </summary>
public GetMaximumLotsResult Zero()
{
return new GetMaximumLotsResult(0, string.Empty, false);
}
/// <summary>
/// Creates a new <see cref="GetMaximumLotsResult"/> with zero quantity and an info message.
/// </summary>
public GetMaximumLotsResult Zero(string reason)
{
return new GetMaximumLotsResult(0, reason, false);
}
/// <summary>
/// Creates a new <see cref="GetMaximumLotsResult"/> for the specified quantity and no message.
/// </summary>
public GetMaximumLotsResult Result(decimal quantity)
{
return new GetMaximumLotsResult(quantity, string.Empty, false);
}
}
}
@@ -0,0 +1,65 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Result type for <see cref="IPositionGroupBuyingPowerModel.GetMaximumLotsForDeltaBuyingPower"/>
/// and <see cref="IPositionGroupBuyingPowerModel.GetMaximumLotsForTargetBuyingPower"/>
/// </summary>
public class GetMaximumLotsResult
{
/// <summary>
/// Returns the maximum number of lots of the position group that can be
/// ordered. This is a whole number and is the <see cref="IPositionGroup.Quantity"/>
/// </summary>
public decimal NumberOfLots { get; }
/// <summary>
/// Returns the reason for which the maximum order quantity is zero
/// </summary>
public string Reason { get; }
/// <summary>
/// Returns true if the zero order quantity is an error condition and will be shown to the user.
/// </summary>
public bool IsError { get; }
/// <summary>
/// Initializes a new instance of the <see cref="GetMaximumOrderQuantityResult"/> class
/// </summary>
/// <param name="numberOfLots">Returns the maximum number of lots of the position group that can be ordered</param>
/// <param name="reason">The reason for which the maximum order quantity is zero</param>
public GetMaximumLotsResult(decimal numberOfLots, string reason = null)
{
NumberOfLots = numberOfLots;
Reason = reason ?? string.Empty;
IsError = !string.IsNullOrEmpty(Reason);
}
/// <summary>
/// Initializes a new instance of the <see cref="GetMaximumOrderQuantityResult"/> class
/// </summary>
/// <param name="numberOfLots">Returns the maximum number of lots of the position group that can be ordered</param>
/// <param name="reason">The reason for which the maximum order quantity is zero</param>
/// <param name="isError">True if the zero order quantity is an error condition</param>
public GetMaximumLotsResult(decimal numberOfLots, string reason, bool isError = true)
{
IsError = isError;
NumberOfLots = numberOfLots;
Reason = reason ?? string.Empty;
}
}
}
@@ -0,0 +1,95 @@
/*
* 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 System.Collections.Generic;
using System.Linq;
using QuantConnect.Orders;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines the parameters for <see cref="IPositionGroupBuyingPowerModel.HasSufficientBuyingPowerForOrder"/>
/// </summary>
public class HasSufficientPositionGroupBuyingPowerForOrderParameters
{
/// <summary>
/// The orders associated with this request
/// </summary>
public List<Order> Orders { get; }
/// <summary>
/// Gets the position group representing the holdings changes contemplated by the order
/// </summary>
public IPositionGroup PositionGroup { get; }
/// <summary>
/// Gets the algorithm's portfolio manager
/// </summary>
public SecurityPortfolioManager Portfolio { get; }
/// <summary>
/// Initializes a new instance of the <see cref="HasSufficientPositionGroupBuyingPowerForOrderParameters"/> class
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="positionGroup">The position group</param>
/// <param name="orders">The orders</param>
public HasSufficientPositionGroupBuyingPowerForOrderParameters(
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup,
List<Order> orders
)
{
Orders = orders;
Portfolio = portfolio;
PositionGroup = positionGroup;
}
/// <summary>
/// This may be called for non-combo type orders where the position group is guaranteed to have exactly one position
/// </summary>
public static implicit operator HasSufficientBuyingPowerForOrderParameters(
HasSufficientPositionGroupBuyingPowerForOrderParameters parameters
)
{
var position = parameters.PositionGroup.Single();
var security = parameters.Portfolio.Securities[position.Symbol];
return new HasSufficientBuyingPowerForOrderParameters(parameters.Portfolio, security, parameters.Orders.Single());
}
/// <summary>
/// Creates a new result indicating that there is sufficient buying power for the contemplated order
/// </summary>
public HasSufficientBuyingPowerForOrderResult Sufficient()
{
return new HasSufficientBuyingPowerForOrderResult(true);
}
/// <summary>
/// Creates a new result indicating that there is insufficient buying power for the contemplated order
/// </summary>
public HasSufficientBuyingPowerForOrderResult Insufficient(string reason)
{
return new HasSufficientBuyingPowerForOrderResult(false, reason);
}
/// <summary>
/// Creates a new result indicating that there was an error
/// </summary>
public HasSufficientBuyingPowerForOrderResult Error(string reason)
{
return new HasSufficientBuyingPowerForOrderResult(false, reason);
}
}
}
+39
View File
@@ -0,0 +1,39 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines a position for inclusion in a group
/// </summary>
public interface IPosition
{
/// <summary>
/// The symbol
/// </summary>
Symbol Symbol { get; }
/// <summary>
/// The quantity
/// </summary>
decimal Quantity { get; }
/// <summary>
/// The unit quantity. The unit quantities of a group define the group. For example, a covered
/// call has 100 units of stock and -1 units of call contracts.
/// </summary>
decimal UnitQuantity { get; }
}
}
@@ -0,0 +1,53 @@
/*
* 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 System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines a group of positions allowing for more efficient use of portfolio margin
/// </summary>
public interface IPositionGroup : IReadOnlyCollection<IPosition>
{
/// <summary>
/// Gets the key identifying this group
/// </summary>
PositionGroupKey Key { get; }
/// <summary>
/// Gets the whole number of units in this position group
/// </summary>
decimal Quantity { get; }
/// <summary>
/// Gets the positions in this group
/// </summary>
IEnumerable<IPosition> Positions { get; }
/// <summary>
/// Gets the buying power model defining how margin works in this group
/// </summary>
IPositionGroupBuyingPowerModel BuyingPowerModel { get; }
/// <summary>
/// Attempts to retrieve the position with the specified symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="position">The position, if found</param>
/// <returns>True if the position was found, otherwise false</returns>
bool TryGetPosition(Symbol symbol, out IPosition position);
}
}
@@ -0,0 +1,103 @@
/*
* 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 System;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Represents a position group's model of buying power
/// </summary>
public interface IPositionGroupBuyingPowerModel : IEquatable<IPositionGroupBuyingPowerModel>
{
/// <summary>
/// Gets the margin currently allocated to the specified holding
/// </summary>
/// <param name="parameters">An object containing the security</param>
/// <returns>The maintenance margin required for the </returns>
MaintenanceMargin GetMaintenanceMargin(PositionGroupMaintenanceMarginParameters parameters);
/// <summary>
/// The margin that must be held in order to increase the position by the provided quantity
/// </summary>
/// <param name="parameters">An object containing the security and quantity</param>
InitialMargin GetInitialMarginRequirement(PositionGroupInitialMarginParameters parameters);
/// <summary>
/// Gets the total margin required to execute the specified order in units of the account currency including fees
/// </summary>
/// <param name="parameters">An object containing the portfolio, the security and the order</param>
/// <returns>The total margin in terms of the currency quoted in the order</returns>
InitialMargin GetInitialMarginRequiredForOrder(PositionGroupInitialMarginForOrderParameters parameters);
/// <summary>
/// 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.
/// </summary>
/// <param name="parameters">An object containing the portfolio and a position group containing the contemplated
/// changes to the portfolio</param>
/// <returns>Returns the portfolio's total portfolio value and margin used before and after the position changes are applied</returns>
ReservedBuyingPowerImpact GetReservedBuyingPowerImpact(
ReservedBuyingPowerImpactParameters parameters
);
/// <summary>
/// Check if there is sufficient buying power for the position group to execute this order.
/// </summary>
/// <param name="parameters">An object containing the portfolio, the position group and the order</param>
/// <returns>Returns buying power information for an order against a position group</returns>
HasSufficientBuyingPowerForOrderResult HasSufficientBuyingPowerForOrder(
HasSufficientPositionGroupBuyingPowerForOrderParameters parameters
);
/// <summary>
/// Computes the amount of buying power reserved by the provided position group
/// </summary>
ReservedBuyingPowerForPositionGroup GetReservedBuyingPowerForPositionGroup(
ReservedBuyingPowerForPositionGroupParameters parameters
);
/// <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>
GetMaximumLotsResult GetMaximumLotsForTargetBuyingPower(
GetMaximumLotsForTargetBuyingPowerParameters parameters
);
/// <summary>
/// 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.
/// </summary>
/// <param name="parameters">An object containing the portfolio, the position group and the delta buying power</param>
/// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns>
/// <remarks>Used by the margin call model to reduce the position by a delta percent.</remarks>
GetMaximumLotsResult GetMaximumLotsForDeltaBuyingPower(
GetMaximumLotsForDeltaBuyingPowerParameters parameters
);
/// <summary>
/// Gets the buying power available for a position group trade
/// </summary>
/// <param name="parameters">A parameters object containing the algorithm's portfolio, security, and order direction</param>
/// <returns>The buying power available for the trade</returns>
PositionGroupBuyingPower GetPositionGroupBuyingPower(PositionGroupBuyingPowerParameters parameters);
}
}
@@ -0,0 +1,59 @@
/*
* 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 System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Resolves position groups from a collection of positions.
/// </summary>
public interface IPositionGroupResolver
{
/// <summary>
/// Attempts to group the specified positions into a new <see cref="IPositionGroup"/> using an
/// appropriate <see cref="IPositionGroupBuyingPowerModel"/> for position groups created via this
/// resolver.
/// </summary>
/// <param name="newPositions">The positions to be grouped</param>
/// <param name="currentPositions">The currently grouped positions</param>
/// <param name="group">The grouped positions when this resolver is able to, otherwise null</param>
/// <returns>True if this resolver can group the specified positions, otherwise false</returns>
bool TryGroup(IReadOnlyCollection<IPosition> newPositions, PositionGroupCollection currentPositions, out IPositionGroup group);
/// <summary>
/// Resolves the position groups that exist within the specified collection of positions.
/// </summary>
/// <param name="positions">The collection of positions</param>
/// <returns>An enumerable of position groups</returns>
PositionGroupCollection Resolve(PositionCollection positions);
/// <summary>
/// Determines the position groups that would be evaluated for grouping of the specified
/// positions were passed into the <see cref="Resolve"/> method.
/// </summary>
/// <remarks>
/// This function allows us to determine a set of impacted groups and run the resolver on just
/// those groups in order to support what-if analysis
/// </remarks>
/// <param name="groups">The existing position groups</param>
/// <param name="positions">The positions being changed</param>
/// <returns>An enumerable containing the position groups that could be impacted by the specified position changes</returns>
IEnumerable<IPositionGroup> GetImpactedGroups(
PositionGroupCollection groups,
IReadOnlyCollection<IPosition> positions
);
}
}
@@ -0,0 +1,33 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Responsible for managing the resolution of position groups for an algorithm.
/// Will only resolve single position groups
/// </summary>
public class NullSecurityPositionGroupModel : SecurityPositionGroupModel
{
/// <summary>
/// Get the position group resolver instance to use
/// </summary>
/// <returns>The position group resolver instance</returns>
protected override IPositionGroupResolver GetPositionGroupResolver()
{
return new CompositePositionGroupResolver(new SecurityPositionGroupResolver(PositionGroupBuyingPowerModel));
}
}
}
@@ -0,0 +1,230 @@
/*
* 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 System;
using System.Linq;
using QuantConnect.Util;
using QuantConnect.Orders;
using System.Collections.Generic;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Option.StrategyMatcher;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Class in charge of resolving option strategy groups which will use the <see cref="OptionStrategyPositionGroupBuyingPowerModel"/>
/// </summary>
public class OptionStrategyPositionGroupResolver : IPositionGroupResolver
{
private readonly SecurityManager _securities;
private readonly OptionStrategyMatcher _strategyMatcher;
/// <summary>
/// Creates the default option strategy group resolver for <see cref="OptionStrategyDefinitions.AllDefinitions"/>
/// </summary>
public OptionStrategyPositionGroupResolver(SecurityManager securities)
: this(securities, OptionStrategyMatcherOptions.ForDefinitions(OptionStrategyDefinitions.AllDefinitions))
{
}
/// <summary>
/// Creates a custom option strategy group resolver
/// </summary>
/// <param name="strategyMatcherOptions">The option strategy matcher options instance to use</param>
/// <param name="securities">The algorithms securities</param>
public OptionStrategyPositionGroupResolver(SecurityManager securities, OptionStrategyMatcherOptions strategyMatcherOptions)
{
_securities = securities;
_strategyMatcher = new OptionStrategyMatcher(strategyMatcherOptions);
}
/// <summary>
/// Attempts to group the specified positions into a new <see cref="IPositionGroup"/> using an
/// appropriate <see cref="IPositionGroupBuyingPowerModel"/> for position groups created via this
/// resolver.
/// </summary>
/// <param name="newPositions">The positions to be grouped</param>
/// <param name="currentPositions">The currently grouped positions</param>
/// <param name="group">The grouped positions when this resolver is able to, otherwise null</param>
/// <returns>True if this resolver can group the specified positions, otherwise false</returns>
public bool TryGroup(IReadOnlyCollection<IPosition> newPositions, PositionGroupCollection currentPositions, out IPositionGroup @group)
{
IEnumerable<IPosition> positions;
if (currentPositions.Count > 0)
{
var impactedGroups = GetImpactedGroups(currentPositions, newPositions);
var positionsToConsiderInNewGroup = impactedGroups.SelectMany(positionGroup => positionGroup.Positions);
positions = newPositions.Concat(positionsToConsiderInNewGroup);
}
else
{
if (newPositions.Count == 1)
{
// there's no existing position and there's only a single position, no strategy will match
@group = null;
return false;
}
positions = newPositions;
}
@group = GetPositionGroups(positions)
.Select(positionGroup =>
{
if (positionGroup.Count == 0)
{
return positionGroup;
}
if (newPositions.Any(position => positionGroup.TryGetPosition(position.Symbol, out position)))
{
return positionGroup;
}
// When none of the new positions are contained in the position group,
// it means that we are liquidating the assets in the new positions
// but some other existing positions were considered as impacted groups.
// Example:
// Buy(OptionStrategies.BullCallSpread(...), 1);
// Buy(OptionStrategies.BearPutSpread(...), 1);
// ...
// Sell(OptionStrategies.BullCallSpread(...), 1);
// Sell(OptionStrategies.BearPutSpread(...), 1);
// -----
// When attempting revert the bull call position group, the bear put group
// will be selected as impacted group, so the group will contain the put positions
// but not the call ones. In this case, we return an valid empty group because the
// liquidation is happening.
return PositionGroup.Empty(new OptionStrategyPositionGroupBuyingPowerModel(null));
})
.Where(positionGroup => positionGroup != null)
.FirstOrDefault();
return @group != null;
}
/// <summary>
/// Resolves the position groups that exist within the specified collection of positions.
/// </summary>
/// <param name="positions">The collection of positions</param>
/// <returns>An enumerable of position groups</returns>
public PositionGroupCollection Resolve(PositionCollection positions)
{
var result = PositionGroupCollection.Empty;
var groups = GetPositionGroups(positions).ToList();
if (groups.Count != 0)
{
result = new PositionGroupCollection(groups);
// we are expected to remove any positions which we resolved into a position group
positions.Remove(result);
}
return result;
}
/// <summary>
/// Determines the position groups that would be evaluated for grouping of the specified
/// positions were passed into the <see cref="Resolve"/> method.
/// </summary>
/// <remarks>
/// This function allows us to determine a set of impacted groups and run the resolver on just
/// those groups in order to support what-if analysis
/// </remarks>
/// <param name="groups">The existing position groups</param>
/// <param name="positions">The positions being changed</param>
/// <returns>An enumerable containing the position groups that could be impacted by the specified position changes</returns>
public IEnumerable<IPositionGroup> GetImpactedGroups(PositionGroupCollection groups, IReadOnlyCollection<IPosition> positions)
{
if(groups.Count == 0)
{
// there's no existing groups, nothing to impact
return Enumerable.Empty<IPositionGroup>();
}
var symbolsSet = positions.Where(position => position.Symbol.SecurityType.HasOptions() || position.Symbol.SecurityType.IsOption())
.SelectMany(position =>
{
return position.Symbol.HasUnderlying ? new[] { position.Symbol, position.Symbol.Underlying } : new[] { position.Symbol };
})
.ToHashSet();
if (symbolsSet.Count == 0)
{
return Enumerable.Empty<IPositionGroup>();
}
// will select groups for which we actually hold some security quantity and any of the changed symbols or underlying are in it if they are options
return groups.Where(group => group.Quantity != 0
&& group.Positions.Any(position1 => symbolsSet.Contains(position1.Symbol)
|| position1.Symbol.HasUnderlying && position1.Symbol.SecurityType.IsOption() && symbolsSet.Contains(position1.Symbol.Underlying)));
}
private IEnumerable<IPositionGroup> GetPositionGroups(IEnumerable<IPosition> positions)
{
foreach (var positionsByUnderlying in positions
.Where(position => position.Symbol.SecurityType.HasOptions() || position.Symbol.SecurityType.IsOption())
.GroupBy(position => position.Symbol.HasUnderlying? position.Symbol.Underlying : position.Symbol)
.Select(x => x.ToList()))
{
var optionPosition = positionsByUnderlying.FirstOrDefault(position => position.Symbol.SecurityType.IsOption());
if (optionPosition == null)
{
// if there isn't any option position we aren't really interested, can't create any option strategy!
continue;
}
var contractMultiplier = (_securities[optionPosition.Symbol].SymbolProperties as OptionSymbolProperties)?.ContractUnitOfTrade ?? 100;
var optionPositionCollection = OptionPositionCollection.FromPositions(positionsByUnderlying, contractMultiplier);
if (optionPositionCollection.Count == 0 && positionsByUnderlying.Count > 0)
{
// we could be liquidating there will be no position left!
yield return PositionGroup.Empty(new OptionStrategyPositionGroupBuyingPowerModel(null));
yield break;
}
var matches = _strategyMatcher.MatchOnce(optionPositionCollection);
if (matches.Strategies.Count == 0)
{
continue;
}
foreach (var matchedStrategy in matches.Strategies)
{
var groupQuantity = Math.Abs(matchedStrategy.OptionLegs.Cast<Leg>().Concat(matchedStrategy.UnderlyingLegs)
.Select(leg => leg.Quantity)
.GreatestCommonDivisor());
var positionsToGroup = matchedStrategy.OptionLegs
.Select(optionLeg => (IPosition)new Position(optionLeg.Symbol, optionLeg.Quantity,
// The unit quantity of each position is the ratio of the quantity of the leg to the group quantity.
// e.g. a butterfly call strategy three legs: 10:-20:10, the unit quantity of each leg is 1:2:1
Math.Abs(optionLeg.Quantity) / groupQuantity))
.Concat(matchedStrategy.UnderlyingLegs.Select(underlyingLeg => new Position(underlyingLeg.Symbol,
underlyingLeg.Quantity * contractMultiplier,
// Same as for the option legs, but we need to multiply by the contract multiplier.
// e.g. a covered call strategy has 100 shares of the underlying, per shorted contract
(Math.Abs(underlyingLeg.Quantity) * contractMultiplier / groupQuantity))))
.ToDictionary(position => position.Symbol);
yield return new PositionGroup(
new PositionGroupKey(new OptionStrategyPositionGroupBuyingPowerModel(matchedStrategy), positionsToGroup.Values),
groupQuantity,
positionsToGroup);
}
}
}
}
}
@@ -0,0 +1,171 @@
/*
* 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 System;
using System.Linq;
using System.Drawing;
using QuantConnect.Interfaces;
using System.Collections.Generic;
using QuantConnect.Data.Auxiliary;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Helper method to sample portfolio margin chart
/// </summary>
public static class PortfolioMarginChart
{
private static string PortfolioMarginTooltip = "{SERIES_NAME}: {VALUE}%";
private static string PortfolioMarginIndexName = "Margin Used (%)";
private static readonly int _portfolioMarginSeriesCount = Configuration.Config.GetInt("portfolio-margin-series-count", 40);
/// <summary>
/// Helper method to add the portfolio margin series into the given chart
/// </summary>
public static void AddSample(Chart portfolioChart, PortfolioState portfolioState, IMapFileProvider mapFileProvider, DateTime currentTime)
{
if (portfolioState == null || portfolioState.PositionGroups == null)
{
return;
}
var topSeries = new HashSet<string>(_portfolioMarginSeriesCount);
foreach (var positionGroup in portfolioState.PositionGroups
.OrderByDescending(x => x.PortfolioValuePercentage)
.DistinctBy(x => GetPositionGroupName(x, mapFileProvider, currentTime))
.Take(_portfolioMarginSeriesCount))
{
topSeries.Add(positionGroup.Name);
}
Series others = null;
ChartPoint currentOthers = null;
foreach (var positionGroup in portfolioState.PositionGroups)
{
var name = GetPositionGroupName(positionGroup, mapFileProvider, currentTime);
if (topSeries.Contains(name))
{
var series = GetOrAddSeries(portfolioChart, name, Color.Empty);
series.AddPoint(new ChartPoint(portfolioState.Time, positionGroup.PortfolioValuePercentage * 100));
continue;
}
others ??= GetOrAddSeries(portfolioChart, "OTHERS", Color.Gray);
var value = positionGroup.PortfolioValuePercentage * 100;
if (currentOthers != null && currentOthers.Time == portfolioState.Time)
{
// we aggregate
currentOthers.y += value;
}
else
{
currentOthers = new ChartPoint(portfolioState.Time, value);
others.AddPoint(currentOthers);
}
}
foreach (var series in portfolioChart.Series.Values)
{
// let's add a null point for the series which have no value for this time
var lastPoint = series.Values.LastOrDefault() as ChartPoint;
if (lastPoint == null || lastPoint.Time != portfolioState.Time && lastPoint.Y.HasValue)
{
series.AddPoint(new ChartPoint(portfolioState.Time, null));
}
}
}
private static string GetPositionGroupName(PositionGroupState positionGroup, IMapFileProvider mapFileProvider, DateTime currentTime)
{
if (positionGroup.Positions.Count == 0)
{
return string.Empty;
}
if (string.IsNullOrEmpty(positionGroup.Name))
{
positionGroup.Name = string.Join(", ", positionGroup.Positions.Select(x =>
{
if (mapFileProvider == null)
{
return x.Symbol.Value;
}
return GetMappedSymbol(mapFileProvider, x.Symbol, currentTime).Value;
}));
}
return positionGroup.Name;
}
private static Series GetOrAddSeries(Chart portfolioChart, string seriesName, Color color)
{
if (!portfolioChart.Series.TryGetValue(seriesName, out var series))
{
series = portfolioChart.Series[seriesName] = new Series(seriesName, SeriesType.Bar, 0, "%")
{
Color = color,
Tooltip = PortfolioMarginTooltip,
IndexName = PortfolioMarginIndexName,
ScatterMarkerSymbol = ScatterMarkerSymbol.None
};
}
return (Series)series;
}
private static Symbol GetMappedSymbol(IMapFileProvider mapFileProvider, Symbol symbol, DateTime referenceTime)
{
if (symbol.RequiresMapping())
{
var mapFileResolver = mapFileProvider.Get(AuxiliaryDataKey.Create(symbol));
if (mapFileResolver.Any())
{
var mapFile = mapFileResolver.ResolveMapFile(symbol);
if (mapFile.Any())
{
symbol = symbol.UpdateMappedSymbol(mapFile.GetMappedSymbol(referenceTime.Date, symbol.Value));
}
}
}
return symbol;
}
/// <summary>
/// Helper method to set the tooltip values after we've sampled and filter series with a single value
/// </summary>
public static void RemoveSinglePointSeries(Chart portfolioChart)
{
// let's remove series which have a single value, since it's a area chart they can't be drawn
portfolioChart.Series = portfolioChart.Series.Values
.Where(x =>
{
var notNullPointsCount = 0;
foreach (var point in x.Values.OfType<ChartPoint>())
{
if (point != null && point.Y.HasValue)
{
notNullPointsCount++;
if (notNullPointsCount > 1)
{
return true;
}
}
}
return notNullPointsCount > 1;
})
.ToDictionary(x => x.Name, x => x);
}
}
}
@@ -0,0 +1,146 @@
/*
* 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 System;
using System.Linq;
using Newtonsoft.Json;
using QuantConnect.Logging;
using QuantConnect.Securities;
using System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Snapshot of an algorithms portfolio state
/// </summary>
public class PortfolioState
{
/// <summary>
/// Utc time this portfolio snapshot was taken
/// </summary>
public DateTime Time { get; set; }
/// <summary>
/// The current total portfolio value
/// </summary>
public decimal TotalPortfolioValue { get; set; }
/// <summary>
/// The margin used
/// </summary>
public decimal TotalMarginUsed { get; set; }
/// <summary>
/// The different positions groups
/// </summary>
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public List<PositionGroupState> PositionGroups { get; set; }
/// <summary>
/// Gets the cash book that keeps track of all currency holdings (only settled cash)
/// </summary>
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, Cash> CashBook { get; set; }
/// <summary>
/// Gets the cash book that keeps track of all currency holdings (only unsettled cash)
/// </summary>
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, Cash> UnsettledCashBook { get; set; }
/// <summary>
/// Helper method to create the portfolio state snapshot
/// </summary>
public static PortfolioState Create(SecurityPortfolioManager portfolioManager, DateTime utcNow, decimal currentPortfolioValue)
{
try
{
var totalMarginUsed = 0m;
var positionGroups = new List<PositionGroupState>(portfolioManager.Positions.Groups.Count);
foreach (var group in portfolioManager.Positions.Groups)
{
var buyingPowerForPositionGroup = group.BuyingPowerModel.GetReservedBuyingPowerForPositionGroup(portfolioManager, group);
var positionGroupState = new PositionGroupState
{
MarginUsed = buyingPowerForPositionGroup,
Positions = group.Positions.ToList()
};
if (currentPortfolioValue != 0)
{
positionGroupState.PortfolioValuePercentage = (buyingPowerForPositionGroup / currentPortfolioValue).RoundToSignificantDigits(4);
}
positionGroups.Add(positionGroupState);
totalMarginUsed += buyingPowerForPositionGroup;
}
var result = new PortfolioState
{
Time = utcNow,
TotalPortfolioValue = currentPortfolioValue,
TotalMarginUsed = totalMarginUsed,
CashBook = portfolioManager.CashBook.Where(pair => pair.Value.Amount != 0).ToDictionary(pair => pair.Key, pair => pair.Value)
};
var unsettledCashBook = portfolioManager.UnsettledCashBook
.Where(pair => pair.Value.Amount != 0)
.ToDictionary(pair => pair.Key, pair => pair.Value);
if (positionGroups.Count > 0)
{
result.PositionGroups = positionGroups;
}
if (unsettledCashBook.Count > 0)
{
result.UnsettledCashBook = unsettledCashBook;
}
return result;
}
catch (Exception e)
{
Log.Error(e);
return null;
}
}
}
/// <summary>
/// Snapshot of a position group state
/// </summary>
public class PositionGroupState
{
/// <summary>
/// Name of this position group
/// </summary>
[JsonIgnore]
public string Name { get; set; }
/// <summary>
/// Currently margin used
/// </summary>
public decimal MarginUsed { get; set; }
/// <summary>
/// The margin used by this position in relation to the total portfolio value
/// </summary>
public decimal PortfolioValuePercentage { get; set; }
/// <summary>
/// The positions which compose this group
/// </summary>
public List<IPosition> Positions { get; set; }
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines a quantity of a security's holdings for inclusion in a position group
/// </summary>
public class Position : IPosition
{
/// <summary>
/// The symbol
/// </summary>
public Symbol Symbol { get; }
/// <summary>
/// The quantity
/// </summary>
public decimal Quantity { get; }
/// <summary>
/// The unit quantity. The unit quantities of a group define the group. For example, a covered
/// call has 100 units of stock and -1 units of call contracts.
/// </summary>
public decimal UnitQuantity { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Position"/> class
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="quantity">The quantity</param>
/// <param name="unitQuantity">The position's unit quantity within its group</param>
public Position(Symbol symbol, decimal quantity, decimal unitQuantity)
{
Symbol = symbol;
Quantity = quantity;
UnitQuantity = unitQuantity;
}
/// <summary>
/// Initializes a new instance of the <see cref="Position"/> class using the security's lot size
/// as it's unit quantity. If quantity is null, then the security's holdings quantity is used.
/// </summary>
/// <param name="security">The security</param>
/// <param name="quantity">The quantity, if null, the security's holdings quantity is used</param>
public Position(Security security, decimal? quantity = null)
: this(security.Symbol, quantity ?? security.Holdings.Quantity, security.SymbolProperties.LotSize)
{
}
/// <summary>Returns a string that represents the current object.</summary>
/// <returns>A string that represents the current object.</returns>
public override string ToString()
{
return $"{Symbol}: {Quantity}";
}
}
}
@@ -0,0 +1,120 @@
/*
* 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 System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Provides a collection type for <see cref="IPosition"/> aimed at providing indexing for
/// common operations required by the resolver implementations.
/// </summary>
public class PositionCollection : IEnumerable<IPosition>
{
private Dictionary<Symbol, IPosition> _positions;
/// <summary>Gets the number of elements in the collection.</summary>
/// <returns>The number of elements in the collection. </returns>
public int Count => _positions.Count;
/// <summary>
/// Initializes a new instance of the <see cref="PositionCollection"/> class
/// </summary>
/// <param name="positions">The positions to include in this collection</param>
public PositionCollection(Dictionary<Symbol, IPosition> positions)
{
_positions = positions;
}
/// <summary>
/// Initializes a new instance of the <see cref="PositionCollection"/> class
/// </summary>
/// <param name="positions">The positions to include in this collection</param>
public PositionCollection(IEnumerable<IPosition> positions)
: this(positions.ToDictionary(p => p.Symbol))
{
}
/// <summary>
/// Removes the quantities in the provided groups from this position collection.
/// This should be called following <see cref="IPositionGroupResolver"/> has resolved
/// position groups in order to update the collection of positions for the next resolver,
/// if one exists.
/// </summary>
/// <param name="groups">The resolved position groups</param>
/// <returns></returns>
public void Remove(IEnumerable<IPositionGroup> groups)
{
foreach (var group in groups)
{
foreach (var position in group.Positions)
{
IPosition existing;
if (!_positions.TryGetValue(position.Symbol, out existing))
{
throw new InvalidOperationException($"Position with symbol {position.Symbol} not found.");
}
var resultingPosition = existing.Deduct(position.Quantity);
// directly remove positions hows quantity is 0
if(resultingPosition.Quantity == 0)
{
_positions.Remove(position.Symbol);
}
else
{
_positions[position.Symbol] = resultingPosition;
}
}
}
}
/// <summary>
/// Clears this collection of all positions
/// </summary>
public void Clear()
{
_positions.Clear();
}
/// <summary>
/// Attempts to retrieve the position with the specified symbol from this collection
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="position">The position</param>
/// <returns>True if the position is found, otherwise false</returns>
public bool TryGetPosition(Symbol symbol, out IPosition position)
{
return _positions.TryGetValue(symbol, out position);
}
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>An enumerator that can be used to iterate through the collection.</returns>
public IEnumerator<IPosition> GetEnumerator()
{
return _positions.Values.GetEnumerator();
}
/// <summary>Returns an enumerator that iterates through a collection.</summary>
/// <returns>An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
@@ -0,0 +1,104 @@
/*
* 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 System;
using System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Provides extension methods for <see cref="IPosition"/>
/// </summary>
public static class PositionExtensions
{
/// <summary>
/// Deducts the specified <paramref name="quantityToDeduct"/> from the specified <paramref name="position"/>
/// </summary>
/// <param name="position">The source position</param>
/// <param name="quantityToDeduct">The quantity to deduct</param>
/// <returns>A new position with the same properties but quantity reduced by the specified amount</returns>
public static IPosition Deduct(this IPosition position, decimal quantityToDeduct)
{
var newQuantity = position.Quantity - quantityToDeduct;
return new Position(position.Symbol, newQuantity, position.UnitQuantity);
}
/// <summary>
/// Combines the provided positions into a single position with the quantities added and the minimum unit quantity.
/// </summary>
/// <param name="position">The position</param>
/// <param name="other">The other position to add</param>
/// <returns>The combined position</returns>
public static IPosition Combine(this IPosition position, IPosition other)
{
if (!position.Symbol.Equals(other.Symbol))
{
throw new ArgumentException($"Position symbols must match in order to combine quantities.");
}
return new Position(position.Symbol,
position.Quantity + other.Quantity,
Math.Min(position.UnitQuantity, other.UnitQuantity)
);
}
/// <summary>
/// Consolidates the provided <paramref name="positions"/> into a dictionary
/// </summary>
/// <param name="positions">The positions to be consolidated</param>
/// <returns>A dictionary containing the consolidated positions</returns>
public static Dictionary<Symbol, IPosition> Consolidate(this IEnumerable<IPosition> positions)
{
var consolidated = new Dictionary<Symbol, IPosition>();
foreach (var position in positions)
{
IPosition existing;
if (consolidated.TryGetValue(position.Symbol, out existing))
{
// if it already exists then combine it with the existing
consolidated[position.Symbol] = existing.Combine(position);
}
else
{
consolidated[position.Symbol] = position;
}
}
return consolidated;
}
/// <summary>
/// Creates a new <see cref="IPosition"/> with quantity equal to <paramref name="numberOfLots"/> times its unit quantity
/// </summary>
/// <param name="position">The position</param>
/// <param name="numberOfLots">The number of lots for the new position</param>
/// <returns>A new position with the specified number of lots</returns>
public static IPosition WithLots(this IPosition position, decimal numberOfLots)
{
var sign = position.Quantity < 0 ? -1 : +1;
return new Position(position.Symbol, numberOfLots * position.UnitQuantity * sign, position.UnitQuantity);
}
/// <summary>
/// Gets the quantity a group would have if the given position were part of it.
/// </summary>
/// <param name="position">The position</param>
/// <returns>The group quantity</returns>
public static decimal GetGroupQuantity(this IPosition position)
{
return position.Quantity / position.UnitQuantity;
}
}
}
@@ -0,0 +1,142 @@
/*
* 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 System.Linq;
using System.Collections;
using System.Collections.Generic;
using System;
using QuantConnect.Securities.Option;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Provides a default implementation of <see cref="IPositionGroup"/>
/// </summary>
public class PositionGroup : IPositionGroup
{
/// <summary>
/// Gets the number of positions in the group
/// </summary>
public int Count => _positions.Count;
/// <summary>
/// Gets the key identifying this group
/// </summary>
public PositionGroupKey Key { get; }
/// <summary>
/// Gets the whole number of units in this position group
/// </summary>
public decimal Quantity { get; }
/// <summary>
/// Gets the positions in this group
/// </summary>
public IEnumerable<IPosition> Positions => _positions.Values;
/// <summary>
/// Gets the buying power model defining how margin works in this group
/// </summary>
public IPositionGroupBuyingPowerModel BuyingPowerModel => Key.BuyingPowerModel;
private readonly Dictionary<Symbol, IPosition> _positions;
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroup"/> class
/// </summary>
/// <param name="buyingPowerModel">The buying power model to use for this group</param>
/// <param name="quantity">The group quantity, which must be the ratio of quantity to unit quantity of each position</param>
/// <param name="positions">The positions comprising this group</param>
/// <exception cref="ArgumentException">Thrown when the quantity is not the ratio of quantity to unit quantity of each position</exception>
public PositionGroup(IPositionGroupBuyingPowerModel buyingPowerModel, decimal quantity, params IPosition[] positions)
: this(new PositionGroupKey(buyingPowerModel, positions), quantity, positions.ToDictionary(p => p.Symbol))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroup"/> class
/// </summary>
/// <param name="key">The deterministic key for this group</param>
/// <param name="quantity">The group quantity, which must be the ratio of quantity to unit quantity of each position</param>
/// <param name="positions">The positions comprising this group</param>
/// <exception cref="ArgumentException">Thrown when the quantity is not the ratio of quantity to unit quantity of each position</exception>
public PositionGroup(PositionGroupKey key, decimal quantity, params IPosition[] positions)
: this(key, quantity, positions.ToDictionary(p => p.Symbol))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroup"/> class
/// </summary>
/// <param name="key">The deterministic key for this group</param>
/// <param name="quantity">The group quantity, which must be the ratio of quantity to unit quantity of each position</param>
/// <param name="positions">The positions comprising this group</param>
/// <exception cref="ArgumentException">Thrown when the quantity is not the ratio of quantity to unit quantity of each position</exception>
public PositionGroup(PositionGroupKey key, decimal quantity, Dictionary<Symbol, IPosition> positions)
{
Key = key;
Quantity = quantity;
_positions = positions;
#if DEBUG
if (positions.Any(kvp => Math.Abs(kvp.Value.Quantity / kvp.Value.UnitQuantity) != Math.Abs(Quantity)))
{
throw new ArgumentException(Messages.PositionGroup.InvalidQuantity(Quantity, positions.Values));
}
#endif
}
/// <summary>
/// Attempts to retrieve the position with the specified symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="position">The position, if found</param>
/// <returns>True if the position was found, otherwise false</returns>
public bool TryGetPosition(Symbol symbol, out IPosition position)
{
return _positions.TryGetValue(symbol, out position);
}
/// <summary>Returns a string that represents the current object.</summary>
/// <returns>A string that represents the current object.</returns>
public override string ToString()
{
return $"{Key}: {Quantity}";
}
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>An enumerator that can be used to iterate through the collection.</returns>
public IEnumerator<IPosition> GetEnumerator()
{
return Positions.GetEnumerator();
}
/// <summary>Returns an enumerator that iterates through a collection.</summary>
/// <returns>An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// Instantiates a default empty position group instance
/// </summary>
/// <param name="buyingPowerModel">The buying power model to use for this group</param>
public static PositionGroup Empty(IPositionGroupBuyingPowerModel buyingPowerModel)
{
return new PositionGroup(new PositionGroupKey(buyingPowerModel, new List<IPosition>()), 0m);
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines the result for <see cref="IPositionGroupBuyingPowerModel.GetPositionGroupBuyingPower"/>
/// </summary>
public class PositionGroupBuyingPower
{
/// <summary>
/// Gets the buying power
/// </summary>
public decimal Value { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroupBuyingPower"/> class
/// </summary>
/// <param name="buyingPower">The buying power</param>
public PositionGroupBuyingPower(decimal buyingPower)
{
Value = buyingPower;
}
/// <summary>
/// Implicit operator from decimal
/// </summary>
public static implicit operator PositionGroupBuyingPower(decimal result)
{
return new PositionGroupBuyingPower(result);
}
/// <summary>
/// Implicit operator to decimal
/// </summary>
public static implicit operator decimal(PositionGroupBuyingPower result)
{
return result.Value;
}
}
}
@@ -0,0 +1,637 @@
/*
* 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
{
/// <summary>
/// Provides a base class for implementations of <see cref="IPositionGroupBuyingPowerModel"/>
/// </summary>
public abstract class PositionGroupBuyingPowerModel : IPositionGroupBuyingPowerModel
{
/// <summary>
/// Gets the percentage of portfolio buying power to leave as a buffer
/// </summary>
protected decimal RequiredFreeBuyingPowerPercent { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroupBuyingPowerModel"/> class
/// </summary>
/// <param name="requiredFreeBuyingPowerPercent">The percentage of portfolio buying power to leave as a buffer</param>
protected PositionGroupBuyingPowerModel(decimal requiredFreeBuyingPowerPercent = 0m)
{
RequiredFreeBuyingPowerPercent = requiredFreeBuyingPowerPercent;
}
/// <summary>
/// Gets the margin currently allocated to the specified holding
/// </summary>
/// <param name="parameters">An object containing the security</param>
/// <returns>The maintenance margin required for the </returns>
public abstract MaintenanceMargin GetMaintenanceMargin(PositionGroupMaintenanceMarginParameters parameters);
/// <summary>
/// The margin that must be held in order to increase the position by the provided quantity
/// </summary>
/// <param name="parameters">An object containing the security and quantity</param>
public abstract InitialMargin GetInitialMarginRequirement(PositionGroupInitialMarginParameters parameters);
/// <summary>
/// Gets the total margin required to execute the specified order in units of the account currency including fees
/// </summary>
/// <param name="parameters">An object containing the portfolio, the security and the order</param>
/// <returns>The total margin in terms of the currency quoted in the order</returns>
public abstract InitialMargin GetInitialMarginRequiredForOrder(PositionGroupInitialMarginForOrderParameters parameters);
/// <summary>
/// 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.
/// </summary>
/// <param name="parameters">An object containing the portfolio and a position group containing the contemplated
/// changes to the portfolio</param>
/// <returns>Returns the portfolio's total portfolio value and margin used before and after the position changes are applied</returns>
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<IPositionGroup>();
// 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);
}
/// <summary>
/// Gets the initial margin required for the specified contemplated position group.
/// Used by <see cref="GetReservedBuyingPowerImpact"/> to get the contemplated groups margin.
/// </summary>
protected virtual decimal GetContemplatedGroupsInitialMargin(SecurityPortfolioManager portfolio, PositionGroupCollection contemplatedGroups,
List<IPosition> 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;
}
/// <summary>
/// Check if there is sufficient buying power for the position group to execute this order.
/// </summary>
/// <param name="parameters">An object containing the portfolio, the position group and the order</param>
/// <returns>Returns buying power information for an order against a position group</returns>
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()}"
));
}
/// <summary>
/// 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 <see cref="HasSufficientBuyingPowerForOrderResult"/> with IsSufficient=false if it
/// fails.
/// </summary>
protected virtual HasSufficientBuyingPowerForOrderResult PassesPositionGroupSpecificBuyingPowerForOrderChecks(
HasSufficientPositionGroupBuyingPowerForOrderParameters parameters,
decimal availableBuyingPower
)
{
return null;
}
/// <summary>
/// Computes the amount of buying power reserved by the provided position group
/// </summary>
public virtual ReservedBuyingPowerForPositionGroup GetReservedBuyingPowerForPositionGroup(
ReservedBuyingPowerForPositionGroupParameters parameters
)
{
return this.GetMaintenanceMargin(parameters.Portfolio, parameters.PositionGroup);
}
/// <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.
///
/// 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 &gt; 0: the order should be placed in the same direction as the reference position group to increase it,
/// without changing the positions' signs.
/// - quantity &lt; 0: the order should be placed in the opposite direction as the reference position group to reduce it,
/// using each position's opposite sign.
/// </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. 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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="parameters">An object containing the portfolio, the position group and the delta buying power</param>
/// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns>
/// <remarks>Used by the margin call model to reduce the position by a delta percent.</remarks>
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
));
}
/// <summary>
/// Gets the buying power available for a position group trade
/// </summary>
/// <param name="parameters">A parameters object containing the algorithm's portfolio, security, and order direction</param>
/// <returns>The buying power available for the trade</returns>
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;
}
/// <summary>
/// Helper function to convert a <see cref="CashAmount"/> to the account currency
/// </summary>
protected virtual decimal ToAccountCurrency(SecurityPortfolioManager portfolio, CashAmount cash)
{
return portfolio.CashBook.ConvertToAccountCurrency(cash).Amount;
}
/// <summary>
/// Helper function to compute the order fees associated with executing market orders for the specified <paramref name="positionGroup"/>
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
/// <param name="other">An object to compare with this object.</param>
/// <returns>true if the current object is equal to the <paramref name="other" /> parameter; otherwise, false.</returns>
public virtual bool Equals(IPositionGroupBuyingPowerModel other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return GetType() == other.GetType();
}
/// <summary>Determines whether the specified object is equal to the current object.</summary>
/// <param name="obj">The object to compare with the current object. </param>
/// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
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);
}
/// <summary>Serves as the default hash function. </summary>
/// <returns>A hash code for the current object.</returns>
public override int GetHashCode()
{
return GetType().GetHashCode();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="portfolio">Current portfolio</param>
/// <param name="currentPositionGroup">Current position group</param>
/// <param name="currentUsedMargin">Current margin reserved for the position</param>
/// <param name="targetFinalMargin">The target margin</param>
/// <param name="groupUnit">Unit position group corresponding to the <paramref name="currentPositionGroup"/></param>
/// <param name="unitMargin">Margin required for the <paramref name="groupUnit"/></param>
/// <param name="finalMargin">Output the final margin allocated for the position group</param>
/// <returns>The size of the order to get safely to our target</returns>
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;
}
}
}
@@ -0,0 +1,114 @@
/*
* 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 System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Provides methods aimed at reducing the noise introduced from having result/parameter types for each method.
/// These methods aim to accept raw arguments and return the desired value type directly.
/// </summary>
public static class PositionGroupBuyingPowerModelExtensions
{
/// <summary>
/// Gets the margin currently allocated to the specified position group
/// </summary>
public static decimal GetMaintenanceMargin(
this IPositionGroupBuyingPowerModel model,
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup
)
{
return model.GetMaintenanceMargin(
new PositionGroupMaintenanceMarginParameters(portfolio, positionGroup)
);
}
/// <summary>
/// The margin that must be held in order to change positions by the changes defined by the provided position group
/// </summary>
public static decimal GetInitialMarginRequirement(
this IPositionGroupBuyingPowerModel model,
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup
)
{
return model.GetInitialMarginRequirement(
new PositionGroupInitialMarginParameters(portfolio, positionGroup)
).Value;
}
/// <summary>
/// Gets the total margin required to execute the specified order in units of the account currency including fees
/// </summary>
public static decimal GetInitialMarginRequiredForOrder(
this IPositionGroupBuyingPowerModel model,
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup,
Order order
)
{
return model.GetInitialMarginRequiredForOrder(
new PositionGroupInitialMarginForOrderParameters(portfolio, positionGroup, order)
).Value;
}
/// <summary>
/// Computes the amount of buying power reserved by the provided position group
/// </summary>
public static decimal GetReservedBuyingPowerForPositionGroup(
this IPositionGroupBuyingPowerModel model,
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup
)
{
return model.GetReservedBuyingPowerForPositionGroup(
new ReservedBuyingPowerForPositionGroupParameters(portfolio, positionGroup)
).AbsoluteUsedBuyingPower;
}
/// <summary>
/// Check if there is sufficient buying power for the position group to execute this order.
/// </summary>
public static HasSufficientBuyingPowerForOrderResult HasSufficientBuyingPowerForOrder(
this IPositionGroupBuyingPowerModel model,
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup,
List<Order> orders
)
{
return model.HasSufficientBuyingPowerForOrder(new HasSufficientPositionGroupBuyingPowerForOrderParameters(
portfolio, positionGroup, orders
));
}
/// <summary>
/// Gets the buying power available for a position group trade
/// </summary>
public static PositionGroupBuyingPower GetPositionGroupBuyingPower(
this IPositionGroupBuyingPowerModel model,
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup,
OrderDirection direction
)
{
return model.GetPositionGroupBuyingPower(new PositionGroupBuyingPowerParameters(
portfolio, positionGroup, direction
));
}
}
}
@@ -0,0 +1,67 @@
/*
* 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;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines the parameters for <see cref="IPositionGroupBuyingPowerModel.GetPositionGroupBuyingPower"/>
/// </summary>
public class PositionGroupBuyingPowerParameters
{
/// <summary>
/// Gets the position group
/// </summary>
public IPositionGroup PositionGroup { get; }
/// <summary>
/// Gets the algorithm's portfolio manager
/// </summary>
public SecurityPortfolioManager Portfolio { get; }
/// <summary>
/// Gets the direction in which buying power is to be computed
/// </summary>
public OrderDirection Direction { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroupBuyingPowerParameters"/> class
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="positionGroup">The position group</param>
/// <param name="direction">The direction to compute buying power in</param>
public PositionGroupBuyingPowerParameters(
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup,
OrderDirection direction
)
{
Portfolio = portfolio;
Direction = direction;
PositionGroup = positionGroup;
}
/// <summary>
/// Implicit operator to dependent function to remove noise
/// </summary>
public static implicit operator ReservedBuyingPowerForPositionGroupParameters(
PositionGroupBuyingPowerParameters parameters
)
{
return new ReservedBuyingPowerForPositionGroupParameters(parameters.Portfolio, parameters.PositionGroup);
}
}
}
@@ -0,0 +1,214 @@
/*
* 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 System.Linq;
using System.Collections;
using System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Provides a collection type for <see cref="IPositionGroup"/>
/// </summary>
public class PositionGroupCollection : IReadOnlyCollection<IPositionGroup>
{
/// <summary>
/// Gets an empty instance of the <see cref="PositionGroupCollection"/> class
/// </summary>
public static PositionGroupCollection Empty => new(new Dictionary<PositionGroupKey, IPositionGroup>(), new Dictionary<Symbol, HashSet<IPositionGroup>>());
/// <summary>
/// Gets the number of positions in this group
/// </summary>
public int Count => _groups.Count;
/// <summary>
/// Gets whether or not this collection contains only default position groups
/// </summary>
public bool IsOnlyDefaultGroups
{
get
{
if (_hasNonDefaultGroups == null)
{
_hasNonDefaultGroups = _groups.Count == 0 || _groups.All(grp => grp.Key.IsDefaultGroup);
}
return _hasNonDefaultGroups.Value;
}
}
/// <summary>
/// Gets the position groups keys in this collection
/// </summary>
public IReadOnlyCollection<PositionGroupKey> Keys => _groups.Keys;
/// <summary>
/// Gets the position groups in this collection
/// </summary>
public IReadOnlyCollection<IPositionGroup> Values => _groups.Values;
private bool? _hasNonDefaultGroups;
private readonly Dictionary<PositionGroupKey, IPositionGroup> _groups;
private readonly Dictionary<Symbol, HashSet<IPositionGroup>> _groupsBySymbol;
internal IEnumerable<KeyValuePair<PositionGroupKey, IPositionGroup>> GetGroups() => _groups;
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroupCollection"/> class
/// </summary>
/// <param name="groups">The position groups keyed by their group key</param>
/// <param name="groupsBySymbol">The position groups keyed by the symbol of each position</param>
public PositionGroupCollection(
Dictionary<PositionGroupKey, IPositionGroup> groups,
Dictionary<Symbol, HashSet<IPositionGroup>> groupsBySymbol
)
{
_groups = groups;
_groupsBySymbol = groupsBySymbol;
}
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroupCollection"/> class
/// </summary>
/// <param name="groups">The position groups</param>
public PositionGroupCollection(IReadOnlyCollection<IPositionGroup> groups)
{
_groups = new();
_groupsBySymbol = new();
foreach (var group in groups)
{
Add(group);
}
}
/// <summary>
/// Creates a new <see cref="PositionGroupCollection"/> that contains all of the position groups
/// in this collection in addition to the specified <paramref name="group"/>. If a group with the
/// same key already exists then it is overwritten.
/// </summary>
public PositionGroupCollection Add(IPositionGroup group)
{
foreach (var position in group)
{
if (!_groupsBySymbol.TryGetValue(position.Symbol, out var groups))
{
_groupsBySymbol[position.Symbol] = groups = new();
}
groups.Add(group);
}
_groups[group.Key] = group;
return this;
}
/// <summary>
/// Determines whether or not a group with the specified key exists in this collection
/// </summary>
/// <param name="key">The group key to search for</param>
/// <returns>True if a group with the specified key was found, false otherwise</returns>
public bool Contains(PositionGroupKey key)
{
return _groups.ContainsKey(key);
}
/// <summary>
/// Gets the <see cref="IPositionGroup"/> matching the specified key. If one does not exist, then an empty
/// group is returned matching the unit quantities defined in the <paramref name="key"/>
/// </summary>
/// <param name="key">The position group key to search for</param>
/// <returns>The position group matching the specified key, or a new empty group if no matching group is found.</returns>
public IPositionGroup this[PositionGroupKey key]
{
get
{
IPositionGroup group;
if (!TryGetGroup(key, out group))
{
return new PositionGroup(key, 0m, key.CreateEmptyPositions());
}
return group;
}
}
/// <summary>
/// Attempts to retrieve the group with the specified key
/// </summary>
/// <param name="key">The group key to search for</param>
/// <param name="group">The position group</param>
/// <returns>True if group with key found, otherwise false</returns>
public bool TryGetGroup(PositionGroupKey key, out IPositionGroup group)
{
return _groups.TryGetValue(key, out group);
}
/// <summary>
/// Attempts to retrieve all groups that contain the provided symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="groups">The groups if any were found, otherwise null</param>
/// <returns>True if groups were found for the specified symbol, otherwise false</returns>
public bool TryGetGroups(Symbol symbol, out IReadOnlyCollection<IPositionGroup> groups)
{
HashSet<IPositionGroup> list;
if (_groupsBySymbol.TryGetValue(symbol, out list) && list?.Count > 0)
{
groups = list;
return true;
}
groups = null;
return false;
}
/// <summary>
/// Merges this position group collection with the provided <paramref name="other"/> collection.
/// </summary>
public PositionGroupCollection CombineWith(PositionGroupCollection other)
{
if(other.Count == 0)
{
return this;
}
if (Count == 0)
{
return other;
}
var result = this;
foreach (var positionGroup in other)
{
result = result.Add(positionGroup);
}
return result;
}
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>An enumerator that can be used to iterate through the collection.</returns>
public IEnumerator<IPositionGroup> GetEnumerator()
{
return _groups.Values.GetEnumerator();
}
/// <summary>Returns an enumerator that iterates through a collection.</summary>
/// <returns>An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
@@ -0,0 +1,149 @@
/*
* 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 System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Util;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Provides extension methods for <see cref="IPositionGroup"/>
/// </summary>
public static class PositionGroupExtensions
{
/// <summary>
/// Gets the position in the <paramref name="group"/> matching the provided <param name="symbol"></param>
/// </summary>
public static IPosition GetPosition(this IPositionGroup group, Symbol symbol)
{
IPosition position;
if (!group.TryGetPosition(symbol, out position))
{
throw new KeyNotFoundException($"No position with symbol '{symbol}' exists in the group: {group}");
}
return position;
}
/// <summary>
/// Creates a new <see cref="IPositionGroup"/> with the specified <paramref name="groupQuantity"/>.
/// If the quantity provided equals the template's quantity then the template is returned.
/// </summary>
/// <param name="template">The group template</param>
/// <param name="groupQuantity">The quantity of the new group</param>
/// <param name="positionMananger">The position manager to use to resolve positions</param>
/// <returns>A position group with the same position ratios as the template but with the specified group quantity</returns>
public static IPositionGroup WithQuantity(this IPositionGroup template, decimal groupQuantity, SecurityPositionGroupModel positionMananger)
{
var positions = template.ToArray(p => p.WithLots(groupQuantity));
// Could result in an inverse strategy that would not get resolved by using the same key
if (groupQuantity < 0)
{
return positionMananger.ResolvePositionGroups(new PositionCollection(positions)).Single();
}
return new PositionGroup(template.Key, groupQuantity, positions);
}
/// <summary>
/// Creates a new <see cref="IPositionGroup"/> with each position's quantity equaling it's unit quantity
/// </summary>
/// <param name="template">The group template</param>
/// <returns>A position group with the same position ratios as the template but with the specified group quantity</returns>
public static IPositionGroup CreateUnitGroup(this IPositionGroup template, SecurityPositionGroupModel positionMananger)
{
return template.WithQuantity(1, positionMananger);
}
/// <summary>
/// Determines whether the position group is empty
/// </summary>
/// <param name="positionGroup">The position group</param>
/// <returns>True if the position group is empty, that is, it has no positions, false otherwise</returns>
public static bool IsEmpty(this IPositionGroup positionGroup)
{
return positionGroup.Count == 0;
}
/// <summary>
/// Checks whether the provided groups are in opposite sides, that is, each of their positions are in opposite sides.
/// </summary>
/// <param name="group">The group to check</param>
/// <param name="other">The group to check against</param>
/// <returns>
/// Whether the position groups are the inverted version of each other, that is, contain the same positions each on the opposite side
/// </returns>
public static bool IsInvertedOf(this IPositionGroup group, IPositionGroup other)
{
return group.Count == other.Count
&& group.All(position => Math.Sign(position.Quantity) == -Math.Sign(other.GetPosition(position.Symbol).Quantity));
}
/// <summary>
/// Checks whether the provided groups are closing/reducing each other, that is, each of their positions are in opposite sides.
/// </summary>
/// <param name="finalGroup">The final position group that would result from a trade</param>
/// <param name="initialGroup">The initial position group before a trade</param>
/// <returns>Whether final resulting position group is a reduction of the initial one</returns>
public static bool Closes(this IPositionGroup finalGroup, IPositionGroup initialGroup)
{
// Liquidating
if (finalGroup.IsEmpty())
{
return true;
}
if (finalGroup.Count != initialGroup.Count)
{
return false;
}
// Liquidating
if (finalGroup.Quantity == 0 &&
// The initial group includes all positions being liquidated
finalGroup.All(position => initialGroup.TryGetPosition(position.Symbol, out _)))
{
return true;
}
// Each of the positions have opposite quantity signs
if (finalGroup.IsInvertedOf(initialGroup))
{
return true;
}
// The final group has a smaller quantity than the initial group
return Math.Abs(finalGroup.Quantity) < Math.Abs(initialGroup.Quantity) &&
finalGroup.All(position => Math.Sign(position.Quantity) == Math.Sign(initialGroup.GetPosition(position.Symbol).Quantity));
}
/// <summary>
/// Gets a user friendly name for the provided <paramref name="group"/>
/// </summary>
public static string GetUserFriendlyName(this IPositionGroup group)
{
if (group.Count == 1)
{
return group.Single().Symbol.ToString();
}
return string.Join("|", group.Select(p => p.Symbol.ToString()));
}
}
}
@@ -0,0 +1,57 @@
/*
* 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;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines parameters for <see cref="IPositionGroupBuyingPowerModel.GetInitialMarginRequiredForOrder"/>
/// </summary>
public class PositionGroupInitialMarginForOrderParameters
{
/// <summary>
/// Gets the algorithm's portfolio manager
/// </summary>
public SecurityPortfolioManager Portfolio { get; }
/// <summary>
/// Gets the position group
/// </summary>
public IPositionGroup PositionGroup { get; }
/// <summary>
/// Gets the order
/// </summary>
public Order Order { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroupInitialMarginForOrderParameters"/> class
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="positionGroup">The position group</param>
/// <param name="order">The order</param>
public PositionGroupInitialMarginForOrderParameters(
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup,
Order order
)
{
Portfolio = portfolio;
PositionGroup = positionGroup;
Order = order;
}
}
}
@@ -0,0 +1,47 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines parameters for <see cref="IPositionGroupBuyingPowerModel.GetInitialMarginRequirement"/>
/// </summary>
public class PositionGroupInitialMarginParameters
{
/// <summary>
/// Gets the algorithm's portfolio manager
/// </summary>
public SecurityPortfolioManager Portfolio { get; }
/// <summary>
/// Gets the position group
/// </summary>
public IPositionGroup PositionGroup { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroupInitialMarginParameters"/> class
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="positionGroup">The position group</param>
public PositionGroupInitialMarginParameters(
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup
)
{
Portfolio = portfolio;
PositionGroup = positionGroup;
}
}
}
@@ -0,0 +1,165 @@
/*
* 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 System;
using System.Linq;
using System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines a unique and deterministic key for <see cref="IPositionGroup"/>
/// </summary>
public sealed class PositionGroupKey : IEquatable<PositionGroupKey>
{
/// <summary>
/// Gets whether or not this key defines a default group
/// </summary>
public bool IsDefaultGroup { get; }
/// <summary>
/// Gets the <see cref="IPositionGroupBuyingPowerModel"/> being used by the group
/// </summary>
public IPositionGroupBuyingPowerModel BuyingPowerModel { get; }
/// <summary>
/// Gets the unit quantities defining the ratio between position quantities in the group
/// </summary>
public IReadOnlyList<Tuple<Symbol, decimal>> UnitQuantities { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroupKey"/> class for groups with a single security
/// </summary>
/// <param name="buyingPowerModel">The group's buying power model</param>
/// <param name="security">The security</param>
public PositionGroupKey(IPositionGroupBuyingPowerModel buyingPowerModel, Security security)
{
IsDefaultGroup = buyingPowerModel.GetType() == typeof(SecurityPositionGroupBuyingPowerModel);
BuyingPowerModel = buyingPowerModel;
UnitQuantities = new[]
{
Tuple.Create(security.Symbol, security.SymbolProperties.LotSize)
};
}
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroupKey"/> class
/// </summary>
/// <param name="buyingPowerModel">The group's buying power model</param>
/// <param name="positions">The positions comprising the group</param>
public PositionGroupKey(IPositionGroupBuyingPowerModel buyingPowerModel, IReadOnlyCollection<IPosition> positions)
{
BuyingPowerModel = buyingPowerModel;
if(positions.Count == 1)
{
var position = positions.First();
UnitQuantities = new[]
{
Tuple.Create(position.Symbol, position.UnitQuantity)
};
}
else
{
// these have to be sorted for determinism
UnitQuantities = positions.OrderBy(x => x.Symbol).Select(p => Tuple.Create(p.Symbol, p.UnitQuantity)).ToList();
}
IsDefaultGroup = UnitQuantities.Count == 1 && BuyingPowerModel.GetType() == typeof(SecurityPositionGroupBuyingPowerModel);
}
/// <summary>
/// Creates a new array of empty positions with unit quantities according to this key
/// </summary>
public IPosition[] CreateEmptyPositions()
{
var positions = new IPosition[UnitQuantities.Count];
for (var i = 0; i < UnitQuantities.Count; i++)
{
var unitQuantity = UnitQuantities[i];
positions[i] = new Position(unitQuantity.Item1, 0m, unitQuantity.Item2);
}
return positions;
}
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
/// <param name="other">An object to compare with this object.</param>
/// <returns>true if the current object is equal to the <paramref name="other" /> parameter; otherwise, false.</returns>
public bool Equals(PositionGroupKey other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return BuyingPowerModel.Equals(other.BuyingPowerModel)
&& UnitQuantities.ListEquals(other.UnitQuantities);
}
/// <summary>Determines whether the specified object is equal to the current object.</summary>
/// <param name="obj">The object to compare with the current object. </param>
/// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
return obj is PositionGroupKey && Equals((PositionGroupKey) obj);
}
/// <summary>Serves as the default hash function. </summary>
/// <returns>A hash code for the current object.</returns>
public override int GetHashCode()
{
unchecked
{
return (BuyingPowerModel.GetHashCode() * 397) ^ UnitQuantities.GetListHashCode();
}
}
/// <summary>Returns a string that represents the current object.</summary>
/// <returns>A string that represents the current object.</returns>
public override string ToString()
{
return $"{string.Join("|", UnitQuantities.Select(x => $"{x.Item1}:{x.Item2.Normalize()}"))}";
}
/// <summary>
/// Equals operator
/// </summary>
public static bool operator ==(PositionGroupKey left, PositionGroupKey right)
{
return Equals(left, right);
}
/// <summary>
/// Not equals operator
/// </summary>
public static bool operator !=(PositionGroupKey left, PositionGroupKey right)
{
return !Equals(left, right);
}
}
}
@@ -0,0 +1,47 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines parameters for <see cref="IPositionGroupBuyingPowerModel.GetMaintenanceMargin"/>
/// </summary>
public class PositionGroupMaintenanceMarginParameters
{
/// <summary>
/// Gets the algorithm's portfolio manager
/// </summary>
public SecurityPortfolioManager Portfolio { get; }
/// <summary>
/// Gets the position group
/// </summary>
public IPositionGroup PositionGroup { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PositionGroupMaintenanceMarginParameters"/> class
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="positionGroup">The position group</param>
public PositionGroupMaintenanceMarginParameters(
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup
)
{
Portfolio = portfolio;
PositionGroup = positionGroup;
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines the result for <see cref="IBuyingPowerModel.GetReservedBuyingPowerForPosition"/>
/// </summary>
public class ReservedBuyingPowerForPositionGroup
{
/// <summary>
/// Gets the reserved buying power
/// </summary>
public decimal AbsoluteUsedBuyingPower { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ReservedBuyingPowerForPosition"/> class
/// </summary>
/// <param name="reservedBuyingPowerForPosition">The reserved buying power for the security's holdings</param>
public ReservedBuyingPowerForPositionGroup(decimal reservedBuyingPowerForPosition)
{
AbsoluteUsedBuyingPower = reservedBuyingPowerForPosition;
}
/// <summary>
/// Implicit operator to <see cref="decimal"/> to remove noise
/// </summary>
public static implicit operator decimal(ReservedBuyingPowerForPositionGroup reservedBuyingPower)
{
return reservedBuyingPower.AbsoluteUsedBuyingPower;
}
/// <summary>
/// Implicit operator to <see cref="decimal"/> to remove noise
/// </summary>
public static implicit operator ReservedBuyingPowerForPositionGroup(decimal reservedBuyingPower)
{
return new ReservedBuyingPowerForPositionGroup(reservedBuyingPower);
}
}
}
@@ -0,0 +1,47 @@
/*
* 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.
*/
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Defines the parameters for <see cref="IBuyingPowerModel.GetReservedBuyingPowerForPosition"/>
/// </summary>
public class ReservedBuyingPowerForPositionGroupParameters
{
/// <summary>
/// Gets the <see cref="IPositionGroup"/>
/// </summary>
public IPositionGroup PositionGroup { get; }
/// <summary>
/// Gets the algorithm's portfolio manager
/// </summary>
public SecurityPortfolioManager Portfolio { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ReservedBuyingPowerForPositionGroupParameters"/> class
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="positionGroup">The position group</param>
public ReservedBuyingPowerForPositionGroupParameters(
SecurityPortfolioManager portfolio,
IPositionGroup positionGroup
)
{
Portfolio = portfolio;
PositionGroup = positionGroup;
}
}
}
@@ -0,0 +1,81 @@
/*
* 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 System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Specifies the impact on buying power from changing security holdings that affects current <see cref="IPositionGroup"/>,
/// including the current reserved buying power, without the change, and a contemplate reserved buying power, which takes
/// into account a contemplated change to the algorithm's positions that impacts current position groups.
/// </summary>
public class ReservedBuyingPowerImpact
{
/// <summary>
/// Gets the current reserved buying power for the impacted groups
/// </summary>
public decimal Current { get; }
/// <summary>
/// Gets the reserved buying power for groups resolved after applying a contemplated change to the impacted groups
/// </summary>
public decimal Contemplated { get; }
/// <summary>
/// Gets the change in reserved buying power, <see cref="Current"/> minus <see cref="Contemplated"/>
/// </summary>
public decimal Delta { get; }
/// <summary>
/// Gets the impacted groups used as the basis for these reserved buying power numbers
/// </summary>
public IReadOnlyCollection<IPositionGroup> ImpactedGroups { get; }
/// <summary>
/// Gets the position changes being contemplated
/// </summary>
public IReadOnlyCollection<IPosition> ContemplatedChanges { get; }
/// <summary>
/// Gets the newly resolved groups resulting from applying the contemplated changes to the impacted groups
/// </summary>
public IReadOnlyCollection<IPositionGroup> ContemplatedGroups { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ReservedBuyingPowerImpact"/> class
/// </summary>
/// <param name="current">The current reserved buying power for impacted groups</param>
/// <param name="contemplated">The reserved buying power for impacted groups after applying the contemplated changes</param>
/// <param name="impactedGroups">The groups impacted by the contemplated changes</param>
/// <param name="contemplatedChanges">The position changes being contemplated</param>
/// <param name="contemplatedGroups">The groups resulting from applying the contemplated changes</param>
public ReservedBuyingPowerImpact(
decimal current,
decimal contemplated,
IReadOnlyCollection<IPositionGroup> impactedGroups,
IReadOnlyCollection<IPosition> contemplatedChanges,
IReadOnlyCollection<IPositionGroup> contemplatedGroups
)
{
Current = current;
Contemplated = contemplated;
Delta = Contemplated - Current;
ImpactedGroups = impactedGroups;
ContemplatedGroups = contemplatedGroups;
ContemplatedChanges = contemplatedChanges;
}
}
}
@@ -0,0 +1,58 @@
/*
* 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 System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Parameters for the <see cref="IPositionGroupBuyingPowerModel.GetReservedBuyingPowerImpact"/>
/// </summary>
public class ReservedBuyingPowerImpactParameters
{
/// <summary>
/// Gets the position changes being contemplated
/// </summary>
public IPositionGroup ContemplatedChanges { get; }
/// <summary>
/// Gets the algorithm's portfolio manager
/// </summary>
public SecurityPortfolioManager Portfolio { get; }
/// <summary>
/// The orders associated with this request
/// </summary>
public List<Order> Orders { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ReservedBuyingPowerImpactParameters"/> class
/// </summary>
/// <param name="portfolio">The algorithm's portfolio manager</param>
/// <param name="contemplatedChanges">The position changes being contemplated</param>
/// <param name="orders">The orders associated with this request</param>
public ReservedBuyingPowerImpactParameters(
SecurityPortfolioManager portfolio,
IPositionGroup contemplatedChanges,
List<Order> orders
)
{
Orders = orders;
Portfolio = portfolio;
ContemplatedChanges = contemplatedChanges;
}
}
}
@@ -0,0 +1,191 @@
/*
* 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 System.Linq;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Provides an implementation of <see cref="IPositionGroupBuyingPowerModel"/> for groups containing exactly one security
/// </summary>
public class SecurityPositionGroupBuyingPowerModel : PositionGroupBuyingPowerModel
{
/// <summary>
/// Gets the margin currently allocated to the specified holding
/// </summary>
/// <param name="parameters">An object containing the security</param>
/// <returns>The maintenance margin required for the </returns>
public override MaintenanceMargin GetMaintenanceMargin(PositionGroupMaintenanceMarginParameters parameters)
{
// SecurityPositionGroupBuyingPowerModel models buying power the same as non-grouped, so we can simply sum up
// the reserved buying power via the security's model. We should really only ever get a single position here,
// but it's not incorrect to ask the model for what the reserved buying power would be using default modeling
var buyingPower = 0m;
foreach (var position in parameters.PositionGroup)
{
var security = parameters.Portfolio.Securities[position.Symbol];
var result = security.BuyingPowerModel.GetMaintenanceMargin(
MaintenanceMarginParameters.ForQuantityAtCurrentPrice(security, position.Quantity)
);
buyingPower += result;
}
return buyingPower;
}
/// <summary>
/// The margin that must be held in order to increase the position by the provided quantity
/// </summary>
/// <param name="parameters">An object containing the security and quantity</param>
public override InitialMargin GetInitialMarginRequirement(PositionGroupInitialMarginParameters parameters)
{
var initialMarginRequirement = 0m;
foreach (var position in parameters.PositionGroup)
{
var security = parameters.Portfolio.Securities[position.Symbol];
initialMarginRequirement += security.BuyingPowerModel.GetInitialMarginRequirement(
security, position.Quantity
);
}
return initialMarginRequirement;
}
/// <summary>
/// Gets the total margin required to execute the specified order in units of the account currency including fees
/// </summary>
/// <param name="parameters">An object containing the portfolio, the security and the order</param>
/// <returns>The total margin in terms of the currency quoted in the order</returns>
public override InitialMargin GetInitialMarginRequiredForOrder(
PositionGroupInitialMarginForOrderParameters parameters
)
{
var initialMarginRequirement = 0m;
foreach (var position in parameters.PositionGroup)
{
// TODO : Support combo order by pull symbol-specific order
var security = parameters.Portfolio.Securities[position.Symbol];
initialMarginRequirement += security.BuyingPowerModel.GetInitialMarginRequiredForOrder(
new InitialMarginRequiredForOrderParameters(parameters.Portfolio.CashBook, security, parameters.Order)
);
}
return initialMarginRequirement;
}
/// <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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="parameters">An object containing the portfolio, the position group and the delta buying power</param>
/// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns>
/// <remarks>Used by the margin call model to reduce the position by a delta percent.</remarks>
public override GetMaximumLotsResult GetMaximumLotsForDeltaBuyingPower(
GetMaximumLotsForDeltaBuyingPowerParameters 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.GetMaximumOrderQuantityForDeltaBuyingPower(
new GetMaximumOrderQuantityForDeltaBuyingPowerParameters(
parameters.Portfolio, security, parameters.DeltaBuyingPower, parameters.MinimumOrderMarginPortfolioPercentage
)
);
var quantity = result.Quantity / security.SymbolProperties.LotSize;
return new GetMaximumLotsResult(quantity, result.Reason, result.IsError);
}
/// <summary>
/// Check if there is sufficient buying power for the position group to execute this order.
/// </summary>
/// <param name="parameters">An object containing the portfolio, the position group and the order</param>
/// <returns>Returns buying power information for an order against a position group</returns>
public override HasSufficientBuyingPowerForOrderResult HasSufficientBuyingPowerForOrder(
HasSufficientPositionGroupBuyingPowerForOrderParameters 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];
return security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(
parameters.Portfolio, security, parameters.Orders.Single()
);
}
/// <summary>
/// Additionally check initial margin requirements if the algorithm only has default position groups
/// </summary>
protected override HasSufficientBuyingPowerForOrderResult PassesPositionGroupSpecificBuyingPowerForOrderChecks(
HasSufficientPositionGroupBuyingPowerForOrderParameters parameters,
decimal availableBuyingPower
)
{
// only check initial margin requirements when the algorithm is only using default position groups
if (!parameters.Portfolio.Positions.IsOnlyDefaultGroups)
{
return null;
}
var symbol = parameters.PositionGroup.Single().Symbol;
var security = parameters.Portfolio.Securities[symbol];
return security.BuyingPowerModel.HasSufficientBuyingPowerForOrder(
parameters.Portfolio, security, parameters.Orders.Single()
);
}
}
}
@@ -0,0 +1,252 @@
/*
* 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 System;
using System.Linq;
using QuantConnect.Orders;
using System.Collections.Generic;
using System.Collections.Specialized;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Responsible for managing the resolution of position groups for an algorithm
/// </summary>
public class SecurityPositionGroupModel : ExtendedDictionary<PositionGroupKey, IPositionGroup>
{
/// <summary>
/// Gets an implementation of <see cref="SecurityPositionGroupModel"/> that will not group multiple securities
/// </summary>
public static readonly SecurityPositionGroupModel Null = new NullSecurityPositionGroupModel();
private bool _requiresGroupResolution;
private SecurityManager _securities;
private PositionGroupCollection _groups;
private IPositionGroupResolver _resolver;
/// <summary>
/// Get's the single security position group buying power model to use
/// </summary>
protected virtual IPositionGroupBuyingPowerModel PositionGroupBuyingPowerModel { get; } = new SecurityPositionGroupBuyingPowerModel();
/// <summary>
/// Gets the set of currently resolved position groups
/// </summary>
public PositionGroupCollection Groups
{
get
{
ResolvePositionGroups();
return _groups;
}
private set
{
_groups = value;
}
}
/// <summary>
/// Gets whether or not the algorithm is using only default position groups
/// </summary>
public bool IsOnlyDefaultGroups => Groups.IsOnlyDefaultGroups;
/// <summary>
/// Gets the number of position groups in this collection
/// </summary>
public override int Count => Groups.Count;
/// <summary>
/// Gets all the available position group keys
/// </summary>
protected override IEnumerable<PositionGroupKey> GetKeys => Groups.Keys;
/// <summary>
/// Gets all the available position groups
/// </summary>
protected override IEnumerable<IPositionGroup> GetValues => Groups.Values;
/// <summary>
/// Gets all the items in the dictionary
/// </summary>
/// <returns>All the items in the dictionary</returns>
public override IEnumerable<KeyValuePair<PositionGroupKey, IPositionGroup>> GetItems() => Groups.GetGroups();
/// <summary>
/// Initializes a new instance of the <see cref="SecurityPositionGroupModel"/> class
/// </summary>
/// <param name="securities">The algorithm's security manager</param>
public virtual void Initialize(SecurityManager securities)
{
_securities = securities;
Groups = PositionGroupCollection.Empty;
_resolver = GetPositionGroupResolver();
foreach (var security in _securities.Values)
{
// if any security already present let's wire the holdings change event
security.Holdings.QuantityChanged += HoldingsOnQuantityChanged;
}
// we must be notified each time our holdings change, so each time a security is added, we
// want to bind to its SecurityHolding.QuantityChanged event so we can trigger the resolver
securities.CollectionChanged += (sender, args) =>
{
var items = args.NewItems ?? new List<object>();
if (args.OldItems != null)
{
foreach (var item in args.OldItems)
{
items.Add(item);
}
}
foreach (Security security in items)
{
if (args.Action == NotifyCollectionChangedAction.Add)
{
security.Holdings.QuantityChanged += HoldingsOnQuantityChanged;
if (security.Invested)
{
// if this security has holdings then we'll need to resolve position groups
_requiresGroupResolution = true;
}
}
else if (args.Action == NotifyCollectionChangedAction.Remove)
{
security.Holdings.QuantityChanged -= HoldingsOnQuantityChanged;
if (security.Invested)
{
// only trigger group resolution if we had holdings in the removed security
_requiresGroupResolution = true;
}
}
}
};
}
/// <summary>
/// Gets the <see cref="IPositionGroup"/> matching the specified <paramref name="key"/>. If one is not found,
/// then a new empty position group is returned.
/// </summary>
public override IPositionGroup this[PositionGroupKey key]
{
get => Groups[key];
set => throw new NotImplementedException("Read-only collection. Cannot set value.");
}
/// <summary>
/// Creates a position group for the specified order, pulling
/// </summary>
/// <param name="orders">The order</param>
/// <param name="group">The resulting position group</param>
/// <returns>A new position group matching the provided order</returns>
public bool TryCreatePositionGroup(List<Order> orders, out IPositionGroup group)
{
var newPositions = orders.Select(order => order.CreatePositions(_securities)).SelectMany(x => x).ToList();
// We send new and current positions to try resolve any strategy being executed by multiple orders
// else the PositionGroup we will get out here will just be the default in those cases
if (!_resolver.TryGroup(newPositions, Groups, out group))
{
return false;
}
return true;
}
/// <summary>
/// Resolves position groups using the specified collection of positions
/// </summary>
/// <param name="positions">The positions to be grouped</param>
/// <returns>A collection of position groups containing all of the provided positions</returns>
public PositionGroupCollection ResolvePositionGroups(PositionCollection positions)
{
return _resolver.Resolve(positions);
}
/// <summary>
/// Determines which position groups could be impacted by changes in the specified positions
/// </summary>
/// <param name="positions">The positions to be changed</param>
/// <returns>All position groups that need to be re-evaluated due to changes in the positions</returns>
public IEnumerable<IPositionGroup> GetImpactedGroups(IReadOnlyCollection<IPosition> positions)
{
return _resolver.GetImpactedGroups(Groups, positions);
}
/// <summary>
/// Creates a <see cref="PositionGroupKey"/> for the security's default position group
/// </summary>
public PositionGroupKey CreateDefaultKey(Security security)
{
return new PositionGroupKey(PositionGroupBuyingPowerModel, security);
}
/// <summary>
/// Gets or creates the default position group for the specified <paramref name="security"/>
/// </summary>
/// <remarks>
/// TODO: position group used here is the default, is this what callers want?
/// </remarks>
public IPositionGroup GetOrCreateDefaultGroup(Security security)
{
var key = CreateDefaultKey(security);
return Groups[key];
}
/// <summary>
/// Get the position group resolver instance to use
/// </summary>
/// <returns>The position group resolver instance</returns>
protected virtual IPositionGroupResolver GetPositionGroupResolver()
{
return new CompositePositionGroupResolver(new OptionStrategyPositionGroupResolver(_securities), new SecurityPositionGroupResolver(PositionGroupBuyingPowerModel));
}
private void HoldingsOnQuantityChanged(object sender, SecurityHoldingQuantityChangedEventArgs e)
{
_requiresGroupResolution = true;
}
/// <summary>
/// Resolves the algorithm's position groups from all of its holdings
/// </summary>
private void ResolvePositionGroups()
{
if (_requiresGroupResolution)
{
_requiresGroupResolution = false;
// TODO : Replace w/ special IPosition impl to always equal security.Quantity and we'll
// use them explicitly for resolution collection so we don't do this each time
var investedPositions = _securities.Where(kvp => kvp.Value.Invested).Select(kvp => (IPosition)new Position(kvp.Value));
var positionsCollection = new PositionCollection(investedPositions);
Groups = ResolvePositionGroups(positionsCollection);
}
}
/// <summary>
/// Tries to get the position group matching the specified key
/// </summary>
/// <param name="key">The key to search for</param>
/// <param name="value">The position group matching the specified key</param>
/// <returns>True if a group with the specified key was found, false otherwise</returns>
public override bool TryGetValue(PositionGroupKey key, out IPositionGroup value)
{
return Groups.TryGetGroup(key, out value);
}
}
}
@@ -0,0 +1,111 @@
/*
* 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 System.Linq;
using System.Collections.Generic;
namespace QuantConnect.Securities.Positions
{
/// <summary>
/// Provides an implementation of <see cref="IPositionGroupResolver"/> that places all positions into a default group of one security.
/// </summary>
public class SecurityPositionGroupResolver : IPositionGroupResolver
{
private readonly IPositionGroupBuyingPowerModel _buyingPowerModel;
/// <summary>
/// Initializes a new instance of the <see cref="SecurityPositionGroupResolver"/> class
/// </summary>
/// <param name="buyingPowerModel">The buying power model to use for created groups</param>
public SecurityPositionGroupResolver(IPositionGroupBuyingPowerModel buyingPowerModel)
{
_buyingPowerModel = buyingPowerModel;
}
/// <summary>
/// Attempts to group the specified positions into a new <see cref="IPositionGroup"/> using an
/// appropriate <see cref="IPositionGroupBuyingPowerModel"/> for position groups created via this
/// resolver.
/// </summary>
/// <param name="newPositions">The positions to be grouped</param>
/// <param name="currentPositions">The currently grouped positions</param>
/// <param name="group">The grouped positions when this resolver is able to, otherwise null</param>
/// <returns>True if this resolver can group the specified positions, otherwise false</returns>
public bool TryGroup(IReadOnlyCollection<IPosition> newPositions, PositionGroupCollection currentPositions, out IPositionGroup group)
{
// we can only create default groupings containing a single security
if (newPositions.Count != 1)
{
group = null;
return false;
}
var key = new PositionGroupKey(_buyingPowerModel, newPositions);
var position = newPositions.First();
group = new PositionGroup(key, position.GetGroupQuantity(), newPositions.ToDictionary(p => p.Symbol));
return true;
}
/// <summary>
/// Resolves the position groups that exist within the specified collection of positions.
/// </summary>
/// <param name="positions">The collection of positions</param>
/// <returns>An enumerable of position groups</returns>
public PositionGroupCollection Resolve(PositionCollection positions)
{
var result = new PositionGroupCollection(positions
.Select(position => new PositionGroup(_buyingPowerModel, position.GetGroupQuantity(), position)).ToList()
);
positions.Clear();
return result;
}
/// <summary>
/// Determines the position groups that would be evaluated for grouping of the specified
/// positions were passed into the <see cref="IPositionGroupResolver.Resolve"/> method.
/// </summary>
/// <remarks>
/// This function allows us to determine a set of impacted groups and run the resolver on just
/// those groups in order to support what-if analysis
/// </remarks>
/// <param name="groups">The existing position groups</param>
/// <param name="positions">The positions being changed</param>
/// <returns>An enumerable containing the position groups that could be impacted by the specified position changes</returns>
public IEnumerable<IPositionGroup> GetImpactedGroups(
PositionGroupCollection groups,
IReadOnlyCollection<IPosition> positions
)
{
var seen = new HashSet<PositionGroupKey>();
foreach (var position in positions)
{
IReadOnlyCollection<IPositionGroup> groupsForSymbol;
if (!groups.TryGetGroups(position.Symbol, out groupsForSymbol))
{
continue;
}
foreach (var group in groupsForSymbol)
{
if (seen.Add(group.Key))
{
yield return group;
}
}
}
}
}
}
+363
View File
@@ -0,0 +1,363 @@
# Position Groups
## Motivation
The motivation behind the position groups feature is to enable algorithms to submit an order for a logical grouping of securities in a single action. In some cases, the margin required for the group is far less than the sum of the margin required for each piece individually. This happens when the grouping provides some sort of hedge, thereby reducing the overall risk of the position, and in some cases, can be a completely market neutral position. A simple example is a covered call strategy, which nominally consists of 100 shares of the underlying equity and short 1 option contract. Since the short contract position is _covered_ by the account holding the underlying, brokerages reduce the margin requirements. The end goal is to enable LEAN to not only accurately model such groupings, but also submit a multi-leg order so the brokerage can process it as a single order. There are some cases where submitting the legs individually is not possible due to margin requirements, but grouping them together allows the order to be successfully processed.
## Design
The position groups feature introduces some new abstractions and key concepts that will be covered in this section.
### IPosition
A position defines _some_ quantity of a security. It may be all of the security's holdings or only a fraction of the holdings. Each position defines a few key properties listed below:
``` csharp
// See: https://github.com/QuantConnect/Lean/blob/refactor-4065-position-groups/Common/Securities/Positions/IPosition.cs
/// <summary>
/// The symbol
/// </summary>
Symbol Symbol { get; }
/// <summary>
/// The quantity
/// </summary>
decimal Quantity { get; }
/// <summary>
/// The unit quantity. The unit quantities of a group define the group. For example, a covered
/// call has 100 units of stock and -1 units of call contracts.
/// </summary>
decimal UnitQuantity { get; }
```
The `Symbol` property is everything you expect it to be, uniquely identifying which security this position is in and the `Quantity` is likewise uninteresting, denoting the directional (position for long negative for short) quantity of the position. The `UnitQuantity` defines the smallest allowable quantity increment according to the definition of the group the position belongs to. For the default group, `SecurityPositionGroup`, the `UnitQuantity` is equal to the security's lot size (`SymbolProperties.LotSize`). An equity position in a group with option contracts will have a `UnitQuantity` equal to the contract's multiplier (`SymbolProperties.ContractMultiplier`). There's an important relationship between the `Quantity` and the `UnitQuantity` which is that `Quantity/UnitQuantity` **must** always yield a whole number and denotes the number of lots. Using the covered call example from earlier, we may have 5 covered calls. Each contract will have a `UnitQuantity` equal to -1 and the underlying equity will have a `UnitQuantity` normally equal to 100, so 5 covered calls yields -5 contracts and 500 shares in the underlying.
### PositionGroupKey
Before diving into the `IPositionGroup` abstraction, it's important to briefly mention the `PositionGroupKey`. This class uniquely defines a position group within the algorithm and is a deterministic identifier constructed from the contained positions' `Symbol` and `UnitQuantity` properties coupled with an `IPositionGroupBuyingPowerModel`. We'll discuss modelling in a later section, but for now it's enough to understand that if two position group contain the same exact position **but** are modeled differently, then LEAN will treat them as different positions. The `UnitQuantities` list is, under the covers, an `ImmutableSortedSet` which guarantees determinism. In other words, `-1 GOOG CALL; +100 GOOG` is the same as `+100 GOOG; -1 GOOG CALL`. The `PositionGroupKey` can be used to index into collection types containing position groups as well as into the `PositionManager` (to be discussed later). This class also offers a variety of convenience functions for creating empty and unit positions and groups, which is pretty cool as it implies that the `PositionGroupKey` contains sufficient information to create an entire `IPositionGroup`. It's essentially a template for a particular group type with an exact set of symbol. More on group _types_ later.
``` csharp
// See: https://github.com/QuantConnect/Lean/blob/refactor-4065-position-groups/Common/Securities/Positions/PositionGroupKey.cs
/// <summary>
/// Gets whether or not this key defines a default group
/// </summary>
public bool IsDefaultGroup { get; }
/// <summary>
/// Gets the <see cref="IPositionGroupBuyingPowerModel"/> being used by the group
/// </summary>
public IPositionGroupBuyingPowerModel BuyingPowerModel { get; }
/// <summary>
/// Gets the unit quantities defining the ratio between position quantities in the group
/// </summary>
public IReadOnlyList<Tuple<Symbol, decimal>> UnitQuantities { get; }
```
### IPositionGroup
A position group is unsurprisingly a grouping of `IPosition` instances. More importantly though, a position group contains the definition of the group, which includes the ratios between the `UnitQuantity` of its constituent positions and the `IPositionGroupBuyingPowerModel`. These definitional pieces are all contained within the `PositionGroupKey` discussed in the previous section. `IPositionGroup` implements the `IReadOnlyCollection<IPosition>` interface, which allows it to be used as an `IEnumerable<IPosition>`. The `Key` property exposes the deterministic identifier and the `Quantity` property exposes how many _units_ of the group there are. Recalling the earlier discussion in the `IPosition` section, where we showed that `Quantity/UnitQuantity` yields the number of lots; the number of lots is exactly equal to, by definition, the position group's quantity. Further, **every** position within the group **must** have the same exact number of lots, and if not, then something has gone terribly wrong! Position groups have definitions that define the ratios between the positions. We keep using the covered call example, but they can be far more complicated. Due to its simplicity, we'll continue with the covered call example, and more specifically, consider a covered call position group with a `Quantity` equal to 5. This means that the ratio of the option contract position's `Quantity/UnitQuantity` equals 5 **and** the ratio of the equity position's `Quantity/UnitQuantity` equal 5. As mentioned earlier, modeling is an important part of the position group's definition, and as such, `IPositionGroup` directly exposes its own model via the `BuyingPowerModel` property, and in this way, `IPositionGroup` is analogous to a `Security` object, in that it's the smallest unit of modelling and trading within LEAN with respect to the position group subsystems. There are some extension methods provided that we'll touch on later and only one method is exposed directly by the interface: `TryGetPosition`, which is intended to behave identically to `IDictionary<K, V>.TryGetValue`, returning `true` and a valid `position` instance _or_ `false` and `default(IPosition)` when the group doesn't contain a position with the provided symbol.
``` csharp
// See: https://github.com/QuantConnect/Lean/blob/refactor-4065-position-groups/Common/Securities/Positions/IPositionGroup.cs
/// <summary>
/// Gets the key identifying this group
/// </summary>
PositionGroupKey Key { get; }
/// <summary>
/// Gets the whole number of units in this position group
/// </summary>
decimal Quantity { get; }
/// <summary>
/// Gets the positions in this group
/// </summary>
IEnumerable<IPosition> Positions { get; }
/// <summary>
/// Gets the buying power model defining how margin works in this group
/// </summary>
IPositionGroupBuyingPowerModel BuyingPowerModel { get; }
/// <summary>
/// Attempts to retrieve the position with the specified symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="position">The position, if found</param>
/// <returns>True if the position was found, otherwise false</returns>
bool TryGetPosition(Symbol symbol, out IPosition position);
```
### IPositionGroupResolver
The position group resolver is responsible for inspecting an algorithm's security holdings and creating a set of groups that minimizes the margin requirement of the entire portfolio. Some brokerages do this automatically for you, such as IB. The default resolver used to match ungrouped security holdings into the default `SecurityPositionGroup` is the `SecurityPositionGroupResolver`. Each _type_ of group (default/options/futures) will have its own resolver. The options resolver (not yet implemented), will integrate the `OptionStrategyMatcher`. The `OptionStrategyMatcher` looks at a set of security holdings with the same underlying, for example, GOOG and all GOOG option contracts, and attempts to arrange these holdings into groups to minimize the total margin required. When adding futures we'll need to add a `FutureStrategyMatcher`. The matchers work by looking at a set of definitions that define the specific ways in which securities can be grouped, such as covered call, but also more complex groupings such as the `Straddle` and `Strangle`. You can see the complete set of option strategy definitions in the `OptionStrategyDefinitions` class. The `OptionStrategyMatcher` loads all of these definitions and matches them to the algorithm's holdings. Once integrated into the yet-to-be-implemented `OptionStrategyPositionGroupResolver`, the results of the match operation will need to be projected into position group instances. Every time the algorithm's holdings change we need to evaluate at least a subset of the holdings to look for unexpected broken groups and determine whether or not breaking the group pushes the margin requirement up too high. It's entirely possible that the algorithm might **not** be able to sell that single share of GOOG from our earlier example because doing so breaks the covered call group and has the potential to increase the margin requirement beyond the maximum allowed.
In addition to performing matching functions, it also serves as a descriptor for how groups are constructed and therefore, the impacts of breaking a particular group. Since running all resolvers on the entire portfolio is an expensive operation, when holdings change its beneficial to only consider groups impacted by the change. Since each _type_ of group has different rules regarding how the positions relate to one another, the resolver exposes the `GetImpactedGroups` function. The first argument is usually the entire set of groups being maintained by the `PositionManager` and the second argument represents the changes being contemplated. I say contemplated because this is part of the 'what if' analysis that is done _before_ LEAN validates an order as being executable/submittable. We ask the resolver which groups are impacted by the requested change, for example -1 GOOG. In this example, the response would include all position groups that the equity GOOG is a member of in addition to all position groups that contain a GOOG option contract. We can then apply our changed positions to this reduced set of position groups and resolve the new position groups. We can then calculate the margin requirements on this new set of position groups and verify that its within bounds. If so, the order may proceed, if not, the order is flagged as insufficient buying power.
This idea of needing to run 'what if' analysis might take a minute before you're convinced that it's absolutely required, but once you understand how dynamic groups are and likewise how easily they can be broken and particularly how much margin is _saved_ through grouping (sometimes over 75%), it quickly becomes clear that breaking a group can have severe implications on the available margin in the algorithm's portfolio. Despite much effort and ample trying, we were unable to come up with a more performant mechanism and at this point are extremely confident that running 'what if' analysis is actually the only way to confidently and consistently get the correct answer every time. This is what led to the introduction of the `GetImpactGroups`, which saves a lot of time in algorithms with hundreds of security holdings and potentially hundreds of non-default groups. Consider being long 100 securities and writing a covered call on each. With `GetImpactedGroups`, if you sell one of the underlying shares, we'll only evaluate groups related to that equity and ignore the other 99 equity/option related groups.
``` csharp
// See: https://github.com/QuantConnect/Lean/blob/refactor-4065-position-groups/Common/Securities/Positions/IPositionGroupResolver.cs
/// <summary>
/// Attempts to group the specified positions into a new <see cref="IPositionGroup"/> using an
/// appropriate <see cref="IPositionGroupBuyingPowerModel"/> for position groups created via this
/// resolver.
/// </summary>
/// <param name="positions">The positions to be grouped</param>
/// <param name="group">The grouped positions when this resolver is able to, otherwise null</param>
/// <returns>True if this resolver can group the specified positions, otherwise false</returns>
bool TryGroup(IReadOnlyCollection<IPosition> positions, out IPositionGroup group);
/// <summary>
/// Resolves the position groups that exist within the specified collection of positions.
/// </summary>
/// <param name="positions">The collection of positions</param>
/// <returns>An enumerable of position groups</returns>
PositionGroupCollection Resolve(PositionCollection positions);
/// <summary>
/// Determines the position groups that would be evaluated for grouping of the specified
/// positions were passed into the <see cref="Resolve"/> method.
/// </summary>
/// <remarks>
/// This function allows us to determine a set of impacted groups and run the resolver on just
/// those groups in order to support what-if analysis
/// </remarks>
/// <param name="groups">The existing position groups</param>
/// <param name="positions">The positions being changed</param>
/// <returns>An enumerable containing the position groups that could be impacted by the specified position changes</returns>
IEnumerable<IPositionGroup> GetImpactedGroups(
PositionGroupCollection groups,
IReadOnlyCollection<IPosition> positions
);
```
### IPositionGroupBuyingPowerModel
Another appropriately named abstraction that leaves mystery on the sidelines. This interface aims to be an `IPositionGroup`-centric one-for-one mapping of the `Security`-centric `IBuyingPowerModel`. The only operations that were not ported from the original are the ones focused on getting/setting leverage, which simply doesn't apply to position groups. As you can see from the below excerpt, position groups require their own margin calculations, their own sufficient buying power for order checks and their own functions for determining the maximum quantity for a given delta/target buying power. I won't go into all of the methods as they're identical in purpose as their `IBuyingPower` counterparts, however, there is one _added_ method, and that is the `GetReservedBuyingPowerImpact`. One of the challenges of dealing with position groups is determining how a particular trade will impact the algorithm's groups. Consider our default case of 5 units of GOOG covered call. If we try to sell 1 GOOG share, bringing our total to 499 (500 - 1) shares of GOOG, it will _break_ one of the position group units. This will likely **increase** the total margin requirement of the portfolio. The `GetReservedBuyingPowerImpact` function exists to perform this 'what if' analysis.
``` csharp
// See: https://github.com/QuantConnect/Lean/blob/refactor-4065-position-groups/Common/Securities/Positions/IPositionGroupBuyingPowerModel.cs
/// <summary>
/// Gets the margin currently allocated to the specified holding
/// </summary>
/// <param name="parameters">An object containing the security</param>
/// <returns>The maintenance margin required for the </returns>
MaintenanceMargin GetMaintenanceMargin(PositionGroupMaintenanceMarginParameters parameters);
/// <summary>
/// The margin that must be held in order to increase the position by the provided quantity
/// </summary>
/// <param name="parameters">An object containing the security and quantity</param>
InitialMargin GetInitialMarginRequirement(PositionGroupInitialMarginParameters parameters);
/// <summary>
/// Gets the total margin required to execute the specified order in units of the account currency including fees
/// </summary>
/// <param name="parameters">An object containing the portfolio, the security and the order</param>
/// <returns>The total margin in terms of the currency quoted in the order</returns>
InitialMargin GetInitialMarginRequiredForOrder(PositionGroupInitialMarginForOrderParameters parameters);
/// <summary>
/// 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.
/// </summary>
/// <param name="parameters">An object containing the portfolio and a position group containing the contemplated
/// changes to the portfolio</param>
/// <returns>Returns the portfolio's total portfolio value and margin used before and after the position changes are applied</returns>
ReservedBuyingPowerImpact GetReservedBuyingPowerImpact(
ReservedBuyingPowerImpactParameters parameters
);
/// <summary>
/// Check if there is sufficient buying power for the position group to execute this order.
/// </summary>
/// <param name="parameters">An object containing the portfolio, the position group and the order</param>
/// <returns>Returns buying power information for an order against a position group</returns>
HasSufficientBuyingPowerForOrderResult HasSufficientBuyingPowerForOrder(
HasSufficientPositionGroupBuyingPowerForOrderParameters parameters
);
/// <summary>
/// Computes the amount of buying power reserved by the provided position group
/// </summary>
ReservedBuyingPowerForPositionGroup GetReservedBuyingPowerForPositionGroup(
ReservedBuyingPowerForPositionGroupParameters parameters
);
/// <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>
GetMaximumLotsResult GetMaximumLotsForTargetBuyingPower(
GetMaximumLotsForTargetBuyingPowerParameters parameters
);
/// <summary>
/// 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.
/// </summary>
/// <param name="parameters">An object containing the portfolio, the position group and the delta buying power</param>
/// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns>
/// <remarks>Used by the margin call model to reduce the position by a delta percent.</remarks>
GetMaximumLotsResult GetMaximumLotsForDeltaBuyingPower(
GetMaximumLotsForDeltaBuyingPowerParameters parameters
);
/// <summary>
/// Gets the buying power available for a position group trade
/// </summary>
/// <param name="parameters">A parameters object containing the algorithm's portfolio, security, and order direction</param>
/// <returns>The buying power available for the trade</returns>
PositionGroupBuyingPower GetPositionGroupBuyingPower(PositionGroupBuyingPowerParameters parameters);
```
### PositionGroupBuyingPowerModel
In the spirit of `BuyingPowerModel`, we've provided a base class for position group specific models to extend, as much of the logic is agnostic to the details of the group thanks to the way the various abstractions have been defined. That being said, when integrating options groups we **will** need to add an `OptionStrategyPositionGroupBuyingPowerModel` which subclasses the default base class `PositionGroupBuyingPowerModel`. The option strategy specific type will need to reference the table provided by Interactive Brokers which describes the margin requirements for each type of option strategy. You can find this table on IB's website [here](https://www.interactivebrokers.com/en/index.php?f=26660). This link, along with research notes and heaps of information outlining the general thought pattern and some of the concerns considered, all accumulated during the initial analysis/investigation phase in the github feature request issue [#4065](https://github.com/QuantConnect/Lean/issues/4065). IB seems to be what the users want, but once that's done I think it makes sense to also implement FINRA models. The takeaway from reading all of the regulations is that FINRA provides a baseline and brokers are free make them more strict (and maybe less strict, but IIRC FINRA set a limit), and indeed brokers can decide to _not_ support the concept of grouping at all. In such cases the broker would charge margin as the simple sum of the constituent parts, ie, no savings from groupings. We'll want to have a mechanism to support this case easily. This can be easily done by configuring the `PositionManager` to only use the `SecurityPositionGroupResolver` (more on that later though).
Back to the `PositionGroupBuyingPowerModel` - you'll notice that these methods make no assumptions as to the type or structure of the position group being evaluated, and as such, uses a lot of 'what if' analysis to make determinations. A particularly interesting bit is the `GetMaximumLotsForTargetBuyingPower` function. This function _should_ be suitable for all subclasses as its phrased in the most general way possible. The `SecurityPositionGroupBuyingPowerModel` _does_ override it for backwards compatibility reasons, particularly because each `IBuyingPowerModel` implementation (I'm looking at you `CashBuyingPowerModel`) implements this functions _slightly_ differently and especially when it comes to how fees are handled. Fees can either be _part_ of the pro-rata operation or fees can be viewed as a fixed cost coming off of the top. It took a while to arrive at a logical reasoning for one or the other, but the `PositionGroupBuyingPowerModel` removes fees off of the top. The reason for this is based on this function's usage, most notably, `QCAlgorithm.SetHoldings`. The purpose of this function is to allocate a particular percentage of the total portfolio value into _something_ (a position group in this case). If the fees were part of the pro-rata allocation then that's like saying we seek a quantity where the **cost** of the order is a particular percentage of total portfolio value, whereas taking the fees off the top is saying that we seek a quantity such that **after** the trade is completed, that position group (or security) will be the requested percentage of the total portfolio value.
Some time was taken to improve the Newton-Rhapson root finding. For some reason all of the `IBuyingPowerModel` implementations of this method have degraded over the years, now requiring two full iterations before returning. This is unnecessary. In a very common case, where fees are directly proportional to the total order value, it all breaks down into a simple linear equation and can by analytically solved exactly _without_ iterating at all (very common in crypto). The time should be taken to refactor the existing `IBuyingPowerModel` implementations by extracting to a single common implementation and parameterizing anything that's special.
Another function worth mentioning is `HasSufficientBuyingPowerForOrder` which has to perform multiple checks now. First we determine that our free buying power is enough to cover the initial margin requirement, then we provide a mechanism via a virtual method `PassesPositionGroupSpecificBuyingPowerForOrderChecks` for subclasses to inject their own checks and finally we perform the 'what if' analysis by invoking `GetChangeInReservedBuyingPower` and verifying that the change isn't greater than the free buying power. The `SecurityPositionGroupBuyingPowerModel` overrides the `PassesPositionGroupSpecificBuyingPowerForOrderChecks` in order to invoke `security.BuyingPowerModel`.
It's worth noting here that **ALL** implementations of `IPositionGroupBuyingPowerModel` should provide reasonable/idiomatic implementations of `GetHashCode` and `Equals`. This is because the model types are used in the `PositionGroupKey`, and as such, equality checks are done against it, but we want to treat these models as value types when it comes to equality checking, i.e, verify private fields are equal and the types are equal - even better, let a tool like ReSharper implement them for you :)
Also worth mentioning here is that **ALL** derived types will have to provide implementations for `GetInitialMarginRequirement`/`GetMaintenanceMargin`/`GetInitialMarginRequiredForOrder`. These functions enable us to implement the tougher functions in the base class and keeps subclasses laser focused on what makes them special, with an aim of preventing and/or reducing copy pasta. An optional override is the `PassesPositionGroupSpecificBuyingPowerForOrderChecks` which the `SecurityPositionGroupBuyingPowerModel` uses to invoke `security.BuyingPowerModel` functions directly.
``` csharp
// See: https://github.com/QuantConnect/Lean/blob/refactor-4065-position-groups/Common/Securities/Positions/PositionGroupBuyingPowerModel.cs
```
### SecurityPositionGroupBuyingPowerModel
Provides an implementation of `IPositionGroupBuyingPowerModel` that delegates to `security.BuyingPowerModel`. This model is intended to be used with the 'default' group and its aim is to provide 100% backwards compatible behavior. Below are the overriden methods with a _very_ brief comment describing how the delegating to the security's models happens. It's important to note here that `IPositionGroup.Quantity` is, from the security's perspective, a number of lots. In our 5 covered call example, there's 5 lots of (-1 GOOG CALL & 100 GOOG) for a total of -5 GOOG CALL & 500 GOOG shares. This is a position group quantity of 5. There are 5 lots of the GOOG equity and 5 lots of -1 GOOG CALL.
``` csharp
// See: https://github.com/QuantConnect/Lean/blob/refactor-4065-position-groups/Common/Securities/Positions/SecurityPositionGroupBuyingPowerModel.cs
/// <summary>
/// Gets the margin currently allocated to the specified holding
/// </summary>
/// <param name="parameters">An object containing the security</param>
/// <returns>The maintenance margin required for the </returns>
public override MaintenanceMargin GetMaintenanceMargin(PositionGroupMaintenanceMarginParameters parameters)
{
// simply delegate to security.BuyingPowerModel.GetMaintenanceMargin
}
/// <summary>
/// The margin that must be held in order to increase the position by the provided quantity
/// </summary>
/// <param name="parameters">An object containing the security and quantity</param>
public override InitialMargin GetInitialMarginRequirement(PositionGroupInitialMarginParameters parameters)
{
// simply delegates to security.BuyingPowerModel.GetInitialMarginRequirement
}
/// <summary>
/// Gets the total margin required to execute the specified order in units of the account currency including fees
/// </summary>
/// <param name="parameters">An object containing the portfolio, the security and the order</param>
/// <returns>The total margin in terms of the currency quoted in the order</returns>
public override InitialMargin GetInitialMarginRequiredForOrder(
PositionGroupInitialMarginForOrderParameters parameters
)
{
// simply delegates to security.BuyingPowerModel.GetInitialMarginRequiredForOrder
}
/// <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
)
{
// simply delegates to security.BuyingPowerModel.GetMaximumOrderQuantityForTargetBuyingPower
// and then converts the result which is in number of lots into a quantity using the lot size
var quantity = result.Quantity / security.SymbolProperties.LotSize;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="parameters">An object containing the portfolio, the position group and the delta buying power</param>
/// <returns>Returns the maximum allowed market order quantity and if zero, also the reason</returns>
/// <remarks>Used by the margin call model to reduce the position by a delta percent.</remarks>
public override GetMaximumLotsResult GetMaximumLotsForDeltaBuyingPower(
GetMaximumLotsForDeltaBuyingPowerParameters parameters
)
{
// simply delegates to security.BuyingPowerModel.GetMaximumOrderQuantityForDeltaBuyingPower
// and converts the maximum quantity into number of lots using the security's lot size
}
/// <summary>
/// Check if there is sufficient buying power for the position group to execute this order.
/// </summary>
/// <param name="parameters">An object containing the portfolio, the position group and the order</param>
/// <returns>Returns buying power information for an order against a position group</returns>
public override HasSufficientBuyingPowerForOrderResult HasSufficientBuyingPowerForOrder(
HasSufficientPositionGroupBuyingPowerForOrderParameters parameters
)
{
// simply delegates to security.BuyingPowerModel.HasSufficientBuyingPowerForOrder
}
/// <summary>
/// Additionally check initial margin requirements if the algorithm only has default position groups
/// </summary>
protected override HasSufficientBuyingPowerForOrderResult PassesPositionGroupSpecificBuyingPowerForOrderChecks(
HasSufficientPositionGroupBuyingPowerForOrderParameters parameters,
decimal availableBuyingPower
)
{
// simply delegates to security.BuyingPowerModel.HasSufficientBuyingPowerForOrder for default groups
}
```
## LEAN Integration
The above highlights the main abstractions and key terms used throughout the position groups feature code changes and commit messages. If you don't understand anything written prior to this sentence, stop, and go read it again. The aforementioned concepts are critical to have a firm understanding in before moving forward with how it all integrates into LEAN, and the rest of this document _assumes_ that the reader understands all of the terminology by this point.
### PositionManager
The `PositionManager` provides a mechanism similar to `SecurityPortfolioManager` to manage positions and position groups. Event handlers are wired up such that after _every_ fill event the `PositionManager` is notified and if required, will invoke the configured `IPositionGroupResolver` to determine the latest and greatest set of groups. Any ungrouped holdings get moved into the default `SecurityPositionGroup` -- the group of last resort. ALL HOLDINGS ARE ALWAYS GROUPED.
The `CompositePositionGroupResolver` needs to be added to the manager. The idea behind this guy is he would hold a list of resolvers configured by the algorithm, for example, one for options, one for futures and the last one would be the default (resolver of last resort) `SecurityPositionGroupResolver`. There's a WIP branch (`origin/refactor-4065-position-groups.wip`) that has an implementation of the composite resolver as well as having it all wired up properly in the position manager. Feel free to pull that in, minor edits are required. The order in which the resolvers are invoked is obviously important. If the `SecurityPositionGroupResolver` went _firsT_ then everything would be grouped before the other resolvers ran, so obviously he needs to run last. His entire job is to group everything that didn't get grouped. Foot stomping here. The ordering of invocation matters. **GREATLY**
Once a `CompositePositionGroupResolver` is implemented, should probably start with it just having the single default resolver for simplicity (`SecurityPositionGroupResolver`), make sure all unit/regression tests are passing and that's a clean/solid breakpoint.
### Other Touch Points
`SecurityHolding.QuantityChanged` event was added and the `PositionManager` listens to this. Every time quantities change (fill) we need to know so that we can re-run the resolvers. `PositionManager.ResolveGroups()` is _also_ invoked via `SecurityPortfolioManager.ProcessFill` when it's a partial/completed fill event (quantity changed). In order for all the margin maths to be correct, we must resolve groups immediately. Consider multiple market orders set to synchronous in the same `OnData` -- if we don't run the resolvers then the buying power models won't even know those orders executed because the buying power models' view of the world is through the lense of position groups, so it's incredibly important that **EVERY** time security holdings change we run the position group resolves to ensure consistent state.