In partnership with

You're overpaying for crypto.

Every exchange has different prices for the same crypto. Most people stick with one and pay whatever it costs.

CoW Swap checks them all automatically. Finds the best price. Executes your trade. Takes 30 seconds.

Stop leaving money on the table.

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 Holiday 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 December 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:

  • Define the Ichimoku strategy and backtest logic

  • Run Bayesian Optimization

  • Apply optimized parameters to train and test data

  • Beautiful interactive Plotly charts

  • Regime duration & performance tables

  • Ready-to-use CSV export

  • Bonus: works on Bitcoin, SPX, or any ticker with one line change

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)

Ichimoku Cloud with optimized parameters and executed trade signals.

In a previous article — I Optimized the Ichimoku Cloud Trading Strategy with Bayesian Optimization from 32% to 168% Returns — I showed how Bayesian Optimization could dramatically improve the performance of a classic Ichimoku Cloud trading strategy.

The results looked impressive. Too impressive.

So I decided to run the only test that really matters:

Can this optimized strategy beat simply buying the market and doing nothing?

This article documents that experiment end-to-end, using strict out-of-sample testing and a brutally simple benchmark. Every code cell below is shown exactly as used, with explanations of intent before each one.

Methodology at a glance

  • Asset: SPY (S&P 500)

  • Training data: 2000–2023

  • Testing data (out-of-sample): 2024 onward

  • Strategy: Optimized Ichimoku Cloud with Bayesian Optimization

  • Benchmark: Buy & Hold

  • Starting capital: $10,000

  • No re-optimization on test data

Code implementation

Install required packages

We begin by installing all dependencies used for data acquisition, optimization, backtesting, and visualization.

# Install necessary packages
%pip install yfinance matplotlib tabulate scikit-optimize -q

Stop Drowning In AI Information Overload

Your inbox is flooded with newsletters. Your feed is chaos. Somewhere in that noise are the insights that could transform your work—but who has time to find them?

The Deep View solves this. We read everything, analyze what matters, and deliver only the intelligence you need. No duplicate stories, no filler content, no wasted time. Just the essential AI developments that impact your industry, explained clearly and concisely.

Replace hours of scattered reading with five focused minutes. While others scramble to keep up, you'll stay ahead of developments that matter. 600,000+ professionals at top companies have already made this switch.

Import libraries

These libraries cover market data, numerical computation, plotting, and Bayesian optimization.

import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
from tabulate import tabulate
from skopt import gp_minimize
from skopt.space import Integer
import numpy as np

Download historical price data

We fetch SPY data from 2000 onward to capture multiple market regimes.

# Download historical stock data
symbol = "SPY"
data = yf.download(symbol, start="2000-01-01")

# Flatten multi-level columns
data.columns = data.columns.get_level_values(0)
data.head()

Visualize the raw price series

Before modeling anything, it helps to visually inspect the underlying asset.

# Plot raw closing price
plt.figure(figsize=(14,6))
plt.plot(data.index, data['Close'], color='black', linewidth=1.5, label=f"{symbol} Closing Price")
plt.title(f"{symbol} Closing Price (2020-2024)")
plt.xlabel("Date")
plt.ylabel("Price ($)")
plt.legend()
plt.savefig(f"figures/{symbol}_closing_price.png", dpi=300)
plt.show()

SPY closing price from 2000 onward, showing multiple bull and bear cycles.

Split data into training and testing sets

The model is optimized only on historical data and evaluated strictly out-of-sample.

# Split into training and testing sets
train = data.loc[:'2023-12-31'].copy()  # Training: 2000–2023
test = data.loc['2024-01-01':].copy()   # Testing: 2024-current only

Define the Ichimoku strategy and backtest logic

This function calculates Ichimoku components, generates trading signals, executes trades, and tracks portfolio equity.

def ichimoku_signals(data, tenkan_period, kijun_period, senkou_b_period):
    df = data.copy()
    high = df['High']
    low = df['Low']
    close = df['Close']

    # Ichimoku lines
    df['tenkan'] = (high.rolling(tenkan_period).max() + low.rolling(tenkan_period).min()) / 2
    df['kijun'] = (high.rolling(kijun_period).max() + low.rolling(kijun_period).min()) / 2
    df['senkou_a'] = ((df['tenkan'] + df['kijun']) / 2).shift(kijun_period)
    df['senkou_b'] = ((high.rolling(senkou_b_period).max() + low.rolling(senkou_b_period).min()) / 2).shift(kijun_period)

    # Signals
    df['above_cloud'] = df['Close'] > df[['senkou_a', 'senkou_b']].max(axis=1)
    df['tenkan_cross'] = (df['tenkan'] > df['kijun']) & (df['tenkan'].shift(1) <= df['kijun'].shift(1))
    df['signal'] = np.where(df['above_cloud'] & df['tenkan_cross'], 1, 0)

    # Backtest
    balance = 10000
    position = 0
    equity_curve = []

    for i in range(len(df)):
        if df['signal'].iloc[i] == 1 and position == 0:
            position = balance / df['Close'].iloc[i]
            balance = 0
        elif position > 0 and df['Close'].iloc[i] < df[['senkou_a','senkou_b']].min(axis=1).iloc[i]:
            balance = position * df['Close'].iloc[i]
            position = 0
        equity_curve.append(balance + position * df['Close'].iloc[i])

    df['equity'] = equity_curve
    df['daily_return'] = df['equity'].pct_change().fillna(0)
    final_equity = df['equity'].iloc[-1]
    total_return = (final_equity - 10000) / 10000 * 100

    return df, total_return, final_equity

