前回、AI(Gemini)に指示書を作ってもらい、AI(Claude)にプログラムを作ってもらった「パーフェクトオーダーEA」は、バックテストの結果が惨敗。1年ちょっとで破綻してしまいました。
その原因は、AIが得意とする「エントリー」の裏側に隠された、あまりに不器用な決済ロジックにありました。
今回は、結果が出ないAI製EAを、エンジニアの視点から「ATR(Average True Range)」という数学的指標を用いて再設計します。
数行のコード修正で、わずか1年と少しで破綻していたEAは、5年間耐え抜き、さらに微益ながら利益を出すEAへと変貌を遂げました。
AIの限界を人間がどう補完するのか。その記録を公開します。
パーフェクトオーダーEAが損失を出し続けた原因
まずは、パーフェクトオーダーEAが損失を出し続けた原因を調べてみましょう。
バックテスト結果をチャートで見てみると、以下の2点が原因として浮かび上がってきました。
- 決済が遅い
- レンジでの取引
決済が遅い
EAでは、短期MAと中期MAの反対方向のクロス確定で決済ですが、以下の画像のように決済条件を満たすまでにレートが戻ってきてしまい、せっかく含み益だった取引が決済時は±0や損失になっていることが多くありました。
要するに、MAがクロスするのを待っている間に、せっかくの含み益が溶けて、逆方向に突き抜けてから決済されている状況ですね。

レンジでの取引
少し見づらかったので、取引履歴を黄色に変更していますが、買いポジションの履歴です。
そこまで値動きのないレンジでエントリー条件を満たしたのでそこまで伸びず、決済時は損失という状況です。

決済が遅い点については、改善の余地が大きくあります。
含み益になった局面があるということは、エントリータイミングは問題なかったと考えられ、もう少し決済が早ければ利確できたということになります。
レンジでの取引については、本当はエントリー条件で除外できればよいのですが、パーフェクトオーダーを使用している性質上、完全に除外するのは難しいです。
というのも、パーフェクトオーダーの初手は、正直ボラティリティが無いことが多いです。
ボラティリティが無いエントリーを除外すると、大きく勝てたはずのエントリーポイントも除外してしまうリスクが大きくなります(うまい除外方法は模索したいところですが…)
解決策は相場の呼吸に合わせた「ATR」
まずは、決済が遅い点について解決してみましょう。
ズバリ、ATRを用いたTP/SLの導入です。
ATR(Average True Range)とは、日本では「真の値幅の平均」とも訳されます。
その足で本当に動いた値幅を計算し、その平均値を求めます。
どういう指標なのかを簡単に言うと「その足でどれくらい値動きがありそうか」の目安です。
「TP/SLの設置なら、固定pipsでよいのでは?」と思いましたか?良い着眼点です。
ただ、今回の場合はATRによるTP/SLの設置の方がメリットがあります。
パーフェクトオーダーはトレンドフォロー型なので、その時のボラティリティにより伸び幅が違います。
つまり、固定pipsだとボラティリティが小さいときには伸びきれず、TPに届かずSLにかかってしまう、ということもでてきます。
「その足でどれくらい値動きがありそうか」を示すATRを用いることで、TPやSLを狭すぎず広すぎずのいい塩梅で設置できるというわけです。
また、TPやSLを狭すぎず広すぎずのいい塩梅で設置するということは、レンジ相場では早めに損切り、または利確ができるので、痛手を小さくすることができるメリットも考えられます。
エンジニアの実装内容
早速ATRを用いたTP/SLを追記しましょう。
まずは、現在のMA決済をOFFにします。
とはいえ削除してしまうのはもったいないので、パラメータ設定で使うかどうかを切り替えられるようにします。
// パラメータ設定
input string MAExitConfig = ""; //=== MA決済設定 ===
input bool MAExitON = false; //└ON/OFF
(中略)
void OnTick()
{
(中略)
// ポジションがある場合は決済条件をチェック
if(MAExitON && hasPosition)
{
CheckCloseCondition(ticket, positionType, ma_short_current, ma_medium_current,
ma_short_prev, ma_medium_prev, hasPosition);
}
(中略)
}「ポジションを持っていたら、決済チェック」を「MA決済がON(true)でポジションを持っていたら、決済チェック」に変更しました。
初期値はfalseです。当分使わないのでね…
さて、次は本命のATRによるTP/SLの実装です。
// パラメータ設定
input string ATR_TPSLConfig = ""; //=== ATR TP/SL設定 ===
input int ATR_TPSL_Period = 14; //├ATR期間
input double ATR_TPAdjust = 3.0; //├TP倍率
input double ATR_SLAdjust = 1.5; //└SL倍率
// 買いエントリー処理(売りも同じく実装)
void OpenBuyOrder()
{
(中略)
// TP/SL計算用ATR : 確定時のATRを使用
double ATR = iATR(_Symbol, _Period, ATR_TPSL_Period, 1);
double TP = ask+ATR*ATR_TPAdjust;
double SL = ask-ATR*ATR_SLAdjust;
double stopLevel = MarketInfo(_Symbol, MODE_STOPLEVEL);
if((NormalizeDouble(ATR*ATR_TPAdjust, _Digits)<=NormalizeDouble(stopLevel*_Point, _Digits)) ||
(NormalizeDouble(ATR*ATR_SLAdjust, _Digits)<=NormalizeDouble(stopLevel*_Point, _Digits)))
{
PrintFormat("[%d] TP/SLがストップレベルに抵触 | ボラティリティ不足のためエントリー回避", __LINE__);
PrintFormat("[%d] ATR : %s | TP : %s | SL : %s",
__LINE__, DoubleToStr(ATR, _Digits), DoubleToStr(ATR*ATR_TPAdjust, _Digits), DoubleToStr(ATR*ATR_SLAdjust, _Digits));
PrintFormat("[%d] ストップレベル : %s", __LINE__, DoubleToStr(stopLevel*_Point, _Digits));
return;
}
// お作法:TP/SL、エントリー価格はNomalizeDoubleで小数点を整頓する
int ticket = OrderSend(_Symbol, OP_BUY, LotSize, NormalizeDouble(ask, _Digits), slippage_points,
NormalizeDouble(SL, _Digits), NormalizeDouble(TP, _Digits), "PerfectOrder Buy", MagicNumber, 0, clrBlue);
(中略)
}ポイントは以下の3つです。
- パラメータ設定でTPやSLの倍率を変更できるようにする(初期値はリスクリワード比率が1:2です)
- ストップレベルに抵触したときは、エントリー回避する
- NormalizeDoubleで小数点の端数を調整する
今回のEAは基本的に1時間足チャートでテストしているわけですが、1時間足チャートでストップレベルに抵触するということは、相当ボラティリティが小さいと考えられます。
その状態で無理にエントリーさせても、トレンドフォロー型のロジックを踏まえると賭けに出ているようなものなので、エントリーはしないようにしました。
ストップレベルとは、ブローカーが指定する「現在価格からこの範囲にはTP/SLを含めた待機注文を発注しないでください」という値幅です。
2026年1月18日時点でFXTFのUSDJPYのストップレベルは50point(5.0pips)ですので、エントリー時にTP/SLを発注する場合は、5.0pipsよりも広い値幅でTPやSLを発注しないといけないということですね。
最近はストップレベルを0にしているブローカーもありますが、使っているブローカーのストップレベルは把握しておきましょう(エントリーに失敗するので…)
NormalizeDoubleは、MT4やMT5のプログラムを作る上でのお作法です。
MT4やMT5で小数点を扱う時、100.0なのに、100.000000000001とか、99.9999999999999とか、すごく小さいブレがでます。これはMT4やMT5の仕様です。
これがなかなか曲者で、プログラムの内容によってはこのブレにより、思っている動作と違う動作になったりします。よくあるのはロット数の計算(複利ロット)ですね。
これが発生すると原因解明などに非常に時間がかかるし面倒なので、NormalizeDoubleという関数で、ブレを消し去るということをします。
意外とNormalizeDoubleはしていないプログラムが多いのです。私も最初はやっていなかったのですが、色々プログラムを作っている経験で今は必ず実装しています。
小数点を使う比較条件や、発注時の価格、ロット数、TPレート、SLレートなどは基本的にNormalizeDoubleで正しく桁数を調整してあげると安心です(パラメータ設定で指定したロット数は、しなくても大丈夫だと思います)
検証結果は5年破綻せず10万円が1.7倍に
さて、それではバックテストです。
設定は前回と同じく以下の通り。
- 通貨ペア:USDJPY
- 時間足:1時間足
- スプレッド:20point(2.0pips)
- 期間:2021年1月1日~2025年12月31日(5年間)
- 初期資金:10万円
バックテスト結果はこうなりました。

