- GuruFinance Insights
- Posts
- I Put the Two Most Popular Trading Strategies to the Test with Bayesian Optimization
I Put the Two Most Popular Trading Strategies to the Test with Bayesian Optimization
Can These Strategies Beat the Market?
Try inFlow Free—$499 Off for a Limited Time
Get started for free and see how simple inventory management can be.
inFlow helps you stay on top of inventory, track costs with precision, and protect your bottom line.
You’ll always know how much you’re spending, what you’re making, and where you can save.
It also simplifies inventory, orders, shipping, and barcode scanning in one easy-to-use system—rated “easy to use” by 93% of users.
Rated 4.6 stars across 500+ reviews on Capterra and named a top pick in multiple competitive comparisons
inFlow connects with Shopify, Amazon, QuickBooks, UPS, and 90+ other tools, so everything works together without the manual work.
Try it free and, for a limited time, save $499 when you upgrade with code EASY499.
An inFlow specialist can show you how to simplify inventory from day one
✅ See how others are navigating change in our case studies
🚀 Compare plans on our pricing page
🚀 Your Investing Journey Just Got Better: Premium Subscriptions Are Here! 🚀
It’s been 4 months since we launched our premium subscription plans at GuruFinance Insights, and the results have been phenomenal! Now, we’re making it even better for you to take your investing game to the next level. Whether you’re just starting out or you’re a seasoned trader, our updated plans are designed to give you the tools, insights, and support you need to succeed.
Here’s what you’ll get as a premium member:
Exclusive Trading Strategies: Unlock proven methods to maximize your returns.
In-Depth Research Analysis: Stay ahead with insights from the latest market trends.
Ad-Free Experience: Focus on what matters most—your investments.
Monthly AMA Sessions: Get your questions answered by top industry experts.
Coding Tutorials: Learn how to automate your trading strategies like a pro.
Masterclasses & One-on-One Consultations: Elevate your skills with personalized guidance.
Our three tailored plans—Starter Investor, Pro Trader, and Elite Investor—are designed to fit your unique needs and goals. Whether you’re looking for foundational tools or advanced strategies, we’ve got you covered.
Don’t wait any longer to transform your investment strategy. The last 4 months have shown just how powerful these tools can be—now it’s your turn to experience the difference.

