In partnership with

A big 2026 starts now

Most people treat this stretch of the year as dead time. But builders like you know it’s actually prime time. And with beehiiv powering your content, world domination is truly in sight.

On beehiiv, you can launch your website in minutes with the AI Web Builder, publish a professional newsletter with ease, and even tap into huge earnings with the beehiiv Ad Network. It’s everything you need to create, grow, and monetize in one place.

In fact, we’re so hyped about what you’ll create, we’re giving you 30% off your first three months with code BIG30. So forget about taking a break. It’s time for a break-through.

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 gold data download (2008 → today)

  • Real 3-state Gaussian HMM for volatility regimes

  • Beautiful interactive Plotly charts

  • Regime duration & performance tables

  • Ready-to-use CSV export

  • Bonus: works on Bitcoin, SPX, or any ticker with one line change

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)

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.

What investment is rudimentary for billionaires but ‘revolutionary’ for 70,571+ investors entering 2026?

Imagine this. You open your phone to an alert. It says, “you spent $236,000,000 more this month than you did last month.”

If you were the top bidder at Sotheby’s fall auctions, it could be reality.

Sounds crazy, right? But when the ultra-wealthy spend staggering amounts on blue-chip art, it’s not just for decoration.

The scarcity of these treasured artworks has helped drive their prices, in exceptional cases, to thin-air heights, without moving in lockstep with other asset classes.

The contemporary and post war segments have even outpaced the S&P 500 overall since 1995.*

Now, over 70,000 people have invested $1.2 billion+ across 500 iconic artworks featuring Banksy, Basquiat, Picasso, and more.

How? You don’t need Medici money to invest in multimillion dollar artworks with Masterworks.

Thousands of members have gotten annualized net returns like 14.6%, 17.6%, and 17.8% from 26 sales to date.

*Based on Masterworks data. Past performance is not indicative of future returns. Important Reg A disclosures: masterworks.com/cd

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.

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, and

  • overbought: 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.

logo

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

Keep Reading