勝率87%は、私の検証スクリプトの1行のバグでした ── 損切りが、未来の6本を見ていた
こんにちは。MT4/MT5のインジケーターを作って、GogoJungleで販売している lab.aiueo です。
インジを作る側で起きたことを、続けて書いていきます。今回は、いちばん書きにくい話です。
勝率87%、PF2.21
2026年8月、ゴールド専用のサインツールを作りました。GD-Signal という名前です。
作ったあと、過去のデータで成績を確かめました。ゴールドの1時間足、2011年1月から2026年8月までの15.6年分。9万1,586本の足です。
結果はこうでした。
- 取引回数:423回
- 勝率:87.0%
- PF(総利益÷総損失):2.21
- いちばん長い連敗:3回
正直に書くと、うれしかったです。
この検証から出したPFを、商品ページと紹介記事に根拠として載せて、販売を始めました。
数日後、その数字が全部無効だと分かりました。
検証に使ったPythonのスクリプトに、バグがありました。466行のうちの、損切りの範囲を決める1行です。
直して、同じデータで回し直した結果です。
| 時間足(15.6年) | 直す前 | 直した後 |
|---|---|---|
| 1時間足 | 423回・勝率87.0%・PF2.21 | 544回・勝率46.3%・PF1.07 |
| 4時間足 | 201回・勝率81.6%・PF1.62 | 248回・勝率40.3%・PF0.68 |
勝率は半分になりました。PFは、1時間足が1をわずかに上回る程度、4時間足は1を下回りました。「優位性がある」と書ける数字ではありません。
私は販売を止めて、記事も取り下げました。買ってくださった方はいなかったので、実害は出ていません。
ただ、もし売れていたら、根拠のない数字で買っていただいたことになります。
勝率87%なのに、勝ちが小さすぎた
あとから見返すと、数字そのものにサインが出ていました。
直す前の1時間足の、1回あたりの損益です。
- 勝ったときの平均:+4.99ドル
- 負けたときの平均:−15.10ドル
勝つときは小さく、負けるときは3倍。勝率が87%もあるのに、1回負けると3回分の勝ちが消える形です。
「勝率がやけに高いのに、勝ちが小さい」。この組み合わせが、今回のバグの指紋でした。
理由はこのあと詳しく書きます。ひとことで言うと、損切りの位置を決めるときに、まだ来ていない未来の6本の足を見ていました。
未来の安値より下に損切りを置けば、しばらくは損切りされません。勝率が上がるのは当然です。
この話を1本目にした理由
自分の失敗を書くのは、正直気が進みません。
それでも1本目にしたのは、理由が2つあります。
1つ目は、検証で高い勝率が出たときに、喜ぶ前に疑う手順を、自分のために残しておきたかったからです。
2つ目は、PythonやExcelで売買ルールを検証している方なら、同じ穴に落ちる可能性があるからです。
MT4の言語(MQL4)で書いたロジックをPythonに移すと、価格の並びの向きが逆になります。このスクリプトでは、その向きを1か所だけ直し忘れていました。しかも、移したあとに1行ずつ見直したのに、その1か所は通り抜けました。
このスクリプトは、AI(Claude Code)と一緒に作ったものです。AIに任せた部分も含めて、数字を世に出したのは私です。
数式は出てきません。コードは数行だけです。Pythonを書かない方にも分かるように、足の並びで説明します。
バグの1行
ゴールドの検証スクリプトの、損切りを計算する部分です。
直す前です。
we = min(i + SWING_LEN + 1, n-1)
sl = (l[i:we+1].min() - buf) if is_buy else (h[i:we+1].max() + buf)
直した後です。
ws = max(i - SWING_LEN - 1, 0)
sl = (l[ws:i+1].min() - buf) if is_buy else (h[ws:i+1].max() + buf)
i はサインが出た足の番号で、SWING_LEN は5です。l は安値の並び、h は高値の並びです。
買いなら「この範囲の足の安値のうち、いちばん低いところ」から少し下に損切りを置く、という処理です。
違うのは範囲だけです。
- 直す前:i から i+6 まで(サインの足と、そのあとの6本)
- 直した後:i−6 から i まで(サインの足と、その前の6本)
前と後ろが、そっくり入れ替わっていました。
価格の並びの向きが、逆になる
なぜこうなったのかを説明します。
元のロジックは、MT4のインジとしてMQL4で書いたものです。損切りの範囲は、意味としてはこう書いてあります。
low[evalBar ~ evalBar+SwingLength+1]
MQL4では、価格の並びは 0番がいちばん新しい足 です。番号が大きくなるほど、過去にさかのぼります。
だから「evalBar から +6」は、サインの足から6本ぶん過去を見ています。
一方、Pythonで価格データを読み込むと、ふつうは 0番がいちばん古い足 になります。番号が大きくなるほど、未来に進みます。
並べると、こうです。
MQL4 (0が最新) … [8][7][6][5][4][3][2][1][0] ← 右端が今
Python(0が最古) [0][1][2][3][4][5][6][7][8] … ← 右へ行くほど未来
同じ「+6」でも、MQL4では過去、Pythonでは未来です。
だから移すときは、番号の向きを全部ひっくり返す必要があります。このスクリプトでも、ほかの箇所はひっくり返してありました。損切りの1行だけが、元の書き方のまま残っていました。
未来の6本を見ると、何が起きるか
ここが一番大事なところです。
エントリーは、サインが出た足の次の足の始値です。損切りは「サインの足と、そのあと6本」の安値の、さらに下に置かれます。
つまり、エントリーしてから6本のあいだは、損切りにかかりようがありません。 その6本の安値より下に、最初から損切りがあるからです(売りなら高値の上。向きが逆なだけで同じです)。
- 1時間足なら、6時間
- 4時間足なら、24時間
この間は、負けようがありません。
一方、利確の目標は、近くにある直前の山(スイング)です。6本もあれば、そこに届くことは珍しくありません。
その結果、こうなります。
- 勝ちは、近い目標に届いた小さな勝ち
- 負けは、6本を過ぎたあとに、遠くに置いた損切りまで下がったときだけ。だから1回が大きい
勝率87%、平均の勝ち4.99ドル、平均の負け15.10ドル。数字の形が、そのまま説明できます。
「未来の安値の下に損切りを置けば、誰でも勝率は上がる」
手法が優秀だったのではなく、答えを見ながら損切りを置いていただけでした。
1行ずつの見直しを、なぜ通り抜けたのか
このスクリプトは、MQL4のコードを横に置いて、関数ごとに「元と同じことをしているか」を確かめながら作りました。
この1行は、その確かめ方では見つかりません。
- 元のコード:evalBar から SwingLength+1 本
- Python:i から SWING_LEN+1 本
見比べると、まったく同じに見えます。「元と同じか」を見る見直しは、この1行を正しいと判定します。
問題は、書き方が同じでも、意味が逆になることでした。
見るべきだったのは「元と同じか」ではなく、「この番号は、サインの足より過去か」です。
確かめる問いが違えば、同じ1行を何回見ても見つからない。
実際、見直しは通っていました。
気づいたきっかけ
見つけたのは、数字を出してから数日後です。
同じ系統のロジックをEA(自動売買のプログラム)にして、MT4のストラテジーテスターで回していました。MT4の上で、元のMQL4のコードがそのまま動く形です。
そのEAの勝率が、Pythonで出した勝率と、まったく合いませんでした。
正直に書くと、最初は「再現できない」と記録して、いったん脇に置きました。テスターとPythonでは、細かい条件がいくつも違います。違って当然だと思いたかったのです。
それでも引っかかって、エントリー・損切り・利確の計算を1つずつ追い直しました。そして、損切りの範囲が未来を向いている1行に行き着きました。
直した数字を、どうやって「正しい」と決めたか
バグを直したら、次は直した数字が正しいかが問題になります。直したつもりで、別の間違いを入れているかもしれません。
決め手は、2つの系統の一致でした。
- MT4の実機で、MQL4のコードそのものを動かしたEAの勝率
- 直したPythonで、同じ通貨ペア・同じ期間を回した勝率
この2つが、1ポイント以内で一致しました。
書いた言語も、動かした環境も違う2つが、同じところに着地しました。ここで初めて、直した数字のほうを信じることにしました。
ゴールドの数字(勝率46.3%・PF1.07)も、同じ直し方をしたスクリプトで出したものです。
今、数字を出す前に必ずやっていること
この件のあと、検証の数字を商品ページや記事に載せる前の確認を決めました。今回のバグに直接関係する4つを書きます。
1. 未来を見ていないか、番号を1つずつ確かめる
コードの見直しとは別の作業として、独立してやります。
スクリプトの中で価格の並びを読んでいる箇所を全部書き出して、1つずつ「この番号は、サインの足より過去か」と問います。i より大きい番号が出てきたら、その場で止まります。
「元と同じか」ではなく「過去か」。問いを変えるだけで、今回の1行は一目で引っかかります。
2. 勝率が7割を超えたら、まずバグを疑う
手法がすごいのかもしれない、と考えるのは最後です。
特に「勝率8割以上で、平均の勝ちが平均の負けの3分の1前後」という形は、今回と同じ未来参照でよく出る形です。見かけたら、まず損切りと利確の計算を疑います。
3. MT4/MT5の実機と突き合わせるまで、数字を出さない
Pythonの数字だけでは出しません。MQLのコードをテスターで動かした結果と、勝率が一致するまでは「未検証」として扱います。
4. スクリプトを、数字と一緒に残す
実は、このスクリプトは一時フォルダに置いたままで、消えかけていました。数字だけが文書に残り、どのコードで出したか追えなくなるところでした。
今は、直す前と直した後のスクリプトを両方、インジのソースと同じ場所に保存しています。直す前後の数字の表も、説明と一緒に残しました。
このあと、どうしたか
GD-Signalは、販売を止めたままです。
インジ本体のコードには、バグはありませんでした。バグがあったのは、成績を測る側のコードです。
その後、ゴールド用には GD-Deck という別の道具を作りました。こちらは設計の段階で、勝率も利益も、成績は一切表示しないし、商品ページにも書かないと決めました。
成績を測るコードがなければ、測るコードのバグで商品が崩れることもありません。
GD-Deck の販売ページはこちらです。
まとめ
- 勝率87%・PF2.21は、検証スクリプトの1行のバグから出た数字だった。直すと勝率46.3%・PF1.07
- 原因は、MQL4(0が最新)とPython(0が最古)で並びの向きが逆なのに、損切りの範囲だけ向きを直し忘れたこと
- 未来の6本の安値の下に損切りを置くと、6本のあいだは負けない。だから「勝率が高く、勝ちが小さい」形になる
- 「元と同じか」の見直しでは見つからない。「この番号は過去か」と問う作業を、別に立てる
- 数字は、MQLの実機と一致するまで出さない。スクリプトは数字と一緒に残す
「自分の道具を自分で評価するときは、評価する側のコードのほうが危ない」
8月から、ずっとそう思っています。
検証の数字に助けられたことも、だまされたこともある方は多いと思います。この記事が、喜ぶ前に一度疑うきっかけになれば嬉しいです。
本記事は、私自身の開発と検証の記録です。記事中の勝率・PFなどの数字は、検証の誤りを説明するためのもので、いずれの製品の成績を示すものでもありません。特定の売買手法や製品による利益を保証するものではありません。投資判断はご自身の責任で行ってください。
最後まで読んでいただき、ありがとうございました。
次回は、OANDA証券のMT4終了を前に、インジを使っている方が移る前に確かめておくことを書きます。