手元でFreqAI(LightGBMの回帰モデル)を使った暗号資産の自動売買ボットをドライラン運用しています。ある日「数日間まったく新規エントリーが発生していない」ことに気づき、AIエージェントに調査させたところ、単なる不具合ではなく「勝率と期待値は別物」という、機械学習トレーディングにありがちな罠が見えてきました。ログや設定を右から左に信じるのではなく、実際に動いているモデルの予測値とバックテストの生データまで遡って検証した過程を共有します。
「取引が止まっている」原因はゲートではなくモデルの予測値だった
このボットには2つの安全装置があります。
- 市場全体の地合い(regime)と、上位のディレクティブ(risk_off/neutral/risk_onなど)によるエントリーゲート
- FreqAIモデルが予測する期待変動率が、一定の閾値(
entry_threshold)を超えているかどうかのシグナル判定
以前、①のゲートが古い判断のまま固定されていて全取引をブロックしていたことがありました。今回は同じ「取引が止まっている」でも原因が違い、ゲートは開いている(risk_offではない)のに、②のシグナルが閾値に届いていませんでした。
FreqAIのREST APIから全銘柄の最新の予測値を直接取得して確認すると、こうなっていました。
LINK/USDT +0.25% (最大)
ADA/USDT +0.36%
DOGE/USDT +0.31%
BTC/USDT +0.18%
...(他は0近辺かマイナス)
entry_threshold は 1.0% に設定されているので、どのペアも閾値の1/4程度にしか届いていません。do_predict=1 なので予測自体は正常に出ており、単に「今は市場が静かで、モデルが1%以上の値動きを予測できるほど強いシグナルが出ていないだけ」という、ある意味で健全な状態でした。
entry_threshold の中身:予測しているのは「2時間先の平均価格」
このモデルの予測ターゲット(ラベル)は次のように定義されています。
def set_freqai_targets(self, dataframe, metadata, **kwargs):
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-label_period)
.rolling(label_period)
.mean()
/ dataframe["close"]
- 1
)
return dataframe
label_period_candles=24、時間足5分足なので、これは「今後2時間(24本の5分足)の平均終値が、現在の終値より何%高いか」を予測する回帰モデルです。entry_threshold=1% は「この2時間先の平均予測が+1%以上のときだけロングを打つ」というフィルターというわけです。
数字だけ見ると「1%」は妥当そうに思えます。ですが、ここで「そもそもこの1%は何を根拠に決まったのか」を確認することにしました。
コードのコメントを鵜呑みにせず、バックテストの生データを開く
このストラテジーのdocstringには、こう書かれていました。
entry_threshold / exit_threshold and the roi/stoploss/trailing values below are hyperopt targets … a handful of large stop-loss exits eat the gains from an otherwise ~65% win rate(6-month walk-forward backtest, 2026-07-05)
「勝率65%」という数字はいかにも説得力がありますが、AIエージェントに指示して実際に保存されているバックテスト結果(zip形式のJSON)を全部開かせて検証したところ、次のような実態が出てきました。
| 期間 | トレード数 | 勝率 | トータル損益 |
|---|---|---|---|
| 2026-01-01〜07-05(6ヶ月・docstringが指す本命) | 792 | 64.6% | -38.7 USDT(-3.9%) |
| 2026-05-01〜07-05 | 24 | 58.3% | ほぼゼロ |
| 2026-06-15〜07-05 | 94 | 69.1% | ほぼゼロ |
| 2026-06-25〜07-05 | 54 | 59.3% | ほぼゼロ |
勝率65%という記述自体は事実でした。しかし同じバックテストのトータル損益は-3.9%、つまり負けています。 docstringが自分で書いている通り、原因は利確と損切りの非対称性です。
minimal_roi = {"0": 0.03, "30": 0.015, "120": 0} # 利確: 最大3%、時間経過で1.5%→0%に減衰
stoploss = -0.05 # 損切り: -5%固定
勝率65%で損益分岐するには「平均利益 ÷ 平均損失 ≥ (1-0.65)/0.65 ≈ 0.54」が必要です。ところが利確の天井が最大3%(しかも30分後には1.5%まで目減り)に対し、損切りは-5%固定。たまに出る-5%の負けが、頻度の多い小さな勝ちの積み上げを静かに食いつぶしていた、というのがこのバックテストの実態でした。「勝率が高い=儲かる」ではない、という機械学習トレーディングの典型的な落とし穴です。
さらに掘ると、「hyperoptで最適化された値」でもなかった
entry_threshold はhyperoptで探索可能なパラメータとして定義されています。
entry_threshold = DecimalParameter(
0.003, 0.03, default=0.01, space="buy", optimize=True, load=True
)
optimize=True, load=True なので、本来はhyperoptで求めたベストパラメータのJSONファイルを自動で読み込んで上書きする仕組みです。ところが実際にリポジトリを確認すると、
- docstringが根拠として名指ししているhyperopt実行スクリプトがそもそも存在しない
- ベストパラメータを読み込むためのJSONファイルも存在しない
という状態でした。つまり今動いている 1.0% は、hyperoptで検証済みの値ではなく、コード上の初期デフォルトがそのまま本番に残っていただけだったのです。コメントには「hyperoptで調整した」と書いてあっても、実際にそれを裏付ける成果物がリポジトリに存在するかは別問題、ということを痛感させられました。
学んだこと
- 「取引が起きない」を即バグ扱いしない。ゲート(判断ロジック)とシグナル(モデルの予測強度)は別レイヤーなので、どちらで止まっているかをまず切り分ける。今回はREST APIから生の予測値を直接取得することで、モデルは正常でシグナルが単に弱いだけと確認できた。
- 勝率だけでストラテジーを評価しない。利確・損切りの非対称性(リスクリワード)まで含めた期待値で見ないと、「勝率65%なのにトータルで負けている」状態を見逃す。
- コード内のコメントや設計意図の記述を、実際のリポジトリの成果物と突き合わせて検証する。「hyperoptで最適化した」という説明があっても、それを裏付けるスクリプトや結果ファイルが実在するとは限らない。AIエージェントに調査を任せる際も、この「言っていることと実物が一致しているか」の裏取りは省略しないようにしています。
AIエージェントに定型的な確認を任せる際は、表面的なログの有無だけでなく、REST APIから一次データを取得したり、バックテストの生JSONを開いて数字を突き合わせたりと、「調べれば裏が取れることは全部裏を取らせる」運用にしています。今回のケースも、docstringの「勝率65%」という記述を鵜呑みにせず、実データまで遡ったことで初めて「トータルでは負けている」「hyperoptスクリプトが実在しない」という2つの事実にたどり着けました。