Maximizing Profits with SPY Options Trading Using Genetic Algorithm-Based 1-Minute Trend Detection
In the fast-paced world of options trading, where every second counts, employing an algorithmic approach can be a game-changer.

In this article, we will explore a unique trading strategy that leverages genetic algorithms to determine trend direction in the SPY options market using a 1-minute timeframe. By optimizing trend identification and minimizing losses, we aim to achieve higher profitability.
Understanding the Genetic Algorithm Approach
The genetic algorithm approach is chosen due to its effectiveness in optimizing solutions for complex problems. By mimicking the principles of natural selection, genetic algorithms iteratively evolve potential solutions to find the most optimal one. In the context of SPY options trading, this approach proves valuable as it prioritizes minimizing losses while maximizing gains.
Exploring the Code
Let’s explore the code step by step and check how this strategy is implemented:
Importing Required Libraries
We begin by importing essential libraries such as tqdm for progress tracking, numpy for numerical computations, pandas for data manipulation, and pygad for the genetic algorithm implementation. Additionally, we suppress warnings for cleaner output.
# Import necessary libraries
from tqdm import tqdm
import numpy as np
import pandas as pd
import pygad
import warnings
warnings.filterwarnings("ignore")Setting Configuration and Constants
We configure our environment and set constants that will govern our trading strategy. These constants include the number of bars to consider (BARS), stop-loss threshold (STOP_LOSS), and percentage over option price (POO), among others.
# Configuration
pd.set_option('display.float_format', lambda x: '%.4f' % x)
# Constants
BARS = 15 # Range: 0 - 30
STOP_LOSS = 0.7 # Range: 0 - 1 (0 -> 0% | 1 -> 100%)
POO = 0.01 # Range: 0 - 1 (0 -> 0% | 1 -> 100%)
OPTIONS_FILE='../data/spy_dte_0.csv.gz'
TRAIN_FILE='../data/spy.2008.2021.csv.gz'
FEES_PER_CONTRACT = 0.6
CASH = 1000
DATE_SPLIT = '2019-06-01'
SOLUTIONS = 30
GENERATIONS = 50Data Processing Methods
We define helper methods to process the data, extract features and targets, and prepare the dataset for model training.
# Helper method to process data and return features and targets
def get_features_targets(df, scale_obs=True):
feature_result = []
dates = []
# Remove duplicated dates
df = df.groupby(by='date').mean().reset_index()
# Get Features based on BARS configuration
features = df[((df['date'].dt.hour == 9) & (df['date'].dt.minute >= 30)) &
(df['date'].dt.hour == 9) & (df['date'].dt.minute < 30 + BARS)]
features = features.groupby(features['date'].dt.date)
for dt, feature in features:
if len(feature) != BARS:
feature = feature.set_index('date')
feature = feature.resample('1T').asfreq().reindex(pd.date_range(str(dt) + ' 09:30:00', str(dt) + f' 09:{30+BARS-1}:00', freq='1T'))
feature = feature.reset_index()
feature['close'] = feature['close'].fillna(method='ffill')
feature['open'] = feature['open'].fillna(feature['close'])
feature = feature.dropna()
if len(feature) == BARS:
feature = feature['close'].values
if scale_obs:
feature -= np.min(feature)
feature /= np.max(np.abs(feature))
feature = np.nan_to_num(feature, nan=0.0, posinf=0.0, neginf=0.0)
feature_result.append(feature)
dates.append(dt)
# Get Targets Trend based on first and last value / day (0: DOWN - 1: UP)
targets = df.set_index('date')
targets = targets.resample('1D').agg({'open':'first', 'close':'last'})
targets = targets.loc[dates].reset_index().sort_values(by='date')
targets['trend'] = np.where(targets['open'] < targets['close'], 1, 0)
return np.array(feature_result), np.array(targets['trend'].values)
# Helper method to return predicted values
def get_predicted_values(solution, features):
pred_y = np.clip(np.dot(features, solution), 0, 1)
pred_y = np.where(pred_y > 0.5, 1, 0)
return pred_yDefining the Fitness Function
We define the fitness function that quantifies how well a given solution (set of genetic parameters) performs. This function uses the F1-score as a measure of a solution’s accuracy in predicting trends.
# Define fitness function to be used by the PyGAD instance
def fitness_func(self, solution, sol_idx):
global train_x, train_y
pred_y = get_predicted_values(solution, train_x)
result = f1_score(train_y, pred_y, average='binary', pos_label=1) + \
f1_score(train_y, pred_y, average='binary', pos_label=0)
return resultReading and Preparing Data
We read and prepare the data from the provided files for both options and training.
# Read options and training data
df_base = pd.read_csv(OPTIONS_FILE, header=0)
df_base['date'] = pd.to_datetime(df_base['date'])
train = pd.read_csv(TRAIN_FILE, header=0)
train['date'] = pd.to_datetime(train['date'])
train = train[train['date'] <= DATE_SPLIT]
test = df_base[['date', 'open_underlying', 'close_underlying']]
test.columns = ['date', 'open', 'close']
test = test.drop_duplicates().reset_index(drop=True)
# Get Features and Targets
train_x, train_y = get_features_targets(train)
test_x, _ = get_features_targets(test)Training the Genetic Algorithm
We utilize the genetic algorithm to find the optimal solution that best predicts trend directions.
# Train the genetic algorithm
with tqdm(total=GENERATIONS) as pbar:
# Create Genetic Algorithm
ga_instance = pygad.GA(num_generations=GENERATIONS,
num_parents_mating=5,
fitness_func=fitness_func,
sol_per_pop=SOLUTIONS,
num_genes=BARS,
gene_space={'low': -1, 'high': 1},
random_seed=42,
on_generation=lambda _: pbar.update(1),
)
# Run the Genetic Algorithm
ga_instance.run()
# Get the best solution
solution, _, _ = ga_instance.best_solution()Determining Trend Direction
We determine the trend direction for each trading day and associate it with the option open price.
# Get first bar (To get the Option Open Price)
df_day_open = df_base[(df_base['date'].dt.hour == 9) & (df_base['date'].dt.minute == 30)]
# Get *BARS* bar (To get Underlying Close Price)
df = df_base[(df_base['date'].dt.hour == 9) & (df_base['date'].dt.minute == 30 + BARS - 1)]
# Add the Option Open Price
df = df.merge(df_day_open[['expire_date','strike','kind','open']],
how='left',
left_on=['expire_date','strike','kind'],
right_on=['expire_date','strike','kind'],
suffixes=('','_dayopen'))
# Keep the first open value for each strike
df = df.rename(columns={'open_dayopen': 'option_open'})
# Predict Trend and add to test df
test['date'] = test['date'].dt.date.astype(str)
test = test[['date']].drop_duplicates().reset_index(drop=True)
test['trend'] = get_predicted_values(solution, test_x)
test['trend'] = np.where(test['trend'] == 0, -1, test['trend'])
# Add Trend to df
df = df.merge(test,
how='left',
left_on=['expire_date'],
right_on=['date'],
suffixes=['','_ga'])
# Remove all previous merged values for trend calculation and rows with NaN values
df = df.loc[:,~df.columns.str.endswith('_ga')]
df = df.dropna()Selecting the Optimal Option
We select the closest in-the-money (ITM) options based on trend direction and calculate trading points.
# Filter all puts when trend is going down and calls when trend is going up
df = df[((df['kind'] == 'P') & (df['trend'] == -1)) |
((df['kind'] == 'C') & (df['trend'] == 1))]
# Calculate Strike distance from Underlying price
df['distance'] = df['trend'] * (df['close_underlying'] - df['strike'])
# Remove OTM & ATM Options
df = df[df['distance'] > 0]
# Get the closest ITM options
idx = df.groupby(['expire_date'])['distance'].transform(min) == df['distance']
df = df[idx]
# Remove distance column
df = df.drop('distance', axis=1)
### Calculate close points ###
# Get trade bars
df_trade = df_base[((df_base['date'].dt.hour == 9) & (df_base['date'].dt.minute > 30 + BARS - 1)) |
(df_base['date'].dt.hour >= 10)]
# Get Option Open and Close Points
df = df_trade.merge(df[['expire_date','kind','strike','option_open']],
how='right',
left_on=['expire_date','kind','strike'],
right_on=['expire_date','kind','strike'])
df.loc[:,'open_point'] = np.where((df['open'] >= df['option_open'] * (1 + POO)) &
((df['open'].shift() < df['option_open'].shift() * (1 + POO)) |
(df['expire_date'] != df['expire_date'].shift())), 1, 0)
df.loc[:,'stop_loss'] = df['option_open'] * STOP_LOSS
df.loc[:,'last_date'] = df.groupby(['expire_date','kind','strike'])['date'].transform('last')
df.loc[:,'close_point'] = np.where(((df['close'] <= df['stop_loss']) &
((df['close'].shift() > df['stop_loss'].shift()) | (df['expire_date'] != df['expire_date'].shift()))) |
(df['last_date'] == df['date']), 1, 0)
df_tmp = df[(df['open_point'] - df['close_point']) == 0]
df = df[(df['open_point'] - df['close_point']) != 0]
df.loc[:,'open_point'] = np.where((df['open_point'] - df['close_point']) == (df['open_point'].shift(-1) - df['close_point'].shift(-1)), 0, df['open_point'])
df = pd.concat([df, df_tmp])
df = df.sort_values(by=['date','expire_date','kind','strike'])
# Get Open Price, Close Price, Open Date, and Close Date
df = df[(df['open_point'] != 0) | (df['close_point'] != 0)]
df['open_price'] = np.where(df['open_point'] == 1, df['open'], np.NaN)
df['close_price'] = np.where(df['close_point'] == 1, df['close'], np.NaN)
df['close_price'] = df['close_price'].fillna(method='bfill', limit=1)
df['close_date'] = np.where(df['open_point'] - df['close_point'] == 0, df['date'], df['date'].shift(-1))
df = df.rename(columns={'date':'open_date'})
# Clean all Rows with NaN values (This is going to remove all invalid closes)
df = df.dropna()
# Clean all the unneeded columns
df = df.drop(['last_date','open_point','close_point','open','close'], axis=1)
df = df.loc[:,~df.columns.str.endswith('_underlying')]
# Save the trigger of the closing
df.loc[:,'trigger'] = np.where(df['close_price'] <= df['stop_loss'], 'SL', 'EXPIRED')Calculating Trading Results
We calculate trading results, including contracts, fees, gross and net results, and summarize the performance.
# Calculate the variables required in the result
df['contracts'] = (CASH // (100 * df['open_price'])).astype(int)
df['fees'] = np.where(df['trigger'] == 'EXPIRED', FEES_PER_CONTRACT, 2 * FEES_PER_CONTRACT) * df['contracts']
df['gross_result'] = df['contracts'] * 100 * (df['close_price'] - df['open_price'])
df['net_result'] = df['gross_result'] - df['fees']
sl = len(df[df["trigger"] == "SL"])
exp = len(df[df["trigger"] != "SL"])
total = len(df)
# Configuration
print(f' CONFIGURATION '.center(70, '*'))
print(f'* Bars: {BARS}')
print(f'* Stop Loss: {STOP_LOSS * 100:.0f}%')
print(f'* Percentage over price: {POO * 100:.0f}%')
# Show the Total Result
print(f' SUMMARIZED RESULT '.center(70, '*'))
print(f'* Trading Days: {len(df["expire_date"].unique())}')
print(f'* Operations: {len(df)} - Stop Loss: {sl} ({100 * sl / total:.2f}%) - Expired: {exp} ({100 * exp / total:.2f}%)')
print(f'* Gross PnL: $ {df["gross_result"].sum():.2f}')
print(f'* Net PnL: $ {df["net_result"].sum():.2f}')
# Show The Monthly Result
print(f' MONTHLY DETAIL RESULT '.center(70, '*'))
df_monthly = df[['expire_date','gross_result','net_result']]
df_monthly['year_month'] = df_monthly['expire_date'].str[0:7]
df_monthly = df_monthly.groupby(['year_month'])[['gross_result','net_result']].sum()
print(df_monthly)Results
The trading strategy utilizing the genetic algorithm to determine trend direction using the first 15 minutes of each trading day has shown promising results. Below is a summary of the results.
*************************** CONFIGURATION ****************************
* Bars: 15
* Stop Loss: 70%
* Percentage over price: 1%
************************* SUMMARIZED RESULT **************************
* Trading Days: 361
* Operations: 594 - Stop Loss: 422 (71.04%) - Expired: 172 (28.96%)
* Gross PnL: $ 51359.00
* Net PnL: $ 47390.00
*********************** MONTHLY DETAIL RESULT ************************
gross_result net_result
year_month
2021-06 -1006.0000 -1070.8000
2021-07 3980.0000 3779.6000
2021-08 -3689.0000 -3945.2000
2021-09 5590.0000 5467.6000
2021-10 1967.0000 1788.8000
2021-11 -4876.0000 -5076.4000
2021-12 1164.0000 1012.8000
2022-01 4946.0000 4759.4000
2022-02 2229.0000 2149.2000
2022-03 9529.0000 9422.2000
2022-04 3669.0000 3537.0000
2022-05 3238.0000 3160.6000
2022-06 -5825.0000 -6013.4000
2022-07 984.0000 865.8000
2022-08 6390.0000 6263.4000
2022-09 8015.0000 7930.4000
2022-10 3904.0000 3818.8000
2022-11 -4463.0000 -4637.6000
2022-12 1142.0000 979.4000
2023-01 6051.0000 5854.8000
2023-02 -709.0000 -909.4000
2023-03 1128.0000 945.6000
2023-04 8138.0000 7955.0000
2023-05 -4515.0000 -4870.2000
2023-06 4378.0000 4222.6000Comparison & Conclusion
Comparing the strategy of the first article and this one,
the genetic algorithm-based approach demonstrates significantly higher gross and net profits. The strategy’s use of genetic algorithms to determine trend direction proves effective in optimizing trading decisions and achieving greater profitability.
The strategy that utilizes only the first and last bars of each 15-minute period of each day for trend detection shows relatively lower profits compared to the genetic algorithm approach. This highlights the potential benefits of employing a more sophisticated algorithmic approach that considers more data points and optimizes trend detection using the genetic algorithm.
It’s important to note that while these results are promising, trading strategies should always be thoroughly backtested and evaluated under various market conditions before being implemented with real capital. Additionally, the comparison underscores the significance of selecting the right algorithmic approach to maximize profits and minimize losses in the dynamic world of options trading.
If you enjoy my work, please support me on Medium by becoming a member through my referral link, and consider giving it a clap as a small gesture of motivation. Thank you!
Download the full source code and the colab notebook of this article from here
Twitter / X: https://twitter.com/diegodegese LinkedIn: https://www.linkedin.com/in/ddegese Github: https://github.com/crapher
Disclaimer: Investing in the stock market involves risk and may not be suitable for all investors. The information provided in this article is for educational purposes only and should not be construed as investment advice or a recommendation to buy or sell any particular security. Always do your own research and consult with a licensed financial advisor before making any investment decisions. Past performance is not indicative of future results.
A Message from InsiderFinance

Thanks for being a part of our community! Before you go:
- 👏 Clap for the story and follow the author 👉
- 📰 View more content in the InsiderFinance Wire
- 📚 Take our FREE Masterclass
- 📈 Discover Powerful Trading Tools





