/*
* 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);
}
}
}
}
}