//+------------------------------------------------------------------+ //| Urban Flux Research — 研究アーカイブ公開版(無償) | //| https://apex-predator-x.com/ | //| | //| 公開日 : 2026-08-22 | //| 配布区分 : 研究アーカイブ / 検証用ソースコード | //| 対応 : MetaTrader 5 (MQL5) | //| ライセンス : 検証・学習目的での閲覧・改変・実行を許諾します。 | //| 再配布する場合は本ヘッダを削除しないでください。 | //| | //| ── 免責 ────────────────────────────────────────────────────── | //| 本コードは研究記録の公開であり、投資助言ではありません。 | //| 過去の検証結果は将来の利益を一切保証しません。 | //| 実口座での使用により生じたいかなる損失についても、 | //| 当方は責任を負いません。必ずデモ口座で検証してください。 | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| ✓ 本EAは時刻基準を修正した版です(週次 reopen アンカー方式) | //| 絶対時刻を使わずバー履歴から週次再開バーを検出するため、 | //| ブローカーのGMTオフセット・DST規則に依存しません。 | //| (TZ-2026-001 / RA-2026-002 の結論を反映) | //| | //| ⚠ ただし実測コスト控除後に生存する可能性があるのは USDJPY のみ | //| です。GBPJPY / EURJPY は再開直後のスプレッドが広く、 | //| 悲観コストで期待値が負になります。 | //| ⚠ 本EAは研究・検証用です。実運用判断には別途Gateを要します。 | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| TokyoMorningEA_v1.04.mq5 | //| Weekly-reopen anchored JPY carry (was: "Monday GMT01:00") | //| | //| ── なぜ v1.04 が必要か(TZ-2026-001 / RA-2026-002)── | //| v1.02/v1.03 は「時計の絶対時刻」でエントリーしていた。 | //| しかし研究データ(生M1 CSV)はブローカーサーバー時刻であり、 | //| そこで観測されたエッジの真の位置は | //| 「週次market reopen の次の1時間」 | //| であることが判明した(RA-2026-002: グロス +3.94pip t=4.71)。 | //| 一方 v1.02 がライブで参照する真UTC01:00 は別の時間帯であり、 | //| そこでは期待値が負である(T-0005)。 | //| | //| ── v1.04 の設計 ── | //| 絶対時刻を一切使わず、**バー履歴から週次再開バーを実行時に検出** | //| し、その次のH1バーで入り、さらに次のバーで出る。 | //| → ライブ・バックテスト・研究の3者が同一の市場イベントを指す。 | //| → ブローカーのGMTオフセット・DST規則に依存しない。 | //| | //| ── 適用上の注意(RA-2026-002 の結論)── | //| コスト後に生存する可能性があるのは USDJPY のみ。 | //| GBPJPY は再開直後スプレッドが広く(p50 3.2 / p95 6.8pip)、 | //| 悲観コストで期待値が負になる。EURJPY も同様に脆い。 | //| 本EAは研究・検証用であり、実運用判断は別途Gateを要する。 | //| | //| ⚠ 過去の検証結果は将来の利益を保証しない。 | //+------------------------------------------------------------------+ #property copyright "Urban Flux Research — RESEARCH-044" #property link "https://apex-predator-x.com/" #property version "1.04" #property description "Weekly-reopen anchored entry (timezone-independent)" #include #include #define MAX_ENTRY_RETRY 3 #define MAX_CLOSE_RETRY 10 #define TIMER_INTERVAL_MS 5000 #define WEEK_SCAN_BARS 400 // 週次再開探索の遡り上限(H1で約2.4週) #define WEEKEND_GAP_SEC (24*3600) // 週末ギャップ判定(研究と同一定義) input group "=== Lot Settings ===" input double InpLots = 0.01; // Fixed lot (used when InpRiskPct=0) input double InpRiskPct = 0.0; // Risk % of balance (0 = fixed lot) input group "=== Spread Settings ===" input double InpMaxSpreadPip = 3.5; // Max allowable spread (pips) — reopen+1h is still wide input bool InpSpreadRetry = true;// Wait for spread to improve within the entry bar input int InpRetryMaxMinute = 30; // Give up after this many minutes into the bar input group "=== Risk Management ===" input int InpSafetyStopPip = 50; // Emergency SL (pips); normal exit is bar-based input int InpMagicNumber = 44004; input bool InpPrintLog = false; //--- ★アンカーは固定。パラメータ化しない(事後最適化を構造的に禁じるため)★ // 研究 RA-2026-002 の定義: 再開バーの「次の」バーで入り、その次のバーで出る。 const int ENTRY_BAR_AFTER_REOPEN = 1; CTrade trade; CPositionInfo pos; bool g_entry_done = false; bool g_exit_done = false; bool g_spread_skip = false; datetime g_week_anchor = 0; // 今週の再開バー時刻 int g_entry_retry = 0; int g_close_retry = 0; datetime g_last_try = 0; ENUM_ORDER_TYPE_FILLING DetectFilling() { long mode = SymbolInfoInteger(Symbol(), SYMBOL_FILLING_MODE); if((mode & SYMBOL_FILLING_IOC) != 0) return ORDER_FILLING_IOC; if((mode & SYMBOL_FILLING_FOK) != 0) return ORDER_FILLING_FOK; return ORDER_FILLING_RETURN; } bool IsJpy() { return StringFind(Symbol(), "JPY") >= 0; } double PipSize() { return IsJpy() ? 0.01 : 0.0001; } double GetSpreadPip() { return (SymbolInfoDouble(Symbol(), SYMBOL_ASK) - SymbolInfoDouble(Symbol(), SYMBOL_BID)) / PipSize(); } //+------------------------------------------------------------------+ //| 週次再開バーの検出(本EAの中核・タイムゾーン非依存) | //| 戻り値: 現在バー(shift 0)が再開バーから数えて何本目か。 | //| 0 = 再開バー自身 / 1 = その次 / -1 = 判定不能 | //+------------------------------------------------------------------+ int BarsSinceWeeklyReopen() { int avail = Bars(Symbol(), PERIOD_H1); if(avail < 10) return -1; int limit = (int)MathMin(avail - 1, WEEK_SCAN_BARS); datetime newer = iTime(Symbol(), PERIOD_H1, 0); if(newer == 0) return -1; for(int i = 1; i <= limit; i++) { datetime older = iTime(Symbol(), PERIOD_H1, i); if(older == 0) return -1; if((long)(newer - older) > WEEKEND_GAP_SEC) return i - 1; // shift i-1 が再開バー → 現在バーはそこから i-1 本後 newer = older; } return -1; // 探索範囲内に週末ギャップなし } datetime WeeklyReopenTime() { int n = BarsSinceWeeklyReopen(); if(n < 0) return 0; return iTime(Symbol(), PERIOD_H1, n); } double CalcLots(double sl_pip) { double step = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP); double lmin = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); double lmax = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX); if(InpRiskPct <= 0.0) return MathMax(lmin, MathMin(lmax, MathFloor(InpLots / step) * step)); double risk = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPct / 100.0; double tv = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_VALUE); double ts = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE); if(ts <= 0.0 || tv <= 0.0) return MathMax(lmin, MathMin(lmax, InpLots)); double pv = tv * (PipSize() / ts); if(pv <= 0.0) return MathMax(lmin, MathMin(lmax, InpLots)); double lots = MathFloor((risk / (sl_pip * pv)) / step) * step; return MathMax(lmin, MathMin(lmax, lots)); } bool HasMyPosition() { for(int i = 0; i < PositionsTotal(); i++) if(pos.SelectByIndex(i) && pos.Magic() == InpMagicNumber && pos.Symbol() == Symbol()) return true; return false; } bool CloseMyPosition(string reason) { for(int i = 0; i < PositionsTotal(); i++) if(pos.SelectByIndex(i) && pos.Magic() == InpMagicNumber && pos.Symbol() == Symbol()) { bool ok = trade.PositionClose(pos.Ticket()); if(InpPrintLog) PrintFormat("v1.04 Close [%s] | %s | ret=%d | ok=%s", reason, Symbol(), trade.ResultRetcode(), ok ? "YES" : "NO"); return ok; } return false; } //--- 週の切り替わり検出(再開バー時刻が変わったら状態をリセット) void RefreshWeek() { datetime anchor = WeeklyReopenTime(); if(anchor != 0 && anchor != g_week_anchor) { g_week_anchor = anchor; g_entry_done = false; g_exit_done = false; g_spread_skip = false; g_entry_retry = 0; g_close_retry = 0; g_last_try = 0; if(InpPrintLog) PrintFormat("v1.04 New week | %s | reopen bar = %s", Symbol(), TimeToString(anchor, TIME_DATE | TIME_MINUTES)); } } void TryEntry() { int n = BarsSinceWeeklyReopen(); if(n != ENTRY_BAR_AFTER_REOPEN) return; // 再開バーの次のバーだけが対象 if(g_entry_done || g_spread_skip || HasMyPosition()) return; double sp = GetSpreadPip(); if(sp > InpMaxSpreadPip) { // このバーに入ってから何分経過したか(絶対時刻ではなくバー基準) long into_bar = (long)(TimeCurrent() - iTime(Symbol(), PERIOD_H1, 0)) / 60; if(!InpSpreadRetry || into_bar >= InpRetryMaxMinute) { g_spread_skip = true; g_entry_done = true; if(InpPrintLog) PrintFormat("v1.04 SKIP(spread) | %s | %.1f > %.1f pip | %d min into bar", Symbol(), sp, InpMaxSpreadPip, (int)into_bar); } return; } if(g_entry_retry >= MAX_ENTRY_RETRY) { g_entry_done = true; return; } if(TimeCurrent() - g_last_try < 1) return; g_last_try = TimeCurrent(); double sl_pip = InpSafetyStopPip; double lots = CalcLots(sl_pip); double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK); double sl = NormalizeDouble(ask - sl_pip * PipSize(), (int)SymbolInfoInteger(Symbol(), SYMBOL_DIGITS)); double need = 0.0; if(OrderCalcMargin(ORDER_TYPE_BUY, Symbol(), lots, ask, need) && need > AccountInfoDouble(ACCOUNT_MARGIN_FREE)) { g_entry_done = true; return; } if(trade.Buy(lots, Symbol(), ask, sl, 0.0, StringFormat("TME_REOPEN1_%s", TimeToString(g_week_anchor, TIME_DATE)))) { g_entry_done = true; if(InpPrintLog) PrintFormat("v1.04 Entry | %s | reopen=%s | bar#%d | ask=%.5f | spread=%.1f", Symbol(), TimeToString(g_week_anchor, TIME_DATE | TIME_MINUTES), n, ask, sp); } else g_entry_retry++; } void TryClose(string trigger) { if(g_exit_done || !HasMyPosition()) return; if(g_close_retry >= MAX_CLOSE_RETRY) return; if(CloseMyPosition(trigger)) g_exit_done = true; else g_close_retry++; } int OnInit() { if(InpRiskPct < 0.0 || InpRiskPct > 20.0) { Alert("v1.04: InpRiskPct must be 0..20"); return INIT_FAILED; } if(InpRetryMaxMinute < 0 || InpRetryMaxMinute > 59) { Alert("v1.04: InpRetryMaxMinute must be 0..59"); return INIT_FAILED; } if(Period() != PERIOD_H1) Print("v1.04 WARNING: designed for H1. Current timeframe differs; anchor detection uses H1 explicitly."); trade.SetExpertMagicNumber(InpMagicNumber); trade.SetDeviationInPoints(50); trade.SetTypeFilling(DetectFilling()); EventSetMillisecondTimer(TIMER_INTERVAL_MS); if(InpPrintLog) PrintFormat("v1.04 Init | %s | anchor = weekly reopen + %d H1 bar(s) | tester=%s", Symbol(), ENTRY_BAR_AFTER_REOPEN, (bool)MQLInfoInteger(MQL_TESTER) ? "YES" : "NO"); return INIT_SUCCEEDED; } void OnTick() { RefreshWeek(); TryEntry(); if(BarsSinceWeeklyReopen() > ENTRY_BAR_AFTER_REOPEN) // 保有バーを過ぎたら決済 TryClose("REOPEN_BAR_EXIT_TICK"); } void OnTimer() { RefreshWeek(); TryEntry(); if(BarsSinceWeeklyReopen() > ENTRY_BAR_AFTER_REOPEN) TryClose("REOPEN_BAR_EXIT_TIMER"); } void OnDeinit(const int reason) { EventKillTimer(); if(HasMyPosition()) PrintFormat("v1.04 WARNING: open position remains on shutdown | %s | reason=%d", Symbol(), reason); } //+------------------------------------------------------------------+