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 DECEMBER2025 for 20% off

Valid only until January 20, 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:

  • NVDA price data (2015–2024) fetched automatically

  • Double MA Crossover strategy (long-only)

  • Optuna hyperparameter tuning: max return vs max Sharpe

  • Test set results (out-of-sample) + signal visualizations

  • Cumulative return curves vs Buy & Hold

  • Clear performance table (return, Sharpe, trades count)

  • Easy to reuse on any ticker

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)

I’ve always been curious about the impact of different optimization goals in trading strategy design.

Most of the time, people tune strategies to maximize returns, but I wanted to know how things would look if I optimized for the Sharpe Ratio instead.

Would the strategy take fewer trades? Would the drawdowns be smaller? Would the final outcome be worse, or better?

To explore this, I took a simple moving average crossover strategy, commonly known as the Double Moving Average Crossover (DMAC) and optimized it in two ways:

  1. first to maximize return, and then to

  2. maximize Sharpe Ratio.

I used NVIDIA’s stock (NVDA) as the case study, ran both experiments over the same historical data, and compared the results.

The Headlines Traders Need Before the Bell

Tired of missing the trades that actually move?

In under five minutes, Elite Trade Club delivers the top stories, market-moving headlines, and stocks to watch — before the open.

Join 200K+ traders who start with a plan, not a scroll.

Setting Up the Environment

We start by installing and importing the necessary libraries. This includes:

  • yfinance for historical price data,

  • pandas and numpy for data handling,

  • matplotlib for plotting, and

  • optuna for optimization.

%pip install yfinance optuna matplotlib pandas numpy tabulate --quiet
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from tabulate import tabulate
import optuna
from optuna.samplers import TPESampler

plt.style.use('dark_background')

Getting the Data

We download NVIDIA’s historical closing price data from 2015 up to the end of 2024 using yfinance. For simplicity, I keep only the adjusted closing prices.

def get_data(ticker="NVDA", start="2015-01-01", end="2024-12-31"):
    df = yf.download(ticker, start=start, end=end)
    df.columns = df.columns.get_level_values(0)
    df = df[['Close']]
    return df

data = get_data()
data.head()

Once fetched, I plotted the closing price to get a visual sense of the dataset.

plt.figure(figsize=(14, 7))
plt.plot(data.index, data['Close'], color='blue', linewidth=1)
plt.title('Closing Price of Ford (Ticker: F)')
plt.xlabel('Date')
plt.ylabel('Close Price ($)')
plt.grid(True)
plt.tight_layout()
plt.savefig('closing_price_plot.png', dpi=300)
plt.show()

Closing Price of NVDA

Train-Test Split

To avoid overfitting, the dataset is split into training and testing sets using an 80/20 ratio.

The training set is used for optimization, and the testing set is used for final evaluation.

# 80/20 train-test split
split_date = data.index[int(len(data) * 0.8)]
train_data = data[:split_date]
test_data = data[split_date:]
print(f"Train: {train_data.index[0].date()} to {train_data.index[-1].date()}")
print(f"Test:  {test_data.index[0].date()} to {test_data.index[-1].date()}")
Train: 2015-01-02 to 2022-12-29
Test:  2022-12-29 to 2024-12-30

Strategy: Double Moving Average Crossover (DMAC)

This strategy buys when a short-term moving average crosses above a longer-term one, and exits when it crosses back below.

I added logic to calculate returns and generate trading signals, including buy/sell flags for later visualization.

def apply_dmac(df, short_window, long_window):
    if short_window >= long_window:
        return pd.DataFrame()

    data = df.copy()
    data['short_ma'] = data['Close'].rolling(window=short_window).mean()
    data['long_ma'] = data['Close'].rolling(window=long_window).mean()

    # Long/flat signals: 1 = long, 0 = flat (cash)
    data['signal'] = 0
    data.loc[data['short_ma'] > data['long_ma'], 'signal'] = 1

    data['position'] = data['signal'].shift(1).fillna(0)

    # Buy and sell flags (optional for plotting)
    data['buy'] = (data['position'] == 1) & (data['position'].shift(1) == 0)
    data['sell'] = (data['position'] == 0) & (data['position'].shift(1) == 1)

    # Calculate returns
    data['returns'] = data['Close'].pct_change()
    data['strategy_returns'] = data['position'] * data['returns']

    # Zero returns before first trade
    first_pos_idx = data['position'].first_valid_index()
    if first_pos_idx is not None:
        data.loc[:first_pos_idx, 'strategy_returns'] = 0

    data.dropna(inplace=True)
    return data

