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()