Comparison of Optimized DMAC and RSI Trading Strategies Against Buy & Hold
When it comes to trading strategies, I’ve often seen people pick parameters arbitrarily. Maybe it’s a 50-day moving average or a 14-day RSI because “that’s what people use.”
But I always wondered: what if we let the data decide?
I let Bayesian Optimization tune their parameters for me. My goal was to see which strategy performs better after data-driven optimization and how each fares against a Buy & Hold baseline using real stock price data from Microsoft (MSFT) between 2020 and 2025.
I’ll show you the actual Python code I used.
Marketing ideas for marketers who hate boring
The best marketing ideas come from marketers who live it. That’s what The Marketing Millennials delivers: real insights, fresh takes, and no fluff. Written by Daniel Murray, a marketer who knows what works, this newsletter cuts through the noise so you can stop guessing and start winning. Subscribe and level up your marketing game.
Background on the Two Strategies
1. Dual Moving Average Crossover (DMAC)
This is a trend-following strategy built on the relationship between two simple moving averages (SMAs):
Short-term SMA: Reacts quickly to price changes.
Long-term SMA: Reacts more slowly, smoothing out the noise.
How it works:
Buy Signal: When the short SMA crosses above the long SMA, it indicates upward momentum.
Sell Signal: When the short SMA crosses below the long SMA, it indicates potential weakness.
Despite its simplicity, DMAC is one of the most used systems by retail and algorithmic traders alike. But the results can vary dramatically depending on the moving average lengths chosen, which is exactly what I optimized here.
2. RSI Mean Reversion
The Relative Strength Index (RSI) is a momentum oscillator ranging from 0 to 100. It captures the magnitude of recent price changes.
How this strategy works:
Buy Signal: When RSI drops below a certain threshold (say, 30), the asset is considered oversold and ripe for a rebound.
Sell Signal: When RSI rises above a threshold (like 70), the asset is considered overbought.
This is a mean-reversion strategy. It assumes that prices will eventually return to the mean, so extreme values present trading opportunities.
Background on the Optimization Algorithm
Tuning these strategies manually would be inefficient and biased. That’s why I used Bayesian Optimization.
Bayesian Optimization is a method for finding the optimum of a function that is expensive to evaluate, like a full backtest.
Instead of testing every possible combination (like grid search), it builds a probabilistic model to decide which parameter set is most promising to try next.
This makes it highly efficient and ideal for hyperparameter tuning in both machine learning and algorithmic trading.
Python Implementation
Installing and Importing Dependencies
%pip install yfinance matplotlib pandas bayesian-optimization
We start by installing the required libraries:
yfinance
: to fetch historical stock data.matplotlib
: for plotting.pandas
: for data manipulation.bayesian-optimization
: to perform our parameter search efficiently.
Next, we import everything we need.
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
from bayes_opt import BayesianOptimization
import matplotlib.dates as mdates
import numpy as np
plt.style.use("dark_background")
Setting Up Experiment Parameters
# Parameters
ticker = "MSFT"
start_date = "2020-01-01"
end_date = "2025-06-30"
train_cutoff_date = "2023-12-31"
initial_capital = 10
Here we define our test parameters:
We’re analyzing Microsoft stock (
MSFT
).The backtest runs from 2020 to mid-2025.
Data before 2023–12–31 is used for training/optimization.
Data after that is reserved for testing.
We start with a mock capital of $10 just for normalization — the scale doesn’t affect percentage returns.
Downloading and Preparing Price Data
df = yf.download(ticker, start=start_date, end=end_date, auto_adjust=True)
df.columns = df.columns.get_level_values(0)
df = df[['Close']]
df.head()
This downloads daily adjusted closing prices for MSFT and selects only the Close
column for our analysis.
Visualizing Train/Test Splits
Here, I wanted a clear visual separation of training vs testing periods.
The following code plots the closing price along with vertical lines and shaded regions to show where the model was trained and where it was evaluated.
plt.figure(figsize=(12, 6))
plt.plot(df.index, df['Close'], label='Close Price', color='blue')
# Convert string dates to datetime
start_dt = pd.to_datetime(start_date)
train_cutoff_dt = pd.to_datetime(train_cutoff_date)
end_dt = pd.to_datetime(end_date)
# Vertical cutoff lines
plt.axvline(start_dt, color='gray', linestyle='--', alpha=0.7)
plt.axvline(train_cutoff_dt, color='orange', linestyle='--', alpha=0.7)
plt.axvline(end_dt, color='gray', linestyle='--', alpha=0.7)
# Highlight train and test regions
plt.axvspan(start_dt, train_cutoff_dt, color='green', alpha=0.2)
plt.axvspan(train_cutoff_dt, end_dt, color='red', alpha=0.2)
# Text labels centered in train/test regions
train_mid = start_dt + (train_cutoff_dt - start_dt) / 2
test_mid = train_cutoff_dt + (end_dt - train_cutoff_dt) / 2
plt.text(train_mid, plt.ylim()[1]*0.9, 'Train', horizontalalignment='center', color='white', fontsize=14, fontweight='bold', alpha=0.7)
plt.text(test_mid, plt.ylim()[1]*0.9, 'Test', horizontalalignment='center', color='white', fontsize=14, fontweight='bold', alpha=0.7)
plt.title(f"{ticker} Closing Price with Train/Test Periods")
plt.xlabel("Date")
plt.ylabel("Price (USD)")
plt.grid(True, linestyle='--', alpha=0.5)
plt.legend()
plt.tight_layout()
plt.savefig("closing_price_with_train_test_periods.png", dpi=300)
plt.show()

