
The headlines that actually moves markets
Tired of missing the trades that actually move markets?
Every weekday, you’ll get a 5-minute Elite Trade Club newsletter covering the top stories, market-moving headlines, and the hottest stocks — delivered before the opening bell.
Whether you’re a casual trader or a serious investor, it’s everything you need to know before making your next move.
Join 200K+ traders who read our 5-minute premarket report to see which stocks are setting up for the day, what news is breaking, and where the smart money’s moving.
By joining, you’ll receive Elite Trade Club emails and select partner insights. See Privacy Policy.
Elite Quant Plan – 14-Day Free Trial (This Week Only)
No card needed. Cancel anytime. Zero risk.
You get immediate access to:
Full code from every article (including today’s HMM notebook)
Private GitHub repos & templates
All premium deep dives (3–5 per month)
2 × 1-on-1 calls with me
One custom bot built/fixed for you
Try the entire Elite experience for 14 days — completely free.
→ Start your free trial now 👇
(Doors close in 7 days or when the post goes out of the spotlight — whichever comes first.)
See you on the inside.
👉 Upgrade Now →
🔔 Limited-Time Holiday Deal: 20% Off Our Complete 2026 Playbook! 🔔
Level up before the year ends!
AlgoEdge Insights: 30+ Python-Powered Trading Strategies – The Complete 2026 Playbook
30+ battle-tested algorithmic trading strategies from the AlgoEdge Insights newsletter – fully coded in Python, backtested, and ready to deploy. Your full arsenal for dominating 2026 markets.
Special Promo: Use code WINTER2025 for 20% off
Valid only until January 10, 2026 — act fast!
👇 Buy Now & Save 👇
Instant access to every strategy we've shared, plus exclusive extras.
— AlgoEdge Insights Team
Premium Members – Your Full Notebook Is Ready
The complete Google Colab notebook from today’s article (with live data, full Hidden Markov Model, interactive charts, statistics, and one-click CSV export) is waiting for you.
Preview of what you’ll get:

Inside:
Automatic S&P 500 data download (1990 → latest available, up to early 2026)
Full genetic algorithm optimization of double moving average crossover parameters (using DEAP)
In-sample training (1990–2019) and out-of-sample testing (2020–2026) with live data
Clear buy/sell signals plotted on price charts
Equity curve comparison vs buy-and-hold benchmark
Seasonal analysis: average monthly returns bar chart
Bonus metrics: Sharpe Ratio and Maximum Drawdown for both strategy and benchmark
Multiple beautiful Matplotlib visualizations (dark theme)
Ready-to-run in one cell — just click "Run all"!
Bonus: Easily adaptable — change the ticker symbol to test on stocks, crypto, or any asset yfinance supports
Free readers – you already got the full breakdown and visuals in the article. Paid members – you get the actual tool.
Not upgraded yet? Fix that in 10 seconds here👇
Google Collab Notebook With Full Code Is Available In the End Of The Article Behind The Paywall 👇 (For Paid Subs Only)

