QuantConnect / Documentation

QuantConnect Wiki Style Documentation Behind QuantConnect
https://www.quantconnect.com/docs/v2/
Apache License 2.0
171 stars 134 forks source link

Securities / Asset Classes / Index Option / Requesting Data / Universes #1901

Open jaredbroad opened 1 week ago

jaredbroad commented 1 week ago

Examples

Remember


For the new examples in the docs:

baobach commented 5 days ago

@LouisSzeto I'll take this one

baobach commented 19 hours ago

Hi @LouisSzeto I am stuck with the 2nd example. My approach for this is:

  1. Create a universe with options that expire in range 30-90 days.
  2. Overwrite on security changed function to handle changes that liquidate changes.removed symbols. Request a list of options using changes.added symbols and filter for options that expire in 90 days to roll over the options. The return of self.option_chain_provider.get_option_contract_list(removed.symbol, self.time) is a list of option contracts and I can apply filters to this list. However, when I place an order, it doesn't work. Checking the log I found that the contract variable is not a Symbol object (Out put of the log SPX YG1PJ1L1CJU6|SPX 31) Example code:

    class PythonTest(QCAlgorithm):
    
    def initialize(self) -> None:
        self.set_start_date(2023,8,1)
        self.set_end_date(2024,1,1)
        self.set_cash(100_000)
        # Subscribe to the option chain.
        self._option = self.add_index_option("SPX", Resolution.DAILY)
        # Select options that have expiry within 30 to 90 days.
        self._option.set_filter(timedelta(30), timedelta(90))
        # ATM strike price
        self._strike = 0
        self.atm_call = None
    
    def on_data(self, slice: Slice) -> None:
        if self.portfolio.invested:
            return
    
        chain = slice.option_chains.get(self._option.symbol)
        if not chain:
            return
    
        calls = [contract for contract in chain if contract.right == OptionRight.CALL]
        self.atm_call = sorted(calls, key=lambda x: abs(chain.underlying.price - x.strike))[0]
        self._strike = self.atm_call.strike
        self.log(f"Buy option with expiry: {self.atm_call.expiry}, and strike price: {self.atm_call.strike}")
    
        if self.atm_call and not self.portfolio[self.atm_call.symbol].invested:
            self.market_order(self.atm_call.symbol, 1)
    
    def on_securities_changed(self, changes: SecurityChanges) -> None:
    
        for removed in changes.removed_securities:
            if removed.symbol == self.atm_call.symbol:
                option_chain = self.option_chain_provider.get_option_contract_list(removed.symbol, self.time)
                target_expiry = self.time + timedelta(90)
                contracts = [contract for contract in option_chain if contract.id.strike_price == self._strike and 85 <= (contract.id.date - target_expiry).days <= 95 and contract.id.option_right == OptionRight.CALL]
                if not contracts: return
                contract = contracts[0]
                # self.liquidate(self.atm_call.symbol)
                # self.market_order(contract.value, 1)
                self.log(contract)
LouisSzeto commented 12 hours ago

Hi @baobach

I believe most of your logic is correct. The on_securities_changed part is a bit over-complicated. You don't really need to roll over in there but just rely on your universe filter and on_data handler, as you have to order the liqudation and the next contract in the next market open after all. Since index options are European options (will not be exercised since we never leave them until expiry) and cash-settled (even leave till exercised, it just affect the cash book), it saves us the extra work on handling the option exercise/assignment like equity options.

CSharp:

namespace QuantConnect.Algorithm.CSharp
{
    public class SampleAlgorithm : QCAlgorithm
    {
        private Option _indexOption;

        public override void Initialize()
        {
            // Subscribe to the index option and filter to get only the ones expiring in 30-90 days
            _indexOption = AddIndexOption("SPX", "SPXW");
            _indexOption.SetFilter((u) => u.IncludeWeeklys().CallsOnly().Expiration(30, 90));
        }

        public override void OnData(Slice slice)
        {
            // Get option chain data for the canonical symbol
            if (!Portfolio.Invested && 
                slice.OptionChains.TryGetValue(_indexOption.Symbol, out var chain))
            {
                // Obtain the ATM call that expires furthest (90 days)
                var expiry = chain.Max(x => x.Expiry);
                var atmCall = chain.Where(x => x.Expiry == expiry)
                    .OrderBy(x => Math.Abs(x.Strike - x.UnderlyingLastPrice))
                    .First();
                // Allocate 10% Capital
                SetHoldings(atmCall.Symbol, 0.1m);
            }
        }

        public override void OnSecuritiesChanged(SecurityChanges changes)
        {
            foreach (var removed in changes.RemovedSecurities)
            {
                // Liquidate the contracts that exit the universe (due to expiry)
                if (Portfolio[removed.Symbol].Invested)
                {
                    Liquidate(removed.Symbol);
                }
            }
        }
    }
}

Python:

class TestAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        # Subscribe to the index option and filter to get only the ones expiring in 30-90 days
        self.index_option = self.add_index_option("SPX", "SPXW")
        self.index_option.set_filter(lambda u: u.include_weeklys().calls_only().expiration(30, 90))

    def on_data(self, slice: Slice) -> None:
        # Get option chain data for the canonical symbol
        chain = slice.option_chains.get(self.index_option.symbol)
        if not self.portfolio.invested and chain:
            # Obtain the ATM call that expires furthest (90 days)
            expiry = max(x.expiry for x in chain)
            atm_call = sorted([x for x in chain if x.expiry == expiry],
                key=lambda x: abs(x.strike - x.underlying_last_price))[0]
            # Allocate 10% Capital
            self.set_holdings(atm_call.symbol, 0.1)

    def on_securities_changed(self, changes):
        for removed in changes.removed_securities:
            # Liquidate the contracts that exit the universe (due to expiry)
            if self.portfolio[removed.symbol].invested:
                self.liquidate(removed.symbol)
baobach commented 12 hours ago

Awesome @LouisSzeto Thanks for the help.

LouisSzeto commented 12 hours ago

Since index options are European options (will not be exercised since we never leave them until expiry) and cash-settled (even leave till exercised, it just affect the cash book), it saves us the extra work on handling the option exercise/assignment like equity options.

@baobach you may add this in the description as well :)