Microsoft’s Closing Price Chart
This visual immediately confirms that our train/test split is logical and covers both bullish and sideways markets, a good real-world testing ground.
Marketing ideas for marketers who hate boring
The best marketing ideas come from marketers who live it. That’s what The Marketing Millennials delivers: real insights, fresh takes, and no fluff. Written by Daniel Murray, a marketer who knows what works, this newsletter cuts through the noise so you can stop guessing and start winning. Subscribe and level up your marketing game.
Calculating Returns
After fetching and plotting the data, we calculate the daily percentage returns from the closing prices.
These returns are essential for both strategies since our backtesting logic will multiply these returns by the strategy’s signal (long, short, or neutral) to simulate equity growth over time.
df['Return'] = df['Close'].pct_change().fillna(0)
df.head()
Next, we split the data into training and testing sets based on the cutoff date we defined earlier:
df_train = df.loc[start_date:train_cutoff_date].copy()
df_test = df.loc[train_cutoff_date:end_date].copy()
This gives us two datasets:
df_train
— used to optimize strategy parameters.df_test
— used to test how those optimized strategies perform on unseen data.
Strategy 1: Dual Moving Average Crossover (DMAC)
Here we define the core of the DMAC strategy:
def backtest_dmac(data, short_window, long_window, capital):
data = data.copy()
data['SMA_Short'] = data['Close'].rolling(int(short_window)).mean()
data['SMA_Long'] = data['Close'].rolling(int(long_window)).mean()
data.dropna(inplace=True)
data['Signal'] = 0
data.loc[data['SMA_Short'] > data['SMA_Long'], 'Signal'] = 1
data.loc[data['SMA_Short'] < data['SMA_Long'], 'Signal'] = -1
data['Position'] = data['Signal'].shift()
data['Position'] = data['Position'].ffill()
data['Strategy_Return'] = data['Position'] * data['Return']
data['Equity Curve'] = (1 + data['Strategy_Return']).cumprod() * capital
return data
What this function does:
Computes short and long Simple Moving Averages.
Generates signals:
1
for long,-1
for short.Shifts the position by one day (you act on yesterday’s signal).
Fills missing positions by forward filling.
Applies the position to daily returns to simulate strategy performance.
Compounds the strategy’s returns to produce an equity curve — the simulated portfolio value over time.
Strategy 2: RSI Mean Reversion
Here’s the backtest logic for the second strategy:
def backtest_rsi(data, rsi_window, oversold, overbought, capital):
data = data.copy()
delta = data['Close'].diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.rolling(int(rsi_window)).mean()
avg_loss = loss.rolling(int(rsi_window)).mean()
rs = avg_gain / (avg_loss + 1e-10)
rsi = 100 - (100 / (1 + rs))
data['RSI'] = rsi
# Generate signals
data['Signal'] = 0
data.loc[data['RSI'] < oversold, 'Signal'] = 1
data.loc[data['RSI'] > overbought, 'Signal'] = -1
data['Position'] = data['Signal'].shift()
data['Position'] = data['Position'].ffill() # <-- Fix applied here
data['Return'] = data['Close'].pct_change().fillna(0)
data['Strategy_Return'] = data['Position'] * data['Return']
data['Equity Curve'] = (1 + data['Strategy_Return']).cumprod() * capital
return data
What this function does:
Computes RSI using rolling gains and losses.
Triggers a buy when RSI drops below
oversold
.Triggers a sell when RSI rises above
overbought
.Like DMAC, it shifts and forward-fills positions, multiplies returns, and compounds the result to track equity.
Optimizing the DMAC Strategy with Bayesian Optimization
Here’s where we stop guessing and start tuning. I used Bayesian Optimization to find the best short/long moving average windows by maximizing the final portfolio value on the training set.
def dmac_objective(short_window, long_window):
short_window = int(round(short_window))
long_window = int(round(long_window))
if short_window >= long_window:
return -1e10
equity = backtest_dmac(df_train, short_window, long_window, initial_capital)['Equity Curve'].iloc[-1]
return equity
This function returns the final equity value for a given set of parameters. If the short window is greater than or equal to the long (which breaks the strategy), it returns a large negative penalty.
dmac_bo = BayesianOptimization(
f=dmac_objective,
pbounds={'short_window': (5, 50), 'long_window': (55, 200)},
random_state=42,
verbose=0
)
dmac_bo.maximize(init_points=5, n_iter=45)
dmac_best = dmac_bo.max['params']
This block:
Initializes the optimizer with bounds.
Runs 5 random points to start, then 45 guided steps.
Stores the best parameters in
dmac_best
.
# Extract best window sizes
best_short = int(round(dmac_best['short_window']))
best_long = int(round(dmac_best['long_window']))
print(f"Optimized DMAC parameters found:")
print(f" Short Window: {best_short}")
print(f" Long Window: {best_long}")
Optimized DMAC parameters found:
Short Window: 31
Long Window: 184
Now we extract the best short and long windows to use later.
Visualizing Optimized DMAC Strategy
# Prepare data for plotting (using the entire dataset or test set)
plot_data = df.copy()
plot_data['SMA_Short'] = plot_data['Close'].rolling(best_short).mean()
plot_data['SMA_Long'] = plot_data['Close'].rolling(best_long).mean()
plt.figure(figsize=(12, 6))
plt.plot(plot_data.index, plot_data['Close'], label='Close Price', color='blue', alpha=0.8)
plt.plot(plot_data.index, plot_data['SMA_Short'], label=f'SMA Short ({best_short})', color='green', linewidth=2)
plt.plot(plot_data.index, plot_data['SMA_Long'], label=f'SMA Long ({best_long})', color='red', linewidth=2)
# Convert string dates to datetime
start_dt = pd.to_datetime(start_date)
train_cutoff_dt = pd.to_datetime(train_cutoff_date)
end_dt = pd.to_datetime(end_date)
# Vertical cutoff lines
plt.axvline(start_dt, color='gray', linestyle='--', alpha=0.7)
plt.axvline(train_cutoff_dt, color='orange', linestyle='--', alpha=0.7)
plt.axvline(end_dt, color='gray', linestyle='--', alpha=0.7)
# Highlight train and test regions
plt.axvspan(start_dt, train_cutoff_dt, color='green', alpha=0.2)
plt.axvspan(train_cutoff_dt, end_dt, color='red', alpha=0.2)
# Text labels centered in train/test regions
train_mid = start_dt + (train_cutoff_dt - start_dt) / 2
test_mid = train_cutoff_dt + (end_dt - train_cutoff_dt) / 2
plt.text(train_mid, plt.ylim()[1]*0.9, 'Train', horizontalalignment='center', color='white', fontsize=14, fontweight='bold', alpha=0.7)
plt.text(test_mid, plt.ylim()[1]*0.9, 'Test', horizontalalignment='center', color='white', fontsize=14, fontweight='bold', alpha=0.7)
plt.title(f"{ticker} Closing Price with Optimized DMAC Windows")
plt.xlabel("Date")
plt.ylabel("Price (USD)")
plt.legend()
plt.grid(True, linestyle='--', alpha=0.5)
plt.tight_layout()
plt.savefig("dmac_optimized_windows.png", dpi=300)
plt.show()

