Overall Statistics
Total Orders
24
Average Win
4.58%
Average Loss
0%
Compounding Annual Return
88.449%
Drawdown
13.100%
Expectancy
-0.286
Start Equity
1000000
End Equity
1815974.65
Net Profit
81.597%
Sharpe Ratio
2.639
Sortino Ratio
2.527
Probabilistic Sharpe Ratio
92.571%
Loss Rate
29%
Win Rate
71%
Profit-Loss Ratio
0
Alpha
0.411
Beta
1.054
Annual Standard Deviation
0.199
Annual Variance
0.039
Information Ratio
2.424
Tracking Error
0.172
Treynor Ratio
0.497
Total Fees
$181.35
Estimated Strategy Capacity
$4000.00
Lowest Capacity Asset
NVDA 32ISKKQPQ3MYU|NVDA RHM8UTD8DT2D
Portfolio Turnover
1.20%
from AlgorithmImports import *

class WheelOptionStrategy(QCAlgorithm):
    '''
    Wheel Strategy on NVDA:
      - Sell 30-delta cash-secured puts ~30 days to expiration
      - Use on_order_event to log assignments, exercises, and expirations
      - Plot daily portfolio equity
      - Ensure no options are held on specific dates
    Start date: 2023-06-30
    '''

    def initialize(self):
        # Add underlying and option
        self._symbol = self.add_equity("NVDA", Resolution.HOUR).symbol
        self._option = self.add_option("NVDA", Resolution.HOUR)
        self._option.set_filter(self.universe_filter)

        # Backtest settings
        self.set_start_date(2023, 6, 30)
        self.set_cash(1000000)

        # Performance chart
        chart = Chart("Performance")
        chart.add_series(Series("Equity", SeriesType.LINE))
        self.add_chart(chart)

        # Strategy state
        self._put_order = None
        self._call_order = None
        self._has_stock = False

        # Schedule daily rolls ~30m after open
        self.schedule.on(
            self.date_rules.every_day(self._symbol),
            self.time_rules.after_market_open(self._symbol, 30),
            self.roll_positions
        )
        # Schedule option liquidation check daily before close
        self.schedule.on(
            self.date_rules.every_day(self._symbol),
            self.time_rules.before_market_close(self._symbol, 1),
            self.check_scheduled_liquidation
        )

    def universe_filter(self, universe):
        return universe.weeklys_only().expiration(timedelta(25), timedelta(35))

    def on_order_event(self, orderEvent):
        # Only process option orders
        symbol = orderEvent.Symbol
        if symbol.ID.SecurityType != SecurityType.OPTION:
            return

        msg = orderEvent.Message.lower() if orderEvent.Message else ""
        cash = self.portfolio.cash
        shares_held = self.portfolio[self._symbol].quantity

        # determine put or call
        right = symbol.ID.OptionRight
        kind = "put" if right == OptionRight.PUT else "call"

        if "assignment" in msg:
            self.debug(f"{self.time} - Assigned {kind} option: {symbol}. Shares held: {shares_held}. Cash: {cash}")
            self._has_stock = True if kind == "put" else False
            self._put_order = None
            self._call_order = None
        elif "exercise" in msg:
            self.debug(f"{self.time} - Exercised {kind} option: {symbol}. Shares held: {shares_held}. Cash: {cash}")
            self._has_stock = True if kind == "put" else False
            self._put_order = None
            self._call_order = None
        elif "otm" in msg:
            self.debug(f"{self.time} - {kind.capitalize()} option expired OTM: {symbol}. Shares held: {shares_held}. Cash: {cash}")
            self._put_order = None
            self._call_order = None

    def on_end_of_day(self, _symbol):
        if _symbol.ID.SecurityType != SecurityType.OPTION:
            return
        if self.time.date() != datetime(2024, 6, 7).date():
            return
        sec = _symbol
        if sec.Invested:
            self.debug(f"{self.time} - Holding {sec.Symbol}, Qty: {sec.Quantity}, Price: {sec.Price}")            

    def check_scheduled_liquidation(self):
        # Liquidate options on June 7, 2024
        if self.time.date() == datetime(2024, 6, 7).date():
            self.liquidate_options()

    def roll_positions(self):
        # Decide when to write new options
        if not self._put_order and not self._has_stock:
            self.sell_put()
        elif self._has_stock and not self._call_order:
            self.sell_call()

    def sell_put(self):
        chains = self.current_slice.option_chains.get(self._option.symbol)
        if not chains:
            return
        puts = [c for c in chains if c.right == OptionRight.PUT and c.expiry.date() > self.time.date()]
        if not puts:
            return
        contract = min(puts, key=lambda c: abs(c.greeks.delta + 0.30))
        max_contracts = int(self.portfolio.cash // (contract.strike * 100))
        if max_contracts <= 1:
            self.debug(f"{self.time} - Insufficient cash for put {contract.symbol}")
            return
        qty = -max_contracts
        self.market_order(contract.symbol, qty)
        self._put_order = (contract.symbol, contract.strike, max_contracts, contract.expiry.date())
        shares_held = self.portfolio[self._symbol].quantity
        cash = self.portfolio.cash
        self.debug(f"{self.time} - Sold put {contract.symbol} qty={qty}. Shares held: {shares_held}. Cash: {cash}")

    def sell_call(self):
        chains = self.current_slice.option_chains.get(self._option.symbol)
        if not chains:
            return
        calls = [c for c in chains if c.right == OptionRight.CALL and c.expiry.date() > self.time.date()]
        if not calls:
            return
        contract = min(calls, key=lambda c: abs(c.greeks.delta - 0.15))
        shares = self.portfolio[self._symbol].quantity
        contracts = shares // 100
        if contracts <= 0:
            self.debug(f"{self.time} - Skip call {contract.symbol}: no shares to cover")
            return
        qty = -contracts
        self.market_order(contract.symbol, qty)
        self._call_order = (contract.symbol, contract.strike, contracts, contract.expiry.date())
        shares_held = self.portfolio[self._symbol].quantity
        cash = self.portfolio.cash
        self.debug(f"{self.time} - Sold call {contract.symbol} qty={qty}. Shares held: {shares_held}. Cash: {cash}")

    def liquidate_options(self):
        # Force close all option positions
        for sym, holding in self.portfolio.items():
            if sym.SecurityType == SecurityType.OPTION and holding.Quantity != 0:
                qty = -holding.Quantity
                self.market_order(sym, qty)
                self.debug(f"{self.time} - Liquidated option {sym} qty={qty}. Cash: {self.portfolio.cash}")
        # reset state
        self._put_order = None
        self._call_order = None