In partnership with

It's not you, it’s your tax tools

Tax teams are stretched thin and spreadsheets aren’t cutting it. This guide helps you figure out what to look for in tax software that saves time, cuts risk, and keeps you ahead of reporting demands.

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

In this article, I’ll walk you through each step of the process:

  1. Collecting and preparing the data

  2. Splitting the data into training and testing sets

  3. Defining the trading strategy

  4. Optimizing the strategy’s parameters

  5. Running backtests to evaluate performance

  6. Comparing the results to a simple buy-and-hold approach with Bitcoin

If you want to explore the full working notebook, you can check it out on my GitHub Repository.

Setup: Importing Libraries and Dependencies

First, I installed and imported all the libraries I needed, including:

  • yfinance for stock data

  • matplotlib for plotting

  • ta library for technical analysis

  • bayesian-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')

Wall Street Isn’t Warning You, But This Chart Might

Vanguard just projected public markets may return only 5% annually over the next decade. In a 2024 report, Goldman Sachs forecasted the S&P 500 may return just 3% annually for the same time frame—stats that put current valuations in the 7th percentile of history.

Translation? The gains we’ve seen over the past few years might not continue for quite a while.

Meanwhile, another asset class—almost entirely uncorrelated to the S&P 500 historically—has overall outpaced it for decades (1995-2024), according to Masterworks data.

Masterworks lets everyday investors invest in shares of multimillion-dollar artworks by legends like Banksy, Basquiat, and Picasso.

And they’re not just buying. They’re exiting—with net annualized returns like 17.6%, 17.8%, and 21.5% among their 23 sales.*

Wall Street won’t talk about this. But the wealthy already are. Shares in new offerings can sell quickly but…

*Past performance is not indicative of future returns. Important Reg A disclosures: masterworks.com/cd.

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

200+ AI Side Hustles to Start Right Now

AI isn't just changing business—it's creating entirely new income opportunities. The Hustle's guide features 200+ ways to make money with AI, from beginner-friendly gigs to advanced ventures. Each comes with realistic income projections and resource requirements. Join 1.5M professionals getting daily insights on emerging tech and business opportunities.

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.

If you want to explore the full working notebook, you can check it out on my GitHub Repository.

Keep Reading

No posts found