DMAC Closing Price with Optimized Windows
This gives us a visual confirmation that the chosen windows reflect distinct crossover patterns and periods of alignment between short and long trends.
RSI Strategy Optimization using Bayesian Optimization
Once we had the RSI strategy logic in place, the next step was tuning it. Specifically, we wanted to find the best combination of:
rsi_window
: the number of periods used to compute the RSI,oversold
: the threshold below which we trigger buy signals, andoverbought
: the threshold above which we trigger sell signals.
Just like with the DMAC strategy, we used Bayesian Optimization to search for the parameter combination that gave the highest final portfolio value over the training period.
Running Bayesian Optimization for RSI
We now define the objective function for the RSI strategy and run Bayesian optimization to fine the optimal parameters.
def rsi_objective(rsi_window, oversold, overbought):
rsi_window = int(round(rsi_window))
oversold = float(oversold)
overbought = float(overbought)
# Make sure oversold < overbought
if oversold >= overbought:
return -1e10
try:
result = backtest_rsi(df_train, rsi_window, oversold, overbought, initial_capital)
result = result.dropna(subset=['Equity Curve']) # <== Drop NaNs
if result.empty:
return -1e10
return result['Equity Curve'].iloc[-1]
except Exception as e:
print("Error during optimization:", e)
return -1e10
rsi_bo = BayesianOptimization(
f=rsi_objective,
pbounds={'rsi_window': (5, 30), 'oversold': (10, 40), 'overbought': (60, 90)},
random_state=42,
verbose=0
)
rsi_bo.maximize(init_points=5, n_iter=45)
rsi_best = rsi_bo.max['params']
We gave it a reasonable search space:
RSI window between 5 and 30,
oversold threshold between 10 and 40,
overbought threshold between 60 and 90.
And after 50 total evaluations, here were the best parameters it found:
print(f"Optimized RSI parameters found:")
print(f" RSI Window: {int(round(rsi_best['rsi_window']))}")
print(f" Oversold Level: {rsi_best['oversold']:.2f}")
print(f" Overbought Level: {rsi_best['overbought']:.2f}")
The output was something like:
Optimized RSI parameters found:
RSI Window: 5
Oversold Level: 40.00
Overbought Level: 78.33
Visualizing the RSI Strategy
We applied the optimized RSI parameters to the full dataset to see how the strategy would have performed. Here’s what that looked like.
# Convert dates to datetime
start_dt = pd.to_datetime(start_date)
train_cutoff_dt = pd.to_datetime(train_cutoff_date)
end_dt = pd.to_datetime(end_date)
# Extract optimized params
opt_rsi_window = int(round(rsi_best['rsi_window']))
opt_oversold = rsi_best['oversold']
opt_overbought = rsi_best['overbought']
# Run backtest with optimized params on full df or test set
rsi_result = backtest_rsi(df.copy(), opt_rsi_window, opt_oversold, opt_overbought, initial_capital)
fig, axs = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# 1. Plot Closing Price
axs[0].plot(rsi_result.index, rsi_result['Close'], label='Close Price', color='blue')
axs[0].set_title(f"{ticker} Close Price")
axs[0].set_ylabel("Price (USD)")
axs[0].grid(True, linestyle='--', alpha=0.5)
axs[0].legend()
# 2. Plot RSI with oversold/overbought lines and signals
axs[1].plot(rsi_result.index, rsi_result['RSI'], label='RSI', color='purple')
axs[1].axhline(opt_oversold, color='green', linestyle='--', label=f'Oversold ({opt_oversold:.1f})')
axs[1].axhline(opt_overbought, color='red', linestyle='--', label=f'Overbought ({opt_overbought:.1f})')
axs[1].set_ylabel("RSI")
axs[1].set_ylim(0, 100)
axs[1].grid(True, linestyle='--', alpha=0.5)
axs[1].legend()
# Add train/test overlays on all subplots
for ax in axs:
ax.axvline(start_dt, color='gray', linestyle='--', alpha=0.7)
ax.axvline(train_cutoff_dt, color='orange', linestyle='--', alpha=0.7)
ax.axvline(end_dt, color='gray', linestyle='--', alpha=0.7)
ax.axvspan(start_dt, train_cutoff_dt, color='green', alpha=0.2)
ax.axvspan(train_cutoff_dt, end_dt, color='red', alpha=0.2)
train_mid = start_dt + (train_cutoff_dt - start_dt) / 2
test_mid = train_cutoff_dt + (end_dt - train_cutoff_dt) / 2
ylim = ax.get_ylim()
ax.text(train_mid, ylim[1]*0.9, 'Train', ha='center', color='white', fontsize=12, fontweight='bold', alpha=0.7)
ax.text(test_mid, ylim[1]*0.9, 'Test', ha='center', color='white', fontsize=12, fontweight='bold', alpha=0.7)
plt.suptitle(
f"RSI Strategy Performance with Optimized Parameters\n"
f"Window={opt_rsi_window}, Oversold={opt_oversold:.2f}, Overbought={opt_overbought:.2f}",
fontsize=16
)
plt.tight_layout(rect=[0, 0, 1, 0.96])
plt.savefig("rsi_optimized_performance.png", dpi=300)
plt.show()

