子供が生まれてからというもの、バックテストのパラメータを手動で試す時間が完全に消えた。以前は移動平均の期間を5日、10日、20日…と変えながら「この組み合わせが一番リターン出るかな」って延々と試していたんだけど、今は深夜にパソコンの前に座る余裕すらない。そんなとき知ったのがOptuna。「これ、育児パパのために作られたやつじゃないか」って思ったのが正直なところ。
なぜOptunaをバックテストに使うのか
Optunaは、Preferred Networks(日本の機械学習企業)が開発したハイパーパラメータ自動最適化フレームワーク。機械学習モデルの調整によく使われるけど、バックテストとの相性がめちゃくちゃいい。
何がいいかというと、「グリッドサーチ(総当たり)」じゃなく、TPE(Tree-structured Parzen Estimator)という賢い探索アルゴリズムで「次はどのパラメータを試すか」を自動で判断してくれること。1000通りのパラメータを全部試すんじゃなく、100回くらいの試行でかなりいい答えに近づける。時間のない社会人には最高。
ちなみに過最適化(オーバーフィッティング)は別問題として残るので、Optunaで出た「最強パラメータ」をそのまま信じるのは危険。あくまで「手動探索の代替」として使うのが正しい。これ、最初に僕がやらかしたやつ。。。
インストールと環境準備
pip install optuna yfinance pandas numpy
yfinanceで日本株データを取得する。銘柄コードの末尾に「.T」をつけるのがポイント(例:トヨタなら「7203.T」)。
バックテスト関数を作る
まず、Optunaに渡す「目的関数」を作る。この関数がパラメータを受け取って、バックテストの結果(シャープレシオなど)を返す。
import optuna
import yfinance as yf
import pandas as pd
import numpy as np
# 日本株データの取得(例:トヨタ自動車)
ticker = "7203.T"
df = yf.download(ticker, start="2022-01-01", end="2025-12-31")
df = df[["Close"]].dropna()
def backtest(short_window, long_window):
"""移動平均クロス戦略のバックテスト"""
if short_window >= long_window:
return -999 # 無効なパラメータ
df["short_ma"] = df["Close"].rolling(short_window).mean()
df["long_ma"] = df["Close"].rolling(long_window).mean()
df = df.dropna()
# シグナル生成(ゴールデンクロスで買い、デッドクロスで売り)
df["signal"] = 0
df.loc[df["short_ma"] > df["long_ma"], "signal"] = 1
df["position"] = df["signal"].shift(1)
# リターン計算
df["returns"] = df["Close"].pct_change()
df["strategy_returns"] = df["returns"] * df["position"]
# シャープレシオ(年換算)
mean_return = df["strategy_returns"].mean()
std_return = df["strategy_returns"].std()
if std_return == 0:
return -999
sharpe = (mean_return / std_return) * np.sqrt(252)
return sharpe
def objective(trial):
"""Optunaの目的関数"""
short_window = trial.suggest_int("short_window", 5, 50)
long_window = trial.suggest_int("long_window", 20, 200)
return backtest(short_window, long_window)
# 最適化の実行
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100, show_progress_bar=True)
print("最適パラメータ:")
print(f" 短期移動平均: {study.best_params['short_window']}日")
print(f" 長期移動平均: {study.best_params['long_window']}日")
print(f" シャープレシオ: {study.best_value:.3f}")
実行すると100回の試行が自動で走る。手動で試すより圧倒的に速い。
結果の可視化
Optunaには便利な可視化機能もある。
import optuna.visualization as vis
# パラメータの重要度
fig = vis.plot_param_importances(study)
fig.show()
# 試行履歴
fig2 = vis.plot_optimization_history(study)
fig2.show()
# パラメータのコンター図(どの組み合わせがいいか視覚化)
fig3 = vis.plot_contour(study, params=["short_window", "long_window"])
fig3.show()
コンター図を見ると「このあたりのパラメータが良さそう」という領域がヒートマップで表示される。「感覚でいじってた」が「データで見える」になる瞬間。
実際に試した結果(トヨタ、3年分)
トヨタ(7203.T)で試したところ、最適パラメータは短期12日・長期89日あたりで落ち着いた。シャープレシオは0.6前後。「これ、バイ&ホールドより良いのか?」って確認したら、バイ&ホールドのほうが若干高かった。。。まあ、製造メーカーはトレンドフォローより長期保有のほうが合ってるのかもしれない。
複数銘柄で試すなら、ループで銘柄ごとにstudyを作って結果をまとめるとよい。
tickers = ["7203.T", "6501.T", "6752.T"] # トヨタ、日立、パナソニック
results = {}
for ticker in tickers:
df = yf.download(ticker, start="2022-01-01", end="2025-12-31")[["Close"]].dropna()
def make_objective(data):
def objective(trial):
short = trial.suggest_int("short_window", 5, 50)
long_ = trial.suggest_int("long_window", 20, 200)
return backtest_with_data(data, short, long_)
return objective
study = optuna.create_study(direction="maximize")
optuna.logging.set_verbosity(optuna.logging.WARNING)
study.optimize(make_objective(df), n_trials=100)
results[ticker] = {
"best_sharpe": study.best_value,
"params": study.best_params
}
print(pd.DataFrame(results).T)
注意点:過最適化に気をつける
Optunaで出た「最適パラメータ」は、あくまでバックテスト期間に最もフィットしたもの。将来も同じ動きをするとは限らない。対策としては:
① ウォークフォワード最適化:訓練期間とテスト期間を分けて繰り返しテストする。② アウトオブサンプルテスト:最適化に使っていないデータで検証する。③ シンプルに保つ:パラメータが多いほど過最適化のリスクが高い。
僕は最初「Optunaで最強のパラメータ見つけた!」って喜んで本番運用したら、2週間でドローダウン15%になったことがある。。。ウォークフォワードは必須。
まとめ
Optunaを使えば、バックテストのパラメータ最適化を完全自動化できる。手動で試行錯誤していた時間を、育児や仕事に回せるのが最高。ただし、過最適化には要注意。
個人的には、Optunaで探索した後にウォークフォワードで検証する流れが今のマイベストプラクティスになってる。次はOptunaとバックテストフレームワーク「vectorbt」を組み合わせて、もっと高速に最適化できるか試してみたい。
