Skip to content

Unequal-wing iron condor maintenance uses the put width when the call wing is wider #9775

Description

@rahulpowar

Unequal-wing iron condor maintenance uses the put width when the call wing is wider

An isolated, zero-order Cloud probe on LEAN 2.5.0.0.18057 returns maintenance margin based only on the put wing for an unequal-wing credit iron condor. The native resolver recognizes the position as one Iron Condor. When the call wing is wider, the returned maintenance is below the gross expiration loss of the four-leg position.

Observed results

All contracts are European SPXW options expiring June 8, 2026. Every position group has quantity 2, with contract multipliers of 100. The probe creates position objects for the resolver; it never changes holdings or submits orders.

Case Long put Short put Short call Long call Put/call widths Native maintenance Gross expiration loss
Equal wings 7090 7185 7510 7605 95 / 95 $19,000 $19,000
Call wing wider 7135 7185 7500 7660 50 / 160 $10,000 $32,000
Put wing wider 7090 7235 7510 7605 145 / 95 $29,000 $29,000

Gross expiration loss is computed independently from the piecewise four-leg payoff at every strike and both tails. It excludes entry premium and fees. These figures measure LEAN native maintenance, not actual brokerage margin.

Expected behavior and source

For this recognized credit condor, the width-based maintenance calculation should account for the wider loss-bearing wing. Both call and put spread helpers already exist in the buying-power model. The condor maintenance branch at the tested LEAN commit calls only GetShortPutLongPutStrikeDifferenceMargin.

Using the maximum of the existing call and put helper results appears to address this credit-condor case while retaining their quantity, contract-unit and currency handling. Reverse/debit strategies, butterflies and other regressions still require native validation before adopting that change.

The initial-margin branch uses the same put helper, but this report directly tests maintenance only. Process-dependent grouping is separately tracked in issue 9648; this calculation issue reproduces with a single recognized group and does not rely on choosing between multiple groupings.

Reproducer

This Python algorithm compiled without warnings and completed the one-session probe with zero orders. Its cash balance is an arbitrary test configuration; no margin is consumed by actual holdings.

from AlgorithmImports import *
from datetime import datetime
import json

from QuantConnect.Securities.Positions import (
    IPosition,
    Position,
    PositionCollection,
    OptionStrategyPositionGroupResolver,
    PositionGroupMaintenanceMarginParameters,
)

CASES = (
    ("equal", (7090, 7185, 7510, 7605)),
    ("call_wider", (7135, 7185, 7500, 7660)),
    ("put_wider", (7090, 7235, 7510, 7605)),
)
QUANTITY = 2


class UnequalWingMarginProbe(QCAlgorithm):
    def initialize(self):
        if self.live_mode:
            raise RuntimeError("Unequal-wing probe supports backtests only")
        self.set_start_date(2026, 6, 8)
        self.set_end_date(2026, 6, 8)
        self.set_cash(200000)
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
        spy = self.add_equity("SPY", Resolution.DAILY).symbol
        self.set_benchmark(spy)
        underlying = self.add_index("SPX", Resolution.MINUTE).symbol
        self._probe_cases = []
        subscribed = set()
        resolver = OptionStrategyPositionGroupResolver(self.securities)
        for name, strikes in CASES:
            native_positions: list[IPosition] = []
            symbols = []
            for index, strike in enumerate(strikes):
                right = OptionRight.PUT if index < 2 else OptionRight.CALL
                sign = 1 if index in (0, 3) else -1
                symbol = Symbol.create_option(underlying, "SPXW", Market.USA,
                    OptionStyle.EUROPEAN, right, strike, datetime(2026, 6, 8))
                if symbol not in subscribed:
                    self.add_index_option_contract(symbol, Resolution.MINUTE)
                    subscribed.add(symbol)
                native_positions.append(Position(symbol, sign * QUANTITY, 1))
                symbols.append(symbol)
            groups = list(resolver.resolve(PositionCollection(native_positions)))
            if len(groups) != 1 or str(groups[0].buying_power_model) != "Iron Condor":
                raise RuntimeError(f"Expected exactly one native Iron Condor for {name}")
            group = groups[0]
            if float(group.quantity) != QUANTITY:
                raise RuntimeError("Native resolver changed the group quantity")
            margin = group.buying_power_model.get_maintenance_margin(
                PositionGroupMaintenanceMarginParameters(self.portfolio, group))
            multipliers = [float(self.securities[symbol].symbol_properties.contract_multiplier)
                for symbol in symbols]
            if multipliers != [100.0] * 4:
                raise RuntimeError(f"Unexpected multipliers: {multipliers}")
            call_width = strikes[3] - strikes[2]
            put_width = strikes[1] - strikes[0]
            payoff = []
            for spot in (0, *strikes, strikes[3] + 1000):
                value = (max(strikes[0] - spot, 0) - max(strikes[1] - spot, 0)
                    - max(spot - strikes[2], 0) + max(spot - strikes[3], 0))
                payoff.append(value * QUANTITY * 100)
            gross_loss = -min(payoff)
            if gross_loss != max(call_width, put_width) * QUANTITY * 100:
                raise RuntimeError("Piecewise payoff and width formula disagree")
            self._probe_cases.append(dict(case=name, strikes=strikes, quantity=QUANTITY,
                call_width=call_width, put_width=put_width,
                native_maintenance_margin=float(margin.value), gross_expiry_loss=gross_loss,
                multipliers=multipliers))

    def on_end_of_algorithm(self):
        self.set_summary_statistic("Wing probe", json.dumps(self._probe_cases, separators=(",", ":")))
        self.set_summary_statistic("Wing probe status", "completed_no_orders")

Can the team confirm the intended unequal-wing maintenance treatment and provide a supported Cloud correction? I can repeat the three fixed cases and the separate grouping reproducer against an updated engine.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions