Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jaredbroad opened this issue Sep 10, 2024 · 6 comments · Fixed by #1911
Open

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

jaredbroad opened this issue Sep 10, 2024 · 6 comments · Fixed by #1911
Assignees

Comments

@jaredbroad
Copy link
Member

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>
@baobach
Copy link
Contributor

baobach commented Sep 13, 2024

@LouisSzeto I'll take this one

@baobach
Copy link
Contributor

baobach commented Sep 17, 2024

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
Copy link
Collaborator

LouisSzeto commented Sep 17, 2024

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
Copy link
Contributor

baobach commented Sep 17, 2024

Awesome @LouisSzeto Thanks for the help.

@LouisSzeto
Copy link
Collaborator

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

AlexCatarino added a commit that referenced this issue Nov 15, 2024
Remove second example since it doesn't address #1901.
@AlexCatarino AlexCatarino reopened this Nov 15, 2024
@AlexCatarino
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants