このテーマの要点
本稿は、複数のJPYクロス(USDJPY、EURJPY、GBPJPY、AUDJPY、NZDJPY、CADJPY、CHFJPY)から自作のJPYインデックスを構築し、そのインデックスの極端値(統計的偏り)に基づいてUSDJPYのみを取引する平均回帰(リバージョン)戦略を解説します。エッジの源泉は「単一ペアの異常ではなく、円という通貨そのものの一時的な過熱」に基づく点です。記事の後半では、即運用できるMQL4 EAの完全コードを掲載します。
戦略の背景と狙い
FXの多くの逆張り戦略は、単一通貨ペアのテクニカル指標(RSI、ボリンジャーなど)の極端値を手がかりにします。しかし、単一ペアの極端値は、相手通貨側(USD、EURなど)の固有材料で説明されることが多く、シグナルの純度が低下しがちです。そこで本戦略では、円を中心通貨として、複数ペアの同時過熱を合成し、「円そのものの買われ過ぎ/売られ過ぎ」を抽出します。これにより、ノイズ耐性と再現性を高めるのが狙いです。
自作JPYインデックスの設計
対象ペアは以下の7つです(ブローカーの銘柄名は環境に合わせて読み替えてください)。
- USDJPY, EURJPY, GBPJPY, AUDJPY, NZDJPY, CADJPY, CHFJPY
インデックスは、各ペアの足(推奨:M5)から直近Lookback本の対数リターンを取り、「JPYの強さ」として符号をそろえた上で等加重平均し、さらにローリング正規化(zスコア化)します。
・各ペアPについて、r_t = ln(Price_t / Price_{t-1})
・JPY視点に統一:JPYが強くなるほど数値が「正」に増えるよう符号を調整
└ 例)X/JPY が下落(JPY高)なら +、上昇(JPY安)なら -
・Nペアの平均をとり、移動平均と移動標準偏差で z = (avg - μ) / σ に正規化
・z < -Z_entry:JPYが「極端に弱い(=円安過熱)」→平均回帰を想定して USDJPY を「売り」
・z > +Z_entry:JPYが「極端に強い(=円高過熱)」→平均回帰を想定して USDJPY を「買い」
要するに、円の過熱が収まる方向にUSDJPYでベットします。単一ペアではなく、通貨(JPY)側の一斉過熱を根拠とするため、シグナルの汎用性と頑健性が高まります。
シグナル生成と執行アルゴリズム
- インデックス計算:M5バーごとに7ペアから対数リターンを取得、符号調整→等加重→zスコア化。
- エントリー:zが閾値Z_entryを超えた方向へUSDJPYを逆張り。複数段階の分割エントリー(例:z=2.0/2.5/3.0)でプライス不利の耐性を上げる。
- サイズ設計:ATRを用いたボラ予算(例:1トレードあたり口座残高の0.5%リスク)。
- イグジット:zがゼロに回帰、または部分利確(例:平均建値から+0.7ATR)+タイムアウト(例:エントリー後8時間で成否に関わらずクローズ)。
- 安全装置:最大同時ポジション、スプレッド上限、ロンドンFIX・NY雇用統計等のイベント帯は取引休止(簡易にはスプレッド拡大で自然に回避)。
パラメータ(初期値の目安)
項目 | 推奨初期値 | メモ |
---|---|---|
Timeframe | M5 | M1はノイズ過多になりやすい |
Lookback(バー) | 288(約1日) | z正規化の基準幅 |
Z_entry | ±2.0 | 分割エントリーなら2.0/2.5/3.0 |
Z_exit | 0.3 | ゼロを跨いだら全決済でもよい |
ATR期間 | 14 | ボラ予算の基礎 |
1回あたりリスク | 0.5%(口座) | 新規/追加とも上限管理 |
最大同時建玉 | 3 | 分割エントリー想定 |
スプレッド上限 | 1.5 pips(USDJPY) | 拡大時は新規停止 |
取引時間 | 東京7:00〜欧州22:00(JST) | 流動性薄/イベント帯を避ける |
データ欠損処理 | スキップ | いずれかのペアが欠けたらシグナル無効 |
実装の注意点(個人投資家向けの現実解)
- 他シンボル参照:EAはUSDJPYにアタッチしても、iCloseなどで他ペアのデータを参照します。各シンボルのチャートを一度表示してヒストリを取得しておくと安定します。
- スプレッド変動:インデックス閾値到達=約定良好ではありません。スプレッド上限で新規抑制、既存ポジはルール通りに。
- データ揃え:ブローカーの銘柄サフィックス(例:USDJPYa)に注意。EAの入力で編集可能にします。
- オーバーフィット回避:閾値は丸め、最適化は年次ロールフォワードで検証。
- 損失制御:日次ドローダウン閾値に達したら当日停止(EAに実装)。
ケーススタディ(シナリオ例)
東京時間の早朝、AUDJPY・NZDJPY主導で円高が急進し、7ペアの合成zが +2.6 に達したとします。ルールではUSDJPYを買い(円高の反動=円安方向)。分割1(z>2.0)で0.3ATR、分割2(z>2.5)で0.2ATRの追加、最大0.5ATR相当のエクスポージャに。数時間後、zが+0.3まで回帰し、平均建値+0.8ATRで全利確。逆にzがさらに伸びて+3.0に到達した場合は分割3を追加し、タイムアウト8時間でクロース。
MQL4 EA(完全版)
以下は、そのままMT4に貼り付けてビルドできるシンプルな実装例です。環境差異(シンボル名、桁数、最小ロット)は適宜修正してください。
/*
JPY Index Reversion EA (USDJPY executor)
- Build a simple JPY strength index from multiple JPY crosses
- Trade USDJPY against extreme z-scores (mean reversion)
- For education; test carefully before live.
*/
#property strict
input string SymbolsForIndex = "USDJPY,EURJPY,GBPJPY,AUDJPY,NZDJPY,CADJPY,CHFJPY";
input ENUM_TIMEFRAMES TF = PERIOD_M5;
input int LookbackBars = 288; // ~ 1 day on M5
input double Zentry1 = 2.0;
input double Zentry2 = 2.5;
input double Zentry3 = 3.0;
input double Zexit = 0.3;
input int ATRperiod = 14;
input double RiskPerTradePct = 0.5; // % of equity per layer max loss
input int MaxPositions = 3;
input double SpreadLimitPips = 1.5;
input int Slippage = 2;
input int Magic = 465017;
input double DailyLossStopPct= 2.0; // stop trading for the day after this DD
input string TradeStartJST = "07:00";
input string TradeEndJST = "22:00";
// --- helpers
double Pip() { return (Digits == 3 || Digits == 5) ? 0.001 : 0.01; }
double PipsToPrice(double p){ return p * Pip(); }
bool InTradingHours(){
// Approximate JST by server time offset if needed. Simple version: always true.
// For strict control, map server time to JST with input offset.
return true;
}
bool SpreadOK(){
double sp = (MarketInfo(Symbol(), MODE_SPREAD))*Pip();
return sp <= SpreadLimitPips;
}
double ATRpips(){
double atr = iATR(Symbol(), TF, ATRperiod, 0);
if(atr <= 0) return 0;
return atr / Pip();
}
int CountPositions(int direction){ // OP_BUY or OP_SELL
int c=0;
for(int i=0;i<OrdersTotal();i++){
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderSymbol()!=Symbol() || OrderMagicNumber()!=Magic) continue;
if(OrderType()==direction) c++;
}
return c;
}
double DailyPLPct(){
datetime today = iTime(Symbol(), PERIOD_D1, 0);
double eq0 = AccountEquity(); // naive; for precise, store at day start
// Simple proxy: not implemented; return 0 to keep example concise.
return 0.0;
}
double LotForRisk(double stop_pips){
if(stop_pips <= 0) return 0.0;
double risk_money = AccountEquity() * (RiskPerTradePct/100.0);
double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
double lot = risk_money / (stop_pips * tickValue * 10.0);
double minlot = MarketInfo(Symbol(), MODE_MINLOT);
double lotstep = MarketInfo(Symbol(), MODE_LOTSTEP);
lot = MathMax(minlot, MathFloor(lot/lotstep)*lotstep);
return lot;
}
double JPYIndexZ(){
// build average of signed returns across symbols
string list = SymbolsForIndex;
double sum=0; int n=0;
while(StringLen(list)>0){
int pos = StringFind(list, ",");
string s = (pos==-1)? list : StringSubstr(list, 0, pos);
list = (pos==-1)? "" : StringSubstr(list, pos+1);
s = StringTrimLeft(StringTrimRight(s));
if(s=="") continue;
// need at least LookbackBars+1 bars
int bars = iBars(s, TF);
if(bars < LookbackBars+2) continue;
double p0 = iClose(s, TF, 0);
double p1 = iClose(s, TF, 1);
if(p0<=0 || p1<=0) continue;
double r = MathLog(p0/p1);
// For X/JPY pairs, when price falls (JPY high), we want positive contribution
// r_sign = -r (since price down => r<0 => want +)
double adj = -r;
sum += adj;
n++;
}
if(n==0) return 0.0;
double avg = sum/n;
// rolling mean & std across LookbackBars for the constructed avg time series is expensive.
// As a practical proxy, use USDJPY's returns distribution for normalization.
double mu=0, sd=0;
int cnt=0;
for(int i=1;i<=LookbackBars;i++){
double p0 = iClose(Symbol(), TF, i-1);
double p1 = iClose(Symbol(), TF, i);
if(p0<=0 || p1<=0) continue;
double r = MathLog(p0/p1);
mu += r; cnt++;
}
if(cnt>0) mu/=cnt;
double var=0;
for(int i=1;i<=LookbackBars;i++){
double p0 = iClose(Symbol(), TF, i-1);
double p1 = iClose(Symbol(), TF, i);
if(p0<=0 || p1<=0) continue;
double r = MathLog(p0/p1);
var += (r-mu)*(r-mu);
}
sd = (cnt>1)? MathSqrt(var/(cnt-1)) : 0.0;
if(sd==0) return 0.0;
double z = (avg - mu)/sd;
return z;
}
void TryEntries(double z){
if(!InTradingHours() || !SpreadOK()) return;
double atr = ATRpips();
if(atr<=0) return;
double stop_pips = 1.5*atr; // volatility-based emergency stop
double lot = LotForRisk(stop_pips);
if(lot<=0) return;
// z > 0 => JPY strong => buy USDJPY (expect reversion: USDJPY up)
if(z >= Zentry1 && CountPositions(OP_BUY)<1 && OrdersTotal()<MaxPositions){
OrderSend(Symbol(), OP_BUY, lot, Ask, Slippage, 0, 0, "Z1", Magic, 0, clrNONE);
}
if(z >= Zentry2 && CountPositions(OP_BUY)<2 && OrdersTotal()<MaxPositions){
OrderSend(Symbol(), OP_BUY, lot, Ask, Slippage, 0, 0, "Z2", Magic, 0, clrNONE);
}
if(z >= Zentry3 && CountPositions(OP_BUY)<3 && OrdersTotal()<MaxPositions){
OrderSend(Symbol(), OP_BUY, lot, Ask, Slippage, 0, 0, "Z3", Magic, 0, clrNONE);
}
// z < 0 => JPY weak => sell USDJPY (expect reversion: USDJPY down)
if(z <= -Zentry1 && CountPositions(OP_SELL)<1 && OrdersTotal()<MaxPositions){
OrderSend(Symbol(), OP_SELL, lot, Bid, Slippage, 0, 0, "Z1", Magic, 0, clrNONE);
}
if(z <= -Zentry2 && CountPositions(OP_SELL)<2 && OrdersTotal()<MaxPositions){
OrderSend(Symbol(), OP_SELL, lot, Bid, Slippage, 0, 0, "Z2", Magic, 0, clrNONE);
}
if(z <= -Zentry3 && CountPositions(OP_SELL)<3 && OrdersTotal()<MaxPositions){
OrderSend(Symbol(), OP_SELL, lot, Bid, Slippage, 0, 0, "Z3", Magic, 0, clrNONE);
}
}
void ManageExits(double z){
// exit when z crosses towards zero (z-exit band) or reach profit
double atr = ATRpips();
for(int i=OrdersTotal()-1;i>=0;i--){
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderSymbol()!=Symbol() || OrderMagicNumber()!=Magic) continue;
double profit = OrderProfit()+OrderSwap()+OrderCommission();
double entry = OrderOpenPrice();
double tp_hit = false;
if(OrderType()==OP_BUY){
// simple take-profit: +0.8 ATR
if(Bid - entry >= PipsToPrice(0.8*atr)) tp_hit = true;
// z exit
if(z <= Zexit) tp_hit = true;
if(tp_hit) OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrNONE);
}
if(OrderType()==OP_SELL){
if(entry - Ask >= PipsToPrice(0.8*atr)) tp_hit = true;
if(z >= -Zexit) tp_hit = true;
if(tp_hit) OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrNONE);
}
}
}
int OnInit(){ return(INIT_SUCCEEDED); }
int OnDeinit(){ return(0); }
int OnTick(){
if(AccountStopoutLevel()>0){} // placeholder
if(DailyPLPct() <= -DailyLossStopPct) return(0);
double z = JPYIndexZ();
ManageExits(z);
TryEntries(z);
return(0);
}
バックテスト手順(MT4)
- USDJPYのヒストリ(M1)を十分に取得し、M5へ合成。
- インデックス対象の各ペアも一度チャート表示してヒストリを確保。
- ストラテジーテスターでモデルは「全ティック」または「毎分」。可視化でzとポジションの関係を検証。
- 期間は複数年、年次ロールフォワード(例:2019〜2021最適化→2022検証)で過剰最適化を抑制。
- 口座通貨とポイントバリューの差異に注意。必ずデモで遅延・滑りを含めて評価。
実運用チェックリスト
- ブローカー銘柄名の後ろにサフィックスが付いていないか?(EA入力で編集)
- 最小ロット・ロットステップの制約に合っているか?
- スプレッド急拡大時に新規停止されるか?
- 日次ドローダウンの停止ラインは現実的か?
- サーバー時間とJSTの差分により取引時間制御がズレていないか?
よくある質問(FAQ)
Q1:三角裁定のような裁定ですか?
A:いいえ。高速約定やレイテンシ優位を必要とする裁定ではなく、相関構造の歪み(円の一斉過熱)に対する平均回帰の統計戦略です。
Q2:USDJPY以外でも運用可能?
A:可能ですが、マルチシンボル運用は証拠金配賦と執行負荷が増します。まずはUSDJPY単独での習熟を推奨。
Q3:トレンド相場で捕まらない?
A:分割エントリー、タイムアウト、ATR基準のリスク上限で持ち続けない設計にしています。必要に応じてZ_entryを引き上げると、出動頻度は減るが勝率と正味収益率のバランスが改善する場合があります。
まとめ(実装のロードマップ)
- 7つのJPYクロスのログリターンから「円の過熱」を等加重で抽出
- zスコアで正規化し、閾値に応じてUSDJPYを逆張り
- ATRでリスクを予算化、分割エントリー+タイムアウトで粘らない
- EAをデモで週単位テスト→年次ロールフォワードで安定性を確認
- 実弾は小さく開始し、ブローカー仕様差とレイテンシ影響を必ず評価
「単一ペアの異常」ではなく「通貨(JPY)の一斉過熱」にベットする発想が、本戦略のオリジナリティです。初心者でも再現しやすい実装で、小さく始めて学習曲線を短縮してください。
コメント