Options Models

Assignment

Introduction

If you sell an Option in a backtest, an assignment model can simulate an Option exercise order on behalf of the buyer and assign you to complete the requirements of the contract.

Set Models

To set the assignment model of an Option, call the SetOptionAssignmentModelset_option_assignment_model method of the Option object inside a security initializer.

public class BrokerageModelExampleAlgorithm : QCAlgorithm
{
    public override void Initialize()
    {
        // In the Initialize method, set the security initializer to seed initial the prices and models of assets.
        SetSecurityInitializer(new MySecurityInitializer(BrokerageModel, new FuncSecuritySeeder(GetLastKnownPrices)));
    }
}

public class MySecurityInitializer : BrokerageModelSecurityInitializer
{
    public MySecurityInitializer(IBrokerageModel brokerageModel, ISecuritySeeder securitySeeder)
        : base(brokerageModel, securitySeeder) {}    
    public override void Initialize(Security security)
    {
        // First, call the superclass definition.
        // This method sets the reality models of each security using the default reality models of the brokerage model.
        base.Initialize(security);

        // Next, overwrite the assignment model
        if (security.Type == SecurityType.Option) // Option type
        {
            (security as Option).SetOptionAssignmentModel(new DefaultOptionAssignmentModel());
        }    }
}
class BrokerageModelExampleAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        # In the Initialize method, set the security initializer to seed initial the prices and models of assets.
        self.set_security_initializer(MySecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))

# Outside of the algorithm class
class MySecurityInitializer(BrokerageModelSecurityInitializer):

    def __init__(self, brokerage_model: IBrokerageModel, security_seeder: ISecuritySeeder) -> None:
        super().__init__(brokerage_model, security_seeder)    
    def initialize(self, security: Security) -> None:
        # First, call the superclass definition.
        # This method sets the reality models of each security using the default reality models of the brokerage model.
        super().initialize(security)

        # Next, overwrite the assignment model
        if security.Type == SecurityType.OPTION: # Option type
            security.set_option_assignment_model(DefaultOptionAssignmentModel())

Default Behavior

The default Option assignment model is the DefaultOptionAssignmentModel. The DefaultOptionAssignmentModel scans your portfolio every hour. It considers exercising American-style Options if they are within 4 days of their expiration and it considers exercising European-style Options on their day of expiration. If you have sold an Option that's 5% in-the-money and the Option exercise order is profitable after the cost of fees, this model exercises the Option.

To view the implementation of this model, see the LEAN GitHub repository.

Model Structure

Option assignment models should implement the IOptionAssignmentModel interface. Extensions of the IOptionAssignmentModel interface must implement the GetAssignmentget_assignment method, which automatically fires at the top of each hour and returns the Option assignments to generate.

Option assignment models should extend the NullOptionAssignmentModel class. Extensions of the NullOptionAssignmentModel class must implement the GetAssignmentget_assignment method, which automatically fires at the top of each hour and returns the Option assignments to generate.

public class CustomOptionAssignmentModelExampleAlgorithm : QCAlgorithm
{
    public override void Initialize()
    {
        var security = AddOption("SPY");
        // Set custom option assignment model for mimicking specific Brokerage most realistic actions
        (security as Option).SetOptionAssignmentModel(new MyOptionAssignmentModel());
    }
}

// Define the custom Option assignment model outside of the algorithm
public class MyOptionAssignmentModel : IOptionAssignmentModel
{
    public OptionAssignmentResult GetAssignment(OptionAssignmentParameters parameters)
    {
        var option = parameters.Option;
        // Check if the contract is ITM
        if ((option.Right == OptionRight.Call && option.Underlying.Price > option.StrikePrice) ||
            (option.Right == OptionRight.Put && option.Underlying.Price < option.StrikePrice))
        {
            return new OptionAssignmentResult(option.Holdings.AbsoluteQuantity, "MyTag");
        }
        return OptionAssignmentResult.Null;
    }
}
class CustomOptionAssignmentModelExampleAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        security = self.add_option("SPY")
        # Set custom option assignment model for mimicking specific Brokerage most realistic actions
        security.set_option_assignment_model(MyOptionAssignmentModel())