Metrics: Sharpe Ratio and Total Return

Two metrics are used for optimization. The Sharpe Ratio accounts for risk-adjusted returns, while total return measures absolute performance. The functions below calculate each metric for a given strategy run.

Sharpe Ratio

def calculate_sharpe(data, risk_free_rate=0.01):
    excess_returns = data['strategy_returns'] - risk_free_rate / 252
    sharpe = np.sqrt(252) * excess_returns.mean() / excess_returns.std()
    return sharpe

Total Return

def calculate_total_return(data):
    cumulative = (1 + data['strategy_returns']).cumprod()
    return cumulative.iloc[-1] - 1  # Total return (not percentage)

Optimization with Optuna

Using Optuna, I created two separate objective functions: one to maximize the Sharpe Ratio and one to maximize total return.

Each function searches for the best short and long moving average windows using the Tree-structured Parzen Estimator (TPE) sampler.

Sharpe Objective

def sharpe_objective(trial):
    short_window = trial.suggest_int("short_window", 5, 50)
    long_window = trial.suggest_int("long_window", short_window + 5, 200)

    df = apply_dmac(train_data, short_window, long_window)
    if df.empty or df['strategy_returns'].std() == 0:
        return -np.inf

    return calculate_sharpe(df)

Total Return Objective

def return_objective(trial):
    short_window = trial.suggest_int("short_window", 5, 50)
    long_window = trial.suggest_int("long_window", short_window + 5, 200)

    df = apply_dmac(train_data, short_window, long_window)
    if df.empty:
        return -np.inf

    return calculate_total_return(df)

I ran each optimization for 50 trials.

Sharpe Optimization

# Sharpe Optimization
sampler1 = TPESampler(seed=42)
sharpe_study = optuna.create_study(direction="maximize", sampler=sampler1)
sharpe_study.optimize(sharpe_objective, n_trials=50)

print("Best Sharpe Ratio:", round(sharpe_study.best_value, 4))
print("Best Parameters:", sharpe_study.best_params)
Best Sharpe Ratio: 1.5201
Best Parameters: {'short_window': 50, 'long_window': 171}

Return Optimization

sampler2 = TPESampler(seed=42)
return_study = optuna.create_study(direction="maximize", sampler=sampler2)
return_study.optimize(return_objective, n_trials=50)

print("Best Parameters:", return_study.best_params)
Best Parameters: {'short_window': 48, 'long_window': 172}

Visualizing the Signals

I visualized the DMAC buy and sell signals for both optimized strategies using the test data.

Sharpe Ratio Optimized Strategy

best_short_s = sharpe_study.best_params['short_window']
best_long_s = sharpe_study.best_params['long_window']
sharpe_df = apply_dmac(test_data, best_short_s, best_long_s)

plt.figure(figsize=(14, 7))
plt.plot(sharpe_df['Close'], label='Price', color='blue', linewidth=1)
plt.plot(sharpe_df['short_ma'], label=f'Short MA ({best_short_s})', color='green')
plt.plot(sharpe_df['long_ma'], label=f'Long MA ({best_long_s})', color='orange')

# Buy and Sell markers
plt.plot(sharpe_df[sharpe_df['buy']].index, sharpe_df[sharpe_df['buy']]['Close'],
         '^', markersize=14, color='lime', label='Buy Signal')
plt.plot(sharpe_df[sharpe_df['sell']].index, sharpe_df[sharpe_df['sell']]['Close'],
         'v', markersize=14, color='red', label='Sell Signal')

plt.title(f'Sharpe Optimized DMAC Signals (Short={best_short_s}, Long={best_long_s})')
plt.xlabel('Date')
plt.ylabel('Price')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.savefig('sharpe_optimized_signals_plot.png', dpi=300)
plt.show()

Sharpe Optimized DMAC Signals

Return Optimized Strategy

best_short_r = return_study.best_params['short_window']
best_long_r = return_study.best_params['long_window']
return_df = apply_dmac(test_data, best_short_r, best_long_r)

plt.figure(figsize=(14, 7))
plt.plot(return_df['Close'], label='Price', color='blue', linewidth=1)
plt.plot(return_df['short_ma'], label=f'Short MA ({best_short_r})', color='green')
plt.plot(return_df['long_ma'], label=f'Long MA ({best_long_r})', color='orange')

# Buy and Sell markers
plt.plot(return_df[return_df['buy']].index, return_df[return_df['buy']]['Close'],
         '^', markersize=14, color='lime', label='Buy Signal')
