Overall Statistics
Total Orders
2798
Average Win
7.58%
Average Loss
-3.21%
Compounding Annual Return
18.113%
Drawdown
22.500%
Expectancy
-1
Start Equity
100000
End Equity
117951.78
Net Profit
17.952%
Sharpe Ratio
0.45
Sortino Ratio
0.549
Probabilistic Sharpe Ratio
33.695%
Loss Rate
100%
Win Rate
0%
Profit-Loss Ratio
2.36
Alpha
-0.133
Beta
1.745
Annual Standard Deviation
0.213
Annual Variance
0.045
Information Ratio
-0.281
Tracking Error
0.126
Treynor Ratio
0.055
Total Fees
$73.69
Estimated Strategy Capacity
$23000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
1.67%
Drawdown Recovery
154
# OP: https://www.quantconnect.com/forum/discussion/18492/code-is-refusing-to-load-options-chains/p1

from AlgorithmImports import *
from collections import deque
import numpy as np
from datetime import timedelta

class SPYOptionsBot(QCAlgorithm):
    # Parameters
    lookback_period         = 252   # daily bars for volatility calc
    required_bullish_days   = 3     # consecutive bullish candles
    std_dev_multiplier      = 1.0
    max_positions           = 3     # max concurrent positions
    position_size           = 0.1   # 10% of portfolio value per trade
    # Expiration changed to 1 to 2 weeks
    min_days_to_expiry      = 7   # 1 week out
    max_days_to_expiry      = 14  # 2 weeks out

    def initialize(self) -> None:
        self.set_start_date(2023, 1, 2)
        self.set_end_date(2023, 12, 31)
        self.set_cash(100_000)

        self.spy = self.add_equity("SPY", Resolution.MINUTE).Symbol
        self.set_benchmark(self.spy)
        self.option = self.add_option("SPY", Resolution.MINUTE)
        # self.option_symbol = self.option.symbol

        self.option.set_filter(self.filter_options)

        # Track closing prices for bullish pattern detection
        self.previous_closes: deque = deque(maxlen=self.required_bullish_days)

        # Keep track of open positions
        self.open_positions: dict = {}

        self.schedule.on(self.date_rules.every_day(), self.time_rules.at(15, 30), self.manage_positions)

    def filter_options(self, universe: OptionFilterUniverse) -> OptionFilterUniverse:
        """
        Filter for strikes near the current price, with expiration
        between 1-2 weeks (7 to 14 days).
        """
        return (universe.strikes(-3, 3).expiration(self.min_days_to_expiry, self.max_days_to_expiry))

    def on_data(self, data: Slice) -> None:
        # We need SPY bar data to detect bullish pattern
        if self.spy not in data.bars: return

        # Track consecutive bullish closes
        bar = data.bars[self.spy]
        self.previous_closes.append(bar.close)

        # Check for bullish pattern and capacity for new trades
        if (len(self.previous_closes) == self.required_bullish_days 
            and all(self.previous_closes[i] > self.previous_closes[i - 1] for i in range(1, self.required_bullish_days))
            and len(self.open_positions) < self.max_positions):
            # self.log(f"Detected {self.required_bullish_days} bullish candles. Looking for put options to sell.")

            # Check if we have any option chain data in this slice
            if self.option.symbol in data.option_chains and data.option_chains[self.option.symbol]:
                option_chain = data.option_chains[self.option.symbol]
                self.sell_put_option(option_chain)
            else:
                pass
                # self.log("No option chain data is available for SPY at this moment.")

    def sell_put_option(self, option_chain: OptionChain) -> None:
        # Current price of the underlying equity
        underlying_price = self.securities[self.spy].price

        # Calculate historical volatility (std dev) over specified lookback
        history = self.history(self.spy, self.lookback_period, Resolution.DAILY)
        if history.empty:
            # self.log("No historical data available to compute volatility.")
            return
        close_col = 'close' if 'close' in history.columns else 'Close'
        stddev = np.std(history[close_col])
        strike_price = underlying_price + (stddev * self.std_dev_multiplier) / 3.2

        # Filter for put options that are out-of-the-money (<= strike_price)
        puts = [contract for contract in option_chain 
            if contract.right == OptionRight.PUT
            and contract.strike <= strike_price
            and (self.time + timedelta(days=self.min_days_to_expiry) <= contract.expiry <= self.time + timedelta(days=self.max_days_to_expiry))]

        if not puts:
            # self.log(f"No suitable put options found. Target strike: {strike_price}")
            return

        # Sort by "closeness" to strike, then volume (descending), then bid price
        puts.sort(key=lambda x: (abs(x.strike - strike_price), -(x.volume or 0), x.bid_price))
        selected_put = puts[0]
        max_position_value = self.portfolio.total_portfolio_value * self.position_size

        # Each contract represents 100 shares
        if selected_put.bid_price > 0:
            quantity = int(max_position_value // (selected_put.bid_price * 100))
        else:
            quantity = 0

        if quantity > 0:
            self.sell(selected_put.symbol, quantity)
            self.open_positions[selected_put.symbol] = {
                'entry_price': selected_put.bid_price,
                'quantity': quantity,
                'entry_time': self.time,
                'strike': selected_put.strike
            }
            self.log(f"Sold {quantity} contracts of {selected_put.symbol} at {selected_put.bid_price}")
        else:
            pass
            # self.log("Calculated position size is zero; not placing any order.")

    def manage_positions(self) -> None:
        """
        Manage open positions daily at 15:30. Closes positions if:
        1) 50% profit is reached (option price <= 50% of entry)
        2) 100% loss is reached (option price >= 2x entry)
        3) Position is held longer than 15 days
        """
        positions_to_close = []
        for symbol, position in list(self.open_positions.items()):
            if symbol not in self.securities:
                # self.log(f"Symbol {symbol} not found in Securities.")
                continue
            current_price = self.securities[symbol].price
            entry_price = position['entry_price']
            days_held = (self.time - position['entry_time']).days
            # Closing conditions
            if (current_price <= entry_price * 0.5    # 50% profit
                or current_price >= entry_price * 2   # 100% loss
                or days_held >= 15):                  # Time-based exit
                self.buy(symbol, position['quantity'])
                positions_to_close.append(symbol)
                self.log(f"Closing position {symbol} after {days_held} days. "
                         f"Entry: {entry_price}, Exit: {current_price}")

        # Remove closed positions from tracking
        for symbol in positions_to_close:
            del self.open_positions[symbol]

    def on_order_event(self, order_event: OrderEvent) -> None:
        if order_event.status == OrderStatus.FILLED:
            self.log(f"Order {order_event.order_id} filled: "
                     f"{order_event.symbol} at {order_event.fill_price}")

    def on_end_of_algorithm(self) -> None:
        self.log(f"Algorithm ended. Final portfolio value: "
                 f"${self.portfolio.total_portfolio_value:,.2f}")