Open jaredbroad opened 1 week ago
@LouisSzeto I'll take this one
Hi @LouisSzeto I am stuck with the 2nd example. My approach for this is:
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)
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)
Awesome @LouisSzeto Thanks for the help.
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 :)
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
For the new examples in the docs:
<h3>Examples</h3>
<p>The following examples demonstrate some common practices for _________.</p>
<h4>Example _: ____________</h4>
for each Example.<h4>Other Examples</h4>
. The sentence under that heading should read<p>For more examples, see the following algorithms:</p>