• GuruFinance Insights
  • Posts
  • Sharpe Ratio vs Return: I Optimized a Trading Strategy for Both So You Don’t Have To

Sharpe Ratio vs Return: I Optimized a Trading Strategy for Both So You Don’t Have To

What NVDA Taught Me About Strategy Optimization and Holding On

In partnership with

The Business Brief Executives Actually Trust

In a world of sensational headlines and shallow analysis, The Daily Upside stands apart. Founded by former bankers and seasoned journalists, it delivers crisp, actionable insights executives actually use to make smarter decisions.

From market-moving developments to deep dives on business trends, The Daily Upside gives leaders clarity on what matters — without the noise.

That’s why over 1 million readers, including C-suite executives and senior decision-makers, start their day with it.

No fluff. No spin. Just business clarity.

🚀 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.

Photo by Adam Śmigielski on Unsplash

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.

📈 Algorithmic Trading Course

I’m launching a premium online course on algorithmic trading starting July 21. The price is $3000, and spots will be limited.

🔥 What to Expect:

  • Build trading bots with Python

  • Backtest strategies with real data

  • Learn trend-following, mean-reversion, and more

  • Live execution, portfolio risk management

  • Bonus: machine learning & crypto trading modules

🎁 Free Bonus:

Everyone who enrolls will get a free e-book:
“100 Trading Strategies in Python” — full of ready-to-use code.

👉 Interested? Join the program now:
I want to get in

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.