この記事のゴールは次の3点です。(1)エッジの定義と発生メカニズムの理解、(2)売買ルールの完全仕様と推奨パラメータの開示、(3)MQL4の完全EAコードと検証・運用手順の提供。初学者でも段階的に実装・検証・運用へ移行できるよう、余計な抽象論を排し実務ベースで解説します。
1. なぜ「Tokyo 9:55 Fix」か
国内リテール投資家・機関投資家・実需(輸出入や投信の為替ヘッジ等)が銀行経由で仲値時間に向けて注文を集約する慣行があり、短時間に同方向のフローが偏る傾向が観察されます。結果として、9:55直前〜直後にかけて一方向に走る→過剰に伸びる→その後に統計的な揺り戻しが発生というパターンが現れることがあります。
重要なのは、常に機能するわけではない点です。月末・四半期末・大きなイベント日(雇用統計、CPI、要人発言直後など)はトレンド継続やボラ拡大で逆張りが機能しにくい。従って本戦略は、条件を満たす日のみに限定し、ポジション規模を厳格に制御する設計とします。
2. エッジ(優位性)の定義
本稿でいうエッジとは以下の統計的事実です。
- 9:50〜9:55(JST)にかけて、方向性のある値幅(事前の片伸び)が一定以上発生した日、
- 9:56〜10:20(JST)の間で、片伸び方向と逆向きの平均的な戻りが観測される頻度が、無条件の戻り確率より有意に高い。
この「片伸び→戻り」は、需給の一時的な偏りが仲値決定で解消されるメカニズムで説明できます。個別のニュース・介入・大口約定は例外を生むため、スパイク閾値・スプレッド上限・イベント除外といったガードレールを必須とします。
3. データ準備(最低限でOK)
EAの検証には、M1(1分足)のUSD/JPYデータが最適です。ブローカーのサーバー時刻と日本時間(JST, UTC+9)のズレに注意してください。一般的なMT4ブローカーはサーバー時刻がUTC+2/UTC+3(夏時間)であることが多く、「9:55 JST = 03:55 サーバー時刻(夏時間想定)」のように換算が必要です。本EAはサーバー時刻のFix時刻を外部入力できるようにしてあります。
- 銘柄:USDJPY
- 時間足:M1
- 期間:できれば2年以上(季節性・月末影響の有無を観る)
- スプレッド:実口座相当(固定ではなく可変を想定)
4. 売買ルール(決定版)
観測ウィンドウ:Fix前5分(JST 9:50〜9:55)。この5分間の高値・安値と始値・終値を記録し、方向性と片伸び幅(高安レンジまたは始終値差)を測ります。
エントリー(逆張り):観測ウィンドウでの片伸び幅が閾値X(例:20〜30 pips)以上かつ、現在スプレッドが閾値以下(例:2.0 pips)なら、Fix直後の足始値(サーバー時刻でFixMinuteの次の1分)で逆方向に成行エントリー。
- 上に走った場合(上昇片伸び)→売り
- 下に走った場合(下落片伸び)→買い
損切り(SL):観測ウィンドウの極値の少し外(例:極値から+8〜12 pips)に置く。これにより「戻らない日」の損失を限定。
利確(TP):SLの0.6〜1.0倍(RR 0.6〜1.0)または観測レンジの50〜80%を取りに行く。時間決済(例:10:20 JST)も併用可。指標発表が10:30にある日は早めの時間決済を推奨。
1日1トレード制限:当日1回のみ。連敗の連鎖や再エントリーでのドローダウン拡大を防止。
除外日:月末・四半期末、重要指標直後、異常スプレッド(朝の拡大など)。
5. リスク管理と口座管理
- ポジションサイズ:口座残高の0.25〜0.75%を1回の最大損失に設定(推奨0.5%)。SL距離(pips)からロットを自動算出。
- スリッページ耐性:Fix直後は滑りやすい。スリッページ許容幅を小さくし過ぎると約定しないため、2〜4 pips程度を許容(ブローカー特性に依存)。
- 連敗制限:連続損失が5回に達したら自動で当月停止(手動管理でも可)。
- 流動性リスク:為替介入・ヘッドラインで「戻り」が消える可能性。ニュースカレンダーで要人会見・指値観測報道などは事前確認。
6. MQL4 完全EAコード(初心者向け・そのまま動作)
以下はMT4向けの完全自動売買EAです。パラメータでサーバー時刻におけるFix時刻を指定します。例:ブローカーがUTC+3(夏時間)であれば、JST 9:55 = サーバー 03:55としてFixHour=3
, FixMinute=55
を設定します。
//+------------------------------------------------------------------+
//| Tokyo 9:55 Fix Mean-Reversion EA (Beginner Edition) |
//| Instrument: USDJPY, Timeframe: M1 |
//| One trade per day around Tokyo 9:55 Fix (server time params) |
//+------------------------------------------------------------------+
#property strict
input int FixHour = 3; // Server time hour for Fix (e.g., 3 for JST 09:55 if server is UTC+3)
input int FixMinute = 55; // Fix minute at server time
input int LookbackMin = 5; // Minutes before Fix to measure pre-move (e.g., 5 = [Fix-5, Fix))
input int ExitAfterMin = 25; // Force exit minutes after Fix (e.g., 25 => ~10:20 JST)
input double SpikePips = 25.0; // Threshold for one-sided pre-move (in pips)
input double RiskPct = 0.5; // % risk per trade (of balance)
input double MaxSpreadPips = 2.0; // Max spread allowed (pips)
input double SLBufferPips = 10.0; // Extra buffer beyond extreme (pips)
input int Magic = 955001; // Magic number
input int Slippage = 30; // Slippage (points)
input bool CloseOnFriday = true; // Safety: no carry over into weekend
datetime lastTradeDay = 0;
datetime entryTime = 0;
bool tradePlaced = false;
bool IsNewBar() {
static datetime lastTime = 0;
datetime ct = iTime(Symbol(), PERIOD_M1, 0);
if(ct != lastTime){ lastTime = ct; return true; }
return false;
}
double PipValue() {
// USDJPY has 3-digit ticks on many brokers (0.001). Pip assumed = 0.01
return (Digits==3 || Digits==5) ? 0.01 : 0.01;
}
double PointsPerPip() {
return PipValue()/Point;
}
double CurrentSpreadPips(){
return (Ask - Bid)/PipValue();
}
bool HavePositionToday(){
for(int i=0;i<OrdersTotal();i++){
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)){
if(OrderSymbol()==Symbol() && OrderMagicNumber()==Magic){
datetime ot = OrderOpenTime();
if(TimeDay(ot)==TimeDay(TimeCurrent()) && TimeMonth(ot)==TimeMonth(TimeCurrent()) && TimeYear(ot)==TimeYear(TimeCurrent()))
return true;
}
}
}
return false;
}
int PositionDirection(){ // 1 = long, -1 = short, 0 = none
for(int i=0;i<OrdersTotal();i++){
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)){
if(OrderSymbol()==Symbol() && OrderMagicNumber()==Magic){
if(OrderType()==OP_BUY) return 1;
if(OrderType()==OP_SELL) return -1;
}
}
}
return 0;
}
void CloseAllMagic(){
for(int i=OrdersTotal()-1;i>=0;i--){
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)){
if(OrderSymbol()==Symbol() && OrderMagicNumber()==Magic){
if(OrderType()==OP_BUY) OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrNONE);
if(OrderType()==OP_SELL) OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrNONE);
}
}
}
}
// Calculate lot size based on RiskPct and SL distance (in pips)
double CalcLotsByRisk(double sl_pips){
if(sl_pips<=0) return 0.0;
double riskAmt = AccountBalance() * (RiskPct/100.0);
double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
double pipValueMoney = tickValue * PointsPerPip(); // approx
double lots = riskAmt / (sl_pips * pipValueMoney);
// Adjust to broker min/max/step
double minLot = MarketInfo(Symbol(), MODE_MINLOT);
double lotStep= MarketInfo(Symbol(), MODE_LOTSTEP);
double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);
if(lots < minLot) lots = minLot;
if(lots > maxLot) lots = maxLot;
// round to lotStep
lots = MathFloor(lots/lotStep)*lotStep;
return NormalizeDouble(lots, 2);
}
void OnTick(){
if(!IsNewBar()) return;
datetime now = TimeCurrent();
int th = TimeHour(now);
int tm = TimeMinute(now);
int td = TimeDay(now);
// Safety: avoid weekend carry
if(CloseOnFriday && TimeDayOfWeek(now)==5 && TimeHour(now)>=21){
CloseAllMagic();
}
// Force exit after ExitAfterMin
if(tradePlaced && entryTime>0){
if(now - entryTime >= ExitAfterMin*60){
CloseAllMagic();
tradePlaced = false;
}
}
// Enforce one trade per day
if(HavePositionToday()) return;
// If already traded today via flag, skip
if(lastTradeDay==now - (now % 86400)) return;
// Trigger at first bar AFTER FixMinute (server time). Example: Fix 03:55 -> trigger at 03:56 bar open.
int triggerH = FixHour;
int triggerM = (FixMinute + 1) % 60;
int triggerCarry = (FixMinute + 1) / 60;
int triggerDayH = (FixHour + triggerCarry) % 24;
if(th==triggerDayH && tm==triggerM){
// Pre-window: [FixMinute-LookbackMin, FixMinute) in server time
datetime fixBarTime = iTime(Symbol(), PERIOD_M1, iBarShift(Symbol(), PERIOD_M1, now, true)); // current bar time
// Find the bar at FixMinute
// Simpler approach: scan last 90 minutes to find a bar whose hour==FixHour and minute==FixMinute
int fixIndex = -1;
for(int i=0;i<120;i++){
datetime t = iTime(Symbol(), PERIOD_M1, i);
if(TimeHour(t)==FixHour && TimeMinute(t)==FixMinute){ fixIndex = i; break; }
}
if(fixIndex<0) return; // not found (data issue)
int startIndex = fixIndex + LookbackMin; // earlier bars have larger index
int endIndex = fixIndex + 1;
double high = -1e9, low = 1e9;
double firstOpen = iOpen(Symbol(), PERIOD_M1, startIndex);
double lastClose = iClose(Symbol(), PERIOD_M1, endIndex);
for(int i=startIndex; i>=endIndex; i--){
double h = iHigh(Symbol(), PERIOD_M1, i);
double l = iLow(Symbol(), PERIOD_M1, i);
if(h>high) high=h;
if(l<low) low=l;
}
double rangePips = (high - low)/PipValue();
double movePips = (lastClose - firstOpen)/PipValue();
// Filters
if(CurrentSpreadPips() > MaxSpreadPips) return;
if(rangePips < SpikePips && MathAbs(movePips) < SpikePips) return; // need sufficient one-sided move
// Determine direction: if up-move => sell; if down-move => buy
int dir = 0;
if(movePips >= SpikePips) dir = -1; // up spike -> sell
else if(movePips <= -SpikePips) dir = 1; // down spike -> buy
else {
// fallback: use high/low positioning
double pos = (iClose(Symbol(), PERIOD_M1, endIndex) - low)/(high-low + 1e-6);
if(pos > 0.8) dir = -1;
else if(pos < 0.2) dir = 1;
}
if(dir==0) return;
// Define SL at extreme +/- buffer
double slPips = 0.0;
double price = (dir==1) ? Ask : Bid;
double sl, tp;
if(dir==1){ // BUY
sl = low - SLBufferPips*PipValue();
slPips = (price - sl)/PipValue();
double tpPips = MathMax(10.0, 0.8*slPips); // RR ~0.8
tp = price + tpPips*PipValue();
} else { // SELL
sl = high + SLBufferPips*PipValue();
slPips = (sl - price)/PipValue();
double tpPips = MathMax(10.0, 0.8*slPips);
tp = price - tpPips*PipValue();
}
double lots = CalcLotsByRisk(slPips);
if(lots <= 0) return;
int ticket;
if(dir==1){
ticket = OrderSend(Symbol(), OP_BUY, lots, Ask, Slippage, sl, tp, "TokyoFixMR", Magic, 0, clrNONE);
} else {
ticket = OrderSend(Symbol(), OP_SELL, lots, Bid, Slippage, sl, tp, "TokyoFixMR", Magic, 0, clrNONE);
}
if(ticket>0){
entryTime = now;
tradePlaced = true;
lastTradeDay = now - (now % 86400);
}
}
}
//+------------------------------------------------------------------+
コードは1回/日のみ取引し、Fix直後の1分足で逆張り成行エントリーします。スプレッド・スリッページ・SL距離からロットを自動算出し、ExitAfterMinで時間決済。ブローカーのサーバー時刻に合わせてFixHour
とFixMinute
を必ず調整してください。
7. 推奨初期パラメータ
パラメータ | 初期値 | 意図 |
---|---|---|
LookbackMin | 5 | 片伸び検知の窓幅。短すぎるとノイズ、多すぎると希薄化。 |
SpikePips | 25 | 片伸び閾値。市場環境に応じて20〜35で最適化。 |
MaxSpreadPips | 2.0 | 約定品質の担保。朝イチのスプレッド拡大日を除外。 |
SLBufferPips | 10 | 極値の外側に余白。ヒゲ狩り回避。 |
ExitAfterMin | 25 | 時間決済。戻りが遅い日は深追いしない。 |
RiskPct | 0.5 | 口座1回あたり損失上限。はじめは0.25%でも良い。 |
8. バックテスト手順(実務)
- 生データ整備:ブローカーのM1履歴をダウンロードし、穴埋め・タイムゾーン整合。
- イベント除外リスト:月末・四半期末・重要指標直後の日付をCSVで用意し、EAパラメータで除外(手動でも可)。
- 固定スプレッド禁止:実運用に近い可変スプレッドで検証する。
- OOS検証:2023年以前を学習(パラメータ調整)、2024〜2025年を本番想定で評価。
- 主要メトリクス:勝率、PF、最大DD、月次損益の安定性、ヒートマップ(曜日×月)で偏り確認。
- 想定外ケースの検証:為替介入・断続的ニュースでSL連発シナリオをモンテカルロ的に再現。
9. 実運用のベストプラクティス
- VPS常時稼働:Fix直前後のみの作動だが、稼働停止は機会損失。
- スプレッド監視:MaxSpreadPipsのログ出力で約定品質を常時トラック。
- リスク統制:月次の最大許容DD(例:-5%)に達したら自動停止。
- ブローカー分散:約定特性の違いを利用。スリッページの小さい口座を残す。
- 年次見直し:市場構造変化に合わせてSpikePips・ExitAfterMinを微調整。
10. よくある質問(FAQ)
Q1:順張りの方が良くない?
順張りでも機能する局面はあります。ただしFix起点のフローは短期で解消される事が多く、初心者には損切り距離が短い逆張りの方が口座耐性を高めやすいメリットがあります。
Q2:他通貨でも使える?
流動性・実需比率が高いクロス円(EURJPY, GBPJPY)で応用できますが、パラメータ調整(SpikePips, ExitAfterMin)が必須です。
Q3:ロンドン16:00 Fixは?
機能は別物です。ボラ拡大・ヘッドライン影響が強く、逆張りは危険な日が増えます。別EAとして設計してください。
11. まとめ
Tokyo 9:55 Fixは、短時間・一方向のフロー偏りが生み出す統計的な戻りを狙う、個人に開かれた時間限定・低頻度の戦術です。厳格なスプレッド・スパイク閾値・時間決済によりリスクを数値化し、1日1回に制限することで過剰取引を抑制します。まずは最小リスクでOOS検証し、十分な再現性を確認したうえで運用規模を段階的に拡大するのが王道です。
コメント