In partnership with

A big 2026 starts now

Most people treat this stretch of the year as dead time. But builders like you know it’s actually prime time. And with beehiiv powering your content, world domination is truly in sight.

On beehiiv, you can launch your website in minutes with the AI Web Builder, publish a professional newsletter with ease, and even tap into huge earnings with the beehiiv Ad Network. It’s everything you need to create, grow, and monetize in one place.

In fact, we’re so hyped about what you’ll create, we’re giving you 30% off your first three months with code BIG30. So forget about taking a break. It’s time for a break-through.

Elite Quant Plan – 14-Day Free Trial (This Week Only)

No card needed. Cancel anytime. Zero risk.

You get immediate access to:

  • Full code from every article (including today’s HMM notebook)

  • Private GitHub repos & templates

  • All premium deep dives (3–5 per month)

  • 2 × 1-on-1 calls with me

  • One custom bot built/fixed for you

Try the entire Elite experience for 14 days — completely free.

→ Start your free trial now 👇

(Doors close in 7 days or when the post goes out of the spotlight — whichever comes first.)

See you on the inside.

👉 Upgrade Now

🔔 Limited-Time Deal: 20% Off Our Complete 2026 Playbook! 🔔

Level up before the year ends!

AlgoEdge Insights: 30+ Python-Powered Trading Strategies – The Complete 2026 Playbook

30+ battle-tested algorithmic trading strategies from the AlgoEdge Insights newsletter – fully coded in Python, backtested, and ready to deploy. Your full arsenal for dominating 2026 markets.

Special Promo: Use code DECEMBER2025 for 20% off

Valid only until January 20, 2025 — act fast!

👇 Buy Now & Save 👇

Instant access to every strategy we've shared, plus exclusive extras.

— AlgoEdge Insights Team

Premium Members – Your Full Notebook Is Ready

The complete Google Colab notebook from today’s article (with live data, full Hidden Markov Model, interactive charts, statistics, and one-click CSV export) is waiting for you.

Preview of what you’ll get:

Inside:

  • Automatic Bitcoin (BTC-USD) daily closing price download via yfinance (2020 → end of 2024)

  • Clean 70/30 chronological train-test split (train: 2020–mid 2023, test: mid 2023–2024)

  • Pure momentum strategy based on Rate of Change (ROC) indicator with buy/sell thresholds

  • Full simulation backtesting engine (long-only, tracks position, cash, portfolio value, signals)

  • Bayesian Optimization (bayes_opt) to smartly tune 3 key parameters: ROC window (3–30), buy threshold (0.1–10), sell threshold (-10 to -0.1)

  • Objective: maximize total return on the training period (20+5 iterations)

  • Ready-to-run in one cell — just execute and watch the optimization + results

  • Bonus: easy to adapt — change symbol='BTC-USD' to any ticker (ETH-USD, SPY, etc.) and re-run

Free readers – you already got the full breakdown and visuals in the article. Paid members – you get the actual tool.

Not upgraded yet? Fix that in 10 seconds here👇

Google Collab Notebook With Full Code Is Available In the End Of The Article Behind The Paywall 👇 (For Paid Subs Only)

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.

But what can you actually DO about the proclaimed ‘AI bubble’? Billionaires know an alternative…

Sure, if you held your stocks since the dotcom bubble, you would’ve been up—eventually. But three years after the dot-com bust the S&P 500 was still far down from its peak. So, how else can you invest when almost every market is tied to stocks?

Lo and behold, billionaires have an alternative way to diversify: allocate to a physical asset class that outpaced the S&P by 15% from 1995 to 2025, with almost no correlation to equities. It’s part of a massive global market, long leveraged by the ultra-wealthy (Bezos, Gates, Rockefellers etc).

Contemporary and post-war art.

Masterworks lets you invest in multimillion-dollar artworks featuring legends like Banksy, Basquiat, and Picasso—without needing millions. Over 70,000 members have together invested more than $1.2 billion across over 500 artworks. So far, 25 sales have delivered net annualized returns like 14.6%, 17.6%, and 17.8%.*

Want access?

Investing involves risk. Past performance not indicative of future returns. Reg A disclosures at masterworks.com/cd

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

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

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.

logo

Subscribe to our premium content to read the rest.

Become a paying subscriber to get access to this post and other subscriber-only content.

Upgrade

Keep Reading