仮想通貨の自動売買におけるバックテストとは、これから稼働させようとしている売買ルールを過去の価格データに当てはめて、もしその期間に稼働させていたらどれだけの損益・ドローダウン・取引回数になっていたかを計算し、ルールの生存可能性を事前に検証する作業である。本記事は2026年7月時点の情報をもとに、ヒストリカルデータの取得からPythonでの戦略実装、実行、指標評価、フォワードテスト(ペーパートレード)を経て本番投入するまでの流れを、そのままコピーして動かせるコードとともに解説する。
自動売買botは「動くコードを書けること」と「利益が残るルールであること」がまったく別の問題である。前者はプログラミングの話であり、後者は検証設計の話だ。多くの初心者が最初の資金を失うのは、コードが動いた喜びのまま検証を飛ばして本番に流してしまうからで、バックテストはこの落差を埋めるための唯一の安価な手段になる。ただし後述するとおり、バックテストは「やれば安心」ではなく「やり方を間違えると、何もしないより危険な自信を生む」道具でもある。
仮想通貨の自動売買におけるバックテストとは
定義と目的
バックテストの目的は「儲かるルールを見つけること」ではなく、「明らかに駄目なルールを本番前に落とすこと」である。この順序を取り違えると、過去データに都合よく当てはまるパラメータを探す作業(過剰最適化)に時間を溶かすことになる。実務上のバックテストは、次の3つを判定するためのフィルタとして機能する。
- そのルールは手数料とスリッページを差し引いても損益がプラス側に残る余地があるか
- 想定していない相場(急落・レンジ・出来高薄)で口座が壊れるほどのドローダウンを出さないか
- 取引回数と保有時間が、自分の運用スタイルとインフラ(VPS・API制限)に見合っているか
バックテスト・フォワードテスト・本番の3段階
自動売買の検証は3段階に分かれる。この区別を曖昧にしたまま「テストした」と言う人が多いので、用語を固定しておきたい。
| 段階 | 使うデータ | 資金 | 主に検証できること | 主に検証できないこと |
|---|---|---|---|---|
| バックテスト | 過去のOHLCV | なし(仮想) | ルールの期待値・ドローダウン・取引頻度 | 実際の約定価格、API障害、板の厚み |
| フォワードテスト(ペーパートレード) | リアルタイム価格 | なし(仮想) | 実データ配信の遅延、シグナル発生頻度、実装バグ | 実際の約定滑り、手数料の実額 |
| 本番(少額) | リアルタイム価格 | 少額の実資金 | 約定価格の乖離、手数料、税務上の記録 | — |
バックテストで合格しても、フォワードテストで想定の半分もシグナルが出ない、というのは日常的に起こる。原因はたいてい、バックテスト側で未来の情報を先読みしていたか、データの粒度が違ったかのどちらかだ。
裁量トレードのバックテストとの違い
チャートを目視で巻き戻して検証する裁量のバックテストと違い、自動売買のバックテストはルールが完全に数式・条件式で表現されている必要がある。「だいたいこのあたりで入る」は実装できない。逆に言えば、バックテストを書こうとした時点で自分のルールが曖昧だったことに気付く、という副次効果がある。ルールの言語化が済んでいない段階でbotを書き始めると、コードの中に無自覚な例外処理が増えていき、結果として何を検証したのか分からなくなる。
バックテストなしで自動売買botを動かすと何が起きるか
検証を省いたbotが壊れるパターンは、おおむね決まっている。自動売買botが失敗する典型パターンで扱っている内容と重なるが、バックテスト観点で整理すると次のようになる。
第一に、そもそも期待値がマイナスのルールを高速で回してしまう。手数料が片道0.05%でも、1日20往復すれば月間で元本の相当割合が手数料として消える。「勝率が高いから大丈夫」というルールほど、1回あたりの利益が小さく手数料負けしやすい。手数料とインフラ費用の積み上がりについてはAI自動売買にかかるコストの内訳で数値例を出している。
第二に、ドローダウンの上限を知らないまま資金を張る。バックテストをしていれば「過去2年で最大32%の含み損を抱えた局面があった」と分かるが、知らずに始めると初回のドローダウンで手動停止してしまい、その直後に回復する、という最悪の順序を踏みやすい。
第三に、実装バグが利益と誤認される。時系列のインデックスをひとつずらしただけで、未来の終値を見ながら売買する「未来予知bot」が簡単に完成する。バックテスト結果が異様に良いとき、9割方は天才的な戦略ではなくバグである。
バックテストに必要な3つの材料
材料1: ヒストリカルデータ(OHLCV)
最低限必要なのは、始値・高値・安値・終値・出来高の5列と、タイムスタンプである。仮想通貨の場合、取引所APIから直接取得できるのが利点で、株式のように有料データベンダーを契約しなくても、多くの取引所が無償の公開APIでローソク足を配信している。
粒度の選び方は戦略の保有時間で決まる。日足で数日保有するスイング系なら日足〜4時間足で足りるが、数分で決済するスキャルピング系を1時間足で検証しても意味がない。一方で1分足を2年分取ると数十万行になり、取得にもAPIレート制限の都合で時間がかかる。
| 足種 | 想定戦略 | 2年分のおおよその行数 | 留意点 |
|---|---|---|---|
| 1分足 | スキャル・高頻度 | 約105万行 | 取得に時間がかかる。約定モデルの粗さが致命的になりやすい |
| 15分足 | デイトレ | 約7万行 | バランスが良く初学者向き |
| 1時間足 | 短期スイング | 約1.7万行 | 取得が速く試行錯誤しやすい |
| 日足 | 中長期スイング | 約730行 | サンプル数が少なく統計的な信頼度が落ちる |
材料2: 戦略ルール(エントリー・エグジット・サイズ)
ルールは最低でも「いつ買うか」「いつ手仕舞うか」「いくら張るか」の3点セットで書く。損切り条件が無いルールは、バックテスト上は含み損を耐え切って勝ちに見えても、本番ではレバレッジや証拠金維持率の都合で強制的に切られるため、結果が一致しない。
材料3: 約定モデル(手数料・スリッページ・約定価格)
ここが最も軽視される。バックテストのライブラリは、デフォルトでは「シグナルが出た足の終値で、手数料ゼロ、無限の流動性で約定する」と仮定しているものがある。この設定のまま出た結果は、現実の損益とは別物と考えたほうがよい。最低でも往復の手数料率とスリッページ相当のコストを入れ、シグナル発生足の「次の足の始値」で約定させるのが実務的な最低ラインである。
実践手順: 5ステップでバックテストを完了させる
ここからは、Python環境を前提に具体的な手順を示す。Pythonの環境構築そのものはPythonで仮想通貨botを作る手順で扱っているため、本記事はバックテストの部分に絞る。
必要なライブラリは次のとおり。2026年7月時点の一般的な構成である。
pip install ccxt pandas numpy backtesting
ステップ1: ヒストリカルデータ(OHLCV)を取得する
CCXTは100を超える取引所の共通インターフェースを提供するPythonライブラリで、fetch_ohlcv メソッドでローソク足を取得できる。多くの取引所は1回のリクエストで返す本数に上限(500〜1000本程度が一般的)を設けているため、since をずらしながらページングして繋ぐ必要がある。
import time
import ccxt
import pandas as pd
def fetch_ohlcv_all(exchange_id: str, symbol: str, timeframe: str = "1h",
since_ms: int | None = None, limit: int = 500) -> pd.DataFrame:
"""取引所からOHLCVをページングで全件取得してDataFrameで返す。"""
ex = getattr(ccxt, exchange_id)({"enableRateLimit": True})
ex.load_markets()
if since_ms is None:
# 直近2年分をおおよそ確保する
since_ms = ex.milliseconds() - 2 * 365 * 24 * 60 * 60 * 1000
rows: list[list] = []
cursor = since_ms
while True:
chunk = ex.fetch_ohlcv(symbol, timeframe=timeframe, since=cursor, limit=limit)
if not chunk:
break
rows += chunk
next_cursor = chunk[-1][0] + 1
if next_cursor <= cursor:
break
cursor = next_cursor
if chunk[-1][0] >= ex.milliseconds():
break
time.sleep(ex.rateLimit / 1000) # レート制限を尊重する
df = pd.DataFrame(rows, columns=["ts", "Open", "High", "Low", "Close", "Volume"])
df = df.drop_duplicates(subset="ts").sort_values("ts")
df["Date"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
return df.set_index("Date")[["Open", "High", "Low", "Close", "Volume"]]
if __name__ == "__main__":
data = fetch_ohlcv_all("kraken", "BTC/JPY", "1h")
data.to_csv("btcjpy_1h.csv")
print(len(data), data.index.min(), data.index.max())
取得したCSVは必ず保存して使い回す。検証のたびにAPIを叩くとレート制限に引っかかるうえ、取引所側のデータ補正で結果が微妙に変わり再現性が失われる。
なお、取引所によってはCCXT経由の対応範囲が現物のみだったり、ローソク足の配信本数に制限があったりする。国内取引所でAPI取引の口座を用意するなら、公開APIとプライベートAPIの両方をドキュメント付きで提供している取引所を選ぶのが実務的だ。GMOコインは公開API・プライベートAPI・WebSocketを提供しており、現物とレバレッジ取引に対応している(2026年7月時点、公式APIドキュメント記載)。
具体的な認証やエンドポイントの叩き方はGMOコインAPIで自動売買を組む手順にまとめている。
ステップ2: データを検品する
取得直後のデータをそのまま使わない。次の4点は必ず確認する。
import pandas as pd
df = pd.read_csv("btcjpy_1h.csv", index_col="Date", parse_dates=True)
# 1. 欠損
print("NaN:", df.isna().sum().sum())
# 2. 重複タイムスタンプ
print("dup:", df.index.duplicated().sum())
# 3. 時間軸の抜け(1時間足なら差分は1hのはず)
gaps = df.index.to_series().diff().value_counts()
print(gaps.head())
# 4. 異常値(高値 < 安値、終値がレンジ外など)
bad = df[(df["High"] < df["Low"]) | (df["Close"] > df["High"]) | (df["Close"] < df["Low"])]
print("broken rows:", len(bad))
時間軸の抜けは、取引所のメンテナンスや上場直後の閑散時間で発生する。抜けを前方補完(ffill)で埋めると、実際には値が動いていない期間に指標が反応してシグナルが出るため、補完せずに「その期間は取引しない」と扱うほうが安全である。
ステップ3: 戦略を実装して実行する
最小構成として、backtesting.py を使った移動平均クロス戦略の実行例を示す。backtesting.py はPandas・NumPy・Bokehの上に構築されたイベント駆動型のフレームワークで、APIが1ページに収まる程度に小さく、最初の1本を書くのに向いている。
import pandas as pd
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
def sma(series: pd.Series, n: int) -> pd.Series:
return pd.Series(series).rolling(n).mean()
class SmaCross(Strategy):
n_fast = 20
n_slow = 60
stop_pct = 0.05 # 5%で損切り
def init(self):
close = self.data.Close
self.ma_fast = self.I(sma, close, self.n_fast)
self.ma_slow = self.I(sma, close, self.n_slow)
def next(self):
price = self.data.Close[-1]
if crossover(self.ma_fast, self.ma_slow):
if self.position.is_short:
self.position.close()
if not self.position:
self.buy(sl=price * (1 - self.stop_pct))
elif crossover(self.ma_slow, self.ma_fast):
if self.position.is_long:
self.position.close()
df = pd.read_csv("btcjpy_1h.csv", index_col="Date", parse_dates=True)
bt = Backtest(
df,
SmaCross,
cash=1_000_000,
commission=0.0012, # 往復想定のコスト(手数料+スリッページ相当)
trade_on_close=False, # シグナル足の終値ではなく次足で約定させる
exclusive_orders=True,
)
stats = bt.run()
print(stats)
bt.plot(open_browser=False, filename="sma_cross.html")
commission と trade_on_close の2つが、現実との距離を決める最重要パラメータである。trade_on_close=True(既定値が真になる構成もある)にすると、シグナルが確定した足の終値そのもので約定したことになり、実際には取れない価格で取れたことになってしまう。必ず False にして次足で約定させる。
ライブラリを使わず、pandasだけで同じことを確認したい場合の最小実装も挙げておく。仕組みを理解するにはこちらのほうが早い。
import numpy as np
import pandas as pd
df = pd.read_csv("btcjpy_1h.csv", index_col="Date", parse_dates=True)
fee = 0.0012 # 往復コスト
df["ma_fast"] = df["Close"].rolling(20).mean()
df["ma_slow"] = df["Close"].rolling(60).mean()
# シグナル: 1=ロング, 0=ノーポジ
raw = (df["ma_fast"] > df["ma_slow"]).astype(int)
# ★重要★ 1本シフトして「シグナル確定の次の足」から建てる(ルックアヘッド防止)
df["pos"] = raw.shift(1).fillna(0)
df["ret"] = df["Close"].pct_change().fillna(0)
df["turnover"] = df["pos"].diff().abs().fillna(0)
df["strategy_ret"] = df["pos"] * df["ret"] - df["turnover"] * fee
df["equity"] = (1 + df["strategy_ret"]).cumprod()
df["peak"] = df["equity"].cummax()
df["dd"] = df["equity"] / df["peak"] - 1
n_year = len(df) / (24 * 365) # 1時間足前提
cagr = df["equity"].iloc[-1] ** (1 / n_year) - 1
sharpe = df["strategy_ret"].mean() / df["strategy_ret"].std() * np.sqrt(24 * 365)
print(f"final equity : {df['equity'].iloc[-1]:.3f}")
print(f"CAGR : {cagr:.2%}")
print(f"max drawdown : {df['dd'].min():.2%}")
print(f"Sharpe : {sharpe:.2f}")
print(f"trades : {int(df['turnover'].sum())}")
raw.shift(1) の1行が、バックテストの信頼性を左右する。これを外すと「その足の終値でクロスを確認して、その足の始値から持っていた」ことになり、成績が劇的に良くなる。良くなったら疑う、が鉄則である。
ステップ4: 指標を評価する
出力された数字を、次の優先順で読む。総リターンから見始めると判断を誤りやすい。
- 取引回数 — 30回未満なら統計的な結論は出せない。パラメータを変えて回数が確保できる設計にするか、期間を延ばす
- 最大ドローダウン — 自分が実際に耐えられる水準か。過去の最大値は将来の最大値ではなく、下限の目安にすぎない
- コスト控除後の損益 — 手数料をゼロにしたときと比べて何割減るかを見る。半分以上減るなら高頻度すぎる
- シャープレシオ/ソルティノレシオ — リスク1単位あたりのリターン。同じ利益なら値動きが滑らかなほうが本番で続けやすい
- 総リターン・CAGR — 最後に見る
ステップ5: フォワードテストに進む
バックテストで合格したルールは、同じコードのまま「注文だけ出さない」モードでリアルタイム稼働させる。取引所のテストネット、あるいはFreqtradeの dry_run のようなペーパートレード機能を使う。最低でも2〜4週間、できれば相場付きが変わる局面をまたいで観測する。詳しい監視の組み方は稼働中botの監視とアラート設計を参照。
評価指標の意味を正しく読む
| 指標 | 意味 | 実務での目安・注意点 |
|---|---|---|
| 総リターン | 期間全体の損益率 | 相場が上昇していただけの可能性がある。必ずバイ・アンド・ホールドと比較する |
| CAGR | 年率換算の複利成長率 | 期間が短いと極端な値になる。1年未満のCAGRは参考値 |
| 最大ドローダウン | 資産曲線の最高値からの最大下落率 | 実運用で最も効く数字。ここが自分の許容度を超えるなら他の指標が良くても不採用 |
| シャープレシオ | 超過リターン÷リターンの標準偏差 | 仮想通貨は分布の裾が厚く、シャープが実態より良く出やすい |
| ソルティノレシオ | 下方リスクのみで割った指標 | 下落局面の痛みを見るならシャープよりこちら |
| 勝率 | 勝ちトレードの割合 | 単独では無意味。損益比とセットでのみ意味を持つ |
| ペイオフレシオ | 平均利益÷平均損失 | 勝率が低くてもここが高ければ成立する |
| プロフィットファクター | 総利益÷総損失 | 1.0で損益ゼロ。コスト控除後で1.0を割るルールは不採用 |
| 取引回数 | 期間中の売買回数 | サンプル数。少なすぎる結果は偶然と区別できない |
| 平均保有時間 | 1トレードあたりの保有期間 | インフラ要件(VPSの安定性・API制限)を決める |
勝率だけを見て判断する例が非常に多いが、勝率90%・ペイオフレシオ0.05のルールは、10回に1回の負けで全部を吐き出す。逆に勝率35%でもペイオフレシオが3.0あれば期待値はプラスになる。バックテストの読み方としては、勝率と損益比は常にペアで確認する。
主要バックテストツールの比較
2026年7月時点で、仮想通貨の自動売買検証によく使われる代表的な選択肢を整理する。
| ツール | 種別 | データ取得 | 学習コスト | 日本語情報量 | 向いている用途 |
|---|---|---|---|---|---|
| backtesting.py | Pythonライブラリ(イベント駆動) | 自前(CCXT等) | 低 | 中 | 最初の1本、単一戦略の検証 |
| Freqtrade | 自動売買フレームワーク(bot本体込み) | 内蔵コマンドでCCXT経由取得 | 中〜高 | 中 | 検証から実運用まで一気通貫 |
| VectorBT | Pythonライブラリ(ベクトル化) | 自前 | 高 | 少 | 大量のパラメータ探索・ポートフォリオ検証 |
| Backtrader | Pythonライブラリ(イベント駆動) | 自前/各種フィード | 中 | 中 | 約定モデルを細かく作り込みたい場合 |
| TradingView Strategy Tester | チャート内蔵(Pine Script) | プラットフォーム内蔵 | 低 | 多 | ノーコード寄り、視覚的な確認 |
対 Freqtrade
Freqtradeはバックテストだけのライブラリではなく、データ取得・バックテスト・ハイパーパラメータ最適化・ペーパートレード・実運用・Telegram通知までを含む自動売買フレームワークである。CCXT経由で100を超える取引所に接続でき、freqtrade download-data でOHLCVを取得し freqtrade backtesting で検証、dry_run でペーパートレード、というワークフローが最初から用意されている。
# 概念的な流れ(設定ファイル config.json が必要)
freqtrade download-data --timeframe 1h --days 730
freqtrade backtesting --strategy MyStrategy --timeframe 1h
freqtrade hyperopt --strategy MyStrategy --hyperopt-loss SharpeHyperOptLoss -e 200
利点は、検証したコードがそのまま本番のbotになることだ。backtesting.pyで検証してから本番用に書き直すと、その書き直しの過程でロジックがずれる。この「検証と本番の実装乖離」は初心者のbotが想定外の動きをする主因のひとつである。
一方で欠点は学習コストで、ディレクトリ構成・設定ファイル・ストラテジークラスの作法を一通り覚える必要がある。「移動平均クロスが機能するか30分で知りたい」だけならbacktesting.pyのほうが速い。また、Freqtradeが接続対象として想定している取引所は海外の大手が中心で、国内取引所で運用したい場合はCCXTの対応状況とAPIの仕様を個別に確認する必要がある。
対 TradingView Strategy Tester
TradingViewのStrategy TesterはPine Scriptで書いた戦略をチャート上でそのまま検証でき、コード量が少なく結果が視覚的に確認できる。プログラミング未経験からの入口としては最も摩擦が小さい。
ただし自動売買のバックテストとして使う場合、次の制約を理解しておく必要がある。ヒストリカルバーはティック単位のデータを持たないため、Strategy Testerはバー内の値動きをすべて再現するわけではない。この粗さを補うBar Magnifier(下位足のデータを使って約定をより精密にする機能)と、利用可能な全履歴を対象に検証するDeep Backtestingは、いずれも上位プラン(Premium)向けの機能として提供されている(2026年7月時点、公式ヘルプ記載)。また、「On Every Tick」オプションを使う戦略はチャート更新でティックデータが失われるためリペイントし、過去バー上では現実的な結果にならないと公式ドキュメントが明記している。
結論として、TradingViewは「アイデアの一次スクリーニング」には優秀だが、そこで出た数字をそのまま本番の期待値として持ち込むべきではない。ノーコードで自動売買に入る道筋はノーコードで仮想通貨botを組む方法で別途整理している。
対 VectorBT
VectorBTは、価格系列とシグナル系列をNumPyの多次元配列として扱い、多数のパラメータ組み合わせを一度に計算する「ベクトル化」型のライブラリである。1件ずつループで回すイベント駆動型と比べて桁違いに速く、数千〜数万通りのパラメータ探索を現実的な時間で回せる点が最大の武器になる。
裏返すと、学習コストは本記事で挙げた中で最も高い。NumPy/pandasの配列操作に習熟していないと、そもそも戦略を表現できない。また速く大量に試せるということは、過剰最適化に最短距離で到達できるということでもある。VectorBTを使うなら、後述するウォークフォワード検証を必ずセットで組む前提と考えたほうがよい。
対 Backtrader
Backtraderは長く使われてきたイベント駆動型のPythonバックテストライブラリで、ブローカーの約定モデルや注文種別を細かく作り込める。指値・逆指値・OCO・部分約定といった現実的な注文挙動を再現したい場合の選択肢になる。反面、記述量は多く、速度はVectorBTに大きく劣る。仮想通貨の自動売買では、まずbacktesting.pyかFreqtradeで方向性を出し、約定の精密さが問題になった段階で検討するのが現実的な順序だろう。
バックテスト結果を歪める7つの罠
ここに挙げる項目は、どれも「バックテストの成績を実態より良く見せる」方向に働く。良い結果が出たときほど、上から順に潰していく。
ルックアヘッドバイアス(先読み) — その時点で知り得ない情報を使ってしまう典型例。シグナル判定に使った足の終値でそのまま約定させる、あるいは指標計算で
shift(1)を忘れる。1本のずれで成績が別物になるため、意図的に1本余分にシフトして結果が壊れるかを確認する「サニティチェック」を習慣にするとよい。過剰最適化(オーバーフィッティング) — パラメータを総当たりで探し、過去データに最も当てはまる組み合わせを採用する行為。過去のノイズを学習しているだけで、将来には再現しない。パラメータを1つ動かしたときに成績が崖のように落ちる組み合わせは、その谷底にたまたま乗っているだけなので採用しない。近傍のパラメータでもなだらかに良い「高原」を選ぶ。
生存者バイアス — 現在も上場している銘柄だけで検証すると、上場廃止・実質無価値化した銘柄の損失が集計から抜け落ちる。過去に取引されていた銘柄群のうち一定数は消えており、現在のリスト基準で選んだユニバースは構造的に成績が良く出る。アルトコインを対象にする戦略では特に効く。
手数料・スリッページの未考慮 — 手数料ゼロで回したバックテストは、高頻度戦略ではほぼ無意味である。メイカー/テイカーの区別、成行前提なのか指値前提なのか、指値なら約定しなかったケースをどう扱うのかまで決める。バックテストで指値が必ず約定する前提を置くと、実際には取れなかった有利な価格を取り続けることになる。
板の薄さ・流動性の無視 — OHLCVデータには板情報が含まれていない。時価総額の小さい銘柄や深夜帯では、想定サイズを一度に約定させると自分の注文で価格が動く。バックテストの想定ロットが、その足の出来高に対して大きすぎないか(例えば出来高の1%を超えていないか)を必ず確認する。
データスヌーピング/期間の恣意的な選択 — 「2023年から2025年で検証したら好成績」という結果は、その期間を選んだこと自体が結果に依存している可能性がある。強気相場だけを切り取れば、ロングに偏った戦略は何をしても勝つ。上昇・下落・レンジがそれぞれ含まれる期間を意識的にまたぐ。
バイ・アンド・ホールドとの比較欠落 — 年率30%の戦略でも、同期間に単純保有が年率60%なら、複雑さとリスクに見合っていない。必ずベンチマークを並べる。手間とインフラ費用を払う価値があるかは、この差分でしか判断できない。
過剰最適化を避ける検証設計
インサンプルとアウトオブサンプルの分割
最も基本的な対策は、データを前半(インサンプル/学習用)と後半(アウトオブサンプル/検証用)に分け、パラメータ調整は前半だけで行い、後半には一度しか触れないというルールを課すことだ。後半で結果を見てからパラメータを直せば、その瞬間に後半もインサンプルになる。
split = int(len(df) * 0.7)
train, test = df.iloc[:split], df.iloc[split:]
# パラメータ探索は train のみで行う
best = None
for n_fast in range(5, 40, 5):
for n_slow in range(30, 150, 10):
if n_fast >= n_slow:
continue
s = run_backtest(train, n_fast, n_slow) # 自作のラッパー
if best is None or s["sharpe"] > best["sharpe"]:
best = {"n_fast": n_fast, "n_slow": n_slow, "sharpe": s["sharpe"]}
# test は最後に1度だけ確認する
final = run_backtest(test, best["n_fast"], best["n_slow"])
print("in-sample :", best)
print("out-sample:", final)
アウトオブサンプルの成績がインサンプルの半分程度に落ちるのは正常である。ゼロやマイナスになるなら、それは過去のノイズを学習していた証拠なので、そのパラメータは捨てる。
ウォークフォワード検証
より実務的なのがウォークフォワード検証だ。「直近N期間で最適化 → 続くM期間で運用 → 窓をずらして繰り返す」を全期間に対して行い、運用期間だけを繋いだ資産曲線を評価する。実際の運用では定期的にパラメータを見直すはずなので、その運用形態をそのまま検証に写し取る発想である。
| 窓 | 最適化期間 | 検証(運用)期間 |
|---|---|---|
| 1 | 2024年1月〜2024年6月 | 2024年7月〜2024年9月 |
| 2 | 2024年4月〜2024年9月 | 2024年10月〜2024年12月 |
| 3 | 2024年7月〜2024年12月 | 2025年1月〜2025年3月 |
各窓の検証期間でおおむね安定してプラスなら、そのルールは「定期的に再最適化しながら運用できる」と判断できる。窓によって成績が激しく振れるなら、パラメータの再最適化そのものが機能していない。
パラメータ感応度を見る
単一の最良値ではなく、パラメータを動かしたときの成績の面(ヒートマップ)を見る。周囲が全滅している中で1点だけ突出している組み合わせは、ほぼ確実にノイズへの当てはめである。backtesting.py の optimize() はパラメータ組み合わせごとの結果をヒートマップ用に返せるので、可視化して形を確認する習慣をつけたい。
複数銘柄・複数期間でのロバストネス確認
BTCで作った戦略をETHや他の主要銘柄でも回してみる。原理に基づいた戦略なら、成績は落ちても方向性は保たれるはずだ。BTCだけで劇的に良く、他では全滅するなら、その戦略はBTCの特定期間に合わせ込まれている疑いが強い。
フォワードテストから本番投入までの流れ
なぜバックテストだけでは足りないのか
バックテストは「過去のデータに対する計算」でしかない。実際の運用では、APIのタイムアウト、WebSocketの切断、取引所のメンテナンス、注文が拒否される最小注文単位の制約、証拠金の計算誤差といった、価格データに現れない要因が損益に効いてくる。フォワードテストは、この「価格以外の全部」を洗い出す工程である。
ペーパートレードで確認すべきこと
- バックテストで想定した頻度どおりにシグナルが出ているか(大きく乖離するなら実装が違う)
- 注文を出そうとしたタイミングの価格と、バックテストが仮定した約定価格の差はどの程度か
- 24時間動かして、エラーやプロセス停止が起きないか(VPSでの常時稼働の設計はbotをVPSで動かす構成を参照)
- 資産・ポジションの記録が正しく残るか(税務計算に必要になる)
本番投入前チェックリスト
- 手数料とスリッページを含めた条件でバックテストがプラス圏に残っている
shift(1)等でルックアヘッドを潰し、意図的に1本ずらしても不自然に成績が改善しないことを確認した- アウトオブサンプル期間、またはウォークフォワードの各窓で成績が崩壊していない
- 最大ドローダウンが自分の許容範囲内で、その水準に達したときの停止基準を事前に決めてある
- 想定ロットが対象銘柄の出来高に対して過大でないことを確認した
- ペーパートレードを最低2週間実施し、シグナル頻度がバックテストと整合している
- 損切り・強制停止(キルスイッチ)が実装され、実際に発火することをテスト済み
- 本番の初期投入額を、全損しても生活に影響しない金額に設定した
このチェックリストのうち1つでも未達なら、投入額をさらに下げるか、投入自体を延期する。バックテストの成績が良いことは、投入額を上げる理由にはならない。
実運用で分かること
当サイトでは実際に稼働させているAI自動売買の記録を/ai-tradingで公開している。数字を見れば分かるとおり、バックテスト上の期待値と実運用の結果には常に差が出る。この差の大きさを把握しておくこと自体が、次の戦略設計の材料になる。リスク側の考え方はAI自動売買のリスク整理にまとめてある。
国内取引所でAPI自動売買を始めるときの実務ポイント
APIの提供範囲を先に確認する
自動売買を前提にするなら、口座開設前に確認すべきは手数料水準よりもAPIの仕様である。具体的には、(1)公開APIでローソク足や板が取れるか、(2)プライベートAPIで発注・約定照会・残高照会ができるか、(3)WebSocketがあるか、(4)レート制限がどの程度か、の4点。GMOコインは公開API・プライベートAPI・WebSocketを提供し、現物取引とレバレッジ取引に対応していることを公式ドキュメントで明示している(2026年7月時点)。
販売所形式と取引所形式の違い
同じ「取引できる」でも、販売所形式はスプレッドが実質的なコストになり、板がないため自動売買の検証と実態が合いにくい。バックテストで使うOHLCVは取引所形式の約定履歴から作られるため、実際の発注も取引所形式に揃えるほうが乖離が小さい。この点は仮想通貨の自動売買を始める全体像でも触れている。
税務と記録
自動売買は取引回数が多くなるため、記録の自動保存を最初から組み込んでおく。約定ごとに日時・銘柄・数量・価格・手数料をCSVかDBに残す設計にしておかないと、後から集計できなくなる。仮想通貨の損益計算は取引形態によって扱いが変わるため、具体的な申告方法は税理士などの専門家に相談してほしい。
まとめ
仮想通貨の自動売買におけるバックテストは、勝てる戦略を発見する装置ではなく、勝てない戦略を安価に捨てるためのフィルタである。2026年7月時点で、CCXTでOHLCVを取得し、backtesting.pyやFreqtradeで検証するという流れは十分に整備されており、環境構築のハードルはほぼ無くなっている。
したがって差が付くのはツール選びではなく検証設計のほうだ。手数料とスリッページを最初から入れる、シグナルの1本先で約定させる、アウトオブサンプルを死守する、ウォークフォワードで再最適化ごと検証する、そして最後に必ずフォワードテストを挟む。この5点を守るだけで、バックテストは自信を水増しする装置から、リスクを事前に把握する装置に変わる。
なお、バックテストの結果は過去の価格データに対する計算結果にすぎず、将来の損益を保証するものではない。本記事は自動売買の検証手法を解説する情報提供であり、特定の銘柄や取引手法を推奨するものでも、投資助言を行うものでもない。実際の運用は自己責任で、失っても支障のない資金の範囲で行ってほしい。
<!-- INTERNAL_LINKS_RELATED -->
関連記事
<!-- INTERNAL_LINKS_RELATED -->