Optimized Moving Averages on the S&P 500 Index Data
There’s something oddly satisfying about teaching a machine to search through a haystack of numbers for a needle that might improve your returns.
And as someone who’s spent years digging into data, building models, and refining intuition, I wanted to see how far I could push a basic moving average crossover strategy using the most classical of techniques: a genetic algorithm.
Could evolutionary computation, often used in academic toy problems, actually help refine a trading idea in a way that’s easy to implement and visually interpret?
Let me show you what I did and what I found.
Consider this our entire pitch:
Morning Brew isn’t your typical business newsletter — mostly because we actually want you to enjoy reading it.
Each morning, we break down the biggest stories in business, tech, and finance with wit, clarity, and just enough personality to make you forget you’re reading the news. Plus, our crosswords and quizzes are a dangerously fun bonus — a little brain boost to go with your morning coffee.
Join over 4 million readers who think staying informed doesn’t have to feel like work.
The Setup: S&P 500 Historical Data
The first step is to grab reliable historical market data.
I used yfinance to download daily closing prices of the S&P 500 index, covering the period from 1990 to the end of 2024.
We’ll also split the data into a training set (1990–2019) and a testing set (2020–2024).
Before diving in, install the necessary libraries:
%pip install yfinance matplotlib pandas deapOnce that’s done, we can import them:
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import calendar
import random
import time
from deap import base, creator, tools, algorithms
plt.style.use("dark_background")Now define the symbol and date ranges:
symbol = "^GSPC"
start_date = "1990-01-01"
end_date = "2024-12-31"
train_cutoff_date = "2019-12-31"Let’s fetch the data and take a peek:
df = yf.download(symbol, start=start_date, end=end_date)
df.columns = df.columns.get_level_values(0)
df = df[['Close']]
df.head()Date Close
1990-01-02 359.690002
1990-01-03 358.760010
1990-01-04 355.670013
1990-01-05 352.200012
1990-01-08 353.790009And visualize the full historical series:
plt.figure(figsize=(14, 6))
plt.plot(df['Close'], label=f"{symbol} Closing Price", color='blue')
plt.title(f"{symbol} Closing Price from {start_date} to {end_date}")
plt.xlabel("Date")
plt.ylabel("Price (USD)")
plt.legend()
plt.grid(True, color='gray', linestyle='--', linewidth=0.5)
plt.savefig("closing_price_plot.png", dpi=300, bbox_inches='tight')
plt.show()
S&P 500 Closing Price Chart
Split the dataset:
df_train = df.loc[start_date:train_cutoff_date].copy()
df_test = df.loc[train_cutoff_date:end_date].copy()The Strategy: Double Moving Average Crossover
This is one of the simplest strategies out there. The Double Moving Average Crossover (DMAC) strategy works in the following way:
Buy when the short-term moving average crosses above the long-term one.
Sell when the opposite happens.
Here’s the backtest function:
def backtest_strategy_double_ma(data, short_window, long_window, initial_capital):
df = data.copy()
# Compute short and long moving averages
df['SMA_Short'] = df['Close'].rolling(window=short_window).mean()
df['SMA_Long'] = df['Close'].rolling(window=long_window).mean()
# Generate buy signal when short MA crosses above long MA
df['Signal'] = 0
df.loc[df['SMA_Short'] > df['SMA_Long'], 'Signal'] = 1
# Lag position by 1 day
df['Position'] = df['Signal'].shift(1)
# Calculate daily returns
df['Return'] = df['Close'].pct_change()
df['Strategy Return'] = df['Position'] * df['Return']
# Equity curve
df['Equity Curve'] = (1 + df['Strategy Return']).cumprod() * initial_capital
# Metrics
final_value = df['Equity Curve'].iloc[-1]
num_trades = df['Position'].diff().abs().sum()
return final_value, num_trades, dfWe’ll use an initial capital of $10:
initial_capital = 10The Optimization: Genetic Algorithm
The goal is to find the best combination of short and long windows using the Genetic Algorithm.
We define an evaluation function that penalizes invalid pairs and returns the final portfolio value:
# Set seed for reproducibility
random.seed(42)
# Define evaluation function to maximize (final equity)
def eval_strategy(individual):
short_window, long_window = individual
# Constraint: short_window < long_window
if short_window >= long_window:
return -np.inf, # Penalize invalid individuals
final_value, trades, _ = backtest_strategy_double_ma(df_train, short_window, long_window, initial_capital)
return final_value, # DEAP expects a tupleDefine the DEAP genetic algorithm setup:
# Create fitness and individual classes
creator.create("FitnessMax", base.Fitness, weights=(1.0,)) # maximize final equity
creator.create("Individual", list, fitness=creator.FitnessMax)
toolbox = base.Toolbox()
# Define attribute generators for short and long windows
toolbox.register("short_window_attr", random.randint, 5, 50)
toolbox.register("long_window_attr", random.randint, 55, 200)
# Structure initializers
toolbox.register("individual", tools.initCycle, creator.Individual,
(toolbox.short_window_attr, toolbox.long_window_attr), n=1)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
# Register the evaluation function
toolbox.register("evaluate", eval_strategy)
# Register the crossover operator (uniform crossover)
toolbox.register("mate", tools.cxUniform, indpb=0.5)We also define a custom mutation function:
# Register a mutation operator (mutate one window by adding/subtracting 1-3 days)
def mutate_individual(individual):
if random.random() < 0.5:
individual[0] += random.choice([-3, -2, -1, 1, 2, 3])
individual[0] = max(5, min(individual[0], 50))
else:
individual[1] += random.choice([-15, -10, -5, 5, 10, 15])
individual[1] = max(55, min(individual[1], 200))
return individual,
toolbox.register("mutate", mutate_individual)
# Register the selection operator (tournament selection)
toolbox.register("select", tools.selTournament, tournsize=3)Now run the optimization multiple times and track the best outcome:
# GA parameters
population_size = 40
num_generations = 20
cx_prob = 0.7 # crossover probability
mut_prob = 0.2 # mutation probability
num_runs = 10
times = []
best_final_value = -float('inf')
best_params_overall = None
best_num_trades = None
for run in range(num_runs):
print(f"Genetic Algorithm Run {run+1}/{num_runs}")
# Re-create population and hall of fame fresh each run
pop = toolbox.population(n=population_size)
hof = tools.HallOfFame(1)
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("max", np.max)
stats.register("min", np.min)
start_time = time.time()
pop, log = algorithms.eaSimple(
pop, toolbox,
cxpb=cx_prob,
mutpb=mut_prob,
ngen=num_generations,
stats=stats,
halloffame=hof,
verbose=True
)
end_time = time.time()
times.append(end_time - start_time)
candidate = hof[0]
candidate_final_value, candidate_num_trades, _ = backtest_strategy_double_ma(df_train, candidate[0], candidate[1], initial_capital)
if candidate_final_value > best_final_value:
best_final_value = candidate_final_value
best_params_overall = candidate
best_num_trades = candidate_num_trades
average_time = sum(times) / num_runs
best_short_window, best_long_window = best_params_overall
print(f"\nAverage GA runtime over {num_runs} runs: {average_time:.2f} seconds ({average_time/60:.2f} minutes).")
print("\nBest Strategy Parameters Found (best of all runs):")
print(f"Short MA Window : {best_short_window}")
print(f"Long MA Window : {best_long_window}")
print(f"Final Value : ${best_final_value:,.2f}")
print(f"Number of Trades: {int(best_num_trades)}")Average GA runtime over 10 runs: 3.59 seconds (0.06 minutes).
Best Strategy Parameters Found (best of all runs):
Short MA Window : 46
Long MA Window : 166
Final Value : $111.11
Number of Trades: 35Results: Visualizing the Strategy
Let’s visualize the moving averages and trades on the training set:
# Run backtest on training data with best short and long MA windows
_, _, df_train_result = backtest_strategy_double_ma(df_train, best_short_window, best_long_window, initial_capital)
# Plot Close price and the two moving averages (full dataset)
plt.figure(figsize=(14, 6))
plt.plot(df_train_result['Close'], label='Close Price', alpha=0.6, color='blue')
plt.plot(df_train_result['SMA_Short'], label=f'{best_short_window}-day SMA', color='orange', linestyle='-')
plt.plot(df_train_result['SMA_Long'], label=f'{best_long_window}-day SMA', color='green', linestyle='--')
# Chart formatting
plt.title(f"Training Set: {symbol} Close Price with {best_short_window}-day and {best_long_window}-day SMAs")
plt.xlabel("Date")
plt.ylabel("Price (USD)")
plt.legend()
plt.grid(True, color='gray', linestyle='--', linewidth=0.5)
plt.tight_layout()
plt.savefig("training_set_sma_plot.png", dpi=300, bbox_inches='tight')
plt.show()
The Moving Averages on the S&P 500 Index Data
Now test the strategy on out-of-sample data:
final_value, trades, df_test_result = backtest_strategy_double_ma(
df_test,
best_short_window,
best_long_window,
initial_capital
)Plot the signals:
plt.figure(figsize=(14, 6))
plt.plot(df_test_result['Close'], label='Close Price', alpha=0.6, color='blue')
plt.plot(df_test_result['SMA_Short'], label=f'{best_short_window}-day SMA', color='orange')
plt.plot(df_test_result['SMA_Long'], label=f'{best_long_window}-day SMA', color='green')
# Identify buy signals: when short MA crosses above long MA
buy_signals = df_test_result[(df_test_result['SMA_Short'].shift(1) <= df_test_result['SMA_Long'].shift(1)) &
(df_test_result['SMA_Short'] > df_test_result['SMA_Long'])]
# Identify sell signals: when short MA crosses below long MA
sell_signals = df_test_result[(df_test_result['SMA_Short'].shift(1) >= df_test_result['SMA_Long'].shift(1)) &
(df_test_result['SMA_Short'] < df_test_result['SMA_Long'])]
# Plot buy/sell signals with enhancements
plt.scatter(buy_signals.index, buy_signals['Close'], marker='^', color='lime', s=120, label='Buy', zorder=5)
plt.scatter(sell_signals.index, sell_signals['Close'], marker='v', color='red', s=120, label='Sell', zorder=5)
plt.title(f"Out-of-Sample Signals: {symbol} ({best_short_window}/{best_long_window}-day SMA)")
plt.xlabel("Date")
plt.ylabel("Price (USD)")
plt.legend()
plt.grid(True, color='gray', linestyle='--', linewidth=0.5)
plt.savefig("out_of_sample_signals_plot.png", dpi=300, bbox_inches='tight')
plt.show()
Out-of-Sample Signals Using the Optimal Window Sizes
Seasonal Behavior: Monthly Patterns
Let’s see how the strategy performs on average by calendar month:
# Calculate monthly returns for strategy and buy & hold
strategy_returns = df_test_result['Strategy Return'].resample('M').apply(lambda x: (1 + x).prod() - 1)
buyhold_returns = df_test_result['Return'].resample('M').apply(lambda x: (1 + x).prod() - 1)
# Convert to DataFrame with Year and Month for strategy
strategy_df = strategy_returns.to_frame(name='Strategy Return')
strategy_df['Year'] = strategy_df.index.year
strategy_df['Month'] = strategy_df.index.month_name()
# Convert to DataFrame with Year and Month for buy & hold
buyhold_df = buyhold_returns.to_frame(name='Buy & Hold Return')
buyhold_df['Year'] = buyhold_df.index.year
buyhold_df['Month'] = buyhold_df.index.month_name()
# Compute average monthly returns across years
avg_strategy = strategy_df.groupby('Month')['Strategy Return'].mean()
avg_buyhold = buyhold_df.groupby('Month')['Buy & Hold Return'].mean()
# Order months Jan to Dec
month_order = list(calendar.month_name)[1:]
avg_strategy = avg_strategy.reindex(month_order) * 100 # convert to percentage
avg_buyhold = avg_buyhold.reindex(month_order) * 100 # convert to percentage
# Bar positions and width
x = np.arange(len(month_order))
width = 0.35
plt.figure(figsize=(14, 7))
# Plot bars for strategy and buy & hold returns
bars1 = plt.bar(x - width/2, avg_strategy, width, label='Strategy', color='green')
bars2 = plt.bar(x + width/2, avg_buyhold, width, label='Buy & Hold', color='orange')
# Horizontal zero line at y=0
plt.axhline(0, color='white', linewidth=1.2)
# Add both horizontal and vertical grid lines with light gray dashed style and opacity
plt.grid(axis='y', color='lightgray', linestyle='--', alpha=0.7)
plt.grid(axis='x', color='lightgray', linestyle='--', alpha=0.7)
# Annotate each bar with its value
for bars in [bars1, bars2]:
for bar in bars:
height = bar.get_height()
if height >= 0:
plt.text(bar.get_x() + bar.get_width() / 2, height + 0.6, f"{height:.1f}%",
ha='center', va='bottom', fontsize=12)
else:
plt.text(bar.get_x() + bar.get_width() / 2, height - 0.6, f"{height:.1f}%",
ha='center', va='top', fontsize=12)
# Set x-axis labels and rotation for readability
plt.xticks(x, month_order, rotation=45)
plt.ylabel('Average Monthly Return (%)')
plt.title('Average Monthly Returns (%) - Strategy vs Buy & Hold')
plt.legend()
plt.tight_layout()
plt.savefig("average_monthly_returns.png", dpi=300, bbox_inches='tight')
plt.show()
Average Monthly Returns between Buy and Hold and the Optimized DMAC Strategy
Performance Comparison
Compare the strategy to a simple buy-and-hold benchmark:
# Calculate Buy & Hold cumulative portfolio value
df_test_result['Buy & Hold'] = (1 + df_test_result['Return']).cumprod() * initial_capital
# Plot strategy equity curve vs buy & hold
plt.figure(figsize=(12, 6))
plt.plot(df_test_result['Equity Curve'], label='Strategy (Double MA)', color='green')
plt.plot(df_test_result['Buy & Hold'], label='Buy & Hold', linestyle='--', color='orange')
plt.title(f"Out-of-Sample Equity Curve: {symbol}")
plt.xlabel("Date")
plt.ylabel("Portfolio Value (USD)")
plt.legend()
plt.grid(True, color='gray', linestyle='--', linewidth=0.5)
plt.savefig("equity_curve_plot.png", dpi=300, bbox_inches='tight')
plt.show()
Out-of-Sample Equity Curve Plot
Create a performance summary:
final_strategy_value = df_test_result['Equity Curve'].iloc[-1]
final_bh_value = df_test_result['Buy & Hold'].iloc[-1]
strategy_return_pct = ((final_strategy_value / initial_capital) - 1) * 100
bh_return_pct = ((final_bh_value / initial_capital) - 1) * 100
summary = pd.DataFrame({
"Metric": [
"Optimized Short MA Window",
"Optimized Long MA Window",
"Final Strategy Value (Test)",
"Final Buy & Hold Value (Test)",
"Strategy Return (%)",
"Buy & Hold Return (%)",
"Number of Trades (Test)",
"Average Time per Run (seconds)"
],
"Value": [
best_short_window,
best_long_window,
final_strategy_value,
final_bh_value,
strategy_return_pct,
bh_return_pct,
int(trades),
average_time
]
}).set_index("Metric")
# Save summary to CSV
summary.to_csv("strategy_summary.csv")
summaryMetric Value
Optimized Short MA Window : 46
Optimized Long MA Window : 166
Final Strategy Value (Test) : 18.453874
Final Buy & Hold Value (Test) : 18.283324
Strategy Return (%) : 84.538742
Buy & Hold Return (%) : 82.833244
Number of Trades (Test) : 3.000000
Average Time per Run (seconds) : 3.590756Analysis
To evaluate the effectiveness of the optimized strategy, I compared it to a basic buy-and-hold benchmark over the same out-of-sample test period.
Both approaches started with an initial portfolio value of $10.
The buy-and-hold strategy, which passively tracks the S&P 500, grew to approximately $18.28.
In comparison, the double moving average strategy — tuned using the genetic algorithm — ended with a slightly higher final value of $18.45.
Although the difference in returns is modest (84.5% vs 82.8%), the Genetic Algorithm–optimized strategy slightly outperformed buy-and-hold. What’s more, it did so with just 3 trades during the entire test period.
The strategy also remained interpretable, using a 46-day short window and a 166-day long window — parameters that were discovered through multiple runs of the genetic algorithm, each averaging under 4 seconds.
This test doesn’t prove the strategy is superior in all environments, but it does demonstrate how a simple rule-based system, when tuned effectively, can compete with passive investing over long periods while maintaining discipline and avoiding overfitting.
Seasonal Behavior: Monthly Patterns
Let’s see how the strategy performs on average by calendar month:
# Calculate monthly returns for strategy and buy & hold
strategy_returns = df_test_result['Strategy Return'].resample('M').apply(lambda x: (1 + x).prod() - 1)
buyhold_returns = df_test_result['Return'].resample('M').apply(lambda x: (1 + x).prod() - 1)
# Convert to DataFrame with Year and Month for strategy
strategy_df = strategy_returns.to_frame(name='Strategy Return')
strategy_df['Year'] = strategy_df.index.year
strategy_df['Month'] = strategy_df.index.month_name()
# Convert to DataFrame with Year and Month for buy & hold
buyhold_df = buyhold_returns.to_frame(name='Buy & Hold Return')
buyhold_df['Year'] = buyhold_df.index.year
buyhold_df['Month'] = buyhold_df.index.month_name()
# Compute average monthly returns across years
avg_strategy = strategy_df.groupby('Month')['Strategy Return'].mean()
avg_buyhold = buyhold_df.groupby('Month')['Buy & Hold Return'].mean()
# Order months Jan to Dec
month_order = list(calendar.month_name)[1:]
avg_strategy = avg_strategy.reindex(month_order) * 100 # convert to percentage
avg_buyhold = avg_buyhold.reindex(month_order) * 100 # convert to percentage
# Bar positions and width
x = np.arange(len(month_order))
width = 0.35
plt.figure(figsize=(14, 7))
# Plot bars for strategy and buy & hold returns
bars1 = plt.bar(x - width/2, avg_strategy, width, label='Strategy', color='green')
bars2 = plt.bar(x + width/2, avg_buyhold, width, label='Buy & Hold', color='orange')
# Horizontal zero line at y=0
plt.axhline(0, color='white', linewidth=1.2)
# Add both horizontal and vertical grid lines with light gray dashed style and opacity
plt.grid(axis='y', color='lightgray', linestyle='--', alpha=0.7)
plt.grid(axis='x', color='lightgray', linestyle='--', alpha=0.7)
# Annotate each bar with its value
for bars in [bars1, bars2]:
for bar in bars:
height = bar.get_height()
if height >= 0:
plt.text(bar.get_x() + bar.get_width() / 2, height + 0.6, f"{height:.1f}%",
ha='center', va='bottom', fontsize=12)
else:
plt.text(bar.get_x() + bar.get_width() / 2, height - 0.6, f"{height:.1f}%",
ha='center', va='top', fontsize=12)
# Set x-axis labels and rotation for readability
plt.xticks(x, month_order, rotation=45)
plt.ylabel('Average Monthly Return (%)')
plt.title('Average Monthly Returns (%) - Strategy vs Buy & Hold')
plt.legend()
plt.tight_layout()
plt.savefig("average_monthly_returns.png", dpi=300, bbox_inches='tight')
plt.show()
Average Monthly Returns between Buy and Hold and the Optimized DMAC Strategy
There are no guarantees in financial markets. This strategy doesn’t promise outperformance — but it illustrates a workflow that’s highly adaptable and transparent.
Optimizing parameters using evolutionary logic is far from new, but it remains underused in applied trading experimentation.
If nothing else, it’s a great way to test, tune, and trust your process.
Subscribe to our premium content to read the rest.
Become a paying subscriber to get access to this post and other subscriber-only content.
Upgrade