Define the optimization objective

The optimizer maximizes total return on the training dataset only.

# Use training data only
def objective(params):
    tenkan, kijun, senkou_b = params
    _, total_return, _ = ichimoku_signals(train, tenkan, kijun, senkou_b)  # uses train data
    return -total_return  # gp_minimize minimizes

The Free Newsletter Fintech Execs Actually Read

Most coverage tells you what happened. Fintech Takes is the free newsletter that tells you why it matters. Each week, I break down the trends, deals, and regulatory shifts shaping the industry — minus the spin. Clear analysis, smart context, and a little humor so you actually enjoy reading it. Subscribe free.

Run Bayesian Optimization

We search across reasonable Ichimoku parameter ranges.

search_space = [
    Integer(5, 20, name='tenkan_period'),    # Tenkan period
    Integer(20, 50, name='kijun_period'),    # Kijun period
    Integer(40, 100, name='senkou_b_period') # Senkou B period
]

result = gp_minimize(objective, search_space, n_calls=25, random_state=42)

Apply optimized parameters to train and test data

Once optimized, the parameters are frozen and applied out-of-sample.

# Apply optimized params to both train and test sets
best_tenkan, best_kijun, best_senkou_b = result.x
best_train_return = -result.fun

# Apply to test (out-of-sample)
train_result, train_return, train_final_equity = ichimoku_signals(
    train.copy(), best_tenkan, best_kijun, best_senkou_b
)

test_result, test_return, test_final_equity = ichimoku_signals(
    test.copy(), best_tenkan, best_kijun, best_senkou_b
)

# Calculate max drawdown on test
max_equity = test_result['equity'].cummax()
drawdown = (test_result['equity'] - max_equity) / max_equity
max_drawdown = drawdown.min() * 100

stats = [
    ["Optimized Tenkan Period", best_tenkan],
    ["Optimized Kijun Period", best_kijun],
    ["Optimized Senkou B Period", best_senkou_b],
    ["Training Return", f"{train_return:.1f}%"],
    ["Testing Return", f"{test_return:.1f}%"],
    ["Max Drawdown (Test)", f"{max_drawdown:.1f}%"]
]

print(tabulate(stats, headers=["Metric", "Value"], tablefmt="rounded_outline"))

Optimized Ichimoku parameters and in-sample vs out-of-sample performance.

╭───────────────────────────┬─────────╮
 Metric                     Value   
├───────────────────────────┼─────────┤
 Optimized Tenkan Period    20      
 Optimized Kijun Period     27      
 Optimized Senkou B Period  100     
 Training Return            288.3%  
 Testing Return             17.5%   
 Max Drawdown (Test)        -10.1%  
╰───────────────────────────┴─────────╯

Implement buy & hold benchmark

This benchmark represents full exposure with zero decision-making.

# Buy & Hold strategy (out-of-sample period only)
buy_hold = test.copy()

initial_capital = 10000
shares = initial_capital / buy_hold['Close'].iloc[0]

buy_hold['equity'] = shares * buy_hold['Close']
buy_hold['daily_return'] = buy_hold['equity'].pct_change().fillna(0)

bh_final_equity = buy_hold['equity'].iloc[-1]
bh_total_return = (bh_final_equity - initial_capital) / initial_capital * 100

Compare drawdowns

Risk matters as much as return.

def max_drawdown(equity):
    peak = equity.cummax()
    drawdown = (equity - peak) / peak
    return drawdown.min() * 100

ichimoku_dd = max_drawdown(test_result['equity'])
buy_hold_dd = max_drawdown(buy_hold['equity'])

Side-by-side performance comparison

comparison = [
    ["Total Return (Test)", f"{test_return:.1f}%", f"{bh_total_return:.1f}%"],
    ["Final Equity", f"${test_result['equity'].iloc[-1]:,.0f}", f"${bh_final_equity:,.0f}"],
    ["Max Drawdown", f"{ichimoku_dd:.1f}%", f"{buy_hold_dd:.1f}%"],
]

