Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,8 @@ public OptionStrategyMatch MatchOnce(OptionPositionCollection positions)
return bestMatch;
}

// the second candidate deprioritizes definitions leaving a short leg uncovered within the strategy
// (naked calls/puts, ladders, short backspreads/straddles/strangles), so short positions are matched
// into covered strategies whenever another grouping of the same positions allows it. this avoids
// greedily carving, for instance, two overlapping bull call spreads into a bull call ladder, whose
// uncovered short leg is charged naked option margin, plus an unmatched long contract
// the second candidate favors definitions with balanced option legs after underlying coverage.
// surplus longs within a strategy can consume the cover needed by shorts elsewhere in the book
var candidateMatch = Match(Options.CoveredShortsFirstDefinitions, positions, out unmatched);
var candidateScore = Options.ObjectiveFunction.ComputeScore(positions, candidateMatch, unmatched);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ public IEnumerable<OptionStrategyDefinition> Definitions
=> _enumeratedDefinitions ??= _definitionEnumerator.Enumerate(_definitions).ToList();

/// <summary>
/// The definitions to be used for matching, deprioritizing those leaving a short option leg uncovered
/// The definitions to be used for matching, favoring balanced option legs after accounting for underlying lots
/// </summary>
public IEnumerable<OptionStrategyDefinition> CoveredShortsFirstDefinitions
=> _coveredShortsFirstDefinitions ??= Definitions.OrderBy(HasUncoveredShortLeg).ToList();
=> _coveredShortsFirstDefinitions ??= Definitions.OrderBy(HasUnbalancedLegs).ToList();

/// <summary>
/// Objective function used to compare different match solutions for a given set of positions/definitions
Expand Down Expand Up @@ -136,11 +136,10 @@ public int GetMaximumLegMatches(int legIndex)
}

/// <summary>
/// Determines whether the definition, matched at the unit level, leaves a short option leg which isn't
/// covered by long legs of the same right or by the underlying lots the definition requires. Only ever
/// evaluated while building <see cref="CoveredShortsFirstDefinitions"/>, which is cached
/// Determines whether the definition leaves uncovered shorts or surplus longs of either right,
/// after accounting for the underlying lots required to cover short legs
/// </summary>
private static bool HasUncoveredShortLeg(OptionStrategyDefinition definition)
private static bool HasUnbalancedLegs(OptionStrategyDefinition definition)
{
var netCalls = 0;
var netPuts = 0;
Expand All @@ -157,7 +156,7 @@ private static bool HasUncoveredShortLeg(OptionStrategyDefinition definition)
}

// long underlying lots cover short calls, short underlying lots cover short puts
return -netCalls > Math.Max(0, definition.UnderlyingLots) || -netPuts > Math.Max(0, -definition.UnderlyingLots);
return -netCalls != Math.Max(0, definition.UnderlyingLots) || -netPuts != Math.Max(0, -definition.UnderlyingLots);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,28 @@ public void FullyCoveredOverlappingDebitSpreadsBookRequiresNoMaintenanceMargin()
Assert.AreEqual(0, _portfolio.TotalMarginUsed);
}

[TestCase(OptionRight.Put)]
[TestCase(OptionRight.Call)]
public void CoveredCreditSpreadsRequireOnlyTheirCombinedStrikeWidths(OptionRight right)
{
_equity.SetMarketPrice(new Tick { Value = 500 });
var expiry = new DateTime(2025, 2, 21);
var direction = right == OptionRight.Put ? -1 : 1;
foreach (var offset in new[] { 0, 5 })
{
var shortOption = _algorithm.AddOptionContract(Symbols.CreateOptionSymbol("SPY", right,
500 + direction * offset, expiry));
var longOption = _algorithm.AddOptionContract(Symbols.CreateOptionSymbol("SPY", right,
500 + direction * (offset + 30), expiry));
shortOption.SetMarketPrice(new Tick { Value = 5 });
longOption.SetMarketPrice(new Tick { Value = 2 });
shortOption.Holdings.SetHoldings(shortOption.Price, -1);
longOption.Holdings.SetHoldings(longOption.Price, 1);
}

Assert.AreEqual(6000, _portfolio.TotalMarginUsed);
}

[Test]
public void OverlappingDebitSpreadOrderRequiresOnlyPremium()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,30 @@ public void MatchesOverlappingDebitSpreadsAsSpreadsInsteadOfLadder()
Assert.AreEqual(4, match.Strategies.Sum(strategy => strategy.OptionLegs.Count));
}

[TestCase(OptionRight.Put, false)]
[TestCase(OptionRight.Put, true)]
[TestCase(OptionRight.Call, false)]
[TestCase(OptionRight.Call, true)]
public void MatchesCoveredCreditSpreadsWithoutLeavingANakedShort(OptionRight right, bool overlapping)
{
var options = right == OptionRight.Put ? Put : Call;
var direction = right == OptionRight.Put ? -1 : 1;
var secondShort = overlapping ? 5 : 35;
var positions = OptionPositionCollection.Empty.AddRange(
Position(options[1000], -1),
Position(options[1000 + direction * 30]),
Position(options[1000 + direction * secondShort], -1),
Position(options[1000 + direction * (secondShort + 30)])
);
var matcher = new OptionStrategyMatcher(OptionStrategyMatcherOptions.ForDefinitions(AllDefinitions));
var match = matcher.MatchOnce(positions);
Assert.AreEqual(2, match.Strategies.Count);
Assert.IsTrue(match.Strategies.All(strategy => strategy.OptionLegs.Count == 2 &&
strategy.OptionLegs.Sum(leg => leg.Quantity) == 0),
string.Join(", ", match.Strategies.Select(strategy => strategy.Name)));
Assert.AreEqual(4, match.Strategies.Sum(strategy => strategy.OptionLegs.Count));
}

[Test]
public void MatchLeavesNoShortContractUncoveredWhenFullCoverageExists()
{
Expand Down