/*
* 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 System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using QuantConnect.Securities.Positions;
namespace QuantConnect.Securities.Option.StrategyMatcher
{
///
/// Provides indexing of option contracts
///
public class OptionPositionCollection : IEnumerable
{
///
/// Gets an empty instance of
///
public static OptionPositionCollection Empty { get; } = new OptionPositionCollection(
ImmutableDictionary.Empty,
ImmutableDictionary>.Empty,
ImmutableDictionary>.Empty,
ImmutableSortedDictionary>.Empty,
ImmutableSortedDictionary>.Empty
);
private readonly ImmutableDictionary _positions;
private readonly ImmutableDictionary> _rights;
private readonly ImmutableDictionary> _sides;
private readonly ImmutableSortedDictionary> _strikes;
private readonly ImmutableSortedDictionary> _expirations;
///
/// Gets the underlying security's symbol
///
public Symbol Underlying => UnderlyingPosition.Symbol ?? Symbol.Empty;
///
/// Gets the total count of unique positions, including the underlying
///
public int Count => _positions.Count;
///
/// Gets whether or not there's any positions in this collection.
///
public bool IsEmpty => _positions.IsEmpty;
///
/// Gets the quantity of underlying shares held
/// TODO : Change to UnderlyingLots
///
public int UnderlyingQuantity => UnderlyingPosition.Quantity;
///
/// Gets the number of unique put contracts held (long or short)
///
public int UniquePuts => _rights[OptionRight.Put].Count;
///
/// Gets the unique number of expirations
///
public int UniqueExpirations => _expirations.Count;
///
/// Gets the number of unique call contracts held (long or short)
///
public int UniqueCalls => _rights[OptionRight.Call].Count;
///
/// Determines if this collection contains a position in the underlying
///
public bool HasUnderlying => UnderlyingQuantity != 0;
///
/// Gets the position
///
public OptionPosition UnderlyingPosition { get; }
///
/// Gets all unique strike prices in the collection, in ascending order.
///
public IEnumerable Strikes => _strikes.Keys;
///
/// Gets all unique expiration dates in the collection, in chronological order.
///
public IEnumerable Expirations => _expirations.Keys;
///
/// Initializes a new instance of the class
///
/// All positions
/// Index of position symbols by option right
/// Index of position symbols by position side (short/long/none)
/// Index of position symbols by strike price
/// Index of position symbols by expiration
public OptionPositionCollection(
ImmutableDictionary positions,
ImmutableDictionary> rights,
ImmutableDictionary> sides,
ImmutableSortedDictionary> strikes,
ImmutableSortedDictionary> expirations
)
{
_sides = sides;
_rights = rights;
_strikes = strikes;
_positions = positions;
_expirations = expirations;
if (_rights.Count != 2)
{
// ensure we always have both rights indexed, even if empty
ImmutableHashSet value;
if (!_rights.TryGetValue(OptionRight.Call, out value))
{
_rights = _rights.SetItem(OptionRight.Call, ImmutableHashSet.Empty);
}
if (!_rights.TryGetValue(OptionRight.Put, out value))
{
_rights = _rights.SetItem(OptionRight.Put, ImmutableHashSet.Empty);
}
}
if (_sides.Count != 3)
{
// ensure we always have all three sides indexed, even if empty
ImmutableHashSet value;
if (!_sides.TryGetValue(PositionSide.None, out value))
{
_sides = _sides.SetItem(PositionSide.None, ImmutableHashSet.Empty);
}
if (!_sides.TryGetValue(PositionSide.Short, out value))
{
_sides = _sides.SetItem(PositionSide.Short, ImmutableHashSet.Empty);
}
if (!_sides.TryGetValue(PositionSide.Long, out value))
{
_sides = _sides.SetItem(PositionSide.Long, ImmutableHashSet.Empty);
}
}
if (!positions.IsEmpty)
{
// assumption here is that 'positions' includes the underlying equity position and
// ONLY option contracts, so all symbols have the underlying equity symbol embedded
// via the Underlying property, except of course, for the underlying itself.
var underlying = positions.First().Key;
if (underlying.HasUnderlying)
{
underlying = underlying.Underlying;
}
// OptionPosition is struct, so no worry about null ref via .Quantity
var underlyingQuantity = positions.GetValueOrDefault(underlying).Quantity;
UnderlyingPosition = new OptionPosition(underlying, underlyingQuantity);
}
#if DEBUG
var errors = Validate().ToList();
if (errors.Count > 0)
{
throw new ArgumentException("OptionPositionCollection validation failed: "
+ Environment.NewLine + string.Join(Environment.NewLine, errors)
);
}
#endif
}
///
/// Determines if a position is held in the specified
///
public bool HasPosition(Symbol symbol)
{
OptionPosition position;
return TryGetPosition(symbol, out position) && position.Quantity != 0;
}
///
/// Retrieves the for the specified
/// if one exists in this collection.
///
public bool TryGetPosition(Symbol symbol, out OptionPosition position)
{
return _positions.TryGetValue(symbol, out position);
}
///
/// Creates a new from the specified enumerable of
///
public static OptionPositionCollection FromPositions(IEnumerable positions)
{
return Empty.AddRange(positions);
}
///
/// Creates a new from the specified enumerable of
///
public static OptionPositionCollection FromPositions(IEnumerable positions, decimal contractMultiplier)
{
return Empty.AddRange(positions.Select(position =>
{
var quantity = (int)position.Quantity;
if (position.Symbol.SecurityType.HasOptions())
{
quantity = (int) (quantity / contractMultiplier);
}
return new OptionPosition(position.Symbol, quantity);
}));
}
///
/// Creates a new from the specified ,
/// filtering based on the
///
public static OptionPositionCollection Create(Symbol underlying, decimal contractMultiplier, IEnumerable holdings)
{
var positions = Empty;
foreach (var holding in holdings)
{
var symbol = holding.Symbol;
if (!symbol.HasUnderlying)
{
if (symbol == underlying)
{
var underlyingLots = (int) (holding.Quantity / contractMultiplier);
positions = positions.Add(new OptionPosition(symbol, underlyingLots));
}
continue;
}
if (symbol.Underlying != underlying)
{
continue;
}
var position = new OptionPosition(symbol, (int) holding.Quantity);
positions = positions.Add(position);
}
return positions;
}
///
/// Creates a new collection that is the result of adding the specified to this collection.
///
public OptionPositionCollection Add(OptionPosition position)
{
if (!position.HasQuantity)
{
// adding nothing doesn't change the collection
return this;
}
var sides = _sides;
var rights = _rights;
var strikes = _strikes;
var positions = _positions;
var expirations = _expirations;
var exists = false;
OptionPosition existing;
var symbol = position.Symbol;
if (positions.TryGetValue(symbol, out existing))
{
exists = true;
position += existing;
}
if (position.HasQuantity)
{
positions = positions.SetItem(symbol, position);
if (!exists && symbol.HasUnderlying)
{
// update indexes when adding a new option contract
sides = sides.Add(position.Side, symbol);
rights = rights.Add(position.Right, symbol);
strikes = strikes.Add(position.Strike, symbol);
positions = positions.SetItem(symbol, position);
expirations = expirations.Add(position.Expiration, symbol);
}
}
else
{
// if the position's quantity went to zero, remove it entirely from the collection when
// removing, be sure to remove strike/expiration indexes. we purposefully keep the rights
// index populated, even with a zero count entry because it's bounded to 2 items (put/call)
positions = positions.Remove(symbol);
if (symbol.HasUnderlying)
{
// keep call/put entries even if goes to zero
var rightsValue = rights[position.Right].Remove(symbol);
rights = rights.SetItem(position.Right, rightsValue);
// keep short/none/long entries even if goes to zero
var sidesValue = sides[position.Side].Remove(symbol);
sides = sides.SetItem(position.Side, sidesValue);
var strikesValue = strikes[position.Strike].Remove(symbol);
strikes = strikesValue.Count > 0
? strikes.SetItem(position.Strike, strikesValue)
: strikes.Remove(position.Strike);
var expirationsValue = expirations[position.Expiration].Remove(symbol);
expirations = expirationsValue.Count > 0
? expirations.SetItem(position.Expiration, expirationsValue)
: expirations.Remove(position.Expiration);
}
}
return new OptionPositionCollection(positions, rights, sides, strikes, expirations);
}
///
/// Creates a new collection that is the result of removing the specified
///
public OptionPositionCollection Remove(OptionPosition position)
{
return Add(position.Negate());
}
///
/// Creates a new collection that is the result of adding the specified to this collection.
///
public OptionPositionCollection AddRange(params OptionPosition[] positions)
{
return AddRange((IEnumerable) positions);
}
///
/// Creates a new collection that is the result of adding the specified to this collection.
///
public OptionPositionCollection AddRange(IEnumerable positions)
{
return positions.Aggregate(this, (current, position) => current + position);
}
///
/// Creates a new collection that is the result of removing the specified
///
public OptionPositionCollection RemoveRange(IEnumerable positions)
{
return AddRange(positions.Select(position => position.Negate()));
}
///
/// Slices this collection, returning a new collection containing only
/// positions with the specified
///
public OptionPositionCollection Slice(OptionRight right, bool includeUnderlying = true)
{
var rights = _rights.Remove(right.Invert());
var positions = ImmutableDictionary.Empty;
if (includeUnderlying && HasUnderlying)
{
positions = positions.Add(Underlying, UnderlyingPosition);
}
var sides = ImmutableDictionary>.Empty;
var strikes = ImmutableSortedDictionary>.Empty;
var expirations = ImmutableSortedDictionary>.Empty;
foreach (var symbol in rights.SelectMany(kvp => kvp.Value))
{
var position = _positions[symbol];
sides = sides.Add(position.Side, symbol);
positions = positions.Add(symbol, position);
strikes = strikes.Add(position.Strike, symbol);
expirations = expirations.Add(position.Expiration, symbol);
}
return new OptionPositionCollection(positions, rights, sides, strikes, expirations);
}
///
/// Slices this collection, returning a new collection containing only
/// positions with the specified
///
public OptionPositionCollection Slice(PositionSide side, bool includeUnderlying = true)
{
var otherSides = GetOtherSides(side);
var sides = _sides.Remove(otherSides[0]).Remove(otherSides[1]);
var positions = ImmutableDictionary.Empty;
if (includeUnderlying && HasUnderlying)
{
positions = positions.Add(Underlying, UnderlyingPosition);
}
var rights = ImmutableDictionary>.Empty;
var strikes = ImmutableSortedDictionary>.Empty;
var expirations = ImmutableSortedDictionary>.Empty;
foreach (var symbol in sides.SelectMany(kvp => kvp.Value))
{
var position = _positions[symbol];
rights = rights.Add(position.Right, symbol);
positions = positions.Add(symbol, position);
strikes = strikes.Add(position.Strike, symbol);
expirations = expirations.Add(position.Expiration, symbol);
}
return new OptionPositionCollection(positions, rights, sides, strikes, expirations);
}
///
/// Slices this collection, returning a new collection containing only
/// positions matching the specified and
///
public OptionPositionCollection Slice(BinaryComparison comparison, decimal strike, bool includeUnderlying = true)
{
var strikes = comparison.Filter(_strikes, strike);
if (strikes.IsEmpty)
{
return includeUnderlying && HasUnderlying ? Empty.Add(UnderlyingPosition) : Empty;
}
var positions = ImmutableDictionary.Empty;
if (includeUnderlying)
{
OptionPosition underlyingPosition;
if (_positions.TryGetValue(Underlying, out underlyingPosition))
{
positions = positions.Add(Underlying, underlyingPosition);
}
}
var sides = ImmutableDictionary>.Empty;
var rights = ImmutableDictionary>.Empty;
var expirations = ImmutableSortedDictionary>.Empty;
foreach (var symbol in strikes.SelectMany(kvp => kvp.Value))
{
var position = _positions[symbol];
sides = sides.Add(position.Side, symbol);
positions = positions.Add(symbol, position);
rights = rights.Add(symbol.ID.OptionRight, symbol);
expirations = expirations.Add(symbol.ID.Date, symbol);
}
return new OptionPositionCollection(positions, rights, sides, strikes, expirations);
}
///
/// Slices this collection, returning a new collection containing only
/// positions matching the specified and
///
public OptionPositionCollection Slice(BinaryComparison comparison, DateTime expiration, bool includeUnderlying = true)
{
var expirations = comparison.Filter(_expirations, expiration);
if (expirations.IsEmpty)
{
return includeUnderlying && HasUnderlying ? Empty.Add(UnderlyingPosition) : Empty;
}
var positions = ImmutableDictionary.Empty;
if (includeUnderlying)
{
OptionPosition underlyingPosition;
if (_positions.TryGetValue(Underlying, out underlyingPosition))
{
positions = positions.Add(Underlying, underlyingPosition);
}
}
var sides = ImmutableDictionary>.Empty;
var rights = ImmutableDictionary>.Empty;
var strikes = ImmutableSortedDictionary>.Empty;
foreach (var symbol in expirations.SelectMany(kvp => kvp.Value))
{
var position = _positions[symbol];
sides = sides.Add(position.Side, symbol);
positions = positions.Add(symbol, position);
rights = rights.Add(symbol.ID.OptionRight, symbol);
strikes = strikes.Add(symbol.ID.StrikePrice, symbol);
}
return new OptionPositionCollection(positions, rights, sides, strikes, expirations);
}
///
/// Returns the set of with the specified
///
public IEnumerable ForSymbols(IEnumerable symbols)
{
foreach (var symbol in symbols)
{
OptionPosition position;
if (_positions.TryGetValue(symbol, out position))
{
yield return position;
}
}
}
///
/// Returns the set of with the specified
///
public IEnumerable ForRight(OptionRight right)
{
ImmutableHashSet symbols;
return _rights.TryGetValue(right, out symbols)
? ForSymbols(symbols)
: Enumerable.Empty();
}
///
/// Returns the set of with the specified
///
public IEnumerable ForSide(PositionSide side)
{
ImmutableHashSet symbols;
return _sides.TryGetValue(side, out symbols)
? ForSymbols(symbols)
: Enumerable.Empty();
}
///
/// Returns the set of with the specified
///
public IEnumerable ForStrike(decimal strike)
{
ImmutableHashSet symbols;
return _strikes.TryGetValue(strike, out symbols)
? ForSymbols(symbols)
: Enumerable.Empty();
}
///
/// Returns the set of with the specified
///
public IEnumerable ForExpiration(DateTime expiration)
{
ImmutableHashSet symbols;
return _expirations.TryGetValue(expiration, out symbols)
? ForSymbols(symbols)
: Enumerable.Empty();
}
/// Returns a string that represents the current object.
/// A string that represents the current object.
public override string ToString()
{
if (Count == 0)
{
return "Empty";
}
return HasUnderlying
? $"{UnderlyingQuantity} {Underlying.Value}: {_positions.Count - 1} contract positions"
: $"{Underlying.Value}: {_positions.Count} contract positions";
}
/// Returns an enumerator that iterates through the collection.
/// An enumerator that can be used to iterate through the collection.
public IEnumerator GetEnumerator()
{
return _positions.Select(kvp => kvp.Value).GetEnumerator();
}
///
/// Validates this collection returning an enumerable of validation errors.
/// This should only be invoked via tests and is automatically invoked via
/// the constructor in DEBUG builds.
///
internal IEnumerable Validate()
{
foreach (var kvp in _positions)
{
var position = kvp.Value;
var symbol = position.Symbol;
if (position.Quantity == 0)
{
yield return $"{position}: Quantity == 0";
}
if (!symbol.HasUnderlying)
{
continue;
}
ImmutableHashSet strikes;
if (!_strikes.TryGetValue(position.Strike, out strikes) || !strikes.Contains(symbol))
{
yield return $"{position}: Not indexed by strike price";
}
ImmutableHashSet expirations;
if (!_expirations.TryGetValue(position.Expiration, out expirations) || !expirations.Contains(symbol))
{
yield return $"{position}: Not indexed by expiration date";
}
}
}
private static readonly PositionSide[] OtherSidesForNone = {PositionSide.Short, PositionSide.Long};
private static readonly PositionSide[] OtherSidesForShort = {PositionSide.None, PositionSide.Long};
private static readonly PositionSide[] OtherSidesForLong = {PositionSide.Short, PositionSide.None};
private static PositionSide[] GetOtherSides(PositionSide side)
{
switch (side)
{
case PositionSide.Short: return OtherSidesForShort;
case PositionSide.None: return OtherSidesForNone;
case PositionSide.Long: return OtherSidesForLong;
default:
throw new ArgumentOutOfRangeException(nameof(side), side, null);
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
///
/// OptionPositionCollection + Operator
///
/// Collection to add to
/// OptionPosition to add
/// OptionPositionCollection with the new position added
public static OptionPositionCollection operator+(OptionPositionCollection positions, OptionPosition position)
{
return positions.Add(position);
}
///
/// OptionPositionCollection - Operator
///
/// Collection to remove from
/// OptionPosition to remove
/// OptionPositionCollection with the position removed
public static OptionPositionCollection operator-(OptionPositionCollection positions, OptionPosition position)
{
return positions.Remove(position);
}
}
}