print(tabulate(
    comparison,
    headers=["Metric", "Optimized Ichimoku", "Buy & Hold"],
    tablefmt="rounded_outline"
))

Out-of-sample performance comparison between optimized Ichimoku and buy & hold.

╭─────────────────────┬──────────────────────┬──────────────╮
 Metric               Optimized Ichimoku    Buy & Hold   
├─────────────────────┼──────────────────────┼──────────────┤
 Total Return (Test)  17.5%                 49.7%        
 Final Equity         $11,745               $14,973      
 Max Drawdown         -10.1%                -18.8%       
╰─────────────────────┴──────────────────────┴──────────────╯

Plot equity curves

This chart makes the trade-off visually obvious.

plt.figure(figsize=(14,7))
plt.plot(test_result.index, test_result['equity'], label="Optimized Ichimoku", linewidth=2)
plt.plot(buy_hold.index, buy_hold['equity'], label="Buy & Hold", linewidth=2, linestyle="--")

plt.title(f"{symbol}: Optimized Ichimoku vs Buy & Hold (Out-of-Sample)")
plt.xlabel("Date")
plt.ylabel("Portfolio Value ($)")
plt.legend()
plt.grid(alpha=0.3)
plt.savefig(f"figures/{symbol}_ichimoku_vs_buy_hold.png", dpi=300)
plt.show()

Equity curves showing smoother risk control vs higher absolute returns.

Visualize Ichimoku signals on price

Finally, we inspect how the strategy behaves on price itself.

data_optimized = test_result 

buy_points = []
sell_points = []
position = 0

for i in range(len(data_optimized)):
    if data_optimized['signal'].iloc[i] == 1 and position == 0:
        buy_points.append((data_optimized.index[i], data_optimized['Close'].iloc[i]))
        position = 1
    elif position == 1 and data_optimized['Close'].iloc[i] < data_optimized[['senkou_a','senkou_b']].min(axis=1).iloc[i]:
        sell_points.append((data_optimized.index[i], data_optimized['Close'].iloc[i]))
        position = 0

buy_df = pd.DataFrame(buy_points, columns=["Date","Price"]).set_index("Date")
sell_df = pd.DataFrame(sell_points, columns=["Date","Price"]).set_index("Date")

plt.figure(figsize=(14,8))
plt.plot(data_optimized.index, data_optimized['Close'], label="Close", color='black', linewidth=1)
plt.plot(data_optimized.index, data_optimized['tenkan'], label="Tenkan (Conversion)", color='blue', linewidth=1.2)
plt.plot(data_optimized.index, data_optimized['kijun'], label="Kijun (Base)", color='red', linewidth=1.2)

plt.fill_between(
    data_optimized.index, data_optimized['senkou_a'], data_optimized['senkou_b'],
    where=data_optimized['senkou_a'] >= data_optimized['senkou_b'],
    color='lightgreen', alpha=0.4
)
plt.fill_between(
    data_optimized.index, data_optimized['senkou_a'], data_optimized['senkou_b'],
    where=data_optimized['senkou_a'] < data_optimized['senkou_b'],
    color='lightcoral', alpha=0.4
)

plt.scatter(buy_df.index, buy_df['Price'], marker="^", color="lime", s=100, label="Buy Signal", zorder=5)
plt.scatter(sell_df.index, sell_df['Price'], marker="v", color="red", s=100, label="Sell Signal", zorder=5)

plt.title(f"{symbol} Ichimoku Cloud (Out-of-Sample, 2024)")
plt.legend(loc="upper left")
plt.savefig(f"figures/{symbol}_ichimoku_out_of_sample.png", dpi=300)
plt.show()

Ichimoku Cloud with optimized parameters and executed trade signals.

Final takeaway

The optimized Ichimoku strategy worked — just not better than doing nothing. On the out-of-sample period in 2024, the strategy returned 17.5%, with a maximum drawdown of -10.1%. In comparison, a simple buy-and-hold approach returned 49.7% over the same period, with a larger drawdown of -18.8%.

This shows that while the optimized strategy reduced risk and smoothed the equity curve, it underperformed in absolute returns during a strong market year. The Ichimoku approach avoided volatility and behaved exactly as a trend-following system should, but full market exposure beat the carefully optimized signals by more than 30 percentage points.

Sometimes, the most sophisticated strategy loses to patience and simplicity, and seeing these numbers side by side makes that point undeniable. This is why out-of-sample testing against real-world benchmarks is invaluable, and why even advanced technical methods must be measured against the simplest baseline: doing nothing.

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