Lean  $LEAN_TAG$
OptionStrategyPositionGroupResolver.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Linq;
18 using QuantConnect.Util;
19 using QuantConnect.Orders;
20 using System.Collections.Generic;
23 
25 {
26  /// <summary>
27  /// Class in charge of resolving option strategy groups which will use the <see cref="OptionStrategyPositionGroupBuyingPowerModel"/>
28  /// </summary>
30  {
31  private readonly SecurityManager _securities;
32  private readonly OptionStrategyMatcher _strategyMatcher;
33 
34  /// <summary>
35  /// Creates the default option strategy group resolver for <see cref="OptionStrategyDefinitions.AllDefinitions"/>
36  /// </summary>
38  : this(securities, OptionStrategyMatcherOptions.ForDefinitions(OptionStrategyDefinitions.AllDefinitions))
39  {
40  }
41 
42  /// <summary>
43  /// Creates a custom option strategy group resolver
44  /// </summary>
45  /// <param name="strategyMatcherOptions">The option strategy matcher options instance to use</param>
46  /// <param name="securities">The algorithms securities</param>
48  {
49  _securities = securities;
50  _strategyMatcher = new OptionStrategyMatcher(strategyMatcherOptions);
51  }
52 
53  /// <summary>
54  /// Attempts to group the specified positions into a new <see cref="IPositionGroup"/> using an
55  /// appropriate <see cref="IPositionGroupBuyingPowerModel"/> for position groups created via this
56  /// resolver.
57  /// </summary>
58  /// <param name="newPositions">The positions to be grouped</param>
59  /// <param name="currentPositions">The currently grouped positions</param>
60  /// <param name="group">The grouped positions when this resolver is able to, otherwise null</param>
61  /// <returns>True if this resolver can group the specified positions, otherwise false</returns>
62  public bool TryGroup(IReadOnlyCollection<IPosition> newPositions, PositionGroupCollection currentPositions, out IPositionGroup @group)
63  {
64  IEnumerable<IPosition> positions;
65  if (currentPositions.Count > 0)
66  {
67  var impactedGroups = GetImpactedGroups(currentPositions, newPositions);
68  var positionsToConsiderInNewGroup = impactedGroups.SelectMany(positionGroup => positionGroup.Positions);
69  positions = newPositions.Concat(positionsToConsiderInNewGroup);
70  }
71  else
72  {
73  if (newPositions.Count == 1)
74  {
75  // there's no existing position and there's only a single position, no strategy will match
76  @group = null;
77  return false;
78  }
79  positions = newPositions;
80  }
81 
82  @group = GetPositionGroups(positions)
83  .Select(positionGroup =>
84  {
85  if (positionGroup.Count == 0)
86  {
87  return positionGroup;
88  }
89 
90  if (newPositions.Any(position => positionGroup.TryGetPosition(position.Symbol, out position)))
91  {
92  return positionGroup;
93  }
94 
95  // When none of the new positions are contained in the position group,
96  // it means that we are liquidating the assets in the new positions
97  // but some other existing positions were considered as impacted groups.
98  // Example:
99  // Buy(OptionStrategies.BullCallSpread(...), 1);
100  // Buy(OptionStrategies.BearPutSpread(...), 1);
101  // ...
102  // Sell(OptionStrategies.BullCallSpread(...), 1);
103  // Sell(OptionStrategies.BearPutSpread(...), 1);
104  // -----
105  // When attempting revert the bull call position group, the bear put group
106  // will be selected as impacted group, so the group will contain the put positions
107  // but not the call ones. In this case, we return an valid empty group because the
108  // liquidation is happening.
110  })
111  .Where(positionGroup => positionGroup != null)
112  .FirstOrDefault();
113 
114  return @group != null;
115  }
116 
117  /// <summary>
118  /// Resolves the position groups that exist within the specified collection of positions.
119  /// </summary>
120  /// <param name="positions">The collection of positions</param>
121  /// <returns>An enumerable of position groups</returns>
123  {
124  var result = PositionGroupCollection.Empty;
125 
126  var groups = GetPositionGroups(positions).ToList();
127  if (groups.Count != 0)
128  {
129  result = new PositionGroupCollection(groups);
130 
131  // we are expected to remove any positions which we resolved into a position group
132  positions.Remove(result);
133  }
134 
135  return result;
136  }
137 
138  /// <summary>
139  /// Determines the position groups that would be evaluated for grouping of the specified
140  /// positions were passed into the <see cref="Resolve"/> method.
141  /// </summary>
142  /// <remarks>
143  /// This function allows us to determine a set of impacted groups and run the resolver on just
144  /// those groups in order to support what-if analysis
145  /// </remarks>
146  /// <param name="groups">The existing position groups</param>
147  /// <param name="positions">The positions being changed</param>
148  /// <returns>An enumerable containing the position groups that could be impacted by the specified position changes</returns>
149  public IEnumerable<IPositionGroup> GetImpactedGroups(PositionGroupCollection groups, IReadOnlyCollection<IPosition> positions)
150  {
151  if(groups.Count == 0)
152  {
153  // there's no existing groups, nothing to impact
154  return Enumerable.Empty<IPositionGroup>();
155  }
156 
157  var symbolsSet = positions.Where(position => position.Symbol.SecurityType.HasOptions() || position.Symbol.SecurityType.IsOption())
158  .SelectMany(position =>
159  {
160  return position.Symbol.HasUnderlying ? new[] { position.Symbol, position.Symbol.Underlying } : new[] { position.Symbol };
161  })
162  .ToHashSet();
163 
164  if (symbolsSet.Count == 0)
165  {
166  return Enumerable.Empty<IPositionGroup>();
167  }
168 
169  // 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
170  return groups.Where(group => group.Quantity != 0
171  && group.Positions.Any(position1 => symbolsSet.Contains(position1.Symbol)
172  || position1.Symbol.HasUnderlying && position1.Symbol.SecurityType.IsOption() && symbolsSet.Contains(position1.Symbol.Underlying)));
173  }
174 
175  private IEnumerable<IPositionGroup> GetPositionGroups(IEnumerable<IPosition> positions)
176  {
177  foreach (var positionsByUnderlying in positions
178  .Where(position => position.Symbol.SecurityType.HasOptions() || position.Symbol.SecurityType.IsOption())
179  .GroupBy(position => position.Symbol.HasUnderlying? position.Symbol.Underlying : position.Symbol)
180  .Select(x => x.ToList()))
181  {
182  var optionPosition = positionsByUnderlying.FirstOrDefault(position => position.Symbol.SecurityType.IsOption());
183  if (optionPosition == null)
184  {
185  // if there isn't any option position we aren't really interested, can't create any option strategy!
186  continue;
187  }
188  var contractMultiplier = (_securities[optionPosition.Symbol].SymbolProperties as OptionSymbolProperties)?.ContractUnitOfTrade ?? 100;
189 
190  var optionPositionCollection = OptionPositionCollection.FromPositions(positionsByUnderlying, contractMultiplier);
191 
192  if (optionPositionCollection.Count == 0 && positionsByUnderlying.Count > 0)
193  {
194  // we could be liquidating there will be no position left!
195  yield return PositionGroup.Empty(new OptionStrategyPositionGroupBuyingPowerModel(null));
196  yield break;
197  }
198 
199  var matches = _strategyMatcher.MatchOnce(optionPositionCollection);
200  if (matches.Strategies.Count == 0)
201  {
202  continue;
203  }
204 
205  foreach (var matchedStrategy in matches.Strategies)
206  {
207  var groupQuantity = Math.Abs(matchedStrategy.OptionLegs.Cast<Leg>().Concat(matchedStrategy.UnderlyingLegs)
208  .Select(leg => leg.Quantity)
209  .GreatestCommonDivisor());
210  var positionsToGroup = matchedStrategy.OptionLegs
211  .Select(optionLeg => (IPosition)new Position(optionLeg.Symbol, optionLeg.Quantity,
212  // The unit quantity of each position is the ratio of the quantity of the leg to the group quantity.
213  // e.g. a butterfly call strategy three legs: 10:-20:10, the unit quantity of each leg is 1:2:1
214  Math.Abs(optionLeg.Quantity) / groupQuantity))
215  .Concat(matchedStrategy.UnderlyingLegs.Select(underlyingLeg => new Position(underlyingLeg.Symbol,
216  underlyingLeg.Quantity * contractMultiplier,
217  // Same as for the option legs, but we need to multiply by the contract multiplier.
218  // e.g. a covered call strategy has 100 shares of the underlying, per shorted contract
219  (Math.Abs(underlyingLeg.Quantity) * contractMultiplier / groupQuantity))))
220  .ToDictionary(position => position.Symbol);
221 
222  yield return new PositionGroup(
223  new PositionGroupKey(new OptionStrategyPositionGroupBuyingPowerModel(matchedStrategy), positionsToGroup.Values),
224  groupQuantity,
225  positionsToGroup);
226  }
227  }
228  }
229  }
230 }