Optimizing a Rate of Change Trading Strategy with Bayesian Methods

A practical walkthrough of combining technical indicators and machine learning to refine trading decisions

In partnership with

Get The Crypto Playbook for 2025

Keeping up with crypto while working a full-time job? Nearly impossible.

But Crypto is on fire and it’s not slowing down, with the industry having just hit a record-high $4 trillion dollar market cap.

And we’re sharing it at no cost when you subscribe to our free daily investment newsletter.

It covers the new Crypto bills that just passed and all the top trends, including the altcoin we think could define this cycle. That’s right, you can catch up on the industry in 5 minutes and still take advantage of this record bull run.

Skip the noise and stay one step ahead of the crypto and stock market.

Stocks & Income is for informational purposes only and is not intended to be used as investment advice. Do your own research.

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

Buy and Sell Signals of Tesla

Introduction

Systematic trading strategies live at the intersection of finance and data science. Rather than relying on gut feeling, these approaches define clear rules for entering and exiting trades.

But even with a robust idea, the real challenge lies in selecting the right parameters:

  • How long should the lookback window be?

  • What threshold should signal a buy or a sell?

This is where Bayesian optimization becomes invaluable. Instead of blindly testing every possible parameter combination, Bayesian methods allow us to intelligently search the parameter space and converge on configurations that improve strategy performance.

In this article, we’ll build and optimize a Rate of Change (ROC) trading strategy. We’ll use Tesla’s stock data (TSLA), calculate buy and sell signals from ROC, and apply Bayesian optimization to discover the most profitable configuration.

By the end, you’ll see how the optimized strategy compares against a simple buy-and-hold approach.

Run ads IRL with AdQuick

With AdQuick, you can now easily plan, deploy and measure campaigns just as easily as digital ads, making them a no-brainer to add to your team’s toolbox.

You can learn more at www.AdQuick.com

Background on the Strategy

The Rate of Change (ROC) is a momentum oscillator that measures the percentage change in price over a given number of periods. It helps identify when momentum is accelerating or decelerating:

  • High positive ROC suggests strong upward momentum.

  • Negative ROC can signal weakness or a potential reversal.

Our trading logic will be:

  • Buy when the ROC rises above a positive threshold.

  • Sell when the ROC falls below a negative threshold.

Instead of guessing the optimal lookback period or thresholds, we’ll let Bayesian optimization handle the search for us.

Installing Dependencies

We begin by installing the necessary Python packages. These include:

  • yfinance for fetching stock data

  • pandas for data manipulation

  • matplotlib for visualization

  • ta for technical indicators

  • bayesian-optimization for the optimization process, and

  • tabulate for clean summaries.

%pip install yfinance bayesian-optimization matplotlib pandas ta tabulate --quiet

Importing Libraries

Next, we import the libraries.

import yfinance as yf
import matplotlib.pyplot as plt
from ta.momentum import ROCIndicator
from bayes_opt import BayesianOptimization
from tabulate import tabulate

Loading Tesla Stock Data

We’ll fetch Tesla’s daily closing prices from January 2020 to December 2024. This dataset will form the foundation for our backtest.

# Load Tesla stock data
symbol = 'TSLA'
initial_cash = 1  # Initial cash for backtesting
data = yf.download(symbol, start='2020-01-01', end='2024-12-31')
data.columns = data.columns.get_level_values(0)
data = data[['Close']]
data.dropna(inplace=True)
data.head()

Visualizing the Closing Price

Before diving into strategies, it’s useful to visualize the stock’s chart.

# Visualize the closing price of the stock
plt.figure(figsize=(14,6))
plt.plot(data.index, data['Close'], label=f'{symbol} Closing Price', color='blue')
plt.title(f'{symbol} Closing Price (2020–2024)')
plt.xlabel('Date')
plt.ylabel('Price (USD)')
plt.grid(True)
plt.legend()
plt.savefig('closing_price.png', dpi=300, bbox_inches='tight')
plt.show()

TSLA Closing Price

Splitting the Data

To avoid overfitting, we split the data into training (70%) and testing (30%). Optimization will be run on the training set, and evaluation will take place on the test set.

# Train-test split (70% train, 30% test)
train_size = int(len(data) * 0.7)
train_data = data.iloc[:train_size].copy()
test_data = data.iloc[train_size:].copy()

