- GuruFinance Insights
- Posts
- How I Optimized a Bitcoin Rate of Change Trading Strategy Using Bayesian Optimization
How I Optimized a Bitcoin Rate of Change Trading Strategy Using Bayesian Optimization
Can AI Beat Buy & Hold on Bitcoin? I Tried It, and Here’s What I Found
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.

Rate of Change vs Buy and Hold on Bitcoin Price Data
I wanted to find out if a simple technical trading strategy called the Rate of Change (ROC), which looks at how much a price has changed over time, could help improve Bitcoin trading when used with some smart adjustments.
Instead of trying complicated methods, I focused on tuning this one indicator to see what it could do.
To find the best settings quickly, I used a method called Bayesian Optimization. It’s a way to test different options smartly without wasting time trying everything randomly.
📈 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
In this article, I’ll walk you through each step of the process:
Collecting and preparing the data
Splitting the data into training and testing sets
Defining the trading strategy
Optimizing the strategy’s parameters
Running backtests to evaluate performance
Comparing the results to a simple buy-and-hold approach with Bitcoin
Setup: Importing Libraries and Dependencies
First, I installed and imported all the libraries I needed, including:
yfinance
for stock datamatplotlib
for plottingta
library for technical analysisbayesian-optimization
package for hyperparameter tuning.
%pip install yfinance bayesian-optimization matplotlib pandas ta tabulate --quiet
import yfinance as yf
import matplotlib.pyplot as plt
from ta.momentum import ROCIndicator
from bayes_opt import BayesianOptimization
from tabulate import tabulate
plt.style.use('dark_background')
Loading the Data
For this project, I chose Bitcoin (BTC-USD), given its volatility and popularity.
I downloaded daily closing prices from 2020
to the end of 2024
.
# Load Bitcoin stock data
symbol = 'BTC-USD'
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 Price Data
Before diving into strategy development, it’s important to understand the data visually.
Here’s the closing price chart over the selected period.
# 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()

Bitcoin’s Closing Price from 2020 to 2024
Train-Test Split
To ensure our strategy is evaluated fairly, I split the data into training (70%) and testing (30%) sets based on date.
# 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]}")
Output:
Training from 2020-01-01 to 2023-07-01
Testing from 2023-07-02 to 2024-12-30
Defining the Backtesting Function
This function takes the data and trading parameters, calculates the ROC indicator, and simulates buying and selling based on thresholds.
It tracks portfolio value over time 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
Objective Function for Bayesian Optimization
To tune the ROC window size and buy/sell thresholds, I defined an objective function that backtests the strategy on training data and returns the strategy’s return.
Bayesian Optimization will try to maximize this return.
def objective(roc_window, buy_threshold, sell_threshold):
returns, _, _, _, _ = backtest_strategy(train_data, roc_window, buy_threshold, sell_threshold)
return returns
Running Bayesian Optimization
I set bounds for each parameter and ran the optimizer for 20 iterations after 5 random initial points.
The output included the best parameters found.
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
Output:
{
'roc_window': np.float64(27.943437909406587),
'buy_threshold': np.float64(0.1),
'sell_threshold': np.float64(-2.722477471437819)
}
Backtesting on Test Data Using Optimized Parameters
With the best parameters, I backtested the strategy on the test set to get final returns and trade counts.
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}")
Output:
Final Return on Test Set: 167.65%
Number of Trades: 24
Comparing Strategy vs Buy-and-Hold
I then compared the portfolio value progression against a simple buy-and-hold strategy that keeps the asset for the entire test period.
# 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')
plt.plot(df.index, df['BuyHold'], label='Buy & Hold', color='green')
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()

ROC Strategy vs. Buy and Hold Portfolio Value Over Time
Visualizing Buy and Sell Signals
To get a clear picture of when the strategy was buying or selling, I plotted signals on top of the closing price.
# 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()

Buy/Sell Signals on Test Set
Final Performance Summary
Lastly, I summarized the performance metrics of the strategy alongside the buy-and-hold benchmark for easy comparison.
# 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"))
Output:
╭───────────────────────┬────────────────┬──────────────╮
│ Metric │ ROC Strategy │ Buy & Hold │
├───────────────────────┼────────────────┼──────────────┤
│ Initial Cash │ $1 │ $1 │
├───────────────────────┼────────────────┼──────────────┤
│ Final Portfolio Value │ $2.68 │ $3.16 │
├───────────────────────┼────────────────┼──────────────┤
│ Total Return (%) │ 167.65% │ 215.58% │
├───────────────────────┼────────────────┼──────────────┤
│ Total Trades Executed │ 24 │ N/A │
├───────────────────────┼────────────────┼──────────────┤
│ Buy Trades │ 12 │ N/A │
├───────────────────────┼────────────────┼──────────────┤
│ Sell Trades │ 12 │ N/A │
├───────────────────────┼────────────────┼──────────────┤
│ Win Rate │ 41.67% │ N/A │
╰───────────────────────┴────────────────┴──────────────╯
While the ROC strategy delivered a solid return of about 168%, it did not quite surpass the 216% return of simply holding Bitcoin over the same period.
The strategy executed 24 trades with a win rate of around 42%, showing that while it can capture gains, it also involves frequent trading and some losses.
This highlights the trade-off between active trading and a passive buy-and-hold approach, active management can offer opportunities but also carries risk and requires careful tuning.
For anyone exploring algorithmic trading, this exercise demonstrates how even a straightforward indicator combined with smart parameter optimization can produce meaningful results. It’s a practical starting point for developing and refining your own trading strategies.