RSI Strategy Perfomance with Optimized Parameters
Just like before, we shaded the training period in green and the testing period in red for easy visual reference.
Buy signals occurred when RSI dropped below the green oversold line.
Sell signals occurred when RSI rose above the red overbought line.
The test period performance looked different from training — which gives insight into how well the strategy generalizes.
Test Performance Comparison: DMAC vs RSI vs Buy & Hold
Once both strategies were optimized using training data, it was time for the real test: how well do they perform on the test set?
We applied the best parameters found for both DMAC and RSI strategies to the df_test
subset. This is how you simulate live trading using a model you trained.
# DMAC
dmac_test = backtest_dmac(
df_test,
short_window=round(dmac_best['short_window']),
long_window=round(dmac_best['long_window']),
capital=initial_capital
)
# RSI
rsi_test = backtest_rsi(
df_test,
rsi_window=round(rsi_best['rsi_window']),
oversold=rsi_best['oversold'],
overbought=rsi_best['overbought'],
capital=initial_capital
)
Both functions return a DataFrame containing the equity curve, trades, and returns.
Performance Summary: Final Value, Return %, and Trades
I summarized the final portfolio value, percentage return, and number of trades for each strategy. I also calculated Buy & Hold using only the Close prices.
All three strategies were then summarized in a DataFrame:
def summarize_results(df, label):
final_value = df['Equity Curve'].iloc[-1]
return_pct = ((final_value / initial_capital) - 1) * 100
trades = df['Position'].diff().abs().sum()
return {
"Strategy": label,
"Final Value": final_value,
"Return (%)": return_pct,
"Trades": int(trades)
}
# Calculate Buy & Hold summary based on df_test Close price
buy_hold_final_value = (df_test['Close'].iloc[-1] / df_test['Close'].iloc[0]) * initial_capital
buy_hold_return_pct = ((buy_hold_final_value / initial_capital) - 1) * 100
buy_hold_summary = {
"Strategy": "Buy & Hold",
"Final Value": buy_hold_final_value,
"Return (%)": buy_hold_return_pct,
"Trades": 0 # no trades for buy & hold
}
# Your existing summaries for DMAC and RSI
dmac_summary = summarize_results(dmac_test, "DMAC")
rsi_summary = summarize_results(rsi_test, "RSI Mean Reversion")
# Combine all summaries
summary_df = pd.DataFrame([dmac_summary, rsi_summary, buy_hold_summary])
summary_df
Strategy Final Value Return (%) Trades
DMAC 7.789603 -22.103972 8
RSI Mean Reversion 11.645477 16.454769 133
Buy & Hold 13.524134 35.241341 0
Visualizing Strategy Returns as Percentage
Instead of plotting absolute portfolio values, I normalized each strategy to start at 0% and then plotted cumulative percentage returns:
plt.figure(figsize=(12, 6))
# Normalize equity curves to start at 0%
dmac_pct = (dmac_test['Equity Curve'] / initial_capital - 1) * 100
rsi_pct = (rsi_test['Equity Curve'] / initial_capital - 1) * 100
buy_hold = (1 + df_test['Close'].pct_change().fillna(0)).cumprod()
buy_hold_pct = (buy_hold / buy_hold.iloc[0] - 1) * 100
# Plot percentage returns
plt.plot(dmac_test.index, dmac_pct, label='DMAC Strategy', color='green')
plt.plot(rsi_test.index, rsi_pct, label='RSI Strategy', color='cyan')
plt.plot(df_test.index, buy_hold_pct, label='Buy & Hold', color='orange', linestyle='--')
plt.title(f"{ticker} - Strategy Return Comparison (%)")
plt.xlabel("Date")
plt.ylabel("Cumulative Return (%)")
plt.legend()
plt.grid(True, linestyle='--', alpha=0.5)
plt.tight_layout()
plt.savefig("strategy_return_comparison.png", dpi=300)
plt.show()

