| Overall Statistics |
|
Total Orders 297 Average Win 0.08% Average Loss -0.05% Compounding Annual Return -0.288% Drawdown 0.200% Expectancy -0.029 Start Equity 500000 End Equity 498924.2 Net Profit -0.215% Sharpe Ratio -13.563 Sortino Ratio -7.438 Probabilistic Sharpe Ratio 0.000% Loss Rate 65% Win Rate 35% Profit-Loss Ratio 1.75 Alpha -0.014 Beta -0 Annual Standard Deviation 0.001 Annual Variance 0 Information Ratio 1.036 Tracking Error 0.199 Treynor Ratio 50.041 Total Fees $325.80 Estimated Strategy Capacity $59000.00 Lowest Capacity Asset SPY Y1OO9THJUODI|SPY R735QTJ8XC9X Portfolio Turnover 0.14% |
# QUANTCONNECT - WORK IN PROGRESS:
# Strategy Idea - see research jupyter notebook for implementation:
#
# 1. Select a High Market Cap Stock or ETF:
# These assets are liquid and exhibit relatively stable price movements
#
# 2. Analyze Daily Percentage Changes:
# Compute the daily percentage changes from historical closing prices.
# Plot the distribution of these changes, which should approximate a normal distribution
# for large-cap stocks or ETFs.
#
# 3. Calculate Key Metrics:
# Compute the mean, E[X] and standard deviation σ of the daily percentage changes.
# Example from the simulation on $AAPl: E[X]=0.000877 (about 0.0877%), and σ=0.0196 (about 1.96%).
#
# 4. Set Up Butterfly Spreads:
# Place a 2x butterfly spread at E[X].
# Place a 1x butterfly spread at E[X]−σ (left standard deviation).
# Place a 1x butterfly spread at E[X]+σ (right standard deviation).
#
# **My Hypothesis**
# Defined Risk/Reward: Butterfly spreads have limited risk and reward,
# making them suitable for range-bound strategies.
#
# Utilization of Statistical Metrics: Using historical data to determine
# expected values and deviations adds a quantitative edge.
#
# High Return Potential: Butterfly spreads can yield high returns if the stock price
# ends near the strike price at expiration.
# sources:
# OptionsStrat: 15D till expiration Long Call Butterfly on $SPY:
# Debit & Max Loss: $77.50 (-100%)
# Max Profit: $422.50 (+544%)
# https://optionstrat.com/build/long-call-butterfly/SPY/.SPY250110C596x2,.SPY250110C601x-4,.SPY250110C606x2
#
# Since butterfly spreads are relatively cheap to open and have a very high max profit,
# we can open several at our determined values (E[x] & std()'s) where assuming that one
# will profit, it will cover the losses of the other 2
#
# Alternatively, this strategy can be adjusted for iron condors
from AlgorithmImports import *
import numpy as np
class ButterflySpreadStrategy(QCAlgorithm):
def initialize(self):
self.set_start_date(2022, 1, 1)
self.set_end_date(2022, 10, 1)
self.set_cash(500000)
option = self.add_option("SPY", Resolution.Minute)
self.symbol = option.symbol
option.set_filter(self.universe_filter)
self.mean_daily_return = None
self.std_dev_daily_return = None
self.positions = {}
self.position_expiries = {}
self.position_quantities = {}
self.initialize_statistics()
def initialize_statistics(self):
history = self.History(self.symbol.Underlying, 252, Resolution.Daily)
if not history.empty:
daily_returns = history['close'].pct_change().dropna()
self.mean_daily_return = float(np.mean(daily_returns))
self.std_dev_daily_return = float(np.std(daily_returns))
def universe_filter(self, universe):
return (universe
.include_weeklys()
.strikes(-5, 5)
.expiration(timedelta(15), timedelta(45)))
def calculate_equidistant_strikes(self, atm_price, available_strikes):
sorted_strikes = sorted(available_strikes)
atm_index = min(range(len(sorted_strikes)), key=lambda i: abs(sorted_strikes[i] - atm_price))
strike_spacing = 1.0
for i in range(1, len(sorted_strikes)):
if atm_index - i >= 0 and atm_index + i < len(sorted_strikes):
lower_strike = sorted_strikes[atm_index - i]
middle_strike = sorted_strikes[atm_index]
upper_strike = sorted_strikes[atm_index + i]
if (abs(middle_strike - lower_strike - strike_spacing) < 0.01 and
abs(upper_strike - middle_strike - strike_spacing) < 0.01):
return lower_strike, middle_strike, upper_strike
return None, None, None
def manage_positions(self, data):
current_date = self.Time
positions_to_remove = []
for strategy_id, expiry in self.position_expiries.items():
days_to_expiry = (expiry - current_date).days
if days_to_expiry <= 5:
if strategy_id in self.positions:
# Close the position by selling the strategy
strategy = self.positions[strategy_id]
quantity = self.position_quantities[strategy_id]
self.sell(strategy, quantity)
positions_to_remove.append(strategy_id)
for strategy_id in positions_to_remove:
del self.positions[strategy_id]
del self.position_expiries[strategy_id]
del self.position_quantities[strategy_id]
def on_data(self, data):
self.manage_positions(data)
if self.portfolio.invested or self.mean_daily_return is None:
return
chain = data.option_chains.get(self.symbol, None)
if not chain or not chain.underlying:
return
expiry = min([contract.expiry for contract in chain])
calls = [contract for contract in chain
if contract.right == OptionRight.Call
and contract.expiry == expiry
and contract.volume > 10]
if len(calls) == 0:
return
atm_price = float(chain.underlying.price)
available_strikes = sorted(set([call.strike for call in calls]))
target_prices = [atm_price] # Focus on ATM butterflies only
for target_price in target_prices:
lower_strike, middle_strike, upper_strike = self.calculate_equidistant_strikes(target_price, available_strikes)
if all([lower_strike, middle_strike, upper_strike]):
strategy_id = f"butterfly_{lower_strike}_{middle_strike}_{upper_strike}"
if strategy_id not in self.positions:
option_strategy = OptionStrategies.call_butterfly(
self.symbol, upper_strike, middle_strike, lower_strike, expiry
)
quantity = 1
self.positions[strategy_id] = option_strategy
self.position_expiries[strategy_id] = expiry
self.position_quantities[strategy_id] = quantity
self.buy(option_strategy, quantity)
def on_order_event(self, order_event):
if order_event.status == OrderStatus.Filled:
self.log(f"Order filled - {order_event.symbol}: {order_event.fill_quantity} @ {order_event.fill_price}")
# Since Notebook is not shareable as a public backtest:
# Here is the code to Research:
#
# from QuantConnect import *
# from QuantConnect.Research import *
# import numpy as np
# import pandas as pd
# import matplotlib.pyplot as plt
# import seaborn as sns
# from scipy import stats
# # Initialize QuantBook
# qb = QuantBook()
# # Add SPY data
# spy = qb.AddEquity("SPY")
# symbol = spy.Symbol
# # Get historical data (3 years of daily data)
# history = qb.History(symbol, 252*3, Resolution.Daily)
# if history.empty:
# raise ValueError("No historical data retrieved")
# # Calculate daily returns
# daily_returns = history['close'].pct_change().dropna()
# # Calculate mean and standard deviation
# mean_return = float(np.mean(daily_returns))
# std_dev = float(np.std(daily_returns))
# # Create the plots
# plt.figure(figsize=(15, 10))
# # Plot 1: Historical Prices
# plt.subplot(2, 2, 1)
# history['close'].plot(title='SPY Historical Prices')
# plt.grid(True)
# # Plot 2: Daily Returns Distribution
# plt.subplot(2, 2, 2)
# sns.histplot(daily_returns, stat='density', kde=True)
# plt.axvline(mean_return, color='r', linestyle='--', label='Mean')
# plt.axvline(mean_return + std_dev, color='g', linestyle='--', label='+1 Std Dev')
# plt.axvline(mean_return - std_dev, color='g', linestyle='--', label='-1 Std Dev')
# plt.title('Distribution of Daily Returns')
# plt.legend()
# plt.grid(True)
# # Plot 3: QQ Plot to verify normality
# plt.subplot(2, 2, 3)
# stats.probplot(daily_returns, dist="norm", plot=plt)
# plt.title('Q-Q Plot of Daily Returns')
# # Calculate butterfly spread strike prices
# current_price = float(history['close'].iloc[-1])
# expected_price = current_price * (1 + mean_return)
# left_wing = current_price * (1 + mean_return - std_dev)
# right_wing = current_price * (1 + mean_return + std_dev)
# # Print strategy details
# print("\nButterfly Spread Strategy Parameters:")
# print(f"Current Price: ${current_price:.2f}")
# print(f"Mean Daily Return: {mean_return:.4%}")
# print(f"Standard Deviation: {std_dev:.4%}")
# print("\nButterfly Spread Strike Prices:")
# print(f"Left Wing (1x): ${left_wing:.2f}")
# print(f"Body (2x): ${expected_price:.2f}")
# print(f"Right Wing (1x): ${right_wing:.2f}")
# plt.tight_layout()
# plt.show()
# # Optional: Get option chain data for verification
# option_chain = qb.GetOptionHistory(symbol, datetime.now())
# strikes = option_chain.GetStrikes()
# if len(strikes) > 0:
# print("\nAvailable Option Strikes near targets:")
# strikes = sorted(strikes)
# print(f"Left Wing Target: {min(strikes, key=lambda x: abs(x - left_wing))}")
# print(f"Body Target: {min(strikes, key=lambda x: abs(x - expected_price))}")
# print(f"Right Wing Target: {min(strikes, key=lambda x: abs(x - right_wing))}")
# And here is the possible improvement idea using monte carlo to get a ideal sortino ratio:
# Add after the existing imports
# from datetime import datetime
# # Add these functions after the imports and before the main code
# def simulate_paths(current_price, mean_return, std_dev, days, num_simulations=10000):
# daily_returns = np.random.normal(mean_return, std_dev, (num_simulations, days))
# paths = current_price * np.exp(np.cumsum(daily_returns, axis=1))
# return paths
# def calculate_sortino_ratio(returns, risk_free_rate=0.02):
# excess_returns = returns - risk_free_rate
# downside_returns = np.where(returns < 0, returns, 0)
# downside_std = np.std(downside_returns)
# if downside_std == 0:
# return 0
# return (np.mean(excess_returns)) / downside_std
# def calculate_butterfly_payoff(paths, lower, center, upper):
# payoffs = np.maximum(paths[:, -1] - lower, 0) - 2 * np.maximum(paths[:, -1] - center, 0) + np.maximum(paths[:, -1] - upper, 0)
# return payoffs
# def optimize_butterfly_strikes(paths, current_price, mean_return, std_dev):
# strike_ranges = {
# 'center': np.arange(0.98, 1.02, 0.001) * current_price,
# 'width': np.arange(0.01, 0.05, 0.001) * current_price
# }
# best_sortino = -np.inf
# optimal_strikes = None
# for center in strike_ranges['center']:
# for width in strike_ranges['width']:
# lower = center - width
# upper = center + width
# payoffs = calculate_butterfly_payoff(paths, lower, center, upper)
# returns = payoffs / (2 * (center - lower)) # Approximate cost of butterfly
# sortino = calculate_sortino_ratio(returns)
# if sortino > best_sortino:
# best_sortino = sortino
# optimal_strikes = (lower, center, upper)
# return optimal_strikes, best_sortino
# # Add this code after your existing plots and before the option chain verification
# print("\nRunning Monte Carlo Simulation for Optimal Strikes...")
# # Simulate price paths for 30 days
# paths = simulate_paths(current_price, mean_return, std_dev, 30)
# # Find optimal strikes using Monte Carlo
# optimal_strikes, best_sortino = optimize_butterfly_strikes(paths, current_price, mean_return, std_dev)
# print("\nMonte Carlo Optimized Butterfly Spread:")
# print(f"Lower Strike: ${optimal_strikes[0]:.2f}")
# print(f"Center Strike: ${optimal_strikes[1]:.2f}")
# print(f"Upper Strike: ${optimal_strikes[2]:.2f}")
# print(f"Sortino Ratio: {best_sortino:.4f}")
# # Add an additional plot for Monte Carlo paths
# plt.figure(figsize=(10, 6))
# plt.plot(paths[:100].T, alpha=0.1, color='blue')
# plt.axhline(y=current_price, color='r', linestyle='--')
# plt.title('Monte Carlo Price Paths (100 simulations)')
# plt.grid(True)
# plt.show()