一応、前回の結果も比較できるように貼りましょう。

こんな感じに変化しました。
| 項目 | 実装前 | 実装後 |
|---|---|---|
| 取引数 | 42 | 160(+118回) |
| 破綻 | 約14ヶ月で破綻 | 破綻せず(5年完走) |
| プロフィットファクター | 0.43 | 1.20 |
| 純益 | -56961.50円 | +78888.37円 |
| 最大ドローダウン | 62.08%(68908.55円) | 32.29%(61518.70円) |
| 勝率 | 21.43% | 37.50% |
| 期待利得 | -1356.23 | 493.05 |
1年ちょっとで破綻していたものが、決済を変えただけで5年完走、しかも利益を出している!これはすごいですね。
エントリーポイントも重要ですが、決済ポイントがいかに重要かがわかる良い検証でした。
勝率が37.50%と低めですが、リスクリワード比率が1:2ですので、損切りしやすい点を考えると及第点といったところでしょうか。
初期値はリスクリワード(損失1:利益2)に設定しています。
勝率が約37%と低くても資産が残るのは、この『損小利大』の設計があるからです。相場の状況に合わせて、この倍率を調整できるようにしました。
ただ、プロフィットファクターが1.2なので、勝率か、利幅かどちらかを改善したほうが良さそうです。
さらに、最大ドローダウンが32%もあり、こちらはまだ改善すべきです。
まとめ:AIとエンジニアの「共作」が最強
AIは確かにロジックを生み出して、コードも作ってくれます。
ただ、完璧なものを生み出してくれるわけではないので、人間がバックテストで検証し、ロジックの調整をしてあげる必要がありますね。
もちろん、検証結果を元にAIと相談もできるので、バックテスト後は全てを人間でやる必要はありません。
AIに投げっぱなしではなく、AIとエンジニア(人間)が共作することで、EAがより良いものになっていきそうです。
次回の課題:ドローダウン32%を「トレーリングストップ」で削る
利益は出るようになりましたが、まだドローダウンが32%と大きめですので、小さくしたいところです。
次回は、含み益を守りながら利益を伸ばす「トレーリングストップ」の実装に挑戦します!

今回作成したコード
今回作成したMQL4コードはこちらです。


コメント