Documentation icon indicating copy to clipboard operation
Documentation copied to clipboard

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

Open jaredbroad opened this issue 1 year ago • 5 comments

Examples

  • Example 1: Expand current 0DTE example 1 to include initialize and trade. Use the new Option universe filtering by greeks.

  • Example 2: Select small universe +30, +90 expiry. In on_security_changed event handler liquidate on removed from universe. Buy the next one 90d out. Goal to demonstrate symbol changed events + rolling contracts. Allocate 10% capital.

Remember

  • Don't use variables that aren't defined.
  • Comment every line or few lines.
  • Include 40-100 word explanation.
  • Declare everything you need each example.
  • Copy from IDE to Docs so confirm it works. Copy-pastable examples.
  • ALWAYS make Examples.html at the end.

For the new examples in the docs:

  • There should be a sentence at the beginning of the <h3>Examples</h3>
    • <p>The following examples demonstrate some common practices for _________.</p>
  • Have 1 <h4>Example _: ____________</h4> for each Example.
    • The example should highlight the concepts that were explained on the respective documentation page.
    • Avoid creating tutorial-style content for the text. The text between should at least have a sentence that explains what the algorithm does. "The following algorithm __________:"
    • If there is an opportunity to link some of the text to other pages/sections in the docs, please link it.
    • The example should be a full algorithm in 1 code block, with conventional styling (PEP8 or typical C#; Ask Grok/ChatGPT to help you style/format the code if you need).
    • Test that you can run each example algorithm in QC Cloud w/o error before adding it to the docs.
  • If we have a list of Demonstration Algorithms, put them all under <h4>Other Examples</h4>. The sentence under that heading should read <p>For more examples, see the following algorithms:</p>

jaredbroad avatar Sep 10 '24 21:09 jaredbroad

@LouisSzeto I'll take this one

baobach avatar Sep 13 '24 00:09 baobach

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)

baobach avatar Sep 17 '24 04:09 baobach

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)

LouisSzeto avatar Sep 17 '24 12:09 LouisSzeto

Awesome @LouisSzeto Thanks for the help.

baobach avatar Sep 17 '24 12:09 baobach

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 :)

LouisSzeto avatar Sep 17 '24 12:09 LouisSzeto

@LouisSzeto , I reopened it because the 2nd example is incorrect, and I removes it ( b735dc3). This code does nothing since LEAN automatically remove expired contracts.

    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)

AlexCatarino avatar Nov 15 '24 21:11 AlexCatarino