# Define the custom Option assignment model outside of the algorithm
class MyOptionAssignmentModel(NullOptionAssignmentModel):

    def get_assignment(self, parameters: OptionAssignmentParameters) -> OptionAssignmentResult:
        option = parameters.option
        # Check if the contract is ITM
        if option.right == OptionRight.CALL and option.underlying.price > option.strike_price
            or option.right == OptionRight.PUT and option.underlying.price < option.strike_price:
            return OptionAssignmentResult(option.holdings.absolute_quantity, "MyTag")
        return OptionAssignmentResult.NULL

For a full example algorithm, see this backtestthis backtest.

The OptionAssignmentParameters object has the following members:

To exercise the Option, return an OptionAssignmentResult with a positive quantity. Otherwise, return OptionAssignmentResult.Null. The OptionAssignmentResult constructor accepts the following arguments:

ArgumentData TypeDescriptionDefault Value
quantitydecimalfloatThe quantity to assign
tagstringstrThe order tag to use

Disable Assignments

To disable early Option assignments, set the Option assignment model to the NullOptionAssignmentModel.

(security as Option).SetOptionAssignmentModel(new NullOptionAssignmentModel());
security.set_option_assignment_model(NullOptionAssignmentModel())

Examples

The following examples demonstrate some common practices for handling Option assignment.

Example 1: Liquidate After Assignment

The following algorithm trades a short straddle strategy on ATM SPY Options that expire within seven days. To capitalize the cash profit, it liquidates the underlying assigned Equity position in the OnAssignmentOrderEventon_assignment_order_event method.

public class OptionAssignmentAlgorithm : QCAlgorithm
{
    private Option _option;

    public override void Initialize()
    {
        SetStartDate(2024, 4, 1);
        SetEndDate(2024, 5, 1);

        // Add SPY Options data for trading.
        _option = AddOption("SPY");
        // Filter for the 2 ATM contracts that expire in 7 days to form a straddle.
        _option.SetFilter((universe) => universe.IncludeWeeklys().Straddle(7));
    }

    public override void OnData(Slice slice)
    {
        // Get the current Option chain and open a new position if we're not already invested.
        if (!Portfolio.Invested && slice.OptionChains.TryGetValue(_option.Symbol, out var chain))
        {
            // Select the strike and expiry of the contracts.
            var strike = chain.Min(x => x.Strike);
            var expiry = chain.Min(x => x.Expiry);
            // Open the straddle position.
            var optionStrategy = OptionStrategies.ShortStraddle(_option.Symbol, strike, expiry);
            Buy(optionStrategy, 2);
        }
    }

    public override void OnAssignmentOrderEvent(OrderEvent assignmentEvent)
    {
        // Liquidate the assigned SPY position.
        MarketOrder(
            assignmentEvent.Symbol.Underlying, 
            -assignmentEvent.Quantity * _option.SymbolProperties.ContractMultiplier
        );
    }
}
class OptionAssignmentAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2024, 4, 1)
        self.set_end_date(2024, 5, 1)
    
        # Add SPY Options data for trading.
        self._option = self.add_option("SPY")
        # Filter for the 2 ATM contracts that expire in 7 days to form a straddle.
        self._option.set_filter(lambda universe: universe.include_weeklys().straddle(7))
        
    def on_data(self, slice: Slice) -> None:
        # Get the current Option chain.
        chain = slice.option_chains.get(self._option.symbol)
        # Open a new position if we're not already invested.
        if chain and not self.portfolio.invested:
            # Select the strike and expiry of the contracts.
            strike = list(chain)[0].strike
            expiry = list(chain)[0].expiry
            # Open the straddle position.
            option_strategy = OptionStrategies.short_straddle(self._option.symbol, strike, expiry)
            self.buy(option_strategy, 2)

    def on_assignment_order_event(self, assignment_event: OrderEvent) -> None:
        # Liquidate the assigned SPY position.
        self.market_order(
            assignment_event.symbol.underlying, 
            -assignment_event.quantity * self._option.symbol_properties.contract_multiplier
        )

Example 2: Wheel Strategy

The following algorithm demonstrates a wheel Option strategy. The strategy is split into separate parts to generate consistent income: selling puts and selling covered call.

public class WheelStrategyAlgorithm : QCAlgorithm
{
    // Define the OTM threshold. A greater value means lower risk of assignment but lower premiums to collect.
    private readonly decimal _otmThreshold = 0.05m;
    private Equity _equity;