print(f"Training from {train_data.index[0]} to {train_data.index[-1]}")
print(f"Testing from {test_data.index[0]} to {test_data.index[-1]}")
Training from 2020-01-02 00:00:00 to 2023-06-29 00:00:00
Testing from 2023-06-30 00:00:00 to 2024-12-30 00:00:00

Backtesting the Strategy

We define a backtest function to simulate trades. It calculates ROC values, executes buy/sell logic, tracks portfolio value, and records trade signals.

def backtest_strategy(df, roc_window, buy_threshold, sell_threshold):
    df = df.copy()
    roc_window = int(roc_window)
    buy_threshold = float(buy_threshold)
    sell_threshold = float(sell_threshold)

    roc = ROCIndicator(close=df['Close'], window=roc_window)
    df['ROC'] = roc.roc()

    position = 0
    cash = initial_cash
    portfolio = []
    trades = 0
    buy_signals = []
    sell_signals = []

    for i in range(roc_window, len(df)):
        if df['ROC'].iloc[i] > buy_threshold and position == 0:
            position = cash / df['Close'].iloc[i]
            cash = 0
            buy_signals.append((df.index[i], df['Close'].iloc[i]))
            trades += 1
        elif df['ROC'].iloc[i] < sell_threshold and position > 0:
            cash = position * df['Close'].iloc[i]
            position = 0
            sell_signals.append((df.index[i], df['Close'].iloc[i]))
            trades += 1
        
        portfolio_value = cash + (position * df['Close'].iloc[i])
        portfolio.append(portfolio_value)

    final_value = cash + position * df['Close'].iloc[-1]
    return_percentage = (final_value - initial_cash) / initial_cash * 100

    return return_percentage, trades, buy_signals, sell_signals, portfolio

Defining the Optimization Objective

The optimizer needs a function that maps strategy parameters to performance. Here, the objective is to maximize returns.

What Smart Investors Read Before the Bell Rings

Clickbait headlines won’t grow your portfolio. That’s why over 1M investors — including Wall Street insiders — start their day with The Daily Upside. Founded by investment bankers and journalists, it cuts through the noise with clear insights on business, markets, and the economy. Stop guessing and get smarter every morning.

def objective(roc_window, buy_threshold, sell_threshold):
    returns, _, _, _, _ = backtest_strategy(train_data, roc_window, buy_threshold, sell_threshold)
    return returns

Bayesian Optimization Setup

We now specify the search space:

  • ROC window between 3 and 30 days.

  • Buy threshold between 0.1% and 10%.

  • Sell threshold between -10% and -0.1%.

The optimizer will explore these ranges to identify the best-performing parameters.

pbounds = {
    'roc_window': (3, 30),
    'buy_threshold': (0.1, 10),
    'sell_threshold': (-10, -0.1)
}

optimizer = BayesianOptimization(
    f=objective,
    pbounds=pbounds,
    random_state=42,
    verbose=2
)

optimizer.maximize(init_points=5, n_iter=20)
best_params = optimizer.max['params']
best_params

Applying the Best Parameters

With the optimized values, we backtest again on the test set to see real-world applicability.

best_roc = int(best_params['roc_window'])
best_buy = float(best_params['buy_threshold'])
best_sell = float(best_params['sell_threshold'])

returns, trades, buy_signals, sell_signals, portfolio = backtest_strategy(
    test_data, best_roc, best_buy, best_sell
)

print(f"Final Return on Test Set: {returns:.2f}%")
print(f"Number of Trades: {trades}")
Final Return on Test Set: 46.51%
Number of Trades: 15

Comparing to Buy and Hold

To understand if the strategy adds value, we compare its portfolio curve against a simple buy-and-hold.

# Visualize portfolio value vs. buy-and-hold on the test set
df = test_data.copy()
df = df.iloc[best_roc:].copy()  # Skip rows lost due to ROC calculation

# Strategy Portfolio Value
df['Strategy'] = portfolio

# Buy-and-hold simulation
initial_price = df['Close'].iloc[0]
df['BuyHold'] = (df['Close'] / initial_price) * initial_cash

# Plot both
plt.figure(figsize=(14,6))
plt.plot(df.index, df['Strategy'], label='ROC Strategy', color='orange', linewidth=2)
plt.plot(df.index, df['BuyHold'], label='Buy & Hold', color='green', linewidth=2)

