要点:FXでは出来高データが限定的ですが、ティックボリューム(約定回数の近似指標)を使えば、価格帯ごとの出来高=ボリュームプロファイルを簡易的に再現できます。本稿では、ティックボリュームで近似したプロファイルから最頻値に当たるPOC(Point of Control)を算出し、「現在価格がPOCから一定以上乖離したら、POC方向へ戻る」ことを狙う逆張りEAを構築します。
設計思想、実務での落とし穴、検証プロセス、そしてMQL4フルコードまで、初心者でも実装可能な水準に落とし込みます。
1. 戦略の狙いと前提
あらゆる市場で「多くの取引が行われた価格」はフェアバリュー(市場参加者が合意しやすい価格帯)になりやすく、価格がそこから乖離した場合には回帰圧力が働くことが多い、という前提に立ちます。株式先物や暗号資産では板・出来高が豊富ですが、店頭FXでは完全な出来高は見えません。そこでティックボリューム(時間足内の価格更新回数)を代替指標にし、価格帯ヒストグラムを作ってPOCを求めます。
本稿の設計はデイトレ〜スキャル寄りです。時間足はM1〜M15が中心、セッション内回帰が主目的。順張りではなく逆張りで、乖離が大きいほど優位性が高まる前提でパラメータを最適化します。
2. 価格帯ボリュームの近似方法(FX向け簡易版)
理想は「各ティックに価格と出来高を紐づけて価格帯ヒストグラムに積み上げる」ことですが、MT4で扱う現実的手段は以下の簡易近似です。
- バケット幅(例:0.5〜2.0ピップ)を決める。
- 直近N本のローソク(例:M5×120本=過去10時間)を対象にする。
- 各バーの高値〜安値をバケット幅で細分化し、そのバーのティックボリュームを各バケットに等配(またはレンジ長で正規化して配分)。
- 全バーを通算して、各バケットの累積値を得る。
- 最も累積値が大きいバケットの中心価格をPOCとする。
等配は荒い近似ですが、実装負荷が低く、見合う精度が得られます。改良するなら「実体(Open/Close)に重み」「ヒゲは軽め」「レンジが広いバーの配分は薄く」などの重み付けも有効です。
3. 売買ロジックの骨子
3.1 エントリー
- 条件A:乖離… 現在のミッド価格((Bid+Ask)/2)がPOCからATR×k以上離れたら逆張り方向に仕掛ける(上方乖離→ショート、下方乖離→ロング)。
- 条件B:ボラティリティ制御… ATRが閾値未満(極端な低ボラ)や、スプレッドが閾値超(広がりすぎ)ならエントリーしない。
- 条件C:時間帯… 東京・ロンドン・NYのうち、自分が検証で優位性を確認した時間帯のみを許可(例:東京午前+ロンドン前半)。
3.2 エグジット
- Take Profit(TP):POC手前(例:POCまでの距離の80%)またはATR×t倍で利益確定。
- Stop Loss(SL):エントリー方向と逆にATR×s倍、もしくは当日のレンジ外に物理的なSL。
- トレーリング:含み益がATR×u倍に到達後、ブレイクイーブン+αへストップを切り上げ。
- 時間切れ:セッション終了時刻で強制クローズ(例:東京終了・NYオープン前の流動性谷間を避ける)。
4. パラメータ設計(推奨初期値)
項目 | 意味 | 推奨初期値 |
---|---|---|
BucketPips | 価格バケット幅 | 1.0 |
LookbackBars | ヒストグラムを作る過去バー数 | 240(M5なら約20時間) |
ATRPeriod | ATR計算期間 | 14 |
EntryK | POCからの乖離係数(×ATR) | 1.0〜1.8 |
SLmult | 損切り距離(×ATR) | 1.2〜1.8 |
TPmult | 利確目標(×ATR) | 0.8〜1.2 または POC基準 |
MaxSpreadPips | 許容スプレッド | 1.5〜2.0 |
SessionHours | 売買許可時刻 | 例:08:00–12:00 / 16:00–20:00 |
RiskPerTrade | 1トレード当たり口座リスク | 0.5%〜1.0% |
5. 実務面での留意点
- ブローカー選定:スプレッドと約定品質が全て。バックテストのスプレッド前提と大きく乖離すると期待値が崩れます。
- シンボル差:主要通貨(EURUSD, USDJPY, GBPUSD)とクロス通貨でプロファイルの安定度が違います。まずは主要通貨から。
- イベント回避:高インパクト指標(雇用統計・CPI・FOMCなど)直前直後はフィルタ推奨。スプレッド急拡大で敗因になりやすい。
- 連続乖離:トレンドが強い日はPOCが移動し続け、回帰が起きないこともある。時間切れクローズを必ず実装。
- リスク分散:通貨分散よりも「時間帯分散」「シグナル強度分散(乖離Zスコア)」が効きやすい。
6. 検証手順(MT4)
- データ準備:対象時間足(M1 or M5)のヒストリカルを十分に取得。欠損があるとPOC推定が歪みます。
- バックテスト:スプレッド固定に加え、可変スプレッドの近似環境でもテスト。広がり時のドローダウン形状を確認。
- パラメータ走査:BucketPips、EntryK、SessionHoursを粗→細の順で探索。過学習を避け、隣接点でも性能が維持される帯域を選定。
- ウォークフォワード:期間分割(例:2年最適化→6ヶ月検証)で堅牢性を担保。
- マルチシンボル:主要通貨で同一設定を当て、システマティックに比較。戦略の汎用性を確認。
7. ありがちな失敗と対策
- 過度な逆張り:小乖離で乱発→手数料負け。ATR基準で最小乖離を定義し、トレード頻度を管理。
- POCの計算負荷:過剰なバケット数&長すぎるLookbackでMT4が重い。M1で使うならLookbackを短縮。
- スプレッド無視:優位性の源泉が数ピップなのに、平均スプレッドが同等なら期待値はゼロ。MaxSpreadPipsを厳格化。
- イベント被弾:指標直撃は逆張りが最も弱い。30〜60分の売買停止窓を設ける。
8. 拡張アイデア
- 重み付き配分:バー実体に厚く、ヒゲに薄く。
- セッション別POC:東京・ロンドン・NYを別々に集計してPOCを切替。
- Zスコア型シグナル:POC乖離をATRだけでなく標準偏差でも規格化。
- POCクロス:POCを中心に往復のモメンタムを捉える順張り版も検証価値あり。
9. 使い方(導入〜運用)
- 以下のMQL4 EAコードを「MetaEditor」→「Experts」に保存し、MT4を再起動。
- 対象シンボルのチャート(M1〜M15)にEAを適用。パラメータは標準から開始。
- ストラテジーテスターで単体検証→フォワード検証→小ロットの実弾検証と段階的に移行。
10. MQL4フルコード(コンパイル可能なテンプレ実装)
POC算出と逆張りロジック、スプレッド・時間帯・ATRの基本フィルタ、リスク%ロット計算、TP/SL/時間切れクローズを含みます。最適化の叩き台としてお使いください。
//+------------------------------------------------------------------+
//| Tick-Volume POC Reversion EA (for MT4) |
//| Author: p-nuts |
//| Note : Educational template. Test carefully before live. |
//+------------------------------------------------------------------+
#property strict
input double BucketPips = 1.0; // 価格バケット幅(ピップ)
input int LookbackBars = 240; // POC集計の過去バー数
input int ATRPeriod = 14; // ATR期間
input double EntryK = 1.2; // エントリー閾値(ATR×k)
input double SLmult = 1.5; // 損切り距離(ATR×)
input double TPmult = 1.0; // 利確距離(ATR×) ※POC基準を使う場合は0に
input double MaxSpreadPips = 2.0; // 許容スプレッド(ピップ)
input double RiskPerTrade = 0.7; // 口座リスク%(0.7=0.7%)
input int StartHour = 8; // 許可開始(ローカル時)
input int EndHour = 20; // 許可終了(ローカル時)
input int Magic = 86420; // マジックナンバー
input int Slippage = 3; // 許容スリッページ(ポイント)
input bool OnePosition = true; // 1ポジ制御
double Pip() { return (MarketInfo(Symbol(), MODE_POINT) * (Digits == 3 || Digits == 5 ? 10 : 1)); }
double ToPoints(double pips) { return pips * Pip() / MarketInfo(Symbol(), MODE_POINT); }
bool TradingHourOk() {
datetime now = TimeLocal();
int h = TimeHour(now);
if(StartHour <= EndHour) return (h >= StartHour && h < EndHour);
// 跨ぎ対応(例:22-3など)
return (h >= StartHour || h < EndHour);
}
double CurrentSpreadPips() {
double spread_points = (MarketInfo(Symbol(), MODE_SPREAD));
return spread_points * MarketInfo(Symbol(), MODE_POINT) / Pip();
}
double ATR(int period) {
return iATR(Symbol(), Period(), period, 0);
}
// 価格をバケットにスナップ
double SnapPrice(double price, double bucketPips) {
double bucket = bucketPips * Pip();
double p = NormalizeDouble(price / (bucket*MarketInfo(Symbol(), MODE_POINT)), 0);
return p * bucket * MarketInfo(Symbol(), MODE_POINT);
}
// POC計算(等配近似)
bool CalcPOC(double &poc_price) {
int bars = MathMin(LookbackBars, iBars(Symbol(), Period())-1);
if(bars <= 10) return false;
double pt = MarketInfo(Symbol(), MODE_POINT);
double pip = Pip();
double bucket_step = bucketPipsToPoints(BucketPips);
// 動的配列:price->weight を辞書的に扱うため、マップ風の配列を2本持つ
double prices[]; ArrayResize(prices, 0);
double weights[]; ArrayResize(weights, 0);
for(int i=1; i<=bars; i++) {
double hi = iHigh(Symbol(), Period(), i);
double lo = iLow(Symbol(), Period(), i);
double tv = iVolume(Symbol(), Period(), i); // ティックボリューム
if(hi <= lo || tv <= 0) continue;
// 高値〜安値を等間隔で刻む
int buckets = (int)MathMax(1, MathFloor((hi - lo) / (bucket_step*pt)));
double step = (hi - lo) / buckets;
for(int b=0; b<=buckets; b++) {
double px = lo + step*b;
double snapped = SnapPrice(px, BucketPips);
// 既存の価格に加算
int idx = -1;
for(int k=0; k weights[max_i]) max_i = i;
poc_price = prices[max_i];
return true;
}
double bucketPipsToPoints(double pips) {
return ToPoints(pips);
}
int OrdersTotalByMagic(int magic) {
int cnt=0;
for(int i=0;i MaxSpreadPips) return(0);
double poc;
if(!CalcPOC(poc)) return(0);
double mid = (Bid + Ask) * 0.5;
double atr = ATR(ATRPeriod);
if(atr <= 0) return(0);
double dev = (mid - poc) / (atr); // 何ATR離れているか
int posCount = OrdersTotalByMagic(Magic);
if(OnePosition && posCount > 0) return(0);
// 方向判定(POCより上→ショート / 下→ロング)
if(MathAbs(dev) >= EntryK) {
// 損切りと利確距離(ピップ)
double sl_pips = SLmult * atr / Pip();
double tp_pips = (TPmult > 0 ? TPmult * atr / Pip() : MathAbs((mid - poc))/Pip()*0.8);
double lots = CalcLotByRisk(sl_pips);
if(lots <= 0) return(0);
int ticket;
if(mid > poc) {
// ショート
double sl = Ask + ToPoints(sl_pips)*MarketInfo(Symbol(), MODE_POINT);
double tp = Bid - ToPoints(tp_pips)*MarketInfo(Symbol(), MODE_POINT);
ticket = OrderSend(Symbol(), OP_SELL, lots, Bid, Slippage, sl, tp, "POC Short", Magic, 0, clrRed);
} else {
// ロング
double sl = Bid - ToPoints(sl_pips)*MarketInfo(Symbol(), MODE_POINT);
double tp = Ask + ToPoints(tp_pips)*MarketInfo(Symbol(), MODE_POINT);
ticket = OrderSend(Symbol(), OP_BUY, lots, Ask, Slippage, sl, tp, "POC Long", Magic, 0, clrBlue);
}
}
// 時間切れクローズ
datetime now = TimeLocal();
int h = TimeHour(now);
if(h == EndHour) {
for(int i=OrdersTotal()-1; i>=0; i--) if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) {
if(OrderMagicNumber()==Magic && OrderSymbol()==Symbol()) {
if(OrderType()==OP_BUY) OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrWhite);
if(OrderType()==OP_SELL) OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrWhite);
}
}
}
return(0);
}
//+------------------------------------------------------------------+
注:性能はブローカー仕様・レイテンシ・スプレッド・銘柄に依存します。必ず十分なバックテストとフォワード検証を行ってください。
11. まとめ
ティックボリュームから価格帯プロファイルを近似し、POC回帰を逆張りで捉える手法は、実装が容易で、拡張の余地も大きいのが強みです。シンプルなロジックでも、乖離の定量化(ATR)と実務フィルタ(時間・スプレッド)を組み合わせれば、初心者でも再現性のある検証が可能です。まずは1通貨・1時間帯で堅実に組み立て、勝てる帯域を見つけてから分散を広げてください。
コメント