最終更新: 2026年06月
MQL5で複数通貨EAのポジション管理を実装する際、マジックナンバー分離・ヘッジ口座API差異・証拠金チェックを正しく設計することが、複数EA並行稼働の安定運用に直結する。
複数のEAを同一口座で稼働させ始めた当初、あるトラブルを経験した。EUR/USDを担当するEAとGBP/USDを担当するEAを同時に動かしていたのだが、EUR/USD側のEAがGBP/USDのポジションを誤クローズするという事態が起きた。MQL4からMQL5への移行直後で、ポジション管理の設計をほとんど考慮せずコードを書いていた時期の話だ。ポジション取得ループのフィルタリングが不十分なまま PositionClose() を呼び出していたのが原因で、気づいたときには他のEAのポジションが消えていた。
その体験から、複数通貨EA並行稼働では「どのEAが持つポジションか」を識別する仕組みが不可欠だと骨身に沁みた。本記事では、マジックナンバーによるEA識別、ヘッジ口座と通常口座のAPI差異、証拠金チェック付きロットサイズ計算、ポートフォリオレベルのドローダウン監視まで、実装上の要点を解説する。
複数通貨EA並行稼働でなぜ問題が起きるのか
MQL5の複数通貨EAでポジション管理の問題が起きる根本原因は、口座単位のフラットなポジションプールに複数EAのポジションが混在し、明示的なフィルタリングなしには自分のポジションを識別できないからだ。 [内部リンク: MQL5 EA基礎設計ガイド]
MetaTrader 5(MT5)では、口座上のポジションは「口座単位」でフラットに管理される。EA Aが開いたEUR/USDも、EA BがGBP/JPYで開いたポジションも、同じポジションプールに放り込まれる仕組みだ。
ここで引っかかるのが、MQL5のポジション操作関数の仕様だ。PositionsTotal() は口座全体のポジション数を返す。「自分のポジションだけを数える」「自分のポジションだけを閉じる」ためには、明示的なフィルタリングが欠かせない。
フィルタリングに使う識別子は主に2つある。
- 通貨ペア(POSITION_SYMBOL): EAが担当する通貨ペアで絞り込む
- マジックナンバー(POSITION_MAGIC): EA固有の識別番号で絞り込む
通貨ペアだけで絞り込む設計は一見シンプルに見えるが、同じ通貨ペアを複数のEAが担当するケースでは通用しない。手動トレードと混在する環境なら、手動ポジションをEAが誤クローズするリスクもある。マジックナンバーとの組み合わせが安全設計の基本だ。
冒頭で述べたトラブルもここが原因だった。当時のコードは通貨ペアの一致しか確認しておらず、別のEAが同じ通貨ペアのポジションを持っていた場合に誤動作する構造になっていた。
PositionsTotal() + PositionGetTicket() ループの基本実装
MQL5のポジション走査は「PositionGetTicket(i)でチケット取得 → PositionSelectByTicket(ticket)で選択 → PositionGetXxx()でプロパティ取得」という2段階構造が必須であり、この手順を守らないとバグの原因になる。 [内部リンク: MQL5 PositionSelect 仕様詳解]
MQL5でのポジション操作はMQL4と比べてAPIが大きく変わっている。MQL4では OrdersTotal() と OrderSelect() を使う設計だったが、MQL5では約定済みポジションと未約定注文が明確に分離された。この変更に気づかないまま移行すると、ポジション操作で意図しない挙動を踏む。
ポジションを走査する基本パターンは次のとおりだ。
// 基本的なポジション走査ループ
int total = PositionsTotal();
for(int i = 0; i < total; i++)
{
// インデックスからチケット番号を取得
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue; // 取得失敗時はスキップ
// チケットでポジションを選択(内部キャッシュを更新)
if(!PositionSelectByTicket(ticket)) continue;
// 選択後にプロパティを取得できる
string symbol = PositionGetString(POSITION_SYMBOL);
long magic = PositionGetInteger(POSITION_MAGIC);
double volume = PositionGetDouble(POSITION_VOLUME);
double profit = PositionGetDouble(POSITION_PROFIT);
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
}
PositionGetTicket(i) はインデックスからチケット番号を取得するだけで、ポジションの詳細プロパティにはアクセスできない。PositionSelectByTicket(ticket) を呼んで初めて、後続の PositionGetString() や PositionGetDouble() が正しい値を返す。
「選択(Select)→プロパティ取得(GetXxx)」の2段階構造がMQL5ポジション操作の要だ。MQL4経験者がMQL5に移行するとき、この構造を見落として PositionGetXxx() 単独で呼び出し、常に直前に選択されたポジションのプロパティが返ってくるバグを作り込むパターンは多い。私も最初のMQL5コードで同じミスを踏んだ。
余談だが、ループ中にポジションが決済されるケースを考慮するなら、PositionsTotal() - 1 から逆順で回す方が堅牢だ。決済時にインデックスがシフトするため、順方向ループだとスキップされるポジションが出る。チケット番号を事前にリストへ収集してからループ外で操作する設計も悪くない。
マジックナンバーによるEA識別と誤クローズ防止
MQL5の複数通貨EA環境でのポジション管理では、通貨ペアとマジックナンバーの2段階フィルタリングが誤クローズ防止の最小要件だ。 [内部リンク: MT5 マジックナンバー設計ガイド]
マジックナンバーは、EAが発注時に注文に付与する整数の識別子だ。同じMT5口座上で複数のEAが稼働していても、マジックナンバーが異なれば互いのポジションを誤認識しない。シンプルな仕組みだが、これを省くと複数EA稼働環境は簡単に崩壊する。
通貨ペアとマジックナンバーの両方で絞り込む実用的なフィルタリング関数を書くとこうなる。
// 複数通貨ペア × マジックナンバーによるポジションフィルタリング
input long MagicNumber = 12345;
int CountPositions(string symbol)
{
int count = 0;
int total = PositionsTotal();
for(int i = 0; i < total; i++)
{
ulong ticket = PositionGetTicket(i);
if(!PositionSelectByTicket(ticket)) continue;
string pos_symbol = PositionGetString(POSITION_SYMBOL);
long pos_magic = PositionGetInteger(POSITION_MAGIC);
// 通貨ペアとマジックナンバーの両方が一致するものだけカウント
if(pos_symbol == symbol && pos_magic == MagicNumber)
count++;
}
return count;
}
見た目はシンプルだが、「このEAが持つポジションだけを数える」という明示的な所有権の概念を実装している点が核心だ。
マジックナンバーの割り当てにはいくつかの慣例がある。私は通常、EAごとに4〜5桁の固定値を割り当てる。チーム開発や複数EAの同時運用を考えるなら、マジックナンバーの管理台帳を作っておくと後で助かる。下3桁をEA種別、上桁を口座環境(デモ/本番)で分けるといった運用も現実的だ。
マジックナンバーによるフィルタリングは誤クローズ防止だけが目的ではない。「どのEAが何回ポジションを開いたか」をログで追跡できるため、パフォーマンス分析にも使える。
Claudeと会話しながらインジケータが作れるhedgrow-fxはこちら — マジックナンバー体系の設計やフィルタリングロジックの実装もAIと対話しながら進めることができる。
ヘッジ口座と通常口座のAPIの違い
ヘッジ口座では複数ポジションが存在するため PositionSelect(symbol) は最小チケットのポジションのみ選択する。ポジションを正確に指定するには PositionSelectByTicket() を使うこと。 [内部リンク: MT5 ヘッジ口座 ネッティング口座 違い]
口座タイプによるAPI仕様の差異は、移行時に踏みやすい落とし穴だ。
通常口座(ネッティング口座) では、同一通貨ペアのポジションは相殺される。EUR/USDのBUY 1.0ロットとBUY 0.5ロットがあれば、1.5ロットのBUYポジションが1本という状態になる。PositionsTotal() で取得できるポジション数は通貨ペアの種類数と同数だ(各通貨ペアで最大1ポジション)。
ヘッジ口座 では、同一通貨ペアに複数のポジションが独立して存在できる。BUY 1.0ロットとBUY 0.5ロットは別々のチケット番号を持つ2本のポジションとして管理される。
この違いがコードに与える影響は小さくない。
// ヘッジ口座での安全なポジション選択
void ProcessHedgePositions(long magic)
{
int total = PositionsTotal();
for(int i = total - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
// ヘッジ口座では必ずチケットで選択する
if(!PositionSelectByTicket(ticket)) continue;
long pos_magic = PositionGetInteger(POSITION_MAGIC);
if(pos_magic != magic) continue;
double volume = PositionGetDouble(POSITION_VOLUME);
Print("チケット: ", ticket, " ロット: ", volume);
}
}
ヘッジ口座では PositionSelectByTicket(ticket) を一貫して使うのが安全設計の原則だ。MQL5リファレンスの記述によれば、PositionSelect() はネッティング口座でのみ確実に機能する設計になっており、ヘッジ口座での利用は推奨されていない。
証拠金チェック付きロットサイズ計算
MQL5の複数通貨EA環境でのロットサイズ計算には、固定比率法による逆算とブローカーの制限値チェック(SYMBOL_VOLUME_MIN / MAX / STEP)を必ず組み合わせることが本番品質の最低条件だ。
固定ロットで運用するシンプルなEAも多いが、口座残高に対して一定のリスク率でロットを決定する「固定比率法」は資金管理の観点から理にかなっている。考え方自体はシンプルで、「このトレードでストップロスにかかった場合の損失額」を事前に固定し、それが口座有効証拠金の risk_pct% を超えないようにロットを逆算するだけだ。
// 証拠金チェック付きロットサイズ計算(tick換算修正済み)
double CalcLotSize(double risk_pct, double sl_pips)
{
double account_equity = AccountInfoDouble(ACCOUNT_EQUITY);
double risk_amount = account_equity * risk_pct / 100.0;
// SYMBOL_TRADE_TICK_VALUEは1ティックあたりの価値
// pipあたり価値に変換するにはpoint/tick_sizeを乗じる
double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
double pip_value = tick_value * (point / tick_size);
double lot = risk_amount / (sl_pips * pip_value);
return NormalizeDouble(lot, 2);
}
SYMBOL_TRADE_TICK_VALUE の注意点: この定数は「1ティックサイズあたりの価値(1ロット基準、口座通貨建て)」を返す。pip単位で計算するには tick_value * (point / tick_size) でpip価値に変換する必要がある。JPY建て通貨ペア(USD/JPY等)やゴールド・CFDではtick_sizeとpointが一致しないケースがあり、この変換を省くと計算誤差が出る。私も最初はこれを省いて、JPY口座で想定外のロットを発注したことがある。
最小・最大ロット制限: NormalizeDouble(lot, 2) だけでは不十分だ。ブローカーによって最小ロット(SYMBOL_VOLUME_MIN)・最大ロット(SYMBOL_VOLUME_MAX)・ロットステップ(SYMBOL_VOLUME_STEP)の制限が異なる。本番運用では追加チェックを入れておきたい。
// ブローカー制限に準拠したロット正規化
double NormalizeLot(double raw_lot, string symbol)
{
double min_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double max_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
double lot_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
double lot = MathFloor(raw_lot / lot_step) * lot_step;
lot = MathMax(lot, min_lot);
lot = MathMin(lot, max_lot);
return lot;
}
証拠金チェックの追加: 発注前に AccountInfoDouble(ACCOUNT_FREEMARGIN) で空き証拠金を確認し、必要証拠金(OrderCalcMargin()で計算可能)を下回る場合は発注を見送る設計が安全だ。
注意: 上記のコードはデモ口座での事前検証を強く推奨する。証拠金計算ロジックはブローカーやシンボルの仕様に依存するため、本番運用前に必ず実動確認すること。
ポートフォリオレベルのドローダウン監視と緊急全決済
ポートフォリオレベルのDD監視は「ACCOUNT_BALANCE - ACCOUNT_EQUITY」の比率を閾値と比較し、超過時に全ポジションを逆順ループで決済する構造で実装する。 [内部リンク: MT5 ドローダウン管理 複数EA]
個々のEAがドローダウン管理を持っていても、ポートフォリオ全体としての含み損が想定外に膨らむリスクは別の話だ。相関の高い通貨ペアを扱う複数EAが同方向に損失を抱えるケースがその典型で、個別管理では捕捉できない。
そこで機能するのが、口座全体を監視するポートフォリオレベルのDD監視だ。
// ポートフォリオDD監視(含み損合計が閾値超えで全決済)
input double MaxPortfolioDD_Pct = 10.0; // 10%以上の含み損で全決済
void CheckPortfolioDDAndClose()
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double dd_pct = (balance - equity) / balance * 100.0;
if(dd_pct >= MaxPortfolioDD_Pct)
{
Print("ポートフォリオDD閾値超過: ", dd_pct, "% → 全ポジション決済");
CTrade trade;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
trade.PositionClose(ticket);
}
}
}
ACCOUNT_BALANCE(残高)とACCOUNT_EQUITY(有効証拠金)の差が含み損の合計だ。有効証拠金が残高を下回っているとき、その差分が現在の含み損になる。
設計上、気を付けるべき点がいくつかある。
閾値の設定について: MaxPortfolioDD_Pct = 10.0 はあくまで例示の値だ。適切な閾値はトレード戦略のリスクプロファイルによって変わる。バックテストで最大ドローダウンが20%程度の戦略なら、緊急停止閾値を30〜40%に置くのが一般的な考え方だが、結局は運用者が自身の資金管理方針に基づいて決める値だ。
全決済ロジックの改良点: 上記は全ポジションを決済する仕様だが、「このEAのポジションだけを全決済する」設計も使えるケースがある。前述のマジックナンバーフィルタリングと組み合わせれば、EA単位での緊急停止も実装できる。
呼び出し頻度: OnTick() 内で毎ティック呼び出す設計が多いが、OnTimer() で定期実行する方が処理負荷を抑えられる。EventSetTimer(1) で1秒ごとに監視するアプローチは、高頻度取引でなければ十分な反応速度だ。
注意: 全ポジション強制クローズは取り返しのつかない操作になる。本番稼働前にデモ口座で閾値の挙動を十分に確認すること。
CTrade.SetExpertMagicNumber()による発注時の自動設定
MQL5のCTradeクラスでは、OnInit()でSetExpertMagicNumber()を一度呼ぶだけで、以降の全発注にマジックナンバーが自動付与され、付与漏れを構造的に防止できる。
ここまでポジションの読み取りにおけるマジックナンバー活用を見てきたが、発注時に正しく付与する仕組みも同じくらい重要だ。
MQL5では CTrade クラスを使った発注が推奨されており、SetExpertMagicNumber() メソッドでマジックナンバーを一括設定できる。
#include <Trade\Trade.mqh>
input long MagicNumber = 12345;
CTrade g_trade; // グローバルスコープで定義
int OnInit()
{
// EAの初期化時に一度だけマジックナンバーを設定
g_trade.SetExpertMagicNumber(MagicNumber);
// スリッページ許容値の設定(3ポイント)
g_trade.SetDeviationInPoints(3);
// 注文タイプのポリシー設定
g_trade.SetTypeFilling(ORDER_FILLING_FOK);
return(INIT_SUCCEEDED);
}
void PlaceBuyOrder(double lot, double sl, double tp)
{
if(!g_trade.Buy(lot, _Symbol, 0, sl, tp, "Entry Signal"))
{
int error = GetLastError();
Print("発注エラー: ", error);
}
}
CTrade クラスの最大のメリットは、マジックナンバーの付与漏れを構造的に防止できることだ。OrderSend() を直接使う場合、MqlTradeRequest 構造体の magic フィールドへの代入を忘れるミスは意外と起きやすい。CTrade オブジェクトをグローバルスコープで定義し、OnInit() でマジックナンバーを一度セットしておけば、以降の全発注関数で自動的に付与される。
複数マジックナンバーを使い分けるなら、こういう設計も現実的だ。
// 複数マジックナンバーを使い分ける設計例
CTrade g_trade_trend; // トレンドフォロー用
CTrade g_trade_counter; // 逆張り用
int OnInit()
{
g_trade_trend.SetExpertMagicNumber(10001);
g_trade_counter.SetExpertMagicNumber(10002);
return(INIT_SUCCEEDED);
}
この設計では、ポジション監視時のフィルタリング条件として POSITION_MAGIC == 10001 か POSITION_MAGIC == 10002 かによって、どのロジックで開いたポジションかを区別できる。
よくある質問(FAQ)
マジックナンバーに0を設定するとどうなりますか?
マジックナンバー0は手動トレードや、SetExpertMagicNumber() を呼ばずに発注した場合のデフォルト値です。フィルタリング条件で POSITION_MAGIC == 0 とすると手動ポジションを誤って操作するリスクがあるため、EAには必ず1以上の固有値を設定することを推奨します。
PositionGetTicket(i)とPositionSelectByTicket()を両方使うのはなぜですか?
PositionGetTicket(i) はインデックスからチケット番号を取得するだけで、ポジションの内部キャッシュは更新されません。PositionSelectByTicket(ticket) を呼ぶことで初めて後続の PositionGetString() や PositionGetDouble() が正しい値を返す仕組みです。どちらか片方だけでは不十分な点に注意が必要です。
ヘッジ口座と通常口座でコードを共通化できますか?
可能ですが、条件分岐が必要です。AccountInfoInteger(ACCOUNT_MARGIN_MODE) の戻り値が ACCOUNT_MARGIN_MODE_RETAIL_HEDGING の場合はヘッジ口座と判定できます。ただし、設計の複雑さを避けるため、本番運用では口座タイプに特化したEAを別々に用意する方が安全です。
ポートフォリオDD(ドローダウン)の閾値は何%が適切ですか?
戦略のリスクプロファイルに依存するため一概に言えません。最低限の基準として、バックテストでの最大ドローダウンの1.5〜2倍程度を緊急停止閾値の出発点とする考え方があります。ただし、これはあくまで検討の入口であり、実運用前に十分なフォワードテストで検証することが必須です。
OnTick()内でPositionsTotal()を毎回呼ぶのはパフォーマンス上問題になりませんか?
MQL5の PositionsTotal() はO(1)のシステムコールであり、通常のEAでは問題になりません。ただし、ループ内で多数の PositionGetXxx() を呼び出す処理は、ポジション数が増えるにつれてコストが増加します。高頻度取引環境や多数ポジション管理が必要な場合は、チケットリストのキャッシュ戦略を検討してください。
CalcLotSizeでsl_pipsに0を渡すとゼロ除算になりませんか?
なります。本記事のサンプルコードは概念説明に特化した最小実装であり、sl_pips <= 0 の場合のガード処理は省略しています。本番コードでは if(sl_pips <= 0.0) return 0.0; のような入力値バリデーションを必ず追加してください。
MQL5で複数通貨EAのポジション管理を自動化・AI化するツールはありますか?
Claudeと会話しながらインジケータが作れるhedgrow-fxはこちら — MQL5のポジション管理ロジック設計やコードの自動生成・デバッグをAIと対話しながら進められる環境として活用できます。
まとめ
複数通貨EA並行稼働でのポジション管理は、以下の設計原則を守れば大半のトラブルを防止できる。
- マジックナンバー + 通貨ペアの2段階フィルタリングを常に実装する
- ヘッジ口座では
PositionSelectByTicket()一択。PositionSelect(symbol)に依存しない - CTrade.SetExpertMagicNumber() をOnInit()で初期化し、発注時のMN付与漏れを構造的に防ぐ
- ポートフォリオレベルのDD監視を口座全体を俯瞰する安全装置として追加する
- ロットサイズ計算には必ずブローカーの制限値チェックを加える
冒頭で述べた誤クローズトラブルは、マジックナンバーによるフィルタリングを追加することで解決した。たった数行の条件分岐が、複数EA稼働環境の安定性を左右する。地味な実装に見えるが、これを省くとある日突然ポジションが消えるという経験をすることになる。
Claudeと会話しながらインジケータが作れるhedgrow-fxはこちら — MQL5の複数通貨EA設計において、ポジション管理ロジックの構造設計からコード実装まで、AIと対話しながら効率的に進めることができる。
免責事項 本記事で紹介するコードおよび手法は教育目的のサンプルです。実際の運用においては、十分なバックテストと自己判断のもとで利用してください。FX取引には元本割れを含む損失リスクが伴います。
著者: 所属・資格:
