Skip to content

Add margin-aware option strategy match selection - #9639

Open
AlexCatarino wants to merge 1 commit into
QuantConnect:masterfrom
AlexCatarino:support/215475229332236-margin-aware-option-grouping
Open

Add margin-aware option strategy match selection#9639
AlexCatarino wants to merge 1 commit into
QuantConnect:masterfrom
AlexCatarino:support/215475229332236-margin-aware-option-grouping

Conversation

@AlexCatarino

Copy link
Copy Markdown
Member

Description

OptionStrategyMatcher.MatchOnce greedily matches strategy definitions in descending leg-count order and never consults the IOptionStrategyMatchObjectiveFunction hook (the long-standing TODO at the top of the class). Because every buying power check re-resolves the whole per-underlying option book through this greedy matching, a portfolio holding only fully-covered debit spreads can get carved into groups containing uncovered short legs, which are charged naked option margin.

Minimal case (from our reproduction, daily 1-contract ATM $5-wide SPY bull call spreads via ComboMarketOrder): holding 598C +1, 603C -1 and ordering 600C +1, 605C -1 (same expiry, SPY ≈ 600) re-groups the combined book into a Bull Call Ladder (600, 603, 605) plus an orphan 598C long. The ladder's second short is charged naked call margin ≈ premium + 20% × underlying ≈ $12k, so a ~$295 net-debit, defined-risk order is rejected with Maintenance Margin Delta ≈ $12.5k. The correct grouping — two Bull Call Spreads — requires no margin beyond the premium, and LEAN's own spread margin formulas agree.

Consequences observed: inconsistent accept/reject on identical trades, day-to-day TotalMarginUsed churn ($0 → $12,758 → $100 → $12,306 on consecutive days) as re-resolution flips between carvings, spurious margin calls, and even long-only option buys (max risk = premium) rejected with ~$12k deltas that are purely re-grouping artifacts.

The change (implements the matcher's TODO):

  • MatchOnce now evaluates a fixed set of two candidate solutions and selects via the configured objective function:
    1. The legacy greedy match over the configured definition order — evaluated first and kept on ties, so every currently-correct grouping is unchanged.
    2. The same greedy match with definitions that leave a short leg uncovered within the strategy (naked calls/puts, ladders, short backspreads/straddles/strangles) deprioritized, so shorts are matched into covered strategies whenever the positions allow it.
  • New default objective function UncoveredShortQuantityOptionStrategyMatchObjectiveFunction: scores a solution by the negated quantity of short option contracts left uncovered, either inside their strategy or unmatched. Uncovered shorts are charged naked option margin, typically an order of magnitude above any risk-defined strategy margin, which makes this a cheap, deterministic proxy for total margin that doesn't require security prices inside the matcher. The previous default, UnmatchedPositionCountOptionStrategyMatchObjectiveFunction, was never consulted anywhere and remains available.
  • Performance/blast radius: when the first candidate has no uncovered shorts (score 0, the maximum) it is returned immediately, so covered books and single-strategy books take exactly the old path at no extra cost. Behavior only changes where the legacy carve left a short uncovered that another carve of the same positions covers.
  • Determinism: the candidate set is fixed-size with no time-budget dependence — identical inputs always produce identical groupings, in backtesting and live.

Why not just reorder definitions (spreads before ladders): a true butterfly book (+1 low, −2 mid, +1 high) would then decompose into a bull call spread plus a bear call spread, charging the bear spread's strike width where the butterfly requires none. Descending-leg-count is sometimes the cheaper carve, so the fix compares solutions instead of hard-coding an order; a butterfly regression test covers this.

Known proxy limitation: the score counts uncovered short contracts and treats any long-covered short as equally covered regardless of strike, so in pathological books (e.g. the only available covering long being extremely deep, with a strike width above ~20% of the underlying) the selected carve may not be the absolute cheapest — but it is always defined-risk rather than naked, and ties always preserve current behavior.

Intentionally untouched (root-cause items 2 and 3 of the issue, kept out to focus this PR):

  • Premiums are still not gated on cash in PositionGroupBuyingPowerModel.HasSufficientBuyingPowerForOrder (compares only against MarginRemaining).
  • OptionStrategyPositionGroupResolver.GetImpactedGroups still matches by underlying; it amplified this bug but is semantically defensible, and with margin-aware selection it becomes harmless.

Related Issue

Fixes #9638

Motivation and Context

Defined-risk option spread books are charged phantom naked-call margin, causing rejected orders, inconsistent margin usage, and spurious margin-call liquidations in backtesting and live. See the issue for the full analysis.

Requires Documentation Change

None.

How Has This Been Tested?

New tests:

  • Tests/Common/Securities/Options/StrategyMatcher/OptionStrategyMatcherTests.cs:
    • {598C +1, 600C +1, 603C -1, 605C -1} (same expiry) matches as two Bull Call Spreads, no ladder, no leftovers.
    • An interleaved book (longs 598×3, 600×2, 604×3, 608×2; shorts 603×3, 605×2, 609×2, 613×1, same expiry) leaves no short contract uncovered: no strategy holds net short calls and all 8 short contracts are matched.
    • A true butterfly book still matches as Butterfly Call (guards against naive definition reordering).
    • A real ladder book still matches as Bull Call Ladder (ties preserve legacy behavior).
  • Tests/Common/Securities/OptionStrategyPositionGroupBuyingPowerModelTests.cs:
    • Holding two overlapping bull call spreads → resolves as two Bull Call Spread groups with TotalMarginUsed == 0.
    • Ordering an overlapping spread with cash covering only its net debit passes HasSufficientBuyingPowerForOrder (used to require ~$12k).
    • A long-only call order against the covered spread book passes with cash covering only its premium.

dotnet build QuantConnect.Lean.sln -c Release builds clean. Targeted suites green: dotnet test Tests/QuantConnect.Tests.csproj --filter "FullyQualifiedName~StrategyMatcher|FullyQualifiedName~PositionGroup|FullyQualifiedName~OptionStrategy".

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • My code follows the code style of this project.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • My branch follows the naming convention bug-<issue#>-<description>

🤖 Generated with Claude Code

OptionStrategyMatcher.MatchOnce greedily matched definitions in
descending leg-count order, never consulting the objective function
hook. Books of overlapping debit spreads were carved into ladders
whose uncovered short leg is charged naked option margin, producing
phantom margin deltas, inconsistent accept/reject decisions and
TotalMarginUsed churn on fully covered, defined-risk books.

MatchOnce now evaluates a second candidate solution that deprioritizes
definitions leaving a short leg uncovered, and selects the best
solution via the objective function. The new default objective
function minimizes the quantity of uncovered short contracts, a
deterministic proxy for the margin required to hold the positions.
Ties preserve the previous grouping, so behavior only changes where
the greedy carve left a short uncovered that another grouping of the
same positions covers.

Fixes QuantConnect#9638

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant