Files
quantconnect--lean/Common/Securities/Option/StrategyMatcher/OptionPositionCollection.cs
T
2026-07-13 13:02:50 +08:00

653 lines
27 KiB
C#

/*
* 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
{
/// <summary>
/// Provides indexing of option contracts
/// </summary>
public class OptionPositionCollection : IEnumerable<OptionPosition>
{
/// <summary>
/// Gets an empty instance of <see cref="OptionPositionCollection"/>
/// </summary>
public static OptionPositionCollection Empty { get; } = new OptionPositionCollection(
ImmutableDictionary<Symbol, OptionPosition>.Empty,
ImmutableDictionary<OptionRight, ImmutableHashSet<Symbol>>.Empty,
ImmutableDictionary<PositionSide, ImmutableHashSet<Symbol>>.Empty,
ImmutableSortedDictionary<decimal, ImmutableHashSet<Symbol>>.Empty,
ImmutableSortedDictionary<DateTime, ImmutableHashSet<Symbol>>.Empty
);
private readonly ImmutableDictionary<Symbol, OptionPosition> _positions;
private readonly ImmutableDictionary<OptionRight, ImmutableHashSet<Symbol>> _rights;
private readonly ImmutableDictionary<PositionSide, ImmutableHashSet<Symbol>> _sides;
private readonly ImmutableSortedDictionary<decimal, ImmutableHashSet<Symbol>> _strikes;
private readonly ImmutableSortedDictionary<DateTime, ImmutableHashSet<Symbol>> _expirations;
/// <summary>
/// Gets the underlying security's symbol
/// </summary>
public Symbol Underlying => UnderlyingPosition.Symbol ?? Symbol.Empty;
/// <summary>
/// Gets the total count of unique positions, including the underlying
/// </summary>
public int Count => _positions.Count;
/// <summary>
/// Gets whether or not there's any positions in this collection.
/// </summary>
public bool IsEmpty => _positions.IsEmpty;
/// <summary>
/// Gets the quantity of underlying shares held
/// TODO : Change to UnderlyingLots
/// </summary>
public int UnderlyingQuantity => UnderlyingPosition.Quantity;
/// <summary>
/// Gets the number of unique put contracts held (long or short)
/// </summary>
public int UniquePuts => _rights[OptionRight.Put].Count;
/// <summary>
/// Gets the unique number of expirations
/// </summary>
public int UniqueExpirations => _expirations.Count;
/// <summary>
/// Gets the number of unique call contracts held (long or short)
/// </summary>
public int UniqueCalls => _rights[OptionRight.Call].Count;
/// <summary>
/// Determines if this collection contains a position in the underlying
/// </summary>
public bool HasUnderlying => UnderlyingQuantity != 0;
/// <summary>
/// Gets the <see cref="Underlying"/> position
/// </summary>
public OptionPosition UnderlyingPosition { get; }
/// <summary>
/// Gets all unique strike prices in the collection, in ascending order.
/// </summary>
public IEnumerable<decimal> Strikes => _strikes.Keys;
/// <summary>
/// Gets all unique expiration dates in the collection, in chronological order.
/// </summary>
public IEnumerable<DateTime> Expirations => _expirations.Keys;
/// <summary>
/// Initializes a new instance of the <see cref="OptionPositionCollection"/> class
/// </summary>
/// <param name="positions">All positions</param>
/// <param name="rights">Index of position symbols by option right</param>
/// <param name="sides">Index of position symbols by position side (short/long/none)</param>
/// <param name="strikes">Index of position symbols by strike price</param>
/// <param name="expirations">Index of position symbols by expiration</param>
public OptionPositionCollection(
ImmutableDictionary<Symbol, OptionPosition> positions,
ImmutableDictionary<OptionRight, ImmutableHashSet<Symbol>> rights,
ImmutableDictionary<PositionSide, ImmutableHashSet<Symbol>> sides,
ImmutableSortedDictionary<decimal, ImmutableHashSet<Symbol>> strikes,
ImmutableSortedDictionary<DateTime, ImmutableHashSet<Symbol>> 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<Symbol> value;
if (!_rights.TryGetValue(OptionRight.Call, out value))
{
_rights = _rights.SetItem(OptionRight.Call, ImmutableHashSet<Symbol>.Empty);
}
if (!_rights.TryGetValue(OptionRight.Put, out value))
{
_rights = _rights.SetItem(OptionRight.Put, ImmutableHashSet<Symbol>.Empty);
}
}
if (_sides.Count != 3)
{
// ensure we always have all three sides indexed, even if empty
ImmutableHashSet<Symbol> value;
if (!_sides.TryGetValue(PositionSide.None, out value))
{
_sides = _sides.SetItem(PositionSide.None, ImmutableHashSet<Symbol>.Empty);
}
if (!_sides.TryGetValue(PositionSide.Short, out value))
{
_sides = _sides.SetItem(PositionSide.Short, ImmutableHashSet<Symbol>.Empty);
}
if (!_sides.TryGetValue(PositionSide.Long, out value))
{
_sides = _sides.SetItem(PositionSide.Long, ImmutableHashSet<Symbol>.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
}
/// <summary>
/// Determines if a position is held in the specified <paramref name="symbol"/>
/// </summary>
public bool HasPosition(Symbol symbol)
{
OptionPosition position;
return TryGetPosition(symbol, out position) && position.Quantity != 0;
}
/// <summary>
/// Retrieves the <see cref="OptionPosition"/> for the specified <paramref name="symbol"/>
/// if one exists in this collection.
/// </summary>
public bool TryGetPosition(Symbol symbol, out OptionPosition position)
{
return _positions.TryGetValue(symbol, out position);
}
/// <summary>
/// Creates a new <see cref="OptionPositionCollection"/> from the specified enumerable of <paramref name="positions"/>
/// </summary>
public static OptionPositionCollection FromPositions(IEnumerable<OptionPosition> positions)
{
return Empty.AddRange(positions);
}
/// <summary>
/// Creates a new <see cref="OptionPositionCollection"/> from the specified enumerable of <paramref name="positions"/>
/// </summary>
public static OptionPositionCollection FromPositions(IEnumerable<IPosition> 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);
}));
}
/// <summary>
/// Creates a new <see cref="OptionPositionCollection"/> from the specified <paramref name="holdings"/>,
/// filtering based on the <paramref name="underlying"/>
/// </summary>
public static OptionPositionCollection Create(Symbol underlying, decimal contractMultiplier, IEnumerable<SecurityHolding> 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;
}
/// <summary>
/// Creates a new collection that is the result of adding the specified <paramref name="position"/> to this collection.
/// </summary>
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);
}
/// <summary>
/// Creates a new collection that is the result of removing the specified <paramref name="position"/>
/// </summary>
public OptionPositionCollection Remove(OptionPosition position)
{
return Add(position.Negate());
}
/// <summary>
/// Creates a new collection that is the result of adding the specified <paramref name="positions"/> to this collection.
/// </summary>
public OptionPositionCollection AddRange(params OptionPosition[] positions)
{
return AddRange((IEnumerable<OptionPosition>) positions);
}
/// <summary>
/// Creates a new collection that is the result of adding the specified <paramref name="positions"/> to this collection.
/// </summary>
public OptionPositionCollection AddRange(IEnumerable<OptionPosition> positions)
{
return positions.Aggregate(this, (current, position) => current + position);
}
/// <summary>
/// Creates a new collection that is the result of removing the specified <paramref name="positions"/>
/// </summary>
public OptionPositionCollection RemoveRange(IEnumerable<OptionPosition> positions)
{
return AddRange(positions.Select(position => position.Negate()));
}
/// <summary>
/// Slices this collection, returning a new collection containing only
/// positions with the specified <paramref name="right"/>
/// </summary>
public OptionPositionCollection Slice(OptionRight right, bool includeUnderlying = true)
{
var rights = _rights.Remove(right.Invert());
var positions = ImmutableDictionary<Symbol, OptionPosition>.Empty;
if (includeUnderlying && HasUnderlying)
{
positions = positions.Add(Underlying, UnderlyingPosition);
}
var sides = ImmutableDictionary<PositionSide, ImmutableHashSet<Symbol>>.Empty;
var strikes = ImmutableSortedDictionary<decimal, ImmutableHashSet<Symbol>>.Empty;
var expirations = ImmutableSortedDictionary<DateTime, ImmutableHashSet<Symbol>>.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);
}
/// <summary>
/// Slices this collection, returning a new collection containing only
/// positions with the specified <paramref name="side"/>
/// </summary>
public OptionPositionCollection Slice(PositionSide side, bool includeUnderlying = true)
{
var otherSides = GetOtherSides(side);
var sides = _sides.Remove(otherSides[0]).Remove(otherSides[1]);
var positions = ImmutableDictionary<Symbol, OptionPosition>.Empty;
if (includeUnderlying && HasUnderlying)
{
positions = positions.Add(Underlying, UnderlyingPosition);
}
var rights = ImmutableDictionary<OptionRight, ImmutableHashSet<Symbol>>.Empty;
var strikes = ImmutableSortedDictionary<decimal, ImmutableHashSet<Symbol>>.Empty;
var expirations = ImmutableSortedDictionary<DateTime, ImmutableHashSet<Symbol>>.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);
}
/// <summary>
/// Slices this collection, returning a new collection containing only
/// positions matching the specified <paramref name="comparison"/> and <paramref name="strike"/>
/// </summary>
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<Symbol, OptionPosition>.Empty;
if (includeUnderlying)
{
OptionPosition underlyingPosition;
if (_positions.TryGetValue(Underlying, out underlyingPosition))
{
positions = positions.Add(Underlying, underlyingPosition);
}
}
var sides = ImmutableDictionary<PositionSide, ImmutableHashSet<Symbol>>.Empty;
var rights = ImmutableDictionary<OptionRight, ImmutableHashSet<Symbol>>.Empty;
var expirations = ImmutableSortedDictionary<DateTime, ImmutableHashSet<Symbol>>.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);
}
/// <summary>
/// Slices this collection, returning a new collection containing only
/// positions matching the specified <paramref name="comparison"/> and <paramref name="expiration"/>
/// </summary>
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<Symbol, OptionPosition>.Empty;
if (includeUnderlying)
{
OptionPosition underlyingPosition;
if (_positions.TryGetValue(Underlying, out underlyingPosition))
{
positions = positions.Add(Underlying, underlyingPosition);
}
}
var sides = ImmutableDictionary<PositionSide, ImmutableHashSet<Symbol>>.Empty;
var rights = ImmutableDictionary<OptionRight, ImmutableHashSet<Symbol>>.Empty;
var strikes = ImmutableSortedDictionary<decimal, ImmutableHashSet<Symbol>>.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);
}
/// <summary>
/// Returns the set of <see cref="OptionPosition"/> with the specified <paramref name="symbols"/>
/// </summary>
public IEnumerable<OptionPosition> ForSymbols(IEnumerable<Symbol> symbols)
{
foreach (var symbol in symbols)
{
OptionPosition position;
if (_positions.TryGetValue(symbol, out position))
{
yield return position;
}
}
}
/// <summary>
/// Returns the set of <see cref="OptionPosition"/> with the specified <paramref name="right"/>
/// </summary>
public IEnumerable<OptionPosition> ForRight(OptionRight right)
{
ImmutableHashSet<Symbol> symbols;
return _rights.TryGetValue(right, out symbols)
? ForSymbols(symbols)
: Enumerable.Empty<OptionPosition>();
}
/// <summary>
/// Returns the set of <see cref="OptionPosition"/> with the specified <paramref name="side"/>
/// </summary>
public IEnumerable<OptionPosition> ForSide(PositionSide side)
{
ImmutableHashSet<Symbol> symbols;
return _sides.TryGetValue(side, out symbols)
? ForSymbols(symbols)
: Enumerable.Empty<OptionPosition>();
}
/// <summary>
/// Returns the set of <see cref="OptionPosition"/> with the specified <paramref name="strike"/>
/// </summary>
public IEnumerable<OptionPosition> ForStrike(decimal strike)
{
ImmutableHashSet<Symbol> symbols;
return _strikes.TryGetValue(strike, out symbols)
? ForSymbols(symbols)
: Enumerable.Empty<OptionPosition>();
}
/// <summary>
/// Returns the set of <see cref="OptionPosition"/> with the specified <paramref name="expiration"/>
/// </summary>
public IEnumerable<OptionPosition> ForExpiration(DateTime expiration)
{
ImmutableHashSet<Symbol> symbols;
return _expirations.TryGetValue(expiration, out symbols)
? ForSymbols(symbols)
: Enumerable.Empty<OptionPosition>();
}
/// <summary>Returns a string that represents the current object.</summary>
/// <returns>A string that represents the current object.</returns>
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";
}
/// <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<OptionPosition> GetEnumerator()
{
return _positions.Select(kvp => kvp.Value).GetEnumerator();
}
/// <summary>
/// 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.
/// </summary>
internal IEnumerable<string> 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<Symbol> strikes;
if (!_strikes.TryGetValue(position.Strike, out strikes) || !strikes.Contains(symbol))
{
yield return $"{position}: Not indexed by strike price";
}
ImmutableHashSet<Symbol> 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();
}
/// <summary>
/// OptionPositionCollection + Operator
/// </summary>
/// <param name="positions">Collection to add to</param>
/// <param name="position">OptionPosition to add</param>
/// <returns>OptionPositionCollection with the new position added</returns>
public static OptionPositionCollection operator+(OptionPositionCollection positions, OptionPosition position)
{
return positions.Add(position);
}
/// <summary>
/// OptionPositionCollection - Operator
/// </summary>
/// <param name="positions">Collection to remove from</param>
/// <param name="position">OptionPosition to remove</param>
/// <returns>OptionPositionCollection with the position removed</returns>
public static OptionPositionCollection operator-(OptionPositionCollection positions, OptionPosition position)
{
return positions.Remove(position);
}
}
}