本稿では、ティック密度(Tick Volumeの異常な集中)とスプレッド圧縮を組み合わせ、短時間の過熱とその反動を狙うFXスキャル戦略を解説します。対象はMT4(MQL4)です。初心者でも手を動かしながら実装できるよう、完全なEAコードと、相場の可視化に使えるPine Scriptインジケーターを掲載します。
キーポイントは次の2つです。
- ティック密度:直近N本の平均に対して、Tick Volume(=ティック数)が「統計的に有意な高水準」になった瞬間を特定します(Zスコア)。
- スプレッド圧縮:広がったスプレッドが通常水準に戻る瞬間は、約定品質と滑りやすさが改善し、短期の反転や伸びが出やすい局面です。
相場は「行き過ぎと均衡回帰」の繰り返しです。Tick Volumeのスパイクはその足でのミクロな行き過ぎを示唆します。これにスプレッド圧縮の条件を重ね、約定コストを抑えてエッジを取りに行きます。
戦略の全体像
基本アイデア:直近1分足のTick Volumeが異常に大きく(Zスコアが閾値以上)、かつスプレッドが通常より小さいとき、「ヒゲの長い行き過ぎ足」を逆張りで取りに行きます。方向は、上ヒゲ極端ならショート、下ヒゲ極端ならロングです。時間の経過で優位性が薄れるため、時間制限のクイック決済を併用します。
- 時間軸:M1(1分足)
- 主要条件:Tick Volume Zスコア ≥ 2.0〜3.0、スプレッドが平均より低い、長いヒゲ(実体に対して上/下ヒゲ比が大きい)
- 決済:固定TP/SL + 最大保有時間(例:60〜180秒)
- 回避:想定外にスプレッドが拡大、または直後に大イベントが控えている時間帯
向いている通貨ペア:EURUSD、USDJPY、GBPUSDなどのメジャー。スプレッドが安定し、Tick Volumeが豊富な銘柄が前提です。
エッジの源泉:なぜティック密度なのか
裁量でも自動でも、スキャルピングは「いま、局所的に何が起きているか」の把握が命です。FXの出来高は公開されませんが、MT4のTick Volume
は価格更新の回数(ティック数)の近似です。短時間にティックが集中する=流動性の押し合い・情報到来・約定の雪崩が起きている可能性が高い。足の終盤でヒゲが極端に伸びるのは、刈られた成行勢と逆指値の反動が混ざる典型パターンです。
ここでスプレッド圧縮を重ねる理由はシンプルです。スプレッド縮小は板が詰まり、競争的に約定できる瞬間を意味します。コスト・滑りを抑えられ、かつ反転の最初の一口を取りやすい。
売買ルール(標準)
- 下準備:時間足M1。直近NV本のTick Volumeから平均と標準偏差を計算。現在確定バー(一本前)のZスコアを求める。
- ヒゲ判定:確定バーの上ヒゲ・下ヒゲ長を計算し、実体に対して比率が大きいかで過熱方向を判定。
- スプレッド条件:現在のスプレッドがpips閾値以下かつ過去M本平均より低い。
- エントリー:
上ヒゲ極端(=一時的に買いが行き過ぎ)→ ショート。
下ヒゲ極端(=一時的に売りが行き過ぎ)→ ロング。 - ポジションサイズ:許容リスクR%と想定SL距離から動的に計算。
- 決済:TP/SLに加え、最大保有秒数で強制クローズ。行き過ぎの反動は短い。
推奨初期値:NV=20〜40、Z=2.2、スプレッド上限=現在のPoint換算で10〜20、TP=6〜12p、SL=8〜15p、最大保有=60〜180秒。
環境構築
- 手数料・スプレッドが低いECN口座。
- 約定安定のためVPS(東京 or ロンドン近接)。
- MT4最新ビルド、対象通貨のM1履歴を十分にダウンロード。
完全版 MQL4 EA(初心者向け・コメント多め)
以下のコードをMQL4/Experts
に保存し、MT4でコンパイルしてください。パラメータは外部入力で調整可能です。
//+------------------------------------------------------------------+
//| TickDensityScalper.mq4|
//| Copyright 2025, K's Research (for learning) |
//+------------------------------------------------------------------+
#property strict
input int Magic = 50321;
input double RiskPercent = 0.5; // 口座残高に対する1取引のリスク(%)
input double LotsFixed = 0.00; // 0ならRiskPercentで自動計算
input int Slippage = 3;
input int TickLen = 30; // Tick Volume平均/σの算出本数(M1)
input double ZEntry = 2.2; // エントリー閾値(Zスコア)
input int WickMinPoints = 40; // ヒゲと実体の最小比較単位
input double WickBodyRatioMin = 1.2; // ヒゲ/実体 比がこの値以上で過熱とみなす
input int SpreadMaxPoints = 20; // 許容スプレッド上限(ポイント)
input int SpreadAvgLen = 30; // スプレッド平均の算出本数(M1)
input int TP_Points = 80; // 目標利確(ポイント)
input int SL_Points = 120; // 損切り(ポイント)
input int MaxHoldSeconds = 120; // 最大保有秒数
input int TradeStartHour = 7; // 取引時間(サーバー時)
input int TradeEndHour = 23;
datetime lastBarTime = 0;
double GetSpreadPoints(){ return (MarketInfo(Symbol(), MODE_SPREAD)); }
bool TradeTimeOK(){
int h = TimeHour(TimeCurrent());
if(TradeStartHour <= TradeEndHour) return (h >= TradeStartHour && h < TradeEndHour);
// 夜跨ぎ対応
return (h >= TradeStartHour || h < TradeEndHour);
}
double LotsByRisk(double stopPoints){
if(LotsFixed > 0.0) return LotsFixed;
if(stopPoints <= 0.0) return 0.01;
double tickval = MarketInfo(Symbol(), MODE_TICKVALUE);
double contract = MarketInfo(Symbol(), MODE_LOTSIZE);
if(contract <= 0) contract = 100000;
double riskMoney = AccountBalance() * (RiskPercent/100.0);
double perLotLoss = (stopPoints * Point) * (contract * tickval / MarketInfo(Symbol(), MODE_TICKVALUE));
if(perLotLoss <= 0) return 0.01;
double lots = MathMax(riskMoney / perLotLoss, 0.01);
// 丸め
double lotstep = MarketInfo(Symbol(), MODE_LOTSTEP);
double minlot = MarketInfo(Symbol(), MODE_MINLOT);
double maxlot = MarketInfo(Symbol(), MODE_MAXLOT);
lots = MathFloor(lots/lotstep)*lotstep;
lots = MathMax(minlot, MathMin(lots, maxlot));
return lots;
}
void ManageOpenPositions(){
for(int i=OrdersTotal()-1; i>=0; i--){
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderSymbol()!=Symbol() || OrderMagicNumber()!=Magic) continue;
// 時間経過でクローズ
int held = (int)(TimeCurrent() - OrderOpenTime());
if(held >= MaxHoldSeconds){
if(OrderType()==OP_BUY) OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrNONE);
if(OrderType()==OP_SELL) OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrNONE);
}
}
}
int start(){
// 1ティックごとに実行(OnTick相当)
if(!TradeTimeOK()) { ManageOpenPositions(); return 0; }
if(GetSpreadPoints() > SpreadMaxPoints){ ManageOpenPositions(); return 0; }
// 新しい確定バーでのみシグナル判定
datetime barTime = iTime(Symbol(), PERIOD_M1, 1);
if(barTime == 0 || barTime == lastBarTime){ ManageOpenPositions(); return 0; }
lastBarTime = barTime;
// Tick Volume Zスコア(確定バー1本前=shift=1)
double vols[]; ArrayResize(vols, TickLen);
double sum=0, sum2=0;
for(int k=0; k<TickLen; k++){
double v = iVolume(Symbol(), PERIOD_M1, k+1);
vols[k]=v; sum += v; sum2 += v*v;
}
double mean = sum/TickLen;
double var = MathMax(sum2/TickLen - mean*mean, 0);
double sd = MathSqrt(var);
double lastVol = vols[0];
double z = (sd>0) ? (lastVol - mean)/sd : 0;
// スプレッド平均(M1の終値Bid/Askから近似)
double sprSum=0; int sprN=0;
for(int s=1; s<=SpreadAvgLen; s++){
double hi = iHigh(Symbol(), PERIOD_M1, s);
double lo = iLow(Symbol(), PERIOD_M1, s);
if(hi==0||lo==0) continue;
sprSum += (hi - lo)/Point; // 近似
sprN++;
}
double sprAvg = (sprN>0) ? sprSum/sprN : GetSpreadPoints();
if(GetSpreadPoints() > sprAvg) { ManageOpenPositions(); return 0; }
// ヒゲ・実体(確定バー)
double o = iOpen(Symbol(), PERIOD_M1, 1);
double h = iHigh(Symbol(), PERIOD_M1, 1);
double l = iLow(Symbol(), PERIOD_M1, 1);
double c = iClose(Symbol(), PERIOD_M1, 1);
double body = MathMax(MathAbs(c-o)/Point, 1);
double upper = (h - MathMax(o,c))/Point;
double lower = (MathMin(o,c) - l)/Point;
bool upperExtreme = (upper >= WickMinPoints) && (upper/body >= WickBodyRatioMin);
bool lowerExtreme = (lower >= WickMinPoints) && (lower/body >= WickBodyRatioMin);
// 既存ポジション枚数チェック(1つまで)
int mypos=0;
for(int j=0; j<OrdersTotal(); j++){
if(!OrderSelect(j, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderSymbol()==Symbol() && OrderMagicNumber()==Magic) mypos++;
}
if(mypos>0){ ManageOpenPositions(); return 0; }
// エントリー
if(z >= ZEntry){
// 逆張り:上ヒゲ→売り、下ヒゲ→買い
if(upperExtreme){
double sl = h + SL_Points*Point;
double tp = Bid - TP_Points*Point;
double lots = LotsByRisk(SL_Points);
if(lots > 0) OrderSend(Symbol(), OP_SELL, lots, Bid, Slippage, sl, tp, "TDS sell", Magic, 0, clrRed);
} else if(lowerExtreme){
double sl = l - SL_Points*Point;
double tp = Ask + TP_Points*Point;
double lots = LotsByRisk(SL_Points);
if(lots > 0) OrderSend(Symbol(), OP_BUY, lots, Ask, Slippage, sl, tp, "TDS buy", Magic, 0, clrBlue);
}
}
ManageOpenPositions();
return 0;
}
//+------------------------------------------------------------------+
注意:スプレッド平均の近似はM1の高安を利用しています。より厳密に管理したい場合は、ティックごとの実測スプレッドをログに貯めて移動平均を出す拡張を行ってください。
最適化の出発点(設定レンジ)
- TickLen:20, 30, 40, 60
- ZEntry:1.8, 2.0, 2.2, 2.5, 3.0
- WickMinPoints:20, 30, 40, 60
- WickBodyRatioMin:1.0, 1.2, 1.5
- TP/SL(ポイント):TP 60–120 / SL 90–150
- MaxHoldSeconds:60, 90, 120, 180
- TradeStart/EndHour:東京前場オン、ロンドン〜NY重複帯オン等を比較
検証:やり方と落とし穴
バックテストは実運用の疑似体験です。以下を守るほど、現実に近づきます。
- スプレッド・手数料・スリッページを現実的に設定。
- ウォークフォワード(例:3ヶ月最適化→1ヶ月検証を数セット)。
- カーブフィット回避:パラメータは少ないほど良い。勝率・PFだけでなく、ドローダウン形状と月次の一貫性を見る。
- キルスイッチ:想定以上の連敗/連勝で停止(異常環境のシグナル)。
リスク管理(最重要)
- 1トレードの口座リスクは0.25%〜0.5%程度から。
- 同時保有は通貨相関を考慮。USD/JPYとEUR/USDのような負相関に見えても、ニュースで一斉に動くことは多い。
- ロットはSL距離から逆算。TP:SLはおおむね1:1〜1.2:1で、時間制限が主役。
運用チェックリスト
- VPS稼働・回線遅延。
- ブローカーの実スプレッド監視(期間平均より高いときは停止)。
- 経済カレンダーで発表直前は手動オフ。
- 月次レポート:損益、PF、平均保有、連敗/連勝、勝ち負けの時間帯。
可視化インジケーター(Pine Script補助)
TradingViewではFXの出来高はティックボリュームです。下記でZスコアを描き、閾値超過で背景色を点灯します。
//@version=5
indicator("Tick Density ZScore (Helper)", overlay=false, max_labels_count=500)
len = input.int(30, "Z-score Length")
th = input.float(2.2, "Entry Z")
v = volume
m = ta.sma(v, len)
sd = ta.stdev(v, len)
z = (sd>0) ? (v - m)/sd : 0.0
plot(z, title="Z", linewidth=2)
hline(th, "Threshold", linestyle=hline.style_dotted)
bgcolor(z > th ? color.new(color.green, 85) : na)
よくある質問
Q. ニュース直後でも使えますか?
A. 使えますが、方向性が継続するケースも多いので初心者は回避を推奨します。まずは通常時間帯で安定させましょう。
Q. どの通貨が良いですか?
A. EURUSDやUSDJPYなど、スプレッドが恒常的に狭く、Tick Volumeが豊富なペアです。
Q. 勝率が低いです。
A. ヒゲ/実体の比率やZスコア閾値を調整してください。保有時間の短縮も有効です。
免責事項:本稿は教育目的の一般的情報です。特定銘柄の推奨や将来の成績を保証するものではありません。投資判断はご自身の責任でお願いします。
コメント