    public override void Initialize()
    {
        SetStartDate(2020, 6, 1);
        SetEndDate(2021, 6, 1);
        SetCash(1000000);

        // Seed the initial price of the each new security so we can add and trade contracts in the same time step.
        SetSecurityInitializer(
            new BrokerageModelSecurityInitializer(BrokerageModel, new FuncSecuritySeeder(GetLastKnownPrices))
        );
        // Add SPY data for trading. Use raw data normalization mode to enable fair strike price comparison.
        _equity = AddEquity("SPY", dataNormalizationMode: DataNormalizationMode.Raw);
    }

    private Symbol GetTargetContract(OptionRight right, decimal targetPrice)
    {
        // Get the Option chain.
        var chain = OptionChain(_equity.Symbol);
        // Select the contracts that are first to expire after 30 days since they have greater premiums and theta 
        // to collect.
        var expiry = chain.Where(x => x.Expiry > Time.AddDays(30))
            .Min(x => x.Expiry);
        // Select the OTM Option contract to start the wheel or covered call.
        Symbol symbol;
        if (right == OptionRight.Call)
        {
            symbol = chain.Where(x => x.Expiry == expiry && x.Right == right && x.Strike >= targetPrice)
                .OrderBy(x => x.Strike)
                .First();
        }
        else
        {
            symbol = chain.Where(x => x.Expiry == expiry && x.Right == right && x.Strike <= targetPrice)
                .OrderByDescending(x => x.Strike)
                .First();
        }
        // Add the Option contract to the algorithm.
        return AddOptionContract(symbol).Symbol;
    }

    public override void OnData(Slice slice)
    {
        // Start the wheel by selling the OTM put.
        if (!Portfolio.Invested && IsMarketOpen(_equity.Symbol))
        {
            var symbol = GetTargetContract(OptionRight.Put, _equity.Price * (1m - _otmThreshold));
            SetHoldings(symbol, -0.2m);
        }
    }

    public override void OnAssignmentOrderEvent(OrderEvent assignmentEvent)
    {
        // After the put is assigned, look for an OTM call to form a covered call with the assigned underlying 
        // position.
        var symbol = GetTargetContract(OptionRight.Call, _equity.Price * (1m + _otmThreshold));
        MarketOrder(symbol, -_equity.Holdings.Quantity / 100m);
    }
}
class WheelStrategyAlgorithm(QCAlgorithm):
    # Define the OTM threshold. A greater value means lower risk of assignment but lower premiums to collect.
    _otm_threshold = 0.05

    def initialize(self):
        self.set_start_date(2020, 6, 1)
        self.set_end_date(2021, 6, 1)
        self.set_cash(1000000)

        # Seed the initial price of the each new security so we can add and trade contracts in the same time step.
        self.set_security_initializer(
            BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))
        )
        # Add SPY data for trading. Use raw data normalization mode to enable fair strike price comparison.
        self._equity = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW)
        
    def _get_target_contract(self, right: OptionRight, target_price: float) -> Symbol:
        # Get the Option chain.
        chain = self.option_chain(self._equity.symbol, flatten=True).data_frame
        # Select the contracts that are first to expire after 30 days since they have greater premiums and theta 
        # to collect.
        expiry_threshold = self.time + timedelta(30)
        expiry = chain[chain.expiry > expiry_threshold].expiry.min()
        # Select the OTM Option contract to start the wheel or covered call.
        symbol = chain[
            (chain.expiry == expiry) &
            (chain.right == right) &
            (chain.strike <= target_price if right == OptionRight.PUT else chain.strike >= target_price)
        ].sort_values('strike', ascending=right == OptionRight.CALL).index[0]
        # Add the Option contract to the algorithm.
        return self.add_option_contract(symbol).symbol

    def on_data(self, slice: Slice) -> None:
        # Start the wheel by selling the OTM put.
        if not self.portfolio.invested and self.is_market_open(self._equity.symbol):
            symbol = self._get_target_contract(OptionRight.PUT, self._equity.price * (1-self._otm_threshold))
            self.set_holdings(symbol, -0.2)
    
    def on_assignment_order_event(self, assignment_event: OrderEvent) -> None:
        # After the put is assigned, look for an OTM call to form a covered call with the assigned underlying 
        # position.
        symbol = self._get_target_contract(OptionRight.CALL, self._equity.price * (1+self._otm_threshold))
        self.market_order(symbol, -self._equity.holdings.quantity / 100)

Other Examples

For more examples, see the following algorithms:

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: