| 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}")