plt.title('ROC Strategy vs. Buy & Hold (Test Set)')
plt.xlabel('Date')
plt.ylabel('Portfolio Value ($)')
plt.legend()
plt.grid(True)
plt.savefig('portfolio_comparison.png', dpi=300, bbox_inches='tight')
plt.show()

Visualizing Buy and Sell Signals

Charts make it easier to evaluate whether entry and exit points align with meaningful price moves.

# Plot buy/sell signals on test set
plt.figure(figsize=(14,6))

# Plot the closing price first
plt.plot(test_data.index, test_data['Close'], label='Price', color='blue', zorder=1)

# Plot buy signals
for buy in buy_signals:
    plt.scatter(buy[0], buy[1], marker='^', color='lime', s=100, label='Buy Signal', zorder=2)

# Plot sell signals
for sell in sell_signals:
    plt.scatter(sell[0], sell[1], marker='v', color='red', s=100, label='Sell Signal', zorder=2)

# Only show each label once
handles, labels = plt.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
plt.legend(by_label.values(), by_label.keys())

plt.title('Buy/Sell Signals on Test Set')
plt.xlabel('Date')
plt.ylabel('Price')
plt.grid(True)
plt.savefig('buy_sell_signals.png', dpi=300, bbox_inches='tight')
plt.show()

Evaluating Strategy Performance

Finally, we summarize results such as returns, trades, and win rate in a table. This allows a structured comparison against buy-and-hold.

# Strategy evaluation
total_trades = len(buy_signals) + len(sell_signals)

if len(buy_signals) == len(sell_signals):
    successful_trades = sum(
        sell[1] > buy[1] for buy, sell in zip(buy_signals, sell_signals)
    )
    win_rate = f"{(successful_trades / len(sell_signals) * 100):.2f}%"
else:
    win_rate = 'Inconsistent buy/sell pairs'

test_df = test_data.copy().iloc[best_roc:].copy()  # align with portfolio calculation start
initial_price = test_df['Close'].iloc[0]
final_price = test_df['Close'].iloc[-1]

final_strategy_value = portfolio[-1]
final_bh_value = (final_price / initial_price) * initial_cash
bh_return_pct = (final_bh_value - initial_cash) / initial_cash * 100
strategy_return_pct = (final_strategy_value - initial_cash) / initial_cash * 100

summary = [
    ["Initial Cash", f"${initial_cash}", f"${initial_cash}"],
    ["Final Portfolio Value", f"${final_strategy_value:.2f}", f"${final_bh_value:.2f}"],
    ["Total Return (%)", f"{strategy_return_pct:.2f}%", f"{bh_return_pct:.2f}%"],
    ["Total Trades Executed", total_trades, "N/A"],
    ["Buy Trades", len(buy_signals), "N/A"],
    ["Sell Trades", len(sell_signals), "N/A"],
    ["Win Rate", win_rate, "N/A"]
]

print(tabulate(summary, headers=["Metric", "ROC Strategy", "Buy & Hold"], tablefmt="rounded_grid"))
╭───────────────────────┬─────────────────────────────┬──────────────╮
 Metric                 ROC Strategy                 Buy & Hold   
├───────────────────────┼─────────────────────────────┼──────────────┤
 Initial Cash           $1                           $1           
├───────────────────────┼─────────────────────────────┼──────────────┤
 Final Portfolio Value  $1.47                        $1.57        
├───────────────────────┼─────────────────────────────┼──────────────┤
 Total Return (%)       46.51%                       57.35%       
├───────────────────────┼─────────────────────────────┼──────────────┤
 Total Trades Executed  15                           N/A          
├───────────────────────┼─────────────────────────────┼──────────────┤
 Buy Trades             8                            N/A          
├───────────────────────┼─────────────────────────────┼──────────────┤
 Sell Trades            7                            N/A          
├───────────────────────┼─────────────────────────────┼──────────────┤
 Win Rate               Inconsistent buy/sell pairs  N/A          
╰───────────────────────┴─────────────────────────────┴──────────────╯

A Step Toward Smarter Strategy Design

This experiment demonstrates the value of combining technical indicators with machine learning techniques.

The ROC strategy, enhanced through Bayesian optimization, highlights how parameter tuning can significantly change outcomes.

While the approach doesn’t guarantee profitability across all markets or timeframes, it illustrates a disciplined framework for research.

From here, this same methodology can be applied to other indicators such as RSI, MACD, or Bollinger Bands, broadening the toolkit while relying on Bayesian optimization to do the heavy lifting.