/* * 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 { /// /// Class in charge of resolving option strategy groups which will use the /// public class OptionStrategyPositionGroupResolver : IPositionGroupResolver { private readonly SecurityManager _securities; private readonly OptionStrategyMatcher _strategyMatcher; /// /// Creates the default option strategy group resolver for /// public OptionStrategyPositionGroupResolver(SecurityManager securities) : this(securities, OptionStrategyMatcherOptions.ForDefinitions(OptionStrategyDefinitions.AllDefinitions)) { } /// /// Creates a custom option strategy group resolver /// /// The option strategy matcher options instance to use /// The algorithms securities public OptionStrategyPositionGroupResolver(SecurityManager securities, OptionStrategyMatcherOptions strategyMatcherOptions) { _securities = securities; _strategyMatcher = new OptionStrategyMatcher(strategyMatcherOptions); } /// /// Attempts to group the specified positions into a new using an /// appropriate for position groups created via this /// resolver. /// /// The positions to be grouped /// The currently grouped positions /// The grouped positions when this resolver is able to, otherwise null /// True if this resolver can group the specified positions, otherwise false public bool TryGroup(IReadOnlyCollection newPositions, PositionGroupCollection currentPositions, out IPositionGroup @group) { IEnumerable 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; } /// /// Resolves the position groups that exist within the specified collection of positions. /// /// The collection of positions /// An enumerable of position groups 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; } /// /// Determines the position groups that would be evaluated for grouping of the specified /// positions were passed into the method. /// /// /// 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 /// /// The existing position groups /// The positions being changed /// An enumerable containing the position groups that could be impacted by the specified position changes public IEnumerable GetImpactedGroups(PositionGroupCollection groups, IReadOnlyCollection positions) { if(groups.Count == 0) { // there's no existing groups, nothing to impact return Enumerable.Empty(); } 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(); } // 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 GetPositionGroups(IEnumerable 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().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); } } } } }