plt.plot(return_df[return_df['sell']].index, return_df[return_df['sell']]['Close'],
         'v', markersize=14, color='red', label='Sell Signal')

plt.title(f'Return Optimized DMAC Signals (Short={best_short_r}, Long={best_long_r})')
plt.xlabel('Date')
plt.ylabel('Price')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.savefig('return_optimized_signals_plot.png', dpi=300)
plt.show()

Return Optimized DMAC Signals

Comparing Performance

To compare performance, I calculated the cumulative returns of both optimized strategies and benchmarked them against a buy-and-hold baseline.

# Get best params for each
sharpe_best = sharpe_study.best_params
return_best = return_study.best_params

# Apply to test set
sharpe_df = apply_dmac(test_data, sharpe_best['short_window'], sharpe_best['long_window'])
return_df = apply_dmac(test_data, return_best['short_window'], return_best['long_window'])

# Cumulative returns
sharpe_cum = (1 + sharpe_df['strategy_returns']).cumprod()
return_cum = (1 + return_df['strategy_returns']).cumprod()
buy_hold_cum = (1 + test_data['Close'].pct_change()).cumprod()

Equity Curve Comparison

plt.figure(figsize=(14, 7))
plt.plot(test_data.index, buy_hold_cum, label="Buy & Hold", color='blue')
plt.plot(sharpe_df.index, sharpe_cum, label=f"Sharpe Optimized", color='orange')
plt.plot(return_df.index, return_cum, label=f"Return Optimized", color='green')
plt.title("Cumulative Returns Comparison")
plt.xlabel("Date")
plt.ylabel("Growth of $1")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.savefig("comparison_returns_plot.png", dpi=300)
plt.show()

Cumulative Returns Comparison

Final Results

Here’s a summary of the final values, returns, Sharpe ratios, and number of trades for each strategy:

sharpe_final = sharpe_cum.iloc[-1]
return_final = return_cum.iloc[-1]
bh_final = buy_hold_cum.iloc[-1]

sharpe_return_pct = (sharpe_final - 1) * 100
return_return_pct = (return_final - 1) * 100
bh_return_pct = (bh_final - 1) * 100

table = [
    ["Strategy", "Final Value ($)", "Return (%)", "Sharpe", "Trades"],
    ["Sharpe Optimized", f"{sharpe_final:.2f}", f"{sharpe_return_pct:.2f}%", 
     f"{calculate_sharpe(sharpe_df):.2f}", int(sharpe_df['buy'].sum() + sharpe_df['sell'].sum())],
    ["Return Optimized", f"{return_final:.2f}", f"{return_return_pct:.2f}%", 
     f"{calculate_sharpe(return_df):.2f}", int(return_df['buy'].sum() + return_df['sell'].sum())],
    ["Buy & Hold", f"{bh_final:.2f}", f"{bh_return_pct:.2f}%", "", ""]
]

print(tabulate(table, headers="firstrow", tablefmt="rounded_grid"))
╭──────────────────┬───────────────────┬──────────────┬──────────┬──────────╮
 Strategy            Final Value ($)  Return (%)    Sharpe    Trades   
├──────────────────┼───────────────────┼──────────────┼──────────┼──────────┤
 Sharpe Optimized               2.83  183.33%       1.85      1        
├──────────────────┼───────────────────┼──────────────┼──────────┼──────────┤
 Return Optimized               2.92  192.26%       1.90      1        
├──────────────────┼───────────────────┼──────────────┼──────────┼──────────┤
 Buy & Hold                     9.42  842.20%                        
╰──────────────────┴───────────────────┴──────────────┴──────────┴──────────╯

What the Results Actually Showed

Both optimized strategies produced similar outcomes on the test set, with return optimization delivering a slightly higher final value and Sharpe ratio.

Interestingly, both executed just one trade over the test period, which suggests that the optimal windows selected by each approach leaned toward longer-term signals.

What stood out most, however, was the performance of buy-and-hold. With a final value of $9.42 and a return of over 842%, it far outpaced either strategy, even though neither optimization technique came close to matching it.

This wasn’t entirely surprising, given NVIDIA’s massive growth over the testing period, but it’s a useful benchmark to keep in mind.

While optimizing for Sharpe did lead to a slightly lower return, it still achieved a solid risk-adjusted profile. The real difference between the two optimized strategies was marginal. In this case, the market itself did most of the heavy lifting, not the strategy.

The takeaway isn’t that optimization doesn’t work, but rather that when a stock has a strong long-term trend, simple buy-and-hold can outperform many timing strategies, regardless of how they’re tuned.

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