Strategy Return Comparison
Bar Plot Comparison
To make the comparison more visual, I plotted a grouped bar chart for:
Final portfolio value
Return percentage
Number of trades
metrics = ['Final Value', 'Return (%)', 'Trades']
strategies = summary_df['Strategy'].values
values = summary_df[metrics].values.T # shape: (3 metrics, 3 strategies)
x = np.arange(len(strategies))
width = 0.25
fig, axs = plt.subplots(1, 3, figsize=(18, 5))
for i, metric in enumerate(metrics):
axs[i].bar(x, values[i], width, color=['green', 'cyan', 'orange'])
axs[i].set_title(metric)
axs[i].set_xticks(x)
axs[i].set_xticklabels(strategies)
axs[i].grid(axis='y', linestyle='--', alpha=0.5)
# Annotate bars with values
for idx, val in enumerate(values[i]):
if metric == 'Return (%)':
axs[i].text(idx, val + max(values[i])*0.01, f"{val:.2f}%", ha='center', va='bottom', fontsize=10)
elif metric == 'Final Value':
axs[i].text(idx, val + max(values[i])*0.01, f"${val:.2f}", ha='center', va='bottom', fontsize=10)
else:
axs[i].text(idx, val + max(values[i])*0.01, f"{int(val)}", ha='center', va='bottom', fontsize=10)
plt.suptitle(f"{ticker} Strategy Performance Comparison", fontsize=16)
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.savefig("strategy_performance_comparison.png", dpi=300)
plt.show()

Strategy Perfomance Comparison
The RSI Mean Reversion strategy generated a positive return of about 16.45%, but it did so with a very high number of trades (133), indicating frequent position changes and potentially higher transaction costs.
The DMAC strategy performed worse during the test period, ending with a loss of roughly -22.1% and making only 8 trades, which suggests it was more conservative but struggled in this market environment.
The Buy & Hold strategy delivered the highest return at 35.24%, without any trades since it remained invested throughout.
These results show that:
While Buy & Hold had the best raw performance, it also assumes continuous market exposure and risk.
The RSI strategy captured gains but required active trading, which might reduce net profits after costs.
The DMAC strategy was less active and more defensive, but here it struggled to keep up with the rising market.
This underscores the importance of matching a strategy to market conditions.
Defensive strategies like DMAC might protect capital better in sideways or down markets but can underperform in strong uptrends. Conversely, buy-and-hold benefits greatly in trending markets but lacks risk control.