//+------------------------------------------------------------------+ //| FXMacroData_NewsFilter.mq4 | //| Pauses trading around scheduled economic releases. | //| Powered by the FXMacroData API, https://fxmacrodata.com | //+------------------------------------------------------------------+ // // WHAT IT DOES // Loads the FXMacroData release calendar for the currencies you // choose, keeps the releases that carry an exact official time, and // publishes a terminal-wide flag while the clock is inside a window // around each release: // FXMD_NEWS_BLOCKED = 1 inside a window, 0 otherwise // FXMD_NEXT_RELEASE = Unix time (UTC) of the active or next release // Any EA in the same terminal can read the flag with one call from // FXMacroData.mqh: // if(FMD_IsNewsBlocked()) return; // // This EA never opens trades. It blocks only EAs that check the flag. // With ClosePositionsBeforeRelease = true it also closes every open // position on the chart symbol CloseMinutesBefore minutes ahead of a // release, whichever EA or person opened it. Pending orders are left // in place. // // With ShowMacroPanel = true the panel also lists the policy rate, // inflation, unemployment and GDP growth of the chart symbol's two // currencies, refreshed with the calendar. MetaTrader allows WebRequest // only from Expert Advisors and scripts, which is why this is an EA. // // SETUP // 1. Copy FXMacroData.mqh -> \MQL4\Include\ // 2. Copy this file -> \MQL4\Experts\ // 3. Tools > Options > Expert Advisors: tick "Allow WebRequest for // listed URL" and add https://api.fxmacrodata.com // 4. Compile (F7), attach to a chart, enable AutoTrading. // 5. USD calendars work without an API key. Other currencies need one. // // All times are compared in UTC (TimeGMT), never in server time. // // DOCUMENTATION https://fxmacrodata.com/documentation/metatrader // GET AN API KEY https://fxmacrodata.com/subscribe //+------------------------------------------------------------------+ #property copyright "FXMacroData" #property link "https://fxmacrodata.com" #property version "1.00" #property description "Blocks trading around scheduled economic releases using the FXMacroData calendar." #property strict #include //--- Importance filter -------------------------------------------------- enum FXMD_IMPORTANCE { FXMD_IMPORTANCE_HIGH = 0, // High: tier 1 or top tier for the currency FXMD_IMPORTANCE_MEDIUM = 1, // Medium: tiers 1 and 2 FXMD_IMPORTANCE_ALL = 2 // All timed releases }; //--- Inputs ------------------------------------------------------------- input string Currencies = "USD"; // Calendars to watch (comma-separated) input string ApiKey = ""; // API key (blank: USD only) input int BlockMinutesBefore = 10; // Block starts N minutes before a release input int BlockMinutesAfter = 10; // Block ends N minutes after a release input FXMD_IMPORTANCE Importance = FXMD_IMPORTANCE_HIGH; // Releases to include input bool ClosePositionsBeforeRelease = false; // Close chart-symbol positions before a release input int CloseMinutesBefore = 2; // Close N minutes before a release input int RefreshMinutes = 60; // Calendar refresh interval (minutes, min 5) input bool ShowPanel = true; // Show the status panel on the chart input bool OnlyChartSymbolCurrencies = true; // Watch only the chart symbol's currencies input bool ShowMacroPanel = false; // Add policy rate, CPI, unemployment, GDP rows //--- Constants ---------------------------------------------------------- #define FXMDNF_PREFIX "FXMDNF_" #define FXMDNF_MIN_REFRESH_MIN 5 #define FXMDNF_CLOSE_RETRY_SEC 10 #define FXMDNF_SLIPPAGE_POINTS 30 //--- State -------------------------------------------------------------- struct FxmdRelease { datetime at; // scheduled release instant, UTC string currency; // "USD" string release; // indicator slug, e.g. "inflation" string name; // display name }; FxmdRelease g_releases[]; string g_watch[]; int g_block_before = 0; // seconds int g_block_after = 0; // seconds int g_close_before = 0; // seconds int g_refresh_sec = 0; datetime g_last_try = 0; // last refresh attempt, UTC datetime g_last_ok = 0; // last refresh that loaded at least one calendar, UTC bool g_last_failed = false; bool g_was_blocked = false; datetime g_last_close_try = 0; //--- Macro snapshot (ShowMacroPanel) ------------------------------------- struct FxmdMacroRow { string label; double value; }; FxmdMacroRow g_macro[]; string g_macro_base = ""; // lowercase, e.g. "eur" string g_macro_quote = ""; // lowercase, e.g. "usd" static const string MACRO_SLUGS[] = {"policy_rate", "inflation", "unemployment", "gdp"}; static const string MACRO_LABELS[] = {"Policy rate", "Inflation", "Unemployment", "GDP growth"}; //+------------------------------------------------------------------+ int OnInit() { g_block_before = (BlockMinutesBefore > 0 ? BlockMinutesBefore : 0) * 60; g_block_after = (BlockMinutesAfter > 0 ? BlockMinutesAfter : 0) * 60; g_close_before = (CloseMinutesBefore > 0 ? CloseMinutesBefore : 0) * 60; g_refresh_sec = (RefreshMinutes > FXMDNF_MIN_REFRESH_MIN ? RefreshMinutes : FXMDNF_MIN_REFRESH_MIN) * 60; ResolveCurrencies(); DetectMacroCurrencies(); if(MQLInfoInteger(MQL_TESTER)) Print("[FXMacroData] Strategy Tester: no live data here, the calendar and macro rows stay empty."); else { RefreshCalendar(); if(ShowMacroPanel) RefreshMacro(); } Evaluate(true); EventSetTimer(1); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); ObjectsDeleteAll(0, FXMDNF_PREFIX); ChartRedraw(0); GlobalVariableSet(FXMD_GV_BLOCKED, 0); GlobalVariableSet(FXMD_GV_NEXT_RELEASE, 0); } //+------------------------------------------------------------------+ void OnTimer() { if(!MQLInfoInteger(MQL_TESTER) && TimeGMT() - g_last_try >= g_refresh_sec) { RefreshCalendar(); if(ShowMacroPanel) RefreshMacro(); } Evaluate(true); } //+------------------------------------------------------------------+ void OnTick() { Evaluate(false); } //+------------------------------------------------------------------+ //| Build the list of calendars to watch. | //+------------------------------------------------------------------+ void ResolveCurrencies() { ArrayResize(g_watch, 0); string base = "", profit = ""; if(OnlyChartSymbolCurrencies) { base = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_BASE); profit = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT); StringToUpper(base); StringToUpper(profit); } string parts[]; int n = StringSplit(Currencies, ',', parts); for(int i = 0; i < n; i++) { string code = CleanCode(parts[i]); if(StringLen(code) != 3) continue; if(OnlyChartSymbolCurrencies && code != base && code != profit) continue; bool dup = false; for(int k = 0; k < ArraySize(g_watch); k++) if(g_watch[k] == code) dup = true; if(dup) continue; int size = ArraySize(g_watch); ArrayResize(g_watch, size + 1); g_watch[size] = code; } if(ArraySize(g_watch) == 0) { if(OnlyChartSymbolCurrencies) Print("[FXMacroData] Nothing to watch on ", _Symbol, " (", base, "/", profit, "). Currencies = \"", Currencies, "\". Add a matching code or set OnlyChartSymbolCurrencies = false."); else Print("[FXMacroData] Nothing to watch. Currencies = \"", Currencies, "\" has no three-letter code."); return; } string joined = ""; for(int i = 0; i < ArraySize(g_watch); i++) joined += (i > 0 ? ", " : "") + g_watch[i]; Print("[FXMacroData] Watching ", joined, " releases, importance: ", ImportanceLabel(), ", window ", g_block_before / 60, " min before to ", g_block_after / 60, " min after."); } //+------------------------------------------------------------------+ //| Upper-case a currency code and strip spaces and quotes. | //+------------------------------------------------------------------+ string CleanCode(const string raw) { string out = ""; int len = StringLen(raw); for(int i = 0; i < len; i++) { ushort c = StringGetCharacter(raw, i); if(c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '"' || c == '\'') continue; out += ShortToString(c); } StringToUpper(out); return out; } //+------------------------------------------------------------------+ string ImportanceLabel() { if(Importance == FXMD_IMPORTANCE_ALL) return "all"; if(Importance == FXMD_IMPORTANCE_MEDIUM) return "medium"; return "high"; } //+------------------------------------------------------------------+ bool PassesImportance(const int tier, const bool top_tier) { if(Importance == FXMD_IMPORTANCE_ALL) return true; if(top_tier) return true; if(Importance == FXMD_IMPORTANCE_HIGH) return tier == 1; return tier == 1 || tier == 2; } //+------------------------------------------------------------------+ //| Reload every watched calendar. A currency whose request fails | //| keeps the releases from its last good load. | //+------------------------------------------------------------------+ void RefreshCalendar() { datetime now = TimeGMT(); g_last_try = now; int watched = ArraySize(g_watch); if(watched == 0) { ArrayResize(g_releases, 0); return; } FxmdRelease fresh[]; int failures = 0; for(int c = 0; c < watched; c++) { string code = g_watch[c]; string lower = code; StringToLower(lower); string json = FMD_GetCalendar(lower, ApiKey); if(StringLen(json) == 0 || StringFind(json, "\"data\":") < 0) { failures++; int kept_old = 0; for(int i = 0; i < ArraySize(g_releases); i++) if(g_releases[i].currency == code) { AppendRelease(fresh, g_releases[i]); kept_old++; } Print("[FXMacroData] ", code, " calendar request failed. Keeping ", kept_old, " release(s) from the last good load."); continue; } int count = FMD_CalendarCount(json); int kept = 0; for(int i = 0; i < count; i++) { long epoch; string release, name; int tier; bool top_tier, timed, assumed; if(!FMD_CalendarRow(json, i, epoch, release, name, tier, top_tier, timed, assumed)) continue; if(!timed || assumed) continue; // date-only marker or estimated time if(!PassesImportance(tier, top_tier)) continue; if((datetime)epoch + g_block_after < now) continue; // window already over FxmdRelease r; r.at = (datetime)epoch; r.currency = code; r.release = release; r.name = name; AppendRelease(fresh, r); kept++; } Print("[FXMacroData] ", code, " calendar: kept ", kept, " of ", count, " row(s)."); } SortReleases(fresh); int total = ArraySize(fresh); ArrayResize(g_releases, total); for(int i = 0; i < total; i++) g_releases[i] = fresh[i]; g_last_failed = (failures > 0); if(failures < watched) g_last_ok = now; } //+------------------------------------------------------------------+ void AppendRelease(FxmdRelease &arr[], const FxmdRelease &r) { int size = ArraySize(arr); ArrayResize(arr, size + 1); arr[size] = r; } //+------------------------------------------------------------------+ //| Insertion sort by release time (lists are short). | //+------------------------------------------------------------------+ void SortReleases(FxmdRelease &arr[]) { int n = ArraySize(arr); for(int i = 1; i < n; i++) { FxmdRelease key = arr[i]; int j = i - 1; while(j >= 0 && arr[j].at > key.at) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } } //+------------------------------------------------------------------+ //| Work out the current state, publish it, act on it. | //+------------------------------------------------------------------+ void Evaluate(const bool draw) { datetime now = TimeGMT(); int next = -1; // first release whose window has not ended for(int i = 0; i < ArraySize(g_releases); i++) { if(g_releases[i].at + g_block_after > now) { next = i; break; } } bool blocked = (next >= 0 && now >= g_releases[next].at - g_block_before); datetime next_at = (next >= 0) ? g_releases[next].at : 0; GlobalVariableSet(FXMD_GV_BLOCKED, blocked ? 1.0 : 0.0); GlobalVariableSet(FXMD_GV_NEXT_RELEASE, (double)next_at); if(blocked && !g_was_blocked) Print("[FXMacroData] Blocking window started: ", Describe(g_releases[next])); else if(!blocked && g_was_blocked) Print("[FXMacroData] Blocking window ended. Trading may resume."); g_was_blocked = blocked; if(ClosePositionsBeforeRelease) MaybeClosePositions(now); if(draw) { if(ShowPanel) DrawPanel(now, next, blocked); else if(ObjectFind(0, FXMDNF_PREFIX + "HDR") >= 0) { ObjectsDeleteAll(0, FXMDNF_PREFIX); ChartRedraw(0); } } } //+------------------------------------------------------------------+ string Describe(const FxmdRelease &r) { return r.currency + " " + r.name + " at " + TimeToString(r.at, TIME_DATE | TIME_MINUTES) + " UTC"; } //+------------------------------------------------------------------+ //| Close every open position on the chart symbol when the clock is | //| inside [release - CloseMinutesBefore, release). Retries every | //| few seconds while positions remain. | //+------------------------------------------------------------------+ void MaybeClosePositions(const datetime now) { int idx = -1; for(int i = 0; i < ArraySize(g_releases); i++) { datetime at = g_releases[i].at; if(now >= at - g_close_before && now < at) { idx = i; break; } } if(idx < 0) return; if(now - g_last_close_try < FXMDNF_CLOSE_RETRY_SEC) return; g_last_close_try = now; CloseChartPositions(g_releases[idx]); } //+------------------------------------------------------------------+ //| Market orders only; pending orders are left in place. | //+------------------------------------------------------------------+ void CloseChartPositions(const FxmdRelease &r) { for(int i = OrdersTotal() - 1; i >= 0; i--) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; if(OrderSymbol() != Symbol()) continue; int type = OrderType(); if(type != OP_BUY && type != OP_SELL) continue; RefreshRates(); double price = (type == OP_BUY) ? MarketInfo(OrderSymbol(), MODE_BID) : MarketInfo(OrderSymbol(), MODE_ASK); int ticket = OrderTicket(); if(OrderClose(ticket, OrderLots(), price, FXMDNF_SLIPPAGE_POINTS, clrNONE)) Print("[FXMacroData] Closed order #", ticket, " on ", Symbol(), " ahead of ", Describe(r), "."); else Print("[FXMacroData] Could not close order #", ticket, ": error ", GetLastError(), "."); } } //+------------------------------------------------------------------+ //| Currencies for the macro rows: the chart symbol's base and | //| profit currency, falling back to the first six symbol letters. | //+------------------------------------------------------------------+ void DetectMacroCurrencies() { g_macro_base = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_BASE); g_macro_quote = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT); if(StringLen(g_macro_base) != 3) g_macro_base = StringSubstr(_Symbol, 0, 3); if(StringLen(g_macro_quote) != 3) g_macro_quote = StringSubstr(_Symbol, 3, 3); StringToLower(g_macro_base); StringToLower(g_macro_quote); } //+------------------------------------------------------------------+ //| Load the four macro indicators for both chart currencies. | //+------------------------------------------------------------------+ void RefreshMacro() { int n = ArraySize(MACRO_SLUGS); ArrayResize(g_macro, n * 2); string base_upper = g_macro_base; string quote_upper = g_macro_quote; StringToUpper(base_upper); StringToUpper(quote_upper); int idx = 0; for(int i = 0; i < n; i++) { g_macro[idx].label = base_upper + " " + MACRO_LABELS[i]; g_macro[idx].value = FMD_LastValue(FMD_GetIndicator(g_macro_base, MACRO_SLUGS[i], ApiKey)); idx++; g_macro[idx].label = quote_upper + " " + MACRO_LABELS[i]; g_macro[idx].value = FMD_LastValue(FMD_GetIndicator(g_macro_quote, MACRO_SLUGS[i], ApiKey)); idx++; } } //+------------------------------------------------------------------+ //| Macro rows under the news block. Returns the next free y. | //+------------------------------------------------------------------+ int DrawMacroRows(int y, const int x, const int line, const color head, const color text, const color muted) { y += 6; PutLabel("MHDR", "Macro snapshot", x, y, head, 9, true); y += line + 1; if(MQLInfoInteger(MQL_TESTER)) { PutLabel("M0", "No live data in the Strategy Tester", x, y, muted, 8, false); return y + line; } int rows = ArraySize(g_macro); if(rows == 0) { PutLabel("M0", "Loading...", x, y, muted, 8, false); return y + line; } for(int i = 0; i < rows; i++) { string val = (g_macro[i].value == EMPTY_VALUE) ? "n/a" : DoubleToString(g_macro[i].value, 2) + "%"; PutLabel("M" + IntegerToString(i), g_macro[i].label + ": " + val, x, y, text, 8, false); y += line - 2; } return y; } //+------------------------------------------------------------------+ //| Status panel, top-right corner, colours chosen for the chart's | //| background so it reads on light and dark themes. | //+------------------------------------------------------------------+ void DrawPanel(const datetime now, const int next, const bool blocked) { bool dark = ChartIsDark(); color text = dark ? C'226,232,240' : C'30,41,59'; color muted = dark ? C'148,163,184' : C'71,85,105'; color head = dark ? C'77,170,252' : C'0,90,158'; color good = dark ? C'52,211,153' : C'4,120,87'; color bad = dark ? C'248,113,113' : C'185,28,28'; int x = 12, y = 30, line = 17; PutLabel("HDR", "FXMacroData News Filter", x, y, head, 9, true); y += line + 1; if(next >= 0) { datetime at = g_releases[next].at; datetime start = at - g_block_before; datetime end = at + g_block_after; string status; if(blocked) status = "BLOCKED until " + TimeToString(end, TIME_MINUTES) + " UTC"; else status = "CLEAR, block starts " + TimeToString(start, TIME_DATE | TIME_MINUTES) + " UTC"; PutLabel("STATUS", status, x, y, blocked ? bad : good, 10, true); y += line + 3; PutLabel("NEXT", (blocked ? "Now: " : "Next: ") + g_releases[next].currency + " " + g_releases[next].name, x, y, text, 9, false); y += line; PutLabel("TIME", TimeToString(ToLocal(at), TIME_DATE | TIME_MINUTES) + " local / " + TimeToString(at, TIME_MINUTES) + " UTC", x, y, text, 9, false); y += line; string countdown = (now < at) ? "Release in " + FormatSpan((long)(at - now)) : "Released " + FormatSpan((long)(now - at)) + " ago"; PutLabel("CD", countdown, x, y, text, 9, false); y += line; } else { PutLabel("STATUS", "CLEAR", x, y, good, 10, true); y += line + 3; PutLabel("NEXT", ArraySize(g_watch) == 0 ? "No calendar selected" : "No timed releases loaded", x, y, text, 9, false); y += line; PutLabel("TIME", " ", x, y, text, 9, false); y += line; PutLabel("CD", " ", x, y, text, 9, false); y += line; } PutLabel("AGE", CalendarAgeText(now), x, y, muted, 8, false); y += line; if(ShowMacroPanel) DrawMacroRows(y, x, line, head, text, muted); ChartRedraw(0); } //+------------------------------------------------------------------+ string CalendarAgeText(const datetime now) { if(MQLInfoInteger(MQL_TESTER)) return "Calendar unavailable in the Strategy Tester"; if(g_last_ok == 0) return g_last_failed ? "Calendar not loaded, last request failed" : "Calendar not loaded"; string txt = "Calendar updated " + FormatSpan((long)(now - g_last_ok)) + " ago"; if(g_last_failed) txt += ", last request failed"; return txt; } //+------------------------------------------------------------------+ //| "2d 03h", "1h 05m 12s" or "05m 12s". | //+------------------------------------------------------------------+ string FormatSpan(const long seconds) { long s = seconds < 0 ? 0 : seconds; long d = s / 86400; long h = (s % 86400) / 3600; long m = (s % 3600) / 60; long sec = s % 60; if(d > 0) return IntegerToString(d) + "d " + Pad2(h) + "h"; if(h > 0) return IntegerToString(h) + "h " + Pad2(m) + "m " + Pad2(sec) + "s"; return Pad2(m) + "m " + Pad2(sec) + "s"; } //+------------------------------------------------------------------+ string Pad2(const long v) { return (v < 10 ? "0" : "") + IntegerToString(v); } //+------------------------------------------------------------------+ //| Shift a UTC instant to the computer's local clock. | //+------------------------------------------------------------------+ datetime ToLocal(const datetime utc) { return utc + (TimeLocal() - TimeGMT()); } //+------------------------------------------------------------------+ bool ChartIsDark() { long bg = ChartGetInteger(0, CHART_COLOR_BACKGROUND); int r = (int)(bg & 0xFF); int g = (int)((bg >> 8) & 0xFF); int b = (int)((bg >> 16) & 0xFF); return (r * 299 + g * 587 + b * 114) / 1000 < 128; } //+------------------------------------------------------------------+ void PutLabel(const string id, const string text, const int x, const int y, const color clr, const int size, const bool bold) { string name = FXMDNF_PREFIX + id; if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_RIGHT_UPPER); ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_RIGHT_UPPER); ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x); ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); ObjectSetString(0, name, OBJPROP_TEXT, text); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, size); ObjectSetString(0, name, OBJPROP_FONT, bold ? "Arial Bold" : "Arial"); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } //+------------------------------------------------------------------+