Overall Statistics
Total Orders
6
Average Win
2.63%
Average Loss
-2.20%
Compounding Annual Return
111.611%
Drawdown
0.100%
Expectancy
0.097
Start Equity
100000
End Equity
100758
Net Profit
0.758%
Sharpe Ratio
9.268
Sortino Ratio
0
Probabilistic Sharpe Ratio
0%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.19
Alpha
-0.027
Beta
0.802
Annual Standard Deviation
0.035
Annual Variance
0.001
Information Ratio
-13.182
Tracking Error
0.009
Treynor Ratio
0.41
Total Fees
$0.00
Estimated Strategy Capacity
$1800000.00
Lowest Capacity Asset
SPXW 32YYLBOK3V13I|SPX 31
Portfolio Turnover
2.11%
Drawdown Recovery
0
# region imports
from AlgorithmImports import *
# endregion

class OptionChainFullExample(QCAlgorithm):
    _last_ticket: OrderTicket = None
    _current_date = None
    _traded_today = False

    def initialize(self) -> None:
        self.set_start_date(2026, 2, 6)
        self.set_end_date(2026, 2, 9)
        self.set_cash(100000)
        self.settings.automatic_indicator_warm_up = True
        self.universe_settings.minimum_time_in_universe = timedelta(0)

        # Warm-up the option contracts as soon as it is added to the algorithm
        self.settings.seed_initial_prices = True

        # The EMA/price cross will determine we trade ATM contracts 
        self.index = self.add_index("SPX")

        self._option_chain_symbol = Symbol.create_canonical_option(self.index, "SPXW", Market.USA, "?SPXW")

    def on_data(self, data: Slice) -> None:
        if self.is_warming_up:
            return

        # Ensure we have SPX bar data
        bar = data.bars.get(self.index)
        if bar is None:
            return

        # Daily reset (first bar of a new calendar day)
        today = self.time.date()
        if self._current_date is None or today != self._current_date:
            self._traded_today = False
            self._current_date = today

        # Only one trade per day
        if self._traded_today:
            return
        
        spot = self.securities[self.index.symbol].price
    
        self._get_at_the_money_put_credit_spread(OptionRight.PUT, spot)
        self._traded_today = True
        return

    def _get_at_the_money_put_credit_spread(self, right: OptionRight, spot: float) -> Option | None:
        chain = self.option_chain(self._option_chain_symbol)
        expiry = min([x.expiry for x in chain])
        contracts = sorted([x for x in chain if x.expiry == expiry and x.right == right],
            key=lambda x: abs(spot - x.strike))

        if not contracts:
            return None
        
        short_contract = contracts[0]
        long_candidates = sorted([x for x in chain if x.expiry == expiry and x.right == right],
            key=lambda x: abs(spot - x.strike - 10))
        long_contract = long_candidates[0]

        self.add_option_contract(short_contract)
        self.add_option_contract(long_contract)

        short_mid = (short_contract.bid_price + short_contract.ask_price) / 2
        long_mid = (long_contract.bid_price + long_contract.ask_price) / 2
        entry_credit = (short_mid - long_mid) * 100  # dollars per spread

        legs = []
        legs.append(Leg.create(short_contract.symbol, -1))
        legs.append(Leg.create(long_contract.symbol, 1))
        tickets = self.combo_market_order(legs, 1)
        combo_price = 0
        for ticket in tickets:
            self.debug(f"Symbol: {ticket.symbol}; Quantity filled: {ticket.quantity_filled}; Fill price: {ticket.average_fill_price}")
            combo_price += ticket.quantity_filled * ticket.average_fill_price
        self.debug(f"Combo price: {abs(combo_price)}")
        return