- GuruFinance Insights
- Posts
- Optimizing a Rate of Change Trading Strategy with Bayesian Methods
Optimizing a Rate of Change Trading Strategy with Bayesian Methods
A practical walkthrough of combining technical indicators and machine learning to refine trading decisions
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 datapandas
for data manipulationmatplotlib
for visualizationta
for technical indicatorsbayesian-optimization
for the optimization process, andtabulate
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.